◈   ∿ algotrading · Intermediate

Backtesting Crypto Strategy: Python Tests Before You Trade

This guide is for crypto traders who can read a chart and want a practical Python framework to test signals, fees, sizing, and drawdowns before going live.

Uncle Solieditor · voc · 07.07.2026 ·views 2
◈   Contents
  1. → What Should a Crypto Backtest Prove First?
  2. → What Data Should I Use for Binance, Bybit, OKX and Coinbase?
  3. → How Do I Code the Signals in Python?
  4. → How Should I Size Positions and Model Costs?
  5. → Which Metrics Matter and What Can Break Them?
  6. → Frequently Asked Questions

Backtesting crypto strategy ideas only helps if the test includes the things that actually hit your PnL: fees, slippage, funding, position size, and regime changes. The goal is not to find the prettiest equity curve; it is to decide whether a setup is worth risking real capital on Binance, Bybit, OKX, Coinbase, or any other venue you trade.

What Should a Crypto Backtest Prove First?

A backtest crypto trading strategy should answer one question before anything else: does the edge survive realistic execution? I usually reject a system fast if a 0.06% round-trip cost turns profit factor from 1.4 to below 1.1, because that edge will not survive a busy perp market.

For BTCUSDT perps on Binance or Bybit, I want to see the same logic work across trending weeks, chop, high funding, and liquidation cascades. For spot BTC-USD on Coinbase, funding is irrelevant, but spread, tax lots, and overnight gap behavior still matter.

Backtest pass/fail checks before optimization
QuestionPractical threshold
Does it work after costs?Positive expectancy after fees plus 2-5 bps slippage per side
Is drawdown survivable?Max drawdown below 25% for a directional swing system
Is sample size real?At least 100 trades, preferably 300+ for intraday rules
Does it survive stress?Still profitable when costs are increased by 50%
Is it exchange-specific?Retest separately on Binance, Bybit, OKX, Coinbase, Bitget, Gate.io, and KuCoin data
VoiceOfChain tracks funding-rate pressure, open interest changes, and market structure in real time across Binance, Bybit and OKX - you can see when your backtest regime is showing up live without building the data stack yourself. [voiceofchain.com]

What Data Should I Use for Binance, Bybit, OKX and Coinbase?

Use the same market you plan to trade. A backtesting bitcoin trading strategy on Coinbase spot data will not tell you how the same rule behaves on OKX BTC-USDT-SWAP, where funding, mark price, and liquidation flows affect entries.

Keep everything in UTC, enter on the next candle after the signal, and store raw exchange candles before resampling. Binance funding normally settles on scheduled intervals, and Bybit funding history is contract-specific, so do not hard-code every perp as if it pays the same way forever.

Official docs worth checking before coding the test
Use caseSource
Binance USDT-M futures candleshttps://developers.binance.com/docs/derivatives/usds-margined-futures/market-data/rest-api/Kline-Candlestick-Data
Binance futures funding mechanicshttps://www.binance.com/en/support/faq/detail/360033525031
Bybit funding rate historyhttps://bybit-exchange.github.io/docs/v5/market/history-fund-rate
OKX market data and public endpointshttps://www.okx.com/docs-v5/en/
Coinbase spot product candleshttps://docs.cdp.coinbase.com/api-reference/exchange-api/rest-api/products/get-product-candles
Backtesting.py framework docshttps://kernc.github.io/backtesting.py/

How Do I Code the Signals in Python?

Start with a simple rule you can explain in one sentence. This example buys a BTC breakout only when the short EMA is above the slow EMA, then exits when price loses the fast EMA; it is basic, but it forces clean signal timing.

import numpy as np
import pandas as pd

def load_ohlcv(path):
    df = pd.read_csv(path, parse_dates=["time"])
    df = df.set_index("time").sort_index()
    cols = ["open", "high", "low", "close", "volume"]
    return df[cols].astype(float)

def add_signals(df):
    df = df.copy()
    df["ema_fast"] = df["close"].ewm(span=20, adjust=False).mean()
    df["ema_slow"] = df["close"].ewm(span=80, adjust=False).mean()
    prior_high = df["high"].rolling(50).max().shift(1)

    long_entry = (df["ema_fast"] > df["ema_slow"]) & (df["close"] > prior_high)
    long_exit = df["close"] < df["ema_fast"]

    df["signal"] = np.select([long_entry, long_exit], [1, 0], default=np.nan)
    df["position"] = df["signal"].ffill().fillna(0)
    df["position_next_bar"] = df["position"].shift(1).fillna(0)
    return df.dropna()

# Setup example
btc = load_ohlcv("BTCUSDT_1h_binance.csv")
btc = add_signals(btc)

How Should I Size Positions and Model Costs?

Position sizing decides whether a decent signal becomes a tradable system or a liquidation story. My default research setting is 0.5%-1.0% account risk per trade, then a hard notional cap so a tight stop does not create accidental 8x exposure.

Position sizing formulas to use before PnL metrics
ComponentFormula
Risk dollarsaccount_equity * risk_pct
Unit riskabs(entry_price - stop_price)
Raw unitsrisk_dollars / unit_risk
Max notional unitsaccount_equity * max_notional_pct / entry_price
Final positionmin(raw_units, max_notional_units)
def position_units(equity, entry, stop, risk_pct=0.01, max_notional_pct=0.30):
    risk_dollars = equity * risk_pct
    unit_risk = abs(entry - stop)
    if unit_risk <= 0:
        return 0
    raw_units = risk_dollars / unit_risk
    max_units = (equity * max_notional_pct) / entry
    return min(raw_units, max_units)

def apply_costs(df, fee_bps=4, slippage_bps=2, funding_col=None):
    df = df.copy()
    pos = df["position_next_bar"]
    turnover = pos.diff().abs().fillna(pos.abs())

    df["gross_ret"] = df["close"].pct_change().fillna(0) * pos.shift(1).fillna(0)
    trading_cost = turnover * ((fee_bps + slippage_bps) / 10000)

    if funding_col:
        funding_cost = df[funding_col].fillna(0) * pos.shift(1).fillna(0)
    else:
        funding_cost = 0

    df["net_ret"] = df["gross_ret"] - trading_cost - funding_cost
    df["equity"] = (1 + df["net_ret"]).cumprod()
    return df

btc = apply_costs(btc, fee_bps=4, slippage_bps=2, funding_col=None)

Funding can be bigger than the trade edge. A positive 0.10% funding rate every 8 hours is roughly 0.30% per day paid by longs, so a slow long-only perp system can look profitable before funding and dead after funding.

Which Metrics Matter and What Can Break Them?

The crypto best strategy is not the one with the highest final equity. I rank systems by risk-adjusted return, drawdown behavior, cost sensitivity, and whether the same idea still works when Bitcoin changes regime.

def performance_metrics(df, periods_per_year=365 * 24):
    r = df["net_ret"].dropna()
    equity = df["equity"].dropna()
    drawdown = equity / equity.cummax() - 1

    years = len(r) / periods_per_year
    cagr = equity.iloc[-1] ** (1 / years) - 1 if years > 0 else np.nan
    sharpe = (r.mean() / r.std()) * np.sqrt(periods_per_year) if r.std() else np.nan
    max_dd = drawdown.min()
    profit_factor = r[r > 0].sum() / abs(r[r < 0].sum()) if (r < 0).any() else np.inf
    trades = int(df["position_next_bar"].diff().abs().fillna(0).sum())

    return {
        "cagr": round(cagr, 4),
        "sharpe": round(sharpe, 2),
        "max_drawdown": round(max_dd, 4),
        "profit_factor": round(profit_factor, 2),
        "trades": trades
    }

print(performance_metrics(btc))
Metric filters I use before paper trading
MetricMinimum I care about
Profit factorAbove 1.3 after fees and slippage
Max drawdownBelow 25% for a swing system, lower for leverage
Trade count100+ trades before trusting the distribution
Cost stressStill profitable with 50% higher costs
Out-of-sample behaviorNo collapse on the last 25%-30% of data

The most common mistake is accidental lookahead: entering at the same candle close that triggered the breakout. The second is pretending every order fills at mid price on a volatile alt, which is how a crypto best strategy turns into a live loss machine.

The honest risk caveat: a backtest can only reject bad ideas; it cannot guarantee a live edge. When exchange rules, fees, tick size, funding intervals, or liquidity change, retest before increasing size.

Frequently Asked Questions

How do I backtest crypto strategy Python?
Load clean OHLCV data, generate signals without lookahead, shift execution to the next bar, subtract fees and slippage, then calculate drawdown and profit factor. A simple pandas test is enough for research; use Backtesting.py or vectorbt when you need order handling and parameter sweeps.
Can I backtest bitcoin strategy with free data?
Yes. Binance public futures candles and Coinbase spot candles are enough to test BTC trend, breakout, and mean-reversion rules on 1h or 1d data. For perps, add funding history before trusting the result.
What is a good Sharpe ratio for a crypto strategy?
After fees, a Sharpe above 1.2 is useful and above 2.0 is strong, but only if max drawdown is controlled. I would rather trade a 1.4 Sharpe system with 12% drawdown than a 2.1 Sharpe system that regularly loses 35%.
Should I backtest spot or futures first?
Backtest the market you will actually trade. Spot is cleaner for signal research, but futures tests are mandatory if you use leverage, shorts, or funding-sensitive setups on Binance, Bybit, OKX, or Bitget.
How many trades are enough for a crypto backtest?
Below 50 trades, the result is usually too fragile to trust. For intraday crypto, I want 100-300 trades across bull, bear, and sideways regimes before I even consider paper trading.
Is there a crypto best strategy that works on every coin?
No. Momentum often works better on BTC and ETH during strong regimes, while small-cap alts need liquidity filters and stricter exits. The crypto best strategy is the one whose risk, fees, and execution assumptions match the venue you actually trade.

The key takeaway is simple: backtesting crypto strategy ideas is a risk filter, not a profit forecast. Build the test around execution first - next-bar fills, realistic fees, funding, slippage, and position sizing. If the system still works after cost stress and out-of-sample checks, paper trade it for 2-4 weeks before adding real size. That process will save you from most strategies that look sharp in Python and bleed once they hit the book.

◈   more on this topic
⌘ api Kraken API Documentation for Crypto Traders: Essentials and Examples