@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,182 @@
|
|
|
1
|
+
import { redirectUrlFor } from '../core/auth.js';
|
|
2
|
+
import { ConfigSchema, isSettableKey, loadConfig, SETTABLE_KEYS, saveConfig } from '../core/config.js';
|
|
3
|
+
import { getSecret, keyringAvailable } from '../core/credentials.js';
|
|
4
|
+
import { UsageError } from '../core/errors.js';
|
|
5
|
+
import { cacheDir, configDir, configFile } from '../core/paths.js';
|
|
6
|
+
import { maskSecret } from '../core/redact.js';
|
|
7
|
+
import { rupees } from '../output/format.js';
|
|
8
|
+
import { heading, renderKeyValue } from '../output/table.js';
|
|
9
|
+
export const configCommands = (program, run) => {
|
|
10
|
+
const config = program.command('config').description('View and change CLI settings');
|
|
11
|
+
config.command('show', { isDefault: true }).description('Show the current configuration').action(run(showConfig));
|
|
12
|
+
config
|
|
13
|
+
.command('set')
|
|
14
|
+
.description('Set a configuration value')
|
|
15
|
+
.argument('<key>', 'Dotted key, e.g. trading.maxOrderValue')
|
|
16
|
+
.argument('<value>')
|
|
17
|
+
.action(run(setConfig));
|
|
18
|
+
config
|
|
19
|
+
.command('unset')
|
|
20
|
+
.description('Remove a configuration value, restoring its default')
|
|
21
|
+
.argument('<key>')
|
|
22
|
+
.action(run(unsetConfig));
|
|
23
|
+
config.command('path').description('Print the paths this CLI reads and writes').action(run(showPaths));
|
|
24
|
+
};
|
|
25
|
+
async function showConfig(ctx) {
|
|
26
|
+
const { io } = ctx;
|
|
27
|
+
const config = await loadConfig();
|
|
28
|
+
if (io.json) {
|
|
29
|
+
io.writeJson(config);
|
|
30
|
+
return;
|
|
31
|
+
}
|
|
32
|
+
const secret = await getSecret('api_secret', { env: ctx.env });
|
|
33
|
+
io.line(heading(io, 'Account'));
|
|
34
|
+
io.line(renderKeyValue(io, [
|
|
35
|
+
['API key', config.apiKey ?? io.dim('not set')],
|
|
36
|
+
['API secret', secret ? `${maskSecret(secret.value)} (${secret.backend})` : io.dim('not set')],
|
|
37
|
+
['Environment', config.env === 'sandbox' ? io.cyan('sandbox') : 'production'],
|
|
38
|
+
[
|
|
39
|
+
'Keyring',
|
|
40
|
+
(await keyringAvailable()) ? io.green('available') : io.yellow('unavailable — using an encrypted file'),
|
|
41
|
+
],
|
|
42
|
+
]));
|
|
43
|
+
io.line(heading(io, 'Trading safety'));
|
|
44
|
+
io.line(renderKeyValue(io, [
|
|
45
|
+
['Trading enabled', config.trading.enabled ? io.green('yes') : io.red('no — all order commands will refuse')],
|
|
46
|
+
['Confirm orders', config.trading.confirm ? 'yes' : io.yellow('no')],
|
|
47
|
+
['Max order value', config.trading.maxOrderValue ? rupees(config.trading.maxOrderValue) : io.dim('no cap')],
|
|
48
|
+
['Strict confirm above', rupees(config.trading.strictConfirmAbove)],
|
|
49
|
+
]));
|
|
50
|
+
io.line(heading(io, 'Login callback'));
|
|
51
|
+
io.line(renderKeyValue(io, [
|
|
52
|
+
['Redirect URL', redirectUrlFor(config.redirectPort, config.redirectPath)],
|
|
53
|
+
['', io.dim('This must match the redirect URL in your Kite developer console exactly.')],
|
|
54
|
+
]));
|
|
55
|
+
io.line(heading(io, 'Output'));
|
|
56
|
+
io.line(renderKeyValue(io, [
|
|
57
|
+
['Colour', config.output.color],
|
|
58
|
+
['Compact tables', config.output.compact ? 'yes' : 'no'],
|
|
59
|
+
]));
|
|
60
|
+
io.note('');
|
|
61
|
+
io.info(`Config file: ${configFile()}`);
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Values arrive from argv as strings, so they are coerced by inspecting the
|
|
65
|
+
* default for that key, then re-validated through the full schema. That way an
|
|
66
|
+
* invalid value is rejected before it is written, rather than breaking every
|
|
67
|
+
* later invocation.
|
|
68
|
+
*/
|
|
69
|
+
async function setConfig(ctx, _opts, command) {
|
|
70
|
+
const [key, rawValue] = command.args;
|
|
71
|
+
if (!key || rawValue === undefined) {
|
|
72
|
+
throw new UsageError('Both a key and a value are required, e.g. `kite config set trading.enabled false`.');
|
|
73
|
+
}
|
|
74
|
+
if (!isSettableKey(key)) {
|
|
75
|
+
const available = Object.keys(SETTABLE_KEYS).join(', ');
|
|
76
|
+
throw new UsageError(`Unknown setting "${key}".`, `Available settings: ${available}.`);
|
|
77
|
+
}
|
|
78
|
+
const config = await loadConfig();
|
|
79
|
+
const coerced = coerce(rawValue, SETTABLE_KEYS[key].type, key);
|
|
80
|
+
const updated = structuredClone(config);
|
|
81
|
+
writePath(updated, key, coerced);
|
|
82
|
+
const parsed = ConfigSchema.safeParse(updated);
|
|
83
|
+
if (!parsed.success) {
|
|
84
|
+
const { z } = await import('zod');
|
|
85
|
+
throw new UsageError(`Invalid value for "${key}":\n${z.prettifyError(parsed.error)}`);
|
|
86
|
+
}
|
|
87
|
+
await saveConfig(parsed.data);
|
|
88
|
+
if (ctx.io.json) {
|
|
89
|
+
ctx.io.writeJson({ key, value: coerced });
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
ctx.io.success(`Set ${ctx.io.bold(key)} to ${JSON.stringify(coerced)}.`);
|
|
93
|
+
if (key === 'trading.enabled' && coerced === false) {
|
|
94
|
+
ctx.io.info('The kill switch is on. All order commands will now refuse before contacting Kite.');
|
|
95
|
+
}
|
|
96
|
+
if (key === 'redirectPort' || key === 'redirectPath') {
|
|
97
|
+
const next = await loadConfig();
|
|
98
|
+
ctx.io.info(`Update your Kite app's redirect URL to ${redirectUrlFor(next.redirectPort, next.redirectPath)}`);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
async function unsetConfig(ctx, _opts, command) {
|
|
102
|
+
const key = command.args[0];
|
|
103
|
+
if (!key)
|
|
104
|
+
throw new UsageError('A key is required.');
|
|
105
|
+
if (!isSettableKey(key)) {
|
|
106
|
+
throw new UsageError(`Unknown setting "${key}".`, `Available settings: ${Object.keys(SETTABLE_KEYS).join(', ')}.`);
|
|
107
|
+
}
|
|
108
|
+
const config = await loadConfig();
|
|
109
|
+
const updated = structuredClone(config);
|
|
110
|
+
deletePath(updated, key);
|
|
111
|
+
const parsed = ConfigSchema.safeParse(updated);
|
|
112
|
+
if (!parsed.success) {
|
|
113
|
+
const { z } = await import('zod');
|
|
114
|
+
throw new UsageError(`Cannot unset "${key}":\n${z.prettifyError(parsed.error)}`);
|
|
115
|
+
}
|
|
116
|
+
await saveConfig(parsed.data);
|
|
117
|
+
if (ctx.io.json) {
|
|
118
|
+
ctx.io.writeJson({ key, unset: true });
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
ctx.io.success(`Unset ${ctx.io.bold(key)}.`);
|
|
122
|
+
}
|
|
123
|
+
async function showPaths(ctx) {
|
|
124
|
+
const { io } = ctx;
|
|
125
|
+
const paths = {
|
|
126
|
+
config: configFile(),
|
|
127
|
+
configDir: configDir(),
|
|
128
|
+
cacheDir: cacheDir(),
|
|
129
|
+
};
|
|
130
|
+
if (io.json) {
|
|
131
|
+
io.writeJson(paths);
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
io.line(renderKeyValue(io, [
|
|
135
|
+
['Config file', paths.config],
|
|
136
|
+
['Config directory', paths.configDir],
|
|
137
|
+
['Cache directory', paths.cacheDir],
|
|
138
|
+
['Secrets', (await keyringAvailable()) ? 'OS keyring' : `${paths.configDir}/credentials.enc`],
|
|
139
|
+
]));
|
|
140
|
+
}
|
|
141
|
+
// ---------------------------------------------------------------------------
|
|
142
|
+
/** Coerce an argv string to the type the setting declares. */
|
|
143
|
+
function coerce(raw, type, key) {
|
|
144
|
+
if (type === 'boolean') {
|
|
145
|
+
if (raw === 'true')
|
|
146
|
+
return true;
|
|
147
|
+
if (raw === 'false')
|
|
148
|
+
return false;
|
|
149
|
+
throw new UsageError(`${key} expects true or false, got "${raw}".`);
|
|
150
|
+
}
|
|
151
|
+
if (type === 'number') {
|
|
152
|
+
const n = Number(raw);
|
|
153
|
+
if (!Number.isFinite(n))
|
|
154
|
+
throw new UsageError(`${key} expects a number, got "${raw}".`);
|
|
155
|
+
return n;
|
|
156
|
+
}
|
|
157
|
+
return raw;
|
|
158
|
+
}
|
|
159
|
+
function writePath(target, path, value) {
|
|
160
|
+
const segments = path.split('.');
|
|
161
|
+
const last = segments.pop();
|
|
162
|
+
let node = target;
|
|
163
|
+
for (const segment of segments) {
|
|
164
|
+
if (typeof node[segment] !== 'object' || node[segment] === null) {
|
|
165
|
+
node[segment] = {};
|
|
166
|
+
}
|
|
167
|
+
node = node[segment];
|
|
168
|
+
}
|
|
169
|
+
node[last] = value;
|
|
170
|
+
}
|
|
171
|
+
function deletePath(target, path) {
|
|
172
|
+
const segments = path.split('.');
|
|
173
|
+
const last = segments.pop();
|
|
174
|
+
let node = target;
|
|
175
|
+
for (const segment of segments) {
|
|
176
|
+
const next = node[segment];
|
|
177
|
+
if (typeof next !== 'object' || next === null)
|
|
178
|
+
return;
|
|
179
|
+
node = next;
|
|
180
|
+
}
|
|
181
|
+
delete node[last];
|
|
182
|
+
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { CommandFactory } from './types.js';
|
|
2
|
+
/**
|
|
3
|
+
* Good Till Triggered orders.
|
|
4
|
+
*
|
|
5
|
+
* Two shapes:
|
|
6
|
+
* single — one trigger price, one order
|
|
7
|
+
* two-leg — OCO: two trigger prices (stop-loss and target), index-matched to
|
|
8
|
+
* two orders; whichever fires first cancels the other
|
|
9
|
+
*
|
|
10
|
+
* GTT orders are always LIMIT — the API rejects anything else.
|
|
11
|
+
*/
|
|
12
|
+
export declare const gttCommands: CommandFactory;
|
|
@@ -0,0 +1,276 @@
|
|
|
1
|
+
import { PRODUCTS } from '../core/api.js';
|
|
2
|
+
import { ExitCode, KiteCliError, UsageError } from '../core/errors.js';
|
|
3
|
+
import { formatInstrumentKey, parseInstrumentKey } from '../core/instruments.js';
|
|
4
|
+
import { dateOnly, money, quantity, rupees } from '../output/format.js';
|
|
5
|
+
import { heading, printTable, renderKeyValue, renderTable } from '../output/table.js';
|
|
6
|
+
import { assertTradingEnabled, confirmAction } from '../safety.js';
|
|
7
|
+
/**
|
|
8
|
+
* Good Till Triggered orders.
|
|
9
|
+
*
|
|
10
|
+
* Two shapes:
|
|
11
|
+
* single — one trigger price, one order
|
|
12
|
+
* two-leg — OCO: two trigger prices (stop-loss and target), index-matched to
|
|
13
|
+
* two orders; whichever fires first cancels the other
|
|
14
|
+
*
|
|
15
|
+
* GTT orders are always LIMIT — the API rejects anything else.
|
|
16
|
+
*/
|
|
17
|
+
export const gttCommands = (program, run) => {
|
|
18
|
+
const gtt = program.command('gtt').description('Manage Good Till Triggered orders');
|
|
19
|
+
gtt
|
|
20
|
+
.command('list', { isDefault: true })
|
|
21
|
+
.description('Show your GTT triggers')
|
|
22
|
+
.option('--active', 'Show only active triggers')
|
|
23
|
+
.action(run(listGtt));
|
|
24
|
+
gtt.command('get').description('Show one GTT trigger in detail').argument('<id>').action(run(getGtt));
|
|
25
|
+
gtt
|
|
26
|
+
.command('place')
|
|
27
|
+
.description('Create a GTT trigger')
|
|
28
|
+
.argument('<instrument>', 'Instrument as EXCHANGE:SYMBOL')
|
|
29
|
+
.requiredOption('-s, --side <side>', 'BUY or SELL')
|
|
30
|
+
.requiredOption('-q, --quantity <n>', 'Quantity')
|
|
31
|
+
.requiredOption('--trigger <price>', 'Trigger price (repeat for a two-leg OCO)', collect, [])
|
|
32
|
+
.requiredOption('--price <price>', 'Limit price for the resulting order (repeat for two-leg)', collect, [])
|
|
33
|
+
.option('--product <product>', `Product (${PRODUCTS.join(', ')})`, 'CNC')
|
|
34
|
+
.action(run(placeGtt));
|
|
35
|
+
gtt.command('delete').description('Delete a GTT trigger').argument('<id>').action(run(deleteGtt));
|
|
36
|
+
};
|
|
37
|
+
function collect(value, previous) {
|
|
38
|
+
return [...previous, value];
|
|
39
|
+
}
|
|
40
|
+
async function listGtt(ctx, opts) {
|
|
41
|
+
ctx.requireSession();
|
|
42
|
+
const all = await ctx.api.getGtts(ctx.signal);
|
|
43
|
+
const rows = opts.active ? all.filter((trigger) => trigger.status === 'active') : all;
|
|
44
|
+
const columns = [
|
|
45
|
+
{ header: 'ID', value: (g, io) => io.dim(String(g.id)) },
|
|
46
|
+
{
|
|
47
|
+
header: 'Symbol',
|
|
48
|
+
value: (g, io) => io.bold(`${g.condition.exchange}:${g.condition.tradingsymbol}`),
|
|
49
|
+
},
|
|
50
|
+
{ header: 'Type', value: (g) => (g.type === 'two-leg' ? 'OCO' : 'single') },
|
|
51
|
+
{
|
|
52
|
+
header: 'Side',
|
|
53
|
+
value: (g, io) => {
|
|
54
|
+
const side = g.orders[0]?.transaction_type;
|
|
55
|
+
return side === 'BUY' ? io.green('BUY') : side === 'SELL' ? io.red('SELL') : '—';
|
|
56
|
+
},
|
|
57
|
+
},
|
|
58
|
+
{
|
|
59
|
+
header: 'Qty',
|
|
60
|
+
value: (g) => quantity(g.orders[0]?.quantity),
|
|
61
|
+
align: 'right',
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
header: 'Trigger',
|
|
65
|
+
value: (g) => g.condition.trigger_values.map((v) => money(v)).join(' / '),
|
|
66
|
+
align: 'right',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
header: 'LTP',
|
|
70
|
+
value: (g) => money(g.condition.last_price),
|
|
71
|
+
align: 'right',
|
|
72
|
+
},
|
|
73
|
+
{ header: 'Status', value: (g, io) => colourGttStatus(io, g.status) },
|
|
74
|
+
{ header: 'Expires', value: (g) => dateOnly(g.expires_at) },
|
|
75
|
+
];
|
|
76
|
+
printTable(ctx.io, rows, columns, rows, {
|
|
77
|
+
compact: ctx.config.output.compact,
|
|
78
|
+
empty: opts.active ? 'No active GTT triggers.' : 'No GTT triggers.',
|
|
79
|
+
});
|
|
80
|
+
}
|
|
81
|
+
function colourGttStatus(io, status) {
|
|
82
|
+
switch (status) {
|
|
83
|
+
case 'active':
|
|
84
|
+
return io.green(status);
|
|
85
|
+
case 'triggered':
|
|
86
|
+
return io.yellow(status);
|
|
87
|
+
case 'cancelled':
|
|
88
|
+
case 'deleted':
|
|
89
|
+
case 'expired':
|
|
90
|
+
return io.dim(status);
|
|
91
|
+
case 'rejected':
|
|
92
|
+
return io.red(status);
|
|
93
|
+
default:
|
|
94
|
+
return status;
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
async function getGtt(ctx, _opts, command) {
|
|
98
|
+
ctx.requireSession();
|
|
99
|
+
const id = requireId(command.args[0]);
|
|
100
|
+
const trigger = await ctx.api.getGtt(id, ctx.signal);
|
|
101
|
+
if (ctx.io.json) {
|
|
102
|
+
ctx.io.writeJson(trigger);
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
const { io } = ctx;
|
|
106
|
+
io.line(heading(io, `GTT ${trigger.id}`));
|
|
107
|
+
io.line(renderKeyValue(io, [
|
|
108
|
+
['Instrument', `${trigger.condition.exchange}:${trigger.condition.tradingsymbol}`],
|
|
109
|
+
['Type', trigger.type === 'two-leg' ? 'two-leg (OCO)' : 'single'],
|
|
110
|
+
['Status', colourGttStatus(io, trigger.status)],
|
|
111
|
+
['Trigger prices', trigger.condition.trigger_values.map((v) => money(v)).join(' / ')],
|
|
112
|
+
['Last price', money(trigger.condition.last_price)],
|
|
113
|
+
['Created', dateOnly(trigger.created_at)],
|
|
114
|
+
['Expires', dateOnly(trigger.expires_at)],
|
|
115
|
+
]));
|
|
116
|
+
io.line(heading(io, 'Orders'));
|
|
117
|
+
io.line(renderTableFor(ctx, trigger.orders, [
|
|
118
|
+
{
|
|
119
|
+
header: 'Side',
|
|
120
|
+
value: (o, io) => (o.transaction_type === 'BUY' ? io.green('BUY') : io.red('SELL')),
|
|
121
|
+
},
|
|
122
|
+
{ header: 'Qty', value: (o) => quantity(o.quantity), align: 'right' },
|
|
123
|
+
{ header: 'Price', value: (o) => money(o.price), align: 'right' },
|
|
124
|
+
{ header: 'Product', value: (o) => o.product ?? '—' },
|
|
125
|
+
{ header: 'Type', value: (o) => o.order_type ?? '—' },
|
|
126
|
+
]));
|
|
127
|
+
}
|
|
128
|
+
async function placeGtt(ctx, opts, command) {
|
|
129
|
+
ctx.requireSession();
|
|
130
|
+
assertTradingEnabled(ctx);
|
|
131
|
+
if (ctx.env === 'sandbox') {
|
|
132
|
+
throw new KiteCliError('The sandbox does not support GTT.', ExitCode.Input);
|
|
133
|
+
}
|
|
134
|
+
const instrumentArg = command.args[0];
|
|
135
|
+
if (!instrumentArg)
|
|
136
|
+
throw new UsageError('An instrument is required.');
|
|
137
|
+
const instrument = parseInstrumentKey(instrumentArg);
|
|
138
|
+
const instrumentKey = formatInstrumentKey(instrument.exchange, instrument.tradingsymbol);
|
|
139
|
+
const side = opts.side.toUpperCase();
|
|
140
|
+
if (side !== 'BUY' && side !== 'SELL')
|
|
141
|
+
throw new UsageError('--side must be BUY or SELL.');
|
|
142
|
+
const qty = Number(opts.quantity);
|
|
143
|
+
if (!Number.isInteger(qty) || qty <= 0)
|
|
144
|
+
throw new UsageError('--quantity must be a positive whole number.');
|
|
145
|
+
const triggers = opts.trigger.map((value) => requirePrice(value, '--trigger'));
|
|
146
|
+
const prices = opts.price.map((value) => requirePrice(value, '--price'));
|
|
147
|
+
if (triggers.length !== prices.length) {
|
|
148
|
+
throw new UsageError(`Got ${triggers.length} trigger price(s) and ${prices.length} limit price(s).`, 'Each --trigger needs a matching --price, in the same order.');
|
|
149
|
+
}
|
|
150
|
+
if (triggers.length !== 1 && triggers.length !== 2) {
|
|
151
|
+
throw new UsageError('A GTT takes either 1 trigger (single) or 2 triggers (two-leg OCO).');
|
|
152
|
+
}
|
|
153
|
+
const product = normaliseProduct(opts.product ?? 'CNC');
|
|
154
|
+
const type = triggers.length === 2 ? 'two-leg' : 'single';
|
|
155
|
+
// The trigger condition needs a current last_price, and Kite validates it.
|
|
156
|
+
const ltpMap = await ctx.api.getLtp([instrumentKey], ctx.signal);
|
|
157
|
+
const lastPrice = ltpMap[instrumentKey]?.last_price;
|
|
158
|
+
if (lastPrice === undefined) {
|
|
159
|
+
throw new KiteCliError(`Could not fetch a last price for ${instrumentKey}, which a GTT requires.`, ExitCode.Input, 'Check the symbol with `kite instruments search`.');
|
|
160
|
+
}
|
|
161
|
+
const params = {
|
|
162
|
+
type,
|
|
163
|
+
condition: {
|
|
164
|
+
exchange: instrument.exchange,
|
|
165
|
+
tradingsymbol: instrument.tradingsymbol,
|
|
166
|
+
trigger_values: triggers,
|
|
167
|
+
last_price: lastPrice,
|
|
168
|
+
},
|
|
169
|
+
orders: triggers.map((_, index) => ({
|
|
170
|
+
exchange: instrument.exchange,
|
|
171
|
+
tradingsymbol: instrument.tradingsymbol,
|
|
172
|
+
transaction_type: side,
|
|
173
|
+
quantity: qty,
|
|
174
|
+
// GTT only supports LIMIT orders.
|
|
175
|
+
order_type: 'LIMIT',
|
|
176
|
+
product,
|
|
177
|
+
price: prices[index],
|
|
178
|
+
})),
|
|
179
|
+
};
|
|
180
|
+
const notionalValue = Math.max(...prices) * qty;
|
|
181
|
+
await confirmAction(ctx, {
|
|
182
|
+
action: `Create ${type} GTT for ${qty} ${instrument.tradingsymbol}`,
|
|
183
|
+
mutatesOrders: true,
|
|
184
|
+
// A GTT places a real order when it triggers.
|
|
185
|
+
increasesExposure: true,
|
|
186
|
+
notionalValue,
|
|
187
|
+
challengeToken: instrument.tradingsymbol,
|
|
188
|
+
details: [
|
|
189
|
+
{ label: 'Instrument', value: instrumentKey },
|
|
190
|
+
{ label: 'Type', value: type === 'two-leg' ? 'two-leg (OCO)' : 'single' },
|
|
191
|
+
{
|
|
192
|
+
label: 'Side',
|
|
193
|
+
value: side === 'BUY' ? ctx.io.green(side) : ctx.io.red(side),
|
|
194
|
+
},
|
|
195
|
+
{ label: 'Quantity', value: quantity(qty) },
|
|
196
|
+
{ label: 'Last price', value: rupees(lastPrice) },
|
|
197
|
+
...triggers.map((trigger, index) => ({
|
|
198
|
+
label: type === 'two-leg' ? (index === 0 ? 'Leg 1' : 'Leg 2') : 'Trigger',
|
|
199
|
+
value: `trigger at ${rupees(trigger)} → LIMIT ${rupees(prices[index])}`,
|
|
200
|
+
})),
|
|
201
|
+
{ label: 'Product', value: product },
|
|
202
|
+
{ label: 'Max value', value: rupees(notionalValue) },
|
|
203
|
+
],
|
|
204
|
+
});
|
|
205
|
+
if (ctx.options.dryRun)
|
|
206
|
+
return;
|
|
207
|
+
const result = await ctx.api.placeGtt(params, ctx.signal);
|
|
208
|
+
if (ctx.io.json) {
|
|
209
|
+
ctx.io.writeJson(result);
|
|
210
|
+
return;
|
|
211
|
+
}
|
|
212
|
+
ctx.io.success(`GTT created: ${ctx.io.bold(String(result.trigger_id))}`);
|
|
213
|
+
ctx.io.info('GTT triggers expire after one year, or when Kite invalidates them.');
|
|
214
|
+
}
|
|
215
|
+
async function deleteGtt(ctx, _opts, command) {
|
|
216
|
+
ctx.requireSession();
|
|
217
|
+
assertTradingEnabled(ctx);
|
|
218
|
+
const id = requireId(command.args[0]);
|
|
219
|
+
// Enrichment for the preview. Deleting a GTT only removes a pending trigger,
|
|
220
|
+
// so a failed lookup does not need to block — but the user must be told the
|
|
221
|
+
// preview below is unverified rather than silently shown "unknown".
|
|
222
|
+
let existing;
|
|
223
|
+
try {
|
|
224
|
+
existing = await ctx.api.getGtt(id, ctx.signal);
|
|
225
|
+
}
|
|
226
|
+
catch {
|
|
227
|
+
ctx.io.warn(`Could not read GTT ${id} from Kite; the details below are unverified.`);
|
|
228
|
+
}
|
|
229
|
+
await confirmAction(ctx, {
|
|
230
|
+
action: `Delete GTT ${id}`,
|
|
231
|
+
mutatesOrders: true,
|
|
232
|
+
details: [
|
|
233
|
+
{ label: 'GTT ID', value: String(id) },
|
|
234
|
+
{
|
|
235
|
+
label: 'Instrument',
|
|
236
|
+
value: existing ? `${existing.condition.exchange}:${existing.condition.tradingsymbol}` : 'unknown',
|
|
237
|
+
},
|
|
238
|
+
{ label: 'Status', value: existing?.status ?? 'unknown' },
|
|
239
|
+
{
|
|
240
|
+
label: 'Triggers',
|
|
241
|
+
value: existing ? existing.condition.trigger_values.map((v) => money(v)).join(' / ') : 'unknown',
|
|
242
|
+
},
|
|
243
|
+
],
|
|
244
|
+
});
|
|
245
|
+
if (ctx.options.dryRun)
|
|
246
|
+
return;
|
|
247
|
+
const result = await ctx.api.deleteGtt(id, ctx.signal);
|
|
248
|
+
if (ctx.io.json) {
|
|
249
|
+
ctx.io.writeJson(result);
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
ctx.io.success(`GTT ${result.trigger_id} deleted.`);
|
|
253
|
+
}
|
|
254
|
+
function requireId(value) {
|
|
255
|
+
const id = Number(value);
|
|
256
|
+
if (!Number.isInteger(id) || id <= 0)
|
|
257
|
+
throw new UsageError('A numeric GTT ID is required.');
|
|
258
|
+
return id;
|
|
259
|
+
}
|
|
260
|
+
function requirePrice(value, flag) {
|
|
261
|
+
const n = Number(value);
|
|
262
|
+
if (!Number.isFinite(n) || n <= 0)
|
|
263
|
+
throw new UsageError(`${flag} must be a positive number.`);
|
|
264
|
+
return n;
|
|
265
|
+
}
|
|
266
|
+
function normaliseProduct(value) {
|
|
267
|
+
const upper = value.toUpperCase();
|
|
268
|
+
if (PRODUCTS.includes(upper))
|
|
269
|
+
return upper;
|
|
270
|
+
throw new UsageError(`Unknown product "${value}".`, `Valid products: ${PRODUCTS.join(', ')}.`);
|
|
271
|
+
}
|
|
272
|
+
function renderTableFor(ctx, rows, columns) {
|
|
273
|
+
return renderTable(ctx.io, rows, columns, {
|
|
274
|
+
compact: ctx.config.output.compact,
|
|
275
|
+
});
|
|
276
|
+
}
|