開発者の手記 #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」の遷移に時間差がある。その時間差を意識して実装すれば、バックテスト結果がより現実的になる。
自分は前回の記事で「ログ出力を徹底しろ」と書いたが、特にこのオーダー状態遷移のログは重要だ。毎回「[時刻] 状態 - 詳細」という形で記録しておくと、バグ調査がめちゃくちゃ早くなる。
まとめ
オーダー管理で失敗しないコツ:
- 状態遷移を図で整理する — Pending → Active → Closed の流れを頭に入れる
- 状態フラグで管理する — 「今どの状態か」を常に把握する
- 状態ごとにロジックを分ける — Pending中は新規エントリーしない
- 決済理由をログに残す — 後から検証するときに必須
これをやるようになってから、オーダー関連のバグはほぼ出なくなった。cBotを組む人は、是非試してみてくれ。
本記事は一般的な情報提供を目的としています。投資助言ではありません。 取引判断はご自身の責任で行ってください。