docs: add rpc/dlob/candles notes
This commit is contained in:
262
doc/drift-perp-contract.md
Normal file
262
doc/drift-perp-contract.md
Normal file
@@ -0,0 +1,262 @@
|
||||
# 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.
|
||||
Reference in New Issue
Block a user