263 lines
8.3 KiB
Markdown
263 lines
8.3 KiB
Markdown
# 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.
|