@pungoyal/kite-cli 0.1.0 → 0.2.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 CHANGED
@@ -2,6 +2,7 @@
2
2
 
3
3
  [![CI](https://github.com/pungoyal/kite-cli/actions/workflows/ci.yml/badge.svg)](https://github.com/pungoyal/kite-cli/actions/workflows/ci.yml)
4
4
  [![npm version](https://img.shields.io/npm/v/@pungoyal/kite-cli.svg)](https://www.npmjs.com/package/@pungoyal/kite-cli)
5
+ [![Release](https://img.shields.io/github/v/release/pungoyal/kite-cli?sort=semver&color=blue)](https://github.com/pungoyal/kite-cli/releases/latest)
5
6
  [![Node.js](https://img.shields.io/badge/node-%E2%89%A5%2022.12-brightgreen.svg)](https://nodejs.org)
6
7
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
7
8
 
@@ -57,6 +58,8 @@ kite login --env sandbox
57
58
  kite --env sandbox holdings
58
59
  ```
59
60
 
61
+ **Running more than one Zerodha account?** See [Multiple accounts](#multiple-accounts).
62
+
60
63
  ## Usage
61
64
 
62
65
  ### Portfolio
@@ -135,8 +138,29 @@ kite gtt place NSE:INFY --side SELL --quantity 10 \
135
138
  --trigger 1400 --price 1395 \
136
139
  --trigger 1700 --price 1695 # two-leg OCO
137
140
  kite gtt delete <id>
141
+
142
+ kite alerts list # your price alerts
143
+ kite alerts create "INDICES:NIFTY 50" --operator above --value 27000
144
+ kite alerts get <uuid> # detail, incl. any attached order
145
+ kite alerts history <uuid> # when it has fired
146
+ kite alerts delete <uuid> [<uuid>...]
138
147
  ```
139
148
 
149
+ Alerts come in two kinds. A **simple** alert just notifies you when a price
150
+ condition is met — it moves no money, so the kill switch and value cap do not
151
+ apply. An **ATO** (Alert-Triggers-Order) alert places a real order when it fires,
152
+ so creating one goes through the same confirmation, value cap and kill switch as
153
+ `orders place`:
154
+
155
+ ```bash
156
+ kite alerts create NSE:INFY --operator below --value 1400 \
157
+ --type ato --side BUY --quantity 10 --order-type LIMIT --price 1400
158
+ ```
159
+
160
+ `--operator` accepts the raw symbols (`>=`, `<=`, `>`, `<`, `==`) or the aliases
161
+ `above`, `below`, `ge`, `le`, `gt`, `lt`, `eq`. Compare against another instrument
162
+ instead of a constant with `--rhs-instrument EXCHANGE:SYMBOL`.
163
+
140
164
  Add `--dry-run` to any of these to see exactly what would be sent, without sending it.
141
165
 
142
166
  ### Scripting
@@ -169,6 +193,59 @@ kite whoami --json || kite login # exit code 3 means "no session"
169
193
 
170
194
  `NO_COLOR` is honoured, and colour is disabled automatically when stdout is not a TTY.
171
195
 
196
+ ## Multiple accounts
197
+
198
+ If you run more than one Zerodha account — your own, a family member's, an HUF — each
199
+ gets a named **profile** with its own Kite Connect app credentials and its own daily
200
+ session. Several accounts can be logged in at once; you choose which one a command
201
+ targets.
202
+
203
+ ```bash
204
+ kite profiles add huf # register a profile (create its Kite app first)
205
+ kite --profile huf login # log in to it (prompts for that app's key + secret)
206
+ kite --profile huf holdings # run any command against it
207
+ kite profiles list # see every profile and its session status
208
+ kite profiles use huf # make it the default for commands without --profile
209
+ ```
210
+
211
+ `profiles add` can take the account's settings up front so `login` doesn't have to
212
+ prompt for them: `--api-key <key>`, `--env sandbox` (the profile's environment,
213
+ default `production`), and `--max-order-value <rupees>` (a per-profile cap). The API
214
+ secret is never a flag — `login` always prompts for it.
215
+
216
+ Selection is resolved fresh every run — there is no hidden "active account" that
217
+ persists silently between commands. The target is chosen by, in order:
218
+
219
+ 1. `--profile <name>` on the command line
220
+ 2. the `KITE_PROFILE` environment variable
221
+ 3. the default set with `kite profiles use`
222
+ 4. otherwise the `default` profile
223
+
224
+ `default` is your original single-account setup (nothing to migrate), and `sandbox`
225
+ is a reserved profile — `--env sandbox` is just shorthand for `--profile sandbox`.
226
+
227
+ Because targeting the wrong account is the costly mistake here, every money-moving
228
+ confirmation shows the **verified account** it will hit — the user id returned by
229
+ Kite, not just the label you chose:
230
+
231
+ ```
232
+ Place BUY order
233
+ Account Priya Sharma (XY9876) · profile huf
234
+ Symbol NSE:INFY
235
+
236
+ ```
237
+
238
+ Safety caps are per profile, inheriting the global setting when unset (an omitted cap
239
+ never means "no cap"):
240
+
241
+ ```bash
242
+ kite --profile huf config set trading.maxOrderValue 50000
243
+ ```
244
+
245
+ For scripts and CI, `KITE_API_KEY` / `KITE_API_SECRET` / `KITE_ACCESS_TOKEN` still
246
+ supply credentials directly. As a safeguard, naming a profile explicitly while those
247
+ are set is refused rather than silently overridden.
248
+
172
249
  ## Safety
173
250
 
174
251
  This tool spends real money, so the defaults are conservative.
@@ -238,6 +315,10 @@ kite config path
238
315
 
239
316
  Config lives at `~/.config/kite/config.json` (`0600`). Override the location with `KITE_CONFIG_DIR`.
240
317
 
318
+ `trading.*`, `apiKey`, and `env` can be set per profile by adding `--profile <name>`
319
+ (see [Multiple accounts](#multiple-accounts)); the remaining keys are global. The
320
+ account this invocation resolves to is selected by `--profile` / `KITE_PROFILE`.
321
+
241
322
  ## Things worth knowing about Kite
242
323
 
243
324
  - **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.
@@ -0,0 +1,12 @@
1
+ import type { CommandFactory } from './types.js';
2
+ /**
3
+ * Price alerts.
4
+ *
5
+ * Two shapes:
6
+ * simple — fires a notification when a condition (e.g. LTP >= 27000) is met.
7
+ * No order, no money moves; treated like a watchlist entry.
8
+ * ato — Alert-Triggers-Order: carries a basket that is placed as a real
9
+ * order when the alert fires. Creating one is order placement in
10
+ * disguise, so it gets the same safety treatment as `orders place`.
11
+ */
12
+ export declare const alertCommands: CommandFactory;
@@ -0,0 +1,529 @@
1
+ import { z } from 'zod';
2
+ import { ALERT_DEFAULT_ATTRIBUTE, ALERT_OPERATORS, ORDER_TYPES, PRODUCTS, VALIDITIES, } from '../core/api.js';
3
+ import { UsageError } from '../core/errors.js';
4
+ import { formatInstrumentKey, parseInstrumentKey } from '../core/instruments.js';
5
+ import { dateTime, money, quantity, rupees } from '../output/format.js';
6
+ import { heading, printTable, renderKeyValue, renderTable } from '../output/table.js';
7
+ import { assertTradingEnabled, confirmAction } from '../safety.js';
8
+ /**
9
+ * Price alerts.
10
+ *
11
+ * Two shapes:
12
+ * simple — fires a notification when a condition (e.g. LTP >= 27000) is met.
13
+ * No order, no money moves; treated like a watchlist entry.
14
+ * ato — Alert-Triggers-Order: carries a basket that is placed as a real
15
+ * order when the alert fires. Creating one is order placement in
16
+ * disguise, so it gets the same safety treatment as `orders place`.
17
+ */
18
+ export const alertCommands = (program, run) => {
19
+ const alerts = program.command('alerts').description('Manage price alerts');
20
+ alerts
21
+ .command('list', { isDefault: true })
22
+ .description('Show your alerts')
23
+ .option('--enabled', 'Show only enabled alerts')
24
+ .option('--disabled', 'Show only disabled alerts')
25
+ .action(run(listAlerts));
26
+ alerts.command('get').description('Show one alert in detail').argument('<uuid>').action(run(getAlert));
27
+ alerts.command('history').description("Show an alert's trigger history").argument('<uuid>').action(run(alertHistory));
28
+ alerts
29
+ .command('create')
30
+ .description('Create a price alert')
31
+ .argument('<instrument>', 'Instrument to watch, as EXCHANGE:SYMBOL (e.g. INDICES:NIFTY 50)')
32
+ .requiredOption('-o, --operator <op>', 'Condition: >=, <=, >, <, == (aliases: above, below, ge, le, gt, lt, eq)')
33
+ .option('--value <n>', 'Threshold to compare against (a constant)')
34
+ .option('--rhs-instrument <instrument>', 'Compare against another instrument instead of a constant')
35
+ .option('--name <name>', 'Alert name (defaults to a description of the condition)')
36
+ .option('--attribute <attribute>', 'Attribute to compare', ALERT_DEFAULT_ATTRIBUTE)
37
+ .option('--type <type>', 'simple or ato (ato places an order when it fires)', 'simple')
38
+ // ATO order flags — mirror `orders place`. Only read when --type ato.
39
+ .option('-s, --side <side>', 'ATO: order side, BUY or SELL')
40
+ .option('-q, --quantity <n>', 'ATO: order quantity')
41
+ .option('--order-type <type>', `ATO: order type (${ORDER_TYPES.join(', ')})`, 'MARKET')
42
+ .option('-p, --price <price>', 'ATO: limit price (for LIMIT/SL orders)')
43
+ .option('--trigger-price <price>', 'ATO: trigger price (for SL/SL-M orders)')
44
+ .option('--product <product>', `ATO: product (${PRODUCTS.join(', ')})`, 'CNC')
45
+ .option('--validity <validity>', `ATO: validity (${VALIDITIES.join(', ')})`, 'DAY')
46
+ .action(run(createAlert));
47
+ alerts
48
+ .command('modify')
49
+ .description('Modify an existing alert')
50
+ .argument('<uuid>')
51
+ .option('-o, --operator <op>', 'New condition operator')
52
+ .option('--value <n>', 'New threshold constant')
53
+ .option('--name <name>', 'New alert name')
54
+ .action(run(modifyAlert));
55
+ alerts
56
+ .command('delete')
57
+ .description('Delete one or more alerts')
58
+ .argument('<uuid...>', 'One or more alert UUIDs')
59
+ .action(run(deleteAlerts));
60
+ };
61
+ // ---------------------------------------------------------------------------
62
+ // Read
63
+ // ---------------------------------------------------------------------------
64
+ async function listAlerts(ctx, opts) {
65
+ ctx.requireSession();
66
+ const all = await ctx.api.getAlerts(ctx.signal);
67
+ let rows = all;
68
+ if (opts.enabled)
69
+ rows = rows.filter((a) => a.status === 'enabled');
70
+ if (opts.disabled)
71
+ rows = rows.filter((a) => a.status === 'disabled');
72
+ const columns = [
73
+ { header: 'UUID', value: (a, io) => io.dim(a.uuid) },
74
+ { header: 'Name', value: (a, io) => io.bold(a.name ?? '—') },
75
+ { header: 'Type', value: (a) => (a.type === 'ato' ? 'ATO' : 'simple') },
76
+ { header: 'Condition', value: (a) => describeCondition(a) },
77
+ { header: 'Status', value: (a, io) => colourAlertStatus(io, a.status) },
78
+ { header: 'Fired', value: (a) => quantity(a.alert_count ?? 0), align: 'right' },
79
+ ];
80
+ printTable(ctx.io, rows, columns, rows, {
81
+ compact: ctx.config.output.compact,
82
+ empty: opts.enabled || opts.disabled ? 'No matching alerts.' : 'No alerts.',
83
+ });
84
+ }
85
+ function colourAlertStatus(io, status) {
86
+ switch (status) {
87
+ case 'enabled':
88
+ return io.green(status);
89
+ case 'disabled':
90
+ return io.yellow(status);
91
+ case 'deleted':
92
+ return io.dim(status);
93
+ default:
94
+ return status;
95
+ }
96
+ }
97
+ /** Render the alert's condition as a human-readable expression. */
98
+ function describeCondition(a) {
99
+ const lhs = `${a.lhs_exchange ?? '?'}:${a.lhs_tradingsymbol ?? '?'}`;
100
+ const op = a.operator ?? '?';
101
+ const rhs = a.rhs_type === 'instrument' ? `${a.rhs_exchange ?? '?'}:${a.rhs_tradingsymbol ?? '?'}` : money(a.rhs_constant);
102
+ return `${lhs} ${op} ${rhs}`;
103
+ }
104
+ async function getAlert(ctx, _opts, command) {
105
+ ctx.requireSession();
106
+ const uuid = requireUuid(command.args[0]);
107
+ const alert = await ctx.api.getAlert(uuid, ctx.signal);
108
+ if (ctx.io.json) {
109
+ ctx.io.writeJson(alert);
110
+ return;
111
+ }
112
+ const { io } = ctx;
113
+ io.line(heading(io, `Alert ${alert.name ?? alert.uuid}`));
114
+ io.line(renderKeyValue(io, [
115
+ ['UUID', alert.uuid],
116
+ ['Type', alert.type === 'ato' ? 'ATO (places an order)' : 'simple'],
117
+ ['Status', colourAlertStatus(io, alert.status)],
118
+ ...(alert.disabled_reason ? [['Disabled reason', alert.disabled_reason]] : []),
119
+ ['Condition', describeCondition(alert)],
120
+ ['Attribute', alert.lhs_attribute ?? '—'],
121
+ ['Times fired', quantity(alert.alert_count ?? 0)],
122
+ ['Created', dateTime(alert.created_at)],
123
+ ['Updated', dateTime(alert.updated_at)],
124
+ ]));
125
+ const items = alert.basket?.items ?? [];
126
+ if (items.length > 0) {
127
+ io.line(heading(io, 'Order basket'));
128
+ io.line(renderTableFor(ctx, items, [
129
+ { header: 'Symbol', value: (i, io) => io.bold(`${i.exchange ?? '?'}:${i.tradingsymbol ?? '?'}`) },
130
+ {
131
+ header: 'Side',
132
+ value: (i, io) => {
133
+ const side = i.params?.transaction_type;
134
+ return side === 'BUY' ? io.green('BUY') : side === 'SELL' ? io.red('SELL') : '—';
135
+ },
136
+ },
137
+ {
138
+ header: 'Qty',
139
+ value: (i) => quantity(i.params?.quantity),
140
+ align: 'right',
141
+ },
142
+ { header: 'Type', value: (i) => i.params?.order_type ?? '—' },
143
+ {
144
+ header: 'Price',
145
+ value: (i) => money(i.params?.price),
146
+ align: 'right',
147
+ },
148
+ { header: 'Product', value: (i) => i.params?.product ?? '—' },
149
+ ]));
150
+ }
151
+ }
152
+ async function alertHistory(ctx, _opts, command) {
153
+ ctx.requireSession();
154
+ const uuid = requireUuid(command.args[0]);
155
+ const history = await ctx.api.getAlertHistory(uuid, ctx.signal);
156
+ if (ctx.io.json) {
157
+ ctx.io.writeJson(history);
158
+ return;
159
+ }
160
+ const columns = [
161
+ { header: 'When', value: (h) => dateTime(h.created_at) },
162
+ { header: 'Condition', value: (h) => h.condition ?? '—' },
163
+ { header: 'Order', value: (h, io) => (h.order_meta ? io.green('placed') : io.dim('—')) },
164
+ ];
165
+ printTable(ctx.io, history, columns, history, {
166
+ compact: ctx.config.output.compact,
167
+ empty: 'This alert has never fired.',
168
+ });
169
+ }
170
+ // ---------------------------------------------------------------------------
171
+ // Create
172
+ // ---------------------------------------------------------------------------
173
+ /**
174
+ * Re-validate Commander's untyped opts at runtime. Same reasoning as
175
+ * `orders place`: the `.option()` declarations aren't checked against this
176
+ * shape, so we turn the type lie into a runtime guarantee before an alert that
177
+ * can place an order is built from it.
178
+ */
179
+ const CreateOptionsSchema = z.object({
180
+ operator: z.string(),
181
+ value: z.coerce.number().optional(),
182
+ rhsInstrument: z.string().optional(),
183
+ name: z.string().optional(),
184
+ attribute: z.string().default(ALERT_DEFAULT_ATTRIBUTE),
185
+ type: z.string().default('simple'),
186
+ side: z.string().optional(),
187
+ quantity: z.coerce.number().int().positive().optional(),
188
+ orderType: z.string().default('MARKET'),
189
+ price: z.coerce.number().positive().optional(),
190
+ triggerPrice: z.coerce.number().positive().optional(),
191
+ product: z.string().default('CNC'),
192
+ validity: z.string().default('DAY'),
193
+ });
194
+ async function createAlert(ctx, rawOpts, command) {
195
+ ctx.requireSession();
196
+ const parsed = CreateOptionsSchema.safeParse(rawOpts);
197
+ if (!parsed.success) {
198
+ throw new UsageError(`Invalid options:\n${z.prettifyError(parsed.error)}`);
199
+ }
200
+ const opts = parsed.data;
201
+ const instrumentArg = command.args[0];
202
+ if (!instrumentArg)
203
+ throw new UsageError('An instrument to watch is required, e.g. `kite alerts create NSE:INFY ...`.');
204
+ const lhs = parseInstrumentKey(instrumentArg);
205
+ const operator = normaliseOperator(opts.operator);
206
+ const type = normaliseType(opts.type);
207
+ // Right-hand side: a constant OR another instrument, never both.
208
+ if (opts.value !== undefined && opts.rhsInstrument !== undefined) {
209
+ throw new UsageError('Use either --value or --rhs-instrument, not both.');
210
+ }
211
+ const params = {
212
+ name: opts.name ?? defaultAlertName(lhs.tradingsymbol, operator, opts),
213
+ type,
214
+ lhs_exchange: lhs.exchange,
215
+ lhs_tradingsymbol: lhs.tradingsymbol,
216
+ lhs_attribute: opts.attribute,
217
+ operator,
218
+ rhs_type: 'constant',
219
+ };
220
+ let conditionValue;
221
+ if (opts.rhsInstrument !== undefined) {
222
+ const rhs = parseInstrumentKey(opts.rhsInstrument);
223
+ params.rhs_type = 'instrument';
224
+ params.rhs_exchange = rhs.exchange;
225
+ params.rhs_tradingsymbol = rhs.tradingsymbol;
226
+ params.rhs_attribute = opts.attribute;
227
+ conditionValue = formatInstrumentKey(rhs.exchange, rhs.tradingsymbol);
228
+ }
229
+ else {
230
+ if (opts.value === undefined) {
231
+ throw new UsageError('A threshold is required. Pass --value <n> or --rhs-instrument <EXCHANGE:SYMBOL>.');
232
+ }
233
+ params.rhs_constant = opts.value;
234
+ conditionValue = rupees(opts.value);
235
+ }
236
+ const lhsKey = formatInstrumentKey(lhs.exchange, lhs.tradingsymbol);
237
+ const details = [
238
+ { label: 'Watch', value: lhsKey },
239
+ { label: 'Condition', value: `${opts.attribute} ${operator} ${conditionValue}` },
240
+ { label: 'Name', value: params.name },
241
+ { label: 'Type', value: type === 'ato' ? ctx.io.yellow('ATO — places an order when it fires') : 'simple' },
242
+ ];
243
+ if (type === 'ato') {
244
+ // ATO creation IS order placement: it needs the kill switch, the value cap,
245
+ // and the full escalating confirmation, exactly like `orders place`.
246
+ assertTradingEnabled(ctx);
247
+ const { basket, notionalValue, orderDetails } = await buildAtoBasket(ctx, opts, lhs);
248
+ params.basket = basket;
249
+ await confirmAction(ctx, {
250
+ action: `Create ATO alert on ${lhsKey}`,
251
+ mutatesOrders: true,
252
+ increasesExposure: true,
253
+ notionalValue,
254
+ challengeToken: lhs.tradingsymbol,
255
+ details: [...details, ...orderDetails],
256
+ });
257
+ }
258
+ else {
259
+ // A simple alert moves no money — no kill switch, no value cap, no typed
260
+ // challenge. Just show what will be created and take a plain confirmation.
261
+ await confirmAction(ctx, {
262
+ action: `Create alert on ${lhsKey}`,
263
+ details,
264
+ });
265
+ }
266
+ if (ctx.options.dryRun)
267
+ return;
268
+ const result = await ctx.api.createAlert(params, ctx.signal);
269
+ if (ctx.io.json) {
270
+ ctx.io.writeJson(result);
271
+ return;
272
+ }
273
+ ctx.io.success(`Alert created: ${ctx.io.bold(result.uuid)}`);
274
+ if (type === 'ato') {
275
+ ctx.io.info('When this fires, Kite places the order in the basket. Acceptance is not execution — check `kite orders list`.');
276
+ }
277
+ }
278
+ /**
279
+ * Build the single-order basket for an ATO alert from the order flags, and
280
+ * price it for the value cap.
281
+ *
282
+ * Returns the notional as UNDEFINED when it cannot be priced, so the safety
283
+ * layer fails closed (escalates to a typed challenge) rather than treating an
284
+ * unknown value as small.
285
+ */
286
+ async function buildAtoBasket(ctx, opts, lhs) {
287
+ if (!opts.side)
288
+ throw new UsageError('--side (BUY or SELL) is required for an ATO alert.');
289
+ if (opts.quantity === undefined)
290
+ throw new UsageError('--quantity is required for an ATO alert.');
291
+ const side = opts.side.toUpperCase();
292
+ if (side !== 'BUY' && side !== 'SELL')
293
+ 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');
297
+ if ((orderType === 'LIMIT' || orderType === 'SL') && opts.price === undefined) {
298
+ throw new UsageError(`--price is required for a ${orderType} order.`);
299
+ }
300
+ if ((orderType === 'SL' || orderType === 'SL-M') && opts.triggerPrice === undefined) {
301
+ throw new UsageError(`--trigger-price is required for a ${orderType} order.`);
302
+ }
303
+ if (orderType === 'MARKET' && opts.price !== undefined) {
304
+ throw new UsageError('--price cannot be used with a MARKET order.');
305
+ }
306
+ // The ATO order trades the LHS (watched) instrument.
307
+ const instrumentKey = formatInstrumentKey(lhs.exchange, lhs.tradingsymbol);
308
+ // Price for the value cap. An explicit limit price is authoritative; otherwise
309
+ // fall back to the last traded price, leaving it undefined if that fails.
310
+ let referencePrice = opts.price;
311
+ if (referencePrice === undefined) {
312
+ try {
313
+ const ltp = await ctx.api.getLtp([instrumentKey], ctx.signal);
314
+ referencePrice = ltp[instrumentKey]?.last_price;
315
+ }
316
+ catch {
317
+ // Quote bucket is 1/sec; a 429 here is routine. Leave undefined.
318
+ }
319
+ }
320
+ if (referencePrice === undefined) {
321
+ ctx.io.warn(`Could not fetch a price for ${instrumentKey}; this alert's order value cannot be estimated.`);
322
+ }
323
+ const notionalValue = referencePrice !== undefined ? referencePrice * opts.quantity : undefined;
324
+ const basket = {
325
+ name: 'kite-cli-alert',
326
+ type: 'alert',
327
+ tags: [],
328
+ items: [
329
+ {
330
+ type: 'insert',
331
+ tradingsymbol: lhs.tradingsymbol,
332
+ exchange: lhs.exchange,
333
+ // Documented baskets use 10000 (a full-allocation weight) for a single
334
+ // item; we follow the docs rather than invent a value.
335
+ weight: 10000,
336
+ params: {
337
+ transaction_type: side,
338
+ order_type: orderType,
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
+ ],
348
+ };
349
+ const orderDetails = [
350
+ {
351
+ label: 'Order',
352
+ value: `${side === 'BUY' ? ctx.io.green(side) : ctx.io.red(side)} ${quantity(opts.quantity)} ${lhs.tradingsymbol}`,
353
+ },
354
+ { label: 'Order type', value: orderType },
355
+ ...(opts.price !== undefined ? [{ label: 'Price', value: rupees(opts.price) }] : []),
356
+ ...(opts.triggerPrice !== undefined ? [{ label: 'Trigger', value: rupees(opts.triggerPrice) }] : []),
357
+ { label: 'Product', value: product },
358
+ { label: 'Validity', value: validity },
359
+ {
360
+ label: 'Est. order value',
361
+ value: notionalValue !== undefined ? rupees(notionalValue) : ctx.io.dim('unknown (no quote available)'),
362
+ },
363
+ ];
364
+ return { basket, notionalValue, orderDetails };
365
+ }
366
+ // ---------------------------------------------------------------------------
367
+ // Modify
368
+ // ---------------------------------------------------------------------------
369
+ async function modifyAlert(ctx, opts, command) {
370
+ ctx.requireSession();
371
+ const uuid = requireUuid(command.args[0]);
372
+ if (opts.operator === undefined && opts.value === undefined && opts.name === undefined) {
373
+ throw new UsageError('Nothing to modify. Pass at least one of --operator, --value or --name.');
374
+ }
375
+ // Kite's PUT replaces the whole alert, so we start from the current one and
376
+ // overlay the changes — including carrying the existing ATO basket through
377
+ // untouched, which is why we read it back rather than reconstructing it.
378
+ const existing = await ctx.api.getAlert(uuid, ctx.signal);
379
+ const type = existing.type === 'ato' ? 'ato' : 'simple';
380
+ const operator = opts.operator !== undefined ? normaliseOperator(opts.operator) : existing.operator;
381
+ if (!operator || !ALERT_OPERATORS.includes(operator)) {
382
+ throw new UsageError('This alert has no valid operator to keep; pass --operator explicitly.');
383
+ }
384
+ const rhsType = existing.rhs_type === 'instrument' ? 'instrument' : 'constant';
385
+ const params = {
386
+ name: opts.name ?? existing.name ?? describeCondition(existing),
387
+ type,
388
+ lhs_exchange: existing.lhs_exchange ?? '',
389
+ lhs_tradingsymbol: existing.lhs_tradingsymbol ?? '',
390
+ lhs_attribute: existing.lhs_attribute ?? ALERT_DEFAULT_ATTRIBUTE,
391
+ operator,
392
+ rhs_type: rhsType,
393
+ };
394
+ if (rhsType === 'instrument') {
395
+ params.rhs_exchange = existing.rhs_exchange;
396
+ params.rhs_tradingsymbol = existing.rhs_tradingsymbol;
397
+ params.rhs_attribute = existing.rhs_attribute ?? ALERT_DEFAULT_ATTRIBUTE;
398
+ }
399
+ else {
400
+ if (opts.value !== undefined) {
401
+ const value = Number(opts.value);
402
+ if (!Number.isFinite(value))
403
+ throw new UsageError('--value must be a number.');
404
+ params.rhs_constant = value;
405
+ }
406
+ else {
407
+ params.rhs_constant = existing.rhs_constant;
408
+ }
409
+ }
410
+ if (existing.basket) {
411
+ // Carry the ATO order through unchanged. The basket read back has richer
412
+ // fields than we send, but Kite accepts the round-trip.
413
+ params.basket = existing.basket;
414
+ }
415
+ const before = describeCondition(existing);
416
+ const after = describeCondition({ ...existing, operator, rhs_constant: params.rhs_constant });
417
+ await confirmAction(ctx, {
418
+ action: `Modify alert ${existing.name ?? uuid}`,
419
+ // An ATO alert carries an order; modifying its trigger changes when that
420
+ // order fires, so apply the trading guard rails as for creation.
421
+ mutatesOrders: type === 'ato',
422
+ details: [
423
+ { label: 'UUID', value: uuid },
424
+ { label: 'Condition', value: before === after ? before : `${before} → ${after}` },
425
+ ...(opts.name !== undefined ? [{ label: 'Name', value: `${existing.name ?? '—'} → ${params.name}` }] : []),
426
+ ],
427
+ });
428
+ if (ctx.options.dryRun)
429
+ return;
430
+ const result = await ctx.api.modifyAlert(uuid, params, ctx.signal);
431
+ if (ctx.io.json) {
432
+ ctx.io.writeJson(result);
433
+ return;
434
+ }
435
+ ctx.io.success(`Alert ${result.uuid} modified.`);
436
+ }
437
+ // ---------------------------------------------------------------------------
438
+ // Delete
439
+ // ---------------------------------------------------------------------------
440
+ async function deleteAlerts(ctx, _opts, command) {
441
+ ctx.requireSession();
442
+ const uuids = command.args.map((arg) => requireUuid(arg));
443
+ if (uuids.length === 0)
444
+ throw new UsageError('At least one alert UUID is required.');
445
+ // Enrichment for the preview. A failed lookup must not block deletion —
446
+ // removing an alert only cancels a pending trigger — but the user is told the
447
+ // details are unverified rather than shown a silent "unknown".
448
+ const known = new Map();
449
+ try {
450
+ for (const alert of await ctx.api.getAlerts(ctx.signal))
451
+ known.set(alert.uuid, alert);
452
+ }
453
+ catch {
454
+ ctx.io.warn('Could not read your alerts from Kite; the details below are unverified.');
455
+ }
456
+ await confirmAction(ctx, {
457
+ action: uuids.length === 1 ? `Delete alert ${uuids[0]}` : `Delete ${uuids.length} alerts`,
458
+ details: uuids.map((uuid) => {
459
+ const alert = known.get(uuid);
460
+ return {
461
+ label: alert?.name ?? uuid,
462
+ value: alert ? `${describeCondition(alert)} (${alert.status})` : ctx.io.dim('unverified'),
463
+ };
464
+ }),
465
+ });
466
+ if (ctx.options.dryRun)
467
+ return;
468
+ const result = await ctx.api.deleteAlerts(uuids, ctx.signal);
469
+ if (ctx.io.json) {
470
+ ctx.io.writeJson(result ?? { deleted: uuids });
471
+ return;
472
+ }
473
+ ctx.io.success(uuids.length === 1 ? `Alert ${uuids[0]} deleted.` : `${uuids.length} alerts deleted.`);
474
+ }
475
+ // ---------------------------------------------------------------------------
476
+ // Helpers
477
+ // ---------------------------------------------------------------------------
478
+ /** Alert IDs are UUID strings, not the numeric ids GTT and orders use. */
479
+ function requireUuid(value) {
480
+ const uuid = value?.trim();
481
+ if (!uuid)
482
+ throw new UsageError('An alert UUID is required.');
483
+ return uuid;
484
+ }
485
+ function normaliseType(value) {
486
+ const lower = value.toLowerCase();
487
+ if (lower === 'simple' || lower === 'ato')
488
+ return lower;
489
+ throw new UsageError(`Unknown alert type "${value}".`, 'Valid types: simple, ato.');
490
+ }
491
+ const OPERATOR_ALIASES = {
492
+ '>=': '>=',
493
+ ge: '>=',
494
+ above: '>=',
495
+ over: '>=',
496
+ '<=': '<=',
497
+ le: '<=',
498
+ below: '<=',
499
+ under: '<=',
500
+ '>': '>',
501
+ gt: '>',
502
+ '<': '<',
503
+ lt: '<',
504
+ '==': '==',
505
+ '=': '==',
506
+ eq: '==',
507
+ };
508
+ function normaliseOperator(value) {
509
+ const op = OPERATOR_ALIASES[value.toLowerCase()] ?? OPERATOR_ALIASES[value];
510
+ if (!op) {
511
+ throw new UsageError(`Unknown operator "${value}".`, 'Valid operators: >=, <=, >, <, == (aliases: above, below, ge, le, gt, lt, eq).');
512
+ }
513
+ return op;
514
+ }
515
+ function defaultAlertName(symbol, operator, opts) {
516
+ const rhs = opts.rhsInstrument ?? (opts.value !== undefined ? String(opts.value) : '');
517
+ return `${symbol} ${operator} ${rhs}`.trim().slice(0, 100);
518
+ }
519
+ function normalise(value, allowed, label) {
520
+ const candidate = value.toUpperCase();
521
+ if (allowed.includes(candidate))
522
+ return candidate;
523
+ throw new UsageError(`Unknown ${label} "${value}".`, `Valid values: ${allowed.join(', ')}.`);
524
+ }
525
+ function renderTableFor(ctx, rows, columns) {
526
+ return renderTable(ctx.io, rows, columns, {
527
+ compact: ctx.config.output.compact,
528
+ });
529
+ }