FVG-Delta Crypto Signal Engine — Agent / Maintainer Guide (v5.5)
This is the single source of truth for any AI agent or engineer taking over this
project. Read it fully before touching app.py. It explains the strategy, the
code workflow, every bug that was fixed in this revision, the current state, and
the rules for updating the app and its docs safely.
1. What the app is
A production-style FastAPI service that scans MEXC UST-M perpetual futures on
closed 15-minute candles, runs a strict Smart-Money-Concept (SMC) staged
state machine, draws annotated chart screenshots, sends Telegram alerts for the
late stages, and serves a live auto-updating dashboard with an in-memory Trade
History. It is hosted on Hugging Face Spaces (Docker SDK).
Single file does almost everything: app.py. Everything else is config/docs/entrypoints.
2. The strategy (must preserve)
The engine hunts the classic SMC continuation sequence. A setup walks forward
through five stages and is never allowed to skip or silently regress:
| Stage |
Meaning |
Where it shows |
| 0 |
Zone acquired: a valid impulse + FVG (fair-value gap) exists |
Internal only |
| 1 |
First retracement liquidity locked and a BoS close beyond the impulse wick extreme, with the FVG still clean (untouched) and liquidity untapped before the break |
Dashboard |
| 2 |
Price breaks and closes past the first liquidity (a real sweep, not a wick) — FVG still clean |
Dashboard |
| 3 |
Price interacts with (touches) the FVG zone |
Dashboard + Telegram |
| 4 |
An opposite-direction confirmation candle prints a clean reversal out of the FVG → verified entry with dynamic SL/TP |
Dashboard + Telegram |
Key rules baked into the code (do not weaken without explicit user request):
- Closed candles only.
bars[-1] (forming) is never used for decisions. The
most recent closed candle is bars[-2] (last_closed = len(bars) - 2).
- FVG must be clean for Stage 1 & 2. If any candle enters the FVG before the
liquidity sweep, the textbook order is broken → invalidate. Only Stage 3 & 4
involve FVG interaction.
- Close through the FVG against the setup = invalidation. This applies before
Stage 1 (waiting), during Stage 1–3, and specifically at Stage 3 a close back
through the FVG (a "Stage-3 invalidation") means the setup must never be
promoted to Stage 4.
- Only NEW opportunities. A confirmation whose SL/TP have already played out
in history is discarded (freshness gate) — it is not surfaced and not recorded.
- Lock-forward indices. Each transition's bar index is searched once and
stored on the
SetupState (bos_break_idx, stage2_break_idx,
fvg_interaction_idx, locked liquidity/BoS). Subsequent cycles only look for
the next transition, so stages don't flicker on transient empty scans.
Dynamic SL / TP (v5.5)
- SL sits a little past the FVG far edge (
z.low for LONG, z.high for
SHORT) plus SL_FVG_BUFFER_ATR. If a recent order-block / body swing sits just
beyond the FVG within OB_MAX_DISTANCE_ATR (default 1.0×ATR), the SL anchors to
that instead — but it is never placed far from the FVG. Floors
(MIN_STOP_ATR_MULT, MIN_STOP_PCT) stop a single candle from instant-stopping.
- Risk
R = max(structural, MIN_STOP_ATR_MULT·ATR, entry·MIN_STOP_PCT/100).
- TP2 = farther of
TP2_RR·R (default 3R) and the nearest resting liquidity /
opposing swing beyond entry (TP_SWING_LOOKBACK). target_source records which.
- TP1 = midpoint between entry and TP2 (50% of the target distance).
- Breakeven is a real outcome: once TP1 prints, the user moves SL to entry, so
if price returns to entry after TP1 the trade resolves as
Breakeven (TP1 then entry) (see _sequential_outcome).
3. Code workflow (how a scan cycle flows)
scanner_loop() # every SCAN_INTERVAL_SECONDS
└─ picks = shard rotation + every active (stage>=1) symbol
└─ process_symbol(symbol)
├─ load_bars() # MEXC kline (gzip/deflate; brotli installed)
├─ evaluate_stages(st) # THE state machine (see below)
├─ record_stage4_open() # when a FRESH stage 4 first appears
├─ _stage4_expired() -> close_stage4_trade() # SL/TP2/breakeven -> history
└─ maybe_send_stage_alerts() # Stage 3 & 4 Telegram (photo first, then text)
evaluate_stages(st) order of operations:
- Acquire a zone via
detect_latest_zone() if none (impulse + FVG quality gates).
- TTL / max-age expiry.
- Hard invalidation: close through FVG against the setup (stages 1–3).
_lock_first_liquidity() — locks first retracement liquidity + immutable BoS
level (impulse wick extreme) once.
- Stage 1 (BoS close), Stage 2 (liquidity sweep, with premature-FVG-touch
invalidation), Stage 3 (FVG interaction), Stage 4 (confirmation candle).
- Stage-3 close-through invalidation guard before any Stage-4 promotion.
- Freshness gate: discard already-played-out confirmations.
final_stage = desired_stage (sequential, monotonic via locked indices).
Charts: draw_chart() (live state) and draw_trade_chart() (closed-trade
snapshot from a stored history row). BoS is drawn as a horizontal line at the
impulse wick extreme from the anchor candle to the breaking candle, with a
diamond on the anchor and an arrow on the break candle. FVG box is anchored to the
gap candle (fvg_idx - 1).
4. What was broken and what this revision fixed
| Symptom reported |
Root cause |
Fix |
| Setups invalidated at Stage 3 were promoted to Stage 4, hit SL, and got recorded |
No guard for a close back through the FVG between interaction and confirmation; max() stage-lock force-jumped to 4 |
Added Stage-3 close-through invalidation guard; removed max() jump so stages are sequential from locked indices |
| No Stage-3 rows ever visible |
Confirmation in history made stage jump 0→4 in one cycle, skipping 3; also /api/state capped states[:320] so later symbols were hidden on live update |
Sequential staging keeps 3 visible until a clean confirm; /api/state now always includes every stage≥1 symbol |
| Already-played-out / already-hit-TP setups surfaced at Stage 1 / Stage 4 |
No freshness check |
Freshness gate: _levels_already_hit() → discard (no promote, no record) |
| Telegram never fired though tables populated |
Stage-4 that already expired reset to 0 before alert; Stage 3 skipped |
Sequential staging + freshness keep genuine live 3/4 states long enough to alert; alert order is photo then text |
| SL too far from price |
SL anchored to the candle before confirmation (could be distant) |
SL now FVG-edge anchored, only extends to a nearby OB within OB_MAX_DISTANCE_ATR |
| Breakeven not modeled |
Outcome used any() (order-blind) |
_sequential_outcome() walks candles in time order; TP1-then-entry = breakeven |
| BoS plotted near the liquidity line / drooping diagonal |
Diagonal from extreme to break close |
Horizontal BoS line at the wick extreme, anchor diamond + break arrow |
| Dashboard live update dropped/hid rows, snapped to empty |
/api/state 320-cap + heavy server-rendered duplication |
Full active-state payload; client patches rows in place from /api/state |
| MEXC requests intermittently failed |
Missing brotli decoder (Can not decode content-encoding: br) |
brotli pinned in requirements.txt; sessions send Accept-Encoding: gzip, deflate |
5. Current state
- Engine: rebuilt, lock-forward, sequential. Verified against live MEXC data:
zones detected, stages advance, fresh Stage-4 setups produce correct
FVG-anchored SL and 3R/liquidity TP2, and resolve to SL / TP2 / breakeven in
Trade History.
- Stage-4 is intentionally selective: at any single instant a brand-new
confirmation (entry candle = latest closed candle, nothing played out yet) is
rare, so a static snapshot may show few/none. Over a continuous live session the
engine accumulates them and writes Trade History on expiry.
- Trade History is in-memory only (
TRADE_HISTORY deque, session-scoped,
wiped on restart). No disk persistence — the HF Spaces FS is ephemeral/read-only.
- Dashboard redesigned: animated gradient/grid background, radar logo with sweep,
minimal header (logo, title, one-line description, tags, "Version 5.5 Active"),
compact metric cards, compact tables that fit to width on desktop and scroll on
mobile, in-place live refresh via
/api/state (no page reload).
6. Important code map
app.py
CFG (env config) ~line 48 ← all tunables; add new ones here
Candle / Zone / SetupState / Stats dataclasses
_sequential_outcome() ← order-aware SL/TP2/breakeven
_stage4_expired() ← terminal resolution (SL/TP2/BE)
record_stage4_open / close_stage4_trade ← Trade History lifecycle
detect_latest_zone() ← impulse + FVG quality gates
_lock_first_liquidity() ← locks liquidity + BoS extreme
build_trade_levels() ← dynamic SL/TP (FVG-anchored)
_levels_already_hit() ← freshness gate
evaluate_stages() ← THE state machine
draw_chart / draw_trade_chart() ← matplotlib overlays
maybe_send_stage_alerts() ← Telegram (stage 3 & 4 only)
process_symbol / scanner_loop() ← orchestration
api_state() ~line 1515 ← live JSON (sends ALL active states)
dashboard() ~line 1607 ← HTML/CSS/JS (f-string)
Other files: preflight_check.py, requirements.txt, start.sh,
run_one_shot.sh, README.md (HF metadata block is immutable),
STRATEGY_DETAILED_SPEC.txt, VARIABLES_AND_SECRETS_GUIDE.txt.
7. Rules for updating the app (do not break it)
- Never alter the README metadata block (first
--- … second ---). Hugging
Face uses it for hosting. Copy it byte-for-byte on any rewrite.
- The dashboard HTML is a Python f-string. Every literal
{ or } in CSS/JS
must be doubled ({{ / }}). After editing, run
python3 -c "import ast; ast.parse(open('app.py').read())" and grep the rendered
HTML for stray single braces / typos before shipping.
- Keep element IDs stable. The live JS targets
last-scan, m-symbols,
m-scanned, m-zones, m-stage1-count…m-stage4-count, m-invalidations,
c-stage1…c-stage4, c-trades, rows-stage1…rows-stage4, rows-trades,
rows-alerts, rows-events. Removing an ID is safe (JS guards with if(el)),
but renaming a still-referenced one breaks live update.
/api/state must keep returning every stage≥1 symbol. Do not re-introduce a
small slice cap on states or live rows will disappear again.
- Closed candles only. Use
bars[-2] / last_closed. Never decide on bars[-1].
- Don't add disk persistence for Trade History (HF FS is ephemeral/read-only).
- Preserve stage strictness: sequential, no skipping, FVG clean for 1&2,
close-through = invalidation, freshness gate for stage 4. If you must tune
sensitivity, prefer CFG env vars over hard-coding.
- Add new tunables to
CFG (with an env override and a sane default) and
document them in VARIABLES_AND_SECRETS_GUIDE.txt.
- Test before shipping:
python preflight_check.py, then a short live boot
(uvicorn app:app) and hit /health, /api/state, /, /api/chart.png.
8. Rules for updating the docs
Whenever you change behavior, update all of these in the same change so they
never drift:
AGENT.md (this file) — add the symptom→cause→fix row and any new rule.
STRATEGY_DETAILED_SPEC.txt — exact strategy/stage/level behavior.
VARIABLES_AND_SECRETS_GUIDE.txt — any new/changed env var, with example values.
README.md — user-facing summary (never touch the metadata block).
Write explanations with concrete examples (worked numbers / a sample symbol),
matching the style already in those files.
9. Deploy notes (Hugging Face Spaces)
- Docker SDK. Container runs
start.sh → preflight_check.py → uvicorn app:app
on $PORT (default 7860).
- Required secrets:
TELEGRAM_BOT_TOKEN, TELEGRAM_CHAT_ID. Without them the UI
still runs; Telegram alerts are skipped and logged.
- Tunables are all env vars (see the variables guide). Defaults are production-safe.
1---2name: 5312-delta-strategy3description: FVG-Delta Crypto Signal Engine — Agent / Maintainer Guide (v5.5)4---56# FVG-Delta Crypto Signal Engine — Agent / Maintainer Guide (v5.5)78This is the single source of truth for any AI agent or engineer taking over this9project. Read it fully before touching `app.py`. It explains the strategy, the10code workflow, every bug that was fixed in this revision, the current state, and11the rules for updating the app and its docs safely.1213---1415## 1. What the app is1617A production-style FastAPI service that scans **MEXC UST-M perpetual futures** on18**closed 15-minute candles**, runs a strict Smart-Money-Concept (SMC) staged19state machine, draws annotated chart screenshots, sends Telegram alerts for the20late stages, and serves a live auto-updating dashboard with an in-memory Trade21History. It is hosted on **Hugging Face Spaces (Docker SDK)**.2223Single file does almost everything: `app.py`. Everything else is config/docs/entrypoints.2425---2627## 2. The strategy (must preserve)2829The engine hunts the classic SMC continuation sequence. A setup walks forward30through five stages and is **never allowed to skip or silently regress**:3132| Stage | Meaning | Where it shows |33|------|---------|----------------|34| 0 | Zone acquired: a valid impulse + FVG (fair-value gap) exists | Internal only |35| 1 | First retracement liquidity locked **and** a BoS close beyond the impulse wick extreme, with the FVG still **clean** (untouched) and liquidity untapped before the break | Dashboard |36| 2 | Price breaks **and closes** past the first liquidity (a real sweep, not a wick) — FVG still clean | Dashboard |37| 3 | Price interacts with (touches) the FVG zone | Dashboard **+ Telegram** |38| 4 | An opposite-direction confirmation candle prints a clean reversal out of the FVG → verified entry with dynamic SL/TP | Dashboard **+ Telegram** |3940Key rules baked into the code (do not weaken without explicit user request):4142- **Closed candles only.** `bars[-1]` (forming) is never used for decisions. The43 most recent *closed* candle is `bars[-2]` (`last_closed = len(bars) - 2`).44- **FVG must be clean for Stage 1 & 2.** If any candle enters the FVG before the45 liquidity sweep, the textbook order is broken → invalidate. Only Stage 3 & 446 involve FVG interaction.47- **Close through the FVG against the setup = invalidation.** This applies before48 Stage 1 (waiting), during Stage 1–3, and specifically at Stage 3 a close back49 through the FVG (a "Stage-3 invalidation") means the setup must **never** be50 promoted to Stage 4.51- **Only NEW opportunities.** A confirmation whose SL/TP have already played out52 in history is discarded (freshness gate) — it is not surfaced and not recorded.53- **Lock-forward indices.** Each transition's bar index is searched once and54 stored on the `SetupState` (`bos_break_idx`, `stage2_break_idx`,55 `fvg_interaction_idx`, locked liquidity/BoS). Subsequent cycles only look for56 the next transition, so stages don't flicker on transient empty scans.5758### Dynamic SL / TP (v5.5)5960- **SL** sits a little **past the FVG far edge** (`z.low` for LONG, `z.high` for61 SHORT) plus `SL_FVG_BUFFER_ATR`. If a recent order-block / body swing sits just62 beyond the FVG within `OB_MAX_DISTANCE_ATR` (default 1.0×ATR), the SL anchors to63 that instead — but it is **never placed far from the FVG**. Floors64 (`MIN_STOP_ATR_MULT`, `MIN_STOP_PCT`) stop a single candle from instant-stopping.65- **Risk** `R = max(structural, MIN_STOP_ATR_MULT·ATR, entry·MIN_STOP_PCT/100)`.66- **TP2** = farther of `TP2_RR·R` (default 3R) and the nearest resting liquidity /67 opposing swing beyond entry (`TP_SWING_LOOKBACK`). `target_source` records which.68- **TP1** = midpoint between entry and TP2 (50% of the target distance).69- **Breakeven** is a real outcome: once TP1 prints, the user moves SL to entry, so70 if price returns to entry after TP1 the trade resolves as71 `Breakeven (TP1 then entry)` (see `_sequential_outcome`).7273---7475## 3. Code workflow (how a scan cycle flows)7677```78scanner_loop() # every SCAN_INTERVAL_SECONDS79 └─ picks = shard rotation + every active (stage>=1) symbol80 └─ process_symbol(symbol)81 ├─ load_bars() # MEXC kline (gzip/deflate; brotli installed)82 ├─ evaluate_stages(st) # THE state machine (see below)83 ├─ record_stage4_open() # when a FRESH stage 4 first appears84 ├─ _stage4_expired() -> close_stage4_trade() # SL/TP2/breakeven -> history85 └─ maybe_send_stage_alerts() # Stage 3 & 4 Telegram (photo first, then text)86```8788`evaluate_stages(st)` order of operations:891. Acquire a zone via `detect_latest_zone()` if none (impulse + FVG quality gates).902. TTL / max-age expiry.913. Hard invalidation: close through FVG against the setup (stages 1–3).924. `_lock_first_liquidity()` — locks first retracement liquidity + immutable BoS93 level (impulse wick extreme) once.945. Stage 1 (BoS close), Stage 2 (liquidity sweep, with premature-FVG-touch95 invalidation), Stage 3 (FVG interaction), Stage 4 (confirmation candle).966. Stage-3 close-through invalidation guard before any Stage-4 promotion.977. Freshness gate: discard already-played-out confirmations.988. `final_stage = desired_stage` (sequential, monotonic via locked indices).99100Charts: `draw_chart()` (live state) and `draw_trade_chart()` (closed-trade101snapshot from a stored history row). BoS is drawn as a **horizontal line at the102impulse wick extreme** from the anchor candle to the breaking candle, with a103diamond on the anchor and an arrow on the break candle. FVG box is anchored to the104gap candle (`fvg_idx - 1`).105106---107108## 4. What was broken and what this revision fixed109110| Symptom reported | Root cause | Fix |111|---|---|---|112| Setups invalidated at Stage 3 were promoted to Stage 4, hit SL, and got recorded | No guard for a close back through the FVG between interaction and confirmation; `max()` stage-lock force-jumped to 4 | Added Stage-3 close-through invalidation guard; removed `max()` jump so stages are sequential from locked indices |113| No Stage-3 rows ever visible | Confirmation in history made stage jump 0→4 in one cycle, skipping 3; also `/api/state` capped `states[:320]` so later symbols were hidden on live update | Sequential staging keeps 3 visible until a clean confirm; `/api/state` now always includes **every** stage≥1 symbol |114| Already-played-out / already-hit-TP setups surfaced at Stage 1 / Stage 4 | No freshness check | Freshness gate: `_levels_already_hit()` → discard (no promote, no record) |115| Telegram never fired though tables populated | Stage-4 that already expired reset to 0 before alert; Stage 3 skipped | Sequential staging + freshness keep genuine live 3/4 states long enough to alert; alert order is photo then text |116| SL too far from price | SL anchored to the candle before confirmation (could be distant) | SL now FVG-edge anchored, only extends to a nearby OB within `OB_MAX_DISTANCE_ATR` |117| Breakeven not modeled | Outcome used `any()` (order-blind) | `_sequential_outcome()` walks candles in time order; TP1-then-entry = breakeven |118| BoS plotted near the liquidity line / drooping diagonal | Diagonal from extreme to break close | Horizontal BoS line at the wick extreme, anchor diamond + break arrow |119| Dashboard live update dropped/hid rows, snapped to empty | `/api/state` 320-cap + heavy server-rendered duplication | Full active-state payload; client patches rows in place from `/api/state` |120| MEXC requests intermittently failed | Missing brotli decoder (`Can not decode content-encoding: br`) | `brotli` pinned in `requirements.txt`; sessions send `Accept-Encoding: gzip, deflate` |121122---123124## 5. Current state125126- Engine: rebuilt, lock-forward, sequential. Verified against live MEXC data:127 zones detected, stages advance, fresh Stage-4 setups produce correct128 FVG-anchored SL and 3R/liquidity TP2, and resolve to SL / TP2 / breakeven in129 Trade History.130- Stage-4 is intentionally selective: at any single instant a brand-new131 confirmation (entry candle = latest closed candle, nothing played out yet) is132 rare, so a static snapshot may show few/none. Over a continuous live session the133 engine accumulates them and writes Trade History on expiry.134- Trade History is **in-memory only** (`TRADE_HISTORY` deque, session-scoped,135 wiped on restart). No disk persistence — the HF Spaces FS is ephemeral/read-only.136- Dashboard redesigned: animated gradient/grid background, radar logo with sweep,137 minimal header (logo, title, one-line description, tags, "Version 5.5 Active"),138 compact metric cards, compact tables that fit to width on desktop and scroll on139 mobile, in-place live refresh via `/api/state` (no page reload).140141---142143## 6. Important code map144145```146app.py147 CFG (env config) ~line 48 ← all tunables; add new ones here148 Candle / Zone / SetupState / Stats dataclasses149 _sequential_outcome() ← order-aware SL/TP2/breakeven150 _stage4_expired() ← terminal resolution (SL/TP2/BE)151 record_stage4_open / close_stage4_trade ← Trade History lifecycle152 detect_latest_zone() ← impulse + FVG quality gates153 _lock_first_liquidity() ← locks liquidity + BoS extreme154 build_trade_levels() ← dynamic SL/TP (FVG-anchored)155 _levels_already_hit() ← freshness gate156 evaluate_stages() ← THE state machine157 draw_chart / draw_trade_chart() ← matplotlib overlays158 maybe_send_stage_alerts() ← Telegram (stage 3 & 4 only)159 process_symbol / scanner_loop() ← orchestration160 api_state() ~line 1515 ← live JSON (sends ALL active states)161 dashboard() ~line 1607 ← HTML/CSS/JS (f-string)162```163164Other files: `preflight_check.py`, `requirements.txt`, `start.sh`,165`run_one_shot.sh`, `README.md` (HF metadata block is immutable),166`STRATEGY_DETAILED_SPEC.txt`, `VARIABLES_AND_SECRETS_GUIDE.txt`.167168---169170## 7. Rules for updating the app (do not break it)1711721. **Never alter the README metadata block** (first `---` … second `---`). Hugging173 Face uses it for hosting. Copy it byte-for-byte on any rewrite.1742. **The dashboard HTML is a Python f-string.** Every literal `{` or `}` in CSS/JS175 must be doubled (`{{` / `}}`). After editing, run176 `python3 -c "import ast; ast.parse(open('app.py').read())"` and grep the rendered177 HTML for stray single braces / typos before shipping.1783. **Keep element IDs stable.** The live JS targets `last-scan`, `m-symbols`,179 `m-scanned`, `m-zones`, `m-stage1-count`…`m-stage4-count`, `m-invalidations`,180 `c-stage1`…`c-stage4`, `c-trades`, `rows-stage1`…`rows-stage4`, `rows-trades`,181 `rows-alerts`, `rows-events`. Removing an ID is safe (JS guards with `if(el)`),182 but renaming a still-referenced one breaks live update.1834. **`/api/state` must keep returning every stage≥1 symbol.** Do not re-introduce a184 small slice cap on `states` or live rows will disappear again.1855. **Closed candles only.** Use `bars[-2]` / `last_closed`. Never decide on `bars[-1]`.1866. **Don't add disk persistence** for Trade History (HF FS is ephemeral/read-only).1877. **Preserve stage strictness:** sequential, no skipping, FVG clean for 1&2,188 close-through = invalidation, freshness gate for stage 4. If you must tune189 sensitivity, prefer CFG env vars over hard-coding.1908. **Add new tunables to `CFG`** (with an env override and a sane default) and191 document them in `VARIABLES_AND_SECRETS_GUIDE.txt`.1929. **Test before shipping:** `python preflight_check.py`, then a short live boot193 (`uvicorn app:app`) and hit `/health`, `/api/state`, `/`, `/api/chart.png`.194195---196197## 8. Rules for updating the docs198199Whenever you change behavior, update **all** of these in the same change so they200never drift:201202- `AGENT.md` (this file) — add the symptom→cause→fix row and any new rule.203- `STRATEGY_DETAILED_SPEC.txt` — exact strategy/stage/level behavior.204- `VARIABLES_AND_SECRETS_GUIDE.txt` — any new/changed env var, with example values.205- `README.md` — user-facing summary (never touch the metadata block).206207Write explanations with concrete examples (worked numbers / a sample symbol),208matching the style already in those files.209210---211212## 9. Deploy notes (Hugging Face Spaces)213214- Docker SDK. Container runs `start.sh` → `preflight_check.py` → `uvicorn app:app`215 on `$PORT` (default 7860).216- Required secrets: `TELEGRAM_BOT_TOKEN`, `TELEGRAM_CHAT_ID`. Without them the UI217 still runs; Telegram alerts are skipped and logged.218- Tunables are all env vars (see the variables guide). Defaults are production-safe.