Files
trade-frontend/doc/drift-perp-contract.md
2026-02-01 21:44:45 +01:00

8.3 KiB
Raw Blame History

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”

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).

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

{
  "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 (topN)
  • 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.