Building Robust Trading Strategies with a Python Backtesting Framework
A few days ago we saw how to build a simple backtesting framework for algorithmic trading using the backtrader
Python library.
I showcased a special class called StrategyForComparison
that allowed for a flexible trading approach with various risk management options.
Now let’s see how you’d utilize it in backtest.py
code.
I’ll walk through a powerful backtesting framework I’ve been using that offers comprehensive testing capabilities for algorithmic trading strategies, with code examples to help you understand how it all works.
What is This Backtesting Framework?
At its core, this framework leverages the popular backtrader
Python library alongside other essential packages like pandas
, quantstats
, and various technical analysis tools. Let’s look at the main components:
import backtrader as bt
import backtrader.analyzers as btanalyzers
import pandas as pd
import quantstats
import inflect
Key Features That Make This Framework Valuable
1. Strategy Ecosystem
The framework includes over 25 trading strategies, all organized in a central dictionary for easy selection:
_STRATEGIES = {
'bb': BollingerAndRsi,
'bb_fib': BollingerBands_Fib_Strategy,
'cpr': CprSingleTimeframe,
'golden_cross': GoldenCross,
'ichimoku': Ichimoku,
'mean_reversion': MeanReversion,
'vwap': VWAP_RSI,
# Many more strategies available...
}
This makes it incredibly simple to test different approaches - just change the strategy parameter when executing the script.
2. Command-Line Interface with Argument Parsing
The framework uses argparse
to provide a flexible command-line interface:
def parse_args():
parser = argparse.ArgumentParser(description='Backtest Runner')
parser.add_argument('--interval', '-i', default='1d')
parser.add_argument('--fromyear', '-f', default=2000, help='Starting date in YYYY format')
parser.add_argument('--toyear', '-t', default=2021, help='Starting date in YYYY format')
parser.add_argument('--cash', default=100000, type=int, help='Starting Cash')
parser.add_argument('--comm', default=0, type=float, help='Commission for operation')
parser.add_argument('--plot', '-p', action='store_true', help='Plot the read data')
parser.add_argument("-sym", '--symbols', nargs='+', help='<Required> Set flag', default=['AAPL'])
parser.add_argument("-s", "--strategy", help="which strategy to run", type=str)
# More arguments available...
return parser.parse_args()
This allows for extensive customization of your backtests from a single command line.
3. Fractional Position Sizing
Unlike many basic frameworks, this one supports fractional position sizing, which is essential for realistic testing:
class CommInfoFractional(bt.CommissionInfo):
def getsize(self, price, cash):
'''Returns fractional size for cash operation @price'''
return self.p.leverage * (cash / price)
# Later in the code:
cerebro.broker.addcommissioninfo(CommInfoFractional(commission=args.comm))
4. Comprehensive Analysis with Multiple Analyzers
The framework adds a battery of analyzers to evaluate strategy performance:
def add_analyzers(cerebro):
# Add analyzers
cerebro.addanalyzer(btanalyzers.SharpeRatio, _name='mysharpe')
cerebro.addanalyzer(btanalyzers.Returns, _name='myreturn')
cerebro.addanalyzer(btanalyzers.DrawDown, _name='mydrawdown')
cerebro.addanalyzer(btanalyzers.Transactions, _name='mytransactions')
cerebro.addanalyzer(btanalyzers.TradeAnalyzer, _name='myanalyzer')
cerebro.addanalyzer(btanalyzers.SQN, _name='mysqn')
cerebro.addanalyzer(bt.analyzers.PyFolio, _name='pyfolio')
5. Detailed Transaction Logging
Every trade is meticulously logged with all relevant details:
def show_transactions(backtest, interval):
logger.info("*** Transactions ***")
analysis = backtest.analyzers.mytransactions.get_analysis()
for key in backtest.analyzers.mytransactions.get_analysis().keys():
ticker_analysis = backtest.analyzers.mytransactions.get_analysis()[key][0]
side_text_s = ["Buy " if x < 0 else "Sell" for x in [ticker_analysis[4]]][0]
side_sign = ' ' if 'Buy' in side_text_s else ''
date_s = _get_date(key, interval)
logger.info(
f'Symbol:{ticker_analysis[3]}; '
f'Date: {date_s}; '
f'Price: {ticker_analysis[1]:5.2f}; '
f'Type: {side_text_s}; '
f'# Shares:{side_sign} {ticker_analysis[0]:.2f}')
6. Enhanced Analytics with QuantStats Integration
The framework creates beautiful HTML reports using the QuantStats library:
def create_quant_stats(backtest_finished, strategy, output_file_name, interval, symbols):
strat = backtest_finished[0]
portfolio_stats = strat.analyzers.getbyname('pyfolio')
returns, positions, transactions, gross_lev = portfolio_stats.get_pf_items()
returns.index = returns.index.tz_convert(None)
metrics = quantstats.reports.metrics(returns, mode='full', display=False)
# Save metrics to tracking system
algo_metrics = metrics['Strategy']
algo_metrics['Symbol'] = symbols
algo_metrics['Algo'] = strategy
algo_metrics['Timeframe'] = interval
tracker.save_df(algo_metrics)
# Generate HTML report
quantstats.reports.html(
returns,
output=output_file_name,
download_filename=output_file_name,
title=strategy)
A Practical Example: Running the Framework
Let’s say I want to test a Golden Cross strategy (50-day/200-day MA crossover) on Apple stock from 2018-2023 with $10,000 initial capital:
python backtest_runner.py --strategy golden_cross --symbols AAPL --fromyear 2018 --toyear 2023 --cash 10000 --analysis --quant
Under the hood, here’s what happens in the runstrategy()
function:
def runstrategy():
start = time.time()
args = parse_args()
# Create directories for reports
os.makedirs(os.path.join(_REPORTS_DIR, args.strategy), exist_ok=True)
os.makedirs(os.path.join(_TRADES_DIR, args.strategy), exist_ok=True)
os.makedirs(os.path.join(_TRANSACTIONS_DIR, args.strategy), exist_ok=True)
# Set up logging
# ...
# Create cerebro engine
cerebro = bt.Cerebro(cheat_on_open=args.cheat_on_open)
# Add strategy
STRATEGY = [_STRATEGIES[args.strategy]]
for strategy in STRATEGY:
cerebro.addstrategy(strategy)
add_analyzers(cerebro)
# Get stock data
stock_data_dict = data_reader.get_stock_data(
args.symbols, args.fromyear, args.toyear, args.interval, args.data_style,
args.data_dir, args.timezone)
# Add data to cerebro
for stock in stock_data_dict:
cerebro.adddata(stock_data_dict[stock], stock)
# Set starting cash and commission model
cerebro.broker.setcash(args.cash)
cerebro.broker.addcommissioninfo(CommInfoFractional(commission=args.comm))
# Run backtest
backtest_finished = cerebro.run()
backtest = backtest_finished[0]
# Generate reports
if args.report:
create_report_csv(cerebro, backtest, args.strategy)
create_transactions_csv(cerebro, backtest, args.interval, args.strategy)
if args.quant:
symbols = '_'.join(args.symbols)
output_file_name = os.path.join(
_REPORTS_DIR, args.strategy,
f'quantstats-{args.strategy}-{symbols}-{args.interval}.html')
create_quant_stats(
backtest_finished, args.strategy, output_file_name, args.interval,
symbols)
# Display results
if args.analysis:
show_results(cerebro, backtest, beginning_cash)
if args.plot:
cerebro.plot(iplot=False)
Output Analysis: Understanding the Results
The framework generates comprehensive performance metrics:
def show_results(cerebro, backtest, beginning_cash):
analysis = backtest.analyzers.myanalyzer.get_analysis()
# Calculate performance metrics
portfolio_value = cerebro.broker.getvalue()
roi = (int)((portfolio_value - beginning_cash) * 100 / beginning_cash)
logger.info(f'Final Portfolio Value: {show_numbers(portfolio_value)}')
logger.info(f'ROI = {roi_s} %')
# Show win/loss statistics
logger.info(f"Number of winners: {analysis['won']['total']}")
logger.info(f"Total profit - winners: {show_numbers(analysis['won']['pnl']['total'])}")
logger.info(f"Number of losers: {analysis['lost']['total']}")
logger.info(f"Total loss - losers: {show_numbers(analysis['lost']['pnl']['total'])}")
# Calculate accuracy
total_accuracy = (analysis['won']['total'] /
(analysis['won']['total'] + analysis['lost']['total'])) * 100
logger.info(f"Total accuracy: {total_accuracy}")
# Show key metrics
logger.info(f"Sharpe Ratio: {backtest.analyzers.mysharpe.get_analysis()['sharperatio']}")
logger.info(f"Max drawdown (pct): {_get_analysis_value('mydrawdown', 'max')['drawdown']:.2f}")
logger.info(f"SQN: {_get_analysis_value('mysqn', 'sqn'):.3f}")
Why This Framework Matters
For Individual Traders
With just a few lines of code, you can test sophisticated strategies:
# Test multiple strategies on a single stock
python backtest_runner.py --strategy golden_cross --symbols AAPL --quant
python backtest_runner.py --strategy rsimacd --symbols AAPL --quant
python backtest_runner.py --strategy supertrend --symbols AAPL --quant
# Compare the HTML reports to determine the best approach
For Quants and Researchers
The framework makes it easy to implement custom strategies. All you need to do is:
- Create a new strategy class that inherits from
bt.Strategy
- Add it to the
_STRATEGIES
dictionary - Run your backtest
# Add to strategies directory, then add to _STRATEGIES dictionary:
_STRATEGIES['my_custom_strategy'] = MyCustomStrategy
For Teams
The CSV exports create a standardized format for sharing results:
def create_report_csv(cerebro, backtest, strategy):
# ...
trades = []
trade_column = [
'symbol', 'buy_date', 'buy_price', 'sell_date', 'sell_price', 'size',
'profit', 'profit_pct'
]
# Extract trade data
# ...
# Write to CSV
report_csv_name = os.path.join(
_TRADES_DIR, strategy, f'trades-{symbol}.csv')
with open(report_csv_name, 'wt') as f:
f.write(','.join(trade_column) + '\n')
f.write('\n'.join(trades))
f.write('\n')
Conclusion
This backtesting framework represents a significant time-saver for anyone serious about algorithmic trading. The code examples shared above demonstrate how accessible yet powerful this system is, combining the strengths of backtrader
with enhanced reporting through quantstats
.
Whether you’re a casual trader exploring technical indicators or a professional quant developing proprietary algorithms, this framework provides the foundation needed to thoroughly validate trading approaches before committing real capital.
In future posts, I’ll dive deeper into specific strategies and show how to extend this framework with custom indicators and risk management techniques.
Happy trading!
Update
I’ve open-sourced the whole repo at abhi1010/backtrader-strategies-compendium.
Disclaimer: This code and analysis are provided for educational purposes only. Always conduct your own analysis and risk management when implementing trading strategies.