feat: add orderbook shadow training pipeline
This commit is contained in:
@@ -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 []
|
||||
|
||||
Reference in New Issue
Block a user