@fullstackcraftllc/floe 0.0.18 → 0.0.19
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/README.md +6 -11
- package/dist/apiclient/index.d.ts +204 -0
- package/dist/apiclient/index.js +721 -0
- package/dist/client/FloeClient.d.ts +4 -0
- package/dist/client/FloeClient.js +50 -0
- package/dist/client/brokers/WebullClient.d.ts +53 -0
- package/dist/client/brokers/WebullClient.js +290 -0
- package/dist/index.d.ts +4 -0
- package/dist/index.js +11 -2
- package/package.json +1 -3
|
@@ -0,0 +1,721 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.ApiClient = exports.APIError = exports.AMTEventCode = exports.AMTEventCategory = void 0;
|
|
4
|
+
exports.NewApiClient = NewApiClient;
|
|
5
|
+
const DEFAULT_HINDSIGHT_BASE_URL = 'https://hindsightapi.com/api';
|
|
6
|
+
const DEFAULT_DEALER_BASE_URL = 'https://vannacharm.com/api';
|
|
7
|
+
const DEFAULT_AMT_BASE_URL = 'https://amtjoy.com/api';
|
|
8
|
+
const DEFAULT_WHEELSCREENER_BASE_URL = 'https://wheelscreener.com/api';
|
|
9
|
+
const DEFAULT_LEAPSSCREENER_BASE_URL = 'https://leapsscreener.com/api';
|
|
10
|
+
const DEFAULT_OPTIONSCREENER_BASE_URL = 'https://option-screener.com/api';
|
|
11
|
+
const DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/;
|
|
12
|
+
// --- AMT Event Categories & Codes ---
|
|
13
|
+
/** Event categories for filtering AMT minute events. */
|
|
14
|
+
exports.AMTEventCategory = {
|
|
15
|
+
TPO: 'TPO',
|
|
16
|
+
Price: 'Price',
|
|
17
|
+
Volume: 'Volume',
|
|
18
|
+
Session: 'Session',
|
|
19
|
+
Overnight: 'Overnight',
|
|
20
|
+
};
|
|
21
|
+
/** Typed event codes emitted by the AMT pipeline (66 codes across 5 categories). */
|
|
22
|
+
exports.AMTEventCode = {
|
|
23
|
+
// --- TPO Events ---
|
|
24
|
+
TPONewPrint: 'TPO_NEW_PRINT',
|
|
25
|
+
TPOPeriodFirstPrint: 'TPO_PERIOD_FIRST_PRINT',
|
|
26
|
+
TPOFirst3Wide: 'TPO_FIRST_3WIDE',
|
|
27
|
+
TPOFirst4Wide: 'TPO_FIRST_4WIDE',
|
|
28
|
+
TPOFirst5Wide: 'TPO_FIRST_5WIDE',
|
|
29
|
+
TPOSinglePrintCreated: 'TPO_SINGLE_PRINT_CREATED',
|
|
30
|
+
TPOSinglePrintFilled: 'TPO_SINGLE_PRINT_FILLED',
|
|
31
|
+
TPOPoorHigh: 'TPO_POOR_HIGH',
|
|
32
|
+
TPOPoorLow: 'TPO_POOR_LOW',
|
|
33
|
+
TPOExcessHigh: 'TPO_EXCESS_HIGH',
|
|
34
|
+
TPOExcessLow: 'TPO_EXCESS_LOW',
|
|
35
|
+
// --- Price Cross Events ---
|
|
36
|
+
PriceNHOD: 'PRICE_NHOD',
|
|
37
|
+
PriceNLOD: 'PRICE_NLOD',
|
|
38
|
+
PriceCrossHBUp: 'PRICE_CROSS_HB_UP',
|
|
39
|
+
PriceCrossHBDown: 'PRICE_CROSS_HB_DOWN',
|
|
40
|
+
PriceCrossTPOCUp: 'PRICE_CROSS_TPOC_UP',
|
|
41
|
+
PriceCrossTPOCDown: 'PRICE_CROSS_TPOC_DOWN',
|
|
42
|
+
PriceCrossVPOCUp: 'PRICE_CROSS_VPOC_UP',
|
|
43
|
+
PriceCrossVPOCDown: 'PRICE_CROSS_VPOC_DOWN',
|
|
44
|
+
PriceCrossVAHUp: 'PRICE_CROSS_VAH_UP',
|
|
45
|
+
PriceCrossVAHDown: 'PRICE_CROSS_VAH_DOWN',
|
|
46
|
+
PriceCrossVALUp: 'PRICE_CROSS_VAL_UP',
|
|
47
|
+
PriceCrossVALDown: 'PRICE_CROSS_VAL_DOWN',
|
|
48
|
+
PriceCrossORHighUp: 'PRICE_CROSS_OR_HIGH_UP',
|
|
49
|
+
PriceCrossORHighDown: 'PRICE_CROSS_OR_HIGH_DOWN',
|
|
50
|
+
PriceCrossORLowUp: 'PRICE_CROSS_OR_LOW_UP',
|
|
51
|
+
PriceCrossORLowDown: 'PRICE_CROSS_OR_LOW_DOWN',
|
|
52
|
+
PriceCrossIBHighUp: 'PRICE_CROSS_IB_HIGH_UP',
|
|
53
|
+
PriceCrossIBHighDown: 'PRICE_CROSS_IB_HIGH_DOWN',
|
|
54
|
+
PriceCrossIBLowUp: 'PRICE_CROSS_IB_LOW_UP',
|
|
55
|
+
PriceCrossIBLowDown: 'PRICE_CROSS_IB_LOW_DOWN',
|
|
56
|
+
PriceCrossPHODUp: 'PRICE_CROSS_PHOD_UP',
|
|
57
|
+
PriceCrossPHODDown: 'PRICE_CROSS_PHOD_DOWN',
|
|
58
|
+
PriceCrossPLODUp: 'PRICE_CROSS_PLOD_UP',
|
|
59
|
+
PriceCrossPLODDown: 'PRICE_CROSS_PLOD_DOWN',
|
|
60
|
+
PriceCrossPrevCloseUp: 'PRICE_CROSS_PREV_CLOSE_UP',
|
|
61
|
+
PriceCrossPrevCloseDown: 'PRICE_CROSS_PREV_CLOSE_DOWN',
|
|
62
|
+
PriceCrossPrevOpenUp: 'PRICE_CROSS_PREV_OPEN_UP',
|
|
63
|
+
PriceCrossPrevOpenDown: 'PRICE_CROSS_PREV_OPEN_DOWN',
|
|
64
|
+
PriceCrossONHUp: 'PRICE_CROSS_ONH_UP',
|
|
65
|
+
PriceCrossONHDown: 'PRICE_CROSS_ONH_DOWN',
|
|
66
|
+
PriceCrossONLUp: 'PRICE_CROSS_ONL_UP',
|
|
67
|
+
PriceCrossONLDown: 'PRICE_CROSS_ONL_DOWN',
|
|
68
|
+
PriceCrossVWAPUp: 'PRICE_CROSS_VWAP_UP',
|
|
69
|
+
PriceCrossVWAPDown: 'PRICE_CROSS_VWAP_DOWN',
|
|
70
|
+
PriceCrossVWAPSD1Up: 'PRICE_CROSS_VWAP_SD1_UP',
|
|
71
|
+
PriceCrossVWAPSD1Down: 'PRICE_CROSS_VWAP_SD1_DOWN',
|
|
72
|
+
PriceCrossVWAPSD2Up: 'PRICE_CROSS_VWAP_SD2_UP',
|
|
73
|
+
PriceCrossVWAPSD2Down: 'PRICE_CROSS_VWAP_SD2_DOWN',
|
|
74
|
+
// --- Volume Profile Events ---
|
|
75
|
+
VolVPOCShiftUp: 'VOL_VPOC_SHIFT_UP',
|
|
76
|
+
VolVPOCShiftDown: 'VOL_VPOC_SHIFT_DOWN',
|
|
77
|
+
VolVAMigrationUp: 'VOL_VA_MIGRATION_UP',
|
|
78
|
+
VolVAMigrationDown: 'VOL_VA_MIGRATION_DOWN',
|
|
79
|
+
VolVAExpanding: 'VOL_VA_EXPANDING',
|
|
80
|
+
VolVAContracting: 'VOL_VA_CONTRACTING',
|
|
81
|
+
// --- Session Structure Events ---
|
|
82
|
+
SessionPeriodTransition: 'SESSION_PERIOD_TRANSITION',
|
|
83
|
+
SessionOREstablished: 'SESSION_OR_ESTABLISHED',
|
|
84
|
+
SessionIBEstablished: 'SESSION_IB_ESTABLISHED',
|
|
85
|
+
SessionIBExtensionUp: 'SESSION_IB_EXTENSION_UP',
|
|
86
|
+
SessionIBExtensionDown: 'SESSION_IB_EXTENSION_DOWN',
|
|
87
|
+
SessionORExtensionUp: 'SESSION_OR_EXTENSION_UP',
|
|
88
|
+
SessionORExtensionDown: 'SESSION_OR_EXTENSION_DOWN',
|
|
89
|
+
SessionGapFill: 'SESSION_GAP_FILL',
|
|
90
|
+
SessionGlobexGapFill: 'SESSION_GLOBEX_GAP_FILL',
|
|
91
|
+
SessionHalfbackReached: 'SESSION_HALFBACK_REACHED',
|
|
92
|
+
// --- Overnight Events (futures only) ---
|
|
93
|
+
ONAsiaOpen: 'ON_ASIA_OPEN',
|
|
94
|
+
ONAsiaClose: 'ON_ASIA_CLOSE',
|
|
95
|
+
ONEUOpen: 'ON_EU_OPEN',
|
|
96
|
+
ONEUClose: 'ON_EU_CLOSE',
|
|
97
|
+
ONNewHigh: 'ON_NEW_HIGH',
|
|
98
|
+
ONNewLow: 'ON_NEW_LOW',
|
|
99
|
+
};
|
|
100
|
+
const STATUS_TEXT = {
|
|
101
|
+
200: 'OK',
|
|
102
|
+
400: 'Bad Request',
|
|
103
|
+
401: 'Unauthorized',
|
|
104
|
+
403: 'Forbidden',
|
|
105
|
+
404: 'Not Found',
|
|
106
|
+
408: 'Request Timeout',
|
|
107
|
+
429: 'Too Many Requests',
|
|
108
|
+
500: 'Internal Server Error',
|
|
109
|
+
502: 'Bad Gateway',
|
|
110
|
+
503: 'Service Unavailable',
|
|
111
|
+
504: 'Gateway Timeout',
|
|
112
|
+
};
|
|
113
|
+
class APIError extends Error {
|
|
114
|
+
constructor(params) {
|
|
115
|
+
const statusCode = params.StatusCode;
|
|
116
|
+
const message = params.Message ?? '';
|
|
117
|
+
const statusText = statusCode > 0 ? `${statusCode} ${statusTextForCode(statusCode)}`.trim() : '';
|
|
118
|
+
const errorMessage = statusText !== '' && message !== ''
|
|
119
|
+
? `api error: ${statusText}: ${message}`
|
|
120
|
+
: message !== ''
|
|
121
|
+
? `api error: ${message}`
|
|
122
|
+
: statusText !== ''
|
|
123
|
+
? `api error: ${statusText}`
|
|
124
|
+
: 'api error';
|
|
125
|
+
super(errorMessage);
|
|
126
|
+
this.name = 'APIError';
|
|
127
|
+
this.StatusCode = statusCode;
|
|
128
|
+
this.Message = message;
|
|
129
|
+
this.SubscriptionEnd = params.SubscriptionEnd ?? '';
|
|
130
|
+
this.RawBody = params.RawBody ?? '';
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
exports.APIError = APIError;
|
|
134
|
+
class ApiClient {
|
|
135
|
+
constructor(apiKey, httpClient) {
|
|
136
|
+
this.apiKey = apiKey.trim();
|
|
137
|
+
const resolvedClient = httpClient ??
|
|
138
|
+
(typeof globalThis.fetch === 'function'
|
|
139
|
+
? ((input, init) => globalThis.fetch(input, init))
|
|
140
|
+
: undefined);
|
|
141
|
+
if (typeof resolvedClient !== 'function') {
|
|
142
|
+
throw new Error('http client is required');
|
|
143
|
+
}
|
|
144
|
+
this.httpClient = resolvedClient;
|
|
145
|
+
this.hindsightBaseURL = DEFAULT_HINDSIGHT_BASE_URL;
|
|
146
|
+
this.dealerBaseURL = DEFAULT_DEALER_BASE_URL;
|
|
147
|
+
this.amtBaseURL = DEFAULT_AMT_BASE_URL;
|
|
148
|
+
this.wheelScreenerBaseURL = DEFAULT_WHEELSCREENER_BASE_URL;
|
|
149
|
+
this.leapsScreenerBaseURL = DEFAULT_LEAPSSCREENER_BASE_URL;
|
|
150
|
+
this.optionScreenerBaseURL = DEFAULT_OPTIONSCREENER_BASE_URL;
|
|
151
|
+
}
|
|
152
|
+
async GetHindsightData(ctx, req) {
|
|
153
|
+
validateHindsightDataRequest(req);
|
|
154
|
+
const query = new URLSearchParams();
|
|
155
|
+
query.set('start_date', req.start_date.trim());
|
|
156
|
+
query.set('end_date', req.end_date.trim());
|
|
157
|
+
if (typeof req.country === 'string' && req.country.trim() !== '') {
|
|
158
|
+
query.set('country', req.country.trim());
|
|
159
|
+
}
|
|
160
|
+
if (typeof req.min_volatility === 'number') {
|
|
161
|
+
query.set('min_volatility', String(req.min_volatility));
|
|
162
|
+
}
|
|
163
|
+
if (typeof req.event === 'string' && req.event.trim() !== '') {
|
|
164
|
+
query.set('event', req.event.trim());
|
|
165
|
+
}
|
|
166
|
+
const body = await this.getRaw(ctx, this.hindsightBaseURL, '/getData', query, true);
|
|
167
|
+
return decodeHindsightEvents(body);
|
|
168
|
+
}
|
|
169
|
+
async GetHindsightSample(ctx) {
|
|
170
|
+
const body = await this.getRaw(ctx, this.hindsightBaseURL, '/getSample', null, true);
|
|
171
|
+
return decodeHindsightEvents(body);
|
|
172
|
+
}
|
|
173
|
+
async GetDealerMinuteSurfaces(ctx, req) {
|
|
174
|
+
validateDealerMinuteSurfacesRequest(req);
|
|
175
|
+
const query = new URLSearchParams();
|
|
176
|
+
query.set('symbol', req.symbol.trim());
|
|
177
|
+
query.set('trade_date', req.trade_date.trim());
|
|
178
|
+
const body = await this.getRaw(ctx, this.dealerBaseURL, '/getMinuteSurfaces', query, true);
|
|
179
|
+
return decodeDealerMinuteSurfaces(body);
|
|
180
|
+
}
|
|
181
|
+
async GetAMTSessionStats(ctx, req) {
|
|
182
|
+
validateAMTRequest(req);
|
|
183
|
+
const query = new URLSearchParams();
|
|
184
|
+
query.set('symbol', req.symbol.trim().toUpperCase());
|
|
185
|
+
query.set('session_id', req.session_id.trim());
|
|
186
|
+
const body = await this.getRaw(ctx, this.amtBaseURL, '/getSessionStats', query, true);
|
|
187
|
+
return decodeAMTSessionStats(body);
|
|
188
|
+
}
|
|
189
|
+
async GetAMTEvents(ctx, req) {
|
|
190
|
+
validateAMTRequest(req);
|
|
191
|
+
const query = new URLSearchParams();
|
|
192
|
+
query.set('symbol', req.symbol.trim().toUpperCase());
|
|
193
|
+
query.set('session_id', req.session_id.trim());
|
|
194
|
+
const body = await this.getRaw(ctx, this.amtBaseURL, '/getAMTEvents', query, true);
|
|
195
|
+
return decodeAMTEvents(body);
|
|
196
|
+
}
|
|
197
|
+
async GetWheelScreenerData(ctx, req) {
|
|
198
|
+
return this.getOptionsScreenerData(ctx, this.wheelScreenerBaseURL, '/get-options', req);
|
|
199
|
+
}
|
|
200
|
+
async GetLeapsScreenerData(ctx, req) {
|
|
201
|
+
return this.getOptionsScreenerData(ctx, this.leapsScreenerBaseURL, '/get-options', req);
|
|
202
|
+
}
|
|
203
|
+
async GetOptionScreenerData(ctx, req) {
|
|
204
|
+
return this.getOptionsScreenerData(ctx, this.optionScreenerBaseURL, '/getOptionsData', req);
|
|
205
|
+
}
|
|
206
|
+
async getOptionsScreenerData(ctx, baseURL, path, req) {
|
|
207
|
+
validateOptionsScreenerRequest(req);
|
|
208
|
+
const query = new URLSearchParams();
|
|
209
|
+
query.set('strategy', req.strategy.trim().toUpperCase());
|
|
210
|
+
if (typeof req.search === 'string' && req.search.trim() !== '') {
|
|
211
|
+
query.set('search', req.search.trim());
|
|
212
|
+
}
|
|
213
|
+
if (typeof req.page === 'number' && req.page > 0) {
|
|
214
|
+
query.set('page', String(req.page));
|
|
215
|
+
}
|
|
216
|
+
if (typeof req.page_size === 'number' && req.page_size > 0) {
|
|
217
|
+
query.set('page_size', String(req.page_size));
|
|
218
|
+
}
|
|
219
|
+
if (typeof req.order_by === 'string' && req.order_by.trim() !== '') {
|
|
220
|
+
query.set('order_by', req.order_by.trim());
|
|
221
|
+
}
|
|
222
|
+
if (typeof req.order_direction === 'string' && req.order_direction.trim() !== '') {
|
|
223
|
+
query.set('order_direction', req.order_direction.trim());
|
|
224
|
+
}
|
|
225
|
+
if (req.extra_params !== undefined && req.extra_params !== null) {
|
|
226
|
+
for (const [key, value] of Object.entries(req.extra_params)) {
|
|
227
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
228
|
+
query.set(key, value.trim());
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
const body = await this.getRaw(ctx, baseURL, path, query, true);
|
|
233
|
+
return decodeOptionsScreenerResponse(body);
|
|
234
|
+
}
|
|
235
|
+
async getRaw(ctx, baseURL, path, query, requiresAPIKey) {
|
|
236
|
+
if (requiresAPIKey && this.apiKey.trim() === '') {
|
|
237
|
+
throw new Error('api key is required');
|
|
238
|
+
}
|
|
239
|
+
const trimmedBaseURL = baseURL.trim();
|
|
240
|
+
if (trimmedBaseURL === '') {
|
|
241
|
+
throw new Error('base URL is required');
|
|
242
|
+
}
|
|
243
|
+
let endpoint;
|
|
244
|
+
try {
|
|
245
|
+
endpoint = new URL(`${trimmedBaseURL.replace(/\/$/, '')}${path}`);
|
|
246
|
+
}
|
|
247
|
+
catch (error) {
|
|
248
|
+
throw new Error(`failed to parse endpoint URL: ${toErrorMessage(error)}`);
|
|
249
|
+
}
|
|
250
|
+
if (query !== null) {
|
|
251
|
+
endpoint.search = query.toString();
|
|
252
|
+
}
|
|
253
|
+
const headers = {
|
|
254
|
+
Accept: 'application/json',
|
|
255
|
+
};
|
|
256
|
+
if (requiresAPIKey) {
|
|
257
|
+
headers['X-API-Key'] = this.apiKey;
|
|
258
|
+
}
|
|
259
|
+
const signal = resolveSignal(ctx);
|
|
260
|
+
let response;
|
|
261
|
+
try {
|
|
262
|
+
response = await this.httpClient(endpoint.toString(), {
|
|
263
|
+
method: 'GET',
|
|
264
|
+
headers,
|
|
265
|
+
signal,
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
catch (error) {
|
|
269
|
+
throw new Error(`request failed: ${toErrorMessage(error)}`);
|
|
270
|
+
}
|
|
271
|
+
let body;
|
|
272
|
+
try {
|
|
273
|
+
body = await response.text();
|
|
274
|
+
}
|
|
275
|
+
catch (error) {
|
|
276
|
+
throw new Error(`failed to read response body: ${toErrorMessage(error)}`);
|
|
277
|
+
}
|
|
278
|
+
if (!response.ok) {
|
|
279
|
+
throw decodeAPIError(response.status, body);
|
|
280
|
+
}
|
|
281
|
+
if (body.trim() === '') {
|
|
282
|
+
throw new Error('empty response body');
|
|
283
|
+
}
|
|
284
|
+
return body;
|
|
285
|
+
}
|
|
286
|
+
}
|
|
287
|
+
exports.ApiClient = ApiClient;
|
|
288
|
+
function NewApiClient(apiKey, httpClient) {
|
|
289
|
+
return new ApiClient(apiKey, httpClient);
|
|
290
|
+
}
|
|
291
|
+
function validateHindsightDataRequest(req) {
|
|
292
|
+
const startDate = (req.start_date ?? '').trim();
|
|
293
|
+
const endDate = (req.end_date ?? '').trim();
|
|
294
|
+
if (startDate === '') {
|
|
295
|
+
throw new Error('start_date is required');
|
|
296
|
+
}
|
|
297
|
+
if (endDate === '') {
|
|
298
|
+
throw new Error('end_date is required');
|
|
299
|
+
}
|
|
300
|
+
const start = parseDateOnly(startDate);
|
|
301
|
+
if (start === null) {
|
|
302
|
+
throw new Error('start_date must be in YYYY-MM-DD format');
|
|
303
|
+
}
|
|
304
|
+
const end = parseDateOnly(endDate);
|
|
305
|
+
if (end === null) {
|
|
306
|
+
throw new Error('end_date must be in YYYY-MM-DD format');
|
|
307
|
+
}
|
|
308
|
+
if (end.getTime() < start.getTime()) {
|
|
309
|
+
throw new Error('end_date must be on or after start_date');
|
|
310
|
+
}
|
|
311
|
+
if (req.min_volatility !== undefined &&
|
|
312
|
+
req.min_volatility !== null &&
|
|
313
|
+
(!Number.isInteger(req.min_volatility) || req.min_volatility < 1 || req.min_volatility > 3)) {
|
|
314
|
+
throw new Error('min_volatility must be between 1 and 3 when provided');
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
function validateDealerMinuteSurfacesRequest(req) {
|
|
318
|
+
const symbol = (req.symbol ?? '').trim();
|
|
319
|
+
const tradeDate = (req.trade_date ?? '').trim();
|
|
320
|
+
if (symbol === '') {
|
|
321
|
+
throw new Error('symbol is required');
|
|
322
|
+
}
|
|
323
|
+
if (tradeDate === '') {
|
|
324
|
+
throw new Error('trade_date is required');
|
|
325
|
+
}
|
|
326
|
+
if (parseDateOnly(tradeDate) === null) {
|
|
327
|
+
throw new Error('trade_date must be in YYYY-MM-DD format');
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
function validateAMTRequest(req) {
|
|
331
|
+
const symbol = (req.symbol ?? '').trim();
|
|
332
|
+
const sessionID = (req.session_id ?? '').trim();
|
|
333
|
+
if (symbol === '') {
|
|
334
|
+
throw new Error('symbol is required');
|
|
335
|
+
}
|
|
336
|
+
if (sessionID === '') {
|
|
337
|
+
throw new Error('session_id is required');
|
|
338
|
+
}
|
|
339
|
+
if (parseDateOnly(sessionID) === null) {
|
|
340
|
+
throw new Error('session_id must be in YYYY-MM-DD format');
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
function decodeAPIError(statusCode, body) {
|
|
344
|
+
const trimmed = body.trim();
|
|
345
|
+
if (trimmed === '') {
|
|
346
|
+
return new APIError({
|
|
347
|
+
StatusCode: statusCode,
|
|
348
|
+
Message: statusTextForCode(statusCode),
|
|
349
|
+
});
|
|
350
|
+
}
|
|
351
|
+
let message = '';
|
|
352
|
+
let subscriptionEnd = '';
|
|
353
|
+
const parsed = parseJSON(body);
|
|
354
|
+
const obj = asRecord(parsed);
|
|
355
|
+
if (obj !== null) {
|
|
356
|
+
message = firstNonEmpty(getStringOrEmpty(obj.error), getStringOrEmpty(obj.message));
|
|
357
|
+
subscriptionEnd = firstNonEmpty(getStringOrEmpty(obj.subscriptionEnd), getStringOrEmpty(obj.subscription_end));
|
|
358
|
+
}
|
|
359
|
+
if (message === '') {
|
|
360
|
+
message = truncateForError(trimmed);
|
|
361
|
+
}
|
|
362
|
+
if (message === '') {
|
|
363
|
+
message = statusTextForCode(statusCode);
|
|
364
|
+
}
|
|
365
|
+
return new APIError({
|
|
366
|
+
StatusCode: statusCode,
|
|
367
|
+
Message: message,
|
|
368
|
+
SubscriptionEnd: subscriptionEnd,
|
|
369
|
+
RawBody: body,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
function decodeHindsightEvents(body) {
|
|
373
|
+
const parsed = parseJSON(body);
|
|
374
|
+
const envelope = asRecord(parsed);
|
|
375
|
+
if (envelope !== null) {
|
|
376
|
+
const hasEnvelope = envelope.success === true ||
|
|
377
|
+
Array.isArray(envelope.data) ||
|
|
378
|
+
nonEmptyString(envelope.error) ||
|
|
379
|
+
nonEmptyString(envelope.message) ||
|
|
380
|
+
nonEmptyString(envelope.subscriptionEnd) ||
|
|
381
|
+
nonEmptyString(envelope.subscription_end);
|
|
382
|
+
if (hasEnvelope) {
|
|
383
|
+
if (envelope.success !== true) {
|
|
384
|
+
throw new APIError({
|
|
385
|
+
StatusCode: 200,
|
|
386
|
+
Message: firstNonEmpty(getStringOrEmpty(envelope.error), getStringOrEmpty(envelope.message), 'request failed'),
|
|
387
|
+
SubscriptionEnd: firstNonEmpty(getStringOrEmpty(envelope.subscriptionEnd), getStringOrEmpty(envelope.subscription_end)),
|
|
388
|
+
RawBody: body,
|
|
389
|
+
});
|
|
390
|
+
}
|
|
391
|
+
const rows = Array.isArray(envelope.data) ? envelope.data : [];
|
|
392
|
+
return rows.map(normalizeHindsightEvent);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
395
|
+
if (Array.isArray(parsed)) {
|
|
396
|
+
return parsed.map(normalizeHindsightEvent);
|
|
397
|
+
}
|
|
398
|
+
throw new Error('failed to decode hindsight response');
|
|
399
|
+
}
|
|
400
|
+
function decodeDealerMinuteSurfaces(body) {
|
|
401
|
+
const parsed = parseJSON(body);
|
|
402
|
+
const envelope = asRecord(parsed);
|
|
403
|
+
if (envelope !== null) {
|
|
404
|
+
const hasEnvelope = envelope.success === true ||
|
|
405
|
+
Array.isArray(envelope.data) ||
|
|
406
|
+
nonEmptyString(envelope.error) ||
|
|
407
|
+
nonEmptyString(envelope.message) ||
|
|
408
|
+
nonEmptyString(envelope.subscriptionEnd) ||
|
|
409
|
+
nonEmptyString(envelope.subscription_end);
|
|
410
|
+
if (hasEnvelope) {
|
|
411
|
+
if (envelope.success !== true) {
|
|
412
|
+
throw new APIError({
|
|
413
|
+
StatusCode: 200,
|
|
414
|
+
Message: firstNonEmpty(getStringOrEmpty(envelope.error), getStringOrEmpty(envelope.message), 'request failed'),
|
|
415
|
+
SubscriptionEnd: firstNonEmpty(getStringOrEmpty(envelope.subscriptionEnd), getStringOrEmpty(envelope.subscription_end)),
|
|
416
|
+
RawBody: body,
|
|
417
|
+
});
|
|
418
|
+
}
|
|
419
|
+
const rows = Array.isArray(envelope.data) ? envelope.data : [];
|
|
420
|
+
return rows.map(normalizeDealerMinuteSurface);
|
|
421
|
+
}
|
|
422
|
+
}
|
|
423
|
+
if (Array.isArray(parsed)) {
|
|
424
|
+
return parsed.map(normalizeDealerMinuteSurface);
|
|
425
|
+
}
|
|
426
|
+
throw new Error('failed to decode dealer minute surfaces response');
|
|
427
|
+
}
|
|
428
|
+
function decodeAMTSessionStats(body) {
|
|
429
|
+
const parsed = parseJSON(body);
|
|
430
|
+
const envelope = asRecord(parsed);
|
|
431
|
+
if (envelope !== null) {
|
|
432
|
+
const hasEnvelope = envelope.success === true ||
|
|
433
|
+
Array.isArray(envelope.data) ||
|
|
434
|
+
nonEmptyString(envelope.error) ||
|
|
435
|
+
nonEmptyString(envelope.message) ||
|
|
436
|
+
nonEmptyString(envelope.subscriptionEnd) ||
|
|
437
|
+
nonEmptyString(envelope.subscription_end);
|
|
438
|
+
if (hasEnvelope) {
|
|
439
|
+
if (envelope.success !== true) {
|
|
440
|
+
throw new APIError({
|
|
441
|
+
StatusCode: 200,
|
|
442
|
+
Message: firstNonEmpty(getStringOrEmpty(envelope.error), getStringOrEmpty(envelope.message), 'request failed'),
|
|
443
|
+
SubscriptionEnd: firstNonEmpty(getStringOrEmpty(envelope.subscriptionEnd), getStringOrEmpty(envelope.subscription_end)),
|
|
444
|
+
RawBody: body,
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
const rows = Array.isArray(envelope.data) ? envelope.data : [];
|
|
448
|
+
return rows.map(normalizeAMTSessionStatsRow);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
if (Array.isArray(parsed)) {
|
|
452
|
+
return parsed.map(normalizeAMTSessionStatsRow);
|
|
453
|
+
}
|
|
454
|
+
throw new Error('failed to decode amt session stats response');
|
|
455
|
+
}
|
|
456
|
+
function decodeAMTEvents(body) {
|
|
457
|
+
const parsed = parseJSON(body);
|
|
458
|
+
const envelope = asRecord(parsed);
|
|
459
|
+
if (envelope !== null) {
|
|
460
|
+
const hasEnvelope = envelope.success === true ||
|
|
461
|
+
Array.isArray(envelope.data) ||
|
|
462
|
+
nonEmptyString(envelope.error) ||
|
|
463
|
+
nonEmptyString(envelope.message) ||
|
|
464
|
+
nonEmptyString(envelope.subscriptionEnd) ||
|
|
465
|
+
nonEmptyString(envelope.subscription_end);
|
|
466
|
+
if (hasEnvelope) {
|
|
467
|
+
if (envelope.success !== true) {
|
|
468
|
+
throw new APIError({
|
|
469
|
+
StatusCode: 200,
|
|
470
|
+
Message: firstNonEmpty(getStringOrEmpty(envelope.error), getStringOrEmpty(envelope.message), 'request failed'),
|
|
471
|
+
SubscriptionEnd: firstNonEmpty(getStringOrEmpty(envelope.subscriptionEnd), getStringOrEmpty(envelope.subscription_end)),
|
|
472
|
+
RawBody: body,
|
|
473
|
+
});
|
|
474
|
+
}
|
|
475
|
+
const rows = Array.isArray(envelope.data) ? envelope.data : [];
|
|
476
|
+
return rows.map(normalizeAMTEventsRow);
|
|
477
|
+
}
|
|
478
|
+
}
|
|
479
|
+
if (Array.isArray(parsed)) {
|
|
480
|
+
return parsed.map(normalizeAMTEventsRow);
|
|
481
|
+
}
|
|
482
|
+
throw new Error('failed to decode amt events response');
|
|
483
|
+
}
|
|
484
|
+
function validateOptionsScreenerRequest(req) {
|
|
485
|
+
const strategy = (req.strategy ?? '').trim();
|
|
486
|
+
if (strategy === '') {
|
|
487
|
+
throw new Error('strategy is required');
|
|
488
|
+
}
|
|
489
|
+
}
|
|
490
|
+
function decodeOptionsScreenerResponse(body) {
|
|
491
|
+
const parsed = parseJSON(body);
|
|
492
|
+
const envelope = asRecord(parsed);
|
|
493
|
+
if (envelope !== null) {
|
|
494
|
+
const hasEnvelope = envelope.success === true ||
|
|
495
|
+
Array.isArray(envelope.data) ||
|
|
496
|
+
nonEmptyString(envelope.error) ||
|
|
497
|
+
nonEmptyString(envelope.message) ||
|
|
498
|
+
nonEmptyString(envelope.subscriptionEnd) ||
|
|
499
|
+
nonEmptyString(envelope.subscription_end);
|
|
500
|
+
if (hasEnvelope) {
|
|
501
|
+
if (envelope.success !== true) {
|
|
502
|
+
throw new APIError({
|
|
503
|
+
StatusCode: 200,
|
|
504
|
+
Message: firstNonEmpty(getStringOrEmpty(envelope.error), getStringOrEmpty(envelope.message), 'request failed'),
|
|
505
|
+
SubscriptionEnd: firstNonEmpty(getStringOrEmpty(envelope.subscriptionEnd), getStringOrEmpty(envelope.subscription_end)),
|
|
506
|
+
RawBody: body,
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
const rows = Array.isArray(envelope.data) ? envelope.data : [];
|
|
510
|
+
return {
|
|
511
|
+
data: rows.map((item) => asRecord(item) ?? {}),
|
|
512
|
+
total: toInteger(envelope.total),
|
|
513
|
+
page: toInteger(envelope.page),
|
|
514
|
+
page_size: toInteger(envelope.page_size),
|
|
515
|
+
};
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
if (Array.isArray(parsed)) {
|
|
519
|
+
return {
|
|
520
|
+
data: parsed.map((item) => asRecord(item) ?? {}),
|
|
521
|
+
total: parsed.length,
|
|
522
|
+
page: 1,
|
|
523
|
+
page_size: parsed.length,
|
|
524
|
+
};
|
|
525
|
+
}
|
|
526
|
+
throw new Error('failed to decode options screener response');
|
|
527
|
+
}
|
|
528
|
+
function normalizeHindsightEvent(value) {
|
|
529
|
+
const row = asRecord(value) ?? {};
|
|
530
|
+
return {
|
|
531
|
+
id: toInteger(row.id),
|
|
532
|
+
event_id: getStringOrEmpty(row.event_id),
|
|
533
|
+
date: getStringOrEmpty(row.date),
|
|
534
|
+
time: getStringOrEmpty(row.time),
|
|
535
|
+
timezone: getStringOrEmpty(row.timezone),
|
|
536
|
+
country: getStringOrEmpty(row.country),
|
|
537
|
+
country_code: getStringOrEmpty(row.country_code),
|
|
538
|
+
event_name: getStringOrEmpty(row.event_name),
|
|
539
|
+
volatility: toInteger(row.volatility),
|
|
540
|
+
actual: toNullableString(row.actual),
|
|
541
|
+
forecast: toNullableString(row.forecast),
|
|
542
|
+
previous: toNullableString(row.previous),
|
|
543
|
+
created_at: toDateOrNull(row.created_at),
|
|
544
|
+
updated_at: toDateOrNull(row.updated_at),
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
function normalizeDealerMinuteSurface(value) {
|
|
548
|
+
const row = asRecord(value) ?? {};
|
|
549
|
+
const rawSurfaces = asRecord(row.surfaces) ?? asRecord(row.surfaces_jsonb) ?? {};
|
|
550
|
+
const rawMetadata = asRecord(row.metadata) ?? asRecord(row.metadata_jsonb) ?? {};
|
|
551
|
+
return {
|
|
552
|
+
id: getStringOrEmpty(row.id),
|
|
553
|
+
run_at: toDateOrNull(row.run_at),
|
|
554
|
+
symbol: getStringOrEmpty(row.symbol),
|
|
555
|
+
trade_date: getStringOrEmpty(row.trade_date),
|
|
556
|
+
minute_ts: toDateOrNull(row.minute_ts),
|
|
557
|
+
session_minute: toInteger(row.session_minute),
|
|
558
|
+
spot: toNumber(row.spot),
|
|
559
|
+
vix: toNumber(row.vix),
|
|
560
|
+
surfaces: normalizeMinuteSurface(rawSurfaces),
|
|
561
|
+
metadata: rawMetadata,
|
|
562
|
+
};
|
|
563
|
+
}
|
|
564
|
+
function normalizeAMTSessionStatsRow(value) {
|
|
565
|
+
const row = asRecord(value) ?? {};
|
|
566
|
+
return {
|
|
567
|
+
symbol: getStringOrEmpty(row.symbol),
|
|
568
|
+
session_id: getStringOrEmpty(row.session_id),
|
|
569
|
+
session_data: asRecord(row.session_data) ?? {},
|
|
570
|
+
};
|
|
571
|
+
}
|
|
572
|
+
function normalizeAMTEventsRow(value) {
|
|
573
|
+
const row = asRecord(value) ?? {};
|
|
574
|
+
const events = Array.isArray(row.events)
|
|
575
|
+
? row.events.map((event) => asRecord(event) ?? {}).filter((event) => Object.keys(event).length > 0)
|
|
576
|
+
: [];
|
|
577
|
+
return {
|
|
578
|
+
symbol: getStringOrEmpty(row.symbol),
|
|
579
|
+
session_id: getStringOrEmpty(row.session_id),
|
|
580
|
+
events,
|
|
581
|
+
};
|
|
582
|
+
}
|
|
583
|
+
function normalizeMinuteSurface(value) {
|
|
584
|
+
return {
|
|
585
|
+
gamma: normalizeSurfacePoints(value.gamma),
|
|
586
|
+
vanna: normalizeSurfacePoints(value.vanna),
|
|
587
|
+
charm: normalizeSurfacePoints(value.charm),
|
|
588
|
+
iv: normalizeSurfacePoints(value.iv),
|
|
589
|
+
};
|
|
590
|
+
}
|
|
591
|
+
function normalizeSurfacePoints(value) {
|
|
592
|
+
if (!Array.isArray(value)) {
|
|
593
|
+
return [];
|
|
594
|
+
}
|
|
595
|
+
return value.map((point) => {
|
|
596
|
+
const rawPoint = asRecord(point) ?? {};
|
|
597
|
+
return {
|
|
598
|
+
strike: rawPoint.strike !== undefined ? toNumber(rawPoint.strike) : undefined,
|
|
599
|
+
value: rawPoint.value !== undefined ? toNumber(rawPoint.value) : undefined,
|
|
600
|
+
x: rawPoint.x !== undefined ? toNumber(rawPoint.x) : undefined,
|
|
601
|
+
y: rawPoint.y !== undefined ? toNumber(rawPoint.y) : undefined,
|
|
602
|
+
};
|
|
603
|
+
});
|
|
604
|
+
}
|
|
605
|
+
function resolveSignal(ctx) {
|
|
606
|
+
if (ctx === null || ctx === undefined) {
|
|
607
|
+
return undefined;
|
|
608
|
+
}
|
|
609
|
+
if (isAbortSignal(ctx)) {
|
|
610
|
+
return ctx;
|
|
611
|
+
}
|
|
612
|
+
return ctx.signal;
|
|
613
|
+
}
|
|
614
|
+
function isAbortSignal(value) {
|
|
615
|
+
return (typeof value === 'object' &&
|
|
616
|
+
value !== null &&
|
|
617
|
+
typeof value.aborted === 'boolean' &&
|
|
618
|
+
typeof value.addEventListener === 'function');
|
|
619
|
+
}
|
|
620
|
+
function parseJSON(value) {
|
|
621
|
+
try {
|
|
622
|
+
return JSON.parse(value);
|
|
623
|
+
}
|
|
624
|
+
catch {
|
|
625
|
+
return null;
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
function parseDateOnly(value) {
|
|
629
|
+
if (!DATE_ONLY_REGEX.test(value)) {
|
|
630
|
+
return null;
|
|
631
|
+
}
|
|
632
|
+
const parsed = new Date(`${value}T00:00:00.000Z`);
|
|
633
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
634
|
+
return null;
|
|
635
|
+
}
|
|
636
|
+
const [year, month, day] = value.split('-').map((part) => Number(part));
|
|
637
|
+
if (parsed.getUTCFullYear() !== year ||
|
|
638
|
+
parsed.getUTCMonth() + 1 !== month ||
|
|
639
|
+
parsed.getUTCDate() !== day) {
|
|
640
|
+
return null;
|
|
641
|
+
}
|
|
642
|
+
return parsed;
|
|
643
|
+
}
|
|
644
|
+
function toDateOrNull(value) {
|
|
645
|
+
if (value === null || value === undefined) {
|
|
646
|
+
return null;
|
|
647
|
+
}
|
|
648
|
+
if (value instanceof Date) {
|
|
649
|
+
return Number.isNaN(value.getTime()) ? null : value;
|
|
650
|
+
}
|
|
651
|
+
if (typeof value === 'string' || typeof value === 'number') {
|
|
652
|
+
const parsed = new Date(value);
|
|
653
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
654
|
+
}
|
|
655
|
+
return null;
|
|
656
|
+
}
|
|
657
|
+
function toInteger(value) {
|
|
658
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
659
|
+
return Math.trunc(value);
|
|
660
|
+
}
|
|
661
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
662
|
+
const parsed = Number.parseInt(value, 10);
|
|
663
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
664
|
+
}
|
|
665
|
+
return 0;
|
|
666
|
+
}
|
|
667
|
+
function toNumber(value) {
|
|
668
|
+
if (typeof value === 'number' && Number.isFinite(value)) {
|
|
669
|
+
return value;
|
|
670
|
+
}
|
|
671
|
+
if (typeof value === 'string' && value.trim() !== '') {
|
|
672
|
+
const parsed = Number.parseFloat(value);
|
|
673
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
674
|
+
}
|
|
675
|
+
return 0;
|
|
676
|
+
}
|
|
677
|
+
function asRecord(value) {
|
|
678
|
+
if (typeof value === 'object' && value !== null && !Array.isArray(value)) {
|
|
679
|
+
return value;
|
|
680
|
+
}
|
|
681
|
+
return null;
|
|
682
|
+
}
|
|
683
|
+
function getStringOrEmpty(value) {
|
|
684
|
+
return typeof value === 'string' ? value : '';
|
|
685
|
+
}
|
|
686
|
+
function toNullableString(value) {
|
|
687
|
+
if (value === null || value === undefined) {
|
|
688
|
+
return null;
|
|
689
|
+
}
|
|
690
|
+
if (typeof value === 'string') {
|
|
691
|
+
return value;
|
|
692
|
+
}
|
|
693
|
+
return String(value);
|
|
694
|
+
}
|
|
695
|
+
function nonEmptyString(value) {
|
|
696
|
+
return typeof value === 'string' && value.trim() !== '';
|
|
697
|
+
}
|
|
698
|
+
function firstNonEmpty(...values) {
|
|
699
|
+
for (const value of values) {
|
|
700
|
+
if (value.trim() !== '') {
|
|
701
|
+
return value.trim();
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
return '';
|
|
705
|
+
}
|
|
706
|
+
function truncateForError(value) {
|
|
707
|
+
const maxLength = 300;
|
|
708
|
+
if (value.length <= maxLength) {
|
|
709
|
+
return value;
|
|
710
|
+
}
|
|
711
|
+
return `${value.slice(0, maxLength)}...`;
|
|
712
|
+
}
|
|
713
|
+
function statusTextForCode(statusCode) {
|
|
714
|
+
return STATUS_TEXT[statusCode] ?? '';
|
|
715
|
+
}
|
|
716
|
+
function toErrorMessage(value) {
|
|
717
|
+
if (value instanceof Error) {
|
|
718
|
+
return value.message;
|
|
719
|
+
}
|
|
720
|
+
return String(value);
|
|
721
|
+
}
|