@finatic/client 0.0.139 → 0.0.141

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