Skip to content

Commit f02fff9

Browse files
authored
fix: query live orders per account (#44)
1 parent 959de17 commit f02fff9

2 files changed

Lines changed: 134 additions & 7 deletions

File tree

src/ib-client.ts

Lines changed: 87 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -739,17 +739,100 @@ export class IBClient {
739739
}
740740
}
741741

742+
private normalizeAccountId(account: any): string | undefined {
743+
if (!account) {
744+
return undefined;
745+
}
746+
747+
if (typeof account === "string") {
748+
return account.trim() || undefined;
749+
}
750+
751+
const id = account.id ?? account.accountId ?? account.account_id ?? account.acctId ?? account.account;
752+
return typeof id === "string" && id.trim() ? id.trim() : undefined;
753+
}
754+
755+
private extractAccountIds(data: any): string[] {
756+
const candidates = [
757+
...(Array.isArray(data) ? data : []),
758+
...(Array.isArray(data?.accounts) ? data.accounts : []),
759+
...(Array.isArray(data?.accountIds) ? data.accountIds : []),
760+
data?.selectedAccount,
761+
data?.selected_account,
762+
];
763+
764+
return [...new Set(
765+
candidates
766+
.map((account) => this.normalizeAccountId(account))
767+
.filter((accountId): accountId is string => Boolean(accountId))
768+
)];
769+
}
770+
771+
private extractOrders(data: any): any[] {
772+
if (Array.isArray(data)) {
773+
return data;
774+
}
775+
776+
if (Array.isArray(data?.orders)) {
777+
return data.orders;
778+
}
779+
780+
return [];
781+
}
782+
783+
private async getOrderAccountIds(): Promise<string[]> {
784+
const accountSources = [
785+
{ label: "/iserver/accounts", fetch: () => this.client.get("/iserver/accounts") },
786+
{ label: "/portfolio/accounts", fetch: () => this.client.get("/portfolio/accounts") },
787+
];
788+
789+
for (const source of accountSources) {
790+
try {
791+
const response = await source.fetch();
792+
const accountIds = this.extractAccountIds(response.data);
793+
if (accountIds.length > 0) {
794+
return accountIds;
795+
}
796+
} catch (error) {
797+
Logger.warn(`[ORDERS] Failed to discover accounts via ${source.label}:`, error);
798+
}
799+
}
800+
801+
return [];
802+
}
803+
742804
async getOrders(accountId?: string): Promise<any> {
743805
try {
744806
const url = "/iserver/account/orders";
745-
const params: any = {};
746807

747808
if (accountId) {
748-
params.accountId = accountId;
809+
const response = await this.client.get(url, { params: { accountId } });
810+
return response.data;
749811
}
750812

751-
const response = await this.client.get(url, { params });
752-
return response.data;
813+
const accountIds = await this.getOrderAccountIds();
814+
if (accountIds.length === 0) {
815+
Logger.warn("[ORDERS] Could not discover account IDs; falling back to unscoped orders request");
816+
const response = await this.client.get(url, { params: {} });
817+
return response.data;
818+
}
819+
820+
const accountResults = [];
821+
const orders: any[] = [];
822+
823+
for (const discoveredAccountId of accountIds) {
824+
const response = await this.client.get(url, { params: { accountId: discoveredAccountId } });
825+
accountResults.push({
826+
accountId: discoveredAccountId,
827+
data: response.data,
828+
});
829+
orders.push(...this.extractOrders(response.data));
830+
}
831+
832+
return {
833+
orders,
834+
accountResults,
835+
};
753836
} catch (error) {
754837
Logger.error("Failed to get orders:", error);
755838

test/ib-client.test.ts

Lines changed: 47 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -477,15 +477,59 @@ describe('IBClient', () => {
477477
});
478478

479479
describe('getOrders', () => {
480-
it('should fetch all orders', async () => {
480+
it('should fetch orders for all discovered trading accounts', async () => {
481+
const mockClient = vi.mocked(axios.create).mock.results[0].value;
482+
const firstAccountOrders = [{ orderId: '123', status: 'Filled' }];
483+
const secondAccountOrders = [{ orderId: '456', status: 'Submitted' }];
484+
485+
mockClient.get
486+
.mockResolvedValueOnce({ data: { accounts: ['U12345', { accountId: 'U67890' }], selectedAccount: 'U12345' } })
487+
.mockResolvedValueOnce({ data: { orders: firstAccountOrders } })
488+
.mockResolvedValueOnce({ data: { orders: secondAccountOrders } });
489+
490+
const result = await client.getOrders();
491+
492+
expect(mockClient.get).toHaveBeenNthCalledWith(1, '/iserver/accounts');
493+
expect(mockClient.get).toHaveBeenNthCalledWith(2, '/iserver/account/orders', { params: { accountId: 'U12345' } });
494+
expect(mockClient.get).toHaveBeenNthCalledWith(3, '/iserver/account/orders', { params: { accountId: 'U67890' } });
495+
expect(result.orders).toEqual([...firstAccountOrders, ...secondAccountOrders]);
496+
expect(result.accountResults).toEqual([
497+
{ accountId: 'U12345', data: { orders: firstAccountOrders } },
498+
{ accountId: 'U67890', data: { orders: secondAccountOrders } },
499+
]);
500+
});
501+
502+
it('should fall back to portfolio accounts when iserver account discovery fails', async () => {
481503
const mockClient = vi.mocked(axios.create).mock.results[0].value;
482504
const mockOrders = [{ orderId: '123', status: 'Filled' }];
483505

484-
mockClient.get.mockResolvedValueOnce({ data: mockOrders });
506+
mockClient.get
507+
.mockRejectedValueOnce(new Error('iserver accounts unavailable'))
508+
.mockResolvedValueOnce({ data: [{ id: 'U12345' }] })
509+
.mockResolvedValueOnce({ data: { orders: mockOrders } });
510+
511+
const result = await client.getOrders();
512+
513+
expect(mockClient.get).toHaveBeenNthCalledWith(1, '/iserver/accounts');
514+
expect(mockClient.get).toHaveBeenNthCalledWith(2, '/portfolio/accounts');
515+
expect(mockClient.get).toHaveBeenNthCalledWith(3, '/iserver/account/orders', { params: { accountId: 'U12345' } });
516+
expect(result.orders).toEqual(mockOrders);
517+
});
518+
519+
it('should fall back to an unscoped orders request when account discovery returns no accounts', async () => {
520+
const mockClient = vi.mocked(axios.create).mock.results[0].value;
521+
const mockOrders = [{ orderId: '123', status: 'Filled' }];
522+
523+
mockClient.get
524+
.mockResolvedValueOnce({ data: { accounts: [] } })
525+
.mockResolvedValueOnce({ data: [] })
526+
.mockResolvedValueOnce({ data: mockOrders });
485527

486528
const result = await client.getOrders();
487529

488-
expect(mockClient.get).toHaveBeenCalledWith('/iserver/account/orders', { params: {} });
530+
expect(mockClient.get).toHaveBeenNthCalledWith(1, '/iserver/accounts');
531+
expect(mockClient.get).toHaveBeenNthCalledWith(2, '/portfolio/accounts');
532+
expect(mockClient.get).toHaveBeenNthCalledWith(3, '/iserver/account/orders', { params: {} });
489533
expect(result).toEqual(mockOrders);
490534
});
491535

0 commit comments

Comments
 (0)