MQL5でカスタムインジケータを作成する(第8回):出来高統合によるより深いマーケットプロファイル分析
はじめに
第7回では、MetaQuotes Language 5 (MQL5)を使用して、ハイブリッドTime Price Opportunity (TPO)マーケットプロファイル指標を開発しました。この指標は、タイムゾーン調整に対応した日中、日次、週次、月次、および固定期間を含む複数のセッション時間足をサポートしていました。また、価格をグリッドへ量子化し、セッションごとの高値、安値、始値、終値データを追跡し、TPOカウントからポイントオブコントロール(PoC)とバリューエリア(VA)を計算しました。さらに、詳細なセッション分析を可能にするため、カスタマイズ可能な色設定によるチャート上への視覚的な描画機能も実装しました。
第8回となる今回は、この指標に出来高統合機能を追加し、より深いマーケットプロファイル分析を可能にします。これには、出来高ベースのPoC、VA、出来高加重平均価格(VWAP)計算、およびハイライト機能が含まれます。今回のアップグレードでは、イニシャルバランス(IB)検出、重要レベルの延長ライン、分割プロファイル、四角形や円形などの代替TPO文字、境界線、固定範囲用の背景矩形、動的な出来高ラベルなどの高度な機能を追加します。これらの機能は、時間足全体で柔軟性を維持しながら利用できます。本記事では以下のトピックを扱います。
記事を読み終える頃には、出来高強化分析に対応した高度なMQL5ハイブリッドTPOマーケットプロファイル指標が完成し、カスタマイズ可能な状態で利用できるようになります。それでは始めましょう。
ハイブリッドTPOマーケットプロファイルへの出来高と高度な機能の統合
ハイブリッドTPOマーケットプロファイルへ出来高データを統合することで、従来の価格と時間による分析に、各価格レベルでの取引出来高情報を加えることができます。これにより、最も高い出来高が発生した価格レベルを示す出来高ベースのPoCを特定できるようになり、単純な滞在時間ではなく、実際の市場参加状況を反映したVAの調整が可能になります。この強化により、市場参加の強さや確度に関する示唆を得ることができます。例えば、高出来高の適正価格ゾーンを潜在的なサポートやレジスタンスとして識別したり、低出来高領域を関心の弱いエリアやブレイクアウトの可能性がある領域として判断したりできます。また、VWAPのような機能は、イントラデイの方向感を判断するための動的な基準を提供します。
今回は、コアとなるセッション管理機能を拡張し、各価格レベルでの出来高累積処理を追加します。さらに、出来高PoCとVWAPをオプションで計算できるようにし、初期期間からIBを検出します。また、高値、安値、VA、ミッドポイントなどの重要レベルに対する延長ライン描画、文字形式の変更、始値のマーキングオプションを追加します。さらに、より明確なアルファベット表示を実現するため、余白付きの分割表示を有効化し、固定範囲背景や出来高ラベルにも対応します。以下に、目標を視覚的に示した図を掲載します。

MQL5での実装
拡張機能の実装を開始するために、まず、新たな出来高関連指標・計算項目に対応できるよう、列挙型、入力パラメータ、およびグローバル変数を拡張します。
//+------------------------------------------------------------------+ //| Hybrid TPO Market Profile PART 2.mq5 | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" #property version "1.00" #property strict #property indicator_chart_window #property indicator_buffers 0 #property indicator_plots 0 //+------------------------------------------------------------------+ //| Enums | //+------------------------------------------------------------------+ enum MarketProfileTimeframe { // Define market profile timeframe enum INTRADAY, // Intraday DAILY, // Daily WEEKLY, // Weekly MONTHLY, // Monthly FIXED // Fixed }; enum TpoCharacterType { // Define TPO character type enum SQUARE, // ■ Square CIRCLE, // ● Circle ALPHABETIC // A-Za-z }; enum MidpointAlgorithm { // Define midpoint algorithm enum HIGH_LOW_MID, // High/Low mid TPO_COUNT_BASED // Number of TPOs }; enum MarkPeriodOpens { // Define mark period opens enum NONE, // No SWAP_CASE, // Swap case USE_ZERO // Use '0' }; enum TextSize { // Define text size enum TINY, // Tiny SMALL, // Small NORMAL // Normal }; //+------------------------------------------------------------------+ //| Inputs | //+------------------------------------------------------------------+ sinput group "Settings" input double ticksPerTpoLetter = 10; // Ticks per letter input bool highlightVolumeProfilePoc = true; // Highlight POC based on VP? input bool useVolumeProfilePocForValueArea = true; // Use VP POC for Value Area? input bool highlightSessionVwap = false; // Highlight session VWAP? input bool showExtensionLines = false; // Show extension lines? input bool splitProfile = false; // Split MP? sinput group "Time" input MarketProfileTimeframe profileTimeframe = DAILY; // Timeframe input string timezone = "Exchange"; // Timezone input string dailySessionRange = "0830-1500"; // Daily session input int intradayProfileLengthMinutes = 60; // Profile length in minutes (Intraday) input datetime fixedTimeRangeStart = D'2026.02.01 08:30'; // From (Fixed) input datetime fixedTimeRangeEnd = D'2026.02.02 15:00'; // Till (Fixed) input bool renderVolumes = true; // Render volume numbers? (Fixed) sinput group "Rendering" input TpoCharacterType tpoCharacterType = SQUARE; // TPO characters input int valueAreaPercent = 70; // Value Area Percent input int initialBalancePeriods = 2; // IB periods input int initialBalanceLineWidth = 2; // IB line width input int priceMarkerWidth = 2; // Price marker width input int priceMarkerLength = 1; // Price marker length input TextSize textSize = NORMAL; // Font size input MarkPeriodOpens markPeriodOpens = NONE; // Mark period open? input MidpointAlgorithm midpointAlgorithm = HIGH_LOW_MID; // Midpoint algo sinput group "Colors" input color defaultTpoColor = clrGray; // Default input color singlePrintColor = 0xd56a6a; // Single Print input color valueAreaColor = clrBlack; // Value Area input color pointOfControlColor = 0x3f7cff; // POC input color volumeProfilePocColor = 0x87c74c; // VP POC input color openColor = clrDodgerBlue; // Open input color closeColor = clrRed; // Close input color initialBalanceHighlightColor = clrDodgerBlue; // IB input color initialBalanceBackgroundColor = 0x606D79; // IB background input color sessionVwapColor = 0xFF9925; // Session VWAP input color pocExtensionColor = 0x87c74c; // POC extension input color valueAreaHighExtensionColor = clrBlack; // VAH extension input color valueAreaLowExtensionColor = clrBlack; // VAL extension input color highExtensionColor = clrRed; // High extension input color lowExtensionColor = clrGreen; // Low extension input color midpointExtensionColor = 0x7649ff; // Midpoint extension input color fixedRangeBackgroundColor = 0x3179f5; // Fixed range background //+------------------------------------------------------------------+ //| Constants | //+------------------------------------------------------------------+ #define MAX_BARS_BACK 5000 #define TPO_CHARACTERS_STRING "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz" //+------------------------------------------------------------------+ //| Structures | //+------------------------------------------------------------------+ struct TpoPriceLevel { // Define TPO price level structure double price; // Store price level string tpoString; // Store TPO string int tpoCount; // Store TPO count double volume; // Store volume }; struct ProfileSessionData { // Define profile session data structure datetime startTime; // Store start time datetime endTime; // Store end time double sessionOpen; // Store session open price double sessionClose; // Store session close price double sessionHigh; // Store session high price double sessionLow; // Store session low price double initialBalanceHigh;// Store initial balance high double initialBalanceLow; // Store initial balance low double vwap; // Store VWAP int pointOfControlIndex; // Store point of control index int volumeProfilePocIndex;// Store volume profile POC index TpoPriceLevel levels[]; // Store array of price levels int periodCount; // Store period count double periodHighs[]; // Store array of period highs double periodLows[]; // Store array of period lows double periodOpens[]; // Store array of period opens double volumeProfilePrices[]; // Store array of volume profile prices double volumeProfileVolumes[]; // Store array of volume profile volumes }; //+------------------------------------------------------------------+ //| Global Variables | //+------------------------------------------------------------------+ string objectPrefix = "HTMP_"; //--- Set object prefix ProfileSessionData sessions[]; //--- Declare sessions array int activeSessionIndex = -1; //--- Initialize active session index double tpoPriceGridStep = 0; //--- Initialize TPO price grid step string tpoCharacterSet[]; //--- Declare TPO character set array datetime previousBarTime = 0; //--- Initialize previous bar time datetime lastCompletedBarTime = 0; //--- Initialize last completed bar time bool isNewSession = false; //--- Initialize new session flag int labelFontSize = 10; //--- Set label font size int maxSessionHistory = 20; //--- Set maximum session history int timezoneOffsetSeconds = 0; //--- Initialize timezone offset in seconds
実装の開始にあたり、拡張版ハイブリッドTPOマーケットプロファイルの設定項目を広げるため、追加の列挙型を定義します。TpoCharacterType列挙型では、異なる表示スタイルに対応するための選択肢を提供します。SQUAREはブロック記号、CIRCLEはドット表示、ALPHABETICは文字ベースのラベル表示を使用します。これにより、従来の単一のラベル表示だけでなく、複数の視覚表現を選べるようになります。次に、MidpointAlgorithmでは、ミッドポイント計算用のアルゴリズムを提供します。HIGH_LOW_MIDはセッション範囲を基準に計算し、TPO_COUNT_BASEDは累積されたTPOカウントを使用して計算します。MarkPeriodOpensには、始値マーキング用のオプションを追加します。NONEはマーキングを無効化し、SWAP_CASEは始値を示すために文字の大文字・小文字を切り替え、USE_ZEROはゼロ文字を使用して始値を示します。最後に、TextSizeでは、TINY、SMALL、NORMALの設定を使用して文字サイズを調整できるようにします。
入力設定については、各セクションごとにグループ化されたユーザー入力を拡張します。新しい設定として、出来高ベースのPoC機能を切り替えるためにhighlightVolumeProfilePocとuseVolumeProfilePocForValueAreaを追加します。また、VWAPを強調表示するhighlightSessionVwap、重要レベルを延長表示するshowExtensionLines、分割されたアルファベット表示をおこなうsplitProfileも追加します。時間設定グループには、固定範囲の出来高ラベル表示を制御するrenderVolumesを追加します。描画設定では、TPO文字タイプ、IB期間、ライン幅、価格マーカーのサイズ、文字サイズ、期間始値マーキング、ミッドポイント計算方法の選択項目を追加します。カラー設定グループでは、出来高プロファイルPoC、始値マーカー、IBのハイライトおよび背景、セッションVWAP、各種延長ライン、固定範囲背景用のカラー入力を追加して拡張します。
構造体についても、出来高統合に対応するため更新します。TpoPriceLevelには、価格、TPO文字列、カウントに加えて出来高フィールドを追加します。ProfileSessionDataには、IBの高値と安値、VWAP、出来高プロファイルPoCのインデックス、期間ごとの高値・安値配列、さらに出来高プロファイル用の価格配列と出来高配列を追加します。グローバル変数もそれに合わせて調整します。isNewSessionをフラグとして追加し、後ほど動的に設定するlabelFontSizeを導入します。一方で、オブジェクト接頭辞、セッション配列、アクティブインデックス、グリッド幅、文字セット、タイムスタンプ、履歴制限、タイムゾーンオフセットなどの既存要素は維持します。次に、初期化イベントハンドラを更新し、以下のようにフォントサイズを設定します。変更箇所を明確にするため、該当部分をハイライトしています。
//+------------------------------------------------------------------+ //| Initialize custom indicator | //+------------------------------------------------------------------+ int OnInit() { IndicatorSetString(INDICATOR_SHORTNAME, "Hybrid TPO Market Profile"); //--- Set indicator short name tpoPriceGridStep = ticksPerTpoLetter * _Point; //--- Calculate TPO price grid step ArrayResize(tpoCharacterSet, 52); //--- Resize TPO character set array for(int i = 0; i < 52; i++) { //--- Loop through characters tpoCharacterSet[i] = StringSubstr(TPO_CHARACTERS_STRING, i, 1); //--- Assign character to array } switch(textSize) { //--- Switch on text size case TINY: labelFontSize = 7; break; //--- Set tiny font size case SMALL: labelFontSize = 9; break; //--- Set small font size case NORMAL: labelFontSize = 11; break; //--- Set normal font size } if(timezone != "Exchange") { //--- Check if timezone is not exchange string tzString = StringSubstr(timezone, 3); //--- Extract timezone string int offset = (int)StringToInteger(tzString); //--- Convert offset to integer timezoneOffsetSeconds = offset * 3600; //--- Calculate timezone offset in seconds } ArrayResize(sessions, 0); //--- Resize sessions array to zero return(INIT_SUCCEEDED); //--- Return initialization success }
ここでフォントサイズを設定することで、異なるチャート上でも視認性を向上させるために文字サイズを調整できるようになります。次におこなうことは、セッション作成関数を調整して、新しい保存用配列に対応し、古いセッションを削除できるようにすることです。これにより、より簡単に管理できるようになります。
//+------------------------------------------------------------------+ //| Delete session objects | //+------------------------------------------------------------------+ void DeleteSessionObjects(datetime sessionTime) { string sessionString = IntegerToString(sessionTime); //--- Convert session time to string int total = ObjectsTotal(0, 0, -1); //--- Get total number of objects for(int i = total - 1; i >= 0; i--) { //--- Loop through objects in reverse string name = ObjectName(0, i, 0, -1); //--- Get object name if(StringFind(name, objectPrefix) == 0 && StringFind(name, sessionString) > 0) //--- Check if matches session ObjectDelete(0, name); //--- Delete object } } //+------------------------------------------------------------------+ //| Create new session | //+------------------------------------------------------------------+ int CreateNewSession() { int size = ArraySize(sessions); //--- Get size of sessions array if(size >= maxSessionHistory) { //--- Check if size exceeds history limit DeleteSessionObjects(sessions[0].startTime); //--- Delete old session objects for(int i = 0; i < size - 1; i++) { //--- Loop to shift sessions sessions[i] = sessions[i + 1]; //--- Copy next session to current } ArrayResize(sessions, size - 1); //--- Resize sessions array size = size - 1; //--- Update size } ArrayResize(sessions, size + 1); //--- Resize sessions array for new session int newIndex = size; //--- Set new index sessions[newIndex].startTime = 0; //--- Initialize start time sessions[newIndex].endTime = 0; //--- Initialize end time sessions[newIndex].sessionOpen = 0; //--- Initialize session open sessions[newIndex].sessionClose = 0; //--- Initialize session close sessions[newIndex].sessionHigh = 0; //--- Initialize session high sessions[newIndex].sessionLow = 0; //--- Initialize session low sessions[newIndex].initialBalanceHigh = 0; //--- Initialize initial balance high sessions[newIndex].initialBalanceLow = 0; //--- Initialize initial balance low sessions[newIndex].vwap = 0; //--- Initialize VWAP sessions[newIndex].pointOfControlIndex = -1; //--- Initialize point of control index sessions[newIndex].volumeProfilePocIndex = -1; //--- Initialize volume profile POC index sessions[newIndex].periodCount = 0; //--- Initialize period count ArrayResize(sessions[newIndex].levels, 0); //--- Resize levels array ArrayResize(sessions[newIndex].periodHighs, 0);//--- Resize period highs array ArrayResize(sessions[newIndex].periodLows, 0); //--- Resize period lows array ArrayResize(sessions[newIndex].periodOpens, 0);//--- Resize period opens array ArrayResize(sessions[newIndex].volumeProfilePrices, 0); //--- Resize volume profile prices array ArrayResize(sessions[newIndex].volumeProfileVolumes, 0); //--- Resize volume profile volumes array return newIndex; //--- Return new index }
まず、特定のセッションに関連付けられたチャートオブジェクトを削除するために、DeleteSessionObjects関数を追加します。この関数では、セッション時間をIntegerToStringを使用して文字列へ変換します。その後、ObjectsTotalで取得したすべてのオブジェクトを後方からループ処理し、ObjectNameを使用してオブジェクト名を取得します。そして、StringFind関数を使用して、オブジェクト名が接頭辞で始まり、かつセッション文字列を含んでいるかを確認し、条件に一致するものを削除します。
次に、新しいプロファイルを管理するためにCreateNewSession関数を拡張します。まず、ArraySizeを使用して現在のセッション配列のサイズを取得します。履歴制限に達している場合は、最も古いセッションの開始時間を引数としてDeleteSessionObjectsを呼び出し、表示されているオブジェクトを削除します。その後、残りのセッションを前方へ移動させ、配列サイズを縮小します。続いて、新しいエントリー用に配列を拡張し、各フィールドを初期化します。これには、IB、高値・安値、VWAP、出来高プロファイルPoCのインデックスなどの新しい項目が含まれます。また、カウントやインデックス値をリセットします。さらに、期間ごとの高値配列、安値配列、出来高プロファイル価格配列、出来高配列などの追加配列サイズを調整します。これらの処理が完了した後、新しく作成されたセッションのインデックスを返します。次に、期間開始を示すために使用する文字の大文字・小文字を変更するヘルパー関数を追加します。
//+------------------------------------------------------------------+ //| Convert string to upper case | //+------------------------------------------------------------------+ string ConvertToUpperCase(string str) { string result = str; //--- Copy string StringToUpper(result); //--- Convert to upper case return result; //--- Return result } //+------------------------------------------------------------------+ //| Convert string to lower case | //+------------------------------------------------------------------+ string ConvertToLowerCase(string str) { string result = str; //--- Copy string StringToLower(result); //--- Convert to lower case return result; //--- Return result } //+------------------------------------------------------------------+ //| Check if character is upper case | //+------------------------------------------------------------------+ bool IsUpperCaseCharacter(string character) { return character == ConvertToUpperCase(character) && character != ConvertToLowerCase(character); //--- Check and return if upper case }
ここでは、文字列の大文字・小文字処理を行うためのユーティリティ関数を追加し、期間開始を示すための大文字・小文字切り替えなどの機能をサポートします。ConvertToUpperCase関数では、入力された文字列をコピーし、StringToUpperを適用して文字列全体を大文字へ変換してから返します。同様に、ConvertToLowerCaseでは、文字列を複製し、StringToLowerを使用して完全な小文字へ変換します。文字の大文字判定をおこなうために、IsUpperCaseCharacter関数では、1つの文字がConvertToUpperCaseによる大文字変換後の文字と一致し、かつConvertToLowerCaseによる小文字変換後の文字とは異なるかを確認します。この条件を満たす場合、その文字が大文字であるとしてtrueを返します。次に、ドット文字や四角形文字の選択肢が追加されたため、レベルへTPO文字を追加する方法を変更します。
//+------------------------------------------------------------------+ //| Add TPO character to level | //+------------------------------------------------------------------+ void AddTpoCharacterToLevel(int sessionIndex, int levelIndex, int periodIndex) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if session index invalid if(levelIndex < 0 || levelIndex >= ArraySize(sessions[sessionIndex].levels)) return; //--- Return if level index invalid string tpoCharacter = ""; //--- Initialize TPO character switch(tpoCharacterType) { //--- Switch on TPO character type case SQUARE: //--- Handle square case tpoCharacter = "■"; //--- Set square character break; //--- Exit case case CIRCLE: //--- Handle circle case tpoCharacter = "●"; //--- Set circle character break; //--- Exit case case ALPHABETIC: //--- Handle alphabetic case tpoCharacter = tpoCharacterSet[periodIndex % 52]; //--- Get alphabetic character if(markPeriodOpens != NONE && periodIndex < ArraySize(sessions[sessionIndex].periodOpens)) { //--- Check if mark opens and valid index double periodOpen = sessions[sessionIndex].periodOpens[periodIndex]; //--- Get period open double levelPrice = sessions[sessionIndex].levels[levelIndex].price; //--- Get level price if(MathAbs(levelPrice - periodOpen) < tpoPriceGridStep / 2) { //--- Check if matches open if(markPeriodOpens == SWAP_CASE) { //--- Check swap case if(IsUpperCaseCharacter(tpoCharacter)) //--- Check if upper tpoCharacter = ConvertToLowerCase(tpoCharacter); //--- Convert to lower else //--- Handle lower tpoCharacter = ConvertToUpperCase(tpoCharacter); //--- Convert to upper } else if(markPeriodOpens == USE_ZERO) { //--- Check use zero tpoCharacter = "0"; //--- Set zero character } } } break; //--- Exit case } sessions[sessionIndex].levels[levelIndex].tpoString += tpoCharacter; //--- Append character to TPO string sessions[sessionIndex].levels[levelIndex].tpoCount++; //--- Increment TPO count }
ここでは、カスタマイズ可能なTPO表現と始値マーキング機能を組み込むために、AddTpoCharacterToLevel関数を拡張します。まず、ArraySizeを使用してセッションとレベルのインデックスを検証し、無効な場合は早期に処理を終了します。次に、空のTPO文字列を初期化し、tpoCharacterType列挙型を使用して分岐を処理します。SQUAREまたはCIRCLEの場合は、ブロック記号やドットなどの固定シンボルを割り当てます。ALPHABETICの場合は、期間インデックスに対して52の剰余演算を使用し、文字セットから対応する文字を選択します。文字については、Wingdings記号を使用するよりも描画結果が良かったため、固定文字を使用する方法を採用しました。ただし、動的性を持たせるためにWingdings記号を使用することも可能です。
アルファベット形式で、かつmarkPeriodOpensがNONEではなく、期間開始配列のサイズが有効な場合は、レベル価格と期間開始価格を比較します。この判定では、MathAbsを使用し、グリッド幅の半分を許容範囲として一致を確認します。一致した場合は、SWAP_CASEではIsUpperCaseCharacterを使用して現在の文字が大文字かを確認し、ConvertToLowerCaseまたはConvertToUpperCaseによって大文字・小文字を切り替えます。USE_ZEROの場合は、文字をゼロに変更します。最後に、生成した文字をレベルの文字列へ追加し、TPOのカウントを増加させてプロファイルを構築します。次に、分割プロファイルが有効な場合にTPO文字列へスペースを追加するための別のヘルパー関数を定義します。これにより、アルファベットを使用した場合に期間ごとの区切りを明確にした分割表示を作成できます。
//+------------------------------------------------------------------+ //| Pad levels for split profile | //+------------------------------------------------------------------+ void PadLevelsForSplitProfile(int sessionIndex) { if(!splitProfile || tpoCharacterType != ALPHABETIC) return; //--- Return if not split or not alphabetic if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid int periodCount = sessions[sessionIndex].periodCount; //--- Get period count int levelCount = ArraySize(sessions[sessionIndex].levels); //--- Get level count for(int i = 0; i < levelCount; i++) { //--- Loop through levels int currentLength = StringLen(sessions[sessionIndex].levels[i].tpoString); //--- Get current length if(currentLength < periodCount) { //--- Check if needs padding for(int j = currentLength; j < periodCount; j++) { //--- Loop to pad sessions[sessionIndex].levels[i].tpoString += " "; //--- Add space } } } }
PadLevelsForSplitProfile関数を追加し、分割プロファイル表示用にアルファベット形式のTPO文字列を準備します。まず、分割モードが無効、または文字タイプがアルファベット形式ではない場合は処理を終了します。また、セッションインデックスがセッション配列のサイズに対して無効な場合も早期に終了します。次に、期間数とレベル数を取得します。その後、各レベルをループ処理し、StringLenを使用して現在の文字列長を確認します。文字列の長さが期間数より短い場合は、内部ループでスペースを追加し、文字列の長さを調整します。これにより、すべての文字列が同じ長さに揃えられ、描画されたプロファイル上で期間ごとの区切りが明確になるよう、一定の間隔を確保できます。次に、レベル情報から出来高プロファイルを作成し、出来高ベースのPoCを検出するための関数を構築します。
//+------------------------------------------------------------------+ //| Build volume profile and find POC | //+------------------------------------------------------------------+ void BuildVolumeProfileAndFindPoc(int sessionIndex) { if(!highlightVolumeProfilePoc) { //--- Check if not highlight VP POC if(sessionIndex >= 0 && sessionIndex < ArraySize(sessions)) //--- Check valid session sessions[sessionIndex].volumeProfilePocIndex = -1; //--- Reset VP POC index return; //--- Return } if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid if(sessions[sessionIndex].startTime == 0) return; //--- Return if no start time ENUM_TIMEFRAMES lowerTimeframe = PERIOD_M1; //--- Set default lower timeframe int currentTimeframeSeconds = PeriodSeconds(_Period); //--- Get current timeframe seconds if(currentTimeframeSeconds < 30 * 60) { //--- Check for M1 lower lowerTimeframe = PERIOD_M1; //--- Set M1 } else if(currentTimeframeSeconds < 60 * 60) { //--- Check for M3 lower lowerTimeframe = PERIOD_M3; //--- Set M3 } else if(currentTimeframeSeconds == 60 * 60) {//--- Check for M10 lower lowerTimeframe = PERIOD_M10; //--- Set M10 } else { //--- Default to H1 lowerTimeframe = PERIOD_H1; //--- Set H1 } if(PeriodSeconds(_Period) <= 60) { //--- Check for 1-minute timeframe int size = ArraySize(sessions[sessionIndex].levels); //--- Get levels size for(int i = 0; i < size; i++) { //--- Loop through levels int volumeProfileIndex = -1; //--- Initialize VP index for(int j = 0; j < ArraySize(sessions[sessionIndex].volumeProfilePrices); j++) { //--- Loop through VP prices if(MathAbs(sessions[sessionIndex].volumeProfilePrices[j] - sessions[sessionIndex].levels[i].price) < _Point / 2) { //--- Check match volumeProfileIndex = j; //--- Set index break; //--- Exit loop } } if(volumeProfileIndex == -1) { //--- Check if new int volumeProfileSize = ArraySize(sessions[sessionIndex].volumeProfilePrices); //--- Get VP size ArrayResize(sessions[sessionIndex].volumeProfilePrices, volumeProfileSize + 1); //--- Resize prices ArrayResize(sessions[sessionIndex].volumeProfileVolumes, volumeProfileSize + 1); //--- Resize volumes sessions[sessionIndex].volumeProfilePrices[volumeProfileSize] = sessions[sessionIndex].levels[i].price; //--- Set price sessions[sessionIndex].volumeProfileVolumes[volumeProfileSize] = sessions[sessionIndex].levels[i].volume; //--- Set volume } else { //--- Handle existing sessions[sessionIndex].volumeProfileVolumes[volumeProfileIndex] += sessions[sessionIndex].levels[i].volume; //--- Add volume } } } else { //--- Handle higher timeframes datetime sessionEnd = (sessions[sessionIndex].endTime > 0) ? sessions[sessionIndex].endTime : TimeCurrent(); //--- Get session end int startBar = iBarShift(_Symbol, lowerTimeframe, sessions[sessionIndex].startTime); //--- Get start bar int endBar = iBarShift(_Symbol, lowerTimeframe, sessionEnd); //--- Get end bar if(startBar < 0 || endBar < 0) return; //--- Return if invalid bars int barCount = startBar - endBar + 1; //--- Calculate bar count if(barCount <= 0) return; //--- Return if no bars double highs[], lows[]; //--- Declare highs and lows arrays long volumes[]; //--- Declare volumes array ArraySetAsSeries(highs, true); //--- Set highs as series ArraySetAsSeries(lows, true); //--- Set lows as series ArraySetAsSeries(volumes, true); //--- Set volumes as series if(CopyHigh(_Symbol, lowerTimeframe, endBar, barCount, highs) <= 0) return; //--- Copy highs if(CopyLow(_Symbol, lowerTimeframe, endBar, barCount, lows) <= 0) return; //--- Copy lows if(CopyTickVolume(_Symbol, lowerTimeframe, endBar, barCount, volumes) <= 0) return; //--- Copy volumes for(int i = 0; i < barCount; i++) { //--- Loop through bars double quantizedHigh = QuantizePriceToGrid(highs[i]); //--- Quantize high double quantizedLow = QuantizePriceToGrid(lows[i]); //--- Quantize low int priceCount = (int)MathMax(1, (quantizedHigh - quantizedLow) / tpoPriceGridStep + 1); //--- Calculate price count double volumePerLevel = (double)volumes[i] / priceCount; //--- Calculate volume per level for(double price = quantizedLow; price <= quantizedHigh; price += tpoPriceGridStep) { //--- Loop through prices int volumeProfileIndex = -1; //--- Initialize VP index for(int j = 0; j < ArraySize(sessions[sessionIndex].volumeProfilePrices); j++) { //--- Loop through VP prices if(MathAbs(sessions[sessionIndex].volumeProfilePrices[j] - price) < _Point / 2) { //--- Check match volumeProfileIndex = j; //--- Set index break; //--- Exit loop } } if(volumeProfileIndex == -1) { //--- Check if new int volumeProfileSize = ArraySize(sessions[sessionIndex].volumeProfilePrices); //--- Get VP size ArrayResize(sessions[sessionIndex].volumeProfilePrices, volumeProfileSize + 1); //--- Resize prices ArrayResize(sessions[sessionIndex].volumeProfileVolumes, volumeProfileSize + 1); //--- Resize volumes sessions[sessionIndex].volumeProfilePrices[volumeProfileSize] = price; //--- Set price sessions[sessionIndex].volumeProfileVolumes[volumeProfileSize] = volumePerLevel; //--- Set volume } else { //--- Handle existing sessions[sessionIndex].volumeProfileVolumes[volumeProfileIndex] += volumePerLevel; //--- Add volume } } } } double maxVolume = 0; //--- Initialize max volume double volumeProfilePocPrice = 0; //--- Initialize VP POC price for(int i = 0; i < ArraySize(sessions[sessionIndex].volumeProfileVolumes); i++) { //--- Loop through volumes if(sessions[sessionIndex].volumeProfileVolumes[i] > maxVolume) { //--- Check if max maxVolume = sessions[sessionIndex].volumeProfileVolumes[i]; //--- Update max volumeProfilePocPrice = sessions[sessionIndex].volumeProfilePrices[i]; //--- Update price } } sessions[sessionIndex].volumeProfilePocIndex = -1; //--- Reset VP POC index for(int i = 0; i < ArraySize(sessions[sessionIndex].levels); i++) { //--- Loop through levels if(MathAbs(sessions[sessionIndex].levels[i].price - volumeProfilePocPrice) < tpoPriceGridStep / 2) { //--- Check match sessions[sessionIndex].volumeProfilePocIndex = i; //--- Set index break; //--- Exit loop } } }
BuildVolumeProfileAndFindPoc関数を実装し、出来高ベースのPoCをハイライト表示するための独立した出来高プロファイルを構築します。まず、highlightVolumeProfilePocを確認し、この機能が有効かどうかを判定します。無効の場合は、有効なセッションに対するインデックスをリセットし、早期に処理を終了します。次に、ArraySizeを使用してセッションインデックスを検証し、開始時間が存在することを確認します。続いて、現在の時間足に応じて動的に下位時間足を選択します。PeriodSecondsを使用し、30分未満の場合は PERIOD_M1、1時間未満の場合はPERIOD_M3、ちょうど1時間の場合はPERIOD_M10、それ以外の場合はPERIOD_H1に設定します。これにより、より細かいデータへアクセスし、正確な出来高分布を取得します。現在の時間足が60秒以下の場合は、既存のレベルをループ処理し、出来高プロファイル配列内に一致する価格があるかを確認します。この判定では、MathAbsを使用し、半ポイント分の許容範囲で比較します。一致する価格が見つからない場合は、ArrayResizeで配列サイズを拡張し、価格と出来高を追加します。一致する場合は、既存の出来高へ加算します。
上位足の場合は、セッション終了時間を設定済みの終了時間、またはTimeCurrentから取得します。その後、下位足を基準にiBarShiftを使用して開始バーと終了バーのインデックスを取得します。有効なインデックスで、かつバー数が正の場合は、高値、安値、出来高用の配列を宣言し、時系列配列として設定します。続いて、CopyHigh、CopyLow、CopyTickVolume を使用してデータを取得します。その後、各バーをループ処理し、高値と安値をQuantizePriceToGridでグリッド化します。価格ステップを計算し、最低でも1以上になるよう調整します。バーの出来高は各価格レベルへ均等に分配し、価格範囲をループ処理して、先ほどと同様に許容範囲を使用した比較によって、出来高プロファイル配列へ追加または累積します。
出来高PoCを特定するために、最大出来高とその価格を0で初期化します。その後、出来高配列をスキャンし、最大値を更新します。最後に、インデックスをリセットし、最大出来高となった価格を基準に最も近いレベルのインデックスを許容範囲で検索します。見つかった場合は、そのインデックスを保存し、描画時に出来高ベースのハイライト表示を有効にします。次に、価格レベルの出来高を使用してVWAPを計算します。これは、取引された量を重みとして使用した平均価格を求める処理になります。
//+------------------------------------------------------------------+ //| Calculate session VWAP | //+------------------------------------------------------------------+ void CalculateSessionVwap(int sessionIndex) { if(!highlightSessionVwap) return; //--- Return if not highlight VWAP if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid double sumPriceVolume = 0; //--- Initialize sum price volume double sumVolume = 0; //--- Initialize sum volume int size = ArraySize(sessions[sessionIndex].levels); //--- Get levels size for(int i = 0; i < size; i++) { //--- Loop through levels sumPriceVolume += sessions[sessionIndex].levels[i].price * sessions[sessionIndex].levels[i].volume; //--- Accumulate price volume sumVolume += sessions[sessionIndex].levels[i].volume; //--- Accumulate volume } if(sumVolume > 0) //--- Check if volume positive sessions[sessionIndex].vwap = sumPriceVolume / sumVolume; //--- Calculate VWAP }
セッションのVWAPを求めるために、CalculateSessionVwap関数を実装します。まず、機能が無効の場合、またはセッションインデックスが有効範囲外の場合は、早期に処理を終了します。次に、価格と出来高の積の合計、および総出来高を格納するための累積変数を設定します。その後、各レベルをループ処理し、各価格に出来高を掛けて積の合計へ加算し、出来高も合計します。総出来高が0より大きい場合は、積の合計を出来高の合計で割り、加重平均値を計算してセッションのvwapフィールドへ保存します。これで計算処理を補助するすべてのヘルパー関数が揃いました。残る作業は、データ表示のための視覚的な描画処理です。まず、アルファベット形式の場合に開始TPO文字をハイライト表示する関数を追加します。これは開始文字にスポットライトを当てるような処理です。必須ではありませんが、素早い視覚的な識別を向上させます。
//+------------------------------------------------------------------+ //| Render open TPO highlight | //+------------------------------------------------------------------+ void RenderOpenTpoHighlight(int sessionIndex, int openLevelIndex, string &displayStrings[]) { if(tpoCharacterType != ALPHABETIC) return; //--- Return if not alphabetic if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if session invalid if(openLevelIndex < 0 || openLevelIndex >= ArraySize(sessions[sessionIndex].levels)) return; //--- Return if level invalid string fullString = displayStrings[openLevelIndex]; //--- Get full display string if(StringLen(fullString) == 0) return; //--- Return if empty string string openCharacter = StringSubstr(fullString, 0, 1); //--- Extract open character string remainingCharacters = StringSubstr(fullString, 1); //--- Extract remaining characters string objectName = objectPrefix + "OpenTPO_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create object name int barIndex = iBarShift(_Symbol, _Period, sessions[sessionIndex].startTime); //--- Get bar index if(barIndex < 0) return; //--- Return if invalid bar index datetime labelTime = iTime(_Symbol, _Period, barIndex); //--- Get label time int x, y; //--- Declare coordinates ChartTimePriceToXY(0, 0, labelTime, sessions[sessionIndex].levels[openLevelIndex].price, x, y); //--- Convert to XY if(ObjectFind(0, objectName) < 0) { //--- Check if object not found ObjectCreate(0, objectName, OBJ_LABEL, 0, 0, 0); //--- Create label object } ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, x); //--- Set X distance ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, y); //--- Set Y distance ObjectSetInteger(0, objectName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner ObjectSetInteger(0, objectName, OBJPROP_ANCHOR, ANCHOR_LEFT); //--- Set anchor ObjectSetInteger(0, objectName, OBJPROP_COLOR, openColor); //--- Set color ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, labelFontSize); //--- Set font size ObjectSetString(0, objectName, OBJPROP_FONT, "Arial"); //--- Set font ObjectSetString(0, objectName, OBJPROP_TEXT, openCharacter); //--- Set text ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, objectName, OBJPROP_HIDDEN, true); //--- Set hidden true if(StringLen(remainingCharacters) > 0) { //--- Check if remaining displayStrings[openLevelIndex] = " " + remainingCharacters; //--- Prepend space } else { //--- Handle no remaining displayStrings[openLevelIndex] = " "; //--- Set space } }
ここでは、アルファベットモードで開始TPO文字を視覚的に強調するために、RenderOpenTpoHighlight関数を追加します。文字タイプがアルファベット形式ではない場合、またはセッションと開始レベルのインデックスが配列サイズに対して無効な場合は、早期に処理を終了します。開始レベルの表示文字列を取得し、StringLenにより空の場合は処理を終了します。その後、StringSubstrを使用して最初の文字を開始マーカーとして取得し、残りの文字をremainingとして取得します。IntegerToStringを使用して接頭辞とセッション開始時間を組み合わせたオブジェクト名を作成し、iBarShiftでバーインデックスを取得します。無効な場合は処理を終了します。iTimeを使用してラベル時間を取得し、ChartTimePriceToXYを使用して時間と価格を座標へ変換します。ObjectFindでオブジェクトが存在しない場合は、ObjectCreateとOBJ_LABELを使用してラベルを作成します。
ObjectSetIntegerとObjectSetStringを使用して、位置、コーナー、アンカー、入力された開始色、動的なフォントサイズ、Arialフォント、開始文字テキスト、選択不可および非表示プロパティを設定します。最後に、プロファイル描画時の配置を維持するため、残りの文字がある場合はその先頭にスペースを追加して表示文字列を更新します。残りの文字がない場合は、単一のスペースを設定します。次に、プロファイルの境界線、IBライン、および重要レベルの延長線を描画します。
//+------------------------------------------------------------------+ //| Render profile border line | //+------------------------------------------------------------------+ void RenderProfileBorderLine(int sessionIndex, int barIndex) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid if(sessions[sessionIndex].sessionHigh == 0 || sessions[sessionIndex].sessionLow == 0) return; //--- Return if no high low datetime startTime = iTime(_Symbol, _Period, barIndex); //--- Get start time color backgroundEdgeColor = 0x606D79; //--- Set background edge color color initialBalanceEdgeColor = initialBalanceHighlightColor; //--- Set IB edge color if(initialBalancePeriods > 0 && sessions[sessionIndex].initialBalanceHigh > 0 && sessions[sessionIndex].initialBalanceLow > 0) { //--- Check IB string edge1Name = objectPrefix + "Edge1_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create edge1 name if(ObjectFind(0, edge1Name) < 0) { //--- Check if not found ObjectCreate(0, edge1Name, OBJ_TREND, 0, startTime, sessions[sessionIndex].sessionLow, startTime, sessions[sessionIndex].initialBalanceLow); //--- Create trend ObjectSetInteger(0, edge1Name, OBJPROP_RAY_RIGHT, false); //--- Set ray right false ObjectSetInteger(0, edge1Name, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, edge1Name, OBJPROP_HIDDEN, true); //--- Set hidden true } ObjectSetDouble(0, edge1Name, OBJPROP_PRICE, 0, sessions[sessionIndex].sessionLow); //--- Set price 0 ObjectSetDouble(0, edge1Name, OBJPROP_PRICE, 1, sessions[sessionIndex].initialBalanceLow); //--- Set price 1 ObjectSetInteger(0, edge1Name, OBJPROP_COLOR, backgroundEdgeColor); //--- Set color ObjectSetInteger(0, edge1Name, OBJPROP_WIDTH, 3); //--- Set width ObjectSetInteger(0, edge1Name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style string edge2Name = objectPrefix + "Edge2_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create edge2 name if(ObjectFind(0, edge2Name) < 0) { //--- Check if not found ObjectCreate(0, edge2Name, OBJ_TREND, 0, startTime, sessions[sessionIndex].initialBalanceLow, startTime, sessions[sessionIndex].initialBalanceHigh); //--- Create trend ObjectSetInteger(0, edge2Name, OBJPROP_RAY_RIGHT, false); //--- Set ray right false ObjectSetInteger(0, edge2Name, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, edge2Name, OBJPROP_HIDDEN, true); //--- Set hidden true } ObjectSetDouble(0, edge2Name, OBJPROP_PRICE, 0, sessions[sessionIndex].initialBalanceLow); //--- Set price 0 ObjectSetDouble(0, edge2Name, OBJPROP_PRICE, 1, sessions[sessionIndex].initialBalanceHigh); //--- Set price 1 ObjectSetInteger(0, edge2Name, OBJPROP_COLOR, initialBalanceEdgeColor); //--- Set color ObjectSetInteger(0, edge2Name, OBJPROP_WIDTH, 3); //--- Set width ObjectSetInteger(0, edge2Name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style string edge3Name = objectPrefix + "Edge3_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create edge3 name if(ObjectFind(0, edge3Name) < 0) { //--- Check if not found ObjectCreate(0, edge3Name, OBJ_TREND, 0, startTime, sessions[sessionIndex].initialBalanceHigh, startTime, sessions[sessionIndex].sessionHigh); //--- Create trend ObjectSetInteger(0, edge3Name, OBJPROP_RAY_RIGHT, false); //--- Set ray right false ObjectSetInteger(0, edge3Name, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, edge3Name, OBJPROP_HIDDEN, true); //--- Set hidden true } ObjectSetDouble(0, edge3Name, OBJPROP_PRICE, 0, sessions[sessionIndex].initialBalanceHigh); //--- Set price 0 ObjectSetDouble(0, edge3Name, OBJPROP_PRICE, 1, sessions[sessionIndex].sessionHigh); //--- Set price 1 ObjectSetInteger(0, edge3Name, OBJPROP_COLOR, backgroundEdgeColor); //--- Set color ObjectSetInteger(0, edge3Name, OBJPROP_WIDTH, 3); //--- Set width ObjectSetInteger(0, edge3Name, OBJPROP_STYLE, STYLE_SOLID); //--- Set style } else { //--- Handle no IB string edgeName = objectPrefix + "Edge_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create edge name if(ObjectFind(0, edgeName) < 0) { //--- Check if not found ObjectCreate(0, edgeName, OBJ_TREND, 0, startTime, sessions[sessionIndex].sessionLow, startTime, sessions[sessionIndex].sessionHigh); //--- Create trend ObjectSetInteger(0, edgeName, OBJPROP_RAY_RIGHT, false); //--- Set ray right false ObjectSetInteger(0, edgeName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, edgeName, OBJPROP_HIDDEN, true); //--- Set hidden true } ObjectSetDouble(0, edgeName, OBJPROP_PRICE, 0, sessions[sessionIndex].sessionLow); //--- Set price 0 ObjectSetDouble(0, edgeName, OBJPROP_PRICE, 1, sessions[sessionIndex].sessionHigh); //--- Set price 1 ObjectSetInteger(0, edgeName, OBJPROP_COLOR, backgroundEdgeColor); //--- Set color ObjectSetInteger(0, edgeName, OBJPROP_WIDTH, 3); //--- Set width ObjectSetInteger(0, edgeName, OBJPROP_STYLE, STYLE_SOLID); //--- Set style } } //+------------------------------------------------------------------+ //| Render initial balance lines | //+------------------------------------------------------------------+ void RenderInitialBalanceLines(int sessionIndex, int barIndex) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid if(sessions[sessionIndex].initialBalanceHigh == 0 || sessions[sessionIndex].initialBalanceLow == 0) return; //--- Return if no IB datetime startTime = iTime(_Symbol, _Period, barIndex); //--- Get start time string initialBalanceHighName = objectPrefix + "IB_High_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create IB high name if(ObjectFind(0, initialBalanceHighName) < 0) { //--- Check if not found ObjectCreate(0, initialBalanceHighName, OBJ_TREND, 0, startTime, sessions[sessionIndex].initialBalanceHigh, startTime, sessions[sessionIndex].initialBalanceHigh); //--- Create trend ObjectSetInteger(0, initialBalanceHighName, OBJPROP_RAY_RIGHT, false); //--- Set ray right false ObjectSetInteger(0, initialBalanceHighName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, initialBalanceHighName, OBJPROP_HIDDEN, true); //--- Set hidden true } ObjectSetInteger(0, initialBalanceHighName, OBJPROP_COLOR, initialBalanceHighlightColor); //--- Set color ObjectSetInteger(0, initialBalanceHighName, OBJPROP_WIDTH, initialBalanceLineWidth); //--- Set width string initialBalanceLowName = objectPrefix + "IB_Low_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create IB low name if(ObjectFind(0, initialBalanceLowName) < 0) { //--- Check if not found ObjectCreate(0, initialBalanceLowName, OBJ_TREND, 0, startTime, sessions[sessionIndex].initialBalanceLow, startTime, sessions[sessionIndex].initialBalanceLow); //--- Create trend ObjectSetInteger(0, initialBalanceLowName, OBJPROP_RAY_RIGHT, false); //--- Set ray right false ObjectSetInteger(0, initialBalanceLowName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, initialBalanceLowName, OBJPROP_HIDDEN, true); //--- Set hidden true } ObjectSetInteger(0, initialBalanceLowName, OBJPROP_COLOR, initialBalanceHighlightColor); //--- Set color ObjectSetInteger(0, initialBalanceLowName, OBJPROP_WIDTH, initialBalanceLineWidth); //--- Set width } //+------------------------------------------------------------------+ //| Render key level extensions | //+------------------------------------------------------------------+ void RenderKeyLevelExtensions(int sessionIndex, int barIndex, int valueAreaUpperIndex, int valueAreaLowerIndex) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid if(ArraySize(sessions[sessionIndex].levels) == 0) return; //--- Return if no levels datetime startTime = iTime(_Symbol, _Period, barIndex); //--- Get start time bool isCurrentSession = (sessionIndex == ArraySize(sessions) - 1); //--- Check if current session datetime endTime; //--- Declare end time bool rayRight; //--- Declare ray right if(isCurrentSession) { //--- Handle current session endTime = startTime + PeriodSeconds(_Period) * 100; //--- Set end time rayRight = true; //--- Set ray right true } else { //--- Handle past session if(sessionIndex + 1 < ArraySize(sessions)) { //--- Check next session int nextBarIndex = iBarShift(_Symbol, _Period, sessions[sessionIndex + 1].startTime); //--- Get next bar endTime = iTime(_Symbol, _Period, nextBarIndex); //--- Set end time } else { //--- Handle last endTime = startTime + PeriodSeconds(_Period) * 100; //--- Set end time } rayRight = false; //--- Set ray right false } if(sessions[sessionIndex].volumeProfilePocIndex >= 0 && sessions[sessionIndex].volumeProfilePocIndex < ArraySize(sessions[sessionIndex].levels)) { //--- Check VP POC double pocPrice = sessions[sessionIndex].levels[sessions[sessionIndex].volumeProfilePocIndex].price; //--- Get POC price string pocExtensionName = objectPrefix + "POC_Ext_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create POC ext name if(ObjectFind(0, pocExtensionName) < 0) { //--- Check if not found ObjectCreate(0, pocExtensionName, OBJ_TREND, 0, startTime, pocPrice, endTime, pocPrice); //--- Create trend ObjectSetInteger(0, pocExtensionName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, pocExtensionName, OBJPROP_HIDDEN, true); //--- Set hidden true } else { //--- Handle existing ObjectSetInteger(0, pocExtensionName, OBJPROP_TIME, 0, startTime); //--- Set time 0 ObjectSetDouble(0, pocExtensionName, OBJPROP_PRICE, 0, pocPrice); //--- Set price 0 ObjectSetInteger(0, pocExtensionName, OBJPROP_TIME, 1, endTime); //--- Set time 1 ObjectSetDouble(0, pocExtensionName, OBJPROP_PRICE, 1, pocPrice); //--- Set price 1 } ObjectSetInteger(0, pocExtensionName, OBJPROP_RAY_RIGHT, rayRight); //--- Set ray right ObjectSetInteger(0, pocExtensionName, OBJPROP_COLOR, pocExtensionColor); //--- Set color ObjectSetInteger(0, pocExtensionName, OBJPROP_WIDTH, 2); //--- Set width ObjectSetInteger(0, pocExtensionName, OBJPROP_STYLE, STYLE_SOLID); //--- Set style } if(valueAreaUpperIndex >= 0 && valueAreaUpperIndex < ArraySize(sessions[sessionIndex].levels)) { //--- Check VAH double valueAreaHighPrice = sessions[sessionIndex].levels[valueAreaUpperIndex].price; //--- Get VAH price string valueAreaHighExtensionName = objectPrefix + "VAH_Ext_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create VAH ext name if(ObjectFind(0, valueAreaHighExtensionName) < 0) { //--- Check if not found ObjectCreate(0, valueAreaHighExtensionName, OBJ_TREND, 0, startTime, valueAreaHighPrice, endTime, valueAreaHighPrice); //--- Create trend ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_HIDDEN, true); //--- Set hidden true } else { //--- Handle existing ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_TIME, 0, startTime); //--- Set time 0 ObjectSetDouble(0, valueAreaHighExtensionName, OBJPROP_PRICE, 0, valueAreaHighPrice); //--- Set price 0 ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_TIME, 1, endTime); //--- Set time 1 ObjectSetDouble(0, valueAreaHighExtensionName, OBJPROP_PRICE, 1, valueAreaHighPrice); //--- Set price 1 } ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_RAY_RIGHT, rayRight); //--- Set ray right ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_COLOR, valueAreaHighExtensionColor); //--- Set color ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_WIDTH, 1); //--- Set width ObjectSetInteger(0, valueAreaHighExtensionName, OBJPROP_STYLE, STYLE_DOT); //--- Set style } if(valueAreaLowerIndex >= 0 && valueAreaLowerIndex < ArraySize(sessions[sessionIndex].levels)) { //--- Check VAL double valueAreaLowPrice = sessions[sessionIndex].levels[valueAreaLowerIndex].price; //--- Get VAL price string valueAreaLowExtensionName = objectPrefix + "VAL_Ext_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create VAL ext name if(ObjectFind(0, valueAreaLowExtensionName) < 0) { //--- Check if not found ObjectCreate(0, valueAreaLowExtensionName, OBJ_TREND, 0, startTime, valueAreaLowPrice, endTime, valueAreaLowPrice); //--- Create trend ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_HIDDEN, true); //--- Set hidden true } else { //--- Handle existing ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_TIME, 0, startTime); //--- Set time 0 ObjectSetDouble(0, valueAreaLowExtensionName, OBJPROP_PRICE, 0, valueAreaLowPrice); //--- Set price 0 ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_TIME, 1, endTime); //--- Set time 1 ObjectSetDouble(0, valueAreaLowExtensionName, OBJPROP_PRICE, 1, valueAreaLowPrice); //--- Set price 1 } ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_RAY_RIGHT, rayRight); //--- Set ray right ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_COLOR, valueAreaLowExtensionColor); //--- Set color ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_WIDTH, 1); //--- Set width ObjectSetInteger(0, valueAreaLowExtensionName, OBJPROP_STYLE, STYLE_DOT); //--- Set style } string highExtensionName = objectPrefix + "High_Ext_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create high ext name if(ObjectFind(0, highExtensionName) < 0) { //--- Check if not found ObjectCreate(0, highExtensionName, OBJ_TREND, 0, startTime, sessions[sessionIndex].sessionHigh, endTime, sessions[sessionIndex].sessionHigh); //--- Create trend ObjectSetInteger(0, highExtensionName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, highExtensionName, OBJPROP_HIDDEN, true); //--- Set hidden true ObjectSetInteger(0, highExtensionName, OBJPROP_STYLE, STYLE_DOT); //--- Set style } else { //--- Handle existing ObjectSetInteger(0, highExtensionName, OBJPROP_TIME, 0, startTime); //--- Set time 0 ObjectSetDouble(0, highExtensionName, OBJPROP_PRICE, 0, sessions[sessionIndex].sessionHigh); //--- Set price 0 ObjectSetInteger(0, highExtensionName, OBJPROP_TIME, 1, endTime); //--- Set time 1 ObjectSetDouble(0, highExtensionName, OBJPROP_PRICE, 1, sessions[sessionIndex].sessionHigh); //--- Set price 1 } ObjectSetInteger(0, highExtensionName, OBJPROP_RAY_RIGHT, rayRight); //--- Set ray right ObjectSetInteger(0, highExtensionName, OBJPROP_COLOR, highExtensionColor); //--- Set color ObjectSetInteger(0, highExtensionName, OBJPROP_WIDTH, 1); //--- Set width string lowExtensionName = objectPrefix + "Low_Ext_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create low ext name if(ObjectFind(0, lowExtensionName) < 0) { //--- Check if not found ObjectCreate(0, lowExtensionName, OBJ_TREND, 0, startTime, sessions[sessionIndex].sessionLow, endTime, sessions[sessionIndex].sessionLow); //--- Create trend ObjectSetInteger(0, lowExtensionName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, lowExtensionName, OBJPROP_HIDDEN, true); //--- Set hidden true ObjectSetInteger(0, lowExtensionName, OBJPROP_STYLE, STYLE_DOT); //--- Set style } else { //--- Handle existing ObjectSetInteger(0, lowExtensionName, OBJPROP_TIME, 0, startTime); //--- Set time 0 ObjectSetDouble(0, lowExtensionName, OBJPROP_PRICE, 0, sessions[sessionIndex].sessionLow); //--- Set price 0 ObjectSetInteger(0, lowExtensionName, OBJPROP_TIME, 1, endTime); //--- Set time 1 ObjectSetDouble(0, lowExtensionName, OBJPROP_PRICE, 1, sessions[sessionIndex].sessionLow); //--- Set price 1 } ObjectSetInteger(0, lowExtensionName, OBJPROP_RAY_RIGHT, rayRight); //--- Set ray right ObjectSetInteger(0, lowExtensionName, OBJPROP_COLOR, lowExtensionColor); //--- Set color ObjectSetInteger(0, lowExtensionName, OBJPROP_WIDTH, 1); //--- Set width double midpointPrice = 0; //--- Initialize midpoint price if(midpointAlgorithm == HIGH_LOW_MID) { //--- Check high low mid midpointPrice = sessions[sessionIndex].sessionHigh - (sessions[sessionIndex].sessionHigh - sessions[sessionIndex].sessionLow) / 2; //--- Calculate midpoint } else { //--- Handle TPO count based int totalTpoCount = GetTotalTpoCount(sessionIndex); //--- Get total TPO int targetTpoCount = totalTpoCount / 2; //--- Calculate target int currentTpoCount = 0; //--- Initialize current for(int i = 0; i < ArraySize(sessions[sessionIndex].levels); i++) { //--- Loop through levels currentTpoCount += sessions[sessionIndex].levels[i].tpoCount; //--- Accumulate TPO if(currentTpoCount >= targetTpoCount) { //--- Check if reached midpointPrice = sessions[sessionIndex].levels[i].price; //--- Set midpoint break; //--- Exit loop } } } if(midpointPrice > 0) { //--- Check if midpoint set string midpointExtensionName = objectPrefix + "Mid_Ext_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create mid ext name if(ObjectFind(0, midpointExtensionName) < 0) { //--- Check if not found ObjectCreate(0, midpointExtensionName, OBJ_TREND, 0, startTime, midpointPrice, endTime, midpointPrice); //--- Create trend ObjectSetInteger(0, midpointExtensionName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, midpointExtensionName, OBJPROP_HIDDEN, true); //--- Set hidden true ObjectSetInteger(0, midpointExtensionName, OBJPROP_STYLE, STYLE_DOT); //--- Set style } else { //--- Handle existing ObjectSetInteger(0, midpointExtensionName, OBJPROP_TIME, 0, startTime); //--- Set time 0 ObjectSetDouble(0, midpointExtensionName, OBJPROP_PRICE, 0, midpointPrice); //--- Set price 0 ObjectSetInteger(0, midpointExtensionName, OBJPROP_TIME, 1, endTime); //--- Set time 1 ObjectSetDouble(0, midpointExtensionName, OBJPROP_PRICE, 1, midpointPrice); //--- Set price 1 } ObjectSetInteger(0, midpointExtensionName, OBJPROP_RAY_RIGHT, rayRight); //--- Set ray right ObjectSetInteger(0, midpointExtensionName, OBJPROP_COLOR, midpointExtensionColor); //--- Set color ObjectSetInteger(0, midpointExtensionName, OBJPROP_WIDTH, 1); //--- Set width } }
RenderProfileBorderLine関数を追加し、セッションの価格範囲を示す垂直境界線をチャート上へ描画します。まず、セッションインデックスを検証し、高値と安値が存在することを確認してから処理を進めます。iTimeで開始時間を取得し、背景エッジ用の色とIBハイライト用の色を設定します。IB期間が正の値で、かつ値が設定されている場合は、ObjectCreateでOBJ_TRENDを使用して3つのトレンドラインを作成または更新します。1つ目は、セッション安値からIB安値までを背景色で描画します。2つ目は、IB安値からIB高値までをハイライト色で描画します。3つ目は、IB高値からセッション高値までを背景色で描画します。各ラインは、ObjectFindで存在を確認し、ObjectSetDoubleで価格を設定します。また、ObjectSetIntegerを使用して、レイ延長なし、選択不可、非表示、ソリッドスタイル、幅3に設定します。IBがない場合は、安値から高値までを背景色で結ぶ1本のトレンドラインを描画し、同じプロパティを設定します。
次に、RenderInitialBalanceLines関数では、IB範囲を視覚化します。値が存在する場合、開始時間を取得し、ObjectCreateでOBJ_TRENDを使用して高値と安値用の2本の水平トレンドラインを作成または更新します。これらのラインには、同じ座標を設定し、レイ延長なし、選択不可、非表示、入力されたハイライト色、ライン幅を設定します。
重要レベルを先へ延長表示するために、RenderKeyLevelExtensions関数を実装します。まず、有効性を確認して開始時間を取得します。次に、現在のセッションかどうかを判定します。現在のセッションの場合は、PeriodSeconds、TimeCurrent、iBarShiftを使用して延長終了時間を計算し、レイ表示を有効にします。過去のセッションの場合は、次のセッション開始時間を使用し、レイ表示なしで処理します。出来高プロファイルPoCについては、有効な場合、その価格位置に入力された延長色と幅2のソリッドラインを描画します。同様に、Value Area HighとValue Area Lowについては、インデックスが有効範囲内の場合、それぞれ対応する色を使用した点線を幅1で作成します。
常にセッション高値と安値については、入力された色を使用した点線の延長ラインを追加します。ミッドポイントについては、midpointAlgorithm列挙型に基づいて計算します。単純な価格範囲の中央値を使用する方法、またはGetTotalTpoCountでTime Price Opportunityカウントを累積し、半分の位置となるレベルを検索する方法のいずれかを使用します。値が正の場合は、ミッドポイント色を使用した点線を描画します。これらすべてのラインは、他のラインと同じ方法でオブジェクトの存在を確認または作成した後、ObjectSetIntegerで時間、レイ、スタイル、その他のプロパティを設定し、ObjectSetDoubleで価格を設定します。次に、固定モードで各レベルの横に出来高数値を表示するためのロジックを定義します。これは、各価格がどれだけ取引されたかを色の強度で示すラベル表示になります。
//+------------------------------------------------------------------+ //| Render fixed range volume labels | //+------------------------------------------------------------------+ void RenderFixedRangeVolumeLabels(int sessionIndex, int barIndex) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid if(ArraySize(sessions[sessionIndex].volumeProfilePrices) == 0) return; //--- Return if no VP prices double maxVolume = 0; //--- Initialize max volume for(int i = 0; i < ArraySize(sessions[sessionIndex].volumeProfileVolumes); i++) { //--- Loop through volumes if(sessions[sessionIndex].volumeProfileVolumes[i] > maxVolume) //--- Check if max maxVolume = sessions[sessionIndex].volumeProfileVolumes[i]; //--- Update max } if(maxVolume == 0) return; //--- Return if no volume datetime labelTime = iTime(_Symbol, _Period, barIndex); //--- Get label time for(int i = 0; i < ArraySize(sessions[sessionIndex].volumeProfilePrices); i++) { //--- Loop through VP prices double price = sessions[sessionIndex].volumeProfilePrices[i]; //--- Get price double volumeValue = sessions[sessionIndex].volumeProfileVolumes[i]; //--- Get volume double ratio = volumeValue / maxVolume; //--- Calculate ratio color volumeColor; //--- Declare color int percent = (int)(100 * ratio); //--- Calculate percent if(percent <= 8) { //--- Check low percent volumeColor = singlePrintColor; //--- Set single print color } else { //--- Handle higher int transparency = (int)MathMax(0, 80 - percent); //--- Calculate transparency volumeColor = ApplyTransparencyToColor(valueAreaColor, transparency); //--- Apply transparency } string objectName = objectPrefix + "Vol_" + IntegerToString(sessions[sessionIndex].startTime) + "_" + IntegerToString(i); //--- Create object name int x, y; //--- Declare coordinates ChartTimePriceToXY(0, 0, labelTime, price, x, y); //--- Convert to XY if(ObjectFind(0, objectName) < 0) { //--- Check if not found ObjectCreate(0, objectName, OBJ_LABEL, 0, 0, 0); //--- Create label } ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, x - 50); //--- Set X distance ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, y); //--- Set Y distance ObjectSetInteger(0, objectName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner ObjectSetInteger(0, objectName, OBJPROP_ANCHOR, ANCHOR_RIGHT); //--- Set anchor ObjectSetInteger(0, objectName, OBJPROP_COLOR, volumeColor); //--- Set color ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, labelFontSize); //--- Set font size ObjectSetString(0, objectName, OBJPROP_FONT, "Arial"); //--- Set font ObjectSetString(0, objectName, OBJPROP_TEXT, IntegerToString((int)MathRound(volumeValue))); //--- Set text ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, objectName, OBJPROP_HIDDEN, true); //--- Set hidden true } } //+------------------------------------------------------------------+ //| Apply transparency to color | //+------------------------------------------------------------------+ color ApplyTransparencyToColor(color baseColor, int transparency) { int red = (int)(baseColor & 0xFF); //--- Extract red int green = (int)((baseColor >> 8) & 0xFF); //--- Extract green int blue = (int)((baseColor >> 16) & 0xFF); //--- Extract blue transparency = (int)MathMin(100, MathMax(0, transparency)); //--- Clamp transparency int alpha = 255 - (transparency * 255 / 100); //--- Calculate alpha return (color)((alpha << 24) | (blue << 16) | (green << 8) | red); //--- Return color with alpha }
RenderFixedRangeVolumeLabels関数を実装し、固定時間足モードでプロファイルの横に出来高値をラベル表示します。まず、セッションインデックスを検証し、出来高プロファイル価格が存在することを確認してから処理を進めます。出来高配列をループ処理し、最大出来高を取得します。最大値を更新しながら確認し、0の場合は除算エラーを防ぐため処理を終了します。次に、iTimeでラベル時間を取得します。その後、各出来高プロファイルエントリーをループ処理し、最大出来高に対する比率を計算し、パーセンテージを求めます。ラベル色は、8%以下の場合はシングルプリント用の色を使用します。それ以上の場合は、ApplyTransparencyToColorを使用してValue Area色へ透明度を適用します。透明度は、MathMaxを使用してパーセント値に基づき0から80の範囲で逆方向に変化します。
各ラベルについて、IntegerToStringを使用して接頭辞、開始時間、インデックスを組み合わせた一意のオブジェクト名を作成します。ChartTimePriceToXYを使用して時間と価格を座標へ変換します。ObjectFindでオブジェクトが存在しない場合は、ObjectCreateとOBJ_LABELを使用してラベルを作成します。ObjectSetIntegerとObjectSetStringを使用して、X座標を左側へオフセットし、左上コーナー、右アンカー、計算された色、動的フォントサイズ、Arialフォント、MathRoundとIntegerToStringを使用して丸めた出来高テキスト、選択不可および非表示プロパティを設定します。
視覚表示で透明度を変更できるようにするため、ApplyTransparencyToColor関数を定義します。この関数では、ビット演算のANDとシフトを使用して、ベースカラーから赤、緑、青の各成分を取得します。入力された透明度は、MathMinとMathMaxを使用して0から100の範囲に制限します。その後、アルファ値を255からスケールされたパーセント値を引いて計算し、アルファ値を24ビット左へシフトして、青・緑・赤の配置とOR演算することで新しいカラー値を返します。最後に、固定時間足プロファイルをチャート上で視覚的に区別するため、固定時間足用の色付き背景矩形を描画できます。
//+------------------------------------------------------------------+ //| Render fixed range background rectangle | //+------------------------------------------------------------------+ void RenderFixedRangeBackgroundRectangle(int sessionIndex) { if(profileTimeframe != FIXED) return; //--- Return if not fixed if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid string objectName = objectPrefix + "FixedBG_" + IntegerToString(sessions[sessionIndex].startTime); //--- Create object name if(ObjectFind(0, objectName) < 0) { //--- Check if not found ObjectCreate(0, objectName, OBJ_RECTANGLE, 0, sessions[sessionIndex].startTime, sessions[sessionIndex].sessionHigh, sessions[sessionIndex].endTime > 0 ? sessions[sessionIndex].endTime : TimeCurrent(), sessions[sessionIndex].sessionLow); //--- Create rectangle ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, objectName, OBJPROP_HIDDEN, true); //--- Set hidden true ObjectSetInteger(0, objectName, OBJPROP_BACK, true); //--- Set back true } ObjectSetInteger(0, objectName, OBJPROP_COLOR, fixedRangeBackgroundColor); //--- Set color ObjectSetInteger(0, objectName, OBJPROP_FILL, true); //--- Set fill true }
RenderFixedRangeBackgroundRectangle関数を追加し、固定時間足プロファイル用の塗りつぶし背景を描画します。これにより、チャート上で固定範囲プロファイルを視覚的に区別しやすくします。まず、時間足がFIXEDではない場合、または配列サイズの確認によってセッションインデックスが無効な場合は、早期に処理を終了します。IntegerToStringを使用して接頭辞とセッション開始時間を組み合わせ、オブジェクト名を作成します。その後、ObjectFindでオブジェクトが存在しない場合は、ObjectCreateとOBJ_RECTANGLEを使用して矩形オブジェクトを作成します。矩形は、セッション高値にある開始時間から、終了時間(終了時間が0の場合はTimeCurrent を使用)までを範囲とし、セッション安値まで描画します。また、選択不可、非表示、他の描画要素の背面表示に設定します。オブジェクトの有無に関係なく、 ObjectSetIntegerを使用して入力された固定範囲背景色を適用し、ソリッド塗りつぶしを有効化します。これにより、プロファイル範囲の背景を色付きで表示できます。これで、プロファイル描画用のメイン関数内でこれらの関数を呼び出し、主要な処理を実行できるようになります。あとは、ティック計算イベントハンドラで各ロジックを呼び出します。
//+------------------------------------------------------------------+ //| Render session profile | //+------------------------------------------------------------------+ void RenderSessionProfile(int sessionIndex) { if(sessionIndex < 0 || sessionIndex >= ArraySize(sessions)) return; //--- Return if index invalid int size = ArraySize(sessions[sessionIndex].levels); //--- Get levels size if(size == 0 || sessions[sessionIndex].startTime == 0) return; //--- Return if no levels or no start time int barIndex = iBarShift(_Symbol, _Period, sessions[sessionIndex].startTime); //--- Get bar index if(barIndex < 0) return; //--- Return if invalid PadLevelsForSplitProfile(sessionIndex); //--- Pad levels for split SortPriceLevelsDescending(sessionIndex); //--- Sort levels descending CalculatePointOfControl(sessionIndex); //--- Calculate POC BuildVolumeProfileAndFindPoc(sessionIndex); //--- Build VP and find POC int totalTpoCount = GetTotalTpoCount(sessionIndex); //--- Get total TPO count int pointOfControlIndex = (useVolumeProfilePocForValueArea && sessions[sessionIndex].volumeProfilePocIndex >= 0) ? sessions[sessionIndex].volumeProfilePocIndex : sessions[sessionIndex].pointOfControlIndex; //--- Select POC index int valueAreaUpperIndex = pointOfControlIndex; //--- Initialize value area upper index int valueAreaLowerIndex = pointOfControlIndex; //--- Initialize value area lower index if(pointOfControlIndex >= 0) { //--- Check valid POC index int targetTpoCount = (int)(totalTpoCount * valueAreaPercent / 100.0); //--- Calculate target TPO count int currentTpoCount = sessions[sessionIndex].levels[pointOfControlIndex].tpoCount; //--- Set current TPO count while(currentTpoCount < targetTpoCount && (valueAreaUpperIndex > 0 || valueAreaLowerIndex < size - 1)) { //--- Loop to expand value area int upperTpoCount = (valueAreaUpperIndex > 0) ? sessions[sessionIndex].levels[valueAreaUpperIndex - 1].tpoCount : 0; //--- Get upper TPO count int lowerTpoCount = (valueAreaLowerIndex < size - 1) ? sessions[sessionIndex].levels[valueAreaLowerIndex + 1].tpoCount : 0; //--- Get lower TPO count if(upperTpoCount >= lowerTpoCount && valueAreaUpperIndex > 0) { //--- Check upper expansion valueAreaUpperIndex--; //--- Decrement upper index currentTpoCount += upperTpoCount; //--- Add upper TPO } else if(valueAreaLowerIndex < size - 1) { //--- Check lower expansion valueAreaLowerIndex++; //--- Increment lower index currentTpoCount += lowerTpoCount; //--- Add lower TPO } else if(valueAreaUpperIndex > 0) { //--- Fallback upper expansion valueAreaUpperIndex--; //--- Decrement upper index currentTpoCount += upperTpoCount; //--- Add upper TPO } else { //--- Break if no more break; //--- Exit loop } } } string displayStrings[]; //--- Declare display strings array ArrayResize(displayStrings, size); //--- Resize display strings for(int i = 0; i < size; i++) { //--- Loop through levels displayStrings[i] = sessions[sessionIndex].levels[i].tpoString; //--- Copy TPO string } int openLevelIndex = -1; //--- Initialize open level index int closeLevelIndex = -1; //--- Initialize close level index if(tpoCharacterType == ALPHABETIC) { //--- Check alphabetic double openPrice = sessions[sessionIndex].sessionOpen; //--- Get open price double closePrice = sessions[sessionIndex].sessionClose; //--- Get close price for(int i = 0; i < size; i++) { //--- Loop to find levels if(openLevelIndex < 0 && MathAbs(sessions[sessionIndex].levels[i].price - openPrice) < tpoPriceGridStep / 2) //--- Check open match openLevelIndex = i; //--- Set open index if(closeLevelIndex < 0 && MathAbs(sessions[sessionIndex].levels[i].price - closePrice) < tpoPriceGridStep / 2) //--- Check close match closeLevelIndex = i; //--- Set close index } RenderOpenTpoHighlight(sessionIndex, openLevelIndex, displayStrings); //--- Render open highlight RenderCloseTpoHighlight(sessionIndex, closeLevelIndex, displayStrings); //--- Render close highlight } for(int i = 0; i < size; i++) { //--- Loop to render levels string objectName = objectPrefix + "TPO_" + IntegerToString(sessions[sessionIndex].startTime) + "_" + IntegerToString(i); //--- Create object name color textColor = defaultTpoColor; //--- Set default color if(sessions[sessionIndex].levels[i].tpoCount == 1) { //--- Check single print textColor = singlePrintColor; //--- Set single print color } if(i >= valueAreaUpperIndex && i <= valueAreaLowerIndex) { //--- Check value area textColor = valueAreaColor; //--- Set value area color } if(i == sessions[sessionIndex].pointOfControlIndex && sessions[sessionIndex].volumeProfilePocIndex != sessions[sessionIndex].pointOfControlIndex) { //--- Check TPO POC textColor = pointOfControlColor; //--- Set POC color } if(highlightVolumeProfilePoc && i == sessions[sessionIndex].volumeProfilePocIndex) { //--- Check VP POC textColor = volumeProfilePocColor; //--- Set VP POC color } if(highlightSessionVwap && MathAbs(sessions[sessionIndex].levels[i].price - sessions[sessionIndex].vwap) < tpoPriceGridStep / 2) { //--- Check VWAP textColor = sessionVwapColor; //--- Set VWAP color } if(ObjectFind(0, objectName) < 0) { //--- Check if object not found ObjectCreate(0, objectName, OBJ_LABEL, 0, 0, 0); //--- Create label ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, 0); //--- Set X distance ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, 0); //--- Set Y distance } datetime labelTime = iTime(_Symbol, _Period, barIndex); //--- Get label time int x, y; //--- Declare coordinates ChartTimePriceToXY(0, 0, labelTime, sessions[sessionIndex].levels[i].price, x, y); //--- Convert to XY ObjectSetInteger(0, objectName, OBJPROP_XDISTANCE, x); //--- Set X distance ObjectSetInteger(0, objectName, OBJPROP_YDISTANCE, y); //--- Set Y distance ObjectSetInteger(0, objectName, OBJPROP_CORNER, CORNER_LEFT_UPPER); //--- Set corner ObjectSetInteger(0, objectName, OBJPROP_ANCHOR, ANCHOR_LEFT); //--- Set anchor ObjectSetInteger(0, objectName, OBJPROP_COLOR, textColor); //--- Set color ObjectSetInteger(0, objectName, OBJPROP_FONTSIZE, labelFontSize); //--- Set font size ObjectSetString(0, objectName, OBJPROP_FONT, "Arial"); //--- Set font ObjectSetString(0, objectName, OBJPROP_TEXT, displayStrings[i]); //--- Set text ObjectSetInteger(0, objectName, OBJPROP_SELECTABLE, false); //--- Set selectable false ObjectSetInteger(0, objectName, OBJPROP_HIDDEN, true); //--- Set hidden true } RenderOpenCloseMarkers(sessionIndex, barIndex); //--- Render open close markers RenderProfileBorderLine(sessionIndex, barIndex); //--- Render profile border if(initialBalancePeriods > 0) //--- Check if IB periods RenderInitialBalanceLines(sessionIndex, barIndex); //--- Render IB lines if(showExtensionLines) //--- Check if show extensions RenderKeyLevelExtensions(sessionIndex, barIndex, valueAreaUpperIndex, valueAreaLowerIndex); //--- Render extensions if(profileTimeframe == FIXED && renderVolumes) //--- Check fixed and render volumes RenderFixedRangeVolumeLabels(sessionIndex, barIndex); //--- Render volume labels RenderFixedRangeBackgroundRectangle(sessionIndex); //--- Render fixed background }
ここでは、マーケットプロファイル描画に高度な機能を組み込むために、RenderSessionProfile関数を拡張します。まず、インデックスとデータの有効性を確認し、無効な場合、レベルが存在しない場合、または開始時間が存在しない場合は処理を終了します。その後、iBarShift関数を使用してバーインデックスを取得します。プロファイルの準備として、分割モードでの配置調整のためにPadLevelsForSplitProfileを呼び出します。次に、SortPriceLevelsDescendingでレベルを降順に並べ替え、CalculatePointOfControlでTime Price OpportunityのPoCを計算し、BuildVolumeProfileAndFindPocで出来高プロファイルを構築して、そのPoCを取得します。
GetTotalTpoCountからTime Price Opportunityの合計カウントを取得した後、PoCのインデックスを条件に応じて選択します。useVolumeProfilePocForValueAreaがtrueで、かつ出来高PoCが存在する場合は、それを使用します。それ以外の場合は、Time Price OpportunityのPoCを使用します。その後、VAインデックスを初期化し、PoCの周囲へ拡張します。valueAreaPercentに基づく目標値へ到達するまで、隣接するレベルのうちTime Price Opportunity数が多い方を優先して選択し、ループ内でカウントを累積します。描画準備として、ArrayResizeを使用して表示用配列のサイズを変更し、Time Price Opportunity文字列をコピーします。アルファベットモードの場合は、グリッド幅の半分を許容範囲としてMathAbsでレベルを確認し、開始レベルと終了レベルのインデックスを検索します。その後、RenderOpenTpoHighlightとRenderCloseTpoHighlightを呼び出してハイライト表示をおこないます。
各レベルを処理するメイン描画ループでは、一意のオブジェクト名を作成し、初期色をデフォルト色に設定します。その後、シングルプリントの場合、Value Area範囲内の場合、独立したTime Price Opportunity PoCの場合、ハイライトされた出来高PoCの場合、または許容範囲を使用したVolume Weighted Average Priceとの一致の場合に応じて色を変更します。ObjectFindでオブジェクトが存在しない場合は、ObjectCreateとOBJ_LABELを使用してラベルを作成します。iTimeでラベル時間を取得した後、ChartTimePriceToXYを使用して時間と価格を座標へ変換します。そして、位置、コーナー、アンカー、色、フォントサイズ、フォント、表示文字列、選択不可および非表示設定を適用します。
最後に、RenderOpenCloseMarkers、RenderProfileBorderLine、期間が設定されている場合のRenderInitialBalanceLines、拡張が有効な場合にValue Areaインデックスを使用するRenderKeyLevelExtensions、固定時間足で出来高がある場合のRenderFixedRangeVolumeLabels、背景用のRenderFixedRangeBackgroundRectangleを呼び出して補助要素を追加します。これで、ティック計算イベントハンドラを更新するだけです。各期間の高値・安値・始値を保存し、期間内でInitial Balanceを計算し、レベルへ出来高を追加し、全体の計算処理としてVWAPを呼び出します。
//+------------------------------------------------------------------+ //| Calculate custom indicator | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime &time[], const double &open[], const double &high[], const double &low[], const double &close[], const long &tick_volume[], const long &volume[], const int &spread[]) { if(rates_total < 2) return 0; //--- Return if insufficient rates datetime currentBarTime = time[rates_total - 1]; //--- Get current bar time bool isNewBar = (currentBarTime != lastCompletedBarTime); //--- Check if new bar if(IsNewSessionStarted(currentBarTime, previousBarTime) || previousBarTime == 0) { //--- Check new session if(activeSessionIndex >= 0 && activeSessionIndex < ArraySize(sessions)) { //--- Check active session sessions[activeSessionIndex].endTime = previousBarTime; //--- Set end time RenderSessionProfile(activeSessionIndex); //--- Render session profile } activeSessionIndex = CreateNewSession(); //--- Create new session sessions[activeSessionIndex].startTime = currentBarTime; //--- Set start time sessions[activeSessionIndex].sessionOpen = open[rates_total - 1]; //--- Set session open sessions[activeSessionIndex].sessionHigh = high[rates_total - 1]; //--- Set session high sessions[activeSessionIndex].sessionLow = low[rates_total - 1]; //--- Set session low lastCompletedBarTime = currentBarTime; //--- Update last completed bar time } previousBarTime = currentBarTime; //--- Update previous bar time if(isNewBar && IsBarEligibleForProcessing(currentBarTime) && activeSessionIndex >= 0) { //--- Check if process bar sessions[activeSessionIndex].sessionHigh = MathMax(sessions[activeSessionIndex].sessionHigh, high[rates_total - 1]); //--- Update session high sessions[activeSessionIndex].sessionLow = MathMin(sessions[activeSessionIndex].sessionLow, low[rates_total - 1]); //--- Update session low sessions[activeSessionIndex].sessionClose = close[rates_total - 1]; //--- Update session close int periodIndex = sessions[activeSessionIndex].periodCount; //--- Get period index ArrayResize(sessions[activeSessionIndex].periodHighs, periodIndex + 1); //--- Resize period highs ArrayResize(sessions[activeSessionIndex].periodLows, periodIndex + 1); //--- Resize period lows ArrayResize(sessions[activeSessionIndex].periodOpens, periodIndex + 1); //--- Resize period opens sessions[activeSessionIndex].periodHighs[periodIndex] = high[rates_total - 1]; //--- Set period high sessions[activeSessionIndex].periodLows[periodIndex] = low[rates_total - 1]; //--- Set period low sessions[activeSessionIndex].periodOpens[periodIndex] = open[rates_total - 1]; //--- Set period open sessions[activeSessionIndex].periodCount++; //--- Increment period count if(periodIndex < initialBalancePeriods) { //--- Check if within IB periods if(periodIndex == 0) { //--- Handle first period sessions[activeSessionIndex].initialBalanceHigh = high[rates_total - 1]; //--- Set IB high sessions[activeSessionIndex].initialBalanceLow = low[rates_total - 1]; //--- Set IB low } else { //--- Handle subsequent sessions[activeSessionIndex].initialBalanceHigh = MathMax(sessions[activeSessionIndex].initialBalanceHigh, high[rates_total - 1]); //--- Update IB high sessions[activeSessionIndex].initialBalanceLow = MathMin(sessions[activeSessionIndex].initialBalanceLow, low[rates_total - 1]); //--- Update IB low } } double quantizedHigh = QuantizePriceToGrid(high[rates_total - 1]); //--- Quantize high double quantizedLow = QuantizePriceToGrid(low[rates_total - 1]); //--- Quantize low for(double price = quantizedLow; price <= quantizedHigh; price += tpoPriceGridStep) { //--- Loop through prices int levelIndex = GetOrCreatePriceLevel(activeSessionIndex, price); //--- Get or create level if(levelIndex >= 0) { //--- Check valid level AddTpoCharacterToLevel(activeSessionIndex, levelIndex, periodIndex); //--- Add TPO character sessions[activeSessionIndex].levels[levelIndex].volume += (double)tick_volume[rates_total - 1] / MathMax(1, (quantizedHigh - quantizedLow) / tpoPriceGridStep + 1); //--- Add volume } } CalculateSessionVwap(activeSessionIndex); //--- Calculate VWAP lastCompletedBarTime = currentBarTime; //--- Update last completed bar time } if(IsBarEligibleForProcessing(currentBarTime) && activeSessionIndex >= 0) { //--- Check if update session sessions[activeSessionIndex].sessionClose = close[rates_total - 1]; //--- Update close sessions[activeSessionIndex].sessionHigh = MathMax(sessions[activeSessionIndex].sessionHigh, high[rates_total - 1]); //--- Update high sessions[activeSessionIndex].sessionLow = MathMin(sessions[activeSessionIndex].sessionLow, low[rates_total - 1]); //--- Update low } for(int i = 0; i < ArraySize(sessions); i++) { //--- Loop through sessions RenderSessionProfile(i); //--- Render profile } return rates_total; //--- Return rates total }
OnCalculateイベントハンドラでは、データ処理中にIB追跡や出来高累積などの新しい機能を統合するためにロジックを更新します。まず、利用可能なレート数が不足している場合は0を返します。現在のバー時間を取得し、新しいバーかどうかを確認します。その後、IsNewSessionStartedによって新しいセッションが開始された場合、または以前の時間が存在しない場合は、アクティブなセッションの終了時間を確定し、ArraySizeによって有効範囲内であることを確認した上でRenderSessionProfileを使用して描画します。続いて、CreateNewSessionを使用して新しいセッションを作成し、現在のバーから基本フィールドを初期化し、前回完了時間を更新します。これは前回のバージョンでおこなった処理と同様です。
前回のバー時間を設定した後、IsBarEligibleForProcessingによって新しい処理対象バーであり、かつアクティブセッションが存在する場合は、MathMaxとMathMin、および終値を使用してセッションの高値・安値を更新します。さらに、期間ごとの高値、安値、始値を配列にリサイズして保存し、期間カウントを増加させます。initialBalancePeriods内の場合は、期間インデックスに基づいてIBの高値と安値を設定または更新します。バーの高値と安値を量子化し、その範囲をループしてGetOrCreatePriceLevelを使用してレベルを取得または作成します。その後、AddTpoCharacterToLevelを使用してTime Price Opportunity文字を追加します。また、MathMaxで最低1に確保した価格ステップを使用し、ティック出来高を価格ステップで割って比例配分した出来高を累積します。その後、CalculateSessionVwapによってVWAPを計算し、前回完了時間を更新します。バーが処理対象の場合は、ライブ更新用にセッションの終値、高値、安値を更新します。その後、すべてのセッションをループして描画し、合計レート数を返します。コンパイルすると、次の結果が得られます。

画像から、出来高ベースのプロファイルロジックと拡張された可視化機能を追加することでインジケーターを更新し、ハイブリッド版にしたことが確認できます。これにより、目的を達成しました。残っている作業は、このプログラムのバックテストをおこなうことです。バックテストについては次のセクションで扱います。
バックテスト
テストを実施しました。以下は、コンパイル後の動作を示す単一のGraphics Interchange Format (GIF)画像です。

結論
結論として、 MQL5のハイブリッドTime Price Opportunity (TPO)マーケットプロファイルインジケーターを拡張し、出来高データを統合することで、出来高ベースのPoC、VA、VWAPを計算できるようにし、カスタマイズ可能なハイライト機能を追加しました。このシステムには、IB検出、重要レベルの延長ライン、分割プロファイル、四角形や円形などの代替TPO文字、境界線、固定範囲背景、動的出来高ラベルなどの高度な機能が追加され、複数の時間足にわたる包括的な視覚分析を可能にします。このアップグレードされたハイブリッドTime Price Opportunityマーケットプロファイルインジケーターにより、市場構造と出来高の動きをより深く理解するための環境が整い、取引におけるさらなる最適化へ進む準備ができました。それでは、よいトレードを。
MetaQuotes Ltdにより英語から翻訳されました。
元の記事: https://www.mql5.com/en/articles/21390
警告: これらの資料についてのすべての権利はMetaQuotes Ltd.が保有しています。これらの資料の全部または一部の複製や再プリントは禁じられています。
この記事はサイトのユーザーによって執筆されたものであり、著者の個人的な見解を反映しています。MetaQuotes Ltdは、提示された情報の正確性や、記載されているソリューション、戦略、または推奨事項の使用によって生じたいかなる結果についても責任を負いません。
初心者からエキスパートへ:イントラデイ戦略の自動化
MQL5取引ツール(第20回):統計的相関分析と回帰分析を用いたCanvasグラフ作成
エラー 146 (「トレードコンテキスト ビジー」) と、その対処方法
MQL5でカスタムインジケータを作成する(第7回):セッション分析のためのハイブリッドTime Price Opportunity (TPO)マーケットプロファイル
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索