diff --git a/crypto_spot_bot/dashboard.py b/crypto_spot_bot/dashboard.py index c390d62..415c002 100644 --- a/crypto_spot_bot/dashboard.py +++ b/crypto_spot_bot/dashboard.py @@ -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""" @@ -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 `