docs(snapshot): sync workspace documentation

This commit is contained in:
u1
2026-03-29 13:14:30 +02:00
parent 93a6e4aa2f
commit 0d3d110026
37 changed files with 8500 additions and 123 deletions

204
3700x.md Normal file
View File

@@ -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`

299
agave-sync-recipe.md Normal file
View File

@@ -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

View File

@@ -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
```

View File

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

142
bot-strategy-reversal.md Normal file
View File

@@ -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 (topN):
- `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: topN + 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 topofbook:
- 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. 2501000ms)
- `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”.

500
bot/bot1.tex Normal file
View File

@@ -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}

359
bots.md Normal file
View File

@@ -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” adhoc; 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. 1030s) → 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-<name>`),
- `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?

344
data-ingest-strategy.md Normal file
View File

@@ -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`

184
dlob-basics.md Normal file
View File

@@ -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) TopN leveli (np. L1…L10) — `dlob_*_derived_latest`
Zapisywane w writerach warstwy pochodnej na podstawie live snapshotów orderbooka:
- Bierzemy znormalizowane top levele 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 topN 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 bestprice “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 (perpoziom)** — 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”.

View File

@@ -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

243
dlob-services.md Normal file
View File

@@ -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 (topN) z każdej strony braliśmy do liczenia “depth”.
- `depth_bid_base` / `depth_ask_base`: suma `size` po topN levelach bid/ask (w base).
- `depth_bid_usd` / `depth_ask_usd`: suma `size_base * price` po topN levelach (w USD).
- `imbalance` ([-1..1]): miara asymetrii płynności:
- `(depth_bid_usd - depth_ask_usd) / (depth_bid_usd + depth_ask_usd)`
- >0 = relatywnie więcej płynności po bid, <0 = po ask.
- `oracle_price`: cena z oracla (np. Pyth) jako punkt odniesienia.
- `mark_price`: mark z rynku/perp (cena referencyjna dla rozliczeń); różni się od oracle/top-of-book.
### Metryki “depth bands” (np. `dlob_depth_bps_latest`)
- `band_bps`: szerokość pasma wokół `mid_price` (np. 5/10/20/50/100/200 bps).
- `bid_usd` / `ask_usd`: płynność po danej stronie, ale **tylko z poziomów mieszczących się w oknie ±`band_bps`** wokół mid.
- `imbalance`: jak wyżej, ale liczony per band.
### Metryki “slippage” (np. `dlob_slippage_latest`)
To jest symulacja gdybym teraz zrobił market order o rozmiarze X na podstawie L2.
- `size_usd`: docelowy rozmiar zlecenia w USD.
- `vwap_price`: średnia cena realizacji (Volume Weighted Average Price) dla symulowanego fill.
- `impact_bps`: koszt/odchylenie względem `mid_price` wyrażone w bps (zwykle na bazie `vwap` vs `mid`).
- `worst_price`: najgorsza cena dotknięta podczas zjadania kolejnych leveli.
- `filled_usd` / `filled_base`: ile realnie udało się wypełnić (może być < docelowego, jeśli brakuje płynności).
- `fill_pct`: procent wypełnienia (100% = pełny fill).
- `levels_consumed`: ile leveli zostało zjedzonych podczas fill.
### Metadane czasu (“świeżość”)
- `ts`: timestamp źródła (czas snapshotu).
- `slot`: slot Solany, z którego pochodzi snapshot (monotoniczny numer czasu chaina).
- `updated_at`: kiedy nasz pipeline zapisał/odświeżył rekord w DB (do oceny, czy dane świeże).
## Redis retention na poziomie k3s i Redisa
Dla `mevnode-bot` retencja Redis jest ustawiana na poziomie `k3s` przez cleaner `CronJob`, a nie przez natywny `TTL` kluczy.
Powod:
- publishery DLOB zapisują do Redis w modelu `latest-only`
- kolejne `SET` nadpisuje poprzedni snapshot tego samego klucza
- zwykly Redis `TTL` bylby kasowany przez kolejne zapisy, wiec nie daje poprawnej retencji semantycznej
Ustawienie operacyjne:
- `hot` (`dlob-hot:`): retencja `15m`
- `all` (`dlob-all:`): retencja `1h`
- cleaner uruchamiany z `CronJob` co `5m`
Sposob dzialania cleanera:
- skanuje klucze `dlob-hot:*` oraz `dlob-all:*`
- odczytuje `ts`, a jesli go nie ma to `updatedAtTs`, z payloadu JSON
- usuwa tylko klucze starsze niz zadany prog wieku
To oznacza:
- aktywne markety pozostaja stale obecne w Redisie
- osierocone klucze po zmianie konfiguracji znikaja automatycznie
- Redis pozostaje warstwa `raw latest snapshot`, bez normalizacji payloadu
Aktualny stan Redis, istotny dla tej decyzji:
- klucze nie maja automatycznej expiracji (`TTL=-1`)
- `maxmemory-policy=noeviction`
- `aof_enabled=0`
Aktualny stan publisherow na poziomie k3s:
- `hot` i `all` sa obecnie rozrozniane przez markety, `REDIS_CLIENT`, `DLOB_SOURCE` i persistent store
- maja osobne `priorityClassName`
- `resources.requests/limits` sa ustawione dla writerow `hot`; publishery maja rozroznienie priorytetow na poziomie klastra
## Priorytety na poziomie k3s
Na poziomie `k3s` rozroznienie `hot` i `all` jest ustawione przez osobne `PriorityClass`.
Parametry:
- `hot`:
- deployment: `dlob-publisher-hot`
- `priorityClassName: dlob-hot`
- wartosc `PriorityClass`: `100000`
- `all`:
- deployment: `dlob-publisher-all`
- `priorityClassName: dlob-all`
- wartosc `PriorityClass`: `50000`
Znaczenie operacyjne:
- `hot` ma wyzszy priorytet schedulera i preemption niz `all`
- przy presji na node `hot` powinien byc traktowany jako sciezka krytyczna
- `all` pozostaje feedem nizszego priorytetu dla pelnego coverage rynku
Zakres tej parametryzacji:
- to jest tylko priorytet na poziomie schedulera `k3s`
- nie ustawia jeszcze `resources.requests` ani `resources.limits`
- nie zmienia logiki publishera, tylko kolejnosc traktowania workloadu przez klaster

54
drift-perps-monitoring.md Normal file
View File

@@ -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`.

533
economic-markers-k3s.md Normal file
View File

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

205
frontent-local-run.md Normal file
View File

@@ -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

275
gitea-repo-cleanup-plan.md Normal file
View File

@@ -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`

143
gitea-tree.md Normal file
View File

@@ -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
```

245
mevnode-bot-architecture.md Normal file
View File

@@ -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`

File diff suppressed because it is too large Load Diff

424
mevnode-bot-topology.html Normal file
View File

@@ -0,0 +1,424 @@
<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>mevnode topology</title>
<style>
:root {
--bg: #f4efe7;
--panel: #fffaf2;
--ink: #1f1c17;
--muted: #6b6358;
--line: #d9ccb7;
--accent: #b6412f;
--accent-2: #0e6f74;
--accent-3: #7c5e10;
--shadow: 0 18px 40px rgba(31, 28, 23, 0.08);
--radius: 18px;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
background:
radial-gradient(circle at top left, rgba(182, 65, 47, 0.12), transparent 28%),
radial-gradient(circle at top right, rgba(14, 111, 116, 0.12), transparent 24%),
linear-gradient(180deg, #fbf7f0 0%, var(--bg) 100%);
color: var(--ink);
}
.page {
max-width: 1480px;
margin: 0 auto;
padding: 32px 20px 48px;
}
.hero {
display: grid;
grid-template-columns: 1.2fr 0.8fr;
gap: 24px;
margin-bottom: 24px;
}
.hero-card,
.legend,
.node,
.strip,
.note {
background: rgba(255, 250, 242, 0.88);
border: 1px solid rgba(217, 204, 183, 0.9);
border-radius: var(--radius);
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
}
.hero-card {
padding: 28px;
}
.eyebrow {
display: inline-block;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid rgba(182, 65, 47, 0.22);
color: var(--accent);
font-size: 12px;
font-weight: 700;
letter-spacing: 0.08em;
text-transform: uppercase;
}
h1 {
margin: 16px 0 10px;
font-family: "IBM Plex Serif", Georgia, serif;
font-size: clamp(2rem, 4vw, 3.8rem);
line-height: 0.98;
}
.hero-card p,
.legend p,
.note p,
.box p,
.strip p {
margin: 0;
color: var(--muted);
line-height: 1.55;
}
.legend {
padding: 24px;
display: grid;
gap: 14px;
align-content: start;
}
.legend-item {
display: flex;
align-items: center;
gap: 12px;
color: var(--ink);
font-weight: 600;
}
.swatch {
width: 14px;
height: 14px;
border-radius: 999px;
flex: 0 0 14px;
}
.swatch.read { background: var(--accent-2); }
.swatch.write { background: var(--accent); }
.swatch.ops { background: var(--accent-3); }
.swatch.store { background: #4a5565; }
.diagram {
display: grid;
grid-template-columns: minmax(280px, 0.9fr) 120px minmax(320px, 1.25fr);
gap: 18px;
align-items: stretch;
}
.node {
padding: 20px;
}
.node h2 {
margin: 0 0 12px;
font-size: 1.35rem;
font-family: "IBM Plex Serif", Georgia, serif;
}
.node-head {
display: flex;
justify-content: space-between;
align-items: baseline;
gap: 12px;
margin-bottom: 14px;
}
.node-head span {
color: var(--muted);
font-size: 13px;
font-weight: 600;
}
.stack {
display: grid;
gap: 10px;
}
.box {
border: 1px solid var(--line);
border-radius: 14px;
padding: 14px;
background: #fffdf8;
}
.box h3 {
margin: 0 0 6px;
font-size: 1rem;
}
.box ul {
margin: 10px 0 0;
padding-left: 18px;
color: var(--ink);
}
.box li {
margin: 6px 0;
line-height: 1.45;
}
.box.read { border-left: 6px solid var(--accent-2); }
.box.write { border-left: 6px solid var(--accent); }
.box.ops { border-left: 6px solid var(--accent-3); }
.box.store { border-left: 6px solid #4a5565; }
.bridge {
display: grid;
grid-template-rows: 1fr auto 1fr auto 1fr;
align-items: center;
justify-items: center;
color: var(--muted);
font-weight: 700;
}
.bridge-line {
width: 2px;
height: 100%;
background:
linear-gradient(180deg,
rgba(14, 111, 116, 0.95) 0%,
rgba(14, 111, 116, 0.95) 38%,
rgba(182, 65, 47, 0.95) 38%,
rgba(182, 65, 47, 0.95) 72%,
rgba(124, 94, 16, 0.95) 72%,
rgba(124, 94, 16, 0.95) 100%);
border-radius: 999px;
min-height: 88px;
}
.bridge-tag {
writing-mode: vertical-rl;
transform: rotate(180deg);
padding: 10px 0;
font-size: 12px;
letter-spacing: 0.08em;
text-transform: uppercase;
}
.strips {
margin-top: 24px;
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 18px;
}
.strip {
padding: 18px;
}
.strip h3 {
margin: 0 0 10px;
font-size: 1rem;
}
.strip code,
.note code {
font-family: "IBM Plex Mono", "Fira Code", monospace;
font-size: 0.95em;
background: rgba(31, 28, 23, 0.06);
padding: 2px 6px;
border-radius: 6px;
}
.note {
margin-top: 24px;
padding: 18px 20px;
}
@media (max-width: 1120px) {
.hero,
.diagram,
.strips {
grid-template-columns: 1fr;
}
.bridge {
grid-template-columns: 1fr auto 1fr auto 1fr;
grid-template-rows: unset;
}
.bridge-line {
width: 100%;
height: 2px;
min-height: 2px;
}
.bridge-tag {
writing-mode: horizontal-tb;
transform: none;
}
}
</style>
</head>
<body>
<div class="page">
<section class="hero">
<div class="hero-card">
<div class="eyebrow">Topology / mev stack</div>
<h1>mevnode_sol daje chain access. mevnode_bot robi produkt, ingest i execution.</h1>
<p>
Ten układ rozdziela warstwę validatora od warstwy aplikacyjnej. Na <strong>sol</strong>
trzymasz tylko Agave, Yellowstone i lokalne RPC. Na <strong>bot</strong> działa
<strong>single-node k3s</strong> z frontendem, API, DLOB, strategiami i bazami danych.
</p>
</div>
<aside class="legend">
<div class="legend-item"><span class="swatch read"></span> Read path: Yellowstone / live stream</div>
<div class="legend-item"><span class="swatch write"></span> Write path: RPC / TPU / Jito</div>
<div class="legend-item"><span class="swatch ops"></span> Ops path: monitoring, metryki, GitOps</div>
<div class="legend-item"><span class="swatch store"></span> Data path: Postgres, Redis, analityka</div>
<p>
Zasada jest prosta: <strong>na bot nie trafia nigdy Agave ani ledger</strong>.
`mevnode_bot` ma tylko klienty, logikę tradingową i interfejs użytkownika.
</p>
</aside>
</section>
<section class="diagram">
<article class="node">
<div class="node-head">
<h2>mevnode_sol</h2>
<span>bare metal / validator host</span>
</div>
<div class="stack">
<div class="box read">
<h3>Agave validator</h3>
<p>Źródło stanu chaina, snapshotów i lokalnego read/write RPC.</p>
<ul>
<li><code>127.0.0.1:8899</code> RPC</li>
<li><code>10.91.0.1:10000</code> Yellowstone gRPC</li>
<li><code>10.91.0.1:8999</code> Prometheus plugin metrics</li>
</ul>
</div>
<div class="box write">
<h3>Write capability</h3>
<p>Bot może później używać lokalnego RPC przez proxy lub iść bezpośrednio przez TPU/Jito.</p>
</div>
<div class="box ops">
<h3>Node-only responsibilities</h3>
<ul>
<li>snapshot recovery</li>
<li>accounts index</li>
<li>geyser / yellowstone plugin</li>
<li>node metrics</li>
</ul>
</div>
</div>
</article>
<div class="bridge" aria-hidden="true">
<div class="bridge-line"></div>
<div class="bridge-tag">wg0 / yellowstone / rpc / metrics</div>
<div class="bridge-line"></div>
<div class="bridge-tag">low latency read + controlled write</div>
<div class="bridge-line"></div>
</div>
<article class="node">
<div class="node-head">
<h2>mevnode_bot</h2>
<span>single-node k3s / app + execution host</span>
</div>
<div class="stack">
<div class="box read">
<h3>Read plane</h3>
<ul>
<li><strong>yellowstone-consumer</strong>: bierze live stream z <code>10.91.0.1:10000</code></li>
<li><strong>trade-ingestor</strong>: transformuje i dystrybuuje dane aplikacyjne</li>
<li><strong>dlob-publisher</strong>: dociąga stan kont przez RPC i buduje DLOB</li>
<li><strong>dlob-server</strong>: wystawia gotowy widok dla API i UI</li>
</ul>
</div>
<div class="box write">
<h3>Execution plane</h3>
<ul>
<li><strong>strategy-engine</strong>: decyzje tradingowe</li>
<li><strong>risk-manager</strong>: limity i walidacja</li>
<li><strong>order-manager</strong>: lifecycle zleceń</li>
<li><strong>tx-router</strong>: wybiera <code>RPC</code>, <code>TPU</code> albo <code>Jito</code></li>
</ul>
</div>
<div class="box store">
<h3>Data plane</h3>
<ul>
<li><strong>Postgres</strong>: trwały storage, pozycje, historia, config</li>
<li><strong>Hasura</strong>: warstwa GraphQL nad Postgres</li>
<li><strong>Redis</strong>: cache, pub/sub, szybki stan DLOB</li>
<li><strong>ClickHouse</strong>: opcjonalnie później pod cięższą analitykę</li>
</ul>
</div>
<div class="box ops">
<h3>Product and ops</h3>
<ul>
<li><strong>frontend</strong> + <strong>api</strong> + <strong>ingress</strong></li>
<li><strong>Prometheus</strong> + <strong>Grafana</strong></li>
<li><strong>Portainer agent</strong> dla widoczności klastra w Portainerze</li>
<li><strong>GitOps</strong>: Argo lub Flux z Gitea</li>
</ul>
</div>
</div>
</article>
</section>
<section class="strips">
<div class="strip">
<h3>Read path</h3>
<p>
<code>Yellowstone gRPC</code> daje live account updates, sloty i transakcje.
<code>Agave RPC</code> zostaje do odczytów punktowych typu
<code>getAccountInfo</code>, <code>getMultipleAccounts</code>,
<code>getHealth</code> i <code>getLatestBlockhash</code>.
</p>
</div>
<div class="strip">
<h3>Write path</h3>
<p>
Wysyłka transakcji nie idzie przez Yellowstone. To robi
<code>RPC</code>, <code>TPU</code> albo <code>Jito</code>.
Dzięki temu read plane i write plane są od siebie operacyjnie odseparowane.
</p>
</div>
<div class="strip">
<h3>Deployment rule</h3>
<p>
Wszystko aplikacyjne i webowe ląduje na <code>mevnode_bot</code>.
Wszystko validatorowe zostaje na <code>mevnode_sol</code>.
To upraszcza debug, upgrade i kontrolę ryzyka.
</p>
</div>
</section>
<section class="note">
<p>
Docelowy przepływ: <code>mevnode_sol -> wg0 -> yellowstone-consumer / dlob-publisher -> redis / postgres -> api -> frontend</code>.
Osobno działa execution path: <code>strategy-engine -> tx-router -> RPC / TPU / Jito</code>.
</p>
</section>
</div>
</body>
</html>

View File

@@ -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ć snowflakea). Każdy deploy ma być **snapshootem**, do którego można wrócić: *git commit w `trade-deploy` + pin do obrazu* (`sha-<shortsha>` 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 snapshoota.
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).

156
momentum-service-v1.md Normal file
View File

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

126
observer-right-rail-v1.md Normal file
View File

@@ -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

240
observer-sol-v1.md Normal file
View File

@@ -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

192
observer-visualizer-v1.md Normal file
View File

@@ -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

168
rpc/README.md Normal file
View File

@@ -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://<rpc_wg_ip>:8899`
- RPC WS (pubsub): `WS_ENDPOINT=ws://<rpc_wg_ip>:8900`
- Yellowstone gRPC: `GRPC_ENDPOINT=http://<rpc_wg_ip>: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).

71
rpc/baremetal-iac.md Normal file
View File

@@ -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 <YOUR_REPO_URL> 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.

136
rpc/baremetal-monitoring.md Normal file
View File

@@ -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://<rpc_wg_ip>:9100/metrics | head`
- RPC probe metryki: `curl -sS http://<rpc_wg_ip>:9100/metrics | rg "solana_rpc_" | head`
- yellowstone: `curl -sS http://<rpc_wg_ip>: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="<twoj-node-exporter-instance>"}`
- `solana_rpc_slot_reference{job="mpabi-node-exporter",instance="<twoj-node-exporter-instance>"}`
- `solana_rpc_slot_lag{job="mpabi-node-exporter",instance="<twoj-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://<rpc_wg_ip>:9100/metrics` (host + solana_rpc_*),
- `http://<rpc_wg_ip>: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 > 12 min (jeśli liczysz lag)
- `node_filesystem_avail_bytes` < 1015% (ledger/accounts szybko zjadają dysk)
- wysokie `node_cpu_seconds_total{mode="iowait"}` (NVMe bottleneck)

121
rpc/bot-executor-api.md Normal file
View File

@@ -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ł” nodea)
- `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.

79
rpc/geyser-dlob.md Normal file
View File

@@ -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://<rpc_wg_ip>:8899`
- `WS_ENDPOINT=ws://<rpc_wg_ip>:8900`
- `GRPC_ENDPOINT=http://<rpc_wg_ip>: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).

89
rpc/software-placement.md Normal file
View File

@@ -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 planey: `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.

232
rpc/topol.html Normal file
View File

@@ -0,0 +1,232 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width, initial-scale=1" />
<title>Professional Drift Trading Stack (Own Solana RPC + Own DLOB + Vast ML)</title>
<style>
:root { color-scheme: light dark; }
body {
font-family: system-ui, -apple-system, Segoe UI, Roboto, Arial, sans-serif;
line-height: 1.5;
margin: 0;
padding: 32px 20px;
max-width: 980px;
margin-inline: auto;
}
header { margin-bottom: 24px; }
h1 { font-size: 1.6rem; margin: 0 0 8px; }
.subtitle { opacity: 0.85; margin: 0; }
.card {
border: 1px solid rgba(127,127,127,0.35);
border-radius: 14px;
padding: 18px 18px;
margin: 14px 0;
background: rgba(127,127,127,0.05);
}
h2 { font-size: 1.2rem; margin: 0 0 10px; }
h3 { font-size: 1.05rem; margin: 14px 0 8px; }
ul { margin: 8px 0 0 18px; }
li { margin: 6px 0; }
code, pre { font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; }
.note {
border-left: 4px solid rgba(127,127,127,0.55);
padding: 10px 12px;
margin: 10px 0 0;
background: rgba(127,127,127,0.07);
border-radius: 10px;
}
.pill {
display: inline-block;
padding: 2px 10px;
border: 1px solid rgba(127,127,127,0.35);
border-radius: 999px;
font-size: 0.85rem;
opacity: 0.9;
}
</style>
</head>
<body>
<header>
<h1>Professional Drift Trading Stack</h1>
<p class="subtitle">
Own Solana RPC + Own Drift DLOB (Orderbook). Main rule:
<strong>keep the RPC box lean</strong>, put “trading services” on your second VPS.
<span class="pill">Target: min 10 markets</span>
<span class="pill">+ Vast (GPU) for ML</span>
</p>
</header>
<section class="card">
<h2>Overview</h2>
<p>
Yes — you can build a professional Drift trading stack with your own Solana RPC + your own DLOB,
but youll want a few supporting services around them. The main rule:
keep the RPC box lean, put “trading services” on your second VPS.
</p>
</section>
<section class="card">
<h2>On the Solana RPC server (dedicated) — keep it lean</h2>
<h3>Must-have</h3>
<ul>
<li>
<strong>Solana validator/RPC node</strong><br />
The base RPC your whole stack reads from / sends transactions through.
</li>
<li>
<strong>WireGuard</strong><br />
So RPC is reachable only privately (your second VPS + your admin).
</li>
<li>
<strong>Firewall (nftables/ufw)</strong><br />
Block RPC ports on public NIC; allow them only on WireGuard.
</li>
<li>
<strong>Time sync (chrony)</strong><br />
For stable networking, logs, and trading timestamps.
</li>
<li>
<strong>Monitoring exporters</strong>
<ul>
<li><strong>node_exporter</strong> (CPU/RAM/disk/iowait/network)</li>
<li><strong>solana-exporter</strong> (RPC/validator health via RPC)</li>
</ul>
</li>
<li>
<strong>Log + disk hygiene</strong>
<ul>
<li>logrotate/journald limits</li>
<li>NVMe health (smartmontools/nvme-cli)</li>
<li>alerts on disk filling / iowait</li>
</ul>
</li>
</ul>
<h3>Optional but “pro”</h3>
<ul>
<li>
<strong>Geyser streaming (Yellowstone gRPC plugin)</strong><br />
This gives ultra-low-latency streams of accounts/tx/slots compared to polling RPC.
Useful if you build your own real-time analytics pipeline.
<div class="note">
For Drift specifically, you can run without Geyser at the beginning,
but its the next step when you want “faster-than-RPC” feeds.
</div>
</li>
</ul>
</section>
<section class="card">
<h2>On the second VPS (your trading / app box) — where “pro trading” lives</h2>
<h3>Must-have</h3>
<ul>
<li>
<strong>Drift DLOB server (self-hosted)</strong><br />
This maintains the Drift decentralized orderbook view “fresh off your RPC” and exposes
REST + WS + gRPC/polling, plus health/metrics.
</li>
<li>
<strong>(Optional but common) Drift Gateway</strong><br />
A self-hosted API gateway to interact with Drift; handy for standardized API endpoints
around trading / market info.
</li>
<li>
<strong>Cache (Redis)</strong><br />
Cache top-of-book, funding, oracle snapshots, risk checks; protects your DLOB + RPC
from bursty bot load.
</li>
<li>
<strong>Metrics + dashboards</strong><br />
Prometheus + Grafana + Alertmanager
<div class="note">
Keep Grafana off the validator box; common ops guidance is to separate monitoring UI for safety.
</div>
</li>
<li>
<strong>Your trading services</strong>
<ul>
<li>strategy engine(s)</li>
<li>execution service (transaction builder/sender)</li>
<li>risk service (position limits, kill-switch, circuit breakers)</li>
</ul>
</li>
</ul>
<h3>Optional, depending on how “institutional” you want</h3>
<ul>
<li>
<strong>Database (Postgres/Timescale)</strong><br />
Persist fills, order events, PnL series, backtesting datasets.
</li>
<li>
<strong>Message bus (NATS/Kafka/Redis Streams)</strong><br />
Decouple ingestion (orderbook/events) from strategies/execution.
</li>
</ul>
</section>
<section class="card">
<h2>On the third box (Vast GPU) — model / research plane</h2>
<h3>What lives here</h3>
<ul>
<li>
<strong>Training (Transformer / ML)</strong><br />
Train on datasets exported from your VPS (orderbook stats, trades, features).
</li>
<li>
<strong>(Optional) Inference endpoint</strong><br />
Serve model decisions back to VPS. The VPS still owns execution.
</li>
</ul>
<div class="note">
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.
</div>
</section>
<section class="card">
<h2>Cost model (since you asked “cost per request”)</h2>
<p>
With your own RPC, there is no per-request billing. The “cost” is:
</p>
<ul>
<li>fixed monthly servers (your €149/m + the second VPS),</li>
<li>and capacity (CPU/RAM/NVMe/bandwidth) consumed by:
<ul>
<li>DLOB syncing from RPC,</li>
<li>number of WS subscriptions,</li>
<li>how many markets you track.</li>
</ul>
</li>
</ul>
<p class="note">
DLOB exists specifically to reduce RPC load by serving orderbook/trade views to clients
instead of every client rebuilding it from chain.
</p>
</section>
<section class="card">
<h2>Minimal “pro” starting set (recommended)</h2>
<ul>
<li><strong>RPC box:</strong> Solana RPC + WireGuard + firewall + node_exporter + solana-exporter</li>
<li><strong>App VPS:</strong> DLOB server + Redis + Prometheus/Grafana + your bot services</li>
</ul>
<p class="note">
For <strong>min 10 markets</strong>, expect the first scaling pressure to come from
continuous streaming + decoding + caching (DLOB + Redis + your strategy/execution),
and from your RPCs WS load. Next step after the minimal set is usually:
better streaming (Geyser) or more RAM/NVMe depending on bottleneck.
</p>
</section>
<footer style="opacity:.75; margin-top: 22px;">
<small>Saved as HTML — you can paste this into a file like <code>drift-stack.html</code>.</small>
</footer>
</body>
</html>

158
stats.md Normal file
View File

@@ -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 topN 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 topofbook (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), mikroceny, 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 topN levelach.
- `depth_bid_usd`, `depth_ask_usd` (USD): suma `size_base * price` po topN 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 “topN 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 slippagevssize.
## 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` (topofbook).
- 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ść topN + 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 , zakres [-1..1]) albo jako wskaźnik liczbowy.
Efekt: ile jest płynności najbliżej topofbook (w definicji topN 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 / multiline:
- 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. 010, 1020, 2050 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`.
- X: `size_usd`.
- 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` ś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 timeseries (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 25 mogą być prawdziwymi wykresami w czasie”, a nie tylko bieżącym odczytem.

View File

@@ -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 proxyuje 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).

37
todo.md Normal file
View File

@@ -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

692
trade-system-flow.html Normal file
View File

@@ -0,0 +1,692 @@
<!doctype html>
<html lang="pl">
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Trade system flow</title>
<style>
:root {
--bg: #efe7db;
--paper: rgba(255, 251, 245, 0.94);
--panel: #fffdf9;
--ink: #1c1814;
--muted: #655d53;
--line: #d9ccb9;
--web: #2a6b8f;
--api: #0f6c72;
--data: #4e5968;
--chain: #b24a32;
--exec: #7b6110;
--ok: #1d6b43;
--warn: #914017;
--shadow: 0 18px 44px rgba(28, 24, 20, 0.10);
--radius: 18px;
}
* { box-sizing: border-box; }
body {
margin: 0;
color: var(--ink);
font-family: "IBM Plex Sans", "Segoe UI", sans-serif;
background:
radial-gradient(circle at top left, rgba(42, 107, 143, 0.12), transparent 28%),
radial-gradient(circle at top right, rgba(178, 74, 50, 0.12), transparent 24%),
linear-gradient(180deg, #f8f3eb 0%, var(--bg) 100%);
}
.page {
max-width: 1540px;
margin: 0 auto;
padding: 28px 18px 42px;
}
.hero,
.card,
.table-card,
.lane,
.note,
.tabs {
background: var(--paper);
border: 1px solid rgba(217, 204, 185, 0.9);
border-radius: var(--radius);
box-shadow: var(--shadow);
backdrop-filter: blur(10px);
}
.hero {
padding: 26px;
margin-bottom: 18px;
}
.eyebrow {
display: inline-flex;
align-items: center;
gap: 8px;
padding: 6px 10px;
border-radius: 999px;
border: 1px solid rgba(28, 24, 20, 0.10);
color: var(--muted);
font-size: 12px;
font-weight: 800;
letter-spacing: 0.08em;
text-transform: uppercase;
}
h1 {
margin: 14px 0 10px;
font-size: clamp(2rem, 4vw, 3.9rem);
line-height: 0.96;
font-family: "IBM Plex Serif", Georgia, serif;
}
p {
margin: 0;
color: var(--muted);
line-height: 1.58;
}
.tabs {
display: flex;
gap: 10px;
flex-wrap: wrap;
padding: 12px;
margin-bottom: 18px;
}
.tab {
border: 1px solid var(--line);
background: #fffaf2;
border-radius: 999px;
padding: 10px 14px;
font: inherit;
font-weight: 800;
cursor: pointer;
transition: 150ms ease;
}
.tab.active,
.tab:hover {
transform: translateY(-1px);
box-shadow: 0 10px 22px rgba(28, 24, 20, 0.08);
}
.tab[data-target="overview"].active { color: var(--web); }
.tab[data-target="routes"].active { color: var(--api); }
.tab[data-target="payloads"].active { color: var(--chain); }
.view { display: none; }
.view.active { display: block; }
.lanes {
display: grid;
grid-template-columns: 1.05fr 0.9fr 0.95fr;
gap: 16px;
margin-bottom: 18px;
}
.lane {
padding: 16px;
}
.lane h2 {
margin: 0 0 6px;
font-size: 1.2rem;
font-family: "IBM Plex Serif", Georgia, serif;
}
.lane small {
display: block;
color: var(--muted);
margin-bottom: 14px;
line-height: 1.45;
}
.stack {
display: grid;
gap: 12px;
}
.card {
padding: 14px;
background: var(--panel);
border-left: 6px solid var(--line);
}
.card h3 {
margin: 0 0 6px;
font-size: 1rem;
}
.meta {
font-size: 11px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
font-weight: 800;
margin-bottom: 8px;
}
.card.web { border-left-color: var(--web); }
.card.api { border-left-color: var(--api); }
.card.data { border-left-color: var(--data); }
.card.chain { border-left-color: var(--chain); }
.card.exec { border-left-color: var(--exec); }
.chips {
display: flex;
flex-wrap: wrap;
gap: 8px;
margin-top: 10px;
}
.chip {
display: inline-block;
padding: 4px 8px;
border-radius: 999px;
font-size: 12px;
font-weight: 700;
background: rgba(28, 24, 20, 0.06);
}
.flow {
display: grid;
gap: 14px;
margin-top: 12px;
}
.flow-row {
display: grid;
grid-template-columns: repeat(5, minmax(0, 1fr));
gap: 12px;
align-items: stretch;
}
.flow-node {
padding: 14px;
border: 1px solid var(--line);
border-radius: 14px;
background: #fffdf9;
min-height: 118px;
position: relative;
}
.flow-node strong {
display: block;
margin-bottom: 8px;
font-size: 0.98rem;
}
.arrow {
display: flex;
align-items: center;
justify-content: center;
font-size: 24px;
color: var(--muted);
font-weight: 700;
}
.table-card {
padding: 14px;
overflow: auto;
}
table {
width: 100%;
border-collapse: collapse;
min-width: 980px;
}
th,
td {
padding: 12px 10px;
border-bottom: 1px solid var(--line);
text-align: left;
vertical-align: top;
line-height: 1.45;
}
th {
font-size: 12px;
text-transform: uppercase;
letter-spacing: 0.08em;
color: var(--muted);
}
code,
pre {
font-family: "IBM Plex Mono", monospace;
}
pre {
margin: 0;
white-space: pre-wrap;
word-break: break-word;
background: rgba(28, 24, 20, 0.04);
border-radius: 12px;
padding: 14px;
line-height: 1.45;
}
.payload-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 16px;
}
.note {
padding: 16px 18px;
margin-top: 18px;
}
.status {
display: inline-flex;
align-items: center;
gap: 6px;
font-size: 12px;
font-weight: 800;
padding: 4px 8px;
border-radius: 999px;
background: rgba(28, 24, 20, 0.06);
}
.ok { color: var(--ok); background: rgba(29, 107, 67, 0.10); }
.warn { color: var(--warn); background: rgba(178, 74, 50, 0.10); }
@media (max-width: 1220px) {
.lanes,
.payload-grid,
.flow-row {
grid-template-columns: 1fr;
}
.arrow {
transform: rotate(90deg);
min-height: 32px;
}
table { min-width: 720px; }
}
</style>
</head>
<body>
<div class="page">
<section class="hero">
<div class="eyebrow">Trade architecture / runtime map</div>
<h1>Skąd dane trafiają, w jakiej postaci i które endpointy naprawdę wystawiamy</h1>
<p>
Ta strona opisuje faktyczny przepływ runtime dla waszej appki: <strong>browser -> frontend -> API/Hasura -> Postgres/Redis</strong>
oraz zależności od <strong>Agave RPC</strong> i <strong>Yellowstone gRPC</strong>. To nie jest ogólny diagram architektury,
tylko mapa wejść, wyjść, formatów danych i tras HTTP/WS używanych przez produkt.
</p>
</section>
<section class="tabs">
<button class="tab active" data-target="overview">Overview</button>
<button class="tab" data-target="routes">Routes / APIs</button>
<button class="tab" data-target="payloads">Payloads / shapes</button>
</section>
<section class="view active" id="overview">
<div class="lanes">
<article class="lane">
<h2>Public web surface</h2>
<small>To widzi użytkownik i to jest jedyny realny entrypoint produktu na zewnątrz.</small>
<div class="stack">
<div class="card web">
<div class="meta">NodePort / browser entry</div>
<h3>trade-frontend</h3>
<p>Serwuje SPA, obsługuje logowanie, robi proxy do API i Hasury.</p>
<div class="chips">
<span class="chip">NodePort 30081</span>
<span class="chip">service 8081</span>
<span class="chip">/api/*</span>
<span class="chip">/graphql</span>
</div>
</div>
<div class="card web">
<div class="meta">Browser calls</div>
<h3>Frontend SPA</h3>
<p>Wykres bierze REST JSON. DLOB live bierze GraphQL WebSocket. Auth idzie przez sesję cookie.</p>
<div class="chips">
<span class="chip">GET /whoami</span>
<span class="chip">POST /auth/login</span>
<span class="chip">GET /api/v1/chart</span>
<span class="chip">WS /graphql</span>
</div>
</div>
</div>
</article>
<article class="lane">
<h2>App services in k3s</h2>
<small>Wewnętrzne usługi aplikacyjne. Tylko frontend jest dziś wystawiony na zewnątrz jako NodePort.</small>
<div class="stack">
<div class="card api">
<div class="meta">ClusterIP / app logic</div>
<h3>trade-api</h3>
<p>REST backend. Zwraca candles, przyjmuje tick ingest, zarządza tokenami.</p>
<div class="chips">
<span class="chip">ClusterIP 8787</span>
<span class="chip">/v1/chart</span>
<span class="chip">/v1/ticks</span>
<span class="chip">/v1/ingest/tick</span>
</div>
</div>
<div class="card data">
<div class="meta">ClusterIP / query plane</div>
<h3>Hasura + Postgres</h3>
<p>Frontend subskrybuje Hasurę po GraphQL WS. API czyta i zapisuje przez GraphQL do Hasury.</p>
<div class="chips">
<span class="chip">Hasura 8080</span>
<span class="chip">Postgres 5432</span>
<span class="chip">GraphQL</span>
<span class="chip">subscriptions</span>
</div>
</div>
<div class="card data">
<div class="meta">Redis-backed read side</div>
<h3>DLOB stack</h3>
<p><span class="status warn">publisher 0/1</span> <span class="status warn">server 0/1</span> bo bootstrapping zależy od zdrowego Agave RPC.</p>
<div class="chips">
<span class="chip">Redis 6379</span>
<span class="chip">dlob-server 6969</span>
<span class="chip">workers</span>
</div>
</div>
</div>
</article>
<article class="lane">
<h2>Chain and execution plane</h2>
<small>To nie jest wystawiane użytkownikowi końcowemu. To feed i write side dla botów.</small>
<div class="stack">
<div class="card chain">
<div class="meta">mevnode_sol / source of truth</div>
<h3>Agave RPC</h3>
<p>Punktowe odczyty: <code>getAccountInfo</code>, <code>getMultipleAccounts</code>, <code>getSlot</code>, <code>getHealth</code>.</p>
<div class="chips">
<span class="chip">127.0.0.1:8899</span>
<span class="chip">RPC only</span>
</div>
</div>
<div class="card chain">
<div class="meta">mevnode_sol / live feed</div>
<h3>Yellowstone gRPC</h3>
<p>Pushowy stream kont, slotów i transakcji po <code>wg0</code>.</p>
<div class="chips">
<span class="chip">10.91.0.1:10000</span>
<span class="chip">token auth</span>
</div>
</div>
<div class="card exec">
<div class="meta">future execution split</div>
<h3>tx-router / TPU / Jito</h3>
<p>To jest write path. Nie mylić z Yellowstone, który jest tylko read streamem.</p>
</div>
</div>
</article>
</div>
<div class="table-card">
<div class="meta">End-to-end runtime flow</div>
<div class="flow">
<div class="flow-row">
<div class="flow-node"><strong>1. Browser</strong>Użytkownik otwiera SPA i trzyma sesję cookie.</div>
<div class="arrow"></div>
<div class="flow-node"><strong>2. trade-frontend</strong>Serwuje UI i robi reverse proxy do <code>/api</code> i <code>/graphql</code>.</div>
<div class="arrow"></div>
<div class="flow-node"><strong>3. trade-api / Hasura</strong>REST dla chartów, GraphQL WS dla live DLOB.</div>
</div>
<div class="flow-row">
<div class="flow-node"><strong>4. Postgres / Redis</strong>Trwały stan i szybki stan pośredni.</div>
<div class="arrow"></div>
<div class="flow-node"><strong>5. DLOB workers</strong>Przeliczają depth, slippage, orderbook views.</div>
<div class="arrow"></div>
<div class="flow-node"><strong>6. Agave / Yellowstone</strong>RPC do odczytów punktowych, gRPC do streamu live.</div>
</div>
</div>
</div>
<div class="note">
<p>
Najważniejszy podział: <strong>chart path = REST JSON przez trade-api</strong>, a <strong>DLOB live = GraphQL WS przez Hasurę</strong>.
To oznacza, że nie cały frontend jedzie jednym typem transportu. Produkt ma dwa równoległe read pathy.
</p>
</div>
</section>
<section class="view" id="routes">
<div class="table-card">
<table>
<thead>
<tr>
<th>Surface</th>
<th>Endpoint</th>
<th>Transport</th>
<th>Input</th>
<th>Output</th>
<th>Where it goes</th>
</tr>
</thead>
<tbody>
<tr>
<td>Frontend</td>
<td><code>GET /whoami</code></td>
<td>HTTP JSON</td>
<td>cookie session</td>
<td><code>{ ok, user, mode }</code></td>
<td>handled directly by <code>trade-frontend</code></td>
</tr>
<tr>
<td>Frontend</td>
<td><code>POST /auth/login</code></td>
<td>HTTP JSON/form</td>
<td><code>username</code>, <code>password</code></td>
<td><code>{ ok, user }</code> + session cookie</td>
<td>handled directly by <code>trade-frontend</code></td>
</tr>
<tr>
<td>Frontend</td>
<td><code>POST /auth/logout</code></td>
<td>HTTP JSON</td>
<td>session cookie</td>
<td><code>{ ok: true }</code></td>
<td>handled directly by <code>trade-frontend</code></td>
</tr>
<tr>
<td>Frontend proxy</td>
<td><code>/api/*</code></td>
<td>HTTP</td>
<td>browser request, frontend injects read token</td>
<td>proxied API response</td>
<td><code>trade-api:8787</code></td>
</tr>
<tr>
<td>Frontend proxy</td>
<td><code>/graphql</code></td>
<td>HTTP GraphQL</td>
<td>GraphQL query/mutation</td>
<td>GraphQL response</td>
<td><code>hasura:8080/v1/graphql</code></td>
</tr>
<tr>
<td>Frontend proxy</td>
<td><code>WS /graphql</code></td>
<td>GraphQL WebSocket</td>
<td>subscription payload</td>
<td>live subscription frames</td>
<td><code>hasura:8080/v1/graphql</code></td>
</tr>
<tr>
<td>API</td>
<td><code>GET /v1/chart</code></td>
<td>HTTP JSON</td>
<td><code>symbol</code>, <code>tf</code>, <code>limit</code>, optional <code>source</code></td>
<td>candles + indicators + flow rows</td>
<td>Hasura function <code>get_drift_candles</code> + table <code>drift_ticks</code></td>
</tr>
<tr>
<td>API</td>
<td><code>POST /v1/ingest/tick</code></td>
<td>HTTP JSON</td>
<td>tick payload</td>
<td><code>{ ok, id }</code></td>
<td>writes tick through Hasura into DB</td>
</tr>
<tr>
<td>API</td>
<td><code>GET /v1/ticks</code></td>
<td>HTTP JSON</td>
<td><code>symbol</code>, optional <code>source</code>, <code>limit</code>, <code>from</code>, <code>to</code></td>
<td>tick list</td>
<td>reads <code>drift_ticks</code> through Hasura</td>
</tr>
<tr>
<td>API admin</td>
<td><code>POST /v1/admin/tokens</code></td>
<td>HTTP JSON</td>
<td>name, scopes</td>
<td>new API token</td>
<td>stored in DB via Hasura</td>
</tr>
<tr>
<td>API admin</td>
<td><code>POST /v1/admin/tokens/revoke</code></td>
<td>HTTP JSON</td>
<td>token id</td>
<td>revocation status</td>
<td>updates token row in DB</td>
</tr>
<tr>
<td>Chain read</td>
<td><code>Agave RPC</code></td>
<td>JSON-RPC</td>
<td>account/state requests</td>
<td>point reads</td>
<td><code>mevnode_sol</code></td>
</tr>
<tr>
<td>Chain live</td>
<td><code>Yellowstone gRPC</code></td>
<td>gRPC streaming</td>
<td>subscription config + token</td>
<td>account/slot/tx stream</td>
<td><code>mevnode_sol</code></td>
</tr>
</tbody>
</table>
</div>
</section>
<section class="view" id="payloads">
<div class="payload-grid">
<div class="table-card">
<div class="meta">Chart response</div>
<pre>{
"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": [] }
}
}</pre>
</div>
<div class="table-card">
<div class="meta">Tick ingest body</div>
<pre>{
"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"
}
}</pre>
</div>
<div class="table-card">
<div class="meta">GraphQL DLOB subscriptions</div>
<pre>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
}
}</pre>
</div>
<div class="table-card">
<div class="meta">L2 payload shape</div>
<pre>{
"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</pre>
</div>
</div>
<div class="note">
<p>
Krytyczne rozróżnienie: <strong>DLOB live nie idzie z REST API</strong>. DLOB do UI idzie przez <strong>Hasura GraphQL subscriptions</strong>,
a chart przez <strong>REST JSON</strong> z <code>trade-api</code>. To są dwa osobne transporty, dwa osobne modele danych i dwa osobne źródła opóźnień.
</p>
</div>
</section>
</div>
<script>
const tabs = [...document.querySelectorAll('.tab')];
const views = [...document.querySelectorAll('.view')];
tabs.forEach((tab) => {
tab.addEventListener('click', () => {
const target = tab.dataset.target;
tabs.forEach((x) => x.classList.toggle('active', x === tab));
views.forEach((view) => view.classList.toggle('active', view.id === target));
});
});
</script>
</body>
</html>

42
visualizer-candles.md Normal file
View File

@@ -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` — mikroruchy (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`.

99
workflow.md Normal file
View File

@@ -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 **snapshootem**, 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-<shortsha>` 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 proxyuje `/api/*` do VPS.
- UI ma też GraphQL (Hasura) pod `/graphql` (HTTP + WS subscriptions) w dev proxyujemy `/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 proxyujemy też `/whoami`, `/auth/*`, `/logout`.
- Dev proxy usuwa `Secure` z `Set-Cookie`, żeby cookie działało na `http://localhost:5173`.
- Na VPS `trade-frontend` proxyuje 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.