@yuants/app-virtual-exchange 0.11.7 → 0.12.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/series-data/fifo-queue.js +25 -0
- package/dist/series-data/fifo-queue.js.map +1 -0
- package/dist/series-data/index.js +2 -0
- package/dist/series-data/index.js.map +1 -0
- package/dist/series-data/scheduler.js +568 -0
- package/dist/series-data/scheduler.js.map +1 -0
- package/lib/series-data/fifo-queue.d.ts +8 -0
- package/lib/series-data/fifo-queue.d.ts.map +1 -0
- package/lib/series-data/fifo-queue.js +29 -0
- package/lib/series-data/fifo-queue.js.map +1 -0
- package/lib/series-data/index.d.ts +2 -0
- package/lib/series-data/index.d.ts.map +1 -0
- package/lib/series-data/index.js +4 -0
- package/lib/series-data/index.js.map +1 -0
- package/lib/series-data/scheduler.d.ts +2 -0
- package/lib/series-data/scheduler.d.ts.map +1 -0
- package/lib/series-data/scheduler.js +570 -0
- package/lib/series-data/scheduler.js.map +1 -0
- package/package.json +3 -1
- package/temp/package-deps.json +10 -4
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export const createFifoQueue = () => {
|
|
2
|
+
let items = [];
|
|
3
|
+
let head = 0;
|
|
4
|
+
return {
|
|
5
|
+
enqueue: (value) => {
|
|
6
|
+
items.push(value);
|
|
7
|
+
},
|
|
8
|
+
dequeue: () => {
|
|
9
|
+
if (head >= items.length)
|
|
10
|
+
return undefined;
|
|
11
|
+
const value = items[head++];
|
|
12
|
+
if (head > 1024 && head * 2 > items.length) {
|
|
13
|
+
items = items.slice(head);
|
|
14
|
+
head = 0;
|
|
15
|
+
}
|
|
16
|
+
return value;
|
|
17
|
+
},
|
|
18
|
+
size: () => items.length - head,
|
|
19
|
+
snapshot: (limit = 20) => {
|
|
20
|
+
const safeLimit = Math.max(0, Math.floor(limit));
|
|
21
|
+
return items.slice(head, head + safeLimit);
|
|
22
|
+
},
|
|
23
|
+
};
|
|
24
|
+
};
|
|
25
|
+
//# sourceMappingURL=fifo-queue.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fifo-queue.js","sourceRoot":"","sources":["../../src/series-data/fifo-queue.ts"],"names":[],"mappings":"AAOA,MAAM,CAAC,MAAM,eAAe,GAAG,GAAiB,EAAE;IAChD,IAAI,KAAK,GAAQ,EAAE,CAAC;IACpB,IAAI,IAAI,GAAG,CAAC,CAAC;IAEb,OAAO;QACL,OAAO,EAAE,CAAC,KAAK,EAAE,EAAE;YACjB,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QACpB,CAAC;QACD,OAAO,EAAE,GAAG,EAAE;YACZ,IAAI,IAAI,IAAI,KAAK,CAAC,MAAM;gBAAE,OAAO,SAAS,CAAC;YAC3C,MAAM,KAAK,GAAG,KAAK,CAAC,IAAI,EAAE,CAAC,CAAC;YAC5B,IAAI,IAAI,GAAG,IAAI,IAAI,IAAI,GAAG,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE;gBAC1C,KAAK,GAAG,KAAK,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;gBAC1B,IAAI,GAAG,CAAC,CAAC;aACV;YACD,OAAO,KAAK,CAAC;QACf,CAAC;QACD,IAAI,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,MAAM,GAAG,IAAI;QAC/B,QAAQ,EAAE,CAAC,KAAK,GAAG,EAAE,EAAE,EAAE;YACvB,MAAM,SAAS,GAAG,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,IAAI,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC,CAAC;YACjD,OAAO,KAAK,CAAC,KAAK,CAAC,IAAI,EAAE,IAAI,GAAG,SAAS,CAAC,CAAC;QAC7C,CAAC;KACF,CAAC;AACJ,CAAC,CAAC","sourcesContent":["export interface IQueue<T> {\n enqueue: (value: T) => void;\n dequeue: () => T | undefined;\n size: () => number;\n snapshot: (limit?: number) => T[];\n}\n\nexport const createFifoQueue = <T>(): IQueue<T> => {\n let items: T[] = [];\n let head = 0;\n\n return {\n enqueue: (value) => {\n items.push(value);\n },\n dequeue: () => {\n if (head >= items.length) return undefined;\n const value = items[head++];\n if (head > 1024 && head * 2 > items.length) {\n items = items.slice(head);\n head = 0;\n }\n return value;\n },\n size: () => items.length - head,\n snapshot: (limit = 20) => {\n const safeLimit = Math.max(0, Math.floor(limit));\n return items.slice(head, head + safeLimit);\n },\n };\n};\n"]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/series-data/index.ts"],"names":[],"mappings":"AAAA,OAAO,aAAa,CAAC","sourcesContent":["import './scheduler';\n"]}
|
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
var _a, _b;
|
|
2
|
+
import { encodeInterestRateSeriesId } from '@yuants/data-interest-rate';
|
|
3
|
+
import { encodeOHLCSeriesId } from '@yuants/data-ohlc';
|
|
4
|
+
import { parseInterestRateServiceMetadataFromSchema, parseOHLCServiceMetadataFromSchema, } from '@yuants/exchange';
|
|
5
|
+
import { Terminal } from '@yuants/protocol';
|
|
6
|
+
import { escapeSQL, requestSQL } from '@yuants/sql';
|
|
7
|
+
import { convertDurationToOffset, encodePath, formatTime } from '@yuants/utils';
|
|
8
|
+
import { catchError, defer, EMPTY, filter, from, map, mergeMap, repeat, tap, timer, toArray } from 'rxjs';
|
|
9
|
+
import { createFifoQueue } from './fifo-queue';
|
|
10
|
+
const terminal = Terminal.fromNodeEnv();
|
|
11
|
+
const isEnabled = process.env.VEX_SERIES_DATA_ENABLED === '1';
|
|
12
|
+
const onlyProductIdPrefix = ((_a = process.env.VEX_SERIES_DATA_ONLY_PRODUCT_ID_PREFIX) !== null && _a !== void 0 ? _a : '').trim();
|
|
13
|
+
const CONFIG = {
|
|
14
|
+
tickIntervalMs: 1000,
|
|
15
|
+
scanFullLoopIntervalMs: 5 * 60000,
|
|
16
|
+
/**
|
|
17
|
+
* Maximum number of inflight series data ingest requests.
|
|
18
|
+
*/
|
|
19
|
+
maxInflight: 20,
|
|
20
|
+
/**
|
|
21
|
+
* Only run tail jobs when the global head backlog is below this threshold.
|
|
22
|
+
*/
|
|
23
|
+
tailOnlyWhenGlobalHeadBelow: 20,
|
|
24
|
+
defaultInterestRateHeadLagMs: 8 * 60 * 60000,
|
|
25
|
+
defaultForwardSeedWindowMs: 24 * 60 * 60000,
|
|
26
|
+
maxBackoffMs: 5 * 60000,
|
|
27
|
+
backoffStepMs: 5000,
|
|
28
|
+
};
|
|
29
|
+
const capabilities = [];
|
|
30
|
+
const mapCapKeyToCapability = new Map();
|
|
31
|
+
const mapCapKeyToState = new Map();
|
|
32
|
+
const mapSeriesKeyToState = new Map();
|
|
33
|
+
let inflight = 0;
|
|
34
|
+
let scanIndex = 0;
|
|
35
|
+
let capRunIndex = 0;
|
|
36
|
+
const LOG_QUEUE_INTERVAL_MS = Number((_b = process.env.VEX_SERIES_DATA_LOG_QUEUE_INTERVAL_MS) !== null && _b !== void 0 ? _b : '10000');
|
|
37
|
+
const getOrCreateCapState = (capKey) => {
|
|
38
|
+
const existing = mapCapKeyToState.get(capKey);
|
|
39
|
+
if (existing)
|
|
40
|
+
return existing;
|
|
41
|
+
const next = {
|
|
42
|
+
capKey,
|
|
43
|
+
headQueue: createFifoQueue(),
|
|
44
|
+
tailQueue: createFifoQueue(),
|
|
45
|
+
pendingHead: new Set(),
|
|
46
|
+
pendingTail: new Set(),
|
|
47
|
+
inflight: false,
|
|
48
|
+
nextEligibleAt: 0,
|
|
49
|
+
backoffMs: 0,
|
|
50
|
+
};
|
|
51
|
+
mapCapKeyToState.set(capKey, next);
|
|
52
|
+
return next;
|
|
53
|
+
};
|
|
54
|
+
const computeSeriesId = (seriesType, product_id, duration) => {
|
|
55
|
+
if (seriesType === 'ohlc')
|
|
56
|
+
return encodeOHLCSeriesId(product_id, duration !== null && duration !== void 0 ? duration : '');
|
|
57
|
+
return encodeInterestRateSeriesId(product_id);
|
|
58
|
+
};
|
|
59
|
+
const getOrCreateSeriesState = (params) => {
|
|
60
|
+
const { capKey, seriesType, product_id, duration, direction } = params;
|
|
61
|
+
const table_name = seriesType === 'ohlc' ? 'ohlc_v2' : 'interest_rate';
|
|
62
|
+
const series_id = computeSeriesId(seriesType, product_id, duration);
|
|
63
|
+
const seriesKey = encodePath(table_name, direction, product_id, duration !== null && duration !== void 0 ? duration : '');
|
|
64
|
+
const existing = mapSeriesKeyToState.get(seriesKey);
|
|
65
|
+
if (existing) {
|
|
66
|
+
existing.capKey = capKey;
|
|
67
|
+
return existing;
|
|
68
|
+
}
|
|
69
|
+
const next = {
|
|
70
|
+
capKey,
|
|
71
|
+
seriesKey,
|
|
72
|
+
seriesType,
|
|
73
|
+
table_name,
|
|
74
|
+
series_id,
|
|
75
|
+
product_id,
|
|
76
|
+
duration,
|
|
77
|
+
direction,
|
|
78
|
+
ranges: [],
|
|
79
|
+
};
|
|
80
|
+
mapSeriesKeyToState.set(seriesKey, next);
|
|
81
|
+
return next;
|
|
82
|
+
};
|
|
83
|
+
const computeHeadLagMs = (series) => {
|
|
84
|
+
var _a;
|
|
85
|
+
if (series.seriesType === 'interest_rate')
|
|
86
|
+
return CONFIG.defaultInterestRateHeadLagMs;
|
|
87
|
+
const offset = convertDurationToOffset((_a = series.duration) !== null && _a !== void 0 ? _a : '');
|
|
88
|
+
if (!isFinite(offset) || offset <= 0)
|
|
89
|
+
return 60000;
|
|
90
|
+
return Math.max(60000, offset);
|
|
91
|
+
};
|
|
92
|
+
const computeOverlapMs = (series) => {
|
|
93
|
+
var _a;
|
|
94
|
+
const maxOverlapMs = 60 * 60000;
|
|
95
|
+
let overlapMs = 60000;
|
|
96
|
+
if (series.seriesType === 'ohlc') {
|
|
97
|
+
const offset = convertDurationToOffset((_a = series.duration) !== null && _a !== void 0 ? _a : '');
|
|
98
|
+
if (isFinite(offset) && offset > 0)
|
|
99
|
+
overlapMs = offset;
|
|
100
|
+
}
|
|
101
|
+
else {
|
|
102
|
+
overlapMs = 60 * 60000;
|
|
103
|
+
}
|
|
104
|
+
if (series.last_window_ms && isFinite(series.last_window_ms) && series.last_window_ms > 0) {
|
|
105
|
+
overlapMs = Math.min(overlapMs, Math.max(1, series.last_window_ms - 1));
|
|
106
|
+
}
|
|
107
|
+
return Math.max(1, Math.min(maxOverlapMs, overlapMs));
|
|
108
|
+
};
|
|
109
|
+
const applyOverlapToRequestTime = (series, baseTime) => {
|
|
110
|
+
const hasAnyRange = series.union_start_ms !== undefined || series.union_end_ms !== undefined || series.ranges.length > 0;
|
|
111
|
+
if (!hasAnyRange)
|
|
112
|
+
return baseTime;
|
|
113
|
+
const overlapMs = computeOverlapMs(series);
|
|
114
|
+
if (series.direction === 'backward')
|
|
115
|
+
return Math.min(baseTime + overlapMs, Date.now());
|
|
116
|
+
return Math.max(0, baseTime - overlapMs);
|
|
117
|
+
};
|
|
118
|
+
const computeBackoffMs = (current) => {
|
|
119
|
+
const next = current <= 0 ? CONFIG.backoffStepMs : Math.min(CONFIG.maxBackoffMs, current + CONFIG.backoffStepMs);
|
|
120
|
+
return next;
|
|
121
|
+
};
|
|
122
|
+
const enqueueCapJob = (capKey, kind, seriesKey) => {
|
|
123
|
+
const capState = getOrCreateCapState(capKey);
|
|
124
|
+
const pendingSet = kind === 'head' ? capState.pendingHead : capState.pendingTail;
|
|
125
|
+
const queue = kind === 'head' ? capState.headQueue : capState.tailQueue;
|
|
126
|
+
if (pendingSet.has(seriesKey))
|
|
127
|
+
return;
|
|
128
|
+
pendingSet.add(seriesKey);
|
|
129
|
+
queue.enqueue({ kind, seriesKey });
|
|
130
|
+
};
|
|
131
|
+
const scheduleSeries = (series) => {
|
|
132
|
+
const now = Date.now();
|
|
133
|
+
const headLagMs = computeHeadLagMs(series);
|
|
134
|
+
const needHead = series.union_end_ms === undefined || now - series.union_end_ms > headLagMs;
|
|
135
|
+
if (needHead) {
|
|
136
|
+
enqueueCapJob(series.capKey, 'head', series.seriesKey);
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
if (series.union_start_ms === undefined)
|
|
140
|
+
return;
|
|
141
|
+
enqueueCapJob(series.capKey, 'tail', series.seriesKey);
|
|
142
|
+
};
|
|
143
|
+
const mergeRangesAndGetUnion = async (series_id, table_name) => {
|
|
144
|
+
const rows = await requestSQL(terminal, `
|
|
145
|
+
WITH locked AS (
|
|
146
|
+
SELECT series_id, table_name, start_time, end_time
|
|
147
|
+
FROM series_data_range
|
|
148
|
+
WHERE series_id = ${escapeSQL(series_id)} AND table_name = ${escapeSQL(table_name)}
|
|
149
|
+
ORDER BY start_time ASC, end_time ASC
|
|
150
|
+
FOR UPDATE
|
|
151
|
+
),
|
|
152
|
+
ordered AS (
|
|
153
|
+
SELECT
|
|
154
|
+
start_time,
|
|
155
|
+
end_time,
|
|
156
|
+
max(end_time) OVER (
|
|
157
|
+
ORDER BY start_time ASC, end_time ASC
|
|
158
|
+
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
|
159
|
+
) AS running_end
|
|
160
|
+
FROM locked
|
|
161
|
+
),
|
|
162
|
+
marks AS (
|
|
163
|
+
SELECT
|
|
164
|
+
start_time,
|
|
165
|
+
end_time,
|
|
166
|
+
running_end,
|
|
167
|
+
CASE
|
|
168
|
+
WHEN start_time >= COALESCE(
|
|
169
|
+
lag(running_end) OVER (ORDER BY start_time ASC, end_time ASC),
|
|
170
|
+
'-infinity'::timestamptz
|
|
171
|
+
) THEN 1
|
|
172
|
+
ELSE 0
|
|
173
|
+
END AS is_new_group
|
|
174
|
+
FROM ordered
|
|
175
|
+
),
|
|
176
|
+
groups AS (
|
|
177
|
+
SELECT
|
|
178
|
+
start_time,
|
|
179
|
+
end_time,
|
|
180
|
+
sum(is_new_group) OVER (
|
|
181
|
+
ORDER BY start_time ASC, end_time ASC
|
|
182
|
+
ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW
|
|
183
|
+
) AS grp
|
|
184
|
+
FROM marks
|
|
185
|
+
),
|
|
186
|
+
merged AS (
|
|
187
|
+
SELECT min(start_time) AS start_time, max(end_time) AS end_time
|
|
188
|
+
FROM groups
|
|
189
|
+
GROUP BY grp
|
|
190
|
+
),
|
|
191
|
+
to_delete AS (
|
|
192
|
+
SELECT l.series_id, l.table_name, l.start_time, l.end_time
|
|
193
|
+
FROM locked l
|
|
194
|
+
WHERE NOT EXISTS (
|
|
195
|
+
SELECT 1
|
|
196
|
+
FROM merged m
|
|
197
|
+
WHERE m.start_time = l.start_time AND m.end_time = l.end_time
|
|
198
|
+
)
|
|
199
|
+
),
|
|
200
|
+
deleted AS (
|
|
201
|
+
DELETE FROM series_data_range t
|
|
202
|
+
USING to_delete d
|
|
203
|
+
WHERE
|
|
204
|
+
t.series_id = d.series_id
|
|
205
|
+
AND t.table_name = d.table_name
|
|
206
|
+
AND t.start_time = d.start_time
|
|
207
|
+
AND t.end_time = d.end_time
|
|
208
|
+
RETURNING 1
|
|
209
|
+
),
|
|
210
|
+
to_insert AS (
|
|
211
|
+
SELECT m.start_time, m.end_time
|
|
212
|
+
FROM merged m
|
|
213
|
+
WHERE NOT EXISTS (
|
|
214
|
+
SELECT 1
|
|
215
|
+
FROM locked l
|
|
216
|
+
WHERE l.start_time = m.start_time AND l.end_time = m.end_time
|
|
217
|
+
)
|
|
218
|
+
),
|
|
219
|
+
inserted AS (
|
|
220
|
+
INSERT INTO series_data_range (series_id, table_name, start_time, end_time)
|
|
221
|
+
SELECT ${escapeSQL(series_id)}, ${escapeSQL(table_name)}, start_time, end_time
|
|
222
|
+
FROM to_insert
|
|
223
|
+
ON CONFLICT DO NOTHING
|
|
224
|
+
RETURNING 1
|
|
225
|
+
)
|
|
226
|
+
SELECT start_time, end_time
|
|
227
|
+
FROM series_data_range
|
|
228
|
+
WHERE series_id = ${escapeSQL(series_id)} AND table_name = ${escapeSQL(table_name)}
|
|
229
|
+
ORDER BY start_time ASC, end_time ASC;
|
|
230
|
+
`);
|
|
231
|
+
if (rows.length === 0)
|
|
232
|
+
return { segments: [] };
|
|
233
|
+
const segments = [];
|
|
234
|
+
let startMs = Number.POSITIVE_INFINITY;
|
|
235
|
+
let endMs = Number.NEGATIVE_INFINITY;
|
|
236
|
+
for (const r of rows) {
|
|
237
|
+
const s = Date.parse(r.start_time);
|
|
238
|
+
const e = Date.parse(r.end_time);
|
|
239
|
+
if (!isNaN(s) && !isNaN(e) && e >= s) {
|
|
240
|
+
segments.push({ startMs: s, endMs: e });
|
|
241
|
+
startMs = Math.min(startMs, s);
|
|
242
|
+
endMs = Math.max(endMs, e);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
if (!isFinite(startMs) || !isFinite(endMs))
|
|
246
|
+
return { segments };
|
|
247
|
+
return { segments, union: { startMs, endMs } };
|
|
248
|
+
};
|
|
249
|
+
const findNearestGap = (segments) => {
|
|
250
|
+
for (let i = segments.length - 2; i >= 0; i--) {
|
|
251
|
+
const left = segments[i];
|
|
252
|
+
const right = segments[i + 1];
|
|
253
|
+
if (left.endMs < right.startMs)
|
|
254
|
+
return { left, right };
|
|
255
|
+
}
|
|
256
|
+
};
|
|
257
|
+
const computeTailTime = (series, now) => {
|
|
258
|
+
var _a, _b, _c, _d, _e;
|
|
259
|
+
const gap = findNearestGap(series.ranges);
|
|
260
|
+
if (gap) {
|
|
261
|
+
return series.direction === 'backward' ? gap.right.startMs : gap.left.endMs;
|
|
262
|
+
}
|
|
263
|
+
const first = series.ranges[0];
|
|
264
|
+
const mostRecent = series.ranges[series.ranges.length - 1];
|
|
265
|
+
if (series.direction === 'backward') {
|
|
266
|
+
return (_c = (_b = (_a = series.union_start_ms) !== null && _a !== void 0 ? _a : first === null || first === void 0 ? void 0 : first.startMs) !== null && _b !== void 0 ? _b : mostRecent === null || mostRecent === void 0 ? void 0 : mostRecent.startMs) !== null && _c !== void 0 ? _c : now;
|
|
267
|
+
}
|
|
268
|
+
const windowMs = (_d = series.last_window_ms) !== null && _d !== void 0 ? _d : CONFIG.defaultForwardSeedWindowMs;
|
|
269
|
+
const start = (_e = series.union_start_ms) !== null && _e !== void 0 ? _e : Math.max(0, now - windowMs);
|
|
270
|
+
return Math.max(0, start - windowMs);
|
|
271
|
+
};
|
|
272
|
+
const computeRequestTime = (series, kind) => {
|
|
273
|
+
var _a;
|
|
274
|
+
const now = Date.now();
|
|
275
|
+
if (series.direction === 'backward') {
|
|
276
|
+
if (kind === 'head')
|
|
277
|
+
return applyOverlapToRequestTime(series, now);
|
|
278
|
+
return applyOverlapToRequestTime(series, computeTailTime(series, now));
|
|
279
|
+
}
|
|
280
|
+
const windowMs = (_a = series.last_window_ms) !== null && _a !== void 0 ? _a : CONFIG.defaultForwardSeedWindowMs;
|
|
281
|
+
if (kind === 'head') {
|
|
282
|
+
if (series.union_end_ms !== undefined)
|
|
283
|
+
return applyOverlapToRequestTime(series, series.union_end_ms);
|
|
284
|
+
return applyOverlapToRequestTime(series, Math.max(0, now - windowMs));
|
|
285
|
+
}
|
|
286
|
+
return applyOverlapToRequestTime(series, computeTailTime(series, now));
|
|
287
|
+
};
|
|
288
|
+
const requestIngest = async (series, time) => {
|
|
289
|
+
var _a;
|
|
290
|
+
if (series.seriesType === 'ohlc') {
|
|
291
|
+
const req = {
|
|
292
|
+
product_id: series.product_id,
|
|
293
|
+
duration: (_a = series.duration) !== null && _a !== void 0 ? _a : '',
|
|
294
|
+
direction: series.direction,
|
|
295
|
+
time,
|
|
296
|
+
};
|
|
297
|
+
return terminal.client.requestForResponseData('IngestOHLC', req);
|
|
298
|
+
}
|
|
299
|
+
const req = {
|
|
300
|
+
product_id: series.product_id,
|
|
301
|
+
direction: series.direction,
|
|
302
|
+
time,
|
|
303
|
+
};
|
|
304
|
+
return terminal.client.requestForResponseData('IngestInterestRate', req);
|
|
305
|
+
};
|
|
306
|
+
const executeJob = async (capState, job) => {
|
|
307
|
+
const series = mapSeriesKeyToState.get(job.seriesKey);
|
|
308
|
+
if (!series)
|
|
309
|
+
return;
|
|
310
|
+
if (job.kind === 'head')
|
|
311
|
+
capState.pendingHead.delete(job.seriesKey);
|
|
312
|
+
else
|
|
313
|
+
capState.pendingTail.delete(job.seriesKey);
|
|
314
|
+
try {
|
|
315
|
+
// make sure each request overlaps existing ranges
|
|
316
|
+
const time = computeRequestTime(series, job.kind);
|
|
317
|
+
const result = await requestIngest(series, time);
|
|
318
|
+
capState.backoffMs = 0;
|
|
319
|
+
capState.nextEligibleAt = 0;
|
|
320
|
+
if (!result.range) {
|
|
321
|
+
capState.backoffMs = computeBackoffMs(capState.backoffMs);
|
|
322
|
+
capState.nextEligibleAt = Date.now() + capState.backoffMs;
|
|
323
|
+
scheduleSeries(series);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
const startMs = Date.parse(result.range.start_time);
|
|
327
|
+
const endMs = Date.parse(result.range.end_time);
|
|
328
|
+
if (!isNaN(startMs) && !isNaN(endMs) && endMs > startMs) {
|
|
329
|
+
series.last_window_ms = endMs - startMs;
|
|
330
|
+
}
|
|
331
|
+
const merged = await mergeRangesAndGetUnion(series.series_id, series.table_name);
|
|
332
|
+
series.ranges = merged.segments;
|
|
333
|
+
if (merged.union) {
|
|
334
|
+
series.union_start_ms = merged.union.startMs;
|
|
335
|
+
series.union_end_ms = merged.union.endMs;
|
|
336
|
+
}
|
|
337
|
+
scheduleSeries(series);
|
|
338
|
+
}
|
|
339
|
+
catch (e) {
|
|
340
|
+
capState.backoffMs = computeBackoffMs(capState.backoffMs);
|
|
341
|
+
capState.nextEligibleAt = Date.now() + capState.backoffMs;
|
|
342
|
+
console.warn(formatTime(Date.now()), '[VEX][SeriesData]CapFailed', `cap=${capState.capKey}`, `backoff_ms=${capState.backoffMs}`, `${e}`);
|
|
343
|
+
scheduleSeries(series);
|
|
344
|
+
}
|
|
345
|
+
};
|
|
346
|
+
const computeGlobalHeadBacklog = () => {
|
|
347
|
+
let total = 0;
|
|
348
|
+
for (const s of mapCapKeyToState.values()) {
|
|
349
|
+
total += s.headQueue.size();
|
|
350
|
+
}
|
|
351
|
+
return total;
|
|
352
|
+
};
|
|
353
|
+
const pickNextRunnableCap = (now) => {
|
|
354
|
+
if (capabilities.length === 0)
|
|
355
|
+
return;
|
|
356
|
+
const n = capabilities.length;
|
|
357
|
+
for (let i = 0; i < n; i++) {
|
|
358
|
+
// Round-robin pick
|
|
359
|
+
const cap = capabilities[capRunIndex++ % n];
|
|
360
|
+
const capState = getOrCreateCapState(cap.capKey);
|
|
361
|
+
if (capState.inflight)
|
|
362
|
+
continue;
|
|
363
|
+
if (capState.nextEligibleAt > now)
|
|
364
|
+
continue;
|
|
365
|
+
if (capState.headQueue.size() === 0 && capState.tailQueue.size() === 0)
|
|
366
|
+
continue;
|
|
367
|
+
return capState;
|
|
368
|
+
}
|
|
369
|
+
};
|
|
370
|
+
const dequeueFromCap = (capState, canRunTail) => {
|
|
371
|
+
const head = capState.headQueue.dequeue();
|
|
372
|
+
if (head)
|
|
373
|
+
return { job: head, kind: 'head' };
|
|
374
|
+
if (!canRunTail)
|
|
375
|
+
return;
|
|
376
|
+
const tail = capState.tailQueue.dequeue();
|
|
377
|
+
if (!tail)
|
|
378
|
+
return;
|
|
379
|
+
return { job: tail, kind: 'tail' };
|
|
380
|
+
};
|
|
381
|
+
const scanProductsOnce = async () => {
|
|
382
|
+
var _a;
|
|
383
|
+
if (capabilities.length === 0)
|
|
384
|
+
return;
|
|
385
|
+
const now = Date.now();
|
|
386
|
+
const cap = capabilities[scanIndex++ % capabilities.length];
|
|
387
|
+
if (cap.nextScanAt > now)
|
|
388
|
+
return;
|
|
389
|
+
const where = [
|
|
390
|
+
`product_id LIKE ${escapeSQL(`${cap.product_id_prefix}%`)}`,
|
|
391
|
+
onlyProductIdPrefix ? `product_id LIKE ${escapeSQL(`${onlyProductIdPrefix}%`)}` : '',
|
|
392
|
+
cap.seriesType === 'interest_rate' ? `COALESCE(no_interest_rate, false) = false` : '',
|
|
393
|
+
]
|
|
394
|
+
.filter(Boolean)
|
|
395
|
+
.join(' AND ');
|
|
396
|
+
const rows = await requestSQL(terminal, `
|
|
397
|
+
SELECT product_id
|
|
398
|
+
FROM product
|
|
399
|
+
WHERE ${where}
|
|
400
|
+
ORDER BY product_id ASC
|
|
401
|
+
`);
|
|
402
|
+
cap.nextScanAt = now + CONFIG.scanFullLoopIntervalMs;
|
|
403
|
+
for (const row of rows) {
|
|
404
|
+
if (cap.seriesType === 'ohlc') {
|
|
405
|
+
for (const duration of (_a = cap.duration_list) !== null && _a !== void 0 ? _a : []) {
|
|
406
|
+
const series = getOrCreateSeriesState({
|
|
407
|
+
capKey: cap.capKey,
|
|
408
|
+
seriesType: 'ohlc',
|
|
409
|
+
product_id: row.product_id,
|
|
410
|
+
duration,
|
|
411
|
+
direction: cap.direction,
|
|
412
|
+
});
|
|
413
|
+
scheduleSeries(series);
|
|
414
|
+
}
|
|
415
|
+
}
|
|
416
|
+
else {
|
|
417
|
+
const series = getOrCreateSeriesState({
|
|
418
|
+
capKey: cap.capKey,
|
|
419
|
+
seriesType: 'interest_rate',
|
|
420
|
+
product_id: row.product_id,
|
|
421
|
+
direction: cap.direction,
|
|
422
|
+
});
|
|
423
|
+
scheduleSeries(series);
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
};
|
|
427
|
+
const tick = async () => {
|
|
428
|
+
if (!isEnabled)
|
|
429
|
+
return;
|
|
430
|
+
try {
|
|
431
|
+
await scanProductsOnce();
|
|
432
|
+
}
|
|
433
|
+
catch (e) {
|
|
434
|
+
console.error(formatTime(Date.now()), '[VEX][SeriesData]ScanFailed', `${e}`);
|
|
435
|
+
}
|
|
436
|
+
const now = Date.now();
|
|
437
|
+
while (inflight < CONFIG.maxInflight) {
|
|
438
|
+
const capState = pickNextRunnableCap(now);
|
|
439
|
+
if (!capState)
|
|
440
|
+
return;
|
|
441
|
+
const globalHeadBacklog = computeGlobalHeadBacklog();
|
|
442
|
+
const canRunTail = globalHeadBacklog < CONFIG.tailOnlyWhenGlobalHeadBelow && capState.headQueue.size() === 0;
|
|
443
|
+
const next = dequeueFromCap(capState, canRunTail);
|
|
444
|
+
if (!next)
|
|
445
|
+
return;
|
|
446
|
+
capState.inflight = true;
|
|
447
|
+
inflight++;
|
|
448
|
+
executeJob(capState, next.job).finally(() => {
|
|
449
|
+
capState.inflight = false;
|
|
450
|
+
inflight--;
|
|
451
|
+
});
|
|
452
|
+
}
|
|
453
|
+
};
|
|
454
|
+
if (!isEnabled) {
|
|
455
|
+
console.info(formatTime(Date.now()), '[VEX][SeriesData]Disabled', 'VEX_SERIES_DATA_ENABLED!=1');
|
|
456
|
+
}
|
|
457
|
+
else {
|
|
458
|
+
terminal.server.provideService('VEX/SeriesData/Peek', {}, async () => {
|
|
459
|
+
const now = Date.now();
|
|
460
|
+
const caps = capabilities.map((cap) => {
|
|
461
|
+
const s = getOrCreateCapState(cap.capKey);
|
|
462
|
+
return {
|
|
463
|
+
capKey: cap.capKey,
|
|
464
|
+
method: cap.method,
|
|
465
|
+
product_id_prefix: cap.product_id_prefix,
|
|
466
|
+
direction: cap.direction,
|
|
467
|
+
head_queue_size: s.headQueue.size(),
|
|
468
|
+
tail_queue_size: s.tailQueue.size(),
|
|
469
|
+
inflight: s.inflight,
|
|
470
|
+
backoff_ms: s.backoffMs,
|
|
471
|
+
nextEligibleAt: s.nextEligibleAt > now ? formatTime(s.nextEligibleAt) : undefined,
|
|
472
|
+
};
|
|
473
|
+
});
|
|
474
|
+
return {
|
|
475
|
+
res: {
|
|
476
|
+
code: 0,
|
|
477
|
+
message: 'OK',
|
|
478
|
+
data: {
|
|
479
|
+
enabled: true,
|
|
480
|
+
only_product_id_prefix: onlyProductIdPrefix || undefined,
|
|
481
|
+
inflight,
|
|
482
|
+
cap_count: capabilities.length,
|
|
483
|
+
global_head_backlog: computeGlobalHeadBacklog(),
|
|
484
|
+
series_count: mapSeriesKeyToState.size,
|
|
485
|
+
caps: caps.slice(0, 50),
|
|
486
|
+
},
|
|
487
|
+
},
|
|
488
|
+
};
|
|
489
|
+
});
|
|
490
|
+
// Setup trace log
|
|
491
|
+
timer(0, LOG_QUEUE_INTERVAL_MS).subscribe(() => {
|
|
492
|
+
const now = Date.now();
|
|
493
|
+
const caps = capabilities
|
|
494
|
+
.map((cap) => {
|
|
495
|
+
const s = getOrCreateCapState(cap.capKey);
|
|
496
|
+
return {
|
|
497
|
+
capKey: cap.capKey,
|
|
498
|
+
head: s.headQueue.size(),
|
|
499
|
+
tail: s.tailQueue.size(),
|
|
500
|
+
inflight: s.inflight,
|
|
501
|
+
backoffMs: s.backoffMs,
|
|
502
|
+
nextEligibleAt: s.nextEligibleAt,
|
|
503
|
+
};
|
|
504
|
+
})
|
|
505
|
+
.filter((x) => x.inflight || x.head > 0 || x.tail > 0 || x.nextEligibleAt > now)
|
|
506
|
+
.slice(0, 20)
|
|
507
|
+
.map((x) => `cap=${x.capKey} head=${x.head} tail=${x.tail} inflight=${x.inflight ? 1 : 0} backoff_ms=${x.backoffMs}${x.nextEligibleAt > now ? ` next=${formatTime(x.nextEligibleAt)}` : ''}`);
|
|
508
|
+
console.info(formatTime(now), '[VEX][SeriesData]Queues', `inflight=${inflight}`, `cap_count=${capabilities.length}`, `global_head_backlog=${computeGlobalHeadBacklog()}`, `series_count=${mapSeriesKeyToState.size}`, caps.length ? `caps=[${caps.join(' | ')}]` : 'caps=[]');
|
|
509
|
+
});
|
|
510
|
+
terminal.terminalInfos$
|
|
511
|
+
.pipe(mergeMap((terminalInfos) => from(terminalInfos).pipe(mergeMap((terminalInfo) => from(Object.values(terminalInfo.serviceInfo || {})).pipe(filter((serviceInfo) => serviceInfo.method === 'IngestOHLC' || serviceInfo.method === 'IngestInterestRate'), map((serviceInfo) => {
|
|
512
|
+
try {
|
|
513
|
+
if (serviceInfo.method === 'IngestOHLC') {
|
|
514
|
+
const meta = parseOHLCServiceMetadataFromSchema(serviceInfo.schema);
|
|
515
|
+
const capKey = encodePath('IngestOHLC', meta.product_id_prefix, meta.direction);
|
|
516
|
+
const existing = mapCapKeyToCapability.get(capKey);
|
|
517
|
+
const duration_list = [...new Set(meta.duration_list)].sort();
|
|
518
|
+
if (existing) {
|
|
519
|
+
existing.duration_list = duration_list;
|
|
520
|
+
return existing;
|
|
521
|
+
}
|
|
522
|
+
return {
|
|
523
|
+
capKey,
|
|
524
|
+
method: 'IngestOHLC',
|
|
525
|
+
seriesType: 'ohlc',
|
|
526
|
+
product_id_prefix: meta.product_id_prefix,
|
|
527
|
+
direction: meta.direction,
|
|
528
|
+
duration_list,
|
|
529
|
+
nextScanAt: 0,
|
|
530
|
+
};
|
|
531
|
+
}
|
|
532
|
+
const meta = parseInterestRateServiceMetadataFromSchema(serviceInfo.schema);
|
|
533
|
+
const capKey = encodePath('IngestInterestRate', meta.product_id_prefix, meta.direction);
|
|
534
|
+
const existing = mapCapKeyToCapability.get(capKey);
|
|
535
|
+
if (existing)
|
|
536
|
+
return existing;
|
|
537
|
+
return {
|
|
538
|
+
capKey,
|
|
539
|
+
method: 'IngestInterestRate',
|
|
540
|
+
seriesType: 'interest_rate',
|
|
541
|
+
product_id_prefix: meta.product_id_prefix,
|
|
542
|
+
direction: meta.direction,
|
|
543
|
+
nextScanAt: 0,
|
|
544
|
+
};
|
|
545
|
+
}
|
|
546
|
+
catch (_a) {
|
|
547
|
+
console.info(formatTime(Date.now()), '[VEX][SeriesData]ParseServiceMetadataFailed', `method=${serviceInfo.method}`, `terminal_id=${terminalInfo.terminal_id}`);
|
|
548
|
+
return;
|
|
549
|
+
}
|
|
550
|
+
}), filter((x) => !!x))), toArray(), tap((nextCaps) => {
|
|
551
|
+
const nextMap = new Map();
|
|
552
|
+
for (const cap of nextCaps) {
|
|
553
|
+
nextMap.set(cap.capKey, cap);
|
|
554
|
+
}
|
|
555
|
+
mapCapKeyToCapability.clear();
|
|
556
|
+
nextMap.forEach((v, k) => mapCapKeyToCapability.set(k, v));
|
|
557
|
+
capabilities.length = 0;
|
|
558
|
+
nextMap.forEach((v) => capabilities.push(v));
|
|
559
|
+
}))))
|
|
560
|
+
.subscribe();
|
|
561
|
+
defer(() => from(tick()))
|
|
562
|
+
.pipe(catchError((e) => {
|
|
563
|
+
console.error(formatTime(Date.now()), '[VEX][SeriesData]TickError', `${e}`);
|
|
564
|
+
return EMPTY;
|
|
565
|
+
}), repeat({ delay: CONFIG.tickIntervalMs }))
|
|
566
|
+
.subscribe();
|
|
567
|
+
}
|
|
568
|
+
//# sourceMappingURL=scheduler.js.map
|