【MQL5】取引時間を制限してみる

当ページのリンクには広告が含まれています。
目次

作るもの

資金管理処理を実装したEA に、時間による取引の制限を追加します。

今回実装する処理は以下のとおり。

  1. 日本時間の土日は取引しないようにする
  2. 日本時間の 23:00:00~08:59:59 はエントリーしないようにする
  3. 日本時間の 00:00:00(~08:59:59) の時点でポジションを保有していたら、手仕舞う

※ 実行環境のタイムゾーンは 東京(UTC+09:00) に設定されているものとする。

②は、普段の裁量トレードで私がそうしているからです。
安心して寝たいからです。

③は、金曜日以外はやらなくても良さそうですが、ここに曜日の条件を追加するのが面倒でした。
ごめんなさい。
大した手間じゃないんですけどね...m(__)m

①は、②、③を実装すれば不要なはずですが、念のため。

今回は、市場の切替わり前や、経済指標発表前後の取引は抑止しません。
こっちのほうが重要かもしれないけど...

参考サイト

実装

※ このページの末尾に、コード全体を記載しています。

バックテスト時の日時について

現在の日時を取得する関数として、TimeLocal関数 や TimeGMT関数 あたりが候補にあがります。
TimeLocal関数は、ローカルPCの現在時刻を取得できます。
TimeGMT関数は、現在のGMTを取得できます。

そのため、これらのどちらかを使用すればよさそうなのですが、残念ながらバックテストでは、これらの関数で取得した時刻をそのままは使用できません

これらの関数にはそれぞれ、公式リファレンスに次の記述があります。

< TimeLocal関数 >

During testing in the strategy tester, TimeLocal() is always equal to TimeCurrent() simulated server time.

< TimeGMT関数 >

During testing in the strategy tester, TimeGMT() is always equal to TimeTradeServer() simulated server time.

詳しいことはさておき、とりあえず、欲している時刻は取得できないようです。

試しに取引時の時刻を TimeLocal関数とTimeGMT関数で取得し、Print関数で出力してみると、次のようになります。
※ 画像をクリックすると、大きく表示されます。

TimeLocal関数で取得した時刻も、TimeGMT関数で取得した時刻も、ローソク足の時刻と同じになります。

比較のために、カスタムインジケータでリアルタイムに出力した時刻も示します。

TimeLocal関数で取得した時刻は、ローソク足の時刻+6時間 (夏時間の場合) となります。
+6時間の部分は、冬時間の期間は+7時間になります。
ローソク足の時刻が日本時間と 6 or 7時間ズレるのは、MT5の仕様です。

TimeGMT関数で取得した時刻は、TimeLocal関数で取得した時刻-9時間となります。
(というか、ローカル時間が GMT+9時間)

バックテスト時は GMTすらズレています
これらのことを踏まえて、リアルタイムで取得したときと同じ状況になるように、日本時間を調整する必要があります...(;´Д`)

具体的には、夏時間なら TimeLocal関数で取得した時間+6時間、冬時間なら+7時間 を日本時間とします。
今回はざっくり夏時間は4~10月、冬時間は11月~3月とします。

< 実装例 >

#define __BACK_TEST__

MqlDateTime GetLocalTime()
{
    // 現在時刻を取得
    MqlDateTime now;
    datetime ndt = TimeLocal(now);

// バックテスト用
#ifdef __BACK_TEST__
    // 冬時間の差分
    MqlDateTime rd = { 1970, 1, 1, 7, 0, 0, 0, 0 };

    // 夏時間
    if ((now.mon >= 4) && (now.mon <= 10))
        rd.hour = 6;

    datetime rdt = StructToTime(rd);

    ndt += rdt;
    TimeToStruct(ndt, now);
#endif

    return now;
}

処理内容はただの時刻の足し算ですので、使用している関数の定義だけ示します。

TimeLocal関数、MqlDateTime構造体

datetime TimeLocal(
  MqlDateTime&  dt_struct      // 構造体型の変数
);

struct MqlDateTime
{
  int year;          // 年
  int mon;           // 月
  int day;           // 日
  int hour;          // 時間
  int min;           // 分
  int sec;           // 秒
  int day_of_week;   // 曜日(日曜が 0、月曜が 1、...土曜が 6 )
  int day_of_year;   // 年の日番号(1月1日には、ゼロの数の値が割り当てられる)
};

datetime型は、1970/1/1 00:00:00 からの経過秒数です。

StructToTime関数

datetime StructToTime(
  MqlDateTime&  dt_struct      // 日付と時刻の構造体
);

TimeToStruct関数

bool TimeToStruct(
  datetime      dt,            // 日付と時刻
  MqlDateTime&  dt_struct      // 値を採用するための構造体
);

戻り値は関数の成否。

エントリーの抑止

bool isSleeping = false;  // 停止中か

void OnTick()
{
    (省略)

    // 足確定
    if (dt != prevBarDt)
    {
        bool prevState = isSleeping;

        // 現在時刻を取得
        MqlDateTime now = GetLocalTime();

        // 土日は取引しない
        isSleeping = (now.day_of_week == 0) || (now.day_of_week == 6);

        // 00:00:00~08:59:59は取引しない
        isSleeping = isSleeping || (now.hour < 9) || (now.hour >= 23);

        if (prevState != isSleeping)
        {
            if (isSleeping)
                PrintFormat("****** Sleep [%d/%.2d/%.2d %.2d:%.2d:%.2d (%d)] ******", now.year, now.mon, now.day, now.hour, now.min, now.sec, now.day_of_week);
            else
                PrintFormat("****** Awake [%d/%.2d/%.2d %.2d:%.2d:%.2d (%d)] ******", now.year, now.mon, now.day, now.hour, now.min, now.sec, now.day_of_week);
        }

        if (status > 0)
            CheckExit();
        else if (!isSleeping)
            CheckEnter();

        (省略)
    }
}

足確定後の処理にて、前述の処理でローカル時刻を取得し、エントリーして良いか判断しています。

時間によるエグジット

void CheckExit()
{
    (省略)

    // 現在時刻を取得
    MqlDateTime now = GetLocalTime();

    // 00:00:00~08:59:59 または
    // ロングポジション保有中で下抜けた または
    // ショートポジション保有中で上抜けた
    if ((now.hour < 9) ||
        ((status == 1) && (rt[0].close < ma[0])) ||
        ((status == 2) && (rt[0].close > ma[0])))
    {
        PrintFormat("****** (exit) Local time=%d/%.2d/%.2d %.2d:%.2d:%.2d (%d)", now.year, now.mon, now.day, now.hour, now.min, now.sec, now.day_of_week);

        (省略)
    }
}

エグジットの条件確認時に、前述の処理でローカル時刻を取得し、時間による判定を追加しています。

動かしてみる

エントリー・エグジットの前に、ローカル時間を出力するようにしています。

夏時間

4月

10月

① 23:00:00 (17:00:00 +6時間) にエントリー停止

② 00:00:00 (18:00:00 +6時間) に保有ポジションをエグジット

③ 9:00:00 ( 3:00:00 +6時間) にエントリー許可

冬時間

11月

3月

① 23:00:00 (16:00:00 +7時間) にエントリー停止

② 00:00:00 (17:00:00 +7時間) に保有ポジションをエグジット

③ 9:00:00 ( 2:00:00 +7時間) にエントリー許可

できてそうですね。

これで、資金管理と合わせて、リスク低減策はとりあえず一段落かな。
スプレッドが拡がってたらエントリーしないというのも入れたいけど、記事にするほどもものでもないし。

次はちょっと、インジケータを弄ってみます。
それではまた。

コード全体

//+------------------------------------------------------------------+
//|                                                      Sample3.mq5 |
//|                                            Copyright 2024, Hailu |
//|                                             https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2024, Hailu"
#property link      "https://www.mql5.com"
#property version   "1.00"

input uint  MaPeriod                = 20;   // MA period
input uint  LimitPoints             = 1000; // Limit width [points]
input uint  StopPoints              = 300;  // Stop width [points]
input uint  AllowableLossPerTrade   = 2;    // Allowable loss per trade [%]
input uint  AllowableLoss           = 20;   // Allowable loss [%]
input ulong AllowableSlipPoints     = 20;   // Allowable slip width [points]

int                     hMA = 0;               // MAインジケータのハンドル
int                     status = 0;            // 0:未エントリー / 1:ロングポジション保有 / 2:ショートポジション保有
datetime                prevBarDt;             // 1つ前のローソク足の開始時刻
ENUM_ORDER_TYPE_FILLING fillMode;              // 執行条件
double                  iniEquity = 0;         // EA起動時の証拠金
bool                    isSleeping = false;    // 停止中か

ulong magic = 3;    // このEAのID

#define __BACK_TEST__

//--------------------------------------------------------------------
// 買い・売り条件を満たしていれば注文する
//--------------------------------------------------------------------
void CheckEnter()
{
    // ローソク足を取得
    MqlRates rt[3];
    if (CopyRates(_Symbol, _Period, 0, 3, rt) != 3)
    {
        PrintFormat("Failed to get history data. [%d]", GetLastError());
        return;
    }

    // MAを取得
    double ma[2];
    if (CopyBuffer(hMA, 0, 0, 2, ma) != 2)
    {
        PrintFormat("Failed to get MA values. [%d]", GetLastError());
        return;
    }

    // 余剰証拠金額を取得
    double freeMargin = AccountInfoDouble(ACCOUNT_MARGIN_FREE);

    // 1ロットの通貨数量を取得
    double tradeSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE);

    // 損切り幅からロット数を求める
    double loss = freeMargin * AllowableLossPerTrade / 100;
    double lots = loss / (StopPoints * _Point) / tradeSize;

    // 現在値とレバレッジからロット数を制限
    double maxLots = freeMargin * 25 / tradeSize / SymbolInfoDouble(_Symbol, SYMBOL_ASK);
    lots = MathMin(lots, maxLots);

    lots = floor(lots * 10) / 10;

    // 上に抜けた
    if ((rt[0].close <= ma[0]) && (rt[1].close > ma[0]))
    {
        MqlDateTime now = GetLocalTime();
        PrintFormat("****** (buy) Local time=%d/%.2d/%.2d %.2d:%.2d:%.2d (%d)", now.year, now.mon, now.day, now.hour, now.min, now.sec, now.day_of_week);

        MqlTradeRequest req;
        MqlTradeResult res;
        ZeroMemory(req);
        ZeroMemory(res);

        req.action       = TRADE_ACTION_DEAL;
        req.symbol       = _Symbol;
        req.volume       = lots;
        req.price        = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
        req.sl           = req.price - StopPoints * _Point;
        req.tp           = req.price + LimitPoints * _Point;
        req.deviation    = AllowableSlipPoints;
        req.type         = ORDER_TYPE_BUY;
        req.type_filling = ORDER_FILLING_IOC;
        req.magic        = magic;

        if (OrderSend(req, res))
        {
            Print(res.comment);

            if (res.retcode != TRADE_RETCODE_DONE)
                PrintFormat("Failed to send an order. [%d]", GetLastError());
            else
                status = 1;
        }
        else
        {
            PrintFormat("Failed to send an order. [%d]", GetLastError());
        }
    }
    // 下に抜けた
    else if ((rt[0].close >= ma[0]) && (rt[1].close < ma[0]))
    {
        MqlDateTime now = GetLocalTime();
        PrintFormat("****** (sell) Local time=%d/%.2d/%.2d %.2d:%.2d:%.2d (%d)", now.year, now.mon, now.day, now.hour, now.min, now.sec, now.day_of_week);

        MqlTradeRequest req;
        MqlTradeResult res;
        ZeroMemory(req);
        ZeroMemory(res);

        req.action       = TRADE_ACTION_DEAL;
        req.symbol       = _Symbol;
        req.volume       = lots;
        req.price        = SymbolInfoDouble(_Symbol, SYMBOL_BID);
        req.sl           = req.price + StopPoints * _Point;
        req.tp           = req.price - LimitPoints * _Point;
        req.deviation    = AllowableSlipPoints;
        req.type         = ORDER_TYPE_SELL;
        req.type_filling = ORDER_FILLING_IOC;
        req.magic        = magic;

        if (OrderSend(req, res))
        {
            Print(res.comment);

            if (res.retcode != TRADE_RETCODE_DONE)
                PrintFormat("Failed to send an order(entry). [%d][%d]", res.retcode, GetLastError());
            else
                status = 2;
        }
        else
        {
            PrintFormat("Failed to send an order(entry). [%d]", GetLastError());
        }
    }
}

//--------------------------------------------------------------------
// 決済条件を満たしていれば決済する
//--------------------------------------------------------------------
void CheckExit()
{
    if (status == 0)
        return;

    // ローソク足を取得
    MqlRates rt[2];
    if (CopyRates(_Symbol, _Period, 0, 2, rt) != 2)
    {
        PrintFormat("Failed to get history data. [%d]", GetLastError());
        return;
    }

    // MAを取得
    double ma[2];
    if (CopyBuffer(hMA, 0, 0, 2, ma) != 2)
    {
        PrintFormat("Failed to get MA values. [%d]", GetLastError());
        return;
    }

    // 現在時刻を取得
    MqlDateTime now = GetLocalTime();

    // 00:00:00~08:59:59 または
    // ロングポジション保有中で下抜けた または
    // ショートポジション保有中で上抜けた
    if ((now.hour < 9) ||
        ((status == 1) && (rt[0].close < ma[0])) ||
        ((status == 2) && (rt[0].close > ma[0])))
    {
        PrintFormat("****** (exit) Local time=%d/%.2d/%.2d %.2d:%.2d:%.2d (%d)", now.year, now.mon, now.day, now.hour, now.min, now.sec, now.day_of_week);

        // 決済
        MqlTradeRequest req;
        MqlTradeResult res;
        ZeroMemory(req);
        ZeroMemory(res);

        req.action       = TRADE_ACTION_DEAL;
        req.position     = PositionGetTicket(0);
        req.symbol       = _Symbol;

        PositionSelectByTicket(req.position);
        req.volume = PositionGetDouble(POSITION_VOLUME);

        req.deviation    = AllowableSlipPoints;
        req.type         = status == 1 ? ORDER_TYPE_SELL : ORDER_TYPE_BUY;
        req.type_filling = ORDER_FILLING_IOC;
        req.magic        = magic;
        
        if (OrderSend(req, res))
        {
            Print(res.comment);

            if (res.retcode != TRADE_RETCODE_DONE)
                PrintFormat("Failed to send an order(exit). [%d][%d]", res.retcode, GetLastError());
        }
        else
        {
            PrintFormat("Failed to send an order(exit). [%d]", GetLastError());
        }
    }
}
//--------------------------------------------------------------------
// ローカル時間を取得
//--------------------------------------------------------------------
MqlDateTime GetLocalTime()
{
    // 現在時刻を取得
    MqlDateTime now;
    datetime ndt = TimeLocal(now);

// バックテスト用
#ifdef __BACK_TEST__
    // 冬時間の差分
    MqlDateTime rd = { 1970, 1, 1, 7, 0, 0, 0, 0 };

    // 夏時間
    if ((now.mon >= 4) && (now.mon <= 10))
        rd.hour = 6;

    datetime rdt = StructToTime(rd);

    ndt += rdt;
    TimeToStruct(ndt, now);
#endif

    return now;
}
//--------------------------------------------------------------------
// 執行条件を選択
//--------------------------------------------------------------------
void SelectFillType()
{
    long fm = SymbolInfoInteger(_Symbol, SYMBOL_FILLING_MODE);

    if ((fm & SYMBOL_FILLING_FOK) != 0)
        fillMode = ORDER_FILLING_FOK;
    else
        fillMode = ORDER_FILLING_IOC;

    PrintFormat("Filling mode = %d", fillMode);
}

//+------------------------------------------------------------------+
//| Expert initialization function                                   |
//+------------------------------------------------------------------+
int OnInit()
{
    // 執行条件を選択
    SelectFillType();

    // 有効証拠金残高を退避
    iniEquity = AccountInfoDouble(ACCOUNT_EQUITY);

    // MAインジケータを作成
    hMA = iMA(_Symbol, _Period, MaPeriod, 0, MODE_SMA, PRICE_CLOSE);
    if (hMA == INVALID_HANDLE)
    {
        PrintFormat("Failed to create MA indicators. [%d]", GetLastError());
        return INIT_FAILED;
    }

    // 1つ前のローソク足の時刻を初期化
    prevBarDt = iTime(_Symbol, _Period, 0);

    return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
//| Expert deinitialization function                                 |
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
//---
   
}
//+------------------------------------------------------------------+
//| Expert tick function                                             |
//+------------------------------------------------------------------+
void OnTick()
{
    // 現在のローソク足の開始時刻を取得
    datetime dt = iTime(_Symbol, _Period, 0);
    if (dt == 0)
    {
        PrintFormat("Failed to get time. [%d]", GetLastError());
        return;
    }

    // 足確定
    if (dt != prevBarDt)
    {
        bool prevState = isSleeping;

        // 現在時刻を取得
        MqlDateTime now = GetLocalTime();

        // 土日は取引しない
        isSleeping = (now.day_of_week == 0) || (now.day_of_week == 6);

        // 23:00:00~08:59:59は取引しない
        isSleeping = isSleeping || (now.hour < 9) || (now.hour >= 23);

        if (prevState != isSleeping)
        {
            if (isSleeping)
                PrintFormat("****** Sleep [%d/%.2d/%.2d %.2d:%.2d:%.2d (%d)] ******", now.year, now.mon, now.day, now.hour, now.min, now.sec, now.day_of_week);
            else
                PrintFormat("****** Awake [%d/%.2d/%.2d %.2d:%.2d:%.2d (%d)] ******", now.year, now.mon, now.day, now.hour, now.min, now.sec, now.day_of_week);
        }

        if (status > 0)
            CheckExit();
        else if (!isSleeping)
            CheckEnter();

        prevBarDt = dt;
    }
}
//+------------------------------------------------------------------+
//| Trade function                                                   |
//+------------------------------------------------------------------+
void OnTrade()
{
    if (PositionsTotal() <= 0)
    {
        status = 0;

        // 有効証拠金額を確認
        double equity = AccountInfoDouble(ACCOUNT_EQUITY);

        // 損失が許容額を超えていれば終了
        if (equity < (iniEquity * (1 - (float)AllowableLoss / 100)))
        {
            PrintFormat("[WARNING] Initial equity=%.1f, Equity=%.1f", iniEquity, equity);
            ExpertRemove();
        }
    }
}
//+------------------------------------------------------------------+
よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

コメント

コメントする

CAPTCHA



reCaptcha の認証期間が終了しました。ページを再読み込みしてください。

目次