文章目录
进展:截止到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接入国内期货实盘交易