QuestDB per il Trading Algoritmico: Le Estensioni SQL che Cambiano le Regole del Gioco
Parte 2 di 3 — disponibile anche in RU · ZH
Avvertenza: Le informazioni fornite in questo articolo sono esclusivamente a scopo educativo e informativo e non costituiscono consulenza finanziaria, d'investimento o di trading. Il trading di criptovalute comporta un rischio significativo di perdita.
Benvenuti alla Parte 2 della nostra serie su QuestDB. Nella Parte 1, abbiamo trattato l'architettura di archiviazione a tre livelli e i principi di progettazione degli schemi. Ora entriamo nel vivo del set di funzionalità davvero differenziante — le estensioni SQL che fanno sembrare QuestDB progettato da trader, per trader.
L'SQL standard è stato creato negli anni '70 per i dati relazionali. Non sa nulla del tempo come concetto di prima classe. Ogni operazione su serie temporali in PostgreSQL o MySQL richiede soluzioni verbose — funzioni finestra, join laterali, CTE impilate tre livelli in profondità. Le estensioni di QuestDB collassano queste query di più paragrafi in singole dichiarazioni espressive.
Vediamo ciascuna con esempi di trading reali.

SAMPLE BY: Aggregazione Nativa per Bucket Temporali
Se c'è una query che ogni sistema di trading esegue più di ogni altra, è l'aggregazione OHLCV — la trasformazione dei trade grezzi in dati a candela. In SQL standard, si scriverebbe qualcosa del tipo:
-- SQL Standard: verboso e lento
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;
In QuestDB, questo diventa:
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;
Tutto qui. SAMPLE BY 1m indica a QuestDB di suddividere i dati in intervalli di 1 minuto, e first() / last() sono funzioni di aggregazione native che rispettano l'ordinamento temporale all'interno di ciascun bucket. Nessun date_trunc, nessuna ginnastica con array_agg, nessun GROUP BY esplicito.
Gli intervalli disponibili sono flessibili: 1s, 5s, 15m, 1h, 1d, 7d — qualsiasi combinazione di unità temporali. Per i mercati crypto che non dormono mai, è possibile allinearsi ai confini del calendario con supporto ai fusi orari:
SAMPLE BY 1d ALIGN TO CALENDAR TIME ZONE 'UTC';
FILL: Gestione delle Lacune nei Dati
I mercati reali hanno lacune — le coppie a bassa liquidità potrebbero non fare trading per minuti o ore. SAMPLE BY supporta diverse strategie FILL:
-- Riempimento con il valore precedente (forward fill — comune in finanza)
SAMPLE BY 15m FILL(PREV);
-- Riempimento con interpolazione lineare
SAMPLE BY 15m FILL(LINEAR);
-- Riempimento con una costante
SAMPLE BY 15m FILL(0);
-- Nessun riempimento — restituisce NULL (predefinito)
SAMPLE BY 15m FILL(NONE);
FILL(PREV) è il pane quotidiano per i dashboard di trading — se in un bucket di 15 minuti non si sono verificati trade, viene propagato in avanti l'ultimo prezzo noto. FILL(LINEAR) è più appropriato per segnali continui come i tassi di finanziamento o i tassi d'interesse.

ASOF JOIN: Il "Fai Quello che Intendo" dell'Allineamento dei Dati di Mercato
Questo è il gioiello della corona di QuestDB, e se hai lavorato con dati di mercato, capirai immediatamente il perché.
Il problema fondamentale: hai i trade in una tabella e le quotazioni (bid/ask) in un'altra. Vuoi sapere qual era la quotazione prevalente nel momento esatto in cui è stato eseguito ciascun trade. In un database normale, i timestamp quasi mai si allineano perfettamente — un trade alle 12:00:00.123 deve corrispondere a una quotazione alle 12:00:00.098, non alle 12:00:00.201.
ASOF JOIN risolve questo in una riga:
SELECT trades.*, quotes.bid, quotes.ask
FROM trades
ASOF JOIN quotes ON (symbol);
Per ogni riga in trades, QuestDB trova la riga in quotes con il timestamp più recente che è minore o uguale al timestamp del trade, abbinando sulla colonna symbol. Nessuna sottoquery correlata. Nessuna funzione finestra. Nessuna logica a livello applicativo.
Questa è la base della Transaction Cost Analysis (TCA) — confronto tra il prezzo di esecuzione e il mercato prevalente al momento dell'esecuzione. In PostgreSQL, l'equivalente richiede un LATERAL JOIN con ORDER BY e LIMIT 1 per ogni riga, che è di ordini di grandezza più lento su dataset di grandi dimensioni.
TOLERANCE: Prevenire i Join con Dati Obsoleti
Ecco una sottigliezza che separa le implementazioni giocattolo dai sistemi di produzione. Cosa succede se una quotazione per un asset a bassa negoziazione ha 5 minuti di ritardo al momento di un trade? In un mercato volatile, una quotazione vecchia di 5 minuti è essenzialmente spazzatura. L'ASOF JOIN predefinito la userebbe comunque — trova la corrispondenza più recente, indipendentemente da quanto sia obsoleta.
La clausola TOLERANCE di QuestDB risolve questo problema:
SELECT trades.*, quotes.bid, quotes.ask
FROM trades
ASOF JOIN quotes ON (symbol)
TOLERANCE 1s;
Ora, se non esiste alcuna quotazione corrispondente entro 1 secondo dal trade, il join restituisce NULL invece di dati obsoleti. Questo è fondamentale per un TCA accurato e per qualsiasi analisi in cui la freschezza dei dati è importante.
Come bonus, TOLERANCE può migliorare significativamente le prestazioni delle query. Senza di essa, il motore potrebbe scansionare molto indietro nella tabella delle quotazioni cercando una corrispondenza. Con TOLERANCE, può terminare anticipatamente la scansione retroattiva una volta che i record sono troppo vecchi per qualificarsi.
LT JOIN e SPLICE JOIN
Due varianti da conoscere: LT JOIN è come ASOF JOIN ma abbina strettamente prima del timestamp (mai uguale). È utile quando si vuole evitare il bias lookahead nel backtesting — si vuole la quotazione che esisteva prima del trade, non una arrivata allo stesso microsecondo.
SPLICE JOIN è un ASOF completo in entrambe le direzioni: per ogni record nella tabella di sinistra trova il record prevalente nella tabella di destra, e per ogni record nella tabella di destra trova il record prevalente nella tabella di sinistra. Il risultato è una timeline unificata e intrecciata di entrambe le fonti dati. Questo è particolarmente utile per creare timeline di eventi unificati da più stream di dati.
HORIZON JOIN: Analisi Post-Trade in una Singola Query
Introdotto in QuestDB 9.3.3, HORIZON JOIN è progettato appositamente per l'analisi del markout — la pietra angolare della valutazione della qualità di esecuzione e della ricerca sulla microstruttura del mercato.
La domanda a cui risponde: "Dopo l'esecuzione di un trade, come si è evoluto il prezzo nei successivi N secondi?" Tradizionalmente, questo richiede auto-join, UNION ALL su più query ASOF, o spostare la logica nel codice applicativo. HORIZON JOIN collassa tutto:
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;
Questo calcola il movimento medio del prezzo a intervalli di 1 secondo fino a 60 secondi dopo ciascun fill. Il motore gestisce il calcolo dell'offset temporale, l'abbinamento ASOF in ciascun punto dell'orizzonte e l'aggregazione — tutto in un singolo passaggio.
Per orizzonti non uniformi — o per guardare prima dell'evento — usa la sintassi LIST:
HORIZON JOIN mid_prices ON (symbol)
LIST (-5s, -1s, 0, 1s, 5s, 30s, 60s);
Questo fornisce sia le curve di markout pre- che post-trade. Combinato con il filtraggio per venue, strategia o dimensione dell'ordine, è possibile costruire un framework completo di qualità di esecuzione all'interno del database.
Il cookbook di QuestDB include cinque pattern di analisi post-trade costruiti su HORIZON JOIN: analisi dello slippage (esecuzione vs. prezzo mid), curve di markout, implementation shortfall (la decomposizione di Perold), scoring delle venue per lo smart order routing e rilevamento della tossicità del flusso (VPIN). Ognuno funziona sulla loro demo live con dati reali.
WINDOW JOIN: Correlazione degli Eventi con i Dati Circostanti
WINDOW JOIN è stato introdotto in QuestDB 9.3. Consente a ciascuna riga di una tabella primaria di essere unita con una finestra temporale di righe di un'altra tabella, con aggregazioni calcolate sulle righe corrispondenti.
Considera uno scenario di trading FX in cui si vuole correlare ciascun trade con i prezzi bid e ask medi nei 10 secondi successivi al trade:
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;
La clausola INCLUDE PREVAILING garantisce di ottenere il prezzo più recente anche se non c'è una corrispondenza esatta al confine della finestra. Questo elimina le pagine di sottoquery che l'analisi a breve orizzonte tipicamente richiede.
Casi d'uso nel trading: calcolo delle condizioni medie di mercato intorno a ciascuna esecuzione, rilevamento di comportamenti di prezzo insoliti in una finestra temporale prima di ordini di grandi dimensioni, correlazione di eventi IoT/infrastrutturali (picchi di latenza di rete) con la qualità di esecuzione.
LATEST ON: Stato Attuale Istantaneo
Un'estensione deceptivamente semplice ma incredibilmente utile. LATEST ON restituisce l'ultima riga per ciascun valore di una colonna di partizionamento:
SELECT * FROM trades
LATEST ON timestamp PARTITION BY symbol, side
ORDER BY timestamp DESC;
Questo fornisce l'ultimo trade per ciascuna coppia (symbol, side) — essenzialmente uno snapshot "stato attuale" in tempo reale. In un database tradizionale, questo richiederebbe una sottoquery correlata o una funzione finestra con ROW_NUMBER().
Per i dashboard di trading che mostrano l'ultimo prezzo su centinaia di simboli, LATEST ON è istantaneo. Combinato con le viste materializzate (che tratteremo nella Parte 3), diventa la base per snapshot del portafoglio in meno di un millisecondo.
TWAP: Media Ponderata Temporale Nativa
L'aggregato twap(price, timestamp) è stato aggiunto in QuestDB 9.3.3. A differenza del VWAP che pondera per volume, il TWAP pondera per durata — ogni prezzo vale fino all'osservazione successiva, e il risultato è l'area sotto la funzione a gradini divisa per il tempo totale.
SELECT symbol,
twap(price, timestamp) AS twap_value,
vwap(price, quantity) AS vwap_value
FROM trades
WHERE timestamp IN today()
SAMPLE BY 1h;
Il TWAP è il benchmark di esecuzione standard per gli ordini algoritmici. Averlo come aggregato nativo che supporta GROUP BY parallelo e SAMPLE BY con tutte le modalità FILL significa che non è necessaria alcuna integrazione lato client — il calcolo viene eseguito interamente all'interno del motore di query.
Funzioni Finestra: La Base dell'Analisi Tecnica
QuestDB supporta le funzioni finestra SQL standard, che costituiscono la spina dorsale del calcolo degli indicatori tecnici:
-- Bande di Bollinger usando la vista OHLC materializzata
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 ha anche introdotto la clausola WINDOW conforme allo standard SQL — definisci una specifica di finestra una volta, referenziarla per nome in più funzioni. Niente più ripetizioni dello stesso PARTITION BY e ORDER BY in ogni espressione:
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);
Query più pulite, meno copia-incolla, stesse prestazioni di esecuzione.
Pattern di Query del Mondo Reale
Ecco alcuni pattern che emergono costantemente nei sistemi di trading in produzione:
Correlazione Cross-Asset
-- Correlazione oraria e giornaliera mobile tra ETH e un altro 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 su Tutti i Simboli
-- RSI a 14 giorni per tutte le coppie 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 (Average True Range)
-- ATR a 14 periodi da barre OHLC a 15 minuti
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;
Il Vantaggio SQL Rispetto al Codice Personalizzato
Potresti chiederti: perché calcolare questi indicatori all'interno del database quando si potrebbe estrarre i dati grezzi in Python e usare ta-lib o pandas?
Tre motivi. Primo, la località dei dati — spostare terabyte di dati tick attraverso la rete per calcolare una media mobile è uno spreco. Il calcolo dovrebbe vivere dove vivono i dati. Secondo, il parallelismo — il motore vettorizzato di QuestDB con compilazione JIT può elaborare queste query su più core con istruzioni SIMD, spesso più velocemente del codice Python single-threaded. Terzo, la coerenza — quando più dashboard, strategie e sistemi di monitoraggio hanno tutti bisogno degli stessi indicatori, avere un'unica fonte di verità nel database elimina i bug di sincronizzazione.
Detto questo, QuestDB non cerca di sostituire l'intero stack di analisi. L'interoperabilità con Parquet significa che è comunque possibile estrarre dati storici direttamente nel pipeline ML senza passare attraverso il database per i carichi di lavoro batch.
In Arrivo nella Parte 3
Nella parte finale, tratteremo le viste materializzate per OHLC in tempo reale (incluse le viste a cascata su più timeframe), gli array 2D per l'analisi nativa del book degli ordini, e un'architettura di riferimento per una piattaforma di trading algoritmico completa basata su QuestDB.
Citazione
@software{soloviov2025questdb_algotrading_p2,
author = {Soloviov, Eugen},
title = {QuestDB for Algorithmic Trading: SQL Extensions That Change the Game},
year = {2025},
url = {https://marketmaker.cc/it/blog/post/questdb-algotrading-sql},
version = {0.1.0},
description = {Approfondimento sulle estensioni SQL time-series di QuestDB: SAMPLE BY, ASOF JOIN, HORIZON JOIN, WINDOW JOIN, LATEST ON e pattern di query di trading nel mondo reale.}
}
Autori
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.