@pungoyal/kite-cli 0.1.1 → 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 +81 -0
- 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 +4 -3
package/README.md
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
[](https://github.com/pungoyal/kite-cli/actions/workflows/ci.yml)
|
|
4
4
|
[](https://www.npmjs.com/package/@pungoyal/kite-cli)
|
|
5
|
+
[](https://github.com/pungoyal/kite-cli/releases/latest)
|
|
5
6
|
[](https://nodejs.org)
|
|
6
7
|
[](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
|
+
}
|