作るもの
資金管理処理を実装したEA に、時間による取引の制限を追加します。
今回実装する処理は以下のとおり。
- 日本時間の土日は取引しないようにする
- 日本時間の 23:00:00~08:59:59 はエントリーしないようにする
- 日本時間の 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();
}
}
}
//+------------------------------------------------------------------+
コメント