Add Torch probe entries and Pi artifact sync

This commit is contained in:
Курнат Андрей
2026-06-24 21:31:05 +03:00
parent 15a50fb575
commit cb8efb7cd7
16 changed files with 307266 additions and 363669 deletions
+4
View File
@@ -67,6 +67,10 @@ TIME_SERIES_MIN_CONFIDENCE=0.4
TIME_SERIES_MAX_ADJUSTMENT=0.08 TIME_SERIES_MAX_ADJUSTMENT=0.08
TIME_SERIES_LSTM_ENABLED=true TIME_SERIES_LSTM_ENABLED=true
TIME_SERIES_LSTM_MODEL_PATH=runtime/lstm_forecaster.json TIME_SERIES_LSTM_MODEL_PATH=runtime/lstm_forecaster.json
TIME_SERIES_PROBE_ENABLED=true
TIME_SERIES_PROBE_MIN_EDGE_PERCENT=0.02
TIME_SERIES_PROBE_MIN_PROBABILITY_UP=0.55
TIME_SERIES_PROBE_SIZE_MULTIPLIER=0.40
STOP_LOSS_PERCENT=0.04 STOP_LOSS_PERCENT=0.04
TAKE_PROFIT_PERCENT=0.035 TAKE_PROFIT_PERCENT=0.035
TRAILING_STOP_PERCENT=0.015 TRAILING_STOP_PERCENT=0.015
+4
View File
@@ -67,6 +67,10 @@ TIME_SERIES_MIN_CONFIDENCE=0.4
TIME_SERIES_MAX_ADJUSTMENT=0.08 TIME_SERIES_MAX_ADJUSTMENT=0.08
TIME_SERIES_LSTM_ENABLED=true TIME_SERIES_LSTM_ENABLED=true
TIME_SERIES_LSTM_MODEL_PATH=runtime/lstm_forecaster.json TIME_SERIES_LSTM_MODEL_PATH=runtime/lstm_forecaster.json
TIME_SERIES_PROBE_ENABLED=true
TIME_SERIES_PROBE_MIN_EDGE_PERCENT=0.02
TIME_SERIES_PROBE_MIN_PROBABILITY_UP=0.55
TIME_SERIES_PROBE_SIZE_MULTIPLIER=0.40
STOP_LOSS_PERCENT=0.04 STOP_LOSS_PERCENT=0.04
TAKE_PROFIT_PERCENT=0.035 TAKE_PROFIT_PERCENT=0.035
TRAILING_STOP_PERCENT=0.015 TRAILING_STOP_PERCENT=0.015
+11 -1
View File
@@ -88,7 +88,13 @@ powershell -ExecutionPolicy Bypass -File tools\run_torch_retrain.ps1
powershell -ExecutionPolicy Bypass -File tools\install_windows_torch_retrainer.ps1 powershell -ExecutionPolicy Bypass -File tools\install_windows_torch_retrainer.ps1
``` ```
По умолчанию Windows-расписание переобучает PyTorch `LSTM/GRU` каждые 6 часов с `--limit 3000` на парах `BTCUSDT,ETHUSDT,SOLUSDT,LTCUSDT`. Параметры можно переопределить через env: `TORCH_RETRAIN_SYMBOLS`, `TORCH_RETRAIN_LIMIT`, `TORCH_RETRAIN_LOOKBACKS`, `TORCH_RETRAIN_ARCHITECTURES`, `TORCH_RETRAIN_HIDDEN_SIZES`, `TORCH_RETRAIN_LAYERS`, `TORCH_RETRAIN_DROPOUTS`, `TORCH_RETRAIN_HORIZON`, `TORCH_RETRAIN_HORIZONS`, `TORCH_RETRAIN_CONTEXT_SYMBOLS`, `TORCH_RETRAIN_FEATURES`, `TORCH_RETRAIN_EPOCHS`, `TORCH_RETRAIN_PATIENCE`, `TORCH_RETRAIN_INTERVAL`, `TORCH_RETRAIN_ENV`. По умолчанию Windows-расписание переобучает PyTorch `LSTM/GRU` каждые 6 часов с `--limit 3000` на парах `BTCUSDT,ETHUSDT,SOLUSDT,LTCUSDT`. Параметры можно переопределить через env: `TORCH_RETRAIN_SYMBOLS`, `TORCH_RETRAIN_LIMIT`, `TORCH_RETRAIN_LOOKBACKS`, `TORCH_RETRAIN_ARCHITECTURES`, `TORCH_RETRAIN_HIDDEN_SIZES`, `TORCH_RETRAIN_LAYERS`, `TORCH_RETRAIN_DROPOUTS`, `TORCH_RETRAIN_HORIZON`, `TORCH_RETRAIN_HORIZONS`, `TORCH_RETRAIN_CONTEXT_SYMBOLS`, `TORCH_RETRAIN_FEATURES`, `TORCH_RETRAIN_SEED`, `TORCH_RETRAIN_EPOCHS`, `TORCH_RETRAIN_PATIENCE`, `TORCH_RETRAIN_INTERVAL`, `TORCH_RETRAIN_ENV`.
Если retrain запускается с `-DeployToPi`, после успешного guard он синхронизирует `runtime/lstm_forecaster.json`, `runtime/torch_retrain_guard.json` и `runtime/torch_threshold_calibration.json` на Raspberry Pi через SSH-ключ и перезапускает сервис `tradebot`. Отдельный запуск sync:
```powershell
powershell -ExecutionPolicy Bypass -File tools\sync_torch_artifacts_to_pi.ps1 -RemoteHost 192.168.0.185 -RemoteUser sevenhill -RemoteRoot /mnt/data/tradebot
```
Внутри recurrent модели используются exportable attention pooling и LayerNorm перед forecast-head; Raspberry Pi по-прежнему исполняет модель из JSON без PyTorch runtime. Внутри recurrent модели используются exportable attention pooling и LayerNorm перед forecast-head; Raspberry Pi по-прежнему исполняет модель из JSON без PyTorch runtime.
@@ -165,6 +171,10 @@ TIME_SERIES_MIN_CONFIDENCE=0.4
TIME_SERIES_MAX_ADJUSTMENT=0.08 TIME_SERIES_MAX_ADJUSTMENT=0.08
TIME_SERIES_LSTM_ENABLED=true TIME_SERIES_LSTM_ENABLED=true
TIME_SERIES_LSTM_MODEL_PATH=runtime/lstm_forecaster.json TIME_SERIES_LSTM_MODEL_PATH=runtime/lstm_forecaster.json
TIME_SERIES_PROBE_ENABLED=true
TIME_SERIES_PROBE_MIN_EDGE_PERCENT=0.02
TIME_SERIES_PROBE_MIN_PROBABILITY_UP=0.55
TIME_SERIES_PROBE_SIZE_MULTIPLIER=0.40
STOP_LOSS_PERCENT=0.04 STOP_LOSS_PERCENT=0.04
TAKE_PROFIT_PERCENT=0.035 TAKE_PROFIT_PERCENT=0.035
TRAILING_STOP_PERCENT=0.015 TRAILING_STOP_PERCENT=0.015
+8
View File
@@ -118,6 +118,10 @@ class Settings:
time_series_max_adjustment: float time_series_max_adjustment: float
time_series_lstm_enabled: bool time_series_lstm_enabled: bool
time_series_lstm_model_path: Path time_series_lstm_model_path: Path
time_series_probe_enabled: bool
time_series_probe_min_edge_percent: float
time_series_probe_min_probability_up: float
time_series_probe_size_multiplier: float
stop_loss_percent: float stop_loss_percent: float
take_profit_percent: float take_profit_percent: float
trailing_stop_percent: float trailing_stop_percent: float
@@ -266,6 +270,10 @@ def load_settings(env_file: str | Path | None = None) -> Settings:
time_series_max_adjustment=_float_env("TIME_SERIES_MAX_ADJUSTMENT", 0.08), time_series_max_adjustment=_float_env("TIME_SERIES_MAX_ADJUSTMENT", 0.08),
time_series_lstm_enabled=_bool_env("TIME_SERIES_LSTM_ENABLED", True), time_series_lstm_enabled=_bool_env("TIME_SERIES_LSTM_ENABLED", True),
time_series_lstm_model_path=Path(os.getenv("TIME_SERIES_LSTM_MODEL_PATH", "runtime/lstm_forecaster.json")), time_series_lstm_model_path=Path(os.getenv("TIME_SERIES_LSTM_MODEL_PATH", "runtime/lstm_forecaster.json")),
time_series_probe_enabled=_bool_env("TIME_SERIES_PROBE_ENABLED", True),
time_series_probe_min_edge_percent=_float_env("TIME_SERIES_PROBE_MIN_EDGE_PERCENT", 0.02),
time_series_probe_min_probability_up=_float_env("TIME_SERIES_PROBE_MIN_PROBABILITY_UP", 0.55),
time_series_probe_size_multiplier=_float_env("TIME_SERIES_PROBE_SIZE_MULTIPLIER", 0.40),
stop_loss_percent=_float_env("STOP_LOSS_PERCENT", 0.04), stop_loss_percent=_float_env("STOP_LOSS_PERCENT", 0.04),
take_profit_percent=_float_env("TAKE_PROFIT_PERCENT", 0.035), take_profit_percent=_float_env("TAKE_PROFIT_PERCENT", 0.035),
trailing_stop_percent=_float_env("TRAILING_STOP_PERCENT", 0.015), trailing_stop_percent=_float_env("TRAILING_STOP_PERCENT", 0.015),
+10 -1
View File
@@ -261,6 +261,10 @@ def _safe_config(settings: Settings) -> dict[str, Any]:
"time_series_max_adjustment": settings.time_series_max_adjustment, "time_series_max_adjustment": settings.time_series_max_adjustment,
"time_series_lstm_enabled": settings.time_series_lstm_enabled, "time_series_lstm_enabled": settings.time_series_lstm_enabled,
"time_series_lstm_model_path": str(settings.time_series_lstm_model_path), "time_series_lstm_model_path": str(settings.time_series_lstm_model_path),
"time_series_probe_enabled": settings.time_series_probe_enabled,
"time_series_probe_min_edge_percent": settings.time_series_probe_min_edge_percent,
"time_series_probe_min_probability_up": settings.time_series_probe_min_probability_up,
"time_series_probe_size_multiplier": settings.time_series_probe_size_multiplier,
"time_series_model_artifact": _time_series_model_artifact(settings), "time_series_model_artifact": _time_series_model_artifact(settings),
"stop_loss_percent": settings.stop_loss_percent, "stop_loss_percent": settings.stop_loss_percent,
"take_profit_percent": settings.take_profit_percent, "take_profit_percent": settings.take_profit_percent,
@@ -678,7 +682,8 @@ HTML = r"""
['Horizon', String(artifact.target_horizon ?? '')], ['Horizon', String(artifact.target_horizon ?? '')],
['Min edge', `${num(config?.time_series_min_edge_percent, 3)}%`], ['Min edge', `${num(config?.time_series_min_edge_percent, 3)}%`],
['Min P(up)', `${num((config?.time_series_min_probability_up || 0) * 100, 1)}%`], ['Min P(up)', `${num((config?.time_series_min_probability_up || 0) * 100, 1)}%`],
['Min confidence', num(config?.time_series_min_confidence, 3)] ['Min confidence', num(config?.time_series_min_confidence, 3)],
['Probe entry', config?.time_series_probe_enabled ? `${num(config?.time_series_probe_min_edge_percent, 3)}% / P ${num((config?.time_series_probe_min_probability_up || 0) * 100, 1)}% / size ${num((config?.time_series_probe_size_multiplier || 0) * 100, 0)}%` : 'off']
]))} ]))}
</div> </div>
${positionsPanel()} ${positionsPanel()}
@@ -699,6 +704,9 @@ HTML = r"""
const quality = market.quality || {}; const quality = market.quality || {};
const signal = latestSignals[symbol] || {}; const signal = latestSignals[symbol] || {};
const minEdge = state.data.config?.time_series_min_edge_percent ?? 0; const minEdge = state.data.config?.time_series_min_edge_percent ?? 0;
const probeEnabled = Boolean(state.data.config?.time_series_probe_enabled);
const probeEdge = state.data.config?.time_series_probe_min_edge_percent ?? 0;
const probeProbability = state.data.config?.time_series_probe_min_probability_up ?? 0;
const minConfidence = state.data.config?.time_series_min_confidence ?? 0; const minConfidence = state.data.config?.time_series_min_confidence ?? 0;
const diagnostics = parseDiagnostics(signal); const diagnostics = parseDiagnostics(signal);
const failed = Object.entries(diagnostics.checks || {}).filter(([, ok]) => !ok).map(([key]) => key); const failed = Object.entries(diagnostics.checks || {}).filter(([, ok]) => !ok).map(([key]) => key);
@@ -716,6 +724,7 @@ HTML = r"""
['Model', modelName(forecast.model)], ['Model', modelName(forecast.model)],
['Edge', `${signed(forecast.expected_return_percent, 4)}% / min ${num(minEdge, 3)}%`], ['Edge', `${signed(forecast.expected_return_percent, 4)}% / min ${num(minEdge, 3)}%`],
['P(up)', num((forecast.probability_up || 0) * 100, 2) + '%'], ['P(up)', num((forecast.probability_up || 0) * 100, 2) + '%'],
['Probe', probeEnabled ? `${num(probeEdge, 3)}% / P ${num(probeProbability * 100, 1)}% / ${diagnostics.edge_mode || 'n/a'}` : 'off'],
['Confidence', `${num(signal.confidence, 4)} / min ${num(minConfidence, 2)}`], ['Confidence', `${num(signal.confidence, 4)} / min ${num(minConfidence, 2)}`],
['Q10/Q50/Q90', `${signed(forecast.quantile_10_percent, 2)} / ${signed(forecast.quantile_50_percent, 2)} / ${signed(forecast.quantile_90_percent, 2)}`], ['Q10/Q50/Q90', `${signed(forecast.quantile_10_percent, 2)} / ${signed(forecast.quantile_50_percent, 2)} / ${signed(forecast.quantile_90_percent, 2)}`],
['Blocked', forecast.block_entry ? 'yes' : 'no'] ['Blocked', forecast.block_entry ? 'yes' : 'no']
+35 -2
View File
@@ -633,6 +633,34 @@ def _torch_forecast_entry_signal(
skill = _safe_float(forecast.get("skill"), 0.0) skill = _safe_float(forecast.get("skill"), 0.0)
min_edge = max(0.0, settings.time_series_min_edge_percent) min_edge = max(0.0, settings.time_series_min_edge_percent)
min_probability = _torch_min_probability(settings) min_probability = _torch_min_probability(settings)
probe_min_edge = max(0.0, min(settings.time_series_probe_min_edge_percent, min_edge))
probe_min_probability = round(
_clamp(settings.time_series_probe_min_probability_up, min_probability, 0.85),
4,
)
full_edge_ok = expected_return >= min_edge
probe_edge_ok = bool(
settings.time_series_probe_enabled
and not full_edge_ok
and expected_return >= probe_min_edge
and probability_up >= probe_min_probability
)
edge_mode = "full" if full_edge_ok else ("probe" if probe_edge_ok else "blocked")
if probe_edge_ok and position_notional > 0:
probe_multiplier = _clamp(settings.time_series_probe_size_multiplier, 0.05, 1.0)
position_notional = round(
min(
settings.max_position_usdt,
max(settings.min_position_usdt, position_notional * probe_multiplier),
),
2,
)
sizing = {
**sizing,
"notional_usdt": position_notional,
"probe_size_multiplier": round(probe_multiplier, 4),
"edge_mode": "probe",
}
confidence = _torch_forecast_confidence(settings, forecast) confidence = _torch_forecast_confidence(settings, forecast)
spread_ok = ticker.spread_percent <= settings.max_spread_percent spread_ok = ticker.spread_percent <= settings.max_spread_percent
liquidity_ok = ticker.turnover_24h >= settings.min_24h_turnover_usdt liquidity_ok = ticker.turnover_24h >= settings.min_24h_turnover_usdt
@@ -641,7 +669,7 @@ def _torch_forecast_entry_signal(
"torch_model_ok": model_ok, "torch_model_ok": model_ok,
"forecast_usable": bool(forecast.get("usable", False)), "forecast_usable": bool(forecast.get("usable", False)),
"forecast_not_blocked": not bool(forecast.get("block_entry", False)), "forecast_not_blocked": not bool(forecast.get("block_entry", False)),
"expected_edge_ok": expected_return >= min_edge, "expected_edge_ok": full_edge_ok or probe_edge_ok,
"probability_ok": probability_up >= min_probability, "probability_ok": probability_up >= min_probability,
"skill_ok": skill > 0.0, "skill_ok": skill > 0.0,
"confidence_ok": confidence >= settings.time_series_min_confidence, "confidence_ok": confidence >= settings.time_series_min_confidence,
@@ -659,6 +687,10 @@ def _torch_forecast_entry_signal(
"atr_trailing_multiplier": _clamp(settings.atr_trailing_multiplier, 0.5, 10.0), "atr_trailing_multiplier": _clamp(settings.atr_trailing_multiplier, 0.5, 10.0),
"expected_return_percent": expected_return, "expected_return_percent": expected_return,
"min_edge_percent": min_edge, "min_edge_percent": min_edge,
"probe_enabled": settings.time_series_probe_enabled,
"probe_min_edge_percent": probe_min_edge,
"probe_min_probability_up": probe_min_probability,
"edge_mode": edge_mode,
"probability_up": probability_up, "probability_up": probability_up,
"min_probability_up": min_probability, "min_probability_up": min_probability,
"min_confidence": settings.time_series_min_confidence, "min_confidence": settings.time_series_min_confidence,
@@ -679,7 +711,8 @@ def _torch_forecast_entry_signal(
( (
"torch_forecast: PyTorch edge confirmed; " "torch_forecast: PyTorch edge confirmed; "
f"model={forecast.get('model')}, p_up={probability_up:.3f}, " f"model={forecast.get('model')}, p_up={probability_up:.3f}, "
f"expected={expected_return:.4f}%, size={position_notional:.2f} USDT" f"expected={expected_return:.4f}%, edge_mode={edge_mode}, "
f"size={position_notional:.2f} USDT"
), ),
diagnostics, diagnostics,
) )
File diff suppressed because it is too large Load Diff
+231 -312
View File
@@ -1,7 +1,7 @@
{ {
"artifact": { "artifact": {
"version": 4, "version": 4,
"created_at": "2026-06-23T19:07:54.434411+00:00", "created_at": "2026-06-24T18:25:26.346861+00:00",
"feature_count": 55, "feature_count": 55,
"target_horizon": 3, "target_horizon": 3,
"target_horizons": [ "target_horizons": [
@@ -16,28 +16,28 @@
"model": "torch_gru", "model": "torch_gru",
"lookback": 64, "lookback": 64,
"hidden_size": 96, "hidden_size": 96,
"skill": 0.15903346077183758, "skill": 0.17771573949336475,
"directional_accuracy": 0.725 "directional_accuracy": 0.7166666666666667
}, },
"ETHUSDT": { "ETHUSDT": {
"model": "torch_gru", "model": "torch_lstm",
"lookback": 64, "lookback": 64,
"hidden_size": 64, "hidden_size": 64,
"skill": 0.09273757527902074, "skill": 0.08854869730624494,
"directional_accuracy": 0.6916666666666667 "directional_accuracy": 0.6916666666666667
}, },
"SOLUSDT": { "SOLUSDT": {
"model": "torch_gru", "model": "torch_lstm",
"lookback": 64, "lookback": 64,
"hidden_size": 96, "hidden_size": 64,
"skill": 0.03400498728351002, "skill": 0.022567259691807694,
"directional_accuracy": 0.6416666666666667 "directional_accuracy": 0.6166666666666667
}, },
"LTCUSDT": { "LTCUSDT": {
"model": "torch_gru", "model": "torch_lstm",
"lookback": 64, "lookback": 64,
"hidden_size": 96, "hidden_size": 64,
"skill": 0.11954702418314447, "skill": 0.106450769315762,
"directional_accuracy": 0.6583333333333333 "directional_accuracy": 0.6583333333333333
} }
} }
@@ -50,142 +50,75 @@
}, },
"recommended": { "recommended": {
"edge": 0.1, "edge": 0.1,
"probability": 0.6, "probability": 0.59,
"confidence": 0.68, "confidence": 0.72,
"trades": 16, "trades": 1,
"wins": 9, "wins": 1,
"win_rate": 0.5625, "win_rate": 1.0,
"total_net_percent": 7.917141995065847, "total_net_percent": 0.09543520096249036,
"average_net_percent": 0.4948213746916154, "average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 1.812991648733242, "max_drawdown_percent": 0.0,
"profit_factor": 3.629214989861449, "profit_factor": 999.0,
"score": 0.5816887551556058 "score": 0.04938446142797822
}, },
"full_replay": { "full_replay": {
"trades": 5, "trades": 1,
"wins": 5, "wins": 1,
"win_rate": 1.0, "win_rate": 1.0,
"total_net_percent": 9.5746, "total_net_percent": 0.0113,
"avg_net_percent": 1.9149, "avg_net_percent": 0.0113,
"max_drawdown_percent": 0.0, "max_drawdown_percent": 0.0,
"profit_factor": 999.0, "profit_factor": 999.0,
"trades_detail": [ "trades_detail": [
{ {
"symbol": "ETHUSDT", "symbol": "ETHUSDT",
"entry_timestamp": 1779940800000, "entry_timestamp": 1780462800000,
"exit_timestamp": 1779962400000, "exit_timestamp": 1780470000000,
"net_percent": 0.1545, "net_percent": 0.0113,
"reason": "forecast_weak_profit_lock", "reason": "forecast_weak_profit_lock",
"held_bars": 6, "held_bars": 2,
"entry_probability": 0.6018, "entry_probability": 0.5905,
"entry_expected_percent": 0.3155 "entry_expected_percent": 0.3024
},
{
"symbol": "ETHUSDT",
"entry_timestamp": 1780300800000,
"exit_timestamp": 1780336800000,
"net_percent": 0.1707,
"reason": "forecast_weak_profit_lock",
"held_bars": 10,
"entry_probability": 0.6164,
"entry_expected_percent": 0.3215
},
{
"symbol": "ETHUSDT",
"entry_timestamp": 1781154000000,
"exit_timestamp": 1781157600000,
"net_percent": 0.1476,
"reason": "forecast_weak_profit_lock",
"held_bars": 1,
"entry_probability": 0.6009,
"entry_expected_percent": 0.3352
},
{
"symbol": "ETHUSDT",
"entry_timestamp": 1781161200000,
"exit_timestamp": 1781164800000,
"net_percent": 0.0713,
"reason": "forecast_weak_profit_lock",
"held_bars": 1,
"entry_probability": 0.6004,
"entry_expected_percent": 0.3017
},
{
"symbol": "ETHUSDT",
"entry_timestamp": 1781445600000,
"exit_timestamp": 1781532000000,
"net_percent": 9.0305,
"reason": "max_hold",
"held_bars": 24,
"entry_probability": 0.6014,
"entry_expected_percent": 0.2946
} }
] ]
}, },
"walk_forward": { "walk_forward": {
"summary": { "summary": {
"trades": 15, "trades": 0,
"wins": 8, "wins": 0,
"win_rate": 0.5333, "win_rate": 0.0,
"total_net_percent": 7.1747, "total_net_percent": 0,
"avg_net_percent": 0.4783, "avg_net_percent": 0.0,
"max_drawdown_percent": 1.3024, "max_drawdown_percent": 0.0,
"profit_factor": 3.471, "profit_factor": 0.0,
"status": "ok" "status": "warn"
}, },
"folds": [ "folds": [
{
"fold": 1,
"train_records": 720,
"test_records": 720,
"thresholds": {
"edge": 0.1,
"probability": 0.6,
"confidence": 0.72,
"trades": 2,
"wins": 1,
"win_rate": 0.5,
"total_net_percent": -0.10348384852443271,
"average_net_percent": -0.051741924262216354,
"max_drawdown_percent": 0.5106090484004788,
"profit_factor": 0.7973325211360753,
"score": -0.014117837267286302
},
"test": {
"trades": 4,
"wins": 2,
"win_rate": 0.5,
"total_net_percent": 0.0557,
"avg_net_percent": 0.0139,
"max_drawdown_percent": 1.3024,
"profit_factor": 1.0428
}
},
{ {
"fold": 2, "fold": 2,
"train_records": 1440, "train_records": 1440,
"test_records": 720, "test_records": 720,
"thresholds": { "thresholds": {
"edge": 0.1, "edge": 0.1,
"probability": 0.55, "probability": 0.59,
"confidence": 0.72, "confidence": 0.72,
"trades": 13, "trades": 1,
"wins": 8, "wins": 1,
"win_rate": 0.6153846153846154, "win_rate": 1.0,
"total_net_percent": 2.3573472403187523, "total_net_percent": 0.09543520096249036,
"average_net_percent": 0.1813344031014425, "average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 1.812991648733242, "max_drawdown_percent": 0.0,
"profit_factor": 2.0042930935847436, "profit_factor": 999.0,
"score": 0.18692024685961114 "score": 0.05733739484151908
}, },
"test": { "test": {
"trades": 11, "trades": 0,
"wins": 6, "wins": 0,
"win_rate": 0.5455, "win_rate": 0.0,
"total_net_percent": 7.119, "total_net_percent": 0,
"avg_net_percent": 0.6472, "avg_net_percent": 0.0,
"max_drawdown_percent": 1.0894, "max_drawdown_percent": 0.0,
"profit_factor": 5.4462 "profit_factor": 0.0
} }
}, },
{ {
@@ -194,16 +127,16 @@
"test_records": 720, "test_records": 720,
"thresholds": { "thresholds": {
"edge": 0.1, "edge": 0.1,
"probability": 0.62, "probability": 0.59,
"confidence": 0.72, "confidence": 0.72,
"trades": 9, "trades": 1,
"wins": 3, "wins": 1,
"win_rate": 0.3333333333333333, "win_rate": 1.0,
"total_net_percent": 7.33207561878414, "total_net_percent": 0.09543520096249036,
"average_net_percent": 0.8146750687537934, "average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 1.4887951136316468, "max_drawdown_percent": 0.0,
"profit_factor": 3.9118496582300852, "profit_factor": 999.0,
"score": 0.8933256829599393 "score": 0.05733739484151908
}, },
"test": { "test": {
"trades": 0, "trades": 0,
@@ -220,266 +153,252 @@
"probability_calibration": { "probability_calibration": {
"samples": 2880, "samples": 2880,
"buckets": [ "buckets": [
{
"bucket": "0.25-0.30",
"samples": 25,
"avg_probability": 0.2947,
"actual_win_rate": 0.12,
"avg_future_net_percent": -0.6611
},
{ {
"bucket": "0.30-0.35", "bucket": "0.30-0.35",
"samples": 38, "samples": 87,
"avg_probability": 0.336, "avg_probability": 0.3257,
"actual_win_rate": 0.1053, "actual_win_rate": 0.1379,
"avg_future_net_percent": -0.6575 "avg_future_net_percent": -0.5883
}, },
{ {
"bucket": "0.35-0.40", "bucket": "0.35-0.40",
"samples": 418, "samples": 276,
"avg_probability": 0.3839, "avg_probability": 0.3787,
"actual_win_rate": 0.2225, "actual_win_rate": 0.221,
"avg_future_net_percent": -0.6359 "avg_future_net_percent": -0.5378
}, },
{ {
"bucket": "0.40-0.45", "bucket": "0.40-0.45",
"samples": 1065, "samples": 1720,
"avg_probability": 0.427, "avg_probability": 0.4263,
"actual_win_rate": 0.2873, "actual_win_rate": 0.3203,
"avg_future_net_percent": -0.4036 "avg_future_net_percent": -0.3469
}, },
{ {
"bucket": "0.45-0.50", "bucket": "0.45-0.50",
"samples": 911, "samples": 629,
"avg_probability": 0.4746, "avg_probability": 0.4701,
"actual_win_rate": 0.3271, "actual_win_rate": 0.31,
"avg_future_net_percent": -0.3061 "avg_future_net_percent": -0.4239
}, },
{ {
"bucket": "0.50-0.55", "bucket": "0.50-0.55",
"samples": 290, "samples": 111,
"avg_probability": 0.5188, "avg_probability": 0.5223,
"actual_win_rate": 0.3966, "actual_win_rate": 0.4414,
"avg_future_net_percent": -0.0583 "avg_future_net_percent": 0.252
}, },
{ {
"bucket": "0.55-0.60", "bucket": "0.55-0.60",
"samples": 104, "samples": 32,
"avg_probability": 0.5758, "avg_probability": 0.5658,
"actual_win_rate": 0.4327, "actual_win_rate": 0.375,
"avg_future_net_percent": 0.0173 "avg_future_net_percent": -0.2001
},
{
"bucket": "0.60-0.65",
"samples": 42,
"avg_probability": 0.6138,
"actual_win_rate": 0.4762,
"avg_future_net_percent": -0.0041
},
{
"bucket": "0.65-0.70",
"samples": 6,
"avg_probability": 0.6679,
"actual_win_rate": 0.3333,
"avg_future_net_percent": 0.6587
},
{
"bucket": "0.70-0.75",
"samples": 6,
"avg_probability": 0.7103,
"actual_win_rate": 0.8333,
"avg_future_net_percent": 2.1268
} }
] ]
}, },
"top_results": [ "top_results": [
{ {
"edge": 0.1, "edge": 0.1,
"probability": 0.62, "probability": 0.59,
"confidence": 0.72, "confidence": 0.72,
"trades": 9, "trades": 1,
"wins": 3, "wins": 1,
"win_rate": 0.3333333333333333, "win_rate": 1.0,
"total_net_percent": 7.33207561878414, "total_net_percent": 0.09543520096249036,
"average_net_percent": 0.8146750687537934, "average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 1.4887951136316468, "max_drawdown_percent": 0.0,
"profit_factor": 3.9118496582300852, "profit_factor": 999.0,
"score": 0.689656915771491 "score": 0.04938446142797822
}, },
{ {
"edge": 0.1, "edge": 0.1,
"probability": 0.62, "probability": 0.59,
"confidence": 0.68, "confidence": 0.68,
"trades": 9, "trades": 1,
"wins": 3, "wins": 1,
"win_rate": 0.3333333333333333, "win_rate": 1.0,
"total_net_percent": 7.33207561878414, "total_net_percent": 0.09543520096249036,
"average_net_percent": 0.8146750687537934, "average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 1.4887951136316468, "max_drawdown_percent": 0.0,
"profit_factor": 3.9118496582300852, "profit_factor": 999.0,
"score": 0.689656915771491 "score": 0.04938446142797822
}, },
{ {
"edge": 0.1, "edge": 0.1,
"probability": 0.62, "probability": 0.59,
"confidence": 0.64, "confidence": 0.64,
"trades": 9, "trades": 1,
"wins": 3, "wins": 1,
"win_rate": 0.3333333333333333, "win_rate": 1.0,
"total_net_percent": 7.33207561878414, "total_net_percent": 0.09543520096249036,
"average_net_percent": 0.8146750687537934, "average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 1.4887951136316468, "max_drawdown_percent": 0.0,
"profit_factor": 3.9118496582300852, "profit_factor": 999.0,
"score": 0.689656915771491 "score": 0.04938446142797822
}, },
{ {
"edge": 0.1, "edge": 0.1,
"probability": 0.62, "probability": 0.59,
"confidence": 0.6, "confidence": 0.6,
"trades": 9, "trades": 1,
"wins": 3, "wins": 1,
"win_rate": 0.3333333333333333, "win_rate": 1.0,
"total_net_percent": 7.33207561878414, "total_net_percent": 0.09543520096249036,
"average_net_percent": 0.8146750687537934, "average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 1.4887951136316468, "max_drawdown_percent": 0.0,
"profit_factor": 3.9118496582300852, "profit_factor": 999.0,
"score": 0.689656915771491 "score": 0.04938446142797822
}, },
{ {
"edge": 0.1, "edge": 0.1,
"probability": 0.62, "probability": 0.59,
"confidence": 0.56, "confidence": 0.56,
"trades": 9, "trades": 1,
"wins": 3, "wins": 1,
"win_rate": 0.3333333333333333, "win_rate": 1.0,
"total_net_percent": 7.33207561878414, "total_net_percent": 0.09543520096249036,
"average_net_percent": 0.8146750687537934, "average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 1.4887951136316468, "max_drawdown_percent": 0.0,
"profit_factor": 3.9118496582300852, "profit_factor": 999.0,
"score": 0.689656915771491 "score": 0.04938446142797822
}, },
{ {
"edge": 0.1, "edge": 0.1,
"probability": 0.62, "probability": 0.59,
"confidence": 0.5, "confidence": 0.5,
"trades": 9, "trades": 1,
"wins": 3, "wins": 1,
"win_rate": 0.3333333333333333, "win_rate": 1.0,
"total_net_percent": 7.33207561878414, "total_net_percent": 0.09543520096249036,
"average_net_percent": 0.8146750687537934, "average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 1.4887951136316468, "max_drawdown_percent": 0.0,
"profit_factor": 3.9118496582300852, "profit_factor": 999.0,
"score": 0.689656915771491 "score": 0.04938446142797822
}, },
{ {
"edge": 0.08, "edge": 0.08,
"probability": 0.62, "probability": 0.59,
"confidence": 0.72, "confidence": 0.72,
"trades": 9, "trades": 1,
"wins": 3, "wins": 1,
"win_rate": 0.3333333333333333, "win_rate": 1.0,
"total_net_percent": 7.33207561878414, "total_net_percent": 0.09543520096249036,
"average_net_percent": 0.8146750687537934, "average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 1.4887951136316468, "max_drawdown_percent": 0.0,
"profit_factor": 3.9118496582300852, "profit_factor": 999.0,
"score": 0.689656915771491 "score": 0.04938446142797822
}, },
{ {
"edge": 0.08, "edge": 0.08,
"probability": 0.62, "probability": 0.59,
"confidence": 0.68, "confidence": 0.68,
"trades": 9, "trades": 1,
"wins": 3, "wins": 1,
"win_rate": 0.3333333333333333, "win_rate": 1.0,
"total_net_percent": 7.33207561878414, "total_net_percent": 0.09543520096249036,
"average_net_percent": 0.8146750687537934, "average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 1.4887951136316468, "max_drawdown_percent": 0.0,
"profit_factor": 3.9118496582300852, "profit_factor": 999.0,
"score": 0.689656915771491 "score": 0.04938446142797822
}, },
{ {
"edge": 0.08, "edge": 0.08,
"probability": 0.62, "probability": 0.59,
"confidence": 0.64, "confidence": 0.64,
"trades": 9, "trades": 1,
"wins": 3, "wins": 1,
"win_rate": 0.3333333333333333, "win_rate": 1.0,
"total_net_percent": 7.33207561878414, "total_net_percent": 0.09543520096249036,
"average_net_percent": 0.8146750687537934, "average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 1.4887951136316468, "max_drawdown_percent": 0.0,
"profit_factor": 3.9118496582300852, "profit_factor": 999.0,
"score": 0.689656915771491 "score": 0.04938446142797822
}, },
{ {
"edge": 0.08, "edge": 0.08,
"probability": 0.62, "probability": 0.59,
"confidence": 0.6, "confidence": 0.6,
"trades": 9, "trades": 1,
"wins": 3, "wins": 1,
"win_rate": 0.3333333333333333, "win_rate": 1.0,
"total_net_percent": 7.33207561878414, "total_net_percent": 0.09543520096249036,
"average_net_percent": 0.8146750687537934, "average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 1.4887951136316468, "max_drawdown_percent": 0.0,
"profit_factor": 3.9118496582300852, "profit_factor": 999.0,
"score": 0.689656915771491 "score": 0.04938446142797822
}, },
{ {
"edge": 0.08, "edge": 0.08,
"probability": 0.62, "probability": 0.59,
"confidence": 0.56, "confidence": 0.56,
"trades": 9, "trades": 1,
"wins": 3, "wins": 1,
"win_rate": 0.3333333333333333, "win_rate": 1.0,
"total_net_percent": 7.33207561878414, "total_net_percent": 0.09543520096249036,
"average_net_percent": 0.8146750687537934, "average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 1.4887951136316468, "max_drawdown_percent": 0.0,
"profit_factor": 3.9118496582300852, "profit_factor": 999.0,
"score": 0.689656915771491 "score": 0.04938446142797822
}, },
{ {
"edge": 0.08, "edge": 0.08,
"probability": 0.62, "probability": 0.59,
"confidence": 0.5, "confidence": 0.5,
"trades": 9, "trades": 1,
"wins": 3, "wins": 1,
"win_rate": 0.3333333333333333, "win_rate": 1.0,
"total_net_percent": 7.33207561878414, "total_net_percent": 0.09543520096249036,
"average_net_percent": 0.8146750687537934, "average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 1.4887951136316468, "max_drawdown_percent": 0.0,
"profit_factor": 3.9118496582300852, "profit_factor": 999.0,
"score": 0.689656915771491 "score": 0.04938446142797822
}, },
{ {
"edge": 0.06, "edge": 0.06,
"probability": 0.62, "probability": 0.59,
"confidence": 0.72, "confidence": 0.72,
"trades": 9, "trades": 1,
"wins": 3, "wins": 1,
"win_rate": 0.3333333333333333, "win_rate": 1.0,
"total_net_percent": 7.33207561878414, "total_net_percent": 0.09543520096249036,
"average_net_percent": 0.8146750687537934, "average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 1.4887951136316468, "max_drawdown_percent": 0.0,
"profit_factor": 3.9118496582300852, "profit_factor": 999.0,
"score": 0.689656915771491 "score": 0.04938446142797822
}, },
{ {
"edge": 0.06, "edge": 0.06,
"probability": 0.62, "probability": 0.59,
"confidence": 0.68, "confidence": 0.68,
"trades": 9, "trades": 1,
"wins": 3, "wins": 1,
"win_rate": 0.3333333333333333, "win_rate": 1.0,
"total_net_percent": 7.33207561878414, "total_net_percent": 0.09543520096249036,
"average_net_percent": 0.8146750687537934, "average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 1.4887951136316468, "max_drawdown_percent": 0.0,
"profit_factor": 3.9118496582300852, "profit_factor": 999.0,
"score": 0.689656915771491 "score": 0.04938446142797822
}, },
{ {
"edge": 0.06, "edge": 0.06,
"probability": 0.62, "probability": 0.59,
"confidence": 0.64, "confidence": 0.64,
"trades": 9, "trades": 1,
"wins": 3, "wins": 1,
"win_rate": 0.3333333333333333, "win_rate": 1.0,
"total_net_percent": 7.33207561878414, "total_net_percent": 0.09543520096249036,
"average_net_percent": 0.8146750687537934, "average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 1.4887951136316468, "max_drawdown_percent": 0.0,
"profit_factor": 3.9118496582300852, "profit_factor": 999.0,
"score": 0.689656915771491 "score": 0.04938446142797822
} }
] ]
} }
+87 -86
View File
@@ -1,95 +1,59 @@
{ {
"accepted": true, "accepted": false,
"reason": "candidate_passed_guard", "reason": "candidate_has_too_few_full_replay_trades",
"current": { "current": {
"score": 0.0, "score": 3.097468,
"recommended": {},
"full_replay": {},
"walk_forward_summary": {}
},
"candidate": {
"score": 3.499028,
"recommended": { "recommended": {
"edge": 0.1, "edge": 0.1,
"probability": 0.52, "probability": 0.6,
"confidence": 0.72, "confidence": 0.68,
"trades": 30, "trades": 16,
"wins": 17, "wins": 9,
"win_rate": 0.5666666666666667, "win_rate": 0.5625,
"total_net_percent": 12.82679871911413, "total_net_percent": 7.917141995065847,
"average_net_percent": 0.42755995730380436, "average_net_percent": 0.4948213746916154,
"max_drawdown_percent": 1.812991648733242, "max_drawdown_percent": 1.812991648733242,
"profit_factor": 3.2963631842987433, "profit_factor": 3.629214989861449,
"score": 0.5882388552951857 "score": 0.5816887551556058
}, },
"full_replay": { "full_replay": {
"trades": 8, "trades": 4,
"wins": 8, "wins": 4,
"win_rate": 1.0, "win_rate": 1.0,
"total_net_percent": 21.0484, "total_net_percent": 9.8223,
"avg_net_percent": 2.631, "avg_net_percent": 2.4556,
"max_drawdown_percent": 0.0, "max_drawdown_percent": 0.0,
"profit_factor": 999.0, "profit_factor": 999.0,
"trades_detail": [ "trades_detail": [
{
"symbol": "ETHUSDT",
"entry_timestamp": 1779832800000,
"exit_timestamp": 1779868800000,
"net_percent": 0.5281,
"reason": "forecast_weak_profit_lock",
"held_bars": 10,
"entry_probability": 0.5747,
"entry_expected_percent": 0.3453
},
{ {
"symbol": "ETHUSDT", "symbol": "ETHUSDT",
"entry_timestamp": 1779940800000, "entry_timestamp": 1779940800000,
"exit_timestamp": 1779984000000, "exit_timestamp": 1779962400000,
"net_percent": 1.3185, "net_percent": 0.1545,
"reason": "forecast_weak_profit_lock", "reason": "forecast_weak_profit_lock",
"held_bars": 12, "held_bars": 6,
"entry_probability": 0.6018, "entry_probability": 0.6036,
"entry_expected_percent": 0.3155 "entry_expected_percent": 0.3223
}, },
{ {
"symbol": "ETHUSDT", "symbol": "ETHUSDT",
"entry_timestamp": 1780300800000, "entry_timestamp": 1780300800000,
"exit_timestamp": 1780347600000, "exit_timestamp": 1780336800000,
"net_percent": 0.2591, "net_percent": 0.1707,
"reason": "forecast_weak_profit_lock", "reason": "forecast_weak_profit_lock",
"held_bars": 13, "held_bars": 10,
"entry_probability": 0.6164, "entry_probability": 0.6181,
"entry_expected_percent": 0.3215 "entry_expected_percent": 0.3255
}, },
{ {
"symbol": "ETHUSDT", "symbol": "ETHUSDT",
"entry_timestamp": 1780768800000, "entry_timestamp": 1781154000000,
"exit_timestamp": 1780855200000, "exit_timestamp": 1781164800000,
"net_percent": 4.4581, "net_percent": 0.4665,
"reason": "max_hold",
"held_bars": 24,
"entry_probability": 0.5632,
"entry_expected_percent": 0.5685
},
{
"symbol": "ETHUSDT",
"entry_timestamp": 1780862400000,
"exit_timestamp": 1780869600000,
"net_percent": 2.4609,
"reason": "forecast_weak_profit_lock", "reason": "forecast_weak_profit_lock",
"held_bars": 2, "held_bars": 3,
"entry_probability": 0.5368, "entry_probability": 0.6017,
"entry_expected_percent": 0.4055 "entry_expected_percent": 0.3383
},
{
"symbol": "ETHUSDT",
"entry_timestamp": 1781139600000,
"exit_timestamp": 1781204400000,
"net_percent": 2.0707,
"reason": "forecast_weak_profit_lock",
"held_bars": 18,
"entry_probability": 0.5775,
"entry_expected_percent": 0.3013
}, },
{ {
"symbol": "ETHUSDT", "symbol": "ETHUSDT",
@@ -98,29 +62,66 @@
"net_percent": 9.0305, "net_percent": 9.0305,
"reason": "max_hold", "reason": "max_hold",
"held_bars": 24, "held_bars": 24,
"entry_probability": 0.6014, "entry_probability": 0.6028,
"entry_expected_percent": 0.2946 "entry_expected_percent": 0.2963
},
{
"symbol": "ETHUSDT",
"entry_timestamp": 1781892000000,
"exit_timestamp": 1781942400000,
"net_percent": 0.9224,
"reason": "forecast_weak_profit_lock",
"held_bars": 14,
"entry_probability": 0.5966,
"entry_expected_percent": 0.2647
} }
] ]
}, },
"walk_forward_summary": { "walk_forward_summary": {
"trades": 16, "trades": 12,
"wins": 8, "wins": 6,
"win_rate": 0.5, "win_rate": 0.5,
"total_net_percent": 6.8682, "total_net_percent": 8.0551,
"avg_net_percent": 0.4293, "avg_net_percent": 0.6713,
"max_drawdown_percent": 1.3024, "max_drawdown_percent": 1.1326,
"profit_factor": 3.1396, "profit_factor": 4.7976,
"status": "ok"
}
},
"candidate": {
"score": 0.323872,
"recommended": {
"edge": 0.1,
"probability": 0.59,
"confidence": 0.72,
"trades": 1,
"wins": 1,
"win_rate": 1.0,
"total_net_percent": 0.09543520096249036,
"average_net_percent": 0.09543520096249036,
"max_drawdown_percent": 0.0,
"profit_factor": 999.0,
"score": 0.04938446142797822
},
"full_replay": {
"trades": 1,
"wins": 1,
"win_rate": 1.0,
"total_net_percent": 0.0113,
"avg_net_percent": 0.0113,
"max_drawdown_percent": 0.0,
"profit_factor": 999.0,
"trades_detail": [
{
"symbol": "ETHUSDT",
"entry_timestamp": 1780462800000,
"exit_timestamp": 1780470000000,
"net_percent": 0.0113,
"reason": "forecast_weak_profit_lock",
"held_bars": 2,
"entry_probability": 0.5905,
"entry_expected_percent": 0.3024
}
]
},
"walk_forward_summary": {
"trades": 0,
"wins": 0,
"win_rate": 0.0,
"total_net_percent": 0,
"avg_net_percent": 0.0,
"max_drawdown_percent": 0.0,
"profit_factor": 0.0,
"status": "warn" "status": "warn"
} }
} }
+4
View File
@@ -83,6 +83,10 @@ def make_settings():
time_series_max_adjustment=0.08, time_series_max_adjustment=0.08,
time_series_lstm_enabled=True, time_series_lstm_enabled=True,
time_series_lstm_model_path=tmp_path / "lstm_forecaster.json", time_series_lstm_model_path=tmp_path / "lstm_forecaster.json",
time_series_probe_enabled=True,
time_series_probe_min_edge_percent=0.02,
time_series_probe_min_probability_up=0.55,
time_series_probe_size_multiplier=0.40,
stop_loss_percent=0.02, stop_loss_percent=0.02,
take_profit_percent=0.035, take_profit_percent=0.035,
trailing_stop_percent=0.015, trailing_stop_percent=0.015,
+4
View File
@@ -39,6 +39,10 @@ def test_safe_config_summarizes_torch_forecast_artifact(make_settings, tmp_path)
config = _safe_config(settings) config = _safe_config(settings)
assert config["time_series_probe_enabled"] is True
assert config["time_series_probe_min_edge_percent"] == 0.02
assert config["time_series_probe_min_probability_up"] == 0.55
assert config["time_series_probe_size_multiplier"] == 0.40
assert config["time_series_model_artifact"] == { assert config["time_series_model_artifact"] == {
"available": True, "available": True,
"type": "pytorch_recurrent_forecaster", "type": "pytorch_recurrent_forecaster",
+74
View File
@@ -284,6 +284,80 @@ def test_torch_forecast_blocks_without_valid_torch_model(make_settings, tmp_path
assert signal.diagnostics["checks"]["torch_model_ok"] is False assert signal.diagnostics["checks"]["torch_model_ok"] is False
def test_torch_forecast_probe_buys_on_positive_high_probability(make_settings, tmp_path) -> None:
settings = make_settings(
tmp_path,
strategy_mode="torch_forecast",
time_series_min_edge_percent=0.10,
time_series_min_probability_up=0.52,
time_series_probe_enabled=True,
time_series_probe_min_edge_percent=0.02,
time_series_probe_min_probability_up=0.55,
time_series_probe_size_multiplier=0.40,
max_position_usdt=25,
stop_loss_percent=0.04,
risk_per_trade_percent=0.01,
)
strategy = SpotStrategy(settings)
ticker = Ticker("SOLUSDT", 65, 64.99, 65.01, 10_000_000, 1000, 1.0)
signal = strategy.entry_signal(
"SOLUSDT",
[],
ticker,
open_positions_for_symbol=0,
forecast={
"usable": True,
"model": "torch_gru",
"expected_return_percent": 0.04,
"probability_up": 0.57,
"skill": 0.05,
"block_entry": False,
},
account={"equity": 100.0},
)
assert signal.action == "BUY"
assert signal.diagnostics["edge_mode"] == "probe"
assert signal.diagnostics["checks"]["expected_edge_ok"] is True
assert signal.diagnostics["position_sizing"]["edge_mode"] == "probe"
assert settings.min_position_usdt <= signal.diagnostics["position_notional_usdt"] < settings.max_position_usdt
def test_torch_forecast_probe_blocks_negative_expected_return(make_settings, tmp_path) -> None:
settings = make_settings(
tmp_path,
strategy_mode="torch_forecast",
time_series_min_edge_percent=0.10,
time_series_min_probability_up=0.52,
time_series_probe_enabled=True,
time_series_probe_min_edge_percent=0.02,
time_series_probe_min_probability_up=0.55,
)
strategy = SpotStrategy(settings)
ticker = Ticker("BTCUSDT", 59_000, 58_999, 59_001, 10_000_000, 1000, 1.0)
signal = strategy.entry_signal(
"BTCUSDT",
[],
ticker,
open_positions_for_symbol=0,
forecast={
"usable": True,
"model": "torch_gru",
"expected_return_percent": -0.03,
"probability_up": 0.60,
"skill": 0.16,
"block_entry": False,
},
account={"equity": 100.0},
)
assert signal.action == "HOLD"
assert signal.diagnostics["edge_mode"] == "blocked"
assert signal.diagnostics["checks"]["expected_edge_ok"] is False
def test_torch_forecast_exits_when_forecast_turns_negative(make_settings, tmp_path) -> None: def test_torch_forecast_exits_when_forecast_turns_negative(make_settings, tmp_path) -> None:
settings = make_settings(tmp_path, strategy_mode="torch_forecast", stop_loss_percent=0.04) settings = make_settings(tmp_path, strategy_mode="torch_forecast", stop_loss_percent=0.04)
strategy = SpotStrategy(settings) strategy = SpotStrategy(settings)
+21 -1
View File
@@ -8,7 +8,12 @@ param(
[string]$Horizons = "", [string]$Horizons = "",
[string]$Features = "", [string]$Features = "",
[string]$ContextSymbols = "", [string]$ContextSymbols = "",
[int]$FirstRunMinutes = 0 [int]$FirstRunMinutes = 0,
[switch]$DeployToPi,
[string]$PiHost = "192.168.0.185",
[string]$PiUser = "sevenhill",
[string]$PiRoot = "/mnt/data/tradebot",
[string]$PiSshKeyPath = ""
) )
$ErrorActionPreference = "Stop" $ErrorActionPreference = "Stop"
@@ -46,6 +51,21 @@ if ($Features) {
if ($ContextSymbols) { if ($ContextSymbols) {
$actionArgs += " -ContextSymbols `"$ContextSymbols`"" $actionArgs += " -ContextSymbols `"$ContextSymbols`""
} }
if ($DeployToPi) {
$actionArgs += " -DeployToPi"
}
if ($PiHost) {
$actionArgs += " -PiHost `"$PiHost`""
}
if ($PiUser) {
$actionArgs += " -PiUser `"$PiUser`""
}
if ($PiRoot) {
$actionArgs += " -PiRoot `"$PiRoot`""
}
if ($PiSshKeyPath) {
$actionArgs += " -PiSshKeyPath `"$PiSshKeyPath`""
}
$action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $actionArgs -WorkingDirectory $RepoRoot $action = New-ScheduledTaskAction -Execute "powershell.exe" -Argument $actionArgs -WorkingDirectory $RepoRoot
$trigger = New-ScheduledTaskTrigger ` $trigger = New-ScheduledTaskTrigger `
-Once ` -Once `
+114
View File
@@ -0,0 +1,114 @@
[CmdletBinding()]
param(
[int]$MinReplayTrades = 8,
[int]$MaxAttempts = 0,
[string]$Symbols = "BTCUSDT,ETHUSDT,SOLUSDT,LTCUSDT",
[int]$Limit = 3000,
[switch]$DeployToPi,
[string]$PiHost = "192.168.0.185",
[string]$PiUser = "sevenhill",
[string]$PiRoot = "/mnt/data/tradebot",
[string]$PiSshKeyPath = "",
[int]$SeedStart = 0
)
$ErrorActionPreference = "Stop"
$RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path
$RuntimeDir = Join-Path $RepoRoot "runtime"
$LoopLog = Join-Path $RuntimeDir "torch_retrain_until_replay8.log"
$GuardReport = Join-Path $RuntimeDir "torch_retrain_guard.json"
$Runner = Join-Path $RepoRoot "tools\run_torch_retrain.ps1"
New-Item -ItemType Directory -Force -Path $RuntimeDir | Out-Null
function Write-LoopLog {
param([string]$Message)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ssK"
"[$timestamp] $Message" | Tee-Object -FilePath $LoopLog -Append
}
function ConvertTo-IntOrZero {
param($Value)
try {
if ($null -eq $Value) {
return 0
}
return [int]$Value
}
catch {
return 0
}
}
function Read-GuardSummary {
if (-not (Test-Path $GuardReport)) {
return [pscustomobject]@{
Accepted = $false
Reason = "guard_report_missing"
ReplayTrades = 0
WalkForwardTrades = 0
}
}
try {
$payload = Get-Content -Raw -LiteralPath $GuardReport | ConvertFrom-Json
return [pscustomobject]@{
Accepted = [bool]$payload.accepted
Reason = [string]$payload.reason
ReplayTrades = ConvertTo-IntOrZero $payload.candidate.full_replay.trades
WalkForwardTrades = ConvertTo-IntOrZero $payload.candidate.walk_forward_summary.trades
}
}
catch {
return [pscustomobject]@{
Accepted = $false
Reason = "guard_report_unreadable"
ReplayTrades = 0
WalkForwardTrades = 0
}
}
}
$attempt = 0
while ($true) {
$attempt += 1
if ($SeedStart -gt 0) {
$attemptSeed = $SeedStart + $attempt - 1
}
else {
$attemptSeed = Get-Random -Minimum 1 -Maximum 2147483647
}
Write-LoopLog "Attempt $attempt started; seed=$attemptSeed; target full_replay.trades >= $MinReplayTrades."
$runnerArgs = @(
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-File", $Runner,
"-Symbols", $Symbols,
"-Limit", $Limit.ToString(),
"-Seed", $attemptSeed.ToString()
)
if ($DeployToPi) {
$runnerArgs += "-DeployToPi"
if ($PiHost) { $runnerArgs += @("-PiHost", $PiHost) }
if ($PiUser) { $runnerArgs += @("-PiUser", $PiUser) }
if ($PiRoot) { $runnerArgs += @("-PiRoot", $PiRoot) }
if ($PiSshKeyPath) { $runnerArgs += @("-PiSshKeyPath", $PiSshKeyPath) }
}
& powershell.exe @runnerArgs 2>&1 | Tee-Object -FilePath $LoopLog -Append
$runnerExit = $LASTEXITCODE
$summary = Read-GuardSummary
Write-LoopLog "Attempt $attempt finished; runner_exit=$runnerExit accepted=$($summary.Accepted) reason=$($summary.Reason) full_replay.trades=$($summary.ReplayTrades) walk_forward.trades=$($summary.WalkForwardTrades)."
if ($summary.ReplayTrades -ge $MinReplayTrades) {
Write-LoopLog "Stop condition reached: full_replay.trades=$($summary.ReplayTrades) >= $MinReplayTrades."
exit 0
}
if ($MaxAttempts -gt 0 -and $attempt -ge $MaxAttempts) {
Write-LoopLog "MaxAttempts=$MaxAttempts reached before replay target."
exit 2
}
Start-Sleep -Seconds 10
}
+68 -2
View File
@@ -11,10 +11,17 @@ param(
[string]$Horizons = "", [string]$Horizons = "",
[string]$Features = "", [string]$Features = "",
[string]$ContextSymbols = "", [string]$ContextSymbols = "",
[int]$Seed = 0,
[int]$Epochs = 0, [int]$Epochs = 0,
[int]$Patience = 0, [int]$Patience = 0,
[string]$Interval = "", [string]$Interval = "",
[string]$EnvFile = "", [string]$EnvFile = "",
[switch]$DeployToPi,
[string]$PiHost = "",
[string]$PiUser = "",
[string]$PiRoot = "",
[string]$PiSshKeyPath = "",
[switch]$NoPiRestart,
[switch]$SkipGuard [switch]$SkipGuard
) )
@@ -52,6 +59,51 @@ function Resolve-Python {
throw "Python was not found. Create .venv or install Python 3.12." throw "Python was not found. Create .venv or install Python 3.12."
} }
function Test-TorchArtifactFile {
param([string]$Path)
if (-not (Test-Path $Path)) {
return $false
}
try {
$payload = Get-Content -Raw -LiteralPath $Path | ConvertFrom-Json
return $payload.type -eq "pytorch_recurrent_forecaster" -and $null -ne $payload.symbols
}
catch {
return $false
}
}
function Sync-AcceptedArtifactsToPi {
if (-not ($DeployToPi -or $env:TORCH_RETRAIN_DEPLOY_TO_PI)) {
Write-RetrainLog "Pi artifact sync disabled."
return
}
$syncScript = Join-Path $RepoRoot "tools\sync_torch_artifacts_to_pi.ps1"
if (-not (Test-Path $syncScript)) {
throw "Pi sync script not found: $syncScript"
}
$syncArgs = @(
"-NoProfile",
"-ExecutionPolicy", "Bypass",
"-File", $syncScript,
"-RepoRoot", $RepoRoot
)
if ($PiHost) { $syncArgs += @("-RemoteHost", $PiHost) }
if ($PiUser) { $syncArgs += @("-RemoteUser", $PiUser) }
if ($PiRoot) { $syncArgs += @("-RemoteRoot", $PiRoot) }
if ($PiSshKeyPath) { $syncArgs += @("-SshKeyPath", $PiSshKeyPath) }
if ($NoPiRestart) { $syncArgs += "-NoRestart" }
Write-RetrainLog "Syncing accepted Torch artifacts to Raspberry Pi."
& powershell.exe @syncArgs 2>&1 | Tee-Object -FilePath $LogFile -Append
if ($LASTEXITCODE -ne 0) {
throw "Pi artifact sync failed with exit code $LASTEXITCODE."
}
Write-RetrainLog "Pi artifact sync completed."
}
if (-not $Symbols -and $env:TORCH_RETRAIN_SYMBOLS) { $Symbols = $env:TORCH_RETRAIN_SYMBOLS } if (-not $Symbols -and $env:TORCH_RETRAIN_SYMBOLS) { $Symbols = $env:TORCH_RETRAIN_SYMBOLS }
if ($Limit -le 0) { if ($Limit -le 0) {
$Limit = if ($env:TORCH_RETRAIN_LIMIT) { [int]$env:TORCH_RETRAIN_LIMIT } else { 3000 } $Limit = if ($env:TORCH_RETRAIN_LIMIT) { [int]$env:TORCH_RETRAIN_LIMIT } else { 3000 }
@@ -65,6 +117,7 @@ if ($Horizon -le 0 -and $env:TORCH_RETRAIN_HORIZON) { $Horizon = [int]$env:TORCH
if (-not $Horizons -and $env:TORCH_RETRAIN_HORIZONS) { $Horizons = $env:TORCH_RETRAIN_HORIZONS } if (-not $Horizons -and $env:TORCH_RETRAIN_HORIZONS) { $Horizons = $env:TORCH_RETRAIN_HORIZONS }
if (-not $Features -and $env:TORCH_RETRAIN_FEATURES) { $Features = $env:TORCH_RETRAIN_FEATURES } if (-not $Features -and $env:TORCH_RETRAIN_FEATURES) { $Features = $env:TORCH_RETRAIN_FEATURES }
if (-not $ContextSymbols -and $env:TORCH_RETRAIN_CONTEXT_SYMBOLS) { $ContextSymbols = $env:TORCH_RETRAIN_CONTEXT_SYMBOLS } if (-not $ContextSymbols -and $env:TORCH_RETRAIN_CONTEXT_SYMBOLS) { $ContextSymbols = $env:TORCH_RETRAIN_CONTEXT_SYMBOLS }
if ($Seed -le 0 -and $env:TORCH_RETRAIN_SEED) { $Seed = [int]$env:TORCH_RETRAIN_SEED }
if ($Epochs -le 0) { $Epochs = if ($env:TORCH_RETRAIN_EPOCHS) { [int]$env:TORCH_RETRAIN_EPOCHS } else { 70 } } if ($Epochs -le 0) { $Epochs = if ($env:TORCH_RETRAIN_EPOCHS) { [int]$env:TORCH_RETRAIN_EPOCHS } else { 70 } }
if ($Patience -le 0) { $Patience = if ($env:TORCH_RETRAIN_PATIENCE) { [int]$env:TORCH_RETRAIN_PATIENCE } else { 8 } } if ($Patience -le 0) { $Patience = if ($env:TORCH_RETRAIN_PATIENCE) { [int]$env:TORCH_RETRAIN_PATIENCE } else { 8 } }
if (-not $Interval -and $env:TORCH_RETRAIN_INTERVAL) { $Interval = $env:TORCH_RETRAIN_INTERVAL } if (-not $Interval -and $env:TORCH_RETRAIN_INTERVAL) { $Interval = $env:TORCH_RETRAIN_INTERVAL }
@@ -110,19 +163,27 @@ try {
if ($Horizons) { $trainerArgs += @("--horizons", $Horizons) } if ($Horizons) { $trainerArgs += @("--horizons", $Horizons) }
if ($Features) { $trainerArgs += @("--features", $Features) } if ($Features) { $trainerArgs += @("--features", $Features) }
if ($ContextSymbols) { $trainerArgs += @("--context-symbols", $ContextSymbols) } if ($ContextSymbols) { $trainerArgs += @("--context-symbols", $ContextSymbols) }
if ($Seed -gt 0) { $trainerArgs += @("--seed", $Seed.ToString()) }
Push-Location $RepoRoot Push-Location $RepoRoot
$pushedLocation = $true $pushedLocation = $true
Write-RetrainLog "Starting PyTorch recurrent retrain: $python $($trainerArgs -join ' ')" Write-RetrainLog "Starting PyTorch recurrent retrain: $python $($trainerArgs -join ' ')"
& $python @trainerArgs 2>&1 | Tee-Object -FilePath $LogFile -Append & $python @trainerArgs 2>&1 | Tee-Object -FilePath $LogFile -Append
if ($LASTEXITCODE -ne 0) { $trainerExitCode = $LASTEXITCODE
throw "Trainer failed with exit code $LASTEXITCODE." if ($trainerExitCode -ne 0) {
if (Test-TorchArtifactFile $CandidateFile) {
Write-RetrainLog "WARNING: Trainer exited with code $trainerExitCode after writing a valid candidate artifact; continuing to guard."
}
else {
throw "Trainer failed with exit code $trainerExitCode."
}
} }
Write-RetrainLog "Finished PyTorch recurrent retrain candidate: $CandidateFile" Write-RetrainLog "Finished PyTorch recurrent retrain candidate: $CandidateFile"
if ($SkipGuard -or -not (Test-Path $ModelFile)) { if ($SkipGuard -or -not (Test-Path $ModelFile)) {
Move-Item -Force -LiteralPath $CandidateFile -Destination $ModelFile Move-Item -Force -LiteralPath $CandidateFile -Destination $ModelFile
Write-RetrainLog "Accepted candidate without guard. Active artifact: $ModelFile" Write-RetrainLog "Accepted candidate without guard. Active artifact: $ModelFile"
Sync-AcceptedArtifactsToPi
exit 0 exit 0
} }
@@ -162,7 +223,12 @@ try {
if ($LASTEXITCODE -ne 0) { if ($LASTEXITCODE -ne 0) {
throw "Retrain guard failed with exit code $LASTEXITCODE." throw "Retrain guard failed with exit code $LASTEXITCODE."
} }
if (Test-Path $CandidateCalibration) {
Copy-Item -Force -LiteralPath $CandidateCalibration -Destination (Join-Path $RuntimeDir "torch_threshold_calibration.json")
Write-RetrainLog "Updated active threshold calibration: $(Join-Path $RuntimeDir "torch_threshold_calibration.json")"
}
Write-RetrainLog "Candidate accepted by guard. Active artifact: $ModelFile" Write-RetrainLog "Candidate accepted by guard. Active artifact: $ModelFile"
Sync-AcceptedArtifactsToPi
} }
catch { catch {
Write-RetrainLog "ERROR: $($_.Exception.Message)" Write-RetrainLog "ERROR: $($_.Exception.Message)"
+95
View File
@@ -0,0 +1,95 @@
[CmdletBinding()]
param(
[string]$RepoRoot = "",
[string]$RemoteHost = "",
[string]$RemoteUser = "",
[string]$RemoteRoot = "",
[string]$SshKeyPath = "",
[string]$ServiceName = "tradebot",
[switch]$NoRestart,
[switch]$DryRun
)
$ErrorActionPreference = "Stop"
if (-not $RepoRoot) { $RepoRoot = (Resolve-Path (Join-Path $PSScriptRoot "..")).Path }
if (-not $RemoteHost -and $env:TORCH_DEPLOY_PI_HOST) { $RemoteHost = $env:TORCH_DEPLOY_PI_HOST }
if (-not $RemoteUser -and $env:TORCH_DEPLOY_PI_USER) { $RemoteUser = $env:TORCH_DEPLOY_PI_USER }
if (-not $RemoteRoot -and $env:TORCH_DEPLOY_PI_ROOT) { $RemoteRoot = $env:TORCH_DEPLOY_PI_ROOT }
if (-not $SshKeyPath -and $env:TORCH_DEPLOY_PI_SSH_KEY) { $SshKeyPath = $env:TORCH_DEPLOY_PI_SSH_KEY }
if (-not $RemoteHost) { $RemoteHost = "192.168.0.185" }
if (-not $RemoteUser) { $RemoteUser = "sevenhill" }
if (-not $RemoteRoot) { $RemoteRoot = "/mnt/data/tradebot" }
$RuntimeDir = Join-Path $RepoRoot "runtime"
$artifactNames = @(
"lstm_forecaster.json",
"torch_retrain_guard.json",
"torch_threshold_calibration.json"
)
$localFiles = @()
foreach ($name in $artifactNames) {
$path = Join-Path $RuntimeDir $name
if (Test-Path $path) {
$localFiles += (Resolve-Path $path).Path
}
}
if ($localFiles.Count -eq 0) {
throw "No Torch artifacts found in $RuntimeDir."
}
function ConvertTo-RemoteSingleQuoted {
param([string]$Value)
return "'" + ($Value -replace "'", "'\''") + "'"
}
function Invoke-LoggedCommand {
param(
[string]$Exe,
[string[]]$Arguments
)
$rendered = @($Exe) + $Arguments
Write-Host ($rendered -join " ")
if ($DryRun) {
return
}
& $Exe @Arguments
if ($LASTEXITCODE -ne 0) {
throw "$Exe failed with exit code $LASTEXITCODE."
}
}
$ssh = (Get-Command "ssh.exe" -ErrorAction SilentlyContinue)
if (-not $ssh) { $ssh = Get-Command "ssh" -ErrorAction Stop }
$scp = (Get-Command "scp.exe" -ErrorAction SilentlyContinue)
if (-not $scp) { $scp = Get-Command "scp" -ErrorAction Stop }
$commonSshArgs = @("-o", "BatchMode=yes", "-o", "StrictHostKeyChecking=accept-new", "-o", "ConnectTimeout=15")
if ($SshKeyPath) {
$expandedKey = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($SshKeyPath)
$commonSshArgs += @("-i", $expandedKey)
}
$remote = "${RemoteUser}@${RemoteHost}"
$remoteRuntime = "$RemoteRoot/runtime"
$remoteIncoming = "$remoteRuntime/.incoming-torch"
$mkdirCommand = "mkdir -p $(ConvertTo-RemoteSingleQuoted $remoteIncoming) $(ConvertTo-RemoteSingleQuoted $remoteRuntime)"
Invoke-LoggedCommand $ssh.Source (@($commonSshArgs + @($remote, $mkdirCommand)))
$destination = "${remote}:$remoteIncoming/"
Invoke-LoggedCommand $scp.Source (@($commonSshArgs + $localFiles + @($destination)))
$moveParts = @()
foreach ($path in $localFiles) {
$name = Split-Path $path -Leaf
$moveParts += "mv -f $(ConvertTo-RemoteSingleQuoted "$remoteIncoming/$name") $(ConvertTo-RemoteSingleQuoted "$remoteRuntime/$name")"
}
$moveCommand = $moveParts -join " && "
Invoke-LoggedCommand $ssh.Source (@($commonSshArgs + @($remote, $moveCommand)))
if (-not $NoRestart) {
$restartCommand = "cd $(ConvertTo-RemoteSingleQuoted $RemoteRoot) && docker compose restart $(ConvertTo-RemoteSingleQuoted $ServiceName)"
Invoke-LoggedCommand $ssh.Source (@($commonSshArgs + @($remote, $restartCommand)))
}
Write-Host "Synced Torch artifacts to ${remote}:$remoteRuntime"