← Quay lại danh sách bài viết
March 4, 2026
5 phút đọc

QuestDB cho Giao dịch Thuật toán: Các Mở rộng SQL Thay đổi Cuộc chơi

QuestDB cho Giao dịch Thuật toán: Các Mở rộng SQL Thay đổi Cuộc chơi
#QuestDB
#SQL
#ASOF JOIN
#SAMPLE BY
#chuỗi thời gian
#giao dịch thuật toán

Phần 2 trong 3 — cũng có tại RU · ZH

Tuyên bố miễn trách nhiệm: Thông tin trong bài viết này chỉ nhằm mục đích giáo dục và cung cấp thông tin, không cấu thành lời khuyên tài chính, đầu tư hay giao dịch. Giao dịch tiền điện tử liên quan đến rủi ro thua lỗ đáng kể.


Chào mừng bạn đến với Phần 2 của chuỗi bài về QuestDB. Trong Phần 1, chúng ta đã tìm hiểu kiến trúc lưu trữ ba tầng và các nguyên tắc thiết kế schema. Bây giờ chúng ta sẽ đi sâu vào bộ tính năng thực sự khác biệt — các mở rộng SQL khiến QuestDB cảm giác như được thiết kế bởi các trader, dành cho các trader.

SQL chuẩn được tạo ra vào những năm 1970 cho dữ liệu quan hệ. Nó không có khái niệm về thời gian như một yếu tố hàng đầu. Mọi thao tác chuỗi thời gian trong PostgreSQL hay MySQL đều đòi hỏi những cách giải quyết dài dòng — window functions, lateral joins, CTEs xếp chồng ba tầng. Các mở rộng của QuestDB đơn giản hóa những truy vấn dài dòng này thành các câu lệnh đơn, súc tích.

Hãy cùng đi qua từng tính năng với các ví dụ giao dịch thực tế.

SAMPLE BY: giao dịch thô được tổng hợp thành nến OHLCV

SAMPLE BY: Tổng hợp Theo Khoảng Thời gian Gốc

Nếu có một truy vấn mà mọi hệ thống giao dịch chạy nhiều hơn bất kỳ truy vấn nào khác, đó là tổng hợp OHLCV — biến đổi các giao dịch thô thành dữ liệu nến. Trong SQL chuẩn, bạn sẽ viết như sau:

-- SQL chuẩn: dài dòng và chậm
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;

Trong QuestDB, điều này trở thành:

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;

Chỉ vậy thôi. SAMPLE BY 1m yêu cầu QuestDB phân chia dữ liệu thành các khoảng 1 phút, và first() / last() là các hàm tổng hợp gốc tôn trọng thứ tự thời gian trong mỗi khoảng. Không cần date_trunc, không cần array_agg phức tạp, không cần GROUP BY tường minh.

Các khoảng thời gian có sẵn rất linh hoạt: 1s, 5s, 15m, 1h, 1d, 7d — bất kỳ tổ hợp đơn vị thời gian nào. Đối với các thị trường tiền điện tử không bao giờ ngủ, bạn có thể căn chỉnh theo ranh giới lịch với hỗ trợ múi giờ:

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

FILL: Xử lý Khoảng Trống trong Dữ liệu

Các thị trường thực tế có khoảng trống — các cặp thanh khoản thấp có thể không giao dịch trong vài phút hoặc vài giờ. SAMPLE BY hỗ trợ nhiều chiến lược FILL:

-- Điền bằng giá trị trước (forward fill — phổ biến trong tài chính)
SAMPLE BY 15m FILL(PREV);

-- Điền bằng nội suy tuyến tính
SAMPLE BY 15m FILL(LINEAR);

-- Điền bằng hằng số
SAMPLE BY 15m FILL(0);

-- Không điền — trả về NULL (mặc định)
SAMPLE BY 15m FILL(NONE);

FILL(PREV) là lựa chọn cơ bản cho các bảng điều khiển giao dịch — nếu không có giao dịch nào trong khoảng 15 phút, giữ nguyên giá cuối cùng đã biết. FILL(LINEAR) phù hợp hơn cho các tín hiệu liên tục như tỷ lệ tài trợ hay lãi suất.

ASOF JOIN căn chỉnh thời gian của giao dịch và báo giá

ASOF JOIN: "Hiểu Ý Tôi Muốn" trong Căn chỉnh Dữ liệu Thị trường

Đây là viên ngọc quý của QuestDB, và nếu bạn đã làm việc với dữ liệu thị trường, bạn sẽ hiểu ngay lý do tại sao.

Vấn đề cơ bản: bạn có các giao dịch trong một bảng và báo giá (bid/ask) trong bảng khác. Bạn muốn biết báo giá hiện hành là bao nhiêu vào chính xác thời điểm mỗi giao dịch được thực hiện. Trong một cơ sở dữ liệu thông thường, các timestamp hầu như không bao giờ căn chỉnh hoàn hảo — một giao dịch tại 12:00:00.123 cần khớp với một báo giá tại 12:00:00.098, không phải 12:00:00.201.

ASOF JOIN giải quyết vấn đề này trong một dòng:

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

Với mỗi hàng trong trades, QuestDB tìm hàng trong quotes có timestamp mới nhất nhỏ hơn hoặc bằng timestamp của giao dịch, khớp theo cột symbol. Không có correlated subqueries. Không có window functions. Không có logic ở tầng ứng dụng.

Đây là nền tảng của Phân tích Chi phí Giao dịch (TCA) — so sánh giá thực hiện của bạn với thị trường hiện hành tại thời điểm thực hiện. Trong PostgreSQL, phương thức tương đương đòi hỏi LATERAL JOIN với ORDER BY và LIMIT 1 cho mỗi hàng, chậm hơn nhiều bậc so với các tập dữ liệu lớn.

TOLERANCE: Ngăn chặn Kết hợp Dữ liệu Cũ

Đây là điểm tinh tế phân biệt các triển khai thử nghiệm với hệ thống sản xuất. Nếu báo giá cho một tài sản giao dịch thưa thớt đã 5 phút tuổi tại thời điểm giao dịch thì sao? Trong thị trường biến động, một báo giá 5 phút tuổi thực chất là vô giá trị. ASOF JOIN mặc định vẫn sẽ sử dụng nó — nó tìm kết quả khớp mới nhất, bất kể dữ liệu cũ đến mức nào.

Mệnh đề TOLERANCE của QuestDB giải quyết vấn đề này:

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

Bây giờ, nếu không có báo giá khớp nào trong vòng 1 giây của giao dịch, kết hợp trả về NULL thay vì dữ liệu cũ. Điều này rất quan trọng cho TCA chính xác và cho bất kỳ phân tích nào mà độ tươi của dữ liệu quan trọng.

Thêm vào đó, TOLERANCE có thể cải thiện đáng kể hiệu suất truy vấn. Không có nó, engine có thể quét ngược sâu vào bảng quotes để tìm kết quả khớp. Với TOLERANCE, nó có thể kết thúc quét ngược sớm khi các bản ghi quá cũ để đủ điều kiện.

LT JOIN và SPLICE JOIN

Hai biến thể đáng biết: LT JOIN giống ASOF JOIN nhưng khớp trước timestamp (không bao giờ bằng). Điều này hữu ích khi bạn cần tránh thiên kiến nhìn trước trong backtesting — bạn muốn báo giá tồn tại trước giao dịch của mình, không phải một báo giá đến tại cùng microsecond.

SPLICE JOIN là ASOF đầy đủ theo cả hai hướng: với mỗi bản ghi trong bảng trái nó tìm bản ghi bảng phải hiện hành, và với mỗi bản ghi trong bảng phải nó tìm bản ghi bảng trái hiện hành. Kết quả là một dòng thời gian hợp nhất, xen kẽ của cả hai nguồn dữ liệu. Điều này đặc biệt hữu ích để tạo các dòng thời gian sự kiện thống nhất từ nhiều luồng dữ liệu.

HORIZON JOIN: Phân tích Sau Giao dịch trong Một Truy vấn Duy nhất

Được giới thiệu trong QuestDB 9.3.3, HORIZON JOIN được xây dựng chuyên biệt cho phân tích markout — nền tảng của đánh giá chất lượng thực thi và nghiên cứu vi cấu trúc thị trường.

Câu hỏi nó trả lời: "Sau khi một giao dịch được thực hiện, giá đã biến động như thế nào trong N giây tiếp theo?" Theo truyền thống, điều này đòi hỏi self-joins, UNION ALL qua nhiều truy vấn ASOF, hoặc chuyển logic sang code ứng dụng. HORIZON JOIN đơn giản hóa tất cả:

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;

Lệnh này tính toán mức biến động giá trung bình theo khoảng 1 giây lên đến 60 giây sau mỗi lệnh fill. Engine xử lý tính toán độ lệch thời gian, khớp ASOF tại mỗi điểm horizon, và tổng hợp — tất cả trong một lần quét duy nhất.

Đối với các horizon không đều — hoặc để nhìn trước sự kiện — sử dụng cú pháp LIST:

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

Điều này cho bạn cả đường cong markout trước và sau giao dịch. Kết hợp với lọc theo sàn, chiến lược, hoặc kích thước lệnh, bạn có thể xây dựng một khung đánh giá chất lượng thực thi hoàn chỉnh bên trong cơ sở dữ liệu.

Sách hướng dẫn của QuestDB bao gồm năm mẫu phân tích sau giao dịch được xây dựng trên HORIZON JOIN: phân tích slippage (thực thi so với giá giữa), đường cong markout, implementation shortfall (phân tích Perold), chấm điểm sàn cho smart order routing, và phát hiện độc tính dòng lệnh (VPIN). Mỗi mẫu chạy trên demo trực tiếp của họ với dữ liệu thực tế.

WINDOW JOIN: Tương quan Sự kiện với Dữ liệu Xung quanh

WINDOW JOIN được giới thiệu trong QuestDB 9.3. Nó cho phép mỗi hàng từ bảng chính được kết hợp với một khoảng thời gian các hàng từ bảng khác, với các tổng hợp được tính toán trên các hàng khớp.

Xem xét một kịch bản giao dịch FX nơi bạn muốn tương quan mỗi giao dịch với giá bid và ask trung bình trong 10 giây sau giao dịch:

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;

Mệnh đề INCLUDE PREVAILING đảm bảo bạn nhận được giá gần nhất ngay cả khi không có kết quả khớp chính xác tại ranh giới cửa sổ. Điều này loại bỏ hàng trang subqueries mà các phân tích horizon ngắn thường đòi hỏi.

Các trường hợp sử dụng trong giao dịch: tính toán điều kiện thị trường trung bình xung quanh mỗi lệnh thực hiện, phát hiện hành vi giá bất thường trong khoảng thời gian trước các lệnh lớn, tương quan các sự kiện IoT/cơ sở hạ tầng (đỉnh độ trễ mạng) với chất lượng thực thi.

LATEST ON: Trạng thái Hiện tại Tức thì

Một mở rộng đơn giản nhưng cực kỳ hữu ích. LATEST ON trả về hàng cuối cùng cho mỗi giá trị của cột phân vùng:

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

Điều này cho bạn giao dịch cuối cùng cho mỗi cặp (symbol, side) — về cơ bản là ảnh chụp "trạng thái hiện tại" theo thời gian thực. Trong cơ sở dữ liệu truyền thống, điều này sẽ đòi hỏi một correlated subquery hoặc window function với ROW_NUMBER().

Đối với các bảng điều khiển giao dịch hiển thị giá mới nhất trên hàng trăm ký hiệu, LATEST ON là tức thì. Kết hợp với materialized views (chúng ta sẽ đề cập trong Phần 3), nó trở thành nền tảng cho các ảnh chụp danh mục dưới millisecond.

TWAP: Giá Trung bình Có trọng số Thời gian Gốc

Hàm tổng hợp twap(price, timestamp) được thêm vào QuestDB 9.3.3. Không giống VWAP có trọng số theo khối lượng, TWAP có trọng số theo thời gian — mỗi giá giữ nguyên cho đến quan sát tiếp theo, và kết quả là diện tích dưới hàm bậc thang chia cho tổng thời gian.

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

TWAP là chuẩn thực thi tiêu chuẩn cho các lệnh thuật toán. Việc có nó như một hàm tổng hợp gốc hỗ trợ GROUP BY và SAMPLE BY song song với tất cả các chế độ FILL có nghĩa là bạn không cần bất kỳ tích hợp phía client nào — tính toán chạy hoàn toàn bên trong engine truy vấn.

Window Functions: Nền tảng của Phân tích Kỹ thuật

QuestDB hỗ trợ các window functions SQL chuẩn, tạo thành xương sống của tính toán chỉ báo kỹ thuật:

-- Bollinger Bands sử dụng materialized view OHLC
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 cũng giới thiệu mệnh đề WINDOW theo chuẩn SQL — định nghĩa một đặc tả cửa sổ một lần, tham chiếu nó theo tên qua nhiều hàm. Không còn phải lặp lại cùng PARTITION BY và ORDER BY trong mọi biểu thức:

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);

Truy vấn gọn hơn, ít sao chép-dán, hiệu suất thực thi giống nhau.

Các Mẫu Truy vấn Thực tế

Hãy để tôi chỉ cho bạn một số mẫu xuất hiện thường xuyên trong các hệ thống giao dịch sản xuất:

Tương quan Đa tài sản

-- Tương quan hàng giờ và hàng ngày luân phiên giữa ETH và một tài sản khác
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 Trên Tất cả Ký hiệu

-- RSI 14 ngày cho tất cả các cặp USDT
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 (Biên độ Thực Trung bình)

-- ATR 14 kỳ từ các thanh OHLC 15 phút
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;

Lợi thế SQL So với Code Tùy chỉnh

Bạn có thể tự hỏi: tại sao tính toán các chỉ báo này bên trong cơ sở dữ liệu khi bạn có thể kéo dữ liệu thô vào Python và sử dụng ta-lib hay pandas?

Ba lý do. Thứ nhất, cục bộ hóa dữ liệu — di chuyển terabyte dữ liệu tick qua mạng để tính toán một trung bình luân phiên là lãng phí. Tính toán nên ở nơi dữ liệu tồn tại. Thứ hai, song song hóa — engine vectorized của QuestDB với biên dịch JIT có thể xử lý các truy vấn này qua nhiều core với lệnh SIMD, thường nhanh hơn code Python đơn luồng. Thứ ba, nhất quán — khi nhiều bảng điều khiển, chiến lược và hệ thống giám sát đều cần cùng chỉ báo, việc có một nguồn sự thật duy nhất trong cơ sở dữ liệu loại bỏ các lỗi đồng bộ hóa.

Tuy nhiên, QuestDB không cố gắng thay thế toàn bộ ngăn xếp phân tích của bạn. Tính tương thích Parquet có nghĩa là bạn vẫn có thể kéo dữ liệu lịch sử trực tiếp vào pipeline ML của mình mà không cần đi qua cơ sở dữ liệu cho các khối lượng công việc theo lô.

Tiếp theo trong Phần 3

Trong phần cuối, chúng ta sẽ đề cập đến materialized views cho OHLC theo thời gian thực (bao gồm các view xếp tầng ở nhiều khung thời gian), mảng 2D cho phân tích order book gốc, và kiến trúc tham chiếu cho một nền tảng giao dịch thuật toán được hỗ trợ hoàn toàn bởi QuestDB.

Trích dẫn

@software{soloviov2025questdb_algotrading_p2,
  author = {Soloviov, Eugen},
  title = {QuestDB for Algorithmic Trading: SQL Extensions That Change the Game},
  year = {2025},
  url = {https://marketmaker.cc/vi/blog/post/questdb-algotrading-sql},
  version = {0.1.0},
  description = {Khám phá sâu các mở rộng SQL chuỗi thời gian của QuestDB: SAMPLE BY, ASOF JOIN, HORIZON JOIN, WINDOW JOIN, LATEST ON, và các mẫu truy vấn giao dịch thực tế.}
}
Tuyên bố miễn trừ trách nhiệm: Thông tin được cung cấp trong bài viết này chỉ nhằm mục đích giáo dục và thông tin, không cấu thành lời khuyên về tài chính, đầu tư hoặc giao dịch. Giao dịch tiền mã hóa tiềm ẩn rủi ro thua lỗ đáng kể.

Tác Giả

Eugen Soloviov
Eugen Soloviov

Trading-systems engineer

Trading-systems engineer building bots since 2017: cross-exchange arbitrage (connected up to 30 venues), cointegration-based pairs arbitrage across spot and futures, scalping, news and sentiment-driven strategies, trend algorithms, and portfolio management and balancing algorithms. Also builds sub-millisecond order execution, big-data warehouses, backtesting engines, AI agents, and trading interfaces (incl. open-source profitmaker.cc). Stack: JS/TS, Python, Rust/Zig/Go, DevOps, backend, frontend, architecture.

Newsletter

Đi Trước Thị Trường

Đăng ký nhận bản tin của chúng tôi để có những thông tin chuyên sâu độc quyền về AI trading, phân tích thị trường và các cập nhật nền tảng.

Chúng tôi tôn trọng quyền riêng tư của bạn. Hủy đăng ký bất kỳ lúc nào.