From 0d3d11002607faa2b3400bed6297cca505924686 Mon Sep 17 00:00:00 2001 From: u1 Date: Sun, 29 Mar 2026 13:14:30 +0200 Subject: [PATCH] docs(snapshot): sync workspace documentation --- 3700x.md | 204 +++++ agave-sync-recipe.md | 299 +++++++ bot-control-plane-staging-v1.md | 151 ++++ bot-microservices-hasura-fastify.md | 209 +++++ bot-strategy-reversal.md | 142 ++++ bot/bot1.tex | 500 ++++++++++++ bots.md | 359 ++++++++ data-ingest-strategy.md | 344 ++++++++ dlob-basics.md | 184 +++++ dlob-retention-feature-store-plan.md | 236 ++++++ dlob-services.md | 243 ++++++ drift-perps-monitoring.md | 54 ++ economic-markers-k3s.md | 533 ++++++++++++ frontent-local-run.md | 205 +++++ gitea-repo-cleanup-plan.md | 275 +++++++ gitea-tree.md | 143 ++++ mevnode-bot-architecture.md | 245 ++++++ mevnode-bot-topology-interactive.html | 1082 +++++++++++++++++++++++++ mevnode-bot-topology.html | 424 ++++++++++ migration.md | 68 +- momentum-service-v1.md | 156 ++++ observer-right-rail-v1.md | 126 +++ observer-sol-v1.md | 240 ++++++ observer-visualizer-v1.md | 192 +++++ rpc/README.md | 168 ++++ rpc/baremetal-iac.md | 71 ++ rpc/baremetal-monitoring.md | 136 ++++ rpc/bot-executor-api.md | 121 +++ rpc/geyser-dlob.md | 79 ++ rpc/software-placement.md | 89 ++ rpc/topol.html | 232 ++++++ stats.md | 158 ++++ steps.md | 85 +- todo.md | 37 + trade-system-flow.html | 692 ++++++++++++++++ visualizer-candles.md | 42 + workflow.md | 99 +++ 37 files changed, 8500 insertions(+), 123 deletions(-) create mode 100644 3700x.md create mode 100644 agave-sync-recipe.md create mode 100644 bot-control-plane-staging-v1.md create mode 100644 bot-microservices-hasura-fastify.md create mode 100644 bot-strategy-reversal.md create mode 100644 bot/bot1.tex create mode 100644 bots.md create mode 100644 data-ingest-strategy.md create mode 100644 dlob-basics.md create mode 100644 dlob-retention-feature-store-plan.md create mode 100644 dlob-services.md create mode 100644 drift-perps-monitoring.md create mode 100644 economic-markers-k3s.md create mode 100644 frontent-local-run.md create mode 100644 gitea-repo-cleanup-plan.md create mode 100644 gitea-tree.md create mode 100644 mevnode-bot-architecture.md create mode 100644 mevnode-bot-topology-interactive.html create mode 100644 mevnode-bot-topology.html create mode 100644 momentum-service-v1.md create mode 100644 observer-right-rail-v1.md create mode 100644 observer-sol-v1.md create mode 100644 observer-visualizer-v1.md create mode 100644 rpc/README.md create mode 100644 rpc/baremetal-iac.md create mode 100644 rpc/baremetal-monitoring.md create mode 100644 rpc/bot-executor-api.md create mode 100644 rpc/geyser-dlob.md create mode 100644 rpc/software-placement.md create mode 100644 rpc/topol.html create mode 100644 stats.md create mode 100644 todo.md create mode 100644 trade-system-flow.html create mode 100644 visualizer-candles.md create mode 100644 workflow.md diff --git a/3700x.md b/3700x.md new file mode 100644 index 0000000..f2af6ab --- /dev/null +++ b/3700x.md @@ -0,0 +1,204 @@ +# 3700X jako drugi bare metal dla Drift perps + +Ten dokument opisuje sensowną rolę drugiego hosta: + +- `AMD Ryzen 7 3700X` +- `128 GB RAM` +- `2 x 1 TB NVMe` +- `1 Gbps` + +W tym układzie zakładamy, że: + +- host `9950X` robi tylko `Agave + Yellowstone Geyser`, +- host `3700X` robi `consumer + processing + lekka/umiarkowana baza`, +- komunikacja między hostami idzie po prywatnej sieci (`private VLAN` albo później `wg0`). + +## Cel + +Rozdzielić role: + +- `9950X`: ingest danych z Solany, bez ciężkiej obróbki +- `3700X`: odbiór streamu, dekodowanie Drift, budowa DLOB, zapis pochodnych danych, API i worker-y + +To zmniejsza ryzyko, że: + +- baza danych, +- parser, +- albo batch/worker + +zwiększą lag walidatora. + +## Co uruchamiać na 3700X + +Tak: + +- Yellowstone gRPC consumer +- parser Drift +- cache stanu w RAM +- worker-y realtime +- Redis / NATS / lekka kolejka +- Postgres o umiarkowanej skali +- read-only API / query layer +- monitoring + +Nie: + +- `agave-validator` +- ciężkie historyczne archiwum raw streamu +- duży ClickHouse na start +- wszystko naraz bez limitów retencji + +## Układ usług + +Przepływ: + +1. `9950X` uruchamia `Agave + Geyser plugin` +2. `3700X` łączy się po prywatnym adresie do `gRPC` +3. consumer odbiera tylko potrzebny scope Drift +4. parser buduje lokalny stan +5. do DB trafiają tylko dane pochodne i potrzebne eventy + +Model: + +- host A = producer +- host B = consumer + processing + +## Dyski + +Najprostszy podział: + +- `nvme0`: + - system + - Docker + - binarki + - cache buildów +- `nvme1`: + - dane consumera + - Postgres + - logi aplikacyjne + - kolejki / spool + +Filesystem: + +- `ext4` na obu dyskach + +## Minimalny scope Yellowstone pod Drift perps + +Na start nie streamuj całej Solany. Dla Drift perps minimalny sensowny zakres to: + +- `slots` +- `transactions` tylko dla programu Drift +- `accounts` dla kont Drift potrzebnych do stanu rynku i userów + +## Konta i strumienie + +Na start streamuj: + +- `State` +- `PerpMarketAccount` dla wybranych rynków +- `SpotMarketAccount` potrzebne do collateral / quote +- `UserAccount` + +Powód: + +- Drift trzyma zlecenia i pozycje w `UserAccount` +- lokalny `DLOB` buduje się właśnie z `UserAccount` +- `PerpMarketAccount` daje stan rynku +- `SpotMarketAccount` jest potrzebny dla cross-margin + +## Filtry v1 + +Najrozsądniejsza wersja startowa: + +- `slots`: włączone +- `transactions`: + - `account_include = [Drift Program ID]` + - bez vote tx +- `accounts`: + - exact accounts dla `State` i marketów + - `UserAccount` dla programu Drift + +Jeśli interesują Cię tylko wybrane rynki, np. `SOL-PERP`, `BTC-PERP`, `ETH-PERP`, to: + +- odbierasz `UserAccount`, +- po dekodzie odrzucasz userów bez pozycji i bez orderów na tych rynkach + +To jest bezpieczniejsze operacyjnie niż próba zbyt agresywnego filtrowania przed dekodem. + +## Co liczyć po stronie consumera + +W RAM trzymaj: + +- cache `UserAccount` +- cache `PerpMarketAccount` +- cache `SpotMarketAccount` +- ostatni slot + +Z tego licz: + +- lokalny `DLOB` +- best bid / ask +- depth +- zmiany pozycji +- fills +- liquidations +- funding / mark / oracle impact + +Do DB zapisuj: + +- eventy biznesowe +- snapshoty pochodne +- agregaty + +Nie zapisuj na start całego raw streamu 1:1. + +## Co daje taki układ + +- mniejszy wpływ processingu na lag walidatora +- bezpieczny transport po prywatnej sieci +- prostsza diagnostyka +- możliwość restartu parsera bez ruszania noda +- łatwiejsze skalowanie w przyszłości + +## Ograniczenia 3700X + +Ten host jest dobry jako: + +- consumer node +- processing node +- umiarkowana DB + +To nie jest idealny host pod: + +- bardzo ciężki ClickHouse +- duże historyczne archiwum +- wiele ciężkich pipeline'ów naraz + +Najbardziej ograniczą Cię: + +- `1 Gbps` +- pojemność `2 x 1 TB` +- RAM przy większej retencji i większej liczbie workerów + +## Rekomendacja startowa + +Na `3700X` uruchom: + +- Yellowstone consumer +- Drift parser +- Redis +- Postgres +- lekkie API + +Na `9950X` zostaw tylko: + +- `Agave` +- `Geyser` + +Najpierw uruchom zakres dla 2-3 rynków perp i dopiero potem rozszerzaj. + +## Powiązane dokumenty + +- `doc/rpc/software-placement.md` +- `doc/rpc/geyser-dlob.md` +- `doc/dlob-services.md` diff --git a/agave-sync-recipe.md b/agave-sync-recipe.md new file mode 100644 index 0000000..008a11d --- /dev/null +++ b/agave-sync-recipe.md @@ -0,0 +1,299 @@ +# Agave Sync Recipe + +Data: 2026-03-13 +Aktualizacja: 2026-03-15 + +Ten plik opisuje praktyczna recepture rozruchu Agave na `mevnode_sol` w dwoch trybach: + +- `catch-up mode`: celem jest dogonienie sieci +- `product mode`: celem jest obsluga workloadow RPC i Yellowstone dla reszty systemu + +## Plik sterujacy + +Aktualny start validatora jest skladany przez: + +- `/usr/local/bin/agave-validator.sh` + +Usluga systemd: + +- `agave-validator.service` + +## Catch-up Mode + +Stan wdrozony na `mevnode_sol` 2026-03-15 podczas przejscia na `Agave 3.1.9`. + +Cel: + +- maksymalnie odchudzic runtime podczas syncu +- nie wystawiac Yellowstone +- nie budowac ciezkiego profilu RPC + +Wlaczone: + +- `--private-rpc` +- `--rpc-bind-address 127.0.0.1` +- `--rpc-port 8899` +- `--accounts-db-cache-limit-mb 32768` +- `--no-voting` +- `--limit-ledger-size 50000000` +- `--minimal-snapshot-download-speed 25000000` +- `--maximum-snapshot-download-abort 8` +- `--use-snapshot-archives-at-startup always` +- `--known-validator 7mF8NZJdREuM1uwYcvKffuY9QJBEoHhNp4hZ4NS2fuXW` +- `--only-known-rpc` +- dodatkowy `--entrypoint 51.89.11.213:8001` dla Frankfurtu + +Wylaczone: + +- `--geyser-plugin-config /etc/agave/geyser.json` +- `--full-rpc-api` +- `--enable-accounts-disk-index` +- `--accounts-index-path /data/ledger/accounts_index` +- `--account-index ...` + +Wazne: + +- `--accounts-index-path` nie moze zostac, jesli `--enable-accounts-disk-index` jest wylaczone +- zostawienie samego `--accounts-index-path` powoduje start failure Agave + +## Product Mode + +To jest profil po pelnym syncu, kiedy node ma znowu obslugiwac reszte systemu. +Stan finalny z 2026-03-15: + +Wlaczone: + +- `--geyser-plugin-config /etc/agave/geyser.json` +- `--full-rpc-api` +- `--enable-accounts-disk-index` +- `--accounts-db-cache-limit-mb 32768` +- `--maximum-full-snapshots-to-retain 1` +- `--maximum-incremental-snapshots-to-retain 1` +- `--use-snapshot-archives-at-startup when-newest` +- `--known-validator 7mF8NZJdREuM1uwYcvKffuY9QJBEoHhNp4hZ4NS2fuXW` +- `--only-known-rpc` + +Cel: + +- Yellowstone dla `mevnode_bot` +- ciezsze metody RPC +- nizszy swap niz przy czysto pamieciowym accounts index + +Uwaga: + +- w aktualnym wdrozeniu nie ma `--accounts-index-path` +- w aktualnym wdrozeniu nie ma `--account-index program-id` +- samo `--enable-accounts-disk-index` zmniejsza presje na swap, ale nie przyspiesza ciezkich `getProgramAccounts` +- retencja snapshotow `1/1` nie jest tutaj opcjonalna; to jest kluczowy bezpiecznik operacyjny + +## Dlaczego retencja `1/1` jest krytyczna + +Na `mevnode_sol` katalog `/data/state` jest jednym filesystemem `2 TB`, na ktorym siedza jednoczesnie: + +- `accounts` +- `accounts disk index` +- archiwa snapshotow +- tymczasowe `tmp-snapshot-archive-*` + +Przy profilu: + +- `--full-rpc-api` +- `--enable-accounts-disk-index` +- `--geyser-plugin-config /etc/agave/geyser.json` + +sam katalog `accounts` potrafi zajmowac ponad `1 TB`, a snapshoty i pliki tymczasowe potrafia dopchnac `/data/state` do `100%`. + +Wniosek praktyczny: + +- `--maximum-full-snapshots-to-retain 1` +- `--maximum-incremental-snapshots-to-retain 1` + +to nie jest tylko "porzadek na dysku", tylko warunek utrzymania zapasu miejsca na: + +- prace `accounts disk index` +- tworzenie nowego snapshotu +- restart z lokalnego full + incremental + +Bez tego Agave moze dojsc do `ENOSPC`, przestac odpowiadac po RPC albo wymusic awaryjny cleanup przed restartem. + +## Dlaczego nie 8 GB + +W tej sesji nie bylo twardych danych, ze samo `8 GB` rozwiazuje problem. Przy `32 GB` host nie byl pod presja pamieci: + +- RAM hosta: okolo `186 GiB` +- available memory: okolo `143 GiB` +- swap praktycznie nieuzywany + +Problem wygladal bardziej na zbyt ciezki profil catch-up niz na brak RAM. + +## Obserwacje z 2026-03-13 + +Przed przelaczeniem na `catch-up mode`: + +- `geyser` byl wylaczony +- nadal wlaczone byly `--full-rpc-api` i `--enable-accounts-disk-index` +- node nie nadganial heada; lokalny slot rosl wolniej niz publiczny mainnet + +Po przelaczeniu na `catch-up mode`: + +- Agave wystartowalo poprawnie bez `full-rpc-api` +- Agave wystartowalo poprawnie bez `accounts-disk-index` +- aktywny proces nie ma juz `geyser`, `full-rpc-api` ani `accounts-disk-index` + +## Kolejnosc wdrozenia z 2026-03-15 + +Poniżej jest praktyczna kolejnosc, ktora byla wykonana na `mevnode_sol`. + +1. Zatrzymac usluge: + +```bash +sudo systemctl stop agave-validator.service +``` + +2. Wyczyscic `ledger` i `state`: + +```bash +sudo rm -rf /data/ledger/* +sudo rm -rf /data/state/* +``` + +3. Zbudowac i podmienic Agave na `3.1.9`: + +- build z taga `v3.1.9` +- instalacja do `/opt/agave-v3.1.9` +- przelaczenie `/opt/agave -> /opt/agave-v3.1.9` + +4. Wystartowac lekki `catch-up mode`: + +- bez `geyser` +- bez `--full-rpc-api` +- bez `--enable-accounts-disk-index` +- bez `--accounts-index-path` +- z `--use-snapshot-archives-at-startup always` + +5. Wymusic szybki source snapshotu z Frankfurtu: + +- `--known-validator 7mF8NZJdREuM1uwYcvKffuY9QJBEoHhNp4hZ4NS2fuXW` +- `--only-known-rpc` +- `--entrypoint 51.89.11.213:8001` + +6. Poczekac az `catch-up mode` dojdzie do `getHealth=ok`. + +7. Zmienic startup snapshotow z `always` na `when-newest`. + +Powod: + +- przy `always` kazdy restart wymuszal ladowanie z archive +- `when-newest` zostawia Agave mozliwosc uzycia nowszego archive, ale nie wymusza tej sciezki przy kazdym restarcie + +8. Wlaczyc `--full-rpc-api` i zrestartowac. + +9. Po ponownym syncu wlaczyc `--enable-accounts-disk-index` i zrestartowac. + +Powod: + +- przy `full-rpc` bez `accounts-disk-index` swap byl praktycznie zapychany +- po wlaczeniu `accounts-disk-index` swap spadl prawie do zera + +10. Po kolejnym syncu wlaczyc `geyser`. + +Praktycznie: + +- dopiac `--geyser-plugin-config /etc/agave/geyser.json` +- upewnic sie, ze `libpath` z configu istnieje +- na `mevnode_sol` trzeba bylo utworzyc symlink: + +```bash +sudo mkdir -p /opt/agave/plugins +sudo ln -sfn /opt/agave.live-3.1.10-20260314-0903/plugins/v12.1.0+solana.3.1.10 /opt/agave/plugins/current +``` + +11. Po wlaczeniu geysera zweryfikowac: + +- `10.91.0.1:10000` nasluchuje +- `10.91.0.1:8999/metrics` odpowiada +- w logu jest: + - `Geyser plugin: accounts_update_notifier: true, transaction_notifier: true, entry_notifier: true` + +12. Na koncu ustawic retencje snapshotow: + +- `--maximum-full-snapshots-to-retain 1` +- `--maximum-incremental-snapshots-to-retain 1` + +Cel: + +- ograniczyc wzrost `/data/state/snapshots` +- zostawic minimalny zestaw do restartu +- nie dopuscic do zapchania `2 TB` `/data/state`, na ktorym siedza tez `accounts` i `accounts disk index` + +13. Po syncu usuwac tylko stare snapshoty, nie ruszajac: + +- aktualnego full snapshotu +- najnowszego ukonczonego incremental +- aktywnego `tmp-snapshot-archive-*` + +To byl praktyczny cleanup, ktory odzyskal kilkaset GB, bez psucia restart path. + +## Jak sprawdzac stan + +Jednym poleceniem snapshot kluczowych metryk: + +```bash +./scripts/ops/agave-metrics.sh mevnode +``` + +Skrypt zbiera: + +- health RPC i lokalny slot +- publiczny head i `lag_slots` +- porty `8899` / `8999` / `10000` +- `rss_mb`, `pcpu`, `pmem` +- `mem_available_mb`, `swap_used_mb` +- `vm_us`, `vm_sy`, `vm_id`, `vm_wa` +- ostatnie `rcvbuf_errors_delta_recent` + +Status uslugi: + +```bash +ssh mevnode 'systemctl --no-pager --full status agave-validator.service | sed -n "1,20p"' +``` + +Czy Yellowstone jest wylaczony: + +```bash +ssh mevnode 'ss -ltnp | grep -E ":(8899|10000|8999)\\b" || true' +``` + +Szybki health: + +```bash +ssh mevnode "printf '%s' '{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getHealth\"}' | curl -sS -H 'Content-Type: application/json' -d @- http://10.91.0.1:8899" +``` + +Pomiar, czy node nadgania: + +```bash +ssh mevnode 'for i in 1 2 3; do ts=$(date +%s); pjson=$(printf "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getSlot\",\"params\":[{\"commitment\":\"processed\"}]}" ); pubjson=$(printf "{\"jsonrpc\":\"2.0\",\"id\":1,\"method\":\"getSlot\"}" ); p=$(curl -sS -m 6 -H "Content-Type: application/json" -d "$pjson" http://10.91.0.1:8899 | sed -n "s/.*\"result\":\\([0-9][0-9]*\\).*/\\1/p"); pub=$(curl -sS -m 6 -H "Content-Type: application/json" -d "$pubjson" https://api.mainnet-beta.solana.com | sed -n "s/.*\"result\":\\([0-9][0-9]*\\).*/\\1/p"); echo "$ts $p $pub"; sleep 10; done' +``` + +Interpretacja: + +- jesli lokalny `processed` rosnie szybciej niz publiczny head, node nadgania +- jesli lokalny `processed` rosnie wolniej, lag dalej narasta + +## Re-enable Po Sync + +Nie wlaczac wszystkiego naraz. + +Kolejnosc rekomendowana: + +1. `--full-rpc-api` +2. `--enable-accounts-disk-index` +3. `--geyser-plugin-config /etc/agave/geyser.json` +4. retencja snapshotow `1/1` + +Po kazdym etapie: + +- restart `agave-validator.service` +- poczekac na `getHealth=ok` +- dopiero potem przechodzic do nastepnego etapu diff --git a/bot-control-plane-staging-v1.md b/bot-control-plane-staging-v1.md new file mode 100644 index 0000000..199407b --- /dev/null +++ b/bot-control-plane-staging-v1.md @@ -0,0 +1,151 @@ +# Bot Control Plane Staging V1 + +Ten dokument opisuje minimalny deploy path potrzebny, żeby `SOL-PERP observer` był widoczny w stagingowym UI i w `trade-api`. + +## Cel + +Zapewnić, że środowisko `trade-staging` ma: +- fizyczne tabele `bot_config`, `bot_state`, `bot_events` w Postgresie +- tracking tych tabel w Hasurze +- możliwość odczytu przez `trade-api` + +Bez tego frontend może tylko pokazać fallback: +- `Bot control plane is not deployed here` + +## Zakres V1 + +V1 nie wdraża jeszcze seedów ani samego procesu observera. + +V1 obejmuje tylko: +- schema SQL +- metadata Hasury +- gotowość `trade-api` do odczytu control-plane + +## Obiekty bazy + +Wymagane są trzy tabele: + +### `bot_config` + +Desired state bota: +- `name` +- `market_name` +- `market_type` +- `mode` +- `kill_switch` +- `params` + +### `bot_state` + +Heartbeat i ostatni stan runtime: +- `last_heartbeat_at` +- `last_action_at` +- `last_error` +- `state` +- `updated_at` + +### `bot_events` + +Append-only audit log: +- `ts` +- `type` +- `payload` + +## Hasura + +Hasura musi trackować: +- `public.bot_config` +- `public.bot_state` +- `public.bot_events` + +W V1 nie wymagamy publicznych permissionów do bezpośredniego odczytu z frontendu, bo panel bota czyta przez `trade-api`. + +## Integracja z `trade-api` + +`trade-api` czyta te tabele przez admin secret Hasury i wystawia: +- `GET /v1/bots` +- `GET /v1/bots/:id` +- `GET /v1/bots/:id/state` +- `GET /v1/bots/:id/events` + +## Kryterium done + +Po wdrożeniu staging powinien spełniać: + +1. Hasura zna `bot_config`, `bot_state`, `bot_events`. +2. `trade-api /v1/bots` zwraca `200` bez fallbacku `bot_control_plane_schema_missing`. +3. Frontend nie pokazuje już komunikatu o braku control-plane z powodu schema gap. + +## Następny krok po V1 + +Po tej zmianie można dopiero sensownie zrobić: + +1. seed `bot_config` dla `SOL-PERP observer` +2. uruchomienie `bot-observer` +3. podgląd realnego stanu w UI + +## Seed `SOL-PERP observer` + +Minimalny seed dla pierwszego observera: + +```json +{ + "name": "sol-observer", + "market_name": "SOL-PERP", + "market_type": "perp", + "mode": "observe", + "kill_switch": false, + "params": { + "strategy": { + "type": "predictive_observer_v1" + }, + "loop": { + "decision_interval_ms": 1000, + "candle_bucket_seconds": 1, + "candles_limit": 64 + }, + "features": { + "mom_fast_s": 3, + "mom_mid_s": 10, + "mom_slow_s": 30, + "vol_window_s": 30, + "depth_band_bps": 10, + "slippage_size_usd": 500 + }, + "gates": { + "freshness_max_ms": 800, + "spread_max_bps": 8, + "slippage_max_bps": 12, + "depth_band_min_usd": 3000 + }, + "sizing": { + "target_notional_usd": 500 + }, + "decision": { + "threshold": 1.2, + "horizon_s": 60 + }, + "scoring": { + "weights": { + "mom_fast": 0.9, + "mom_mid": 0.35, + "mom_slow_reversal": 0.55, + "imbalance": 5.0, + "mark_vs_oracle": 0.08, + "spread": 0.15, + "slippage": 0.12 + } + } + } +} +``` + +Po wdrożeniu schema + metadata seed można wstawić przez `trade-api`: + +```bash +curl -sS \ + -H 'content-type: application/json' \ + -H 'x-admin-secret: YOUR_API_ADMIN_SECRET' \ + --data @seed-sol-observer.json \ + http://localhost:8787/v1/bots +``` diff --git a/bot-microservices-hasura-fastify.md b/bot-microservices-hasura-fastify.md new file mode 100644 index 0000000..80a04d2 --- /dev/null +++ b/bot-microservices-hasura-fastify.md @@ -0,0 +1,209 @@ +# Bot Microservices: Hasura + Fastify + +Ten dokument jest kontraktem architektonicznym dla nowych serwisow bota. + +## Zasady + +1. Zawsze najpierw powstaje dokument kontraktu serwisu, potem implementacja. +2. Hasura jest jedynym source of truth dla danych i stanu wspoldzielonego. +3. Fastify jest shell-em mikroserwisu: HTTP, health, readiness, trigger, observability. +4. Logika domenowa nie siedzi w Fastify handlerach. Jest w czystych modulach `processing`. +5. Serwisy maja byc male i wyspecjalizowane. Kazdy liczy jeden skladnik decyzji albo jedna spojna klase skladnikow. +6. Serwis nie staje sie drugim systemem stanu. Jesli wynik ma byc wspoldzielony, zapisujemy go do Hasury albo wystawiamy przez endpoint. +7. Podzial na mikroserwisy robimy po granicach domenowych i kontraktach danych, nie po pomocniczych funkcjach. + +## Model runtime + +Kazdy serwis ma ten sam ogolny przeplyw: + +`Hasura GraphQL -> input adapter -> processing -> output adapter -> Fastify endpoint` + +Warstwy: + +- `inputs/hasura` + - zapytania GraphQL + - mapowanie rekordow Hasury do lokalnych struktur +- `processing` + - czyste funkcje liczace cechy, score, klasyfikacje, gating +- `outputs/hasura` + - zapis wyniku, eventu, state snapshotu +- `transport/http` + - Fastify + - endpointy health, ready, latest result, admin trigger +- `orchestration` + - petla, harmonogram, retry, timeouty, kolejnosc krokow + +## Rola Fastify + +Fastify nie jest miejscem na model matematyczny. + +Fastify odpowiada za: + +- `GET /healthz` +- `GET /readyz` +- `GET /result/latest` +- `GET /result/:id` +- `POST /run` +- start i stop serwisu +- logowanie i metryki + +Fastify moze odpalac processing synchronicznie albo przez worker/job, ale sama logika ma pozostac poza transportem. + +## Rola Hasury + +Hasura jest source of truth dla: + +- konfiguracji bota +- snapshotow i tabel pochodnych +- wynikow i eventow serwisow +- historii potrzebnej do debugowania i replay + +Mikroserwis: + +- czyta z Hasury +- liczy wynik +- opcjonalnie zapisuje wynik do Hasury +- opcjonalnie wystawia ten wynik przez HTTP + +Nie trzymamy osobnego "prawdziwego" stanu tylko w RAM mikroserwisu. + +## Granica mikroserwisu + +Dobry mikroserwis: + +- ma jeden glowny kontrakt wejsciowy +- ma jeden glowny wynik +- ma jasny owner danych +- da sie niezaleznie uruchomic i przetestowac + +Zly mikroserwis: + +- laczy wiele niespojnych modeli w jednym procesie +- ma wiele roznych odpowiedzialnosci +- staje sie nowym centrum stanu + +## Proponowany podzial + +Minimalne serwisy obliczeniowe: + +1. `bot-config-service` + - czyta konfiguracje z Hasury + - wystawia znormalizowany kontrakt configu + +2. `market-snapshot-service` + - czyta candles i derived DLOB z Hasury + - wystawia ujednolicony snapshot rynku + +3. `momentum-service` + - liczy `mom_3s`, `mom_10s`, `mom_30s`, `vol_30s` + +4. `microstructure-service` + - liczy spread, depth, imbalance, slippage + +5. `observer-service` + - scala wyniki serwisow skladowych + - ocenia gate'y + - liczy finalny wynik obserwacji + - zapisuje event i aktualny state + +Nie trzeba wdrazac ich wszystkich od razu jako osobne procesy. Najpierw dokumentujemy kontrakty, potem mozemy zaczac od modularnego monolitu i dopiero potem wycinac osobne procesy. + +## Czy bloczek modelu zawsze jest osobnym serwisem + +Nie. + +Bloczki z modelu matematycznego w `bot1.tex` sa optymalna jednostka dokumentacji i analizy. +Nie kazdy bloczek jest jednak optymalna jednostka deploymentu. + +Zasada: + +- `bot1.tex` opisuje bloczki matematyczne +- kontrakt serwisu grupuje bloczki, ktore maja wspolne wejscie i wspolny wynik runtime +- osobny proces wdrazamy dopiero wtedy, gdy taka granica ma sens operacyjny + +To oznacza, ze: + +- `momentum + volatility` to dobry jeden serwis, bo oba licza sie z candles +- `spread + depth + imbalance + slippage` to dobry jeden serwis, bo wszystkie licza sie z DLOB +- `gating + scoring + final observer result` lepiej trzymac razem na poczatku niz rozbijac zbyt drobno +- `mom_3s`, `mom_10s`, `mom_30s` jako trzy osobne procesy nie maja sensu + +## Rekomendowane granice dla strategii v1 + +Najbardziej sensowny podzial runtime dla obecnej strategii: + +1. `market-snapshot-service` + - jeden odczyt z Hasury + - candles i derived DLOB + +2. `momentum-service` + - wszystko liczone tylko z candles + - `mom_3s`, `mom_10s`, `mom_30s`, `vol_30s` + +3. `microstructure-service` + - wszystko liczone tylko z derived DLOB + - spread, depth, imbalance, slippage + +4. `observer-service` + - pobiera wyniki serwisow skladowych albo liczy je lokalnie + - ocenia gate'y + - liczy score i finalny wynik + - zapisuje eventy i state + +To jest lepsze niz rozbijanie strategii 1:1 po kazdym wzorze, bo: + +- zmniejsza liczbe round-tripow do Hasury +- zmniejsza narzut operacyjny +- zachowuje czytelne granice domenowe +- nadal pozwala skalowac glowne klasy obliczen niezaleznie + +## Zasada "small services" + +Mniejsze serwisy sa preferowane, bo: + +- latwiej je skalowac niezaleznie +- latwiej je testowac +- latwiej je debugowac +- latwiej wymienic jeden model bez ruszania reszty + +Ale "mniejsze" nie znaczy "po jednej funkcji na proces". + +Jednostka podzialu to: + +- spojny kontrakt danych +- spojna odpowiedzialnosc obliczeniowa +- sensowny deployment boundary + +## CPU i deployment + +Mamy duzy zapas CPU, wiec wolno nam preferowac prostsze, bardziej izolowane procesy zamiast upychania wszystkiego do jednego serwisu. + +To nie zmienia zasad: + +- nie duplikowac source of truth +- nie mieszac transportu z logika +- nie rozbijac na mikroserwisy bez kontraktu + +## Kontrakt dla nowego serwisu + +Kazdy nowy serwis powinien miec dokument opisujacy: + +- nazwe i cel +- wejscie z Hasury +- wynik obliczen +- zapis wyjscia do Hasury +- endpointy Fastify +- model bledow +- SLA petli albo triggera + +## Stan biezacy + +`services/bot-observer` zostal juz przesuniety w dobra strone: + +- Fastify jako shell +- Hasura client osobno +- snapshot loader osobno +- model decyzji osobno +- state/event write osobno + +Kolejny krok to dalsze cięcie po kontraktach, nie po plikach. diff --git a/bot-strategy-reversal.md b/bot-strategy-reversal.md new file mode 100644 index 0000000..112d736 --- /dev/null +++ b/bot-strategy-reversal.md @@ -0,0 +1,142 @@ +# Strategia bota: “Reversal (sekundy → minuty)” — metryki i parametry + +Cel: podejmować decyzje w **sekundach**, wejść w pozycję na **minuty** tylko gdy mikrostruktura (orderbook) sprzyja, a następnie zarządzać pozycją w prosty i kontrolowalny sposób. + +Ten dokument jest propozycją “MVP strategii” i listą parametrów, nad którymi będziemy iterować. + +## Dane wejściowe (features) — co już mamy + +### Trend/impuls (krótki horyzont) +Źródło: ticki/candles z `drift_ticks` (np. przez `get_drift_candles(...)`). + +Przykładowe serie: +- `px_1s` / `px_5s`: zamknięcia z krótkich świec, +- `mom_3s`, `mom_10s`, `mom_30s`: momentum/ROC, +- `vol_30s`: zmienność krótkoterminowa. + +### Mikrostruktura (koszt wejścia i “czy da się wejść”) +Źródło: DLOB workery → Hasura/DB: +- `dlob_stats_latest`: `mid_price`, `spread_bps`, `depth_bid_usd`, `depth_ask_usd`, `imbalance` +- `dlob_depth_bps_latest`: `bid_usd/ask_usd` w pasmach ±bps (np. 10/20/50) +- `dlob_slippage_latest`: `impact_bps` dla progów `size_usd` (np. 100/500/1000/…) + +Ważne: “liquidity” i “kasa” (USD notional) liczymy z L2 jako sumy `size_base * price` (opis w `doc/dlob-basics.md`). + +## Architektura decyzji (loop) + +Strategia jest podzielona na 2 pętle: + +1) **Detektor warunków (sekundy)**: + - wykrywa potencjalny reversal, + - sprawdza bramki mikrostruktury (gates), + - ustala “desired exposure” (np. +K USD, -K USD, albo 0). +2) **Zarządzanie pozycją (minuty)**: + - utrzymuje pozycję do warunku wyjścia (czas/SL/TP/degradacja warunków), + - dba o order management (limit/chase) i idempotencję. + +## Bramki wejścia (gates) — żeby nie wchodzić w złych warunkach + +Wejście w trade jest dozwolone tylko jeśli wszystkie gates są spełnione: + +### 1) Freshness gate (dane muszą być świeże) +- `now - dlob_stats_latest.updated_at < freshness_max_ms` +- analogicznie dla depth/slippage, jeśli ich używasz w danym sygnale. + +### 2) Spread gate +- `dlob_stats_latest.spread_bps <= spread_max_bps` + +### 3) Slippage gate (dla planowanego rozmiaru) +- `dlob_slippage_latest.impact_bps(size_usd=entry_notional_usd, side=buy|sell) <= slippage_max_bps` + +### 4) Liquidity gate +Wariant A (top‑N): +- `min(depth_bid_usd, depth_ask_usd) >= depth_topn_min_usd` + +Wariant B (band bps): +- `min(bid_usd(band_bps=X), ask_usd(band_bps=X)) >= depth_band_min_usd` + +W praktyce często warto użyć obu: top‑N + band (bo mierzą różne rzeczy). + +## Trigger reversal (sekundy) — szkic logiki + +To jest intencjonalnie prosty szkic, który można zastąpić modelem ML później. + +Przykład (entry LONG): +- `mom_30s < 0` (krótkoterminowo spadkowo), +- `mom_3s > 0` (zaczyna się odbicie), +- opcjonalnie: `imbalance` rośnie (przechyla się na bid) albo spread się zawęża, +- gates spełnione. + +Przykład (entry SHORT) analogicznie: +- `mom_30s > 0`, `mom_3s < 0`, itd. + +## Wystawienie i “modyfikacja” wejścia (order policy) + +### Zalecenie MVP: limit/post-only + chase +Zamiast market (który “nie jest modyfikowalny”), utrzymujemy order przy top‑of‑book: + +- Place entry limit: + - buy: blisko `best_bid` (np. `best_bid + tick`) + - sell: blisko `best_ask` (np. `best_ask - tick`) +- Jeśli order nie fill i cena “ucieka”: + - cancel + place nowy (reprice), + - ale z ograniczeniami: `cooldown_ms`, `max_orders_per_min`, `reprice_bps`. + +Market order może być fallback tylko przy `urgency=true` i nadal spełnionych gates (zwłaszcza slippage). + +## Zarządzanie pozycją (minuty) — proste reguły + +Minimalne wyjścia (wystarczy na start): +- **max hold time**: `hold_max_s` (po czasie wychodzimy zawsze), +- **stop loss**: `stop_bps` od ceny wejścia, +- **take profit**: `take_bps` od ceny wejścia, +- **degradacja warunków**: wyjście, gdy np. `spread_bps` lub `impact_bps` przekracza limit przez X sekund. + +## Parametry bota (do `bot_config`) — propozycja + +Parametry są zgrupowane tak, żeby łatwo je stroić. + +### A) Sizing i częstotliwość +- `entry_notional_usd`: ile USD “na wejście” +- `min_trade_usd`: próg “ignoruj małe delty” +- `decision_interval_ms`: jak często liczysz sygnał (np. 250–1000ms) +- `hold_max_s`: maks. czas trzymania pozycji + +### B) Gates mikrostruktury +- `freshness_max_ms` +- `spread_max_bps` +- `slippage_max_bps` +- `depth_topn_min_usd` +- `depth_band_bps` (np. 10 lub 20) +- `depth_band_min_usd` + +### C) Reversal trigger +- `mom_fast_s` (np. 3s) +- `mom_slow_s` (np. 30s) +- `mom_entry_threshold` (jak mocny sygnał) +- `imbalance_entry_threshold` (opcjonalnie) + +### D) Order policy (limit/chase) +- `order_policy`: `limit_post_only | limit | market` +- `reprice_bps` +- `reprice_after_ms` +- `cooldown_ms` +- `max_orders_per_min` + +### E) Risk/exit +- `stop_bps` +- `take_bps` +- `exit_on_bad_spread_bps` + `exit_on_bad_spread_s` +- `exit_on_bad_slippage_bps` + `exit_on_bad_slippage_s` + +## Metryki bota (do logowania w `bot_events`) + +Żeby stroić strategię, logujemy na każdy “decision tick”: +- `features`: snapshot (albo hash + wybrane wartości: spread, slippage, depth, imbalance, mom) +- `gates`: które przeszły/nie przeszły + wartości +- `decision`: `target_exposure_usd`, `side`, `confidence`, `urgency` +- `actions`: place/cancel/close (z parametrami) +- `outcome`: fill, avg price, realized PnL (jeśli mamy), czas w pozycji + +To pozwala później robić offline analizy: “dlaczego wchodził”, “gdzie przegrywa”, “jak dobrać progi”. + diff --git a/bot/bot1.tex b/bot/bot1.tex new file mode 100644 index 0000000..bebc0e4 --- /dev/null +++ b/bot/bot1.tex @@ -0,0 +1,500 @@ +\documentclass[11pt,a4paper]{article} +\usepackage[T1]{fontenc} +\usepackage[margin=2.5cm]{geometry} +\usepackage{amsmath} +\usepackage{amssymb} +\usepackage{booktabs} +\usepackage{fancyhdr} +\usepackage{longtable} +\usepackage[hidelinks]{hyperref} + +\newcommand{\docauthor}{M. Pabiszczak} +\newcommand{\docdate}{2026-03-26} +\newcommand{\docrevision}{6825e0ad-a4be-427b-9559-0c3b6f744248} + +\pagestyle{fancy} +\fancyhf{} +\fancyhead[L]{\docauthor} +\fancyhead[C]{rewizja \docrevision} +\fancyhead[R]{\docdate} +\fancyfoot[C]{\thepage} +\setlength{\headheight}{14pt} + +\title{Model matematyczny observera SOL-PERP} +\author{} +\date{} + +\begin{document} +\maketitle +\thispagestyle{fancy} + +\section{Cel dokumentu} + +Ten dokument formalizuje aktualny baseline observera dla \texttt{SOL-PERP}. +Observer realizuje petle obserwacji rynku, ktora: +\begin{itemize} + \item co okolo \(1\) sekunde pobiera snapshot rynku, + \item liczy zestaw cech (features), + \item przepuszcza je przez bramki jakosci danych (gates), + \item wyznacza sygnal \texttt{long}, \texttt{short} albo \texttt{flat}, + \item zapisuje wynik do \texttt{bot\_state} oraz \texttt{bot\_events}. +\end{itemize} + +Model jest deterministycznym liniowym modelem scoringowym z komponentami +momentum i mikrostruktury rynku. + +\section{Architektura logiczna} + +Wersja biezaca ma nastepujacy przeplyw: +\[ +\text{Hasura / derived DLOB / candles} +\rightarrow +\text{feature extraction} +\rightarrow +\text{gates} +\rightarrow +\text{score} +\rightarrow +\text{decision event}. +\] + +Wersja docelowa doda jeszcze: +\[ +\text{decision} +\rightarrow +\text{desired state} +\rightarrow +\text{risk engine} +\rightarrow +\text{order manager} +\rightarrow +\text{execution}. +\] + +\section{Dane wejsciowe} + +Observer korzysta z dwoch klas danych: +\begin{itemize} + \item swiece z funkcji \texttt{get\_drift\_candles(...)} dla rynku + \texttt{SOL-PERP}, + \item znormalizowany snapshot orderbooka z + \texttt{dlob\_hot\_derived\_latest} z fallbackiem do + \texttt{dlob\_all\_derived\_latest}. +\end{itemize} + +Z derived read-modelu pobierane sa w szczegolnosci: +\begin{equation} +\texttt{mark\_price},\; +\texttt{oracle\_price},\; +\texttt{mid\_price},\; +\texttt{spread\_bps},\; +\texttt{bids\_norm},\; +\texttt{asks\_norm}. +\label{eq:read-model-fields} +\end{equation} + +Kazdy poziom orderbooka po normalizacji ma forme: +\begin{equation} +\ell = (p, q, n), +\label{eq:normalized-level} +\end{equation} +gdzie \(p\) oznacza cene, \(q\) rozmiar w bazie, a \(n\) notional w USD. + +\section{Definicje cech} + +\subsection{Momentum} + +Niech \(c_t\) oznacza cene zamkniecia ostatniej swiecy \(1s\). Dla horyzontu +\(k\) sekund momentum liczymy jako zwrot w basis points zgodnie ze wzorem +\eqref{eq:momentum}: +\begin{equation} +\mathrm{mom}_k(t) = 10^4 \left(\frac{c_t}{c_{t-k}} - 1\right). +\label{eq:momentum} +\end{equation} + +W implementacji uzywane sa trzy horyzonty opisane w~\eqref{eq:momentum-horizons}: +\begin{equation} +\mathrm{mom}_{3s},\quad \mathrm{mom}_{10s},\quad \mathrm{mom}_{30s}. +\label{eq:momentum-horizons} +\end{equation} + +Na poziomie runtime ten blok modelu powinien byc realizowany przez osobny +\texttt{momentum-service}. Kontrakt serwisu jest nastepujacy: +\begin{itemize} + \item wejscie: swiece \(1s\) z Hasury przez \texttt{get\_drift\_candles(...)} oraz + parametry horyzontow i okna zmiennosci, + \item processing: obliczenie \(\mathrm{mom}_{3s}\), \(\mathrm{mom}_{10s}\), + \(\mathrm{mom}_{30s}\) i \(\mathrm{vol}_{30s}\), + \item wyjscie: wynik wystawiony przez endpoint HTTP mikroserwisu, bez tworzenia + nowego source of truth poza Hasura. +\end{itemize} + +\subsection{Zmiennosc} + +Dla okna \(W\) sekund budujemy ciag zwrotow: +Zwroty elementarne liczymy wzorem \eqref{eq:returns}: +\begin{equation} +r_i = 10^4 \left(\frac{c_i}{c_{i-1}} - 1\right). +\label{eq:returns} +\end{equation} + +Nastepnie liczona jest odchylenie standardowe tych zwrotow wedlug +\eqref{eq:volatility}: +\begin{equation} +\mathrm{vol}_W(t) = +\sqrt{ +\frac{1}{N} +\sum_{i=1}^{N} +\left(r_i - \bar r\right)^2 +}. +\label{eq:volatility} +\end{equation} + +W biezacej wersji observer uzywa: +\begin{equation} +\mathrm{vol}_{30s}. +\label{eq:volatility-window} +\end{equation} + +\subsection{Odchylenie mark od oracle} + +Niech \(m_t\) oznacza \texttt{mark\_price}, a \(o_t\) oznacza +\texttt{oracle\_price}. Odchylenie mark od oracle liczymy wzorem +\eqref{eq:mvo}: +\begin{equation} +\mathrm{mvo}(t) = 10^4 \left(\frac{m_t}{o_t} - 1\right). +\label{eq:mvo} +\end{equation} + +Ta zmienna karze wejscie w trade, gdy mark znacaco odbiega od oracle. + +\subsection{Depth w pasmie plus-minus b bps} + +Niech \(p_t^{mid}\) oznacza mid-price oraz \(b\) szerokosc pasma w bps. + +Do depth po stronie bid bierzemy poziomy spelniajace nierownosc +\eqref{eq:depth-band-bid}: +\begin{equation} +p \ge p_t^{mid}\left(1 - \frac{b}{10^4}\right). +\label{eq:depth-band-bid} +\end{equation} + +Do depth po stronie ask bierzemy poziomy spelniajace nierownosc +\eqref{eq:depth-band-ask}: +\begin{equation} +p \le p_t^{mid}\left(1 + \frac{b}{10^4}\right). +\label{eq:depth-band-ask} +\end{equation} + +Nastepnie liczymy zagregowany depth po obu stronach zgodnie z +\eqref{eq:depth-bid} oraz \eqref{eq:depth-ask}: +\begin{align} +D^{bid}_b(t) = \sum_{\ell \in \mathcal{B}_b(t)} n_{\ell}, +\label{eq:depth-bid}\\ +D^{ask}_b(t) = \sum_{\ell \in \mathcal{A}_b(t)} n_{\ell}. +\label{eq:depth-ask} +\end{align} + +Imbalance w pasmie definiuje wzor \eqref{eq:imbalance}: +\begin{equation} +I_b(t) = +\frac{D^{bid}_b(t) - D^{ask}_b(t)} + {D^{bid}_b(t) + D^{ask}_b(t)}. +\label{eq:imbalance} +\end{equation} + +W praktyce model uzywa: +\begin{equation} +\texttt{depth\_bid\_usd},\quad +\texttt{depth\_ask\_usd},\quad +\texttt{depth\_imbalance}. +\label{eq:depth-feature-set} +\end{equation} + +\subsection{Slippage dla zadanego notionalu} + +Niech \(Q\) oznacza planowany notional wejscia w USD. Dla strony \emph{buy} +symulujemy konsumowanie kolejnych poziomow z \texttt{asks\_norm}, a dla strony +\emph{sell} z \texttt{bids\_norm}. + +VWAP z symulowanego wykonania liczymy wzorem \eqref{eq:vwap}: +\begin{equation} +\mathrm{VWAP}_s(Q, t) = +\frac{\text{filled\_usd}}{\text{filled\_base}}. +\label{eq:vwap} +\end{equation} + +Impact w basis points dla strony \emph{buy} i \emph{sell} definiuja odpowiednio +wzory \eqref{eq:slippage-buy} i \eqref{eq:slippage-sell}: +\begin{align} +\mathrm{slip}_{buy}(Q, t) = +10^4 \left(\frac{\mathrm{VWAP}_{buy}(Q, t)}{p_t^{mid}} - 1\right), +\label{eq:slippage-buy}\\ +\mathrm{slip}_{sell}(Q, t) = +10^4 \left(1 - \frac{\mathrm{VWAP}_{sell}(Q, t)}{p_t^{mid}}\right). +\label{eq:slippage-sell} +\end{align} + +W modelu logowane sa: +\begin{equation} +\texttt{buy\_slippage\_bps},\quad +\texttt{sell\_slippage\_bps}. +\label{eq:slippage-feature-set} +\end{equation} + +\subsection{Freshness} + +Kazdy snapshot ma znacznik czasu \texttt{updated\_at}. Dla statystyk, depth i +obu slippage liczony jest wiek danych w milisekundach. Ostatecznie maksymalny +wiek danych definiuje wzor \eqref{eq:data-age}: +\begin{equation} +\mathrm{dataAge}(t) = \max( +\mathrm{age}_{stats}, +\mathrm{age}_{depth}, +\mathrm{age}_{buySlip}, +\mathrm{age}_{sellSlip} +). +\label{eq:data-age} +\end{equation} + +\section{Bramki decyzyjne} + +Decyzja kierunkowa jest dozwolona tylko wtedy, gdy wszystkie bramki sa spelnione. + +\subsection{Freshness gate} +Freshness gate korzysta bezposrednio z~\eqref{eq:data-age} i ma postac: +\begin{equation} +\mathrm{dataAge}(t) \le F_{\max}. +\label{eq:gate-freshness} +\end{equation} + +\subsection{Spread gate} +Spread gate ma postac: +\begin{equation} +\mathrm{spread}_{bps}(t) \le S_{\max}. +\label{eq:gate-spread} +\end{equation} + +\subsection{Slippage gate} +Slippage gate odwoluje sie do \eqref{eq:slippage-buy} i +\eqref{eq:slippage-sell}: +\begin{equation} +\max\left( +\mathrm{slip}_{buy}(Q, t), +\mathrm{slip}_{sell}(Q, t) +\right) +\le L_{\max}. +\label{eq:gate-slippage} +\end{equation} + +\subsection{Depth gate} +Depth gate opiera sie na wielkosciach z \eqref{eq:depth-bid} i +\eqref{eq:depth-ask}: +\begin{equation} +\min\left( +D^{bid}_b(t), +D^{ask}_b(t) +\right) +\ge D_{\min}. +\label{eq:gate-depth} +\end{equation} + +\subsection{History gate} + +Musi istniec wystarczajaca liczba swiec, zeby policzyc najdluzsze momentum: +\begin{equation} +N_{candles} \ge k_{slow} + 1. +\label{eq:gate-history} +\end{equation} + +\section{Model scoringowy} + +Niech: +\begin{equation} +w_f,\; w_m,\; w_s,\; w_i,\; w_o,\; w_{sp},\; w_{sl} +\label{eq:weights} +\end{equation} +oznaczaja kolejno wagi dla: +\begin{itemize} + \item szybkiego momentum, + \item sredniego momentum, + \item skladnika reversal z dlugiego momentum, + \item imbalance, + \item odchylenia mark-vs-oracle, + \item spread, + \item slippage. +\end{itemize} + +Score dla long definiuje rownanie \eqref{eq:score-long}: +\begin{equation} +\mathrm{Score}_{long}(t) = +w_f \, \mathrm{mom}_{3s}(t) +{}+ w_m \, \mathrm{mom}_{10s}(t) +{}- w_s \, \mathrm{mom}_{30s}(t) +{}+ w_i \, I_b(t) +{}- w_o \, \mathrm{mvo}(t) +{}- w_{sp} \, \mathrm{spread}_{bps}(t) +{}- w_{sl} \, \mathrm{slip}_{buy}(Q, t). +\label{eq:score-long} +\end{equation} + +Score dla short definiuje rownanie \eqref{eq:score-short}: +\begin{equation} +\mathrm{Score}_{short}(t) = +- w_f \, \mathrm{mom}_{3s}(t) +- w_m \, \mathrm{mom}_{10s}(t) +{}+ w_s \, \mathrm{mom}_{30s}(t) +{}- w_i \, I_b(t) +{}+ w_o \, \mathrm{mvo}(t) +{}- w_{sp} \, \mathrm{spread}_{bps}(t) +{}- w_{sl} \, \mathrm{slip}_{sell}(Q, t). +\label{eq:score-short} +\end{equation} + +Interpretacja: +\begin{itemize} + \item dodatnie krotkie momentum wzmacnia long i oslabia short, + \item dodatnie dlugie momentum jest traktowane kontrariansko przez skladnik + reversal, + \item dodatni imbalance po stronie bid wspiera long, + \item zbyt duzy spread i zbyt duzy slippage karza obie strony, + \item wysokie mark-vs-oracle dziala anty-long i pro-short. +\end{itemize} + +\section{Regula decyzji} + +Jesli dowolna bramka nie przechodzi, wynik to: +\begin{equation} +\texttt{side} = \texttt{flat}, \qquad \texttt{skipReason} = \texttt{gate\_failed}. +\label{eq:decision-gate-failed} +\end{equation} + +W przeciwnym razie kierunek wybieramy wedlug \eqref{eq:decision-side}: +\begin{equation} +\texttt{side} = +\arg\max\left( +\mathrm{Score}_{long}(t), +\mathrm{Score}_{short}(t) +\right). +\label{eq:decision-side} +\end{equation} + +Niech +\begin{equation} +\mathrm{Score}_{best}(t) = +\max\left( +\mathrm{Score}_{long}(t), +\mathrm{Score}_{short}(t) +\right). +\label{eq:score-best} +\end{equation} + +Jesli zachodzi warunek progowy \eqref{eq:decision-threshold-test}: +\begin{equation} +\left|\mathrm{Score}_{best}(t)\right| < \Theta, +\label{eq:decision-threshold-test} +\end{equation} +to wynik rowniez jest: +\begin{equation} +\texttt{side} = \texttt{flat}, \qquad \texttt{skipReason} = \texttt{below\_threshold}. +\label{eq:decision-below-threshold} +\end{equation} + +Jesli prog jest przekroczony, bot produkuje sygnal: +\begin{equation} +\texttt{targetNotionalUsd} = Q, +\qquad +\texttt{horizonSeconds} = H. +\label{eq:decision-target} +\end{equation} + +Confidence jest normalizowane liniowo zgodnie ze wzorem +\eqref{eq:decision-confidence}: +\begin{equation} +\texttt{confidence} = +\min\left( +0.99, +\max\left(0, \frac{|\mathrm{Score}_{best}(t)|}{3 \Theta}\right) +\right). +\label{eq:decision-confidence} +\end{equation} + +\section{Parametry modelu i wartosci domyslne} + +\begin{longtable}{lll} +\toprule +Grupa & Parametr & Domyslna wartosc \\ +\midrule +\endhead +loop & \texttt{decision\_interval\_ms} & \(1000\) ms \\ +loop & \texttt{candle\_bucket\_seconds} & \(1\) s \\ +loop & \texttt{candles\_limit} & \(64\) \\ +features & \texttt{mom\_fast\_s} & \(3\) \\ +features & \texttt{mom\_mid\_s} & \(10\) \\ +features & \texttt{mom\_slow\_s} & \(30\) \\ +features & \texttt{vol\_window\_s} & \(30\) \\ +features & \texttt{depth\_band\_bps} & \(10\) \\ +features & \texttt{slippage\_size\_usd} & \(500\) \\ +gates & \texttt{freshness\_max\_ms} & \(800\) ms \\ +gates & \texttt{spread\_max\_bps} & \(8\) bps \\ +gates & \texttt{slippage\_max\_bps} & \(12\) bps \\ +gates & \texttt{depth\_band\_min\_usd} & \(3000\) USD \\ +sizing & \texttt{target\_notional\_usd} & \(500\) USD \\ +decision & \texttt{threshold} & \(1.2\) \\ +decision & \texttt{horizon\_s} & \(60\) s \\ +weights & \texttt{mom\_fast} & \(0.9\) \\ +weights & \texttt{mom\_mid} & \(0.35\) \\ +weights & \texttt{mom\_slow\_reversal} & \(0.55\) \\ +weights & \texttt{imbalance} & \(5.0\) \\ +weights & \texttt{mark\_vs\_oracle} & \(0.08\) \\ +weights & \texttt{spread} & \(0.15\) \\ +weights & \texttt{slippage} & \(0.12\) \\ +\bottomrule +\end{longtable} + +\section{Co bot zapisuje na kazdym ticku} + +Na kazdym przebiegu petli zapisywane sa: +\begin{itemize} + \item czasy i wiek danych: + \texttt{query\_latency\_ms}, \texttt{data\_age\_ms}, + \texttt{stats\_updated\_at}, \texttt{depth\_updated\_at}, + \texttt{buy\_slippage\_updated\_at}, \texttt{sell\_slippage\_updated\_at}, + \item cechy: + \texttt{mark\_price}, \texttt{oracle\_price}, \texttt{mid\_price}, + \texttt{spread\_bps}, \texttt{depth\_bid\_usd}, \texttt{depth\_ask\_usd}, + \texttt{depth\_imbalance}, \texttt{buy\_slippage\_bps}, + \texttt{sell\_slippage\_bps}, \texttt{mark\_vs\_oracle\_bps}, + \texttt{mom\_3s}, \texttt{mom\_10s}, \texttt{mom\_30s}, \texttt{vol\_30s}, + \item status gates: + \texttt{fresh}, \texttt{spread\_ok}, \texttt{slippage\_ok}, + \texttt{depth\_ok}, \texttt{has\_candles}, + \item wynik: + \texttt{side}, \texttt{confidence}, \texttt{long\_score}, + \texttt{short\_score}, \texttt{target\_notional\_usd}, + \texttt{skip\_reason}. +\end{itemize} + +To daje pelny material do strojenia progow, wag i pozniejszego przejscia z +observera do executora. + +\section{Wnioski} + +Aktualny bot jest formalnie: +\begin{itemize} + \item deterministycznym baseline'em, + \item modelem liniowym z recznie dobranymi wagami, + \item filtrem wejsc opartym o jakosc mikrostruktury, + \item systemem zbierania danych do pozniejszego modelu tradingowego. +\end{itemize} + +Najwazniejszy kolejny krok to rozdzielenie: +\begin{equation} +\text{signal} \neq \text{trade}. +\label{eq:signal-not-trade} +\end{equation} + +Observer powinien dalej generowac sygnal i dane treningowe, a przyszly executor +powinien osobno realizowac risk management, order management i kill switch. + +\end{document} diff --git a/bots.md b/bots.md new file mode 100644 index 0000000..d692dea --- /dev/null +++ b/bots.md @@ -0,0 +1,359 @@ +# Boty (auto-trading) na Drift PERP — architektura VPS + Vast + +Ten dokument opisuje docelową architekturę “botów”, które: +- potrafią otworzyć pozycję na wybranym rynku PERP, +- mierzą i publikują statsy (m.in. DLOB/L2, spread, depth, slippage), +- mogą zostać **zatrzymane / zmodyfikowane / natychmiast spłaszczone** (exit) w każdej chwili, +- działają na VPS (k3s) bez ręcznych zmian “na żywo” (snapshot deploy; patrz `doc/workflow.md`). + +## Założenia i cele + +### Cele +- **Separation of concerns:** model/strategia nie ma dostępu do kluczy; wykonanie transakcji jest tylko na VPS. +- **Kill switch:** jeden przełącznik ma natychmiast zatrzymać bota i wyjść z pozycji (cancel + close/reduce-only). +- **Desired-state loop:** bot nie “wydaje komend” ad‑hoc; utrzymuje stan docelowy (np. target exposure) i zbiega do niego. +- **Obserwowalność:** każda decyzja i akcja ma log/metrykę/event w DB. + +Słownik pojęć (DLOB/L1…L10): `doc/dlob-basics.md`. +Strategia MVP (sekundy → minuty): `doc/bot-strategy-reversal.md`. + +### Non-goals (na start) +- HFT / ultra-low-latency (na początku stawiamy na stabilność i kontrolę). +- Multi-venue routing i arbitraż między giełdami. +- Zarządzanie portfelem wielu subkont per user (to może wejść później). + +## Warstwy systemu (docelowo) + +### 1) Data plane (VPS/k3s): market data + statsy +Cel: zapewnić botom i UI spójne, “lokalne” źródła danych. + +Aktualne elementy (już istnieją w repo/stacku): +- `dlob-publisher-hot` → buduje hot-path DLOB z Solany i publikuje `dlob-hot:*` do `dlob-redis`. +- `dlob-publisher-all` → buduje pełny feed rynku i publikuje `dlob-all:*` do `dlob-redis`. +- `dlob-hot-redis-to-postgres-raw-writer` → zapisuje raw `hot` do PostgreSQL. +- `dlob-hot-postgres-to-postgres-derived-writer` → buduje derived `hot`. +- `dlob-all-redis-to-postgres-derived-writer` → buduje derived `all`. +- `Hasura` → wystawia te dane jako GraphQL + subscriptions dla UI i botów. + +Konfiguracja rynków: +- `DLOB_MARKETS` (symbole: np. `SOL-PERP`, `BTC-PERP`, `1MBONK-PERP`). +- `PERP_MARKETS_TO_LOAD` (indeksy rynków ładowane przez `dlob-publisher-hot` / `dlob-publisher-all`). + +### 2) Execution plane (VPS/k3s): bot executor (klucze + transakcje) +Cel: jedyne miejsce, gdzie występują klucze prywatne i podpisywanie transakcji. + +Warianty uruchomienia: +- **A) 1 Deployment na bota**: proste izolowanie i rollout per bot. +- **B) 1 Deployment “executor” obsługujący wiele botów**: łatwiejszy pooling połączeń/RPC, ale większy blast radius. + +Minimalne wymagania executora: +- Czyta konfigurację bota (desired state) z DB (Hasura/Postgres). +- Subskrybuje statsy/telemetrię z Hasury (GraphQL + subscriptions) lub odpytuje okresowo. +- Wykonuje akcje tradingowe (place/modify/cancel/close) z ograniczeniami ryzyka. +- Ma **hard kill switch** (patrz niżej). + +## Maszyneria bota (to jest kluczowe) + +To, czy bot “działa bezpiecznie”, zależy mniej od samego modelu, a bardziej od maszynerii: pętli kontrolnej, zarządzania orderami i bezpiecznego wyjścia. + +Poniżej minimalny podział odpowiedzialności wewnątrz executora (nawet jeśli to jest jeden proces): + +- **Config store**: odczyt `bot_config` (desired state) + zapis `bot_state`. +- **Market data adapter**: pobiera features z Hasury (np. `dlob_stats_latest`, `dlob_depth_bps_latest`, `dlob_slippage_latest`, candles). +- **Strategy adapter**: tworzy propozycję “desired” (lokalnie lub z Vast). Nie robi transakcji. +- **Risk engine**: weryfikuje limity (max pozycja, max slippage, max order rate, timeouts, freshness danych). +- **Order manager**: mapuje “desired” → konkretne akcje: place/modify/cancel; pilnuje idempotencji i retry. +- **Position reconciler**: porównuje stan konta (pozycje/ordery) z DB i “desired”; wykrywa rozjazdy. +- **Kill switch**: zawsze dostępny; wymusza cancel + close i przejście do `mode=off`. + +### Stan bota jako state machine (proponowane) + +Traktuj to jak automat stanów, żeby uniknąć chaosu “ifów”: +- `off`: bot nie wysyła transakcji (może tylko heartbeat). +- `observe`: liczy/loguje decyzje, ale nie handluje. +- `trade`: aktywny, utrzymuje desired-state. +- `panic`: tryb awaryjny; cancel + close; potem `off`. + +Opcjonalne stany pośrednie (jeśli potrzebne): `entering`, `managing`, `exiting`. + +### Minimalny model danych (MVP) + +Docelowo boty muszą być “sterowane” i audytowalne z DB: + +- `bot_config` (sterowanie): + - `mode`: `off|observe|trade` + - `market_name` (np. `SOL-PERP`) + - `target_exposure_usd` / `target_base` + - limity: `max_position_usd`, `max_slippage_bps`, `max_orders_per_min`, `cooldown_ms` + - `kill_switch` (bool) + - `strategy`: `{ type, params, model_endpoint, model_version }` +- `bot_state` (status runtime): + - `last_heartbeat_at`, `last_error`, `last_action_at` + - `position_snapshot` (opcjonalnie) +- `bot_events` (audit log): + - `decision` (wejście modelu/strategii + features hash + wersja) + - `order_sent`, `order_ack`, `order_filled`, `order_canceled` + - `panic_exit`, `error` + +W MVP możesz zacząć od: `bot_config` + `bot_events`, a `bot_state` dodać po pierwszym działającym loopie. + +### 3) Model plane (Vast): sygnały i inference (bez kluczy) +Cel: trenowanie/inference modeli na Vast, ale bez dostępu do środków. + +Kontrakt między VPS a Vast: +- VPS wysyła “features snapshot” (np. statsy DLOB + candles + pozycja). +- Vast zwraca “decision” (np. `target_base`, `target_notional_usd`, `side`, `confidence`, `params`). +- VPS decyduje o wykonaniu i podpisuje transakcje. + +Zasada: Vast **nigdy** nie dostaje sekretów (RPC keys, prywatne klucze, tokeny). + +### 4) RPC: dedykowany Solana RPC/WS (VPS) +Cel: stabilność + mniejsze opóźnienia dla `dlob-publisher` i executora. + +Założenia: +- `dlob-publisher` i executor używają tego samego RPC/WS (lub dwóch endpointów tej samej infrastruktury). +- W przyszłości możliwy fallback na publiczny endpoint (z ograniczeniami). + +## Desired-state loop (core idea) + +Bot działa jako pętla kontrolna: +1) Odczytaj `desired_state` (z DB) + bieżący stan (pozycja, ordery, rynek). +2) Policz różnicę (`delta`) i oceń ryzyko/limity. +3) Wykonaj minimalny zestaw akcji, żeby przybliżyć się do `desired_state`. +4) Zapisz eventy (co zrobił i dlaczego). + +Przykładowe `desired_state` (na start): +- `mode`: `off | observe | trade` +- `market`: `SOL-PERP` (lub lista) +- `target_exposure_usd` albo `target_base` +- `max_position_usd`, `max_leverage`, `max_slippage_bps` +- `order_policy`: `market | limit | post_only | chase` +- `cooldown_ms`, `max_orders_per_min` + +## Kill switch (must-have) + +Kill switch ma działać bez udziału modelu i bez UI. + +Źródła kill switch (kolejność priorytetu): +1) **Env var** executora (np. `BOT_KILL_SWITCH=1`) → natychmiastowy “panic mode”. +2) Flaga w DB (np. `bot_config.kill_switch=true`). +3) Warunek bezpieczeństwa (np. RPC lag, brak danych, brak świeżości L2, zbyt duży drawdown). + +Akcja kill switch (docelowy runbook): +- `cancel_all` na rynku (lub subkoncie), +- `close_position` (preferowane: reduce-only, kontrola slippage), +- przejście bota do `mode=off` + zapis eventu “panic_exit”. + +## Awaria VPS: co się dzieje z transakcjami i jak się zabezpieczyć + +Kluczowe fakty: +- **Nie da się “ubić” wysłanej transakcji** na Solanie. Jeśli tx została przyjęta do sieci i wyląduje w bloku, jej efekt jest on-chain. +- To, co można zrobić po awarii, to **kolejne transakcje korygujące**: `cancel` / `close` / `reduce-only`. + +W praktyce awarie dzielą się na 3 przypadki: + +1) **Tx nie weszła (dropped/expired)** → po restarcie robisz reconcile i jedziesz dalej. +2) **Tx weszła i postawiła order** → po restarcie reconcile zobaczy otwarte ordery i może je utrzymać/zmodyfikować/cancel. +3) **Tx weszła i zmieniła pozycję** → po restarcie reconcile zobaczy pozycję i może ją domknąć lub dopasować do desired-state. + +### Reconcile po starcie (must-have) + +Każdy bot po restarcie powinien: +- wczytać `bot_config` (desired state), +- pobrać stan on-chain (pozycje + otwarte ordery dla rynku/subkonta), +- porównać “observed” vs “desired” i wykonać minimalne akcje korekcyjne. + +To chroni przed sytuacją: “wysłaliśmy akcję, VPS padł przed zapisaniem eventu”. + +## Guardian / Safety bot (żeby wyjść mimo padnięcia executora) + +Jeśli wymaganie brzmi: “w każdej chwili możemy wyjść”, to kill switch nie może mieszkać tylko w tym samym VPS/klastrze co executor. + +Proponowany wzorzec: +- Osobny, mały serwis **`bot-guardian`** uruchomiony **poza** głównym VPS (drugi VPS / inny klaster / inna VM). +- Guardian ma dostęp do: + - RPC/WS, + - klucza lub uprawnień do cancel/close (patrz uwagi bezpieczeństwa), + - sygnału “czy executor żyje” (np. `bot_state.last_heartbeat_at` w DB albo endpoint health). + +Zachowanie: +- Jeśli heartbeat executora jest starszy niż `T_fail` (np. 10–30s) → guardian włącza “panic mode”: + - `cancel_all_orders`, + - `close_position` (reduce-only; z limitem slippage), + - zapis eventu `panic_exit` (jeśli ma dostęp do DB). + +Uwaga: guardian nie potrzebuje modelu/AI. Ma być “głupi i niezawodny”. + +### Minimalne bezpieczeństwo guardiana + +- Klucze: trzymane jako secret tylko na hoście guardiana; nie w repo. +- Zakres: jeśli to możliwe, ogranicz klucz do subkonta / minimalnych uprawnień (w zależności od możliwości Drift/Solana). +- Rate-limit: zabezpieczenie przed pętlą panic (np. `panic_cooldown_ms`). + +## HA executora (opcjonalnie): leader election + +Jeśli chcesz 2 repliki executora (na jednym lub wielu węzłach), potrzebujesz single-writer: +- tylko **leader** składa transakcje, +- **follower** obserwuje i jest gotowy przejąć. + +Wzorzec: +- lease w DB/Redis (np. rekord “lock” z TTL), +- leader odnawia lease co N sekund, +- brak odnowienia → follower przejmuje i robi reconcile. + +To ogranicza ryzyko “double-trading”, ale nie zastępuje guardiana (bo oba mogą paść razem). + +## Model kosztów (expected vs realized) — entry/exit/modify + +Bot powinien umieć policzyć: +- **expected costs** (przed wejściem/wyjściem): czy trade ma sens i czy mieści się w limitach, +- **realized costs** (po fakcie): ile faktycznie kosztowało wejście/wyjście + ile “spaliły” modyfikacje. + +W praktyce koszty dzielą się na 3 kubełki: +1) koszt mikrostruktury (spread/slippage/market impact), +2) fee maker/taker + funding (opcjonalnie), +3) koszt tx na Solanie (network fee + priority) oraz koszt modyfikacji (cancel+place). + +### 1) Koszt wejścia/wyjścia z orderbooka (spread/slippage) + +Źródła danych: +- `dlob_stats_latest`: `mid_price`, `spread_bps`, `best_bid_price`, `best_ask_price` +- `dlob_slippage_latest`: `impact_bps`, `vwap_price`, `fill_pct` dla `size_usd` i `side` + +Warianty: +- **Taker/market (lub agresywny limit krzyżujący)**: używaj `impact_bps` z `dlob_slippage_latest` jako “expected execution cost vs mid”. +- **Maker/post-only**: policz 2 scenariusze: + - “maker-fill”: koszt vs mid wynikający z `limit_price` (zwykle lepszy niż taker), + - “fallback-taker”: jeśli nie fill → koszt = `impact_bps` (jak taker). + +Przykładowa definicja kosztu vs mid (bps), jeśli liczysz z cen: +- buy: `cost_bps = (fill_price / mid_at_submit - 1) * 10_000` +- sell: `cost_bps = (1 - fill_price / mid_at_submit) * 10_000` + +Uwaga: `mid` się rusza, więc to jest miara “kosztu wykonania względem chwilowego środka rynku” (przydatna do strojenia), a nie pełne PnL. + +### 2) Fee maker/taker i funding (opcjonalnie) + +Na MVP trzymaj fee jako parametry konfiguracyjne: +- `fee_maker_bps` +- `fee_taker_bps` + +Realized fee licz po fill: +- `fee_usd = fill_notional_usd * fee_rate_bps / 10_000` + +Funding dla holdów “minuty” zwykle jest mały; można dodać później. + +### 3) Koszt transakcyjny (Solana fee + priority) i koszt modyfikacji + +Fakty: +- “modyfikacja” ordera to zazwyczaj **cancel + place** → zwykle 2 transakcje. +- Nie da się “ubić” wysłanej transakcji; można tylko wysłać korektę (cancel/close) kolejną tx. + +Definicje: +- `tx_fee_usd` = koszt jednej transakcji w USD (network fee + priority fee). +- `tx_fee_bps = (tx_fee_usd / notional_usd) * 10_000` + +Koszt modyfikacji (expected): +- `expected_modify_cost_usd ≈ expected_reprices * (tx_fee_cancel_usd + tx_fee_place_usd)` +- `expected_modify_cost_bps = (expected_modify_cost_usd / entry_notional_usd) * 10_000` + +Źródło `tx_fee_usd`: +- na start: stałe estymaty w `bot_config`, +- później: estymata “priority fee” z feedu/endpointu + przelicznik SOL→USD. + +### Expected total cost (proponowana metryka bramkująca) + +Na ticku decyzji licz: +- `expected_total_cost_bps = execution_bps + fee_bps + tx_bps + expected_modify_bps (+ funding_bps)` + +I bramkuj wejście/wyjście: +- `expected_total_cost_bps <= max_expected_total_cost_bps` + +## Parametry kosztów (do `bot_config`) — propozycja + +Minimalny zestaw: +- `fee_maker_bps`, `fee_taker_bps` +- `max_expected_total_cost_bps` +- `max_expected_modify_bps` (opcjonalnie osobny limit na “cancel+place spam”) +- `tx_fee_usd_est` (albo `tx_fee_sol_est` + `sol_price_usd_est`) +- `expected_reprices_per_entry` + +To działa zarówno dla strategii regułowych, jak i ML (model może dać `urgency`, ale executor i tak liczy koszty). + +## Logowanie kosztów (do `bot_events`) — propozycja + +Żeby stroić progi i ocenić jakość wejść, zapisuj: + +### Na decyzji (przed wysłaniem orderów) +- `mid_at_submit`, `spread_bps_at_submit` +- `slippage_quote`: `impact_bps`, `vwap_price`, `fill_pct` (dla danego `size_usd`) +- `expected_cost_bps_breakdown`: `{ execution_bps, fee_bps, tx_bps, expected_modify_bps, total_bps }` +- `order_intent`: `{ policy, limit_price, size_usd, side }` + +### Po fill / wyjściu +- `realized_execution_bps` (vs `mid_at_submit`), osobno entry i exit +- `realized_fee_usd`, `realized_tx_usd` +- `modify_count`: liczba cancel+place w trakcie wejścia/zarządzania +- `time_to_fill_ms`, `hold_time_s` + +To daje realną pętlę feedbacku: “koszt vs jakość sygnału” i “czy chase zjada edge”. + +## Obserwowalność i audyt (DB + logi) + +W DB trzymamy: +- Konfigurację (`bot_config`) — desired state i limity. +- Stan (`bot_state`) — ostatnia decyzja, last heartbeat, ostatni błąd. +- Eventy (`bot_events`) — “decision”, “order_sent”, “order_filled”, “panic_exit”, “error”. + +MVP może zacząć od samego `bot_events` + `bot_config`, a resztę dodać po pierwszym działającym loopie. + +## Interfejsy (kontrakty) + +### VPS ↔ Hasura +- Executor czyta: `dlob_*_latest` + `drift_ticks`/candles + własne tabele botów. +- Executor pisze: `bot_events`, `bot_state` (oraz ewentualnie `bot_orders`). + +### VPS ↔ Vast +- HTTP endpoint (lub gRPC) “predict”. +- Kontrakty muszą być wersjonowane (`model_version`) i logowane w `bot_events`. + +## Bezpieczeństwo (sekrety) + +Zasady: +- Prywatne klucze i tokeny są tylko w K8s Secret (na VPS) i nigdy nie trafiają do repo. +- Vast nie dostaje sekretów. +- Logi nie mogą wypisywać URL z api-key ani payloadów z sekretami. + +## Deployment (k3s + GitOps) + +Zasady deployu: snapshoty, brak ręcznych zmian na VPS (`doc/workflow.md`). + +Docelowe komponenty w `trade-deploy` (do dopięcia): +- `Deployment/bot-executor` (albo `Deployment/bot-`), +- `Secret/bot-executor-keys` (klucze/konfiguracja), +- NetworkPolicy (opcjonalnie) ograniczająca ruch do Vast i RPC. + +## Plan budowy (iteracyjnie) + +### Etap 0: Observe-only (bez handlu) +- `bot_config` + `bot_events`. +- Executor subskrybuje `dlob_stats_latest` i loguje “decision” bez transakcji. +- Kill switch działa (przełącza tryb i loguje). + +### Etap 1: Paper trading / dry-run +- Executor wylicza ordery i zapisuje je do DB, ale nie wysyła na chain. +- UI pokazuje “co by zrobił”. + +### Etap 2: Live trading (minimal) +- Wejście/wyjście market/limit z restrykcjami ryzyka. +- Panic exit: cancel + close. + +### Etap 3: Integracja z modelem (Vast) +- Predictor endpoint, wersjonowanie modeli, fallback (jeśli Vast down → observe/off). + +## Otwarte pytania (do uzupełnienia) + +- Czy bot ma działać na jednym rynku PERP czy wielu jednocześnie? +- Jaki tryb wejścia/wyjścia: market vs limit vs post-only/chase? +- Jakie KPI/limity: max drawdown, max slippage, max position, timeouts? +- Jak rozdzielamy konta/subkonta (1 bot = 1 subkonto?) i jak to audytujemy? diff --git a/data-ingest-strategy.md b/data-ingest-strategy.md new file mode 100644 index 0000000..c05c3c2 --- /dev/null +++ b/data-ingest-strategy.md @@ -0,0 +1,344 @@ +# Data ingest strategy + +## Cel + +Zachowac niski latency dla logiki tradingowej i read-side, ale rownoczesnie miec trwala persystencje danych do UI, replayu, debugowania i analityki. + +Nie robimy modelu `DB-first` jako jedynej sciezki przetwarzania. + +## Rekomendowany model + +Stosujemy `dual-path`: + +1. `hot path` +`Yellowstone gRPC / Agave RPC -> consumer -> Redis / internal state -> strategy / read-side` + +2. `durable path` +`ten sam consumer -> Postgres -> UI / Hasura / replay / debug` + +Opcjonalnie pozniej: + +3. `history / analytics path` +`consumer lub batch exporter -> ClickHouse` + +## Dlaczego nie DB-first + +Model: + +`gRPC / RPC -> DB -> dopiero potem przetwarzanie` + +nie jest dobry jako glowna sciezka, bo: + +- dodaje latency do kazdego eventu +- uzaleznia ingest od kondycji bazy +- pogarsza odpornosc calego pipeline +- zwieksza write amplification przy duzym wolumenie zdarzen +- utrudnia low-latency strategie i read-side + +DB ma byc miejscem persystencji i read modelu, nie bramka przed logika runtime. + +## Rola Yellowstone gRPC + +Yellowstone ma byc glownym zrodlem `live feed`: + +- account updates +- tx updates +- slot updates +- program updates + +Sciezka: + +`Yellowstone -> consumer -> hot state` + +oraz rownolegle: + +`Yellowstone -> normalization / persistence -> Postgres` + +Yellowstone nie powinien byc uzalezniony od tego, czy Postgres aktualnie pisze szybko. + +## Rola Agave RPC + +RPC nie jest naturalnym feedem eventowym. + +RPC powinno sluzyc glownie do: + +- point reads +- bootstrapu klienta +- recovery +- getAccountInfo / getMultipleAccounts +- getHealth / getSlot +- write path dla tx, jesli nie idziemy przez TPU/Jito + +RPC nie powinno byc traktowane jak glowny event stream do zrzucania wszystkiego do DB. + +## Rola Redis + +Redis powinien byc `hot state layer`: + +- szybki stan DLOB +- cache +- fanout wewnetrzny +- ewentualnie stream / queue dla workerow + +To jest warstwa pod: + +- strategy engine +- orderbook read-side +- szybkie projekcje + +Nie trzymamy tam historii jako source of truth. + +## Rola Postgres + +Postgres powinien byc `durable read model`: + +- dane dla UI +- dane dla Hasury +- znormalizowane ticki +- pochodne tabele DLOB +- stan aplikacji +- konfiguracja +- historia operacyjna + +Postgres jest dobry do: + +- frontend +- dashboard +- replay lekkiego zakresu +- audyt aplikacyjny + +## Kiedy ClickHouse + +ClickHouse dodajemy dopiero, gdy: + +- wolumen raw feedu bedzie duzy +- potrzebna bedzie ciezsza analityka historyczna +- bedziemy chcieli trzymac dluzsza historie eventow i tx + +Na obecnym etapie nie jest konieczny do uruchomienia runtime. + +## Docelowy przeplyw + +```text +mevnode_sol + Agave RPC + Yellowstone gRPC + | + v +mevnode_bot + consumer + |\ + | +--> Redis -> DLOB/read-side -> strategy/workers + | + +----> Postgres -> Hasura/API -> frontend +``` + +## Zasada architektoniczna + +- `gRPC / RPC` dostarcza dane do runtime +- `Redis` utrzymuje szybki stan operacyjny +- `Postgres` utrzymuje trwaly stan i read model +- `frontend` i `Hasura` czytaja z Postgresa +- `strategy` i `workers` nie czekaja na zapis do DB, aby przetwarzac dane + +## Rekomendacja dla projektu + +Na teraz: + +1. `Yellowstone -> consumer -> Redis` +2. `consumer -> Postgres` +3. `Hasura / API / UI <- Postgres` +4. `strategy / read-side <- Redis` +5. `RPC` zostawic do bootstrapu, point reads i write path + +To daje najlepszy kompromis: + +- niski latency +- odpornosc runtime +- sensowna persystencja +- dobra baza pod UI i replay + +## Ocena obecnego ukladu na k3s + +Obecny runtime na `mevnode_bot / trade-staging` jest tylko czesciowo zgodny z docelowym modelem. + +### Co jest zgodne + +- `postgres` jest postawiony jako `StatefulSet`, wiec nadaje sie na trwały storage +- `dlob-redis` jest osobna lekka usluga i pasuje do roli cache / hot state +- `hasura` i `trade-api` siedza nad `Postgresem`, co jest poprawne dla durable read modelu +- `frontend` korzysta z `API/Hasury`, a nie bezposrednio z `Redis` + +### Co nie jest zgodne + +- `dlob-publisher` i `dlob-server` nie sa zdrowe, wiec hot path nie jest operacyjnie domkniety +- `dlob-publisher` jest podpiety do `gRPC`, ale nie jest glownym i pewnym writerem do `Postgresa` +- `trade-ingestor` nie ingestuje danych bezposrednio z chain source, tylko polluje dane pochodne z `Hasury` +- persistent path jest wiec dzisiaj czesciowo odwrocony: + - `derived view -> ingest -> Postgres` + - zamiast: + - `chain source -> normalize -> Postgres` + +## Plan zmian + +### Cel koncowy + +Chcemy dojsc do ukladu: + +```text +Yellowstone gRPC / Agave RPC + | + v +consumer + |\ + | +--> Redis -> hot state / workers / strategy + | + +----> Postgres -> Hasura / API / frontend / replay +``` + +### Etap 1. Naprawa hot path + +Najpierw trzeba przywrocic dzialanie: + +- `dlob-publisher` +- `dlob-server` + +Bez zdrowego publishera nie ma sensu budowac dalszej warstwy `Redis = hot state`. + +Zakres: + +- ustabilizowac `Agave RPC` i `Yellowstone` +- doprowadzic `dlob-publisher` do `Ready` +- potwierdzic, ze `Redis` dostaje aktualny stan +- doprowadzic `dlob-server` do `Ready` + +### Etap 2. Uczynic publisher glownym consumerem danych chain + +Obecnie `trade-ingestor` korzysta z danych pochodnych z `Hasury`. + +To trzeba zmienic tak, aby glownym miejscem wejscia danych byl komponent konsumujacy: + +- `Yellowstone gRPC` +- `Agave RPC` + +Rekomendacja: + +- rozszerzyc `dlob-publisher` +- albo wydzielic nowy `market-consumer` + +ale w obu przypadkach komponent ma robic: + +- odczyt live z `Yellowstone` +- wymagane point reads z `RPC` +- normalizacje danych +- jednoczesny zapis do `Redis` i `Postgresa` + +### Etap 3. Ustalic twardy podzial rol Redis / Postgres + +#### Redis + +Do `Redis` trafia tylko to, co potrzebne jako szybki stan runtime: + +- latest DLOB state +- latest L2 +- quick cache dla workerow +- fanout do read-side i strategii +- dane z TTL lub dane odtwarzalne + +`Redis` nie jest source of truth. + +#### Postgres + +Do `Postgresa` trafia trwaly model danych: + +- znormalizowane ticki +- znormalizowane snapshoty rynku +- pochodne tabele pod `Hasure` +- historia operacyjna +- stan aplikacji + +`Postgres` jest source of truth dla aplikacji i UI. + +### Etap 4. Przebudowac worker path + +Docelowy model dla workerow: + +- `Redis in` +- `Postgres out` + +To znaczy: + +- workery czytaja szybki stan z `Redis` +- licza projekcje +- zapisują wynik do `Postgresa` + +Dotyczy to szczegolnie: + +- `dlob-worker` +- `dlob-depth-worker` +- `dlob-slippage-worker` + +### Etap 5. Wygasic obecny odwrocony ingest + +Obecny `trade-ingestor` zapisuje dane do `Postgresa` na podstawie danych juz pochodnych z `Hasury`. + +To nie powinno byc glowna sciezka ingestu. + +Mozliwe role po zmianie: + +- backfill +- sanity check +- fallback +- testowy importer + +Ale nie: + +- glowny chain ingest + +### Etap 6. Utrwalic read model pod UI + +Po zmianach: + +- `Hasura` czyta tylko z `Postgresa` +- `trade-api` czyta tylko z `Postgresa` +- `frontend` czyta przez `API/Hasure` + +Frontend nie powinien zalezec od `Redis`. + +### Etap 7. Zostawic RPC w waskiej roli + +`Agave RPC` zostawiamy do: + +- bootstrapu +- point reads +- recovery +- write path dla tx + +Nie robimy z `RPC` glownej magistrali trwałego ingestu. + +## Plan wdrozenia krok po kroku + +1. Naprawic `dlob-publisher` i `dlob-server`. +2. Potwierdzic, ze `Redis` jest zasilany poprawnym hot state. +3. Rozszerzyc publisher albo dodac nowy consumer tak, aby pisal rownolegle do: + - `Redis` + - `Postgresa` +4. Zdefiniowac schemat tabel normalized data w `Postgresie`. +5. Przepiac workery na model: + - read z `Redis` + - write do `Postgresa` +6. Zostawic `Hasure` i `trade-api` jako warstwe odczytowa nad `Postgresem`. +7. Zdegradowac `trade-ingestor` z roli glownego ingestu do roli pomocniczej albo go usunac z glownej sciezki. + +## Rekomendacja praktyczna + +Najmniejsza sensowna zmiana to nie budowac od razu nowego systemu od zera, tylko: + +1. naprawic obecny `dlob-publisher` +2. dodac mu zapis do `Postgresa` +3. stopniowo wyprowadzac `trade-ingestor` z glownego path + +To jest najmniej ryzykowna droga do modelu: + +- `Redis = cache / hot state` +- `Postgres = persistent normalized store` diff --git a/dlob-basics.md b/dlob-basics.md new file mode 100644 index 0000000..e4e1ea0 --- /dev/null +++ b/dlob-basics.md @@ -0,0 +1,184 @@ +# 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 aktualny łańcuch od źródła do metryk w live runtime: + +### Warstwa A: On-chain → DLOB w pamięci (VPS/k3s) +Komponenty: `dlob-publisher-hot` i `dlob-publisher-all`. + +- Łączą się do Solany przez prywatny RPC/WS/gRPC. +- Budują DLOB w pamięci. +- Publikują snapshoty do Redis: + - `dlob-hot:*` + - `dlob-all:*` + +To jest najbliżej źródła i najbardziej “real-time”. + +### Warstwa B: Redis → PostgreSQL (VPS/k3s) +Komponenty: + +- `dlob-hot-redis-to-postgres-raw-writer` +- `dlob-hot-postgres-to-postgres-derived-writer` +- `dlob-all-redis-to-postgres-derived-writer` + +Ten etap: + +- utrwala raw `hot` w PostgreSQL, +- buduje derived layer `hot` i `all`, +- zapisuje znormalizowane poziomy orderbooka oraz metryki top-of-book do tabel `dlob_*_derived_*`. + +### Warstwa C: Hasura + aplikacja + +- `Hasura` wystawia derived tables jako GraphQL + subscriptions. +- UI i narzędzia aplikacyjne czytają z `dlob_hot_derived_latest` oraz `dlob_all_derived_latest`. +- Metryki depth/slippage nie są już liczone przez osobne workery k3s; bieżąca ścieżka aplikacji wylicza je z danych L2/derived. + +## 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_*_derived_latest` +Zapisywane w writerach warstwy pochodnej na podstawie live snapshotów orderbooka: + +- Bierzemy znormalizowane top level’e z `bids_norm` i `asks_norm`. +- Każdy level ma: + - `price` + - `size_base` + - “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 +W aktualnej ścieżce aplikacji to liczenie jest robione z danych L2/derived, a nie przez osobny backend worker: + +- 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/dlob-retention-feature-store-plan.md b/dlob-retention-feature-store-plan.md new file mode 100644 index 0000000..90555cf --- /dev/null +++ b/dlob-retention-feature-store-plan.md @@ -0,0 +1,236 @@ +# DLOB Retention And Feature Store Plan + +Status na `2026-03-29`. + +Ten dokument zapisuje robocze ustalenia o: + +- retencji danych DLOB na `mevnode_bot` +- ryzyku zajetosci dysku +- tym, co warto trzymac dlugo pod modele / "economic transformers" + +## Aktualny stan operacyjny + +### Dysk + +- root `/` na `mevnode_bot`: `3.6T` +- zajete: ok. `471G` +- wolne: ok. `3.0T` +- biezace uzycie: ok. `14%` + +Najwiekszy konsument miejsca: + +- PostgreSQL / TimescaleDB na local volume poda `postgres-0` +- rozmiar bazy `crypto`: ok. `457 GB` + +Najwieksze hypertable: + +- `public.dlob_all_derived_ts`: ok. `260 GB` +- `public.dlob_hot_snapshot_ts`: ok. `177 GB` +- `public.dlob_hot_derived_ts`: ok. `19 GB` + +Wniosek: + +- miejsca nie brakuje teraz +- ale storage DLOB jest juz na tyle duzy, ze trzeba nim zarzadzac swiadomie + +## Od kiedy sa dane + +- `dlob_hot_snapshot_ts`: od `2026-03-17 00:23:36.013+00` +- `dlob_hot_derived_ts`: od `2026-03-17 00:23:36.013+00` +- `dlob_all_derived_ts`: od `2026-03-17 00:44:42.386+00` + +## Aktualna retencja TimescaleDB + +Na `2026-03-29` retencja jest juz ustawiona i joby sa `Success`. + +- `dlob_hot_snapshot_ts`: `30 days` +- `dlob_all_derived_ts`: `30 days` +- `dlob_hot_derived_ts`: `180 days` +- legacy `dlob_stats_ts` / `dlob_depth_bps_ts` / `dlob_slippage_ts*`: `7 days` + +Wazne doprecyzowanie: + +- stare rekordy nie beda "nadpisywane" +- stare chunki beda usuwane przez polityki retencji + +Praktycznie: + +- tabele `30-day` zaczna byc realnie przycinane okolo `2026-04-16` +- `dlob_hot_derived_ts` zacznie byc realnie przycinane dopiero po osiagnieciu ok. `180 days` + +## Czy trzeba juz robic ring buffer + +Nie pilnie. + +Lepsza kolejnosc: + +1. Zostawic obecna retencje. +2. Sprawdzic po `2026-04-16`, czy `30-day` tabele rzeczywiscie zaczely sie wyplaszczac rozmiarowo. +3. Wlaczyc kompresje TimescaleDB dla duzych hypertabli. +4. Dopiero potem decydowac, czy potrzebny jest dodatkowy custom ring buffer. + +Powod: + +- miejsca jest duzo +- retencja juz istnieje +- najwiekszy zysk teraz da kompresja, nie customowa logika bufora + +## Wazna obserwacja + +Na `2026-03-29` kompresja TimescaleDB dla tabel `dlob_*` jest wylaczona: + +- `public.dlob_all_derived_ts`: `compression_enabled = false` +- `public.dlob_hot_snapshot_ts`: `compression_enabled = false` +- `public.dlob_hot_derived_ts`: `compression_enabled = false` + +To jest najprostszy kandydat na kolejny krok oszczedzania miejsca. + +## Co warto trzymac dlugo dla modeli + +Nie warto trzymac wszystkiego w raw snapshotach. + +Do treningu modeli bardziej oplacalne jest trzymanie: + +- stalych cech numerycznych +- etykiet future outcome +- kilku skal czasowych + +Zamiast: + +- pelnego `payload JSON` +- wszystkich poziomow `bids/asks` dla kazdego ticka +- duplikowania raw i derived w dlugim horyzoncie + +## Rekomendowana piramida danych + +### Warstwa 1. Short-term raw + +Przechowywac krotko jako source of truth: + +- `dlob_hot_snapshot_ts` +- `dlob_all_derived_ts` + +Cel: + +- debugging +- replay +- reprocessing +- walidacja feature engineering + +### Warstwa 2. Feature bars + +Z raw / derived budowac stale, numeryczne wektory cech: + +- `1s` feature bars na `180d+` +- `5s` albo `15s` feature bars na `1y+` +- `1m` regime bars bardzo dlugo + +To powinno byc glownym wejsciem do modeli. + +### Warstwa 3. Labels + +Osobno trzymac etykiety przyszlego wyniku: + +- `fwd_ret_1s` +- `fwd_ret_5s` +- `fwd_ret_30s` +- `fwd_ret_5m` +- `max_up` +- `max_down` +- adverse selection +- fill probability proxy +- edge net of fees / funding / slippage + +## Jakie cechy warto trzymac + +### Mikrostruktura + +- `mid` +- `microprice` +- `spread_bps` +- `mark - oracle` +- `mid - oracle` +- basis + +### Plynnosc i ksiega + +- `best_bid_size` +- `best_ask_size` +- imbalance +- depth w koszykach `1/2/5/10/20/50 bps` +- slope / curvature booka + +### Aktywnosc i jakosc feedu + +- liczba update'ow w bucketcie +- udział `indicative` vs normalnych snapshotow +- staleness feedu +- luki czasowe + +### Statystyki krotkoterminowe + +- realized vol `5s` +- realized vol `30s` +- realized vol `5m` +- momentum score +- mean reversion score +- spread capture proxy +- slippage dla stalych notionals + +## Proponowane tabele docelowe + +Minimalny zestaw: + +- `dlob_features_1s` +- `dlob_features_5s` +- `dlob_regime_1m` +- `bot_labels_horizons` + +Kazda z tych tabel powinna miec: + +- `event_ts` +- `source` +- `market_type` +- `market_index` +- zestaw cech numerycznych +- opcjonalnie `meta` + +To da: + +- staly rozmiar rekordu +- tansze trenowanie +- latwiejsze sample / batch / join +- mniej miejsca niz raw JSON + +## Dlaczego to jest lepsze od trzymania wszystkiego + +Bo modele nie potrzebuja pelnego surowego payloadu do kazdego kroku. + +Dla modeli wazniejsze sa: + +- stabilne, stale-wymiarowe wejscia +- konsekwentne etykiety +- latwe agregowanie po oknach czasu + +Raw trzeba trzymac krotko. +Feature store i labels trzeba trzymac dlugo. + +## Rekomendacja na kolejny tydzien + +1. Nie wdrazac jeszcze custom ring buffer. +2. Zweryfikowac po `2026-04-16`, czy `30-day` retencja rzeczywiscie przycina `hot_snapshot` i `all_derived`. +3. Przygotowac plan wlaczenia kompresji TimescaleDB dla: + - `dlob_all_derived_ts` + - `dlob_hot_snapshot_ts` + - `dlob_hot_derived_ts` +4. Zaprojektowac schemat `dlob_features_1s`, `dlob_features_5s`, `dlob_regime_1m`, `bot_labels_horizons`. +5. Uznac raw DLOB za short-term storage, a feature bars + labels za long-term storage pod modele. + +## Decyzja robocza + +Obecny kierunek: + +- retencja raw zostaje +- kompresja powinna byc kolejnym krokiem +- dlugoterminowo nie trzymac wszystkiego jako raw snapshots +- dla modeli przejsc na warstwy cech i etykiet o nizszej skali diff --git a/dlob-services.md b/dlob-services.md new file mode 100644 index 0000000..45e58b4 --- /dev/null +++ b/dlob-services.md @@ -0,0 +1,243 @@ +# 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 diff --git a/drift-perps-monitoring.md b/drift-perps-monitoring.md new file mode 100644 index 0000000..d451ce5 --- /dev/null +++ b/drift-perps-monitoring.md @@ -0,0 +1,54 @@ +# Drift Perps – na co nasłuchiwać w monitoringu + +## 1) Co jest najważniejsze + +Jeżeli chcesz monitorować handel na Drift Perps, to nie wystarczy jedno konto. +Najważniejsze są: + +- `Drift Program ID`: `dRiftyHA39MWEi3m9aunc5MzRF1JYuBsbn6VPcn33UH` +- Konta rynków perp: `PerpMarket` (po `marketIndex` dla interesujących Cię marketów) +- Konto `EventSubscriber` – eventy handlowe i stanu DLOB +- Konta użytkownika/firmy jeśli śledzisz własne pozycje/realizację: `User`, `UserStats` + +## 2) Co monitorować jako pierwsze (w praktyce) + +- `OrderRecord` (utworzenia/zmiany zleceń) +- `OrderActionRecord` (fill/cancel/modify) +- `LiquidationRecord` +- `FundingRateRecord` +- `FundingPaymentRecord` +- `SettlePnlRecord` + +To zwykle wystarcza, żeby zobaczyć: + +- aktywność order flow, +- wykonania pozycji, +- likwidacje, +- warunki finansowania. + +## 3) Co zwykle monitoruje się do DLOB/realtime + +- `OrderSubscriber` + `DLOBSubscriber` (SDK Drift) lub dedykowane streamy DLOB. +- Feed order book w czasie rzeczywistym zamiast ręcznego `getProgramAccounts` na wszystkie konta. +- Filtrowanie tylko po: + - `marketType == perp` + - `marketIndex` (tylko rynki, które Cię interesują) + +## 4) Minimalna konfiguracja startowa + +1. Podepnij `DriftClient` z listą `perpMarketIndexes`. +2. Włącz `EventSubscriber` na wyżej wymienione `eventTypes`. +3. Dodaj parser dla eventów, który wyciąga: + - `marketIndex` + - `marketType` + - `user` + - `price`, `size`, `taker/maker`, `direction`, `liq/liab`, `time` +4. Odrzuć eventy off-market albo nie-tego rodzaju marketu. + +## 5) Odpowiedź na Twoje pytanie o „jaki konto monitorować” + +Nie ma jednego konta. + +- Jeśli chcesz widzieć **cały handel perps**: subskrybuj eventy z `EventSubscriber` + DLOB. +- Jeśli chcesz widzieć **konkretnych użytkowników**: dorzuć filtr na ich `User/UserStats`. +- Jeśli chcesz widzieć **konkretny rynek**: filtruj po `marketIndex` i `marketType = perp`. \ No newline at end of file diff --git a/economic-markers-k3s.md b/economic-markers-k3s.md new file mode 100644 index 0000000..915db4e --- /dev/null +++ b/economic-markers-k3s.md @@ -0,0 +1,533 @@ +# Economic Markers In K3s + +## Cel + +Chcemy dodawac markery ekonomiczne i tradingowe jako osobne komponenty runtime na `mevnode_bot`, bez mieszania ich z samym orderbookiem i bez bezposredniego odpytywania kazdego markera z frontendu. + +Docelowy model: + +`worker -> Postgres -> Hasura/API -> visualizer` + +To ma byc warstwa: + +- szybka do rozwijania +- niezalezna per marker +- latwa do obserwacji i debugowania +- bez ryzyka, ze jeden eksperymentalny marker psuje caly runtime + +## Zasada architektoniczna + +Markery liczymy w osobnych kontenerach na `k3s`, ale nie wystawiamy ich bezposrednio do UI. + +Kazdy marker: + +1. czyta dane z juz istniejacego runtime: + - `Hasura` + - `Postgres` + - `Redis` + - opcjonalnie `trade-api` + - opcjonalnie bezposrednio `mevnode_sol RPC`, jesli marker naprawde tego potrzebuje + +2. liczy wlasny sygnal + +3. zapisuje wynik do `Postgresa` + +4. `Hasura` trackuje tabele markerow + +5. `visualizer` rysuje: + - overlay na chart + - osobny panel pod chartem + - badge / regime / warning nad marketem + +Frontend nie powinien znac topologii workerow. + +## Dlaczego osobne kontenery + +To daje kilka korzysci: + +- izolacja awarii +- latwe rollouty +- rozne cadence per marker +- mozliwosc wlaczania / wylaczania markerow niezaleznie +- prostszy profiling CPU i memory +- latwiejszy eksperyment workflow + +Dobry podzial: + +- `fast markers` + - 200-500 ms + - mikrostruktura + - imbalance + - orderbook slope + - spread regime + +- `medium markers` + - 1-5 s + - funding pressure + - basis + - depth depletion + - oracle divergence + +- `slow markers` + - 10-60 s + - volatility regime + - realized vol + - trend regime + - event calendars + +## Rekomendowany model danych + +Nie robimy osobnej tabeli per kazdy marker. + +Na start lepszy jest wspolny model: + +### 1. `market_markers_latest` + +Jedna tabela z najnowsza wartoscia markera. + +Przyklad kolumn: + +- `source` +- `market_name` +- `marker_name` +- `ts` +- `value_num` +- `value_text` +- `value_bool` +- `band` +- `confidence` +- `severity` +- `meta jsonb` +- `updated_at` + +Klucz logiczny: + +`(source, market_name, marker_name)` + +To jest warstwa pod: + +- karty market details +- badge typu `risk on`, `thin book`, `high dislocation` +- ostatni stan markera + +### 2. `market_markers_ts` + +Historia timeseries dla markerow. + +Przyklad kolumn: + +- `event_ts` +- `id` +- `source` +- `market_name` +- `marker_name` +- `value_num` +- `value_text` +- `value_bool` +- `band` +- `confidence` +- `severity` +- `meta jsonb` +- `inserted_at` + +To jest warstwa pod: + +- overlaye na chart +- osobne panele +- replay +- debug +- porownanie markerow w czasie + +### 3. `market_marker_events` + +Opcjonalna tabela tylko dla zmian stanu i alertow. + +Przyklad: + +- `event_ts` +- `source` +- `market_name` +- `marker_name` +- `event_type` +- `prev_value` +- `next_value` +- `severity` +- `meta` + +To jest pod: + +- timeline alertow +- debug strategy +- feed zdarzen na UI + +## Typy markerow + +Najlepiej od razu rozdzielic markery na klasy. + +### Overlay markers + +To sa serie rysowane bezposrednio na wykresie ceny: + +- fair value +- local VWAP +- short-term imbalance line +- oracle divergence bands +- liquidation zones +- micro support / resistance + +Dane: + +- `value_num` +- opcjonalnie `band` +- opcjonalnie `meta.upper`, `meta.lower` + +### Panel markers + +To sa osobne serie pod wykresem: + +- spread bps +- imbalance +- depth asymmetry +- book velocity +- trade aggression +- realized vol + +### State markers + +To sa dyskretne stany: + +- `trend_up` +- `range` +- `high_risk` +- `thin_liquidity` +- `event_window` + +Tu zwykle: + +- `value_text` +- `value_bool` +- `severity` +- `confidence` + +## Rekomendowane markery na start + +Pierwszy pakiet powinien byc maly i praktyczny. + +### Marker 1. `oracle_dislocation_bps` + +Liczy: + +`(mark - oracle) / oracle * 10_000` + +Cel: + +- pokazac odklejenie od oracle +- ostrzegac, kiedy orderbook/mark zaczyna byc sztucznie przesuniety + +Prezentacja: + +- panel pod chartem +- badge nad marketem po przekroczeniu progow + +### Marker 2. `top12_imbalance` + +Liczy: + +`(bid_usd - ask_usd) / (bid_usd + ask_usd)` + +na poziomach zblizonych do tego, co pokazuje orderbook UI + +Cel: + +- prosty sygnal przewagi strony +- zgodnosc z tym, co trader widzi na ekranie + +Prezentacja: + +- panel +- opcjonalnie mini sparkline obok orderbooka + +### Marker 3. `spread_regime` + +Liczy stan: + +- `tight` +- `normal` +- `wide` + +na bazie `spread_bps` + +Cel: + +- szybki filtr, czy rynek nadaje sie do wejscia + +Prezentacja: + +- badge w market details + +### Marker 4. `depth_depletion` + +Liczy: + +- spadek plynnosci top-of-book vs rolling baseline + +Cel: + +- wykrywanie, ze ksiazka staje sie pusta zanim ruch przyspieszy + +Prezentacja: + +- panel +- alert event + +### Marker 5. `micro_vol_regime` + +Liczy: + +- rolling realized volatility na `1s` / `5s` / `1m` + +Cel: + +- odroznic spokojny market od marketu o zbyt duzym noise + +Prezentacja: + +- panel pod chartem + +## K3s deployment model + +Na start proponuje namespace `trade`. + +Kazdy marker to osobny `Deployment`, ale z tym samym kontraktem runtime. + +Przyklad nazw: + +- `marker-oracle-dislocation` +- `marker-top12-imbalance` +- `marker-spread-regime` +- `marker-depth-depletion` +- `marker-micro-vol` + +Kazdy deployment: + +- `replicas: 1` +- `ConfigMap` dla parametrow +- `Secret` tylko jesli musi czytac cos wrazliwego +- write tylko do Postgresa / Hasury + +Nie robimy dla nich publicznych `Service`, jesli nie maja HTTP health/debug endpointu. + +Jesli worker ma HTTP: + +- tylko `ClusterIP` +- endpointy: + - `/healthz` + - `/metrics` + - `/debug/state` + +## Read path dla markerow + +Na start marker powinien czytac glownie z durable read modelu: + +- `dlob_hot_snapshot_latest` +- `dlob_hot_derived_latest` +- `dlob_hot_derived_ts` +- `dlob_all_derived_latest` +- `dlob_all_derived_ts` +- `dlob_stats_latest` +- `dlob_depth_bps_latest` +- `dlob_slippage_latest` + +To daje: + +- prostszy kod +- mniej zaleznosci +- zgodnosc z tym, co widzi UI + +Bezposredni odczyt z `mevnode_sol RPC` ma sens tylko wtedy, gdy: + +- marker potrzebuje point read accountow +- marker potrzebuje szybszego feedu niz Postgres +- marker ma byc trading-critical, a nie tylko UI/debug + +## Write path dla markerow + +Najbezpieczniejszy model: + +- worker liczy marker +- worker robi `upsert` do `market_markers_latest` +- worker robi `insert` do `market_markers_ts` + +Opcjonalnie: + +- worker emituje event do `market_marker_events`, gdy zmienia sie `band` albo `severity` + +## Hasura + +Hasura powinna trackowac: + +- `market_markers_latest` +- `market_markers_ts` +- `market_marker_events` + +Public read: + +- tylko wybrane kolumny +- filtr po `market_name` +- opcjonalnie filtr po `marker_name` + +To pozwala frontendowi robic: + +- subskrypcje latest +- query historii per marker +- timeline eventow + +## Visualizer + +Visualizer powinien miec trzy tryby prezentacji markerow. + +### 1. Overlay na chart + +Dla markerow typu line / band: + +- `fair value` +- `oracle dislocation bands` +- `micro support` + +UI: + +- toggle per marker +- wspolny selector `Indicators / Markers` + +### 2. Panele pod chartem + +Dla markerow oscylatorowych: + +- imbalance +- spread bps +- micro vol + +UI: + +- jeden panel moze pokazywac kilka serii +- albo osobne male panele + +### 3. Market badges + +Dla stanow: + +- `thin book` +- `wide spread` +- `high dislocation` +- `event risk` + +UI: + +- nad chartem +- nad orderbookiem +- w market details + +## Co jest trading-critical, a co research-only + +Trzeba to rozdzielic od poczatku. + +### Trading-critical + +Markery, ktore moga wejsc do decyzji runtime: + +- dislocation +- spread regime +- top book depletion +- aggressive imbalance + +Dla nich: + +- niski latency +- jasne SLA +- monitoring +- alerting + +### Research-only + +Markery, ktore sa glownie dla obserwacji: + +- eksperymentalne composite scores +- event overlays +- egzotyczne oscylatory + +Dla nich: + +- moga byc liczone wolniej +- moga siedziec w osobnych workerach +- nie powinny blokowac runtime + +## Rekomendowany rollout + +### Etap 1 + +Zrobic wspolne tabele: + +- `market_markers_latest` +- `market_markers_ts` + +oraz trackowanie w Hasurze. + +### Etap 2 + +Uruchomic 2 pierwsze markery: + +- `oracle_dislocation_bps` +- `top12_imbalance` + +### Etap 3 + +Dorysowac to w visualizerze: + +- 1 overlay +- 1 panel +- 1 badge + +### Etap 4 + +Dopiero potem dodawac kolejne markery. + +Nie zaczynac od 10 workerow naraz. + +## Kontrakt implementacyjny workerow + +Kazdy worker powinien miec: + +- jeden glowny loop +- czytelny input set +- czytelny output set +- idempotentny write +- logging per cycle +- proste metryki Prometheus + +Minimum metryk: + +- `marker_compute_duration_ms` +- `marker_last_success_ts` +- `marker_rows_written_total` +- `marker_failures_total` +- `marker_input_staleness_ms` + +## Moja rekomendacja dla tego projektu + +Na teraz najlepszy model to: + +1. osobne workery markerow w `k3s` +2. wspolne tabele `latest + ts` +3. `Hasura` jako read layer +4. `visualizer` jako warstwa renderu +5. `mevnode_sol RPC` tylko dla markerow, ktore naprawde musza wyjsc poza aktualny read model + +To jest wystarczajaco porzadne pod trade, a jednoczesnie nadal lekkie i szybkie do rozwijania. + +## Co warto zrobic jutro + +1. Dodac SQL dla `market_markers_latest` i `market_markers_ts`. +2. Doliczyc pierwszy marker `oracle_dislocation_bps`. +3. Doliczyc drugi marker `top12_imbalance`. +4. Wystawic obie serie przez Hasure. +5. Narysowac je w visualizerze jako proof of workflow. diff --git a/frontent-local-run.md b/frontent-local-run.md new file mode 100644 index 0000000..812bd20 --- /dev/null +++ b/frontent-local-run.md @@ -0,0 +1,205 @@ +# Frontend Local Run + +Ten dokument opisuje, jak uruchamiać lokalnie `apps/visualizer` i podpiąć go pod backend ze `trade-staging` na `mevnode_bot`. + +## Założenia + +- pracujesz w repo `/home/user/dev/trade` +- masz dostęp SSH do `user@mevnode_bot` +- chcesz uruchamiać tylko lokalny frontend; backend zostaje na stagingu + +## 1. Uruchom tunel SSH + +Frontend lokalny używa trzech upstreamów: + +- `13081 -> trade-frontend` na `mevnode_bot` +- `18787 -> trade-api` w klastrze `trade-staging` +- `18080 -> hasura` w klastrze `trade-staging` + +Najpierw sprawdź aktualny `ClusterIP` dla `trade-api` i `hasura`: + +```bash +ssh user@mevnode_bot "kubectl -n trade-staging get svc trade-api hasura -o custom-columns=NAME:.metadata.name,CLUSTER_IP:.spec.clusterIP" +``` + +Potem uruchom tunel. Wstaw zwrócone adresy zamiast `TRADE_API_CLUSTER_IP` i `HASURA_CLUSTER_IP`: + +```bash +ssh -o ExitOnForwardFailure=yes -N \ + -L 13081:127.0.0.1:30081 \ + -L 18787:TRADE_API_CLUSTER_IP:8787 \ + -L 18080:HASURA_CLUSTER_IP:8080 \ + user@mevnode_bot +``` + +Uwagi: + +- `13081` obsługuje `/whoami`, `/auth`, `/logout` +- `18787` obsługuje `/api` +- `18080` obsługuje `/graphql` i `/graphql-ws` +- jeśli tunel padnie, lokalny Vite zacznie zwracać `ECONNREFUSED` +- `18080 -> 127.0.0.1:8080` nie zakładaj w ciemno; Hasura nie musi być wystawiona lokalnie na `mevnode_bot` + +## 2. Sprawdź lokalną konfigurację visualizera + +`apps/visualizer/.env.local` powinno mieć co najmniej: + +```env +VITE_DEV_PORT=5174 + +VITE_API_URL=/api +VITE_HASURA_URL=/graphql +VITE_HASURA_WS_URL=ws://127.0.0.1:18080/v1/graphql +VITE_GRAPHQL_POLL_MS=1000 + +VITE_SYMBOL=SOL-PERP +VITE_TF=1s +VITE_POLL_MS=1000 +VITE_LIMIT=300 + +API_PROXY_TARGET=http://127.0.0.1:18787 +API_PROXY_STRIP_PREFIX=/api +FRONTEND_PROXY_TARGET=http://127.0.0.1:13081 +GRAPHQL_PROXY_TARGET=http://127.0.0.1:18080 +GRAPHQL_PROXY_PATH=/v1/graphql +``` + +Ważne: + +- `VITE_HASURA_URL` ma zostać ustawione na `/graphql` +- `VITE_HASURA_WS_URL` ma wskazywać bezpośrednio na tunel Hasury `ws://127.0.0.1:18080/v1/graphql` +- nie ustawiaj `VITE_GRAPHQL_POLL_ONLY=1`, jeśli chcesz live `ws` dla orderbooka i stats +- `npm run visualizer:dev` respektuje lokalne `.env.*` i nie powinno już nadpisywać tego adresem z `tokens/hasura.json` + +## 3. Uruchom lokalny Vite + +Z roota repo: + +```bash +cd /home/user/dev/trade +npm install +npm run visualizer:dev +``` + +Visualiser wystartuje na: + +```text +http://localhost:5174/ +``` + +## 4. Zaloguj się + +Po wejściu na `http://localhost:5174/` aplikacja powinna pokazać ekran logowania. + +Użyj stagingowych danych sesyjnych. W local dev nie polegamy już na `DEV_PROXY_AUTH_USER`. + +## 5. Smoke check + +Po zalogowaniu sprawdź: + +- górny pasek pokazuje użytkownika i przycisk `Wyloguj` +- orderbook ma rekordy dla `SOL-PERP` +- panel DLOB pokazuje `live` +- chart nie pokazuje `Failed to fetch` + +## Szybki troubleshooting + +### Sprawdź porty + +```bash +ss -ltnp | rg ':(5174|13081|18080|18787)\b' +``` + +### Sprawdź sesję + +```bash +curl -i http://localhost:5174/whoami +``` + +Po zalogowaniu powinno zwrócić `200` i `{"ok":true,"user":"...","mode":"session"}`. + +### Sprawdź chart API + +```bash +curl -i 'http://localhost:5174/api/v1/chart?symbol=SOL-PERP&tf=1s&limit=5' +``` + +Jeśli tu jest `ECONNREFUSED`, problem siedzi w tunelu `18787`. + +### Sprawdź GraphQL proxy + +```bash +curl -i \ + -H 'content-type: application/json' \ + --data '{"query":"query { dlob_hot_snapshot_latest(limit: 1, where: {market_name: {_eq: \"SOL-PERP\"}}) { market_name } }"}' \ + http://localhost:5174/graphql +``` + +Jeśli przeglądarka próbuje iść do `localhost:8080` zamiast do `localhost:5174/graphql`, to Vite nie wczytał poprawnego `.env.local`. Zrestartuj `npm run visualizer:dev`. + +### Objaw: logowanie działa, ale DLOB / L2 pokazuje `Hasura HTTP 500` + +Typowy objaw: + +- logowanie przez `/auth/login` przechodzi +- `/whoami` zwraca `200` +- w UI pola `DLOB` i `L2` pokazują `Hasura HTTP 500` +- w logu Vite widać `http proxy error: /v1/graphql` oraz `ECONNRESET` albo `ECONNREFUSED` + +Najczęstsza przyczyna: + +- tunel `18080` jest skierowany na zły remote target +- nie zakładaj, że `mevnode_bot` ma lokalny nasłuch Hasury na `127.0.0.1:8080` + +Sprawdź aktualny adres Hasury: + +```bash +ssh user@mevnode_bot "kubectl -n trade-staging get svc hasura -o wide" +``` + +Jeśli `ClusterIP` Hasury to np. `10.43.174.102`, to tunel powinien wyglądać tak: + +```bash +ssh -o ExitOnForwardFailure=yes -N \ + -L 13081:127.0.0.1:30081 \ + -L 18787:TRADE_API_CLUSTER_IP:8787 \ + -L 18080:10.43.174.102:8080 \ + user@mevnode_bot +``` + +Szybki test po poprawce: + +```bash +curl -i \ + -H 'content-type: application/json' \ + --data '{"query":"query { __typename }"}' \ + http://127.0.0.1:18080/v1/graphql +``` + +To powinno zwrócić `200 OK` i `{"data":{"__typename":"query_root"}}`. + +### Objaw: DLOB działa, ale `Last` / `Oracle` albo chart są puste + +To nie musi oznaczać problemu z frontendem. + +Najpierw sprawdź live DLOB: + +```bash +curl -sS \ + -H 'content-type: application/json' \ + --data '{"query":"query($market:String!){ dlob_hot_derived_latest(where:{market_name:{_eq:$market},is_indicative:{_eq:false}},limit:1){ market_name mark_price oracle_price best_bid_price best_ask_price updated_at }}","variables":{"market":"SOL-PERP"}}' \ + http://localhost:5174/graphql +``` + +Potem sprawdź chart API: + +```bash +curl -i 'http://localhost:5174/api/v1/chart?symbol=SOL-PERP&tf=1s&limit=5' +curl -i 'http://localhost:5174/api/v1/ticks?symbol=SOL-PERP&limit=5' +``` + +Interpretacja: + +- jeśli GraphQL zwraca świeże `updated_at`, ale `/api/v1/chart` zwraca `candles: []`, to problem siedzi w stagingowym API albo danych wejściowych do funkcji świec +- jeśli `/api/v1/ticks` zwraca tylko stare rekordy, staging nie ma świeżych ticków dla chart API +- summary w UI może wtedy żyć z fallbacku DLOB, ale sam wykres dalej będzie pusty, dopóki staging nie zacznie zwracać świec diff --git a/gitea-repo-cleanup-plan.md b/gitea-repo-cleanup-plan.md new file mode 100644 index 0000000..9a9a331 --- /dev/null +++ b/gitea-repo-cleanup-plan.md @@ -0,0 +1,275 @@ +# Gitea repo cleanup plan + +Stan na `2026-03-13`. + +Cel: +- uporzadkowac repo `trade/*` na `gitea.mpabi.pl` +- wskazac jednoznaczne source of truth +- odseparowac repo aktywne od historycznych lub porzuconych +- uniknac kasowania aktywnych danych w ciemno + +## Zasady + +1. Najpierw archiwizacja, nie usuwanie. +2. Najpierw ustalic source of truth, potem porzadkowac branche. +3. Nie zostawiac repo, ktore duplikuja aktywny kod w innym miejscu. +4. `trade-deploy` i `trade-iac` traktowac jako repo operacyjne o wysokim priorytecie. +5. Po cleanupie kazde aktywne repo ma miec: + - poprawny remote + - aktywnego ownera + - jasna role + - aktualne README + +## Repo po repo + +### `trade-frontend` + +Status: +- aktywne +- lokalne source of truth dla UI i `apps/visualizer` +- na Gitei sa juz aktualne branche robocze + +Decyzja: +- zostawic + +Dzialania: +- utrzymac jako repo dla: + - frontend SPA + - visualizer + - dokumentacji frontend/runtime, jesli ma zostac blisko kodu +- zamknac stare branche po merge +- nie wydzielac ponownie `trade-visualizer` + +### `trade-api` + +Status: +- aktywne logicznie +- lokalny checkout nadal wskazuje na stare `rv32i.pl` +- lokalny `main` rozjechany z `gitea.mpabi.pl/trade/trade-api` + +Decyzja: +- zostawic +- uporzadkowac natychmiast + +Dzialania: +- przepiac local remote na `gitea.mpabi.pl` +- porownac lokalny `main` z Gitea `main` +- zdecydowac: + - czy lokalny checkout jest historyczny i do porzucenia + - czy trzeba go zrebase'owac/cherry-pickowac na nowe repo +- dopiero potem wznowic aktywne prace + +### `trade-ingestor` + +Status: +- aktywne +- lokalny checkout jest zgodny z Gitea +- rola w runtime wymaga przedefiniowania + +Decyzja: +- zostawic + +Dzialania: +- zachowac repo +- zmienic role z glownego ingestu na: + - pomocniczy ingest + - backfill + - normalizacja danych pomocniczych +- dopisac README z aktualna rola po redesignie DLOB + +### `trade-drift-dlob` + +Status: +- repo istnieje na Gitei +- lokalnie aktywnie pracujesz na `dlob-server` +- source of truth nie jest jeszcze czyste + +Decyzja: +- zostawic +- uporzadkowac zaraz po `trade-api` + +Dzialania: +- zdecydowac, czy `dlob-server` ma zostac przeniesiony do `trade-drift-dlob` +- jesli tak: + - uznac `trade-drift-dlob` za docelowe repo + - przeniesc aktualny kod i workflow tam +- jesli nie: + - zarchiwizowac `trade-drift-dlob` + - ale tylko po migracji kodu do repo docelowego + +### `trade-deploy` + +Status: +- aktywne +- repo GitOps +- na Gitei jest aktualny branch z overlay `mevnode-bot` + +Decyzja: +- zostawic + +Dzialania: +- utrzymac jako jedyne source of truth dla runtime `k3s` +- nie trzymac runtime patchy poza tym repo +- porzadkowac branche po merge + +### `trade-iac` + +Status: +- aktywne +- zawiera bootstrap/infra +- branch roboczy `step2-solana-rpc` jest na Gitei + +Decyzja: +- zostawic + +Dzialania: +- utrzymac jako source of truth dla: + - bootstrap hostow + - k3s + - wireguard + - provisioning +- po merge wyczyscic stare branche robocze + +### `trade-infra` + +Status: +- repo istnieje +- lokalny checkout jest zsynchronizowany +- jego rola nie jest dzis jednoznaczna + +Decyzja: +- tymczasowo zostawic +- wymagac decyzji architektonicznej + +Dzialania: +- wybrac jedna z dwoch rol: + - umbrella/superproject + - repo narzedziowe ops +- jesli nie dostanie konkretnej roli, zarchiwizowac + +### `trade-doc` + +Status: +- repo istnieje +- lokalny checkout zsynchronizowany +- czesc aktywnej dokumentacji zyje jednak w `trade-frontend` + +Decyzja: +- tymczasowo zostawic +- wymagac decyzji o modelu dokumentacji + +Dzialania: +- wybrac jedna z dwoch strategii: + - docs centralne w `trade-doc` + - docs przy repo kodowym +- jesli wybierzesz docs przy kodzie: + - `trade-doc` zarchiwizowac po migracji najwazniejszych materialow + +### `trade-visualizer` + +Status: +- repo istnieje na Gitei +- lokalnie `visualizer` zyje w `trade-frontend` +- brak sensu utrzymywac dwoch rownoleglych repo dla tego samego obszaru + +Decyzja: +- archiwizowac + +Dzialania: +- sprawdzic, czy nie ma tam unikalnego kodu do uratowania +- jesli nie ma: + - oznaczyc repo jako archived + - dopisac w opisie, ze `visualizer` jest teraz w `trade-frontend` + +### `trade-inc` + +Status: +- repo istnieje na Gitei +- brak lokalnego checkoutu +- brak widocznego uzycia w deployu i codziennej pracy + +Decyzja: +- archiwizowac + +Dzialania: +- sprawdzic, czy nie ma tam czegos krytycznego historycznie +- jesli nie ma: + - oznaczyc repo jako archived + +## Priorytety wykonania + +### Faza 1. Uporzadkowanie aktywnych repo + +1. `trade-api` +2. `trade-drift-dlob` +3. `trade-deploy` +4. `trade-iac` + +Cel: +- miec wszystkie aktywne repo na poprawnych remote +- miec jasny source of truth dla kodu i deployu + +### Faza 2. Decyzje architektoniczne + +1. `trade-doc` +2. `trade-infra` + +Cel: +- zdecydowac, czy te repo maja nadal odrebna wartosc + +### Faza 3. Archiwizacja repo zbednych + +1. `trade-visualizer` +2. `trade-inc` + +Cel: +- odchudzic Gitee bez utraty historii + +## Plan zmian operacyjnych + +### Krok 1. `trade-api` + +- przepiac local remote na `gitea.mpabi.pl` +- pobrac `origin/main` +- porownac commit lokalny z Gitea +- zdecydowac, czy: + - zachowac lokalny checkout + - czy porzucic go i klonowac od nowa + +### Krok 2. `trade-drift-dlob` + +- porownac repo `trade-drift-dlob` z lokalnym `dlob-server` +- wybrac repo docelowe +- dopiero potem przenosic redesign DLOB + +### Krok 3. `trade-doc` vs docs przy kodzie + +- jesli dokumentacja ma byc centralna: + - przenosic nowe materialy do `trade-doc` +- jesli dokumentacja ma byc przy repo: + - stopniowo wygaszac `trade-doc` + +### Krok 4. Archiwizacja + +- ustawic `Archived` na: + - `trade-visualizer` + - `trade-inc` +- bez usuwania repo + +## Rekomendacja koncowa + +Docelowy zestaw aktywnych repo: +- `trade-frontend` +- `trade-api` +- `trade-ingestor` +- `trade-drift-dlob` +- `trade-deploy` +- `trade-iac` + +Repo warunkowe: +- `trade-infra` +- `trade-doc` + +Repo do archiwizacji: +- `trade-visualizer` +- `trade-inc` diff --git a/gitea-tree.md b/gitea-tree.md new file mode 100644 index 0000000..19448f3 --- /dev/null +++ b/gitea-tree.md @@ -0,0 +1,143 @@ +# Gitea Tree + +To drzewo rozdziela: + +- `repo kodowe`: gdzie utrzymywany jest kod danego bloku +- `repo deployowe`: gdzie utrzymywane sa manifesty uruchomieniowe + +## Drzewo + +```text +trade-frontend.git +├─ trade-frontend +├─ trade-api +├─ momentum-service +└─ bot-observer + +trade-drift-dlob.git +├─ dlob-publisher-hot +├─ dlob-publisher-all +├─ dlob-hot-redis-to-postgres-raw-writer +├─ dlob-hot-postgres-to-postgres-derived-writer +└─ dlob-all-redis-to-postgres-derived-writer + +trade-deploy.git +├─ deploy trade-frontend +├─ deploy trade-api +├─ deploy dlob-publisher-hot +├─ deploy dlob-publisher-all +├─ deploy dlob-hot-redis-to-postgres-raw-writer +├─ deploy dlob-hot-postgres-to-postgres-derived-writer +├─ deploy dlob-all-redis-to-postgres-derived-writer +├─ dlob-redis +├─ postgres +└─ hasura + +trade-iac.git +└─ host / infra provisioning +``` + +## Uwaga + +- `deploy` nie oznacza osobnego kodu biznesowego. Oznacza warstwe uruchomienia: `k8s`, `kustomize`, `ArgoCD`, `env`, `secrets`, `resources`, `probes`. +- Jeden blok moze miec osobno `repo kodowe` i osobno `repo deployowe`. +- Dla aktywnego stacku DLOB kod siedzi glownie w `trade-drift-dlob.git`, a uruchomienie w `trade-deploy.git`. + +## Czy robic repo per bloczek + +Nie jako domyslny kierunek. + +Lepszy jest podzial `repo per domena/runtime` niz `repo per deployment`. + +Powody: + +- repo per bloczek szybko duplikuje typy, helpery, CI i releasy +- publishery i writery DLOB sa ze soba mocno sprzezone +- wiele zmian przechodzi przez kilka blokow naraz +- koszt synchronizacji wielu malych repo zwykle przewaza zysk + +Dla obecnego ukladu sensowny podzial to: + +- `trade-drift-dlob.git` dla warstwy market data / DLOB runtime +- `trade-frontend.git` dla UI i lekkich uslug aplikacyjnych +- `trade-deploy.git` dla uruchomienia w klastrze +- `trade-iac.git` dla hosta i infrastruktury + +## Proponowany Podzial: trade-bot.git + +Jesli wydzielac bota do osobnego repo, to tylko warstwe bot logic / strategy / execution. + +Naturalni kandydaci: + +- `bot-observer` +- `momentum-service` +- przyszly executor +- future control-plane / config-state adaptery + +Tego nie warto pakowac do repo bota: + +- `dlob-publisher-hot` +- `dlob-publisher-all` +- writery DLOB +- `postgres` +- `hasura` +- runtime `dlob-redis` + +To jest wspolny data plane, a nie logika jednego bota. + +## Rola trade-deploy i trade-iac + +Po wydzieleniu bota uklad powinien byc taki: + +- `trade-bot.git` + kod bota: observer, momentum, strategy, executor, adaptery + +- `trade-deploy.git` + manifesty uruchomieniowe: `Deployment`, `CronJob`, `Secret` refs, `env`, `resources`, `rollout` + +- `trade-iac.git` + infrastruktura nizszego poziomu: hosty, provisioning, `k3s` bootstrap, storage, DNS, certy, siec + +Praktyczny podzial odpowiedzialnosci: + +- `trade-iac` = jak istnieje srodowisko +- `trade-deploy` = co jest wdrozone na srodowisku +- `trade-bot` = co robi bot + +## Docelowe Drzewo + +```text +trade-frontend.git +├─ trade-frontend +└─ trade-api + +trade-drift-dlob.git +├─ dlob-publisher-hot +├─ dlob-publisher-all +├─ dlob-hot-redis-to-postgres-raw-writer +├─ dlob-hot-postgres-to-postgres-derived-writer +└─ dlob-all-redis-to-postgres-derived-writer + +trade-bot.git +├─ bot-observer +├─ momentum-service +├─ strategy +├─ executor +└─ bot control-plane + +trade-deploy.git +├─ deploy trade-frontend +├─ deploy trade-api +├─ deploy trade-bot +├─ deploy dlob-publisher-hot +├─ deploy dlob-publisher-all +├─ deploy dlob-hot-redis-to-postgres-raw-writer +├─ deploy dlob-hot-postgres-to-postgres-derived-writer +├─ deploy dlob-all-redis-to-postgres-derived-writer +├─ dlob-redis +├─ postgres +└─ hasura + +trade-iac.git +└─ host / infra provisioning +``` diff --git a/mevnode-bot-architecture.md b/mevnode-bot-architecture.md new file mode 100644 index 0000000..3f8da00 --- /dev/null +++ b/mevnode-bot-architecture.md @@ -0,0 +1,245 @@ +# Mevnode Bot Architecture + +## Zasada glowna + +`mevnode_bot` nigdy nie uruchamia warstwy Solana node: + +- bez `agave` +- bez `ledger` +- bez `yellowstone server` +- bez validator stack + +`mevnode_sol` pozostaje jedynym hostem node-level: + +- `agave-validator` +- `yellowstone geyser plugin` +- `yellowstone gRPC` +- plugin metrics +- lokalny RPC +- opcjonalnie pozniej RPC proxy + +## Schemat `mevnode_bot` + +```text ++--------------------------------------------------------------+ +| mevnode_bot | +| single-node k3s only | +| | +| ingress/web | +| - frontend | +| - api | +| | +| trading/read | +| - yellowstone-consumer | +| - market ingest / parser | +| - dlob-worker | +| | +| trading/core | +| - strategy-engine | +| - risk-manager | +| - order-manager | +| - tx-router | +| | +| tx paths | +| - tpu client | +| - jito client | +| - rpc client / later rpc proxy target on mevnode_sol | +| | +| data | +| - postgres | +| - redis optional | +| - clickhouse later optional | +| | +| ops | +| - prometheus | +| - grafana | +| - loki optional | +| | +| platform | +| - argo/flux | +| - sealed-secrets or sops | +| - internal registry access | ++--------------------------------------------------------------+ +``` + +## Przeplyw danych + +```text +Yellowstone on mevnode_sol + | + v +yellowstone-consumer + | + v +parser / ingest / dlob-worker + | + v +strategy-engine + | + v +risk-manager / order-manager + | + v +tx-router + | | | + v v v + TPU Jito RPC +``` + +## Namespace plan + +- `web` +- `trade` +- `data` +- `ops` +- `infra` + +## Kolejnosc wdrozenia + +1. `web`: `frontend`, `api` +2. `data`: `postgres` +3. `trade`: `yellowstone-consumer` +4. `trade`: `strategy-engine`, `tx-router` +5. `ops`: `prometheus`, `grafana` + +## Read i write path + +Read path: + +- `mevnode_bot -> wg0 -> mevnode_sol:10000` + +Metrics path: + +- `mevnode_bot -> wg0 -> mevnode_sol:8999` + +Write path pozniej: + +- `mevnode_bot -> TPU/Jito` +- albo `mevnode_bot -> RPC proxy -> mevnode_sol:127.0.0.1:8899` + +## Zasada architektoniczna + +- `mevnode_sol` = source of truth dla chain access +- `mevnode_bot` = execution and product node + +## K3s object map + +### Namespace `web` + +- `Deployment` `frontend` +- `Service` `frontend` typu `ClusterIP` +- `Deployment` `api` +- `Service` `api` typu `ClusterIP` +- `Ingress` `web` + +Przeznaczenie: + +- `frontend` wystawia UI +- `api` wystawia backend HTTP +- `Ingress` publikuje tylko web entrypoint + +### Namespace `trade` + +- `Deployment` `yellowstone-consumer` +- `Deployment` `dlob-worker` +- `Deployment` `strategy-engine` +- `Deployment` `risk-manager` +- `Deployment` `order-manager` +- `Deployment` `tx-router` +- `Service` `strategy-engine` typu `ClusterIP` jesli inne komponenty maja sie do niego odwolywac +- `Service` `tx-router` typu `ClusterIP` tylko jesli potrzebne bedzie sterowanie po HTTP/gRPC + +Przeznaczenie: + +- `yellowstone-consumer` czyta stream z `mevnode_sol` +- `dlob-worker` robi ingest i przygotowanie danych +- `strategy-engine` podejmuje decyzje +- `risk-manager` egzekwuje limity +- `order-manager` sklada i nadzoruje lifecycle zlecen +- `tx-router` wybiera `TPU`, `Jito` albo `RPC` + +Uwagi: + +- `strategy-engine`, `order-manager` i `tx-router` mozna uruchamiac z `hostNetwork: true`, jesli bedzie potrzeba minimalizacji narzutu sieciowego +- `yellowstone-consumer` powinien miec `Secret` z `x-token` i endpointem `10.91.0.1:10000` + +### Namespace `data` + +- `StatefulSet` `postgres` +- `Service` `postgres` typu `ClusterIP` +- `PersistentVolumeClaim` dla `postgres` +- `StatefulSet` `redis` opcjonalnie +- `Service` `redis` typu `ClusterIP` +- `PersistentVolumeClaim` dla `redis` tylko jesli ma byc trwaly cache +- `StatefulSet` `clickhouse` dopiero pozniej +- `Service` `clickhouse` typu `ClusterIP` +- `PersistentVolumeClaim` dla `clickhouse` + +Przeznaczenie: + +- `postgres` dla stanu systemu, pozycji, filli, konfiguracji i jobow +- `redis` dla cache, kolejek, lockow i rate limitu +- `clickhouse` dla ciezszej analityki i archiwizacji eventow + +### Namespace `ops` + +- `StatefulSet` `prometheus` +- `Service` `prometheus` typu `ClusterIP` +- `Deployment` `grafana` +- `Service` `grafana` typu `ClusterIP` +- `Deployment` `promtail` opcjonalnie +- `StatefulSet` albo `Deployment` `loki` opcjonalnie + +Przeznaczenie: + +- `prometheus` zbiera metryki z `k3s` i z aplikacji +- `grafana` daje dashboardy +- `loki/promtail` sa opcjonalne do logow + +### Namespace `infra` + +- `Deployment` `argocd-server` albo kontrolery `flux` +- `Deployment` `argocd-repo-server` jesli wybierzemy `Argo CD` +- `Deployment` `sealed-secrets-controller` albo integracja `sops` +- `Secret` / `ExternalSecret` dla tokenow i kluczy + +Przeznaczenie: + +- GitOps sync z Gitea +- zarzadzanie sekretami +- bootstrap aplikacji i manifestow + +## Service exposure rules + +- tylko `frontend` i `api` wychodza przez `Ingress` +- `postgres`, `redis`, `clickhouse`, `strategy-engine`, `tx-router` zostaja wewnetrzne jako `ClusterIP` +- bez `LoadBalancer` +- bez publicznego wystawiania komponentow tradingowych +- `yellowstone-consumer` nie potrzebuje publicznego `Service` + +## Storage rules + +- `postgres` na trwałym wolumenie +- `clickhouse` tylko na szybkim lokalnym storage +- `redis` bez PVC jesli ma byc tylko cache +- aplikacje tradingowe jako stateless `Deployment`, jesli stan trzymamy w `postgres` + +## Config and secrets + +- `ConfigMap` dla endpointow, feature flags i runtime config +- `Secret` dla: + - tokenu Yellowstone + - kluczy API + - danych do `postgres` + - danych do `Jito` + - kluczy signing/delegate + +## Kolejnosc wdrozenia w k3s + +1. `infra`: GitOps i secrets +2. `data`: `postgres` +3. `web`: `frontend`, `api`, `Ingress` +4. `trade`: `yellowstone-consumer` +5. `trade`: `strategy-engine`, `risk-manager`, `order-manager` +6. `trade`: `tx-router` +7. `ops`: `prometheus`, `grafana` diff --git a/mevnode-bot-topology-interactive.html b/mevnode-bot-topology-interactive.html new file mode 100644 index 0000000..82b9f8c --- /dev/null +++ b/mevnode-bot-topology-interactive.html @@ -0,0 +1,1082 @@ + + + + + + mevnode interactive topology + + + +
+
+
+
Interactive topology / click to inspect
+

Interaktywny diagram uslug na mevnode_bot i zaleznosci od mevnode_sol

+

+ Klikaj komponenty albo filtruj sciezki. Diagram rozdziela read plane, + write plane, data plane i ops plane. Celem jest utrzymanie twardego podzialu: + validator zostaje na sol, a produkt, ingest i execution + dzialaja na bot w k3s. +

+
+ + + + + +
+
+ + +
+ +
+
+
+
+
+
+

mevnode_sol

+ bare metal +
+ +
+
validator / rpc
+

Agave validator

+

Source of truth dla chain state, snapshotow i lokalnego RPC.

+
+ 127.0.0.1:8899 + getAccountInfo + getMultipleAccounts +
+
+ +
+
geyser / live stream
+

Yellowstone gRPC

+

Pushuje account updates, sloty, tx i block meta po wg0.

+
+ 10.91.0.1:10000 + x-token +
+
+ +
+
monitoring
+

Plugin metrics

+

Metryki pluginu i gRPC do Prometheusa i diagnostyki.

+
+ 10.91.0.1:8999 +
+
+
+ +
+ +
yellowstone / live feed
+ +
rpc / tpu / jito
+ +
+ +
+
+

mevnode_bot

+ single-node k3s +
+ +
+
trade namespace
+

yellowstone-consumer

+

Bierze live feed z Yellowstone i zamienia go na dane wewnetrzne.

+
+ accounts + slots + transactions +
+
+ +
+
trade namespace
+

trade-ingestor

+

Transformuje dane i zapisuje je do DB lub feedow uslugowych.

+
+ +
+
trade namespace
+

dlob-publisher

+

Czyta RPC Agavy i buduje stan DLOB, ktory trafia do Redis.

+
+ getMultipleAccounts + Redis +
+
+ +
+
trade namespace
+

dlob-server

+

Serwuje gotowy widok DLOB dla API i wizualizacji.

+
+ +
+
execution
+

strategy-engine

+

Konsumuje feed i decyduje o akcjach tradingowych.

+
+ +
+
execution
+

tx-router

+

Wybiera write path: RPC, TPU albo Jito.

+
+ +
+
data namespace
+

Postgres + Hasura

+

Trwaly storage, stan aplikacji, historia, pozycje i GraphQL.

+
+ +
+
data namespace
+

Redis

+

Cache, pub/sub i szybki stan DLOB dla serwisow read-side.

+
+ +
+
web namespace
+

Frontend + API + Ingress

+

Warstwa produktu. UI pokazuje dane, API agreguje odpowiedzi dla klienta.

+
+ +
+
ops namespace
+

Prometheus / Grafana / Portainer / GitOps

+

Monitoring, obsluga klastra, deployment z Gitea i obserwowalnosc.

+
+
+
+
+
+ + +
+ +
+
+

k3s object map

+

+ To jest aktualny rozklad obiektow w klastrze mevnode_bot: + workloady, uslugi i typy ekspozycji. Tu widac juz konkretne + Deployment, StatefulSet, ClusterIP, + NodePort i stan gotowosci. +

+
+ +
+
+

kube-system

+ Podstawowe komponenty klastra potrzebne, zeby single-node k3s dzialal stabilnie. + +
+
+
+ metrics-server +
Deployment + ClusterIP
+
+ 1/1 ready +
+

Service metrics-server na porcie 443.

+
+ +
+
+
+ coredns +
Deployment + ClusterIP
+
+ 1/1 ready +
+

Service kube-dns z DNS wewnatrz klastra.

+
+ +
+
+
+ local-path-provisioner +
Deployment
+
+ 1/1 ready +
+

Provisioning lokalnego storage dla PVC na pojedynczym nodzie.

+
+
+ +
+

portainer

+ Most pomiedzy Dockerowym Portainerem na hoscie a lokalnym klastrem k3s. + +
+
+
+ portainer-agent +
Deployment + NodePort
+
+ 1/1 ready +
+

Service portainer-agent wystawiony jako NodePort 30778 -> 9001.

+
+ +
+
+
+ portainer-agent-headless +
Headless Service
+
+ cluster wiring +
+

Uzywany przez agenta do wewnetrznego cluster discovery.

+
+
+ +
+

trade-staging

+ Glowny namespace produktu: web, ingest, DLOB i bazy danych. + +
+
+
+ trade-frontend +
Deployment + NodePort
+
+ 1/1 ready +
+

Service trade-frontend: NodePort 30081 -> 8081.

+
+ +
+
+
+ trade-api +
Deployment + ClusterIP
+
+ 1/1 ready +
+

Service trade-api na porcie 8787.

+
+ +
+
+
+ trade-ingestor +
Deployment
+
+ 1/1 ready +
+

Ingest pipeline bez osobnego Service. Przetwarza dane w tle.

+
+ +
+
+
+ dlob-redis +
Deployment + ClusterIP
+
+ 1/1 ready +
+

Service dlob-redis na porcie 6379.

+
+ +
+
+
+ dlob-server +
Deployment + ClusterIP
+
+ 0/1 not ready +
+

Service dlob-server na porcie 6969. Zalezy od poprawnie napelnionego stanu DLOB.

+
+ +
+
+
+ dlob-publisher +
Deployment
+
+ 0/1 not ready +
+

Nie ma Service. To komponent zalezn y od zdrowego Agave RPC i bootstrapu Drift account reads.

+
+ +
+
+
+ dlob-worker / depth / slippage +
Deployments
+
+ 1/1 ready +
+

Workery pochodnych widokow orderbooka, bez ekspozycji zewnetrznej.

+
+ +
+
+
+ postgres +
StatefulSet + Headless Service
+
+ 1/1 ready +
+

Service postgres na 5432, headless dla stabilnej tozsamosci StatefulSet.

+
+ +
+
+
+ hasura +
Deployment + ClusterIP
+
+ 1/1 ready +
+

Service hasura na porcie 8080.

+
+
+
+
+
+ + + + diff --git a/mevnode-bot-topology.html b/mevnode-bot-topology.html new file mode 100644 index 0000000..0de0005 --- /dev/null +++ b/mevnode-bot-topology.html @@ -0,0 +1,424 @@ + + + + + + mevnode topology + + + +
+
+
+
Topology / mev stack
+

mevnode_sol daje chain access. mevnode_bot robi produkt, ingest i execution.

+

+ Ten układ rozdziela warstwę validatora od warstwy aplikacyjnej. Na sol + trzymasz tylko Agave, Yellowstone i lokalne RPC. Na bot działa + single-node k3s z frontendem, API, DLOB, strategiami i bazami danych. +

+
+ + +
+ +
+
+
+

mevnode_sol

+ bare metal / validator host +
+
+
+

Agave validator

+

Źródło stanu chaina, snapshotów i lokalnego read/write RPC.

+
    +
  • 127.0.0.1:8899 RPC
  • +
  • 10.91.0.1:10000 Yellowstone gRPC
  • +
  • 10.91.0.1:8999 Prometheus plugin metrics
  • +
+
+ +
+

Write capability

+

Bot może później używać lokalnego RPC przez proxy lub iść bezpośrednio przez TPU/Jito.

+
+ +
+

Node-only responsibilities

+
    +
  • snapshot recovery
  • +
  • accounts index
  • +
  • geyser / yellowstone plugin
  • +
  • node metrics
  • +
+
+
+
+ + + +
+
+

mevnode_bot

+ single-node k3s / app + execution host +
+
+
+

Read plane

+
    +
  • yellowstone-consumer: bierze live stream z 10.91.0.1:10000
  • +
  • trade-ingestor: transformuje i dystrybuuje dane aplikacyjne
  • +
  • dlob-publisher: dociąga stan kont przez RPC i buduje DLOB
  • +
  • dlob-server: wystawia gotowy widok dla API i UI
  • +
+
+ +
+

Execution plane

+
    +
  • strategy-engine: decyzje tradingowe
  • +
  • risk-manager: limity i walidacja
  • +
  • order-manager: lifecycle zleceń
  • +
  • tx-router: wybiera RPC, TPU albo Jito
  • +
+
+ +
+

Data plane

+
    +
  • Postgres: trwały storage, pozycje, historia, config
  • +
  • Hasura: warstwa GraphQL nad Postgres
  • +
  • Redis: cache, pub/sub, szybki stan DLOB
  • +
  • ClickHouse: opcjonalnie później pod cięższą analitykę
  • +
+
+ +
+

Product and ops

+
    +
  • frontend + api + ingress
  • +
  • Prometheus + Grafana
  • +
  • Portainer agent dla widoczności klastra w Portainerze
  • +
  • GitOps: Argo lub Flux z Gitea
  • +
+
+
+
+
+ +
+
+

Read path

+

+ Yellowstone gRPC daje live account updates, sloty i transakcje. + Agave RPC zostaje do odczytów punktowych typu + getAccountInfo, getMultipleAccounts, + getHealth i getLatestBlockhash. +

+
+ +
+

Write path

+

+ Wysyłka transakcji nie idzie przez Yellowstone. To robi + RPC, TPU albo Jito. + Dzięki temu read plane i write plane są od siebie operacyjnie odseparowane. +

+
+ +
+

Deployment rule

+

+ Wszystko aplikacyjne i webowe ląduje na mevnode_bot. + Wszystko validatorowe zostaje na mevnode_sol. + To upraszcza debug, upgrade i kontrolę ryzyka. +

+
+
+ +
+

+ Docelowy przepływ: mevnode_sol -> wg0 -> yellowstone-consumer / dlob-publisher -> redis / postgres -> api -> frontend. + Osobno działa execution path: strategy-engine -> tx-router -> RPC / TPU / Jito. +

+
+
+ + diff --git a/migration.md b/migration.md index 5d69b9b..640bba2 100644 --- a/migration.md +++ b/migration.md @@ -253,6 +253,23 @@ Nie commituj sekretów do gita. W GitOps wybierz jedną drogę: 16) Workflow CI: build+push obrazów (i ewentualny commit do `trade-deploy`). 17) Zasady promocji: staging → prod (np. tylko tag/release). +### Etap 4b: Workflow zmian (dev → staging) + snapshoty/rollback + +Rekomendacja: nie robimy “ręcznych” zmian na VPS (żeby nie tworzyć snowflake’a). Każdy deploy ma być **snapshoot’em**, do którego można wrócić: *git commit w `trade-deploy` + pin do obrazu* (`sha-` albo digest; bez `latest`). + +Standardowy flow: +1) Zmiany robisz lokalnie (nie musisz odpalać lokalnego Dockera; na start wystarczy szybki build/typecheck). +2) Push do gita (PR/merge). +3) CI buduje i pushuje obrazy, a następnie aktualizuje `trade-deploy` (tag/digest + ewentualnie `BUILD_TIMESTAMP`). +4) Argo CD (auto-sync) wdraża do `trade-staging`. +5) Testujesz na VPS (UI/API/ingestor). + +Rollback (szybki, preferowany): +- cofasz zmianę w `trade-deploy` (`git revert` / powrót do poprzedniej rewizji w Argo) → Argo wraca do poprzedniego snapshoot’a. + +Rollback (bezpieczny dla “dużych” zmian, np. ingest/schema): +- użyj wersjonowania vN (osobna tabela/funkcja/porty) + cutover ingestora; jeśli zmiana nie siądzie, robisz cut back vN → v1 (dane w starej tabeli zostają). + ## 9) Wersjonowanie v1/v2… (równoległe wdrożenia + cutover) Repo ma już pattern wersjonowania w Compose (`scripts/ops/*` + `devops/versions/vN.env`). @@ -337,54 +354,3 @@ trade-infra/ - ingest ticków (`trade-ingestor` → `trade-api`). Uwaga: lokalnie repo nie ma historii commitów, więc refaktor będzie startem „od zera” w nowych repo (initial commit per subrepo). - -### Status wdrożenia (superproject) - -- Subrepo mają już initial import i są dostępne na Gitei: - - `trade/trade-api` - - `trade/trade-ingestor` - - `trade/trade-frontend` - - `trade/trade-doc` -- Superproject `trade/trade-infra` jest utworzony i zawiera `.gitmodules` + submodules (w tym `trade/trade-deploy`). -- Argo CD nadal śledzi `trade/trade-deploy` (bez zmian w klastrze). - -Klonowanie superproject: - -```bash -git clone --recurse-submodules https://rv32i.pl/trade/trade-infra.git -``` - -## 13) Dostęp i logowanie (admin vs użytkownicy) - -Wymaganie: -- **admin** ma dostęp do wszystkich serwisów (np. Portainer, Argo CD, itp.) -- pozostali użytkownicy logują się **wyłącznie** do `trade` (`trade.rv32i.pl`) - -Decyzja: na ten moment **wdrażamy logowanie tylko dla `trade`**. Pozostałe serwisy (Portainer/Argo/pgAdmin/Hasura) zostają ze swoimi, osobnymi loginami. - -Rekomendowana metoda dla `trade`: **Traefik `basicAuth` na Ingress (Middleware)**. - -### Plan wdrożenia (staging → prod) - -1) Inwentaryzacja publicznych endpointów: - - sprawdzić które serwisy mają Ingress (hosty), - - serwisy administracyjne traktować jako „admin-only” (docelowo: ukryte za port-forward/VPN/IP allowlist). -2) `trade` (multi-user): - - przygotować `htpasswd` z wieloma wpisami (bcrypt), - - utworzyć `Secret` w namespace `trade-staging` (np. `trade-basic-auth`) z plikiem `users`, - - dodać `Middleware` `basicAuth` wskazujący na secret, - - podpiąć middleware do `Ingress/trade-frontend`. -3) Wyłączyć/dopasować auth w aplikacji `trade-frontend`: - - obecnie serwer frontendu ma wbudowany basic auth (plik `frontend.json`), - - żeby nie było „podwójnego logowania”, dodać tryb `BASIC_AUTH_MODE=off` (albo podobny) i ustawić go w `trade-deploy`. -4) Testy: - - `https://trade.rv32i.pl` → 401 bez auth, 200/304 z auth dla każdego usera, - - regresja: proxy `/api/*` w frontendzie nadal działa (token read jest dodawany po stronie serwera frontendu). -5) Utrzymanie: - - dodanie/usunięcie usera = update secreta (`htpasswd`) + rollout (bez zmian w kodzie), - - docelowo przenieść sekrety do GitOps (np. SOPS/SealedSecrets/ExternalSecrets). - -### Status (trade) - -- `trade-staging` ma wdrożone logowanie na Ingress (Traefik `basicAuth`) oraz wyłączony wbudowany basic auth w `trade-frontend` (`BASIC_AUTH_MODE=off`). -- UI pokazuje zalogowanego użytkownika (`GET /whoami`) i ma przycisk `Wyloguj` (`GET /logout`, best-effort dla Basic Auth). diff --git a/momentum-service-v1.md b/momentum-service-v1.md new file mode 100644 index 0000000..f7f33ed --- /dev/null +++ b/momentum-service-v1.md @@ -0,0 +1,156 @@ +# Momentum Service V1 + +Ten dokument definiuje pierwszy osobny mikroserwis dla bloku `Momentum` z +[bot1.tex](/home/user/dev/trade/doc/bot/bot1.tex). + +## Cel + +`momentum-service` liczy tylko skladniki momentum i zmiennosci z candles. + +Zakres v1: +- `mom_3s` +- `mom_10s` +- `mom_30s` +- `vol_30s` + +Poza zakresem v1: +- spread +- depth +- slippage +- gating +- scoring +- zapis decyzji tradingowej + +## Zasady + +- `Hasura` jest jedynym source of truth dla danych wejsciowych. +- `Fastify` jest shell-em mikroserwisu i wystawia wynik przez endpointy. +- Serwis nie utrzymuje osobnego trwalego stanu prawdy. +- W v1 wynik jest wystawiany przez HTTP i trzymany tylko jako latest runtime snapshot. + +## Dlaczego to jest osobny serwis + +`momentum-service` jest dobrym deployment boundary, bo caly jego processing zalezy tylko +od candles i nie potrzebuje danych DLOB. + +To znaczy: +- `momentum` i `volatility` sa razem w jednym serwisie +- poszczegolne horyzonty `mom_3s`, `mom_10s`, `mom_30s` nie sa osobnymi procesami +- granica serwisu jest wyznaczona przez wspolne wejscie i wspolny wynik runtime, a nie przez pojedynczy wzor + +## Wejscie + +Serwis czyta z Hasury: +- `bot_config` po `BOT_ID` albo `BOT_NAME` +- `get_drift_candles(...)` + +Minimalne dane z `bot_config`: + +```json +{ + "market_name": "SOL-PERP", + "params": { + "loop": { + "decision_interval_ms": 1000, + "candle_bucket_seconds": 1, + "candles_limit": 64 + }, + "features": { + "mom_fast_s": 3, + "mom_mid_s": 10, + "mom_slow_s": 30, + "vol_window_s": 30 + } + } +} +``` + +## Processing + +Model liczenia odpowiada definicjom z `bot1.tex`: + +```text +mom_k(t) = 10^4 * (c_t / c_{t-k} - 1) +``` + +oraz: + +```text +vol_W = stddev(r_i), gdzie r_i = 10^4 * (c_i / c_{i-1} - 1) +``` + +Serwis nie uzywa orderbooka ani tabel derived DLOB. + +## Wyjscie + +V1 wystawia wynik przez: +- `GET /healthz` +- `GET /readyz` +- `GET /status` +- `GET /result/latest` +- `POST /run` + +Przykladowy wynik `GET /result/latest`: + +```json +{ + "ok": true, + "service": "momentum-service", + "version": "momentum-service-v1", + "bot": { + "id": "uuid", + "name": "sol-observer" + }, + "market": { + "name": "SOL-PERP", + "candle_bucket_seconds": 1, + "candles_limit": 64 + }, + "features": { + "mom_3s": 3.8, + "mom_10s": 6.1, + "mom_30s": -12.5, + "vol_30s": 4.2 + }, + "snapshot": { + "latest_candle_at": "2026-03-27T22:44:10.000Z", + "candles_count": 64, + "query_latency_ms": 41, + "compute_latency_ms": 1 + } +} +``` + +## Runtime + +Petla v1: +- laduje `bot_config` +- pobiera candles +- liczy momentum i volatility +- odswieza `latest result` + +Domyslnie: +- loop `1000 ms` +- candles `1s` +- `64` swiece + +## Bledy + +Serwis zwraca: +- `503` na `/readyz`, gdy nie ma swiezego poprawnego wyniku +- `404` na nieznane endpointy +- `500` na `POST /run`, jesli tick zakonczyl sie bledem + +Blad Hasury nie zatrzymuje procesu na stale. Serwis pozostaje online, ale `ready` +przechodzi na `false`, dopoki kolejny poprawny tick nie odswiezy wyniku. + +## Relacja do innych serwisow + +`momentum-service` jest kandydatem na osobny building block dla: +- `observer-service` +- `scoring-service` +- downstream debug UI + +W kolejnych wersjach wynik moze byc: +- nadal tylko czytany przez HTTP, +- albo dodatkowo zapisywany do osobnej tabeli w Hasurze. diff --git a/observer-right-rail-v1.md b/observer-right-rail-v1.md new file mode 100644 index 0000000..e1a3c05 --- /dev/null +++ b/observer-right-rail-v1.md @@ -0,0 +1,126 @@ +# Observer Right Rail V1 + +Ten dokument opisuje kompaktowy widget `Bot State Machine` w prawej kolumnie visualizera. + +## Cel + +Prawy panel ma pokazywać operatorowi stale widoczny summary stanu bota bez przełączania na dolny tab `Bot`. + +Widget nie zastępuje pełnego panelu debugowego na dole. + +Podział odpowiedzialności: +- prawa kolumna: aktualny stan operacyjny bota +- dolny tab `Bot`: szczegóły, event feed, feature snapshot, debug + +## Źródło danych + +Widget używa dokładnie tego samego payloadu co dolny panel: + +- `GET /api/v1/bots?limit=100` +- `GET /api/v1/bots/:id/state` +- `GET /api/v1/bots/:id/events?limit=24` + +Nie wprowadza nowych endpointów. + +## Schema summary + +Widget czyta tylko podzbiór istniejącego kontraktu `bot_state.state`. + +### `bot_config` + +- `id` +- `name` +- `market_name` +- `mode` +- `kill_switch` + +### `bot_state` + +- `last_heartbeat_at` +- `last_error` +- `state` + +### `bot_state.state` + +Pola używane w prawym panelu: + +```json +{ + "status": "observe", + "mode": "observe", + "observe_only": true, + "last_snapshot": { + "query_latency_ms": 82, + "data_age_ms": 214 + }, + "last_gates": { + "fresh": true, + "spread_ok": true, + "slippage_ok": true, + "depth_ok": true, + "all": true + }, + "last_decision": { + "side": "long", + "confidence": 0.72, + "target_notional_usd": 500, + "skip_reason": null + }, + "counters": { + "loops": 1012, + "errors": 1 + } +} +``` + +## Sekcje widgetu + +### 1. State Machine + +Stała linia etapów: + +- `off` +- `observe` +- `paper` +- `trade` +- `panic` + +Aktywny etap wybieramy z: +1. `state.status` +2. `state.mode` +3. `bot.mode` + +Jeśli `kill_switch === true`, etap `panic` ma priorytet wizualny. + +### 2. Runtime Summary + +- `desired mode` +- `runtime status` +- `heartbeat age` +- `data age` +- `query latency` + +### 3. Decision Summary + +- `side` +- `confidence` +- `target_notional_usd` +- `skip_reason` jeśli istnieje + +### 4. Gates + +Kompaktowe chipy: +- `fresh` +- `spread` +- `slippage` +- `depth` + +## Zakres V1 + +V1 dodaje tylko read-only summary do prawej kolumny. + +Poza zakresem: +- mutacje `mode` +- `kill-switch` z UI +- event feed po prawej +- pełna historia decyzji po prawej diff --git a/observer-sol-v1.md b/observer-sol-v1.md new file mode 100644 index 0000000..1b4dba3 --- /dev/null +++ b/observer-sol-v1.md @@ -0,0 +1,240 @@ +# SOL-PERP Observer V1 + +Ten dokument definiuje pierwszy działający observer bota dla `SOL-PERP`. + +Celem nie jest jeszcze trading, tylko: +- stabilny loop `1s` +- powtarzalny snapshot feature'ów +- decyzja `long|flat|short` +- heartbeat i audyt w `bot_state` / `bot_events` + +## Zakres + +- rynek: `SOL-PERP` +- tryb: `observe` +- źródło danych v1: `Hasura` +- bez kluczy prywatnych +- bez składania transakcji + +## Architektura + +- `bot_config` pozostaje desired state i źródłem parametrów +- `bot-observer` czyta `bot_config`, pobiera snapshot rynku, liczy feature'y i zapisuje wynik +- `bot_state` przechowuje heartbeat, ostatni błąd i ostatni wynik loopa +- `bot_events` przechowuje append-only audyt decyzji i błędów + +## Źródła danych + +Observer czyta z Hasury: +- `get_drift_candles(...)` +- `dlob_hot_derived_latest` dla hot marketów z fallbackiem do `dlob_all_derived_latest` + +Depth bands i slippage nie są już czytane z osobnych legacy tabel. +Observer liczy je lokalnie z `bids_norm` / `asks_norm` nowego derived read-model. + +Wersja v1 zakłada małe query tylko dla jednego rynku. + +## Parametry w `bot_config.params` + +Minimalny kontrakt: + +```json +{ + "strategy": { + "type": "predictive_observer_v1" + }, + "loop": { + "decision_interval_ms": 1000, + "candle_bucket_seconds": 1, + "candles_limit": 64 + }, + "features": { + "mom_fast_s": 3, + "mom_mid_s": 10, + "mom_slow_s": 30, + "vol_window_s": 30, + "depth_band_bps": 10, + "slippage_size_usd": 500 + }, + "gates": { + "freshness_max_ms": 800, + "spread_max_bps": 8, + "slippage_max_bps": 12, + "depth_band_min_usd": 3000 + }, + "sizing": { + "target_notional_usd": 500 + }, + "decision": { + "threshold": 1.2, + "horizon_s": 60 + }, + "scoring": { + "weights": { + "mom_fast": 0.9, + "mom_mid": 0.35, + "mom_slow_reversal": 0.55, + "imbalance": 5.0, + "mark_vs_oracle": 0.08, + "spread": 0.15, + "slippage": 0.12 + } + } +} +``` + +## Feature Snapshot + +Na każdy tick observer liczy: +- `mark_price` +- `oracle_price` +- `mid_price` +- `spread_bps` +- `stats_imbalance` +- `depth_bid_usd` +- `depth_ask_usd` +- `depth_imbalance` +- `buy_slippage_bps` +- `sell_slippage_bps` +- `mark_vs_oracle_bps` +- `mom_3s` +- `mom_10s` +- `mom_30s` +- `vol_30s` +- `data_age_ms` + +## Gates + +Jeśli którykolwiek gate nie przechodzi, observer nie produkuje sygnału kierunkowego i zapisuje event `skip`. + +Wersja v1: +- `data_age_ms <= freshness_max_ms` +- `spread_bps <= spread_max_bps` +- `max(buy_slippage_bps, sell_slippage_bps) <= slippage_max_bps` +- `min(depth_bid_usd, depth_ask_usd) >= depth_band_min_usd` + +## Model decyzji + +V1 nie używa ML. To baseline scoring do zebrania danych i benchmarku. + +Long side: + +```text +long_score = + mom_fast * w_mom_fast + + mom_mid * w_mom_mid + - mom_slow * w_mom_slow_reversal + + depth_imbalance * w_imbalance + - mark_vs_oracle_bps * w_mark_vs_oracle + - spread_bps * w_spread + - buy_slippage_bps * w_slippage +``` + +Short side jest lustrzanym odbiciem long side. + +Decyzja: +- jeśli gates fail -> `flat` +- jeśli `max(abs(long_score), abs(short_score)) < threshold` -> `flat` +- w przeciwnym razie `long` albo `short` + +## Event `decision` + +```json +{ + "version": "observer-sol-v1", + "bot": { + "id": "uuid", + "name": "sol-observer", + "mode": "observe", + "market_name": "SOL-PERP", + "kill_switch": false + }, + "runtime": { + "service": "bot-observer", + "observe_only": true, + "loop_ms": 1000, + "query_latency_ms": 82, + "loop_latency_ms": 96 + }, + "snapshot": { + "data_age_ms": 214, + "stats_updated_at": "2026-03-23T20:12:31.112Z", + "depth_updated_at": "2026-03-23T20:12:31.112Z", + "buy_slippage_updated_at": "2026-03-23T20:12:31.112Z", + "sell_slippage_updated_at": "2026-03-23T20:12:31.112Z" + }, + "features": { + "mom_3s": 3.8, + "mom_10s": 6.1, + "mom_30s": -12.5, + "vol_30s": 5.9, + "spread_bps": 2.1, + "depth_imbalance": 0.18, + "buy_slippage_bps": 4.2, + "sell_slippage_bps": 4.0, + "mark_vs_oracle_bps": -1.3 + }, + "gates": { + "fresh": true, + "spread_ok": true, + "slippage_ok": true, + "depth_ok": true + }, + "decision": { + "side": "long", + "confidence": 0.72, + "long_score": 1.91, + "short_score": -2.34, + "target_notional_usd": 500, + "horizon_s": 60 + } +} +``` + +## Event `skip` + +Observer zapisuje `skip`, gdy: +- dane są stale +- brakuje wymaganych metryk +- gates nie przechodzą +- bot jest w trybie innym niż aktywny observe loop + +Payload `skip` ma ten sam nagłówek co `decision`, ale z polem: + +```json +{ + "skip_reason": "gate_failed:freshness" +} +``` + +## `bot_state.state` + +V1 zapisuje w `bot_state.state` znormalizowany stan loopa: +- `service` +- `version` +- `observe_only` +- `market_name` +- `mode` +- `status` +- `last_decision` +- `last_features` +- `last_gates` +- `last_snapshot` +- `counters` + +## Zmienne środowiskowe + +Najważniejsze: +- `BOT_ID` albo `BOT_NAME` +- `HASURA_GRAPHQL_URL` +- `HASURA_ADMIN_SECRET` +- `DLOB_SOURCE` opcjonalnie, jeśli chcesz zawęzić derived read-model do konkretnego `source` +- `CANDLES_SOURCE` opcjonalnie, jeśli chcesz zawęzić candles do konkretnego źródła ticków +- `OBSERVER_PORT` opcjonalnie, domyślnie `8791` + +## Cel wersji v1 + +V1 ma odpowiedzieć na trzy pytania: +- czy Hasura daje stabilny loop `1s` dla jednego rynku +- czy feature snapshot jest wystarczająco świeży +- czy baseline scoring daje sensowny materiał do dalszego strojenia i późniejszego ML diff --git a/observer-visualizer-v1.md b/observer-visualizer-v1.md new file mode 100644 index 0000000..a830c98 --- /dev/null +++ b/observer-visualizer-v1.md @@ -0,0 +1,192 @@ +# Observer Visualizer V1 + +Ten dokument opisuje pierwszy panel UI dla `bot-observer` w visualizerze. + +## Cel + +Pokazać operatorowi stan observera bez liczenia logiki bota po stronie frontendu. + +UI ma: +- czytać gotowy stan z control plane +- pokazywać bieżącą decyzję, gates i feature snapshot +- pokazywać heartbeat, błędy i feed eventów + +## Źródła danych + +Panel czyta przez istniejący `trade-api`: + +- `GET /api/v1/bots?limit=100` +- `GET /api/v1/bots/:id/state` +- `GET /api/v1/bots/:id/events?limit=100` + +Frontend nie odpytuje Hasury bezpośrednio dla panelu observera. + +## Zasada wyboru aktywnego bota + +Panel rozwiązuje bota w kolejności: + +1. `VITE_BOT_ID` jeśli ustawione +2. `VITE_BOT_NAME` jeśli ustawione +3. pierwszy bot, którego `market_name === aktualny symbol na wykresie` + +Jeśli żaden bot nie pasuje, panel pokazuje stan `No bot configured`. + +## Schema danych w UI + +### `bot_config` + +UI używa: +- `id` +- `name` +- `market_name` +- `market_type` +- `mode` +- `kill_switch` +- `updated_at` + +### `bot_state` + +UI używa: +- `last_heartbeat_at` +- `last_action_at` +- `last_error` +- `updated_at` +- `state` + +### `bot_state.state` + +Observer zapisuje już gotowy summary payload. UI czyta tylko: + +```json +{ + "service": "bot-observer", + "version": "observer-sol-v1", + "observe_only": true, + "market_name": "SOL-PERP", + "market_type": "perp", + "mode": "observe", + "kill_switch": false, + "status": "observe", + "strategy_type": "predictive_observer_v1", + "loop_ms": 1000, + "last_snapshot": { + "query_latency_ms": 82, + "data_age_ms": 214, + "candles_count": 64, + "stats_age_ms": 180, + "depth_age_ms": 210, + "buy_slippage_age_ms": 205, + "sell_slippage_age_ms": 208 + }, + "last_features": { + "mark_price": 182.12, + "oracle_price": 182.08, + "mid_price": 182.11, + "spread_bps": 2.1, + "stats_imbalance": 0.08, + "depth_bid_usd": 12400, + "depth_ask_usd": 9700, + "depth_imbalance": 0.12, + "buy_slippage_bps": 4.2, + "sell_slippage_bps": 4.0, + "mark_vs_oracle_bps": 0.22, + "mom_3s": 3.8, + "mom_10s": 6.1, + "mom_30s": -12.5, + "vol_30s": 5.9 + }, + "last_gates": { + "fresh": true, + "spread_ok": true, + "slippage_ok": true, + "depth_ok": true, + "has_candles": true, + "all": true + }, + "last_decision": { + "side": "long", + "confidence": 0.72, + "long_score": 1.91, + "short_score": -2.34, + "target_notional_usd": 500, + "horizon_s": 60, + "skip_reason": null + }, + "counters": { + "loops": 1012, + "decisions": 620, + "skips": 382, + "errors": 1 + } +} +``` + +## Sekcje panelu + +### 1. Status + +- `mode` +- `kill_switch` +- `status` +- `observe_only` +- `heartbeat age` +- `last_error` + +### 2. Current Decision + +- `side` +- `confidence` +- `target_notional_usd` +- `horizon_s` +- `long_score` +- `short_score` +- `skip_reason` + +### 3. Gates + +- `fresh` +- `spread_ok` +- `slippage_ok` +- `depth_ok` +- `has_candles` + +### 4. Feature Snapshot + +- `spread_bps` +- `mark_vs_oracle_bps` +- `mom_3s` +- `mom_10s` +- `mom_30s` +- `vol_30s` +- `depth_bid_usd` +- `depth_ask_usd` +- `depth_imbalance` +- `buy_slippage_bps` +- `sell_slippage_bps` +- `data_age_ms` +- `query_latency_ms` + +### 5. Event Feed + +Pokazujemy ostatnie eventy: +- `decision` +- `skip` +- `status` +- `error` + +UI nie renderuje pełnego JSON, tylko krótkie summary + timestamp. + +## Zakres V1 + +V1 to tylko wizualizacja aktualnego stanu observera. + +Poza zakresem: +- edycja `bot_config` +- ręczne sterowanie `mode` +- wykres historyczny decyzji na świecach +- live subscriptions + +V2 może dodać: +- markery decyzji na wykresie +- odtwarzanie `decision history` +- mutacje control-plane z UI diff --git a/rpc/README.md b/rpc/README.md new file mode 100644 index 0000000..ba90884 --- /dev/null +++ b/rpc/README.md @@ -0,0 +1,168 @@ +# Baremetal RPC + VPS + Vast (Drift / DLOB / boty) + +Ten folder zbiera w jednym miejscu ustalenia dot. topologii: + +- **Bare metal**: prywatny Solana RPC (opcjonalnie z Geyser/Yellowstone dla feedów realtime). +- **VPS**: “app/trading box” (k3s) z DLOB, statystykami, API i executorem transakcji. +- **Vast (GPU, min 5090)**: trening transformera + ewentualnie inference, ale **bez sekretów**. + +Dokumenty powiązane: + +- `doc/dlob-services.md` — jak działają serwisy DLOB na VPS (k3s) i przepływ danych. +- `doc/bots.md` — architektura botów (data plane / execution plane / model plane). +- `doc/rpc/bot-executor-api.md` — minimalne endpointy i auth dla control plane / executora. +- `doc/rpc/baremetal-monitoring.md` — jakie metryki zbieramy na RPC boxie i jak to wdrażamy (IaC). +- `doc/rpc/baremetal-iac.md` — jak utrzymywać `mevnode-baremetal` jako IaC z runnera na VPS. + +## Założenia (najważniejsze) + +1) **RPC box ma być “lean”**: na bare metalu uruchamiamy tylko Solana RPC + minimum infrastruktury (VPN, firewall, monitoring). + +2) **Drift/DLOB i reszta “biznesu” na VPS**: tam są publishery DLOB, Redis, warstwa persistence do Postgresa, DB, API, UI i executor. + +3) **Vast = model plane**: trening (i ewentualnie inference) robimy na GPU, ale Vast **nigdy nie dostaje kluczy** ani tokenów do środków. + +4) **Start od 2 marketów**: na początek ograniczamy DLOB do małej listy rynków, np. `PERP_MARKETS_TO_LOAD=0,75` (indeksy marketów PERP). + +## Topologia (skrót) + +``` + (public) + ┌────────────────────────────────────────────────────┐ + │ Internet / UI │ + │ trade-frontend + GraphQL (Hasura) + control-plane │ + └───────────────▲────────────────────────────────────┘ + │ + │ HTTPS (auth), brak “sign & send” + │ +┌──────────────────┴───────────────────────────────────────────────────┐ +│ VPS (k3s / trade-staging) │ +│ │ +│ Data plane: │ +│ - dlob-publisher-hot/all → dlob-redis │ +│ - hot/all writers → Postgres → Hasura (GraphQL/WS) │ +│ │ +│ Execution plane: │ +│ - bot-executor (jedyny komponent z kluczami, podpisuje transakcje) │ +│ - trade-api (tokeny, admin ops, pomocnicze endpointy) │ +│ │ +│ Monitoring: Prometheus/Grafana/Alertmanager │ +└──────────────────▲───────────────────────────────────────────────────┘ + │ WireGuard (private) + │ RPC/WS + opcjonalnie gRPC (Geyser) +┌──────────────────┴───────────────────────────────────────────────────┐ +│ Bare metal (RPC box) │ +│ - agave/solana-validator jako prywatny RPC (bez publicznego dostępu) │ +│ - (opcjonalnie) Geyser/Yellowstone gRPC plugin │ +│ - wireguard + firewall + chrony + eksportery │ +└──────────────────────────────────────────────────────────────────────┘ + + (public, outbound) +┌──────────────────────────────────────────────────────────────────────┐ +│ Vast (GPU, min 5090) │ +│ - trening transformera na zebranych danych │ +│ - ewentualnie inference endpoint │ +│ - brak sekretów (brak kluczy, brak tokenów do środków) │ +└──────────────────────────────────────────────────────────────────────┘ +``` + +## Bare metal: co instalować i dlaczego + +Minimalny zestaw (zgodnie z regułą “keep it lean”): + +- **Solana RPC node** (validator w trybie RPC/non-voting). +- **WireGuard**: RPC dostępny tylko prywatnie (VPS + admin). +- **Firewall**: zablokowane porty RPC/WS na publicznym interfejsie; allow tylko na interfejsie WG. +- **Chrony**: stabilny czas. +- **Monitoring**: + - `node_exporter` (CPU/RAM/dysk/iowait/sieć), + - `solana-exporter` lub równoważny probe health przez RPC. +- **Higiena dysk/logów**: rotacja logów, limity journald, monitoring NVMe. + +Opcjonalnie (kolejny poziom “pro”): + +- **Geyser / Yellowstone gRPC**: realtime stream kont/tx/slotów (mniejsze opóźnienia niż polling). + +### Geyser + Drift DLOB (praktycznie) + +Jeśli uruchamiasz Yellowstone gRPC na RPC boxie, to na VPS możesz przełączyć `dlob-publisher` +na feed gRPC (zamiast Solana WS), a jednocześnie dalej trzymać prywatny dostęp po WireGuard. + +- RPC HTTP (JSON-RPC): `ENDPOINT=http://:8899` +- RPC WS (pubsub): `WS_ENDPOINT=ws://:8900` +- Yellowstone gRPC: `GRPC_ENDPOINT=http://:10000` (+ `TOKEN`, jeśli włączasz auth) + +Konfiguracja DLOB (VPS) w praktyce (skrót): + +- `PERP_MARKETS_TO_LOAD=0,75` (start od 2 marketów) +- `USE_ORDER_SUBSCRIBER=true` +- `USE_GRPC=true` + `GRPC_ENDPOINT` + `TOKEN` + `GRPC_CLIENT=yellowstone` +- (opcjonalnie) `DISABLE_GPA_REFRESH=true` żeby ograniczyć polling + +Więcej: `doc/rpc/geyser-dlob.md`. + +Uwaga o 192 GB RAM: + +- Na mainnet łatwo “dobić” pamięć jeśli włączysz ciężkie indeksy kont / rozbudowane API / wielu klientów. +- Dlatego tym bardziej trzymamy RPC jako **prywatny** (mała liczba klientów: DLOB + executor + admin) i przenosimy obliczenia na VPS. + +## VPS (k3s): co działa i jak jest używane + +W `trade-staging` (k3s) mamy dwa krytyczne “plany”: + +### Data plane (market data + statsy) + +Opis szczegółowy: `doc/dlob-services.md`. + +- `dlob-publisher-hot` i `dlob-publisher-all`: budują DLOB “off your RPC” i publikują snapshoty do Redis. + - Na start ogranicz rynki hot-setu przez `PERP_MARKETS_TO_LOAD`. +- `dlob-redis`: szybki cache dla publisherów i writerów. +- `dlob-hot-redis-to-postgres-raw-writer`, `dlob-hot-postgres-to-postgres-derived-writer`, `dlob-all-redis-to-postgres-derived-writer`: + - zapisują raw `hot` i derived `hot/all` do Postgresa, + - Hasura wystawia te dane jako GraphQL + subscriptions. + +### Execution plane (transakcje na Drift) + +Opis szczegółowy: `doc/bots.md` (separation of concerns + kill switch). + +- **`bot-executor`**: + - jedyny komponent z kluczami (seed/keypair), + - wykonuje `place/modify/cancel/close` + limity ryzyka + kill switch, + - komunikuje się z DB (config/desired state + event log). + +### Jak wystawić API na zewnątrz (propozycja) + +Kluczowa zasada: + +- **Nie wystawiamy publicznie “sign & send tx”**. + +Wystawiamy tylko control plane: + +- ustawianie `bot_config` (desired state / kill switch), +- odczyt `bot_state` / `bot_events`, +- market data / statsy przez Hasurę (GraphQL) i/lub własne read-only endpointy. + +Warstwa dostępu (rekomendacja): + +- Sieć: WireGuard / allowlist IP. +- Aplikacja: tokeny + scopes. +- (opcjonalnie) mTLS/HMAC między serwisami. + +Executor czyta “desired state” z DB i sam podpisuje transakcje w klastrze. + +## IaC (Git) dla bare metalu + +Konfigurację RPC boxa (Solana RPC + Geyser + WireGuard + firewall + exporters) warto trzymać w Git +i wdrażać z VPS (np. Ansible po WireGuard). + +Starter (szkielet): `infra/baremetal-solana-rpc/`. + +## Vast (GPU): jak to wpinamy + +Model plane wg `doc/bots.md`: + +- VPS zbiera dane (features, statsy, candles, eventy) i składa dataset. +- Trening transformera na Vast (min 5090). +- Artefakty modelu wracają na VPS (lub inference zostaje na Vast), ale: + - **Vast nie dostaje kluczy**, + - VPS podejmuje decyzję o wykonaniu i podpisuje transakcje (executor). diff --git a/rpc/baremetal-iac.md b/rpc/baremetal-iac.md new file mode 100644 index 0000000..5b9f63e --- /dev/null +++ b/rpc/baremetal-iac.md @@ -0,0 +1,71 @@ +# Bare metal (mevnode) jako IaC: runner na VPS + +Cel: utrzymywać `mevnode-baremetal` (Solana RPC box) jako **IaC w Git**, wdrażane z VPS. + +Dlaczego z VPS: + +- masz stały “runner” (k3s box), który i tak żyje 24/7, +- łatwiej ograniczyć dostęp do bare metalu (WireGuard + allowlist), +- nie mieszasz kluczy i narzędzi na laptopie. + +## Założenia + +- Na VPS masz alias SSH `mevnode-baremetal` (w `~/.ssh/config`). +- Na bare metalu w `root/.ssh/authorized_keys` jest publiczny klucz z VPS. +- Repo `trade` jest dostępne na VPS (clone). + +## Katalog IaC w repo + +W tym repo jest starter Ansible: + +- `infra/baremetal-solana-rpc/` — Agave (systemd) + opcjonalny Geyser + monitoring. + +Sekrety (identity keypair, tokeny) **nie są w repo**. + +## Setup na VPS (runner) + +1) Zainstaluj Ansible (Ubuntu/Debian na VPS): + +```bash +sudo apt-get update +sudo apt-get install -y ansible +``` + +2) Clone repo (jeśli nie masz): + +```bash +git clone trade +cd trade +``` + +3) Uzupełnij inventory/vars (lokalne, nie commitujemy): + +```bash +cd infra/baremetal-solana-rpc/ansible +cp inventory/hosts.ini.example inventory/hosts.ini +cp group_vars/solana_rpc.yml.example group_vars/solana_rpc.yml +``` + +4) Dostosuj `group_vars/solana_rpc.yml` pod Wasz WireGuard IP, ścieżki danych i opcje Geyser. + +5) Wdroż: + +```bash +ansible-playbook -i inventory/hosts.ini playbook.yml +``` + +## Co to już robi (MVP) + +- instaluje pakiety bazowe + `chrony`, +- przygotowuje usera `solana` + katalogi danych/logów, +- wrzuca systemd unit `solana-rpc` (Agave), +- (opcjonalnie) wrzuca config Yellowstone i podpina plugin, +- instaluje monitoring (node_exporter + probe RPC) opisany w `doc/rpc/baremetal-monitoring.md`. + +## Co dalej (kolejne warstwy) + +- WireGuard + firewall (żeby RPC/WS/gRPC nie wisiały publicznie), +- ograniczenie `RPC/WS` bind do WG IP, +- rotacja logów / limity journald, +- backup identity keypair + runbook odtwarzania. + diff --git a/rpc/baremetal-monitoring.md b/rpc/baremetal-monitoring.md new file mode 100644 index 0000000..17de926 --- /dev/null +++ b/rpc/baremetal-monitoring.md @@ -0,0 +1,136 @@ +# Monitoring bare metalu (Solana RPC box) + +Cel: mieć **twarde metryki** dla prywatnego RPC (i opcjonalnie Geysera), żeby: + +- szybko wykrywać `slot lag`, spadki wydajności i problemy I/O, +- mieć “SLO” dla komponentów zależnych na VPS (DLOB / executor), +- nie zgadywać “czy to RAM/CPU/dysk/sieć”. + +Wszystko poniżej zakłada, że RPC box jest **prywatny** (dostęp tylko po WireGuard). + +## Co instalujemy (MVP) + +### 1) Node exporter (hostowe metryki) + +Pakiet: `prometheus-node-exporter` + +Daje standardowe metryki hosta: + +- CPU / iowait, +- RAM, +- filesystem usage, +- network, +- podstawowe statystyki procesów. + +Bindujemy go na IP WireGuard (żeby nie wystawiać publicznie). + +### 2) RPC probe → node_exporter textfile collector + +To jest mały probe uruchamiany systemd timerem, który zapisuje plik `*.prom` +do textfile collector node_exportera. + +Wystawia m.in.: + +- `solana_rpc_up` (1/0), +- `solana_rpc_latency_seconds{method="getHealth|getSlot"}`, +- `solana_rpc_slot`, +- `solana_rpc_block_height`, +- `solana_rpc_slot_lag` (opcjonalnie, jeśli podasz endpoint referencyjny). + +### 3) Yellowstone (Geyser) Prometheus endpoint (opcjonalnie) + +Jeśli uruchamiasz plugin `yellowstone-grpc`, on sam wystawia `/metrics` +(adres/port z configu pluginu). + +To jest osobny target do scrapowania w Prometheus. + +## Gdzie jest IaC (Ansible) + +IaC jest w `infra/baremetal-solana-rpc/`: + +- playbook: `infra/baremetal-solana-rpc/ansible/playbook.yml` +- przykładowe zmienne: `infra/baremetal-solana-rpc/ansible/group_vars/solana_rpc.yml.example` + +Monitoring jest domyślnie “włączony” w przykładzie. Najważniejsze zmienne: + +- `node_exporter_listen` (np. `10.10.0.2:9100`) +- `node_exporter_textfile_collector_dir` +- `solana_rpc_probe_endpoint` (np. `http://10.10.0.2:8899`) +- `solana_rpc_probe_reference_endpoint` (opcjonalnie; slot lag vs referencja) +- `yellowstone_prometheus_listen` (np. `10.10.0.2:8999`) + +## Jak wdrożyć (z VPS / runnera) + +1) Uzupełnij inventory i vars (bez commitowania sekretów): + +```bash +cd infra/baremetal-solana-rpc/ansible +cp inventory/hosts.ini.example inventory/hosts.ini +cp group_vars/solana_rpc.yml.example group_vars/solana_rpc.yml +``` + +2) Uruchom playbook: + +```bash +ansible-playbook -i inventory/hosts.ini playbook.yml +``` + +## Jak sprawdzić, że działa + +Z VPS (po WireGuard): + +- node exporter: `curl -sS http://:9100/metrics | head` +- RPC probe metryki: `curl -sS http://:9100/metrics | rg "solana_rpc_" | head` +- yellowstone: `curl -sS http://:8999/metrics | head` (jeśli włączone) + +### Slot lag + +Żeby dashboard miał metrykę `solana_rpc_slot_lag`, ustaw `solana_rpc_probe_reference_endpoint` na publiczny RPC (przykład: `https://api.mainnet-beta.solana.com`). W konfiguracji Ansible: + +```yaml +solana_rpc_probe_reference_endpoint: "https://api.mainnet-beta.solana.com" +``` + +Po restarcie timer/probe: + +- `systemctl restart solana-rpc-textfile-probe.timer` +- `systemctl start solana-rpc-textfile-probe.service` + +W Prometheus/Grafanie: + +- `solana_rpc_slot{job="mpabi-node-exporter",instance=""}` +- `solana_rpc_slot_reference{job="mpabi-node-exporter",instance=""}` +- `solana_rpc_slot_lag{job="mpabi-node-exporter",instance=""}` + +### Alerty Prometheus + +W `trade-deploy/kustomize/infra/monitoring-extras` dodane zostały alerty: + +- `AgaveRPCDown` — `solana_rpc_up == 0` przez >30s +- `AgaveRPCSlotLagHigh` — lag > 50 slotów przez >2m +- `AgaveRPCSlotLagCritical` — lag > 500 slotów przez >2m +- `AgaveIOHigh` — wysoki throughput na `nvme` (read+write > 300MiB/s przez 5m) +- `AgaveIOWaitHigh` — `iowait > 20%` przez 5m + +Po deployu ArgoCD (`monitoring-extras`) alerty są od razu aktywne. + +Jeśli chcesz od razu zaciągnąć zmianę: + +- `kubectl -n argocd get application monitoring-extras -o wide` +- `kubectl -n argocd annotate application monitoring-extras argocd.argoproj.io/refresh=hard --overwrite` +- `kubectl -n monitoring get prometheusrules.monitoring.coreos.com agave-rpc-alerts -o wide` +## Prometheus na VPS: co scrapować + +Najprostszy wariant to statyczne targety po WG: + +- `http://:9100/metrics` (host + solana_rpc_*), +- `http://:8999/metrics` (yellowstone, jeśli używasz). + +Zasada: żadnych portów RPC/metrics na publicznym NIC. + +## Minimalne alerty (polecane) + +- `solana_rpc_up == 0` przez > 30s +- `solana_rpc_slot_lag > 50` przez > 1–2 min (jeśli liczysz lag) +- `node_filesystem_avail_bytes` < 10–15% (ledger/accounts szybko zjadają dysk) +- wysokie `node_cpu_seconds_total{mode="iowait"}` (NVMe bottleneck) diff --git a/rpc/bot-executor-api.md b/rpc/bot-executor-api.md new file mode 100644 index 0000000..bd053c9 --- /dev/null +++ b/rpc/bot-executor-api.md @@ -0,0 +1,121 @@ +# Minimalne API dla bot-executora (k3s) + auth + +Ten dokument opisuje **minimalny** zestaw endpointów i zasad bezpieczeństwa dla warstwy +**execution plane** (`bot-executor`) i **control plane** (API do sterowania botami) na VPS (k3s). + +Cel: wysyłać transakcje na Drift **bez wystawiania publicznie** endpointu typu “podpisz i wyślij tx”. + +Powiązane: + +- Architektura botów: `doc/bots.md` +- DLOB / statsy na VPS: `doc/dlob-services.md` +- Topologia: `doc/rpc/README.md` + +## Rozdział odpowiedzialności + +### `bot-executor` (execution plane, private) + +- Ma klucze prywatne (tylko tutaj). +- Podpisuje i wysyła transakcje na Drift przez prywatny Solana RPC. +- Czyta “desired state” z DB (`bot_config`) i zapisuje audyt (`bot_events`) + heartbeat (`bot_state`). +- **Nie** przyjmuje z zewnątrz “raw tx do podpisu”. + +### Control plane (public/edge, auth) + +- Ustawianie `bot_config` (mode/target/limity/kill switch). +- Odczyt `bot_state` i `bot_events`. +- Autoryzacja: tokeny/ACL/WireGuard (zależnie od klienta). + +Control plane może być zrobiony na dwa sposoby: + +1) **Hasura GraphQL** (z rolami i RLS) — szybkie, ale wymaga dopięcia uprawnień. +2) **`trade-api` endpointy** (HTTP) — prostsze do “opiniowania” auth (Bearer token scopes). + +## Model danych (minimum) + +Minimalne tabele (wg `doc/bots.md`): + +- `bot_config`: desired state (mode, market, limity, kill_switch, strategia) +- `bot_state`: heartbeat + ostatni błąd/akcja +- `bot_events`: log audytowy (decision/order/panic/error) + +## Auth (rekomendacja minimalna) + +Warstwy: + +1) **Sieć**: tylko przez WireGuard / allowlist IP (najprostsze i skuteczne). +2) **Aplikacja**: tokeny + scopes (np. `Authorization: Bearer ...`). +3) (Opcjonalnie) **mTLS/HMAC** service-to-service w klastrze, jeśli chcesz twardsze granice. + +W tym repo masz już wzorzec tokenów w `api_tokens` + walidacja w `services/api/server.mjs`. + +### Scopes (propozycja) + +- `bots:read` — odczyt `bot_config/state/events` +- `bots:write` — zmiana `bot_config` (mode/params/kill switch) +- `exec:panic` — wymuszenie panic (jeśli zdecydujesz się na osobny endpoint; zwykle wystarczy `kill_switch=true`) + +## Control plane API (HTTP) — propozycja endpointów + +Te endpointy wystawiaj publicznie (za auth), ale **niech one tylko zmieniają DB**, a nie wysyłają transakcje. + +### Konfiguracja bota + +- `GET /v1/bots` (scope: `bots:read`) + - lista botów (id, name, market, mode, kill_switch, updated_at) + +- `GET /v1/bots/:id` (scope: `bots:read`) + - pełny `bot_config` + +- `POST /v1/bots` (scope: `bots:write`) + - tworzy nowy `bot_config` + +- `PATCH /v1/bots/:id` (scope: `bots:write`) + - aktualizacja (np. limity, target exposure, params) + +### Kill switch / mode (najczęstsze akcje) + +- `POST /v1/bots/:id/mode` (scope: `bots:write`) + - body: `{ "mode": "off" | "observe" | "trade" }` + +- `POST /v1/bots/:id/kill-switch` (scope: `bots:write`) + - body: `{ "enabled": true | false }` + - Executor traktuje `enabled=true` jako “panic” i wykonuje runbook (`cancel_all` + `close_position`) opisany w `doc/bots.md`. + +### Status i audyt + +- `GET /v1/bots/:id/state` (scope: `bots:read`) + - heartbeat, last_error, last_action_at + +- `GET /v1/bots/:id/events?limit=200` (scope: `bots:read`) + - ostatnie eventy (decision/order/panic/error) + +## `bot-executor` API (private) — minimalne endpointy + +To API **nie musi być wystawione** poza klaster. Wystarczy Service `ClusterIP`. + +- `GET /healthz` — liveness (bez auth) +- `GET /readyz` — readiness (bez auth; np. czy jest połączenie z RPC + DB) +- (Opcjonalnie) `POST /v1/reconcile` — ręczny reconcile (auth tylko wewnętrzny / admin) + +Zamiast `POST /v1/panic` preferuj sterowanie przez DB (`kill_switch=true`), bo to daje audyt i działa nawet, +gdy control plane jest odcięty od samego executora. + +## Deployment w k3s (propozycja) + +- `Deployment/bot-executor`: + - env: RPC endpoints (po WG), Hasura URL, market list, itp. + - secret: keypair / seed, ewentualnie API tokeny + - zasoby: limity CPU/RAM (żeby executor nie “zjadał” node’a) + +- `Service/bot-executor` (ClusterIP) + - brak Ingress (domyślnie) + +- Monitoring: + - `Prometheus` scrape `/metrics` (jeśli dodasz) + +## Co z “wysłaniem kontraktu” na Drift? + +- `bot-executor` robi to przez Drift SDK → Solana RPC (`sendTransaction`). +- Control plane **nie** wysyła tx; tylko ustawia desired state. + diff --git a/rpc/geyser-dlob.md b/rpc/geyser-dlob.md new file mode 100644 index 0000000..c937b06 --- /dev/null +++ b/rpc/geyser-dlob.md @@ -0,0 +1,79 @@ +# Geyser (Yellowstone gRPC) + Drift DLOB + +Ten dokument jest “praktycznym mostem” między: + +- **RPC boxem** (bare metal) z `agave/solana-validator` i pluginem **Geyser/Yellowstone gRPC**, +- a **VPS** (k3s), gdzie działa **Drift DLOB** (publisher + redis + server + statsy). + +Cel: realtime feed “prosto z noda” (gRPC), bez wystawiania RPC publicznie. + +## Założenia sieciowe (must-have) + +- Bare metal ↔ VPS po **WireGuard**. +- Porty RPC/WS/gRPC dostępne **tylko** po WG (albo bind do WG IP + firewall). +- Publicznie nie wystawiamy: + - Solana RPC/WS, + - Yellowstone gRPC, + - Redis. + +## Porty (typowe) + +To nie są twarde wymagania, ale typowy setup wygląda tak: + +- Solana JSON-RPC HTTP: `8899` +- Solana pubsub WS: `8900` +- Yellowstone gRPC: `10000` + +## Jak to mapuje się na env (VPS) + +W `trade-staging` trzymaj endpointy jako secrety i wstrzykuj do podów: + +- `ENDPOINT=http://:8899` +- `WS_ENDPOINT=ws://:8900` +- `GRPC_ENDPOINT=http://:10000` +- `TOKEN=...` (jeśli włączasz auth w Yellowstone) + +Uwaga: + +- W naszym kodzie `dlob-publisher` w trybie gRPC używa gRPC głównie do `OrderSubscriber`. + DriftClient (accountSubscriber) nadal polega na WS/polling (gRPC dla driftClient jest w kodzie wyłączone), + więc **WS endpoint nadal musi działać**. +- Dla `OrderSubscriber` w trybie gRPC mamy lokalny auto-reconnect. Jeśli Yellowstone zamknie stream albo zwroci `14 UNAVAILABLE: Connection dropped`, publisher nie powinien juz wychodzic z `exit 1`; zamiast tego czyści stream i probuje wznowic subskrypcje. Backoff jest sterowany przez `GRPC_RECONNECT_DELAY_MS` i domyslnie wynosi `1000 ms`. + +## DLOB: zalecana konfiguracja na start (2 markety) + +Przykład ustawień dla `dlob-publisher`: + +- `ENV=mainnet-beta` +- `PERP_MARKETS_TO_LOAD=0,75` (2 markety) +- `USE_ORDER_SUBSCRIBER=true` +- `USE_WEBSOCKET=true` (dla DriftClient / slotów) +- `USE_GRPC=true` +- `GRPC_CLIENT=yellowstone` +- `GRPC_ENDPOINT=...` +- `TOKEN=...` (jeśli wymagany) +- (opcjonalnie) `DISABLE_GPA_REFRESH=true` (żeby zmniejszyć polling) + +Jeśli po drodze okaże się, że gRPC jest niestabilne, nadal możesz wrócić do trybu WS: + +- wyłącz `USE_GRPC` i zostaw `USE_WEBSOCKET=true`. + +Ale operacyjnie najpierw warto sprawdzic logi reconnectu, bo krotkie dropy streamu powinny byc teraz obslugiwane bez restartu poda. + +## Minimalne testy “czy działa” + +Z perspektywy VPS: + +1) Sprawdź, czy gRPC port jest osiągalny po WG (np. TCP connect do `10000`). +2) W logach `dlob-publisher` powinno być widoczne, że `useGrpc: true` (albo analogiczna flaga). +3) Metryki DLOB powinny pokazywać rosnące liczniki update’ów (publisher) oraz świeże sloty. + +## Co to daje vs zwykły RPC/WS + +- Niższa latencja i stabilniejszy stream zmian kont (szczególnie, gdy klientów WS jest dużo). +- Mniej “pchania” ciężkich subskrypcji przez publiczny RPC. + +Co to nie rozwiązuje: + +- “Pamięciożerności” DLOB jako takiej (stan orderbooka i user map rośnie z rynkami i aktywnością). +- Wymaga nadal rozsądnej konfiguracji rynku (na start: mała lista marketów). diff --git a/rpc/software-placement.md b/rpc/software-placement.md new file mode 100644 index 0000000..a2d0b4e --- /dev/null +++ b/rpc/software-placement.md @@ -0,0 +1,89 @@ +# Strategia rozmieszczenia softu (bare metal / VPS / Vast) + +Celem jest: + +- **stabilny, prywatny feed danych** (Solana RPC → DLOB → statsy), +- **bezpieczne wykonanie transakcji** (klucze tylko na VPS), +- **skalowanie ML** (trening na Vast), bez kompromisu bezpieczeństwa. + +Źródła kontekstu w repo: + +- DLOB i przepływ danych: `doc/dlob-services.md` +- Boty i podział na plane’y: `doc/bots.md` + +## Zasady (priorytety) + +1) **RPC box jest “lean”** — żadnych baz danych, Grafany, workerów, botów. +2) **Klucze tylko w execution plane** — żadnych kluczy na Vast i w UI. +3) **Publicznie tylko read/control plane** — bez endpointu “podpisz i wyślij”. +4) **Ogranicz rynki** na start (np. `PERP_MARKETS_TO_LOAD=0,75`), żeby odchudzić cały pipeline. + +## Co gdzie uruchamiamy + +### Bare metal (Solana RPC box) + +Uruchamiaj: + +- `agave/solana-validator` jako prywatny RPC (non-voting). +- (Opcjonalnie) `geyser` / `yellowstone-grpc` plugin, jeśli chcesz feed “faster-than-RPC”. +- WireGuard + firewall + chrony. +- Monitoring: `node_exporter` + probe zdrowia RPC. + +Nie uruchamiaj: + +- DLOB, Redis, Hasura/Postgres, Grafana, botów. + +Powód: RAM 192 GB jest cenny dla noda; trzymamy minimalną liczbę procesów i klientów. + +### VPS (k3s / “app + trading box”) + +Uruchamiaj: + +- **Data plane**: + - `dlob-publisher-hot` i `dlob-publisher-all` (czytają z prywatnego RPC/WS lub gRPC), + - `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 (GraphQL/WS dla “latest” tabel i sterowania botami). +- **Execution plane**: + - `bot-executor` (klucze, podpisywanie transakcji, risk, kill switch). +- **Control plane / UI**: + - `trade-frontend` (proxy/auth + UI), + - `trade-api` (narzędzia: tokeny, admin, pomocnicze endpointy). +- Monitoring: + - Prometheus/Grafana/Alertmanager. + +Wystawianie na zewnątrz (rekomendacja): + +- Publicznie: UI + GraphQL (Hasura) + ewentualnie read-only API. +- Control plane: endpointy do ustawiania `bot_config` i odczytu `bot_state/bot_events` (z auth). +- **Nie publicznie**: `bot-executor`, prywatny RPC, Redis. + +### Vast (GPU / “model plane”) + +Uruchamiaj: + +- Trening transformera na danych wyeksportowanych z VPS (batch). +- Ewentualnie inference endpoint (jeśli chcesz oddzielić obciążenie od VPS). + +Nie uruchamiaj / nie przechowuj: + +- kluczy prywatnych, +- tokenów do środków, +- dostępu do prywatnego RPC po VPN (nie jest potrzebny do treningu). + +## Kontrakt VPS ↔ Vast (bezpieczny) + +- VPS wysyła: “feature snapshots” / dataset (bez sekretów). +- Vast zwraca: “decision” (np. target exposure/side/confidence). +- VPS decyduje o wykonaniu (risk engine) i podpisuje transakcję (executor). + +## Minimalny start (2 markety) + +1) DLOB: + - `PERP_MARKETS_TO_LOAD=0,75` (i analogicznie dla TOB monitoring, jeśli używasz). +2) Execution: + - `bot-executor` czyta config z DB, nie z requestów “na żywo”. +3) Public API: + - brak “sign & send”; tylko control-plane + read-only dane. diff --git a/rpc/topol.html b/rpc/topol.html new file mode 100644 index 0000000..a0ce715 --- /dev/null +++ b/rpc/topol.html @@ -0,0 +1,232 @@ + + + + + + Professional Drift Trading Stack (Own Solana RPC + Own DLOB + Vast ML) + + + + +
+

Professional Drift Trading Stack

+

+ 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 + + Vast (GPU) for ML +

+
+ +
+

Overview

+

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

+
+ +
+

On the Solana RPC server (dedicated) — keep it lean

+ +

Must-have

+
    +
  • + Solana validator/RPC node
    + The base RPC your whole stack reads from / sends transactions through. +
  • +
  • + WireGuard
    + So RPC is reachable only privately (your second VPS + your admin). +
  • +
  • + Firewall (nftables/ufw)
    + Block RPC ports on public NIC; allow them only on WireGuard. +
  • +
  • + Time sync (chrony)
    + For stable networking, logs, and trading timestamps. +
  • +
  • + Monitoring exporters +
      +
    • node_exporter (CPU/RAM/disk/iowait/network)
    • +
    • solana-exporter (RPC/validator health via RPC)
    • +
    +
  • +
  • + Log + disk hygiene +
      +
    • logrotate/journald limits
    • +
    • NVMe health (smartmontools/nvme-cli)
    • +
    • alerts on disk filling / iowait
    • +
    +
  • +
+ +

Optional but “pro”

+
    +
  • + Geyser streaming (Yellowstone gRPC plugin)
    + This gives ultra-low-latency streams of accounts/tx/slots compared to polling RPC. + Useful if you build your own real-time analytics pipeline. +
    + For Drift specifically, you can run without Geyser at the beginning, + but it’s the next step when you want “faster-than-RPC” feeds. +
    +
  • +
+
+ +
+

On the second VPS (your trading / app box) — where “pro trading” lives

+ +

Must-have

+
    +
  • + Drift DLOB server (self-hosted)
    + This maintains the Drift decentralized orderbook view “fresh off your RPC” and exposes + REST + WS + gRPC/polling, plus health/metrics. +
  • +
  • + (Optional but common) Drift Gateway
    + A self-hosted API gateway to interact with Drift; handy for standardized API endpoints + around trading / market info. +
  • +
  • + Cache (Redis)
    + Cache top-of-book, funding, oracle snapshots, risk checks; protects your DLOB + RPC + from bursty bot load. +
  • +
  • + Metrics + dashboards
    + Prometheus + Grafana + Alertmanager +
    + Keep Grafana off the validator box; common ops guidance is to separate monitoring UI for safety. +
    +
  • +
  • + Your trading services +
      +
    • strategy engine(s)
    • +
    • execution service (transaction builder/sender)
    • +
    • risk service (position limits, kill-switch, circuit breakers)
    • +
    +
  • +
+ +

Optional, depending on how “institutional” you want

+
    +
  • + Database (Postgres/Timescale)
    + Persist fills, order events, PnL series, backtesting datasets. +
  • +
  • + Message bus (NATS/Kafka/Redis Streams)
    + Decouple ingestion (orderbook/events) from strategies/execution. +
  • +
+
+ +
+

On the third box (Vast GPU) — model / research plane

+ +

What lives here

+
    +
  • + Training (Transformer / ML)
    + Train on datasets exported from your VPS (orderbook stats, trades, features). +
  • +
  • + (Optional) Inference endpoint
    + Serve model decisions back to VPS. The VPS still owns execution. +
  • +
+ +
+ Security rule: Vast should never hold private keys nor get access to your funds. + Treat it as a “signal generator”; execution and signing must remain on the VPS. +
+
+ +
+

Cost model (since you asked “cost per request”)

+

+ With your own RPC, there is no per-request billing. The “cost” is: +

+
    +
  • fixed monthly servers (your €149/m + the second VPS),
  • +
  • and capacity (CPU/RAM/NVMe/bandwidth) consumed by: +
      +
    • DLOB syncing from RPC,
    • +
    • number of WS subscriptions,
    • +
    • how many markets you track.
    • +
    +
  • +
+

+ DLOB exists specifically to reduce RPC load by serving orderbook/trade views to clients + instead of every client rebuilding it from chain. +

+
+ +
+

Minimal “pro” starting set (recommended)

+
    +
  • RPC box: Solana RPC + WireGuard + firewall + node_exporter + solana-exporter
  • +
  • App VPS: DLOB server + Redis + Prometheus/Grafana + your bot services
  • +
+

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

+
+ +
+ Saved as HTML — you can paste this into a file like drift-stack.html. +
+ + diff --git a/stats.md b/stats.md new file mode 100644 index 0000000..46796ff --- /dev/null +++ b/stats.md @@ -0,0 +1,158 @@ +# DLOB stats — definicje i wizualizacja + +Ten dokument opisuje metryki liczone z orderbooka DLOB (Drift Limit Order Book) oraz propozycje, jak je prezentować w naszej wizualizacji (warstwami/panelami). + +## Źródła danych (tabele) + +### `dlob_l2_latest` (snapshot L2, “surowy”) + +Snapshot top‑N leveli orderbooka per market. + +- `bids` / `asks`: tablice poziomów `{ price, size }` (wartości zwykle w “skalowanych intach” wg `PRICE_PRECISION` i `BASE_PRECISION`). +- `best_bid_price` / `best_ask_price`, `mark_price`, `oracle_price`: w praktyce do szybkiego odczytu top‑of‑book (ale najdokładniej liczyć z `bids/asks`). +- `ts`, `slot`: czas/slot źródłowego snapshota z DLOB. +- `updated_at`: kiedy worker zapisał snapshot do DB (do oceny “świeżości”). + +Z tego źródła robimy: orderbook UI (paski/heat), mikro‑ceny, symulacje fill. + +### `dlob_stats_latest` (agregat z L2 pod UI) + +Pochodne metryki liczone na podstawie `dlob_l2_latest` (lub równoważnego L2), trzymane jako “latest” per market. + +Metryki: + +- `best_bid_price`, `best_ask_price` (USD): najlepszy bid/ask. +- `mid_price` (USD): `(best_bid_price + best_ask_price) / 2`. +- `spread_abs` (USD): `best_ask_price - best_bid_price`. +- `spread_bps` (bps): `(spread_abs / mid_price) * 10_000` (1 bps = 0.01%). +- `depth_levels` (liczba): ile leveli z każdej strony weszło do “depth”. +- `depth_bid_base`, `depth_ask_base` (base asset): suma size (w jednostkach bazowych) po top‑N levelach. +- `depth_bid_usd`, `depth_ask_usd` (USD): suma `size_base * price` po top‑N levelach. +- `imbalance` ([-1..1]): `(depth_bid_usd - depth_ask_usd) / (depth_bid_usd + depth_ask_usd)`; >0 = relatywnie większa płynność po bid. +- `mark_price`, `oracle_price` (USD): ceny referencyjne (mark i oracle). +- `ts`, `slot`, `updated_at`: metadane czasu/świeżości. + +To jest najszybsze źródło do overlay na wykresie i do KPI w headerze. + +### `dlob_depth_bps_latest` (płynność w pasmach wokół mid) + +Metryki głębokości, ale nie “top‑N leveli” tylko “okno odległości od ceny” w bps. + +Klucz: + +- `(market_name, band_bps)` np. 5/10/20/50/100/200 bps. + +Interpretacja: + +- Dla danego `band_bps` sumujemy płynność tylko z poziomów, które mieszczą się w oknie ±`band_bps` wokół `mid_price`. + +Metryki: + +- `bid_base`, `ask_base` (base asset): suma size w oknie. +- `bid_usd`, `ask_usd` (USD): suma `size_base * price` w oknie. +- `imbalance` ([-1..1]): jak wyżej, ale per band. +- `mid_price`, `best_bid_price`, `best_ask_price` (USD): do kontekstu wyliczeń. +- `ts`, `slot`, `updated_at`, `raw`: metadane/diagnostyka. + +To jest najlepsze źródło do wykresów “jak gruby jest orderbook blisko ceny”. + +### `dlob_slippage_latest` (symulacja slippage vs rozmiar) + +Symulacja wykonania zlecenia rynkowego po L2. + +Klucz: + +- `(market_name, side, size_usd)` gdzie `side ∈ {buy,sell}` a `size_usd` to predefiniowane progi (np. 100/500/1000/…). + +Metryki: + +- `impact_bps` (bps): wpływ wykonania vs `mid_price` (zwykle `vwap` względem mid). +- `vwap_price` (USD): średnia cena wykonania. +- `worst_price` (USD): najgorszy poziom dotknięty podczas fill. +- `filled_usd`, `filled_base`: ile realnie weszło w fill (gdy brak płynności, może być < docelowego). +- `fill_pct` (%): 100% = pełny fill. +- `levels_consumed`: ile leveli zostało “zjedzone”. +- `mid_price`, `ts`, `slot`, `updated_at`, `raw`: metadane/diagnostyka. + +To jest idealne do “Dynamic Slippage” w formularzu i do wykresu slippage‑vs‑size. + +## Jak nanieść na wizualizację (warstwy/panele) + +Poniżej propozycja warstw, pogrupowanych tak, żeby serie w jednej warstwie były w tej samej “domenie” (jednostki i semantyka). + +### Warstwa 1: Cena / Quotes (oś Y = USD, overlay na głównym wykresie) + +Źródło: `dlob_stats_latest`. + +- Linie: `mark_price` i `oracle_price` (referencje). +- Linie: `best_bid_price` i `best_ask_price` (top‑of‑book). +- Opcjonalnie: `mid_price` jako linia przerywana. +- Opcjonalnie: “spread band” (wypełnienie między bid i ask). + +Efekt: w jednym miejscu widać gdzie jest rynek + jak szeroki jest spread. + +### Warstwa 2: Spread (panel pod wykresem, oś Y = bps) + +Źródło: `dlob_stats_latest`. + +- Linia: `spread_bps`. +- Tooltip/secondary: `spread_abs` (USD) jako dodatkowa informacja (zwykle bez drugiej osi, żeby nie mieszać skali). + +Efekt: szybkie “koszt wejścia/wyjścia” i jego zmienność. + +### Warstwa 3: Płynność top‑N + imbalance (panel, oś Y = USD + opcjonalnie linia imbalance) + +Źródło: `dlob_stats_latest`. + +- Area: `depth_bid_usd` i `depth_ask_usd` (dwie serie, zielona/czerwona). +- Opcjonalnie: linia `imbalance` (druga oś, zakres [-1..1]) albo jako wskaźnik liczbowy. + +Efekt: ile jest płynności “najbliżej” top‑of‑book (w definicji top‑N leveli). + +### Warstwa 4: Płynność jako funkcja odległości (bps bands) (panel) + +Źródło: `dlob_depth_bps_latest`. + +Dwa czytelne warianty prezentacji: + +1) Fan chart / multi‑line: + - Linie `bid_usd(band_bps)` i `ask_usd(band_bps)` dla kilku bandów (np. 10/50/200). + +2) Stacked: + - Słupki/area pokazujące “ile dodaje kolejne pasmo” (np. 0–10, 10–20, 20–50 bps), osobno dla bid i ask. + +Efekt: “jak szybko rośnie płynność, gdy odchodzę od mid”. + +### Warstwa 5: Slippage vs size (osobny panel XY, nie timeline) + +Źródło: `dlob_slippage_latest`. + +- Oś X: `size_usd`. +- Oś Y: `impact_bps`. +- Dwie krzywe: `buy` i `sell`. +- Marker: aktualny “Order Value” z formularza (punkt na krzywej). + +Efekt: bardzo czytelna krzywa kosztu wykonania względem rozmiaru. + +### Warstwa 6: Heat / “paski” orderbooka (widok orderbook albo overlay z ograniczeniem) + +Źródło: `dlob_l2_latest`. + +- Paski (zielone/czerwone) per poziom ceny, intensywność ∝ `size` (jak na Drift UI). +- To najlepiej działa jako: + - osobny panel “Orderbook” (snapshot), albo + - “edge overlay” przy prawej krawędzi wykresu (bez historii). + +Efekt: “gdzie stoją ściany” i jak się zmieniają. + +## Uwaga o historii (“latest” vs wykres w czasie) + +Tabele `*_latest` są świetne do live UI i subscriptions, ale **nie przechowują historii** do rysowania timeline (np. spread przez ostatnie 24h). + +Jeśli chcemy historię: + +- opcja A: dodać osobne tabele time‑series (np. `dlob_stats_ts`, `dlob_depth_bps_ts`, `dlob_slippage_ts`) i zasilać je workerem, +- opcja B: rozszerzyć ingest ticków (`drift_ticks`) o dodatkowe pola/nową tabelę eventów dla metryk orderbooka. + +Wtedy warstwy 2–5 mogą być prawdziwymi wykresami “w czasie”, a nie tylko bieżącym odczytem. + diff --git a/steps.md b/steps.md index 0599b3c..5f7f011 100644 --- a/steps.md +++ b/steps.md @@ -215,76 +215,17 @@ Uwaga: **nie zapisuję sekretów** (hasła, tokeny, prywatne klucze) – jeśli - Plan refaktoru opisany w `doc/migration.md` (sekcja „Metoda superproject”). - Użytkownik zaakceptował `trade/trade-infra` jako superproject; do decyzji pozostało: `submodules` vs `subtree` (rekomendacja: submodules, jeśli chcemy zachować niezależne repo + pinowanie wersji). -### Superproject: implementacja (git submodules) -- Przygotowano i wypchnięto initial commity do subrepo: - - `trade/trade-api` - - `trade/trade-ingestor` - - `trade/trade-frontend` - - `trade/trade-doc` (to repo) -- Utworzono `trade/trade-infra` jako superproject i dodano submodules: - - `api/` → `trade/trade-api` - - `frontend/` → `trade/trade-frontend` - - `ingestor/` → `trade/trade-ingestor` - - `deploy/` → `trade/trade-deploy` - - `doc/` → `trade/trade-doc` -- Do push użyto tokena repo-scope wygenerowanego w podzie Gitei; token zapisany lokalnie w `gitea/token` (gitignored), wartość nie jest logowana. +## 2026-01-10 -### Dostęp: admin vs użytkownicy (plan) -- Wymaganie: admin ma dostęp do wszystkich serwisów; pozostali użytkownicy logują się tylko do `trade.rv32i.pl`. -- Decyzja: logowanie wdrażamy tylko dla `trade`; pozostałe serwisy zostają z własnymi loginami. -- Wybrano metodę dla `trade`: Traefik `basicAuth` na Ingress (Middleware). -- Plan wdrożenia opisany w `migration.md` (sekcja „Dostęp i logowanie”). - -### Logowanie do `trade` (Traefik basicAuth) – wdrożenie -- Utworzono `Secret/trade-basic-auth` w `trade-staging` (format `htpasswd` w kluczu `users`) z userami: `admin`, `mpabi`, `mkost33` (bez logowania haseł). -- Dodano `Middleware/trade-basic-auth` (Traefik CRD) i podpięto do `Ingress/trade-frontend` adnotacją `traefik.ingress.kubernetes.io/router.middlewares`. -- Zaktualizowano `trade-frontend`: - - dodano `BASIC_AUTH_MODE=off` (wyłącza wbudowany basic auth w aplikacji), - - wdrożono nowy obraz `rv32i.pl/trade/trade-frontend:sha-8217bae`. -- Weryfikacja: `https://trade.rv32i.pl` zwraca `401` z basic auth (Traefik), a pod `trade-frontend` loguje `basicAuthMode: "off"`. - -### UI: status użytkownika + wylogowanie -- Dodano endpoint `GET /whoami` w serwerze `trade-frontend`, który zwraca username z nagłówka ustawianego przez Traefik (`headerField` w middleware). -- Dodano w UI w prawym górnym rogu status „kto jest zalogowany” oraz przycisk `Wyloguj`. -- `Wyloguj` przekierowuje do `GET /logout` (best-effort dla Basic Auth; w praktyce wymusza ponowny prompt i pozwala przełączyć użytkownika). -- Zaktualizowano manifesty: - - `Middleware/trade-basic-auth` ma `headerField: X-Trade-User`, - - `trade-frontend` wdrożony jako `rv32i.pl/trade/trade-frontend:sha-5f8c2ef`. - -### UI: poprawa `Wyloguj` + rollout -- Naprawiono `/logout` w `trade-frontend`: po wymuszeniu ponownej autoryzacji następuje redirect do `/` (żeby nie utknąć na 401/prompt loop). -- Zbudowano i wypchnięto obraz `rv32i.pl/trade/trade-frontend:sha-1b0820f`. -- `trade/trade-deploy`: bump obrazu frontendu (commit `add373d`) i push do Gitea. -- `trade/trade-infra`: bump submodules `deploy` + `frontend` (commit `dc03bd1`) i push do Gitea. -- Weryfikacja na VPS (k3s): - - `Deployment/trade-frontend` używa `rv32i.pl/trade/trade-frontend:sha-1b0820f`. - - `Ingress/trade-frontend` ma middleware `trade-staging-trade-basic-auth@kubernetescrd`. - - `Middleware/trade-basic-auth` ma `headerField: X-Trade-User`. - - `https://trade.rv32i.pl/whoami` zwraca `401` bez auth; z nagłówkiem `X-Trade-User` zwraca JSON z userem. - -### UI: przywrócenie statusu portfela + poprawa układu topbara -- Przywrócono domyślne elementy `TopNav` (w tym „Main Account”) jako „status portfela”; `Wyloguj` jest skrajnie po prawej. -- Zmieniono implementację `TopNav`: dodano prop `rightEndSlot`, żeby dokładać elementy po prawej bez zastępowania domyślnego UI. -- Zmieniono `AuthStatus`: kompaktowy „badge” z userem + przycisk `Wyloguj`. -- Zbudowano i wypchnięto obraz `rv32i.pl/trade/trade-frontend:sha-77122e0`. -- `trade/trade-deploy`: bump obrazu frontendu (commit `0851e52`) i push do Gitea. -- `trade/trade-infra`: bump submodules `deploy` + `frontend` (commit `2e570b2`) i push do Gitea. -- Weryfikacja na VPS (k3s): `Deployment/trade-frontend` używa `rv32i.pl/trade/trade-frontend:sha-77122e0`, pod `Running`. -- Uwaga: MCP SSH timeoutował; weryfikację wykonano po zwykłym `ssh` z naszym kluczem (bez logowania sekretów). - -### Auth: formularz logowania zamiast popup (session cookie) -- Cel: zastąpić przeglądarkowy popup HTTP BasicAuth normalną formatką logowania w aplikacji. -- `trade-frontend` (commit `e20a1f5`, obraz `rv32i.pl/trade/trade-frontend:sha-e20a1f5`): - - dodano sesję `HttpOnly` cookie + endpointy `POST /auth/login` i `POST /auth/logout` (`GET /logout` = redirect), - - `POST /auth/login` weryfikuje usera po `htpasswd` (binarka `htpasswd` w obrazie; plik z K8s secret), - - `/api/*` jest blokowane bez zalogowania (401 JSON, bez `WWW-Authenticate`, więc bez popupu), - - UI pokazuje ekran logowania, dopóki `GET /whoami` nie zwróci usera. -- GitOps (`trade/trade-deploy`): - - bump obrazu do `sha-e20a1f5` (commit `f949a72`), - - usunięto middleware Traefik `basicAuth` z `Ingress/trade-frontend` i wdrożono auth na poziomie aplikacji (commit `e7d4d40`), - - `Deployment/trade-frontend` montuje sekrety: - - `trade-staging/Secret/trade-basic-auth` → `/auth/users` (htpasswd), - - `trade-staging/Secret/trade-session-secret` → `/auth/session-secret` (HMAC do podpisu sesji; wartość nie logowana). -- VPS (k3s): - - utworzono `trade-staging/Secret/trade-session-secret` bez commitowania do gita (wartość wygenerowana losowo, nie logowana), - - weryfikacja: `https://trade.rv32i.pl/` = `200` bez popupu; `https://trade.rv32i.pl/api/...` = `401` bez zalogowania; `POST /auth/login` działa (401 dla złych danych). +### V2: GraphQL + WS (Hasura) + DLOB stats (staging) +- `trade/trade-deploy`: + - Podbito obraz frontendu do `gitea.mpabi.pl/trade/trade-frontend:sha-f85e6da` (UI proxy’uje Hasurę pod `/graphql` + WS pod `/graphql-ws`). + - W Hasurze włączono `HASURA_GRAPHQL_UNAUTHORIZED_ROLE=public` (UI bez tokena; bootstrap nadaje ograniczone `select`). + - W schemacie Postgresa dodano tabele pod statystyki DLOB: `public.dlob_l2_latest` i `public.dlob_stats_latest` (w `kustomize/base/initdb/001_init.sql`). + - Dodano job migracji DB dla istniejących wolumenów: `kustomize/base/postgres/job-migrate.yaml` (Argo hook; uruchamia `psql -f 001_init.sql`). + - `kustomize/base/hasura/job-bootstrap.yaml` działa jako Argo hook (re-run na sync) i trackuje tabele/permissions DLOB. + - Dodano `dlob-worker` w k3s: `kustomize/base/dlob-worker/*` (Deployment + script jako ConfigMap); worker polluje `https://dlob.drift.trade/l2` i upsertuje do Hasury dla `PUMP/SOL/BONK/BTC/ETH` perps. +- `apps/visualizer` (frontend na laptopie, dev mode): + - `apps/visualizer/__start` odpala Vite z proxy do `https://trade.mpabi.pl` + ustawia `VITE_HASURA_WS_URL=/graphql-ws`. + - `apps/visualizer/src/lib/graphqlWs.ts` wspiera względny `VITE_HASURA_WS_URL` (np. `/graphql-ws`) i normalizuje go do pełnego `ws(s)://...`. + - `apps/visualizer/src/lib/hasura.ts` domyślnie używa `/graphql` (zgodnie z flow: dev UI → staging przez proxy). diff --git a/todo.md b/todo.md new file mode 100644 index 0000000..4fc31ab --- /dev/null +++ b/todo.md @@ -0,0 +1,37 @@ +# Trade App PG Migration TODO + +Cel: trzymać `trade app` wyłącznie na danych z naszego Postgresa (`raw` / `derived`), bez zależności od legacy +agregatów `dlob_l2_latest`, `dlob_stats_latest`, `dlob_depth_bps_latest`, `dlob_slippage_latest`. + +## Stan docelowy + +- wykresy zostają na `trade-api /v1/chart` +- orderbook i KPI DLOB czytają `dlob_hot_snapshot_latest`, `dlob_hot_derived_latest` albo `dlob_all_derived_latest` +- depth bands i slippage są liczone w frontendzie z tego samego L2 z Postgresa +- po drugiej fazie można wyciąć z app-path `dlob-server`, `dlob-worker`, `dlob-depth-worker`, `dlob-slippage-worker` +- ścieżka `hot` zostaje dla execution/debug, nie dla głównego widoku `trade app` + +## Plan + +- [x] Faza 1: przepiąć `useDlobStats` i `useDlobL2` na `raw` / `derived` z filtrem `is_indicative=false` +- [x] Faza 2: przepiąć `useDlobDepthBands` i `useDlobSlippage` off legacy tabel i liczyć je bezpośrednio z orderbooka z Postgresa +- [ ] Faza 3: jeśli będzie potrzebne, przenieść te same obliczenia do backendu / ts writerów jako cache lub time series +- [ ] Faza 4: usunąć z app-path zależność od `dlob-server` i workerów legacy po potwierdzeniu zgodności UI + +## Mapowanie fazy 1 + +- `best_bid_price` -> `bestBid` +- `best_ask_price` -> `bestAsk` +- `mid_price` -> `mid` +- `spread_quote` -> `spreadAbs` +- `spread_bps` -> `spreadBps` +- `depth_bid_quote` -> `depthBidUsd` +- `depth_ask_quote` -> `depthAskUsd` +- `bids_norm` / `asks_norm` albo `bids` / `asks` -> orderbook L2 + +## Uwaga + +- UI powinno czytać tylko `is_indicative=false` +- `hot` ma preferować raw (`dlob_hot_snapshot_latest`) tam, gdzie potrzebna jest pełna drabinka +- `all` ma czytać `dlob_all_derived_latest` +- frontend nie powinien zależeć od legacy tabel agregatowych diff --git a/trade-system-flow.html b/trade-system-flow.html new file mode 100644 index 0000000..f82f5b2 --- /dev/null +++ b/trade-system-flow.html @@ -0,0 +1,692 @@ + + + + + + Trade system flow + + + +
+
+
Trade architecture / runtime map
+

Skąd dane trafiają, w jakiej postaci i które endpointy naprawdę wystawiamy

+

+ Ta strona opisuje faktyczny przepływ runtime dla waszej appki: browser -> frontend -> API/Hasura -> Postgres/Redis + oraz zależności od Agave RPC i Yellowstone gRPC. To nie jest ogólny diagram architektury, + tylko mapa wejść, wyjść, formatów danych i tras HTTP/WS używanych przez produkt. +

+
+ +
+ + + +
+ +
+
+
+

Public web surface

+ To widzi użytkownik i to jest jedyny realny entrypoint produktu na zewnątrz. +
+
+
NodePort / browser entry
+

trade-frontend

+

Serwuje SPA, obsługuje logowanie, robi proxy do API i Hasury.

+
+ NodePort 30081 + service 8081 + /api/* + /graphql +
+
+
+
Browser calls
+

Frontend SPA

+

Wykres bierze REST JSON. DLOB live bierze GraphQL WebSocket. Auth idzie przez sesję cookie.

+
+ GET /whoami + POST /auth/login + GET /api/v1/chart + WS /graphql +
+
+
+
+ +
+

App services in k3s

+ Wewnętrzne usługi aplikacyjne. Tylko frontend jest dziś wystawiony na zewnątrz jako NodePort. +
+
+
ClusterIP / app logic
+

trade-api

+

REST backend. Zwraca candles, przyjmuje tick ingest, zarządza tokenami.

+
+ ClusterIP 8787 + /v1/chart + /v1/ticks + /v1/ingest/tick +
+
+
+
ClusterIP / query plane
+

Hasura + Postgres

+

Frontend subskrybuje Hasurę po GraphQL WS. API czyta i zapisuje przez GraphQL do Hasury.

+
+ Hasura 8080 + Postgres 5432 + GraphQL + subscriptions +
+
+
+
Redis-backed read side
+

DLOB stack

+

publisher 0/1 server 0/1 bo bootstrapping zależy od zdrowego Agave RPC.

+
+ Redis 6379 + dlob-server 6969 + workers +
+
+
+
+ +
+

Chain and execution plane

+ To nie jest wystawiane użytkownikowi końcowemu. To feed i write side dla botów. +
+
+
mevnode_sol / source of truth
+

Agave RPC

+

Punktowe odczyty: getAccountInfo, getMultipleAccounts, getSlot, getHealth.

+
+ 127.0.0.1:8899 + RPC only +
+
+
+
mevnode_sol / live feed
+

Yellowstone gRPC

+

Pushowy stream kont, slotów i transakcji po wg0.

+
+ 10.91.0.1:10000 + token auth +
+
+
+
future execution split
+

tx-router / TPU / Jito

+

To jest write path. Nie mylić z Yellowstone, który jest tylko read streamem.

+
+
+
+
+ +
+
End-to-end runtime flow
+
+
+
1. BrowserUżytkownik otwiera SPA i trzyma sesję cookie.
+
+
2. trade-frontendSerwuje UI i robi reverse proxy do /api i /graphql.
+
+
3. trade-api / HasuraREST dla chartów, GraphQL WS dla live DLOB.
+
+
+
4. Postgres / RedisTrwały stan i szybki stan pośredni.
+
+
5. DLOB workersPrzeliczają depth, slippage, orderbook views.
+
+
6. Agave / YellowstoneRPC do odczytów punktowych, gRPC do streamu live.
+
+
+
+ +
+

+ Najważniejszy podział: chart path = REST JSON przez trade-api, a DLOB live = GraphQL WS przez Hasurę. + To oznacza, że nie cały frontend jedzie jednym typem transportu. Produkt ma dwa równoległe read pathy. +

+
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
SurfaceEndpointTransportInputOutputWhere it goes
FrontendGET /whoamiHTTP JSONcookie session{ ok, user, mode }handled directly by trade-frontend
FrontendPOST /auth/loginHTTP JSON/formusername, password{ ok, user } + session cookiehandled directly by trade-frontend
FrontendPOST /auth/logoutHTTP JSONsession cookie{ ok: true }handled directly by trade-frontend
Frontend proxy/api/*HTTPbrowser request, frontend injects read tokenproxied API responsetrade-api:8787
Frontend proxy/graphqlHTTP GraphQLGraphQL query/mutationGraphQL responsehasura:8080/v1/graphql
Frontend proxyWS /graphqlGraphQL WebSocketsubscription payloadlive subscription frameshasura:8080/v1/graphql
APIGET /v1/chartHTTP JSONsymbol, tf, limit, optional sourcecandles + indicators + flow rowsHasura function get_drift_candles + table drift_ticks
APIPOST /v1/ingest/tickHTTP JSONtick payload{ ok, id }writes tick through Hasura into DB
APIGET /v1/ticksHTTP JSONsymbol, optional source, limit, from, totick listreads drift_ticks through Hasura
API adminPOST /v1/admin/tokensHTTP JSONname, scopesnew API tokenstored in DB via Hasura
API adminPOST /v1/admin/tokens/revokeHTTP JSONtoken idrevocation statusupdates token row in DB
Chain readAgave RPCJSON-RPCaccount/state requestspoint readsmevnode_sol
Chain liveYellowstone gRPCgRPC streamingsubscription config + tokenaccount/slot/tx streammevnode_sol
+
+
+ +
+
+
+
Chart response
+
{
+  "ok": true,
+  "symbol": "SOL-PERP",
+  "tf": "1m",
+  "bucketSeconds": 60,
+  "candles": [
+    {
+      "time": 1710000000,
+      "open": 132.1,
+      "high": 133.2,
+      "low": 131.8,
+      "close": 132.9,
+      "volume": 412,
+      "oracle": 132.7,
+      "flow": { "up": 0.46, "down": 0.41, "flat": 0.13 },
+      "flowRows": [1, 1, 0, -1],
+      "flowMoves": [0.2, 0.1, 0, 0.3]
+    }
+  ],
+  "indicators": {
+    "sma20": [{ "time": 1710000000, "value": 131.4 }],
+    "ema20": [{ "time": 1710000000, "value": 131.8 }],
+    "bb20": { "upper": [], "lower": [], "mid": [] },
+    "rsi14": [],
+    "macd": { "macd": [], "signal": [] }
+  }
+}
+
+ +
+
Tick ingest body
+
{
+  "ts": "2026-03-13T11:20:00.000Z",
+  "market_index": 0,
+  "symbol": "SOL-PERP",
+  "oracle_price": "132.70",
+  "mark_price": "132.91",
+  "oracle_slot": 406125100,
+  "source": "drift",
+  "raw": {
+    "provider": "internal"
+  }
+}
+
+ +
+
GraphQL DLOB subscriptions
+
subscription DlobStats($market: String!) {
+  dlob_stats_latest(where: {market_name: {_eq: $market}}, limit: 1) {
+    market_name
+    mark_price
+    oracle_price
+    best_bid_price
+    best_ask_price
+    mid_price
+    spread_abs
+    spread_bps
+    depth_bid_usd
+    depth_ask_usd
+    imbalance
+    updated_at
+  }
+}
+
+ +
+
L2 payload shape
+
{
+  "market_name": "SOL-PERP",
+  "bids": [
+    { "price": 132910000, "size": 2500000000 }
+  ],
+  "asks": [
+    { "price": 132930000, "size": 1700000000 }
+  ],
+  "updated_at": "2026-03-13T11:20:01.000Z"
+}
+
+Frontend przelicza to przez:
+- pricePrecision = 1_000_000
+- basePrecision = 1_000_000_000
+
+
+ +
+

+ Krytyczne rozróżnienie: DLOB live nie idzie z REST API. DLOB do UI idzie przez Hasura GraphQL subscriptions, + a chart przez REST JSON z trade-api. To są dwa osobne transporty, dwa osobne modele danych i dwa osobne źródła opóźnień. +

+
+
+
+ + + + diff --git a/visualizer-candles.md b/visualizer-candles.md new file mode 100644 index 0000000..7a0260c --- /dev/null +++ b/visualizer-candles.md @@ -0,0 +1,42 @@ +# Visualizer: świeczki + “brick stack” pod świecą + +## Timeframe (tf) + +W visualizerze `tf` to długość świecy (bucket) przekazywana do API: + +- `3s`, `5s`, `15s`, `30s` — mikro‑ruchy (dużo szumu, ale świetne do obserwacji mikrostruktury) +- `1m`, `5m`, `15m`, `1h`, `4h`, `1d` — klasyczne interwały + +Kiedy ma to sens: +- `3s/5s`: gdy chcesz widzieć “jak cena się buduje” w krótkich falach (np. po newsie / w dużej zmienności). +- `15s/30s`: często najlepszy kompromis między szumem a czytelnością, jeżeli patrzysz na very-short-term. + +## Co pokazuje “brick stack” na dole + +Pod każdą świecą rysujemy słupek złożony z “bricków” (małych segmentów) odpowiadających kolejnym krokom czasu wewnątrz świecy. + +Kolory bricków: +- zielony = w tym kroku cena poszła w górę +- czerwony = w tym kroku cena poszła w dół +- niebieski = w tym kroku cena była stała (flat) + +Wysokość bricków: +- zielony/czerwony: proporcjonalna do `|Δprice|` w danym kroku +- niebieski: stała (unit height) + +Bricki są rozdzielone cienką czarną linią (1px), żeby było widać strukturę “krok po kroku”. + +## Jakie pola musi zwracać API + +Endpoint `GET /v1/chart` zwraca w każdej świecy: + +- `flow`: udziały czasu `up/down/flat` w całym buckecie (0..1) +- `flowRows`: tablica kierunków per krok czasu: `-1` (down), `0` (flat), `1` (up) +- `flowMoves`: tablica “move magnitude” per krok czasu (wartości dodatnie; 0 jeśli flat) + +To właśnie `flowRows` + `flowMoves` są używane do narysowania brick stacka. + +## Domyślny rynek + +W visualizerze domyślnie ustawiony jest `SOL-PERP`. + diff --git a/workflow.md b/workflow.md new file mode 100644 index 0000000..2e3a7bf --- /dev/null +++ b/workflow.md @@ -0,0 +1,99 @@ +# Workflow pracy w projekcie `trade` (dev → staging na VPS) + snapshot/rollback + +Ten plik jest “source of truth” dla sposobu pracy nad zmianami w `trade`. +Cel: **zero ręcznych zmian na VPS**, każdy deploy jest **snapshoot’em**, do którego można wrócić. + +## Dla agenta / po restarcie sesji + +1) Przeczytaj ten plik: `doc/workflow.md`. +2) Kontekst funkcjonalny: `README.md`, `info.md`. +3) Kontekst stacka: `doc/workflow-api-ingest.md` oraz `devops/*/README.md`. +4) Stan VPS/k3s + GitOps: `doc/migration.md` i log zmian: `doc/steps.md`. + +## Zasady (must-have) + +- **Nie edytujemy “na żywo” VPS** (żadnych ręcznych poprawek w kontenerach/plikach na serwerze) → tylko Git + CI + Argo. +- **Sekrety nie trafiają do gita**: `tokens/*.json` są gitignored; w dokumentacji/logach redagujemy hasła/tokeny. +- **Brak `latest`**: obrazy w deployu są przypięte do `sha-` albo digesta. +- **Każda zmiana = snapshot**: “wersja” to commit w repo deploy + przypięte obrazy. + +## Domyślne środowisko pracy (ważne) + +- **Frontend**: domyślnie pracujemy lokalnie (Vite) i łączymy się z backendem na VPS (staging) przez proxy. Deploy frontendu na VPS robimy tylko jeśli jest to wyraźnie powiedziane (“wdrażam na VPS”). +- **Backend (trade-api + ingestor)**: zmiany backendu weryfikujemy/wdrażamy na VPS (staging), bo tam działa ingestor i tam są dane. Nie traktujemy lokalnego uruchomienia backendu jako domyślnego (tylko na wyraźną prośbę do debugowania). + +## Standardowy flow zmian (polecany) + +1) Zmiana w kodzie lokalnie. + - Nie musisz odpalać lokalnego Dockera; na start rób szybkie walidacje (build/typecheck). +2) Commit + push (najlepiej przez PR). +3) CI: + - build → push obrazów (tag `sha-*` lub digest), + - aktualizacja `trade-deploy` (bump obrazu/rewizji). +4) Argo CD (auto-sync na staging) wdraża nowy snapshot w `trade-staging`. +5) Test na VPS: + - API: `/healthz`, `/v1/ticks`, `/v1/chart` + - UI: `trade.mpabi.pl` + - Ingest: logi `trade-ingestor` + napływ ticków do tabeli. + +## Snapshoty i rollback (playbook) + +### Rollback szybki (preferowany) + +- Cofnij snapshot w repo deploy: + - `git revert` commita, który podbił obrazy, **albo** + - w Argo cofnij do poprzedniej rewizji (ten sam efekt). + +Efekt: Argo wraca do poprzedniego “dobrego” zestawu obrazów i konfiguracji. + +### Rollback bezpieczny dla “dużych” zmian (schema/ingest) + +Jeśli zmiana dotyka danych/ingestu, rób ją jako nową wersję vN: +- nowa tabela: `drift_ticks_vN` +- nowa funkcja: `get_drift_candles_vN` +- osobny deploy API/UI (inne porty/sufiksy), a ingest przełączany “cutover”. + +W razie problemów: robisz “cut back” vN → v1 (stare dane zostają nietknięte). + +## Lokalne uruchomienie (opcjonalne, do debugowania) + +Dokładna instrukcja: `doc/workflow-api-ingest.md`. + +Skrót: +```bash +npm install +docker compose -f devops/db/docker-compose.yml up -d +docker compose -f devops/tools/bootstrap/docker-compose.yml run --rm db-init +docker compose -f devops/tools/bootstrap/docker-compose.yml run --rm hasura-bootstrap +docker compose -f devops/app/docker-compose.yml up -d --build api +npm run token:api -- --scopes write --out tokens/alg.json +npm run token:api -- --scopes read --out tokens/read.json +docker compose -f devops/app/docker-compose.yml up -d --build frontend +docker compose -f devops/app/docker-compose.yml --profile ingest up -d --build +``` + +### Frontend dev (Vite) z backendem na VPS (staging) + +Jeśli chcesz szybko iterować nad UI bez deploya, możesz odpalić lokalny Vite i podpiąć go do backendu na VPS przez istniejący proxy `/api` na `trade.mpabi.pl`. + +- Vite trzyma `VITE_API_URL=/api` (default) i proxy’uje `/api/*` do VPS. +- UI ma też GraphQL (Hasura) pod `/graphql` (HTTP + WS subscriptions) – w dev proxy’ujemy `/graphql` i `/graphql-ws` do VPS, żeby subscriptions działały na `http://localhost:5173`. +- Auth w staging jest w trybie `session` (`/auth/login`, cookie `trade_session`), więc w dev proxy’ujemy też `/whoami`, `/auth/*`, `/logout`. +- Dev proxy usuwa `Secure` z `Set-Cookie`, żeby cookie działało na `http://localhost:5173`. +- Na VPS `trade-frontend` proxy’uje dalej do `trade-api` i wstrzykuje read-token **server-side** (token nie trafia do przeglądarki). + +Przykład: + +```bash +cd apps/visualizer +bash __start +``` + +Jeśli staging ma dodatkowy basic auth (np. Traefik `basicAuth`), dodaj: +`API_PROXY_BASIC_AUTH='USER:PASS'` albo `API_PROXY_BASIC_AUTH_FILE=tokens/frontend.json` (pola `username`/`password`). + +## Definicja “done” dla zmiany + +- Jest snapshot (commit w deploy) i można wrócić jednym ruchem. +- Staging działa (API/ingest/UI) i ma podstawowe smoke-checki. +- Sekrety nie zostały dodane do repo ani do logów/komentarzy.