算法交易中的神经符号化系统:结合符号化规则和神经网络
神经符号化系统概述:规则与神经网络相结合的原理
想象一下,您正尝试向计算机解释如何在证券交易所进行交易。一方面,我们有经典的规则和形态 — “头与肩”、“双底”、以及任何交易者熟悉的数百种其它形态。我们当中的许多人都曾以 MQL5 编写过 EA,试图遵照这些形态编码。但市场是一个鲜活的有机体,它持续变化,严格的规则往往会失败。
另一方面,还有神经网络 — 时尚、强力,但有时它们的决策完全不透明。将历史数据投喂给 LSTM 网络,它会以相当的准确性做出预测。但这些决定背后的原因往往仍是个谜。在交易中,每一个错误步骤的代价都是真金白银。
我记得几年前,我在交易算法中常与这种困境相搏。经典形态会产生误报,神经网络有时会在没有任何逻辑的情况下产生令人难以置信的预测。之后它点醒了我:如果我们将这两种方式结合会怎样?如果我们使用明确的规则作为系统框架,将神经网络作为一种参考市场现状的自适应机制,会怎样?
这就是神经符号化系统的算法交易思路如何诞生的。将其想象成一位经验丰富的交易员,他了解所有经典形态和规则,但也知道如何适应市场,能细致考虑到细微差别和关系。这样的系统具备清晰规则的“骨架”、和神经网络形式的“肌肉”,增加了灵活性和适应性。
在本文中,我将解释我的团队如何利用 Python 开发这样的系统,并展示如何将经典形态分析与现代机器学习方法相结合。我们将步步贯穿架构,从基本组件、到复杂的决策机制,当然,我将分享真实代码和测试结果。
准备好进入经典交易规则与神经网络相遇的世界了吗?那我们出发!
交易中的符号化规则:形态及其统计
我们从简单的事情开始:什么是市场形态?在经典技术分析中,这是图表上的一个特定图例,例如“双底”或“旗形”。但当我们说到程序化交易系统时,我们需要更抽象地思考。在我们的代码中,形态是一个价格变动序列,以二进制形式编码:1 表示上涨,0 表示下跌。
您或许会说,这看似很初级?并非如此。这种表示给了我们一个强力分析工具。我们取序列 [1, 1, 0, 1, 0] 为例 — 这不仅仅是一组数字,而是一个编码的小趋势。在 Python 中,我们能够用简单但有效的代码来搜索这样的形态:
pattern = tuple(np.where(data['close'].diff() > 0, 1, 0))
但真正的魔力始于我们开始分析统计数据。对于每种形态,我们能计算三个关键参数:
- 频率 — 该形态在历史中出现的次数
- 胜率 — 遵照一个形态,价格沿预测方向移动的次数多少
- 可靠性 — 这是一个复杂的指标,同时参考频率和胜率
此为我实践中的一个真实例子:EURUSD H4 上的形态 [1, 1, 1, 0, 0],显示胜率为 68%,每年发生频率超过 200 次。听起来很诱人,对吧?但此处重要的是不要陷入过度优化的陷阱。
这就是我们添加动态可靠性过滤器的原因:
reliability = frequency * winrate * (1 - abs(0.5 - winrate))
这个方程式的简洁令人惊叹。它不仅考虑到频率和胜率,但也高效惩罚了可疑形态,这往往被证明是一种统计异常。
形态的长度是另一回事。短期形态(3-4 柱线)很常见,但会产生很多的噪音。长期的(20-25 柱线)更可靠,但罕见。中庸之道通常在 5-8 柱线范围。虽然,我承认,对于某些金融产品,我曾在 12-柱线形态上看到出色的结果。
重点是预测横向范围。在我们的系统中,我们取 forecast_horizon 参数,据其决定我们取之前多少根柱线来尝试预测走势。经验上,我们得到 6 这个数值 — 它在预测准确性和交易机会之间提供了最优平衡。
但当我们开始分析不同市场条件下形态时,最有趣的事情发生。相同的形态在不同的波动性、或一天里不同时间的行为可能完全不同。这就是为什么简单的统计只是第一步。这就是神经网络发挥作用的地方,但我们将在下一节中讨论这一点。
用于市场数据分析的神经网络架构
现在我们来看看我们系统的“大脑” — 神经网络。经过广泛的实验,我们确定了一种混合架构,其结合了处理时间序列的 LSTM 层,和处理形态统计特征的全连接层。
为什么是 LSTM?关键点在于市场数据不仅仅是一组数字,而是一个序列,其中每个数值都与前一个相关。LSTM 网络在捕获这种长期依赖关系方面表现出色。以下是我们的网络基本结构:
model = tf.keras.Sequential([ tf.keras.layers.LSTM(256, input_shape=input_shape, return_sequences=True), tf.keras.layers.Dropout(0.4), tf.keras.layers.LSTM(128), tf.keras.layers.Dropout(0.3), tf.keras.layers.Dense(64, activation='relu'), tf.keras.layers.Dense(1, activation='sigmoid') ])
注意 Dropout 层 — 这是我们对抗过度拟合的保护。在系统的早期版本中,我们并未使用它们,网络在历史数据上完美运行,但在真实市场上一败涂地。Dropout 在训练期间随机切断一些神经元,迫使网络搜索更稳健的形态。
重点是输入数据的维度。input_shape 参数由三个关键因素判定:
- 分析窗口大小(在我们的例子中是 10 个时间步)
- 基本特征数量(价格、交易量、技术指标)
- 从形态中提取的特征数
结果是一个 (batch_size, 10, features) 维度的张量,其中 “features” 是所有特征的总数。这正是第一个 LSTM 层期望的数据格式。
注意第一个 LSTM 层中的 return_sequences=True 参数。这意味着该层返回每个时间步的输出序列,而不仅只是最后一步。这令第二个 LSTM 层能够获得有关时间动态的更详细信息。但第二个 LSTM 只产生最终状态 — 它的输出进入全连接层。
全连接层(Dense)充当“解释器” — 它们将 LSTM 发现的复杂形态转化为具体的解决方案。配以 ReLU 激活的第一个 Dense 层处理非线性依赖关系,而配以 sigmoid 激活的最后一层产生价格走势上涨的概率。
模型编译过程值得特别注意:
model.compile( optimizer='adam', loss='binary_crossentropy', metrics=['accuracy', tf.keras.metrics.Precision(), tf.keras.metrics.Recall()] )
我们使用 Adam 优化器,它已被证明对非平稳数据(例如市场价格)有效。二元交叉熵作为损失函数,这对于我们的二元分类问题(预测价格变动方向)是理想选择。一组量值不仅有助于跟踪误报和漏报各项预测的准确性、还有预测的品质。
在开发期间,我们验证了不同的网络配置。我们尝试添加卷积层(CNN)来识别局部形态,并尝试了注意力机制,但最终得出的结论是,架构的简单性和透明度更为重要。网络越复杂,就越难以解释其决策,在交易中,理解系统运行背后的逻辑至关重要。
形态集成到神经网络中:输入数据丰富
现在来到最有趣的部分:我们如何将经典形态与神经网络“穿插”。这不仅仅是特征的级联,而是初步数据处理和分析的整套系统。
我们从一组基本的输入数据开始。对于每个时间点,我们形成一个多维特征向量,包括:
base_features = [ 'close', # Close price 'volume', # Volume 'rsi', # Relative Strength Index 'macd', # MACD 'bb_upper', 'bb_lower' # Bollinger Bands borders ]
然而,这仅仅是开始。主要创新是增加了形态统计。对于每种形态,我们计算三个关键指标:
pattern_stats = {
'winrate': np.mean(outcomes), # Percentage of successful triggers
'frequency': len(outcomes), # Occurrence frequency
'reliability': len(outcomes) * np.mean(outcomes) * (1 - abs(0.5 - np.mean(outcomes))) # Reliability
}
应特别注意最后一个量值 — 可靠性。这是我们的专有开发,不仅考虑了频率和胜率,还有统计数据的“可疑性”。如果胜率太接近 100%、或波动太大,可靠性指标就会降低。
将这些数据集成到神经网络中需要特别小心。
def prepare_data(df): # We normalize the basic features using MinMaxScaler X_base = self.scaler.fit_transform(df[base_features].values) # For pattern statistics we use special normalization pattern_features = self.pattern_analyzer.extract_pattern_features( df, lookback=len(df) ) return np.column_stack((X_base, pattern_features))
解决形态尺寸不同的问题:
def extract_pattern_features(self, data, lookback=100): features_per_length = 5 # fixed number of features per pattern total_features = len(self.pattern_lengths) * features_per_length features = np.zeros((len(data) - lookback, total_features)) # ... filling the feature array
每个形态,无论其长度如何,都会转换为固定维度的向量。这解决了有效形态数量变化的问题,并允许神经网络处理恒定维度的输入。
参考市场背景是另一回事。我们添加了表征市场当前状况的特殊功能:
market_features = {
'volatility': calculate_atr(data), # Volatility via ATR
'trend_strength': calculate_adx(data), # Trend strength via ADX
'market_phase': identify_market_phase(data) # Market phase
}
这有助于系统适应不同的市场条件。例如,在高波动期间,我们会自动提升对形态可靠性的需求。
重点是处理缺失的数据。在实际交易中,这是一个常见问题,尤其是在处理多个时间帧时。我们结合多种方法来解决:
# Fill in the blanks, taking into account the specifics of each feature df['close'] = df['close'].fillna(method='ffill') # for prices df['volume'] = df['volume'].fillna(df['volume'].rolling(24).mean()) # for volumes pattern_features = np.nan_to_num(pattern_features, nan=-1) # for pattern features
如是结果,神经网络收到完整且一致的数据集,其中经典技术形态有机地补充了基本市场指标。这为系统提供了独有优势:它能依赖经过时间考验的形态,以及训练期间发现的复杂关系。
决策系统:从分析到信号
我们来谈谈系统如何实际制定决策。暂时忘记神经网络和形态 — 归根结底,我们需要做出明确的决定:是否入场。如果我们入场,那么我们需要知道交易量。
我们的基本逻辑很简单:我们取两个数据流 — 来自神经网络的预测和形态统计。神经网络为我们给出了上行/下行走势的概率,这些形态确认或反驳了这一预测。但如常,魔鬼就在细节之中。
以下是幕后发生的事情:
def get_trading_decision(self, market_data): # Get a forecast from the neural network prediction = self.model.predict(market_data) # Extract active patterns patterns = self.pattern_analyzer.get_active_patterns(market_data) # Basic check of market conditions if not self._market_conditions_ok(): return None # Do not trade if something is wrong # Check the consistency of signals if not self._signals_aligned(prediction, patterns): return None # No consensus - no deal # Calculate the signal confidence confidence = self._calculate_confidence(prediction, patterns) # Determine the position size size = self._get_position_size(confidence) return TradingSignal( direction='BUY' if prediction > 0.5 else 'SELL', size=size, confidence=confidence, patterns=patterns )
我们首先检查基本市场条件。并非尖端科学,只是常识:
def _market_conditions_ok(self): # Check the time if not self.is_trading_session(): return False # Look at the spread if self.current_spread > self.MAX_ALLOWED_SPREAD: return False # Check volatility if self.current_atr > self.volatility_threshold: return False return True
接下来是信号一致性检查。此处重点是,我们不要求所有信号都完美一致。主要指标不相互矛盾就足够了:
def _signals_aligned(self, ml_prediction, pattern_signals): # Define the basic direction ml_direction = ml_prediction > 0.5 # Count how many patterns confirm it confirming_patterns = sum(1 for p in pattern_signals if p.predicted_direction == ml_direction) # At least 60% of patterns need to be confirmed return confirming_patterns / len(pattern_signals) >= 0.6
最艰难的部分是计算信号置信度。经过对各种方式的大量实验和分析,我们得到一个组合指标,既参考了神经网络预测的统计可靠性,以及检测到形态的历史表现:
def _calculate_confidence(self, prediction, patterns): # Baseline confidence from ML model base_confidence = abs(prediction - 0.5) * 2 # Consider confirming patterns pattern_confidence = self._get_pattern_confidence(patterns) # Weighted average with empirically selected ratios return (base_confidence * 0.7 + pattern_confidence * 0.3)
这种决策架构展示了混合方式的效率,其中经典的技术分析方法有机地补充了机器学习的能力。系统的每个组件都对最终决策做出贡献,而多级检查系统则确保了必要程度的可靠性,以及对各种市场条件的弹性。
结束语
将经典形态与神经网络分析相结合,产生了一个定性上的新结果:神经网络捕捉微妙的市场关系,而经过时间考验的形态则提供了交易决策的基本结构。在我们的测试中,该方式比纯粹的技术分析、及单独使用机器学习,展现出始终如一的更佳结果。
一个重要的发现是认识到简单性和可解释性至关重要。我们特意避免了更复杂的架构,转而采用透明、且易于理解的系统。这不仅能更好地全面掌控交易决策,且还有根据市场条件的变化快速进行调整的能力。在许多人追求复杂的世界中,简单性已被证明是我们的竞争优势。
我希望我们的经验对那些也在探索经典交易和人工智能交汇可能性边界的人有用。毕竟,正是在这样的跨学科领域,往往诞生出最有趣和最实用的解决方案。继续验证,但请记住,交易中没有灵丹妙药。仅有一条持续发展和改进工具之路。
本文由MetaQuotes Ltd译自俄文
原文地址: https://www.mql5.com/ru/articles/16894
注意: MetaQuotes Ltd.将保留所有关于这些材料的权利。全部或部分复制或者转载这些材料将被禁止。
本文由网站的一位用户撰写,反映了他们的个人观点。MetaQuotes Ltd 不对所提供信息的准确性负责,也不对因使用所述解决方案、策略或建议而产生的任何后果负责。
在 MQL5 中构建自优化智能交易系统(第六部分):防止爆仓
价格行为分析工具包开发(第十四部分):抛物线转向与反转工具
MQL5交易策略自动化(第八部分):构建基于蝴蝶谐波形态的智能交易系统(EA)
发表文章《算法交易中的神经符号系统:符号规则与神经网络的结合》:
作者:叶夫根尼-科什坚科