diff --git a/public/index.html b/public/index.html index 5fc6ed2..7a1efbb 100644 --- a/public/index.html +++ b/public/index.html @@ -405,18 +405,17 @@ function toggleCard(id){ let payoffChartInst = null; function parseOptionSym(sym) { - // Angel format: UNDERLYING + YY + M(1digit) + DD + STRIKE + CE/PE - // e.g. SENSEX2651479000CE → underlying=SENSEX, yy=26, m=5, dd=14, strike=79000, type=CE - let m = sym.match(/^([A-Z]+)(d{2})(d{1})(d{2})(d+)(CE|PE)$/); + var RE1 = new RegExp("^([A-Z]+)(\\d{2})(\\d{1})(\\d{2})(\\d+)(CE|PE)$"); + var m = sym.match(RE1); if (m) { - const exp = new Date(`20${m[2]}-${m[3].padStart(2,'0')}-${m[4]}`); - return { underlying: m[1], strike: parseInt(m[5]), type: m[6], expiry: exp }; + var exp = new Date(20+m[2]+-+m[3].padStart(2,0)+-+m[4]); + return { underlying: m[1], strike: parseInt(m[5]), type: m[6], expiry: isNaN(exp)?null:exp }; } - // Fallback: UNDERLYING + YYMMDD + STRIKE + CE/PE (2-digit month) - m = sym.match(/^([A-Z]+)(d{2})(d{2})(d{2})(d+)(CE|PE)$/); + var RE2 = new RegExp("^([A-Z]+)(\\d{2})(\\d{2})(\\d{2})(\\d+)(CE|PE)$"); + m = sym.match(RE2); if (m) { - const exp = new Date(`20${m[2]}-${m[3]}-${m[4]}`); - return { underlying: m[1], strike: parseInt(m[5]), type: m[6], expiry: exp }; + var exp2 = new Date(20+m[2]+-+m[3]+-+m[4]); + return { underlying: m[1], strike: parseInt(m[5]), type: m[6], expiry: isNaN(exp2)?null:exp2 }; } return null; } @@ -530,51 +529,15 @@ async function loadPayoff() { const gc = dk ? 'rgba(255,255,255,.05)' : 'rgba(0,0,0,.05)'; const tc = dk ? '#3D444D' : '#C4B8B0'; - // Profit fill (above zero) and loss fill (below zero) — use two datasets with clip trick - const profitData = ys.map(y => ({ y: Math.max(y, 0) })); - const lossData = ys.map(y => ({ y: Math.min(y, 0) })); - + canvas._beLines = breakevens; canvas._spotLine = spot; canvas._chartXs = xs; if (payoffChartInst) payoffChartInst.destroy(); - - const annotations = {}; - // Breakeven vertical lines - breakevens.forEach((be, i) => { - annotations['be'+i] = { - type: 'line', xMin: be, xMax: be, - borderColor: 'rgba(245,166,35,.7)', borderWidth: 1.5, borderDash: [5,4], - label: { display: true, content: be.toLocaleString('en-IN'), position: 'start', - color: '#F5A623', font: { size: 10, family: 'Geist Mono' }, yAdjust: -8 } - }; - }); - // Current spot - if (spot) { - annotations['spot'] = { - type: 'line', xMin: spot, xMax: spot, - borderColor: 'rgba(255,107,74,.8)', borderWidth: 2, borderDash: [6,3], - label: { display: true, content: spot.toLocaleString('en-IN'), position: 'end', - color: '#FF6B4A', font: { size: 10, family: 'Geist Mono' }, yAdjust: 8 } - }; - } - payoffChartInst = new Chart(canvas, { type: 'line', - data: { - labels: xs, - datasets: [ - { - label: 'Profit', - data: ys, - borderColor: ys[Math.floor(ys.length/2)] >= 0 ? '#2ECC71' : '#E74C3C', - borderWidth: 2.5, - tension: 0, - pointRadius: 0, - fill: { - target: { value: 0 }, - above: 'rgba(46,204,113,0.15)', - below: 'rgba(231,76,60,0.15)', - }, - }, - ], + data: { labels: xs, datasets: [ + {label:'Profit Zone',data:ys.map(function(y){return y>=0?y:0;}),borderColor:'transparent',backgroundColor:'rgba(46,204,113,0.18)',tension:0,pointRadius:0,fill:true}, + {label:'Loss Zone',data:ys.map(function(y){return y<0?y:0;}),borderColor:'transparent',backgroundColor:'rgba(231,76,60,0.18)',tension:0,pointRadius:0,fill:true}, + {label:'P&L',data:ys,borderColor:'#FF6B4A',borderWidth:2.5,tension:0,pointRadius:0,fill:false}, + ] }, }, options: { responsive: true, maintainAspectRatio: false, animation: false, @@ -594,9 +557,6 @@ async function loadPayoff() { }, }, }, - annotation: (window.ChartAnnotation && { - annotations, - }), }, scales: { x: { @@ -620,6 +580,60 @@ async function loadPayoff() { } // End payoff graph + +var _payoffPlugin = { + id: 'payoffLines', + afterDraw: function(chart) { + if (!chart.canvas || !chart.canvas._beLines) return; + var ctx = chart.ctx; + var xs = chart.canvas._chartXs; + if (!xs || !xs.length) return; + var xScale = chart.scales.x, yScale = chart.scales.y; + var lo = xs[0], hi = xs[xs.length-1], range = hi - lo; + if (!range) return; + function xPx(v) { return xScale.left + (v-lo)/range*(xScale.right-xScale.left); } + ctx.save(); + // Breakeven lines — amber dashed + (chart.canvas._beLines || []).forEach(function(be) { + var x = xPx(be); + if (x < xScale.left || x > xScale.right) return; + ctx.beginPath(); + ctx.setLineDash([5,4]); + ctx.strokeStyle = 'rgba(245,166,35,0.85)'; + ctx.lineWidth = 1.5; + ctx.moveTo(x, yScale.top); + ctx.lineTo(x, yScale.bottom); + ctx.stroke(); + ctx.setLineDash([]); + ctx.fillStyle = '#F5A623'; + ctx.font = '9px monospace'; + ctx.textAlign = 'center'; + ctx.fillText(Number(be).toLocaleString('en-IN'), x, yScale.top + 10); + }); + // Spot line — coral dashed + var spot = chart.canvas._spotLine; + if (spot) { + var x = xPx(spot); + if (x >= xScale.left && x <= xScale.right) { + ctx.beginPath(); + ctx.setLineDash([6,3]); + ctx.strokeStyle = 'rgba(255,107,74,0.9)'; + ctx.lineWidth = 2; + ctx.moveTo(x, yScale.top); + ctx.lineTo(x, yScale.bottom); + ctx.stroke(); + ctx.setLineDash([]); + ctx.fillStyle = '#FF6B4A'; + ctx.font = 'bold 9px monospace'; + ctx.textAlign = 'center'; + ctx.fillText('Spot ' + Number(spot).toLocaleString('en-IN'), x, yScale.bottom - 4); + } + } + ctx.restore(); + } +}; +Chart.register(_payoffPlugin); + loadConfig();refresh(); setInterval(refresh,60000); setInterval(loadMarket,15000);