Backtesting Guide
Backtesting runs a trading strategy against historical OHLCV market data so you can measure its returns, risk, and drawdown before risking real capital.
This guide walks through building a complete backtest with StockAPIS data — pulling candles, computing signals, simulating trades, and scoring the result with standard metrics.
Core Backtest Metrics
Total Return
Measures cumulative growth of equity over the test period:
total_return = (final_equity / starting_equity - 1) * 100
# Example
starting_equity = 10000
final_equity = 13400
total_return = (final_equity / starting_equity - 1) * 100 # 34.0%Sharpe Ratio
Risk-adjusted return — how much excess return per unit of volatility:
import numpy as np
# daily_returns is a list of per-period returns
mean_r = np.mean(daily_returns)
std_r = np.std(daily_returns)
# Annualize for daily crypto data (365 trading days)
sharpe = (mean_r / std_r) * np.sqrt(365)
# Sharpe > 1 is decent, > 2 is strong, > 3 is excellentMaximum Drawdown
Largest peak-to-trough equity decline — your worst-case pain:
def max_drawdown(equity_curve):
peak = equity_curve[0]
max_dd = 0
for value in equity_curve:
peak = max(peak, value)
dd = (peak - value) / peak
max_dd = max(max_dd, dd)
return max_dd * 100 # percent
# Lower is better. -20% means you lost a fifth at the worst point.Complete Backtest Example
from stockapis import StockAPIS
api = StockAPIS(api_key='your_api_key')
def backtest_sma_crossover(symbol, exchange='binance'):
# Pull historical OHLCV candles (daily, 2 years)
candles = api.platforms.crypto_exchanges.get_ohlcv(
exchange=exchange,
symbol=symbol,
interval='1d',
limit=730
)
closes = [c.close for c in candles]
# Strategy parameters
fast_window = 20
slow_window = 50
starting_equity = 10000
# Backtest state
equity = starting_equity
position = 0.0 # units held
cash = starting_equity
equity_curve = []
returns = []
trades = 0
def sma(values, window, i):
if i < window:
return None
return sum(values[i - window:i]) / window
prev_equity = starting_equity
for i in range(len(closes)):
price = closes[i]
fast = sma(closes, fast_window, i)
slow = sma(closes, slow_window, i)
# Signal: go long when fast SMA crosses above slow SMA
if fast is not None and slow is not None:
if fast > slow and position == 0:
# Buy with all cash
position = cash / price
cash = 0.0
trades += 1
elif fast < slow and position > 0:
# Sell entire position
cash = position * price
position = 0.0
trades += 1
# Mark-to-market equity
equity = cash + position * price
equity_curve.append(equity)
returns.append(equity / prev_equity - 1)
prev_equity = equity
# Compute metrics
import numpy as np
total_return = (equity_curve[-1] / starting_equity - 1) * 100
mean_r = np.mean(returns)
std_r = np.std(returns) or 1e-9
sharpe = (mean_r / std_r) * np.sqrt(365)
peak = equity_curve[0]
max_dd = 0
for v in equity_curve:
peak = max(peak, v)
max_dd = max(max_dd, (peak - v) / peak)
# Print report
print(f"Backtest: SMA {fast_window}/{slow_window} on {symbol} ({exchange})")
print(f"\nPerformance:")
print(f" Starting Equity: ${starting_equity:,}")
print(f" Final Equity: ${equity_curve[-1]:,.0f}")
print(f" Total Return: {total_return:+.2f}%")
print(f"\nRisk:")
print(f" Sharpe Ratio: {sharpe:.2f}")
print(f" Max Drawdown: -{max_dd * 100:.2f}%")
print(f" Trades: {trades}")
# Verdict
if sharpe >= 2 and max_dd < 0.20:
print(f"\n✓ STRONG STRATEGY")
elif sharpe >= 1 and max_dd < 0.35:
print(f"\n○ MODERATE STRATEGY")
else:
print(f"\n✗ WEAK STRATEGY")
return {
'total_return': total_return,
'sharpe': sharpe,
'max_drawdown': max_dd * 100,
'trades': trades
}
# Run the backtest
metrics = backtest_sma_crossover('BTCUSDT', exchange='binance')Comparing Symbols and Markets
Run the same strategy across multiple assets to see where it holds up:
def compare_symbols(symbols, exchange='binance'):
results = []
for symbol in symbols:
# Pull candles and market context
candles = api.platforms.crypto_exchanges.get_ohlcv(
exchange=exchange,
symbol=symbol,
interval='1d',
limit=730
)
stats = api.platforms.crypto_data.get_market_stats(symbol=symbol)
# Reuse the backtest engine
m = backtest_sma_crossover(symbol, exchange=exchange)
results.append({
'symbol': symbol,
'sharpe': m['sharpe'],
'return': m['total_return'],
'drawdown': m['max_drawdown'],
'volume_24h': stats.volume_24h
})
# Rank by risk-adjusted return
results.sort(key=lambda x: x['sharpe'], reverse=True)
print("Strategy Rankings by Sharpe:")
for i, r in enumerate(results, 1):
print(f"{i}. {r['symbol']}")
print(f" Sharpe: {r['sharpe']:.2f} | Return: {r['return']:+.1f}% | DD: -{r['drawdown']:.1f}%")
return results
compare_symbols(['BTCUSDT', 'ETHUSDT', 'SOLUSDT'])Avoiding Overfitting with Walk-Forward Testing
A strategy that looks perfect on one history is often curve-fit. Split the data into in-sample (tune) and out-of-sample (validate) windows:
def walk_forward(symbol, exchange='binance', splits=4):
candles = api.platforms.crypto_exchanges.get_ohlcv(
exchange=exchange,
symbol=symbol,
interval='1d',
limit=1460 # ~4 years
)
closes = [c.close for c in candles]
window = len(closes) // splits
out_of_sample_sharpes = []
for s in range(splits - 1):
train = closes[s * window:(s + 1) * window]
test = closes[(s + 1) * window:(s + 2) * window]
# Tune parameters on `train`, then evaluate untouched on `test`.
# Only out-of-sample results count toward the verdict.
best_params = tune_on(train) # your tuning routine
oos = evaluate(test, best_params) # your eval routine
out_of_sample_sharpes.append(oos['sharpe'])
print(f"Fold {s + 1}: out-of-sample Sharpe = {oos['sharpe']:.2f}")
avg = sum(out_of_sample_sharpes) / len(out_of_sample_sharpes)
print(f"\nMean out-of-sample Sharpe: {avg:.2f}")
print("If this is far below your in-sample Sharpe, the strategy is overfit.")
return out_of_sample_sharpesAlways account for trading costs. Add exchange fees (Binance and Coinbase typically charge 0.1%–0.6% per fill) and slippage to every simulated trade, or your backtest will overstate returns versus live execution.
Quick Start
from stockapis import StockAPIS
api = StockAPIS(api_key='your_api_key')
# Pull historical daily candles for backtesting
candles = api.platforms.crypto_exchanges.get_ohlcv(
exchange='binance',
symbol='BTCUSDT',
interval='1d',
limit=365
)
# Quick buy-and-hold benchmark
first, last = candles[0].close, candles[-1].close
hold_return = (last / first - 1) * 100
print(f"Buy & Hold Return: {hold_return:+.2f}%")Next Steps
- Browse every data source on /platforms and the crypto exchanges and stock exchanges categories.
- See the Binance integration for OHLCV, order book, and trade-stream endpoints.
- Layer in alternative data from financial news and social signals to test sentiment-driven strategies.
- For aggregated candles and reference data, see financial data APIs and crypto data.
- New to the platform? Start with the API getting started guide, check pricing, or contact us.