很多时候为了运行复杂的策略用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 策略库的难度。