""" MarketMind — Gradio Dashboard A premium trading terminal UI for multi-agent financial market simulation. Optimized for Hugging Face Spaces deployment. """ import sys import os import time import json import pandas as pd import numpy as np import plotly.graph_objects as go from plotly.subplots import make_subplots import gradio as gr from datetime import datetime # Ensure imports work from this directory sys.path.insert(0, os.path.dirname(__file__)) from engine.simulation import SimulationEngine, SimulationConfig from agents.momentum_agent import MomentumAgent from agents.mean_reversion_agent import MeanReversionAgent from agents.fundamental_agent import FundamentalAgent from agents.market_maker_agent import MarketMakerAgent from agents.noise_trader import NoiseTrader # ─── AGENT BUILDER ──────────────────────────────────────────────── def build_agents(n_mom, n_mr, n_fund, n_noise, n_mm): agents = [] for i in range(n_mom): agents.append(MomentumAgent(f"momentum_{i+1}")) for i in range(n_mr): agents.append(MeanReversionAgent(f"meanrev_{i+1}")) for i in range(n_fund): agents.append(FundamentalAgent(f"fundamental_{i+1}", fair_value=100.0)) for i in range(n_noise): agents.append(NoiseTrader(f"noise_{i+1}")) for i in range(n_mm): agents.append(MarketMakerAgent(f"marketmaker_{i+1}")) return agents # ─── CHART BUILDERS ─────────────────────────────────────────────── COLORS = { "price": "#00d4ff", "fair_value": "#ff3366", "spread": "#ffaa00", "volume": "#7c4dff", "bg": "rgba(0,0,0,0)", "grid": "rgba(255,255,255,0.04)", "text": "#8892b0", "agents": ["#00d4ff", "#00ff88", "#ff3366", "#ffaa00", "#7c4dff", "#ff6b9d", "#c084fc", "#34d399", "#f87171", "#fbbf24"], } def build_main_chart(ticks_data): """Build the primary price + volume + spread multi-panel chart.""" ticks = [r["tick"] for r in ticks_data] prices = [r["mid_price"] if r["mid_price"] else 100.0 for r in ticks_data] fair_vals = [r.get("true_fair_value", 100.0) for r in ticks_data] spreads = [r["spread"] if r["spread"] else 0.0 for r in ticks_data] volumes = [r["volume"] for r in ticks_data] regimes = [r["regime"] for r in ticks_data] fig = make_subplots( rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.03, row_heights=[0.6, 0.2, 0.2], subplot_titles=None, ) # Price line fig.add_trace(go.Scatter( x=ticks, y=prices, mode="lines", line=dict(color=COLORS["price"], width=2.5), name="Market Price", fill="tozeroy", fillcolor="rgba(0, 212, 255, 0.05)", ), row=1, col=1) # Fair value fig.add_trace(go.Scatter( x=ticks, y=fair_vals, mode="lines", line=dict(color=COLORS["fair_value"], width=1.5, dash="dot"), name="Fair Value", ), row=1, col=1) # Regime color bands regime_colors = {"Efficient": "rgba(0,255,136,0.06)", "Trending": "rgba(255,170,0,0.06)", "Volatile": "rgba(255,51,102,0.06)", "Crashed": "rgba(255,0,0,0.10)"} # Optimized Regime Bands (calculate once) regimes = [t["regime"] for t in ticks_data] ticks = [t["tick"] for t in ticks_data] if regimes: prev_regime = regimes[0] band_start = ticks[0] for i in range(1, len(regimes)): if regimes[i] != prev_regime or i == len(regimes) - 1: fig.add_vrect( x0=band_start, x1=ticks[i], fillcolor=regime_colors.get(prev_regime, "rgba(0,0,0,0)"), layer="below", line_width=0, row=1, col=1, ) band_start = ticks[i] prev_regime = regimes[i] # Volume bars fig.add_trace(go.Bar( x=ticks, y=volumes, marker_color=COLORS["volume"], opacity=0.6, name="Volume", ), row=2, col=1) # Spread fig.add_trace(go.Scatter( x=ticks, y=spreads, mode="lines", line=dict(color=COLORS["spread"], width=2), fill="tozeroy", fillcolor="rgba(255,170,0,0.08)", name="Spread", ), row=3, col=1) # Layout # Add minimal range to avoid the "zoomed in on noise" look prices_only = [t["price"] for t in ticks_data] if ticks_data else [100.0] min_p, max_p = min(prices_only), max(prices_only) if max_p - min_p < 1.0: center = (max_p + min_p) / 2 min_p, max_p = center - 0.5, center + 0.5 fig.update_layout( template="plotly_dark", paper_bgcolor=COLORS["bg"], plot_bgcolor=COLORS["bg"], font=dict(family="JetBrains Mono, monospace", color=COLORS["text"]), height=620, margin=dict(l=50, r=20, t=30, b=30), legend=dict( orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1, bgcolor="rgba(0,0,0,0)", font=dict(size=11), ), showlegend=True, ) # Force Y axis range on the price plot (row 1) fig.update_yaxes(range=[min_p * 0.998, max_p * 1.002], row=1, col=1) # Hide plotly modebar tools for cleaner UI fig.update_layout(modebar_remove=['zoom', 'pan', 'select', 'lasso2d', 'zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d']) for row in range(1, 4): fig.update_xaxes( gridcolor=COLORS["grid"], zeroline=False, showticklabels=(row == 3), row=row, col=1, ) fig.update_yaxes( gridcolor=COLORS["grid"], zeroline=False, row=row, col=1, ) fig.update_yaxes(title_text="Price", row=1, col=1) fig.update_yaxes(title_text="Vol", row=2, col=1) fig.update_yaxes(title_text="Spread", row=3, col=1) fig.update_xaxes(title_text="Tick", row=3, col=1) return fig def build_pnl_chart(pnl_data, agents): """Build the agent PnL leaderboard chart.""" fig = go.Figure() agent_ids = [a.agent_id for a in agents] for idx, aid in enumerate(agent_ids): agent_rows = [r for r in pnl_data if r["agent_id"] == aid] ticks = [r["tick"] for r in agent_rows] pnls = [r["pnl"] for r in agent_rows] fig.add_trace(go.Scatter( x=ticks, y=pnls, mode="lines", line=dict(color=COLORS["agents"][idx % len(COLORS["agents"])], width=2), name=aid, )) fig.update_layout( template="plotly_dark", paper_bgcolor=COLORS["bg"], plot_bgcolor=COLORS["bg"], font=dict(family="JetBrains Mono, monospace", color=COLORS["text"]), height=350, margin=dict(l=50, r=20, t=30, b=30), legend=dict( orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1, bgcolor="rgba(0,0,0,0)", font=dict(size=10), ), yaxis_title="PnL ($)", xaxis_title="Tick", xaxis=dict(gridcolor=COLORS["grid"], zeroline=False), yaxis=dict(gridcolor=COLORS["grid"], zeroline=False), ) # Zero line fig.add_hline(y=0, line_dash="dash", line_color="rgba(255,255,255,0.15)", line_width=1) fig.update_layout(dragmode=False, hovermode="x unified") fig.update_layout(modebar_orientation='h', modebar_remove=['zoom', 'pan', 'select', 'lasso2d', 'zoomIn2d', 'zoomOut2d', 'autoScale2d', 'resetScale2d']) return fig def build_leaderboard(agent_pnl_rows, ticks_data): """Create a pandas dataframe for the agent leaderboard with advanced metrics.""" from engine.metrics import calculate_sharpe_ratio, calculate_max_drawdown, calculate_win_rate if not agent_pnl_rows: return pd.DataFrame() # Map of agent_id -> list of PnL values pnl_map = {} for row in agent_pnl_rows: aid = row["agent_id"] if aid not in pnl_map: pnl_map[aid] = [] pnl_map[aid].append(row["pnl"]) leaderboard_data = [] for aid, pnl_series in pnl_map.items(): final_pnl = pnl_series[-1] sharpe = calculate_sharpe_ratio(pnl_series) mdd = calculate_max_drawdown(pnl_series) wr = calculate_win_rate(pnl_series) leaderboard_data.append({ "Agent ID": aid, "Total PnL": f"${final_pnl:,.2f}", "Sharpe": f"{sharpe:.2f}", "Max DD": f"{mdd:.1%}", "Win Rate": f"{wr:.1%}" }) df = pd.DataFrame(leaderboard_data) if not df.empty: df = df.sort_values(by="Total PnL", ascending=False) return df def build_stats_html(ticks_data, pnl_data, elapsed): """Build the live stats panel as HTML.""" from datetime import datetime if not ticks_data: return "

No data

" last = ticks_data[-1] first_price = ticks_data[0]["mid_price"] or 100.0 last_price = last["mid_price"] or 100.0 pct_change = ((last_price - first_price) / first_price) * 100 total_volume = sum(r["volume"] for r in ticks_data) total_trades = sum(r["trade_count"] for r in ticks_data) avg_spread = np.mean([r["spread"] for r in ticks_data if r["spread"]]) if ticks_data else 0 regime = last.get("regime", "Unknown") timestamp = datetime.now().strftime("%H:%M:%S") regime_colors = { "Efficient": "#00ff88", "Trending": "#ffaa00", "Volatile": "#ff3366", "Crashed": "#ff0000", } rc = regime_colors.get(regime, "#8892b0") return f"""
TOTAL TRADES
{total_trades}
VOLUME
{total_volume:,}
AVG SPREAD
{avg_spread:.4f}
LAST UPDATE
{datetime.now().strftime('%H:%M:%S')}
""" # ─── SIMULATION RUNNER ──────────────────────────────────────────── def run_simulation(n_mom, n_mr, n_fund, n_noise, n_mm, num_ticks, warmup_ticks, volatility, use_llm, api_key, hf_model, vllm_url, progress=gr.Progress()): """Run the full simulation and return all visualization components.""" print(f"DEBUG: Starting simulation - LLM: {use_llm}, URL: {vllm_url}") if use_llm and not api_key.strip(): # Only require API key if not a local address is_local = "localhost" in vllm_url or "127.0.0.1" in vllm_url or "0.0.0.0" in vllm_url if not is_local: raise gr.Error("API Key is required when Live LLM Mode is enabled for remote providers.") agents = build_agents(int(n_mom), int(n_mr), int(n_fund), int(n_noise), int(n_mm)) if not agents: raise gr.Error("Add at least one agent to run the simulation.") config = SimulationConfig( num_ticks=int(num_ticks), initial_price=100.0, use_llm=use_llm, vllm_base_url=vllm_url if vllm_url else "https://api-inference.huggingface.co/v1", vllm_model=hf_model if hf_model else "Qwen/Qwen2.5-7B-Instruct", vllm_api_key=api_key if api_key else "EMPTY", log_to_csv=False, base_volatility=volatility, warmup_ticks=int(warmup_ticks), enable_seed_liquidity=True, fee_per_trade=0.01 ) engine = SimulationEngine(agents, config) try: t0 = time.time() # Ensure output directory exists for CSV generation os.makedirs(config.output_dir, exist_ok=True) # Generator for real-time updates print(f"DEBUG: Executing simulation loop - LLM Mode: {use_llm}") for tick in engine.run_generator(): is_llm_tick = use_llm and tick > int(warmup_ticks) # LinePlot streams perfectly, so we can yield every tick without flickering! if True: ticks_data = engine.csv_rows pnl_data = engine.agent_pnl_rows if ticks_data: import pandas as pd raw_df = pd.DataFrame(ticks_data) # Melt price data with human-readable legend labels main_df = raw_df.drop(columns=['price'], errors='ignore').melt( id_vars=['tick'], value_vars=['mid_price', 'true_fair_value'], var_name='metric', value_name='price' ) main_df['metric'] = main_df['metric'].map({ 'mid_price': '📈 Mid Price', 'true_fair_value': '🎯 Fair Value' }) spread_df = raw_df[['tick', 'spread']].copy() volume_df = raw_df[['tick', 'volume']].copy() pnl_df = pd.DataFrame(pnl_data) leaderboard = build_leaderboard(pnl_data, ticks_data) stats_html = build_stats_html(ticks_data, pnl_data, time.time() - t0) api_status = "🟢 API OK" if use_llm and engine.llm_client and engine.llm_client.error_count > 0: api_status = f"🔴 API ERROR ({engine.llm_client.error_count})" # Clean status message for demo (no tick count) status_msg = f"{api_status} | Market Status: {engine.metrics.classify_regime()} | Current Price: ${ticks_data[-1]['mid_price']:.2f}" yield main_df, pnl_df, spread_df, volume_df, leaderboard, stats_html, None, status_msg # Slower sleep (0.2s) for a more cinematic, less 'jittery' feel if not is_llm_tick: time.sleep(0.2) else: time.sleep(0.05) print(f"DEBUG: Simulation complete in {time.time()-t0:.2f}s") # Final build ticks_data = engine.csv_rows pnl_data = engine.agent_pnl_rows import pandas as pd raw_df = pd.DataFrame(ticks_data) main_df = raw_df.drop(columns=['price'], errors='ignore').melt( id_vars=['tick'], value_vars=['mid_price', 'true_fair_value'], var_name='metric', value_name='price' ) main_df['metric'] = main_df['metric'].map({ 'mid_price': '📈 Mid Price', 'true_fair_value': '🎯 Fair Value' }) spread_df = raw_df[['tick', 'spread']].copy() volume_df = raw_df[['tick', 'volume']].copy() pnl_df = pd.DataFrame(pnl_data) leaderboard = build_leaderboard(pnl_data, ticks_data) stats_html = build_stats_html(ticks_data, pnl_data, time.time() - t0) # Create temporary export file export_path = "marketmind_simulation.csv" raw_df.to_csv(export_path, index=False) # After simulation is done, write CSVs engine._write_csvs() status_msg = "✅ Simulation Complete" yield main_df, pnl_df, spread_df, volume_df, leaderboard, stats_html, export_path, status_msg except Exception as e: print(f"CRITICAL ERROR in run_simulation: {str(e)}") import traceback traceback.print_exc() raise gr.Error(f"Simulation Failed: {str(e)}") # ─── CUSTOM CSS ─────────────────────────────────────────────────── CUSTOM_CSS = """ @import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;800&family=JetBrains+Mono:wght@400;500&display=swap'); /* ── Global ─────────────────────────────────────── */ html, body { background: #0a0b10 !important; } .gradio-container { max-width: 100% !important; font-family: 'Inter', sans-serif !important; background: linear-gradient(160deg, #0a0b10 0%, #111827 50%, #0d1117 100%) !important; min-height: 100vh; } .main { background: transparent !important; } footer { display: none !important; } /* ── Top Bar ────────────────────────────────────── */ .title-bar { background: linear-gradient(135deg, rgba(0,212,255,0.08), rgba(124,77,255,0.08)); border: 1px solid rgba(0,212,255,0.12); border-radius: 16px; padding: 24px 32px; margin-bottom: 16px; position: relative; overflow: hidden; } .title-bar::before { content: ''; position: absolute; top: 0; left: 0; right: 0; height: 2px; background: linear-gradient(90deg, #00d4ff, #7c4dff, #ff3366); } .title-bar h1 { margin: 0 0 4px 0; font-size: 2em; font-weight: 800; background: linear-gradient(135deg, #00d4ff 0%, #7c4dff 50%, #ff3366 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; letter-spacing: -1px; } .title-bar p { margin: 0; color: #8892b0; font-size: 0.95em; max-width: 700px; } /* ── Stat Cards ─────────────────────────────────── */ .stat-card { background: rgba(17,24,39,0.7); border: 1px solid rgba(255,255,255,0.06); border-radius: 12px; padding: 14px 16px; text-align: center; } .stat-label { font-family: 'JetBrains Mono', monospace; font-size: 0.65em; color: #5a6785; letter-spacing: 1.5px; text-transform: uppercase; margin-bottom: 4px; } .stat-value { font-family: 'JetBrains Mono', monospace; font-size: 1.3em; font-weight: 600; color: #e2e8f0; } .stat-delta { font-family: 'JetBrains Mono', monospace; font-size: 0.85em; font-weight: 500; } /* ── Panel Sections ─────────────────────────────── */ .panel-header { font-family: 'JetBrains Mono', monospace; font-size: 0.75em; color: #00d4ff; letter-spacing: 2px; text-transform: uppercase; margin: 16px 0 8px 0; padding-bottom: 6px; border-bottom: 1px solid rgba(0,212,255,0.15); } /* ── Gradio Overrides ───────────────────────────── */ .dark .block { background: rgba(17,24,39,0.5) !important; border: 1px solid rgba(255,255,255,0.05) !important; border-radius: 12px !important; } .dark .label-wrap { color: #8892b0 !important; } .dark input, .dark textarea, .dark select { background: rgba(15,20,35,0.8) !important; border: 1px solid rgba(255,255,255,0.08) !important; color: #e2e8f0 !important; border-radius: 8px !important; } .dark .primary { background: linear-gradient(135deg, #00d4ff 0%, #7c4dff 100%) !important; border: none !important; font-weight: 600 !important; letter-spacing: 0.5px !important; transition: all 0.3s ease !important; box-shadow: 0 4px 15px rgba(0,212,255,0.25) !important; } .dark .primary:hover { box-shadow: 0 6px 25px rgba(0,212,255,0.4) !important; transform: translateY(-1px) !important; } .dark table { font-family: 'JetBrains Mono', monospace !important; font-size: 0.85em !important; } /* -- Checkbox Pop -- */ input[type="checkbox"] { appearance: none; -webkit-appearance: none; height: 20px; width: 20px; background-color: rgba(0,212,255,0.1); border: 2px solid #00d4ff !important; border-radius: 4px; cursor: pointer; display: inline-block; position: relative; vertical-align: middle; } input[type="checkbox"]:checked { background-color: #00d4ff !important; box-shadow: 0 0 10px rgba(0,212,255,0.5); } input[type="checkbox"]:checked::after { content: '✓'; position: absolute; color: #0a0b10; font-size: 14px; font-weight: 800; left: 4px; top: -2px; } .dark label span { color: #00d4ff !important; font-weight: 800 !important; letter-spacing: 0.5px; } /* ── Scrollbar ──────────────────────────────────── */ ::-webkit-scrollbar { width: 6px; } ::-webkit-scrollbar-track { background: rgba(0,0,0,0.2); } ::-webkit-scrollbar-thumb { background: rgba(0,212,255,0.3); border-radius: 3px; } """ # ─── GRADIO APP ─────────────────────────────────────────────────── def create_app(): with gr.Blocks( title="MarketMind | Multi-Agent Market Simulation", ) as app: # ── Title Bar ── gr.HTML("""

⚡ MarketMind

Multi-agent financial market simulation powered by LLM agents competing inside a continuous double auction. Adjust the agent composition to discover if the market self-organizes to efficiency — or collapses into chaos.

""") with gr.Row(): # ══════════════════════════════════════════════ # LEFT PANEL — Controls # ══════════════════════════════════════════════ with gr.Column(scale=1, min_width=280): gr.HTML('
⚙ Engine
') use_llm = gr.Checkbox(label="Live LLM Mode", value=False, info="Check this to use external API for live inference") with gr.Accordion("🔑 Live LLM Settings", open=True) as llm_settings: engine_preset = gr.Radio( ["AMD Cloud / HF", "Groq", "Together AI", "Google Gemini", "Local (vLLM/Ollama)", "Custom"], label="Infrastructure Preset", value="AMD Cloud / HF" ) api_key = gr.Textbox(label="API Key", type="password", placeholder="hf_... or gsk_...", interactive=True) hf_model = gr.Textbox(label="Model ID", value="Qwen/Qwen2.5-7B-Instruct", interactive=True) vllm_url = gr.Textbox(label="Inference Base URL", value="https://api-inference.huggingface.co/v1", placeholder="http://YOUR_AMD_IP:8000/v1", interactive=True) def update_preset(preset): if preset == "Groq": return "llama-3.1-8b-instant", "https://api.groq.com/openai/v1" elif preset == "Together AI": return "meta-llama/Meta-Llama-3.1-8B-Instruct-Turbo", "https://api.together.xyz/v1" elif preset == "Google Gemini": return "gemini-1.5-flash", "https://generativelanguage.googleapis.com/v1beta/openai/" elif preset == "Local (vLLM/Ollama)": return "llama3", "http://localhost:8000/v1" elif preset == "Custom": return "", "" else: return "Qwen/Qwen2.5-7B-Instruct", "https://api-inference.huggingface.co/v1" engine_preset.change( fn=update_preset, inputs=[engine_preset], outputs=[hf_model, vllm_url] ) gr.HTML('
🧬 Agent Composition
') n_mom = gr.Slider(0, 10, value=2, step=1, label="Momentum Traders") n_mr = gr.Slider(0, 10, value=1, step=1, label="Mean Reversion") n_fund = gr.Slider(0, 10, value=1, step=1, label="Fundamental") n_noise = gr.Slider(0, 10, value=1, step=1, label="Noise Traders") n_mm = gr.Slider(0, 5, value=1, step=1, label="Market Makers") gr.HTML('
🔧 Parameters
') num_ticks = gr.Slider(20, 500, value=150, step=10, label="Simulation Ticks") warmup_ticks = gr.Slider(0, 50, value=5, step=5, label="Market Warm-up (Ticks)", info="Establishing baseline before LLMs take over") volatility = gr.Slider(0.0, 0.05, value=0.005, step=0.001, label="Market Volatility") run_btn = gr.Button("▶ Execute Simulation", variant="primary", size="lg") live_status = gr.Markdown("Ready to simulate...") # Stats panel (populated after simulation) gr.HTML('
📊 Session Stats
') stats_panel = gr.HTML("

Run a simulation to see stats

") gr.HTML('
💾 Export Data
') export_file = gr.File(label="📥 Download Tick Data (CSV)", interactive=False) # ══════════════════════════════════════════════ # RIGHT PANEL — Charts & Results # ══════════════════════════════════════════════ with gr.Column(scale=3): main_chart = gr.LinePlot( label="Market Overview", x="tick", y="price", color="metric", title="Live Mid Price vs Fair Value", tooltip=["tick", "metric", "price"], height=300 ) pnl_chart = gr.LinePlot( label="Agent PnL Tracker", x="tick", y="pnl", color="agent_id", title="Agent PnL (Mark-to-Market)", tooltip=["tick", "agent_id", "pnl"], height=300 ) with gr.Row(): spread_chart = gr.LinePlot( label="Bid-Ask Spread", x="tick", y="spread", title="Spread", tooltip=["tick", "spread"], height=200 ) volume_chart = gr.BarPlot( label="Trade Volume", x="tick", y="volume", title="Volume per Tick", tooltip=["tick", "volume"], height=200 ) leaderboard = gr.DataFrame( label="🏆 Global Performance Metrics", interactive=False, wrap=True, ) # ── Wire up the button ── run_btn.click( fn=run_simulation, inputs=[n_mom, n_mr, n_fund, n_noise, n_mm, num_ticks, warmup_ticks, volatility, use_llm, api_key, hf_model, vllm_url], outputs=[main_chart, pnl_chart, spread_chart, volume_chart, leaderboard, stats_panel, export_file, live_status] ) # Enable queuing for streaming/generator support — MUST be inside create_app # so it works on HF Spaces (which import the app object directly) app.queue(default_concurrency_limit=5) return app # ─── ENTRY POINT ────────────────────────────────────────────────── if __name__ == "__main__": app = create_app() app.launch( server_port=7860, css=CUSTOM_CSS, theme=gr.themes.Base( primary_hue=gr.themes.colors.cyan, secondary_hue=gr.themes.colors.purple, neutral_hue=gr.themes.colors.slate, font=gr.themes.GoogleFont("Inter"), font_mono=gr.themes.GoogleFont("JetBrains Mono"), ), )