quant-ai / scripts /render_lob_wave_terminal_preview.py
Ypeng12's picture
feat(ai): restructure repository layout into clean docs and scripts directories
7594fe3
Raw History Blame Contribute Delete
11.9 kB
# render_lob_wave_terminal_preview.py
"""
Generates an institutional-grade, high-resolution dark-mode preview image of the
LOB Microstructure Wave Alpha Terminal for direct display on GitHub README and documentation.
"""
import os
import sys
import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.patches as patches
script_dir = os.path.dirname(os.path.abspath(__file__))
project_root = os.path.dirname(script_dir)
backend_dir = os.path.join(project_root, "backend")
if backend_dir not in sys.path:
sys.path.insert(0, backend_dir)
from app.ml.lob_microstructure_ml import MicrostructureWaveAlphaEngine
from app.alpha_engine import InstitutionalAlphaEngine
def render_terminal_preview():
# Load model and TSLA 5m data
wave_engine = MicrostructureWaveAlphaEngine.load()
alpha_engine = InstitutionalAlphaEngine()
fpath = os.path.join(backend_dir, "data", "datasets", "advanced_dataset_TSLA.parquet")
df = pd.read_parquet(fpath)
for col in ['open', 'high', 'low', 'close', 'volume']:
if col in df.columns and col.capitalize() not in df.columns:
df[col.capitalize()] = df[col]
df_5m = df.resample('5min').agg({
'Open': 'first', 'High': 'max', 'Low': 'min', 'Close': 'last', 'Volume': 'sum'
}).dropna()
df_5m['hour'] = df_5m.index.hour
df_5m['min'] = df_5m.index.minute
df_5m = df_5m[(df_5m['hour'] > 9) | ((df_5m['hour'] == 9) & (df_5m['min'] >= 30))]
df_5m = df_5m[(df_5m['hour'] < 16) | ((df_5m['hour'] == 16) & (df_5m['min'] == 0))]
df_5m['day'] = df_5m.index.strftime('%Y-%m-%d')
latest_day = df_5m['day'].iloc[-1]
day_df = df_5m[df_5m['day'] == latest_day].copy()
# Extract features & predict
df_feat = wave_engine.build_microstructure_features(day_df)
X = df_feat[wave_engine.FEATURE_COLS].fillna(0.0)
p_long_all = wave_engine.model.predict_proba(X)[:, 1]
times = day_df.index.strftime('%H:%M').tolist()
closes = day_df['Close'].values
highs = day_df['High'].values
lows = day_df['Low'].values
opens = day_df['Open'].values
ema9 = day_df['Close'].ewm(span=9, adjust=False).mean().values
ema21 = day_df['Close'].ewm(span=21, adjust=False).mean().values
pv = (day_df['Close'] * day_df['Volume']).cumsum()
vwap = (pv / day_df['Volume'].cumsum().replace(0, 1.0)).values
ofi = df_feat['feature_ofi'].values
micro_drift = df_feat['feature_micro_drift_bps'].values if 'feature_micro_drift_bps' in df_feat.columns else df_feat['feature_micro_drift'].values * 100.0
queue_imb = df_feat['feature_queue_imbalance'].values
sweep_vel = df_feat['feature_sweep_vel'].values
comp_scores = []
signals = []
last_sig_idx = -999
last_sig_dir = None
for i in range(len(day_df)):
row = day_df.iloc[i].to_dict()
prev_row = day_df.iloc[i-1].to_dict() if i > 0 else None
p_l = float(p_long_all[i])
a_eval = alpha_engine.evaluate_composite_alpha(row, prev_row, ml_p_win_long=p_l)
score = float(a_eval.get('composite_alpha_score', 0.0))
comp_scores.append(score)
p_s = 1.0 - p_l
cur_dir = None
if score > 15 and p_l >= 0.54:
cur_dir = 'LONG'
elif score < -15 and p_s >= 0.54:
cur_dir = 'SHORT'
if cur_dir is not None:
if cur_dir != last_sig_dir or (i - last_sig_idx >= 5):
signals.append({
'index': i,
'direction': cur_dir,
'price': closes[i],
'high': highs[i],
'low': lows[i],
'p_win': round(p_l * 100 if cur_dir == 'LONG' else p_s * 100, 1),
'alpha': round(score, 1)
})
last_sig_idx = i
last_sig_dir = cur_dir
# Plotting setup with high-end dark theme & macOS CJK font support
plt.style.use('dark_background')
plt.rcParams['font.sans-serif'] = ['PingFang SC', 'Heiti SC', 'STHeiti', 'Arial Unicode MS', 'DejaVu Sans']
plt.rcParams['axes.unicode_minus'] = False
fig = plt.figure(figsize=(16, 12), dpi=200, facecolor='#080a11')
# Custom layout
gs = fig.add_gridspec(5, 1, height_ratios=[0.5, 2.5, 1.0, 1.0, 1.0], hspace=0.35)
# 0. Title & KPI Header Ax
ax_header = fig.add_subplot(gs[0])
ax_header.set_facecolor('#0f1422')
ax_header.axis('off')
# Rounded border rectangle for header
rect = patches.FancyBboxPatch((0.01, 0.05), 0.98, 0.90, boxstyle="round,pad=0.02,rounding_size=0.03",
facecolor='#0f1422', edgecolor='#1e293b', transform=ax_header.transAxes)
ax_header.add_patch(rect)
title_text = "LOB 订单流微观结构波浪研判终端 (Microstructure Wave Alpha Terminal)"
sub_text = f"Ticker: TSLA | Date: {latest_day} | Out-of-Sample Purged CV: 72.07% | Teza Capital ($1.6B AUM, Sharpe > 5.0) 15~30m Wave"
ax_header.text(0.03, 0.65, title_text, fontsize=14, fontweight='bold', color='#38bdf8', transform=ax_header.transAxes)
ax_header.text(0.03, 0.25, sub_text, fontsize=9.5, color='#94a3b8', transform=ax_header.transAxes)
open_p, close_p = opens[0], closes[-1]
pnl_pct = (close_p - open_p) / open_p * 100
pnl_color = '#10b981' if pnl_pct >= 0 else '#f43f5e'
kpi_str = f"Open: {open_p:.2f} USD • Close: {close_p:.2f} USD • Day PnL: {pnl_pct:+.2f}% • Wave Signals: {len(signals)} • Continuous Alpha"
ax_header.text(0.97, 0.45, kpi_str, fontsize=10, fontweight='bold', color=pnl_color, ha='right', transform=ax_header.transAxes)
# 1. K-line and Wave Signals
ax_kline = fig.add_subplot(gs[1])
ax_kline.set_facecolor('#0f1422')
x_indices = np.arange(len(day_df))
# Draw Candlesticks
width = 0.55
for i in range(len(day_df)):
o, c, h, l = opens[i], closes[i], highs[i], lows[i]
col = '#10b981' if c >= o else '#f43f5e'
# Wick
ax_kline.plot([i, i], [l, h], color=col, linewidth=1.2, zorder=2)
# Body
body_bottom = min(o, c)
body_height = max(abs(c - o), 0.08)
rect = patches.Rectangle((i - width/2, body_bottom), width, body_height, facecolor=col, edgecolor=col, zorder=3)
ax_kline.add_patch(rect)
# Technical lines
ax_kline.plot(x_indices, ema9, label='EMA 9', color='#f59e0b', linewidth=1.3, alpha=0.9, zorder=4)
ax_kline.plot(x_indices, ema21, label='EMA 21', color='#38bdf8', linewidth=1.3, alpha=0.9, zorder=4)
ax_kline.plot(x_indices, vwap, label='VWAP', color='#c084fc', linewidth=1.6, linestyle='--', alpha=0.85, zorder=4)
# Draw Debounced Wave Markers
for sig in signals:
idx = sig['index']
is_long = sig['direction'] == 'LONG'
marker = '^' if is_long else 'v'
sig_color = '#10b981' if is_long else '#f43f5e'
y_pos = lows[idx] - 0.75 if is_long else highs[idx] + 0.75
va = 'top' if is_long else 'bottom'
ax_kline.scatter(idx, y_pos, marker=marker, s=110, color=sig_color, edgecolor='#ffffff', linewidth=1.2, zorder=5)
lbl = f"{'Long' if is_long else 'Short'} {sig['p_win']:.0f}%"
ax_kline.text(idx, y_pos + (-0.55 if is_long else 0.55), lbl, color='#ffffff', fontsize=8, fontweight='bold',
ha='center', va=va, bbox=dict(boxstyle="round,pad=0.2", facecolor=sig_color, alpha=0.85, edgecolor='none'), zorder=6)
ax_kline.set_ylabel('Price (USD)', fontsize=10, color='#94a3b8')
ax_kline.set_title('TSLA 5-Minute Bars & 15-30m Wave Turning Signals (Wave Long / Short - Debounced)', fontsize=11, fontweight='bold', color='#f8fafc', loc='left')
ax_kline.legend(loc='upper right', framealpha=0.4, fontsize=9)
ax_kline.grid(True, linestyle=':', alpha=0.25, color='#334155')
ax_kline.set_xlim(-1, len(day_df))
ax_kline.set_ylim(lows.min() - 1.2, highs.max() + 2.2)
ax_kline.set_xticks([])
# 2. OFI & Microprice Drift
ax_ofi = fig.add_subplot(gs[2])
ax_ofi.set_facecolor('#0f1422')
bar_colors = ['#10b981' if v >= 0 else '#f43f5e' for v in ofi]
ax_ofi.bar(x_indices, ofi, color=bar_colors, width=0.6, alpha=0.8, label='Order Flow Imbalance (OFI)')
ax_drift = ax_ofi.twinx()
ax_drift.plot(x_indices, micro_drift, color='#a855f7', linewidth=1.6, label='Microprice Drift (bps)')
ax_drift.set_ylabel('Drift (bps)', fontsize=9, color='#a855f7')
ax_drift.grid(False)
ax_ofi.set_ylabel('OFI', fontsize=9, color='#94a3b8')
ax_ofi.set_title('[1] Order Flow Imbalance (OFI) & Microprice Drift (bps)', fontsize=10.5, fontweight='bold', color='#f8fafc', loc='left')
ax_ofi.legend(loc='upper left', framealpha=0.4, fontsize=8.5)
ax_drift.legend(loc='upper right', framealpha=0.4, fontsize=8.5)
ax_ofi.grid(True, linestyle=':', alpha=0.25, color='#334155')
ax_ofi.set_xlim(-1, len(day_df))
ax_ofi.set_xticks([])
# 3. Queue Imbalance & Sweep Velocity
ax_q = fig.add_subplot(gs[3])
ax_q.set_facecolor('#0f1422')
ax_q.plot(x_indices, queue_imb, color='#f59e0b', linewidth=1.5, label='Queue Depth Imbalance')
ax_q.plot(x_indices, sweep_vel, color='#38bdf8', linewidth=1.4, alpha=0.85, label='Sweep Velocity')
ax_q.axhline(0, color='#334155', linestyle='--', linewidth=0.8)
ax_q.set_ylabel('Queue / Sweep', fontsize=9, color='#94a3b8')
ax_q.set_title('[2] LOB Queue Depth Imbalance & Institutional Sweep Velocity', fontsize=10.5, fontweight='bold', color='#f8fafc', loc='left')
ax_q.legend(loc='upper right', framealpha=0.4, fontsize=8.5)
ax_q.grid(True, linestyle=':', alpha=0.25, color='#334155')
ax_q.set_xlim(-1, len(day_df))
ax_q.set_xticks([])
# 4. Composite Alpha Score & Wave Win Rate
ax_alpha = fig.add_subplot(gs[4])
ax_alpha.set_facecolor('#0f1422')
ax_alpha.plot(x_indices, comp_scores, color='#38bdf8', linewidth=1.8, label='Composite Alpha Score')
ax_alpha.fill_between(x_indices, 0, comp_scores, color='#38bdf8', alpha=0.2)
ax_alpha.axhline(0, color='#334155', linestyle='--', linewidth=0.8)
ax_p = ax_alpha.twinx()
ax_p.plot(x_indices, p_long_all * 100.0, color='#10b981', linewidth=1.8, label='Wave Long Win Rate (%)')
ax_p.set_ylabel('Win Rate (%)', fontsize=9, color='#10b981')
ax_p.set_ylim(20, 80)
ax_p.grid(False)
ax_alpha.set_ylabel('Alpha Score', fontsize=9, color='#38bdf8')
ax_alpha.set_ylim(-60, 60)
ax_alpha.set_title('[3] Composite Multi-Factor Alpha Score & 15~30m Wave Win Rate (%)', fontsize=10.5, fontweight='bold', color='#f8fafc', loc='left')
ax_alpha.legend(loc='upper left', framealpha=0.4, fontsize=8.5)
ax_p.legend(loc='upper right', framealpha=0.4, fontsize=8.5)
ax_alpha.grid(True, linestyle=':', alpha=0.25, color='#334155')
ax_alpha.set_xlim(-1, len(day_df))
# Format X-axis with sparse time labels
step = max(1, len(times) // 10)
xticks_pos = list(range(0, len(times), step))
xticks_labels = [times[i] for i in xticks_pos]
ax_alpha.set_xticks(xticks_pos)
ax_alpha.set_xticklabels(xticks_labels, rotation=0, fontsize=9, color='#94a3b8')
ax_alpha.set_xlabel('Market Time (EST 09:30 - 16:00)', fontsize=10, color='#94a3b8')
# Output file
out_dir = os.path.join(os.path.dirname(os.path.abspath(__file__)), "assets")
os.makedirs(out_dir, exist_ok=True)
out_png = os.path.join(out_dir, "lob_microstructure_wave_terminal.png")
plt.subplots_adjust(top=0.96, bottom=0.06, left=0.06, right=0.94, hspace=0.32)
fig.savefig(out_png, dpi=200, facecolor='#080a11', edgecolor='none')
plt.close(fig)
print(f"✅ High-resolution preview image successfully rendered at: {out_png}")
if __name__ == "__main__":
render_terminal_preview()