Keep bot operational when forecast model is unavailable

This commit is contained in:
Курнат Андрей
2026-07-13 11:57:25 +03:00
parent da53483164
commit 668e606ee2
22 changed files with 706 additions and 73 deletions
+26 -3
View File
@@ -213,6 +213,7 @@ class TimeSeriesForecaster:
probability=self.settings.time_series_min_probability_up,
confidence=self.settings.time_series_min_confidence,
)
symbol_eligible = _calibration_symbol_eligible(calibration, symbol)
entry = _torch_recurrent_entry(symbol, artifact)
model = _torch_recurrent_model_name(symbol, artifact)
clip = _clamp(_float_entry(entry or {}, "clip", 8.0), 1.0, 50.0)
@@ -274,7 +275,8 @@ class TimeSeriesForecaster:
)
conservative_return_percent = min(expected_return_percent, q50_percent)
block_entry = bool(
(expected_return_percent <= -min_edge and probability_up <= 0.45)
not symbol_eligible
or (expected_return_percent <= -min_edge and probability_up <= 0.45)
or (q50_percent <= -min_edge and probability_up <= 0.48)
)
reason = _reason(
@@ -284,6 +286,8 @@ class TimeSeriesForecaster:
skill=skill,
block_entry=block_entry,
)
if not symbol_eligible:
reason = "symbol excluded by train-only calibration"
return TimeSeriesForecast(
enabled=True,
usable=True,
@@ -344,7 +348,9 @@ class TimeSeriesForecaster:
min_edge=min_edge,
max_adjustment=self.settings.time_series_max_adjustment,
)
block_entry = bool(expected_return_percent <= -min_edge and probability_up <= 0.45)
block_entry = bool(
not symbol_eligible or (expected_return_percent <= -min_edge and probability_up <= 0.45)
)
reason = _reason(
model=model,
expected_return_percent=expected_return_percent,
@@ -352,6 +358,8 @@ class TimeSeriesForecaster:
skill=skill,
block_entry=block_entry,
)
if not symbol_eligible:
reason = "symbol excluded by train-only calibration"
return TimeSeriesForecast(
enabled=True,
usable=True,
@@ -496,6 +504,16 @@ def _calibrated_thresholds(
}
def _calibration_symbol_eligible(calibration: dict[str, Any], symbol: str | None) -> bool:
if not isinstance(calibration, dict) or "eligible_symbols" not in calibration:
return True
eligible = calibration.get("eligible_symbols")
if not isinstance(eligible, list) or not symbol:
return False
allowed = {str(value).strip().upper() for value in eligible if str(value).strip()}
return symbol.strip().upper() in allowed
def _model_freshness(artifact: dict[str, Any], max_age_hours: float) -> tuple[str, float | None, bool]:
raw = str(artifact.get("created_at", "")).strip() if isinstance(artifact, dict) else ""
if not raw:
@@ -998,7 +1016,12 @@ def _torch_recurrent_entry(symbol: str | None, artifact: dict[str, Any]) -> dict
entry = default if isinstance(default, dict) else None
if not isinstance(entry, dict):
return None
if not isinstance(entry.get("state_dict"), dict):
members = entry.get("ensemble_members")
has_member_state = isinstance(members, list) and any(
isinstance(member, dict) and isinstance(member.get("state_dict"), dict)
for member in members
)
if not isinstance(entry.get("state_dict"), dict) and not has_member_state:
return None
return entry