Download app.py from lablab-ai-amd-developer-hackathon/Marketmind: direct link, hf CLI and curl.
- Browser
- Download file 29.2 kB
-
https://huggingface.co/spaces/lablab-ai-amd-developer-hackathon/Marketmind/resolve/main/app.py
- Command line
-
hf download hf://spaces/lablab-ai-amd-developer-hackathon/Marketmind/app.py
-
curl -L -o app.py https://huggingface.co/spaces/lablab-ai-amd-developer-hackathon/Marketmind/resolve/main/app.py
29.2 kB
| """ | |
| 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 "<p>No data</p>" | |
| 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""" | |
| <div class="stats-container" style="display:grid; grid-template-columns: 1fr 1fr; gap: 12px;"> | |
| <div class="stat-card"> | |
| <div class="stat-label">TOTAL TRADES</div> | |
| <div class="stat-value">{total_trades}</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">VOLUME</div> | |
| <div class="stat-value">{total_volume:,}</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">AVG SPREAD</div> | |
| <div class="stat-value">{avg_spread:.4f}</div> | |
| </div> | |
| <div class="stat-card"> | |
| <div class="stat-label">LAST UPDATE</div> | |
| <div class="stat-value">{datetime.now().strftime('%H:%M:%S')}</div> | |
| </div> | |
| </div> | |
| """ | |
| # ─── 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(""" | |
| <div class="title-bar"> | |
| <h1>⚡ MarketMind</h1> | |
| <p>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.</p> | |
| </div> | |
| """) | |
| with gr.Row(): | |
| # ══════════════════════════════════════════════ | |
| # LEFT PANEL — Controls | |
| # ══════════════════════════════════════════════ | |
| with gr.Column(scale=1, min_width=280): | |
| gr.HTML('<div class="panel-header">⚙ Engine</div>') | |
| 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('<div class="panel-header">🧬 Agent Composition</div>') | |
| 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('<div class="panel-header">🔧 Parameters</div>') | |
| 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('<div class="panel-header">📊 Session Stats</div>') | |
| stats_panel = gr.HTML("<p style='color:#5a6785;text-align:center;padding:20px;'>Run a simulation to see stats</p>") | |
| gr.HTML('<div class="panel-header">💾 Export Data</div>') | |
| 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"), | |
| ), | |
| ) | |