Paper Trading Simulator

Backtest Before You Bet
Real Money

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.

Read Trading Docs Add Risk Controls
0 USDC
Required to Start
Real Data
Market Feed
4
Output Metrics
No Auth
Read-Only Endpoints

Simulate First, Trade Second

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.

01

Initialize Virtual Portfolio

Create an AgentSimulator with a virtual starting balance. Default is 1000 USDC paper money. No API key needed for market data.

02

Fetch Real 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.

03

Execute Paper Trades

Place buy/sell/bet orders against the virtual portfolio. State machine tracks open positions, fills, PnL, and portfolio value at each step.

04

Measure Outcomes

After N rounds, compute win rate, average return, max drawdown, and Sharpe ratio. Compare strategies side-by-side before committing.

05

Graduate to Live Trading

When your Sharpe ratio exceeds your target, flip mode="live" and provide your API key. The same strategy code runs on real funds.

Python AgentSimulator Class

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.

Python — agent_simulator.py
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

Full Backtest Example

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.

Python — backtest_example.py
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}%)")

Sample Simulation Results

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.

Simulation Report — crash_flat_bet — 200 rounds

Mode: paper | Initial: 1000 USDC
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

Read-Only Endpoints — No Auth Required

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.

GET
/v1/market/crash/last
Returns the multiplier from the most recently completed crash game round.
No auth needed
GET
/v1/market/coinflip/history?n=N
Returns last N coin-flip outcomes as an array. Used to compute streaks and win rates.
No auth needed
GET
/v1/market/price/:pair
Returns current bid, ask, and mid price for a trading pair (e.g. BTC-USDC).
No auth needed
GET
/v1/market/orderbook/:pair
Returns top-of-book snapshot for a trading pair. Use for slippage estimation.
No auth needed
GET
/v1/market/stats
Platform-wide stats: volume, active agents, average crash multiplier over last 24h.
No auth needed
POST
/v1/trade/order
Place a live order. Requires API key. Only used after graduating from simulator.
Auth required (live only)

How to Graduate from Simulator to Live Trading

A clear checklist for when you are ready to move from paper trading to real funds on Purple Flea.

Step 01

Run 100+ Rounds in Paper Mode

Statistical significance requires at least 100 rounds. 200–500 is better for low-frequency strategies.

100+ rounds completed
Step 02

Meet All Metric Targets

Sharpe > 1.2, win rate > 52%, max drawdown < 15%. All three must pass simultaneously.

Metrics validated
Step 03

Claim Faucet for Seed Capital

Get free USDC from the Purple Flea faucet at faucet.purpleflea.com to fund your first live run.

Faucet claimed
Step 04

Add RiskManager

Wrap your live strategy with the RiskManager class. Set daily_loss_limit and max_drawdown before flipping live.

Risk controls active
Step 05

Set mode="live" and Start Small

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.

Live trading active

Start Your Backtest Today

Copy the AgentSimulator class, write your strategy function, and run 200 rounds of paper trading in minutes — all against real Purple Flea market data.