241 lines
5.3 KiB
Markdown
241 lines
5.3 KiB
Markdown
# 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
|