English Deutsch
preview
オープニングレンジブレイクアウト日中取引戦略の解読

オープニングレンジブレイクアウト日中取引戦略の解読

MetaTrader 5トレーディング |
1 384 11
Zhuo Kai Chen
Zhuo Kai Chen

はじめに

オープニングレンジブレイクアウト(ORB)戦略は、市場が開いた直後に形成される初期の取引レンジが、買い手と売り手が価値に合意する重要な価格レベルを反映しているという考えに基づいて構築されています。特定のレンジを上抜けまたは下抜けするブレイクアウトを特定することで、市場の方向性が明確になるにつれて発生することが多いモメンタムを利用し、トレーダーは利益を狙うことができます。 

本記事では、Concretum Groupの論文から応用された3つのORB戦略を紹介します。まず、研究の背景として、主要な概念および使用された方法論について解説します。その後、それぞれの戦略について、仕組みの解説、シグナルルールの一覧、統計的なパフォーマンス分析をおこないます。最後に、ポートフォリオの観点から戦略を検証し、分散投資というテーマにも焦点を当てます。  

本記事ではプログラミングの詳細な解説はおこなわず、主に研究課程に重点を置きます。具体的には、戦略の再現、分析、検証などの手順です。これは、取引の優位性を模索する読者や、これらの戦略の研究・再現過程に関心のある方に適しています。なお、エキスパートアドバイザー(EA)用のすべてのMQL5コードは公開しますので、読者ご自身でこのフレームワークを拡張していただくことも可能です。


研究の背景

このセクションでは、本記事で戦略を分析する際に使用する研究手法と、後のセクションでも登場する主要な概念について説明します。  

Concretum Groupは、数少ない日中取引戦略を開発している学術研究チームのひとつです。今回参考にする彼らの研究では、市場のオープンからクローズまで(米東部時間9:30~16:00)の間に取引をおこなう戦略に焦点を当てています。私たちが使用しているブローカーのサーバー時間はUTC+2/3であるため、これはサーバー時間では18:30〜24:00に相当します。テストをおこなう際は、ご自身のブローカーのタイムゾーンに合わせて調整してください。  

元の研究では、QQQというETF(上場投資信託)を対象に取引をおこなっています。QQQは、ナスダック100指数を追跡するETFです。ナスダック100は、ナスダック市場に上場している大手テクノロジー企業100社のパフォーマンスを表す指数です。ただし、ナスダック100自体は直接取引できず、その派生商品を通じて取引します。QQQは、これらの企業にひとつの銘柄を通じて投資する手段を提供しており、個人投資家にも人気です。本記事では、テスト対象としてQQQではなくUSTEC(ナスダック100指数に連動するCFD)を使用します。CFD(差金決済取引)を使うことで、原資産を保有せずに価格変動に対して投機的な取引が可能となり、多くの場合レバレッジを利用して損益を拡大することができます。

本記事で導入する重要な2つの指標がアルファとベータです。アルファは、ある投資が市場のベンチマーク(例:株価指数)と比較してどれだけ超過リターンを上げたかを示します。これは投資が期待以上の成果を出しているかを測るものであり、戦略の優位性を表します。ベータは、市場全体の動きに対してその投資がどれだけ感応的(連動的)かを示す指標です。ベータが1.0であれば市場と同じ動きをし、1を上回ると市場以上に価格が変動しやすく、1を下回ると市場よりも穏やかに動くことを意味します。これらの指標を理解することで、その戦略が市場のトレンド(方向性)に依存しているのか、あるいは独自の優位性に基づいて成果を上げているのかを見極めることができます。特に、株価指数や仮想通貨などトレンドが出やすい資産においては、戦略のバイアスや依存度を判断するうえで非常に重要です。

アルファとベータは次のように計算されます。

アルファベータ

Riは投資のリターン、Rfはリスクフリーレート(一般的には米国債の利回りなどに基づくが、無視されることもある)、Rmは市場のリターンを指します。共分散および分散は、通常、日次リターンを用いて計算されます。

本記事で後ほど使用する重要な指標のひとつが、VWAP(出来高加重平均価格:Volume Weighted Average Price)です。計算式は以下のとおりです。

vwap

VWAPの基本的な考え方は、ある期間における真の平均取引コストを反映する価格を、出来高で加重して算出することにあります。単純な平均価格とは異なり、VWAPは取引量の多い価格により多くの重みを与えるため、より公正なベンチマークとなります。

アルゴリズム取引におけるVWAPの一般的な用途は以下のとおりです。

  • トレンドフィルターとして使用
  • トレーリングストップとして使用
  • シグナル生成に使用(例:価格がVWAPをクロスしたときにエントリー)

VWAPの計算は、通常市場オープンの最初のローソク足から開始します。上記の式では、Piはi番目のローソク足の価格(通常は終値)を、Viはそのローソク足の取引量を表します。なお、CFDブローカーによっては流動性プロバイダーの違いにより取引量に差異が出る可能性がありますが、相対的な重み付けは概ね一貫しているため、VWAPの信頼性に大きな影響はありません。

本記事では、leverage space risk model(レバレッジ空間リスクモデル)を実装します。この手法では、各取引において口座残高の一定割合をリスクに投じるというルールを採用しており、ストップロスに到達した際にのみ損失が確定します。ストップロスの幅は、資産価格に対する一定のパーセンテージとし、価格の変動性に適応させます。取引ごとのリスクは、シンプルさを保つために概数(キリのよい数字)で設定し、最大ドローダウンを約20%以内に抑えることを目標とします。各戦略は、2020年1月1日から2025年1月1日までの5年間にわたってテストをおこない、直近の相場環境における収益性を評価するのに十分なデータを収集します。徹底的な統計的な分析では、累積リターン(パーセンテージ)に基づくバイアンドホールド戦略との比較および、個別のパフォーマンス指標を用いた評価を実施します。


戦略1:寄り付き足の方向

最初に紹介する戦略は、Concretum Groupの論文『Can Day Trading Really Be Profitable?』で提案された、クラシックなオープニングブレイクアウト戦略です。この戦略のシグナルルールの背景には、短期的な価格モメンタムを捉えつつ、デイトレーダーにとって実用的かつリスク管理を重視した設計という意図があります。著者たちは、市場オープン直後に見られる高いボラティリティと方向性のあるモメンタムを活用するためにORB(オープニングレンジブレイクアウト)手法を選択しています。この時間帯は、機関投資家の活動が表面化しやすく、個人トレーダーがその方向性を1日のトレンド判断の手がかりとして利用できる重要なウィンドウとされています。

論文を精査した結果、元の戦略を改善できるポイントがいくつかあることが分かりました。元の戦略では、最初の5分足の高値または安値をストップロスとし、10Rの利益確定(テイクプロフィット)を設定していました。この方法は理論上は有効でも、実運用では個人トレーダーにとって非現実的でした。初動5分足を基準としたタイトなストップロスは、相対的な取引コストを増加させる傾向がありました。また、日中に全ポジションをクローズするルールのもとでは、10Rという大きな利益目標はほとんど達成されず、過剰設定となっていました。さらに、元の戦略にはレジーム(市場環境)に基づくフィルターが存在していなかったため、移動平均を使ったフィルターを加えることで、戦略の信頼性を高めることができると考えました。

修正版シグナルルール:

  • 市場オープンから5分後、最初の5分足が陽線(ブル)かつその終値が350期間移動平均より上なら買いエントリー。
  • 市場オープンから5分後、最初の5分足が陰線(ベア)かつその終値が350期間移動平均より下なら売りエントリー。
  • 市場クローズ5分前にすべてのポジションを決済。
  • ストップロスはエントリー価格から1%。
  • 1回の取引でリスクをとる資金は全体残高の2%。

EAの完全なMQL5コード:

//USTEC-M5
#include <Trade/Trade.mqh>
CTrade trade;

input int startHour = 18;
input int startMinute = 35;
input int endHour = 23;
input int endMinute = 55;
input double risk = 2.0;
input double slp = 0.01;
input int MaPeriods = 350;
input int Magic = 0;

int barsTotal = 0;
int handleMa;
double lastClose=0;
double lastOpen = 0;
double lot = 0.1;

//+------------------------------------------------------------------+
//|Initialization function                                           |
//+------------------------------------------------------------------+ 
int OnInit()
  {
   trade.SetExpertMagicNumber(Magic);
   handleMa = iMA(_Symbol,PERIOD_CURRENT,MaPeriods,0,MODE_SMA,PRICE_CLOSE);
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//|Deinitialization function                                         |
//+------------------------------------------------------------------+ 
void OnDeinit(const int reason)
  {
  }

//+------------------------------------------------------------------+
//|On tick function                                                  |
//+------------------------------------------------------------------+ 
void OnTick()
  {
   int bars = iBars(_Symbol, PERIOD_CURRENT);
   if(barsTotal != bars){
      barsTotal=bars;
      double ma[];
      CopyBuffer(handleMa,0,1,1,ma);
      
      if(MarketOpen()){
         lastClose = iClose(_Symbol,PERIOD_CURRENT,1);
         lastOpen = iOpen(_Symbol,PERIOD_CURRENT,1);
         if(lastClose<lastOpen&&lastClose<ma[0])executeSell();
         if (lastClose>lastOpen&&lastClose>ma[0]) executeBuy();
      }
      
      if(MarketClose()){
         for(int i = PositionsTotal()-1; i>=0; i--){
            ulong pos = PositionGetTicket(i);
            string symboll = PositionGetSymbol(i);
            if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)trade.PositionClose(pos);
            }
       }  
   }
}

//+------------------------------------------------------------------+
//| Detect if market is opened                                       |
//+------------------------------------------------------------------+
bool MarketOpen()
{
    datetime currentTime = TimeTradeServer(); 
    MqlDateTime timeStruct;
    TimeToStruct(currentTime, timeStruct);
    int currentHour = timeStruct.hour;
    int currentMinute = timeStruct.min;
    if (currentHour == startHour &&currentMinute==startMinute)return true;
    else return false;
}

//+------------------------------------------------------------------+
//| Detect if market is closed                                       |
//+------------------------------------------------------------------+
bool MarketClose()
{
    datetime currentTime = TimeTradeServer(); 
    MqlDateTime timeStruct;
    TimeToStruct(currentTime, timeStruct);
    int currentHour = timeStruct.hour;
    int currentMinute = timeStruct.min;

    if (currentHour == endHour && currentMinute == endMinute)return true;
    else return false;
}

//+------------------------------------------------------------------+
//| Sell execution function                                          |
//+------------------------------------------------------------------+        
void executeSell() {      
       double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
       bid = NormalizeDouble(bid,_Digits);
       double sl = bid*(1+slp);
       sl = NormalizeDouble(sl, _Digits);
       lot = calclots(bid*slp);
       trade.Sell(lot,_Symbol,bid,sl);    
}
  
//+------------------------------------------------------------------+
//| Buy execution function                                           |
//+------------------------------------------------------------------+   
void executeBuy() {
       double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
       ask = NormalizeDouble(ask,_Digits);
       double sl = ask*(1-slp);
       sl = NormalizeDouble(sl, _Digits);
       lot = calclots(ask*slp);
       trade.Buy(lot,_Symbol,ask,sl);
}

//+------------------------------------------------------------------+
//| Calculate lot size based on risk and stop loss range             |
//+------------------------------------------------------------------+
double calclots(double slpoints) {
    double riskAmount = AccountInfoDouble(ACCOUNT_BALANCE) * risk / 100;
    double ticksize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
    double tickvalue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
    double lotstep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
    double moneyperlotstep = slpoints / ticksize * tickvalue * lotstep;
    double lots = MathFloor(riskAmount / moneyperlotstep) * lotstep;
    lots = MathMin(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX));
    lots = MathMax(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN));
    return lots;
}

典型的な取引は次のようになります。

orb1例

バックテスト結果:

orb1設定

orb1パラメータ

orb1資産曲線

orb1結果

移動平均フィルターを使用しない場合、元の戦略ルールでは毎営業日1回の取引が発生します。フィルターを追加することで、取引回数は約半分に減少しました。平均保有時間が取引セッション全体にわたるため、戦略のパフォーマンスはある程度マクロな市場トレンドの影響を反映します。実際に、買いポジションがより多く発生し、勝率も高くなる傾向が見られました。全体として、この戦略は1.23のプロフィットファクターと2.81のシャープレシオを達成しました。これは、非常に堅実なパフォーマンスを示すものです。また、使用しているルールは非常にシンプルで過剰適合のリスクが低く、このことから、バックテストで得られた良好な結果が実運用でも再現される可能性が高いことを示唆しています。

orb1比較

orb1ドローダウン

EAは、過去5年間にわたってUSTECのバイアンドホールド戦略を大きく上回る成績を収めました。特に注目すべきは、最大ドローダウンを18%に抑えており、これはベンチマークの半分程度である点です。資産曲線は全体的に滑らかで、唯一の停滞期は2022年後半から2023年前半にかけての短期間にとどまっており、この時期はちょうどUSTECが大きなドローダウンに直面していた時期でもあります。

orb1月次リターン

orb1月次ドローダウン

Alpha: 1.6017
Beta: 0.0090

ベータが0.9%であるということは、この戦略の日次リターンが原資産との相関性がわずか0.9%しかないことを示しており、戦略の優位性は主にそのルール自体に起因しており、市場のトレンドに依存していないことが分かります。ドローダウンとリターンのブレは少なく、2020年のコロナショックのような極端な市場環境(レジーム)にも耐性があることを示唆しています。ほとんどの月がプラス収益であり、損失が出た月も軽微で、最悪の月間ドローダウンは10.2%にとどまりました。総じて、これは実運用に適しており、かつ利益を期待できる戦略であると評価できます。


戦略2:VWAPトレンドフォロー

2つ目の戦略は、市場オープン時のトレンドフォロー型戦略であり、論文『Volume Weighted Average Price (VWAP) The Holy Grail for Day Trading Systems』で紹介されたものです。 この戦略のシグナルルールの背景には、VWAPを出来高加重の明確なベンチマークとして活用し、日中のトレンドを見極めるという狙いがあります。価格がVWAPの上でクローズすればロング(買い)、下でクローズすればショート(売り)というシンプルなルールでエントリーを判断し、ノイズを除外しつつ明確なモメンタムを捉えることを目指します。このようなシンプルな構造は、デイトレーダーにとって実行可能で再現性の高いシグナルを提供するという利点があります。 この古典的なトレンドフォロー戦略は、特に高ボラティリティ環境で優れたパフォーマンスを発揮し、長期のトレンドに乗ることで高いリスクリワードを実現します。株式市場が開いている5時間の間に、インデックスは大きく動く傾向があり、この時間的流動性が本戦略の成功を支える重要な要素となっています。

元の論文では、1分足の時間枠で取引されており、それが最も効果的であると主張されていました。しかし、私のテストでは、15分足時間枠の方が本戦略には適していることがわかりました。これは、おそらくCFD取引の方がETFに比べて取引コストが高く、頻繁な売買に不向きであることが原因です。また、元の論文ではストップロスが設定されていませんでしたが、私たちのアプローチでは時間枠がより長いため、ストップロスを導入しています。これは、事故的な損失を回避するセーフティネットかつリスク計算の基準値として機能します。最後に、前述の戦略と同様に移動平均を用いたトレンドフィルターも追加しています。

修正版シグナルルール:

  • 市場オープン後、ポジション未保有の状態で、直近15分足の終値がVWAPおよび300期間移動平均を上回っている場合は買いエントリー
  • 市場オープン後、ポジション未保有の状態で、直近15分足の終値がVWAPおよび300期間移動平均を下回っている場合は売りエントリー
  • ストップロスはエントリー価格から0.8%
  • 1回の取引でリスクをとる資金は全体残高の2%。

EAの完全なMQL5コード:

//USTEC-M15
#include <Trade/Trade.mqh>
CTrade trade;

input int startHour = 18;
input int startMinute = 35;
input int endHour = 23;
input int endMinute = 45;

input double risk = 2.0;
input double slp = 0.008;
input int MaPeriods = 300;
input int Magic = 0;

int barsTotal = 0;
int handleMa;
double lastClose=0;
double lot = 0.1;

//+------------------------------------------------------------------+
//|Initialization function                                           |
//+------------------------------------------------------------------+ 
int OnInit()
  {
   trade.SetExpertMagicNumber(Magic);
   handleMa = iMA(_Symbol,PERIOD_CURRENT,MaPeriods,0,MODE_SMA,PRICE_CLOSE);
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//|Deinitialization function                                         |
//+------------------------------------------------------------------+ 
void OnDeinit(const int reason)
  { 
  }

//+------------------------------------------------------------------+
//|On tick function                                                  |
//+------------------------------------------------------------------+ 
void OnTick()
  {
   int bars = iBars(_Symbol, PERIOD_CURRENT);
   if(barsTotal != bars){
      barsTotal=bars;
      bool NotInPosition = true;
      double ma[];
      CopyBuffer(handleMa,0,1,1,ma);
      if(MarketOpened()&&!MarketClosed()){
         lastClose = iClose(_Symbol,PERIOD_CURRENT,1);
         int startIndex = getSessionStartIndex();
         double vwap = getVWAP(startIndex);
         for(int i = PositionsTotal()-1; i>=0; i--){
            ulong pos = PositionGetTicket(i);
            string symboll = PositionGetSymbol(i);
            if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol){
               if((PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY&&lastClose<vwap)||(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL&&lastClose>vwap))trade.PositionClose(pos);
               else NotInPosition=false;
            }
         }
         if(lastClose<vwap&&NotInPosition&&lastClose<ma[0])executeSell();
         if(lastClose>vwap&&NotInPosition&&lastClose>ma[0]) executeBuy();
       } 
       if(MarketClosed()){
          for(int i = PositionsTotal()-1; i>=0; i--){
            ulong pos = PositionGetTicket(i);
            string symboll = PositionGetSymbol(i);
            if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)trade.PositionClose(pos);
          }
       }
        
   }
}

//+------------------------------------------------------------------+
//| Detect if market is opened                                       |
//+------------------------------------------------------------------+ 
bool MarketOpened()
{
    datetime currentTime = TimeTradeServer(); 
    MqlDateTime timeStruct;
    TimeToStruct(currentTime, timeStruct);
    int currentHour = timeStruct.hour;
    int currentMinute = timeStruct.min;
    if (currentHour >= startHour &&currentMinute>=startMinute)return true;
    else return false;
}

//+------------------------------------------------------------------+
//| Detect if market is closed                                       |
//+------------------------------------------------------------------+ 
bool MarketClosed()
{
    datetime currentTime = TimeTradeServer(); 
    MqlDateTime timeStruct;
    TimeToStruct(currentTime, timeStruct);
    int currentHour = timeStruct.hour;
    int currentMinute = timeStruct.min;

    if (currentHour >= endHour && currentMinute >= endMinute)return true;
    else return false;
}

//+------------------------------------------------------------------+
//| Sell execution function                                          |
//+------------------------------------------------------------------+        
void executeSell() {      
       double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
       bid = NormalizeDouble(bid,_Digits);
       double sl = bid*(1+slp);
       sl = NormalizeDouble(sl, _Digits);
       lot = calclots(bid*slp);
       trade.Sell(lot,_Symbol,bid,sl);    
}

//+------------------------------------------------------------------+
//| Buy execution function                                           |
//+------------------------------------------------------------------+    
void executeBuy() {
       double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
       ask = NormalizeDouble(ask,_Digits);
       double sl = ask*(1-slp);
       sl = NormalizeDouble(sl, _Digits);
       lot = calclots(ask*slp);
       trade.Buy(lot,_Symbol,ask,sl);
}

//+------------------------------------------------------------------+
//| Get VWAP function                                                |
//+------------------------------------------------------------------+
double getVWAP(int startCandle)
{
   double sumPV = 0.0;  // Sum of (price * volume)
   long sumV = 0.0;    // Sum of volume

   // Loop from the starting candle index down to 1 (excluding current candle)
   for(int i = startCandle; i >= 1; i--)
   {
      // Calculate typical price: (High + Low + Close) / 3
      double high = iHigh(_Symbol, PERIOD_CURRENT, i);
      double low = iLow(_Symbol, PERIOD_CURRENT, i);
      double close = iClose(_Symbol, PERIOD_CURRENT, i);
      double typicalPrice = (high + low + close) / 3.0;

      // Get volume and update sums
      long volume = iVolume(_Symbol, PERIOD_CURRENT, i);
      sumPV += typicalPrice * volume;
      sumV += volume;
   }

   // Calculate VWAP or return 0 if no volume
   if(sumV == 0)
      return 0.0;
   
   double vwap = sumPV / sumV;

   // Plot the dot
   datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
   string objName = "VWAP" + TimeToString(currentBarTime, TIME_MINUTES);
   ObjectCreate(0, objName, OBJ_ARROW, 0, currentBarTime, vwap);
   ObjectSetInteger(0, objName, OBJPROP_COLOR, clrGreen);    // Green dot
   ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_DOT);   // Dot style
   ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1);           // Size of the dot
   
   return vwap;
}

//+------------------------------------------------------------------+
//| Find the index of the candle corresponding to the session open   |
//+------------------------------------------------------------------+
int getSessionStartIndex()
{
   int sessionIndex = 1;
   // Loop over bars until we find the session open
   for(int i = 1; i <=1000; i++)
   {
      datetime barTime = iTime(_Symbol, PERIOD_CURRENT, i);
      MqlDateTime dt;
      TimeToStruct(barTime, dt);
      
      if(dt.hour == startHour && dt.min == startMinute-5)
      {
         sessionIndex = i;
         break;
      }
   }
      
   return sessionIndex;
}

//+------------------------------------------------------------------+
//| Calculate lot size based on risk and stop loss range             |
//+------------------------------------------------------------------+
double calclots(double slpoints) {
    double riskAmount = AccountInfoDouble(ACCOUNT_BALANCE) * risk / 100;
    double ticksize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
    double tickvalue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
    double lotstep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
    double moneyperlotstep = slpoints / ticksize * tickvalue * lotstep;
    double lots = MathFloor(riskAmount / moneyperlotstep) * lotstep;
    lots = MathMin(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX));
    lots = MathMax(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN));
    return lots;
}

典型的な取引は次のようになります。

orb2例

バックテスト結果:

orb2設定

orb2パラメータ

orb2資産曲線

orb2結果

最初のオープニングブレイクアウト戦略と比較すると、この戦略はより頻繁に取引をおこない、1日に平均1回以上の取引を実施しています。この取引回数の増加は、市場時間中に価格が再びVWAPをクロスした際に再エントリーを許可していることに起因します。勝率は42%であり、50%を下回っています。これはダイナミックなトレーリングストップを用いるトレンドフォロー手法としては一般的な数値です。この設定は、リスクリワード比率の高い取引を優先する一方で、ストップロスにかかるリスクも増加させる傾向があります。パフォーマンス指標としては、シャープレシオが3.57、プロフィットファクターが1.26と非常に高い水準を示しており、優れた成果を上げています。

orb2比較

orb2ドローダウン

この戦略は、バイアンドホールド戦略を大きく上回り、5年間で501%のリターンを達成しました。最大ドローダウンは16%に抑えられており、最も悪い時期は2021年後半でした。これはUSTECの最悪期とは異なっており、パフォーマンスの非相関性を示唆しています。

orb2月次収益

orb2月次ドローダウン

Alpha: 4.8714
Beta: 0.0985

この戦略のベータ値は、最初の戦略と同じく低く、原資産との相関が低いことを示しています。特に注目すべきは、最大ドローダウンはほぼ同じ水準ながら、アルファが最初の戦略の3倍に達している点です。この優位性は、より頻繁な取引、より短い保有期間、および同一日のうちにロングとショート両方の機会を活かした内部分散効果から生まれると考えられます。また、月次の成績表からも、ドローダウンやリターンが月ごとに均等かつ一貫して分布しており、安定したパフォーマンスが確認されています。


戦略3:Concretum Bandsブレイクアウト

3つ目の戦略は、市場オープン時間中に取引されるノイズレンジブレイクアウト戦略です。この戦略は最初に、論文『Beat the Market An Effective Intraday Momentum Strategy for S&P500 ETF (SPY)』で紹介され、その後X(旧Twitter)で話題となりました。 Concretum Bandsブレイクアウト戦略のシグナルルールの背景には、日中取引における需給のアンバランスから生じる重要な価格変動を捉えることを目的としています。この戦略では、前日の終値または当日の始値を基に、ボラティリティに応じた調整を加えたボラティリティバンドを用いています。これにより、価格のランダムな変動(ノイズ)が発生する「ノイズエリア」を定義します。ルールは、市場のノイズを除外し、確率の高いモメンタムの転換を捉え、変動性の違いに対応することを目指しています。つまり、一時的な価格の揺らぎではなく、本物のトレンドの始まりに沿った取引をおこなうためのフィルターとして機能します。

バンドの計算は次のとおりです。

バンドの公式

元の論文のシグナルルールは非常によく練られているため、本記事では大きな変更は加えません。取引対象資産は引き続きUSTECを使用し、リスク管理の方法も簡潔にするため同じアプローチを維持します。これにより、論文の結果とは異なる結果になる可能性があります。シグナルルールは以下の通りです。

  • 市場オープン後、1分足が上限バンドを上抜けたら買いエントリー
  • 市場オープン後、1分足が下限バンドを下抜けたら売りエントリー
  • 市場クローズ時にすべてのポジションを決済
  • ストップロスはエントリー価格から1%に設定し、さらにVWAPをトレーリングストップとして利用
  • 1回の取引あたりのリスクは口座残高の4%

EAの完全なMQL5コード:

//USTEC-M1
#include <Trade/Trade.mqh>
CTrade trade;

input int startHour = 18;
input int startMinute = 35;
input int endHour = 23;
input int endMinute = 55;

input double risk = 4.0;
input double slp = 0.01;
input int Magic = 0;
input int maPeriod = 400;

int barsTotal = 0;
int handleMa;
double lastClose=0;
double lastOpen = 0;
double lot = 0.1;

//+------------------------------------------------------------------+
//|Initialization function                                           |
//+------------------------------------------------------------------+ 
int OnInit()
  {
   trade.SetExpertMagicNumber(Magic);
   handleMa = iMA(_Symbol,PERIOD_CURRENT,maPeriod,0,MODE_SMA,PRICE_CLOSE);
   return(INIT_SUCCEEDED);
  }

//+------------------------------------------------------------------+
//|Deinitialization function                                         |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
  {  
  }

//+------------------------------------------------------------------+
//|On tick function                                                  |
//+------------------------------------------------------------------+ 
void OnTick()
  {
   int bars = iBars(_Symbol, PERIOD_CURRENT);
   if(barsTotal != bars){
      barsTotal=bars;
      bool NotInPosition = true;
      double ma[];
      CopyBuffer(handleMa,0,1,1,ma);
      if(MarketOpened()&&!MarketClosed()){
         lastClose = iClose(_Symbol,PERIOD_CURRENT,1);
         lastOpen = iOpen(_Symbol,PERIOD_CURRENT,1);
         int startIndex = getSessionStartIndex();
         double vwap = getVWAP(startIndex);
         for(int i = PositionsTotal()-1; i>=0; i--){
            ulong pos = PositionGetTicket(i);
            string symboll = PositionGetSymbol(i);
            if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol){
               if((PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY&&lastClose<vwap)||(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL&&lastClose>vwap))trade.PositionClose(pos);
               else NotInPosition=false;
            }
         }
         double lower = getLowerBand();
         double upper = getUpperBand();
         if(NotInPosition&&lastOpen>lower&&lastClose<lower&&lastClose<ma[0])executeSell();
         if(NotInPosition&&lastOpen<upper&&lastClose>upper&&lastClose>ma[0]) executeBuy();
       } 
       if(MarketClosed()){
          for(int i = PositionsTotal()-1; i>=0; i--){
            ulong pos = PositionGetTicket(i);
            string symboll = PositionGetSymbol(i);
            if(PositionGetInteger(POSITION_MAGIC) == Magic&&symboll== _Symbol)trade.PositionClose(pos);
          }
       }
        
   }
}

//+------------------------------------------------------------------+
//| Detect if market is opened                                       |
//+------------------------------------------------------------------+ 
bool MarketOpened()
{
    datetime currentTime = TimeTradeServer(); 
    MqlDateTime timeStruct;
    TimeToStruct(currentTime, timeStruct);
    int currentHour = timeStruct.hour;
    int currentMinute = timeStruct.min;
    if (currentHour >= startHour &&currentMinute>=startMinute)return true;
    else return false;
}

//+------------------------------------------------------------------+
//| Detect if market is closed                                       |
//+------------------------------------------------------------------+ 
bool MarketClosed()
{
    datetime currentTime = TimeTradeServer(); 
    MqlDateTime timeStruct;
    TimeToStruct(currentTime, timeStruct);
    int currentHour = timeStruct.hour;
    int currentMinute = timeStruct.min;
    if (currentHour >= endHour && currentMinute >= endMinute)return true;
    else return false;
}

//+------------------------------------------------------------------+
//| Sell execution function                                          |
//+------------------------------------------------------------------+        
void executeSell() {      
       double bid = SymbolInfoDouble(_Symbol, SYMBOL_BID);
       bid = NormalizeDouble(bid,_Digits);
       double sl = bid*(1+slp);
       sl = NormalizeDouble(sl, _Digits);
       lot = calclots(bid*slp);
       trade.Sell(lot,_Symbol,bid,sl);    
}

//+------------------------------------------------------------------+
//| Buy execution function                                           |
//+------------------------------------------------------------------+     
void executeBuy() {
       double ask = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
       ask = NormalizeDouble(ask,_Digits);
       double sl = ask*(1-slp);
       sl = NormalizeDouble(sl, _Digits);
       lot = calclots(ask*slp);
       trade.Buy(lot,_Symbol,ask,sl);
}

//+------------------------------------------------------------------+
//| Get VWAP function                                                |
//+------------------------------------------------------------------+
double getVWAP(int startCandle)
{
   double sumPV = 0.0;  // Sum of (price * volume)
   long sumV = 0.0;    // Sum of volume

   // Loop from the starting candle index down to 1 (excluding current candle)
   for(int i = startCandle; i >= 1; i--)
   {
      // Calculate typical price: (High + Low + Close) / 3
      double high = iHigh(_Symbol, PERIOD_CURRENT, i);
      double low = iLow(_Symbol, PERIOD_CURRENT, i);
      double close = iClose(_Symbol, PERIOD_CURRENT, i);
      double typicalPrice = (high + low + close) / 3.0;

      // Get volume and update sums
      long volume = iVolume(_Symbol, PERIOD_CURRENT, i);
      sumPV += typicalPrice * volume;
      sumV += volume;
   }

   // Calculate VWAP or return 0 if no volume
   if(sumV == 0)
      return 0.0;
   
   double vwap = sumPV / sumV;

   // Plot the dot
   datetime currentBarTime = iTime(_Symbol, PERIOD_CURRENT, 0);
   string objName = "VWAP" + TimeToString(currentBarTime, TIME_MINUTES);
   ObjectCreate(0, objName, OBJ_ARROW, 0, currentBarTime, vwap);
   ObjectSetInteger(0, objName, OBJPROP_COLOR, clrGreen);    // Green dot
   ObjectSetInteger(0, objName, OBJPROP_STYLE, STYLE_DOT);   // Dot style
   ObjectSetInteger(0, objName, OBJPROP_WIDTH, 1);           // Size of the dot
   
   return vwap;
}

//+------------------------------------------------------------------+
//| Find the index of the candle corresponding to the session open   |
//+------------------------------------------------------------------+
int getSessionStartIndex()
{
   int sessionIndex = 1;
   // Loop over bars until we find the session open
   for(int i = 1; i <=1000; i++)
   {
      datetime barTime = iTime(_Symbol, PERIOD_CURRENT, i);
      MqlDateTime dt;
      TimeToStruct(barTime, dt);
      
      if(dt.hour == startHour && dt.min == 30)
      {
         sessionIndex = i;
         break;
      }
   }
      
   return sessionIndex;
}

//+------------------------------------------------------------------+
//| Get the number of bars from now to market open                   |
//+------------------------------------------------------------------+
int getBarShiftForTime(datetime day_start, int hour, int minute) {
    MqlDateTime dt;
    TimeToStruct(day_start, dt);
    dt.hour = hour;
    dt.min = minute;
    dt.sec = 0;
    datetime target_time = StructToTime(dt);
    int shift = iBarShift(_Symbol, PERIOD_M1, target_time, true);
    return shift;
}

//+------------------------------------------------------------------+
//| Get the upper Concretum band value                               |
//+------------------------------------------------------------------+
double getUpperBand() {
    // Get the time of the current bar
    datetime current_time = iTime(_Symbol, PERIOD_CURRENT, 0);
    MqlDateTime current_dt;
    TimeToStruct(current_time, current_dt);
    int current_hour = current_dt.hour;
    int current_min = current_dt.min;
    
    // Find today's opening price at 9:30 AM
    datetime today_start = iTime(_Symbol, PERIOD_D1, 0);
    int bar_at_930_today = getBarShiftForTime(today_start, 9, 30);
    if (bar_at_930_today < 0) return 0; // Return 0 if no 9:30 bar exists
    double open_930_today = iOpen(_Symbol, PERIOD_M1, bar_at_930_today);
    if (open_930_today == 0) return 0; // No valid price
    
    // Calculate sigma based on the past 14 days
    double sum_moves = 0;
    int valid_days = 0;
    for (int i = 1; i <= 14; i++) {
        datetime day_start = iTime(_Symbol, PERIOD_D1, i);
        int bar_at_930 = getBarShiftForTime(day_start, 9, 30);
        int bar_at_HHMM = getBarShiftForTime(day_start, current_hour, current_min);
        if (bar_at_930 < 0 || bar_at_HHMM < 0) continue; // Skip if bars don't exist
        double open_930 = iOpen(_Symbol, PERIOD_M1, bar_at_930);
        double close_HHMM = iClose(_Symbol, PERIOD_M1, bar_at_HHMM);
        if (open_930 == 0) continue; // Skip if no valid opening price
        double move = MathAbs(close_HHMM / open_930 - 1);
        sum_moves += move;
        valid_days++;
    }
    if (valid_days == 0) return 0; // Return 0 if no valid data
    double sigma = sum_moves / valid_days;
    
    // Calculate the upper band
    double upper_band = open_930_today * (1 + sigma);
    
    // Plot a blue dot at the upper band level
    string obj_name = "UpperBand_" + TimeToString(current_time, TIME_DATE|TIME_MINUTES|TIME_SECONDS);
    ObjectCreate(0, obj_name, OBJ_ARROW, 0, current_time, upper_band);
    ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 159); // Dot symbol
    ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue);
    ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 2);
    
    return upper_band;
}

//+------------------------------------------------------------------+
//| Get the lower Concretum band value                               |
//+------------------------------------------------------------------+
double getLowerBand() {
    // Get the time of the current bar
    datetime current_time = iTime(_Symbol, PERIOD_CURRENT, 0);
    MqlDateTime current_dt;
    TimeToStruct(current_time, current_dt);
    int current_hour = current_dt.hour;
    int current_min = current_dt.min;
    
    // Find today's opening price at 9:30 AM
    datetime today_start = iTime(_Symbol, PERIOD_D1, 0);
    int bar_at_930_today = getBarShiftForTime(today_start, 9, 30);
    if (bar_at_930_today < 0) return 0; // Return 0 if no 9:30 bar exists
    double open_930_today = iOpen(_Symbol, PERIOD_M1, bar_at_930_today);
    if (open_930_today == 0) return 0; // No valid price
    
    // Calculate sigma based on the past 14 days
    double sum_moves = 0;
    int valid_days = 0;
    for (int i = 1; i <= 14; i++) {
        datetime day_start = iTime(_Symbol, PERIOD_D1, i);
        int bar_at_930 = getBarShiftForTime(day_start, 9, 30);
        int bar_at_HHMM = getBarShiftForTime(day_start, current_hour, current_min);
        if (bar_at_930 < 0 || bar_at_HHMM < 0) continue; // Skip if bars don't exist
        double open_930 = iOpen(_Symbol, PERIOD_M1, bar_at_930);
        double close_HHMM = iClose(_Symbol, PERIOD_M1, bar_at_HHMM);
        if (open_930 == 0) continue; // Skip if no valid opening price
        double move = MathAbs(close_HHMM / open_930 - 1);
        sum_moves += move;
        valid_days++;
    }
    if (valid_days == 0) return 0; // Return 0 if no valid data
    double sigma = sum_moves / valid_days;
    
    // Calculate the lower band
    double lower_band = open_930_today * (1 - sigma);
    
    // Plot a red dot at the lower band level
    string obj_name = "LowerBand_" + TimeToString(current_time, TIME_DATE|TIME_MINUTES|TIME_SECONDS);
    ObjectCreate(0, obj_name, OBJ_ARROW, 0, current_time, lower_band);
    ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 159); // Dot symbol
    ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrRed);
    ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 2);
    
    return lower_band;
}

//+------------------------------------------------------------------+
//| Calculate lot size based on risk and stop loss range             |
//+------------------------------------------------------------------+
double calclots(double slpoints) {
    double riskAmount = AccountInfoDouble(ACCOUNT_BALANCE) * risk / 100;
    double ticksize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
    double tickvalue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
    double lotstep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
    double moneyperlotstep = slpoints / ticksize * tickvalue * lotstep;
    double lots = MathFloor(riskAmount / moneyperlotstep) * lotstep;
    lots = MathMin(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX));
    lots = MathMax(lots, SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN));
    return lots;
}

典型的な取引は次のようになります。

orb3例

バックテスト結果:

orb3設定

orb3パラメータ

orb3資産曲線

orb3結果

この戦略は、最初のORB戦略とほぼ同じ頻度で取引をおこない、約2営業日に1回が平均的です。価格変動が時にノイズレンジ内に留まり、バンドを突破できないことがあるため、毎日は取引がおこなわれません。勝率は50%を下回っていますが、これはVWAPをダイナミックなトレーリングストップとして使用しているためです。プロフィットファクターは1.3、シャープレシオは5.9と、ドローダウンに対して非常に高いリターンを示しています。

orb3比較

orb3ドローダウン

この戦略は、バイアンドホールド戦略をわずかに上回る成績を示しながら、最大ドローダウンは半分に抑えられています。しかし、前述の戦略よりも比較的頻繁に大きなドローダウン期間を経験している点が特徴です。これは、パフォーマンス自体は優れているものの、新たな高値を更新するまでに長期的なドローダウンを耐えなければならない場面が多いことを示しています。

orb3月次収益

orb3月次ドローダウン

Alpha: 1.6562
Beta: -0.1183

この戦略のベータは-11%であり、原資産との相関がわずかに逆相関であることを示しています。これは、市場のトレンドとは逆方向に動く優位性を求めるトレーダーにとって好ましい結果です。他の2つの戦略と比べると、ドローダウン月が約50%と多いものの、利益が出る月のリターンはより高い傾向にあります。このパターンからは、実運用では長期間のドローダウンを耐え忍び、より大きなリターンの局面を辛抱強く待つ必要があることが示唆されます。十分なサンプル数と堅実な期間を経て、この戦略は引き続き実運用可能であると言えます。


振り返り

前回の記事では、単一の戦略ではなくモデルシステムを構築することについて探求しました。本記事でも同様の考え方を適用しています。今回紹介した3つの戦略はいずれも株式市場のオープニングレンジブレイクアウトを起点とし、各々が収益性の高いバリエーションとして実証されています。また、学術論文を自分たちの知識と直感を組み合わせて応用し、戦略の優位性を見つけるという洞察も共有しました。このアプローチは、堅牢な取引概念を発掘し、自身の理解を深めるうえで非常に有効です。

3つの収益性のある戦略を手に入れた今、次に考えるべきはポートフォリオの視点です。それらの合算した結果、相関関係、全体の最大ドローダウンを調査し、同時に運用する前に慎重に評価する必要があります。アルゴリズム取引において、分散投資こそが真の「ホーリーグレイル(聖杯)」と言えます。これは、異なる期間における複数の戦略のドローダウンを相殺する効果があり、ある意味では、許容できるドローダウンによって最大リターンが制限されるため、多様な戦略を組み合わせることで同じドローダウン水準を維持しつつ、エクスポージャーを増やし、リターンを高めることができます。ただし、リスクを無限に拡大できるわけではなく、最小リスクは個々の取引のリスクを下回ることはありません。

分散投資を実現する一般的な方法としては以下があります。

  • 同一戦略モデルを異なる相関の低い資産に分散して運用する
  • 単一資産に対して異なる戦略モデルを運用する
  • オプション、裁定取引、個別株選択など、異なる取引手法に資金を振り分ける

重要なのは、分散が多ければ良いというわけではなく、相関が低い分散が重要であるということです。たとえば、同じ戦略をすべての暗号通貨市場に適用するのは理想的ではありません。なぜなら、ほとんどの暗号資産は広範囲で非常に高い相関を持っているからです。さらに、バックテスト上の分散だけに頼るのも誤解を生みやすいです。なぜなら、相関は日次リターンや月次リターンなどの期間によって変化し、市場の急激なレジームシフトの際には、戦略間の相関が予期せず歪んだり変動したりするからです。そのため、一部のトレーダーはバックテスト結果の相関だけでなく、実運用の結果の相関も併せて評価し、戦略の優位性が減少していないか確認することを好みます。

このような知識を踏まえたうえで、以下に3つの戦略の合成パフォーマンスのバックテスト統計を示します。

複合資産曲線

複合ドローダウン

資産とドローダウンの曲線は、異なる戦略が様々な期間においてお互いのドローダウンを相殺し合っている様子を視覚的に示しています。最大ドローダウンは約10%に抑えられており、これは個々の戦略の最大ドローダウン(いずれも15%以上)と比べて著しく低い水準です。

総合月次収益

合計月次ドローダウン

ドローダウンとリターンは月ごとに均等に分布しているように見え、特定の極端な相場環境がバックテストのパフォーマンスに偏って影響を与えているわけではないことを示唆しています。これは、3,000以上のサンプル数と、取引ごとに一貫したリスク配分がおこなわれていることから、合理的な結果と言えます。

相関行列

相関とは、各戦略のバックテストにおけるエクイティカーブの動きの類似度を示す指標であり、-1は正反対の動きを、1は完全に同じ動きを意味します。通常は2つの対象を比較して算出されます。ここでは、資産曲線の時間軸をx軸、リターン軸をy軸として相関を計算しています。

相関関係

相関行列は、3つ以上の戦略間のパフォーマンス相関を視覚化するのに役立ちます。月次期間で分析したところ、各戦略の月次リターンはわずかに相関しており、平均で約0.3でした。相関が0.5以下であれば許容範囲ですが、できれば負の相関が望ましいです。ロングとショートの両方を取っているにもかかわらず、すべての戦略は正の相関を示しています。これは、おそらく同じ資産で取引しているためです。この詳細な分析から、合成した最大ドローダウンは個別戦略よりも低いものの、月次リターンは似通っていることがわかりました。つまり、同じ資産に対して似たような戦略を取っているため、リターンが大きく変わらないということです。このことは、これらの戦略を同じポートフォリオにまとめるよりも、異なる資産や戦略と組み合わせて運用したほうが良いことを示唆しています。


結論

本記事では、Concretum Groupの学術論文に基づく3つの日中オープニングレンジブレイクアウト戦略をレビューしました。まず研究の背景を概説し、記事全体で用いられる主要な概念や手法を説明しました。次に、3つの戦略の動機を探り、改善点を明確にし、シグナルルール、MQL5コード、バックテストの統計解析を提示しました。最後に、このプロセスを振り返り、分散投資の重要性を紹介し、3戦略の統合パフォーマンスを分析しました。

本記事は、戦略開発の真の堅牢性についての洞察を提供しています。より深い統計解析は、戦略のパフォーマンス全体像とポートフォリオ内での役割を広い視点から捉えることを可能にします。これらすべての取り組みは、実運用前に理解を深め、自信を持つためのものです。読者の皆様には、紹介したフレームワークを用いて研究プロセスを再現し、EAの開発に挑戦することをお勧めします。


ファイルの表

ファイル名説明
ORB1.mq5                                     最初の戦略のMQL5 EAスクリプト
ORB2.mq52番目の戦略のMQL5 EAスクリプト
ORB3.mq53番目の戦略のMQL5 EAスクリプト

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

添付されたファイル |
ORB.zip (7.11 KB)
最後のコメント | ディスカッションに移動 (11)
Zhuo Kai Chen
Zhuo Kai Chen | 25 4月 2025 において 01:19

Ps.ORB3では、マーケットのオープン時間を9:30にハードコードしました。ORB3では、マーケットオープン時間を9:30にハードコードしました。

//+------------------------------------------------------------------+
//| コンクレタム・バンド上限値の取得|
//+------------------------------------------------------------------+
double getUpperBand(int target_hour = 17, int target_min = 30) {
    // 現在のバーの時刻を取得
    datetime current_time = iTime(_Symbol, PERIOD_CURRENT, 0);
    MqlDateTime current_dt;
    TimeToStruct(current_time, current_dt);
    int current_hour = current_dt.hour;
    int current_min = current_dt.min;
    
    // ターゲット時間(例:サーバー時間17:30)における本日の始値を検索する。
    datetime today_start = iTime(_Symbol, PERIOD_D1, 0);
    int bar_at_target_today = getBarShiftForTime(today_start, target_hour, target_min);
    if (bar_at_target_today < 0) return 0; // ターゲットバーが存在しない場合は0を返す
    double open_target_today = iOpen(_Symbol, PERIOD_M1, bar_at_target_today);
    if (open_target_today == 0) return 0; // 有効な価格なし
    
    // 過去14日間に基づくシグマの計算
    double sum_moves = 0;
    int valid_days = 0;
    for (int i = 1; i <= 14; i++) {
        datetime day_start = iTime(_Symbol, PERIOD_D1, i);
        int bar_at_target = getBarShiftForTime(day_start, target_hour, target_min);
        int bar_at_HHMM = getBarShiftForTime(day_start, current_hour, current_min);
        if (bar_at_target < 0 || bar_at_HHMM < 0) continue; // バーが存在しない場合はスキップする
        double open_target = iOpen(_Symbol, PERIOD_M1, bar_at_target);
        double close_HHMM = iClose(_Symbol, PERIOD_M1, bar_at_HHMM);
        if (open_target == 0) continue; // 有効な初値がない場合はスキップする
        double move = MathAbs(close_HHMM / open_target - 1);
        sum_moves += move;
        valid_days++;
    }
    if (valid_days == 0) return 0; // 有効なデータがない場合は0を返す
    double sigma = sum_moves / valid_days;
    
    // 上限バンドを計算する
    double upper_band = open_target_today * (1 + sigma);
    
    // 上のバンド・レベルに青い点をプロットする。
    string obj_name = "UpperBand_" + TimeToString(current_time, TIME_DATE|TIME_MINUTES|TIME_SECONDS);
    ObjectCreate(0, obj_name, OBJ_ARROW, 0, current_time, upper_band);
    ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 159); // ドット記号
    ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrBlue);
    ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 2);
    
    return upper_band;
}

//+------------------------------------------------------------------+
//| コンクレタム・バンドの下限値を得る|
//+------------------------------------------------------------------+
double getLowerBand(int target_hour = 17, int target_min = 30) {
    // 現在のバーの時刻を取得
    datetime current_time = iTime(_Symbol, PERIOD_CURRENT, 0);
    MqlDateTime current_dt;
    TimeToStruct(current_time, current_dt);
    int current_hour = current_dt.hour;
    int current_min = current_dt.min;
    
    // ターゲット時間(例:サーバー時間17:30)における本日の始値を検索する。
    datetime today_start = iTime(_Symbol, PERIOD_D1, 0);
    int bar_at_target_today = getBarShiftForTime(today_start, target_hour, target_min);
    if (bar_at_target_today < 0) return 0; // ターゲットバーが存在しない場合は0を返す
    double open_target_today = iOpen(_Symbol, PERIOD_M1, bar_at_target_today);
    if (open_target_today == 0) return 0; // 有効な価格なし
    
    // 過去14日間に基づくシグマの計算
    double sum_moves = 0;
    int valid_days = 0;
    for (int i = 1; i <= 14; i++) {
        datetime day_start = iTime(_Symbol, PERIOD_D1, i);
        int bar_at_target = getBarShiftForTime(day_start, target_hour, target_min);
        int bar_at_HHMM = getBarShiftForTime(day_start, current_hour, current_min);
        if (bar_at_target < 0 || bar_at_HHMM < 0) continue; // バーが存在しない場合はスキップする
        double open_target = iOpen(_Symbol, PERIOD_M1, bar_at_target);
        double close_HHMM = iClose(_Symbol, PERIOD_M1, bar_at_HHMM);
        if (open_target == 0) continue; // 有効な初値がない場合はスキップする
        double move = MathAbs(close_HHMM / open_target - 1);
        sum_moves += move;
        valid_days++;
    }
    if (valid_days == 0) return 0; // 有効なデータがない場合は0を返す
    double sigma = sum_moves / valid_days;
    
    // 下限バンドを計算する
    double lower_band = open_target_today * (1 - sigma);
    
    // 下のバンド・レベルに赤い点をプロットする。
    string obj_name = "LowerBand_" + TimeToString(current_time, TIME_DATE|TIME_MINUTES|TIME_SECONDS);
    ObjectCreate(0, obj_name, OBJ_ARROW, 0, current_time, lower_band);
    ObjectSetInteger(0, obj_name, OBJPROP_ARROWCODE, 159); // ドット記号
    ObjectSetInteger(0, obj_name, OBJPROP_COLOR, clrRed);
    ObjectSetInteger(0, obj_name, OBJPROP_WIDTH, 2);
    
    return lower_band;
}

計算時間を変更することで、ストラテジーをさらに最適化できるかもしれません。)

Digitus
Digitus | 25 4月 2025 において 18:15
Zhuo Kai Chen #:

OMG、これを見逃すなんて信じられない。そうあるべきだ:

うっかりミスで本当に申し訳ない。注意深く読んで、指摘してくれてありがとう。

オープンマーケットとクローズドマーケットを1つの関数に統合しましたので、ご心配なく。

bool MarketState()
{
   MqlDateTime structTime;
   TimeCurrent(structTime);
   structTime.sec = 0;
   structTime.hour = startHour;
   structTime.min = startMinute; 
   datetime timeStart = StructToTime(structTime);
   structTime.hour = endHour;
   structTime.min = endMinute;   
   datetime timeEnd = StructToTime(structTime);
   if(TimeCurrent() >= timeStart && TimeCurrent() < timeEnd)return true;
   else return false;
}


もう一つ、バックテストにブローカーのOHLCデータを使用していますが、遅延はありません。これらのバックテストは、スリッページとリクオートのためのランダムな遅延がある実際のティックデータで行われたバックテストと比較すると、少し楽観的なようです。

ご苦労様でした!

Zhuo Kai Chen
Zhuo Kai Chen | 27 4月 2025 において 02:37
Digitus #:

大丈夫、オープンマーケットとクローズドマーケットはすでに一つの機能に統合してある。


もう一つ、バックテストにはブローカーのOHLCデータを使用し、遅延はありません。これらのバックテストは、スリッページとリクオートのためのランダムな遅延を持つ実際のティックデータで行われたバックテストと比較して、少し楽観的なようです。

ご苦労様でした!

ご改造ありがとうございます!私のGithubですべてのコードを更新しました。

ご心配いただいている点についてですが、取引ロジックは新しいバーごとに発生し、ティックの動きには関与しません。その上、平均保有時間は数時間のようなもので、スリッページが大きな問題になることはないと思います。5年以上リアルティックのデータを提供しているブローカーはほとんどありませんし、OHLCは1分足で十分です。

Zero Trader
Zero Trader | 20 6月 2025 において 01:58
素晴らしい記事
Muhammad Syamil Bin Abdullah
Muhammad Syamil Bin Abdullah | 20 6月 2025 において 15:19
分かち合ってくれてありがとう。
プライスアクション分析ツールキットの開発(第20回):External Flow (IV) — Correlation Pathfinder プライスアクション分析ツールキットの開発(第20回):External Flow (IV) — Correlation Pathfinder
Correlation Pathfinderは、「プライスアクション分析ツールキット開発」連載の一環として、通貨ペアの動的な関係を理解するための新しいアプローチを提供します。このツールはデータの収集と分析を自動化し、EUR/USDやGBP/USDなどのペアがどのように連動して動いているかを可視化します。リスク管理を強化し、より効果的にチャンスを捉えるための実用的かつリアルタイムな情報で、取引戦略のレベルを引き上げましょう。
ダイナミックマルチペアEAの形成(第2回):ポートフォリオの分散化と最適化 ダイナミックマルチペアEAの形成(第2回):ポートフォリオの分散化と最適化
ポートフォリオの分散化と最適化とは、複数の資産に戦略的に投資を分散しながら、リスク調整後のパフォーマンス指標に基づいてリターンを最大化する理想的な資産配分を選定する手法です。
既存のMQL5取引戦略へのAIモデルの統合 既存のMQL5取引戦略へのAIモデルの統合
このトピックでは、強化学習モデル(LSTMなど)や機械学習ベースの予測モデルのような訓練済みAIモデルを、既存のMQL5取引戦略に組み込むことに焦点を当てています。
PythonとMQL5を使用した特徴量エンジニアリング(第4回):UMAP回帰によるローソク足パターン認識 PythonとMQL5を使用した特徴量エンジニアリング(第4回):UMAP回帰によるローソク足パターン認識
次元削減手法は、機械学習モデルのパフォーマンスを向上させるために広く用いられています。ここでは、UMAP (Uniform Manifold Approximation and Projection)という比較的新しい手法について説明します。UMAPは、古い手法に見られるデータの歪みや人工的な構造といった欠点を明確に克服することを目的として開発されました。UMAPは非常に強力な次元削減技術であり、似たローソク足を新たに効果的にグループ化できるため、アウトオブサンプル(未知データ)に対する誤差率を低減し、取引パフォーマンスを向上させることができます。