開発者の手記 #4 — オーダー管理の「状態遷移」を図で整理した話

拓海だ。前回までは「時間軸」「インジケーター計算」「ログ出力」の話をした。今回は、cBotを動かしてて一番バグが出やすい「オーダー管理」について書く。

オーダー状態を甘く見ると、ポジション管理が破綻する

cBotでエントリーしたら、当たり前のようにポジションが出来ると思ってないか。実は違う。

オーダーを発注してから、実際にポジションになるまでに、複数の「状態」を経由する。その状態遷移を理解してないと、同じロジックで何度も注文が出たり、決済されないはずのポジションがいつの間にか決済されてたり、意図しない約定が発生したりする。

自分も最初、ここで本当に困った。「なぜか2ロット注文されてる」「SLが機能してない」「TP決済の後、なぜか売却シグナルが出た」…こんなことが起きた。

オーダーの状態遷移は3段階

cTraderのオーダーは、大きく3つの状態を持つ。

1. Pending(待機中)

発注直後。市場に注文が出ているが、まだ約定してない状態。指値注文なら「この価格まで来たら約定させる」という待ち状態。成行注文でも、一瞬だがこの状態を通る。

このとき大事なのは「Pending状態のオーダーは、ポジションではない」ということ。

つまり、OnBar()で毎回エントリーロジックを判定してると、Pending状態のオーダーがまだ約定してないのに「あ、条件が満たされてる、もう一回注文しよう」と、2回目の注文を出してしまう。

自分がこれで引っかかったのは、ATRベースのストップロス幅で計算してたとき。毎bar毎に「SL幅が広がってるから、新しいオーダーを出そう」と判定して、複数オーダーが溜まってた。

2. Active(有効中)

オーダーが約定した状態。ポジションが出来てる。このとき、SLやTPが有効になる。

Activeになったら、エントリーロジックの判定を止める。これが重要だ。

3. Closed(決済済み)

ポジションが決済された状態。SLで損切りされたか、TPで利確されたか、手動で決済されたか。理由は様々だが、この状態になったら終わり。

状態遷移を図に整理する

これを図に落とすとこんな感じだ。

[Pending] → [Active] → [Closed]
   ↑         ↓          ↓
   |    (SL/TP有効)   (終了)
   |         ↓
   |    [Active]
   |         |
   └─────────┘
   (キャンセル)

Pending状態で「あ、条件が変わった」と判定したら、オーダーをキャンセルして新しいのを出す。Active状態になったら、新規エントリーロジックは一切実行しない。決済ロジック(SL/TP)だけ走らせる。

実装のコツ:状態フラグを持つ

自分が採用してる方法は「現在、どの状態にあるか」をフラグで管理することだ。

private bool isOrderPending = false;
private bool isOrderActive = false;
private Position currentPosition = null;

protected override void OnBar()
{
    // 1. Active状態なら、エントリーロジックは実行しない
    if (isOrderActive)
    {
        // 決済ロジックのみ
        CheckExitCondition();
        return;
    }

    // 2. Pending状態なら、新規オーダーは出さない
    if (isOrderPending)
    {
        // Pending状態の監視のみ
        CheckPendingOrder();
        return;
    }

    // 3. どちらでもなければ、エントリーロジックを実行
    CheckEntryCondition();
}

private void CheckEntryCondition()
{
    if (/* エントリー条件 */)
    {
        // オーダー発注
        PlaceOrder();
        isOrderPending = true;
        Log($"[{Time}] Order placed (Pending)");
    }
}

private void CheckPendingOrder()
{
    // cTraderの Orders コレクションを監視
    foreach (var order in Orders)
    {
        if (order.State == OrderState.Accepted)
        {
            // まだPending
            continue;
        }
        else if (order.State == OrderState.Filled)
        {
            // Active状態に遷移
            isOrderPending = false;
            isOrderActive = true;
            currentPosition = Positions.First(p => p.Id == order.Position.Id);
            Log($"[{Time}] Order filled (Active)");
        }
    }
}

private void CheckExitCondition()
{
    if (currentPosition == null || currentPosition.State != PositionState.Open)
    {
        // 決済済み
        isOrderActive = false;
        currentPosition = null;
        Log($"[{Time}] Position closed (Closed)");
        return;
    }

    if (/* 決済条件 */)
    {
        currentPosition.Close();
    }
}

こんな感じで「今、どの状態か」を明確に管理することで、二重注文や意図しない決済がほぼ出なくなる。

決済理由をログで記録する

もう1つ重要なのが「なぜ決済されたのか」をログに記録することだ。

private void OnPositionClosed(PositionClosedEventArgs args)
{
    var position = args.Position;
    var pnl = position.NetProfit;
    var closeReason = "Unknown";

    if (Math.Abs(position.Commissions) > 0) closeReason = "SL Hit";
    if (/* TP条件 */) closeReason = "TP Hit";
    if (/* ロジック判定 */) closeReason = "Logic Exit";

    Log($"[{Time}] Position closed - Reason: {closeReason}, PnL: {pnl}");
}

こうすると「あ、このポジション、実はSLで決済されてたんだ」とか「決済ロジックが想定と違う価格で実行されてた」とか、後から検証しやすくなる。

バックテストと実運用の差を減らす

この状態遷移を正確に実装することで、バックテストと実運用の差がかなり減る。

バックテストでは「オーダーは即座に約定する」という仮定になることが多いんだが、実運用では「Pending → Active → Closed」の遷移に時間差がある。その時間差を意識して実装すれば、バックテスト結果がより現実的になる。

自分は前回の記事で「ログ出力を徹底しろ」と書いたが、特にこのオーダー状態遷移のログは重要だ。毎回「[時刻] 状態 - 詳細」という形で記録しておくと、バグ調査がめちゃくちゃ早くなる。

まとめ

オーダー管理で失敗しないコツ:

  1. 状態遷移を図で整理する — Pending → Active → Closed の流れを頭に入れる
  2. 状態フラグで管理する — 「今どの状態か」を常に把握する
  3. 状態ごとにロジックを分ける — Pending中は新規エントリーしない
  4. 決済理由をログに残す — 後から検証するときに必須

これをやるようになってから、オーダー関連のバグはほぼ出なくなった。cBotを組む人は、是非試してみてくれ。

本記事は一般的な情報提供を目的としています。投資助言ではありません。 取引判断はご自身の責任で行ってください。