@yuants/app-virtual-exchange 0.9.3 → 0.10.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.
Files changed (45) hide show
  1. package/dist/position.js +17 -0
  2. package/dist/position.js.map +1 -1
  3. package/dist/quote/__tests__/implementations.test.js +218 -0
  4. package/dist/quote/__tests__/implementations.test.js.map +1 -0
  5. package/dist/quote/implementations/v0.js +7 -9
  6. package/dist/quote/implementations/v0.js.map +1 -1
  7. package/dist/quote/implementations/v1.js +10 -4
  8. package/dist/quote/implementations/v1.js.map +1 -1
  9. package/dist/quote/prefix-matcher.js +7 -0
  10. package/dist/quote/prefix-matcher.js.map +1 -0
  11. package/dist/quote/request-key.js +20 -0
  12. package/dist/quote/request-key.js.map +1 -0
  13. package/dist/quote/service.js +38 -15
  14. package/dist/quote/service.js.map +1 -1
  15. package/dist/quote/upstream-routing.js +300 -0
  16. package/dist/quote/upstream-routing.js.map +1 -0
  17. package/lib/position.d.ts.map +1 -1
  18. package/lib/position.js +17 -0
  19. package/lib/position.js.map +1 -1
  20. package/lib/quote/__tests__/implementations.test.d.ts +2 -0
  21. package/lib/quote/__tests__/implementations.test.d.ts.map +1 -0
  22. package/lib/quote/__tests__/implementations.test.js +220 -0
  23. package/lib/quote/__tests__/implementations.test.js.map +1 -0
  24. package/lib/quote/implementations/v0.d.ts.map +1 -1
  25. package/lib/quote/implementations/v0.js +7 -9
  26. package/lib/quote/implementations/v0.js.map +1 -1
  27. package/lib/quote/implementations/v1.d.ts.map +1 -1
  28. package/lib/quote/implementations/v1.js +10 -4
  29. package/lib/quote/implementations/v1.js.map +1 -1
  30. package/lib/quote/prefix-matcher.d.ts +8 -0
  31. package/lib/quote/prefix-matcher.d.ts.map +1 -0
  32. package/lib/quote/prefix-matcher.js +11 -0
  33. package/lib/quote/prefix-matcher.js.map +1 -0
  34. package/lib/quote/request-key.d.ts +2 -0
  35. package/lib/quote/request-key.d.ts.map +1 -0
  36. package/lib/quote/request-key.js +24 -0
  37. package/lib/quote/request-key.js.map +1 -0
  38. package/lib/quote/service.js +38 -15
  39. package/lib/quote/service.js.map +1 -1
  40. package/lib/quote/upstream-routing.d.ts +15 -0
  41. package/lib/quote/upstream-routing.d.ts.map +1 -0
  42. package/lib/quote/upstream-routing.js +304 -0
  43. package/lib/quote/upstream-routing.js.map +1 -0
  44. package/package.json +11 -11
  45. package/temp/package-deps.json +21 -17
@@ -0,0 +1,300 @@
1
+ import { parseQuoteServiceMetadataFromSchema, } from '@yuants/exchange';
2
+ import { Terminal } from '@yuants/protocol';
3
+ import { encodePath, formatTime, listWatch, newError } from '@yuants/utils';
4
+ import { EMPTY, filter, firstValueFrom, from, map, mergeMap, of, tap, toArray } from 'rxjs';
5
+ import { createSortedPrefixMatcher } from './prefix-matcher';
6
+ import { fnv1a64HexFromStrings } from './request-key';
7
+ const terminal = Terminal.fromNodeEnv();
8
+ const quoteServiceInfos$ = terminal.terminalInfos$.pipe(mergeMap((infos) => from(infos).pipe(
9
+ //
10
+ mergeMap((info) => {
11
+ var _a;
12
+ return from(Object.values((_a = info.serviceInfo) !== null && _a !== void 0 ? _a : {})).pipe(filter((serviceInfo) => serviceInfo.method === 'GetQuotes'), map((serviceInfo) => ({
13
+ terminal_id: info.terminal_id,
14
+ serviceInfo,
15
+ })), toArray());
16
+ }))));
17
+ const mapGroupIdToGroup = new Map();
18
+ /**
19
+ * Build provider groups from runtime terminal infos.
20
+ *
21
+ * Note: `fields` is schema `const`, so VEX must keep a stable order (lexicographical sort).
22
+ */
23
+ quoteServiceInfos$
24
+ .pipe(listWatch((v) => v.serviceInfo.service_id, (v) => {
25
+ var _a;
26
+ console.info(formatTime(Date.now()), `[VEX][QUOTE]DiscoveringGetQuotesProvider...`, `from terminal ${v.terminal_id}`, `service ${v.serviceInfo.service_id}`, `schema: ${JSON.stringify(v.serviceInfo.schema)}`);
27
+ try {
28
+ const metadata = parseQuoteServiceMetadataFromSchema(v.serviceInfo.schema);
29
+ const fields = [...metadata.fields].sort();
30
+ const group_id = encodePath(metadata.product_id_prefix, fields.join(','), (_a = metadata.max_products_per_request) !== null && _a !== void 0 ? _a : '');
31
+ const provider = {
32
+ terminal_id: v.terminal_id,
33
+ service_id: v.serviceInfo.service_id || v.serviceInfo.method,
34
+ };
35
+ if (mapGroupIdToGroup.get(group_id)) {
36
+ mapGroupIdToGroup.get(group_id).mapTerminalIdToInstance.set(provider.terminal_id, provider);
37
+ }
38
+ else {
39
+ const next = {
40
+ group_id,
41
+ meta: metadata,
42
+ mapTerminalIdToInstance: new Map([[provider.terminal_id, provider]]),
43
+ };
44
+ mapGroupIdToGroup.set(group_id, next);
45
+ console.info('11111111', [...mapGroupIdToGroup.values()]);
46
+ }
47
+ return of(void 0).pipe(
48
+ //
49
+ tap({
50
+ unsubscribe: () => {
51
+ var _a, _b;
52
+ (_a = mapGroupIdToGroup.get(group_id)) === null || _a === void 0 ? void 0 : _a.mapTerminalIdToInstance.delete(v.terminal_id);
53
+ if (((_b = mapGroupIdToGroup.get(group_id)) === null || _b === void 0 ? void 0 : _b.mapTerminalIdToInstance.size) === 0) {
54
+ mapGroupIdToGroup.delete(group_id);
55
+ }
56
+ },
57
+ }));
58
+ }
59
+ catch (_b) {
60
+ // Ignore invalid schemas/providers
61
+ console.info(`[VEX][Quote] Ignored GetQuotes provider from terminal ${v.terminal_id} `, `service ${v.serviceInfo.service_id} due to invalid schema.`);
62
+ return EMPTY;
63
+ }
64
+ }))
65
+ .subscribe();
66
+ // -----------------------------------------------------------------------------
67
+ // Load balancing & upstream request execution
68
+ // -----------------------------------------------------------------------------
69
+ const mapGroupIdToRoundRobinIndex = new Map();
70
+ const pickInstance = (group_id, instances) => {
71
+ var _a;
72
+ if (instances.length === 0)
73
+ throw newError('VEX_QUOTE_PROVIDER_INSTANCE_EMPTY', { group_id });
74
+ const nextIndex = ((_a = mapGroupIdToRoundRobinIndex.get(group_id)) !== null && _a !== void 0 ? _a : 0) % instances.length;
75
+ mapGroupIdToRoundRobinIndex.set(group_id, nextIndex + 1);
76
+ return instances[nextIndex];
77
+ };
78
+ /**
79
+ * Call a specified vendor terminal + service instance.
80
+ *
81
+ * Any non-0 response is treated as fatal (strict freshness requirement).
82
+ */
83
+ const requestGetQuotes = async (terminal, instance, req) => {
84
+ const res = await firstValueFrom(terminal.client
85
+ .request('GetQuotes', instance.terminal_id, req, instance.service_id)
86
+ .pipe(map((msg) => msg.res), filter((v) => v !== undefined)));
87
+ if (res.code !== 0) {
88
+ throw newError('VEX_QUOTE_PROVIDER_ERROR', { instance, res });
89
+ }
90
+ if (res.data === undefined) {
91
+ throw newError('VEX_QUOTE_PROVIDER_DATA_MISSING', { instance, res });
92
+ }
93
+ return res.data;
94
+ };
95
+ /**
96
+ * Per-provider (group_id) concurrency limit: 1.
97
+ * Implemented as a per-group promise tail.
98
+ */
99
+ const mapGroupIdToTailPromise = new Map();
100
+ const runWithProviderGroupConcurrencyLimit1 = async (group_id, fn) => {
101
+ var _a;
102
+ const prev = (_a = mapGroupIdToTailPromise.get(group_id)) !== null && _a !== void 0 ? _a : Promise.resolve();
103
+ let resolveCurrent = () => { };
104
+ const current = new Promise((resolve) => {
105
+ resolveCurrent = resolve;
106
+ });
107
+ mapGroupIdToTailPromise.set(group_id, prev.then(() => current));
108
+ await prev;
109
+ try {
110
+ return await fn();
111
+ }
112
+ finally {
113
+ resolveCurrent();
114
+ }
115
+ };
116
+ /**
117
+ * A tiny async limiter: used as a global concurrency cap to avoid request explosions.
118
+ */
119
+ const createConcurrencyLimiter = (concurrency) => {
120
+ const queue = [];
121
+ let active = 0;
122
+ const next = () => {
123
+ if (active >= concurrency)
124
+ return;
125
+ const task = queue.shift();
126
+ if (!task)
127
+ return;
128
+ active++;
129
+ task();
130
+ };
131
+ return async (fn) => {
132
+ return await new Promise((resolve, reject) => {
133
+ queue.push(async () => {
134
+ try {
135
+ resolve(await fn());
136
+ }
137
+ catch (e) {
138
+ reject(e);
139
+ }
140
+ finally {
141
+ active--;
142
+ next();
143
+ }
144
+ });
145
+ next();
146
+ });
147
+ };
148
+ };
149
+ // Global concurrency cap for upstream `GetQuotes` calls (provider-level cap is handled separately).
150
+ const limitGetQuotes = createConcurrencyLimiter(32);
151
+ /**
152
+ * In-flight dedup:
153
+ * Same (provider group + product batch) should share a single upstream request promise.
154
+ */
155
+ const mapKeyToInFlightGetQuotesPromise = new Map();
156
+ const requestGetQuotesInFlight = (terminal, key, planned) => {
157
+ const existing = mapKeyToInFlightGetQuotesPromise.get(key);
158
+ if (existing)
159
+ return existing;
160
+ const promise = limitGetQuotes(() => runWithProviderGroupConcurrencyLimit1(planned.group_id, async () => {
161
+ const instance = pickInstance(planned.group_id, planned.instances);
162
+ return await requestGetQuotes(terminal, instance, planned.req);
163
+ })).finally(() => {
164
+ mapKeyToInFlightGetQuotesPromise.delete(key);
165
+ });
166
+ mapKeyToInFlightGetQuotesPromise.set(key, promise);
167
+ return promise;
168
+ };
169
+ const buildProviderIndices = (groups) => {
170
+ const mapGroupIdToGroup = new Map(groups.map((x) => [x.group_id, x]));
171
+ const prefixMatcher = createSortedPrefixMatcher(groups.map((group) => ({ prefix: group.meta.product_id_prefix, value: group.group_id })));
172
+ const mapFieldToGroupIds = new Map();
173
+ for (const group of groups) {
174
+ for (const field of group.meta.fields) {
175
+ let groupIds = mapFieldToGroupIds.get(field);
176
+ if (!groupIds) {
177
+ groupIds = new Set();
178
+ mapFieldToGroupIds.set(field, groupIds);
179
+ }
180
+ groupIds.add(group.group_id);
181
+ }
182
+ }
183
+ return { mapGroupIdToGroup, prefixMatcher, mapFieldToGroupIds };
184
+ };
185
+ /**
186
+ * L1 quote routing (per `docs/zh-Hans/code-guidelines/exchange.md`):
187
+ * For each missed (product_id, field), route to `S_product_id ∩ S_field`.
188
+ */
189
+ const routeMisses = (cacheMissed, indices, updated_at) => {
190
+ const { prefixMatcher, mapFieldToGroupIds } = indices;
191
+ const mapProductIdToGroupIds = new Map();
192
+ const productsByGroupId = new Map();
193
+ const unroutableProducts = new Set();
194
+ // Field unavailable: return "" but keep updated_at satisfied to avoid repeated misses.
195
+ const unavailableAction = {};
196
+ for (const miss of cacheMissed) {
197
+ const { product_id, field } = miss;
198
+ let productGroupIds = mapProductIdToGroupIds.get(product_id);
199
+ if (!productGroupIds) {
200
+ productGroupIds = prefixMatcher.match(product_id);
201
+ mapProductIdToGroupIds.set(product_id, productGroupIds);
202
+ }
203
+ if (productGroupIds.length === 0) {
204
+ unroutableProducts.add(product_id);
205
+ continue;
206
+ }
207
+ const fieldGroupIds = mapFieldToGroupIds.get(field);
208
+ if (!fieldGroupIds) {
209
+ if (!unavailableAction[product_id])
210
+ unavailableAction[product_id] = {};
211
+ unavailableAction[product_id][field] = ['', updated_at];
212
+ continue;
213
+ }
214
+ let matched = false;
215
+ for (const group_id of productGroupIds) {
216
+ if (!fieldGroupIds.has(group_id))
217
+ continue;
218
+ matched = true;
219
+ let productIds = productsByGroupId.get(group_id);
220
+ if (!productIds) {
221
+ productIds = new Set();
222
+ productsByGroupId.set(group_id, productIds);
223
+ }
224
+ productIds.add(product_id);
225
+ }
226
+ if (!matched) {
227
+ if (!unavailableAction[product_id])
228
+ unavailableAction[product_id] = {};
229
+ unavailableAction[product_id][field] = ['', updated_at];
230
+ }
231
+ }
232
+ return { productsByGroupId, unavailableAction, unroutableProducts };
233
+ };
234
+ const createRequestKey = (group_id, batchProductIds) => encodePath(group_id, fnv1a64HexFromStrings(batchProductIds));
235
+ const planRequests = (productsByGroupId, mapGroupIdToGroup) => {
236
+ var _a;
237
+ const plannedRequests = [];
238
+ for (const [group_id, productIdSet] of productsByGroupId) {
239
+ const group = mapGroupIdToGroup.get(group_id);
240
+ if (!group)
241
+ continue;
242
+ const sortedProductIds = [...productIdSet].sort();
243
+ const max = (_a = group.meta.max_products_per_request) !== null && _a !== void 0 ? _a : sortedProductIds.length;
244
+ for (let i = 0; i < sortedProductIds.length; i += max) {
245
+ const batchProductIds = sortedProductIds.slice(i, i + max);
246
+ const key = createRequestKey(group_id, batchProductIds);
247
+ plannedRequests.push({
248
+ key,
249
+ planned: {
250
+ group_id,
251
+ instances: Array.from(group.mapTerminalIdToInstance.values()),
252
+ req: { product_ids: batchProductIds, fields: group.meta.fields },
253
+ },
254
+ });
255
+ }
256
+ }
257
+ return plannedRequests;
258
+ };
259
+ export const fillQuoteStateFromUpstream = async (params) => {
260
+ const { terminal, quoteState, cacheMissed, updated_at } = params;
261
+ if (cacheMissed.length === 0)
262
+ return;
263
+ const providerGroups = Array.from(mapGroupIdToGroup.values());
264
+ console.info(formatTime(Date.now()), `[VEX][Quote]UpstreamProviderDiscovery`, ` Discovered ${providerGroups.length} GetQuotes provider groups from terminal infos.`, JSON.stringify(providerGroups));
265
+ if (providerGroups.length === 0) {
266
+ throw newError('VEX_QUOTE_PROVIDER_NOT_FOUND', { method: 'GetQuotes' });
267
+ }
268
+ const indices = buildProviderIndices(providerGroups);
269
+ const { productsByGroupId, unavailableAction, unroutableProducts } = routeMisses(cacheMissed, indices, updated_at);
270
+ console.info(formatTime(Date.now()), `[VEX][Quote]RouteDispatched`, ` Routed ${cacheMissed.length} missed quotes to ${productsByGroupId.size} provider groups, ` +
271
+ `${unroutableProducts.size} unroutable products.`, JSON.stringify({
272
+ productsByGroupId: [...productsByGroupId.entries()].map(([group_id, productIds]) => ({
273
+ group_id,
274
+ product_ids: [...productIds],
275
+ })),
276
+ unroutable_products: [...unroutableProducts],
277
+ unavailable_action: unavailableAction,
278
+ }));
279
+ if (unroutableProducts.size !== 0) {
280
+ throw newError('VEX_QUOTE_PRODUCT_UNROUTABLE', {
281
+ updated_at,
282
+ unroutable_products: [...unroutableProducts].slice(0, 200),
283
+ unroutable_products_total: unroutableProducts.size,
284
+ });
285
+ }
286
+ quoteState.update(unavailableAction);
287
+ const plannedRequests = planRequests(productsByGroupId, indices.mapGroupIdToGroup);
288
+ console.info(formatTime(Date.now()), `[VEX][Quote]RequestPlanned`, `Planned ${plannedRequests.length} upstream GetQuotes requests.`, JSON.stringify(plannedRequests.map(({ key, planned }) => ({
289
+ key,
290
+ group_id: planned.group_id,
291
+ product_ids: planned.req.product_ids,
292
+ fields: planned.req.fields,
293
+ }))));
294
+ const actions = await Promise.all(plannedRequests.map(async ({ key, planned }) => await requestGetQuotesInFlight(terminal, key, planned)));
295
+ console.debug(formatTime(Date.now()), `[VEX][Quote]RequestReceived`, `Received ${actions.length} upstream GetQuotes responses.`);
296
+ for (const action of actions) {
297
+ quoteState.update(action);
298
+ }
299
+ };
300
+ //# sourceMappingURL=upstream-routing.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"upstream-routing.js","sourceRoot":"","sources":["../../src/quote/upstream-routing.ts"],"names":[],"mappings":"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","sourcesContent":["import {\n IQuoteUpdateAction as IExchangeQuoteUpdateAction,\n IQuoteServiceMetadata,\n IQuoteServiceRequestByVEX,\n parseQuoteServiceMetadataFromSchema,\n} from '@yuants/exchange';\nimport { Terminal } from '@yuants/protocol';\nimport { encodePath, formatTime, listWatch, newError } from '@yuants/utils';\nimport { EMPTY, filter, firstValueFrom, from, map, mergeMap, of, tap, timer, toArray } from 'rxjs';\nimport { createSortedPrefixMatcher } from './prefix-matcher';\nimport { fnv1a64HexFromStrings } from './request-key';\nimport { IQuoteKey, IQuoteState, IQuoteUpdateAction } from './types';\n\nexport interface IQuoteMiss {\n product_id: string;\n field: IQuoteKey;\n}\n\n// -----------------------------------------------------------------------------\n// Provider discovery (schema-driven)\n// -----------------------------------------------------------------------------\n\ninterface IQuoteProviderInstance {\n terminal_id: string;\n service_id: string;\n}\n\n/**\n * A \"provider group\" is a capability signature of `GetQuotes`.\n * Multiple vendor terminals may provide the same capability; VEX load-balances across instances.\n */\ninterface IQuoteProviderGroup {\n group_id: string;\n meta: IQuoteServiceMetadata;\n mapTerminalIdToInstance: Map<string, IQuoteProviderInstance>;\n}\n\ntype IPlannedRequest = {\n group_id: string;\n instances: IQuoteProviderInstance[];\n req: IQuoteServiceRequestByVEX;\n};\n\nconst terminal = Terminal.fromNodeEnv();\n\nconst quoteServiceInfos$ = terminal.terminalInfos$.pipe(\n mergeMap((infos) =>\n from(infos).pipe(\n //\n mergeMap((info) =>\n from(Object.values(info.serviceInfo ?? {})).pipe(\n filter((serviceInfo) => serviceInfo.method === 'GetQuotes'),\n map((serviceInfo) => ({\n terminal_id: info.terminal_id,\n serviceInfo,\n })),\n toArray(),\n ),\n ),\n ),\n ),\n);\n\nconst mapGroupIdToGroup = new Map<string, IQuoteProviderGroup>();\n\n/**\n * Build provider groups from runtime terminal infos.\n *\n * Note: `fields` is schema `const`, so VEX must keep a stable order (lexicographical sort).\n */\nquoteServiceInfos$\n .pipe(\n listWatch(\n (v) => v.serviceInfo.service_id,\n (v) => {\n console.info(\n formatTime(Date.now()),\n `[VEX][QUOTE]DiscoveringGetQuotesProvider...`,\n `from terminal ${v.terminal_id}`,\n `service ${v.serviceInfo.service_id}`,\n `schema: ${JSON.stringify(v.serviceInfo.schema)}`,\n );\n try {\n const metadata = parseQuoteServiceMetadataFromSchema(v.serviceInfo.schema);\n const fields = [...(metadata.fields as unknown as IQuoteKey[])].sort();\n const group_id = encodePath(\n metadata.product_id_prefix,\n fields.join(','),\n metadata.max_products_per_request ?? '',\n );\n const provider: IQuoteProviderInstance = {\n terminal_id: v.terminal_id,\n service_id: v.serviceInfo.service_id || v.serviceInfo.method,\n };\n if (mapGroupIdToGroup.get(group_id)) {\n mapGroupIdToGroup.get(group_id)!.mapTerminalIdToInstance.set(provider.terminal_id, provider);\n } else {\n const next: IQuoteProviderGroup = {\n group_id,\n meta: metadata,\n mapTerminalIdToInstance: new Map([[provider.terminal_id, provider]]),\n };\n mapGroupIdToGroup.set(group_id, next);\n console.info('11111111', [...mapGroupIdToGroup.values()]);\n }\n return of(void 0).pipe(\n //\n tap({\n unsubscribe: () => {\n mapGroupIdToGroup.get(group_id)?.mapTerminalIdToInstance.delete(v.terminal_id);\n if (mapGroupIdToGroup.get(group_id)?.mapTerminalIdToInstance.size === 0) {\n mapGroupIdToGroup.delete(group_id);\n }\n },\n }),\n );\n } catch {\n // Ignore invalid schemas/providers\n console.info(\n `[VEX][Quote] Ignored GetQuotes provider from terminal ${v.terminal_id} `,\n `service ${v.serviceInfo.service_id} due to invalid schema.`,\n );\n return EMPTY;\n }\n },\n ),\n )\n .subscribe();\n\n// -----------------------------------------------------------------------------\n// Load balancing & upstream request execution\n// -----------------------------------------------------------------------------\n\nconst mapGroupIdToRoundRobinIndex = new Map<string, number>();\nconst pickInstance = (group_id: string, instances: IQuoteProviderInstance[]): IQuoteProviderInstance => {\n if (instances.length === 0) throw newError('VEX_QUOTE_PROVIDER_INSTANCE_EMPTY', { group_id });\n const nextIndex = (mapGroupIdToRoundRobinIndex.get(group_id) ?? 0) % instances.length;\n mapGroupIdToRoundRobinIndex.set(group_id, nextIndex + 1);\n return instances[nextIndex];\n};\n\n/**\n * Call a specified vendor terminal + service instance.\n *\n * Any non-0 response is treated as fatal (strict freshness requirement).\n */\nconst requestGetQuotes = async (\n terminal: Terminal,\n instance: IQuoteProviderInstance,\n req: IQuoteServiceRequestByVEX,\n): Promise<IExchangeQuoteUpdateAction> => {\n const res = await firstValueFrom(\n terminal.client\n .request<IQuoteServiceRequestByVEX, IExchangeQuoteUpdateAction>(\n 'GetQuotes',\n instance.terminal_id,\n req,\n instance.service_id,\n )\n .pipe(\n map((msg) => msg.res),\n filter((v): v is Exclude<typeof v, undefined> => v !== undefined),\n ),\n );\n if (res.code !== 0) {\n throw newError('VEX_QUOTE_PROVIDER_ERROR', { instance, res });\n }\n if (res.data === undefined) {\n throw newError('VEX_QUOTE_PROVIDER_DATA_MISSING', { instance, res });\n }\n return res.data as any;\n};\n\n/**\n * Per-provider (group_id) concurrency limit: 1.\n * Implemented as a per-group promise tail.\n */\nconst mapGroupIdToTailPromise = new Map<string, Promise<void>>();\nconst runWithProviderGroupConcurrencyLimit1 = async <T>(\n group_id: string,\n fn: () => Promise<T>,\n): Promise<T> => {\n const prev = mapGroupIdToTailPromise.get(group_id) ?? Promise.resolve();\n let resolveCurrent: () => void = () => {};\n const current = new Promise<void>((resolve) => {\n resolveCurrent = resolve;\n });\n mapGroupIdToTailPromise.set(\n group_id,\n prev.then(() => current),\n );\n await prev;\n try {\n return await fn();\n } finally {\n resolveCurrent();\n }\n};\n\n/**\n * A tiny async limiter: used as a global concurrency cap to avoid request explosions.\n */\nconst createConcurrencyLimiter = (concurrency: number) => {\n const queue: Array<() => void> = [];\n let active = 0;\n const next = () => {\n if (active >= concurrency) return;\n const task = queue.shift();\n if (!task) return;\n active++;\n task();\n };\n return async <T>(fn: () => Promise<T>): Promise<T> => {\n return await new Promise<T>((resolve, reject) => {\n queue.push(async () => {\n try {\n resolve(await fn());\n } catch (e) {\n reject(e);\n } finally {\n active--;\n next();\n }\n });\n next();\n });\n };\n};\n\n// Global concurrency cap for upstream `GetQuotes` calls (provider-level cap is handled separately).\nconst limitGetQuotes = createConcurrencyLimiter(32);\n\n/**\n * In-flight dedup:\n * Same (provider group + product batch) should share a single upstream request promise.\n */\nconst mapKeyToInFlightGetQuotesPromise = new Map<string, Promise<IExchangeQuoteUpdateAction>>();\nconst requestGetQuotesInFlight = (terminal: Terminal, key: string, planned: IPlannedRequest) => {\n const existing = mapKeyToInFlightGetQuotesPromise.get(key);\n if (existing) return existing;\n const promise = limitGetQuotes(() =>\n runWithProviderGroupConcurrencyLimit1(planned.group_id, async () => {\n const instance = pickInstance(planned.group_id, planned.instances);\n return await requestGetQuotes(terminal, instance, planned.req);\n }),\n ).finally(() => {\n mapKeyToInFlightGetQuotesPromise.delete(key);\n });\n mapKeyToInFlightGetQuotesPromise.set(key, promise);\n return promise;\n};\n\n// -----------------------------------------------------------------------------\n// Routing indices (prefix + field inverted index)\n// -----------------------------------------------------------------------------\n\ntype IProviderIndices = ReturnType<typeof buildProviderIndices>;\n\nconst buildProviderIndices = (groups: IQuoteProviderGroup[]) => {\n const mapGroupIdToGroup = new Map(groups.map((x) => [x.group_id, x] as const));\n const prefixMatcher = createSortedPrefixMatcher(\n groups.map((group) => ({ prefix: group.meta.product_id_prefix, value: group.group_id })),\n );\n const mapFieldToGroupIds = new Map<IQuoteKey, Set<string>>();\n for (const group of groups) {\n for (const field of group.meta.fields) {\n let groupIds = mapFieldToGroupIds.get(field);\n if (!groupIds) {\n groupIds = new Set<string>();\n mapFieldToGroupIds.set(field, groupIds);\n }\n groupIds.add(group.group_id);\n }\n }\n return { mapGroupIdToGroup, prefixMatcher, mapFieldToGroupIds };\n};\n\n/**\n * L1 quote routing (per `docs/zh-Hans/code-guidelines/exchange.md`):\n * For each missed (product_id, field), route to `S_product_id ∩ S_field`.\n */\nconst routeMisses = (\n cacheMissed: IQuoteMiss[],\n indices: IProviderIndices,\n updated_at: number,\n): {\n productsByGroupId: Map<string, Set<string>>;\n unavailableAction: IQuoteUpdateAction;\n unroutableProducts: Set<string>;\n} => {\n const { prefixMatcher, mapFieldToGroupIds } = indices;\n\n const mapProductIdToGroupIds = new Map<string, string[]>();\n\n const productsByGroupId = new Map<string, Set<string>>();\n const unroutableProducts = new Set<string>();\n // Field unavailable: return \"\" but keep updated_at satisfied to avoid repeated misses.\n const unavailableAction: IQuoteUpdateAction = {};\n\n for (const miss of cacheMissed) {\n const { product_id, field } = miss;\n\n let productGroupIds = mapProductIdToGroupIds.get(product_id);\n if (!productGroupIds) {\n productGroupIds = prefixMatcher.match(product_id);\n mapProductIdToGroupIds.set(product_id, productGroupIds);\n }\n if (productGroupIds.length === 0) {\n unroutableProducts.add(product_id);\n continue;\n }\n\n const fieldGroupIds = mapFieldToGroupIds.get(field);\n if (!fieldGroupIds) {\n if (!unavailableAction[product_id]) unavailableAction[product_id] = {};\n unavailableAction[product_id]![field] = ['', updated_at];\n continue;\n }\n\n let matched = false;\n for (const group_id of productGroupIds) {\n if (!fieldGroupIds.has(group_id)) continue;\n matched = true;\n let productIds = productsByGroupId.get(group_id);\n if (!productIds) {\n productIds = new Set<string>();\n productsByGroupId.set(group_id, productIds);\n }\n productIds.add(product_id);\n }\n\n if (!matched) {\n if (!unavailableAction[product_id]) unavailableAction[product_id] = {};\n unavailableAction[product_id]![field] = ['', updated_at];\n }\n }\n\n return { productsByGroupId, unavailableAction, unroutableProducts };\n};\n\nconst createRequestKey = (group_id: string, batchProductIds: string[]) =>\n encodePath(group_id, fnv1a64HexFromStrings(batchProductIds));\n\nconst planRequests = (\n productsByGroupId: Map<string, Set<string>>,\n mapGroupIdToGroup: Map<string, IQuoteProviderGroup>,\n): Array<{ key: string; planned: IPlannedRequest }> => {\n const plannedRequests: Array<{ key: string; planned: IPlannedRequest }> = [];\n for (const [group_id, productIdSet] of productsByGroupId) {\n const group = mapGroupIdToGroup.get(group_id);\n if (!group) continue;\n const sortedProductIds = [...productIdSet].sort();\n const max = group.meta.max_products_per_request ?? sortedProductIds.length;\n for (let i = 0; i < sortedProductIds.length; i += max) {\n const batchProductIds = sortedProductIds.slice(i, i + max);\n const key = createRequestKey(group_id, batchProductIds);\n plannedRequests.push({\n key,\n planned: {\n group_id,\n instances: Array.from(group.mapTerminalIdToInstance.values()),\n req: { product_ids: batchProductIds, fields: group.meta.fields as any },\n },\n });\n }\n }\n return plannedRequests;\n};\n\nexport const fillQuoteStateFromUpstream = async (params: {\n terminal: Terminal;\n quoteState: IQuoteState;\n cacheMissed: IQuoteMiss[];\n updated_at: number;\n}): Promise<void> => {\n const { terminal, quoteState, cacheMissed, updated_at } = params;\n if (cacheMissed.length === 0) return;\n\n const providerGroups = Array.from(mapGroupIdToGroup.values());\n console.info(\n formatTime(Date.now()),\n `[VEX][Quote]UpstreamProviderDiscovery`,\n ` Discovered ${providerGroups.length} GetQuotes provider groups from terminal infos.`,\n JSON.stringify(providerGroups),\n );\n if (providerGroups.length === 0) {\n throw newError('VEX_QUOTE_PROVIDER_NOT_FOUND', { method: 'GetQuotes' });\n }\n\n const indices = buildProviderIndices(providerGroups);\n const { productsByGroupId, unavailableAction, unroutableProducts } = routeMisses(\n cacheMissed,\n indices,\n updated_at,\n );\n\n console.info(\n formatTime(Date.now()),\n `[VEX][Quote]RouteDispatched`,\n ` Routed ${cacheMissed.length} missed quotes to ${productsByGroupId.size} provider groups, ` +\n `${unroutableProducts.size} unroutable products.`,\n JSON.stringify({\n productsByGroupId: [...productsByGroupId.entries()].map(([group_id, productIds]) => ({\n group_id,\n product_ids: [...productIds],\n })),\n unroutable_products: [...unroutableProducts],\n unavailable_action: unavailableAction,\n }),\n );\n\n if (unroutableProducts.size !== 0) {\n throw newError('VEX_QUOTE_PRODUCT_UNROUTABLE', {\n updated_at,\n unroutable_products: [...unroutableProducts].slice(0, 200),\n unroutable_products_total: unroutableProducts.size,\n });\n }\n\n quoteState.update(unavailableAction);\n\n const plannedRequests = planRequests(productsByGroupId, indices.mapGroupIdToGroup);\n\n console.info(\n formatTime(Date.now()),\n `[VEX][Quote]RequestPlanned`,\n `Planned ${plannedRequests.length} upstream GetQuotes requests.`,\n JSON.stringify(\n plannedRequests.map(({ key, planned }) => ({\n key,\n group_id: planned.group_id,\n product_ids: planned.req.product_ids,\n fields: planned.req.fields,\n })),\n ),\n );\n\n const actions = await Promise.all(\n plannedRequests.map(async ({ key, planned }) => await requestGetQuotesInFlight(terminal, key, planned)),\n );\n\n console.debug(\n formatTime(Date.now()),\n `[VEX][Quote]RequestReceived`,\n `Received ${actions.length} upstream GetQuotes responses.`,\n // JSON.stringify(actions),\n );\n\n for (const action of actions) {\n quoteState.update(action as unknown as IQuoteUpdateAction);\n }\n};\n"]}
@@ -1 +1 @@
1
- {"version":3,"file":"position.d.ts","sourceRoot":"","sources":["../src/position.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAqC5C,eAAO,MAAM,gBAAgB,cAAqB,SAAS,EAAE,KAAG,QAAQ,SAAS,EAAE,CA+DlF,CAAC;AAEF,eAAO,MAAM,cAAc,WAAkB,MAAM,EAAE,KAAG,QAAQ,MAAM,EAAE,CA4BvE,CAAC"}
1
+ {"version":3,"file":"position.d.ts","sourceRoot":"","sources":["../src/position.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,SAAS,EAAE,MAAM,sBAAsB,CAAC;AACjD,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAqC5C,eAAO,MAAM,gBAAgB,cAAqB,SAAS,EAAE,KAAG,QAAQ,SAAS,EAAE,CAiFlF,CAAC;AAEF,eAAO,MAAM,cAAc,WAAkB,MAAM,EAAE,KAAG,QAAQ,MAAM,EAAE,CA4BvE,CAAC"}
package/lib/position.js CHANGED
@@ -52,6 +52,23 @@ const polyfillPosition = async (positions) => {
52
52
  }
53
53
  pos.valuation = Math.abs((theProduct.value_scale || 1) * pos.volume * pos.closable_price);
54
54
  }
55
+ if (quote && pos.size) {
56
+ const sizeNum = +pos.size;
57
+ if (pos.current_price === undefined) {
58
+ if (sizeNum > 0) {
59
+ // 多头头寸使用买一价作为可平仓价格,如果没有买一价则使用最新价
60
+ pos.current_price = (quote.ask_price || quote.last_price) + '';
61
+ }
62
+ else {
63
+ // 空头头寸使用卖一价作为可平仓价格,如果没有卖一价则使用最新价
64
+ pos.current_price = (quote.bid_price || quote.last_price) + '';
65
+ }
66
+ }
67
+ // 计算名义价值
68
+ if (pos.notional === undefined) {
69
+ pos.notional = sizeNum * (+pos.current_price || 0) + '';
70
+ }
71
+ }
55
72
  // 利率相关信息的追加
56
73
  if (quote) {
57
74
  if (quote.interest_rate_next_settled_at !== null) {
@@ -1 +1 @@
1
- {"version":3,"file":"position.js","sourceRoot":"","sources":["../src/position.ts"],"names":[],"mappings":"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","sourcesContent":["import { createCache } from '@yuants/cache';\nimport { IPosition } from '@yuants/data-account';\nimport { IOrder } from '@yuants/data-order';\nimport { createClientProductCache } from '@yuants/data-product';\nimport { IQuote } from '@yuants/data-quote';\nimport { Terminal } from '@yuants/protocol';\nimport { escapeSQL, requestSQL } from '@yuants/sql';\nimport { newError } from '@yuants/utils';\n\nconst terminal = Terminal.fromNodeEnv();\nconst productCache = createClientProductCache(terminal);\nconst quoteCache = createCache<IQuote>(\n async (product_id) => {\n const sql = `select * from quote where product_id = ${escapeSQL(product_id)}`;\n const [quote] = await requestSQL<IQuote[]>(terminal, sql);\n return quote;\n },\n { expire: 30_000 },\n);\n\nconst interestRateIntervalCache = createCache(\n async (product_id: string) => {\n const sql = `select created_at from interest_rate where series_id = ${escapeSQL(\n product_id,\n )} order by created_at desc limit 2`;\n const rates = await requestSQL<{ created_at: string }[]>(terminal, sql);\n if (rates.length < 2) return undefined;\n const prev = new Date(rates[0].created_at).getTime();\n const prevOfPrev = new Date(rates[1].created_at).getTime();\n const interval = prev - prevOfPrev;\n return {\n prev,\n prevOfPrev,\n interval,\n };\n },\n { swrAfter: 60_000, expire: 3600_000 },\n);\n\nexport const polyfillPosition = async (positions: IPosition[]): Promise<IPosition[]> => {\n // TODO: 使用 batch query SQL 优化 product / quote 查询性能\n for (const pos of positions) {\n const [theProduct, quote, interestRateInterval] = await Promise.all([\n productCache.query(pos.product_id),\n quoteCache.query(pos.product_id),\n interestRateIntervalCache.query(pos.product_id),\n ]);\n\n // 估值 = value_scale * volume * closable_price\n if (theProduct) {\n if (theProduct.base_currency) {\n pos.base_currency = theProduct.base_currency;\n }\n if (theProduct.quote_currency) {\n pos.quote_currency = theProduct.quote_currency;\n }\n if (pos.size === undefined && pos.volume !== undefined && pos.direction !== undefined) {\n pos.size = (pos.direction === 'LONG' ? 1 : -1) * pos.volume * (theProduct.value_scale || 1) + '';\n }\n if (pos.free_size === undefined && pos.free_volume !== undefined && pos.direction !== undefined) {\n pos.free_size =\n (pos.direction === 'LONG' ? 1 : -1) * pos.free_volume * (theProduct.value_scale || 1) + '';\n }\n pos.valuation = Math.abs((theProduct.value_scale || 1) * pos.volume * pos.closable_price);\n }\n\n // 利率相关信息的追加\n if (quote) {\n if (quote.interest_rate_next_settled_at !== null) {\n const nextSettledAt = new Date(quote.interest_rate_next_settled_at).getTime();\n // 优先使用行情数据中的下一个结算时间\n pos.settlement_scheduled_at = nextSettledAt;\n // 优先使用下一个结算时间推算结算间隔\n if (interestRateInterval !== undefined) {\n const interval = nextSettledAt - interestRateInterval.prev;\n pos.settlement_interval = interval;\n }\n } else if (quote.interest_rate_next_settled_at === null && interestRateInterval !== undefined) {\n // 估算下一个结算时间\n // 找到 prev + k * interval > now 的最小 k,则下一个结算时间为 prev + k * interval\n const k = Math.ceil((Date.now() - interestRateInterval.prev) / interestRateInterval.interval);\n pos.settlement_scheduled_at = interestRateInterval.prev + k * interestRateInterval.interval;\n }\n\n // 如果还没有结算间隔,则使用 interest rate 表的时间间隔作为结算间隔\n if (pos.settlement_interval === undefined && interestRateInterval) {\n pos.settlement_interval = interestRateInterval.interval;\n }\n\n if (pos.direction === 'LONG') {\n if (quote.interest_rate_long !== null) {\n pos.interest_to_settle = +quote.interest_rate_long * pos.valuation;\n }\n }\n if (pos.direction === 'SHORT') {\n if (quote.interest_rate_short !== null) {\n pos.interest_to_settle = +quote.interest_rate_short * pos.valuation;\n }\n }\n }\n }\n return positions;\n};\n\nexport const polyfillOrders = async (orders: IOrder[]): Promise<IOrder[]> => {\n for (const order of orders) {\n const theProduct = await productCache.query(order.product_id);\n if (theProduct) {\n if (order.size !== undefined) {\n const sizeNum = +order.size;\n const sizeStep = theProduct.volume_step * theProduct.value_scale;\n if (!(sizeStep > 0)) throw newError('INVALID_SIZE_STEP', { product: theProduct, sizeStep });\n // check size is multiple of sizeStep\n if (Math.abs(sizeNum - Math.round(sizeNum / sizeStep) * sizeStep) > 1e-16) {\n throw newError('INVALID_ORDER_SIZE_NOT_MULTIPLE_OF_SIZE_STEP', {\n order,\n sizeStep,\n sizeNum,\n product: theProduct,\n });\n }\n\n if (sizeNum >= 0) {\n order.order_direction = order.is_close ? 'CLOSE_SHORT' : 'OPEN_LONG';\n } else {\n order.order_direction = order.is_close ? 'CLOSE_LONG' : 'OPEN_SHORT';\n }\n order.volume = Math.abs(sizeNum) / theProduct.value_scale;\n }\n }\n }\n return orders;\n};\n"]}
1
+ {"version":3,"file":"position.js","sourceRoot":"","sources":["../src/position.ts"],"names":[],"mappings":"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","sourcesContent":["import { createCache } from '@yuants/cache';\nimport { IPosition } from '@yuants/data-account';\nimport { IOrder } from '@yuants/data-order';\nimport { createClientProductCache } from '@yuants/data-product';\nimport { IQuote } from '@yuants/data-quote';\nimport { Terminal } from '@yuants/protocol';\nimport { escapeSQL, requestSQL } from '@yuants/sql';\nimport { newError } from '@yuants/utils';\n\nconst terminal = Terminal.fromNodeEnv();\nconst productCache = createClientProductCache(terminal);\nconst quoteCache = createCache<IQuote>(\n async (product_id) => {\n const sql = `select * from quote where product_id = ${escapeSQL(product_id)}`;\n const [quote] = await requestSQL<IQuote[]>(terminal, sql);\n return quote;\n },\n { expire: 30_000 },\n);\n\nconst interestRateIntervalCache = createCache(\n async (product_id: string) => {\n const sql = `select created_at from interest_rate where series_id = ${escapeSQL(\n product_id,\n )} order by created_at desc limit 2`;\n const rates = await requestSQL<{ created_at: string }[]>(terminal, sql);\n if (rates.length < 2) return undefined;\n const prev = new Date(rates[0].created_at).getTime();\n const prevOfPrev = new Date(rates[1].created_at).getTime();\n const interval = prev - prevOfPrev;\n return {\n prev,\n prevOfPrev,\n interval,\n };\n },\n { swrAfter: 60_000, expire: 3600_000 },\n);\n\nexport const polyfillPosition = async (positions: IPosition[]): Promise<IPosition[]> => {\n // TODO: 使用 batch query SQL 优化 product / quote 查询性能\n for (const pos of positions) {\n const [theProduct, quote, interestRateInterval] = await Promise.all([\n productCache.query(pos.product_id),\n quoteCache.query(pos.product_id),\n interestRateIntervalCache.query(pos.product_id),\n ]);\n\n // 估值 = value_scale * volume * closable_price\n if (theProduct) {\n if (theProduct.base_currency) {\n pos.base_currency = theProduct.base_currency;\n }\n if (theProduct.quote_currency) {\n pos.quote_currency = theProduct.quote_currency;\n }\n if (pos.size === undefined && pos.volume !== undefined && pos.direction !== undefined) {\n pos.size = (pos.direction === 'LONG' ? 1 : -1) * pos.volume * (theProduct.value_scale || 1) + '';\n }\n if (pos.free_size === undefined && pos.free_volume !== undefined && pos.direction !== undefined) {\n pos.free_size =\n (pos.direction === 'LONG' ? 1 : -1) * pos.free_volume * (theProduct.value_scale || 1) + '';\n }\n pos.valuation = Math.abs((theProduct.value_scale || 1) * pos.volume * pos.closable_price);\n }\n\n if (quote && pos.size) {\n const sizeNum = +pos.size;\n if (pos.current_price === undefined) {\n if (sizeNum > 0) {\n // 多头头寸使用买一价作为可平仓价格,如果没有买一价则使用最新价\n pos.current_price = (quote.ask_price || quote.last_price) + '';\n } else {\n // 空头头寸使用卖一价作为可平仓价格,如果没有卖一价则使用最新价\n pos.current_price = (quote.bid_price || quote.last_price) + '';\n }\n }\n\n // 计算名义价值\n if (pos.notional === undefined) {\n pos.notional = sizeNum * (+pos.current_price || 0) + '';\n }\n }\n\n // 利率相关信息的追加\n if (quote) {\n if (quote.interest_rate_next_settled_at !== null) {\n const nextSettledAt = new Date(quote.interest_rate_next_settled_at).getTime();\n // 优先使用行情数据中的下一个结算时间\n pos.settlement_scheduled_at = nextSettledAt;\n // 优先使用下一个结算时间推算结算间隔\n if (interestRateInterval !== undefined) {\n const interval = nextSettledAt - interestRateInterval.prev;\n pos.settlement_interval = interval;\n }\n } else if (quote.interest_rate_next_settled_at === null && interestRateInterval !== undefined) {\n // 估算下一个结算时间\n // 找到 prev + k * interval > now 的最小 k,则下一个结算时间为 prev + k * interval\n const k = Math.ceil((Date.now() - interestRateInterval.prev) / interestRateInterval.interval);\n pos.settlement_scheduled_at = interestRateInterval.prev + k * interestRateInterval.interval;\n }\n\n // 如果还没有结算间隔,则使用 interest rate 表的时间间隔作为结算间隔\n if (pos.settlement_interval === undefined && interestRateInterval) {\n pos.settlement_interval = interestRateInterval.interval;\n }\n\n if (pos.direction === 'LONG') {\n if (quote.interest_rate_long !== null) {\n pos.interest_to_settle = +quote.interest_rate_long * pos.valuation;\n }\n }\n if (pos.direction === 'SHORT') {\n if (quote.interest_rate_short !== null) {\n pos.interest_to_settle = +quote.interest_rate_short * pos.valuation;\n }\n }\n }\n }\n return positions;\n};\n\nexport const polyfillOrders = async (orders: IOrder[]): Promise<IOrder[]> => {\n for (const order of orders) {\n const theProduct = await productCache.query(order.product_id);\n if (theProduct) {\n if (order.size !== undefined) {\n const sizeNum = +order.size;\n const sizeStep = theProduct.volume_step * theProduct.value_scale;\n if (!(sizeStep > 0)) throw newError('INVALID_SIZE_STEP', { product: theProduct, sizeStep });\n // check size is multiple of sizeStep\n if (Math.abs(sizeNum - Math.round(sizeNum / sizeStep) * sizeStep) > 1e-16) {\n throw newError('INVALID_ORDER_SIZE_NOT_MULTIPLE_OF_SIZE_STEP', {\n order,\n sizeStep,\n sizeNum,\n product: theProduct,\n });\n }\n\n if (sizeNum >= 0) {\n order.order_direction = order.is_close ? 'CLOSE_SHORT' : 'OPEN_LONG';\n } else {\n order.order_direction = order.is_close ? 'CLOSE_LONG' : 'OPEN_SHORT';\n }\n order.volume = Math.abs(sizeNum) / theProduct.value_scale;\n }\n }\n }\n return orders;\n};\n"]}
@@ -0,0 +1,2 @@
1
+ export {};
2
+ //# sourceMappingURL=implementations.test.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"implementations.test.d.ts","sourceRoot":"","sources":["../../../src/quote/__tests__/implementations.test.ts"],"names":[],"mappings":""}