# 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