@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,389 @@
|
|
|
1
|
+
import { MAX_DAYS_PER_REQUEST, parseInterval } from '../core/api.js';
|
|
2
|
+
import { UsageError } from '../core/errors.js';
|
|
3
|
+
import { formatInstrumentKey, parseInstrumentKey } from '../core/instruments.js';
|
|
4
|
+
import { compactNumber, dateTime, money, parseUserDate, percent, quantity, timeOnly } from '../output/format.js';
|
|
5
|
+
import { heading, printTable, renderKeyValue, renderTable } from '../output/table.js';
|
|
6
|
+
export const marketCommands = (program, run) => {
|
|
7
|
+
program
|
|
8
|
+
.command('quote')
|
|
9
|
+
.description('Show full quotes with market depth')
|
|
10
|
+
.argument('<instruments...>', 'One or more instruments, e.g. NSE:INFY NSE:TCS')
|
|
11
|
+
.option('--depth', 'Show the full 5-level order book')
|
|
12
|
+
.action(run(quoteCommand));
|
|
13
|
+
program
|
|
14
|
+
.command('ltp')
|
|
15
|
+
.description('Show last traded prices (fastest quote endpoint)')
|
|
16
|
+
.argument('<instruments...>', 'One or more instruments, e.g. NSE:INFY')
|
|
17
|
+
.action(run(ltpCommand));
|
|
18
|
+
program
|
|
19
|
+
.command('ohlc')
|
|
20
|
+
.description('Show open/high/low/close plus last price')
|
|
21
|
+
.argument('<instruments...>', 'One or more instruments')
|
|
22
|
+
.action(run(ohlcCommand));
|
|
23
|
+
program
|
|
24
|
+
.command('history')
|
|
25
|
+
.description('Fetch historical candles')
|
|
26
|
+
.argument('<instrument>', 'Instrument as EXCHANGE:SYMBOL')
|
|
27
|
+
.option('-i, --interval <interval>', 'Candle interval', 'day')
|
|
28
|
+
.option('--from <date>', 'Start date (YYYY-MM-DD or a relative offset like 30d)', '30d')
|
|
29
|
+
.option('--to <date>', 'End date (YYYY-MM-DD)', 'today')
|
|
30
|
+
.option('--oi', 'Include open interest')
|
|
31
|
+
.option('--continuous', 'Stitch expired contracts (futures only)')
|
|
32
|
+
.option('--csv', 'Emit CSV instead of a table')
|
|
33
|
+
.option('--limit <n>', 'Show only the last N candles in table view', '30')
|
|
34
|
+
.action(run(historyCommand));
|
|
35
|
+
const instruments = program.command('instruments').description('Browse the instrument master');
|
|
36
|
+
instruments
|
|
37
|
+
.command('search')
|
|
38
|
+
.description('Search instruments by symbol or name')
|
|
39
|
+
.argument('<query>', 'Search text, e.g. INFY or "nifty bank"')
|
|
40
|
+
.option('-e, --exchange <exchange>', 'Filter by exchange, e.g. NSE, NFO')
|
|
41
|
+
.option('-t, --type <type>', 'Filter by instrument type, e.g. EQ, FUT, CE, PE')
|
|
42
|
+
.option('-n, --limit <n>', 'Maximum results', '25')
|
|
43
|
+
.action(run(searchCommand));
|
|
44
|
+
instruments.command('refresh').description('Re-download the instrument master').action(run(refreshCommand));
|
|
45
|
+
};
|
|
46
|
+
async function quoteCommand(ctx, opts, command) {
|
|
47
|
+
ctx.requireSession();
|
|
48
|
+
const keys = normaliseKeys(command.args);
|
|
49
|
+
const quotes = await ctx.api.getQuote(keys, ctx.signal);
|
|
50
|
+
if (ctx.io.json) {
|
|
51
|
+
ctx.io.writeJson(quotes);
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const { io } = ctx;
|
|
55
|
+
warnAboutMissing(ctx, keys, quotes);
|
|
56
|
+
const entries = keys.map((key) => [key, quotes[key]]).filter(([, q]) => q !== undefined);
|
|
57
|
+
if (!opts.depth && entries.length > 1) {
|
|
58
|
+
const rows = entries.map(([key, q]) => ({ key, quote: q }));
|
|
59
|
+
const columns = [
|
|
60
|
+
{ header: 'Instrument', value: (r, io) => io.bold(r.key) },
|
|
61
|
+
{
|
|
62
|
+
header: 'LTP',
|
|
63
|
+
value: (r) => money(r.quote.last_price),
|
|
64
|
+
align: 'right',
|
|
65
|
+
},
|
|
66
|
+
{
|
|
67
|
+
header: 'Change',
|
|
68
|
+
value: (r, io) => {
|
|
69
|
+
const change = changePercent(r.quote);
|
|
70
|
+
return io.signed(change ?? 0, percent(change));
|
|
71
|
+
},
|
|
72
|
+
align: 'right',
|
|
73
|
+
},
|
|
74
|
+
{
|
|
75
|
+
header: 'Open',
|
|
76
|
+
value: (r) => money(r.quote.ohlc?.open),
|
|
77
|
+
align: 'right',
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
header: 'High',
|
|
81
|
+
value: (r) => money(r.quote.ohlc?.high),
|
|
82
|
+
align: 'right',
|
|
83
|
+
},
|
|
84
|
+
{ header: 'Low', value: (r) => money(r.quote.ohlc?.low), align: 'right' },
|
|
85
|
+
{
|
|
86
|
+
header: 'Close',
|
|
87
|
+
value: (r) => money(r.quote.ohlc?.close),
|
|
88
|
+
align: 'right',
|
|
89
|
+
},
|
|
90
|
+
{
|
|
91
|
+
header: 'Volume',
|
|
92
|
+
value: (r) => compactNumber(r.quote.volume),
|
|
93
|
+
align: 'right',
|
|
94
|
+
},
|
|
95
|
+
];
|
|
96
|
+
io.line(renderTableOrEmpty(ctx, rows, columns));
|
|
97
|
+
return;
|
|
98
|
+
}
|
|
99
|
+
for (const [key, quote] of entries) {
|
|
100
|
+
renderQuoteDetail(ctx, key, quote, opts.depth ?? false);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
function renderQuoteDetail(ctx, key, quote, showDepth) {
|
|
104
|
+
const { io } = ctx;
|
|
105
|
+
const change = changePercent(quote);
|
|
106
|
+
io.line(heading(io, key));
|
|
107
|
+
io.line(renderKeyValue(io, [
|
|
108
|
+
['Last price', io.bold(money(quote.last_price))],
|
|
109
|
+
['Change', io.signed(change ?? 0, `${percent(change)} (${money(quote.net_change)})`)],
|
|
110
|
+
['Open', money(quote.ohlc?.open)],
|
|
111
|
+
['High', money(quote.ohlc?.high)],
|
|
112
|
+
['Low', money(quote.ohlc?.low)],
|
|
113
|
+
['Prev close', money(quote.ohlc?.close)],
|
|
114
|
+
['Volume', compactNumber(quote.volume)],
|
|
115
|
+
['Avg price', money(quote.average_price)],
|
|
116
|
+
['Buy qty', quantity(quote.buy_quantity)],
|
|
117
|
+
['Sell qty', quantity(quote.sell_quantity)],
|
|
118
|
+
...(quote.oi !== undefined ? [['Open interest', compactNumber(quote.oi)]] : []),
|
|
119
|
+
['Circuit', `${money(quote.lower_circuit_limit)} – ${money(quote.upper_circuit_limit)}`],
|
|
120
|
+
['Last trade', dateTime(quote.last_trade_time)],
|
|
121
|
+
]));
|
|
122
|
+
if (showDepth && quote.depth) {
|
|
123
|
+
const rows = Array.from({ length: 5 }, (_, i) => ({
|
|
124
|
+
bid: quote.depth.buy[i],
|
|
125
|
+
ask: quote.depth.sell[i],
|
|
126
|
+
}));
|
|
127
|
+
io.line('');
|
|
128
|
+
io.line(renderTableOrEmpty(ctx, rows, [
|
|
129
|
+
{
|
|
130
|
+
header: 'Bid Qty',
|
|
131
|
+
value: (r) => quantity(r.bid?.quantity),
|
|
132
|
+
align: 'right',
|
|
133
|
+
},
|
|
134
|
+
{
|
|
135
|
+
header: 'Orders',
|
|
136
|
+
value: (r) => quantity(r.bid?.orders),
|
|
137
|
+
align: 'right',
|
|
138
|
+
},
|
|
139
|
+
{
|
|
140
|
+
header: 'Bid',
|
|
141
|
+
value: (r, io) => io.green(money(r.bid?.price)),
|
|
142
|
+
align: 'right',
|
|
143
|
+
},
|
|
144
|
+
{
|
|
145
|
+
header: 'Ask',
|
|
146
|
+
value: (r, io) => io.red(money(r.ask?.price)),
|
|
147
|
+
align: 'right',
|
|
148
|
+
},
|
|
149
|
+
{
|
|
150
|
+
header: 'Orders',
|
|
151
|
+
value: (r) => quantity(r.ask?.orders),
|
|
152
|
+
align: 'right',
|
|
153
|
+
},
|
|
154
|
+
{
|
|
155
|
+
header: 'Ask Qty',
|
|
156
|
+
value: (r) => quantity(r.ask?.quantity),
|
|
157
|
+
align: 'right',
|
|
158
|
+
},
|
|
159
|
+
]));
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
async function ltpCommand(ctx, _opts, command) {
|
|
163
|
+
ctx.requireSession();
|
|
164
|
+
const keys = normaliseKeys(command.args);
|
|
165
|
+
const quotes = await ctx.api.getLtp(keys, ctx.signal);
|
|
166
|
+
if (ctx.io.json) {
|
|
167
|
+
ctx.io.writeJson(quotes);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
warnAboutMissing(ctx, keys, quotes);
|
|
171
|
+
const rows = keys
|
|
172
|
+
.map((key) => ({ key, quote: quotes[key] }))
|
|
173
|
+
.filter((row) => row.quote !== undefined);
|
|
174
|
+
ctx.io.line(renderTableOrEmpty(ctx, rows, [
|
|
175
|
+
{ header: 'Instrument', value: (r, io) => io.bold(r.key) },
|
|
176
|
+
{
|
|
177
|
+
header: 'LTP',
|
|
178
|
+
value: (r) => money(r.quote.last_price),
|
|
179
|
+
align: 'right',
|
|
180
|
+
},
|
|
181
|
+
]));
|
|
182
|
+
}
|
|
183
|
+
async function ohlcCommand(ctx, _opts, command) {
|
|
184
|
+
ctx.requireSession();
|
|
185
|
+
const keys = normaliseKeys(command.args);
|
|
186
|
+
const quotes = await ctx.api.getOhlc(keys, ctx.signal);
|
|
187
|
+
if (ctx.io.json) {
|
|
188
|
+
ctx.io.writeJson(quotes);
|
|
189
|
+
return;
|
|
190
|
+
}
|
|
191
|
+
warnAboutMissing(ctx, keys, quotes);
|
|
192
|
+
const rows = keys
|
|
193
|
+
.map((key) => ({ key, quote: quotes[key] }))
|
|
194
|
+
.filter((row) => row.quote !== undefined);
|
|
195
|
+
ctx.io.line(renderTableOrEmpty(ctx, rows, [
|
|
196
|
+
{ header: 'Instrument', value: (r, io) => io.bold(r.key) },
|
|
197
|
+
{
|
|
198
|
+
header: 'LTP',
|
|
199
|
+
value: (r) => money(r.quote.last_price),
|
|
200
|
+
align: 'right',
|
|
201
|
+
},
|
|
202
|
+
{
|
|
203
|
+
header: 'Open',
|
|
204
|
+
value: (r) => money(r.quote.ohlc?.open),
|
|
205
|
+
align: 'right',
|
|
206
|
+
},
|
|
207
|
+
{
|
|
208
|
+
header: 'High',
|
|
209
|
+
value: (r) => money(r.quote.ohlc?.high),
|
|
210
|
+
align: 'right',
|
|
211
|
+
},
|
|
212
|
+
{ header: 'Low', value: (r) => money(r.quote.ohlc?.low), align: 'right' },
|
|
213
|
+
{
|
|
214
|
+
header: 'Close',
|
|
215
|
+
value: (r) => money(r.quote.ohlc?.close),
|
|
216
|
+
align: 'right',
|
|
217
|
+
},
|
|
218
|
+
{
|
|
219
|
+
header: 'Change',
|
|
220
|
+
value: (r, io) => {
|
|
221
|
+
const close = r.quote.ohlc?.close ?? 0;
|
|
222
|
+
const change = close === 0 ? undefined : ((r.quote.last_price - close) / close) * 100;
|
|
223
|
+
return io.signed(change ?? 0, percent(change));
|
|
224
|
+
},
|
|
225
|
+
align: 'right',
|
|
226
|
+
},
|
|
227
|
+
]));
|
|
228
|
+
}
|
|
229
|
+
async function historyCommand(ctx, opts, command) {
|
|
230
|
+
ctx.requireSession();
|
|
231
|
+
const key = command.args[0];
|
|
232
|
+
if (!key)
|
|
233
|
+
throw new UsageError('An instrument is required, e.g. `kite history NSE:INFY`.');
|
|
234
|
+
const interval = parseInterval(opts.interval ?? 'day');
|
|
235
|
+
const from = parseUserDate(opts.from ?? '30d');
|
|
236
|
+
const to = parseUserDate(opts.to ?? 'today');
|
|
237
|
+
if (!from)
|
|
238
|
+
throw new UsageError(`Could not parse --from "${opts.from}".`);
|
|
239
|
+
if (!to)
|
|
240
|
+
throw new UsageError(`Could not parse --to "${opts.to}".`);
|
|
241
|
+
await ctx.instruments.load({ signal: ctx.signal });
|
|
242
|
+
// Historical data accepts ONLY the numeric instrument token, never a symbol.
|
|
243
|
+
const token = ctx.instruments.requireToken(key);
|
|
244
|
+
const spanDays = Math.ceil((to.getTime() - from.getTime()) / 86_400_000);
|
|
245
|
+
const perRequest = MAX_DAYS_PER_REQUEST[interval];
|
|
246
|
+
if (spanDays > perRequest && !ctx.io.json) {
|
|
247
|
+
const requests = Math.ceil(spanDays / perRequest);
|
|
248
|
+
ctx.io.info(`Range spans ${spanDays} days; Kite allows ${perRequest} per request at ${interval}. ` +
|
|
249
|
+
`Fetching in ${requests} chunks (rate limited to 3/sec).`);
|
|
250
|
+
}
|
|
251
|
+
const candles = await ctx.api.getHistorical({
|
|
252
|
+
instrument_token: token,
|
|
253
|
+
interval,
|
|
254
|
+
from,
|
|
255
|
+
to,
|
|
256
|
+
oi: opts.oi ?? false,
|
|
257
|
+
continuous: opts.continuous ?? false,
|
|
258
|
+
}, ctx.signal);
|
|
259
|
+
if (ctx.io.json) {
|
|
260
|
+
ctx.io.writeJson({
|
|
261
|
+
instrument: key,
|
|
262
|
+
instrument_token: token,
|
|
263
|
+
interval,
|
|
264
|
+
candles,
|
|
265
|
+
});
|
|
266
|
+
return;
|
|
267
|
+
}
|
|
268
|
+
if (opts.csv) {
|
|
269
|
+
ctx.io.line(`timestamp,open,high,low,close,volume${opts.oi ? ',oi' : ''}`);
|
|
270
|
+
for (const candle of candles) {
|
|
271
|
+
ctx.io.line(candle.join(','));
|
|
272
|
+
}
|
|
273
|
+
return;
|
|
274
|
+
}
|
|
275
|
+
const limit = Number(opts.limit ?? 30);
|
|
276
|
+
const shown = Number.isFinite(limit) && limit > 0 ? candles.slice(-limit) : candles;
|
|
277
|
+
const columns = [
|
|
278
|
+
{ header: 'Date', value: (c) => formatCandleTime(c[0], interval) },
|
|
279
|
+
{ header: 'Open', value: (c) => money(c[1]), align: 'right' },
|
|
280
|
+
{ header: 'High', value: (c) => money(c[2]), align: 'right' },
|
|
281
|
+
{ header: 'Low', value: (c) => money(c[3]), align: 'right' },
|
|
282
|
+
{
|
|
283
|
+
header: 'Close',
|
|
284
|
+
value: (c, io) => io.signed(c[4] - c[1], money(c[4])),
|
|
285
|
+
align: 'right',
|
|
286
|
+
},
|
|
287
|
+
{ header: 'Volume', value: (c) => compactNumber(c[5]), align: 'right' },
|
|
288
|
+
...(opts.oi
|
|
289
|
+
? [
|
|
290
|
+
{
|
|
291
|
+
header: 'OI',
|
|
292
|
+
value: (c) => compactNumber(c[6]),
|
|
293
|
+
align: 'right',
|
|
294
|
+
},
|
|
295
|
+
]
|
|
296
|
+
: []),
|
|
297
|
+
];
|
|
298
|
+
ctx.io.line(renderTableOrEmpty(ctx, shown, columns));
|
|
299
|
+
if (candles.length > shown.length) {
|
|
300
|
+
ctx.io.info(`Showing the last ${shown.length} of ${candles.length} candles. Use --limit 0 for all, or --csv.`);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
function formatCandleTime(timestamp, interval) {
|
|
304
|
+
return interval === 'day' ? timestamp.slice(0, 10) : `${timestamp.slice(0, 10)} ${timeOnly(timestamp)}`;
|
|
305
|
+
}
|
|
306
|
+
async function searchCommand(ctx, opts, command) {
|
|
307
|
+
ctx.requireSession();
|
|
308
|
+
const query = command.args.join(' ');
|
|
309
|
+
await ctx.instruments.load({ signal: ctx.signal });
|
|
310
|
+
const results = ctx.instruments.search(query, {
|
|
311
|
+
exchange: opts.exchange?.toUpperCase(),
|
|
312
|
+
type: opts.type?.toUpperCase(),
|
|
313
|
+
limit: Number(opts.limit ?? 25),
|
|
314
|
+
});
|
|
315
|
+
const columns = [
|
|
316
|
+
{
|
|
317
|
+
header: 'Instrument',
|
|
318
|
+
value: (i, io) => io.bold(formatInstrumentKey(i.exchange, i.tradingsymbol)),
|
|
319
|
+
},
|
|
320
|
+
{ header: 'Name', value: (i) => i.name ?? '—' },
|
|
321
|
+
{ header: 'Type', value: (i) => i.instrument_type ?? '—' },
|
|
322
|
+
{ header: 'Expiry', value: (i) => i.expiry || '—' },
|
|
323
|
+
{
|
|
324
|
+
header: 'Strike',
|
|
325
|
+
value: (i) => (i.strike ? money(i.strike) : '—'),
|
|
326
|
+
align: 'right',
|
|
327
|
+
},
|
|
328
|
+
{ header: 'Lot', value: (i) => quantity(i.lot_size), align: 'right' },
|
|
329
|
+
{
|
|
330
|
+
header: 'Token',
|
|
331
|
+
value: (i, io) => io.dim(String(i.instrument_token)),
|
|
332
|
+
align: 'right',
|
|
333
|
+
},
|
|
334
|
+
];
|
|
335
|
+
printTable(ctx.io, results, columns, results, {
|
|
336
|
+
compact: ctx.config.output.compact,
|
|
337
|
+
empty: `No instruments matched "${query}".`,
|
|
338
|
+
});
|
|
339
|
+
}
|
|
340
|
+
async function refreshCommand(ctx) {
|
|
341
|
+
ctx.requireSession();
|
|
342
|
+
ctx.io.info('Downloading the instrument master (this is a few MB)…');
|
|
343
|
+
await ctx.instruments.load({ force: true, signal: ctx.signal });
|
|
344
|
+
if (ctx.io.json) {
|
|
345
|
+
ctx.io.writeJson({ refreshed: true, count: ctx.instruments.size });
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
348
|
+
ctx.io.success(`Cached ${ctx.instruments.size.toLocaleString('en-IN')} instruments.`);
|
|
349
|
+
ctx.io.info('Kite regenerates this list once a day, around 08:30 IST.');
|
|
350
|
+
}
|
|
351
|
+
// ---------------------------------------------------------------------------
|
|
352
|
+
function normaliseKeys(args) {
|
|
353
|
+
if (args.length === 0) {
|
|
354
|
+
throw new UsageError('At least one instrument is required, e.g. `kite quote NSE:INFY`.');
|
|
355
|
+
}
|
|
356
|
+
return args.map((arg) => {
|
|
357
|
+
const parsed = parseInstrumentKey(arg);
|
|
358
|
+
return formatInstrumentKey(parsed.exchange, parsed.tradingsymbol);
|
|
359
|
+
});
|
|
360
|
+
}
|
|
361
|
+
/**
|
|
362
|
+
* Kite omits instruments it has no data for rather than returning nulls, so a
|
|
363
|
+
* silent gap is the normal failure mode for a typo or an expired contract.
|
|
364
|
+
* Surfacing it explicitly saves a lot of confusion.
|
|
365
|
+
*/
|
|
366
|
+
function warnAboutMissing(ctx, keys, quotes) {
|
|
367
|
+
const missing = keys.filter((key) => quotes[key] === undefined);
|
|
368
|
+
if (missing.length > 0) {
|
|
369
|
+
ctx.io.warn(`No data for: ${missing.join(', ')}`);
|
|
370
|
+
ctx.io.info('The symbol may be wrong or the contract expired. Try `kite instruments search`.');
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
/**
|
|
374
|
+
* Percentage change against the previous close.
|
|
375
|
+
*
|
|
376
|
+
* Kite sends `net_change` on quotes but it is frequently 0, so the percentage
|
|
377
|
+
* is derived from OHLC instead.
|
|
378
|
+
*/
|
|
379
|
+
function changePercent(quote) {
|
|
380
|
+
const close = quote.ohlc?.close;
|
|
381
|
+
if (close === undefined || close === 0)
|
|
382
|
+
return undefined;
|
|
383
|
+
return ((quote.last_price - close) / close) * 100;
|
|
384
|
+
}
|
|
385
|
+
function renderTableOrEmpty(ctx, rows, columns) {
|
|
386
|
+
return renderTable(ctx.io, rows, columns, {
|
|
387
|
+
compact: ctx.config.output.compact,
|
|
388
|
+
});
|
|
389
|
+
}
|