Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
91 changes: 87 additions & 4 deletions src/ib-client.ts
Original file line number Diff line number Diff line change
Expand Up @@ -739,17 +739,100 @@ export class IBClient {
}
}

private normalizeAccountId(account: any): string | undefined {
if (!account) {
return undefined;
}

if (typeof account === "string") {
return account.trim() || undefined;
}

const id = account.id ?? account.accountId ?? account.account_id ?? account.acctId ?? account.account;
return typeof id === "string" && id.trim() ? id.trim() : undefined;
}

private extractAccountIds(data: any): string[] {
const candidates = [
...(Array.isArray(data) ? data : []),
...(Array.isArray(data?.accounts) ? data.accounts : []),
...(Array.isArray(data?.accountIds) ? data.accountIds : []),
data?.selectedAccount,
data?.selected_account,
];

return [...new Set(
candidates
.map((account) => this.normalizeAccountId(account))
.filter((accountId): accountId is string => Boolean(accountId))
)];
}

private extractOrders(data: any): any[] {
if (Array.isArray(data)) {
return data;
}

if (Array.isArray(data?.orders)) {
return data.orders;
}

return [];
}

private async getOrderAccountIds(): Promise<string[]> {
const accountSources = [
{ label: "/iserver/accounts", fetch: () => this.client.get("/iserver/accounts") },
{ label: "/portfolio/accounts", fetch: () => this.client.get("/portfolio/accounts") },
];

for (const source of accountSources) {
try {
const response = await source.fetch();
const accountIds = this.extractAccountIds(response.data);
if (accountIds.length > 0) {
return accountIds;
}
} catch (error) {
Logger.warn(`[ORDERS] Failed to discover accounts via ${source.label}:`, error);
}
}

return [];
}

async getOrders(accountId?: string): Promise<any> {
try {
const url = "/iserver/account/orders";
const params: any = {};

if (accountId) {
params.accountId = accountId;
const response = await this.client.get(url, { params: { accountId } });
return response.data;
}

const response = await this.client.get(url, { params });
return response.data;
const accountIds = await this.getOrderAccountIds();
if (accountIds.length === 0) {
Logger.warn("[ORDERS] Could not discover account IDs; falling back to unscoped orders request");
const response = await this.client.get(url, { params: {} });
return response.data;
}

const accountResults = [];
const orders: any[] = [];

for (const discoveredAccountId of accountIds) {
const response = await this.client.get(url, { params: { accountId: discoveredAccountId } });
accountResults.push({
accountId: discoveredAccountId,
data: response.data,
});
orders.push(...this.extractOrders(response.data));
}

return {
orders,
accountResults,
};
} catch (error) {
Logger.error("Failed to get orders:", error);

Expand Down
50 changes: 47 additions & 3 deletions test/ib-client.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -477,15 +477,59 @@ describe('IBClient', () => {
});

describe('getOrders', () => {
it('should fetch all orders', async () => {
it('should fetch orders for all discovered trading accounts', async () => {
const mockClient = vi.mocked(axios.create).mock.results[0].value;
const firstAccountOrders = [{ orderId: '123', status: 'Filled' }];
const secondAccountOrders = [{ orderId: '456', status: 'Submitted' }];

mockClient.get
.mockResolvedValueOnce({ data: { accounts: ['U12345', { accountId: 'U67890' }], selectedAccount: 'U12345' } })
.mockResolvedValueOnce({ data: { orders: firstAccountOrders } })
.mockResolvedValueOnce({ data: { orders: secondAccountOrders } });

const result = await client.getOrders();

expect(mockClient.get).toHaveBeenNthCalledWith(1, '/iserver/accounts');
expect(mockClient.get).toHaveBeenNthCalledWith(2, '/iserver/account/orders', { params: { accountId: 'U12345' } });
expect(mockClient.get).toHaveBeenNthCalledWith(3, '/iserver/account/orders', { params: { accountId: 'U67890' } });
expect(result.orders).toEqual([...firstAccountOrders, ...secondAccountOrders]);
expect(result.accountResults).toEqual([
{ accountId: 'U12345', data: { orders: firstAccountOrders } },
{ accountId: 'U67890', data: { orders: secondAccountOrders } },
]);
});

it('should fall back to portfolio accounts when iserver account discovery fails', async () => {
const mockClient = vi.mocked(axios.create).mock.results[0].value;
const mockOrders = [{ orderId: '123', status: 'Filled' }];

mockClient.get.mockResolvedValueOnce({ data: mockOrders });
mockClient.get
.mockRejectedValueOnce(new Error('iserver accounts unavailable'))
.mockResolvedValueOnce({ data: [{ id: 'U12345' }] })
.mockResolvedValueOnce({ data: { orders: mockOrders } });

const result = await client.getOrders();

expect(mockClient.get).toHaveBeenNthCalledWith(1, '/iserver/accounts');
expect(mockClient.get).toHaveBeenNthCalledWith(2, '/portfolio/accounts');
expect(mockClient.get).toHaveBeenNthCalledWith(3, '/iserver/account/orders', { params: { accountId: 'U12345' } });
expect(result.orders).toEqual(mockOrders);
});

it('should fall back to an unscoped orders request when account discovery returns no accounts', async () => {
const mockClient = vi.mocked(axios.create).mock.results[0].value;
const mockOrders = [{ orderId: '123', status: 'Filled' }];

mockClient.get
.mockResolvedValueOnce({ data: { accounts: [] } })
.mockResolvedValueOnce({ data: [] })
.mockResolvedValueOnce({ data: mockOrders });

const result = await client.getOrders();

expect(mockClient.get).toHaveBeenCalledWith('/iserver/account/orders', { params: {} });
expect(mockClient.get).toHaveBeenNthCalledWith(1, '/iserver/accounts');
expect(mockClient.get).toHaveBeenNthCalledWith(2, '/portfolio/accounts');
expect(mockClient.get).toHaveBeenNthCalledWith(3, '/iserver/account/orders', { params: {} });
expect(result).toEqual(mockOrders);
});

Expand Down
Loading