Clarify forecast model labels in dashboard
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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"],
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user