gr33njj.dev
case study · algorithmic trading system

TRAD3: turning a pretty reversal chart into code that doesn't lie in hindsight

Every trader loves drawing the same pattern on a historical chart: a strong impulse, a climax, a break of structure, a retest, a new trend. On the screenshot it looks obvious. The problem is it's all drawn after the reversal already happened — in real time the same pattern looks like noise from ten similar false breakouts, and only one of them is real.

27 instruments tracked
Weighted scoring, no repaint
FastAPI + Telegram + SQLite
TRAD3
trade.gr33njj.dev/login
TRAD3 login screen, owner-only access

Runs 24/7 against Binance's top crypto perpetuals plus gold and oil, and pushes every alert straight to Telegram. Panel access is Telegram Login-gated to the owner for now.

01Idea

Not phase 1 → phase 5, but a score

The first naive version was a rigid chain of states: impulse fades → climax → break of level → retest → confirmation. Pretty on paper; in practice half of real reversals never complete that chain — sometimes there's no clear climax, sometimes there's no retest because the trend already reversed without one (a classic V-shaped reversal).

So TRAD3 is built on a mandatory core plus weighted scoring, not a rigid sequence:

  • Mandatory: a confirmed break of structure (BOS) and confirmation after it — either a retest with a reversal candle, or an impulsive continuation without a retest.
  • Optional, by points: impulse fading before the break (+2), climax on volume/volatility (+2), a break with expanding volume (+1), RSI/OBV divergence (+1), a clear rejection on the retest (+2).

A signal only flies to Telegram once the score clears a threshold — and the message itself shows what that score is made of.

Not “the system said enter,” but “here are five components, three fired — judge for yourself.”
02No repainting

A pure function instead of a stateful machine

The key requirement across the whole system: swing points and levels must never move retroactively. A swing high only counts as confirmed once price has pulled back one and a half to two ATR from it — the system deliberately sees structure with a delay of a few bars. That's the price for a backtest that can't lie: what the system showed yesterday won't change today.

Technically this simplified the architecture more than expected: instead of a murky stateful automaton with persistent state, I ended up with a pure function that recomputes the entire phase picture from scratch on every tick, over a rolling window of ~400 closed bars. Since past swings are never repainted by definition, recomputing from zero gives the same result as an incremental update — without a whole class of bugs like “forgot to reset a counter after a restart.” The only thing that needs to persist between ticks is the fact that a given signal was already sent — a plain UNIQUE constraint in SQLite keyed on symbol, tf, ts_of_break, entry_type.

03Architecture

Two containers, one shared volume

detection pipeline
Binance (ccxt, REST) ──► detection.py (свинги, BOS, ретест, скоринг)
                              │
                              ├─► SQLite (WAL): состояния машин, лог сигналов, MAE/MFE
                              ├─► mplfinance: PNG-график с разметкой
                              ├─► Telegram-бот (aiogram): алерт с кнопками 👍/👎/🗑
                              └─► веб-панель (FastAPI): та же история, с графиками

Two Docker containers — the engine and the web panel — share one SQLite volume. The panel deliberately isn't on Next.js: it's a read-only showcase over the same database, it doesn't need a second Node process, FastAPI + Jinja2 is enough. Sign-in is via the Telegram Login Widget, access is currently limited to my own account.

I also factored out a source-agnostic Candle(symbol, tf, ts, o, h, l, c, volume, source) abstraction. Right now the only source is Binance, but once forex through MT5 happens (that physically needs a Windows terminal), the detection core won't care where a bar came from. The difference between “crypto now” and “forex later” is just the implementation of one Feed class, not a rewrite of the engine.

Signals
trade.gr33njj.dev/signals
Signal list and detection state machines
Signal #4
trade.gr33njj.dev/signals/4
Signal detail: chart, levels, score breakdown
Health
trade.gr33njj.dev/health
Feed health status
04Unexpected find

Binance doesn't only trade crypto

On the first run, tickers like XAU/USDT, CL/USDT, and SKHYNIX/USDT snuck into the top-25 by volume — gold, oil, a South Korean stock. Turns out Binance USDM Futures has long sold not just crypto perpetuals but “TRADIFI” perpetuals on commodities and equities, and by volume they comfortably compete with altcoins. Had to filter by the underlyingType == "COIN" field in market metadata.

A fun side effect: since gold and oil are there as genuine exchange-traded crypto perpetuals, I simply added XAU/USDT and CL/USDT straight to the watchlist — no Windows VPS or MT5 needed, those are only required for real forex pairs like EURUSD.

Telegram alerts

T3
TRAD3 by gr33njj
bot
XAU/USDT SHORT reversal signal, score 5/8ESPORTS/USDT SHORT reversal signal, score 7/8
05Honest status

What's actually proven, and what isn't

Battle-tested

  • Backfill across 27 instruments (25 crypto + gold + oil) runs on real history without a single error.
  • The full path from signal to alert — entry/stop/target calculation, chart rendering, Telegram delivery with buttons, saving to the panel — was run end to end by hand.

Not proven yet

  • Thresholds (breakout buffer in ATR, retest zone width, timeouts, scoring weights) are defaults from the initial architecture discussion, not the result of a backtest.
  • The system currently runs in pure paper mode: not a single real order, only alerts and my own thumbs-up/down labeling.

The honest next step is to run this same detection offline over a year or two of history and see what percentage of BOS turns out false, instead of trusting numbers picked “by feel” during a brainstorm. I deliberately chose to see the live system in action first and calibrate later, based on accumulated statistics — that's a conscious trade-off, not an oversight.

27
instruments tracked
2
Docker containers
5
scoring components
~400
bars in the rolling window
06What's next

Three directions

  • Offline calibration of thresholds against accumulated signal history.
  • Forex via MT5 — EURUSD/GBPUSD/USDJPY (gold and oil are already covered through Binance).
  • Maybe access for a couple more people — Telegram Login already verifies identity, it just needs a wider allow-list.
For now — watching the alert feed and learning whether to trust my own score.
next step

Need a similar alerting or trading system?

From market data to a Telegram alert with a chart — I'll design the pipeline for your instruments.