Fix remote training and model validation pipeline
This commit is contained in:
@@ -0,0 +1,87 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from tools.calibrate_torch_thresholds import (
|
||||
CalibrationResult,
|
||||
ForecastRecord,
|
||||
_apply_platt_calibration,
|
||||
_choose_recommendation,
|
||||
_fit_platt_calibration,
|
||||
_entry_validation_skill,
|
||||
)
|
||||
|
||||
|
||||
def _result(*, trades: int, average: float, total: float, profit_factor: float) -> CalibrationResult:
|
||||
return CalibrationResult(
|
||||
edge=0.05,
|
||||
probability=0.52,
|
||||
confidence=0.4,
|
||||
trades=trades,
|
||||
wins=max(0, trades // 2),
|
||||
win_rate=0.5,
|
||||
total_net_percent=total,
|
||||
average_net_percent=average,
|
||||
max_drawdown_percent=1.0,
|
||||
profit_factor=profit_factor,
|
||||
score=1.0,
|
||||
)
|
||||
|
||||
|
||||
def _record(index: int, probability: float, future: float) -> ForecastRecord:
|
||||
return ForecastRecord(
|
||||
symbol="BTCUSDT",
|
||||
index=index,
|
||||
timestamp=index,
|
||||
close=100.0,
|
||||
high=101.0,
|
||||
low=99.0,
|
||||
next_open=100.0,
|
||||
next_timestamp=index + 1,
|
||||
atr=1.0,
|
||||
expected_percent=0.1,
|
||||
probability_up=probability,
|
||||
confidence=0.5,
|
||||
skill=0.1,
|
||||
q50_percent=0.1,
|
||||
block_entry=False,
|
||||
future_net_percent=future,
|
||||
benchmark_entry=False,
|
||||
benchmark_exit=False,
|
||||
)
|
||||
|
||||
|
||||
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)],
|
||||
min_trades=30,
|
||||
)
|
||||
|
||||
assert selected is None
|
||||
|
||||
|
||||
def test_calibration_selects_only_viable_result() -> None:
|
||||
viable = _result(trades=30, average=0.2, total=6.0, profit_factor=1.4)
|
||||
|
||||
assert _choose_recommendation([viable], min_trades=30) is viable
|
||||
|
||||
|
||||
def test_platt_calibration_learns_probability_direction_from_train_records() -> None:
|
||||
records = [
|
||||
_record(index, 0.8 if index % 2 else 0.2, -1.0 if index % 2 else 1.0)
|
||||
for index in range(100)
|
||||
]
|
||||
|
||||
calibration = _fit_platt_calibration(records)
|
||||
calibrated = _apply_platt_calibration(
|
||||
[_record(101, 0.8, -1.0), _record(102, 0.2, 1.0)],
|
||||
calibration,
|
||||
)
|
||||
|
||||
assert calibration["slope"] < 0
|
||||
assert calibrated[0].probability_up < calibrated[1].probability_up
|
||||
|
||||
|
||||
def test_entry_quality_never_falls_back_to_holdout_skill() -> None:
|
||||
entry = {"validation_skill": 0.12, "skill": 0.99, "holdout_skill": 0.99}
|
||||
|
||||
assert _entry_validation_skill(entry) == 0.12
|
||||
assert _entry_validation_skill({"skill": 0.99, "holdout_skill": 0.99}) == 0.0
|
||||
@@ -304,6 +304,60 @@ def test_time_series_forecaster_attaches_quality_gate(make_settings, tmp_path) -
|
||||
assert forecast.quality_gate["status"] == "fail"
|
||||
|
||||
|
||||
def test_time_series_forecaster_uses_symbol_calibration(make_settings, tmp_path) -> None:
|
||||
artifact_path = tmp_path / "lstm_forecaster.json"
|
||||
_write_torch_gru_artifact(artifact_path, head_bias=0.2)
|
||||
(tmp_path / "torch_threshold_calibration.json").write_text(
|
||||
json.dumps(
|
||||
{
|
||||
"validation": {"status": "pass", "passed": True},
|
||||
"recommended": {"edge": 0.08, "probability": 0.52, "confidence": 0.4},
|
||||
"symbol_recommendations": {
|
||||
"BTCUSDT": {"edge": 0.03, "probability": 0.55, "confidence": 0.45}
|
||||
},
|
||||
}
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
settings = make_settings(
|
||||
tmp_path,
|
||||
time_series_lstm_model_path=artifact_path,
|
||||
time_series_forecast_horizon=1,
|
||||
)
|
||||
|
||||
forecast = TimeSeriesForecaster(settings).forecast(
|
||||
_candles_from_returns([0.0001] * 140), symbol="BTCUSDT"
|
||||
)
|
||||
|
||||
assert forecast.calibrated_min_edge_percent == 0.03
|
||||
assert forecast.calibrated_min_probability_up == 0.55
|
||||
assert forecast.calibrated_min_confidence == 0.45
|
||||
|
||||
|
||||
def test_time_series_forecaster_averages_ensemble_members(make_settings, tmp_path) -> None:
|
||||
artifact_path = tmp_path / "lstm_forecaster.json"
|
||||
_write_torch_gru_artifact(artifact_path, head_bias=0.9)
|
||||
artifact = json.loads(artifact_path.read_text(encoding="utf-8"))
|
||||
entry = artifact["symbols"]["BTCUSDT"]
|
||||
entry["ensemble_members"] = [
|
||||
{"state_dict": entry["state_dict"], "head_weight": [0.0, 0.0], "head_bias": bias}
|
||||
for bias in (0.1, 0.3)
|
||||
]
|
||||
artifact_path.write_text(json.dumps(artifact), encoding="utf-8")
|
||||
settings = make_settings(
|
||||
tmp_path,
|
||||
time_series_lstm_model_path=artifact_path,
|
||||
time_series_forecast_horizon=1,
|
||||
)
|
||||
|
||||
forecast = TimeSeriesForecaster(settings).forecast(
|
||||
_candles_from_returns([0.0001] * 140), symbol="BTCUSDT"
|
||||
)
|
||||
|
||||
assert forecast.usable is True
|
||||
assert 0.015 <= forecast.expected_return_percent <= 0.025
|
||||
|
||||
|
||||
def test_time_series_forecaster_reads_multifeature_direct_horizon_artifact(make_settings, tmp_path) -> None:
|
||||
artifact_path = tmp_path / "lstm_forecaster.json"
|
||||
_write_multifeature_torch_gru_artifact(artifact_path, head_bias=0.2)
|
||||
|
||||
@@ -48,6 +48,42 @@ def test_training_coordinator_preserves_boolean_resume_candidate_parameter(tmp_p
|
||||
assert requested["job"]["parameters"] == {"resume_candidate": True}
|
||||
|
||||
|
||||
def test_training_coordinator_reports_worker_identity_from_heartbeat(tmp_path) -> None:
|
||||
coordinator = TrainingCoordinator(tmp_path)
|
||||
|
||||
heartbeat = coordinator.heartbeat(
|
||||
{
|
||||
"worker_id": "SEVENHILL:G:\\Repos\\TradeBot",
|
||||
"name": "SEVENHILL",
|
||||
"path": "G:\\Repos\\TradeBot",
|
||||
}
|
||||
)
|
||||
|
||||
assert heartbeat["worker"]["name"] == "SEVENHILL"
|
||||
assert heartbeat["worker"]["path"] == "G:\\Repos\\TradeBot"
|
||||
assert heartbeat["status"]["worker"] == heartbeat["worker"]
|
||||
|
||||
|
||||
def test_training_coordinator_records_rejected_candidate_as_completed_training(tmp_path) -> None:
|
||||
coordinator = TrainingCoordinator(tmp_path)
|
||||
job = coordinator.request_retrain({"source": "android"})["job"]
|
||||
coordinator.claim({"worker_id": "worker-1"})
|
||||
|
||||
completed = coordinator.complete(
|
||||
job["id"],
|
||||
{
|
||||
"success": True,
|
||||
"message": "training completed; candidate rejected by quality gate",
|
||||
"summary": {"accepted": False, "reason": "candidate_failed_honest_validation"},
|
||||
},
|
||||
)
|
||||
|
||||
assert completed["job"]["status"] == "completed"
|
||||
assert completed["job"]["phase"] == "completed"
|
||||
assert completed["job"]["progress_percent"] == 100
|
||||
assert completed["job"]["model_decision"] == "rejected"
|
||||
|
||||
|
||||
def test_training_coordinator_accepts_chunked_artifact_upload(tmp_path) -> None:
|
||||
coordinator = TrainingCoordinator(tmp_path)
|
||||
job = coordinator.request_retrain({"source": "test"})["job"]
|
||||
|
||||
Reference in New Issue
Block a user