
基于交易量的神经网络分析:未来趋势的关键
在所有交易日益自动化的时代,我们有必要重温过去交易者的一些认知。其中一条声称:交易量是解开一切谜题的钥匙。的确,将技术分析和交易量分析作为特征输入到机器学习模型中,将会是很有用且非常有趣的。或许,在正确的解读下,这将为我们带来理想的结果。在本文中,我们将评估一种使用 LSTM 架构来分析交易量及基于交易量的特征的方法。
我们的系统将分析交易量异常,并预测未来的价格走势。我想指出的该系统的关键特征包括:异常交易量检测、交易量聚类,以及直接通过 Python + MetaTrader 5 的组合进行模型训练。
我们还将进行全面的回测,并将结果可视化。该模型在俄罗斯股票市场的小时图时间框架上表现出特别的效果,这一点在过去一年对俄罗斯联邦储蓄银行(Sberbank)股票历史数据的测试结果中得到了证实。在本文中,我将详细研究该系统的架构、其运行原理以及实际应用结果。
代码剖析:从数据到预测
让我们深入挖掘,尝试创建一个能够真正理解当前交易量状况的系统。让我们从简单的事情开始——我们接收和处理数据的方式。一方面,这没什么复杂的——下载数据然后进行处理……但“魔鬼”总是藏在细节之中。
数据源:深入挖掘
下面是我们的数据加载函数。
def get_mt5_data(self, symbol, timeframe, start_date, end_date): try: self.logger.info(f"MT5 data request: {symbol}, {timeframe}, {start_date} - {end_date}") rates = mt5.copy_rates_range(symbol, timeframe, start_date, end_date) df = pd.DataFrame(rates)
这看起来非常简单。我特意使用 copy_rates_range 而不是更简单的 copy_rates_from。我们这样做是为了在处理流动性不足的品种时,不会丢失零交易量的周期。
接下来,我们开始处理特征和指标。
预处理:数据准备的艺术
我们不要浪费时间在选择特征上,而是专注于几个最明显的特征。
def preprocess_data(self, df): # Basic volume indicators df['vol_ma5'] = df['real_volume'].rolling(window=5).mean() df['vol_ma20'] = df['real_volume'].rolling(window=20).mean() df['vol_ratio'] = df['real_volume'] / df['vol_ma20'] # ML indicators df['price_momentum'] = df['close'].pct_change(24) df['volume_momentum'] = df['real_volume'].pct_change(24) df['volume_volatility'] = df['real_volume'].pct_change().rolling(24).std() df['price_volume_correlation'] = df['price_change'].rolling(24).corr( df['real_volume'].pct_change() )
处理特征选择就像为管弦乐队调音。在数据的交响乐中,每个特征都有其自身的角色和独特的声音。让我们来看看我们的基础特征集。
第一个是最简单的:我们计算成交量的移动平均线。周期为5的成交量均线能捕捉最微小的波动,而周期为20的均线则反应更强劲的成交量趋势。
成交量与其均线的比值可能也很有趣。当未来出现急剧跳涨时,通常会发生强劲的价格冲击。
我们还观察了过去24个K线(周期)的价格动量和成交量动量。
还有一个更有趣的特征,叫做成交量波动率。我愿意称其为“市场情绪”指标。当成交量波动率增加时,这可能表明有来自大玩家的资金大量注入市场。
我们的模型也考虑了价格与成交量之间的相关性。最后,我们一定会实时查看所有这些信号,将我们新创建的指标可视化。
性能瓶颈
为避免系统过载,我们可以实现数据分批和并行计算。换句话说,我们将数据分割成小块,然后并行处理它们。
这个简单的技术能将数据处理速度提升数倍,并且有助于在处理海量数据时避免内存泄漏问题。
在文章的下一部分,我将谈论最有趣的部分——系统如何检测异常成交量,以及之后会发生什么。
寻找“黑天鹅”:如何识别异常成交量?
我们都听说过什么是异常成交量,以及如何在图表上看到它们。也许,任何有经验的交易员都能发现它们。但我们如何将这种经验嵌入到代码中呢?如何将寻找此类成交量的逻辑形式化?
寻找异常
经过一系列实验,我在这个领域的研究最终采用了“孤立森林”(Isolation Forest)方法。为什么是这种方法?嗯,像z分数或百分位数这样的经典方法可能会错过局部的、微小的异常,但重要的不是绝对值或百分比,而是那些在其余成交量中显得突出——并脱离了整体背景的成交量。
def detect_volume_anomalies(self, df): scaler = StandardScaler() volume_normalized = scaler.fit_transform(df[['real_volume']]) iso_forest = IsolationForest(contamination=0.1, random_state=42) df['is_anomaly'] = iso_forest.fit_predict(volume_normalized)
当然,最好能对这个参数进行一些调整,而更好的解决方案是使用像BGA(贝叶斯遗传算法)这样的算法来选择所有的模型设置。我将其设置为教科书中推荐的0.05,这对应于5%的异常率。但真实市场比想象的要嘈杂得多。因此,这个门槛被稍微提高了一点。将异常与价格变动一起进行可视化分组,亲眼看看它们,也会很有用(我们将在下文回到这个话题)。
聚类:寻找模式
仅凭异常点不足以进行良好的预测。我们还需要对成交量进行聚类。我们将专注于以下聚类方案:
def cluster_volumes(self, df, n_clusters=3): features = ['real_volume', 'vol_ratio', 'volatility'] X = StandardScaler().fit_transform(df[features]) kmeans = KMeans(n_clusters=n_clusters, random_state=42) df['volume_cluster'] = kmeans.fit_predict(X)
用于聚类的特征非常简单。我认为只对实际成交量本身进行聚类会很奇怪,否则我们为什么要创建自己的特征和指标呢?然而,特征的数量以及成交量指标,都有改进的空间。
选择三个聚类,是因为我习惯性地将所有成交量划分为“背景或吸筹”成交量、“启动和运行”成交量,以及“极端波动”成交量。
意外的发现
处理数据后,我们发现了几个模式和序列,例如,异常成交量之后会出现第三类成交量,然后是活跃成交量,再之后,报价才会朝某个方向变动。
这在证券交易所开盘后的最初几个小时内尤其明显。在这里,创建一个聚类及其伴随价格变动的热力图会很有用。
神经网络:如何训练机器读懂市场
由于我使用神经网络已经很久了,将神经网络应用于我们的成交量分析是合乎逻辑的。我还没有尝试过LSTM架构,但在看到该架构在其他领域的应用实例后,我终于决定尝试一下。
让我们来仔细研究一下它。
架构:少即是多
越简单越好。我想出了一个极其简单的架构:
class LSTMModel(nn.Module): def __init__(self, input_size, hidden_size=64, num_layers=2, dropout=0.2): super(LSTMModel, self).__init__() self.lstm = nn.LSTM( input_size=input_size, hidden_size=hidden_size, num_layers=num_layers, dropout=dropout, batch_first=True ) self.dropout = nn.Dropout(dropout) self.linear = nn.Linear(hidden_size, 1)
乍一看,整个架构看起来非常原始,只有两个LSTM层和一个线性层。但力量恰恰蕴藏于简单之中。因为,遗憾的是,如果我们构建一个更庞大、更深的网络来进行深度学习,结果就会是过拟合。最初,我构建了一个复杂得多的网络——包含三个LSTM层、额外的全连接层和一个复杂的Dropout结构。结果令人印象深刻……在测试数据上。但一旦网络遇到真实的长期市场,一切都崩溃了。也就是说,我们观察到了过拟合。
与过拟合的斗争
过拟合是现代神经网络最大的问题。神经网络非常擅长在测试数据区域中学习寻找关联性,但在真实的市场条件下却完全迷失了方向。以下是我尝试在所呈现的架构中具体解决这个问题的方法:
- 单个层无法处理成交量与价格之间关系的复杂性。
- 三个层可能会在实际上并不存在关联的地方找到关联。
隐藏层的大小是按标准方式选择的——64个神经元。使用更多的神经元可能会更好。将来,当我引入一个能有效对抗过拟合的解决方案时,我们就能使用拥有更多神经元的、更复杂的架构。
输入数据:特征选择的艺术
让我们来看看用于训练的输入特征:
features = [ 'vol_ratio', 'vol_ma5', 'volatility', 'volume_cluster', 'is_anomaly', 'price_momentum', 'volume_momentum', 'volume_volatility', 'price_volume_correlation' ]
我们可以对特征集进行大量的实验。我们可以添加技术指标、价格导数、成交量导数、价格和成交量的导数,任何我们喜欢的东西。但请记住,更多的特征并不总会提高预测的质量。而且,每一个看似最合乎逻辑的特征,实际上都可能只是数据中的简单噪声。
在这里,‘volume_cluster’(成交量聚类)和 ‘is_anomaly’(是否为异常)的组合看起来很有趣。单独来看,这些特征很不起眼,但它们协同作用时,就非常有趣了。当异常成交量出现在特定的聚类中时,它会对预测产生不同寻常的影响。
意外的发现
事实证明,该系统在价格走势强劲的时期最为有效。它在大多数交易者认为无法解读的时刻,也就是在横盘市场和盘整期间,也表现出色。正是在这些时刻,异常和成交量聚类分析系统看到了我们肉眼无法察觉的东西。
在下一节中,我将谈谈该系统在真实交易中的表现,并分享具体的信号示例。
从预测到交易:将信号转化为利润
任何算法交易者都知道:一个简单的预测模型是不够的。需要将其发展成一个可运作的交易策略。但我们如何在实践中应用我们的模型呢?让我们来弄清楚这一点。在文章的下一部分,您将看到的不仅仅是枯燥的理论,而是真实的实践,包括真实的模拟交易、算法的强化、对抗过拟合的改进,但就目前而言,我们还是先满足于我们研究中常规的理论部分。
交易信号剖析
在开发交易策略时,关键点之一是交易信号的生成。在我的策略中,信号是基于模型的预测生成的,这些预测反映了下一个时期的预期回报。
def backtest_prediction_strategy(self, df, lookback=24): # Generating signals based on predictions df['signal'] = 0 signal_threshold = 0.001 # Threshold 0.1% df.loc[df['predicted_return'] > signal_threshold, 'signal'] = 1 df.loc[df['predicted_return'] < -signal_threshold, 'signal'] = -1
选择信号阈值
一方面,我们可以简单地将阈值设置为0以上。在这种情况下,我们将产生大量信号,但由于点差、佣金和市场噪音,这些信号会充满干扰。这种方法可能导致大量错误信号,从而对策略效率产生负面影响。
因此,最合理的决定似乎是提高预测盈利能力的阈值至0.1%-0.2%。这使我们能够剔除大部分噪音,并减少佣金的影响,因为只有当预测到显著的价格变动时,才会生成信号。
signal_threshold = 0.001 # 阈值 0.1%
应用信号时考虑时间偏移
一旦信号生成,它们就会被应用于价格,并考虑24个周期的前向偏移。这使我们能够考虑到做出交易决策与执行该决策之间的延迟。
df['strategy_returns'] = df['signal'].shift(24) * df['price_change']
24个周期的偏移意味着,在 t 时刻生成的信号,会被应用到 t + 24时刻的价格上。这一点很重要,因为在现实中,交易决策无法被即时执行。这种方法使得对交易策略效率的评估更加现实。
计算策略盈利能力
策略盈利能力是通过偏移后的信号与价格变动的乘积来计算的:
df['strategy_returns'] = df['signal'].shift(24) * df['price_change']
如果信号等于 1,策略盈利能力将等于价格变动(price_change)。如果信号等于 -1,策略盈利能力将等于价格变动的负值(-price_change)。如果信号等于 0,策略盈利能力将为零。
因此,将信号偏移24个周期,使我们能够考虑到做出交易决策与执行之间的延迟,这使得对策略效率的评估更加现实。
黄金分割点
经过数周的测试,我最终将阈值确定为0.1%。原因如下:
- 在此阈值下,系统生成信号的频率相当高
- 大约52-63%的交易是盈利的
- 每笔交易的平均利润大约是佣金的2.5倍
最不寻常的发现是,大多数错误信号在时间上也会呈现出聚集性。如果你愿意,可以考虑增加一个这样的时间过滤器,我们将在文章的下一部分对此进行探讨。
def apply_time_filter(self, df): # We trade only during active hours trading_hours = df['time'].dt.hour df.loc[~trading_hours.between(10, 12), 'signal'] = 0
风险管理
建仓逻辑与持仓管理逻辑(即交易期间对持有头寸的管理)本身就是一个独立的话题。一方面,这里最显而易见的解决方案是使用固定的止损和止盈,但市场过于不可预测且充满动态性,以至于亏损和盈利的界限无法用常规的逻辑来描述。
我们的解决方案相当简单——使用预测的波动率来动态设置止损位:
def calculate_stop_levels(self, predicted_return, predicted_volatility): base_stop = abs(predicted_return) * 0.7 volatility_adjust = predicted_volatility * 1.5 return max(base_stop, volatility_adjust)
这种方法也需要进一步的测试。也可以应用VaR(风险价值)风险分析模型,根据这个古老但可靠有效的系统来选择止损和止盈位。
意外的发现
一个有趣的发现是,连续的信号序列可以预示非常强烈的行情变动。当市场平均波动率急剧飙升时,也会出现问题,此时我们的阈值就不再足以进行有效交易。如果你注意观察,图表上的回撤期恰恰是与高波动率相关联的……但对我们来说,这不是问题!我们将在下一节中解决并消除这个问题。
可视化与日志记录:如何避免在数据中迷失
对我们来说,同样重要的是不要忘记日志系统。总而言之,所有与打印(prints)、日志(logs)、输出(outputs)和程序注释相关的内容,在调试阶段都至关重要。通过这种方式,你可以非常快速、高效地找到代码中问题的根源。
日志系统:细节决定成败
该日志系统基于一个简单但高效的格式:
log_format = '%(asctime)s [%(levelname)s] %(message)s' date_format = '%Y-%m-%d %H:%M:%S' logger = logging.getLogger('VolumeAnalyzer') logger.setLevel(logging.DEBUG)
你可能会问,这有什么难的。这是在我经历了几次痛苦的经历后才找到的格式,当时我无法理解系统为何在特定时刻开仓。
现在,系统的每一个动作都会在日志中留下清晰的痕迹。我同样确保记录下与异常交易量相关的时刻:
self.logger.info(f"Abnormal volume detected: {volume:.2f}") self.logger.debug(f"Context: cluster {cluster}, volatility {volatility:.4f}")
我们还需要可视化。手动交易的经历给我留下了一个根深蒂固的习惯——即对所有事物进行可视化观察,就像看待最普通的图表一样审视数据。以下是我们用于可视化的代码:
def visualize_results(self, df): plt.figure(figsize=(15, 12)) # Price and signal chart plt.subplot(3, 1, 1) plt.plot(df['time'], df['close'], 'k-', label='Price', alpha=0.7) plt.scatter(df[df['signal'] == 1]['time'], df[df['signal'] == 1]['close'], marker='^', color='g', label='Buy')
我们的第一张图,是带有模型生成信号的、最常见的Sber(俄罗斯联邦储蓄银行)价格图表。我们还会通过高亮显示那些存在异常交易量的K线来补充这些信号。这有助于我们理解系统将市场读得像一本打开的书一样透彻的那些时刻。
第二张图是预测的收益率。在这里我们可以清楚地看到,在所选资产报价出现大幅波动之前,通常会出现一连串非常强烈的预测信号。因此,这就引出了一个想法,即考虑仅基于这一特定观察来创建一个系统。当然,交易数量会因此减少,但我们追求的不是数量,而是质量,不是吗?
第三张图是累计收益率图,其中回撤部分已高亮显示。
从理论到实践:成果与展望
让我们来总结一下系统的运行成果——不仅仅是枯燥的数字,这些发现能帮助所有对交易中成交量分析感兴趣的人。
首先,市场确实在通过成交额和交易量与我们“对话”。但这种语言远比想象的要复杂。在我看来,像VSA(成交量分析)这样的经典方法正迅速变得过时,无法跟上市场同样快速的发展。形态正变得越来越复杂,而交易量也形成了肉眼几乎难以察觉的非常复杂的模式。
总而言之,根据我近三年的机器学习经验,我只能简要概括:市场每年都在变得更加复杂,而作用于其上的算法(它们通过订单流在一定程度上形成了趋势和积聚)也正变得日益复杂。我们面前将是神经网络之间的战争——机器之间为争夺市场而战,看谁的机器会更高效。
在总结系统工作时,我不仅想分享数据,也想分享那些对从事体量分析工作的所有人都有用的主要发现。
在365天的SBER股票交易中,该系统展现了令人印象深刻的结果:
- 总收益率:365.0% 年化(无杠杆)
- 盈利交易占比:50.73%
但这些数字并非最重要的。更重要的是,该系统已被证明能够适应多种市场状况。它在趋势市场和横盘市场中同样表现良好,尽管信号的性质会发生明显变化。
系统在高波动性时期的表现尤其引人注目。正是在大多数交易者选择离场观望的时候,神经网络却在成交量流中找到了最清晰的形态。也许这是因为,在这样的时刻,机构参与者会留下更明显的行动“痕迹”。
这个项目教会了我什么- 交易中的机器学习并非灵丹妙药。只有在对市场有深刻理解和对特征进行精心设计的基础上,才能取得成功。
- 简单是可持续性的关键。每当我试图通过增加新层或新特征来使模型复杂化时,系统都会变得越来越脆弱。
- 体量需要在上下文中进行分析。仅凭异常交易量或聚类本身意义不大。当我们开始观察它们与其他因素的相互作用时,魔力才会显现。
接下来是什么?
系统仍在持续演进。我目前正在研究一些改进方向:
- 根据市场阶段进行自适应参数调整
- 集成实时订单流以进行更精确的分析
- 扩展至俄罗斯市场的其他交易品种
系统的源代码在附件中提供。欢迎提出改进建议。我尤其想听听那些尝试将系统适配到其他交易品种的人的经验。
结论
总而言之,我想指出,近几个月来对我来说最有价值的发现,便是将我们今天所讨论的体量分析这类经典方法,与机器学习、神经网络和大数据等新技术相结合。
事实证明,前人的经验历久弥新,依然充满活力。我们的任务,是消化这些经验,去其糟粕、取其精华,并利用最新技术,从我们这一代交易者的视角对其进行改进。当然,我们也不能落后于现代时代:量子机器学习、用于预测价格和交易量的量子算法,以及面向机器学习的多维特征,这些都将是未来的发展方向。我已经尝试在IBM的20量子比特量子超级计算机上分析市场。结果非常有趣,我一定会在未来的文章中向大家介绍。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/16062
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
This article was written by a user of the site and reflects their personal views. MetaQuotes Ltd is not responsible for the accuracy of the information presented, nor for any consequences resulting from the use of the solutions, strategies or recommendations described.


