Daily Crypto Bollinger Band Thresholds in a CCXT Backtest
Summary
This Lumibot example demonstrates a daily cryptocurrency strategy using Bollinger Bands with historical data supplied through CCXT backtesting. It calculates a rolling mean and standard deviation over a configurable window, then derives upper and lower bands and the price’s normalized position within them. The strategy buys when the prior observation’s band position falls below a negative threshold and sells when it rises above a value greater than one, treating unusually low and high readings as entry and exit signals.
The example sizes purchases as a configurable fraction of available cash divided by the latest price, and sells the recorded quantity. It sets a 24-hour market schedule and illustrates a crypto pair and exchange over a stated date range. The document provides implementation details but no backtest results or evidence that the thresholds are profitable. Its signal thresholds lie outside the usual band range, and practical outcomes will depend on data timing, execution assumptions, fees, and the selected exchange and pair.
Key ideas
- The strategy calculates daily Bollinger Bands from a rolling window of closing prices.
- It buys on a prior band-position reading below a negative threshold and sells above a threshold greater than one.
- Purchase size is based on a configurable fraction of cash and the latest price.
- The example uses market orders and records the quantity opened for its later exit.
- No performance results are provided, and the example depends on backtest data and execution assumptions.
Tags
Full text
# ccxt_backtesting_example.py
```py
from datetime import datetime
from pandas import DataFrame
from lumibot.backtesting import CcxtBacktesting
from lumibot.entities import Asset, Order
from lumibot.strategies.strategy import Strategy
class CcxtBacktestingExampleStrategy(Strategy):
def initialize(self, asset:tuple[Asset,Asset] = None,
cash_at_risk:float=.25,window:int=21):
if asset is None:
raise ValueError("You must provide a valid asset pair")
# for crypto, market is 24/7
self.set_market("24/7")
self.sleeptime = "1D"
self.asset = asset
self.base, self.quote = asset
self.window = window
self.symbol = f"{self.base.symbol}/{self.quote.symbol}"
self.last_trade = None
self.order_quantity = 0.0
self.cash_at_risk = cash_at_risk
def _position_sizing(self):
cash = self.get_cash()
last_price = self.get_last_price(asset=self.asset,quote=self.quote)
if last_price is None:
return cash, last_price, 0.0
quantity = round(cash * self.cash_at_risk / last_price,0)
return cash, last_price, quantity
def _get_historical_prices(self):
return self.get_historical_prices(asset=self.asset,length=self.window,
timestep="day",quote=self.quote).df
def _get_bbands(self,history_df:DataFrame):
# BBL (Lower Bollinger Band): Can act as a support level based on price volatility, and can indicate an 'oversold' condition if the price falls below this line.
# BBM (Breaking Bollinger Bands): This is essentially a moving average over a selected period of time, used as a reference point for price trends.
# BBU (Upper Bollinger Band): Can act as a resistance level based on price volatility, and can indicate an 'overbought' condition if the price moves above this line.
# BBB (Bollinger Band Width): Indicates the distance between the upper and lower bands, with a higher value indicating a more volatile market.
# BBP (Bollinger Band Percentage): This shows where the current price is located within the Bollinger Bands as a percentage, where a value close to 0 means that the price is close to the lower band, and a value close to 1 means that the price is close to the upper band.
# return bbands
num_std_dev = 2.0
close = 'close'
df = DataFrame(index=history_df.index)
df[close] = history_df[close]
df['bbm'] = df[close].rolling(window=self.window).mean()
df['bbu'] = df['bbm'] + df[close].rolling(window=self.window).std() * num_std_dev
df['bbl'] = df['bbm'] - df[close].rolling(window=self.window).std() * num_std_dev
df['bbb'] = (df['bbu'] - df['bbl']) / df['bbm']
df['bbp'] = (df[close] - df['bbl']) / (df['bbu'] - df['bbl'])
return df
def on_trading_iteration(self):
# During the backtest, we get the current time with self.get_datetime().
# The time interval is self.sleeptime.
current_dt = self.get_datetime()
cash, last_price, quantity = self._position_sizing()
history_df = self._get_historical_prices()
bbands = self._get_bbands(history_df)
prev_bbp = bbands[bbands.index < current_dt].tail(1).bbp.values[0]
if prev_bbp < -0.13 and cash > 0 and self.last_trade != Order.OrderSide.BUY and quantity > 0.0:
order = self.create_order(self.base,
quantity,
side = Order.OrderSide.BUY,
type = Order.OrderType.MARKET,
quote=self.quote)
self.submit_order(order)
self.last_trade = Order.OrderSide.BUY
self.order_quantity = quantity
self.log_message(f"Last buy trade was at {current_dt}")
elif prev_bbp > 1.2 and self.last_trade != Order.OrderSide.SELL and self.order_quantity > 0.0:
order = self.create_order(self.base,
self.order_quantity,
side = Order.OrderSide.SELL,
type = Order.OrderType.MARKET,
quote=self.quote)
self.submit_order(order)
self.last_trade = Order.OrderSide.SELL
self.order_quantity = 0.0
self.log_message(f"Last sell trade was at {current_dt}")
if __name__ == "__main__":
base_symbol = "ETH"
quote_symbol = "USDT"
start_date = datetime(2023,2,11)
end_date = datetime(2024,2,12)
asset = (Asset(symbol=base_symbol, asset_type="crypto"),
Asset(symbol=quote_symbol, asset_type="crypto"))
exchange_id = "kraken" #"kucoin" #"bybit" #"okx" #"bitmex" # "binance"
# CcxtBacktesting default data download limit is 50,000
# If you want to change the maximum data download limit, you can do so by using 'max_data_download_limit'.
kwargs = {
# "max_data_download_limit":10000, # optional
"exchange_id":exchange_id,
}
CcxtBacktesting.MIN_TIMESTEP = "day"
results, strat_obj = CcxtBacktestingExampleStrategy.run_backtest(
CcxtBacktesting,
start_date,
end_date,
benchmark_asset=f"{base_symbol}/{quote_symbol}",
quote_asset=Asset(symbol=quote_symbol, asset_type="crypto"),
parameters={
"asset":asset,
"cash_at_risk":.25,
"window":21,},
**kwargs,
)
```Shown in full with attribution under the source's licence. Licence: GPL-3.0
This summary was written by Stratmill's research agent from the original; it is not a copy of the source.