说明
项目结束了,这几天把量化第一版搭起来,量化很重要,现在可以迈出第一步了。首先要关注的是回测,和前不久写的这篇文章呼应,测试的确是一个相对独立,又非常重要的部件。过去比较少关注在方面上,所以现在要特别的加强。
内容
1 前言
量化的模型和策略方面我觉得还是不难的,而且每个人都可以有自己的思路,所以反而没啥好说。
我觉得建立回测服务是比较重要的,因为很多想法行不行的通,最好直接跑一个服务来测试。
1 以测试为导向,不仅是量化的要点,也是其他AI项目的第一规则
2 强调框架性,通过框架设计才能保证稳定、灵活,同时框架一定会带来管理和性能上的损耗,要平衡好。
3 通过微服务体系构建分布式的服务。
以上三点是通用的。
回到眼前这个具体的问题,如何构造一个测试方法+服务来快速的获取KPI,这些KPI足以证明模型的泛化性良好,这样才可以让我们放心的去用。
2 框架
对于一个完整的功能来说,必须要有框架设计。否则迟早要回头返工。
不考虑状态的框架,也是不完整的框架。
从强化学习或者模拟类优化(遗传算法)来看,算法的迭代和演进是需要”投石问路“的部分的。
假设未来算法在自动执行了,那么有80%会按照当前已知的最优路径前进,剩余的20%则会进行搜索,以便进行优化、纠偏等动作。
所以状态(State)、行为(Action)和回报(Return)是一个智能体的三元组,这个很精辟。
除了把框架当成智能体,我们也会希望它按照清晰、固定的规则去可靠执行。
所以很有意思,在决策领域我们会看到规则和模型,前者是简单、清晰和固定的,而后者是复杂、隐晦和多变的;规则和模型的关系恰如现在非智能框架与智能框架的关系。
我觉得可以从人的角度来近似解释这两者的矛盾关系,叫「择善固执」。
在理想的状态下,我们择善了,然后固执下去,最终获得大丰收。实际情况是,我们很可能不知道什么是善。例如达芬奇在学画画时画的无数个鸡蛋,或者我在开始做人工智能时只盯着专业时间的累积,反复的用Python去搭建各种工具。
在我们的认知很有限时,可能只能通过很简单、很模糊的大方向做下去,通过不停的做某些事,让大脑和身体得到反馈,才能不断的在一条专业路线上不断前进。
在做的时候我们可能又要不停的想办法,在合理的时间内给到最好的结果,这就是择善。接着就会不断的提出设想、POC、推翻设想或接受设想。
很多功在事后发现是无用功,但我们不是先知。
所以我想,对于框架或者决策,都会有”傻“的部分和”聪明“的部分。这两者可以独立存在,又可以互相配合。
通过上面的逻辑推演、辩证,我认为:
- 1 第一阶段还是出”傻“框架,可能只是人工枚举有限的状态,并指定一些规则(例如止盈止损)
- 2 第二阶段会出”聪明“框架,这种框架是自适应的,这种框架也是不稳定的(主动随机)
- 3 第三阶段既傻又聪明的框架,可以认为是那些企业家偶像或者管理者偶像:即具备了极大的远见,又能carry所有的难题,直到胜利
3 实现
先封一版吧。
先看结果,标的是沪深300指数, 每次限额5000元交易。信号用的是我之前开发的一版,可能不是非常完美,但问题是不大的。
{'total_win': 13247.159999999996,
'total_loss': -3199.1800000000003,
'orders': 74,
'avg_npr': 135.7835135135135,
'np_sum': 10047.98,
'mid_hold_days': 7.0,
'total_hold_days': 740,
'all_days': 3504,
'money_usage': 0.21118721461187215,
'monthly_win_loss': buy_yymon loss win net win_loss_ratio
0 2013-05 0.00 95.15 95.15 inf
1 2014-01 0.00 113.73 113.73 inf
2 2014-11 0.00 711.40 711.40 inf
3 2014-12 0.00 723.00 723.00 inf
4 2015-01 -273.60 137.72 -135.88 0.50
5 2015-03 0.00 1384.86 1384.86 inf
6 2015-04 0.00 703.84 703.84 inf
7 2015-06 -548.83 0.00 -548.83 0.00
8 2015-07 -738.23 1203.84 465.61 1.63
9 2015-08 -393.78 264.10 -129.68 0.67
10 2015-09 0.00 240.89 240.89 inf
11 2015-11 0.00 464.76 464.76 inf
12 2016-01 -44.29 192.74 148.45 4.35
13 2016-02 0.00 261.10 261.10 inf
14 2016-03 0.00 67.36 67.36 inf
15 2016-05 0.00 26.66 26.66 inf
16 2018-02 0.00 257.48 257.48 inf
17 2018-07 0.00 218.70 218.70 inf
18 2018-08 0.00 57.68 57.68 inf
19 2018-09 0.00 128.52 128.52 inf
20 2018-10 0.00 173.43 173.43 inf
21 2019-02 0.00 430.75 430.75 inf
22 2019-03 0.00 403.74 403.74 inf
23 2019-05 -26.74 113.72 86.98 4.25
24 2019-06 0.00 249.26 249.26 inf
25 2019-09 0.00 96.89 96.89 inf
26 2019-12 0.00 65.36 65.36 inf
27 2020-02 0.00 446.12 446.12 inf
28 2020-03 0.98 0.00 0.98 0.00
29 2020-06 0.00 875.37 875.37 inf
30 2020-07 -252.60 165.60 -87.00 0.66
31 2020-08 0.00 167.04 167.04 inf
32 2020-09 0.00 178.57 178.57 inf
33 2020-11 0.00 199.40 199.40 inf
34 2020-12 0.00 337.80 337.80 inf
35 2021-01 0.00 465.90 465.90 inf
36 2021-02 -195.25 0.00 -195.25 0.00
37 2021-03 -278.40 197.10 -81.30 0.71
38 2021-04 0.00 156.50 156.50 inf
39 2021-05 0.00 110.47 110.47 inf
40 2021-07 0.00 114.00 114.00 inf
41 2021-08 0.00 116.10 116.10 inf
42 2021-09 0.00 33.80 33.80 inf
43 2021-12 0.00 126.90 126.90 inf
44 2022-01 -42.20 0.00 -42.20 0.00
45 2022-03 0.00 277.08 277.08 inf
46 2022-04 -148.32 0.00 -148.32 0.00
47 2022-06 0.00 208.73 208.73 inf
48 2022-09 -257.92 0.00 -257.92 0.00
49 2022-10 0.00 264.36 264.36 inf
50 2022-12 0.00 19.64 19.64 inf,
'yearly_result': buy_yy loss win net win_loss_ratio
0 2013 0.00 95.15 95.15 inf
1 2014 0.00 1548.13 1548.13 inf
2 2015 -1954.44 4400.01 2445.57 2.25
3 2016 -44.29 547.86 503.57 12.37
4 2018 0.00 835.81 835.81 inf
5 2019 -26.74 1359.72 1332.98 50.85
6 2020 -251.62 2369.90 2118.28 9.42
7 2021 -473.65 1320.77 847.12 2.79
8 2022 -448.44 769.81 321.37 1.72}
资金曲线是这样的,过于理想以至于我会有点怀疑。
信号(模型)的事先放一放,问题不大。这里主要还是看构造了哪些对象来实现回测。
Naive
我不太喜欢用中括号引用数据,所以构造了一个简单的对象,可以用点来连接属性。
class Naive:
def __init__(self):
pass
def dict(self):
return self.__dict__
Order
Order是一个核心的功能对象。
- 1 Order会引起资产的变化
- 2 资产的变化可能会作用到Order
- 3 Order本身也有一些简单的风控规则
- 4 Order需要提供结果的分析
Order的主要属性:
- 1 参数类属性。例如每个订单的止盈止损(上面的测试用了15%,-5%)
- 2 过程变量。例如存放打开和关闭订单的列表
- 3 每个时隙的数据 slot_data。(Naive实例)
Order的主要方法:
- 1 setattr : 将每个时隙的数据刷新slot_data。
- 2 buy: 按照时隙的高点买入。
- 3 sell: 按照时隙的低点卖出。
- 4 evaluate: 评估,以决定是否按照止盈止损规则卖出。
- 5 analysis: 分析,得到一开始看到的结果。主要从整体盈利、盈亏比、中位持仓时间、整体持仓时间、按月和按年的盈亏情况来看。一看盈利能力,一看稳定性。
在使用时,buy只会根据signal来行动;而sell不仅会根据signal,还会根据evaluate中包含的规则。
- 1 当signal为-1时,一定会强制全部卖出
- 2 当signal为0或缺失,则进行风险判定,如果有触发规则的订单会被平掉。
Capital
一开始我以为是比较核心的,但实际上是简单的。
Capital的主要属性:
- 1 参数类。损失和回撤。
- 2 过程变量。有资产变化的时隙数据。
Capital的主要方法:
- 1 drawback 看回撤
- 2 cap_loss 看损失(本金)
- 3 evaluate 调用以上两个方法
Signal
严格意义上这次并没有开发完整,但是框架也是很简单
Signal的主要属性:
- 1 参数类。如状态, Test、Prod
- 2 时隙数据。
Signal的主要方法:
- 1 setattr: 刷新数据,在测试中做好了slot~signal字典,在生产中可能要读入一大批时隙特征。
- 2 fake_predict:查字典式的给出时隙信号
- 3 predict: 一个形式上统一的预测方法
- 4 *model_predict: 本次没写,这个可以把模型放在本地来预测
- 5 *api_predict: 本次没写,以web api方式获取信号(预测模型封装为服务)
Strategy
将Order、Capital和Signal三个对象结合起来,每次刷新数据后调用一次判断。将所有的回测数据整理好就可以进行了。
核心的一段调用代码
# 执行流程
def pass_process(self):
if self.rule_result =='Pass':
self.passattr()
cur_signal = self.Signal.predict()
if cur_signal ==1:
print('>>signal 1')
self.Order.buy()
elif cur_signal == -1:
print('>>signal -1')
self.Order.sell(is_force=True)
else:
self.Order.evaluate()
self.Order.sell()
the_capital_change = self.Order.get_change_tuple()
if the_capital_change:
self.Capital.appendattr(the_capital_change)
整体测试
a_strategy = Strategy(singal_dict=test_signal_dict1, init_cash=6000, capital_drawback_stop =-0.1, order_win_stop = 0.15, order_loss_stop = -0.05)
for i in tqdm.tqdm(range(len_of_trade)):
tem_dict = dict(trade_df.iloc[i])
a_strategy.process(tem_dict)
if a_strategy.rule_result =='Stop':
print('Stopped @ %i' %i)
break
4 结语
有两点。
第一,这次测试使用对象化(OOP)去解决问题。现在看来我也可以适应这种方式,应该可以达到我期望的效果(更大规模的集成和效率提升)。
这次设计和测试的过程很短,有出现过一个小bug,查起来还挺麻烦的。所以后续除了要从设计上分开,调试和测试也应该分开来独立进行。
总体是好的。
第二,这次拍了一个测试的版本。后续把模型重塑一下应该就可以实战了,目前看来,这条路子是通的。