feat: add orderbook shadow training pipeline

This commit is contained in:
Курнат Андрей
2026-07-15 09:44:29 +03:00
parent f7a625586e
commit 5d8ad1437e
19 changed files with 1486 additions and 23 deletions
+33 -1
View File
@@ -26,6 +26,7 @@ from crypto_spot_bot.bybit import BybitClient
from crypto_spot_bot.config import load_settings
from crypto_spot_bot.indicators import add_indicators
from crypto_spot_bot.models import Candle
from crypto_spot_bot.orderbook_features import ORDERBOOK_FEATURES, load_orderbook_feature_map
from crypto_spot_bot.time_series import (
DEFAULT_TORCH_FEATURES,
_barrier_outcome,
@@ -97,6 +98,14 @@ def main() -> None:
context_symbols = sorted(set(symbols + _symbols(args.context_symbols, ())))
horizon = args.horizon if args.horizon > 0 else settings.time_series_forecast_horizon
round_trip_cost = _artifact_round_trip_cost(artifact, settings)
orderbook_features: dict[str, dict[int, dict[str, float]]] = {}
if args.orderbook_db:
orderbook_features, _orderbook_manifest = load_orderbook_feature_map(
args.orderbook_db,
interval=settings.base_interval,
symbols=symbols,
min_samples_per_bucket=args.orderbook_min_samples_per_bucket,
)
market_candles: dict[str, list[Candle]] = {}
for symbol in context_symbols:
@@ -125,6 +134,7 @@ def main() -> None:
min_candles=max(30, settings.time_series_min_candles),
calibration_window=args.calibration_window,
batch_size=args.batch_size,
orderbook_features=orderbook_features,
)
records.extend(symbol_records)
per_symbol_counts[symbol] = len(symbol_records)
@@ -304,6 +314,8 @@ def _parse_args() -> argparse.Namespace:
parser.add_argument("--min-oos-folds-with-trades", type=int, default=2, help="Minimum walk-forward folds that must produce trades.")
parser.add_argument("--min-oos-profit-factor", type=float, default=1.10, help="Minimum out-of-sample profit factor.")
parser.add_argument("--min-benchmark-edge-percent", type=float, default=0.0, help="Required total-net percent advantage over the benchmark.")
parser.add_argument("--orderbook-db", default="", help="SQLite cache used by an artifact with L1 features.")
parser.add_argument("--orderbook-min-samples-per-bucket", type=int, default=20)
return parser.parse_args()
@@ -352,6 +364,7 @@ def _forecast_records(
min_candles: int,
calibration_window: int,
batch_size: int,
orderbook_features: dict[str, dict[int, dict[str, float]]] | None = None,
) -> list[ForecastRecord]:
entry = _torch_recurrent_entry(symbol, artifact)
model = _torch_recurrent_model_name(symbol, artifact)
@@ -364,6 +377,7 @@ def _forecast_records(
symbol=symbol,
market_candles=market_candles,
trend_candles=trend_candles,
orderbook_features=orderbook_features,
)
closes = [float(candle.close) for candle in candles]
decision_horizon = _calibration_horizon(entry, horizon, explicit=horizon_is_explicit)
@@ -376,6 +390,18 @@ def _forecast_records(
start += 1
if calibration_window > 0:
start = max(start, end - calibration_window)
lookback = max(1, int(float(entry.get("lookback", 64))))
requires_orderbook = any(name in ORDERBOOK_FEATURES for name in feature_names)
symbol_orderbook = (orderbook_features or {}).get(symbol.upper(), {})
valid_indices = {
index
for index in range(start, max(start, end))
if not requires_orderbook
or all(
candles[position].timestamp in symbol_orderbook
for position in range(index - lookback + 1, index + 1)
)
}
batched_records = _batch_forecast_records(
symbol=symbol,
candles=candles,
@@ -389,6 +415,7 @@ def _forecast_records(
start=start,
end=end,
batch_size=batch_size,
valid_indices=valid_indices,
)
if batched_records is not None:
return batched_records
@@ -398,6 +425,8 @@ def _forecast_records(
# belong exclusively to the final quality gate and cannot influence replay.
skill = _entry_validation_skill(entry)
for index in range(start, max(start, end)):
if index not in valid_indices:
continue
prediction = _torch_recurrent_predict(
_log_returns(closes[: index + 1]),
symbol,
@@ -476,6 +505,7 @@ def _batch_forecast_records(
start: int,
end: int,
batch_size: int,
valid_indices: set[int] | None = None,
) -> list[ForecastRecord] | None:
if torch is None or RecurrentReturnModel is None:
return None
@@ -494,7 +524,9 @@ def _batch_forecast_records(
indices = [
index
for index in range(start, max(start, end))
if index - lookback + 1 >= 0 and index + decision_horizon < len(closes)
if index - lookback + 1 >= 0
and index + decision_horizon < len(closes)
and (valid_indices is None or index in valid_indices)
]
if not indices:
return []