@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/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