
How to Build a Cryptocurrency Quantitative Trading Strategy with Python: From Market Data to Backtesting
Learn how to build a cryptocurrency quantitative trading strategy with Python, covering AllTick market data retrieval, EMA trading signals, data cleaning, backtesting logic, and transaction cost evaluation.
For developers building cryptocurrency quantitative trading strategies, writing a buy or sell signal is not the difficult part. The challenge is confirming that the signal is based on reliable market data and that the backtest results accurately reflect the strategy’s performance.
For example, when testing a dual moving average strategy using hourly BTC/USDT candlestick data, the results may be misleading if the data contains duplicate timestamps, the strategy uses the current candle’s closing price before it is available, or trading fees are ignored. Even if the backtest equity curve keeps rising, that does not necessarily mean the strategy has practical value.
Therefore, the key to quantitative trading with Python is not just calculating indicators. It is connecting market data retrieval, data cleaning, signal generation, and backtesting into a complete workflow.
Start with Reliable Market Data
Before building a strategy, you first need to determine which type of market data to use. For medium- to low-frequency strategies based on trend indicators, historical candlestick data is usually sufficient for initial research. However, research into order book changes, short-term price impact, or high-frequency trading may require more granular tick data.
For an hourly dual moving average strategy, historical data can be retrieved through AllTick’s cryptocurrency K-line API. The API uses HTTP GET requests, with quote-b-api/kline as the base path. Authentication and request conditions are passed through the token and a URL-encoded JSON query parameter. The K-line data includes fields such as timestamps, open prices, high prices, low prices, and close prices.
After retrieving the data, you should not immediately calculate indicators. Convert the price fields to numeric values, standardize the time format, sort the records in ascending timestamp order, and remove duplicate timestamps. If candlesticks are missing, do not automatically fill them with the previous candle’s closing price. Doing so may introduce market movements that never actually occurred into the backtest.
Retrieve Market Data and Generate Trading Signals with Python
The following example uses 20-period and 50-period exponential moving averages (EMAs) to build a simple trend strategy. When the short-period EMA is above the long-period EMA, the strategy holds a long position; otherwise, it stays out of the market. This example demonstrates the development and validation workflow for a quantitative trading strategy. It is not an optimized trading system.
First, install the required dependencies:
pip install requests pandasSet the ALLTICK_API_TOKEN environment variable, then run the following code:
import os
import json
import uuid
import requests
import pandas as pd
BASE_URL = "https://quote.alltick.co/quote-b-api"
TOKEN = os.environ["ALLTICK_API_TOKEN"]
def fetch_klines(code="BTCUSDT", count=500):
query = {
"trace": str(uuid.uuid4()),
"data": {
"code": code,
"kline_type": 5, # 1-hour K-line
"kline_timestamp_end": 0,
"query_kline_num": count,
"adjust_type": 0
}
}
response = requests.get(
f"{BASE_URL}/kline",
params={
"token": TOKEN,
"query": json.dumps(query)
},
timeout=15
)
response.raise_for_status()
result = response.json()
if result.get("ret") != 200:
raise RuntimeError(f"API error: {result.get('msg')}")
rows = result.get("data", {}).get("kline_list", [])
if not rows:
raise ValueError("No K-line data returned")
df = pd.DataFrame(rows).rename(columns={
"open_price": "open",
"high_price": "high",
"low_price": "low",
"close_price": "close"
})
df["timestamp"] = pd.to_numeric(df["timestamp"])
df["timestamp"] = pd.to_datetime(
df["timestamp"], unit="s", utc=True
)
for col in ["open", "high", "low", "close"]:
df[col] = pd.to_numeric(df[col], errors="coerce")
return (
df.dropna(subset=["close"])
.sort_values("timestamp")
.drop_duplicates("timestamp")
.set_index("timestamp")
)
df = fetch_klines()
df["ema20"] = df["close"].ewm(
span=20, adjust=False
).mean()
df["ema50"] = df["close"].ewm(
span=50, adjust=False
).mean()
df["signal"] = (df["ema20"] > df["ema50"]).astype(int)
# Use the signal from the previous candle to avoid looking ahead to the current candle's close
df["position"] = df["signal"].shift(1).fillna(0)
df["market_return"] = df["close"].pct_change().fillna(0)
df["turnover"] = (
df["position"].diff().abs()
.fillna(df["position"].abs())
)
# Example fee rate: estimate 5 basis points for each position change
fee_rate = 0.0005
df["strategy_return"] = (
df["position"] * df["market_return"]
- df["turnover"] * fee_rate
)
test = df.iloc[50:].copy()
test["equity"] = (1 + test["strategy_return"]).cumprod()
print("Strategy cumulative return:", round(
test["equity"].iloc[-1] - 1, 4
))
BTCUSDT in the code is an example trading symbol. Before making an actual request, confirm that it matches the symbol supported by the AllTick product available to your account. The API allows up to 500 K-lines per request. If you need a longer history for backtesting, retrieve the data in batches using the timestamp parameters supported by the cryptocurrency API, then remove duplicates when merging the results.
Why Backtest Results Should Not Be Evaluated by Return Alone
The code above uses EMAs to identify trends and delays the signal by one period. This avoids generating a signal from the current candle’s close and then assuming that the strategy could have entered a position at that closing price in advance.
The code also includes an estimated trading fee, but this remains a simplified backtest. The fee rate in the example is a placeholder parameter, not the actual fee charged by an exchange. Slippage, funding rates, order execution, and market impact are also excluded from the model. Therefore, the cumulative return produced by the code should not be treated directly as a prediction of actual trading performance.
When formally evaluating a strategy, you should also examine maximum drawdown, number of trades, win rate, and returns after costs. Use out-of-sample data to verify whether the strategy remains effective. For strategies with many parameters, avoid repeatedly adjusting them until the historical equity curve looks best, as this can increase the risk of overfitting.
Another easily overlooked issue is data coverage. Five hundred hourly candles represent approximately 20 days of data. This is not enough to determine whether a strategy remains stable across different market cycles. Longer historical datasets, tests under different volatility conditions, and checks for missing intervals are also important to the reliability of a backtest.
From Historical Backtesting to Real-Time Market Data
Historical data is suitable for developing and validating strategies, while real-time market data can be used for subsequent monitoring and signal updates. AllTick provides cryptocurrency market data and supports REST and WebSocket connections. Developers can first establish their research workflow using historical K-line data, then assess whether a real-time data stream is necessary based on the strategy’s required update frequency and data granularity.
Before development, review the AllTick Crypto API product page and the K-line API technical documentation to confirm available trading symbols, historical data coverage, API limits, and request parameters.
A reliable quantitative trading strategy does more than generate buy and sell signals. It must allow developers to trace the data source, explain execution assumptions, and evaluate results after trading costs are deducted. Only when both the data and the backtesting logic withstand scrutiny can historical performance serve as a useful reference.