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; }