From 51a783389656bbb45f408788425c1f70d1b41a4e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=D0=9A=D1=83=D1=80=D0=BD=D0=B0=D1=82=20=D0=90=D0=BD=D0=B4?= =?UTF-8?q?=D1=80=D0=B5=D0=B9?= Date: Tue, 14 Jul 2026 23:28:53 +0300 Subject: [PATCH] fix: calibrate dynamic model symbols --- tests/test_calibrate_thresholds.py | 16 ++++++++++++++++ tools/calibrate_torch_thresholds.py | 29 +++++++++++++++++++++++++---- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/tests/test_calibrate_thresholds.py b/tests/test_calibrate_thresholds.py index d720478..6ac1b97 100644 --- a/tests/test_calibrate_thresholds.py +++ b/tests/test_calibrate_thresholds.py @@ -8,6 +8,7 @@ from tools.calibrate_torch_thresholds import ( _average_selected_predictions, _apply_platt_calibration, _build_torch_model, + _calibration_symbols, _choose_recommendation, _full_backtest, _fit_platt_calibration, @@ -62,6 +63,21 @@ def _record(index: int, probability: float, future: float) -> ForecastRecord: ) +def test_calibration_symbols_follow_explicit_configured_artifact_precedence() -> None: + artifact = {"symbols": {"btcusdt": {}, "ethusdt": {}}} + + assert _calibration_symbols("solusdt, xrpusdt", ("ADAUSDT",), artifact) == [ + "SOLUSDT", + "XRPUSDT", + ] + assert _calibration_symbols("", ("ADAUSDT",), artifact) == ["ADAUSDT"] + assert _calibration_symbols("", (), artifact) == ["BTCUSDT", "ETHUSDT"] + + +def test_calibration_symbols_reject_malformed_artifact_symbols() -> None: + assert _calibration_symbols("", (), {"symbols": []}) == [] + + def test_calibration_does_not_fallback_to_too_few_trades() -> None: selected = _choose_recommendation( [_result(trades=1, average=2.0, total=2.0, profit_factor=999.0)], diff --git a/tools/calibrate_torch_thresholds.py b/tools/calibrate_torch_thresholds.py index 73615bc..481a823 100644 --- a/tools/calibrate_torch_thresholds.py +++ b/tools/calibrate_torch_thresholds.py @@ -88,13 +88,13 @@ def main() -> None: if torch is not None and args.threads > 0: torch.set_num_threads(args.threads) settings = load_settings(args.env) - client = BybitClient(settings) - symbols = _symbols(args.symbols, settings.symbols) - context_symbols = sorted(set(symbols + _symbols(args.context_symbols, ()))) artifact_path = Path(args.artifact or settings.time_series_lstm_model_path) artifact_bytes = artifact_path.read_bytes() artifact_sha256 = hashlib.sha256(artifact_bytes).hexdigest() artifact = json.loads(artifact_bytes.decode("utf-8")) + client = BybitClient(settings) + symbols = _calibration_symbols(args.symbols, settings.symbols, artifact) + 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) @@ -277,7 +277,11 @@ def _parse_args() -> argparse.Namespace: parser = argparse.ArgumentParser(description="Calibrate TradeBot Torch forecast entry thresholds.") parser.add_argument("--env", default=None, help="Path to .env file.") parser.add_argument("--artifact", default="", help="Path to lstm_forecaster.json.") - parser.add_argument("--symbols", default="", help="Comma-separated symbols. Defaults to configured fixed symbols.") + parser.add_argument( + "--symbols", + default="", + help="Comma-separated symbols. Defaults to configured fixed symbols, then artifact symbols.", + ) parser.add_argument("--context-symbols", default="BTCUSDT,ETHUSDT", help="Cross-asset context symbols.") parser.add_argument("--limit", type=int, default=2000, help="Hourly candles per symbol.") parser.add_argument("--trend-limit", type=int, default=320, help="Daily candles per symbol.") @@ -308,6 +312,23 @@ def _symbols(raw: str, fallback: tuple[str, ...] | list[str]) -> list[str]: return [str(item).upper() for item in fallback] +def _calibration_symbols( + raw: str, + configured: tuple[str, ...] | list[str], + artifact: dict[str, Any], +) -> list[str]: + explicit = _symbols(raw, ()) + if explicit: + return explicit + fixed = _symbols("", configured) + if fixed: + return fixed + artifact_symbols = artifact.get("symbols") + if not isinstance(artifact_symbols, dict): + return [] + return [str(symbol).strip().upper() for symbol in artifact_symbols if str(symbol).strip()] + + def _forecast_records( *, symbol: str,