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