@huskly/ibkr-client 0.6.0 → 0.7.0
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.
- package/README.md +26 -49
- package/dist/ibkr/ibkrApiTypes.d.ts +74 -0
- package/dist/ibkr/ibkrApiTypes.d.ts.map +1 -1
- package/dist/ibkr/ibkrClient.d.ts +23 -2
- package/dist/ibkr/ibkrClient.d.ts.map +1 -1
- package/dist/ibkr/ibkrClient.js +329 -2
- package/dist/ibkr/ibkrClient.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/dist/types.d.ts +65 -9
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +4 -6
- package/dist/types.js.map +1 -1
- package/package.json +4 -10
- package/dist/cli/account.d.ts +0 -7
- package/dist/cli/account.d.ts.map +0 -1
- package/dist/cli/account.js +0 -27
- package/dist/cli/account.js.map +0 -1
- package/dist/cli/index.d.ts +0 -3
- package/dist/cli/index.d.ts.map +0 -1
- package/dist/cli/index.js +0 -50
- package/dist/cli/index.js.map +0 -1
- package/dist/cli/orders.d.ts +0 -4
- package/dist/cli/orders.d.ts.map +0 -1
- package/dist/cli/orders.js +0 -5
- package/dist/cli/orders.js.map +0 -1
- package/dist/cli/positions.d.ts +0 -12
- package/dist/cli/positions.d.ts.map +0 -1
- package/dist/cli/positions.js +0 -106
- package/dist/cli/positions.js.map +0 -1
- package/dist/cli/quote.d.ts +0 -4
- package/dist/cli/quote.d.ts.map +0 -1
- package/dist/cli/quote.js +0 -5
- package/dist/cli/quote.js.map +0 -1
- package/dist/cli/shared.d.ts +0 -12
- package/dist/cli/shared.d.ts.map +0 -1
- package/dist/cli/shared.js +0 -31
- package/dist/cli/shared.js.map +0 -1
- package/dist/format.d.ts +0 -13
- package/dist/format.d.ts.map +0 -1
- package/dist/format.js +0 -32
- package/dist/format.js.map +0 -1
package/README.md
CHANGED
|
@@ -1,9 +1,10 @@
|
|
|
1
|
-
# IBKR
|
|
1
|
+
# IBKR Client (`@huskly/ibkr-client`)
|
|
2
2
|
|
|
3
|
-
A
|
|
4
|
-
over **OAuth 1.0a**
|
|
5
|
-
|
|
6
|
-
|
|
3
|
+
A reusable TypeScript client for the **Interactive Brokers Web API**,
|
|
4
|
+
authenticating over **OAuth 1.0a** without the Client Portal Gateway. This
|
|
5
|
+
package contains no command-line interface; terminal commands and presentation
|
|
6
|
+
belong exclusively in
|
|
7
|
+
[huskly-cli](https://github.com/felipecsl/huskly-cli).
|
|
7
8
|
|
|
8
9
|
The OAuth 1.0a live-session-token handshake is performed by the
|
|
9
10
|
[`ibkr-client`](https://github.com/art1c0/ibkr-client) package. See the
|
|
@@ -12,17 +13,15 @@ the keys below are generated and registered in the IBKR self-service portal.
|
|
|
12
13
|
|
|
13
14
|
## Layout
|
|
14
15
|
|
|
15
|
-
The
|
|
16
|
-
huskly-cli's `@huskly/schwab-client` + CLI split:
|
|
16
|
+
The repository contains only the reusable client library:
|
|
17
17
|
|
|
18
18
|
| Path | Purpose |
|
|
19
19
|
| ------------------------ | ------------------------------------------------------------------------ |
|
|
20
|
-
| `src/types.ts` | Broker-neutral `BrokerClient` interface
|
|
20
|
+
| `src/types.ts` | Broker-neutral `BrokerClient` interface and normalized domain types |
|
|
21
21
|
| `src/ibkr/ibkrClient.ts` | `IbkrClient` — typed wrapper over `ibkr-client` implementing `BrokerClient` |
|
|
22
22
|
| `src/ibkr/oauthConfig.ts`| Builds the OAuth config from `.pem` files + env vars |
|
|
23
23
|
| `src/ibkr/dhPrime.ts` | Extracts the DH prime (hex) from `dhparam.pem` |
|
|
24
24
|
| `src/ibkr/optionContract.ts` | Canonical OSI parsing and formatting for IBKR option contracts |
|
|
25
|
-
| `src/cli/` | `commander` program, `--broker` flag, and command handlers |
|
|
26
25
|
|
|
27
26
|
The `*.pem` files (`private_signature.pem`, `private_encryption.pem`,
|
|
28
27
|
`dhparam.pem`, plus the public keys) are the cryptographic material from the
|
|
@@ -31,7 +30,7 @@ wiki setup step and are git-ignored.
|
|
|
31
30
|
## Setup
|
|
32
31
|
|
|
33
32
|
```bash
|
|
34
|
-
|
|
33
|
+
yarn install --frozen-lockfile
|
|
35
34
|
```
|
|
36
35
|
|
|
37
36
|
Provide the account-specific secrets, either by copying the template:
|
|
@@ -56,43 +55,21 @@ Optional environment variables:
|
|
|
56
55
|
- `IBKR_KEYS_DIR` — directory holding the `.pem` files (defaults to the current
|
|
57
56
|
working directory).
|
|
58
57
|
- `IBKR_ACCOUNT_ID` — target a specific account (otherwise the first is used).
|
|
58
|
+
- `IBKR_TRANSACTION_CURRENCY` — transaction-query currency (defaults to `USD`).
|
|
59
59
|
|
|
60
|
-
##
|
|
60
|
+
## Library API
|
|
61
61
|
|
|
62
|
-
|
|
62
|
+
`IbkrClient` owns IBKR authentication, requests, raw response types, and
|
|
63
|
+
normalization. Consumers such as huskly-cli provide presentation, command
|
|
64
|
+
routing, and caching. Its broker-neutral account API includes:
|
|
63
65
|
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
npm run dev -- positions --csv # CSV instead of a table
|
|
70
|
-
```
|
|
71
|
-
|
|
72
|
-
Or build and use the `ibkr-cli` binary:
|
|
73
|
-
|
|
74
|
-
```bash
|
|
75
|
-
npm run build
|
|
76
|
-
node dist/cli/index.js account
|
|
77
|
-
```
|
|
78
|
-
|
|
79
|
-
A global `--broker <ibkr|schwab>` flag (default `ibkr`) selects the broker.
|
|
80
|
-
Only IBKR is implemented today; `schwab` is reserved for the huskly-cli merge.
|
|
81
|
-
|
|
82
|
-
Example output:
|
|
83
|
-
|
|
84
|
-
```
|
|
85
|
-
💰 Account Summary
|
|
86
|
-
|
|
87
|
-
Authenticated: yes Competing: no
|
|
88
|
-
Account: U********
|
|
89
|
-
──────────────────────────────────────────────────
|
|
90
|
-
Net Liquidation: $123,456.78
|
|
91
|
-
Available Funds: $...
|
|
92
|
-
...
|
|
93
|
-
```
|
|
66
|
+
- `getAccountBalances()` and `getPositions()` for account state.
|
|
67
|
+
- `getQuotes()` and `searchInstruments()` for equity/ETF discovery and quotes.
|
|
68
|
+
- `fetchTransactionHistory()` for normalized portfolio transactions.
|
|
69
|
+
- `fetchOrders()` for normalized live orders, including aggregate `WORKING`
|
|
70
|
+
matching across IBKR's active order states.
|
|
94
71
|
|
|
95
|
-
|
|
72
|
+
### Strategy market data
|
|
96
73
|
|
|
97
74
|
The reusable `IbkrClient` also exposes typed, read-only strategy data:
|
|
98
75
|
|
|
@@ -152,12 +129,12 @@ NODE
|
|
|
152
129
|
## Development
|
|
153
130
|
|
|
154
131
|
```bash
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
132
|
+
yarn lint # eslint
|
|
133
|
+
yarn format # prettier --write
|
|
134
|
+
yarn typecheck # tsc --noEmit
|
|
135
|
+
yarn test # typecheck + native node:test suite
|
|
136
|
+
yarn build # tsc -> dist/
|
|
137
|
+
yarn run check # lint + format:check + typecheck
|
|
161
138
|
```
|
|
162
139
|
|
|
163
140
|
CI (`.github/workflows/ci.yml`) runs lint, format check, typecheck, tests, and build on
|
|
@@ -51,6 +51,80 @@ export interface IbkrStockListing {
|
|
|
51
51
|
}
|
|
52
52
|
/** `trsrv/stocks` response: keyed by the requested symbol. */
|
|
53
53
|
export type IbkrStocksResponse = Record<string, IbkrStockListing[] | undefined>;
|
|
54
|
+
export interface IbkrTransactionsResponse {
|
|
55
|
+
currency?: string;
|
|
56
|
+
from?: number;
|
|
57
|
+
to?: number;
|
|
58
|
+
includesRealTime?: boolean;
|
|
59
|
+
transactions?: IbkrTransaction[];
|
|
60
|
+
}
|
|
61
|
+
export interface IbkrTransaction {
|
|
62
|
+
date?: string;
|
|
63
|
+
rawDate?: string;
|
|
64
|
+
cur?: string;
|
|
65
|
+
fxRate?: number;
|
|
66
|
+
pr?: number;
|
|
67
|
+
qty?: number;
|
|
68
|
+
acctid?: string;
|
|
69
|
+
amt?: number;
|
|
70
|
+
conid?: number;
|
|
71
|
+
type?: string;
|
|
72
|
+
desc?: string;
|
|
73
|
+
}
|
|
74
|
+
export interface IbkrLiveOrdersResponse {
|
|
75
|
+
orders?: IbkrLiveOrder[];
|
|
76
|
+
snapshot?: boolean;
|
|
77
|
+
}
|
|
78
|
+
export interface IbkrBrokerageAccountsResponse {
|
|
79
|
+
accounts?: string[];
|
|
80
|
+
selectedAccount?: string;
|
|
81
|
+
}
|
|
82
|
+
export interface IbkrSwitchAccountResponse {
|
|
83
|
+
set?: boolean;
|
|
84
|
+
acctId?: string;
|
|
85
|
+
}
|
|
86
|
+
export interface IbkrLiveOrder {
|
|
87
|
+
account?: string;
|
|
88
|
+
acct?: string;
|
|
89
|
+
orderId?: number | string;
|
|
90
|
+
order_id?: number | string;
|
|
91
|
+
conid?: number;
|
|
92
|
+
ticker?: string;
|
|
93
|
+
symbol?: string;
|
|
94
|
+
description1?: string;
|
|
95
|
+
contractDescription1?: string;
|
|
96
|
+
contract_description_1?: string;
|
|
97
|
+
side?: string;
|
|
98
|
+
orderType?: string;
|
|
99
|
+
order_type?: string;
|
|
100
|
+
orderStatus?: string;
|
|
101
|
+
order_status?: string;
|
|
102
|
+
status?: string;
|
|
103
|
+
totalSize?: string | number;
|
|
104
|
+
total_size?: string | number;
|
|
105
|
+
size?: string | number;
|
|
106
|
+
cumFill?: string | number;
|
|
107
|
+
cum_fill?: string | number;
|
|
108
|
+
filledQuantity?: string | number;
|
|
109
|
+
remainingQuantity?: string | number;
|
|
110
|
+
sizeAndFills?: string;
|
|
111
|
+
size_and_fills?: string;
|
|
112
|
+
avgPrice?: string | number;
|
|
113
|
+
averagePrice?: string | number;
|
|
114
|
+
average_price?: string | number;
|
|
115
|
+
price?: string | number;
|
|
116
|
+
limitPrice?: string | number;
|
|
117
|
+
stopPrice?: string | number;
|
|
118
|
+
orderDescription?: string;
|
|
119
|
+
order_description?: string;
|
|
120
|
+
orderDesc?: string;
|
|
121
|
+
orderDescriptionWithContract?: string;
|
|
122
|
+
order_description_with_contract?: string;
|
|
123
|
+
lastExecutionTime?: string;
|
|
124
|
+
lastExecutionTime_r?: number;
|
|
125
|
+
orderTime?: string;
|
|
126
|
+
order_time?: string;
|
|
127
|
+
}
|
|
54
128
|
/** One OHLCV bar from `iserver/marketdata/history`. */
|
|
55
129
|
export interface IbkrMarketDataHistoryBar {
|
|
56
130
|
o?: number;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ibkrApiTypes.d.ts","sourceRoot":"","sources":["../../src/ibkr/ibkrApiTypes.ts"],"names":[],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"ibkrApiTypes.d.ts","sourceRoot":"","sources":["../../src/ibkr/ibkrApiTypes.ts"],"names":[],"mappings":"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"}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { IbkrOauth1Config } from "./oauthConfig.js";
|
|
2
|
-
import type { AccountBalances, AuthStatus, BrokerClient, BrokerPosition, BrokerQuote, OptionContract, OptionMarketQuote, OptionQuoteRequest, OptionRight, PriceHistoryBar, PriceHistoryRequest } from "../types.js";
|
|
2
|
+
import type { AccountBalances, AuthStatus, BrokerAccountOrders, BrokerClient, BrokerInstrument, BrokerInstrumentSearchProjection, BrokerOrdersOptions, BrokerPosition, BrokerQuote, BrokerTransactionHistory, OptionContract, OptionMarketQuote, OptionQuoteRequest, OptionRight, PriceHistoryBar, PriceHistoryRequest } from "../types.js";
|
|
3
3
|
/**
|
|
4
4
|
* Typed IBKR Web API client implementing the broker-neutral {@link BrokerClient}.
|
|
5
5
|
* Wraps the `ibkr-client` npm package, which performs the OAuth 1.0a
|
|
@@ -25,7 +25,10 @@ export declare class IbkrClient implements BrokerClient {
|
|
|
25
25
|
private normalizePosition;
|
|
26
26
|
getQuotes(symbols: string[]): Promise<Record<string, BrokerQuote>>;
|
|
27
27
|
/** Resolve equity/ETF symbols to IBKR contracts via `trsrv/stocks`. */
|
|
28
|
-
|
|
28
|
+
searchInstruments(symbol: string, projection?: BrokerInstrumentSearchProjection): Promise<BrokerInstrument[]>;
|
|
29
|
+
fetchTransactionHistory(startDate: Date, endDate: Date): Promise<BrokerTransactionHistory[]>;
|
|
30
|
+
fetchOrders(options: BrokerOrdersOptions): Promise<BrokerAccountOrders[]>;
|
|
31
|
+
private prepareBrokerageAccount;
|
|
29
32
|
private normalizeStockListing;
|
|
30
33
|
private normalizeStockContract;
|
|
31
34
|
private resolveQuoteContract;
|
|
@@ -45,6 +48,24 @@ export declare class IbkrClient implements BrokerClient {
|
|
|
45
48
|
private fetchOptionQuotes;
|
|
46
49
|
private historyDays;
|
|
47
50
|
private fetchQuoteHistory;
|
|
51
|
+
private normalizeTransaction;
|
|
52
|
+
private normalizeOrder;
|
|
53
|
+
private normalizeOrderLeg;
|
|
54
|
+
private normalizeOrderStatus;
|
|
55
|
+
private normalizeOrderType;
|
|
56
|
+
private normalizeOrderSide;
|
|
57
|
+
private ibkrStatusFilter;
|
|
58
|
+
private orderMatchesStatus;
|
|
59
|
+
private orderBelongsToAccount;
|
|
60
|
+
private orderInDateRange;
|
|
61
|
+
private orderTimeMs;
|
|
62
|
+
private parseOrderTime;
|
|
63
|
+
private firstNumber;
|
|
64
|
+
private firstPositiveNumber;
|
|
65
|
+
private quantityFromDescription;
|
|
66
|
+
private filledQuantityFromSizeAndFills;
|
|
67
|
+
private parseTransactionTime;
|
|
68
|
+
private transactionKey;
|
|
48
69
|
/** Overridable in request-level tests so snapshot warm-up does not sleep. */
|
|
49
70
|
protected wait(ms: number): Promise<void>;
|
|
50
71
|
private normalizeQuote;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ibkrClient.d.ts","sourceRoot":"","sources":["../../src/ibkr/ibkrClient.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,KAAK,EACV,eAAe,EACf,UAAU,EACV,YAAY,
|
|
1
|
+
{"version":3,"file":"ibkrClient.d.ts","sourceRoot":"","sources":["../../src/ibkr/ibkrClient.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,kBAAkB,CAAC;AACzD,OAAO,KAAK,EACV,eAAe,EACf,UAAU,EACV,mBAAmB,EACnB,YAAY,EACZ,gBAAgB,EAChB,gCAAgC,EAGhC,mBAAmB,EACnB,cAAc,EACd,WAAW,EAEX,wBAAwB,EACxB,cAAc,EACd,iBAAiB,EACjB,kBAAkB,EAClB,WAAW,EACX,eAAe,EACf,mBAAmB,EACpB,MAAM,aAAa,CAAC;AAmLrB;;;;;GAKG;AACH,qBAAa,UAAW,YAAW,YAAY;IAC7C,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAgB;IACpC,OAAO,CAAC,WAAW,CAAC,CAAgB;IACpC,OAAO,CAAC,gBAAgB,CAAC,CAAkB;IAC3C,OAAO,CAAC,QAAQ,CAAC,eAAe,CAAgD;gBAEpE,MAAM,EAAE,gBAAgB;IAIpC,6EAA6E;IAC7E,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC;IASf,aAAa,IAAI,OAAO,CAAC,UAAU,CAAC;IAWpC,YAAY,IAAI,OAAO,CAAC,MAAM,CAAC;IAY/B,kBAAkB,IAAI,OAAO,CAAC,eAAe,CAAC;IAc9C,YAAY,CAAC,MAAM,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,EAAE,CAAC;IAa9D,yEAAyE;YAC3D,iBAAiB;IAc/B,kFAAkF;YACpE,WAAW;IAqBzB,OAAO,CAAC,iBAAiB;IAsBnB,SAAS,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,WAAW,CAAC,CAAC;IAyCxE,uEAAuE;IACjE,iBAAiB,CACrB,MAAM,EAAE,MAAM,EACd,UAAU,GAAE,gCAAkD,GAC7D,OAAO,CAAC,gBAAgB,EAAE,CAAC;IAiBxB,uBAAuB,CAC3B,SAAS,EAAE,IAAI,EACf,OAAO,EAAE,IAAI,GACZ,OAAO,CAAC,wBAAwB,EAAE,CAAC;IAoChC,WAAW,CAAC,OAAO,EAAE,mBAAmB,GAAG,OAAO,CAAC,mBAAmB,EAAE,CAAC;YA2BjE,uBAAuB;IAerC,OAAO,CAAC,qBAAqB;IAgB7B,OAAO,CAAC,sBAAsB;YAehB,oBAAoB;IAgClC,qFAAqF;IAC/E,eAAe,CAAC,KAAK,EAAE,mBAAmB,GAAG,OAAO,CAAC,eAAe,EAAE,CAAC;IA4B7E,mFAAmF;IAC7E,iBAAiB,CACrB,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,WAAW,EAClB,QAAQ,EAAE,MAAM,EAChB,MAAM,EAAE,MAAM,GACb,OAAO,CAAC,MAAM,EAAE,CAAC;IAuBpB,2FAA2F;IACrF,cAAc,CAAC,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,iBAAiB,EAAE,CAAC;IAclF,2EAA2E;IACrE,cAAc,CAAC,KAAK,EAAE,kBAAkB,GAAG,OAAO,CAAC,iBAAiB,GAAG,IAAI,CAAC;IAMlF,2EAA2E;IACrE,iBAAiB,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC,cAAc,GAAG,IAAI,CAAC;YAgBxD,qBAAqB;IAYnC,OAAO,CAAC,eAAe;YAWT,mBAAmB;YAgEnB,iBAAiB;IAiE/B,OAAO,CAAC,WAAW;YAeL,iBAAiB;IAqB/B,OAAO,CAAC,oBAAoB;IA2C5B,OAAO,CAAC,cAAc;IAsDtB,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,oBAAoB;IAS5B,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,kBAAkB;IAQ1B,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,kBAAkB;IAS1B,OAAO,CAAC,qBAAqB;IAK7B,OAAO,CAAC,gBAAgB;IAKxB,OAAO,CAAC,WAAW;IAKnB,OAAO,CAAC,cAAc;IAyBtB,OAAO,CAAC,WAAW;IASnB,OAAO,CAAC,mBAAmB;IAQ3B,OAAO,CAAC,uBAAuB;IAS/B,OAAO,CAAC,8BAA8B;IAOtC,OAAO,CAAC,oBAAoB;IAmC5B,OAAO,CAAC,cAAc;IAUtB,6EAA6E;IAC7E,SAAS,CAAC,IAAI,CAAC,EAAE,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC;IAIzC,OAAO,CAAC,cAAc;IAoDtB,OAAO,CAAC,gBAAgB;IAOxB,OAAO,CAAC,kBAAkB;IAO1B,OAAO,CAAC,aAAa;IAQrB,OAAO,CAAC,cAAc;IAOtB,OAAO,CAAC,cAAc;IAStB,OAAO,CAAC,iBAAiB;IAQzB,OAAO,CAAC,eAAe;IAQvB,OAAO,CAAC,cAAc;IAmBtB,iEAAiE;cACjD,WAAW,CAAC,CAAC,EAAE,KAAK,EAAE;QACpC,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;QACtE,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC,CAAC,CAAC;cAIE,GAAG,CAAC,CAAC,EAAE,KAAK,EAAE;QAC5B,IAAI,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;QAChB,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,IAAI,GAAG,SAAS,CAAC,CAAC;QACtE,IAAI,CAAC,EAAE,MAAM,CAAC;KACf,GAAG,OAAO,CAAC,CAAC,CAAC;IAkBd,OAAO,CAAC,mBAAmB;IAqB3B,OAAO,CAAC,mBAAmB;IAc3B,OAAO,CAAC,qBAAqB;IAK7B,OAAO,CAAC,iBAAiB;IASzB,OAAO,CAAC,WAAW;CAuBpB"}
|
package/dist/ibkr/ibkrClient.js
CHANGED
|
@@ -39,6 +39,27 @@ const OPTION_MARKETDATA_BATCH_SIZE = 100;
|
|
|
39
39
|
const READ_ONLY_REQUEST_MAX_RETRIES = 3;
|
|
40
40
|
const REQUEST_RETRY_BASE_DELAY_MS = 250;
|
|
41
41
|
const REQUEST_RETRY_MAX_DELAY_MS = 5_000;
|
|
42
|
+
const DAY_MS = 24 * 60 * 60 * 1000;
|
|
43
|
+
const IBKR_STATUS_FILTERS = {
|
|
44
|
+
CANCELED: "cancelled",
|
|
45
|
+
CANCELLED: "cancelled",
|
|
46
|
+
FILLED: "filled",
|
|
47
|
+
PENDING_CANCEL: "pending_cancel",
|
|
48
|
+
PENDING_SUBMIT: "pending_submit",
|
|
49
|
+
PRE_SUBMITTED: "pre_submitted",
|
|
50
|
+
SUBMITTED: "submitted",
|
|
51
|
+
};
|
|
52
|
+
const IBKR_WORKING_STATUSES = new Set([
|
|
53
|
+
"API_PENDING",
|
|
54
|
+
"PENDING_SUBMIT",
|
|
55
|
+
"PRE_SUBMITTED",
|
|
56
|
+
"SUBMITTED",
|
|
57
|
+
"PENDING_CANCEL",
|
|
58
|
+
]);
|
|
59
|
+
/** Extract the canonical OSI symbol embedded in an IBKR option description. */
|
|
60
|
+
function extractOsiPositionSymbol(contractDescription) {
|
|
61
|
+
return /\[([A-Z]+\s*\d{6}[CP]\d{8})\s+\d+\]\s*$/.exec(contractDescription)?.[1];
|
|
62
|
+
}
|
|
42
63
|
const sleep = (ms) => new Promise((r) => setTimeout(r, ms));
|
|
43
64
|
function parseRetryAfter(raw) {
|
|
44
65
|
const asString = typeof raw === "string" ? raw.trim() : undefined;
|
|
@@ -221,8 +242,12 @@ export class IbkrClient {
|
|
|
221
242
|
normalizePosition(p, dayPnl) {
|
|
222
243
|
const qty = p.position ?? 0;
|
|
223
244
|
const assetClass = p.assetClass ?? "";
|
|
245
|
+
const contractDescription = p.contractDesc ?? String(p.conid ?? "-");
|
|
246
|
+
const symbol = assetClass === "OPT"
|
|
247
|
+
? (extractOsiPositionSymbol(contractDescription) ?? contractDescription)
|
|
248
|
+
: contractDescription;
|
|
224
249
|
return {
|
|
225
|
-
symbol
|
|
250
|
+
symbol,
|
|
226
251
|
assetType: ASSET_CLASS_LABELS[assetClass] ?? (assetClass || "-"),
|
|
227
252
|
longQuantity: qty > 0 ? qty : 0,
|
|
228
253
|
shortQuantity: qty < 0 ? Math.abs(qty) : 0,
|
|
@@ -264,7 +289,10 @@ export class IbkrClient {
|
|
|
264
289
|
return quotes;
|
|
265
290
|
}
|
|
266
291
|
/** Resolve equity/ETF symbols to IBKR contracts via `trsrv/stocks`. */
|
|
267
|
-
async searchInstruments(symbol) {
|
|
292
|
+
async searchInstruments(symbol, projection = "symbol-search") {
|
|
293
|
+
if (projection !== "symbol-search" && projection !== "search") {
|
|
294
|
+
throw new Error(`IBKR search currently supports only symbol-search/search projections (got '${projection}').`);
|
|
295
|
+
}
|
|
268
296
|
const query = symbol.trim().toUpperCase();
|
|
269
297
|
if (!query)
|
|
270
298
|
return [];
|
|
@@ -274,6 +302,71 @@ export class IbkrClient {
|
|
|
274
302
|
});
|
|
275
303
|
return (response[query] ?? []).flatMap((listing) => this.normalizeStockListing(query, listing));
|
|
276
304
|
}
|
|
305
|
+
async fetchTransactionHistory(startDate, endDate) {
|
|
306
|
+
const accountId = await this.getAccountId();
|
|
307
|
+
const rows = await this.fetchAllPositions(accountId);
|
|
308
|
+
const positionsByConid = new Map(rows
|
|
309
|
+
.filter((position) => position.conid !== undefined)
|
|
310
|
+
.map((position) => [position.conid, position]));
|
|
311
|
+
const transactionsByKey = new Map();
|
|
312
|
+
const days = Math.max(1, Math.ceil((endDate.getTime() - startDate.getTime()) / DAY_MS) + 1);
|
|
313
|
+
for (const conid of positionsByConid.keys()) {
|
|
314
|
+
const response = await this.req({
|
|
315
|
+
path: "pa/transactions",
|
|
316
|
+
method: "POST",
|
|
317
|
+
data: {
|
|
318
|
+
acctIds: [accountId],
|
|
319
|
+
conids: [conid],
|
|
320
|
+
currency: process.env["IBKR_TRANSACTION_CURRENCY"] ?? "USD",
|
|
321
|
+
days,
|
|
322
|
+
},
|
|
323
|
+
});
|
|
324
|
+
for (const transaction of response.transactions ?? []) {
|
|
325
|
+
const normalized = this.normalizeTransaction(transaction, positionsByConid);
|
|
326
|
+
const time = new Date(normalized.time).getTime();
|
|
327
|
+
if (time < startDate.getTime() || time > endDate.getTime())
|
|
328
|
+
continue;
|
|
329
|
+
transactionsByKey.set(this.transactionKey(normalized), normalized);
|
|
330
|
+
}
|
|
331
|
+
}
|
|
332
|
+
return [{ accountNumber: accountId, transactions: [...transactionsByKey.values()] }];
|
|
333
|
+
}
|
|
334
|
+
async fetchOrders(options) {
|
|
335
|
+
const accountId = await this.getAccountId();
|
|
336
|
+
await this.prepareBrokerageAccount(accountId);
|
|
337
|
+
const params = {};
|
|
338
|
+
if (options.status && options.status.toUpperCase() !== "WORKING") {
|
|
339
|
+
params["filters"] = this.ibkrStatusFilter(options.status);
|
|
340
|
+
}
|
|
341
|
+
const response = await this.req({
|
|
342
|
+
path: "iserver/account/orders",
|
|
343
|
+
params,
|
|
344
|
+
});
|
|
345
|
+
let orders = (response.orders ?? [])
|
|
346
|
+
.filter((order) => this.orderBelongsToAccount(order, accountId))
|
|
347
|
+
.map((order) => this.normalizeOrder(order))
|
|
348
|
+
.filter((order) => this.orderMatchesStatus(order, options.status))
|
|
349
|
+
.filter((order) => this.orderInDateRange(order, options.fromEnteredTime, options.toEnteredTime))
|
|
350
|
+
.sort((left, right) => this.orderTimeMs(right) - this.orderTimeMs(left));
|
|
351
|
+
if (options.maxResults !== undefined)
|
|
352
|
+
orders = orders.slice(0, options.maxResults);
|
|
353
|
+
return [{ accountNumber: accountId, orders }];
|
|
354
|
+
}
|
|
355
|
+
async prepareBrokerageAccount(accountId) {
|
|
356
|
+
const brokerageAccounts = await this.req({
|
|
357
|
+
path: "iserver/accounts",
|
|
358
|
+
});
|
|
359
|
+
if (brokerageAccounts.selectedAccount === accountId)
|
|
360
|
+
return;
|
|
361
|
+
if (brokerageAccounts.accounts && !brokerageAccounts.accounts.includes(accountId)) {
|
|
362
|
+
throw new Error(`IBKR account ${accountId} is not available for trading/order queries.`);
|
|
363
|
+
}
|
|
364
|
+
await this.req({
|
|
365
|
+
path: "iserver/account",
|
|
366
|
+
method: "POST",
|
|
367
|
+
data: { acctId: accountId },
|
|
368
|
+
});
|
|
369
|
+
}
|
|
277
370
|
normalizeStockListing(symbol, listing) {
|
|
278
371
|
const assetType = listing.assetClass === "STK" ? "EQUITY" : listing.assetClass;
|
|
279
372
|
const contracts = listing.contracts ?? [];
|
|
@@ -561,6 +654,240 @@ export class IbkrClient {
|
|
|
561
654
|
return undefined;
|
|
562
655
|
}
|
|
563
656
|
}
|
|
657
|
+
normalizeTransaction(transaction, positionsByConid) {
|
|
658
|
+
const conid = transaction.conid;
|
|
659
|
+
const position = conid === undefined ? undefined : positionsByConid.get(conid);
|
|
660
|
+
const assetType = position?.assetClass === undefined
|
|
661
|
+
? undefined
|
|
662
|
+
: (ASSET_CLASS_LABELS[position.assetClass] ?? position.assetClass);
|
|
663
|
+
const symbol = position?.contractDesc ?? (conid === undefined ? undefined : String(conid));
|
|
664
|
+
const description = transaction.desc ?? symbol;
|
|
665
|
+
const time = this.parseTransactionTime(transaction)?.toISOString() ?? "";
|
|
666
|
+
const type = (transaction.type ?? "TRANSACTION").toUpperCase();
|
|
667
|
+
const transferItem = {
|
|
668
|
+
instrument: {
|
|
669
|
+
...(assetType === undefined ? {} : { assetType }),
|
|
670
|
+
...(symbol === undefined ? {} : { symbol }),
|
|
671
|
+
...(description === undefined ? {} : { description }),
|
|
672
|
+
},
|
|
673
|
+
...(transaction.qty === undefined ? {} : { amount: transaction.qty }),
|
|
674
|
+
...(transaction.pr === undefined ? {} : { cost: transaction.pr }),
|
|
675
|
+
transferItemType: type,
|
|
676
|
+
};
|
|
677
|
+
const activityId = [
|
|
678
|
+
conid === undefined ? "unknown" : String(conid),
|
|
679
|
+
time,
|
|
680
|
+
transaction.qty === undefined ? "" : String(transaction.qty),
|
|
681
|
+
transaction.amt === undefined ? "" : String(transaction.amt),
|
|
682
|
+
].join(":");
|
|
683
|
+
return {
|
|
684
|
+
activityId,
|
|
685
|
+
time,
|
|
686
|
+
type,
|
|
687
|
+
status: "VALID",
|
|
688
|
+
...(transaction.acctid === undefined ? {} : { subAccount: transaction.acctid }),
|
|
689
|
+
...(description === undefined ? {} : { description }),
|
|
690
|
+
netAmount: toNumber(transaction.amt),
|
|
691
|
+
transferItems: [transferItem],
|
|
692
|
+
};
|
|
693
|
+
}
|
|
694
|
+
normalizeOrder(order) {
|
|
695
|
+
const description = order.orderDescriptionWithContract ??
|
|
696
|
+
order.order_description_with_contract ??
|
|
697
|
+
order.orderDesc ??
|
|
698
|
+
order.orderDescription ??
|
|
699
|
+
order.order_description;
|
|
700
|
+
const symbol = order.description1 ??
|
|
701
|
+
order.contract_description_1 ??
|
|
702
|
+
order.contractDescription1 ??
|
|
703
|
+
order.symbol ??
|
|
704
|
+
order.ticker;
|
|
705
|
+
const quantity = this.firstPositiveNumber(order.total_size, order.totalSize, order.size) ??
|
|
706
|
+
this.quantityFromDescription(description);
|
|
707
|
+
const filledQuantity = this.firstNumber(order.cum_fill, order.cumFill, order.filledQuantity) ??
|
|
708
|
+
this.filledQuantityFromSizeAndFills(order.size_and_fills ?? order.sizeAndFills);
|
|
709
|
+
const remainingQuantity = order.remainingQuantity !== undefined
|
|
710
|
+
? toNumber(order.remainingQuantity)
|
|
711
|
+
: quantity !== undefined && filledQuantity !== undefined
|
|
712
|
+
? Math.max(0, quantity - filledQuantity)
|
|
713
|
+
: undefined;
|
|
714
|
+
const status = this.normalizeOrderStatus(order.order_status ?? order.orderStatus ?? order.status);
|
|
715
|
+
const price = this.firstPositiveNumber(order.limitPrice, order.price, order.avgPrice, order.average_price, order.averagePrice);
|
|
716
|
+
const stopPrice = this.firstPositiveNumber(order.stopPrice);
|
|
717
|
+
const orderId = order.order_id ?? order.orderId;
|
|
718
|
+
const enteredTime = this.parseOrderTime(order)?.toISOString();
|
|
719
|
+
const orderType = this.normalizeOrderType(order.order_type ?? order.orderType);
|
|
720
|
+
return {
|
|
721
|
+
...(orderId === undefined ? {} : { orderId }),
|
|
722
|
+
...(enteredTime === undefined ? {} : { enteredTime }),
|
|
723
|
+
...(status === undefined ? {} : { status }),
|
|
724
|
+
...(orderType === undefined ? {} : { orderType }),
|
|
725
|
+
...(quantity === undefined ? {} : { quantity }),
|
|
726
|
+
...(filledQuantity === undefined ? {} : { filledQuantity }),
|
|
727
|
+
...(remainingQuantity === undefined ? {} : { remainingQuantity }),
|
|
728
|
+
...(price === undefined ? {} : { price }),
|
|
729
|
+
...(stopPrice === undefined ? {} : { stopPrice }),
|
|
730
|
+
orderLegCollection: [this.normalizeOrderLeg(order, symbol)],
|
|
731
|
+
};
|
|
732
|
+
}
|
|
733
|
+
normalizeOrderLeg(order, symbol) {
|
|
734
|
+
const fallbackSymbol = symbol ?? (order.conid === undefined ? undefined : String(order.conid));
|
|
735
|
+
const instruction = this.normalizeOrderSide(order.side);
|
|
736
|
+
return {
|
|
737
|
+
...(instruction === undefined ? {} : { instruction }),
|
|
738
|
+
instrument: { ...(fallbackSymbol === undefined ? {} : { symbol: fallbackSymbol }) },
|
|
739
|
+
};
|
|
740
|
+
}
|
|
741
|
+
normalizeOrderStatus(status) {
|
|
742
|
+
if (!status)
|
|
743
|
+
return undefined;
|
|
744
|
+
const normalized = status
|
|
745
|
+
.replace(/([a-z])([A-Z])/g, "$1_$2")
|
|
746
|
+
.replace(/\s+/g, "_")
|
|
747
|
+
.toUpperCase();
|
|
748
|
+
return normalized === "CANCELLED" ? "CANCELED" : normalized;
|
|
749
|
+
}
|
|
750
|
+
normalizeOrderType(type) {
|
|
751
|
+
if (!type)
|
|
752
|
+
return undefined;
|
|
753
|
+
if (type === "MKT")
|
|
754
|
+
return "MARKET";
|
|
755
|
+
if (type === "LMT")
|
|
756
|
+
return "LIMIT";
|
|
757
|
+
if (type === "STP")
|
|
758
|
+
return "STOP";
|
|
759
|
+
return type.replace(/\s+/g, "_").toUpperCase();
|
|
760
|
+
}
|
|
761
|
+
normalizeOrderSide(side) {
|
|
762
|
+
if (!side)
|
|
763
|
+
return undefined;
|
|
764
|
+
const upper = side.toUpperCase();
|
|
765
|
+
if (upper === "B" || upper === "BUY")
|
|
766
|
+
return "BUY";
|
|
767
|
+
if (upper === "S" || upper === "SELL")
|
|
768
|
+
return "SELL";
|
|
769
|
+
return upper;
|
|
770
|
+
}
|
|
771
|
+
ibkrStatusFilter(status) {
|
|
772
|
+
const normalized = status.toUpperCase();
|
|
773
|
+
return IBKR_STATUS_FILTERS[normalized] ?? normalized.toLowerCase();
|
|
774
|
+
}
|
|
775
|
+
orderMatchesStatus(order, requestedStatus) {
|
|
776
|
+
if (!requestedStatus)
|
|
777
|
+
return true;
|
|
778
|
+
const normalizedStatus = this.normalizeOrderStatus(requestedStatus);
|
|
779
|
+
if (normalizedStatus === "WORKING") {
|
|
780
|
+
return order.status !== undefined && IBKR_WORKING_STATUSES.has(order.status);
|
|
781
|
+
}
|
|
782
|
+
return order.status === normalizedStatus;
|
|
783
|
+
}
|
|
784
|
+
orderBelongsToAccount(order, accountId) {
|
|
785
|
+
const account = order.account ?? order.acct;
|
|
786
|
+
return account === undefined || account === accountId;
|
|
787
|
+
}
|
|
788
|
+
orderInDateRange(order, fromDate, toDate) {
|
|
789
|
+
const timeMs = this.orderTimeMs(order);
|
|
790
|
+
return !Number.isFinite(timeMs) || (timeMs >= fromDate.getTime() && timeMs <= toDate.getTime());
|
|
791
|
+
}
|
|
792
|
+
orderTimeMs(order) {
|
|
793
|
+
const parsed = order.enteredTime ? new Date(order.enteredTime).getTime() : Number.NaN;
|
|
794
|
+
return Number.isNaN(parsed) ? Number.NEGATIVE_INFINITY : parsed;
|
|
795
|
+
}
|
|
796
|
+
parseOrderTime(order) {
|
|
797
|
+
if (order.lastExecutionTime_r !== undefined) {
|
|
798
|
+
const parsed = new Date(order.lastExecutionTime_r);
|
|
799
|
+
if (!Number.isNaN(parsed.getTime()))
|
|
800
|
+
return parsed;
|
|
801
|
+
}
|
|
802
|
+
const value = order.order_time ?? order.orderTime ?? order.lastExecutionTime;
|
|
803
|
+
if (!value)
|
|
804
|
+
return undefined;
|
|
805
|
+
const compact = /^(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})(\d{2})$/.exec(value);
|
|
806
|
+
if (compact) {
|
|
807
|
+
const [, year, month, day, hour, minute, second] = compact;
|
|
808
|
+
return new Date(Date.UTC(2000 + Number(year), Number(month) - 1, Number(day), Number(hour), Number(minute), Number(second)));
|
|
809
|
+
}
|
|
810
|
+
const parsed = new Date(value);
|
|
811
|
+
return Number.isNaN(parsed.getTime()) ? undefined : parsed;
|
|
812
|
+
}
|
|
813
|
+
firstNumber(...values) {
|
|
814
|
+
for (const value of values) {
|
|
815
|
+
if (value === undefined)
|
|
816
|
+
continue;
|
|
817
|
+
const numeric = Number(value);
|
|
818
|
+
if (Number.isFinite(numeric))
|
|
819
|
+
return numeric;
|
|
820
|
+
}
|
|
821
|
+
return undefined;
|
|
822
|
+
}
|
|
823
|
+
firstPositiveNumber(...values) {
|
|
824
|
+
for (const value of values) {
|
|
825
|
+
const numeric = this.firstNumber(value);
|
|
826
|
+
if (numeric !== undefined && numeric > 0)
|
|
827
|
+
return numeric;
|
|
828
|
+
}
|
|
829
|
+
return undefined;
|
|
830
|
+
}
|
|
831
|
+
quantityFromDescription(description) {
|
|
832
|
+
const quantity = description
|
|
833
|
+
? /\b(?:Bought|Sold|Buy|Sell)\s+(?<quantity>[\d.]+)/i.exec(description)?.groups?.["quantity"]
|
|
834
|
+
: undefined;
|
|
835
|
+
if (!quantity)
|
|
836
|
+
return undefined;
|
|
837
|
+
const parsed = Number(quantity);
|
|
838
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
839
|
+
}
|
|
840
|
+
filledQuantityFromSizeAndFills(value) {
|
|
841
|
+
const quantity = value ? /(?<quantity>[\d.]+)/.exec(value)?.groups?.["quantity"] : undefined;
|
|
842
|
+
if (!quantity)
|
|
843
|
+
return undefined;
|
|
844
|
+
const parsed = Number(quantity);
|
|
845
|
+
return Number.isFinite(parsed) ? parsed : undefined;
|
|
846
|
+
}
|
|
847
|
+
parseTransactionTime(transaction) {
|
|
848
|
+
if (transaction.rawDate && /^\d{8}$/.test(transaction.rawDate)) {
|
|
849
|
+
const year = transaction.rawDate.slice(0, 4);
|
|
850
|
+
const month = transaction.rawDate.slice(4, 6);
|
|
851
|
+
const day = transaction.rawDate.slice(6, 8);
|
|
852
|
+
return new Date(`${year}-${month}-${day}T00:00:00`);
|
|
853
|
+
}
|
|
854
|
+
const value = transaction.date;
|
|
855
|
+
if (!value)
|
|
856
|
+
return undefined;
|
|
857
|
+
const parsed = new Date(value);
|
|
858
|
+
if (!Number.isNaN(parsed.getTime()))
|
|
859
|
+
return parsed;
|
|
860
|
+
const match = /^(?:\w{3}) (?<month>\w{3}) (?<day>\d{1,2}) (?<time>\d{2}:\d{2}:\d{2}) (?<zone>\w{3}) (?<year>\d{4})$/.exec(value);
|
|
861
|
+
if (!match?.groups)
|
|
862
|
+
return undefined;
|
|
863
|
+
const zoneOffsets = {
|
|
864
|
+
EST: "-05:00",
|
|
865
|
+
EDT: "-04:00",
|
|
866
|
+
CST: "-06:00",
|
|
867
|
+
CDT: "-05:00",
|
|
868
|
+
MST: "-07:00",
|
|
869
|
+
MDT: "-06:00",
|
|
870
|
+
PST: "-08:00",
|
|
871
|
+
PDT: "-07:00",
|
|
872
|
+
UTC: "Z",
|
|
873
|
+
GMT: "Z",
|
|
874
|
+
};
|
|
875
|
+
const { month, day, time, zone, year } = match.groups;
|
|
876
|
+
if (!month || !day || !time || !zone || !year)
|
|
877
|
+
return undefined;
|
|
878
|
+
const normalized = `${day.padStart(2, "0")} ${month} ${year} ${time} ${zoneOffsets[zone] ?? "Z"}`;
|
|
879
|
+
const fallback = new Date(normalized);
|
|
880
|
+
return Number.isNaN(fallback.getTime()) ? undefined : fallback;
|
|
881
|
+
}
|
|
882
|
+
transactionKey(transaction) {
|
|
883
|
+
return [
|
|
884
|
+
transaction.activityId,
|
|
885
|
+
transaction.time,
|
|
886
|
+
transaction.type,
|
|
887
|
+
transaction.netAmount,
|
|
888
|
+
transaction.transferItems?.[0]?.amount ?? "",
|
|
889
|
+
].join(":");
|
|
890
|
+
}
|
|
564
891
|
/** Overridable in request-level tests so snapshot warm-up does not sleep. */
|
|
565
892
|
wait(ms) {
|
|
566
893
|
return sleep(ms);
|