preparando step 3 para dejarlo fino
This commit is contained in:
@@ -87,6 +87,103 @@ def _accumulate_equity(initial: float, returns_pct: List[float]) -> List[float]:
|
|||||||
return eq
|
return eq
|
||||||
|
|
||||||
|
|
||||||
|
def _compute_wf_diagnostics(
|
||||||
|
*,
|
||||||
|
window_returns_pct: List[float],
|
||||||
|
window_trades: List[int],
|
||||||
|
window_equity: List[float],
|
||||||
|
test_days: int,
|
||||||
|
rolling_window: int = 3,
|
||||||
|
hist_bins: int = 10,
|
||||||
|
) -> Dict[str, Any]:
|
||||||
|
"""Compute backend-ready diagnostics for Step 3 visualization.
|
||||||
|
|
||||||
|
Notes:
|
||||||
|
- Operates at *WF window* granularity (not daily).
|
||||||
|
- All series are aligned to windows (len == n_windows) except equity/drawdown
|
||||||
|
which include initial point (len == n_windows + 1).
|
||||||
|
"""
|
||||||
|
returns = np.asarray(window_returns_pct, dtype=float)
|
||||||
|
n = int(len(returns))
|
||||||
|
|
||||||
|
mean_r = float(np.mean(returns)) if n else 0.0
|
||||||
|
std_r = float(np.std(returns, ddof=0)) if n else 0.0
|
||||||
|
pos_rate = float(np.mean(returns > 0.0)) if n else 0.0
|
||||||
|
|
||||||
|
# Linear trend of window returns over time (per window index)
|
||||||
|
if n >= 2:
|
||||||
|
x = np.arange(n, dtype=float)
|
||||||
|
slope = float(np.polyfit(x, returns, 1)[0])
|
||||||
|
else:
|
||||||
|
slope = 0.0
|
||||||
|
|
||||||
|
# Rolling Sharpe-like over windows
|
||||||
|
k = int(max(1, rolling_window))
|
||||||
|
roll = [None] * n
|
||||||
|
if n and k > 1:
|
||||||
|
scale = float(np.sqrt(k))
|
||||||
|
for i in range(k - 1, n):
|
||||||
|
seg = returns[i - k + 1 : i + 1]
|
||||||
|
m = float(np.mean(seg))
|
||||||
|
s = float(np.std(seg, ddof=0))
|
||||||
|
roll[i] = float((m / s) * scale) if s > 0 else 0.0
|
||||||
|
elif n and k == 1:
|
||||||
|
roll = [float(r) for r in returns.tolist()]
|
||||||
|
|
||||||
|
# Histogram of window returns
|
||||||
|
if n >= 2:
|
||||||
|
bins = int(max(3, min(hist_bins, n)))
|
||||||
|
counts, edges = np.histogram(returns, bins=bins)
|
||||||
|
hist_counts = [int(c) for c in counts.tolist()]
|
||||||
|
hist_edges = [float(e) for e in edges.tolist()]
|
||||||
|
elif n == 1:
|
||||||
|
hist_counts = [1]
|
||||||
|
hist_edges = [float(returns[0] - 1.0), float(returns[0] + 1.0)]
|
||||||
|
else:
|
||||||
|
hist_counts = []
|
||||||
|
hist_edges = []
|
||||||
|
|
||||||
|
# Drawdown series from equity
|
||||||
|
eq = [float(x) for x in (window_equity or [])]
|
||||||
|
dd = []
|
||||||
|
peak = None
|
||||||
|
for v in eq:
|
||||||
|
if peak is None or v > peak:
|
||||||
|
peak = v
|
||||||
|
dd.append(float((v / peak - 1.0) * 100.0) if peak and peak > 0 else 0.0)
|
||||||
|
|
||||||
|
# Trades density
|
||||||
|
td = int(test_days) if int(test_days) > 0 else 1
|
||||||
|
t_int = [int(t) for t in (window_trades or [])]
|
||||||
|
trades_per_day = [float(t) / float(td) for t in t_int]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"stability": {
|
||||||
|
"n_windows": n,
|
||||||
|
"mean_return_pct": mean_r,
|
||||||
|
"std_return_pct": std_r,
|
||||||
|
"positive_window_rate": pos_rate,
|
||||||
|
"return_slope_per_window": slope,
|
||||||
|
},
|
||||||
|
"rolling": {
|
||||||
|
"rolling_window": k,
|
||||||
|
"rolling_sharpe_like": roll,
|
||||||
|
},
|
||||||
|
"distribution": {
|
||||||
|
"hist_bin_edges": hist_edges,
|
||||||
|
"hist_counts": hist_counts,
|
||||||
|
},
|
||||||
|
"drawdown": {
|
||||||
|
"equity": eq,
|
||||||
|
"drawdown_pct": dd,
|
||||||
|
},
|
||||||
|
"trades": {
|
||||||
|
"trades_per_window": t_int,
|
||||||
|
"trades_per_day": trades_per_day,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
# Main
|
# Main
|
||||||
# --------------------------------------------------
|
# --------------------------------------------------
|
||||||
@@ -289,6 +386,7 @@ def inspect_strategies_config(
|
|||||||
"oos_total_return_pct": 0.0,
|
"oos_total_return_pct": 0.0,
|
||||||
"oos_max_dd_worst_pct": 0.0,
|
"oos_max_dd_worst_pct": 0.0,
|
||||||
"degradation_sharpe": None,
|
"degradation_sharpe": None,
|
||||||
|
"diagnostics": _compute_wf_diagnostics(window_returns_pct=[], window_trades=[], window_equity=[float(payload.account_equity)], test_days=int(payload.wf.test_days)),
|
||||||
"windows": [],
|
"windows": [],
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -298,6 +396,7 @@ def inspect_strategies_config(
|
|||||||
"window_returns_pct": [],
|
"window_returns_pct": [],
|
||||||
"window_equity": [float(payload.account_equity)],
|
"window_equity": [float(payload.account_equity)],
|
||||||
"window_trades": [],
|
"window_trades": [],
|
||||||
|
"diagnostics": _compute_wf_diagnostics(window_returns_pct=[], window_trades=[], window_equity=[float(payload.account_equity)], test_days=int(payload.wf.test_days)),
|
||||||
}
|
}
|
||||||
|
|
||||||
if overall_status == "ok":
|
if overall_status == "ok":
|
||||||
@@ -348,13 +447,24 @@ def inspect_strategies_config(
|
|||||||
"params": dict(r["params"]) if isinstance(r["params"], dict) else r["params"],
|
"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()
|
||||||
|
|
||||||
eq_curve = _accumulate_equity(float(payload.account_equity), oos_returns)
|
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_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_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
|
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
|
||||||
|
|
||||||
|
diagnostics = _compute_wf_diagnostics(
|
||||||
|
window_returns_pct=oos_returns,
|
||||||
|
window_trades=win_df["trades"].astype(int).tolist(),
|
||||||
|
window_equity=eq_curve,
|
||||||
|
test_days=int(payload.wf.test_days),
|
||||||
|
rolling_window=3,
|
||||||
|
hist_bins=10,
|
||||||
|
)
|
||||||
|
|
||||||
|
# keep worst-window DD also at top-level for backwards compatibility
|
||||||
|
diagnostics["drawdown"]["worst_window_dd_pct"] = float(oos_max_dd)
|
||||||
|
|
||||||
results.append({
|
results.append({
|
||||||
"strategy_id": sid,
|
"strategy_id": sid,
|
||||||
"status": status,
|
"status": status,
|
||||||
@@ -367,6 +477,7 @@ def inspect_strategies_config(
|
|||||||
"oos_total_return_pct": float(oos_total_return),
|
"oos_total_return_pct": float(oos_total_return),
|
||||||
"oos_max_dd_worst_pct": float(oos_max_dd),
|
"oos_max_dd_worst_pct": float(oos_max_dd),
|
||||||
"degradation_sharpe": None,
|
"degradation_sharpe": None,
|
||||||
|
"diagnostics": diagnostics,
|
||||||
"windows": windows_out,
|
"windows": windows_out,
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -375,6 +486,7 @@ def inspect_strategies_config(
|
|||||||
"window_returns_pct": oos_returns,
|
"window_returns_pct": oos_returns,
|
||||||
"window_equity": eq_curve,
|
"window_equity": eq_curve,
|
||||||
"window_trades": win_df["trades"].tolist(),
|
"window_trades": win_df["trades"].tolist(),
|
||||||
|
"diagnostics": diagnostics,
|
||||||
}
|
}
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
|
|||||||
@@ -1,38 +0,0 @@
|
|||||||
# src/web/api/deps.py
|
|
||||||
from functools import lru_cache
|
|
||||||
|
|
||||||
from src.paper.state_store import StateStore
|
|
||||||
|
|
||||||
from .settings import settings
|
|
||||||
from .providers.base import StateProvider
|
|
||||||
from .providers.sqlite import SQLiteStateProvider
|
|
||||||
from .providers.mock import MockStateProvider
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
# StateStore singleton (solo para SQLite provider)
|
|
||||||
# --------------------------------------------------
|
|
||||||
@lru_cache(maxsize=1)
|
|
||||||
def get_store() -> StateStore:
|
|
||||||
"""
|
|
||||||
Singleton del StateStore.
|
|
||||||
La API es read-only, así que es seguro compartir conexión.
|
|
||||||
"""
|
|
||||||
return StateStore(settings.state_db_path)
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
# Provider selector
|
|
||||||
# --------------------------------------------------
|
|
||||||
def get_provider() -> StateProvider:
|
|
||||||
"""
|
|
||||||
Devuelve el provider adecuado según configuración.
|
|
||||||
|
|
||||||
- mock_mode = True → MockStateProvider
|
|
||||||
- mock_mode = False → SQLiteStateProvider (real)
|
|
||||||
"""
|
|
||||||
if settings.mock_mode:
|
|
||||||
return MockStateProvider()
|
|
||||||
|
|
||||||
store = get_store()
|
|
||||||
return SQLiteStateProvider(store)
|
|
||||||
@@ -1,80 +0,0 @@
|
|||||||
|
|
||||||
# src/web/api/main.py
|
|
||||||
from fastapi import FastAPI, Request
|
|
||||||
from fastapi.middleware.cors import CORSMiddleware
|
|
||||||
from fastapi.staticfiles import StaticFiles
|
|
||||||
from fastapi.templating import Jinja2Templates
|
|
||||||
|
|
||||||
from .settings import settings
|
|
||||||
|
|
||||||
# Routers
|
|
||||||
from .routers.health import router as health_router
|
|
||||||
from .routers.bot import router as bot_router
|
|
||||||
from .routers.equity import router as equity_router
|
|
||||||
from .routers.trades import router as trades_router
|
|
||||||
from .routers.metrics import router as metrics_router
|
|
||||||
from .routers.events import router as events_router
|
|
||||||
from .routers.positions import router as positions_router
|
|
||||||
|
|
||||||
|
|
||||||
def create_app() -> FastAPI:
|
|
||||||
app = FastAPI(
|
|
||||||
title=settings.api_title,
|
|
||||||
version=settings.api_version,
|
|
||||||
)
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
# CORS (solo lectura)
|
|
||||||
# --------------------------------------------------
|
|
||||||
app.add_middleware(
|
|
||||||
CORSMiddleware,
|
|
||||||
allow_origins=[
|
|
||||||
"http://localhost",
|
|
||||||
"http://127.0.0.1",
|
|
||||||
"http://localhost:3000",
|
|
||||||
"http://127.0.0.1:3000",
|
|
||||||
],
|
|
||||||
allow_credentials=True,
|
|
||||||
allow_methods=["GET"],
|
|
||||||
allow_headers=["*"],
|
|
||||||
)
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
# Static files (UI)
|
|
||||||
# --------------------------------------------------
|
|
||||||
app.mount(
|
|
||||||
"/static",
|
|
||||||
StaticFiles(directory="src/web/ui/static"),
|
|
||||||
name="static",
|
|
||||||
)
|
|
||||||
|
|
||||||
templates = Jinja2Templates(directory="src/web/ui/templates")
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
# UI routes
|
|
||||||
# --------------------------------------------------
|
|
||||||
@app.get("/")
|
|
||||||
def dashboard(request: Request):
|
|
||||||
return templates.TemplateResponse(
|
|
||||||
"dashboard.html",
|
|
||||||
{"request": request},
|
|
||||||
)
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
# API routers (versionados)
|
|
||||||
# --------------------------------------------------
|
|
||||||
api_prefix = settings.api_prefix
|
|
||||||
|
|
||||||
app.include_router(health_router, prefix=api_prefix)
|
|
||||||
app.include_router(bot_router, prefix=api_prefix)
|
|
||||||
app.include_router(equity_router, prefix=api_prefix)
|
|
||||||
app.include_router(trades_router, prefix=api_prefix)
|
|
||||||
app.include_router(metrics_router, prefix=api_prefix)
|
|
||||||
app.include_router(events_router, prefix=api_prefix)
|
|
||||||
# app.include_router(positions_router, prefix=api_prefix)
|
|
||||||
|
|
||||||
return app
|
|
||||||
|
|
||||||
|
|
||||||
# Instancia ASGI
|
|
||||||
app = create_app()
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
# src/web/api/providers/base.py
|
|
||||||
from abc import ABC, abstractmethod
|
|
||||||
from typing import Optional, Dict, Any, List
|
|
||||||
|
|
||||||
|
|
||||||
class StateProvider(ABC):
|
|
||||||
"""
|
|
||||||
Contrato que define qué datos expone el bot a la API.
|
|
||||||
La UI y los routers dependen SOLO de esto.
|
|
||||||
"""
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# Core state
|
|
||||||
# -----------------------------
|
|
||||||
@abstractmethod
|
|
||||||
def get_broker(self) -> Optional[Dict[str, Any]]:
|
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_loop(self) -> Optional[Dict[str, Any]]:
|
|
||||||
...
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def get_metrics(self) -> Optional[Dict[str, Any]]:
|
|
||||||
...
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# Trades
|
|
||||||
# -----------------------------
|
|
||||||
@abstractmethod
|
|
||||||
def list_trades(
|
|
||||||
self,
|
|
||||||
limit: int = 200,
|
|
||||||
symbol: Optional[str] = None,
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
...
|
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
# src/web/api/providers/mock.py
|
|
||||||
import random
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
from typing import Optional, Dict, Any, List
|
|
||||||
|
|
||||||
from .base import StateProvider
|
|
||||||
|
|
||||||
|
|
||||||
class MockStateProvider(StateProvider):
|
|
||||||
"""
|
|
||||||
Provider mock para desarrollo de UI sin bot corriendo.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def _now(self) -> str:
|
|
||||||
return datetime.now(timezone.utc).isoformat()
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# Core state
|
|
||||||
# -----------------------------
|
|
||||||
def get_broker(self) -> Optional[Dict[str, Any]]:
|
|
||||||
equity = 10_000 + random.uniform(-300, 600)
|
|
||||||
cash = 4_000 + random.uniform(-100, 100)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"initial_cash": 10_000,
|
|
||||||
"cash": cash,
|
|
||||||
"equity": equity,
|
|
||||||
"realized_pnl": equity - 10_000,
|
|
||||||
"positions": {},
|
|
||||||
"last_price": {"BTC/USDT": 42_000 + random.uniform(-200, 200)},
|
|
||||||
"trades_count": random.randint(5, 25),
|
|
||||||
"updated_at": self._now(),
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_loop(self) -> Optional[Dict[str, Any]]:
|
|
||||||
curve = [10_000 + i * 3 for i in range(120)]
|
|
||||||
return {
|
|
||||||
"last_ts": self._now(),
|
|
||||||
"equity_curve": curve,
|
|
||||||
"equity_timestamps": [],
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_metrics(self) -> Optional[Dict[str, Any]]:
|
|
||||||
return {
|
|
||||||
"cagr": 0.21,
|
|
||||||
"max_drawdown": -0.14,
|
|
||||||
"calmar_ratio": 1.5,
|
|
||||||
"volatility": 0.28,
|
|
||||||
"time_in_drawdown": 0.39,
|
|
||||||
"ulcer_index": 7.4,
|
|
||||||
}
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# Trades
|
|
||||||
# -----------------------------
|
|
||||||
def list_trades(
|
|
||||||
self,
|
|
||||||
limit: int = 200,
|
|
||||||
symbol: Optional[str] = None,
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
return []
|
|
||||||
@@ -1,72 +0,0 @@
|
|||||||
# src/web/api/providers/sqlite.py
|
|
||||||
from typing import Optional, Dict, Any, List
|
|
||||||
from datetime import datetime, timedelta
|
|
||||||
|
|
||||||
from src.paper.state_store import StateStore
|
|
||||||
from .base import StateProvider
|
|
||||||
|
|
||||||
|
|
||||||
class SQLiteStateProvider(StateProvider):
|
|
||||||
"""
|
|
||||||
Provider real que lee del StateStore (SQLite).
|
|
||||||
Read-only.
|
|
||||||
"""
|
|
||||||
|
|
||||||
def __init__(self, store: StateStore):
|
|
||||||
self.store = store
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# Core state
|
|
||||||
# -----------------------------
|
|
||||||
def get_broker(self) -> Optional[Dict[str, Any]]:
|
|
||||||
return self.store.load_broker_snapshot()
|
|
||||||
|
|
||||||
def get_loop(self) -> Optional[Dict[str, Any]]:
|
|
||||||
return self.store.load_loop_state()
|
|
||||||
|
|
||||||
def get_metrics(self) -> Optional[Dict[str, Any]]:
|
|
||||||
# opcional, por ahora puede devolver None
|
|
||||||
return None
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# Equity
|
|
||||||
# -----------------------------
|
|
||||||
def get_equity_state(self) -> Dict[str, Any]:
|
|
||||||
"""
|
|
||||||
Estado actual de equity (último snapshot).
|
|
||||||
"""
|
|
||||||
broker = self.store.load_broker_snapshot() or {}
|
|
||||||
|
|
||||||
return {
|
|
||||||
"equity": broker.get("equity"),
|
|
||||||
"cash": broker.get("cash"),
|
|
||||||
"updated_at": broker.get("updated_at"),
|
|
||||||
}
|
|
||||||
|
|
||||||
def get_equity_curve(self, range: str) -> Dict[str, List]:
|
|
||||||
"""
|
|
||||||
Serie temporal de equity filtrada por rango.
|
|
||||||
"""
|
|
||||||
now = datetime.utcnow()
|
|
||||||
|
|
||||||
from_ts = None
|
|
||||||
if range == "1h":
|
|
||||||
from_ts = now - timedelta(hours=1)
|
|
||||||
elif range == "6h":
|
|
||||||
from_ts = now - timedelta(hours=6)
|
|
||||||
elif range == "24h":
|
|
||||||
from_ts = now - timedelta(hours=24)
|
|
||||||
elif range == "all":
|
|
||||||
from_ts = None
|
|
||||||
|
|
||||||
return self.store.load_equity_curve(from_ts=from_ts)
|
|
||||||
|
|
||||||
# -----------------------------
|
|
||||||
# Trades
|
|
||||||
# -----------------------------
|
|
||||||
def list_trades(
|
|
||||||
self,
|
|
||||||
limit: int = 200,
|
|
||||||
symbol: Optional[str] = None,
|
|
||||||
) -> List[Dict[str, Any]]:
|
|
||||||
return self.store.load_trades(limit=limit)
|
|
||||||
@@ -1,39 +0,0 @@
|
|||||||
# src/web/api/routers/bot.py
|
|
||||||
from fastapi import APIRouter, Depends
|
|
||||||
from datetime import datetime, timezone
|
|
||||||
|
|
||||||
from src.web.api.deps import get_provider
|
|
||||||
from src.web.api.providers.base import StateProvider
|
|
||||||
from src.web.api.settings import settings
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/bot", tags=["bot"])
|
|
||||||
|
|
||||||
|
|
||||||
def _parse_iso(ts: str) -> datetime:
|
|
||||||
return datetime.fromisoformat(ts.replace("Z", "+00:00"))
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/status")
|
|
||||||
def bot_status(provider: StateProvider = Depends(get_provider)):
|
|
||||||
broker = provider.get_broker()
|
|
||||||
loop = provider.get_loop()
|
|
||||||
|
|
||||||
if not broker:
|
|
||||||
return {
|
|
||||||
"state": "INIT",
|
|
||||||
"heartbeat_ts": None,
|
|
||||||
"last_ts": loop.get("last_ts") if loop else None,
|
|
||||||
}
|
|
||||||
|
|
||||||
heartbeat_ts = broker.get("updated_at")
|
|
||||||
age = (
|
|
||||||
datetime.now(timezone.utc) - _parse_iso(heartbeat_ts)
|
|
||||||
).total_seconds()
|
|
||||||
|
|
||||||
state = "RUNNING" if age <= settings.heartbeat_stale_seconds else "ERROR"
|
|
||||||
|
|
||||||
return {
|
|
||||||
"state": state,
|
|
||||||
"heartbeat_ts": heartbeat_ts,
|
|
||||||
"last_ts": loop.get("last_ts") if loop else None,
|
|
||||||
}
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
# src/web/api/routers/equity.py
|
|
||||||
|
|
||||||
from fastapi import APIRouter, Depends, Query
|
|
||||||
from src.web.api.v1.deps import get_provider
|
|
||||||
from src.web.api.v1.providers.base import StateProvider
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/equity", tags=["equity"])
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
# Equity state (KPIs)
|
|
||||||
# --------------------------------------------------
|
|
||||||
@router.get("/state")
|
|
||||||
def equity_state(provider: StateProvider = Depends(get_provider)):
|
|
||||||
broker = provider.get_broker() or {}
|
|
||||||
|
|
||||||
equity = broker.get("equity")
|
|
||||||
cash = broker.get("cash")
|
|
||||||
realized_pnl = broker.get("realized_pnl")
|
|
||||||
updated_at = broker.get("updated_at")
|
|
||||||
|
|
||||||
return {
|
|
||||||
"cash": cash,
|
|
||||||
"equity": equity,
|
|
||||||
"realized_pnl": realized_pnl,
|
|
||||||
"updated_at": updated_at,
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
# Equity curve (TIME SERIES)
|
|
||||||
# --------------------------------------------------
|
|
||||||
@router.get("/curve")
|
|
||||||
def equity_curve(
|
|
||||||
range: str = Query("all", pattern="^(1h|6h|24h|all)$"),
|
|
||||||
provider: StateProvider = Depends(get_provider),
|
|
||||||
):
|
|
||||||
"""
|
|
||||||
Devuelve la equity curve filtrada por rango temporal.
|
|
||||||
"""
|
|
||||||
return provider.get_equity_curve(range=range)
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
# src/web/api/routers/events.py
|
|
||||||
from fastapi import APIRouter, Query
|
|
||||||
from pathlib import Path
|
|
||||||
from collections import deque
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/events", tags=["events"])
|
|
||||||
|
|
||||||
|
|
||||||
def _tail_file(path: Path, n: int) -> list[str]:
|
|
||||||
if not path.exists():
|
|
||||||
return []
|
|
||||||
dq = deque(maxlen=n)
|
|
||||||
with path.open("r", encoding="utf-8", errors="ignore") as f:
|
|
||||||
for line in f:
|
|
||||||
dq.append(line.rstrip())
|
|
||||||
return list(dq)
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
|
||||||
def events(
|
|
||||||
limit: int = Query(200, ge=1, le=2000),
|
|
||||||
kind: str = Query("trading", pattern="^(trading|errors)$"),
|
|
||||||
):
|
|
||||||
logs_dir = Path("logs")
|
|
||||||
pattern = "trading_bot_*.log" if kind == "trading" else "errors_*.log"
|
|
||||||
|
|
||||||
files = sorted(
|
|
||||||
logs_dir.glob(pattern),
|
|
||||||
key=lambda p: p.stat().st_mtime,
|
|
||||||
reverse=True,
|
|
||||||
)
|
|
||||||
|
|
||||||
if not files:
|
|
||||||
return {"items": []}
|
|
||||||
|
|
||||||
lines = _tail_file(files[0], limit)
|
|
||||||
return {
|
|
||||||
"items": lines,
|
|
||||||
"file": files[0].name,
|
|
||||||
}
|
|
||||||
@@ -1,20 +0,0 @@
|
|||||||
# src/web/api/routers/heallth.py
|
|
||||||
from fastapi import APIRouter, Depends
|
|
||||||
from src.web.api.deps import get_provider
|
|
||||||
from src.web.api.providers.base import StateProvider
|
|
||||||
|
|
||||||
router = APIRouter(tags=["health"])
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("/health")
|
|
||||||
def health(provider: StateProvider = Depends(get_provider)):
|
|
||||||
broker = provider.get_broker()
|
|
||||||
loop = provider.get_loop()
|
|
||||||
metrics = provider.get_metrics()
|
|
||||||
|
|
||||||
return {
|
|
||||||
"ok": True,
|
|
||||||
"has_broker": broker is not None,
|
|
||||||
"has_loop": loop is not None,
|
|
||||||
"has_metrics": metrics is not None,
|
|
||||||
}
|
|
||||||
@@ -1,11 +0,0 @@
|
|||||||
# src/web/api/routers/metrics.py
|
|
||||||
from fastapi import APIRouter, Depends
|
|
||||||
from src.web.api.deps import get_provider
|
|
||||||
from src.web.api.providers.base import StateProvider
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/metrics", tags=["metrics"])
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
|
||||||
def metrics(provider: StateProvider = Depends(get_provider)):
|
|
||||||
return provider.get_metrics() or {}
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
# src/web/api/routers/positions.py
|
|
||||||
# src/web/api/routers/positions.py
|
|
||||||
from fastapi import APIRouter, Depends
|
|
||||||
|
|
||||||
from src.web.api.deps import get_provider
|
|
||||||
from src.web.api.providers.base import StateProvider
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/positions", tags=["positions"])
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
|
||||||
def positions(provider: StateProvider = Depends(get_provider)):
|
|
||||||
broker = provider.get_broker() or {}
|
|
||||||
positions = broker.get("positions") or {}
|
|
||||||
|
|
||||||
out = []
|
|
||||||
for _, p in positions.items():
|
|
||||||
qty = float(p.get("qty", 0.0))
|
|
||||||
if qty > 0:
|
|
||||||
out.append(p)
|
|
||||||
|
|
||||||
return {
|
|
||||||
"items": out,
|
|
||||||
"updated_at": broker.get("updated_at"),
|
|
||||||
}
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
# src/web/api/routers/trades.py
|
|
||||||
from fastapi import APIRouter, Depends, Query
|
|
||||||
from typing import Optional
|
|
||||||
|
|
||||||
from src.web.api.deps import get_provider
|
|
||||||
from src.web.api.providers.base import StateProvider
|
|
||||||
|
|
||||||
router = APIRouter(prefix="/trades", tags=["trades"])
|
|
||||||
|
|
||||||
|
|
||||||
@router.get("")
|
|
||||||
def list_trades(
|
|
||||||
limit: int = Query(200, ge=1, le=5000),
|
|
||||||
symbol: Optional[str] = None,
|
|
||||||
provider: StateProvider = Depends(get_provider),
|
|
||||||
):
|
|
||||||
trades = provider.list_trades(limit=limit, symbol=symbol)
|
|
||||||
return {
|
|
||||||
"items": trades,
|
|
||||||
"count": len(trades),
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
# src/web/api/settings.py
|
|
||||||
from pydantic import BaseModel
|
|
||||||
from pathlib import Path
|
|
||||||
import os
|
|
||||||
|
|
||||||
PROJECT_ROOT = Path(__file__).resolve().parents[3]
|
|
||||||
# src/web/api/settings.py → api → web → src → trading-bot
|
|
||||||
|
|
||||||
|
|
||||||
class Settings(BaseModel):
|
|
||||||
# --------------------------------------------------
|
|
||||||
# API
|
|
||||||
# --------------------------------------------------
|
|
||||||
api_prefix: str = "/api/v1"
|
|
||||||
api_title: str = "Trading Bot API"
|
|
||||||
api_version: str = "1.0.0"
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
# Data source
|
|
||||||
# --------------------------------------------------
|
|
||||||
state_db_path: Path = PROJECT_ROOT / "data/paper_trading/state.db"
|
|
||||||
|
|
||||||
# --------------------------------------------------
|
|
||||||
# Runtime behaviour
|
|
||||||
# --------------------------------------------------
|
|
||||||
heartbeat_stale_seconds: int = 180 # si no hay heartbeat → ERROR
|
|
||||||
mock_mode: bool = False
|
|
||||||
|
|
||||||
|
|
||||||
def load_settings() -> Settings:
|
|
||||||
"""
|
|
||||||
Carga settings desde ENV sin romper defaults.
|
|
||||||
"""
|
|
||||||
return Settings(
|
|
||||||
state_db_path=Path(
|
|
||||||
os.getenv("STATE_DB_PATH",
|
|
||||||
PROJECT_ROOT / "data/paper_trading/state.db"
|
|
||||||
)
|
|
||||||
),
|
|
||||||
heartbeat_stale_seconds=int(
|
|
||||||
os.getenv("HEARTBEAT_STALE_SECONDS", 180)
|
|
||||||
),
|
|
||||||
mock_mode=os.getenv("MOCK_MODE", "false").lower() == "true",
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
settings = load_settings()
|
|
||||||
@@ -68,6 +68,8 @@ class StrategyRunResultSchema(BaseModel):
|
|||||||
oos_max_dd_worst_pct: float
|
oos_max_dd_worst_pct: float
|
||||||
degradation_sharpe: Optional[float] = None
|
degradation_sharpe: Optional[float] = None
|
||||||
|
|
||||||
|
diagnostics: Optional[Dict[str, Any]] = None
|
||||||
|
|
||||||
windows: List[WindowRowSchema]
|
windows: List[WindowRowSchema]
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
9
src/web/ui/v1/static/css/tabler.min.css
vendored
9
src/web/ui/v1/static/css/tabler.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -1,7 +0,0 @@
|
|||||||
const API_BASE = "/api/v1";
|
|
||||||
|
|
||||||
async function apiGet(path) {
|
|
||||||
const res = await fetch(`${API_BASE}${path}`);
|
|
||||||
if (!res.ok) throw new Error(res.statusText);
|
|
||||||
return res.json();
|
|
||||||
}
|
|
||||||
@@ -1,306 +0,0 @@
|
|||||||
let equityChart = null;
|
|
||||||
let showDrawdown = true;
|
|
||||||
let currentRange = "all";
|
|
||||||
|
|
||||||
// --------------------------------------------------
|
|
||||||
// STATUS
|
|
||||||
// --------------------------------------------------
|
|
||||||
async function updateStatus() {
|
|
||||||
const data = await apiGet("/bot/status");
|
|
||||||
const el = document.getElementById("bot-status");
|
|
||||||
|
|
||||||
el.textContent = data.state;
|
|
||||||
el.className =
|
|
||||||
"ms-auto badge " +
|
|
||||||
(data.state === "RUNNING"
|
|
||||||
? "bg-green"
|
|
||||||
: data.state === "ERROR"
|
|
||||||
? "bg-red"
|
|
||||||
: "bg-secondary");
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------------------------------
|
|
||||||
// KPI
|
|
||||||
// --------------------------------------------------
|
|
||||||
async function updateEquity() {
|
|
||||||
const data = await apiGet("/equity/state");
|
|
||||||
document.getElementById("kpi-equity").textContent =
|
|
||||||
data.equity?.toFixed(2) ?? "—";
|
|
||||||
document.getElementById("kpi-pnl").textContent =
|
|
||||||
data.realized_pnl?.toFixed(2) ?? "—";
|
|
||||||
}
|
|
||||||
|
|
||||||
async function fetchTrades() {
|
|
||||||
const data = await apiGet("/trades?limit=500");
|
|
||||||
return data.items || [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------------------------------
|
|
||||||
// MAIN CHART
|
|
||||||
// --------------------------------------------------
|
|
||||||
async function updateCurve() {
|
|
||||||
const data = await apiGet(`/equity/curve?range=${currentRange}`);
|
|
||||||
|
|
||||||
const labels = data.timestamps;
|
|
||||||
const equity = data.equity;
|
|
||||||
const cash = data.cash;
|
|
||||||
|
|
||||||
// -----------------------------
|
|
||||||
// Max Equity (for drawdown)
|
|
||||||
// -----------------------------
|
|
||||||
const maxEquityCurve = [];
|
|
||||||
let runningMax = -Infinity;
|
|
||||||
|
|
||||||
for (let i = 0; i < equity.length; i++) {
|
|
||||||
runningMax = Math.max(runningMax, equity[i]);
|
|
||||||
maxEquityCurve.push(runningMax);
|
|
||||||
}
|
|
||||||
|
|
||||||
// -----------------------------
|
|
||||||
// Max Drawdown KPI
|
|
||||||
// -----------------------------
|
|
||||||
let maxDD = 0;
|
|
||||||
for (let i = 0; i < equity.length; i++) {
|
|
||||||
const dd = (equity[i] / maxEquityCurve[i] - 1) * 100;
|
|
||||||
maxDD = Math.min(maxDD, dd);
|
|
||||||
}
|
|
||||||
|
|
||||||
const elDD = document.getElementById("kpi-max-dd");
|
|
||||||
if (elDD) elDD.textContent = `${maxDD.toFixed(2)} %`;
|
|
||||||
|
|
||||||
// -----------------------------
|
|
||||||
// Trades → markers
|
|
||||||
// -----------------------------
|
|
||||||
const trades = await fetchTrades();
|
|
||||||
const buyPoints = [];
|
|
||||||
const sellPoints = [];
|
|
||||||
|
|
||||||
const minEquity = Math.min(...equity);
|
|
||||||
const maxEquity = Math.max(...equity);
|
|
||||||
const offset = Math.max((maxEquity - minEquity) * 0.05, 350);
|
|
||||||
|
|
||||||
trades.forEach(t => {
|
|
||||||
if (!t.timestamp || !t.side) return;
|
|
||||||
|
|
||||||
const tradeTs = new Date(t.timestamp).getTime();
|
|
||||||
let idx = labels.length - 1;
|
|
||||||
|
|
||||||
for (let i = labels.length - 1; i >= 0; i--) {
|
|
||||||
if (new Date(labels[i]).getTime() <= tradeTs) {
|
|
||||||
idx = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const y = equity[idx];
|
|
||||||
if (y == null) return;
|
|
||||||
|
|
||||||
if (t.side === "BUY") {
|
|
||||||
buyPoints.push({ x: labels[idx], y: y - offset, trade: t });
|
|
||||||
}
|
|
||||||
|
|
||||||
if (t.side === "SELL" || t.side === "CLOSE") {
|
|
||||||
sellPoints.push({ x: labels[idx], y: y + offset, trade: t });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
// --------------------------------------------------
|
|
||||||
// INIT CHART
|
|
||||||
// --------------------------------------------------
|
|
||||||
if (!equityChart) {
|
|
||||||
const ctx = document.getElementById("equityChart").getContext("2d");
|
|
||||||
|
|
||||||
equityChart = new Chart(ctx, {
|
|
||||||
type: "line",
|
|
||||||
data: {
|
|
||||||
labels,
|
|
||||||
datasets: [
|
|
||||||
{
|
|
||||||
label: "Max Equity",
|
|
||||||
data: maxEquityCurve,
|
|
||||||
borderColor: "rgba(0,0,0,0)",
|
|
||||||
pointRadius: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Equity",
|
|
||||||
data: equity,
|
|
||||||
borderColor: "#206bc4",
|
|
||||||
backgroundColor: "rgba(214,57,57,0.15)",
|
|
||||||
pointRadius: 0,
|
|
||||||
fill: {
|
|
||||||
target: 0,
|
|
||||||
above: "rgba(0,0,0,0)",
|
|
||||||
below: "rgba(214,57,57,0.15)"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
{
|
|
||||||
label: "Cash",
|
|
||||||
data: cash,
|
|
||||||
borderColor: "#2fb344",
|
|
||||||
borderDash: [5, 5],
|
|
||||||
pointRadius: 0
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "scatter",
|
|
||||||
label: "BUY",
|
|
||||||
data: buyPoints,
|
|
||||||
pointStyle: "triangle",
|
|
||||||
pointRotation: 0,
|
|
||||||
pointRadius: 6,
|
|
||||||
backgroundColor: "#2fb344"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
type: "scatter",
|
|
||||||
label: "SELL",
|
|
||||||
data: sellPoints,
|
|
||||||
pointStyle: "triangle",
|
|
||||||
pointRotation: 180,
|
|
||||||
pointRadius: 6,
|
|
||||||
backgroundColor: "#d63939"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
options: {
|
|
||||||
responsive: true,
|
|
||||||
animation: false,
|
|
||||||
|
|
||||||
// -----------------------------
|
|
||||||
// SOLUCIÓN 1: TIME SCALE
|
|
||||||
// -----------------------------
|
|
||||||
scales: {
|
|
||||||
x: {
|
|
||||||
type: "time",
|
|
||||||
time: {
|
|
||||||
tooltipFormat: "yyyy-MM-dd HH:mm",
|
|
||||||
displayFormats: {
|
|
||||||
minute: "HH:mm",
|
|
||||||
hour: "HH:mm",
|
|
||||||
day: "MMM dd"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// -----------------------------
|
|
||||||
// SOLUCIÓN 2: ZOOM + PAN
|
|
||||||
// -----------------------------
|
|
||||||
plugins: {
|
|
||||||
zoom: {
|
|
||||||
limits: {
|
|
||||||
x: { min: "original", max: "original" }
|
|
||||||
},
|
|
||||||
pan: {
|
|
||||||
enabled: true,
|
|
||||||
mode: "x",
|
|
||||||
modifierKey: "ctrl"
|
|
||||||
},
|
|
||||||
zoom: {
|
|
||||||
wheel: { enabled: true },
|
|
||||||
pinch: { enabled: true },
|
|
||||||
mode: "x"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
// -----------------------------
|
|
||||||
// SOLUCIÓN 3: DECIMATION
|
|
||||||
// -----------------------------
|
|
||||||
decimation: {
|
|
||||||
enabled: true,
|
|
||||||
algorithm: "lttb",
|
|
||||||
samples: 500
|
|
||||||
},
|
|
||||||
|
|
||||||
// -----------------------------
|
|
||||||
// LEYENDA
|
|
||||||
// -----------------------------
|
|
||||||
legend: {
|
|
||||||
display: true,
|
|
||||||
labels: {
|
|
||||||
usePointStyle: true,
|
|
||||||
generateLabels(chart) {
|
|
||||||
return chart.data.datasets
|
|
||||||
// ❌ fuera Max Equity
|
|
||||||
.map((ds, i) => ({ ds, i }))
|
|
||||||
.filter(({ ds }) => ds.label !== "Max Equity")
|
|
||||||
.map(({ ds, i }) => {
|
|
||||||
const isScatter = ds.type === "scatter";
|
|
||||||
|
|
||||||
return {
|
|
||||||
text: ds.label,
|
|
||||||
datasetIndex: i,
|
|
||||||
hidden: !chart.isDatasetVisible(i),
|
|
||||||
|
|
||||||
// 🎨 colores reales del dataset
|
|
||||||
fillStyle: isScatter ? ds.backgroundColor : ds.borderColor,
|
|
||||||
strokeStyle: ds.borderColor,
|
|
||||||
|
|
||||||
// 📏 línea vs punto
|
|
||||||
lineWidth: isScatter ? 0 : 3,
|
|
||||||
borderDash: ds.borderDash || [],
|
|
||||||
|
|
||||||
// 🔺 BUY / SELL = triángulos reales
|
|
||||||
pointStyle: isScatter ? ds.pointStyle : "line",
|
|
||||||
rotation: isScatter ? ds.pointRotation || 0 : 0,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
equityChart.data.labels = labels;
|
|
||||||
equityChart.data.datasets[0].data = maxEquityCurve;
|
|
||||||
equityChart.data.datasets[1].data = equity;
|
|
||||||
equityChart.data.datasets[2].data = cash;
|
|
||||||
equityChart.data.datasets[3].data = buyPoints;
|
|
||||||
equityChart.data.datasets[4].data = sellPoints;
|
|
||||||
equityChart.update("none");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------------------------------
|
|
||||||
// SOLUCIÓN 4: TIME WINDOWS
|
|
||||||
// --------------------------------------------------
|
|
||||||
function setTimeWindow(hours) {
|
|
||||||
if (!equityChart) return;
|
|
||||||
|
|
||||||
if (hours === "ALL") {
|
|
||||||
equityChart.options.scales.x.min = undefined;
|
|
||||||
equityChart.options.scales.x.max = undefined;
|
|
||||||
} else {
|
|
||||||
const now = Date.now();
|
|
||||||
equityChart.options.scales.x.min = now - hours * 3600_000;
|
|
||||||
equityChart.options.scales.x.max = now;
|
|
||||||
}
|
|
||||||
|
|
||||||
equityChart.update();
|
|
||||||
}
|
|
||||||
|
|
||||||
async function updateEvents() {
|
|
||||||
const data = await apiGet("/events?limit=20");
|
|
||||||
document.getElementById("events-log").textContent =
|
|
||||||
data.items.join("\n");
|
|
||||||
}
|
|
||||||
|
|
||||||
// --------------------------------------------------
|
|
||||||
// UI
|
|
||||||
// --------------------------------------------------
|
|
||||||
document.getElementById("reset-zoom")?.addEventListener("click", () => {
|
|
||||||
equityChart?.resetZoom();
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById("toggle-dd")?.addEventListener("change", e => {
|
|
||||||
showDrawdown = e.target.checked;
|
|
||||||
equityChart.data.datasets[1].fill = showDrawdown
|
|
||||||
? { target: 0, above: "rgba(0,0,0,0)", below: "rgba(214,57,57,0.15)" }
|
|
||||||
: false;
|
|
||||||
equityChart.update("none");
|
|
||||||
});
|
|
||||||
|
|
||||||
// --------------------------------------------------
|
|
||||||
poll(updateStatus, 2000);
|
|
||||||
poll(updateEquity, 5000);
|
|
||||||
poll(updateCurve, 10000);
|
|
||||||
poll(updateEvents, 10000);
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
function poll(fn, interval) {
|
|
||||||
fn();
|
|
||||||
return setInterval(fn, interval);
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
<!doctype html>
|
|
||||||
<html lang="en">
|
|
||||||
<head>
|
|
||||||
<meta charset="utf-8">
|
|
||||||
<title>{% block title %}Trading Bot{% endblock %}</title>
|
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
|
||||||
<link href="/static/css/tabler.min.css" rel="stylesheet">
|
|
||||||
</head>
|
|
||||||
<body>
|
|
||||||
|
|
||||||
<div class="page">
|
|
||||||
<header class="navbar navbar-expand-md navbar-light d-print-none">
|
|
||||||
<div class="container-xl">
|
|
||||||
<span class="navbar-brand">
|
|
||||||
🤖 Trading Bot
|
|
||||||
</span>
|
|
||||||
<div id="bot-status" class="ms-auto badge bg-secondary">
|
|
||||||
INIT
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</header>
|
|
||||||
|
|
||||||
<div class="page-wrapper">
|
|
||||||
<div class="container-xl">
|
|
||||||
{% block content %}{% endblock %}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script src="/static/js/api.js"></script>
|
|
||||||
<script src="/static/js/polling.js"></script>
|
|
||||||
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.1/dist/chart.umd.min.js"></script>
|
|
||||||
|
|
||||||
{% block scripts %}{% endblock %}
|
|
||||||
</body>
|
|
||||||
</html>
|
|
||||||
@@ -1,76 +0,0 @@
|
|||||||
{% extends "base.html" %}
|
|
||||||
{% block title %}Dashboard{% endblock %}
|
|
||||||
|
|
||||||
{% block content %}
|
|
||||||
<div class="row row-deck row-cards">
|
|
||||||
|
|
||||||
<!-- KPIs -->
|
|
||||||
<div class="col-sm-6 col-lg-3">
|
|
||||||
<div class="card"><div class="card-body">
|
|
||||||
<div class="subheader">Equity</div>
|
|
||||||
<div id="kpi-equity" class="h1">—</div>
|
|
||||||
</div></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-sm-6 col-lg-3">
|
|
||||||
<div class="card"><div class="card-body">
|
|
||||||
<div class="subheader">PnL</div>
|
|
||||||
<div id="kpi-pnl" class="h1">—</div>
|
|
||||||
</div></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="col-sm-6 col-lg-3">
|
|
||||||
<div class="card"><div class="card-body">
|
|
||||||
<div class="subheader">Max DD</div>
|
|
||||||
<div id="kpi-max-dd" class="h1">—</div>
|
|
||||||
</div></div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- CHART -->
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card position-relative">
|
|
||||||
<div class="card-header">
|
|
||||||
<h3 class="card-title">Equity Curve</h3>
|
|
||||||
|
|
||||||
<div class="btn-group position-absolute top-0 end-0 m-2">
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" onclick="setTimeWindow(1)">1h</button>
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" onclick="setTimeWindow(6)">6h</button>
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" onclick="setTimeWindow(24)">24h</button>
|
|
||||||
<button class="btn btn-sm btn-outline-secondary" onclick="setTimeWindow('ALL')">ALL</button>
|
|
||||||
<button id="reset-zoom" class="btn btn-sm btn-outline-secondary">Reset</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="form-check form-switch position-absolute top-20 start-50 m-2">
|
|
||||||
<input class="form-check-input" type="checkbox" id="toggle-dd" checked>
|
|
||||||
<label class="form-check-label">Drawdown</label>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="card-body">
|
|
||||||
<canvas id="equityChart" height="120"></canvas>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- EVENTS -->
|
|
||||||
<div class="col-12">
|
|
||||||
<div class="card">
|
|
||||||
<div class="card-header"><h3 class="card-title">Events</h3></div>
|
|
||||||
<div class="card-body">
|
|
||||||
<pre id="events-log" style="max-height:200px; overflow:auto;"></pre>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
{% endblock %}
|
|
||||||
|
|
||||||
{% block scripts %}
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
|
||||||
<!-- ✅ Time adapter necesario para scales.x.type="time" -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-adapter-date-fns@3"></script>
|
|
||||||
<!-- ✅ Zoom plugin -->
|
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-zoom@2.0.1"></script>
|
|
||||||
<!-- Tu dashboard -->
|
|
||||||
<script src="/static/js/dashboard.js"></script>
|
|
||||||
{% endblock %}
|
|
||||||
@@ -728,6 +728,179 @@ async function pollStatus(jobId) {
|
|||||||
}, 1000);
|
}, 1000);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
// =================================================
|
||||||
|
// DIFFERENT RENDER PLOTS
|
||||||
|
// =================================================
|
||||||
|
|
||||||
|
function renderEquityAndReturns(strategyId, s, data) {
|
||||||
|
|
||||||
|
console.log("Plotly object:", Plotly); // Esto debería mostrarte el objeto Plotly completo
|
||||||
|
|
||||||
|
if (Plotly && Plotly.subplots) {
|
||||||
|
console.log("make_subplots is available");
|
||||||
|
} else {
|
||||||
|
console.error("make_subplots is NOT available");
|
||||||
|
}
|
||||||
|
|
||||||
|
// Crea el subplot con dos filas y una columna (1x2)
|
||||||
|
var fig = Plotly.subplots.make_subplots({
|
||||||
|
rows: 2, // 2 filas
|
||||||
|
cols: 1, // 1 columna
|
||||||
|
shared_xaxes: true, // Compartir el eje X entre ambos gráficos
|
||||||
|
vertical_spacing: 0.1, // Espacio entre los gráficos
|
||||||
|
subplot_titles: [`Equity — ${strategyId}`, `Returns & Trades — ${strategyId}`], // Títulos para cada subgráfico
|
||||||
|
column_widths: [0.7] // Ajustar el ancho de las columnas si es necesario
|
||||||
|
});
|
||||||
|
|
||||||
|
// Datos de Equity
|
||||||
|
const equityTrace = {
|
||||||
|
y: s.window_equity, // Datos de la equity
|
||||||
|
type: "scatter", // Tipo de gráfico: línea
|
||||||
|
mode: "lines", // Modo: línea
|
||||||
|
name: "Equity"
|
||||||
|
};
|
||||||
|
|
||||||
|
// Datos de Return %
|
||||||
|
const returnsTrace = {
|
||||||
|
y: s.window_returns_pct || [], // Datos de returns
|
||||||
|
type: "bar", // Tipo de gráfico: barra
|
||||||
|
name: "Return %",
|
||||||
|
marker: { color: "#3b82f6" }, // Color de la barra
|
||||||
|
yaxis: "y1", // Asociar al primer eje Y
|
||||||
|
};
|
||||||
|
|
||||||
|
// Datos de Trades
|
||||||
|
const tradesTrace = {
|
||||||
|
y: s.window_trades || [], // Datos de trades
|
||||||
|
type: "bar", // Tipo de gráfico: barra
|
||||||
|
name: "Trades",
|
||||||
|
marker: { color: "#f59e0b" }, // Color de la barra
|
||||||
|
yaxis: "y2", // Asociar al segundo eje Y
|
||||||
|
};
|
||||||
|
|
||||||
|
// Agregar la traza de "Equity" al subplot (fila 1, columna 1)
|
||||||
|
fig.addTrace(equityTrace, 1, 1);
|
||||||
|
|
||||||
|
// Agregar las trazas de "Returns" y "Trades" al subplot (fila 2, columna 1)
|
||||||
|
fig.addTrace(returnsTrace, 2, 1);
|
||||||
|
fig.addTrace(tradesTrace, 2, 1);
|
||||||
|
|
||||||
|
// Configurar los ejes y los márgenes
|
||||||
|
fig.update_layout({
|
||||||
|
title: `Strategy Overview — ${strategyId}`,
|
||||||
|
yaxis: {
|
||||||
|
title: "Equity",
|
||||||
|
showgrid: true
|
||||||
|
},
|
||||||
|
yaxis2: {
|
||||||
|
title: "Return % / Trades",
|
||||||
|
overlaying: "y",
|
||||||
|
side: "right",
|
||||||
|
showgrid: true
|
||||||
|
},
|
||||||
|
xaxis: {
|
||||||
|
title: "Windows",
|
||||||
|
showgrid: true
|
||||||
|
},
|
||||||
|
showlegend: true
|
||||||
|
});
|
||||||
|
|
||||||
|
// Renderizar en el contenedor del gráfico
|
||||||
|
Plotly.newPlot("plot_strategy", fig);
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderRollingSharpe(strategyId, s, data) {
|
||||||
|
const roll = s.diagnostics?.rolling?.rolling_sharpe_like || [];
|
||||||
|
const x = roll.map((_, i) => i + 1);
|
||||||
|
|
||||||
|
Plotly.newPlot("plot_strategy", [{
|
||||||
|
x,
|
||||||
|
y: roll,
|
||||||
|
type: "scatter",
|
||||||
|
mode: "lines+markers",
|
||||||
|
name: `Rolling Sharpe-like (k=${s.diagnostics?.rolling?.rolling_window ?? "?"})`
|
||||||
|
}], {
|
||||||
|
margin: { t: 40 },
|
||||||
|
title: `Rolling Sharpe-like — ${strategyId}`,
|
||||||
|
xaxis: { title: "Window" },
|
||||||
|
yaxis: { title: "Sharpe-like" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderOOSReturnsDistribution(strategyId, s, data) {
|
||||||
|
const edges = s.diagnostics?.distribution?.hist_bin_edges || [];
|
||||||
|
const counts = s.diagnostics?.distribution?.hist_counts || [];
|
||||||
|
|
||||||
|
const centers = edges.map((_, i) => (edges[i] + edges[i + 1]) / 2);
|
||||||
|
|
||||||
|
Plotly.newPlot("plot_strategy", [{
|
||||||
|
x: centers,
|
||||||
|
y: counts,
|
||||||
|
type: "bar",
|
||||||
|
name: "OOS Return% (bins)"
|
||||||
|
}], {
|
||||||
|
margin: { t: 40 },
|
||||||
|
title: `OOS Returns Distribution — ${strategyId}`,
|
||||||
|
xaxis: { title: "Return % (window)" },
|
||||||
|
yaxis: { title: "Count" }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDrawdownEvolution(strategyId, s, data) {
|
||||||
|
const dd = s.diagnostics?.drawdown?.drawdown_pct || [];
|
||||||
|
const x = dd.map((_, i) => i);
|
||||||
|
|
||||||
|
Plotly.newPlot("plot_strategy", [{
|
||||||
|
x,
|
||||||
|
y: dd,
|
||||||
|
type: "scatter",
|
||||||
|
mode: "lines",
|
||||||
|
name: "Drawdown %"
|
||||||
|
}], {
|
||||||
|
margin: { t: 40 },
|
||||||
|
title: `Drawdown Evolution — ${strategyId}`,
|
||||||
|
xaxis: { title: "Point (initial + windows)" },
|
||||||
|
yaxis: { title: "Drawdown %", zeroline: true }
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderTradeDensity(strategyId, s, data) {
|
||||||
|
const tpw = s.diagnostics?.trades?.trades_per_window || [];
|
||||||
|
const tpd = s.diagnostics?.trades?.trades_per_day || [];
|
||||||
|
const x = tpw.map((_, i) => i + 1);
|
||||||
|
|
||||||
|
Plotly.newPlot("plot_strategy", [
|
||||||
|
{
|
||||||
|
x,
|
||||||
|
y: tpw,
|
||||||
|
type: "bar",
|
||||||
|
name: "Trades / window",
|
||||||
|
yaxis: "y1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
x,
|
||||||
|
y: tpd,
|
||||||
|
type: "scatter",
|
||||||
|
mode: "lines+markers",
|
||||||
|
name: "Trades / day",
|
||||||
|
yaxis: "y2"
|
||||||
|
}
|
||||||
|
], {
|
||||||
|
margin: { t: 40 },
|
||||||
|
title: `Trade Density — ${strategyId}`,
|
||||||
|
barmode: "group",
|
||||||
|
xaxis: { title: "Window" },
|
||||||
|
yaxis: { title: "Trades / window" },
|
||||||
|
yaxis2: {
|
||||||
|
title: "Trades / day",
|
||||||
|
overlaying: "y",
|
||||||
|
side: "right",
|
||||||
|
zeroline: false
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// =================================================
|
// =================================================
|
||||||
// RENDER RESULTS
|
// RENDER RESULTS
|
||||||
// =================================================
|
// =================================================
|
||||||
@@ -854,6 +1027,7 @@ function populatePlotSelector(data) {
|
|||||||
function selectStrategy(strategyId, data) {
|
function selectStrategy(strategyId, data) {
|
||||||
if (!strategyId || !data) return;
|
if (!strategyId || !data) return;
|
||||||
|
|
||||||
|
// Actualiza selectedStrategyId
|
||||||
selectedStrategyId = strategyId;
|
selectedStrategyId = strategyId;
|
||||||
|
|
||||||
const row = (data.results || []).find(r => r.strategy_id === strategyId);
|
const row = (data.results || []).find(r => r.strategy_id === strategyId);
|
||||||
@@ -880,35 +1054,26 @@ function selectStrategy(strategyId, data) {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 3) Renderizar serie si existe
|
// 3) Verificar si los datos de la estrategia están disponibles
|
||||||
const s = data?.series?.strategies?.[strategyId];
|
const strategyData = data?.series?.strategies?.[selectedStrategyId];
|
||||||
if (!s) {
|
|
||||||
// fallback explícito (por si backend antiguo no manda series_available)
|
if (!strategyData) {
|
||||||
showPlotAlert(
|
showPlotAlert(
|
||||||
row?.status === "fail" ? "danger" : "warning",
|
"warning",
|
||||||
`${(row?.status || "warning").toUpperCase()} — ${strategyId}`,
|
`No data available — ${strategyId}`,
|
||||||
row?.message || "No chart series available for this strategy.",
|
"Strategy data not available for rendering.",
|
||||||
row?.warnings
|
[]
|
||||||
);
|
);
|
||||||
clearPlots();
|
clearPlots();
|
||||||
highlightSelectedRow(strategyId);
|
highlightSelectedRow(strategyId);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
renderStrategyCharts(strategyId, s, data);
|
// 4) Mantén el gráfico previamente seleccionado en el dropdown
|
||||||
highlightSelectedRow(strategyId);
|
const chartType = document.getElementById("plot_strategy_select").value;
|
||||||
|
renderChart(chartType, selectedStrategyId, strategyData, data); // Renderiza el gráfico correctamente
|
||||||
|
|
||||||
// 4) Caso “serie vacía” (opción B) -> warning explícito (aunque ya lo tengas)
|
highlightSelectedRow(strategyId);
|
||||||
const trd = s.window_trades || [];
|
|
||||||
const hasTrades = Array.isArray(trd) && trd.some(v => (v ?? 0) > 0);
|
|
||||||
if (!hasTrades && row?.status !== "fail") {
|
|
||||||
showPlotAlert(
|
|
||||||
"warning",
|
|
||||||
`NO TRADES — ${strategyId}`,
|
|
||||||
"Walk-forward produced no closed trades in OOS. Charts may be flat/empty.",
|
|
||||||
row?.warnings
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderValidateResponse(data) {
|
function renderValidateResponse(data) {
|
||||||
@@ -1011,115 +1176,27 @@ function renderValidateResponse(data) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderStrategyCharts(strategyId, s, data) {
|
function renderChart(chartType, strategyId, s, data) {
|
||||||
|
switch (chartType) {
|
||||||
if (!s) return
|
case "equity":
|
||||||
|
renderEquityAndReturns(strategyId, s, data);
|
||||||
// ============================
|
break;
|
||||||
// 1️⃣ EQUITY
|
case "rolling_sharpe":
|
||||||
// ============================
|
renderRollingSharpe(strategyId, s, data);
|
||||||
|
break;
|
||||||
Plotly.newPlot("plot_equity", [{
|
case "hist_oos_returns":
|
||||||
y: s.window_equity,
|
renderOOSReturnsDistribution(strategyId, s, data);
|
||||||
type: "scatter",
|
break;
|
||||||
mode: "lines",
|
case "drawdown":
|
||||||
name: "Equity"
|
renderDrawdownEvolution(strategyId, s, data);
|
||||||
}], {
|
break;
|
||||||
margin: { t: 20 },
|
case "trade_density":
|
||||||
title: `Equity — ${strategyId}`
|
renderTradeDensity(strategyId, s, data);
|
||||||
});
|
break;
|
||||||
|
default:
|
||||||
// ============================
|
renderEquityAndReturns(strategyId, s, data);
|
||||||
// 2️⃣ RETURNS + TRADES
|
break;
|
||||||
// ============================
|
}
|
||||||
|
|
||||||
const ret = s.window_returns_pct || [];
|
|
||||||
const trd = s.window_trades || [];
|
|
||||||
|
|
||||||
const retMax = Math.max(0, ...ret);
|
|
||||||
const retMin = Math.min(0, ...ret);
|
|
||||||
|
|
||||||
const minTrades = data.config?.wf?.min_trades_test ?? 10;
|
|
||||||
|
|
||||||
const trdMaxRaw = Math.max(0, ...trd);
|
|
||||||
const trdMax = Math.max(trdMaxRaw, minTrades);
|
|
||||||
|
|
||||||
// Evitar divisiones raras
|
|
||||||
const retPosSpan = Math.max(1e-9, retMax);
|
|
||||||
const retNegSpan = Math.abs(retMin);
|
|
||||||
|
|
||||||
// Alinear el 0 visualmente
|
|
||||||
const trdNegSpan = (retNegSpan / retPosSpan) * trdMax;
|
|
||||||
|
|
||||||
const y1Range = [retMin, retMax];
|
|
||||||
const y2Range = [-trdNegSpan, trdMax];
|
|
||||||
|
|
||||||
Plotly.newPlot("plot_returns", [
|
|
||||||
|
|
||||||
{
|
|
||||||
y: ret,
|
|
||||||
type: "bar",
|
|
||||||
name: "Return %",
|
|
||||||
marker: { color: "#3b82f6" },
|
|
||||||
yaxis: "y1",
|
|
||||||
offsetgroup: "returns",
|
|
||||||
alignmentgroup: "group1"
|
|
||||||
},
|
|
||||||
|
|
||||||
{
|
|
||||||
y: trd,
|
|
||||||
type: "bar",
|
|
||||||
name: "Trades",
|
|
||||||
marker: { color: "#f59e0b" },
|
|
||||||
yaxis: "y2",
|
|
||||||
offsetgroup: "trades",
|
|
||||||
alignmentgroup: "group1"
|
|
||||||
}
|
|
||||||
|
|
||||||
], {
|
|
||||||
|
|
||||||
margin: { t: 20 },
|
|
||||||
barmode: "group",
|
|
||||||
|
|
||||||
yaxis: {
|
|
||||||
title: "Return %",
|
|
||||||
range: y1Range,
|
|
||||||
zeroline: true,
|
|
||||||
zerolinewidth: 2
|
|
||||||
},
|
|
||||||
|
|
||||||
yaxis2: {
|
|
||||||
title: "Trades",
|
|
||||||
overlaying: "y",
|
|
||||||
side: "right",
|
|
||||||
range: y2Range,
|
|
||||||
zeroline: true,
|
|
||||||
zerolinewidth: 2
|
|
||||||
},
|
|
||||||
|
|
||||||
shapes: [
|
|
||||||
{
|
|
||||||
type: "line",
|
|
||||||
x0: -0.5,
|
|
||||||
x1: trd.length - 0.5,
|
|
||||||
y0: minTrades,
|
|
||||||
y1: minTrades,
|
|
||||||
yref: "y2",
|
|
||||||
line: {
|
|
||||||
color: "red",
|
|
||||||
width: 2,
|
|
||||||
dash: "dash"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
],
|
|
||||||
|
|
||||||
legend: {
|
|
||||||
orientation: "h"
|
|
||||||
},
|
|
||||||
|
|
||||||
title: `Returns & Trades — ${strategyId}`
|
|
||||||
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function highlightSelectedRow(strategyId) {
|
function highlightSelectedRow(strategyId) {
|
||||||
@@ -1310,6 +1387,17 @@ async function init() {
|
|||||||
|
|
||||||
wireButtons();
|
wireButtons();
|
||||||
|
|
||||||
|
document.getElementById("plot_strategy_select").addEventListener("change", function() {
|
||||||
|
const chartType = this.value;
|
||||||
|
const strategyData = lastValidationResult.series.strategies[selectedStrategyId];
|
||||||
|
|
||||||
|
// Verifica que selectedStrategyId tenga el valor correcto
|
||||||
|
console.log("selectedStrategyId:", selectedStrategyId);
|
||||||
|
console.log("Strategy Data:", strategyData);
|
||||||
|
|
||||||
|
renderChart(chartType, selectedStrategyId, strategyData, lastValidationResult);
|
||||||
|
});
|
||||||
|
|
||||||
const strategies = await fetchAvailableStrategies();
|
const strategies = await fetchAvailableStrategies();
|
||||||
renderStrategiesList(strategies);
|
renderStrategiesList(strategies);
|
||||||
|
|
||||||
@@ -1329,7 +1417,7 @@ function ensurePlotAlertContainer() {
|
|||||||
let el = document.getElementById("plot_alert");
|
let el = document.getElementById("plot_alert");
|
||||||
if (el) return el;
|
if (el) return el;
|
||||||
|
|
||||||
const anchor = document.getElementById("plot_equity");
|
const anchor = document.getElementById("plot_strategy");
|
||||||
if (!anchor || !anchor.parentElement) return null;
|
if (!anchor || !anchor.parentElement) return null;
|
||||||
|
|
||||||
el = document.createElement("div");
|
el = document.createElement("div");
|
||||||
@@ -1373,10 +1461,8 @@ function clearPlotAlert() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function clearPlots() {
|
function clearPlots() {
|
||||||
const eq = document.getElementById("plot_equity");
|
const eq = document.getElementById("plot_strategy");
|
||||||
const ret = document.getElementById("plot_returns");
|
|
||||||
if (eq) eq.innerHTML = "";
|
if (eq) eq.innerHTML = "";
|
||||||
if (ret) ret.innerHTML = "";
|
|
||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById("lock_inherited")
|
document.getElementById("lock_inherited")
|
||||||
|
|||||||
266950
src/web/ui/v2/static/js/plotly.js
Normal file
266950
src/web/ui/v2/static/js/plotly.js
Normal file
File diff suppressed because one or more lines are too long
@@ -306,15 +306,18 @@
|
|||||||
<div class="row g-3">
|
<div class="row g-3">
|
||||||
<div class="col-md-4">
|
<div class="col-md-4">
|
||||||
<label class="form-label">Strategy plot</label>
|
<label class="form-label">Strategy plot</label>
|
||||||
<select id="plot_strategy_select" class="form-select"></select>
|
<select id="plot_strategy_select" class="form-select">
|
||||||
|
<option value="equity">Equity + Returns & Trades</option>
|
||||||
|
<option value="rolling_sharpe">Rolling Sharpe-like</option>
|
||||||
|
<option value="hist_oos_returns">OOS Returns Distribution</option>
|
||||||
|
<option value="drawdown">Drawdown Evolution</option>
|
||||||
|
<option value="trade_density">Trade Density</option>
|
||||||
|
</select>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="mt-3">
|
<div class="mt-3">
|
||||||
<div id="plot_equity" style="height: 320px;"></div>
|
<div id="plot_strategy" style="height: 320px;"></div>
|
||||||
</div>
|
|
||||||
<div class="mt-3">
|
|
||||||
<div id="plot_returns" style="height: 320px;"></div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<hr class="my-4">
|
<hr class="my-4">
|
||||||
@@ -345,6 +348,6 @@
|
|||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="https://cdn.plot.ly/plotly-2.27.0.min.js"></script>
|
<script src="/static/js/plotly.js"></script>
|
||||||
<script src="/static/js/pages/calibration_strategies.js"></script>
|
<script src="/static/js/pages/calibration_strategies.js"></script>
|
||||||
{% endblock %}
|
{% endblock %}
|
||||||
|
|||||||
@@ -1,366 +0,0 @@
|
|||||||
{% 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