基于backtrader和miniQMT/FutuOpen API的量化交易系统设计与实现

本文介绍了作者设计和实现Python量化交易系统的过程,包括使用Backtrader进行回测,自定义数据源,以及借助qstock进行实盘交易。重点讨论了选股、回测和实盘交易的步骤,并提到了一些关键的第三方库如Tushare、TA-Lib和pyecharts。还分享了一个极简实盘交易的示例,以及如何处理Backtrader的数据源。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >


进展:截止到8月中旬,完成了backtrader和miniQMT的对接,支持多股多策略执行。解决了分钟线合入到日线的问题,这样指标计算就基于最新行情和历史日线数据了。完善了界面实时行情刷新。目前在网上还没有看到相关案例。欢迎大家加我微信(makuku76)交流。

2025年初,实盘投入运行。对接A股和美股。

前言

经过一段时间准备,在国金也开了户,准备着手搭建自己的量化交易系统了。因为没有一款现有的系统适合自己的全部需求,考虑到今后长期的发展维护,肯定是自研更合适。当然,也不会全部自己造轮子,二八开吧,80%的东西取材网上。

目标:基本自动运行和下单,少量人工干预,收益率超过10%,满足赚点小生活费的目标。 主要还是以了解股市为主。

本文将记录下个人实践中遇到的主要问题。
下面是本人开发的界面预览:
在这里插入图片描述在这里插入图片描述在这里插入图片描述

系统架构

下图是一个相对完整的架构,但未必全部需要实现。初期主要以回测和策略编写为主。
在这里插入图片描述个人思路 通过qstock或miniQMT取行情数据,喂给backtrader做回测,然后调用miniQMT做交易。
miniQMT只能跑在windows上?这好解决,给它包一层RESTful service,这样你的量化系统就可以运行在macOS或其它平台上了。

实现过程

本文倒序法,先讲重要的。三部曲:选股、回测、实盘。选股建立自己的股票池,然后用不同策略跑回测,最后对接券商交易API,让量化交易系统不间断跑起来,振荡频率可以是每分钟取一次行情。

选股

第一步、选股。直接采用问财的API,界面也照搬:
在这里插入图片描述

回测

第二步,回测。策略和Signal要动态加载和切换。

实盘

第三步,实盘。

  • 自定义数据源。Backder自定义数据源需要继承DataBase,实现其中的_load()方法,把数据赋值给lines成员变量。DataBase继承AbstractDataBase。
class MyDataBase(DataBase):
	def islive(self):
		return True # 如果返回False是用历史数据回测
	def start(self):
		# 初始化
	def stop(self):
		# 清理动作
	def _load(self):
		# 不断获取行情数据
		## return True  代表从数据源获取数据成功
        ## return False 代表因为某种原因(比如历史数据源全部数据已经输出完毕)数据源关闭
        ## return None  代表暂时无法从数据源获取最新数据,但是以后会有(比如实时数据源中最新的bar还未生成)

示例:

class BianceData(DataBase):
    params = (
        ('tradingCoin', None),
        ('targetCoin', None),
        ('interval', None),
        ('fromDate', '1970-01-01 00:00:00'),
        ('toDate', '2099-01-01 23:59:59'),
        ('name', ''),
    )
 
    def __init__(self):
        self.result = None
        api_key = 'xxxxxxxxxxxxxxxxxxxxx'
        api_secret = 'xxxxxxxxxxxxxxxxxxxxxx'
        self.client = Client(api_key, api_secret)
 
    def start(self):
        symbol = self.p.tradingCoin.upper()+self.p.targetCoin.upper()
        fromDate = int(dt.datetime.strptime(self.p.fromDate, '%Y-%m-%d %H:%M:%S').timestamp()*1000)
        toDate = int(dt.datetime.strptime(self.p.toDate, '%Y-%m-%d %H:%M:%S').timestamp()*1000)
        data = self.client.get_historical_klines(symbol=symbol, interval=self.p.interval, start_str=fromDate, end_str=toDate)
        self.result =iter(data)
 
    def stop(self):
        pass
 
    def _load(self):
        if self.result is None:
            return False
        try:
            one_row = next(self.result)
        except StopIteration:
            return False
        datetime = dt.datetime.fromtimestamp(one_row[0]//1000)
        self.lines.datetime[0] = date2num(datetime)
        self.lines.open[0] = float(one_row[1])
        self.lines.high[0] = float(one_row[2])
        self.lines.low[0] = float(one_row[3])
        self.lines.close[0] = float(one_row[4])
        self.lines.volume[0] = int(one_row[8])
        self.lines.openinterest[0] = -1
        return True

自定义Broker,实现其中next()方法。不同于bt用于回测的Broker,其内部有getposition的实现,而自定义的Broker需要自己管理仓位,实现getposition()方法。

一个极简实盘交易的完整示例

下面是完整示例,极具参考价值。网上很多实盘接入的例子的基本原型就是此例。

import datetime
import random
from collections import deque

from backtrader import BrokerBase, DataBase, BackBroker
import backtrader as bt
import pandas as pd


class MyStrategy(bt.Strategy):
    def __init__(self):
    		# 添加其它lines数据
        self.dataclose = self.datas[0].close
        self.high = {d: d.lines.high for d in self.datas}

    def log(self, txt, dt=None):
        ''' 记录交易过程'''
        dt = dt or self.datas[0].datetime.date(0)
        print('%s, %s' % (dt.isoformat(), txt))

    def notify_data(self, data, status, *args, **kwargs):
        self.live_data = True

    def notify_order(self, order):
        print(order)

    def next(self):
        # 此处完成实际买卖
        self.log('Close, %.2f' % self.dataclose[0])
        if ... :
           self.sell()
        else:
           self.buy()
        if self.live_data:
            pass

class MyFeed(DataBase):
    # 定义状态机
    _ST_LIVE, _ST_HISTORBACK, _ST_OVER = range(3)

    def __init__(self):
        super(MyFeed, self).__init__()
        self._laststatus = DataBase.LIVE
        self._data = deque()

    def haslivedata(self):
        return self._state == self._ST_LIVE and self._data

    def islive(self):
        return True

    def start(self):
        print('MyFeed start...')
        DataBase.start(self)
        self._start_live()


    def stop(self):
        pass

    def _start_live(self):
        self._state = self._ST_LIVE
        self.put_notification(self.LIVE)

    def _load(self):
        if self._state == self._ST_OVER:
            return False
        print('加载行情数据 ...')
        from_date = datetime.datetime.utcnow() - datetime.timedelta(minutes=5 * 16)
        self.lines.datetime[0] = self.date2num(datetime.datetime.strptime('20240101', '%Y%m%d'))
        self.lines.open[0] = random.randint(10, 20)
        self.lines.high[0] = random.randint(20, 30)
        self.lines.low[0] = random.randint(1, 10)
        self.lines.close[0] = random.randint(1, 30)
        self.lines.volume[0] = random.randint(1000, 200000)
        self.lines.openinterest[0] = -1

        return True ## 一定要返回True,这是让trader一直运行的关键


class MyBroker(BackBroker):
    def __init__(self):
        super(MyBroker, self).__init__()

    def next(self):
        super(MyBroker, self).next()

    def get_notification(self):
        return super(MyBroker, self).get_notification()
    
    def buy(self, strategy, data, **kwargs):
      ## 此处对接券商交易系统
      return super(MyBroker, self).buy(strategy, data, **kwargs)

    def sell(self, strategy, data, **kwargs):
      ## 此处对接券商交易系统
      return super(MyBroker, self).sell(strategy, data, **kwargs)

if __name__ == '__main__':
    broker = MyBroker()

    cerebro = bt.Cerebro(quicknotify=True)
    cerebro.setbroker(broker)
    cerebro.broker.setcash(100000.0)
    cerebro.addstrategy(MyStrategy)
    cerebro.adddata(MyFeed())
    cerebro.run()

在策略中,可以调三个函数完成下单:buy、sell和close。真实的交易动作是在Broker的buy()和sell()方法里完成的,这两个方法也是需要我们自己去实现的。原型在BrokerBase类中:

    def buy(self, owner, data, size, price=None, plimit=None,
            exectype=None, valid=None, tradeid=0, oco=None,
            trailamount=None, trailpercent=None,
            **kwargs):

        raise NotImplementedError

    def sell(self, owner, data, size, price=None, plimit=None,
             exectype=None, valid=None, tradeid=0, oco=None,
             trailamount=None, trailpercent=None,
             **kwargs):

        raise NotImplementedError

值得注意的是:MyBroker类里重载BackBroker里的方法时,要记得调用父类里的方法,否则影响backtrader的计算。

涉及到的第三方库

  • NumPy、pandas、Matplotlib
  • Tushare、Baostock
  • TA-Lib
  • echarts、pyecharts
  • wencai、pywencai

为何选pyecharts作为界面展示?

比如目前非常流行的echarts库,它是百度开源的基于Javascript的可视化库,用于生成商业级数 据图表,可以流畅的运行在PC和移动设备上,兼容当前绝大部分浏览器(IE6/7/8/9/10/11, chrome,firefox,Safari等),用它所生成的图表可视化效果非常好。
但是使用echarts还是需要一定的前端知识,为了使它能够与Python对接,有团队推出了Python的 网页版可视化库pyecharts,无需涉及任何前端的编程,仅仅利用几行Python代码就能轻松在 Python中生成echarts风格的图表,通过浏览器即可打开查看,使用起来很简单,图表效果也非常 美观大方。我们就采用了wxPython结合pyecharts这种实现方案,这个组合在搭建自己的量化交易系统中非常 有用。
pyecharts 分为 v0.5.x 和 v1 两个大版本,v0.5.x 版本将不再进行维护,v1是新版本系列, 从 v1.0.0 开始,很不幸的是v0.5.x 和v1 间不兼容。此处的例程更新至1.7.0版本的pyecharts,一定要注意两个版本使用方法差别较大。

Backtrader数据源处理

在给backtrader喂数据时,难免遇到形形色色的外部数据格式,可以用下面两种方法处理之。

方式一:列名转换和日期自定义

外部数据是DataFrame,但格式不符合backtrader要求,需要转一下列名。同时需要转一下日期字段,要转成Timestamp对象,否则会报错:AttributeError: 'str' object has no attribute 'to_pydatetime'

        data = pd.read_csv(r'data/100100.csv', parse_dates=True)
        data = change_column_name(data)
        data['datetime'] = pd.to_datetime(data['datetime'], format='%Y-%m-%d')

        # 筛选需要的数据
        daily_price = data[["datetime", "open", "high", "low", "close", "volume"]]
        # daily_price = daily_price.set_index("datetime", verify_integrity=True)
        # daily_price.set_index('datetime', inplace=True)

        cerebro = bt.Cerebro()

        # dataframe = MyData(dataname=daily_price)
        dataframe = bt.feeds.PandasData(dataname=daily_price, datetime=-1)
        
        cerebro.adddata(dataframe)
        # 添加策略
        cerebro.addstrategy(MyStrategy)
        # 运行回测
        cerebro.run()
        # 绘制结果
        cerebro.plot()

其中datetime=-1 是让backtrader直接采用名称为datatime的列。也可以直接指定datetime所在的列,如datetime=6

def change_column_name(data):
    name_clomuns = data.columns.tolist()
    new_name_dict = {}

    for name in name_clomuns:
        if name == '日期':
            new_name_dict[name] = 'datetime'
        if name == '开盘价':
            new_name_dict[name] = 'open'
        if name == '股票代码':
            new_name_dict[name] = 'code'
    
    data.rename(columns=new_name_dict, inplace=True) ## 就地转换,把data修改掉
    return data

方式二:自定义GenericCSVData类型

数据源为外部csv文件,但是是随意的格式,如:

datetime    name    code  open   high   low  close   volume      turnover  turnover_rate
2024-03-01  三六零  601360  8.78   9.65  8.73   9.65  3480811  3.241301e+09           4.87
2024-03-04  三六零  601360  9.79  10.20  9.63   9.99  4404500  4.376126e+09           6.16
2024-03-05  三六零  601360  9.80   9.99  9.56   9.69  2821916  2.756809e+09           3.95
2024-03-06  三六零  601360  9.51   9.70  9.37   9.53  2000944  1.906219e+09           2.80
2024-03-07  三六零  601360  9.51   9.63  9.11   9.15  2050286  1.916216e+09           2.87

即以空格为分隔符,则可以定义自己的类型:

class MyHLOC(btfeed.GenericCSVData):
    params = (
        ('fromdate', datetime.datetime(2000, 1, 1)),
        ('todate', datetime.datetime(2024, 12, 31)),
        ('nullvalue', 0.0),
        ('dtformat', ('%Y-%m-%d')),
        ('tmformat', ('%H.%M.%S')),
        ('separator', ' '),

        ('datetime', 0),
        # ('time', 1),
        ('high', 4),
        ('low', 5),
        ('open', 3),
        ('close', 6),
        ('volume', 7),
        ('openinterest', -1)
    )

使用之:

data = MyHLOC(dataname='data/601360.txt')
cerebro.adddata(data)

qstock+backtrader示例

qstock获取行情, 然后backtrader做回测。给backtrader喂的数据包括以下类别的数据:Open(开盘价), High(最高价), Low(最低价), Close(收盘价), Volume(成交量), OpenInterest(无的话设置为0), open_interest就是指未平仓合约,是在某一交收月份期货市场中未通过抵消或交收套现的合约数,国内股市用不到。

策略类

import backtrader as bt
import qstock  # 这是一个假设性的导入,具体要根据 qstock 的实际使用方式来定

# 定义策略类
class MyStrategy(bt.Strategy):
    params = (
        ('someparam', 10),
    )

    def log(self, txt, dt=None):
        dt = dt or self.datas[0].datetime.date(0)
        print(f'{dt.isoformat()}, {txt}')

    def __init__(self):
        # 假设我们添加了一个移动平均线指标
        self.sma = bt.indicators.SimpleMovingAverage(self.datas[0], period=10)

    def notify_order(self, order):
        if order.status in [order.Submitted, order.Accepted]:
            # 订单提交和接受
            return
        if order.status in [order.Completed]:
            if order.isbuy():
                self.log(f'BUY EXECUTED, Price: {order.executed.price}, Cost: {order.executed.value}, Comm: {order.executed.comm}')
            elif order.issell():
                self.log(f'SELL EXECUTED, Price: {order.executed.price}, Cost: {order.executed.value}, Comm: {order.executed.comm}')
            self.bar_executed = len(self)
        elif order.status in [order.Canceled, order.Margin, order.Rejected]:
            self.log('Order Canceled/Margin/Rejected')
        self.order = None

    def notify_trade(self, trade):
        if not trade.isclosed:
            return
        self.log(f'OPERATION PROFIT, GROSS: {trade.pnl}, NET: {trade.pnlcomm}')

    def next(self):
        # 简单的买入卖出逻辑
        if self.sma > self.datas[0].close[0]:
            self.buy()
        elif self.sma < self.datas[0].close[0]:
            self.sell()



主程序

    # 获取数据
    his_data = qstock.get_data('603533', start='20240220', end='20240410', freq='d')     
    his_data['datetime'] = his_data.index ## qstock将日期作为索引了
  
    # 创建 Backtrader Cerebro 实例
    cerebro = bt.Cerebro()
    data = bt.feeds.PandasData(dataname=his_data, datetime=-1)
    # 添加数据源
    cerebro.adddata(data)

    # 添加策略
    cerebro.addstrategy(MyStrategy)
    cerebro.broker.setcash(100000.0)    
    print('启动资金总额: %.2f' % cerebro.broker.getvalue())
    cerebro.run()
    print('最终资金总额: %.2f' % cerebro.broker.getvalue())

    # 绘制结果
    # cerebro.plot()

策略管理

TODO(敬请期待…)
股价站上5日线,涨跌幅大于5%,市盈率大于等于0小于等于25

参考链接

  • livetrader: livetrader 是一个整合了行情和交易接口的工具链
  • QUANTAXIS: QUANTAXIS 支持任务调度 分布式部署的 股票/期货/期权 数据/回测/模拟/交易/可视化/多账户 纯本地量化解决方案
  • bt-ctpbee-store: backtrader接入国内期货实盘交易
基于Python的个人量化交易系统的设计实现代码主要包括以下几个部分:数据获取、策略制定、交易执行风险控制。 首先,数据获取是量化交易的基础。我们可以利用Python的库如Pandas、Numpy或者Quandl等获取金融市场的历史数据或者实时数据,包括股票、期货、外汇等各种金融工具的行情数据。 其次,策略制定是量化交易系统的核心。我们可以利用Python编写各种量化交易策略,如均线策略、动量策略、套利策略等。通过Python的数据分析机器学习库(如Scikit-learn、Tensorflow等),我们可以进行策略的优化回测,评估策略的盈利能力风险水平。 接着,交易执行是量化交易系统的重要组成部分。我们可以利用Python交易API接口,将编写好的交易策略交易账户连接起来,实现自动化交易Python交易API包括各种证券公司的交易接口,如雪球、米筐等。 最后,风险控制是量化交易系统的关键。我们可以利用Python编写各种风险控制模块,包括止损、风险分散等。通过Python的数据分析能力,我们可以对交易策略的风险进行监控管理,保证交易系统的稳健性安全性。 综上所述,基于Python的个人量化交易系统设计实现代码需要充分利用Python的数据分析、机器学习交易API等功能,完成数据获取、策略制定、交易执行风险控制等各个环节,从而构建一个完整的量化交易系统。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

北极象

如果觉得对您有帮助,鼓励一下

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值