feat: add orderbook shadow training pipeline
This commit is contained in:
+115
-1
@@ -2,6 +2,7 @@ from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
import logging
|
||||
import math
|
||||
import sqlite3
|
||||
from datetime import datetime
|
||||
|
||||
@@ -14,7 +15,7 @@ from crypto_spot_bot.models import BotStatus, Signal, Ticker, utc_now
|
||||
from crypto_spot_bot.patterns import PatternAnalyzer
|
||||
from crypto_spot_bot.strategy import SpotStrategy, torch_model_readiness_reasons
|
||||
from crypto_spot_bot.storage import Storage
|
||||
from crypto_spot_bot.time_series import TimeSeriesForecaster
|
||||
from crypto_spot_bot.time_series import TimeSeriesForecaster, _barrier_outcome
|
||||
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
@@ -31,6 +32,7 @@ class CryptoSpotBot:
|
||||
pattern_analyzer: PatternAnalyzer,
|
||||
learner: TradeLearner,
|
||||
forecaster: TimeSeriesForecaster | None = None,
|
||||
shadow_forecaster: TimeSeriesForecaster | None = None,
|
||||
llm_advisor=None,
|
||||
):
|
||||
self.settings = settings
|
||||
@@ -41,6 +43,7 @@ class CryptoSpotBot:
|
||||
self.pattern_analyzer = pattern_analyzer
|
||||
self.learner = learner
|
||||
self.forecaster = forecaster
|
||||
self.shadow_forecaster = shadow_forecaster
|
||||
self.llm_advisor = llm_advisor
|
||||
self.running = False
|
||||
self.started_at: datetime | None = None
|
||||
@@ -52,6 +55,8 @@ class CryptoSpotBot:
|
||||
self._last_reconciliation_at: datetime | None = None
|
||||
self._last_prune_at: datetime | None = None
|
||||
self._consecutive_loop_errors = 0
|
||||
self._orderbook_feature_cache_key: tuple[tuple[str, int], ...] = ()
|
||||
self._orderbook_feature_cache: dict[str, dict[int, dict[str, float]]] = {}
|
||||
|
||||
async def start(self) -> None:
|
||||
if self.running:
|
||||
@@ -416,6 +421,25 @@ class CryptoSpotBot:
|
||||
self.market.patterns = patterns
|
||||
|
||||
def _update_forecasts(self) -> None:
|
||||
cache_key = tuple(
|
||||
(symbol, rows[-1].timestamp if rows else 0)
|
||||
for symbol, rows in sorted(self.market.candles.items())
|
||||
)
|
||||
earliest_timestamp = min(
|
||||
(rows[0].timestamp for rows in self.market.candles.values() if rows),
|
||||
default=0,
|
||||
)
|
||||
if cache_key != self._orderbook_feature_cache_key:
|
||||
orderbook_features, _manifest = self.storage.recent_aggregated_orderbook_features(
|
||||
interval=self.settings.base_interval,
|
||||
symbols=self.market.symbols,
|
||||
after_timestamp_ms=earliest_timestamp,
|
||||
min_samples_per_bucket=20,
|
||||
)
|
||||
self._orderbook_feature_cache = orderbook_features
|
||||
self._orderbook_feature_cache_key = cache_key
|
||||
else:
|
||||
orderbook_features = self._orderbook_feature_cache
|
||||
if (
|
||||
self.forecaster is None
|
||||
or not self.settings.time_series_forecast_enabled
|
||||
@@ -429,8 +453,98 @@ class CryptoSpotBot:
|
||||
symbol=symbol,
|
||||
market_candles=self.market.candles,
|
||||
trend_candles=self.market.trend_candles.get(symbol, []),
|
||||
orderbook_features=orderbook_features,
|
||||
).as_dict()
|
||||
self.market.forecasts = forecasts
|
||||
self._update_shadow_forecasts(orderbook_features)
|
||||
|
||||
def _update_shadow_forecasts(
|
||||
self,
|
||||
orderbook_features: dict[str, dict[int, dict[str, float]]],
|
||||
) -> None:
|
||||
if self.shadow_forecaster is None:
|
||||
self.market.shadow_forecasts = {}
|
||||
return
|
||||
model_sha256 = self.shadow_forecaster.artifact_sha256()
|
||||
if not model_sha256:
|
||||
self.market.shadow_forecasts = {}
|
||||
return
|
||||
forecasts: dict[str, dict] = {}
|
||||
for symbol in self.market.symbols:
|
||||
candles = self.market.candles.get(symbol, [])
|
||||
forecast = self.shadow_forecaster.forecast(
|
||||
candles,
|
||||
symbol=symbol,
|
||||
market_candles=self.market.candles,
|
||||
trend_candles=self.market.trend_candles.get(symbol, []),
|
||||
orderbook_features=orderbook_features,
|
||||
).as_dict()
|
||||
forecast["shadow"] = True
|
||||
forecast["model_sha256"] = model_sha256
|
||||
forecasts[symbol] = forecast
|
||||
self._record_and_settle_shadow(symbol, candles, forecast, model_sha256)
|
||||
self.market.shadow_forecasts = forecasts
|
||||
|
||||
def _record_and_settle_shadow(
|
||||
self,
|
||||
symbol: str,
|
||||
candles: list,
|
||||
forecast: dict,
|
||||
model_sha256: str,
|
||||
) -> None:
|
||||
if candles and forecast.get("usable"):
|
||||
probability = float(
|
||||
forecast.get("probability_take_profit_first")
|
||||
if forecast.get("probability_take_profit_first") is not None
|
||||
else forecast.get("probability_up", 0.5)
|
||||
)
|
||||
expected = float(forecast.get("expected_return_percent", 0.0) or 0.0)
|
||||
eligible = bool(
|
||||
not forecast.get("block_entry")
|
||||
and expected >= float(forecast.get("calibrated_min_edge_percent", 0.0) or 0.0)
|
||||
and probability >= float(forecast.get("calibrated_min_probability_up", 0.5) or 0.5)
|
||||
)
|
||||
self.storage.insert_shadow_prediction(
|
||||
model_sha256=model_sha256,
|
||||
symbol=symbol,
|
||||
forecast_timestamp_ms=candles[-1].timestamp,
|
||||
horizon=max(1, int(forecast.get("horizon", 1) or 1)),
|
||||
reference_price=float(candles[-1].close),
|
||||
expected_return_percent=expected,
|
||||
probability_up=probability,
|
||||
eligible_signal=eligible,
|
||||
)
|
||||
if not candles:
|
||||
return
|
||||
indexes = {candle.timestamp: index for index, candle in enumerate(candles)}
|
||||
round_trip_cost = 2.0 * (
|
||||
float(self.settings.taker_fee_rate) + float(self.settings.slippage_rate)
|
||||
)
|
||||
for row in self.storage.pending_shadow_predictions(
|
||||
model_sha256=model_sha256,
|
||||
symbol=symbol,
|
||||
):
|
||||
index = indexes.get(int(row.get("forecast_timestamp_ms", 0) or 0))
|
||||
horizon = max(1, int(row.get("horizon", 1) or 1))
|
||||
if index is None or index + horizon >= len(candles):
|
||||
continue
|
||||
outcome = _barrier_outcome(
|
||||
candles,
|
||||
end_index=index,
|
||||
horizon=horizon,
|
||||
stop_loss_percent=float(self.settings.stop_loss_percent),
|
||||
take_profit_percent=float(self.settings.take_profit_percent),
|
||||
round_trip_cost=round_trip_cost,
|
||||
)
|
||||
if outcome is None:
|
||||
continue
|
||||
actual_log_return, take_profit_first = outcome
|
||||
actual_return_percent = (math.exp(actual_log_return) - 1.0) * 100.0
|
||||
self.storage.settle_shadow_prediction(
|
||||
int(row["id"]),
|
||||
actual_return_percent=actual_return_percent,
|
||||
take_profit_first=take_profit_first >= 0.5,
|
||||
)
|
||||
|
||||
def status(self) -> BotStatus:
|
||||
live_ready = self.settings.live_ready
|
||||
|
||||
Reference in New Issue
Block a user