Exploring Quantitative Trading with CCXT and Backtrader

ยท

What Are CCXT and Backtrader?

CCXT is an open-source project designed for cryptocurrency exchanges. It provides a unified API interface that enables users to trade across different exchanges. In this context, we'll use CCXT's Python interface for trading.

Backtrader is a Python-based quantitative trading backtesting framework known for its simplicity and ease of use.

Both libraries can be easily installed via pip:

pip install ccxt backtrader

Using CCXT

Several key concepts in CCXT:

  1. Exchange: The trading platform, including Binance, OKX, etc. Here, we'll use Binance's interface.
  2. Symbol: The trading pair, such as BTC/USDT or ETH/USDT. The first currency is the base currency, while the second is the quote currency. For example, BTC/USDT represents the price of one BTC in USDT.

Creating an Exchange Object

To use CCXT, you first need to create an exchange object. Here's how to do it for Binance:

exchange = ccxt.binance({
    "enableRateLimit": True,
    "proxy": {
        "http": "127.0.0.1:7890",
        "https": "127.0.0.1:7890"
    }
})

For fetching market data, API keys aren't required. However, for trading or account queries, you'll need them:

exchange.apiKey = "your api key"
exchange.secret = "your secret key"
# Test
balance = exchange.fetch_balance()
# If no error occurs, the API key and secret are correct

Fetching Historical Data

To query historical data, use exchange.fetch_ohlcv. OHLCV stands for Open, High, Low, Close, Volume. Here's how to fetch daily data:

symbol = "BTC/USDT"
time_interval = '1d'
since_time = datetime(2021, 1, 1)
to_time = datetime(2024, 8, 1)
df = pd.DataFrame()
while since_time < to_time:
    since = exchange.parse8601(since_time.strftime("%Y-%m-%d %H:%M:%S"))
    data = exchange.fetch_ohlcv(symbol=symbol,
                               timeframe=time_interval,
                               since=since,
                               limit=500)
    new_df = pd.DataFrame(data, dtype=float)
    new_df.rename(columns={0: 'MTS', 1: 'open', 2: 'high', 3: 'low', 4: 'close', 5: 'volume'}, inplace=True)
    new_df['candle_begin_time'] = pd.to_datetime(new_df['MTS'], unit='ms')
    new_df['candle_begin_time_GMT8'] = new_df['candle_begin_time'] + timedelta(hours=8)
    new_df = new_df[['candle_begin_time_GMT8', 'open', 'high', 'low', 'close', 'volume']]
    df = pd.concat([df, new_df], ignore_index=True)
    since_time = df['candle_begin_time_GMT8'].iloc[-1] + timedelta(days=1)
df.to_csv(dataset_path, index=False)

The resulting CSV file will have six columns: timestamp, open price, high price, low price, closing price, and volume.

๐Ÿ‘‰ Learn more about cryptocurrency trading strategies

Backtrader Backtesting

Backtrader operates around a Cerebro (Spanish for "brain") instance. The general workflow is:

  1. Create a Cerebro instance.
  2. Add data.
  3. Add strategies.
  4. Run backtests.

Data Sources: backtrader.feeds

Backtrader uses feeds to provide data sources. These feeds could be from Yahoo Finance, CSV files, or Pandas DataFrames. Here, we'll use the CSV file created earlier:

data = btfeeds.GenericCSVData(
    dataname=dataset_path,
    timeframe=bt.TimeFrame.Minutes,
    fromdate=datetime(2021, 1, 1),
    todate=datetime(2024, 8, 1),
    nullvalue=0.0,
    dtformat=('%Y-%m-%d %H:%M:%S'),
    datetime=0,
    open=1,
    high=2,
    low=3,
    close=4,
    volume=5,
    openinterest=-1
)

Strategies: backtrader.Strategy

Create your own strategy by inheriting from bt.Strategy:

class TestStrategy(bt.Strategy):
    params = (
        ("maperiod", 15),
        ("printlog", False),
    )
    
    def log(self, txt, dt=None, doprint=False):
        if self.params.printlog or doprint:
            dt = dt or self.datas[0].datetime.date(0)
            logging.info("{}, {}".format(dt.isoformat(), txt))

    def __init__(self):
        self.dataclose = self.datas[0].close
        self.order = None
        self.buyprice = None
        self.buycomm = None
        self.sma = btind.SimpleMovingAverage(
            self.datas[0], period=self.params.maperiod)
        
    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            return
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log("BUY EXECUTED, Price: {:.2f}, Cost: {:.2f}, Comm: {:.2f}".format(
                    order.executed.price,
                    order.executed.value,
                    order.executed.comm),
                    doprint=True)
            else:
                self.log("SELL EXECUTED, Price: {:.2f}, Cost: {:.2f}, Comm: {:.2f}".format(
                    order.executed.price,
                    order.executed.value,
                    order.executed.comm),
                    doprint=True)
    
    def notify_trade(self, trade):
        if not trade.isclosed:
            return
        self.log("OPERATION PROFIT, GROSS: {:.2f}, NET: {:.2f}".format(
            trade.pnl, trade.pnlcomm),
            doprint=True)
    
    def next(self):
        self.log("Close, {:.2f}".format(self.dataclose[0]))
        if self.order:
            return
        if not self.position:
            if self.dataclose[0] > self.sma[0]:
                self.log("BUY CREATE, {:.2f}".format(self.dataclose[0]))
                self.order = self.buy()
        else:
            if self.dataclose[0] < self.sma[0]:
                self.log("SELL CREATE, {:.2f}".format(self.dataclose[0]))
                self.order = self.sell()
    
    def stop(self):
        self.log("MA Period: {}, Ending Value: {}".format(self.params.maperiod, self.broker.getvalue()),
            doprint=True)

This strategy implements a simple moving average crossover system:

Additional Cerebro Settings

cerebro = bt.Cerebro()
cerebro.addstrategy(TestStrategy)
cerebro.adddata(data)
cerebro.broker.setcash(1000)
cerebro.broker.setcommission(commission=0.001)
cerebro.addsizer(bt.sizers.FixedSize, stake=0.01)
cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio')
cerebro.run()
cerebro.plot()

For performance analysis, we use QuantStats:

returns = cerebro.run()
pyfoliozer = result[0].analyzers.getbyname('pyfolio')
returns, positions, transactions, gross_lev = pyfoliozer.get_pf_items()
qs.reports.html(returns, output='backtest.html', title='Backtest Report')

๐Ÿ‘‰ Discover advanced trading techniques

FAQ

1. What's the difference between CCXT and Backtrader?
CCXT focuses on connecting to exchanges and fetching market data, while Backtrader specializes in strategy backtesting and execution.

2. Do I need API keys to fetch historical data?
No, API keys are only required for trading or accessing account-specific information.

3. What brokers does Backtrader support?
Backtrader has built-in support for simulated brokers. For live trading, you can connect to various brokers through CCXT or other interfaces.

4. Can I use Backtrader for stocks or forex?
Yes, Backtrader is asset-class agnostic. While our example uses cryptocurrency, it works equally well with stocks, forex, or other instruments.

5. How do I optimize my strategy parameters?