From 1fbc9279a4a54293e95f4b128c62f254be38324f Mon Sep 17 00:00:00 2001 From: Manohar Date: Fri, 19 Jun 2026 16:02:50 +0530 Subject: [PATCH] fix: parse SENSEX weekly symbol format DD+MON+STRIKE (no year) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Angel One SENSEX weeklies use e.g. SENSEX26JUN77300PE where 26=day, JUN=month, 77300=strike — no year in the symbol. The previous parser tried YY+MON+DD, treating 26 as year and 77 as day, giving an invalid date (2026-06-77) and returning null for ALL option positions. Added Format 3: DD(1-2) + MON(3-letter) + STRIKE + TYPE Year is inferred: current year, or next year if date is >7 days past. Format 2 (YY+MON+DD) is left as-is so it still falls through to Format 3 when its parsed date is invalid (day=77 etc). Co-Authored-By: Claude Sonnet 4.6 --- src/ai/greeks.ts | 46 +++++++++++++++++++++++++++++++++++----------- 1 file changed, 35 insertions(+), 11 deletions(-) diff --git a/src/ai/greeks.ts b/src/ai/greeks.ts index ee42bb0..8b57e47 100644 --- a/src/ai/greeks.ts +++ b/src/ai/greeks.ts @@ -166,14 +166,27 @@ export function calcGreeks(input: GreeksInput): Greeks { return { iv, delta, gamma, theta, vega, rho, ok: true }; } +const MONTH_MAP: Record = { + JAN:'01',FEB:'02',MAR:'03',APR:'04',MAY:'05',JUN:'06', + JUL:'07',AUG:'08',SEP:'09',OCT:'10',NOV:'11',DEC:'12', +}; + /** - * Parse SENSEX option symbol -> strike/type/expiry. - * Supports two formats: - * - SENSEX2660475500CE -> Jun 04 2026, 75500 CE (YY+M+DD) - * - SENSEX26MAY28076100CE -> May 28 2026, 76100 CE (YY+MON+DD) + * Parse Angel One option symbol -> strike/type/expiry. + * Handles three formats observed in the wild: + * + * Format 1 — YY + M(1-digit) + DD + STRIKE + * SENSEX2660475500CE → 2026-06-04, strike 75500 + * + * Format 2 — YY + MON + DD + STRIKE (NIFTY carry-forward style) + * NIFTY28FEB2522500CE → 2025-02-28, strike 22500 + * (NOTE: regex groups are DD, MON, YY order — Angel puts day first) + * + * Format 3 — DD + MON + STRIKE (SENSEX weekly, no year in symbol) + * SENSEX26JUN77300PE → 2026-06-26, strike 77300, year inferred */ export function parseOptionSymbol(sym: string): { underlying: string; strike: number; type: 'CE' | 'PE'; expiry: Date } | null { - // Format 1: YY + M(1) + DD + // Format 1: YY + M(1) + DD + STRIKE let m = sym.match(/^([A-Z]+)(\d{2})(\d{1})(\d{2})(\d+)(CE|PE)$/); if (m) { const yy = m[2], mo = m[3].padStart(2, '0'), dd = m[4]; @@ -182,19 +195,30 @@ export function parseOptionSymbol(sym: string): { underlying: string; strike: nu return { underlying: m[1], strike: parseInt(m[5]), type: m[6] as 'CE' | 'PE', expiry: exp }; } } - // Format 2: YY + MON + DD + // Format 2: YY + MON + DD + STRIKE (kept for backward compat; falls through on invalid dates) + // Note: SENSEX26JUN77300PE is NOT this format — it falls through to Format 3 below. m = sym.match(/^([A-Z]+)(\d{2})(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(\d{2})(\d+)(CE|PE)$/i); if (m) { - const months: Record = { - JAN:'01',FEB:'02',MAR:'03',APR:'04',MAY:'05',JUN:'06', - JUL:'07',AUG:'08',SEP:'09',OCT:'10',NOV:'11',DEC:'12', - }; - const yy = m[2], mo = months[m[3].toUpperCase()], dd = m[4]; + const yy = m[2], mo = MONTH_MAP[m[3].toUpperCase()], dd = m[4]; const exp = new Date(`20${yy}-${mo}-${dd}T15:30:00+05:30`); if (!isNaN(exp.getTime())) { return { underlying: m[1], strike: parseInt(m[5]), type: m[6].toUpperCase() as 'CE' | 'PE', expiry: exp }; } } + // Format 3: DD + MON + STRIKE (SENSEX weekly — no year, infer from calendar) + m = sym.match(/^([A-Z]+)(\d{1,2})(JAN|FEB|MAR|APR|MAY|JUN|JUL|AUG|SEP|OCT|NOV|DEC)(\d+)(CE|PE)$/i); + if (m) { + const dd = m[2].padStart(2, '0'), mo = MONTH_MAP[m[3].toUpperCase()]; + const thisYear = new Date().getFullYear(); + // Try current year first; if date is already >7 days in the past, try next year + let exp = new Date(`${thisYear}-${mo}-${dd}T15:30:00+05:30`); + if (isNaN(exp.getTime()) || exp.getTime() < Date.now() - 7 * 86400000) { + exp = new Date(`${thisYear + 1}-${mo}-${dd}T15:30:00+05:30`); + } + if (!isNaN(exp.getTime())) { + return { underlying: m[1], strike: parseInt(m[4]), type: m[5].toUpperCase() as 'CE' | 'PE', expiry: exp }; + } + } return null; }