Skip to Content
GuidesBacktesting with OHLCV

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 excellent

Maximum 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_sharpes

Always 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

Last updated on