MiniQMT+Backtrader:平民股票量化界的“黄金搭档”,永远的神!

我一直觉得,有行情、有回测、免费、想用什么库随便装,这才量化框架该有的样子。

可现实里,很多所谓的"量化平台"却给了我们截然相反的体感:

平台封闭、扩展性差、行情接口限制多甚至收费;策略想法很美好,但是想要实现却被平台限制得死死的。要接个外部数据源、装个库,都得绕来绕去;数据用别人的,逻辑被限制,连回测速度都要看平台心情。

更别提,有的平台主打"零代码""拖拉拽",对稍微有点编程能力的朋友来说,反而成了束缚:啥都能点,但啥都调不了。

直到花姐遇到了 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()
相关推荐
IT_102416 分钟前
Spring Boot项目开发实战销售管理系统——系统设计!
大数据·spring boot·后端
ai小鬼头1 小时前
AIStarter最新版怎么卸载AI项目?一键删除操作指南(附路径设置技巧)
前端·后端·github
Touper.1 小时前
SpringBoot -- 自动配置原理
java·spring boot·后端
思则变2 小时前
[Pytest] [Part 2]增加 log功能
开发语言·python·pytest
一只叫煤球的猫2 小时前
普通程序员,从开发到管理岗,为什么我越升职越痛苦?
前端·后端·全栈
一只鹿鹿鹿2 小时前
信息化项目验收,软件工程评审和检查表单
大数据·人工智能·后端·智慧城市·软件工程
漫谈网络2 小时前
WebSocket 在前后端的完整使用流程
javascript·python·websocket
专注VB编程开发20年2 小时前
开机自动后台运行,在Windows服务中托管ASP.NET Core
windows·后端·asp.net
程序员岳焱2 小时前
Java 与 MySQL 性能优化:MySQL全文检索查询优化实践
后端·mysql·性能优化
一只叫煤球的猫3 小时前
手撕@Transactional!别再问事务为什么失效了!Spring-tx源码全面解析!
后端·spring·面试