← All tools · Guides

backtrader: a first strategy on China A-share daily bars

Hands-on, part 3. Run a dual moving average on real China A-share daily bars. The point is an honest backtest, not a flattering return.

Short answer: install backtrader, feed it an AKShare daily table for one A-share, and run a 5-day / 20-day moving-average cross. Leave the fill on the next open. The snippet charges commission of 0.0003 and slippage of 0.1%, and it does not charge the 0.05% sell-side stamp tax. Read Sharpe and maximum drawdown against the CSI 300, and do not trust a result that used today's close twice.

How to choose: For a first backtest on your own computer, use backtrader and start with one dual moving average: buy 100 shares when the 5-day average crosses above the 20-day average, and sell when it crosses back. The signal is known only after the close, and the order fills at the next bar's open. Do not fill it at the same day's close. The snippet sets commission to 0.0003 and slippage to 0.1%. The comment in the Chinese source calls that commission a simplified both-sides rate of 2.5 per 10,000, which is not the same number. Keep the code at 0.0003 and treat it as simplified. setcommission does not split buy and sell, and the Chinese page also adds a 0.05% stamp tax on the sell for a real A-share bill. Prices use forward adjustment, adjust="qfq". Choosing among nine frameworks is a different article. This one only runs the first strategy.

Who it is for: Someone who already writes a little Python and has China A-share daily bars from AKShare, or who will load the DataFrame from the data walkthrough. The goal is one honest dual moving-average run: next-open fills, a simplified cost, how to read Sharpe and maximum drawdown, and the four mistakes this page refuses.

Who it is not for: Someone who wants a hosted site to supply the data and the compute, or who is choosing a broker terminal or a trading bot. It is also the wrong page for comparing nine backtesting frameworks, or for picking a maintained replacement for backtrader. It is not the tool-order guide and not the site FAQ. If you do not have daily bars yet, start with the AKShare data walkthrough.

The same walkthrough in Chinese is backtrader: a first strategy. It is part 3 of that series. The previous part, which this page feeds, is AKShare into SQLite. The same walkthrough in Chinese is the AKShare data walkthrough (Chinese). Comparing nine frameworks is how to choose a backtesting framework. A shorter note on whether backtrader is still maintained, and four alternatives, is backtrader and maintained alternatives. That note is not this article.

Facts below follow the Chinese walkthrough. The code was not re-run for this English page. This site has no commercial relationship with backtrader or AKShare. This is not investment advice. A historical backtest is not future performance.

01 · Why backtrader

backtrader is pure Python and event-driven, and the community around it is mature. For a person who wants to check one idea and see a result, it is the short path. The install has no heavy dependencies:

pip install backtrader
# Optional: plotting needs matplotlib, and older releases fit better (matplotlib>=3.8 errors).
# This walkthrough prints metrics and skips the plot install.

02 · Feed the data: an AKShare DataFrame into backtrader

Use the cleaned shape from the data walkthrough. The index is a DatetimeIndex and the columns are lowercase English names, so PandasData can read the frame directly. AKShare returns the date, open, high, low, close, and volume under Chinese column names. The snippet renames those columns. The strings in the source below are those Chinese names:

import akshare as ak
import backtrader as bt
import pandas as pd

df = ak.stock_zh_a_hist(symbol="000001", period="daily",
                        start_date="20230101", end_date="20250801", adjust="qfq")
df["date"] = pd.to_datetime(df["日期"])
df = df.rename(columns={"开盘": "open", "最高": "high", "最低": "low",
                        "收盘": "close", "成交量": "volume"})
df = df.set_index("date")[["open", "high", "low", "close", "volume"]]

symbol="000001" is one A-share, period="daily" is daily bars, and adjust="qfq" is forward-adjusted prices. The window in the snippet is 1 Jan 2023 through 1 Aug 2025.

03 · The first strategy: a dual moving average

Buy when the short average crosses above the long average, and sell when it crosses back. That is the usual teaching strategy:

class SmaCross(bt.Strategy):
    params = dict(fast=5, slow=20)

    def __init__(self):
        ma_fast = bt.ind.SMA(self.data.close, period=self.p.fast)
        ma_slow = bt.ind.SMA(self.data.close, period=self.p.slow)
        self.crossover = bt.ind.CrossOver(ma_fast, ma_slow)

    def next(self):
        if not self.position and self.crossover > 0:
            self.buy(size=100)        # one China A-share board lot is 100 shares
        elif self.position and self.crossover < 0:
            self.close()

Fill rule: an order placed inside next() fills, by default, at the open of the next bar. That is the honest rule. The signal is confirmed after the close, and the trade can happen only on a later bar. If you change the fill to the same day's close, you are using a price the signal did not have.

04 · Run it, and put an A-share cost in

cerebro = bt.Cerebro()
cerebro.adddata(bt.feeds.PandasData(dataname=df))
cerebro.addstrategy(SmaCross)
cerebro.broker.setcash(100000)
cerebro.broker.setcommission(commission=0.0003)   # 0.0003, simplified, same rate both sides
cerebro.broker.set_slippage_perc(0.001)           # 0.1% slippage

cerebro.addanalyzer(bt.analyzers.Returns, _name="ret")
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name="sharpe", timeframe=bt.TimeFrame.Days)
cerebro.addanalyzer(bt.analyzers.DrawDown, _name="dd")

result = cerebro.run()
print("ending value:", round(cerebro.broker.getvalue(), 0))
print("annualized return:", result[0].analyzers.ret.get_analysis()["rnorm"])
print("Sharpe:", result[0].analyzers.sharpe.get_analysis()["sharperatio"])
print("max drawdown: %.1f%%" % result[0].analyzers.dd.get_analysis()["max"]["drawdown"])

What the cost leaves out: the code sets commission=0.0003 and slippage of 0.1% (0.001). Starting cash is 100,000. The comment in the Chinese source, on that same commission line, calls the rate a simplified both-sides commission of 2.5 per 10,000. 2.5 per 10,000 is 0.00025, not 0.0003. This page does not change the code. Run 0.0003, and treat the comment as a label for a simplified rate, not as a second number to type.

setcommission does not charge a different rate on the sell. The Chinese page states the real A-share bill as commission on both sides plus a sell-side stamp tax of 0.05%. For a strategy that trades rarely, the gap is small. An exact cost needs a commission class that splits the two sides. This walkthrough stays on the simplified rate.

05 · How to read the result

06 · Four mistakes to refuse

Once the backtest is a habit, a daily data update and a weekly scheduled run need a machine that stays on. That is the job of the cloud quant desk. The same article in Chinese is the Chinese setup guide. The Chinese edition of this page also links an Alibaba Cloud new-customer promo. The price is the same as going there yourself, and the discount is whatever the official page shows that day. The link on the Chinese page is an affiliate link.

After this backtest passes, run about two weeks of paper trading before any live order. That walkthrough is paper trading after a backtest. The same article in Chinese is paper trading (Chinese).

Frequently asked questions

How to choose a first backtrader strategy?

For a first backtest on your own computer, use backtrader and start with one dual moving average: buy 100 shares when the 5-day average crosses above the 20-day average, and sell when it crosses back. The signal is known only after the close, and the order fills at the next bar's open. Do not fill it at the same day's close. The snippet sets commission to 0.0003 and slippage to 0.1%. The comment in the Chinese source calls that commission a simplified both-sides rate of 2.5 per 10,000, which is not the same number. Keep the code at 0.0003 and treat it as simplified. setcommission does not split buy and sell, and the Chinese page also adds a 0.05% stamp tax on the sell for a real A-share bill. Prices use forward adjustment, adjust="qfq". Choosing among nine frameworks is a different article. This one only runs the first strategy.

Who it is for?

Someone who already writes a little Python and has China A-share daily bars from AKShare, or who will load the DataFrame from the data walkthrough. The goal is one honest dual moving-average run: next-open fills, a simplified cost, how to read Sharpe and maximum drawdown, and the four mistakes this page refuses.

Who it is not for?

Someone who wants a hosted site to supply the data and the compute, or who is choosing a broker terminal or a trading bot. It is also the wrong page for comparing nine backtesting frameworks, or for picking a maintained replacement for backtrader. It is not the tool-order guide and not the site FAQ. If you do not have daily bars yet, start with the AKShare data walkthrough.

When does a backtrader order fill?

By default, an order placed inside next() fills at the next bar's open. The signal is confirmed only after the close, so the earliest honest fill is the next bar. Filling at the same day's close uses a price the trade did not have. That is lookahead.

How do you read Sharpe and maximum drawdown?

On this page, a long-run Sharpe above 1 is acceptable, a Sharpe above 2 is a reason to look for lookahead first, and a negative Sharpe is worse than holding still. Maximum drawdown decides whether you would have stayed with the rule: most people abandon a 30% drawdown by hand at -15%. Also compare the same dates with the CSI 300. Beating that benchmark is the extra return. Otherwise an index would have done the job.

Which four mistakes does this walkthrough refuse?

Lookahead: a signal from today's close filled at today's close. Missing costs: no commission and no slippage, so a strategy that trades often looks profitable. Overfitting: tuning fast and slow until the history looks best, which fits noise. Hold out a stretch of data that never takes part in the tuning, and look at it once, at the end. Bad adjustment: unadjusted prices invent a gap on an ex-dividend day. Use qfq, forward-adjusted prices, and refresh the whole history on a schedule.

This tutorial is not investment advice. A historical backtest is not future performance. The stamp tax and the commission comment are stated as the Chinese page states them. Broker fees change. Check the bill you would actually pay before you treat a backtest as a cost.