nsekit 0.3.1 → 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 +17 -0
- package/dist/brokers/dhan/DhanBroker.d.ts.map +1 -1
- package/dist/brokers/dhan/dhan-auth.d.ts +41 -7
- package/dist/brokers/dhan/dhan-auth.d.ts.map +1 -1
- package/dist/brokers/dhan/dhan-constants.d.ts +1 -0
- package/dist/brokers/dhan/dhan-constants.d.ts.map +1 -1
- package/dist/brokers/dhan/dhan-instruments.d.ts.map +1 -1
- package/dist/brokers/dhan/dhan-mapper.d.ts +1 -0
- package/dist/brokers/dhan/dhan-mapper.d.ts.map +1 -1
- package/dist/brokers/dhan/dhan-socket.d.ts +8 -0
- package/dist/brokers/dhan/dhan-socket.d.ts.map +1 -1
- package/dist/brokers/finvasia/FinvasiaBroker.d.ts +43 -0
- 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 +1 -1
- package/dist/brokers/finvasia/finvasia-instruments.d.ts.map +1 -1
- package/dist/brokers/finvasia/finvasia-mapper.d.ts +5 -1
- package/dist/brokers/finvasia/finvasia-mapper.d.ts.map +1 -1
- package/dist/brokers/finvasia/finvasia-socket.d.ts +9 -2
- 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 +1949 -82
- 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 +51 -51
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);
|
|
@@ -19448,8 +19456,9 @@ class InstrumentMaster {
|
|
|
19448
19456
|
if (local.ok)
|
|
19449
19457
|
return local;
|
|
19450
19458
|
if (brokerInstruments.capabilities.searchAPI && brokerInstruments.searchAPI) {
|
|
19451
|
-
const { symbol } = this.parseInput(input);
|
|
19452
|
-
const
|
|
19459
|
+
const { exchange, symbol } = this.parseInput(input);
|
|
19460
|
+
const inferredExchange = exchange ?? (/-(?:CE|PE|FUT)-/.test(symbol) ? "NFO" : undefined);
|
|
19461
|
+
const apiResult = await brokerInstruments.searchAPI(symbol, inferredExchange);
|
|
19453
19462
|
if (apiResult.ok && apiResult.value.length > 0) {
|
|
19454
19463
|
for (const entry of apiResult.value)
|
|
19455
19464
|
this.mergeEntry(brokerId, entry);
|
|
@@ -19595,6 +19604,11 @@ class InstrumentMaster {
|
|
|
19595
19604
|
securityId: entry.brokerToken,
|
|
19596
19605
|
sid: entry.brokerSymbol
|
|
19597
19606
|
};
|
|
19607
|
+
} else if (brokerId === "fivepaisa") {
|
|
19608
|
+
instrument.brokerTokens.fivepaisa = {
|
|
19609
|
+
scripCode: entry.brokerToken,
|
|
19610
|
+
exchangeType: entry.raw?.["ExchType"] ?? "C"
|
|
19611
|
+
};
|
|
19598
19612
|
}
|
|
19599
19613
|
if (entry.expiry && !instrument.expiry)
|
|
19600
19614
|
instrument.expiry = entry.expiry;
|
|
@@ -19626,7 +19640,9 @@ class InstrumentMaster {
|
|
|
19626
19640
|
delete instrument.brokerTokens.finvasia;
|
|
19627
19641
|
else if (brokerId === "dhan")
|
|
19628
19642
|
delete instrument.brokerTokens.dhan;
|
|
19629
|
-
|
|
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;
|
|
19630
19646
|
if (!hasAnyBroker) {
|
|
19631
19647
|
this.instruments.delete(key);
|
|
19632
19648
|
}
|
|
@@ -20192,8 +20208,8 @@ function safeSide(raw) {
|
|
|
20192
20208
|
}
|
|
20193
20209
|
function parseTimestamp(ts) {
|
|
20194
20210
|
if (!ts)
|
|
20195
|
-
return
|
|
20196
|
-
return new Date(ts).getTime();
|
|
20211
|
+
return 0;
|
|
20212
|
+
return new Date(ts).getTime() || 0;
|
|
20197
20213
|
}
|
|
20198
20214
|
function inferInstrumentType(symbol, exchange) {
|
|
20199
20215
|
if (exchange === "NSE" || exchange === "BSE")
|
|
@@ -20592,8 +20608,9 @@ class ZerodhaInstruments {
|
|
|
20592
20608
|
const lotSizeRaw = fields["lot_size"] ?? fields[COL.LOT_SIZE] ?? "1";
|
|
20593
20609
|
const tickSizeRaw = fields["tick_size"] ?? fields[COL.TICK_SIZE] ?? "0.05";
|
|
20594
20610
|
const instTypeRaw = fields["instrument_type"] ?? fields[COL.INSTRUMENT_TYPE] ?? "EQ";
|
|
20611
|
+
const segmentRaw = fields["segment"] ?? fields[COL.SEGMENT] ?? "";
|
|
20595
20612
|
const exchange = EXCHANGE_MAP[exchangeRaw] ?? "NSE";
|
|
20596
|
-
const instrumentType = mapInstrumentType(instTypeRaw);
|
|
20613
|
+
const instrumentType = segmentRaw.toUpperCase() === "INDICES" ? "IDX" : mapInstrumentType(instTypeRaw);
|
|
20597
20614
|
return {
|
|
20598
20615
|
tradingSymbol,
|
|
20599
20616
|
exchange,
|
|
@@ -20654,6 +20671,9 @@ class ZerodhaBroker {
|
|
|
20654
20671
|
return null;
|
|
20655
20672
|
return new Date(this.session.expiresAt);
|
|
20656
20673
|
}
|
|
20674
|
+
getSession() {
|
|
20675
|
+
return this.session;
|
|
20676
|
+
}
|
|
20657
20677
|
async sync(force) {
|
|
20658
20678
|
return this._iMaster.syncBroker(this.id, this.instruments, force);
|
|
20659
20679
|
}
|
|
@@ -20839,6 +20859,25 @@ class ZerodhaBroker {
|
|
|
20839
20859
|
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
20840
20860
|
}
|
|
20841
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
|
+
}
|
|
20842
20881
|
subscribeTicks(tokens, cb) {
|
|
20843
20882
|
this.ensureSocket();
|
|
20844
20883
|
return this.socket.subscribe(tokens, cb);
|
|
@@ -21011,8 +21050,9 @@ var VALIDITY_FROM_SHOONYA = {
|
|
|
21011
21050
|
DAY: "DAY",
|
|
21012
21051
|
IOC: "IOC"
|
|
21013
21052
|
};
|
|
21014
|
-
var SHOONYA_API_BASE = "https://api.shoonya.com/
|
|
21053
|
+
var SHOONYA_API_BASE = "https://api.shoonya.com/NorenWClientAPI";
|
|
21015
21054
|
var SHOONYA_WS_URL = "wss://api.shoonya.com/NorenWSTP/";
|
|
21055
|
+
var SHOONYA_WS_OAUTH_URL = "wss://api.shoonya.com/NorenWSAPI/";
|
|
21016
21056
|
var SHOONYA_INSTRUMENTS_BASE = "https://api.shoonya.com/";
|
|
21017
21057
|
var SHOONYA_INSTRUMENT_FILES = {
|
|
21018
21058
|
NSE: "NSE_symbols.txt.zip",
|
|
@@ -21032,6 +21072,7 @@ var CANDLE_INTERVAL_TO_SHOONYA = {
|
|
|
21032
21072
|
|
|
21033
21073
|
// src/brokers/finvasia/finvasia-auth.ts
|
|
21034
21074
|
import crypto2 from "crypto";
|
|
21075
|
+
var SHOONYA_OAUTH_BASE = "https://trade.shoonya.com/OAuthlogin/authorize/oauth";
|
|
21035
21076
|
function generateTOTP(secret) {
|
|
21036
21077
|
const base32Chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ234567";
|
|
21037
21078
|
const decoded = [];
|
|
@@ -21056,7 +21097,7 @@ function generateTOTP(secret) {
|
|
|
21056
21097
|
return String(code % 1e6).padStart(6, "0");
|
|
21057
21098
|
}
|
|
21058
21099
|
async function authenticate2(creds) {
|
|
21059
|
-
const { userId, password, totpSecret, apiKey } = creds;
|
|
21100
|
+
const { userId, password, totpSecret, apiKey, vendorCode } = creds;
|
|
21060
21101
|
if (!userId)
|
|
21061
21102
|
return Err(new Error("Missing userId in credentials"));
|
|
21062
21103
|
if (!password)
|
|
@@ -21068,28 +21109,42 @@ async function authenticate2(creds) {
|
|
|
21068
21109
|
try {
|
|
21069
21110
|
const totp = generateTOTP(totpSecret);
|
|
21070
21111
|
const passwordHash = crypto2.createHash("sha256").update(password).digest("hex");
|
|
21071
|
-
const
|
|
21112
|
+
const vc = vendorCode || "NOREN_API";
|
|
21113
|
+
const appKey = crypto2.createHash("sha256").update(`${userId}|S3cur3!d`).digest("hex");
|
|
21072
21114
|
const jData = JSON.stringify({
|
|
21073
21115
|
source: "API",
|
|
21074
|
-
apkversion: "1
|
|
21116
|
+
apkversion: "1",
|
|
21075
21117
|
uid: userId,
|
|
21076
21118
|
pwd: passwordHash,
|
|
21077
21119
|
factor2: totp,
|
|
21078
|
-
vc
|
|
21120
|
+
vc,
|
|
21079
21121
|
appkey: appKey,
|
|
21080
21122
|
imei: "nsekit"
|
|
21081
21123
|
});
|
|
21082
|
-
const
|
|
21083
|
-
|
|
21084
|
-
|
|
21085
|
-
|
|
21086
|
-
|
|
21087
|
-
|
|
21088
|
-
|
|
21089
|
-
|
|
21090
|
-
|
|
21091
|
-
|
|
21092
|
-
|
|
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)}`));
|
|
21093
21148
|
}
|
|
21094
21149
|
if (json.stat !== "Ok" || !json.susertoken) {
|
|
21095
21150
|
return Err(new Error(`Shoonya login failed: ${json.emsg ?? "Unknown error"}`));
|
|
@@ -21111,6 +21166,87 @@ async function authenticate2(creds) {
|
|
|
21111
21166
|
async function refreshSession2(_session) {
|
|
21112
21167
|
return Err(new Error("Finvasia/Shoonya does not support session refresh. Re-authenticate with TOTP."));
|
|
21113
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
|
+
}
|
|
21114
21250
|
function isSessionValid2(session) {
|
|
21115
21251
|
if (!session)
|
|
21116
21252
|
return false;
|
|
@@ -21169,13 +21305,18 @@ function inferSide2(qty) {
|
|
|
21169
21305
|
}
|
|
21170
21306
|
function parseTimestamp2(ts) {
|
|
21171
21307
|
if (!ts)
|
|
21172
|
-
return
|
|
21308
|
+
return 0;
|
|
21173
21309
|
const asNum = Number(ts);
|
|
21174
21310
|
if (!isNaN(asNum) && asNum > 1000000000000)
|
|
21175
21311
|
return asNum;
|
|
21176
21312
|
if (!isNaN(asNum) && asNum > 1e9)
|
|
21177
21313
|
return asNum * 1000;
|
|
21178
|
-
|
|
21314
|
+
const shoonya = ts.match(/^(\d{2}:\d{2}:\d{2})\s+(\d{2})-(\d{2})-(\d{4})$/);
|
|
21315
|
+
if (shoonya) {
|
|
21316
|
+
const [, time, dd, mm, yyyy] = shoonya;
|
|
21317
|
+
return new Date(`${yyyy}-${mm}-${dd}T${time}`).getTime() || 0;
|
|
21318
|
+
}
|
|
21319
|
+
return new Date(ts).getTime() || 0;
|
|
21179
21320
|
}
|
|
21180
21321
|
function toOrderParams2(unified, uid, actid) {
|
|
21181
21322
|
const params = {
|
|
@@ -21203,17 +21344,21 @@ function fromPosition2(pos) {
|
|
|
21203
21344
|
const buyQty = num(pos.daybuyqty);
|
|
21204
21345
|
const sellQty = num(pos.daysellqty);
|
|
21205
21346
|
const realizedPnl = num(pos.rpnl);
|
|
21206
|
-
const
|
|
21347
|
+
const ltp = num(pos.lp);
|
|
21348
|
+
const entryPrice = num(pos.upldprc);
|
|
21349
|
+
const avgPrice = entryPrice > 0 ? entryPrice : num(pos.netavgprc);
|
|
21350
|
+
const unrealizedPnl = entryPrice > 0 ? (ltp - entryPrice) * netQty : num(pos.urmtom);
|
|
21351
|
+
const tradingSymbol = pos.tsym.replace(/-EQ$|-BE$|-BL$|-IL$|-IQ$/, "");
|
|
21207
21352
|
return {
|
|
21208
21353
|
broker: "finvasia",
|
|
21209
|
-
tradingSymbol
|
|
21354
|
+
tradingSymbol,
|
|
21210
21355
|
exchange: safeExchange2(pos.exch),
|
|
21211
21356
|
side: inferSide2(netQty),
|
|
21212
21357
|
quantity: Math.abs(netQty),
|
|
21213
21358
|
buyQty,
|
|
21214
21359
|
sellQty,
|
|
21215
|
-
avgPrice
|
|
21216
|
-
ltp
|
|
21360
|
+
avgPrice,
|
|
21361
|
+
ltp,
|
|
21217
21362
|
pnl: realizedPnl + unrealizedPnl,
|
|
21218
21363
|
realizedPnl,
|
|
21219
21364
|
unrealizedPnl,
|
|
@@ -21250,7 +21395,7 @@ function fromTick2(tick, symbolLookup) {
|
|
|
21250
21395
|
return {
|
|
21251
21396
|
broker: "finvasia",
|
|
21252
21397
|
token: tick.tk,
|
|
21253
|
-
tradingSymbol: symbolLookup ?? tick.tk,
|
|
21398
|
+
tradingSymbol: symbolLookup ?? tick.ts ?? tick.tk,
|
|
21254
21399
|
ltp: num(tick.lp),
|
|
21255
21400
|
open: num(tick.o),
|
|
21256
21401
|
high: num(tick.h),
|
|
@@ -21265,15 +21410,17 @@ function fromTick2(tick, symbolLookup) {
|
|
|
21265
21410
|
timestamp: parseTimestamp2(tick.ft)
|
|
21266
21411
|
};
|
|
21267
21412
|
}
|
|
21268
|
-
function fromHolding2(holding) {
|
|
21413
|
+
function fromHolding2(holding, ltp = 0) {
|
|
21269
21414
|
const first = holding.exch_tsym?.[0];
|
|
21270
21415
|
const qty = num(holding.holdqty) + num(holding.dpqty) + num(holding.npoadqty) + num(holding.btstqty) - num(holding.usedqty);
|
|
21271
21416
|
const avgPrice = num(holding.upldprc);
|
|
21272
|
-
|
|
21417
|
+
ltp = num(holding.lp) || num(holding.c) || ltp;
|
|
21273
21418
|
const pnl = (ltp - avgPrice) * qty;
|
|
21419
|
+
const rawSymbol = first?.tsym ?? "";
|
|
21420
|
+
const tradingSymbol = rawSymbol.replace(/-EQ$|-BE$|-BL$|-IL$|-IQ$/, "");
|
|
21274
21421
|
return {
|
|
21275
21422
|
broker: "finvasia",
|
|
21276
|
-
tradingSymbol
|
|
21423
|
+
tradingSymbol,
|
|
21277
21424
|
exchange: safeExchange2(first?.exch ?? "NSE"),
|
|
21278
21425
|
isin: holding.isin ?? "",
|
|
21279
21426
|
quantity: qty,
|
|
@@ -21327,26 +21474,37 @@ class FinvasiaSocket {
|
|
|
21327
21474
|
subscribedTokens = new Set;
|
|
21328
21475
|
userId;
|
|
21329
21476
|
accessToken;
|
|
21477
|
+
authMethod;
|
|
21330
21478
|
reconnectAttempts = 0;
|
|
21331
21479
|
maxReconnects = 5;
|
|
21332
21480
|
reconnectTimer = null;
|
|
21333
|
-
|
|
21481
|
+
onAuthFailed;
|
|
21482
|
+
constructor(userId, accessToken, onAuthFailed, authMethod = "totp") {
|
|
21334
21483
|
this.userId = userId;
|
|
21335
21484
|
this.accessToken = accessToken;
|
|
21485
|
+
this.authMethod = authMethod;
|
|
21486
|
+
this.onAuthFailed = onAuthFailed;
|
|
21336
21487
|
}
|
|
21337
21488
|
connect() {
|
|
21338
21489
|
if (this.state === "CONNECTED" || this.state === "CONNECTING")
|
|
21339
21490
|
return;
|
|
21340
21491
|
this.state = "CONNECTING";
|
|
21492
|
+
const wsUrl = this.authMethod === "oauth" ? SHOONYA_WS_OAUTH_URL : SHOONYA_WS_URL;
|
|
21341
21493
|
try {
|
|
21342
|
-
this.ws = new WS(
|
|
21494
|
+
this.ws = new WS(wsUrl);
|
|
21343
21495
|
} catch {
|
|
21344
21496
|
this.state = "DISCONNECTED";
|
|
21345
21497
|
return;
|
|
21346
21498
|
}
|
|
21347
21499
|
this.ws.on("open", () => {
|
|
21348
|
-
console.log(`[FinvasiaSocket] WS open, sending auth for uid=${this.userId}`);
|
|
21349
|
-
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({
|
|
21350
21508
|
t: "c",
|
|
21351
21509
|
uid: this.userId,
|
|
21352
21510
|
actid: this.userId,
|
|
@@ -21382,6 +21540,18 @@ class FinvasiaSocket {
|
|
|
21382
21540
|
this.state = "DISCONNECTED";
|
|
21383
21541
|
this.subscribedTokens.clear();
|
|
21384
21542
|
}
|
|
21543
|
+
reconnect(newToken) {
|
|
21544
|
+
this.clearReconnectTimer();
|
|
21545
|
+
if (newToken)
|
|
21546
|
+
this.accessToken = newToken;
|
|
21547
|
+
this.reconnectAttempts = 0;
|
|
21548
|
+
if (this.ws) {
|
|
21549
|
+
this.ws.close();
|
|
21550
|
+
this.ws = null;
|
|
21551
|
+
}
|
|
21552
|
+
this.state = "DISCONNECTED";
|
|
21553
|
+
this.connect();
|
|
21554
|
+
}
|
|
21385
21555
|
subscribe(tokens, callback, symbolMap) {
|
|
21386
21556
|
const sub = {
|
|
21387
21557
|
id: nextSubId3(),
|
|
@@ -21392,7 +21562,11 @@ class FinvasiaSocket {
|
|
|
21392
21562
|
for (const t of tokens) {
|
|
21393
21563
|
this.subscribedTokens.add(t);
|
|
21394
21564
|
if (symbolMap?.has(t)) {
|
|
21395
|
-
|
|
21565
|
+
const sym = symbolMap.get(t);
|
|
21566
|
+
this.tokenToSymbol.set(t, sym);
|
|
21567
|
+
const sep = t.indexOf("|");
|
|
21568
|
+
if (sep !== -1)
|
|
21569
|
+
this.tokenToSymbol.set(t.slice(sep + 1), sym);
|
|
21396
21570
|
}
|
|
21397
21571
|
}
|
|
21398
21572
|
if (this.ws && this.state === "CONNECTED") {
|
|
@@ -21425,17 +21599,32 @@ class FinvasiaSocket {
|
|
|
21425
21599
|
setSymbolMap(map) {
|
|
21426
21600
|
for (const [token, symbol] of map) {
|
|
21427
21601
|
this.tokenToSymbol.set(token, symbol);
|
|
21602
|
+
const sep = token.indexOf("|");
|
|
21603
|
+
if (sep !== -1) {
|
|
21604
|
+
this.tokenToSymbol.set(token.slice(sep + 1), symbol);
|
|
21605
|
+
}
|
|
21428
21606
|
}
|
|
21429
21607
|
}
|
|
21430
21608
|
handleMessage(data) {
|
|
21431
21609
|
const type = data["t"];
|
|
21432
|
-
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") {
|
|
21433
21617
|
if (data["s"] === "OK") {
|
|
21434
21618
|
this.state = "CONNECTED";
|
|
21435
21619
|
this.reconnectAttempts = 0;
|
|
21436
21620
|
if (this.subscribedTokens.size > 0) {
|
|
21437
21621
|
this.sendSubscribe(Array.from(this.subscribedTokens));
|
|
21438
21622
|
}
|
|
21623
|
+
} else {
|
|
21624
|
+
console.error(`[FinvasiaSocket] Auth rejected (${type} NOT_OK) \u2014 token invalid, stopping reconnect`);
|
|
21625
|
+
this.reconnectAttempts = this.maxReconnects;
|
|
21626
|
+
this.state = "DISCONNECTED";
|
|
21627
|
+
this.onAuthFailed?.();
|
|
21439
21628
|
}
|
|
21440
21629
|
return;
|
|
21441
21630
|
}
|
|
@@ -21444,6 +21633,10 @@ class FinvasiaSocket {
|
|
|
21444
21633
|
const rawToken = tick.tk;
|
|
21445
21634
|
const exch = data["e"] ?? "";
|
|
21446
21635
|
const fullKey = exch ? `${exch}|${rawToken}` : rawToken;
|
|
21636
|
+
if (tick.ts && !this.tokenToSymbol.has(fullKey)) {
|
|
21637
|
+
this.tokenToSymbol.set(fullKey, tick.ts);
|
|
21638
|
+
this.tokenToSymbol.set(rawToken, tick.ts);
|
|
21639
|
+
}
|
|
21447
21640
|
const symbol = this.tokenToSymbol.get(fullKey) ?? this.tokenToSymbol.get(rawToken);
|
|
21448
21641
|
const unified = fromTick2(tick, symbol);
|
|
21449
21642
|
unified.token = fullKey;
|
|
@@ -21497,6 +21690,27 @@ class FinvasiaSocket {
|
|
|
21497
21690
|
}
|
|
21498
21691
|
|
|
21499
21692
|
// src/brokers/finvasia/finvasia-instruments.ts
|
|
21693
|
+
import { inflateRawSync } from "zlib";
|
|
21694
|
+
function extractTextFromZip(buffer) {
|
|
21695
|
+
const bytes = new Uint8Array(buffer);
|
|
21696
|
+
if (bytes.length < 30 || bytes[0] !== 80 || bytes[1] !== 75 || bytes[2] !== 3 || bytes[3] !== 4) {
|
|
21697
|
+
return new TextDecoder().decode(buffer);
|
|
21698
|
+
}
|
|
21699
|
+
const view = new DataView(buffer);
|
|
21700
|
+
const compressionMethod = view.getUint16(8, true);
|
|
21701
|
+
const compressedSize = view.getUint32(18, true);
|
|
21702
|
+
const filenameLength = view.getUint16(26, true);
|
|
21703
|
+
const extraFieldLength = view.getUint16(28, true);
|
|
21704
|
+
const dataOffset = 30 + filenameLength + extraFieldLength;
|
|
21705
|
+
const compressedData = new Uint8Array(buffer, dataOffset, compressedSize);
|
|
21706
|
+
if (compressionMethod === 0) {
|
|
21707
|
+
return new TextDecoder().decode(compressedData);
|
|
21708
|
+
}
|
|
21709
|
+
if (compressionMethod === 8) {
|
|
21710
|
+
return new TextDecoder().decode(inflateRawSync(compressedData));
|
|
21711
|
+
}
|
|
21712
|
+
throw new Error(`Unsupported ZIP compression method: ${compressionMethod}`);
|
|
21713
|
+
}
|
|
21500
21714
|
var COL2 = {
|
|
21501
21715
|
EXCHANGE: 0,
|
|
21502
21716
|
TOKEN: 1,
|
|
@@ -21509,6 +21723,18 @@ var COL2 = {
|
|
|
21509
21723
|
STRIKE: 8,
|
|
21510
21724
|
OPTION_TYPE: 9
|
|
21511
21725
|
};
|
|
21726
|
+
var HEADER_MAP = {
|
|
21727
|
+
Exchange: "exchange",
|
|
21728
|
+
Token: "token",
|
|
21729
|
+
LotSize: "lot_size",
|
|
21730
|
+
Symbol: "symbol",
|
|
21731
|
+
TradingSymbol: "trading_symbol",
|
|
21732
|
+
Expiry: "expiry",
|
|
21733
|
+
Instrument: "instrument_type",
|
|
21734
|
+
OptionType: "option_type",
|
|
21735
|
+
StrikePrice: "strike",
|
|
21736
|
+
TickSize: "tick_size"
|
|
21737
|
+
};
|
|
21512
21738
|
var EXCHANGE_MAP2 = {
|
|
21513
21739
|
NSE: "NSE",
|
|
21514
21740
|
BSE: "BSE",
|
|
@@ -21524,7 +21750,9 @@ function mapInstrumentType2(instType, optionType) {
|
|
|
21524
21750
|
if (upper === "PE")
|
|
21525
21751
|
return "PE";
|
|
21526
21752
|
const it = instType?.toUpperCase();
|
|
21527
|
-
if (it === "
|
|
21753
|
+
if (it === "INDEX")
|
|
21754
|
+
return "IDX";
|
|
21755
|
+
if (it === "EQ" || it === "EQUITY")
|
|
21528
21756
|
return "EQ";
|
|
21529
21757
|
if (it?.startsWith("FUT") || it?.includes("FUT"))
|
|
21530
21758
|
return "FUT";
|
|
@@ -21542,10 +21770,23 @@ function parseStrike2(raw) {
|
|
|
21542
21770
|
const n = parseFloat(raw);
|
|
21543
21771
|
return isNaN(n) || n === 0 ? undefined : n;
|
|
21544
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
|
+
};
|
|
21545
21785
|
function deriveUnderlying2(symbol, tradingSymbol, instType) {
|
|
21546
21786
|
if (instType === "EQ")
|
|
21547
21787
|
return symbol || tradingSymbol;
|
|
21548
|
-
|
|
21788
|
+
const raw = symbol || tradingSymbol.replace(/\d.*/g, "");
|
|
21789
|
+
return UNDERLYING_ALIASES[raw] ?? raw;
|
|
21549
21790
|
}
|
|
21550
21791
|
|
|
21551
21792
|
class FinvasiaInstruments {
|
|
@@ -21568,7 +21809,7 @@ class FinvasiaInstruments {
|
|
|
21568
21809
|
const response = await fetch(url);
|
|
21569
21810
|
if (!response.ok)
|
|
21570
21811
|
continue;
|
|
21571
|
-
const text = await response.
|
|
21812
|
+
const text = extractTextFromZip(await response.arrayBuffer());
|
|
21572
21813
|
const lines = text.split(`
|
|
21573
21814
|
`);
|
|
21574
21815
|
let headers = null;
|
|
@@ -21576,10 +21817,10 @@ class FinvasiaInstruments {
|
|
|
21576
21817
|
const trimmed = line.trim();
|
|
21577
21818
|
if (!trimmed)
|
|
21578
21819
|
continue;
|
|
21579
|
-
const fields = trimmed.split("
|
|
21820
|
+
const fields = trimmed.split(",");
|
|
21580
21821
|
if (!headers) {
|
|
21581
21822
|
if (fields[0] === "Exchange" || fields[0] === "exch") {
|
|
21582
|
-
headers = fields;
|
|
21823
|
+
headers = fields.map((h) => HEADER_MAP[h] ?? h.toLowerCase());
|
|
21583
21824
|
continue;
|
|
21584
21825
|
}
|
|
21585
21826
|
headers = [];
|
|
@@ -21695,6 +21936,7 @@ class FinvasiaBroker {
|
|
|
21695
21936
|
};
|
|
21696
21937
|
session = null;
|
|
21697
21938
|
socket = null;
|
|
21939
|
+
credentials = null;
|
|
21698
21940
|
instrumentsImpl;
|
|
21699
21941
|
_iMaster;
|
|
21700
21942
|
constructor(options) {
|
|
@@ -21703,13 +21945,36 @@ class FinvasiaBroker {
|
|
|
21703
21945
|
this._iMaster = new InstrumentMaster(options);
|
|
21704
21946
|
}
|
|
21705
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
|
+
}
|
|
21706
21967
|
const result = await authenticate2(creds);
|
|
21707
21968
|
if (!result.ok)
|
|
21708
21969
|
return result;
|
|
21970
|
+
this.credentials = creds;
|
|
21709
21971
|
this.session = result.value;
|
|
21710
21972
|
this.instrumentsImpl.setCredentials(this.session.userId, this.session.accessToken);
|
|
21711
21973
|
return result;
|
|
21712
21974
|
}
|
|
21975
|
+
getOAuthURL(clientId) {
|
|
21976
|
+
return getOAuthURL(clientId);
|
|
21977
|
+
}
|
|
21713
21978
|
async refreshSession(session) {
|
|
21714
21979
|
return refreshSession2(session);
|
|
21715
21980
|
}
|
|
@@ -21721,6 +21986,19 @@ class FinvasiaBroker {
|
|
|
21721
21986
|
return null;
|
|
21722
21987
|
return new Date(this.session.expiresAt);
|
|
21723
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
|
+
}
|
|
21724
22002
|
async sync(force) {
|
|
21725
22003
|
return this._iMaster.syncBroker(this.id, this.instruments, force);
|
|
21726
22004
|
}
|
|
@@ -21826,6 +22104,10 @@ class FinvasiaBroker {
|
|
|
21826
22104
|
const uid = this.session?.userId ?? "";
|
|
21827
22105
|
const raw = await this.post("OrderBook", { uid });
|
|
21828
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
|
+
}
|
|
21829
22111
|
return Ok([]);
|
|
21830
22112
|
}
|
|
21831
22113
|
return Ok(raw.map(fromOrder2));
|
|
@@ -21858,8 +22140,13 @@ class FinvasiaBroker {
|
|
|
21858
22140
|
try {
|
|
21859
22141
|
const uid = this.session?.userId ?? "";
|
|
21860
22142
|
const raw = await this.post("PositionBook", { uid, actid: uid });
|
|
21861
|
-
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
|
+
}
|
|
21862
22148
|
return Ok([]);
|
|
22149
|
+
}
|
|
21863
22150
|
return Ok(raw.map(fromPosition2));
|
|
21864
22151
|
} catch (err) {
|
|
21865
22152
|
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -21873,8 +22160,13 @@ class FinvasiaBroker {
|
|
|
21873
22160
|
actid: uid,
|
|
21874
22161
|
prd: "C"
|
|
21875
22162
|
});
|
|
21876
|
-
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
|
+
}
|
|
21877
22168
|
return Ok([]);
|
|
22169
|
+
}
|
|
21878
22170
|
return Ok(raw.map(fromHolding2));
|
|
21879
22171
|
} catch (err) {
|
|
21880
22172
|
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
@@ -21998,10 +22290,30 @@ class FinvasiaBroker {
|
|
|
21998
22290
|
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
21999
22291
|
}
|
|
22000
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
|
+
}
|
|
22001
22309
|
subscribeTicks(tokens, cb) {
|
|
22002
22310
|
this.ensureSocket();
|
|
22003
22311
|
return this.socket.subscribe(tokens, cb);
|
|
22004
22312
|
}
|
|
22313
|
+
setSymbolMap(map) {
|
|
22314
|
+
this.ensureSocket();
|
|
22315
|
+
this.socket.setSymbolMap(map);
|
|
22316
|
+
}
|
|
22005
22317
|
unsubscribeTicks(sub) {
|
|
22006
22318
|
if (this.socket) {
|
|
22007
22319
|
this.socket.unsubscribe(sub);
|
|
@@ -22018,18 +22330,25 @@ class FinvasiaBroker {
|
|
|
22018
22330
|
actid: uid
|
|
22019
22331
|
});
|
|
22020
22332
|
if (raw.stat !== "Ok") {
|
|
22021
|
-
return Err(new Error(
|
|
22333
|
+
return Err(new Error(`Margins failed: ${raw.emsg ?? "Unknown error"}`));
|
|
22022
22334
|
}
|
|
22023
22335
|
const cash = parseFloat(raw["cash"] ?? "0");
|
|
22024
|
-
const marginUsed = parseFloat(raw["marginused"] ?? "0");
|
|
22336
|
+
const marginUsed = parseFloat(raw["marginused"] ?? raw["peak_mar"] ?? "0");
|
|
22337
|
+
const blockedAmt = parseFloat(raw["blk_amt"] ?? "0");
|
|
22025
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;
|
|
22026
22344
|
return Ok({
|
|
22027
|
-
available
|
|
22028
|
-
used
|
|
22029
|
-
total
|
|
22345
|
+
available,
|
|
22346
|
+
used,
|
|
22347
|
+
total,
|
|
22030
22348
|
collateral,
|
|
22031
22349
|
segments: {
|
|
22032
|
-
equity: { available:
|
|
22350
|
+
equity: { available: eqAvailable, used: 0 },
|
|
22351
|
+
commodity: { available: comAvailable, used: 0 }
|
|
22033
22352
|
}
|
|
22034
22353
|
});
|
|
22035
22354
|
} catch (err) {
|
|
@@ -22041,9 +22360,11 @@ class FinvasiaBroker {
|
|
|
22041
22360
|
const margins = await this.getMargins();
|
|
22042
22361
|
if (!margins.ok)
|
|
22043
22362
|
return margins;
|
|
22363
|
+
const eq = margins.value.segments["equity"]?.available ?? 0;
|
|
22364
|
+
const com = margins.value.segments["commodity"]?.available ?? 0;
|
|
22044
22365
|
return Ok({
|
|
22045
|
-
equity:
|
|
22046
|
-
commodity:
|
|
22366
|
+
equity: eq,
|
|
22367
|
+
commodity: com,
|
|
22047
22368
|
total: margins.value.available
|
|
22048
22369
|
});
|
|
22049
22370
|
} catch (err) {
|
|
@@ -22090,10 +22411,18 @@ class FinvasiaBroker {
|
|
|
22090
22411
|
if (!this.session)
|
|
22091
22412
|
throw new Error("Not authenticated");
|
|
22092
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}`;
|
|
22093
22422
|
const response = await fetch(`${SHOONYA_API_BASE}/${endpoint}`, {
|
|
22094
22423
|
method: "POST",
|
|
22095
|
-
headers
|
|
22096
|
-
body
|
|
22424
|
+
headers,
|
|
22425
|
+
body
|
|
22097
22426
|
});
|
|
22098
22427
|
const text = await response.text();
|
|
22099
22428
|
let parsed;
|
|
@@ -22102,16 +22431,33 @@ class FinvasiaBroker {
|
|
|
22102
22431
|
} catch {
|
|
22103
22432
|
throw new Error(`Shoonya ${endpoint} failed (${response.status}): ${text.slice(0, 200)}`);
|
|
22104
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
|
+
}
|
|
22105
22441
|
return parsed;
|
|
22106
22442
|
}
|
|
22107
22443
|
ensureSocket() {
|
|
22108
|
-
if (this.socket)
|
|
22109
|
-
return;
|
|
22110
22444
|
if (!this.session)
|
|
22111
22445
|
throw new Error("Cannot create socket without an active session");
|
|
22112
|
-
|
|
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";
|
|
22453
|
+
this.socket = new FinvasiaSocket(this.session.userId, this.session.accessToken, () => {
|
|
22454
|
+
this.handleSocketAuthFailed();
|
|
22455
|
+
}, authMethod);
|
|
22113
22456
|
this.socket.connect();
|
|
22114
22457
|
}
|
|
22458
|
+
handleSocketAuthFailed() {
|
|
22459
|
+
console.warn("[FinvasiaBroker] Socket auth rejected (ck NOT_OK) \u2014 waiting for user-bot to re-authenticate");
|
|
22460
|
+
}
|
|
22115
22461
|
}
|
|
22116
22462
|
// src/brokers/dhan/dhan-constants.ts
|
|
22117
22463
|
var EXCHANGE_TO_DHAN = {
|
|
@@ -22181,6 +22527,7 @@ var VALIDITY_FROM_DHAN = {
|
|
|
22181
22527
|
IOC: "IOC"
|
|
22182
22528
|
};
|
|
22183
22529
|
var DHAN_API_BASE = "https://api.dhan.co/v2";
|
|
22530
|
+
var DHAN_AUTH_BASE = "https://auth.dhan.co";
|
|
22184
22531
|
var DHAN_WS_URL = "wss://api-feed.dhan.co";
|
|
22185
22532
|
var DHAN_INSTRUMENTS_URL = "https://images.dhan.co/api-data/api-scrip-master.csv";
|
|
22186
22533
|
var CANDLE_INTERVAL_TO_DHAN = {
|
|
@@ -22211,12 +22558,12 @@ async function authenticate3(creds) {
|
|
|
22211
22558
|
return Err(new Error(`Dhan auth validation failed (${response.status}): ${body}`));
|
|
22212
22559
|
}
|
|
22213
22560
|
const profile = await response.json();
|
|
22214
|
-
const
|
|
22561
|
+
const twentyFourHoursMs = 24 * 60 * 60 * 1000;
|
|
22215
22562
|
const session = {
|
|
22216
22563
|
accessToken,
|
|
22217
22564
|
refreshToken: undefined,
|
|
22218
|
-
expiresAt: Date.now() +
|
|
22219
|
-
ttlSeconds:
|
|
22565
|
+
expiresAt: Date.now() + twentyFourHoursMs,
|
|
22566
|
+
ttlSeconds: 24 * 60 * 60,
|
|
22220
22567
|
brokerId: "dhan",
|
|
22221
22568
|
userId: profile.clientId ?? clientId,
|
|
22222
22569
|
metadata: { name: profile.name, email: profile.email }
|
|
@@ -22226,8 +22573,116 @@ async function authenticate3(creds) {
|
|
|
22226
22573
|
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
22227
22574
|
}
|
|
22228
22575
|
}
|
|
22229
|
-
async function
|
|
22230
|
-
|
|
22576
|
+
async function generateConsent(creds) {
|
|
22577
|
+
const { apiKey: appId, apiSecret: appSecret, userId: clientId } = creds;
|
|
22578
|
+
if (!appId)
|
|
22579
|
+
return Err(new Error("Missing apiKey (App ID) in credentials"));
|
|
22580
|
+
if (!appSecret)
|
|
22581
|
+
return Err(new Error("Missing apiSecret (App Secret) in credentials"));
|
|
22582
|
+
if (!clientId)
|
|
22583
|
+
return Err(new Error("Missing userId (Dhan Client ID) in credentials"));
|
|
22584
|
+
try {
|
|
22585
|
+
const response = await fetch(`${DHAN_AUTH_BASE}/app/generate-consent?client_id=${clientId}`, {
|
|
22586
|
+
method: "POST",
|
|
22587
|
+
headers: {
|
|
22588
|
+
"Content-Type": "application/json",
|
|
22589
|
+
app_id: appId,
|
|
22590
|
+
app_secret: appSecret
|
|
22591
|
+
},
|
|
22592
|
+
body: JSON.stringify({})
|
|
22593
|
+
});
|
|
22594
|
+
if (!response.ok) {
|
|
22595
|
+
const body = await response.text();
|
|
22596
|
+
return Err(new Error(`Dhan generate-consent failed (${response.status}): ${body}`));
|
|
22597
|
+
}
|
|
22598
|
+
const data = await response.json();
|
|
22599
|
+
if (!data.consentAppId) {
|
|
22600
|
+
return Err(new Error(data.message ?? "Failed to generate consent \u2014 no consentAppId returned"));
|
|
22601
|
+
}
|
|
22602
|
+
const loginUrl = `${DHAN_AUTH_BASE}/login/consentApp-login?consentAppId=${data.consentAppId}`;
|
|
22603
|
+
return Ok({ consentAppId: data.consentAppId, loginUrl });
|
|
22604
|
+
} catch (err) {
|
|
22605
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
22606
|
+
}
|
|
22607
|
+
}
|
|
22608
|
+
async function consumeConsent(creds, tokenId) {
|
|
22609
|
+
const { apiKey: appId, apiSecret: appSecret, userId: clientId } = creds;
|
|
22610
|
+
if (!appId)
|
|
22611
|
+
return Err(new Error("Missing apiKey (App ID) in credentials"));
|
|
22612
|
+
if (!appSecret)
|
|
22613
|
+
return Err(new Error("Missing apiSecret (App Secret) in credentials"));
|
|
22614
|
+
if (!tokenId)
|
|
22615
|
+
return Err(new Error("Missing tokenId from callback"));
|
|
22616
|
+
try {
|
|
22617
|
+
const response = await fetch(`${DHAN_AUTH_BASE}/app/consumeApp-consent?tokenId=${encodeURIComponent(tokenId)}`, {
|
|
22618
|
+
method: "POST",
|
|
22619
|
+
headers: {
|
|
22620
|
+
"Content-Type": "application/json",
|
|
22621
|
+
app_id: appId,
|
|
22622
|
+
app_secret: appSecret
|
|
22623
|
+
},
|
|
22624
|
+
body: JSON.stringify({})
|
|
22625
|
+
});
|
|
22626
|
+
if (!response.ok) {
|
|
22627
|
+
const body = await response.text();
|
|
22628
|
+
return Err(new Error(`Dhan consumeApp-consent failed (${response.status}): ${body}`));
|
|
22629
|
+
}
|
|
22630
|
+
const data = await response.json();
|
|
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"));
|
|
22634
|
+
}
|
|
22635
|
+
const twentyFourHoursMs = 24 * 60 * 60 * 1000;
|
|
22636
|
+
const session = {
|
|
22637
|
+
accessToken: newToken,
|
|
22638
|
+
refreshToken: undefined,
|
|
22639
|
+
expiresAt: Date.now() + twentyFourHoursMs,
|
|
22640
|
+
ttlSeconds: 24 * 60 * 60,
|
|
22641
|
+
brokerId: "dhan",
|
|
22642
|
+
userId: clientId ?? "",
|
|
22643
|
+
metadata: { authMode: "oauth" }
|
|
22644
|
+
};
|
|
22645
|
+
return Ok(session);
|
|
22646
|
+
} catch (err) {
|
|
22647
|
+
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
22648
|
+
}
|
|
22649
|
+
}
|
|
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
|
+
}
|
|
22231
22686
|
}
|
|
22232
22687
|
function isSessionValid3(session) {
|
|
22233
22688
|
if (!session)
|
|
@@ -22256,10 +22711,10 @@ function safeSide3(raw) {
|
|
|
22256
22711
|
}
|
|
22257
22712
|
function parseTimestamp3(ts) {
|
|
22258
22713
|
if (!ts)
|
|
22259
|
-
return
|
|
22714
|
+
return 0;
|
|
22260
22715
|
if (typeof ts === "number")
|
|
22261
22716
|
return ts > 1000000000000 ? ts : ts * 1000;
|
|
22262
|
-
return new Date(ts).getTime() ||
|
|
22717
|
+
return new Date(ts).getTime() || 0;
|
|
22263
22718
|
}
|
|
22264
22719
|
function inferInstrumentType3(exchangeSegment, tradingSymbol) {
|
|
22265
22720
|
if (exchangeSegment === "NSE_EQ" || exchangeSegment === "BSE_EQ")
|
|
@@ -22368,6 +22823,7 @@ function fromTick3(tick, symbolLookup) {
|
|
|
22368
22823
|
function fromHolding3(holding, ltp = 0) {
|
|
22369
22824
|
const qty = holding.totalQty ?? holding.availableQty;
|
|
22370
22825
|
const avgPrice = holding.avgCostPrice;
|
|
22826
|
+
ltp = holding.lastTradedPrice ?? ltp;
|
|
22371
22827
|
const pnl = (ltp - avgPrice) * qty;
|
|
22372
22828
|
return {
|
|
22373
22829
|
broker: "dhan",
|
|
@@ -22416,13 +22872,14 @@ function nextSubId4() {
|
|
|
22416
22872
|
return `dsub_${subIdCounter4}`;
|
|
22417
22873
|
}
|
|
22418
22874
|
var EXCH_SEG_FROM_BYTE = {
|
|
22419
|
-
0: "
|
|
22875
|
+
0: "IDX_I",
|
|
22420
22876
|
1: "NSE_EQ",
|
|
22421
22877
|
2: "NSE_FNO",
|
|
22422
22878
|
3: "NSE_CURRENCY",
|
|
22423
22879
|
4: "BSE_EQ",
|
|
22424
|
-
5: "
|
|
22425
|
-
|
|
22880
|
+
5: "MCX_COMM",
|
|
22881
|
+
7: "BSE_CURRENCY",
|
|
22882
|
+
8: "BSE_FNO"
|
|
22426
22883
|
};
|
|
22427
22884
|
function splitToken(token) {
|
|
22428
22885
|
const sep = token.includes("|") ? "|" : ":";
|
|
@@ -22460,6 +22917,7 @@ class DhanSocket {
|
|
|
22460
22917
|
return;
|
|
22461
22918
|
}
|
|
22462
22919
|
this.ws.on("open", () => {
|
|
22920
|
+
console.log(`[DhanSocket] WS connected for clientId=${this.clientId}`);
|
|
22463
22921
|
this.state = "CONNECTED";
|
|
22464
22922
|
this.reconnectAttempts = 0;
|
|
22465
22923
|
this.startHeartbeat();
|
|
@@ -22469,6 +22927,11 @@ class DhanSocket {
|
|
|
22469
22927
|
});
|
|
22470
22928
|
this.ws.on("message", (raw, isBinary) => {
|
|
22471
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
|
+
}
|
|
22472
22935
|
if (isBinary) {
|
|
22473
22936
|
const buf = raw instanceof ArrayBuffer ? Buffer.from(raw) : Buffer.isBuffer(raw) ? raw : Array.isArray(raw) ? Buffer.concat(raw) : Buffer.from(raw);
|
|
22474
22937
|
this.handleBinaryMessage(buf);
|
|
@@ -22478,12 +22941,20 @@ class DhanSocket {
|
|
|
22478
22941
|
}
|
|
22479
22942
|
} catch {}
|
|
22480
22943
|
});
|
|
22481
|
-
this.ws.on("close", () => {
|
|
22944
|
+
this.ws.on("close", (code, reason) => {
|
|
22945
|
+
console.warn(`[DhanSocket] WS closed: code=${code}, reason=${reason.toString()}`);
|
|
22482
22946
|
this.state = "DISCONNECTED";
|
|
22483
22947
|
this.stopHeartbeat();
|
|
22484
22948
|
this.attemptReconnect();
|
|
22485
22949
|
});
|
|
22486
|
-
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
|
+
});
|
|
22487
22958
|
}
|
|
22488
22959
|
disconnect() {
|
|
22489
22960
|
this.clearReconnectTimer();
|
|
@@ -22549,7 +23020,33 @@ class DhanSocket {
|
|
|
22549
23020
|
const securityId = data.readInt32LE(4);
|
|
22550
23021
|
const exchangeSegment = EXCH_SEG_FROM_BYTE[exchSegByte] ?? "NSE_EQ";
|
|
22551
23022
|
const fullKey = `${exchangeSegment}|${securityId}`;
|
|
22552
|
-
if (responseCode ===
|
|
23023
|
+
if (responseCode === 1 && data.length >= 32) {
|
|
23024
|
+
const ltp = data.readFloatLE(8);
|
|
23025
|
+
const ltt = data.readInt32LE(12);
|
|
23026
|
+
const open = data.readFloatLE(16);
|
|
23027
|
+
const close = data.readFloatLE(20);
|
|
23028
|
+
const high = data.readFloatLE(24);
|
|
23029
|
+
const low = data.readFloatLE(28);
|
|
23030
|
+
const tick = {
|
|
23031
|
+
securityId,
|
|
23032
|
+
LTP: ltp,
|
|
23033
|
+
open,
|
|
23034
|
+
high,
|
|
23035
|
+
low,
|
|
23036
|
+
close,
|
|
23037
|
+
volume: 0,
|
|
23038
|
+
bestBidPrice: 0,
|
|
23039
|
+
bestAskPrice: 0,
|
|
23040
|
+
bestBidQty: 0,
|
|
23041
|
+
bestAskQty: 0,
|
|
23042
|
+
exchangeSegment,
|
|
23043
|
+
timestamp: ltt > 0 ? ltt * 1000 : Date.now()
|
|
23044
|
+
};
|
|
23045
|
+
const symbol = this.tokenToSymbol.get(fullKey) ?? this.tokenToSymbol.get(String(securityId));
|
|
23046
|
+
const unified = fromTick3(tick, symbol);
|
|
23047
|
+
unified.token = fullKey;
|
|
23048
|
+
this.dispatchTick(unified);
|
|
23049
|
+
} else if (responseCode === 2 && data.length >= 16) {
|
|
22553
23050
|
const ltp = data.readFloatLE(8);
|
|
22554
23051
|
const ltt = data.readInt32LE(12);
|
|
22555
23052
|
const tick = {
|
|
@@ -22605,7 +23102,7 @@ class DhanSocket {
|
|
|
22605
23102
|
handleJsonMessage(data) {
|
|
22606
23103
|
if (data["type"] === "ticker_data") {
|
|
22607
23104
|
const tick = data;
|
|
22608
|
-
const fullKey = tick.exchangeSegment ? `${tick.exchangeSegment}
|
|
23105
|
+
const fullKey = tick.exchangeSegment ? `${tick.exchangeSegment}:${tick.securityId}` : String(tick.securityId);
|
|
22609
23106
|
const symbol = this.tokenToSymbol.get(fullKey) ?? this.tokenToSymbol.get(String(tick.securityId));
|
|
22610
23107
|
const unified = fromTick3(tick, symbol);
|
|
22611
23108
|
unified.token = fullKey;
|
|
@@ -22627,7 +23124,9 @@ class DhanSocket {
|
|
|
22627
23124
|
InstrumentCount: instruments.length,
|
|
22628
23125
|
InstrumentList: instruments
|
|
22629
23126
|
};
|
|
22630
|
-
|
|
23127
|
+
const msg = JSON.stringify(request);
|
|
23128
|
+
console.log(`[DhanSocket] Sending subscribe: ${msg}`);
|
|
23129
|
+
this.ws.send(msg);
|
|
22631
23130
|
}
|
|
22632
23131
|
sendUnsubscribe(tokens) {
|
|
22633
23132
|
if (!this.ws || this.state !== "CONNECTED")
|
|
@@ -22702,6 +23201,15 @@ var DHAN_EXCHANGE_MAP = {
|
|
|
22702
23201
|
NFO: "NFO",
|
|
22703
23202
|
MCX: "MCX"
|
|
22704
23203
|
};
|
|
23204
|
+
function resolveDhanExchange(exchId, segment) {
|
|
23205
|
+
if (segment === "D")
|
|
23206
|
+
return exchId === "BSE" ? "BFO" : "NFO";
|
|
23207
|
+
if (segment === "C")
|
|
23208
|
+
return "CDS";
|
|
23209
|
+
if (segment === "M")
|
|
23210
|
+
return "MCX";
|
|
23211
|
+
return exchId === "BSE" ? "BSE" : "NSE";
|
|
23212
|
+
}
|
|
22705
23213
|
function mapInstrumentType3(instName, optionType) {
|
|
22706
23214
|
const opt = optionType?.toUpperCase();
|
|
22707
23215
|
if (opt === "CE")
|
|
@@ -22709,7 +23217,9 @@ function mapInstrumentType3(instName, optionType) {
|
|
|
22709
23217
|
if (opt === "PE")
|
|
22710
23218
|
return "PE";
|
|
22711
23219
|
const inst = instName?.toUpperCase();
|
|
22712
|
-
if (inst
|
|
23220
|
+
if (inst === "INDEX")
|
|
23221
|
+
return "IDX";
|
|
23222
|
+
if (inst?.includes("EQUITY") || inst === "EQ")
|
|
22713
23223
|
return "EQ";
|
|
22714
23224
|
if (inst?.includes("FUT"))
|
|
22715
23225
|
return "FUT";
|
|
@@ -22843,14 +23353,16 @@ class DhanInstruments {
|
|
|
22843
23353
|
const securityId = fields["SEM_SMST_SECURITY_ID"] ?? fields["securityId"] ?? "";
|
|
22844
23354
|
const tradingSymbol = fields["SEM_TRADING_SYMBOL"] ?? fields["tradingSymbol"] ?? "";
|
|
22845
23355
|
const customSymbol = fields["SEM_CUSTOM_SYMBOL"] ?? fields["customSymbol"] ?? tradingSymbol;
|
|
22846
|
-
const
|
|
23356
|
+
const exchId = fields["SEM_EXM_EXCH_ID"] ?? "";
|
|
23357
|
+
const segment = fields["SEM_SEGMENT"] ?? "";
|
|
23358
|
+
const exchangeSegment = fields["exchangeSegment"] ?? "";
|
|
22847
23359
|
const instNameRaw = fields["SEM_INSTRUMENT_NAME"] ?? fields["instrumentType"] ?? "EQUITY";
|
|
22848
23360
|
const optionType = fields["SEM_OPTION_TYPE"] ?? fields["optionType"] ?? "";
|
|
22849
23361
|
const lotSizeRaw = fields["SEM_LOT_UNITS"] ?? fields["lotSize"] ?? "1";
|
|
22850
23362
|
const tickSizeRaw = fields["SEM_TICK_SIZE"] ?? fields["tickSize"] ?? "0.05";
|
|
22851
23363
|
const expiryRaw = fields["SEM_EXPIRY_DATE"] ?? fields["expiryDate"] ?? "";
|
|
22852
23364
|
const strikeRaw = fields["SEM_STRIKE_PRICE"] ?? fields["strikePrice"] ?? "";
|
|
22853
|
-
const exchange = DHAN_EXCHANGE_MAP[
|
|
23365
|
+
const exchange = exchId && segment ? resolveDhanExchange(exchId, segment) : DHAN_EXCHANGE_MAP[exchangeSegment] ?? EXCHANGE_FROM_DHAN[exchangeSegment] ?? "NSE";
|
|
22854
23366
|
const instrumentType = mapInstrumentType3(instNameRaw, optionType);
|
|
22855
23367
|
return {
|
|
22856
23368
|
tradingSymbol,
|
|
@@ -22907,6 +23419,7 @@ class DhanBroker {
|
|
|
22907
23419
|
session = null;
|
|
22908
23420
|
socket = null;
|
|
22909
23421
|
clientId = "";
|
|
23422
|
+
oauthCreds = null;
|
|
22910
23423
|
instrumentsImpl;
|
|
22911
23424
|
_iMaster;
|
|
22912
23425
|
constructor(options) {
|
|
@@ -22924,7 +23437,14 @@ class DhanBroker {
|
|
|
22924
23437
|
return result;
|
|
22925
23438
|
}
|
|
22926
23439
|
async refreshSession(session) {
|
|
22927
|
-
|
|
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;
|
|
22928
23448
|
}
|
|
22929
23449
|
isSessionValid() {
|
|
22930
23450
|
return isSessionValid3(this.session);
|
|
@@ -22934,6 +23454,27 @@ class DhanBroker {
|
|
|
22934
23454
|
return null;
|
|
22935
23455
|
return new Date(this.session.expiresAt);
|
|
22936
23456
|
}
|
|
23457
|
+
getSession() {
|
|
23458
|
+
return this.session;
|
|
23459
|
+
}
|
|
23460
|
+
async generateConsent(creds) {
|
|
23461
|
+
this.oauthCreds = creds;
|
|
23462
|
+
this.clientId = creds.userId ?? "";
|
|
23463
|
+
return generateConsent(creds);
|
|
23464
|
+
}
|
|
23465
|
+
async consumeConsent(tokenId) {
|
|
23466
|
+
if (!this.oauthCreds) {
|
|
23467
|
+
return Err(new Error("Call generateConsent() before consumeConsent()"));
|
|
23468
|
+
}
|
|
23469
|
+
const result = await consumeConsent(this.oauthCreds, tokenId);
|
|
23470
|
+
if (!result.ok)
|
|
23471
|
+
return result;
|
|
23472
|
+
this.session = result.value;
|
|
23473
|
+
this.clientId = this.oauthCreds.userId ?? "";
|
|
23474
|
+
this.instrumentsImpl.setCredentials(this.clientId, this.session.accessToken);
|
|
23475
|
+
this.oauthCreds = null;
|
|
23476
|
+
return result;
|
|
23477
|
+
}
|
|
22937
23478
|
async sync(force) {
|
|
22938
23479
|
return this._iMaster.syncBroker(this.id, this.instruments, force);
|
|
22939
23480
|
}
|
|
@@ -22953,9 +23494,11 @@ class DhanBroker {
|
|
|
22953
23494
|
if (!asyncResolved.ok) {
|
|
22954
23495
|
return Err(new Error(`Cannot resolve "${params.tradingSymbol}". Either run broker.sync() first to use unified nsekit symbols, or provide exchange and the broker-native tradingSymbol directly.`));
|
|
22955
23496
|
}
|
|
22956
|
-
|
|
23497
|
+
const nativeId = asyncResolved.value.brokerTokens.dhan?.securityId ?? params.tradingSymbol;
|
|
23498
|
+
resolvedParams = { ...params, exchange: asyncResolved.value.exchange, tradingSymbol: nativeId };
|
|
22957
23499
|
} else {
|
|
22958
|
-
|
|
23500
|
+
const nativeId = resolved.value.brokerTokens.dhan?.securityId ?? params.tradingSymbol;
|
|
23501
|
+
resolvedParams = { ...params, exchange: resolved.value.exchange, tradingSymbol: nativeId };
|
|
22959
23502
|
}
|
|
22960
23503
|
}
|
|
22961
23504
|
const dhanParams = toOrderParams3(resolvedParams, this.clientId);
|
|
@@ -23192,10 +23735,31 @@ class DhanBroker {
|
|
|
23192
23735
|
return Err(err instanceof Error ? err : new Error(String(err)));
|
|
23193
23736
|
}
|
|
23194
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
|
+
}
|
|
23195
23755
|
subscribeTicks(tokens, cb) {
|
|
23196
23756
|
this.ensureSocket();
|
|
23197
23757
|
return this.socket.subscribe(tokens, cb);
|
|
23198
23758
|
}
|
|
23759
|
+
setSymbolMap(map) {
|
|
23760
|
+
this.ensureSocket();
|
|
23761
|
+
this.socket.setSymbolMap(map);
|
|
23762
|
+
}
|
|
23199
23763
|
unsubscribeTicks(sub) {
|
|
23200
23764
|
if (this.socket) {
|
|
23201
23765
|
this.socket.unsubscribe(sub);
|
|
@@ -23207,7 +23771,8 @@ class DhanBroker {
|
|
|
23207
23771
|
async getMargins() {
|
|
23208
23772
|
try {
|
|
23209
23773
|
const raw = await this.request("GET", "/fundlimit");
|
|
23210
|
-
|
|
23774
|
+
console.log("Dhan getMargins - raw response:", raw);
|
|
23775
|
+
const available = Number(raw["availabelBalance"] ?? raw["availabelBalance"] ?? 0);
|
|
23211
23776
|
const used = Number(raw["utilizedAmount"] ?? raw["blockedMargin"] ?? 0);
|
|
23212
23777
|
const collateral = Number(raw["collateralAmount"] ?? 0);
|
|
23213
23778
|
return Ok({
|
|
@@ -23226,6 +23791,7 @@ class DhanBroker {
|
|
|
23226
23791
|
async getFunds() {
|
|
23227
23792
|
try {
|
|
23228
23793
|
const margins = await this.getMargins();
|
|
23794
|
+
console.log("Dhan getFunds - margins:", margins);
|
|
23229
23795
|
if (!margins.ok)
|
|
23230
23796
|
return margins;
|
|
23231
23797
|
return Ok({
|
|
@@ -23687,6 +24253,9 @@ class PaperBroker {
|
|
|
23687
24253
|
return null;
|
|
23688
24254
|
return new Date(this.session.expiresAt);
|
|
23689
24255
|
}
|
|
24256
|
+
getSession() {
|
|
24257
|
+
return this.session;
|
|
24258
|
+
}
|
|
23690
24259
|
async placeOrder(params) {
|
|
23691
24260
|
try {
|
|
23692
24261
|
if (params.side === "BUY") {
|
|
@@ -23818,6 +24387,12 @@ class PaperBroker {
|
|
|
23818
24387
|
}
|
|
23819
24388
|
return Err(new Error("Paper broker requires a data source for candles. Call setDataSource()."));
|
|
23820
24389
|
}
|
|
24390
|
+
toBrokerToken(_instrument, _format = "ws") {
|
|
24391
|
+
return null;
|
|
24392
|
+
}
|
|
24393
|
+
getIndexTokens(_format = "ws") {
|
|
24394
|
+
return new Map;
|
|
24395
|
+
}
|
|
23821
24396
|
subscribeTicks(tokens, cb) {
|
|
23822
24397
|
this.subIdCounter += 1;
|
|
23823
24398
|
const subId = `paper_sub_${this.subIdCounter}`;
|
|
@@ -24009,8 +24584,1298 @@ class PaperBroker {
|
|
|
24009
24584
|
};
|
|
24010
24585
|
}
|
|
24011
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
|
+
}
|
|
24012
25878
|
// src/index.ts
|
|
24013
|
-
var VERSION = "0.3.0";
|
|
24014
25879
|
function createBroker(brokerId, options) {
|
|
24015
25880
|
switch (brokerId) {
|
|
24016
25881
|
case "zerodha":
|
|
@@ -24019,6 +25884,8 @@ function createBroker(brokerId, options) {
|
|
|
24019
25884
|
return new FinvasiaBroker(options);
|
|
24020
25885
|
case "dhan":
|
|
24021
25886
|
return new DhanBroker(options);
|
|
25887
|
+
case "fivepaisa":
|
|
25888
|
+
return new FivePaisaBroker(options);
|
|
24022
25889
|
case "paper":
|
|
24023
25890
|
return new PaperBroker(options);
|
|
24024
25891
|
default: {
|
|
@@ -24034,7 +25901,6 @@ export {
|
|
|
24034
25901
|
createBroker,
|
|
24035
25902
|
ZerodhaBroker,
|
|
24036
25903
|
WSManager,
|
|
24037
|
-
VERSION,
|
|
24038
25904
|
SessionManager,
|
|
24039
25905
|
SessionExpiredError,
|
|
24040
25906
|
RateLimitError,
|
|
@@ -24044,6 +25910,7 @@ export {
|
|
|
24044
25910
|
NsekitError,
|
|
24045
25911
|
InstrumentNotFoundError,
|
|
24046
25912
|
InstrumentMaster,
|
|
25913
|
+
FivePaisaBroker,
|
|
24047
25914
|
FinvasiaBroker,
|
|
24048
25915
|
Err,
|
|
24049
25916
|
DhanBroker,
|