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()
相关推荐
间彧35 分钟前
Windows Server,如何使用WSFC+nginx实现集群故障转移
后端
间彧41 分钟前
Nginx + Keepalived 实现高可用集群(Linux下)
后端
间彧42 分钟前
在Kubernetes中如何部署高可用的Nginx Ingress Controller?
后端
间彧44 分钟前
Ribbon负载均衡器和Nginx负载均衡器有什么区别
后端
间彧1 小时前
Nacos详解与项目实战
后端
间彧1 小时前
nginx、网关Gateway、Nacos、多个服务实例之间的数据链路详解
后端
间彧1 小时前
Nacos与Eureka在性能上有哪些具体差异?
后端
间彧1 小时前
详解Nacos健康状态监测机制
后端
间彧1 小时前
如何利用Nacos实现配置的灰度发布?
后端
毕业设计制作和分享1 小时前
springboot159基于springboot框架开发的景区民宿预约系统的设计与实现
java·spring boot·后端