fix: parse SENSEX weekly symbol format DD+MON+STRIKE (no year)

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 <noreply@anthropic.com>
This commit is contained in:
Manohar 2026-06-19 16:02:50 +05:30
parent 02cc8ed9b3
commit 1fbc9279a4

View file

@ -166,14 +166,27 @@ export function calcGreeks(input: GreeksInput): Greeks {
return { iv, delta, gamma, theta, vega, rho, ok: true }; return { iv, delta, gamma, theta, vega, rho, ok: true };
} }
const MONTH_MAP: Record<string, string> = {
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. * Parse Angel One option symbol -> strike/type/expiry.
* Supports two formats: * Handles three formats observed in the wild:
* - SENSEX2660475500CE -> Jun 04 2026, 75500 CE (YY+M+DD) *
* - SENSEX26MAY28076100CE -> May 28 2026, 76100 CE (YY+MON+DD) * 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 { 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)$/); let m = sym.match(/^([A-Z]+)(\d{2})(\d{1})(\d{2})(\d+)(CE|PE)$/);
if (m) { if (m) {
const yy = m[2], mo = m[3].padStart(2, '0'), dd = m[4]; 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 }; 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); 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) { if (m) {
const months: Record<string, string> = { const yy = m[2], mo = MONTH_MAP[m[3].toUpperCase()], dd = m[4];
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 exp = new Date(`20${yy}-${mo}-${dd}T15:30:00+05:30`); const exp = new Date(`20${yy}-${mo}-${dd}T15:30:00+05:30`);
if (!isNaN(exp.getTime())) { if (!isNaN(exp.getTime())) {
return { underlying: m[1], strike: parseInt(m[5]), type: m[6].toUpperCase() as 'CE' | 'PE', expiry: exp }; 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; return null;
} }