@tigeropenapi/tigeropen 0.5.0 → 0.5.4
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/cjs/client/api-response.js +17 -15
- package/dist/cjs/client/token-methods.js +42 -0
- package/dist/cjs/config/client-config.js +59 -11
- package/dist/cjs/config/config-parser.js +19 -9
- package/dist/cjs/config/index.js +2 -1
- package/dist/cjs/index.js +2 -1
- package/dist/cjs/model/enums.js +4 -0
- package/dist/cjs/model/order-helpers.js +126 -21
- package/dist/cjs/quote/quote-client.js +112 -21
- package/dist/cjs/trade/trade-client.js +74 -16
- package/dist/esm/client/api-response.js +17 -15
- package/dist/esm/client/token-methods.js +37 -0
- package/dist/esm/config/client-config.js +59 -12
- package/dist/esm/config/config-parser.js +19 -9
- package/dist/esm/config/index.js +1 -1
- package/dist/esm/index.js +1 -1
- package/dist/esm/model/enums.js +4 -0
- package/dist/esm/model/order-helpers.js +119 -20
- package/dist/esm/quote/quote-client.js +79 -21
- package/dist/esm/trade/trade-client.js +41 -16
- package/dist/types/client/api-response.d.ts +6 -5
- package/dist/types/client/token-methods.d.ts +34 -0
- package/dist/types/config/client-config.d.ts +2 -0
- package/dist/types/config/index.d.ts +1 -1
- package/dist/types/index.d.ts +3 -3
- package/dist/types/model/enums.d.ts +5 -1
- package/dist/types/model/order-helpers.d.ts +48 -8
- package/dist/types/model/order.d.ts +52 -1
- package/dist/types/model/quote.d.ts +56 -0
- package/dist/types/model/trade-requests.d.ts +41 -0
- package/dist/types/quote/quote-client.d.ts +31 -8
- package/dist/types/trade/trade-client.d.ts +19 -3
- package/package.json +1 -1
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.QuoteClient = void 0;
|
|
4
37
|
exports.parseOptionIdentifier = parseOptionIdentifier;
|
|
@@ -12,6 +45,7 @@ exports.parseOptionIdentifier = parseOptionIdentifier;
|
|
|
12
45
|
const http_client_1 = require("../client/http-client");
|
|
13
46
|
const api_request_1 = require("../client/api-request");
|
|
14
47
|
const api_response_1 = require("../client/api-response");
|
|
48
|
+
const tokenMethods = __importStar(require("../client/token-methods"));
|
|
15
49
|
/**
|
|
16
50
|
* resolveOptionTimezone returns the timezone string to use for expiry conversion.
|
|
17
51
|
* Explicit tz takes precedence; otherwise infer from symbol (.HK suffix → Asia/Hong_Kong, else America/New_York).
|
|
@@ -94,6 +128,25 @@ class QuoteClient {
|
|
|
94
128
|
static fromConfig(config) {
|
|
95
129
|
return new QuoteClient(new http_client_1.HttpClient(config, undefined, { useQuoteServerUrl: true }));
|
|
96
130
|
}
|
|
131
|
+
// ── Token management ──────────────────────────────────────────────────────
|
|
132
|
+
/** Call the `user_token_refresh` API and return the new token. Read-only. */
|
|
133
|
+
async queryToken() {
|
|
134
|
+
return tokenMethods.queryToken(this.httpClient);
|
|
135
|
+
}
|
|
136
|
+
/** Refresh the token and update the in-memory config token. */
|
|
137
|
+
async refreshToken(tokenManager) {
|
|
138
|
+
return tokenMethods.refreshToken(this.httpClient, tokenManager);
|
|
139
|
+
}
|
|
140
|
+
/**
|
|
141
|
+
* Start background token auto-refresh. Returns a TokenManager to stop with `tm.stopAutoRefresh()`.
|
|
142
|
+
*
|
|
143
|
+
* @param refreshDurationSecs - Refresh threshold in seconds (minimum 30).
|
|
144
|
+
* @param checkIntervalMs - How often to check, in milliseconds (default 300 000 / 5 min).
|
|
145
|
+
* @param tokenWriter - Optional callback invoked after each successful refresh.
|
|
146
|
+
*/
|
|
147
|
+
startTokenAutoRefresh(refreshDurationSecs, checkIntervalMs, tokenWriter) {
|
|
148
|
+
return tokenMethods.startTokenAutoRefresh(this.httpClient, refreshDurationSecs, checkIntervalMs, tokenWriter);
|
|
149
|
+
}
|
|
97
150
|
async callInto(method, bizParams, version) {
|
|
98
151
|
const request = (0, api_request_1.createApiRequest)(method, bizParams, version);
|
|
99
152
|
const response = await this.httpClient.executeRequest(request);
|
|
@@ -155,8 +208,10 @@ class QuoteClient {
|
|
|
155
208
|
*
|
|
156
209
|
* @param timezone - Optional IANA timezone for expiry conversion (e.g. 'America/New_York').
|
|
157
210
|
* If omitted, defaults to America/New_York for US symbols and Asia/Hong_Kong for .HK symbols.
|
|
211
|
+
* @param returnGreekValue - Whether to return Greeks (delta/gamma/theta/vega/rho).
|
|
212
|
+
* @param optionFilter - Optional filter for in-the-money status, IV range, open interest range, Greeks ranges.
|
|
158
213
|
*/
|
|
159
|
-
async getOptionChain(items, timezone) {
|
|
214
|
+
async getOptionChain(items, timezone, returnGreekValue, optionFilter) {
|
|
160
215
|
const optionBasic = items.map(([symbol, expiry]) => {
|
|
161
216
|
if (!/^\d{4}-\d{2}-\d{2}$/.test(expiry)) {
|
|
162
217
|
throw new Error(`invalid expiry date, expected YYYY-MM-DD: ${expiry}`);
|
|
@@ -165,37 +220,27 @@ class QuoteClient {
|
|
|
165
220
|
const expiryMs = localMidnightMs(expiry, tz);
|
|
166
221
|
return { symbol, expiry: expiryMs };
|
|
167
222
|
});
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
* If omitted, defaults to America/New_York for US symbols and Asia/Hong_Kong for .HK symbols.
|
|
175
|
-
*/
|
|
176
|
-
async getOptionQuote(identifiers, timezone) {
|
|
177
|
-
const optionBasic = identifiers.map((id) => {
|
|
178
|
-
const p = parseOptionIdentifier(id, timezone);
|
|
179
|
-
return { symbol: p.symbol, expiry: p.expiryMs, right: p.right, strike: p.strike };
|
|
180
|
-
});
|
|
181
|
-
return this.callInto('option_brief', { option_basic: optionBasic }, '2.0');
|
|
182
|
-
}
|
|
183
|
-
/** @deprecated Use getOptionQuote instead. */
|
|
184
|
-
async getOptionBrief(identifiers) {
|
|
185
|
-
return this.getOptionQuote(identifiers);
|
|
223
|
+
const body = { option_basic: optionBasic };
|
|
224
|
+
if (returnGreekValue !== undefined)
|
|
225
|
+
body['return_greek_value'] = returnGreekValue;
|
|
226
|
+
if (optionFilter !== undefined)
|
|
227
|
+
body['option_filter'] = optionFilter;
|
|
228
|
+
return this.callInto('option_chain', body, '3.0');
|
|
186
229
|
}
|
|
187
230
|
/**
|
|
188
231
|
* Option K-line by OCC-style identifier.
|
|
189
232
|
*
|
|
190
233
|
* @param timezone - Optional IANA timezone for expiry conversion (e.g. 'America/New_York').
|
|
191
234
|
* If omitted, defaults to America/New_York for US symbols and Asia/Hong_Kong for .HK symbols.
|
|
235
|
+
* @param limit - Maximum number of bars to return.
|
|
236
|
+
* @param sortDir - Sort direction ('asc' or 'desc').
|
|
192
237
|
*/
|
|
193
|
-
async getOptionKline(identifiers, period, beginTime = -1, endTime = -1, timezone) {
|
|
238
|
+
async getOptionKline(identifiers, period, beginTime = -1, endTime = -1, timezone, limit, sortDir) {
|
|
194
239
|
const resolvedBegin = beginTime === 0 ? -1 : beginTime;
|
|
195
240
|
const resolvedEnd = endTime === 0 ? -1 : endTime;
|
|
196
241
|
const optionQuery = identifiers.map((id) => {
|
|
197
242
|
const p = parseOptionIdentifier(id, timezone);
|
|
198
|
-
|
|
243
|
+
const entry = {
|
|
199
244
|
symbol: p.symbol,
|
|
200
245
|
expiry: p.expiryMs,
|
|
201
246
|
right: p.right,
|
|
@@ -204,9 +249,31 @@ class QuoteClient {
|
|
|
204
249
|
begin_time: resolvedBegin,
|
|
205
250
|
end_time: resolvedEnd,
|
|
206
251
|
};
|
|
252
|
+
if (limit !== undefined && limit > 0)
|
|
253
|
+
entry['limit'] = limit;
|
|
254
|
+
if (sortDir !== undefined && sortDir !== '')
|
|
255
|
+
entry['sort_dir'] = sortDir;
|
|
256
|
+
return entry;
|
|
207
257
|
});
|
|
208
258
|
return this.callInto('option_kline', { option_query: optionQuery }, '2.0');
|
|
209
259
|
}
|
|
260
|
+
/**
|
|
261
|
+
* Real-time option quotes by OCC-style identifier.
|
|
262
|
+
*
|
|
263
|
+
* @param timezone - Optional IANA timezone for expiry conversion (e.g. 'America/New_York').
|
|
264
|
+
* If omitted, defaults to America/New_York for US symbols and Asia/Hong_Kong for .HK symbols.
|
|
265
|
+
*/
|
|
266
|
+
async getOptionQuote(identifiers, timezone) {
|
|
267
|
+
const optionBasic = identifiers.map((id) => {
|
|
268
|
+
const p = parseOptionIdentifier(id, timezone);
|
|
269
|
+
return { symbol: p.symbol, expiry: p.expiryMs, right: p.right, strike: p.strike };
|
|
270
|
+
});
|
|
271
|
+
return this.callInto('option_brief', { option_basic: optionBasic }, '2.0');
|
|
272
|
+
}
|
|
273
|
+
/** @deprecated Use getOptionQuote instead. */
|
|
274
|
+
async getOptionBrief(identifiers) {
|
|
275
|
+
return this.getOptionQuote(identifiers);
|
|
276
|
+
}
|
|
210
277
|
// === Futures ===
|
|
211
278
|
async getFutureExchange() {
|
|
212
279
|
return this.callInto('future_exchange', { secType: 'FUT' });
|
|
@@ -524,6 +591,30 @@ class QuoteClient {
|
|
|
524
591
|
return [];
|
|
525
592
|
return Object.values(grouped).flat();
|
|
526
593
|
}
|
|
594
|
+
/** Corporate symbol change (flattened). wire: corporate_action (action_type=symbol_change) */
|
|
595
|
+
async getCorporateSymbolChange(req) {
|
|
596
|
+
const body = { ...req, actionType: 'symbol_change' };
|
|
597
|
+
const grouped = await this.callInto('corporate_action', body);
|
|
598
|
+
if (!grouped || typeof grouped !== 'object')
|
|
599
|
+
return [];
|
|
600
|
+
return Object.values(grouped).flat();
|
|
601
|
+
}
|
|
602
|
+
/** Corporate delisting (flattened). wire: corporate_action (action_type=delisting) */
|
|
603
|
+
async getCorporateDelisting(req) {
|
|
604
|
+
const body = { ...req, actionType: 'delisting' };
|
|
605
|
+
const grouped = await this.callInto('corporate_action', body);
|
|
606
|
+
if (!grouped || typeof grouped !== 'object')
|
|
607
|
+
return [];
|
|
608
|
+
return Object.values(grouped).flat();
|
|
609
|
+
}
|
|
610
|
+
/** Corporate IPO (flattened). wire: corporate_action (action_type=ipo) */
|
|
611
|
+
async getCorporateIPO(req) {
|
|
612
|
+
const body = { ...req, actionType: 'ipo' };
|
|
613
|
+
const grouped = await this.callInto('corporate_action', body);
|
|
614
|
+
if (!grouped || typeof grouped !== 'object')
|
|
615
|
+
return [];
|
|
616
|
+
return Object.values(grouped).flat();
|
|
617
|
+
}
|
|
527
618
|
/** Financial currency per symbol. wire: financial_currency */
|
|
528
619
|
async getFinancialCurrency(req) {
|
|
529
620
|
return this.callInto('financial_currency', req);
|
|
@@ -1,4 +1,37 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.TradeClient = void 0;
|
|
4
37
|
/**
|
|
@@ -13,6 +46,7 @@ exports.TradeClient = void 0;
|
|
|
13
46
|
const http_client_1 = require("../client/http-client");
|
|
14
47
|
const api_request_1 = require("../client/api-request");
|
|
15
48
|
const api_response_1 = require("../client/api-response");
|
|
49
|
+
const tokenMethods = __importStar(require("../client/token-methods"));
|
|
16
50
|
const order_status_1 = require("../model/order-status");
|
|
17
51
|
/** Normalize `Order.status` field in-place (handles int → string). */
|
|
18
52
|
function normalizeOrderInPlace(o) {
|
|
@@ -44,8 +78,42 @@ class TradeClient {
|
|
|
44
78
|
static fromConfig(config, account, secretKey) {
|
|
45
79
|
return new TradeClient(new http_client_1.HttpClient(config), account, secretKey);
|
|
46
80
|
}
|
|
81
|
+
// ── Token management ──────────────────────────────────────────────────────
|
|
82
|
+
/** Call the `user_token_refresh` API and return the new token. Read-only. */
|
|
83
|
+
async queryToken() {
|
|
84
|
+
return tokenMethods.queryToken(this.httpClient);
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Refresh the token: call the API, update the in-memory config token, and
|
|
88
|
+
* optionally persist via the provided TokenManager (file + writer callback).
|
|
89
|
+
*
|
|
90
|
+
* @param tokenManager - optional TokenManager for file persistence and writer callback
|
|
91
|
+
*/
|
|
92
|
+
async refreshToken(tokenManager) {
|
|
93
|
+
return tokenMethods.refreshToken(this.httpClient, tokenManager);
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Start background token auto-refresh. Returns a TokenManager that can be
|
|
97
|
+
* used to stop the background task via `tm.stopAutoRefresh()`.
|
|
98
|
+
*
|
|
99
|
+
* @param refreshDurationSecs - Refresh threshold in seconds (minimum 30).
|
|
100
|
+
* @param checkIntervalMs - How often to check, in milliseconds (default 300 000 / 5 min).
|
|
101
|
+
* @param tokenWriter - Optional callback invoked after each successful refresh.
|
|
102
|
+
*/
|
|
103
|
+
startTokenAutoRefresh(refreshDurationSecs, checkIntervalMs, tokenWriter) {
|
|
104
|
+
return tokenMethods.startTokenAutoRefresh(this.httpClient, refreshDurationSecs, checkIntervalMs, tokenWriter);
|
|
105
|
+
}
|
|
47
106
|
async callInto(method, bizParams) {
|
|
48
|
-
|
|
107
|
+
// Inject secretKey into every request when available (institution accounts require it).
|
|
108
|
+
// Individual methods that already set secretKey will not be overwritten (|| not &&).
|
|
109
|
+
let params = bizParams;
|
|
110
|
+
if (this.secretKey && params !== null && typeof params === 'object' && !Array.isArray(params)) {
|
|
111
|
+
const p = params;
|
|
112
|
+
if (p['secretKey'] == null && p['secret_key'] == null) {
|
|
113
|
+
params = { ...p, secretKey: this.secretKey };
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
const request = (0, api_request_1.createApiRequest)(method, params);
|
|
49
117
|
const response = await this.httpClient.executeRequest(request);
|
|
50
118
|
return (0, api_response_1.unmarshalData)(response.data);
|
|
51
119
|
}
|
|
@@ -78,25 +146,18 @@ class TradeClient {
|
|
|
78
146
|
}
|
|
79
147
|
// === Order operations ===
|
|
80
148
|
async placeOrder(order) {
|
|
81
|
-
|
|
82
|
-
if (!params.secretKey && this.secretKey)
|
|
83
|
-
params.secretKey = this.secretKey;
|
|
84
|
-
return this.callInto('place_order', params);
|
|
149
|
+
return this.callInto('place_order', { ...order, account: this.account });
|
|
85
150
|
}
|
|
86
151
|
async previewOrder(order) {
|
|
87
152
|
return this.callInto('preview_order', { ...order, account: this.account });
|
|
88
153
|
}
|
|
89
154
|
async modifyOrder(id, order) {
|
|
90
|
-
|
|
91
|
-
if (!params.secretKey && this.secretKey)
|
|
92
|
-
params.secretKey = this.secretKey;
|
|
93
|
-
return this.callInto('modify_order', params);
|
|
155
|
+
return this.callInto('modify_order', { ...order, account: this.account, id });
|
|
94
156
|
}
|
|
95
157
|
async cancelOrder(id, secretKey) {
|
|
96
|
-
const sk = secretKey ?? this.secretKey;
|
|
97
158
|
const params = { account: this.account, id };
|
|
98
|
-
if (
|
|
99
|
-
params.secretKey =
|
|
159
|
+
if (secretKey !== undefined)
|
|
160
|
+
params.secretKey = secretKey; // per-call override takes precedence; undefined means use config default
|
|
100
161
|
return this.callInto('cancel_order', params);
|
|
101
162
|
}
|
|
102
163
|
// === Order queries (BREAKING v0.4.0) ===
|
|
@@ -134,14 +195,11 @@ class TradeClient {
|
|
|
134
195
|
}
|
|
135
196
|
/**
|
|
136
197
|
* Get a single order by ID (v0.4.0 new).
|
|
137
|
-
* Wire method: `orders` with id/order_id param; server returns a single Order object
|
|
138
|
-
* (not `{items:[]}` envelope).
|
|
139
|
-
*
|
|
140
198
|
* Returns `undefined` if no match.
|
|
141
199
|
*/
|
|
142
200
|
async getOrder(req) {
|
|
143
201
|
const merged = withAccount(req, this.account);
|
|
144
|
-
const out = await this.callInto('
|
|
202
|
+
const out = await this.callInto('order_no', merged);
|
|
145
203
|
if (!out || (!out.id && !out.orderId))
|
|
146
204
|
return undefined;
|
|
147
205
|
normalizeOrderInPlace(out);
|
|
@@ -3,29 +3,33 @@
|
|
|
3
3
|
*/
|
|
4
4
|
import { TigerError } from './errors.js';
|
|
5
5
|
/**
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
6
|
+
* Replaces bare int64 values (≥17 digits, not part of a float) with quoted
|
|
7
|
+
* strings so they survive JSON.parse without precision loss.
|
|
8
|
+
* 17+ digits are guaranteed to exceed Number.MAX_SAFE_INTEGER (≈9.007e15),
|
|
9
|
+
* so 16-digit microsecond timestamps remain as numbers.
|
|
9
10
|
*/
|
|
10
11
|
function patchLargeIntegers(text) {
|
|
11
|
-
// Mask all existing JSON string values so their content isn't touched.
|
|
12
12
|
const strings = [];
|
|
13
13
|
const masked = text.replace(/"(?:[^"\\]|\\.)*"/g, (match) => {
|
|
14
14
|
strings.push(match);
|
|
15
15
|
return '\x01' + (strings.length - 1) + '\x01';
|
|
16
16
|
});
|
|
17
|
-
//
|
|
18
|
-
|
|
19
|
-
//
|
|
17
|
+
// Match bare integers preceded by a JSON structural character or line start,
|
|
18
|
+
// not followed by decimal point or exponent (avoids matching float components).
|
|
19
|
+
// The `m` flag makes `^` match line-start for multi-line JSON.
|
|
20
|
+
const patched = masked.replace(/([:,\[{]|^)\s*(-?\d{17,})(?![.\deE])/gm, (full, prefix, num) => {
|
|
21
|
+
return prefix + '"' + num + '"';
|
|
22
|
+
});
|
|
20
23
|
return patched.replace(/\x01(\d+)\x01/g, (_, i) => strings[parseInt(i, 10)]);
|
|
21
24
|
}
|
|
22
25
|
/**
|
|
23
|
-
* Parse the raw HTTP response body.
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
+
* Parse the raw HTTP response body. Throws TigerError when code != 0.
|
|
27
|
+
*
|
|
28
|
+
* patchLargeIntegers is applied before JSON.parse to preserve int64 order IDs
|
|
29
|
+
* (e.g. 28868646234578944) as strings rather than losing precision.
|
|
26
30
|
*/
|
|
27
31
|
export function parseApiResponse(body) {
|
|
28
|
-
const resp = JSON.parse(body);
|
|
32
|
+
const resp = JSON.parse(patchLargeIntegers(body));
|
|
29
33
|
if (resp.code !== 0) {
|
|
30
34
|
throw new TigerError(resp.code, resp.message);
|
|
31
35
|
}
|
|
@@ -33,15 +37,13 @@ export function parseApiResponse(body) {
|
|
|
33
37
|
}
|
|
34
38
|
/**
|
|
35
39
|
* Decode an ApiResponse.data payload into a typed value.
|
|
36
|
-
* Handles
|
|
37
|
-
*
|
|
40
|
+
* Handles double-encoded JSON (where `data` is a JSON string wrapping JSON),
|
|
41
|
+
* seen on some trade endpoints.
|
|
38
42
|
*/
|
|
39
43
|
export function unmarshalData(data) {
|
|
40
44
|
if (data == null)
|
|
41
45
|
return undefined;
|
|
42
46
|
if (typeof data === 'string') {
|
|
43
|
-
// Attempt to parse JSON string (double-encoded case).
|
|
44
|
-
// Patch large integers before parsing so int64 IDs survive as strings.
|
|
45
47
|
try {
|
|
46
48
|
return JSON.parse(patchLargeIntegers(data));
|
|
47
49
|
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Call the `user_token_refresh` API and return the new token string.
|
|
3
|
+
* Read-only: does NOT update the stored token.
|
|
4
|
+
* Use `refreshToken()` to also update the in-memory config and optionally persist.
|
|
5
|
+
*/
|
|
6
|
+
export async function queryToken(httpClient) {
|
|
7
|
+
return httpClient.queryToken();
|
|
8
|
+
}
|
|
9
|
+
/**
|
|
10
|
+
* Refresh the token: call the API, update the in-memory config token, and
|
|
11
|
+
* optionally persist via the provided TokenManager (file + writer callback).
|
|
12
|
+
*
|
|
13
|
+
* @param httpClient - The underlying HTTP client
|
|
14
|
+
* @param tokenManager - optional TokenManager for file persistence and writer callback
|
|
15
|
+
*/
|
|
16
|
+
export async function refreshToken(httpClient, tokenManager) {
|
|
17
|
+
return httpClient.refreshToken(tokenManager);
|
|
18
|
+
}
|
|
19
|
+
/**
|
|
20
|
+
* Start background token auto-refresh. Returns a TokenManager that can be
|
|
21
|
+
* used to stop the background task via `tm.stopAutoRefresh()`.
|
|
22
|
+
*
|
|
23
|
+
* @param httpClient - The underlying HTTP client
|
|
24
|
+
* @param refreshDurationSecs - Refresh threshold in seconds (minimum 30).
|
|
25
|
+
* When the token age exceeds this value the refresh is triggered.
|
|
26
|
+
* @param checkIntervalMs - How often to check whether a refresh is needed,
|
|
27
|
+
* in milliseconds. Defaults to 300 000 ms (5 minutes).
|
|
28
|
+
* @param tokenWriter - Optional callback invoked with the new token string
|
|
29
|
+
* after each successful refresh.
|
|
30
|
+
*/
|
|
31
|
+
export function startTokenAutoRefresh(httpClient, refreshDurationSecs, checkIntervalMs, tokenWriter) {
|
|
32
|
+
return httpClient.startTokenAutoRefresh(null, {
|
|
33
|
+
refreshDuration: refreshDurationSecs,
|
|
34
|
+
refreshInterval: checkIntervalMs,
|
|
35
|
+
tokenWriter,
|
|
36
|
+
});
|
|
37
|
+
}
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
* Priority: environment variables > code options (incl. config file) > defaults
|
|
10
10
|
*/
|
|
11
11
|
import { readFileSync, existsSync, statSync } from 'fs';
|
|
12
|
-
import { join } from 'path';
|
|
12
|
+
import { join, dirname } from 'path';
|
|
13
13
|
import { homedir, networkInterfaces } from 'os';
|
|
14
14
|
import { parsePropertiesString } from './config-parser.js';
|
|
15
15
|
import { queryDomains, resolveDynamicServerUrl, resolveDynamicQuoteServerUrl } from './domain.js';
|
|
@@ -22,6 +22,10 @@ const DEFAULT_TIMEOUT = 15;
|
|
|
22
22
|
const DEFAULT_SERVER_URL = 'https://openapi.tigerfintech.com/gateway';
|
|
23
23
|
/** Tiger public key for response signature verification (Base64-encoded) */
|
|
24
24
|
const TIGER_PUBLIC_KEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDNF3G8SoEcCZh2rshUbayDgLLrj6rKgzNMxDL2HSnKcB0+GPOsndqSv+a4IBu9+I3fyBp5hkyMMG2+AXugd9pMpy6VxJxlNjhX1MYbNTZJUT4nudki4uh+LMOkIBHOceGNXjgB+cXqmlUnjlqha/HgboeHSnSgpM3dKSJQlIOsDwIDAQAB';
|
|
25
|
+
/** Sandbox / QA tiger public key for response signature verification */
|
|
26
|
+
export const SANDBOX_TIGER_PUBLIC_KEY = 'MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCbm21i11hgAENGd3/f280PSe4g9YGkS3TEXBYMidihTvHHf' +
|
|
27
|
+
'+tJ0PYD0o3PruI0hl3qhEjHTAxb75T5YD3SGK4IBhHn/Rk6mhqlGgI+bBrBVYaXixmHfRo75RpUUuWACyeqQk' +
|
|
28
|
+
'ZckgR0McxuW9xRMIa2cXZOoL1E4SL4lXKGhKoWbwIDAQAB';
|
|
25
29
|
/** Environment variable names */
|
|
26
30
|
const ENV_TIGER_ID = 'TIGEROPEN_TIGER_ID';
|
|
27
31
|
const ENV_PRIVATE_KEY = 'TIGEROPEN_PRIVATE_KEY';
|
|
@@ -29,6 +33,9 @@ const ENV_ACCOUNT = 'TIGEROPEN_ACCOUNT';
|
|
|
29
33
|
const ENV_SECRET_KEY = 'TIGEROPEN_SECRET_KEY';
|
|
30
34
|
const ENV_TOKEN = 'TIGEROPEN_TOKEN';
|
|
31
35
|
const ENV_TOKEN_FILE = 'TIGEROPEN_TOKEN_FILE';
|
|
36
|
+
const ENV_TIGER_PUBLIC_KEY = 'TIGEROPEN_TIGER_PUBLIC_KEY';
|
|
37
|
+
const ENV_SERVER_URL = 'TIGEROPEN_SERVER_URL';
|
|
38
|
+
const ENV_QUOTE_SERVER_URL = 'TIGEROPEN_QUOTE_SERVER_URL';
|
|
32
39
|
/**
|
|
33
40
|
* Load and parse a Java-style properties file.
|
|
34
41
|
*
|
|
@@ -101,23 +108,32 @@ export function createClientConfig(options) {
|
|
|
101
108
|
const opts = options ?? {};
|
|
102
109
|
// Load properties: explicit path > auto-discovery
|
|
103
110
|
let fileProps = {};
|
|
111
|
+
let configFileDir;
|
|
104
112
|
if (opts.propertiesFilePath) {
|
|
105
113
|
// Support both directory path and full file path
|
|
106
114
|
let filePath = opts.propertiesFilePath;
|
|
107
115
|
if (existsSync(filePath) && statSync(filePath).isDirectory()) {
|
|
116
|
+
configFileDir = filePath;
|
|
108
117
|
filePath = join(filePath, CONFIG_FILE_NAME);
|
|
109
118
|
}
|
|
110
119
|
else if (!existsSync(filePath) && !filePath.endsWith('.properties')) {
|
|
111
120
|
// Path doesn't exist as-is; try treating it as a directory
|
|
112
121
|
const asDir = join(filePath, CONFIG_FILE_NAME);
|
|
113
|
-
if (existsSync(asDir))
|
|
122
|
+
if (existsSync(asDir)) {
|
|
123
|
+
configFileDir = filePath;
|
|
114
124
|
filePath = asDir;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
else {
|
|
128
|
+
// It's a file path: derive directory from it
|
|
129
|
+
configFileDir = dirname(filePath);
|
|
115
130
|
}
|
|
116
131
|
fileProps = loadPropertiesFile(filePath);
|
|
117
132
|
}
|
|
118
133
|
else {
|
|
119
134
|
const discovered = discoverConfigFile();
|
|
120
135
|
if (discovered) {
|
|
136
|
+
configFileDir = dirname(discovered);
|
|
121
137
|
fileProps = loadPropertiesFile(discovered);
|
|
122
138
|
}
|
|
123
139
|
}
|
|
@@ -130,12 +146,15 @@ export function createClientConfig(options) {
|
|
|
130
146
|
const license = opts.license || fileProps['license'];
|
|
131
147
|
const language = opts.language || fileProps['language'] || DEFAULT_LANGUAGE;
|
|
132
148
|
const timezone = opts.timezone || fileProps['timezone'];
|
|
149
|
+
// tiger_public_key from properties file (e.g. for QA/sandbox environments)
|
|
150
|
+
let tigerPublicKey = opts.tigerPublicKey || fileProps['tiger_public_key'] || TIGER_PUBLIC_KEY;
|
|
133
151
|
// Environment variables override everything
|
|
134
152
|
const envTigerId = process.env[ENV_TIGER_ID];
|
|
135
153
|
const envPrivateKey = process.env[ENV_PRIVATE_KEY];
|
|
136
154
|
const envAccount = process.env[ENV_ACCOUNT];
|
|
137
155
|
const envSecretKey = process.env[ENV_SECRET_KEY];
|
|
138
156
|
const envToken = process.env[ENV_TOKEN];
|
|
157
|
+
const envTigerPublicKey = process.env[ENV_TIGER_PUBLIC_KEY];
|
|
139
158
|
if (envTigerId) {
|
|
140
159
|
tigerId = envTigerId;
|
|
141
160
|
}
|
|
@@ -151,6 +170,9 @@ export function createClientConfig(options) {
|
|
|
151
170
|
if (envToken) {
|
|
152
171
|
token = envToken;
|
|
153
172
|
}
|
|
173
|
+
if (envTigerPublicKey) {
|
|
174
|
+
tigerPublicKey = envTigerPublicKey;
|
|
175
|
+
}
|
|
154
176
|
// Token loading priority: env TIGEROPEN_TOKEN > tokenLoader > token file (TIGEROPEN_TOKEN_FILE or default)
|
|
155
177
|
if (!token) {
|
|
156
178
|
if (opts.tokenLoader) {
|
|
@@ -171,8 +193,9 @@ export function createClientConfig(options) {
|
|
|
171
193
|
}
|
|
172
194
|
}
|
|
173
195
|
else {
|
|
174
|
-
// Load from token file: TIGEROPEN_TOKEN_FILE env var > default
|
|
175
|
-
const tokenFilePath = process.env[ENV_TOKEN_FILE]
|
|
196
|
+
// Load from token file: TIGEROPEN_TOKEN_FILE env var > config dir > default
|
|
197
|
+
const tokenFilePath = process.env[ENV_TOKEN_FILE]
|
|
198
|
+
|| (configFileDir ? join(configFileDir, 'tiger_openapi_token.properties') : 'tiger_openapi_token.properties');
|
|
176
199
|
const tm = new TokenManager({ filePath: tokenFilePath });
|
|
177
200
|
try {
|
|
178
201
|
token = tm.loadTokenSync();
|
|
@@ -189,29 +212,53 @@ export function createClientConfig(options) {
|
|
|
189
212
|
if (!privateKey) {
|
|
190
213
|
throw new Error(`privateKey is required. Set it via options.privateKey, a config file, or the ${ENV_PRIVATE_KEY} environment variable.`);
|
|
191
214
|
}
|
|
192
|
-
// Resolve server URL: explicit > dynamic domain > default
|
|
215
|
+
// Resolve server URL: env var > explicit opts > file > dynamic domain > default
|
|
193
216
|
const enableDynamicDomain = opts.enableDynamicDomain ?? true;
|
|
194
217
|
let serverUrl;
|
|
195
218
|
let quoteServerUrl;
|
|
196
219
|
// Query dynamic domains once (may spawn a child process); reuse for both URLs.
|
|
197
220
|
let domainConf = {};
|
|
198
|
-
|
|
221
|
+
const envServerUrl = process.env[ENV_SERVER_URL];
|
|
222
|
+
const envQuoteServerUrl = process.env[ENV_QUOTE_SERVER_URL];
|
|
223
|
+
const needServerUrl = !envServerUrl && !opts.serverUrl && !fileProps['server_url'];
|
|
224
|
+
const needQuoteServerUrl = !envQuoteServerUrl && !opts.quoteServerUrl && !fileProps['quote_server_url'];
|
|
225
|
+
if (enableDynamicDomain && (needServerUrl || needQuoteServerUrl)) {
|
|
199
226
|
domainConf = queryDomains(license);
|
|
200
227
|
}
|
|
201
|
-
if (
|
|
228
|
+
if (envServerUrl) {
|
|
229
|
+
serverUrl = envServerUrl;
|
|
230
|
+
}
|
|
231
|
+
else if (opts.serverUrl) {
|
|
202
232
|
serverUrl = opts.serverUrl;
|
|
203
233
|
}
|
|
234
|
+
else if (fileProps['server_url']) {
|
|
235
|
+
serverUrl = fileProps['server_url'];
|
|
236
|
+
}
|
|
204
237
|
else {
|
|
205
238
|
const dynamicUrl = resolveDynamicServerUrl(domainConf, license);
|
|
206
239
|
serverUrl = dynamicUrl || DEFAULT_SERVER_URL;
|
|
207
240
|
}
|
|
208
|
-
// Resolve quote server URL: explicit > dynamic domain > serverUrl fallback
|
|
209
|
-
if (
|
|
241
|
+
// Resolve quote server URL: env var > explicit opts > file > dynamic domain > serverUrl fallback
|
|
242
|
+
if (envQuoteServerUrl) {
|
|
243
|
+
quoteServerUrl = envQuoteServerUrl;
|
|
244
|
+
}
|
|
245
|
+
else if (opts.quoteServerUrl) {
|
|
210
246
|
quoteServerUrl = opts.quoteServerUrl;
|
|
211
247
|
}
|
|
248
|
+
else if (fileProps['quote_server_url']) {
|
|
249
|
+
quoteServerUrl = fileProps['quote_server_url'];
|
|
250
|
+
}
|
|
212
251
|
else {
|
|
213
|
-
|
|
214
|
-
|
|
252
|
+
// Only use dynamic domain if serverUrl itself came from dynamic resolution (not pinned by env/opts/file).
|
|
253
|
+
// When serverUrl is pinned, prefer it as the quote fallback to avoid mixing environments.
|
|
254
|
+
const serverUrlPinned = !!(envServerUrl || opts.serverUrl || fileProps['server_url']);
|
|
255
|
+
if (!serverUrlPinned) {
|
|
256
|
+
const dynamicQuoteUrl = resolveDynamicQuoteServerUrl(domainConf, license);
|
|
257
|
+
quoteServerUrl = dynamicQuoteUrl || serverUrl;
|
|
258
|
+
}
|
|
259
|
+
else {
|
|
260
|
+
quoteServerUrl = serverUrl;
|
|
261
|
+
}
|
|
215
262
|
}
|
|
216
263
|
return {
|
|
217
264
|
tigerId,
|
|
@@ -230,7 +277,7 @@ export function createClientConfig(options) {
|
|
|
230
277
|
serverUrl,
|
|
231
278
|
quoteServerUrl,
|
|
232
279
|
deviceId: detectDeviceId(),
|
|
233
|
-
tigerPublicKey
|
|
280
|
+
tigerPublicKey,
|
|
234
281
|
};
|
|
235
282
|
}
|
|
236
283
|
/**
|
|
@@ -33,34 +33,34 @@ export function parsePropertiesString(content) {
|
|
|
33
33
|
let continuation = false;
|
|
34
34
|
for (const rawLine of lines) {
|
|
35
35
|
if (continuation) {
|
|
36
|
-
// 续行:去除前导空格后拼接
|
|
37
36
|
const trimmedLeft = rawLine.replace(/^[ \t]+/, '');
|
|
38
|
-
if (trimmedLeft.
|
|
37
|
+
if (trimmedLeft === '' || trimmedLeft.startsWith('#') || trimmedLeft.startsWith('!')) {
|
|
38
|
+
continuation = false;
|
|
39
|
+
}
|
|
40
|
+
else if (endsWithContinuation(trimmedLeft)) {
|
|
39
41
|
currentLine += trimmedLeft.slice(0, -1);
|
|
40
42
|
continue;
|
|
41
43
|
}
|
|
42
|
-
|
|
43
|
-
|
|
44
|
+
else {
|
|
45
|
+
currentLine += trimmedLeft;
|
|
46
|
+
continuation = false;
|
|
47
|
+
}
|
|
44
48
|
}
|
|
45
49
|
else {
|
|
46
50
|
const trimmed = rawLine.trim();
|
|
47
|
-
// 跳过空行
|
|
48
51
|
if (trimmed === '') {
|
|
49
52
|
continue;
|
|
50
53
|
}
|
|
51
|
-
// 跳过注释行
|
|
52
54
|
if (trimmed.startsWith('#') || trimmed.startsWith('!')) {
|
|
53
55
|
continue;
|
|
54
56
|
}
|
|
55
|
-
|
|
56
|
-
if (trimmed.endsWith('\\')) {
|
|
57
|
+
if (endsWithContinuation(trimmed)) {
|
|
57
58
|
currentLine = trimmed.slice(0, -1);
|
|
58
59
|
continuation = true;
|
|
59
60
|
continue;
|
|
60
61
|
}
|
|
61
62
|
currentLine = trimmed;
|
|
62
63
|
}
|
|
63
|
-
// 解析键值对
|
|
64
64
|
const result = parseKeyValue(currentLine);
|
|
65
65
|
if (result) {
|
|
66
66
|
props[result[0]] = result[1];
|
|
@@ -76,6 +76,16 @@ export function parsePropertiesString(content) {
|
|
|
76
76
|
}
|
|
77
77
|
return props;
|
|
78
78
|
}
|
|
79
|
+
/**
|
|
80
|
+
* 判断一行是否以续行标记结尾(奇数个末尾反斜杠)。
|
|
81
|
+
*/
|
|
82
|
+
function endsWithContinuation(line) {
|
|
83
|
+
let count = 0;
|
|
84
|
+
for (let i = line.length - 1; i >= 0 && line[i] === '\\'; i--) {
|
|
85
|
+
count++;
|
|
86
|
+
}
|
|
87
|
+
return count % 2 === 1;
|
|
88
|
+
}
|
|
79
89
|
/**
|
|
80
90
|
* 解析单行键值对,支持 = 和 : 分隔符。
|
|
81
91
|
* 值中可以包含 = 或 :,只按第一个分隔符拆分。
|