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
+83
View File
@@ -1,13 +1,18 @@
from __future__ import annotations
from types import SimpleNamespace
from tools.calibrate_torch_thresholds import (
CalibrationResult,
ForecastRecord,
_average_selected_predictions,
_apply_platt_calibration,
_choose_recommendation,
_full_backtest,
_fit_platt_calibration,
_entry_validation_skill,
)
from tools.train_torch_recurrent_forecaster import _ensemble_candidate
def _result(*, trades: int, average: float, total: float, profit_factor: float) -> CalibrationResult:
@@ -85,3 +90,81 @@ def test_entry_quality_never_falls_back_to_holdout_skill() -> None:
assert _entry_validation_skill(entry) == 0.12
assert _entry_validation_skill({"skill": 0.99, "holdout_skill": 0.99}) == 0.0
def test_batched_ensemble_averages_decoded_predictions() -> None:
averaged = _average_selected_predictions(
[
{"expected_return": 0.01, "q50": 0.02, "probability_up": 0.6},
{"expected_return": 0.03, "q50": 0.04, "probability_up": 0.8},
]
)
assert averaged == {
"expected_return": 0.02,
"q50": 0.03,
"probability_up": 0.7,
}
def test_multi_seed_export_does_not_duplicate_first_member_weights() -> None:
members = [
{
"validation_mae": 0.1,
"state_dict": {"weight": [seed]},
"head_weight": [[seed]],
"head_bias": [seed],
}
for seed in (7, 19)
]
exported = _ensemble_candidate(members, [7, 19])
assert exported["ensemble_size"] == 2
assert exported["ensemble_seeds"] == [7, 19]
assert len(exported["ensemble_members"]) == 2
assert "state_dict" not in exported
assert "head_weight" not in exported
def test_single_seed_export_keeps_only_top_level_weights() -> None:
exported = _ensemble_candidate(
[
{
"validation_mae": 0.1,
"state_dict": {"weight": [7]},
"head_weight": [[7]],
"head_bias": [7],
}
],
[7],
)
assert exported["ensemble_size"] == 1
assert exported["state_dict"] == {"weight": [7]}
assert "ensemble_members" not in exported
def test_full_backtest_never_uses_global_threshold_for_ineligible_symbol() -> None:
btc = [_record(index, 0.8, 1.0) for index in range(3)]
eth = [_record(index, 0.8, 1.0) for index in range(3)]
for record in eth:
record.symbol = "ETHUSDT"
thresholds = _result(trades=3, average=1.0, total=3.0, profit_factor=999.0)
replay = _full_backtest(
btc + eth,
thresholds,
horizon=3,
round_trip_cost=0.0,
settings=SimpleNamespace(
stop_loss_percent=0.04,
take_profit_percent=0.035,
stop_loss_exit_enabled=True,
atr_trailing_multiplier=2.2,
),
symbol_thresholds={"BTCUSDT": thresholds},
require_symbol_thresholds=True,
)
assert {row["symbol"] for row in replay["symbol_breakdown"]} == {"BTCUSDT"}