English Deutsch
preview
知っておくべきMQL5ウィザードのテクニック(第76回): Awesome Oscillatorのパターンとエンベロープチャネルを教師あり学習で利用する

知っておくべきMQL5ウィザードのテクニック(第76回): Awesome Oscillatorのパターンとエンベロープチャネルを教師あり学習で利用する

MetaTrader 5統合 |
86 0
Stephen Njuki
Stephen Njuki

はじめに

前回の記事では、オーサムオシレータ(AO: Awesome Oscillator)とエンベロープチャネル(Envelopes Channel)のインディケーターの組み合わせを紹介しました。そして、そのペアのテスト結果では、10パターンのうち7~8パターンが2年間のテスト期間でフォワードテストに成功しました。私たちは通常、インディケーターのペアを紹介した後、その信号のパフォーマンスに機械学習がどのような影響を与えるかを探ります。今回の記事も例外ではなく、Pattern_4、Pattern_8、Pattern_9が教師あり学習ネットワークによるフィルタで補強された場合に、どのように影響を受けるかを検証します。ネットワークでは、ドット積カーネルとクロスタイムアテンションを用いてカーネルとチャネルのサイズを決定するCNNを使用しています。


ドット積カーネルとクロスタイムアテンション

このカーネルはアテンション機構の一種で、通常は異なる時間ステップや層からの特徴量として与えられた2つのシーケンスに対して、それぞれのペア間でドット積を用いて注意スコアを計算します。このアプローチにより、時間をまたいだ関係性や、過去または未来の特徴が現在の特徴にどのように関連しているかを浮き彫りにすることができます。具体的には、次の2つのシーケンスがある場合を考えます。

X=[x1​,x2​,...,xT​]

Y=[y1,y2,...,yS]

各ペア(xi,yj)に対して、ドット積スコアを次のように計算します。 

Scorei,j = xi⋅yj

あるいは、j方向やi方向に対してソフトマックスを適用してアテンション重みを算出し、その重みを使って時間をまたいだ特徴量のブレンドや再重み付けをおこなうことも可能です。この方法はパラメータ効率が高く、プロジェクション以外の追加学習パラメータを必要としません。また、可変長シーケンス、不規則サンプリング、モーダル間や時間を跨ぐ相互作用にも対応できる柔軟性があります。さらに、局所的な畳み込み領域だけでなく、関連性の高い時間的瞬間に注目できるため強力です。応用例としては、遅延が重要な時系列予測、時間軸でフレームが関連するビデオ解析、音声処理、そして時間的依存関係が単純でないあらゆる領域が挙げられます。

では、なぜ私たちはこのカーネルをCNNのカーネルチャネルサイズの設計指針として選択したのでしょうか。まずカーネルについてですが、CNNのカーネルは空間的および時間的特性を持ちます。アテンションカーネルはどの時間ステップが重要であるかを示してくれます。小さく集中したカーネルでは目立たないかもしれませんが、カーネルが広がるにつれてより多くの文脈を捉えることが可能です。CNNは本質的に局所的であるため、アテンション機構はデータシーケンスの時間的動態に応じてカーネルサイズを調整し、効果的受容野を拡大または集中させることができます。

次にチャネルについてですが、アテンションカーネルはどの特徴量チャネルや次元が「最も注目されているか」を特定できます。これはチャネルの削減(プルーニング)の指針になります。アテンションが特定のチャネルを一貫して無視する場合、それらは不要な重みとなり、結果的に全体のチャネルサイズを縮小できます。また、これは拡張の指標でもあります。アテンションが拡散して明確な焦点がない場合は、より豊かな表現を得るためにチャネルを増やす必要があります。私たちのアプローチは、これまでの記事で取り上げてきたCNNの拡張手法と同様に、固定的なカーネルやチャネルサイズではなく、動的な設計を採用するものです。学習および推論の過程においてアテンションパターンを利用し、CNNを再構成することで、モデルの適応性と性能を向上させることを目的としています。その目標は、非定常な金融市場データに対して、より効率的で効果的なモデルを実現することです。

このようにモデルの適応性と性能の向上を目指した動的アプローチである一方で、いくつかの欠点も存在します。まず第一に、入力データのシーケンス長に対して計算量がO(N^2)となる点です。一方で、従来の固定CNNは通常O(N)です。長いシーケンスの場合、この計算コストは実用上の制約になります。次に、解釈性に関するパラドックスが挙げられます。アテンションスコア自体は「わかりやすい」指標ですが、それを動的なカーネルおよびチャネル選択と統合することで、モデルの構造が複雑化し、外部の第三者の状況に適用する際に意味のある情報を抽出し、解釈することが難しくなります。

さらに、この手法が常に優れているとは限りません。いくつかのテストでは、画像のエッジ検出のような純粋に局所的なタスクにおいて、従来のCNNの方がアテンションを多く用いるモデルよりも高い性能を示す場合があることが確認されています。また、アテンションカーネルはクロスタイム依存関係を有効に学習するために、従来のCNNよりもはるかに多くのデータを学習および最適化の過程で必要とします。最後に、過学習のリスクもあります。アテンションがアーキテクチャの変化を導く場合、テストデータのノイズやランダム性に過度に適応してしまう可能性があります。

私たちのアプローチの代替案としては、dilation convolution(膨張畳み込み層)を用いてカーネルサイズを増やすことなく受容野を広げ、広く疎な時間的差異をカバーする方法や、dynamic/adaptive convolution(動的/適応畳み込み)を使用して入力データやアテンションシグナルの性質に応じてカーネル重みを設定する方法、あるいはdepthwise separable convolution(深さ単位分離可能畳み込み)を採用してチャネルサイズのみを変化させ計算効率を重視する方法などがあります。また、squeeze and excitationブロックやtemporal convolutional network (TCN)を使用する方法も考えられます。これらはいずれも私たちのアプローチに対して独自の工夫を加えるものであり、それぞれ利点と課題を持っています。ここでは、CNNを調整する上で実現可能な手法を示すためにこれらを挙げました。


ネットワーク

ドット積クロスタイムアテンションカーネルの概要を簡単に確認したところで、次はコードの詳細を掘り下げていきます。このネットワーク本体のコードは、クラスとして以下のように構成されています。

import torch
import torch.nn as nn
import torch.nn.functional as F

class DotProductAttentionConv1D(nn.Module):
    def __init__(self, input_length=100):
        super(DotProductAttentionConv1D, self).__init__()
        self.input_length = input_length

        # Deeper and wider design with attention
        self.kernel_sizes, self.channels = self._design_architecture()

        self.conv_layers = nn.ModuleList()
        self.attention_layers = nn.ModuleList()  # For cross-time attention
        in_channels = 1

        for i, (out_channels, kernel_size) in enumerate(zip(self.channels, self.kernel_sizes)):
            # Convolutional block
            conv_layer = nn.Sequential(
                nn.Conv1d(in_channels, out_channels, kernel_size=kernel_size, padding=kernel_size // 2),
                nn.BatchNorm1d(out_channels),
                nn.ReLU(),
                nn.Dropout(0.2))
            self.conv_layers.append(conv_layer)
            
            # Attention block (cross-time attention)
            if i % 2 == 0:  # Apply attention to every other layer
                attn_layer = nn.MultiheadAttention(embed_dim=out_channels, num_heads=4)
                self.attention_layers.append(attn_layer)
            else:
                self.attention_layers.append(None)
            
            in_channels = out_channels

        # Fully connected head (same as original)
        self.head = nn.Sequential(
            nn.AdaptiveAvgPool1d(1),
            nn.Flatten(),
            nn.Linear(in_channels, 128),
            nn.ReLU(),
            nn.Dropout(0.2),
            nn.Linear(128, 64),
            nn.ReLU(),
            nn.Linear(64, 1),
            nn.Sigmoid()
        )

    def _dot_product_kernel(self, x):
        """Dot product similarity kernel with positional encoding"""
        # x shape: (B, C, L)
        x = x.permute(0, 2, 1)  # (B, L, C)
        # Compute dot product attention
        attention_scores = torch.bmm(x, x.transpose(1, 2))  # (B, L, L)
        attention_weights = F.softmax(attention_scores / (x.size(-1) ** 0.5), dim=-1)
        return torch.bmm(attention_weights, x).permute(0, 2, 1)  # (B, C, L)

    def _design_architecture(self):
        # Simulate attention response pattern
        num_layers = 10
        kernel_sizes = [3 + (i % 4) * 2 for i in range(num_layers)]  # cyclic 3, 5, 7, 9
        channels = [32 * (i + 1) for i in range(num_layers)]  # 32, 64, ..., 320
        
        return kernel_sizes, channels

    def forward(self, x):
        x = x.unsqueeze(1)  # (B, 1, L)
        
        for conv_layer, attn_layer in zip(self.conv_layers, self.attention_layers):
            x = conv_layer(x)
            
            if attn_layer is not None:
                # Reshape for attention (MultiheadAttention expects seq_len first)
                attn_input = x.permute(2, 0, 1)  # (L, B, C)
                attn_output, _ = attn_layer(attn_input, attn_input, attn_input)
                x = attn_output.permute(1, 2, 0)  # (B, C, L)
                
                # Also apply dot product kernel
                x = self._dot_product_kernel(x)
        
        return self.head(x)

まず、このクラスはPyTorchでネットワークを構築する際の標準であるnn.Moduleから継承しています。パラメータであるinput_lengthは期待されるシーケンスの長さを指定しますが、調整をおこなえば可変長の入力シーケンスも扱うことができます。superディレクティブは基底クラスを初期化するものであり、PyTorchの内部処理が正しく機能するために欠かせない非常に重要なステップです。これにより、PyTorchの内部機構が正しく動作する仕組みになっています。ここから、アーキテクチャ設計、つまりカーネルおよびチャネルサイズの設定に進みます。

先に説明したように、本モデルでは動的アプローチを採用しています。ハードコーディングの代わりに、モデルがアルゴリズム的にカーネルサイズとチャネルサイズを決定し、各フォワードパスで処理される入力データ内のアテンションパターンに適応できるようにしています。参考までに、過去の実行やメタラーニングから得たアテンション統計を各層のカーネル/チャネル選択にリンクさせるのも有効な方法でしょう。

_design_architecture関数を実行したら、次は層構築の段階に進みます。ここでModuleList関数を用いることで、可変深度のネットワークを構築できます。これは、層を動的に積み重ねる場合に非常に重要なステップです。次に、層の積み重ねをおこなうforループに入ります。この段階では、各畳み込み層に対して個別にカーネルサイズを割り当てます。これにより、必要に応じて広いフィルタや狭いフィルタを使い分けることができます。最適な設定は後にアテンションカーネルが判断します。パディングは、カーネルサイズを2で割った値を使用して設定されており、ほとんどのカーネルで出力の長さが入力と同じになる「sameパディング」を実現します。これは時系列データにおいて時間ステップの整合性を保つために非常に重要です。最後に、バッチ正規化、ReLU活性化関数、ドロップアウトといったクラシックなディープラーニング手法を組み合わせ、学習の安定化、収束の高速化、およびモデルの正則化を実現します。

同じforループ内では、上述の畳み込みブロックに加えて「アテンションブロック」も定義されています。ここでは、Transformerの中核であるマルチヘッドアテンション層を交互に追加しています。このアプローチにより、すべての層にアテンションを適用するのではなく、計算コストを節約しつつ依存関係を捉えることができます。これによって、モデルは局所的なCNN的特徴量抽出と大域的な推論とを切り替えることができます。最後に、in-channelsパラメータにout-channelsの値を代入します。これは、各特徴マップチャネルが埋め込み次元として扱われ、CNN表現とアテンション表現が統一されるためです。

次に、クラス初期化関数の最終部分では、全結合ヘッドを定義します。ここでは、入力長に依存せずに時間次元を1に縮小するため、アダプティブプーリングを使用しています。その後、全結合層に備えてフラット化をおこないます。これらの全結合層は、非線形変換を積み重ね、最終的にシグモイド出力で終了します。出力は二値分類スコアまたは正規化スコアとして解釈できますが、最終的には価格トレンドの予測を表します。0.5未満の値は弱気、0.5を超える値は強気を意味します。ドロップアウトは全結合層段階で過学習を防ぐ役割を果たします。

このネットワークのクラス初期化部分を確認したところで、次はドット積カーネル関数に進みます。カーネルを計算する際、最初に入力をB-L-C形式に変換し、入力データベクトルシーケンス全体でのドット積演算を容易にします。その後、バッチ行列積(bmm)を実行し、各バッチにおけるすべてのペアワイズなドット積を効率的に計算します。GPUを用いることで、この処理をさらに高速化することも可能です。次にソフトマックススケーリングをおこないます。これはアテンションカーネルで一般的に使用される操作であり、スケーリングの温度は入力ベクトルサイズまたは埋め込み次元の平方根です。

最後にもう一度バッチ行列積を実行し、異なる時間点からの特徴をアテンション重みに基づいてブレンドします。さらに、出力を再びパーミュートして入力ベクトルの形状に戻し、後段処理に適した形式に整えます。この関数によってネットワークは時間を超えた情報認識を獲得し、通常の畳み込みでは到達できない特性を得ます。この関数はネットワークのフォワードパス関数内で呼び出されます。

次の特別な関数はアーキテクチャ設計ヘルパーです。これは、過去の記事でカスタムCNNを構築した際にも使用したものと同様の関数であり、今回も「アテンションに基づく」変動をシミュレートするために一連のカーネルサイズを周期的に変化させています。同時に、チャネル数を段階的に増加させています。これは、後段でより抽象的で高次元な表現を学習するというディープラーニングの典型的なアプローチです。

最後に定義される関数は、通常通りフォワードパス関数です。まず、入力ベクトルを前処理し、1D畳み込みで必要となる中間のチャネル次元を追加します。これが完了したら、各層において畳み込みを実行し、入力から局所的特徴量を抽出します。続いて条件付きアテンション処理をおこないます。これは、アテンション層が存在する場合にのみ適用されるチェックです。初期化関数で、このクラスではこれらのアテンション層を交互に割り当て、すべての層には割り当てていないことを思い出してください。

アテンション層が存在した場合、私たちの場合はもともと(batch, channel, length)の形を持っているため、PyTorchのMultiheadAttentionが期待する特定の形式(channel, batch, length)に合わせて並び替えます。アテンション出力を得た後、前述のドット積カーネル関数を呼び出して処理をおこないます。補足として、フォワードパスではTransformer (MultiheadAttention)とドット積の両方を使用していますが、代替的にどちらか一方だけを使用することも可能です。この切り替えは、どのアルゴリズムがモデルの精度を支えているかを確かめる目的でおこなえます。最終的な出力はヘッドに渡され、分類または回帰処理がおこなわれます。

セクション別コードの概要

セクション 役割 重要性

__init__ + super

基盤クラスのセットアップ

PyTorch機能に必須

self.kernel_sizes、channels

動的CNN設計

入力に適応した特徴量抽出

ModuleList層

モジュール化された拡張可能な構造

スケーラビリティと実験性の確保

畳み込みブロック

局所特徴量抽出

CNNの基本的強み(正規化など)

MultiheadAttentionブロック

時間を超えた依存関係の取得

長期的な相関を捕捉

_dot_product_kernel

明示的なアテンション計算

クロスタイムリンクの補完的信号

全結合ヘッド

集約および出力処理

分類・回帰などタスクに柔軟対応

forwardのロジック

ネットワーク内のデータフロー

各操作の適切な適用を保証

_design_architecture

カーネル/チャネルの選択方法

データまたはアテンションに基づく設計が可能


オーサムオシレーター関数

MQL5で使用されるインジケーター関数は、PythonでMetaTrader 5モジュールを使用する際にはインポートされません。したがって、既存のライブラリを利用するか、自作する必要があります。私たちはこれまで後者の方法を採用してきたため、今回もそれを踏襲し、Pythonでオーサムオシレーター(AO: Awesome Oscillator)関数を次のように実装します。

def Awesome_Oscillator(df: pd.DataFrame, short_period: int = 5, long_period: int = 34) -> pd.DataFrame:
    """
    Calculate the Bill Williams Awesome Oscillator (AO) and append it to the input DataFrame.
    
    AO = SMA(Median Price, short_period) - SMA(Median Price, long_period)

    Args:
        df (pd.DataFrame): DataFrame with 'high' and 'low' columns.
        short_period (int): Short period for SMA (default 5).
        long_period (int): Long period for SMA (default 34).

    Returns:
        pd.DataFrame: Input DataFrame with 'AO' column added.
    """
    required_cols = {'high', 'low'}
    if not required_cols.issubset(df.columns):
        raise ValueError("DataFrame must contain 'high' and 'low' columns")
    if not all(p > 0 for p in [short_period, long_period]):
        raise ValueError("Period values must be positive integers")

    result_df = df.copy()
    median_price = (result_df['high'] + result_df['low']) / 2
    short_sma = median_price.rolling(window=short_period).mean()
    long_sma = median_price.rolling(window=long_period).mean()
    result_df['AO'] = short_sma - long_sma

    return result_df

この関数でインポートするモジュールとしては、まずpandasがあります。これは表形式データや時系列データの操作に優れており、データフレームの扱いやローリングウィンドウ操作を得意としています。また、ここでは明示的には呼び出していませんが、内部では高速な数値演算の基盤となるNumPyも使用されます。関数シグネチャ(defの後にある行)は、関数を呼び出す際に必要な入力データ型を明確に示しています。この明快さは、IDEの自動補完機能と組み合わさることで、開発効率を高めます。平均化期間のデフォルト値も事前に定義されており、これはビル・ウィリアムズによるクラシックな設定に準拠しています。

シグネチャの後では、まず関数の入力データを検証することから始めます。最初に確認するのは、入力として必要なデータフレームそのものです。データフレームには「high」と「low」の2つの列が存在している必要があります。これは、後でカラムが欠落していた場合に発生する難解なバグを防ぐための健全性チェックです。さらに、入力された期間が有効な正の整数であることも確認します。ユーザーフレンドリーなエラーメッセージを伴うこのチェックにより、早い段階で異常を検出し、診断を容易にします。このように入力データを防御的に検証する姿勢は、NaN値の発生を未然に防ぐために極めて重要です。外部から渡されるデータは決して鵜呑みにしてはいけません。

次に、入力データフレームのコピーを作成します。これはオリジナルのデータを純粋な状態のまま保つための処理です。非破壊的であることは、複数のインジケーター関数を組み合わせるようなパイプライン構築において特に重要です。続いて、コピーしたデータフレームから価格の中央値を計算します。これはAOの中心的な入力であり、各バーにおける平均取引価格を反映します。終値のみを使うのではなく、高値と安値の平均を取ることで、始値と終値の急変によるノイズ(いわゆる「whipsaw」)を抑制する狙いがあります。その後、短期および長期の単純移動平均(SMA)を計算します。ここで使用するrolling().mean()関数は、スムーズなトレンドシグナルを形成する移動平均を生成します。短期と長期の比較こそがオシレーターの基本ロジックであり、速いトレンドと遅いトレンドの差(距離)によってモメンタムを測定します。

最後に、コピーしたデータフレームに新しい列「AO」を追加します。これが実際のオーサムオシレーター値です。短期モメンタムが長期モメンタムよりも高い場合はポジティブであり、強気な傾向を示します。逆に短期モメンタムが低い場合はネガティブで、弱気または上昇の勢いが衰えているサインとなります。前回の記事でも述べたように、AOはゼロラインを基準とするインジケーターであるため、このラインを跨ぐクロスオーバーは非常に重要なシグナルとなります。


エンベロープチャネル関数

Pythonにおけるエンベロープチャネルの実装は次のようになります。

def Envelope_Channels(df: pd.DataFrame, period: int = 20, deviation: float = 0.025) -> pd.DataFrame:
    """
    Calculate Envelope Channels (Upper & Lower Bands) and append to the input DataFrame.

    Envelope Channels = SMA(close, period) * (1 ± deviation)

    Args:
        df (pd.DataFrame): DataFrame with 'close' column.
        period (int): Period for SMA calculation (default 20).
        deviation (float): Deviation as a decimal (e.g., 0.025 for 2.5%, default 0.025).

    Returns:
        pd.DataFrame: Input DataFrame with 'Envelope_Upper' and 'Envelope_Lower' columns added.
    """
    required_cols = {'close'}
    if not required_cols.issubset(df.columns):
        raise ValueError("DataFrame must contain 'close' column")
    if period <= 0 or deviation < 0:
        raise ValueError("Period must be positive and deviation non-negative")

    result_df = df.copy()
    sma = result_df['close'].rolling(window=period).mean()
    result_df['Envelope_Upper'] = sma * (1 + deviation)
    result_df['Envelope_Lower'] = sma * (1 - deviation)
    result_df['Envelope_Mid'] = 0.5 * (result_df['Envelope_Upper'] + result_df['Envelope_Lower'])

    return result_df

AOの場合と同様に、この関数のシグネチャにも型ヒントが付与されており、periodとdeviationの入力には妥当なデフォルト値が設定されています。入力データフレームの検証では、close列が存在するかどうかをチェックします。これは、この関数で必要となる唯一のカラムであり、いわゆる「Garbage in, garbage out」を防ぐための基本的なガードです。また、入力されたperiodがゼロではないこと、deviationが負の値ではないことも確認します。これらの要件を満たさない場合、ValueErrorが発生し、スクリプトの実行が停止します。 

次に、入力データフレームのコピーを作成し、これを結果用データフレーム(result_df)とします。これは前述の通り、また過去の記事でも説明したように、安全な実践方法です。最初の計算は、終値の平滑化移動平均(SMA)です。エンベロープはこの基準線を基に構築されます。より滑らかなSMAはノイズを減らす効果がありますが、その分遅れが増えます。このSMAがバンド全体の重心として機能します。このベースラインを得たら、上方および下方のエンベロープバッファを計算します。これらが実際のチャネルにあたります。

価格が上側のバンドを上回る場合、それは買われすぎの状態を示す可能性があります。逆に、価格が下側のバンドを下回る場合は、売られすぎの状態を示す可能性があります。deviationは小数で指定され、デフォルト値は2.5% (0.025)です。この偏差値の選択は、取引対象となる資産に最適化されるべきであり、非常にセンシティブなパラメータである傾向があります。ボラティリティの高い資産の場合、偏差を最大5%まで広げることも可能です。最後に、データフレームに追加するもうひとつのバッファはエンベロープの中央線です。これは上側バンドと下側バンドの平均値を単純に取ることで求められます。


特徴量

本記事では、前回の記事で紹介した10種類のシグナルパターンのうち、3つをテスト対象としています。これらのシグナルパターンは、本記事では特徴量と呼んでいますが、ネットワークへの入力として利用されるため、両者は目的上ほぼ同義として扱うことができます。前回の記事、そしてこれまでの連載で扱ってきたように、各特徴量は2つのインジケーターから得られるシグナルを組み合わせたベクトルです。これは0と1で構成されたビット列であり、過去の記事では、この特徴量を単純な2要素構成から拡張し、各インジケーターごとの強気・弱気シグナルを個別にマッピングする試みもおこないました。しかしながら、そのテスト結果は芳しくなく、総合的な強気/弱気シグナルのみに焦点を当てた現在の方法の方が、はるかに良好な結果を得られています。

今回再検討するのは、Feature_4、Feature_8、Feature_9の3種類です。ここで採用する各特徴量関数の一般構造は、これまで使用してきたものから大きく逸脱するものではありません。各関数は、NumPyの2次元配列を出力します。その形状は、データフレームの行数に等しく、列数は2です。各行の[0]インデックスには、固有の強気(買い)パターンが、[1]インデックスには、固有の弱気(売り)マーカーが格納されます。本記事の形式では、1が買い/売りパターンの存在を、0がその不在を示します。各パターンは、AOとエンベロープチャネルという2つのインジケーターのシグナルを組み合わせて構成されます。複数バー間の比較をおこなう際には、各データフレームにおいてshift[n]を使用します。そのため、読み込まれる各データフレームには、十分なデータ量が必要となります。


Feature_4

復習すると、このパターンの基本ロジックは次の通りです。AOがゼロライン上で谷を形成し、かつ価格がエンベロープの下半分に位置している場合、強気シグナル(買い)をマークします。逆に、AOがゼロライン下でピークを形成し、価格がエンベロープの上半分に位置している場合は、弱気シグナル(売り)をマークします。Pythonでの実装は次のとおりです。

def feature_4(df):
    """
//+------------------------------------------------------------------+
//| Check for Pattern 4.                                             |
//+------------------------------------------------------------------+

    """
    feature = np.zeros((len(df), 2))
    
    feature[:, 0] = ((df['AO'].shift(2) > df['AO'].shift(1)) &
                     (df['AO'].shift(1) < df['AO']) &
                     (df['AO'].shift(1) > 0.0) &
                     (df['close'].shift(2) >= df['Envelope_Mid'].shift(2)) &
                     (df['close'].shift(2) <= df['Envelope_Lower'].shift(2)) &
                     (df['close'].shift(1) >= df['Envelope_Mid'].shift(1)) &
                     (df['close'].shift(1) <= df['Envelope_Lower'].shift(1)) &
                     (df['close'] >= df['Envelope_Mid']) &
                     (df['close'] <= df['Envelope_Lower'])).astype(int)
    
    feature[:, 1] = ((df['AO'].shift(2) < df['AO'].shift(1)) &
                     (df['AO'].shift(1) > df['AO']) &
                     (df['AO'].shift(1) < 0.0) &
                     (df['close'].shift(2) <= df['Envelope_Mid'].shift(2)) &
                     (df['close'].shift(2) >= df['Envelope_Upper'].shift(2)) &
                     (df['close'].shift(1) <= df['Envelope_Mid'].shift(1)) &
                     (df['close'].shift(1) >= df['Envelope_Upper'].shift(1)) &
                     (df['close'] <= df['Envelope_Mid']) &
                     (df['close'] >= df['Envelope_Upper'])).astype(int)
    
    
    feature[0, :] = 0
    feature[1, :] = 0
    
    return feature

まず、上記のコードでは、出力配列(feature)をゼロで初期化するところから始めています。これは、初期状態ではどのシグナルも存在しないことを意味し、同時に列数を2列に固定します。次に、1列目のインデックス(feature[:, 0])に対して、強気シグナルの条件がすべて満たされているかどうかを確認します。AOとエンベロープの両方の条件が満たされて初めて、その列に「1」が割り当てられます。AO側の条件は、「V字型」を形成することです。これはモメンタムが一時的に減速し、その後再び上昇することを意味し、強気反転の兆候とみなされます。また、このV字型がゼロラインの上側、すなわち強気領域内で形成される必要があります。エンベロープ側の条件は、価格がエンベロープの中央線付近、あるいはその下側に位置していることです。ただし、下限バンドを明確に割り込んでいない、つまり「売られすぎ」ではない状態が前提です。これは一時的な押し目またはトレンド再開の予兆として解釈できます。

いずれかの列に「1」が割り当てられるということは、その対の列(もう一方の列)は必ず「0」となることを意味します。強気・弱気のシグナルは鏡像関係にあるためです。弱気ロジックの条件は上記の反転であり、AOがゼロライン下でピークを形成し、価格がエンベロープの上半分に位置している場合に「1」が割り当てられます。最後に、feature[0, :]とfeature[1, :]をゼロに設定しています。これは、シフト操作によってインデックス2つ分の比較をおこなっているため、シリーズの冒頭で誤ってシグナルが出るのを防ぐためです。過去の記事でも述べたように、これはNaN値に起因する誤検出を防止する重要なステップです。

本記事では、このFeature_4を、前節で説明したCNNフィルタと組み合わせて最適化し、フォワードテストをおこないました。前回の記事と同様に、銘柄はUSD/JPY、時間足は30分足、テスト期間は2023年および2024年です。Feature_4は前回の記事での結果よりもやや損失を伴うものの、フォワードウォーク性能が改善されました。そのレポートを以下に示します。

r4

c4


Feature_8

前回の記事で取り上げたように、このシグナルパターンの強気ロジックは、AOと価格の双方が一貫して上昇している状態を示します。具体的には、AOがゼロラインの上側に位置し、かつ価格がエンベロープの下側バンドから離れる方向に推移しているときに、強気シグナルが発生します。これに対し、弱気ロジックでは、AOと価格がともに下降トレンドを描き、AOがゼロラインの下側にあり、さらに価格が下側バンドを割り込んで下落している場合にシグナルが発生します。これをPythonで実装すると、次のようになります。

def feature_8(df):
    """
//+------------------------------------------------------------------+
//| Check for Pattern 8.                                             |
//+------------------------------------------------------------------+
    """
    feature = np.zeros((len(df), 2))
    
    feature[:, 0] = ((df['AO'].shift(2) > 0.0) &
                     (df['AO'].shift(1) > df['AO'].shift(2)) &
                     (df['AO'] > df['AO'].shift(1)) &
                     (df['close'].shift(2) > df['Envelope_Lower'].shift(2)) &
                     (df['close'].shift(1) > df['close'].shift(2)) &
                     (df['close'] > df['close'].shift(1))).astype(int)
    
    feature[:, 1] = ((df['AO'].shift(2) < 0.0) &
                     (df['AO'].shift(1) < df['AO'].shift(2)) &
                     (df['AO'] > df['AO'].shift(1)) &
                     (df['close'].shift(2) < df['Envelope_Lower'].shift(2)) &
                     (df['close'].shift(1) < df['close'].shift(2)) &
                     (df['close'] < df['close'].shift(1))).astype(int)
    
    
    feature[0, :] = 0
    feature[1, :] = 0
    
    return feature

したがって、上記のコードにおいて、まず最初のインデックスには、強気シグナルが存在することを確認したうえで「1」を割り当てます。これは、AOがプラス圏で明確に上昇を続けており、価格も同様に上向きの加速を示している場合です。さらに、価格がエンベロープの下限より上に位置していることを確認する必要があります。これにより、単なるデッド・キャット・バウンス(一時的な反発)ではないことを保証します。次に、2番目のインデックスには、AOがマイナス圏からスタートし、下降を続けていたものの、直近で上向きに転じる場合に「1」を付与します。これは一時的なリトレースメントの可能性を示します。ただし同時に、価格がエンベロープの下限を下抜け、強い下降トレンドの中でさらに下落を続けている必要があります。最後に、Feature_4の場合と同様に、最初の2つのインデックスをゼロに初期化します。Feature_8のテスト結果としては、フォワードウォークで利益を上げることはできませんでした。そのレポートを以下に示します。

r8

c8


Feature_9

最後のシグナルパターンの基本的なロジックは、前回の記事で述べたように、強気シグナルの場合、AOが上方で反転しつつもゼロの上にとどまり、価格がミッドラインまで下落して反発するというものです。弱気シグナルの場合は、AOが下方で反転しつつもゼロの下にとどまり、価格がミッドラインまで上昇しますが、その後それを超えて上昇することに失敗します。これをPythonで次のように実装します。

def feature_9(df):
    """
//+------------------------------------------------------------------+
//| Check for Pattern 9.                                             |
//+------------------------------------------------------------------+
    """
    feature = np.zeros((len(df), 2))
    
    feature[:, 0] = ((df['AO'].shift(2) > df['AO'].shift(1)) &
                     (df['AO'].shift(1) > df['AO']) &
                     (df['AO'] > 0.0) &
                     (df['close'].shift(2) > df['Envelope_Mid'].shift(2)) &
                     (df['close'].shift(1) <= df['Envelope_Mid'].shift(1)) &
                     (df['close'] > df['Envelope_Mid'])).astype(int)
    
    feature[:, 1] = ((df['AO'].shift(2) < df['AO'].shift(1)) &
                     (df['AO'].shift(1) < df['AO']) &
                     (df['AO'] < 0.0) &
                     (df['close'].shift(2) < df['Envelope_Mid'].shift(2)) &
                     (df['close'].shift(1) >= df['Envelope_Mid'].shift(1)) &
                     (df['close'] < df['Envelope_Mid'])).astype(int)
    
    
    feature[0, :] = 0
    feature[1, :] = 0
    
    return feature

私たちの実装方針は、上で取り上げた2つの関数と同様のものであるため、すでに説明した主要なポイントを繰り返すことはしません。最後のパターンをテストした結果、ほぼ好ましいフォワードウォークを得ることができました。このレポートを以下に示します。

r9

c9


結論

まとめると、本記事では、AOとエンベロープチャネルのシグナルを改善するために、ドット積カーネルとクロスタイムアテンションに基づいたCNNを用いた機械学習の統合について検討しました。テストでは、以前に検証した3つのパターン(Pattern_4、Pattern_8、Pattern_9)に焦点を当て、高度なニューラルフィルタリングを追加することで予測性能を向上できるかを確認しました。

私たちの動的CNN手法は特にFeature_4およびFeature_9で有効であり、以前は不調だったフォワードウォークを、損失がより抑えられた実行結果に変えることができました。カーネルのアテンション機構により、ネットワークは時間的関連性に基づいてカーネルサイズとチャネル次元を動的に適応させることができ、市場行動におけるより深いパターンを捉えることができました。この適応性は、非定常な金融市場を扱う上で有効に機能しました。ここでの要点は、ボラティリティ、トレンドの力学、または市場レジームの変化により、固定的なモデルは一般的にパフォーマンスが低下するということです。

しかし、すべての結果が良好だったわけではありません。持続的な方向性モメンタムに大きく依存するFeature_8は、CNNによる適応性の向上があっても、収益性を達成することができませんでした。これは、モデルの改良にかかわらず、特定のシグナルには限界があることを示しており、取引戦略におけるインジケーターおよびシグナル選択の重要性を浮き彫りにしています。本記事で取り上げた教師あり学習に加えて、シグナルの改善手段として強化学習を再検討することもできます。今後の記事では、ここで取り上げた特徴に対してこれを検討することができます。

名前 説明
WZ-76.mq5 ヘッダにインクルードファイルを示すウィザード組み立てEA
SignalWZ-76.mqh MQL5ウィザードアセンブリで使用されるカスタムシグナルクラス
76-4.onnx Pattern_4シグナル用エクスポートネットワーク
76-8.onnx Pattern_8シグナル用エクスポートネットワーク
76-9.onnx Pattern_9シグナル用エクスポートネットワーク

MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/18878

添付されたファイル |
WZ-76.mq5 (7.04 KB)
SignalWZ_76.mqh (15.86 KB)
76_4.onnx (10269.44 KB)
76_8.onnx (10270.85 KB)
76_9.onnx (10270.85 KB)
MQL5での取引戦略の自動化(第24回):リスク管理とトレーリングストップを備えたロンドンセッションブレイクアウトシステム MQL5での取引戦略の自動化(第24回):リスク管理とトレーリングストップを備えたロンドンセッションブレイクアウトシステム
本記事では、ロンドン市場開場前のレンジブレイクアウトを検出し、任意の取引タイプおよびリスク設定に基づいてペンディング注文(指値・逆指値注文)を自動で発注する「ロンドンセッションブレイクアウトシステム」を開発します。トレーリングストップ、リスクリワード比率、最大ドローダウン制限、そしてリアルタイム監視と管理をおこなうためのコントロールパネルなどの機能も組み込みます。
データサイエンスとML(第46回):PythonでN-BEATSを使った株式市場予測 データサイエンスとML(第46回):PythonでN-BEATSを使った株式市場予測
N-BEATSは、時系列予測のために設計された革新的なディープラーニングモデルです。このモデルは、ARIMAやPROPHET、VARなどの従来の時系列予測モデルを超えることを目指して公開されました。本記事では、このモデルについて説明し、株式市場の予測にどのように活用できるかを紹介します。
MQL5入門(第19回):ウォルフ波動の自動検出 MQL5入門(第19回):ウォルフ波動の自動検出
本記事では、強気(上昇)および弱気(下降)のウォルフ波動パターンをプログラムで識別し、MQL5を使用して取引する方法を紹介します。ウォルフ波動構造をプログラムで検出し、それに基づいて取引の実行方法を詳しく解説します。これには、主要なスイングポイントの検出、パターンルールの検証、シグナルに基づくエキスパートアドバイザー(EA)の準備が含まれます。
古典的な戦略を再構築する(第14回):複数戦略分析 古典的な戦略を再構築する(第14回):複数戦略分析
本記事では、取引戦略のアンサンブル構築と、MT5遺伝的最適化を用いた戦略パラメータの調整について、引き続き検討していきます。本日はPythonでデータを分析し、モデルがどの戦略が優れているかをより正確に予測でき、市場リターンを直接予測するよりも高い精度を達成できることを示しました。しかし、統計モデルを用いてアプリケーションをテストしたところ、パフォーマンスは著しく低下しました。その後、遺伝的最適化が相関性の高い戦略を優先していたことが判明し、私たちは投票の重みを固定し、インジケーター設定の最適化に焦点を当てるよう方法を修正しました。