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
|
||||
|
||||
|
||||
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
|
||||
# --------------------------------------------------
|
||||
@@ -289,6 +386,7 @@ def inspect_strategies_config(
|
||||
"oos_total_return_pct": 0.0,
|
||||
"oos_max_dd_worst_pct": 0.0,
|
||||
"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": [],
|
||||
})
|
||||
|
||||
@@ -298,6 +396,7 @@ def inspect_strategies_config(
|
||||
"window_returns_pct": [],
|
||||
"window_equity": [float(payload.account_equity)],
|
||||
"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":
|
||||
@@ -348,13 +447,24 @@ def inspect_strategies_config(
|
||||
"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)
|
||||
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
|
||||
|
||||
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({
|
||||
"strategy_id": sid,
|
||||
"status": status,
|
||||
@@ -367,6 +477,7 @@ def inspect_strategies_config(
|
||||
"oos_total_return_pct": float(oos_total_return),
|
||||
"oos_max_dd_worst_pct": float(oos_max_dd),
|
||||
"degradation_sharpe": None,
|
||||
"diagnostics": diagnostics,
|
||||
"windows": windows_out,
|
||||
})
|
||||
|
||||
@@ -375,6 +486,7 @@ def inspect_strategies_config(
|
||||
"window_returns_pct": oos_returns,
|
||||
"window_equity": eq_curve,
|
||||
"window_trades": win_df["trades"].tolist(),
|
||||
"diagnostics": diagnostics,
|
||||
}
|
||||
|
||||
except Exception as e:
|
||||
@@ -422,4 +534,4 @@ def inspect_strategies_config(
|
||||
if include_series:
|
||||
out["series"] = series
|
||||
|
||||
return out
|
||||
return out
|
||||
@@ -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
|
||||
degradation_sharpe: Optional[float] = None
|
||||
|
||||
diagnostics: Optional[Dict[str, Any]] = None
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
|
||||
// =================================================
|
||||
// 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
|
||||
// =================================================
|
||||
@@ -854,6 +1027,7 @@ function populatePlotSelector(data) {
|
||||
function selectStrategy(strategyId, data) {
|
||||
if (!strategyId || !data) return;
|
||||
|
||||
// Actualiza selectedStrategyId
|
||||
selectedStrategyId = strategyId;
|
||||
|
||||
const row = (data.results || []).find(r => r.strategy_id === strategyId);
|
||||
@@ -880,35 +1054,26 @@ function selectStrategy(strategyId, data) {
|
||||
return;
|
||||
}
|
||||
|
||||
// 3) Renderizar serie si existe
|
||||
const s = data?.series?.strategies?.[strategyId];
|
||||
if (!s) {
|
||||
// fallback explícito (por si backend antiguo no manda series_available)
|
||||
// 3) Verificar si los datos de la estrategia están disponibles
|
||||
const strategyData = data?.series?.strategies?.[selectedStrategyId];
|
||||
|
||||
if (!strategyData) {
|
||||
showPlotAlert(
|
||||
row?.status === "fail" ? "danger" : "warning",
|
||||
`${(row?.status || "warning").toUpperCase()} — ${strategyId}`,
|
||||
row?.message || "No chart series available for this strategy.",
|
||||
row?.warnings
|
||||
"warning",
|
||||
`No data available — ${strategyId}`,
|
||||
"Strategy data not available for rendering.",
|
||||
[]
|
||||
);
|
||||
clearPlots();
|
||||
highlightSelectedRow(strategyId);
|
||||
return;
|
||||
}
|
||||
|
||||
renderStrategyCharts(strategyId, s, data);
|
||||
highlightSelectedRow(strategyId);
|
||||
// 4) Mantén el gráfico previamente seleccionado en el dropdown
|
||||
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)
|
||||
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
|
||||
);
|
||||
}
|
||||
highlightSelectedRow(strategyId);
|
||||
}
|
||||
|
||||
function renderValidateResponse(data) {
|
||||
@@ -1011,115 +1176,27 @@ function renderValidateResponse(data) {
|
||||
}
|
||||
}
|
||||
|
||||
function renderStrategyCharts(strategyId, s, data) {
|
||||
|
||||
if (!s) return
|
||||
|
||||
// ============================
|
||||
// 1️⃣ EQUITY
|
||||
// ============================
|
||||
|
||||
Plotly.newPlot("plot_equity", [{
|
||||
y: s.window_equity,
|
||||
type: "scatter",
|
||||
mode: "lines",
|
||||
name: "Equity"
|
||||
}], {
|
||||
margin: { t: 20 },
|
||||
title: `Equity — ${strategyId}`
|
||||
});
|
||||
|
||||
// ============================
|
||||
// 2️⃣ RETURNS + TRADES
|
||||
// ============================
|
||||
|
||||
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 renderChart(chartType, strategyId, s, data) {
|
||||
switch (chartType) {
|
||||
case "equity":
|
||||
renderEquityAndReturns(strategyId, s, data);
|
||||
break;
|
||||
case "rolling_sharpe":
|
||||
renderRollingSharpe(strategyId, s, data);
|
||||
break;
|
||||
case "hist_oos_returns":
|
||||
renderOOSReturnsDistribution(strategyId, s, data);
|
||||
break;
|
||||
case "drawdown":
|
||||
renderDrawdownEvolution(strategyId, s, data);
|
||||
break;
|
||||
case "trade_density":
|
||||
renderTradeDensity(strategyId, s, data);
|
||||
break;
|
||||
default:
|
||||
renderEquityAndReturns(strategyId, s, data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function highlightSelectedRow(strategyId) {
|
||||
@@ -1309,6 +1386,17 @@ async function init() {
|
||||
.addEventListener("change", updateStopUI);
|
||||
|
||||
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();
|
||||
renderStrategiesList(strategies);
|
||||
@@ -1329,7 +1417,7 @@ function ensurePlotAlertContainer() {
|
||||
let el = document.getElementById("plot_alert");
|
||||
if (el) return el;
|
||||
|
||||
const anchor = document.getElementById("plot_equity");
|
||||
const anchor = document.getElementById("plot_strategy");
|
||||
if (!anchor || !anchor.parentElement) return null;
|
||||
|
||||
el = document.createElement("div");
|
||||
@@ -1373,10 +1461,8 @@ function clearPlotAlert() {
|
||||
}
|
||||
|
||||
function clearPlots() {
|
||||
const eq = document.getElementById("plot_equity");
|
||||
const ret = document.getElementById("plot_returns");
|
||||
const eq = document.getElementById("plot_strategy");
|
||||
if (eq) eq.innerHTML = "";
|
||||
if (ret) ret.innerHTML = "";
|
||||
}
|
||||
|
||||
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="col-md-4">
|
||||
<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 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 id="plot_strategy" style="height: 320px;"></div>
|
||||
</div>
|
||||
|
||||
<hr class="my-4">
|
||||
@@ -345,6 +348,6 @@
|
||||
|
||||
</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>
|
||||
{% 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