shoonya-sdk 0.3.1

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.
@@ -0,0 +1,595 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __name = (target, value) => __defProp(target, "name", { value, configurable: true });
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/shoonya-sdk.ts
31
+ var shoonya_sdk_exports = {};
32
+ __export(shoonya_sdk_exports, {
33
+ Shoonya: () => Shoonya
34
+ });
35
+ module.exports = __toCommonJS(shoonya_sdk_exports);
36
+
37
+ // src/utils/sha256.ts
38
+ var import_node_crypto = __toESM(require("crypto"));
39
+ function sha256(data) {
40
+ const hash = import_node_crypto.default.createHash("sha256");
41
+ return hash.update(data, "utf8").digest("hex");
42
+ }
43
+ __name(sha256, "sha256");
44
+
45
+ // src/paths.ts
46
+ var paths = {
47
+ login: "QuickAuth",
48
+ logout: "Logout",
49
+ forgotPassword: "ForgotPassword",
50
+ changePassword: "Changepwd",
51
+ watchlist: "MWList",
52
+ getWatchlist: "MarketWatch",
53
+ searchScrip: "SearchScrip",
54
+ userInfo: "UserDetails",
55
+ clientInfo: "ClientDetails",
56
+ quotes: "GetQuotes",
57
+ addScripToWL: "AddMultiScripsToMW",
58
+ removeScripFromWL: "DeleteMultiMWScrips",
59
+ securityInfo: "GetSecurityInfo",
60
+ placeOrder: "PlaceOrder",
61
+ modifyOrder: "ModifyOrder",
62
+ cancelOrder: "CancelOrder",
63
+ exitSNOOrder: "ExitSNOOrder",
64
+ historicData: "TPSeries",
65
+ optionChain: "GetOptionChain",
66
+ orderBook: "OrderBook"
67
+ };
68
+
69
+ // src/shoonya-sdk.ts
70
+ var import_ws = require("ws");
71
+ var import_events = require("events");
72
+ var import_node_cron = __toESM(require("node-cron"));
73
+
74
+ // src/utils/logger.ts
75
+ var import_fs = require("fs");
76
+ var import_path = require("path");
77
+ var Logger = class {
78
+ static {
79
+ __name(this, "Logger");
80
+ }
81
+ logPath;
82
+ maxSize;
83
+ /**
84
+ *
85
+ * @param logPath path of the log file
86
+ * @param maxSize max size a log file should have (in bytes)
87
+ */
88
+ constructor(logPath, maxSize) {
89
+ this.logPath = logPath;
90
+ this.maxSize = maxSize;
91
+ if (!(0, import_fs.existsSync)(this.logPath)) {
92
+ (0, import_fs.mkdirSync)((0, import_path.dirname)(this.logPath), { recursive: true });
93
+ (0, import_fs.writeFileSync)(this.logPath, "", "utf8");
94
+ }
95
+ }
96
+ checkLogSize() {
97
+ const dirName = (0, import_path.dirname)(this.logPath);
98
+ try {
99
+ const stats = (0, import_fs.statSync)(this.logPath);
100
+ if (stats.size >= this.maxSize) {
101
+ const rotatedPath = (0, import_path.join)(dirName, `log_${Date.now().toString()}.log`);
102
+ this.logPath = rotatedPath;
103
+ (0, import_fs.writeFileSync)(rotatedPath, "", "utf-8");
104
+ }
105
+ } catch (err) {
106
+ throw new Error("file not found");
107
+ }
108
+ this.deleteOldestFile();
109
+ }
110
+ log(message, type = "INFO") {
111
+ this.checkLogSize();
112
+ const logMessage = `${type}: [${(/* @__PURE__ */ new Date()).toISOString()}]: ${message}
113
+ `;
114
+ (0, import_fs.appendFileSync)(this.logPath, logMessage, { encoding: "utf-8" });
115
+ }
116
+ getRecentLog() {
117
+ const dir = (0, import_fs.readdirSync)((0, import_path.dirname)(this.logPath), { recursive: true });
118
+ const file = (0, import_fs.readFileSync)(dir.at(-1), "utf8");
119
+ return file;
120
+ }
121
+ deleteOldestFile() {
122
+ const dirName = (0, import_path.dirname)(this.logPath);
123
+ const files = (0, import_fs.readdirSync)(dirName);
124
+ const currentDate = /* @__PURE__ */ new Date();
125
+ const sevenDaysAgo = (/* @__PURE__ */ new Date()).setDate(currentDate.getDate() - 7);
126
+ for (const file of files) {
127
+ const filePath = (0, import_path.join)(dirName, file);
128
+ const stat = (0, import_fs.statSync)(filePath);
129
+ if (stat.birthtimeMs < sevenDaysAgo) {
130
+ (0, import_fs.unlinkSync)(filePath);
131
+ }
132
+ }
133
+ }
134
+ };
135
+ var logger_default = Logger;
136
+
137
+ // src/shoonya-sdk.ts
138
+ var import_totp_generator = __toESM(require("totp-generator"));
139
+ var Shoonya = class extends import_events.EventEmitter {
140
+ static {
141
+ __name(this, "Shoonya");
142
+ }
143
+ accessToken;
144
+ userId;
145
+ logger;
146
+ logging = false;
147
+ httpBaseUrl = "https://api.shoonya.com/NorenWClientTP/";
148
+ wsBaseUrl = "wss://api.shoonya.com/NorenWSTP/";
149
+ ws;
150
+ connectTimer;
151
+ disconnectTimer;
152
+ accountId;
153
+ twoFa;
154
+ password;
155
+ apiKey;
156
+ cronJobRunning = false;
157
+ scripList;
158
+ constructor(options) {
159
+ const { logging = false } = options || {};
160
+ super({ captureRejections: true });
161
+ this.userId = "";
162
+ this.accessToken = "";
163
+ this.accountId = this.userId;
164
+ this.logging = logging;
165
+ if (logging) {
166
+ this.logger = new logger_default(
167
+ `./logs/log_${Date.now().toString()}.log`,
168
+ 1024 * 10
169
+ //1KB * 10 = 10KB
170
+ );
171
+ }
172
+ }
173
+ async request(path, body) {
174
+ const jData = `jData=${JSON.stringify(body.data)}${body.key ? "&jKey=" + body.key : ""}`;
175
+ try {
176
+ const data = await fetch(this.httpBaseUrl + paths[path], {
177
+ method: "POST",
178
+ body: jData,
179
+ keepalive: true
180
+ }).then((res) => {
181
+ return res.json();
182
+ });
183
+ return data;
184
+ } catch (err) {
185
+ if (this.logging) {
186
+ const errMessage = "Failed to fetch data from api while performing: " + path;
187
+ this.logger.log(errMessage, "ERROR");
188
+ console.error(errMessage);
189
+ }
190
+ console.error(err);
191
+ }
192
+ }
193
+ // Rest API
194
+ /**
195
+ *
196
+ * @param credentials User Credential (username, password, appkeys etc.)
197
+ * @returns user information and access token
198
+ */
199
+ async login(rawCred) {
200
+ const cred = {
201
+ apkversion: "1.0.0",
202
+ uid: rawCred.userId,
203
+ pwd: sha256(rawCred.password),
204
+ factor2: (0, import_totp_generator.default)(rawCred.twoFa),
205
+ vc: rawCred?.vendorCode || rawCred.userId + "_U",
206
+ appkey: sha256(`${rawCred.userId}|${rawCred.apiKey}`),
207
+ imei: rawCred?.imei || "api",
208
+ source: "API"
209
+ };
210
+ try {
211
+ const req = await this.request("login", { data: cred });
212
+ this.userId = req.uid;
213
+ this.accessToken = req.susertoken;
214
+ this.accountId = this.userId;
215
+ this.apiKey = rawCred.apiKey;
216
+ this.password = rawCred.password;
217
+ this.twoFa = rawCred.twoFa;
218
+ return req;
219
+ } catch (err) {
220
+ if (this.logging) {
221
+ const fallBackMessage = "Attempted to login but it failed";
222
+ const errMessage = err?.message || fallBackMessage;
223
+ this.logger.log(errMessage, "ERROR");
224
+ console.error(errMessage);
225
+ }
226
+ console.error("Login Failed", err);
227
+ }
228
+ }
229
+ /**
230
+ * @param query scrip name (BankNifty, Sensex etc.)
231
+ * @param exchange exchange name (NSE, BSE etc.)
232
+ * @returns
233
+ */
234
+ async searchScrip(query, exchange) {
235
+ if (!this.accessToken) {
236
+ throw "Login First before seaching";
237
+ }
238
+ const data = {
239
+ uid: this.userId,
240
+ stext: query,
241
+ exch: exchange
242
+ };
243
+ return this.request("searchScrip", {
244
+ data,
245
+ key: this.accessToken
246
+ });
247
+ }
248
+ async getWatchlistsName() {
249
+ if (!this.accessToken) {
250
+ throw "Login First before accessing watchlists";
251
+ }
252
+ return this.request("watchlist", {
253
+ data: { uid: this.userId },
254
+ key: this.accessToken
255
+ });
256
+ }
257
+ async getWatchlist(listName) {
258
+ if (!this.accessToken) {
259
+ throw "Login First before accessing watchlists";
260
+ }
261
+ return this.request("getWatchlist", {
262
+ data: { uid: this.userId, wlname: listName },
263
+ key: this.accessToken
264
+ });
265
+ }
266
+ async forgetPassword(PAN, DOB) {
267
+ const data = {
268
+ uid: this.userId,
269
+ pan: PAN,
270
+ dob: DOB
271
+ };
272
+ return this.request("forgotPassword", { data });
273
+ }
274
+ /**
275
+ *
276
+ * @param oldPass sha256 of old pass
277
+ * @param newPass new password in plain text
278
+ * @returns
279
+ */
280
+ async changePassword(oldPass, newPass) {
281
+ const data = {
282
+ uid: this.userId,
283
+ oldpwd: oldPass,
284
+ pwd: newPass
285
+ };
286
+ return this.request("changePassword", { data });
287
+ }
288
+ async getUserDetails() {
289
+ return this.request("userInfo", {
290
+ data: { uid: this.userId },
291
+ key: this.accessToken
292
+ });
293
+ }
294
+ /**
295
+ * @param brokerId logged in user's brokers Name / Id
296
+ * @returns
297
+ */
298
+ async getClientDetails(brokerId) {
299
+ const data = {
300
+ uid: this.userId,
301
+ actid: this.accountId,
302
+ brkname: brokerId
303
+ };
304
+ return this.request("clientInfo", { data, key: this.accessToken });
305
+ }
306
+ async getQuotes(exchange, contractToken) {
307
+ const data = {
308
+ uid: this.userId,
309
+ exch: exchange,
310
+ token: contractToken
311
+ };
312
+ return this.request("quotes", { data, key: this.accessToken });
313
+ }
314
+ /**
315
+ *
316
+ * @param listName name of watchlist in which user want to add script
317
+ * @param scrips list of the scrip
318
+ * @returns
319
+ */
320
+ async addScripToWatchList(listName, scrips) {
321
+ const data = {
322
+ uid: this.userId,
323
+ wlname: listName,
324
+ scrips: scrips.join("#")
325
+ };
326
+ return this.request("addScripToWL", { data, key: this.accessToken });
327
+ }
328
+ /**
329
+ *
330
+ * @param listName name of watchlist in which user want to add script
331
+ * @param scrips list of the scrip
332
+ * @returns
333
+ */
334
+ async removeScripFromWatchList(listName, scrips) {
335
+ const data = {
336
+ uid: this.userId,
337
+ wlname: listName,
338
+ scrips: scrips.join("#")
339
+ };
340
+ return this.request("removeScripFromWL", { data, key: this.accessToken });
341
+ }
342
+ async getSecurityInfo(exchange, contractToken) {
343
+ const data = {
344
+ uid: this.userId,
345
+ exch: exchange,
346
+ token: contractToken
347
+ };
348
+ return this.request("securityInfo", { data, key: this.accessToken });
349
+ }
350
+ async placeOrder(details) {
351
+ const data = {
352
+ uid: details.uid || this.userId,
353
+ actid: details.actid || this.accountId,
354
+ ...details
355
+ };
356
+ return this.request("placeOrder", {
357
+ data,
358
+ key: this.accessToken
359
+ });
360
+ }
361
+ async modifyOrder(orderDetail) {
362
+ orderDetail.uid ??= this.userId;
363
+ orderDetail.actid ??= this.accountId;
364
+ return this.request("modifyOrder", {
365
+ data: orderDetail,
366
+ key: this.accessToken
367
+ });
368
+ }
369
+ async cancelOrder(orderNo) {
370
+ return this.request("cancelOrder", {
371
+ data: {
372
+ uid: this.userId,
373
+ norenordno: orderNo
374
+ },
375
+ key: this.accessToken
376
+ });
377
+ }
378
+ /**
379
+ *
380
+ * @param orderNo Noren order number, which needs to be modified
381
+ * @param product Allowed for only H and B products (Cover order and bracket order)
382
+ * @returns
383
+ */
384
+ async exitSNOOrder(orderNo, product) {
385
+ const data = {
386
+ norenordno: orderNo,
387
+ prd: product,
388
+ uid: this.userId
389
+ };
390
+ return this.request("exitSNOOrder", { data, key: this.accessToken });
391
+ }
392
+ async getHistoricData(option) {
393
+ const data = { uid: this.userId, ...option };
394
+ return this.request("historicData", { data, key: this.accessToken });
395
+ }
396
+ async getOptionChain(option) {
397
+ const data = {
398
+ uid: this.userId,
399
+ tsym: encodeURIComponent(option.tradingSymbol),
400
+ exch: option.exchange,
401
+ strprc: option.midPrice,
402
+ cnt: option.count
403
+ };
404
+ return this.request("optionChain", {
405
+ data,
406
+ key: this.accessToken
407
+ });
408
+ }
409
+ async orderBook(prd) {
410
+ const data = { uid: this.userId, prd };
411
+ return this.request("orderBook", {
412
+ data,
413
+ key: this.accessToken
414
+ });
415
+ }
416
+ /**
417
+ * Its a custom method which will return token number of option
418
+ * it can be used to start websocket connection to that tokenƒ
419
+ *
420
+ * @param params info about symbol
421
+ * @returns
422
+ */
423
+ async getOptionToken(params) {
424
+ const { strikePrice, exchange = "NFO", count = "5" } = params;
425
+ const optionType = params.optionType.at(0);
426
+ const symbol = params.symbol.toUpperCase();
427
+ for await (const expiry of this.getNext7FormattedDate()) {
428
+ const optionToPick = `${symbol}${expiry}${optionType}${strikePrice}`;
429
+ const optionChain = await this.getOptionChain({
430
+ count,
431
+ exchange,
432
+ midPrice: strikePrice.toString(),
433
+ tradingSymbol: optionToPick
434
+ });
435
+ if (optionChain.stat === "Not_Ok")
436
+ continue;
437
+ for (const { tsym, token } of optionChain.values) {
438
+ if (tsym === optionToPick) {
439
+ return { token, exchange, tsym };
440
+ }
441
+ }
442
+ }
443
+ return null;
444
+ }
445
+ getFormattedExpiry(date) {
446
+ const stringDate = date.toString();
447
+ const [_, month, day, year] = stringDate.split(" ");
448
+ const newMonth = month.toUpperCase();
449
+ const newYear = year.slice(2);
450
+ return `${day}${newMonth}${newYear}`;
451
+ }
452
+ getNext7FormattedDate() {
453
+ const dates = [];
454
+ let currentDate = /* @__PURE__ */ new Date();
455
+ for (let i = 0; i < 7; i++) {
456
+ const formattedDate = this.getFormattedExpiry(currentDate);
457
+ dates.push(formattedDate);
458
+ currentDate = new Date(currentDate.setDate(currentDate.getDate() + 1));
459
+ }
460
+ return dates;
461
+ }
462
+ // WebSocket API
463
+ connectWS() {
464
+ if (this.ws && (this.ws.readyState === 0 || this.ws.readyState === 1)) {
465
+ return;
466
+ }
467
+ this.ws = new import_ws.WebSocket(this.wsBaseUrl);
468
+ this.ws.addEventListener("open", () => {
469
+ let msg = {
470
+ t: "c",
471
+ uid: this.userId,
472
+ actid: this.accountId,
473
+ source: "API",
474
+ susertoken: this.accessToken
475
+ };
476
+ this.ws.send(JSON.stringify(msg));
477
+ if (this.cronJobRunning) {
478
+ this.subscribe(this.scripList);
479
+ return;
480
+ }
481
+ import_node_cron.default.schedule("43,00 3,10 * * 1-5", async () => {
482
+ this.cronJobRunning = true;
483
+ const hour = (/* @__PURE__ */ new Date()).getUTCHours();
484
+ const min = (/* @__PURE__ */ new Date()).getUTCMinutes();
485
+ if (hour === 3 && min === 43) {
486
+ try {
487
+ const cred = {
488
+ apiKey: this.apiKey,
489
+ password: this.password,
490
+ twoFa: this.twoFa,
491
+ userId: this.userId
492
+ };
493
+ await this.login(cred);
494
+ this.connectWS();
495
+ } catch (err) {
496
+ console.error(err);
497
+ const errMessage = `Token refreshing failed. ERR: ${err.message}`;
498
+ this.logging && this.logger.log(errMessage, "ERROR");
499
+ this.emit("stopped", errMessage);
500
+ }
501
+ }
502
+ if (hour === 10 && min === 0) {
503
+ this.disconnect();
504
+ }
505
+ });
506
+ });
507
+ this.ws.addEventListener("message", (e) => {
508
+ const result = JSON.parse(e.data.toString());
509
+ if (result.t === "dk") {
510
+ this.logging && this.logger.log(`Subscribed to ${result.ts}. Listening...`);
511
+ }
512
+ if (result.lp && result.t === "df") {
513
+ this.emit("priceUpdate", result);
514
+ }
515
+ if (result.t === "ok") {
516
+ this.emit("orderUpdate", result);
517
+ }
518
+ if (result.t === "ck") {
519
+ clearTimeout(this.connectTimer);
520
+ this.connectTimer = setTimeout(() => {
521
+ this.emit("connect", e);
522
+ this.logging && this.logger.log("Connected with Shoonya API! Streaming Data...");
523
+ }, 3e3);
524
+ return;
525
+ }
526
+ clearTimeout(this.disconnectTimer);
527
+ this.disconnectTimer = setTimeout(() => {
528
+ const day = (/* @__PURE__ */ new Date()).getDay();
529
+ if (day in [5, 6])
530
+ return;
531
+ const err = "API have stopped to respond! there could be a problem with API or market has closed";
532
+ this.logging && this.logger.log(err, "WARN");
533
+ this.emit("stopped", err);
534
+ }, 6e4 * 60 * 18 - 15e3);
535
+ });
536
+ }
537
+ /**
538
+ *
539
+ * subscribes for list of scrip by which WS returns them every time their price changes.
540
+ *
541
+ * `searchScrip` method can be used to get scripToken from scripName
542
+ * @param scripList scrip list requires list of scrip name in format of `exchangeName|scripToken`
543
+ */
544
+ subscribe(scripList) {
545
+ const msg = {
546
+ t: "d",
547
+ // depth subscription
548
+ k: scripList.join("#")
549
+ };
550
+ this.scripList = scripList;
551
+ if (this.ws) {
552
+ this.ws.send(JSON.stringify(msg));
553
+ }
554
+ }
555
+ unsubscribe(scripList) {
556
+ const msg = {
557
+ t: "ud",
558
+ // unsubcribe depth subscription
559
+ k: scripList.join("#")
560
+ };
561
+ if (this.ws) {
562
+ this.ws.send(JSON.stringify(msg));
563
+ }
564
+ }
565
+ subscribeOrderUpdate() {
566
+ const msg = {
567
+ t: "o",
568
+ // order update
569
+ actid: this.userId
570
+ };
571
+ if (this.ws) {
572
+ this.ws.send(JSON.stringify(msg));
573
+ }
574
+ }
575
+ unsubscribeOrderUpdate() {
576
+ const msg = {
577
+ t: "ud"
578
+ // unsubscribe order update
579
+ };
580
+ if (this.ws) {
581
+ this.ws.send(JSON.stringify(msg));
582
+ }
583
+ }
584
+ disconnect() {
585
+ if (this.ws && this.ws.readyState !== 2 && this.ws.readyState !== 3) {
586
+ this.ws.close();
587
+ this.logging && this.logger.log("Websocket Connection with Shoonya API is now closed!");
588
+ this.emit("close");
589
+ }
590
+ }
591
+ };
592
+ // Annotate the CommonJS export names for ESM import in node:
593
+ 0 && (module.exports = {
594
+ Shoonya
595
+ });