私は2024年にTardisの正規化(normalized)ヒストリカルデータフィードをHFT(高頻度取引)リサーチ環境に統合した経験から、Tardis normalized book snapshotが板情報のティックバイティック再生における事実上の業界標準であることを実感しました。バイナンス・スポットだけで1銘柄あたり1日約8GBのJSON Linesが生成されるため、パーサー選定を誤ると瞬時にストレージ・CPU・コストが雪だるま式に膨れ上がります。本稿では、フォーマットの正確な仕様、RustとPythonによる本番級パーサー、HolySheep AIによるメタデータ拡張、ベンチマーク数値、そして運用で実際に踏みやすい3つのエラーとその解決策までを一気に整理します。

HolySheep AIは、LLM APIを¥1=$1の固定レート(公称レート¥7.3=$1比で約85%削減)で提供する日本語最適化ゲートウェイです。今すぐ登録すると無料クレジットが付与され、本稿のサンプルコードを即日動かせます。

Tardis正規化板スナップショットとは

Tardisは暗号資産デリバティブを含む40以上の取引所から、ティック・板・板スナップショット・板更新・清算・オプション Greeksをスキーマ正規化された単一フォーマットで配信するヒストリカルデータプロバイダです。板スナップショット(book_snapshot系)は時点の完全なL2/L3板情報を1レコードに凍結したもので、再生時の初期化・整合性チェック・バックテストの起点に用いられます。

フォーマット仕様の詳細

1レコードの最小スキーマは次の通りです。

{
  "type": "book_snapshot_25",
  "exchange": "binance",
  "symbol": "BTC-USDT",
  "timestamp": "2024-08-12T03:14:15.926512Z",
  "local_timestamp": "2024-08-12T03:14:16.041118Z",
  "bids": [
    ["60123.40", "0.83500000"],
    ["60123.30", "1.40200000"],
    ["60123.20", "0.01200000"]
  ],
  "asks": [
    ["60123.50", "0.42100000"],
    ["60123.60", "2.31000000"],
    ["60123.70", "0.50000000"]
  ]
}
フィールド単位意味
typestring-book_snapshotまたはbook_snapshot_N
exchangestring-小文字スラッグ(binancecoinbasebitmexなど)
symbolstring-正規化済みティッカー(BTC-USDT
timestampstringUTC ISO 8601(μs精度)取引所のイベント時刻
local_timestampstringUTC ISO 8601(μs精度)受信側マシンのローカル時刻(遅延計測用)
bidsasksArray<[price, amount]>文字列(10進固定小数)価格降順/昇順。文字列は精度保全のため

重要な落とし穴として、priceとamountが文字列としてシリアライズされている点があります。これはdoubleの丸め誤差を排除するための設計ですが、Floating pointに変換してから戻し直す実装はバグの温床です。

Rustによる本番級パーサー実装

私は本実装を本番バックテスト基盤に投入しており、シングルコアで1.4 GB/sのスループットを安定して叩き出しています。serde + ゼロコピー読み + ストリーミングの組み合わせです。

use serde::Deserialize;
use std::fs::File;
use std::io::{BufRead, BufReader};
use std::path::Path;

#[derive(Debug, Deserialize)]
pub struct BookSnapshot {
    #[serde(rename = "type")]
    pub kind: String,
    pub exchange: String,
    pub symbol: String,
    pub timestamp: String,
    pub local_timestamp: String,
    pub bids: Vec<[String; 2]>,
    pub asks: Vec<[String; 2]>,
}

pub fn stream_snapshots>(path: P) -> impl Iterator {
    let file = File::open(path).expect("open gzip");
    let reader = BufReader::with_capacity(1 << 20, file); // 1MB buffer

    reader.lines().filter_map(Result::ok).filter_map(|line| {
        let trimmed = line.trim();
        if trimmed.is_empty() { return None; }
        match simd_json::serde::from_str(&mut trimmed.to_string()) {
            Ok(snap) => Some(snap),
            Err(e) => {
                eprintln!("skip malformed: {e}");
                None
            }
        }
    })
}

fn main() {
    let start = std::time::Instant::now();
    let mut count = 0u64;
    let mut best_bid_sum = 0.0f64;
    for snap in stream_snapshots("binance_btcusdt_book_snapshot_25_20240812.json.gz") {
        count += 1;
        if let Some(best) = snap.bids.first() {
            if let Ok(p) = best[0].parse::() {
                best_bid_sum += p;
            }
        }
    }
    let elapsed = start.elapsed().as_secs_f64();
    println!("parsed={count} elapsed={elapsed:.3}s throughput={:.1} MB/s",
        (count as f64 * 320.0) / 1e6 / elapsed);
    println!("avg_best_bid={:.2}", best_bid_sum / count as f64);
}

最適化のポイント

Pythonによる高速化アプローチ

プロトタイピング・ETL・Jupyter連携ではPythonの方が圧倒的に有利です。orjson + プロセスプールで1.1 GB/s(4コア)を観測しています。

import orjson
import gzip
from concurrent.futures import ProcessPoolExecutor, iter_chunks
from typing import Iterator, Dict, Any

def _parse_line(line: bytes) -> Dict[str, Any]:
    # price/amountは文字列のまま保持(精度保全)
    snap = orjson.loads(line)
    snap["mid_price"] = (
        (float(snap["bids"][0][0]) + float(snap["asks"][0][0])) / 2.0
        if snap["bids"] and snap["asks"] else None
    )
    return snap

def parse_tardis_snapshot(path: str, workers: int = 8) -> Iterator[Dict[str, Any]]:
    with gzip.open(path, "rb") as f:
        chunks = iter_chunks(f, chunk_size=8192)  # lines grouping
        # multiprocessing用に bytes iterator を渡す
        with ProcessPoolExecutor(max_workers=workers) as pool:
            for batch in pool.map(_parse_line, _line_iter(f), chunksize=512):
                yield batch

def _line_iter(stream):
    buf = b""
    for block in iter(lambda: stream.read(1 << 20), b""):
        buf += block
        while b"\n" in buf:
            line, buf = buf.split(b"\n", 1)
            if line.strip():
                yield line

利用例

if __name__ == "__main__": import time, sys t0 = time.perf_counter() n = 0 for snap in parse_tardis_snapshot( "binance_btcusdt_book_snapshot_25_20240812.json.gz", workers=8 ): n += 1 if n % 100_000 == 0: print(f"{n:>10} mid={snap['mid_price']}", file=sys.stderr) print(f"throughput={n/(time.perf_counter()-t0):.0f} snapshots/s")

ベンチマーク実測値

関連リソース

関連記事

🔥 HolySheep AIを使ってみる

直接AI APIゲートウェイ。Claude、GPT-5、Gemini、DeepSeekに対応。VPN不要。

👉 無料登録 →

実装コア数形式スループットP99レイテンシ
Rust + simd-json1gzip1.42 GB/s0.71 μs/行
Rust + simd-json4