@pungoyal/kite-cli 0.2.0 → 0.3.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 +81 -6
- package/dist/commands/alerts.d.ts +28 -0
- package/dist/commands/alerts.js +257 -76
- package/dist/commands/margins.d.ts +34 -0
- package/dist/commands/margins.js +258 -0
- package/dist/commands/mf.d.ts +10 -0
- package/dist/commands/mf.js +85 -0
- package/dist/core/schemas.d.ts +3 -0
- package/dist/run.js +5 -0
- package/package.json +7 -2
package/README.md
CHANGED
|
@@ -4,7 +4,8 @@
|
|
|
4
4
|
[](https://www.npmjs.com/package/@pungoyal/kite-cli)
|
|
5
5
|
[](https://github.com/pungoyal/kite-cli/releases/latest)
|
|
6
6
|
[](https://nodejs.org)
|
|
7
|
-
[](LICENSE)
|
|
7
|
+
[](https://github.com/pungoyal/kite-cli/blob/main/LICENSE)
|
|
8
|
+
[](https://pungoyal.github.io/kite-cli/)
|
|
8
9
|
|
|
9
10
|
An **unofficial**, secure, scriptable command-line interface for the [Zerodha Kite Connect](https://kite.trade/docs/connect/v3/) API.
|
|
10
11
|
|
|
@@ -62,6 +63,8 @@ kite --env sandbox holdings
|
|
|
62
63
|
|
|
63
64
|
## Usage
|
|
64
65
|
|
|
66
|
+
The highlights are below; every command's full flag list is in [the command reference](https://pungoyal.github.io/kite-cli/commands).
|
|
67
|
+
|
|
65
68
|
### Portfolio
|
|
66
69
|
|
|
67
70
|
```bash
|
|
@@ -75,6 +78,19 @@ kite authorise # authorise demat holdings for selling
|
|
|
75
78
|
|
|
76
79
|
If a sell order fails with exit code 12 ("needs authorisation at depository"), run `kite authorise` — it requests a CDSL authorisation and opens the browser page that completes it. Pass specific ISINs to authorise only those instruments.
|
|
77
80
|
|
|
81
|
+
### Mutual funds
|
|
82
|
+
|
|
83
|
+
```bash
|
|
84
|
+
kite mf holdings # your MF holdings with P&L
|
|
85
|
+
kite mf orders # MF orders from the last 7 days
|
|
86
|
+
kite mf sips # your active SIPs
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Mutual funds are read-only over Kite Connect — placing MF orders and managing
|
|
90
|
+
SIPs is not available via the API (a purchase needs a bank debit the API can't
|
|
91
|
+
authorise). `mf orders` only reaches back 7 days, so an empty list doesn't mean
|
|
92
|
+
you have no MF history.
|
|
93
|
+
|
|
78
94
|
### Market data
|
|
79
95
|
|
|
80
96
|
```bash
|
|
@@ -157,12 +173,48 @@ kite alerts create NSE:INFY --operator below --value 1400 \
|
|
|
157
173
|
--type ato --side BUY --quantity 10 --order-type LIMIT --price 1400
|
|
158
174
|
```
|
|
159
175
|
|
|
176
|
+
The order an ATO fires need not be on the instrument you watch, and it can be a
|
|
177
|
+
**basket of several orders** across different instruments. Use `--order` — one
|
|
178
|
+
per leg, repeatable — where each leg is
|
|
179
|
+
`EXCHANGE:SYMBOL:SIDE:QTY` followed by optional attributes (an order type,
|
|
180
|
+
product, validity, a bare price, or `trigger=<n>`), in any order:
|
|
181
|
+
|
|
182
|
+
```bash
|
|
183
|
+
# Watch INDIGO's spot price; when it drops, buy the future and trim a hedge.
|
|
184
|
+
kite alerts create NSE:INDIGO --operator below --value 3850 --type ato \
|
|
185
|
+
--order 'NFO:INDIGO25AUGFUT:BUY:150:MARKET:NRML' \
|
|
186
|
+
--order 'NSE:RELIANCE:SELL:10:LIMIT:2900'
|
|
187
|
+
```
|
|
188
|
+
|
|
189
|
+
The value cap sums every leg and fails closed if any one cannot be priced. The
|
|
190
|
+
`--order` form and the single-order flags above (`--side`/`--quantity`/…) are
|
|
191
|
+
mutually exclusive.
|
|
192
|
+
|
|
160
193
|
`--operator` accepts the raw symbols (`>=`, `<=`, `>`, `<`, `==`) or the aliases
|
|
161
194
|
`above`, `below`, `ge`, `le`, `gt`, `lt`, `eq`. Compare against another instrument
|
|
162
195
|
instead of a constant with `--rhs-instrument EXCHANGE:SYMBOL`.
|
|
163
196
|
|
|
164
197
|
Add `--dry-run` to any of these to see exactly what would be sent, without sending it.
|
|
165
198
|
|
|
199
|
+
### Margins & charges
|
|
200
|
+
|
|
201
|
+
Work out what an order or a basket would cost before placing it. Nothing is
|
|
202
|
+
sent to the exchange — these only call Kite's calculators:
|
|
203
|
+
|
|
204
|
+
```bash
|
|
205
|
+
kite margins order NFO:NIFTY25AUGFUT:BUY:75:NRML # required margin, per order
|
|
206
|
+
kite margins basket NFO:NIFTY25AUGFUT:BUY:75:NRML \
|
|
207
|
+
NFO:NIFTY25AUGFUT:SELL:75:NRML # net margin, with hedge benefit
|
|
208
|
+
kite margins charges NSE:INFY:BUY:10:1500 # brokerage + taxes (contract note)
|
|
209
|
+
```
|
|
210
|
+
|
|
211
|
+
Each order is `EXCHANGE:SYMBOL:SIDE:QTY` followed by optional attributes (an
|
|
212
|
+
order type, product, variety, a bare price, or `trigger=<n>`), in any order;
|
|
213
|
+
product defaults to CNC and variety to regular. `margins basket` accepts
|
|
214
|
+
`--no-consider-positions` to ignore your existing positions when netting.
|
|
215
|
+
`margins charges` needs a real price (the execution price), since charges are a
|
|
216
|
+
percentage of quantity × price.
|
|
217
|
+
|
|
166
218
|
### Scripting
|
|
167
219
|
|
|
168
220
|
Every command supports `--json`, writes data to stdout and everything else to stderr, and returns a meaningful exit code.
|
|
@@ -246,9 +298,11 @@ For scripts and CI, `KITE_API_KEY` / `KITE_API_SECRET` / `KITE_ACCESS_TOKEN` sti
|
|
|
246
298
|
supply credentials directly. As a safeguard, naming a profile explicitly while those
|
|
247
299
|
are set is refused rather than silently overridden.
|
|
248
300
|
|
|
301
|
+
→ Full reference, including credential storage precedence and per-profile inheritance: [the configuration reference](https://pungoyal.github.io/kite-cli/configuration).
|
|
302
|
+
|
|
249
303
|
## Safety
|
|
250
304
|
|
|
251
|
-
This tool spends real money, so the defaults are conservative.
|
|
305
|
+
This tool spends real money, so the defaults are conservative. → Full model, including the order-tag reconciliation flow and why cancels/converts are exempt from the value cap: [the safety model](https://pungoyal.github.io/kite-cli/safety).
|
|
252
306
|
|
|
253
307
|
**Confirmation.** Order commands render the resolved order — the actual instrument token, lot size, and computed value, not an echo of your flags — and wait for confirmation. That's deliberate: a flag echo can't catch "I typed the wrong symbol and it resolved to a different contract," which is the expensive mistake.
|
|
254
308
|
|
|
@@ -287,13 +341,13 @@ Automatic retries are restricted to `GET`/`HEAD` at the transport layer. `POST`,
|
|
|
287
341
|
|
|
288
342
|
Your API secret is never accepted as a command-line argument, because argv is visible to any local process via `ps` and lands in shell history. It's prompted for, or read from the environment.
|
|
289
343
|
|
|
290
|
-
**Redaction.** Access tokens are registered with a scrubber that runs over every log line, error message, and stack trace. The two paths that carry a token — the `Authorization` header and the WebSocket URL, where it's a query parameter — are covered explicitly and [tested](test/redact.test.ts).
|
|
344
|
+
**Redaction.** Access tokens are registered with a scrubber that runs over every log line, error message, and stack trace. The two paths that carry a token — the `Authorization` header and the WebSocket URL, where it's a query parameter — are covered explicitly and [tested](https://github.com/pungoyal/kite-cli/blob/main/test/redact.test.ts).
|
|
291
345
|
|
|
292
346
|
**TOTP.** This CLI will never ask for or store your 2FA seed. Storing it next to your API secret would collapse both authentication factors into one, which is exactly what the SEBI 2FA mandate exists to prevent. Login happens in your browser; the CLI only sees the resulting request token.
|
|
293
347
|
|
|
294
348
|
**Supply chain.** 10 direct runtime dependencies, most of them zero-dependency. Published from GitHub Actions via [npm Trusted Publishing](https://docs.npmjs.com/trusted-publishers/) with OIDC — no long-lived publish token exists. Provenance attestation is generated automatically; verify it with `npm audit signatures`. All CI actions are pinned to full commit SHAs, and dependency lifecycle scripts are disabled.
|
|
295
349
|
|
|
296
|
-
To report a vulnerability, see [SECURITY.md](SECURITY.md).
|
|
350
|
+
To report a vulnerability, see [SECURITY.md](https://github.com/pungoyal/kite-cli/blob/main/SECURITY.md).
|
|
297
351
|
|
|
298
352
|
## Configuration
|
|
299
353
|
|
|
@@ -319,6 +373,8 @@ Config lives at `~/.config/kite/config.json` (`0600`). Override the location wit
|
|
|
319
373
|
(see [Multiple accounts](#multiple-accounts)); the remaining keys are global. The
|
|
320
374
|
account this invocation resolves to is selected by `--profile` / `KITE_PROFILE`.
|
|
321
375
|
|
|
376
|
+
→ Every key and environment variable, with precedence: [the configuration reference](https://pungoyal.github.io/kite-cli/configuration).
|
|
377
|
+
|
|
322
378
|
## Things worth knowing about Kite
|
|
323
379
|
|
|
324
380
|
- **Sessions expire at 6:00 AM IST daily.** This is a regulatory requirement and there's no way around it — you log in once per trading day. Refresh tokens exist in the API but are only issued to exchange-approved platforms, not individual subscribers.
|
|
@@ -329,6 +385,25 @@ account this invocation resolves to is selected by `--profile` / `KITE_PROFILE`.
|
|
|
329
385
|
- **Instruments are cached by `exchange:tradingsymbol`, never by token.** Exchanges reuse numeric instrument tokens after expiry, so a token-keyed cache silently resolves to the wrong contract after a rollover.
|
|
330
386
|
- **Mutual funds are read-only** over the API — placing MF orders requires a bank debit that has no API path.
|
|
331
387
|
|
|
388
|
+
→ Hit one of these? See [the troubleshooting guide](https://pungoyal.github.io/kite-cli/troubleshooting) for the symptom-first version.
|
|
389
|
+
|
|
390
|
+
## Documentation
|
|
391
|
+
|
|
392
|
+
This README covers installation and everyday usage. The full reference is a
|
|
393
|
+
searchable site at **[pungoyal.github.io/kite-cli](https://pungoyal.github.io/kite-cli/)**:
|
|
394
|
+
|
|
395
|
+
- [Safety model](https://pungoyal.github.io/kite-cli/safety) — the full layered safety model (kill
|
|
396
|
+
switch, value cap, confirmation escalation, order-tag reconciliation).
|
|
397
|
+
- [Configuration](https://pungoyal.github.io/kite-cli/configuration) — every config key and
|
|
398
|
+
environment variable, with precedence.
|
|
399
|
+
- [Troubleshooting](https://pungoyal.github.io/kite-cli/troubleshooting) — symptom-first fixes
|
|
400
|
+
for session expiry, rate limits, login issues, and more.
|
|
401
|
+
- [Command reference](https://pungoyal.github.io/kite-cli/commands) — full flag-by-flag reference for
|
|
402
|
+
every command, generated from `--help`.
|
|
403
|
+
- [Library API](https://pungoyal.github.io/kite-cli/api) — the library/programmatic API surface.
|
|
404
|
+
|
|
405
|
+
The same pages are browsable as Markdown in [`docs/`](https://github.com/pungoyal/kite-cli/tree/main/docs).
|
|
406
|
+
|
|
332
407
|
## Library use
|
|
333
408
|
|
|
334
409
|
The client is exported if you want it without the CLI:
|
|
@@ -360,7 +435,7 @@ npm run build
|
|
|
360
435
|
|
|
361
436
|
## Contributing
|
|
362
437
|
|
|
363
|
-
Bug reports, ideas, and pull requests are welcome. See [CONTRIBUTING.md](CONTRIBUTING.md) for the development workflow and the two non-negotiables (safety defaults and secret redaction), and [CHANGELOG.md](CHANGELOG.md) for release history. Security issues go through [SECURITY.md](SECURITY.md), never a public issue.
|
|
438
|
+
Bug reports, ideas, and pull requests are welcome. See [CONTRIBUTING.md](https://github.com/pungoyal/kite-cli/blob/main/CONTRIBUTING.md) for the development workflow and the two non-negotiables (safety defaults and secret redaction), and [CHANGELOG.md](https://github.com/pungoyal/kite-cli/blob/main/CHANGELOG.md) for release history. Security issues go through [SECURITY.md](https://github.com/pungoyal/kite-cli/blob/main/SECURITY.md), never a public issue.
|
|
364
439
|
|
|
365
440
|
## Disclaimer
|
|
366
441
|
|
|
@@ -370,4 +445,4 @@ Trading involves risk of financial loss. This software is provided as-is under t
|
|
|
370
445
|
|
|
371
446
|
## Licence
|
|
372
447
|
|
|
373
|
-
[MIT](LICENSE)
|
|
448
|
+
[MIT](https://github.com/pungoyal/kite-cli/blob/main/LICENSE)
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type OrderType, type Product, type TransactionType, type Validity } from '../core/api.js';
|
|
1
2
|
import type { CommandFactory } from './types.js';
|
|
2
3
|
/**
|
|
3
4
|
* Price alerts.
|
|
@@ -10,3 +11,30 @@ import type { CommandFactory } from './types.js';
|
|
|
10
11
|
* disguise, so it gets the same safety treatment as `orders place`.
|
|
11
12
|
*/
|
|
12
13
|
export declare const alertCommands: CommandFactory;
|
|
14
|
+
/** One order in an ATO basket, fully resolved and validated. */
|
|
15
|
+
export interface AtoLeg {
|
|
16
|
+
exchange: string;
|
|
17
|
+
tradingsymbol: string;
|
|
18
|
+
side: TransactionType;
|
|
19
|
+
quantity: number;
|
|
20
|
+
orderType: OrderType;
|
|
21
|
+
price: number | undefined;
|
|
22
|
+
triggerPrice: number | undefined;
|
|
23
|
+
product: Product;
|
|
24
|
+
validity: Validity;
|
|
25
|
+
}
|
|
26
|
+
/**
|
|
27
|
+
* Parse one `--order` spec into a leg.
|
|
28
|
+
*
|
|
29
|
+
* Grammar: `EXCHANGE:SYMBOL:SIDE:QTY` followed by any number of optional
|
|
30
|
+
* attribute tokens. Each attribute is either a bare vocabulary word (an order
|
|
31
|
+
* type, a product, a validity, or a number read as the price) or an explicit
|
|
32
|
+
* `key=value` (type, product, validity, price, trigger).
|
|
33
|
+
*
|
|
34
|
+
* Fails closed (invariant #1): every trailing token must be classified exactly
|
|
35
|
+
* once. An unrecognised token, a duplicated field, or an empty field rejects the
|
|
36
|
+
* whole spec — a silently mis-parsed leg is a real order with the wrong
|
|
37
|
+
* parameters. A trigger price is only ever set explicitly (`trigger=<n>`), never
|
|
38
|
+
* positionally, so an SL order can't be misread from two bare numbers.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseOrderSpec(spec: string): AtoLeg;
|
package/dist/commands/alerts.js
CHANGED
|
@@ -35,14 +35,24 @@ export const alertCommands = (program, run) => {
|
|
|
35
35
|
.option('--name <name>', 'Alert name (defaults to a description of the condition)')
|
|
36
36
|
.option('--attribute <attribute>', 'Attribute to compare', ALERT_DEFAULT_ATTRIBUTE)
|
|
37
37
|
.option('--type <type>', 'simple or ato (ato places an order when it fires)', 'simple')
|
|
38
|
-
// ATO
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
.option('--
|
|
45
|
-
|
|
38
|
+
// ATO basket. Two shapes, never mixed:
|
|
39
|
+
// --order — a full basket leg, repeatable, each on its own instrument
|
|
40
|
+
// (independent of the watched one). This is how you place
|
|
41
|
+
// orders on a different symbol, or several at once.
|
|
42
|
+
// the flags below — a shorthand for a single order on the *watched*
|
|
43
|
+
// instrument, kept for back-compat.
|
|
44
|
+
.option('--order <spec>', 'ATO: a basket leg as EXCHANGE:SYMBOL:SIDE:QTY[:TYPE][:PRICE][:PRODUCT][:VALIDITY][:trigger=<n>]. Repeatable.', collect, [])
|
|
45
|
+
// ATO single-order flags — a shorthand for one order on the watched
|
|
46
|
+
// instrument. Read only when --type ato and no --order is given; combining
|
|
47
|
+
// them with --order is a hard error, so they carry NO defaults here (a
|
|
48
|
+
// default would be invisible to that guard and silently ignored).
|
|
49
|
+
.option('-s, --side <side>', 'ATO: order side, BUY or SELL (single-order form)')
|
|
50
|
+
.option('-q, --quantity <n>', 'ATO: order quantity (single-order form)')
|
|
51
|
+
.option('--order-type <type>', `ATO: order type, default MARKET (${ORDER_TYPES.join(', ')}) (single-order form)`)
|
|
52
|
+
.option('-p, --price <price>', 'ATO: limit price (single-order form; for LIMIT/SL)')
|
|
53
|
+
.option('--trigger-price <price>', 'ATO: trigger price (single-order form; for SL/SL-M)')
|
|
54
|
+
.option('--product <product>', `ATO: product, default CNC (${PRODUCTS.join(', ')}) (single-order form)`)
|
|
55
|
+
.option('--validity <validity>', `ATO: validity, default DAY (${VALIDITIES.join(', ')}) (single-order form)`)
|
|
46
56
|
.action(run(createAlert));
|
|
47
57
|
alerts
|
|
48
58
|
.command('modify')
|
|
@@ -183,13 +193,16 @@ const CreateOptionsSchema = z.object({
|
|
|
183
193
|
name: z.string().optional(),
|
|
184
194
|
attribute: z.string().default(ALERT_DEFAULT_ATTRIBUTE),
|
|
185
195
|
type: z.string().default('simple'),
|
|
196
|
+
order: z.array(z.string()).default([]),
|
|
186
197
|
side: z.string().optional(),
|
|
187
198
|
quantity: z.coerce.number().int().positive().optional(),
|
|
188
|
-
|
|
199
|
+
// No defaults: these are single-order-form flags, and defaulting them would
|
|
200
|
+
// make them undetectable to the guard that forbids mixing them with --order.
|
|
201
|
+
orderType: z.string().optional(),
|
|
189
202
|
price: z.coerce.number().positive().optional(),
|
|
190
203
|
triggerPrice: z.coerce.number().positive().optional(),
|
|
191
|
-
product: z.string().
|
|
192
|
-
validity: z.string().
|
|
204
|
+
product: z.string().optional(),
|
|
205
|
+
validity: z.string().optional(),
|
|
193
206
|
});
|
|
194
207
|
async function createAlert(ctx, rawOpts, command) {
|
|
195
208
|
ctx.requireSession();
|
|
@@ -244,7 +257,8 @@ async function createAlert(ctx, rawOpts, command) {
|
|
|
244
257
|
// ATO creation IS order placement: it needs the kill switch, the value cap,
|
|
245
258
|
// and the full escalating confirmation, exactly like `orders place`.
|
|
246
259
|
assertTradingEnabled(ctx);
|
|
247
|
-
const
|
|
260
|
+
const legs = resolveAtoLegs(opts, lhs);
|
|
261
|
+
const { basket, notionalValue, orderDetails } = await buildAtoBasket(ctx, legs);
|
|
248
262
|
params.basket = basket;
|
|
249
263
|
await confirmAction(ctx, {
|
|
250
264
|
action: `Create ATO alert on ${lhsKey}`,
|
|
@@ -276,24 +290,40 @@ async function createAlert(ctx, rawOpts, command) {
|
|
|
276
290
|
}
|
|
277
291
|
}
|
|
278
292
|
/**
|
|
279
|
-
*
|
|
280
|
-
*
|
|
281
|
-
*
|
|
282
|
-
*
|
|
283
|
-
*
|
|
284
|
-
* unknown value as small.
|
|
293
|
+
* Resolve the ATO basket's legs from the parsed options. The `--order` form
|
|
294
|
+
* (one leg per flag, each on its own instrument) and the single-order flag form
|
|
295
|
+
* are mutually exclusive: mixing them is a hard error rather than a silent
|
|
296
|
+
* precedence rule, because the two describe different orders and a guessed
|
|
297
|
+
* winner would place the wrong one.
|
|
285
298
|
*/
|
|
286
|
-
|
|
299
|
+
function resolveAtoLegs(opts, lhs) {
|
|
300
|
+
if (opts.order.length > 0) {
|
|
301
|
+
const usedSingleFlags = opts.side !== undefined ||
|
|
302
|
+
opts.quantity !== undefined ||
|
|
303
|
+
opts.price !== undefined ||
|
|
304
|
+
opts.triggerPrice !== undefined ||
|
|
305
|
+
opts.orderType !== undefined ||
|
|
306
|
+
opts.product !== undefined ||
|
|
307
|
+
opts.validity !== undefined;
|
|
308
|
+
if (usedSingleFlags) {
|
|
309
|
+
throw new UsageError('Use either --order (repeatable) or the single-order flags (--side/--quantity/--price/...), not both.');
|
|
310
|
+
}
|
|
311
|
+
return opts.order.map(parseOrderSpec);
|
|
312
|
+
}
|
|
313
|
+
return [legFromFlags(opts, lhs)];
|
|
314
|
+
}
|
|
315
|
+
/** Build a single leg on the *watched* instrument from the shorthand flags. */
|
|
316
|
+
function legFromFlags(opts, lhs) {
|
|
287
317
|
if (!opts.side)
|
|
288
|
-
throw new UsageError('--side (BUY or SELL) is required for an ATO alert.');
|
|
318
|
+
throw new UsageError('--side (BUY or SELL) is required for an ATO alert (or use --order).');
|
|
289
319
|
if (opts.quantity === undefined)
|
|
290
|
-
throw new UsageError('--quantity is required for an ATO alert.');
|
|
320
|
+
throw new UsageError('--quantity is required for an ATO alert (or use --order).');
|
|
291
321
|
const side = opts.side.toUpperCase();
|
|
292
322
|
if (side !== 'BUY' && side !== 'SELL')
|
|
293
323
|
throw new UsageError('--side must be BUY or SELL.');
|
|
294
|
-
const orderType = normalise(opts.orderType, ORDER_TYPES, 'order type');
|
|
295
|
-
const product = normalise(opts.product, PRODUCTS, 'product');
|
|
296
|
-
const validity = normalise(opts.validity, VALIDITIES, 'validity');
|
|
324
|
+
const orderType = normalise(opts.orderType ?? 'MARKET', ORDER_TYPES, 'order type');
|
|
325
|
+
const product = normalise(opts.product ?? 'CNC', PRODUCTS, 'product');
|
|
326
|
+
const validity = normalise(opts.validity ?? 'DAY', VALIDITIES, 'validity');
|
|
297
327
|
if ((orderType === 'LIMIT' || orderType === 'SL') && opts.price === undefined) {
|
|
298
328
|
throw new UsageError(`--price is required for a ${orderType} order.`);
|
|
299
329
|
}
|
|
@@ -303,64 +333,200 @@ async function buildAtoBasket(ctx, opts, lhs) {
|
|
|
303
333
|
if (orderType === 'MARKET' && opts.price !== undefined) {
|
|
304
334
|
throw new UsageError('--price cannot be used with a MARKET order.');
|
|
305
335
|
}
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
336
|
+
return {
|
|
337
|
+
exchange: lhs.exchange,
|
|
338
|
+
tradingsymbol: lhs.tradingsymbol,
|
|
339
|
+
side: side,
|
|
340
|
+
quantity: opts.quantity,
|
|
341
|
+
orderType,
|
|
342
|
+
price: opts.price,
|
|
343
|
+
triggerPrice: opts.triggerPrice,
|
|
344
|
+
product,
|
|
345
|
+
validity,
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
/**
|
|
349
|
+
* Parse one `--order` spec into a leg.
|
|
350
|
+
*
|
|
351
|
+
* Grammar: `EXCHANGE:SYMBOL:SIDE:QTY` followed by any number of optional
|
|
352
|
+
* attribute tokens. Each attribute is either a bare vocabulary word (an order
|
|
353
|
+
* type, a product, a validity, or a number read as the price) or an explicit
|
|
354
|
+
* `key=value` (type, product, validity, price, trigger).
|
|
355
|
+
*
|
|
356
|
+
* Fails closed (invariant #1): every trailing token must be classified exactly
|
|
357
|
+
* once. An unrecognised token, a duplicated field, or an empty field rejects the
|
|
358
|
+
* whole spec — a silently mis-parsed leg is a real order with the wrong
|
|
359
|
+
* parameters. A trigger price is only ever set explicitly (`trigger=<n>`), never
|
|
360
|
+
* positionally, so an SL order can't be misread from two bare numbers.
|
|
361
|
+
*/
|
|
362
|
+
export function parseOrderSpec(spec) {
|
|
363
|
+
const raw = spec.trim();
|
|
364
|
+
const tokens = raw.split(':').map((t) => t.trim());
|
|
365
|
+
if (tokens.length < 4) {
|
|
366
|
+
throw new UsageError(`Malformed --order "${spec}".`, 'Expected at least EXCHANGE:SYMBOL:SIDE:QTY, e.g. NFO:INDIGO25AUGFUT:BUY:150:MARKET:NRML.');
|
|
367
|
+
}
|
|
368
|
+
// Guaranteed present by the length check above.
|
|
369
|
+
const [exchangeTok, symbolTok, sideTok, qtyTok, ...rest] = tokens;
|
|
370
|
+
const { exchange, tradingsymbol } = parseInstrumentKey(`${exchangeTok}:${symbolTok}`);
|
|
371
|
+
const side = sideTok.toUpperCase();
|
|
372
|
+
if (side !== 'BUY' && side !== 'SELL') {
|
|
373
|
+
throw new UsageError(`--order side must be BUY or SELL, got "${sideTok}" in "${spec}".`);
|
|
374
|
+
}
|
|
375
|
+
const quantity = Number(qtyTok);
|
|
376
|
+
if (!Number.isInteger(quantity) || quantity <= 0) {
|
|
377
|
+
throw new UsageError(`--order quantity must be a positive integer, got "${qtyTok}" in "${spec}".`);
|
|
378
|
+
}
|
|
379
|
+
let orderType;
|
|
380
|
+
let price;
|
|
381
|
+
let triggerPrice;
|
|
382
|
+
let product;
|
|
383
|
+
let validity;
|
|
384
|
+
const setOnce = (current, next, label) => {
|
|
385
|
+
if (current !== undefined)
|
|
386
|
+
throw new UsageError(`--order "${spec}" sets ${label} more than once.`);
|
|
387
|
+
return next;
|
|
388
|
+
};
|
|
389
|
+
for (const tok of rest) {
|
|
390
|
+
if (tok === '') {
|
|
391
|
+
throw new UsageError(`--order "${spec}" has an empty field. Remove the stray ":".`);
|
|
315
392
|
}
|
|
316
|
-
|
|
317
|
-
|
|
393
|
+
const eq = tok.indexOf('=');
|
|
394
|
+
if (eq !== -1) {
|
|
395
|
+
const key = tok.slice(0, eq).trim().toLowerCase();
|
|
396
|
+
const value = tok.slice(eq + 1).trim();
|
|
397
|
+
switch (key) {
|
|
398
|
+
case 'type':
|
|
399
|
+
orderType = setOnce(orderType, normalise(value, ORDER_TYPES, 'order type'), 'the order type');
|
|
400
|
+
break;
|
|
401
|
+
case 'product':
|
|
402
|
+
product = setOnce(product, normalise(value, PRODUCTS, 'product'), 'the product');
|
|
403
|
+
break;
|
|
404
|
+
case 'validity':
|
|
405
|
+
validity = setOnce(validity, normalise(value, VALIDITIES, 'validity'), 'the validity');
|
|
406
|
+
break;
|
|
407
|
+
case 'price':
|
|
408
|
+
price = setOnce(price, parsePositive(value, 'price', spec), 'the price');
|
|
409
|
+
break;
|
|
410
|
+
case 'trigger':
|
|
411
|
+
triggerPrice = setOnce(triggerPrice, parsePositive(value, 'trigger', spec), 'the trigger price');
|
|
412
|
+
break;
|
|
413
|
+
default:
|
|
414
|
+
throw new UsageError(`--order "${spec}" has an unknown field "${key}".`, 'Valid keys: type, price, trigger, product, validity.');
|
|
415
|
+
}
|
|
416
|
+
continue;
|
|
417
|
+
}
|
|
418
|
+
const upper = tok.toUpperCase();
|
|
419
|
+
if (ORDER_TYPES.includes(upper)) {
|
|
420
|
+
orderType = setOnce(orderType, upper, 'the order type');
|
|
421
|
+
}
|
|
422
|
+
else if (PRODUCTS.includes(upper)) {
|
|
423
|
+
product = setOnce(product, upper, 'the product');
|
|
424
|
+
}
|
|
425
|
+
else if (VALIDITIES.includes(upper)) {
|
|
426
|
+
validity = setOnce(validity, upper, 'the validity');
|
|
427
|
+
}
|
|
428
|
+
else if (isNumeric(tok)) {
|
|
429
|
+
// A bare number is always the price. A trigger must be given explicitly.
|
|
430
|
+
price = setOnce(price, parsePositive(tok, 'price', spec), 'the price');
|
|
431
|
+
}
|
|
432
|
+
else {
|
|
433
|
+
throw new UsageError(`--order "${spec}" has an unrecognised field "${tok}".`, 'Fields after QTY are an order type, product, validity, a price, or trigger=<n>.');
|
|
318
434
|
}
|
|
319
435
|
}
|
|
320
|
-
|
|
321
|
-
|
|
436
|
+
const type = orderType ?? 'MARKET';
|
|
437
|
+
if ((type === 'LIMIT' || type === 'SL') && price === undefined) {
|
|
438
|
+
throw new UsageError(`--order "${spec}" is a ${type} order and needs a price.`);
|
|
322
439
|
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
product,
|
|
340
|
-
validity,
|
|
341
|
-
quantity: opts.quantity,
|
|
342
|
-
price: opts.price ?? 0,
|
|
343
|
-
trigger_price: opts.triggerPrice ?? 0,
|
|
344
|
-
variety: 'regular',
|
|
345
|
-
},
|
|
346
|
-
},
|
|
347
|
-
],
|
|
440
|
+
if ((type === 'SL' || type === 'SL-M') && triggerPrice === undefined) {
|
|
441
|
+
throw new UsageError(`--order "${spec}" is a ${type} order and needs trigger=<price>.`);
|
|
442
|
+
}
|
|
443
|
+
if (type === 'MARKET' && price !== undefined) {
|
|
444
|
+
throw new UsageError(`--order "${spec}" is a MARKET order and cannot set a price.`);
|
|
445
|
+
}
|
|
446
|
+
return {
|
|
447
|
+
exchange,
|
|
448
|
+
tradingsymbol,
|
|
449
|
+
side: side,
|
|
450
|
+
quantity,
|
|
451
|
+
orderType: type,
|
|
452
|
+
price,
|
|
453
|
+
triggerPrice,
|
|
454
|
+
product: product ?? 'CNC',
|
|
455
|
+
validity: validity ?? 'DAY',
|
|
348
456
|
};
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
457
|
+
}
|
|
458
|
+
/**
|
|
459
|
+
* Price every leg and assemble the basket for the value cap and confirmation.
|
|
460
|
+
*
|
|
461
|
+
* Returns the notional as UNDEFINED when *any* leg cannot be priced, so the
|
|
462
|
+
* safety layer fails closed (escalates to a typed challenge) rather than
|
|
463
|
+
* treating an unknown total as small. Legs without an explicit limit price are
|
|
464
|
+
* quoted in a single batched LTP call.
|
|
465
|
+
*/
|
|
466
|
+
async function buildAtoBasket(ctx, legs) {
|
|
467
|
+
// Quote every leg that has no explicit price, in one call (the quote bucket
|
|
468
|
+
// is 1/sec, so N separate lookups would rate-limit).
|
|
469
|
+
const needQuote = [
|
|
470
|
+
...new Set(legs.filter((l) => l.price === undefined).map((l) => formatInstrumentKey(l.exchange, l.tradingsymbol))),
|
|
363
471
|
];
|
|
472
|
+
let ltp = {};
|
|
473
|
+
if (needQuote.length > 0) {
|
|
474
|
+
try {
|
|
475
|
+
ltp = await ctx.api.getLtp(needQuote, ctx.signal);
|
|
476
|
+
}
|
|
477
|
+
catch {
|
|
478
|
+
// A 429 here is routine; leave legs unpriced so the cap fails closed.
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
let notionalValue = 0;
|
|
482
|
+
const items = [];
|
|
483
|
+
const orderDetails = [];
|
|
484
|
+
legs.forEach((leg, i) => {
|
|
485
|
+
const key = formatInstrumentKey(leg.exchange, leg.tradingsymbol);
|
|
486
|
+
const referencePrice = leg.price ?? ltp[key]?.last_price;
|
|
487
|
+
if (referencePrice === undefined) {
|
|
488
|
+
// One unpriceable leg voids the whole total — never sum around a gap.
|
|
489
|
+
notionalValue = undefined;
|
|
490
|
+
ctx.io.warn(`Could not fetch a price for ${key}; this alert's order value cannot be estimated.`);
|
|
491
|
+
}
|
|
492
|
+
else if (notionalValue !== undefined) {
|
|
493
|
+
notionalValue += referencePrice * leg.quantity;
|
|
494
|
+
}
|
|
495
|
+
items.push({
|
|
496
|
+
type: 'insert',
|
|
497
|
+
tradingsymbol: leg.tradingsymbol,
|
|
498
|
+
exchange: leg.exchange,
|
|
499
|
+
// Documented single-item baskets use 10000 (a full-allocation weight). The
|
|
500
|
+
// multi-item weighting is undocumented; params drive the actual order, so
|
|
501
|
+
// we keep 10000 per leg rather than invent a split.
|
|
502
|
+
weight: 10000,
|
|
503
|
+
params: {
|
|
504
|
+
transaction_type: leg.side,
|
|
505
|
+
order_type: leg.orderType,
|
|
506
|
+
product: leg.product,
|
|
507
|
+
validity: leg.validity,
|
|
508
|
+
quantity: leg.quantity,
|
|
509
|
+
price: leg.price ?? 0,
|
|
510
|
+
trigger_price: leg.triggerPrice ?? 0,
|
|
511
|
+
variety: 'regular',
|
|
512
|
+
},
|
|
513
|
+
});
|
|
514
|
+
const sideText = leg.side === 'BUY' ? ctx.io.green(leg.side) : ctx.io.red(leg.side);
|
|
515
|
+
const bits = [`${leg.orderType}`, leg.product];
|
|
516
|
+
if (leg.price !== undefined)
|
|
517
|
+
bits.push(`@ ${rupees(leg.price)}`);
|
|
518
|
+
if (leg.triggerPrice !== undefined)
|
|
519
|
+
bits.push(`trigger ${rupees(leg.triggerPrice)}`);
|
|
520
|
+
orderDetails.push({
|
|
521
|
+
label: legs.length === 1 ? 'Order' : `Order ${i + 1}`,
|
|
522
|
+
value: `${sideText} ${quantity(leg.quantity)} ${key} (${bits.join(', ')})`,
|
|
523
|
+
});
|
|
524
|
+
});
|
|
525
|
+
orderDetails.push({
|
|
526
|
+
label: legs.length === 1 ? 'Est. order value' : 'Est. total value',
|
|
527
|
+
value: notionalValue !== undefined ? rupees(notionalValue) : ctx.io.dim('unknown (no quote available)'),
|
|
528
|
+
});
|
|
529
|
+
const basket = { name: 'kite-cli-alert', type: 'alert', tags: [], items };
|
|
364
530
|
return { basket, notionalValue, orderDetails };
|
|
365
531
|
}
|
|
366
532
|
// ---------------------------------------------------------------------------
|
|
@@ -522,6 +688,21 @@ function normalise(value, allowed, label) {
|
|
|
522
688
|
return candidate;
|
|
523
689
|
throw new UsageError(`Unknown ${label} "${value}".`, `Valid values: ${allowed.join(', ')}.`);
|
|
524
690
|
}
|
|
691
|
+
/** Commander reducer for a repeatable option: accumulate values into an array. */
|
|
692
|
+
function collect(value, previous) {
|
|
693
|
+
return [...previous, value];
|
|
694
|
+
}
|
|
695
|
+
/** A finite number literal — rejects '', 'NaN', '1e', '2900abc', etc. */
|
|
696
|
+
function isNumeric(value) {
|
|
697
|
+
return value !== '' && Number.isFinite(Number(value));
|
|
698
|
+
}
|
|
699
|
+
function parsePositive(value, label, spec) {
|
|
700
|
+
const n = Number(value);
|
|
701
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
702
|
+
throw new UsageError(`--order "${spec}" has an invalid ${label} "${value}"; expected a positive number.`);
|
|
703
|
+
}
|
|
704
|
+
return n;
|
|
705
|
+
}
|
|
525
706
|
function renderTableFor(ctx, rows, columns) {
|
|
526
707
|
return renderTable(ctx.io, rows, columns, {
|
|
527
708
|
compact: ctx.config.output.compact,
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { type OrderType, type Product, type TransactionType, type Variety } from '../core/api.js';
|
|
2
|
+
import type { CommandFactory } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* Margin and charge calculators — read-only.
|
|
5
|
+
*
|
|
6
|
+
* These POST a hypothetical set of orders to Kite and return what they would
|
|
7
|
+
* cost: `order` gives the per-order required margin (no netting), `basket` the
|
|
8
|
+
* net margin for the set (with spread/hedge benefit), and `charges` the
|
|
9
|
+
* itemised brokerage/tax breakdown (a "virtual contract note"). Nothing is
|
|
10
|
+
* placed, so none of the trading safety layer applies.
|
|
11
|
+
*/
|
|
12
|
+
export declare const marginCommands: CommandFactory;
|
|
13
|
+
/** A hypothetical order, resolved from a spec, ready to serialise per endpoint. */
|
|
14
|
+
export interface OrderSpec {
|
|
15
|
+
exchange: string;
|
|
16
|
+
tradingsymbol: string;
|
|
17
|
+
transactionType: TransactionType;
|
|
18
|
+
quantity: number;
|
|
19
|
+
orderType: OrderType;
|
|
20
|
+
product: Product;
|
|
21
|
+
variety: Variety;
|
|
22
|
+
price: number | undefined;
|
|
23
|
+
triggerPrice: number | undefined;
|
|
24
|
+
}
|
|
25
|
+
/**
|
|
26
|
+
* Parse one `EXCHANGE:SYMBOL:SIDE:QTY[:attrs...]` spec.
|
|
27
|
+
*
|
|
28
|
+
* Attributes after QTY are classified by content — an order type, a product, a
|
|
29
|
+
* variety, a bare number (the price), or `trigger=<n>`. Fails closed: an
|
|
30
|
+
* unrecognised token, a duplicated field, or an empty field rejects the whole
|
|
31
|
+
* spec rather than silently defaulting, since a mis-parsed order yields a
|
|
32
|
+
* silently wrong margin or charge.
|
|
33
|
+
*/
|
|
34
|
+
export declare function parseOrderSpec(spec: string): OrderSpec;
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { ORDER_TYPES, PRODUCTS, VARIETIES, } from '../core/api.js';
|
|
2
|
+
import { UsageError } from '../core/errors.js';
|
|
3
|
+
import { formatInstrumentKey, parseInstrumentKey } from '../core/instruments.js';
|
|
4
|
+
import { money, rupees } from '../output/format.js';
|
|
5
|
+
import { heading, printTable, renderKeyValue } from '../output/table.js';
|
|
6
|
+
/**
|
|
7
|
+
* Margin and charge calculators — read-only.
|
|
8
|
+
*
|
|
9
|
+
* These POST a hypothetical set of orders to Kite and return what they would
|
|
10
|
+
* cost: `order` gives the per-order required margin (no netting), `basket` the
|
|
11
|
+
* net margin for the set (with spread/hedge benefit), and `charges` the
|
|
12
|
+
* itemised brokerage/tax breakdown (a "virtual contract note"). Nothing is
|
|
13
|
+
* placed, so none of the trading safety layer applies.
|
|
14
|
+
*/
|
|
15
|
+
export const marginCommands = (program, run) => {
|
|
16
|
+
const margins = program.command('margins').description('Calculate order margins and charges (nothing is placed)');
|
|
17
|
+
const orderExample = 'Each order is EXCHANGE:SYMBOL:SIDE:QTY[:TYPE][:PRODUCT][:VARIETY][:PRICE][:trigger=<n>], ' +
|
|
18
|
+
"e.g. 'NFO:NIFTY25AUGFUT:BUY:75:MARKET:NRML'. Product defaults to CNC, variety to regular.";
|
|
19
|
+
margins
|
|
20
|
+
.command('order')
|
|
21
|
+
.description('Required margin for each order on its own')
|
|
22
|
+
.argument('<orders...>', orderExample)
|
|
23
|
+
.action(run(orderMargins));
|
|
24
|
+
margins
|
|
25
|
+
.command('basket')
|
|
26
|
+
.description('Net margin for a basket of orders, with spread/hedge benefit')
|
|
27
|
+
.argument('<orders...>', orderExample)
|
|
28
|
+
.option('--no-consider-positions', 'Ignore existing positions when netting')
|
|
29
|
+
.action(run(basketMargins));
|
|
30
|
+
margins
|
|
31
|
+
.command('charges')
|
|
32
|
+
.description('Itemised charges for a set of executed orders (virtual contract note)')
|
|
33
|
+
.argument('<orders...>', `${orderExample} A non-zero price (the execution price) is required.`)
|
|
34
|
+
.action(run(orderCharges));
|
|
35
|
+
};
|
|
36
|
+
/**
|
|
37
|
+
* Parse one `EXCHANGE:SYMBOL:SIDE:QTY[:attrs...]` spec.
|
|
38
|
+
*
|
|
39
|
+
* Attributes after QTY are classified by content — an order type, a product, a
|
|
40
|
+
* variety, a bare number (the price), or `trigger=<n>`. Fails closed: an
|
|
41
|
+
* unrecognised token, a duplicated field, or an empty field rejects the whole
|
|
42
|
+
* spec rather than silently defaulting, since a mis-parsed order yields a
|
|
43
|
+
* silently wrong margin or charge.
|
|
44
|
+
*/
|
|
45
|
+
export function parseOrderSpec(spec) {
|
|
46
|
+
const tokens = spec
|
|
47
|
+
.trim()
|
|
48
|
+
.split(':')
|
|
49
|
+
.map((t) => t.trim());
|
|
50
|
+
if (tokens.length < 4) {
|
|
51
|
+
throw new UsageError(`Malformed order "${spec}".`, "Expected at least EXCHANGE:SYMBOL:SIDE:QTY, e.g. 'NFO:NIFTY25AUGFUT:BUY:75:NRML'.");
|
|
52
|
+
}
|
|
53
|
+
const [exchangeTok, symbolTok, sideTok, qtyTok, ...rest] = tokens;
|
|
54
|
+
const { exchange, tradingsymbol } = parseInstrumentKey(`${exchangeTok}:${symbolTok}`);
|
|
55
|
+
const side = sideTok.toUpperCase();
|
|
56
|
+
if (side !== 'BUY' && side !== 'SELL') {
|
|
57
|
+
throw new UsageError(`Order side must be BUY or SELL, got "${sideTok}" in "${spec}".`);
|
|
58
|
+
}
|
|
59
|
+
const quantity = Number(qtyTok);
|
|
60
|
+
if (!Number.isInteger(quantity) || quantity <= 0) {
|
|
61
|
+
throw new UsageError(`Order quantity must be a positive integer, got "${qtyTok}" in "${spec}".`);
|
|
62
|
+
}
|
|
63
|
+
let orderType;
|
|
64
|
+
let product;
|
|
65
|
+
let variety;
|
|
66
|
+
let price;
|
|
67
|
+
let triggerPrice;
|
|
68
|
+
const setOnce = (current, next, label) => {
|
|
69
|
+
if (current !== undefined)
|
|
70
|
+
throw new UsageError(`Order "${spec}" sets ${label} more than once.`);
|
|
71
|
+
return next;
|
|
72
|
+
};
|
|
73
|
+
for (const tok of rest) {
|
|
74
|
+
if (tok === '')
|
|
75
|
+
throw new UsageError(`Order "${spec}" has an empty field. Remove the stray ":".`);
|
|
76
|
+
const eq = tok.indexOf('=');
|
|
77
|
+
if (eq !== -1) {
|
|
78
|
+
const key = tok.slice(0, eq).trim().toLowerCase();
|
|
79
|
+
const value = tok.slice(eq + 1).trim();
|
|
80
|
+
if (key === 'price')
|
|
81
|
+
price = setOnce(price, parsePositive(value, 'price', spec), 'the price');
|
|
82
|
+
else if (key === 'trigger')
|
|
83
|
+
triggerPrice = setOnce(triggerPrice, parsePositive(value, 'trigger', spec), 'the trigger price');
|
|
84
|
+
else if (key === 'type')
|
|
85
|
+
orderType = setOnce(orderType, normalise(value, ORDER_TYPES, 'order type'), 'the order type');
|
|
86
|
+
else if (key === 'product')
|
|
87
|
+
product = setOnce(product, normalise(value, PRODUCTS, 'product'), 'the product');
|
|
88
|
+
else if (key === 'variety')
|
|
89
|
+
variety = setOnce(variety, normalise(value, VARIETIES, 'variety'), 'the variety');
|
|
90
|
+
else
|
|
91
|
+
throw new UsageError(`Order "${spec}" has an unknown field "${key}".`, 'Valid keys: type, product, variety, price, trigger.');
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
const upper = tok.toUpperCase();
|
|
95
|
+
const lower = tok.toLowerCase();
|
|
96
|
+
if (ORDER_TYPES.includes(upper)) {
|
|
97
|
+
orderType = setOnce(orderType, upper, 'the order type');
|
|
98
|
+
}
|
|
99
|
+
else if (PRODUCTS.includes(upper)) {
|
|
100
|
+
product = setOnce(product, upper, 'the product');
|
|
101
|
+
}
|
|
102
|
+
else if (VARIETIES.includes(lower)) {
|
|
103
|
+
variety = setOnce(variety, lower, 'the variety');
|
|
104
|
+
}
|
|
105
|
+
else if (isNumeric(tok)) {
|
|
106
|
+
// A bare number is the price. A trigger must be given explicitly.
|
|
107
|
+
price = setOnce(price, parsePositive(tok, 'price', spec), 'the price');
|
|
108
|
+
}
|
|
109
|
+
else {
|
|
110
|
+
throw new UsageError(`Order "${spec}" has an unrecognised field "${tok}".`, 'Fields after QTY are an order type, product, variety, a price, or trigger=<n>.');
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
return {
|
|
114
|
+
exchange,
|
|
115
|
+
tradingsymbol,
|
|
116
|
+
transactionType: side,
|
|
117
|
+
quantity,
|
|
118
|
+
orderType: orderType ?? 'MARKET',
|
|
119
|
+
product: product ?? 'CNC',
|
|
120
|
+
variety: variety ?? 'regular',
|
|
121
|
+
price,
|
|
122
|
+
triggerPrice,
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
function parseSpecs(args) {
|
|
126
|
+
if (args.length === 0)
|
|
127
|
+
throw new UsageError('At least one order is required.');
|
|
128
|
+
return args.map(parseOrderSpec);
|
|
129
|
+
}
|
|
130
|
+
/** Serialise for /margins/orders and /margins/basket (price + trigger_price). */
|
|
131
|
+
function toMarginOrder(o) {
|
|
132
|
+
return {
|
|
133
|
+
exchange: o.exchange,
|
|
134
|
+
tradingsymbol: o.tradingsymbol,
|
|
135
|
+
transaction_type: o.transactionType,
|
|
136
|
+
variety: o.variety,
|
|
137
|
+
product: o.product,
|
|
138
|
+
order_type: o.orderType,
|
|
139
|
+
quantity: o.quantity,
|
|
140
|
+
price: o.price ?? 0,
|
|
141
|
+
trigger_price: o.triggerPrice ?? 0,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
/**
|
|
145
|
+
* Serialise for /charges/orders (average_price + order_id, no price/trigger).
|
|
146
|
+
* Charges are a percentage of quantity × average_price, so a zero price yields
|
|
147
|
+
* a plausible-looking ≈₹0 that is silently wrong — require a real price.
|
|
148
|
+
*/
|
|
149
|
+
function toChargesOrder(o, index) {
|
|
150
|
+
if (o.price === undefined || o.price <= 0) {
|
|
151
|
+
throw new UsageError(`Order "${formatInstrumentKey(o.exchange, o.tradingsymbol)}" needs a non-zero price for charges.`, 'Charges are computed from quantity × execution price; add a price, e.g. :1500 or price=1500.');
|
|
152
|
+
}
|
|
153
|
+
return {
|
|
154
|
+
// A virtual contract note needs an order_id per leg; the index is fine.
|
|
155
|
+
order_id: String(index + 1),
|
|
156
|
+
exchange: o.exchange,
|
|
157
|
+
tradingsymbol: o.tradingsymbol,
|
|
158
|
+
transaction_type: o.transactionType,
|
|
159
|
+
variety: o.variety,
|
|
160
|
+
product: o.product,
|
|
161
|
+
order_type: o.orderType,
|
|
162
|
+
quantity: o.quantity,
|
|
163
|
+
average_price: o.price,
|
|
164
|
+
};
|
|
165
|
+
}
|
|
166
|
+
// ---------------------------------------------------------------------------
|
|
167
|
+
// Commands
|
|
168
|
+
// ---------------------------------------------------------------------------
|
|
169
|
+
async function orderMargins(ctx, _opts, command) {
|
|
170
|
+
ctx.requireSession();
|
|
171
|
+
const specs = parseSpecs(command.args);
|
|
172
|
+
const margins = await ctx.api.orderMargins(specs.map(toMarginOrder), ctx.signal);
|
|
173
|
+
const columns = marginColumns();
|
|
174
|
+
printTable(ctx.io, margins, columns, margins, { compact: ctx.config.output.compact, empty: 'No margins returned.' });
|
|
175
|
+
if (ctx.io.json)
|
|
176
|
+
return;
|
|
177
|
+
const total = margins.reduce((sum, m) => sum + (m.total ?? 0), 0);
|
|
178
|
+
ctx.io.line('');
|
|
179
|
+
ctx.io.line(` Total margin required ${rupees(total)}`);
|
|
180
|
+
}
|
|
181
|
+
async function basketMargins(ctx, opts, command) {
|
|
182
|
+
ctx.requireSession();
|
|
183
|
+
const specs = parseSpecs(command.args);
|
|
184
|
+
const basket = await ctx.api.basketMargins(specs.map(toMarginOrder), opts.considerPositions !== false, ctx.signal);
|
|
185
|
+
if (ctx.io.json) {
|
|
186
|
+
ctx.io.writeJson(basket);
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
const { io } = ctx;
|
|
190
|
+
io.line(heading(io, 'Per order'));
|
|
191
|
+
printTable(io, basket.orders, marginColumns(), basket.orders, {
|
|
192
|
+
compact: ctx.config.output.compact,
|
|
193
|
+
empty: 'No orders.',
|
|
194
|
+
});
|
|
195
|
+
// `final` is the margin actually blocked after spread/hedge benefit; `initial`
|
|
196
|
+
// is the gross figure before it. The difference is the benefit itself.
|
|
197
|
+
const finalTotal = basket.final?.total ?? 0;
|
|
198
|
+
const initialTotal = basket.initial?.total ?? 0;
|
|
199
|
+
io.line('');
|
|
200
|
+
io.line(renderKeyValue(io, [
|
|
201
|
+
['Margin before benefit', rupees(initialTotal)],
|
|
202
|
+
['Spread/hedge benefit', rupees(Math.max(0, initialTotal - finalTotal))],
|
|
203
|
+
['Net margin required', io.bold(rupees(finalTotal))],
|
|
204
|
+
]));
|
|
205
|
+
}
|
|
206
|
+
async function orderCharges(ctx, _opts, command) {
|
|
207
|
+
ctx.requireSession();
|
|
208
|
+
const specs = parseSpecs(command.args);
|
|
209
|
+
const charges = await ctx.api.orderCharges(specs.map((o, i) => toChargesOrder(o, i)), ctx.signal);
|
|
210
|
+
const columns = [
|
|
211
|
+
{ header: 'Symbol', value: (m, io) => io.bold(`${m.exchange ?? '?'}:${m.tradingsymbol ?? '?'}`) },
|
|
212
|
+
{ header: 'Brokerage', value: (m) => money(m.charges?.brokerage), align: 'right' },
|
|
213
|
+
{ header: 'STT/CTT', value: (m) => money(m.charges?.transaction_tax), align: 'right' },
|
|
214
|
+
{ header: 'Txn', value: (m) => money(m.charges?.exchange_turnover_charge), align: 'right' },
|
|
215
|
+
{ header: 'GST', value: (m) => money(m.charges?.gst?.total), align: 'right' },
|
|
216
|
+
{ header: 'Stamp', value: (m) => money(m.charges?.stamp_duty), align: 'right' },
|
|
217
|
+
{ header: 'SEBI', value: (m) => money(m.charges?.sebi_turnover_charge), align: 'right' },
|
|
218
|
+
{ header: 'Total', value: (m, io) => io.bold(money(m.charges?.total)), align: 'right' },
|
|
219
|
+
];
|
|
220
|
+
printTable(ctx.io, charges, columns, charges, {
|
|
221
|
+
compact: ctx.config.output.compact,
|
|
222
|
+
empty: 'No charges returned.',
|
|
223
|
+
});
|
|
224
|
+
if (ctx.io.json)
|
|
225
|
+
return;
|
|
226
|
+
const total = charges.reduce((sum, m) => sum + (m.charges?.total ?? 0), 0);
|
|
227
|
+
ctx.io.line('');
|
|
228
|
+
ctx.io.line(` Total charges ${rupees(total)}`);
|
|
229
|
+
}
|
|
230
|
+
// ---------------------------------------------------------------------------
|
|
231
|
+
// Helpers
|
|
232
|
+
// ---------------------------------------------------------------------------
|
|
233
|
+
function marginColumns() {
|
|
234
|
+
return [
|
|
235
|
+
{ header: 'Symbol', value: (m, io) => io.bold(`${m.exchange ?? '?'}:${m.tradingsymbol ?? '?'}`) },
|
|
236
|
+
{ header: 'SPAN', value: (m) => money(m.span), align: 'right' },
|
|
237
|
+
{ header: 'Exposure', value: (m) => money(m.exposure), align: 'right' },
|
|
238
|
+
{ header: 'Premium', value: (m) => money(m.option_premium), align: 'right' },
|
|
239
|
+
{ header: 'Var', value: (m) => money(m.var), align: 'right' },
|
|
240
|
+
{ header: 'Total', value: (m, io) => io.bold(money(m.total)), align: 'right' },
|
|
241
|
+
];
|
|
242
|
+
}
|
|
243
|
+
function normalise(value, allowed, label) {
|
|
244
|
+
const candidate = label === 'variety' ? value.toLowerCase() : value.toUpperCase();
|
|
245
|
+
if (allowed.includes(candidate))
|
|
246
|
+
return candidate;
|
|
247
|
+
throw new UsageError(`Unknown ${label} "${value}".`, `Valid values: ${allowed.join(', ')}.`);
|
|
248
|
+
}
|
|
249
|
+
function isNumeric(value) {
|
|
250
|
+
return value !== '' && Number.isFinite(Number(value));
|
|
251
|
+
}
|
|
252
|
+
function parsePositive(value, label, spec) {
|
|
253
|
+
const n = Number(value);
|
|
254
|
+
if (!Number.isFinite(n) || n <= 0) {
|
|
255
|
+
throw new UsageError(`Order "${spec}" has an invalid ${label} "${value}"; expected a positive number.`);
|
|
256
|
+
}
|
|
257
|
+
return n;
|
|
258
|
+
}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import type { CommandFactory } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Mutual funds — read only.
|
|
4
|
+
*
|
|
5
|
+
* Kite Connect does not offer MF order placement or SIP management over the
|
|
6
|
+
* API (an MF purchase needs a bank debit the API can't authorise), so this is
|
|
7
|
+
* holdings, recent orders, and SIPs — the three read endpoints — and nothing
|
|
8
|
+
* that moves money. No kill switch, value cap, or confirmation applies.
|
|
9
|
+
*/
|
|
10
|
+
export declare const mfCommands: CommandFactory;
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { dateTime, money, quantity, rupees, signedRupees } from '../output/format.js';
|
|
2
|
+
import { printTable } from '../output/table.js';
|
|
3
|
+
/**
|
|
4
|
+
* Mutual funds — read only.
|
|
5
|
+
*
|
|
6
|
+
* Kite Connect does not offer MF order placement or SIP management over the
|
|
7
|
+
* API (an MF purchase needs a bank debit the API can't authorise), so this is
|
|
8
|
+
* holdings, recent orders, and SIPs — the three read endpoints — and nothing
|
|
9
|
+
* that moves money. No kill switch, value cap, or confirmation applies.
|
|
10
|
+
*/
|
|
11
|
+
export const mfCommands = (program, run) => {
|
|
12
|
+
const mf = program.command('mf').description('View mutual fund holdings, orders and SIPs');
|
|
13
|
+
mf.command('holdings', { isDefault: true }).description('Show your mutual fund holdings').action(run(mfHoldings));
|
|
14
|
+
mf.command('orders').description('Show mutual fund orders from the last 7 days').action(run(mfOrders));
|
|
15
|
+
mf.command('sips').description('Show your mutual fund SIPs').action(run(mfSips));
|
|
16
|
+
};
|
|
17
|
+
async function mfHoldings(ctx) {
|
|
18
|
+
ctx.requireSession();
|
|
19
|
+
const rows = await ctx.api.getMfHoldings(ctx.signal);
|
|
20
|
+
const columns = [
|
|
21
|
+
{ header: 'Fund', value: (h, io) => io.bold(h.fund ?? h.tradingsymbol) },
|
|
22
|
+
{ header: 'Folio', value: (h) => h.folio ?? '—' },
|
|
23
|
+
{ header: 'Units', value: (h) => quantity(h.quantity), align: 'right' },
|
|
24
|
+
{ header: 'Avg', value: (h) => money(h.average_price), align: 'right' },
|
|
25
|
+
{ header: 'NAV', value: (h) => money(h.last_price), align: 'right' },
|
|
26
|
+
{ header: 'Value', value: (h) => money(h.last_price * h.quantity), align: 'right' },
|
|
27
|
+
{ header: 'P&L', value: (h, io) => io.signed(h.pnl, signedRupees(h.pnl)), align: 'right' },
|
|
28
|
+
];
|
|
29
|
+
printTable(ctx.io, rows, columns, rows, {
|
|
30
|
+
compact: ctx.config.output.compact,
|
|
31
|
+
empty: 'No mutual fund holdings.',
|
|
32
|
+
});
|
|
33
|
+
if (ctx.io.json)
|
|
34
|
+
return;
|
|
35
|
+
const totalValue = rows.reduce((sum, h) => sum + h.last_price * h.quantity, 0);
|
|
36
|
+
const totalPnl = rows.reduce((sum, h) => sum + h.pnl, 0);
|
|
37
|
+
if (rows.length > 0) {
|
|
38
|
+
const { io } = ctx;
|
|
39
|
+
io.line('');
|
|
40
|
+
io.line(` Current value ${rupees(totalValue)} P&L ${io.signed(totalPnl, signedRupees(totalPnl))}`);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
async function mfOrders(ctx) {
|
|
44
|
+
ctx.requireSession();
|
|
45
|
+
const rows = await ctx.api.getMfOrders(ctx.signal);
|
|
46
|
+
const columns = [
|
|
47
|
+
{ header: 'Order ID', value: (o, io) => io.dim(o.order_id) },
|
|
48
|
+
{ header: 'Fund', value: (o, io) => io.bold(o.fund ?? o.tradingsymbol ?? '—') },
|
|
49
|
+
{
|
|
50
|
+
header: 'Side',
|
|
51
|
+
value: (o, io) => o.transaction_type === 'BUY'
|
|
52
|
+
? io.green('BUY')
|
|
53
|
+
: o.transaction_type === 'SELL'
|
|
54
|
+
? io.red('SELL')
|
|
55
|
+
: (o.transaction_type ?? '—'),
|
|
56
|
+
},
|
|
57
|
+
{ header: 'Status', value: (o) => o.status ?? '—' },
|
|
58
|
+
{ header: 'Units', value: (o) => quantity(o.quantity ?? undefined), align: 'right' },
|
|
59
|
+
{ header: 'Amount', value: (o) => money(o.amount ?? undefined), align: 'right' },
|
|
60
|
+
{ header: 'When', value: (o) => dateTime(o.order_timestamp ?? undefined) },
|
|
61
|
+
];
|
|
62
|
+
printTable(ctx.io, rows, columns, rows, {
|
|
63
|
+
compact: ctx.config.output.compact,
|
|
64
|
+
// An empty list can just mean nothing was placed recently, not that you have
|
|
65
|
+
// no MF history — the endpoint only reaches back 7 days.
|
|
66
|
+
empty: 'No mutual fund orders in the last 7 days.',
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async function mfSips(ctx) {
|
|
70
|
+
ctx.requireSession();
|
|
71
|
+
const rows = await ctx.api.getMfSips(ctx.signal);
|
|
72
|
+
const columns = [
|
|
73
|
+
{ header: 'SIP ID', value: (s, io) => io.dim(s.sip_id) },
|
|
74
|
+
{ header: 'Fund', value: (s, io) => io.bold(s.fund ?? s.tradingsymbol ?? '—') },
|
|
75
|
+
{ header: 'Status', value: (s) => s.status ?? '—' },
|
|
76
|
+
{ header: 'Instalment', value: (s) => money(s.instalment_amount), align: 'right' },
|
|
77
|
+
{ header: 'Done', value: (s) => quantity(s.instalments), align: 'right' },
|
|
78
|
+
{ header: 'Frequency', value: (s) => s.frequency ?? '—' },
|
|
79
|
+
{ header: 'Next', value: (s) => dateTime(s.next_instalment ?? undefined) },
|
|
80
|
+
];
|
|
81
|
+
printTable(ctx.io, rows, columns, rows, {
|
|
82
|
+
compact: ctx.config.output.compact,
|
|
83
|
+
empty: 'No mutual fund SIPs.',
|
|
84
|
+
});
|
|
85
|
+
}
|
package/dist/core/schemas.d.ts
CHANGED
|
@@ -683,6 +683,7 @@ export declare const BasketMarginSchema: z.ZodObject<{
|
|
|
683
683
|
}, z.core.$loose>>>;
|
|
684
684
|
charges: z.ZodOptional<z.ZodUnknown>;
|
|
685
685
|
}, z.core.$loose>;
|
|
686
|
+
export type BasketMargin = z.infer<typeof BasketMarginSchema>;
|
|
686
687
|
export declare const MfHoldingSchema: z.ZodObject<{
|
|
687
688
|
folio: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
688
689
|
fund: z.ZodOptional<z.ZodString>;
|
|
@@ -707,6 +708,7 @@ export declare const MfOrderSchema: z.ZodObject<{
|
|
|
707
708
|
amount: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
708
709
|
average_price: z.ZodOptional<z.ZodNullable<z.ZodNumber>>;
|
|
709
710
|
}, z.core.$loose>;
|
|
711
|
+
export type MfOrder = z.infer<typeof MfOrderSchema>;
|
|
710
712
|
export declare const MfSipSchema: z.ZodObject<{
|
|
711
713
|
sip_id: z.ZodString;
|
|
712
714
|
tradingsymbol: z.ZodOptional<z.ZodString>;
|
|
@@ -717,6 +719,7 @@ export declare const MfSipSchema: z.ZodObject<{
|
|
|
717
719
|
frequency: z.ZodOptional<z.ZodString>;
|
|
718
720
|
next_instalment: z.ZodOptional<z.ZodNullable<z.ZodString>>;
|
|
719
721
|
}, z.core.$loose>;
|
|
722
|
+
export type MfSip = z.infer<typeof MfSipSchema>;
|
|
720
723
|
export declare const InstrumentSchema: z.ZodObject<{
|
|
721
724
|
instrument_token: z.ZodNumber;
|
|
722
725
|
exchange_token: z.ZodOptional<z.ZodNumber>;
|
package/dist/run.js
CHANGED
|
@@ -77,6 +77,8 @@ export async function run(opts = {}) {
|
|
|
77
77
|
const { orderCommands } = await import('./commands/orders.js');
|
|
78
78
|
const { gttCommands } = await import('./commands/gtt.js');
|
|
79
79
|
const { alertCommands } = await import('./commands/alerts.js');
|
|
80
|
+
const { marginCommands } = await import('./commands/margins.js');
|
|
81
|
+
const { mfCommands } = await import('./commands/mf.js');
|
|
80
82
|
const { watchCommands } = await import('./commands/watch.js');
|
|
81
83
|
const { configCommands } = await import('./commands/config.js');
|
|
82
84
|
// commandsGroup applies to every command registered after it, so the group
|
|
@@ -87,12 +89,15 @@ export async function run(opts = {}) {
|
|
|
87
89
|
profileCommands(program, withContext);
|
|
88
90
|
program.commandsGroup('Portfolio:');
|
|
89
91
|
portfolioCommands(program, withContext);
|
|
92
|
+
program.commandsGroup('Mutual funds:');
|
|
93
|
+
mfCommands(program, withContext);
|
|
90
94
|
program.commandsGroup('Market data:');
|
|
91
95
|
marketCommands(program, withContext);
|
|
92
96
|
program.commandsGroup('Trading:');
|
|
93
97
|
orderCommands(program, withContext);
|
|
94
98
|
gttCommands(program, withContext);
|
|
95
99
|
alertCommands(program, withContext);
|
|
100
|
+
marginCommands(program, withContext);
|
|
96
101
|
program.commandsGroup('Streaming:');
|
|
97
102
|
watchCommands(program, withContext);
|
|
98
103
|
program.commandsGroup('Settings:');
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@pungoyal/kite-cli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.0",
|
|
4
4
|
"description": "Unofficial command-line interface for the Zerodha Kite Connect API — not affiliated with or endorsed by Zerodha",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"zerodha",
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"type": "git",
|
|
19
19
|
"url": "git+https://github.com/pungoyal/kite-cli.git"
|
|
20
20
|
},
|
|
21
|
-
"homepage": "https://github.
|
|
21
|
+
"homepage": "https://pungoyal.github.io/kite-cli",
|
|
22
22
|
"bugs": {
|
|
23
23
|
"url": "https://github.com/pungoyal/kite-cli/issues"
|
|
24
24
|
},
|
|
@@ -53,6 +53,11 @@
|
|
|
53
53
|
"format": "biome format --write",
|
|
54
54
|
"hooks:install": "git config core.hooksPath .githooks",
|
|
55
55
|
"check:deps": "node scripts/check-deps.mjs",
|
|
56
|
+
"docs:commands": "node scripts/generate-command-reference.mjs",
|
|
57
|
+
"docs:commands:check": "node scripts/generate-command-reference.mjs --check",
|
|
58
|
+
"docs:dev": "npm --prefix docs install && npm --prefix docs run dev",
|
|
59
|
+
"docs:build": "npm --prefix docs ci && npm --prefix docs run build",
|
|
60
|
+
"docs:preview": "npm --prefix docs run preview",
|
|
56
61
|
"lint:publish": "publint && attw --pack . --profile esm-only",
|
|
57
62
|
"check:dist": "node -e \"const{existsSync,readFileSync}=require('node:fs');if(!existsSync('dist/cli.js'))throw new Error('dist/ is missing — run npm run build before publishing');if(!readFileSync('dist/cli.js','utf8').startsWith('#!/usr/bin/env node'))throw new Error('dist/cli.js lost its shebang')\"",
|
|
58
63
|
"prepublishOnly": "npm run build && npm run check:dist"
|