nsekit 0.3.2 → 0.3.3
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/README.md +3 -2
- package/dist/brokers/dhan/DhanBroker.d.ts +3 -0
- package/dist/brokers/dhan/DhanBroker.d.ts.map +1 -1
- package/dist/brokers/dhan/dhan-auth.d.ts +8 -3
- package/dist/brokers/dhan/dhan-auth.d.ts.map +1 -1
- package/dist/brokers/dhan/dhan-instruments.d.ts.map +1 -1
- package/dist/brokers/dhan/dhan-socket.d.ts.map +1 -1
- package/dist/brokers/finvasia/FinvasiaBroker.d.ts +36 -1
- package/dist/brokers/finvasia/FinvasiaBroker.d.ts.map +1 -1
- package/dist/brokers/finvasia/finvasia-auth.d.ts +40 -4
- package/dist/brokers/finvasia/finvasia-auth.d.ts.map +1 -1
- package/dist/brokers/finvasia/finvasia-constants.d.ts +3 -1
- package/dist/brokers/finvasia/finvasia-constants.d.ts.map +1 -1
- package/dist/brokers/finvasia/finvasia-instruments.d.ts.map +1 -1
- package/dist/brokers/finvasia/finvasia-socket.d.ts +2 -1
- package/dist/brokers/finvasia/finvasia-socket.d.ts.map +1 -1
- package/dist/brokers/fivepaisa/FivePaisaBroker.d.ts +70 -0
- package/dist/brokers/fivepaisa/FivePaisaBroker.d.ts.map +1 -0
- package/dist/brokers/fivepaisa/fivepaisa-auth.d.ts +53 -0
- package/dist/brokers/fivepaisa/fivepaisa-auth.d.ts.map +1 -0
- package/dist/brokers/fivepaisa/fivepaisa-constants.d.ts +57 -0
- package/dist/brokers/fivepaisa/fivepaisa-constants.d.ts.map +1 -0
- package/dist/brokers/fivepaisa/fivepaisa-instruments.d.ts +34 -0
- package/dist/brokers/fivepaisa/fivepaisa-instruments.d.ts.map +1 -0
- package/dist/brokers/fivepaisa/fivepaisa-mapper.d.ts +170 -0
- package/dist/brokers/fivepaisa/fivepaisa-mapper.d.ts.map +1 -0
- package/dist/brokers/fivepaisa/fivepaisa-socket.d.ts +67 -0
- package/dist/brokers/fivepaisa/fivepaisa-socket.d.ts.map +1 -0
- package/dist/brokers/paper/PaperBroker.d.ts +3 -0
- package/dist/brokers/paper/PaperBroker.d.ts.map +1 -1
- package/dist/brokers/zerodha/ZerodhaBroker.d.ts +3 -0
- package/dist/brokers/zerodha/ZerodhaBroker.d.ts.map +1 -1
- package/dist/brokers/zerodha/zerodha-instruments.d.ts.map +1 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1704 -78
- package/dist/instruments/instrument-master.d.ts +4 -0
- package/dist/instruments/instrument-master.d.ts.map +1 -1
- package/dist/interfaces/broker.interface.d.ts +7 -0
- package/dist/interfaces/broker.interface.d.ts.map +1 -1
- package/dist/types/broker.d.ts +7 -0
- package/dist/types/broker.d.ts.map +1 -1
- package/dist/types/common.d.ts +2 -2
- package/dist/types/common.d.ts.map +1 -1
- package/dist/types/instruments.d.ts +4 -0
- package/dist/types/instruments.d.ts.map +1 -1
- package/package.json +52 -61
package/dist/index.js
CHANGED
|
@@ -19407,6 +19407,14 @@ class InstrumentMaster {
|
|
|
19407
19407
|
const key = this.makeKey(exchange, tradingSymbol);
|
|
19408
19408
|
return this.instruments.get(key);
|
|
19409
19409
|
}
|
|
19410
|
+
getIndices() {
|
|
19411
|
+
const indices = [];
|
|
19412
|
+
for (const inst of this.instruments.values()) {
|
|
19413
|
+
if (inst.instrumentType === "IDX")
|
|
19414
|
+
indices.push(inst);
|
|
19415
|
+
}
|
|
19416
|
+
return indices;
|
|
19417
|
+
}
|
|
19410
19418
|
getByToken(brokerToken, brokerId) {
|
|
19411
19419
|
const tokenKey = `${brokerId}:${brokerToken}`;
|
|
19412
19420
|
const entry = this.tokenIndex.get(tokenKey);
|
|
@@ -19596,6 +19604,11 @@ class InstrumentMaster {
|
|
|
19596
19604
|
securityId: entry.brokerToken,
|
|
19597
19605
|
sid: entry.brokerSymbol
|
|
19598
19606
|
};
|
|
19607
|
+
} else if (brokerId === "fivepaisa") {
|
|
19608
|
+
instrument.brokerTokens.fivepaisa = {
|
|
19609
|
+
scripCode: entry.brokerToken,
|
|
19610
|
+
exchangeType: entry.raw?.["ExchType"] ?? "C"
|
|
19611
|
+
};
|
|
19599
19612
|
}
|
|
19600
19613
|
if (entry.expiry && !instrument.expiry)
|
|
19601
19614
|
instrument.expiry = entry.expiry;
|
|
@@ -19627,7 +19640,9 @@ class InstrumentMaster {
|
|
|
19627
19640
|
delete instrument.brokerTokens.finvasia;
|
|
19628
19641
|
else if (brokerId === "dhan")
|
|
19629
19642
|
delete instrument.brokerTokens.dhan;
|
|
19630
|
-
|
|
19643
|
+
else if (brokerId === "fivepaisa")
|
|
19644
|
+
delete instrument.brokerTokens.fivepaisa;
|
|
19645
|
+
const hasAnyBroker = instrument.brokerTokens.zerodha !== undefined || instrument.brokerTokens.finvasia !== undefined || instrument.brokerTokens.dhan !== undefined || instrument.brokerTokens.fivepaisa !== undefined;
|
|
19631
19646
|
if (!hasAnyBroker) {
|
|
19632
19647
|
this.instruments.delete(key);
|
|
19633
19648
|
}
|
|
@@ -20593,8 +20608,9 @@ class ZerodhaInstruments {
|
|
|
20593
20608
|
const lotSizeRaw = fields["lot_size"] ?? fields[COL.LOT_SIZE] ?? "1";
|
|
20594
20609
|
const tickSizeRaw = fields["tick_size"] ?? fields[COL.TICK_SIZE] ?? "0.05";
|
|
20595
20610
|
const instTypeRaw = fields["instrument_type"] ?? fields[COL.INSTRUMENT_TYPE] ?? "EQ";
|
|
20611
|
+
const segmentRaw = fields["segment"] ?? fields[COL.SEGMENT] ?? "";
|
|
20596
20612
|
const exchange = EXCHANGE_MAP[exchangeRaw] ?? "NSE";
|
|
20597
|
-
const instrumentType = mapInstrumentType(instTypeRaw);
|
|
20613
|
+
const instrumentType = segmentRaw.toUpperCase() === "INDICES" ? "IDX" : mapInstrumentType(instTypeRaw);
|
|
20598
20614
|
return {
|
|
20599
20615
|
tradingSymbol,
|
|
20600
20616
|
exchange,
|
|
@@ -20655,6 +20671,9 @@ class ZerodhaBroker {
|
|
|
20655
20671
|
return null;
|
|
20656
20672
|
return new Date(this.session.expiresAt);
|
|
20657
20673
|
}
|
|
20674
|
+
getSession() {
|
|
20675
|
+
return this.session;
|
|
20676
|
+
}
|
|
20658
20677
|
async sync(force) {
|
|
20659
20678
|
return this._iMaster.syncBroker(this.id, this.instruments, force);
|
|
20660
20679
|
}
|
|
@@ -20840,6 +20859,25 @@ class ZerodhaBroker {
|
|
|
20840
20859
|
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
20841
20860
|
}
|
|
20842
20861
|
}
|
|
20862
|
+
toBrokerToken(instrument, format = "ws") {
|
|
20863
|
+
if (format === "rest") {
|
|
20864
|
+
const kiteExch = EXCHANGE_TO_KITE[instrument.exchange] ?? instrument.exchange;
|
|
20865
|
+
return `${kiteExch}:${instrument.tradingSymbol}`;
|
|
20866
|
+
}
|
|
20867
|
+
const it = instrument.brokerTokens.zerodha?.instrument_token;
|
|
20868
|
+
if (it == null)
|
|
20869
|
+
return null;
|
|
20870
|
+
return String(it);
|
|
20871
|
+
}
|
|
20872
|
+
getIndexTokens(format = "ws") {
|
|
20873
|
+
const result = new Map;
|
|
20874
|
+
for (const inst of this._iMaster.getIndices()) {
|
|
20875
|
+
const token = this.toBrokerToken(inst, format);
|
|
20876
|
+
if (token)
|
|
20877
|
+
result.set(inst.underlying.toUpperCase(), token);
|
|
20878
|
+
}
|
|
20879
|
+
return result;
|
|
20880
|
+
}
|
|
20843
20881
|
subscribeTicks(tokens, cb) {
|
|
20844
20882
|
this.ensureSocket();
|
|
20845
20883
|
return this.socket.subscribe(tokens, cb);
|
|
@@ -21012,8 +21050,9 @@ var VALIDITY_FROM_SHOONYA = {
|
|
|
21012
21050
|
DAY: "DAY",
|
|
21013
21051
|
IOC: "IOC"
|
|
21014
21052
|
};
|
|
21015
|
-
var SHOONYA_API_BASE = "https://api.shoonya.com/
|
|
21053
|
+
var SHOONYA_API_BASE = "https://api.shoonya.com/NorenWClientAPI";
|
|
21016
21054
|
var SHOONYA_WS_URL = "wss://api.shoonya.com/NorenWSTP/";
|
|
21055
|
+
var SHOONYA_WS_OAUTH_URL = "wss://api.shoonya.com/NorenWSAPI/";
|
|
21017
21056
|
var SHOONYA_INSTRUMENTS_BASE = "https://api.shoonya.com/";
|
|
21018
21057
|
var SHOONYA_INSTRUMENT_FILES = {
|
|
21019
21058
|
NSE: "NSE_symbols.txt.zip",
|
|
@@ -21033,6 +21072,7 @@ var CANDLE_INTERVAL_TO_SHOONYA = {
|
|
|
21033
21072
|
|
|
21034
21073
|
// src/brokers/finvasia/finvasia-auth.ts
|
|
21035
21074
|
import crypto2 from "crypto";
|
|
21075
|
+
var SHOONYA_OAUTH_BASE = "https://trade.shoonya.com/OAuthlogin/authorize/oauth";
|
|
21036
21076
|
function generateTOTP(secret) {
|
|
21037
21077
|
const base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
21038
21078
|
const decoded = [];
|
|
@@ -21057,7 +21097,7 @@ function generateTOTP(secret) {
|
|
|
21057
21097
|
return String(code % 1e6).padStart(6, "0");
|
|
21058
21098
|
}
|
|
21059
21099
|
async function authenticate2(creds) {
|
|
21060
|
-
const { userId, password, totpSecret, apiKey } = creds;
|
|
21100
|
+
const { userId, password, totpSecret, apiKey, vendorCode } = creds;
|
|
21061
21101
|
if (!userId)
|
|
21062
21102
|
return Err(new Error("Missing userId in credentials"));
|
|
21063
21103
|
if (!password)
|
|
@@ -21069,28 +21109,42 @@ async function authenticate2(creds) {
|
|
|
21069
21109
|
try {
|
|
21070
21110
|
const totp = generateTOTP(totpSecret);
|
|
21071
21111
|
const passwordHash = crypto2.createHash("sha256").update(password).digest("hex");
|
|
21072
|
-
const
|
|
21112
|
+
const vc = vendorCode || "NOREN_API";
|
|
21113
|
+
const appKey = crypto2.createHash("sha256").update(`${userId}|S3cur3!d`).digest("hex");
|
|
21073
21114
|
const jData = JSON.stringify({
|
|
21074
21115
|
source: "API",
|
|
21075
|
-
apkversion: "1
|
|
21116
|
+
apkversion: "1",
|
|
21076
21117
|
uid: userId,
|
|
21077
21118
|
pwd: passwordHash,
|
|
21078
21119
|
factor2: totp,
|
|
21079
|
-
vc
|
|
21120
|
+
vc,
|
|
21080
21121
|
appkey: appKey,
|
|
21081
21122
|
imei: "nsekit"
|
|
21082
21123
|
});
|
|
21083
|
-
const
|
|
21084
|
-
|
|
21085
|
-
|
|
21086
|
-
|
|
21087
|
-
|
|
21088
|
-
|
|
21089
|
-
|
|
21090
|
-
|
|
21091
|
-
|
|
21092
|
-
|
|
21093
|
-
|
|
21124
|
+
const AUTH_HOSTS = [
|
|
21125
|
+
SHOONYA_API_BASE,
|
|
21126
|
+
"https://trade.shoonya.com/NorenWClientAPI"
|
|
21127
|
+
];
|
|
21128
|
+
let text = "";
|
|
21129
|
+
let json = null;
|
|
21130
|
+
for (const host of AUTH_HOSTS) {
|
|
21131
|
+
const response = await fetch(`${host}/QuickAuth`, {
|
|
21132
|
+
method: "POST",
|
|
21133
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
21134
|
+
body: `jData=${jData}`
|
|
21135
|
+
});
|
|
21136
|
+
text = await response.text();
|
|
21137
|
+
try {
|
|
21138
|
+
json = JSON.parse(text);
|
|
21139
|
+
break;
|
|
21140
|
+
} catch {
|
|
21141
|
+
if (response.status >= 500)
|
|
21142
|
+
continue;
|
|
21143
|
+
return Err(new Error(`Shoonya auth: invalid response (${response.status}): ${text.slice(0, 200)}`));
|
|
21144
|
+
}
|
|
21145
|
+
}
|
|
21146
|
+
if (!json) {
|
|
21147
|
+
return Err(new Error(`Shoonya auth: all hosts returned errors. Last: ${text.slice(0, 200)}`));
|
|
21094
21148
|
}
|
|
21095
21149
|
if (json.stat !== "Ok" || !json.susertoken) {
|
|
21096
21150
|
return Err(new Error(`Shoonya login failed: ${json.emsg ?? "Unknown error"}`));
|
|
@@ -21112,6 +21166,87 @@ async function authenticate2(creds) {
|
|
|
21112
21166
|
async function refreshSession2(_session) {
|
|
21113
21167
|
return Err(new Error("Finvasia/Shoonya does not support session refresh. Re-authenticate with TOTP."));
|
|
21114
21168
|
}
|
|
21169
|
+
function getOAuthURL(clientId) {
|
|
21170
|
+
return `${SHOONYA_OAUTH_BASE}?client_id=${encodeURIComponent(clientId)}`;
|
|
21171
|
+
}
|
|
21172
|
+
async function exchangeAuthCode(authCode, secretCode, clientId, uid) {
|
|
21173
|
+
if (!authCode)
|
|
21174
|
+
return Err(new Error("Missing authCode"));
|
|
21175
|
+
if (!secretCode)
|
|
21176
|
+
return Err(new Error("Missing secretCode"));
|
|
21177
|
+
if (!clientId)
|
|
21178
|
+
return Err(new Error("Missing clientId"));
|
|
21179
|
+
if (!uid)
|
|
21180
|
+
return Err(new Error("Missing uid"));
|
|
21181
|
+
try {
|
|
21182
|
+
const checksum = crypto2.createHash("sha256").update(clientId + secretCode + authCode).digest("hex");
|
|
21183
|
+
const jData = JSON.stringify({
|
|
21184
|
+
code: authCode,
|
|
21185
|
+
checksum,
|
|
21186
|
+
uid
|
|
21187
|
+
});
|
|
21188
|
+
const response = await fetch(`${SHOONYA_API_BASE}/GenAcsTok`, {
|
|
21189
|
+
method: "POST",
|
|
21190
|
+
headers: { "Content-Type": "application/x-www-form-urlencoded" },
|
|
21191
|
+
body: `jData=${jData}`
|
|
21192
|
+
});
|
|
21193
|
+
const text = await response.text();
|
|
21194
|
+
let json;
|
|
21195
|
+
try {
|
|
21196
|
+
json = JSON.parse(text);
|
|
21197
|
+
} catch {
|
|
21198
|
+
return Err(new Error(`Shoonya GenAcsTok: invalid response (${response.status}): ${text.slice(0, 200)}`));
|
|
21199
|
+
}
|
|
21200
|
+
if (json.stat !== "Ok" || !json.access_token) {
|
|
21201
|
+
const rawEmsg = String(json.emsg ?? "");
|
|
21202
|
+
const lower = rawEmsg.toLowerCase();
|
|
21203
|
+
let friendly;
|
|
21204
|
+
if (/(:\s*5\b|no data|invalid code|code expired|invalid authcode)/i.test(rawEmsg)) {
|
|
21205
|
+
friendly = "Invalid or expired auth code. Auth codes are single-use and expire within a few minutes \u2014 please log in to Shoonya again and copy a fresh code.";
|
|
21206
|
+
} else if (lower.includes("checksum") || lower.includes("invalid signature")) {
|
|
21207
|
+
friendly = "Invalid checksum. Verify that Client ID and Secret Code are correct in the broker settings.";
|
|
21208
|
+
} else if (lower.includes("invalid uid") || lower.includes("invalid user")) {
|
|
21209
|
+
friendly = "Invalid User ID. Check the User ID in the broker settings.";
|
|
21210
|
+
} else if (lower.includes("client_id") || lower.includes("clientid")) {
|
|
21211
|
+
friendly = "Invalid Client ID. It should typically be in the format <UID>_U (e.g. FN116677_U).";
|
|
21212
|
+
} else if (rawEmsg) {
|
|
21213
|
+
friendly = `Shoonya OAuth token exchange failed: ${rawEmsg}`;
|
|
21214
|
+
} else {
|
|
21215
|
+
friendly = "Shoonya OAuth token exchange failed with an unknown error.";
|
|
21216
|
+
}
|
|
21217
|
+
return Err(new Error(friendly));
|
|
21218
|
+
}
|
|
21219
|
+
const session = {
|
|
21220
|
+
accessToken: String(json.access_token),
|
|
21221
|
+
refreshToken: json.refresh_token ? String(json.refresh_token) : undefined,
|
|
21222
|
+
expiresAt: getEndOfDay2(),
|
|
21223
|
+
ttlSeconds: secondsUntilEndOfDay2(),
|
|
21224
|
+
brokerId: "finvasia",
|
|
21225
|
+
userId: String(json.USERID ?? json.uid ?? uid),
|
|
21226
|
+
metadata: {
|
|
21227
|
+
actid: json.actid,
|
|
21228
|
+
uname: json.uname,
|
|
21229
|
+
brkname: json.brkname,
|
|
21230
|
+
lastAccess: json.lastaccesstime,
|
|
21231
|
+
authMethod: "oauth"
|
|
21232
|
+
}
|
|
21233
|
+
};
|
|
21234
|
+
return Ok(session);
|
|
21235
|
+
} catch (err) {
|
|
21236
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
21237
|
+
}
|
|
21238
|
+
}
|
|
21239
|
+
function buildSessionFromToken(accessToken, uid, refreshToken) {
|
|
21240
|
+
return {
|
|
21241
|
+
accessToken,
|
|
21242
|
+
refreshToken,
|
|
21243
|
+
expiresAt: getEndOfDay2(),
|
|
21244
|
+
ttlSeconds: secondsUntilEndOfDay2(),
|
|
21245
|
+
brokerId: "finvasia",
|
|
21246
|
+
userId: uid,
|
|
21247
|
+
metadata: { authMethod: "oauth" }
|
|
21248
|
+
};
|
|
21249
|
+
}
|
|
21115
21250
|
function isSessionValid2(session) {
|
|
21116
21251
|
if (!session)
|
|
21117
21252
|
return false;
|
|
@@ -21339,28 +21474,37 @@ class FinvasiaSocket {
|
|
|
21339
21474
|
subscribedTokens = new Set;
|
|
21340
21475
|
userId;
|
|
21341
21476
|
accessToken;
|
|
21477
|
+
authMethod;
|
|
21342
21478
|
reconnectAttempts = 0;
|
|
21343
21479
|
maxReconnects = 5;
|
|
21344
21480
|
reconnectTimer = null;
|
|
21345
21481
|
onAuthFailed;
|
|
21346
|
-
constructor(userId, accessToken, onAuthFailed) {
|
|
21482
|
+
constructor(userId, accessToken, onAuthFailed, authMethod = "totp") {
|
|
21347
21483
|
this.userId = userId;
|
|
21348
21484
|
this.accessToken = accessToken;
|
|
21485
|
+
this.authMethod = authMethod;
|
|
21349
21486
|
this.onAuthFailed = onAuthFailed;
|
|
21350
21487
|
}
|
|
21351
21488
|
connect() {
|
|
21352
21489
|
if (this.state === "CONNECTED" || this.state === "CONNECTING")
|
|
21353
21490
|
return;
|
|
21354
21491
|
this.state = "CONNECTING";
|
|
21492
|
+
const wsUrl = this.authMethod === "oauth" ? SHOONYA_WS_OAUTH_URL : SHOONYA_WS_URL;
|
|
21355
21493
|
try {
|
|
21356
|
-
this.ws = new WS(
|
|
21494
|
+
this.ws = new WS(wsUrl);
|
|
21357
21495
|
} catch {
|
|
21358
21496
|
this.state = "DISCONNECTED";
|
|
21359
21497
|
return;
|
|
21360
21498
|
}
|
|
21361
21499
|
this.ws.on("open", () => {
|
|
21362
|
-
console.log(`[FinvasiaSocket] WS open, sending auth for uid=${this.userId}`);
|
|
21363
|
-
const authMsg = JSON.stringify({
|
|
21500
|
+
console.log(`[FinvasiaSocket] WS open, sending auth for uid=${this.userId} (method=${this.authMethod})`);
|
|
21501
|
+
const authMsg = this.authMethod === "oauth" ? JSON.stringify({
|
|
21502
|
+
t: "a",
|
|
21503
|
+
uid: this.userId,
|
|
21504
|
+
actid: this.userId,
|
|
21505
|
+
accesstoken: this.accessToken,
|
|
21506
|
+
source: "API"
|
|
21507
|
+
}) : JSON.stringify({
|
|
21364
21508
|
t: "c",
|
|
21365
21509
|
uid: this.userId,
|
|
21366
21510
|
actid: this.userId,
|
|
@@ -21463,7 +21607,13 @@ class FinvasiaSocket {
|
|
|
21463
21607
|
}
|
|
21464
21608
|
handleMessage(data) {
|
|
21465
21609
|
const type = data["t"];
|
|
21466
|
-
if (type === "
|
|
21610
|
+
if (type === "h") {
|
|
21611
|
+
if (this.ws && this.ws.readyState === WS.OPEN) {
|
|
21612
|
+
this.ws.send(JSON.stringify({ t: "h" }));
|
|
21613
|
+
}
|
|
21614
|
+
return;
|
|
21615
|
+
}
|
|
21616
|
+
if (type === "ck" || type === "ak") {
|
|
21467
21617
|
if (data["s"] === "OK") {
|
|
21468
21618
|
this.state = "CONNECTED";
|
|
21469
21619
|
this.reconnectAttempts = 0;
|
|
@@ -21471,7 +21621,7 @@ class FinvasiaSocket {
|
|
|
21471
21621
|
this.sendSubscribe(Array.from(this.subscribedTokens));
|
|
21472
21622
|
}
|
|
21473
21623
|
} else {
|
|
21474
|
-
console.error(`[FinvasiaSocket] Auth rejected (
|
|
21624
|
+
console.error(`[FinvasiaSocket] Auth rejected (${type} NOT_OK) \u2014 token invalid, stopping reconnect`);
|
|
21475
21625
|
this.reconnectAttempts = this.maxReconnects;
|
|
21476
21626
|
this.state = "DISCONNECTED";
|
|
21477
21627
|
this.onAuthFailed?.();
|
|
@@ -21600,7 +21750,9 @@ function mapInstrumentType2(instType, optionType) {
|
|
|
21600
21750
|
if (upper === "PE")
|
|
21601
21751
|
return "PE";
|
|
21602
21752
|
const it = instType?.toUpperCase();
|
|
21603
|
-
if (it === "
|
|
21753
|
+
if (it === "INDEX")
|
|
21754
|
+
return "IDX";
|
|
21755
|
+
if (it === "EQ" || it === "EQUITY")
|
|
21604
21756
|
return "EQ";
|
|
21605
21757
|
if (it?.startsWith("FUT") || it?.includes("FUT"))
|
|
21606
21758
|
return "FUT";
|
|
@@ -21618,10 +21770,23 @@ function parseStrike2(raw) {
|
|
|
21618
21770
|
const n = parseFloat(raw);
|
|
21619
21771
|
return isNaN(n) || n === 0 ? undefined : n;
|
|
21620
21772
|
}
|
|
21773
|
+
var UNDERLYING_ALIASES = {
|
|
21774
|
+
BSXOPT: "SENSEX",
|
|
21775
|
+
BSXFUT: "SENSEX",
|
|
21776
|
+
NFIDX: "NIFTY",
|
|
21777
|
+
BKIDX: "BANKNIFTY",
|
|
21778
|
+
"Nifty 50": "NIFTY50",
|
|
21779
|
+
"Nifty Bank": "BANKNIFTY",
|
|
21780
|
+
"Nifty Fin Services": "FINNIFTY",
|
|
21781
|
+
"NIFTY MID SELECT": "MIDCPNIFTY",
|
|
21782
|
+
"Nifty Next 50": "NIFTYNEXT50",
|
|
21783
|
+
INDIAVIX: "INDIAVIX"
|
|
21784
|
+
};
|
|
21621
21785
|
function deriveUnderlying2(symbol, tradingSymbol, instType) {
|
|
21622
21786
|
if (instType === "EQ")
|
|
21623
21787
|
return symbol || tradingSymbol;
|
|
21624
|
-
|
|
21788
|
+
const raw = symbol || tradingSymbol.replace(/\d.*/g, "");
|
|
21789
|
+
return UNDERLYING_ALIASES[raw] ?? raw;
|
|
21625
21790
|
}
|
|
21626
21791
|
|
|
21627
21792
|
class FinvasiaInstruments {
|
|
@@ -21780,6 +21945,25 @@ class FinvasiaBroker {
|
|
|
21780
21945
|
this._iMaster = new InstrumentMaster(options);
|
|
21781
21946
|
}
|
|
21782
21947
|
async authenticate(creds) {
|
|
21948
|
+
if (creds.accessToken && creds.userId) {
|
|
21949
|
+
const session = buildSessionFromToken(creds.accessToken, creds.userId, creds.refreshToken);
|
|
21950
|
+
this.credentials = creds;
|
|
21951
|
+
this.session = session;
|
|
21952
|
+
this.instrumentsImpl.setCredentials(session.userId, session.accessToken);
|
|
21953
|
+
return Ok(session);
|
|
21954
|
+
}
|
|
21955
|
+
if (creds.authCode) {
|
|
21956
|
+
if (!creds.clientId || !creds.secretCode || !creds.userId) {
|
|
21957
|
+
return Err(new Error("Finvasia OAuth: authCode requires clientId, secretCode, and userId"));
|
|
21958
|
+
}
|
|
21959
|
+
const result2 = await exchangeAuthCode(creds.authCode, creds.secretCode, creds.clientId, creds.userId);
|
|
21960
|
+
if (!result2.ok)
|
|
21961
|
+
return result2;
|
|
21962
|
+
this.credentials = creds;
|
|
21963
|
+
this.session = result2.value;
|
|
21964
|
+
this.instrumentsImpl.setCredentials(this.session.userId, this.session.accessToken);
|
|
21965
|
+
return result2;
|
|
21966
|
+
}
|
|
21783
21967
|
const result = await authenticate2(creds);
|
|
21784
21968
|
if (!result.ok)
|
|
21785
21969
|
return result;
|
|
@@ -21788,6 +21972,9 @@ class FinvasiaBroker {
|
|
|
21788
21972
|
this.instrumentsImpl.setCredentials(this.session.userId, this.session.accessToken);
|
|
21789
21973
|
return result;
|
|
21790
21974
|
}
|
|
21975
|
+
getOAuthURL(clientId) {
|
|
21976
|
+
return getOAuthURL(clientId);
|
|
21977
|
+
}
|
|
21791
21978
|
async refreshSession(session) {
|
|
21792
21979
|
return refreshSession2(session);
|
|
21793
21980
|
}
|
|
@@ -21799,6 +21986,19 @@ class FinvasiaBroker {
|
|
|
21799
21986
|
return null;
|
|
21800
21987
|
return new Date(this.session.expiresAt);
|
|
21801
21988
|
}
|
|
21989
|
+
getSession() {
|
|
21990
|
+
return this.session;
|
|
21991
|
+
}
|
|
21992
|
+
async disconnect() {
|
|
21993
|
+
if (this.socket) {
|
|
21994
|
+
try {
|
|
21995
|
+
this.socket.disconnect();
|
|
21996
|
+
} catch {}
|
|
21997
|
+
this.socket = null;
|
|
21998
|
+
}
|
|
21999
|
+
this.session = null;
|
|
22000
|
+
this.credentials = null;
|
|
22001
|
+
}
|
|
21802
22002
|
async sync(force) {
|
|
21803
22003
|
return this._iMaster.syncBroker(this.id, this.instruments, force);
|
|
21804
22004
|
}
|
|
@@ -21904,6 +22104,10 @@ class FinvasiaBroker {
|
|
|
21904
22104
|
const uid = this.session?.userId ?? "";
|
|
21905
22105
|
const raw = await this.post("OrderBook", { uid });
|
|
21906
22106
|
if (!Array.isArray(raw)) {
|
|
22107
|
+
const obj = raw;
|
|
22108
|
+
if (obj.stat === "Not_Ok" && obj.emsg) {
|
|
22109
|
+
return Err(new Error(`OrderBook failed: ${obj.emsg}`));
|
|
22110
|
+
}
|
|
21907
22111
|
return Ok([]);
|
|
21908
22112
|
}
|
|
21909
22113
|
return Ok(raw.map(fromOrder2));
|
|
@@ -21936,8 +22140,13 @@ class FinvasiaBroker {
|
|
|
21936
22140
|
try {
|
|
21937
22141
|
const uid = this.session?.userId ?? "";
|
|
21938
22142
|
const raw = await this.post("PositionBook", { uid, actid: uid });
|
|
21939
|
-
if (!Array.isArray(raw))
|
|
22143
|
+
if (!Array.isArray(raw)) {
|
|
22144
|
+
const obj = raw;
|
|
22145
|
+
if (obj.stat === "Not_Ok" && obj.emsg) {
|
|
22146
|
+
return Err(new Error(`PositionBook failed: ${obj.emsg}`));
|
|
22147
|
+
}
|
|
21940
22148
|
return Ok([]);
|
|
22149
|
+
}
|
|
21941
22150
|
return Ok(raw.map(fromPosition2));
|
|
21942
22151
|
} catch (err) {
|
|
21943
22152
|
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -21951,8 +22160,13 @@ class FinvasiaBroker {
|
|
|
21951
22160
|
actid: uid,
|
|
21952
22161
|
prd: "C"
|
|
21953
22162
|
});
|
|
21954
|
-
if (!Array.isArray(raw))
|
|
22163
|
+
if (!Array.isArray(raw)) {
|
|
22164
|
+
const obj = raw;
|
|
22165
|
+
if (obj.stat === "Not_Ok" && obj.emsg) {
|
|
22166
|
+
return Err(new Error(`Holdings failed: ${obj.emsg}`));
|
|
22167
|
+
}
|
|
21955
22168
|
return Ok([]);
|
|
22169
|
+
}
|
|
21956
22170
|
return Ok(raw.map(fromHolding2));
|
|
21957
22171
|
} catch (err) {
|
|
21958
22172
|
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -22076,6 +22290,22 @@ class FinvasiaBroker {
|
|
|
22076
22290
|
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
22077
22291
|
}
|
|
22078
22292
|
}
|
|
22293
|
+
toBrokerToken(instrument, format = "ws") {
|
|
22294
|
+
const token = instrument.brokerTokens.finvasia?.token;
|
|
22295
|
+
if (!token)
|
|
22296
|
+
return null;
|
|
22297
|
+
const sep = format === "rest" ? ":" : "|";
|
|
22298
|
+
return `${instrument.exchange}${sep}${token}`;
|
|
22299
|
+
}
|
|
22300
|
+
getIndexTokens(format = "ws") {
|
|
22301
|
+
const result = new Map;
|
|
22302
|
+
for (const inst of this._iMaster.getIndices()) {
|
|
22303
|
+
const token = this.toBrokerToken(inst, format);
|
|
22304
|
+
if (token)
|
|
22305
|
+
result.set(inst.underlying.toUpperCase(), token);
|
|
22306
|
+
}
|
|
22307
|
+
return result;
|
|
22308
|
+
}
|
|
22079
22309
|
subscribeTicks(tokens, cb) {
|
|
22080
22310
|
this.ensureSocket();
|
|
22081
22311
|
return this.socket.subscribe(tokens, cb);
|
|
@@ -22100,18 +22330,25 @@ class FinvasiaBroker {
|
|
|
22100
22330
|
actid: uid
|
|
22101
22331
|
});
|
|
22102
22332
|
if (raw.stat !== "Ok") {
|
|
22103
|
-
return Err(new Error(
|
|
22333
|
+
return Err(new Error(`Margins failed: ${raw.emsg ?? "Unknown error"}`));
|
|
22104
22334
|
}
|
|
22105
22335
|
const cash = parseFloat(raw["cash"] ?? "0");
|
|
22106
|
-
const marginUsed = parseFloat(raw["marginused"] ?? "0");
|
|
22336
|
+
const marginUsed = parseFloat(raw["marginused"] ?? raw["peak_mar"] ?? "0");
|
|
22337
|
+
const blockedAmt = parseFloat(raw["blk_amt"] ?? "0");
|
|
22107
22338
|
const collateral = parseFloat(raw["collateral"] ?? "0");
|
|
22339
|
+
const eqAvailable = parseFloat(raw["mr_der_a"] ?? "0");
|
|
22340
|
+
const comAvailable = parseFloat(raw["mr_com_a"] ?? "0");
|
|
22341
|
+
const available = cash;
|
|
22342
|
+
const used = marginUsed + blockedAmt;
|
|
22343
|
+
const total = available + used;
|
|
22108
22344
|
return Ok({
|
|
22109
|
-
available
|
|
22110
|
-
used
|
|
22111
|
-
total
|
|
22345
|
+
available,
|
|
22346
|
+
used,
|
|
22347
|
+
total,
|
|
22112
22348
|
collateral,
|
|
22113
22349
|
segments: {
|
|
22114
|
-
equity: { available:
|
|
22350
|
+
equity: { available: eqAvailable, used: 0 },
|
|
22351
|
+
commodity: { available: comAvailable, used: 0 }
|
|
22115
22352
|
}
|
|
22116
22353
|
});
|
|
22117
22354
|
} catch (err) {
|
|
@@ -22123,9 +22360,11 @@ class FinvasiaBroker {
|
|
|
22123
22360
|
const margins = await this.getMargins();
|
|
22124
22361
|
if (!margins.ok)
|
|
22125
22362
|
return margins;
|
|
22363
|
+
const eq = margins.value.segments["equity"]?.available ?? 0;
|
|
22364
|
+
const com = margins.value.segments["commodity"]?.available ?? 0;
|
|
22126
22365
|
return Ok({
|
|
22127
|
-
equity:
|
|
22128
|
-
commodity:
|
|
22366
|
+
equity: eq,
|
|
22367
|
+
commodity: com,
|
|
22129
22368
|
total: margins.value.available
|
|
22130
22369
|
});
|
|
22131
22370
|
} catch (err) {
|
|
@@ -22172,10 +22411,18 @@ class FinvasiaBroker {
|
|
|
22172
22411
|
if (!this.session)
|
|
22173
22412
|
throw new Error("Not authenticated");
|
|
22174
22413
|
const jData = JSON.stringify(payload);
|
|
22414
|
+
const isOAuth = this.session.metadata?.authMethod === "oauth";
|
|
22415
|
+
const headers = isOAuth ? {
|
|
22416
|
+
"Content-Type": "application/x-www-form-urlencoded",
|
|
22417
|
+
Authorization: `Bearer ${this.session.accessToken}`
|
|
22418
|
+
} : {
|
|
22419
|
+
"Content-Type": "application/x-www-form-urlencoded"
|
|
22420
|
+
};
|
|
22421
|
+
const body = isOAuth ? `jData=${jData}` : `jData=${jData}&jKey=${this.session.accessToken}`;
|
|
22175
22422
|
const response = await fetch(`${SHOONYA_API_BASE}/${endpoint}`, {
|
|
22176
22423
|
method: "POST",
|
|
22177
|
-
headers
|
|
22178
|
-
body
|
|
22424
|
+
headers,
|
|
22425
|
+
body
|
|
22179
22426
|
});
|
|
22180
22427
|
const text = await response.text();
|
|
22181
22428
|
let parsed;
|
|
@@ -22184,36 +22431,32 @@ class FinvasiaBroker {
|
|
|
22184
22431
|
} catch {
|
|
22185
22432
|
throw new Error(`Shoonya ${endpoint} failed (${response.status}): ${text.slice(0, 200)}`);
|
|
22186
22433
|
}
|
|
22434
|
+
const obj = parsed;
|
|
22435
|
+
if (obj && obj["stat"] === "Not_Ok" && obj["emsg"]) {
|
|
22436
|
+
const emsg = String(obj["emsg"]).toLowerCase();
|
|
22437
|
+
if (emsg.includes("session") || emsg.includes("invalid") || emsg.includes("not logged")) {
|
|
22438
|
+
console.error(`[FinvasiaBroker] SESSION ERROR on ${endpoint}: ${obj["emsg"]} (token: ...${this.session.accessToken.slice(-8)})`);
|
|
22439
|
+
}
|
|
22440
|
+
}
|
|
22187
22441
|
return parsed;
|
|
22188
22442
|
}
|
|
22189
22443
|
ensureSocket() {
|
|
22190
|
-
if (this.socket)
|
|
22191
|
-
return;
|
|
22192
22444
|
if (!this.session)
|
|
22193
22445
|
throw new Error("Cannot create socket without an active session");
|
|
22446
|
+
if (this.socket) {
|
|
22447
|
+
if (this.socket.getConnectionState() === "DISCONNECTED") {
|
|
22448
|
+
this.socket.reconnect(this.session.accessToken);
|
|
22449
|
+
}
|
|
22450
|
+
return;
|
|
22451
|
+
}
|
|
22452
|
+
const authMethod = this.session.metadata?.authMethod === "oauth" ? "oauth" : "totp";
|
|
22194
22453
|
this.socket = new FinvasiaSocket(this.session.userId, this.session.accessToken, () => {
|
|
22195
22454
|
this.handleSocketAuthFailed();
|
|
22196
|
-
});
|
|
22455
|
+
}, authMethod);
|
|
22197
22456
|
this.socket.connect();
|
|
22198
22457
|
}
|
|
22199
22458
|
handleSocketAuthFailed() {
|
|
22200
|
-
|
|
22201
|
-
console.error("[FinvasiaBroker] Socket auth failed but no credentials stored \u2014 cannot re-authenticate");
|
|
22202
|
-
return;
|
|
22203
|
-
}
|
|
22204
|
-
console.log("[FinvasiaBroker] Socket auth failed, re-authenticating...");
|
|
22205
|
-
authenticate2(this.credentials).then((result) => {
|
|
22206
|
-
if (!result.ok) {
|
|
22207
|
-
console.error("[FinvasiaBroker] Re-authentication failed:", result.error instanceof Error ? result.error.message : result.error);
|
|
22208
|
-
return;
|
|
22209
|
-
}
|
|
22210
|
-
this.session = result.value;
|
|
22211
|
-
this.instrumentsImpl.setCredentials(this.session.userId, this.session.accessToken);
|
|
22212
|
-
console.log("[FinvasiaBroker] Re-authenticated successfully, reconnecting socket");
|
|
22213
|
-
this.socket?.reconnect(this.session.accessToken);
|
|
22214
|
-
}).catch((err) => {
|
|
22215
|
-
console.error("[FinvasiaBroker] Re-authentication exception:", err instanceof Error ? err.message : err);
|
|
22216
|
-
});
|
|
22459
|
+
console.warn("[FinvasiaBroker] Socket auth rejected (ck NOT_OK) \u2014 waiting for user-bot to re-authenticate");
|
|
22217
22460
|
}
|
|
22218
22461
|
}
|
|
22219
22462
|
// src/brokers/dhan/dhan-constants.ts
|
|
@@ -22315,12 +22558,12 @@ async function authenticate3(creds) {
|
|
|
22315
22558
|
return Err(new Error(`Dhan auth validation failed (${response.status}): ${body}`));
|
|
22316
22559
|
}
|
|
22317
22560
|
const profile = await response.json();
|
|
22318
|
-
const
|
|
22561
|
+
const twentyFourHoursMs = 24 * 60 * 60 * 1000;
|
|
22319
22562
|
const session = {
|
|
22320
22563
|
accessToken,
|
|
22321
22564
|
refreshToken: undefined,
|
|
22322
|
-
expiresAt: Date.now() +
|
|
22323
|
-
ttlSeconds:
|
|
22565
|
+
expiresAt: Date.now() + twentyFourHoursMs,
|
|
22566
|
+
ttlSeconds: 24 * 60 * 60,
|
|
22324
22567
|
brokerId: "dhan",
|
|
22325
22568
|
userId: profile.clientId ?? clientId,
|
|
22326
22569
|
metadata: { name: profile.name, email: profile.email }
|
|
@@ -22385,15 +22628,16 @@ async function consumeConsent(creds, tokenId) {
|
|
|
22385
22628
|
return Err(new Error(`Dhan consumeApp-consent failed (${response.status}): ${body}`));
|
|
22386
22629
|
}
|
|
22387
22630
|
const data = await response.json();
|
|
22388
|
-
|
|
22389
|
-
|
|
22631
|
+
const newToken = data.accessToken ?? data.token;
|
|
22632
|
+
if (!newToken) {
|
|
22633
|
+
return Err(new Error(data.message ?? "Failed to get access token \u2014 no token in response"));
|
|
22390
22634
|
}
|
|
22391
|
-
const
|
|
22635
|
+
const twentyFourHoursMs = 24 * 60 * 60 * 1000;
|
|
22392
22636
|
const session = {
|
|
22393
|
-
accessToken:
|
|
22637
|
+
accessToken: newToken,
|
|
22394
22638
|
refreshToken: undefined,
|
|
22395
|
-
expiresAt: Date.now() +
|
|
22396
|
-
ttlSeconds:
|
|
22639
|
+
expiresAt: Date.now() + twentyFourHoursMs,
|
|
22640
|
+
ttlSeconds: 24 * 60 * 60,
|
|
22397
22641
|
brokerId: "dhan",
|
|
22398
22642
|
userId: clientId ?? "",
|
|
22399
22643
|
metadata: { authMode: "oauth" }
|
|
@@ -22403,8 +22647,42 @@ async function consumeConsent(creds, tokenId) {
|
|
|
22403
22647
|
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
22404
22648
|
}
|
|
22405
22649
|
}
|
|
22406
|
-
async function refreshSession3(
|
|
22407
|
-
|
|
22650
|
+
async function refreshSession3(session) {
|
|
22651
|
+
const { accessToken, userId: clientId } = session;
|
|
22652
|
+
if (!accessToken)
|
|
22653
|
+
return Err(new Error("Missing accessToken in session"));
|
|
22654
|
+
if (!clientId)
|
|
22655
|
+
return Err(new Error("Missing userId (client_id) in session"));
|
|
22656
|
+
try {
|
|
22657
|
+
const response = await fetch(`${DHAN_API_BASE}/RenewToken`, {
|
|
22658
|
+
method: "GET",
|
|
22659
|
+
headers: {
|
|
22660
|
+
"Content-Type": "application/json",
|
|
22661
|
+
"access-token": accessToken,
|
|
22662
|
+
dhanClientId: clientId
|
|
22663
|
+
}
|
|
22664
|
+
});
|
|
22665
|
+
if (!response.ok) {
|
|
22666
|
+
const body = await response.text();
|
|
22667
|
+
return Err(new Error(`Dhan token renewal failed (${response.status}): ${body}`));
|
|
22668
|
+
}
|
|
22669
|
+
const data = await response.json();
|
|
22670
|
+
const newToken = data.accessToken ?? data.token;
|
|
22671
|
+
if (!newToken) {
|
|
22672
|
+
return Err(new Error(data.message ?? `Token renewal failed \u2014 no token in response: ${JSON.stringify(data)}`));
|
|
22673
|
+
}
|
|
22674
|
+
const twentyFourHoursMs = 24 * 60 * 60 * 1000;
|
|
22675
|
+
const expiresAt = data.expiryTime ? new Date(data.expiryTime).getTime() : Date.now() + twentyFourHoursMs;
|
|
22676
|
+
const renewed = {
|
|
22677
|
+
...session,
|
|
22678
|
+
accessToken: newToken,
|
|
22679
|
+
expiresAt,
|
|
22680
|
+
ttlSeconds: Math.floor((expiresAt - Date.now()) / 1000)
|
|
22681
|
+
};
|
|
22682
|
+
return Ok(renewed);
|
|
22683
|
+
} catch (err) {
|
|
22684
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
22685
|
+
}
|
|
22408
22686
|
}
|
|
22409
22687
|
function isSessionValid3(session) {
|
|
22410
22688
|
if (!session)
|
|
@@ -22639,6 +22917,7 @@ class DhanSocket {
|
|
|
22639
22917
|
return;
|
|
22640
22918
|
}
|
|
22641
22919
|
this.ws.on("open", () => {
|
|
22920
|
+
console.log(`[DhanSocket] WS connected for clientId=${this.clientId}`);
|
|
22642
22921
|
this.state = "CONNECTED";
|
|
22643
22922
|
this.reconnectAttempts = 0;
|
|
22644
22923
|
this.startHeartbeat();
|
|
@@ -22648,6 +22927,11 @@ class DhanSocket {
|
|
|
22648
22927
|
});
|
|
22649
22928
|
this.ws.on("message", (raw, isBinary) => {
|
|
22650
22929
|
try {
|
|
22930
|
+
if (!isBinary) {
|
|
22931
|
+
console.log(`[DhanSocket] WS JSON msg: ${raw.toString().slice(0, 200)}`);
|
|
22932
|
+
} else {
|
|
22933
|
+
console.log(`[DhanSocket] WS binary msg: ${Buffer.isBuffer(raw) ? raw.length : "?"} bytes`);
|
|
22934
|
+
}
|
|
22651
22935
|
if (isBinary) {
|
|
22652
22936
|
const buf = raw instanceof ArrayBuffer ? Buffer.from(raw) : Buffer.isBuffer(raw) ? raw : Array.isArray(raw) ? Buffer.concat(raw) : Buffer.from(raw);
|
|
22653
22937
|
this.handleBinaryMessage(buf);
|
|
@@ -22657,12 +22941,20 @@ class DhanSocket {
|
|
|
22657
22941
|
}
|
|
22658
22942
|
} catch {}
|
|
22659
22943
|
});
|
|
22660
|
-
this.ws.on("close", () => {
|
|
22944
|
+
this.ws.on("close", (code, reason) => {
|
|
22945
|
+
console.warn(`[DhanSocket] WS closed: code=${code}, reason=${reason.toString()}`);
|
|
22661
22946
|
this.state = "DISCONNECTED";
|
|
22662
22947
|
this.stopHeartbeat();
|
|
22663
22948
|
this.attemptReconnect();
|
|
22664
22949
|
});
|
|
22665
|
-
this.ws.on("error", () => {
|
|
22950
|
+
this.ws.on("error", (err) => {
|
|
22951
|
+
console.error(`[DhanSocket] WS error: ${err.message}`, {
|
|
22952
|
+
code: err.code,
|
|
22953
|
+
type: err.type,
|
|
22954
|
+
stack: err.stack?.split(`
|
|
22955
|
+
`).slice(0, 3).join(" | ")
|
|
22956
|
+
});
|
|
22957
|
+
});
|
|
22666
22958
|
}
|
|
22667
22959
|
disconnect() {
|
|
22668
22960
|
this.clearReconnectTimer();
|
|
@@ -22727,7 +23019,7 @@ class DhanSocket {
|
|
|
22727
23019
|
const exchSegByte = data.readUInt8(3);
|
|
22728
23020
|
const securityId = data.readInt32LE(4);
|
|
22729
23021
|
const exchangeSegment = EXCH_SEG_FROM_BYTE[exchSegByte] ?? "NSE_EQ";
|
|
22730
|
-
const fullKey = `${exchangeSegment}
|
|
23022
|
+
const fullKey = `${exchangeSegment}|${securityId}`;
|
|
22731
23023
|
if (responseCode === 1 && data.length >= 32) {
|
|
22732
23024
|
const ltp = data.readFloatLE(8);
|
|
22733
23025
|
const ltt = data.readInt32LE(12);
|
|
@@ -22832,7 +23124,9 @@ class DhanSocket {
|
|
|
22832
23124
|
InstrumentCount: instruments.length,
|
|
22833
23125
|
InstrumentList: instruments
|
|
22834
23126
|
};
|
|
22835
|
-
|
|
23127
|
+
const msg = JSON.stringify(request);
|
|
23128
|
+
console.log(`[DhanSocket] Sending subscribe: ${msg}`);
|
|
23129
|
+
this.ws.send(msg);
|
|
22836
23130
|
}
|
|
22837
23131
|
sendUnsubscribe(tokens) {
|
|
22838
23132
|
if (!this.ws || this.state !== "CONNECTED")
|
|
@@ -22923,7 +23217,9 @@ function mapInstrumentType3(instName, optionType) {
|
|
|
22923
23217
|
if (opt === "PE")
|
|
22924
23218
|
return "PE";
|
|
22925
23219
|
const inst = instName?.toUpperCase();
|
|
22926
|
-
if (inst
|
|
23220
|
+
if (inst === "INDEX")
|
|
23221
|
+
return "IDX";
|
|
23222
|
+
if (inst?.includes("EQUITY") || inst === "EQ")
|
|
22927
23223
|
return "EQ";
|
|
22928
23224
|
if (inst?.includes("FUT"))
|
|
22929
23225
|
return "FUT";
|
|
@@ -23141,7 +23437,14 @@ class DhanBroker {
|
|
|
23141
23437
|
return result;
|
|
23142
23438
|
}
|
|
23143
23439
|
async refreshSession(session) {
|
|
23144
|
-
|
|
23440
|
+
const result = await refreshSession3(session);
|
|
23441
|
+
if (result.ok) {
|
|
23442
|
+
this.session = result.value;
|
|
23443
|
+
if (!this.clientId && result.value.userId)
|
|
23444
|
+
this.clientId = result.value.userId;
|
|
23445
|
+
this.instrumentsImpl.setCredentials(this.clientId, result.value.accessToken);
|
|
23446
|
+
}
|
|
23447
|
+
return result;
|
|
23145
23448
|
}
|
|
23146
23449
|
isSessionValid() {
|
|
23147
23450
|
return isSessionValid3(this.session);
|
|
@@ -23151,6 +23454,9 @@ class DhanBroker {
|
|
|
23151
23454
|
return null;
|
|
23152
23455
|
return new Date(this.session.expiresAt);
|
|
23153
23456
|
}
|
|
23457
|
+
getSession() {
|
|
23458
|
+
return this.session;
|
|
23459
|
+
}
|
|
23154
23460
|
async generateConsent(creds) {
|
|
23155
23461
|
this.oauthCreds = creds;
|
|
23156
23462
|
this.clientId = creds.userId ?? "";
|
|
@@ -23429,6 +23735,23 @@ class DhanBroker {
|
|
|
23429
23735
|
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
23430
23736
|
}
|
|
23431
23737
|
}
|
|
23738
|
+
toBrokerToken(instrument, format = "ws") {
|
|
23739
|
+
const sid = instrument.brokerTokens.dhan?.securityId;
|
|
23740
|
+
if (!sid)
|
|
23741
|
+
return null;
|
|
23742
|
+
const seg = instrument.instrumentType === "IDX" ? "IDX_I" : EXCHANGE_TO_DHAN[instrument.exchange] ?? "NSE_EQ";
|
|
23743
|
+
const sep = format === "rest" ? ":" : "|";
|
|
23744
|
+
return `${seg}${sep}${sid}`;
|
|
23745
|
+
}
|
|
23746
|
+
getIndexTokens(format = "ws") {
|
|
23747
|
+
const result = new Map;
|
|
23748
|
+
for (const inst of this._iMaster.getIndices()) {
|
|
23749
|
+
const token = this.toBrokerToken(inst, format);
|
|
23750
|
+
if (token)
|
|
23751
|
+
result.set(inst.underlying.toUpperCase(), token);
|
|
23752
|
+
}
|
|
23753
|
+
return result;
|
|
23754
|
+
}
|
|
23432
23755
|
subscribeTicks(tokens, cb) {
|
|
23433
23756
|
this.ensureSocket();
|
|
23434
23757
|
return this.socket.subscribe(tokens, cb);
|
|
@@ -23448,7 +23771,8 @@ class DhanBroker {
|
|
|
23448
23771
|
async getMargins() {
|
|
23449
23772
|
try {
|
|
23450
23773
|
const raw = await this.request("GET", "/fundlimit");
|
|
23451
|
-
|
|
23774
|
+
console.log("Dhan getMargins - raw response:", raw);
|
|
23775
|
+
const available = Number(raw["availabelBalance"] ?? raw["availabelBalance"] ?? 0);
|
|
23452
23776
|
const used = Number(raw["utilizedAmount"] ?? raw["blockedMargin"] ?? 0);
|
|
23453
23777
|
const collateral = Number(raw["collateralAmount"] ?? 0);
|
|
23454
23778
|
return Ok({
|
|
@@ -23467,6 +23791,7 @@ class DhanBroker {
|
|
|
23467
23791
|
async getFunds() {
|
|
23468
23792
|
try {
|
|
23469
23793
|
const margins = await this.getMargins();
|
|
23794
|
+
console.log("Dhan getFunds - margins:", margins);
|
|
23470
23795
|
if (!margins.ok)
|
|
23471
23796
|
return margins;
|
|
23472
23797
|
return Ok({
|
|
@@ -23928,6 +24253,9 @@ class PaperBroker {
|
|
|
23928
24253
|
return null;
|
|
23929
24254
|
return new Date(this.session.expiresAt);
|
|
23930
24255
|
}
|
|
24256
|
+
getSession() {
|
|
24257
|
+
return this.session;
|
|
24258
|
+
}
|
|
23931
24259
|
async placeOrder(params) {
|
|
23932
24260
|
try {
|
|
23933
24261
|
if (params.side === "BUY") {
|
|
@@ -24059,6 +24387,12 @@ class PaperBroker {
|
|
|
24059
24387
|
}
|
|
24060
24388
|
return Err(new Error("Paper broker requires a data source for candles. Call setDataSource()."));
|
|
24061
24389
|
}
|
|
24390
|
+
toBrokerToken(_instrument, _format = "ws") {
|
|
24391
|
+
return null;
|
|
24392
|
+
}
|
|
24393
|
+
getIndexTokens(_format = "ws") {
|
|
24394
|
+
return new Map;
|
|
24395
|
+
}
|
|
24062
24396
|
subscribeTicks(tokens, cb) {
|
|
24063
24397
|
this.subIdCounter += 1;
|
|
24064
24398
|
const subId = `paper_sub_${this.subIdCounter}`;
|
|
@@ -24250,8 +24584,1298 @@ class PaperBroker {
|
|
|
24250
24584
|
};
|
|
24251
24585
|
}
|
|
24252
24586
|
}
|
|
24587
|
+
// src/brokers/fivepaisa/fivepaisa-auth.ts
|
|
24588
|
+
import crypto3 from "crypto";
|
|
24589
|
+
|
|
24590
|
+
// src/brokers/fivepaisa/fivepaisa-constants.ts
|
|
24591
|
+
var FIVEPAISA_API_BASE = "https://Openapi.5paisa.com/VendorsAPI/Service1.svc";
|
|
24592
|
+
var FIVEPAISA_HISTORICAL_BASE = "https://openapi.5paisa.com/V2/historical";
|
|
24593
|
+
var FIVEPAISA_SCRIP_MASTER_BASE = "https://openapi.5paisa.com/VendorsAPI/Service1.svc/ScripMaster/segment";
|
|
24594
|
+
var ROUTES = {
|
|
24595
|
+
TOTP_LOGIN: `${FIVEPAISA_API_BASE}/TOTPLogin`,
|
|
24596
|
+
ACCESS_TOKEN: `${FIVEPAISA_API_BASE}/GetAccessToken`,
|
|
24597
|
+
MARGIN: `${FIVEPAISA_API_BASE}/V4/Margin`,
|
|
24598
|
+
ORDER_BOOK: `${FIVEPAISA_API_BASE}/V3/OrderBook`,
|
|
24599
|
+
HOLDINGS: `${FIVEPAISA_API_BASE}/V3/Holding`,
|
|
24600
|
+
POSITIONS: `${FIVEPAISA_API_BASE}/V1/NetPositionNetWise`,
|
|
24601
|
+
PLACE_ORDER: `${FIVEPAISA_API_BASE}/V1/PlaceOrderRequest`,
|
|
24602
|
+
MODIFY_ORDER: `${FIVEPAISA_API_BASE}/V1/ModifyOrderRequest`,
|
|
24603
|
+
CANCEL_ORDER: `${FIVEPAISA_API_BASE}/V1/CancelOrderRequest`,
|
|
24604
|
+
ORDER_STATUS: `${FIVEPAISA_API_BASE}/V2/OrderStatus`,
|
|
24605
|
+
TRADE_BOOK: `${FIVEPAISA_API_BASE}/V1/TradeBook`,
|
|
24606
|
+
TRADE_INFO: `${FIVEPAISA_API_BASE}/V1/TradeInformation`,
|
|
24607
|
+
MARKET_FEED: `${FIVEPAISA_API_BASE}/MarketFeed`,
|
|
24608
|
+
MARKET_FEED_V1: `${FIVEPAISA_API_BASE}/V1/MarketFeed`,
|
|
24609
|
+
MARKET_DEPTH: `${FIVEPAISA_API_BASE}/MarketDepth`
|
|
24610
|
+
};
|
|
24611
|
+
var REQUEST_CODES = {
|
|
24612
|
+
MARGIN: "5PMarginV3",
|
|
24613
|
+
ORDER_BOOK: "5POrdBkV2",
|
|
24614
|
+
HOLDINGS: "5PHoldingV2",
|
|
24615
|
+
POSITIONS: "5PNPNWV1",
|
|
24616
|
+
PLACE_ORDER: "5PPlaceOrdReq",
|
|
24617
|
+
MODIFY_ORDER: "5PModifyOrdReq",
|
|
24618
|
+
CANCEL_ORDER: "5PCancelOrdReq",
|
|
24619
|
+
ORDER_STATUS: "5POrdStatus",
|
|
24620
|
+
TRADE_BOOK: "5PTrdBkV1",
|
|
24621
|
+
TRADE_INFO: "5PTrdInfo",
|
|
24622
|
+
MARKET_FEED: "5PMF",
|
|
24623
|
+
MARKET_DEPTH: "5PMD"
|
|
24624
|
+
};
|
|
24625
|
+
var EXCHANGE_TO_5P = {
|
|
24626
|
+
NSE: "N",
|
|
24627
|
+
BSE: "B",
|
|
24628
|
+
NFO: "N",
|
|
24629
|
+
BFO: "B",
|
|
24630
|
+
MCX: "M",
|
|
24631
|
+
CDS: "N"
|
|
24632
|
+
};
|
|
24633
|
+
var EXCHANGE_FROM_5P = {
|
|
24634
|
+
N: "NSE",
|
|
24635
|
+
B: "BSE",
|
|
24636
|
+
M: "MCX"
|
|
24637
|
+
};
|
|
24638
|
+
var EXCHANGE_TYPE_MAP = {
|
|
24639
|
+
NSE: "C",
|
|
24640
|
+
BSE: "C",
|
|
24641
|
+
NFO: "D",
|
|
24642
|
+
BFO: "D",
|
|
24643
|
+
MCX: "D",
|
|
24644
|
+
CDS: "U"
|
|
24645
|
+
};
|
|
24646
|
+
var PRODUCT_TO_5P = {
|
|
24647
|
+
INTRADAY: true,
|
|
24648
|
+
DELIVERY: false,
|
|
24649
|
+
NORMAL: false
|
|
24650
|
+
};
|
|
24651
|
+
var PRODUCT_FROM_5P = (isIntraday) => isIntraday ? "INTRADAY" : "DELIVERY";
|
|
24652
|
+
var ORDER_TYPE_FROM_5P = (isStopLoss, price) => {
|
|
24653
|
+
if (isStopLoss)
|
|
24654
|
+
return "SL";
|
|
24655
|
+
if (price === 0)
|
|
24656
|
+
return "MARKET";
|
|
24657
|
+
return "LIMIT";
|
|
24658
|
+
};
|
|
24659
|
+
var STATUS_FROM_5P = {
|
|
24660
|
+
Pending: "PENDING",
|
|
24661
|
+
"Fully Executed": "FILLED",
|
|
24662
|
+
Cancelled: "CANCELLED",
|
|
24663
|
+
Rejected: "REJECTED",
|
|
24664
|
+
Modified: "OPEN",
|
|
24665
|
+
"After Market Order Req Received": "PENDING",
|
|
24666
|
+
Traded: "FILLED",
|
|
24667
|
+
Open: "OPEN",
|
|
24668
|
+
"Partially Executed": "OPEN",
|
|
24669
|
+
pending: "PENDING",
|
|
24670
|
+
"fully executed": "FILLED",
|
|
24671
|
+
cancelled: "CANCELLED",
|
|
24672
|
+
rejected: "REJECTED",
|
|
24673
|
+
Reject: "REJECTED",
|
|
24674
|
+
traded: "FILLED",
|
|
24675
|
+
open: "OPEN",
|
|
24676
|
+
"partially executed": "OPEN",
|
|
24677
|
+
Expired: "CANCELLED",
|
|
24678
|
+
expired: "CANCELLED"
|
|
24679
|
+
};
|
|
24680
|
+
var VALIDITY_TO_5P = {
|
|
24681
|
+
DAY: false,
|
|
24682
|
+
IOC: true
|
|
24683
|
+
};
|
|
24684
|
+
var CANDLE_INTERVAL_TO_5P = {
|
|
24685
|
+
"1m": "1m",
|
|
24686
|
+
"5m": "5m",
|
|
24687
|
+
"15m": "15m",
|
|
24688
|
+
"1h": "60m",
|
|
24689
|
+
"1d": "1d"
|
|
24690
|
+
};
|
|
24691
|
+
|
|
24692
|
+
// src/brokers/fivepaisa/fivepaisa-auth.ts
|
|
24693
|
+
function generateTOTP2(secret) {
|
|
24694
|
+
const base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
24695
|
+
let bits = "";
|
|
24696
|
+
for (const ch of secret.toUpperCase().replace(/[^A-Z2-7]/g, "")) {
|
|
24697
|
+
const idx = base32Chars.indexOf(ch);
|
|
24698
|
+
if (idx >= 0)
|
|
24699
|
+
bits += idx.toString(2).padStart(5, "0");
|
|
24700
|
+
}
|
|
24701
|
+
const decoded = [];
|
|
24702
|
+
for (let i = 0;i + 8 <= bits.length; i += 8) {
|
|
24703
|
+
decoded.push(parseInt(bits.slice(i, i + 8), 2));
|
|
24704
|
+
}
|
|
24705
|
+
const key = Buffer.from(decoded);
|
|
24706
|
+
const timeStep = Math.floor(Date.now() / 1000 / 30);
|
|
24707
|
+
const timeBuffer = Buffer.alloc(8);
|
|
24708
|
+
timeBuffer.writeUInt32BE(0, 0);
|
|
24709
|
+
timeBuffer.writeUInt32BE(timeStep, 4);
|
|
24710
|
+
const hmac = crypto3.createHmac("sha1", key).update(timeBuffer).digest();
|
|
24711
|
+
const offset = hmac[hmac.length - 1] & 15;
|
|
24712
|
+
const code = (hmac[offset] & 127) << 24 | (hmac[offset + 1] & 255) << 16 | (hmac[offset + 2] & 255) << 8 | hmac[offset + 3] & 255;
|
|
24713
|
+
return String(code % 1e6).padStart(6, "0");
|
|
24714
|
+
}
|
|
24715
|
+
async function authenticate4(creds) {
|
|
24716
|
+
const userKey = creds.userKey ?? creds.apiKey;
|
|
24717
|
+
const clientCode = creds.clientCode;
|
|
24718
|
+
const encryptionKey = creds.encryptionKey;
|
|
24719
|
+
const userId = creds.userId ?? "";
|
|
24720
|
+
const pin = creds.pin;
|
|
24721
|
+
if (!userKey)
|
|
24722
|
+
return Err(new Error("Missing userKey/apiKey in credentials"));
|
|
24723
|
+
if (!clientCode)
|
|
24724
|
+
return Err(new Error("Missing clientCode in credentials"));
|
|
24725
|
+
if (!pin)
|
|
24726
|
+
return Err(new Error("Missing pin in credentials"));
|
|
24727
|
+
if (!encryptionKey)
|
|
24728
|
+
return Err(new Error("Missing encryptionKey in credentials"));
|
|
24729
|
+
const totpSecret = creds.totpSecret;
|
|
24730
|
+
const totpCode = totpSecret ? generateTOTP2(totpSecret) : creds.totp;
|
|
24731
|
+
if (!totpCode) {
|
|
24732
|
+
return Err(new Error("Missing totpSecret or totp (6-digit code) in credentials"));
|
|
24733
|
+
}
|
|
24734
|
+
try {
|
|
24735
|
+
const totpPayload = {
|
|
24736
|
+
head: { Key: userKey },
|
|
24737
|
+
body: { Email_ID: clientCode, TOTP: totpCode, PIN: pin }
|
|
24738
|
+
};
|
|
24739
|
+
console.log("[5paisa] Step 1: TOTPLogin...");
|
|
24740
|
+
const totpRes = await fetch(ROUTES.TOTP_LOGIN, {
|
|
24741
|
+
method: "POST",
|
|
24742
|
+
headers: { "Content-Type": "application/json" },
|
|
24743
|
+
body: JSON.stringify(totpPayload)
|
|
24744
|
+
});
|
|
24745
|
+
let totpJson = await totpRes.json();
|
|
24746
|
+
if (!totpJson.body?.RequestToken && totpSecret && totpJson.body?.Message?.toLowerCase().includes("used in past")) {
|
|
24747
|
+
const secondsIntoStep = Math.floor(Date.now() / 1000) % 30;
|
|
24748
|
+
const waitMs = (30 - secondsIntoStep + 1) * 1000;
|
|
24749
|
+
console.log(`[5paisa] TOTP already used \u2014 waiting ${Math.ceil(waitMs / 1000)}s for next window...`);
|
|
24750
|
+
await new Promise((r) => setTimeout(r, waitMs));
|
|
24751
|
+
const freshTotp = generateTOTP2(totpSecret);
|
|
24752
|
+
const retryRes = await fetch(ROUTES.TOTP_LOGIN, {
|
|
24753
|
+
method: "POST",
|
|
24754
|
+
headers: { "Content-Type": "application/json" },
|
|
24755
|
+
body: JSON.stringify({ head: { Key: userKey }, body: { Email_ID: clientCode, TOTP: freshTotp, PIN: pin } })
|
|
24756
|
+
});
|
|
24757
|
+
totpJson = await retryRes.json();
|
|
24758
|
+
}
|
|
24759
|
+
if (!totpJson.body?.RequestToken) {
|
|
24760
|
+
return Err(new Error(`5paisa TOTP login failed: ${totpJson.body?.Message ?? "No RequestToken returned"}`));
|
|
24761
|
+
}
|
|
24762
|
+
console.log("[5paisa] Step 1 OK: got RequestToken");
|
|
24763
|
+
return exchangeRequestToken(totpJson.body.RequestToken, userKey, encryptionKey, userId, clientCode);
|
|
24764
|
+
} catch (err) {
|
|
24765
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
24766
|
+
}
|
|
24767
|
+
}
|
|
24768
|
+
async function exchangeRequestToken(requestToken, userKey, encryptionKey, userId, clientCode) {
|
|
24769
|
+
try {
|
|
24770
|
+
const accessPayload = {
|
|
24771
|
+
head: { Key: userKey },
|
|
24772
|
+
body: {
|
|
24773
|
+
RequestToken: requestToken,
|
|
24774
|
+
EncryKey: encryptionKey,
|
|
24775
|
+
UserId: userId
|
|
24776
|
+
}
|
|
24777
|
+
};
|
|
24778
|
+
console.log("[5paisa] Step 2: GetAccessToken...");
|
|
24779
|
+
const accessRes = await fetch(ROUTES.ACCESS_TOKEN, {
|
|
24780
|
+
method: "POST",
|
|
24781
|
+
headers: { "Content-Type": "application/json" },
|
|
24782
|
+
body: JSON.stringify(accessPayload)
|
|
24783
|
+
});
|
|
24784
|
+
const accessJson = await accessRes.json();
|
|
24785
|
+
if (!accessJson.body?.AccessToken) {
|
|
24786
|
+
return Err(new Error(`5paisa access token failed: ${accessJson.body?.Message ?? "No AccessToken returned"}`));
|
|
24787
|
+
}
|
|
24788
|
+
const resolvedClientCode = accessJson.body.ClientCode || clientCode;
|
|
24789
|
+
console.log(`[5paisa] Step 2 OK: authenticated as ${resolvedClientCode}`);
|
|
24790
|
+
const session = {
|
|
24791
|
+
accessToken: accessJson.body.AccessToken,
|
|
24792
|
+
refreshToken: undefined,
|
|
24793
|
+
expiresAt: getEndOfDay3(),
|
|
24794
|
+
ttlSeconds: secondsUntilEndOfDay3(),
|
|
24795
|
+
brokerId: "fivepaisa",
|
|
24796
|
+
userId: resolvedClientCode,
|
|
24797
|
+
metadata: { userKey, requestToken }
|
|
24798
|
+
};
|
|
24799
|
+
return Ok(session);
|
|
24800
|
+
} catch (err) {
|
|
24801
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
24802
|
+
}
|
|
24803
|
+
}
|
|
24804
|
+
async function authenticateWithToken(creds) {
|
|
24805
|
+
const accessToken = creds.accessToken;
|
|
24806
|
+
const clientCode = creds.clientCode ?? creds.userId ?? "";
|
|
24807
|
+
if (!accessToken)
|
|
24808
|
+
return Err(new Error("Missing accessToken in credentials"));
|
|
24809
|
+
return Ok({
|
|
24810
|
+
accessToken,
|
|
24811
|
+
refreshToken: undefined,
|
|
24812
|
+
expiresAt: getEndOfDay3(),
|
|
24813
|
+
ttlSeconds: secondsUntilEndOfDay3(),
|
|
24814
|
+
brokerId: "fivepaisa",
|
|
24815
|
+
userId: clientCode,
|
|
24816
|
+
metadata: { userKey: creds.userKey ?? creds.apiKey }
|
|
24817
|
+
});
|
|
24818
|
+
}
|
|
24819
|
+
async function refreshSession4(_session) {
|
|
24820
|
+
return Err(new Error("5paisa does not support session refresh. Re-authenticate with TOTP."));
|
|
24821
|
+
}
|
|
24822
|
+
function isSessionValid4(session) {
|
|
24823
|
+
if (!session)
|
|
24824
|
+
return false;
|
|
24825
|
+
return Date.now() < session.expiresAt;
|
|
24826
|
+
}
|
|
24827
|
+
function getEndOfDay3() {
|
|
24828
|
+
const now = new Date;
|
|
24829
|
+
const eod = new Date(now);
|
|
24830
|
+
eod.setHours(23, 59, 59, 999);
|
|
24831
|
+
return eod.getTime();
|
|
24832
|
+
}
|
|
24833
|
+
function secondsUntilEndOfDay3() {
|
|
24834
|
+
return Math.max(0, Math.floor((getEndOfDay3() - Date.now()) / 1000));
|
|
24835
|
+
}
|
|
24836
|
+
|
|
24837
|
+
// src/brokers/fivepaisa/fivepaisa-mapper.ts
|
|
24838
|
+
function num2(val, fallback = 0) {
|
|
24839
|
+
if (val === undefined || val === null || val === "")
|
|
24840
|
+
return fallback;
|
|
24841
|
+
const n = Number(val);
|
|
24842
|
+
return isNaN(n) ? fallback : n;
|
|
24843
|
+
}
|
|
24844
|
+
function safeExchange4(exch, exchType) {
|
|
24845
|
+
if (exchType === "D") {
|
|
24846
|
+
return exch === "B" ? "BFO" : "NFO";
|
|
24847
|
+
}
|
|
24848
|
+
if (exchType === "U")
|
|
24849
|
+
return "CDS";
|
|
24850
|
+
return EXCHANGE_FROM_5P[exch] ?? "NSE";
|
|
24851
|
+
}
|
|
24852
|
+
function inferInstrumentType4(exchType, symbol) {
|
|
24853
|
+
if (exchType === "C")
|
|
24854
|
+
return "EQ";
|
|
24855
|
+
if (symbol.endsWith("CE"))
|
|
24856
|
+
return "CE";
|
|
24857
|
+
if (symbol.endsWith("PE"))
|
|
24858
|
+
return "PE";
|
|
24859
|
+
return "FUT";
|
|
24860
|
+
}
|
|
24861
|
+
function inferSide4(qty) {
|
|
24862
|
+
if (qty > 0)
|
|
24863
|
+
return "LONG";
|
|
24864
|
+
if (qty < 0)
|
|
24865
|
+
return "SHORT";
|
|
24866
|
+
return "FLAT";
|
|
24867
|
+
}
|
|
24868
|
+
function parse5pDate(dateStr) {
|
|
24869
|
+
if (!dateStr)
|
|
24870
|
+
return 0;
|
|
24871
|
+
const match = dateStr.match(/\/Date\((\d+)/);
|
|
24872
|
+
if (match)
|
|
24873
|
+
return parseInt(match[1], 10);
|
|
24874
|
+
const ts = new Date(dateStr).getTime();
|
|
24875
|
+
if (ts && !isNaN(ts))
|
|
24876
|
+
return ts;
|
|
24877
|
+
const cleaned = dateStr.replace(/-/g, " ").replace(/\s+/g, " ").trim();
|
|
24878
|
+
const ts2 = new Date(cleaned).getTime();
|
|
24879
|
+
if (ts2 && !isNaN(ts2))
|
|
24880
|
+
return ts2;
|
|
24881
|
+
console.warn(`[5paisa] Unparseable date: "${dateStr}"`);
|
|
24882
|
+
return 0;
|
|
24883
|
+
}
|
|
24884
|
+
function normalize5pSymbol(scripName) {
|
|
24885
|
+
if (!scripName)
|
|
24886
|
+
return "";
|
|
24887
|
+
const optMatch = scripName.match(/^(.+?)\s+(\d{1,2})\s+(\w{3})\s+(\d{4})\s+(CE|PE)\s+([\d.]+)$/i);
|
|
24888
|
+
if (optMatch) {
|
|
24889
|
+
const [, underlying, day, mon, year, optType, strike] = optMatch;
|
|
24890
|
+
const strikeClean = strike.replace(/\.00$/, "");
|
|
24891
|
+
const yy = year.slice(2);
|
|
24892
|
+
const monUpper = mon.toUpperCase();
|
|
24893
|
+
return `${underlying.trim()}-${strikeClean}-${optType.toUpperCase()}-${day.padStart(2, "0")}${monUpper}${yy}`;
|
|
24894
|
+
}
|
|
24895
|
+
const futMatch = scripName.match(/^(.+?)\s+(\d{1,2})\s+(\w{3})\s+(\d{4})\s+FUT$/i);
|
|
24896
|
+
if (futMatch) {
|
|
24897
|
+
const [, underlying, day, mon, year] = futMatch;
|
|
24898
|
+
const yy = year.slice(2);
|
|
24899
|
+
return `${underlying.trim()}-FUT-${day.padStart(2, "0")}${mon.toUpperCase()}${yy}`;
|
|
24900
|
+
}
|
|
24901
|
+
return scripName;
|
|
24902
|
+
}
|
|
24903
|
+
function safeStatus4(raw) {
|
|
24904
|
+
const direct = STATUS_FROM_5P[raw];
|
|
24905
|
+
if (direct)
|
|
24906
|
+
return direct;
|
|
24907
|
+
const lower = STATUS_FROM_5P[raw.toLowerCase()];
|
|
24908
|
+
if (lower)
|
|
24909
|
+
return lower;
|
|
24910
|
+
const lc = raw.toLowerCase();
|
|
24911
|
+
if (lc.includes("reject"))
|
|
24912
|
+
return "REJECTED";
|
|
24913
|
+
if (lc.includes("cancel") || lc.includes("expired"))
|
|
24914
|
+
return "CANCELLED";
|
|
24915
|
+
if (lc.includes("traded") || lc.includes("executed") || lc.includes("filled"))
|
|
24916
|
+
return "FILLED";
|
|
24917
|
+
if (lc.includes("open") || lc.includes("partial"))
|
|
24918
|
+
return "OPEN";
|
|
24919
|
+
console.warn(`[5paisa] Unmapped order status: "${raw}" \u2014 defaulting to PENDING`);
|
|
24920
|
+
return "PENDING";
|
|
24921
|
+
}
|
|
24922
|
+
function toOrderPayload(params, clientCode, appSource) {
|
|
24923
|
+
const isStopLoss = params.type === "SL" || params.type === "SL_M";
|
|
24924
|
+
const isMarket = params.type === "MARKET" || params.type === "SL_M";
|
|
24925
|
+
const date = new Date;
|
|
24926
|
+
date.setDate(date.getDate() + 1);
|
|
24927
|
+
return {
|
|
24928
|
+
ClientCode: clientCode,
|
|
24929
|
+
Exchange: EXCHANGE_TO_5P[params.exchange],
|
|
24930
|
+
ExchangeType: EXCHANGE_TYPE_MAP[params.exchange],
|
|
24931
|
+
Price: isMarket ? 0 : params.price ?? 0,
|
|
24932
|
+
OrderID: 0,
|
|
24933
|
+
OrderType: params.side,
|
|
24934
|
+
Qty: params.quantity,
|
|
24935
|
+
UniqueOrderID: "1",
|
|
24936
|
+
DisQty: params.disclosedQty ?? params.quantity,
|
|
24937
|
+
IsStopLossOrder: isStopLoss,
|
|
24938
|
+
StopLossPrice: params.triggerPrice ?? 0,
|
|
24939
|
+
IsIOCOrder: VALIDITY_TO_5P[params.validity],
|
|
24940
|
+
IsIntraday: PRODUCT_TO_5P[params.product],
|
|
24941
|
+
IsAHOrder: "N",
|
|
24942
|
+
ValidTillDate: `/Date(${date.getTime()})/`,
|
|
24943
|
+
AppSource: appSource,
|
|
24944
|
+
ScripCode: 0,
|
|
24945
|
+
ScripData: "",
|
|
24946
|
+
RemoteOrderID: params.tag ?? ""
|
|
24947
|
+
};
|
|
24948
|
+
}
|
|
24949
|
+
function fromOrder4(order) {
|
|
24950
|
+
const qty = num2(order.Qty);
|
|
24951
|
+
const filled = num2(order.TradedQty);
|
|
24952
|
+
const exchType = String(order.ExchType ?? "C");
|
|
24953
|
+
return {
|
|
24954
|
+
orderId: String(order.ExchOrderID || order.BrokerOrderId),
|
|
24955
|
+
brokerOrderId: String(order.BrokerOrderId),
|
|
24956
|
+
broker: "fivepaisa",
|
|
24957
|
+
tradingSymbol: normalize5pSymbol(order.ScripName ?? ""),
|
|
24958
|
+
exchange: safeExchange4(String(order.Exch), exchType),
|
|
24959
|
+
side: order.BuySell === "B" ? "BUY" : "SELL",
|
|
24960
|
+
type: ORDER_TYPE_FROM_5P(order.WithSL === "Y", num2(order.Rate)),
|
|
24961
|
+
product: PRODUCT_FROM_5P(order.DelvIntra === "I"),
|
|
24962
|
+
quantity: qty,
|
|
24963
|
+
filledQty: filled,
|
|
24964
|
+
pendingQty: num2(order.PendingQty),
|
|
24965
|
+
price: num2(order.Rate),
|
|
24966
|
+
triggerPrice: num2(order.SLTriggerRate) || undefined,
|
|
24967
|
+
averagePrice: filled > 0 ? num2(order.TradedPrice ?? order.Rate) : 0,
|
|
24968
|
+
status: safeStatus4(order.OrderStatus),
|
|
24969
|
+
validity: num2(order.OrderValidity) === 3 ? "IOC" : "DAY",
|
|
24970
|
+
tag: order.RemoteOrderID || undefined,
|
|
24971
|
+
placedAt: parse5pDate(String(order.BrokerOrderTime ?? order.OrderDateTime ?? "")),
|
|
24972
|
+
updatedAt: parse5pDate(String(order.ExchOrderTime ?? order.BrokerOrderTime ?? ""))
|
|
24973
|
+
};
|
|
24974
|
+
}
|
|
24975
|
+
function fromPosition4(pos) {
|
|
24976
|
+
const netQty = num2(pos.NetQty);
|
|
24977
|
+
const buyQty = num2(pos.BuyQty);
|
|
24978
|
+
const sellQty = num2(pos.SellQty);
|
|
24979
|
+
const ltp = num2(pos.LTP);
|
|
24980
|
+
const bookedPnl = num2(pos.BookedPL);
|
|
24981
|
+
const mtom = num2(pos.MTOM);
|
|
24982
|
+
const buyAvg = num2(pos.BuyAvgRate);
|
|
24983
|
+
const sellAvg = num2(pos.SellAvgRate);
|
|
24984
|
+
const avgPrice = netQty > 0 ? buyAvg : netQty < 0 ? sellAvg : 0;
|
|
24985
|
+
const exchType = String(pos.ExchType ?? "C");
|
|
24986
|
+
return {
|
|
24987
|
+
broker: "fivepaisa",
|
|
24988
|
+
tradingSymbol: normalize5pSymbol(pos.ScripName ?? pos.Symbol ?? ""),
|
|
24989
|
+
exchange: safeExchange4(String(pos.Exch), exchType),
|
|
24990
|
+
side: inferSide4(netQty),
|
|
24991
|
+
quantity: Math.abs(netQty),
|
|
24992
|
+
buyQty,
|
|
24993
|
+
sellQty,
|
|
24994
|
+
avgPrice,
|
|
24995
|
+
ltp,
|
|
24996
|
+
pnl: bookedPnl + mtom,
|
|
24997
|
+
realizedPnl: bookedPnl,
|
|
24998
|
+
unrealizedPnl: mtom,
|
|
24999
|
+
product: PRODUCT_FROM_5P(true),
|
|
25000
|
+
instrumentType: inferInstrumentType4(exchType, pos.ScripName ?? "")
|
|
25001
|
+
};
|
|
25002
|
+
}
|
|
25003
|
+
function fromHolding4(holding) {
|
|
25004
|
+
const qty = num2(holding.Quantity);
|
|
25005
|
+
const ltp = num2(holding.CurrentPrice);
|
|
25006
|
+
return {
|
|
25007
|
+
broker: "fivepaisa",
|
|
25008
|
+
tradingSymbol: holding.Symbol ?? holding.FullName ?? "",
|
|
25009
|
+
exchange: num2(holding.NseCode) > 0 ? "NSE" : "BSE",
|
|
25010
|
+
isin: "",
|
|
25011
|
+
quantity: qty,
|
|
25012
|
+
avgPrice: 0,
|
|
25013
|
+
ltp,
|
|
25014
|
+
pnl: 0,
|
|
25015
|
+
dayChange: 0,
|
|
25016
|
+
dayChangePercent: 0
|
|
25017
|
+
};
|
|
25018
|
+
}
|
|
25019
|
+
function fromMarketFeed(item) {
|
|
25020
|
+
const ltp = num2(item.LastRate);
|
|
25021
|
+
const close = num2(item.PClose);
|
|
25022
|
+
const change = ltp - close;
|
|
25023
|
+
const changePct = close !== 0 ? change / close * 100 : 0;
|
|
25024
|
+
const exchType = String(item.ExchType ?? "C");
|
|
25025
|
+
return {
|
|
25026
|
+
tradingSymbol: String(item.ScripData ?? item.Token ?? item.ScripCode),
|
|
25027
|
+
exchange: safeExchange4(String(item.Exch), exchType),
|
|
25028
|
+
ltp,
|
|
25029
|
+
open: num2(item.OpenRate),
|
|
25030
|
+
high: num2(item.High),
|
|
25031
|
+
low: num2(item.Low),
|
|
25032
|
+
close,
|
|
25033
|
+
volume: num2(item.TotalQty),
|
|
25034
|
+
oi: item.OpenInterest ? num2(item.OpenInterest) : undefined,
|
|
25035
|
+
bid: num2(item.BidRate),
|
|
25036
|
+
ask: num2(item.OffRate),
|
|
25037
|
+
bidQty: num2(item.BidQty),
|
|
25038
|
+
askQty: num2(item.OffQty),
|
|
25039
|
+
upperCircuit: 0,
|
|
25040
|
+
lowerCircuit: 0,
|
|
25041
|
+
change,
|
|
25042
|
+
changePercent: changePct,
|
|
25043
|
+
timestamp: parse5pDate(String(item.TickDt ?? ""))
|
|
25044
|
+
};
|
|
25045
|
+
}
|
|
25046
|
+
function fromTradeBookEntry(t) {
|
|
25047
|
+
const exchType = String(t.ExchType ?? "C");
|
|
25048
|
+
return {
|
|
25049
|
+
tradeId: t.TradeNo ?? "",
|
|
25050
|
+
orderId: String(t.ExchOrderID || t.BrokerOrderId),
|
|
25051
|
+
tradingSymbol: normalize5pSymbol(t.ScripName ?? ""),
|
|
25052
|
+
exchange: safeExchange4(String(t.Exch), exchType),
|
|
25053
|
+
side: t.BuySell === "B" ? "BUY" : "SELL",
|
|
25054
|
+
quantity: num2(t.Qty),
|
|
25055
|
+
price: num2(t.Rate),
|
|
25056
|
+
product: PRODUCT_FROM_5P(t.DelvIntra === "I"),
|
|
25057
|
+
timestamp: parse5pDate(String(t.TradeTime ?? ""))
|
|
25058
|
+
};
|
|
25059
|
+
}
|
|
25060
|
+
|
|
25061
|
+
// src/brokers/fivepaisa/fivepaisa-socket.ts
|
|
25062
|
+
import WS3 from "ws";
|
|
25063
|
+
var WS_URLS = {
|
|
25064
|
+
A: "wss://aopenfeed.5paisa.com/feeds/api/chat?Value1=",
|
|
25065
|
+
B: "wss://bopenfeed.5paisa.com/feeds/api/chat?Value1=",
|
|
25066
|
+
C: "wss://openfeed.5paisa.com/feeds/api/chat?Value1="
|
|
25067
|
+
};
|
|
25068
|
+
var DEFAULT_WS_URL = "wss://openfeed.5paisa.com/feeds/api/chat?Value1=";
|
|
25069
|
+
function decodeJwtPayload(token) {
|
|
25070
|
+
try {
|
|
25071
|
+
const parts = token.split(".");
|
|
25072
|
+
if (parts.length < 2)
|
|
25073
|
+
return {};
|
|
25074
|
+
const payload = parts[1].replace(/-/g, "+").replace(/_/g, "/");
|
|
25075
|
+
const json = Buffer.from(payload, "base64").toString("utf-8");
|
|
25076
|
+
return JSON.parse(json);
|
|
25077
|
+
} catch {
|
|
25078
|
+
return {};
|
|
25079
|
+
}
|
|
25080
|
+
}
|
|
25081
|
+
function getWsUrl(accessToken) {
|
|
25082
|
+
const payload = decodeJwtPayload(accessToken);
|
|
25083
|
+
const server = String(payload["RedirectServer"] ?? "C");
|
|
25084
|
+
return WS_URLS[server] ?? DEFAULT_WS_URL;
|
|
25085
|
+
}
|
|
25086
|
+
var subIdCounter5 = 0;
|
|
25087
|
+
function nextSubId5() {
|
|
25088
|
+
return `5p_sub_${++subIdCounter5}`;
|
|
25089
|
+
}
|
|
25090
|
+
|
|
25091
|
+
class FivePaisaSocket {
|
|
25092
|
+
ws = null;
|
|
25093
|
+
state = "DISCONNECTED";
|
|
25094
|
+
subscriptions = new Map;
|
|
25095
|
+
subscribedTokens = new Set;
|
|
25096
|
+
tokenToSymbol = new Map;
|
|
25097
|
+
clientCode;
|
|
25098
|
+
accessToken;
|
|
25099
|
+
reconnectAttempts = 0;
|
|
25100
|
+
maxReconnects = 5;
|
|
25101
|
+
reconnectTimer = null;
|
|
25102
|
+
constructor(clientCode, accessToken) {
|
|
25103
|
+
this.clientCode = clientCode;
|
|
25104
|
+
this.accessToken = accessToken;
|
|
25105
|
+
}
|
|
25106
|
+
connect() {
|
|
25107
|
+
if (this.state === "CONNECTED" || this.state === "CONNECTING")
|
|
25108
|
+
return;
|
|
25109
|
+
this.state = "CONNECTING";
|
|
25110
|
+
const baseUrl = getWsUrl(this.accessToken);
|
|
25111
|
+
const url = `${baseUrl}${this.accessToken}|${this.clientCode}`;
|
|
25112
|
+
this.ws = new WS3(url);
|
|
25113
|
+
this.ws.on("open", () => {
|
|
25114
|
+
this.state = "CONNECTED";
|
|
25115
|
+
this.reconnectAttempts = 0;
|
|
25116
|
+
if (this.subscribedTokens.size > 0) {
|
|
25117
|
+
this.sendSubscription("Subscribe", Array.from(this.subscribedTokens));
|
|
25118
|
+
}
|
|
25119
|
+
});
|
|
25120
|
+
this.ws.on("message", (data) => {
|
|
25121
|
+
this.handleMessage(data);
|
|
25122
|
+
});
|
|
25123
|
+
this.ws.on("close", () => {
|
|
25124
|
+
this.state = "DISCONNECTED";
|
|
25125
|
+
this.scheduleReconnect();
|
|
25126
|
+
});
|
|
25127
|
+
this.ws.on("error", (err) => {
|
|
25128
|
+
console.warn("[FivePaisaSocket] WS error:", err.message);
|
|
25129
|
+
});
|
|
25130
|
+
}
|
|
25131
|
+
disconnect() {
|
|
25132
|
+
this.state = "DISCONNECTED";
|
|
25133
|
+
this.reconnectAttempts = this.maxReconnects;
|
|
25134
|
+
if (this.reconnectTimer) {
|
|
25135
|
+
clearTimeout(this.reconnectTimer);
|
|
25136
|
+
this.reconnectTimer = null;
|
|
25137
|
+
}
|
|
25138
|
+
if (this.ws) {
|
|
25139
|
+
this.ws.close();
|
|
25140
|
+
this.ws = null;
|
|
25141
|
+
}
|
|
25142
|
+
}
|
|
25143
|
+
reconnect(newToken) {
|
|
25144
|
+
if (newToken)
|
|
25145
|
+
this.accessToken = newToken;
|
|
25146
|
+
this.reconnectAttempts = 0;
|
|
25147
|
+
if (this.ws) {
|
|
25148
|
+
this.ws.close();
|
|
25149
|
+
this.ws = null;
|
|
25150
|
+
}
|
|
25151
|
+
this.connect();
|
|
25152
|
+
}
|
|
25153
|
+
subscribe(tokens, callback) {
|
|
25154
|
+
const id = nextSubId5();
|
|
25155
|
+
const sub = { id, tokens, callback };
|
|
25156
|
+
this.subscriptions.set(id, sub);
|
|
25157
|
+
const newTokens = [];
|
|
25158
|
+
for (const t of tokens) {
|
|
25159
|
+
if (!this.subscribedTokens.has(t)) {
|
|
25160
|
+
this.subscribedTokens.add(t);
|
|
25161
|
+
newTokens.push(t);
|
|
25162
|
+
}
|
|
25163
|
+
}
|
|
25164
|
+
if (this.state === "DISCONNECTED") {
|
|
25165
|
+
this.connect();
|
|
25166
|
+
} else if (newTokens.length > 0 && this.state === "CONNECTED") {
|
|
25167
|
+
this.sendSubscription("Subscribe", newTokens);
|
|
25168
|
+
}
|
|
25169
|
+
return sub;
|
|
25170
|
+
}
|
|
25171
|
+
unsubscribe(sub) {
|
|
25172
|
+
this.subscriptions.delete(sub.id);
|
|
25173
|
+
const stillNeeded = new Set;
|
|
25174
|
+
for (const [, s] of this.subscriptions) {
|
|
25175
|
+
for (const t of s.tokens)
|
|
25176
|
+
stillNeeded.add(t);
|
|
25177
|
+
}
|
|
25178
|
+
const toRemove = sub.tokens.filter((t) => !stillNeeded.has(t));
|
|
25179
|
+
for (const t of toRemove)
|
|
25180
|
+
this.subscribedTokens.delete(t);
|
|
25181
|
+
if (toRemove.length > 0 && this.state === "CONNECTED") {
|
|
25182
|
+
this.sendSubscription("Unsubscribe", toRemove);
|
|
25183
|
+
}
|
|
25184
|
+
if (this.subscriptions.size === 0) {
|
|
25185
|
+
this.disconnect();
|
|
25186
|
+
}
|
|
25187
|
+
}
|
|
25188
|
+
setSymbolMap(map) {
|
|
25189
|
+
for (const [k, v] of map) {
|
|
25190
|
+
this.tokenToSymbol.set(k, v);
|
|
25191
|
+
}
|
|
25192
|
+
}
|
|
25193
|
+
getConnectionState() {
|
|
25194
|
+
return this.state;
|
|
25195
|
+
}
|
|
25196
|
+
sendSubscription(operation, tokens) {
|
|
25197
|
+
if (!this.ws || this.ws.readyState !== WS3.OPEN)
|
|
25198
|
+
return;
|
|
25199
|
+
const feedData = tokens.map((token) => {
|
|
25200
|
+
const parts = token.split(":");
|
|
25201
|
+
if (parts.length === 3) {
|
|
25202
|
+
return { Exch: parts[0], ExchType: parts[1], ScripCode: parseInt(parts[2], 10) };
|
|
25203
|
+
}
|
|
25204
|
+
if (parts.length === 2) {
|
|
25205
|
+
return { Exch: parts[0], ExchType: "C", ScripCode: parseInt(parts[1], 10) };
|
|
25206
|
+
}
|
|
25207
|
+
return { Exch: "N", ExchType: "C", ScripCode: parseInt(parts[0], 10) };
|
|
25208
|
+
});
|
|
25209
|
+
const payload = {
|
|
25210
|
+
Method: "MarketFeedV3",
|
|
25211
|
+
Operation: operation,
|
|
25212
|
+
ClientCode: this.clientCode,
|
|
25213
|
+
MarketFeedData: feedData
|
|
25214
|
+
};
|
|
25215
|
+
this.ws.send(JSON.stringify(payload));
|
|
25216
|
+
}
|
|
25217
|
+
handleMessage(data) {
|
|
25218
|
+
try {
|
|
25219
|
+
const raw = typeof data === "string" ? data : data.toString("utf-8");
|
|
25220
|
+
const parsed = JSON.parse(raw);
|
|
25221
|
+
const items = Array.isArray(parsed) ? parsed : [parsed];
|
|
25222
|
+
for (const item of items) {
|
|
25223
|
+
if (item.ReqType)
|
|
25224
|
+
continue;
|
|
25225
|
+
if (item.OpenInterest !== undefined && item.LastRate === undefined)
|
|
25226
|
+
continue;
|
|
25227
|
+
if (!item.Token && !item.LastRate)
|
|
25228
|
+
continue;
|
|
25229
|
+
const token = String(item.Token ?? "");
|
|
25230
|
+
const symbolLookup = this.tokenToSymbol.get(token);
|
|
25231
|
+
const tick = {
|
|
25232
|
+
broker: "fivepaisa",
|
|
25233
|
+
token,
|
|
25234
|
+
tradingSymbol: symbolLookup ?? item.Symbol ?? token,
|
|
25235
|
+
ltp: num3(item.LastRate),
|
|
25236
|
+
open: num3(item.OpenRate),
|
|
25237
|
+
high: num3(item.High),
|
|
25238
|
+
low: num3(item.Low),
|
|
25239
|
+
close: num3(item.PClose),
|
|
25240
|
+
volume: num3(item.TotalQty),
|
|
25241
|
+
oi: item.OpenInterest ? num3(item.OpenInterest) : undefined,
|
|
25242
|
+
bid: num3(item.BidRate),
|
|
25243
|
+
ask: num3(item.OffRate),
|
|
25244
|
+
bidQty: num3(item.BidQty),
|
|
25245
|
+
askQty: num3(item.OffQty),
|
|
25246
|
+
timestamp: Date.now()
|
|
25247
|
+
};
|
|
25248
|
+
this.dispatch(tick);
|
|
25249
|
+
}
|
|
25250
|
+
} catch {}
|
|
25251
|
+
}
|
|
25252
|
+
dispatch(tick) {
|
|
25253
|
+
for (const [, sub] of this.subscriptions) {
|
|
25254
|
+
if (sub.tokens.some((t) => t === tick.token || t.endsWith(`:${tick.token}`))) {
|
|
25255
|
+
sub.callback(tick);
|
|
25256
|
+
}
|
|
25257
|
+
}
|
|
25258
|
+
}
|
|
25259
|
+
scheduleReconnect() {
|
|
25260
|
+
if (this.reconnectAttempts >= this.maxReconnects)
|
|
25261
|
+
return;
|
|
25262
|
+
if (this.subscriptions.size === 0)
|
|
25263
|
+
return;
|
|
25264
|
+
this.reconnectAttempts++;
|
|
25265
|
+
this.state = "RECONNECTING";
|
|
25266
|
+
const delay = Math.min(1000 * Math.pow(2, this.reconnectAttempts - 1), 30000);
|
|
25267
|
+
this.reconnectTimer = setTimeout(() => {
|
|
25268
|
+
this.reconnectTimer = null;
|
|
25269
|
+
this.connect();
|
|
25270
|
+
}, delay);
|
|
25271
|
+
}
|
|
25272
|
+
}
|
|
25273
|
+
function num3(val) {
|
|
25274
|
+
if (val === undefined || val === null || val === "")
|
|
25275
|
+
return 0;
|
|
25276
|
+
const n = Number(val);
|
|
25277
|
+
return isNaN(n) ? 0 : n;
|
|
25278
|
+
}
|
|
25279
|
+
|
|
25280
|
+
// src/brokers/fivepaisa/fivepaisa-instruments.ts
|
|
25281
|
+
var SEGMENTS = ["nse_eq", "nse_fo", "bse_eq", "bse_fo", "mcx_fo", "nse_currency"];
|
|
25282
|
+
function mapExchange(exch, exchType) {
|
|
25283
|
+
if (exch === "M")
|
|
25284
|
+
return "MCX";
|
|
25285
|
+
if (exchType === "D")
|
|
25286
|
+
return exch === "B" ? "BFO" : "NFO";
|
|
25287
|
+
if (exchType === "U")
|
|
25288
|
+
return "CDS";
|
|
25289
|
+
if (exch === "B")
|
|
25290
|
+
return "BSE";
|
|
25291
|
+
return "NSE";
|
|
25292
|
+
}
|
|
25293
|
+
function mapInstrumentType4(scripType, expiry) {
|
|
25294
|
+
const upper = (scripType ?? "").toUpperCase();
|
|
25295
|
+
if (upper === "CE")
|
|
25296
|
+
return "CE";
|
|
25297
|
+
if (upper === "PE")
|
|
25298
|
+
return "PE";
|
|
25299
|
+
if (upper === "XX")
|
|
25300
|
+
return expiry ? "FUT" : "EQ";
|
|
25301
|
+
if (upper === "EQ" || upper === "")
|
|
25302
|
+
return "EQ";
|
|
25303
|
+
return "FUT";
|
|
25304
|
+
}
|
|
25305
|
+
function parseExpiry4(raw) {
|
|
25306
|
+
if (!raw || raw === "")
|
|
25307
|
+
return;
|
|
25308
|
+
const d = new Date(raw);
|
|
25309
|
+
return isNaN(d.getTime()) ? undefined : d;
|
|
25310
|
+
}
|
|
25311
|
+
function parseStrike4(raw) {
|
|
25312
|
+
const n = parseFloat(raw);
|
|
25313
|
+
return isNaN(n) || n === 0 ? undefined : n;
|
|
25314
|
+
}
|
|
25315
|
+
|
|
25316
|
+
class FivePaisaInstruments {
|
|
25317
|
+
capabilities = {
|
|
25318
|
+
bulkDump: true,
|
|
25319
|
+
searchAPI: false,
|
|
25320
|
+
rawFileCache: true,
|
|
25321
|
+
rawFilePath: "data/instruments/fivepaisa-instruments.csv"
|
|
25322
|
+
};
|
|
25323
|
+
accessToken = "";
|
|
25324
|
+
clientCode = "";
|
|
25325
|
+
setCredentials(clientCode, accessToken) {
|
|
25326
|
+
this.clientCode = clientCode;
|
|
25327
|
+
this.accessToken = accessToken;
|
|
25328
|
+
}
|
|
25329
|
+
async* streamDump() {
|
|
25330
|
+
for (const segment of SEGMENTS) {
|
|
25331
|
+
const url = `${FIVEPAISA_SCRIP_MASTER_BASE}/${segment}`;
|
|
25332
|
+
try {
|
|
25333
|
+
const response = await fetch(url);
|
|
25334
|
+
if (!response.ok)
|
|
25335
|
+
continue;
|
|
25336
|
+
const text = await response.text();
|
|
25337
|
+
const lines = text.split(`
|
|
25338
|
+
`);
|
|
25339
|
+
let headers = null;
|
|
25340
|
+
for (const line of lines) {
|
|
25341
|
+
const trimmed = line.trim();
|
|
25342
|
+
if (!trimmed)
|
|
25343
|
+
continue;
|
|
25344
|
+
const fields = trimmed.split(",");
|
|
25345
|
+
if (!headers) {
|
|
25346
|
+
headers = fields.map((h) => h.trim());
|
|
25347
|
+
continue;
|
|
25348
|
+
}
|
|
25349
|
+
const row = { _segment: segment };
|
|
25350
|
+
for (let i = 0;i < headers.length && i < fields.length; i++) {
|
|
25351
|
+
row[headers[i]] = fields[i]?.trim() ?? "";
|
|
25352
|
+
}
|
|
25353
|
+
yield row;
|
|
25354
|
+
}
|
|
25355
|
+
} catch {
|
|
25356
|
+
continue;
|
|
25357
|
+
}
|
|
25358
|
+
}
|
|
25359
|
+
}
|
|
25360
|
+
normalize(raw) {
|
|
25361
|
+
const f = raw;
|
|
25362
|
+
const exch = f["Exch"] ?? "N";
|
|
25363
|
+
const exchType = f["ExchType"] ?? "C";
|
|
25364
|
+
const scripCode = f["ScripCode"] ?? "";
|
|
25365
|
+
const scripData = f["ScripData"] ?? "";
|
|
25366
|
+
const name = f["Name"] ?? "";
|
|
25367
|
+
const fullName = f["FullName"] ?? "";
|
|
25368
|
+
const scripType = f["ScripType"] ?? "";
|
|
25369
|
+
const strikeRate = f["StrikeRate"] ?? "";
|
|
25370
|
+
const expiry = f["Expiry"] ?? "";
|
|
25371
|
+
const isin = f["ISIN"] ?? "";
|
|
25372
|
+
const lotSize = parseInt(f["LotSize"] ?? "1", 10) || 1;
|
|
25373
|
+
const tickSize = parseFloat(f["TickSize"] ?? "0.05") || 0.05;
|
|
25374
|
+
const symbolRoot = f["SymbolRoot"] ?? "";
|
|
25375
|
+
const exchange = mapExchange(exch, exchType);
|
|
25376
|
+
const isIndex = scripCode.startsWith("9999");
|
|
25377
|
+
const instrumentType = isIndex ? "IDX" : mapInstrumentType4(scripType, expiry);
|
|
25378
|
+
const tradingSymbol = scripData || name || fullName;
|
|
25379
|
+
const underlying = instrumentType === "EQ" ? name || symbolRoot || tradingSymbol : symbolRoot || name || tradingSymbol.replace(/\d.*/g, "").trim();
|
|
25380
|
+
return {
|
|
25381
|
+
tradingSymbol,
|
|
25382
|
+
exchange,
|
|
25383
|
+
instrumentType,
|
|
25384
|
+
underlying,
|
|
25385
|
+
expiry: parseExpiry4(expiry),
|
|
25386
|
+
strike: parseStrike4(strikeRate),
|
|
25387
|
+
lotSize,
|
|
25388
|
+
tickSize,
|
|
25389
|
+
brokerToken: scripCode,
|
|
25390
|
+
brokerSymbol: scripData,
|
|
25391
|
+
raw
|
|
25392
|
+
};
|
|
25393
|
+
}
|
|
25394
|
+
}
|
|
25395
|
+
|
|
25396
|
+
// src/brokers/fivepaisa/FivePaisaBroker.ts
|
|
25397
|
+
class FivePaisaBroker {
|
|
25398
|
+
id = "fivepaisa";
|
|
25399
|
+
name = "5paisa";
|
|
25400
|
+
instruments;
|
|
25401
|
+
has = {
|
|
25402
|
+
bulkDump: true,
|
|
25403
|
+
searchAPI: false,
|
|
25404
|
+
optionChain: false,
|
|
25405
|
+
historicalCandles: true,
|
|
25406
|
+
websocket: true,
|
|
25407
|
+
pnlReport: false
|
|
25408
|
+
};
|
|
25409
|
+
session = null;
|
|
25410
|
+
socket = null;
|
|
25411
|
+
credentials = null;
|
|
25412
|
+
instrumentsImpl;
|
|
25413
|
+
_iMaster;
|
|
25414
|
+
constructor(options) {
|
|
25415
|
+
this.instrumentsImpl = new FivePaisaInstruments;
|
|
25416
|
+
this.instruments = this.instrumentsImpl;
|
|
25417
|
+
this._iMaster = new InstrumentMaster(options);
|
|
25418
|
+
}
|
|
25419
|
+
async authenticate(creds) {
|
|
25420
|
+
const result = creds.accessToken ? await authenticateWithToken(creds) : await authenticate4(creds);
|
|
25421
|
+
if (!result.ok)
|
|
25422
|
+
return result;
|
|
25423
|
+
this.credentials = creds;
|
|
25424
|
+
this.session = result.value;
|
|
25425
|
+
this.instrumentsImpl.setCredentials(this.session.userId, this.session.accessToken);
|
|
25426
|
+
return result;
|
|
25427
|
+
}
|
|
25428
|
+
async refreshSession(session) {
|
|
25429
|
+
return refreshSession4(session);
|
|
25430
|
+
}
|
|
25431
|
+
isSessionValid() {
|
|
25432
|
+
return isSessionValid4(this.session);
|
|
25433
|
+
}
|
|
25434
|
+
getSessionExpiry() {
|
|
25435
|
+
if (!this.session)
|
|
25436
|
+
return null;
|
|
25437
|
+
return new Date(this.session.expiresAt);
|
|
25438
|
+
}
|
|
25439
|
+
getSession() {
|
|
25440
|
+
return this.session;
|
|
25441
|
+
}
|
|
25442
|
+
async sync(force) {
|
|
25443
|
+
return this._iMaster.syncBroker(this.id, this.instruments, force);
|
|
25444
|
+
}
|
|
25445
|
+
resolve(input) {
|
|
25446
|
+
return this._iMaster.resolve(input);
|
|
25447
|
+
}
|
|
25448
|
+
search(query, exchange, instrumentType, limit) {
|
|
25449
|
+
return this._iMaster.search(query, exchange, instrumentType, limit);
|
|
25450
|
+
}
|
|
25451
|
+
async placeOrder(params) {
|
|
25452
|
+
try {
|
|
25453
|
+
const exchange = params.exchange ?? "NSE";
|
|
25454
|
+
const clientCode = this.session?.userId ?? "";
|
|
25455
|
+
const appSource = parseInt(this.credentials?.appSource ?? "0", 10) || 0;
|
|
25456
|
+
let scripCode = 0;
|
|
25457
|
+
const resolveKey = `${exchange}:${params.tradingSymbol}`;
|
|
25458
|
+
const resolved = this.resolve(resolveKey);
|
|
25459
|
+
if (resolved.ok && resolved.value.brokerTokens.fivepaisa?.scripCode) {
|
|
25460
|
+
scripCode = parseInt(resolved.value.brokerTokens.fivepaisa.scripCode, 10);
|
|
25461
|
+
}
|
|
25462
|
+
if (scripCode === 0) {
|
|
25463
|
+
const results = this.search(params.tradingSymbol, exchange, undefined, 5);
|
|
25464
|
+
for (const r of results) {
|
|
25465
|
+
if (r.brokerTokens.fivepaisa?.scripCode) {
|
|
25466
|
+
scripCode = parseInt(r.brokerTokens.fivepaisa.scripCode, 10);
|
|
25467
|
+
break;
|
|
25468
|
+
}
|
|
25469
|
+
}
|
|
25470
|
+
}
|
|
25471
|
+
if (scripCode === 0) {
|
|
25472
|
+
return Err(new Error(`Cannot resolve ScripCode for ${params.tradingSymbol} on ${exchange}`));
|
|
25473
|
+
}
|
|
25474
|
+
const body = toOrderPayload({ ...params, exchange }, clientCode, appSource);
|
|
25475
|
+
body.ScripCode = scripCode;
|
|
25476
|
+
const payload = {
|
|
25477
|
+
head: { ...this.buildHead(), requestCode: REQUEST_CODES.PLACE_ORDER },
|
|
25478
|
+
body
|
|
25479
|
+
};
|
|
25480
|
+
const res = await this.post(ROUTES.PLACE_ORDER, payload);
|
|
25481
|
+
console.log("[5paisa] PlaceOrder response:", JSON.stringify(res));
|
|
25482
|
+
const brokerOid = res.BrokerOrderID ?? 0;
|
|
25483
|
+
const isRejected = (res.Status ?? 0) !== 0 || (res.RMSResponseCode ?? 0) < 0 || (res.Message ?? "").toLowerCase().includes("rejected");
|
|
25484
|
+
if (isRejected) {
|
|
25485
|
+
return Err(new Error(res.Message ?? `Order rejected (Status=${res.Status}, RMS=${res.RMSResponseCode})`));
|
|
25486
|
+
}
|
|
25487
|
+
if (brokerOid === 0) {
|
|
25488
|
+
return Err(new Error(`Order placement failed: ${res.Message ?? "No BrokerOrderID returned"}`));
|
|
25489
|
+
}
|
|
25490
|
+
return Ok({
|
|
25491
|
+
orderId: String(brokerOid),
|
|
25492
|
+
brokerOrderId: String(brokerOid),
|
|
25493
|
+
status: "PENDING",
|
|
25494
|
+
timestamp: Date.now()
|
|
25495
|
+
});
|
|
25496
|
+
} catch (err) {
|
|
25497
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
25498
|
+
}
|
|
25499
|
+
}
|
|
25500
|
+
async modifyOrder(orderId, params) {
|
|
25501
|
+
try {
|
|
25502
|
+
const body = {
|
|
25503
|
+
ExchOrderID: orderId,
|
|
25504
|
+
Qty: params.quantity,
|
|
25505
|
+
Price: params.price
|
|
25506
|
+
};
|
|
25507
|
+
const payload = {
|
|
25508
|
+
head: { ...this.buildHead(), requestCode: REQUEST_CODES.MODIFY_ORDER },
|
|
25509
|
+
body: { ...this.buildBody(), ...body }
|
|
25510
|
+
};
|
|
25511
|
+
const res = await this.post(ROUTES.MODIFY_ORDER, payload);
|
|
25512
|
+
return Ok({
|
|
25513
|
+
orderId: String(res.ExchOrderID ?? orderId),
|
|
25514
|
+
brokerOrderId: String(res.BrokerOrderID ?? ""),
|
|
25515
|
+
status: "OPEN",
|
|
25516
|
+
timestamp: Date.now()
|
|
25517
|
+
});
|
|
25518
|
+
} catch (err) {
|
|
25519
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
25520
|
+
}
|
|
25521
|
+
}
|
|
25522
|
+
async cancelOrder(orderId) {
|
|
25523
|
+
try {
|
|
25524
|
+
const payload = {
|
|
25525
|
+
head: { ...this.buildHead(), requestCode: REQUEST_CODES.CANCEL_ORDER },
|
|
25526
|
+
body: { ...this.buildBody(), ExchOrderID: orderId }
|
|
25527
|
+
};
|
|
25528
|
+
await this.post(ROUTES.CANCEL_ORDER, payload);
|
|
25529
|
+
return Ok({
|
|
25530
|
+
orderId,
|
|
25531
|
+
status: "CANCELLED",
|
|
25532
|
+
timestamp: Date.now()
|
|
25533
|
+
});
|
|
25534
|
+
} catch (err) {
|
|
25535
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
25536
|
+
}
|
|
25537
|
+
}
|
|
25538
|
+
async getOrders() {
|
|
25539
|
+
try {
|
|
25540
|
+
const payload = {
|
|
25541
|
+
head: { ...this.buildHead(), requestCode: REQUEST_CODES.ORDER_BOOK },
|
|
25542
|
+
body: this.buildBody()
|
|
25543
|
+
};
|
|
25544
|
+
const res = await this.post(ROUTES.ORDER_BOOK, payload);
|
|
25545
|
+
if (!res.OrderBookDetail || res.OrderBookDetail.length === 0) {
|
|
25546
|
+
return Ok([]);
|
|
25547
|
+
}
|
|
25548
|
+
return Ok(res.OrderBookDetail.map(fromOrder4));
|
|
25549
|
+
} catch (err) {
|
|
25550
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
25551
|
+
}
|
|
25552
|
+
}
|
|
25553
|
+
async getOrderHistory(orderId) {
|
|
25554
|
+
try {
|
|
25555
|
+
const ordersResult = await this.getOrders();
|
|
25556
|
+
if (!ordersResult.ok)
|
|
25557
|
+
return Err(ordersResult.error);
|
|
25558
|
+
const matching = ordersResult.value.filter((o) => o.brokerOrderId === orderId || o.orderId === orderId);
|
|
25559
|
+
if (matching.length === 0)
|
|
25560
|
+
return Ok([]);
|
|
25561
|
+
const order = matching[0];
|
|
25562
|
+
let avgPrice = order.averagePrice ?? 0;
|
|
25563
|
+
if (order.status === "FILLED" && avgPrice === 0) {
|
|
25564
|
+
try {
|
|
25565
|
+
const tradePayload = {
|
|
25566
|
+
head: { ...this.buildHead(), requestCode: REQUEST_CODES.TRADE_BOOK },
|
|
25567
|
+
body: this.buildBody()
|
|
25568
|
+
};
|
|
25569
|
+
const tradeRes = await this.post(ROUTES.TRADE_BOOK, tradePayload);
|
|
25570
|
+
const trades = tradeRes.TradeBookDetail?.filter((t) => String(t.BrokerOrderId) === orderId) ?? [];
|
|
25571
|
+
if (trades.length > 0) {
|
|
25572
|
+
const totalQty = trades.reduce((s, t) => s + (t.Qty ?? 0), 0);
|
|
25573
|
+
const totalVal = trades.reduce((s, t) => s + (t.Rate ?? 0) * (t.Qty ?? 0), 0);
|
|
25574
|
+
avgPrice = totalQty > 0 ? totalVal / totalQty : 0;
|
|
25575
|
+
}
|
|
25576
|
+
} catch {}
|
|
25577
|
+
}
|
|
25578
|
+
const updates = matching.map((o) => ({
|
|
25579
|
+
orderId: o.brokerOrderId || o.orderId,
|
|
25580
|
+
status: o.status,
|
|
25581
|
+
filledQty: o.filledQty ?? 0,
|
|
25582
|
+
averagePrice: o === order ? avgPrice : o.averagePrice ?? 0,
|
|
25583
|
+
timestamp: Date.now(),
|
|
25584
|
+
message: ""
|
|
25585
|
+
}));
|
|
25586
|
+
return Ok(updates);
|
|
25587
|
+
} catch (err) {
|
|
25588
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
25589
|
+
}
|
|
25590
|
+
}
|
|
25591
|
+
async getPositions() {
|
|
25592
|
+
try {
|
|
25593
|
+
const payload = {
|
|
25594
|
+
head: { ...this.buildHead(), requestCode: REQUEST_CODES.POSITIONS },
|
|
25595
|
+
body: this.buildBody()
|
|
25596
|
+
};
|
|
25597
|
+
const res = await this.post(ROUTES.POSITIONS, payload);
|
|
25598
|
+
if (!res.NetPositionDetail || res.NetPositionDetail.length === 0) {
|
|
25599
|
+
return Ok([]);
|
|
25600
|
+
}
|
|
25601
|
+
return Ok(res.NetPositionDetail.map(fromPosition4));
|
|
25602
|
+
} catch (err) {
|
|
25603
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
25604
|
+
}
|
|
25605
|
+
}
|
|
25606
|
+
async getHoldings() {
|
|
25607
|
+
try {
|
|
25608
|
+
const payload = {
|
|
25609
|
+
head: { ...this.buildHead(), requestCode: REQUEST_CODES.HOLDINGS },
|
|
25610
|
+
body: this.buildBody()
|
|
25611
|
+
};
|
|
25612
|
+
const res = await this.post(ROUTES.HOLDINGS, payload);
|
|
25613
|
+
if (!res.Data || res.Data.length === 0) {
|
|
25614
|
+
return Ok([]);
|
|
25615
|
+
}
|
|
25616
|
+
return Ok(res.Data.map(fromHolding4));
|
|
25617
|
+
} catch (err) {
|
|
25618
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
25619
|
+
}
|
|
25620
|
+
}
|
|
25621
|
+
async getLTP(symbols) {
|
|
25622
|
+
try {
|
|
25623
|
+
const result = new Map;
|
|
25624
|
+
const feedData = this.parseSymbolsToFeedData(symbols);
|
|
25625
|
+
const payload = {
|
|
25626
|
+
head: { key: this.credentials?.userKey ?? this.credentials?.apiKey ?? "" },
|
|
25627
|
+
body: {
|
|
25628
|
+
MarketFeedData: feedData,
|
|
25629
|
+
LastRequestTime: "/Date(0)/",
|
|
25630
|
+
RefreshRate: "H"
|
|
25631
|
+
}
|
|
25632
|
+
};
|
|
25633
|
+
const res = await this.post(ROUTES.MARKET_FEED_V1, payload);
|
|
25634
|
+
if (res.Data && Array.isArray(res.Data)) {
|
|
25635
|
+
for (let i = 0;i < res.Data.length; i++) {
|
|
25636
|
+
result.set(symbols[i], res.Data[i].LastRate ?? 0);
|
|
25637
|
+
}
|
|
25638
|
+
}
|
|
25639
|
+
return Ok(result);
|
|
25640
|
+
} catch (err) {
|
|
25641
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
25642
|
+
}
|
|
25643
|
+
}
|
|
25644
|
+
async getQuote(symbols) {
|
|
25645
|
+
try {
|
|
25646
|
+
const result = new Map;
|
|
25647
|
+
const feedData = this.parseSymbolsToFeedData(symbols);
|
|
25648
|
+
const payload = {
|
|
25649
|
+
head: { key: this.credentials?.userKey ?? this.credentials?.apiKey ?? "" },
|
|
25650
|
+
body: {
|
|
25651
|
+
MarketFeedData: feedData,
|
|
25652
|
+
LastRequestTime: "/Date(0)/",
|
|
25653
|
+
RefreshRate: "H"
|
|
25654
|
+
}
|
|
25655
|
+
};
|
|
25656
|
+
const res = await this.post(ROUTES.MARKET_FEED_V1, payload);
|
|
25657
|
+
if (res.Data && Array.isArray(res.Data)) {
|
|
25658
|
+
for (let i = 0;i < res.Data.length; i++) {
|
|
25659
|
+
result.set(symbols[i], fromMarketFeed(res.Data[i]));
|
|
25660
|
+
}
|
|
25661
|
+
}
|
|
25662
|
+
return Ok(result);
|
|
25663
|
+
} catch (err) {
|
|
25664
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
25665
|
+
}
|
|
25666
|
+
}
|
|
25667
|
+
async getOptionChain(_underlying, _expiry) {
|
|
25668
|
+
return Err(new Error("5paisa does not expose a direct option chain API via OpenAPI."));
|
|
25669
|
+
}
|
|
25670
|
+
async getCandles(symbol, interval, from, to) {
|
|
25671
|
+
try {
|
|
25672
|
+
const token = this.session?.accessToken ?? "";
|
|
25673
|
+
const parts = symbol.split(":");
|
|
25674
|
+
let exch = "N";
|
|
25675
|
+
let exchType = "C";
|
|
25676
|
+
let scripCode = "";
|
|
25677
|
+
if (parts.length === 3) {
|
|
25678
|
+
exch = parts[0];
|
|
25679
|
+
exchType = parts[1];
|
|
25680
|
+
scripCode = parts[2];
|
|
25681
|
+
} else if (parts.length === 2) {
|
|
25682
|
+
exch = parts[0];
|
|
25683
|
+
scripCode = parts[1];
|
|
25684
|
+
} else {
|
|
25685
|
+
scripCode = parts[0];
|
|
25686
|
+
}
|
|
25687
|
+
const timeframe = CANDLE_INTERVAL_TO_5P[interval] ?? "1d";
|
|
25688
|
+
const fromStr = from.toISOString().slice(0, 10);
|
|
25689
|
+
const toStr = to.toISOString().slice(0, 10);
|
|
25690
|
+
const url = `${FIVEPAISA_HISTORICAL_BASE}/${exch}/${exchType}/${scripCode}/${timeframe}?from=${fromStr}&end=${toStr}`;
|
|
25691
|
+
const res = await fetch(url, {
|
|
25692
|
+
headers: {
|
|
25693
|
+
"Content-Type": "application/json",
|
|
25694
|
+
Authorization: `bearer ${token}`
|
|
25695
|
+
}
|
|
25696
|
+
});
|
|
25697
|
+
const text = await res.text();
|
|
25698
|
+
let json;
|
|
25699
|
+
try {
|
|
25700
|
+
json = JSON.parse(text);
|
|
25701
|
+
} catch {
|
|
25702
|
+
return Err(new Error(`Historical API returned non-JSON (${res.status}): ${text.slice(0, 200)}`));
|
|
25703
|
+
}
|
|
25704
|
+
if (json.status !== "success" && json.message) {
|
|
25705
|
+
return Err(new Error(`Historical API error: ${json.message}`));
|
|
25706
|
+
}
|
|
25707
|
+
const rawCandles = json.data?.candles;
|
|
25708
|
+
if (!Array.isArray(rawCandles))
|
|
25709
|
+
return Ok([]);
|
|
25710
|
+
const candles = rawCandles.map((c) => ({
|
|
25711
|
+
timestamp: new Date(String(c[0])).getTime(),
|
|
25712
|
+
open: Number(c[1]) || 0,
|
|
25713
|
+
high: Number(c[2]) || 0,
|
|
25714
|
+
low: Number(c[3]) || 0,
|
|
25715
|
+
close: Number(c[4]) || 0,
|
|
25716
|
+
volume: Number(c[5]) || 0
|
|
25717
|
+
}));
|
|
25718
|
+
return Ok(candles);
|
|
25719
|
+
} catch (err) {
|
|
25720
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
25721
|
+
}
|
|
25722
|
+
}
|
|
25723
|
+
toBrokerToken(instrument, _format = "ws") {
|
|
25724
|
+
const fp = instrument.brokerTokens.fivepaisa;
|
|
25725
|
+
if (!fp?.scripCode)
|
|
25726
|
+
return null;
|
|
25727
|
+
const exch = EXCHANGE_TO_5P[instrument.exchange] ?? "N";
|
|
25728
|
+
const exchType = fp.exchangeType ?? EXCHANGE_TYPE_MAP[instrument.exchange] ?? "C";
|
|
25729
|
+
return `${exch}:${exchType}:${fp.scripCode}`;
|
|
25730
|
+
}
|
|
25731
|
+
getIndexTokens(format = "ws") {
|
|
25732
|
+
const result = new Map;
|
|
25733
|
+
for (const inst of this._iMaster.getIndices()) {
|
|
25734
|
+
const token = this.toBrokerToken(inst, format);
|
|
25735
|
+
if (token)
|
|
25736
|
+
result.set(inst.underlying.toUpperCase(), token);
|
|
25737
|
+
}
|
|
25738
|
+
return result;
|
|
25739
|
+
}
|
|
25740
|
+
subscribeTicks(tokens, cb) {
|
|
25741
|
+
this.ensureSocket();
|
|
25742
|
+
return this.socket.subscribe(tokens, cb);
|
|
25743
|
+
}
|
|
25744
|
+
unsubscribeTicks(sub) {
|
|
25745
|
+
if (this.socket) {
|
|
25746
|
+
this.socket.unsubscribe(sub);
|
|
25747
|
+
}
|
|
25748
|
+
}
|
|
25749
|
+
getConnectionState() {
|
|
25750
|
+
return this.socket?.getConnectionState() ?? "DISCONNECTED";
|
|
25751
|
+
}
|
|
25752
|
+
async getMargins() {
|
|
25753
|
+
try {
|
|
25754
|
+
const payload = {
|
|
25755
|
+
head: { ...this.buildHead(), requestCode: REQUEST_CODES.MARGIN },
|
|
25756
|
+
body: this.buildBody()
|
|
25757
|
+
};
|
|
25758
|
+
const res = await this.post(ROUTES.MARGIN, payload);
|
|
25759
|
+
if (!res.EquityMargin || res.EquityMargin.length === 0) {
|
|
25760
|
+
return Err(new Error(`Failed to fetch margins: ${res.Message ?? "no EquityMargin in response"}`));
|
|
25761
|
+
}
|
|
25762
|
+
const m = res.EquityMargin[0];
|
|
25763
|
+
const available = Number(m.AvailableMargin ?? m.NetAvailableMargin ?? 0);
|
|
25764
|
+
const used = Number(m.MarginUsed ?? 0);
|
|
25765
|
+
const collateral = Number(m.Collateral ?? 0);
|
|
25766
|
+
const total = available + used;
|
|
25767
|
+
return Ok({
|
|
25768
|
+
available,
|
|
25769
|
+
used,
|
|
25770
|
+
total,
|
|
25771
|
+
collateral,
|
|
25772
|
+
segments: {
|
|
25773
|
+
equity: { available, used }
|
|
25774
|
+
}
|
|
25775
|
+
});
|
|
25776
|
+
} catch (err) {
|
|
25777
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
25778
|
+
}
|
|
25779
|
+
}
|
|
25780
|
+
async getFunds() {
|
|
25781
|
+
try {
|
|
25782
|
+
const margins = await this.getMargins();
|
|
25783
|
+
if (!margins.ok)
|
|
25784
|
+
return margins;
|
|
25785
|
+
return Ok({
|
|
25786
|
+
equity: margins.value.available,
|
|
25787
|
+
commodity: 0,
|
|
25788
|
+
total: margins.value.available
|
|
25789
|
+
});
|
|
25790
|
+
} catch (err) {
|
|
25791
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
25792
|
+
}
|
|
25793
|
+
}
|
|
25794
|
+
async getTradeHistory(_from, _to) {
|
|
25795
|
+
try {
|
|
25796
|
+
const payload = {
|
|
25797
|
+
head: { ...this.buildHead(), requestCode: REQUEST_CODES.TRADE_BOOK },
|
|
25798
|
+
body: this.buildBody()
|
|
25799
|
+
};
|
|
25800
|
+
const res = await this.post(ROUTES.TRADE_BOOK, payload);
|
|
25801
|
+
if (!res.TradeBookDetail || res.TradeBookDetail.length === 0) {
|
|
25802
|
+
return Ok([]);
|
|
25803
|
+
}
|
|
25804
|
+
return Ok(res.TradeBookDetail.map(fromTradeBookEntry));
|
|
25805
|
+
} catch (err) {
|
|
25806
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
25807
|
+
}
|
|
25808
|
+
}
|
|
25809
|
+
async getPnLReport(_from, _to) {
|
|
25810
|
+
return Err(new Error("5paisa does not expose a direct PnL report API."));
|
|
25811
|
+
}
|
|
25812
|
+
parseSymbolsToFeedData(symbols) {
|
|
25813
|
+
return symbols.map((sym) => {
|
|
25814
|
+
const parts = sym.split(":");
|
|
25815
|
+
if (parts.length === 3) {
|
|
25816
|
+
return { Exch: parts[0], ExchType: parts[1], ScripCode: parseInt(parts[2], 10) };
|
|
25817
|
+
}
|
|
25818
|
+
if (parts.length === 2) {
|
|
25819
|
+
return { Exch: parts[0], ExchType: "C", ScripCode: parseInt(parts[1], 10) };
|
|
25820
|
+
}
|
|
25821
|
+
return { Exch: "N", ExchType: "C", ScripCode: parseInt(parts[0], 10) };
|
|
25822
|
+
});
|
|
25823
|
+
}
|
|
25824
|
+
buildHead() {
|
|
25825
|
+
return {
|
|
25826
|
+
appName: this.credentials?.appName ?? "",
|
|
25827
|
+
appVer: "1.0",
|
|
25828
|
+
key: this.credentials?.userKey ?? this.credentials?.apiKey ?? "",
|
|
25829
|
+
osName: "WEB",
|
|
25830
|
+
requestCode: "",
|
|
25831
|
+
userId: this.credentials?.userId ?? "",
|
|
25832
|
+
password: this.credentials?.password ?? ""
|
|
25833
|
+
};
|
|
25834
|
+
}
|
|
25835
|
+
buildBody() {
|
|
25836
|
+
return {
|
|
25837
|
+
ClientCode: this.session?.userId ?? ""
|
|
25838
|
+
};
|
|
25839
|
+
}
|
|
25840
|
+
async post(url, payload) {
|
|
25841
|
+
if (!this.session)
|
|
25842
|
+
throw new Error("Not authenticated");
|
|
25843
|
+
console.log(`[5paisa] POST ${url}`);
|
|
25844
|
+
console.log(`[5paisa] Payload:`, JSON.stringify(payload).slice(0, 500));
|
|
25845
|
+
const response = await fetch(url, {
|
|
25846
|
+
method: "POST",
|
|
25847
|
+
headers: {
|
|
25848
|
+
"Content-Type": "application/json",
|
|
25849
|
+
Authorization: `Bearer ${this.session.accessToken}`
|
|
25850
|
+
},
|
|
25851
|
+
body: JSON.stringify(payload)
|
|
25852
|
+
});
|
|
25853
|
+
const text = await response.text();
|
|
25854
|
+
let json;
|
|
25855
|
+
try {
|
|
25856
|
+
json = JSON.parse(text);
|
|
25857
|
+
} catch {
|
|
25858
|
+
throw new Error(`5paisa API error (${response.status}): non-JSON response: ${text.slice(0, 200)}`);
|
|
25859
|
+
}
|
|
25860
|
+
if (!json.body) {
|
|
25861
|
+
throw new Error(`5paisa API error (${response.status}): no body in response`);
|
|
25862
|
+
}
|
|
25863
|
+
return json.body;
|
|
25864
|
+
}
|
|
25865
|
+
ensureSocket() {
|
|
25866
|
+
if (!this.session)
|
|
25867
|
+
throw new Error("Cannot create socket without an active session");
|
|
25868
|
+
if (this.socket) {
|
|
25869
|
+
if (this.socket.getConnectionState() === "DISCONNECTED") {
|
|
25870
|
+
this.socket.reconnect(this.session.accessToken);
|
|
25871
|
+
}
|
|
25872
|
+
return;
|
|
25873
|
+
}
|
|
25874
|
+
this.socket = new FivePaisaSocket(this.session.userId, this.session.accessToken);
|
|
25875
|
+
this.socket.connect();
|
|
25876
|
+
}
|
|
25877
|
+
}
|
|
24253
25878
|
// src/index.ts
|
|
24254
|
-
var VERSION = "0.3.0";
|
|
24255
25879
|
function createBroker(brokerId, options) {
|
|
24256
25880
|
switch (brokerId) {
|
|
24257
25881
|
case "zerodha":
|
|
@@ -24260,6 +25884,8 @@ function createBroker(brokerId, options) {
|
|
|
24260
25884
|
return new FinvasiaBroker(options);
|
|
24261
25885
|
case "dhan":
|
|
24262
25886
|
return new DhanBroker(options);
|
|
25887
|
+
case "fivepaisa":
|
|
25888
|
+
return new FivePaisaBroker(options);
|
|
24263
25889
|
case "paper":
|
|
24264
25890
|
return new PaperBroker(options);
|
|
24265
25891
|
default: {
|
|
@@ -24275,7 +25901,6 @@ export {
|
|
|
24275
25901
|
createBroker,
|
|
24276
25902
|
ZerodhaBroker,
|
|
24277
25903
|
WSManager,
|
|
24278
|
-
VERSION,
|
|
24279
25904
|
SessionManager,
|
|
24280
25905
|
SessionExpiredError,
|
|
24281
25906
|
RateLimitError,
|
|
@@ -24285,6 +25910,7 @@ export {
|
|
|
24285
25910
|
NsekitError,
|
|
24286
25911
|
InstrumentNotFoundError,
|
|
24287
25912
|
InstrumentMaster,
|
|
25913
|
+
FivePaisaBroker,
|
|
24288
25914
|
FinvasiaBroker,
|
|
24289
25915
|
Err,
|
|
24290
25916
|
DhanBroker,
|