Advanced ~45 min Python Build guide

Backtest Your Strategy

Use historical indicator data to validate your strategies before risking real capital. This guide covers building a simple backtesting framework with Python.

1 sections 1 copy-paste code sample Python
1Guide sections
1Code samples
AdvancedLevel
45 minTime

Simple RSI Backtest

This example tests a mean-reversion strategy: buy when RSI drops below 30, sell when it recovers above 50.

Python Simple RSI Backtest
import requests
from datetime import datetime

API_KEY = "YOUR_API_KEY"
BASE = "https://tickatlas.com/v1"
headers = {"X-API-Key": API_KEY}

def get_indicator_series(symbol, indicator, timeframe, bars=500):
    """Historical indicator values: data.series = [{time, value}, ...]."""
    params = {
        "symbol": symbol, "indicator": indicator,
        "timeframe": timeframe, "limit": bars
    }
    resp = requests.get(f"{BASE}/indicator/history",
        headers=headers, params=params).json()
    return resp["data"]["series"]

def get_candles(symbol, timeframe, bars=500):
    """Matching prices: data.candles = [{time, open, high, low, close, volume}]."""
    params = {"symbol": symbol, "timeframe": timeframe, "limit": bars}
    resp = requests.get(f"{BASE}/ohlc",
        headers=headers, params=params).json()
    return resp["data"]["candles"]

def _at(ts):
    """/v1/ohlc suffixes its timestamps with 'Z', the series does not."""
    return ts.rstrip("Z")

def backtest_rsi(symbol, timeframe="H1", bars=500):
    """Simple RSI mean-reversion backtest."""
    # Indicator values and prices are separate endpoints — join them on time.
    closes = {_at(c["time"]): c["close"]
              for c in get_candles(symbol, timeframe, bars)}
    history = [p for p in get_indicator_series(symbol, "RSI_14", timeframe, bars)
               if p["value"] is not None and _at(p["time"]) in closes]

    trades = []
    position = None

    for i, bar in enumerate(history):
        rsi = bar["value"]
        price = closes[_at(bar["time"])]

        if position is None and rsi < 30:
            position = {"entry": price, "entry_rsi": rsi, "bar": i}
        elif position and rsi > 50:
            pnl = price - position["entry"]
            trades.append({
                "entry": position["entry"], "exit": price,
                "pnl_pips": pnl * 10000, "bars_held": i - position["bar"]
            })
            position = None

    wins = [t for t in trades if t["pnl_pips"] > 0]
    total = len(trades)
    win_rate = len(wins) / total * 100 if total else 0
    avg_pnl = sum(t["pnl_pips"] for t in trades) / total if total else 0

    print(f"Symbol: {symbol} | Timeframe: {timeframe}")
    print(f"Total trades: {total} | Win rate: {win_rate:.1f}%")
    print(f"Avg PnL: {avg_pnl:.1f} pips")
    return trades

# Run backtest
trades = backtest_rsi("EURUSD", "H1", 500)

Key Metrics to Track

Win Rate Percentage of profitable trades in the run. Only meaningful next to average win and loss size — a low win rate and a high one can produce the same equity curve.
Profit Factor Gross profit divided by gross loss. Above 1.0 means the run finished ahead; spread and slippage come out of whatever margin is left.
Max Drawdown Largest peak-to-trough decline in equity across the run. It is the loss the strategy would have required you to sit through.
Sharpe Ratio Return per unit of volatility. Comparable only between runs measured the same way, over the same period and bar count.

Best Practices

Test across multiple symbols and timeframes to avoid curve-fitting
Use out-of-sample data for final validation (don't optimize on all data)
Account for spread and slippage in your PnL calculations
Include transaction costs in profitability analysis
Test in both trending and ranging market conditions

Production hardening

The code above is the happy path. These are the concerns that decide whether it survives contact with a real deployment.

Keep the key server-side. The API key authenticates with the X-API-Key header and must never reach a browser bundle. Proxy it, or use a public widget key, which is domain-scoped and revocable. Authentication
Handle 429 before you need to. Rate limits are per key and per minute. Back off on 429 rather than retrying immediately, and read the X-RateLimit-* headers on every response. Rate limits
Branch on the error code, not the message. Errors carry a stable machine-readable code; the human-readable text can change. Codes were unified in v3.15. Error handling
Expect gaps, and do not invent values. Markets close, feeds stall, and a retention window can reject a request outright. Surface an explicit unavailable state rather than substituting a zero or the last known price. Troubleshooting
Cache what you poll. Responses are already cached briefly upstream, so polling faster than the data changes spends credits without improving freshness. Cache on your side and poll on the cadence your timeframe actually updates. Pricing and credits
Watch retention per timeframe. History depth is set per timeframe, never per plan, so a request that works on D1 can fall outside the window on M1. Check the published windows before backfilling. Timeframes
Rotate keys and scope them. Issue a separate key per deployment so one can be revoked without taking the others down, and rotate on a schedule rather than after an incident. Authentication
Log the request, not the key. Record endpoint, parameters, status and latency so a failure is reproducible. Never log the key itself, and scrub it from error reports.

Related Guides

Everything this guide touches, linked directly — so it never dead-ends.

Build against live market data

Start with the data layer already solved.

Create an API key, run the first request, then extend one layer at a time. Every account starts pay-as-you-go with $2.50 of credit.