量化投资:第9节 港股市场的回测

0
回复
4820
查看
[复制链接]

24

主题

15

回帖

227

积分

中级会员

积分
227
来源: 2019-11-20 19:31:43 显示全部楼层 |阅读模式
上一节讲解的是A股市场的回测,本节讲解港股市场的回测示例。
买入因子,卖出因子等依然使用相同的设置,如下所示:
  1. <span style="background-color: white;"># 设置初始资金数
  2. read_cash = 1000000

  3. # 买入因子依然延用向上突破因子
  4. buy_factors = [{'xd': 60, 'class': AbuFactorBuyBreak},
  5.                {'xd': 42, 'class': AbuFactorBuyBreak}]

  6. # 卖出因子继续使用上一节使用的因子
  7. sell_factors = [
  8.     {'stop_loss_n': 1.0, 'stop_win_n': 3.0,
  9.      'class': AbuFactorAtrNStop},
  10.     {'class': AbuFactorPreAtrNStop, 'pre_atr_n': 1.5},
  11.     {'class': AbuFactorCloseAtrNStop, 'close_atr_n': 1.5}
  12. ]</span>
复制代码

1. 港股市场的回测示例
择时股票池使用沙盒缓存数据中的如下股票:
港股市场:
  • 中国恒大(hk03333)
  • 腾讯控股(hk00700)
  • 长城汽车(hk02333)
  • 中国信达(hk01359)
  • 复星国际(hk00656)
  • 金山软件(hk03888)
  • 中国平安(hk02318)
  • 恒生指数(hkHSI)

代码如下所示:
  1. <span style="background-color: white;"># 择时股票池
  2. choice_symbols = ['hk03333', 'hk00700', 'hk02333', 'hk01359', 'hk00656', 'hk03888', 'hk02318']</span>
复制代码
与A股市场类似首先将abupy量化环境设置为港股,代码如下所示:
  1. <span style="background-color: white;"># 设置市场类型为港股
  2. abupy.env.g_market_target = EMarketTargetType.E_MARKET_TARGET_HK</span>
复制代码

  1. <span style="background-color: white;">abu_result_tuple, kl_pd_manger = abu.run_loop_back(read_cash,
  2.                                                    buy_factors,
  3.                                                    sell_factors,
  4.                                                    n_folds=6,
  5.                                                    choice_symbols=choice_symbols)
  6. AbuMetricsBase.show_general(*abu_result_tuple, only_show_returns=True)</span>
复制代码

  1. <span style="background-color: white;">买入后卖出的交易数量:202
  2. 买入后尚未卖出的交易数量:3
  3. 胜率:50.4950%
  4. 平均获利期望:12.9899%
  5. 平均亏损期望:-5.9130%
  6. 盈亏比:2.4770
  7. 策略收益: 85.8217%
  8. 基准收益: 18.5928%
  9. 策略年化收益: 14.6923%
  10. 基准年化收益: 3.1830%
  11. 策略买入成交比例:90.7317%
  12. 策略资金利用率比例:32.5890%
  13. 策略共执行1472个交易日</span>
复制代码
112030.png
从上收益曲线可以看到在大概在2015年上旬的有一次收益猛烈的拉升出现,下面输出top10笔收益,代码如下所示:
  1. <span style="background-color: white;">orders_pd = abu_result_tuple.orders_pd
  2. top_10 = orders_pd.sort_values(by='profit')[::-1].dropna(subset=['sell_price'])[:10]
  3. top_10.filter(['symbol', 'buy_date', 'buy_price', 'sell_date', 'sell_price', 'profit'])</span>
复制代码
112031.png
从输出可以清晰的看出,top3的高收益交易都发生在2015年3月份买入,4月份卖出,3笔收益都非常高。
结合HSI基准在3-4月之间也有一个拉升,这里的收益曲线猛烈拉升也可以理解了。
很多时候编写出一个策略后发现预期收益非常高,遇到这样的情况,如果你是认真的,那就应该考虑怎么降低收益,降低的不仅是收益也是风险,对应的提高的将是系统的稳定性。
怎样才能降低风险,提高稳定性,比如上面的回测,是不是要把3-4月份这次收益猛烈的拉升滤除才叫降低风险呢?
当然不是,上面的3-4月份拉升收益是顺势的收益,如果要把这段过滤将是逆势行为。
降低风险是不是需要通过买对冲来实现?
其实也不建议个人投资者买对冲,因为对冲的产品一般都是和时间相关的,比如期权,期货,增加一个时间维度会将整个交易复杂度提升几个数量级,并不建议一般交易者进行这种对冲,特别是以股票为主要投资对象的一般交易者(但是可以在策略中做相同品种的相关性对冲,在之后的章节会有完成相关性策略示例)。
我认为最适合一般投资者的的提高稳定性的方法就是降低交易频率,比如ump裁判拦截,主要就是通过学习策略中的失败交易,从不同角度进行学习交易特点,指导决策拦截交易,拦截了大量的交易可以节省佣金,更重大的意义在于边裁避免了重大的风险。
本节讲解在不考虑使用ump系统的情况下,示例如何通过硬编码策略来降低交易频率,提高系统的稳定性。
2. 优化策略,提高系统的稳定性
下面的示例将在突破策略AbuFactorBuyBreak基础上进行降低交易频率,提高系统的稳定性处理。
首先看下面使用AbuTLine.show_least_valid_poly示例,通过get_price获取港股大盘HSI一个月收盘价格数据,然后使用AbuTLine.show_least_valid_poly进行poly判断,它返回的结果是检测至少poly次拟合曲线可以代表原始曲线y的走势,如下所示显示1次
曲线即可以代表20160301-20160401这一个月的拟合走势,如下所示:
  1. <span style="background-color: white;">hsi = get_price('hkHSI', start_date='20160301', end_date='20160401')
  2. tl.AbuTLine(hsi.price, 'HSI').show_least_valid_poly()</span>
复制代码
112032.png
  1. <span style="background-color: white;">1</span>
复制代码
验证poly(默认=1)次多项式拟合回归的趋势曲线是否能代表原始曲线y的走势主要代码,请阅读源代码AbuTLine
如上20160301-20160401使用一次拟合曲线即可以代表原始曲线,下面向前推一个月20160128-20160229:

  1. <span style="background-color: white;">hsi = get_price('hkHSI', start_date='20160128', end_date='20160229')
  2. tl.AbuTLine(hsi.price, 'HSI').show_least_valid_poly()</span>
复制代码
112033.png
  1. <span style="background-color: white;">4</span>
复制代码

如上结果显示至少需要最少4次以上的拟合才可以代表原始曲线,实际上poly的次数越大,越代表此段周期内波动比较大,从上可视化图也可清晰看出,上面需要4次拟合才能代表的走势明显处在震荡走势中。
下面根据上面这个判断依据编写策略基本,策略和之前一直使用的AbuFactorBuyBreak基本一样,不同的是实现fit_month方法(fit_month即在回测策略中每一个月执行一次的方法), 在fit_month中使用show_least_valid_poly计算这个月大盘价格走势的least poly值,如果大于一个阀值比如2,就说明最近大盘走势震荡,那么就封锁交易,直到在fit_month中计算出的least poly值小于阀值,解锁交易,代码实现如下所示:

  1. <span style="background-color: white;">class AbuSDBreak(AbuFactorBuyXD, BuyCallMixin):
  2.     """示例买入因子: 在AbuFactorBuyBreak基础上进行降低交易频率,提高系统的稳定性处理"""
  3.     def _init_self(self, **kwargs):
  4.         """
  5.         :param kwargs: kwargs可选参数poly值,poly在fit_month中和每一个月大盘计算的poly比较,
  6.         若是大盘的poly大于poly认为走势震荡,poly默认为2
  7.         """
  8.         super(AbuSDBreak, self)._init_self(**kwargs)
  9.         # poly阀值,self.poly在fit_month中和每一个月大盘计算的poly比较,若是大盘的poly大于poly认为走势震荡
  10.         self.poly = kwargs.pop('poly', 2)
  11.         # 是否封锁买入策略进行择时交易
  12.         self.lock = False

  13.     def fit_month(self, today):
  14.         # fit_month即在回测策略中每一个月执行一次的方法
  15.         # 策略中拥有self.benchmark,即交易基准对象,AbuBenchmark实例对象,benchmark.kl_pd即对应的市场大盘走势
  16.         benchmark_df = self.benchmark.kl_pd
  17.         # 拿出大盘的今天
  18.         benchmark_today = benchmark_df[benchmark_df.date == today.date]
  19.         if benchmark_today.empty:
  20.             return 0
  21.         # 要拿大盘最近一个月的走势,准备切片的start,end
  22.         end_key = int(benchmark_today.iloc[0].key)
  23.         start_key = end_key - 20
  24.         if start_key < 0:
  25.             return False

  26.         # 使用切片切出从今天开始向前20天的数据
  27.         benchmark_month = benchmark_df[start_key:end_key + 1]
  28.         # 通过大盘最近一个月的收盘价格做为参数构造AbuTLine对象
  29.         benchmark_month_line = AbuTLine(benchmark_month.close, 'benchmark month line')
  30.         # 计算这个月最少需要几次拟合才能代表走势曲线
  31.         least = benchmark_month_line.show_least_valid_poly(show=False)

  32.         if least >= self.poly:
  33.             # 如果最少的拟合次数大于阀值self.poly,说明走势成立,大盘非震荡走势,解锁交易
  34.             self.lock = False
  35.         else:
  36.             # 如果最少的拟合次数小于阀值self.poly,说明大盘处于震荡走势,封锁策略进行交易
  37.             self.lock = True

  38.     def fit_day(self, today):
  39.         if self.lock:
  40.             # 如果封锁策略进行交易的情况下,策略不进行择时
  41.             return None

  42.         # 今天的收盘价格达到xd天内最高价格则符合买入条件
  43.         if today.close == self.xd_kl.close.max():
  44.             return self.buy_tomorrow()</span>
复制代码
下面重新组成买入因子字典,使用刚刚编写的买入策略AbuSDBreak,突破周期还是60,42:
  1. <span style="background-color: white;">buy_factors = [{'xd': 60, 'class': AbuSDBreak},
  2.                {'xd': 42, 'class': AbuSDBreak}]</span>
复制代码

  1. <span style="background-color: white;"># 使用run_loop_back运行策略
  2. abu_result_tuple, kl_pd_manger = abu.run_loop_back(read_cash,
  3.                                                    buy_factors,
  4.                                                    sell_factors,
  5.                                                    choice_symbols=choice_symbols,
  6.                                                    n_folds=6)
  7. AbuMetricsBase.show_general(*abu_result_tuple, only_show_returns=True)</span>
复制代码

  1. <span style="background-color: white;">买入后卖出的交易数量:122
  2. 买入后尚未卖出的交易数量:3
  3. 胜率:61.4754%
  4. 平均获利期望:11.5062%
  5. 平均亏损期望:-4.7367%
  6. 盈亏比:3.9707
  7. 策略收益: 91.2822%
  8. 基准收益: 18.5928%
  9. 策略年化收益: 15.6271%
  10. 基准年化收益: 3.1830%
  11. 策略买入成交比例:98.4000%
  12. 策略资金利用率比例:22.8984%
  13. 策略共执行1472个交易日</span>
复制代码
112034.png
直观上的收益曲线变的平缓了些,交易数量减少了接近40%。
使用AbuSDBreak,降低交易频率,提高系统的稳定性处理的回测结果:
  • 买入后卖出的交易数量:123
  • 胜率:60.9756%
  • 平均获利期望:12.3589%
  • 平均亏损期望:-4.6619%
  • 盈亏比:4.2144
  • 策略收益: 100.1270%
  • 基准收益: 18.5928%
  • 策略年化收益: 17.1413%
  • 基准年化收益: 3.1830%
  • 策略买入成交比例:98.4127%
  • 策略资金利用率比例:22.3742%
  • 策略共执行1472个交易日

本节时使用AbuFactorBuyBreak的回测结果:
  • 买入后卖出的交易数量:201
  • 胜率:50.7463%
  • 平均获利期望:13.2619%
  • 平均亏损期望:-5.9121%
  • 盈亏比:2.6158
  • 策略收益: 93.7512%
  • 基准收益: 18.5928%
  • 策略年化收益: 16.0498%
  • 基准年化收益: 3.1830%
  • 策略买入成交比例:91.1765%
  • 策略资金利用率比例:32.3410%
  • 策略共执行1472个交易日

上面这种提高系统稳定性的方法需要在策略添加一些交易规则,阀值进行交易的筛选过滤,后面的章节会示例使用abupy中ump裁判拦截系统,其优点是:
  • 不需要在具体策略中硬编码
  • 不需要人工设定阀值,即且使得代码逻辑清晰
  • 分离基础策略和策略优化监督模块,提高灵活度和适配性
  • 发现策略中隐藏的交易策略问题
  • 可以通过不断的学习新的交易数据(给每一个裁判看更多的比赛录像,提高比赛录像水准),提高拦截水平

读者可自行使用上面的AbuSDBreak做为上一节A股回测的策略,重新进行回测,从结果你可以发现与本节港股的回测类似,交易频率降低,交易成功概率提高。
3 将优化策略的'策略'做为类装饰器进行封装
在上面说过使用ump模块的优点有个是:分离基础策略和策略优化监督模块,提高灵活度和适配性,
实际上上面使用的AbuSDBreak中的优化策略可以封装在一个装饰器类中,之后编写的所有策略都可以选择是否通过这个类装饰器进行策略优化,代码如下所示:
  1. <span style="background-color: white;">class AbuLeastPolyWrap(object):
  2.     """
  3.         做为买入因子策略装饰器封装show_least_valid_poly对大盘震荡大的情况下封锁交易
  4.     """

  5.     def __call__(self, cls):
  6.         """只做为买入因子策略类的装饰器"""
  7.         
  8.         if isinstance(cls, six.class_types):
  9.             # 只做为类装饰器使用
  10.             
  11.             init_self = cls._init_self
  12.             org_fit_day = cls.fit_day
  13.             
  14.             # fit_month不是必须实现的
  15.             org_fit_month = getattr(cls, 'fit_month', None)
  16.             def init_self_wrapped(*args, **kwargs):
  17.                 # 拿出被装饰的self对象
  18.                 warp_self = args[0]
  19.                 # 外部可以设置poly阀值,self.poly在fit_month中和每一个月大盘计算的poly比较,
  20.                 # 若是大盘的poly大于poly认为走势震荡
  21.                 warp_self.poly = kwargs.pop('poly', 2)
  22.                 # 是否封锁买入策略进行择时交易
  23.                 warp_self.lock = False
  24.                 # 调用原始的_init_self
  25.                 init_self(*args, **kwargs)
  26.                     
  27.             def fit_day_wrapped(*args, **kwargs):
  28.                 # 拿出被装饰的self对象
  29.                 warp_self = args[0]
  30.                 if warp_self.lock:
  31.                     # 如果封锁策略进行交易的情况下,策略不进行择时
  32.                     return None
  33.                 return org_fit_day(*args, **kwargs)
  34.                                 
  35.             def fit_month_wrapped(*args, **kwargs):
  36.                 warp_self = args[0]
  37.                 today = args[1]
  38.                 # fit_month即在回测策略中每一个月执行一次的方法
  39.                 # 策略中拥有self.benchmark,即交易基准对象,AbuBenchmark实例对象,benchmark.kl_pd即对应的市场大盘走势
  40.                 benchmark_df = warp_self.benchmark.kl_pd
  41.                 # 拿出大盘的今天
  42.                 benchmark_today = benchmark_df[benchmark_df.date == today.date]
  43.                 if benchmark_today.empty:
  44.                     return 0
  45.                 # 要拿大盘最近一个月的走势,准备切片的start,end
  46.                 end_key = int(benchmark_today.ix[0].key)
  47.                 start_key = end_key - 20
  48.                 if start_key < 0:
  49.                     return 0

  50.                 # 使用切片切出从今天开始向前20天的数据
  51.                 benchmark_month = benchmark_df[start_key:end_key+1]
  52.                 # 通过大盘最近一个月的收盘价格做为参数构造AbuTLine对象
  53.                 benchmark_month_line = tl.AbuTLine(benchmark_month.close, 'benchmark month line')
  54.                 # 计算这个月最少需要几次拟合才能代表走势曲线
  55.                 least = benchmark_month_line.show_least_valid_poly(show=False)

  56.                 if least >= warp_self.poly:
  57.                     # 如果最少的拟合次数大于阀值self.poly,说明走势成立,大盘非震荡走势,解锁交易
  58.                     warp_self.lock=False
  59.                 else:
  60.                     # 如果最少的拟合次数小于阀值self.poly,说明大盘处于震荡走势,封锁策略进行交易
  61.                     warp_self.lock=True
  62.                     
  63.                 if org_fit_month is not None:
  64.                     return org_fit_month(*args, **kwargs)

  65.             cls._init_self = init_self_wrapped
  66.             init_self_wrapped.__name__ = '_init_self'
  67.                                 
  68.             cls.fit_day = fit_day_wrapped
  69.             fit_day_wrapped.__name__ = 'fit_day'
  70.             
  71.             cls.fit_month = fit_month_wrapped
  72.             fit_month_wrapped.__name__ = 'fit_month'

  73.             return cls
  74.         else:
  75.             raise TypeError('AbuLeastPolyWrap just for class warp')</span>
复制代码
下面编写一个很简单的策略,连续涨两天就买入股票,示例AbuLeastPolyWrap的使用:
  1. <span style="background-color: white;">@AbuLeastPolyWrap()
  2. class AbuTwoDayBuy(AbuFactorBuyTD, BuyCallMixin):
  3.     """示例AbuLeastPolyWrap,混入BuyCallMixin,即向上突破触发买入event"""
  4.     def _init_self(self, **kwargs):
  5.         """简单示例什么都不编写了"""
  6.         pass

  7.     def fit_day(self, today):
  8.         """
  9.         针对每一个交易日拟合买入交易策略,今天涨,昨天涨就买
  10.         :param today: 当前驱动的交易日金融时间序列数据
  11.         :return:
  12.         """
  13.         # 今天的涨幅
  14.         td_change = today.p_change
  15.         # 昨天的涨幅
  16.         yd_change = self.yesterday.p_change

  17.         if td_change > 0 and 0 < yd_change < td_change:
  18.             # 连续涨两天, 且今天的涨幅比昨天还高 ->买入, 用到了今天的涨幅,只能明天买
  19.             return self.buy_tomorrow()
  20.         return None</span>
复制代码

如下通过字典装载买入因子,只使用AbuTwoDayBuy一个买入因子,字典中设定poly=2, 即多于2次拟合的趋势都认为大盘是处于震荡走势,封锁交易行为,因为基础策略AbuTwoDayBuy中的实现属于频繁买入策略,所以AbuLeastPolyWrap装饰器装饰这个策略的目的就是控制降低交易频率,提过稳定性。
备注: 读者可尝试切换poly值1-4来进行回测,查看回测效果
  1. <span style="background-color: white;">buy_factors = [{'class': AbuTwoDayBuy, 'poly': 2}]</span>
复制代码
最后依然使用abu.run_loop_back对交易进行回测:
  1. <span style="background-color: white;">abu_result_tuple, kl_pd_manger = abu.run_loop_back(read_cash,
  2.                                                    buy_factors,
  3.                                                    sell_factors,
  4.                                                    choice_symbols=choice_symbols,
  5.                                                    n_folds=6)
  6. AbuMetricsBase.show_general(*abu_result_tuple, only_show_returns=True)</span>
复制代码

买入后卖出的交易数量:598
买入后尚未卖出的交易数量:13
胜率:47.6589%
平均获利期望:12.2090%
平均亏损期望:-7.2464%
盈亏比:1.7095
策略收益: 108.1026%
基准收益: 18.5928%
策略年化收益: 18.5067%
基准年化收益: 3.1830%
策略买入成交比例:55.3191%
策略资金利用率比例:49.3860%
策略共执行1472个交易日

112035.png
可以看到AbuTwoDayBuy这个策略有多么简单甚至简陋,但是配合AbuLeastPolyWrap进行回测的效果也还可以。
简单的基础策略是最好的,简单策略和复杂策略输出的都只是什么时候买,什么时候卖。
基础策略追求的就应该是简单(可以一句话说明你的基础策略),不要刻意追求复杂的策略,对量化策略失败结果的人工分析,通过外层辅助优化策略,指导策略,让策略自我进行学习,自我调整才是应该追求复杂的地方。
在《量化交易之路》中反复强调:投资的目标需要有一个比较准确的预测结果,投机的目标是获取交易概率优势,量化交易更倾向于投机范畴,预测肯定了确定性,概率优势不需要肯定确定性,对确定性认识的差异导致交易者最终的交易行为产生根本区别,复杂的策略和简单的策略都对应着概率优势,但是对于策略本身复杂的策略并不一定比简单的策略有效。
对于港股交易还有一个相对其它市场特殊的地方,即每手的数量,A股市场每手的数量是定值100,美股一股也可以买卖,港股是每一个交易品种都有自己对应的每手数量,下面使用AbuHkUnit对每手数量进行查询:
  1. <span style="background-color: white;">unit = AbuHkUnit()
  2. {symbol: unit.query_unit(symbol) for symbol in choice_symbols}</span>
复制代码

  1. <span style="background-color: white;">{'hk00656': 500,
  2. 'hk00700': 100,
  3. 'hk01359': 1000,
  4. 'hk02318': 500,
  5. 'hk02333': 500,
  6. 'hk03333': 1000,
  7. 'hk03888': 1000}</span>
复制代码

可以看到每一个股票的unit都是不同的,在abupy内部会根据资金管理计算出来的买入数量和对应股票的unit进行对比,不满足一手的不能成交,其它市场的实现也类似。



回复

使用道具 举报

您需要登录后才可以回帖 登录 | 免费注册
关注微信