Explain Torch forecast inputs in dashboard
This commit is contained in:
@@ -511,6 +511,62 @@ HTML = r"""
|
|||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
.forecast-chip b { display: block; color: var(--muted); font-size: 10px; margin-bottom: 2px; }
|
.forecast-chip b { display: block; color: var(--muted); font-size: 10px; margin-bottom: 2px; }
|
||||||
|
.feature-panel {
|
||||||
|
margin: 0 0 9px;
|
||||||
|
border: 1px solid var(--border);
|
||||||
|
border-radius: 7px;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
.feature-panel summary {
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 8px 9px;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 850;
|
||||||
|
color: var(--accent);
|
||||||
|
list-style-position: inside;
|
||||||
|
}
|
||||||
|
.feature-note {
|
||||||
|
padding: 0 9px 8px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 11px;
|
||||||
|
line-height: 1.35;
|
||||||
|
}
|
||||||
|
.feature-table-wrap {
|
||||||
|
max-height: 360px;
|
||||||
|
overflow: auto;
|
||||||
|
border-top: 1px solid var(--border);
|
||||||
|
}
|
||||||
|
.feature-table {
|
||||||
|
font-size: 12px;
|
||||||
|
min-width: 720px;
|
||||||
|
}
|
||||||
|
.feature-table th,
|
||||||
|
.feature-table td {
|
||||||
|
white-space: normal;
|
||||||
|
vertical-align: top;
|
||||||
|
padding: 7px 8px;
|
||||||
|
}
|
||||||
|
.feature-table .param-name {
|
||||||
|
font-weight: 850;
|
||||||
|
color: var(--text);
|
||||||
|
}
|
||||||
|
.feature-table .technical-name {
|
||||||
|
margin-top: 2px;
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 10px;
|
||||||
|
word-break: break-word;
|
||||||
|
}
|
||||||
|
.feature-group {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 20px;
|
||||||
|
padding: 2px 6px;
|
||||||
|
border-radius: 5px;
|
||||||
|
background: var(--panel-2);
|
||||||
|
color: var(--muted);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
.layout { grid-template-columns: 1.2fr 0.8fr; align-items: start; }
|
.layout { grid-template-columns: 1.2fr 0.8fr; align-items: start; }
|
||||||
.panel { padding: 14px; min-width: 0; }
|
.panel { padding: 14px; min-width: 0; }
|
||||||
.table-wrap { overflow: auto; max-height: 330px; border: 1px solid var(--border); border-radius: 7px; }
|
.table-wrap { overflow: auto; max-height: 330px; border: 1px solid var(--border); border-radius: 7px; }
|
||||||
@@ -767,6 +823,7 @@ HTML = r"""
|
|||||||
<span>${num(pattern.score, 2)}</span>
|
<span>${num(pattern.score, 2)}</span>
|
||||||
</div>
|
</div>
|
||||||
${forecastHtml(market.forecast || {})}
|
${forecastHtml(market.forecast || {})}
|
||||||
|
${featureSnapshotHtml(market.forecast || {})}
|
||||||
<canvas width="520" height="220"></canvas>
|
<canvas width="520" height="220"></canvas>
|
||||||
<div class="indicators">
|
<div class="indicators">
|
||||||
<div class="indicator"><b>RSI14</b>${num(last.rsi_14, 2)}</div>
|
<div class="indicator"><b>RSI14</b>${num(last.rsi_14, 2)}</div>
|
||||||
@@ -847,6 +904,43 @@ HTML = r"""
|
|||||||
</div>`;
|
</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function featureSnapshotHtml(forecast) {
|
||||||
|
const rows = Array.isArray(forecast?.feature_snapshot) ? forecast.feature_snapshot : [];
|
||||||
|
if (!rows.length) return '';
|
||||||
|
const body = rows.map(feature => `
|
||||||
|
<tr>
|
||||||
|
<td>
|
||||||
|
<div class="param-name">${escapeHtml(feature.label || feature.name || '-')}</div>
|
||||||
|
<div class="technical-name">${escapeHtml(feature.name || '')}</div>
|
||||||
|
</td>
|
||||||
|
<td><span class="feature-group">${escapeHtml(feature.group || '-')}</span></td>
|
||||||
|
<td>${escapeHtml(feature.raw_display ?? valueText(feature.raw_value))}</td>
|
||||||
|
<td>${escapeHtml(feature.model_display ?? valueText(feature.model_value))}</td>
|
||||||
|
<td>${escapeHtml(feature.meaning || '')}</td>
|
||||||
|
<td>${escapeHtml(feature.interpretation || '')}</td>
|
||||||
|
</tr>
|
||||||
|
`).join('');
|
||||||
|
return `<details class="feature-panel">
|
||||||
|
<summary>Входы нейросети (${rows.length})</summary>
|
||||||
|
<div class="feature-note">Текущее значение берется с последней закрытой свечи. Значение в модели - это нормализованный вход после обучающих mean/scale; именно его получает LSTM/GRU.</div>
|
||||||
|
<div class="feature-table-wrap">
|
||||||
|
<table class="feature-table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Параметр</th>
|
||||||
|
<th>Группа</th>
|
||||||
|
<th>Сейчас</th>
|
||||||
|
<th>В модели</th>
|
||||||
|
<th>Что это</th>
|
||||||
|
<th>Что значит</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>${body}</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</details>`;
|
||||||
|
}
|
||||||
|
|
||||||
function drawCandles(canvas, candles) {
|
function drawCandles(canvas, candles) {
|
||||||
const ctx = canvas.getContext('2d');
|
const ctx = canvas.getContext('2d');
|
||||||
const w = canvas.width, h = canvas.height;
|
const w = canvas.width, h = canvas.height;
|
||||||
@@ -1117,6 +1211,10 @@ HTML = r"""
|
|||||||
const sign = parsed > 0 ? '+' : '';
|
const sign = parsed > 0 ? '+' : '';
|
||||||
return `${sign}${parsed.toFixed(digits)}`;
|
return `${sign}${parsed.toFixed(digits)}`;
|
||||||
}
|
}
|
||||||
|
function valueText(value, digits = 4) {
|
||||||
|
const parsed = Number(value);
|
||||||
|
return Number.isFinite(parsed) ? parsed.toFixed(digits) : '-';
|
||||||
|
}
|
||||||
function escapeHtml(value) {
|
function escapeHtml(value) {
|
||||||
return String(value).replace(/[&<>"']/g, ch => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));
|
return String(value).replace(/[&<>"']/g, ch => ({'&':'&','<':'<','>':'>','"':'"',"'":'''}[ch]));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -69,6 +69,65 @@ DEFAULT_TORCH_FEATURES = (
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
FEATURE_DESCRIPTIONS: dict[str, tuple[str, str, str]] = {
|
||||||
|
"return_1": ("Цена", "Доходность 1ч", "Изменение цены закрытия за последнюю 1h свечу."),
|
||||||
|
"return_3": ("Цена", "Доходность 3ч", "Изменение цены закрытия за последние 3 часовые свечи."),
|
||||||
|
"return_6": ("Цена", "Доходность 6ч", "Изменение цены закрытия за последние 6 часовых свечей."),
|
||||||
|
"return_12": ("Цена", "Доходность 12ч", "Изменение цены закрытия за последние 12 часовых свечей."),
|
||||||
|
"return_24": ("Цена", "Доходность 24ч", "Изменение цены закрытия за последние 24 часовые свечи."),
|
||||||
|
"range_percent": ("Свеча", "Диапазон свечи", "Размер high-low последней свечи относительно цены закрытия."),
|
||||||
|
"body_percent": ("Свеча", "Тело свечи", "Разница close-open относительно цены закрытия; знак показывает цвет свечи."),
|
||||||
|
"upper_wick_percent": ("Свеча", "Верхняя тень", "Насколько далеко цена уходила выше тела свечи."),
|
||||||
|
"lower_wick_percent": ("Свеча", "Нижняя тень", "Насколько далеко цена уходила ниже тела свечи."),
|
||||||
|
"volume_change": ("Объем", "Изменение объема", "Изменение объема последней свечи относительно предыдущей."),
|
||||||
|
"volume_ratio": ("Объем", "Объем к MA20", "Отклонение текущего объема от средней за 20 свечей."),
|
||||||
|
"volume_percentile_20": ("Объем", "Процентиль объема", "Позиция текущего объема среди последних 20 свечей."),
|
||||||
|
"atr_percent": ("Волатильность", "ATR14 %", "Средний торговый диапазон ATR14 относительно цены."),
|
||||||
|
"atr_ratio_20": ("Волатильность", "ATR к среднему", "Отклонение текущего ATR от среднего ATR за 20 свечей."),
|
||||||
|
"realized_volatility_12": ("Волатильность", "Реализованная вола 12ч", "Фактическая волатильность доходностей за 12 свечей."),
|
||||||
|
"realized_volatility_24": ("Волатильность", "Реализованная вола 24ч", "Фактическая волатильность доходностей за 24 свечи."),
|
||||||
|
"rsi_centered": ("Индикаторы", "RSI14 от 50", "RSI14, приведенный к центру 50: выше нуля сильнее покупатели."),
|
||||||
|
"rsi_slope_6": ("Индикаторы", "Наклон RSI 6ч", "Изменение RSI14 за последние 6 свечей."),
|
||||||
|
"macd_hist_percent": ("Индикаторы", "MACD histogram", "MACD histogram относительно цены; знак показывает импульс."),
|
||||||
|
"macd_hist_slope_3": ("Индикаторы", "Наклон MACD hist", "Изменение MACD histogram за последние 3 свечи."),
|
||||||
|
"ema50_gap_percent": ("EMA/тренд", "Цена к EMA50", "Расстояние цены закрытия до EMA50."),
|
||||||
|
"ema200_gap_percent": ("EMA/тренд", "Цена к EMA200", "Расстояние цены закрытия до EMA200."),
|
||||||
|
"ema20_slope_6": ("EMA/тренд", "Наклон EMA20", "Изменение EMA20 за последние 6 свечей."),
|
||||||
|
"ema50_slope_12": ("EMA/тренд", "Наклон EMA50", "Изменение EMA50 за последние 12 свечей."),
|
||||||
|
"ema200_slope_24": ("EMA/тренд", "Наклон EMA200", "Изменение EMA200 за последние 24 свечи."),
|
||||||
|
"ema50_ema200_gap_percent": ("EMA/тренд", "EMA50 к EMA200", "Расстояние EMA50 относительно EMA200."),
|
||||||
|
"range_position_50": ("Цена", "Позиция в диапазоне 50ч", "Где текущая цена внутри high-low диапазона последних 50 свечей."),
|
||||||
|
"trend_return_4h": ("Цена", "Тренд 4ч", "Изменение цены за последние 4 свечи."),
|
||||||
|
"trend_return_24h": ("Цена", "Тренд 24ч", "Изменение цены за последние 24 свечи."),
|
||||||
|
"daily_close_ema200_gap_percent": ("Дневной тренд", "D цена к EMA200", "Расстояние дневного close до дневной EMA200."),
|
||||||
|
"daily_ema50_ema200_gap_percent": ("Дневной тренд", "D EMA50 к EMA200", "Расстояние дневной EMA50 относительно дневной EMA200."),
|
||||||
|
"daily_ema50_slope": ("Дневной тренд", "D наклон EMA50", "Изменение дневной EMA50 за последние несколько дневных свечей."),
|
||||||
|
"btc_return_1": ("BTC/ETH контекст", "BTC 1ч", "Изменение BTCUSDT за последнюю 1h свечу."),
|
||||||
|
"btc_return_3": ("BTC/ETH контекст", "BTC 3ч", "Изменение BTCUSDT за последние 3 часа."),
|
||||||
|
"btc_return_6": ("BTC/ETH контекст", "BTC 6ч", "Изменение BTCUSDT за последние 6 часов."),
|
||||||
|
"btc_return_24": ("BTC/ETH контекст", "BTC 24ч", "Изменение BTCUSDT за последние 24 часа."),
|
||||||
|
"eth_return_1": ("BTC/ETH контекст", "ETH 1ч", "Изменение ETHUSDT за последнюю 1h свечу."),
|
||||||
|
"eth_return_3": ("BTC/ETH контекст", "ETH 3ч", "Изменение ETHUSDT за последние 3 часа."),
|
||||||
|
"eth_return_6": ("BTC/ETH контекст", "ETH 6ч", "Изменение ETHUSDT за последние 6 часов."),
|
||||||
|
"eth_return_24": ("BTC/ETH контекст", "ETH 24ч", "Изменение ETHUSDT за последние 24 часа."),
|
||||||
|
"relative_btc_return_3": ("BTC/ETH контекст", "Сила к BTC 3ч", "Доходность пары за 3 часа минус доходность BTC за 3 часа."),
|
||||||
|
"relative_eth_return_3": ("BTC/ETH контекст", "Сила к ETH 3ч", "Доходность пары за 3 часа минус доходность ETH за 3 часа."),
|
||||||
|
"btc_eth_return_spread_3": ("BTC/ETH контекст", "BTC-ETH 3ч", "Разница 3-часовой доходности BTC и ETH."),
|
||||||
|
"pattern_score": ("Шаблон", "Оценка шаблона", "Числовая оценка текущего рыночного шаблона от 0 до 1."),
|
||||||
|
"pattern_bullish": ("Шаблон", "Бычий шаблон", "Флаг, что текущий шаблон похож на бычий."),
|
||||||
|
"pattern_bearish": ("Шаблон", "Медвежий шаблон", "Флаг, что текущий шаблон похож на медвежий."),
|
||||||
|
"pattern_range": ("Шаблон", "Флэтовый шаблон", "Флаг, что рынок похож на диапазон/флэт."),
|
||||||
|
"pattern_pullback": ("Шаблон", "Откат", "Флаг отката внутри восходящего движения."),
|
||||||
|
"pattern_oversold_reversal": ("Шаблон", "Перепроданный разворот", "Флаг возможного разворота после перепроданности."),
|
||||||
|
"pattern_stabilized_drop": ("Шаблон", "Падение стабилизировалось", "Флаг замедления падения и попытки стабилизации."),
|
||||||
|
"pattern_breakout": ("Шаблон", "Пробой вверх", "Флаг пробоя верхней части диапазона с объемом."),
|
||||||
|
"pattern_breakdown": ("Шаблон", "Пробой вниз", "Флаг пробоя нижней части диапазона с объемом."),
|
||||||
|
"pattern_fast_drop": ("Шаблон", "Быстрое падение", "Флаг резкого снижения или сильной перепроданности."),
|
||||||
|
"pattern_volume_spike": ("Шаблон", "Всплеск объема", "Флаг объема заметно выше обычного."),
|
||||||
|
"pattern_range_position_20": ("Шаблон", "Позиция в диапазоне 20ч", "Где цена внутри high-low диапазона последних 20 свечей."),
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
@dataclass(slots=True)
|
@dataclass(slots=True)
|
||||||
class TimeSeriesForecast:
|
class TimeSeriesForecast:
|
||||||
enabled: bool
|
enabled: bool
|
||||||
@@ -92,6 +151,7 @@ class TimeSeriesForecast:
|
|||||||
quantile_90_percent: float
|
quantile_90_percent: float
|
||||||
conservative_return_percent: float
|
conservative_return_percent: float
|
||||||
target_transform: str
|
target_transform: str
|
||||||
|
feature_snapshot: list[dict[str, Any]] = field(default_factory=list)
|
||||||
horizon_forecasts: dict[str, Any] = field(default_factory=dict)
|
horizon_forecasts: dict[str, Any] = field(default_factory=dict)
|
||||||
candidates: list[dict[str, Any]] = field(default_factory=list)
|
candidates: list[dict[str, Any]] = field(default_factory=list)
|
||||||
|
|
||||||
@@ -126,6 +186,7 @@ class TimeSeriesForecaster:
|
|||||||
artifact = self._load_lstm_artifact()
|
artifact = self._load_lstm_artifact()
|
||||||
entry = _torch_recurrent_entry(symbol, artifact)
|
entry = _torch_recurrent_entry(symbol, artifact)
|
||||||
model = _torch_recurrent_model_name(symbol, artifact)
|
model = _torch_recurrent_model_name(symbol, artifact)
|
||||||
|
clip = _clamp(_float_entry(entry or {}, "clip", 8.0), 1.0, 50.0)
|
||||||
feature_rows = (
|
feature_rows = (
|
||||||
_feature_matrix(
|
_feature_matrix(
|
||||||
candles,
|
candles,
|
||||||
@@ -137,6 +198,7 @@ class TimeSeriesForecaster:
|
|||||||
if entry
|
if entry
|
||||||
else []
|
else []
|
||||||
)
|
)
|
||||||
|
feature_snapshot = _feature_snapshot(feature_rows, entry, clip)
|
||||||
if not model or not _can_use_torch_recurrent(returns, symbol, artifact, feature_rows):
|
if not model or not _can_use_torch_recurrent(returns, symbol, artifact, feature_rows):
|
||||||
return _empty_forecast(True, "no valid PyTorch LSTM/GRU model for symbol")
|
return _empty_forecast(True, "no valid PyTorch LSTM/GRU model for symbol")
|
||||||
|
|
||||||
@@ -215,6 +277,7 @@ class TimeSeriesForecaster:
|
|||||||
quantile_90_percent=round(q90_percent, 4),
|
quantile_90_percent=round(q90_percent, 4),
|
||||||
conservative_return_percent=round(conservative_return_percent, 4),
|
conservative_return_percent=round(conservative_return_percent, 4),
|
||||||
target_transform=str(entry.get("target_transform", "net_return_over_volatility")),
|
target_transform=str(entry.get("target_transform", "net_return_over_volatility")),
|
||||||
|
feature_snapshot=feature_snapshot,
|
||||||
horizon_forecasts=_public_horizon_forecasts(prediction),
|
horizon_forecasts=_public_horizon_forecasts(prediction),
|
||||||
candidates=[{"model": model, "mae_percent": round(model_mae * 100, 4)}],
|
candidates=[{"model": model, "mae_percent": round(model_mae * 100, 4)}],
|
||||||
)
|
)
|
||||||
@@ -274,6 +337,7 @@ class TimeSeriesForecaster:
|
|||||||
quantile_90_percent=round(expected_return_percent + volatility_percent, 4),
|
quantile_90_percent=round(expected_return_percent + volatility_percent, 4),
|
||||||
conservative_return_percent=round(expected_return_percent, 4),
|
conservative_return_percent=round(expected_return_percent, 4),
|
||||||
target_transform=str(entry.get("target_transform", "direct_log_return")),
|
target_transform=str(entry.get("target_transform", "direct_log_return")),
|
||||||
|
feature_snapshot=feature_snapshot,
|
||||||
horizon_forecasts={},
|
horizon_forecasts={},
|
||||||
candidates=[{"model": model, "mae_percent": round(model_mae * 100, 4)}],
|
candidates=[{"model": model, "mae_percent": round(model_mae * 100, 4)}],
|
||||||
)
|
)
|
||||||
@@ -322,6 +386,7 @@ def _empty_forecast(enabled: bool, reason: str) -> TimeSeriesForecast:
|
|||||||
quantile_90_percent=0.0,
|
quantile_90_percent=0.0,
|
||||||
conservative_return_percent=0.0,
|
conservative_return_percent=0.0,
|
||||||
target_transform="none",
|
target_transform="none",
|
||||||
|
feature_snapshot=[],
|
||||||
horizon_forecasts={},
|
horizon_forecasts={},
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1033,6 +1098,156 @@ def _normalize_feature_rows(rows: list[list[float]], entry: dict[str, Any], clip
|
|||||||
return normalized
|
return normalized
|
||||||
|
|
||||||
|
|
||||||
|
def _feature_snapshot(
|
||||||
|
feature_rows: list[list[float]],
|
||||||
|
entry: dict[str, Any] | None,
|
||||||
|
clip: float,
|
||||||
|
) -> list[dict[str, Any]]:
|
||||||
|
if not entry or not feature_rows:
|
||||||
|
return []
|
||||||
|
names = _feature_names(entry)
|
||||||
|
latest = feature_rows[-1]
|
||||||
|
normalized_rows = _normalize_feature_rows([latest], entry, clip)
|
||||||
|
normalized = normalized_rows[-1] if normalized_rows else []
|
||||||
|
means = _float_vector(entry.get("feature_means"))
|
||||||
|
scales = _float_vector(entry.get("feature_scales"))
|
||||||
|
snapshot: list[dict[str, Any]] = []
|
||||||
|
for index, name in enumerate(names):
|
||||||
|
raw_value = float(latest[index]) if index < len(latest) else 0.0
|
||||||
|
model_value = float(normalized[index]) if index < len(normalized) else 0.0
|
||||||
|
group, label, meaning = FEATURE_DESCRIPTIONS.get(
|
||||||
|
name,
|
||||||
|
("Прочее", name, "Технический входной признак модели."),
|
||||||
|
)
|
||||||
|
snapshot.append(
|
||||||
|
{
|
||||||
|
"name": name,
|
||||||
|
"label": label,
|
||||||
|
"group": group,
|
||||||
|
"raw_value": round(raw_value, 10),
|
||||||
|
"raw_display": _feature_raw_display(name, raw_value),
|
||||||
|
"model_value": round(model_value, 4),
|
||||||
|
"model_display": f"{model_value:+.2f}",
|
||||||
|
"mean": round(float(means[index]), 10) if index < len(means) else 0.0,
|
||||||
|
"scale": round(float(scales[index]), 10) if index < len(scales) else 1.0,
|
||||||
|
"meaning": meaning,
|
||||||
|
"interpretation": _feature_interpretation(name, raw_value, model_value),
|
||||||
|
}
|
||||||
|
)
|
||||||
|
return snapshot
|
||||||
|
|
||||||
|
|
||||||
|
def _feature_raw_display(name: str, value: float) -> str:
|
||||||
|
if _feature_is_log_percent(name):
|
||||||
|
return f"{(math.exp(value) - 1) * 100:+.3f}%"
|
||||||
|
if _feature_is_linear_percent(name):
|
||||||
|
return f"{value * 100:+.3f}%"
|
||||||
|
if name in {"rsi_centered"}:
|
||||||
|
return f"RSI {value * 50 + 50:.1f}"
|
||||||
|
if name in {"rsi_slope_6"}:
|
||||||
|
return f"{value * 50:+.2f} RSI"
|
||||||
|
if name in {"volume_percentile_20", "range_position_50", "pattern_range_position_20"}:
|
||||||
|
return f"{value * 100:.1f}%"
|
||||||
|
if name.startswith("pattern_") and name != "pattern_score":
|
||||||
|
return "да" if value >= 0.5 else "нет"
|
||||||
|
if name == "pattern_score":
|
||||||
|
return f"{value:.2f}"
|
||||||
|
return f"{value:+.4f}"
|
||||||
|
|
||||||
|
|
||||||
|
def _feature_interpretation(name: str, value: float, model_value: float) -> str:
|
||||||
|
norm = _model_value_text(model_value)
|
||||||
|
if name.startswith("pattern_") and name != "pattern_score" and name != "pattern_range_position_20":
|
||||||
|
state = "шаблон активен" if value >= 0.5 else "шаблон не активен"
|
||||||
|
return f"{state}; {norm}."
|
||||||
|
if name in {"volume_percentile_20", "range_position_50", "pattern_range_position_20"}:
|
||||||
|
if value >= 0.8:
|
||||||
|
state = "значение находится в верхней части диапазона"
|
||||||
|
elif value <= 0.2:
|
||||||
|
state = "значение находится в нижней части диапазона"
|
||||||
|
else:
|
||||||
|
state = "значение около середины диапазона"
|
||||||
|
return f"{state}; {norm}."
|
||||||
|
if name in {"volume_ratio", "atr_ratio_20"}:
|
||||||
|
state = "выше среднего" if value > 0 else "ниже среднего" if value < 0 else "около среднего"
|
||||||
|
return f"{state}; {norm}."
|
||||||
|
if name == "rsi_centered":
|
||||||
|
rsi = value * 50 + 50
|
||||||
|
if rsi >= 65:
|
||||||
|
state = "RSI высокий"
|
||||||
|
elif rsi <= 35:
|
||||||
|
state = "RSI низкий"
|
||||||
|
else:
|
||||||
|
state = "RSI в средней зоне"
|
||||||
|
return f"{state}; {norm}."
|
||||||
|
if _feature_is_log_percent(name) or _feature_is_linear_percent(name) or name.endswith("_slope"):
|
||||||
|
if value > 0:
|
||||||
|
state = "положительное значение"
|
||||||
|
elif value < 0:
|
||||||
|
state = "отрицательное значение"
|
||||||
|
else:
|
||||||
|
state = "нейтральное значение"
|
||||||
|
return f"{state}; {norm}."
|
||||||
|
if name == "pattern_score":
|
||||||
|
if value >= 0.65:
|
||||||
|
state = "шаблон скорее поддерживает long"
|
||||||
|
elif value <= 0.35:
|
||||||
|
state = "шаблон скорее против long"
|
||||||
|
else:
|
||||||
|
state = "шаблон нейтральный"
|
||||||
|
return f"{state}; {norm}."
|
||||||
|
return norm + "."
|
||||||
|
|
||||||
|
|
||||||
|
def _model_value_text(value: float) -> str:
|
||||||
|
magnitude = abs(value)
|
||||||
|
if magnitude >= 2.0:
|
||||||
|
return "для модели это сильное отклонение от обучающей нормы"
|
||||||
|
if magnitude >= 1.0:
|
||||||
|
return "для модели это заметное отклонение от обучающей нормы"
|
||||||
|
return "для модели это близко к обычному диапазону"
|
||||||
|
|
||||||
|
|
||||||
|
def _feature_is_log_percent(name: str) -> bool:
|
||||||
|
return (
|
||||||
|
name.startswith("return_")
|
||||||
|
or name.startswith("btc_return_")
|
||||||
|
or name.startswith("eth_return_")
|
||||||
|
or name.startswith("relative_")
|
||||||
|
or name in {
|
||||||
|
"volume_change",
|
||||||
|
"trend_return_4h",
|
||||||
|
"trend_return_24h",
|
||||||
|
"ema20_slope_6",
|
||||||
|
"ema50_slope_12",
|
||||||
|
"ema200_slope_24",
|
||||||
|
"daily_ema50_slope",
|
||||||
|
"btc_eth_return_spread_3",
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _feature_is_linear_percent(name: str) -> bool:
|
||||||
|
return name in {
|
||||||
|
"range_percent",
|
||||||
|
"body_percent",
|
||||||
|
"upper_wick_percent",
|
||||||
|
"lower_wick_percent",
|
||||||
|
"volume_ratio",
|
||||||
|
"atr_percent",
|
||||||
|
"atr_ratio_20",
|
||||||
|
"realized_volatility_12",
|
||||||
|
"realized_volatility_24",
|
||||||
|
"macd_hist_percent",
|
||||||
|
"macd_hist_slope_3",
|
||||||
|
"ema50_gap_percent",
|
||||||
|
"ema200_gap_percent",
|
||||||
|
"ema50_ema200_gap_percent",
|
||||||
|
"daily_close_ema200_gap_percent",
|
||||||
|
"daily_ema50_ema200_gap_percent",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _torch_recurrent_hidden(
|
def _torch_recurrent_hidden(
|
||||||
sequence: list[list[float]],
|
sequence: list[list[float]],
|
||||||
*,
|
*,
|
||||||
|
|||||||
@@ -322,3 +322,7 @@ def test_time_series_forecaster_reads_probabilistic_multi_horizon_artifact(make_
|
|||||||
assert forecast.probability_up > 0.85
|
assert forecast.probability_up > 0.85
|
||||||
assert forecast.quantile_10_percent <= forecast.quantile_50_percent <= forecast.quantile_90_percent
|
assert forecast.quantile_10_percent <= forecast.quantile_50_percent <= forecast.quantile_90_percent
|
||||||
assert sorted(forecast.horizon_forecasts) == ["1", "3"]
|
assert sorted(forecast.horizon_forecasts) == ["1", "3"]
|
||||||
|
assert [item["name"] for item in forecast.feature_snapshot] == ["return_1", "range_percent"]
|
||||||
|
assert forecast.feature_snapshot[0]["label"] == "Доходность 1ч"
|
||||||
|
assert forecast.feature_snapshot[0]["raw_display"].endswith("%")
|
||||||
|
assert "диапазон" in forecast.feature_snapshot[0]["interpretation"]
|
||||||
|
|||||||
Reference in New Issue
Block a user