聚宽马上要关停其实盘量化接口,我虽然没有用聚宽的实盘,但却是其回测和研究环境的重度用户。由于我目前的策略换仓频率不是很频繁,所以我都是先在聚宽研究环境里面跑,然后手动下单。虽然聚宽实盘关停对我影响不大,但是觉得万一哪天回测,研究环境啥的也给砍了咋办。所以还是得提前想好对策。
目前市面上比较主流的可以支持个人用户做量化的系统就是QMT和Ptrade,QMT所有数据和策略保存在本地,运行策略的时候需要电脑保持实时开机状态。Ptrade在云端运行,需要上传策略。QMT更加面向机构和专业投资者,两者的区别不再赘述,感兴趣的朋友可以自己查找相关资料。目前QMT和Ptrade的开户门槛都有所下降。 Ptrade 和QMT都支持Python,Ptrade的API更接近聚宽,所以更容易上手, 学习门槛也较低。但是由于我可能需要访问外网,和调用一些另类数据,所以我还是选择了QMT。
QMT是迅投开发的,券商在购买了QMT软件之后,会做一些客制化开发,比如国信的QMT(iQuant)就把VBA语言直接给拿掉了,确实这年头谁还用VBA(说来惭愧,身为一个计算机专业科班出身的我在接触QMT之前都没听说过这语言)。网上很少有相关的学习QMT策略开发的资料,基本上是看迅投官网的文档,这文档写的也是一言难尽。我主要把我目前使用QMT过程中遇到的几个坑总结一下
(1)QMT模拟版本和正式版本是有区别的。你在券商开户之后,客户经理一般会先给你使用个模拟客户端。客户经理,或者迅投的客服会告诉你模拟版和正式版在API使用上都是一样。由于客户经理和迅投客服并非技术出身。这也不能怪他们。我在使用过程中就发现了一些不一致的地方。比如,模拟端的get_market_data_ex函数对于停牌的股票,返回的是停牌前的价格。但是正式版就直接返回空。
(2)迅投官网的文档写的真是一言难尽,有些地方甚至是错的,比如对于ContextInfo.is_suspended_stock 的第二个参数的描述,1和0 的作用正好相反。
(3)使用QMT正式版下载财务数据,不管我选择什么样的区间。下载的数据量都是一样的。这个问题有待解决或者券商的进一步说明。
以上就是我目前遇到的几个坑,以后遇到更多问题会做进一步补充。
考虑到QMT可以学习的资料太少,文档写的又很烂,API设计也很迷(居然用23,24 来代表买进和卖出,还有1101 这种,反正就很迷)。QMT也没有聚宽这么多现成的因子可以使用,回测也非常不友好,一不小心就给带沟里去了。聚宽上有很多帖子建议可以把聚宽信号传到云端redis,然后QMT来读取下单。但是我个人不太喜欢这种方式,涉及的系统越多,可能越不稳定,万一聚宽关了呢?云端崩了呢?本着头铁的原则,我还是决定用QMT重写聚宽上的代码。
我重写了一个成长小市值策略,并且做了一下回测,基本上能够复制聚宽上的收益率,过去一年的回测结果如下:
QMT 还提供类似持仓分析功能:
学习开发最好的方式就是看代码,接下来是代码时间,全网独家。这里面其实还有很多细节需要注意和修改,所以 千万不要直接拿去实盘!!!
#coding:gbk
"""
小市值策略
"""
import pandas as pd
import numpy as np
import time
from datetime import datetime
class a():
pass
A = a() #创建空的类的实例 用来保存委托状态
def init(ContextInfo):
A.acct = 'testS'
ContextInfo.set_account(A.acct)
A.acct_type = 'STOCK'
A.buy_code = 23
A.sell_code = 24
ContextInfo.buy_stock_count = 10
# ContextInfo.run_time('trade', '30nSecond', '2013-12-25 14:30:00')
def trade(ContextInfo):
realtime = ContextInfo.get_bar_timetag(ContextInfo.barpos)
now = timetag_to_datetime(realtime,'%H%M%S')
nowDate = timetag_to_datetime(realtime,'%Y%m%d %H:%M:%S')
account = get_trade_detail_data(A.acct, A.acct_type, 'account')
if len(account)==0:
print(f'账号{A.acct} 未登录 请检查')
return
if '141000' >= now >= '135000':
holdings = get_trade_detail_data(A.acct, A.acct_type, 'position')
A.holdings = {i.m_strInstrumentID + '.' + i.m_strExchangeID : i.m_nCanUseVolume for i in holdings}
#ContextInfo.stock_pool = ContextInfo.get_stock_list_in_sector('沪深300')
ContextInfo.stock_pool = ContextInfo.get_stock_list_in_sector('沪深A股')
A.stock_list = prepare_stock_list(ContextInfo)
stocks_to_sell = [s for s in A.holdings.keys() if s not in A.stock_list]
num_stocks_to_buy = ContextInfo.buy_stock_count - len(stocks_to_sell)
stocks_to_buy = []
for s in A.stock_list:
if s not in A.holdings.keys():
stocks_to_buy.append(s)
if len(stocks_to_buy) >= num_stocks_to_buy:
break
A.stocks_to_buy = stocks_to_buy
for s in stocks_to_sell:
msg = f"小市值 {s} 卖出 {A.holdings[s]/100} 手"
print(nowDate, msg)
if not ContextInfo.is_suspended_stock(s):
#order_lots(s, -A.holdings[s]/100, ContextInfo, A.acct)
passorder(A.sell_code, 1101, A.acct, s, 14, -1, A.holdings[s], '小市值策略', 2, msg, ContextInfo)
if '144000' >= now >= '142000':
# 获取可用资金
print('可用资金', get_total_value(A.acct,'STOCK'))
if len(A.stocks_to_buy) > 0:
value = get_total_value(A.acct,'STOCK') / len(A.stocks_to_buy)
for s in A.stocks_to_buy: # 立即以最新价格下单
latest_price = ContextInfo.get_market_data_ex(['close'], \
[s],period='1m',count = 1)[s]['close'][0]
vol = value // (latest_price *100)
msg = f"小市值 {s} 买入 {vol}手"
print(nowDate, msg)
#order_lots(s, vol, ContextInfo, A.acct)
#order_lots(s, 10, ContextInfo, '55010416')
passorder(A.buy_code, 1101, A.acct, s, 14, -1, vol*100, '小市值策略', 2, msg, ContextInfo)
else:
print(nowDate, '无需换仓')
def handlebar(ContextInfo):
trade(ContextInfo)
def prepare_stock_list(ContextInfo):
'''
选股模块,根据因子选出预持有的股票
'''
endDate = ContextInfo.get_bar_timetag(ContextInfo.barpos)
startDate = endDate - 30 * 24 * 3600 * 1000
startDate = timetag_to_datetime(startDate,'%Y%m%d')
endDate = timetag_to_datetime(endDate,'%Y%m%d')
A.endDate = endDate
stock_pool = filter_st_stock(ContextInfo,ContextInfo.stock_pool)
# 过滤停牌
stock_pool = filter_paused_stock(ContextInfo,stock_pool)
# 过滤科创板
stock_pool = filter_kcb_stock(ContextInfo, stock_pool)
stock_pool = filter_new_stock(ContextInfo, stock_pool)
#净利润增长率前10%
stock_pool = get_factor_filter_list(ContextInfo, stock_pool, 'PERSHAREINDEX',
'inc_net_profit_rate', asc=False, p=0.1)
#PEG 前50%
stock_pool = get_peg_filter_list(ContextInfo, stock_pool, p=0.5)
# 获取小市值股票
scores = {}
for s in stock_pool:
price_data = ContextInfo.get_instrumentdetail(s)
if price_data['TotalVolumn'] > 0:
scores[s] = price_data['PreClose']*price_data['TotalVolumn'] # data['TotalVolumn'] 有时候返回0
else:
fieldList = ['CAPITALSTRUCTURE.total_capital']
share_data = ContextInfo.get_financial_data(fieldList, [s], startDate, \
endDate, report_type = 'report_time')
scores[s] = price_data['PreClose'] * share_data['total_capital'][0]
df = pd.DataFrame.from_dict(scores, orient='index')
df.columns = ['cap']
df = df.sort_values(by=['cap'])
stock_pool = df.index.values.tolist()[:30]
# 过滤涨跌停, !! 这个过滤要放在最后,股票太多,调用get_market_data_ex 频率受限
stock_pool = filter_limitup_stock(ContextInfo,stock_pool)
stock_pool = filter_limitdown_stock(ContextInfo,stock_pool)
return stock_pool[:ContextInfo.buy_stock_count]
def get_total_value(account_id,datatype):#(账号,账户类型)
'''
获取账户当前可用现金
'''
result = 0
result_list = get_trade_detail_data(account_id,datatype,'ACCOUNT')
for obj in result_list:
result = obj.m_dAvailable
return result
def get_peg_filter_list(ContextInfo, stock_list, p= 0.5):
realtime = ContextInfo.get_bar_timetag(ContextInfo.barpos)
endDate = realtime - 24 * 3600 * 1000
startDate = realtime - 14 * 24 * 3600 * 1000
endDate = timetag_to_datetime(endDate,'%Y%m%d')
startDate = timetag_to_datetime(startDate,'%Y%m%d')
score_list = []
filter_stocks = []
for s in stock_list:
data = ContextInfo.get_financial_data(['PERSHAREINDEX.s_fa_eps_basic', 'PERSHAREINDEX.adjusted_net_profit_rate'],
[s], startDate, endDate, report_type = 'report_time')
price_data = ContextInfo.get_local_data(s, startDate, endDate, '1d', count=1)
if len(price_data.values()) == 0:
continue
price = list(price_data.values())[0]['close']
if data['s_fa_eps_basic'][0] > 0:
score_list.append(price * data['adjusted_net_profit_rate'][0] / data['s_fa_eps_basic'][0])
filter_stocks.append(s)
df = pd.DataFrame(columns=['code','score'])
df['code'] = filter_stocks
df['score'] = score_list
df = df.dropna()
df = df[df['score']>0]
df.sort_values(by='score', ascending=True, inplace=True)
filter_list = list(df.code)[0:int(p*len(stock_list))]
return filter_list
def get_factor_filter_list(ContextInfo, stock_list, table, field, asc=True,p=0.3):
realtime = ContextInfo.get_bar_timetag(ContextInfo.barpos)
endDate = realtime - 24 * 3600 * 1000
startDate = realtime - 14 * 24 * 3600 * 1000
endDate = timetag_to_datetime(endDate,'%Y%m%d')
startDate = timetag_to_datetime(startDate,'%Y%m%d')
score_list = []
for s in stock_list:
data = ContextInfo.get_financial_data([table + '.' + field], [s], startDate,
endDate, report_type = 'report_time')
score_list.append(data[field][0])
df = pd.DataFrame(columns=['code','score'])
df['code'] = stock_list
df['score'] = score_list
df = df.dropna()
df = df[df['score']>0]
df.sort_values(by='score', ascending=asc, inplace=True)
filter_list = list(df.code)[0:int(p*len(stock_list))]
return filter_list
#2-1 过滤停牌股票
def filter_paused_stock(ContextInfo,stock_list):
return [stock for stock in stock_list if not ContextInfo.is_suspended_stock(stock, 1)]
def filter_new_stock(ContextInfo, stock_list):
realtime = ContextInfo.get_bar_timetag(ContextInfo.barpos)
startDate = realtime - 250 * 24 * 3600 * 1000
startDate = int(timetag_to_datetime(startDate,'%Y%m%d'))
return [stock for stock in stock_list if ContextInfo.get_instrumentdetail(stock)['OpenDate'] < startDate]
#2-2 过滤ST及其他具有退市标签的股票
def filter_st_stock(ContextInfo,stock_list):
return [stock for stock in stock_list
if 'ST' not in ContextInfo.get_stock_name(stock)
and '*' not in ContextInfo.get_stock_name(stock)
and '退' not in ContextInfo.get_stock_name(stock)
and ContextInfo.get_instrumentdetail(stock)['InstrumentID'] is not None]
# 过滤掉科创板
def filter_kcb_stock(ContextInfo, stock_list):
return [stock for stock in stock_list if not stock.startswith('688')]
# 过滤涨停的股票
def filter_limitup_stock(ContextInfo, stock_list):
# 已存在于持仓的股票即使涨停也不过滤,避免此股票再次可买,但因被过滤而导致选择别的股票
filter_list = []
for stock in stock_list:
if stock in A.holdings.keys():
filter_list.append(stock)
continue
price_data = ContextInfo.get_market_data_ex(['close'],[stock],period='30m', end_time=A.endDate, count = 1)[stock]['close']
if len(price_data) > 0 and price_data[0] < ContextInfo.get_instrumentdetail(stock)['UpStopPrice']:
filter_list.append(stock)
return filter_list
# 过滤跌停股票
def filter_limitdown_stock(ContextInfo,stock_list):
filter_list = []
for stock in stock_list:
price_data = ContextInfo.get_market_data_ex(['close'],[stock],period='30m',end_time=A.endDate, count = 1)[stock]['close']
if len(price_data) > 0 and price_data[0] > ContextInfo.get_instrumentdetail(stock)['DownStopPrice']:
filter_list.append(stock)
return filter_list