@tigeropenapi/tigeropen 0.5.4 → 0.5.7

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 (73) hide show
  1. package/README.md +80 -0
  2. package/dist/cjs/auth/authentication.js +74 -0
  3. package/dist/cjs/auth/index.js +24 -0
  4. package/dist/cjs/auth/oauth2/client-registration.js +44 -0
  5. package/dist/cjs/auth/oauth2/device-authorization.js +40 -0
  6. package/dist/cjs/auth/oauth2/errors.js +64 -0
  7. package/dist/cjs/auth/oauth2/http-utils.js +112 -0
  8. package/dist/cjs/auth/oauth2/index.js +42 -0
  9. package/dist/cjs/auth/oauth2/loopback-receiver.js +215 -0
  10. package/dist/cjs/auth/oauth2/metadata.js +34 -0
  11. package/dist/cjs/auth/oauth2/oauth2-authentication.js +74 -0
  12. package/dist/cjs/auth/oauth2/session-manager.js +762 -0
  13. package/dist/cjs/auth/oauth2/token-store.js +252 -0
  14. package/dist/cjs/auth/oauth2/token.js +76 -0
  15. package/dist/cjs/push/index.js +1 -0
  16. package/dist/cjs/push/pb/Request.js +12 -1
  17. package/dist/cjs/push/proto-message.js +5 -1
  18. package/dist/cjs/push/push-client.js +34 -16
  19. package/dist/cjs/push/tick-util.js +172 -0
  20. package/dist/cjs/quote/quote-client.js +59 -12
  21. package/dist/cjs/trade/trade-client.js +55 -6
  22. package/dist/cjs/version.js +1 -1
  23. package/dist/esm/auth/authentication.js +68 -0
  24. package/dist/esm/auth/index.js +3 -0
  25. package/dist/esm/auth/oauth2/client-registration.js +40 -0
  26. package/dist/esm/auth/oauth2/device-authorization.js +36 -0
  27. package/dist/esm/auth/oauth2/errors.js +59 -0
  28. package/dist/esm/auth/oauth2/http-utils.js +105 -0
  29. package/dist/esm/auth/oauth2/index.js +11 -0
  30. package/dist/esm/auth/oauth2/loopback-receiver.js +178 -0
  31. package/dist/esm/auth/oauth2/metadata.js +30 -0
  32. package/dist/esm/auth/oauth2/oauth2-authentication.js +70 -0
  33. package/dist/esm/auth/oauth2/session-manager.js +725 -0
  34. package/dist/esm/auth/oauth2/token-store.js +214 -0
  35. package/dist/esm/auth/oauth2/token.js +71 -0
  36. package/dist/esm/package.json +3 -0
  37. package/dist/esm/push/index.js +1 -0
  38. package/dist/esm/push/pb/Request.js +12 -1
  39. package/dist/esm/push/proto-message.js +5 -1
  40. package/dist/esm/push/push-client.js +34 -16
  41. package/dist/esm/push/tick-util.js +165 -0
  42. package/dist/esm/quote/quote-client.js +59 -12
  43. package/dist/esm/trade/trade-client.js +55 -6
  44. package/dist/esm/version.js +1 -1
  45. package/dist/types/auth/authentication.d.ts +98 -0
  46. package/dist/types/auth/index.d.ts +4 -0
  47. package/dist/types/auth/oauth2/client-registration.d.ts +32 -0
  48. package/dist/types/auth/oauth2/device-authorization.d.ts +40 -0
  49. package/dist/types/auth/oauth2/errors.d.ts +46 -0
  50. package/dist/types/auth/oauth2/http-utils.d.ts +21 -0
  51. package/dist/types/auth/oauth2/index.d.ts +16 -0
  52. package/dist/types/auth/oauth2/loopback-receiver.d.ts +29 -0
  53. package/dist/types/auth/oauth2/metadata.d.ts +32 -0
  54. package/dist/types/auth/oauth2/oauth2-authentication.d.ts +31 -0
  55. package/dist/types/auth/oauth2/session-manager.d.ts +241 -0
  56. package/dist/types/auth/oauth2/token-store.d.ts +49 -0
  57. package/dist/types/auth/oauth2/token.d.ts +56 -0
  58. package/dist/types/index.d.ts +2 -2
  59. package/dist/types/model/order.d.ts +62 -11
  60. package/dist/types/model/quote-requests.d.ts +21 -2
  61. package/dist/types/model/quote.d.ts +72 -14
  62. package/dist/types/model/trade-requests.d.ts +17 -4
  63. package/dist/types/model/trade.d.ts +11 -1
  64. package/dist/types/push/callbacks.d.ts +3 -7
  65. package/dist/types/push/index.d.ts +2 -1
  66. package/dist/types/push/pb/Request.d.ts +1 -0
  67. package/dist/types/push/proto-message.d.ts +4 -1
  68. package/dist/types/push/push-client.d.ts +33 -0
  69. package/dist/types/push/tick-util.d.ts +30 -0
  70. package/dist/types/quote/quote-client.d.ts +27 -6
  71. package/dist/types/trade/trade-client.d.ts +27 -3
  72. package/dist/types/version.d.ts +1 -1
  73. package/package.json +8 -4
@@ -0,0 +1,214 @@
1
+ /**
2
+ * File-backed OAuth 2.0 authorization state.
3
+ *
4
+ * Storage layout:
5
+ *
6
+ * ~/.tiger/openapi/
7
+ * tokens/{clientId}.json authorization state, mode 0600, removed on logout
8
+ * clients/{issuerHash}-{surface}.json dynamic registration, retained across logout
9
+ *
10
+ * Registration files are keyed by issuer because the client ID is not yet available
11
+ * during lookup, and by surface so that each entry point registers its own client.
12
+ * Without the surface, whichever entry point ran first would own the registration and
13
+ * every other one would silently inherit its identity: one name on the user's
14
+ * authorization page, one revocation switch for all of them, and one shared token file
15
+ * whose refresh-token rotation they would race over.
16
+ *
17
+ * Token files need no surface: each surface registers a distinct client ID, so their
18
+ * token files are already distinct. A manually configured client ID is shared on
19
+ * purpose -- one client ID means one application.
20
+ *
21
+ * Storage root precedence is: explicit argument, `TIGEROPEN_HOME`, then
22
+ * `~/.tiger/openapi`. The format is shared with the Java and Python SDKs.
23
+ */
24
+ import { createHash } from 'crypto';
25
+ import * as fs from 'fs';
26
+ import { homedir } from 'os';
27
+ import * as path from 'path';
28
+ import { OAuth2ClientRegistration } from './client-registration.js';
29
+ import { OAuth2Category, OAuth2Error } from './errors.js';
30
+ import { OAuth2Token, TOKEN_SCHEMA_VERSION } from './token.js';
31
+ export const ENV_HOME = 'TIGEROPEN_HOME';
32
+ /**
33
+ * Entry point owning a registration. Tools built on this SDK must pass their own value,
34
+ * otherwise they would be filed as the TypeScript SDK and share its client. Keep in
35
+ * step with the Java SDK's `sdk-java` and the Python SDK's `sdk-python`.
36
+ */
37
+ export const DEFAULT_SURFACE = 'sdk-typescript';
38
+ const SAFE_CHARS = /[^a-zA-Z0-9\-_.]/g;
39
+ /**
40
+ * Surfaces become part of a file name, so they are restricted rather than sanitised: a
41
+ * silently rewritten surface would file two entry points under one name.
42
+ */
43
+ const SURFACE_PATTERN = /^[a-z0-9][a-z0-9-]*$/;
44
+ export class OAuth2TokenStore {
45
+ /**
46
+ * @param home storage root; defaults to `TIGEROPEN_HOME` then `~/.tiger/openapi`
47
+ * @param surface entry point owning the registration, e.g. `sdk-typescript`, `cli`.
48
+ * Lowercase alphanumerics and hyphens only
49
+ */
50
+ constructor(home, surface = DEFAULT_SURFACE) {
51
+ const base = home || process.env[ENV_HOME] || path.join(homedir(), '.tiger', 'openapi');
52
+ this.home = path.resolve(expandHome(base));
53
+ this.surface = validateSurface(surface);
54
+ }
55
+ /** Load a token, treating a missing file or issuer mismatch as a cache miss. */
56
+ loadToken(clientId, expectedIssuer) {
57
+ const file = this.tokenFile(clientId);
58
+ if (!fs.existsSync(file))
59
+ return null;
60
+ let token;
61
+ try {
62
+ token = OAuth2Token.fromJSON(JSON.parse(fs.readFileSync(file, 'utf-8')));
63
+ }
64
+ catch (e) {
65
+ // A corrupt file tells us nothing; treat it as no authorization rather than crash.
66
+ console.error(`[TigerOpen] read token file fail: ${file}`, e);
67
+ return null;
68
+ }
69
+ if (!token)
70
+ return null;
71
+ if (token.schemaVersion !== TOKEN_SCHEMA_VERSION) {
72
+ // Unknown schema versions are not interpreted.
73
+ throw new OAuth2Error(OAuth2Category.StorageFailed, `unsupported token schema_version: ${token.schemaVersion}`);
74
+ }
75
+ if (expectedIssuer !== undefined && expectedIssuer !== token.issuer) {
76
+ return null;
77
+ }
78
+ return token;
79
+ }
80
+ saveToken(token) {
81
+ if (!token || !token.clientId) {
82
+ throw new OAuth2Error(OAuth2Category.StorageFailed, 'token or clientId is empty, cannot save');
83
+ }
84
+ writeAtomic(this.tokenFile(token.clientId), JSON.stringify(token.toJSON(), null, 2));
85
+ }
86
+ /**
87
+ * Delete authorization state while retaining client registration.
88
+ *
89
+ * @returns the path removed, or null when there was nothing to remove. A clientId is
90
+ * not its own file name, so a caller reporting what it deleted has to be told rather
91
+ * than build the path itself
92
+ */
93
+ deleteToken(clientId) {
94
+ if (!clientId)
95
+ return null;
96
+ const file = this.tokenFile(clientId);
97
+ return deleteFile(file) ? file : null;
98
+ }
99
+ loadClientId(issuer) {
100
+ return this.loadClientRegistration(issuer)?.clientId ?? null;
101
+ }
102
+ loadClientRegistration(issuer) {
103
+ const file = this.clientFile(issuer);
104
+ if (!fs.existsSync(file))
105
+ return null;
106
+ try {
107
+ return OAuth2ClientRegistration.fromJSON(JSON.parse(fs.readFileSync(file, 'utf-8')));
108
+ }
109
+ catch (e) {
110
+ console.error(`[TigerOpen] read client registration file fail: ${file}`, e);
111
+ return null;
112
+ }
113
+ }
114
+ saveClientRegistration(registration) {
115
+ if (!registration || !registration.issuer) {
116
+ throw new OAuth2Error(OAuth2Category.StorageFailed, 'registration or issuer is empty, cannot save');
117
+ }
118
+ writeAtomic(this.clientFile(registration.issuer), JSON.stringify(registration.toJSON(), null, 2));
119
+ }
120
+ /** Delete registration state after an `invalid_client` response. */
121
+ deleteClientRegistration(issuer) {
122
+ if (!issuer)
123
+ return;
124
+ deleteFile(this.clientFile(issuer));
125
+ }
126
+ tokenFile(clientId) {
127
+ return path.join(this.home, 'tokens', `${safeFileName(clientId)}.json`);
128
+ }
129
+ clientFile(issuer) {
130
+ return path.join(this.home, 'clients', `${hashIssuer(issuer)}-${this.surface}.json`);
131
+ }
132
+ }
133
+ /** Return the Java-compatible 32-character SHA-256 issuer file key. */
134
+ export function hashIssuer(issuer) {
135
+ return createHash('sha256').update(issuer || '', 'utf-8').digest('hex').slice(0, 32);
136
+ }
137
+ /** Sanitize an untrusted client ID for use as a file name. */
138
+ function safeFileName(raw) {
139
+ let name = (raw || '').replace(SAFE_CHARS, '_');
140
+ while (name.includes('..')) {
141
+ name = name.replace(/\.\./g, '__');
142
+ }
143
+ return name || '_';
144
+ }
145
+ /** Reject a surface that cannot be used verbatim as part of a file name. */
146
+ function validateSurface(surface) {
147
+ if (typeof surface !== 'string' || !SURFACE_PATTERN.test(surface)) {
148
+ throw new OAuth2Error(OAuth2Category.StorageFailed, `invalid surface '${surface}': expected lowercase alphanumerics and hyphens, e.g. 'cli'`);
149
+ }
150
+ return surface;
151
+ }
152
+ function expandHome(value) {
153
+ if (value === '~')
154
+ return homedir();
155
+ if (value.startsWith('~/') || value.startsWith(`~${path.sep}`)) {
156
+ return path.join(homedir(), value.slice(2));
157
+ }
158
+ return value;
159
+ }
160
+ /** @returns true when the file was there and is now gone */
161
+ function deleteFile(file) {
162
+ try {
163
+ if (fs.existsSync(file)) {
164
+ fs.unlinkSync(file);
165
+ return true;
166
+ }
167
+ }
168
+ catch (e) {
169
+ console.error(`[TigerOpen] delete file fail: ${file}`, e);
170
+ }
171
+ return false;
172
+ }
173
+ /**
174
+ * Atomically replace a file through a same-directory temporary file.
175
+ *
176
+ * Same-filesystem replacement prevents partial JSON writes. Directories and files are
177
+ * restricted to modes 0700 and 0600 where supported.
178
+ */
179
+ function writeAtomic(file, content) {
180
+ const dir = path.dirname(file);
181
+ try {
182
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
183
+ restrict(dir, 0o700);
184
+ // Same directory, so the rename below stays on one filesystem and is atomic.
185
+ const tmp = path.join(dir, `.tmp-${process.pid}-${Date.now()}-${Math.random().toString(36).slice(2)}.json`);
186
+ try {
187
+ fs.writeFileSync(tmp, content, { encoding: 'utf-8', mode: 0o600 });
188
+ restrict(tmp, 0o600);
189
+ fs.renameSync(tmp, file);
190
+ }
191
+ catch (e) {
192
+ try {
193
+ fs.unlinkSync(tmp);
194
+ }
195
+ catch {
196
+ // The temporary file was never created; nothing to clean up.
197
+ }
198
+ throw e;
199
+ }
200
+ restrict(file, 0o600);
201
+ }
202
+ catch (e) {
203
+ throw new OAuth2Error(OAuth2Category.StorageFailed, `write file fail: ${file}`, e);
204
+ }
205
+ }
206
+ /** Restrict a credential file or directory where the platform supports it. */
207
+ function restrict(target, mode) {
208
+ try {
209
+ fs.chmodSync(target, mode);
210
+ }
211
+ catch {
212
+ // Windows and some network filesystems have no POSIX modes; the write still stands.
213
+ }
214
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Persisted OAuth 2.0 authorization state.
3
+ *
4
+ * The `issuer` and `clientId` bind credentials to an authorization server and client,
5
+ * preventing credential reuse across environments. The on-disk field names are
6
+ * snake_case and shared with the Java and Python SDKs.
7
+ */
8
+ /** On-disk format version. An unrecognised version is an error, not a guess. */
9
+ export const TOKEN_SCHEMA_VERSION = 1;
10
+ export function currentMillis() {
11
+ return Date.now();
12
+ }
13
+ /** An access token and its metadata. */
14
+ export class OAuth2Token {
15
+ constructor(init = {}) {
16
+ this.schemaVersion = init.schemaVersion ?? TOKEN_SCHEMA_VERSION;
17
+ this.issuer = init.issuer;
18
+ this.clientId = init.clientId;
19
+ this.accessToken = init.accessToken;
20
+ this.refreshToken = init.refreshToken;
21
+ this.tokenType = init.tokenType || 'Bearer';
22
+ this.scope = init.scope;
23
+ this.expiresAt = init.expiresAt ?? 0;
24
+ }
25
+ /** Time until expiry; negative once already expired. */
26
+ remainingMillis() {
27
+ return this.expiresAt - currentMillis();
28
+ }
29
+ /**
30
+ * @param aheadMillis how early to refresh. The window exists because time passes
31
+ * between sending a request and the server validating it, so using a token right up
32
+ * to its expiry produces intermittent 401s
33
+ */
34
+ needsRefresh(aheadMillis) {
35
+ return this.remainingMillis() <= aheadMillis;
36
+ }
37
+ hasRefreshToken() {
38
+ return !!this.refreshToken;
39
+ }
40
+ toJSON() {
41
+ return {
42
+ schema_version: this.schemaVersion,
43
+ issuer: this.issuer,
44
+ client_id: this.clientId,
45
+ access_token: this.accessToken,
46
+ refresh_token: this.refreshToken,
47
+ token_type: this.tokenType,
48
+ scope: this.scope,
49
+ expires_at: this.expiresAt,
50
+ };
51
+ }
52
+ static fromJSON(data) {
53
+ if (!data)
54
+ return null;
55
+ return new OAuth2Token({
56
+ schemaVersion: data.schema_version ?? TOKEN_SCHEMA_VERSION,
57
+ issuer: data.issuer,
58
+ clientId: data.client_id,
59
+ accessToken: data.access_token,
60
+ refreshToken: data.refresh_token,
61
+ tokenType: data.token_type || 'Bearer',
62
+ scope: data.scope,
63
+ expiresAt: data.expires_at ?? 0,
64
+ });
65
+ }
66
+ /** Prints no token values. */
67
+ toString() {
68
+ return `OAuth2Token{clientId=${this.clientId}, issuer=${this.issuer}, scope=${this.scope}, `
69
+ + `expiresAt=${this.expiresAt}, hasRefreshToken=${this.hasRefreshToken()}}`;
70
+ }
71
+ }
@@ -0,0 +1,3 @@
1
+ {
2
+ "type": "module"
3
+ }
@@ -4,5 +4,6 @@
4
4
  export { SubjectType } from './push-message.js';
5
5
  export * from './callbacks.js';
6
6
  export * from './varint.js';
7
+ export * from './tick-util.js';
7
8
  export * from './proto-message.js';
8
9
  export { PushClient, ConnectionState } from './push-client.js';
@@ -200,7 +200,7 @@ export const Request_Connect = {
200
200
  },
201
201
  };
202
202
  function createBaseRequest_Subscribe() {
203
- return { dataType: 0, symbols: undefined, account: undefined, market: undefined };
203
+ return { dataType: 0, symbols: undefined, account: undefined, market: undefined, assetQuoteType: undefined };
204
204
  }
205
205
  export const Request_Subscribe = {
206
206
  encode(message, writer = new BinaryWriter()) {
@@ -216,6 +216,9 @@ export const Request_Subscribe = {
216
216
  if (message.market !== undefined) {
217
217
  writer.uint32(34).string(message.market);
218
218
  }
219
+ if (message.assetQuoteType !== undefined) {
220
+ writer.uint32(42).string(message.assetQuoteType);
221
+ }
219
222
  return writer;
220
223
  },
221
224
  decode(input, length) {
@@ -253,6 +256,13 @@ export const Request_Subscribe = {
253
256
  message.market = reader.string();
254
257
  continue;
255
258
  }
259
+ case 5: {
260
+ if (tag !== 42) {
261
+ break;
262
+ }
263
+ message.assetQuoteType = reader.string();
264
+ continue;
265
+ }
256
266
  }
257
267
  if ((tag & 7) === 4 || tag === 0) {
258
268
  break;
@@ -270,6 +280,7 @@ export const Request_Subscribe = {
270
280
  message.symbols = object.symbols ?? undefined;
271
281
  message.account = object.account ?? undefined;
272
282
  message.market = object.market ?? undefined;
283
+ message.assetQuoteType = object.assetQuoteType ?? undefined;
273
284
  return message;
274
285
  },
275
286
  };
@@ -45,8 +45,11 @@ export function buildHeartBeatMessage() {
45
45
  }
46
46
  /**
47
47
  * 构建 SUBSCRIBE 请求消息
48
+ *
49
+ * @param assetQuoteType 资产行情类型枚举名(ETH / RTH / OVERNIGHT),
50
+ * 仅对 Asset / Position 两种订阅有意义,其余场景传 undefined
48
51
  */
49
- export function buildSubscribeMessage(dataType, symbols, account, market) {
52
+ export function buildSubscribeMessage(dataType, symbols, account, market, assetQuoteType) {
50
53
  return Request.fromPartial({
51
54
  command: SocketCommon_Command.SUBSCRIBE,
52
55
  id: nextRequestId(),
@@ -55,6 +58,7 @@ export function buildSubscribeMessage(dataType, symbols, account, market) {
55
58
  symbols,
56
59
  account,
57
60
  market,
61
+ assetQuoteType,
58
62
  }),
59
63
  });
60
64
  }
@@ -11,6 +11,7 @@
11
11
  import * as tls from 'tls';
12
12
  import { signWithRSA } from '../signer/signer.js';
13
13
  import { SubjectType } from './push-message.js';
14
+ import { convertTradeTick } from './tick-util.js';
14
15
  import { Request } from './pb/Request.js';
15
16
  import { Response } from './pb/Response.js';
16
17
  import { SocketCommon_Command, SocketCommon_DataType } from './pb/SocketCommon.js';
@@ -58,6 +59,8 @@ export class PushClient {
58
59
  this.subscriptions = new Map();
59
60
  /** Account-level subscriptions: subject -> account string used at subscribe time */
60
61
  this.accountSubs = new Map();
62
+ /** Account-level subscriptions: subject -> assetQuoteType used at subscribe time (reconnect replay) */
63
+ this.accountSubQuoteTypes = new Map();
61
64
  this.heartbeatTimer = null;
62
65
  this.reconnectTimer = null;
63
66
  this.stopped = false;
@@ -318,7 +321,7 @@ export class PushClient {
318
321
  this.doSubscribe(subject, Array.from(symbols));
319
322
  }
320
323
  for (const [subject, account] of this.accountSubs) {
321
- this.doSubscribe(subject, undefined, account);
324
+ this.doSubscribe(subject, undefined, account, undefined, this.accountSubQuoteTypes.get(subject));
322
325
  }
323
326
  }
324
327
  /** Dispatch push data to the appropriate callback based on dataType */
@@ -342,8 +345,10 @@ export class PushClient {
342
345
  cb.onDepth?.(pushData.quoteDepthData);
343
346
  break;
344
347
  case SocketCommon_DataType.TradeTick:
348
+ if (pushData.tickData)
349
+ cb.onFullTick?.(pushData.tickData);
345
350
  if (pushData.tradeTickData)
346
- cb.onTick?.(pushData.tradeTickData);
351
+ cb.onTick?.(convertTradeTick(pushData.tradeTickData));
347
352
  break;
348
353
  case SocketCommon_DataType.Asset:
349
354
  if (pushData.assetData)
@@ -379,18 +384,17 @@ export class PushClient {
379
384
  cb.onQuote?.(pushData.quoteData);
380
385
  break;
381
386
  default:
382
- // FullTick and QuoteBBO use TradeTick/Quote dataType
383
- // TickData (fullTick) via tickData field
387
+ // QuoteBBO and some full-tick payloads reuse an existing dataType.
384
388
  if (pushData.tickData)
385
389
  cb.onFullTick?.(pushData.tickData);
386
390
  break;
387
391
  }
388
392
  }
389
393
  /** Internal subscribe method */
390
- doSubscribe(subject, symbols, account, market) {
394
+ doSubscribe(subject, symbols, account, market, assetQuoteType) {
391
395
  const dataType = subjectToDataType(subject);
392
396
  const symbolsStr = symbols ? symbols.join(',') : undefined;
393
- const req = buildSubscribeMessage(dataType, symbolsStr, account, market);
397
+ const req = buildSubscribeMessage(dataType, symbolsStr, account, market, assetQuoteType);
394
398
  this.sendMessage(req);
395
399
  }
396
400
  /** Internal unsubscribe method */
@@ -550,27 +554,41 @@ export class PushClient {
550
554
  this.removeSubscription(SubjectType.Market, [market]);
551
555
  }
552
556
  // ===== Account push subscribe/unsubscribe =====
553
- /** Subscribe to asset changes */
554
- subscribeAsset(account) {
555
- const acct = account || this.config.account;
556
- this.doSubscribe(SubjectType.Asset, undefined, acct);
557
- this.accountSubs.set(SubjectType.Asset, acct);
557
+ /**
558
+ * Shared implementation for the account/options overloads of
559
+ * subscribeAsset / subscribePosition.
560
+ */
561
+ subscribeAccount(subject, accountOrOptions) {
562
+ const opts = typeof accountOrOptions === 'string'
563
+ ? { account: accountOrOptions }
564
+ : (accountOrOptions ?? {});
565
+ const acct = opts.account || this.config.account;
566
+ this.doSubscribe(subject, undefined, acct, undefined, opts.assetQuoteType);
567
+ this.accountSubs.set(subject, acct);
568
+ if (opts.assetQuoteType) {
569
+ this.accountSubQuoteTypes.set(subject, opts.assetQuoteType);
570
+ }
571
+ else {
572
+ this.accountSubQuoteTypes.delete(subject);
573
+ }
574
+ }
575
+ subscribeAsset(accountOrOptions) {
576
+ this.subscribeAccount(SubjectType.Asset, accountOrOptions);
558
577
  }
559
578
  /** Unsubscribe from asset changes */
560
579
  unsubscribeAsset() {
561
580
  this.doUnsubscribe(SubjectType.Asset);
562
581
  this.accountSubs.delete(SubjectType.Asset);
582
+ this.accountSubQuoteTypes.delete(SubjectType.Asset);
563
583
  }
564
- /** Subscribe to position changes */
565
- subscribePosition(account) {
566
- const acct = account || this.config.account;
567
- this.doSubscribe(SubjectType.Position, undefined, acct);
568
- this.accountSubs.set(SubjectType.Position, acct);
584
+ subscribePosition(accountOrOptions) {
585
+ this.subscribeAccount(SubjectType.Position, accountOrOptions);
569
586
  }
570
587
  /** Unsubscribe from position changes */
571
588
  unsubscribePosition() {
572
589
  this.doUnsubscribe(SubjectType.Position);
573
590
  this.accountSubs.delete(SubjectType.Position);
591
+ this.accountSubQuoteTypes.delete(SubjectType.Position);
574
592
  }
575
593
  /** Subscribe to order status */
576
594
  subscribeOrder(account) {
@@ -0,0 +1,165 @@
1
+ const PART_CODE_NAME_MAP = {
2
+ a: 'NYSE American, LLC (NYSE American)',
3
+ b: 'NASDAQ OMX BX, Inc. (NASDAQ OMX BX)',
4
+ c: 'NYSE National, Inc. (NYSE National)',
5
+ d: 'FINRA Alternative Display Facility (ADF)',
6
+ h: 'MIAX Pearl Exchange, LLC (MIAX)',
7
+ i: 'International Securities Exchange, LLC (ISE)',
8
+ j: 'Cboe EDGA Exchange, Inc. (Cboe EDGA)',
9
+ k: 'Cboe EDGX Exchange, Inc. (Cboe EDGX)',
10
+ l: 'Long-Term Stock Exchange, Inc. (LTSE)',
11
+ m: 'NYSE Chicago, Inc. (NYSE Chicago)',
12
+ n: 'New York Stock Exchange, LLC (NYSE)',
13
+ p: 'NYSE Arca, Inc. (NYSE Arca)',
14
+ s: 'Consolidated Tape System (CTS)',
15
+ t: 'NASDAQ Stock Market, LLC (NASDAQ)',
16
+ u: 'Members Exchange, LLC (MEMX)',
17
+ v: "Investors' Exchange, LLC. (IEX)",
18
+ w: 'CBOE Stock Exchange, Inc. (CBSX)',
19
+ x: 'NASDAQ OMX PSX, Inc. (NASDAQ OMX PSX)',
20
+ y: 'Cboe BYX Exchange, Inc. (Cboe BYX)',
21
+ z: 'Cboe BZX Exchange, Inc. (Cboe BZX)',
22
+ };
23
+ const PART_CODE_SHORT_NAME_MAP = {
24
+ a: 'AMEX',
25
+ b: 'BX',
26
+ c: 'NSX',
27
+ d: 'ADF',
28
+ h: 'MIAX',
29
+ i: 'ISE',
30
+ j: 'EDGA',
31
+ k: 'EDGX',
32
+ l: 'LTSE',
33
+ m: 'CHO',
34
+ n: 'NYSE',
35
+ p: 'ARCA',
36
+ s: 'CTS',
37
+ t: 'NSDQ',
38
+ u: 'MEMX',
39
+ v: 'IEX',
40
+ w: 'CBSX',
41
+ x: 'PSX',
42
+ y: 'BYX',
43
+ z: 'BZX',
44
+ };
45
+ const US_TRADE_COND = {
46
+ ' ': 'US_REGULAR_SALE',
47
+ B: 'US_BUNCHED_TRADE',
48
+ C: 'US_CASH_TRADE',
49
+ F: 'US_INTERMARKET_SWEEP',
50
+ G: 'US_BUNCHED_SOLD_TRADE',
51
+ H: 'US_PRICE_VARIATION_TRADE',
52
+ I: 'US_ODD_LOT_TRADE',
53
+ K: 'US_RULE_127_OR_155_TRADE',
54
+ L: 'US_SOLD_LAST',
55
+ M: 'US_MARKET_CENTER_CLOSE_PRICE',
56
+ N: 'US_NEXT_DAY_TRADE',
57
+ O: 'US_MARKET_CENTER_OPENING_TRADE',
58
+ P: 'US_PRIOR_REFERENCE_PRICE',
59
+ Q: 'US_MARKET_CENTER_OPEN_PRICE',
60
+ R: 'US_SELLER',
61
+ T: 'US_FORM_T',
62
+ U: 'US_EXTENDED_TRADING_HOURS',
63
+ V: 'US_CONTINGENT_TRADE',
64
+ W: 'US_AVERAGE_PRICE_TRADE',
65
+ X: 'US_CROSS_TRADE',
66
+ Z: 'US_SOLD_OUT_OF_SEQUENCE',
67
+ '0': 'US_ODD_LOST_CROSS_TRADE',
68
+ '4': 'US_DERIVATIVELY_PRICED',
69
+ '5': 'US_MARKET_CENTER_RE_OPENING_TRADE',
70
+ '6': 'US_MARKET_CENTER_CLOSING_TRADE',
71
+ '7': 'US_QUALIFIED_CONTINGENT_TRADE',
72
+ '9': 'US_CONSOLIDATED_LAST_PRICE_PER_LISTING_PACKET',
73
+ };
74
+ const HK_TRADE_COND = {
75
+ ' ': 'HK_AUTOMATCH_NORMAL',
76
+ D: 'HK_ODD_LOT_TRADE',
77
+ U: 'HK_AUCTION_TRADE',
78
+ '*': 'HK_OVERSEAS_TRADE',
79
+ P: 'HK_LATE_TRADE_OFF_EXCHG',
80
+ M: 'HK_NON_DIRECT_OFF_EXCHG_TRADE',
81
+ X: 'HK_DIRECT_OFF_EXCHG_TRADE',
82
+ Y: 'HK_AUTOMATIC_INTERNALIZED',
83
+ };
84
+ /** Returns the full exchange name for a partCode letter, or the raw code if unknown. */
85
+ export function getPartNameByCode(code) {
86
+ if (!code)
87
+ return code;
88
+ return PART_CODE_NAME_MAP[code] ?? code;
89
+ }
90
+ /** Returns the short exchange name for a partCode letter, or the raw code if unknown. */
91
+ export function getPartShortNameByCode(code) {
92
+ if (!code)
93
+ return code;
94
+ return PART_CODE_SHORT_NAME_MAP[code] ?? code;
95
+ }
96
+ /** Resolves a raw cond character to a readable string. */
97
+ export function getTradeCondByCode(isUs, ch) {
98
+ const map = isUs ? US_TRADE_COND : HK_TRADE_COND;
99
+ return map[ch] ?? ch;
100
+ }
101
+ const US_SYMBOL_RE = /^[A-Z]+(\.[A-Z0-9]+)?$/;
102
+ /** Reports whether symbol is a US stock (uppercase letters, optional dot suffix). */
103
+ export function isUsStockSymbol(symbol) {
104
+ return US_SYMBOL_RE.test(symbol);
105
+ }
106
+ function isUsSymbol(symbol) {
107
+ return US_SYMBOL_RE.test(symbol);
108
+ }
109
+ function getTradeCond(isUs, ch) {
110
+ const map = isUs ? US_TRADE_COND : HK_TRADE_COND;
111
+ return map[ch] ?? ch;
112
+ }
113
+ /** Decodes a TradeTickData pb message into a PushTradeTick with resolved partCode/partName. */
114
+ export function convertTradeTick(src) {
115
+ if (src.secType === 'FUT')
116
+ return convertFutureTick(src);
117
+ return convertStockTick(src);
118
+ }
119
+ function convertStockTick(src) {
120
+ const isUs = isUsSymbol(src.symbol);
121
+ const denominator = Math.pow(10, src.priceOffset);
122
+ const ticks = [];
123
+ let currentTime = 0;
124
+ for (let i = 0; i < src.time.length; i++) {
125
+ currentTime += src.time[i];
126
+ const condCh = src.cond.length > i ? src.cond[i] : ' ';
127
+ const rawCode = i < src.partCode.length ? src.partCode[i] : '';
128
+ ticks.push({
129
+ sn: src.sn + i,
130
+ time: currentTime,
131
+ price: (src.priceBase + src.price[i]) / denominator,
132
+ volume: src.volume[i],
133
+ tickType: src.type.length > i ? src.type[i] : '',
134
+ cond: getTradeCond(isUs, condCh),
135
+ partCode: getPartShortNameByCode(rawCode),
136
+ partName: getPartNameByCode(rawCode),
137
+ });
138
+ }
139
+ return { symbol: src.symbol, secType: src.secType, quoteLevel: src.quoteLevel, timestamp: src.timestamp, ticks };
140
+ }
141
+ function convertFutureTick(src) {
142
+ const denominator = Math.pow(10, src.priceOffset);
143
+ const ticks = [];
144
+ let currentTime = 0;
145
+ let startSn = src.sn;
146
+ for (let i = 0; i < src.time.length; i++) {
147
+ currentTime += src.time[i];
148
+ const curPrice = (src.priceBase + src.price[i]) / denominator;
149
+ const mv = src.mergedVols[i];
150
+ for (let j = 0; j < mv.vol.length; j++) {
151
+ ticks.push({
152
+ sn: startSn * 10 + j,
153
+ time: currentTime,
154
+ price: curPrice,
155
+ volume: mv.vol[j],
156
+ tickType: '',
157
+ cond: '',
158
+ partCode: '',
159
+ partName: '',
160
+ });
161
+ }
162
+ startSn++;
163
+ }
164
+ return { symbol: src.symbol, secType: src.secType, quoteLevel: '', timestamp: src.timestamp, ticks };
165
+ }