14f130aa5a
Nuova pagina NiceGUI "Paper" che legge le tabelle paper_trading_*:
- 4 metric card: Equity, P/L cumulato %, Trades chiusi, Open/Tick count
- Equity curve plotly con hline initial capital
- Tre tabelle: open positions, ultimi 30 tick (ts/bar/symbol/signal/action),
trades chiusi (entry/exit/pnl/fees)
- Run selector dropdown + status badge + auto-refresh REFRESH_INTERVAL_S
dashboard/data.py: aggiunti 6 helper read-only su SQLite (paper_runs_df,
paper_equity_df, paper_positions_df, paper_trades_df, paper_ticks_df,
paper_run_summary). Connessione separata da Repository per usare
direttamente lo schema paper_trading_* senza passare per la classe di
write PaperRepository.
dashboard/nicegui_app.py: aggiunto import pandas (necessario per
to_datetime nell'equity curve), nav link "Paper" nell'header,
@ui.page("/paper") con helper _paper_runs_options + _paper_equity_figure.
Chiude il primo TODO della roadmap sez 10.1 ("Pagina dashboard
paper-trading").
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
174 lines
5.9 KiB
Python
174 lines
5.9 KiB
Python
from __future__ import annotations
|
|
|
|
import json
|
|
import sqlite3
|
|
from pathlib import Path
|
|
from typing import Any
|
|
|
|
import pandas as pd # type: ignore[import-untyped]
|
|
|
|
from ..persistence.repository import Repository
|
|
|
|
|
|
def get_repo(db_path: str | Path) -> Repository:
|
|
return Repository(db_path=db_path)
|
|
|
|
|
|
def list_runs_df(repo: Repository) -> pd.DataFrame:
|
|
return pd.DataFrame(repo.list_runs())
|
|
|
|
|
|
def get_run_overview(repo: Repository, run_id: str) -> dict[str, Any]:
|
|
run = repo.get_run(run_id)
|
|
return {
|
|
"name": run["name"],
|
|
"started_at": run["started_at"],
|
|
"completed_at": run["completed_at"],
|
|
"status": run["status"],
|
|
"total_cost_usd": run["total_cost_usd"],
|
|
"config": json.loads(run["config_json"]),
|
|
}
|
|
|
|
|
|
def generations_df(repo: Repository, run_id: str) -> pd.DataFrame:
|
|
return pd.DataFrame(repo.list_generations(run_id))
|
|
|
|
|
|
def evaluations_df(repo: Repository, run_id: str) -> pd.DataFrame:
|
|
return pd.DataFrame(repo.list_evaluations(run_id))
|
|
|
|
|
|
def genomes_df(
|
|
repo: Repository, run_id: str, generation_idx: int | None = None
|
|
) -> pd.DataFrame:
|
|
rows = repo.list_genomes(run_id, generation_idx)
|
|
flat: list[dict[str, Any]] = []
|
|
for r in rows:
|
|
payload = json.loads(r["payload_json"])
|
|
flat.append(
|
|
{
|
|
"id": r["id"],
|
|
"generation_idx": r["generation_idx"],
|
|
**payload,
|
|
}
|
|
)
|
|
return pd.DataFrame(flat)
|
|
|
|
|
|
def _paper_conn(db_path: str | Path) -> sqlite3.Connection:
|
|
conn = sqlite3.connect(str(db_path))
|
|
conn.row_factory = sqlite3.Row
|
|
return conn
|
|
|
|
|
|
def paper_runs_df(db_path: str | Path) -> pd.DataFrame:
|
|
with _paper_conn(db_path) as conn:
|
|
rows = conn.execute(
|
|
"SELECT id, name, started_at, stopped_at, status, initial_capital, config_json "
|
|
"FROM paper_trading_runs ORDER BY started_at DESC"
|
|
).fetchall()
|
|
return pd.DataFrame([dict(r) for r in rows])
|
|
|
|
|
|
def paper_equity_df(db_path: str | Path, run_id: str) -> pd.DataFrame:
|
|
with _paper_conn(db_path) as conn:
|
|
rows = conn.execute(
|
|
"SELECT ts, equity, cash, positions_value FROM paper_trading_equity "
|
|
"WHERE paper_run_id=? ORDER BY ts ASC",
|
|
(run_id,),
|
|
).fetchall()
|
|
return pd.DataFrame([dict(r) for r in rows])
|
|
|
|
|
|
def paper_positions_df(db_path: str | Path, run_id: str) -> pd.DataFrame:
|
|
with _paper_conn(db_path) as conn:
|
|
rows = conn.execute(
|
|
"SELECT symbol, side, qty, entry_price, entry_ts "
|
|
"FROM paper_trading_positions WHERE paper_run_id=? ORDER BY symbol",
|
|
(run_id,),
|
|
).fetchall()
|
|
return pd.DataFrame([dict(r) for r in rows])
|
|
|
|
|
|
def paper_trades_df(db_path: str | Path, run_id: str, limit: int = 100) -> pd.DataFrame:
|
|
with _paper_conn(db_path) as conn:
|
|
rows = conn.execute(
|
|
"SELECT symbol, side, qty, entry_price, exit_price, entry_ts, exit_ts, pnl, fees "
|
|
"FROM paper_trading_trades WHERE paper_run_id=? ORDER BY exit_ts DESC LIMIT ?",
|
|
(run_id, limit),
|
|
).fetchall()
|
|
return pd.DataFrame([dict(r) for r in rows])
|
|
|
|
|
|
def paper_ticks_df(db_path: str | Path, run_id: str, limit: int = 50) -> pd.DataFrame:
|
|
with _paper_conn(db_path) as conn:
|
|
rows = conn.execute(
|
|
"SELECT ts, bar_ts, symbol, close_price, signal, action_taken "
|
|
"FROM paper_trading_ticks WHERE paper_run_id=? ORDER BY ts DESC LIMIT ?",
|
|
(run_id, limit),
|
|
).fetchall()
|
|
return pd.DataFrame([dict(r) for r in rows])
|
|
|
|
|
|
def paper_run_summary(db_path: str | Path, run_id: str) -> dict[str, Any]:
|
|
"""Aggrega metriche sintetiche per la pagina paper trading."""
|
|
with _paper_conn(db_path) as conn:
|
|
run = conn.execute(
|
|
"SELECT id, name, started_at, stopped_at, status, initial_capital, config_json "
|
|
"FROM paper_trading_runs WHERE id=?",
|
|
(run_id,),
|
|
).fetchone()
|
|
if run is None:
|
|
return {}
|
|
run = dict(run)
|
|
|
|
eq_row = conn.execute(
|
|
"SELECT equity, cash, positions_value, ts FROM paper_trading_equity "
|
|
"WHERE paper_run_id=? ORDER BY ts DESC LIMIT 1",
|
|
(run_id,),
|
|
).fetchone()
|
|
|
|
trades_agg = conn.execute(
|
|
"SELECT COUNT(*) AS n, COALESCE(SUM(pnl),0) AS sum_pnl, "
|
|
"COALESCE(SUM(fees),0) AS sum_fees FROM paper_trading_trades "
|
|
"WHERE paper_run_id=?",
|
|
(run_id,),
|
|
).fetchone()
|
|
|
|
tick_agg = conn.execute(
|
|
"SELECT COUNT(*) AS n, MAX(ts) AS last_ts FROM paper_trading_ticks "
|
|
"WHERE paper_run_id=?",
|
|
(run_id,),
|
|
).fetchone()
|
|
|
|
positions_n = conn.execute(
|
|
"SELECT COUNT(*) AS n FROM paper_trading_positions WHERE paper_run_id=?",
|
|
(run_id,),
|
|
).fetchone()["n"]
|
|
|
|
initial = float(run["initial_capital"])
|
|
current_equity = float(eq_row["equity"]) if eq_row is not None else initial
|
|
pnl_pct = (current_equity - initial) / initial * 100.0 if initial else 0.0
|
|
|
|
return {
|
|
"id": run["id"],
|
|
"name": run["name"],
|
|
"status": run["status"],
|
|
"started_at": run["started_at"],
|
|
"stopped_at": run["stopped_at"],
|
|
"initial_capital": initial,
|
|
"config": json.loads(run["config_json"]),
|
|
"current_equity": current_equity,
|
|
"current_cash": float(eq_row["cash"]) if eq_row is not None else initial,
|
|
"current_positions_value": float(eq_row["positions_value"]) if eq_row is not None else 0.0,
|
|
"last_equity_ts": eq_row["ts"] if eq_row is not None else None,
|
|
"pnl_abs": current_equity - initial,
|
|
"pnl_pct": pnl_pct,
|
|
"n_trades": int(trades_agg["n"]),
|
|
"trades_pnl": float(trades_agg["sum_pnl"]),
|
|
"trades_fees": float(trades_agg["sum_fees"]),
|
|
"n_ticks": int(tick_agg["n"]),
|
|
"last_tick_ts": tick_agg["last_ts"],
|
|
"n_open_positions": int(positions_n),
|
|
}
|