← 記事一覧に戻る
March 4, 2026
読了時間: 5分

アルゴリズム取引のためのQuestDB:ゲームを変えるSQL拡張機能

アルゴリズム取引のためのQuestDB:ゲームを変えるSQL拡張機能
#QuestDB
#SQL
#ASOF JOIN
#SAMPLE BY
#time-series
#algorithmic trading

パート2/3 — RU · ZH でも利用可能

免責事項:この記事で提供される情報は教育および情報提供を目的としたものであり、金融、投資、または取引に関するアドバイスを構成するものではありません。暗号通貨取引には重大な損失リスクが伴います。


QuestDBシリーズのパート2へようこそ。パート1では、3層ストレージアーキテクチャとスキーマ設計原則を扱いました。今回は真に差別化する機能セット — QuestDBをトレーダーのために、トレーダーによって設計されたかのように感じさせるSQL拡張機能 — に入ります。

標準SQLは1970年代にリレーショナルデータのために作成されました。時間をファーストクラスの概念として何も知りません。PostgreSQLやMySQLでのすべての時系列操作は、冗長な回避策 — ウィンドウ関数、ラテラルジョイン、3段重ねのCTE — を必要とします。QuestDBの拡張機能は、これらの複数段落のクエリを単一の表現力豊かな文に圧縮します。

実際の取引例でそれぞれを見ていきましょう。

SAMPLE BY:生取引をOHLCVローソク足に集約

SAMPLE BY:ネイティブな時間バケット集約

すべての取引システムが他のどのクエリよりも多く実行するクエリがあるとすれば、それはOHLCV集約 — 生取引をローソク足データに変換すること — です。標準SQLでは以下のように書きます:

-- Standard SQL: verbose and slow
SELECT
  date_trunc('minute', timestamp) AS bucket,
  symbol,
  (array_agg(price ORDER BY timestamp))[1] AS open,
  max(price) AS high,
  min(price) AS low,
  (array_agg(price ORDER BY timestamp DESC))[1] AS close,
  sum(quantity) AS volume
FROM trades
WHERE timestamp >= now() - interval '1 hour'
GROUP BY bucket, symbol
ORDER BY bucket;

QuestDBでは以下のようになります:

SELECT timestamp, symbol,
  first(price) AS open,
  max(price) AS high,
  min(price) AS low,
  last(price) AS close,
  sum(quantity) AS volume
FROM trades
WHERE timestamp IN today()
SAMPLE BY 1m;

これだけです。SAMPLE BY 1mはQuestDBにデータを1分間隔でバケット化するよう指示し、first() / last()は各バケット内の時間順序を尊重するネイティブ集約関数です。date_truncarray_aggの曲芸も明示的なGROUP BYも不要です。

利用可能なインターバルは柔軟です:1s5s15m1h1d7d — 時間単位の任意の組み合わせ。眠らない暗号通貨市場では、タイムゾーン対応でカレンダー境界に揃えることができます:

SAMPLE BY 1d ALIGN TO CALENDAR TIME ZONE 'UTC';

FILL:データのギャップ処理

実際の市場にはギャップがあります — 流動性の低いペアは数分や数時間取引されないことがあります。SAMPLE BYはいくつかのFILL戦略をサポートしています:

-- Fill with previous value (forward fill — common in finance)
SAMPLE BY 15m FILL(PREV);

-- Fill with linear interpolation
SAMPLE BY 15m FILL(LINEAR);

-- Fill with a constant
SAMPLE BY 15m FILL(0);

-- No fill — return NULL (default)
SAMPLE BY 15m FILL(NONE);

FILL(PREV)は取引ダッシュボードの定番です — 15分バケット内で取引がなければ、最後に判明した価格を繰り越します。FILL(LINEAR)はファンディングレートや金利のような連続的なシグナルにより適しています。

ASOF JOIN:取引と気配値の時間的アライメント

ASOF JOIN:マーケットデータアライメントの「意図どおりに動く」

これはQuestDBの至宝であり、マーケットデータを扱ったことがある方ならすぐにその理由を理解するでしょう。

根本的な問題:あるテーブルに取引があり、別のテーブルに気配値(bid/ask)があります。各取引が約定した正確な瞬間における有効な気配値を知りたい。通常のデータベースでは、タイムスタンプが完全に一致することはほぼありません — 12:00:00.123の取引は12:00:00.201ではなく12:00:00.098の気配値にマッチする必要があります。

ASOF JOINはこれを1行で解決します:

SELECT trades.*, quotes.bid, quotes.ask
FROM trades
ASOF JOIN quotes ON (symbol);

tradesの各行に対して、QuestDBはquotesから取引のタイムスタンプ以下の最新のタイムスタンプを持つ行を見つけ、symbol列でマッチングします。相関サブクエリなし。ウィンドウ関数なし。アプリケーションレベルのロジックなし。

これはTransaction Cost Analysis(TCA)の基盤です — 約定時点の有効な市場と約定価格を比較します。PostgreSQLでは同等の処理に各行に対するORDER BYとLIMIT 1を伴うLATERAL JOINが必要であり、大規模データセットでは桁違いに遅くなります。

TOLERANCE:古いデータのJOIN防止

おもちゃの実装と本番システムを分ける微妙な点があります。薄商いの資産の気配値が取引時点で5分前のものだったらどうでしょうか?変動の激しい市場では、5分前の気配値は本質的にゴミです。デフォルトのASOF JOINはそれでも使用します — どれだけ古くても最新のマッチを見つけます。

QuestDBのTOLERANCE句がこれを修正します:

SELECT trades.*, quotes.bid, quotes.ask
FROM trades
ASOF JOIN quotes ON (symbol)
TOLERANCE 1s;

取引から1秒以内にマッチする気配値がない場合、JOINは古いデータの代わりにNULLを返します。これは正確なTCAや、データの鮮度が重要なあらゆる分析に不可欠です。

おまけとして、TOLERANCEはクエリパフォーマンスを大幅に向上させることができます。これがないと、エンジンはマッチを探すために気配値テーブルをはるか過去までスキャンする可能性があります。TOLERANCEがあれば、レコードが古すぎて対象外になった時点で後方スキャンを早期終了できます。

LT JOINとSPLICE JOIN

知っておくべき2つのバリエーション:LT JOINはASOF JOINに似ていますが、タイムスタンプの厳密に前にマッチします(等しいことはない)。これはバックテストで先読みバイアスを避ける必要がある場合に便利です — 同じマイクロ秒に到着したものではなく、取引に存在した気配値が欲しいのです。

SPLICE JOINは双方向の完全なASOFです:左テーブルの各レコードに対して有効な右テーブルのレコードを見つけ、右テーブルの各レコードに対して有効な左テーブルのレコードを見つけます。結果は両方のデータソースのマージされたインターリーブタイムラインです。これは複数のデータストリームから統合イベントタイムラインを作成する場合に特に有用です。

HORIZON JOIN:単一クエリでの約定後分析

QuestDB 9.3.3で導入されたHORIZON JOINは、マークアウト分析 — 約定品質評価と市場マイクロストラクチャ研究の基盤 — のために特別に構築されています。

答える質問:「取引が約定された後、次のN秒間で価格はどのように推移したか?」従来は、セルフジョイン、複数のASOFクエリにわたるUNION ALL、またはアプリケーションコードへのロジック移行が必要でした。HORIZON JOINがすべてを圧縮します:

SELECT h.offset / 1000000 AS offset_sec,
  avg(mid.price - fill.price) AS avg_markout
FROM fills
HORIZON JOIN mid_prices ON (symbol)
RANGE BETWEEN 0 AND 60s STEP 1s;

これは各約定後1秒間隔で最大60秒までの平均価格変動を計算します。エンジンが時間オフセット計算、各ホライズンポイントでのASOFマッチング、集約を処理します — すべて単一のパスで。

不均一なホライズンの場合、またはイベントを見る場合はLIST構文を使用します:

HORIZON JOIN mid_prices ON (symbol)
LIST (-5s, -1s, 0, 1s, 5s, 30s, 60s);

これにより、約定前後のマークアウト曲線が得られます。取引所、戦略、または注文サイズによるフィルタリングと組み合わせることで、データベース内で完全な約定品質フレームワークを構築できます。

QuestDBのクックブックには、HORIZON JOINに基づく5つの約定後分析パターンが含まれています:スリッページ分析(約定対ミッド価格)、マークアウト曲線、実施コスト(Perold分解)、スマートオーダールーティングのための取引所スコアリング、フロー毒性検出(VPIN)。各パターンは実データを使用したライブデモで動作します。

WINDOW JOIN:イベントと周辺データの相関

WINDOW JOINはQuestDB 9.3で導入されました。プライマリテーブルの各行を別のテーブルの時間ウィンドウの行とJOINし、マッチング行に対して集約を計算できます。

取引後10秒間の平均bidとask価格と各取引を相関させたいFX取引シナリオを考えてみましょう:

SELECT
  trades.timestamp,
  trades.symbol,
  trades.price,
  avg(quotes.bid) AS avg_bid,
  avg(quotes.ask) AS avg_ask
FROM trades
WINDOW JOIN quotes ON (symbol)
RANGE BETWEEN 0 AND 10s FOLLOWING
INCLUDE PREVAILING;

INCLUDE PREVAILING句により、ウィンドウ境界で正確なマッチがなくても最新の価格が得られます。これにより、短期ホライズン分析が通常必要とする何ページものサブクエリが不要になります。

取引での使用例:各約定前後の平均市場状況の計算、大口注文前の時間ウィンドウでの異常な価格行動の検出、IoT/インフラストラクチャイベント(ネットワークレイテンシスパイク)と約定品質の相関。

LATEST ON:即時の現在状態

一見シンプルだが非常に便利な拡張機能。LATEST ONはパーティショニング列の各値に対して最後の行を返します:

SELECT * FROM trades
LATEST ON timestamp PARTITION BY symbol, side
ORDER BY timestamp DESC;

これにより、各(symbol, side)ペアの最後の取引 — 本質的にリアルタイムの「現在の状態」スナップショット — が得られます。従来のデータベースでは、相関サブクエリまたはROW_NUMBER()を持つウィンドウ関数が必要です。

数百のシンボルにわたる最新価格を表示する取引ダッシュボードでは、LATEST ONは瞬時です。マテリアライズドビュー(パート3で扱います)と組み合わせると、サブミリ秒のポートフォリオスナップショットの基盤になります。

TWAP:ネイティブ時間加重平均

twap(price, timestamp)集約はQuestDB 9.3.3で追加されました。ボリュームで重み付けするVWAPとは異なり、TWAPは期間で重み付けします — 各価格は次の観察まで保持され、結果はステップ関数の下の面積を総時間で割ったものです。

SELECT symbol,
  twap(price, timestamp) AS twap_value,
  vwap(price, quantity) AS vwap_value
FROM trades
WHERE timestamp IN today()
SAMPLE BY 1h;

TWAPはアルゴリズム注文の標準的な約定ベンチマークです。並列GROUP BYとすべてのFILLモードを持つSAMPLE BYをサポートするネイティブ集約として持つことは、クライアント側の統合が不要 — 計算はすべてクエリエンジン内で実行される — ことを意味します。

ウィンドウ関数:テクニカル分析の基盤

QuestDBは標準SQLウィンドウ関数をサポートしており、テクニカルインジケーター計算のバックボーンを形成します:

-- Bollinger Bands using materialized OHLC view
WITH stats AS (
  SELECT timestamp, close,
    AVG(close) OVER (
      ORDER BY timestamp
      ROWS BETWEEN 19 PRECEDING AND CURRENT ROW
    ) AS sma20,
    AVG(close * close) OVER (
      ORDER BY timestamp
      ROWS BETWEEN 19 PRECEDING AND CURRENT ROW
    ) AS avg_close_sq
  FROM trades_OHLC_15m
  WHERE timestamp BETWEEN dateadd('h', -24, now()) AND now()
    AND symbol = 'BTC-USDT'
)
SELECT timestamp,
  sma20,
  sma20 + 2 * sqrt(avg_close_sq - sma20 * sma20) AS upper_band,
  sma20 - 2 * sqrt(avg_close_sq - sma20 * sma20) AS lower_band
FROM stats;

QuestDB 9.3.3ではSQL標準のWINDOW句も導入されました — ウィンドウ仕様を一度定義し、複数の関数で名前で参照します。すべての式で同じPARTITION BYとORDER BYを繰り返す必要はありません:

SELECT timestamp, symbol,
  avg(price) OVER w AS avg_price,
  stddev(price) OVER w AS std_price,
  min(price) OVER w AS min_price,
  max(price) OVER w AS max_price
FROM trades
WINDOW w AS (PARTITION BY symbol ORDER BY timestamp ROWS BETWEEN 99 PRECEDING AND CURRENT ROW);

よりクリーンなクエリ、コピペの削減、同じ実行パフォーマンス。

実際のクエリパターン

本番の取引システムで頻繁に登場するパターンを紹介します:

クロスアセット相関

-- Rolling hourly and daily correlation between ETH and another asset
WITH data AS (
  SELECT ETHUSD.timestamp,
    corr(ETHUSD.price, asset.price) AS corr
  FROM ETHUSD
  ASOF JOIN asset
  SAMPLE BY 1m
)
SELECT timestamp,
  avg(corr) OVER (ORDER BY timestamp
    RANGE BETWEEN 1 HOUR PRECEDING AND CURRENT ROW) AS hourly_corr,
  avg(corr) OVER (ORDER BY timestamp
    RANGE BETWEEN 24 HOUR PRECEDING AND CURRENT ROW) AS daily_corr
FROM data;

全シンボルRSI

-- 14-day RSI for all USDT pairs
WITH gains_losses AS (
  SELECT timestamp, symbol,
    CASE WHEN close > lag(close) OVER (PARTITION BY symbol ORDER BY timestamp)
      THEN close - lag(close) OVER (PARTITION BY symbol ORDER BY timestamp)
      ELSE 0 END AS gain,
    CASE WHEN close < lag(close) OVER (PARTITION BY symbol ORDER BY timestamp)
      THEN lag(close) OVER (PARTITION BY symbol ORDER BY timestamp) - close
      ELSE 0 END AS loss
  FROM trades_latest_1d
  WHERE symbol LIKE '%-USDT'
)
SELECT timestamp, symbol,
  100 - 100 / (1 + avg_gain / NULLIF(avg_loss, 0)) AS rsi
FROM (
  SELECT timestamp, symbol,
    AVG(gain) OVER (PARTITION BY symbol ORDER BY timestamp
      ROWS BETWEEN 13 PRECEDING AND CURRENT ROW) AS avg_gain,
    AVG(loss) OVER (PARTITION BY symbol ORDER BY timestamp
      ROWS BETWEEN 13 PRECEDING AND CURRENT ROW) AS avg_loss
  FROM gains_losses
);

ATR(Average True Range)

-- 14-period ATR from 15-minute OHLC bars
WITH tr AS (
  SELECT timestamp, symbol,
    GREATEST(
      high - low,
      ABS(high - lag(close) OVER (PARTITION BY symbol ORDER BY timestamp)),
      ABS(low - lag(close) OVER (PARTITION BY symbol ORDER BY timestamp))
    ) AS true_range
  FROM trades_OHLC_15m
)
SELECT timestamp, symbol,
  AVG(true_range) OVER (PARTITION BY symbol ORDER BY timestamp
    ROWS BETWEEN 13 PRECEDING AND CURRENT ROW) AS atr_14
FROM tr;

カスタムコードに対するSQLの優位性

なぜPythonで生データを取得してta-libやpandasを使う代わりに、データベース内でこれらのインジケーターを計算するのかと疑問に思うかもしれません。

3つの理由があります。第一に、データの局所性 — テラバイトのティックデータをネットワーク経由で転送してローリング平均を計算するのは無駄です。計算はデータのある場所で行うべきです。第二に、並列性 — QuestDBのJITコンパイルを伴うベクトル化エンジンはSIMD命令で複数のコアにわたってこれらのクエリを処理でき、多くの場合シングルスレッドのPythonコードより高速です。第三に、一貫性 — 複数のダッシュボード、戦略、監視システムがすべて同じインジケーターを必要とする場合、データベースに単一の信頼できるソースを持つことで同期バグが排除されます。

とはいえ、QuestDBは分析スタック全体を置き換えようとしているわけではありません。Parquetの相互運用性により、バッチワークロードではデータベースを経由せずにMLパイプラインに直接過去データを取り込むことができます。

パート3の予告

最終パートでは、リアルタイムOHLCのためのマテリアライズドビュー(複数タイムフレームのカスケードビューを含む)、ネイティブ板情報分析のための2次元配列、QuestDBを基盤としたアルゴリズム取引プラットフォームの完全なリファレンスアーキテクチャをカバーします。

Citation

@software{soloviov2025questdb_algotrading_p2,
  author = {Soloviov, Eugen},
  title = {QuestDB for Algorithmic Trading: SQL Extensions That Change the Game},
  year = {2025},
  url = {https://marketmaker.cc/en/blog/post/questdb-algotrading-sql},
  version = {0.1.0},
  description = {Deep dive into QuestDB's time-series SQL extensions: SAMPLE BY, ASOF JOIN, HORIZON JOIN, WINDOW JOIN, LATEST ON, and real-world trading query patterns.}
}
blog.disclaimer

MarketMaker.cc Team

クオンツ・リサーチ&戦略

Telegramで議論する
Newsletter

市場の先を行く

ニュースレターを購読して、独占的なAI取引の洞察、市場分析、プラットフォームの更新情報を受け取りましょう。

プライバシーを尊重します。いつでも配信停止可能です。