# 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()