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:
DaM
2026-02-15 17:01:00 +01:00
parent 4365366e7d
commit 547a909965
13 changed files with 2852 additions and 93 deletions

View 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

View 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)

View File

@@ -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,

View File

@@ -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
@@ -128,6 +129,17 @@ def create_app() -> FastAPI:
}, },
) )
@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,
},
)
# -------------------------------------------------- # --------------------------------------------------
# API routers (versionados) # API routers (versionados)
@@ -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

View 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"})

View File

@@ -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(

View 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]

View File

@@ -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):

File diff suppressed because it is too large Load Diff

View File

@@ -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
// ================================================= // =================================================

View File

@@ -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
};
}); });
if (out.length === 0) { strategies.push({
throw new Error("Select at least 1 strategy"); strategy_id: slot.strategy_id,
} parameters: parameters
return out; });
});
return strategies;
} }
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,8 +996,35 @@ function wireButtons() {
}); });
} }
function applyInheritedLock() {
const locked = document.getElementById("lock_inherited").checked;
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() { async function init() {
await loadStrategyCatalog();
addStrategySlot();
loadContextFromLocalStorage(); loadContextFromLocalStorage();
loadFromStep2();
applyInheritedLock();
document.getElementById("stop_type")
.addEventListener("change", updateStopUI);
wireButtons(); wireButtons();
const strategies = await fetchAvailableStrategies(); const strategies = await fetchAvailableStrategies();

View File

@@ -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 %}

View File

@@ -78,75 +78,107 @@
</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> <!-- Risk Configuration -->
<select id="stop_type" class="form-select"> <!-- ================= -->
<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="fixed">fixed</option>
<option value="trailing">trailing</option> <option value="trailing">trailing</option>
<option value="atr">atr</option> <option value="atr">atr</option>
</select> </select>
</div> </div>
<div class="col-md-3"> <div id="stop_fraction_group" class="col-md-4">
<label class="form-label">Stop fraction (%)</label> <label class="form-label">Stop fraction (%)</label>
<input id="stop_fraction" class="form-control" type="number" step="0.01" value="1.0"> <input id="stop_fraction" class="form-control inherited-field" type="number" step="0.01">
</div> </div>
<div class="col-md-3"> <div id="atr_group" class="col-md-4 d-none">
<label class="form-label">ATR period</label> <label class="form-label">ATR period</label>
<input id="atr_period" class="form-control" type="number" step="1" value="14"> <input id="atr_period" class="form-control inherited-field" type="number">
</div> </div>
<div class="col-md-3"> <div id="atr_multiplier_group" class="col-md-4 d-none">
<label class="form-label">ATR multiplier</label> <label class="form-label">ATR multiplier</label>
<input id="atr_multiplier" class="form-control" type="number" step="0.1" value="3.0"> <input id="atr_multiplier" class="form-control inherited-field" type="number" step="0.1">
</div> </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>
<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"> <!-- 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> </div>
<div class="col-md-3"> <!-- ================= -->
<label class="form-label">Max DD (%)</label> <!-- Optional Parameters -->
<input id="max_drawdown_pct" class="form-control" type="number" step="0.1" value="20"> <!-- ================= -->
<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>
<div class="col-md-3"> <div class="col-md-4">
<label class="form-label">Daily loss limit (%) (optional)</label> <label class="form-label">Max consecutive losses</label>
<input id="daily_loss_limit_pct" class="form-control" type="number" step="0.1" value=""> <input id="max_consecutive_losses" class="form-control optional-field" type="number">
</div> </div>
<div class="col-md-3"> <div class="col-md-4">
<label class="form-label">Max consecutive losses (optional)</label> <label class="form-label">Cooldown bars</label>
<input id="max_consecutive_losses" class="form-control" type="number" step="1" value=""> <input id="cooldown_bars" class="form-control optional-field" type="number">
</div>
</div> </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 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>
</div> </div>
</div> </div>
@@ -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>