QuestDB للتداول الخوارزمي: ملحقات SQL التي تُغيّر قواعد اللعبة
الجزء 2 من 3 — متاح أيضاً بـ RU · ZH
إخلاء مسؤولية: المعلومات المقدمة في هذا المقال هي لأغراض تعليمية ومعلوماتية فقط ولا تشكل نصيحة مالية أو استثمارية أو تداولية. ينطوي تداول العملات المشفرة على مخاطر خسارة كبيرة.
مرحباً بكم في الجزء 2 من سلسلة QuestDB. في الجزء 1، تناولنا بنية التخزين ثلاثية الطبقات ومبادئ تصميم المخطط. الآن ندخل في مجموعة الميزات المُميزة حقاً — ملحقات SQL التي تجعل QuestDB يبدو كأنه صُمم من قبل متداولين، للمتداولين.
أُنشئ SQL القياسي في سبعينيات القرن العشرين للبيانات العلائقية. لا يعرف شيئاً عن الزمن كمفهوم من الدرجة الأولى. كل عملية سلاسل زمنية في PostgreSQL أو MySQL تتطلب حلولاً مطولة — دوال النوافذ، والربط الجانبي، وتعابير الجدول المشتركة المكدسة بثلاث طبقات. ملحقات QuestDB تضغط استعلامات متعددة الفقرات هذه في تعابير مفردة ومعبّرة.
لنستعرض كل واحدة مع أمثلة تداول حقيقية.

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 بتقسيم البيانات إلى فترات دقيقة واحدة، و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: "افعل ما أعنيه" لمحاذاة بيانات السوق
هذه هي جوهرة QuestDB، وإذا عملت مع بيانات السوق، ستفهم فوراً لماذا.
المشكلة الأساسية: لديك صفقات في جدول وأسعار (bid/ask) في جدول آخر. تريد معرفة السعر السائد في اللحظة الدقيقة التي نُفذت فيها كل صفقة. في قاعدة بيانات عادية، نادراً ما تتطابق الطوابع الزمنية تماماً — صفقة عند 12:00:00.123 تحتاج أن تتطابق مع سعر عند 12:00:00.098، وليس 12:00:00.201.
ASOF JOIN يحل هذا في سطر واحد:
SELECT trades.*, quotes.bid, quotes.ask
FROM trades
ASOF JOIN quotes ON (symbol);
لكل صف في trades، يجد QuestDB الصف في quotes بأحدث طابع زمني أقل من أو يساوي طابع الصفقة الزمني، مع المطابقة على عمود symbol. لا استعلامات فرعية مترابطة. لا دوال نوافذ. لا منطق على مستوى التطبيق.
هذا أساس تحليل تكاليف المعاملات (TCA) — مقارنة سعر تنفيذك بالسوق السائد وقت التنفيذ. في PostgreSQL، المكافئ يتطلب LATERAL JOIN مع ORDER BY وLIMIT 1 لكل صف، وهو أبطأ بأوامر من الحجم على مجموعات البيانات الكبيرة.
TOLERANCE: منع ربط البيانات القديمة
هناك دقة تفصل التطبيقات اللعبية عن أنظمة الإنتاج. ماذا لو كان سعر أصل خفيف التداول عمره 5 دقائق وقت الصفقة؟ في سوق متقلب، سعر عمره 5 دقائق هو قمامة بشكل أساسي. ASOF JOIN الافتراضي سيظل يستخدمه — يجد أحدث تطابق بغض النظر عن مدى قِدمه.
جملة TOLERANCE في QuestDB تصلح هذا:
SELECT trades.*, quotes.bid, quotes.ask
FROM trades
ASOF JOIN quotes ON (symbol)
TOLERANCE 1s;
الآن، إذا لم يوجد سعر مطابق خلال ثانية واحدة من الصفقة، يُرجع الربط NULL بدلاً من بيانات قديمة. هذا حاسم لـ TCA الدقيق ولأي تحليلات تهم فيها حداثة البيانات.
كمكافأة، يمكن لـ TOLERANCE تحسين أداء الاستعلام بشكل كبير. بدونه، قد يمسح المحرك بعيداً في جدول الأسعار بحثاً عن تطابق. مع TOLERANCE، يمكنه إنهاء المسح العكسي مبكراً حالما تكون السجلات قديمة جداً للتأهل.
LT JOIN و SPLICE JOIN
نوعان يستحقان المعرفة: LT JOIN مثل ASOF JOIN لكنه يتطابق قبل الطابع الزمني بشكل صارم (وليس مساوياً). هذا مفيد عندما تحتاج تجنب تحيز الاستشراف في الاختبار الرجعي — تريد السعر الذي كان موجوداً قبل صفقتك، وليس الذي وصل في نفس الميكرو ثانية.
SPLICE JOIN هو ASOF كامل في كلا الاتجاهين: لكل سجل في الجدول الأيسر يجد سجل الجدول الأيمن السائد، ولكل سجل في الجدول الأيمن يجد سجل الجدول الأيسر السائد. النتيجة هي خط زمني مُدمج ومتداخل من كلا مصدري البيانات. هذا مفيد بشكل خاص لإنشاء خطوط زمنية موحدة للأحداث من تدفقات بيانات متعددة.
HORIZON JOIN: تحليل ما بعد التنفيذ في استعلام واحد
قُدم HORIZON JOIN في QuestDB 9.3.3، وهو مبني خصيصاً لتحليل المعايير — حجر الأساس لتقييم جودة التنفيذ وأبحاث البنية الدقيقة للسوق.
السؤال الذي يجيب عليه: "بعد تنفيذ صفقة، كيف تطور السعر خلال الثواني N التالية؟" تقليدياً، يتطلب هذا ربطاً ذاتياً، أو UNION ALL عبر استعلامات ASOF متعددة، أو نقل المنطق إلى كود التطبيق. 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;
يحسب هذا متوسط حركة السعر بفواصل ثانية واحدة حتى 60 ثانية بعد كل تنفيذ. يتعامل المحرك مع حساب إزاحة الوقت، ومطابقة ASOF عند كل نقطة أفق، والتجميع — كل ذلك في مرور واحد.
للآفاق غير المنتظمة — أو للنظر قبل الحدث — استخدم صيغة LIST:
HORIZON JOIN mid_prices ON (symbol)
LIST (-5s, -1s, 0, 1s, 5s, 30s, 60s);
هذا يعطيك منحنيات المعايير قبل وبعد التنفيذ. مع التصفية حسب المكان أو الاستراتيجية أو حجم الأمر، يمكنك بناء إطار عمل كامل لجودة التنفيذ داخل قاعدة البيانات.
يتضمن كتاب وصفات QuestDB خمسة أنماط لتحليل ما بعد التنفيذ مبنية على HORIZON JOIN: تحليل الانزلاق (التنفيذ مقابل سعر الوسط)، ومنحنيات المعايير، والعجز التنفيذي (تحليل Perold)، وتسجيل الأماكن لتوجيه الأوامر الذكي، وكشف سمية التدفق (VPIN). كل نمط يعمل على العرض التوضيحي المباشر ببيانات حقيقية.
WINDOW JOIN: ربط الأحداث بالبيانات المحيطة
قُدم WINDOW JOIN في QuestDB 9.3. يسمح لكل صف من جدول أساسي بالربط مع نافذة زمنية من صفوف جدول آخر، مع حساب التجميعات على الصفوف المطابقة.
لنتأمل سيناريو تداول عملات حيث تريد ربط كل صفقة بمتوسط أسعار bid وask في الثواني العشر التالية للصفقة:
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 المتوازي وSAMPLE BY مع جميع أوضاع FILL يعني أنك لا تحتاج أي تكامل من جانب العميل — الحساب يعمل بالكامل داخل محرك الاستعلام.
دوال النوافذ: أساس التحليل الفني
يدعم 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 أيضاً جملة WINDOW القياسية في SQL — عرّف مواصفات النافذة مرة واحدة، وارجع إليها بالاسم عبر دوال متعددة. لا مزيد من تكرار نفس 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 (متوسط المدى الحقيقي)
-- 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 في الوقت الفعلي (بما في ذلك العروض المتتالية بأُطُر زمنية متعددة)، والمصفوفات ثنائية الأبعاد لتحليلات دفتر الأوامر الأصلية، والبنية المرجعية لمنصة تداول خوارزمي كاملة مبنية على 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.}
}
MarketMaker.cc Team
البحوث والاستراتيجيات الكمية