diff --git a/docs/plans/2026-06-04-meet-kevin-trade-execution-fix.md b/docs/plans/2026-06-04-meet-kevin-trade-execution-fix.md new file mode 100644 index 0000000..42258ea --- /dev/null +++ b/docs/plans/2026-06-04-meet-kevin-trade-execution-fix.md @@ -0,0 +1,80 @@ +# Meet Kevin — trade-execution fix (go-live) + +**Date:** 2026-06-04 +**Status:** shipped (Phases 1–4 merged) → deploying + +## Problem + +Phase 2 "paper trading" was enabled (`TRADING_KEVIN_ENABLE_TRADING=true`) but +was a **silent no-op**: 89 Kevin signals reached the executor and every one was +dropped at sizing. The Alpaca paper account had **zero** Kevin orders (all 50 +historical fills were `SIMPLE` orders from the normal signal-generator path). +Six distinct breaks sat between "infer a call from Kevin's video" and "place + +record + manage the trade": + +1. Sizing read price only from `sentiment_context["current_price"]` (None for + Kevin) → `qty=0` → skip. +2. The bridge never put a price on the signal. +3. Orders were always built `SIMPLE` — `stop_loss_pct`/`take_profit_pct` were + ignored, so no brackets. +4. Persisted trades hardcoded `strategy_id=None` → unattributed, invisible to + the dashboard (which filters by Kevin's strategy id). +5. The exit scanner was orphaned — the cron was wired nowhere, so exits never + ran; it also lacked an "already-closed" guard. +6. No realized P&L was ever written, and bracket stop/take-profit legs that + fill at Alpaca never passed through our executor (close + P&L invisible). + +## Fixes (TDD throughout) + +| Phase | Change | Commit | +|---|---|---| +| 1 | `TradeSignal.current_price`; bridge sets it; sizing honors `target_dollars` and resolves price; BRACKET orders for LONG entries (EXIT stays SIMPLE) | `14407d3` | +| 2 | Persist `strategy_id` on the trade row + execution (record + dashboard) | `a8b0d33` | +| 3 | EXIT closes the full held position; realized P&L on close; wire exit scanner on its cron + offsetting-SELL guard | `52b3c76` | +| 4 | `broker.get_order(nested)` + `list_orders`; `Trade.broker_order_id` (migration `f6a7b8c9d0e1`); reconcile loop books bracket auto-closes + P&L idempotently, syncs status, logs drift | `82dc622` | + +Design decision: sizing **honors the bridge's `target_dollars`** rather than +re-deriving in the executor — the bridge already applied conviction-weighting +and per-ticker headroom the executor can't see. Brackets trigger on entry +direction + presence of both pct legs (the bridge stamps pcts even on exits, so +the LONG guard is what keeps exits simple). + +## Cold start — "new signals only" + +The live state was already new-only at go-live: bridge cursor +`kevin:bridge:last_mention_id = 316` (= MAX mention id, all prior mentions +behind the cursor) and `kevin:deferred_signals` empty. No flush required. The +stale "deferred" backlog seen in `kevin_signal_bridge_state` was audit-row +*classification*, not queued orders. + +## Migration handling + a latent gap + +The Woodpecker pipeline (`.woodpecker.yml`) has **no migration step** — it runs +tests → builds images → patches the deployment. Migrations only run via the +TF-managed `trading-bot-migrations` Job (`alembic upgrade head`), which the +deploy does not trigger. Migration `f6a7b8c9d0e1` (nullable +`trades.broker_order_id` + index) is backward-compatible, so it was applied to +the live DB ahead of the rollout (atomic DDL + version stamp); the TF Job will +no-op on its next run. + +**Recommendation (separate, not done here):** add a `run-migrations` step to the +pipeline (a K8s Job on the freshly-built image, before `update-deployment`) so +future migrations apply automatically on deploy. Requires `create jobs` RBAC for +the Woodpecker service account (infra repo). + +## Guardrails (live) + +Per-strategy caps already enforced in the risk manager: 10 trades/day, $20k/day +allocation, 20% equity-drawdown halt (permanent pause), 5% daily-loss circuit +(24h pause). Paper account only (`TRADING_PAPER_TRADING=true`). + +## Go-live verification + +1. CI green; `trading-bot-workers` 6/6 on the new image. +2. Next Kevin video → mention (id > 316) → bridge emits → executor places a + **BRACKET** paper order → `trades` row with `strategy_id = kevin` → + dashboard `/meet-kevin/strategy` shows it; `#trading-bot` Slack posts the + entry. (First real trade occurs on Kevin's next upload — the watcher polls + every ~3h.) +3. On a stop/take-profit fill, the reconcile loop books the closing SELL + + realized P&L.