Files
trade-doc/dlob-services.md

244 lines
10 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# Serwisy DLOB na VPS (k3s / `trade-staging`)
Ten dokument opisuje aktualny, live runtime DLOB na `mevnode_bot` w namespace `trade-staging`.
## Czy legacy read-path pracuje na VPS?
Nie. W aktywnym stacku nie ma już starej ścieżki REST + backend workers.
Obecny read-path jest oparty o:
- `dlob-publisher-hot`
- `dlob-publisher-all`
- `dlob-redis`
- `dlob-hot-redis-to-postgres-raw-writer`
- `dlob-hot-postgres-to-postgres-derived-writer`
- `dlob-all-redis-to-postgres-derived-writer`
- `postgres`
- `hasura`
## Czy na VPS jest GraphQL/WS dla stats i orderbook?
Tak. GraphQL i websocket subscriptions wystawia **Hasura**, a źródłem danych są tabele pochodne w PostgreSQL.
Z zewnątrz korzystamy przez frontend (proxy) pod:
- HTTP: `https://trade.rv32i.pl/graphql`
- WS: `wss://trade.rv32i.pl/graphql`
Najważniejsze tabele live dla UI:
- `dlob_hot_derived_latest`
- `dlob_hot_derived_ts`
- `dlob_all_derived_latest`
- `dlob_all_derived_ts`
- `drift_ticks` dla wykresów
## TL;DR: kto co robi
### `dlob-publisher-hot`
- Rola: utrzymuje “żywy” DLOB dla hot-setu marketów i publikuje raw snapshoty do Redis pod `dlob-hot:*`.
- Wejście: Solana RPC/WS/gRPC z secreta `trade-dlob-rpc`, Drift SDK, `PERP_MARKETS_TO_LOAD`.
- Wyjście: bieżący stan orderbooka do `dlob-redis`.
### `dlob-hot-redis-to-postgres-raw-writer`
- Rola: zapisuje kanoniczny raw payload `hot` z Redis do PostgreSQL.
- Wejście: `dlob-hot:*`.
- Wyjście:
- `dlob_hot_snapshot_latest`
- `dlob_hot_snapshot_ts`
### `dlob-hot-postgres-to-postgres-derived-writer`
- Rola: normalizuje warstwę `hot` na derived tables dla UI i downstream processing.
- Wejście: raw hot z PostgreSQL.
- Wyjście:
- `dlob_hot_derived_latest`
- `dlob_hot_derived_ts`
### `dlob-publisher-all`
- Rola: utrzymuje szeroki, pełny feed rynku i publikuje go do Redis pod `dlob-all:*`.
- Wejście: ten sam upstream Solana/Yellowstone co `hot`, ale bez ograniczenia do shortlisty.
- Wyjście: pełny stan rynku do `dlob-redis`.
### `dlob-all-redis-to-postgres-derived-writer`
- Rola: buduje tylko warstwę pochodną dla `all`, bez przechowywania raw append-only.
- Wejście: `dlob-all:*`.
- Wyjście:
- `dlob_all_derived_latest`
- `dlob_all_derived_ts`
### `dlob-redis`
- Rola: szybki cache i punkt wymiany danych między publisherami i writerami.
- Uwagi: to nie jest trwałe źródło prawdy; trwałość daje PostgreSQL.
### `postgres` + `hasura`
- `postgres`: trwałe składowanie raw `hot`, derived `hot/all`, `drift_ticks`, tokenów i danych aplikacyjnych.
- `hasura`: query + subscription plane dla UI.
### `trade-ingestor`
- Rola: zasila `drift_ticks` i ścieżkę wykresów.
- Uwaga: to osobny app-ingest path, nie zamiennik dla DLOB publisherów.
## Jak to się spina (przepływ danych)
1. `dlob-publisher-hot` i `dlob-publisher-all` budują live state z prywatnego RPC/WS/gRPC i publikują snapshoty do `dlob-redis`.
2. `dlob-hot-redis-to-postgres-raw-writer` zapisuje raw `hot` do PostgreSQL.
3. `dlob-hot-postgres-to-postgres-derived-writer` tworzy znormalizowaną warstwę `hot`.
4. `dlob-all-redis-to-postgres-derived-writer` tworzy znormalizowaną warstwę `all`.
5. `hasura` wystawia derived tables jako GraphQL/WS dla UI.
6. `trade-api` i `trade-ingestor` obsługują osobno chart/ticks path.
## Co UI i API biorą z tej warstwy
- DLOB / L2 / stats: z `dlob_hot_derived_*` i `dlob_all_derived_*` przez Hasurę.
- Chart / ticks: z `drift_ticks` przez `trade-api`.
- Metryki depth/slippage nie są już osobnymi workerami w k3s; są liczone z danych L2/derived w aktualnej ścieżce aplikacji.
## Co to jest L1 / L2 / L3 (orderbook)
- `L1`: najlepszy bid i ask.
- `L2`: zagregowane poziomy cenowe `{ price, size }`.
- `L3`: pojedyncze zlecenia.
W obecnym stacku live warstwa aplikacyjna korzysta przede wszystkim z pochodnej reprezentacji L2 zapisanej w PostgreSQL (`bids_norm`, `asks_norm` i metryki top-of-book), a nie z osobnego REST serwera DLOB.
## Szybka diagnostyka na VPS
```bash
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n trade-staging get deploy | grep -E 'trade-|dlob-|hasura'
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n trade-staging logs deploy/dlob-publisher-hot --tail=80
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n trade-staging logs deploy/dlob-publisher-all --tail=80
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n trade-staging logs deploy/dlob-hot-redis-to-postgres-raw-writer --tail=80
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n trade-staging logs deploy/dlob-hot-postgres-to-postgres-derived-writer --tail=80
KUBECONFIG=/etc/rancher/k3s/k3s.yaml kubectl -n trade-staging logs deploy/dlob-all-redis-to-postgres-derived-writer --tail=80
```
## Słownik pojęć (bid/ask/spread i metryki)
### Podstawy orderbooka
- **Bid**: zlecenia kupna (chęć kupna). W orderbooku “bid side”.
- **Ask**: zlecenia sprzedaży (chęć sprzedaży). W orderbooku “ask side”.
- **Best bid / best ask**: najlepsza (najwyższa) cena kupna i najlepsza (najniższa) cena sprzedaży na topie księgi (L1).
- **Spread**: różnica pomiędzy `best_ask` a `best_bid`. Im mniejszy spread, tym “taniej” wejść/wyjść (mniej kosztów natychmiastowej realizacji).
- **Mid price**: cena “po środku”: `(best_bid + best_ask) / 2`. Używana jako punkt odniesienia do bps i slippage.
- **Level**: pojedynczy poziom cenowy w L2 (np. `price=100.00`, `size=12.3`).
- **Size**: ilość/płynność na poziomie (zwykle w jednostkach “base asset”).
- **Base / Quote**:
- `base` = instrument bazowy (np. SOL),
- `quote` = waluta wyceny (często USD).
## Kolory w UI (visualizer)
- `bid` / “buy side” = zielony (`.pos`, `#22c55e`)
- `ask` / “sell side” = czerwony (`.neg`, `#ef4444`)
- “flat” / brak zmiany = niebieski (`#60a5fa`) — używany m.in. w “brick stack” pod świecami
### Jednostki i skróty
- **bps (basis points)**: 1 bps = 0.01% = `0.0001`. Np. 25 bps = 0.25%.
- **USD**: u nas wiele wartości jest przeliczanych do USD (np. `size_base * price`).
### Metryki “stats” (np. `dlob_stats_latest`)
- `spread_abs` (USD): `best_ask - best_bid`.
- `spread_bps` (bps): `(spread_abs / mid_price) * 10_000`.
- `depth_levels`: ile leveli (topN) z każdej strony braliśmy do liczenia “depth”.
- `depth_bid_base` / `depth_ask_base`: suma `size` po topN levelach bid/ask (w base).
- `depth_bid_usd` / `depth_ask_usd`: suma `size_base * price` po topN levelach (w USD).
- `imbalance` ([-1..1]): miara asymetrii płynności:
- `(depth_bid_usd - depth_ask_usd) / (depth_bid_usd + depth_ask_usd)`
- >0 = relatywnie więcej płynności po bid, <0 = po ask.
- `oracle_price`: cena z oracla (np. Pyth) jako punkt odniesienia.
- `mark_price`: mark z rynku/perp (cena referencyjna dla rozliczeń); różni się od oracle/top-of-book.
### Metryki “depth bands” (np. `dlob_depth_bps_latest`)
- `band_bps`: szerokość pasma wokół `mid_price` (np. 5/10/20/50/100/200 bps).
- `bid_usd` / `ask_usd`: płynność po danej stronie, ale **tylko z poziomów mieszczących się w oknie ±`band_bps`** wokół mid.
- `imbalance`: jak wyżej, ale liczony per band.
### Metryki “slippage” (np. `dlob_slippage_latest`)
To jest symulacja gdybym teraz zrobił market order o rozmiarze X na podstawie L2.
- `size_usd`: docelowy rozmiar zlecenia w USD.
- `vwap_price`: średnia cena realizacji (Volume Weighted Average Price) dla symulowanego fill.
- `impact_bps`: koszt/odchylenie względem `mid_price` wyrażone w bps (zwykle na bazie `vwap` vs `mid`).
- `worst_price`: najgorsza cena dotknięta podczas zjadania kolejnych leveli.
- `filled_usd` / `filled_base`: ile realnie udało się wypełnić (może być < docelowego, jeśli brakuje płynności).
- `fill_pct`: procent wypełnienia (100% = pełny fill).
- `levels_consumed`: ile leveli zostało zjedzonych podczas fill.
### Metadane czasu (“świeżość”)
- `ts`: timestamp źródła (czas snapshotu).
- `slot`: slot Solany, z którego pochodzi snapshot (monotoniczny numer czasu chaina).
- `updated_at`: kiedy nasz pipeline zapisał/odświeżył rekord w DB (do oceny, czy dane świeże).
## Redis retention na poziomie k3s i Redisa
Dla `mevnode-bot` retencja Redis jest ustawiana na poziomie `k3s` przez cleaner `CronJob`, a nie przez natywny `TTL` kluczy.
Powod:
- publishery DLOB zapisują do Redis w modelu `latest-only`
- kolejne `SET` nadpisuje poprzedni snapshot tego samego klucza
- zwykly Redis `TTL` bylby kasowany przez kolejne zapisy, wiec nie daje poprawnej retencji semantycznej
Ustawienie operacyjne:
- `hot` (`dlob-hot:`): retencja `15m`
- `all` (`dlob-all:`): retencja `1h`
- cleaner uruchamiany z `CronJob` co `5m`
Sposob dzialania cleanera:
- skanuje klucze `dlob-hot:*` oraz `dlob-all:*`
- odczytuje `ts`, a jesli go nie ma to `updatedAtTs`, z payloadu JSON
- usuwa tylko klucze starsze niz zadany prog wieku
To oznacza:
- aktywne markety pozostaja stale obecne w Redisie
- osierocone klucze po zmianie konfiguracji znikaja automatycznie
- Redis pozostaje warstwa `raw latest snapshot`, bez normalizacji payloadu
Aktualny stan Redis, istotny dla tej decyzji:
- klucze nie maja automatycznej expiracji (`TTL=-1`)
- `maxmemory-policy=noeviction`
- `aof_enabled=0`
Aktualny stan publisherow na poziomie k3s:
- `hot` i `all` sa obecnie rozrozniane przez markety, `REDIS_CLIENT`, `DLOB_SOURCE` i persistent store
- maja osobne `priorityClassName`
- `resources.requests/limits` sa ustawione dla writerow `hot`; publishery maja rozroznienie priorytetow na poziomie klastra
## Priorytety na poziomie k3s
Na poziomie `k3s` rozroznienie `hot` i `all` jest ustawione przez osobne `PriorityClass`.
Parametry:
- `hot`:
- deployment: `dlob-publisher-hot`
- `priorityClassName: dlob-hot`
- wartosc `PriorityClass`: `100000`
- `all`:
- deployment: `dlob-publisher-all`
- `priorityClassName: dlob-all`
- wartosc `PriorityClass`: `50000`
Znaczenie operacyjne:
- `hot` ma wyzszy priorytet schedulera i preemption niz `all`
- przy presji na node `hot` powinien byc traktowany jako sciezka krytyczna
- `all` pozostaje feedem nizszego priorytetu dla pelnego coverage rynku
Zakres tej parametryzacji:
- to jest tylko priorytet na poziomie schedulera `k3s`
- nie ustawia jeszcze `resources.requests` ani `resources.limits`
- nie zmienia logiki publishera, tylko kolejnosc traktowania workloadu przez klaster