244 lines
10 KiB
Markdown
244 lines
10 KiB
Markdown
# 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 (top‑N) z każdej strony braliśmy do liczenia “depth”.
|
||
- `depth_bid_base` / `depth_ask_base`: suma `size` po top‑N levelach bid/ask (w base).
|
||
- `depth_bid_usd` / `depth_ask_usd`: suma `size_base * price` po top‑N 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 są ś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
|