diff --git a/doc/candles-cache.md b/doc/candles-cache.md new file mode 100644 index 0000000..8e011b7 --- /dev/null +++ b/doc/candles-cache.md @@ -0,0 +1,39 @@ +# Candles cache: precompute wszystkich timeframe (1s…1d) + +Cel: przełączanie `tf` w UI ma być natychmiastowe. Backend ma **ciągle liczyć** i **przechowywać** świeczki dla wszystkich timeframe: + +`1s 3s 5s 15s 30s 1m 3m 5m 15m 30m 1h 4h 12h 1d` + +## Jak to działa + +1) Ticki (append-only) lądują w `drift_ticks`. +2) Worker `candles-cache-worker`: + - liczy świeczki dla **każdego** `bucket_seconds` bezpośrednio z `drift_ticks`, + - trzyma w DB “ostatnie N” świec (domyślnie `N=1024`) per `(symbol, source, tf)`, + - jeśli danych historycznych jest mniej (np. brak wielu dni) — zapisuje tylko to, co istnieje, + - robi backfill/warmup przy starcie i potem dopisuje “na bieżąco” w pętli. +3) API `GET /v1/chart` czyta **cache-first** z `drift_candles_cache` (fallback do on-demand funkcji, jeśli cache pusty). + +## Tabela + +- `drift_candles_cache` (Timescale hypertable, partycjonowanie po `bucket`) + - `bucket_seconds` = długość świecy w sekundach + - `source=''` oznacza “(any)” (brak filtra po źródle ticków) + +## Worker + +Plik: `services/candles-worker/candles-cache-worker.mjs` + +Env: +- `CANDLES_SYMBOLS` (np. `SOL-PERP,PUMP-PERP`) +- `CANDLES_SOURCES` (np. `any,drift_oracle`) +- `CANDLES_TFS` (np. `1s,3s,5s,15s,...,1d`) +- `CANDLES_TARGET_POINTS` (default `1024`) +- `CANDLES_BACKFILL_DAYS` (opcjonalnie: wymusza minimalny warmup “co najmniej X dni”) +- `CANDLES_POLL_MS` (default `5000`) + +## Dlaczego to jest szybkie + +- najcięższe agregacje są robione raz i utrzymywane “na bieżąco”, +- przełączenie `tf` to tylko query po gotowych wierszach (`order_by bucket desc limit N`), +- “flow/brick stack” w `/v1/chart` jest liczone z cache “point candles” (np. `1s/3s/5s/15s/…`) bez skanowania `drift_ticks`. diff --git a/doc/dlob-basics.md b/doc/dlob-basics.md new file mode 100644 index 0000000..7254e6e --- /dev/null +++ b/doc/dlob-basics.md @@ -0,0 +1,216 @@ +# DLOB + L1…L10 — podstawy (co jest czym i gdzie to liczymy) + +Ten dokument wyjaśnia pojęcia: +- **DLOB** (Drift Limit Order Book), +- **L1 / L2 / L3** oraz potoczne **L1…L10**, +- na jakich warstwach w naszym stacku powstają dane i metryki, +- gdzie “pracuje AI” (modele/strategie) vs gdzie jest execution (order placement). + +## Co to jest DLOB + +**DLOB** = *Decentralized Limit Order Book* w Drift. + +W praktyce: to jest **księga zleceń** dla rynku (np. `SOL-PERP`): +- **bids** = zlecenia kupna (po stronie bid), +- **asks** = zlecenia sprzedaży (po stronie ask). + +Księga ma wiele “poziomów” cenowych; przy każdej cenie stoi pewna ilość (size). + +## L1 / L2 / L3 (format i sens) + +### L1 (Top of Book) +L1 to skrót od “top of book”: +- **best bid** = najwyższa cena kupna (pierwszy poziom po stronie bid), +- **best ask** = najniższa cena sprzedaży (pierwszy poziom po stronie ask). + +Z L1 najczęściej liczysz: +- **spread** = `best_ask - best_bid`, +- **mid** = `(best_bid + best_ask) / 2`. + +### L2 (zagregowane poziomy) +L2 to lista poziomów (levels) po obu stronach: +- `bids: [{ price, size }, ...]` (zwykle posortowane malejąco po `price`) +- `asks: [{ price, size }, ...]` (zwykle posortowane rosnąco po `price`) + +To jest najpopularniejszy “orderbook UI”: słupki/heat per poziom ceny. + +### L3 (pojedyncze zlecenia) +L3 to “niezagregowane” dane: pojedyncze zlecenia (większy wolumen danych). +U nas pod UI i metryki zazwyczaj wystarcza L2. + +## L1…L10 (co to znaczy w praktyce) + +**L1…L10** to potoczne określenie: +> “pierwsze 10 poziomów z L2 najbliżej top of book”. + +To nie jest osobny format; to po prostu wycinek L2. + +W naszym stacku “ile leveli bierzemy” kontroluje: +- `DLOB_DEPTH` (np. 10 → “L1…L10”). + +## Jak to działa w naszym stacku (warstwy) + +Poniżej “łańcuch” od źródła do metryk: + +### Warstwa A: On-chain → DLOB w pamięci (VPS/k3s) +Komponent: `dlob-publisher`. + +- Łączy się do Solany przez `ENDPOINT` (HTTP RPC) i `WS_ENDPOINT` (WebSocket). +- Subskrybuje konta/zdarzenia i buduje DLOB (orderbook) w pamięci. +- Publikuje snapshoty do Redis (u nas: `dlob-redis`). + +To jest najbliżej źródła i zwykle najbardziej “real-time”. + +### Warstwa B: Cache + REST API (VPS/k3s) +Komponenty: `dlob-redis` + `dlob-server`. + +- `dlob-redis` trzyma snapshoty/publish. +- `dlob-server` udostępnia HTTP: + - `GET /l2?marketName=SOL-PERP&depth=10` → L2 (bids/asks + best bid/ask itp.) + - `GET /l3?...` → L3 (jeśli potrzebujesz) + +To jest warstwa dystrybucji danych “w klastrze”, żeby inne serwisy nie musiały gadać bezpośrednio z Solaną. + +Uwaga o rynkach: +- `dlob-publisher` ładuje rynki wg `PERP_MARKETS_TO_LOAD` (indeksy) / `SPOT_MARKETS_TO_LOAD`. +- Jeśli rynek nie jest załadowany przez publisher, `dlob-server` nie rozpozna `marketName`. + +### Warstwa C: Metryki w DB/Hasura (VPS/k3s) +Komponenty: `dlob-worker`, `dlob-depth-worker`, `dlob-slippage-worker`. + +To są “workery pod UI/AI”, które liczą metryki i zapisują je do Postgresa (Hasura). + +#### `dlob-worker` (collector + basic stats) +Wejście: +- odpytuje `dlob-server` po HTTP `/l2` (źródło L2), +- rynki: `DLOB_MARKETS`, +- głębokość (ile leveli): `DLOB_DEPTH`, +- częstotliwość: `DLOB_POLL_MS`. + +Wyjście (upsert do DB): +- `dlob_l2_latest` = snapshot L2 “latest” per market, +- `dlob_stats_latest` = pochodne metryki liczone z top‑N leveli (N=`DLOB_DEPTH`), m.in.: + - `mid_price`, `spread_abs`, `spread_bps`, + - `depth_bid_*` / `depth_ask_*`, + - `imbalance`. + +Czyli: jeśli pytasz “gdzie liczymy L1…L10 metryki” → tutaj (w `dlob-worker`), bo bierze top‑N leveli z L2. + +#### `dlob-depth-worker` (depth w bandach bps) +Wejście: +- czyta z DB `dlob_l2_latest` (czyli już “przetworzone” L2). + +Wyjście: +- `dlob_depth_bps_latest` = płynność w pasmach wokół mid (np. ±5/10/20/50/100/200 bps). + +To nie jest “L1…L10”, tylko “ile płynności mieści się w oknie cenowym” wokół mid. + +#### `dlob-slippage-worker` (slippage vs size) +Wejście: +- czyta z DB `dlob_l2_latest`. + +Wyjście: +- `dlob_slippage_latest` = symulacja wykonania zlecenia (market) po L2 dla progów `DLOB_SLIPPAGE_SIZES_USD`. + +To jest bardzo użyteczne jako feature do strategii (“ile kosztuje wejście/wyjście teraz dla X USD”). + +## Gdzie “pracuje AI” (TFT itp.) + +AI/strategia powinna pracować na warstwie “features”, a nie na surowych subskrypcjach Solany: + +Najczęstszy zestaw wejść dla modelu: +- candles/ticki (np. `drift_ticks` + `get_drift_candles(...)`), +- bieżące statsy z DLOB: + - `dlob_stats_latest` (mid/spread/depth/imbalance), + - `dlob_depth_bps_latest` (depth w bandach), + - `dlob_slippage_latest` (slippage vs size), +- opcjonalnie pełny snapshot L2 (z `dlob_l2_latest`), jeśli model potrzebuje “kształtu” książki. + +Kluczowa zasada bezpieczeństwa: +- **Model (np. na Vast)** może sugerować “desired state” (wejść/wyjść/parametry), +- **Executor na VPS** zawsze odpowiada za: + - risk checks, + - składanie/cancel/close, + - klucze prywatne i podpisywanie transakcji, + - kill switch. + +## Szybki słownik (1-liner) + +- **bid**: kupno, zielona strona książki +- **ask**: sprzedaż, czerwona strona książki +- **best bid / best ask (L1)**: top-of-book +- **spread**: koszt “wejścia/wyjścia natychmiast” (ask-bid) +- **mid**: punkt odniesienia między bid/ask +- **L2**: lista poziomów `{price,size}` +- **L1…L10**: top 10 poziomów z L2 (u nas kontrolowane przez `DLOB_DEPTH`) + +## Jak liczymy “liquidity” i “kasa” (USD) w metrykach + +W UI/DB słowo “liquidity” zwykle oznacza **depth**: “ile wolumenu stoi w orderbooku blisko ceny”. +U nas trzymamy to rozdzielnie dla bid/ask oraz w dwóch wariantach: + +### A) Top‑N leveli (np. L1…L10) — `dlob_stats_latest` +Liczone w `dlob-worker` na podstawie L2 z `/l2`: + +- Bierzemy pierwsze `N = DLOB_DEPTH` leveli z `bids` i `asks`. +- Każdy level ma: + - `price = price_int / PRICE_PRECISION` + - `size_base = size_int / BASE_PRECISION` + - “kasa” (notional) na tym levelu: `size_usd = size_base * price` +- Sumujemy po levelach: + - `depth_bid_base = Σ size_base` (po stronie bid), + - `depth_bid_usd = Σ (size_base * price)` (po stronie bid), + - analogicznie `depth_ask_base`, `depth_ask_usd` (po stronie ask). + +To odpowiada intuicji “ile jest płynności na L1…LN”. + +### B) Okno cenowe w bps od mid — `dlob_depth_bps_latest` +Liczone w `dlob-depth-worker` na podstawie `dlob_l2_latest`: + +- Dla pasma `band_bps` wyznaczamy: + - `minBidPrice = mid * (1 - band_bps/10_000)` + - `maxAskPrice = mid * (1 + band_bps/10_000)` +- Sumujemy wszystkie levele, które mieszczą się w tym oknie: + - bids: `price >= minBidPrice` + - asks: `price <= maxAskPrice` +- Liczymy sumy: + - `bid_base`, `bid_usd`, `ask_base`, `ask_usd` tak jak wyżej (`usd = base * price`). + +To odpowiada intuicji “ile płynności jest *blisko* ceny w ±X bps”. + +### Ważne doprecyzowanie + +Te liczby to **notional z orderbooka** (ile “stoi” na poziomach cenowych). +Nie są to “pieniądze w kontrakcie”, tylko przybliżenie kosztu/pojemności wykonania przy danej cenie i bez przesunięcia rynku. + +## Spec: Orderbook UI (L1…L10 + “liquidity bars”) + +Wizualizacja orderbooka (jak na screenach) jest oparta o L2 i pokazuje tylko top‑N leveli: +- `N` = liczba leveli wyświetlanych na stronę (np. 10 → “L1…L10”). + +### Kolumny / wartości + +Na każdym levelu liczymy: +- `size_usd = size_base * price` + +W UI pokazujemy: +- `Size (USD)` = `size_usd` dla danego poziomu, +- `Total (USD)` = suma skumulowana od best‑price “w głąb” (cumulative): + - bids: kumulacja od best bid w dół, + - asks: kumulacja od best ask w górę (w UI zwykle best ask jest bliżej środka). + +### “Liquidity bars” (znormalizowane słupki tła) + +Żeby “na oko” widzieć gdzie stoi płynność: + +1) **Level bar (per‑poziom)** — normalizacja do największego `size_usd` w widocznych levelach danej strony: + - `level_scale = size_usd / max(size_usd w widoku)` +2) **Total bar (cumulative)** — normalizacja do największego `total_usd` w widocznych levelach danej strony: + - `total_scale = total_usd / max(total_usd w widoku)` + +Żeby duże “ściany” nie zabijały kontrastu, warto użyć krzywej: +- `scale_curved = sqrt(clamp01(scale))` + +Interpretacja: +- **level bar** = “ile stoi na tym poziomie”, +- **total bar** = “ile stoi łącznie do tego poziomu”. diff --git a/doc/drift-costs.md b/doc/drift-costs.md new file mode 100644 index 0000000..4794186 --- /dev/null +++ b/doc/drift-costs.md @@ -0,0 +1,120 @@ +# Drift Perp: koszty wejścia/edycji/wyjścia (stan na 2026-01-31) + +Ten dokument zbiera **wszystkie realne składowe kosztu** przy handlu perps na Drift, żebyśmy mogli je liczyć na backendzie i wizualizować w UI. + +## 1) Składowe kosztu (per trade / per pozycja) + +### A. Opłata transakcyjna Drift (maker/taker) +- **Taker fee**: procent od **notional** (wartości pozycji w USD/USDC). +- **Maker fee**: zwykle **ujemny** (rebate) dla zleceń maker (np. post-only), zgodnie z aktualnym cennikiem. +- Stawki zależą od wolumenu 30D oraz stakingu DRIFT (dodatkowe zniżki / większe rebate). +- W **High Leverage Mode** taker fee może być podbite (np. 2× najniższy tier). +> TODO: potwierdzić aktualne stawki fee (z Drift SDK / on-chain) i zapisać je jako “source of truth” dla backendu. + +**Wzór (pojedynczy fill):** +- `notional = |size_base| * fill_price` +- `trade_fee_usd = notional * fee_rate` (dla maker `fee_rate` może być < 0) + +### B. Slippage / spread (koszt rynkowy) +To nie jest fee protokołu, ale realny koszt wejścia/wyjścia: +- `slippage_cost_usd ≈ (fill_price - mid_price) * size_base` (znak zależy od long/short) +- U nas to powinno być liczone z DLOB (L2 + symulacja fill). + +### C. Funding (koszt/zarobek w czasie trzymania pozycji) +- Funding jest naliczany w czasie i realizowany przy akcjach użytkownika (trade/deposit/withdraw) – w praktyce dla krótkich holdingów (minuty–1h) zwykle jest małym składnikiem, ale nie zawsze zerowym. + +**Wzór (upraszczając):** +- `funding_usd ≈ Σ (position_notional_usd * funding_rate_interval)` + +### D. P&L settlement / “unsettled P&L” (wpływ na withdraw) +- Żeby **wypłacić zysk**, czasem trzeba wykonać `settlePNL` (rozlicza P&L do P&L Pool; nie zamyka pozycji, tylko zmienia cost basis). +- Jeśli brakuje środków w per-market P&L Pool, zysk może być częściowo **unsettled** i nie będzie w pełni wypłacalny od razu. + +### E. Liquidation penalty (jeśli konto spadnie poniżej maintenance) +- Przy wejściu w liquidację protokół najpierw anuluje otwarte ordery/LP, a następnie liquidator może redukować pozycje. +- “Penalty/fee” jest ustawiana per-market i zwykle jest wyższa niż zwykły taker fee (żeby dać rebate liquidatorowi). + +### F. Koszt sieci Solana (per instrukcja / per tx) +To koszt “infrastrukturalny” każdej akcji on-chain (order, cancel, modify, settlePNL, deposit/withdraw, close). +- **Base fee**: 5000 lamports per signature (minimum). +- **Priority fee**: opcjonalny, zależy od congestion. +- Jednorazowo może dojść **rent/account creation** (np. token account), jeśli czegoś brakuje. + +## 2) “Ile kosztuje” konkretna akcja (checklista) + +### Wejście w pozycję (open / increase) +1) **Solana tx fee** (base + ewentualnie priority) +2) **Drift trading fee** (maker/taker) od notional +3) **Slippage/spread** (z DLOB) +4) (w tle) funding zaczyna naliczać się w czasie + +### Zmiana pozycji (increase/decrease/flip) +To po prostu kolejny trade: +- znowu `tx fee + trading fee + slippage` +- oraz często realizacja funding (zależy od tego czy funding został zaktualizowany) + +### Wyjście z pozycji (close) +1) `tx fee` +2) `trading fee` (druga strona round-trip) +3) `slippage` +4) **realized PnL** = różnica cen ± funding − fees +5) jeśli chcesz wypłacić: możliwe `settlePNL` oraz limit z P&L pool + +### Edycja zlecenia (modify) +Zwykle koszt to: +- `tx fee` (czasem modify = cancel+place, zależnie od ścieżki w kliencie) +- brak trading fee, jeśli nie było fill + +### Cancel zlecenia +- `tx fee` +- brak trading fee (jeśli 0 fill) + +### Monitorowanie zysku / risk (PnL, margin, health) +On-chain: bez kosztu, jeśli tylko czytasz RPC/indexera. +Koszt pojawia się dopiero przy akcjach typu trade/cancel/settle/withdraw. + +## 3) Przykład liczbowy (taker, round-trip) + +Załóż: +- `notional = 10,000 USDC` +- `taker_fee_rate = 0.0350%` (PRZYKŁAD – realna stawka zależy od tieru) + +Wtedy: +- wejście: `10,000 * 0.00035 = 3.50 USDC` +- wyjście: `3.50 USDC` +- razem fee (bez slippage/funding): `7.00 USDC` + 2× Solana tx fee (+ priority jeśli ustawisz). + +## 4) Co musimy znać, żeby liczyć to “dokładnie” w backendzie + +Minimalny zestaw wejść: +- market (np. `SOL-PERP`) +- order type (market/limit/post-only), przewidywany fill path (taker vs maker) +- notional/size, przewidywany fill (DLOB simulation) +- fee tier użytkownika + staking/discounty + ew. “fee adjusted markets” +- funding history + horyzont (np. 1h/4h/24h/7d) +- czy chcemy uwzględniać `settlePNL` oraz status “unsettled PnL” przed withdraw + +--- + +## 5) Słownik (kluczowe pojęcia w UI/API) + +Poniżej jest skrót pojęć, których używamy w warstwach “Costs (New)” i “Costs (Active)”: + +- `notional` — wartość pozycji w USD (np. 10 USD); na tym liczymy bps i fee. +- `bps` (basis points) — punkty bazowe: `1 bps = 0.01% = 0.0001`. + Przeliczenie na koszt: `koszt_usd ≈ notional_usd * bps / 10_000`. +- `fee` — opłata protokołu Drift (maker/taker) od `notional`; zwykle stała dla danego trybu/tieru. +- `tx fee` — koszt transakcji na Solanie (base fee + ewentualny priority fee). +- `slippage` — koszt rynkowy wejścia/wyjścia, bo wykonujesz się gorzej niż `mid` (zależy od płynności). +- `impact (bps)` — slippage wyrażony w bps (dla danego notionalu). +- `spread` — różnica `best_ask - best_bid`; “minimalny” koszt natychmiastowego wejścia/wyjścia w płytkim booku. +- `mid` — `(best_bid + best_ask) / 2`; punkt odniesienia ceny z orderbooka. +- `VWAP` — średnia cena wykonania dla danego rozmiaru (symulacja fill po L2). +- `breakeven (bps)` — minimalny ruch ceny (w bps), żeby koszty się zwróciły (wyjść na 0). +- `PnL` (profit and loss) — zysk/strata: + - `unrealized PnL` — “na papierze”, gdy pozycja jest otwarta (zależy od ceny teraz), + - `realized PnL` — zrealizowany po zamknięciu (lub częściowym zamknięciu) pozycji, + - `net PnL` — PnL po odjęciu kosztów (`fee + tx + slippage + funding`). +- `funding` — okresowa płatność long↔short; koszt albo zysk zależny od rynku i czasu trzymania. +- `close now` — estymata kosztu natychmiastowego zamknięcia pozycji (zwykle po przeciwnej stronie booka). +- `modify` / `reprice` — koszt “zarządzania zleceniem” (cancel+place itp.), głównie `tx fee` (czasem wielokrotnie). diff --git a/doc/drift-data-bez-solana-rpc.md b/doc/drift-data-bez-solana-rpc.md new file mode 100644 index 0000000..ec01ae6 --- /dev/null +++ b/doc/drift-data-bez-solana-rpc.md @@ -0,0 +1,109 @@ +# Drift / Solana: czy mamy dostęp do danych bez Solana RPC? + +Pytanie ma dwa znaczenia — rozdzielmy je jasno: + +1) **bez własnego (bare metal) RPC** — czyli nie utrzymujemy swojego `solana-validator --rpc`, ale korzystamy z dostawcy RPC albo zewnętrznych serwisów, +2) **bez żadnego RPC w ogóle** — czyli nikt w naszym systemie nie pyta Solany o stan on‑chain. + +TL;DR: +- **Bez własnego RPC**: tak, da się na start (hosted RPC +/lub serwisy zewnętrzne). +- **Bez żadnego RPC**: tylko częściowo (dane “rynkowe” można brać z zewnętrznego DLOB), ale **stan konta/pozycji/fille/funding** i tak pochodzi z chaina, więc ktoś musi mieć RPC. + +--- + +## Co z Twojego “speca” da się mieć bez własnego RPC? + +Poniżej mapowanie kategorii danych (z Twojego opisu A–F) na źródła: + +### B) Prices / microstructure (oracle/mark/BBO, “close now”) + +**Da się bez własnego RPC**: +- Tak. W naszym stacku te dane mogą pochodzić z pipeline DLOB (`dlob_*_latest`) i ticków (`drift_ticks`), które są już w DB i dostępne przez Hasurę / `trade-api`. + +**Da się bez żadnego RPC w naszej infra** (czyli “my nie łączymy się do RPC”): +- Częściowo tak, jeśli polegamy na zewnętrznym źródle L2/BBO (np. `https://dlob.drift.trade`) — ale to źródło i tak jest zasilane przez czyjeś RPC. + +### A) Position snapshot (pozycja: base, entry, side) + +**Bez własnego RPC**: +- Tak, jeśli mamy **jakikolwiek** komponent (executor/collector) korzystający z hosted RPC (Helius/QuickNode/itp.) i zapisujący snapshot pozycji do DB. + +**Bez żadnego RPC**: +- Praktycznie nie (pozycja jest stanem konta on‑chain). Wyjątek: jeśli Twój executor/bot sam utrzymuje lokalny stan i zapisuje go do DB — ale po restarcie i tak potrzebujesz reconcile z chaina (czyli RPC). + +### C) Account risk (margin/liquidation/health) + +**Bez własnego RPC**: +- Tak, jeśli collector liczy to na backendzie z danych Drift (przez hosted RPC) i zapisuje do TS (`contract_metrics_ts` / analogicznie). + +**Bez żadnego RPC**: +- Nie, bo margin/liq zależy od stanu konta i parametrów rynku on‑chain. + +### D) Fills / trades (realized PnL + fees + slippage) + +**Bez własnego RPC**: +- Tak, jeśli: + - executor składa zlecenia i loguje fille do `bot_events` (to już mamy jako koncept), albo + - collector subskrybuje eventy transakcji / kont przez hosted RPC i zapisuje fille do DB. + +**Bez żadnego RPC**: +- Tylko jeśli fille są już zapisane w DB (np. przez bota). Na bieżąco — ktoś musi je wyciągać z chaina. + +### E) Funding / payments + +**Bez własnego RPC**: +- Tak, ale ktoś musi pobierać funding rate / funding payment (hosted RPC lub inny feed) i zapisywać do DB. + +**Bez żadnego RPC**: +- Jak wyżej: tylko z historii zapisanej w DB; na żywo potrzebujesz źródła z chaina. + +### F) Order lifecycle costs (cancel/replace/tx) + +**Bez własnego RPC**: +- Tak, jeśli executor: + - loguje akcje (create/cancel/replace) i ich koszty (`tx_fee_usd`, priority fee) do `bot_events`, albo + - collector wyciąga metryki tx z RPC i mapuje do orderów. + +**Bez żadnego RPC**: +- Tylko retrospektywnie (jeśli już w DB). + +--- + +## Co to znaczy praktycznie dla architektury “backend liczy, UI tylko wyświetla”? + +UI/Visualizer **może działać bez bezpośredniego kontaktu z RPC** (łączy się do `trade-api` + Hasura). + +Natomiast backend “compute” (k3s) ma dwie opcje zasilania: + +1) **Hosted RPC** (na start) + - pro: szybciej, taniej, mniej ops, + - con: limit subskrypcji/WS, możliwe rwania, vendor lock‑in. + +2) **Własny RPC + Geyser/Yellowstone** (docelowo) + - pro: kontrola, stabilność na większej skali, streaming “pro”, + - con: koszt i ops (dyski/IO, tuning, monitoring). + +W obu przypadkach “backend liczy” działa tak samo — różni się tylko źródło surowych danych. + +--- + +## Co już mamy w DB “bez RPC w UI” + +Z obecnego pipeline (VPS/k3s) mamy “rynkowe” dane pod BBO/slippage: +- `dlob_l2_latest`, `dlob_stats_latest`, `dlob_slippage_latest`, `dlob_depth_bps_latest` (+ TS przez `*_ts`) +- `drift_ticks` (ticki/ceny) + +To wystarcza do: +- estymat wejścia/wyjścia (“close now”), +- wykresów spread/slippage/depth, +- części SIM (model slippage/fee) **bez** znajomości pełnego stanu konta. + +Do pełnego `contract_metrics_ts` (PnL + risk) brakuje nam jeszcze stałego feedu: +- pozycji konta + margin/liq, +- filli i funding (albo z chaina, albo z logów executora). + +## Zobacz też + +- “Kanoniczna” architektura w pełni self-hosted (RPC + DLOB): `doc/rpc-dlob-kanoniczna-architektura.md` +- Runbook: bare metal RPC + Geyser/Yellowstone gRPC: `doc/solana-rpc-geyser-setup.md` +- Mapa dokumentów o RPC/DLOB/metrykach: `doc/solana-rpc.md` diff --git a/doc/drift-perp-contract.md b/doc/drift-perp-contract.md new file mode 100644 index 0000000..8cde554 --- /dev/null +++ b/doc/drift-perp-contract.md @@ -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. diff --git a/doc/k3s-runtime-map.md b/doc/k3s-runtime-map.md new file mode 100644 index 0000000..78f8661 --- /dev/null +++ b/doc/k3s-runtime-map.md @@ -0,0 +1,165 @@ +# k3s runtime map (VPS `qstack`) — co działa i po co + +Ten dokument opisuje **aktualny runtime na VPS** (k3s) dla projektu `trade`: jakie komponenty działają w klastrze, jak płynie dane oraz które tabele/metrki są “źródłem prawdy” dla UI. + +Zakładamy namespace: `trade-staging`. + +## TL;DR (logika) + +- **Dane są zbierane i liczone na backendzie** (k3s). +- UI (`trade-frontend`) **tylko wizualizuje** i proxy’uje ruch (API + GraphQL + WS). +- Hasura to **jedyny GraphQL/WS** na “metrics/live” (subscriptions). +- Postgres/Timescale trzyma: + - ticki (`drift_ticks`) + candles (funkcja `get_drift_candles`) + - “latest” warstw DLOB (`dlob_*_latest`) + - (opcjonalnie) historię warstw (`dlob_*_ts`) + +## Mapa (ruch z zewnątrz) + +``` +Internet + | + v +Ingress/Traefik + | + +--> trade-frontend (https://trade.mpabi.pl) + | - /api -> trade-api + | - /graphql -> hasura + | - /graphql-ws -> hasura (WS subscriptions; protokół graphql-ws) + | + +--> (opcjonalnie inne ingressy w tym samym klastrze) +``` + +### `trade-frontend` (UI + reverse-proxy) + +- **Rola:** UI + proxy do usług w klastrze. +- **Dlaczego:** przeglądarka nie dostaje sekretów; token read jest wstrzykiwany server‑side, a WS działa przez proxy. +- **Wejście:** HTTP/WS od użytkownika. +- **Wyjście:** + - `/api/*` → `trade-api` + - `/graphql` (HTTP) → `hasura` + - `/graphql-ws` (WS) → `hasura` + +## Mapa (dane rynkowe — ticki / candles) + +``` +Solana RPC/WS (zewn.) + | + v +trade-ingestor + | + v +trade-api -> Postgres/Timescale (drift_ticks) + | + +--> /v1/chart (candles + wskaźniki liczone na backendzie) + | + v +Hasura (GraphQL query/subscriptions dla wybranych tabel) +``` + +### `trade-ingestor` + +- **Rola:** pobiera dane (oracle/mark) i wysyła ticki do API. +- **Wyjście:** ticki zapisane do `drift_ticks` przez `trade-api`. + +### `trade-api` + +- **Rola:** API dla UI i algów (healthz, ticks, chart). +- **DB:** zapis do `drift_ticks`. +- **Agregacje:** candles (`get_drift_candles`) + wskaźniki (backend). + +## Mapa (DLOB — orderbook + metryki warstw) + +``` +Solana RPC/WS (zewn.) + | + v +dlob-publisher ----> dlob-redis <---- dlob-server (/l2, /l3) + \ + \ (opcjonalne źródło L2) + v + dlob-worker (kolektor L2) + | + v + Postgres/Hasura: dlob_l2_latest + dlob_stats_latest + | + +------------+------------+ + | | + v v + dlob-depth-worker dlob-slippage-worker + (bands ±bps) (impact vs size USD) + | | + v v + Postgres/Hasura: dlob_depth_bps_latest Postgres/Hasura: dlob_slippage_latest + | + v + (opcjonalnie) dlob-ts-archiver + | + v + Postgres/Timescale: dlob_stats_ts / dlob_depth_bps_ts / dlob_slippage_ts +``` + +### `dlob-publisher` + +- **Rola:** utrzymuje “żywy” DLOB (on‑chain) i publikuje snapshoty do Redis. +- **Wejście:** Solana RPC/WS. +- **Wyjście:** publikacja do `dlob-redis`. + +### `dlob-redis` + +- **Rola:** cache/pubsub pomiędzy publisherem i serwerem HTTP. + +### `dlob-server` + +- **Rola:** serwuje REST `/l2` i `/l3` na podstawie cache Redis (do debugowania i/lub jako źródło L2). + +### `dlob-worker` (kolektor L2 + “basic stats”) + +- **Rola:** pobiera snapshoty L2 i liczy podstawowe metryki (`dlob_stats_latest`). +- **Źródło L2:** w praktyce: + - albo zewnętrzne `https://dlob.drift.trade/l2` + - albo wewnętrzne `http://dlob-server:6969/l2` (jeśli tak ustawione) +- **Zapis do tabel:** + - `dlob_l2_latest` (raw L2) + - `dlob_stats_latest` (bid/ask/mid/spread/depth/imbalance) + +### `dlob-depth-worker` (depth bands ±bps) + +- **Rola:** liczy płynność w pasmach ±bps wokół mid. +- **Źródło:** `dlob_l2_latest` +- **Zapis:** `dlob_depth_bps_latest` (klucz: `market_name + band_bps`) + +### `dlob-slippage-worker` (slippage vs size) + +- **Rola:** symuluje market fill po L2 dla zadanych rozmiarów (USD) i liczy `impact_bps`. +- **Źródło:** `dlob_l2_latest` +- **Zapis:** `dlob_slippage_latest` (klucz: `market_name + side + size_usd`) + +### `dlob-ts-archiver` (historia warstw) + +- **Rola:** zapisuje “timeline” dla warstw do hypertabli Timescale (historia pod UI). +- **Źródło:** `dlob_stats_latest`, `dlob_depth_bps_latest`, `dlob_slippage_latest` +- **Zapis:** `dlob_stats_ts`, `dlob_depth_bps_ts`, `dlob_slippage_ts` +- **Retencja (startowo):** ~7 dni (policy w Timescale). + +## Co UI realnie czyta (dla “nowych funkcji”) + +- live/subscriptions: + - `dlob_stats_latest` + - `dlob_depth_bps_latest` + - `dlob_slippage_latest` +- (jeśli dołączymy w UI wykres “historia”): + - `dlob_stats_ts` + - `dlob_depth_bps_ts` + - `dlob_slippage_ts` + +## Najczęstsze miejsca problemów (diagnostyka) + +- Jeśli UI nie pokazuje warstw: + - sprawdź czy Hasura trackuje tabele i ma `public select` (bootstrap job), + - sprawdź, czy workery odświeżają `updated_at` w `dlob_*_latest`. +- Jeśli `dlob-worker` loguje 503: + - to zwykle problem na ścieżce `DLOB_HTTP_URL` (upstream/LB/IPv6) — wtedy przełącz na `http://dlob-server:6969`. +- Jeśli WS subscriptions nie łączą: + - sprawdź proxy `/graphql-ws` w `trade-frontend` i origin/CORS w Hasurze. + diff --git a/doc/rpc-dlob-kanoniczna-architektura.md b/doc/rpc-dlob-kanoniczna-architektura.md new file mode 100644 index 0000000..3ed0e5b --- /dev/null +++ b/doc/rpc-dlob-kanoniczna-architektura.md @@ -0,0 +1,115 @@ +# Kanoniczna architektura Drift: własny Solana RPC + własny DLOB + +Poniżej jest krótka i konkretna notatka: **co da się wyciągnąć z własnego Solana RPC** oraz **po co jest własny DLOB** w kontekście Drift (perp) i metryk pod trading/SIM. + +## 1) Własny Solana RPC — co z niego wyciągniesz + +Z **własnego RPC** (full node, a do backfillu najlepiej archival) możesz pobrać **wszystkie dane kontowe i ryzyko**. + +### Dane pozycji i konta (RPC) + +- pozycja (long/short, size, entry) +- unrealized PnL +- realized PnL (z konta użytkownika / fills) +- margin, free collateral +- liquidation price +- health / margin ratio +- funding (naliczony + historyczny) + +Źródło: **konta programu Drift** (User, PerpMarket, SpotMarket). Technicznie: subskrypcje kont + (jeśli potrzebne) `getProgramAccounts`. + +### Fills / transakcje / fees (RPC) + +- fill price +- fee (maker/taker) +- reduce / add +- tx fee + priority fee + +Źródła: +- logi transakcji, +- eventy Drift, +- historia transakcji walleta. + +Uwaga: backfill 7d+ jest ciężki bez archival RPC, ale nadal wykonalny (koszt/IO/limity). + +### Ceny (RPC) + +- oracle price +- mark price (ze stanu rynku) + +W praktyce wystarczy RPC + subskrypcje kont. + +## 2) Własny DLOB — po co i co daje + +**DLOB jest off-chain**, ale jest budowany z **on-chain zleceń limit**. + +Co daje DLOB (i to jest kluczowe do “close now” i slippage): +- best bid / best ask (BBO) +- mid price +- spread +- realistyczny slippage +- sensowne “close now cost” (na podstawie top-of-book / L2) + +Bez DLOB zwykle zostaje heurystyka na mark/oracle + założony spread/slippage. + +### Jak to zrobić praktycznie + +Najprostsza opcja to uruchomienie serwisu DLOB (publisher/server) z Drift SDK, który: +- subskrybuje RPC/WS, +- buduje orderbook, +- wystawia API (BBO/depth itp.), +- a worker liczy metryki (spread/depth/slippage) i zapisuje je do DB. + +W tym repo mamy opis aktualnego pipeline DLOB w `doc/dlob-services.md` oraz plan “RPC + Geyser/Yellowstone” w `doc/solana-rpc-geyser-setup.md`. + +## 3) Mapowanie: metryki → źródło danych + +| Metryka | RPC | DLOB | +| --- | --- | --- | +| unrealized PnL | ✅ | ❌ | +| realized / net PnL | ✅ | ❌ | +| fees / funding / tx | ✅ | ❌ | +| margin / liq / health | ✅ | ❌ | +| time in trade | ✅ | ❌ | +| best bid / ask | ❌ | ✅ | +| spread / mid | ❌ | ✅ | +| close now cost | ⚠️ heurystyka | ✅ | +| expected slippage | ⚠️ | ✅ | + +## 4) 100% self-hosted (bez vendor lock‑in) + +Da się zrobić w pełni self-hosted (bez Heliusa/cudzych API). + +Prosty diagram: + +``` +[ Solana RPC (+ WS) ] + ↓ +[ Drift SDK / subscriptions ] + ↓ +[ DLOB (publisher/server) ] + ↓ +[ Worker (metrics TS) ] + ↓ +[ API / Monitor / SIM ] + ↓ +[ UI (tylko rysuje) ] +``` + +## 5) Jedyny realny haczyk (operacyjnie) + +- `getProgramAccounts` + websockety wymagają solidnego RPC. +- Tanie/limtowane RPC często: + - blokują/limitują GPA, + - ucinają payload, + - dropią WS. + +Własny RPC = stabilność i przewidywalność na większej skali. + +## 6) TL;DR + +- Tak: wyciągniesz wszystko z własnego RPC + własnego DLOB. +- RPC = pozycja, PnL, ryzyko, funding. +- DLOB = bid/ask, spread, slippage, close-now. +- To pasuje idealnie pod scalping + SIM (backend liczy, UI tylko wyświetla). + diff --git a/doc/rpc/topol.html b/doc/rpc/topol.html new file mode 100644 index 0000000..37ea3d2 --- /dev/null +++ b/doc/rpc/topol.html @@ -0,0 +1,211 @@ + + +
+ + ++ Own Solana RPC + Own Drift DLOB (Orderbook). Main rule: + keep the RPC box lean, put “trading services” on your second VPS. + Target: min 10 markets +
++ Yes — you can build a professional Drift trading stack with your own Solana RPC + your own DLOB, + but you’ll want a few supporting services around them. The main rule: + keep the RPC box lean, put “trading services” on your second VPS. +
++ With your own RPC, there is no per-request billing. The “cost” is: +
++ DLOB exists specifically to reduce RPC load by serving orderbook/trade views to clients + instead of every client rebuilding it from chain. +
++ For min 10 markets, expect the first scaling pressure to come from + continuous streaming + decoding + caching (DLOB + Redis + your strategy/execution), + and from your RPC’s WS load. Next step after the minimal set is usually: + better streaming (Geyser) or more RAM/NVMe depending on bottleneck. +
+