向量化回测之后的风险管理

大规模部署自动驾驶汽车的一个重要障碍是安全保障。 ——Majid Khonji 等,2019 年 拥有更好的预测能够提高判断的价值。毕竟,如果你不知道自己有多不喜欢被雨 淋湿或有多讨厌带伞,那么知道下雨的可能性是没有用的。 ——Ajay Agrawal 等,2018 年 一般来说,向量化回测使人们能够根据原来的策略(纯形式上的同样方式)判断基于预测 的算法交易策略的经济潜力。在实际应用中,大多数人工智能体除了预测模型之外,还有 更多的组成部分。例如,自动驾驶汽车的人工智能不是独立的,而是拥有大量规则和启发 式算法,这些规则和算法用于限制人工智能采取或能够采取的行动。在自动驾驶汽车的环 境中,这主要与管理风险有关,比如由碰撞导致的风险。 在金融环境中,人工智能体或交易机器人也不会按原样部署。相反,通常会有许多标准的 风控措施,比如(跟踪)止损单或获利单。道理是很清楚的。在金融市场上进行定向押注 时,要避免过大的损失。同样,当达到一定的利润水平时,投资的成功则需通过提前平仓 来保护。如何处理这些风控措施往往取决于人类的判断,可能需要对相关数据和统计数字 进行正式分析。从概念上讲,这是 Agrawal 等(2018)在书中讨论的一个主要观点:人工 智能提供了改进的预测,但人类的判断在制定决策规则和行动边界方面仍然发挥着作用。 本章有 3 个目的。第一,对经过训练的深度 Q 学习智能体所产生的向量化和基于事件的流 行算法交易策略进行回测。此后,这种智能体被称为交易机器人。第二,评估与应用交易 策略的金融工具相关的风险。第三,使用本章介绍的事件法,对止损单等典型风控措施进 行回测。与向量化回测相比,基于事件的回测的主要优点是在建模和分析决策规则与风控 管理措施方面具有更高的灵活性。换句话说,它允许人们在使用向量化编程方法时放大事 图灵社区会员 cxc_3612(17665373813) 专享 尊重版权250 | 第 11 章 件发展的背景细节。 11.1 节介绍并训练了基于第 9 章 Q 学习金融智能体的交易机器人。11.2 节使用第 10 章的向 量化回测来判断交易机器人的(纯)经济性能。11.3 节介绍了基于事件的回测。首先讨论 了基类。然后基于基类对交易机器人进行了回测。在这方面,也可参考 Hilpisch(2020)的 第 6 章。11.4 节分析了对制定风险管理规则非常重要的选定的统计指标,比如最大回撤和 真实波动幅度均值(ATR)。11.5 节回测了主要风控措施对交易机器人性能的影响。 11.1 交易机器人 本节介绍了一款基于第 9 章的 Q 学习金融智能体 FQLAgent 的交易机器人,这是后面几节 将要分析的交易机器人。和往常一样,首先导入必要的库并进行环境设置。 In [1]: import os import numpy as np import pandas as pd from pylab import plt, mpl plt.style.use('seaborn') mpl.rcParams['savefig.dpi'] = 300 mpl.rcParams['font.family'] = 'serif' pd.set_option('mode.chained_assignment', None) pd.set_option('display.float_format', '{:.4f}'.format) np.set_printoptions(suppress=True, precision=4) os.environ['PYTHONHASHSEED'] = '0' 11.7 节展示了一个 Python 模块,其中包括下面将使用的 Finance 类。本节提供了带有 TradingBot 类和一些辅助函数的 Python 模块,以用于绘制训练结果和验证结果。这两个类 都非常接近第 9 章中介绍的类,这就是这里使用它们而没有做进一步解释的原因。 下面的代码会训练交易机器人使用历史的日终数据,包括用于验证的子数据集。图 11-1 显 示了不同训练回合获得的平均总奖励。 In [2]: import finance import tradingbot Using TensorFlow backend. In [3]: symbol = 'EUR=' features = [symbol, 'r', 's', 'm', 'v'] In [4]: a = 0 b = 1750 c = 250 In [5]: learn_env = finance.Finance(symbol, features, window=20, lags=3, leverage=1, min_performance=0.9, min_accuracy=0.475, start=a, end=a + b, mu=None, std=None) In [6]: learn_env.data.info() DatetimeIndex: 1750 entries, 2010-02-02 to 2017-01-12 Data columns (total 6 columns):风险管理 | 251 # Column Non-Null Count Dtype --- ------ -------------- ----- 0 EUR= 1750 non-null float64 1 r 1750 non-null float64 2 s 1750 non-null float64 3 m 1750 non-null float64 4 v 1750 non-null float64 5 d 1750 non-null int64 dtypes: float64(5), int64(1) memory usage: 95.7 KB In [7]: valid_env = finance.Finance(symbol, features=learn_env.features, window=learn_env.window, lags=learn_env.lags, leverage=learn_env.leverage, min_performance=0.0, min_accuracy=0.0, start=a + b, end=a + b + c, mu=learn_env.mu, std=learn_env.std) In [8]: valid_env.data.info() DatetimeIndex: 250 entries, 2017-01-13 to 2018-01-10 Data columns (total 6 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 EUR= 250 non-null float64 1 r 250 non-null float64 2 s 250 non-null float64 3 m 250 non-null float64 4 v 250 non-null float64 5 d 250 non-null int64 dtypes: float64(5), int64(1) memory usage: 13.7 KB In [9]: tradingbot.set_seeds(100) agent = tradingbot.TradingBot(24, 0.001, learn_env, valid_env) In [10]: episodes = 61 In [11]: %time agent.learn(episodes) ======================================================================= episode: 10/61 | VALIDATION | treward: 247 | perf: 0.936 | eps: 0.95 ======================================================================= ======================================================================= episode: 20/61 | VALIDATION | treward: 247 | perf: 0.897 | eps: 0.86 ======================================================================= ======================================================================= episode: 30/61 | VALIDATION | treward: 247 | perf: 1.035 | eps: 0.78 ======================================================================= ======================================================================= episode: 40/61 | VALIDATION | treward: 247 | perf: 0.935 | eps: 0.70 ======================================================================= ======================================================================= episode: 50/61 | VALIDATION | treward: 247 | perf: 0.890 | eps: 0.64 ======================================================================= ======================================================================= episode: 60/61 | VALIDATION | treward: 247 | perf: 0.998 | eps: 0.58 =======================================================================252 | 第 11 章 episode: 61/61 | treward: 17 | perf: 0.979 | av: 475.1 | max: 1747 CPU times: user 51.4 s, sys: 2.53 s, total: 53.9 s Wall time: 47 s In [12]: tradingbot.plot_treward(agent) 回合 总 奖 励 移动平均线 回归线 图 11-1:每个训练回合的平均总奖励 图 11-2 比较了交易机器人在训练数据集与验证数据集上的总体表现,由于在探索和利用间 交替使用,训练数据集表现出了相当大的差异,验证数据集则仅用于利用。 In [13]: tradingbot.plot_performance(agent) 回合 总 收 益 训练数据集 回归线(训练集) 验证数据集 回归线(验证集) 图 11-2:交易机器人在训练数据集和验证数据集上的总体表现 这个训练好的交易机器人在下一节中用于回测。风险管理 | 253 11.2 向量化回测 向量化回测不能直接应用于交易机器人。第 10 章使用 DNN 说明过该方法。在这种情况 下,首先准备好具有特征和标签子集的数据,然后将其输入 DNN 模型以立即生成所有预 测。在强化学习环境中,数据是通过一步一步地与环境交互来生成和收集的。 为此,下面的 Python 代码定义了 backtest 函数,该函数将 TradingBot 实例和 Finance 实 例作为输入。它在 Finance 环境列的原始 Data Frame 对象中生成了交易机器人所持有的头 寸和最终策略收益。 In [14]: def reshape(s): return np.reshape(s, [1, learn_env.lags, learn_env.n_features]) ➊ In [15]: def backtest(agent, env): env.min_accuracy = 0.0 env.min_performance = 0.0 done = False env.data['p'] = 0 ➋ state = env.reset() while not done: action = np.argmax( agent.model.predict(reshape(state))[0, 0]) ➌ position = 1 if action == 1 else -1 ➍ env.data.loc[:, 'p'].iloc[env.bar] = position ➎ state, reward, done, info = env.step(action) env.data['s'] = env.data['p'] * env.data['r'] * learn_env.leverage ➏ ➊ 改变单个特性 – 标签组合的形式。 ➋ 生成头寸值列。 ➌ 给定训练好的 DNN 模型,得到最优动作(预测)。 ➍ 导出最终头寸(+1 表示多头 / 向上,–1 表示空头 / 向下)…… ➎ ……并存储在相应列的合适的索引位置。 ➏ 计算给定头寸值策略的对数收益率。 配置了 backtest 函数后,向量化回测可以像第 10 章那样简化为几行 Python 代码。 图 11-3 比较了被动基准投资与交易机器人(策略投资)的总体表现。 In [16]: env = agent.learn_env ➊ In [17]: backtest(agent, env) ➋ In [18]: env.data['p'].iloc[env.lags:].value_counts() ➌ Out[18]: 1 961 -1 786 Name: p, dtype: int64 In [19]: env.data[['r', 's']].iloc[env.lags:].sum().apply(np.exp) ➍254 | 第 11 章 Out[19]: r 0.7725 s 1.5155 dtype: float64 In [20]: env.data[['r', 's']].iloc[env.lags:].sum().apply(np.exp) - 1 ➎ Out[20]: r -0.2275 s 0.5155 dtype: float64 In [21]: env.data[['r', 's']].iloc[env.lags:].cumsum( ).apply(np.exp).plot(figsize=(10, 6)); ➊ 指定相关环境。 ➋ 生成所需的额外数据。 ➌ 计算多头头寸和空头头寸的数量。 ➍ 计算被动基准投资(r)和策略(s)投资的总收益…… ➎ ……以及相应的净收益。 时间 2011年 2012年 2013年 2014年 2015年 2016年 2017年 2010年 图 11-3:被动基准投资和交易机器人的总体表现(样本内) 为了更真实地了解交易机器人的性能,下面的 Python 代码使用交易机器人尚未看到的数据 创建了一个测试环境。图 11-4 显示了与被动基准投资相比,交易机器人的表现情况。 In [22]: test_env = finance.Finance(symbol, features=learn_env.features, window=learn_env.window, lags=learn_env.lags, leverage=learn_env.leverage, min_performance=0.0, min_accuracy=0.0, start=a + b + c, end=None, mu=learn_env.mu, std=learn_env.std)风险管理 | 255 In [23]: env = test_env In [24]: backtest(agent, env) In [25]: env.data['p'].iloc[env.lags:].value_counts() Out[25]: -1 437 1 56 Name: p, dtype: int64 In [26]: env.data[['r', 's']].iloc[env.lags:].sum().apply(np.exp) Out[26]: r 0.9144 s 1.0992 dtype: float64 In [27]: env.data[['r', 's']].iloc[env.lags:].sum().apply(np.exp) - 1 Out[27]: r -0.0856 s 0.0992 dtype: float64 In [28]: env.data[['r', 's']].iloc[env.lags:].cumsum( ).apply(np.exp).plot(figsize=(10, 6)); 日日期期 时间 2018年4月 2018年7月 2018年10月 2019年1月 2019年4月 2019年7月 2019年10月 2020年1月 2018年1月 图 11-4:被动基准投资和交易机器人的总体表现(样本外) 在没有设置任何风控措施的情况下,样本外的表现似乎已经很不错了。然而,为了能够正确 判断一个交易策略的真实表现,应该包括风控措施。这就是基于事件的回测发挥作用的地方。 11.3 基于事件的回测 鉴于上一节的结果,没有任何风控措施的样本外表现似乎已经很不错了。然而,为了能够 正确分析风控措施(比如跟踪止损单),需要使用基于事件的回测,这也是本节将介绍的 判断算法交易策略收益的另一种方法。256 | 第 11 章 11.7.3 节会介绍 BacktestingBase 类,它可以灵活地用于测试不同类型的方向性交易策略。 代码的重要行上有详细的注释。此基类提供了以下方法。 get_date_price() 对于给定的 bar(包含金融数据的 DataFrame 对象的索引值),该方法会返回相关的 date 和 price。 print_balance() 对于给定的 bar,该方法会打印交易机器人的当前(现金)余额。 calculate_net_wealth() 对于给定的 price,该方法会返回由当前(现金)余额和工具头寸组成的净资产。 print_net_wealth() 对于给定的 bar,该方法会打印交易机器人的净资产。 place_buy_order(),place_sell_order() 对于给定的 bar 和给定的 units 数量或给定的 amount,这两种方法会发出买入单或卖出 单,并相应地调整相关的数量(比如,计算交易成本)。 close_out() 在给定的 bar 上,该方法会关闭未平仓头寸,并计算和报告业绩统计数据。 下面的 Python 代码演示了 BacktestingBase 类函数的实例如何基于一些简单的步骤运行。 In [29]: import backtesting as bt In [30]: bb = bt.BacktestingBase(env=agent.learn_env, model=agent.model, amount=10000, ptc=0.0001, ftc=1.0, verbose=True) ➊ In [31]: bb.initial_amount ➋ Out[31]: 10000 In [32]: bar = 100 ➌ In [33]: bb.get_date_price(bar) ➍ Out[33]: ('2010-06-25', 1.2374) In [34]: bb.env.get_state(bar) ➎ Out[34]: EUR= r s m v Date 2010-06-22 -0.0242 -0.5622 -0.0916 -0.2022 1.5316 2010-06-23 0.0176 0.6940 -0.0939 -0.0915 1.5563 2010-06-24 0.0354 0.3034 -0.0865 0.6391 1.0890 In [35]: bb.place_buy_order(bar, amount=5000) ➏ 2010-06-25 | buy 4040 units for 1.2374 2010-06-25 | current balance = 4999.40 In [36]: bb.print_net_wealth(2 * bar) ➐ 2010-11-16 | net wealth = 10450.17 图灵社区会员 cxc_3612(17665373813) 专享 尊重版权风险管理 | 257 In [37]: bb.place_sell_order(2 * bar, units=1000) ➑ 2010-11-16 | sell 1000 units for 1.3492 2010-11-16 | current balance = 6347.47 In [38]: bb.close_out(3 * bar) ➒ ================================================== 2011-04-11 | *** CLOSING OUT *** 2011-04-11 | sell 3040 units for 1.4434 2011-04-11 | current balance = 10733.97 2011-04-11 | net performance [%] = 7.3397 2011-04-11 | number of trades [#] = 3 ================================================== ➊ 实例化 BacktestingBase 对象。 ➋ 查找 initial_amount 属性值。 ➌ 固定 bar 值。 ➍ 检索 bar 值的 date 值和 price 值。 ➎ 检索 bar 值的 Finance 环境的状态。 ➏ 使用 amount 参数下买入单。 ➐ 打印稍后的时间点的净资产(2 * bar)。 ➑ 使用 units 参数在稍后的时间点下卖出单。 ➒ 了结剩余的多头头寸(3 * bar)。 继承自 BacktestingBase 类的 TBBacktester 类为交易机器人实现了基于事件的回测。 In [39]: class TBBacktester(bt.BacktestingBase): def _reshape(self, state): ''' 用来对状态对象进行重塑的辅助函数 ''' return np.reshape(state, [1, self.env.lags, self.env.n_features]) def backtest_strategy(self): ''' 基于事件对交易机器人进行性能回测 ''' self.units = 0 self.position = 0 self.trades = 0 self.current_balance = self.initial_amount self.net_wealths = list() for bar in range(self.env.lags, len(self.env.data)): date, price = self.get_date_price(bar) if self.trades == 0: print(50 * '=') print(f'{date} | *** START BACKTEST ***') self.print_balance(bar) print(50 * '=') state = self.env.get_state(bar) ➊ action = np.argmax(self.model.predict( self._reshape(state.values))[0, 0]) ➋ position = 1 if action == 1 else -1 ➌ if self.position in [0, -1] and position == 1: ➍ 图灵社区会员 cxc_3612(17665373813) 专享 尊重版权258 | 第 11 章 if self.verbose: print(50 * '-') print(f'{date} | *** GOING LONG ***') if self.position == -1: self.place_buy_order(bar - 1, units=-self.units) self.place_buy_order(bar - 1, amount=self.current_balance) if self.verbose: self.print_net_wealth(bar) self.position = 1 elif self.position in [0, 1] and position == -1: ➎ if self.verbose: print(50 * '-') print(f'{date} | *** GOING SHORT ***') if self.position == 1: self.place_sell_order(bar - 1, units=self.units) self.place_sell_order(bar - 1, amount=self.current_balance) if self.verbose: self.print_net_wealth(bar) self.position = -1 self.net_wealths.append((date, self.calculate_net_wealth(price))) ➏ self.net_wealths = pd.DataFrame(self.net_wealths, columns=['date', 'net_wealth']) ➏ self.net_wealths.set_index('date', inplace=True) ➏ self.net_wealths.index = pd.DatetimeIndex( self.net_wealths.index) ➏ self.close_out(bar) ➊ 检索 Finance 环境的状态。 ➋ 根据状态和 model 对象生成最优操作(预测)。 ➌ 根据最优操作(预测)得到最优头寸(多头 / 空头)。 ➍ 如果满足条件,就进入多头头寸。 ➎ 如果满足条件,就进入空头头寸。 ➏ 收集随时间变化的净资产值,并将其转换为 DataFrame 对象。 TBBacktester 类的应用很简单,因为 Finance 实例和 TradingBot 实例已经可用。下面的代 码首先在学习环境数据上对交易机器人进行回测,其中既包括无交易成本的数据也包括有 交易成本的数据。图 11-5 直观地比较了这两种情况。 In [40]: env = learn_env In [41]: tb = TBBacktester(env, agent.model, 10000, 0.0, 0, verbose=False) ➊ In [42]: tb.backtest_strategy() ➊ ================================================== 2010-02-05 | *** START BACKTEST *** 2010-02-05 | current balance = 10000.00 ==================================================风险管理 | 259 ================================================== 2017-01-12 | *** CLOSING OUT *** 2017-01-12 | current balance = 14601.85 2017-01-12 | net performance [%] = 46.0185 2017-01-12 | number of trades [#] = 828 ================================================== In [43]: tb_ = TBBacktester(env, agent.model, 10000, 0.00012, 0.0, verbose=False) In [44]: tb_.backtest_strategy() ➋ ================================================== 2010-02-05 | *** START BACKTEST *** 2010-02-05 | current balance = 10000.00 ================================================== ================================================== 2017-01-12 | *** CLOSING OUT *** 2017-01-12 | current balance = 13222.08 2017-01-12 | net performance [%] = 32.2208 2017-01-12 | number of trades [#] = 828 ================================================== In [45]: ax = tb.net_wealths.plot(figsize=(10, 6)) tb_.net_wealths.columns = ['net_wealth (after tc)'] tb_.net_wealths.plot(ax=ax); ➊ 无交易成本基于事件的样本内回测。 ➋ 有交易成本基于事件的样本内回测。 净资产 净资产(无交易成本) 时间 2011年 2012年 2013年 2014年 2015年 2016年 2017年 2010年 15 000 14 000 13 000 12 000 11 000 10 000 图 11-5:交易机器人在有交易成本和无交易成本两种情况下的总体表现(样本内) 图 11-6 再次对包含交易成本前后测试环境数据的交易机器人的总体表现进行了比较。 In [46]: env = test_env In [47]: tb = TBBacktester(env, agent.model, 10000, 0.0, 0, verbose=False) ➊260 | 第 11 章 In [48]: tb.backtest_strategy() ➊ ================================================== 2018-01-17 | *** START BACKTEST *** 2018-01-17 | current balance = 10000.00 ================================================== ================================================== 2019-12-31 | *** CLOSING OUT *** 2019-12-31 | current balance = 10936.79 2019-12-31 | net performance [%] = 9.3679 2019-12-31 | number of trades [#] = 186 ================================================== In [49]: tb_ = TBBacktester(env, agent.model, 10000, 0.00012, 0.0, verbose=False) In [50]: tb_.backtest_strategy() ➋ ================================================== 2018-01-17 | *** START BACKTEST *** 2018-01-17 | current balance = 10000.00 ================================================== ================================================== 2019-12-31 | *** CLOSING OUT *** 2019-12-31 | current balance = 10695.72 2019-12-31 | net performance [%] = 6.9572 2019-12-31 | number of trades [#] = 186 ================================================== In [51]: ax = tb.net_wealths.plot(figsize=(10, 6)) tb_.net_wealths.columns = ['net_wealth (after tc)'] tb_.net_wealths.plot(ax=ax); ➊ 无交易成本基于事件的样本外回测。 ➋ 有交易成本基于事件的样本外回测。 时间 净资产 净资产(无交易成本) 2018年4月 2018年7月 2018年10月 2019年1月 2019年4月 2019年7月 2019年10月 2020年1月 2018年1月 11 500 11 250 11 000 10 750 10 500 10 250 10 000 9750 图 11-6:交易机器人在有交易成本和无交易成本两种情况下的总体表现(样本外) 图灵社区会员 cxc_3612(17665373813) 专享 尊重版权风险管理 | 261 在考虑交易成本前,如何比较基于事件的回测性能与向量化回测性能?图 11-7 对比了一段 时间内标准化的净资产与总收益间的关系。由于所采用的技术方法不同,因此这两个时间 序列并不完全相同,但非常相似。性能差异主要可以解释为基于事件的回测对每个头寸假 定的数量相同。向量化回测考虑了复合效应,导致报告的性能略有提高。 In [52]: ax = (tb.net_wealths / tb.net_wealths.iloc[0]).plot(figsize=(10, 6)) tp = env.data[['r', 's']].iloc[env.lags:].cumsum().apply(np.exp) (tp / tp.iloc[0]).plot(ax=ax); 净资产 时间 2018年4月 2018年7月 2018年10月 2019年1月 2019年4月 2019年7月 2019年10月 2020年1月 2018年1月 图 11-7:在向量化回测和基于事件的回测两种情况下,被动基准投资和交易机器人的总体表现 性能差异 向量化回测的性能数据和基于事件的回测的性能数据非常接近,但并不完全 相同。在前一种情况下,假设金融工具是完全可分割的,复利也是连续进行 的。在后一种情况下,只接受金融工具的完整单位进行交易,这更接近现 实。净资产的计算是基于价格差异,例如,其所使用的基于事件的代码不会 检查当前余额是否足够大,以用现金支付某笔交易。这无疑是一个简化的假 设,例如,保证金购买可能并不总是可行。这方面的代码调整可以轻松地添 加到 BacktestingBase 类中。 11.4 风险评估 风控措施的应用需要了解交易所选择的金融工具所涉及的风险。因此,为了合理设置止损 单等风控措施参数,对标的工具的风险进行评估非常重要。有很多方法可以用来衡量金融 工具的风险,既有非方向性的风控措施,比如波动率或 ATR;也有方向性的风控措施,比 如最大回撤或风险价值(VaR)。262 | 第 11 章 当设定止损(SL)单、跟踪止损(TSL)单或止盈(TP)单的目标水平时,通常的做法是 将这些水平与 ATR 关联起来。1 下面的 Python 代码会以绝对和相对的方式计算金融工具的 ATR,交易机器人会在该金融工具上进行训练和回测(欧元 / 美元汇率)。该计算依赖于来 自学习环境的数据,并使用 14 天(条)这种典型窗口长度。图 11-8 显示了计算值,这些 值随时间显著变化。 In [53]: data = pd.DataFrame(learn_env.data[symbol]) ➊ In [54]: data.head() ➊ Out[54]: EUR= Date 2010-02-02 1.3961 2010-02-03 1.3898 2010-02-04 1.3734 2010-02-05 1.3662 2010-02-08 1.3652 In [55]: window = 14 ➋ In [56]: data['min'] = data[symbol].rolling(window).min() ➌ In [57]: data['max'] = data[symbol].rolling(window).max() ➍ In [58]: data['mami'] = data['max'] - data['min'] ➎ In [59]: data['mac'] = abs(data['max'] - data[symbol].shift(1)) ➏ In [60]: data['mic'] = abs(data['min'] - data[symbol].shift(1)) ➐ In [61]: data['atr'] = np.maximum(data['mami'], data['mac']) ➑ In [62]: data['atr'] = np.maximum(data['atr'], data['mic']) ➒ In [63]: data['atr%'] = data['atr'] / data[symbol] ➓ In [64]: data[['atr', 'atr%']].plot(subplots=True, figsize=(10, 6)); ➊ 原始 DataFrame 对象中的价格序列。 ➋ 用于计算的窗口长度。 ➌ 滚动最小值。 ➍ 滚动最大值。 ➎ 滚动最大值与最小值之差。 ➏ 滚动最大值与前一天价格的绝对差值。 ➐ 滚动最小值与前一天价格的绝对差值。 ➑ 最大 / 最小差价与最大价差中的最大值。 注 1: 有关 ATR 度量的详细信息,请参阅 ATR(1)Investopedia 或 ATR(2)Investopedia。风险管理 | 263 ➒ 上一个最大值和最小价差之间的最大值(=ATR)。 ➓ ATR 的绝对值和价格的百分比。 时间 2011年 2012年 2013年 2014年 2015年 2016年 2017年 2010年 图 11-8: 以绝对值(价格)和相对值(%)形式表示的 ATR 下面的代码以绝对值和相对值的形式显示了 ATR 的最终值。例如,一个典型的规则是设置 止损水平为进入价格减去 x 倍 ATR。根据交易员或投资者的风险偏好,x 可能小于或大于 1。这就是人类判断或正式风险政策发挥作用的地方。如果 x = 1,则将止损水平设置为低 于入门水平的 2% 左右。 In [65]: data[['atr', 'atr%']].tail() Out[65]: atr atr% Date 2017-01-06 0.0218 0.0207 2017-01-09 0.0218 0.0206 2017-01-10 0.0218 0.0207 2017-01-11 0.0199 0.0188 2017-01-12 0.0206 0.0194 然而,杠杆在这种情况下扮演着重要的角色。如果使用的杠杆率为 10(这对外汇交易来说 实际上是很低的),那么 ATR 数字需要乘以杠杆率。因此,对于假定的 ATR 系数 1,与之 前相同的止损水平将被设定为 20% 左右,而不是仅仅 2%。或者取整个数据集的 ATR 中 值,设定为 25% 左右。 In [66]: leverage = 10 In [67]: data[['atr', 'atr%']].tail() * leverage Out[67]: atr atr% Date 2017-01-06 0.2180 0.2070 2017-01-09 0.2180 0.2062 2017-01-10 0.2180 0.2066264 | 第 11 章 2017-01-11 0.1990 0.1881 2017-01-12 0.2060 0.1942 In [68]: data[['atr', 'atr%']].median() * leverage Out[68]: atr 0.3180 atr% 0.2481 dtype: float64 将止损水平或止盈水平与 ATR 联系起来的基本理念是,应避免将它们设置得过低或过高。 考虑一个杠杆率为 10 倍、ATR 为 20% 的头寸。仅将止损水平设置为 3% 或 5% 可能会降 低头寸的金融风险,但它会引入过早发生的止损风险,这是由于金融工具的典型波动造成 的。这种在一定范围内的“典型波动”通常被称为噪声。一般而言,止损单应保护市场免 受比典型价格波动(噪声)更大的不利市场波动的影响。 对于获利回吐水平也是如此。如果将它设得太高(比如是 ATR 水平的 3 倍),则无法获得 可观的利润,头寸可能会保持太长时间,直到它们放弃以前的利润。即使在这种情况下可 以使用正式的分析和数学公式,但设定这样的目标水平更多是为了艺术而不是科学。在金 融环境中,设定目标水平有相当大的自由度,而人类的判断可以起到拯救的作用。在其他 情况下,比如自动驾驶汽车,这是不同的,因为不需要人类的判断来指示人工智能避免与 人类的任何碰撞。 非正态性与非线性 当保证金或投资权益用完时,保证金止损会结束交易头寸。假设一个有保证 金止损的杠杆交易头寸,例如,杠杆率为 10 时,保证金是 10% 的权益。交 易工具中 10% 或更大的不利变动会吞噬所有的权益,并触发头寸的平仓—— 损失 100% 的权益。标的资产有利的变动,比如 25% 的变动,就会带来 150% 的权益收益率。即使交易工具的收益率是正态分布的,杠杆和保证金 止损也会导致收益率的非正态分布以及交易工具与交易头寸间的非对称、非 线性关系。 11.5 风控措施回测 了解金融工具的 ATR 通常是实施风控措施的良好开端。为了正确回测典型的风控订单的效 果,对 BacktestingBase 类进行一些调整是有帮助的。下面的 Python 代码提供了一个新的 基类 BacktestBaseRM,它继承自 BacktestingBase,有助于跟踪前一次交易的进入价格以及 自那次交易以来的最高价格和最低价格。这些值用于计算止损单、跟踪止损单和止盈单所 涉及的基于事件的回测性能。 # # 基于事件的回测 # --基类 (2) # # (c) Dr. Yves J. Hilpisch # from backtesting import *风险管理 | 265 class BacktestingBaseRM(BacktestingBase): def set_prices(self, price): ''' 设置用来进行性能回测的价格,比如测试跟踪止损是否命中 ''' self.entry_price = price ➊ self.min_price = price ➋ self.max_price = price ➌ def place_buy_order(self, bar, amount=None, units=None, gprice=None): ''' 对于给定的bar和给定的amount或给定的units数量下一个买入单 ''' date, price = self.get_date_price(bar) if gprice is not None: price = gprice if units is None: units = int(amount / price) self.current_balance -= (1 + self.ptc) * units * price + self.ftc self.units += units self.trades += 1 self.set_prices(price) ➍ if self.verbose: print(f'{date} | buy {units} units for {price:.4f}') self.print_balance(bar) def place_sell_order(self, bar, amount=None, units=None, gprice=None): ''' 对于给定的bar和给定的amount或给定的units数量下一个卖出单 ''' date, price = self.get_date_price(bar) if gprice is not None: price = gprice if units is None: units = int(amount / price) self.current_balance += (1 - self.ptc) * units * price - self.ftc self.units -= units self.trades += 1 self.set_prices(price) ➍ if self.verbose: print(f'{date} | sell {units} units for {price:.4f}') self.print_balance(bar) ➊ 设置最近交易的进入价格。 ➋ 设置自最近交易以来的初始最低价格。 ➌ 设置自最近交易以来的初始最高价格。 ➍ 设置交易执行后的相关价格。 基于这个新的基类,11.7.4 节提供了一个新的回测类 TBBacktesterRM,它允许包含止损单、 跟踪止损单和止盈单。相关代码部分将在接下来的内容中进行讨论。如上一节所计算的, 回测示例的参数将定位在大约 2% 的 ATR 水平上。266 | 第 11 章 EUT 和风控措施 EUT、MVP 和 CAPM(参见第 3 章和第 4 章)假设金融主体知道金融工具 收益率的未来分布。MPT 和 CAPM 进一步假设收益率是呈正态分布的,并 且在市场投资组合的收益率和交易的金融工具的收益率之间存在线性关系。 止损单、跟踪止损单和止盈单的使用除了会导致杠杆作用外,还会导致杠杆 作用与保证金止损相结合形成“有保证的非正态”分布,以及交易头寸相对 于交易工具的高度不对称、非线性回报。 11.5.1 止损 第一个风控措施是止损单。它固定了一个特定的价格水平,或者更常见的是,固定了能够 触发平仓的百分比值。如果一个非杠杆头寸的进入价格为 100,止损水平被设置为 5%,则 多头头寸会在 95 处平仓,而空头头寸会在 105 处平仓。 下面的 Python 代码是处理止损单的 TBBacktesterRM 类的相关部分。对止损单来说,该类 允许用户指定订单的价格水平是否有保证。2 使用有保证的止损价格水平可能会导致过于乐 观的性能结果。 # 止损单 if sl is not None and self.position != 0: ➊ rc = (price - self.entry_price) / self.entry_price ➋ if self.position == 1 and rc < -self.sl: ➌ print(50 * '-') if guarantee: price = self.entry_price * (1 - self.sl) print(f'*** STOP LOSS (LONG | {-self.sl:.4f}) ***') else: print(f'*** STOP LOSS (LONG | {rc:.4f}) ***') self.place_sell_order(bar, units=self.units, gprice=price) ➍ self.wait = wait ➎ self.position = 0 ➏ elif self.position == -1 and rc > self.sl: ➐ print(50 * '-') if guarantee: price = self.entry_price * (1 + self.sl) print(f'*** STOP LOSS (SHORT | -{self.sl:.4f}) ***') else: print(f'*** STOP LOSS (SHORT | -{rc:.4f}) ***') self.place_buy_order(bar, units=-self.units, gprice=price) ➑ self.wait = wait ➎ self.position = 0 ➏ ➊ 检查是否定义了止损,头寸是否为中性。 ➋ 根据最后一笔交易的进入价格计算收益。 ➌ 检查是否给定了一个多头头寸的止损事件。 注 2: 有担保的止损单可能只适用于某些司法管辖区的某些经纪客户群体,比如散户投资者 / 交易员。风险管理 | 267 ➍ 以当前价格或保证价格平仓多头头寸。 ➎ 设置在下一个交易发生之前等待的条数。 ➏ 设置头寸为中性。 ➐ 检查是否为空头头寸给出了一个止损事件。 ➑ 以当前价格或保证价格平仓空头头寸。 下面的 Python 代码分别对没有止损单和有止损单的交易机器人的交易策略进行了回测。对 于给定的参数,止损单对策略性能有负面影响。 In [69]: import tbbacktesterrm as tbbrm In [70]: env = test_env In [71]: tb = tbbrm.TBBacktesterRM(env, agent.model, 10000, 0.0, 0, verbose=False) ➊ In [72]: tb.backtest_strategy(sl=None, tsl=None, tp=None, wait=5) ➋ ================================================== 2018-01-17 | *** START BACKTEST *** 2018-01-17 | current balance = 10000.00 ================================================== ================================================== 2019-12-31 | *** CLOSING OUT *** 2019-12-31 | current balance = 10936.79 2019-12-31 | net performance [%] = 9.3679 2019-12-31 | number of trades [#] = 186 ================================================== In [73]: tb.backtest_strategy(sl=0.0175, tsl=None, tp=None, wait=5, guarantee=False) ➌ ================================================== 2018-01-17 | *** START BACKTEST *** 2018-01-17 | current balance = 10000.00 ================================================== -------------------------------------------------- *** STOP LOSS (SHORT | -0.0203) *** ================================================== 2019-12-31 | *** CLOSING OUT *** 2019-12-31 | current balance = 10717.32 2019-12-31 | net performance [%] = 7.1732 2019-12-31 | number of trades [#] = 188 ================================================== In [74]: tb.backtest_strategy(sl=0.017, tsl=None, tp=None, wait=5, guarantee=True) ➍ ================================================== 2018-01-17 | *** START BACKTEST *** 2018-01-17 | current balance = 10000.00 ================================================== -------------------------------------------------- *** STOP LOSS (SHORT | -0.0170) ***268 | 第 11 章 ================================================== 2019-12-31 | *** CLOSING OUT *** 2019-12-31 | current balance = 10753.52 2019-12-31 | net performance [%] = 7.5352 2019-12-31 | number of trades [#] = 188 ================================================== ➊ 实例化风险管理的回测类。 ➋ 在没有任何风控措施的情况下,对交易机器人的性能进行回测。 ➌ 使用止损单(无保证金)对交易机器人的性能进行回测。 ➍ 使用止损单(有保证金)对交易机器人的性能进行回测。 11.5.2 跟踪止损 与常规的止损单不同,只要在基本指令下达后观察到新的高点,跟踪止损单就会进行调 整。假设无杠杆多头头寸的基础订单的进入价格为 95,跟踪止损被设置为 5%。如果工具 价格达到 100 并回落到 95,则意味着这是一个跟踪止损事件,头寸会在进入价格水平结 束。如果价格达到 110,并回落到 104.5,则意味着这是另一个跟踪止损事件。 下面的 Python 代码是处理跟踪止损单的 TBBacktesterRM 类的相关部分。要正确处理这样 的风控措施,需要跟踪最高价格和最低价格。最高价格适用于多头头寸,最低价格则适用 于空头头寸。 # 跟踪止损单 if tsl is not None and self.position != 0: self.max_price = max(self.max_price, price) ➊ self.min_price = min(self.min_price, price) ➋ rc_1 = (price - self.max_price) / self.entry_price ➌ rc_2 = (self.min_price - price) / self.entry_price ➍ if self.position == 1 and rc_1 < -self.tsl: ➎ print(50 * '-') print(f'*** TRAILING SL (LONG | {rc_1:.4f}) ***') self.place_sell_order(bar, units=self.units) self.wait = wait self.position = 0 elif self.position == -1 and rc_2 < -self.tsl: ➏ print(50 * '-') print(f'*** TRAILING SL (SHORT | {rc_2:.4f}) ***') self.place_buy_order(bar, units=-self.units) self.wait = wait self.position = 0 ➊ 如有必要,就更新最高价格。 ➋ 如有必要,就更新最低价格。 ➌ 计算多头头寸的相关收益。 ➍ 计算空头头寸的相关收益。风险管理 | 269 ➎ 检查是否为多头头寸给出了一个跟踪止损事件。 ➏ 检查是否为空头头寸给出了一个跟踪止损事件。 正如下面的回测结果所示,与没有跟踪止损单的策略相比,在给定参数的情况下使用跟踪 止损单会降低总体性能。 In [75]: tb.backtest_strategy(sl=None, tsl=0.015, tp=None, wait=5) ➊ ================================================== 2018-01-17 | *** START BACKTEST *** 2018-01-17 | current balance = 10000.00 ================================================== -------------------------------------------------- *** TRAILING SL (SHORT | -0.0152) *** -------------------------------------------------- *** TRAILING SL (SHORT | -0.0169) *** -------------------------------------------------- *** TRAILING SL (SHORT | -0.0164) *** -------------------------------------------------- *** TRAILING SL (SHORT | -0.0191) *** -------------------------------------------------- *** TRAILING SL (SHORT | -0.0166) *** -------------------------------------------------- *** TRAILING SL (SHORT | -0.0194) *** -------------------------------------------------- *** TRAILING SL (SHORT | -0.0172) *** -------------------------------------------------- *** TRAILING SL (SHORT | -0.0181) *** -------------------------------------------------- *** TRAILING SL (SHORT | -0.0153) *** -------------------------------------------------- *** TRAILING SL (SHORT | -0.0160) *** ================================================== 2019-12-31 | *** CLOSING OUT *** 2019-12-31 | current balance = 10577.93 2019-12-31 | net performance [%] = 5.7793 2019-12-31 | number of trades [#] = 201 ================================================== ➊ 用跟踪止损单对交易机器人的性能进行回测。 11.5.3 止盈 最后是止盈单。一个止盈单将平仓达到一定利润水平的头寸。假设一个非杠杆多头头寸以 100 的价格开仓,止盈单被设置为 5% 的水平。如果价格达到 105,则平仓。 下面 TBBacktesterRM 类的代码最终显示了处理止盈单的部分。给定止损单和跟踪止损单代 码的引用,止盈实现是很简单的。对于止盈单,可以选择使用与相关高 / 低价格水平相比 的保证价格水平进行回测,但这很可能导致性能值过于乐观。3 注 3: 止盈单有一个固定的目标价格水平。因此,用一个时间间隔的高价来计算多头头寸或用这个时间间隔 的低价来计算空头头寸是不现实的。 图灵社区会员 cxc_3612(17665373813) 专享 尊重版权270 | 第 11 章 # 止盈单 if tp is not None and self.position != 0: rc = (price - self.entry_price) / self.entry_price if self.position == 1 and rc > self.tp: print(50 * '-') if guarantee: price = self.entry_price * (1 + self.tp) print(f'*** TAKE PROFIT (LONG | {self.tp:.4f}) ***') else: print(f'*** TAKE PROFIT (LONG | {rc:.4f}) ***') self.place_sell_order(bar, units=self.units, gprice=price) self.wait = wait self.position = 0 elif self.position == -1 and rc < -self.tp: print(50 * '-') if guarantee: price = self.entry_price * (1 - self.tp) print(f'*** TAKE PROFIT (SHORT | {self.tp:.4f}) ***') else: print(f'*** TAKE PROFIT (SHORT | {-rc:.4f}) ***') self.place_buy_order(bar, units=-self.units, gprice=price) self.wait = wait self.position = 0 与被动基准投资相比,在给定的参数条件下,增加一个无保证金的止盈单可以显著提高交 易机器人的性能。考虑到之前必须考虑的事,这个结果可能过于乐观了。因此,在这种情 况下,带保证金的止盈单使其性能值更加现实。 In [76]: tb.backtest_strategy(sl=None, tsl=None, tp=0.015, wait=5, guarantee=False) ➊ ================================================== 2018-01-17 | *** START BACKTEST *** 2018-01-17 | current balance = 10000.00 ================================================== -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0155) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0155) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0204) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0240) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0168) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0156) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0183) *** ================================================== 2019-12-31 | *** CLOSING OUT *** 2019-12-31 | current balance = 11210.33 2019-12-31 | net performance [%] = 12.1033 2019-12-31 | number of trades [#] = 198 ==================================================风险管理 | 271 In [77]: tb.backtest_strategy(sl=None, tsl=None, tp=0.015, wait=5, guarantee=True) ➋ ================================================== 2018-01-17 | *** START BACKTEST *** 2018-01-17 | current balance = 10000.00 ================================================== -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0150) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0150) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0150) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0150) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0150) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0150) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0150) *** ================================================== 2019-12-31 | *** CLOSING OUT *** 2019-12-31 | current balance = 10980.86 2019-12-31 | net performance [%] = 9.8086 2019-12-31 | number of trades [#] = 198 ================================================== ➊ 用一个止盈单对交易机器人的性能进行回测(无保证金)。 ➋ 用一个止盈单对交易机器人的性能进行回测(有保证金)。 当然,止损单 / 跟踪止损单也可以和止盈单合并。下面的 Python 代码的回测结果在这两种 情况下都比没有风控措施的策略结果更糟糕。在风险管理方面,几乎没有“免费的午餐”。 In [78]: tb.backtest_strategy(sl=0.015, tsl=None, tp=0.0185, wait=5) ➊ ================================================== 2018-01-17 | *** START BACKTEST *** 2018-01-17 | current balance = 10000.00 ================================================== -------------------------------------------------- *** STOP LOSS (SHORT | -0.0203) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0202) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0213) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0240) *** -------------------------------------------------- *** STOP LOSS (SHORT | -0.0171) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0188) *** -------------------------------------------------- *** STOP LOSS (SHORT | -0.0153) *** -------------------------------------------------- *** STOP LOSS (SHORT | -0.0154) *** 图灵社区会员 cxc_3612(17665373813) 专享 尊重版权272 | 第 11 章 ================================================== 2019-12-31 | *** CLOSING OUT *** 2019-12-31 | current balance = 10552.00 2019-12-31 | net performance [%] = 5.5200 2019-12-31 | number of trades [#] = 201 ================================================== In [79]: tb.backtest_strategy(sl=None, tsl=0.02, tp=0.02, wait=5) ➋ ================================================== 2018-01-17 | *** START BACKTEST *** 2018-01-17 | current balance = 10000.00 ================================================== -------------------------------------------------- *** TRAILING SL (SHORT | -0.0235) *** -------------------------------------------------- *** TRAILING SL (SHORT | -0.0202) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0250) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0227) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0240) *** -------------------------------------------------- *** TRAILING SL (SHORT | -0.0216) *** -------------------------------------------------- *** TAKE PROFIT (SHORT | 0.0241) *** -------------------------------------------------- *** TRAILING SL (SHORT | -0.0206) *** ================================================== 2019-12-31 | *** CLOSING OUT *** 2019-12-31 | current balance = 10346.38 2019-12-31 | net performance [%] = 3.4638 2019-12-31 | number of trades [#] = 198 ================================================== ➊ 使用止损单和止盈单对交易机器人的性能进行回测。 ➋ 使用跟踪止损单和止盈单对交易机器人的性能进行回测。 性能影响 风控措施有其合理性和好处。然而,降低风险的代价可能是整体业绩下滑。 另外,使用止盈单的回测例子显示了业绩的改善,这可以解释为,给定金融 工具的 ATR,某一利润水平是能够实现的。但任何希望看到更高利润的愿 望,通常都会在市场再次扭转时破灭。 11.6 结论 本章有 3 个主题。首先以向量化和基于事件的方式对样本外交易机器人(训练有素的深度 Q 学习智能体)的性能进行了回测。然后以 ATR 指标的形式评估了风险,该指标衡量了 利率金融工具价格的典型变化。最后,本章讨论并回测了基于事件的典型风控措施的止损风险管理 | 273 单、跟踪止损单和止盈单形式。 与自动驾驶汽车类似,交易机器人很少只基于人工智能的预测来部署。为了避免巨大的下 行风险并提高(风险调整后的)业绩,风控措施通常会发挥作用。本章所讨论的标准风控 措施既适用于大多数交易平台,也适用于散户交易者。第 12 章将以 Oanda 交易平台为例 说明这一点。基于事件的回测方法提供了算法上的灵活性,可以正确地回测此类风控措施 的效果。虽然“降低风险”听起来很吸引人,但回测结果表明,降低风险往往是有代价 的:与没有任何风控措施的纯策略相比,性能可能更低。然而,当进行精细调试时,结果 显示,像止盈单这样的风控措施也可以对性能产生积极影响。 11.7 Python代码 11.7.1 金融环境 下面是带有 Finance 环境类的 Python 模块。 # # 金融环境 # # (c) Dr. Yves J. Hilpisch # Artificial Intelligence in Finance # import math import random import numpy as np import pandas as pd class observation_space: def __init__(self, n): self.shape = (n,) class action_space: def __init__(self, n): self.n = n def sample(self): return random.randint(0, self.n - 1) class Finance: intraday = False if intraday: url = 'http://hilpisch.com/aiif_eikon_id_eur_usd.csv' else: url = 'http://hilpisch.com/aiif_eikon_eod_data.csv' def __init__(self, symbol, features, window, lags, leverage=1, min_performance=0.85, min_accuracy=0.5, start=0, end=None, mu=None, std=None):274 | 第 11 章 self.symbol = symbol self.features = features self.n_features = len(features) self.window = window self.lags = lags self.leverage = leverage self.min_performance = min_performance self.min_accuracy = min_accuracy self.start = start self.end = end self.mu = mu self.std = std self.observation_space = observation_space(self.lags) self.action_space = action_space(2) self._get_data() self._prepare_data() def _get_data(self): self.raw = pd.read_csv(self.url, index_col=0, parse_dates=True).dropna() if self.intraday: self.raw = self.raw.resample('30min', label='right').last() self.raw = pd.DataFrame(self.raw['CLOSE']) self.raw.columns = [self.symbol] def _prepare_data(self): self.data = pd.DataFrame(self.raw[self.symbol]) self.data = self.data.iloc[self.start:] self.data['r'] = np.log(self.data / self.data.shift(1)) self.data.dropna(inplace=True) self.data['s'] = self.data[self.symbol].rolling(self.window).mean() self.data['m'] = self.data['r'].rolling(self.window).mean() self.data['v'] = self.data['r'].rolling(self.window).std() self.data.dropna(inplace=True) if self.mu is None: self.mu = self.data.mean() self.std = self.data.std() self.data_ = (self.data - self.mu) / self.std self.data['d'] = np.where(self.data['r'] > 0, 1, 0) self.data['d'] = self.data['d'].astype(int) if self.end is not None: self.data = self.data.iloc[:self.end - self.start] self.data_ = self.data_.iloc[:self.end - self.start] def _get_state(self): return self.data_[self.features].iloc[self.bar - self.lags:self.bar] def get_state(self, bar): return self.data_[self.features].iloc[bar - self.lags:bar] def seed(self, seed): random.seed(seed) np.random.seed(seed)风险管理 | 275 def reset(self): self.treward = 0 self.accuracy = 0 self.performance = 1 self.bar = self.lags state = self.data_[self.features].iloc[self.bar - self.lags:self.bar] return state.values def step(self, action): correct = action == self.data['d'].iloc[self.bar] ret = self.data['r'].iloc[self.bar] * self.leverage reward_1 = 1 if correct else 0 reward_2 = abs(ret) if correct else -abs(ret) self.treward += reward_1 self.bar += 1 self.accuracy = self.treward / (self.bar - self.lags) self.performance *= math.exp(reward_2) if self.bar >= len(self.data): done = True elif reward_1 == 1: done = False elif (self.performance < self.min_performance and self.bar > self.lags + 15): done = True elif (self.accuracy < self.min_accuracy and self.bar > self.lags + 15): done = True else: done = False state = self._get_state() info = {} return state.values, reward_1 + reward_2 * 5, done, info 11.7.2 交易机器人 下面是带有 TradingBot 类的 Python 模块,它基于一个 Q 学习金融智能体。 # # Q学习金融智能体 # # (c) Dr. Yves J. Hilpisch # Artificial Intelligence in Finance # import os import random import numpy as np from pylab import plt, mpl from collections import deque import tensorflow as tf from keras.layers import Dense, Dropout from keras.models import Sequential from keras.optimizers import Adam, RMSprop276 | 第 11 章 os.environ['PYTHONHASHSEED'] = '0' plt.style.use('seaborn') mpl.rcParams['savefig.dpi'] = 300 mpl.rcParams['font.family'] = 'serif' def set_seeds(seed=100): ''' 为所有的随机数生成器设置种子 ''' random.seed(seed) np.random.seed(seed) tf.random.set_seed(seed) class TradingBot: def __init__(self, hidden_units, learning_rate, learn_env, valid_env=None, val=True, dropout=False): self.learn_env = learn_env self.valid_env = valid_env self.val = val self.epsilon = 1.0 self.epsilon_min = 0.1 self.epsilon_decay = 0.99 self.learning_rate = learning_rate self.gamma = 0.5 self.batch_size = 128 self.max_treward = 0 self.averages = list() self.trewards = [] self.performances = list() self.aperformances = list() self.vperformances = list() self.memory = deque(maxlen=2000) self.model = self._build_model(hidden_units, learning_rate, dropout) def _build_model(self, hu, lr, dropout): ''' 创建DNN模型 ''' model = Sequential() model.add(Dense(hu, input_shape=( self.learn_env.lags, self.learn_env.n_features), activation='relu')) if dropout: model.add(Dropout(0.3, seed=100)) model.add(Dense(hu, activation='relu')) if dropout: model.add(Dropout(0.3, seed=100)) model.add(Dense(2, activation='linear')) model.compile( loss='mse', optimizer=RMSprop(lr=lr) ) return model def act(self, state): ''' 基于探索或者利用而选择不同的动作 '''风险管理 | 277 if random.random() 基于存储的经验重新训练DNN模型 ''' batch = random.sample(self.memory, self.batch_size) for state, action, reward, next_state, done in batch: if not done: reward += self.gamma * np.amax( self.model.predict(next_state)[0, 0]) target = self.model.predict(state) target[0, 0, action] = reward self.model.fit(state, target, epochs=1, verbose=False) if self.epsilon > self.epsilon_min: self.epsilon *= self.epsilon_decay def learn(self, episodes): ''' 训练DQL智能体 ''' for e in range(1, episodes + 1): state = self.learn_env.reset() state = np.reshape(state, [1, self.learn_env.lags, self.learn_env.n_features]) for _ in range(10000): action = self.act(state) next_state, reward, done, info = self.learn_env.step(action) next_state = np.reshape(next_state, [1, self.learn_env.lags, self.learn_env.n_features]) self.memory.append([state, action, reward, next_state, done]) state = next_state if done: treward = _ + 1 self.trewards.append(treward) av = sum(self.trewards[-25:]) / 25 perf = self.learn_env.performance self.averages.append(av) self.performances.append(perf) self.aperformances.append( sum(self.performances[-25:]) / 25) self.max_treward = max(self.max_treward, treward) templ = 'episode: {:2d}/{} | treward: {:4d} | ' templ += 'perf: {:5.3f} | av: {:5.1f} | max: {:4d}' print(templ.format(e, episodes, treward, perf, av, self.max_treward), end='\r') break if self.val: self.validate(e, episodes) if len(self.memory) > self.batch_size: self.replay() print()278 | 第 11 章 def validate(self, e, episodes): ''' 验证DQL智能体 ''' state = self.valid_env.reset() state = np.reshape(state, [1, self.valid_env.lags, self.valid_env.n_features]) for _ in range(10000): action = np.argmax(self.model.predict(state)[0, 0]) next_state, reward, done, info = self.valid_env.step(action) state = np.reshape(next_state, [1, self.valid_env.lags, self.valid_env.n_features]) if done: treward = _ + 1 perf = self.valid_env.performance self.vperformances.append(perf) if e % int(episodes / 6) == 0: templ = 71 * '=' templ += '\nepisode: {:2d}/{} | VALIDATION | ' templ += 'treward: {:4d} | perf: {:5.3f} | eps: {:.2f}\n' templ += 71 * '=' print(templ.format(e, episodes, treward, perf, self.epsilon)) break def plot_treward(agent): ''' 为每个训练回合的总奖励绘图 ''' plt.figure(figsize=(10, 6)) x = range(1, len(agent.averages) + 1) y = np.polyval(np.polyfit(x, agent.averages, deg=3), x) plt.plot(x, agent.averages, label='moving average') plt.plot(x, y, 'r--', label='regression') plt.xlabel('episodes') plt.ylabel('total reward') plt.legend() def plot_performance(agent): ''' 为每个训练回合的总收益绘图 ''' plt.figure(figsize=(10, 6)) x = range(1, len(agent.performances) + 1) y = np.polyval(np.polyfit(x, agent.performances, deg=3), x) plt.plot(x, agent.performances[:], label='training') plt.plot(x, y, 'r--', label='regression (train)') if agent.val: y_ = np.polyval(np.polyfit(x, agent.vperformances, deg=3), x) plt.plot(x, agent.vperformances[:], label='validation') plt.plot(x, y_, 'r-.', label='regression (valid)') plt.xlabel('episodes') plt.ylabel('gross performance') plt.legend()风险管理 | 279 11.7.3 回测基类 下面是带有 BacktestingBase 类的 Python 模块,用于基于事件的回测。 # # 基于事件的回测 # --基类 (1) # # (c) Dr. Yves J. Hilpisch # Artificial Intelligence in Finance # class BacktestingBase: def __init__(self, env, model, amount, ptc, ftc, verbose=False): self.env = env ➊ self.model = model ➋ self.initial_amount = amount ➌ self.current_balance = amount ➌ self.ptc = ptc ➍ self.ftc = ftc ➎ self.verbose = verbose ➏ self.units = 0 ➐ self.trades = 0 ➑ def get_date_price(self, bar): ''' 返回给定的bar所对应的日期和价格 ''' date = str(self.env.data.index[bar])[:10] ➒ price = self.env.data[self.env.symbol].iloc[bar] ➓ return date, price def print_balance(self, bar): ''' 打印给定的bar所对应的现金余额 ''' date, price = self.get_date_price(bar) print(f'{date} | current balance = {self.current_balance:.2f}') def calculate_net_wealth(self, price): return self.current_balance + self.units * price def print_net_wealth(self, bar): ''' 打印给定的bar所对应的净资产(现金+头寸) ''' date, price = self.get_date_price(bar) net_wealth = self.calculate_net_wealth(price) print(f'{date} | net wealth = {net_wealth:.2f}') def place_buy_order(self, bar, amount=None, units=None): ''' 对于给定的bar和给定的amount或给定的units数量下一个买入单 ''' date, price = self.get_date_price(bar) if units is None: units = int(amount / price) # units = amount / price 图灵社区会员 cxc_3612(17665373813) 专享 尊重版权280 | 第 11 章 self.current_balance -= (1 + self.ptc) * \ units * price + self.ftc self.units += units self.trades += 1 if self.verbose: print(f'{date} | buy {units} units for {price:.4f}') self.print_balance(bar) def place_sell_order(self, bar, amount=None, units=None): ''' 对于给定的bar和给定的amount或给定的units数量下一个卖出单 ''' date, price = self.get_date_price(bar) if units is None: units = int(amount / price) # units = amount / price self.current_balance += (1 - self.ptc) * \ units * price - self.ftc self.units -= units self.trades += 1 if self.verbose: print(f'{date} | sell {units} units for {price:.4f}') self.print_balance(bar) def close_out(self, bar): ''' 在给定的bar上关闭所有未平仓头寸 ''' date, price = self.get_date_price(bar) print(50 * '=') print(f'{date} | *** CLOSING OUT ***') if self.units < 0: self.place_buy_order(bar, units=-self.units) else: self.place_sell_order(bar, units=self.units) if not self.verbose: print(f'{date} | current balance = {self.current_balance:.2f}') perf = (self.current_balance / self.initial_amount - 1) * 100 print(f'{date} | net performance [%] = {perf:.4f}') print(f'{date} | number of trades [#] = {self.trades}') print(50 * '=') ➊ 相关 Finance 环境。 ➋ 相关的 DNN 模型(来自交易机器人)。 ➌ 初始 / 当前余额。 ➍ 成比例交易成本。 ➎ 固定交易成本。 ➏ 打印是否冗长。 ➐ 交易金融工具的初始单位数。 ➑ 执行的初始交易数量。风险管理 | 281 ➒ 给定 bar 下的相关日期。 ➓ 给定 bar 下的金融工具相关价格。 给定 bar 下的日期和当前余额的输出。 根据当前余额和工具头寸计算净资产。 给定 bar 下的日期和净资产输出。 在给定交易金额的情况下进行交易的单位数。 交易和相关成本对当前余额的影响。 调整持有单位数量。 调整交易数量。 了解空头头寸…… ……或是多头头寸。 给定初始金额和最终流动余额的净收益。 11.7.4 回测类 以下是 Python 模块,其中 TBBacktesterRM 类用于基于事件的回测,该回测包括风控措施 (止损单、跟踪止损单、止盈单)。 # # 基于事件的回测 # --交易机器人回测(包含风险管理) # # (c) Dr. Yves J. Hilpisch # import numpy as np import pandas as pd import backtestingrm as btr class TBBacktesterRM(btr.BacktestingBaseRM): def _reshape(self, state): ''' 用来对状态对象进行重塑的辅助函数 ''' return np.reshape(state, [1, self.env.lags, self.env.n_features]) def backtest_strategy(self, sl=None, tsl=None, tp=None, wait=5, guarantee=False): ''' 基于事件的交易机器人性能回测,包括止损、跟踪止损和止盈 ''' self.units = 0 self.position = 0 self.trades = 0 self.sl = sl self.tsl = tsl 图灵社区会员 cxc_3612(17665373813) 专享 尊重版权282 | 第 11 章 self.tp = tp self.wait = 0 self.current_balance = self.initial_amount self.net_wealths = list() for bar in range(self.env.lags, len(self.env.data)): self.wait = max(0, self.wait - 1) date, price = self.get_date_price(bar) if self.trades == 0: print(50 * '=') print(f'{date} | *** START BACKTEST ***') self.print_balance(bar) print(50 * '=') # 止损单 if sl is not None and self.position != 0: rc = (price - self.entry_price) / self.entry_price if self.position == 1 and rc < -self.sl: print(50 * '-') if guarantee: price = self.entry_price * (1 - self.sl) print(f'*** STOP LOSS (LONG | {-self.sl:.4f}) ***') else: print(f'*** STOP LOSS (LONG | {rc:.4f}) ***') self.place_sell_order(bar, units=self.units, gprice=price) self.wait = wait self.position = 0 elif self.position == -1 and rc > self.sl: print(50 * '-') if guarantee: price = self.entry_price * (1 + self.sl) print(f'*** STOP LOSS (SHORT | -{self.sl:.4f}) ***') else: print(f'*** STOP LOSS (SHORT | -{rc:.4f}) ***') self.place_buy_order(bar, units=-self.units, gprice=price) self.wait = wait self.position = 0 # 跟踪止损单 if tsl is not None and self.position != 0: self.max_price = max(self.max_price, price) self.min_price = min(self.min_price, price) rc_1 = (price - self.max_price) / self.entry_price rc_2 = (self.min_price - price) / self.entry_price if self.position == 1 and rc_1 < -self.tsl: print(50 * '-') print(f'*** TRAILING SL (LONG | {rc_1:.4f}) ***') self.place_sell_order(bar, units=self.units) self.wait = wait self.position = 0 elif self.position == -1 and rc_2 < -self.tsl: print(50 * '-') print(f'*** TRAILING SL (SHORT | {rc_2:.4f}) ***') self.place_buy_order(bar, units=-self.units) self.wait = wait self.position = 0风险管理 | 283 # 止盈单 if tp is not None and self.position != 0: rc = (price - self.entry_price) / self.entry_price if self.position == 1 and rc > self.tp: print(50 * '-') if guarantee: price = self.entry_price * (1 + self.tp) print(f'*** TAKE PROFIT (LONG | {self.tp:.4f}) ***') else: print(f'*** TAKE PROFIT (LONG | {rc:.4f}) ***') self.place_sell_order(bar, units=self.units, gprice=price) self.wait = wait self.position = 0 elif self.position == -1 and rc < -self.tp: print(50 * '-') if guarantee: price = self.entry_price * (1 - self.tp) print(f'*** TAKE PROFIT (SHORT | {self.tp:.4f}) ***') else: print(f'*** TAKE PROFIT (SHORT | {-rc:.4f}) ***') self.place_buy_order(bar, units=-self.units, gprice=price) self.wait = wait self.position = 0 state = self.env.get_state(bar) action = np.argmax(self.model.predict( self._reshape(state.values))[0, 0]) position = 1 if action == 1 else -1 if self.position in [0, -1] and position == 1 and self.wait == 0: if self.verbose: print(50 * '-') print(f'{date} | *** GOING LONG ***') if self.position == -1: self.place_buy_order(bar - 1, units=-self.units) self.place_buy_order(bar - 1, amount=self.current_balance) if self.verbose: self.print_net_wealth(bar) self.position = 1 elif self.position in [0, 1] and position == -1 and self.wait == 0: if self.verbose: print(50 * '-') print(f'{date} | *** GOING SHORT ***') if self.position == 1: self.place_sell_order(bar - 1, units=self.units) self.place_sell_order(bar - 1, amount=self.current_balance) if self.verbose: self.print_net_wealth(bar) self.position = -1 self.net_wealths.append((date, self.calculate_net_wealth(price))) self.net_wealths = pd.DataFrame(self.net_wealths, columns=['date', 'net_wealth']) self.net_wealths.set_index('date', inplace=True) self.net_wealths.index = pd.DatetimeIndex(self.net_wealths.index) self.close_out(bar)

  • 5
    点赞
  • 8
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值