リプレイシステムの開発 - 市場シミュレーション(第24回):FOREX (V)
はじめに
前回の「リプレイシステムの開発 - 市場シミュレーション(第23回):FOREX (IV)」稿では、シミュレーションシステムの部分的なブロッキングの実装に注目しました。このブロックが必要だったのは、システムが極端に少ない取引量に対処するのが困難だったためです。この制限は、最後のプロットタイプに基づいてシミュレーションを実行しようとしたときに明らかになり、シミュレーションを生成しようとするとシステムがクラッシュする危険性がありました。この問題は、1分足バーで表される取引量が不十分なときに特に顕著でした。この問題を解決するために、今日は、Bidプロットに基づくシミュレーションで以前使用されていた原則に従って、実装を適応させる方法を見ていきましょう。このアプローチはFOREX市場で広く使用されているので、このトピックに関する記事はすでに5回目になります。しかし今回は、株式市場シミュレーションシステムの改良が目的なので、特に通貨には注目しません。
変更を実行に移しましょう。
最初の手順は、クラスにprivate構造体を導入することです。これは、LastとBidの両シミュレーションモードに共通するデータがあるからです。本質的に価値観であるこれらの共通要素は、1つの構造体にまとめられます。そこで、次のような構造体を定義します。
struct st00 { bool bHigh, bLow; int iMax; }m_Marks;
これは単純な構造体のように見えますが、コードを改良するのに十分な堅牢性を備えています。共通の価値観を1か所に集めることで、仕事の効率を大幅に上げることができます。
この準備の後、コードの最初の本格的な修正を始めることができます。
inline int Simulation(const MqlRates &rate, MqlTick &tick[]) { m_Marks.iMax = (int) rate.tick_volume - 1; m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high); m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low); Simulation_Time(rate, tick); if (m_IsPriceBID) Simulation_BID(rate, tick); else Simulation_LAST(rate, tick); else return -1; CorretTime(tick); return m_Marks.iMax; }
本日は、Last価格に基づくシミュレーションを妨げていた制限を取り除き、このタイプのシミュレーションに特化した新しいエントリポイントをご紹介します。操作の仕組みはすべて、FOREX市場の原理に基づいています。この手順の主な違いは、BidシミュレーションとLastシミュレーションの分離です。ただし、時間をランダム化し、C_Replayクラスに適合するように調整するために使用された方法は、両方のシミュレーションで同じままであることに注意することが重要です。この一貫性は良いことです。なぜなら、特にティック間の時間を管理する場合に、もしどちらかのモードを変更すれば、もう一方のモードも恩恵を受けるからです。当然ながら、ここで述べた修正は、Bidプロットタイプに基づくシミュレーションにも影響します。これらの変更はかなり理解しやすいので、詳細は省きます。
ターゲットのコードに戻りましょう。Lastベースのシミュレーション関数に呼び出しを追加すると、この呼び出しの最初のポイントを見ることができます。以下は、この関数の内部構造です。
inline void Simulation_LAST(const MqlRates &rate, MqlTick &tick[]) { if (CheckViability_LAST(rate)) { }else { } DistributeVolumeReal(rate, tick); }
この文脈では、Lastプロットを扱う際に2つの重要な手順を実行します。最初の手順は、以前の記事で説明したように、シミュレーションにランダムウォークシステムを使用する可能性をテストすることです。このトピックを勉強したことがない方には、「リプレイシステムの開発 - 市場シミュレーション(第15回):シミュレーターの誕生(V) - ランダムウォーク」稿を読むことをお勧めします。第2の手順は、取引量を可能なティック間で配分することです。これらの手順は、Lastプロットスタイルに基づいたシミュレーションをおこなう際に重要です。
構造を詳しく説明する前に、Lastベースのシミュレーションに欠かせない2つの関数について見てみましょう。1つ目の関数を以下に示します。
inline bool CheckViability_LAST(const MqlRates &rate) { #define macro_AdjustSafetyFator(A) (A + (A * 1.4)); double v0, v1, v2; v0 = macro_AdjustSafetyFator(rate.high - rate.low); v1 = (rate.open - rate.low); v2 = (rate.high - rate.open); v0 += macro_AdjustSafetyFator(v1 > v2 ? v1 : v2); v1 = (rate.close - rate.low); v2 = (rate.high - rate.close); v0 += macro_AdjustSafetyFator(v1 > v2 ? v1 : v2); return ((int)(v0 / m_TickSize) < rate.tick_volume); #undef macro_AdjustSafetyFator }
この関数は、利用可能な範囲内でランダムウォークを生成する可能性を確認する役割を果たします。どのようにでしょうか。このメソッドは、現在使用可能なティックの数を決定します。この情報は、扱うバーから提供され、カバーする必要のあるティック数とともに、関数内で計算されます。
この値は、カバーする領域を決定するために直接使用するものではないことにご注意ください。というのも、このような直接的なアプローチを用いると、結果として得られるランダムウォークが人工的で、過度に予測可能な動きに見えてしまうからです。この問題を軽減するために、計算を調整します。この設定はマクロを使用して実装され、ランダムウォークを正しく生成するための安全係数として機能する30%大きい領域を定義します。もう1つの重要な点は、ランダム化によってそのような拡張が必要になる可能性があるため、計算において可能な限り最大の距離を常に考慮する必要があることです。したがって、この可能性は計算過程ですでに考慮されています。
最終的な結果は、ランダムウォーク法を使用するのが適切なのか、それとも別のもっと直接的な無作為化法を使用するべきなのかを示しています。しかし、この判断は呼び出しプロシージャが下すものであり、ここではありません。
以下、2つ目の関数について詳しく説明します。
inline void DistributeVolumeReal(const MqlRates &rate, MqlTick &tick[]) { for (int c0 = 0; c0 <= m_Marks.iMax; c0++) tick[c0].volume_real = 1.0; for (int c0 = (int)(rate.real_volume - rate.tick_volume); c0 > 0; c0--) tick[RandomLimit(0, m_Marks.iMax)].volume_real += 1.0; }
ここでは、1分足バーの総取引量を無作為に配分することを目標とします。最初のループは、各ティックが最小の初期体積を受け取れるように、主配分を実行します。2つ目のループは、残りの体積が1つのティックに集中しないように無作為に分配する役割を担っています。この可能性はまだ存在しますが、採用された分配方法によってその可能性は大幅に減少しています。
これらの関数はすでにオリジナルの実装の一部であり、ランダムウォークの実装に関する記事で説明されています。しかし今回は、開発したコードの再利用性を最大化するため、よりモジュール化されたアプローチをとっています。
さらに熟考を重ねた結果、BidとLastのシミュレーションに共通する要素を特定しました。これには、エントリポイントとエグジットポイントの定義や、エンドポイントの定義が含まれます。この点を踏まえ、私たちは過去に開発されたコードの再利用に努めています。そのためには、前回の記事で紹介したコードを変更する必要があります。変更点は以下の通りです。
inline void Mount_BID(const int iPos, const double price, const int spread, MqlTick &tick[]) inline void MountPrice(const int iPos, const double price, const int spread, MqlTick &tick[]) { if (m_IsPriceBID) { tick[iPos].bid = price; tick[iPos].ask = NormalizeDouble(price + (m_TickSize * spread), m_NDigits); }else tick[iPos].last = NormalizeDouble(price, m_NDigits); }
まず、古い関数名を新しい関数名に置き換え、シミュレーションがBidとLastのどちらのプロットタイプに基づくかを決定する内部テストを挿入します。したがって、同じ関数を、1分足バーファイルで観察されたデータに従って、BidとLastの値に基づいてティックを生成するために適応させることができます。
この変更には2つの調整も必要です。私たちの目標は、BidとLastシミュレーションを単純化した方法で統合し、それぞれの本当にユニークな側面だけを適切な方法で処理することです。その他の点については一般的な対応となります。以下は、シミュレーションクラスのコードの変更点です。
inline void Simulation_BID(const MqlRates &rate, MqlTick &tick[]) { Mount_BID(0, rate.open, rate.spread, tick); for (int c0 = 1; c0 < m_Marks.iMax; c0++) { Mount_BID(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((int)(rate.spread | (m_Marks.iMax & 0xF)), 0)), tick); MountPrice(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((int)(rate.spread | (m_Marks.iMax & 0xF)), 0)), tick); m_Marks.bHigh = (rate.high == tick[c0].bid) || m_Marks.bHigh; m_Marks.bLow = (rate.low == tick[c0].bid) || m_Marks.bLow; } if (!m_Marks.bLow) Mount_BID(Unique(rate.high, tick), rate.low, rate.spread, tick); if (!m_Marks.bHigh) Mount_BID(Unique(rate.low, tick), rate.high, rate.spread, tick); Mount_BID(m_Marks.iMax, rate.close, rate.spread, tick); }
消された行は、コードが正しく機能し続けることを保証するために取り除かれます。しかし、Lastを表示するためには、シミュレーションシステム全体に共通する形で、これらの消された部分が必要です。この共通コードを以下の関数に移しました。
inline int Simulation(const MqlRates &rate, MqlTick &tick[]) { m_Marks.iMax = (int) rate.tick_volume - 1; m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high); m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low); Simulation_Time(rate, tick); MountPrice(0, rate.open, rate.spread, tick); if (m_IsPriceBID) Simulation_BID(rate, tick); else Simulation_LAST(rate, tick); if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick); if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick); MountPrice(m_Marks.iMax, rate.close, rate.spread, tick); CorretTime(tick); return m_Marks.iMax; }
そして今、私たちは決定的な瞬間を迎えました。大きな変更を加えることなく、インテリジェントなコードの再利用だけで、両方のシミュレーションを統合することができました。こうして、BidとLastのプロットタイプに基づいたシミュレーションができました。入力値、出力値、制限値がランダム化シミュレーションで事前に指定されていない場合は、自動的に入力値、出力値、制限値を含めます。この戦略的な調整によって、シミュレーションシステムの範囲を大幅に拡大しました。加えて、コードの複雑さは増しておらず、コード自体もそれほど増加していません。これまで紹介したコードを使用すれば、Bidベースのシミュレーションで良いパフォーマンスが得られるでしょう。そして、Lastプロットタイプでチャート上に少なくとも1本のバーを表示することができます。正しい値で定義されているにもかかわらず、最小値はゼロになってしまうからです。これは、時間だけが定義された未初期化のティックでは、Lastの値はすべてゼロに等しいからです。取引量をすでに分配しているという事実がなければ、これは問題にはならないでしょう。それでは、各ティックにLast価格の値を入れる関数に戻りましょう。シミュレーションに正しいデータを提供する必要があります。
Last価格シミュレーション関数に変更はありません。しかし、Bidのシミュレーションコードを見ていると、ある考えが浮かんできます。特に、利用可能なティック数がランダムウォークを完全に実行するのに十分でない場合、Last価格をシミュレーションするために同じコードを使用することはできないでしょうか。また、こんな疑問もありました。入念な分析の結果、必要なのはBidモデリング機能のわずかな修正だけだという結論に達しました。しかし、将来コードを変更しなければならなくなったときに混乱を避けるためには、今のうちに慎重に変更を計画しておく必要があります。Bidシミュレーションの手順は、先ほどの関数によって開始されます。Lastシミュレーションの概念が類似していることを考えれば、前回の呼び出しをそのまま維持する方法を探すことができます。そこで、ランダムウォークが適用されない状況でのLastシミュレーションもカバーできるように、Bidシミュレーション関数を適応させます。
このアプローチに疑問を持つ方もいらっしゃるかもしれませんが、Bidプロット用のシミュレーションコードを、Lastベースのプロット用のシミュレーションに適応させた方法を紹介します。
inline void Simulation_BID(const MqlRates &rate, MqlTick &tick[]) inline void Random_Price(const MqlRates &rate, MqlTick &tick[]) { for (int c0 = 1; c0 < m_Marks.iMax; c0++) { MountPrice(c0, NormalizeDouble(RandomLimit(rate.high, rate.low), m_NDigits), (rate.spread + RandomLimit((int)(rate.spread | (m_Marks.iMax & 0xF)), 0)), tick); m_Marks.bHigh = (rate.high == (m_IsPriceBID ? tick[c0].bid : tick[c0].last)) || m_Marks.bHigh; m_Marks.bLow = (rate.low == (m_IsPriceBID ? tick[c0].bid : tick[c0].last)) || m_Marks.bLow; m_Marks.bHigh = (rate.high == tick[c0].bid) || m_Marks.bHigh; m_Marks.bLow = (rate.low == tick[c0].bid) || m_Marks.bLow; } }
実行時の混乱を避けるため、関数名を変更することにしました。より汎用なコードを手に入れることができるのだから、安いものです。この適応のアイデアは次のようなものです。この2つのコード要素は特に注目に値します。三項演算子は不明瞭だと考える方もいらっしゃりますが、これはC言語の貴重な遺産であり、多くの便利さを提供しています。これらのセグメントは、プロットの種類を確認し、それに応じて価格を調整します。プロットの種類にかかわらず、無作為化は同じ方法でおこなわれることにご注意ください。こうして、この2つの方法を組み合わせて、BidとLastのための効果的なシミュレーションシステムを作り上げることができました。
変更後、シミュレーションは「リプレイシステムの開発 - 市場シミュレーション(第13回):シミュレーターの誕生(III)」」で説明したものと非常によく似たものになりました。しかし、まだランダムウォークシミュレーションをシステムに実装していません。現時点では、提示されたオプションに従ってコードが調整されているからです。
inline void Simulation_LAST(const MqlRates &rate, MqlTick &tick[]) { if (CheckViability_LAST(rate)) { }else Random_Price(rate, tick); DistributeVolumeReal(rate, tick); }
したがって、ランダムウォークの使用が適切と思われる典型的なシナリオはまだモデル化していません。しかし、私たちの目標は、Last価格シミュレーションと同じように、ある条件下でBid価格シミュレーションにランダムウォークを使わせることです。そして疑問はこれです。そんなことが可能なのでしょうか。さらに、FOREXのような市場でも値動きをシミュレーションするランダムウォーク法の恩恵を受けられるように、このアプローチをさらに面白く、ロバストにすることはできるでしょうか。答えは「はい」です。可能です。Last価格を構築するために特別にランダムウォークを実装する前に、いくつかの変更を加える必要があります。
inline bool CheckViability(const MqlRates &rate) { #define macro_AdjustSafetyFator(A) (int)(A + ceil(A * 1.7)) int i0, i1, i2; i0 = macro_AdjustSafetyFator((rate.high - rate.low) / m_TickSize); i1 = (int)((rate.open - rate.low) / m_TickSize); i2 = (int)((rate.high - rate.open) / m_TickSize); i0 += macro_AdjustSafetyFator(i1 > i2 ? i1 : i2); i0 += macro_AdjustSafetyFator((i1 > i2 ? (rate.high - rate.close) : (rate.close - rate.low) / m_TickSize)); return (i0 < rate.tick_volume); #undef macro_AdjustSafetyFator }
上記の関数は、以前ランダムウォーク運動の生成の実現可能性を評価するために使用した関数を拡張したものです。技術的な詳細や高度な安全係数の導入のため、私たちはこのアプローチを洗練させ、動作能力を誤解させないようにしました。この変更は、確認手順がもはやLastベースタイプのモデリングだけに限定されないため、正当化されます。また、Bidのシミュレーションにおけるランダムウォークの適用性を評価するためにも使用されます。一見、簡単そうに見えますが、特別な注意が必要です。この点をよりよく説明するために、図01を見てみましょう。
図01:最長経路の計算
この関数は、まさにこれをおこないます。つまり、1分足バーを作成するための可能な限り長い方法を計算するのです。この方法論は、Bidタイプも恩恵を受けられるよう、プロセスを合理化するよう調整されています。安全係数が1.4から1.7に増加し、ランダムウォークを使用することが非常に難しくなっている資産もあります。計算は、バーの始値とその両極端の間の距離を決定することから始まります。この情報を使用して、計算の最初の手順では大きい方の値を使用します。図01に示すように、バーを動かすには別の値が使用されます。最後に、ランダムウォークを使用することが可能かどうかを簡単な計算で確認します。
クラスコードにさらに変更を加えなければならないとお考えかもしれません。その通りですが、この変更は、より調和のとれた統合を確実にするような形で実施されます。
inline int Simulation(const MqlRates &rate, MqlTick &tick[]) { m_Marks.iMax = (int) rate.tick_volume - 1; m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high); m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low); Simulation_Time(rate, tick); MountPrice(0, rate.open, rate.spread, tick); if (CheckViability(rate)) { }else Random_Price(rate, tick); if (!m_IsPriceBID) DistributeVolumeReal(rate, tick); if (m_IsPriceBID) Random_Price(rate, tick); else Simulation_LAST(rate, tick); if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick); if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick); MountPrice(m_Marks.iMax, rate.close, rate.spread, tick); CorretTime(tick); return m_Marks.iMax; }
消された部分を削除し、緑で示した部分を追加します。FOREXのような市場で1分足バーが株式市場と同じようなティックを発生させる場合もあれば、その逆もあります。これにより、シミュレーターはティック出来高に関係なく、幅広い市場の動きをカバーすることができます。しかし、ランダムウォークを生成するコードはまだ上記の関数に含まれていないことに注意する必要があります。そこで、この機能を中心に、このコードが両方のタイプのプロットに対してどのように実装されるかを見てみましょう。
Bid価格とLast価格に対するランダムウォークの実装
上述したように、C_Simulationクラスは、BidとLastのプロットシミュレーションの間で一貫した処理を提供するように設計されています。目標は、可能な限り正確なシミュレーションをおこなうことでした。私たちは、次の手順が、複雑さを増すことなく、必要最小限のコードでランダムウォークを処理できる手順を実装することだという、重要な点に到達しました。この適応は、「リプレイシステムの開発 - 市場シミュレーション(第15回):シミュレーターの誕生(V) - ランダムウォーク」稿を読むことをお勧めします。よって、ランダムウォークのオリジナルの実装や、そのアイデアがどのように生まれたかについては詳しく触れません。さらに詳しく読みたい方は、紹介した記事を確認することをお勧めします。ここでは、このコードを新しい文脈に適応させることに焦点を当てます。
inline int RandomWalk(int In, int Out, const double Open, const double Close, double High, double Low, const int Spread, MqlTick &tick[], int iMode) { double vStep, vNext, price, vH = High, vL = Low; char i0 = 0; vNext = vStep = (Out - In) / ((High - Low) / m_TickSize); for (int c0 = In, c1 = 0, c2 = 0; c0 <= Out; c0++, c1++) { price = (m_IsPriceBID ? tick[c0 - 1].bid : tick[c0 - 1].last) + (m_TickSize * ((rand() & 1) == 1 ? -1 : 1)); price = (price > High ? price - m_TickSize : (price < Low ? price + m_TickSize : price)); MountPrice(c0, price, (Spread + RandomLimit((int)(Spread | (m_Marks.iMax & 0xF)), 0)), tick); switch (iMode) { case 0: if (price == Close) return c0; else break; case 1: i0 |= (price == High ? 0x01 : 0); i0 |= (price == Low ? 0x02 : 0); vH = (i0 == 3 ? High : vH); vL = (i0 ==3 ? Low : vL); break; default: break; } if (((int)floor(vNext)) >= c1) continue; if ((++c2) <= 3) continue; vNext += vStep; if (iMode != 2) { if (Close > vL) vL = (i0 == 3 ? vL : vL + m_TickSize); else vH = (i0 == 3 ? vH : vH - m_TickSize); }else { vL = (((c2 & 1) == 1) ? (Close > vL ? vL + m_TickSize : vL) : (Close < vH ? vL : vL + m_TickSize)); vH = (((c2 & 1) == 1) ? (Close > vL ? vH : vH - m_TickSize) : (Close < vH ? vH - m_TickSize : vH)); } } return Out; }
今回の変更は、機能をそのままに、コードの構造をシンプルにするためのものです。ここで特に興味深いのは、使用されるプロットのタイプに合わせて、新しい値を作成するために前の値がどのように読み込まれるかということです。この柔軟性は、シミュレーターの機能にとって非常に重要です。値を決定するために、すでに知られており、本稿で紹介する関数を使用します。これによりプロセスの開発が促進されます。すでに述べたように、この関数の機能については別の記事で取り上げたので、ここでは詳しく説明しません。
では、最終的な関数の構成を見てみましょう。これは、バーデータに基づいてシミュレーションの呼び出しを生成するように設計された関数が、期待された目標を達成できるかどうかをテストすると同時に、実装のこの段階を完了するための最初の試みです。目標は、BidベースとLastベースのシミュレーションを効果的にカバーすることです。以下は、関数コードの詳細です。
inline int Simulation(const MqlRates &rate, MqlTick &tick[]) { int i0, i1; bool b0 = ((rand() & 1) == 1); m_Marks.iMax = (int) rate.tick_volume - 1; m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high); m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low); Simulation_Time(rate, tick); MountPrice(0, rate.open, rate.spread, tick); if (CheckViability(rate)) { i0 = (int)(MathMin(m_Marks.iMax / 3.0, m_Marks.iMax * 0.2)); i1 = m_Marks.iMax - i0; i0 = RandomWalk(1, i0, rate.open, (b0 ? rate.high : rate.low), rate.high, rate.low, rate.spread, tick, 0); RandomWalk(i0, i1, (m_IsPriceBID ? tick[i0].bid : tick[i0].last), (b0 ? rate.low : rate.high), rate.high, rate.low, rate.spread, tick, 1); RandomWalk(i1, m_Marks.iMax, (m_IsPriceBID ? tick[i1].bid : tick[i1].last), rate.close, rate.high, rate.low, rate.spread, tick, 2); m_Marks.bLow = m_Marks.bHigh = true; }else Random_Price(rate, tick); if (!m_IsPriceBID) DistributeVolumeReal(rate, tick); if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick); if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick); MountPrice(m_Marks.iMax, rate.close, rate.spread, tick); CorretTime(tick); return m_Marks.iMax; }
この部分は、バーを横切るランダムウォークをシミュレーションする役割を担っています。このプロセスは過去にも使用されていましたが、生成コードに直接統合されていました。現在は、より理解しやすく、分析しやすい場所に移され、初心者プログラマーでもアクセスしやすくなっています。よく見ると、シミュレーションシステムはランダムウォークが可能かどうかを評価していることがわかります。それが可能であれば、システムはそれを使用して、不可能であれば、別の方法に頼ることになります。従って、いかなる状況においても、価格変動や価格シフトの発生を保証します。これはFOREX市場にも株式市場にも当てはまります。私たちの目標は、可能な限り最高のシミュレーションを提供するために常に適応することであり、バーが示すものから逸脱することなく、達成可能なすべての価格帯をカバーすることです。
状況によっては、あるバーがランダムウォークシミュレーションに適さないことがある一方で、後続のバーがすぐにそのプロセスを使用することがあることを理解することが重要です。その結果、価格は調和の取れた滑らかなものから、より唐突なものまで様々です。この食い違いは、必ずしもシミュレーションやリプレイシステムの失敗を示すものではなく、むしろ、あるバーで急激な値動きが必要であったことの結果であり、より滑らかなランダムウォークシミュレーションのためには、大きな取引量が伴わなかった可能性があります。取引量が多ければ、ランダムウォーク法を使用することができるかもしれませんが、実際の価格が滑らかに動いたことを意味しません。場合によっては、動きが急であることもありますが、取引されたティックの密度が高いため、シミュレーションでランダムウォークを適用することができたのであり、必ずしもその特定のバーの実際の市場状況を反映しているわけではありません。
私たちはすでに理想的な解決策、つまり目標を達成しているように見えるかもしれませんが、まだそこに到達していません。ランダムウォーク法は、1分足の取引回数が多い場合に広く使用されていますが、1分足の取引回数が必要以上に少ない場合には適用できません。さらに、高値と安値の距離がティック数に近い場合、完全に無作為なウォークを使用してバーの動きをシミュレーションすると、奇妙に見えるシミュレーションになります。そのような場合は、本連載の別の記事「リプレイシステムの開発 - 市場シミュレーション(第11回):シミュレーターの誕生(I)」で取り上げたモデルを再考する必要があります。ここではバーの中で反転を起こすシステムが提案されています。
このようなシステムを導入するというアイデアは、適切であるだけでなく、可能であるように思えます。目標は、現実的で有効な動きを生成することであり、価格値を完全に無作為にトリガーすることではありません。したがって、中心的な問題はもはや時間ではなく、価格に示された価値です。特に、経験豊富なトレーダーなら値動きのロジックを見分けるような状況で、ロジックなしに値を生成する関数を使用するのは意欲喪失につながります。ただし、この問題にも解決策があります。連載第11回から現在に至るまでのアプローチを統合することを目指すことです。この解決策は初心者にはすぐにはわからないかもしれませんが、プログラミングの経験が豊富な人には非常にわかりやすいものです。ゼロから新しいシミュレーション関数を作ることはありません。滑らかでない動きと滑らかな動きを交互におこないますが、これはシミュレーター自体によって決まります。動きの滑らかさに関する結論は、始値、終値、高値、安値、ティック量の5つの情報のみに基づいて決まります。この選択に必要なデータはこれだけです。ここで最終的な解答を述べるつもりはありません。私の目標は、1分間のバーの中で動きを作り出し、シミュレーションするために可能な多くの方法の1つを示すことです。
様々なシナリオでランダムウォークを使用する - 最小努力経路をたどる
前述したように、何らかのロジックを含むメソッドを探す必要があります。1分足バーを使用し、10分足や15分足などの高周期チャートを使用する場合でも、無作為化だけに依存すると満足のいく結果は得られません。理想的なのは、一方の端からもう一方の端への急激な移行を避けるために、動きが緩やかであることです。そのため、動きは徐々に描かれ、無作為な印象を与えますが、実際は単純な数学的計算の結果であり、見かけ上の複雑さを生み出しています。これがストキャスティクスな動きの基礎の1つです。
よりスマートで滑らかな流れを作るためには、既存の機能をある程度排除し、動きをコントロールするルールを確立する必要があります。ある方向に強制的に動かそうとしてはいけないことにご注意ください。しかし、MetaTrader 5が適切と考えるように処理をおこなうように、MetaTrader 5のルールを定義する必要があります。そのためには、まずランダムウォークのコードを修正する必要があります。改定後の規約は以下の通りです。
inline int RandomWalk(int In, int Out, const double Open, const double Close, double High, double Low, const int Spread, MqlTick &tick[], int iMode, int iDesloc) { double vStep, vNext, price, vH = High, vL = Low; char i0 = 0; vNext = vStep = (Out - In) / ((High - Low) / m_TickSize); for (int c0 = In, c1 = 0, c2 = 0; c0 <= Out; c0++, c1++) { price = (m_IsPriceBID ? tick[c0 - 1].bid : tick[c0 - 1].last) + (m_TickSize * ((rand() & 1) == 1 ? -1 : 1)); price = (price > vH ? price - m_TickSize : (price < vL ? price + m_TickSize : price)); price = (m_IsPriceBID ? tick[c0 - 1].bid : tick[c0 - 1].last) + (m_TickSize * ((rand() & 1) == 1 ? -iDesloc : iDesloc)); price = (price > vH ? vH : (price < vL ? vL : price)); MountPrice(c0, price, (Spread + RandomLimit((int)(Spread | (m_Marks.iMax & 0xF)), 0)), tick); switch (iMode) { case 1: i0 |= (price == High ? 0x01 : 0); i0 |= (price == Low ? 0x02 : 0); vH = (i0 == 3 ? High : vH); vL = (i0 ==3 ? Low : vL); break; case 0: if (price == Close) return c0; default: break; } if (((int)floor(vNext)) >= c1) continue; else if ((++c2) <= 3) continue; vNext += vStep; vL = (iMode != 2 ? (Close > vL ? (i0 == 3 ? vL : vL + m_TickSize) : vL) : (((c2 & 1) == 1) ? (Close > vL ? vL + m_TickSize : vL) : (Close < vH ? vL : vL + m_TickSize))); vH = (iMode != 2 ? (Close > vL ? vH : (i0 == 3 ? vH : vH - m_TickSize)) : (((c2 & 1) == 1) ? (Close > vL ? vH : vH - m_TickSize) : (Close < vH ? vH - m_TickSize : vH))); if (iMode == 2) { vL = (((c2 & 1) == 1) ? (Close > vL ? vL + m_TickSize : vL) : (Close < vH ? vL : vL + m_TickSize)); vH = (((c2 & 1) == 1) ? (Close > vL ? vH : vH - m_TickSize) : (Close < vH ? vH - m_TickSize : vH)); }else { if (Close > vL) vL = (i0 == 3 ? vL : vL + m_TickSize); else vH = (i0 == 3 ? vH : vH - m_TickSize); } } return Out; }
変更点には、いくつかのコードセグメントを新しい緑色の追加コードに置き換えることが含まれます。微妙な変更に見えるかもしれませんが、以前のバージョンよりもはるかに柔軟性が増しています。以前は、ティックごとに隙間なく連続的に動いていたため、ランダムウォークを滑らかにシミュレーションするには大量の取引が必要でした。1分足にギャップを導入することで、必要な取引回数を大幅に減らすことができるため、1分足の数量やパラメータを変えてシステムをシミュレーションすることができます。この結果、始値、終値、高値、低値の4つの基本値に達したときにランダムウォークによって生成される動きがグラフィカルに適応されます。中間的な振る舞いはランダムウォークによって決定されます。しかし、重要なのはランダムウォークを呼び出す関数です。この関数の詳細は後述します。
inline int Simulation(const MqlRates &rate, MqlTick &tick[]) { int i0, i1, i2; bool b0; m_Marks.iMax = (int) rate.tick_volume - 1; m_Marks.bHigh = (rate.open == rate.high) || (rate.close == rate.high); m_Marks.bLow = (rate.open == rate.low) || (rate.close == rate.low); Simulation_Time(rate, tick); MountPrice(0, rate.open, rate.spread, tick); if (CheckViability(rate)) if (m_Marks.iMax > 10) { i0 = (int)(MathMin(m_Marks.iMax / 3.0, m_Marks.iMax * 0.2)); i1 = m_Marks.iMax - i0; i2 = (int)(((rate.high - rate.low) / m_TickSize) / i0); i2 = (i2 == 0 ? 1 : i2); b0 = (m_Marks.iMax >= 1000 ? ((rand() & 1) == 1) : (rate.high - rate.open) < (rate.open - rate.low)); i0 = RandomWalk(1, i0, rate.open, (b0 ? rate.high : rate.low), rate.high, rate.low, rate.spread, tick, 0, i2); RandomWalk(i0, i1, (m_IsPriceBID ? tick[i0].bid : tick[i0].last), (b0 ? rate.low : rate.high), rate.high, rate.low, rate.spread, tick, 1, i2); RandomWalk(i1, m_Marks.iMax, (m_IsPriceBID ? tick[i1].bid : tick[i1].last), rate.close, rate.high, rate.low, rate.spread, tick, 2, i2); m_Marks.bHigh = m_Marks.bLow = true; }else Random_Price(rate, tick); if (!m_IsPriceBID) DistributeVolumeReal(rate, tick); if (!m_Marks.bLow) MountPrice(Unique(rate.high, tick), rate.low, rate.spread, tick); if (!m_Marks.bHigh) MountPrice(Unique(rate.low, tick), rate.high, rate.spread, tick); MountPrice(m_Marks.iMax, rate.close, rate.spread, tick); CorretTime(tick); return m_Marks.iMax; }
この関数は、この記事を通して使用してきたのと同じ方法で、ランダムウォークを作る可能性をもう確認しないことを示しています。これで、評価はすべてこの関数の中でおこなわれるようになりました。奇妙に思われるかもしれませんが、システムは最小取引量わずか10回でランダムウォークを実行しようとします。この閾値以下の出来高については、純粋なランダム化が使用され、この特定の文脈ではランダムウォークよりも効率的であると考えられています。革新的な点は、1分バーの中にギャップを作ることで、これは前述の特別な計算によって保証されています。ランダムウォークが正しく機能するためには、少なくとも1ティックが生成されるようにすることが重要です。
しかし、このプロセスに課題がないわけではありません。ランダムウォークが効果的であるためには、さらなる制御が必要です。この追加制御は特別な確認によっておこなわれ、その値は必要に応じて調整できます。取引量が1分間に1000を超えると、シミュレーションシステムは、最初に行くべき高値と安値を無作為に決定して経路を選択することができます。一方、出来高が設定した出来高より少ない場合、ランダムウォークの最初の方向は、始値とバーの高値または安値の近さに基づいて決定されます。
この方法は「最小努力経路」として知られ、必要な動作の数が歩行距離の合計よりも少ない場合に有効です。これにより、不必要に長く複雑なルートを選択することを避けることができます。この計算アプローチにより、この記事で提案されたいくつかの議論や方法は、最終的なアプリケーションには現れないかもしれません。次の2つの図は、システムの有効性を示すものです。実際のティックデータに基づくグラフと、最小努力戦略を用いたシミュレーション結果です。
図02:実データに基づくチャート
図03:システムでシミュレーションされたデータを使用して作成されたチャート
一見すると同じチャートに見えますが、そうではありません。よく見ると、各図に記載されているデータソースがそれぞれの違いを際立たせているなど、違いが見つかるかもしれません。この比較では、EURUSD資産、すなわちFOREX通貨ペアに特化したアプリケーションで提示されたデータを使用して実験をおこなうようにユーザーを招待します。このデモンストレーションは、シミュレーション手法がLastとBidの両方のプロットタイプに適応できることを示しており、既存のデータに対してシステムの性能をテストすることができます。
結論
この記事は、リプレイ/シミュレーションシステムを完全に機能させるための重要な準備段階です。次回は、リプレイ/シミュレーションサービスについて説明する前に、最終的に必要な設定について見ていきましょう。この段階は、テスト条件下でのシステムの性能と有効性を理解しようとする者にとって重要です。
添付ファイルについての重要な注意:データサイズが大きいため、特に将来の資産に関するリアルティックの場合は、特定の資産または市場に関連する4つのファイルを提供します。メインファイルには、現在の開発段階までのシステムのソースコードが含まれています。システム構造と機能の完全性を確保するため、すべてのファイルはダウンロードし、MQL5エディターで指定されたディレクトリに保存する必要があります。
MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/11189
- 無料取引アプリ
- 8千を超えるシグナルをコピー
- 金融ニュースで金融マーケットを探索