docs(snapshot): sync workspace documentation
This commit is contained in:
204
3700x.md
Normal file
204
3700x.md
Normal 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
299
agave-sync-recipe.md
Normal 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
|
||||||
151
bot-control-plane-staging-v1.md
Normal file
151
bot-control-plane-staging-v1.md
Normal 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
|
||||||
|
```
|
||||||
209
bot-microservices-hasura-fastify.md
Normal file
209
bot-microservices-hasura-fastify.md
Normal 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
142
bot-strategy-reversal.md
Normal 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 (top‑N):
|
||||||
|
- `min(depth_bid_usd, depth_ask_usd) >= depth_topn_min_usd`
|
||||||
|
|
||||||
|
Wariant B (band bps):
|
||||||
|
- `min(bid_usd(band_bps=X), ask_usd(band_bps=X)) >= depth_band_min_usd`
|
||||||
|
|
||||||
|
W praktyce często warto użyć obu: top‑N + band (bo mierzą różne rzeczy).
|
||||||
|
|
||||||
|
## Trigger reversal (sekundy) — szkic logiki
|
||||||
|
|
||||||
|
To jest intencjonalnie prosty szkic, który można zastąpić modelem ML później.
|
||||||
|
|
||||||
|
Przykład (entry LONG):
|
||||||
|
- `mom_30s < 0` (krótkoterminowo spadkowo),
|
||||||
|
- `mom_3s > 0` (zaczyna się odbicie),
|
||||||
|
- opcjonalnie: `imbalance` rośnie (przechyla się na bid) albo spread się zawęża,
|
||||||
|
- gates spełnione.
|
||||||
|
|
||||||
|
Przykład (entry SHORT) analogicznie:
|
||||||
|
- `mom_30s > 0`, `mom_3s < 0`, itd.
|
||||||
|
|
||||||
|
## Wystawienie i “modyfikacja” wejścia (order policy)
|
||||||
|
|
||||||
|
### Zalecenie MVP: limit/post-only + chase
|
||||||
|
Zamiast market (który “nie jest modyfikowalny”), utrzymujemy order przy top‑of‑book:
|
||||||
|
|
||||||
|
- Place entry limit:
|
||||||
|
- buy: blisko `best_bid` (np. `best_bid + tick`)
|
||||||
|
- sell: blisko `best_ask` (np. `best_ask - tick`)
|
||||||
|
- Jeśli order nie fill i cena “ucieka”:
|
||||||
|
- cancel + place nowy (reprice),
|
||||||
|
- ale z ograniczeniami: `cooldown_ms`, `max_orders_per_min`, `reprice_bps`.
|
||||||
|
|
||||||
|
Market order może być fallback tylko przy `urgency=true` i nadal spełnionych gates (zwłaszcza slippage).
|
||||||
|
|
||||||
|
## Zarządzanie pozycją (minuty) — proste reguły
|
||||||
|
|
||||||
|
Minimalne wyjścia (wystarczy na start):
|
||||||
|
- **max hold time**: `hold_max_s` (po czasie wychodzimy zawsze),
|
||||||
|
- **stop loss**: `stop_bps` od ceny wejścia,
|
||||||
|
- **take profit**: `take_bps` od ceny wejścia,
|
||||||
|
- **degradacja warunków**: wyjście, gdy np. `spread_bps` lub `impact_bps` przekracza limit przez X sekund.
|
||||||
|
|
||||||
|
## Parametry bota (do `bot_config`) — propozycja
|
||||||
|
|
||||||
|
Parametry są zgrupowane tak, żeby łatwo je stroić.
|
||||||
|
|
||||||
|
### A) Sizing i częstotliwość
|
||||||
|
- `entry_notional_usd`: ile USD “na wejście”
|
||||||
|
- `min_trade_usd`: próg “ignoruj małe delty”
|
||||||
|
- `decision_interval_ms`: jak często liczysz sygnał (np. 250–1000ms)
|
||||||
|
- `hold_max_s`: maks. czas trzymania pozycji
|
||||||
|
|
||||||
|
### B) Gates mikrostruktury
|
||||||
|
- `freshness_max_ms`
|
||||||
|
- `spread_max_bps`
|
||||||
|
- `slippage_max_bps`
|
||||||
|
- `depth_topn_min_usd`
|
||||||
|
- `depth_band_bps` (np. 10 lub 20)
|
||||||
|
- `depth_band_min_usd`
|
||||||
|
|
||||||
|
### C) Reversal trigger
|
||||||
|
- `mom_fast_s` (np. 3s)
|
||||||
|
- `mom_slow_s` (np. 30s)
|
||||||
|
- `mom_entry_threshold` (jak mocny sygnał)
|
||||||
|
- `imbalance_entry_threshold` (opcjonalnie)
|
||||||
|
|
||||||
|
### D) Order policy (limit/chase)
|
||||||
|
- `order_policy`: `limit_post_only | limit | market`
|
||||||
|
- `reprice_bps`
|
||||||
|
- `reprice_after_ms`
|
||||||
|
- `cooldown_ms`
|
||||||
|
- `max_orders_per_min`
|
||||||
|
|
||||||
|
### E) Risk/exit
|
||||||
|
- `stop_bps`
|
||||||
|
- `take_bps`
|
||||||
|
- `exit_on_bad_spread_bps` + `exit_on_bad_spread_s`
|
||||||
|
- `exit_on_bad_slippage_bps` + `exit_on_bad_slippage_s`
|
||||||
|
|
||||||
|
## Metryki bota (do logowania w `bot_events`)
|
||||||
|
|
||||||
|
Żeby stroić strategię, logujemy na każdy “decision tick”:
|
||||||
|
- `features`: snapshot (albo hash + wybrane wartości: spread, slippage, depth, imbalance, mom)
|
||||||
|
- `gates`: które przeszły/nie przeszły + wartości
|
||||||
|
- `decision`: `target_exposure_usd`, `side`, `confidence`, `urgency`
|
||||||
|
- `actions`: place/cancel/close (z parametrami)
|
||||||
|
- `outcome`: fill, avg price, realized PnL (jeśli mamy), czas w pozycji
|
||||||
|
|
||||||
|
To pozwala później robić offline analizy: “dlaczego wchodził”, “gdzie przegrywa”, “jak dobrać progi”.
|
||||||
|
|
||||||
500
bot/bot1.tex
Normal file
500
bot/bot1.tex
Normal 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
359
bots.md
Normal 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” ad‑hoc; utrzymuje stan docelowy (np. target exposure) i zbiega do niego.
|
||||||
|
- **Obserwowalność:** każda decyzja i akcja ma log/metrykę/event w DB.
|
||||||
|
|
||||||
|
Słownik pojęć (DLOB/L1…L10): `doc/dlob-basics.md`.
|
||||||
|
Strategia MVP (sekundy → minuty): `doc/bot-strategy-reversal.md`.
|
||||||
|
|
||||||
|
### Non-goals (na start)
|
||||||
|
- HFT / ultra-low-latency (na początku stawiamy na stabilność i kontrolę).
|
||||||
|
- Multi-venue routing i arbitraż między giełdami.
|
||||||
|
- Zarządzanie portfelem wielu subkont per user (to może wejść później).
|
||||||
|
|
||||||
|
## Warstwy systemu (docelowo)
|
||||||
|
|
||||||
|
### 1) Data plane (VPS/k3s): market data + statsy
|
||||||
|
Cel: zapewnić botom i UI spójne, “lokalne” źródła danych.
|
||||||
|
|
||||||
|
Aktualne elementy (już istnieją w repo/stacku):
|
||||||
|
- `dlob-publisher-hot` → buduje hot-path DLOB z Solany i publikuje `dlob-hot:*` do `dlob-redis`.
|
||||||
|
- `dlob-publisher-all` → buduje pełny feed rynku i publikuje `dlob-all:*` do `dlob-redis`.
|
||||||
|
- `dlob-hot-redis-to-postgres-raw-writer` → zapisuje raw `hot` do PostgreSQL.
|
||||||
|
- `dlob-hot-postgres-to-postgres-derived-writer` → buduje derived `hot`.
|
||||||
|
- `dlob-all-redis-to-postgres-derived-writer` → buduje derived `all`.
|
||||||
|
- `Hasura` → wystawia te dane jako GraphQL + subscriptions dla UI i botów.
|
||||||
|
|
||||||
|
Konfiguracja rynków:
|
||||||
|
- `DLOB_MARKETS` (symbole: np. `SOL-PERP`, `BTC-PERP`, `1MBONK-PERP`).
|
||||||
|
- `PERP_MARKETS_TO_LOAD` (indeksy rynków ładowane przez `dlob-publisher-hot` / `dlob-publisher-all`).
|
||||||
|
|
||||||
|
### 2) Execution plane (VPS/k3s): bot executor (klucze + transakcje)
|
||||||
|
Cel: jedyne miejsce, gdzie występują klucze prywatne i podpisywanie transakcji.
|
||||||
|
|
||||||
|
Warianty uruchomienia:
|
||||||
|
- **A) 1 Deployment na bota**: proste izolowanie i rollout per bot.
|
||||||
|
- **B) 1 Deployment “executor” obsługujący wiele botów**: łatwiejszy pooling połączeń/RPC, ale większy blast radius.
|
||||||
|
|
||||||
|
Minimalne wymagania executora:
|
||||||
|
- Czyta konfigurację bota (desired state) z DB (Hasura/Postgres).
|
||||||
|
- Subskrybuje statsy/telemetrię z Hasury (GraphQL + subscriptions) lub odpytuje okresowo.
|
||||||
|
- Wykonuje akcje tradingowe (place/modify/cancel/close) z ograniczeniami ryzyka.
|
||||||
|
- Ma **hard kill switch** (patrz niżej).
|
||||||
|
|
||||||
|
## Maszyneria bota (to jest kluczowe)
|
||||||
|
|
||||||
|
To, czy bot “działa bezpiecznie”, zależy mniej od samego modelu, a bardziej od maszynerii: pętli kontrolnej, zarządzania orderami i bezpiecznego wyjścia.
|
||||||
|
|
||||||
|
Poniżej minimalny podział odpowiedzialności wewnątrz executora (nawet jeśli to jest jeden proces):
|
||||||
|
|
||||||
|
- **Config store**: odczyt `bot_config` (desired state) + zapis `bot_state`.
|
||||||
|
- **Market data adapter**: pobiera features z Hasury (np. `dlob_stats_latest`, `dlob_depth_bps_latest`, `dlob_slippage_latest`, candles).
|
||||||
|
- **Strategy adapter**: tworzy propozycję “desired” (lokalnie lub z Vast). Nie robi transakcji.
|
||||||
|
- **Risk engine**: weryfikuje limity (max pozycja, max slippage, max order rate, timeouts, freshness danych).
|
||||||
|
- **Order manager**: mapuje “desired” → konkretne akcje: place/modify/cancel; pilnuje idempotencji i retry.
|
||||||
|
- **Position reconciler**: porównuje stan konta (pozycje/ordery) z DB i “desired”; wykrywa rozjazdy.
|
||||||
|
- **Kill switch**: zawsze dostępny; wymusza cancel + close i przejście do `mode=off`.
|
||||||
|
|
||||||
|
### Stan bota jako state machine (proponowane)
|
||||||
|
|
||||||
|
Traktuj to jak automat stanów, żeby uniknąć chaosu “ifów”:
|
||||||
|
- `off`: bot nie wysyła transakcji (może tylko heartbeat).
|
||||||
|
- `observe`: liczy/loguje decyzje, ale nie handluje.
|
||||||
|
- `trade`: aktywny, utrzymuje desired-state.
|
||||||
|
- `panic`: tryb awaryjny; cancel + close; potem `off`.
|
||||||
|
|
||||||
|
Opcjonalne stany pośrednie (jeśli potrzebne): `entering`, `managing`, `exiting`.
|
||||||
|
|
||||||
|
### Minimalny model danych (MVP)
|
||||||
|
|
||||||
|
Docelowo boty muszą być “sterowane” i audytowalne z DB:
|
||||||
|
|
||||||
|
- `bot_config` (sterowanie):
|
||||||
|
- `mode`: `off|observe|trade`
|
||||||
|
- `market_name` (np. `SOL-PERP`)
|
||||||
|
- `target_exposure_usd` / `target_base`
|
||||||
|
- limity: `max_position_usd`, `max_slippage_bps`, `max_orders_per_min`, `cooldown_ms`
|
||||||
|
- `kill_switch` (bool)
|
||||||
|
- `strategy`: `{ type, params, model_endpoint, model_version }`
|
||||||
|
- `bot_state` (status runtime):
|
||||||
|
- `last_heartbeat_at`, `last_error`, `last_action_at`
|
||||||
|
- `position_snapshot` (opcjonalnie)
|
||||||
|
- `bot_events` (audit log):
|
||||||
|
- `decision` (wejście modelu/strategii + features hash + wersja)
|
||||||
|
- `order_sent`, `order_ack`, `order_filled`, `order_canceled`
|
||||||
|
- `panic_exit`, `error`
|
||||||
|
|
||||||
|
W MVP możesz zacząć od: `bot_config` + `bot_events`, a `bot_state` dodać po pierwszym działającym loopie.
|
||||||
|
|
||||||
|
### 3) Model plane (Vast): sygnały i inference (bez kluczy)
|
||||||
|
Cel: trenowanie/inference modeli na Vast, ale bez dostępu do środków.
|
||||||
|
|
||||||
|
Kontrakt między VPS a Vast:
|
||||||
|
- VPS wysyła “features snapshot” (np. statsy DLOB + candles + pozycja).
|
||||||
|
- Vast zwraca “decision” (np. `target_base`, `target_notional_usd`, `side`, `confidence`, `params`).
|
||||||
|
- VPS decyduje o wykonaniu i podpisuje transakcje.
|
||||||
|
|
||||||
|
Zasada: Vast **nigdy** nie dostaje sekretów (RPC keys, prywatne klucze, tokeny).
|
||||||
|
|
||||||
|
### 4) RPC: dedykowany Solana RPC/WS (VPS)
|
||||||
|
Cel: stabilność + mniejsze opóźnienia dla `dlob-publisher` i executora.
|
||||||
|
|
||||||
|
Założenia:
|
||||||
|
- `dlob-publisher` i executor używają tego samego RPC/WS (lub dwóch endpointów tej samej infrastruktury).
|
||||||
|
- W przyszłości możliwy fallback na publiczny endpoint (z ograniczeniami).
|
||||||
|
|
||||||
|
## Desired-state loop (core idea)
|
||||||
|
|
||||||
|
Bot działa jako pętla kontrolna:
|
||||||
|
1) Odczytaj `desired_state` (z DB) + bieżący stan (pozycja, ordery, rynek).
|
||||||
|
2) Policz różnicę (`delta`) i oceń ryzyko/limity.
|
||||||
|
3) Wykonaj minimalny zestaw akcji, żeby przybliżyć się do `desired_state`.
|
||||||
|
4) Zapisz eventy (co zrobił i dlaczego).
|
||||||
|
|
||||||
|
Przykładowe `desired_state` (na start):
|
||||||
|
- `mode`: `off | observe | trade`
|
||||||
|
- `market`: `SOL-PERP` (lub lista)
|
||||||
|
- `target_exposure_usd` albo `target_base`
|
||||||
|
- `max_position_usd`, `max_leverage`, `max_slippage_bps`
|
||||||
|
- `order_policy`: `market | limit | post_only | chase`
|
||||||
|
- `cooldown_ms`, `max_orders_per_min`
|
||||||
|
|
||||||
|
## Kill switch (must-have)
|
||||||
|
|
||||||
|
Kill switch ma działać bez udziału modelu i bez UI.
|
||||||
|
|
||||||
|
Źródła kill switch (kolejność priorytetu):
|
||||||
|
1) **Env var** executora (np. `BOT_KILL_SWITCH=1`) → natychmiastowy “panic mode”.
|
||||||
|
2) Flaga w DB (np. `bot_config.kill_switch=true`).
|
||||||
|
3) Warunek bezpieczeństwa (np. RPC lag, brak danych, brak świeżości L2, zbyt duży drawdown).
|
||||||
|
|
||||||
|
Akcja kill switch (docelowy runbook):
|
||||||
|
- `cancel_all` na rynku (lub subkoncie),
|
||||||
|
- `close_position` (preferowane: reduce-only, kontrola slippage),
|
||||||
|
- przejście bota do `mode=off` + zapis eventu “panic_exit”.
|
||||||
|
|
||||||
|
## Awaria VPS: co się dzieje z transakcjami i jak się zabezpieczyć
|
||||||
|
|
||||||
|
Kluczowe fakty:
|
||||||
|
- **Nie da się “ubić” wysłanej transakcji** na Solanie. Jeśli tx została przyjęta do sieci i wyląduje w bloku, jej efekt jest on-chain.
|
||||||
|
- To, co można zrobić po awarii, to **kolejne transakcje korygujące**: `cancel` / `close` / `reduce-only`.
|
||||||
|
|
||||||
|
W praktyce awarie dzielą się na 3 przypadki:
|
||||||
|
|
||||||
|
1) **Tx nie weszła (dropped/expired)** → po restarcie robisz reconcile i jedziesz dalej.
|
||||||
|
2) **Tx weszła i postawiła order** → po restarcie reconcile zobaczy otwarte ordery i może je utrzymać/zmodyfikować/cancel.
|
||||||
|
3) **Tx weszła i zmieniła pozycję** → po restarcie reconcile zobaczy pozycję i może ją domknąć lub dopasować do desired-state.
|
||||||
|
|
||||||
|
### Reconcile po starcie (must-have)
|
||||||
|
|
||||||
|
Każdy bot po restarcie powinien:
|
||||||
|
- wczytać `bot_config` (desired state),
|
||||||
|
- pobrać stan on-chain (pozycje + otwarte ordery dla rynku/subkonta),
|
||||||
|
- porównać “observed” vs “desired” i wykonać minimalne akcje korekcyjne.
|
||||||
|
|
||||||
|
To chroni przed sytuacją: “wysłaliśmy akcję, VPS padł przed zapisaniem eventu”.
|
||||||
|
|
||||||
|
## Guardian / Safety bot (żeby wyjść mimo padnięcia executora)
|
||||||
|
|
||||||
|
Jeśli wymaganie brzmi: “w każdej chwili możemy wyjść”, to kill switch nie może mieszkać tylko w tym samym VPS/klastrze co executor.
|
||||||
|
|
||||||
|
Proponowany wzorzec:
|
||||||
|
- Osobny, mały serwis **`bot-guardian`** uruchomiony **poza** głównym VPS (drugi VPS / inny klaster / inna VM).
|
||||||
|
- Guardian ma dostęp do:
|
||||||
|
- RPC/WS,
|
||||||
|
- klucza lub uprawnień do cancel/close (patrz uwagi bezpieczeństwa),
|
||||||
|
- sygnału “czy executor żyje” (np. `bot_state.last_heartbeat_at` w DB albo endpoint health).
|
||||||
|
|
||||||
|
Zachowanie:
|
||||||
|
- Jeśli heartbeat executora jest starszy niż `T_fail` (np. 10–30s) → guardian włącza “panic mode”:
|
||||||
|
- `cancel_all_orders`,
|
||||||
|
- `close_position` (reduce-only; z limitem slippage),
|
||||||
|
- zapis eventu `panic_exit` (jeśli ma dostęp do DB).
|
||||||
|
|
||||||
|
Uwaga: guardian nie potrzebuje modelu/AI. Ma być “głupi i niezawodny”.
|
||||||
|
|
||||||
|
### Minimalne bezpieczeństwo guardiana
|
||||||
|
|
||||||
|
- Klucze: trzymane jako secret tylko na hoście guardiana; nie w repo.
|
||||||
|
- Zakres: jeśli to możliwe, ogranicz klucz do subkonta / minimalnych uprawnień (w zależności od możliwości Drift/Solana).
|
||||||
|
- Rate-limit: zabezpieczenie przed pętlą panic (np. `panic_cooldown_ms`).
|
||||||
|
|
||||||
|
## HA executora (opcjonalnie): leader election
|
||||||
|
|
||||||
|
Jeśli chcesz 2 repliki executora (na jednym lub wielu węzłach), potrzebujesz single-writer:
|
||||||
|
- tylko **leader** składa transakcje,
|
||||||
|
- **follower** obserwuje i jest gotowy przejąć.
|
||||||
|
|
||||||
|
Wzorzec:
|
||||||
|
- lease w DB/Redis (np. rekord “lock” z TTL),
|
||||||
|
- leader odnawia lease co N sekund,
|
||||||
|
- brak odnowienia → follower przejmuje i robi reconcile.
|
||||||
|
|
||||||
|
To ogranicza ryzyko “double-trading”, ale nie zastępuje guardiana (bo oba mogą paść razem).
|
||||||
|
|
||||||
|
## Model kosztów (expected vs realized) — entry/exit/modify
|
||||||
|
|
||||||
|
Bot powinien umieć policzyć:
|
||||||
|
- **expected costs** (przed wejściem/wyjściem): czy trade ma sens i czy mieści się w limitach,
|
||||||
|
- **realized costs** (po fakcie): ile faktycznie kosztowało wejście/wyjście + ile “spaliły” modyfikacje.
|
||||||
|
|
||||||
|
W praktyce koszty dzielą się na 3 kubełki:
|
||||||
|
1) koszt mikrostruktury (spread/slippage/market impact),
|
||||||
|
2) fee maker/taker + funding (opcjonalnie),
|
||||||
|
3) koszt tx na Solanie (network fee + priority) oraz koszt modyfikacji (cancel+place).
|
||||||
|
|
||||||
|
### 1) Koszt wejścia/wyjścia z orderbooka (spread/slippage)
|
||||||
|
|
||||||
|
Źródła danych:
|
||||||
|
- `dlob_stats_latest`: `mid_price`, `spread_bps`, `best_bid_price`, `best_ask_price`
|
||||||
|
- `dlob_slippage_latest`: `impact_bps`, `vwap_price`, `fill_pct` dla `size_usd` i `side`
|
||||||
|
|
||||||
|
Warianty:
|
||||||
|
- **Taker/market (lub agresywny limit krzyżujący)**: używaj `impact_bps` z `dlob_slippage_latest` jako “expected execution cost vs mid”.
|
||||||
|
- **Maker/post-only**: policz 2 scenariusze:
|
||||||
|
- “maker-fill”: koszt vs mid wynikający z `limit_price` (zwykle lepszy niż taker),
|
||||||
|
- “fallback-taker”: jeśli nie fill → koszt = `impact_bps` (jak taker).
|
||||||
|
|
||||||
|
Przykładowa definicja kosztu vs mid (bps), jeśli liczysz z cen:
|
||||||
|
- buy: `cost_bps = (fill_price / mid_at_submit - 1) * 10_000`
|
||||||
|
- sell: `cost_bps = (1 - fill_price / mid_at_submit) * 10_000`
|
||||||
|
|
||||||
|
Uwaga: `mid` się rusza, więc to jest miara “kosztu wykonania względem chwilowego środka rynku” (przydatna do strojenia), a nie pełne PnL.
|
||||||
|
|
||||||
|
### 2) Fee maker/taker i funding (opcjonalnie)
|
||||||
|
|
||||||
|
Na MVP trzymaj fee jako parametry konfiguracyjne:
|
||||||
|
- `fee_maker_bps`
|
||||||
|
- `fee_taker_bps`
|
||||||
|
|
||||||
|
Realized fee licz po fill:
|
||||||
|
- `fee_usd = fill_notional_usd * fee_rate_bps / 10_000`
|
||||||
|
|
||||||
|
Funding dla holdów “minuty” zwykle jest mały; można dodać później.
|
||||||
|
|
||||||
|
### 3) Koszt transakcyjny (Solana fee + priority) i koszt modyfikacji
|
||||||
|
|
||||||
|
Fakty:
|
||||||
|
- “modyfikacja” ordera to zazwyczaj **cancel + place** → zwykle 2 transakcje.
|
||||||
|
- Nie da się “ubić” wysłanej transakcji; można tylko wysłać korektę (cancel/close) kolejną tx.
|
||||||
|
|
||||||
|
Definicje:
|
||||||
|
- `tx_fee_usd` = koszt jednej transakcji w USD (network fee + priority fee).
|
||||||
|
- `tx_fee_bps = (tx_fee_usd / notional_usd) * 10_000`
|
||||||
|
|
||||||
|
Koszt modyfikacji (expected):
|
||||||
|
- `expected_modify_cost_usd ≈ expected_reprices * (tx_fee_cancel_usd + tx_fee_place_usd)`
|
||||||
|
- `expected_modify_cost_bps = (expected_modify_cost_usd / entry_notional_usd) * 10_000`
|
||||||
|
|
||||||
|
Źródło `tx_fee_usd`:
|
||||||
|
- na start: stałe estymaty w `bot_config`,
|
||||||
|
- później: estymata “priority fee” z feedu/endpointu + przelicznik SOL→USD.
|
||||||
|
|
||||||
|
### Expected total cost (proponowana metryka bramkująca)
|
||||||
|
|
||||||
|
Na ticku decyzji licz:
|
||||||
|
- `expected_total_cost_bps = execution_bps + fee_bps + tx_bps + expected_modify_bps (+ funding_bps)`
|
||||||
|
|
||||||
|
I bramkuj wejście/wyjście:
|
||||||
|
- `expected_total_cost_bps <= max_expected_total_cost_bps`
|
||||||
|
|
||||||
|
## Parametry kosztów (do `bot_config`) — propozycja
|
||||||
|
|
||||||
|
Minimalny zestaw:
|
||||||
|
- `fee_maker_bps`, `fee_taker_bps`
|
||||||
|
- `max_expected_total_cost_bps`
|
||||||
|
- `max_expected_modify_bps` (opcjonalnie osobny limit na “cancel+place spam”)
|
||||||
|
- `tx_fee_usd_est` (albo `tx_fee_sol_est` + `sol_price_usd_est`)
|
||||||
|
- `expected_reprices_per_entry`
|
||||||
|
|
||||||
|
To działa zarówno dla strategii regułowych, jak i ML (model może dać `urgency`, ale executor i tak liczy koszty).
|
||||||
|
|
||||||
|
## Logowanie kosztów (do `bot_events`) — propozycja
|
||||||
|
|
||||||
|
Żeby stroić progi i ocenić jakość wejść, zapisuj:
|
||||||
|
|
||||||
|
### Na decyzji (przed wysłaniem orderów)
|
||||||
|
- `mid_at_submit`, `spread_bps_at_submit`
|
||||||
|
- `slippage_quote`: `impact_bps`, `vwap_price`, `fill_pct` (dla danego `size_usd`)
|
||||||
|
- `expected_cost_bps_breakdown`: `{ execution_bps, fee_bps, tx_bps, expected_modify_bps, total_bps }`
|
||||||
|
- `order_intent`: `{ policy, limit_price, size_usd, side }`
|
||||||
|
|
||||||
|
### Po fill / wyjściu
|
||||||
|
- `realized_execution_bps` (vs `mid_at_submit`), osobno entry i exit
|
||||||
|
- `realized_fee_usd`, `realized_tx_usd`
|
||||||
|
- `modify_count`: liczba cancel+place w trakcie wejścia/zarządzania
|
||||||
|
- `time_to_fill_ms`, `hold_time_s`
|
||||||
|
|
||||||
|
To daje realną pętlę feedbacku: “koszt vs jakość sygnału” i “czy chase zjada edge”.
|
||||||
|
|
||||||
|
## Obserwowalność i audyt (DB + logi)
|
||||||
|
|
||||||
|
W DB trzymamy:
|
||||||
|
- Konfigurację (`bot_config`) — desired state i limity.
|
||||||
|
- Stan (`bot_state`) — ostatnia decyzja, last heartbeat, ostatni błąd.
|
||||||
|
- Eventy (`bot_events`) — “decision”, “order_sent”, “order_filled”, “panic_exit”, “error”.
|
||||||
|
|
||||||
|
MVP może zacząć od samego `bot_events` + `bot_config`, a resztę dodać po pierwszym działającym loopie.
|
||||||
|
|
||||||
|
## Interfejsy (kontrakty)
|
||||||
|
|
||||||
|
### VPS ↔ Hasura
|
||||||
|
- Executor czyta: `dlob_*_latest` + `drift_ticks`/candles + własne tabele botów.
|
||||||
|
- Executor pisze: `bot_events`, `bot_state` (oraz ewentualnie `bot_orders`).
|
||||||
|
|
||||||
|
### VPS ↔ Vast
|
||||||
|
- HTTP endpoint (lub gRPC) “predict”.
|
||||||
|
- Kontrakty muszą być wersjonowane (`model_version`) i logowane w `bot_events`.
|
||||||
|
|
||||||
|
## Bezpieczeństwo (sekrety)
|
||||||
|
|
||||||
|
Zasady:
|
||||||
|
- Prywatne klucze i tokeny są tylko w K8s Secret (na VPS) i nigdy nie trafiają do repo.
|
||||||
|
- Vast nie dostaje sekretów.
|
||||||
|
- Logi nie mogą wypisywać URL z api-key ani payloadów z sekretami.
|
||||||
|
|
||||||
|
## Deployment (k3s + GitOps)
|
||||||
|
|
||||||
|
Zasady deployu: snapshoty, brak ręcznych zmian na VPS (`doc/workflow.md`).
|
||||||
|
|
||||||
|
Docelowe komponenty w `trade-deploy` (do dopięcia):
|
||||||
|
- `Deployment/bot-executor` (albo `Deployment/bot-<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
344
data-ingest-strategy.md
Normal 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
184
dlob-basics.md
Normal 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) Top‑N leveli (np. L1…L10) — `dlob_*_derived_latest`
|
||||||
|
Zapisywane w writerach warstwy pochodnej na podstawie live snapshotów orderbooka:
|
||||||
|
|
||||||
|
- Bierzemy znormalizowane top level’e z `bids_norm` i `asks_norm`.
|
||||||
|
- Każdy level ma:
|
||||||
|
- `price`
|
||||||
|
- `size_base`
|
||||||
|
- “kasa” (notional) na tym levelu: `size_usd = size_base * price`
|
||||||
|
- Sumujemy po levelach:
|
||||||
|
- `depth_bid_base = Σ size_base` (po stronie bid),
|
||||||
|
- `depth_bid_usd = Σ (size_base * price)` (po stronie bid),
|
||||||
|
- analogicznie `depth_ask_base`, `depth_ask_usd` (po stronie ask).
|
||||||
|
|
||||||
|
To odpowiada intuicji “ile jest płynności na L1…LN”.
|
||||||
|
|
||||||
|
### B) Okno cenowe w bps od mid
|
||||||
|
W aktualnej ścieżce aplikacji to liczenie jest robione z danych L2/derived, a nie przez osobny backend worker:
|
||||||
|
|
||||||
|
- Dla pasma `band_bps` wyznaczamy:
|
||||||
|
- `minBidPrice = mid * (1 - band_bps/10_000)`
|
||||||
|
- `maxAskPrice = mid * (1 + band_bps/10_000)`
|
||||||
|
- Sumujemy wszystkie levele, które mieszczą się w tym oknie:
|
||||||
|
- bids: `price >= minBidPrice`
|
||||||
|
- asks: `price <= maxAskPrice`
|
||||||
|
- Liczymy sumy:
|
||||||
|
- `bid_base`, `bid_usd`, `ask_base`, `ask_usd` tak jak wyżej (`usd = base * price`).
|
||||||
|
|
||||||
|
To odpowiada intuicji “ile płynności jest *blisko* ceny w ±X bps”.
|
||||||
|
|
||||||
|
### Ważne doprecyzowanie
|
||||||
|
|
||||||
|
Te liczby to **notional z orderbooka** (ile “stoi” na poziomach cenowych).
|
||||||
|
Nie są to “pieniądze w kontrakcie”, tylko przybliżenie kosztu/pojemności wykonania przy danej cenie i bez przesunięcia rynku.
|
||||||
|
|
||||||
|
## Spec: Orderbook UI (L1…L10 + “liquidity bars”)
|
||||||
|
|
||||||
|
Wizualizacja orderbooka (jak na screenach) jest oparta o L2 i pokazuje tylko top‑N leveli:
|
||||||
|
- `N` = liczba leveli wyświetlanych na stronę (np. 10 → “L1…L10”).
|
||||||
|
|
||||||
|
### Kolumny / wartości
|
||||||
|
|
||||||
|
Na każdym levelu liczymy:
|
||||||
|
- `size_usd = size_base * price`
|
||||||
|
|
||||||
|
W UI pokazujemy:
|
||||||
|
- `Size (USD)` = `size_usd` dla danego poziomu,
|
||||||
|
- `Total (USD)` = suma skumulowana od best‑price “w głąb” (cumulative):
|
||||||
|
- bids: kumulacja od best bid w dół,
|
||||||
|
- asks: kumulacja od best ask w górę (w UI zwykle best ask jest bliżej środka).
|
||||||
|
|
||||||
|
### “Liquidity bars” (znormalizowane słupki tła)
|
||||||
|
|
||||||
|
Żeby “na oko” widzieć gdzie stoi płynność:
|
||||||
|
|
||||||
|
1) **Level bar (per‑poziom)** — normalizacja do największego `size_usd` w widocznych levelach danej strony:
|
||||||
|
- `level_scale = size_usd / max(size_usd w widoku)`
|
||||||
|
2) **Total bar (cumulative)** — normalizacja do największego `total_usd` w widocznych levelach danej strony:
|
||||||
|
- `total_scale = total_usd / max(total_usd w widoku)`
|
||||||
|
|
||||||
|
Żeby duże “ściany” nie zabijały kontrastu, warto użyć krzywej:
|
||||||
|
- `scale_curved = sqrt(clamp01(scale))`
|
||||||
|
|
||||||
|
Interpretacja:
|
||||||
|
- **level bar** = “ile stoi na tym poziomie”,
|
||||||
|
- **total bar** = “ile stoi łącznie do tego poziomu”.
|
||||||
236
dlob-retention-feature-store-plan.md
Normal file
236
dlob-retention-feature-store-plan.md
Normal 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
243
dlob-services.md
Normal 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 (top‑N) z każdej strony braliśmy do liczenia “depth”.
|
||||||
|
- `depth_bid_base` / `depth_ask_base`: suma `size` po top‑N levelach bid/ask (w base).
|
||||||
|
- `depth_bid_usd` / `depth_ask_usd`: suma `size_base * price` po top‑N levelach (w USD).
|
||||||
|
- `imbalance` ([-1..1]): miara asymetrii płynności:
|
||||||
|
- `(depth_bid_usd - depth_ask_usd) / (depth_bid_usd + depth_ask_usd)`
|
||||||
|
- >0 = relatywnie więcej płynności po bid, <0 = po ask.
|
||||||
|
- `oracle_price`: cena z oracla (np. Pyth) jako punkt odniesienia.
|
||||||
|
- `mark_price`: “mark” z rynku/perp (cena referencyjna dla rozliczeń); różni się od oracle/top-of-book.
|
||||||
|
|
||||||
|
### Metryki “depth bands” (np. `dlob_depth_bps_latest`)
|
||||||
|
|
||||||
|
- `band_bps`: szerokość pasma wokół `mid_price` (np. 5/10/20/50/100/200 bps).
|
||||||
|
- `bid_usd` / `ask_usd`: płynność po danej stronie, ale **tylko z poziomów mieszczących się w oknie ±`band_bps`** wokół mid.
|
||||||
|
- `imbalance`: jak wyżej, ale liczony per band.
|
||||||
|
|
||||||
|
### Metryki “slippage” (np. `dlob_slippage_latest`)
|
||||||
|
|
||||||
|
To jest symulacja “gdybym teraz zrobił market order o rozmiarze X” na podstawie L2.
|
||||||
|
|
||||||
|
- `size_usd`: docelowy rozmiar zlecenia w USD.
|
||||||
|
- `vwap_price`: średnia cena realizacji (Volume Weighted Average Price) dla symulowanego fill.
|
||||||
|
- `impact_bps`: koszt/odchylenie względem `mid_price` wyrażone w bps (zwykle na bazie `vwap` vs `mid`).
|
||||||
|
- `worst_price`: najgorsza cena dotknięta podczas “zjadania” kolejnych leveli.
|
||||||
|
- `filled_usd` / `filled_base`: ile realnie udało się wypełnić (może być < docelowego, jeśli brakuje płynności).
|
||||||
|
- `fill_pct`: procent wypełnienia (100% = pełny fill).
|
||||||
|
- `levels_consumed`: ile leveli zostało “zjedzonych” podczas fill.
|
||||||
|
|
||||||
|
### Metadane czasu (“świeżość”)
|
||||||
|
|
||||||
|
- `ts`: timestamp źródła (czas snapshotu).
|
||||||
|
- `slot`: slot Solany, z którego pochodzi snapshot (monotoniczny “numer czasu” chaina).
|
||||||
|
- `updated_at`: kiedy nasz pipeline zapisał/odświeżył rekord w DB (do oceny, czy dane są świeże).
|
||||||
|
|
||||||
|
## Redis retention na poziomie k3s i Redisa
|
||||||
|
|
||||||
|
Dla `mevnode-bot` retencja Redis jest ustawiana na poziomie `k3s` przez cleaner `CronJob`, a nie przez natywny `TTL` kluczy.
|
||||||
|
|
||||||
|
Powod:
|
||||||
|
- publishery DLOB zapisują do Redis w modelu `latest-only`
|
||||||
|
- kolejne `SET` nadpisuje poprzedni snapshot tego samego klucza
|
||||||
|
- zwykly Redis `TTL` bylby kasowany przez kolejne zapisy, wiec nie daje poprawnej retencji semantycznej
|
||||||
|
|
||||||
|
Ustawienie operacyjne:
|
||||||
|
- `hot` (`dlob-hot:`): retencja `15m`
|
||||||
|
- `all` (`dlob-all:`): retencja `1h`
|
||||||
|
- cleaner uruchamiany z `CronJob` co `5m`
|
||||||
|
|
||||||
|
Sposob dzialania cleanera:
|
||||||
|
- skanuje klucze `dlob-hot:*` oraz `dlob-all:*`
|
||||||
|
- odczytuje `ts`, a jesli go nie ma to `updatedAtTs`, z payloadu JSON
|
||||||
|
- usuwa tylko klucze starsze niz zadany prog wieku
|
||||||
|
|
||||||
|
To oznacza:
|
||||||
|
- aktywne markety pozostaja stale obecne w Redisie
|
||||||
|
- osierocone klucze po zmianie konfiguracji znikaja automatycznie
|
||||||
|
- Redis pozostaje warstwa `raw latest snapshot`, bez normalizacji payloadu
|
||||||
|
|
||||||
|
Aktualny stan Redis, istotny dla tej decyzji:
|
||||||
|
- klucze nie maja automatycznej expiracji (`TTL=-1`)
|
||||||
|
- `maxmemory-policy=noeviction`
|
||||||
|
- `aof_enabled=0`
|
||||||
|
|
||||||
|
Aktualny stan publisherow na poziomie k3s:
|
||||||
|
- `hot` i `all` sa obecnie rozrozniane przez markety, `REDIS_CLIENT`, `DLOB_SOURCE` i persistent store
|
||||||
|
- maja osobne `priorityClassName`
|
||||||
|
- `resources.requests/limits` sa ustawione dla writerow `hot`; publishery maja rozroznienie priorytetow na poziomie klastra
|
||||||
|
|
||||||
|
## Priorytety na poziomie k3s
|
||||||
|
|
||||||
|
Na poziomie `k3s` rozroznienie `hot` i `all` jest ustawione przez osobne `PriorityClass`.
|
||||||
|
|
||||||
|
Parametry:
|
||||||
|
- `hot`:
|
||||||
|
- deployment: `dlob-publisher-hot`
|
||||||
|
- `priorityClassName: dlob-hot`
|
||||||
|
- wartosc `PriorityClass`: `100000`
|
||||||
|
- `all`:
|
||||||
|
- deployment: `dlob-publisher-all`
|
||||||
|
- `priorityClassName: dlob-all`
|
||||||
|
- wartosc `PriorityClass`: `50000`
|
||||||
|
|
||||||
|
Znaczenie operacyjne:
|
||||||
|
- `hot` ma wyzszy priorytet schedulera i preemption niz `all`
|
||||||
|
- przy presji na node `hot` powinien byc traktowany jako sciezka krytyczna
|
||||||
|
- `all` pozostaje feedem nizszego priorytetu dla pelnego coverage rynku
|
||||||
|
|
||||||
|
Zakres tej parametryzacji:
|
||||||
|
- to jest tylko priorytet na poziomie schedulera `k3s`
|
||||||
|
- nie ustawia jeszcze `resources.requests` ani `resources.limits`
|
||||||
|
- nie zmienia logiki publishera, tylko kolejnosc traktowania workloadu przez klaster
|
||||||
54
drift-perps-monitoring.md
Normal file
54
drift-perps-monitoring.md
Normal 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
533
economic-markers-k3s.md
Normal 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
205
frontent-local-run.md
Normal 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
275
gitea-repo-cleanup-plan.md
Normal 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
143
gitea-tree.md
Normal 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
245
mevnode-bot-architecture.md
Normal 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`
|
||||||
1082
mevnode-bot-topology-interactive.html
Normal file
1082
mevnode-bot-topology-interactive.html
Normal file
File diff suppressed because it is too large
Load Diff
424
mevnode-bot-topology.html
Normal file
424
mevnode-bot-topology.html
Normal 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>
|
||||||
68
migration.md
68
migration.md
@@ -253,6 +253,23 @@ Nie commituj sekretów do gita. W GitOps wybierz jedną drogę:
|
|||||||
16) Workflow CI: build+push obrazów (i ewentualny commit do `trade-deploy`).
|
16) Workflow CI: build+push obrazów (i ewentualny commit do `trade-deploy`).
|
||||||
17) Zasady promocji: staging → prod (np. tylko tag/release).
|
17) Zasady promocji: staging → prod (np. tylko tag/release).
|
||||||
|
|
||||||
|
### Etap 4b: Workflow zmian (dev → staging) + snapshoty/rollback
|
||||||
|
|
||||||
|
Rekomendacja: nie robimy “ręcznych” zmian na VPS (żeby nie tworzyć snowflake’a). Każdy deploy ma być **snapshoot’em**, do którego można wrócić: *git commit w `trade-deploy` + pin do obrazu* (`sha-<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 snapshoot’a.
|
||||||
|
|
||||||
|
Rollback (bezpieczny dla “dużych” zmian, np. ingest/schema):
|
||||||
|
- użyj wersjonowania vN (osobna tabela/funkcja/porty) + cutover ingestora; jeśli zmiana nie siądzie, robisz cut back vN → v1 (dane w starej tabeli zostają).
|
||||||
|
|
||||||
## 9) Wersjonowanie v1/v2… (równoległe wdrożenia + cutover)
|
## 9) Wersjonowanie v1/v2… (równoległe wdrożenia + cutover)
|
||||||
|
|
||||||
Repo ma już pattern wersjonowania w Compose (`scripts/ops/*` + `devops/versions/vN.env`).
|
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`).
|
- 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).
|
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
156
momentum-service-v1.md
Normal 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
126
observer-right-rail-v1.md
Normal 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
240
observer-sol-v1.md
Normal 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
192
observer-visualizer-v1.md
Normal 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
168
rpc/README.md
Normal 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
71
rpc/baremetal-iac.md
Normal 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
136
rpc/baremetal-monitoring.md
Normal 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 > 1–2 min (jeśli liczysz lag)
|
||||||
|
- `node_filesystem_avail_bytes` < 10–15% (ledger/accounts szybko zjadają dysk)
|
||||||
|
- wysokie `node_cpu_seconds_total{mode="iowait"}` (NVMe bottleneck)
|
||||||
121
rpc/bot-executor-api.md
Normal file
121
rpc/bot-executor-api.md
Normal 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ł” node’a)
|
||||||
|
|
||||||
|
- `Service/bot-executor` (ClusterIP)
|
||||||
|
- brak Ingress (domyślnie)
|
||||||
|
|
||||||
|
- Monitoring:
|
||||||
|
- `Prometheus` scrape `/metrics` (jeśli dodasz)
|
||||||
|
|
||||||
|
## Co z “wysłaniem kontraktu” na Drift?
|
||||||
|
|
||||||
|
- `bot-executor` robi to przez Drift SDK → Solana RPC (`sendTransaction`).
|
||||||
|
- Control plane **nie** wysyła tx; tylko ustawia desired state.
|
||||||
|
|
||||||
79
rpc/geyser-dlob.md
Normal file
79
rpc/geyser-dlob.md
Normal 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
89
rpc/software-placement.md
Normal 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 plane’y: `doc/bots.md`
|
||||||
|
|
||||||
|
## Zasady (priorytety)
|
||||||
|
|
||||||
|
1) **RPC box jest “lean”** — żadnych baz danych, Grafany, workerów, botów.
|
||||||
|
2) **Klucze tylko w execution plane** — żadnych kluczy na Vast i w UI.
|
||||||
|
3) **Publicznie tylko read/control plane** — bez endpointu “podpisz i wyślij”.
|
||||||
|
4) **Ogranicz rynki** na start (np. `PERP_MARKETS_TO_LOAD=0,75`), żeby odchudzić cały pipeline.
|
||||||
|
|
||||||
|
## Co gdzie uruchamiamy
|
||||||
|
|
||||||
|
### Bare metal (Solana RPC box)
|
||||||
|
|
||||||
|
Uruchamiaj:
|
||||||
|
|
||||||
|
- `agave/solana-validator` jako prywatny RPC (non-voting).
|
||||||
|
- (Opcjonalnie) `geyser` / `yellowstone-grpc` plugin, jeśli chcesz feed “faster-than-RPC”.
|
||||||
|
- WireGuard + firewall + chrony.
|
||||||
|
- Monitoring: `node_exporter` + probe zdrowia RPC.
|
||||||
|
|
||||||
|
Nie uruchamiaj:
|
||||||
|
|
||||||
|
- DLOB, Redis, Hasura/Postgres, Grafana, botów.
|
||||||
|
|
||||||
|
Powód: RAM 192 GB jest cenny dla noda; trzymamy minimalną liczbę procesów i klientów.
|
||||||
|
|
||||||
|
### VPS (k3s / “app + trading box”)
|
||||||
|
|
||||||
|
Uruchamiaj:
|
||||||
|
|
||||||
|
- **Data plane**:
|
||||||
|
- `dlob-publisher-hot` i `dlob-publisher-all` (czytają z prywatnego RPC/WS lub gRPC),
|
||||||
|
- `dlob-redis`,
|
||||||
|
- `dlob-hot-redis-to-postgres-raw-writer`,
|
||||||
|
- `dlob-hot-postgres-to-postgres-derived-writer`,
|
||||||
|
- `dlob-all-redis-to-postgres-derived-writer`,
|
||||||
|
- Postgres + Hasura (GraphQL/WS dla “latest” tabel i sterowania botami).
|
||||||
|
- **Execution plane**:
|
||||||
|
- `bot-executor` (klucze, podpisywanie transakcji, risk, kill switch).
|
||||||
|
- **Control plane / UI**:
|
||||||
|
- `trade-frontend` (proxy/auth + UI),
|
||||||
|
- `trade-api` (narzędzia: tokeny, admin, pomocnicze endpointy).
|
||||||
|
- Monitoring:
|
||||||
|
- Prometheus/Grafana/Alertmanager.
|
||||||
|
|
||||||
|
Wystawianie na zewnątrz (rekomendacja):
|
||||||
|
|
||||||
|
- Publicznie: UI + GraphQL (Hasura) + ewentualnie read-only API.
|
||||||
|
- Control plane: endpointy do ustawiania `bot_config` i odczytu `bot_state/bot_events` (z auth).
|
||||||
|
- **Nie publicznie**: `bot-executor`, prywatny RPC, Redis.
|
||||||
|
|
||||||
|
### Vast (GPU / “model plane”)
|
||||||
|
|
||||||
|
Uruchamiaj:
|
||||||
|
|
||||||
|
- Trening transformera na danych wyeksportowanych z VPS (batch).
|
||||||
|
- Ewentualnie inference endpoint (jeśli chcesz oddzielić obciążenie od VPS).
|
||||||
|
|
||||||
|
Nie uruchamiaj / nie przechowuj:
|
||||||
|
|
||||||
|
- kluczy prywatnych,
|
||||||
|
- tokenów do środków,
|
||||||
|
- dostępu do prywatnego RPC po VPN (nie jest potrzebny do treningu).
|
||||||
|
|
||||||
|
## Kontrakt VPS ↔ Vast (bezpieczny)
|
||||||
|
|
||||||
|
- VPS wysyła: “feature snapshots” / dataset (bez sekretów).
|
||||||
|
- Vast zwraca: “decision” (np. target exposure/side/confidence).
|
||||||
|
- VPS decyduje o wykonaniu (risk engine) i podpisuje transakcję (executor).
|
||||||
|
|
||||||
|
## Minimalny start (2 markety)
|
||||||
|
|
||||||
|
1) DLOB:
|
||||||
|
- `PERP_MARKETS_TO_LOAD=0,75` (i analogicznie dla TOB monitoring, jeśli używasz).
|
||||||
|
2) Execution:
|
||||||
|
- `bot-executor` czyta config z DB, nie z requestów “na żywo”.
|
||||||
|
3) Public API:
|
||||||
|
- brak “sign & send”; tylko control-plane + read-only dane.
|
||||||
232
rpc/topol.html
Normal file
232
rpc/topol.html
Normal 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 you’ll 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 it’s 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 RPC’s 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
158
stats.md
Normal 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 top‑N leveli orderbooka per market.
|
||||||
|
|
||||||
|
- `bids` / `asks`: tablice poziomów `{ price, size }` (wartości zwykle w “skalowanych intach” wg `PRICE_PRECISION` i `BASE_PRECISION`).
|
||||||
|
- `best_bid_price` / `best_ask_price`, `mark_price`, `oracle_price`: w praktyce do szybkiego odczytu top‑of‑book (ale najdokładniej liczyć z `bids/asks`).
|
||||||
|
- `ts`, `slot`: czas/slot źródłowego snapshota z DLOB.
|
||||||
|
- `updated_at`: kiedy worker zapisał snapshot do DB (do oceny “świeżości”).
|
||||||
|
|
||||||
|
Z tego źródła robimy: orderbook UI (paski/heat), mikro‑ceny, symulacje fill.
|
||||||
|
|
||||||
|
### `dlob_stats_latest` (agregat z L2 pod UI)
|
||||||
|
|
||||||
|
Pochodne metryki liczone na podstawie `dlob_l2_latest` (lub równoważnego L2), trzymane jako “latest” per market.
|
||||||
|
|
||||||
|
Metryki:
|
||||||
|
|
||||||
|
- `best_bid_price`, `best_ask_price` (USD): najlepszy bid/ask.
|
||||||
|
- `mid_price` (USD): `(best_bid_price + best_ask_price) / 2`.
|
||||||
|
- `spread_abs` (USD): `best_ask_price - best_bid_price`.
|
||||||
|
- `spread_bps` (bps): `(spread_abs / mid_price) * 10_000` (1 bps = 0.01%).
|
||||||
|
- `depth_levels` (liczba): ile leveli z każdej strony weszło do “depth”.
|
||||||
|
- `depth_bid_base`, `depth_ask_base` (base asset): suma size (w jednostkach bazowych) po top‑N levelach.
|
||||||
|
- `depth_bid_usd`, `depth_ask_usd` (USD): suma `size_base * price` po top‑N levelach.
|
||||||
|
- `imbalance` ([-1..1]): `(depth_bid_usd - depth_ask_usd) / (depth_bid_usd + depth_ask_usd)`; >0 = relatywnie większa płynność po bid.
|
||||||
|
- `mark_price`, `oracle_price` (USD): ceny referencyjne (mark i oracle).
|
||||||
|
- `ts`, `slot`, `updated_at`: metadane czasu/świeżości.
|
||||||
|
|
||||||
|
To jest najszybsze źródło do overlay na wykresie i do KPI w headerze.
|
||||||
|
|
||||||
|
### `dlob_depth_bps_latest` (płynność w pasmach wokół mid)
|
||||||
|
|
||||||
|
Metryki głębokości, ale nie “top‑N leveli” tylko “okno odległości od ceny” w bps.
|
||||||
|
|
||||||
|
Klucz:
|
||||||
|
|
||||||
|
- `(market_name, band_bps)` np. 5/10/20/50/100/200 bps.
|
||||||
|
|
||||||
|
Interpretacja:
|
||||||
|
|
||||||
|
- Dla danego `band_bps` sumujemy płynność tylko z poziomów, które mieszczą się w oknie ±`band_bps` wokół `mid_price`.
|
||||||
|
|
||||||
|
Metryki:
|
||||||
|
|
||||||
|
- `bid_base`, `ask_base` (base asset): suma size w oknie.
|
||||||
|
- `bid_usd`, `ask_usd` (USD): suma `size_base * price` w oknie.
|
||||||
|
- `imbalance` ([-1..1]): jak wyżej, ale per band.
|
||||||
|
- `mid_price`, `best_bid_price`, `best_ask_price` (USD): do kontekstu wyliczeń.
|
||||||
|
- `ts`, `slot`, `updated_at`, `raw`: metadane/diagnostyka.
|
||||||
|
|
||||||
|
To jest najlepsze źródło do wykresów “jak gruby jest orderbook blisko ceny”.
|
||||||
|
|
||||||
|
### `dlob_slippage_latest` (symulacja slippage vs rozmiar)
|
||||||
|
|
||||||
|
Symulacja wykonania zlecenia rynkowego po L2.
|
||||||
|
|
||||||
|
Klucz:
|
||||||
|
|
||||||
|
- `(market_name, side, size_usd)` gdzie `side ∈ {buy,sell}` a `size_usd` to predefiniowane progi (np. 100/500/1000/…).
|
||||||
|
|
||||||
|
Metryki:
|
||||||
|
|
||||||
|
- `impact_bps` (bps): wpływ wykonania vs `mid_price` (zwykle `vwap` względem mid).
|
||||||
|
- `vwap_price` (USD): średnia cena wykonania.
|
||||||
|
- `worst_price` (USD): najgorszy poziom dotknięty podczas fill.
|
||||||
|
- `filled_usd`, `filled_base`: ile realnie weszło w fill (gdy brak płynności, może być < docelowego).
|
||||||
|
- `fill_pct` (%): 100% = pełny fill.
|
||||||
|
- `levels_consumed`: ile leveli zostało “zjedzone”.
|
||||||
|
- `mid_price`, `ts`, `slot`, `updated_at`, `raw`: metadane/diagnostyka.
|
||||||
|
|
||||||
|
To jest idealne do “Dynamic Slippage” w formularzu i do wykresu slippage‑vs‑size.
|
||||||
|
|
||||||
|
## Jak nanieść na wizualizację (warstwy/panele)
|
||||||
|
|
||||||
|
Poniżej propozycja warstw, pogrupowanych tak, żeby serie w jednej warstwie były w tej samej “domenie” (jednostki i semantyka).
|
||||||
|
|
||||||
|
### Warstwa 1: Cena / Quotes (oś Y = USD, overlay na głównym wykresie)
|
||||||
|
|
||||||
|
Źródło: `dlob_stats_latest`.
|
||||||
|
|
||||||
|
- Linie: `mark_price` i `oracle_price` (referencje).
|
||||||
|
- Linie: `best_bid_price` i `best_ask_price` (top‑of‑book).
|
||||||
|
- Opcjonalnie: `mid_price` jako linia przerywana.
|
||||||
|
- Opcjonalnie: “spread band” (wypełnienie między bid i ask).
|
||||||
|
|
||||||
|
Efekt: w jednym miejscu widać gdzie jest rynek + jak szeroki jest spread.
|
||||||
|
|
||||||
|
### Warstwa 2: Spread (panel pod wykresem, oś Y = bps)
|
||||||
|
|
||||||
|
Źródło: `dlob_stats_latest`.
|
||||||
|
|
||||||
|
- Linia: `spread_bps`.
|
||||||
|
- Tooltip/secondary: `spread_abs` (USD) jako dodatkowa informacja (zwykle bez drugiej osi, żeby nie mieszać skali).
|
||||||
|
|
||||||
|
Efekt: szybkie “koszt wejścia/wyjścia” i jego zmienność.
|
||||||
|
|
||||||
|
### Warstwa 3: Płynność top‑N + imbalance (panel, oś Y = USD + opcjonalnie linia imbalance)
|
||||||
|
|
||||||
|
Źródło: `dlob_stats_latest`.
|
||||||
|
|
||||||
|
- Area: `depth_bid_usd` i `depth_ask_usd` (dwie serie, zielona/czerwona).
|
||||||
|
- Opcjonalnie: linia `imbalance` (druga oś, zakres [-1..1]) albo jako wskaźnik liczbowy.
|
||||||
|
|
||||||
|
Efekt: ile jest płynności “najbliżej” top‑of‑book (w definicji top‑N leveli).
|
||||||
|
|
||||||
|
### Warstwa 4: Płynność jako funkcja odległości (bps bands) (panel)
|
||||||
|
|
||||||
|
Źródło: `dlob_depth_bps_latest`.
|
||||||
|
|
||||||
|
Dwa czytelne warianty prezentacji:
|
||||||
|
|
||||||
|
1) Fan chart / multi‑line:
|
||||||
|
- Linie `bid_usd(band_bps)` i `ask_usd(band_bps)` dla kilku bandów (np. 10/50/200).
|
||||||
|
|
||||||
|
2) Stacked:
|
||||||
|
- Słupki/area pokazujące “ile dodaje kolejne pasmo” (np. 0–10, 10–20, 20–50 bps), osobno dla bid i ask.
|
||||||
|
|
||||||
|
Efekt: “jak szybko rośnie płynność, gdy odchodzę od mid”.
|
||||||
|
|
||||||
|
### Warstwa 5: Slippage vs size (osobny panel XY, nie timeline)
|
||||||
|
|
||||||
|
Źródło: `dlob_slippage_latest`.
|
||||||
|
|
||||||
|
- Oś X: `size_usd`.
|
||||||
|
- Oś Y: `impact_bps`.
|
||||||
|
- Dwie krzywe: `buy` i `sell`.
|
||||||
|
- Marker: aktualny “Order Value” z formularza (punkt na krzywej).
|
||||||
|
|
||||||
|
Efekt: bardzo czytelna krzywa kosztu wykonania względem rozmiaru.
|
||||||
|
|
||||||
|
### Warstwa 6: Heat / “paski” orderbooka (widok orderbook albo overlay z ograniczeniem)
|
||||||
|
|
||||||
|
Źródło: `dlob_l2_latest`.
|
||||||
|
|
||||||
|
- Paski (zielone/czerwone) per poziom ceny, intensywność ∝ `size` (jak na Drift UI).
|
||||||
|
- To najlepiej działa jako:
|
||||||
|
- osobny panel “Orderbook” (snapshot), albo
|
||||||
|
- “edge overlay” przy prawej krawędzi wykresu (bez historii).
|
||||||
|
|
||||||
|
Efekt: “gdzie stoją ściany” i jak się zmieniają.
|
||||||
|
|
||||||
|
## Uwaga o historii (“latest” vs wykres w czasie)
|
||||||
|
|
||||||
|
Tabele `*_latest` są świetne do live UI i subscriptions, ale **nie przechowują historii** do rysowania timeline (np. spread przez ostatnie 24h).
|
||||||
|
|
||||||
|
Jeśli chcemy historię:
|
||||||
|
|
||||||
|
- opcja A: dodać osobne tabele time‑series (np. `dlob_stats_ts`, `dlob_depth_bps_ts`, `dlob_slippage_ts`) i zasilać je workerem,
|
||||||
|
- opcja B: rozszerzyć ingest ticków (`drift_ticks`) o dodatkowe pola/nową tabelę eventów dla metryk orderbooka.
|
||||||
|
|
||||||
|
Wtedy warstwy 2–5 mogą być prawdziwymi wykresami “w czasie”, a nie tylko bieżącym odczytem.
|
||||||
|
|
||||||
85
steps.md
85
steps.md
@@ -215,76 +215,17 @@ Uwaga: **nie zapisuję sekretów** (hasła, tokeny, prywatne klucze) – jeśli
|
|||||||
- Plan refaktoru opisany w `doc/migration.md` (sekcja „Metoda superproject”).
|
- 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).
|
- 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)
|
## 2026-01-10
|
||||||
- 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.
|
|
||||||
|
|
||||||
### Dostęp: admin vs użytkownicy (plan)
|
### V2: GraphQL + WS (Hasura) + DLOB stats (staging)
|
||||||
- Wymaganie: admin ma dostęp do wszystkich serwisów; pozostali użytkownicy logują się tylko do `trade.rv32i.pl`.
|
- `trade/trade-deploy`:
|
||||||
- Decyzja: logowanie wdrażamy tylko dla `trade`; pozostałe serwisy zostają z własnymi loginami.
|
- Podbito obraz frontendu do `gitea.mpabi.pl/trade/trade-frontend:sha-f85e6da` (UI proxy’uje Hasurę pod `/graphql` + WS pod `/graphql-ws`).
|
||||||
- Wybrano metodę dla `trade`: Traefik `basicAuth` na Ingress (Middleware).
|
- W Hasurze włączono `HASURA_GRAPHQL_UNAUTHORIZED_ROLE=public` (UI bez tokena; bootstrap nadaje ograniczone `select`).
|
||||||
- Plan wdrożenia opisany w `migration.md` (sekcja „Dostęp i logowanie”).
|
- 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`).
|
||||||
### Logowanie do `trade` (Traefik basicAuth) – wdrożenie
|
- `kustomize/base/hasura/job-bootstrap.yaml` działa jako Argo hook (re-run na sync) i trackuje tabele/permissions DLOB.
|
||||||
- Utworzono `Secret/trade-basic-auth` w `trade-staging` (format `htpasswd` w kluczu `users`) z userami: `admin`, `mpabi`, `mkost33` (bez logowania haseł).
|
- 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.
|
||||||
- Dodano `Middleware/trade-basic-auth` (Traefik CRD) i podpięto do `Ingress/trade-frontend` adnotacją `traefik.ingress.kubernetes.io/router.middlewares`.
|
- `apps/visualizer` (frontend na laptopie, dev mode):
|
||||||
- Zaktualizowano `trade-frontend`:
|
- `apps/visualizer/__start` odpala Vite z proxy do `https://trade.mpabi.pl` + ustawia `VITE_HASURA_WS_URL=/graphql-ws`.
|
||||||
- dodano `BASIC_AUTH_MODE=off` (wyłącza wbudowany basic auth w aplikacji),
|
- `apps/visualizer/src/lib/graphqlWs.ts` wspiera względny `VITE_HASURA_WS_URL` (np. `/graphql-ws`) i normalizuje go do pełnego `ws(s)://...`.
|
||||||
- wdrożono nowy obraz `rv32i.pl/trade/trade-frontend:sha-8217bae`.
|
- `apps/visualizer/src/lib/hasura.ts` domyślnie używa `/graphql` (zgodnie z flow: dev UI → staging przez proxy).
|
||||||
- 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).
|
|
||||||
|
|||||||
37
todo.md
Normal file
37
todo.md
Normal 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
692
trade-system-flow.html
Normal 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
42
visualizer-candles.md
Normal 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` — mikro‑ruchy (dużo szumu, ale świetne do obserwacji mikrostruktury)
|
||||||
|
- `1m`, `5m`, `15m`, `1h`, `4h`, `1d` — klasyczne interwały
|
||||||
|
|
||||||
|
Kiedy ma to sens:
|
||||||
|
- `3s/5s`: gdy chcesz widzieć “jak cena się buduje” w krótkich falach (np. po newsie / w dużej zmienności).
|
||||||
|
- `15s/30s`: często najlepszy kompromis między szumem a czytelnością, jeżeli patrzysz na very-short-term.
|
||||||
|
|
||||||
|
## Co pokazuje “brick stack” na dole
|
||||||
|
|
||||||
|
Pod każdą świecą rysujemy słupek złożony z “bricków” (małych segmentów) odpowiadających kolejnym krokom czasu wewnątrz świecy.
|
||||||
|
|
||||||
|
Kolory bricków:
|
||||||
|
- zielony = w tym kroku cena poszła w górę
|
||||||
|
- czerwony = w tym kroku cena poszła w dół
|
||||||
|
- niebieski = w tym kroku cena była stała (flat)
|
||||||
|
|
||||||
|
Wysokość bricków:
|
||||||
|
- zielony/czerwony: proporcjonalna do `|Δprice|` w danym kroku
|
||||||
|
- niebieski: stała (unit height)
|
||||||
|
|
||||||
|
Bricki są rozdzielone cienką czarną linią (1px), żeby było widać strukturę “krok po kroku”.
|
||||||
|
|
||||||
|
## Jakie pola musi zwracać API
|
||||||
|
|
||||||
|
Endpoint `GET /v1/chart` zwraca w każdej świecy:
|
||||||
|
|
||||||
|
- `flow`: udziały czasu `up/down/flat` w całym buckecie (0..1)
|
||||||
|
- `flowRows`: tablica kierunków per krok czasu: `-1` (down), `0` (flat), `1` (up)
|
||||||
|
- `flowMoves`: tablica “move magnitude” per krok czasu (wartości dodatnie; 0 jeśli flat)
|
||||||
|
|
||||||
|
To właśnie `flowRows` + `flowMoves` są używane do narysowania brick stacka.
|
||||||
|
|
||||||
|
## Domyślny rynek
|
||||||
|
|
||||||
|
W visualizerze domyślnie ustawiony jest `SOL-PERP`.
|
||||||
|
|
||||||
99
workflow.md
Normal file
99
workflow.md
Normal 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 **snapshoot’em**, do którego można wrócić.
|
||||||
|
|
||||||
|
## Dla agenta / po restarcie sesji
|
||||||
|
|
||||||
|
1) Przeczytaj ten plik: `doc/workflow.md`.
|
||||||
|
2) Kontekst funkcjonalny: `README.md`, `info.md`.
|
||||||
|
3) Kontekst stacka: `doc/workflow-api-ingest.md` oraz `devops/*/README.md`.
|
||||||
|
4) Stan VPS/k3s + GitOps: `doc/migration.md` i log zmian: `doc/steps.md`.
|
||||||
|
|
||||||
|
## Zasady (must-have)
|
||||||
|
|
||||||
|
- **Nie edytujemy “na żywo” VPS** (żadnych ręcznych poprawek w kontenerach/plikach na serwerze) → tylko Git + CI + Argo.
|
||||||
|
- **Sekrety nie trafiają do gita**: `tokens/*.json` są gitignored; w dokumentacji/logach redagujemy hasła/tokeny.
|
||||||
|
- **Brak `latest`**: obrazy w deployu są przypięte do `sha-<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 proxy’uje `/api/*` do VPS.
|
||||||
|
- UI ma też GraphQL (Hasura) pod `/graphql` (HTTP + WS subscriptions) – w dev proxy’ujemy `/graphql` i `/graphql-ws` do VPS, żeby subscriptions działały na `http://localhost:5173`.
|
||||||
|
- Auth w staging jest w trybie `session` (`/auth/login`, cookie `trade_session`), więc w dev proxy’ujemy też `/whoami`, `/auth/*`, `/logout`.
|
||||||
|
- Dev proxy usuwa `Secure` z `Set-Cookie`, żeby cookie działało na `http://localhost:5173`.
|
||||||
|
- Na VPS `trade-frontend` proxy’uje dalej do `trade-api` i wstrzykuje read-token **server-side** (token nie trafia do przeglądarki).
|
||||||
|
|
||||||
|
Przykład:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd apps/visualizer
|
||||||
|
bash __start
|
||||||
|
```
|
||||||
|
|
||||||
|
Jeśli staging ma dodatkowy basic auth (np. Traefik `basicAuth`), dodaj:
|
||||||
|
`API_PROXY_BASIC_AUTH='USER:PASS'` albo `API_PROXY_BASIC_AUTH_FILE=tokens/frontend.json` (pola `username`/`password`).
|
||||||
|
|
||||||
|
## Definicja “done” dla zmiany
|
||||||
|
|
||||||
|
- Jest snapshot (commit w deploy) i można wrócić jednym ruchem.
|
||||||
|
- Staging działa (API/ingest/UI) i ma podstawowe smoke-checki.
|
||||||
|
- Sekrety nie zostały dodane do repo ani do logów/komentarzy.
|
||||||
Reference in New Issue
Block a user