Keep bot operational when forecast model is unavailable
This commit is contained in:
@@ -159,6 +159,7 @@ def main() -> None:
|
||||
settings=settings,
|
||||
)
|
||||
symbol_recommendations: dict[str, dict[str, Any]] = {}
|
||||
symbol_threshold_results: dict[str, CalibrationResult] = {}
|
||||
for symbol in symbols:
|
||||
symbol_records = [record for record in records if record.symbol == symbol]
|
||||
symbol_results = _calibrate_strategy(
|
||||
@@ -177,7 +178,8 @@ def main() -> None:
|
||||
) if symbol_results else None
|
||||
if symbol_selected is not None:
|
||||
symbol_recommendations[symbol] = _result_dict(symbol_selected)
|
||||
calibration_insufficient = recommended is None
|
||||
symbol_threshold_results[symbol] = symbol_selected
|
||||
calibration_insufficient = recommended is None or not symbol_threshold_results
|
||||
if recommended is None:
|
||||
recommended = _empty_recommendation(
|
||||
_float_grid(args.edge_grid),
|
||||
@@ -185,6 +187,16 @@ def main() -> None:
|
||||
_float_grid(args.confidence_grid),
|
||||
)
|
||||
full_backtest = {**_stats([]), "trades_detail": [], "symbol_breakdown": []}
|
||||
elif symbol_threshold_results:
|
||||
full_backtest = _full_backtest(
|
||||
records,
|
||||
recommended,
|
||||
horizon=horizon,
|
||||
round_trip_cost=round_trip_cost,
|
||||
settings=settings,
|
||||
symbol_thresholds=symbol_threshold_results,
|
||||
require_symbol_thresholds=True,
|
||||
)
|
||||
print("\nRECOMMENDED")
|
||||
print(_result_line(recommended))
|
||||
print("\nFULL_REPLAY")
|
||||
@@ -248,6 +260,7 @@ def main() -> None:
|
||||
"recommended": _result_dict(deployment_recommended),
|
||||
"calibration_insufficient": calibration_insufficient,
|
||||
"symbol_recommendations": deployment_symbol_recommendations,
|
||||
"eligible_symbols": sorted(deployment_symbol_recommendations),
|
||||
"full_replay": full_backtest,
|
||||
"walk_forward": walk_forward,
|
||||
"benchmark": benchmark,
|
||||
@@ -419,8 +432,8 @@ def _batch_forecast_records(
|
||||
horizons = _entry_target_horizons(entry)
|
||||
if not horizons:
|
||||
return None
|
||||
model = _build_torch_model(entry, model_name)
|
||||
if model is None:
|
||||
models = _build_torch_models(entry, model_name)
|
||||
if not models:
|
||||
return None
|
||||
|
||||
lookback = int(_clamp(_float_entry(entry, "lookback", 64.0), 4.0, 512.0))
|
||||
@@ -438,7 +451,8 @@ def _batch_forecast_records(
|
||||
|
||||
records: list[ForecastRecord] = []
|
||||
skill = _entry_validation_skill(entry)
|
||||
model.eval()
|
||||
for model in models:
|
||||
model.eval()
|
||||
with torch.no_grad():
|
||||
for offset in range(0, len(indices), max(1, batch_size)):
|
||||
batch_indices = indices[offset : offset + max(1, batch_size)]
|
||||
@@ -453,17 +467,25 @@ def _batch_forecast_records(
|
||||
for index in batch_indices
|
||||
]
|
||||
batch = torch.tensor(windows, dtype=torch.float32)
|
||||
outputs = model(batch).detach().cpu().tolist()
|
||||
for index, output in zip(batch_indices, outputs):
|
||||
selected = _decode_selected_output(
|
||||
output,
|
||||
entry=entry,
|
||||
candles=candles,
|
||||
closes=closes,
|
||||
index=index,
|
||||
horizon=decision_horizon,
|
||||
clip=clip,
|
||||
round_trip_cost=round_trip_cost,
|
||||
outputs_by_model = [model(batch).detach().cpu().tolist() for model in models]
|
||||
for batch_offset, index in enumerate(batch_indices):
|
||||
selected = _average_selected_predictions(
|
||||
[
|
||||
decoded
|
||||
for outputs in outputs_by_model
|
||||
if (
|
||||
decoded := _decode_selected_output(
|
||||
outputs[batch_offset],
|
||||
entry=entry,
|
||||
candles=candles,
|
||||
closes=closes,
|
||||
index=index,
|
||||
horizon=decision_horizon,
|
||||
clip=clip,
|
||||
round_trip_cost=round_trip_cost,
|
||||
)
|
||||
) is not None
|
||||
]
|
||||
)
|
||||
if selected is None:
|
||||
continue
|
||||
@@ -502,11 +524,21 @@ def _batch_forecast_records(
|
||||
return records
|
||||
|
||||
|
||||
def _build_torch_models(entry: dict[str, Any], model_name: str) -> list[Any]:
|
||||
members = entry.get("ensemble_members")
|
||||
if isinstance(members, list) and members:
|
||||
base = {key: value for key, value in entry.items() if key != "ensemble_members"}
|
||||
models = [
|
||||
_build_torch_model({**base, **member}, model_name)
|
||||
for member in members
|
||||
if isinstance(member, dict)
|
||||
]
|
||||
return [model for model in models if model is not None]
|
||||
model = _build_torch_model(entry, model_name)
|
||||
return [model] if model is not None else []
|
||||
|
||||
|
||||
def _build_torch_model(entry: dict[str, Any], model_name: str) -> Any | None:
|
||||
if isinstance(entry.get("ensemble_members"), list) and entry["ensemble_members"]:
|
||||
# Ensemble inference is handled by the shared pure-Python runtime so
|
||||
# calibration and production use the exact same averaging path.
|
||||
return None
|
||||
if torch is None or RecurrentReturnModel is None:
|
||||
return None
|
||||
architecture = "lstm" if model_name == "torch_lstm" else "gru" if model_name == "torch_gru" else ""
|
||||
@@ -560,6 +592,15 @@ def _build_torch_model(entry: dict[str, Any], model_name: str) -> Any | None:
|
||||
return model
|
||||
|
||||
|
||||
def _average_selected_predictions(rows: list[dict[str, float]]) -> dict[str, float] | None:
|
||||
if not rows:
|
||||
return None
|
||||
return {
|
||||
name: sum(float(row[name]) for row in rows) / len(rows)
|
||||
for name in ("expected_return", "q50", "probability_up")
|
||||
}
|
||||
|
||||
|
||||
def _decode_selected_output(
|
||||
output: list[float],
|
||||
*,
|
||||
@@ -638,6 +679,7 @@ def _full_backtest(
|
||||
settings: Any,
|
||||
detail_limit: int = 50,
|
||||
symbol_thresholds: dict[str, CalibrationResult] | None = None,
|
||||
require_symbol_thresholds: bool = False,
|
||||
) -> dict[str, Any]:
|
||||
positions: dict[str, dict[str, Any]] = {}
|
||||
trades: list[float] = []
|
||||
@@ -708,6 +750,8 @@ def _full_backtest(
|
||||
|
||||
if record.symbol in positions:
|
||||
continue
|
||||
if require_symbol_thresholds and record.symbol not in (symbol_thresholds or {}):
|
||||
continue
|
||||
if _candidate_allows(record, active_thresholds.edge, active_thresholds.probability, active_thresholds.confidence):
|
||||
positions[record.symbol] = {
|
||||
"entry_price": record.next_open,
|
||||
@@ -907,6 +951,7 @@ def _walk_forward(
|
||||
settings=settings,
|
||||
detail_limit=0,
|
||||
symbol_thresholds=symbol_thresholds,
|
||||
require_symbol_thresholds=True,
|
||||
)
|
||||
test_rows = test_backtest.get("trades_detail", [])
|
||||
test_trades = [float(row.get("net_percent", 0.0) or 0.0) for row in test_rows if isinstance(row, dict)]
|
||||
@@ -921,6 +966,7 @@ def _walk_forward(
|
||||
"symbol_thresholds": {
|
||||
symbol: _result_dict(value) for symbol, value in symbol_thresholds.items()
|
||||
},
|
||||
"eligible_symbols": sorted(symbol_thresholds),
|
||||
"probability_calibration": probability_calibration,
|
||||
"test": {key: value for key, value in test_backtest.items() if key != "trades_detail"},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user