English Русский 中文 Español Deutsch Português
preview
リプレイシステムの開発—市場シミュレーション(第3回):設定の調整(I)

リプレイシステムの開発—市場シミュレーション(第3回):設定の調整(I)

MetaTrader 5 | 31 7月 2023, 09:46
170 0
Daniel Jose
Daniel Jose

はじめに

前回の「リプレイシステムの開発—市場シミュレーション(第2回):最初の実験(II)」稿では、市場シミュレーションに使用するために、適切な処理時間内に1分足のバーを生成できるシステムを作成しました。しかし、何が起こるかを制御することはできないと理解していました。私たちの能力は、いくつかのポイントを選び、他のポイントを調整することに限られていました。システムの実行にはほとんど選択肢が残されていませんでした。

この記事では、この状況の改善を試みます。分析をより扱いやすくするために、いくつかの追加コントロールを使用します。統計分析やチャートコントロールの面で完全に機能するシステムを手に入れるには、まだ多くの仕事をしなければなりませんが、これは良いスタートです。

今回は、いくつかの調整をおこなうだけなので、比較的短い記事になるでしょう。このステップでは詳細は省きます。ここでの目標は、リプレイをより簡単に導入し、システムを実践したい人たちのために分析するために必要なコントロールの基礎を築くことです。


計画

この計画ステップは非常に簡単です。前回の記事でシステムがどのように機能したかを見れば、何をすべきかは明らかだからです。一時停止、再生、そしてなにより、特定の時間を選択して調査を開始できるようなコントロールフォームを作成する必要があります。

現在のビューでは、常に最初の取引ティックから開始します。市場5時間目、つまり14:00から調査をおこないたいとします(市場が9:00に開くと仮定して)。この場合、リプレイを5時間待ってから必要な分析をおこなうことになります。これは完全に不可能です。なぜなら、リプレイを停止しようとすると閉じられてしまい、最初の取引ティックからやり直す必要があるからです。

アイデア自体は興味深いものですが、今の仕組みではモチベーションが下がってしまうため、今すぐに何をしなければならないかは明確です。

大まかな方向性が決まったので、あとは実行に移すだけです。


実装

実装は非常に興味深いものになるでしょう。真の制御システムを作成するには、最も単純なものから最も多様なものまで、さまざまなパスを通過する必要があるためです。ただし、記事の公開順に従って、スキップしたり、いくつかのステップを進めたりすることなく説明を注意深く読めば、すべての手順を簡単に理解できます。

多くの方が使うと思われているかもしれませんが、システムでDLLを使うつもりはありません。リプレイシステムは純粋にMQL5言語を使って実装します。MetaTrader 5を最大限に活用し、必要な機能を作成する際にプラットフォーム内でどこまでできるかを示すことがここでの狙いです。外部実装に頼ることは、MQL5で作業する楽しみの多くを奪い、MQL5はニーズを満たせないという印象を与えます。

前回の記事で使用されたコードを見ると、システムがリプレイを作成するためにサービスを使用していることがわかります。また、それを開始するスクリプトも含まれていました。このスクリプトによって、サービスはカスタム銘柄にティックを送信し、リプレイを作成することができました。シンプルなスイッチングメカニズムを採用しましたが、この方法はより効率的な制御には適していません。もっと困難な道を歩まなければなりません。


超基本的なEAの作成

EAを使ってコントロールを実装してみましょう。このEAは、サービスがバーのティックを生成するタイミングとしないタイミングを制御します。

なぜEAなのでしょうか。EAの代わりに指標を使っても同じように機能しますが、後で注文シミュレーションシステムを作るのに必要になるので、EAを使いたいと思います。さらに、別の連載「一からの取引エキスパートアドバイザーの開発」で紹介した注文システムも使ってみます。今のところ、注文システムについては心配する必要はありません。

基本的なEAの完全なコードを以下に示します。

#property copyright "Daniel Jose"
#property version   "1.00"
//+------------------------------------------------------------------+
#include <Market Replay\C_Controls.mqh>
//+------------------------------------------------------------------+
C_Controls      Control;
//+------------------------------------------------------------------+
int OnInit()
{
        Control.Init();
                
        return INIT_SUCCEEDED;
}
//+------------------------------------------------------------------+
void OnDeinit(const int reason) {}
//+------------------------------------------------------------------+
void OnTick() {}
//+------------------------------------------------------------------+
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        Control.DispatchMessage(id, lparam, dparam, sparam);
}
//+------------------------------------------------------------------+

コードは非常にシンプルですが、サービスの操作をコントロールするには十分です。では、コードのある部分、つまり上で強調したコントロールオブジェクトクラスを見てみましょう。開発の初期段階では、コードはそれほど複雑ではありません。リプレイサービスをリプレイ・一時停止するためのボタンを1つだけ実装する予定です。では、現段階でのこのクラスを見てみましょう。

まず注意すべき点は以下の通りです。

#property copyright "Daniel Jose"
//+------------------------------------------------------------------+
#include <Market Replay\Interprocess.mqh>
//+------------------------------------------------------------------+
#define def_ButtonPlay  "Images\\Market Replay\\Play.bmp"
#define def_ButtonPause "Images\\Market Replay\\Pause.bmp"
#resource "\\" + def_ButtonPlay
#resource "\\" + def_ButtonPause
//+------------------------------------------------------------------+
#define def_PrefixObjectName "Market Replay _ "

最初の重要な点は、のヘッダーファイルです。詳しくは後述します。次に、リプレイボタンと一時停止ボタンを表すビットマップオブジェクトをいくつか定義します。複雑なことは何もありません。これらのポイントを定義したら、クラスコードに移ります。完全なコードを以下に示します。

class C_Controls
{
        private :
//+------------------------------------------------------------------+
                string  m_szBtnPlay;
                long            m_id;
//+------------------------------------------------------------------+
                void CreateBtnPlayPause(long id)
                        {
                                m_szBtnPlay = def_PrefixObjectName + "Play";
                                ObjectCreate(id, m_szBtnPlay, OBJ_BITMAP_LABEL, 0, 0, 0);
                                ObjectSetInteger(id, m_szBtnPlay, OBJPROP_XDISTANCE, 5);
                                ObjectSetInteger(id, m_szBtnPlay, OBJPROP_YDISTANCE, 25);
                                ObjectSetInteger(id, m_szBtnPlay, OBJPROP_STATE, false);
                                ObjectSetString(id, m_szBtnPlay, OBJPROP_BMPFILE, 0, "::" + def_ButtonPause);
                                ObjectSetString(id, m_szBtnPlay, OBJPROP_BMPFILE, 1, "::" + def_ButtonPlay);
                        }
//+------------------------------------------------------------------+
        public  :
//+------------------------------------------------------------------+
                C_Controls()
                        {
                                m_szBtnPlay = NULL;
                        }
//+------------------------------------------------------------------+
                ~C_Controls()
                        {
                                ObjectDelete(ChartID(), m_szBtnPlay);
                        }               
//+------------------------------------------------------------------+
                void Init(void)
                        {
                                if (m_szBtnPlay != NULL) return;
                                CreateBtnPlayPause(m_id = ChartID());
                                GlobalVariableTemp(def_GlobalVariableReplay);
                                ChartRedraw();
                        }
//+------------------------------------------------------------------+
                void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
                        {
                                u_Interprocess Info;
                                
                                switch (id)
                                {
                                        case CHARTEVENT_OBJECT_CLICK:
                                                if (sparam == m_szBtnPlay)
                                                {
                                                        Info.s_Infos.isPlay = (bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE);
                                                        GlobalVariableSet(def_GlobalVariableReplay, Info.Value);
                                                }
                                                break;
                                }
                        }
//+------------------------------------------------------------------+
};

ここにはInitDispatchMessageの2つの主要関数があります。 これらは、EAの操作に必要なすべての作業をこの早い段階で実装します。これらの詳細を説明するために、以下で2つの関数を見てみましょう。Initから始めましょう。

void Init(void)
{
        if (m_szBtnPlay != NULL) return;
        CreateBtnPlayPause(m_id = ChartID());
        GlobalVariableTemp(def_GlobalVariableReplay);
        ChartRedraw();
}

Initが呼ばれると、まずコントロール要素が以前に作成されているかどうかを確認します。すでに起きている場合は、戻ります。チャートの期間を変更したりEAにチャートの再読み込みを要求するような変更を加えた場合(これは頻繁に起こります)、リプレイサービスの状態は変更されないため、これは重要です。つまり、サービスが実行中(一時停止中)であれば、そのまま継続し、プレイ中であれば、ティックを送信し続けます。

最初の呼び出しであれば、メインコントロールが作成され、現時点では再生ボタンと一時停止ボタンのみとなる。次に、EAとサービス間の通信に使用するグローバルターミナル値を作成します。この時点では、値を代入せずに変数を作っているだけです。

その後、オブジェクトをスクリーンに適用しなければなりません。 この強制アップデートがおこなわれないと、EAは読み込まれますがサービスは停止され、システムがクラッシュしたかのように思われるからです。しかし実際には、オブジェクトがプロットされ、市場のリプレイを実行できるように、MetaTrader 5がチャートを更新してくれるのを待つことになります。

どんなに簡単かにお気づきでしょうか。DispatchMessage関数のコードを見てみましょう。これも現時点ではきわめシンプルで鵜s。以下はそのコードです。

void DispatchMessage(const int id, const long &lparam, const double &dparam, const string &sparam)
{
        u_Interprocess Info;
                        
        switch (id)
        {
                case CHARTEVENT_OBJECT_CLICK:
                        if (sparam == m_szBtnPlay)
                        {
                                Info.s_Infos.isPlay = (bool) ObjectGetInteger(m_id, m_szBtnPlay, OBJPROP_STATE);
                                GlobalVariableSet(def_GlobalVariableReplay, Info.Value);
                        }
                        break;
        }
}

私たちはMetaTrader 5を使ってすべてを管理しています。u_Interprocessunionを使ってグローバルターミナル値を設定し ビットマップボタンの状態を確認します。 このように ターミナルのグローバル変数を調整し、リプレイの作成を担当するサービスプロセスに渡すようにします。

このため、リプレイシステムは常に一時停止状態でスタートします。すべてのオブジェクトを含むEAがチャートに読み込まれれたら、いつでも好きなときにリプレイしたり、市場リプレイを一時停止したりすることができます。これで少しは面白くなるでしょう。

Interprocess.mqhファイルについて

お察しの通り、システムをスクリプトではなくEAを使用するように変更したことで、リプレイサービスにいくつかの変更が生じました。これらの変更を検討する前に、Interprocess.mqhファイルを見てみましょう。現在の完全なコードは以下です。
#define def_GlobalVariableReplay "Replay Infos"
//+------------------------------------------------------------------+
union u_Interprocess
{
        double Value;
        struct st_0
        {
                bool isPlay;
                struct st_1
                {
                        char Hours;
                        char Minutes;
                }Time[3];
        }s_Infos;
};

このシンプルな定義は、私たちに名前を与えてくれますが、それはただの名前ではありません。これは、この段階でEAとサービス間の通信を可能にするために使用されるグローバルターミナル変数の名前となります。しかし、経験の浅いユーザーにとって複雑なのはunionの部分です。

このunionが実際に何を表しているのかを見て、EAとサービス間の情報の受け渡しにどのように使われているのかを理解しましょう。そもそも、複雑さを理解するためには、各データタイプのデータが何ビット使われるかを知らなければなりません。下の表をご覧ください。作業を楽にするために、以下の表をよく理解しておくことをお勧めします。

種類 ビット数
Bool 1ビット
CharまたはUChar 8ビット
ShortまたはUShort 16ビット
IntまたはUInt 32ビット
LongまたはULong 64ビット

この表は、符号付き整数と符号なし整数の型とビット数をリストします(ビットとバイトを混同しないでください。)。ビットは、オンまたはオフの状態を表す情報の最小単位で、2進数では1と0になります。バイトはビットの集合です。

この表を見ると、次の考えが明確ではないかもしれません。uchar型の変数にはbool型の変数が8つあります。つまり、uchar変数は8つのbool変数の「union」(この言葉はちょっと違います)に相当します。コードでは次のようになります。

union u_00
{
        char info;
        bool bits[8];
}Data;

このunionの長さは8ビット、つまり1バイトです。配列のビットを書き込み、特定の位置を選択することで、情報の内容を変更することができます。例えば、Data.infoを0x12にするには、以下の2つのうちどちらかを実行できます。

Data.info = 0x12;

or 

Data.bits[4] = true;
Data.bits[1] = true;

いずれにせよ、Data.info変数の初期ビットがすべて0に設定されていれば、同じ結果が得られます。それがunionです。

さて、元のコードに戻りましょう。64ビットシステムで見られる最大の型は、long型(符号あり)またはulong型(符号なし)です。符号があれば、負の値を表すことができます。符号なしで表せるのは正の数だけです。つまり、この場合は次のようになります。

それぞれの正方形は1ビットを表し、QWORDという名前は現代のプログラミング言語の母体であるアセンブリ言語に由来します。同じ構造は、別の型であるfloatにも見られます。

floatは、値が厳密ではないが、計算可能な値を表すために使用できる変数です。基本的には2種類あります。

種類 ビット数
float 32ビット
double 64ビット

これは前述した整数型に似ており、各ビットはオンかオフの状態を表します。floatでは、同じ値がロジックを表すことはありません。それらは少し異なる創造の原則に従っていますが、今は考慮しません。ここで重要な詳細は違います。

ターミナルグローバル変数が使用している型を見ると、float型、より正確にはdouble型(64ビット)しかないことがわかります。ここで質問です。同じ長さを持つ整数型はなんでしょうか。お答えの通り、同じ64ビットを持つlongタイプです。longdoubleを組み合わせれば、まったく異なる2つのものを同時に表現することができます。

しかし、ここには少し複雑な問題があります。どの型が使われているかはどうやってわかるのでしょうか。これを解決するために、完全な型は使わず、その断片だけを使い、その断片に名前をつけます。こうして、Interprocess.mqhファイルのコードで見ることができるunionができたのです。

実際、doubleを使うつもりはありません。double型で生成されるべき値を直接手で書こうとするのは、まったく不適切で、簡単でもありません。その代わりに、名前付きのパーツを使用してこの作成をおこない、適切なビットに0または1を表す正しい値を設定します。その後、グローバルターミナル変数にdouble値を入れ、他のプロセス(この場合はサービス)がその値を受け取り、何をすべきかを正確に理解してデコードします。

すべてが非常にシンプルでわかりやすいルールでおこなわれているのがわかるでしょう。floatを直接作り、その意味を理解しようとすれば、これは非常に難しいことです。

unionとは何か、そしてunionをどのように使っていくかは、これではっきりしたと思います。ただ、覚えておいてください。double型の64ビットのターミナルグローバル変数を使いたい場合、作成されるunionは同じ64ビットを超えてはいけません。


リプレイサービスの作成方法について

おそらく、何が起こっているのかを理解するために、最も注意を払う必要があるのはこの部分でしょう。理解せずに何かをすれば、すべてが台無しになります。これは簡単なことのように聞こえますが、もし誤解していると、なぜこのシステムは説明され実演されたとおりに動くのに、自分のワークステーションでは動かせないのかと不思議に思うような細部があります。

では、リプレイサービスを見てみましょう。今のところはまだ非常にコンパクトでシンプルです。そのコード全体を以下に示します。

#property service
#property copyright "Daniel Jose"
#property version   "1.00"
//+------------------------------------------------------------------+
#include <Market Replay\C_Replay.mqh>
//+------------------------------------------------------------------+
input string    user01 = "WINZ21_202110220900_202110221759"; //File with ticks
//+------------------------------------------------------------------+
C_Replay        Replay;
//+------------------------------------------------------------------+
void OnStart()
{
        ulong t1;
        int delay = 3;
        long id;
        u_Interprocess Info;
        bool bTest = false;
        
        if (!Replay.CreateSymbolReplay(user01)) return;
        id = Replay.ViewReplay();
        Print("Waiting for permission to start replay ...");
        while (!GlobalVariableCheck(def_GlobalVariableReplay)) Sleep(750);
        Print("Replay service started ...");
        t1 = GetTickCount64();
        while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.Value)))
        {
                if (!Info.s_Infos.isPlay)
                {
                        if (!bTest)     bTest = (Replay.Event_OnTime() > 0); else       t1 = GetTickCount64();
                }else if ((GetTickCount64() - t1) >= (uint)(delay))
                {
                        if ((delay = Replay.Event_OnTime()) < 0) break;
                        t1 = GetTickCount64();
                }
        }
        Replay.CloseReplay();
        Print("Replay service finished ...");
}
//+------------------------------------------------------------------+

このコードを使ってWINZ21_202110220900_202110221759ファイルを作成し、それを実行してみると、何も起こらないことがわかるでしょう。添付ファイルを使って、このコードから実行しようとしても、何も起こりません。でも、なぜでしょうか。その理由は id = Replay.ViewReplay()にあります。このコードは、市場リプレイシステムを実際に使用するためには理解しなければならないことをおこなっています。何をするにしても、何が起こっているのかを理解しなければ、何も意味がありません。ViewReplay()内のコードを見る前に、まず上記のコードのデータの流れを理解しましょう。

この仕組みを理解するために、次の断片から、より小さな部分に分解してみましょう。

if (!Replay.CreateSymbolReplay(user01)) return;

この行は指定されたファイルから取引されたティックデータを読み込みます。読み込みに失敗すると、サービスは単に終了します。

id = Replay.ViewReplay();

この行はEAを読み込みますが、後で詳しく説明するので、先に進みましょう。

while (!GlobalVariableCheck(def_GlobalVariableReplay)) Sleep(750);

上の行はループの中で、EAが読み込まれるのを待つか、グローバルターミナル変数を作成する何かを待つことになります。これは、サービス環境の外部で実行されているプロセス間の通信形態として機能します。

t1 = GetTickCount64();

この行は、サービス内部カウンタの最初のキャプチャを実行します。この最初のキャプチャは必要かもしれないし、必要でないかもしれません。有効化するとすぐに一時停止モードに入るので、通常はまったく必要ありません。

while ((ChartSymbol(id) != "") && (GlobalVariableGet(def_GlobalVariableReplay, Info.Value)))

この点は非常に興味深いものです。ここには2つのテストがあります。そのいずれかが失敗すると、リプレイサービスは終了します。最初の場合、リプレイ資産ウィンドウがターミナルに存在するかどうかを確認します。トレーダーがこのウィンドウを閉じると、リプレイは実行されなくなるため、リプレイシステムは終了します。2つ目の場合、テストすると同時に、ターミナルグローバル変数から値を取得します。この変数が存在しなくなると、サービスも終了します。

        u_Interprocess Info;

//...

        if (!Info.s_Infos.isPlay)

ここでは、トレーダーまたはリプレイユーザーから通知された条件を確認します。プレイモードであれば、このテストは失敗しますが、一時停止モードに入れば、成功します。どのようにunionを使ってdouble値内の正しいビットをキャプチャしているかに注目してください。このunionがなければ、これは不可能なことです。

一時停止モードに入ったら、次の行を実行します。

if (!bTest) bTest = (Replay.Event_OnTime() > 0); else t1 = GetTickCount64();

この行では、最初の取引ティックだけが資産に送信されます。これは後述するいくつかの理由から重要です。これが完了すると、リプレイサービスが一時停止するたびに、タイマーの現在値をキャプチャすることになります。確かに、この「一時停止」モードは、サービスが実際に一時停止していることを意味するものではありません。リプレイ銘柄にティックを送信していないだけです。それが「一時停止」であると言っている理由です。

しかし、ユーザーまたはトレーダーが市場リプレイを開始または再開したい場合は、新しいコード行を入力します。以下に示します。

else if ((GetTickCount64() - t1) >= (uint)(delay))

ティック間の遅延の値に基づいて、新しいティックを送信する必要があるかどうかを確認します。この値は次のコード行で得られます。

if ((delay = Replay.Event_OnTime()) < 0) break;

遅延が0より小さい場合、リプレイサービスは終了します。これは通常、最後の取引ティックがリプレイ資産に送信されたときに起こります。

これらの関数は、最後のティックが送信されるか、リプレイ資産チャートが閉じられるまで実行されます。こうなると、次の行が実行されます。

Replay.CloseReplay();

これでリプレイは永久に終了します。

このコードはすべて非常に美しく、理解しやいものですが、ここでC_Replayという1つのクラスを指している点がいくつかあることにお気づきでしょうか。では、このクラスを見てみましょう。そのコードは、これまでの記事で見てきたものと多くの共通点があります。しかし、もっと注目に値する部分があります。これこそが、私たちがこれから見ようとしていることです。


C_ReplayクラスのViewReplayはなぜ重要なのか

このコードを以下に示します。

long ViewReplay(void)
{
        m_IdReplay = ChartOpen(def_SymbolReplay, PERIOD_M1);
        ChartApplyTemplate(m_IdReplay, "Market Replay.tpl");
        ChartRedraw(m_IdReplay);
        return m_IdReplay;
}

この4行のどこがリプレイの発生を許したり防いだりするのに重要なのかと疑問に思われるかもしれません。これはかなり単純なコードですが、非常に強力です。とても強力で、すべてが正しいと思われるときでさえ邪魔になります。

この瞬間を見てみましょう。まず最初に、リプレイ資産の名前でチャートを開き、期間を1分に設定します。前2回の記事でおわかりのように、この時間はいつでも好きなときに変更できます。

これが完了したら、特定のテンプレートを読み込み、新しく開いたチャートウィンドウに適用します。.このテンプレートは非常に特殊であることに注意することが重要です。このテンプレートを作成するには、もし削除してしまった場合(添付ファイルにあります)、市場リプレイシステムからEAをコンパイルし、このEAを任意の資産に適用する必要があります。そしてこのチャートをテンプレートとして保存し、Market Replayという名前をつけるだけです。このファイルが存在しない場合、あるいはEAが存在しない場合、何をしてもシステム全体が失敗します。

ある意味、EAの代わりに指標を使えば解決できるかもしれません。この場合、MQL5を介してこの指標を呼び出すことになります(理論上は)。しかし、冒頭で述べたように、私には指標ではなくEAを使う理由があります。そこで、可能な限りシンプルな方法で読み込みの問題を解決するために、リプレイシステムのEAを含むテンプレートを使用します。

しかし、EAが読み込まれるとターミナルグローバル変数が作成され、システムが動作する準備ができていることがサービスに通知されるため、これを実行するという単純な事実はあまり保証されません。ただし、コントロールが表示されるまでには時間がかかります。少し速くするために、リプレイ資産チャートでオブジェクトを強制的に更新します。

ここでリプレイ資産チャートのIDを返します。他の場所ではできないからです。チャートがいつ閉じられたかをサービスが知るために、この情報が必要です。

C_Replayクラスの他のすべての関数は、理解するのが非常に簡単なので、この記事では考慮しません。


結論

以下のビデオでは、システムがどのように読み込まれ、実際にどのように機能するかを見ることができます。



次回は、リプレイシステムがどのタイミングでスタートするかを選択できるように、ポジションコントロールシステムを作成します。またお会いしましょう。


MetaQuotes Ltdによりポルトガル語から翻訳されました。
元の記事: https://www.mql5.com/pt/articles/10706

添付されたファイル |
Market_Replay.zip (10282.77 KB)
データサイエンスと機械学習(第14回):コホネンマップを使って市場で自分の道を見つける データサイエンスと機械学習(第14回):コホネンマップを使って市場で自分の道を見つける
複雑で変化し続ける市場をナビゲートする、最先端の取引アプローチをお探しですか。人工ニューラルネットワークの革新的な形態であるコホネンマップは、市場データの隠れたパターンやトレンドを発見するのに役立ちます。この記事では、コホネンマップがどのように機能するのか、そして、より賢く、より効果的な取引戦略を開発するために、どのように活用できるのかを探ります。経験豊富なトレーダーも、これから取引を始める人も、このエキサイティングな新しいアプローチを見逃す手はありません。
MQL5の圏論(第8回):モノイド MQL5の圏論(第8回):モノイド
MQL5における圏論の実装についての連載を続けます。今回は、ルールと単位元を含むことで、圏論を他のデータ分類法と一線を画す始域(集合)としてモノイドを紹介します。
Rebuyのアルゴリズム:多通貨取引シミュレーション Rebuyのアルゴリズム:多通貨取引シミュレーション
本稿では、多通貨の価格設定をシミュレートする数理モデルを作成し、前回理論計算から始めた取引効率を高めるメカニズム探求の一環として、分散原理の研究を完成させます。
リプレイシステムの開発—市場シミュレーション(第2回):最初の実験(II) リプレイシステムの開発—市場シミュレーション(第2回):最初の実験(II)
今回は、1分という目標を達成するために、別の方法を試してみましょう。ただし、このタスクは思っているほど単純ではありません。