@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,102 @@
|
|
|
1
|
+
import { chmod, readFile, unlink, writeFile } from 'node:fs/promises';
|
|
2
|
+
import { z } from 'zod';
|
|
3
|
+
import { configDir, ensurePrivateDir, sessionFile } from './paths.js';
|
|
4
|
+
/**
|
|
5
|
+
* Non-secret session metadata.
|
|
6
|
+
*
|
|
7
|
+
* The access token itself lives in the OS keyring; only its expiry and the
|
|
8
|
+
* identity it belongs to are stored here. That split lets `kite whoami` show
|
|
9
|
+
* who you are and when your session dies without unlocking the keyring, and
|
|
10
|
+
* lets `kite logout` drop the token without touching the API secret.
|
|
11
|
+
*/
|
|
12
|
+
export const SessionMetaSchema = z.object({
|
|
13
|
+
userId: z.string(),
|
|
14
|
+
userName: z.string().optional(),
|
|
15
|
+
broker: z.string().optional(),
|
|
16
|
+
env: z.string(),
|
|
17
|
+
apiKey: z.string(),
|
|
18
|
+
/** ISO 8601. Kite invalidates all tokens at 06:00 IST daily. */
|
|
19
|
+
expiresAt: z.string(),
|
|
20
|
+
loginTime: z.string().optional(),
|
|
21
|
+
exchanges: z.array(z.string()).default([]),
|
|
22
|
+
products: z.array(z.string()).default([]),
|
|
23
|
+
});
|
|
24
|
+
export async function loadSessionMeta() {
|
|
25
|
+
let raw;
|
|
26
|
+
try {
|
|
27
|
+
raw = await readFile(sessionFile(), 'utf8');
|
|
28
|
+
}
|
|
29
|
+
catch (err) {
|
|
30
|
+
if (err.code === 'ENOENT')
|
|
31
|
+
return null;
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
try {
|
|
35
|
+
const result = SessionMetaSchema.safeParse(JSON.parse(raw));
|
|
36
|
+
return result.success ? result.data : null;
|
|
37
|
+
}
|
|
38
|
+
catch {
|
|
39
|
+
return null;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
export async function saveSessionMeta(meta) {
|
|
43
|
+
await ensurePrivateDir(configDir());
|
|
44
|
+
const path = sessionFile();
|
|
45
|
+
await writeFile(path, `${JSON.stringify(meta, null, 2)}\n`, {
|
|
46
|
+
mode: 0o600,
|
|
47
|
+
encoding: 'utf8',
|
|
48
|
+
});
|
|
49
|
+
if (process.platform !== 'win32') {
|
|
50
|
+
await chmod(path, 0o600);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
export async function clearSessionMeta() {
|
|
54
|
+
try {
|
|
55
|
+
await unlink(sessionFile());
|
|
56
|
+
}
|
|
57
|
+
catch (err) {
|
|
58
|
+
if (err.code !== 'ENOENT')
|
|
59
|
+
throw err;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Kite access tokens expire at 06:00 IST the following day — a regulatory
|
|
64
|
+
* requirement, not a configurable session length.
|
|
65
|
+
*
|
|
66
|
+
* This is a floor, not a guarantee: a master logout from Kite web invalidates
|
|
67
|
+
* the token immediately, and there is no way to detect that except by getting a
|
|
68
|
+
* 403. Callers must treat TokenException as authoritative regardless of what
|
|
69
|
+
* this says.
|
|
70
|
+
*/
|
|
71
|
+
export function nextTokenExpiry(now = new Date()) {
|
|
72
|
+
// Work out "now" in IST, decide whether 6 AM IST has already passed today,
|
|
73
|
+
// then build the corresponding UTC instant.
|
|
74
|
+
const istOffsetMs = 5.5 * 3600 * 1000;
|
|
75
|
+
const istNow = new Date(now.getTime() + istOffsetMs);
|
|
76
|
+
const year = istNow.getUTCFullYear();
|
|
77
|
+
const month = istNow.getUTCMonth();
|
|
78
|
+
const day = istNow.getUTCDate();
|
|
79
|
+
const hour = istNow.getUTCHours();
|
|
80
|
+
// 06:00 IST today, expressed as a UTC instant.
|
|
81
|
+
let expiryUtcMs = Date.UTC(year, month, day, 6, 0, 0) - istOffsetMs;
|
|
82
|
+
if (hour >= 6) {
|
|
83
|
+
// Already past 6 AM IST, so the token dies at 6 AM IST tomorrow.
|
|
84
|
+
expiryUtcMs += 24 * 3600 * 1000;
|
|
85
|
+
}
|
|
86
|
+
return new Date(expiryUtcMs);
|
|
87
|
+
}
|
|
88
|
+
export function isExpired(meta, now = new Date()) {
|
|
89
|
+
const expiry = Date.parse(meta.expiresAt);
|
|
90
|
+
if (Number.isNaN(expiry))
|
|
91
|
+
return true;
|
|
92
|
+
return now.getTime() >= expiry;
|
|
93
|
+
}
|
|
94
|
+
/** Human-readable time remaining, e.g. "4h 12m". */
|
|
95
|
+
export function timeUntilExpiry(meta, now = new Date()) {
|
|
96
|
+
const remaining = Date.parse(meta.expiresAt) - now.getTime();
|
|
97
|
+
if (Number.isNaN(remaining) || remaining <= 0)
|
|
98
|
+
return 'expired';
|
|
99
|
+
const hours = Math.floor(remaining / 3_600_000);
|
|
100
|
+
const minutes = Math.floor((remaining % 3_600_000) / 60_000);
|
|
101
|
+
return hours > 0 ? `${hours}h ${minutes}m` : `${minutes}m`;
|
|
102
|
+
}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import type { Endpoints } from './config.js';
|
|
3
|
+
/**
|
|
4
|
+
* Kite streaming quotes over WebSocket.
|
|
5
|
+
*
|
|
6
|
+
* Frame layout (all integers BIG-endian):
|
|
7
|
+
*
|
|
8
|
+
* [0..2) int16 number of packets in this message
|
|
9
|
+
* [2..4) int16 byte length of packet 1
|
|
10
|
+
* [4..N) packet 1
|
|
11
|
+
* ... (repeating int16 length prefix, then payload)
|
|
12
|
+
*
|
|
13
|
+
* A 1-byte message is a heartbeat.
|
|
14
|
+
*
|
|
15
|
+
* The single most important rule here: **dispatch on packet byte length, not on
|
|
16
|
+
* the mode you subscribed with.** Indices and tradeable instruments share the
|
|
17
|
+
* same subscription modes but have completely different layouts — and their
|
|
18
|
+
* OHLC fields are even in a different order. Five valid sizes exist:
|
|
19
|
+
*
|
|
20
|
+
* 8 LTP mode, any instrument
|
|
21
|
+
* 28 index, quote mode
|
|
22
|
+
* 32 index, full mode
|
|
23
|
+
* 44 tradeable, quote mode
|
|
24
|
+
* 184 tradeable, full mode
|
|
25
|
+
*/
|
|
26
|
+
export type TickerMode = 'ltp' | 'quote' | 'full';
|
|
27
|
+
export interface Depth {
|
|
28
|
+
price: number;
|
|
29
|
+
quantity: number;
|
|
30
|
+
orders: number;
|
|
31
|
+
}
|
|
32
|
+
export interface Tick {
|
|
33
|
+
instrumentToken: number;
|
|
34
|
+
tradable: boolean;
|
|
35
|
+
mode: TickerMode;
|
|
36
|
+
lastPrice: number;
|
|
37
|
+
lastQuantity?: number;
|
|
38
|
+
averagePrice?: number;
|
|
39
|
+
volume?: number;
|
|
40
|
+
buyQuantity?: number;
|
|
41
|
+
sellQuantity?: number;
|
|
42
|
+
ohlc?: {
|
|
43
|
+
open: number;
|
|
44
|
+
high: number;
|
|
45
|
+
low: number;
|
|
46
|
+
close: number;
|
|
47
|
+
};
|
|
48
|
+
/** Percentage change against the previous close, computed client-side. */
|
|
49
|
+
change?: number;
|
|
50
|
+
lastTradeTime?: Date;
|
|
51
|
+
exchangeTimestamp?: Date;
|
|
52
|
+
oi?: number;
|
|
53
|
+
oiDayHigh?: number;
|
|
54
|
+
oiDayLow?: number;
|
|
55
|
+
depth?: {
|
|
56
|
+
buy: Depth[];
|
|
57
|
+
sell: Depth[];
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
export declare function divisorFor(instrumentToken: number): number;
|
|
61
|
+
export declare function isTradable(instrumentToken: number): boolean;
|
|
62
|
+
/** Parse one packet. Returns null for a size we do not recognise. */
|
|
63
|
+
export declare function parsePacket(buf: Buffer): Tick | null;
|
|
64
|
+
/**
|
|
65
|
+
* Split a binary message into ticks.
|
|
66
|
+
*
|
|
67
|
+
* Every read is bounds-checked. A truncated or malformed frame must not throw
|
|
68
|
+
* inside the tick loop — a single bad frame should drop that message, not kill
|
|
69
|
+
* a running dashboard.
|
|
70
|
+
*/
|
|
71
|
+
export declare function parseBinaryMessage(data: Buffer): Tick[];
|
|
72
|
+
export interface TickerOptions {
|
|
73
|
+
apiKey: string;
|
|
74
|
+
accessToken: string;
|
|
75
|
+
endpoints: Endpoints;
|
|
76
|
+
/** Required by the sandbox WebSocket, which the official SDKs omit. */
|
|
77
|
+
userId?: string | undefined;
|
|
78
|
+
maxRetries?: number;
|
|
79
|
+
maxReconnectDelayMs?: number;
|
|
80
|
+
/** Force a reconnect if no data (including heartbeats) arrives for this long. */
|
|
81
|
+
readTimeoutMs?: number;
|
|
82
|
+
}
|
|
83
|
+
export interface TickerEvents {
|
|
84
|
+
connect: [];
|
|
85
|
+
ticks: [Tick[]];
|
|
86
|
+
orderUpdate: [unknown];
|
|
87
|
+
message: [unknown];
|
|
88
|
+
error: [Error];
|
|
89
|
+
close: [{
|
|
90
|
+
code: number;
|
|
91
|
+
reason: string;
|
|
92
|
+
}];
|
|
93
|
+
reconnect: [{
|
|
94
|
+
attempt: number;
|
|
95
|
+
delayMs: number;
|
|
96
|
+
}];
|
|
97
|
+
noreconnect: [];
|
|
98
|
+
}
|
|
99
|
+
/** Kite caps a single connection at 3000 instruments, and 3 connections per key. */
|
|
100
|
+
export declare const MAX_INSTRUMENTS_PER_CONNECTION = 3000;
|
|
101
|
+
export declare const MAX_CONNECTIONS_PER_KEY = 3;
|
|
102
|
+
export declare class Ticker extends EventEmitter<TickerEvents> {
|
|
103
|
+
private ws;
|
|
104
|
+
private readonly opts;
|
|
105
|
+
/** Desired subscription state, replayed on every reconnect. */
|
|
106
|
+
private readonly subscriptions;
|
|
107
|
+
private readonly modes;
|
|
108
|
+
private attempt;
|
|
109
|
+
private closedByUser;
|
|
110
|
+
private watchdog;
|
|
111
|
+
private reconnectTimer;
|
|
112
|
+
constructor(options: TickerOptions);
|
|
113
|
+
private url;
|
|
114
|
+
connect(): void;
|
|
115
|
+
private handleTextMessage;
|
|
116
|
+
/**
|
|
117
|
+
* Watchdog: a TCP connection can die without a close event, leaving the
|
|
118
|
+
* dashboard silently frozen. Kite sends heartbeats continuously, so silence
|
|
119
|
+
* beyond the timeout means the connection is dead.
|
|
120
|
+
*/
|
|
121
|
+
private armWatchdog;
|
|
122
|
+
private clearWatchdog;
|
|
123
|
+
private scheduleReconnect;
|
|
124
|
+
private send;
|
|
125
|
+
private replaySubscriptions;
|
|
126
|
+
subscribe(tokens: number[], mode?: TickerMode): void;
|
|
127
|
+
unsubscribe(tokens: number[]): void;
|
|
128
|
+
setMode(mode: TickerMode, tokens: number[]): void;
|
|
129
|
+
get connected(): boolean;
|
|
130
|
+
close(): void;
|
|
131
|
+
}
|
|
@@ -0,0 +1,367 @@
|
|
|
1
|
+
import { EventEmitter } from 'node:events';
|
|
2
|
+
import WebSocket from 'ws';
|
|
3
|
+
import { redactUrl } from './redact.js';
|
|
4
|
+
/**
|
|
5
|
+
* Exchange segment is the low byte of the instrument token, and it determines
|
|
6
|
+
* the price divisor.
|
|
7
|
+
*
|
|
8
|
+
* Kite's docs mention only the currency case ("divide by 10000000"), and omit
|
|
9
|
+
* BSE currency entirely — that one divides by 10000. Getting this wrong yields
|
|
10
|
+
* prices that are wrong by three orders of magnitude, silently.
|
|
11
|
+
*/
|
|
12
|
+
const Segment = {
|
|
13
|
+
NseCM: 1,
|
|
14
|
+
NseFO: 2,
|
|
15
|
+
NseCD: 3,
|
|
16
|
+
BseCM: 4,
|
|
17
|
+
BseFO: 5,
|
|
18
|
+
BseCD: 6,
|
|
19
|
+
McxFO: 7,
|
|
20
|
+
McxSX: 8,
|
|
21
|
+
Indices: 9,
|
|
22
|
+
};
|
|
23
|
+
export function divisorFor(instrumentToken) {
|
|
24
|
+
const segment = instrumentToken & 0xff;
|
|
25
|
+
if (segment === Segment.NseCD)
|
|
26
|
+
return 10_000_000;
|
|
27
|
+
if (segment === Segment.BseCD)
|
|
28
|
+
return 10_000;
|
|
29
|
+
return 100;
|
|
30
|
+
}
|
|
31
|
+
export function isTradable(instrumentToken) {
|
|
32
|
+
return (instrumentToken & 0xff) !== Segment.Indices;
|
|
33
|
+
}
|
|
34
|
+
function toDate(epochSeconds) {
|
|
35
|
+
// Kite sends 0 when a timestamp is not applicable.
|
|
36
|
+
return epochSeconds > 0 ? new Date(epochSeconds * 1000) : undefined;
|
|
37
|
+
}
|
|
38
|
+
function percentChange(lastPrice, close) {
|
|
39
|
+
if (close === 0)
|
|
40
|
+
return undefined;
|
|
41
|
+
return ((lastPrice - close) * 100) / close;
|
|
42
|
+
}
|
|
43
|
+
/** Parse one packet. Returns null for a size we do not recognise. */
|
|
44
|
+
export function parsePacket(buf) {
|
|
45
|
+
const token = buf.readUInt32BE(0);
|
|
46
|
+
const divisor = divisorFor(token);
|
|
47
|
+
const tradable = isTradable(token);
|
|
48
|
+
// --- LTP mode: 8 bytes, any instrument ---------------------------------
|
|
49
|
+
if (buf.length === 8) {
|
|
50
|
+
return {
|
|
51
|
+
instrumentToken: token,
|
|
52
|
+
tradable,
|
|
53
|
+
mode: 'ltp',
|
|
54
|
+
lastPrice: buf.readUInt32BE(4) / divisor,
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
// --- Index packets: 28 (quote) / 32 (full) ------------------------------
|
|
58
|
+
// NOTE the field order is high/low/open/close here, NOT open/high/low/close
|
|
59
|
+
// as in tradeable packets. Transposing these is an easy and silent bug.
|
|
60
|
+
if (buf.length === 28 || buf.length === 32) {
|
|
61
|
+
const lastPrice = buf.readUInt32BE(4) / divisor;
|
|
62
|
+
const close = buf.readUInt32BE(20) / divisor;
|
|
63
|
+
const tick = {
|
|
64
|
+
instrumentToken: token,
|
|
65
|
+
tradable: false,
|
|
66
|
+
mode: buf.length === 28 ? 'quote' : 'full',
|
|
67
|
+
lastPrice,
|
|
68
|
+
ohlc: {
|
|
69
|
+
high: buf.readUInt32BE(8) / divisor,
|
|
70
|
+
low: buf.readUInt32BE(12) / divisor,
|
|
71
|
+
open: buf.readUInt32BE(16) / divisor,
|
|
72
|
+
close,
|
|
73
|
+
},
|
|
74
|
+
change: percentChange(lastPrice, close),
|
|
75
|
+
};
|
|
76
|
+
if (buf.length === 32) {
|
|
77
|
+
tick.exchangeTimestamp = toDate(buf.readUInt32BE(28));
|
|
78
|
+
}
|
|
79
|
+
return tick;
|
|
80
|
+
}
|
|
81
|
+
// --- Tradeable packets: 44 (quote) / 184 (full) -------------------------
|
|
82
|
+
if (buf.length === 44 || buf.length === 184) {
|
|
83
|
+
const lastPrice = buf.readUInt32BE(4) / divisor;
|
|
84
|
+
const close = buf.readUInt32BE(40) / divisor;
|
|
85
|
+
const tick = {
|
|
86
|
+
instrumentToken: token,
|
|
87
|
+
tradable: true,
|
|
88
|
+
mode: buf.length === 44 ? 'quote' : 'full',
|
|
89
|
+
lastPrice,
|
|
90
|
+
lastQuantity: buf.readUInt32BE(8),
|
|
91
|
+
averagePrice: buf.readUInt32BE(12) / divisor,
|
|
92
|
+
volume: buf.readUInt32BE(16),
|
|
93
|
+
buyQuantity: buf.readUInt32BE(20),
|
|
94
|
+
sellQuantity: buf.readUInt32BE(24),
|
|
95
|
+
ohlc: {
|
|
96
|
+
open: buf.readUInt32BE(28) / divisor,
|
|
97
|
+
high: buf.readUInt32BE(32) / divisor,
|
|
98
|
+
low: buf.readUInt32BE(36) / divisor,
|
|
99
|
+
close,
|
|
100
|
+
},
|
|
101
|
+
change: percentChange(lastPrice, close),
|
|
102
|
+
};
|
|
103
|
+
if (buf.length === 184) {
|
|
104
|
+
tick.lastTradeTime = toDate(buf.readUInt32BE(44));
|
|
105
|
+
tick.oi = buf.readUInt32BE(48);
|
|
106
|
+
tick.oiDayHigh = buf.readUInt32BE(52);
|
|
107
|
+
tick.oiDayLow = buf.readUInt32BE(56);
|
|
108
|
+
tick.exchangeTimestamp = toDate(buf.readUInt32BE(60));
|
|
109
|
+
// Market depth: 10 entries of 12 bytes each, from offset 64.
|
|
110
|
+
// Each entry: int32 quantity, int32 price, int16 orders, 2 bytes padding.
|
|
111
|
+
const buy = [];
|
|
112
|
+
const sell = [];
|
|
113
|
+
for (let i = 0; i < 10; i += 1) {
|
|
114
|
+
const offset = 64 + i * 12;
|
|
115
|
+
const entry = {
|
|
116
|
+
quantity: buf.readUInt32BE(offset),
|
|
117
|
+
price: buf.readUInt32BE(offset + 4) / divisor,
|
|
118
|
+
orders: buf.readUInt16BE(offset + 8),
|
|
119
|
+
// bytes offset+10..offset+12 are padding
|
|
120
|
+
};
|
|
121
|
+
if (i < 5)
|
|
122
|
+
buy.push(entry);
|
|
123
|
+
else
|
|
124
|
+
sell.push(entry);
|
|
125
|
+
}
|
|
126
|
+
tick.depth = { buy, sell };
|
|
127
|
+
}
|
|
128
|
+
return tick;
|
|
129
|
+
}
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Split a binary message into ticks.
|
|
134
|
+
*
|
|
135
|
+
* Every read is bounds-checked. A truncated or malformed frame must not throw
|
|
136
|
+
* inside the tick loop — a single bad frame should drop that message, not kill
|
|
137
|
+
* a running dashboard.
|
|
138
|
+
*/
|
|
139
|
+
export function parseBinaryMessage(data) {
|
|
140
|
+
// 1-byte messages are heartbeats.
|
|
141
|
+
if (data.length < 2)
|
|
142
|
+
return [];
|
|
143
|
+
const ticks = [];
|
|
144
|
+
const packetCount = data.readInt16BE(0);
|
|
145
|
+
let offset = 2;
|
|
146
|
+
for (let i = 0; i < packetCount; i += 1) {
|
|
147
|
+
if (offset + 2 > data.length)
|
|
148
|
+
break;
|
|
149
|
+
const length = data.readInt16BE(offset);
|
|
150
|
+
offset += 2;
|
|
151
|
+
if (length <= 0 || offset + length > data.length)
|
|
152
|
+
break;
|
|
153
|
+
const packet = data.subarray(offset, offset + length);
|
|
154
|
+
offset += length;
|
|
155
|
+
try {
|
|
156
|
+
const tick = parsePacket(packet);
|
|
157
|
+
if (tick)
|
|
158
|
+
ticks.push(tick);
|
|
159
|
+
}
|
|
160
|
+
catch {
|
|
161
|
+
// Unrecognised layout; skip this packet and keep the stream alive.
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
return ticks;
|
|
165
|
+
}
|
|
166
|
+
/** Kite caps a single connection at 3000 instruments, and 3 connections per key. */
|
|
167
|
+
export const MAX_INSTRUMENTS_PER_CONNECTION = 3000;
|
|
168
|
+
export const MAX_CONNECTIONS_PER_KEY = 3;
|
|
169
|
+
export class Ticker extends EventEmitter {
|
|
170
|
+
ws;
|
|
171
|
+
opts;
|
|
172
|
+
/** Desired subscription state, replayed on every reconnect. */
|
|
173
|
+
subscriptions = new Set();
|
|
174
|
+
modes = new Map();
|
|
175
|
+
attempt = 0;
|
|
176
|
+
closedByUser = false;
|
|
177
|
+
watchdog;
|
|
178
|
+
reconnectTimer;
|
|
179
|
+
constructor(options) {
|
|
180
|
+
super();
|
|
181
|
+
this.opts = {
|
|
182
|
+
apiKey: options.apiKey,
|
|
183
|
+
accessToken: options.accessToken,
|
|
184
|
+
endpoints: options.endpoints,
|
|
185
|
+
userId: options.userId,
|
|
186
|
+
maxRetries: options.maxRetries ?? 50,
|
|
187
|
+
maxReconnectDelayMs: options.maxReconnectDelayMs ?? 60_000,
|
|
188
|
+
readTimeoutMs: options.readTimeoutMs ?? 10_000,
|
|
189
|
+
};
|
|
190
|
+
}
|
|
191
|
+
url() {
|
|
192
|
+
const url = new URL(this.opts.endpoints.ws);
|
|
193
|
+
url.searchParams.set('api_key', this.opts.apiKey);
|
|
194
|
+
url.searchParams.set('access_token', this.opts.accessToken);
|
|
195
|
+
// The sandbox ticker will not authenticate without this; production ignores it.
|
|
196
|
+
if (this.opts.userId)
|
|
197
|
+
url.searchParams.set('user_id', this.opts.userId);
|
|
198
|
+
return url.toString();
|
|
199
|
+
}
|
|
200
|
+
connect() {
|
|
201
|
+
if (this.ws && (this.ws.readyState === WebSocket.OPEN || this.ws.readyState === WebSocket.CONNECTING)) {
|
|
202
|
+
return;
|
|
203
|
+
}
|
|
204
|
+
this.closedByUser = false;
|
|
205
|
+
const target = this.url();
|
|
206
|
+
const socket = new WebSocket(target, { handshakeTimeout: 10_000 });
|
|
207
|
+
socket.binaryType = 'nodebuffer';
|
|
208
|
+
this.ws = socket;
|
|
209
|
+
socket.on('open', () => {
|
|
210
|
+
this.attempt = 0;
|
|
211
|
+
this.armWatchdog();
|
|
212
|
+
this.emit('connect');
|
|
213
|
+
// The server keeps no subscription state across connections, so the full
|
|
214
|
+
// desired state must be replayed every time.
|
|
215
|
+
this.replaySubscriptions();
|
|
216
|
+
});
|
|
217
|
+
socket.on('message', (data, isBinary) => {
|
|
218
|
+
this.armWatchdog();
|
|
219
|
+
if (isBinary) {
|
|
220
|
+
const buf = toBuffer(data);
|
|
221
|
+
// A 1-byte payload is a heartbeat: liveness only, no ticks.
|
|
222
|
+
if (buf.length === 1)
|
|
223
|
+
return;
|
|
224
|
+
const ticks = parseBinaryMessage(buf);
|
|
225
|
+
if (ticks.length > 0)
|
|
226
|
+
this.emit('ticks', ticks);
|
|
227
|
+
return;
|
|
228
|
+
}
|
|
229
|
+
this.handleTextMessage(toBuffer(data).toString('utf8'));
|
|
230
|
+
});
|
|
231
|
+
// Kite sends a heartbeat frame; treat any ping as liveness too.
|
|
232
|
+
socket.on('ping', () => this.armWatchdog());
|
|
233
|
+
socket.on('pong', () => this.armWatchdog());
|
|
234
|
+
socket.on('error', (err) => {
|
|
235
|
+
// The ws error message can embed the full URL, which carries the access
|
|
236
|
+
// token as a query parameter. Never emit it unredacted.
|
|
237
|
+
this.emit('error', new Error(`${redactUrl(this.opts.endpoints.ws)}: ${redactMessage(err.message, target)}`));
|
|
238
|
+
});
|
|
239
|
+
socket.on('close', (code, reason) => {
|
|
240
|
+
// Guard against a stale socket's close event racing a newer connection.
|
|
241
|
+
if (this.ws !== socket)
|
|
242
|
+
return;
|
|
243
|
+
this.clearWatchdog();
|
|
244
|
+
this.emit('close', { code, reason: reason.toString('utf8') });
|
|
245
|
+
if (!this.closedByUser)
|
|
246
|
+
this.scheduleReconnect();
|
|
247
|
+
});
|
|
248
|
+
}
|
|
249
|
+
handleTextMessage(raw) {
|
|
250
|
+
let payload;
|
|
251
|
+
try {
|
|
252
|
+
payload = JSON.parse(raw);
|
|
253
|
+
}
|
|
254
|
+
catch {
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
257
|
+
switch (payload.type) {
|
|
258
|
+
case 'order':
|
|
259
|
+
// Same payload shape as an HTTP postback. For a single-user CLI this is
|
|
260
|
+
// the recommended way to receive order updates — no public URL needed.
|
|
261
|
+
this.emit('orderUpdate', payload.data);
|
|
262
|
+
break;
|
|
263
|
+
case 'error':
|
|
264
|
+
this.emit('error', new Error(String(payload.data ?? 'Ticker error')));
|
|
265
|
+
break;
|
|
266
|
+
default:
|
|
267
|
+
this.emit('message', payload.data);
|
|
268
|
+
break;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
/**
|
|
272
|
+
* Watchdog: a TCP connection can die without a close event, leaving the
|
|
273
|
+
* dashboard silently frozen. Kite sends heartbeats continuously, so silence
|
|
274
|
+
* beyond the timeout means the connection is dead.
|
|
275
|
+
*/
|
|
276
|
+
armWatchdog() {
|
|
277
|
+
this.clearWatchdog();
|
|
278
|
+
this.watchdog = setTimeout(() => {
|
|
279
|
+
this.ws?.terminate();
|
|
280
|
+
}, this.opts.readTimeoutMs);
|
|
281
|
+
this.watchdog.unref?.();
|
|
282
|
+
}
|
|
283
|
+
clearWatchdog() {
|
|
284
|
+
if (this.watchdog) {
|
|
285
|
+
clearTimeout(this.watchdog);
|
|
286
|
+
this.watchdog = undefined;
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
scheduleReconnect() {
|
|
290
|
+
if (this.attempt >= this.opts.maxRetries) {
|
|
291
|
+
this.emit('noreconnect');
|
|
292
|
+
return;
|
|
293
|
+
}
|
|
294
|
+
this.attempt += 1;
|
|
295
|
+
// Exponential backoff with jitter. Jitter matters: without it, several
|
|
296
|
+
// ticker processes restarted together would reconnect in lockstep.
|
|
297
|
+
const base = Math.min(2 ** this.attempt * 1000, this.opts.maxReconnectDelayMs);
|
|
298
|
+
const delayMs = Math.round(base / 2 + Math.random() * (base / 2));
|
|
299
|
+
this.emit('reconnect', { attempt: this.attempt, delayMs });
|
|
300
|
+
this.reconnectTimer = setTimeout(() => this.connect(), delayMs);
|
|
301
|
+
this.reconnectTimer.unref?.();
|
|
302
|
+
}
|
|
303
|
+
send(payload) {
|
|
304
|
+
if (this.ws?.readyState === WebSocket.OPEN) {
|
|
305
|
+
this.ws.send(JSON.stringify(payload));
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
replaySubscriptions() {
|
|
309
|
+
if (this.subscriptions.size === 0)
|
|
310
|
+
return;
|
|
311
|
+
this.send({ a: 'subscribe', v: [...this.subscriptions] });
|
|
312
|
+
// Group tokens by mode so we send one message per mode rather than per token.
|
|
313
|
+
const byMode = new Map();
|
|
314
|
+
for (const [token, mode] of this.modes) {
|
|
315
|
+
if (!this.subscriptions.has(token))
|
|
316
|
+
continue;
|
|
317
|
+
const list = byMode.get(mode) ?? [];
|
|
318
|
+
list.push(token);
|
|
319
|
+
byMode.set(mode, list);
|
|
320
|
+
}
|
|
321
|
+
for (const [mode, tokens] of byMode) {
|
|
322
|
+
this.send({ a: 'mode', v: [mode, tokens] });
|
|
323
|
+
}
|
|
324
|
+
}
|
|
325
|
+
subscribe(tokens, mode = 'quote') {
|
|
326
|
+
for (const token of tokens) {
|
|
327
|
+
this.subscriptions.add(token);
|
|
328
|
+
this.modes.set(token, mode);
|
|
329
|
+
}
|
|
330
|
+
this.send({ a: 'subscribe', v: tokens });
|
|
331
|
+
this.send({ a: 'mode', v: [mode, tokens] });
|
|
332
|
+
}
|
|
333
|
+
unsubscribe(tokens) {
|
|
334
|
+
for (const token of tokens) {
|
|
335
|
+
this.subscriptions.delete(token);
|
|
336
|
+
this.modes.delete(token);
|
|
337
|
+
}
|
|
338
|
+
this.send({ a: 'unsubscribe', v: tokens });
|
|
339
|
+
}
|
|
340
|
+
setMode(mode, tokens) {
|
|
341
|
+
for (const token of tokens)
|
|
342
|
+
this.modes.set(token, mode);
|
|
343
|
+
this.send({ a: 'mode', v: [mode, tokens] });
|
|
344
|
+
}
|
|
345
|
+
get connected() {
|
|
346
|
+
return this.ws?.readyState === WebSocket.OPEN;
|
|
347
|
+
}
|
|
348
|
+
close() {
|
|
349
|
+
this.closedByUser = true;
|
|
350
|
+
this.clearWatchdog();
|
|
351
|
+
if (this.reconnectTimer)
|
|
352
|
+
clearTimeout(this.reconnectTimer);
|
|
353
|
+
this.ws?.close();
|
|
354
|
+
this.ws = undefined;
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
function toBuffer(data) {
|
|
358
|
+
if (Buffer.isBuffer(data))
|
|
359
|
+
return data;
|
|
360
|
+
if (Array.isArray(data))
|
|
361
|
+
return Buffer.concat(data);
|
|
362
|
+
return Buffer.from(data);
|
|
363
|
+
}
|
|
364
|
+
/** Strip the connection URL (and therefore the access token) out of an error. */
|
|
365
|
+
function redactMessage(message, url) {
|
|
366
|
+
return message.split(url).join(redactUrl(url));
|
|
367
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Programmatic entry point.
|
|
3
|
+
*
|
|
4
|
+
* The CLI is the product, but the client underneath it is useful on its own —
|
|
5
|
+
* these exports let you script against Kite with the same rate limiting,
|
|
6
|
+
* validation, redaction and error taxonomy the CLI uses.
|
|
7
|
+
*/
|
|
8
|
+
export { type GttParams, KiteApi, type ModifyOrderParams, type PlaceOrderParams, } from './core/api.js';
|
|
9
|
+
export { buildLoginUrl, computeChecksum, computePostbackChecksum, verifyPostbackChecksum, } from './core/auth.js';
|
|
10
|
+
export { type ClientOptions, KiteClient, setDispatcher, } from './core/client.js';
|
|
11
|
+
export { type Endpoints, type Environment, endpointsFor, SANDBOX_CREDENTIALS, } from './core/config.js';
|
|
12
|
+
export { AuthRequiredError, ExitCode, KiteApiError, KiteCliError, type KiteErrorType, NetworkError, UsageError, } from './core/errors.js';
|
|
13
|
+
export { InstrumentStore, parseInstrumentKey, parseInstrumentsCsv, } from './core/instruments.js';
|
|
14
|
+
export { ORDER_LIMITS, type RateCategory, RateLimiter, } from './core/ratelimit.js';
|
|
15
|
+
export { maskSecret, redact, redactString, redactUrl, registerSecret, } from './core/redact.js';
|
|
16
|
+
export * from './core/schemas.js';
|
|
17
|
+
export { divisorFor, isTradable, parseBinaryMessage, parsePacket, type Tick, Ticker, type TickerMode, type TickerOptions, } from './core/ticker.js';
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Programmatic entry point.
|
|
3
|
+
*
|
|
4
|
+
* The CLI is the product, but the client underneath it is useful on its own —
|
|
5
|
+
* these exports let you script against Kite with the same rate limiting,
|
|
6
|
+
* validation, redaction and error taxonomy the CLI uses.
|
|
7
|
+
*/
|
|
8
|
+
export { KiteApi, } from './core/api.js';
|
|
9
|
+
export { buildLoginUrl, computeChecksum, computePostbackChecksum, verifyPostbackChecksum, } from './core/auth.js';
|
|
10
|
+
export { KiteClient, setDispatcher, } from './core/client.js';
|
|
11
|
+
export { endpointsFor, SANDBOX_CREDENTIALS, } from './core/config.js';
|
|
12
|
+
export { AuthRequiredError, ExitCode, KiteApiError, KiteCliError, NetworkError, UsageError, } from './core/errors.js';
|
|
13
|
+
export { InstrumentStore, parseInstrumentKey, parseInstrumentsCsv, } from './core/instruments.js';
|
|
14
|
+
export { ORDER_LIMITS, RateLimiter, } from './core/ratelimit.js';
|
|
15
|
+
export { maskSecret, redact, redactString, redactUrl, registerSecret, } from './core/redact.js';
|
|
16
|
+
export * from './core/schemas.js';
|
|
17
|
+
export { divisorFor, isTradable, parseBinaryMessage, parsePacket, Ticker, } from './core/ticker.js';
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Number, currency and date formatting.
|
|
3
|
+
*
|
|
4
|
+
* Everything is rendered in the Indian locale and IST, because that is the only
|
|
5
|
+
* frame of reference Kite operates in. Rupee amounts use the Indian digit
|
|
6
|
+
* grouping (lakh/crore), which `en-IN` gives us for free.
|
|
7
|
+
*/
|
|
8
|
+
/** A rupee amount, e.g. "12,34,567.89". No symbol — callers add it. */
|
|
9
|
+
export declare function money(value: number | undefined | null): string;
|
|
10
|
+
/** A rupee amount with the symbol, e.g. "₹12,34,567.89". */
|
|
11
|
+
export declare function rupees(value: number | undefined | null): string;
|
|
12
|
+
/** A signed rupee amount, e.g. "+₹1,200.00" / "-₹340.50". */
|
|
13
|
+
export declare function signedRupees(value: number | undefined | null): string;
|
|
14
|
+
/** Large rupee amounts abbreviated as L / Cr, the conventional Indian units. */
|
|
15
|
+
export declare function compactRupees(value: number | undefined | null): string;
|
|
16
|
+
export declare function percent(value: number | undefined | null, digits?: number): string;
|
|
17
|
+
export declare function quantity(value: number | undefined | null): string;
|
|
18
|
+
/** Compact volume, e.g. "1.2M". */
|
|
19
|
+
export declare function compactNumber(value: number | undefined | null): string;
|
|
20
|
+
export declare function dateTime(value: Date | string | undefined | null): string;
|
|
21
|
+
export declare function timeOnly(value: Date | string | undefined | null): string;
|
|
22
|
+
export declare function dateOnly(value: Date | string | undefined | null): string;
|
|
23
|
+
/**
|
|
24
|
+
* Parse a user-supplied date for historical queries.
|
|
25
|
+
*
|
|
26
|
+
* Accepts YYYY-MM-DD, "YYYY-MM-DD HH:MM:SS", and relative offsets like "7d",
|
|
27
|
+
* "3m", "1y" meaning "N units ago".
|
|
28
|
+
*/
|
|
29
|
+
export declare function parseUserDate(value: string, now?: Date): Date | null;
|
|
30
|
+
/** Truncate to a display width, appending an ellipsis. */
|
|
31
|
+
export declare function truncate(value: string, max: number): string;
|