7年13倍的量化策略实盘体验和感悟

7年13倍的量化策略

2022年9月,我开发了一个7年13倍的量化策略,策略的基本交易想法是:做多近期最具“潜力”的小盘股。其中,具有潜力指的是:成长性好+近期变盘可能性大+其他一些结合了个人交易经验的因素(基本面+技术面)。使用的因子包括:

  • 市值因子
  • 某些我认为重要的财务因子
  • 经营现金流相关因子(它比归母净利润真实一点)
  • 使用某些大盘择时因子进行简单的风控
  • 其他一些我认为重要的短期量价因子

此外,对因子进行了行业中性化和移动平均平滑处理,避免换仓过于频繁。

回测参数:时间段2015-01-01~2022-09-20,滑点千分之一,开平仓手续费各万3,印花税千3,股池为沪深主板。

Alt text

Alt text

回测累计收益

除了较高的收益外,上图还可发现该策略的一些优点和缺点,

  1. 在加入了大盘择时因子做初步的风控后,策略能有效地逃离暴跌的股灾(比如2015下半年,2022年4月等)。
  2. 2022年整体熊市的情况下,该策略反而保持了很高的收益,策略中可能构造出了目前尚未失效的因子。(2023年初更新:该策略2022年收益最终翻倍)
  3. 峰度达到4.75,是比较高的,
  4. 该策略有明显的缺点:基于市值因子,偏向交易小盘股,因此在市场爆拉白马股的2017年,该策略有明显回撤。且策略的容量十分有限。
  5. 该策略有明显的周期性,符合因子收益本身具有的周期性特征。
  6. 该策略的最大回撤仍然较大(主要由于2015-2016的股灾,最大回撤46%),实际交易应该使用更好的手段控制回撤,比如使用期权对冲获取Alpha等。

滚动夏普呈现周期性,在策略有效时,夏普较高:

滚动夏普

从业绩归因来看,超额收益来源于交互效应,该策略的收益来源于在超配上涨行业的同时超配行业中表现较好的标的:

业绩归因

尽管该策略仍有很多需要改进的地方,但从资金曲线上看,近期它的收益很高,在市场低迷的2022年更显得难得(说实话,现在感觉2022年只要是不亏的策略都显得眉清目秀的…),于是决定用小资金实盘一段时间,看看量化回测和实盘有什么区别。

接入实盘

从2022年9月20日起,我使用迅投QMT对该策略进行实盘,具available_num体地,我使用聚宽的数据源每天计算得到调仓信号,将最新仓位写入本地csv,用QMT刷本地csv文件进行调仓,策略是低频的日度策略,起始资金是10000元(小资金试错),因为策略使用的小市值因子和大盘择时信号部分地自带仓位管理和止盈止损,因此没有额外再进行策略择时。

注意,这种方案只适用于低频量化交易,对于高频量化交易,应该使用C/C++编写程序,至少应该用C++写csv文件,再让迅投刷文件,否则等信号算出来到下单,黄花菜都凉了。

与此同时,我用该策略运行了一个聚宽的模拟交易,作为理论收益和我的实盘进行对比。

QMT代码方面,刷csv执行调仓的封装函数为:

def execute_plan():
    plan = pd.read_csv(trade_plan_path)
    plan = plan[plan['change_num'] != 0]
    hold_list = check_all_hold()
    plan = pd.merge(plan, hold_list[['stock_code','position_num','available_num']], on = ['stock_code'], how = 'left')
    plan = plan[['stock_code','stock_name','position_num','available_num','change_num']]
    plan['position_num'].fillna(value = 0, inplace = True)
    plan['available_num'].fillna(value = 0, inplace = True)
    plan['progress'] = 'Unfinished'
    plan['order_times'] = 0
    plan['change_success_num'] = 0
    plan['fail_num'] = 0
    
    order_list = check_all_order_state()
    if order_list is not None:
        for i in range(len(order_list)):
            if order_list['order_type'].iloc[i] == 'BUY' and order_list['status'].iloc[i] == '已报':
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'fail_num'] += order_list['order_num'].iloc[i]
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'order_times'] += 1
            if order_list['order_type'].iloc[i] == 'BUY' and order_list['status'].iloc[i] == '部撤':
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'change_success_num'] += order_list['success_num'].iloc[i]
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'order_times'] += 1
            if order_list['order_type'].iloc[i] == 'BUY' and order_list['status'].iloc[i] == '部成':
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'change_success_num'] += order_list['success_num'].iloc[i]
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'fail_num'] += order_list['order_num'].iloc[i] - order_list['success_num'].iloc[i]
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'order_times'] += 1
            if order_list['order_type'].iloc[i] == 'BUY' and order_list['status'].iloc[i] == '已成':
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'change_success_num'] += order_list['success_num'].iloc[i]
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'order_times'] += 1
            if order_list['order_type'].iloc[i] == 'BUY' and order_list['status'].iloc[i] == '已撤':
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'order_times'] += 1
            
            if order_list['order_type'].iloc[i] == 'SELL' and order_list['status'].iloc[i] == '已报':
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'fail_num'] -= order_list['order_num'].iloc[i]
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'order_times'] += 1
            if order_list['order_type'].iloc[i] == 'SELL' and order_list['status'].iloc[i] == '部撤':
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'change_success_num'] -= order_list['success_num'].iloc[i]
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'order_times'] += 1
            if order_list['order_type'].iloc[i] == 'SELL' and order_list['status'].iloc[i] == '部成':
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'change_success_num'] -= order_list['success_num'].iloc[i]
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'fail_num'] -= order_list['order_num'].iloc[i] - order_list['success_num'].iloc[i]
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'order_times'] += 1
            if order_list['order_type'].iloc[i] == 'SELL' and order_list['status'].iloc[i] == '已成':
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'change_success_num'] -= order_list['success_num'].iloc[i]
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'order_times'] += 1
            if order_list['order_type'].iloc[i] == 'SELL' and order_list['status'].iloc[i] == '已撤':
                plan.loc[plan['stock_code'] == order_list['stock_code'].iloc[i], 'order_times'] += 1
    plan.loc[-plan['change_num'] > plan['available_num'] + abs(plan['fail_num']), 'progress'] = '持仓不足'
    plan.loc[plan['change_num'] == plan['change_success_num'], 'progress'] = 'Done'

    exe_plan = plan.copy()
    exe_plan = exe_plan[exe_plan['progress'] == 'pending']
    if not exe_plan.empty:
        for i in range(len(exe_plan)):
            if abs(exe_plan['change_num'].iloc[i]) - exe_plan['success_num'].iloc[i] - exe_plan['fail_num'].iloc[i] > 0:
                exe_num = exe_plan['change_num'].iloc[i] - exe_plan['success_num'].iloc[i] - exe_plan['fail_num'].iloc[i]
                if exe_num > 0:
                    exe_price = xtdata.get_full_tick([exe_plan['stock_code'].iloc[i]])[exe_plan['stock_code'].iloc[i]]['lastPrice'] + exe_plan['order_times'].iloc[i] * 0.01
                    order_result = xt_trader.order_stock(account, exe_plan['stock_code'].iloc[i], xtconstant.STOCK_BUY, int(exe_num), 11, exe_price, '2号策略', '自动买入')
                    if order_result >= 0:
                        print(f"({i}) 下单成功:{exe_plan['stock_code'].iloc[i]} {exe_plan['stock_name'].iloc[i]} 以 {exe_price} 买入 {format(exe_num,'.0f')} 股")
                    if order_result == -1:
                        print(f"({i}) 下单失败:{exe_plan['stock_code'].iloc[i]} {exe_plan['stock_name'].iloc[i]} 以 {exe_price} 买入 {format(exe_num,'.0f')} 股")
                if exe_num < 0:
                    exe_price = xtdata.get_full_tick([exe_plan['stock_code'].iloc[i]])[exe_plan['stock_code'].iloc[i]]['lastPrice'] - exe_plan['order_times'].iloc[i] * 0.01
                    order_result = xt_trader.order_stock(account, exe_plan['stock_code'].iloc[i], xtconstant.STOCK_SELL, int(-exe_num), 11, exe_price, '2号策略', '自动卖出')
                    if order_result >= 0:
                        print(f"({i}) 下单成功:{exe_plan['stock_code'].iloc[i]} {exe_plan['stock_name'].iloc[i]} 以 {format(exe_price,'.2f')} 卖出 {format(-exe_num,'.0f')} 股")
                    if order_result == -1:
                        print(f"({i}) 下单失败:{exe_plan['stock_code'].iloc[i]} {exe_plan['stock_name'].iloc[i]} 以 {format(exe_price,'.2f')} 卖出 {format(-exe_num,'.0f')} 股")
        return 0
    else:
        return 1

从2022年9月20日到2023年6月11日,聚宽的模拟交易模块显示,我如果按该策略进行实盘交易,我的累计收益达到37.66%,估计年化收益达到57.87%,最大回撤仅10.62%,期末资产应达到13766.08元,如下图所示:

模拟交易

从我的QMT实盘来看,实盘操作该策略收益达到35.13%,期末资产达13513元。

从结果上说,还是不错的,从2022年10月10日开始盈利,今年收益不错,有把握到2023年初的行情!

可以看到实际的实盘收益和理论收益还是有差距的,虽然赚钱了,但不多,也不敢多,因为感觉还有非常多的不足。量化实盘中,有一些小小的感悟,简单介绍下:

  1. 需要增加对成交失败的处理:策略设定上,使用开盘价买入成交,但实际上开盘的价格可能抢不到,为了保证成交优先级,我会挂价格较高的单,但是这样有时候会正好买到高点去,而不是开盘价。如果使用集合竞价挂单,又会直接影响开盘价,似乎不太合理。特别是小盘股,这种实盘偏差问题尤为严重。是否不要按开盘价,而是开盘后等它稍微跌一点再买入?交易时机是一个需要考虑很多因素的问题,并没有那么简单啊。
  2. 过去收益并不代表未来收益:过去回测7年13倍的策略,直到2022年9月19日还是赚钱的,搞笑的是,我从2022年9月19日开始跑实盘后,策略马上开始亏损,连续亏损到10月10日,20天亏了10%(见上图),对道心造成了很大的影响,几乎想关停策略,如果是私募多头产品,使用的是大资金,这种一买入就亏10%的情况很可能是客户难以承受的。也许应该改进策略,让资金曲线尽可能平滑一点。
  3. 量化的盈利实际上来自统计概率:很多实盘股票我主观看其实不能把握,甚至怀疑,最终选出来的股票也不一定上涨,策略有时也自动帮我亏了很多钱,但是长期来看(至少目前)确实带来了可观的收益。
  4. 包含小市值因子的策略容量非常有限:如前所述,在实盘中,小盘股的盘口很可能并没有足够的对手方供大资金买卖,而且很容易发生策略拥挤,导致策略失效,这个策略表现呈现周期性,小市值因子的拥堵度周期性功不可没。
  5. 实际成交时间差异:QMT无法绑定服务器网卡(至少我的不能),而且必须保持开机才能运行策略,我作为一介学生,每天上课、开会、做项目,有的时候实在没法运行,只能人工事后补仓,这也造成和理论收益的不一致。个人投资者真正跑量化的话,还是需要单独电脑的。
  6. 可以结合主观主线判断增加 Alpha:今年的市场主线发生变化,从年初开始就可以预期,TMT科技股将成为今年绝对的投资主线,此外,疫情放开+中特估的中字头股票,以及最近有所起色的半导体(第二阶段基金增仓的细分行业),该策略并没有很好地捕捉到这些行情,否则可以赚的更多。优化方向:精简股池到市场主线中,增加跟上市场结构性行情的概率。
  7. 可以结合主观风格判断增加 Alpha:今年的主观交易情况上看,对市场资金流向的分析在今年更加重要,我的策略没有加入资金流相关因子,也没有捕捉到这部分行情。

综合来看,这个策略使用因子投资逻辑,回测年化收益约40%,最大收益得到有效控制,作为初次尝试还是不错的,而且其中可能构建了市场中目前仍然有效的复合因子。然而,该策略并不适合大资金运作,从回测评价的统计指标(下图)来看,也有很多不足之处,应该考虑替换掉小市值因子,替换股池为中证500,更多地考虑策略容量和风险对冲的问题。

各回测指标表现(聚宽):

策略回测表现

Hengzhao Hong
Hengzhao Hong
Ph.D Student in Statistics, Front-End Engineer

创造的过程,大多是“赝品”最终战胜“正品”的过程——从模仿开始,直至超越。

Related