Crypto Backtesting with CCXT Data Adapter¶
This notebook demonstrates how to use the CCXTAdapter to fetch cryptocurrency data from multiple exchanges and perform a simple backtesting strategy.
Features Demonstrated:¶
- Fetching crypto OHLCV data from Binance, Coinbase, and Kraken
- Data validation and schema verification
- Multi-exchange price comparison
- Simple moving average crossover strategy
- Performance metrics calculation
📋 Notebook Information
- RustyBT Version: 0.1.2+
- Last Validated: 2025-11-07
- API Compatibility: Verified ✅
- Documentation: API Reference
In [ ]:
Copied!
# Import required libraries
import contextlib
import matplotlib.pyplot as plt
import pandas as pd
import polars as pl
from rustybt.data.adapters import CCXTAdapter
# Import required libraries
import contextlib
import matplotlib.pyplot as plt
import pandas as pd
import polars as pl
from rustybt.data.adapters import CCXTAdapter
1. Initialize CCXTAdapter¶
Create adapters for different exchanges. By default, CCXT uses spot markets.
In [ ]:
Copied!
# Initialize adapters for different exchanges
binance_adapter = CCXTAdapter(exchange_id="binance")
coinbase_adapter = CCXTAdapter(exchange_id="coinbase")
kraken_adapter = CCXTAdapter(exchange_id="kraken")
# Initialize adapters for different exchanges
binance_adapter = CCXTAdapter(exchange_id="binance")
coinbase_adapter = CCXTAdapter(exchange_id="coinbase")
kraken_adapter = CCXTAdapter(exchange_id="kraken")
2. Fetch BTC/USDT Data from Binance¶
Fetch 30 days of hourly BTC/USDT data from Binance.
In [ ]:
Copied!
# Fetch BTC/USDT data from Binance
btc_data = await binance_adapter.fetch(
symbols=["BTC/USDT"],
start_date=pd.Timestamp("2024-01-01"),
end_date=pd.Timestamp("2024-01-31"),
resolution="1h",
)
# Fetch BTC/USDT data from Binance
btc_data = await binance_adapter.fetch(
symbols=["BTC/USDT"],
start_date=pd.Timestamp("2024-01-01"),
end_date=pd.Timestamp("2024-01-31"),
resolution="1h",
)
3. Validate Data Quality¶
Verify OHLCV relationships and data integrity.
In [ ]:
Copied!
# Validate data
try:
binance_adapter.validate(btc_data)
print("✅ Data validation passed - OHLCV relationships are valid")
except Exception as e:
print(f"⚠️ Validation warning: {e}")
# Validate data
try:
binance_adapter.validate(btc_data)
print("✅ Data validation passed - OHLCV relationships are valid")
except Exception as e:
print(f"⚠️ Validation warning: {e}")
4. Multi-Exchange Price Comparison¶
Compare BTC prices across Binance, Coinbase, and Kraken.
In [ ]:
Copied!
# Fetch daily BTC data from multiple exchanges
exchanges_data = {}
# Binance: BTC/USDT
binance_btc = await binance_adapter.fetch(
symbols=["BTC/USDT"],
start_date=pd.Timestamp("2024-01-01"),
end_date=pd.Timestamp("2024-01-31"),
resolution="1d",
)
exchanges_data["Binance"] = binance_btc
# Coinbase: BTC/USD
coinbase_btc = await coinbase_adapter.fetch(
symbols=["BTC/USD"],
start_date=pd.Timestamp("2024-01-01"),
end_date=pd.Timestamp("2024-01-31"),
resolution="1d",
)
exchanges_data["Coinbase"] = coinbase_btc
# Kraken: BTC/USD
kraken_btc = await kraken_adapter.fetch(
symbols=["BTC/USD"],
start_date=pd.Timestamp("2024-01-01"),
end_date=pd.Timestamp("2024-01-31"),
resolution="1d",
)
exchanges_data["Kraken"] = kraken_btc
# Display summary for each exchange
for exchange, data in exchanges_data.items():
print(f"\n{exchange}:")
print(f" Rows: {len(data)}")
print(f" Date range: {data['timestamp'].min()} to {data['timestamp'].max()}")
# Fetch daily BTC data from multiple exchanges
exchanges_data = {}
# Binance: BTC/USDT
binance_btc = await binance_adapter.fetch(
symbols=["BTC/USDT"],
start_date=pd.Timestamp("2024-01-01"),
end_date=pd.Timestamp("2024-01-31"),
resolution="1d",
)
exchanges_data["Binance"] = binance_btc
# Coinbase: BTC/USD
coinbase_btc = await coinbase_adapter.fetch(
symbols=["BTC/USD"],
start_date=pd.Timestamp("2024-01-01"),
end_date=pd.Timestamp("2024-01-31"),
resolution="1d",
)
exchanges_data["Coinbase"] = coinbase_btc
# Kraken: BTC/USD
kraken_btc = await kraken_adapter.fetch(
symbols=["BTC/USD"],
start_date=pd.Timestamp("2024-01-01"),
end_date=pd.Timestamp("2024-01-31"),
resolution="1d",
)
exchanges_data["Kraken"] = kraken_btc
# Display summary for each exchange
for exchange, data in exchanges_data.items():
print(f"\n{exchange}:")
print(f" Rows: {len(data)}")
print(f" Date range: {data['timestamp'].min()} to {data['timestamp'].max()}")
In [ ]:
Copied!
# Plot price comparison
plt.figure(figsize=(14, 7))
for exchange, data in exchanges_data.items():
# Convert to pandas for plotting
df_pd = data.to_pandas()
df_pd["close_float"] = df_pd["close"].astype(float)
plt.plot(df_pd["timestamp"], df_pd["close_float"], label=exchange, linewidth=2)
plt.title("BTC Price Comparison Across Exchanges", fontsize=16, fontweight="bold")
plt.xlabel("Date", fontsize=12)
plt.ylabel("Price (USD)", fontsize=12)
plt.legend(fontsize=10)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Plot price comparison
plt.figure(figsize=(14, 7))
for exchange, data in exchanges_data.items():
# Convert to pandas for plotting
df_pd = data.to_pandas()
df_pd["close_float"] = df_pd["close"].astype(float)
plt.plot(df_pd["timestamp"], df_pd["close_float"], label=exchange, linewidth=2)
plt.title("BTC Price Comparison Across Exchanges", fontsize=16, fontweight="bold")
plt.xlabel("Date", fontsize=12)
plt.ylabel("Price (USD)", fontsize=12)
plt.legend(fontsize=10)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
5. Simple Moving Average Crossover Strategy¶
Implement a basic SMA crossover strategy:
- Buy when short SMA crosses above long SMA
- Sell when short SMA crosses below long SMA
In [ ]:
Copied!
# Calculate moving averages
btc_strategy = btc_data.clone()
# Convert Decimal to float for rolling calculations
btc_strategy = btc_strategy.with_columns([pl.col("close").cast(pl.Float64).alias("close_float")])
# Calculate SMAs
btc_strategy = btc_strategy.with_columns(
[
pl.col("close_float").rolling_mean(window_size=20).alias("sma_20"),
pl.col("close_float").rolling_mean(window_size=50).alias("sma_50"),
]
)
# Generate signals
btc_strategy = btc_strategy.with_columns(
[(pl.col("sma_20") > pl.col("sma_50")).cast(pl.Int32).alias("signal")]
)
# Detect crossovers (signal changes)
btc_strategy = btc_strategy.with_columns(
[(pl.col("signal") - pl.col("signal").shift(1)).alias("position_change")]
)
# Calculate moving averages
btc_strategy = btc_data.clone()
# Convert Decimal to float for rolling calculations
btc_strategy = btc_strategy.with_columns([pl.col("close").cast(pl.Float64).alias("close_float")])
# Calculate SMAs
btc_strategy = btc_strategy.with_columns(
[
pl.col("close_float").rolling_mean(window_size=20).alias("sma_20"),
pl.col("close_float").rolling_mean(window_size=50).alias("sma_50"),
]
)
# Generate signals
btc_strategy = btc_strategy.with_columns(
[(pl.col("sma_20") > pl.col("sma_50")).cast(pl.Int32).alias("signal")]
)
# Detect crossovers (signal changes)
btc_strategy = btc_strategy.with_columns(
[(pl.col("signal") - pl.col("signal").shift(1)).alias("position_change")]
)
In [ ]:
Copied!
# Plot strategy signals
strategy_pd = btc_strategy.to_pandas()
plt.figure(figsize=(14, 7))
# Plot price and SMAs
plt.plot(
strategy_pd["timestamp"],
strategy_pd["close_float"],
label="BTC/USDT Price",
linewidth=2,
alpha=0.7,
)
plt.plot(
strategy_pd["timestamp"], strategy_pd["sma_20"], label="SMA 20", linewidth=1.5, linestyle="--"
)
plt.plot(
strategy_pd["timestamp"], strategy_pd["sma_50"], label="SMA 50", linewidth=1.5, linestyle="--"
)
# Mark buy signals (crossover up)
buys = strategy_pd[strategy_pd["position_change"] == 1]
plt.scatter(
buys["timestamp"],
buys["close_float"],
color="green",
marker="^",
s=200,
label="Buy Signal",
zorder=5,
)
# Mark sell signals (crossover down)
sells = strategy_pd[strategy_pd["position_change"] == -1]
plt.scatter(
sells["timestamp"],
sells["close_float"],
color="red",
marker="v",
s=200,
label="Sell Signal",
zorder=5,
)
plt.title("Moving Average Crossover Strategy - BTC/USDT", fontsize=16, fontweight="bold")
plt.xlabel("Date", fontsize=12)
plt.ylabel("Price (USD)", fontsize=12)
plt.legend(fontsize=10)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
# Plot strategy signals
strategy_pd = btc_strategy.to_pandas()
plt.figure(figsize=(14, 7))
# Plot price and SMAs
plt.plot(
strategy_pd["timestamp"],
strategy_pd["close_float"],
label="BTC/USDT Price",
linewidth=2,
alpha=0.7,
)
plt.plot(
strategy_pd["timestamp"], strategy_pd["sma_20"], label="SMA 20", linewidth=1.5, linestyle="--"
)
plt.plot(
strategy_pd["timestamp"], strategy_pd["sma_50"], label="SMA 50", linewidth=1.5, linestyle="--"
)
# Mark buy signals (crossover up)
buys = strategy_pd[strategy_pd["position_change"] == 1]
plt.scatter(
buys["timestamp"],
buys["close_float"],
color="green",
marker="^",
s=200,
label="Buy Signal",
zorder=5,
)
# Mark sell signals (crossover down)
sells = strategy_pd[strategy_pd["position_change"] == -1]
plt.scatter(
sells["timestamp"],
sells["close_float"],
color="red",
marker="v",
s=200,
label="Sell Signal",
zorder=5,
)
plt.title("Moving Average Crossover Strategy - BTC/USDT", fontsize=16, fontweight="bold")
plt.xlabel("Date", fontsize=12)
plt.ylabel("Price (USD)", fontsize=12)
plt.legend(fontsize=10)
plt.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()
6. Calculate Simple Performance Metrics¶
Calculate basic performance metrics for the strategy.
In [ ]:
Copied!
# Calculate returns
btc_strategy = btc_strategy.with_columns(
[(pl.col("close_float") / pl.col("close_float").shift(1) - 1).alias("returns")]
)
# Calculate strategy returns (only when signal = 1)
btc_strategy = btc_strategy.with_columns(
[(pl.col("returns") * pl.col("signal").shift(1)).alias("strategy_returns")]
)
# Remove NaN values
btc_strategy_clean = btc_strategy.drop_nulls()
# Calculate cumulative returns
returns_pd = btc_strategy_clean.to_pandas()
returns_pd["cumulative_returns"] = (1 + returns_pd["returns"]).cumprod() - 1
returns_pd["cumulative_strategy_returns"] = (1 + returns_pd["strategy_returns"]).cumprod() - 1
# Calculate metrics
total_return = returns_pd["cumulative_returns"].iloc[-1]
strategy_return = returns_pd["cumulative_strategy_returns"].iloc[-1]
# Calculate returns
btc_strategy = btc_strategy.with_columns(
[(pl.col("close_float") / pl.col("close_float").shift(1) - 1).alias("returns")]
)
# Calculate strategy returns (only when signal = 1)
btc_strategy = btc_strategy.with_columns(
[(pl.col("returns") * pl.col("signal").shift(1)).alias("strategy_returns")]
)
# Remove NaN values
btc_strategy_clean = btc_strategy.drop_nulls()
# Calculate cumulative returns
returns_pd = btc_strategy_clean.to_pandas()
returns_pd["cumulative_returns"] = (1 + returns_pd["returns"]).cumprod() - 1
returns_pd["cumulative_strategy_returns"] = (1 + returns_pd["strategy_returns"]).cumprod() - 1
# Calculate metrics
total_return = returns_pd["cumulative_returns"].iloc[-1]
strategy_return = returns_pd["cumulative_strategy_returns"].iloc[-1]
In [ ]:
Copied!
# Plot cumulative returns
plt.figure(figsize=(14, 7))
plt.plot(
returns_pd["timestamp"], returns_pd["cumulative_returns"] * 100, label="Buy & Hold", linewidth=2
)
plt.plot(
returns_pd["timestamp"],
returns_pd["cumulative_strategy_returns"] * 100,
label="SMA Crossover Strategy",
linewidth=2,
)
plt.title("Cumulative Returns Comparison", fontsize=16, fontweight="bold")
plt.xlabel("Date", fontsize=12)
plt.ylabel("Cumulative Return (%)", fontsize=12)
plt.legend(fontsize=10)
plt.grid(True, alpha=0.3)
plt.axhline(y=0, color="black", linestyle="-", linewidth=0.5)
plt.tight_layout()
plt.show()
# Plot cumulative returns
plt.figure(figsize=(14, 7))
plt.plot(
returns_pd["timestamp"], returns_pd["cumulative_returns"] * 100, label="Buy & Hold", linewidth=2
)
plt.plot(
returns_pd["timestamp"],
returns_pd["cumulative_strategy_returns"] * 100,
label="SMA Crossover Strategy",
linewidth=2,
)
plt.title("Cumulative Returns Comparison", fontsize=16, fontweight="bold")
plt.xlabel("Date", fontsize=12)
plt.ylabel("Cumulative Return (%)", fontsize=12)
plt.legend(fontsize=10)
plt.grid(True, alpha=0.3)
plt.axhline(y=0, color="black", linestyle="-", linewidth=0.5)
plt.tight_layout()
plt.show()
7. CCXT Adapter Configuration Options¶
The CCXTAdapter supports various configuration options:
In [ ]:
Copied!
# Example: Using testnet mode (if available)
testnet_adapter = CCXTAdapter(exchange_id="binance", testnet=True)
# Example: With API credentials (for private endpoints)
# authenticated_adapter = CCXTAdapter(
# exchange_id='binance',
# api_key='your_api_key',
# api_secret='your_api_secret'
# )
# Supported resolutions
# Rate limiting is automatically handled
# Example: Using testnet mode (if available)
testnet_adapter = CCXTAdapter(exchange_id="binance", testnet=True)
# Example: With API credentials (for private endpoints)
# authenticated_adapter = CCXTAdapter(
# exchange_id='binance',
# api_key='your_api_key',
# api_secret='your_api_secret'
# )
# Supported resolutions
# Rate limiting is automatically handled
Summary¶
This notebook demonstrated:
- ✅ Data Fetching: Retrieved crypto OHLCV data from multiple exchanges
- ✅ Data Validation: Verified data quality and OHLCV relationships
- ✅ Multi-Exchange Comparison: Compared prices across Binance, Coinbase, and Kraken
- ✅ Strategy Implementation: Built a simple moving average crossover strategy
- ✅ Performance Analysis: Calculated and visualized strategy returns
Next Steps¶
- Implement more sophisticated strategies (RSI, MACD, Bollinger Bands)
- Add transaction costs and slippage
- Use RustyBT's full backtesting engine for complete portfolio simulation
- Optimize strategy parameters using walk-forward analysis
- Test on multiple cryptocurrencies and timeframes