一からの取引エキスパートアドバイザーの開発
はじめに
金融市場でのトレーダー数は増加しています。その多くはおそらく注文システムがどのように機能するかさえ知らないでしょう。しかし、何が起こっているのかを本当に知りたがっているトレーダーもおり、彼らは状況を制御するためにそれがすべてどのように機能するかを理解しようとします。
もちろん、MetaTrader5で取引ポジションを詳細に制御することもできますが、手動注文機能だけを使用することは、経験の浅いトレーダーにとっては非常に困難でリスクが高い場合があります。さらに、先物取引をしたい場合はすべてのフィールドをタイムリーに正しく入力する必要があります。そのような取引は悪夢に変わる可能性があります。注文にかけられる時間がほとんどなくても入力に時間がかかり、入力をどこか間違えれば、良い機会を逃したり、資金を失うことさえあります。
では、エキスパートアドバイザー(EA)を使用して作業を簡単にした場合はどうなるでしょうか。この場合、レバレッジや失う余裕のある金額、稼ぎたい金額など、いくつかの詳細を指定できます(「ポイント」や「ピップ」のように不明確なものではなく貨幣単位で)。次に、チャート上のマウスポインタを使用して注文を出す場所を示し、売買のどちらかを指定します...
計画
何かを作成する上で最も難しい部分は、物事がどのように機能するかを理解することです。作成するコードが複雑になるほど実行時のエラーが発生する可能性が高くなるため、必要最小限のコードを作成できるように、アイデアを非常に明確に定式化する必要があります。これを念頭に置いて、私は、MetaTrader 5によって提供される可能性を最大限に活用しながら、コードを非常に簡潔にしようとしました。このプラットフォームは非常に信頼性が高く、常にテストされているため、プラットフォーム側でエラーが発生することはありません。
コードはOOP(オブジェクト指向プログラミング)を使用します。このアプローチにより、コードを分離し、新しい機能を追加して改善したい場合に備えて、コードの保守と将来の開発が容易になります。
この記事で説明するEAは、B3(ブラジル取引所)での取引、特に先物(ミニインデックスとミニドル)の取引用に設計されていますが、最小限の変更ですべての市場に拡張できます。取引資産をリストまたはチェックしないようにして簡潔化するために、次の列挙を使用します。
enum eTypeSymbolFast {WIN, WDO, OTHER};
特別な機能を使用する別のアセットを取引する場合は、それを列挙に追加します。これにもコードでの小さな変更が必要ですが、列挙によってエラーの可能性も減るので、はるかに簡単になります。コードの興味深い部分は、AdjustPrice関数です。
double AdjustPrice(const double arg) { double v0, v1; if(m_Infos.TypeSymbol == OTHER) return arg; v0 = (m_Infos.TypeSymbol == WDO ? round(arg * 10.0) : round(arg)); v1 = fmod(round(v0), 5.0); v0 -= ((v1 != 0) || (v1 != 5) ? v1 : 0); return (m_Infos.TypeSymbol == WDO ? v0 / 10.0 : v0); };
この関数は、チャートの正確なポイントに線を配置するために、価格で使用される値を調整します。チャートに単に線を引くことができないのはなぜでしょうか。これは、一部の資産では価格間に特定のステップがあるためです。WDO(ミニドル)の場合、このステップはわずか0.5ポイントです。WIN(ミニインデックス)の場合は5ポイントで、株式の場合は0.01ポイントです。 つまり、ポイント値は資産ごとに異なります。これにより、価格が正しいティック値に調整され、注文で適切な値が使用されます。そうしないと、誤って入力された注文がサーバーによって拒否される可能性があります。
この機能がないと、注文に使用する正しい値を知ることが難しい場合があります。 したがって、サーバーが注文が正しく処理されていないことを通知し、注文が実行されないようにする可能性があります。次に、エキスパートアドバイザーの心臓部であるCreateOrderPendent関数に進みましょう。 この関数は次のとおりです。
ulong CreateOrderPendent(const bool IsBuy, const double Volume, const double Price, const double Take, const double Stop, const bool DayTrade = true) { double last = SymbolInfoDouble(m_szSymbol, SYMBOL_LAST); ZeroMemory(TradeRequest); ZeroMemory(TradeResult); TradeRequest.action = TRADE_ACTION_PENDING; TradeRequest.symbol = m_szSymbol; TradeRequest.volume = Volume; TradeRequest.type = (IsBuy ? (last >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : (last < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP)); TradeRequest.price = NormalizeDouble(Price, m_Infos.nDigits); TradeRequest.sl = NormalizeDouble(Stop, m_Infos.nDigits); TradeRequest.tp = NormalizeDouble(Take, m_Infos.nDigits); TradeRequest.type_time = (DayTrade ? ORDER_TIME_DAY : ORDER_TIME_GTC); TradeRequest.stoplimit = 0; TradeRequest.expiration = 0; TradeRequest.type_filling = ORDER_FILLING_RETURN; TradeRequest.deviation = 1000; TradeRequest.comment = "Order Generated by Experts Advisor."; if(!OrderSend(TradeRequest, TradeResult)) { MessageBox(StringFormat("Error Number: %d", TradeResult.retcode), "Nano EA"); return 0; }; return TradeResult.order; };
この関数は非常に簡潔で、安全性のために設計されています。ここでOCO注文(一方が他方をキャンセル)を作成します。注文は取引サーバーに送信されます。LIMIT またはSTOP 注文を使用していることに注意してください。 これは、このタイプの注文がより単純であり、突然の価格変動があってもその実行が保証されるためです。
使用される注文タイプは、執行価格と取引商品の現在の価格、および買いポジションと売りポジションのどちらを入力するかによって異なります。 これは次の行に実装されています。
TradeRequest.type = (IsBuy ? (last >= Price ? ORDER_TYPE_BUY_LIMIT : ORDER_TYPE_BUY_STOP) : (last < Price ? ORDER_TYPE_SELL_LIMIT : ORDER_TYPE_SELL_STOP));
次の行で取引商品を指定することにより、両建て注文を作成することもできます。
TradeRequest.symbol = m_szSymbol;
ただし、そうする場合は、チャートが正しくなくなるため、両建て注文システムを介して未決注文を処理するためにいくつかのコードを追加する必要もあります。 例を見てみましょう。 フルインデックスチャート(IND)でミニインデックス(WIN)を取引することはできますが、MetaTrader 5をINDチャートで使用している場合、WIN未決注文は表示されません。したがって、注文を表示するにはコードを追加する必要があります。 これは、ポジションの値を読み取り、チャート上に線として表示することで実行することができ、銘柄の取引履歴を取引および追跡するときに非常に役立ちます。 たとえば、両建て注文を使用すると、WIN$チャート(ミニインデックス履歴チャート)を使用してWIN(ミニインデックス)を取引できます。
次に、次のコード行に注意してください。
TradeRequest.price = NormalizeDouble(Price, m_Infos.nDigits); TradeRequest.sl = NormalizeDouble(Stop, m_Infos.nDigits); TradeRequest.tp = NormalizeDouble(Take, m_Infos.nDigits);
これらの3行は、OCO注文ストップレベルとポジションを開く価格を作成します。数秒しか続かない短期注文を取引する場合、ボラティリティによって価格が明確な方向性なしにポイント間を移動する可能性があるため、OCO注文を使用せずに取引を開始することはお勧めできません。OCOを使用する場合、取引サーバー自体にポジションを任せます。OCO注文は次のように表示されます。
編集ウィンドウでは、同じ注文が次のようになります。
すべての必須フィールドに入力すると、サーバーが注文を管理します。最大利益または最大損失に達するとすぐに、システムは注文を決済します。ただし、最大利益または最大損失を指定しない場合、別のイベントが発生するまで注文は開いたままになる可能性があります。注文タイプがデイトレードに設定されている場合、注文は取引日の終わりに決済されます。それ以外の場合は、手動で決済するか、ポジションを開いたままにする資金がなくなるまで、ポジションは開いたままになります。
一部のエキスパートアドバイザーシステムは、注文を使用してポジションを決済します。ポジションが開くと、同じボリュームで指定されたポイントでポジションを決済する反対の注文が送信されます。ただし、セッション中に何らかの理由で資産が競売にかけられた場合は未決注文がキャンセルされる必要があるので、これは一部のシナリオでは機能しない場合があり、置き換える必要があります。これはEAの操作を複雑にします。アクティブな注文とアクティブでない注文の確認を追加する必要があり、何か問題があった場合、EAは基準なしで次々に注文を送信することになります。
void Initilize(int nContracts, int FinanceTake, int FinanceStop, color cp, color ct, color cs, bool b1) { string sz0 = StringSubstr(m_szSymbol = _Symbol, 0, 3); double v1 = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE) / SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE); m_Infos.Id = ChartID(); m_Infos.TypeSymbol = ((sz0 == "WDO") || (sz0 == "DOL") ? WDO : ((sz0 == "WIN") || (sz0 == "IND") ? WIN : OTHER)); m_Infos.nDigits = (int) SymbolInfoInteger(m_szSymbol, SYMBOL_DIGITS); m_Infos.Volume = nContracts * (m_VolMinimal = SymbolInfoDouble(m_szSymbol, SYMBOL_VOLUME_MIN)); m_Infos.TakeProfit = AdjustPrice(FinanceTake * v1 / m_Infos.Volume); m_Infos.StopLoss = AdjustPrice(FinanceStop * v1 / m_Infos.Volume); m_Infos.IsDayTrade = b1; CreateHLine(m_Infos.szHLinePrice, m_Infos.cPrice = cp); CreateHLine(m_Infos.szHLineTake, m_Infos.cTake = ct); CreateHLine(m_Infos.szHLineStop, m_Infos.cStop = cs); ChartSetInteger(m_Infos.Id, CHART_COLOR_VOLUME, m_Infos.cPrice); ChartSetInteger(m_Infos.Id, CHART_COLOR_STOP_LEVEL, m_Infos.cStop); };
上記のルーチンは、ユーザーが指定したEAデータの開始を担当します。OCO注文を作成します。このルーチンでは、次の変更を加えるだけで済みます。
m_Infos.TypeSymbol = ((sz0 == "WDO") || (sz0 == "DOL") ? WDO : ((sz0 == "WIN") || (sz0 == "IND") ? WIN : OTHER));
ここでは、特定のものが必要な場合に、現在の種類に加えて取引銘柄の種類を追加します。
m_Infos.Volume = nContracts * (m_VolMinimal = SymbolInfoDouble(m_szSymbol, SYMBOL_VOLUME_MIN)); m_Infos.TakeProfit = AdjustPrice(FinanceTake * v1 / m_Infos.Volume); m_Infos.StopLoss = AdjustPrice(FinanceStop * v1 / m_Infos.Volume);
上記の3行は、正しい注文を作成するために必要な調整を行います。nContractsはレバレッジ係数で、1、2、3などの値を使用します。言い換えれば、取引される銘柄の最小ボリュームを知る必要はありません。本当に必要なのは、この最小ボリュームのレバレッジ係数を示すことだけです。たとえば、必要な最小ボリュームが5契約で、レバレッジ係数3を指定した場合、15契約の注文が開始されます。他の2行は、ユーザーが指定したパラメーターに基づいて決済指値と決済逆指値を設定します。レベルは注文量に応じて調整されます。注文が増えるとレベルが下がり、その逆も同様です。このコードを使用すると、すべてがEAで計算されるため、ポジションを作成するために計算を行う必要はありません。ユーザーは、どの金融商品をどのレバレッジ係数で取引するか、どれだけ稼ぎたいか、そして、どれだけ失う準備ができているかをEAに指示し、EAが適切な注文を行います。
inline void MoveTo(int X, int Y, uint Key) { int w = 0; datetime dt; bool bEClick, bKeyBuy, bKeySell; double take = 0, stop = 0, price; bEClick = (Key & 0x01) == 0x01; //Left mouse button click bKeyBuy = (Key & 0x04) == 0x04; //Pressed SHIFT bKeySell = (Key & 0x08) == 0x08; //Pressed CTRL ChartXYToTimePrice(m_Infos.Id, X, Y, w, dt, price); ObjectMove(m_Infos.Id, m_Infos.szHLinePrice, 0, 0, price = (bKeyBuy != bKeySell ? AdjustPrice(price) : 0)); ObjectMove(m_Infos.Id, m_Infos.szHLineTake, 0, 0, take = price + (m_Infos.TakeProfit * (bKeyBuy ? 1 : -1))); ObjectMove(m_Infos.Id, m_Infos.szHLineStop, 0, 0, stop = price + (m_Infos.StopLoss * (bKeyBuy ? -1 : 1))); if((bEClick) && (bKeyBuy != bKeySell)) CreateOrderPendent(bKeyBuy, m_Infos.Volume, price, take, stop, m_Infos.IsDayTrade); ObjectSetInteger(m_Infos.Id, m_Infos.szHLinePrice, OBJPROP_COLOR, (bKeyBuy != bKeySell ? m_Infos.cPrice : clrNONE)); ObjectSetInteger(m_Infos.Id, m_Infos.szHLineTake, OBJPROP_COLOR, (take > 0 ? m_Infos.cTake : clrNONE)); ObjectSetInteger(m_Infos.Id, m_Infos.szHLineStop, OBJPROP_COLOR, (stop > 0 ? m_Infos.cStop : clrNONE)); };
上記のコードは、作成される注文を示しています。マウスの動きを使用して、注文が出される場所を示します。ただし、購入するか(Shiftキーを長押し)、売却するか(Ctrlキーを長押し)をEAに通知する必要があります。マウスの左ボタンをクリックすると、その時点で未決注文が作成されます。
損益分岐点など、より多くのデータを表示する必要がある場合は、関連するオブジェクトをコードに追加します。
これで、EA全体が機能し、OCO注文を作成できるようになりました。ただし、ここではすべてが完璧というわけではありません...
OCO注文の問題
OCO注文には1つの問題があります。これは、MetaTrader5システムや取引サーバーの障害ではなく、市場に常に存在するボラティリティ自体に関連しています。理論的には、価格はロールバックなしで直線的に変動するはずですが、ボラティリティが高く、ローソク足の内部にギャップが生じることがあります。(逆)指値注文の価格がであるポイントでこれらのギャップが発生した場合、これらのポイントはトリガーされないため、ポジションは決済されません。また、ユーザーがこれらのポイントを移動すると、決済逆指値と決済指値によって形成されるコリドーを超えた価格が発生する可能性があります。この場合、注文は決済されません。これは非常に危険な状況であり、予測することは不可能です。プログラマーは、起こりうる損害を最小限に抑えるために、関連するメカニズムを提供する必要があります。
価格を更新してコリドー内に維持するために、2つのサブルーチンを使用します。最初のものは次のとおりです。
void UpdatePosition(void) { for(int i0 = PositionsTotal() - 1; i0 >= 0; i0--) if(PositionGetSymbol(i0) == m_szSymbol) { m_Take = PositionGetDouble(POSITION_TP); m_Stop = PositionGetDouble(POSITION_SL); m_IsBuy = PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY; m_Volume = PositionGetDouble(POSITION_VOLUME); m_Ticket = PositionGetInteger(POSITION_TICKET); } };
これは、ポジションが変わるたびにMetaTrader5によって呼び出されるOnTradeで呼び出されます。次に使用されるサブルーチンは、OnTickによって呼び出されます。価格がコリドー内またはOCO注文の制限内にあることを確認します。以下のようになります。
inline bool CheckPosition(const double price = 0, const int factor = 0) { double last; if(m_Ticket == 0) return false; last = SymbolInfoDouble(m_szSymbol, SYMBOL_LAST); if(m_IsBuy) { if((last > m_Take) || (last < m_Stop)) return ClosePosition(); if((price > 0) && (price >= last)) return ClosePosition(factor); } else { if((last < m_Take) || (last > m_Stop)) return ClosePosition(); if((price > 0) && (price <= last)) return ClosePosition(factor); } return false; };
このコード部分は、ティックが変更されるたびに実行されるため重要で、計算とテストが可能な限り効率的に実行されるように可能な限り単純にする必要があります。コリドー内の価格を維持しながら、興味深いものをチェックして必要に応じて削除しています。この追加のテストについては、次のセクションで説明します。このサブルーチン内には、次の関数呼び出しがあります。
bool ClosePosition(const int arg = 0) { double v1 = arg * m_VolMinimal; if(!PositionSelectByTicket(m_Ticket)) return false; ZeroMemory(TradeRequest); ZeroMemory(TradeResult); TradeRequest.action = TRADE_ACTION_DEAL; TradeRequest.type = (m_IsBuy ? ORDER_TYPE_SELL : ORDER_TYPE_BUY); TradeRequest.price = SymbolInfoDouble(m_szSymbol, (m_IsBuy ? SYMBOL_BID : SYMBOL_ASK)); TradeRequest.position = m_Ticket; TradeRequest.symbol = m_szSymbol; TradeRequest.volume = ((v1 == 0) || (v1 > m_Volume) ? m_Volume : v1); TradeRequest.deviation = 1000; if(!OrderSend(TradeRequest, TradeResult)) { MessageBox(StringFormat("Error Number: %d", TradeResult.retcode), "Nano EA"); return false; } else m_Ticket = 0; return true; };
この関数は指定されたボリュームを決済して、保護として機能します。ただし、この関数はMetaTrader 5クライアントターミナルで実行されるため、接続する必要があることを忘れないでください。サーバーに接続できなかった場合、この関数はまったく役に立たなくなります。
これらの最後の2つのコードを見ると、特定の時点で特定のボリュームを決済できることがわかります。そうすることで、部分決済を行うか、露出を減らします。この関数の使い方を考えてみましょう。
部分注文での作業
部分注文は、多くのトレーダーが気に入って使用しているものです。エキスパートアドバイザーでは、部分注文での作業が許可されていますが、部分注文は別の問題の対象となるため、このようなコードの実装方法については説明しません。ただし、部分決済を使用して作業を実装する場合は、CheckPositionルーチンを呼び出して、注文が実行される価格とボリュームを指定するだけです。後はEAが行います。
部分注文は非常に個別であり、すべての人を満足させる一般化された解決策を作成するのは難しいため、特殊なケースであると思います。スイングする可能性があるため、動的配列の使用はここでは適切ではありません。EAを閉じない場合に、デイトレードのみで機能します。何らかの理由でEAを閉じる必要がある場合、配列による解決法は機能しません。データ形式は、このデータで何をするかによって異なります。
とにかく、問題が生じるリスクが大きいので、ポジションを開いた注文を使用した部分決済はできるだけ避けるべきです。説明させてください。あなたが3xレバレッジの買いポジションを持っていて、1xレバレッジポジションを持ちながら2xで利益を上げたいとしましょう。これは、2倍のレバレッジを売ることで実現できます。ただし、EAが成行売り注文を送信すると、ボラティリティによって価格が上昇し、売り注文が実際に実行される前に指値に達する可能性があります。この場合、EAは不利な方向に新しいショートポジションを開きます。オプションで、売り指値または売り逆指値を送信して、2xレバレッジでポジションを減らすことができます。これは適切な解決策のように思われるかもしれません。ただし、価格が部分ポイントに達する前に別の注文が送信された場合はどうなるでしょうかポジションが停止され、少し後に注文が再び開かれて、損失が増加するという、非常に不愉快な不意打ちにあう可能性があります。ボラティリティが強くなると、前述したのと同じ状況になります。
したがって、私の意見では、プログラマーとして、部分注文を作成するための最良のオプションは成行価格での注文の送信をエミュレートすることです。ただし、開いているボリュームを超えないように十分に注意する必要があります。このEAで実行したのはまさにこの方法です。必要に応じて、他の部分決済メソッドを実装してください。
終わりに
取引のためのエキスパートアドバイザーを作成することは、一部の人々が考えるほど簡単ではありません。プログラミング時によく直面する他の問題と比べると非常に簡単ですが、資金を危険にさらすほど安定した信頼性の高いものを構築することは、多くの場合困難な作業です。この記事では、EAをプログラムするために必要な知識を持っていないMetaTrader 5初心者に役立つ何かを提案しました。これは良いスタートです。このEAは、より信頼できる方法で注文を出すのに役立つだけで注文は出しません。注文が出されると、EAは他に何もすることがなく、上記のコードフラグメントを除いて、さらにMetaTrader5が機能し始めます。
この記事で紹介するエキスパートアドバイザーは、パラメーターのセットを操作するためにさまざまな方法で改善できますが、これには、MetaTrader5からより独立するためのより多くのコードが必要になります。
このEAの成功は、MetaTrader 5自体を使用してそのコードにないアクションを実行するために、非常に安定していて信頼性があることです。
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/10085
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索