はじめに
クオンツの研究工程で時間を食うのは、戦略ロジックそのものより「データの整形・結合・欠損処理・特徴量生成」である、ということが多いと考えている。筆者自身、Parquet に貯めた OHLCV やティックデータを pandas / Polrs で読み、結合して、また書き戻す、という往復を日常的にやっている。
その往復を、SQL で・ローカルで・サーバなしで回せるのが DuckDB である。本稿では、IT 寄りの読者を想定しつつ、「システムトレードの研究サイクルで DuckDB が何に役立つのか」を具体的に整理する。結論を先に言えば、筆者の見立てでは DuckDB は 研究・検証・特徴量生成レイヤの第一候補であり、注文・約定・ポジションといった System of Record(正本データ)には PostgreSQL などの伝統的 RDB を使う、というハイブリッド構成が最も素直になることが多い。
DuckDB とは何か(3行で)
- インプロセス(組み込み)で動く、分析特化の SQL データベース。Python/R/Julia の同一プロセス内にライブラリとしてリンクされ、別サーバプロセスを起動しない。
- 列指向ストレージ+ベクトル化実行で、集計・結合・ウィンドウ処理といった OLAP クエリに強い。
- Parquet / CSV / S3 上のファイルを、インポートなしで直接 SQL クエリできる。
位置づけとしては「SQLite の OLAP 版」と考えるとわかりやすい。SQLite が行指向・B-tree で OLTP 寄りなのに対し、DuckDB は列指向・ベクトル化で分析寄り、という棲み分けになっている。
なぜクオンツにとって嬉しいのか
1. 研究サイクルの反復速度が上がる
クオンツの研究は「仮説 → データ抽出 → 特徴量作成 → 検証 → 修正」の往復で成り立っている。ここで DB サーバを立てる、接続情報を管理する、データを抽出して手元に持ってくる、という工程が一回ごとに発生すると、反復の摩擦が地味に効いてくる。
DuckDB はホストプロセス(ノートブックなど)と同じアドレス空間で動くので、
- 接続は
duckdb.connect()の一行 - pandas / Polars / Arrow のオブジェクトを直接参照したり、効率よく受け渡したりできる
- SQL の実行結果はそのまま DataFrame として受け取れる
という具合に、「SQL を書いた方が楽な処理は SQL で、Python の方が楽な処理は Python で」という使い分けが、接続の壁なしにできる。筆者の体感では、これは単なる利便性以上に、探索的なクエリを躊躇なく打てる心理的効果が大きいと考えている。
2. Parquet をそのまま扱える(インポート不要)
クオンツの履歴データは、銘柄×日付のパーティションで Parquet に蓄積する構成が多いと思う。DuckDB はこの Parquet を「テーブルにロードする」のではなく「ファイルそのものをテーブルとして SQL で読む」ことができる。
SELECT symbol, AVG(close) AS avg_close
FROM read_parquet('s3://my-bucket/bars/date=2024-*/bars.parquet')
WHERE ts >= '2024-06-01'
GROUP BY symbol;
これはデータレイクアーキテクチャと相性が良い。特に、
- 必要な列だけを読むので I/O が節約できる
- パーティション(
date=*のようなグロブ)でファイル単位の枝刈りが効く - S3 も
httpfs拡張を入れればローカルファイルと同じ構文で読める
といった点が、「Parquet に貯めてある市場データを、ちょっと見たい」というニーズに刺さる。
3. 時系列データの時刻ずれ結合(ASOF JOIN)
金融データで頻出の処理に、「約定時刻に最も近い直前の気配(bid/ask)を引く」「指標発表時点での直前の株価を引く」というものがある。通常の SQL では LATERAL JOIN + ORDER BY + LIMIT 1 のような書き方になり、意図が読みづらくなりがちだ。
DuckDB は ASOF JOIN を標準機能として持っており、これが1行で書ける。クオンツ実務で特に刺さる典型例を 3 つ挙げる。
例1: スリッページ分析(約定ログに直前 bid/ask を付ける)
-- 約定価格と、その時点の最良気配とのズレを見る
SELECT
t.symbol,
t.ts AS trade_ts,
t.price,
t.side,
q.bid,
q.ask,
CASE
WHEN t.side = 'BUY' THEN t.price - q.ask
WHEN t.side = 'SELL' THEN q.bid - t.price
END AS slippage
FROM trades t
ASOF JOIN quotes q
ON t.symbol = q.symbol AND t.ts >= q.ts;
例2: イベントスタディ(イベント時点の直前価格を引く)
-- 決算発表などのイベント時点での直前価格を基準にリターンを計算
SELECT
e.symbol,
e.event_ts,
e.event_type,
p.close AS price_before_event
FROM events e
ASOF JOIN prices p
ON e.symbol = p.symbol AND e.event_ts >= p.ts;
例3: 日次クロスセクションのファクター値生成
-- 各銘柄について、日次の最新ファンダメンタル値を引いてファクター化
COPY (
SELECT
d.symbol,
d.date,
d.close,
f.pbr,
f.roe,
f.debt_equity
FROM daily_bars d
ASOF JOIN fundamentals f
ON d.symbol = f.symbol AND d.date >= f.report_date
) TO 'factors_daily.parquet' (FORMAT PARQUET);
ティック・板・約定・マクロ指標といった「時刻が完全には揃わないデータ同士の結合」は、システムトレードの特徴量エンジニアリングで避けて通れない工程なので、この一機能だけでも DuckDB を触る価値はあると考えている。
4. メモリを超えるデータもスピルで扱える
単一ノードで巨大データを処理する設計になっており、メモリを超える処理はディスクへスピル(一時書き出し)される。公式は TPC-H SF=100,000(100TB 相当)規模の検証例を示しているが、これは 1.5TB RAM・192 コア・45TB NVMe といったハイエンド EC2 上での結果である点には注意が要る。
実務感覚としては、ノート PC でも数十 GB、条件次第で数百 GB クラスの Parquet を扱えることがある、くらいに期待値を置くのが現実的だと考えている。
ただし後述するように、ウィンドウ関数などはメモリを食いやすいので、何でも無限に処理できるわけではない点には注意が必要だ。
典型的なアーキテクチャ像
筆者が現実的だと考えている構成は以下のようなものである。
この図で示したいことは、レイヤを 3 段に分けて役割を明確化する、という点に尽きる。
1段目:データソース層(OLTP と 市場データ/オルタナ)
左側は PostgreSQL などの OLTP で、注文・約定・ポジションといった正本データを保持する役割を担う。右側はベンダー API や CSV として入ってくる市場データ・オルタナデータで、こちらは追記型で増えていくイメージになる。この 2 つは性格がまったく違うので、最初から同じストレージに置かないのが素直だと考えている。
2段目:Parquet データレイク層
OLTP からのバッチ/CDC と、市場データの整形結果を、Parquet 形式でローカル SSD または S3 に集約する。ここがハブになる。Parquet にしておけば、列指向の恩恵(必要列だけの読み込み、圧縮効率)を受けられるし、複数の研究者が同じファイルを同時に読むこともできる。
3段目:DuckDB による分析層
Parquet レイクを入力として、DuckDB が研究・特徴量生成・検証を担う。図の中で強調しているのは DuckDB の 4 つの武器である。
- ASOF JOIN による時系列結合:時刻が完全には一致しない気配と約定のようなデータを、SQL で簡潔に結合できる
- スケールアップ設計:メモリを超える巨大データも、ディスクへのスピル機能により単一ノードで処理できる
- Parquet のネイティブ処理:インポート不要で Parquet を直接読み込み、Python / R から連携して呼び出せる
- インプロセス実行:ネットワークを介さず高速にクエリを実行できる
出力層
DuckDB の成果物は、特徴量 Parquet(再利用可能な形で保存)、ダッシュボード(可視化・レポート)、バックテスト入力(戦略検証への受け渡し)という 3 系統に分かれる。特徴量 Parquet として書き戻すことで、次の研究サイクルの入力に回せる、という循環構造になっているのがポイントだ。
ポイントとしての「役割分離」
この構成の本質は、正本データ(OLTP)と分析(OLAP)を物理的に分離することにある。DuckDB は書き込みが同時に 1 プロセスしかできないという制約があるため、多数のサービスが並行して書き込む OLTP 用途には向かない。逆に言えば、Parquet に落としてしまえば、複数の研究者が各自の DuckDB から並列に読むことは問題なくできる(書き込みはパイプライン側で単一化する)。
図の右下にまとめたように、DB の棲み分けは以下のように整理できる。
| DB種類 | 主な用途 | クオンツ視点での長所 |
|---|---|---|
| DuckDB | リサーチ・特徴量生成 | インプロセスで軽量。ASOF JOIN が強力 |
| PostgreSQL | 注文・ポジション管理 | 同時実行書き込みと権限管理に優れる |
| MongoDB | 柔軟なドキュメント管理 | スキーマ変更に強く、水平スケールが可能 |
Python からの実践例
S3 上の Parquet からリターンを計算して特徴量を書き出す
import duckdb
con = duckdb.connect("research.duckdb")
# S3 拡張(最近のバージョンでは自動ロードされることも多いが、明示しておく)
con.execute("INSTALL httpfs; LOAD httpfs;")
# 資格情報: 実運用では IAM ロールや credential_chain を優先する
# (固定キーの直書きは履歴やディスクに平文で残るリスクがあるため避ける)
con.execute("""
CREATE OR REPLACE SECRET s3_cred (
TYPE s3,
PROVIDER credential_chain,
REGION 'ap-northeast-1'
);
""")
# 同居プロセスとしての安全策(リソース上限)
con.execute("SET threads = 8;")
con.execute("SET memory_limit = '8GB';")
con.execute("SET temp_directory = './duckdb_tmp';")
# 対数リターンを計算してテーブル化
con.execute("""
CREATE OR REPLACE TABLE features AS
WITH bars AS (
SELECT symbol, ts, close
FROM read_parquet('s3://your-bucket/bars/date=*/bars.parquet')
WHERE ts >= TIMESTAMP '2024-01-01'
)
SELECT
symbol,
ts,
close,
log(close) - log(lag(close) OVER (PARTITION BY symbol ORDER BY ts)) AS log_ret
FROM bars;
""")
# Parquet に書き出し(他の研究サイクルから再利用)
con.execute("""
COPY (SELECT * FROM features)
TO 'features.parquet' (FORMAT PARQUET);
""")
# そのまま pandas で受け取ることもできる
df = con.execute("SELECT * FROM features WHERE symbol = 'USDJPY'").df()
この例で使っている機能(S3 直接読み込み、Secrets Manager、リソース上限、Parquet 書き出し)はすべて DuckDB の標準機能・標準拡張でまかなえる。
pandas / Polars との相互運用
DuckDB の嬉しい点の一つは、Python のオブジェクトをそのままテーブルとして SQL で参照できることだ。
import pandas as pd
import duckdb
df = pd.read_parquet("bars.parquet")
# df をそのまま SQL でクエリできる(コピーされない)
result = duckdb.sql("""
SELECT symbol, AVG(close) AS avg_close
FROM df
GROUP BY symbol
""").df()
pandas と SQL のどちらで書いても動く局面が増えるので、「ウィンドウ関数は SQL、行単位の変換は pandas」といった使い分けがしやすい。
他ツール・他 DB との比較(クオンツ視点での棲み分け)
まず押さえるべきは pandas / Polars との関係
クオンツ読者が日常的に比較する相手は、おそらく MongoDB ではなく pandas / Polars だろう。DuckDB を「DB としてすごい」と捉えるより、「pandas / Polars で回している研究フローのどこを置き換えると得か」という視点で見た方が実務的に腹落ちしやすいと考えている。
| ツール | 強み | クオンツでの使い所 |
|---|---|---|
| DuckDB | JOIN / ウィンドウ / 集計 / Parquet レイクの横断クエリに強い。SQL で宣言的に書ける | 複数 Parquet をまたぐ集計、ASOF JOIN、特徴量生成 |
| Polars | DataFrame 式ベースの高速変換、lazy 実行、式合成の明快さ | 行単位の変換が多い前処理、パイプライン的な変換の連結 |
| pandas | 豊富な周辺ライブラリ、既存資産、軽い探索での手軽さ | アドホックな可視化、小規模データ、scikit-learn 等との連携 |
筆者の使い分けの感覚としては、**「複数ファイルをまたぐ集計・結合は DuckDB、行単位の変換連結は Polars、最終的な可視化や ML 連携は pandas」**という住み分けになることが多い。これらは排他ではなく、DuckDB の結果を .pl() や .df() でそのまま受け渡せるので、混在運用しやすい。
サーバ型 DB との棲み分け
正本データの管理は、やはりサーバ型の DB に任せるのが素直だと考えている。
| DB | 役割 | クオンツでの使い所 |
|---|---|---|
| PostgreSQL / MySQL | サーバ型 OLTP | 注文・約定・ポジション等の正本データ、多ユーザ運用 |
| SQLite | 組み込み OLTP | アプリ同梱の小規模データ(設定・状態など) |
| MongoDB | ドキュメント指向 NoSQL | スキーマレスなログ・オルタナデータ、スケールアウト前提の運用 |
判断基準をざっくり整理すると以下のようになる。
- 書き込みが同時多発するか? → Yes なら PostgreSQL / MySQL / MongoDB、No なら DuckDB で足りる
- 分析クエリ(集計・結合・ウィンドウ)が中心か? → Yes なら DuckDB、No(行単位更新中心)なら OLTP 系
- 分散スケールアウトが必要か? → 必要なら MongoDB や専用 DWH、単一ノードで足りるなら DuckDB
個人〜小チームのクオンツ研究の多くは単一ノードで収まる範囲にあり、DuckDB で足りることが多いと考えている。逆に、取引執行システムのバックエンドのように「多数クライアントが同時に小さな更新を書き込む」ケースは最初から DuckDB の想定外なので、そこは PostgreSQL などを素直に使うのが良い。
注意点・落とし穴
DuckDB は便利だが、以下の点は事前に理解しておいた方が良いと思う。
1. 同時書き込みは 1 プロセスのみ(スレッドの話ではない)
読み書き可能な DB ファイルに対して同時接続できるのは 1 プロセスに限られる(複数プロセスから同時に開けるのは読み取り専用の場合のみ)。これはインプロセス DB ゆえの制約で、回避策はパイプライン側で書き込みを単一化することになる。
なお、これは同一プロセス内の複数スレッド利用とは別の話である。制約の中心は複数プロセス間の同時書き込みにあり、同一プロセス内であれば複数スレッドから insert / read を行うことは問題ない。「DuckDB は 1 スレッドしか使えない」という誤解とは別物なので注意したい。
2. ウィンドウ関数はメモリを食いやすい
ウィンドウ関数は「入力全体をバッファするブロッキング演算子」として実装されており、大きなパーティションに対して使うとメモリを大量に消費することがある。スピルは効くが、I/O コストが跳ね返ってくるので、パーティション設計や段階的集計で緩和する工夫が要ることが多い。
3. セキュリティはプロセス権限が前提
DuckDB の SQL は「実行ユーザの権限で何でもできる」ものとして扱われる(bash や Python と同じ感覚)。拡張のロードやファイルアクセスも SQL から可能なので、未信頼の SQL を実行する前提のサービスに DuckDB を直接外に晒すのは避けた方が良い。研究用途で自分のデータを触る分には問題にならないが、サービス化するならコンテナ隔離や外部アクセス無効化などの防御線を張る必要がある。
4. 分析特化で、OLTP の置き換えではない
何度か書いているが、DuckDB は PostgreSQL の代わりにはならない。小規模更新が高頻度で発生する正本データは OLTP 系の DB に任せ、DuckDB は「そこから流れ出てくる Parquet を分析する」役割に徹させるのが素直だと考えている。
まとめ
クオンツ・システムトレーダーにとって DuckDB が価値を持つのは、大雑把に言えば次の 3 点に集約できると考えている。
- 研究サイクルの摩擦を減らす:インプロセスで動くので、接続・データ移動・環境構築のオーバーヘッドがほぼない
- Parquet をそのまま扱える:データレイク構成と相性が良く、インポート工程を省略できる
- 時系列 SQL に強い:ASOF JOIN をはじめ、時刻ずれのあるイベントデータの結合が簡潔に書ける
一方で、同時書き込み制約、ウィンドウ関数のメモリ消費、セキュリティモデルといった点は事前に理解しておく必要がある。これらは「DuckDB がダメ」という話ではなく、「DuckDB がどこを主戦場にしているか」の裏返しと捉えるのが正確だと思う。
個人的な結論としては、正本は RDB、分析は DuckDB というハイブリッド構成が、少なくとも個人〜小チームのクオンツ研究では最もコスパが良いと考えている。まずはローカルの Parquet を一枚、duckdb.connect() で開いてみるところから始めてみるのが早いと思う。
補足: 本稿の内容は筆者の実務経験と公式ドキュメントに基づくが、性能特性はワークロード・データ量・ハードウェアに強く依存するため、実際の採用前には代表的なクエリで検証することを推奨する。
