@ethosagent/tools-india-broker-zerodha 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/CHANGELOG.md +3 -0
- package/LICENSE +21 -0
- package/README.md +135 -0
- package/dist/cli.d.ts +1 -0
- package/dist/cli.js +735 -0
- package/dist/cli.js.map +1 -0
- package/dist/index.d.ts +74 -0
- package/dist/index.js +850 -0
- package/dist/index.js.map +1 -0
- package/dist/panel.d.ts +24 -0
- package/dist/panel.js +394 -0
- package/dist/panel.js.map +1 -0
- package/package.json +116 -0
- package/skills/portfolio_review.md +30 -0
- package/skills/position_sizing.md +4 -0
- package/skills/trade_confirmation.md +36 -0
package/dist/cli.js
ADDED
|
@@ -0,0 +1,735 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
// src/cli.ts
|
|
4
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "fs";
|
|
5
|
+
import { dirname, join } from "path";
|
|
6
|
+
import { fileURLToPath } from "url";
|
|
7
|
+
|
|
8
|
+
// src/auth.ts
|
|
9
|
+
import { createHash } from "crypto";
|
|
10
|
+
var KITE_SESSION_URL = "https://api.kite.trade/session/token";
|
|
11
|
+
function buildLoginUrl(apiKey) {
|
|
12
|
+
return `https://kite.trade/connect/login?v=3&api_key=${apiKey}`;
|
|
13
|
+
}
|
|
14
|
+
function computeChecksum(apiKey, requestToken, apiSecret) {
|
|
15
|
+
return createHash("sha256").update(apiKey + requestToken + apiSecret).digest("hex");
|
|
16
|
+
}
|
|
17
|
+
async function exchangeToken(apiKey, apiSecret, requestToken) {
|
|
18
|
+
const checksum = computeChecksum(apiKey, requestToken, apiSecret);
|
|
19
|
+
const body = new URLSearchParams();
|
|
20
|
+
body.set("api_key", apiKey);
|
|
21
|
+
body.set("request_token", requestToken);
|
|
22
|
+
body.set("checksum", checksum);
|
|
23
|
+
const res = await fetch(KITE_SESSION_URL, {
|
|
24
|
+
method: "POST",
|
|
25
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
26
|
+
body: body.toString()
|
|
27
|
+
});
|
|
28
|
+
const json = await res.json();
|
|
29
|
+
if (json.status !== "success") {
|
|
30
|
+
throw new Error(`Token exchange failed: ${json.message ?? "unknown error"}`);
|
|
31
|
+
}
|
|
32
|
+
return json.data;
|
|
33
|
+
}
|
|
34
|
+
async function validateToken(apiKey, accessToken) {
|
|
35
|
+
try {
|
|
36
|
+
const res = await fetch("https://api.kite.trade/user/profile", {
|
|
37
|
+
method: "GET",
|
|
38
|
+
headers: {
|
|
39
|
+
Authorization: `token ${apiKey}:${accessToken}`,
|
|
40
|
+
"X-Kite-Version": "3"
|
|
41
|
+
}
|
|
42
|
+
});
|
|
43
|
+
if (res.status === 403) {
|
|
44
|
+
return { valid: false, expiresHint: "Token expired" };
|
|
45
|
+
}
|
|
46
|
+
const json = await res.json();
|
|
47
|
+
if (json.error_type === "TokenException") {
|
|
48
|
+
return { valid: false, expiresHint: "Token expired" };
|
|
49
|
+
}
|
|
50
|
+
if (json.status === "success" && json.data) {
|
|
51
|
+
return {
|
|
52
|
+
valid: true,
|
|
53
|
+
userId: json.data.user_id,
|
|
54
|
+
expiresHint: "tonight at midnight IST"
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
return { valid: false, expiresHint: "Unknown validation error" };
|
|
58
|
+
} catch {
|
|
59
|
+
return { valid: false, expiresHint: "Network error during validation" };
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// src/kite-client.ts
|
|
64
|
+
var KITE_BASE = "https://api.kite.trade";
|
|
65
|
+
function authHeaders(creds) {
|
|
66
|
+
return {
|
|
67
|
+
Authorization: `token ${creds.apiKey}:${creds.accessToken}`,
|
|
68
|
+
"X-Kite-Version": "3",
|
|
69
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
var KiteTokenExpiredError = class extends Error {
|
|
73
|
+
code = "TOKEN_EXPIRED";
|
|
74
|
+
constructor(message = "Kite access token expired. Renew via zerodha-broker auth.") {
|
|
75
|
+
super(message);
|
|
76
|
+
this.name = "KiteTokenExpiredError";
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
var KiteApiError = class extends Error {
|
|
80
|
+
constructor(message, errorType, statusCode) {
|
|
81
|
+
super(message);
|
|
82
|
+
this.errorType = errorType;
|
|
83
|
+
this.statusCode = statusCode;
|
|
84
|
+
this.name = "KiteApiError";
|
|
85
|
+
}
|
|
86
|
+
errorType;
|
|
87
|
+
statusCode;
|
|
88
|
+
};
|
|
89
|
+
async function kiteGet(creds, path) {
|
|
90
|
+
const res = await fetch(`${KITE_BASE}${path}`, {
|
|
91
|
+
method: "GET",
|
|
92
|
+
headers: authHeaders(creds)
|
|
93
|
+
});
|
|
94
|
+
const body = await res.json();
|
|
95
|
+
if (res.status === 403 || body.error_type === "TokenException") {
|
|
96
|
+
throw new KiteTokenExpiredError(body.message);
|
|
97
|
+
}
|
|
98
|
+
if (body.status === "error") {
|
|
99
|
+
throw new KiteApiError(
|
|
100
|
+
body.message ?? "Unknown error",
|
|
101
|
+
body.error_type ?? "Unknown",
|
|
102
|
+
res.status
|
|
103
|
+
);
|
|
104
|
+
}
|
|
105
|
+
return body.data;
|
|
106
|
+
}
|
|
107
|
+
async function fetchMargins(creds) {
|
|
108
|
+
return kiteGet(creds, "/user/margins");
|
|
109
|
+
}
|
|
110
|
+
async function fetchHoldings(creds) {
|
|
111
|
+
return kiteGet(creds, "/portfolio/holdings");
|
|
112
|
+
}
|
|
113
|
+
async function fetchPositions(creds) {
|
|
114
|
+
return kiteGet(creds, "/portfolio/positions");
|
|
115
|
+
}
|
|
116
|
+
async function fetchOrders(creds) {
|
|
117
|
+
return kiteGet(creds, "/orders");
|
|
118
|
+
}
|
|
119
|
+
async function placeOrder(creds, params) {
|
|
120
|
+
const variety = params.variety ?? "regular";
|
|
121
|
+
const body = new URLSearchParams();
|
|
122
|
+
body.set("exchange", params.exchange);
|
|
123
|
+
body.set("tradingsymbol", params.tradingsymbol);
|
|
124
|
+
body.set("transaction_type", params.transaction_type);
|
|
125
|
+
body.set("quantity", String(params.quantity));
|
|
126
|
+
body.set("order_type", params.order_type);
|
|
127
|
+
body.set("product", params.product);
|
|
128
|
+
body.set("validity", params.validity ?? "DAY");
|
|
129
|
+
if (params.price != null) body.set("price", String(params.price));
|
|
130
|
+
if (params.trigger_price != null) body.set("trigger_price", String(params.trigger_price));
|
|
131
|
+
const res = await fetch(`${KITE_BASE}/orders/${variety}`, {
|
|
132
|
+
method: "POST",
|
|
133
|
+
headers: authHeaders(creds),
|
|
134
|
+
body: body.toString()
|
|
135
|
+
});
|
|
136
|
+
const json = await res.json();
|
|
137
|
+
if (res.status === 403 || json.error_type === "TokenException") {
|
|
138
|
+
throw new KiteTokenExpiredError(json.message);
|
|
139
|
+
}
|
|
140
|
+
if (json.status === "error") {
|
|
141
|
+
throw new KiteApiError(
|
|
142
|
+
json.message ?? "Order placement failed",
|
|
143
|
+
json.error_type ?? "Unknown",
|
|
144
|
+
res.status
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
return json.data;
|
|
148
|
+
}
|
|
149
|
+
async function cancelOrder(creds, orderId, variety = "regular") {
|
|
150
|
+
const res = await fetch(`${KITE_BASE}/orders/${variety}/${orderId}`, {
|
|
151
|
+
method: "DELETE",
|
|
152
|
+
headers: authHeaders(creds)
|
|
153
|
+
});
|
|
154
|
+
const json = await res.json();
|
|
155
|
+
if (res.status === 403 || json.error_type === "TokenException") {
|
|
156
|
+
throw new KiteTokenExpiredError(json.message);
|
|
157
|
+
}
|
|
158
|
+
if (json.status === "error") {
|
|
159
|
+
throw new KiteApiError(
|
|
160
|
+
json.message ?? "Order cancellation failed",
|
|
161
|
+
json.error_type ?? "Unknown",
|
|
162
|
+
res.status
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
return json.data;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
// src/store.ts
|
|
169
|
+
import Database from "better-sqlite3";
|
|
170
|
+
|
|
171
|
+
// src/schema.ts
|
|
172
|
+
var SQL_CREATE_HOLDINGS_CACHE = `
|
|
173
|
+
CREATE TABLE IF NOT EXISTS holdings_cache (
|
|
174
|
+
symbol TEXT PRIMARY KEY,
|
|
175
|
+
exchange TEXT NOT NULL,
|
|
176
|
+
isin TEXT,
|
|
177
|
+
quantity INTEGER NOT NULL,
|
|
178
|
+
t1_quantity INTEGER NOT NULL DEFAULT 0,
|
|
179
|
+
avg_price REAL NOT NULL,
|
|
180
|
+
ltp REAL,
|
|
181
|
+
pnl REAL,
|
|
182
|
+
pnl_pct REAL,
|
|
183
|
+
day_change REAL,
|
|
184
|
+
product TEXT NOT NULL,
|
|
185
|
+
refreshed_at INTEGER NOT NULL
|
|
186
|
+
) STRICT;
|
|
187
|
+
`;
|
|
188
|
+
var SQL_CREATE_ORDER_LOG = `
|
|
189
|
+
CREATE TABLE IF NOT EXISTS order_log (
|
|
190
|
+
id TEXT PRIMARY KEY,
|
|
191
|
+
created_at INTEGER NOT NULL,
|
|
192
|
+
symbol TEXT NOT NULL,
|
|
193
|
+
exchange TEXT NOT NULL,
|
|
194
|
+
transaction_type TEXT NOT NULL,
|
|
195
|
+
quantity INTEGER NOT NULL,
|
|
196
|
+
order_type TEXT NOT NULL,
|
|
197
|
+
price REAL,
|
|
198
|
+
product TEXT NOT NULL,
|
|
199
|
+
dry_run INTEGER NOT NULL,
|
|
200
|
+
kite_order_id TEXT,
|
|
201
|
+
status TEXT NOT NULL,
|
|
202
|
+
rejection_reason TEXT,
|
|
203
|
+
agent_session TEXT
|
|
204
|
+
) STRICT;
|
|
205
|
+
`;
|
|
206
|
+
var SQL_CREATE_SYNC_META = `
|
|
207
|
+
CREATE TABLE IF NOT EXISTS sync_meta (
|
|
208
|
+
key TEXT PRIMARY KEY,
|
|
209
|
+
fetched_at INTEGER NOT NULL,
|
|
210
|
+
status TEXT NOT NULL DEFAULT 'ok'
|
|
211
|
+
) STRICT;
|
|
212
|
+
`;
|
|
213
|
+
function migrate(db) {
|
|
214
|
+
db.pragma("journal_mode = WAL");
|
|
215
|
+
db.pragma("foreign_keys = ON");
|
|
216
|
+
db.exec(SQL_CREATE_HOLDINGS_CACHE);
|
|
217
|
+
db.exec(SQL_CREATE_ORDER_LOG);
|
|
218
|
+
db.exec(SQL_CREATE_SYNC_META);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// src/store.ts
|
|
222
|
+
var TTL = {
|
|
223
|
+
HOLDINGS: 60 * 60 * 1e3
|
|
224
|
+
// 1 hour
|
|
225
|
+
};
|
|
226
|
+
var ZerodhaStore = class {
|
|
227
|
+
db;
|
|
228
|
+
constructor(dbPath) {
|
|
229
|
+
this.db = new Database(dbPath);
|
|
230
|
+
migrate(this.db);
|
|
231
|
+
}
|
|
232
|
+
close() {
|
|
233
|
+
this.db.close();
|
|
234
|
+
}
|
|
235
|
+
// -- Holdings cache --------------------------------------------------------
|
|
236
|
+
replaceHoldings(holdings) {
|
|
237
|
+
const now = Date.now();
|
|
238
|
+
const tx = this.db.transaction(() => {
|
|
239
|
+
this.db.prepare("DELETE FROM holdings_cache").run();
|
|
240
|
+
const stmt = this.db.prepare(`
|
|
241
|
+
INSERT INTO holdings_cache
|
|
242
|
+
(symbol, exchange, isin, quantity, t1_quantity, avg_price, ltp, pnl, pnl_pct, day_change, product, refreshed_at)
|
|
243
|
+
VALUES
|
|
244
|
+
(@symbol, @exchange, @isin, @quantity, @t1Quantity, @avgPrice, @ltp, @pnl, @pnlPct, @dayChange, @product, @refreshedAt)
|
|
245
|
+
`);
|
|
246
|
+
for (const h of holdings) {
|
|
247
|
+
stmt.run({
|
|
248
|
+
symbol: h.symbol,
|
|
249
|
+
exchange: h.exchange,
|
|
250
|
+
isin: h.isin,
|
|
251
|
+
quantity: h.quantity,
|
|
252
|
+
t1Quantity: h.t1Quantity,
|
|
253
|
+
avgPrice: h.avgPrice,
|
|
254
|
+
ltp: h.ltp,
|
|
255
|
+
pnl: h.pnl,
|
|
256
|
+
pnlPct: h.pnlPct,
|
|
257
|
+
dayChange: h.dayChange,
|
|
258
|
+
product: h.product,
|
|
259
|
+
refreshedAt: h.refreshedAt
|
|
260
|
+
});
|
|
261
|
+
}
|
|
262
|
+
this.db.prepare("INSERT OR REPLACE INTO sync_meta (key, fetched_at, status) VALUES (?, ?, ?)").run("holdings", now, "ok");
|
|
263
|
+
});
|
|
264
|
+
tx();
|
|
265
|
+
}
|
|
266
|
+
getHoldings() {
|
|
267
|
+
const rows = this.db.prepare("SELECT * FROM holdings_cache").all();
|
|
268
|
+
return rows.map((r) => ({
|
|
269
|
+
symbol: r.symbol,
|
|
270
|
+
exchange: r.exchange,
|
|
271
|
+
isin: r.isin,
|
|
272
|
+
quantity: r.quantity,
|
|
273
|
+
t1Quantity: r.t1_quantity,
|
|
274
|
+
avgPrice: r.avg_price,
|
|
275
|
+
ltp: r.ltp,
|
|
276
|
+
pnl: r.pnl,
|
|
277
|
+
pnlPct: r.pnl_pct,
|
|
278
|
+
dayChange: r.day_change,
|
|
279
|
+
product: r.product,
|
|
280
|
+
refreshedAt: r.refreshed_at
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
isStale(key, ttlMs) {
|
|
284
|
+
const row = this.db.prepare("SELECT fetched_at FROM sync_meta WHERE key = ?").get(key);
|
|
285
|
+
if (!row) return true;
|
|
286
|
+
return Date.now() - row.fetched_at > ttlMs;
|
|
287
|
+
}
|
|
288
|
+
getLastFetchedAt(key) {
|
|
289
|
+
const row = this.db.prepare("SELECT fetched_at FROM sync_meta WHERE key = ?").get(key);
|
|
290
|
+
return row?.fetched_at ?? 0;
|
|
291
|
+
}
|
|
292
|
+
setSyncMeta(key, status = "ok") {
|
|
293
|
+
this.db.prepare("INSERT OR REPLACE INTO sync_meta (key, fetched_at, status) VALUES (?, ?, ?)").run(key, Date.now(), status);
|
|
294
|
+
}
|
|
295
|
+
// -- Order audit log -------------------------------------------------------
|
|
296
|
+
logOrder(row) {
|
|
297
|
+
this.db.prepare(
|
|
298
|
+
`INSERT INTO order_log
|
|
299
|
+
(id, created_at, symbol, exchange, transaction_type, quantity, order_type, price, product, dry_run, kite_order_id, status, rejection_reason, agent_session)
|
|
300
|
+
VALUES
|
|
301
|
+
(@id, @createdAt, @symbol, @exchange, @transactionType, @quantity, @orderType, @price, @product, @dryRun, @kiteOrderId, @status, @rejectionReason, @agentSession)`
|
|
302
|
+
).run({
|
|
303
|
+
id: row.id,
|
|
304
|
+
createdAt: row.createdAt,
|
|
305
|
+
symbol: row.symbol,
|
|
306
|
+
exchange: row.exchange,
|
|
307
|
+
transactionType: row.transaction,
|
|
308
|
+
quantity: row.quantity,
|
|
309
|
+
orderType: row.orderType,
|
|
310
|
+
price: row.price,
|
|
311
|
+
product: row.product,
|
|
312
|
+
dryRun: row.dryRun ? 1 : 0,
|
|
313
|
+
kiteOrderId: row.kiteOrderId,
|
|
314
|
+
status: row.status,
|
|
315
|
+
rejectionReason: row.rejectionReason,
|
|
316
|
+
agentSession: row.agentSession
|
|
317
|
+
});
|
|
318
|
+
}
|
|
319
|
+
getOrderLog(limit = 50) {
|
|
320
|
+
const rows = this.db.prepare("SELECT * FROM order_log ORDER BY created_at DESC LIMIT ?").all(limit);
|
|
321
|
+
return rows.map((r) => ({
|
|
322
|
+
id: r.id,
|
|
323
|
+
createdAt: r.created_at,
|
|
324
|
+
symbol: r.symbol,
|
|
325
|
+
exchange: r.exchange,
|
|
326
|
+
transaction: r.transaction_type,
|
|
327
|
+
quantity: r.quantity,
|
|
328
|
+
orderType: r.order_type,
|
|
329
|
+
price: r.price,
|
|
330
|
+
product: r.product,
|
|
331
|
+
dryRun: r.dry_run === 1,
|
|
332
|
+
kiteOrderId: r.kite_order_id,
|
|
333
|
+
status: r.status,
|
|
334
|
+
rejectionReason: r.rejection_reason,
|
|
335
|
+
agentSession: r.agent_session
|
|
336
|
+
}));
|
|
337
|
+
}
|
|
338
|
+
clean() {
|
|
339
|
+
this.db.prepare("DELETE FROM holdings_cache").run();
|
|
340
|
+
this.db.prepare("DELETE FROM order_log").run();
|
|
341
|
+
this.db.prepare("DELETE FROM sync_meta").run();
|
|
342
|
+
return { tablesCleared: ["holdings_cache", "order_log", "sync_meta"] };
|
|
343
|
+
}
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
// src/cli.ts
|
|
347
|
+
function getPackageRoot() {
|
|
348
|
+
const __filename = fileURLToPath(import.meta.url);
|
|
349
|
+
return join(dirname(__filename), "..");
|
|
350
|
+
}
|
|
351
|
+
function getFlag(args, flag) {
|
|
352
|
+
const i = args.indexOf(flag);
|
|
353
|
+
return i !== -1 ? args[i + 1] : void 0;
|
|
354
|
+
}
|
|
355
|
+
function hasFlag(args, flag) {
|
|
356
|
+
return args.includes(flag);
|
|
357
|
+
}
|
|
358
|
+
function getDbPath() {
|
|
359
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
360
|
+
const dbPath = process.env.ZERODHA_DB ?? `${home}/.ethos/zerodha/zerodha.db`;
|
|
361
|
+
const dir = dirname(dbPath);
|
|
362
|
+
if (!existsSync(dir)) {
|
|
363
|
+
mkdirSync(dir, { recursive: true });
|
|
364
|
+
}
|
|
365
|
+
return dbPath;
|
|
366
|
+
}
|
|
367
|
+
function secretsDir() {
|
|
368
|
+
const home = process.env.HOME ?? process.env.USERPROFILE ?? "/tmp";
|
|
369
|
+
return `${home}/.ethos/secrets/brokers/zerodha`;
|
|
370
|
+
}
|
|
371
|
+
function readSecret(name) {
|
|
372
|
+
try {
|
|
373
|
+
const path = join(secretsDir(), name);
|
|
374
|
+
return readFileSync(path, "utf-8").trim();
|
|
375
|
+
} catch {
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
function writeSecret(name, value) {
|
|
380
|
+
const dir = secretsDir();
|
|
381
|
+
if (!existsSync(dir)) {
|
|
382
|
+
mkdirSync(dir, { recursive: true });
|
|
383
|
+
}
|
|
384
|
+
writeFileSync(join(dir, name), value, { mode: 384 });
|
|
385
|
+
}
|
|
386
|
+
function getCredentials() {
|
|
387
|
+
const apiKey = readSecret("apiKey");
|
|
388
|
+
const accessToken = readSecret("accessToken");
|
|
389
|
+
if (!apiKey || !accessToken) {
|
|
390
|
+
console.error(
|
|
391
|
+
"Missing credentials. Store api_key and access_token in ~/.ethos/secrets/brokers/zerodha/"
|
|
392
|
+
);
|
|
393
|
+
process.exit(1);
|
|
394
|
+
}
|
|
395
|
+
return { apiKey, accessToken };
|
|
396
|
+
}
|
|
397
|
+
function printHelp() {
|
|
398
|
+
console.log(`zerodha-broker \u2014 Zerodha Kite Connect CLI for Ethos agents
|
|
399
|
+
|
|
400
|
+
Commands:
|
|
401
|
+
auth Print login URL and instructions
|
|
402
|
+
auth --request-token TOKEN Exchange request_token for access_token
|
|
403
|
+
auth status Check if stored access token is valid
|
|
404
|
+
|
|
405
|
+
holdings Print all equity holdings with P&L
|
|
406
|
+
positions Print open positions (intraday + overnight)
|
|
407
|
+
orders Print today's order book
|
|
408
|
+
margins Print available funds and margin utilisation
|
|
409
|
+
|
|
410
|
+
order --symbol SYM --qty N --side BUY|SELL --type MARKET|LIMIT [--price P]
|
|
411
|
+
Simulate an order (dry-run)
|
|
412
|
+
order --confirm ... Place a real order
|
|
413
|
+
cancel --order-id ID Cancel a pending order
|
|
414
|
+
|
|
415
|
+
log [--limit N] Print agent order log (default: last 20)
|
|
416
|
+
clean Wipe local cache and order log
|
|
417
|
+
|
|
418
|
+
version
|
|
419
|
+
help
|
|
420
|
+
|
|
421
|
+
Environment:
|
|
422
|
+
ZERODHA_DB=~/.ethos/zerodha/zerodha.db (default)
|
|
423
|
+
|
|
424
|
+
Examples:
|
|
425
|
+
zerodha-broker auth
|
|
426
|
+
zerodha-broker auth --request-token abc123
|
|
427
|
+
zerodha-broker auth status
|
|
428
|
+
zerodha-broker holdings
|
|
429
|
+
zerodha-broker order --symbol RELIANCE --qty 10 --side BUY --type LIMIT --price 2980
|
|
430
|
+
zerodha-broker order --symbol RELIANCE --qty 10 --side BUY --type LIMIT --price 2980 --confirm
|
|
431
|
+
`);
|
|
432
|
+
}
|
|
433
|
+
async function cmdAuth(args) {
|
|
434
|
+
const requestToken = getFlag(args, "--request-token");
|
|
435
|
+
if (args[1] === "status") {
|
|
436
|
+
const { apiKey: apiKey2, accessToken } = getCredentials();
|
|
437
|
+
const result = await validateToken(apiKey2, accessToken);
|
|
438
|
+
if (result.valid) {
|
|
439
|
+
console.log(`Token valid. User: ${result.userId}. Expires: ${result.expiresHint}`);
|
|
440
|
+
} else {
|
|
441
|
+
console.log(`Token expired or invalid. ${result.expiresHint}`);
|
|
442
|
+
console.log(`Run: zerodha-broker auth --request-token TOKEN`);
|
|
443
|
+
}
|
|
444
|
+
return;
|
|
445
|
+
}
|
|
446
|
+
if (requestToken) {
|
|
447
|
+
const apiKey2 = readSecret("apiKey");
|
|
448
|
+
const apiSecret = readSecret("apiSecret");
|
|
449
|
+
if (!apiKey2 || !apiSecret) {
|
|
450
|
+
console.error("Missing apiKey or apiSecret in ~/.ethos/secrets/brokers/zerodha/");
|
|
451
|
+
process.exit(1);
|
|
452
|
+
}
|
|
453
|
+
console.log("Exchanging request_token for access_token...");
|
|
454
|
+
const result = await exchangeToken(apiKey2, apiSecret, requestToken);
|
|
455
|
+
writeSecret("accessToken", result.access_token);
|
|
456
|
+
console.log(`Success! User: ${result.user_id}`);
|
|
457
|
+
console.log(`Access token stored. Valid until midnight IST.`);
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
const apiKey = readSecret("apiKey");
|
|
461
|
+
if (!apiKey) {
|
|
462
|
+
console.error("Missing apiKey in ~/.ethos/secrets/brokers/zerodha/apiKey");
|
|
463
|
+
process.exit(1);
|
|
464
|
+
}
|
|
465
|
+
const url = buildLoginUrl(apiKey);
|
|
466
|
+
console.log(`Open this URL in your browser to log in:
|
|
467
|
+
|
|
468
|
+
${url}
|
|
469
|
+
`);
|
|
470
|
+
console.log("After login, copy the request_token from the redirect URL and run:");
|
|
471
|
+
console.log(" zerodha-broker auth --request-token TOKEN");
|
|
472
|
+
}
|
|
473
|
+
async function cmdHoldings() {
|
|
474
|
+
const { apiKey, accessToken } = getCredentials();
|
|
475
|
+
const holdings = await fetchHoldings({ apiKey, accessToken });
|
|
476
|
+
if (holdings.length === 0) {
|
|
477
|
+
console.log("No holdings found.");
|
|
478
|
+
return;
|
|
479
|
+
}
|
|
480
|
+
const store = new ZerodhaStore(getDbPath());
|
|
481
|
+
try {
|
|
482
|
+
const now = Date.now();
|
|
483
|
+
const rows = holdings.map((h) => ({
|
|
484
|
+
symbol: h.tradingsymbol,
|
|
485
|
+
exchange: h.exchange,
|
|
486
|
+
isin: h.isin,
|
|
487
|
+
quantity: h.quantity,
|
|
488
|
+
t1Quantity: h.t1_quantity,
|
|
489
|
+
avgPrice: h.average_price,
|
|
490
|
+
ltp: h.last_price,
|
|
491
|
+
pnl: h.pnl,
|
|
492
|
+
pnlPct: h.average_price > 0 ? Number((h.pnl / (h.quantity * h.average_price) * 100).toFixed(2)) : null,
|
|
493
|
+
dayChange: h.day_change_percentage,
|
|
494
|
+
product: h.product,
|
|
495
|
+
refreshedAt: now
|
|
496
|
+
}));
|
|
497
|
+
store.replaceHoldings(rows);
|
|
498
|
+
} finally {
|
|
499
|
+
store.close();
|
|
500
|
+
}
|
|
501
|
+
console.log("Symbol Qty Avg Price LTP P&L P&L%");
|
|
502
|
+
console.log("\u2500".repeat(70));
|
|
503
|
+
for (const h of holdings) {
|
|
504
|
+
const pnlPct = h.average_price > 0 ? (h.pnl / (h.quantity * h.average_price) * 100).toFixed(2) : "0.00";
|
|
505
|
+
console.log(
|
|
506
|
+
`${h.tradingsymbol.padEnd(16)}${String(h.quantity).padStart(4)} ${h.average_price.toFixed(2).padStart(10)} ${h.last_price.toFixed(2).padStart(10)} ${h.pnl.toFixed(2).padStart(10)} ${pnlPct.padStart(6)}%`
|
|
507
|
+
);
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
async function cmdPositions() {
|
|
511
|
+
const { apiKey, accessToken } = getCredentials();
|
|
512
|
+
const positions = await fetchPositions({ apiKey, accessToken });
|
|
513
|
+
if (positions.day.length === 0 && positions.net.length === 0) {
|
|
514
|
+
console.log("No open positions.");
|
|
515
|
+
return;
|
|
516
|
+
}
|
|
517
|
+
if (positions.day.length > 0) {
|
|
518
|
+
console.log("Day positions:");
|
|
519
|
+
for (const p of positions.day) {
|
|
520
|
+
console.log(
|
|
521
|
+
` ${p.tradingsymbol} ${p.product} qty=${p.quantity} avg=${p.average_price} ltp=${p.last_price} pnl=${p.pnl}`
|
|
522
|
+
);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (positions.net.length > 0) {
|
|
526
|
+
console.log("Net positions:");
|
|
527
|
+
for (const p of positions.net) {
|
|
528
|
+
console.log(
|
|
529
|
+
` ${p.tradingsymbol} ${p.product} qty=${p.quantity} avg=${p.average_price} ltp=${p.last_price} pnl=${p.pnl}`
|
|
530
|
+
);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
async function cmdOrders() {
|
|
535
|
+
const { apiKey, accessToken } = getCredentials();
|
|
536
|
+
const orders = await fetchOrders({ apiKey, accessToken });
|
|
537
|
+
if (orders.length === 0) {
|
|
538
|
+
console.log("No orders today.");
|
|
539
|
+
return;
|
|
540
|
+
}
|
|
541
|
+
for (const o of orders) {
|
|
542
|
+
console.log(
|
|
543
|
+
`${o.order_id} ${o.tradingsymbol} ${o.transaction_type} qty=${o.quantity} type=${o.order_type} price=${o.price} status=${o.status} ${o.order_timestamp}`
|
|
544
|
+
);
|
|
545
|
+
}
|
|
546
|
+
}
|
|
547
|
+
async function cmdMargins() {
|
|
548
|
+
const { apiKey, accessToken } = getCredentials();
|
|
549
|
+
const margins = await fetchMargins({ apiKey, accessToken });
|
|
550
|
+
const eq = margins.equity;
|
|
551
|
+
console.log(`Equity margins:`);
|
|
552
|
+
console.log(` Net available: INR ${eq.net}`);
|
|
553
|
+
console.log(` Opening balance: INR ${eq.available.opening_balance}`);
|
|
554
|
+
console.log(` M2M unrealised: INR ${eq.utilised.m2m_unrealised}`);
|
|
555
|
+
console.log(` Exposure used: INR ${eq.utilised.exposure}`);
|
|
556
|
+
console.log(` Option premium: INR ${eq.utilised.option_premium}`);
|
|
557
|
+
console.log(` Equity enabled: ${eq.enabled}`);
|
|
558
|
+
}
|
|
559
|
+
async function cmdOrder(args) {
|
|
560
|
+
const symbol = getFlag(args, "--symbol");
|
|
561
|
+
const qtyStr = getFlag(args, "--qty");
|
|
562
|
+
const side = getFlag(args, "--side");
|
|
563
|
+
const orderType = getFlag(args, "--type");
|
|
564
|
+
const priceStr = getFlag(args, "--price");
|
|
565
|
+
const confirm = hasFlag(args, "--confirm");
|
|
566
|
+
if (!symbol || !qtyStr || !side || !orderType) {
|
|
567
|
+
console.error("Required: --symbol, --qty, --side, --type");
|
|
568
|
+
process.exit(1);
|
|
569
|
+
}
|
|
570
|
+
const quantity = Number.parseInt(qtyStr, 10);
|
|
571
|
+
const price = priceStr ? Number.parseFloat(priceStr) : void 0;
|
|
572
|
+
const exchange = getFlag(args, "--exchange") ?? "NSE";
|
|
573
|
+
if (confirm) {
|
|
574
|
+
const { apiKey, accessToken } = getCredentials();
|
|
575
|
+
const result = await placeOrder(
|
|
576
|
+
{ apiKey, accessToken },
|
|
577
|
+
{
|
|
578
|
+
exchange,
|
|
579
|
+
tradingsymbol: symbol,
|
|
580
|
+
transaction_type: side,
|
|
581
|
+
quantity,
|
|
582
|
+
order_type: orderType,
|
|
583
|
+
product: "CNC",
|
|
584
|
+
price
|
|
585
|
+
}
|
|
586
|
+
);
|
|
587
|
+
console.log(`Order placed: ${result.order_id}`);
|
|
588
|
+
const store = new ZerodhaStore(getDbPath());
|
|
589
|
+
try {
|
|
590
|
+
store.logOrder({
|
|
591
|
+
id: crypto.randomUUID(),
|
|
592
|
+
createdAt: Date.now(),
|
|
593
|
+
symbol,
|
|
594
|
+
exchange,
|
|
595
|
+
transaction: side,
|
|
596
|
+
quantity,
|
|
597
|
+
orderType,
|
|
598
|
+
price: price ?? null,
|
|
599
|
+
product: "CNC",
|
|
600
|
+
dryRun: false,
|
|
601
|
+
kiteOrderId: result.order_id,
|
|
602
|
+
status: "placed",
|
|
603
|
+
rejectionReason: null,
|
|
604
|
+
agentSession: null
|
|
605
|
+
});
|
|
606
|
+
} finally {
|
|
607
|
+
store.close();
|
|
608
|
+
}
|
|
609
|
+
} else {
|
|
610
|
+
const priceDesc = orderType === "LIMIT" && price != null ? `LIMIT ${price.toFixed(2)}` : "MARKET";
|
|
611
|
+
console.log(
|
|
612
|
+
`DRY RUN: Would place ${side} ${quantity} ${symbol} @ ${priceDesc} on ${exchange} (CNC)`
|
|
613
|
+
);
|
|
614
|
+
console.log("Add --confirm to place the order for real.");
|
|
615
|
+
const store = new ZerodhaStore(getDbPath());
|
|
616
|
+
try {
|
|
617
|
+
store.logOrder({
|
|
618
|
+
id: crypto.randomUUID(),
|
|
619
|
+
createdAt: Date.now(),
|
|
620
|
+
symbol,
|
|
621
|
+
exchange,
|
|
622
|
+
transaction: side,
|
|
623
|
+
quantity,
|
|
624
|
+
orderType,
|
|
625
|
+
price: price ?? null,
|
|
626
|
+
product: "CNC",
|
|
627
|
+
dryRun: true,
|
|
628
|
+
kiteOrderId: null,
|
|
629
|
+
status: "dry_run",
|
|
630
|
+
rejectionReason: null,
|
|
631
|
+
agentSession: null
|
|
632
|
+
});
|
|
633
|
+
} finally {
|
|
634
|
+
store.close();
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
async function cmdCancel(args) {
|
|
639
|
+
const orderId = getFlag(args, "--order-id");
|
|
640
|
+
if (!orderId) {
|
|
641
|
+
console.error("Required: --order-id");
|
|
642
|
+
process.exit(1);
|
|
643
|
+
}
|
|
644
|
+
const { apiKey, accessToken } = getCredentials();
|
|
645
|
+
const result = await cancelOrder({ apiKey, accessToken }, orderId);
|
|
646
|
+
console.log(`Order cancelled: ${result.order_id}`);
|
|
647
|
+
}
|
|
648
|
+
function cmdLog(args) {
|
|
649
|
+
const limitStr = getFlag(args, "--limit");
|
|
650
|
+
const limit = limitStr ? Number.parseInt(limitStr, 10) : 20;
|
|
651
|
+
const store = new ZerodhaStore(getDbPath());
|
|
652
|
+
try {
|
|
653
|
+
const log = store.getOrderLog(limit);
|
|
654
|
+
if (log.length === 0) {
|
|
655
|
+
console.log("No orders in log.");
|
|
656
|
+
return;
|
|
657
|
+
}
|
|
658
|
+
for (const o of log) {
|
|
659
|
+
const ts = new Date(o.createdAt).toISOString();
|
|
660
|
+
const dryTag = o.dryRun ? "[DRY]" : "[REAL]";
|
|
661
|
+
console.log(
|
|
662
|
+
`${ts} ${dryTag} ${o.transaction} ${o.quantity} ${o.symbol} @ ${o.orderType} ${o.price ?? "MKT"} status=${o.status} kite_id=${o.kiteOrderId ?? "n/a"}`
|
|
663
|
+
);
|
|
664
|
+
}
|
|
665
|
+
} finally {
|
|
666
|
+
store.close();
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
function cmdClean() {
|
|
670
|
+
const store = new ZerodhaStore(getDbPath());
|
|
671
|
+
try {
|
|
672
|
+
const result = store.clean();
|
|
673
|
+
console.log(`Cleared tables: ${result.tablesCleared.join(", ")}`);
|
|
674
|
+
} finally {
|
|
675
|
+
store.close();
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
async function main() {
|
|
679
|
+
const args = process.argv.slice(2);
|
|
680
|
+
const command = args[0];
|
|
681
|
+
if (!command || command === "--help" || command === "-h" || command === "help") {
|
|
682
|
+
printHelp();
|
|
683
|
+
return;
|
|
684
|
+
}
|
|
685
|
+
if (command === "version") {
|
|
686
|
+
const pkgPath = join(getPackageRoot(), "package.json");
|
|
687
|
+
const pkg = JSON.parse(readFileSync(pkgPath, "utf-8"));
|
|
688
|
+
console.log(pkg.version);
|
|
689
|
+
return;
|
|
690
|
+
}
|
|
691
|
+
if (command === "auth") {
|
|
692
|
+
await cmdAuth(args);
|
|
693
|
+
return;
|
|
694
|
+
}
|
|
695
|
+
if (command === "holdings") {
|
|
696
|
+
await cmdHoldings();
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
699
|
+
if (command === "positions") {
|
|
700
|
+
await cmdPositions();
|
|
701
|
+
return;
|
|
702
|
+
}
|
|
703
|
+
if (command === "orders") {
|
|
704
|
+
await cmdOrders();
|
|
705
|
+
return;
|
|
706
|
+
}
|
|
707
|
+
if (command === "margins") {
|
|
708
|
+
await cmdMargins();
|
|
709
|
+
return;
|
|
710
|
+
}
|
|
711
|
+
if (command === "order") {
|
|
712
|
+
await cmdOrder(args);
|
|
713
|
+
return;
|
|
714
|
+
}
|
|
715
|
+
if (command === "cancel") {
|
|
716
|
+
await cmdCancel(args);
|
|
717
|
+
return;
|
|
718
|
+
}
|
|
719
|
+
if (command === "log") {
|
|
720
|
+
cmdLog(args);
|
|
721
|
+
return;
|
|
722
|
+
}
|
|
723
|
+
if (command === "clean") {
|
|
724
|
+
cmdClean();
|
|
725
|
+
return;
|
|
726
|
+
}
|
|
727
|
+
console.error(`Unknown command: ${command}
|
|
728
|
+
Run "zerodha-broker help" for usage.`);
|
|
729
|
+
process.exit(1);
|
|
730
|
+
}
|
|
731
|
+
main().catch((err) => {
|
|
732
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
733
|
+
process.exit(1);
|
|
734
|
+
});
|
|
735
|
+
//# sourceMappingURL=cli.js.map
|