feat(candles): support oracle OHLC basis
This commit is contained in:
@@ -1006,6 +1006,7 @@ async function handler(cfg, req, res) {
|
|||||||
|
|
||||||
const symbol = (url.searchParams.get('symbol') || '').trim();
|
const symbol = (url.searchParams.get('symbol') || '').trim();
|
||||||
const source = (url.searchParams.get('source') || '').trim();
|
const source = (url.searchParams.get('source') || '').trim();
|
||||||
|
const basisRaw = (url.searchParams.get('basis') || '').trim().toLowerCase();
|
||||||
const tf = (url.searchParams.get('tf') || url.searchParams.get('timeframe') || '1m').trim();
|
const tf = (url.searchParams.get('tf') || url.searchParams.get('timeframe') || '1m').trim();
|
||||||
const limit = clampInt(url.searchParams.get('limit') || '300', 10, 2000);
|
const limit = clampInt(url.searchParams.get('limit') || '300', 10, 2000);
|
||||||
|
|
||||||
@@ -1023,6 +1024,11 @@ async function handler(cfg, req, res) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const sourceKey = source || '';
|
const sourceKey = source || '';
|
||||||
|
const basis = basisRaw === 'mark' ? 'mark' : basisRaw === 'oracle' || !basisRaw ? 'oracle' : null;
|
||||||
|
if (!basis) {
|
||||||
|
sendJson(res, 400, { ok: false, error: 'invalid_basis' }, cfg.corsOrigin);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Cache-first: read precomputed candles from `drift_candles_cache`.
|
// Cache-first: read precomputed candles from `drift_candles_cache`.
|
||||||
@@ -1039,6 +1045,9 @@ async function handler(cfg, req, res) {
|
|||||||
high
|
high
|
||||||
low
|
low
|
||||||
close
|
close
|
||||||
|
oracle_open
|
||||||
|
oracle_high
|
||||||
|
oracle_low
|
||||||
oracle_close
|
oracle_close
|
||||||
ticks
|
ticks
|
||||||
}
|
}
|
||||||
@@ -1064,6 +1073,9 @@ async function handler(cfg, req, res) {
|
|||||||
high
|
high
|
||||||
low
|
low
|
||||||
close
|
close
|
||||||
|
oracle_open
|
||||||
|
oracle_high
|
||||||
|
oracle_low
|
||||||
oracle_close
|
oracle_close
|
||||||
ticks
|
ticks
|
||||||
}
|
}
|
||||||
@@ -1085,11 +1097,23 @@ async function handler(cfg, req, res) {
|
|||||||
.reverse()
|
.reverse()
|
||||||
.map((r) => {
|
.map((r) => {
|
||||||
const time = Math.floor(Date.parse(r.bucket) / 1000);
|
const time = Math.floor(Date.parse(r.bucket) / 1000);
|
||||||
const open = Number(r.open);
|
const oracleClose = r.oracle_close == null ? null : Number(r.oracle_close);
|
||||||
const high = Number(r.high);
|
const oracleOpen = r.oracle_open == null ? oracleClose : Number(r.oracle_open);
|
||||||
const low = Number(r.low);
|
const oracleHigh = r.oracle_high == null ? oracleClose : Number(r.oracle_high);
|
||||||
const close = Number(r.close);
|
const oracleLow = r.oracle_low == null ? oracleClose : Number(r.oracle_low);
|
||||||
const oracle = r.oracle_close == null ? null : Number(r.oracle_close);
|
|
||||||
|
const markOpen = Number(r.open);
|
||||||
|
const markHigh = Number(r.high);
|
||||||
|
const markLow = Number(r.low);
|
||||||
|
const markClose = Number(r.close);
|
||||||
|
|
||||||
|
const open = basis === 'oracle' ? oracleOpen : markOpen;
|
||||||
|
const high = basis === 'oracle' ? oracleHigh : markHigh;
|
||||||
|
const low = basis === 'oracle' ? oracleLow : markLow;
|
||||||
|
const close = basis === 'oracle' ? oracleClose : markClose;
|
||||||
|
|
||||||
|
// Always expose oracle close (even if basis=mark).
|
||||||
|
const oracle = oracleClose;
|
||||||
const volume = Number(r.ticks || 0);
|
const volume = Number(r.ticks || 0);
|
||||||
return { time, open, high, low, close, volume, oracle };
|
return { time, open, high, low, close, volume, oracle };
|
||||||
})
|
})
|
||||||
@@ -1131,6 +1155,7 @@ async function handler(cfg, req, res) {
|
|||||||
) {
|
) {
|
||||||
bucket
|
bucket
|
||||||
close
|
close
|
||||||
|
oracle_close
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`;
|
`;
|
||||||
@@ -1151,7 +1176,10 @@ async function handler(cfg, req, res) {
|
|||||||
for (const r of ptsRows) {
|
for (const r of ptsRows) {
|
||||||
const t = tsToUnixSeconds(r.bucket);
|
const t = tsToUnixSeconds(r.bucket);
|
||||||
if (t == null) continue;
|
if (t == null) continue;
|
||||||
const p = parseNumeric(r.close);
|
const p =
|
||||||
|
basis === 'oracle'
|
||||||
|
? parseNumeric(r.oracle_close) ?? parseNumeric(r.close)
|
||||||
|
: parseNumeric(r.close) ?? parseNumeric(r.oracle_close);
|
||||||
if (p == null) continue;
|
if (p == null) continue;
|
||||||
const idx = Math.floor((t - firstStart) / bucketSeconds);
|
const idx = Math.floor((t - firstStart) / bucketSeconds);
|
||||||
const start = firstStart + idx * bucketSeconds;
|
const start = firstStart + idx * bucketSeconds;
|
||||||
@@ -1222,6 +1250,7 @@ async function handler(cfg, req, res) {
|
|||||||
candlesFunction: cfg.candlesFunction,
|
candlesFunction: cfg.candlesFunction,
|
||||||
symbol,
|
symbol,
|
||||||
source: source || null,
|
source: source || null,
|
||||||
|
basis,
|
||||||
tf,
|
tf,
|
||||||
bucketSeconds,
|
bucketSeconds,
|
||||||
candles,
|
candles,
|
||||||
|
|||||||
@@ -173,21 +173,55 @@ function sqlUpsertCandlesFromTicks({ symbol, sourceKey, bucketSeconds, fromIso,
|
|||||||
max(px) AS high,
|
max(px) AS high,
|
||||||
min(px) AS low,
|
min(px) AS low,
|
||||||
(array_agg(px ORDER BY ts DESC))[1] AS close,
|
(array_agg(px ORDER BY ts DESC))[1] AS close,
|
||||||
|
(array_agg(oracle_px ORDER BY ts ASC))[1] AS oracle_open,
|
||||||
|
max(oracle_px) AS oracle_high,
|
||||||
|
min(oracle_px) AS oracle_low,
|
||||||
(array_agg(oracle_px ORDER BY ts DESC))[1] AS oracle_close,
|
(array_agg(oracle_px ORDER BY ts DESC))[1] AS oracle_close,
|
||||||
count(*)::bigint AS ticks
|
count(*)::bigint AS ticks
|
||||||
FROM base
|
FROM base
|
||||||
GROUP BY bucket
|
GROUP BY bucket
|
||||||
)
|
)
|
||||||
INSERT INTO public.drift_candles_cache
|
INSERT INTO public.drift_candles_cache
|
||||||
(bucket, bucket_seconds, symbol, source, open, high, low, close, oracle_close, ticks, updated_at)
|
(
|
||||||
|
bucket,
|
||||||
|
bucket_seconds,
|
||||||
|
symbol,
|
||||||
|
source,
|
||||||
|
open,
|
||||||
|
high,
|
||||||
|
low,
|
||||||
|
close,
|
||||||
|
oracle_open,
|
||||||
|
oracle_high,
|
||||||
|
oracle_low,
|
||||||
|
oracle_close,
|
||||||
|
ticks,
|
||||||
|
updated_at
|
||||||
|
)
|
||||||
SELECT
|
SELECT
|
||||||
bucket, ${bucketSeconds}, ${sqlLit(symbol)}, ${sqlLit(sourceKey)}, open, high, low, close, oracle_close, ticks, now()
|
bucket,
|
||||||
|
${bucketSeconds},
|
||||||
|
${sqlLit(symbol)},
|
||||||
|
${sqlLit(sourceKey)},
|
||||||
|
open,
|
||||||
|
high,
|
||||||
|
low,
|
||||||
|
close,
|
||||||
|
oracle_open,
|
||||||
|
oracle_high,
|
||||||
|
oracle_low,
|
||||||
|
oracle_close,
|
||||||
|
ticks,
|
||||||
|
now()
|
||||||
FROM agg
|
FROM agg
|
||||||
ON CONFLICT (bucket, bucket_seconds, symbol, source) DO UPDATE SET
|
ON CONFLICT (bucket, bucket_seconds, symbol, source) DO UPDATE SET
|
||||||
open = EXCLUDED.open,
|
open = EXCLUDED.open,
|
||||||
high = EXCLUDED.high,
|
high = EXCLUDED.high,
|
||||||
low = EXCLUDED.low,
|
low = EXCLUDED.low,
|
||||||
close = EXCLUDED.close,
|
close = EXCLUDED.close,
|
||||||
|
oracle_open = EXCLUDED.oracle_open,
|
||||||
|
oracle_high = EXCLUDED.oracle_high,
|
||||||
|
oracle_low = EXCLUDED.oracle_low,
|
||||||
oracle_close = EXCLUDED.oracle_close,
|
oracle_close = EXCLUDED.oracle_close,
|
||||||
ticks = EXCLUDED.ticks,
|
ticks = EXCLUDED.ticks,
|
||||||
updated_at = now();
|
updated_at = now();
|
||||||
|
|||||||
@@ -121,10 +121,17 @@ CREATE TABLE IF NOT EXISTS public.drift_candles (
|
|||||||
high numeric,
|
high numeric,
|
||||||
low numeric,
|
low numeric,
|
||||||
close numeric,
|
close numeric,
|
||||||
|
oracle_open numeric,
|
||||||
|
oracle_high numeric,
|
||||||
|
oracle_low numeric,
|
||||||
oracle_close numeric,
|
oracle_close numeric,
|
||||||
ticks bigint
|
ticks bigint
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ALTER TABLE public.drift_candles ADD COLUMN IF NOT EXISTS oracle_open numeric;
|
||||||
|
ALTER TABLE public.drift_candles ADD COLUMN IF NOT EXISTS oracle_high numeric;
|
||||||
|
ALTER TABLE public.drift_candles ADD COLUMN IF NOT EXISTS oracle_low numeric;
|
||||||
|
|
||||||
-- Precomputed candle cache (materialized by a worker).
|
-- Precomputed candle cache (materialized by a worker).
|
||||||
-- Purpose: make tf switching instant by reading ready-made candles instead of aggregating `drift_ticks` on demand.
|
-- Purpose: make tf switching instant by reading ready-made candles instead of aggregating `drift_ticks` on demand.
|
||||||
-- NOTE: `source=''` means "any source" (no source filter).
|
-- NOTE: `source=''` means "any source" (no source filter).
|
||||||
@@ -137,12 +144,19 @@ CREATE TABLE IF NOT EXISTS public.drift_candles_cache (
|
|||||||
high numeric NOT NULL,
|
high numeric NOT NULL,
|
||||||
low numeric NOT NULL,
|
low numeric NOT NULL,
|
||||||
close numeric NOT NULL,
|
close numeric NOT NULL,
|
||||||
|
oracle_open numeric,
|
||||||
|
oracle_high numeric,
|
||||||
|
oracle_low numeric,
|
||||||
oracle_close numeric,
|
oracle_close numeric,
|
||||||
ticks bigint NOT NULL DEFAULT 0,
|
ticks bigint NOT NULL DEFAULT 0,
|
||||||
updated_at timestamptz NOT NULL DEFAULT now(),
|
updated_at timestamptz NOT NULL DEFAULT now(),
|
||||||
PRIMARY KEY (bucket, bucket_seconds, symbol, source)
|
PRIMARY KEY (bucket, bucket_seconds, symbol, source)
|
||||||
);
|
);
|
||||||
|
|
||||||
|
ALTER TABLE public.drift_candles_cache ADD COLUMN IF NOT EXISTS oracle_open numeric;
|
||||||
|
ALTER TABLE public.drift_candles_cache ADD COLUMN IF NOT EXISTS oracle_high numeric;
|
||||||
|
ALTER TABLE public.drift_candles_cache ADD COLUMN IF NOT EXISTS oracle_low numeric;
|
||||||
|
|
||||||
SELECT create_hypertable('drift_candles_cache', 'bucket', if_not_exists => TRUE, migrate_data => TRUE);
|
SELECT create_hypertable('drift_candles_cache', 'bucket', if_not_exists => TRUE, migrate_data => TRUE);
|
||||||
|
|
||||||
CREATE INDEX IF NOT EXISTS drift_candles_cache_symbol_source_bucket_idx
|
CREATE INDEX IF NOT EXISTS drift_candles_cache_symbol_source_bucket_idx
|
||||||
@@ -174,6 +188,9 @@ AS $$
|
|||||||
c.high,
|
c.high,
|
||||||
c.low,
|
c.low,
|
||||||
c.close,
|
c.close,
|
||||||
|
c.oracle_open,
|
||||||
|
c.oracle_high,
|
||||||
|
c.oracle_low,
|
||||||
c.oracle_close,
|
c.oracle_close,
|
||||||
c.ticks
|
c.ticks
|
||||||
FROM public.drift_candles_cache c, src
|
FROM public.drift_candles_cache c, src
|
||||||
@@ -201,6 +218,9 @@ AS $$
|
|||||||
max(px) AS high,
|
max(px) AS high,
|
||||||
min(px) AS low,
|
min(px) AS low,
|
||||||
(array_agg(px ORDER BY ts DESC))[1] AS close,
|
(array_agg(px ORDER BY ts DESC))[1] AS close,
|
||||||
|
(array_agg(oracle_px ORDER BY ts ASC))[1] AS oracle_open,
|
||||||
|
max(oracle_px) AS oracle_high,
|
||||||
|
min(oracle_px) AS oracle_low,
|
||||||
(array_agg(oracle_px ORDER BY ts DESC))[1] AS oracle_close,
|
(array_agg(oracle_px ORDER BY ts DESC))[1] AS oracle_close,
|
||||||
count(*) AS ticks
|
count(*) AS ticks
|
||||||
FROM raw_fallback
|
FROM raw_fallback
|
||||||
@@ -236,6 +256,9 @@ AS $$
|
|||||||
d.high,
|
d.high,
|
||||||
d.low,
|
d.low,
|
||||||
d.close,
|
d.close,
|
||||||
|
d.oracle_open,
|
||||||
|
d.oracle_high,
|
||||||
|
d.oracle_low,
|
||||||
d.oracle_close,
|
d.oracle_close,
|
||||||
d.ticks
|
d.ticks
|
||||||
FROM series s
|
FROM series s
|
||||||
@@ -261,6 +284,9 @@ AS $$
|
|||||||
g.high,
|
g.high,
|
||||||
g.low,
|
g.low,
|
||||||
g.close,
|
g.close,
|
||||||
|
g.oracle_open,
|
||||||
|
g.oracle_high,
|
||||||
|
g.oracle_low,
|
||||||
g.oracle_close,
|
g.oracle_close,
|
||||||
g.ticks,
|
g.ticks,
|
||||||
COALESCE(
|
COALESCE(
|
||||||
@@ -282,6 +308,9 @@ AS $$
|
|||||||
COALESCE(high, ff_close) AS high,
|
COALESCE(high, ff_close) AS high,
|
||||||
COALESCE(low, ff_close) AS low,
|
COALESCE(low, ff_close) AS low,
|
||||||
COALESCE(close, ff_close) AS close,
|
COALESCE(close, ff_close) AS close,
|
||||||
|
COALESCE(oracle_open, ff_oracle) AS oracle_open,
|
||||||
|
COALESCE(oracle_high, ff_oracle) AS oracle_high,
|
||||||
|
COALESCE(oracle_low, ff_oracle) AS oracle_low,
|
||||||
COALESCE(oracle_close, ff_oracle) AS oracle_close,
|
COALESCE(oracle_close, ff_oracle) AS oracle_close,
|
||||||
COALESCE(ticks, 0) AS ticks
|
COALESCE(ticks, 0) AS ticks
|
||||||
FROM ff
|
FROM ff
|
||||||
|
|||||||
Reference in New Issue
Block a user