# Drift PERP “kontrakt bota” (SOL-PERP) — spec intent → egzekucja → audyt Ten dokument definiuje **przyszłościowy** kontrakt między: - **Vast (model/transformer na GPU)**: generuje *trade intent* (bez sekretów), - **k3s/VPS (executor)**: waliduje ryzyko, wystawia i prowadzi zlecenia na Drift, loguje zdarzenia, - **UI (visualizer)**: tylko wizualizuje warstwy i stan kontraktów (live + historia). Kluczowa zasada: **model nigdy nie ma kluczy** i nie “handluje”. Handluje tylko executor w k3s. Powiązane: - Strategia “eskalacja horyzontu” (1m→5m→15m→30m→1h z bramkami): `doc/strategy-eskalacja-horyzontu.md` --- ## 1) Co nazywamy “kontraktem” (u nas vs Drift) Na Drift istnieją: - **orders** (zlecenia): limit/market/trigger, post-only, reduce-only, IOC/GTC itd. - **position** (pozycja): rozmiar, kierunek, średnia cena, PnL itd. - **konto/margin**: collateral i health. W naszym systemie **kontrakt bota** to byt aplikacyjny (DB + logika), który: 1) opisuje *intent* (wejście + prowadzenie + wyjście), 2) mapuje intent na 1..N orderów na Drift, 3) jest **idempotentny** (nie dubluje orderów po restarcie), 4) jest **modyfikowalny** (cancel+place / zmiana desired state), 5) jest **kończony** (exit policy lub kill-switch), 6) jest **audytowalny** (pełny log decyzji i akcji). --- ## 2) Wybór “lepszy i przyszłościowy” ### A) Cena jako offset, nie absolutna cena (recommended) Model zwraca cenę wejścia/wyjścia jako **offset** (ticks/bps) względem top-of-book / mid, a nie jako `limit_price`. Dlaczego: - odporniejsze na latency (cena się przesuwa, offset pozostaje sensowny), - łatwiejsze “reprice” (edit policy jest naturalna), - mniejsze ryzyko “starej ceny” przy krótkim TTL. Executor i tak zna: - `best_bid/best_ask/mid`, - tick size i step size, - aktualne gates (spread/slippage/depth/freshness). ### B) Desired-state jako rdzeń (recommended) Kontrakt jest prowadzony jako **desired-state loop**: - model/kontrakt mówi “co chcę mieć” (np. `target_exposure_usd`), - executor porównuje “observed vs desired” i wykonuje minimalne akcje. To upraszcza: - edycję (zmiana target, update policy), - reconcile po restarcie, - panic exit. --- ## 3) Role: Vast vs executor (k3s) ### Vast (model) zwraca - kompletny **trade_intent**: parametry wejścia/prowadzenia/wyjścia, - sugestie gates (np. spread/slippage/depth), **ale** nie może ich omijać, - `confidence/urgency` (metadata). ### Executor (k3s) jest “single source of execution” - waliduje gates i limity, - normalizuje do tick/step, - nadaje idempotentne `client_order_id`, - składa/canceluje/zamyka (reduce-only), - prowadzi state machine, - loguje eventy i mierzy koszty. --- ## 4) Spec: `trade_intent` (Vast → k3s) Format jest wersjonowany: `intent_schema_version`. ### 4.1 Minimalny szkielet ```jsonc { "intent_schema_version": 1, "decision_id": "ulid-or-uuid", "bot_id": "bot-sol-perp-01", "ts": "2026-01-31T00:00:00.000Z", "ttl_ms": 15000, "market_name": "SOL-PERP", "subaccount_id": 0, "mode": "enter|manage|exit|panic", "confidence": 0.0, "urgency": 0.0, "desired": { "target_exposure_usd": 0, "max_position_usd": 200, "min_trade_usd": 5 }, "entry": { "side": "long|short", "order_type": "post_only_limit|limit|market", "size_usd": 25, "limit_offset": { "ref": "best_bid|best_ask|mid", "ticks": 1 }, "time_in_force": "GTC|IOC", "cancel_if_not_filled_ms": 8000 }, "manage": { "reprice_after_ms": 750, "reprice_offset_ticks": 1, "max_reprices_per_min": 30, "cooldown_ms": 250 }, "exit": { "max_hold_s": 180, "stop_loss_bps": 25, "take_profit_bps": 35, "exit_order_type": "reduce_only_limit|reduce_only_market", "exit_limit_offset": { "ref": "best_bid|best_ask|mid", "ticks": 1 } }, "gates": { "freshness_max_ms": 1500, "max_spread_bps": 10, "max_slippage_bps": 25, "min_depth_topn_usd": 5000, "min_depth_band": { "band_bps": 20, "min_usd": 8000 } } } ``` ### 4.2 Zasady interpretacji - `ttl_ms`: po tym czasie executor ma prawo *zignorować* intent (stary sygnał). - `mode`: - `enter`: wolno otwierać/rozszerzać pozycję, - `manage`: tylko zarządzanie już istniejącą pozycją/ordreami (bez zwiększania ryzyka), - `exit`: przejście do `target_exposure_usd=0` i zamykanie, - `panic`: natychmiast cancel + close (reduce-only), potem `off`. - `desired.target_exposure_usd` jest źródłem prawdy, ale executor ma **hard cap** `max_position_usd` (model może sugerować, executor egzekwuje). - `limit_offset`: - `ref=best_bid` dla wejścia long (maker), - `ref=best_ask` dla wejścia short, - ticks/bps są zaokrąglane do tick size rynku. --- ## 5) Mapowanie `trade_intent` → Drift order params (PERP) Executor buduje “order template” (pseudopola): - `marketIndex` (wynik mapowania `market_name`) - `direction`: `long|short` - `orderType`: `market|limit` (+ `trigger*` jeśli później dodamy SL/TP jako ordery trigger) - `baseAssetAmount` (z `size_usd` przeliczone do base i zaokrąglone do `baseStepSize`) - `price` (dla limit): z `limit_offset` + aktualne top-of-book, zaokrąglone do `priceTickSize` - `postOnly`: true, jeśli `order_type=post_only_limit` - `reduceOnly`: true na wyjściu (`exit_order_type=reduce_only_*`) - `immediateOrCancel`: true, jeśli `time_in_force=IOC` - `clientOrderId` / `userOrderId`: deterministycznie z `decision_id` (patrz niżej) ### 5.1 Idempotencja: `client_order_id` Wymaganie: po restarcie executora nie może dojść do “double order”. Zasada: - każde wejście/wyjście ma stabilne `client_order_id` wywiedzione z `decision_id`, - jeśli Drift nie wspiera pełnego “modify”, executor robi `cancel + place` ale zachowuje spójne ID (np. `decision_id` + suffix `-r1`, `-r2` dla reprices). --- ## 6) State machine kontraktu (minimal) Rekomendowane stany: - `off` (nie handluje) - `pending_entry` (intent zaakceptowany, order wysłany) - `entered` (pozycja ≠ 0 lub entry fill) - `managing` (utrzymuje desired state / repricing) - `exiting` (reduce-only close w toku) - `closed` (pozycja 0, brak orderów) - `rejected` (gates fail / TTL expired) - `panic` (cancel+close; potem `off`) Każda zmiana stanu = event do DB. --- ## 7) Co UI wizualizuje (warstwy) a co liczy backend UI nie liczy. UI: - subskrybuje `*_latest` (live), - pobiera `*_ts` (historia), - renderuje warstwy i kontrakty. Backend liczy: - DLOB: `dlob_*_latest` + (docelowo) `dlob_*_ts` - ticks/candles: `drift_ticks` + `get_drift_candles(...)` - kontrakty: `bot_intents` / `bot_contracts` / `bot_events` (+ TS wersje) --- ## 8) Metryki do strojenia (co mierzyć i jakie okna czasowe) Cel: stroić gates, politykę repricing i parametry exit. ### 8.1 Mikrostruktura (gates) — z czego stroimy progi Źródła: `dlob_stats_*`, `dlob_depth_*`, `dlob_slippage_*`. Mierz (per market, live + TS): - `spread_bps` - `impact_bps` dla docelowych `size_usd` (buy/sell) - `depth_bid_usd`, `depth_ask_usd` (top‑N) - depth w bandach (`band_bps`): `bid_usd/ask_usd` - `freshness_ms` (now - updated_at) Okno strojenia: - start: **7 dni** TS (wystarczy do percentyli i pór doby), - docelowo: 30+ dni (downsample 1m/5m) dla stabilniejszych reżimów. Jak stroić: - progi na percentylach (P90/P95), nie na średniej, - osobne percentyle per “godzina doby” (płynność) i per “vol regime”. ### 8.2 Jakość egzekucji (czy entry/manage działa) Źródła: `bot_events` (audyt) + snapshoty z momentu decyzji. Mierz per kontrakt: - `time_to_first_fill_ms`, `time_to_full_fill_ms` - `fill_pct`, `avg_fill_price` - `reprice_count`, `cancel_count` - `expected_execution_bps` (z DLOB w chwili decyzji) vs `realized_execution_bps` - “churn cost”: `tx_count` i (jeśli liczymy) `priority_fee` sumarycznie Okno strojenia: - 24h (szybki smoke po deployu), - 7 dni (tuning), - 30 dni (stabilizacja). ### 8.3 Wynik i ryzyko (czy strategia ma edge) Mierz: - `hold_time_s` - reason exit: `tp|sl|time|regime|panic` - MAE/MFE w bps w trakcie hold - PnL (jeśli macie komplet danych) albo proxy w bps Okno: - 7 dni minimalnie, ale sensowniej 30+ dni (reżimy). --- ## 9) Następny krok implementacyjny (po akceptacji) 1) Dodać tabele TS dla warstw (min. 7 dni retencji). 2) Dodać tabele i logi kontraktów (`bot_contracts`, `bot_events`, opcjonalnie `bot_intents_ts`). 3) Dodać “executor” (observe → dry-run → live) oraz integrację Vast.