Harden trading, training, and monitoring
This commit is contained in:
@@ -4,6 +4,7 @@ import json
|
||||
import math
|
||||
from bisect import bisect_right
|
||||
from dataclasses import asdict, dataclass, field
|
||||
from datetime import UTC, datetime
|
||||
from typing import Any
|
||||
|
||||
from crypto_spot_bot.config import Settings
|
||||
@@ -156,6 +157,9 @@ class TimeSeriesForecast:
|
||||
candidates: list[dict[str, Any]] = field(default_factory=list)
|
||||
quality_gate_passed: bool | None = None
|
||||
quality_gate: dict[str, Any] = field(default_factory=dict)
|
||||
model_created_at: str = ""
|
||||
model_age_hours: float | None = None
|
||||
model_fresh: bool = False
|
||||
|
||||
def as_dict(self) -> dict[str, Any]:
|
||||
return asdict(self)
|
||||
@@ -188,6 +192,10 @@ class TimeSeriesForecaster:
|
||||
return _empty_forecast(True, "not enough returns for PyTorch forecast")
|
||||
|
||||
artifact = self._load_lstm_artifact()
|
||||
model_created_at, model_age_hours, model_fresh = _model_freshness(
|
||||
artifact,
|
||||
self.settings.time_series_model_max_age_hours,
|
||||
)
|
||||
quality_gate = self._load_quality_gate()
|
||||
quality_gate_passed = _quality_gate_passed(quality_gate)
|
||||
entry = _torch_recurrent_entry(symbol, artifact)
|
||||
@@ -288,6 +296,9 @@ class TimeSeriesForecaster:
|
||||
candidates=[{"model": model, "mae_percent": round(model_mae * 100, 4)}],
|
||||
quality_gate_passed=quality_gate_passed,
|
||||
quality_gate=quality_gate,
|
||||
model_created_at=model_created_at,
|
||||
model_age_hours=model_age_hours,
|
||||
model_fresh=model_fresh,
|
||||
)
|
||||
|
||||
direct_horizon = _is_direct_horizon(entry)
|
||||
@@ -350,6 +361,9 @@ class TimeSeriesForecaster:
|
||||
candidates=[{"model": model, "mae_percent": round(model_mae * 100, 4)}],
|
||||
quality_gate_passed=quality_gate_passed,
|
||||
quality_gate=quality_gate,
|
||||
model_created_at=model_created_at,
|
||||
model_age_hours=model_age_hours,
|
||||
model_fresh=model_fresh,
|
||||
)
|
||||
|
||||
def _load_lstm_artifact(self) -> dict[str, Any]:
|
||||
@@ -420,6 +434,9 @@ def _empty_forecast(enabled: bool, reason: str) -> TimeSeriesForecast:
|
||||
candidates=[],
|
||||
quality_gate_passed=None,
|
||||
quality_gate={},
|
||||
model_created_at="",
|
||||
model_age_hours=None,
|
||||
model_fresh=False,
|
||||
)
|
||||
|
||||
|
||||
@@ -436,6 +453,20 @@ def _quality_gate_passed(quality_gate: dict[str, Any]) -> bool | None:
|
||||
return None
|
||||
|
||||
|
||||
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:
|
||||
return "", None, False
|
||||
try:
|
||||
created_at = datetime.fromisoformat(raw.replace("Z", "+00:00"))
|
||||
except ValueError:
|
||||
return raw, None, False
|
||||
if created_at.tzinfo is None:
|
||||
created_at = created_at.replace(tzinfo=UTC)
|
||||
age_hours = max(0.0, (datetime.now(UTC) - created_at.astimezone(UTC)).total_seconds() / 3600)
|
||||
return raw, round(age_hours, 4), age_hours <= max(0.1, max_age_hours)
|
||||
|
||||
|
||||
def _log_returns(closes: list[float]) -> list[float]:
|
||||
return [math.log(closes[index] / closes[index - 1]) for index in range(1, len(closes))]
|
||||
|
||||
|
||||
Reference in New Issue
Block a user