Download server.js from jeeva780/Selva-Option-Backtest: direct link, hf CLI and curl.
- Browser
- Download file 14.2 kB
-
https://huggingface.co/spaces/jeeva780/Selva-Option-Backtest/resolve/main/server.js
- Command line
-
hf download hf://spaces/jeeva780/Selva-Option-Backtest/server.js
-
curl -L -o server.js https://huggingface.co/spaces/jeeva780/Selva-Option-Backtest/resolve/main/server.js
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}`); | |
| }); | |