作るもの
ごく単純なルールで取引するEA に、最低限の資金管理処理を追加します。
今回実装する処理は以下のとおり。
- 損切り幅と有効証拠金額からロット数を決める
- EA起動時から証拠金が一定割合減ったら、EAを終了させる
EAを過去数年分動かしてみると、ずっと同じロット数で取引するのは現実に即してないなと思ったので、①を追加します。
②はついでです。
いつか EAを運用してみたくなったら、少なくともこれは必要になるだろうなと思いました。
ビビリなんで。(;´Д`)
参考サイト
実装
※ このページの末尾に、コード全体を記載しています。
ロット数の調整
input uint StopPoints = 300; // Stop width [points]
input uint AllowableLossPerTrade = 2; // Allowable loss per trade [%]
(省略)
// 買い・売り条件を満たしていれば注文する
void CheckEnter()
{
(省略)
// 余剰証拠金額を取得
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;
PrintFormat("****** freeMargin=%.1f, loss=%.1f, size=%.1f, lots=%.1f", freeMargin, loss, tradeSize, lots);
(省略)
}
// 決済条件を満たしていれば決済する
void CheckExit()
{
(省略)
MqlTradeRequest req;
ZeroMemory(req);
req.position = PositionGetTicket(0);
PositionSelectByTicket(req.position);
req.volume = PositionGetDouble(POSITION_VOLUME);
(省略)
}
余剰証拠金額を取得する
余剰証拠金額は、AccountInfoDouble関数で取得できます。
AccountInfoDouble関数の定義は以下のとおり。
double AccountInfoDouble(
ENUM_ACCOUNT_INFO_DOUBLE property_id // プロパティ識別子
);
property_id に ACCOUNT_MARGIN_FREE を指定すると、余剰証拠金額を取得できます。
property_id には、他にも ACCOUNT_PROFIT(未決済の損益) や、後述する ACCOUNT_EQUITY(有効証拠金額) などを指定できます。
1ロットの通貨数量を取得する
1ロットの通貨数量は SymbolInfoDouble関数で取得できます。
SymbolInfoDouble(_Symbol, SYMBOL_TRADE_CONTRACT_SIZE)
と記述すると取得できます。
ロット数を求める
以下の値から、あーだこーだしてロット数を求めます。算数です。
なお、この例ではクロス円しか考慮していません。
- 余剰証拠金額
- 1ロットの通貨数量
- 損切り幅
- 1取引当たりの許容損失割合[%]
- 現在値 (この例では常にASKを採用)
- レバレッジ (この例では25倍固定)
最後に、私の口座の都合により、小数点以下第2位を切り捨てています。
PrintFormat は動作確認用に書いています。
( ****** は、操作ログでこのメッセージを見つけ易くするためのものです。)
エグジット時のロット数
エグジット時に MqlTradeRequest構造体の volume(ロット数) に設定する値を、ポジション情報から取得します。
まず、PositionSelectByTicket関数を使用して、操作対象のポジションを選択します。
PositionSelectByTicket関数の定義は以下のとおり。
bool PositionSelectByTicket(
ulong ticket // ポジションチケット
);
戻り値は関数の成否。
ticket には、MqlTradeRequest構造体の position に設定する値をそのまま指定します。
次に、PositionGetDouble関数を使用して、ロット数を取得します。
PositionGetDouble関数の定義は以下のとおり。
double PositionGetDouble(
ENUM_POSITION_PROPERTY_DOUBLE property_id // プロパティ識別子
);
関数が失敗した場合は 0(ゼロ)が返される。
property_id に POSITION_VOLUME を指定すると、ロット数を取得できます。
この例では PositionSelectByTicket関数 や PositionGetDouble関数 がエラーを返しても何もしていませんが、何かしらのエラー処理を行ったほうが安全です。
動かしてみる
まずは、増えていくパターン。
最初は 0.6ロット。
※ 画像をクリックすると、大きく表示されます。

増えた!

次は減るパターン。

大丈夫そうか。
それじゃあ、次にいこう。
損失によるEA終了
input uint AllowableLoss = 20; // Allowable loss [%]
double iniEquity = 0; // EA起動時の証拠金
(省略)
int OnInit()
{
// 有効証拠金残高を退避
iniEquity = AccountInfoDouble(ACCOUNT_EQUITY);
(省略)
}
void OnTrade()
{
if (PositionsTotal() <= 0)
{
// 有効証拠金額を確認
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
// 損失が許容額を超えていれば終了
if (equity < (iniEquity * (1 - (float)AllowableLoss / 100)))
{
PrintFormat("****** iniEquity=%.1f, equity=%.1f", iniEquity, equity);
ExpertRemove();
}
}
}
EA起動時の有効証拠金額を確認する
OnInit() で有効証拠金額を退避しておきます。
前述のとおり、有効証拠金額は AccountInfoDouble関数で取得できます。
引数には ACCOUNT_EQUITY を指定します。
エグジット後に有効証拠金額を確認する
今回はポジションを1つしか保有しないプログラムを基にしているので、OnTrade() で保有ポジション数が 0(ゼロ) のときにだけ損失額を確認すれば十分です。
ここでも有効証拠金額を取得し、EA起動時からの損失額が入力パラメータの許容損失割合[%] を超えていれば、EAを終了させます。
EAの終了は、ExpertRemove関数で行えます。
ExpertRemove関数の定義は以下のとおり。
void ExpertRemove();
動かしてみる

よしよし。
証拠金が 20%減ったところで EAを終了している。
これで、ほんとに最低限の資金管理処理はできたと思います。
次は、取引時間の制限かなぁ。
それではまた。
コード全体
//+------------------------------------------------------------------+
//| Sample2.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起動時の証拠金
ulong magic = 2; // このEAのID
//--------------------------------------------------------------------
// 買い・売り条件を満たしていれば注文する
//--------------------------------------------------------------------
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]))
{
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]))
{
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;
}
// ロングポジション保有中で下抜けた または
// ショートポジション保有中で上抜けた
if (((status == 1) && (rt[0].close < ma[0])) ||
((status == 2) && (rt[0].close > ma[0])))
{
// 決済
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());
}
}
}
//--------------------------------------------------------------------
// 執行条件を選択
//--------------------------------------------------------------------
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)
{
if (status > 0)
CheckExit();
else
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();
}
}
}
//+------------------------------------------------------------------+
コメント