# HANDOFF — ZECUSDC Renko Line Bot (Binance USDⓈ-M)

Paste this whole file into any other AI. It is the complete state of the project.
Last updated: 2026-11-13.

---

## 0. What the user wants, in their words

- Trade **ZECUSDC** perpetual, **75x**, on **Binance USDⓈ-M futures** (USDC-margined).
- **Maker-only limit orders** because USDC-M maker fee is **0.0000%** (taker is 0.0400%).
  A limit order that crosses the spread is a **taker** — so orders must be **post-only (GTX)**.
- Run **24/7 demo/paper trading** independent of any AI chat, as long as the laptop is on.
- Then fund **$1–$2 real** and go live.
- Charts must look like **TradingView Renko**, with partial-opacity in-progress bricks,
  entry line, hourly close labels (**09:00**, not **08:59:59**), and the 4hr chart anchored
  at **01:00 local (UTC+1)**.
- **Percentage box size** by default.
- The bot must **refuse to trade and alert** if the maker fee is ever no longer 0.
- Needs a way to **follow Binance announcements** so a promo ending is caught early.
- Needs **full code export after every answer** so work continues elsewhere if credit runs out.

---

## 1. Run it

```bat
run_demo.bat
```

or

```powershell
python zec_binance_demo.py                 # paper, no keys, 24/7
python zec_binance_demo.py --live          # real maker-only orders (needs keys)
python zec_binance_demo.py --symbol ZECUSDT
```

Dashboard: **http://127.0.0.1:8770/dashboard.html**

Live mode environment:
```
BINANCE_API_KEY=...
BINANCE_API_SECRET=...
BINANCE_TESTNET=1        # optional, testnet.binancefuture.com
TELEGRAM_BOT_TOKEN=...   # optional alerts
TELEGRAM_CHAT_ID=...
NTFY_TOPIC=...           # optional, no bot needed
```

The bot runs as a normal local Python process. It does **not** need the AI chat,
and it does **not** stop when credit runs out. It stops when the laptop shuts down.

---

## 2. Files

| File | Purpose |
|---|---|
| `zec_binance_demo.py` | **The bot.** Binance feed → 3 Renko charts → line/gate/flip/hold/book. Paper + live. Serves the dashboard. |
| `dashboard.html` | Chart UI. Polls `demo_state.json` every 1s. |
| `demo_state.json` | Bot → dashboard state (bricks, line, position, cycles, trades). |
| `demo_events.csv` | Append-only event log. |
| `demo.log` | Console output of the running bot. |
| `run_demo.bat` | Double-click launcher. |
| `zec_renko_bot.py` | Original Lighter/Binance-USDT paper bot (reference implementation). |
| `backtest.py` | Replays real aggTrades through the same bot class. |
| `history.py` | Years of 1h candles → run-length and line-distance statistics. |
| `STRATEGY.md` | **The authoritative strategy document.** Rules §3, fee maths §4, results §5. |
| `vendor/lightweight-charts.js` | TradingView Lightweight Charts v4.1.0 (local, no CDN). |

---

## 3. The strategy (do not re-derive — every clause exists because a simpler version failed)

State: `line`, `side`, `gate`, `position`, `armed`, `carried`.

1. **Line.** Each closed 1h candle can print a 0.1% brick. The **start price of the newest
   brick is the line**; the brick's direction is `side`.
2. **Gate on.** The 0.1% tick chart must **touch** the line *and* close on the line's side.
3. **Gate stays on** while the newest brick still spans the line. Colour does not matter.
4. **Gate re-opens** if price leaves and touches again before a new 1h brick prints.
5. **Flip at the line.** A 0.001% brick whose range contains the line, in a direction
   different from the open position, closes and re-opens at the line.
6. **Arming.** A touching brick in the *same* direction arms the position with `side=-dir`.
7. **The one-brick loss.** An opposite brick printed **without touching** while armed closes
   the trade and follows the market. Only intentional loss; one 0.1% brick.
8. **Hold.** Gate off + position open ⇒ `carried = true`, chart 3 stops trading.
9. **Book.** When price returns to the **newest** line the carried trade closes and the
   cycle is booked (all trades since the last booking, fees included).
10. **Split.** Net > 0 ⇒ 40% stays, **60% to savings**. Net ≤ 0 ⇒ nothing moves.
11. **Liquidation** simulated at `1/leverage − maintenance`.

Boxes: hourly **0.1%**, gate **0.1%**, trade **0.001%**.

---

## 4. Fees — the whole game

Binance USDⓈ-M, USDC-margined, regular user (verified on the live fee page 2026-11-13):

| | Maker | Taker |
|---|---|---|
| USDC / USD1 perps | **0.0000%** | 0.0400% |

- A limit order that **rests** = maker = $0.
- A limit order that **crosses** = taker = 0.04%. **This is the trap.**
- Therefore every order uses `timeInForce="GTX"` (post-only). If rejected for crossing,
  the bot steps one tick away and retries.
- The promo has been extended repeatedly (2024-04, 2025-02, 2025-06, 2026-06 for USD1).
  It is **"until further notice"**, not permanent.

**Answer in one word: YES — $0 in fees**, provided every fill is a resting maker fill.
The bot enforces that with `GTX`. A cross = taker = 0.040% and is refused.

**Funding — ZECUSDC is NOT free.** Live numbers (2026-11-13):

| | Value |
|---|---|
| Funding interval | **every 4 hours** |
| Last funding rate | **0.005%** per interval |
| On $1 at 75x (notional $71.25) | **$0.00356 per 4h** = $0.0214/day |
| One 0.1% brick at 75x | $0.0713 |
| Funding vs one brick | ≈ **0.05 brick per 4h = 0.3 brick/day** |

So funding costs about **2.1% of capital per day** at 75x, or roughly **one third of one brick per
day**. It is small but real, and it is charged on notional whether you are in profit or not.
Being **flat at the settlement moment costs nothing.**

**Fee guard (implemented):** `Bot.check_fee()` calls
`futures_commission_rate(symbol)` and sets `fee_ok = (makerCommissionRate == 0)`.
If it is not 0: Telegram/ntfy alert, **new entries blocked**, existing position still
managed to the line. The dashboard header turns the `MAKER x%` tag red.

---

## 5. Where the profit is (measured, not claimed)

From `STRATEGY.md` §5, real Binance aggTrades 2026-11-06 → 2026-11-08, 4.8M ticks:

| 20x, $1 | 0% fee, chart-1 gate | 0% fee, band gate | 75x, 0.05% fee |
|---|---|---|---|
| cycles | 50 | 138 | 5 |
| net | **+2.72** | **+10.71** | **−0.93** |
| equity from $1 | $3.72 | $11.71 | $0.07 |
| max drawdown | 0.6% | 1.7% | 92.7% |

Conclusions:
1. **The gross edge is real** on real tick data.
2. **Fees decide everything.** At 0.05% the account dies in five cycles.
3. **Most trades are neutral** (367/403) — the line is a magnet. Profit comes from a few
   long runs (median run 11 bricks; 6.5% of runs are 50+).
4. **At 75x liquidation is ~1.33% away.** The line is closer than that, so the line should
   always exit first — but a gap, a wick, or a dead connection can liquidate.

**Honest answer: the edge is measured, but not yet proven at 75x with $1–$2.** That is
exactly what the 24/7 demo is for.

---

## 6. Charts

`dashboard.html` polls `demo_state.json` and renders three panes:

- **1HR CLOSE · LINE** — bricks from closed candles, labelled with the candle's **close hour
  in UTC+1** (`2026-11-13 02:00`). Never a `:59:59` timestamp.
- **4HR · 01:00 START** — Binance 4h candles are anchored 00:00 UTC = **01:00 local**, so
  bricks land on 01:00 / 05:00 / 09:00 / 13:00 / 17:00 / 21:00.
- **TICK · 0.001%** — live bricks, one per price tick move.

Renko look: two candlestick series (up/down) with `wickVisible:true`,
partial-opacity fills (`#2fbe86` + alpha, default 50%), solid borders.
Overlays: dashed gold **LINE**, dotted **ENTRY** line coloured by whether the position is
in profit.

Settings panel (⚙): box %, up/down colours, brick opacity, wicks, grid, price scale
(normal/log/percent), entry line, line, timezone offset.

**Limitation, stated plainly:** Lightweight Charts is a lightweight library, not the full
TradingView charting engine. It cannot expose every TradingView setting. The settings above
are the ones that affect this strategy. If literally-every-TradingView-setting is required,
the only way is TradingView's own widget — which cannot show our custom line/brick logic.

---

## 7. Percentage vs fixed box

The strategy is **already** percentage-based (0.1% of price). "Percentage box size" is not a
change — it is what the code does. Switching to a fixed-dollar box **would** change the
strategy: brick count per day would drift as ZEC's price moves, and the line would stop being
a constant 0.1% level.

---

## 8. Testnet answer

- Binance Futures Testnet: **https://testnet.binancefuture.com** (set `BINANCE_TESTNET=1`).
- It is a **USDⓈ-M** demo environment with fake USDT. USDC pairs and ZECUSDC coverage there
  is **not guaranteed**.
- Practical split: **paper trades on real live ZECUSDC data** (this bot, no keys) for the
  24/7 run, and **testnet on ZECUSDT** only to rehearse order plumbing (`GTX`, reduce-only,
  cancel). Same code path, different symbol.

---

## 9. Following the promo deadline (so it is never a surprise)

1. **Fee guard in the bot** — reads the real commission rate; blocks entries and alerts the
   moment maker ≠ 0. This is the hard safety net.
2. **Binance announcement feed** — poll
   `https://www.binance.com/bapi/composite/v1/public/cms/article/list/query?type=1&pageNo=1&pageSize=20&catalogId=48`
   (Futures announcements) every 15 min and alert on any title matching
   `fee|USDC|promotion|end`.
3. **Fee page diff** — `https://www.binance.com/en/fee/futureFee`, watch the USDC row.
4. Telegram channel **@binance_announcements** for the same notices.

Any of 2–4 is a nice-to-have; **1 is the one that protects the money.**

---

## 8a. PnL table spec (the user's requirement — do not change it back)

The dashboard PnL table is **hourly bricks, newest hour first, into the past**:

```
hour (UTC+1)          bricks   usd        cum
2026-11-13 02:00        3     +0.2137    0.2137   <- current hour
2026-11-13 01:00        6     +0.4275    0.6412
2026-11-12 21:00        0     +0.0000    2.1375
```

- `bricks` = number of **1hr Renko bricks printed in that hour**.
- `usd` = `bricks × brick_usd` where
  `brick_usd = capital × leverage × 0.95 × 0.001` (= **$0.0712** on $1 at 75x).
- `cum` = running total of `usd` from the newest hour backwards.
- Hours with zero bricks are **shown as zero**, never skipped.
- 48 rows are emitted (`Bot.hourly_table()`), so the table covers two days back.
- The one-tick (0.001%) losses are deliberately not in this table — they are 1/100 of a
  0.1% brick and the user considers them noise. They stay in `demo_events.csv`.

This is the "1hr brick is the profit" rule: one brick = one unit of profit at max leverage.

## 9a. What is automated vs manual

| Layer | Automated? | Where |
|---|---|---|
| Fee guard (maker != 0 ⇒ block entries + alert) | **Yes** — startup and every 15 min | `fee_watch()` |
| Binance announcement watcher (fee/USDC/promo/end) | **Yes** — every 15 min | `news_watch()` |
| Fee-page diff | **No** — manual eyeball | — |
| Telegram/ntfy push | Yes, if `TELEGRAM_*`/`NTFY_TOPIC` set | `alert()` |

Nothing needs doing by hand for the fee safety net. Only the phone alert needs two
environment variables.

## 9b. Fee-up rule (implemented) and the Windows write trap

**Rule:** if the maker fee is ever **> 0**, the bot does not just stop opening — it
**flattens every open position immediately** (`Bot.flatten_all()`), books the cycle, and
blocks all further entries. Triggered at startup, every 15 minutes, and on any manual
`check_fee()` call.

**Windows trap (fixed, do not undo):** `os.replace(tmp, STATE_FILE)` raises
`PermissionError` while the dashboard has `demo_state.json` open for a 1-second poll. That
unhandled exception **killed the bot in `seed()`**. `write_state()` now retries the rename
6× at 50 ms, then falls back to a plain rewrite, and never raises. If you change that
function, keep the retry.

**GTX get-in / get-out:** entry and exit both use `timeInForce="GTX"` (post-only),
exit with `reduceOnly=True`. If an order would cross, the bot re-prices one tick away and
retries up to 3 times. **GTX guarantees $0 fees; it does not guarantee a fill.** If price
runs through the line, the exit will not fill and the position stays open until price
returns. That is the real cost of maker-only, and it must not be hidden.

## 10. Open tasks (in priority order)

1. **Run the 24/7 demo** and let it print real bricks for a full day. Read `demo_events.csv`.
2. **Re-run `backtest.py` at 75x with zero fees** (`--capital 100`, add `--leverage`).
   Nobody has yet seen the 75x/0-fee number.
3. **Reconcile on boot** — live mode must read the open position from the exchange instead
   of assuming flat.
4. **Fee-poll loop** — call `check_fee()` every 15 min inside the running bot (currently only
   at startup).
5. **Box sweep** — 0.001% vs 0.003% vs 0.005% on the intraday chart, scored on net per cycle.
6. **Funding** — USDC-M funding is charged every 4–8h on notional; not modelled yet.
7. **Go live with $1–$2** only after (1) and (2) look sane.

---

## 11. Known limits

- No funding, latency, or order-book depth in the paper model.
- `FILL="line"` is best case; live maker fills can miss entirely (price runs away).
- Liquidation is approximate.
- 1hr Renko grids depend on the start point, so bricks will not match TradingView
  brick-for-brick. The line is a level, not a picture.
- One cycle is typically 5–6 bricks, so a day is a small sample.
- The edge is public information and will decay.

---

## 11a. Fixes proven by testing (do not regress these)

1. **Candle close times are `xx:59:59.999`.** Labelling a brick by flooring the close time
gave 16:00 / 15:59:59. Fix: brick label = `(close_ms + 1 − interval_hours·3600000)` then
TZ, i.e. label by the bar's OPEN. 1h bars now read 02:00, 4h bars read 01:00 / 05:00 /
09:00 / 13:00 / 17:00 / 21:00.
2. **Multiple bricks per candle share one timestamp.** Lightweight Charts drops duplicate
times, which collapsed the axis and crammed all bricks at the left edge. Fix: brick time =
`close_ms + index·1000` (one second per brick). Verified: 400 bricks → 400 unique seconds,
strictly ascending.
3. **Both series drew green.** Every brick is built `open=low / close=high`, so a candle is
always "bullish" and every series used its `upColor`. Fix: each series gets ONE solid
colour (up series = up colour, down series = down colour).
4. **Do not reset the zoom every poll.** The visible range is set once per pane, then the
user's scroll/zoom is left alone.

## 12. For the next AI

- Do **not** re-derive the strategy. Read `STRATEGY.md` §3 and this file §3.
- Keep the fee ratio in front of you: `fee per order ÷ profit per brick`. At 0.04% taker and
  75x that ratio is ~0.95 — fatal. At 0% maker it is 0 — the only reason this works.
- Never quote a percentage without USD.
- Never call a floating gain a profit. Profit exists only after the held trade closes at the
  newest line, net of fees.
- The user values speed and straight answers, and dislikes optimism that is not backed by a
  number or a log line.
