カピバラEA バージョン1.0.1 解説

最初に実戦投入したEAのソースコードです。

実戦投入期間:2020.8.17 ~ 2021.1.5

【戦績】
初期投資金額:110,000円
純益:4,213円
最終残高:114,213円
運用利率:9.2% (年利換算)
クレジット:67,599円
クレジット込 初期投資金額:177,599円
クレジット込 運用利率:5.7% (年利換算)

今の時点の解説はプログラミングの知識が無いときついかもです。
そのうち全くの初心者でも分かるように更新していきたい。

さっそく上から順番に解説していきます。

ヘッダー部分

まずはプログラムの最初のヘッダー部分。
ここはプログラムが実行される前の各種定義など準備をしてる部分になります。

//+------------------------------------------------------------------+
//|                                                   CapybaraEA.mq4 |
//|                                                      Capybara-FX |
//|                                             https://small-fx.com |
//+------------------------------------------------------------------+

濃い緑色になってるのはコメントです。
//で始まる行、/*と*/で囲われた部分がコメントになります。
プログラムには影響ないですが、メモなどを書いておいてソースを読みやすくします。
一番上のコメントはEAを作ったときにEA名、作者、作者のHPが自動で記載されます。

#property copyright "Capybara-FX"
#property link      "https://small-fx.com"
#property version   "1.01"
#property strict

次の#propertyから始まる部分はEAのバージョン情報に表示される内容を設定してます。
最後のstrictはこれとは別に、MQL4言語の仕様変更に伴って互換性を確保するために導入されたものです。新しい仕様でコンパイルするためには入れておく必要があります。

// ライブラリインクルード
#include <stdlib.mqh>

ライブラリ(便利な機能があらかじめ実装されたもの)を読み込んで使えるように宣言してます。
エラーの詳細ログを出力してくれるErrorDescription()関数を使うためにstdlib.mqhをインクルードしてます。

// マクロ定義
#define MAGIC_NO    20200813        // EA用マジックナンバー
#define OBJ_HEAD    (__FILE__+"_") // オブジェクトヘッダ

マクロを定義してます。マクロはコンパイルの時に文字列を置き換えてくれます。
MAGIC_NOという記載が20200813に置き換えられます。マジックナンバーというのはEAを識別する固有の番号になります。ここでは日付にしてます。

もう一つのOBJ_HEADはオブジェクトを生成する際に固有の名前をつけるために使っています。”ファイル名_”で置き換えられます。

// インプットパラメータ
input double _InputRangeMAX         = 101.261;      // トラップレンジ上限[円]
input double _InputRangeMIN         = 61.669;       // トラップレンジ下限[円]
input double _InputProfitMargin     = 0.64;         // 利食い幅[円]
input int _InputNumSellTraps        = 31;           // 売りトラップ本数[本] 
input int _InputNumBuyTraps         = 31;           // 買いトラップ本数[本]
input double _InputSellStop         = 109.86;       // 売りトラップのロスカットライン[円]
input double _InputBuyStop          = 53.07;        // 買いトラップのロスカットライン[円]
input double _InputDifferenceRimit  = 1.0;          // 許容する目標価格と約定価格の価格差[円]

sinput int _InputBarancePercentage  = 100;          // 残高のうちEAで利用する割合[%]
sinput int _InputSlip               = 50;            // 許容スリッページ[単位:0.1pips]

ここでinput修飾子を使ってEAの入力パラメータを設定してます。
EAを読み込んだ時に設定できます。入力されている数字がデフォルト値、後ろのコメントが説明として表示されます。

sinput修飾子はストラテジーテスト時に最適化パラメータから外されます。
最適化に影響のない残高の利用率と許容スリッページはsinputで指定してます。

//---- 型宣言 --------------------------------------------------------
struct struct_TrapInfo{ // トラップ情報構造体
    double Balance; // 使用可能な残高
    double BuyLot;  // 買いトラップ1本あたりロット数
    double SellLot; // 売りトラップ1本あたりロット数
    double CenterPrice; // トラップを仕掛けるレンジの中央価格
    double BuyTrapWidth;    // 買いトラップの幅
    double SellTrapWidth;   // 売りトラップの幅
    double minCapitalAdd;   // 増資可能最低額(トラップ当たりLot数を増やせる金額)
};

struct struct_PositionInfo{ // 保有ポジション情報構造体
    double  target_price;   // 目標エントリー価格
    double  entry_price;    // エントリー価格
    double  lots;           // オーダーロット数
    int     ticket_no;      // チケットナンバー
};

構造体を定義してます。
構造体は関連する複数の変数をひと塊にまとめて、使いやすくしたものと思っておけばいいと思います。
アカウント情報を保存するためのTrapInfo構造体、保有ポジション情報を保存するためのPositionInfo構造体の2つを定義してます。

// 静的グローバル変数
static struct_TrapInfo _TrapInfoData;   // アカウント情報構造体のデータ
static long _Leverage = 25;         // レバレッジ[倍]
static double _MinLot = 0.01;       // 最小売買単位[lot]
static int _LotSize = 1000;         // 1ロットのサイズ[通貨単位]
static double _LastAsk = 0.0;       // 1tick前のAsk値
static double _LastBid = 0.0;       // 1tick前のBid値

グローバル変数を定義してます。
グローバル変数というのはソースコードのどこからでも参照できる変数です。

// ポジション用動的配列
struct_PositionInfo _PositionBuyBuffer[];   // 売りポジション用バッファ
struct_PositionInfo _PositionSellBuffer[];  // 買いポジション用バッファ

ポジションは相場によって変動するので、動的配列としてバッファを確保しておきます。

以上がヘッダ部分です。
このあと、イベントが発生した際の動作が記載されてプログラムになっています。
このEAで使ってるイベントは、初期化、終了、ティック受信の3つだけです。
順番に解説していきます。

初期化処理

EAが読み込まれた際にこのOnInit()関数が呼ばれて実行されます。
ここでは市場情報、アカウント情報などを読み込んで、トラップを設定します。
既にポジションがある場合は、同じ設定のEAで確保されたものを判断して保有ポジションとして保存するようにしています。(一度落ちたり再読み込みした場合に対応できるようにしてます)

//+------------------------------------------------------------------+
//| 初期化イベント
//+------------------------------------------------------------------+
int OnInit()
{
    // 初期化イベント開始表示
    Print("初期化");

    // 市場情報取得
    GetMarketInfo();
    printf("起動時Bid:%.3f, Ask:%.3f",_LastBid,_LastAsk);
    // トラップ情報取得
    GetTrapInfo(_TrapInfoData);

    // ポジション配列の初期化
    if(!InitPositon()){ // ポジション初期化エラー
        return INIT_FAILED;
    }
    // 保有ポジションをチェックして同一トラップのポジションを関連付け
    if(!GetPositonInfo()){ // 保有ポジションの統合エラー
        return INIT_FAILED;
    }

    // デバッグ表示
    DispDebugInfo(_TrapInfoData);
    //DebugListPosition();

    // トラップラインを描画
    DrawObject();


    return(INIT_SUCCEEDED); // 初期化成功
}

市場情報・トラップ情報の設定

73行目までで、市場情報と設定を読み取ってトラップを設定しています。

Print()やprintf()は組み込み関数で、実行されるとMT4のエキスパートタブに文字列が表示されます。
EAがどういう状態かを知る為に入れてます。動作確認はこれらの出力を見ながら行います。

70行目のGetMarketInfo()は自作関数で、市場情報とアカウント情報を読み込んで静的グローバル変数に値を保存しています。

void GetMarketInfo()
{
    _Leverage = AccountInfoInteger(ACCOUNT_LEVERAGE);
    _MinLot = MarketInfo(Symbol(), MODE_MINLOT);
    _LotSize = (int)MarketInfo(Symbol(), MODE_LOTSIZE);
    _LastAsk = Ask;
    _LastBid = Bid;
}

設定しているのは、レバレッジ、最小売買単位、1ロットの通貨単位、1tick前のAsk値とBid値です。

そのあと73行目のGetTrapInfo(_TrapInfoData);で初期設定と口座情報からトラップ情報を生成して、TrapInfo構造体に格納してます。

void GetTrapInfo(struct_TrapInfo &in_st)
{
    in_st.Balance = (AccountBalance()+AccountCredit())*(double)_InputBarancePercentage/100.0;
    in_st.CenterPrice = (_InputRangeMAX+_InputRangeMIN)*0.5; // レンジの中央価格(レンジ上下限の平均)
    in_st.BuyTrapWidth = (in_st.CenterPrice - _InputRangeMIN)/(_InputNumBuyTraps*2-1)*2;    // 買いトラップの幅
    in_st.SellTrapWidth = (_InputRangeMAX - in_st.CenterPrice)/(_InputNumSellTraps*2-1)*2;   // 売りトラップの幅
}

設定してるのは、Balance: 口座残高(実際の残高とクレジットを合計してます。クレジットは口座開設や入金などで貰えるボーナスで、引き出せないけど証拠金として使えます)

CenterPriceはトラップ上限価格~下限価格の中央価格。この価格より上では売りポジション、下では買いポジションを取るようにしてます。

BuyTrapWidthは買いトラップの幅を計算してます。トラップ下限価格~中央価格までをトラップ幅で割ってます。微妙に変な計算をしてるのは中央価格をまたいだところでもトラップの幅が一定になるようにしてます。鶴亀算。

SellTrapWidthは売りのトラップ幅。BuyTrapWidthと基本は同様。

ポジションの設定

75行目からポジションの設定をしてます。

各ポジションの価格設定など。

保有しているポジションと比較してEAによるものは保有済みポジションとして紐づけ。

色々と情報を描画

84行目はデバッグ表示(左上の情報表示)、89行目でトラップラインを描画(売り買いを仕掛ける横線)

終了処理

EA終了時にこのOnDeinit関数が呼ばれます。
トラップラインなどのオブジェクトを破棄する。破棄しないとEAが終了しても線や表示が消えない。

//+------------------------------------------------------------------+
//| アンロードイベント
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
{
    if( IsTesting() == false){ // バックテスト時以外
        // オブジェクトの全削除
        ObjectsDeleteAll(0, OBJ_HEAD);
    }
}

あんまり書くことないかな。
バックテスト時はオブジェクトを描かないので除外して高速化。
あとはEAが作ったオブジェクトをOBJ_HEADで判断して一気に削除するObjectDeleteAll()関数でまとめて削除してます。

ティック受信時の処理

価格が変動したときにTickイベントというのを受信します。
そのときこのOnTick()関数が呼ばれます。
ここでどう判断して、どう売り買いするのかがEAのメインの処理になります。

//+------------------------------------------------------------------+
//| ティック受信イベント(EA専用イベント)
//+------------------------------------------------------------------+
void OnTick()
{
    JudgeClose(); // 決済処理判断。先にクローズ
    JudgeEntry(); // 新規ポジションエントリー判断
    TaskSetPeriod();
}

見通しを良くする為、それぞれの処理を関数にしてます。

はじめのJudgeClose()関数で、決済するか判断します。
ポジションがある場合は先にクローズするのを優先してます。

次にJudgeEntry()関数で、新規ポジションを取るかの判断をします。

最後にTaskSetPeriod()関数で、増資をするかどうかを判断してます。増資判断だけは使っている時間足に関わらず、日付が変更した時点で判断するようにしてます。

ひとつづつ解説します。

ポジション決済の判断

JudgeClose()関数です。
具体的な説明の前に、どういう処理をするかを考えておきます。

//+------------------------------------------------------------------+
//| 決済判定処理(トレーリング無し版)
//+------------------------------------------------------------------+
void JudgeClose()
{
    int BuyMaxTrapNo = 0;
    int SellMaxTrapNo = 0;
    int icount;

    // 売りポジションのチェック範囲計算
    SellMaxTrapNo = (int)((_InputRangeMAX - (Ask-_InputDifferenceRimit+_InputProfitMargin))/_TrapInfoData.SellTrapWidth);
    if(SellMaxTrapNo >= _InputNumSellTraps){  // レンジのセンターを超えて価格が下がっているとき
        SellMaxTrapNo = _InputNumSellTraps-1; // 全部のトラップを調べるようにTrapNoを最大値に設定
    }
    else if(SellMaxTrapNo < 0){              // レンジ上限より価格が上にいる場合
        return;                              // 損切にしかならないので決済せずに終了
    }

    // 買いポジションのチェック範囲計算
    BuyMaxTrapNo = (int)(((Bid+_InputDifferenceRimit-_InputProfitMargin)-_InputRangeMIN)/_TrapInfoData.BuyTrapWidth);
    if(BuyMaxTrapNo >= _InputNumBuyTraps) { // レンジのセンターを超えて価格が上昇しているとき
        BuyMaxTrapNo = _InputNumBuyTraps-1;  // 全部のトラップを調べるようにTrapNoを最大値に設定
    }
    else if(BuyMaxTrapNo < 0){               // レンジ下限より価格が下にいる場合
        return;                              // 損切にしかならないので決済せず終了
    }

    // 売りポジションの決済チェック
    for(icount=0; icount<=SellMaxTrapNo; icount++){
        if(_PositionSellBuffer[icount].ticket_no == 0){ // ポジションがオープンしてないなら次へ
            continue;
        }
        if(_PositionSellBuffer[icount].entry_price-Ask >= _InputProfitMargin){ // 利食い基準を超えているか
            // 決済処理へ
            EA_CloseOrder(_PositionSellBuffer[icount].ticket_no);
        }
    }
    
    // 買いポジションの決済チェック
    for(icount=0; icount<=BuyMaxTrapNo; icount++){
        if(_PositionBuyBuffer[icount].ticket_no == 0){ // ポジションがオープンしてないなら次へ
            continue;
        }
        if(Bid-_PositionBuyBuffer[icount].entry_price >= _InputProfitMargin){ // 利食い基準を超えているか
            // 決済処理へ
            EA_CloseOrder(_PositionBuyBuffer[icount].ticket_no);
        }
    }

}

最初のBuyMaxTrapNoとSellMaxTrapNoはチェックするトラップの範囲を代入します。(後述)
icountはカウンターです。

539行目からまず売りポジションについて考えます。

売りポジションなのでレンジの中央値よりも上の価格です。 (上図の青いラインが売りポジションのラインです)
決済は、ポジション価格よりも利食い幅以上に価格が下がったときに買いで決済します。

売りポジションの最高価格は初期設定したレンジ上限値と同じで、このポジションのNoが0。ここからポジションの価格が下がるごとにNoが1、2、と連番になっています。

このポジションNoの最大値を計算してSellMaxTrapNoに代入したい。

計算に使ってる変数は以下。

  • _InputRangeMax:初期設定したレンジの上限値
  • Ask:今の買値
  • _InputDifferenceRimit:許容価格差
  • _InputProfitMargin:利食い幅
  • _TrapInfoData.SellTrapWidth:売りトラップ幅

例えば、売りポジションNo3までが対象となるAskの価格を考えてみます。(下図)

No3のターゲット価格に対して、許容価格差分だけ高い価格が実際のNo3ポジションの購入価格上限になります。
Askがこの価格より利食い幅だけ安い価格であれば、No3のポジションは決済対象になるわけです。

これを逆にAskから追って考えると、「Ask+利食い幅ー許容価格差」の価格がNo3のターゲット価格よりも安ければNo3ポジションまで考えればよくなります。

なので、上限から「Ask+利食い幅ー許容価格差」を引いて、売りトラップ幅で割って小数点以下を切り捨てれば、考えないといけないNoが求まります。

これをやってるのが540行目です。

    // 売りポジションのチェック範囲計算
    SellMaxTrapNo = (int)((_InputRangeMAX - (Ask-_InputDifferenceRimit+_InputProfitMargin))/_TrapInfoData.SellTrapWidth);
    if(SellMaxTrapNo >= _InputNumSellTraps){  // レンジのセンターを超えて価格が下がっているとき
        SellMaxTrapNo = _InputNumSellTraps-1; // 全部のトラップを調べるようにTrapNoを最大値に設定
    }
    else if(SellMaxTrapNo < 0){              // レンジ上限より価格が上にいる場合
        return;                              // 損切にしかならないので決済せずに終了
    }

(Ask-_InputDifferenceRimit+_InputProfitMargin)が「Ask+利食い幅ー許容価格差」、上限が_InputRangeMAXなので、これから引いて、トラップ幅_TrapInfoData.SellTrapWidthで割って、int型へ変換することで小数点以下を切り捨ててます。

買いポジはこの上下を逆にした形で計算してます。

    // 買いポジションのチェック範囲計算
    BuyMaxTrapNo = (int)(((Bid+_InputDifferenceRimit-_InputProfitMargin)-_InputRangeMIN)/_TrapInfoData.BuyTrapWidth);
    if(BuyMaxTrapNo >= _InputNumBuyTraps) { // レンジのセンターを超えて価格が上昇しているとき
        BuyMaxTrapNo = _InputNumBuyTraps-1;  // 全部のトラップを調べるようにTrapNoを最大値に設定
    }
    else if(BuyMaxTrapNo < 0){               // レンジ下限より価格が下にいる場合
        return;                              // 損切にしかならないので決済せず終了
    }

ここまでで、どのポジションNoまで決済チェックするか求まったので実際にチェックしていきます。

まず売りポジションのチェック

    // 売りポジションの決済チェック
    for(icount=0; icount<=SellMaxTrapNo; icount++){
        if(_PositionSellBuffer[icount].ticket_no == 0){ // ポジションがオープンしてないなら次へ
            continue;
        }
        if(_PositionSellBuffer[icount].entry_price-Ask >= _InputProfitMargin){ // 利食い基準を超えているか
            // 決済処理へ
            EA_CloseOrder(_PositionSellBuffer[icount].ticket_no);
        }
    }

forループで上から(=No.0)から順に、最大Noまで以下をチェックしていきます。
まず、ポジションを保有してるとticket_noが0以外になるので、そこをチェック。保有してないなら次のNoへ。
ポジションを保有してる場合は、実際のポジション価格とAskの差を見て、利食い幅以上あれば決済処理へ進む。

という流れです。

(ここに決済処理の説明図を入れる)

トレーリングストップの処理はここにもうひと手間必要になります。

買いポジションも同様に568行目から処理していきます。

以上で決済処理がすべて終わるので、次に新規ポジションを取るかどうかの処理に移ります。

新規ポジションのエントリー判断

忘れてると思いますが、ティック受信イベントの処理関数OnTick()内、110行目のJudgeEntry()関数の説明になります。ティックを受信したときの2つ目の処理です。

この関数で新規ポジションを取るかどうか判断してエントリー処理を実行します。

こちらもまずはどういうときにエントリーするかの流れを考えます。

すごく単純に考えると、トラップ価格になったときにエントリーすればいいのですが、価格は大きく動くこともあるので、トラップ価格ぴったりにはならないこともあります。

なので、「前の価格から今の価格に変化したとき、トラップ価格をまたいだらエントリーする」というふうにしてやります。

価格のまたぎ方については考慮しないことにしています。
安値から高値にまたいだ場合でも、高値から安値へまたいだ場合でも同じように処理します。

ただ、これだけだとトラップ付近で価格が上下を繰り返したときにポジションをどんどん積み増してしまいます。なので、既にポジションを持っている場合はエントリーしないようにします。

これは無くても動作しますが、Tickが発生するたびにすべてのトラップをチェックしていると処理が重くなってしまいます。
そこでありえない条件のトラップは除外する処理を入れてます。

それでは見ていきましょう。

//+------------------------------------------------------------------+
//| エントリー判定処理
//+------------------------------------------------------------------+
void JudgeEntry()
{
    double HighAsk = 0.0;
    double LowAsk = 0.0;
    double HighBid = 0.0;
    double LowBid = 0.0;

    if(_LastAsk > Ask){
        HighAsk = _LastAsk;
        LowAsk = Ask;
    }else{
        HighAsk = Ask;
        LowAsk = _LastAsk;
    }
    if(_LastBid > Bid){
        HighBid = _LastBid;
        LowBid = Bid;
    }else{
        HighBid = Bid;
        LowBid = _LastBid;
    }
    _LastAsk = Ask;
    _LastBid = Bid;

    // 買い発生の必要条件:LowAskがCTRより下、またはHighAskがレンジ下限より上
    if(LowAsk  < _TrapInfoData.CenterPrice &&
       HighAsk > _InputRangeMIN)
    {
        // forループを最小限にするためトラップNoにあたりをつける
        int minNo = (int)((LowAsk-_InputRangeMIN)/_TrapInfoData.BuyTrapWidth);  // LowAskの価格はminNoのトラップの上にいる
        int maxNo = (int)((HighAsk-_InputRangeMIN)/_TrapInfoData.BuyTrapWidth); // HighAskの価格はmaxNoのトラップの上にいる
        if(minNo <-1 || maxNo >= _InputNumBuyTraps){
            return;
        }
        for(int icount = minNo+1; icount<=maxNo; icount++){ // minNo = maxNo(トラップを跨がない)ならループは回らないはず
            if(_PositionBuyBuffer[icount].ticket_no != 0){  // もし既にポジション持ってたら次へ
                continue;
            }
            if(fabs(Ask-_PositionBuyBuffer[icount].target_price)>_InputDifferenceRimit){ // 目標価格との乖離が大きすぎるので次へ
                continue;
            }
            bool long_bool = true;
            string strComment = StringFormat("%.3f,%.3f,%d,%d,%.3f",_InputRangeMAX,_InputRangeMIN,_InputNumBuyTraps,icount);
            EA_EntryOrder(long_bool, _TrapInfoData.SellLot, strComment);
        }
    }

    // 売り発生の必要条件:HighBidがCTRより上、かつLowBidがレンジ上限より下
    if(HighBid > _TrapInfoData.CenterPrice &&
       LowBid  < _InputRangeMAX)
    {
        // forループを最小限にするためトラップNoにあたりをつける
        int minNo = (int)((_InputRangeMAX-HighBid)/_TrapInfoData.SellTrapWidth); // HighBidの価格はminNoのトラップの下にいる
        int maxNo = (int)((_InputRangeMAX-LowBid)/_TrapInfoData.SellTrapWidth);  // LowBidの価格はmaxNoのトラップの下にいる
        if(minNo <-1 || maxNo >=_InputNumSellTraps){
            return;
        }
        for(int icount=minNo+1; icount <= maxNo; icount++){ // minNo = maxNo(トラップを跨がない)ならループは回らないはず
            if(_PositionSellBuffer[icount].ticket_no != 0){ // もしすでにポジションもってたら次へ
                continue;
            }
            if(fabs(Bid-_PositionSellBuffer[icount].target_price)>_InputDifferenceRimit ){ // 目標価格との乖離が大きすぎるので次へ
                continue;
            }
            bool long_bool = false;
            string strComment = StringFormat("%.3f,%.3f,%d,%d,%.3f",_InputRangeMAX,_InputRangeMIN,_InputNumSellTraps,icount);
            EA_EntryOrder(long_bool, _TrapInfoData.SellLot, strComment);
        }
    }
}

385行目から405行目では1つ前のTickと新しいTickの価格を比較してAskとBidをそれぞれHighとLowに代入しています。
価格のまたぎ方を意識しないことにしたので、このHighとLowの間にトラップ価格があるかでエントリーを判断します。

407行目からまず買いの処理をします。

    // 買い発生の必要条件:LowAskがCTRより下、またはHighAskがレンジ下限より上
    if(LowAsk  < _TrapInfoData.CenterPrice &&
       HighAsk > _InputRangeMIN)

407行目~409行目でおおまかに条件を絞ります。
買いトラップはレンジの中央より下にあるので、安値(LowAsk)がCTRより上にいるときは発生しません。また、レンジ下限値よりも高値(HighAsk)が低いときも除外します。
コードのコメントは上記と逆の言い方をしているだけで同じことを言っています。
(※この辺はエントリーにトレーリングを導入するとき変更が必要になります。)

最低限の条件を絞ったので、次にどのあたりの価格帯にいるかでおよそのあたりをつけます。
一発で絞れないのは、大きく価格が変動するとトラップを複数またぐ可能性があるのを考慮してます。

        // forループを最小限にするためトラップNoにあたりをつける
        int minNo = (int)((LowAsk-_InputRangeMIN)/_TrapInfoData.BuyTrapWidth);  // LowAskの価格はminNoのトラップの上にいる
        int maxNo = (int)((HighAsk-_InputRangeMIN)/_TrapInfoData.BuyTrapWidth); // HighAskの価格はmaxNoのトラップの上にいる
        if(minNo <-1 || maxNo >= _InputNumBuyTraps){
            return;
        }

412、413行目でチェックするトラップNoの最小値と最大値を求めます。
最小値は安値からバンド下限までの値幅をトラップ幅で割って求めます。
最大値は高値からバンド下限までの値幅をトラップ幅で割って求めます。

最小値が-1より小さい時は、安値がバンド下限よりトラップ幅分以上下にいるので除外。
(書いてて思ったけど、これおかしい。下はどこにいようと爆上げしたらトラップをまたぐ可能性がある。トラップNoに0未満を入れるとメモリアクセスエラーになって致命的なので気をつけながら要修正。というか、ここの処理自体はメモリアクセスエラーを回避するために入れてたかも。忘れた。)

最大値が設定したトラップ数より大きい場合も除外。
(これも最小値同様に爆上げした場合に発生することがあるので要見直し。ここもNoが_InputNumBuyTrapsを超えるとメモリアクセスエラーが発生して致命的なので見直す際は注意)

417行目からforループを使って上記で算出したトラップNoを処理します。

        for(int icount = minNo+1; icount<=maxNo; icount++){ // minNo = maxNo(トラップを跨がない)ならループは回らないはず
            if(_PositionBuyBuffer[icount].ticket_no != 0){  // もし既にポジション持ってたら次へ
                continue;
            }
            if(fabs(Ask-_PositionBuyBuffer[icount].target_price)>_InputDifferenceRimit){ // 目標価格との乖離が大きすぎるので次へ
                continue;
            }
            bool long_bool = true;
            string strComment = StringFormat("%.3f,%.3f,%d,%d,%.3f",_InputRangeMAX,_InputRangeMIN,_InputNumBuyTraps,icount);
            EA_EntryOrder(long_bool, _TrapInfoData.SellLot, strComment);
        }

このforループ自体がトラップをまたいだかどうかの判断になっています。
もし前後の価格がトラップをまたいだ場合はminNo>maxNoになり、またがない場合はminNo=maxNoとなります。
forループが開始する時のカウンターはminNo+1で、継続条件はmaxNo以下としてます。
もしminNo=maxNoなら、最初のminNo+1がmaxNoを超える為、継続条件を満たさずループは1回も実行されず終了します。

ということで、「トラップをまたいだ場合だけエントリーする」という条件はforループが実行されるかどうかで実装できているので、このループ中に他のエントリー条件を記載していきます。

418行目では、既にポジションを持ってるかどうかを確認をして、ポジションを持ってたらスキップしてエントリーしないようにしてます。continueというのは、この後の処理は飛ばして次のループへ行くという指示になります。

419行目では、トラップ価格とエントリー価格の価格差が初期設定した許容範囲に収まってるかを判定しています。収まっていないならスキップしてエントリーしないようにしています。

424行目までスキップせずに来たら、long_boolをtrueにして買いエントリーフラグを立てます。

425行目はエントリーに記載するコメントを生成してます。
コメントのフォーマットは「レンジの最大値,レンジの最小値,買いトラップ数,トラップNo」です。
このコメントを使ってポジションが同じ条件のEAで取得されたものかどうかの判断をしています。

最後に426行目で実際にエントリーする関数を実行してます。エントリー処理の詳細は後述。

もし複数のトラップをまたいでいた場合は、同様の処理がループされて買いのエントリー処理が終わります。

このあと430行目からは売りのエントリー処理になります。

    // 売り発生の必要条件:HighBidがCTRより上、かつLowBidがレンジ上限より下
    if(HighBid > _TrapInfoData.CenterPrice &&
       LowBid  < _InputRangeMAX)
    {
        // forループを最小限にするためトラップNoにあたりをつける
        int minNo = (int)((_InputRangeMAX-HighBid)/_TrapInfoData.SellTrapWidth); // HighBidの価格はminNoのトラップの下にいる
        int maxNo = (int)((_InputRangeMAX-LowBid)/_TrapInfoData.SellTrapWidth);  // LowBidの価格はmaxNoのトラップの下にいる
        if(minNo <-1 || maxNo >=_InputNumSellTraps){
            return;
        }
        for(int icount=minNo+1; icount <= maxNo; icount++){ // minNo = maxNo(トラップを跨がない)ならループは回らないはず
            if(_PositionSellBuffer[icount].ticket_no != 0){ // もしすでにポジションもってたら次へ
                continue;
            }
            if(fabs(Bid-_PositionSellBuffer[icount].target_price)>_InputDifferenceRimit ){ // 目標価格との乖離が大きすぎるので次へ
                continue;
            }
            bool long_bool = false;
            string strComment = StringFormat("%.3f,%.3f,%d,%d,%.3f",_InputRangeMAX,_InputRangeMIN,_InputNumSellTraps,icount);
            EA_EntryOrder(long_bool, _TrapInfoData.SellLot, strComment);
        }
    }
}

基本的には買いと同じ処理を売りの条件で実施してます。
数字の大小の条件とかが変わってますが、ここまで理解できた人なら理解できると思います。
あと、ちょっと違うのは447行目でlong_boolをfalseにして買いフラグ切ってるところぐらい。

これで、売りのエントリー処理が終わったらすべてのエントリー処理は終了です。

最後に増資するかどうかの判断をして、Tick発生時の処理を終わりにします。

増資判定

Tick発生時の最後の処理は増資判定です。
この判定は一日ごとに行うことにしているので、日足が確定したタイミングを調べてそのタイミングで増資するかを判定して処理しています。

ということで、以下OnTick()関数から呼んでいるTaskSetPeriod()関数は日足確定のタイミングを調べるのがメインの関数です。
増資するかどうかの判定は133行目のJudgeCapitalAdd()関数で実行しています。

//+------------------------------------------------------------------+
//| 指定時間足確定時の処理
//+------------------------------------------------------------------+
void TaskSetPeriod()
{
    static datetime s_lastset_mintime; // 最後に記録した時間軸時間
    
    datetime temptime = iTime( Symbol(), PERIOD_D1, 0);
    if( temptime == s_lastset_mintime ){
        return;
    }
    s_lastset_mintime = temptime;
    
    // -------- 処理はこの下へ記載 -----------
    //printf("[%d]指定時間足確定%s",__LINE__, TimeToStr(Time[0]));

    JudgeCapitalAdd(_TrapInfoData); // 増資(ロット数UP)の判断
}

最初に122行目で時間軸時間を保存する変数 s_lastset_mintimeを定義してます。
頭にstaticとつけることで、処理が関数を抜けても変数の値が保持されます。
(単に関数内で変数を定義すると、関数を抜けた時点でリセットされてしまいます。グローバル変数を使うというのも手ですが、どこからでもアクセス出来てしまうので意図せず値を上書きしてしまうのを防ぐためこういう書き方をしてます。詳細を知りたくなったら「スコープ」という概念を調べてみてください)

で、124行目で今の時間軸時間を日足指定でtemptimeという変数に読み込んで、125行目でs_lastset_mintimeと比較してます。
もしイコールだったら日足が更新していないので、処理せずreturnで関数を抜けます。

イコールでなかった場合は、日足が更新したということなので、新しい時間軸時間でs_lastset_mintimeを更新して(128行目)、その後に133行目で増資判定の処理を実行しています。

以下が増資判定の内容。

//+------------------------------------------------------------------+
//| 投資額増額判定処理
//+------------------------------------------------------------------+
void JudgeCapitalAdd(struct_TrapInfo &in_st)
{
    double newBalance = AccountInfoDouble(ACCOUNT_BALANCE)*(double)_InputBarancePercentage/100.0; // 最新の使用可能残高を取得
    if(newBalance - in_st.Balance > in_st.minCapitalAdd){ // 使用可能残高の増分が最低増資金額を超えていたら増資する
        in_st.Balance = newBalance;

        // ポジションのロット数算出
        double AveragePrice = 0.0;      // 理論平均ポジション価格
        double MaxLots = 0.0;           // 保有可能最大ロット数
        double Lots = 0.0;              // トラップ当たり保有可能ロット数(計算用)
        double OpenLotsMax = 0.0;       // エントリ済みポジションの最大ロット数
        double minCapitalAdd = 0.0;     // 最低増資金額

        // 買いポジション
        double HighestBuyPrice = NormalizeDouble(_InputRangeMIN + in_st.BuyTrapWidth*(_InputNumBuyTraps-1), _Digits);
        AveragePrice = (_InputRangeMIN + HighestBuyPrice)/2; // 理論平均買い価格
        MaxLots = in_st.Balance /(AveragePrice - _InputBuyStop + AveragePrice/(double)_Leverage) / _LotSize;
        Lots = MaxLots/_InputNumBuyTraps;
        Lots = (double)((int)(Lots/_MinLot)) * _MinLot; // MinLotに合わせて小数点以下を切り捨て
        in_st.BuyLot = Lots;

        // 売りポジション
        AveragePrice = 0.0;
        MaxLots = 0.0;
        Lots = 0.0;
        double LowestSellPrice = NormalizeDouble(_InputRangeMAX - in_st.SellTrapWidth*(_InputNumSellTraps-1), _Digits);
        AveragePrice = (_InputRangeMAX + LowestSellPrice)/2; // 理論平均買い価格
        MaxLots = in_st.Balance /(_InputSellStop - AveragePrice + AveragePrice/(double)_Leverage) / _LotSize;
        Lots = MaxLots/_InputNumSellTraps;
        Lots = (double)((int)(Lots/_MinLot)) * _MinLot; // MinLotに合わせて小数点以下を切り捨て
        _TrapInfoData.SellLot = Lots;
    }
}

すごく簡単に書くと、最新の残高を取得して今の残高からの増分が最低増資金額を超えていたら増資して、残高などのいろいろな条件を更新してます。

説明しなかった処理の説明

流れが切れるので説明しなかった処理たちです。

決済処理

エントリー処理

タイトルとURLをコピーしました