@pungoyal/kite-cli 0.2.1 → 0.4.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 +50 -1
- package/dist/commands/alerts.d.ts +28 -0
- package/dist/commands/alerts.js +257 -76
- package/dist/commands/auth.d.ts +9 -0
- package/dist/commands/auth.js +74 -5
- package/dist/commands/completion.d.ts +35 -0
- package/dist/commands/completion.js +240 -0
- package/dist/commands/doctor.d.ts +4 -0
- package/dist/commands/doctor.js +190 -0
- 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/commands/register.d.ts +25 -0
- package/dist/commands/register.js +75 -0
- package/dist/core/auth.d.ts +9 -0
- package/dist/core/auth.js +39 -0
- package/dist/core/client.js +8 -1
- package/dist/core/schemas.d.ts +3 -0
- package/dist/run.js +7 -39
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -50,7 +50,7 @@ Then:
|
|
|
50
50
|
kite login
|
|
51
51
|
```
|
|
52
52
|
|
|
53
|
-
This opens your browser, you log in to Zerodha normally (including your TOTP), and the CLI captures the callback on loopback. Your API secret goes into your OS keyring; the daily access token is stored alongside it.
|
|
53
|
+
This opens your browser, you log in to Zerodha normally (including your TOTP), and the CLI captures the callback on loopback. The login URL is also printed to the terminal — press `c` while it's waiting to copy it to your clipboard (handy if the browser didn't open, or you want to log in on another device). Your API secret goes into your OS keyring; the daily access token is stored alongside it.
|
|
54
54
|
|
|
55
55
|
**Want to try it without a subscription or real money?** Zerodha runs a public sandbox:
|
|
56
56
|
|
|
@@ -78,6 +78,19 @@ kite authorise # authorise demat holdings for selling
|
|
|
78
78
|
|
|
79
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.
|
|
80
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
|
+
|
|
81
94
|
### Market data
|
|
82
95
|
|
|
83
96
|
```bash
|
|
@@ -160,12 +173,48 @@ kite alerts create NSE:INFY --operator below --value 1400 \
|
|
|
160
173
|
--type ato --side BUY --quantity 10 --order-type LIMIT --price 1400
|
|
161
174
|
```
|
|
162
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
|
+
|
|
163
193
|
`--operator` accepts the raw symbols (`>=`, `<=`, `>`, `<`, `==`) or the aliases
|
|
164
194
|
`above`, `below`, `ge`, `le`, `gt`, `lt`, `eq`. Compare against another instrument
|
|
165
195
|
instead of a constant with `--rhs-instrument EXCHANGE:SYMBOL`.
|
|
166
196
|
|
|
167
197
|
Add `--dry-run` to any of these to see exactly what would be sent, without sending it.
|
|
168
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
|
+
|
|
169
218
|
### Scripting
|
|
170
219
|
|
|
171
220
|
Every command supports `--json`, writes data to stdout and everything else to stderr, and returns a meaningful exit code.
|
|
@@ -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,
|
package/dist/commands/auth.d.ts
CHANGED
|
@@ -1,2 +1,11 @@
|
|
|
1
|
+
import type { Context } from '../context.js';
|
|
1
2
|
import type { CommandFactory } from './types.js';
|
|
2
3
|
export declare const authCommands: CommandFactory;
|
|
4
|
+
/**
|
|
5
|
+
* While waiting for the browser callback, listen for a `c` keypress to copy the
|
|
6
|
+
* login URL, and for Ctrl-C to abort. Entering raw mode makes us responsible for
|
|
7
|
+
* both restoring the terminal and re-handling Ctrl-C (raw mode no longer raises
|
|
8
|
+
* SIGINT). Returns a no-op when stdin is not a TTY, and an idempotent cleanup
|
|
9
|
+
* that both the caller and the Ctrl-C path can safely call.
|
|
10
|
+
*/
|
|
11
|
+
export declare function listenForKeys(ctx: Context, loginUrl: string, onInterrupt: () => void): () => void;
|
package/dist/commands/auth.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { isCancel, note, password, text } from '@clack/prompts';
|
|
2
|
-
import { buildLoginUrl, computeChecksum, generateState, openBrowser, redirectUrlFor, waitForCallback, } from '../core/auth.js';
|
|
2
|
+
import { buildLoginUrl, computeChecksum, copyToClipboard, generateState, openBrowser, redirectUrlFor, waitForCallback, } from '../core/auth.js';
|
|
3
3
|
import { loadConfig, SANDBOX_CREDENTIALS, saveConfig } from '../core/config.js';
|
|
4
4
|
import { deleteAllSecrets, getSecret, keyringAvailable, setSecret } from '../core/credentials.js';
|
|
5
5
|
import { AbortedError, ExitCode, KiteCliError } from '../core/errors.js';
|
|
@@ -145,23 +145,92 @@ async function callbackFlow(ctx, loginUrl) {
|
|
|
145
145
|
io.info(`Your Kite app's redirect URL must be exactly: ${io.bold(redirectUrl)}`);
|
|
146
146
|
io.info('Set it at https://developers.kite.trade if login fails.');
|
|
147
147
|
io.note('');
|
|
148
|
+
// Show the URL unconditionally: the browser may not have opened, may have
|
|
149
|
+
// landed in the wrong profile, or the user may want to log in on another
|
|
150
|
+
// device. The URL carries only the api_key and CSRF state — no token.
|
|
151
|
+
io.info('Login URL:');
|
|
152
|
+
io.note(` ${loginUrl}`);
|
|
153
|
+
io.note('');
|
|
148
154
|
const opened = await openBrowser(loginUrl);
|
|
149
155
|
if (opened) {
|
|
150
156
|
io.info('Opened your browser to complete login…');
|
|
151
157
|
}
|
|
152
158
|
else {
|
|
153
|
-
io.warn('Could not open a browser automatically. Open
|
|
154
|
-
io.note(` ${loginUrl}`);
|
|
159
|
+
io.warn('Could not open a browser automatically. Open the URL above manually.');
|
|
155
160
|
}
|
|
156
|
-
|
|
161
|
+
// Let the user grab the URL without selecting it in the terminal. Copy-on-`c`
|
|
162
|
+
// and Ctrl-C both funnel through `interrupted`, which loses the race against a
|
|
163
|
+
// successful callback. A single Ctrl-C aborts the wait — raw mode swallows the
|
|
164
|
+
// SIGINT, and the loopback promise never observes ctx.signal on its own.
|
|
165
|
+
let interrupt = () => { };
|
|
166
|
+
const interrupted = new Promise((_, reject) => {
|
|
167
|
+
interrupt = () => reject(new AbortedError('Interrupted.'));
|
|
168
|
+
});
|
|
169
|
+
const onAbort = () => interrupt();
|
|
170
|
+
if (ctx.signal.aborted)
|
|
171
|
+
onAbort();
|
|
172
|
+
else
|
|
173
|
+
ctx.signal.addEventListener('abort', onAbort, { once: true });
|
|
174
|
+
const stopKeys = listenForKeys(ctx, loginUrl, interrupt);
|
|
175
|
+
io.info(`Waiting for the callback (press ${io.bold('c')} to copy the login URL, Ctrl-C to abort)…`);
|
|
157
176
|
try {
|
|
158
|
-
const result = await server.promise;
|
|
177
|
+
const result = await Promise.race([server.promise, interrupted]);
|
|
159
178
|
return result.requestToken;
|
|
160
179
|
}
|
|
161
180
|
finally {
|
|
181
|
+
// Always restore the terminal and detach listeners, even on abort — the
|
|
182
|
+
// loopback promise may never settle, so this cannot hang off it.
|
|
183
|
+
stopKeys();
|
|
184
|
+
ctx.signal.removeEventListener('abort', onAbort);
|
|
162
185
|
server.close();
|
|
163
186
|
}
|
|
164
187
|
}
|
|
188
|
+
/**
|
|
189
|
+
* While waiting for the browser callback, listen for a `c` keypress to copy the
|
|
190
|
+
* login URL, and for Ctrl-C to abort. Entering raw mode makes us responsible for
|
|
191
|
+
* both restoring the terminal and re-handling Ctrl-C (raw mode no longer raises
|
|
192
|
+
* SIGINT). Returns a no-op when stdin is not a TTY, and an idempotent cleanup
|
|
193
|
+
* that both the caller and the Ctrl-C path can safely call.
|
|
194
|
+
*/
|
|
195
|
+
// Exported for tests: the raw-mode lifecycle (enter, restore, detach) and the
|
|
196
|
+
// Ctrl-C byte path are the risky parts, and a real TTY can't be driven in CI.
|
|
197
|
+
export function listenForKeys(ctx, loginUrl, onInterrupt) {
|
|
198
|
+
const { io } = ctx;
|
|
199
|
+
const stdin = process.stdin;
|
|
200
|
+
if (!stdin.isTTY)
|
|
201
|
+
return () => { };
|
|
202
|
+
const wasRaw = stdin.isRaw;
|
|
203
|
+
let stopped = false;
|
|
204
|
+
const cleanup = () => {
|
|
205
|
+
if (stopped)
|
|
206
|
+
return;
|
|
207
|
+
stopped = true;
|
|
208
|
+
stdin.off('data', onData);
|
|
209
|
+
stdin.setRawMode(wasRaw);
|
|
210
|
+
stdin.pause();
|
|
211
|
+
};
|
|
212
|
+
const onData = (chunk) => {
|
|
213
|
+
// 0x03 is Ctrl-C, which raw mode delivers as a byte instead of a SIGINT.
|
|
214
|
+
if (chunk.length === 1 && chunk[0] === 0x03) {
|
|
215
|
+
cleanup();
|
|
216
|
+
onInterrupt();
|
|
217
|
+
return;
|
|
218
|
+
}
|
|
219
|
+
const key = chunk.toString('utf8');
|
|
220
|
+
if (key === 'c' || key === 'C') {
|
|
221
|
+
void copyToClipboard(loginUrl).then((ok) => {
|
|
222
|
+
if (ok)
|
|
223
|
+
io.success('Login URL copied to clipboard.');
|
|
224
|
+
else
|
|
225
|
+
io.warn('Could not copy to the clipboard (no clipboard tool found).');
|
|
226
|
+
});
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
stdin.setRawMode(true);
|
|
230
|
+
stdin.resume();
|
|
231
|
+
stdin.on('data', onData);
|
|
232
|
+
return cleanup;
|
|
233
|
+
}
|
|
165
234
|
/** Fallback for remote shells, where no browser can reach 127.0.0.1. */
|
|
166
235
|
async function manualFlow(ctx, loginUrl) {
|
|
167
236
|
const { io } = ctx;
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { CommandFactory } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Shell completion scripts, generated from the live command tree.
|
|
4
|
+
*
|
|
5
|
+
* Rather than hand-maintain a completion file per shell — the exact docs-rot
|
|
6
|
+
* trap a CLI that places real orders cannot afford — this walks the same
|
|
7
|
+
* registration path run() uses (with a no-op runner) and emits command names,
|
|
8
|
+
* subcommand names, and long flags straight from it. A new command or flag is
|
|
9
|
+
* completable as soon as it is added; the only user step is to regenerate after
|
|
10
|
+
* upgrading, the same contract gh, rustup, and kubectl ship.
|
|
11
|
+
*
|
|
12
|
+
* Depth is intentionally capped at command → subcommand + flags, which covers
|
|
13
|
+
* every path this CLI has (`config set`, `margins order`, `orders place`).
|
|
14
|
+
*/
|
|
15
|
+
declare const SHELLS: readonly ['bash', 'zsh', 'fish'];
|
|
16
|
+
type Shell = (typeof SHELLS)[number];
|
|
17
|
+
export declare const completionCommands: CommandFactory;
|
|
18
|
+
export interface CompletionModel {
|
|
19
|
+
/** Top-level command names (excluding commander's built-in `help`). */
|
|
20
|
+
commands: string[];
|
|
21
|
+
/** Subcommand names keyed by their parent command name. */
|
|
22
|
+
subcommands: Record<string, string[]>;
|
|
23
|
+
/** Long flags valid on every command. */
|
|
24
|
+
globalFlags: string[];
|
|
25
|
+
/** Human descriptions for the shells that render them (zsh, fish). */
|
|
26
|
+
descriptions: Record<string, string>;
|
|
27
|
+
}
|
|
28
|
+
/**
|
|
29
|
+
* Build the completion model by registering the real command tree onto a
|
|
30
|
+
* throwaway program. The runner is a no-op: completion needs each command's
|
|
31
|
+
* name, options, and subcommands, never its behaviour.
|
|
32
|
+
*/
|
|
33
|
+
export declare function buildModel(): Promise<CompletionModel>;
|
|
34
|
+
export declare function renderScript(shell: Shell, model: CompletionModel): string;
|
|
35
|
+
export {};
|