Step 3 y 4 medio preparados. Ha habido una decision es separar en dos step distintos la eleccion de estrategias y luego su optimizacion. A partir de aqui vamos a hacer una refactorizacion quirurgica de los Steps 3 y 4.
Prompt para Char GPT: Estamos trabajando en un Trading Bot con arquitectura backend/frontend separada. Stack: - Backend: FastAPI (Python 3.12) - Frontend: HTML + Vanilla JS + Tabler UI - DB: PostgreSQL - Cache opcional: Redis - Proyecto estructurado bajo /src - Carpeta /reports fuera de src Wizard actual: Step 1 · Data Step 2 · Risk & Stops Step 3 · Strategies (actualmente mezcla validación y optimización) Step 4 · Optimization (renombrado pero no 100% ajustado aún) Decisión arquitectónica ya tomada: - Step 3 será Strategy Validation (parámetros fijos, sin grid) - Step 4 será Parameter Optimization (grid min/max/step) Importante: - Ya he duplicado los archivos para separar Step 3 y Step 4. - No queremos rehacer desde cero. - Queremos hacer una refactorización quirúrgica. - Queremos eliminar lógica de grid del Step 3. - Queremos mantener infraestructura WF, async jobs, ranking y reporting. Objetivo de esta sesión: Refactorizar Step 3 (Validation) de forma limpia y profesional partiendo del código actual. Reglas: - No romper Step 4. - No reescribir todo desde cero. - Simplificar quirúrgicamente. - Mantener coherencia de arquitectura. - Mantener compatibilidad con Step 2 (risk snapshot heredado). - Mantener generación de PDF. - Mantener botón Promote to Optimization. Te adjunto el zip completo de la carpeta src. Analiza la estructura primero. No escribas código todavía. Primero dame: 1. Un diagnóstico estructural. 2. Qué archivos tocar. 3. Qué eliminar. 4. Qué simplificar. 5. Qué mantener. 6. Orden de refactorización seguro. Después empezaremos la refactorización paso a paso. Despues empezaremos la refactorizacion paso a paso.
This commit is contained in:
362
src/calibration/optimization_inspector.py
Normal file
362
src/calibration/optimization_inspector.py
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
# src/calibration/strategies_inspector.py
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from typing import Any, Dict, List
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from src.data.storage import StorageManager
|
||||||
|
from src.utils.logger import log
|
||||||
|
|
||||||
|
from src.core.walk_forward import WalkForwardValidator
|
||||||
|
|
||||||
|
from src.risk.stops.fixed_stop import FixedStop
|
||||||
|
from src.risk.stops.trailing_stop import TrailingStop
|
||||||
|
from src.risk.stops.atr_stop import ATRStop
|
||||||
|
|
||||||
|
from src.risk.sizing.percent_risk import PercentRiskSizer
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Strategy registry (con metadata de parámetros)
|
||||||
|
# --------------------------------------------------
|
||||||
|
from src.strategies.moving_average import MovingAverageCrossover
|
||||||
|
from src.strategies.rsi_strategy import RSIStrategy
|
||||||
|
from src.strategies.buy_and_hold import BuyAndHold
|
||||||
|
|
||||||
|
|
||||||
|
STRATEGY_REGISTRY = {
|
||||||
|
"moving_average": {
|
||||||
|
"class": MovingAverageCrossover,
|
||||||
|
"params": ["fast_period", "slow_period"],
|
||||||
|
},
|
||||||
|
"rsi": {
|
||||||
|
"class": RSIStrategy,
|
||||||
|
"params": ["rsi_period", "overbought", "oversold"],
|
||||||
|
},
|
||||||
|
"buy_and_hold": {
|
||||||
|
"class": BuyAndHold,
|
||||||
|
"params": [],
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Helpers
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
def list_available_strategies() -> List[Dict[str, Any]]:
|
||||||
|
"""
|
||||||
|
Devuelve metadata completa para UI.
|
||||||
|
"""
|
||||||
|
out = []
|
||||||
|
|
||||||
|
for sid, entry in STRATEGY_REGISTRY.items():
|
||||||
|
out.append({
|
||||||
|
"strategy_id": sid,
|
||||||
|
"name": entry["class"].__name__,
|
||||||
|
"params": entry["params"],
|
||||||
|
"tags": [], # puedes rellenar más adelante
|
||||||
|
})
|
||||||
|
|
||||||
|
return out
|
||||||
|
|
||||||
|
|
||||||
|
def _build_stop_loss(stop_schema) -> object | None:
|
||||||
|
if stop_schema.type == "fixed":
|
||||||
|
return FixedStop(stop_fraction=float(stop_schema.stop_fraction))
|
||||||
|
if stop_schema.type == "trailing":
|
||||||
|
return TrailingStop(stop_fraction=float(stop_schema.stop_fraction))
|
||||||
|
if stop_schema.type == "atr":
|
||||||
|
return ATRStop(
|
||||||
|
atr_period=int(stop_schema.atr_period),
|
||||||
|
multiplier=float(stop_schema.atr_multiplier),
|
||||||
|
)
|
||||||
|
raise ValueError(f"Unknown stop type: {stop_schema.type}")
|
||||||
|
|
||||||
|
|
||||||
|
def _build_position_sizer(risk_schema) -> PercentRiskSizer:
|
||||||
|
return PercentRiskSizer(risk_fraction=float(risk_schema.risk_fraction))
|
||||||
|
|
||||||
|
|
||||||
|
def _cap_units_by_max_position_fraction(units: float, capital: float, entry_price: float, max_position_fraction: float) -> float:
|
||||||
|
max_units = (capital * max_position_fraction) / entry_price
|
||||||
|
return float(min(units, max_units))
|
||||||
|
|
||||||
|
|
||||||
|
def _accumulate_equity(initial: float, returns_pct: List[float]) -> List[float]:
|
||||||
|
eq = [float(initial)]
|
||||||
|
cur = float(initial)
|
||||||
|
for r in returns_pct:
|
||||||
|
cur *= (1.0 + float(r) / 100.0)
|
||||||
|
eq.append(float(cur))
|
||||||
|
return eq
|
||||||
|
|
||||||
|
|
||||||
|
def _build_param_values(min_v: float, max_v: float, step: float) -> List[float]:
|
||||||
|
min_v = float(min_v)
|
||||||
|
max_v = float(max_v)
|
||||||
|
step = float(step)
|
||||||
|
|
||||||
|
# Valor único si min == max
|
||||||
|
if min_v == max_v:
|
||||||
|
return [min_v]
|
||||||
|
|
||||||
|
# Valor único si step <= 1
|
||||||
|
if step <= 1:
|
||||||
|
return [min_v]
|
||||||
|
|
||||||
|
values = []
|
||||||
|
v = min_v
|
||||||
|
while v <= max_v:
|
||||||
|
values.append(v)
|
||||||
|
v += step
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Main
|
||||||
|
# --------------------------------------------------
|
||||||
|
|
||||||
|
def inspect_strategies_config(
|
||||||
|
*,
|
||||||
|
storage: StorageManager,
|
||||||
|
payload,
|
||||||
|
data_quality: Dict[str, Any],
|
||||||
|
include_series: bool,
|
||||||
|
progress_callback=None,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
|
||||||
|
df = storage.load_ohlcv(symbol=payload.symbol, timeframe=payload.timeframe)
|
||||||
|
if df is None or df.empty:
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"status": "fail",
|
||||||
|
"checks": {},
|
||||||
|
"message": "No OHLCV data",
|
||||||
|
"results": [],
|
||||||
|
}
|
||||||
|
|
||||||
|
checks: Dict[str, Any] = {}
|
||||||
|
checks["data_quality"] = {
|
||||||
|
"status": data_quality.get("status", "unknown"),
|
||||||
|
"message": data_quality.get("message", ""),
|
||||||
|
}
|
||||||
|
|
||||||
|
if data_quality.get("status") == "fail":
|
||||||
|
return {
|
||||||
|
"valid": False,
|
||||||
|
"status": "fail",
|
||||||
|
"checks": checks,
|
||||||
|
"message": "Step 1 data quality is FAIL. Strategies cannot be validated.",
|
||||||
|
"results": [],
|
||||||
|
"series": {} if include_series else None,
|
||||||
|
}
|
||||||
|
|
||||||
|
stop_loss = _build_stop_loss(payload.stop)
|
||||||
|
base_sizer = _build_position_sizer(payload.risk)
|
||||||
|
|
||||||
|
train_td = pd.Timedelta(days=int(payload.wf.train_days))
|
||||||
|
test_td = pd.Timedelta(days=int(payload.wf.test_days))
|
||||||
|
step_td = pd.Timedelta(days=int(payload.wf.step_days or payload.wf.test_days))
|
||||||
|
|
||||||
|
overall_status = "ok"
|
||||||
|
results: List[Dict[str, Any]] = []
|
||||||
|
series: Dict[str, Any] = {"strategies": {}} if include_series else {}
|
||||||
|
|
||||||
|
for sel in payload.strategies:
|
||||||
|
|
||||||
|
sid = sel.strategy_id
|
||||||
|
entry = STRATEGY_REGISTRY.get(sid)
|
||||||
|
|
||||||
|
if entry is None:
|
||||||
|
results.append({
|
||||||
|
"strategy_id": sid,
|
||||||
|
"status": "fail",
|
||||||
|
"message": f"Unknown strategy_id: {sid}",
|
||||||
|
"n_windows": 0,
|
||||||
|
"oos_final_equity": payload.account_equity,
|
||||||
|
"oos_total_return_pct": 0.0,
|
||||||
|
"oos_max_dd_worst_pct": 0.0,
|
||||||
|
"degradation_sharpe": None,
|
||||||
|
"windows": [],
|
||||||
|
})
|
||||||
|
overall_status = "fail"
|
||||||
|
continue
|
||||||
|
|
||||||
|
strategy_class = entry["class"]
|
||||||
|
valid_params = set(entry["params"])
|
||||||
|
range_params = set(sel.parameters.keys())
|
||||||
|
|
||||||
|
|
||||||
|
# 🔒 Validación estricta de parámetros
|
||||||
|
if range_params != valid_params:
|
||||||
|
msg = f"Parameter keys {range_params} do not match expected {valid_params}"
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"strategy_id": sid,
|
||||||
|
"status": "fail",
|
||||||
|
"message": msg,
|
||||||
|
"n_windows": 0,
|
||||||
|
"oos_final_equity": payload.account_equity,
|
||||||
|
"oos_total_return_pct": 0.0,
|
||||||
|
"oos_max_dd_worst_pct": 0.0,
|
||||||
|
"degradation_sharpe": None,
|
||||||
|
"windows": [],
|
||||||
|
})
|
||||||
|
overall_status = "fail"
|
||||||
|
continue
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Convert ranges -> param_grid real
|
||||||
|
# --------------------------------------------------
|
||||||
|
param_grid = {}
|
||||||
|
|
||||||
|
for pname, prange in sel.parameters.items():
|
||||||
|
values = _build_param_values(
|
||||||
|
min_v=prange.min,
|
||||||
|
max_v=prange.max,
|
||||||
|
step=prange.step,
|
||||||
|
)
|
||||||
|
param_grid[pname] = values
|
||||||
|
|
||||||
|
# Wrapper sizer
|
||||||
|
class _CappedSizer(type(base_sizer)):
|
||||||
|
def __init__(self, inner):
|
||||||
|
self.inner = inner
|
||||||
|
|
||||||
|
def calculate_size(self, *, capital, entry_price, stop_price=None, max_capital=None, volatility=None):
|
||||||
|
u = self.inner.calculate_size(
|
||||||
|
capital=capital,
|
||||||
|
entry_price=entry_price,
|
||||||
|
stop_price=stop_price,
|
||||||
|
max_capital=max_capital,
|
||||||
|
volatility=volatility,
|
||||||
|
)
|
||||||
|
return _cap_units_by_max_position_fraction(
|
||||||
|
units=float(u),
|
||||||
|
capital=float(capital),
|
||||||
|
entry_price=float(entry_price),
|
||||||
|
max_position_fraction=float(payload.risk.max_position_fraction),
|
||||||
|
)
|
||||||
|
|
||||||
|
capped_sizer = _CappedSizer(base_sizer)
|
||||||
|
|
||||||
|
log.info(f"🧠 Step3 | WF run | strategy={sid}")
|
||||||
|
|
||||||
|
try:
|
||||||
|
wf = WalkForwardValidator(
|
||||||
|
strategy_class=strategy_class,
|
||||||
|
param_grid=param_grid,
|
||||||
|
data=df,
|
||||||
|
train_window=train_td,
|
||||||
|
test_window=test_td,
|
||||||
|
step_size=step_td,
|
||||||
|
initial_capital=float(payload.account_equity),
|
||||||
|
commission=float(payload.commission),
|
||||||
|
slippage=float(payload.slippage),
|
||||||
|
optimizer_metric=str(payload.optimization.optimizer_metric),
|
||||||
|
position_sizer=capped_sizer,
|
||||||
|
stop_loss=stop_loss,
|
||||||
|
max_combinations=int(payload.optimization.max_combinations),
|
||||||
|
progress_callback=progress_callback,
|
||||||
|
)
|
||||||
|
|
||||||
|
wf_res = wf.run()
|
||||||
|
win_df: pd.DataFrame = wf_res["windows"]
|
||||||
|
|
||||||
|
if win_df is None or win_df.empty:
|
||||||
|
status = "fail"
|
||||||
|
msg = "WF produced no valid windows"
|
||||||
|
overall_status = "fail"
|
||||||
|
windows_out = []
|
||||||
|
oos_returns = []
|
||||||
|
else:
|
||||||
|
trades = win_df["trades"].astype(int).tolist()
|
||||||
|
too_few = sum(t < int(payload.optimization.min_trades_test) for t in trades)
|
||||||
|
|
||||||
|
if too_few > 0:
|
||||||
|
status = "warning"
|
||||||
|
msg = f"{too_few} windows below min_trades_test"
|
||||||
|
if overall_status == "ok":
|
||||||
|
overall_status = "warning"
|
||||||
|
else:
|
||||||
|
status = "ok"
|
||||||
|
msg = "WF OK"
|
||||||
|
|
||||||
|
windows_out = []
|
||||||
|
for _, r in win_df.iterrows():
|
||||||
|
windows_out.append({
|
||||||
|
"window": int(r["window"]),
|
||||||
|
"train_start": str(r["train_start"]),
|
||||||
|
"train_end": str(r["train_end"]),
|
||||||
|
"test_start": str(r["test_start"]),
|
||||||
|
"test_end": str(r["test_end"]),
|
||||||
|
"return_pct": float(r["return_pct"]),
|
||||||
|
"sharpe": float(r["sharpe"]),
|
||||||
|
"max_dd_pct": float(r["max_dd_pct"]),
|
||||||
|
"trades": int(r["trades"]),
|
||||||
|
"params": dict(r["params"]) if isinstance(r["params"], dict) else r["params"],
|
||||||
|
})
|
||||||
|
|
||||||
|
oos_returns = win_df["return_pct"].astype(float).tolist()
|
||||||
|
|
||||||
|
eq_curve = _accumulate_equity(float(payload.account_equity), oos_returns)
|
||||||
|
oos_final = float(eq_curve[-1]) if eq_curve else float(payload.account_equity)
|
||||||
|
oos_total_return = (oos_final / float(payload.account_equity) - 1.0) * 100.0
|
||||||
|
oos_max_dd = float(np.min(win_df["max_dd_pct"])) if (win_df is not None and not win_df.empty) else 0.0
|
||||||
|
|
||||||
|
results.append({
|
||||||
|
"strategy_id": sid,
|
||||||
|
"status": status,
|
||||||
|
"message": msg,
|
||||||
|
"n_windows": int(len(windows_out)),
|
||||||
|
"oos_final_equity": oos_final,
|
||||||
|
"oos_total_return_pct": float(oos_total_return),
|
||||||
|
"oos_max_dd_worst_pct": float(oos_max_dd),
|
||||||
|
"degradation_sharpe": None,
|
||||||
|
"windows": windows_out,
|
||||||
|
})
|
||||||
|
|
||||||
|
if include_series:
|
||||||
|
series["strategies"][sid] = {
|
||||||
|
"window_returns_pct": oos_returns,
|
||||||
|
"window_equity": eq_curve,
|
||||||
|
}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.error(f"❌ Step3 WF error | strategy={sid} | {e}")
|
||||||
|
results.append({
|
||||||
|
"strategy_id": sid,
|
||||||
|
"status": "fail",
|
||||||
|
"message": f"Exception: {e}",
|
||||||
|
"n_windows": 0,
|
||||||
|
"oos_final_equity": float(payload.account_equity),
|
||||||
|
"oos_total_return_pct": 0.0,
|
||||||
|
"oos_max_dd_worst_pct": 0.0,
|
||||||
|
"degradation_sharpe": None,
|
||||||
|
"windows": [],
|
||||||
|
})
|
||||||
|
overall_status = "fail"
|
||||||
|
|
||||||
|
valid = overall_status != "fail"
|
||||||
|
|
||||||
|
human_msg = {
|
||||||
|
"ok": "Strategies validation OK",
|
||||||
|
"warning": "Strategies validation has warnings",
|
||||||
|
"fail": "Strategies validation FAILED",
|
||||||
|
}[overall_status]
|
||||||
|
|
||||||
|
out = {
|
||||||
|
"valid": valid,
|
||||||
|
"status": overall_status,
|
||||||
|
"checks": checks,
|
||||||
|
"message": human_msg,
|
||||||
|
"results": results,
|
||||||
|
}
|
||||||
|
|
||||||
|
if include_series:
|
||||||
|
out["series"] = series
|
||||||
|
|
||||||
|
return out
|
||||||
94
src/calibration/reports/optimization_report.py
Normal file
94
src/calibration/reports/optimization_report.py
Normal file
@@ -0,0 +1,94 @@
|
|||||||
|
# src/calibration/reports/strategies_report.py
|
||||||
|
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Any, Dict
|
||||||
|
|
||||||
|
from reportlab.platypus import SimpleDocTemplate, Paragraph, Spacer, Table, TableStyle, PageBreak
|
||||||
|
from reportlab.lib.styles import getSampleStyleSheet
|
||||||
|
from reportlab.lib import colors
|
||||||
|
|
||||||
|
|
||||||
|
def _simple_kv_table(title: str, dct: Dict[str, Any]):
|
||||||
|
rows = [["Key", "Value"]] + [[str(k), str(v)] for k, v in dct.items()]
|
||||||
|
t = Table(rows, hAlign="LEFT")
|
||||||
|
t.setStyle(TableStyle([
|
||||||
|
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
|
||||||
|
("GRID", (0, 0), (-1, -1), 0.5, colors.grey),
|
||||||
|
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||||
|
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||||
|
]))
|
||||||
|
return [Paragraph(title, getSampleStyleSheet()["Heading2"]), t, Spacer(1, 12)]
|
||||||
|
|
||||||
|
|
||||||
|
def generate_strategies_report_pdf(
|
||||||
|
*,
|
||||||
|
output_path: Path,
|
||||||
|
context: Dict[str, Any],
|
||||||
|
config: Dict[str, Any],
|
||||||
|
results: Dict[str, Any],
|
||||||
|
):
|
||||||
|
"""Minimal v1 report. We'll enrich with charts/tables next iterations."""
|
||||||
|
|
||||||
|
output_path.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
styles = getSampleStyleSheet()
|
||||||
|
doc = SimpleDocTemplate(str(output_path))
|
||||||
|
|
||||||
|
story = []
|
||||||
|
story.append(Paragraph("Calibration · Step 3 · Strategies", styles["Title"]))
|
||||||
|
story.append(Spacer(1, 12))
|
||||||
|
|
||||||
|
story.append(Paragraph(f"Status: {results.get('status')}", styles["Normal"]))
|
||||||
|
story.append(Spacer(1, 12))
|
||||||
|
|
||||||
|
for block in _simple_kv_table("Context", context):
|
||||||
|
story.append(block)
|
||||||
|
for block in _simple_kv_table("Configuration", config):
|
||||||
|
story.append(block)
|
||||||
|
|
||||||
|
story.append(Paragraph("Results (per strategy)", styles["Heading2"]))
|
||||||
|
story.append(Spacer(1, 6))
|
||||||
|
|
||||||
|
for r in results.get("results", []):
|
||||||
|
story.append(Paragraph(
|
||||||
|
f"<b>{r.get('strategy_id')}</b> — {r.get('status').upper()} — {r.get('message','')}",
|
||||||
|
styles["Normal"],
|
||||||
|
))
|
||||||
|
story.append(Spacer(1, 4))
|
||||||
|
|
||||||
|
summary = {
|
||||||
|
"n_windows": r.get("n_windows"),
|
||||||
|
"oos_final_equity": r.get("oos_final_equity"),
|
||||||
|
"oos_total_return_pct": r.get("oos_total_return_pct"),
|
||||||
|
"oos_max_dd_worst_pct": r.get("oos_max_dd_worst_pct"),
|
||||||
|
}
|
||||||
|
for block in _simple_kv_table("Summary", summary):
|
||||||
|
story.append(block)
|
||||||
|
|
||||||
|
# Window table (first 20 to keep PDF light)
|
||||||
|
windows = r.get("windows", [])
|
||||||
|
if windows:
|
||||||
|
rows = [["Window", "Test return %", "Sharpe", "Max DD %", "Trades", "Params"]]
|
||||||
|
for w in windows[:20]:
|
||||||
|
rows.append([
|
||||||
|
str(w.get("window")),
|
||||||
|
f"{float(w.get('return_pct',0.0)):.2f}",
|
||||||
|
f"{float(w.get('sharpe',0.0)):.2f}",
|
||||||
|
f"{float(w.get('max_dd_pct',0.0)):.2f}",
|
||||||
|
str(w.get("trades")),
|
||||||
|
str(w.get("params")),
|
||||||
|
])
|
||||||
|
t = Table(rows, hAlign="LEFT", colWidths=[45, 75, 55, 65, 55, 240])
|
||||||
|
t.setStyle(TableStyle([
|
||||||
|
("BACKGROUND", (0, 0), (-1, 0), colors.lightgrey),
|
||||||
|
("GRID", (0, 0), (-1, -1), 0.25, colors.grey),
|
||||||
|
("FONTNAME", (0, 0), (-1, 0), "Helvetica-Bold"),
|
||||||
|
("VALIGN", (0, 0), (-1, -1), "TOP"),
|
||||||
|
]))
|
||||||
|
story.append(Paragraph("Walk-Forward windows (first 20)", styles["Heading3"]))
|
||||||
|
story.append(t)
|
||||||
|
story.append(Spacer(1, 12))
|
||||||
|
|
||||||
|
story.append(PageBreak())
|
||||||
|
|
||||||
|
doc.build(story)
|
||||||
@@ -94,6 +94,27 @@ def _accumulate_equity(initial: float, returns_pct: List[float]) -> List[float]:
|
|||||||
return eq
|
return eq
|
||||||
|
|
||||||
|
|
||||||
|
def _build_param_values(min_v: float, max_v: float, step: float) -> List[float]:
|
||||||
|
min_v = float(min_v)
|
||||||
|
max_v = float(max_v)
|
||||||
|
step = float(step)
|
||||||
|
|
||||||
|
# Valor único si min == max
|
||||||
|
if min_v == max_v:
|
||||||
|
return [min_v]
|
||||||
|
|
||||||
|
# Valor único si step <= 1
|
||||||
|
if step <= 1:
|
||||||
|
return [min_v]
|
||||||
|
|
||||||
|
values = []
|
||||||
|
v = min_v
|
||||||
|
while v <= max_v:
|
||||||
|
values.append(v)
|
||||||
|
v += step
|
||||||
|
|
||||||
|
return values
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# Main
|
# Main
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
@@ -166,11 +187,13 @@ def inspect_strategies_config(
|
|||||||
|
|
||||||
strategy_class = entry["class"]
|
strategy_class = entry["class"]
|
||||||
valid_params = set(entry["params"])
|
valid_params = set(entry["params"])
|
||||||
grid_params = set(sel.param_grid.keys())
|
range_params = set(sel.parameters.keys())
|
||||||
|
|
||||||
|
|
||||||
# 🔒 Validación estricta de parámetros
|
# 🔒 Validación estricta de parámetros
|
||||||
if grid_params != valid_params:
|
if range_params != valid_params:
|
||||||
msg = f"Param grid keys {grid_params} do not match expected {valid_params}"
|
msg = f"Parameter keys {range_params} do not match expected {valid_params}"
|
||||||
|
|
||||||
results.append({
|
results.append({
|
||||||
"strategy_id": sid,
|
"strategy_id": sid,
|
||||||
"status": "fail",
|
"status": "fail",
|
||||||
@@ -185,6 +208,19 @@ def inspect_strategies_config(
|
|||||||
overall_status = "fail"
|
overall_status = "fail"
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
# --------------------------------------------------
|
||||||
|
# Convert ranges -> param_grid real
|
||||||
|
# --------------------------------------------------
|
||||||
|
param_grid = {}
|
||||||
|
|
||||||
|
for pname, prange in sel.parameters.items():
|
||||||
|
values = _build_param_values(
|
||||||
|
min_v=prange.min,
|
||||||
|
max_v=prange.max,
|
||||||
|
step=prange.step,
|
||||||
|
)
|
||||||
|
param_grid[pname] = values
|
||||||
|
|
||||||
# Wrapper sizer
|
# Wrapper sizer
|
||||||
class _CappedSizer(type(base_sizer)):
|
class _CappedSizer(type(base_sizer)):
|
||||||
def __init__(self, inner):
|
def __init__(self, inner):
|
||||||
@@ -212,7 +248,7 @@ def inspect_strategies_config(
|
|||||||
try:
|
try:
|
||||||
wf = WalkForwardValidator(
|
wf = WalkForwardValidator(
|
||||||
strategy_class=strategy_class,
|
strategy_class=strategy_class,
|
||||||
param_grid=sel.param_grid,
|
param_grid=param_grid,
|
||||||
data=df,
|
data=df,
|
||||||
train_window=train_td,
|
train_window=train_td,
|
||||||
test_window=test_td,
|
test_window=test_td,
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from .settings import settings
|
|||||||
from src.web.api.v2.routers.calibration_data import router as calibration_data_router
|
from src.web.api.v2.routers.calibration_data import router as calibration_data_router
|
||||||
from src.web.api.v2.routers.calibration_risk import router as calibration_risk_router
|
from src.web.api.v2.routers.calibration_risk import router as calibration_risk_router
|
||||||
from src.web.api.v2.routers.calibration_strategies import router as calibration_strategies_router
|
from src.web.api.v2.routers.calibration_strategies import router as calibration_strategies_router
|
||||||
|
from src.web.api.v2.routers.calibration_optimization import router as calibration_optimization_router
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# Logging
|
# Logging
|
||||||
@@ -127,6 +128,17 @@ def create_app() -> FastAPI:
|
|||||||
"step": 3,
|
"step": 3,
|
||||||
},
|
},
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@app.get("/calibration/optimization", response_class=HTMLResponse)
|
||||||
|
def calibration_risk_page(request: Request):
|
||||||
|
return templates.TemplateResponse(
|
||||||
|
"pages/calibration/calibration_optimization.html",
|
||||||
|
{
|
||||||
|
"request": request,
|
||||||
|
"page": "calibration",
|
||||||
|
"step": 4,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
@@ -136,6 +148,7 @@ def create_app() -> FastAPI:
|
|||||||
app.include_router(calibration_data_router, prefix=api_prefix)
|
app.include_router(calibration_data_router, prefix=api_prefix)
|
||||||
app.include_router(calibration_risk_router, prefix=api_prefix)
|
app.include_router(calibration_risk_router, prefix=api_prefix)
|
||||||
app.include_router(calibration_strategies_router, prefix=api_prefix)
|
app.include_router(calibration_strategies_router, prefix=api_prefix)
|
||||||
|
app.include_router(calibration_optimization_router, prefix=api_prefix)
|
||||||
|
|
||||||
return app
|
return app
|
||||||
|
|
||||||
|
|||||||
206
src/web/api/v2/routers/calibration_optimization.py
Normal file
206
src/web/api/v2/routers/calibration_optimization.py
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
# src/web/api/v2/routers/calibration_strategies.py
|
||||||
|
|
||||||
|
import logging
|
||||||
|
import re
|
||||||
|
import uuid
|
||||||
|
from pathlib import Path
|
||||||
|
from typing import Dict
|
||||||
|
|
||||||
|
from fastapi import APIRouter, Depends, HTTPException, Request
|
||||||
|
from fastapi.responses import JSONResponse, HTMLResponse
|
||||||
|
|
||||||
|
from src.data.storage import StorageManager
|
||||||
|
from src.calibration.optimization_inspector import (
|
||||||
|
inspect_strategies_config,
|
||||||
|
list_available_strategies,
|
||||||
|
)
|
||||||
|
from src.calibration.reports.optimization_report import generate_strategies_report_pdf
|
||||||
|
|
||||||
|
from ..schemas.calibration_optimization import (
|
||||||
|
CalibrationStrategiesInspectRequest,
|
||||||
|
CalibrationStrategiesInspectResponse,
|
||||||
|
CalibrationStrategiesValidateResponse,
|
||||||
|
)
|
||||||
|
|
||||||
|
logger = logging.getLogger("tradingbot.api.v2")
|
||||||
|
|
||||||
|
router = APIRouter(
|
||||||
|
prefix="/calibration/optimization",
|
||||||
|
tags=["calibration"],
|
||||||
|
)
|
||||||
|
|
||||||
|
WF_JOBS: Dict[str, Dict] = {}
|
||||||
|
|
||||||
|
def get_storage() -> StorageManager:
|
||||||
|
return StorageManager.from_env()
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/catalog")
|
||||||
|
def strategy_catalog():
|
||||||
|
strategies = list_available_strategies()
|
||||||
|
|
||||||
|
# Añadimos defaults sugeridos
|
||||||
|
for s in strategies:
|
||||||
|
s["parameters_meta"] = [
|
||||||
|
{
|
||||||
|
"name": p,
|
||||||
|
"type": "int",
|
||||||
|
"default_min": 10,
|
||||||
|
"default_max": 50,
|
||||||
|
"default_step": 10,
|
||||||
|
}
|
||||||
|
for p in s["params"]
|
||||||
|
]
|
||||||
|
|
||||||
|
return {"strategies": strategies}
|
||||||
|
|
||||||
|
@router.post("/inspect", response_model=CalibrationStrategiesInspectResponse)
|
||||||
|
def inspect_strategies(
|
||||||
|
payload: CalibrationStrategiesInspectRequest,
|
||||||
|
storage: StorageManager = Depends(get_storage),
|
||||||
|
):
|
||||||
|
df = storage.load_ohlcv(symbol=payload.symbol, timeframe=payload.timeframe)
|
||||||
|
if df is None or df.empty:
|
||||||
|
raise HTTPException(status_code=400, detail="No OHLCV data found. Run Step 1 first.")
|
||||||
|
|
||||||
|
from .calibration_data import analyze_data_quality
|
||||||
|
data_quality = analyze_data_quality(df, payload.timeframe)
|
||||||
|
|
||||||
|
result = inspect_strategies_config(
|
||||||
|
storage=storage,
|
||||||
|
payload=payload,
|
||||||
|
data_quality=data_quality,
|
||||||
|
include_series=False,
|
||||||
|
)
|
||||||
|
return CalibrationStrategiesInspectResponse(**result)
|
||||||
|
|
||||||
|
@router.post("/validate", response_model=CalibrationStrategiesValidateResponse)
|
||||||
|
def validate_strategies(
|
||||||
|
payload: CalibrationStrategiesInspectRequest,
|
||||||
|
storage: StorageManager = Depends(get_storage),
|
||||||
|
):
|
||||||
|
df = storage.load_ohlcv(symbol=payload.symbol, timeframe=payload.timeframe)
|
||||||
|
if df is None or df.empty:
|
||||||
|
raise HTTPException(status_code=400, detail="No OHLCV data found. Run Step 1 first.")
|
||||||
|
|
||||||
|
from .calibration_data import analyze_data_quality
|
||||||
|
data_quality = analyze_data_quality(df, payload.timeframe)
|
||||||
|
|
||||||
|
result = inspect_strategies_config(
|
||||||
|
storage=storage,
|
||||||
|
payload=payload,
|
||||||
|
data_quality=data_quality,
|
||||||
|
include_series=True,
|
||||||
|
)
|
||||||
|
return CalibrationStrategiesValidateResponse(**result)
|
||||||
|
|
||||||
|
@router.post("/report")
|
||||||
|
def report_strategies(
|
||||||
|
payload: CalibrationStrategiesInspectRequest,
|
||||||
|
storage: StorageManager = Depends(get_storage),
|
||||||
|
):
|
||||||
|
logger.info(f"🧾 Generating strategies report | {payload.symbol} {payload.timeframe}")
|
||||||
|
|
||||||
|
df = storage.load_ohlcv(symbol=payload.symbol, timeframe=payload.timeframe)
|
||||||
|
if df is None or df.empty:
|
||||||
|
raise HTTPException(status_code=400, detail="No OHLCV data found. Run Step 1 first.")
|
||||||
|
|
||||||
|
from .calibration_data import analyze_data_quality
|
||||||
|
data_quality = analyze_data_quality(df, payload.timeframe)
|
||||||
|
|
||||||
|
result = inspect_strategies_config(
|
||||||
|
storage=storage,
|
||||||
|
payload=payload,
|
||||||
|
data_quality=data_quality,
|
||||||
|
include_series=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
# ---------------------------------------------
|
||||||
|
# Prepare PDF output path (outside src)
|
||||||
|
# ---------------------------------------------
|
||||||
|
project_root = Path(__file__).resolve().parents[4] # .../src
|
||||||
|
# project_root currently points to src/web/api/v2/routers -> parents[4] == src
|
||||||
|
project_root = project_root.parent # repo root
|
||||||
|
|
||||||
|
reports_dir = project_root / "reports" / "strategies"
|
||||||
|
reports_dir.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
safe_symbol = re.sub(r"[^a-zA-Z0-9_-]", "_", payload.symbol)
|
||||||
|
filename = f"strategies_report_{safe_symbol}_{payload.timeframe}_{uuid.uuid4().hex}.pdf"
|
||||||
|
|
||||||
|
symbol_dir = reports_dir / safe_symbol
|
||||||
|
symbol_dir.mkdir(exist_ok=True)
|
||||||
|
|
||||||
|
output_path = symbol_dir / filename
|
||||||
|
|
||||||
|
generate_strategies_report_pdf(
|
||||||
|
output_path=output_path,
|
||||||
|
context={
|
||||||
|
"Symbol": payload.symbol,
|
||||||
|
"Timeframe": payload.timeframe,
|
||||||
|
"Account equity": payload.account_equity,
|
||||||
|
},
|
||||||
|
config={
|
||||||
|
"Stop type": payload.stop.type,
|
||||||
|
"Risk per trade (%)": payload.risk.risk_fraction * 100,
|
||||||
|
"Max position fraction (%)": payload.risk.max_position_fraction * 100,
|
||||||
|
"WF train_days": payload.wf.train_days,
|
||||||
|
"WF test_days": payload.wf.test_days,
|
||||||
|
"WF step_days": payload.wf.step_days or payload.wf.test_days,
|
||||||
|
"Optimizer metric": payload.optimization.optimizer_metric,
|
||||||
|
"Max combinations": payload.optimization.max_combinations,
|
||||||
|
},
|
||||||
|
results=result,
|
||||||
|
)
|
||||||
|
|
||||||
|
public_url = f"/reports/strategies/{safe_symbol}/{filename}"
|
||||||
|
return JSONResponse(content={"status": result.get("status", "ok"), "url": public_url})
|
||||||
|
|
||||||
|
@router.post("/run")
|
||||||
|
def run_strategies_async(
|
||||||
|
payload: CalibrationStrategiesInspectRequest,
|
||||||
|
storage: StorageManager = Depends(get_storage),
|
||||||
|
):
|
||||||
|
import threading
|
||||||
|
import uuid
|
||||||
|
|
||||||
|
job_id = uuid.uuid4().hex
|
||||||
|
|
||||||
|
WF_JOBS[job_id] = {
|
||||||
|
"status": "running",
|
||||||
|
"progress": 0,
|
||||||
|
"current_window": 0,
|
||||||
|
"total_windows": 0,
|
||||||
|
"current_strategy": None,
|
||||||
|
"result": None,
|
||||||
|
}
|
||||||
|
|
||||||
|
def background_job():
|
||||||
|
|
||||||
|
def progress_cb(window_id, total_windows):
|
||||||
|
WF_JOBS[job_id]["current_window"] = window_id
|
||||||
|
WF_JOBS[job_id]["total_windows"] = total_windows
|
||||||
|
WF_JOBS[job_id]["progress"] = int(
|
||||||
|
window_id / total_windows * 100
|
||||||
|
)
|
||||||
|
|
||||||
|
result = inspect_strategies_config(
|
||||||
|
storage=storage,
|
||||||
|
payload=payload,
|
||||||
|
data_quality={"status": "ok"},
|
||||||
|
include_series=True,
|
||||||
|
progress_callback=progress_cb, # ← lo pasamos
|
||||||
|
)
|
||||||
|
|
||||||
|
WF_JOBS[job_id]["status"] = "done"
|
||||||
|
WF_JOBS[job_id]["progress"] = 100
|
||||||
|
WF_JOBS[job_id]["result"] = result
|
||||||
|
|
||||||
|
thread = threading.Thread(target=background_job)
|
||||||
|
thread.start()
|
||||||
|
|
||||||
|
return {"job_id": job_id}
|
||||||
|
|
||||||
|
@router.get("/status/{job_id}")
|
||||||
|
def get_status(job_id: str):
|
||||||
|
return WF_JOBS.get(job_id, {"status": "unknown"})
|
||||||
@@ -35,9 +35,24 @@ def get_storage() -> StorageManager:
|
|||||||
return StorageManager.from_env()
|
return StorageManager.from_env()
|
||||||
|
|
||||||
|
|
||||||
@router.get("/available")
|
@router.get("/catalog")
|
||||||
def available_strategies():
|
def strategy_catalog():
|
||||||
return {"strategies": list_available_strategies()}
|
strategies = list_available_strategies()
|
||||||
|
|
||||||
|
# Añadimos defaults sugeridos
|
||||||
|
for s in strategies:
|
||||||
|
s["parameters_meta"] = [
|
||||||
|
{
|
||||||
|
"name": p,
|
||||||
|
"type": "int",
|
||||||
|
"default_min": 10,
|
||||||
|
"default_max": 50,
|
||||||
|
"default_step": 10,
|
||||||
|
}
|
||||||
|
for p in s["params"]
|
||||||
|
]
|
||||||
|
|
||||||
|
return {"strategies": strategies}
|
||||||
|
|
||||||
@router.post("/inspect", response_model=CalibrationStrategiesInspectResponse)
|
@router.post("/inspect", response_model=CalibrationStrategiesInspectResponse)
|
||||||
def inspect_strategies(
|
def inspect_strategies(
|
||||||
|
|||||||
89
src/web/api/v2/schemas/calibration_optimization.py
Normal file
89
src/web/api/v2/schemas/calibration_optimization.py
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
# src/web/api/v2/schemas/calibration_strategies.py
|
||||||
|
|
||||||
|
from typing import Any, Dict, List, Literal, Optional
|
||||||
|
from pydantic import BaseModel, Field
|
||||||
|
|
||||||
|
from .calibration_risk import StopConfigSchema, RiskConfigSchema, GlobalRiskRulesSchema
|
||||||
|
|
||||||
|
|
||||||
|
class WalkForwardConfigSchema(BaseModel):
|
||||||
|
train_days: int = Field(..., gt=0)
|
||||||
|
test_days: int = Field(..., gt=0)
|
||||||
|
step_days: Optional[int] = Field(None, gt=0) # if None => step = test_days
|
||||||
|
|
||||||
|
|
||||||
|
class OptimizationConfigSchema(BaseModel):
|
||||||
|
optimizer_metric: str = Field("sharpe_ratio")
|
||||||
|
max_combinations: int = Field(500, gt=0)
|
||||||
|
min_trades_train: int = Field(30, ge=0)
|
||||||
|
min_trades_test: int = Field(10, ge=0)
|
||||||
|
|
||||||
|
|
||||||
|
class ParameterRangeSchema(BaseModel):
|
||||||
|
min: float
|
||||||
|
max: float
|
||||||
|
step: float
|
||||||
|
|
||||||
|
|
||||||
|
class StrategySelectionSchema(BaseModel):
|
||||||
|
strategy_id: str
|
||||||
|
parameters: Dict[str, ParameterRangeSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class CalibrationStrategiesInspectRequest(BaseModel):
|
||||||
|
symbol: str
|
||||||
|
timeframe: str
|
||||||
|
|
||||||
|
# snapshot from Step 2 (closed)
|
||||||
|
stop: StopConfigSchema
|
||||||
|
risk: RiskConfigSchema
|
||||||
|
global_rules: GlobalRiskRulesSchema
|
||||||
|
account_equity: float = Field(..., gt=0)
|
||||||
|
|
||||||
|
strategies: List[StrategySelectionSchema]
|
||||||
|
wf: WalkForwardConfigSchema
|
||||||
|
optimization: OptimizationConfigSchema
|
||||||
|
|
||||||
|
commission: float = Field(0.001, ge=0)
|
||||||
|
slippage: float = Field(0.0005, ge=0)
|
||||||
|
|
||||||
|
|
||||||
|
class WindowRowSchema(BaseModel):
|
||||||
|
window: int
|
||||||
|
train_start: str
|
||||||
|
train_end: str
|
||||||
|
test_start: str
|
||||||
|
test_end: str
|
||||||
|
|
||||||
|
return_pct: float
|
||||||
|
sharpe: float
|
||||||
|
max_dd_pct: float
|
||||||
|
trades: int
|
||||||
|
params: Dict[str, Any]
|
||||||
|
|
||||||
|
|
||||||
|
class StrategyRunResultSchema(BaseModel):
|
||||||
|
strategy_id: str
|
||||||
|
status: Literal["ok", "warning", "fail"]
|
||||||
|
message: str
|
||||||
|
|
||||||
|
n_windows: int
|
||||||
|
oos_final_equity: float
|
||||||
|
oos_total_return_pct: float
|
||||||
|
oos_max_dd_worst_pct: float
|
||||||
|
degradation_sharpe: Optional[float] = None
|
||||||
|
|
||||||
|
windows: List[WindowRowSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class CalibrationStrategiesInspectResponse(BaseModel):
|
||||||
|
valid: bool
|
||||||
|
status: Literal["ok", "warning", "fail"]
|
||||||
|
checks: Dict[str, Any]
|
||||||
|
message: str
|
||||||
|
|
||||||
|
results: List[StrategyRunResultSchema]
|
||||||
|
|
||||||
|
|
||||||
|
class CalibrationStrategiesValidateResponse(CalibrationStrategiesInspectResponse):
|
||||||
|
series: Dict[str, Any]
|
||||||
@@ -19,9 +19,15 @@ class OptimizationConfigSchema(BaseModel):
|
|||||||
min_trades_test: int = Field(10, ge=0)
|
min_trades_test: int = Field(10, ge=0)
|
||||||
|
|
||||||
|
|
||||||
|
class ParameterRangeSchema(BaseModel):
|
||||||
|
min: float
|
||||||
|
max: float
|
||||||
|
step: float
|
||||||
|
|
||||||
|
|
||||||
class StrategySelectionSchema(BaseModel):
|
class StrategySelectionSchema(BaseModel):
|
||||||
strategy_id: str
|
strategy_id: str
|
||||||
param_grid: Dict[str, List[Any]]
|
parameters: Dict[str, ParameterRangeSchema]
|
||||||
|
|
||||||
|
|
||||||
class CalibrationStrategiesInspectRequest(BaseModel):
|
class CalibrationStrategiesInspectRequest(BaseModel):
|
||||||
|
|||||||
1040
src/web/ui/v2/static/js/pages/calibration_optimization.js
Normal file
1040
src/web/ui/v2/static/js/pages/calibration_optimization.js
Normal file
File diff suppressed because it is too large
Load Diff
@@ -89,6 +89,10 @@ async function inspectCalibrationRisk() {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
console.log("[calibration_risk] inspect response:", data);
|
console.log("[calibration_risk] inspect response:", data);
|
||||||
|
|
||||||
|
if (data.status === "ok" || data.status === "warning") {
|
||||||
|
persistRiskParametersForStep3();
|
||||||
|
}
|
||||||
|
|
||||||
renderRiskResult(payload, data);
|
renderRiskResult(payload, data);
|
||||||
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
@@ -189,6 +193,10 @@ async function validateCalibrationRisk() {
|
|||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
console.log("[calibration_risk] inspect response:", data);
|
console.log("[calibration_risk] inspect response:", data);
|
||||||
|
|
||||||
|
if (data.status === "ok" || data.status === "warning") {
|
||||||
|
persistRiskParametersForStep3();
|
||||||
|
}
|
||||||
|
|
||||||
renderRiskResult(payload, data);
|
renderRiskResult(payload, data);
|
||||||
|
|
||||||
// --------------------------------------------------
|
// --------------------------------------------------
|
||||||
@@ -698,6 +706,13 @@ function num(id) {
|
|||||||
return Number.isFinite(n) ? n : null;
|
return Number.isFinite(n) ? n : null;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function str(id) {
|
||||||
|
const el = document.getElementById(id);
|
||||||
|
if (!el) return null;
|
||||||
|
const v = el.value;
|
||||||
|
return v === null || v === undefined ? null : String(v);
|
||||||
|
}
|
||||||
|
|
||||||
function buildRiskPayload() {
|
function buildRiskPayload() {
|
||||||
const symbol = localStorage.getItem("calibration.symbol");
|
const symbol = localStorage.getItem("calibration.symbol");
|
||||||
const timeframe = localStorage.getItem("calibration.timeframe");
|
const timeframe = localStorage.getItem("calibration.timeframe");
|
||||||
@@ -742,6 +757,34 @@ function buildRiskPayload() {
|
|||||||
return payload;
|
return payload;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function persistRiskParametersForStep3() {
|
||||||
|
|
||||||
|
try {
|
||||||
|
|
||||||
|
const dataToPersist = {
|
||||||
|
risk_fraction: num("risk_fraction"),
|
||||||
|
max_position_fraction: num("max_position_fraction"),
|
||||||
|
stop_type: str("stop_type"),
|
||||||
|
stop_fraction: num("stop_fraction"),
|
||||||
|
atr_period: num("atr_period"),
|
||||||
|
atr_multiplier: num("atr_multiplier"),
|
||||||
|
max_drawdown_pct: num("max_drawdown_pct"),
|
||||||
|
daily_loss_limit_pct: num("daily_loss_limit_pct"),
|
||||||
|
max_consecutive_losses: num("max_consecutive_losses"),
|
||||||
|
cooldown_bars: num("cooldown_bars"),
|
||||||
|
};
|
||||||
|
|
||||||
|
Object.entries(dataToPersist).forEach(([key, value]) => {
|
||||||
|
localStorage.setItem(`calibration.${key}`, value ?? "");
|
||||||
|
});
|
||||||
|
|
||||||
|
console.log("[calibration_risk] Parameters saved for Step 3 ✅");
|
||||||
|
|
||||||
|
} catch (err) {
|
||||||
|
console.error("[calibration_risk] Persist failed ❌", err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// =================================================
|
// =================================================
|
||||||
// INIT
|
// INIT
|
||||||
// =================================================
|
// =================================================
|
||||||
|
|||||||
@@ -2,6 +2,10 @@
|
|||||||
|
|
||||||
console.log("[calibration_strategies] script loaded ✅", new Date().toISOString());
|
console.log("[calibration_strategies] script loaded ✅", new Date().toISOString());
|
||||||
|
|
||||||
|
let STRATEGY_CATALOG = [];
|
||||||
|
let strategySlots = [];
|
||||||
|
const MAX_STRATEGIES = 10;
|
||||||
|
|
||||||
// =================================================
|
// =================================================
|
||||||
// WIZARD NAVIGATION
|
// WIZARD NAVIGATION
|
||||||
// =================================================
|
// =================================================
|
||||||
@@ -125,35 +129,51 @@ function buildPayload() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function collectSelectedStrategies() {
|
function collectSelectedStrategies() {
|
||||||
const items = document.querySelectorAll("[data-strategy-item]");
|
|
||||||
const out = [];
|
|
||||||
|
|
||||||
items.forEach((node) => {
|
const strategies = [];
|
||||||
const checkbox = node.querySelector("input[type=checkbox]");
|
|
||||||
if (!checkbox || !checkbox.checked) return;
|
|
||||||
|
|
||||||
const sid = checkbox.getAttribute("data-strategy-id");
|
strategySlots.forEach((slot, index) => {
|
||||||
const textarea = node.querySelector("textarea");
|
|
||||||
let grid = {};
|
|
||||||
if (textarea && textarea.value.trim()) {
|
|
||||||
try {
|
|
||||||
grid = JSON.parse(textarea.value);
|
|
||||||
} catch (e) {
|
|
||||||
throw new Error(`Invalid JSON param_grid for ${sid}: ${e.message}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
out.push({ strategy_id: sid, param_grid: grid });
|
if (!slot.strategy_id) return;
|
||||||
|
|
||||||
|
const strategyMeta = STRATEGY_CATALOG.find(
|
||||||
|
s => s.strategy_id === slot.strategy_id
|
||||||
|
);
|
||||||
|
|
||||||
|
const parameters = {};
|
||||||
|
|
||||||
|
strategyMeta.params.forEach(paramName => {
|
||||||
|
|
||||||
|
const min = parseFloat(
|
||||||
|
document.getElementById(`${paramName}_min_${index}`)?.value
|
||||||
|
);
|
||||||
|
|
||||||
|
const max = parseFloat(
|
||||||
|
document.getElementById(`${paramName}_max_${index}`)?.value
|
||||||
|
);
|
||||||
|
|
||||||
|
const step = parseFloat(
|
||||||
|
document.getElementById(`${paramName}_step_${index}`)?.value
|
||||||
|
);
|
||||||
|
|
||||||
|
parameters[paramName] = {
|
||||||
|
min: min,
|
||||||
|
max: max,
|
||||||
|
step: step
|
||||||
|
};
|
||||||
|
});
|
||||||
|
|
||||||
|
strategies.push({
|
||||||
|
strategy_id: slot.strategy_id,
|
||||||
|
parameters: parameters
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (out.length === 0) {
|
return strategies;
|
||||||
throw new Error("Select at least 1 strategy");
|
|
||||||
}
|
|
||||||
return out;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchAvailableStrategies() {
|
async function fetchAvailableStrategies() {
|
||||||
const res = await fetch("/api/v2/calibration/strategies/available");
|
const res = await fetch("/api/v2/calibration/strategies/catalog");
|
||||||
const data = await res.json();
|
const data = await res.json();
|
||||||
return data.strategies || [];
|
return data.strategies || [];
|
||||||
}
|
}
|
||||||
@@ -180,6 +200,398 @@ function setVal(id, value) {
|
|||||||
el.value = value ?? "";
|
el.value = value ?? "";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function loadFromStep2() {
|
||||||
|
|
||||||
|
document.getElementById("risk_fraction").value =
|
||||||
|
localStorage.getItem("calibration.risk_fraction") ?? "";
|
||||||
|
|
||||||
|
document.getElementById("max_position_fraction").value =
|
||||||
|
localStorage.getItem("calibration.max_position_fraction") ?? "";
|
||||||
|
|
||||||
|
document.getElementById("stop_type").value =
|
||||||
|
localStorage.getItem("calibration.stop_type") ?? "fixed";
|
||||||
|
|
||||||
|
document.getElementById("stop_fraction").value =
|
||||||
|
localStorage.getItem("calibration.stop_fraction") ?? "";
|
||||||
|
|
||||||
|
document.getElementById("atr_period").value =
|
||||||
|
localStorage.getItem("calibration.atr_period") ?? "";
|
||||||
|
|
||||||
|
document.getElementById("atr_multiplier").value =
|
||||||
|
localStorage.getItem("calibration.atr_multiplier") ?? "";
|
||||||
|
|
||||||
|
document.getElementById("max_drawdown_pct").value =
|
||||||
|
localStorage.getItem("calibration.max_drawdown_pct") ?? "";
|
||||||
|
|
||||||
|
document.getElementById("daily_loss_limit_pct").value =
|
||||||
|
localStorage.getItem("calibration.daily_loss_limit_pct") ?? "";
|
||||||
|
|
||||||
|
document.getElementById("max_consecutive_losses").value =
|
||||||
|
localStorage.getItem("calibration.max_consecutive_losses") ?? "";
|
||||||
|
|
||||||
|
document.getElementById("cooldown_bars").value =
|
||||||
|
localStorage.getItem("calibration.cooldown_bars") ?? "";
|
||||||
|
|
||||||
|
// Forzar actualización de UI según stop heredado
|
||||||
|
setTimeout(() => {
|
||||||
|
updateStopUI();
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
console.log("[calibration_strategies] Parameters loaded from Step 2 ✅");
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateStopUI() {
|
||||||
|
|
||||||
|
const type = document.getElementById("stop_type").value;
|
||||||
|
|
||||||
|
const stopFraction = document.getElementById("stop_fraction_group");
|
||||||
|
const atrPeriod = document.getElementById("atr_group");
|
||||||
|
const atrMultiplier = document.getElementById("atr_multiplier_group");
|
||||||
|
|
||||||
|
if (type === "fixed" || type === "trailing") {
|
||||||
|
stopFraction.classList.remove("d-none");
|
||||||
|
atrPeriod.classList.add("d-none");
|
||||||
|
atrMultiplier.classList.add("d-none");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (type === "atr") {
|
||||||
|
stopFraction.classList.add("d-none");
|
||||||
|
atrPeriod.classList.remove("d-none");
|
||||||
|
atrMultiplier.classList.remove("d-none");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadStrategyCatalog() {
|
||||||
|
|
||||||
|
const res = await fetch("/api/v2/calibration/strategies/catalog");
|
||||||
|
const data = await res.json();
|
||||||
|
|
||||||
|
STRATEGY_CATALOG = data.strategies;
|
||||||
|
}
|
||||||
|
|
||||||
|
function addStrategySlot() {
|
||||||
|
|
||||||
|
// Si ya hay un slot vacío al final, no crear otro
|
||||||
|
if (strategySlots.length > 0 &&
|
||||||
|
strategySlots[strategySlots.length - 1].strategy_id === null) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (strategySlots.length >= MAX_STRATEGIES) return;
|
||||||
|
|
||||||
|
const index = strategySlots.length;
|
||||||
|
|
||||||
|
strategySlots.push({
|
||||||
|
strategy_id: null,
|
||||||
|
parameters: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
renderStrategySlot(index);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderStrategySlot(index) {
|
||||||
|
|
||||||
|
const container = document.getElementById("strategies_container");
|
||||||
|
|
||||||
|
const slot = document.createElement("div");
|
||||||
|
slot.className = "card p-3";
|
||||||
|
slot.id = `strategy_slot_${index}`;
|
||||||
|
|
||||||
|
slot.innerHTML = `
|
||||||
|
<div class="mb-3">
|
||||||
|
<label class="form-label">Strategy ${index + 1}</label>
|
||||||
|
<select class="form-select" id="strategy_select_${index}">
|
||||||
|
<option value="">None</option>
|
||||||
|
${STRATEGY_CATALOG.map(s =>
|
||||||
|
`<option value="${s.strategy_id}">${s.name}</option>`
|
||||||
|
).join("")}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="strategy_params_${index}" class="row g-3"></div>
|
||||||
|
<div class="mt-2 text-end">
|
||||||
|
<small class="text-muted">
|
||||||
|
Combinations:
|
||||||
|
<span id="strategy_combo_${index}">0</span>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
container.appendChild(slot);
|
||||||
|
|
||||||
|
document
|
||||||
|
.getElementById(`strategy_select_${index}`)
|
||||||
|
.addEventListener("change", (e) => {
|
||||||
|
onStrategySelected(index, e.target.value);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function onStrategySelected(index, strategyId) {
|
||||||
|
|
||||||
|
if (!strategyId) {
|
||||||
|
removeStrategySlot(index);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
strategySlots[index].strategy_id = strategyId;
|
||||||
|
|
||||||
|
renderParametersOnly(index, strategyId);
|
||||||
|
|
||||||
|
// Si es el último slot activo, añadir nuevo vacío
|
||||||
|
if (index === strategySlots.length - 1 &&
|
||||||
|
strategySlots.length < MAX_STRATEGIES) {
|
||||||
|
|
||||||
|
strategySlots.push({ strategy_id: null, parameters: {} });
|
||||||
|
renderStrategySlot(strategySlots.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCombinationCounter();
|
||||||
|
}
|
||||||
|
|
||||||
|
function validateParameterInputs() {
|
||||||
|
|
||||||
|
let valid = true;
|
||||||
|
|
||||||
|
document.querySelectorAll(".param-input").forEach(input => {
|
||||||
|
input.classList.remove("is-invalid");
|
||||||
|
});
|
||||||
|
|
||||||
|
strategySlots.forEach((slot, index) => {
|
||||||
|
|
||||||
|
if (!slot.strategy_id) return;
|
||||||
|
|
||||||
|
const strategyMeta = STRATEGY_CATALOG.find(
|
||||||
|
s => s.strategy_id === slot.strategy_id
|
||||||
|
);
|
||||||
|
|
||||||
|
strategyMeta.params.forEach(paramName => {
|
||||||
|
|
||||||
|
const minEl = document.getElementById(`${paramName}_min_${index}`);
|
||||||
|
const maxEl = document.getElementById(`${paramName}_max_${index}`);
|
||||||
|
const stepEl = document.getElementById(`${paramName}_step_${index}`);
|
||||||
|
|
||||||
|
const min = parseFloat(minEl?.value);
|
||||||
|
const max = parseFloat(maxEl?.value);
|
||||||
|
const step = parseFloat(stepEl?.value);
|
||||||
|
|
||||||
|
if (max < min) {
|
||||||
|
maxEl.classList.add("is-invalid");
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (step <= 0) {
|
||||||
|
stepEl.classList.add("is-invalid");
|
||||||
|
valid = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
updateCombinationCounter();
|
||||||
|
|
||||||
|
return valid;
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateCombinationCounter() {
|
||||||
|
|
||||||
|
let globalTotal = 1;
|
||||||
|
let hasAnyStrategy = false;
|
||||||
|
|
||||||
|
strategySlots.forEach((slot, index) => {
|
||||||
|
|
||||||
|
if (!slot.strategy_id) return;
|
||||||
|
|
||||||
|
hasAnyStrategy = true;
|
||||||
|
|
||||||
|
const strategyMeta = STRATEGY_CATALOG.find(
|
||||||
|
s => s.strategy_id === slot.strategy_id
|
||||||
|
);
|
||||||
|
|
||||||
|
let strategyTotal = 1;
|
||||||
|
|
||||||
|
strategyMeta.params.forEach(paramName => {
|
||||||
|
|
||||||
|
const min = parseFloat(
|
||||||
|
document.getElementById(`${paramName}_min_${index}`)?.value
|
||||||
|
);
|
||||||
|
|
||||||
|
const max = parseFloat(
|
||||||
|
document.getElementById(`${paramName}_max_${index}`)?.value
|
||||||
|
);
|
||||||
|
|
||||||
|
const step = parseFloat(
|
||||||
|
document.getElementById(`${paramName}_step_${index}`)?.value
|
||||||
|
);
|
||||||
|
|
||||||
|
if (isNaN(min) || isNaN(max) || isNaN(step)) return;
|
||||||
|
|
||||||
|
if (min === max || step == 0) {
|
||||||
|
strategyTotal *= 1;
|
||||||
|
} else {
|
||||||
|
const count = Math.floor((max - min) / step) + 1;
|
||||||
|
strategyTotal *= Math.max(count, 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
});
|
||||||
|
|
||||||
|
const perStrategyEl = document.getElementById(`strategy_combo_${index}`);
|
||||||
|
if (perStrategyEl) {
|
||||||
|
perStrategyEl.textContent = strategyTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
globalTotal *= strategyTotal;
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!hasAnyStrategy) globalTotal = 0;
|
||||||
|
|
||||||
|
const globalEl = document.getElementById("combination_counter");
|
||||||
|
if (globalEl) globalEl.textContent = globalTotal;
|
||||||
|
|
||||||
|
applyCombinationWarnings(globalTotal);
|
||||||
|
updateTimeEstimate(globalTotal);
|
||||||
|
|
||||||
|
return globalTotal;
|
||||||
|
}
|
||||||
|
|
||||||
|
function applyCombinationWarnings(total) {
|
||||||
|
|
||||||
|
const maxComb = parseInt(
|
||||||
|
document.getElementById("opt_max_combinations")?.value || 0
|
||||||
|
);
|
||||||
|
|
||||||
|
const counter = document.getElementById("combination_counter");
|
||||||
|
if (!counter) return;
|
||||||
|
|
||||||
|
counter.classList.remove("text-warning", "text-danger");
|
||||||
|
|
||||||
|
if (total > 10000) {
|
||||||
|
counter.classList.add("text-danger");
|
||||||
|
} else if (maxComb && total > maxComb) {
|
||||||
|
counter.classList.add("text-warning");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateTimeEstimate(totalComb) {
|
||||||
|
|
||||||
|
const trainDays = parseInt(
|
||||||
|
document.getElementById("wf_train_days")?.value || 0
|
||||||
|
);
|
||||||
|
|
||||||
|
const testDays = parseInt(
|
||||||
|
document.getElementById("wf_test_days")?.value || 0
|
||||||
|
);
|
||||||
|
|
||||||
|
const approxWindows = Math.max(
|
||||||
|
Math.floor(365 / testDays),
|
||||||
|
1
|
||||||
|
);
|
||||||
|
|
||||||
|
const operations = totalComb * approxWindows;
|
||||||
|
|
||||||
|
// 0.003s por combinación (estimación conservadora)
|
||||||
|
const seconds = operations * 0.003;
|
||||||
|
|
||||||
|
let label;
|
||||||
|
|
||||||
|
if (seconds < 60) {
|
||||||
|
label = `~ ${seconds.toFixed(1)} sec`;
|
||||||
|
} else if (seconds < 3600) {
|
||||||
|
label = `~ ${(seconds / 60).toFixed(1)} min`;
|
||||||
|
} else {
|
||||||
|
label = `~ ${(seconds / 3600).toFixed(1)} h`;
|
||||||
|
}
|
||||||
|
|
||||||
|
const el = document.getElementById("wf_time_estimate");
|
||||||
|
if (el) el.textContent = label;
|
||||||
|
}
|
||||||
|
|
||||||
|
function removeStrategySlot(index) {
|
||||||
|
|
||||||
|
strategySlots.splice(index, 1);
|
||||||
|
|
||||||
|
rerenderStrategySlots();
|
||||||
|
}
|
||||||
|
|
||||||
|
function rerenderStrategySlots() {
|
||||||
|
|
||||||
|
const container = document.getElementById("strategies_container");
|
||||||
|
container.innerHTML = "";
|
||||||
|
|
||||||
|
const currentStrategies = strategySlots
|
||||||
|
.filter(s => s.strategy_id !== null);
|
||||||
|
|
||||||
|
strategySlots = [];
|
||||||
|
|
||||||
|
currentStrategies.forEach((slotData, index) => {
|
||||||
|
|
||||||
|
strategySlots.push({
|
||||||
|
strategy_id: slotData.strategy_id,
|
||||||
|
parameters: {}
|
||||||
|
});
|
||||||
|
|
||||||
|
renderStrategySlot(index);
|
||||||
|
|
||||||
|
const select = document.getElementById(`strategy_select_${index}`);
|
||||||
|
select.value = slotData.strategy_id;
|
||||||
|
|
||||||
|
renderParametersOnly(index, slotData.strategy_id);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Siempre añadir un slot vacío al final
|
||||||
|
if (strategySlots.length < MAX_STRATEGIES) {
|
||||||
|
strategySlots.push({ strategy_id: null, parameters: {} });
|
||||||
|
renderStrategySlot(strategySlots.length - 1);
|
||||||
|
}
|
||||||
|
|
||||||
|
updateCombinationCounter();
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderParametersOnly(index, strategyId) {
|
||||||
|
|
||||||
|
const paramsContainer = document.getElementById(`strategy_params_${index}`);
|
||||||
|
paramsContainer.innerHTML = "";
|
||||||
|
|
||||||
|
if (!strategyId) return;
|
||||||
|
|
||||||
|
const strategyMeta = STRATEGY_CATALOG.find(
|
||||||
|
s => s.strategy_id === strategyId
|
||||||
|
);
|
||||||
|
|
||||||
|
if (!strategyMeta) return;
|
||||||
|
|
||||||
|
strategyMeta.params.forEach(paramName => {
|
||||||
|
|
||||||
|
const col = document.createElement("div");
|
||||||
|
col.className = "col-md-4";
|
||||||
|
|
||||||
|
col.innerHTML = `
|
||||||
|
<label class="form-label fw-semibold">${paramName}</label>
|
||||||
|
<div class="row g-2">
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">Min</small>
|
||||||
|
<input type="number"
|
||||||
|
class="form-control param-input"
|
||||||
|
id="${paramName}_min_${index}">
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">Max</small>
|
||||||
|
<input type="number"
|
||||||
|
class="form-control param-input"
|
||||||
|
id="${paramName}_max_${index}">
|
||||||
|
</div>
|
||||||
|
<div class="col">
|
||||||
|
<small class="text-muted">Step</small>
|
||||||
|
<input type="number"
|
||||||
|
class="form-control param-input"
|
||||||
|
id="${paramName}_step_${index}">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
paramsContainer.appendChild(col);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// =================================================
|
// =================================================
|
||||||
// PROGRESS BAR
|
// PROGRESS BAR
|
||||||
// =================================================
|
// =================================================
|
||||||
@@ -451,6 +863,12 @@ async function validateStrategies() {
|
|||||||
if (text) txt.textContent = text;
|
if (text) txt.textContent = text;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!validateParameterInputs()) {
|
||||||
|
alert("Please fix parameter errors before running WF.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 0) Reset UI
|
// 0) Reset UI
|
||||||
setProgress(0, "Starting...");
|
setProgress(0, "Starting...");
|
||||||
@@ -578,10 +996,37 @@ function wireButtons() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
async function init() {
|
function applyInheritedLock() {
|
||||||
loadContextFromLocalStorage();
|
const locked = document.getElementById("lock_inherited").checked;
|
||||||
wireButtons();
|
const fields = document.querySelectorAll(".inherited-field");
|
||||||
|
|
||||||
|
fields.forEach(f => {
|
||||||
|
f.disabled = locked;
|
||||||
|
if (locked) {
|
||||||
|
f.classList.add("bg-light");
|
||||||
|
} else {
|
||||||
|
f.classList.remove("bg-light");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
document.getElementById("lock_inherited")
|
||||||
|
.addEventListener("change", applyInheritedLock);
|
||||||
|
|
||||||
|
|
||||||
|
async function init() {
|
||||||
|
await loadStrategyCatalog();
|
||||||
|
addStrategySlot();
|
||||||
|
|
||||||
|
loadContextFromLocalStorage();
|
||||||
|
loadFromStep2();
|
||||||
|
applyInheritedLock();
|
||||||
|
|
||||||
|
document.getElementById("stop_type")
|
||||||
|
.addEventListener("change", updateStopUI);
|
||||||
|
|
||||||
|
wireButtons();
|
||||||
|
|
||||||
const strategies = await fetchAvailableStrategies();
|
const strategies = await fetchAvailableStrategies();
|
||||||
renderStrategiesList(strategies);
|
renderStrategiesList(strategies);
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,366 @@
|
|||||||
|
{% extends "layout.html" %}
|
||||||
|
|
||||||
|
{% block content %}
|
||||||
|
<div class="container-xl">
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- Wizard header -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<div class="d-flex align-items-center mb-4">
|
||||||
|
|
||||||
|
<!-- Back arrow -->
|
||||||
|
<div class="me-3">
|
||||||
|
<a href="/calibration/risk" class="btn btn-outline-secondary btn-icon">
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="icon icon-tabler icon-tabler-arrow-left"
|
||||||
|
width="24" height="24" viewBox="0 0 24 24"
|
||||||
|
stroke-width="2" stroke="currentColor"
|
||||||
|
fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z"/>
|
||||||
|
<path d="M15 6l-6 6l6 6"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="flex-grow-1 text-center">
|
||||||
|
<h2 class="mb-0">Calibración · Paso 3 · Strategies</h2>
|
||||||
|
<div class="text-secondary">Optimización + Walk Forward (OOS)</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- Forward arrow (disabled until OK) -->
|
||||||
|
<div class="ms-3">
|
||||||
|
<a
|
||||||
|
id="next-step-btn"
|
||||||
|
href="#"
|
||||||
|
class="btn btn-outline-secondary btn-icon"
|
||||||
|
aria-disabled="true"
|
||||||
|
title="Next step not implemented yet"
|
||||||
|
>
|
||||||
|
<svg xmlns="http://www.w3.org/2000/svg"
|
||||||
|
class="icon icon-tabler icon-tabler-arrow-right"
|
||||||
|
width="24" height="24" viewBox="0 0 24 24"
|
||||||
|
stroke-width="2" stroke="currentColor"
|
||||||
|
fill="none" stroke-linecap="round" stroke-linejoin="round">
|
||||||
|
<path stroke="none" d="M0 0h24v24H0z"/>
|
||||||
|
<path d="M9 6l6 6l-6 6"/>
|
||||||
|
</svg>
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- Context -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Context</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Symbol</label>
|
||||||
|
<input id="symbol" class="form-control" placeholder="BTC/USDT">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Timeframe</label>
|
||||||
|
<input id="timeframe" class="form-control" placeholder="1h">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Account equity</label>
|
||||||
|
<input id="account_equity" class="form-control" type="number" step="0.01" value="10000">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-secondary">
|
||||||
|
Tip: Symbol y timeframe se cargan desde Step 1 (localStorage). Si no aparecen, rellénalos manualmente.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- Risk & Stops -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
|
<h3 class="card-title mb-0">Risk & Stops(Step 2)</h3>
|
||||||
|
|
||||||
|
<div class="form-check form-switch m-0">
|
||||||
|
<input class="form-check-input" type="checkbox" id="lock_inherited" checked>
|
||||||
|
<label class="form-check-label" for="lock_inherited">
|
||||||
|
Bloquear parámetros heredados
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<!-- ================= -->
|
||||||
|
<!-- Risk Configuration -->
|
||||||
|
<!-- ================= -->
|
||||||
|
<h4 class="mb-3">Risk Configuration</h4>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Risk per Trade (%)</label>
|
||||||
|
<input id="risk_fraction" class="form-control inherited-field" type="number" step="0.01">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Max Position Size (%)</label>
|
||||||
|
<input id="max_position_fraction" class="form-control inherited-field" type="number" step="0.1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ================= -->
|
||||||
|
<!-- Stop Configuration -->
|
||||||
|
<!-- ================= -->
|
||||||
|
<h4 class="mb-3">Stop Configuration</h4>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Stop Type</label>
|
||||||
|
<select id="stop_type" class="form-select inherited-field">
|
||||||
|
<option value="fixed">fixed</option>
|
||||||
|
<option value="trailing">trailing</option>
|
||||||
|
<option value="atr">atr</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="stop_fraction_group" class="col-md-4">
|
||||||
|
<label class="form-label">Stop fraction (%)</label>
|
||||||
|
<input id="stop_fraction" class="form-control inherited-field" type="number" step="0.01">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="atr_group" class="col-md-4 d-none">
|
||||||
|
<label class="form-label">ATR period</label>
|
||||||
|
<input id="atr_period" class="form-control inherited-field" type="number">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="atr_multiplier_group" class="col-md-4 d-none">
|
||||||
|
<label class="form-label">ATR multiplier</label>
|
||||||
|
<input id="atr_multiplier" class="form-control inherited-field" type="number" step="0.1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ================= -->
|
||||||
|
<!-- Global Rules -->
|
||||||
|
<!-- ================= -->
|
||||||
|
<h4 class="mb-3">Global Rules</h4>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Max Drawdown (%)</label>
|
||||||
|
<input id="max_drawdown_pct" class="form-control inherited-field" type="number" step="0.1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ================= -->
|
||||||
|
<!-- Optional Parameters -->
|
||||||
|
<!-- ================= -->
|
||||||
|
<h4 class="mb-3">Optional Parameters</h4>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Daily loss limit (%)</label>
|
||||||
|
<input id="daily_loss_limit_pct" class="form-control optional-field" type="number" step="0.1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Max consecutive losses</label>
|
||||||
|
<input id="max_consecutive_losses" class="form-control optional-field" type="number">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Cooldown bars</label>
|
||||||
|
<input id="cooldown_bars" class="form-control optional-field" type="number">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- WF + Optimizer config -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Walk-Forward & Optimization</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Train days</label>
|
||||||
|
<input id="wf_train_days" class="form-control" type="number" step="1" value="120">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Test days</label>
|
||||||
|
<input id="wf_test_days" class="form-control" type="number" step="1" value="30">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Step days (optional)</label>
|
||||||
|
<input id="wf_step_days" class="form-control" type="number" step="1" value="">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Metric</label>
|
||||||
|
<select id="opt_metric" class="form-select">
|
||||||
|
<option value="sharpe_ratio">sharpe_ratio</option>
|
||||||
|
<option value="total_return">total_return</option>
|
||||||
|
<option value="max_drawdown">max_drawdown</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Max combinations</label>
|
||||||
|
<input id="opt_max_combinations" class="form-control" type="number" step="1" value="300">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Min trades (train)</label>
|
||||||
|
<input id="opt_min_trades_train" class="form-control" type="number" step="1" value="30">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Min trades (test)</label>
|
||||||
|
<input id="opt_min_trades_test" class="form-control" type="number" step="1" value="10">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Commission</label>
|
||||||
|
<input id="commission" class="form-control" type="number" step="0.0001" value="0.001">
|
||||||
|
</div>
|
||||||
|
<div class="col-md-3">
|
||||||
|
<label class="form-label">Slippage</label>
|
||||||
|
<input id="slippage" class="form-control" type="number" step="0.0001" value="0.0005">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- Strategy selection -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Strategies</h3>
|
||||||
|
<div class="card-actions">
|
||||||
|
<button id="refresh_strategies_btn" class="btn btn-sm btn-outline-secondary">Refresh</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="strategies_container" class="d-flex flex-column gap-4"></div>
|
||||||
|
<div class="card p-3">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<strong>Total combinations</strong>
|
||||||
|
<span id="combination_counter">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-end">
|
||||||
|
<small class="text-muted">
|
||||||
|
Estimated WF time:
|
||||||
|
<span id="wf_time_estimate">~ 0 sec</span>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3 text-secondary">
|
||||||
|
Cada estrategia incluye un <b>param_grid</b> en JSON.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- Actions -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<div class="d-flex gap-2 mb-4">
|
||||||
|
<button id="validate_strategies_btn" class="btn btn-primary">
|
||||||
|
Validate (WF)
|
||||||
|
</button>
|
||||||
|
<button id="report_strategies_btn" class="btn btn-outline-primary">
|
||||||
|
Generate PDF report
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- Prograss Bar -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<div id="wf_progress_card" class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Walk-Forward Progress</h3>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
|
||||||
|
<div class="progress mb-2">
|
||||||
|
<div
|
||||||
|
id="wfProgressBar"
|
||||||
|
class="progress-bar progress-bar-striped progress-bar-animated"
|
||||||
|
role="progressbar"
|
||||||
|
style="width: 0%"
|
||||||
|
>
|
||||||
|
0%
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="wf_progress_text" class="text-secondary small">
|
||||||
|
Waiting to start...
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- Results -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<div class="card mb-4">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Results</h3>
|
||||||
|
<div class="card-actions">
|
||||||
|
<span id="strategies_status_badge" class="badge bg-secondary">—</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<div id="strategies_message" class="mb-3 text-secondary">Run validation to see results.</div>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Strategy plot</label>
|
||||||
|
<select id="plot_strategy_select" class="form-select"></select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="mt-3">
|
||||||
|
<div id="plot_equity" style="height: 320px;"></div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-3">
|
||||||
|
<div id="plot_returns" style="height: 320px;"></div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<hr class="my-4">
|
||||||
|
|
||||||
|
<div id="strategies_table_wrap"></div>
|
||||||
|
|
||||||
|
<details class="mt-3">
|
||||||
|
<summary class="text-secondary">Debug JSON</summary>
|
||||||
|
<pre id="strategies_debug" class="mt-2" style="max-height: 300px; overflow:auto;"></pre>
|
||||||
|
</details>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ========================= -->
|
||||||
|
<!-- PDF Viewer -->
|
||||||
|
<!-- ========================= -->
|
||||||
|
<div id="pdf_viewer_section" class="card mb-4 d-none">
|
||||||
|
<div class="card-header">
|
||||||
|
<h3 class="card-title">Strategies Report (PDF)</h3>
|
||||||
|
<div class="card-actions">
|
||||||
|
<button id="close_pdf_btn" class="btn btn-sm btn-outline-secondary">Close</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="card-body">
|
||||||
|
<iframe id="pdf_frame" style="width: 100%; height: 800px; border: none;"></iframe>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
|
||||||
|
<script src="/static/js/pages/calibration_optimization.js"></script>
|
||||||
|
{% endblock %}
|
||||||
@@ -78,77 +78,109 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ========================= -->
|
<!-- ========================= -->
|
||||||
<!-- Risk & Stops snapshot -->
|
<!-- Risk & Stops -->
|
||||||
<!-- ========================= -->
|
<!-- ========================= -->
|
||||||
<div class="card mb-4">
|
<div class="card mb-4">
|
||||||
<div class="card-header">
|
<div class="card-header d-flex justify-content-between align-items-center">
|
||||||
<h3 class="card-title">Risk & Stops snapshot (Step 2)</h3>
|
<h3 class="card-title mb-0">Risk & Stops(Step 2)</h3>
|
||||||
<div class="card-actions">
|
|
||||||
<button id="load_step2_btn" class="btn btn-sm btn-outline-primary">Load from Step 2</button>
|
<div class="form-check form-switch m-0">
|
||||||
|
<input class="form-check-input" type="checkbox" id="lock_inherited" checked>
|
||||||
|
<label class="form-check-label" for="lock_inherited">
|
||||||
|
Bloquear parámetros heredados
|
||||||
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div class="row g-3">
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Stop type</label>
|
|
||||||
<select id="stop_type" class="form-select">
|
|
||||||
<option value="fixed">fixed</option>
|
|
||||||
<option value="trailing">trailing</option>
|
|
||||||
<option value="atr">atr</option>
|
|
||||||
</select>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3">
|
<!-- ================= -->
|
||||||
<label class="form-label">Stop fraction (%)</label>
|
<!-- Risk Configuration -->
|
||||||
<input id="stop_fraction" class="form-control" type="number" step="0.01" value="1.0">
|
<!-- ================= -->
|
||||||
</div>
|
<h4 class="mb-3">Risk Configuration</h4>
|
||||||
|
|
||||||
<div class="col-md-3">
|
<div class="row g-3 mb-4">
|
||||||
<label class="form-label">ATR period</label>
|
<div class="col-md-4">
|
||||||
<input id="atr_period" class="form-control" type="number" step="1" value="14">
|
<label class="form-label">Risk per Trade (%)</label>
|
||||||
</div>
|
<input id="risk_fraction" class="form-control inherited-field" type="number" step="0.01">
|
||||||
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">ATR multiplier</label>
|
|
||||||
<input id="atr_multiplier" class="form-control" type="number" step="0.1" value="3.0">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Risk per trade (%)</label>
|
|
||||||
<input id="risk_fraction" class="form-control" type="number" step="0.01" value="1.0">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Max position fraction (%)</label>
|
|
||||||
<input id="max_position_fraction" class="form-control" type="number" step="0.1" value="95">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Max DD (%)</label>
|
|
||||||
<input id="max_drawdown_pct" class="form-control" type="number" step="0.1" value="20">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Daily loss limit (%) (optional)</label>
|
|
||||||
<input id="daily_loss_limit_pct" class="form-control" type="number" step="0.1" value="">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Max consecutive losses (optional)</label>
|
|
||||||
<input id="max_consecutive_losses" class="form-control" type="number" step="1" value="">
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-md-3">
|
|
||||||
<label class="form-label">Cooldown bars (optional)</label>
|
|
||||||
<input id="cooldown_bars" class="form-control" type="number" step="1" value="">
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="mt-3 text-secondary">
|
|
||||||
Este snapshot se envía al backend para reproducibilidad y para que WF/optimizer use el mismo sizing/stop.
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Max Position Size (%)</label>
|
||||||
|
<input id="max_position_fraction" class="form-control inherited-field" type="number" step="0.1">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<!-- ================= -->
|
||||||
|
<!-- Stop Configuration -->
|
||||||
|
<!-- ================= -->
|
||||||
|
<h4 class="mb-3">Stop Configuration</h4>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Stop Type</label>
|
||||||
|
<select id="stop_type" class="form-select inherited-field">
|
||||||
|
<option value="fixed">fixed</option>
|
||||||
|
<option value="trailing">trailing</option>
|
||||||
|
<option value="atr">atr</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="stop_fraction_group" class="col-md-4">
|
||||||
|
<label class="form-label">Stop fraction (%)</label>
|
||||||
|
<input id="stop_fraction" class="form-control inherited-field" type="number" step="0.01">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="atr_group" class="col-md-4 d-none">
|
||||||
|
<label class="form-label">ATR period</label>
|
||||||
|
<input id="atr_period" class="form-control inherited-field" type="number">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div id="atr_multiplier_group" class="col-md-4 d-none">
|
||||||
|
<label class="form-label">ATR multiplier</label>
|
||||||
|
<input id="atr_multiplier" class="form-control inherited-field" type="number" step="0.1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
|
||||||
|
<!-- ================= -->
|
||||||
|
<!-- Global Rules -->
|
||||||
|
<!-- ================= -->
|
||||||
|
<h4 class="mb-3">Global Rules</h4>
|
||||||
|
|
||||||
|
<div class="row g-3 mb-4">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Max Drawdown (%)</label>
|
||||||
|
<input id="max_drawdown_pct" class="form-control inherited-field" type="number" step="0.1">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- ================= -->
|
||||||
|
<!-- Optional Parameters -->
|
||||||
|
<!-- ================= -->
|
||||||
|
<h4 class="mb-3">Optional Parameters</h4>
|
||||||
|
|
||||||
|
<div class="row g-3">
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Daily loss limit (%)</label>
|
||||||
|
<input id="daily_loss_limit_pct" class="form-control optional-field" type="number" step="0.1">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Max consecutive losses</label>
|
||||||
|
<input id="max_consecutive_losses" class="form-control optional-field" type="number">
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="col-md-4">
|
||||||
|
<label class="form-label">Cooldown bars</label>
|
||||||
|
<input id="cooldown_bars" class="form-control optional-field" type="number">
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- ========================= -->
|
<!-- ========================= -->
|
||||||
<!-- WF + Optimizer config -->
|
<!-- WF + Optimizer config -->
|
||||||
@@ -216,7 +248,19 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="card-body">
|
<div class="card-body">
|
||||||
<div id="strategies_list" class="row g-3"></div>
|
<div id="strategies_container" class="d-flex flex-column gap-4"></div>
|
||||||
|
<div class="card p-3">
|
||||||
|
<div class="d-flex justify-content-between">
|
||||||
|
<strong>Total combinations</strong>
|
||||||
|
<span id="combination_counter">0</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="mt-2 text-end">
|
||||||
|
<small class="text-muted">
|
||||||
|
Estimated WF time:
|
||||||
|
<span id="wf_time_estimate">~ 0 sec</span>
|
||||||
|
</small>
|
||||||
|
</div>
|
||||||
<div class="mt-3 text-secondary">
|
<div class="mt-3 text-secondary">
|
||||||
Cada estrategia incluye un <b>param_grid</b> en JSON.
|
Cada estrategia incluye un <b>param_grid</b> en JSON.
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user