Step 3 y 4 medio preparados. Ha habido una decision es separar en dos step distintos la eleccion de estrategias y luego su optimizacion. A partir de aqui vamos a hacer una refactorizacion quirurgica de los Steps 3 y 4.
Prompt para Char GPT: Estamos trabajando en un Trading Bot con arquitectura backend/frontend separada. Stack: - Backend: FastAPI (Python 3.12) - Frontend: HTML + Vanilla JS + Tabler UI - DB: PostgreSQL - Cache opcional: Redis - Proyecto estructurado bajo /src - Carpeta /reports fuera de src Wizard actual: Step 1 · Data Step 2 · Risk & Stops Step 3 · Strategies (actualmente mezcla validación y optimización) Step 4 · Optimization (renombrado pero no 100% ajustado aún) Decisión arquitectónica ya tomada: - Step 3 será Strategy Validation (parámetros fijos, sin grid) - Step 4 será Parameter Optimization (grid min/max/step) Importante: - Ya he duplicado los archivos para separar Step 3 y Step 4. - No queremos rehacer desde cero. - Queremos hacer una refactorización quirúrgica. - Queremos eliminar lógica de grid del Step 3. - Queremos mantener infraestructura WF, async jobs, ranking y reporting. Objetivo de esta sesión: Refactorizar Step 3 (Validation) de forma limpia y profesional partiendo del código actual. Reglas: - No romper Step 4. - No reescribir todo desde cero. - Simplificar quirúrgicamente. - Mantener coherencia de arquitectura. - Mantener compatibilidad con Step 2 (risk snapshot heredado). - Mantener generación de PDF. - Mantener botón Promote to Optimization. Te adjunto el zip completo de la carpeta src. Analiza la estructura primero. No escribas código todavía. Primero dame: 1. Un diagnóstico estructural. 2. Qué archivos tocar. 3. Qué eliminar. 4. Qué simplificar. 5. Qué mantener. 6. Orden de refactorización seguro. Después empezaremos la refactorización paso a paso. Despues empezaremos la refactorizacion paso a paso.
This commit is contained in:
362
src/calibration/optimization_inspector.py
Normal file
362
src/calibration/optimization_inspector.py
Normal file
@@ -0,0 +1,362 @@
|
||||
# src/calibration/strategies_inspector.py
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Any, Dict, List
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from src.data.storage import StorageManager
|
||||
from src.utils.logger import log
|
||||
|
||||
from src.core.walk_forward import WalkForwardValidator
|
||||
|
||||
from src.risk.stops.fixed_stop import FixedStop
|
||||
from src.risk.stops.trailing_stop import TrailingStop
|
||||
from src.risk.stops.atr_stop import ATRStop
|
||||
|
||||
from src.risk.sizing.percent_risk import PercentRiskSizer
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Strategy registry (con metadata de parámetros)
|
||||
# --------------------------------------------------
|
||||
from src.strategies.moving_average import MovingAverageCrossover
|
||||
from src.strategies.rsi_strategy import RSIStrategy
|
||||
from src.strategies.buy_and_hold import BuyAndHold
|
||||
|
||||
|
||||
STRATEGY_REGISTRY = {
|
||||
"moving_average": {
|
||||
"class": MovingAverageCrossover,
|
||||
"params": ["fast_period", "slow_period"],
|
||||
},
|
||||
"rsi": {
|
||||
"class": RSIStrategy,
|
||||
"params": ["rsi_period", "overbought", "oversold"],
|
||||
},
|
||||
"buy_and_hold": {
|
||||
"class": BuyAndHold,
|
||||
"params": [],
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
# --------------------------------------------------
|
||||
# Helpers
|
||||
# --------------------------------------------------
|
||||
|
||||
def list_available_strategies() -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Devuelve metadata completa para UI.
|
||||
"""
|
||||
out = []
|
||||
|
||||
for sid, entry in STRATEGY_REGISTRY.items():
|
||||
out.append({
|
||||
"strategy_id": sid,
|
||||
"name": entry["class"].__name__,
|
||||
"params": entry["params"],
|
||||
"tags": [], # puedes rellenar más adelante
|
||||
})
|
||||
|
||||
return out
|
||||
|
||||
|
||||
def _build_stop_loss(stop_schema) -> object | None:
|
||||
if stop_schema.type == "fixed":
|
||||
return FixedStop(stop_fraction=float(stop_schema.stop_fraction))
|
||||
if stop_schema.type == "trailing":
|
||||
return TrailingStop(stop_fraction=float(stop_schema.stop_fraction))
|
||||
if stop_schema.type == "atr":
|
||||
return ATRStop(
|
||||
atr_period=int(stop_schema.atr_period),
|
||||
multiplier=float(stop_schema.atr_multiplier),
|
||||
)
|
||||
raise ValueError(f"Unknown stop type: {stop_schema.type}")
|
||||
|
||||
|
||||
def _build_position_sizer(risk_schema) -> PercentRiskSizer:
|
||||
return PercentRiskSizer(risk_fraction=float(risk_schema.risk_fraction))
|
||||
|
||||
|
||||
def _cap_units_by_max_position_fraction(units: float, capital: float, entry_price: float, max_position_fraction: float) -> float:
|
||||
max_units = (capital * max_position_fraction) / entry_price
|
||||
return float(min(units, max_units))
|
||||
|
||||
|
||||
def _accumulate_equity(initial: float, returns_pct: List[float]) -> List[float]:
|
||||
eq = [float(initial)]
|
||||
cur = float(initial)
|
||||
for r in returns_pct:
|
||||
cur *= (1.0 + float(r) / 100.0)
|
||||
eq.append(float(cur))
|
||||
return eq
|
||||
|
||||
|
||||
def _build_param_values(min_v: float, max_v: float, step: float) -> List[float]:
|
||||
min_v = float(min_v)
|
||||
max_v = float(max_v)
|
||||
step = float(step)
|
||||
|
||||
# Valor único si min == max
|
||||
if min_v == max_v:
|
||||
return [min_v]
|
||||
|
||||
# Valor único si step <= 1
|
||||
if step <= 1:
|
||||
return [min_v]
|
||||
|
||||
values = []
|
||||
v = min_v
|
||||
while v <= max_v:
|
||||
values.append(v)
|
||||
v += step
|
||||
|
||||
return values
|
||||
|
||||
# --------------------------------------------------
|
||||
# Main
|
||||
# --------------------------------------------------
|
||||
|
||||
def inspect_strategies_config(
|
||||
*,
|
||||
storage: StorageManager,
|
||||
payload,
|
||||
data_quality: Dict[str, Any],
|
||||
include_series: bool,
|
||||
progress_callback=None,
|
||||
) -> Dict[str, Any]:
|
||||
|
||||
df = storage.load_ohlcv(symbol=payload.symbol, timeframe=payload.timeframe)
|
||||
if df is None or df.empty:
|
||||
return {
|
||||
"valid": False,
|
||||
"status": "fail",
|
||||
"checks": {},
|
||||
"message": "No OHLCV data",
|
||||
"results": [],
|
||||
}
|
||||
|
||||
checks: Dict[str, Any] = {}
|
||||
checks["data_quality"] = {
|
||||
"status": data_quality.get("status", "unknown"),
|
||||
"message": data_quality.get("message", ""),
|
||||
}
|
||||
|
||||
if data_quality.get("status") == "fail":
|
||||
return {
|
||||
"valid": False,
|
||||
"status": "fail",
|
||||
"checks": checks,
|
||||
"message": "Step 1 data quality is FAIL. Strategies cannot be validated.",
|
||||
"results": [],
|
||||
"series": {} if include_series else None,
|
||||
}
|
||||
|
||||
stop_loss = _build_stop_loss(payload.stop)
|
||||
base_sizer = _build_position_sizer(payload.risk)
|
||||
|
||||
train_td = pd.Timedelta(days=int(payload.wf.train_days))
|
||||
test_td = pd.Timedelta(days=int(payload.wf.test_days))
|
||||
step_td = pd.Timedelta(days=int(payload.wf.step_days or payload.wf.test_days))
|
||||
|
||||
overall_status = "ok"
|
||||
results: List[Dict[str, Any]] = []
|
||||
series: Dict[str, Any] = {"strategies": {}} if include_series else {}
|
||||
|
||||
for sel in payload.strategies:
|
||||
|
||||
sid = sel.strategy_id
|
||||
entry = STRATEGY_REGISTRY.get(sid)
|
||||
|
||||
if entry is None:
|
||||
results.append({
|
||||
"strategy_id": sid,
|
||||
"status": "fail",
|
||||
"message": f"Unknown strategy_id: {sid}",
|
||||
"n_windows": 0,
|
||||
"oos_final_equity": payload.account_equity,
|
||||
"oos_total_return_pct": 0.0,
|
||||
"oos_max_dd_worst_pct": 0.0,
|
||||
"degradation_sharpe": None,
|
||||
"windows": [],
|
||||
})
|
||||
overall_status = "fail"
|
||||
continue
|
||||
|
||||
strategy_class = entry["class"]
|
||||
valid_params = set(entry["params"])
|
||||
range_params = set(sel.parameters.keys())
|
||||
|
||||
|
||||
# 🔒 Validación estricta de parámetros
|
||||
if range_params != valid_params:
|
||||
msg = f"Parameter keys {range_params} do not match expected {valid_params}"
|
||||
|
||||
results.append({
|
||||
"strategy_id": sid,
|
||||
"status": "fail",
|
||||
"message": msg,
|
||||
"n_windows": 0,
|
||||
"oos_final_equity": payload.account_equity,
|
||||
"oos_total_return_pct": 0.0,
|
||||
"oos_max_dd_worst_pct": 0.0,
|
||||
"degradation_sharpe": None,
|
||||
"windows": [],
|
||||
})
|
||||
overall_status = "fail"
|
||||
continue
|
||||
|
||||
# --------------------------------------------------
|
||||
# Convert ranges -> param_grid real
|
||||
# --------------------------------------------------
|
||||
param_grid = {}
|
||||
|
||||
for pname, prange in sel.parameters.items():
|
||||
values = _build_param_values(
|
||||
min_v=prange.min,
|
||||
max_v=prange.max,
|
||||
step=prange.step,
|
||||
)
|
||||
param_grid[pname] = values
|
||||
|
||||
# Wrapper sizer
|
||||
class _CappedSizer(type(base_sizer)):
|
||||
def __init__(self, inner):
|
||||
self.inner = inner
|
||||
|
||||
def calculate_size(self, *, capital, entry_price, stop_price=None, max_capital=None, volatility=None):
|
||||
u = self.inner.calculate_size(
|
||||
capital=capital,
|
||||
entry_price=entry_price,
|
||||
stop_price=stop_price,
|
||||
max_capital=max_capital,
|
||||
volatility=volatility,
|
||||
)
|
||||
return _cap_units_by_max_position_fraction(
|
||||
units=float(u),
|
||||
capital=float(capital),
|
||||
entry_price=float(entry_price),
|
||||
max_position_fraction=float(payload.risk.max_position_fraction),
|
||||
)
|
||||
|
||||
capped_sizer = _CappedSizer(base_sizer)
|
||||
|
||||
log.info(f"🧠 Step3 | WF run | strategy={sid}")
|
||||
|
||||
try:
|
||||
wf = WalkForwardValidator(
|
||||
strategy_class=strategy_class,
|
||||
param_grid=param_grid,
|
||||
data=df,
|
||||
train_window=train_td,
|
||||
test_window=test_td,
|
||||
step_size=step_td,
|
||||
initial_capital=float(payload.account_equity),
|
||||
commission=float(payload.commission),
|
||||
slippage=float(payload.slippage),
|
||||
optimizer_metric=str(payload.optimization.optimizer_metric),
|
||||
position_sizer=capped_sizer,
|
||||
stop_loss=stop_loss,
|
||||
max_combinations=int(payload.optimization.max_combinations),
|
||||
progress_callback=progress_callback,
|
||||
)
|
||||
|
||||
wf_res = wf.run()
|
||||
win_df: pd.DataFrame = wf_res["windows"]
|
||||
|
||||
if win_df is None or win_df.empty:
|
||||
status = "fail"
|
||||
msg = "WF produced no valid windows"
|
||||
overall_status = "fail"
|
||||
windows_out = []
|
||||
oos_returns = []
|
||||
else:
|
||||
trades = win_df["trades"].astype(int).tolist()
|
||||
too_few = sum(t < int(payload.optimization.min_trades_test) for t in trades)
|
||||
|
||||
if too_few > 0:
|
||||
status = "warning"
|
||||
msg = f"{too_few} windows below min_trades_test"
|
||||
if overall_status == "ok":
|
||||
overall_status = "warning"
|
||||
else:
|
||||
status = "ok"
|
||||
msg = "WF OK"
|
||||
|
||||
windows_out = []
|
||||
for _, r in win_df.iterrows():
|
||||
windows_out.append({
|
||||
"window": int(r["window"]),
|
||||
"train_start": str(r["train_start"]),
|
||||
"train_end": str(r["train_end"]),
|
||||
"test_start": str(r["test_start"]),
|
||||
"test_end": str(r["test_end"]),
|
||||
"return_pct": float(r["return_pct"]),
|
||||
"sharpe": float(r["sharpe"]),
|
||||
"max_dd_pct": float(r["max_dd_pct"]),
|
||||
"trades": int(r["trades"]),
|
||||
"params": dict(r["params"]) if isinstance(r["params"], dict) else r["params"],
|
||||
})
|
||||
|
||||
oos_returns = win_df["return_pct"].astype(float).tolist()
|
||||
|
||||
eq_curve = _accumulate_equity(float(payload.account_equity), oos_returns)
|
||||
oos_final = float(eq_curve[-1]) if eq_curve else float(payload.account_equity)
|
||||
oos_total_return = (oos_final / float(payload.account_equity) - 1.0) * 100.0
|
||||
oos_max_dd = float(np.min(win_df["max_dd_pct"])) if (win_df is not None and not win_df.empty) else 0.0
|
||||
|
||||
results.append({
|
||||
"strategy_id": sid,
|
||||
"status": status,
|
||||
"message": msg,
|
||||
"n_windows": int(len(windows_out)),
|
||||
"oos_final_equity": oos_final,
|
||||
"oos_total_return_pct": float(oos_total_return),
|
||||
"oos_max_dd_worst_pct": float(oos_max_dd),
|
||||
"degradation_sharpe": None,
|
||||
"windows": windows_out,
|
||||
})
|
||||
|
||||
if include_series:
|
||||
series["strategies"][sid] = {
|
||||
"window_returns_pct": oos_returns,
|
||||
"window_equity": eq_curve,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
log.error(f"❌ Step3 WF error | strategy={sid} | {e}")
|
||||
results.append({
|
||||
"strategy_id": sid,
|
||||
"status": "fail",
|
||||
"message": f"Exception: {e}",
|
||||
"n_windows": 0,
|
||||
"oos_final_equity": float(payload.account_equity),
|
||||
"oos_total_return_pct": 0.0,
|
||||
"oos_max_dd_worst_pct": 0.0,
|
||||
"degradation_sharpe": None,
|
||||
"windows": [],
|
||||
})
|
||||
overall_status = "fail"
|
||||
|
||||
valid = overall_status != "fail"
|
||||
|
||||
human_msg = {
|
||||
"ok": "Strategies validation OK",
|
||||
"warning": "Strategies validation has warnings",
|
||||
"fail": "Strategies validation FAILED",
|
||||
}[overall_status]
|
||||
|
||||
out = {
|
||||
"valid": valid,
|
||||
"status": overall_status,
|
||||
"checks": checks,
|
||||
"message": human_msg,
|
||||
"results": results,
|
||||
}
|
||||
|
||||
if include_series:
|
||||
out["series"] = series
|
||||
|
||||
return out
|
||||
Reference in New Issue
Block a user