プライスアクション分析ツールキットの開発(第37回):Sentiment Tilt Meter
はじめに
本記事は、連載記事プライスアクション分析ツールキットの開発の第37回となります。私の一貫した目標は、トレーダーが市場の挙動を解釈するうえで役立つ実用的なツールを開発し、必要に応じてその分析作業の一部を自動化できるようにすることです。ここまでに、スタンドアロンのMQL5ユーティリティ、外部連携、そしてハイブリッド型ソリューションを紹介してきました。本日は、そのツールキットをさらに拡張する、有用な実践的な追加機能を紹介します。
データ可視化は、ラベルの重なりや過度なインジケーター、場当たり的な注釈類によって煩雑になりやすく、その結果として本来のシグナル変化や根拠が把握しにくくなります。この曖昧さは意思決定を妨げ、ルールベースのシステムに対する信頼性を低下させ、さらに自動化を規律的におこなうために必要な監査証跡の作成を難しくします。
Sentiment Tilt Meter (STM)はこの問題に対処するため、複数の時間足におけるローソク足レベルの方向性指標を集約し、平滑化した単一のスカラー値(–100から+100)として出力します。このスコアは、コンパクトで見やすいダッシュボードレイアウト内に表示されます。矢印やテキストラベルといった永続的な注釈はバーの終了時刻に正確にアンカーされ、各アラートがタイムスタンプと価格レベルの両方で独立して検証できるようになっています。
STMは、反転の閾値、符号反転時の変動量と継続時間、モメンタム条件、時間足ごとの重み付けといった明確でパラメータ化されたルールを提供し、ユーザーがノイズ除去と応答性のトレードオフを意図に応じて調整できるようになっています。
本記事では、まずストラテジーの概要を説明し、その後MQL5実装、テスト結果と考察を示し、最後に簡潔なまとめで締めくくります。次の目次を参照してください。
戦略の概要
市場にはノイズが多く、このノイズこそが、一貫した意思決定を阻害する最大の要因です。インジケーターが積み重なり、ラベルが重なり合うと、価格がなぜ動いたのかを判別することはすぐに困難になります。Sentiment Tilt Meter (STM)はその霧を切り裂くために作られました。短期的な市場の傾きをひとつの検証可能な指標として示し、チャート上でその証拠を可視化することで、推測ではなく規律に基づいて判断できるようにします。
ここでの「センチメント」とは、市場の短期的な傾き、すなわち最近のローソク足における買いや売りの純推進力のことです。STMは価格変動から直接この傾きを測定し、それぞれの完成したローソク足を小さく解釈しやすいスコアに変換します。マイクロ特徴量は以下の通りです。
- Candle Body Ratio (CBR)
![]()
- Close Position Percent (CPP)

- Volatility-Adjusted Distance (VAD)
![]()
ここでATRは直近のトゥルーレンジの基準値であり、
Clamp ( x , a , b ) =max ( a , min ( b , x ) ) \operatorname{clamp}(x,a,b)=\max(a,\min(b,x)) clamp(x,a,b)=max(a,min(b,x))
です。各ローソク足のミニスコアは、以下の例のように重み付きで算出されます。
![]()
これを[ − 1 , + 1 ] [-1, +1] [−1,+1]の範囲に制限(クランプ)し、さらに簡易的な信頼度や静穏な市場に対する補正因子を乗じます。時間足ごとのスコアはサンプル内で平均化され、ユーザー指定の重みで時間足を横断して融合され、
![]()
最終的に指数平滑化されます。
![]()
以下は価格とスコアの図です。

スコア図は、時間に沿った平滑化されたセンチメント値を表示し、生の符号反転点には小さな「×」を付け、マグニチュード、持続性、モメンタムのフィルタを適用した後の受理済みフリップシグナルには大きな丸を付けます。この可視化により、誤シグナルを引き起こす可能性のある小さなノイズの変動と、フィルタによって許可された実際の反転を簡単に識別することができます。
価格図では、受理されたシグナルの価格系列内での位置を表示します。矢印や「BUY」「SELL」ラベルは、シグナル発生時の価格に正確に配置され、シグナルがサポートやレジスタンスレベル、トレンド方向、または近隣のスイングポイントと整合しているかを即座に確認することができます。
受理の観点では、EAが生成するシグナルは時間と価格の追跡が即座に可能であり、バックテストおよび短期フォワードテストにおいて堅牢であること、明らかな誤検知をエントリー可能なレベルまで低減できることが求められます。これらの条件を満たしている場合、STMはその役割を果たしており、短期的な市場の傾きを明確かつ調整可能で、検証可能な形で提供できることになります。
MQL5での実装
実装を始める前に、環境を正しくセットアップするために2分ほど時間を確保してください。こうすることで、後々大幅に時間を節約できます。MetaTrader 5とMetaEditorがインストールされていることを確認してください。MetaEditorは単体アプリケーションとして起動することも、MetaTrader 5からアクセスすることも可能であり、どちらの方法でも問題ありません。
エキスパートアドバイザー(EA)を準備するには以下の手順をおこないます。
- MetaEditorを開きます。
- [ファイル]→[新規作成]→[エキスパートアドバイザ(テンプレート)]に移動します。
- 新しく作成されたファイルにEAソースコードを貼り付けます。
- わかりやすい名前を付けてファイルを保存します。
- F7キーを押してコードをコンパイルします。
コンパイルがエラーや警告なしで完了することを確認します。問題が報告された場合はコードを見直し、修正した上で再コンパイルし、コンパイラがクリーンビルドを報告するまで繰り返します。
MetaTrader 5に戻って、以下をおこないます。
- EAをストラテジーテスターに読み込むか、直接チャートにアタッチします。
- ダッシュボード、ログ、シグナルを監視します。
- [エキスパート]タブと[ジャーナル]で実行時メッセージやデバッグ情報を確認します。
このセットアップにより、効率的な開発とテストのために環境が正しく構成されます。 この段階から、Sentiment Tilt Meter (STM)EAを作成するステップに沿って進めていきます。開発プロセスの各段階を丁寧にガイドします。
ヘッダーおよびメタデータブロックは、EAの識別情報、著作権、作者プロフィールへのリンク、コンパイルモード(#property strict)を設定します。これらの行は宣言的なものですが重要で、コンパイル済みプログラム内にバージョン情報や帰属情報を埋め込み、より厳密なコンパイル時チェックを可能にします。その直後にEAは標準の取引ヘルパーライブラリ「Trade.mqh」をインクルードし、CTradeクラスやその他の取引関連のヘルパーにアクセスできるようにします。今回のファイルではCTrade g_tradeをほとんど使用していない(あるいはまったく使用していない)場合でも、このライブラリをインクルードすることで、後で注文を出す必要が出た場合にリファクタリングなしで対応できるように準備してあります。
//+------------------------------------------------------------------+ //| STM EA| //| Copyright 2025, MetaQuotes Ltd.| //| https://www.mql5.com/ja/users/lynnchris| //+------------------------------------------------------------------+ #property copyright "Copyright 2025, MetaQuotes Ltd." #property link "https://www.mql5.com/ja/users/lynnchris" #property version "1.0" #property strict #include <Trade\Trade.mqh>
入力ブロックは、ユーザーが設定可能なすべてのパラメータを定義します。入力項目は目的別にグループ化されており、時間足の選択、シグナルアルゴリズムの調整、表示レイアウトおよびパネルスタイル、音声/プッシュ通知、矢印やマーカーの動作などに分かれています。プライマリの時間足とオプションの追加時間足を使うことで、EAは複数の時間足からのシグナルを融合できます。融合比率は、InpWeightPrimary、InpWeightExtra1、InpWeightExtra2の各重みで制御します。平滑化(InpSmoothAlpha)は、生の融合スコアに対する指数平滑化を管理します。閾値調整用の変数(例:InpFlipThreshold、InpPositiveZone、InpNegativeZone)は、EAが大きな反転と見なすタイミングや、ポジティブ/ネガティブゾーンに入ったと判断する条件を定義します。
表示関連の入力は、チャート上のダッシュボードの設定を制御します。サイズ、角の位置、フォント、色、ヒストグラムバーの数、パネルの余白などです。矢印やマーカーの入力では、チャート上に表示されるシグナルを有効化したり調整したりできます。また、いくつかの安全チェック(最低バー数、符号反転の大きさ、モメンタム要件)により、ノイズや誤反転を減らすことができます。すべての入力は明示的に型指定されており、EAのダイアログに表示され、コードを編集することなく調整可能です。
// --- User inputs (layout / visuals tuned) ---------------------------- input ENUM_TIMEFRAMES InpTFPrimary = PERIOD_M5; input bool InpUseMultiTF = true; input ENUM_TIMEFRAMES InpExtraTF1 = PERIOD_M15; input ENUM_TIMEFRAMES InpExtraTF2 = PERIOD_H1; input int InpLookback = 20; input int InpSSWindow = 5; input int InpATRPeriod = 14; input double InpATRMultiplier = 1.0; input double InpVolQuietThreshold = 0.4; input int InpHistogramMaxBars = 24; input int InpPanelPadding = 10; input int InpPanelCorner = CORNER_LEFT_UPPER; input int InpPanelX = 5; input int InpPanelY = 25; input string InpFont = "Arial"; input int InpTitleFontSize = 12; input int InpScoreFontSize = 18; input int InpSmallFontSize = 9; input color InpPanelColor = clrBlack; input int InpPanelAlpha = 200; input bool InpEnableSound = true; input string InpSoundFile = "alert.wav"; input bool InpEnablePush = false; input double InpWeightPrimary = 0.6; input double InpWeightExtra1 = 0.2; input double InpWeightExtra2 = 0.2; input double InpSmoothAlpha = 0.28; input double InpFlipThreshold = 60.0; input double InpPositiveZone = 30.0; input double InpNegativeZone = -30.0; input int InpMinBarsForSignal = 3; // arrows / marker options input bool InpShowArrows = true; input int InpArrowFontSize = 16; input int InpArrowOffsetPoints = 8; input int InpMaxSignalsToKeep = 50; input bool InpSignalOnSignFlip = true; input double InpMinSignFlipAbs = 6.0; input int InpSignFlipHoldBars = 1; input bool InpRequireMomentum = true; input double InpMinMomentum = 0.5; //+------------------------------------------------------------------+定数とグローバル変数が続きます。EAは、チャートごとに一意のg_prefix(銘柄+チャートID)を構築し、チャート上のオブジェクトに名前空間を付与して、他のEAや手動で作成されたオブジェクトとの衝突を回避します。各時間足のATRインジケーター用のハンドル、ヒストグラム用のバッファ(g_hist_buffer)、および循環ストレージを管理するインデックスも用意されています。グローバル変数は、平滑化された値の状態(g_smoothed_score、g_prev_smoothed)、大きな反転を検出するために使用された最後のアラートスコア、現在のg_zone_state(1、-1、0)も保持します。CTradeg_tradeは、EAが将来的に注文を発行する拡張をおこなう場合に備えて宣言されています。g_last_signal_textとg_last_signal_timeを保存することで、UIがパネル上で最新のアクションを表示できるようになります。
// globals long g_chart_id = 0; string g_prefix = ""; string ui_bg_name = ""; string ui_shadow_name = ""; string ui_title_name = ""; string ui_score_name = ""; string ui_zone_name = ""; string ui_hist_base = ""; string ui_recent_name = ""; string ui_advice_name = ""; string ui_signal_name = ""; int g_atr_handle_primary = INVALID_HANDLE; int g_atr_handle_extra1 = INVALID_HANDLE; int g_atr_handle_extra2 = INVALID_HANDLE; double g_hist_buffer[]; int g_hist_idx = 0; int g_hist_count = 0; double g_smoothed_score = 0.0; double g_prev_smoothed = 0.0; double g_last_alert_score = 0.0; int g_zone_state = 0; CTrade g_trade; string g_last_signal_text = "None"; datetime g_last_signal_time = 0; const string BASE_HIST = "STM_HBAR_"; //+------------------------------------------------------------------+いくつかの小さなユーティリティルーチンがあり、UIや表示コードをより堅牢にしています。ARGB_uintは、アルファ値とMQLの色からARGBの符号なし整数を生成し、半透明の矩形背景をきれいに指定できるようにします。EstimateTextWidthは、フォントサイズと調整済みの文字幅係数に基づいた文字列の簡易的なピクセル幅推定器であり、ゾーンバッジや最新テキスト文字列を配置する際にラベルの重なりを避けるために使用します。これらの近似は、正確なレイアウトが必須ではないチャート上のパネルでは十分に合理的であり、衝突が発生すると見た目が不快になるため有用です。
// ARGB helper - returns a color as unsigned int uint ARGB_uint(int a, color c) { uint u = ((uint)c) & 0x00FFFFFF; return ((((uint)a) & 0xFF) << 24) | u; } // approximate text width estimator (pixels) int EstimateTextWidth(string txt,int fontSize) { if(StringLen(txt) <= 0) return 6; double factor = 0.58; int w = (int)MathRound(StringLen(txt) * fontSize * factor); return MathMax(8, w); } //+------------------------------------------------------------------+DrawTextとDrawCellは、チャート上のラベルおよび矩形ラベルを作成・設定するための安全なラッパーです。これらはオブジェクト作成とプロパティ設定のパターン(隅の位置、X/Y距離、フォント、選択可能フラグ、Zオーダーなど)を一元化しており、すべてのUIラベルが同じ規約に従うようにします。これらのヘルパーを使用することで、冗長なボイラープレートコードを避けることができ、パネルが再構築または更新される際にも一貫した動作を保証できます。
// safe DrawText - sets font explicitly and uses exact x/y void DrawText(string id,string txt,int x,int y,color clr,int sz) { if(ObjectFind(g_chart_id,id) < 0) ObjectCreate(g_chart_id,id,OBJ_LABEL,0,0,0); ObjectSetInteger(g_chart_id,id,OBJPROP_CORNER,InpPanelCorner); ObjectSetInteger(g_chart_id,id,OBJPROP_XDISTANCE,x); ObjectSetInteger(g_chart_id,id,OBJPROP_YDISTANCE,y); ObjectSetInteger(g_chart_id,id,OBJPROP_COLOR,(int)clr); ObjectSetInteger(g_chart_id,id,OBJPROP_FONTSIZE,sz); ObjectSetString(g_chart_id,id,OBJPROP_FONT,InpFont); ObjectSetString(g_chart_id,id,OBJPROP_TEXT,txt); ObjectSetInteger(g_chart_id,id,OBJPROP_SELECTABLE,false); ObjectSetInteger(g_chart_id,id,OBJPROP_ZORDER,0); } // small helper to draw rectangle label cell void DrawCell(string id,int x,int y,int w,int h,color bg,color br) { if(ObjectFind(g_chart_id,id) < 0) ObjectCreate(g_chart_id,id,OBJ_RECTANGLE_LABEL,0,0,0); ObjectSetInteger(g_chart_id,id,OBJPROP_CORNER,InpPanelCorner); ObjectSetInteger(g_chart_id,id,OBJPROP_XDISTANCE,x); ObjectSetInteger(g_chart_id,id,OBJPROP_YDISTANCE,y); ObjectSetInteger(g_chart_id,id,OBJPROP_XSIZE,w); ObjectSetInteger(g_chart_id,id,OBJPROP_YSIZE,h); ObjectSetInteger(g_chart_id,id,OBJPROP_BGCOLOR,(int)bg); ObjectSetInteger(g_chart_id,id,OBJPROP_BORDER_COLOR,(int)br); ObjectSetInteger(g_chart_id,id,OBJPROP_SELECTABLE,false); ObjectSetInteger(g_chart_id,id,OBJPROP_ZORDER,1); }CreateUIObjectsとDeleteUIObjectsは、すべてのUI要素のライフサイクルを管理します。CreateUIObjectsは、背景、シャドウ、タイトル、スコア、ゾーン、最新情報、ヒストグラムラベルのスロットを事前作成し、デフォルトのフォントとZオーダーを設定します。DeleteUIObjectsはdeinit時にこれらをクリーンアップします。これは、EAを削除した際にチャートを整然と保つための有用なプラクティスです。InpHistogramMaxBarsに基づいて固定数のヒストグラムラベルオブジェクトを事前作成することで、実行時の頻繁なオブジェクト作成を避け、パフォーマンスと安定性を向上させます。
void CreateUIObjects() { int default_w = 200; int default_h = 80; int scol = (int)ARGB_uint(InpShadowAlpha, InpShadowColor); DrawCell(ui_shadow_name, InpPanelX + 3, InpPanelY + 3, default_w, default_h, (color)scol, InpGridClr); DrawCell(ui_bg_name, InpPanelX, InpPanelY, default_w, default_h, InpPanelBG, InpGridClr); DrawText(ui_title_name, "", InpPanelX + InpPanelPadding, InpPanelY + InpPanelPadding, InpTxtClr, InpTitleFontSize); DrawText(ui_score_name, "", InpPanelX + InpPanelPadding, InpPanelY + InpPanelPadding + InpTitleFontSize + 4, InpTxtClr, InpScoreFontSize); DrawText(ui_zone_name, "", InpPanelX + default_w - InpPanelPadding - 80, InpPanelY + InpPanelPadding + 4, InpTxtClr, InpSmallFontSize); DrawText(ui_advice_name, "", InpPanelX + InpPanelPadding, InpPanelY + InpPanelPadding + InpTitleFontSize + InpScoreFontSize + 8, InpTxtClr, InpSmallFontSize); DrawText(ui_recent_name, "", InpPanelX + InpPanelPadding, InpPanelY + default_h - InpPanelPadding - 18, InpTxtClr, InpSmallFontSize); DrawText(ui_signal_name, "", InpPanelX + InpPanelPadding + 120, InpPanelY + InpPanelPadding + InpTitleFontSize + InpScoreFontSize + 8, InpTxtClr, InpSmallFontSize); for(int i = 0; i < InpHistogramMaxBars; i++) { string name = ui_hist_base + IntegerToString(i); if(ObjectFind(g_chart_id, name) < 0) { ObjectCreate(g_chart_id, name, OBJ_LABEL, 0, 0, 0); ObjectSetInteger(g_chart_id, name, OBJPROP_CORNER, InpPanelCorner); ObjectSetString(g_chart_id, name, OBJPROP_FONT, InpFont); ObjectSetInteger(g_chart_id, name, OBJPROP_FONTSIZE, InpSmallFontSize); ObjectSetInteger(g_chart_id, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(g_chart_id, name, OBJPROP_ZORDER,0); } } } void DeleteUIObjects() { if(g_chart_id == 0) return; if(ObjectFind(g_chart_id, ui_shadow_name) >= 0) ObjectDelete(g_chart_id, ui_shadow_name); if(ObjectFind(g_chart_id, ui_bg_name) >= 0) ObjectDelete(g_chart_id, ui_bg_name); if(ObjectFind(g_chart_id, ui_title_name) >= 0) ObjectDelete(g_chart_id, ui_title_name); if(ObjectFind(g_chart_id, ui_score_name) >= 0) ObjectDelete(g_chart_id, ui_score_name); if(ObjectFind(g_chart_id, ui_zone_name) >= 0) ObjectDelete(g_chart_id, ui_zone_name); if(ObjectFind(g_chart_id, ui_recent_name) >= 0) ObjectDelete(g_chart_id, ui_recent_name); if(ObjectFind(g_chart_id, ui_advice_name) >= 0) ObjectDelete(g_chart_id, ui_advice_name); if(ObjectFind(g_chart_id, ui_signal_name) >= 0) ObjectDelete(g_chart_id, ui_signal_name); for(int i = 0; i < InpHistogramMaxBars; i++) { string name = ui_hist_base + IntegerToString(i); if(ObjectFind(g_chart_id, name) >= 0) ObjectDelete(g_chart_id, name); } }
RefreshUIは、パネルレイアウトとヒストグラムを実際に計算して描画する場所です。フォントの高さを測定し、タイトル、スコア、小テキスト、ヒストグラム行のスペースを確保します。ヒストグラムが収まるようにパネル幅を動的に計算しますが、妥当な最小幅は維持します。シャドウと背景の矩形を描画し、タイトルとの重なりを避けながらゾーンバッジの配置を計算します(EstimateTextWidthを使用)。平滑化スコアの色(scoreCol)は閾値に基づいて選択されます。「Recent:」行は最新のプライマリ時間足(クローズとオープンおよびそのポイントレンジ)から生成され、オーバーフローする場合は省略記号で切り詰められます。
ヒストグラムについては、マグニチュードを視覚的に表現するためのテキストブロックのグリフ数を計算し、各グリフを事前作成されたラベルオブジェクトに割り当て、符号に応じて色を選択します。このルーチンは防御的に設計されており、利用可能なスペースをチェックし、必要に応じて間隔を調整します。ラベルの衝突を避けつつ、パネルをコンパクトで読みやすく保つよう設計されています。
void RefreshUI(double rawScore, double smoothScore) { int title_h = InpTitleFontSize + 6; int score_h = InpScoreFontSize + 8; int small_h = InpSmallFontSize + 4; int hist_area_h = 22; int gap_between_sections = 6; int hist_px_step = 7; int hist_px_width = InpHistogramMaxBars * hist_px_step; int panel_width = MathMax(340, hist_px_width + InpPanelPadding*2 + 30); int extra_recent_space = small_h + 12; int panel_height = InpPanelPadding*2 + title_h + score_h + small_h + hist_area_h + (gap_between_sections * 4) + extra_recent_space; int scol = (int)ARGB_uint(InpShadowAlpha, InpShadowColor); DrawCell(ui_shadow_name, InpPanelX + 3, InpPanelY + 3, panel_width, panel_height, (color)scol, InpGridClr); DrawCell(ui_bg_name, InpPanelX, InpPanelY, panel_width, panel_height, InpPanelBG, InpGridClr); int x_start = InpPanelX + InpPanelPadding; int y_title = InpPanelY + InpPanelPadding; string titleText = StringFormat("Sentiment Tilt Meter — %s [%s]", Symbol(), TFToText(InpTFPrimary)); DrawText(ui_title_name, titleText, x_start, y_title, InpTxtClr, InpTitleFontSize); // ... zone, score, advice, recent and histogram painting (omitted here for brevity) // The full EA code contains the rest and iterates label objects to draw histogram blocks. }TFToTextは、ENUM_TIMEFRAMESを短い人間向けのテキストラベル(M1、M5、M15、H1など)に変換する単純なヘルパーです。タイトルで使用され、ユーザーがEAが参照しているプライマリ時間足を常に把握できるようにします。
string TFToText(ENUM_TIMEFRAMES tf) { switch(tf) { case PERIOD_M1: return "M1"; case PERIOD_M5: return "M5"; case PERIOD_M15: return "M15"; case PERIOD_M30: return "M30"; case PERIOD_H1: return "H1"; case PERIOD_H4: return "H4"; case PERIOD_D1: return "D1"; case PERIOD_W1: return "W1"; case PERIOD_MN1: return "MN"; default: return IntegerToString((int)tf); } }
OnInitは初期化をおこないます。チャートIDを取得してオブジェクト接頭辞を構築し、ヒストグラムバッファをリサイズして初期化します。要求された時間足のATRインジケーターハンドル(iATR)を作成します(マルチTFモードが有効な場合のみ)。初期の生融合センチメントを計算(ComputeFusedSentiment)、平滑化値とゾーン状態を設定し、UIオブジェクトを作成してRefreshUIで初期パネルを描画します。また、1秒タイマーを設置(EventSetTimer(1))し、OnTimerで新しいプライマリバーを検知できるようにします。この設計では、平滑化は最初の生値で初期化されるため、最初に表示される平滑値も安定しています。コードは初期ゾーンを正しく設定するために早期にUpdateZoneStateを呼び出しています。
OnDeinitはEAが削除された際のクリーンアップをおこないます。タイマーを停止し、UIオブジェクトを削除し、このEAが作成したシグナルマーカー(g_prefix+"SIG_"命名規則)をすべて除去します。また、ATRインジケーターハンドルをIndicatorReleaseで解放します。これにより、リソースがクリーンに保たれ、チャート上に残骸ラベルが残ったり、インジケーターハンドルがリークしたりすることを防ぎます。
int OnInit() { g_chart_id = ChartID(); g_prefix = Symbol() + "_" + IntegerToString((int)g_chart_id) + "_"; ui_bg_name = g_prefix + "BG"; ui_shadow_name = g_prefix + "SHDW"; ui_title_name = g_prefix + "TITLE"; ui_score_name = g_prefix + "SCORE"; ui_zone_name = g_prefix + "ZONE"; ui_hist_base = g_prefix + BASE_HIST; ui_recent_name = g_prefix + "RECENT"; ui_advice_name = g_prefix + "ADVICE"; ui_signal_name = g_prefix + "LASTSIG"; ArrayResize(g_hist_buffer, InpHistogramMaxBars); ArrayInitialize(g_hist_buffer, 0.0); g_hist_idx = 0; g_hist_count = 0; g_atr_handle_primary = iATR(Symbol(), InpTFPrimary, InpATRPeriod); if(InpUseMultiTF) { g_atr_handle_extra1 = iATR(Symbol(), InpExtraTF1, InpATRPeriod); g_atr_handle_extra2 = iATR(Symbol(), InpExtraTF2, InpATRPeriod); } double raw_init = ComputeFusedSentiment(); g_smoothed_score = raw_init; g_prev_smoothed = g_smoothed_score; g_last_alert_score = raw_init; UpdateZoneState(raw_init); CreateUIObjects(); RefreshUI(raw_init, g_smoothed_score); EventSetTimer(1); return(INIT_SUCCEEDED); } void OnDeinit(const int reason) { EventKillTimer(); DeleteUIObjects(); // delete signal markers created by this EA (prefix-based) string name; int total = ObjectsTotal(g_chart_id); for(int i = total - 1; i >= 0; --i) { name = ObjectName(g_chart_id, i); if(StringFind(name, g_prefix + "SIG_") == 0) ObjectDelete(g_chart_id, name); } if(g_atr_handle_primary != INVALID_HANDLE) IndicatorRelease(g_atr_handle_primary); if(g_atr_handle_extra1 != INVALID_HANDLE) IndicatorRelease(g_atr_handle_extra1); if(g_atr_handle_extra2 != INVALID_HANDLE) IndicatorRelease(g_atr_handle_extra2); }EAはOnTimerをメインのポーリング機構として使用します。現在のプライマリ時間足バーのタイムスタンプ(iTime(Symbol(), InpTFPrimary, 0))をチェックし、そのタイムスタンプが最後に処理した値と異なる場合にHandleNewPrimaryBarを呼び出します。この方法により、タイマーが発生するたびに処理をおこなうのではなく、新しく確定したバーごとに一度だけ動作させることができます。これは、バー単位のシグナルを生成したい場合に便利なパターンであり、OnTickではなくタイマーを使う理由を示しています。
datetime g_last_primary_time = 0; void OnTimer() { datetime t = iTime(Symbol(), InpTFPrimary, 0); if(t == g_last_primary_time) return; g_last_primary_time = t; HandleNewPrimaryBar(); } void HandleNewPrimaryBar() { double raw = ComputeFusedSentiment(); double alpha = MathMax(0.0, MathMin(1.0, InpSmoothAlpha)); g_prev_smoothed = g_smoothed_score; g_smoothed_score = alpha * raw + (1.0 - alpha) * g_smoothed_score; g_hist_buffer[g_hist_idx] = g_smoothed_score; g_hist_idx = (g_hist_idx + 1) % InpHistogramMaxBars; if(g_hist_count < InpHistogramMaxBars) g_hist_count++; RefreshUI(raw, g_smoothed_score); ProcessAlerts(raw, g_smoothed_score); }
HandleNewPrimaryBarは、ComputeFusedSentimentを使って新しい生融合センチメントを計算し、設定されたアルファで指数平滑化を適用します(g_smoothed_score = alpha * raw + (1-alpha) * prev)。平滑化された値は循環ヒストグラムバッファに格納され、インデックスとカウントが更新されます。UIはリフレッシュされ、ProcessAlertsが呼び出されて、この新しい平滑値がメッセージやシグナルをトリガーすべきかを確認します。循環バッファはg_hist_idxをInpHistogramMaxBarsでモジュロ演算することで実装され、g_hist_countは有効なエントリ数を追跡します。これにより、視覚的なヒストグラムや符号保持チェックのためにローリング履歴が保持されます。
ComputeFusedSentimentはトップレベルのスコア集約関数です。まず、3つの重み入力の絶対値を正規化して合計が1になるようにします(合計が0の場合は安全のためプライマリのみを使用)。次に、プライマリ時間足と、有効であれば2つの追加時間足についてComputeTFScoreを呼び出します。最後に加重平均を計算し、融合出力を[-100, 100]の範囲に制限します。これにより、各時間足ごとの生スケールに関わらず、下流のUIや閾値ロジックを一貫して維持できます。
double ComputeFusedSentiment() { double w1 = MathAbs(InpWeightPrimary); double w2 = MathAbs(InpWeightExtra1); double w3 = MathAbs(InpWeightExtra2); double sum = w1 + w2 + w3; if(sum <= 0.0) { w1 = 1.0; w2 = 0.0; w3 = 0.0; sum = 1.0; } w1 /= sum; w2 /= sum; w3 /= sum; double s1 = ComputeTFScore(Symbol(), InpTFPrimary, InpLookback, InpSSWindow, g_atr_handle_primary); if(!InpUseMultiTF) return s1; double s2 = ComputeTFScore(Symbol(), InpExtraTF1, InpLookback, InpSSWindow, g_atr_handle_extra1); double s3 = ComputeTFScore(Symbol(), InpExtraTF2, InpLookback, InpSSWindow, g_atr_handle_extra2); double fused = s1 * w1 + s2 * w2 + s3 * w3; return MathMax(-100.0, MathMin(100.0, fused)); } double ComputeTFScore(string sym, ENUM_TIMEFRAMES tf, int lookback, int ssWindow, int atrHandle) { MqlRates rates[]; int needed = MathMax(lookback, ssWindow) + 6; int copied = CopyRates(sym, tf, 0, needed, rates); if(copied <= 0) return 0.0; double atr = 0.0001; if(atrHandle != INVALID_HANDLE) { double abuf[]; if(CopyBuffer(atrHandle, 0, 1, 1, abuf) > 0) atr = abuf[0] * InpATRMultiplier; } else { int rc = MathMin(10, copied - 1); double ar = 0.0; for(int i = 1; i <= rc; i++) ar += (rates[i].high - rates[i].low); if(rc > 0) ar /= rc; if(ar > 0) atr = ar * 0.6; } int limit = MathMin(ssWindow, copied - 1); double avgRange = 0.0; int vr = MathMin(10, copied - 1); for(int k = 1; k <= vr; k++) avgRange += (rates[k].high - rates[k].low); if(vr > 0) avgRange /= vr; double sumSS = 0.0; int count = 0; for(int i = 1; i <= limit; i++) { double open = rates[i].open; double close = rates[i].close; double high = rates[i].high; double low = rates[i].low; double range = high - low; if(range <= 0.0) continue; double dir = (close > open) ? 1.0 : -1.0; double cbr = (MathAbs(close - open) / range) * dir; double cpp = (((close - low) / range) - 0.5) * 2.0 * dir; double vad = 0.0; if(atr > 0.0) vad = (range / atr) - 1.0; vad = MathMax(-1.0, MathMin(3.0, vad)); double ss = cbr * 0.4 + cpp * 0.3 + vad * 0.3; ss = MathMax(-1.0, MathMin(1.0, ss)); bool conf = false; if((i - 1) >= 0 && (i - 1) <= copied - 1) { double nOpen = rates[i - 1].open; double nClose = rates[i - 1].close; if(dir > 0.0 && nClose > nOpen) conf = true; if(dir < 0.0 && nClose < nOpen) conf = true; } double confFactor = conf ? 1.0 : 0.6; double volFactor = 1.0; if(atr > 0.0 && avgRange / atr < InpVolQuietThreshold) volFactor = 0.6; double finalSS = ss * confFactor * volFactor; sumSS += finalSS; count++; } if(count == 0) return 0.0; double avgSS = sumSS / count; return avgSS * 100.0; }
ComputeTFScoreは、時間足ごとのコアなシグナル特徴量を計算する箇所です。要求された時間足の最近のMqlRatesバーをコピーし、ATRの代理値を計算します。ATRハンドルが存在する場合はインジケーターバッファをコピーし、存在しない場合は単純な平均トゥルーレンジを計算して代替します。コードはまずavgRangeを算出し、最大ssWindow本の過去バーに対して3つの正規化特徴量を導出します。これらは、cbr(クローズ対オープンの範囲に対する相対値で方向と強さを捉える)、cpp(バー内のクローズ位置を方向に応じて正規化)、vad(ボリューム/ボラティリティ調整距離:ATRに対する範囲比-1)です。各特徴量には重み(それぞれ0.4、0.3、0.3)が付与され、[-1,1]に制限され、さらに2つの信頼度係数を乗じます。前バーで方向が確認されたかどうかをチェックするconfFactor、ATRに対して市場が異常に静かな場合に寄与を減らすvolFactorです。
バーごとの最終センチメントサンプルfinalSSは、ウィンドウ全体で累積・平均化され、利便性のため100倍されます。要するに、このルーチンは方向バイアス、バー内の位置、相対的範囲の大きさ、簡単なバー間確認を1つの時間足スコアにエンコードしています。
RecentSmoothedSignHoldは、符号反転ロジックで使用されるヘルパーで、最新バーの平滑値がすべて新しい符号と一致することを確認します。循環バッファg_hist_bufferを遡ってチェックし、必要な最近の平滑値のいずれかが符号テストに失敗した場合はfalseを返します。これにより、単発の異常バーによるシグナルのちらつきを防ぎます。
ProcessAlertsは、メッセージおよびチャートシグナルの判断エンジンです。まず、十分な履歴(InpMinBarsForSignal)があるかを確認し、その後大きな急反転(g_last_alert_scoreとの差の絶対値がInpFlipThresholdを超える)をチェックします。その場合、アラートを送信し、ゾーン状態を更新し、必要に応じてBUY/SELLテキストマーカーを描画します。大きな反転でない場合は、「InpPositiveZone/InpNegativeZone」に基づき新しいゾーンを計算します。InpSignalOnSignFlipが有効な場合は、符号反転(正→負または負→正)もチェックし、3つの受理条件を適用します:新しい平滑値の最小絶対値(InpMinSignFlipAbs)、最近の符号保持(InpSignFlipHoldBars)、およびオプションのモメンタム要件(InpRequireMomentum + InpMinMomentum)です。すべてのテストを通過した場合にのみ、符号反転を真のシグナルとして受理し、チャートマーカーを描画し、g_last_alert_scoreを更新し、g_zone_stateを設定します。
ゾーンが通常どおり変化した場合(ポジティブまたはネガティブゾーンに入る場合)も同様にアラートを送信し、最後のシグナルと時刻を記録し、マーカーを描画します。その結果、段階的で保守的なシグナル戦略となり、雑多なノイズよりも明確な構造変化を優先する設計になっています。
bool RecentSmoothedSignHold(int bars, int desiredSign) { if(bars <= 1) return true; if(g_hist_count < bars) return false; int max = InpHistogramMaxBars; int idx = (g_hist_idx - 1 + max) % max; for(int i = 0; i < bars; i++) { double v = g_hist_buffer[(idx - i + max) % max]; if(desiredSign == 1 && v <= 0.0) return false; if(desiredSign == -1 && v >= 0.0) return false; } return true; } void ProcessAlerts(double rawScore, double smoothScore) { if(g_hist_count < InpMinBarsForSignal) { PrintFormat("%s: waiting hist_count=%d (need %d)", __FUNCTION__, g_hist_count, InpMinBarsForSignal); return; } // large flip detection if(MathAbs(smoothScore - g_last_alert_score) >= InpFlipThreshold) { string m = StringFormat("STM Flip: %.1f -> %.1f on %s", g_last_alert_score, smoothScore, Symbol()); SendAlert(m); g_last_alert_score = smoothScore; UpdateZoneState(smoothScore); int newZoneFlip = 0; if(smoothScore >= InpPositiveZone) newZoneFlip = 1; else if(smoothScore <= InpNegativeZone) newZoneFlip = -1; if(newZoneFlip != 0) { g_last_signal_text = (newZoneFlip == 1) ? "BUY" : "SELL"; g_last_signal_time = TimeCurrent(); DrawSignalOnChart(newZoneFlip, smoothScore); PruneOldSignals(InpMaxSignalsToKeep); } RefreshUI(rawScore, g_smoothed_score); return; } }UpdateZoneStateは単純な関数で、平滑化スコアと設定されたポジティブ/ネガティブゾーン閾値に基づき、g_zone_stateを1、-1、または0に設定します。SendAlertはアラート送信を集中管理する関数で、有効であればサウンドを再生し、プッシュ通知を送信し、Alert()(ローカルターミナルダイアログ)とPrint()でログに出力します。アラート処理を集中化することで、将来的にメール送信やWebhookなどに拡張する際も容易になります。
void UpdateZoneState(double smoothScore) { if(smoothScore >= InpPositiveZone) g_zone_state = 1; else if(smoothScore <= InpNegativeZone) g_zone_state = -1; else g_zone_state = 0; } void SendAlert(string msg) { if(InpEnableSound) PlaySound(InpSoundFile); if(InpEnablePush) SendNotification(msg); Alert(msg); Print(__FILE__, ": ", msg); }
DrawSignalOnChartは、BUY/SELLテキストおよび矢印グリフを、最後に確定したプライマリバー(インデックス1)に正確に配置する役割を持ちます。タイムスタンプとg_hist_idxを使用して一意のオブジェクト名を生成し、複数のシグナルが区別されるようにします。テキストラベルを矢印の上下に配置するための小さな価格オフセットを計算し、矢印は正確に価格に置かれます。チャート上には2つのテキストオブジェクトを作成します。テキストラベル用と矢印グリフ用です。EAは簡単な矢印グリフの選択をサポートし、フォント、色、Zオーダー、選択不可設定をおこなうことで、これらの注釈がトレーダーの手動チャート作業を妨げないようにします。シグナル作成後、PruneOldSignalsを呼び出し、チャート上に過剰なマーカーが残らないようにします。
PruneOldSignalsは実用的なハウスキーピング機能です。チャート上のすべてのオブジェクトを列挙し、EAの接頭辞+SIG_で始まるオブジェクトを選択します。オブジェクト名に埋め込まれたタイムスタンプを抽出し、タイムスタンプ順にソート(最小値を繰り返し削除することで実装)して、古いものから削除し、keepで指定された数だけ残します。これにより、最新のInpMaxSignalsToKeep個のマーカーのみが残り、チャートの散乱を防ぎ、オブジェクト列挙も適切なkeep値で低コストに維持されます。
void DrawSignalOnChart(int zone, double score) { datetime tm = iTime(Symbol(), InpTFPrimary, 1); double price = iClose(Symbol(), InpTFPrimary, 1); if(tm == 0 || price == 0.0) return; double point = SymbolInfoDouble(Symbol(), SYMBOL_POINT); double textOffsetPts = MathMax(1, InpArrowOffsetPoints); double textOffset = point * textOffsetPts * 1.5; string name = g_prefix + "SIG_" + IntegerToString((int)tm) + "_" + IntegerToString(g_hist_idx); string arrName = g_prefix + "SIG_ARR_" + IntegerToString((int)tm) + "_" + IntegerToString(g_hist_idx); if(ObjectFind(g_chart_id, name) >= 0) ObjectDelete(g_chart_id, name); if(ObjectFind(g_chart_id, arrName) >= 0) ObjectDelete(g_chart_id, arrName); double text_price = (zone == 1) ? price + textOffset : price - textOffset; if(ObjectCreate(g_chart_id, name, OBJ_TEXT, 0, tm, text_price)) { string txt = (zone == 1) ? "BUY" : ((zone == -1) ? "SELL" : "NEUTRAL"); color col = (zone == 1) ? clrLime : ((zone == -1) ? clrRed : clrSilver); ObjectSetString(g_chart_id, name, OBJPROP_TEXT, txt); ObjectSetInteger(g_chart_id, name, OBJPROP_COLOR, (int)col); ObjectSetInteger(g_chart_id, name, OBJPROP_FONTSIZE, 12); ObjectSetString(g_chart_id, name, OBJPROP_FONT, InpFont); ObjectSetInteger(g_chart_id, name, OBJPROP_SELECTABLE, false); ObjectSetInteger(g_chart_id, name, OBJPROP_ZORDER, 2); } if(InpShowArrows) { string arrowTxt = (zone == 1) ? "▲" : ((zone == -1) ? "v" : "■"); if(ObjectCreate(g_chart_id, arrName, OBJ_TEXT, 0, tm, price)) { color acol = (zone == 1) ? clrLime : ((zone == -1) ? clrRed : clrSilver); ObjectSetString(g_chart_id, arrName, OBJPROP_TEXT, arrowTxt); ObjectSetInteger(g_chart_id, arrName, OBJPROP_COLOR, (int)acol); ObjectSetInteger(g_chart_id, arrName, OBJPROP_FONTSIZE, InpArrowFontSize); ObjectSetString(g_chart_id, arrName, OBJPROP_FONT, InpFont); ObjectSetInteger(g_chart_id, arrName, OBJPROP_SELECTABLE, false); ObjectSetInteger(g_chart_id, arrName, OBJPROP_ZORDER, 3); } } PruneOldSignals(InpMaxSignalsToKeep); } void PruneOldSignals(int keep) { if(keep <= 0) return; int total = ObjectsTotal(g_chart_id); string names[]; int times[]; int n = 0; string sigPrefix = g_prefix + "SIG_"; for(int i = 0; i < total; i++) { string nm = ObjectName(g_chart_id, i); if(StringFind(nm, sigPrefix) == 0) { string rest = StringSubstr(nm, StringLen(sigPrefix)); int pos = StringFind(rest, "_"); if(pos > 0) { string tsStr = StringSubstr(rest, 0, pos); int ts = (int)StringToInteger(tsStr); ArrayResize(names, n+1); ArrayResize(times, n+1); names[n] = nm; times[n] = ts; n++; } } } if(n <= keep) return; while(n > keep) { int minIdx = 0; for(int j = 1; j < n; j++) if(times[j] < times[minIdx]) minIdx = j; ObjectDelete(g_chart_id, names[minIdx]); for(int k = minIdx; k < n-1; k++) { names[k] = names[k+1]; times[k] = times[k+1]; } ArrayResize(names, n-1); ArrayResize(times, n-1); n--; } }
最後に、EAには複数の防御的かつUX重視の設計選択が含まれています。オブジェクト名の一意接頭辞による衝突回避、インジケーターハンドルが失敗した場合のATR計算のフォールバック、融合スコアを固定範囲に制限、ノイズを減らすための設定可能なアルファによる平滑化、層状のシグナル受理(反転の閾値、符号反転の大きさ、保持、モメンタム、ゾーンエントリ)などです。UIコードでは、ゾーンバッジや最新テキスト配置時の衝突回避がおこなわれており、ヒストグラムはグラフィックではなくテキストブロックを使用することで、軽量かつ古いMetaTrader 5ビルドとの互換性も確保されています。
テストと結果
このセクションでは、EAのテスト結果を確認します。バックテストの結果とライブパフォーマンス指標の両方を扱います。
以下は、Crash 300 IndexでのバックテストのGIFです。ダッシュボードパネルはSTMの状態をコンパクトかつ監査しやすくまとめたものです。最上行にはツール名、銘柄(Crash 300 Index)、およびアクティブ時間足(M5)が表示されます。その下には、大きく色分けされた数値によるセンチメントスコアがあり、平滑化された市場の傾きが−100〜+100スケールで示されます。さらに短いバイアスラベル(例:BULL、BEAR、NEUTRAL)があり、即座に定性的な傾向を把握できます。スコアの直下には最新シグナル(例:「Bias: Long BUY @ 16:30」)が表示され、方向と正確なバーのタイムスタンプを検証できます。水平の強度バーがマグニチュードを可視化し、コンパクトなRecent行では最新ローソク足の指標を示し、スコアの文脈を理解できます。すべての要素は確定バーにアンカーされているため、すべてのアラートやラベルは正確な時間と価格に紐づきます。

Crash 1000 Indexでのライブテストでは、EAがBUYシグナルを発行し、確定バーのタイムスタンプと照合したところ、短期保有で正しいことが確認できました。ダッシュボードには明確なポジティブ傾きが表示され、BUYマーカーは正確なバーのクローズ価格にアンカーされていたため、動きとタイミングをチャート上で完全に監査可能です。この結果は、STMが耐久性のある短期ブルバイアスを検出できる能力を示しています。平滑化とモメンタムチェックによりノイズがフィルタリングされ、早期のホイップソーを回避できました。一方で、ポジションサイズ、ストップ、利確目標などのトレード管理はトレーダーに委ねられます。
- Crash 300 Indexのライブテスト
結論
Sentiment Tilt Meter (STM)まさに設計どおりに機能しました。Crash 300およびCrash 1000での運用中に、短期的なバイアスを明確かつ信頼性高く示しました。シグナルは検証しやすく、すべての矢印やラベルは確定バーと価格に紐づいているため、何がいつ起こったかを推測せずに確認できます。平滑化とモメンタムチェックにより、微小な値動きでメーターが揺れ動くことを防ぎ、時間足ごとの重み付けにより、自分のスタイルにとって重要な視野を優先することができます。
このツールはシグナル生成器兼記録ツールであり、完全な取引システムではありません。市場が傾いているタイミングを示してくれますが、ポジションサイズの設定、ストップの設定、エグジット管理はおこないません。結果は銘柄や取引セッションによって異なります。Crashインデックスでは良好な挙動が確認されましたが、FX、株式、先物などでは異なるチューニングが必要になる場合があります。
STMはフィルタまたは確認層として使用してください。シグナルをCSVに記録し、複数銘柄でのバックテストや短期フォワードテストをおこなったうえで、自動エントリーの検討をおこなうのが望ましいです。実際にシグナルからエントリーする場合は、単純なリスク計画(固定R、明確なストップルール、高い時間足での確認)で包み、一度のノイズの多いセッションで月間の優位性を損なわないよう注意してください。
要するに、STMは短期市場の傾きを明確で監査可能な形で提供します。意思決定を迅速化し、規律を維持できます。実際に資金を運用する前に、リスク管理と検証ステップと組み合わせて使用してください。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/19137
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
古典的な戦略を再構築する(第15回):デイリーブレイクアウト取引戦略
MQLを使用したFirebaseでのCRUD操作
知っておくべきMQL5ウィザードのテクニック(第79回):教師あり学習でのゲーターオシレーターとA/Dオシレーターの使用
MQL5で自己最適化エキスパートアドバイザーを構築する(第12回):行列分解を用いた線形分類器の構築
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索