リアルタイムFX取引システムにおいて、複数通貨ペアの同時購読は一見すると単なるスケーラビリティの問題に見える。しかし実運用に入ると、すぐにより本質的な問題が現れる。それが**データの乱順(Out-of-Order Data)**である。

特にEURUSD、GBPUSD、USDJPYのような複数ペアを同時に購読する高頻度環境では、同じ時間帯のデータであっても必ずしも時系列順に到達するとは限らない。ネットワーク遅延、サーバー側の並列処理、クライアント側のスケジューリングによって順序が崩れる。

一見すると単なる順序の乱れだが、取引システムにおいては以下に直接影響する:

  • ローソク足生成のズレ
  • インジケーター計算の歪み
  • マーケットメイキングの異常な価格更新
  • リスク判定の誤作動

本質的にはこれはデータの問題ではなく、ストリーミングシステムの一貫性問題である。

1. 複数通貨ペア購読の本質:1接続・複数時間軸

多くのFX APIのWebSocket設計は「単一接続・複数購読」である:

  • 1本のTCP接続
  • 複数シンボル同時購読
  • サーバーが並列でデータ送信

例えば:

  • EURUSD:更新頻度が高い
  • GBPUSD:変動が大きく断続的
  • USDJPY:特定時間帯に流動性集中

これらはサーバー側では並列生成されるが、クライアント側では1本のストリームに統合される。

その結果:

複数の独立した時間軸が単一キューに強制統合される。

これにより順序の保証が崩れる。

2. 乱順データはどのように発生するか?

乱順は主に4つの層で発生する:

2.1. ネットワークジッター

TCPは信頼性は保証するが順序は保証しない。

  • 異なるルート
  • キュー遅延
  • バッチACK

結果として後のデータが先に届くことがある。

2.2. サーバー側並列処理

マーケットサーバーはマルチスレッド構造:

  • symbol Aはスレッド1
  • symbol Bはスレッド2

統合時に時間順には並ばない。

2.3. クライアントイベントループ

PythonやNode.jsでは:

  • IOと処理は非同期
  • コールバック遅延あり

そのため:

到達順 ≠ 生成順 ≠ 時刻順

2.4. 共通バッファ構造

単純実装では:

全symbol → 1つのon_message → 1キュー

これにより構造的順序は完全に崩壊する。

3. 乱順データの実務影響

3.1. ローソク足の破損

  • 後のtickが先に処理される
    → OHLCが壊れる

3.2. モメンタムの歪み

  • return(t)とreturn(t-1)が崩れる
    → シグナルがノイズ化

3.3. マーケットメイキング異常

  • スプレッド拡大
  • ミッド価格不安定
  • ヘッジ誤作動

4. 本質的解決:時間とメッセージ順序を分離する

重要なのは「乱れを防ぐこと」ではなく:

クライアント側で決定的な順序を再構築すること

キーとなる要素:

  • timestamp
  • seq_id
  • symbol

5. エンジニアリング解決:3層構造

第1層:symbolごとの分離

streams = {
    "EURUSD": Queue(),
    "GBPUSD": Queue(),
    "USDJPY": Queue()
}

各通貨ペアごとに独立した時間軸を維持する。

第2層:時間バッファ(Reorder Buffer)

BUFFER_MS = 200

処理手順:

  1. tickを受信
  2. 即時処理しない
  3. バッファに格納
  4. timestamp順に整列して出力

第3層:単調増加シーケンス検証

APIがseq_idを提供する場合:

last_seq = {}

def check_order(symbol, tick):
    seq = tick["seq"]

    if symbol not in last_seq:
        last_seq[symbol] = seq
        return True

    if seq > last_seq[symbol]:
        last_seq[symbol] = seq
        return True

    return False

これにより遅延パケットや重複データを除外できる。

6. 実運用レベルのWebSocket構成

import websocket
import json
import uuid
import time
from collections import defaultdict, deque

API_KEY = "YOUR_API_KEY"
WS_URL = f"wss://quote.alltick.co/quote-b-ws-api?token={API_KEY}"

SYMBOLS = ["EURUSD", "GBPUSD", "USDJPY"]

buffers = defaultdict(deque)
last_seq = {}

def subscribe_msg():
    return {
        "cmd_id": 22004,
        "seq_id": int(time.time()),
        "trace": str(uuid.uuid4()),
        "data": {
            "symbol_list": [{"code": s} for s in SYMBOLS]
        }
    }

def on_open(ws):
    ws.send(json.dumps(subscribe_msg()))

def process_tick(symbol, tick):
    seq = tick.get("seq")

    if seq is not None:
        if symbol in last_seq and seq <= last_seq[symbol]:
            return
        last_seq[symbol] = seq

    buffers[symbol].append(tick)

def on_message(ws, message):
    msg = json.loads(message)

    if msg.get("cmd_id") == 22998:
        tick = msg["data"]
        symbol = tick["code"]
        process_tick(symbol, tick)

def start():
    ws = websocket.WebSocketApp(
        WS_URL,
        on_open=on_open,
        on_message=on_message
    )
    ws.run_forever(ping_interval=10)

if __name__ == "__main__":
    start()

7. FX市場がCryptoより乱順になりやすい理由

  • 流動性提供者(LP)が分散
  • 通貨ペアごとの更新頻度差
  • セッション依存性(アジア・ロンドン・NY)

結果:

データは連続ストリームではなく、バースト型ストリームになる

8. 上級アーキテクチャ:イベント時間システムへ

より高度なシステムでは以下を採用する:

● event-time処理

arrival-timeではなくイベント発生時刻を基準にする

● watermark機構

遅延データを一定範囲まで許容する

  • 遅延tickを受け入れる
  • 一定時間超過で破棄

● シンボル間クロック同期

複数通貨ペアの時間基準を統一する

9. システムが安定したときに見えるもの

乱順問題が解消されると、市場データは次のように変化する:

  • EURUSDの不規則な跳動が消える
  • GBPUSDのスパイクが安定する
  • USDJPYの流れが構造化される

結果として見えるのは単なる価格ではなく:

構造化された時間システム

になる。

その上で、AllTick API のようなインフラは、単なる価格配信ではなく、エンジニアリング可能なリアルタイム時間ストリーム基盤として機能する。