@pungoyal/kite-cli 0.1.1 → 0.2.1
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 +113 -6
- package/dist/commands/alerts.d.ts +12 -0
- package/dist/commands/alerts.js +529 -0
- package/dist/commands/auth.js +104 -28
- package/dist/commands/config.js +56 -13
- package/dist/commands/profiles.d.ts +10 -0
- package/dist/commands/profiles.js +213 -0
- package/dist/commands/watch.js +1 -1
- package/dist/context.d.ts +6 -0
- package/dist/context.js +36 -18
- package/dist/core/api.d.ts +249 -42
- package/dist/core/api.js +100 -1
- package/dist/core/config.d.ts +77 -23
- package/dist/core/config.js +53 -21
- package/dist/core/credentials.d.ts +3 -2
- package/dist/core/credentials.js +13 -7
- package/dist/core/errors.d.ts +1 -1
- package/dist/core/errors.js +2 -2
- package/dist/core/paths.d.ts +9 -2
- package/dist/core/paths.js +12 -3
- package/dist/core/profiles.d.ts +78 -0
- package/dist/core/profiles.js +140 -0
- package/dist/core/schemas.d.ts +78 -5
- package/dist/core/schemas.js +55 -0
- package/dist/core/session.d.ts +3 -2
- package/dist/core/session.js +7 -5
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/run.js +6 -0
- package/dist/safety.js +12 -1
- package/package.json +9 -3
|
@@ -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
|
+
}
|