Remove legacy LSTM retraining

This commit is contained in:
Codex
2026-06-20 22:07:21 +03:00
parent d9c0317675
commit ccf457481b
14 changed files with 179 additions and 619 deletions
+6 -9
View File
@@ -5,6 +5,9 @@ from dataclasses import dataclass
from pathlib import Path
FIXED_SPOT_SYMBOLS = ("BTCUSDT", "ETHUSDT", "HYPEUSDT", "SOLUSDT", "LTCUSDT", "XRPUSDT")
def _load_dotenv(path: Path) -> None:
if not path.exists():
return
@@ -97,9 +100,6 @@ class Settings:
time_series_min_edge_percent: float
time_series_max_adjustment: float
time_series_lstm_enabled: bool
time_series_lstm_lookback: int
time_series_lstm_units: int
time_series_lstm_ridge: float
time_series_lstm_model_path: Path
stop_loss_percent: float
take_profit_percent: float
@@ -172,9 +172,9 @@ def load_settings(env_file: str | Path | None = None) -> Settings:
bybit_api_key=os.getenv("BYBIT_API_KEY", ""),
bybit_api_secret=os.getenv("BYBIT_API_SECRET", ""),
starting_balance_usdt=_float_env("STARTING_BALANCE_USDT", 100.0),
auto_select_symbols=_bool_env("AUTO_SELECT_SYMBOLS", True),
top_symbols_count=_int_env("TOP_SYMBOLS_COUNT", 6),
symbols=_symbols_env("SYMBOLS"),
auto_select_symbols=_bool_env("AUTO_SELECT_SYMBOLS", False),
top_symbols_count=_int_env("TOP_SYMBOLS_COUNT", len(FIXED_SPOT_SYMBOLS)),
symbols=_symbols_env("SYMBOLS") or FIXED_SPOT_SYMBOLS,
base_interval=os.getenv("BASE_INTERVAL", "1"),
kline_limit=_int_env("KLINE_LIMIT", 240),
loop_interval_seconds=_int_env("LOOP_INTERVAL_SECONDS", 5),
@@ -220,9 +220,6 @@ def load_settings(env_file: str | Path | None = None) -> Settings:
time_series_min_edge_percent=_float_env("TIME_SERIES_MIN_EDGE_PERCENT", 0.04),
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_lookback=_int_env("TIME_SERIES_LSTM_LOOKBACK", 32),
time_series_lstm_units=_int_env("TIME_SERIES_LSTM_UNITS", 6),
time_series_lstm_ridge=_float_env("TIME_SERIES_LSTM_RIDGE", 0.0001),
time_series_lstm_model_path=Path(os.getenv("TIME_SERIES_LSTM_MODEL_PATH", "runtime/lstm_forecaster.json")),
stop_loss_percent=_float_env("STOP_LOSS_PERCENT", 0.02),
take_profit_percent=_float_env("TAKE_PROFIT_PERCENT", 0.035),
+14 -14
View File
@@ -217,9 +217,6 @@ def _safe_config(settings: Settings) -> dict[str, Any]:
"time_series_min_edge_percent": settings.time_series_min_edge_percent,
"time_series_max_adjustment": settings.time_series_max_adjustment,
"time_series_lstm_enabled": settings.time_series_lstm_enabled,
"time_series_lstm_lookback": settings.time_series_lstm_lookback,
"time_series_lstm_units": settings.time_series_lstm_units,
"time_series_lstm_ridge": settings.time_series_lstm_ridge,
"time_series_lstm_model_path": str(settings.time_series_lstm_model_path),
"time_series_model_artifact": _time_series_model_artifact(settings),
"stop_loss_percent": settings.stop_loss_percent,
@@ -266,16 +263,19 @@ def _time_series_model_artifact(settings: Settings) -> dict[str, Any]:
}
)
artifact_type = str(data.get("type", "")).strip()
if artifact_type == "pytorch_recurrent_forecaster":
label = "PyTorch LSTM/GRU"
elif artifact_type == "lstm_reservoir_ridge_params":
label = "легкий LSTM fallback"
else:
label = artifact_type or "настройки прогноза"
if artifact_type != "pytorch_recurrent_forecaster":
return {
"available": False,
"type": artifact_type or "unknown",
"label": "устаревший файл модели не используется",
"created_at": data.get("created_at", ""),
"symbol_count": len(rows),
"models": models,
}
return {
"available": True,
"type": artifact_type or "unknown",
"label": label,
"type": artifact_type,
"label": "PyTorch LSTM/GRU",
"created_at": data.get("created_at", ""),
"symbol_count": len(rows),
"models": models,
@@ -289,7 +289,7 @@ def _forecast_model_label(model: str) -> str:
if normalized == "torch_gru":
return "PyTorch GRU"
if normalized == "lstm":
return "легкий LSTM"
return "устаревший LSTM"
if normalized == "gru":
return "GRU"
return model
@@ -742,7 +742,7 @@ HTML = r"""
const names = {
torch_lstm: 'PyTorch LSTM',
torch_gru: 'PyTorch GRU',
lstm: 'Легкий LSTM',
lstm: 'Устаревший LSTM',
naive: 'Baseline',
drift: 'Drift',
ewma: 'EWMA',
@@ -757,7 +757,7 @@ HTML = r"""
return String(reason || '')
.replaceAll('torch_lstm', 'PyTorch LSTM')
.replaceAll('torch_gru', 'PyTorch GRU')
.replaceAll('модель lstm', 'модель легкий LSTM');
.replaceAll('модель lstm', 'модель устаревший LSTM');
}
function modelArtifactSummary(config) {
-145
View File
@@ -3,7 +3,6 @@ from __future__ import annotations
import json
import math
from dataclasses import asdict, dataclass, field
from functools import lru_cache
from typing import Any
from crypto_spot_bot.config import Settings
@@ -174,8 +173,6 @@ def _validate_candidates(
torch_model = _torch_recurrent_model_name(symbol, lstm_artifact or {})
if torch_model and _can_use_torch_recurrent(returns, symbol, lstm_artifact or {}):
models.append(torch_model)
if _can_use_lstm(returns, settings, symbol, lstm_artifact or {}):
models.append("lstm")
rows: list[dict[str, float | str]] = []
start = max(8, len(returns) - validation_window)
for model in models:
@@ -211,8 +208,6 @@ def _predict_next_return(
return _ar_predict(returns, 3)
if model in {"torch_lstm", "torch_gru"}:
return _torch_recurrent_predict(returns, symbol, lstm_artifact or {})
if model == "lstm" and settings is not None:
return _lstm_predict(returns, settings, symbol, lstm_artifact or {})
return 0.0
@@ -244,52 +239,6 @@ def _ar_predict(returns: list[float], lag_count: int) -> float:
return _clamp(prediction, -cap, cap)
def _can_use_lstm(
returns: list[float],
settings: Settings,
symbol: str | None,
lstm_artifact: dict[str, Any],
) -> bool:
if not settings.time_series_lstm_enabled:
return False
params = _lstm_params(settings, symbol, lstm_artifact)
return len(returns) >= params["lookback"] + 16
def _lstm_params(settings: Settings, symbol: str | None, lstm_artifact: dict[str, Any]) -> dict[str, float | int]:
params: dict[str, float | int] = {
"lookback": settings.time_series_lstm_lookback,
"units": settings.time_series_lstm_units,
"ridge": settings.time_series_lstm_ridge,
}
default_params = lstm_artifact.get("default")
if isinstance(default_params, dict):
params.update(_clean_lstm_params(default_params))
symbols = lstm_artifact.get("symbols")
symbol_params = symbols.get(symbol.upper()) if symbol and isinstance(symbols, dict) else None
if isinstance(symbol_params, dict):
params.update(_clean_lstm_params(symbol_params))
return {
"lookback": int(_clamp(float(params["lookback"]), 6.0, 128.0)),
"units": int(_clamp(float(params["units"]), 2.0, 16.0)),
"ridge": _clamp(float(params["ridge"]), 1e-8, 0.5),
}
def _clean_lstm_params(data: dict[str, Any]) -> dict[str, float | int]:
clean: dict[str, float | int] = {}
for key in ("lookback", "units", "ridge"):
value = data.get(key)
if isinstance(value, (int, float)):
clean[key] = value
elif isinstance(value, str):
try:
clean[key] = float(value)
except ValueError:
continue
return clean
def _torch_recurrent_model_name(symbol: str | None, lstm_artifact: dict[str, Any]) -> str | None:
entry = _torch_recurrent_entry(symbol, lstm_artifact)
if not entry:
@@ -519,39 +468,6 @@ def _dot(left: list[float], right: list[float]) -> float:
return sum(left[index] * right[index] for index in range(min(len(left), len(right))))
def _lstm_predict(
returns: list[float],
settings: Settings,
symbol: str | None,
lstm_artifact: dict[str, Any],
) -> float:
params = _lstm_params(settings, symbol, lstm_artifact)
lookback = int(params["lookback"])
units = int(params["units"])
ridge = float(params["ridge"])
if len(returns) <= lookback + 8:
return _predict_next_return("drift", returns)
scale = _return_scale(returns)
normalized = [_clamp(value / scale, -6.0, 6.0) for value in returns]
states = _lstm_states(normalized, units)
rows: list[list[float]] = []
targets: list[float] = []
for index in range(lookback, len(returns)):
rows.append([1.0] + states[index - 1])
targets.append(normalized[index])
coeffs = _ols(rows, targets, ridge)
if not coeffs:
return _predict_next_return("drift", returns)
features = [1.0] + states[-1]
prediction = sum(coeff * feature for coeff, feature in zip(coeffs, features))
prediction = _clamp(prediction, -4.0, 4.0) * scale
recent_abs = sorted(abs(value) for value in returns[-48:]) if len(returns) >= 8 else [0.01]
cap = max(recent_abs[int(len(recent_abs) * 0.9)], 0.0002)
return _clamp(prediction, -cap, cap)
def _return_scale(returns: list[float]) -> float:
recent = returns[-120:] if len(returns) > 120 else returns
values = sorted(abs(value) for value in recent if math.isfinite(value))
@@ -562,67 +478,6 @@ def _return_scale(returns: list[float]) -> float:
return max(max(median, mean * 0.5), 1e-5)
def _lstm_states(normalized_returns: list[float], units: int) -> list[list[float]]:
weights = _lstm_weights(units)
hidden = [0.0 for _ in range(units)]
cell = [0.0 for _ in range(units)]
states: list[list[float]] = []
for value in normalized_returns:
hidden, cell = _lstm_step(value, hidden, cell, weights)
states.append(hidden[:])
return states
@lru_cache(maxsize=16)
def _lstm_weights(units: int) -> tuple[list[list[float]], list[list[list[float]]], list[list[float]]]:
input_weights: list[list[float]] = []
recurrent_weights: list[list[list[float]]] = []
biases: list[list[float]] = []
base_biases = (-0.15, 0.70, 0.05, 0.0)
for gate in range(4):
gate_input: list[float] = []
gate_recurrent: list[list[float]] = []
gate_bias: list[float] = []
for unit in range(units):
gate_input.append(0.55 * math.sin((gate + 1) * (unit + 1) * 1.61803398875))
gate_recurrent.append(
[
0.14 * math.sin((gate + 3) * (unit + 1) * (source + 1) * 0.731)
for source in range(units)
]
)
gate_bias.append(base_biases[gate] + 0.03 * math.sin((gate + 1) * (unit + 1)))
input_weights.append(gate_input)
recurrent_weights.append(gate_recurrent)
biases.append(gate_bias)
return input_weights, recurrent_weights, biases
def _lstm_step(
value: float,
hidden: list[float],
cell: list[float],
weights: tuple[list[list[float]], list[list[list[float]]], list[list[float]]],
) -> tuple[list[float], list[float]]:
input_weights, recurrent_weights, biases = weights
units = len(hidden)
next_hidden = [0.0 for _ in range(units)]
next_cell = [0.0 for _ in range(units)]
for unit in range(units):
gate_values = []
for gate in range(4):
raw = input_weights[gate][unit] * value + biases[gate][unit]
raw += sum(recurrent_weights[gate][unit][source] * hidden[source] for source in range(units))
gate_values.append(raw)
input_gate = _sigmoid(gate_values[0])
forget_gate = _sigmoid(gate_values[1])
output_gate = _sigmoid(gate_values[2])
candidate = math.tanh(gate_values[3])
next_cell[unit] = forget_gate * cell[unit] + input_gate * candidate
next_hidden[unit] = output_gate * math.tanh(next_cell[unit])
return next_hidden, next_cell
def _sigmoid(value: float) -> float:
if value >= 40:
return 1.0