← 기사 목록으로
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_trunc도, array_agg 곡예도, 명시적 GROUP BY도 필요 없습니다.

사용 가능한 간격은 유연합니다: 1s, 5s, 15m, 1h, 1d, 7d — 시간 단위의 임의 조합. 잠들지 않는 암호화폐 시장을 위해 타임존을 인식한 캘린더 경계에 맞출 수 있습니다:

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이 이를 한 줄로 해결합니다:

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초 이내에 매칭되는 호가가 없으면 오래된 데이터 대신 NULL을 반환합니다. 정확한 TCA와 데이터 신선도가 중요한 모든 분석에 필수적입니다.

보너스로 TOLERANCE는 쿼리 성능을 크게 향상시킬 수 있습니다. 이것 없이는 엔진이 매치를 찾기 위해 호가 테이블을 한참 과거까지 스캔할 수 있습니다. TOLERANCE가 있으면 레코드가 너무 오래되어 자격이 없을 때 후방 스캔을 조기 종료할 수 있습니다.

LT JOIN과 SPLICE JOIN

알아둘 가치가 있는 두 가지 변형: 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가지 체결 후 분석 패턴이 포함되어 있습니다: 슬리피지 분석(체결 vs 미드 가격), 마크아웃 곡선, 구현 비용(Perold 분해), 스마트 오더 라우팅을 위한 거래소 스코어링, 플로우 독성 감지(VPIN). 각 패턴은 실제 데이터를 사용한 라이브 데모에서 실행됩니다.

WINDOW JOIN: 이벤트와 주변 데이터 상관

WINDOW JOIN은 QuestDB 9.3에서 도입되었습니다. 프라이머리 테이블의 각 행을 다른 테이블의 시간 윈도우 행과 조인하고, 매칭된 행에 대해 집계를 계산할 수 있습니다.

거래 후 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를 사용하는 대신 데이터베이스 내에서 이러한 지표를 계산하는지 궁금할 수 있습니다.

세 가지 이유가 있습니다. 첫째, 데이터 지역성 — 테라바이트의 틱 데이터를 네트워크를 통해 이동시켜 롤링 평균을 계산하는 것은 낭비입니다. 계산은 데이터가 있는 곳에서 해야 합니다. 둘째, 병렬성 — 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 트레이딩 통찰력, 시장 분석 및 플랫폼 업데이트를 받아보세요.

귀하의 개인정보를 존중합니다. 언제든지 구독을 취소할 수 있습니다.