实盘量化QMT踩坑记录(一)


聚宽马上要关停其实盘量化接口,我虽然没有用聚宽的实盘,但却是其回测和研究环境的重度用户。由于我目前的策略换仓频率不是很频繁,所以我都是先在聚宽研究环境里面跑,然后手动下单。虽然聚宽实盘关停对我影响不大,但是觉得万一哪天回测,研究环境啥的也给砍了咋办。所以还是得提前想好对策。

目前市面上比较主流的可以支持个人用户做量化的系统就是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