Add Torch probe entries and Pi artifact sync
This commit is contained in:
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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),
|
||||||
|
|||||||
@@ -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']
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|||||||
+306496
-363264
File diff suppressed because it is too large
Load Diff
+231
-312
@@ -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
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
|
|||||||
@@ -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",
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
@@ -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 `
|
||||||
|
|||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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)"
|
||||||
|
|||||||
@@ -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"
|
||||||
Reference in New Issue
Block a user