@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
package/dist/core/api.js
ADDED
|
@@ -0,0 +1,472 @@
|
|
|
1
|
+
import { z } from 'zod';
|
|
2
|
+
import { UsageError } from './errors.js';
|
|
3
|
+
import { AuctionSchema, BasketMarginSchema, CandlesSchema, GttCreateResultSchema, GttSchema, HoldingSchema, LtpMapSchema, MarginsSchema, MfHoldingSchema, MfOrderSchema, MfSipSchema, OhlcMapSchema, OrderMarginSchema, OrderSchema, PlaceOrderResultSchema, PositionsSchema, ProfileSchema, QuoteMapSchema, SessionSchema, TradeSchema, } from './schemas.js';
|
|
4
|
+
export const VARIETIES = ['regular', 'amo', 'co', 'iceberg', 'auction'];
|
|
5
|
+
export const ORDER_TYPES = ['MARKET', 'LIMIT', 'SL', 'SL-M'];
|
|
6
|
+
export const PRODUCTS = ['CNC', 'NRML', 'MIS', 'MTF'];
|
|
7
|
+
export const VALIDITIES = ['DAY', 'IOC', 'TTL'];
|
|
8
|
+
export const EXCHANGES = ['NSE', 'BSE', 'NFO', 'CDS', 'BCD', 'MCX', 'BFO'];
|
|
9
|
+
export class KiteApi {
|
|
10
|
+
client;
|
|
11
|
+
constructor(client) {
|
|
12
|
+
this.client = client;
|
|
13
|
+
}
|
|
14
|
+
// -------------------------------------------------------------------------
|
|
15
|
+
// Session
|
|
16
|
+
// -------------------------------------------------------------------------
|
|
17
|
+
/** Exchange a request_token for an access_token. */
|
|
18
|
+
async createSession(params) {
|
|
19
|
+
return this.client.request({
|
|
20
|
+
method: 'POST',
|
|
21
|
+
path: '/session/token',
|
|
22
|
+
schema: SessionSchema,
|
|
23
|
+
form: {
|
|
24
|
+
api_key: this.client.apiKey,
|
|
25
|
+
request_token: params.requestToken,
|
|
26
|
+
checksum: params.checksum,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
async invalidateSession(accessToken) {
|
|
31
|
+
return this.client.request({
|
|
32
|
+
method: 'DELETE',
|
|
33
|
+
path: '/session/token',
|
|
34
|
+
schema: z.unknown(),
|
|
35
|
+
query: { api_key: this.client.apiKey, access_token: accessToken },
|
|
36
|
+
});
|
|
37
|
+
}
|
|
38
|
+
async getProfile(signal) {
|
|
39
|
+
return this.client.request({
|
|
40
|
+
method: 'GET',
|
|
41
|
+
path: '/user/profile',
|
|
42
|
+
schema: ProfileSchema,
|
|
43
|
+
signal,
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
async getMargins(signal) {
|
|
47
|
+
return this.client.request({
|
|
48
|
+
method: 'GET',
|
|
49
|
+
path: '/user/margins',
|
|
50
|
+
schema: MarginsSchema,
|
|
51
|
+
signal,
|
|
52
|
+
});
|
|
53
|
+
}
|
|
54
|
+
// -------------------------------------------------------------------------
|
|
55
|
+
// Orders
|
|
56
|
+
// -------------------------------------------------------------------------
|
|
57
|
+
async getOrders(signal) {
|
|
58
|
+
return this.client.request({
|
|
59
|
+
method: 'GET',
|
|
60
|
+
path: '/orders',
|
|
61
|
+
schema: z.array(OrderSchema),
|
|
62
|
+
signal,
|
|
63
|
+
});
|
|
64
|
+
}
|
|
65
|
+
/** Returns the full state history of one order, oldest first. */
|
|
66
|
+
async getOrderHistory(orderId, signal) {
|
|
67
|
+
return this.client.request({
|
|
68
|
+
method: 'GET',
|
|
69
|
+
path: `/orders/${encodeURIComponent(orderId)}`,
|
|
70
|
+
schema: z.array(OrderSchema),
|
|
71
|
+
signal,
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
async getTrades(signal) {
|
|
75
|
+
return this.client.request({
|
|
76
|
+
method: 'GET',
|
|
77
|
+
path: '/trades',
|
|
78
|
+
schema: z.array(TradeSchema),
|
|
79
|
+
signal,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
async getOrderTrades(orderId, signal) {
|
|
83
|
+
return this.client.request({
|
|
84
|
+
method: 'GET',
|
|
85
|
+
path: `/orders/${encodeURIComponent(orderId)}/trades`,
|
|
86
|
+
schema: z.array(TradeSchema),
|
|
87
|
+
signal,
|
|
88
|
+
});
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Place an order.
|
|
92
|
+
*
|
|
93
|
+
* This is never retried automatically anywhere in the stack. If it fails at
|
|
94
|
+
* the network level the caller must reconcile via `findOrderByTag`, because
|
|
95
|
+
* a timed-out request may still have been executed.
|
|
96
|
+
*/
|
|
97
|
+
async placeOrder(params, signal) {
|
|
98
|
+
const { variety, ...rest } = params;
|
|
99
|
+
return this.client.request({
|
|
100
|
+
method: 'POST',
|
|
101
|
+
path: `/orders/${variety}`,
|
|
102
|
+
category: 'order',
|
|
103
|
+
schema: PlaceOrderResultSchema,
|
|
104
|
+
form: rest,
|
|
105
|
+
signal,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
async modifyOrder(params, signal) {
|
|
109
|
+
const { variety, order_id, ...rest } = params;
|
|
110
|
+
return this.client.request({
|
|
111
|
+
method: 'PUT',
|
|
112
|
+
path: `/orders/${variety}/${encodeURIComponent(order_id)}`,
|
|
113
|
+
category: 'order',
|
|
114
|
+
schema: z.looseObject({ order_id: z.string() }),
|
|
115
|
+
form: rest,
|
|
116
|
+
signal,
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
async cancelOrder(params, signal) {
|
|
120
|
+
return this.client.request({
|
|
121
|
+
method: 'DELETE',
|
|
122
|
+
path: `/orders/${params.variety}/${encodeURIComponent(params.order_id)}`,
|
|
123
|
+
category: 'order',
|
|
124
|
+
schema: z.looseObject({ order_id: z.string() }),
|
|
125
|
+
query: params.parent_order_id ? { parent_order_id: params.parent_order_id } : undefined,
|
|
126
|
+
signal,
|
|
127
|
+
});
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* Reconciliation helper for the no-idempotency problem.
|
|
131
|
+
*
|
|
132
|
+
* Kite has no client-suppliable idempotency key (the `guid` field in the
|
|
133
|
+
* response is server-assigned, despite its confusing description). Zerodha's
|
|
134
|
+
* own guidance for a failed/timed-out placement is: do NOT retry — fetch the
|
|
135
|
+
* orderbook and look for your tag. Only place again if it is absent.
|
|
136
|
+
*/
|
|
137
|
+
async findOrderByTag(tag, signal) {
|
|
138
|
+
const orders = await this.getOrders(signal);
|
|
139
|
+
return orders.filter((order) => order.tag === tag || order.tags?.includes(tag));
|
|
140
|
+
}
|
|
141
|
+
// -------------------------------------------------------------------------
|
|
142
|
+
// GTT
|
|
143
|
+
// -------------------------------------------------------------------------
|
|
144
|
+
async getGtts(signal) {
|
|
145
|
+
return this.client.request({
|
|
146
|
+
method: 'GET',
|
|
147
|
+
path: '/gtt/triggers',
|
|
148
|
+
schema: z.array(GttSchema),
|
|
149
|
+
signal,
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
async getGtt(id, signal) {
|
|
153
|
+
return this.client.request({
|
|
154
|
+
method: 'GET',
|
|
155
|
+
path: `/gtt/triggers/${id}`,
|
|
156
|
+
schema: GttSchema,
|
|
157
|
+
signal,
|
|
158
|
+
});
|
|
159
|
+
}
|
|
160
|
+
async placeGtt(params, signal) {
|
|
161
|
+
return this.client.request({
|
|
162
|
+
method: 'POST',
|
|
163
|
+
path: '/gtt/triggers',
|
|
164
|
+
schema: GttCreateResultSchema,
|
|
165
|
+
form: serialiseGtt(params),
|
|
166
|
+
signal,
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
async modifyGtt(id, params, signal) {
|
|
170
|
+
return this.client.request({
|
|
171
|
+
method: 'PUT',
|
|
172
|
+
path: `/gtt/triggers/${id}`,
|
|
173
|
+
schema: GttCreateResultSchema,
|
|
174
|
+
form: serialiseGtt(params),
|
|
175
|
+
signal,
|
|
176
|
+
});
|
|
177
|
+
}
|
|
178
|
+
async deleteGtt(id, signal) {
|
|
179
|
+
return this.client.request({
|
|
180
|
+
method: 'DELETE',
|
|
181
|
+
path: `/gtt/triggers/${id}`,
|
|
182
|
+
schema: GttCreateResultSchema,
|
|
183
|
+
signal,
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
// -------------------------------------------------------------------------
|
|
187
|
+
// Portfolio
|
|
188
|
+
// -------------------------------------------------------------------------
|
|
189
|
+
async getHoldings(signal) {
|
|
190
|
+
return this.client.request({
|
|
191
|
+
method: 'GET',
|
|
192
|
+
path: '/portfolio/holdings',
|
|
193
|
+
schema: z.array(HoldingSchema),
|
|
194
|
+
signal,
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
async getPositions(signal) {
|
|
198
|
+
return this.client.request({
|
|
199
|
+
method: 'GET',
|
|
200
|
+
path: '/portfolio/positions',
|
|
201
|
+
schema: PositionsSchema,
|
|
202
|
+
signal,
|
|
203
|
+
});
|
|
204
|
+
}
|
|
205
|
+
async getAuctions(signal) {
|
|
206
|
+
return this.client.request({
|
|
207
|
+
method: 'GET',
|
|
208
|
+
path: '/portfolio/holdings/auctions',
|
|
209
|
+
schema: z.array(AuctionSchema),
|
|
210
|
+
signal,
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
/** Convert a position between products. Note this is PUT, not POST. */
|
|
214
|
+
async convertPosition(params, signal) {
|
|
215
|
+
return this.client.request({
|
|
216
|
+
method: 'PUT',
|
|
217
|
+
path: '/portfolio/positions',
|
|
218
|
+
schema: z.boolean().or(z.unknown()),
|
|
219
|
+
form: params,
|
|
220
|
+
signal,
|
|
221
|
+
});
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Initiate CDSL authorisation for selling holdings — the HTTP 428 recovery
|
|
225
|
+
* path.
|
|
226
|
+
*
|
|
227
|
+
* With no ISINs this authorises the whole demat account; with ISINs it
|
|
228
|
+
* authorises only those instruments. The client encodes an array as repeated
|
|
229
|
+
* `isin` form fields, which is the shape Kite expects.
|
|
230
|
+
*/
|
|
231
|
+
async authoriseHoldings(isins, signal) {
|
|
232
|
+
return this.client.request({
|
|
233
|
+
method: 'POST',
|
|
234
|
+
path: '/portfolio/holdings/authorise',
|
|
235
|
+
schema: z.looseObject({ request_id: z.string() }),
|
|
236
|
+
form: isins?.length ? { isin: isins } : {},
|
|
237
|
+
signal,
|
|
238
|
+
});
|
|
239
|
+
}
|
|
240
|
+
/** Browser URL the user must visit to complete a holdings authorisation. */
|
|
241
|
+
authorisationUrl(requestId) {
|
|
242
|
+
return `https://kite.zerodha.com/connect/portfolio/authorise/holdings/${encodeURIComponent(this.client.apiKey)}/${encodeURIComponent(requestId)}`;
|
|
243
|
+
}
|
|
244
|
+
// -------------------------------------------------------------------------
|
|
245
|
+
// Market data
|
|
246
|
+
// -------------------------------------------------------------------------
|
|
247
|
+
/**
|
|
248
|
+
* Full quotes. Kite caps this at 500 instruments per call and 1 request per
|
|
249
|
+
* second, so large lists are chunked and paced by the limiter.
|
|
250
|
+
*/
|
|
251
|
+
async getQuote(instruments, signal) {
|
|
252
|
+
return this.batchedQuote(instruments, 500, '/quote', QuoteMapSchema, signal);
|
|
253
|
+
}
|
|
254
|
+
/** OHLC + last price. Caps at 1000 instruments per call. */
|
|
255
|
+
async getOhlc(instruments, signal) {
|
|
256
|
+
return this.batchedQuote(instruments, 1000, '/quote/ohlc', OhlcMapSchema, signal);
|
|
257
|
+
}
|
|
258
|
+
/** Last traded price only. Caps at 1000 instruments per call. */
|
|
259
|
+
async getLtp(instruments, signal) {
|
|
260
|
+
return this.batchedQuote(instruments, 1000, '/quote/ltp', LtpMapSchema, signal);
|
|
261
|
+
}
|
|
262
|
+
async batchedQuote(instruments, chunkSize, path, schema, signal) {
|
|
263
|
+
if (instruments.length === 0)
|
|
264
|
+
return {};
|
|
265
|
+
const merged = {};
|
|
266
|
+
for (const chunk of chunks(instruments, chunkSize)) {
|
|
267
|
+
const result = await this.client.request({
|
|
268
|
+
method: 'GET',
|
|
269
|
+
path,
|
|
270
|
+
category: 'quote',
|
|
271
|
+
schema,
|
|
272
|
+
query: { i: chunk },
|
|
273
|
+
signal,
|
|
274
|
+
});
|
|
275
|
+
Object.assign(merged, result);
|
|
276
|
+
}
|
|
277
|
+
return merged;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Historical candles, transparently chunked to respect per-interval range
|
|
281
|
+
* limits, then merged and de-duplicated.
|
|
282
|
+
*/
|
|
283
|
+
async getHistorical(params, signal) {
|
|
284
|
+
const maxDays = MAX_DAYS_PER_REQUEST[params.interval];
|
|
285
|
+
const ranges = splitDateRange(params.from, params.to, maxDays);
|
|
286
|
+
const all = [];
|
|
287
|
+
for (const range of ranges) {
|
|
288
|
+
const result = await this.client.request({
|
|
289
|
+
method: 'GET',
|
|
290
|
+
path: `/instruments/historical/${params.instrument_token}/${params.interval}`,
|
|
291
|
+
category: 'historical',
|
|
292
|
+
schema: CandlesSchema,
|
|
293
|
+
query: {
|
|
294
|
+
from: formatIstDateTime(range.from),
|
|
295
|
+
to: formatIstDateTime(range.to),
|
|
296
|
+
continuous: params.continuous ? 1 : undefined,
|
|
297
|
+
oi: params.oi ? 1 : undefined,
|
|
298
|
+
},
|
|
299
|
+
signal,
|
|
300
|
+
});
|
|
301
|
+
all.push(...result.candles);
|
|
302
|
+
}
|
|
303
|
+
// Chunk boundaries are inclusive on both ends, so adjacent ranges can
|
|
304
|
+
// repeat a candle. De-duplicate on timestamp, preserving order.
|
|
305
|
+
const seen = new Set();
|
|
306
|
+
return all.filter((candle) => {
|
|
307
|
+
const key = candle[0];
|
|
308
|
+
if (seen.has(key))
|
|
309
|
+
return false;
|
|
310
|
+
seen.add(key);
|
|
311
|
+
return true;
|
|
312
|
+
});
|
|
313
|
+
}
|
|
314
|
+
/** The daily instrument dump as raw CSV. Not an API envelope. */
|
|
315
|
+
async getInstrumentsCsv(exchange, signal) {
|
|
316
|
+
return this.client.requestText({
|
|
317
|
+
path: exchange ? `/instruments/${exchange}` : '/instruments',
|
|
318
|
+
// /instruments is the one route the sandbox does NOT serve under /oms.
|
|
319
|
+
noPrefix: true,
|
|
320
|
+
signal,
|
|
321
|
+
});
|
|
322
|
+
}
|
|
323
|
+
// -------------------------------------------------------------------------
|
|
324
|
+
// Margins & charges (JSON bodies, unlike the rest of the API)
|
|
325
|
+
// -------------------------------------------------------------------------
|
|
326
|
+
async orderMargins(orders, signal) {
|
|
327
|
+
return this.client.request({
|
|
328
|
+
method: 'POST',
|
|
329
|
+
path: '/margins/orders',
|
|
330
|
+
schema: z.array(OrderMarginSchema),
|
|
331
|
+
json: orders,
|
|
332
|
+
signal,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
async basketMargins(orders, considerPositions = true, signal) {
|
|
336
|
+
return this.client.request({
|
|
337
|
+
method: 'POST',
|
|
338
|
+
path: '/margins/basket',
|
|
339
|
+
schema: BasketMarginSchema,
|
|
340
|
+
query: { consider_positions: considerPositions },
|
|
341
|
+
json: orders,
|
|
342
|
+
signal,
|
|
343
|
+
});
|
|
344
|
+
}
|
|
345
|
+
async orderCharges(orders, signal) {
|
|
346
|
+
return this.client.request({
|
|
347
|
+
method: 'POST',
|
|
348
|
+
path: '/charges/orders',
|
|
349
|
+
schema: z.array(OrderMarginSchema),
|
|
350
|
+
json: orders,
|
|
351
|
+
signal,
|
|
352
|
+
});
|
|
353
|
+
}
|
|
354
|
+
// -------------------------------------------------------------------------
|
|
355
|
+
// Mutual funds (read-only — the current docs state that placing MF orders is
|
|
356
|
+
// not available over the API because it requires a bank debit)
|
|
357
|
+
// -------------------------------------------------------------------------
|
|
358
|
+
async getMfHoldings(signal) {
|
|
359
|
+
return this.client.request({
|
|
360
|
+
method: 'GET',
|
|
361
|
+
path: '/mf/holdings',
|
|
362
|
+
schema: z.array(MfHoldingSchema),
|
|
363
|
+
signal,
|
|
364
|
+
});
|
|
365
|
+
}
|
|
366
|
+
/** Note: only returns the last 7 days of orders. */
|
|
367
|
+
async getMfOrders(signal) {
|
|
368
|
+
return this.client.request({
|
|
369
|
+
method: 'GET',
|
|
370
|
+
path: '/mf/orders',
|
|
371
|
+
schema: z.array(MfOrderSchema),
|
|
372
|
+
signal,
|
|
373
|
+
});
|
|
374
|
+
}
|
|
375
|
+
async getMfSips(signal) {
|
|
376
|
+
return this.client.request({
|
|
377
|
+
method: 'GET',
|
|
378
|
+
path: '/mf/sips',
|
|
379
|
+
schema: z.array(MfSipSchema),
|
|
380
|
+
signal,
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
/**
|
|
385
|
+
* GTT takes JSON-encoded strings inside form fields — not a JSON body, and not
|
|
386
|
+
* plain form fields. An easy thing to get wrong.
|
|
387
|
+
*/
|
|
388
|
+
function serialiseGtt(params) {
|
|
389
|
+
return {
|
|
390
|
+
type: params.type,
|
|
391
|
+
condition: JSON.stringify(params.condition),
|
|
392
|
+
orders: JSON.stringify(params.orders),
|
|
393
|
+
};
|
|
394
|
+
}
|
|
395
|
+
// ---------------------------------------------------------------------------
|
|
396
|
+
// Historical helpers
|
|
397
|
+
// ---------------------------------------------------------------------------
|
|
398
|
+
export const HISTORICAL_INTERVALS = [
|
|
399
|
+
'minute',
|
|
400
|
+
'3minute',
|
|
401
|
+
'5minute',
|
|
402
|
+
'10minute',
|
|
403
|
+
'15minute',
|
|
404
|
+
'30minute',
|
|
405
|
+
'60minute',
|
|
406
|
+
'day',
|
|
407
|
+
];
|
|
408
|
+
/**
|
|
409
|
+
* Maximum date range per request, by interval.
|
|
410
|
+
*
|
|
411
|
+
* These limits are NOT in the official docs — they come from Zerodha staff on
|
|
412
|
+
* the developer forum. The `day` value is 1900 rather than the 2000 quoted
|
|
413
|
+
* there, because Zerodha's own sandbox chunking helper uses 1900 and the docs
|
|
414
|
+
* note that larger `day` ranges error out.
|
|
415
|
+
*/
|
|
416
|
+
export const MAX_DAYS_PER_REQUEST = {
|
|
417
|
+
minute: 60,
|
|
418
|
+
'3minute': 100,
|
|
419
|
+
'5minute': 100,
|
|
420
|
+
'10minute': 100,
|
|
421
|
+
'15minute': 200,
|
|
422
|
+
'30minute': 200,
|
|
423
|
+
'60minute': 400,
|
|
424
|
+
day: 1900,
|
|
425
|
+
};
|
|
426
|
+
export function parseInterval(value) {
|
|
427
|
+
if (HISTORICAL_INTERVALS.includes(value)) {
|
|
428
|
+
return value;
|
|
429
|
+
}
|
|
430
|
+
throw new UsageError(`Unknown interval "${value}".`, `Supported intervals: ${HISTORICAL_INTERVALS.join(', ')}.`);
|
|
431
|
+
}
|
|
432
|
+
const DAY_MS = 86_400_000;
|
|
433
|
+
export function splitDateRange(from, to, maxDays) {
|
|
434
|
+
if (from.getTime() > to.getTime()) {
|
|
435
|
+
throw new UsageError('`--from` must be earlier than `--to`.');
|
|
436
|
+
}
|
|
437
|
+
const ranges = [];
|
|
438
|
+
let cursor = from.getTime();
|
|
439
|
+
const end = to.getTime();
|
|
440
|
+
const span = maxDays * DAY_MS;
|
|
441
|
+
while (cursor <= end) {
|
|
442
|
+
const chunkEnd = Math.min(cursor + span, end);
|
|
443
|
+
ranges.push({ from: new Date(cursor), to: new Date(chunkEnd) });
|
|
444
|
+
if (chunkEnd >= end)
|
|
445
|
+
break;
|
|
446
|
+
// Step one second past the chunk end so ranges do not overlap by a full day.
|
|
447
|
+
cursor = chunkEnd + 1000;
|
|
448
|
+
}
|
|
449
|
+
return ranges;
|
|
450
|
+
}
|
|
451
|
+
/** Kite expects "yyyy-mm-dd hh:mm:ss" in IST. */
|
|
452
|
+
export function formatIstDateTime(date) {
|
|
453
|
+
const parts = new Intl.DateTimeFormat('en-CA', {
|
|
454
|
+
timeZone: 'Asia/Kolkata',
|
|
455
|
+
year: 'numeric',
|
|
456
|
+
month: '2-digit',
|
|
457
|
+
day: '2-digit',
|
|
458
|
+
hour: '2-digit',
|
|
459
|
+
minute: '2-digit',
|
|
460
|
+
second: '2-digit',
|
|
461
|
+
hour12: false,
|
|
462
|
+
}).formatToParts(date);
|
|
463
|
+
const get = (type) => parts.find((p) => p.type === type)?.value ?? '00';
|
|
464
|
+
// Intl can render midnight as hour "24" in some locales/engines.
|
|
465
|
+
const hour = get('hour') === '24' ? '00' : get('hour');
|
|
466
|
+
return `${get('year')}-${get('month')}-${get('day')} ${hour}:${get('minute')}:${get('second')}`;
|
|
467
|
+
}
|
|
468
|
+
export function* chunks(items, size) {
|
|
469
|
+
for (let i = 0; i < items.length; i += size) {
|
|
470
|
+
yield items.slice(i, i + size);
|
|
471
|
+
}
|
|
472
|
+
}
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import type { Endpoints } from './config.js';
|
|
2
|
+
/**
|
|
3
|
+
* The Kite Connect login handshake.
|
|
4
|
+
*
|
|
5
|
+
* 1. Open https://kite.zerodha.com/connect/login?v=3&api_key=...
|
|
6
|
+
* 2. User authenticates in their browser (including mandatory TOTP 2FA).
|
|
7
|
+
* 3. Kite redirects to the registered URL with ?request_token=...
|
|
8
|
+
* 4. POST /session/token with checksum = SHA256(api_key + request_token + api_secret)
|
|
9
|
+
*
|
|
10
|
+
* The CLI never sees the user's password or TOTP. We deliberately do not offer
|
|
11
|
+
* to store a TOTP seed: holding the 2FA secret alongside the API secret would
|
|
12
|
+
* collapse both factors into one, which is precisely what the SEBI 2FA mandate
|
|
13
|
+
* exists to prevent.
|
|
14
|
+
*/
|
|
15
|
+
/** checksum = SHA256(api_key + request_token + api_secret), hex. */
|
|
16
|
+
export declare function computeChecksum(apiKey: string, requestToken: string, apiSecret: string): string;
|
|
17
|
+
/**
|
|
18
|
+
* Postback checksum — note this is a DIFFERENT concatenation from the login
|
|
19
|
+
* checksum: SHA256(order_id + order_timestamp + api_secret).
|
|
20
|
+
*/
|
|
21
|
+
export declare function computePostbackChecksum(orderId: string, orderTimestamp: string, apiSecret: string): string;
|
|
22
|
+
export declare function verifyPostbackChecksum(received: string, orderId: string, orderTimestamp: string, apiSecret: string): boolean;
|
|
23
|
+
/**
|
|
24
|
+
* Constant-time string comparison that cannot throw.
|
|
25
|
+
*
|
|
26
|
+
* `timingSafeEqual` requires equal BYTE lengths and throws RangeError
|
|
27
|
+
* otherwise, so the length guard must be on the encoded buffers — not on
|
|
28
|
+
* `String.length`, which counts UTF-16 code units and can match while the byte
|
|
29
|
+
* lengths differ. Both call sites compare attacker-influenced input.
|
|
30
|
+
*/
|
|
31
|
+
export declare function safeCompare(a: string, b: string): boolean;
|
|
32
|
+
export interface LoginUrlOptions {
|
|
33
|
+
apiKey: string;
|
|
34
|
+
endpoints: Endpoints;
|
|
35
|
+
/** Opaque CSRF value echoed back by Kite via redirect_params. */
|
|
36
|
+
state: string;
|
|
37
|
+
}
|
|
38
|
+
export declare function buildLoginUrl(opts: LoginUrlOptions): string;
|
|
39
|
+
export declare function generateState(): string;
|
|
40
|
+
export interface CallbackResult {
|
|
41
|
+
requestToken: string;
|
|
42
|
+
}
|
|
43
|
+
export interface CallbackServerOptions {
|
|
44
|
+
port: number;
|
|
45
|
+
path: string;
|
|
46
|
+
state: string;
|
|
47
|
+
timeoutMs?: number;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Run a one-shot loopback HTTP server to capture the request_token.
|
|
51
|
+
*
|
|
52
|
+
* Kite's developer console requires HTTPS for redirect URLs but explicitly
|
|
53
|
+
* permits plain http:// for localhost and 127.0.0.1, which is the standard
|
|
54
|
+
* pattern for native apps. We bind to 127.0.0.1 specifically (never 0.0.0.0),
|
|
55
|
+
* so the callback is not reachable from the network.
|
|
56
|
+
*/
|
|
57
|
+
export declare function waitForCallback(opts: CallbackServerOptions): {
|
|
58
|
+
promise: Promise<CallbackResult>;
|
|
59
|
+
close: () => void;
|
|
60
|
+
};
|
|
61
|
+
export declare function redirectUrlFor(port: number, path: string): string;
|
|
62
|
+
/**
|
|
63
|
+
* Open a URL in the user's default browser.
|
|
64
|
+
*
|
|
65
|
+
* The URL is passed as an argv element to a fixed binary, never through a
|
|
66
|
+
* shell, so a crafted URL cannot become command injection.
|
|
67
|
+
*/
|
|
68
|
+
export declare function openBrowser(url: string): Promise<boolean>;
|