很多时候为了运行复杂的策略用python速度会很慢,而核心部分用C 编写可以大幅提升策略的运行速度。另外通达信、金字塔等主流证券软件都支持C 的dll库,而且可以很方便地图形化展示策略结果,那么策略核心部分用C 编写成dll库也是一种通用的跨平台方案。
而传统的python对C 库调用方法,需要自己编写很多封装函数,且聚宽的策略回测平台本身也不支持调用本地的C 库。
这时可以借助一个开源的第三方平台hikyuu来方便地完成该需求。将jqdata与hikyuu整合起来实现C /python混合编程。
首先我们需要在hikyuu的C 工程文件中添加自己的策略代码,自己的策略代码可以作为自定义指标的一部分。
在hikyuu_msvc10工程下增加一个指标,首先在indicator/crt目录下增加一个策略的包装头文件,例如一个移动平均线的策略:
#ifndef EMA_H_
#define EMA_H_
#include "../Indicator.h"
namespace hku {
/**
* 指数移动平均线(Exponential Moving Average)
* @param n 计算均值的周期窗口,必须为大于0的整数
* @ingroup Indicator
*/
Indicator HKU_API EMA(int n = 22);
/**
* 指数移动平均线(Exponential Moving Average)
* @param data 待计算的源数据
* @param n 计算均值的周期窗口,必须为大于0的整数
* @ingroup Indicator
*/
Indicator HKU_API EMA(const Indicator& data, int n = 22);
} /* namespace */
#endif /* EMA_H_ */
然后在indicator/imp目录中增加这个策略的实现类,包含头文件和实现文件:
#ifndef EMA_H_
#define EMA_H_
#include "../Indicator.h"
namespace hku {
/*
* 指数移动平均线(Exponential Moving Average)
* 参数: n: 计算均值的周期窗口,必须为大于0的整数
* 抛弃数 = 0
*/
class Ema: public IndicatorImp {
INDICATOR_IMP(Ema)
INDICATOR_IMP_NO_PRIVATE_MEMBER_SERIALIZATION
public:
Ema();
virtual ~Ema();
};
} /* namespace hku */
#endif /* EMA_H_ */
#include "Ema.h"
namespace hku {
Ema::Ema(): IndicatorImp("EMA", 1) {
setParam("n", 22);
}
Ema::~Ema() {
}
bool Ema::check() {
int n = getParam("n");
if (n <= 0) {
HKU_ERROR("Invalid param[n] must > 0 ! [Ema::check]");
return false;
}
return true;
}
void Ema::_calculate(const Indicator& indicator) {
size_t total = indicator.size();
int n = getParam("n");
m_discard = indicator.discard();
if (total <= m_discard) {
return;
}
size_t startPos = discard();
price_t ema = indicator[startPos];
_set(ema, startPos);
price_t multiplier = 2.0 / (n 1);
for (size_t i = startPos 1; i < total; i) {
ema = (indicator[i] - ema) * multiplier ema;
_set(ema, i);
}
}
Indicator HKU_API EMA(int n) {
IndicatorImpPtr p = make_shared();
p->setParam("n", n);
return Indicator(p);
}
Indicator HKU_API EMA(const Indicator& data, int n) {
IndicatorImpPtr p = make_shared();
p->setParam("n", n);
p->calculate(data);
return Indicator(p);
}
} /* namespace hku */
最后在indicator目录下的build_in.h文件中增加包含关系:
#include "crt/EMA.h"
在indicator目录下的export.cpp文件中导出策略类:
#include "imp/Ema.h"
BOOST_CLASS_EXPORT(hku::Ema)
最后编译整个hikyuu_msvc10工程得到一个新的dll库,直接代替原hikyuu相应的dll库就实现了C 策略类的导出。
第二步就是在python中直接使用这个新的移动平均线策略指标。
首先导入jqdata和hikyuu
from jqdatasdk import *
from hikyuu.interactive.interactive import *
然后封装一个由jqdata作为数据源的自定义数据源类,具体的要实现的接口可以参考hikyuu平台的文档。这个封装只需要编写一次即可,不需要每个策略都编写。
封装类如下:
from .._hikyuu import KDataDriver, DataDriverFactory
from hikyuu import KRecord, Query, Datetime, Parameter
from jqdatasdk import *
from datetime import *
class jqdataKDataDriver(KDataDriver):
def __init__(self):
super(jqdataKDataDriver, self).__init__('jqdata')
def _init(self):
"""【重载接口】(可选)初始化子类私有变量"""
self._max = {Query.DAY:10,
Query.WEEK:2,
Query.MONTH:1,
Query.QUARTER:1,
#Query.HALFYEAR:1,
Query.YEAR:1,
Query.MIN:25,
Query.MIN5:25,
Query.MIN15:25,
Query.MIN30:25,
Query.MIN60:25}
return
def loadKData(self, market, code, ktype, start_ix, end_ix, out_buffer):
"""
【重载接口】(必须)按指定的位置[start_ix, end_ix)读取K线数据至out_buffer
:param str market: 市场标识
:param str code: 证券代码
:param KQuery.KType ktype: K线类型
:param int start_ix: 起始位置
:param int end_ix: 结束位置
:param KRecordListPtr out_buffer: 传入的数据缓存,读取数据后使用
out_buffer.append(krecord) 加入数据
"""
if start_ix >= end_ix or start_ix <0 or end_ix <0:
return
data = self._get_bars(market, code, ktype)
if len(data) < start_ix:
return
total = end_ix if end_ix < len(data) else len(data)
for i in range(start_ix, total):
record = KRecord()
record.datetime = Datetime(data.index[i])
record.openPrice = data['open'][i]
record.highPrice = data['high'][i]
record.lowPrice = data['low'][i]
record.closePrice = data['close'][i]
record.transAmount = data['money'][i]
record.transCount = data['volume'][i]
out_buffer.append(record)
def getCount(self, market, code, ktype):
"""
【重载接口】(必须)获取K线数量
:param str market: 市场标识
:param str code: 证券代码
:param KQuery.KType ktype: K线类型
"""
data = self._get_bars(market, code, ktype)
return len(data)
def _getIndexRangeByDate(self, market, code, query):
"""
【重载接口】(必须)按日期获取指定的K线数据
:param str market: 市场标识
:param str code: 证券代码
:param KQuery query: 日期查询条件(QueryByDate)
"""
print("getIndexRangeByDate")
if query.queryType != Query.DATE:
return (0, 0)
start_datetime = query.startDatetime
end_datetime = query.endDatetime
if start_datetime >= end_datetime or start_datetime > Datetime.max():
return (0, 0)
data = self._get_bars(market, code, query.kType)
total = len(data)
if total == 0:
return (0, 0)
mid, low = 0, 0
high = total-1
while low <= high:
tmp_datetime = Datetime(data.index[high])
if start_datetime > tmp_datetime:
mid = high 1
break
tmp_datetime = Datetime(data.index[low])
if tmp_datetime >= start_datetime:
mid = low
break
mid = (low high) // 2
tmp_datetime = Datetime(data.index[mid])
if start_datetime > tmp_datetime:
low = mid 1
else:
high = mid - 1
if mid >= total:
return (0, 0)
start_pos = mid
low = mid
high = total - 1
while low <= high:
tmp_datetime = Datetime(data.index[high])
if end_datetime > tmp_datetime:
mid = high 1
break
tmp_datetime = Datetime(data.index[low])
if tmp_datetime >= end_datetime:
mid = low
break
mid = (low high) // 2
tmp_datetime = Datetime(data.index[mid])
if end_datetime > tmp_datetime:
low = mid 1
else:
high = mid - 1
end_pos = total if mid >= total else mid
if start_pos >= end_pos:
return (0,0)
return (start_pos, end_pos)
def getKRecord(self, market, code, pos, ktype):
"""
【重载接口】(必须)获取指定位置的K线记录
:param str market: 市场标识
:param str code: 证券代码
:param int pos: 指定位置(大于等于0)
:param KQuery.KType ktype: K线类型
"""
record = KRecord()
if pos < 0:
return record
data = self._get_bars(market, code, ktype)
if data is None:
return record
if pos < len(data):
record.datetime = Datetime(data.index[pos])
record.openPrice = data['open'][pos]
record.highPrice = data['high'][pos]
record.lowPrice = data['low'][pos]
record.closePrice = data['close'][pos]
record.transAmount = data['money'][pos]
record.transCount = data['volume'][pos]
return record
def _trans_ktype(self, ktype): #此处的周月季年数据只是近似的,目前jqdata未提供聚宽网络平台上的get_bar函数,不能直接取,需要自行用日线数据拼装
ktype_map = {Query.MIN: '1m',
Query.MIN5: '5m',
Query.MIN15: '15m',
Query.MIN30: '30m',
Query.MIN60: '60m',
Query.DAY: '1d',
Query.WEEK: '7d',
Query.MONTH: '30d',
Query.QUARTER: '90d',
Query.YEAR: '365d'}
return ktype_map.get(ktype)
def _get_bars(self, market, code, ktype):
data = []
username = self.getParam('username')
password = self.getParam('password')
auth(username, password)
jqdataCode = normalize_code(code)
jqdata_ktype = self._trans_ktype(ktype)
if jqdata_ktype is None:
print("jqdata_ktype == None")
return data
print(jqdataCode)
security_info = get_security_info(jqdataCode)
if security_info is None: #有可能取不到任何信息
return data
#print(security_info)
data = get_price(jqdataCode, security_info.start_date, datetime.now(), jqdata_ktype)
return data
在interactive.py文件中替换原来的数据源即可
DataDriverFactory.regKDataDriver(jqdataKDataDriver())
jqdata_param = Parameter()
jqdata_param.set('type', 'jqdata')
jqdata_param.set('username', '用户名')
jqdata_param.set('password', '密码')
base_param = sm.getBaseInfoDriverParameter()
block_param = sm.getBlockDriverParameter()
kdata_param = sm.getKDataDriverParameter()
preload_param = sm.getPreloadParameter()
hku_param = sm.getHikyuuParameter()
#切换K线数据驱动,重新初始化
sm.init(base_param, block_param, jqdata_param, preload_param, hku_param)
最后一步就是在python中直接使用jqdata数据源调用C 编写的指标了
s = sm['sz000001']
k = s.getKData(Query(-200))
#抽取K线收盘价指标,一般指标计算参数只能是指标类型,所以必须先将K线数据生成指标类型
c = CLOSE(k)
#调用自定义的C 均线策略计算收盘价的EMA指标
a = EMA(c)
#绘制指标
c.plot(legend_on=True)
a.plot(new=False, legend_on=True)
#绘制柱状图
a.bar()
以上三步,中最复杂的第二步,写一次后就可以通用,这样可以大大简化,python中,调用C 策略库的难度。