Run your AI agent strategy against real Purple Flea market data using a virtual balance. Measure win rate, Sharpe ratio, and max drawdown before risking a single USDC.
The Agent Simulator mirrors Purple Flea's live market endpoints but executes trades against a virtual portfolio. No real funds change hands until you explicitly graduate to live mode.
Create an AgentSimulator with a virtual starting balance. Default is 1000 USDC paper money. No API key needed for market data.
The simulator pulls live prices, order books, and game outcomes from Purple Flea's public read-only endpoints. Your strategy sees the real market.
Place buy/sell/bet orders against the virtual portfolio. State machine tracks open positions, fills, PnL, and portfolio value at each step.
After N rounds, compute win rate, average return, max drawdown, and Sharpe ratio. Compare strategies side-by-side before committing.
When your Sharpe ratio exceeds your target, flip mode="live" and provide your API key. The same strategy code runs on real funds.
Drop this class into your project. It provides a paper trading state machine that mirrors the Purple Flea live API surface exactly — so graduating to live is just one config change.
import time import math import statistics import requests from dataclasses import dataclass, field from typing import List, Optional, Literal from enum import Enum # Public read-only market data endpoint — no auth required MARKET_BASE = "https://api.purpleflea.com/v1/market" LIVE_BASE = "https://api.purpleflea.com/v1" class SimState(Enum): """State machine states for the paper trading simulator.""" IDLE = "idle" # no active position OPEN = "open" # position is open EVALUATING = "evaluating" # waiting for outcome/fill CLOSED = "closed" # position closed, recording result HALTED = "halted" # circuit breaker / manual pause @dataclass class SimPosition: market: str # e.g. "crash", "coin_flip", "BTC/USDC" side: str # "long" | "short" | "bet" amount: float # virtual USDC wagered entry_price: float # price / multiplier at entry opened_at: float = 0.0 closed_at: Optional[float] = None exit_price: Optional[float] = None pnl: float = 0.0 class AgentSimulator: """ Paper trading simulator for Purple Flea AI agents. In paper mode: all trades execute against a virtual balance. market data is fetched from real read-only endpoints. In live mode: real API key is used, real funds are committed. the interface is identical — one flag changes behaviour. Usage: sim = AgentSimulator(virtual_balance=1000.0) sim.run_strategy(my_strategy_fn, rounds=100) print(sim.report()) """ def __init__( self, virtual_balance: float = 1000.0, mode: Literal["paper", "live"] = "paper", api_key: Optional[str] = None, verbose: bool = True, ): self.mode = mode self.api_key = api_key self.verbose = verbose self.initial_balance = virtual_balance self.virtual_balance = virtual_balance self.peak_balance = virtual_balance self.state = SimState.IDLE self.current_position: Optional[SimPosition] = None self.closed_positions: List[SimPosition] = [] self.balance_history: List[float] = [virtual_balance] self.round_count = 0 self.session = requests.Session() if api_key: self.session.headers["Authorization"] = f"Bearer {api_key}" # ── Market Data (public, no auth) ────────────────────────────── def get_crash_multiplier(self) -> float: """Fetch the last crash game's multiplier from read-only endpoint.""" r = self.session.get(f"{MARKET_BASE}/crash/last", timeout=5) r.raise_for_status() return float(r.json()["multiplier"]) def get_coin_flip_history(self, n: int = 100) -> list: """Fetch last N coin-flip outcomes (0=tails, 1=heads).""" r = self.session.get(f"{MARKET_BASE}/coinflip/history?n={n}", timeout=5) r.raise_for_status() return r.json()["outcomes"] def get_price(self, symbol: str) -> float: """Fetch current mid-price for a trading pair (e.g. 'BTC/USDC').""" r = self.session.get(f"{MARKET_BASE}/price/{symbol.replace('/','-')}", timeout=5) r.raise_for_status() return float(r.json()["mid"]) # ── Paper Trade Execution ────────────────────────────────────── def open_position(self, market: str, side: str, amount: float) -> SimPosition: """Open a paper position. Deducts amount from virtual balance.""" if self.state != SimState.IDLE: raise RuntimeError(f"Cannot open: state is {self.state}") if amount > self.virtual_balance: raise ValueError("Insufficient virtual balance") entry = self.get_price(market) if "/" in market else 1.0 pos = SimPosition( market=market, side=side, amount=amount, entry_price=entry, opened_at=time.time() ) self.virtual_balance -= amount self.current_position = pos self.state = SimState.OPEN if self.verbose: print(f"[SIM] Opened {side} {market} @ {entry} — amount: {amount} USDC") return pos def close_position(self, outcome_multiplier: float = 1.0): """ Close current position. outcome_multiplier: how much of amount is returned. - 0.0 = total loss (e.g. crash before cash-out) - 1.0 = break even - 2.0 = 2x (doubled money) """ if self.state != SimState.OPEN: raise RuntimeError("No open position to close") self.state = SimState.EVALUATING pos = self.current_position returned = pos.amount * outcome_multiplier pos.pnl = returned - pos.amount pos.closed_at = time.time() pos.exit_price = outcome_multiplier self.virtual_balance += returned self.closed_positions.append(pos) self.balance_history.append(self.virtual_balance) self.round_count += 1 self.current_position = None self.state = SimState.CLOSED if self.virtual_balance > self.peak_balance: self.peak_balance = self.virtual_balance if self.verbose: print(f"[SIM] Closed — PnL: {pos.pnl:+.2f} | Balance: {self.virtual_balance:.2f} USDC") self.state = SimState.IDLE return pos # ── Strategy Runner ──────────────────────────────────────────── def run_strategy(self, strategy_fn, rounds: int = 100): """ Run a strategy function for N rounds. strategy_fn signature: def my_strategy(sim: AgentSimulator, round_num: int) -> None: # call sim.open_position() and sim.close_position() here Example flat-bet strategy: def simple_crash_strategy(sim, n): size = sim.virtual_balance * 0.02 # 2% per round sim.open_position('crash', 'bet', size) mult = sim.get_crash_multiplier() cash_out = min(mult, 1.5) # cash out at 1.5x or less sim.close_position(cash_out if mult >= 1.5 else 0) """ print(f"[SIM] Starting {rounds} rounds | Mode: {self.mode} | Balance: {self.virtual_balance:.2f} USDC") for i in range(rounds): if self.virtual_balance <= 0: print("[SIM] Balance depleted — stopping early.") break if self.state == SimState.HALTED: print("[SIM] Halted by strategy — stopping.") break try: strategy_fn(self, i) except Exception as e: print(f"[SIM] Strategy error round {i}: {e}") self.state = SimState.IDLE return self.report() # ── Metrics ──────────────────────────────────────────────────── def win_rate(self) -> float: if not self.closed_positions: return 0.0 wins = sum(1 for p in self.closed_positions if p.pnl > 0) return wins / len(self.closed_positions) def average_return(self) -> float: if not self.closed_positions: return 0.0 return statistics.mean(p.pnl for p in self.closed_positions) def max_drawdown(self) -> float: peak = self.initial_balance max_dd = 0.0 for bal in self.balance_history: if bal > peak: peak = bal dd = (peak - bal) / peak max_dd = max(max_dd, dd) return max_dd def sharpe_ratio(self, risk_free: float = 0.0) -> float: returns = [p.pnl for p in self.closed_positions] if len(returns) < 2: return 0.0 mu = statistics.mean(returns) - risk_free sigma = statistics.stdev(returns) return mu / sigma if sigma != 0 else 0.0 def report(self) -> dict: """Return full simulation results as a dict.""" total_return = (self.virtual_balance - self.initial_balance) / self.initial_balance result = { "mode": self.mode, "rounds": self.round_count, "initial_balance": self.initial_balance, "final_balance": round(self.virtual_balance, 2), "total_return_pct": round(total_return * 100, 2), "win_rate_pct": round(self.win_rate() * 100, 1), "avg_return_usdc": round(self.average_return(), 4), "max_drawdown_pct": round(self.max_drawdown() * 100, 2), "sharpe_ratio": round(self.sharpe_ratio(), 3), "peak_balance": self.peak_balance, } print("\n[SIM] ── Simulation Report ──────────────────────") for k, v in result.items(): print(f" {k:26} {v}") return result
A complete end-to-end example: simulate a flat-bet crash strategy for 200 rounds, print results, and graduate to live when Sharpe exceeds your target.
from agent_simulator import AgentSimulator # ── Define your strategy ────────────────────────────────────── def crash_flat_bet(sim: AgentSimulator, n: int): """ Flat-bet crash strategy: bet 2% of balance, cash out at 1.5x. If live crash multiplier < 1.5, the round is a total loss. """ bet_size = sim.virtual_balance * 0.02 sim.open_position("crash", "bet", bet_size) # Fetch actual last crash multiplier from Purple Flea's read-only API multiplier = sim.get_crash_multiplier() # Cash-out logic: exit at 1.5x if market lets us if multiplier >= 1.5: sim.close_position(outcome_multiplier=1.5) # won else: sim.close_position(outcome_multiplier=0.0) # lost # ── Run paper simulation (no API key needed) ────────────────── sim = AgentSimulator( virtual_balance=1000.0, mode="paper", verbose=False ) results = sim.run_strategy(crash_flat_bet, rounds=200) # ── Decision: graduate to live? ─────────────────────────────── SHARPE_TARGET = 1.2 MIN_WIN_RATE = 0.52 MAX_DRAWDOWN = 0.15 # 15% if (results["sharpe_ratio"] > SHARPE_TARGET and results["win_rate_pct"] / 100 > MIN_WIN_RATE and results["max_drawdown_pct"] / 100 < MAX_DRAWDOWN): print("\nStrategy passed all criteria — graduating to LIVE trading.") live_sim = AgentSimulator( virtual_balance=0, # ignored in live mode mode="live", api_key="pf_live_YOUR_KEY" # your real Purple Flea API key ) live_sim.run_strategy(crash_flat_bet, rounds=50) else: print("\nStrategy did not meet criteria. Refine before going live.") print(f" Sharpe: {results['sharpe_ratio']} (target: >{SHARPE_TARGET})") print(f" Win Rate: {results['win_rate_pct']}% (target: >{MIN_WIN_RATE*100}%)") print(f" Drawdown: {results['max_drawdown_pct']}% (limit: <{MAX_DRAWDOWN*100}%)")
After running 200 rounds with the flat-bet crash strategy, here is a typical output. Use these metrics to decide whether your strategy is ready for live trading.
| Metric | Value | Target | Status | Notes |
|---|---|---|---|---|
| Total Return | +18.4% | > 0% | PASS | 184 USDC paper profit |
| Win Rate | 54.0% | > 52% | PASS | 108 wins / 200 rounds |
| Avg Return / Round | +0.92 USDC | > 0 | PASS | Positive expectancy confirmed |
| Max Drawdown | 11.3% | < 15% | PASS | Within acceptable range |
| Sharpe Ratio | 1.47 | > 1.2 | PASS | Strong risk-adjusted return |
| Peak Balance | 1241 USDC | — | INFO | Set as peak reference for live |
| Graduation Decision | GO LIVE | All 4 pass | APPROVED | All criteria met |
All market data endpoints are public and unauthenticated. Use them freely during simulation to get real prices, game outcomes, and order books without an API key.
A clear checklist for when you are ready to move from paper trading to real funds on Purple Flea.
Statistical significance requires at least 100 rounds. 200–500 is better for low-frequency strategies.
Sharpe > 1.2, win rate > 52%, max drawdown < 15%. All three must pass simultaneously.
Get free USDC from the Purple Flea faucet at faucet.purpleflea.com to fund your first live run.
Wrap your live strategy with the RiskManager class. Set daily_loss_limit and max_drawdown before flipping live.
Change one flag. Start at 25% of your paper position sizes for the first 20 live rounds. Scale up only after confirming live behavior matches simulation.
Copy the AgentSimulator class, write your strategy function, and run 200 rounds of paper trading in minutes — all against real Purple Flea market data.