jeeva780's picture
Upload 7 files
948485c verified
Raw History Blame Contribute Delete
14.2 kB
const express = require("express");
const path = require("path");
const BreezeConnect = require("breezeconnect").BreezeConnect;
try {
require("dotenv").config();
} catch (_) {}
const app = express();
const PORT = Number(process.env.PORT || 7860);
app.use(express.json({ limit: "1mb" }));
app.use(express.static(path.join(__dirname, "public")));
const BREEZE_API_KEY = process.env.BREEZE_API_KEY || "";
const BREEZE_API_SECRET = process.env.BREEZE_API_SECRET || "";
let breeze = null;
let connected = false;
let customer = null;
let lastError = null;
// This Space is designed for a single owner/user. The Breeze session is kept
// in server memory only and is cleared when the Space restarts or logout is used.
function clearSession() {
breeze = null;
connected = false;
customer = null;
}
function isoAt(dateStr, hhmm, endOfMinute = false) {
const [h, m] = hhmm.split(":").map(Number);
const ms = endOfMinute ? 59_000 : 0;
return new Date(Date.UTC(
Number(dateStr.slice(0, 4)),
Number(dateStr.slice(5, 7)) - 1,
Number(dateStr.slice(8, 10)),
h, m, 0, ms
)).toISOString();
}
function addDays(dateStr, n) {
const d = new Date(`${dateStr}T00:00:00Z`);
d.setUTCDate(d.getUTCDate() + n);
return d.toISOString().slice(0, 10);
}
function weekday(dateStr) {
return new Date(`${dateStr}T00:00:00Z`).getUTCDay();
}
function isWeekday(dateStr) {
const w = weekday(dateStr);
return w >= 1 && w <= 5;
}
function isEntryDay(dateStr) {
const w = weekday(dateStr);
return w >= 1 && w <= 4;
}
function getNextCalendarTradingDay(dateStr) {
for (let i = 1; i <= 8; i++) {
const d = addDays(dateStr, i);
if (isWeekday(d)) return d;
}
return null;
}
// Retained from the supplied strategy.
// Mon -> Tue, Tue -> next Tue, Wed -> next Tue, Thu -> next Tue.
function getStrategyExpiry(dateStr) {
const w = weekday(dateStr);
const days = { 1: 1, 2: 7, 3: 6, 4: 5 }[w];
return days == null ? null : addDays(dateStr, days);
}
function roundATM(spot) {
return Math.round(Number(spot) / 50) * 50;
}
function unwrapSuccess(resp) {
if (!resp) return [];
if (Array.isArray(resp)) return resp;
if (Array.isArray(resp.Success)) return resp.Success;
if (Array.isArray(resp.success)) return resp.success;
if (Array.isArray(resp.data)) return resp.data;
if (resp.Success && typeof resp.Success === "object") return [resp.Success];
return [];
}
function responseError(resp) {
if (!resp) return "Empty Breeze response";
if (resp.Error) return String(resp.Error);
if (resp.error) return String(resp.error);
if (resp.Message) return String(resp.Message);
if (resp.message) return String(resp.message);
return null;
}
function normalizeBars(resp) {
return unwrapSuccess(resp)
.map(x => ({
datetime: x.datetime || x.dateTime || x.timestamp,
open: Number(x.open),
high: Number(x.high),
low: Number(x.low),
close: Number(x.close),
volume: Number(x.volume || 0),
open_interest: Number(x.open_interest || 0)
}))
.filter(x =>
x.datetime &&
Number.isFinite(x.open) &&
Number.isFinite(x.high) &&
Number.isFinite(x.low) &&
Number.isFinite(x.close)
)
.sort((a, b) => String(a.datetime).localeCompare(String(b.datetime)));
}
function localTimeFromBreeze(dt) {
// Breeze examples return "YYYY-MM-DD HH:mm:ss" in IST for historical candles.
const s = String(dt);
const m = s.match(/(\d{2}):(\d{2}):(\d{2})$/);
return m ? Number(m[1]) * 60 + Number(m[2]) : null;
}
async function breezeHistorical(params) {
if (!breeze || !connected) {
throw new Error("Breeze is not connected.");
}
const resp = await breeze.getHistoricalDatav2(params);
const err = responseError(resp);
if (err) throw new Error(err);
const bars = normalizeBars(resp);
if (!bars.length) {
throw new Error("Breeze returned no historical candles for the requested contract/date.");
}
return bars;
}
async function getNiftySpotAt1515(dateStr) {
const bars = await breezeHistorical({
interval: "1minute",
fromDate: isoAt(dateStr, "15:15"),
toDate: isoAt(dateStr, "15:15", true),
stockCode: "NIFTY",
exchangeCode: "NSE",
productType: "cash"
});
const bar = bars.find(b => localTimeFromBreeze(b.datetime) === 15 * 60 + 15) || bars[bars.length - 1];
return { price: bar.close, bar };
}
async function getOptionBars(dateStr, expiryStr, right, strike, fromTime, toTime) {
return breezeHistorical({
interval: "1minute",
fromDate: isoAt(dateStr, fromTime),
toDate: isoAt(dateStr, toTime, true),
stockCode: "NIFTY",
exchangeCode: "NFO",
productType: "options",
expiryDate: isoAt(expiryStr, "07:00"),
right: right.toLowerCase(),
strikePrice: String(strike)
});
}
function simulatePosition(entryPrice, bars, targetPts, slPts, squareOffMin) {
const target = entryPrice + targetPts;
const sl = entryPrice - slPts;
for (const b of bars) {
const min = localTimeFromBreeze(b.datetime);
if (min == null || min < 9 * 60 + 15) continue;
if (min > squareOffMin) break;
// Preserve the supplied strategy's conservative same-candle rule:
// SL is evaluated before target.
if (b.low <= sl) {
return {
exitPrice: sl,
exitMin: min,
reason: "SL"
};
}
if (b.high >= target) {
return {
exitPrice: target,
exitMin: min,
reason: "TARGET"
};
}
}
const usable = bars.filter(b => {
const m = localTimeFromBreeze(b.datetime);
return m != null && m <= squareOffMin;
});
if (!usable.length) {
return {
exitPrice: entryPrice,
exitMin: squareOffMin,
reason: "NO_NEXT_DAY_DATA"
};
}
const last = usable[usable.length - 1];
return {
exitPrice: last.close,
exitMin: localTimeFromBreeze(last.datetime),
reason: "NEXT_DAY_SQUAREOFF"
};
}
function minutesToHHMM(min) {
const h = Math.floor(min / 60);
const m = min % 60;
return `${String(h).padStart(2, "0")}:${String(m).padStart(2, "0")}`;
}
async function runRealBacktest(cfg, progressCb = () => {}) {
const start = cfg.start;
const end = cfg.end;
const targetPts = Number(cfg.targetPts);
const slPts = Number(cfg.slPts);
const brokerage = Number(cfg.brokerage);
const sqOffMin = Number(cfg.sqOffMin);
if (![targetPts, slPts, brokerage, sqOffMin].every(Number.isFinite)) {
throw new Error("Invalid backtest parameters.");
}
if (start > end) throw new Error("Start date cannot be after end date.");
const entryDates = [];
for (let d = start; d <= end; d = addDays(d, 1)) {
if (isEntryDay(d)) entryDates.push(d);
}
const trades = [];
const daily = [];
for (let i = 0; i < entryDates.length; i++) {
const entryDate = entryDates[i];
progressCb({
current: i + 1,
total: entryDates.length,
message: `FETCHING REAL DATA ${i + 1}/${entryDates.length} · ${entryDate}`
});
const nextDay = getNextCalendarTradingDay(entryDate);
if (!nextDay) continue;
const expiry = getStrategyExpiry(entryDate);
if (!expiry) continue;
// Real NIFTY cash price at 15:15 determines ATM.
const spotResult = await getNiftySpotAt1515(entryDate);
const atm = roundATM(spotResult.price);
let dayPnl = 0;
for (const right of ["call", "put"]) {
// Real entry premium at 15:15.
const entryBars = await getOptionBars(
entryDate,
expiry,
right,
atm,
"15:15",
"15:15"
);
const entryBar =
entryBars.find(b => localTimeFromBreeze(b.datetime) === 15 * 60 + 15) ||
entryBars[entryBars.length - 1];
if (!entryBar) {
throw new Error(`No ${right.toUpperCase()} entry candle for ${entryDate} ${atm} ${expiry}.`);
}
const entryPrice = entryBar.close;
// Real next-trading-day option bars.
const nextBars = await getOptionBars(
nextDay,
expiry,
right,
atm,
"09:15",
minutesToHHMM(sqOffMin)
);
const exit = simulatePosition(
entryPrice,
nextBars,
targetPts,
slPts,
sqOffMin
);
const qty = 65;
const gross = (exit.exitPrice - entryPrice) * qty;
const net = gross - brokerage;
dayPnl += net;
trades.push({
entryDate,
entryTime: "15:15",
exitDate: nextDay,
exitTime: minutesToHHMM(exit.exitMin),
dayHeld: 1,
right: right.toUpperCase() === "CALL" ? "CE" : "PE",
strike: atm,
expiry,
lots: 1,
qty,
entry: entryPrice,
exit: exit.exitPrice,
gross,
brokerage,
net,
reason: exit.reason,
spotAtEntry: spotResult.price
});
}
daily.push({
entryDate,
entryTime: "15:15",
exitDate: nextDay,
exitTime: minutesToHHMM(sqOffMin),
pnl: dayPnl
});
}
let cum = 0;
for (const d of daily) {
cum += d.pnl;
d.cum = cum;
d.result = d.pnl > 0 ? "PROFIT" : d.pnl < 0 ? "LOSS" : "FLAT";
}
return { trades, daily };
}
function calculateMetrics(trades) {
const pnl = trades.map(t => Number(t.net));
if (!pnl.length) {
return {
trades: 0, wins: 0, losses: 0, winRate: 0,
grossProfit: 0, grossLoss: 0, brokerage: 0, netPnl: 0,
avgTrade: 0, avgWin: 0, avgLoss: 0,
maxProfit: 0, maxLoss: 0, profitFactor: 0,
maxDrawdown: 0, expectancy: 0
};
}
const wins = pnl.filter(v => v > 0);
const losses = pnl.filter(v => v < 0);
const grossProfit = wins.reduce((a, b) => a + b, 0);
const grossLoss = Math.abs(losses.reduce((a, b) => a + b, 0));
const netPnl = pnl.reduce((a, b) => a + b, 0);
let eq = 0;
let peak = 0;
let dd = 0;
for (const v of pnl) {
eq += v;
peak = Math.max(peak, eq);
dd = Math.min(dd, eq - peak);
}
return {
trades: pnl.length,
wins: wins.length,
losses: losses.length,
winRate: wins.length / pnl.length * 100,
grossProfit,
grossLoss,
brokerage: trades.reduce((a, t) => a + Number(t.brokerage || 0), 0),
netPnl,
avgTrade: netPnl / pnl.length,
avgWin: wins.length ? grossProfit / wins.length : 0,
avgLoss: losses.length ? -grossLoss / losses.length : 0,
maxProfit: Math.max(...pnl),
maxLoss: Math.min(...pnl),
profitFactor: grossLoss > 0 ? grossProfit / grossLoss : null,
maxDrawdown: dd,
expectancy: netPnl / pnl.length
};
}
app.get("/api/health", (req, res) => {
res.json({
ok: true,
service: "selva-option-backtest",
port: PORT,
breezeConfigured: Boolean(BREEZE_API_KEY && BREEZE_API_SECRET),
connected
});
});
app.get("/api/status", (req, res) => {
res.json({
connected,
customer,
breezeConfigured: Boolean(BREEZE_API_KEY && BREEZE_API_SECRET),
lastError
});
});
app.get("/api/login-url", (req, res) => {
if (!BREEZE_API_KEY) {
return res.status(500).json({ error: "BREEZE_API_KEY is missing in Hugging Face Secrets." });
}
const url = `https://api.icicidirect.com/apiuser/login?api_key=${encodeURIComponent(BREEZE_API_KEY)}`;
res.json({ url });
});
app.post("/api/connect", async (req, res) => {
try {
if (!BREEZE_API_KEY || !BREEZE_API_SECRET) {
return res.status(500).json({
error: "BREEZE_API_KEY / BREEZE_API_SECRET are not configured in Hugging Face Secrets."
});
}
const apiSession = String(req.body?.apiSession || "").trim();
if (!apiSession) {
return res.status(400).json({ error: "API Session is required." });
}
const bc = new BreezeConnect({ appKey: BREEZE_API_KEY });
const sessionResp = await bc.generateSession(BREEZE_API_SECRET, apiSession);
const sessionErr = responseError(sessionResp);
if (sessionErr) {
clearSession();
lastError = sessionErr;
return res.status(401).json({ error: sessionErr });
}
// generateSession mutates the Breeze client with the access token/session.
let details = null;
try {
const detailResp = await bc.getCustomerDetails(apiSession);
details = unwrapSuccess(detailResp)[0] || null;
const detailErr = responseError(detailResp);
if (detailErr) {
// Session itself is still valid; do not fail solely because details call
// is unavailable in a particular SDK version.
lastError = detailErr;
}
} catch (e) {
lastError = e.message;
}
breeze = bc;
connected = true;
customer = details;
lastError = null;
res.json({
ok: true,
connected: true,
customer
});
} catch (e) {
clearSession();
lastError = e.message;
res.status(500).json({ error: e.message });
}
});
app.post("/api/logout", (req, res) => {
clearSession();
lastError = null;
res.json({ ok: true });
});
app.post("/api/backtest", async (req, res) => {
if (!connected || !breeze) {
return res.status(401).json({ error: "Connect Breeze before running the backtest." });
}
const cfg = {
start: String(req.body?.start || ""),
end: String(req.body?.end || ""),
targetPts: Number(req.body?.targetPts ?? 20),
slPts: Number(req.body?.slPts ?? 10),
brokerage: Number(req.body?.brokerage ?? 40),
sqOffMin: (() => {
const s = String(req.body?.sqOff || "15:10");
const [h, m] = s.split(":").map(Number);
return h * 60 + m;
})()
};
try {
lastError = null;
const result = await runRealBacktest(cfg);
res.json({
ok: true,
source: "ICICI DIRECT BREEZE HISTORICAL DATA",
strategy: {
entry: "15:15",
legs: ["ATM CE", "ATM PE"],
qty: 65,
targetPts: cfg.targetPts,
slPts: cfg.slPts,
squareOff: String(req.body?.sqOff || "15:10")
},
...result,
metrics: calculateMetrics(result.trades)
});
} catch (e) {
lastError = e.message;
res.status(500).json({ error: e.message });
}
});
app.get("*", (req, res) => {
res.sendFile(path.join(__dirname, "public", "index.html"));
});
app.listen(PORT, "0.0.0.0", () => {
console.log(`Selva Option Backtest listening on 0.0.0.0:${PORT}`);
});