@pungoyal/kite-cli 0.1.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/LICENSE +21 -0
- package/README.md +292 -0
- package/dist/cli.d.ts +2 -0
- package/dist/cli.js +7 -0
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +265 -0
- package/dist/commands/config.d.ts +2 -0
- package/dist/commands/config.js +182 -0
- package/dist/commands/gtt.d.ts +12 -0
- package/dist/commands/gtt.js +276 -0
- package/dist/commands/market.d.ts +2 -0
- package/dist/commands/market.js +389 -0
- package/dist/commands/orders.d.ts +2 -0
- package/dist/commands/orders.js +679 -0
- package/dist/commands/portfolio.d.ts +2 -0
- package/dist/commands/portfolio.js +286 -0
- package/dist/commands/types.d.ts +16 -0
- package/dist/commands/types.js +1 -0
- package/dist/commands/watch.d.ts +18 -0
- package/dist/commands/watch.js +285 -0
- package/dist/context.d.ts +40 -0
- package/dist/context.js +126 -0
- package/dist/core/api.d.ts +843 -0
- package/dist/core/api.js +472 -0
- package/dist/core/auth.d.ts +68 -0
- package/dist/core/auth.js +220 -0
- package/dist/core/client.d.ts +76 -0
- package/dist/core/client.js +285 -0
- package/dist/core/config.d.ts +114 -0
- package/dist/core/config.js +163 -0
- package/dist/core/credentials.d.ts +27 -0
- package/dist/core/credentials.js +182 -0
- package/dist/core/errors.d.ts +83 -0
- package/dist/core/errors.js +152 -0
- package/dist/core/instruments.d.ts +87 -0
- package/dist/core/instruments.js +288 -0
- package/dist/core/paths.d.ts +11 -0
- package/dist/core/paths.js +56 -0
- package/dist/core/ratelimit.d.ts +57 -0
- package/dist/core/ratelimit.js +196 -0
- package/dist/core/redact.d.ts +48 -0
- package/dist/core/redact.js +181 -0
- package/dist/core/schemas.d.ts +662 -0
- package/dist/core/schemas.js +433 -0
- package/dist/core/secretstore.d.ts +11 -0
- package/dist/core/secretstore.js +138 -0
- package/dist/core/session.d.ts +37 -0
- package/dist/core/session.js +102 -0
- package/dist/core/ticker.d.ts +131 -0
- package/dist/core/ticker.js +367 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +17 -0
- package/dist/output/format.d.ts +31 -0
- package/dist/output/format.js +162 -0
- package/dist/output/io.d.ts +70 -0
- package/dist/output/io.js +143 -0
- package/dist/output/table.d.ts +28 -0
- package/dist/output/table.js +73 -0
- package/dist/run.d.ts +15 -0
- package/dist/run.js +161 -0
- package/dist/safety.d.ts +93 -0
- package/dist/safety.js +154 -0
- package/package.json +85 -0
|
@@ -0,0 +1,679 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { ORDER_TYPES, PRODUCTS, VALIDITIES, VARIETIES, } from '../core/api.js';
|
|
3
|
+
import { ExitCode, KiteApiError, KiteCliError, NetworkError, UsageError } from '../core/errors.js';
|
|
4
|
+
import { formatInstrumentKey, parseInstrumentKey } from '../core/instruments.js';
|
|
5
|
+
import { TERMINAL_ORDER_STATUSES } from '../core/schemas.js';
|
|
6
|
+
import { dateTime, money, quantity, rupees, timeOnly } from '../output/format.js';
|
|
7
|
+
import { heading, printTable, renderKeyValue, renderTable } from '../output/table.js';
|
|
8
|
+
import { assertTradingEnabled, buildOrderTag, confirmAction } from '../safety.js';
|
|
9
|
+
export const orderCommands = (program, run) => {
|
|
10
|
+
const orders = program.command('orders').description('View and manage orders');
|
|
11
|
+
orders
|
|
12
|
+
.command('list', { isDefault: true })
|
|
13
|
+
.description("Show today's orderbook")
|
|
14
|
+
.option('--open', 'Show only orders that are still working')
|
|
15
|
+
.action(run(listOrders));
|
|
16
|
+
orders
|
|
17
|
+
.command('get')
|
|
18
|
+
.description('Show the full state history of one order')
|
|
19
|
+
.argument('<order-id>')
|
|
20
|
+
.action(run(getOrder));
|
|
21
|
+
orders
|
|
22
|
+
.command('place')
|
|
23
|
+
.description('Place an order')
|
|
24
|
+
.argument('<instrument>', 'Instrument as EXCHANGE:SYMBOL, e.g. NSE:INFY')
|
|
25
|
+
.requiredOption('-s, --side <side>', 'BUY or SELL')
|
|
26
|
+
.requiredOption('-q, --quantity <n>', 'Quantity')
|
|
27
|
+
.option('-t, --type <type>', `Order type (${ORDER_TYPES.join(', ')})`, 'MARKET')
|
|
28
|
+
.option('-p, --price <price>', 'Limit price (required for LIMIT and SL)')
|
|
29
|
+
.option('--trigger-price <price>', 'Trigger price (required for SL and SL-M)')
|
|
30
|
+
.option('--product <product>', `Product (${PRODUCTS.join(', ')})`, 'CNC')
|
|
31
|
+
.option('--variety <variety>', `Variety (${VARIETIES.join(', ')})`, 'regular')
|
|
32
|
+
.option('--validity <validity>', `Validity (${VALIDITIES.join(', ')})`, 'DAY')
|
|
33
|
+
.option('--validity-ttl <minutes>', 'Minutes to live, for TTL validity')
|
|
34
|
+
.option('--disclosed-quantity <n>', 'Disclosed quantity')
|
|
35
|
+
.option('--iceberg-legs <n>', 'Number of iceberg legs (2-50)')
|
|
36
|
+
.option('--iceberg-quantity <n>', 'Quantity per iceberg leg')
|
|
37
|
+
.option('--tag <tag>', 'Custom tag, max 20 alphanumeric characters')
|
|
38
|
+
.action(run(placeOrder));
|
|
39
|
+
orders
|
|
40
|
+
.command('modify')
|
|
41
|
+
.description('Modify a pending order')
|
|
42
|
+
.argument('<order-id>')
|
|
43
|
+
.option('-q, --quantity <n>', 'New quantity')
|
|
44
|
+
.option('-p, --price <price>', 'New limit price')
|
|
45
|
+
.option('--trigger-price <price>', 'New trigger price')
|
|
46
|
+
.option('-t, --type <type>', `New order type (${ORDER_TYPES.join(', ')})`)
|
|
47
|
+
.option('--validity <validity>', `New validity (${VALIDITIES.join(', ')})`)
|
|
48
|
+
.option('--variety <variety>', 'Order variety (inferred from the orderbook if omitted)')
|
|
49
|
+
.action(run(modifyOrder));
|
|
50
|
+
orders
|
|
51
|
+
.command('cancel')
|
|
52
|
+
.description('Cancel a pending order')
|
|
53
|
+
.argument('<order-id>')
|
|
54
|
+
.option('--variety <variety>', 'Order variety (inferred from the orderbook if omitted)')
|
|
55
|
+
.action(run(cancelOrder));
|
|
56
|
+
program.command('trades').description("Show today's executed trades").action(run(listTrades));
|
|
57
|
+
};
|
|
58
|
+
// ---------------------------------------------------------------------------
|
|
59
|
+
// Read
|
|
60
|
+
// ---------------------------------------------------------------------------
|
|
61
|
+
async function listOrders(ctx, opts) {
|
|
62
|
+
ctx.requireSession();
|
|
63
|
+
const all = await ctx.api.getOrders(ctx.signal);
|
|
64
|
+
const rows = opts.open ? all.filter((order) => !TERMINAL_ORDER_STATUSES.has(order.status)) : all;
|
|
65
|
+
printTable(ctx.io, rows, orderColumns(), rows, {
|
|
66
|
+
compact: ctx.config.output.compact,
|
|
67
|
+
empty: opts.open ? 'No working orders.' : 'No orders today.',
|
|
68
|
+
});
|
|
69
|
+
}
|
|
70
|
+
function orderColumns() {
|
|
71
|
+
return [
|
|
72
|
+
{ header: 'Time', value: (o) => timeOnly(o.order_timestamp) },
|
|
73
|
+
{ header: 'Order ID', value: (o, io) => io.dim(o.order_id) },
|
|
74
|
+
{ header: 'Symbol', value: (o, io) => io.bold(o.tradingsymbol ?? '—') },
|
|
75
|
+
{
|
|
76
|
+
header: 'Side',
|
|
77
|
+
value: (o, io) => o.transaction_type === 'BUY' ? io.green('BUY') : o.transaction_type === 'SELL' ? io.red('SELL') : '—',
|
|
78
|
+
},
|
|
79
|
+
{ header: 'Type', value: (o) => o.order_type ?? '—' },
|
|
80
|
+
{ header: 'Product', value: (o) => o.product ?? '—' },
|
|
81
|
+
{
|
|
82
|
+
header: 'Qty',
|
|
83
|
+
value: (o) => `${quantity(o.filled_quantity ?? 0)}/${quantity(o.quantity ?? 0)}`,
|
|
84
|
+
align: 'right',
|
|
85
|
+
},
|
|
86
|
+
{
|
|
87
|
+
header: 'Price',
|
|
88
|
+
value: (o) => money(o.average_price || o.price),
|
|
89
|
+
align: 'right',
|
|
90
|
+
},
|
|
91
|
+
{ header: 'Status', value: (o, io) => colourStatus(io, o.status) },
|
|
92
|
+
];
|
|
93
|
+
}
|
|
94
|
+
function colourStatus(io, status) {
|
|
95
|
+
switch (status) {
|
|
96
|
+
case 'COMPLETE':
|
|
97
|
+
return io.green(status);
|
|
98
|
+
case 'REJECTED':
|
|
99
|
+
return io.red(status);
|
|
100
|
+
case 'CANCELLED':
|
|
101
|
+
return io.dim(status);
|
|
102
|
+
case 'OPEN':
|
|
103
|
+
case 'TRIGGER PENDING':
|
|
104
|
+
return io.yellow(status);
|
|
105
|
+
default:
|
|
106
|
+
return status;
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
async function getOrder(ctx, _opts, command) {
|
|
110
|
+
ctx.requireSession();
|
|
111
|
+
const orderId = command.args[0];
|
|
112
|
+
if (!orderId)
|
|
113
|
+
throw new UsageError('An order ID is required.');
|
|
114
|
+
const history = await ctx.api.getOrderHistory(orderId, ctx.signal);
|
|
115
|
+
if (history.length === 0) {
|
|
116
|
+
throw new KiteCliError(`No order found with ID ${orderId}.`, ExitCode.Input);
|
|
117
|
+
}
|
|
118
|
+
if (ctx.io.json) {
|
|
119
|
+
ctx.io.writeJson(history);
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const latest = history[history.length - 1];
|
|
123
|
+
const { io } = ctx;
|
|
124
|
+
io.line(heading(io, `Order ${orderId}`));
|
|
125
|
+
io.line(renderKeyValue(io, [
|
|
126
|
+
['Symbol', `${latest.exchange ?? ''}:${latest.tradingsymbol ?? '—'}`],
|
|
127
|
+
['Side', latest.transaction_type ?? '—'],
|
|
128
|
+
['Type', `${latest.order_type ?? '—'} / ${latest.product ?? '—'} / ${latest.variety ?? '—'}`],
|
|
129
|
+
['Quantity', `${quantity(latest.filled_quantity ?? 0)} filled of ${quantity(latest.quantity ?? 0)}`],
|
|
130
|
+
['Price', money(latest.price)],
|
|
131
|
+
['Average price', money(latest.average_price)],
|
|
132
|
+
['Trigger price', latest.trigger_price ? money(latest.trigger_price) : '—'],
|
|
133
|
+
['Status', colourStatus(io, latest.status)],
|
|
134
|
+
['Message', latest.status_message ?? '—'],
|
|
135
|
+
['Tag', latest.tag ?? '—'],
|
|
136
|
+
['Placed', dateTime(latest.order_timestamp)],
|
|
137
|
+
]));
|
|
138
|
+
io.line(heading(io, 'State history'));
|
|
139
|
+
io.line(renderTableFor(ctx, history, [
|
|
140
|
+
{ header: 'Time', value: (o) => timeOnly(o.order_timestamp) },
|
|
141
|
+
{ header: 'Status', value: (o, io) => colourStatus(io, o.status) },
|
|
142
|
+
{
|
|
143
|
+
header: 'Filled',
|
|
144
|
+
value: (o) => quantity(o.filled_quantity ?? 0),
|
|
145
|
+
align: 'right',
|
|
146
|
+
},
|
|
147
|
+
{
|
|
148
|
+
header: 'Pending',
|
|
149
|
+
value: (o) => quantity(o.pending_quantity ?? 0),
|
|
150
|
+
align: 'right',
|
|
151
|
+
},
|
|
152
|
+
{ header: 'Message', value: (o) => o.status_message ?? '' },
|
|
153
|
+
]));
|
|
154
|
+
const trades = await ctx.api.getOrderTrades(orderId, ctx.signal);
|
|
155
|
+
if (trades.length > 0) {
|
|
156
|
+
io.line(heading(io, 'Fills'));
|
|
157
|
+
io.line(renderTableFor(ctx, trades, tradeColumns()));
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
async function listTrades(ctx) {
|
|
161
|
+
ctx.requireSession();
|
|
162
|
+
const trades = await ctx.api.getTrades(ctx.signal);
|
|
163
|
+
printTable(ctx.io, trades, tradeColumns(), trades, {
|
|
164
|
+
compact: ctx.config.output.compact,
|
|
165
|
+
empty: 'No trades today.',
|
|
166
|
+
});
|
|
167
|
+
}
|
|
168
|
+
function tradeColumns() {
|
|
169
|
+
return [
|
|
170
|
+
{
|
|
171
|
+
header: 'Time',
|
|
172
|
+
value: (t) => timeOnly(t.fill_timestamp ?? t.exchange_timestamp),
|
|
173
|
+
},
|
|
174
|
+
{ header: 'Trade ID', value: (t, io) => io.dim(t.trade_id) },
|
|
175
|
+
{ header: 'Symbol', value: (t, io) => io.bold(t.tradingsymbol ?? '—') },
|
|
176
|
+
{
|
|
177
|
+
header: 'Side',
|
|
178
|
+
value: (t, io) => (t.transaction_type === 'BUY' ? io.green('BUY') : io.red('SELL')),
|
|
179
|
+
},
|
|
180
|
+
{ header: 'Qty', value: (t) => quantity(t.quantity), align: 'right' },
|
|
181
|
+
{ header: 'Price', value: (t) => money(t.average_price), align: 'right' },
|
|
182
|
+
{
|
|
183
|
+
header: 'Value',
|
|
184
|
+
value: (t) => money((t.average_price ?? 0) * (t.quantity ?? 0)),
|
|
185
|
+
align: 'right',
|
|
186
|
+
},
|
|
187
|
+
];
|
|
188
|
+
}
|
|
189
|
+
// ---------------------------------------------------------------------------
|
|
190
|
+
// Place
|
|
191
|
+
// ---------------------------------------------------------------------------
|
|
192
|
+
/**
|
|
193
|
+
* Commander types `.opts<T>()` as a cast rather than inferring from the
|
|
194
|
+
* `.option()` declarations, so nothing checks that this shape matches what was
|
|
195
|
+
* registered. Re-validating here turns that silent type lie into a runtime
|
|
196
|
+
* guarantee — worth it on the one command that spends money.
|
|
197
|
+
*/
|
|
198
|
+
const PlaceOptionsSchema = z.object({
|
|
199
|
+
side: z.string(),
|
|
200
|
+
quantity: z.coerce.number().int().positive(),
|
|
201
|
+
type: z.string().default('MARKET'),
|
|
202
|
+
price: z.coerce.number().positive().optional(),
|
|
203
|
+
triggerPrice: z.coerce.number().positive().optional(),
|
|
204
|
+
product: z.string().default('CNC'),
|
|
205
|
+
variety: z.string().default('regular'),
|
|
206
|
+
validity: z.string().default('DAY'),
|
|
207
|
+
validityTtl: z.coerce.number().int().positive().optional(),
|
|
208
|
+
disclosedQuantity: z.coerce.number().int().nonnegative().optional(),
|
|
209
|
+
icebergLegs: z.coerce.number().int().min(2).max(50).optional(),
|
|
210
|
+
icebergQuantity: z.coerce.number().int().positive().optional(),
|
|
211
|
+
tag: z
|
|
212
|
+
.string()
|
|
213
|
+
.max(20)
|
|
214
|
+
.regex(/^[a-zA-Z0-9]*$/, 'Tag must be alphanumeric.')
|
|
215
|
+
.optional(),
|
|
216
|
+
});
|
|
217
|
+
async function placeOrder(ctx, rawOpts, command) {
|
|
218
|
+
ctx.requireSession();
|
|
219
|
+
assertTradingEnabled(ctx);
|
|
220
|
+
const parsed = PlaceOptionsSchema.safeParse(rawOpts);
|
|
221
|
+
if (!parsed.success) {
|
|
222
|
+
throw new UsageError(`Invalid options:\n${z.prettifyError(parsed.error)}`);
|
|
223
|
+
}
|
|
224
|
+
const opts = parsed.data;
|
|
225
|
+
const instrumentArg = command.args[0];
|
|
226
|
+
if (!instrumentArg)
|
|
227
|
+
throw new UsageError('An instrument is required, e.g. `kite orders place NSE:INFY ...`.');
|
|
228
|
+
const instrument = parseInstrumentKey(instrumentArg);
|
|
229
|
+
const instrumentKey = formatInstrumentKey(instrument.exchange, instrument.tradingsymbol);
|
|
230
|
+
const side = opts.side.toUpperCase();
|
|
231
|
+
if (side !== 'BUY' && side !== 'SELL')
|
|
232
|
+
throw new UsageError('--side must be BUY or SELL.');
|
|
233
|
+
const orderType = normalise(opts.type, ORDER_TYPES, 'order type');
|
|
234
|
+
const product = normalise(opts.product, PRODUCTS, 'product');
|
|
235
|
+
const validity = normalise(opts.validity, VALIDITIES, 'validity');
|
|
236
|
+
const variety = normalise(opts.variety, VARIETIES, 'variety', false);
|
|
237
|
+
// --- input coherence ----------------------------------------------------
|
|
238
|
+
if ((orderType === 'LIMIT' || orderType === 'SL') && opts.price === undefined) {
|
|
239
|
+
throw new UsageError(`--price is required for a ${orderType} order.`);
|
|
240
|
+
}
|
|
241
|
+
if ((orderType === 'SL' || orderType === 'SL-M') && opts.triggerPrice === undefined) {
|
|
242
|
+
throw new UsageError(`--trigger-price is required for a ${orderType} order.`);
|
|
243
|
+
}
|
|
244
|
+
if (orderType === 'MARKET' && opts.price !== undefined) {
|
|
245
|
+
throw new UsageError('--price cannot be used with a MARKET order.');
|
|
246
|
+
}
|
|
247
|
+
if (validity === 'TTL' && opts.validityTtl === undefined) {
|
|
248
|
+
throw new UsageError('--validity-ttl is required when validity is TTL.');
|
|
249
|
+
}
|
|
250
|
+
if (variety === 'iceberg' && (opts.icebergLegs === undefined || opts.icebergQuantity === undefined)) {
|
|
251
|
+
throw new UsageError('--iceberg-legs and --iceberg-quantity are required for an iceberg order.');
|
|
252
|
+
}
|
|
253
|
+
if (ctx.env === 'sandbox' && orderType === 'MARKET') {
|
|
254
|
+
throw new UsageError('The sandbox does not accept MARKET orders.', 'Use --type LIMIT with a price near the last traded price.');
|
|
255
|
+
}
|
|
256
|
+
// --- resolve the instrument so the preview shows what will really trade ---
|
|
257
|
+
await ctx.instruments.load({ signal: ctx.signal }).catch(() => {
|
|
258
|
+
// A stale or missing instrument cache must not block order placement; we
|
|
259
|
+
// simply lose the enrichment below.
|
|
260
|
+
});
|
|
261
|
+
const resolved = ctx.instruments.lookupKey(instrumentKey);
|
|
262
|
+
// --- estimate notional value for the cap and confirmation escalation ------
|
|
263
|
+
// An unknown value is NOT treated as "safe": assertWithinValueCap refuses
|
|
264
|
+
// when a cap is configured, and the confirmation escalates to a typed
|
|
265
|
+
// challenge. So a failed quote lookup costs friction, never protection.
|
|
266
|
+
let referencePrice = opts.price;
|
|
267
|
+
let priceLookupFailed = false;
|
|
268
|
+
if (referencePrice === undefined) {
|
|
269
|
+
try {
|
|
270
|
+
const ltp = await ctx.api.getLtp([instrumentKey], ctx.signal);
|
|
271
|
+
referencePrice = ltp[instrumentKey]?.last_price;
|
|
272
|
+
if (referencePrice === undefined)
|
|
273
|
+
priceLookupFailed = true;
|
|
274
|
+
}
|
|
275
|
+
catch {
|
|
276
|
+
// The quote endpoint is capped at 1 req/sec, so a 429 here is routine.
|
|
277
|
+
priceLookupFailed = true;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
if (priceLookupFailed) {
|
|
281
|
+
ctx.io.warn(`Could not fetch a price for ${instrumentKey}; this order's value cannot be estimated.`);
|
|
282
|
+
}
|
|
283
|
+
const notionalValue = referencePrice !== undefined ? referencePrice * opts.quantity : undefined;
|
|
284
|
+
// A unique tag is ALWAYS set, even when the user supplied one. Kite has no
|
|
285
|
+
// idempotency key, so this is the only way to tell "the request failed" from
|
|
286
|
+
// "the request succeeded but the response was lost" — and a non-unique tag
|
|
287
|
+
// would make that check report the wrong order.
|
|
288
|
+
const tag = buildOrderTag(opts.tag);
|
|
289
|
+
await confirmAction(ctx, {
|
|
290
|
+
action: `Place ${side} order for ${opts.quantity} ${instrument.tradingsymbol}`,
|
|
291
|
+
mutatesOrders: true,
|
|
292
|
+
increasesExposure: true,
|
|
293
|
+
notionalValue,
|
|
294
|
+
challengeToken: instrument.tradingsymbol,
|
|
295
|
+
details: [
|
|
296
|
+
{ label: 'Instrument', value: instrumentKey },
|
|
297
|
+
...(resolved
|
|
298
|
+
? [
|
|
299
|
+
{
|
|
300
|
+
label: 'Resolved',
|
|
301
|
+
value: `${resolved.name ?? resolved.tradingsymbol} (token ${resolved.instrument_token})`,
|
|
302
|
+
},
|
|
303
|
+
...(resolved.lot_size && resolved.lot_size > 1
|
|
304
|
+
? [
|
|
305
|
+
{
|
|
306
|
+
label: 'Lot size',
|
|
307
|
+
value: `${resolved.lot_size} (${opts.quantity / resolved.lot_size} lots)`,
|
|
308
|
+
},
|
|
309
|
+
]
|
|
310
|
+
: []),
|
|
311
|
+
]
|
|
312
|
+
: [
|
|
313
|
+
{
|
|
314
|
+
label: 'Resolved',
|
|
315
|
+
value: ctx.io.yellow('not in the local instrument cache'),
|
|
316
|
+
},
|
|
317
|
+
]),
|
|
318
|
+
{
|
|
319
|
+
label: 'Side',
|
|
320
|
+
value: side === 'BUY' ? ctx.io.green(side) : ctx.io.red(side),
|
|
321
|
+
},
|
|
322
|
+
{ label: 'Quantity', value: quantity(opts.quantity) },
|
|
323
|
+
{ label: 'Order type', value: orderType },
|
|
324
|
+
...(opts.price !== undefined ? [{ label: 'Price', value: rupees(opts.price) }] : []),
|
|
325
|
+
...(opts.triggerPrice !== undefined ? [{ label: 'Trigger', value: rupees(opts.triggerPrice) }] : []),
|
|
326
|
+
{ label: 'Product', value: product },
|
|
327
|
+
{ label: 'Variety', value: variety },
|
|
328
|
+
{ label: 'Validity', value: validity },
|
|
329
|
+
{
|
|
330
|
+
label: 'Est. value',
|
|
331
|
+
value: notionalValue !== undefined ? rupees(notionalValue) : ctx.io.dim('unknown (no quote available)'),
|
|
332
|
+
},
|
|
333
|
+
{ label: 'Tag', value: tag },
|
|
334
|
+
],
|
|
335
|
+
});
|
|
336
|
+
if (ctx.options.dryRun)
|
|
337
|
+
return;
|
|
338
|
+
const params = {
|
|
339
|
+
variety,
|
|
340
|
+
tradingsymbol: instrument.tradingsymbol,
|
|
341
|
+
exchange: instrument.exchange,
|
|
342
|
+
transaction_type: side,
|
|
343
|
+
order_type: orderType,
|
|
344
|
+
quantity: opts.quantity,
|
|
345
|
+
product,
|
|
346
|
+
validity,
|
|
347
|
+
price: opts.price,
|
|
348
|
+
trigger_price: opts.triggerPrice,
|
|
349
|
+
disclosed_quantity: opts.disclosedQuantity,
|
|
350
|
+
validity_ttl: opts.validityTtl,
|
|
351
|
+
iceberg_legs: opts.icebergLegs,
|
|
352
|
+
iceberg_quantity: opts.icebergQuantity,
|
|
353
|
+
tag,
|
|
354
|
+
};
|
|
355
|
+
// Heads-up as this process approaches Kite's order caps; the limiter refuses
|
|
356
|
+
// outright once a cap is actually reached.
|
|
357
|
+
if (ctx.client.limiter.nearOrderLimit()) {
|
|
358
|
+
const usage = ctx.client.limiter.orderUsage();
|
|
359
|
+
ctx.io.warn(`Approaching Kite's order caps this session (${usage.minute}/min, ${usage.day}/day).`);
|
|
360
|
+
}
|
|
361
|
+
let result;
|
|
362
|
+
try {
|
|
363
|
+
result = await ctx.api.placeOrder(params, ctx.signal);
|
|
364
|
+
}
|
|
365
|
+
catch (err) {
|
|
366
|
+
// THE critical path. An ambiguous failure means the order may have reached
|
|
367
|
+
// the OMS and executed. Retrying blindly is how a CLI buys twice, so we
|
|
368
|
+
// reconcile against the tag we chose instead.
|
|
369
|
+
if (isAmbiguousFailure(err)) {
|
|
370
|
+
await reconcileAfterFailure(ctx, tag, err);
|
|
371
|
+
return;
|
|
372
|
+
}
|
|
373
|
+
throw err;
|
|
374
|
+
}
|
|
375
|
+
// With autoslice the response is an array with mixed successes and errors.
|
|
376
|
+
const orderIds = [];
|
|
377
|
+
const errors = [];
|
|
378
|
+
if (Array.isArray(result)) {
|
|
379
|
+
for (const entry of result) {
|
|
380
|
+
// Loose schemas widen these to `unknown`, and a sliced response mixes
|
|
381
|
+
// successes with errors, so check the shape rather than trusting it.
|
|
382
|
+
const orderId = entry.order_id;
|
|
383
|
+
if (typeof orderId === 'string') {
|
|
384
|
+
orderIds.push(orderId);
|
|
385
|
+
}
|
|
386
|
+
else {
|
|
387
|
+
const message = entry.error?.message;
|
|
388
|
+
errors.push(typeof message === 'string' ? message : 'Unknown slice error');
|
|
389
|
+
}
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
else {
|
|
393
|
+
orderIds.push(result.order_id);
|
|
394
|
+
}
|
|
395
|
+
// Set before any early return: a sliced order can partially fail, and a
|
|
396
|
+
// script reading only the exit code must not see success.
|
|
397
|
+
if (errors.length > 0)
|
|
398
|
+
process.exitCode = ExitCode.Order;
|
|
399
|
+
if (ctx.io.json) {
|
|
400
|
+
ctx.io.writeJson({ order_ids: orderIds, errors, tag });
|
|
401
|
+
return;
|
|
402
|
+
}
|
|
403
|
+
for (const orderId of orderIds) {
|
|
404
|
+
ctx.io.success(`Order placed: ${ctx.io.bold(orderId)}`);
|
|
405
|
+
}
|
|
406
|
+
for (const error of errors) {
|
|
407
|
+
ctx.io.error(`Slice failed: ${error}`);
|
|
408
|
+
}
|
|
409
|
+
if (orderIds.length > 0) {
|
|
410
|
+
// Acceptance by the OMS is not execution — the docs are explicit about this.
|
|
411
|
+
ctx.io.info(`Accepted by the OMS. That is not the same as executed — check with \`kite orders get ${orderIds[0]}\`.`);
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
/**
|
|
415
|
+
* Did this failure leave us unsure whether the order was executed?
|
|
416
|
+
*
|
|
417
|
+
* A socket error is the obvious case, but a 5xx is equally ambiguous: Kite's
|
|
418
|
+
* gateway can fail *after* the OMS accepted the order. Treating those as clean
|
|
419
|
+
* failures would send the user to `hintForApiError`'s "retry shortly" advice
|
|
420
|
+
* and buy twice. A 4xx is unambiguous — the request was rejected before it
|
|
421
|
+
* could execute — so those propagate normally.
|
|
422
|
+
*/
|
|
423
|
+
function isAmbiguousFailure(err) {
|
|
424
|
+
if (err instanceof NetworkError)
|
|
425
|
+
return true;
|
|
426
|
+
if (err instanceof KiteApiError) {
|
|
427
|
+
// 429 is included: Kite may have rate-limited the response, not the order.
|
|
428
|
+
return err.status >= 500 || err.status === 429;
|
|
429
|
+
}
|
|
430
|
+
return false;
|
|
431
|
+
}
|
|
432
|
+
/**
|
|
433
|
+
* Recover from an ambiguous order placement failure.
|
|
434
|
+
*
|
|
435
|
+
* Zerodha's own guidance: do NOT retry a failed POST /orders. Fetch the
|
|
436
|
+
* orderbook and look for the tag; only place again if it is absent.
|
|
437
|
+
*/
|
|
438
|
+
async function reconcileAfterFailure(ctx, tag, cause) {
|
|
439
|
+
const { io } = ctx;
|
|
440
|
+
io.warn(`The order request failed: ${cause.message}`);
|
|
441
|
+
io.info('Checking whether it reached Kite anyway…');
|
|
442
|
+
try {
|
|
443
|
+
const matches = await ctx.api.findOrderByTag(tag, ctx.signal);
|
|
444
|
+
if (matches.length > 0) {
|
|
445
|
+
const order = matches[0];
|
|
446
|
+
if (io.json) {
|
|
447
|
+
ctx.io.writeJson({
|
|
448
|
+
order_ids: matches.map((o) => o.order_id),
|
|
449
|
+
tag,
|
|
450
|
+
reconciled: true,
|
|
451
|
+
});
|
|
452
|
+
return;
|
|
453
|
+
}
|
|
454
|
+
io.warn(`The order DID reach Kite: ${io.bold(order.order_id)} (${order.status}).`);
|
|
455
|
+
io.info('It was not placed twice. Do not re-run this command.');
|
|
456
|
+
process.exitCode = ExitCode.Upstream;
|
|
457
|
+
return;
|
|
458
|
+
}
|
|
459
|
+
if (io.json) {
|
|
460
|
+
ctx.io.writeJson({ order_ids: [], tag, reconciled: true, placed: false });
|
|
461
|
+
process.exitCode = ExitCode.Upstream;
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
io.warn(`No order was found for tag ${tag}. It looks like the order did not reach Kite.`);
|
|
465
|
+
io.info(`Verify with \`kite orders list\` before retrying, in case it simply has not appeared yet.`);
|
|
466
|
+
process.exitCode = ExitCode.Upstream;
|
|
467
|
+
}
|
|
468
|
+
catch {
|
|
469
|
+
// Reconciliation itself failed — the worst case. Be explicit that we do
|
|
470
|
+
// not know, rather than guessing.
|
|
471
|
+
io.error('Could not reach Kite to check whether the order was placed.');
|
|
472
|
+
io.error(`Check your orderbook manually for tag ${io.bold(tag)} BEFORE retrying.`);
|
|
473
|
+
process.exitCode = ExitCode.Upstream;
|
|
474
|
+
}
|
|
475
|
+
}
|
|
476
|
+
// ---------------------------------------------------------------------------
|
|
477
|
+
// Modify / cancel
|
|
478
|
+
// ---------------------------------------------------------------------------
|
|
479
|
+
async function modifyOrder(ctx, opts, command) {
|
|
480
|
+
ctx.requireSession();
|
|
481
|
+
assertTradingEnabled(ctx);
|
|
482
|
+
const orderId = command.args[0];
|
|
483
|
+
if (!orderId)
|
|
484
|
+
throw new UsageError('An order ID is required.');
|
|
485
|
+
const lookup = await findOrder(ctx, orderId);
|
|
486
|
+
const variety = resolveVariety(lookup, opts.variety, orderId, 'Modifying');
|
|
487
|
+
const existing = lookup.status === 'found' ? lookup.order : undefined;
|
|
488
|
+
const changes = [];
|
|
489
|
+
const params = {
|
|
490
|
+
variety,
|
|
491
|
+
order_id: orderId,
|
|
492
|
+
};
|
|
493
|
+
if (opts.quantity !== undefined) {
|
|
494
|
+
params.quantity = requirePositiveInt(opts.quantity, '--quantity');
|
|
495
|
+
changes.push({
|
|
496
|
+
label: 'Quantity',
|
|
497
|
+
value: `${quantity(existing?.quantity)} → ${quantity(params.quantity)}`,
|
|
498
|
+
});
|
|
499
|
+
}
|
|
500
|
+
if (opts.price !== undefined) {
|
|
501
|
+
params.price = requirePositiveNumber(opts.price, '--price');
|
|
502
|
+
changes.push({
|
|
503
|
+
label: 'Price',
|
|
504
|
+
value: `${money(existing?.price)} → ${money(params.price)}`,
|
|
505
|
+
});
|
|
506
|
+
}
|
|
507
|
+
if (opts.triggerPrice !== undefined) {
|
|
508
|
+
params.trigger_price = requirePositiveNumber(opts.triggerPrice, '--trigger-price');
|
|
509
|
+
changes.push({
|
|
510
|
+
label: 'Trigger',
|
|
511
|
+
value: `${money(existing?.trigger_price)} → ${money(params.trigger_price)}`,
|
|
512
|
+
});
|
|
513
|
+
}
|
|
514
|
+
if (opts.type !== undefined) {
|
|
515
|
+
params.order_type = normalise(opts.type, ORDER_TYPES, 'order type');
|
|
516
|
+
changes.push({
|
|
517
|
+
label: 'Order type',
|
|
518
|
+
value: `${existing?.order_type ?? '—'} → ${params.order_type}`,
|
|
519
|
+
});
|
|
520
|
+
}
|
|
521
|
+
if (opts.validity !== undefined) {
|
|
522
|
+
params.validity = normalise(opts.validity, VALIDITIES, 'validity');
|
|
523
|
+
changes.push({
|
|
524
|
+
label: 'Validity',
|
|
525
|
+
value: `${existing?.validity ?? '—'} → ${params.validity}`,
|
|
526
|
+
});
|
|
527
|
+
}
|
|
528
|
+
if (changes.length === 0) {
|
|
529
|
+
throw new UsageError('Nothing to modify. Pass at least one of --quantity, --price, --trigger-price, --type or --validity.');
|
|
530
|
+
}
|
|
531
|
+
// Price the modified order for the cap and escalation checks.
|
|
532
|
+
//
|
|
533
|
+
// Falling back to `?? 0` would silently produce a notional of 0 for any
|
|
534
|
+
// order with no limit price (MARKET, SL-M) — and a 0 reads as "small", which
|
|
535
|
+
// is exactly backwards. Resolve a real reference price, falling back to the
|
|
536
|
+
// last traded price, and leave it UNDEFINED if we genuinely cannot tell so
|
|
537
|
+
// the safety layer fails closed.
|
|
538
|
+
const effectiveQuantity = params.quantity ?? existing?.quantity;
|
|
539
|
+
let effectivePrice = params.price ?? (existing?.price || undefined);
|
|
540
|
+
if (effectivePrice === undefined && existing?.tradingsymbol && existing.exchange) {
|
|
541
|
+
const key = formatInstrumentKey(existing.exchange, existing.tradingsymbol);
|
|
542
|
+
try {
|
|
543
|
+
const ltp = await ctx.api.getLtp([key], ctx.signal);
|
|
544
|
+
effectivePrice = ltp[key]?.last_price;
|
|
545
|
+
}
|
|
546
|
+
catch {
|
|
547
|
+
// Leave undefined — confirmAction escalates rather than waving it through.
|
|
548
|
+
}
|
|
549
|
+
}
|
|
550
|
+
const notionalValue = effectiveQuantity !== undefined && effectivePrice !== undefined && effectivePrice > 0
|
|
551
|
+
? effectiveQuantity * effectivePrice
|
|
552
|
+
: undefined;
|
|
553
|
+
// Only a modify that actually RAISES exposure — a bigger quantity, a higher
|
|
554
|
+
// limit price, or a switch to a less-bounded order type — should be subject to
|
|
555
|
+
// the value cap. Lowering the quantity or the price reduces exposure, and
|
|
556
|
+
// blocking that because the cap could not be priced would be the same safety
|
|
557
|
+
// inversion as blocking a cancel. When the existing order is unknown we cannot
|
|
558
|
+
// prove it is a reduction, so we fail closed and treat it as increasing.
|
|
559
|
+
const raisesQuantity = params.quantity !== undefined && (existing?.quantity === undefined || params.quantity > existing.quantity);
|
|
560
|
+
const raisesPrice = params.price !== undefined && (existing?.price === undefined || params.price > existing.price);
|
|
561
|
+
const loosensType = params.order_type === 'MARKET' || params.order_type === 'SL-M';
|
|
562
|
+
const increasesExposure = raisesQuantity || raisesPrice || loosensType;
|
|
563
|
+
await confirmAction(ctx, {
|
|
564
|
+
action: `Modify order ${orderId}`,
|
|
565
|
+
mutatesOrders: true,
|
|
566
|
+
increasesExposure,
|
|
567
|
+
notionalValue,
|
|
568
|
+
challengeToken: existing?.tradingsymbol,
|
|
569
|
+
details: [
|
|
570
|
+
{ label: 'Order ID', value: orderId },
|
|
571
|
+
{
|
|
572
|
+
label: 'Symbol',
|
|
573
|
+
value: existing ? `${existing.exchange}:${existing.tradingsymbol}` : 'unknown',
|
|
574
|
+
},
|
|
575
|
+
{ label: 'Status', value: existing?.status ?? 'unknown' },
|
|
576
|
+
{ label: 'Variety', value: variety },
|
|
577
|
+
...changes,
|
|
578
|
+
],
|
|
579
|
+
});
|
|
580
|
+
if (ctx.options.dryRun)
|
|
581
|
+
return;
|
|
582
|
+
const result = await ctx.api.modifyOrder(params, ctx.signal);
|
|
583
|
+
if (ctx.io.json) {
|
|
584
|
+
ctx.io.writeJson(result);
|
|
585
|
+
return;
|
|
586
|
+
}
|
|
587
|
+
ctx.io.success(`Order ${result.order_id} modified.`);
|
|
588
|
+
// Kite hard-caps modifications; past 25 you must cancel and re-place.
|
|
589
|
+
ctx.io.info('Kite allows at most 25 modifications per order.');
|
|
590
|
+
}
|
|
591
|
+
async function cancelOrder(ctx, opts, command) {
|
|
592
|
+
ctx.requireSession();
|
|
593
|
+
assertTradingEnabled(ctx);
|
|
594
|
+
const orderId = command.args[0];
|
|
595
|
+
if (!orderId)
|
|
596
|
+
throw new UsageError('An order ID is required.');
|
|
597
|
+
const lookup = await findOrder(ctx, orderId);
|
|
598
|
+
const variety = resolveVariety(lookup, opts.variety, orderId, 'Cancelling');
|
|
599
|
+
const existing = lookup.status === 'found' ? lookup.order : undefined;
|
|
600
|
+
if (existing && TERMINAL_ORDER_STATUSES.has(existing.status)) {
|
|
601
|
+
throw new KiteCliError(`Order ${orderId} is already ${existing.status} and cannot be cancelled.`, ExitCode.Input);
|
|
602
|
+
}
|
|
603
|
+
await confirmAction(ctx, {
|
|
604
|
+
action: `Cancel order ${orderId}`,
|
|
605
|
+
mutatesOrders: true,
|
|
606
|
+
details: [
|
|
607
|
+
{ label: 'Order ID', value: orderId },
|
|
608
|
+
{
|
|
609
|
+
label: 'Symbol',
|
|
610
|
+
value: existing ? `${existing.exchange}:${existing.tradingsymbol}` : 'unknown',
|
|
611
|
+
},
|
|
612
|
+
{ label: 'Side', value: existing?.transaction_type ?? 'unknown' },
|
|
613
|
+
{ label: 'Quantity', value: quantity(existing?.quantity) },
|
|
614
|
+
{ label: 'Status', value: existing?.status ?? 'unknown' },
|
|
615
|
+
{ label: 'Variety', value: variety },
|
|
616
|
+
],
|
|
617
|
+
});
|
|
618
|
+
if (ctx.options.dryRun)
|
|
619
|
+
return;
|
|
620
|
+
const result = await ctx.api.cancelOrder({
|
|
621
|
+
variety,
|
|
622
|
+
order_id: orderId,
|
|
623
|
+
...(existing?.parent_order_id ? { parent_order_id: existing.parent_order_id } : {}),
|
|
624
|
+
}, ctx.signal);
|
|
625
|
+
if (ctx.io.json) {
|
|
626
|
+
ctx.io.writeJson(result);
|
|
627
|
+
return;
|
|
628
|
+
}
|
|
629
|
+
ctx.io.success(`Order ${result.order_id} cancelled.`);
|
|
630
|
+
}
|
|
631
|
+
async function findOrder(ctx, orderId) {
|
|
632
|
+
try {
|
|
633
|
+
const orders = await ctx.api.getOrders(ctx.signal);
|
|
634
|
+
const order = orders.find((candidate) => candidate.order_id === orderId);
|
|
635
|
+
return order ? { status: 'found', order } : { status: 'not-found' };
|
|
636
|
+
}
|
|
637
|
+
catch (error) {
|
|
638
|
+
return { status: 'lookup-failed', error };
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
/**
|
|
642
|
+
* Resolve the variety for a mutating operation on an existing order.
|
|
643
|
+
*
|
|
644
|
+
* Fails closed when the orderbook could not be read and the user did not say
|
|
645
|
+
* which variety it is — guessing 'regular' would target the wrong endpoint.
|
|
646
|
+
*/
|
|
647
|
+
function resolveVariety(lookup, explicit, orderId, verb) {
|
|
648
|
+
if (explicit)
|
|
649
|
+
return normalise(explicit, VARIETIES, 'variety', false);
|
|
650
|
+
if (lookup.status === 'found')
|
|
651
|
+
return (lookup.order.variety ?? 'regular');
|
|
652
|
+
if (lookup.status === 'lookup-failed') {
|
|
653
|
+
throw new KiteCliError(`Could not read the orderbook, so the variety of order ${orderId} is unknown.`, ExitCode.Upstream, `Retry, or pass --variety explicitly if you know it. ${verb} an order at the wrong variety endpoint fails or targets the wrong order.`);
|
|
654
|
+
}
|
|
655
|
+
throw new KiteCliError(`Order ${orderId} is not in today's orderbook.`, ExitCode.Input, 'Check the ID with `kite orders list`.');
|
|
656
|
+
}
|
|
657
|
+
function normalise(value, allowed, label, upper = true) {
|
|
658
|
+
const candidate = upper ? value.toUpperCase() : value.toLowerCase();
|
|
659
|
+
if (allowed.includes(candidate))
|
|
660
|
+
return candidate;
|
|
661
|
+
throw new UsageError(`Unknown ${label} "${value}".`, `Valid values: ${allowed.join(', ')}.`);
|
|
662
|
+
}
|
|
663
|
+
function requirePositiveInt(value, flag) {
|
|
664
|
+
const n = Number(value);
|
|
665
|
+
if (!Number.isInteger(n) || n <= 0)
|
|
666
|
+
throw new UsageError(`${flag} must be a positive whole number.`);
|
|
667
|
+
return n;
|
|
668
|
+
}
|
|
669
|
+
function requirePositiveNumber(value, flag) {
|
|
670
|
+
const n = Number(value);
|
|
671
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
672
|
+
throw new UsageError(`${flag} must be a positive number.`);
|
|
673
|
+
return n;
|
|
674
|
+
}
|
|
675
|
+
function renderTableFor(ctx, rows, columns) {
|
|
676
|
+
return renderTable(ctx.io, rows, columns, {
|
|
677
|
+
compact: ctx.config.output.compact,
|
|
678
|
+
});
|
|
679
|
+
}
|