我一直觉得,有行情、有回测、免费、想用什么库随便装,这才量化框架该有的样子。
可现实里,很多所谓的"量化平台"却给了我们截然相反的体感:
平台封闭、扩展性差、行情接口限制多甚至收费;策略想法很美好,但是想要实现却被平台限制得死死的。要接个外部数据源、装个库,都得绕来绕去;数据用别人的,逻辑被限制,连回测速度都要看平台心情。
更别提,有的平台主打"零代码""拖拉拽",对稍微有点编程能力的朋友来说,反而成了束缚:啥都能点,但啥都调不了。
直到花姐遇到了 MiniQMT + Backtrader,才是真正实现了量化自由。
今天这篇文章,花姐就带你从0到1打造一个年化22.77%的策略。
整个量化框架用的就是MiniQMT + Backtrader
行情数据从哪里来
看过花姐文章的,肯定知道花姐之前一直用的是AKShare这个库,从东财来获取行情数据,但是,近期东财反爬虫机制导致AKShare库非常不稳定,经常被封IP,数据无法获取。
查了很多资料以后,终于发现一个免费获取股票行情的方法------XtQuant XtQuant是基于迅投MiniQMT衍生出来的一套完善的Python策略运行框架,其中Xtdata负责提供行情数据(历史和实时的K线和分笔)、财务数据、合约基础信息、板块和行业分类信息等通用的行情数据
Xttrader负责实盘交易,可以和 MiniQMT 客户端交互进行报单、撤单、查询资产、查询委托、查询成交、查询持仓以及接收资金、委托、成交和持仓等变动的主推消息。
最关键的是MiniQMT和XtQuant都是免费的,唯一不足的就是XtQuant不支持策略回测,所以才有了这篇文章,给它加上Backtrader就形成了一个完整的闭环。

接下来说说策略 方便大家搞懂XtQuant+Backtrader是如何进行回测的,我这里给大家准备了一个回测收益还不错的策略。
策略的核心是低位开仓+网格交易
当日线出现底分型,且底分型是近20个K线的最低点,这个时候我们就买1/5的仓位,当最新买入的仓位跌了5%就再加仓1/5的仓位,直到满仓为止,如果其中某个1/5的仓位盈利了8%我们就止盈,直到全部仓位为0以后我们在从头开始这个循环。
策略有了怎么实现
首先打开券商提供的QMT客户端,然后勾选独立交易点登录,就进入到MiniQMT的模式了。你的QMT是否支持miniQMT就需要你去问券商了。

接下来创建一个Python虚拟环境,然后从XtQuant官网下载XtQuant的Python包 这是下载地址 https://dict.thinktrader.net/nativeApi/download_xtquant.html
下载以后是个rar压缩文件,解压以后把它放到你Python对应虚拟环境的site-packages文件夹夹中
比如花姐的是H:\program\anaconda\envs\pyqmt\Lib\site-packages

XtQuant包安装以后,就可以获取行情数据了
python
from xtquant import xtdata
code = "xxxxxx.SZ"
period = "1d"
xtdata.download_history_data(stock_code=code,period=period,incrementally=True)
# 增量下载行情数据(开高低收,等等)到本地
history_data = xtdata.get_market_data_ex([],[code],period=period,count=-1,dividend_type="front_ratio")
df = history_data[code]
dividend_type 除权方式,可选值为 'none':不复权 'front':前复权 'back':后复权 'front_ratio': 等比前复权 'back_ratio': 等比后复权
有了行情数据,接下来就该Backtrader上场了
Backtrader 是一个用 Python 编写的开源量化回测框架,专为策略研究、历史回测和模拟交易设计。它结构清晰、功能强大,在量化圈中被广泛用于个人和教育场景,堪称"轻量级回测中的天花板"。 以下是一个最小Backtrader策略结构模板:
python
import backtrader as bt
# 1. 策略类定义
class MyStrategy(bt.Strategy):
def __init__(self):
# 初始化指标,比如简单均线
self.ma = bt.indicators.SimpleMovingAverage(self.data.close, period=10)
def next(self):
# 简单策略逻辑:价格上穿均线买入,下穿卖出
if self.data.close[0] > self.ma[0] and not self.position:
self.buy()
elif self.data.close[0] < self.ma[0] and self.position:
self.sell()
# 2. 创建Cerebro引擎
cerebro = bt.Cerebro()
cerebro.addstrategy(MyStrategy)
# 3. 加载数据(以CSV为例)
data = bt.feeds.GenericCSVData(
dataname='your_data.csv', # 请替换成你的CSV路径
dtformat='%Y-%m-%d',
timeframe=bt.TimeFrame.Days,
openinterest=-1,
volume=-1,
open=1,
high=2,
low=3,
close=4,
datetime=0
)
cerebro.adddata(data)
# 4. 设置初始资金
cerebro.broker.setcash(100000)
# 5. 启动回测
cerebro.run()
# 6. 输出回测结果
print(f"最终资金: {cerebro.broker.getvalue():.2f}")
# 7. 可视化
cerebro.plot()
上完整策略代码
最基本的知识都掌握了,接下来我们用 XtQuant + Backtrader 来实现一开始说的策略 策略开始之前先看看收益情况
bash
总收益率: 28.57%
年化收益率: 22.77%
日均收益率: 0.0814%
当前回撤: 1.65%
当前亏损金额: 2229.56
当前回撤持续时间: 7 天
最大历史回撤: 18.21%
最大历史亏损金额: 18802.03
最大回撤持续时间: 71 天
夏普比率: 1.67


python
import backtrader as bt
import pandas as pd
import math
from xtquant import xtdata
from datetime import datetime
def get_hq(code):
period = "1d"
xtdata.enable_hello = False
xtdata.download_history_data(stock_code=code, period=period, incrementally=True)
history_data = xtdata.get_market_data_ex([], [code], period=period, count=-1, dividend_type="front_ratio")
df = history_data[code]
df['openinterest'] = 0
df.index = pd.to_datetime(df.index.astype(str), format='%Y%m%d')
return df
class MyStrategy(bt.Strategy):
params = dict(
grid_pct_down=0.05,
grid_pct_up=0.08,
max_open=5,#最大开仓次数,每次开仓五分之一
min_unit=100
)
def __init__(self):
self.order_list = []
self.grid_positions = [] # 记录每一层网格的买入价和数量
self.last_buy_price = None #记录最近的一次买入价格
self.trade_log = []
def log(self, txt):
dt = self.datas[0].datetime.date(0)
print(f'{dt.isoformat()} - {txt}')
def next(self):
data = self.datas[0]
pos = self.getposition(data).size
# print(data.datetime.date(0) ,pos)
# 底分型识别逻辑(通俗处理:中间K线最低点比前一日和后一日都低)
low_past20 = [data.low[i] for i in range(-20, 0)]
is_bottom = (
data.low[-1] < data.low[0] and data.low[-1] < data.low[-2] and
data.low[-1] == min(low_past20)
)
# 当前总市值
# total_value = self.broker.getvalue()
cash = self.broker.get_cash()
cur_price = data.close[0]
if pos == 0 and is_bottom:
# 初次开仓
size = math.floor(cash /self.p.max_open/ cur_price / self.p.min_unit) * self.p.min_unit
if size > 0:
self.last_buy_price = cur_price
self.grid_positions.append((cur_price, size))
order = self.buy(size=size)
self.order_list.append(order)
self.log_trade(data._name, "BUY", cur_price, size)
elif pos > 0:
# 止盈卖出 先卖出现有止盈的持仓
new_grid_positions = []
for buy_price, size in self.grid_positions:
if cur_price >= buy_price * (1 + self.p.grid_pct_up):
order = self.sell(size=size)
self.order_list.append(order)
self.log_trade(data._name, "SELL", cur_price, size)
else:
new_grid_positions.append((buy_price, size))
self.grid_positions = new_grid_positions
# 网格加仓
if self.grid_positions:
# last_price, last_size = self.grid_positions[-1]
# 补仓
if cur_price < self.last_buy_price * (1 - self.p.grid_pct_down):
left_open = self.p.max_open - len(self.grid_positions) #剩余开仓次数
if left_open>0:
size = math.floor(cash / left_open /cur_price / self.p.min_unit) * self.p.min_unit
if size > 0:
self.last_buy_price = cur_price
self.grid_positions.append((cur_price, size))
order = self.buy(size=size)
self.order_list.append(order)
self.log_trade(data._name, "BUY", cur_price, size)
def notify_order(self, order):
if order.status in [order.Completed]:
self.order_list.remove(order)
def log_trade(self, symbol, side, price, size):
trade = {
'symbol': symbol,
'datetime': self.datas[0].datetime.datetime(0),
'side': side,
'price': price,
'size': size
}
self.trade_log.append(trade)
def stop(self):
df = pd.DataFrame(self.trade_log)
df.to_csv('trade_log.csv', index=False)
self.log("交易记录已保存至 trade_log.csv")
if __name__ == "__main__":
code = 'xxxxxx.SZ'
df = get_hq(code)
cerebro = bt.Cerebro()
data = bt.feeds.PandasData(dataname=df, name=code ,fromdate=datetime(2024, 1, 1))
cerebro.adddata(data)
cerebro.addstrategy(MyStrategy)
cerebro.broker.setcash(100000)
cerebro.addanalyzer(bt.analyzers.Transactions, _name='txn')
cerebro.addanalyzer(bt.analyzers.Returns, _name='returns')
cerebro.addanalyzer(bt.analyzers.SharpeRatio, _name='sharpe')
cerebro.addanalyzer(bt.analyzers.DrawDown, _name='drawdown')
cerebro.addanalyzer(bt.analyzers.TimeReturn, _name='timereturn')
result = cerebro.run()
strategy = result[0]
# 获取分析结果
returns = strategy.analyzers.returns.get_analysis()
sharpe = strategy.analyzers.sharpe.get_analysis()
drawdown = strategy.analyzers.drawdown.get_analysis()
txn = strategy.analyzers.txn.get_analysis()
print(f"总收益率: {returns['rtot']:.2%}")
print(f"年化收益率: {returns['rnorm']:.2%}")
print(f"日均收益率: {returns['ravg']:.4%}")
print(f"当前回撤: {drawdown['drawdown']:.2f}%")
print(f"当前亏损金额: {drawdown['moneydown']:.2f}")
print(f"当前回撤持续时间: {drawdown['len']} 天")
print(f"最大历史回撤: {drawdown['max']['drawdown']:.2f}%")
print(f"最大历史亏损金额: {drawdown['max']['moneydown']:.2f}")
print(f"最大回撤持续时间: {drawdown['max']['len']} 天")
print(f"夏普比率: {sharpe['sharperatio']:.2f}")
print(f"交易记录为:{txn}")
returns = strategy.analyzers.timereturn.get_analysis()
returns_series = pd.Series(returns)
cum_returns = (1 + returns_series).cumprod()
import matplotlib.pyplot as plt
plt.rcParams['font.family'] = 'SimHei' # 黑体
plt.rcParams['axes.unicode_minus'] = False # 正确显示负号
cum_returns.plot(title='资金曲线(净值)')
cerebro.plot()