The Backtest Engine
for Prediction Markets
Test trading strategies against historical Polymarket data with real orderbook simulation, slippage modeling, and pay-per-use pricing. One endpoint — full results.
Authentication
All backtest requests are authenticated with an API key passed in the X-API-Key header.
pcap_ and look like pcap_k9m3xR7vL2pQ8wN4jF6....Create and manage keys from your dashboard. The plaintext key is shown only once at creation time.
X-API-Key: pcap_k9m3xR7vL2pQ8wN4jF6hT1bY...
Quickstart
One request is all you need. Here's a complete example in three languages.
curl -X POST https://api.polycop.polybacktest.com/v1/backtest \ -H "X-API-Key: pcap_k9m3xR7vL2pQ8wN4..." \ -H "Content-Type: application/json" \ -d '{ "coin": "btc", "num_markets": 100, "market_type": "15m", "direction": "buy_down", "order_type": "limit", "limit_price": 0.85, "duration_seconds": 120, "min_share_price": 0.70, "max_share_price": 0.95, "take_profit_pct": 15, "stop_loss_pct": 50 }'
import requests response = requests.post( "https://api.polycop.polybacktest.com/v1/backtest", headers={"X-API-Key": "pcap_k9m3xR7vL2pQ8wN4..."}, json={ "coin": "btc", "num_markets": 100, "market_type": "15m", "direction": "buy_down", "order_type": "limit", "limit_price": 0.85, "duration_seconds": 120, "min_share_price": 0.70, "max_share_price": 0.95, "take_profit_pct": 15, "stop_loss_pct": 50, }, ) data = response.json() summary = data["summary"] print(f"PnL: ${summary['pnl_absolute']:.2f} ({summary['pnl_pct']:.1f}%)") print(f"Win Rate: {summary['win_rate_pct']:.1f}%") print(f"Trades: {summary['total_trades']}") print(f"Markets: {summary['markets_tested']} tested, {summary['markets_won']} won") print(f"Drawdown: {summary['max_drawdown_pct']:.1f}% (${summary['drawdown']:.2f})") print(f"Cost: ${summary['cost_charged']}")
const res = await fetch("https://api.polycop.polybacktest.com/v1/backtest", { method: "POST", headers: { "X-API-Key": "pcap_k9m3xR7vL2pQ8wN4...", "Content-Type": "application/json", }, body: JSON.stringify({ coin: "btc", num_markets: 100, market_type: "15m", direction: "buy_down", order_type: "limit", limit_price: 0.85, duration_seconds: 120, min_share_price: 0.70, max_share_price: 0.95, take_profit_pct: 15, stop_loss_pct: 50, }), }); const { summary, trades, equity_curve } = await res.json(); console.log(`PnL: $${summary.pnl_absolute.toFixed(2)} (${summary.pnl_pct.toFixed(1)}%)`); console.log(`Win Rate: ${summary.win_rate_pct.toFixed(1)}%`); console.log(`Trades: ${summary.total_trades} across ${summary.markets_tested} markets`);
Endpoint
Run a trading strategy against historical Polymarket data. Simulates entries and exits using real orderbook snapshots with VWAP fill pricing, slippage checks, and configurable take-profit / stop-loss.
Cost is deducted from your wallet atomically on success. If your balance is insufficient, you'll get a 402 error and nothing is charged.
Request Parameters
Send as JSON body. All parameters have sensible defaults except limit_price and duration_seconds which are required when using limit orders.
Scope & Market Selection
| Parameter | Type | Description | |
|---|---|---|---|
coin | string | OPTIONAL | "btc", "eth", or "sol". Default: "btc" |
num_markets | integer | OPTIONAL | Number of markets: 10, 100, or 1000. Default: 10 |
market_type | string | OPTIONAL | Market duration: "5m" "15m" "1hr" "4hr" "24hr". Default: "15m" |
tag | string | OPTIONAL | Custom label for this run (e.g. "aggressive-v2") |
Entry Conditions
| Parameter | Type | Description | |
|---|---|---|---|
direction | string | OPTIONAL | "buy_down" (bet No wins) or "buy_up" (bet Yes wins). Default: "buy_down" |
trigger_start | string | OPTIONAL | Window start offset from market open (MM:SS). Default: "13:00" |
trigger_end | string | OPTIONAL | Window end offset (MM:SS). Default: "15:00" |
min_share_price | float | OPTIONAL | Min token price filter (0–1). Default: 0.7 |
max_share_price | float | OPTIONAL | Max token price filter (0–1). Default: 0.95 |
min_coin_change | float | OPTIONAL | Min coin price change from market open (USD). Default: 50.0 |
max_coin_change | float | OPTIONAL | Max coin price change (USD). Default: 9999.0 |
min_btc_change | float | OPTIONAL | Legacy alias for min_coin_change |
max_btc_change | float | OPTIONAL | Legacy alias for max_coin_change |
Order Configuration
| Parameter | Type | Description | |
|---|---|---|---|
order_type | string | OPTIONAL | "market" or "limit". Default: "limit" |
limit_price | float | REQUIRED* | Max entry price (0–1). Required when order_type="limit" |
duration_seconds | integer | REQUIRED* | How long the limit order stays open (1–86400s). Required for limit orders |
single_trade_buy_amount | float | OPTIONAL | USD per trade round. Default: 5.0 |
number_of_rounds | integer | OPTIONAL | Max trade rounds (1–100,000). Default: 10000 |
max_slippage_pct | float | OPTIONAL | Reject entry if slippage exceeds this % (0–100) |
Exit Conditions
| Parameter | Type | Description | |
|---|---|---|---|
take_profit_pct | float | OPTIONAL | Exit when profit reaches this % (e.g. 15 = +15%) |
stop_loss_pct | float | OPTIONAL | Exit when loss reaches this % (e.g. 50 = -50%) |
take_profit | float | OPTIONAL | Absolute take-profit price (0–1). Exit when token price ≥ this value. Cannot combine with take_profit_pct |
stop_loss | float | OPTIONAL | Absolute stop-loss price (0–1). Exit when token price ≤ this value. Cannot combine with stop_loss_pct |
Response
A successful response contains four top-level fields:
{
"settings": {
"tag": "aggressive-dip-buy",
"trigger_start": "13:00",
"trigger_end": "15:00",
"direction": "buy_down",
"order_type": "limit",
"limit_price": 0.85,
"duration_seconds": 120,
"min_share_price": 0.7,
"max_share_price": 0.95,
"min_coin_change": 50.0,
"max_coin_change": 9999.0,
"single_trade_buy_amount": 10.0,
"number_of_rounds": 10000,
"take_profit_pct": 15,
"stop_loss_pct": 50,
"take_profit": null,
"stop_loss": null,
"max_slippage_pct": null,
"coin": "btc",
"num_markets": 100,
"market_type": "15m"
},
"summary": {
"total_trades": 186,
"total_volume": 1860.00,
"total_capital_used": 930.00,
"win_rate_pct": 62.4,
"pnl_absolute": 148.32,
"pnl_pct": 15.94,
"avg_buy_price": 0.812,
"avg_sell_price": 0.871,
"max_drawdown_pct": 8.3,
"drawdown": -0.83,
"fees_impact": 0.0,
"markets_tested": 97,
"markets_won": 58,
"markets_lost": 39,
"trigger_rate_abs": 93,
"trigger_rate_pct": 95.88,
"max_consecutive_losses": 4,
"cost_charged": 0.05
},
"equity_curve": [
{ "timestamp": "2025-01-10T12:13:00Z", "portfolio_value": 10000.00 },
{ "timestamp": "2025-01-10T12:13:45Z", "portfolio_value": 10012.50 },
// ... one point per trade event
],
"trades": [
{
"timestamp": "2025-01-10T12:13:22Z",
"market": "will-btc-go-above-95000",
"side": "buy",
"price": 0.82,
"size": 12.19,
"portfolio_value": 10000.00,
"prediction": "down",
"coin_price": 94852.31
},
{
"timestamp": "2025-01-10T12:28:00Z",
"market": "will-btc-go-above-95000",
"side": "sell",
"price": 1.0,
"size": 12.19,
"portfolio_value": 10002.19,
"prediction": "down",
"coin_price": 95017.44
}
// ... all trades (buy/sell pairs)
]
}
Summary Fields
| Field | Type | Description |
|---|---|---|
total_trades | int | Total buy + sell trades executed |
total_volume | float | Total USD volume traded |
total_capital_used | float | Total capital deployed (buys only) |
win_rate_pct | float | Percentage of winning trades |
pnl_absolute | float | Net profit/loss in USD |
pnl_pct | float | Net profit/loss as percentage of capital |
avg_buy_price | float | Average entry price across all buys |
avg_sell_price | float | Average exit price across all sells |
max_drawdown_pct | float | Maximum peak-to-trough portfolio decline (%) |
drawdown | float | Maximum peak-to-trough portfolio decline in USD (negative value) |
markets_tested | int | Number of markets with data (may be < num_markets) |
markets_won | int | Markets where strategy was profitable |
markets_lost | int | Markets where strategy lost money |
trigger_rate_abs | int | Markets where at least one entry triggered |
trigger_rate_pct | float | Trigger rate as percentage of markets tested |
max_consecutive_losses | int | Longest losing streak |
cost_charged | float | USD deducted from your wallet for this run |
Trade Fields
Each entry in the trades array represents a single buy or sell execution.
| Field | Type | Description |
|---|---|---|
timestamp | string | ISO-8601 timestamp of the snapshot at which the trade executed |
market | string | Market ID / slug the trade was placed in |
side | string | "buy" (entry) or "sell" (exit) |
price | float | Token fill price (0–1). For buys with orderbook data, this is the VWAP fill price including slippage. |
size | float | Trade size in USD |
portfolio_value | float | Simulated portfolio value after the trade |
prediction | string | "down" or "up" — which side of the market was taken |
coin_price | float | Underlying coin price (USD) at the moment the trade was triggered. For a buy, this is the coin price at entry; for the matching sell, the coin price at exit. |
Error Codes
All errors return JSON with a detail field.
{
"detail": "Insufficient wallet balance. Required: $0.0500, Available: $0.0100. Please top up your wallet."
}
| Status | Meaning | What to do |
|---|---|---|
| 200 | Success | Backtest completed. Parse the response. |
| 401 | Unauthorized | Check your X-API-Key header. Key may be missing, invalid, or revoked. |
| 402 | Payment Required | Wallet balance too low. Top up via the dashboard. |
| 403 | Forbidden | Account is disabled. Contact support. |
| 422 | Validation Error | Invalid params. Check market_type, limit_price, direction, etc. |
| 429 | Rate Limited | Too many requests. Back off and retry. |
| 500 | Server Error | Internal failure. The detail field has specifics. |
Strategy Tips
buy_down — You're buying the "No" token, betting the market resolves downward. The entry price filter (min/max_share_price) applies to price_down.buy_up — You're buying the "Yes" token. Price filter applies to price_up.
trigger_start / trigger_end define when the strategy looks for entries, as an offset from market open. For a 15-minute market, "13:00" to "15:00" means "look for entries in the last 2 minutes before close." Narrow windows reduce noise; wider windows catch more opportunities.
Limit: Only enters if the VWAP fill price is ≤
limit_price. The order stays open for duration_seconds. Lower limit_price = better entries but fewer fills.Market: Enters at whatever price the orderbook gives. No
limit_price needed. Use max_slippage_pct to cap how much slippage you'll accept.
Without TP/SL, positions are held until market resolution (binary outcome: 0 or 1). Adding TP/SL lets you exit mid-market based on price movement in the orderbook snapshots. Use the
_pct variants for percentage-based exits relative to entry price, or the absolute variants (take_profit / stop_loss) for fixed price targets. You cannot combine both for the same exit type.
Example Strategies
// Tight price filter, low slippage, wide trigger window { "direction": "buy_down", "order_type": "limit", "limit_price": 0.80, "duration_seconds": 60, "min_share_price": 0.70, "max_share_price": 0.85, "max_slippage_pct": 5, "take_profit_pct": 10, "stop_loss_pct": 30, "num_markets": 1000, "market_type": "15m" }
// Wide price range, high coin volatility filter, no stop loss { "direction": "buy_down", "order_type": "market", "min_share_price": 0.55, "max_share_price": 0.95, "min_coin_change": 200, "single_trade_buy_amount": 25, "take_profit_pct": 25, "num_markets": 1000, "market_type": "1hr" }
// Short markets, tight TP/SL, fast trigger window { "direction": "buy_up", "order_type": "limit", "limit_price": 0.60, "duration_seconds": 30, "trigger_start": "03:00", "trigger_end": "04:30", "min_share_price": 0.45, "max_share_price": 0.65, "take_profit_pct": 8, "stop_loss_pct": 15, "num_markets": 100, "market_type": "5m" }
Full Examples
Complete, copy-paste-ready examples showing the exact request and what you get back. Replace the API key with your own pcap_* key.
1. Minimal Request (defaults + market order)
The simplest possible call. Uses all defaults: 10 markets, 15m, buy_down, market order. No limit price needed.
curl -X POST https://api.polycop.polybacktest.com/v1/backtest \ -H "X-API-Key: pcap_k9m3xR7vL2pQ8wN4jF6hT1bY9cD5eA3" \ -H "Content-Type: application/json" \ -d '{ "order_type": "market", "num_markets": 10 }'
{
"settings": {
"tag": null,
"trigger_start": "13:00",
"trigger_end": "15:00",
"min_share_price": 0.7,
"max_share_price": 0.95,
"min_coin_change": 50.0,
"max_coin_change": 9999.0,
"direction": "buy_down",
"order_type": "market",
"limit_price": null,
"duration_seconds": null,
"single_trade_buy_amount": 5.0,
"number_of_rounds": 10000,
"take_profit_pct": null,
"stop_loss_pct": null,
"take_profit": null,
"stop_loss": null,
"max_slippage_pct": null,
"coin": "btc",
"num_markets": 10,
"market_type": "15m"
},
"summary": {
"total_trades": 14,
"total_volume": 70.00,
"total_capital_used": 35.00,
"win_rate_pct": 57.1,
"pnl_absolute": 3.42,
"pnl_pct": 9.77,
"avg_buy_price": 0.826,
"avg_sell_price": 0.875,
"max_drawdown_pct": 5.1,
"drawdown": -0.51,
"fees_impact": 0.0,
"markets_tested": 10,
"markets_won": 4,
"markets_lost": 3,
"trigger_rate_abs": 7,
"trigger_rate_pct": 70.0,
"max_consecutive_losses": 2,
"cost_charged": 0.015
},
"equity_curve": [
{ "timestamp": "2025-02-01T14:13:10Z", "portfolio_value": 5000.00 },
{ "timestamp": "2025-02-01T14:13:10Z", "portfolio_value": 4995.00 },
// ...
],
"trades": [
{ "timestamp": "2025-02-01T14:13:10Z", "market": "will-btc-hit-98500-2", "side": "buy", "price": 0.83, "size": 6.02, "portfolio_value": 5000.00, "prediction": "down", "coin_price": 98412.55 },
{ "timestamp": "2025-02-01T14:15:00Z", "market": "will-btc-hit-98500-2", "side": "sell", "price": 1.0, "size": 6.02, "portfolio_value": 5001.02, "prediction": "down", "coin_price": 98587.10 },
// ... more trades
]
}
2. Limit Order with Take-Profit & Stop-Loss
A complete limit order setup on 100 markets. Only enters if fill price ≤ $0.85, order stays open for 2 minutes. Exits at +15% profit or -50% loss.
curl -X POST https://api.polycop.polybacktest.com/v1/backtest \ -H "X-API-Key: pcap_k9m3xR7vL2pQ8wN4jF6hT1bY9cD5eA3" \ -H "Content-Type: application/json" \ -d '{ "coin": "btc", "num_markets": 100, "market_type": "15m", "direction": "buy_down", "order_type": "limit", "limit_price": 0.85, "duration_seconds": 120, "min_share_price": 0.70, "max_share_price": 0.95, "min_coin_change": 50, "max_coin_change": 9999, "single_trade_buy_amount": 10, "number_of_rounds": 10000, "take_profit_pct": 15, "stop_loss_pct": 50, "tag": "limit-with-tp-sl" }'
{
"settings": {
"tag": "limit-with-tp-sl",
"trigger_start": "13:00",
"trigger_end": "15:00",
"min_share_price": 0.7,
"max_share_price": 0.95,
"min_coin_change": 50.0,
"max_coin_change": 9999.0,
"direction": "buy_down",
"order_type": "limit",
"limit_price": 0.85,
"duration_seconds": 120,
"single_trade_buy_amount": 10.0,
"number_of_rounds": 10000,
"take_profit_pct": 15.0,
"stop_loss_pct": 50.0,
"take_profit": null,
"stop_loss": null,
"max_slippage_pct": null,
"coin": "btc",
"num_markets": 100,
"market_type": "15m"
},
"summary": {
"total_trades": 186,
"total_volume": 1860.00,
"total_capital_used": 930.00,
"win_rate_pct": 62.4,
"pnl_absolute": 148.32,
"pnl_pct": 15.94,
"avg_buy_price": 0.812,
"avg_sell_price": 0.871,
"max_drawdown_pct": 8.3,
"drawdown": -0.83,
"fees_impact": 0.0,
"markets_tested": 97,
"markets_won": 58,
"markets_lost": 39,
"trigger_rate_abs": 93,
"trigger_rate_pct": 95.88,
"max_consecutive_losses": 4,
"cost_charged": 0.05
},
"equity_curve": [
{ "timestamp": "2025-01-10T12:13:00Z", "portfolio_value": 10000.00 },
{ "timestamp": "2025-01-10T12:13:45Z", "portfolio_value": 10012.50 },
{ "timestamp": "2025-01-10T12:14:22Z", "portfolio_value": 10008.10 },
// ... one point per trade event
],
"trades": [
{ "timestamp": "2025-01-10T12:13:22Z", "market": "will-btc-go-above-95000", "side": "buy", "price": 0.82, "size": 12.19, "portfolio_value": 10000.00, "prediction": "down", "coin_price": 94852.31 },
{ "timestamp": "2025-01-10T12:14:01Z", "market": "will-btc-go-above-95000", "side": "sell", "price": 0.94, "size": 12.19, "portfolio_value": 10012.50, "prediction": "down", "coin_price": 94978.12 },
// ... all buy/sell pairs
]
}
3. Full Python Script
A complete, runnable Python script that calls the API and prints a formatted summary.
import requests import json API_KEY = "pcap_k9m3xR7vL2pQ8wN4jF6hT1bY9cD5eA3" # replace with your key URL = "https://api.polycop.polybacktest.com/v1/backtest" payload = { "coin": "btc", "num_markets": 100, "market_type": "15m", "direction": "buy_down", # Limit order: only buy at 0.85 or less, keep order open 2 min "order_type": "limit", "limit_price": 0.85, "duration_seconds": 120, # Only enter if token price is between 0.70 and 0.95 "min_share_price": 0.70, "max_share_price": 0.95, # Only enter if coin price moved at least $50 from market open "min_coin_change": 50, # $10 per trade, exit at +15% or -50% "single_trade_buy_amount": 10, "take_profit_pct": 15, "stop_loss_pct": 50, "tag": "my-test-run", } response = requests.post(URL, headers={"X-API-Key": API_KEY}, json=payload) if response.status_code != 200: print(f"Error {response.status_code}: {response.json()['detail']}") exit(1) data = response.json() s = data["summary"] print("=== Backtest Results ===") print(f"Markets tested: {s['markets_tested']}") print(f"Total trades: {s['total_trades']}") print(f"Win rate: {s['win_rate_pct']:.1f}%") print(f"PnL: ${s['pnl_absolute']:.2f} ({s['pnl_pct']:.1f}%)") print(f"Max drawdown: {s['max_drawdown_pct']:.1f}% (${s['drawdown']:.2f})") print(f"Avg buy price: {s['avg_buy_price']:.3f}") print(f"Avg sell price: {s['avg_sell_price']:.3f}") print(f"Markets won: {s['markets_won']} / {s['markets_tested']}") print(f"Trigger rate: {s['trigger_rate_pct']:.1f}%") print(f"Longest streak: {s['max_consecutive_losses']} consecutive losses") print(f"Cost charged: ${s['cost_charged']}") print(f"\nFirst 5 trades:") for t in data["trades"][:5]: print(f" {t['side']:4s} {t['market']:40s} @ {t['price']:.3f} (${t['size']:.2f})")
4. Full JavaScript (Node.js / Browser)
Works in Node 18+ (native fetch) or any modern browser console.
const API_KEY = "pcap_k9m3xR7vL2pQ8wN4jF6hT1bY9cD5eA3"; // replace with your key const res = await fetch("https://api.polycop.polybacktest.com/v1/backtest", { method: "POST", headers: { "X-API-Key": API_KEY, "Content-Type": "application/json", }, body: JSON.stringify({ coin: "btc", num_markets: 100, market_type: "15m", direction: "buy_down", order_type: "limit", limit_price: 0.85, duration_seconds: 120, min_share_price: 0.70, max_share_price: 0.95, min_coin_change: 50, single_trade_buy_amount: 10, take_profit_pct: 15, stop_loss_pct: 50, tag: "js-test", }), }); if (!res.ok) { const err = await res.json(); console.error(`Error ${res.status}: ${err.detail}`); } else { const { summary, trades, equity_curve } = await res.json(); console.log("=== Backtest Results ==="); console.log(`Markets tested: ${summary.markets_tested}`); console.log(`Total trades: ${summary.total_trades}`); console.log(`Win rate: ${summary.win_rate_pct.toFixed(1)}%`); console.log(`PnL: $${summary.pnl_absolute.toFixed(2)} (${summary.pnl_pct.toFixed(1)}%)`); console.log(`Max drawdown: ${summary.max_drawdown_pct.toFixed(1)}% ($${summary.drawdown.toFixed(2)})`); console.log(`Cost charged: $${summary.cost_charged}`); console.log(`Equity points: ${equity_curve.length}`); console.log(`Trade count: ${trades.length}`); }
5. Market Order — Buy Up on 1hr Markets
Betting that the coin will hit the target. Market orders, no limit price needed. Uses slippage cap instead.
curl -X POST https://api.polycop.polybacktest.com/v1/backtest \ -H "X-API-Key: pcap_k9m3xR7vL2pQ8wN4jF6hT1bY9cD5eA3" \ -H "Content-Type: application/json" \ -d '{ "coin": "btc", "num_markets": 1000, "market_type": "1hr", "direction": "buy_up", "order_type": "market", "min_share_price": 0.50, "max_share_price": 0.80, "min_coin_change": 100, "max_slippage_pct": 10, "single_trade_buy_amount": 20, "take_profit_pct": 20, "stop_loss_pct": 40, "trigger_start": "30:00", "trigger_end": "55:00", "tag": "buy-up-1hr" }'
{
"settings": {
"tag": "buy-up-1hr",
"trigger_start": "30:00",
"trigger_end": "55:00",
"min_share_price": 0.5,
"max_share_price": 0.8,
"min_coin_change": 100.0,
"max_coin_change": 9999.0,
"direction": "buy_up",
"order_type": "market",
"limit_price": null,
"duration_seconds": null,
"single_trade_buy_amount": 20.0,
"number_of_rounds": 10000,
"take_profit_pct": 20.0,
"stop_loss_pct": 40.0,
"take_profit": null,
"stop_loss": null,
"max_slippage_pct": 10.0,
"coin": "btc",
"num_markets": 1000,
"market_type": "1hr"
},
"summary": {
"total_trades": 1240,
"total_volume": 24800.00,
"total_capital_used": 12400.00,
"win_rate_pct": 54.8,
"pnl_absolute": 892.16,
"pnl_pct": 7.19,
"avg_buy_price": 0.641,
"avg_sell_price": 0.713,
"max_drawdown_pct": 12.4,
"drawdown": -2.48,
"fees_impact": 0.0,
"markets_tested": 948,
"markets_won": 340,
"markets_lost": 280,
"trigger_rate_abs": 620,
"trigger_rate_pct": 65.40,
"max_consecutive_losses": 7,
"cost_charged": 0.10
},
"equity_curve": [ /* ... */ ],
"trades": [ /* ... */ ]
}
6. Handling Errors
What happens when things go wrong — and what the response looks like.
Missing limit_price on a limit order
curl -X POST https://api.polycop.polybacktest.com/v1/backtest \ -H "X-API-Key: pcap_k9m3xR7vL2pQ8wN4jF6hT1bY9cD5eA3" \ -H "Content-Type: application/json" \ -d '{ "order_type": "limit" }' // missing limit_price and duration_seconds!
{
"detail": [
{
"msg": "Value error, limit_price is required when order_type is 'limit'",
"type": "value_error"
}
]
}
Insufficient balance
{
"detail": "Insufficient wallet balance. Required: $0.0500, Available: $0.0100. Please top up your wallet."
}
Invalid API key
{
"detail": "Invalid API key"
}
PolyCap API v1.0.0 — Built by PolyBackTest