Clarify forecast model labels in dashboard

This commit is contained in:
Codex
2026-06-20 21:37:34 +03:00
parent 92538850ad
commit d9c0317675
2 changed files with 155 additions and 8 deletions
+123 -8
View File
@@ -221,6 +221,7 @@ def _safe_config(settings: Settings) -> dict[str, Any]:
"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,
"take_profit_percent": settings.take_profit_percent,
"trailing_stop_percent": settings.trailing_stop_percent,
@@ -235,6 +236,65 @@ def _safe_config(settings: Settings) -> dict[str, Any]:
}
def _time_series_model_artifact(settings: Settings) -> dict[str, Any]:
path = settings.time_series_lstm_model_path
try:
data = json.loads(path.read_text(encoding="utf-8"))
except (OSError, json.JSONDecodeError):
return {
"available": False,
"type": "missing",
"label": "нет файла модели",
"symbol_count": 0,
"models": [],
}
if not isinstance(data, dict):
return {
"available": False,
"type": "invalid",
"label": "файл модели не распознан",
"symbol_count": 0,
"models": [],
}
symbols = data.get("symbols")
rows = list(symbols.values()) if isinstance(symbols, dict) else []
models = sorted(
{
_forecast_model_label(str(row.get("model", row.get("architecture", "lstm"))))
for row in rows
if isinstance(row, dict)
}
)
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 "настройки прогноза"
return {
"available": True,
"type": artifact_type or "unknown",
"label": label,
"created_at": data.get("created_at", ""),
"symbol_count": len(rows),
"models": models,
}
def _forecast_model_label(model: str) -> str:
normalized = model.strip().lower()
if normalized == "torch_lstm":
return "PyTorch LSTM"
if normalized == "torch_gru":
return "PyTorch GRU"
if normalized == "lstm":
return "легкий LSTM"
if normalized == "gru":
return "GRU"
return model
HTML = r"""
<!doctype html>
<html lang="ru">
@@ -677,12 +737,69 @@ HTML = r"""
}
}
function modelName(model) {
const key = String(model || '').toLowerCase();
const names = {
torch_lstm: 'PyTorch LSTM',
torch_gru: 'PyTorch GRU',
lstm: 'Легкий LSTM',
naive: 'Baseline',
drift: 'Drift',
ewma: 'EWMA',
ar1: 'AR(1)',
ar3: 'AR(3)',
none: '-'
};
return names[key] || String(model || '-');
}
function modelReason(reason) {
return String(reason || '')
.replaceAll('torch_lstm', 'PyTorch LSTM')
.replaceAll('torch_gru', 'PyTorch GRU')
.replaceAll('модель lstm', 'модель легкий LSTM');
}
function modelArtifactSummary(config) {
if (!config.time_series_lstm_enabled) {
return 'выкл';
}
const artifact = config.time_series_model_artifact || {};
if (!artifact.available) {
return artifact.label || 'нет файла модели';
}
const parts = [artifact.label || 'модель прогноза'];
if (Number(artifact.symbol_count || 0) > 0) {
parts.push(`${artifact.symbol_count} пар`);
}
if (Array.isArray(artifact.models) && artifact.models.length) {
parts.push(artifact.models.join(', '));
}
const date = shortDateTime(artifact.created_at);
if (date) {
parts.push(date);
}
return parts.join(' · ');
}
function shortDateTime(value) {
if (!value) return '';
const date = new Date(value);
if (Number.isNaN(date.getTime())) return '';
return date.toLocaleString('ru-RU', {
day: '2-digit',
month: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
}
function forecastHtml(forecast) {
if (!forecast || !forecast.usable) {
return `<div class="forecast-line"><div class="forecast-chip"><b>Прогноз</b>${escapeHtml(forecast?.reason || 'нет данных')}</div></div>`;
return `<div class="forecast-line"><div class="forecast-chip"><b>Прогноз</b>${escapeHtml(modelReason(forecast?.reason || 'нет данных'))}</div></div>`;
}
return `<div class="forecast-line">
<div class="forecast-chip"><b>Модель</b>${escapeHtml(forecast.model || '-')}</div>
<div class="forecast-chip"><b>Модель</b>${escapeHtml(modelName(forecast.model || '-'))}</div>
<div class="forecast-chip"><b>P роста</b>${num((forecast.probability_up || 0) * 100, 1)}%</div>
<div class="forecast-chip"><b>Ожидание</b><span class="${signedClass(forecast.expected_return_percent || 0)}">${signedNum(forecast.expected_return_percent, 3)}%</span></div>
<div class="forecast-chip"><b>Волат.</b>${num(forecast.volatility_percent, 3)}%</div>
@@ -781,7 +898,7 @@ HTML = r"""
const expected = Number(forecast.expected_return_percent);
const probability = Number(forecast.probability_up);
const skill = Number(forecast.skill);
const model = escapeHtml(forecast.model || '-');
const model = escapeHtml(modelName(forecast.model || '-'));
const expectedText = Number.isFinite(expected) ? `${signedNum(expected, 3)}%` : '-';
const probabilityText = Number.isFinite(probability) ? `P${num(probability * 100, 1)}%` : 'P-';
const skillText = Number.isFinite(skill) ? `S${num(skill, 3)}` : 'S-';
@@ -817,10 +934,8 @@ HTML = r"""
['Модельный горизонт', `${config.time_series_forecast_horizon} свечи`],
['Walk-forward окно', `${config.time_series_validation_window} свечей`],
['Мин. edge прогноза', `${num(config.time_series_min_edge_percent, 3)}%`],
['LSTM-кандидат', yesNo(config.time_series_lstm_enabled)],
['LSTM окно / юниты', `${config.time_series_lstm_lookback} / ${config.time_series_lstm_units}`],
['LSTM ridge', `${num(config.time_series_lstm_ridge, 5)}`],
['LSTM файл', config.time_series_lstm_model_path || '-'],
['Нейропрогноз', modelArtifactSummary(config)],
['Файл модели', config.time_series_lstm_model_path || '-'],
['Лимит в позициях', money(config.max_total_exposure_usdt)],
['Лимит позиций', `${config.max_open_positions} всего / ${config.max_positions_per_symbol} на пару`],
['Стоп / цель', `${num(config.stop_loss_percent * 100, 2)}% / ${num(config.take_profit_percent * 100, 2)}%`],
@@ -831,7 +946,7 @@ HTML = r"""
['Поток данных', yesNo(config.websocket_enabled)]
];
document.getElementById('configGrid').innerHTML = keys.map(([k, v]) => `
<div class="config-item"><span class="muted">${k}</span><strong>${v}</strong></div>
<div class="config-item"><span class="muted">${escapeHtml(k)}</span><strong>${escapeHtml(String(v))}</strong></div>
`).join('');
}
function renderFastMode(config) {
+32
View File
@@ -1,6 +1,9 @@
from __future__ import annotations
import json
from crypto_spot_bot.dashboard import _apply_fast_trading
from crypto_spot_bot.dashboard import _safe_config
from crypto_spot_bot.storage import Storage
@@ -15,3 +18,32 @@ def test_apply_fast_trading_updates_runtime_and_env(make_settings, tmp_path) ->
assert settings.fast_trading_enabled is True
assert storage.get_runtime("fast_trading_enabled") is True
assert "FAST_TRADING_ENABLED=true" in settings.env_file_path.read_text(encoding="utf-8")
def test_safe_config_summarizes_torch_forecast_artifact(make_settings, tmp_path) -> None:
artifact_path = tmp_path / "lstm_forecaster.json"
artifact_path.write_text(
json.dumps(
{
"type": "pytorch_recurrent_forecaster",
"created_at": "2026-06-20T18:15:05+00:00",
"symbols": {
"BTCUSDT": {"model": "torch_lstm"},
"ETHUSDT": {"model": "torch_gru"},
},
}
),
encoding="utf-8",
)
settings = make_settings(tmp_path, time_series_lstm_model_path=artifact_path)
config = _safe_config(settings)
assert config["time_series_model_artifact"] == {
"available": True,
"type": "pytorch_recurrent_forecaster",
"label": "PyTorch LSTM/GRU",
"created_at": "2026-06-20T18:15:05+00:00",
"symbol_count": 2,
"models": ["PyTorch GRU", "PyTorch LSTM"],
}