Compare commits
3 Commits
4a4039c03d
...
main
| Author | SHA1 | Date | |
|---|---|---|---|
| 7f1ac694e4 | |||
| 33c3831bf2 | |||
| f0c70e2e1f |
@@ -142,6 +142,7 @@ class Settings:
|
|||||||
take_profit_percent: float
|
take_profit_percent: float
|
||||||
trailing_stop_percent: float
|
trailing_stop_percent: float
|
||||||
min_hold_seconds: int
|
min_hold_seconds: int
|
||||||
|
min_exit_net_percent: float
|
||||||
entry_cooldown_seconds: int
|
entry_cooldown_seconds: int
|
||||||
max_daily_drawdown_usdt: float
|
max_daily_drawdown_usdt: float
|
||||||
min_cash_reserve_usdt: float
|
min_cash_reserve_usdt: float
|
||||||
@@ -294,6 +295,7 @@ def load_settings(env_file: str | Path | None = None) -> Settings:
|
|||||||
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),
|
||||||
min_hold_seconds=_int_env("MIN_HOLD_SECONDS", 180),
|
min_hold_seconds=_int_env("MIN_HOLD_SECONDS", 180),
|
||||||
|
min_exit_net_percent=_float_env("MIN_EXIT_NET_PERCENT", 0.20),
|
||||||
entry_cooldown_seconds=_int_env("ENTRY_COOLDOWN_SECONDS", 180),
|
entry_cooldown_seconds=_int_env("ENTRY_COOLDOWN_SECONDS", 180),
|
||||||
max_daily_drawdown_usdt=_float_env("MAX_DAILY_DRAWDOWN_USDT", 6.0),
|
max_daily_drawdown_usdt=_float_env("MAX_DAILY_DRAWDOWN_USDT", 6.0),
|
||||||
min_cash_reserve_usdt=_float_env("MIN_CASH_RESERVE_USDT", 5.0),
|
min_cash_reserve_usdt=_float_env("MIN_CASH_RESERVE_USDT", 5.0),
|
||||||
|
|||||||
@@ -323,6 +323,7 @@ def _safe_config(settings: Settings) -> dict[str, Any]:
|
|||||||
"take_profit_percent": settings.take_profit_percent,
|
"take_profit_percent": settings.take_profit_percent,
|
||||||
"trailing_stop_percent": settings.trailing_stop_percent,
|
"trailing_stop_percent": settings.trailing_stop_percent,
|
||||||
"min_hold_seconds": settings.min_hold_seconds,
|
"min_hold_seconds": settings.min_hold_seconds,
|
||||||
|
"min_exit_net_percent": settings.min_exit_net_percent,
|
||||||
"entry_cooldown_seconds": settings.entry_cooldown_seconds,
|
"entry_cooldown_seconds": settings.entry_cooldown_seconds,
|
||||||
"max_daily_drawdown_usdt": settings.max_daily_drawdown_usdt,
|
"max_daily_drawdown_usdt": settings.max_daily_drawdown_usdt,
|
||||||
"min_cash_reserve_usdt": settings.min_cash_reserve_usdt,
|
"min_cash_reserve_usdt": settings.min_cash_reserve_usdt,
|
||||||
|
|||||||
@@ -394,6 +394,7 @@ class SpotStrategy:
|
|||||||
effective_take_profit = position.entry_price * (1 + take_profit_percent)
|
effective_take_profit = position.entry_price * (1 + take_profit_percent)
|
||||||
trailing = position.trailing_stop(trailing_percent)
|
trailing = position.trailing_stop(trailing_percent)
|
||||||
estimated_exit_net_percent = _estimated_exit_net_percent(position, price, self.settings)
|
estimated_exit_net_percent = _estimated_exit_net_percent(position, price, self.settings)
|
||||||
|
min_exit_net_percent = _min_exit_net_percent(self.settings)
|
||||||
diagnostics = {
|
diagnostics = {
|
||||||
"price": price,
|
"price": price,
|
||||||
"entry_price": position.entry_price,
|
"entry_price": position.entry_price,
|
||||||
@@ -408,6 +409,7 @@ class SpotStrategy:
|
|||||||
"adaptive_rules": adaptive,
|
"adaptive_rules": adaptive,
|
||||||
"forecast": forecast,
|
"forecast": forecast,
|
||||||
"estimated_exit_net_percent": round(estimated_exit_net_percent, 4),
|
"estimated_exit_net_percent": round(estimated_exit_net_percent, 4),
|
||||||
|
"min_exit_net_percent": min_exit_net_percent,
|
||||||
"min_exit_profit_percent": float(adaptive.get("min_exit_profit_percent", 0.0) or 0.0),
|
"min_exit_profit_percent": float(adaptive.get("min_exit_profit_percent", 0.0) or 0.0),
|
||||||
}
|
}
|
||||||
if effective_stop_loss is not None and price <= effective_stop_loss:
|
if effective_stop_loss is not None and price <= effective_stop_loss:
|
||||||
@@ -438,6 +440,7 @@ class SpotStrategy:
|
|||||||
estimated_exit_net_percent=estimated_exit_net_percent,
|
estimated_exit_net_percent=estimated_exit_net_percent,
|
||||||
stop_loss_percent=stop_loss_percent,
|
stop_loss_percent=stop_loss_percent,
|
||||||
min_edge_percent=self.settings.time_series_min_edge_percent,
|
min_edge_percent=self.settings.time_series_min_edge_percent,
|
||||||
|
min_exit_net_percent=min_exit_net_percent,
|
||||||
)
|
)
|
||||||
if forecast_exit is not None:
|
if forecast_exit is not None:
|
||||||
action, confidence, reason = forecast_exit
|
action, confidence, reason = forecast_exit
|
||||||
@@ -876,6 +879,7 @@ def _torch_forecast_exit_signal(
|
|||||||
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)
|
||||||
estimated_exit_net_percent = _estimated_exit_net_percent(position, price, settings)
|
estimated_exit_net_percent = _estimated_exit_net_percent(position, price, settings)
|
||||||
|
min_exit_net_percent = _min_exit_net_percent(settings)
|
||||||
entry_path = str(position.entry_diagnostics.get("entry_path", ""))
|
entry_path = str(position.entry_diagnostics.get("entry_path", ""))
|
||||||
entry_edge_mode = str(position.entry_diagnostics.get("edge_mode", ""))
|
entry_edge_mode = str(position.entry_diagnostics.get("edge_mode", ""))
|
||||||
rebound_fallback_position = entry_path == "rebound_fallback" or entry_edge_mode == "rebound_fallback"
|
rebound_fallback_position = entry_path == "rebound_fallback" or entry_edge_mode == "rebound_fallback"
|
||||||
@@ -899,6 +903,7 @@ def _torch_forecast_exit_signal(
|
|||||||
"min_probability_up": min_probability,
|
"min_probability_up": min_probability,
|
||||||
"skill": skill,
|
"skill": skill,
|
||||||
"estimated_exit_net_percent": round(estimated_exit_net_percent, 4),
|
"estimated_exit_net_percent": round(estimated_exit_net_percent, 4),
|
||||||
|
"min_exit_net_percent": min_exit_net_percent,
|
||||||
"atr_14": latest.atr_14 if latest else None,
|
"atr_14": latest.atr_14 if latest else None,
|
||||||
}
|
}
|
||||||
hold_seconds = (utc_now() - position.opened_at).total_seconds()
|
hold_seconds = (utc_now() - position.opened_at).total_seconds()
|
||||||
@@ -909,13 +914,15 @@ def _torch_forecast_exit_signal(
|
|||||||
if price >= position.take_profit:
|
if price >= position.take_profit:
|
||||||
return Signal(position.symbol, "SELL", 0.96, "torch_forecast: take-profit hit", diagnostics)
|
return Signal(position.symbol, "SELL", 0.96, "torch_forecast: take-profit hit", diagnostics)
|
||||||
if atr_trailing_stop is not None and price <= atr_trailing_stop:
|
if atr_trailing_stop is not None and price <= atr_trailing_stop:
|
||||||
if estimated_exit_net_percent < 0:
|
if estimated_exit_net_percent < min_exit_net_percent:
|
||||||
diagnostics["atr_exit_blocked_by_cost"] = True
|
diagnostics["atr_exit_blocked_by_min_profit"] = True
|
||||||
|
if estimated_exit_net_percent < 0:
|
||||||
|
diagnostics["atr_exit_blocked_by_cost"] = True
|
||||||
return Signal(
|
return Signal(
|
||||||
position.symbol,
|
position.symbol,
|
||||||
"HOLD",
|
"HOLD",
|
||||||
0.45,
|
0.45,
|
||||||
"torch_forecast: ATR trailing touched, but exit is not worth fees",
|
"torch_forecast: ATR trailing touched, but exit profit is below minimum",
|
||||||
diagnostics,
|
diagnostics,
|
||||||
)
|
)
|
||||||
return Signal(position.symbol, "SELL", 0.94, "torch_forecast: ATR trailing stop hit", diagnostics)
|
return Signal(position.symbol, "SELL", 0.94, "torch_forecast: ATR trailing stop hit", diagnostics)
|
||||||
@@ -956,23 +963,26 @@ def _torch_forecast_exit_signal(
|
|||||||
estimated_exit_net_percent=estimated_exit_net_percent,
|
estimated_exit_net_percent=estimated_exit_net_percent,
|
||||||
stop_loss_percent=stop_loss_percent,
|
stop_loss_percent=stop_loss_percent,
|
||||||
min_edge_percent=min_edge,
|
min_edge_percent=min_edge,
|
||||||
|
min_exit_net_percent=min_exit_net_percent,
|
||||||
)
|
)
|
||||||
if forecast_exit is not None:
|
if forecast_exit is not None:
|
||||||
action, confidence, reason = forecast_exit
|
action, confidence, reason = forecast_exit
|
||||||
return Signal(position.symbol, action, confidence, reason, diagnostics)
|
return Signal(position.symbol, action, confidence, reason, diagnostics)
|
||||||
diagnostics["forecast_exit_blocked_by_cost"] = True
|
diagnostics["forecast_exit_blocked_by_min_profit"] = True
|
||||||
|
if estimated_exit_net_percent < 0:
|
||||||
|
diagnostics["forecast_exit_blocked_by_cost"] = True
|
||||||
return Signal(
|
return Signal(
|
||||||
position.symbol,
|
position.symbol,
|
||||||
"HOLD",
|
"HOLD",
|
||||||
0.44,
|
0.44,
|
||||||
(
|
(
|
||||||
"torch_forecast: forecast weakened, but exit is not worth fees; "
|
"torch_forecast: forecast weakened, but exit profit is below minimum; "
|
||||||
f"p_up={probability_up:.3f}, expected={expected_return:.4f}%"
|
f"p_up={probability_up:.3f}, expected={expected_return:.4f}%"
|
||||||
),
|
),
|
||||||
diagnostics,
|
diagnostics,
|
||||||
)
|
)
|
||||||
weak_hold = expected_return < min_edge or probability_up < min_probability or skill <= 0.0
|
weak_hold = expected_return < min_edge or probability_up < min_probability or skill <= 0.0
|
||||||
if weak_hold and estimated_exit_net_percent >= 0:
|
if weak_hold and estimated_exit_net_percent >= min_exit_net_percent:
|
||||||
return Signal(
|
return Signal(
|
||||||
position.symbol,
|
position.symbol,
|
||||||
"SELL",
|
"SELL",
|
||||||
@@ -983,6 +993,8 @@ def _torch_forecast_exit_signal(
|
|||||||
),
|
),
|
||||||
diagnostics,
|
diagnostics,
|
||||||
)
|
)
|
||||||
|
if weak_hold and estimated_exit_net_percent >= 0:
|
||||||
|
diagnostics["weak_exit_blocked_by_min_profit"] = True
|
||||||
return Signal(position.symbol, "HOLD", 0.35, "torch_forecast: PyTorch hold confirmed", diagnostics)
|
return Signal(position.symbol, "HOLD", 0.35, "torch_forecast: PyTorch hold confirmed", diagnostics)
|
||||||
|
|
||||||
|
|
||||||
@@ -1633,6 +1645,10 @@ def _estimated_exit_net_percent(position: Position, price: float, settings: Sett
|
|||||||
return gross_percent - round_trip_cost_percent
|
return gross_percent - round_trip_cost_percent
|
||||||
|
|
||||||
|
|
||||||
|
def _min_exit_net_percent(settings: Settings) -> float:
|
||||||
|
return round(_clamp(settings.min_exit_net_percent, 0.0, 5.0), 4)
|
||||||
|
|
||||||
|
|
||||||
def _adaptive_indicator_exit_allowed(adaptive: dict, mode_key: str, estimated_exit_net_percent: float) -> bool:
|
def _adaptive_indicator_exit_allowed(adaptive: dict, mode_key: str, estimated_exit_net_percent: float) -> bool:
|
||||||
mode = str(adaptive.get(mode_key, "normal")).lower()
|
mode = str(adaptive.get(mode_key, "normal")).lower()
|
||||||
if mode != "profit_only":
|
if mode != "profit_only":
|
||||||
@@ -1649,6 +1665,7 @@ def _forecast_exit_signal(
|
|||||||
estimated_exit_net_percent: float,
|
estimated_exit_net_percent: float,
|
||||||
stop_loss_percent: float,
|
stop_loss_percent: float,
|
||||||
min_edge_percent: float,
|
min_edge_percent: float,
|
||||||
|
min_exit_net_percent: float,
|
||||||
) -> tuple[str, float, str] | None:
|
) -> tuple[str, float, str] | None:
|
||||||
if not forecast.get("usable"):
|
if not forecast.get("usable"):
|
||||||
return None
|
return None
|
||||||
@@ -1660,7 +1677,7 @@ def _forecast_exit_signal(
|
|||||||
if not strong_negative:
|
if not strong_negative:
|
||||||
return None
|
return None
|
||||||
reason = forecast.get("reason") or "ожидается снижение"
|
reason = forecast.get("reason") or "ожидается снижение"
|
||||||
if estimated_exit_net_percent >= 0:
|
if estimated_exit_net_percent >= min_exit_net_percent:
|
||||||
return "SELL", 0.82, f"прогноз временного ряда ухудшился: {reason}; фиксируем результат"
|
return "SELL", 0.82, f"прогноз временного ряда ухудшился: {reason}; фиксируем результат"
|
||||||
loss_from_entry = ((price - position.entry_price) / position.entry_price) if position.entry_price else 0.0
|
loss_from_entry = ((price - position.entry_price) / position.entry_price) if position.entry_price else 0.0
|
||||||
soft_loss_limit = -max(0.003, stop_loss_percent * 0.35)
|
soft_loss_limit = -max(0.003, stop_loss_percent * 0.35)
|
||||||
|
|||||||
@@ -94,6 +94,7 @@ def make_settings():
|
|||||||
take_profit_percent=0.035,
|
take_profit_percent=0.035,
|
||||||
trailing_stop_percent=0.015,
|
trailing_stop_percent=0.015,
|
||||||
min_hold_seconds=180,
|
min_hold_seconds=180,
|
||||||
|
min_exit_net_percent=0.20,
|
||||||
entry_cooldown_seconds=180,
|
entry_cooldown_seconds=180,
|
||||||
max_daily_drawdown_usdt=6.0,
|
max_daily_drawdown_usdt=6.0,
|
||||||
min_cash_reserve_usdt=5.0,
|
min_cash_reserve_usdt=5.0,
|
||||||
|
|||||||
@@ -954,6 +954,81 @@ def test_torch_forecast_holds_atr_trailing_exit_that_does_not_cover_fees(make_se
|
|||||||
assert signal.diagnostics["atr_exit_blocked_by_cost"] is True
|
assert signal.diagnostics["atr_exit_blocked_by_cost"] is True
|
||||||
|
|
||||||
|
|
||||||
|
def test_torch_forecast_holds_atr_trailing_exit_below_min_profit(make_settings, tmp_path) -> None:
|
||||||
|
settings = make_settings(tmp_path, strategy_mode="torch_forecast", min_hold_seconds=60, min_exit_net_percent=0.20)
|
||||||
|
strategy = SpotStrategy(settings)
|
||||||
|
candles = _trend_entry_candles(close=100.35)
|
||||||
|
candles[-1].atr_14 = 0.6
|
||||||
|
position = Position(
|
||||||
|
1,
|
||||||
|
"MNTUSDT",
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
100,
|
||||||
|
0.1,
|
||||||
|
96,
|
||||||
|
120,
|
||||||
|
102,
|
||||||
|
opened_at=utc_now() - timedelta(seconds=600),
|
||||||
|
)
|
||||||
|
ticker = Ticker("MNTUSDT", 100.35, 100.34, 100.36, 10_000_000, 1000, 1.0)
|
||||||
|
|
||||||
|
signal = strategy.exit_signal(
|
||||||
|
position,
|
||||||
|
candles,
|
||||||
|
ticker,
|
||||||
|
forecast={
|
||||||
|
"usable": True,
|
||||||
|
"model": "torch_lstm",
|
||||||
|
"expected_return_percent": 0.4,
|
||||||
|
"probability_up": 0.58,
|
||||||
|
"skill": 0.18,
|
||||||
|
"block_entry": False,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert signal.action == "HOLD"
|
||||||
|
assert signal.diagnostics["atr_exit_blocked_by_min_profit"] is True
|
||||||
|
assert signal.diagnostics["estimated_exit_net_percent"] < settings.min_exit_net_percent
|
||||||
|
|
||||||
|
|
||||||
|
def test_torch_forecast_holds_negative_forecast_exit_below_min_profit(make_settings, tmp_path) -> None:
|
||||||
|
settings = make_settings(tmp_path, strategy_mode="torch_forecast", min_hold_seconds=60, min_exit_net_percent=0.20)
|
||||||
|
strategy = SpotStrategy(settings)
|
||||||
|
position = Position(
|
||||||
|
1,
|
||||||
|
"BTCUSDT",
|
||||||
|
1,
|
||||||
|
100,
|
||||||
|
100,
|
||||||
|
0.1,
|
||||||
|
96,
|
||||||
|
120,
|
||||||
|
100.5,
|
||||||
|
opened_at=utc_now() - timedelta(seconds=600),
|
||||||
|
)
|
||||||
|
ticker = Ticker("BTCUSDT", 100.35, 100.34, 100.36, 10_000_000, 1000, 1.0)
|
||||||
|
|
||||||
|
signal = strategy.exit_signal(
|
||||||
|
position,
|
||||||
|
_trend_entry_candles(close=100.35),
|
||||||
|
ticker,
|
||||||
|
forecast={
|
||||||
|
"usable": True,
|
||||||
|
"model": "torch_lstm",
|
||||||
|
"expected_return_percent": -0.2,
|
||||||
|
"probability_up": 0.40,
|
||||||
|
"skill": 0.18,
|
||||||
|
"block_entry": False,
|
||||||
|
"reason": "model turned down",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
assert signal.action == "HOLD"
|
||||||
|
assert signal.diagnostics["forecast_exit_blocked_by_min_profit"] is True
|
||||||
|
assert signal.diagnostics["estimated_exit_net_percent"] < settings.min_exit_net_percent
|
||||||
|
|
||||||
|
|
||||||
def test_torch_forecast_rebound_fallback_holds_without_model(make_settings, tmp_path) -> None:
|
def test_torch_forecast_rebound_fallback_holds_without_model(make_settings, tmp_path) -> None:
|
||||||
settings = make_settings(tmp_path, strategy_mode="torch_forecast", min_hold_seconds=180)
|
settings = make_settings(tmp_path, strategy_mode="torch_forecast", min_hold_seconds=180)
|
||||||
strategy = SpotStrategy(settings)
|
strategy = SpotStrategy(settings)
|
||||||
|
|||||||
@@ -40,6 +40,15 @@ 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 Resolve-WindowlessPython {
|
||||||
|
$python = Resolve-Python
|
||||||
|
$pythonw = Join-Path (Split-Path -Parent $python) "pythonw.exe"
|
||||||
|
if (Test-Path $pythonw) {
|
||||||
|
return $pythonw
|
||||||
|
}
|
||||||
|
return $python
|
||||||
|
}
|
||||||
|
|
||||||
if ($ApiAuth) {
|
if ($ApiAuth) {
|
||||||
[Environment]::SetEnvironmentVariable("TRADEBOT_API_AUTH", $ApiAuth, "User")
|
[Environment]::SetEnvironmentVariable("TRADEBOT_API_AUTH", $ApiAuth, "User")
|
||||||
$env:TRADEBOT_API_AUTH = $ApiAuth
|
$env:TRADEBOT_API_AUTH = $ApiAuth
|
||||||
@@ -59,7 +68,7 @@ if (-not $KeepLegacyRetrainer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
$python = Resolve-Python
|
$python = Resolve-WindowlessPython
|
||||||
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
$currentUser = [System.Security.Principal.WindowsIdentity]::GetCurrent().Name
|
||||||
$arguments = @(
|
$arguments = @(
|
||||||
"-u",
|
"-u",
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ import json
|
|||||||
import os
|
import os
|
||||||
import platform
|
import platform
|
||||||
import queue
|
import queue
|
||||||
|
import re
|
||||||
import subprocess
|
import subprocess
|
||||||
import sys
|
import sys
|
||||||
import threading
|
import threading
|
||||||
@@ -124,6 +125,7 @@ def run_retrain(args: argparse.Namespace, job_id: str, job: dict[str, Any], repo
|
|||||||
text=True,
|
text=True,
|
||||||
encoding="utf-8",
|
encoding="utf-8",
|
||||||
errors="replace",
|
errors="replace",
|
||||||
|
**hidden_subprocess_kwargs(),
|
||||||
) as process:
|
) as process:
|
||||||
reader = threading.Thread(target=read_output, name="training-output-reader", daemon=True)
|
reader = threading.Thread(target=read_output, name="training-output-reader", daemon=True)
|
||||||
reader.start()
|
reader.start()
|
||||||
@@ -140,7 +142,7 @@ def run_retrain(args: argparse.Namespace, job_id: str, job: dict[str, Any], repo
|
|||||||
log(log_path, message)
|
log(log_path, message)
|
||||||
line_count += 1
|
line_count += 1
|
||||||
if message:
|
if message:
|
||||||
last_message = message[-220:]
|
last_message = friendly_training_message(message)
|
||||||
except queue.Empty:
|
except queue.Empty:
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@@ -163,6 +165,78 @@ def run_retrain(args: argparse.Namespace, job_id: str, job: dict[str, Any], repo
|
|||||||
report_progress(args, job_id, "running", "guard", 70, "Guard завершён, подготавливаю артефакты")
|
report_progress(args, job_id, "running", "guard", 70, "Guard завершён, подготавливаю артефакты")
|
||||||
|
|
||||||
|
|
||||||
|
def friendly_training_message(message: str) -> str:
|
||||||
|
cleaned = message.strip()
|
||||||
|
if not cleaned:
|
||||||
|
return "PyTorch обучает модель"
|
||||||
|
|
||||||
|
if "Starting PyTorch recurrent retrain:" in cleaned:
|
||||||
|
return "PyTorch LSTM/GRU запущен: готовлю данные и варианты модели"
|
||||||
|
|
||||||
|
started = re.search(
|
||||||
|
r"training started: symbols=(?P<symbols>\d+) interval=(?P<interval>\d+) "
|
||||||
|
r"limit=(?P<limit>\d+) epochs=(?P<epochs>\d+)",
|
||||||
|
cleaned,
|
||||||
|
)
|
||||||
|
if started:
|
||||||
|
interval = started.group("interval")
|
||||||
|
timeframe = "1h" if interval == "60" else f"{interval}m"
|
||||||
|
return (
|
||||||
|
f"Старт обучения: {started.group('symbols')} пар, таймфрейм {timeframe}, "
|
||||||
|
f"история {started.group('limit')} свечей, до {started.group('epochs')} эпох"
|
||||||
|
)
|
||||||
|
|
||||||
|
pair_started = re.search(r"^(?P<symbol>[A-Z0-9]+): training started \((?P<index>\d+)/(?P<total>\d+)\)", cleaned)
|
||||||
|
if pair_started:
|
||||||
|
return (
|
||||||
|
f"{pair_started.group('symbol')}: обучение пары "
|
||||||
|
f"{pair_started.group('index')}/{pair_started.group('total')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
preparing = re.search(r"^(?P<symbol>[A-Z0-9]+): preparing lookback=(?P<lookback>\d+)", cleaned)
|
||||||
|
if preparing:
|
||||||
|
return f"{preparing.group('symbol')}: готовлю окно {preparing.group('lookback')} свечей"
|
||||||
|
|
||||||
|
fitting = re.search(
|
||||||
|
r"^(?P<symbol>[A-Z0-9]+): fitting (?P<arch>lstm|gru) "
|
||||||
|
r"lookback=(?P<lookback>\d+) hidden=(?P<hidden>\d+) "
|
||||||
|
r"layers=(?P<layers>\d+) dropout=(?P<dropout>[0-9.]+)",
|
||||||
|
cleaned,
|
||||||
|
)
|
||||||
|
if fitting:
|
||||||
|
return (
|
||||||
|
f"{fitting.group('symbol')}: обучаю {fitting.group('arch').upper()}, "
|
||||||
|
f"окно {fitting.group('lookback')}, нейронов {fitting.group('hidden')}, "
|
||||||
|
f"слоёв {fitting.group('layers')}, dropout {fitting.group('dropout')}"
|
||||||
|
)
|
||||||
|
|
||||||
|
model = re.search(
|
||||||
|
r"^(?P<symbol>[A-Z0-9]+): model=torch_(?P<arch>lstm|gru).*?"
|
||||||
|
r"mae=(?P<mae>[0-9.]+)%.*?skill=(?P<skill>-?[0-9.]+).*?dir=(?P<direction>[0-9.]+)",
|
||||||
|
cleaned,
|
||||||
|
)
|
||||||
|
if model:
|
||||||
|
direction = float(model.group("direction")) * 100
|
||||||
|
skill = float(model.group("skill")) * 100
|
||||||
|
return (
|
||||||
|
f"{model.group('symbol')}: выбран {model.group('arch').upper()}, "
|
||||||
|
f"ошибка {model.group('mae')}%, skill {skill:.1f}%, направление {direction:.1f}%"
|
||||||
|
)
|
||||||
|
|
||||||
|
if "Calibrating current artifact" in cleaned:
|
||||||
|
return "Проверяю текущую модель на replay"
|
||||||
|
if "Calibrating candidate artifact" in cleaned:
|
||||||
|
return "Проверяю новую модель на replay"
|
||||||
|
if "Running retrain guard" in cleaned:
|
||||||
|
return "Gate сравнивает новую модель с текущей"
|
||||||
|
if "Candidate rejected by guard" in cleaned:
|
||||||
|
return "Новая модель обучилась, но gate не дал ей ходу"
|
||||||
|
if "Candidate accepted by guard" in cleaned:
|
||||||
|
return "Новая модель прошла gate и стала активной"
|
||||||
|
|
||||||
|
return cleaned[-220:]
|
||||||
|
|
||||||
|
|
||||||
def training_heartbeat_message(now: float, started_at: float, last_output_at: float, last_message: str) -> str:
|
def training_heartbeat_message(now: float, started_at: float, last_output_at: float, last_message: str) -> str:
|
||||||
elapsed = format_duration(now - started_at)
|
elapsed = format_duration(now - started_at)
|
||||||
idle_seconds = max(0.0, now - last_output_at)
|
idle_seconds = max(0.0, now - last_output_at)
|
||||||
@@ -296,7 +370,11 @@ def log(path: Path, message: str) -> None:
|
|||||||
path.parent.mkdir(parents=True, exist_ok=True)
|
path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
stamp = datetime.now().astimezone().isoformat(timespec="seconds")
|
stamp = datetime.now().astimezone().isoformat(timespec="seconds")
|
||||||
line = f"[{stamp}] {message}"
|
line = f"[{stamp}] {message}"
|
||||||
print(line, flush=True)
|
if sys.stdout is not None:
|
||||||
|
try:
|
||||||
|
print(line, flush=True)
|
||||||
|
except OSError:
|
||||||
|
pass
|
||||||
with path.open("a", encoding="utf-8") as handle:
|
with path.open("a", encoding="utf-8") as handle:
|
||||||
handle.write(line + "\n")
|
handle.write(line + "\n")
|
||||||
|
|
||||||
@@ -309,6 +387,18 @@ def read_json(path: Path) -> dict[str, Any]:
|
|||||||
return data if isinstance(data, dict) else {}
|
return data if isinstance(data, dict) else {}
|
||||||
|
|
||||||
|
|
||||||
|
def hidden_subprocess_kwargs() -> dict[str, Any]:
|
||||||
|
if os.name != "nt":
|
||||||
|
return {}
|
||||||
|
startupinfo = subprocess.STARTUPINFO()
|
||||||
|
startupinfo.dwFlags |= subprocess.STARTF_USESHOWWINDOW
|
||||||
|
startupinfo.wShowWindow = 0
|
||||||
|
return {
|
||||||
|
"creationflags": getattr(subprocess, "CREATE_NO_WINDOW", 0),
|
||||||
|
"startupinfo": startupinfo,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def quote_for_log(value: str) -> str:
|
def quote_for_log(value: str) -> str:
|
||||||
return f'"{value}"' if " " in value else value
|
return f'"{value}"' if " " in value else value
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user