プライスアクション分析ツールキットの開発(第62回):MQL5による適応型平行チャネル検出とブレイクアウトシステムの構築
内容
はじめに
本連載の前回の記事では、3つのスイングポイントで妥当性を確認するトレンドラインブレイクアウトのフレームワークについて解説しました。テストの結果、この手法は明確なブレイクアウト機会を効果的に検出できることが確認されました。しかし、このアプローチには1つの制限があることが明らかになりました。それは、構築されたトレンドラインの方向に発生するブレイクアウトのみを検出できるという点です。つまり、価格が反対方向へ動いた場合や、監視対象ではない側で発生した場合、そのブレイクアウト構造を検出することができませんでした。
本記事では、このアプローチを拡張し、適応型平行チャネル検出およびブレイクアウトシステムを導入します。このシステムは、スイングポイントを基にチャネルを構築し、そのチャネル内での価格変動を監視しながら、ブレイクアウトを動的に確認します。チャネルの両側を追跡することで、重要な価格変動を見逃さない設計となっています。
本システムの中心的な目的は、トレンドラインの幾何学的分析、適応的な再計算、そしてリアルタイム検証を組み合わせ、MQL5 ユーザー向けの堅牢なプライスアクション分析ツールを提供することです。また、このシステムは、将来的な機能拡張の基盤としても設計されています。例えば、ボラティリティフィルタの統合、ATRベースの調整機能、高度なブレイクアウト確認手法などを追加することで、さらに精度の高い分析環境を構築できます。
平行チャネルと構造的ブレイクアウト
平行チャネルとは、2本の等間隔なトレンドラインによって形成されるチャートパターンです。1本はスイングハイを結び、もう1本はスイングローを結ぶことで、プライスアクションを一定の構造的レンジ内に収めます。平行チャネルはテクニカル分析における基本的な概念の1つであり、市場の短期および中期的な方向性を示すと同時に、トレーダーが潜在的なブレイクアウト領域を予測するための手がかりとなります。
ブレイクアウトは、価格がチャネルの上限または下限を明確に突破した場合に発生します。特に構造的ブレイクアウトは重要であり、現在のトレンドの継続または反転を示唆することが多くあります。これらのブレイクアウトを高い信頼性で検出するには、正確なチャネル構築とリアルタイムでの確認メカニズムの両方が必要です。
トレンドラインを基にしたチャネル構築
このシステムの基盤は、構造的な意味を持つスイングハイおよびスイングローを正確に検出することです。単純にすべての小さな価格変動を対象とするのではなく、検出処理には ATR ベースのフィルタを組み込み、重要性の低い値動きを除外します。これにより、十分な価格変化を伴うスイングのみが分析対象となり、ノイズを低減しながら市場構造の整合性を維持できます。
有効なスイングポイントが特定されると、チャネル上側の境界線は連続するスイングハイを結ぶことで構築されます。このラインが、チャネル構造の方向性を決定する基準となります。続いて、スイングポイント間の関係から算出された等距離のオフセットを利用して、対応する下側の境界線が平行に配置されます。その結果、単なる任意のラインではなく、実際の市場構造を反映した幾何学的に一貫した平行チャネルが形成されます。

正確な平行投影を使用することは非常に重要です。2本のトレンドラインを個別に近似するのではなく、このシステムでは上下の境界線の傾きを一致させます。これにより、チャネルの対称性が維持され、価格を一貫した範囲内に収める構造として機能します。新しいスイングポイントが形成されるたびに、チャネルは再計算されます。この継続的な構造調整によって、システムはリアルタイムの価格変化に同期しながら、チャネルの幾何学的な妥当性を維持します。
構造的ブレイクアウトの検出
ブレイクアウトは、価格がチャネルの上限または下限を超えた状態でローソク足を確定した場合に認識されます。ただし、すべての境界突破が有効なシグナルになるわけではありません。小さな価格スパイクや流動性スイープによる誤シグナルを防ぐため、ブレイクアウトの確認には最低限の条件が設定されています。
主要な確認条件では、価格が境界線の外側で明確にローソク足を確定することを要求します。さらに、検証精度を高めるため、最近の価格変動と比較して十分な値動きの拡大が発生していることを確認するボラティリティベースの条件、ブレイク後のリテストによる確認のような追加フィルタを適用できます。リテスト確認は、ブレイクアウトの信頼性をさらに高める仕組みです。最初の境界突破後、価格は一時的に突破した水準まで戻り、そのレベルを再確認する場合があります。この際、価格がその水準でサポート・レジスタンスとして機能し、再びブレイクアウト方向へ動けば、そのシグナルは構造的な裏付けを得ます。この仕組みにより、早すぎるエントリーを減らし、勢いの弱いブレイクアウトを除外できます。

確認されたブレイクアウトイベントは、リペイントしない矢印としてチャート上に表示されます。これらのマーカーは永続的に保存されるため、過去のシグナルを後から分析・検証できます。適応的なチャネル再計算と永続的なシグナル保存を分離する設計は、分析結果の信頼性を維持する上で重要です。
適応動作とリアルタイム再計算
金融市場は常に変化しているため、あらゆる市場構造分析ツールには適応能力が求められます。そのため、このシステムでは新しい価格データが利用可能になるたびにチャネル構造を再計算します。ただし、ライブ環境での不安定な変化を防ぐため、処理は確定済みローソク足を基準に実行されます。
各再計算では、スイングポイントの有効性の再評価、 必要に応じたチャネル形状の再構築、更新された構造に基づいてブレイクアウト条件が再評価されます。以前のチャネルが依然として構造的に有効である場合、そのチャネルは維持されます。一方、新たな価格動向によって以前の構造が無効化された場合は、新しいチャネルが生成されます。また、ブレイクアウト条件も変化するチャネルに対して継続的に検証されます。これにより、早すぎるシグナルを防ぎながら、重要な市場構造の変化を見逃しません。上下両方のチャネル境界を同時に監視することで、このシステムは方向性による偏りを回避します。上昇方向と下降方向の価格変動は同等に評価され、片側だけを見る分析ではなく、プライスアクション全体を包括的に把握できます。

この適応型フレームワークにより、本システムは市場変化に対応しながらシグナルの整合性を維持する、ライブ市場構造スキャナーとして機能します。
MQL5におけるシステムアーキテクチャ
システムの概要
このセクションでは、エキスパートアドバイザー(EA)の内部構成について説明します。各コンポーネントがどのようにモジュール化され、相互に連携しながら、生の市場データを信頼性の高いブレイクアウトシグナルへと変換するのかを解説します。このアーキテクチャは、可読性、保守性、および堅牢性を重視して設計されており、高精度な市場分析と柔軟なカスタマイズを可能にします。EA は MetaTrader 5 向けに最適化された、高度にモジュール化されたイベント駆動型アーキテクチャを採用しています。各モジュールは明確な役割を持って独立して動作しながらも、相互にシームレスに連携することで、一体化されたシステムとして機能します。
システムは新しいバーが形成されるたびに処理を実行します。まず、直近のスイングポイント(市場の重要な転換点)を基に平行チャネルを再構築し、そのチャネルがあらかじめ定義されたすべての条件を満たしているかを検証します。検証では、傾きの一貫性、タッチ回数、新しさ、ボラティリティ条件のような複数の条件が評価されます。これらはすべて14期間の ATRを用いて正規化されます。この正規化により、異なる銘柄や時間足でも一貫した判定基準が維持され、市場ボラティリティの変化にも自動的に適応できます。信頼できるチャネルが検出されると、チャート上には延長可能なトレンドラインとラベルが描画され、トレーダーは現在の市場構造を直感的に把握できます。続いて、システムは直近の価格推移がブレイクアウトを示しているかどうかを判定します。チャネルラインをリアルタイムの価格へ投影し、複数の妥当性確認フィルタによってブレイクアウトの妥当性を確認するとともに、誤検出を抑えるようシグナルを管理します。
モジュール構成と役割
スイングポイント検出モジュール
このモジュールは、生の価格データを解析し、分析全体の基盤となる重要なスイングハイおよびスイングローを検出します。通常は直近 150 本程度のローソク足を対象とし、設定可能な範囲内で価格データを走査します。そして、フラクタルの考え方を利用して、周囲の価格より明確に突出した高値や安値を検出します。具体的には、指定したルックバック期間(SwingLookback)内において、あるローソク足の高値が周囲の高値を上回っているか、あるいは安値が周囲の安値を下回っているかを判定します。小さな価格変動に反応しないよう、ATRフィルタを有効にすることもできます。この場合、ATRに基づいて算出される最小スイング幅 (SwingSizeATRFactor)を超える値動きだけが、有効なスイングとして採用されます。検出されたスイングポイントは専用の配列に保存され、時系列順に並べ替えられるとともに、後続モジュールから参照しやすいよう順序番号が割り当てられます。また、スイングポイントはチャート上に矢印やラベルとして表示することもできるため、現在の市場構造を視覚的に把握しやすくなります。
//+------------------------------------------------------------------+ //| Detect significant swing highs and lows | //+------------------------------------------------------------------+ void FindSignificantSwings(const MqlRates &rates[],int totalBars, SwingPoint &highs[],int &highCount, SwingPoint &lows[],int &lowCount) { highCount = 0; lowCount = 0; double minSize = UseATRFiltering ? currentATR * SwingSizeATRFactor : MinSwingSize; for(int i = SwingLookback; i < totalBars - SwingLookback; i++) { //--- Detect Swing High bool isHigh = true; double currentHigh = rates[i].high; for(int j=1; j<=SwingLookback; j++) { if(rates[i-j].high >= currentHigh || rates[i+j].high >= currentHigh) { isHigh = false; break; } } if(isHigh) { double leftLow = MathMin(rates[i-1].low,rates[i-2].low); double rightLow = MathMin(rates[i+1].low,rates[i+2].low); double swingSize = currentHigh - MathMax(leftLow,rightLow); if(swingSize >= minSize && highCount < 50) { highs[highCount].time = rates[i].time; highs[highCount].price = currentHigh; highs[highCount].barIndex = i; highs[highCount].size = swingSize; highs[highCount].isHigh = true; highCount++; } } //--- Detect Swing Low bool isLow = true; double currentLow = rates[i].low; for(int j=1; j<=SwingLookback; j++) { if(rates[i-j].low <= currentLow || rates[i+j].low <= currentLow) { isLow = false; break; } } if(isLow) { double leftHigh = MathMax(rates[i-1].high,rates[i-2].high); double rightHigh = MathMax(rates[i+1].high,rates[i+2].high); double swingSize = MathMin(leftHigh,rightHigh) - currentLow; if(swingSize >= minSize && lowCount < 50) { lows[lowCount].time = rates[i].time; lows[lowCount].price = currentLow; lows[lowCount].barIndex = i; lows[lowCount].size = swingSize; lows[lowCount].isHigh = false; lowCount++; } } } if(DebugMode) Print("Swings found: Highs=",highCount," Lows=",lowCount); } //+------------------------------------------------------------------+
チャネル構築・スコアリングモジュール
この中核モジュールは、スイングハイとスイングローのすべての組み合わせを網羅的に探索し、最も信頼性が高く、構造的に妥当な平行チャネルを検出します。各候補チャネルは、トレンドラインの傾きの類似性やチャネル幅など、複数の基準に基づいて評価されます。また、MaxSlopeDifferenceやMinChannelWidthATRといった条件による早期除外フィルタも適用されます。各候補については、改良版 CountTouchesOnLine() 関数を使用して、上側および下側トレンドラインへのタッチ回数を集計します。この関数は、ライン上または許容誤差内に位置し、かつそのトレンドラインに対して意味のある位置に存在するスイングポイントのみを対象として判定します。
タッチ回数は、候補チャネルが直近の市場構造にどれだけ適合しているかを示す重要な評価指標です。チャネルのスコアは、上下トレンドラインへの総タッチ回数、直近のスイングポイントに対するボーナス(Recency Bonus)、最近のスイングポイントがトレンドラインとどの程度相互作用しているかを示す強度指標を組み合わせて算出されます。これらの評価に基づき、すべての最低条件を満たした候補の中から最も高いスコアを獲得したものが、最適な平行チャネルとして採用されます。このチャネルは、直近の市場構造において最も重要なサポートまたはレジスタンス領域を表すものとなります。
//+------------------------------------------------------------------+ //| Find the best parallel channel from swing points | //+------------------------------------------------------------------+ bool FindBestChannel(const SwingPoint &highs[],int highCount, const SwingPoint &lows[],int lowCount, const MqlRates &rates[],int totalBars, Channel &bestChannel) { bestChannel.score = 0.0; if(highCount < 2 || lowCount < 2) { if(DebugMode) Print("Not enough swings: highs=",highCount," lows=",lowCount); return false; } double tolerance = TouchToleranceATR * currentATR; for(int hi1=0; hi1<highCount-1; hi1++) { for(int hi2=hi1+1; hi2<highCount; hi2++) { double dt_high = (double)(highs[hi2].time - highs[hi1].time); if(dt_high <= 0) continue; double slope_high = (highs[hi2].price - highs[hi1].price) / dt_high; for(int lo1=0; lo1<lowCount-1; lo1++) { for(int lo2=lo1+1; lo2<lowCount; lo2++) { double dt_low = (double)(lows[lo2].time - lows[lo1].time); if(dt_low <= 0) continue; double slope_low = (lows[lo2].price - lows[lo1].price) / dt_low; double avgSlope = (MathAbs(slope_high) + MathAbs(slope_low)) / 2.0; double diff = MathAbs(slope_high - slope_low); double maxDiff = MaxSlopeDifference * MathMax(avgSlope,0.000001); if(diff > maxDiff) continue; double upper_at_l1 = highs[hi1].price + slope_high * (lows[lo1].time - highs[hi1].time); double upper_at_l2 = highs[hi1].price + slope_high * (lows[lo2].time - highs[hi1].time); if(upper_at_l1 <= lows[lo1].price || upper_at_l2 <= lows[lo2].price) continue; double dist1 = upper_at_l1 - lows[lo1].price; double dist2 = upper_at_l2 - lows[lo2].price; double width = (dist1 + dist2) / 2.0; if(width < MinChannelWidthATR * currentATR) continue; double lower_at_l1 = upper_at_l1 - width; double lower_at_l2 = upper_at_l2 - width; if(lower_at_l1 > lows[lo1].price + tolerance || lower_at_l2 > lows[lo2].price + tolerance) continue; int recentHigh = 0; int touchesHigh = CountTouchesOnLine(highs[hi1],highs[hi2],highs,highCount,true,tolerance,RecentTouchBars,recentHigh); SwingPoint lowerAnchor1, lowerAnchor2; lowerAnchor1.time = highs[hi1].time; lowerAnchor1.price = highs[hi1].price - width; lowerAnchor2.time = highs[hi2].time; lowerAnchor2.price = highs[hi2].price - width; int recentLow = 0; int touchesLow = CountTouchesOnLine(lowerAnchor1,lowerAnchor2,lows,lowCount,false,tolerance,RecentTouchBars,recentLow); if(touchesHigh < MinTouchPointsRequired || touchesLow < MinTouchPointsRequired) continue; if(MinRecentTouches > 0 && (recentHigh < MinRecentTouches || recentLow < MinRecentTouches)) continue; double score = (touchesHigh + touchesLow) * 25.0; double recencyBonus = (1.0 - (double)MathMin(highs[hi2].barIndex,lows[lo2].barIndex) / totalBars) * 30.0; score += recencyBonus; if(score > bestChannel.score) { bestChannel.high1 = highs[hi1]; bestChannel.high2 = highs[hi2]; bestChannel.low1 = lows[lo1]; bestChannel.low2 = lows[lo2]; bestChannel.slope = slope_high; bestChannel.width = width; bestChannel.highTouches = touchesHigh; bestChannel.lowTouches = touchesLow; bestChannel.score = score; bestChannel.type = ClassifyChannel(slope_high); bestChannel.strength = GradeStrength(touchesHigh + touchesLow); if(DebugMode) Print(StringFormat("Candidate accepted: H-touches=%d, L-touches=%d, width=%g, score=%g", touchesHigh,touchesLow,width,score)); } } } } } return (bestChannel.score > 0.0); } //+------------------------------------------------------------------+
チャネル探索では、スイングハイとスイングローの組み合わせを二重ループによって評価します。この処理は理論上は二次時間計算量となりますが、積極的な早期除外フィルタを採用しているため、実際の処理負荷は適切に抑えられています。例えば、許容される最大傾き差や ATR によって正規化された最小チャネル幅といった条件により、有望ではない候補は詳細な評価をおこなう前に除外されます。さらに、スイングポイントの検出対象を一定のルックバック期間内に限定することで、候補となる組み合わせ数が無制限に増加することを防いでいます。このような段階的な候補の絞り込みにより、市場のボラティリティが高い局面でも、チャネル探索エンジンは高い処理性能を維持できます。
可視化モジュール
最適な平行チャネルが決定されると、このモジュールがチャート上に分かりやすく描画します。描画されるのは2本の平行なトレンドラインです。上側のラインは最初のスイングハイと将来方向へ投影した点を結び、下側のラインはチャネル幅だけオフセットして配置されることで、両ラインの平行性を維持します。これらのトレンドラインは、設定可能な本数(LineExtensionBars)だけ未来方向へ延長されるため、将来のサポート・レジスタンス帯を視覚的に把握できます。トレンドラインはティックごとに動的に再生成されるため、リアルタイムの価格データと常に同期し、古い描画が残ることによるチャートの煩雑化を防ぎます。さらに、分析結果を分かりやすくするために、スイングポイントを示す十字マーカー、トレンドラインへのタッチ回数、チャネルの強さ、売買方向のバイアス(買いのみ、売りのみ、中立)を示すラベルのような補助的な表示も追加されます。これらすべての描画オブジェクトには、TL_PREFIX を接頭辞とした名前が付けられます。また、ティックごとに既存のオブジェクトを削除して再作成することで、チャート表示を常に正確かつ見やすい状態に保っています。
//+------------------------------------------------------------------+ //| Draw the parallel channel with extension | //+------------------------------------------------------------------+ void DrawChannel(const Channel &ch) { string upName = TL_PREFIX + "UPPER"; string dnName = TL_PREFIX + "LOWER"; string lblName = TL_PREFIX + "LABEL"; color upperColor = UpperLineColor; color lowerColor = LowerLineColor; //--- Compute future extension point datetime lastSwingTime = MathMax(ch.high2.time,ch.low2.time); datetime futureTime = lastSwingTime + LineExtensionBars * PeriodSeconds(_Period); double futureUpper = LinePriceAtTime(ch.high1,ch.high2,futureTime); double futureLower = futureUpper - ch.width; //--- Upper line (from first high to future point) if(ObjectFind(0,upName) < 0) ObjectCreate(0,upName,OBJ_TREND,0,ch.high1.time,ch.high1.price,futureTime,futureUpper); else { ObjectMove(0,upName,0,ch.high1.time,ch.high1.price); ObjectMove(0,upName,1,futureTime,futureUpper); } ObjectSetInteger(0,upName,OBJPROP_COLOR,upperColor); ObjectSetInteger(0,upName,OBJPROP_WIDTH,LineWidth); ObjectSetInteger(0,upName,OBJPROP_STYLE,LineStyle); ObjectSetInteger(0,upName,OBJPROP_RAY_RIGHT,false); ObjectSetInteger(0,upName,OBJPROP_BACK,false); //--- Lower line (parallel, starting at same time as upper) double lower1 = ch.high1.price - ch.width; if(ObjectFind(0,dnName) < 0) ObjectCreate(0,dnName,OBJ_TREND,0,ch.high1.time,lower1,futureTime,futureLower); else { ObjectMove(0,dnName,0,ch.high1.time,lower1); ObjectMove(0,dnName,1,futureTime,futureLower); } ObjectSetInteger(0,dnName,OBJPROP_COLOR,lowerColor); ObjectSetInteger(0,dnName,OBJPROP_WIDTH,LineWidth); ObjectSetInteger(0,dnName,OBJPROP_STYLE,LineStyle); ObjectSetInteger(0,dnName,OBJPROP_RAY_RIGHT,false); ObjectSetInteger(0,dnName,OBJPROP_BACK,false); //--- Optional low reference crosses if(ShowLowReferenceCrosses) { string cross1 = TL_PREFIX + "LOWREF1", cross2 = TL_PREFIX + "LOWREF2"; if(ObjectFind(0,cross1) < 0) ObjectCreate(0,cross1,OBJ_ARROW,0,ch.low1.time,ch.low1.price); else ObjectMove(0,cross1,0,ch.low1.time,ch.low1.price); ObjectSetInteger(0,cross1,OBJPROP_ARROWCODE,159); ObjectSetInteger(0,cross1,OBJPROP_COLOR,clrGray); ObjectSetInteger(0,cross1,OBJPROP_WIDTH,1); ObjectSetInteger(0,cross1,OBJPROP_BACK,false); if(ObjectFind(0,cross2) < 0) ObjectCreate(0,cross2,OBJ_ARROW,0,ch.low2.time,ch.low2.price); else ObjectMove(0,cross2,0,ch.low2.time,ch.low2.price); ObjectSetInteger(0,cross2,OBJPROP_ARROWCODE,159); ObjectSetInteger(0,cross2,OBJPROP_COLOR,clrGray); ObjectSetInteger(0,cross2,OBJPROP_WIDTH,1); ObjectSetInteger(0,cross2,OBJPROP_BACK,false); } //--- Label datetime labelTime = MathMax(ch.high2.time,ch.low2.time); double labelPrice = (ch.high1.price + ch.high2.price)/2.0 - ch.width/2.0; string strengthText = (ch.strength == HIGH ? "HIGH" : (ch.strength == MEDIUM ? "MEDIUM" : "LOW")); string biasText = "NEUTRAL"; if(UseChannelTypeFilter) { if(ch.type == ASCENDING) biasText = "BUY ONLY"; else if(ch.type == DESCENDING) biasText = "SELL ONLY"; } string labelText = "Touches: " + IntegerToString(ch.highTouches + ch.lowTouches) + "\nStrength: " + strengthText + "\nBias: " + biasText; if(ObjectFind(0,lblName) < 0) ObjectCreate(0,lblName,OBJ_TEXT,0,labelTime,labelPrice); else ObjectMove(0,lblName,0,labelTime,labelPrice); ObjectSetString(0,lblName,OBJPROP_TEXT,labelText); ObjectSetInteger(0,lblName,OBJPROP_COLOR,clrWhite); ObjectSetInteger(0,lblName,OBJPROP_FONTSIZE,10); ObjectSetInteger(0,lblName,OBJPROP_BACK,false); } //+------------------------------------------------------------------+
ブレイクアウトの判定は、重複したシグナルや早すぎるシグナルの発生を防ぐため、状態遷移モデルに基づいて管理されます。検出されたイベントは、それぞれ以下の状態を順番に遷移します。
- ニュートラル(Neutral):ブレイクアウトは発生していません。
- ブレイクアウト待機中(Pending Breakout) — 境界線の突破は検出されていますが、(リテスト確認を有効にしている場合は)リテストによる確認を待っている状態です。
- ブレイクアウト確認済み:すべての判定条件を満たし、シグナルが発行された状態です。
- 失効/無効(Expired / Invalidated) — ブレイクアウトが確認される前に条件を満たさなくなり、シグナルが無効になった状態です。
//+------------------------------------------------------------------+ //| Checks for breakout crossing and applies filters | //+------------------------------------------------------------------+ void CheckChannelBreakouts(const MqlRates &rates[],const Channel &ch, const SwingPoint &highs[],int highCount, const SwingPoint &lows[],int lowCount) { if(ArraySize(rates) < 3) return; datetime t1 = rates[1].time; datetime t2 = rates[2].time; double buf = BreakoutBufferPrice(); double minStrength = BreakoutMinStrengthATR * currentATR; double strongThreshold = BreakoutStrongThreshold * currentATR; int totalTouches = ch.highTouches + ch.lowTouches; double upper1 = LinePriceAtTime(ch.high1,ch.high2,t1); double upper2 = LinePriceAtTime(ch.high1,ch.high2,t2); double pUp1 = BreakoutUseClose ? rates[1].close : rates[1].high; double pUp2 = BreakoutUseClose ? rates[2].close : rates[2].high; bool crossedUp = false; if(BreakoutStrictMode) crossedUp = (pUp2 <= upper2 + buf) && (pUp1 > upper1 + buf); else crossedUp = (pUp1 > upper1 + buf); double upStrength = pUp1 - (upper1 + buf); bool strongUp = (minStrength == 0) || (upStrength >= minStrength); bool superStrongUp = (strongThreshold > 0) && (upStrength >= strongThreshold); double lower1 = upper1 - ch.width; double lower2 = upper2 - ch.width; double pLow1 = BreakoutUseClose ? rates[1].close : rates[1].low; double pLow2 = BreakoutUseClose ? rates[2].close : rates[2].low; bool crossedDown = false; if(BreakoutStrictMode) crossedDown = (pLow2 >= lower2 - buf) && (pLow1 < lower1 - buf); else crossedDown = (pLow1 < lower1 - buf); double downStrength = (lower1 - buf) - pLow1; bool strongDown = (minStrength == 0) || (downStrength >= minStrength); bool superStrongDown = (strongThreshold > 0) && (downStrength >= strongThreshold); bool isSweepUp = false, isSweepDown = false; if(UseLiquiditySweepFilter) { double high = rates[1].high, low = rates[1].low, close = rates[1].close; isSweepUp = (high > upper1 + buf) && (close <= upper1 + buf); isSweepDown = (low < lower1 - buf) && (close >= lower1 - buf); } bool volExpanded = true; if(UseVolatilityExpansion && atrBuffer[0] > 0) { double atrNow = atrBuffer[0]; double atr10 = atrBuffer[10]; if(atr10 > 0) volExpanded = (atrNow / atr10) >= MinVolatilityExpansion; } bool structBullish = true, structBearish = true; if(UseStructuralBreak) { SwingPoint lastHigh, lastLow; GetLastSwing(highs,highCount,lows,lowCount,lastHigh,lastLow); if(crossedUp && lastHigh.time > 0) structBullish = (pUp1 > lastHigh.price); if(crossedDown && lastLow.time > 0) structBearish = (pLow1 < lastLow.price); } bool allowBullish = true, allowBearish = true; if(UseChannelTypeFilter) { if(ch.type == ASCENDING) allowBearish = false; else if(ch.type == DESCENDING) allowBullish = false; } //--- Bullish breakout handling if(crossedUp && strongUp && allowBullish && !isSweepUp && volExpanded && structBullish) { bool trigger = superStrongUp; if(!trigger) { trigger = true; for(int i=1; i<=BreakoutConfirmationBars; i++) { if(i >= ArraySize(rates)) { trigger = false; break; } datetime ti = rates[i].time; double ui = LinePriceAtTime(ch.high1,ch.high2,ti); double pi = BreakoutUseClose ? rates[i].close : rates[i].high; if(!(pi > ui + buf && (pi - (ui + buf)) >= minStrength)) { trigger = false; break; } } } if(trigger) { if(UseRetestMode) { // Store pending breakout if(pendingCount >= ArraySize(pendingBreakouts)) ArrayResize(pendingBreakouts,pendingCount+10); PendingBreakout pb; pb.breakoutTime = t1; pb.price = pUp1; pb.isBullish = true; pb.linePrice = upper1; pb.totalTouches = totalTouches; pb.channelSlope = ch.slope; pb.channelWidth = ch.width; pb.high1 = ch.high1; pb.high2 = ch.high2; pendingBreakouts[pendingCount++] = pb; if(DebugMode) Print("Bullish pending retest at ",TimeToString(t1)); } else { double arrowPrice = rates[1].low - currentATR*0.15; DrawPermanentArrow(t1,arrowPrice,true,totalTouches,rates[1].low,rates[1].high); } if(AlertOnBreakout) Alert(_Symbol," ",EnumToString(_Period),": BUY breakout"); } } //--- Bearish breakout handling (symmetrical) if(crossedDown && strongDown && allowBearish && !isSweepDown && volExpanded && structBearish) { bool trigger = superStrongDown; if(!trigger) { trigger = true; for(int i=1; i<=BreakoutConfirmationBars; i++) { if(i >= ArraySize(rates)) { trigger = false; break; } datetime ti = rates[i].time; double ui = LinePriceAtTime(ch.high1,ch.high2,ti); double li = ui - ch.width; double pi = BreakoutUseClose ? rates[i].close : rates[i].low; if(!(pi < li - buf && ((li - buf) - pi) >= minStrength)) { trigger = false; break; } } } if(trigger) { if(UseRetestMode) { if(pendingCount >= ArraySize(pendingBreakouts)) ArrayResize(pendingBreakouts,pendingCount+10); PendingBreakout pb; pb.breakoutTime = t1; pb.price = pLow1; pb.isBullish = false; pb.linePrice = lower1; pb.totalTouches = totalTouches; pb.channelSlope = ch.slope; pb.channelWidth = ch.width; pb.high1 = ch.high1; pb.high2 = ch.high2; pendingBreakouts[pendingCount++] = pb; if(DebugMode) Print("Bearish pending retest at ",TimeToString(t1)); } else { double arrowPrice = rates[1].high + currentATR*0.15; DrawPermanentArrow(t1,arrowPrice,false,totalTouches,rates[1].low,rates[1].high); } if(AlertOnBreakout) Alert(_Symbol," ",EnumToString(_Period),": SELL breakout"); } } } //+------------------------------------------------------------------+
この状態遷移に基づく設計により、各ブレイクアウトは決定論的に処理され、一度だけ判定されます。その結果、シグナルの重複を防ぐとともに、通常モードおよびリテストモードのいずれにおいても、判定ロジックの透明性と一貫性が確保されます。
シグナル管理・永続化モジュール
このモジュールは、チャートの再読み込み、EA の再初期化、あるいはコードの再コンパイル後も、シグナルや各種表示を確実に保持する役割を担います。具体的には、breakoutsArrayのような配列を管理し、確認済みのブレイクアウトについて、発生時刻、価格、シグナルの強さなどの情報を保存します。また、ブレイクアウト水準を示す矢印やゾーンをチャート上に恒久的なオブジェクトとして描画します。設定でUseBreakoutZonesが有効になっている場合は、これらのゾーンを将来方向へ延長して表示することもできます。リテストモードでは、価格がブレイクアウトゾーンへ戻るかどうかを継続的に監視し、リテストが正常に完了した時点で初めてブレイクアウトを確定します。さらに、描画オブジェクトのライフサイクルも管理します。ティックごとに一時的なオブジェクトは削除・再生成する一方で、確定済みのシグナルは保持し続けるため、チャートを更新しても重要なシグナルが失われることはありません。このような設計により、チャートの再読み込みやプログラムの変更があっても、一貫した視覚情報と売買判断の基盤が維持され、トレーダーが混乱することを防ぎます。
//+------------------------------------------------------------------+ //| Store and draw confirmed signals | //+------------------------------------------------------------------+ void DrawPermanentArrow(datetime time,double price,bool isBullish,int totalTouches,double low=0,double high=0) { string arrowName = SIG_PREFIX + (isBullish ? "BUY_" : "SELL_") + IntegerToString((int)time) + "_" + IntegerToString(totalTouches); //--- Avoid duplicate arrows if(ObjectFind(0,arrowName) >= 0) return; if(!ObjectCreate(0,arrowName,OBJ_ARROW,0,time,price)) { if(DebugMode) Print("Failed to create arrow: ",GetLastError()); return; } ObjectSetInteger(0,arrowName,OBJPROP_ARROWCODE,isBullish ? 233 : 234); ObjectSetInteger(0,arrowName,OBJPROP_COLOR,isBullish ? BullArrowColor : BearArrowColor); ObjectSetInteger(0,arrowName,OBJPROP_WIDTH,ArrowSize); ObjectSetInteger(0,arrowName,OBJPROP_BACK,false); ObjectSetInteger(0,arrowName,OBJPROP_SELECTABLE,false); ObjectSetInteger(0,arrowName,OBJPROP_HIDDEN,true); string desc = "Channel Breakout " + (isBullish ? "BULLISH" : "BEARISH") + " (" + IntegerToString(totalTouches) + " touches)"; ObjectSetString(0,arrowName,OBJPROP_TEXT,desc); if(UseBreakoutZones && high > low) DrawBreakoutZone(time,low,high,isBullish,totalTouches); //--- Store in persistent array if(breakoutsCount >= ArraySize(breakoutsArray)) ArrayResize(breakoutsArray,breakoutsCount+10); breakoutsArray[breakoutsCount].time = time; breakoutsArray[breakoutsCount].price = price; breakoutsArray[breakoutsCount].isBullish = isBullish; breakoutsArray[breakoutsCount].totalTouches = totalTouches; breakoutsCount++; } //+------------------------------------------------------------------+
新しいブレイクアウトシグナルを保存または描画する前に、システムは同一の市場構造に対する同等のシグナルがすでに記録されていないかを確認します。この仕組みにより、ボラティリティの高い局面や、同一ローソク足内で複数のティックが発生した場合でも、同じブレイクアウトを示す矢印が繰り返し描画されることを防ぎます。保存される各ブレイクアウトには、発生時刻と対応する市場構造の情報が記録されます。これにより、システムは「1つのブレイクアウトイベントにつき1つのシグナル」というルールを厳密に適用できます。その結果、チャート上の表示は常に明確に保たれ、分析結果の信頼性も維持されます。
補助・ユーティリティモジュール
補助モジュールは、ATR の計算、描画オブジェクトの管理、各種数学処理など、システム全体で共通して利用される基本機能を提供します。ATRは専用ハンドル(iATR)によって管理され、新しいローソク足が確定するたびに更新されます。この値は市場ボラティリティを正規化した指標として、各種フィルタやブレイクアウトゾーンの描画など、システム全体で利用されます。LinePriceAtTime()は、指定した時刻におけるトレンドライン上の価格を計算する関数です。一方、CountTouchesOnLine() は、各チャネル境界(レール)に対して意味のあるスイングポイントの数を集計します。この関数では、トレンドライン上または許容誤差内に位置し、かつそのラインとの位置関係が有効なスイングポイントのみを対象として評価します。
また、チャネル候補の傾きを解析し、上昇チャネル、下降チャネル、水平チャネルのいずれかに分類する関数も用意されています。この分類結果は、チャネルのフィルタリングや売買方向のバイアスの判定に利用されます。さらに、タッチ回数に基づいてチャネルの強さを高、中、低の3段階に評価する機能も備えています。これにより、信頼性の低いチャネルを効率的に除外できます。システム全体には充実したデバッグ機能も組み込まれています。判定結果、計算過程、および各種フィルタの評価内容を詳細にログへ出力するため、開発者やトレーダーはパラメータの調整、問題の切り分け、さらにはシステムがどのような根拠で判断を下したのかを容易に確認できます。
//+------------------------------------------------------------------+ //| Calculate the slope between two swing points | //+------------------------------------------------------------------+ double CalculateSlope(const SwingPoint &a,const SwingPoint &b) { double deltaTime = (b.time - a.time); if(deltaTime == 0) return(0); return (b.price - a.price)/deltaTime; } //+------------------------------------------------------------------+ //+------------------------------------------------------------------+ //| Count touches on a trendline considering only swings on relevant side | //+------------------------------------------------------------------+ int CountTouchesOnLine(const SwingPoint &a,const SwingPoint &b, const SwingPoint &swings[],int swingCount, bool isUpperLine,double tolerance, int recentBars,int &recentCount) { int touches = 2; // the two anchor swings recentCount = 0; double timeDiff = (double)(b.time - a.time); if(timeDiff <= 0) return touches; double slope = (b.price - a.price) / timeDiff; for(int i=0; i<swingCount; i++) { if(swings[i].time == a.time || swings[i].time == b.time) continue; if(swings[i].time < a.time || swings[i].time > b.time) continue; double linePrice = a.price + slope * (swings[i].time - a.time); double distance = isUpperLine ? linePrice - swings[i].price : swings[i].price - linePrice; bool correctSide = (isUpperLine && distance >= -tolerance) || (!isUpperLine && distance >= -tolerance); if(correctSide && MathAbs(distance) <= tolerance) { touches++; if(swings[i].barIndex <= recentBars) recentCount++; } } return touches; } //+------------------------------------------------------------------+
分析の各段階では、SwingPoint、Channel、BreakoutInfoなどの明確に定義されたデータ構造体を使用します。この構造化された設計により、各モジュールが扱うデータの所有範囲が明確になり、役割が適切に分離されます。単に価格データを関数間で受け渡すのではなく、必要な情報をカプセル化したオブジェクトを受け渡すことで、データの整合性を維持し、ロジックの不整合が発生するリスクを低減できます。この設計は保守性の向上にも寄与しており、将来的に複数のチャネルを同時に追跡する機能や、複数銘柄への対応機能を追加する場合でも、システム全体のアーキテクチャを大幅に変更することなく拡張できます。
データフローとモジュール間の連携

データ処理は、スイングポイント検出モジュールから始まります。このモジュールは直近の価格データを解析し、市場構造を形成する重要な転換点であるスイングハイおよびスイングローを検出します。検出されたスイングポイントは、チャネル構築・スコアリングモジュールへ渡されます。このモジュールでは、スイングポイントに最も適合する統計的に有意な平行チャネルを網羅的に探索します。各候補チャネルは、トレンドラインへのタッチ回数、形成時期の新しさ(Recency)、およびチャネルの強さなどの指標を用いて評価・スコアリングされます。そして、最も高いスコアを獲得したチャネルが選択されます。選択されたチャネルは、可視化モジュールへ渡され、将来方向へ延長されたトレンドラインとともに、各種ラベルやマーカーをチャート上へ描画します。
一方で、ブレイクアウト検出・確認モジュールは、これらのトレンドラインをリアルタイム価格へ投影し、価格がチャネルを突破していないかを継続的に監視します。ブレイクアウトの可能性が検出されると、ボラティリティ、構造的ブレイク、流動性、およびボラティリティ拡大など、複数の条件を用いてその妥当性を検証します。すべての条件を満たした場合にのみ、システムはブレイクアウトを確定し、即時、またはリテスト確認後に売買シグナルを生成します。
生成されたシグナルは、シグナル管理・永続化モジュールによって管理されます。このモジュールは、シグナルおよびその描画情報を永続的に保存し、チャートを再読み込みした後でも正しく表示されるようにします。また、描画オブジェクトのライフサイクルも管理しており、ティックごとに一時的なオブジェクトを削除・再生成する一方で、確定済みのシグナルやブレイクアウトゾーンは保持し続けます。その結果、トレーダーは常に最新かつ分かりやすい視覚情報を得られるだけでなく、過去に確定したブレイクアウトシグナルも継続して参照できます。
この一連の処理では、データは構造化されたデータ型を用いて管理されます。スイングポイントからチャネル候補、そして確定したブレイクアウトシグナルへと段階的に受け渡され、各モジュールはそれぞれの役割に専念します。この整理されたデータフローによって、システム全体の処理は分かりやすく、柔軟で拡張しやすいものとなり、高品質で信頼性の高いブレイクアウトシグナルを提供できます。
実行ライフサイクルとイベント処理
このEAは、MetaTrader 5 のイベント駆動型実行モデルに厳密に従って動作します。初期化処理は OnInit()で実行されます。この段階で、ATR などのインジケータハンドル、各種設定パラメータ、および描画オブジェクトの接頭辞が初期化されます。また、すべての永続配列もこの段階で確保されるため、市場データの処理が開始される前にシステム全体の準備が完了します。
分析処理の中心となるワークフローは OnTick()内で実行されます。ただし、計算効率を維持するため、スイングポイントの検出や平行チャネルの再構築といった負荷の大きい構造解析は、新しいローソク足が確定した場合にのみ実行されます。この判定は、最後に処理したローソク足の時刻を保持し、現在のローソク足の開始時刻と比較することでおこなわれます(「ブレイクアウト検出・確認モジュール」 のOnTick()サンプルにあるstatic datetime lastBarTime参照)。このように、新しいバーが形成された場合にのみ負荷の大きい処理を実行することで、ティックごとに同じ計算を繰り返すことを避けながらも、ブレイクアウトが発生した際にはリアルタイムに反応できます。
EAがチャートから削除された場合や再コンパイルされた場合には、OnDeinit()が実行されます。この関数では、描画オブジェクトやインジケータハンドルが適切に解放され、不要なオブジェクトがチャート上に残らないよう管理されます。このような厳密なライフサイクル管理により、EA は再初期化後も安定して動作し、高い処理効率と予測可能な動作を維持できます。
テストと結果
このセクションでは、本ツールの性能および信頼性を評価するために実施したテストについて説明します。評価は、互いに補完し合う以下の2つの観点から実施しました。
1) デモ口座を用いたリアルタイム環境でのテスト
本ツールをデモ口座のチャート上で稼働させ、リアルタイムの市場環境における応答性、検出精度、および動作の安定性を検証しました。このテストでは、実際の価格変動に対してシステムが適切に反応し、有効なブレイクアウトを正確に検出できること、誤シグナルを効果的に排除できること、そして明確な視覚アラートをリアルタイムで表示できることを確認しました。
- ライブテスト:EUR/USD(1時間足)
下図は、2026年2月中旬の EURUSD(H1)チャートにおいて、本システムが検出した典型的な下降平行チャネルのブレイクアウトを示しています。この例は、モジュール化されたアーキテクチャが実際の市場環境に近い条件下でも有効に機能することを示しています。

チャートには、2026年2月11日頃から17日頃にかけて形成された、明確な下降平行チャネルが描かれています。上側の境界線(緑色)は複数のスイングハイを結び、下側の境界線はそれと平行に配置され、複数のスイングローに沿って推移しています。インジケータによって表示される主な要素は以下のとおりです。
- スイングポイント:スイングハイは赤い丸、スイングローは緑色の丸で表示されます。ATRに基づく最小スイング幅のフィルタによって、小さな価格変動は除外されています。
- チャネルライン:LineExtensionBarsにより将来方向へ延長され、明確な下降トレンドを示しています。また、ClassifyChannel()によってDESCENDING(下降チャネル)と判定されています。
- タッチ回数:チャネルの上下両方の境界線で複数回のタッチが確認され、高いチャネルスコアと MEDIUM/HIGH の強度評価に寄与しています。
- ブレイクアウトシグナル:2026年2月18日、1.1832~1.1820付近でローソク足がチャネル下限を明確に終値ベースで下抜けた後、赤色の下向き矢印が表示されています。
この例は、スイングポイントの検出、平行チャネルの構築、ブレイクアウトの確認、そして永続的なシグナル表示が連携して動作し、リアルタイムのトレード判断に役立つ精度の高い情報を提供できることを示しています。
2) 過去のデータを用いたバックテスト
続いて、さまざまな市場環境における性能を評価するため、過去の価格データを用いた包括的なバックテストを実施しました。このテストでは、主に有効なブレイクアウトの検出精度、誤シグナルの発生率、 勝率、リスクリワード比などのパフォーマンス指標が評価されました。
- バックテスト:EUR/USD(1時間足)
下図は、EAのバックテスト結果を示しています。システムは市場構造を形成する重要なスイングポイントを安定して検出し、小さな価格変動を除外しながら、信頼性の高い平行チャネルを構築しています。また、上方向、下方向のいずれのブレイクアウトも安定して確実に捉えており、確認済みのシグナルは矢印によって明確に表示されています。

バックテスト全体を通じて、本EAは市場構造を一貫して正確に捉え、ブレイクアウトを適切に検証するとともに、分かりやすい視覚的シグナルを提供できることが確認されました。これらの結果は、多様な市場環境においても本ツールが高い堅牢性と信頼性を備えていることを示しています。
結論
本システムのモジュール化されたアーキテクチャは、MQL5における平行チャネルのブレイクアウトを検出、検証、および可視化するための包括的なアプローチを実現しています。スイングポイントの検出、チャネル構築、ブレイクアウトの確認、シグナルの可視化、そして適応的な再計算といった各コンポーネントを明確に分離することで、リアルタイムの市場環境においても高い検出精度と優れた応答性を両立しています。
デモ口座を用いたリアルタイムテストでは、市場ノイズを効果的に排除しながら重要なブレイクアウトを正確に検出し、分かりやすく実践的な視覚シグナルを提供できることが確認されました。また、複数の過去データセットを用いたバックテストでは、EAの堅牢性が検証されました。その結果、市場構造を形成する重要なスイングポイントを安定して検出し、信頼性の高い平行チャネルを構築するとともに、上昇、下降の両方向におけるブレイクアウトを一貫して検出できることが確認されました。
これらの検証結果から、本システムは市場構造に基づく高精度なブレイクアウト分析を求めるトレーダーにとって、信頼性の高い基盤を提供することが分かります。リアルタイム市場への適応性、ヒストリカルデータで実証された信頼性、そして継続的な視覚フィードバックを兼ね備えることで、ユーザーは市場の動きをより的確に把握し、十分に検証されたシグナルに基づいて判断を下すことができます。その結果、本システムはMQL5を用いたプロフェッショナルなチャート分析において、実用性と汎用性を兼ね備えた有力なツールとなります。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/21443
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
共和分株式による統計的裁定取引(最終回):特化型データベースを用いたデータ分析
MQL5取引ツール(第21回):回帰グラフにサイバーパンクテーマを追加する
ラリー・ウィリアムズの『市場の秘密』(第13回):隠れスマッシュデー反転パターンの自動化
初心者からエキスパートへ: トレンドフィルタによる流動性戦略の拡張
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索