@finatic/client 0.0.142 → 0.9.1

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 (64) hide show
  1. package/CHANGELOG.md +16 -0
  2. package/LICENSE +39 -0
  3. package/README.md +54 -425
  4. package/dist/index.d.ts +7329 -1579
  5. package/dist/index.js +8110 -6114
  6. package/dist/index.js.map +1 -1
  7. package/dist/index.mjs +8047 -6085
  8. package/dist/index.mjs.map +1 -1
  9. package/package.json +77 -33
  10. package/dist/types/core/client/ApiClient.d.ts +0 -270
  11. package/dist/types/core/client/FinaticConnect.d.ts +0 -332
  12. package/dist/types/core/portal/PortalUI.d.ts +0 -37
  13. package/dist/types/index.d.ts +0 -12
  14. package/dist/types/lib/logger/index.d.ts +0 -2
  15. package/dist/types/lib/logger/logger.d.ts +0 -4
  16. package/dist/types/lib/logger/logger.types.d.ts +0 -28
  17. package/dist/types/mocks/MockApiClient.d.ts +0 -171
  18. package/dist/types/mocks/MockDataProvider.d.ts +0 -139
  19. package/dist/types/mocks/MockFactory.d.ts +0 -53
  20. package/dist/types/mocks/utils.d.ts +0 -24
  21. package/dist/types/themes/portalPresets.d.ts +0 -9
  22. package/dist/types/types/api/auth.d.ts +0 -93
  23. package/dist/types/types/api/broker.d.ts +0 -421
  24. package/dist/types/types/api/core.d.ts +0 -46
  25. package/dist/types/types/api/errors.d.ts +0 -31
  26. package/dist/types/types/api/orders.d.ts +0 -39
  27. package/dist/types/types/api/portfolio.d.ts +0 -55
  28. package/dist/types/types/common/pagination.d.ts +0 -33
  29. package/dist/types/types/connect.d.ts +0 -58
  30. package/dist/types/types/index.d.ts +0 -13
  31. package/dist/types/types/portal.d.ts +0 -204
  32. package/dist/types/types/ui/theme.d.ts +0 -104
  33. package/dist/types/utils/brokerUtils.d.ts +0 -30
  34. package/dist/types/utils/errors.d.ts +0 -45
  35. package/dist/types/utils/events.d.ts +0 -12
  36. package/dist/types/utils/themeUtils.d.ts +0 -34
  37. package/src/core/client/ApiClient.ts +0 -2004
  38. package/src/core/client/FinaticConnect.ts +0 -1606
  39. package/src/core/portal/PortalUI.ts +0 -335
  40. package/src/index.d.ts +0 -23
  41. package/src/index.ts +0 -100
  42. package/src/lib/logger/index.ts +0 -3
  43. package/src/lib/logger/logger.ts +0 -332
  44. package/src/lib/logger/logger.types.ts +0 -34
  45. package/src/mocks/MockApiClient.ts +0 -1058
  46. package/src/mocks/MockDataProvider.ts +0 -986
  47. package/src/mocks/MockFactory.ts +0 -97
  48. package/src/mocks/utils.ts +0 -133
  49. package/src/themes/portalPresets.ts +0 -1307
  50. package/src/types/api/auth.ts +0 -112
  51. package/src/types/api/broker.ts +0 -461
  52. package/src/types/api/core.ts +0 -53
  53. package/src/types/api/errors.ts +0 -35
  54. package/src/types/api/orders.ts +0 -45
  55. package/src/types/api/portfolio.ts +0 -59
  56. package/src/types/common/pagination.ts +0 -164
  57. package/src/types/connect.ts +0 -56
  58. package/src/types/index.ts +0 -25
  59. package/src/types/portal.ts +0 -214
  60. package/src/types/ui/theme.ts +0 -105
  61. package/src/utils/brokerUtils.ts +0 -104
  62. package/src/utils/errors.ts +0 -104
  63. package/src/utils/events.ts +0 -66
  64. package/src/utils/themeUtils.ts +0 -165
@@ -1,2004 +0,0 @@
1
- import { Order } from '../../types/api/orders';
2
- import {
3
- BrokerInfo,
4
- BrokerAccount,
5
- BrokerOrder,
6
- BrokerPosition,
7
- BrokerBalance,
8
- BrokerDataOptions,
9
- DisconnectCompanyResponse,
10
- } from '../../types/api/broker';
11
- import { BrokerOrderParams, BrokerExtras } from '../../types/api/broker';
12
- import { CryptoOrderOptions, OptionsOrderOptions, OrderResponse } from '../../types/api/orders';
13
- import { BrokerConnection } from '../../types/api/broker';
14
- import {
15
- OrdersFilter,
16
- PositionsFilter,
17
- AccountsFilter,
18
- BalancesFilter,
19
- OrderFill,
20
- OrderEvent,
21
- OrderGroup,
22
- PositionLot,
23
- PositionLotFill,
24
- OrderFillsFilter,
25
- OrderEventsFilter,
26
- OrderGroupsFilter,
27
- PositionLotsFilter,
28
- PositionLotFillsFilter,
29
- } from '../../types/api/broker';
30
- import { TradingContext } from '../../types/api/orders';
31
- import { ApiPaginationInfo, PaginatedResult } from '../../types/common/pagination';
32
- import { ApiResponse } from '../../types/api/core';
33
- import { PortalUrlResponse } from '../../types/api/core';
34
- import { setupLogger, buildLoggerExtra, LoggerExtra } from '../../lib/logger';
35
- import {
36
- DeviceInfo,
37
- SessionState,
38
- TokenInfo,
39
- SessionResponse,
40
- SessionResponseData,
41
- OtpRequestResponse,
42
- OtpVerifyResponse,
43
- SessionValidationResponse,
44
- SessionAuthenticateResponse,
45
- UserToken,
46
- RefreshTokenRequest,
47
- RefreshTokenResponse,
48
- } from '../../types/api/auth';
49
- import {
50
- ApiError,
51
- SessionError,
52
- AuthenticationError,
53
- AuthorizationError,
54
- RateLimitError,
55
- CompanyAccessError,
56
- OrderError,
57
- OrderValidationError,
58
- TradingNotEnabledError,
59
- } from '../../utils/errors';
60
- // Supabase import removed - SDK no longer depends on Supabase
61
-
62
- export class ApiClient {
63
- private readonly baseUrl: string;
64
- protected readonly deviceInfo?: DeviceInfo;
65
- protected currentSessionState: SessionState | null = null;
66
- protected currentSessionId: string | null = null;
67
- private tradingContext: TradingContext = {};
68
-
69
- // Session management (no Supabase needed)
70
-
71
- // Session and company context
72
- private companyId: string | null = null;
73
- private csrfToken: string | null = null;
74
- private readonly logger = setupLogger('FinaticClientSDK.ApiClient', undefined, {
75
- codebase: 'FinaticClientSDK',
76
- });
77
-
78
- private buildLoggerExtra(functionName: string, metadata?: Record<string, unknown>): LoggerExtra {
79
- return {
80
- module: 'ApiClient',
81
- function: functionName,
82
- ...(metadata ? buildLoggerExtra(metadata) : {}),
83
- };
84
- }
85
-
86
- constructor(baseUrl: string, deviceInfo?: DeviceInfo) {
87
- this.baseUrl = baseUrl;
88
- this.deviceInfo = deviceInfo;
89
- // Ensure baseUrl doesn't end with a slash
90
- this.baseUrl = baseUrl.replace(/\/$/, '');
91
- // Append /api/v1 if not already present
92
- if (!this.baseUrl.includes('/api/v1')) {
93
- this.baseUrl = `${this.baseUrl}/api/v1`;
94
- }
95
-
96
- // No Supabase initialization needed - SDK is clean
97
- }
98
-
99
- // Supabase initialization removed - SDK no longer depends on Supabase
100
-
101
- /**
102
- * Set session context (session ID, company ID, CSRF token)
103
- */
104
- setSessionContext(sessionId: string, companyId: string, csrfToken?: string): void {
105
- this.currentSessionId = sessionId;
106
- this.companyId = companyId;
107
- this.csrfToken = csrfToken || null;
108
- }
109
-
110
- /**
111
- * Get the current session ID
112
- */
113
- getCurrentSessionId(): string | null {
114
- return this.currentSessionId;
115
- }
116
-
117
- /**
118
- * Get the current company ID
119
- */
120
- getCurrentCompanyId(): string | null {
121
- return this.companyId;
122
- }
123
-
124
- /**
125
- * Get the current CSRF token
126
- */
127
- getCurrentCsrfToken(): string | null {
128
- return this.csrfToken;
129
- }
130
-
131
- /**
132
- * Get a valid access token (session-based auth - no tokens needed)
133
- */
134
- async getValidAccessToken(): Promise<string> {
135
- // Session-based auth - return empty token as we use session headers
136
- return '';
137
- }
138
-
139
- // Token expiration check removed - session-based auth doesn't use expiring tokens
140
-
141
- // Supabase refresh method removed - SDK no longer uses Supabase tokens
142
-
143
- /**
144
- * Perform the actual Supabase session refresh
145
- */
146
- // Supabase refresh method removed - SDK no longer uses Supabase tokens
147
-
148
- /**
149
- * Clear session tokens (useful for logout)
150
- */
151
- clearTokens(): void {
152
- // Session-based auth - no tokens to clear
153
- }
154
-
155
- /**
156
- * Get current session info (for debugging/testing) - session-based auth
157
- */
158
- getTokenInfo(): { accessToken: string; refreshToken: string; expiresAt: number } | null {
159
- // Session-based auth - no tokens to return
160
- return null;
161
- }
162
-
163
- /**
164
- * Make a request to the API.
165
- */
166
- protected async request<T>(
167
- path: string,
168
- options: {
169
- method: string;
170
- headers?: Record<string, string>;
171
- body?: any;
172
- params?: Record<string, string>;
173
- }
174
- ): Promise<T> {
175
- // Ensure path starts with a slash
176
- const normalizedPath = path.startsWith('/') ? path : `/${path}`;
177
- const url = new URL(`${this.baseUrl}${normalizedPath}`);
178
-
179
- if (options.params) {
180
- Object.entries(options.params).forEach(([key, value]) => {
181
- url.searchParams.append(key, value);
182
- });
183
- }
184
-
185
- // Get Supabase JWT token
186
- const accessToken = await this.getValidAccessToken();
187
-
188
- // Build comprehensive headers object with all available session data
189
- const comprehensiveHeaders: Record<string, string> = {
190
- 'Content-Type': 'application/json',
191
- Authorization: `Bearer ${accessToken}`,
192
- };
193
-
194
- // Add device info if available
195
- if (this.deviceInfo) {
196
- comprehensiveHeaders['X-Device-Info'] = JSON.stringify({
197
- ip_address: this.deviceInfo.ip_address || '',
198
- user_agent: this.deviceInfo.user_agent || '',
199
- fingerprint: this.deviceInfo.fingerprint || '',
200
- });
201
- }
202
-
203
- // Add session headers if available (filter out empty values)
204
- if (this.currentSessionId && this.currentSessionId.trim() !== '') {
205
- comprehensiveHeaders['X-Session-ID'] = this.currentSessionId;
206
- comprehensiveHeaders['Session-ID'] = this.currentSessionId;
207
- }
208
-
209
- if (this.companyId && this.companyId.trim() !== '') {
210
- comprehensiveHeaders['X-Company-ID'] = this.companyId;
211
- }
212
-
213
- if (this.csrfToken && this.csrfToken.trim() !== '') {
214
- comprehensiveHeaders['X-CSRF-Token'] = this.csrfToken;
215
- }
216
-
217
- // Add any additional headers from options (these will override defaults)
218
- if (options.headers) {
219
- Object.entries(options.headers).forEach(([key, value]) => {
220
- if (value !== undefined && value !== null && value.trim() !== '') {
221
- comprehensiveHeaders[key] = value;
222
- }
223
- });
224
- }
225
-
226
- // Safari-specific fix: Ensure all headers are explicitly set and not empty
227
- // Safari can be strict about header formatting and empty values
228
- const safariSafeHeaders: Record<string, string> = {};
229
- Object.entries(comprehensiveHeaders).forEach(([key, value]) => {
230
- if (value && value.trim() !== '') {
231
- // Ensure header names are properly formatted for Safari
232
- const normalizedKey = key.trim();
233
- const normalizedValue = value.trim();
234
- safariSafeHeaders[normalizedKey] = normalizedValue;
235
- }
236
- });
237
-
238
- this.logger.debug('Dispatching API request', this.buildLoggerExtra('request', {
239
- url: url.toString(),
240
- method: options.method,
241
- has_body: Boolean(options.body),
242
- }));
243
-
244
- const response = await fetch(url.toString(), {
245
- method: options.method,
246
- headers: safariSafeHeaders,
247
- body: options.body ? JSON.stringify(options.body) : undefined,
248
- });
249
-
250
- this.logger.debug('Received API response', this.buildLoggerExtra('request', {
251
- url: url.toString(),
252
- status: response.status,
253
- }));
254
-
255
- if (!response.ok) {
256
- const error = await response.json();
257
- const apiError = this.handleError(response.status, error);
258
- this.logger.exception('API request failed', apiError, this.buildLoggerExtra('request', {
259
- url: url.toString(),
260
- status: response.status,
261
- }));
262
- throw apiError;
263
- }
264
-
265
- const data = await response.json();
266
-
267
- // Check if the response has a success field and it's false
268
- if (data && typeof data === 'object' && 'success' in data && data.success === false) {
269
- // For order endpoints, provide more context
270
- const isOrderEndpoint = path.includes('/brokers/orders');
271
- if (isOrderEndpoint) {
272
- // Add context that this is an order-related error
273
- data._isOrderError = true;
274
- }
275
- const apiError = this.handleError(data.status_code || 500, data);
276
- this.logger.exception('API response indicated failure', apiError, this.buildLoggerExtra('request', {
277
- url: url.toString(),
278
- status: data.status_code || 500,
279
- }));
280
- throw apiError;
281
- }
282
-
283
- // Check if the response has a status_code field indicating an error (4xx or 5xx)
284
- if (data && typeof data === 'object' && 'status_code' in data && data.status_code >= 400) {
285
- const apiError = this.handleError(data.status_code, data);
286
- this.logger.exception('API status code error', apiError, this.buildLoggerExtra('request', {
287
- url: url.toString(),
288
- status: data.status_code,
289
- }));
290
- throw apiError;
291
- }
292
-
293
- // Check if the response has errors field with content
294
- if (
295
- data &&
296
- typeof data === 'object' &&
297
- 'errors' in data &&
298
- data.errors &&
299
- Array.isArray(data.errors) &&
300
- data.errors.length > 0
301
- ) {
302
- const apiError = this.handleError(data.status_code || 500, data);
303
- this.logger.exception('API response contained errors', apiError, this.buildLoggerExtra('request', {
304
- url: url.toString(),
305
- status: data.status_code || 500,
306
- }));
307
- throw apiError;
308
- }
309
-
310
- return data;
311
- }
312
-
313
- /**
314
- * Handle API errors. This method can be overridden by language-specific implementations.
315
- */
316
- protected handleError(status: number, error: any): ApiError {
317
- // Extract message from the error object with multiple fallback options
318
- let message = 'API request failed';
319
-
320
- if (error && typeof error === 'object') {
321
- // Try different possible message fields
322
- message =
323
- error.message ||
324
- error.detail?.message ||
325
- error.error?.message ||
326
- error.errors?.[0]?.message ||
327
- (typeof error.errors === 'string' ? error.errors : null) ||
328
- 'API request failed';
329
- }
330
-
331
- // Check if this is an order-related error (either from order endpoints or order validation)
332
- const isOrderError =
333
- error._isOrderError ||
334
- message.includes('ORDER_FAILED') ||
335
- message.includes('AUTH_ERROR') ||
336
- message.includes('not found or not available for trading') ||
337
- message.includes('Symbol') ||
338
- (error.errors &&
339
- Array.isArray(error.errors) &&
340
- error.errors.some(
341
- (e: any) =>
342
- e.category === 'INVALID_ORDER' ||
343
- e.message?.includes('Symbol') ||
344
- e.message?.includes('not available for trading')
345
- ));
346
-
347
- if (isOrderError) {
348
- // Check if this is a validation error (400 status with specific validation messages)
349
- const isValidationError =
350
- status === 400 &&
351
- (message.includes('not found or not available for trading') ||
352
- message.includes('Symbol') ||
353
- (error.errors &&
354
- Array.isArray(error.errors) &&
355
- error.errors.some(
356
- (e: any) =>
357
- e.category === 'INVALID_ORDER' ||
358
- e.message?.includes('Symbol') ||
359
- e.message?.includes('not available for trading')
360
- )));
361
-
362
- if (isValidationError) {
363
- return new OrderValidationError(message, error);
364
- }
365
-
366
- // For order placement errors, provide more specific error messages
367
- if (message.includes('ORDER_FAILED') || message.includes('AUTH_ERROR')) {
368
- // Extract the specific error from the nested structure
369
- const orderErrorMatch = message.match(/\[([^\]]+)\]/g);
370
- if (orderErrorMatch && orderErrorMatch.length > 0) {
371
- // Take the last error in the chain (most specific)
372
- const specificError = orderErrorMatch[orderErrorMatch.length - 1].replace(/[[\]]/g, '');
373
- message = `Order failed: ${specificError}`;
374
- }
375
- }
376
-
377
- // Use OrderError for order-related failures
378
- return new OrderError(message, error);
379
- }
380
-
381
- switch (status) {
382
- case 400:
383
- return new SessionError(message, error);
384
- case 401:
385
- return new AuthenticationError(
386
- message || 'Unauthorized: Invalid or missing session token',
387
- error
388
- );
389
- case 403:
390
- if (error.detail?.code === 'NO_COMPANY_ACCESS') {
391
- return new CompanyAccessError(
392
- error.detail.message || 'No broker connections found for this company',
393
- error.detail
394
- );
395
- }
396
- if (error.detail?.code === 'TRADING_NOT_ENABLED') {
397
- return new TradingNotEnabledError(
398
- error.detail.message || 'Trading is not enabled for your company',
399
- error.detail
400
- );
401
- }
402
- return new AuthorizationError(
403
- message || 'Forbidden: No access to the requested data',
404
- error
405
- );
406
- case 404:
407
- return new ApiError(
408
- status,
409
- message || 'Not found: The requested data does not exist',
410
- error
411
- );
412
- case 429:
413
- return new RateLimitError(message || 'Rate limit exceeded', error);
414
- case 500:
415
- return new ApiError(status, message || 'Internal server error', error);
416
- default:
417
- return new ApiError(status, message || 'API request failed', error);
418
- }
419
- }
420
-
421
- // Session Management
422
- async startSession(token: string, userId?: string): Promise<SessionResponse> {
423
- const response = await this.request<SessionResponse>('/session/start', {
424
- method: 'POST',
425
- headers: {
426
- 'Content-Type': 'application/json',
427
- 'One-Time-Token': token,
428
- 'X-Device-Info': JSON.stringify({
429
- ip_address: this.deviceInfo?.ip_address || '',
430
- user_agent: this.deviceInfo?.user_agent || '',
431
- fingerprint: this.deviceInfo?.fingerprint || '',
432
- }),
433
- },
434
- body: {
435
- user_id: userId,
436
- },
437
- });
438
-
439
- // Store session ID and set state to ACTIVE
440
- this.currentSessionId = response.data.session_id;
441
- this.currentSessionState = SessionState.ACTIVE;
442
-
443
- return response;
444
- }
445
-
446
- // OTP Flow
447
- async requestOtp(sessionId: string, email: string): Promise<OtpRequestResponse> {
448
- return this.request<OtpRequestResponse>('/auth/otp/request', {
449
- method: 'POST',
450
- headers: {
451
- 'Content-Type': 'application/json',
452
- 'X-Session-ID': sessionId,
453
- 'X-Device-Info': JSON.stringify({
454
- ip_address: this.deviceInfo?.ip_address || '',
455
- user_agent: this.deviceInfo?.user_agent || '',
456
- fingerprint: this.deviceInfo?.fingerprint || '',
457
- }),
458
- },
459
- body: {
460
- email,
461
- },
462
- });
463
- }
464
-
465
- async verifyOtp(sessionId: string, otp: string): Promise<OtpVerifyResponse> {
466
- const response = await this.request<OtpVerifyResponse>('/auth/otp/verify', {
467
- method: 'POST',
468
- headers: {
469
- 'Content-Type': 'application/json',
470
- 'X-Session-ID': sessionId,
471
- 'X-Device-Info': JSON.stringify({
472
- ip_address: this.deviceInfo?.ip_address || '',
473
- user_agent: this.deviceInfo?.user_agent || '',
474
- fingerprint: this.deviceInfo?.fingerprint || '',
475
- }),
476
- },
477
- body: {
478
- otp,
479
- },
480
- });
481
-
482
- // OTP verification successful - tokens are handled by Supabase client
483
- if (response.success && response.data) {
484
- // Note: Supabase JWT will be handled by the Supabase client
485
- // The backend now creates/retrieves Supabase users and returns session info
486
- }
487
-
488
- return response;
489
- }
490
-
491
- // Direct Authentication
492
- async authenticateDirectly(
493
- sessionId: string,
494
- userId: string
495
- ): Promise<SessionAuthenticateResponse> {
496
- // Ensure session is active before authenticating
497
- if (this.currentSessionState !== SessionState.ACTIVE) {
498
- throw new SessionError('Session must be in ACTIVE state to authenticate');
499
- }
500
-
501
- const response = await this.request<SessionAuthenticateResponse>('/session/authenticate', {
502
- method: 'POST',
503
- headers: {
504
- 'Content-Type': 'application/json',
505
- 'Session-ID': sessionId,
506
- 'X-Session-ID': sessionId,
507
- 'X-Device-Info': JSON.stringify({
508
- ip_address: this.deviceInfo?.ip_address || '',
509
- user_agent: this.deviceInfo?.user_agent || '',
510
- fingerprint: this.deviceInfo?.fingerprint || '',
511
- }),
512
- },
513
- body: {
514
- session_id: sessionId,
515
- user_id: userId,
516
- },
517
- });
518
-
519
- // Store tokens after successful direct authentication
520
- if (response.success && response.data) {
521
- // Session-based auth - no token storage needed
522
- }
523
-
524
- return response;
525
- }
526
-
527
- // Portal Management
528
- /**
529
- * Get the portal URL for an active session
530
- * @param sessionId The session identifier
531
- * @returns Portal URL response
532
- * @throws SessionError if session is not in ACTIVE state
533
- */
534
- public async getPortalUrl(sessionId: string): Promise<PortalUrlResponse> {
535
- if (this.currentSessionState !== SessionState.ACTIVE) {
536
- throw new SessionError('Session must be in ACTIVE state to get portal URL');
537
- }
538
-
539
- return this.request<PortalUrlResponse>('/session/portal', {
540
- method: 'GET',
541
- headers: {
542
- 'Content-Type': 'application/json',
543
- 'Session-ID': sessionId,
544
- 'X-Session-ID': sessionId,
545
- 'X-Device-Info': JSON.stringify({
546
- ip_address: this.deviceInfo?.ip_address || '',
547
- user_agent: this.deviceInfo?.user_agent || '',
548
- fingerprint: this.deviceInfo?.fingerprint || '',
549
- }),
550
- },
551
- });
552
- }
553
-
554
- async completePortalSession(sessionId: string): Promise<PortalUrlResponse> {
555
- return this.request<PortalUrlResponse>(`/portal/${sessionId}/complete`, {
556
- method: 'POST',
557
- headers: {
558
- 'Content-Type': 'application/json',
559
- },
560
- });
561
- }
562
-
563
- // Portfolio Management
564
-
565
- async getOrders(): Promise<{ data: Order[] }> {
566
- const accessToken = await this.getValidAccessToken();
567
- return this.request<{ data: Order[] }>('/brokers/data/orders', {
568
- method: 'GET',
569
- headers: {
570
- Authorization: `Bearer ${accessToken}`,
571
- },
572
- });
573
- }
574
-
575
- // Enhanced Trading Methods with Session Management
576
- async placeBrokerOrder(
577
- params: Partial<BrokerOrderParams> & {
578
- symbol: string;
579
- orderQty: number;
580
- action: 'Buy' | 'Sell';
581
- orderType: 'Market' | 'Limit' | 'Stop' | 'StopLimit';
582
- assetType: 'equity' | 'equity_option' | 'crypto' | 'forex' | 'future' | 'future_option';
583
- },
584
- extras: BrokerExtras = {},
585
- connection_id?: string
586
- ): Promise<OrderResponse> {
587
- const accessToken = await this.getValidAccessToken();
588
-
589
- // Get broker and account from context or params
590
- const broker = params.broker || this.tradingContext.broker;
591
- const accountNumber = params.accountNumber || this.tradingContext.accountNumber;
592
-
593
- if (!broker) {
594
- throw new Error('Broker not set. Call setBroker() or pass broker parameter.');
595
- }
596
-
597
- if (!accountNumber) {
598
- throw new Error('Account not set. Call setAccount() or pass accountNumber parameter.');
599
- }
600
-
601
- // Merge context with provided parameters
602
- const fullParams: BrokerOrderParams = {
603
- broker:
604
- ((params.broker || this.tradingContext.broker) as
605
- | 'robinhood'
606
- | 'tasty_trade'
607
- | 'ninja_trader') ||
608
- (() => {
609
- throw new Error('Broker not set. Call setBroker() or pass broker parameter.');
610
- })(),
611
- accountNumber:
612
- params.accountNumber ||
613
- this.tradingContext.accountNumber ||
614
- (() => {
615
- throw new Error('Account not set. Call setAccount() or pass accountNumber parameter.');
616
- })(),
617
- symbol: params.symbol,
618
- orderQty: params.orderQty,
619
- action: params.action,
620
- orderType: params.orderType,
621
- assetType: params.assetType,
622
- timeInForce: params.timeInForce || 'day',
623
- price: params.price,
624
- stopPrice: params.stopPrice,
625
- order_id: params.order_id,
626
- };
627
-
628
- // Build request body with camelCase parameter names
629
- const requestBody = this.buildOrderRequestBody(fullParams, extras);
630
-
631
- // Add query parameters if connection_id is provided
632
- const queryParams: Record<string, string> = {};
633
- if (connection_id) {
634
- queryParams.connection_id = connection_id;
635
- }
636
-
637
- return this.request<OrderResponse>('/brokers/orders', {
638
- method: 'POST',
639
- headers: {
640
- 'Content-Type': 'application/json',
641
- Authorization: `Bearer ${accessToken}`,
642
- 'Session-ID': this.currentSessionId || '',
643
- 'X-Session-ID': this.currentSessionId || '',
644
- 'X-Device-Info': JSON.stringify(this.deviceInfo),
645
- },
646
- body: requestBody,
647
- params: queryParams,
648
- });
649
- }
650
-
651
- async cancelBrokerOrder(
652
- orderId: string,
653
- broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
654
- extras: any = {},
655
- connection_id?: string
656
- ): Promise<OrderResponse> {
657
- const accessToken = await this.getValidAccessToken();
658
-
659
- const selectedBroker = broker || this.tradingContext.broker;
660
- if (!selectedBroker) {
661
- throw new Error('Broker not set. Call setBroker() or pass broker parameter.');
662
- }
663
-
664
- const accountNumber = this.tradingContext.accountNumber;
665
-
666
- // Build query parameters as required by API documentation
667
- const queryParams: Record<string, string> = {};
668
-
669
- // Add optional parameters if available
670
- if (accountNumber) {
671
- queryParams.account_number = accountNumber.toString();
672
- }
673
- if (connection_id) {
674
- queryParams.connection_id = connection_id;
675
- }
676
-
677
- // Build optional request body if extras are provided
678
- let body: any = undefined;
679
- if (Object.keys(extras).length > 0) {
680
- body = {
681
- broker: selectedBroker,
682
- order: {
683
- order_id: orderId,
684
- account_number: accountNumber,
685
- ...extras,
686
- },
687
- };
688
- }
689
-
690
- return this.request<OrderResponse>(`/brokers/orders/${orderId}`, {
691
- method: 'DELETE',
692
- headers: {
693
- 'Content-Type': 'application/json',
694
- 'Session-ID': this.currentSessionId || '',
695
- 'X-Session-ID': this.currentSessionId || '',
696
- 'X-Device-Info': JSON.stringify(this.deviceInfo),
697
- },
698
- body,
699
- params: queryParams,
700
- });
701
- }
702
-
703
- async modifyBrokerOrder(
704
- orderId: string,
705
- params: Partial<BrokerOrderParams>,
706
- broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
707
- extras: any = {},
708
- connection_id?: string
709
- ): Promise<OrderResponse> {
710
- const accessToken = await this.getValidAccessToken();
711
-
712
- const selectedBroker = broker || this.tradingContext.broker;
713
- if (!selectedBroker) {
714
- throw new Error('Broker not set. Call setBroker() or pass broker parameter.');
715
- }
716
-
717
- // Build request body with camelCase parameter names and include broker
718
- const requestBody = this.buildModifyRequestBody(params, extras, selectedBroker);
719
-
720
- // Add query parameters if connection_id is provided
721
- const queryParams: Record<string, string> = {};
722
- if (connection_id) {
723
- queryParams.connection_id = connection_id;
724
- }
725
-
726
- return this.request<OrderResponse>(`/brokers/orders/${orderId}`, {
727
- method: 'PATCH',
728
- headers: {
729
- 'Content-Type': 'application/json',
730
- 'Session-ID': this.currentSessionId || '',
731
- 'X-Session-ID': this.currentSessionId || '',
732
- 'X-Device-Info': JSON.stringify(this.deviceInfo),
733
- },
734
- body: requestBody,
735
- params: queryParams,
736
- });
737
- }
738
-
739
- // Context management methods
740
- setBroker(broker: 'robinhood' | 'tasty_trade' | 'ninja_trader'): void {
741
- this.tradingContext.broker = broker;
742
- // Clear account when broker changes
743
- this.tradingContext.accountNumber = undefined;
744
- this.tradingContext.accountId = undefined;
745
- }
746
-
747
- setAccount(accountNumber: string, accountId?: string): void {
748
- this.tradingContext.accountNumber = accountNumber;
749
- this.tradingContext.accountId = accountId;
750
- }
751
-
752
-
753
- // Stock convenience methods
754
- async placeStockMarketOrder(
755
- symbol: string,
756
- orderQty: number,
757
- action: 'Buy' | 'Sell',
758
- broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
759
- accountNumber?: string,
760
- extras: BrokerExtras = {},
761
- connection_id?: string
762
- ): Promise<OrderResponse> {
763
- return this.placeBrokerOrder(
764
- {
765
- symbol,
766
- orderQty,
767
- action,
768
- orderType: 'Market',
769
- assetType: 'equity',
770
- timeInForce: 'day',
771
- broker,
772
- accountNumber,
773
- },
774
- extras,
775
- connection_id
776
- );
777
- }
778
-
779
- async placeStockLimitOrder(
780
- symbol: string,
781
- orderQty: number,
782
- action: 'Buy' | 'Sell',
783
- price: number,
784
- timeInForce: 'day' | 'gtc' = 'gtc',
785
- broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
786
- accountNumber?: string,
787
- extras: BrokerExtras = {},
788
- connection_id?: string
789
- ): Promise<OrderResponse> {
790
- return this.placeBrokerOrder(
791
- {
792
- symbol,
793
- orderQty,
794
- action,
795
- orderType: 'Limit',
796
- assetType: 'equity',
797
- price,
798
- timeInForce,
799
- broker,
800
- accountNumber,
801
- },
802
- extras,
803
- connection_id
804
- );
805
- }
806
-
807
- async placeStockStopOrder(
808
- symbol: string,
809
- orderQty: number,
810
- action: 'Buy' | 'Sell',
811
- stopPrice: number,
812
- timeInForce: 'day' | 'gtc' = 'day',
813
- broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
814
- accountNumber?: string,
815
- extras: BrokerExtras = {},
816
- connection_id?: string
817
- ): Promise<OrderResponse> {
818
- return this.placeBrokerOrder(
819
- {
820
- symbol,
821
- orderQty,
822
- action,
823
- orderType: 'Stop',
824
- assetType: 'equity',
825
- stopPrice,
826
- timeInForce,
827
- broker,
828
- accountNumber,
829
- },
830
- extras,
831
- connection_id
832
- );
833
- }
834
-
835
- // Crypto convenience methods
836
- async placeCryptoMarketOrder(
837
- symbol: string,
838
- orderQty: number,
839
- action: 'Buy' | 'Sell',
840
- options: CryptoOrderOptions = {},
841
- broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
842
- accountNumber?: string,
843
- extras: BrokerExtras = {}
844
- ): Promise<OrderResponse> {
845
- return this.placeBrokerOrder(
846
- {
847
- symbol,
848
- orderQty,
849
- action,
850
- orderType: 'Market',
851
- assetType: 'crypto',
852
- timeInForce: 'day',
853
- broker,
854
- accountNumber,
855
- ...options,
856
- },
857
- extras
858
- );
859
- }
860
-
861
- async placeCryptoLimitOrder(
862
- symbol: string,
863
- orderQty: number,
864
- action: 'Buy' | 'Sell',
865
- price: number,
866
- timeInForce: 'day' | 'gtc' = 'gtc',
867
- options: CryptoOrderOptions = {},
868
- broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
869
- accountNumber?: string,
870
- extras: BrokerExtras = {}
871
- ): Promise<OrderResponse> {
872
- return this.placeBrokerOrder(
873
- {
874
- symbol,
875
- orderQty,
876
- action,
877
- orderType: 'Limit',
878
- assetType: 'crypto',
879
- price,
880
- timeInForce,
881
- broker,
882
- accountNumber,
883
- ...options,
884
- },
885
- extras
886
- );
887
- }
888
-
889
- // Options convenience methods
890
- async placeOptionsMarketOrder(
891
- symbol: string,
892
- orderQty: number,
893
- action: 'Buy' | 'Sell',
894
- options: OptionsOrderOptions,
895
- broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
896
- accountNumber?: string,
897
- extras: BrokerExtras = {}
898
- ): Promise<OrderResponse> {
899
- return this.placeBrokerOrder(
900
- {
901
- symbol,
902
- orderQty,
903
- action,
904
- orderType: 'Market',
905
- assetType: 'equity_option',
906
- timeInForce: 'day',
907
- broker,
908
- accountNumber,
909
- ...options,
910
- },
911
- extras
912
- );
913
- }
914
-
915
- async placeOptionsLimitOrder(
916
- symbol: string,
917
- orderQty: number,
918
- action: 'Buy' | 'Sell',
919
- price: number,
920
- options: OptionsOrderOptions,
921
- timeInForce: 'day' | 'gtc' = 'gtc',
922
- broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
923
- accountNumber?: string,
924
- extras: BrokerExtras = {}
925
- ): Promise<OrderResponse> {
926
- return this.placeBrokerOrder(
927
- {
928
- symbol,
929
- orderQty,
930
- action,
931
- orderType: 'Limit',
932
- assetType: 'equity_option',
933
- price,
934
- timeInForce,
935
- broker,
936
- accountNumber,
937
- ...options,
938
- },
939
- extras
940
- );
941
- }
942
-
943
- // Futures convenience methods
944
- async placeFuturesMarketOrder(
945
- symbol: string,
946
- orderQty: number,
947
- action: 'Buy' | 'Sell',
948
- broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
949
- accountNumber?: string,
950
- extras: BrokerExtras = {}
951
- ): Promise<OrderResponse> {
952
- return this.placeBrokerOrder(
953
- {
954
- symbol,
955
- orderQty,
956
- action,
957
- orderType: 'Market',
958
- assetType: 'future',
959
- timeInForce: 'day',
960
- broker,
961
- accountNumber,
962
- },
963
- extras
964
- );
965
- }
966
-
967
- async placeFuturesLimitOrder(
968
- symbol: string,
969
- orderQty: number,
970
- action: 'Buy' | 'Sell',
971
- price: number,
972
- timeInForce: 'day' | 'gtc' = 'gtc',
973
- broker?: 'robinhood' | 'tasty_trade' | 'ninja_trader',
974
- accountNumber?: string,
975
- extras: BrokerExtras = {}
976
- ): Promise<OrderResponse> {
977
- return this.placeBrokerOrder(
978
- {
979
- symbol,
980
- orderQty,
981
- action,
982
- orderType: 'Limit',
983
- assetType: 'future',
984
- price,
985
- timeInForce,
986
- broker,
987
- accountNumber,
988
- },
989
- extras
990
- );
991
- }
992
-
993
- private buildOrderRequestBody(params: BrokerOrderParams, extras: BrokerExtras = {}) {
994
- const baseOrder: any = {
995
- order_id: params.order_id,
996
- orderType: params.orderType,
997
- assetType: params.assetType,
998
- action: params.action,
999
- timeInForce: params.timeInForce,
1000
- accountNumber: params.accountNumber,
1001
- symbol: params.symbol,
1002
- orderQty: params.orderQty,
1003
- };
1004
-
1005
- if (params.price !== undefined) baseOrder.price = params.price;
1006
- if (params.stopPrice !== undefined) baseOrder.stopPrice = params.stopPrice;
1007
-
1008
- // Apply broker-specific defaults – map camelCase extras property keys to snake_case before merging
1009
- const brokerExtras = this.applyBrokerDefaults(params.broker, extras);
1010
-
1011
- return {
1012
- broker: params.broker,
1013
- order: {
1014
- ...baseOrder,
1015
- ...brokerExtras,
1016
- },
1017
- };
1018
- }
1019
-
1020
- private buildModifyRequestBody(params: Partial<BrokerOrderParams>, extras: any, broker: string) {
1021
- const order: any = {};
1022
-
1023
- if (params.order_id !== undefined) order.order_id = params.order_id;
1024
- if (params.orderType !== undefined) order.orderType = params.orderType;
1025
- if (params.assetType !== undefined) order.assetType = params.assetType;
1026
- if (params.action !== undefined) order.action = params.action;
1027
- if (params.timeInForce !== undefined) order.timeInForce = params.timeInForce;
1028
- if (params.accountNumber !== undefined) order.accountNumber = params.accountNumber;
1029
- if (params.symbol !== undefined) order.symbol = params.symbol;
1030
- if (params.orderQty !== undefined) order.orderQty = params.orderQty;
1031
- if (params.price !== undefined) order.price = params.price;
1032
- if (params.stopPrice !== undefined) order.stopPrice = params.stopPrice;
1033
-
1034
- // Apply broker-specific defaults (handles snake_case conversion)
1035
- const brokerExtras = this.applyBrokerDefaults(broker, extras);
1036
-
1037
- return {
1038
- broker,
1039
- order: {
1040
- ...order,
1041
- ...brokerExtras,
1042
- },
1043
- };
1044
- }
1045
-
1046
- private applyBrokerDefaults(broker: string, extras: any): any {
1047
- // If the caller provided a broker-scoped extras object (e.g. { ninjaTrader: { ... } })
1048
- // pull the nested object for easier processing.
1049
- if (extras && typeof extras === 'object') {
1050
- const scoped =
1051
- broker === 'robinhood'
1052
- ? extras.robinhood
1053
- : broker === 'ninja_trader'
1054
- ? extras.ninjaTrader
1055
- : broker === 'tasty_trade'
1056
- ? extras.tastyTrade
1057
- : undefined;
1058
- if (scoped) {
1059
- extras = scoped;
1060
- }
1061
- }
1062
-
1063
- switch (broker) {
1064
- case 'robinhood':
1065
- return {
1066
- ...extras,
1067
- extendedHours: extras?.extendedHours ?? extras?.extended_hours ?? true,
1068
- marketHours: extras?.marketHours ?? extras?.market_hours ?? 'regular_hours',
1069
- trailType: extras?.trailType ?? extras?.trail_type ?? 'percentage',
1070
- };
1071
- case 'ninja_trader':
1072
- return {
1073
- ...extras,
1074
- accountSpec: extras?.accountSpec ?? extras?.account_spec ?? '',
1075
- isAutomated: extras?.isAutomated ?? extras?.is_automated ?? true,
1076
- };
1077
- case 'tasty_trade':
1078
- return {
1079
- ...extras,
1080
- automatedSource: extras?.automatedSource ?? extras?.automated_source ?? true,
1081
- };
1082
- default:
1083
- return extras;
1084
- }
1085
- }
1086
-
1087
- async getUserToken(sessionId: string): Promise<UserToken> {
1088
- const accessToken = await this.getValidAccessToken();
1089
- return this.request<UserToken>(`/session/${sessionId}/user`, {
1090
- method: 'GET',
1091
- headers: {
1092
- Authorization: `Bearer ${accessToken}`,
1093
- },
1094
- });
1095
- }
1096
-
1097
- /**
1098
- * Get the current session state
1099
- */
1100
- getCurrentSessionState(): SessionState | null {
1101
- return this.currentSessionState;
1102
- }
1103
-
1104
- /**
1105
- * Refresh the current session to extend its lifetime
1106
- * Note: This now uses Supabase session refresh instead of custom endpoint
1107
- */
1108
- async refreshSession(): Promise<{
1109
- success: boolean;
1110
- response_data: {
1111
- session_id: string;
1112
- company_id: string;
1113
- status: string;
1114
- expires_at: string;
1115
- user_id: string;
1116
- auto_login: boolean;
1117
- };
1118
- message: string;
1119
- status_code: number;
1120
- }> {
1121
- if (!this.currentSessionId || !this.companyId) {
1122
- throw new SessionError('No active session to refresh');
1123
- }
1124
-
1125
- // Session-based auth - no token refresh needed
1126
-
1127
- // Return session info in expected format
1128
- return {
1129
- success: true,
1130
- response_data: {
1131
- session_id: this.currentSessionId,
1132
- company_id: this.companyId,
1133
- status: 'active',
1134
- expires_at: new Date(Date.now() + 60 * 60 * 1000).toISOString(), // 1 hour from now
1135
- user_id: '', // Session-based auth - user_id comes from session
1136
- auto_login: false,
1137
- },
1138
- message: 'Session refreshed successfully',
1139
- status_code: 200,
1140
- };
1141
- }
1142
-
1143
- // Broker Data Management
1144
- async getBrokerList(): Promise<{
1145
- _id: string;
1146
- response_data: BrokerInfo[];
1147
- message: string;
1148
- status_code: number;
1149
- warnings: null;
1150
- errors: null;
1151
- }> {
1152
- // Public endpoint - no auth required
1153
- return this.request<{
1154
- _id: string;
1155
- response_data: BrokerInfo[];
1156
- message: string;
1157
- status_code: number;
1158
- warnings: null;
1159
- errors: null;
1160
- }>('/brokers/', {
1161
- method: 'GET',
1162
- });
1163
- }
1164
-
1165
- async getBrokerAccounts(options?: BrokerDataOptions): Promise<{
1166
- _id: string;
1167
- response_data: BrokerAccount[];
1168
- message: string;
1169
- status_code: number;
1170
- warnings: null;
1171
- errors: null;
1172
- }> {
1173
- const accessToken = await this.getValidAccessToken();
1174
- const params: Record<string, string> = {};
1175
-
1176
- if (options?.broker_name) {
1177
- params.broker_id = options.broker_name;
1178
- }
1179
- if (options?.account_id) {
1180
- params.account_id = options.account_id;
1181
- }
1182
- if (options?.symbol) {
1183
- params.symbol = options.symbol;
1184
- }
1185
-
1186
- return this.request<{
1187
- _id: string;
1188
- response_data: BrokerAccount[];
1189
- message: string;
1190
- status_code: number;
1191
- warnings: null;
1192
- errors: null;
1193
- }>('/brokers/data/accounts', {
1194
- method: 'GET',
1195
- headers: {
1196
- Authorization: `Bearer ${accessToken}`,
1197
- },
1198
- params,
1199
- });
1200
- }
1201
-
1202
- async getBrokerOrders(options?: BrokerDataOptions): Promise<{
1203
- _id: string;
1204
- response_data: BrokerOrder[];
1205
- message: string;
1206
- status_code: number;
1207
- warnings: null;
1208
- errors: null;
1209
- }> {
1210
- const accessToken = await this.getValidAccessToken();
1211
- const params: Record<string, string> = {};
1212
-
1213
- if (options?.broker_name) {
1214
- params.broker_id = options.broker_name;
1215
- }
1216
- if (options?.account_id) {
1217
- params.account_id = options.account_id;
1218
- }
1219
- if (options?.symbol) {
1220
- params.symbol = options.symbol;
1221
- }
1222
-
1223
- return this.request<{
1224
- _id: string;
1225
- response_data: BrokerOrder[];
1226
- message: string;
1227
- status_code: number;
1228
- warnings: null;
1229
- errors: null;
1230
- }>('/brokers/data/orders', {
1231
- method: 'GET',
1232
- headers: {
1233
- Authorization: `Bearer ${accessToken}`,
1234
- },
1235
- params,
1236
- });
1237
- }
1238
-
1239
- async getBrokerPositions(options?: BrokerDataOptions): Promise<{
1240
- _id: string;
1241
- response_data: BrokerPosition[];
1242
- message: string;
1243
- status_code: number;
1244
- warnings: null;
1245
- errors: null;
1246
- }> {
1247
- const accessToken = await this.getValidAccessToken();
1248
- const params: Record<string, string> = {};
1249
-
1250
- if (options?.broker_name) {
1251
- params.broker_id = options.broker_name;
1252
- }
1253
- if (options?.account_id) {
1254
- params.account_id = options.account_id;
1255
- }
1256
- if (options?.symbol) {
1257
- params.symbol = options.symbol;
1258
- }
1259
-
1260
- return this.request<{
1261
- _id: string;
1262
- response_data: BrokerPosition[];
1263
- message: string;
1264
- status_code: number;
1265
- warnings: null;
1266
- errors: null;
1267
- }>('/brokers/data/positions', {
1268
- method: 'GET',
1269
- headers: {
1270
- Authorization: `Bearer ${accessToken}`,
1271
- },
1272
- params,
1273
- });
1274
- }
1275
-
1276
- async getBrokerBalances(options?: BrokerDataOptions): Promise<{
1277
- _id: string;
1278
- response_data: BrokerBalance[];
1279
- message: string;
1280
- status_code: number;
1281
- warnings: null;
1282
- errors: null;
1283
- }> {
1284
- const accessToken = await this.getValidAccessToken();
1285
- const params: Record<string, string> = {};
1286
-
1287
- if (options?.broker_name) {
1288
- params.broker_id = options.broker_name;
1289
- }
1290
- if (options?.account_id) {
1291
- params.account_id = options.account_id;
1292
- }
1293
- if (options?.symbol) {
1294
- params.symbol = options.symbol;
1295
- }
1296
-
1297
- return this.request<{
1298
- _id: string;
1299
- response_data: BrokerBalance[];
1300
- message: string;
1301
- status_code: number;
1302
- warnings: null;
1303
- errors: null;
1304
- }>('/brokers/data/balances', {
1305
- method: 'GET',
1306
- headers: {
1307
- Authorization: `Bearer ${accessToken}`,
1308
- },
1309
- params,
1310
- });
1311
- }
1312
-
1313
- async getBrokerConnections(): Promise<{
1314
- _id: string;
1315
- response_data: BrokerConnection[];
1316
- message: string;
1317
- status_code: number;
1318
- warnings: null;
1319
- errors: null;
1320
- }> {
1321
- const accessToken = await this.getValidAccessToken();
1322
- return this.request<{
1323
- _id: string;
1324
- response_data: BrokerConnection[];
1325
- message: string;
1326
- status_code: number;
1327
- warnings: null;
1328
- errors: null;
1329
- }>('/brokers/connections', {
1330
- method: 'GET',
1331
- headers: {
1332
- Authorization: `Bearer ${accessToken}`,
1333
- },
1334
- });
1335
- }
1336
-
1337
- async getBalances(filters?: any): Promise<{
1338
- _id: string;
1339
- response_data: any[];
1340
- message: string;
1341
- status_code: number;
1342
- warnings: null;
1343
- errors: null;
1344
- }> {
1345
- const accessToken = await this.getValidAccessToken();
1346
- const params = new URLSearchParams();
1347
- if (filters) {
1348
- Object.entries(filters).forEach(([key, value]) => {
1349
- if (value !== undefined && value !== null) {
1350
- params.append(key, String(value));
1351
- }
1352
- });
1353
- }
1354
-
1355
- const queryString = params.toString();
1356
- const url = queryString ? `/brokers/data/balances?${queryString}` : '/brokers/data/balances';
1357
-
1358
- return this.request<{
1359
- _id: string;
1360
- response_data: any[];
1361
- message: string;
1362
- status_code: number;
1363
- warnings: null;
1364
- errors: null;
1365
- }>(url, {
1366
- method: 'GET',
1367
- headers: {
1368
- Authorization: `Bearer ${accessToken}`,
1369
- },
1370
- });
1371
- }
1372
-
1373
- // Page-based pagination methods
1374
- async getBrokerOrdersPage(
1375
- page: number = 1,
1376
- perPage: number = 100,
1377
- filters?: OrdersFilter
1378
- ): Promise<PaginatedResult<BrokerOrder[]>> {
1379
- const accessToken = await this.getValidAccessToken();
1380
- const offset = (page - 1) * perPage;
1381
- const params: Record<string, string> = {
1382
- limit: perPage.toString(),
1383
- offset: offset.toString(),
1384
- };
1385
-
1386
- // Add filter parameters
1387
- if (filters) {
1388
- if (filters.broker_id) params.broker_id = filters.broker_id;
1389
- if (filters.connection_id) params.connection_id = filters.connection_id;
1390
- if (filters.account_id) params.account_id = filters.account_id;
1391
- if (filters.symbol) params.symbol = filters.symbol;
1392
- if (filters.status) params.status = filters.status;
1393
- if (filters.side) params.side = filters.side;
1394
- if (filters.asset_type) params.asset_type = filters.asset_type;
1395
- if (filters.created_after) params.created_after = filters.created_after;
1396
- if (filters.created_before) params.created_before = filters.created_before;
1397
- }
1398
-
1399
- const response = await this.request<ApiResponse<BrokerOrder[]>>('/brokers/data/orders', {
1400
- method: 'GET',
1401
- headers: {
1402
- Authorization: `Bearer ${accessToken}`,
1403
- },
1404
- params,
1405
- });
1406
-
1407
- // Create navigation callback for pagination
1408
- const navigationCallback = async (
1409
- newOffset: number,
1410
- newLimit: number
1411
- ): Promise<PaginatedResult<BrokerOrder[]>> => {
1412
- const newParams: Record<string, string> = {
1413
- limit: newLimit.toString(),
1414
- offset: newOffset.toString(),
1415
- };
1416
-
1417
- // Add filter parameters
1418
- if (filters) {
1419
- if (filters.broker_id) newParams.broker_id = filters.broker_id;
1420
- if (filters.connection_id) newParams.connection_id = filters.connection_id;
1421
- if (filters.account_id) newParams.account_id = filters.account_id;
1422
- if (filters.symbol) newParams.symbol = filters.symbol;
1423
- if (filters.status) newParams.status = filters.status;
1424
- if (filters.side) newParams.side = filters.side;
1425
- if (filters.asset_type) newParams.asset_type = filters.asset_type;
1426
- if (filters.created_after) newParams.created_after = filters.created_after;
1427
- if (filters.created_before) newParams.created_before = filters.created_before;
1428
- }
1429
-
1430
- const newResponse = await this.request<ApiResponse<BrokerOrder[]>>('/brokers/data/orders', {
1431
- method: 'GET',
1432
- headers: {
1433
- Authorization: `Bearer ${accessToken}`,
1434
- },
1435
- params: newParams,
1436
- });
1437
-
1438
- return new PaginatedResult(
1439
- newResponse.response_data,
1440
- newResponse.pagination || {
1441
- has_more: false,
1442
- next_offset: newOffset,
1443
- current_offset: newOffset,
1444
- limit: newLimit,
1445
- },
1446
- navigationCallback
1447
- );
1448
- };
1449
-
1450
- return new PaginatedResult(
1451
- response.response_data,
1452
- response.pagination || {
1453
- has_more: false,
1454
- next_offset: offset,
1455
- current_offset: offset,
1456
- limit: perPage,
1457
- },
1458
- navigationCallback
1459
- );
1460
- }
1461
-
1462
- async getBrokerAccountsPage(
1463
- page: number = 1,
1464
- perPage: number = 100,
1465
- filters?: AccountsFilter
1466
- ): Promise<PaginatedResult<BrokerAccount[]>> {
1467
- const accessToken = await this.getValidAccessToken();
1468
- const offset = (page - 1) * perPage;
1469
- const params: Record<string, string> = {
1470
- limit: perPage.toString(),
1471
- offset: offset.toString(),
1472
- };
1473
-
1474
- // Add filter parameters
1475
- if (filters) {
1476
- if (filters.broker_id) params.broker_id = filters.broker_id;
1477
- if (filters.connection_id) params.connection_id = filters.connection_id;
1478
- if (filters.account_type) params.account_type = filters.account_type;
1479
- if (filters.status) params.status = filters.status;
1480
- if (filters.currency) params.currency = filters.currency;
1481
- }
1482
-
1483
- const response = await this.request<ApiResponse<BrokerAccount[]>>('/brokers/data/accounts', {
1484
- method: 'GET',
1485
- headers: {
1486
- Authorization: `Bearer ${accessToken}`,
1487
- },
1488
- params,
1489
- });
1490
-
1491
- // Create navigation callback for pagination
1492
- const navigationCallback = async (
1493
- newOffset: number,
1494
- newLimit: number
1495
- ): Promise<PaginatedResult<BrokerAccount[]>> => {
1496
- const newParams: Record<string, string> = {
1497
- limit: newLimit.toString(),
1498
- offset: newOffset.toString(),
1499
- };
1500
-
1501
- // Add filter parameters
1502
- if (filters) {
1503
- if (filters.broker_id) newParams.broker_id = filters.broker_id;
1504
- if (filters.connection_id) newParams.connection_id = filters.connection_id;
1505
- if (filters.account_type) newParams.account_type = filters.account_type;
1506
- if (filters.status) newParams.status = filters.status;
1507
- if (filters.currency) newParams.currency = filters.currency;
1508
- }
1509
-
1510
- const newResponse = await this.request<ApiResponse<BrokerAccount[]>>(
1511
- '/brokers/data/accounts',
1512
- {
1513
- method: 'GET',
1514
- headers: {
1515
- Authorization: `Bearer ${accessToken}`,
1516
- },
1517
- params: newParams,
1518
- }
1519
- );
1520
-
1521
- return new PaginatedResult(
1522
- newResponse.response_data,
1523
- newResponse.pagination || {
1524
- has_more: false,
1525
- next_offset: newOffset,
1526
- current_offset: newOffset,
1527
- limit: newLimit,
1528
- },
1529
- navigationCallback
1530
- );
1531
- };
1532
-
1533
- return new PaginatedResult(
1534
- response.response_data,
1535
- response.pagination || {
1536
- has_more: false,
1537
- next_offset: offset,
1538
- current_offset: offset,
1539
- limit: perPage,
1540
- },
1541
- navigationCallback
1542
- );
1543
- }
1544
-
1545
- async getBrokerPositionsPage(
1546
- page: number = 1,
1547
- perPage: number = 100,
1548
- filters?: PositionsFilter
1549
- ): Promise<PaginatedResult<BrokerPosition[]>> {
1550
- const accessToken = await this.getValidAccessToken();
1551
- const offset = (page - 1) * perPage;
1552
- const params: Record<string, string> = {
1553
- limit: perPage.toString(),
1554
- offset: offset.toString(),
1555
- };
1556
-
1557
- // Add filter parameters
1558
- if (filters) {
1559
- if (filters.broker_id) params.broker_id = filters.broker_id;
1560
- if (filters.account_id) params.account_id = filters.account_id;
1561
- if (filters.symbol) params.symbol = filters.symbol;
1562
- if (filters.position_status) params.position_status = filters.position_status;
1563
- if (filters.side) params.side = filters.side;
1564
- }
1565
-
1566
- const response = await this.request<ApiResponse<BrokerPosition[]>>('/brokers/data/positions', {
1567
- method: 'GET',
1568
- headers: {
1569
- Authorization: `Bearer ${accessToken}`,
1570
- },
1571
- params,
1572
- });
1573
-
1574
- // Create navigation callback for pagination
1575
- const navigationCallback = async (
1576
- newOffset: number,
1577
- newLimit: number
1578
- ): Promise<PaginatedResult<BrokerPosition[]>> => {
1579
- const newParams: Record<string, string> = {
1580
- limit: newLimit.toString(),
1581
- offset: newOffset.toString(),
1582
- };
1583
-
1584
- // Add filter parameters
1585
- if (filters) {
1586
- if (filters.broker_id) newParams.broker_id = filters.broker_id;
1587
- if (filters.account_id) newParams.account_id = filters.account_id;
1588
- if (filters.symbol) newParams.symbol = filters.symbol;
1589
- if (filters.position_status) newParams.position_status = filters.position_status;
1590
- if (filters.side) newParams.side = filters.side;
1591
- }
1592
-
1593
- const newResponse = await this.request<ApiResponse<BrokerPosition[]>>(
1594
- '/brokers/data/positions',
1595
- {
1596
- method: 'GET',
1597
- headers: {
1598
- Authorization: `Bearer ${accessToken}`,
1599
- },
1600
- params: newParams,
1601
- }
1602
- );
1603
-
1604
- return new PaginatedResult(
1605
- newResponse.response_data,
1606
- newResponse.pagination || {
1607
- has_more: false,
1608
- next_offset: newOffset,
1609
- current_offset: newOffset,
1610
- limit: newLimit,
1611
- },
1612
- navigationCallback
1613
- );
1614
- };
1615
-
1616
- return new PaginatedResult(
1617
- response.response_data,
1618
- response.pagination || {
1619
- has_more: false,
1620
- next_offset: offset,
1621
- current_offset: offset,
1622
- limit: perPage,
1623
- },
1624
- navigationCallback
1625
- );
1626
- }
1627
-
1628
- async getBrokerBalancesPage(
1629
- page: number = 1,
1630
- perPage: number = 100,
1631
- filters?: BalancesFilter
1632
- ): Promise<PaginatedResult<BrokerBalance[]>> {
1633
- const accessToken = await this.getValidAccessToken();
1634
- const offset = (page - 1) * perPage;
1635
- const params: Record<string, string> = {
1636
- limit: perPage.toString(),
1637
- offset: offset.toString(),
1638
- };
1639
-
1640
- // Add filter parameters
1641
- if (filters) {
1642
- if (filters.broker_id) params.broker_id = filters.broker_id;
1643
- if (filters.connection_id) params.connection_id = filters.connection_id;
1644
- if (filters.account_id) params.account_id = filters.account_id;
1645
- if (filters.is_end_of_day_snapshot !== undefined)
1646
- params.is_end_of_day_snapshot = filters.is_end_of_day_snapshot.toString();
1647
- if (filters.balance_created_after)
1648
- params.balance_created_after = filters.balance_created_after;
1649
- if (filters.balance_created_before)
1650
- params.balance_created_before = filters.balance_created_before;
1651
- if (filters.with_metadata !== undefined)
1652
- params.with_metadata = filters.with_metadata.toString();
1653
- }
1654
-
1655
- const response = await this.request<ApiResponse<BrokerBalance[]>>('/brokers/data/balances', {
1656
- method: 'GET',
1657
- headers: {
1658
- Authorization: `Bearer ${accessToken}`,
1659
- },
1660
- params,
1661
- });
1662
-
1663
- // Create navigation callback for pagination
1664
- const navigationCallback = async (
1665
- newOffset: number,
1666
- newLimit: number
1667
- ): Promise<PaginatedResult<BrokerBalance[]>> => {
1668
- const newParams: Record<string, string> = {
1669
- limit: newLimit.toString(),
1670
- offset: newOffset.toString(),
1671
- };
1672
-
1673
- // Add filter parameters
1674
- if (filters) {
1675
- if (filters.broker_id) newParams.broker_id = filters.broker_id;
1676
- if (filters.connection_id) newParams.connection_id = filters.connection_id;
1677
- if (filters.account_id) newParams.account_id = filters.account_id;
1678
- if (filters.is_end_of_day_snapshot !== undefined)
1679
- newParams.is_end_of_day_snapshot = filters.is_end_of_day_snapshot.toString();
1680
- if (filters.balance_created_after)
1681
- newParams.balance_created_after = filters.balance_created_after;
1682
- if (filters.balance_created_before)
1683
- newParams.balance_created_before = filters.balance_created_before;
1684
- if (filters.with_metadata !== undefined)
1685
- newParams.with_metadata = filters.with_metadata.toString();
1686
- }
1687
-
1688
- const newResponse = await this.request<ApiResponse<BrokerBalance[]>>(
1689
- '/brokers/data/balances',
1690
- {
1691
- method: 'GET',
1692
- headers: {
1693
- Authorization: `Bearer ${accessToken}`,
1694
- },
1695
- params: newParams,
1696
- }
1697
- );
1698
-
1699
- return new PaginatedResult(
1700
- newResponse.response_data,
1701
- newResponse.pagination || {
1702
- has_more: false,
1703
- next_offset: newOffset,
1704
- current_offset: newOffset,
1705
- limit: newLimit,
1706
- },
1707
- navigationCallback
1708
- );
1709
- };
1710
-
1711
- return new PaginatedResult(
1712
- response.response_data,
1713
- response.pagination || {
1714
- has_more: false,
1715
- next_offset: offset,
1716
- current_offset: offset,
1717
- limit: perPage,
1718
- },
1719
- navigationCallback
1720
- );
1721
- }
1722
-
1723
- // Navigation methods
1724
- async getNextPage<T>(
1725
- previousResult: PaginatedResult<T>,
1726
- fetchFunction: (offset: number, limit: number) => Promise<PaginatedResult<T>>
1727
- ): Promise<PaginatedResult<T> | null> {
1728
- if (!previousResult.hasNext) {
1729
- return null;
1730
- }
1731
-
1732
- return fetchFunction(previousResult.metadata.nextOffset, previousResult.metadata.limit);
1733
- }
1734
-
1735
- /**
1736
- * Check if this is a mock client
1737
- * @returns false for real API client
1738
- */
1739
- isMockClient(): boolean {
1740
- return false;
1741
- }
1742
-
1743
- /**
1744
- * Disconnect a company from a broker connection
1745
- * @param connectionId - The connection ID to disconnect
1746
- * @returns Promise with disconnect response
1747
- */
1748
- async disconnectCompany(connectionId: string): Promise<DisconnectCompanyResponse> {
1749
- const accessToken = await this.getValidAccessToken();
1750
- return this.request<DisconnectCompanyResponse>(`/brokers/disconnect/${connectionId}`, {
1751
- method: 'DELETE',
1752
- headers: {
1753
- Authorization: `Bearer ${accessToken}`,
1754
- },
1755
- });
1756
- }
1757
-
1758
- /**
1759
- * Get order fills for a specific order
1760
- * @param orderId - The order ID
1761
- * @param filter - Optional filter parameters
1762
- * @returns Promise with order fills response
1763
- */
1764
- async getOrderFills(
1765
- orderId: string,
1766
- filter?: OrderFillsFilter
1767
- ): Promise<{
1768
- _id: string;
1769
- response_data: OrderFill[];
1770
- message: string;
1771
- status_code: number;
1772
- warnings: null;
1773
- errors: null;
1774
- }> {
1775
- const accessToken = await this.getValidAccessToken();
1776
- const params: Record<string, string> = {};
1777
-
1778
- if (filter?.connection_id) {
1779
- params.connection_id = filter.connection_id;
1780
- }
1781
- if (filter?.limit) {
1782
- params.limit = filter.limit.toString();
1783
- }
1784
- if (filter?.offset) {
1785
- params.offset = filter.offset.toString();
1786
- }
1787
-
1788
- return this.request<{
1789
- _id: string;
1790
- response_data: OrderFill[];
1791
- message: string;
1792
- status_code: number;
1793
- warnings: null;
1794
- errors: null;
1795
- }>(`/brokers/data/orders/${orderId}/fills`, {
1796
- method: 'GET',
1797
- headers: {
1798
- Authorization: `Bearer ${accessToken}`,
1799
- },
1800
- params,
1801
- });
1802
- }
1803
-
1804
- /**
1805
- * Get order events for a specific order
1806
- * @param orderId - The order ID
1807
- * @param filter - Optional filter parameters
1808
- * @returns Promise with order events response
1809
- */
1810
- async getOrderEvents(
1811
- orderId: string,
1812
- filter?: OrderEventsFilter
1813
- ): Promise<{
1814
- _id: string;
1815
- response_data: OrderEvent[];
1816
- message: string;
1817
- status_code: number;
1818
- warnings: null;
1819
- errors: null;
1820
- }> {
1821
- const accessToken = await this.getValidAccessToken();
1822
- const params: Record<string, string> = {};
1823
-
1824
- if (filter?.connection_id) {
1825
- params.connection_id = filter.connection_id;
1826
- }
1827
- if (filter?.limit) {
1828
- params.limit = filter.limit.toString();
1829
- }
1830
- if (filter?.offset) {
1831
- params.offset = filter.offset.toString();
1832
- }
1833
-
1834
- return this.request<{
1835
- _id: string;
1836
- response_data: OrderEvent[];
1837
- message: string;
1838
- status_code: number;
1839
- warnings: null;
1840
- errors: null;
1841
- }>(`/brokers/data/orders/${orderId}/events`, {
1842
- method: 'GET',
1843
- headers: {
1844
- Authorization: `Bearer ${accessToken}`,
1845
- },
1846
- params,
1847
- });
1848
- }
1849
-
1850
- /**
1851
- * Get order groups
1852
- * @param filter - Optional filter parameters
1853
- * @returns Promise with order groups response
1854
- */
1855
- async getOrderGroups(
1856
- filter?: OrderGroupsFilter
1857
- ): Promise<{
1858
- _id: string;
1859
- response_data: OrderGroup[];
1860
- message: string;
1861
- status_code: number;
1862
- warnings: null;
1863
- errors: null;
1864
- }> {
1865
- const accessToken = await this.getValidAccessToken();
1866
- const params: Record<string, string> = {};
1867
-
1868
- if (filter?.broker_id) {
1869
- params.broker_id = filter.broker_id;
1870
- }
1871
- if (filter?.connection_id) {
1872
- params.connection_id = filter.connection_id;
1873
- }
1874
- if (filter?.limit) {
1875
- params.limit = filter.limit.toString();
1876
- }
1877
- if (filter?.offset) {
1878
- params.offset = filter.offset.toString();
1879
- }
1880
- if (filter?.created_after) {
1881
- params.created_after = filter.created_after;
1882
- }
1883
- if (filter?.created_before) {
1884
- params.created_before = filter.created_before;
1885
- }
1886
-
1887
- return this.request<{
1888
- _id: string;
1889
- response_data: OrderGroup[];
1890
- message: string;
1891
- status_code: number;
1892
- warnings: null;
1893
- errors: null;
1894
- }>('/brokers/data/orders/groups', {
1895
- method: 'GET',
1896
- headers: {
1897
- Authorization: `Bearer ${accessToken}`,
1898
- },
1899
- params,
1900
- });
1901
- }
1902
-
1903
- /**
1904
- * Get position lots (tax lots for positions)
1905
- * @param filter - Optional filter parameters
1906
- * @returns Promise with position lots response
1907
- */
1908
- async getPositionLots(
1909
- filter?: PositionLotsFilter
1910
- ): Promise<{
1911
- _id: string;
1912
- response_data: PositionLot[];
1913
- message: string;
1914
- status_code: number;
1915
- warnings: null;
1916
- errors: null;
1917
- }> {
1918
- const accessToken = await this.getValidAccessToken();
1919
- const params: Record<string, string> = {};
1920
-
1921
- if (filter?.broker_id) {
1922
- params.broker_id = filter.broker_id;
1923
- }
1924
- if (filter?.connection_id) {
1925
- params.connection_id = filter.connection_id;
1926
- }
1927
- if (filter?.account_id) {
1928
- params.account_id = filter.account_id;
1929
- }
1930
- if (filter?.symbol) {
1931
- params.symbol = filter.symbol;
1932
- }
1933
- if (filter?.position_id) {
1934
- params.position_id = filter.position_id;
1935
- }
1936
- if (filter?.limit) {
1937
- params.limit = filter.limit.toString();
1938
- }
1939
- if (filter?.offset) {
1940
- params.offset = filter.offset.toString();
1941
- }
1942
-
1943
- return this.request<{
1944
- _id: string;
1945
- response_data: PositionLot[];
1946
- message: string;
1947
- status_code: number;
1948
- warnings: null;
1949
- errors: null;
1950
- }>('/brokers/data/positions/lots', {
1951
- method: 'GET',
1952
- headers: {
1953
- Authorization: `Bearer ${accessToken}`,
1954
- },
1955
- params,
1956
- });
1957
- }
1958
-
1959
- /**
1960
- * Get position lot fills for a specific lot
1961
- * @param lotId - The position lot ID
1962
- * @param filter - Optional filter parameters
1963
- * @returns Promise with position lot fills response
1964
- */
1965
- async getPositionLotFills(
1966
- lotId: string,
1967
- filter?: PositionLotFillsFilter
1968
- ): Promise<{
1969
- _id: string;
1970
- response_data: PositionLotFill[];
1971
- message: string;
1972
- status_code: number;
1973
- warnings: null;
1974
- errors: null;
1975
- }> {
1976
- const accessToken = await this.getValidAccessToken();
1977
- const params: Record<string, string> = {};
1978
-
1979
- if (filter?.connection_id) {
1980
- params.connection_id = filter.connection_id;
1981
- }
1982
- if (filter?.limit) {
1983
- params.limit = filter.limit.toString();
1984
- }
1985
- if (filter?.offset) {
1986
- params.offset = filter.offset.toString();
1987
- }
1988
-
1989
- return this.request<{
1990
- _id: string;
1991
- response_data: PositionLotFill[];
1992
- message: string;
1993
- status_code: number;
1994
- warnings: null;
1995
- errors: null;
1996
- }>(`/brokers/data/positions/lots/${lotId}/fills`, {
1997
- method: 'GET',
1998
- headers: {
1999
- Authorization: `Bearer ${accessToken}`,
2000
- },
2001
- params,
2002
- });
2003
- }
2004
- }