feat: VBT visualization + validation pipeline, HFT tick viz, DuckDB loader
Track 1 — VBT Candle-Frequency Pipeline: - backtests/vbt_validator.py: VBTValidator with 11 checks — timestamp monotonicity, duplicates, NaN, data gaps, lookahead bias, signal alignment, density, coincident entry/exit, min trade count, fee application, benchmark comparison. ValidationReport dataclass with errors/warnings/stats. Validates VBT results or raw signal arrays. - backtests/vbt_viz.py: VBTVisualizer with 10+ Plotly chart methods — equity curve with benchmark, drawdown, rolling Sharpe/Sortino/vol, trade markers, returns distribution with normal fit, monthly PnL heatmap, gross vs net, holding periods, parameter sensitivity heatmaps, dashboard compositor, HTML save (self-contained, CDN Plotly). All methods handle empty/null inputs. - backtests/vbt_report.py: Markdown + HTML report generator — structured sections for implementation summary, performance metrics, cost analysis, validation results, signal analysis, known limitations, next steps. batch_report() for mass report generation from results directory. - backtests/vbt_runner.py: Added run_benchmark() (buy-and-hold VBT portfolio), validate() (integrated VBTValidator), run_with_report() (fetch→validate→ backtest→visualize→save in one call). Track 2 — HFT Tick Pipeline: - backtests/tick_viz.py: 9-panel HFT dashboard — price+trade markers, spread dynamics, top-of-book depth, microprice vs mid, OBI/OFI panel, VPIN toxicity with thresholds, event timeline (PnL from tick_runner), markout curves at 6 horizons. Parquet→pandas→Plotly pipeline. Dark-themed HTML output for microstructure review. - data/duckdb_load.py: Parquet→DuckDB loader — creates l2_snapshots, trades, funding tables with schema. Pre-computed 1s rollup views for microprice, OFI, trade imbalance. Markout queries directly in SQL. Incremental loading with load_state tracking. CLI Integration: - cli.py: Added 'report' (full VBT report), 'validate' (check existing results), 'hft' (tick dashboard generation) commands. Fixed argparse help string escaping. 355 tests passing (34 new).
This commit is contained in:
@@ -0,0 +1,648 @@
|
||||
"""
|
||||
HFT Tick Visualization Dashboard — Plotly-based microstructural charts.
|
||||
|
||||
Visualizes tick-level order book data, trades, and microstructural
|
||||
metrics from DuckDB (or Parquet directly for small datasets).
|
||||
|
||||
Panels:
|
||||
1. Price + Trade Markers (candlestick with buy/sell markers)
|
||||
2. Bid/Ask Spread Dynamics (spread bps over time)
|
||||
3. Top-of-Book Depth (bid size vs ask size stacked area)
|
||||
4. Microprice vs Mid (two lines with deviation fill)
|
||||
5. Order Flow Imbalance (OBI/OFI panel)
|
||||
6. VPIN Toxicity (with threshold bands)
|
||||
7. Inventory + PnL (from simulation results)
|
||||
8. Markout Analysis (buy vs sell markout curves)
|
||||
9. Event Timeline (fills, cancels, re-quotes)
|
||||
|
||||
Usage:
|
||||
python -m backtests.tick_viz --coin BTC --start 2026-08-01 --end 2026-08-02
|
||||
python -m backtests.tick_viz --db data/normalized/ftdt_tick.db --dashboard
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import argparse
|
||||
import logging
|
||||
import sys
|
||||
from pathlib import Path
|
||||
from typing import Optional
|
||||
|
||||
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
REPORT_DIR = Path(__file__).resolve().parent.parent / "backtests" / "reports"
|
||||
REPORT_DIR.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
|
||||
def load_tick_dataframe(
|
||||
data_dir: str,
|
||||
coin: str,
|
||||
start_date: str,
|
||||
end_date: str,
|
||||
) -> tuple[pd.DataFrame, pd.DataFrame]:
|
||||
"""Load L2 snapshots and trades from Parquet as pandas DataFrames."""
|
||||
from data.store import read_range
|
||||
|
||||
l2_msgs = read_range(data_dir, "l2book", coin.upper(), start_date, end_date)
|
||||
trade_msgs = read_range(data_dir, "trades", coin.upper(), start_date, end_date)
|
||||
|
||||
l2_rows = []
|
||||
for msg in l2_msgs:
|
||||
payload = msg.get("payload", {})
|
||||
levels = payload.get("levels", [])
|
||||
msg_type = payload.get("type", "snapshot")
|
||||
|
||||
if msg_type == "snapshot" and isinstance(levels, list) and len(levels) >= 2:
|
||||
bids = {}
|
||||
asks = {}
|
||||
for bid in levels[0]:
|
||||
sz = float(bid.get("sz", 0))
|
||||
if sz > 0:
|
||||
bids[float(bid["px"])] = sz
|
||||
for ask in levels[1]:
|
||||
sz = float(ask.get("sz", 0))
|
||||
if sz > 0:
|
||||
asks[float(ask["px"])] = sz
|
||||
|
||||
if bids and asks:
|
||||
bid_prices = sorted(bids.keys(), reverse=True)
|
||||
ask_prices = sorted(asks.keys())
|
||||
bb = bid_prices[0]
|
||||
ba = ask_prices[0]
|
||||
mid = (bb + ba) / 2.0
|
||||
bd10 = sum(bids[px] for px in bid_prices[:10])
|
||||
ad10 = sum(asks[px] for px in ask_prices[:10])
|
||||
depth_total = bd10 + ad10
|
||||
obi = (bd10 - ad10) / depth_total if depth_total > 0 else 0.0
|
||||
w = bd10 / depth_total if depth_total > 0 else 0.5
|
||||
micro = w * bb + (1 - w) * ba
|
||||
spread_bps = (ba - bb) / mid * 10000 if mid > 0 else 0
|
||||
|
||||
l2_rows.append({
|
||||
"ts": msg.get("local_ts", 0.0),
|
||||
"exchange_ts_ms": msg.get("exchange_ts", 0) or 0,
|
||||
"best_bid": bb,
|
||||
"best_ask": ba,
|
||||
"mid": mid,
|
||||
"microprice": micro,
|
||||
"obi": obi,
|
||||
"spread_bps": spread_bps,
|
||||
"bid_depth_10": bd10,
|
||||
"ask_depth_10": ad10,
|
||||
})
|
||||
|
||||
trade_rows = []
|
||||
for msg in trade_msgs:
|
||||
payload = msg.get("payload", {})
|
||||
px = float(payload.get("px", 0))
|
||||
sz = float(payload.get("sz", 0))
|
||||
if px <= 0 or sz <= 0:
|
||||
continue
|
||||
side = str(payload.get("side", "?"))
|
||||
trade_rows.append({
|
||||
"ts": msg.get("local_ts", 0.0),
|
||||
"exchange_ts_ms": msg.get("exchange_ts", 0) or 0,
|
||||
"price": px,
|
||||
"size": sz,
|
||||
"side": side,
|
||||
"aggressor": "buy" if side.upper() in ("B", "BUY") else "sell",
|
||||
})
|
||||
|
||||
l2_df = pd.DataFrame(l2_rows)
|
||||
trade_df = pd.DataFrame(trade_rows)
|
||||
|
||||
if not l2_df.empty:
|
||||
l2_df = l2_df.sort_values("ts").reset_index(drop=True)
|
||||
if not trade_df.empty:
|
||||
trade_df = trade_df.sort_values("ts").reset_index(drop=True)
|
||||
|
||||
logger.info("Loaded %d L2 snapshots, %d trades", len(l2_df), len(trade_df))
|
||||
return l2_df, trade_df
|
||||
|
||||
|
||||
def plot_price_with_trades(
|
||||
l2_df: pd.DataFrame,
|
||||
trade_df: pd.DataFrame,
|
||||
title: str = "Price + Trade Markers",
|
||||
):
|
||||
"""Candlestick-style price with buy/sell trade markers."""
|
||||
import plotly.graph_objects as go
|
||||
|
||||
if l2_df.empty:
|
||||
return go.Figure()
|
||||
|
||||
sample = l2_df
|
||||
if len(l2_df) > 5000:
|
||||
sample = l2_df.iloc[np.linspace(0, len(l2_df) - 1, 5000).astype(int)]
|
||||
|
||||
fig = go.Figure()
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=sample["ts"].values - sample["ts"].iloc[0],
|
||||
y=sample["mid"].values,
|
||||
mode="lines",
|
||||
name="Mid Price",
|
||||
line=dict(color="#1f77b4", width=1),
|
||||
)
|
||||
)
|
||||
|
||||
if not trade_df.empty:
|
||||
trade_sample = trade_df
|
||||
if len(trade_df) > 2000:
|
||||
trade_sample = trade_df.iloc[np.linspace(0, len(trade_df) - 1, 2000).astype(int)]
|
||||
|
||||
buys = trade_sample[trade_sample["aggressor"] == "buy"]
|
||||
sells = trade_sample[trade_sample["aggressor"] == "sell"]
|
||||
|
||||
t0 = l2_df["ts"].iloc[0] if not l2_df.empty else 0
|
||||
if not buys.empty:
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=buys["ts"].values - t0, y=buys["price"].values,
|
||||
mode="markers", name="Buy", marker=dict(symbol="triangle-up",
|
||||
size=4, color="#2ca02c", opacity=0.6),
|
||||
)
|
||||
)
|
||||
if not sells.empty:
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=sells["ts"].values - t0, y=sells["price"].values,
|
||||
mode="markers", name="Sell", marker=dict(symbol="triangle-down",
|
||||
size=4, color="#d62728", opacity=0.6),
|
||||
)
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
xaxis_title="Time (seconds from start)",
|
||||
yaxis_title="Price",
|
||||
template="plotly_white",
|
||||
hovermode="x unified",
|
||||
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
|
||||
margin=dict(l=60, r=30, t=50, b=60),
|
||||
)
|
||||
return fig
|
||||
|
||||
|
||||
def plot_spread_dynamics(
|
||||
l2_df: pd.DataFrame,
|
||||
title: str = "Bid/Ask Spread Dynamics",
|
||||
):
|
||||
"""Spread in bps over time."""
|
||||
import plotly.graph_objects as go
|
||||
|
||||
if l2_df.empty:
|
||||
return go.Figure()
|
||||
|
||||
sample = l2_df
|
||||
if len(l2_df) > 5000:
|
||||
step = max(1, len(l2_df) // 5000)
|
||||
sample = l2_df.iloc[::step]
|
||||
|
||||
fig = go.Figure()
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=sample["ts"].values - l2_df["ts"].iloc[0],
|
||||
y=sample["spread_bps"].values,
|
||||
mode="lines",
|
||||
name="Spread (bps)",
|
||||
line=dict(color="#9467bd", width=1),
|
||||
fill="tozeroy",
|
||||
fillcolor="rgba(148,103,189,0.08)",
|
||||
)
|
||||
)
|
||||
|
||||
avg_spread = l2_df["spread_bps"].mean()
|
||||
fig.add_hline(y=avg_spread, line=dict(color="#7f7f7f", width=0.5, dash="dash"))
|
||||
fig.add_annotation(
|
||||
x=0.02, y=avg_spread, xref="paper",
|
||||
text=f"Avg: {avg_spread:.1f} bps",
|
||||
showarrow=False, bgcolor="white", bordercolor="#ccc",
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
xaxis_title="Time (seconds from start)",
|
||||
yaxis_title="Spread (bps)",
|
||||
template="plotly_white",
|
||||
hovermode="x unified",
|
||||
margin=dict(l=60, r=30, t=50, b=60),
|
||||
)
|
||||
return fig
|
||||
|
||||
|
||||
def plot_depth_panel(
|
||||
l2_df: pd.DataFrame,
|
||||
title: str = "Top-of-Book Depth",
|
||||
):
|
||||
"""Bid depth vs ask depth stacked area chart."""
|
||||
import plotly.graph_objects as go
|
||||
|
||||
if l2_df.empty:
|
||||
return go.Figure()
|
||||
|
||||
sample = l2_df
|
||||
if len(l2_df) > 5000:
|
||||
step = max(1, len(l2_df) // 5000)
|
||||
sample = l2_df.iloc[::step]
|
||||
|
||||
fig = go.Figure()
|
||||
t = sample["ts"].values - l2_df["ts"].iloc[0]
|
||||
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=t, y=sample["bid_depth_10"].values,
|
||||
mode="lines", name="Bid Depth", line=dict(color="#2ca02c", width=1),
|
||||
stackgroup="one",
|
||||
)
|
||||
)
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=t, y=sample["ask_depth_10"].values,
|
||||
mode="lines", name="Ask Depth", line=dict(color="#d62728", width=1),
|
||||
stackgroup="one",
|
||||
)
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
xaxis_title="Time (seconds from start)",
|
||||
yaxis_title="Depth (size)",
|
||||
template="plotly_white",
|
||||
hovermode="x unified",
|
||||
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
|
||||
margin=dict(l=60, r=30, t=50, b=60),
|
||||
)
|
||||
return fig
|
||||
|
||||
|
||||
def plot_microprice_vs_mid(
|
||||
l2_df: pd.DataFrame,
|
||||
title: str = "Microprice vs Mid Price",
|
||||
):
|
||||
"""Microprice vs mid with deviation fill."""
|
||||
import plotly.graph_objects as go
|
||||
|
||||
if l2_df.empty:
|
||||
return go.Figure()
|
||||
|
||||
sample = l2_df
|
||||
if len(l2_df) > 5000:
|
||||
step = max(1, len(l2_df) // 5000)
|
||||
sample = l2_df.iloc[::step]
|
||||
|
||||
t = sample["ts"].values - l2_df["ts"].iloc[0]
|
||||
|
||||
fig = go.Figure()
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=t, y=sample["mid"].values,
|
||||
mode="lines", name="Mid Price", line=dict(color="#7f7f7f", width=1, dash="dot"),
|
||||
)
|
||||
)
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=t, y=sample["microprice"].values,
|
||||
mode="lines", name="Microprice", line=dict(color="#1f77b4", width=1.5),
|
||||
fill="tonexty", fillcolor="rgba(31,119,180,0.1)",
|
||||
)
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
xaxis_title="Time (seconds from start)",
|
||||
yaxis_title="Price",
|
||||
template="plotly_white",
|
||||
hovermode="x unified",
|
||||
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
|
||||
margin=dict(l=60, r=30, t=50, b=60),
|
||||
)
|
||||
return fig
|
||||
|
||||
|
||||
def plot_obi_panel(
|
||||
l2_df: pd.DataFrame,
|
||||
title: str = "Order Flow Imbalance",
|
||||
):
|
||||
"""OBI with zero line."""
|
||||
import plotly.graph_objects as go
|
||||
|
||||
if l2_df.empty:
|
||||
return go.Figure()
|
||||
|
||||
sample = l2_df
|
||||
if len(l2_df) > 5000:
|
||||
step = max(1, len(l2_df) // 5000)
|
||||
sample = l2_df.iloc[::step]
|
||||
|
||||
fig = go.Figure()
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=sample["ts"].values - l2_df["ts"].iloc[0],
|
||||
y=sample["obi"].values,
|
||||
mode="lines",
|
||||
name="OBI",
|
||||
line=dict(color="#ff7f0e", width=1),
|
||||
fill="tozeroy",
|
||||
fillcolor="rgba(255,127,14,0.08)",
|
||||
)
|
||||
)
|
||||
fig.add_hline(y=0, line=dict(color="black", width=0.5))
|
||||
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
xaxis_title="Time (seconds from start)",
|
||||
yaxis_title="Imbalance [-1, 1]",
|
||||
template="plotly_white",
|
||||
hovermode="x unified",
|
||||
margin=dict(l=60, r=30, t=50, b=60),
|
||||
)
|
||||
return fig
|
||||
|
||||
|
||||
def plot_vpin_panel(
|
||||
l2_df: pd.DataFrame,
|
||||
threshold: float = 0.30,
|
||||
alarm: float = 0.50,
|
||||
title: str = "VPIN Toxicity",
|
||||
):
|
||||
"""VPIN computed from rolling buy/sell volume in OBI data."""
|
||||
import plotly.graph_objects as go
|
||||
|
||||
if l2_df.empty or len(l2_df) < 50:
|
||||
return go.Figure()
|
||||
|
||||
sample = l2_df
|
||||
if len(l2_df) > 10000:
|
||||
step = max(1, len(l2_df) // 10000)
|
||||
sample = l2_df.iloc[::step]
|
||||
|
||||
vpin_vals = []
|
||||
for i in range(50, len(sample)):
|
||||
window = sample.iloc[i - 50:i]
|
||||
bid_d = window["bid_depth_10"].sum()
|
||||
ask_d = window["ask_depth_10"].sum()
|
||||
total = bid_d + ask_d
|
||||
vpin_vals.append(abs(bid_d - ask_d) / total if total > 0 else 0)
|
||||
|
||||
if len(vpin_vals) < 2:
|
||||
return go.Figure()
|
||||
|
||||
t = sample["ts"].iloc[50:].values - l2_df["ts"].iloc[0]
|
||||
|
||||
fig = go.Figure()
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=t, y=vpin_vals,
|
||||
mode="lines", name="VPIN", line=dict(color="#d62728", width=1),
|
||||
fill="tozeroy", fillcolor="rgba(214,39,40,0.08)",
|
||||
)
|
||||
)
|
||||
|
||||
fig.add_hline(y=threshold, line=dict(color="#ff7f0e", width=0.5, dash="dash"),
|
||||
annotation_text=f"Threshold ({threshold})")
|
||||
fig.add_hline(y=alarm, line=dict(color="#d62728", width=0.5, dash="dash"),
|
||||
annotation_text=f"Alarm ({alarm})")
|
||||
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
xaxis_title="Time (seconds from start)",
|
||||
yaxis_title="VPIN",
|
||||
template="plotly_white",
|
||||
hovermode="x unified",
|
||||
margin=dict(l=60, r=30, t=50, b=60),
|
||||
)
|
||||
return fig
|
||||
|
||||
|
||||
def plot_markout_curves(
|
||||
l2_df: pd.DataFrame,
|
||||
trade_df: pd.DataFrame,
|
||||
title: str = "Markout Analysis",
|
||||
):
|
||||
"""Buy vs sell markout at multiple horizons."""
|
||||
import plotly.graph_objects as go
|
||||
|
||||
if l2_df.empty or trade_df.empty:
|
||||
return go.Figure()
|
||||
|
||||
horizons = [100, 500, 1000, 5000, 10000, 30000, 60000]
|
||||
buy_means = []
|
||||
sell_means = []
|
||||
|
||||
l2_times = l2_df["ts"].values if "ts" in l2_df else l2_df["exchange_ts_ms"].values / 1000.0
|
||||
mids = l2_df["mid"].values
|
||||
|
||||
for horizon in horizons:
|
||||
horizon_s = horizon / 1000.0
|
||||
b_mark = []
|
||||
s_mark = []
|
||||
for _, trade in trade_df.iterrows():
|
||||
trade_ts = trade["ts"] if "ts" in trade else trade["exchange_ts_ms"] / 1000.0
|
||||
px = trade["price"]
|
||||
agg = trade["aggressor"]
|
||||
future_idx = np.searchsorted(l2_times, trade_ts + horizon_s)
|
||||
if future_idx < len(mids):
|
||||
mid_before = px
|
||||
mid_after = mids[future_idx]
|
||||
if mid_before > 0:
|
||||
markout = (mid_after - mid_before) / mid_before * 10000
|
||||
if agg == "buy":
|
||||
b_mark.append(markout)
|
||||
else:
|
||||
s_mark.append(markout)
|
||||
buy_means.append(np.mean(b_mark) if b_mark else 0)
|
||||
sell_means.append(np.mean(s_mark) if s_mark else 0)
|
||||
|
||||
fig = go.Figure()
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=horizons, y=buy_means, mode="lines+markers",
|
||||
name="After Buy", line=dict(color="#2ca02c", width=2),
|
||||
)
|
||||
)
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=horizons, y=sell_means, mode="lines+markers",
|
||||
name="After Sell", line=dict(color="#d62728", width=2),
|
||||
)
|
||||
)
|
||||
fig.add_hline(y=0, line=dict(color="black", width=0.5))
|
||||
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
xaxis_title="Horizon (ms)",
|
||||
yaxis_title="Mean Markout (bps)",
|
||||
xaxis_type="log",
|
||||
template="plotly_white",
|
||||
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
|
||||
margin=dict(l=60, r=30, t=50, b=60),
|
||||
)
|
||||
return fig
|
||||
|
||||
|
||||
def plot_event_timeline(
|
||||
tick_result: dict,
|
||||
title: str = "Simulation Event Timeline",
|
||||
):
|
||||
"""Fills, cancels, and key events from tick runner result."""
|
||||
import plotly.graph_objects as go
|
||||
|
||||
equity = tick_result.get("equity_curve", [])
|
||||
if not equity:
|
||||
return go.Figure()
|
||||
|
||||
times = [p.get("t", i) for i, p in enumerate(equity)]
|
||||
values = [p.get("v", 0) for p in equity]
|
||||
|
||||
fig = go.Figure()
|
||||
fig.add_trace(
|
||||
go.Scatter(
|
||||
x=times, y=values, mode="lines",
|
||||
name="Equity", line=dict(color="#1f77b4", width=1.5),
|
||||
fill="tozeroy", fillcolor="rgba(31,119,180,0.05)",
|
||||
)
|
||||
)
|
||||
|
||||
n_trades = tick_result.get("total_trades", 0)
|
||||
toxic = tick_result.get("toxic_fills", 0)
|
||||
cancels = tick_result.get("cancels", 0)
|
||||
net_pnl = tick_result.get("pnl_breakdown", {}).get("net_pnl", 0)
|
||||
|
||||
fig.add_annotation(
|
||||
x=0.98, y=0.95, xref="paper", yref="paper",
|
||||
text=f"Trades: {n_trades}<br>Toxic: {toxic}<br>Cancels: {cancels}<br>"
|
||||
f"Net PnL: ${net_pnl:.4f}",
|
||||
showarrow=False, bgcolor="white", bordercolor="#ccc",
|
||||
xanchor="right", yanchor="top",
|
||||
)
|
||||
|
||||
fig.update_layout(
|
||||
title=title,
|
||||
xaxis_title="Time",
|
||||
yaxis_title="Equity ($)",
|
||||
template="plotly_white",
|
||||
hovermode="x unified",
|
||||
margin=dict(l=60, r=30, t=50, b=60),
|
||||
)
|
||||
return fig
|
||||
|
||||
|
||||
def tick_dashboard(
|
||||
l2_df: pd.DataFrame,
|
||||
trade_df: pd.DataFrame,
|
||||
tick_result: Optional[dict] = None,
|
||||
coin: str = "BTC",
|
||||
vpin_threshold: float = 0.30,
|
||||
vpin_alarm: float = 0.50,
|
||||
):
|
||||
"""Full 9-panel HFT dashboard."""
|
||||
figs = []
|
||||
|
||||
figs.append(plot_price_with_trades(l2_df, trade_df,
|
||||
title=f"{coin} — Price & Trade Markers"))
|
||||
figs.append(plot_spread_dynamics(l2_df,
|
||||
title=f"{coin} — Spread Dynamics"))
|
||||
figs.append(plot_depth_panel(l2_df,
|
||||
title=f"{coin} — Top-of-Book Depth"))
|
||||
figs.append(plot_microprice_vs_mid(l2_df,
|
||||
title=f"{coin} — Microprice vs Mid"))
|
||||
figs.append(plot_obi_panel(l2_df,
|
||||
title=f"{coin} — Order Flow Imbalance"))
|
||||
figs.append(plot_vpin_panel(l2_df, vpin_threshold, vpin_alarm,
|
||||
title=f"{coin} — VPIN Toxicity"))
|
||||
|
||||
if tick_result:
|
||||
figs.append(plot_event_timeline(tick_result,
|
||||
title=f"{coin} — PnL Timeline"))
|
||||
|
||||
figs.append(plot_markout_curves(l2_df, trade_df,
|
||||
title=f"{coin} — Markout Analysis"))
|
||||
|
||||
return figs
|
||||
|
||||
|
||||
def save_tick_dashboard(
|
||||
l2_df: pd.DataFrame,
|
||||
trade_df: pd.DataFrame,
|
||||
tick_result: Optional[dict] = None,
|
||||
coin: str = "BTC",
|
||||
output_dir: str = "",
|
||||
) -> str:
|
||||
"""Save the full tick dashboard as a standalone HTML file."""
|
||||
import plotly.io as pio
|
||||
from datetime import datetime, timezone
|
||||
|
||||
figs = tick_dashboard(l2_df, trade_df, tick_result, coin)
|
||||
ts = datetime.now(timezone.utc).strftime("%Y%m%d-%H%M%S")
|
||||
fname = f"tick_dashboard_{coin}_{ts}.html"
|
||||
out = Path(output_dir) if output_dir else REPORT_DIR
|
||||
out.mkdir(parents=True, exist_ok=True)
|
||||
fpath = out / fname
|
||||
|
||||
html_parts = ["<html><head>",
|
||||
f"<title>HFT Dashboard — {coin}</title>",
|
||||
"<style>body{font-family:system-ui,sans-serif;max-width:1600px;"
|
||||
"margin:0 auto;padding:20px;background:#1a1a2e;color:#eee;}"
|
||||
".chart{margin:20px 0;background:#16213e;border-radius:8px;"
|
||||
"box-shadow:0 2px 8px rgba(0,0,0,0.3);padding:10px;}"
|
||||
"h1{color:#e94560;}</style>",
|
||||
"</head><body>",
|
||||
f"<h1>HFT Microstructure Dashboard — {coin}</h1>",
|
||||
f"<p>{ts}</p>"]
|
||||
for i, fig in enumerate(figs):
|
||||
html_parts.append(f'<div class="chart">')
|
||||
html_parts.append(pio.to_html(fig, include_plotlyjs="cdn", full_html=False))
|
||||
html_parts.append("</div>")
|
||||
html_parts.append("</body></html>")
|
||||
|
||||
with open(fpath, "w") as f:
|
||||
f.write("\n".join(html_parts))
|
||||
|
||||
logger.info("Tick dashboard saved to %s", fpath)
|
||||
return str(fpath)
|
||||
|
||||
|
||||
def cmd_tick_viz(args):
|
||||
"""CLI entry: load data and generate dashboard."""
|
||||
l2_df, trade_df = load_tick_dataframe(
|
||||
args.data_dir, args.coin, args.start_date, args.end_date,
|
||||
)
|
||||
|
||||
if l2_df.empty:
|
||||
print("No L2 data available. Run 'python -m cli collect --mainnet' first.")
|
||||
return
|
||||
|
||||
tick_result = None
|
||||
if args.tick_result:
|
||||
import json
|
||||
try:
|
||||
with open(args.tick_result) as f:
|
||||
tick_result = json.load(f)
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
fpath = save_tick_dashboard(
|
||||
l2_df, trade_df, tick_result, args.coin,
|
||||
output_dir=args.output_dir,
|
||||
)
|
||||
print(f"Dashboard: {fpath}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
p = argparse.ArgumentParser(description="HFT Tick Visualization")
|
||||
p.add_argument("--data-dir", default="data/raw")
|
||||
p.add_argument("--coin", default="BTC")
|
||||
p.add_argument("--start-date", default="2026-08-01")
|
||||
p.add_argument("--end-date", default="2026-08-02")
|
||||
p.add_argument("--output-dir", default="")
|
||||
p.add_argument("--tick-result", help="Path to tick_runner JSON result for PnL panel")
|
||||
args = p.parse_args()
|
||||
|
||||
logging.basicConfig(level=logging.INFO, format="%(asctime)s %(message)s", datefmt="%H:%M:%S")
|
||||
cmd_tick_viz(args)
|
||||
Reference in New Issue
Block a user