Status when starting a new project on Chat GPT

This commit is contained in:
DaM
2026-03-01 16:50:15 +01:00
parent 547a909965
commit 2ec51d0daa
10 changed files with 1232 additions and 510 deletions

View File

@@ -21,27 +21,11 @@ from src.risk.sizing.percent_risk import PercentRiskSizer
# -------------------------------------------------- # --------------------------------------------------
# Strategy registry (con metadata de parámetros) # Strategy registry (con metadata de parámetros)
# -------------------------------------------------- # --------------------------------------------------
from src.strategies.registry import STRATEGY_REGISTRY
from src.strategies.moving_average import MovingAverageCrossover from src.strategies.moving_average import MovingAverageCrossover
from src.strategies.rsi_strategy import RSIStrategy from src.strategies.rsi_strategy import RSIStrategy
from src.strategies.buy_and_hold import BuyAndHold 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 # Helpers
# -------------------------------------------------- # --------------------------------------------------
@@ -49,15 +33,24 @@ STRATEGY_REGISTRY = {
def list_available_strategies() -> List[Dict[str, Any]]: def list_available_strategies() -> List[Dict[str, Any]]:
""" """
Devuelve metadata completa para UI. Devuelve metadata completa para UI.
Usa parameters_schema() como fuente de verdad.
""" """
out = []
for sid, entry in STRATEGY_REGISTRY.items(): out: List[Dict[str, Any]] = []
for strategy_id, strategy_class in STRATEGY_REGISTRY.items():
if not hasattr(strategy_class, "parameters_schema"):
continue
schema = strategy_class.parameters_schema()
out.append({ out.append({
"strategy_id": sid, "strategy_id": strategy_id,
"name": entry["class"].__name__, "name": strategy_class.__name__,
"params": entry["params"], "params": list(schema.keys()),
"tags": [], # puedes rellenar más adelante "parameters_schema": schema, # 🔥 ahora enviamos schema completo
"tags": [],
}) })
return out return out
@@ -67,7 +60,7 @@ def _build_stop_loss(stop_schema) -> object | None:
if stop_schema.type == "fixed": if stop_schema.type == "fixed":
return FixedStop(stop_fraction=float(stop_schema.stop_fraction)) return FixedStop(stop_fraction=float(stop_schema.stop_fraction))
if stop_schema.type == "trailing": if stop_schema.type == "trailing":
return TrailingStop(stop_fraction=float(stop_schema.stop_fraction)) return TrailingStop(trailing_fraction=float(stop_schema.stop_fraction))
if stop_schema.type == "atr": if stop_schema.type == "atr":
return ATRStop( return ATRStop(
atr_period=int(stop_schema.atr_period), atr_period=int(stop_schema.atr_period),
@@ -94,27 +87,6 @@ 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
# -------------------------------------------------- # --------------------------------------------------
@@ -162,14 +134,21 @@ def inspect_strategies_config(
step_td = pd.Timedelta(days=int(payload.wf.step_days or payload.wf.test_days)) step_td = pd.Timedelta(days=int(payload.wf.step_days or payload.wf.test_days))
overall_status = "ok" overall_status = "ok"
log.info(f"🔥 Strategies received: {len(payload.strategies)}")
results: List[Dict[str, Any]] = [] results: List[Dict[str, Any]] = []
series: Dict[str, Any] = {"strategies": {}} if include_series else {} series: Dict[str, Any] = {"strategies": {}} if include_series else {}
log.info(f"🔥 Strategies received: {len(payload.strategies)}")
for sel in payload.strategies: for sel in payload.strategies:
sid = sel.strategy_id sid = sel.strategy_id
entry = STRATEGY_REGISTRY.get(sid) entry = STRATEGY_REGISTRY.get(sid)
log.info(f"🧠 Step3 | Processing strategy: {sid}")
if entry is None: if entry is None:
results.append({ results.append({
"strategy_id": sid, "strategy_id": sid,
@@ -183,10 +162,13 @@ def inspect_strategies_config(
"windows": [], "windows": [],
}) })
overall_status = "fail" overall_status = "fail"
log.error(f"❌ Strategy not found in registry: {sid}")
continue continue
strategy_class = entry["class"] strategy_class = STRATEGY_REGISTRY[sid]
valid_params = set(entry["params"])
schema = strategy_class.parameters_schema()
valid_params = set(schema.keys())
range_params = set(sel.parameters.keys()) range_params = set(sel.parameters.keys())
@@ -209,17 +191,13 @@ def inspect_strategies_config(
continue continue
# -------------------------------------------------- # --------------------------------------------------
# Convert ranges -> param_grid real # Build fixed_params (VALIDATION MODE)
# -------------------------------------------------- # --------------------------------------------------
param_grid = {} fixed_params = {}
for pname, pvalue in sel.parameters.items():
fixed_params[pname] = pvalue
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)):
@@ -248,7 +226,8 @@ def inspect_strategies_config(
try: try:
wf = WalkForwardValidator( wf = WalkForwardValidator(
strategy_class=strategy_class, strategy_class=strategy_class,
param_grid=param_grid, param_grid=None,
fixed_params=fixed_params,
data=df, data=df,
train_window=train_td, train_window=train_td,
test_window=test_td, test_window=test_td,
@@ -256,36 +235,47 @@ def inspect_strategies_config(
initial_capital=float(payload.account_equity), initial_capital=float(payload.account_equity),
commission=float(payload.commission), commission=float(payload.commission),
slippage=float(payload.slippage), slippage=float(payload.slippage),
optimizer_metric=str(payload.optimization.optimizer_metric),
position_sizer=capped_sizer, position_sizer=capped_sizer,
stop_loss=stop_loss, stop_loss=stop_loss,
max_combinations=int(payload.optimization.max_combinations),
progress_callback=progress_callback, progress_callback=progress_callback,
) )
wf_res = wf.run() wf_res = wf.run()
win_df: pd.DataFrame = wf_res["windows"] 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 = [] oos_returns = []
oos_dd = []
warnings_list = []
n_windows = 0
if win_df is None or win_df.empty:
status = "warning"
msg = "No closed trades in OOS"
warnings_list.append("Walk-forward produced no closed trades.")
else: else:
oos_returns = win_df["return_pct"].tolist()
oos_dd = win_df["max_dd_pct"].tolist()
n_windows = len(win_df)
trades = win_df["trades"].astype(int).tolist() trades = win_df["trades"].astype(int).tolist()
too_few = sum(t < int(payload.optimization.min_trades_test) for t in trades) too_few = sum(t < int(payload.wf.min_trades_test) for t in trades)
if too_few > 0: if too_few > 0:
warnings_list.append(
f"{too_few} test windows have fewer than {payload.wf.min_trades_test} trades"
)
windows_out = []
if warnings_list:
status = "warning" status = "warning"
msg = f"{too_few} windows below min_trades_test" msg = "Validation completed with warnings"
if overall_status == "ok": if overall_status == "ok":
overall_status = "warning" overall_status = "warning"
else: else:
status = "ok" status = "ok"
msg = "WF OK" msg = "WF OK"
windows_out = []
for _, r in win_df.iterrows(): for _, r in win_df.iterrows():
windows_out.append({ windows_out.append({
"window": int(r["window"]), "window": int(r["window"]),
@@ -311,6 +301,7 @@ def inspect_strategies_config(
"strategy_id": sid, "strategy_id": sid,
"status": status, "status": status,
"message": msg, "message": msg,
"warnings": warnings_list if status == "warning" else [],
"n_windows": int(len(windows_out)), "n_windows": int(len(windows_out)),
"oos_final_equity": oos_final, "oos_final_equity": oos_final,
"oos_total_return_pct": float(oos_total_return), "oos_total_return_pct": float(oos_total_return),
@@ -323,6 +314,7 @@ def inspect_strategies_config(
series["strategies"][sid] = { series["strategies"][sid] = {
"window_returns_pct": oos_returns, "window_returns_pct": oos_returns,
"window_equity": eq_curve, "window_equity": eq_curve,
"window_trades": win_df["trades"].tolist(),
} }
except Exception as e: except Exception as e:

View File

@@ -1,6 +1,6 @@
# src/backtest/walk_forward.py # src/core/walk_forward.py
import pandas as pd import pandas as pd
from typing import List, Dict, Optional from typing import List, Dict, Optional, Callable, Any
from src.core.optimizer import ParameterOptimizer from src.core.optimizer import ParameterOptimizer
from src.core.engine import Engine from src.core.engine import Engine
from src.risk.sizing.base import PositionSizer from src.risk.sizing.base import PositionSizer
@@ -19,7 +19,7 @@ class WalkForwardValidator:
def __init__( def __init__(
self, self,
strategy_class, strategy_class,
param_grid: dict, param_grid: Optional[dict],
data: pd.DataFrame, data: pd.DataFrame,
train_window: pd.Timedelta, train_window: pd.Timedelta,
test_window: pd.Timedelta, test_window: pd.Timedelta,
@@ -34,9 +34,12 @@ class WalkForwardValidator:
stop_loss: Optional[StopLoss] = None, stop_loss: Optional[StopLoss] = None,
max_combinations: Optional[int] = None, max_combinations: Optional[int] = None,
progress_callback: Optional[callable] = None, progress_callback: Optional[callable] = None,
fixed_params: Optional[dict] = None,
): ):
self.strategy_class = strategy_class self.strategy_class = strategy_class
self.param_grid = param_grid self.param_grid = param_grid
self.fixed_params = fixed_params
self.data = data.sort_index() self.data = data.sort_index()
self.train_window = train_window self.train_window = train_window
@@ -62,6 +65,13 @@ class WalkForwardValidator:
if not self.data.index.is_monotonic_increasing: if not self.data.index.is_monotonic_increasing:
raise ValueError("data.index debe estar ordenado cronológicamente") raise ValueError("data.index debe estar ordenado cronológicamente")
# ✅ Validación de modo (NUEVO, mínimo y claro)
if self.param_grid is not None and self.fixed_params is not None:
raise ValueError("WalkForwardValidator: usa param_grid (optimization) o fixed_params (validation), no ambos.")
if self.param_grid is None and self.fixed_params is None:
raise ValueError("WalkForwardValidator: debes pasar param_grid o fixed_params.")
# ------------------------------------------------------------------ # ------------------------------------------------------------------
# 🔹 GENERACIÓN DE VENTANAS TEMPORALES # 🔹 GENERACIÓN DE VENTANAS TEMPORALES
# ------------------------------------------------------------------ # ------------------------------------------------------------------
@@ -186,6 +196,13 @@ class WalkForwardValidator:
continue continue
# 1⃣ Optimización TRAIN # 1⃣ Optimización TRAIN
best_train_metric = None
if self.fixed_params is not None:
# ✅ VALIDATION MODE: sin optimización
best_params = self.fixed_params
else:
# ✅ OPTIMIZATION MODE
optimizer = ParameterOptimizer( optimizer = ParameterOptimizer(
strategy_class=self.strategy_class, strategy_class=self.strategy_class,
data=train_data, data=train_data,
@@ -261,6 +278,7 @@ class WalkForwardValidator:
"n_windows": len(rows), "n_windows": len(rows),
"data_start": self.data.index.min(), "data_start": self.data.index.min(),
"data_end": self.data.index.max(), "data_end": self.data.index.max(),
"mode": "validation" if self.fixed_params is not None else "optimization"
}, },
"windows": pd.DataFrame(rows), "windows": pd.DataFrame(rows),
"raw_results": raw_results, "raw_results": raw_results,

View File

@@ -10,7 +10,12 @@ class MovingAverageCrossover(Strategy):
""" """
Estrategia de cruce de medias móviles Estrategia de cruce de medias móviles
Señales: Señales:@classmethod
def default_parameters(cls) -> dict:
return {
"fast_period": 10,
"slow_period": 30,
}
- BUY: Cruce alcista de medias + (ADX >= threshold si está activado) - BUY: Cruce alcista de medias + (ADX >= threshold si está activado)
- SELL: Cruce bajista de medias - SELL: Cruce bajista de medias
- HOLD: En cualquier otro caso - HOLD: En cualquier otro caso
@@ -28,6 +33,8 @@ class MovingAverageCrossover(Strategy):
Sin ADX todavía → primero evaluamos la señal “pura” Sin ADX todavía → primero evaluamos la señal “pura”
""" """
strategy_id = "moving_average"
def __init__( def __init__(
self, self,
fast_period: int = 20, fast_period: int = 20,
@@ -55,7 +62,38 @@ class MovingAverageCrossover(Strategy):
if self.ma_type not in ['sma', 'ema']: if self.ma_type not in ['sma', 'ema']:
raise ValueError("ma_type debe ser 'sma' o 'ema'") raise ValueError("ma_type debe ser 'sma' o 'ema'")
# ------------------------------------------------------------------ @classmethod
def parameters_schema(cls) -> dict:
return {
"fast_period": {
"type": "int",
"min": 1,
"max": 500,
"default": 20,
},
"slow_period": {
"type": "int",
"min": 1,
"max": 500,
"default": 50,
},
"ma_type": {
"type": "enum",
"choices": ["sma", "ema"],
"default": "ema",
},
"use_adx": {
"type": "bool",
"default": False,
},
"adx_threshold": {
"type": "float",
"min": 1,
"max": 100,
"default": 20.0,
},
}
def init_indicators(self, data: pd.DataFrame) -> pd.DataFrame: def init_indicators(self, data: pd.DataFrame) -> pd.DataFrame:
""" """

View File

@@ -0,0 +1,16 @@
# src/strategies/registry.py
from .moving_average import MovingAverageCrossover
from .rsi_strategy import RSIStrategy
from .buy_and_hold import BuyAndHold
ALL_STRATEGIES = [
MovingAverageCrossover,
RSIStrategy,
]
STRATEGY_REGISTRY = {
cls.strategy_id: cls
for cls in ALL_STRATEGIES
}

View File

@@ -20,6 +20,8 @@ class RSIStrategy(Strategy):
overbought_threshold: Umbral de sobrecompra (default: 70) overbought_threshold: Umbral de sobrecompra (default: 70)
""" """
strategy_id = "rsi"
def __init__(self, rsi_period: int = 14, oversold_threshold: float = 30, overbought_threshold: float = 70): def __init__(self, rsi_period: int = 14, oversold_threshold: float = 30, overbought_threshold: float = 70):
params = { params = {
@@ -34,6 +36,30 @@ class RSIStrategy(Strategy):
self.oversold = oversold_threshold self.oversold = oversold_threshold
self.overbought = overbought_threshold self.overbought = overbought_threshold
@classmethod
def parameters_schema(cls) -> dict:
return {
"rsi_period": {
"type": "int",
"min": 1,
"max": 200,
"default": 14,
},
"oversold": {
"type": "float",
"min": 0,
"max": 100,
"default": 30,
},
"overbought": {
"type": "float",
"min": 0,
"max": 100,
"default": 70,
},
}
def init_indicators(self, data: pd.DataFrame) -> pd.DataFrame: def init_indicators(self, data: pd.DataFrame) -> pd.DataFrame:
""" """
Calcula el RSI Calcula el RSI

View File

@@ -10,6 +10,7 @@ from fastapi import APIRouter, Depends, HTTPException, Request
from fastapi.responses import JSONResponse, HTMLResponse from fastapi.responses import JSONResponse, HTMLResponse
from src.data.storage import StorageManager from src.data.storage import StorageManager
from src.strategies.registry import STRATEGY_REGISTRY
from src.calibration.strategies_inspector import ( from src.calibration.strategies_inspector import (
inspect_strategies_config, inspect_strategies_config,
list_available_strategies, list_available_strategies,
@@ -37,22 +38,40 @@ def get_storage() -> StorageManager:
@router.get("/catalog") @router.get("/catalog")
def strategy_catalog(): def strategy_catalog():
strategies = list_available_strategies() strategies = list_available_strategies()
# Añadimos defaults sugeridos enriched = []
for s in strategies:
s["parameters_meta"] = [ for s in strategies:
{
"name": p, strategy_id = s["strategy_id"]
"type": "int", strategy_class = STRATEGY_REGISTRY[strategy_id]
"default_min": 10,
"default_max": 50, schema = strategy_class.parameters_schema()
"default_step": 10,
} parameters_meta = []
for p in s["params"]
] for name, meta in schema.items():
parameters_meta.append({
"name": name,
"type": meta.get("type"),
"default_value": meta.get("default"),
"choices": meta.get("choices"),
"min": meta.get("min"),
"max": meta.get("max"),
})
enriched.append({
"strategy_id": strategy_id,
"name": s["name"],
"params": list(schema.keys()),
"parameters_meta": parameters_meta,
})
return {"strategies": enriched}
return {"strategies": strategies}
@router.post("/inspect", response_model=CalibrationStrategiesInspectResponse) @router.post("/inspect", response_model=CalibrationStrategiesInspectResponse)
def inspect_strategies( def inspect_strategies(
@@ -147,9 +166,9 @@ def report_strategies(
"WF train_days": payload.wf.train_days, "WF train_days": payload.wf.train_days,
"WF test_days": payload.wf.test_days, "WF test_days": payload.wf.test_days,
"WF step_days": payload.wf.step_days or payload.wf.test_days, "WF step_days": payload.wf.step_days or payload.wf.test_days,
"Optimizer metric": payload.optimization.optimizer_metric, "Min trades per window (test)": payload.wf.min_trades_test,
"Max combinations": payload.optimization.max_combinations,
}, },
results=result, results=result,
) )

View File

@@ -1,33 +1,24 @@
# src/web/api/v2/schemas/calibration_strategies.py # src/web/api/v2/schemas/calibration_strategies.py
from typing import Any, Dict, List, Literal, Optional from typing import Any, Dict, List, Literal, Optional, Union
from pydantic import BaseModel, Field from pydantic import BaseModel, Field
from .calibration_risk import StopConfigSchema, RiskConfigSchema, GlobalRiskRulesSchema from .calibration_risk import StopConfigSchema, RiskConfigSchema, GlobalRiskRulesSchema
ParameterValue = Union[int, float, bool, str]
class WalkForwardConfigSchema(BaseModel): class WalkForwardConfigSchema(BaseModel):
train_days: int = Field(..., gt=0) train_days: int = Field(..., gt=0)
test_days: int = Field(..., gt=0) test_days: int = Field(..., gt=0)
step_days: Optional[int] = Field(None, gt=0) # if None => step = test_days step_days: Optional[int] = Field(None, gt=0)
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) 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
parameters: Dict[str, ParameterRangeSchema] parameters: Dict[str, ParameterValue]
class CalibrationStrategiesInspectRequest(BaseModel): class CalibrationStrategiesInspectRequest(BaseModel):
@@ -42,7 +33,6 @@ class CalibrationStrategiesInspectRequest(BaseModel):
strategies: List[StrategySelectionSchema] strategies: List[StrategySelectionSchema]
wf: WalkForwardConfigSchema wf: WalkForwardConfigSchema
optimization: OptimizationConfigSchema
commission: float = Field(0.001, ge=0) commission: float = Field(0.001, ge=0)
slippage: float = Field(0.0005, ge=0) slippage: float = Field(0.0005, ge=0)

File diff suppressed because it is too large Load Diff

View File

@@ -187,7 +187,7 @@
<!-- ========================= --> <!-- ========================= -->
<div class="card mb-4"> <div class="card mb-4">
<div class="card-header"> <div class="card-header">
<h3 class="card-title">Walk-Forward & Optimization</h3> <h3 class="card-title">Walk-Forward Validation (OOS)</h3>
</div> </div>
<div class="card-body"> <div class="card-body">
<div class="row g-3"> <div class="row g-3">
@@ -203,26 +203,10 @@
<label class="form-label">Step days (optional)</label> <label class="form-label">Step days (optional)</label>
<input id="wf_step_days" class="form-control" type="number" step="1" value=""> <input id="wf_step_days" class="form-control" type="number" step="1" value="">
</div> </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"> <div class="col-md-3">
<label class="form-label">Max combinations</label> <label class="form-label">Min trades per window (test)</label>
<input id="opt_max_combinations" class="form-control" type="number" step="1" value="300"> <input id="wf_min_trades_test" class="form-control" type="number" step="1" value="10">
</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>
<div class="col-md-3"> <div class="col-md-3">
@@ -262,7 +246,7 @@
</small> </small>
</div> </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 utiliza parámetros fijos (validación OOS sin grid).
</div> </div>
</div> </div>
</div> </div>

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_strategies.js"></script>
{% endblock %}