Status when starting a new project on Chat GPT
This commit is contained in:
@@ -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,49 +235,60 @@ 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"]
|
||||||
|
|
||||||
|
oos_returns = []
|
||||||
|
oos_dd = []
|
||||||
|
warnings_list = []
|
||||||
|
n_windows = 0
|
||||||
|
|
||||||
if win_df is None or win_df.empty:
|
if win_df is None or win_df.empty:
|
||||||
status = "fail"
|
status = "warning"
|
||||||
msg = "WF produced no valid windows"
|
msg = "No closed trades in OOS"
|
||||||
overall_status = "fail"
|
warnings_list.append("Walk-forward produced no closed trades.")
|
||||||
windows_out = []
|
|
||||||
oos_returns = []
|
|
||||||
else:
|
else:
|
||||||
trades = win_df["trades"].astype(int).tolist()
|
oos_returns = win_df["return_pct"].tolist()
|
||||||
too_few = sum(t < int(payload.optimization.min_trades_test) for t in trades)
|
oos_dd = win_df["max_dd_pct"].tolist()
|
||||||
|
n_windows = len(win_df)
|
||||||
|
|
||||||
if too_few > 0:
|
trades = win_df["trades"].astype(int).tolist()
|
||||||
status = "warning"
|
too_few = sum(t < int(payload.wf.min_trades_test) for t in trades)
|
||||||
msg = f"{too_few} windows below min_trades_test"
|
|
||||||
if overall_status == "ok":
|
|
||||||
overall_status = "warning"
|
|
||||||
else:
|
|
||||||
status = "ok"
|
|
||||||
msg = "WF OK"
|
|
||||||
|
|
||||||
windows_out = []
|
if too_few > 0:
|
||||||
for _, r in win_df.iterrows():
|
warnings_list.append(
|
||||||
windows_out.append({
|
f"{too_few} test windows have fewer than {payload.wf.min_trades_test} trades"
|
||||||
"window": int(r["window"]),
|
)
|
||||||
"train_start": str(r["train_start"]),
|
|
||||||
"train_end": str(r["train_end"]),
|
windows_out = []
|
||||||
"test_start": str(r["test_start"]),
|
|
||||||
"test_end": str(r["test_end"]),
|
if warnings_list:
|
||||||
"return_pct": float(r["return_pct"]),
|
status = "warning"
|
||||||
"sharpe": float(r["sharpe"]),
|
msg = "Validation completed with warnings"
|
||||||
"max_dd_pct": float(r["max_dd_pct"]),
|
if overall_status == "ok":
|
||||||
"trades": int(r["trades"]),
|
overall_status = "warning"
|
||||||
"params": dict(r["params"]) if isinstance(r["params"], dict) else r["params"],
|
else:
|
||||||
})
|
status = "ok"
|
||||||
|
msg = "WF OK"
|
||||||
|
|
||||||
|
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()
|
oos_returns = win_df["return_pct"].astype(float).tolist()
|
||||||
|
|
||||||
@@ -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:
|
||||||
|
|||||||
@@ -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,26 +196,33 @@ class WalkForwardValidator:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
# 1️⃣ Optimización TRAIN
|
# 1️⃣ Optimización TRAIN
|
||||||
optimizer = ParameterOptimizer(
|
best_train_metric = None
|
||||||
strategy_class=self.strategy_class,
|
|
||||||
data=train_data,
|
|
||||||
initial_capital=self.initial_capital,
|
|
||||||
commission=self.commission,
|
|
||||||
slippage=self.slippage,
|
|
||||||
position_size=self.position_size,
|
|
||||||
position_sizer=self.position_sizer,
|
|
||||||
stop_loss=self.stop_loss,
|
|
||||||
max_combinations=self.max_combinations,
|
|
||||||
)
|
|
||||||
|
|
||||||
opt_df = optimizer.optimize(self.param_grid)
|
if self.fixed_params is not None:
|
||||||
|
# ✅ VALIDATION MODE: sin optimización
|
||||||
|
best_params = self.fixed_params
|
||||||
|
else:
|
||||||
|
# ✅ OPTIMIZATION MODE
|
||||||
|
optimizer = ParameterOptimizer(
|
||||||
|
strategy_class=self.strategy_class,
|
||||||
|
data=train_data,
|
||||||
|
initial_capital=self.initial_capital,
|
||||||
|
commission=self.commission,
|
||||||
|
slippage=self.slippage,
|
||||||
|
position_size=self.position_size,
|
||||||
|
position_sizer=self.position_sizer,
|
||||||
|
stop_loss=self.stop_loss,
|
||||||
|
max_combinations=self.max_combinations,
|
||||||
|
)
|
||||||
|
|
||||||
if opt_df.empty:
|
opt_df = optimizer.optimize(self.param_grid)
|
||||||
log.warning(f"WF #{wid} sin resultados de optimización")
|
|
||||||
continue
|
|
||||||
|
|
||||||
best_params = optimizer.get_best_params(metric=self.optimizer_metric)
|
if opt_df.empty:
|
||||||
best_train_metric = opt_df[self.optimizer_metric].max()
|
log.warning(f"WF #{wid} sin resultados de optimización")
|
||||||
|
continue
|
||||||
|
|
||||||
|
best_params = optimizer.get_best_params(metric=self.optimizer_metric)
|
||||||
|
best_train_metric = opt_df[self.optimizer_metric].max()
|
||||||
|
|
||||||
# 2️⃣ Backtest TEST (OOS)
|
# 2️⃣ Backtest TEST (OOS)
|
||||||
strategy = self.strategy_class(**best_params)
|
strategy = self.strategy_class(**best_params)
|
||||||
@@ -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,
|
||||||
|
|||||||
@@ -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:
|
||||||
"""
|
"""
|
||||||
|
|||||||
16
src/strategies/registry.py
Normal file
16
src/strategies/registry.py
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
|||||||
@@ -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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -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
@@ -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>
|
||||||
|
|||||||
@@ -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 %}
|
||||||
Reference in New Issue
Block a user