feat: strategy payoff graph — breakeven, max P&L, R/R, spot line, profit/loss shading
This commit is contained in:
parent
7cf1de885e
commit
4b0bfb2a12
1 changed files with 267 additions and 1 deletions
|
|
@ -8,6 +8,7 @@
|
||||||
<link rel="preconnect" href="https://fonts.googleapis.com">
|
<link rel="preconnect" href="https://fonts.googleapis.com">
|
||||||
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Geist:wght@300;400;500;600&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
|
<link href="https://fonts.googleapis.com/css2?family=DM+Serif+Display:ital@0;1&family=Geist:wght@300;400;500;600&family=Geist+Mono:wght@400;500&display=swap" rel="stylesheet">
|
||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.4.0/dist/chart.umd.min.js"></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chartjs-plugin-annotation@3.0.1/dist/chartjs-plugin-annotation.min.js"></script>
|
||||||
<style>
|
<style>
|
||||||
:root{--coral:#FF6B4A;--amber:#F5A623;--green:#2ECC71;--red:#E74C3C;--tr:.22s cubic-bezier(.4,0,.2,1)}
|
:root{--coral:#FF6B4A;--amber:#F5A623;--green:#2ECC71;--red:#E74C3C;--tr:.22s cubic-bezier(.4,0,.2,1)}
|
||||||
[data-theme="dark"]{--bg:#0D1117;--bg2:#161B22;--bg3:#1C2330;--border:rgba(255,255,255,.07);--border2:rgba(255,255,255,.13);--text:#E6EDF3;--text2:#8B949E;--text3:#3D444D;--card:rgba(22,27,34,.97);--shadow:0 4px 28px rgba(0,0,0,.45);--shadow-lg:0 8px 48px rgba(0,0,0,.6);--glass:rgba(22,27,34,.8)}
|
[data-theme="dark"]{--bg:#0D1117;--bg2:#161B22;--bg3:#1C2330;--border:rgba(255,255,255,.07);--border2:rgba(255,255,255,.13);--text:#E6EDF3;--text2:#8B949E;--text3:#3D444D;--card:rgba(22,27,34,.97);--shadow:0 4px 28px rgba(0,0,0,.45);--shadow-lg:0 8px 48px rgba(0,0,0,.6);--glass:rgba(22,27,34,.8)}
|
||||||
|
|
@ -132,6 +133,16 @@
|
||||||
.pcard.pt .pval { font-size: 1.7rem; }
|
.pcard.pt .pval { font-size: 1.7rem; }
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
.pf-stat{padding:12px 16px;border-right:1px solid var(--border);text-align:center}
|
||||||
|
.pf-stat:last-child{border-right:none}
|
||||||
|
.pfs-lbl{font-size:.6rem;font-weight:600;letter-spacing:.08em;text-transform:uppercase;color:var(--text3);margin-bottom:5px}
|
||||||
|
.pfs-val{font-family:'DM Serif Display',serif;font-size:1.1rem;color:var(--text)}
|
||||||
|
@media(max-width:768px){
|
||||||
|
#sec-payoff .pf-stat{grid-column:span 2}
|
||||||
|
#sec-payoff [style*="grid-template-columns:repeat(6"]{grid-template-columns:repeat(2,1fr)!important}
|
||||||
|
}
|
||||||
|
|
||||||
</style>
|
</style>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
@ -190,6 +201,40 @@
|
||||||
<tfoot id="pos-foot" style="display:none"><tr><td colspan="4">Portfolio Total</td><td id="f-u" class="mono">—</td><td id="f-r" class="mono">—</td><td id="f-t" class="mono">—</td></tr></tfoot>
|
<tfoot id="pos-foot" style="display:none"><tr><td colspan="4">Portfolio Total</td><td id="f-u" class="mono">—</td><td id="f-r" class="mono">—</td><td id="f-t" class="mono">—</td></tr></tfoot>
|
||||||
</table></div>
|
</table></div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div class="card collapsible" id="sec-payoff" style="margin-bottom:16px">
|
||||||
|
<div class="card-head" onclick="toggleCard('sec-payoff')">
|
||||||
|
<span class="card-title">Strategy Payoff at Expiry</span>
|
||||||
|
<div style="display:flex;align-items:center;gap:8px">
|
||||||
|
<span id="pf-underlying" style="font-size:.7rem;font-weight:600;color:var(--text2);font-family:'Geist Mono',monospace"></span>
|
||||||
|
<span id="pf-strategy" style="font-size:.68rem;padding:2px 8px;border-radius:9999px;background:var(--bg3);border:1px solid var(--border2);color:var(--text2)"></span>
|
||||||
|
</div>
|
||||||
|
<span class="collapse-arrow">▼</span>
|
||||||
|
</div>
|
||||||
|
<div class="collapse-body">
|
||||||
|
<!-- Stats row -->
|
||||||
|
<div style="display:grid;grid-template-columns:repeat(6,1fr);gap:0;border-bottom:1px solid var(--border)">
|
||||||
|
<div class="pf-stat" id="pfs-credit"><div class="pfs-lbl">NET PREMIUM</div><div class="pfs-val">—</div></div>
|
||||||
|
<div class="pf-stat" id="pfs-maxp"><div class="pfs-lbl">MAX PROFIT</div><div class="pfs-val up">—</div></div>
|
||||||
|
<div class="pf-stat" id="pfs-maxl"><div class="pfs-lbl">MAX LOSS</div><div class="pfs-val dn">—</div></div>
|
||||||
|
<div class="pf-stat" id="pfs-rr"><div class="pfs-lbl">REWARD/RISK</div><div class="pfs-val">—</div></div>
|
||||||
|
<div class="pf-stat" id="pfs-be"><div class="pfs-lbl">BREAKEVEN(S)</div><div class="pfs-val" style="font-size:.78rem">—</div></div>
|
||||||
|
<div class="pf-stat" id="pfs-dte"><div class="pfs-lbl">DTE</div><div class="pfs-val">—</div></div>
|
||||||
|
</div>
|
||||||
|
<!-- Chart -->
|
||||||
|
<div style="padding:16px 16px 12px;height:260px;position:relative">
|
||||||
|
<canvas id="payoff-chart"></canvas>
|
||||||
|
<div id="payoff-empty" style="display:none;position:absolute;inset:0;align-items:center;justify-content:center;color:var(--text3);font-size:.8rem;flex-direction:column;gap:6px">
|
||||||
|
<span style="font-size:1.4rem">📈</span>
|
||||||
|
<span>No option positions to analyse</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div style="padding:0 16px 12px;font-size:.65rem;color:var(--text3)">
|
||||||
|
Theoretical payoff at expiry based on avg cost. Does not account for time value or IV changes.
|
||||||
|
Current spot shown as dashed line.
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div class="card collapsible" id="sec-alrt" style="margin-bottom:16px">
|
<div class="card collapsible" id="sec-alrt" style="margin-bottom:16px">
|
||||||
<div class="card-head" onclick="toggleCard('sec-alrt')"><span class="card-title">Alert History</span><div style="display:flex;align-items:center;gap:8px"><span id="alrt-hcount" style="font-size:.7rem;font-weight:600;padding:2px 9px;border-radius:9999px;background:rgba(245,166,35,.15);color:var(--amber);display:none"></span><span id="alrt-hmuted" style="font-size:.7rem;font-weight:600;padding:2px 9px;border-radius:9999px;background:rgba(231,76,60,.12);color:var(--red);display:none">MUTED</span></div><span class="collapse-arrow">▼</span></div>
|
<div class="card-head" onclick="toggleCard('sec-alrt')"><span class="card-title">Alert History</span><div style="display:flex;align-items:center;gap:8px"><span id="alrt-hcount" style="font-size:.7rem;font-weight:600;padding:2px 9px;border-radius:9999px;background:rgba(245,166,35,.15);color:var(--amber);display:none"></span><span id="alrt-hmuted" style="font-size:.7rem;font-weight:600;padding:2px 9px;border-radius:9999px;background:rgba(231,76,60,.12);color:var(--red);display:none">MUTED</span></div><span class="collapse-arrow">▼</span></div>
|
||||||
<div class="collapse-body"><table>
|
<div class="collapse-body"><table>
|
||||||
|
|
@ -223,6 +268,7 @@
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<script>
|
<script>
|
||||||
|
if(window.ChartAnnotation)Chart.register(window.ChartAnnotation);
|
||||||
let theme=localStorage.getItem('theme')||'dark';
|
let theme=localStorage.getItem('theme')||'dark';
|
||||||
document.documentElement.setAttribute('data-theme',theme);
|
document.documentElement.setAttribute('data-theme',theme);
|
||||||
document.getElementById('theme-btn').textContent=theme==='dark'?'\u2600\ufe0f':'\ud83c\udf19';
|
document.getElementById('theme-btn').textContent=theme==='dark'?'\u2600\ufe0f':'\ud83c\udf19';
|
||||||
|
|
@ -336,7 +382,7 @@ async function saveOverride(key){
|
||||||
const mute=document.getElementById('om-'+key)?.value;
|
const mute=document.getElementById('om-'+key)?.value;
|
||||||
await fetch('/api/config/'+encodeURIComponent(key),{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({alert_threshold_pct:pct?parseFloat(pct):null,muted_until:mute?new Date(mute).toISOString():null})});
|
await fetch('/api/config/'+encodeURIComponent(key),{method:'PUT',headers:{'Content-Type':'application/json'},body:JSON.stringify({alert_threshold_pct:pct?parseFloat(pct):null,muted_until:mute?new Date(mute).toISOString():null})});
|
||||||
}
|
}
|
||||||
async function refresh(){await Promise.all([loadPositions(),loadAlerts(),loadHealth(),loadChart(curH),loadMarket()]);}
|
async function refresh(){await Promise.all([loadPositions(),loadAlerts(),loadHealth(),loadChart(curH),loadMarket(),loadPayoff()]);}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
@ -354,6 +400,226 @@ function toggleCard(id){
|
||||||
Object.entries(s).forEach(([id,c])=>{if(c){const el=document.getElementById(id);if(el)el.classList.add('collapsed');}});
|
Object.entries(s).forEach(([id,c])=>{if(c){const el=document.getElementById(id);if(el)el.classList.add('collapsed');}});
|
||||||
}catch(e){}
|
}catch(e){}
|
||||||
})();
|
})();
|
||||||
|
|
||||||
|
// ── Payoff Graph ──────────────────────────────────────────────────────────────
|
||||||
|
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)$/);
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
// Fallback: UNDERLYING + YYMMDD + STRIKE + CE/PE (2-digit month)
|
||||||
|
m = sym.match(/^([A-Z]+)(d{2})(d{2})(d{2})(d+)(CE|PE)$/);
|
||||||
|
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 };
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function calcPayoff(positions, spot) {
|
||||||
|
let total = 0;
|
||||||
|
for (const p of positions) {
|
||||||
|
const info = parseOptionSym(p.tradingsymbol);
|
||||||
|
if (!info) continue;
|
||||||
|
const intrinsic = info.type === 'CE' ? Math.max(spot - info.strike, 0) : Math.max(info.strike - spot, 0);
|
||||||
|
// payoff per unit at expiry = intrinsic - avg_price (for long)
|
||||||
|
// netqty: positive=long, negative=short → multiply directly
|
||||||
|
total += (intrinsic - p.avg_price) * p.netqty;
|
||||||
|
}
|
||||||
|
return total;
|
||||||
|
}
|
||||||
|
|
||||||
|
function detectStrategy(positions) {
|
||||||
|
const calls = positions.filter(p => p._parsed?.type === 'CE');
|
||||||
|
const puts = positions.filter(p => p._parsed?.type === 'PE');
|
||||||
|
const longs = positions.filter(p => p.netqty > 0);
|
||||||
|
const shorts = positions.filter(p => p.netqty < 0);
|
||||||
|
if (positions.length === 1) return longs.length ? 'Long ' + (calls.length ? 'Call' : 'Put') : 'Short ' + (calls.length ? 'Call' : 'Put');
|
||||||
|
if (calls.length === 2 && puts.length === 0) return longs.length === 1 && shorts.length === 1 ? 'Bull Call Spread' : 'Call Spread';
|
||||||
|
if (puts.length === 2 && calls.length === 0) return longs.length === 1 && shorts.length === 1 ? 'Bear Put Spread' : 'Put Spread';
|
||||||
|
if (calls.length >= 1 && puts.length >= 1 && shorts.length === positions.length) return 'Short Strangle/Straddle';
|
||||||
|
if (calls.length >= 1 && puts.length >= 1 && longs.length === positions.length) return 'Long Strangle/Straddle';
|
||||||
|
if (positions.length === 4) return 'Iron Condor / Butterfly';
|
||||||
|
return positions.length + '-Leg Strategy';
|
||||||
|
}
|
||||||
|
|
||||||
|
async function loadPayoff() {
|
||||||
|
const [posRes, mktRes] = await Promise.all([
|
||||||
|
fetch('/api/positions').then(r=>r.json()).catch(()=>({data:[]})),
|
||||||
|
fetch('/api/market').then(r=>r.json()).catch(()=>({data:[]})),
|
||||||
|
]);
|
||||||
|
|
||||||
|
const canvas = document.getElementById('payoff-chart');
|
||||||
|
const emptyEl = document.getElementById('payoff-empty');
|
||||||
|
|
||||||
|
const open = (posRes.data || []).filter(p => p.is_closed === 0);
|
||||||
|
// Only option positions (have CE/PE in symbol)
|
||||||
|
const opts = open.filter(p => /CE|PE/.test(p.tradingsymbol)).map(p => {
|
||||||
|
p._parsed = parseOptionSym(p.tradingsymbol);
|
||||||
|
return p;
|
||||||
|
}).filter(p => p._parsed);
|
||||||
|
|
||||||
|
if (!opts.length) {
|
||||||
|
canvas.style.display = 'none';
|
||||||
|
emptyEl.style.display = 'flex';
|
||||||
|
['pfs-credit','pfs-maxp','pfs-maxl','pfs-rr','pfs-be','pfs-dte'].forEach(id => {
|
||||||
|
document.querySelector('#'+id+' .pfs-val').textContent = '—';
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
canvas.style.display = 'block';
|
||||||
|
emptyEl.style.display = 'none';
|
||||||
|
|
||||||
|
// Get current spot from market data
|
||||||
|
const mktMap = Object.fromEntries((mktRes.data || []).map(q => [q.key, q.price]));
|
||||||
|
const underlying = opts[0]._parsed.underlying;
|
||||||
|
const mktKey = underlying === 'SENSEX' ? 'SENSEX' : underlying === 'NIFTY' ? 'NIFTY50' : underlying === 'BANKNIFTY' ? 'BANKNIFTY' : null;
|
||||||
|
const spot = mktKey ? (mktMap[mktKey] || 0) : 0;
|
||||||
|
|
||||||
|
// Build price range: span all strikes ±15%
|
||||||
|
const strikes = opts.map(p => p._parsed.strike);
|
||||||
|
const minS = Math.min(...strikes), maxS = Math.max(...strikes);
|
||||||
|
const pad = Math.max((maxS - minS) * 0.6, minS * 0.08);
|
||||||
|
const lo = Math.floor((Math.min(minS, spot || minS) - pad) / 100) * 100;
|
||||||
|
const hi = Math.ceil((Math.max(maxS, spot || maxS) + pad) / 100) * 100;
|
||||||
|
const step = Math.max(Math.round((hi - lo) / 120 / 100) * 100, 50);
|
||||||
|
|
||||||
|
const xs = [], ys = [];
|
||||||
|
for (let s = lo; s <= hi; s += step) { xs.push(s); ys.push(calcPayoff(opts, s)); }
|
||||||
|
|
||||||
|
const maxY = Math.max(...ys), minY = Math.min(...ys);
|
||||||
|
|
||||||
|
// Breakevens: sign changes
|
||||||
|
const breakevens = [];
|
||||||
|
for (let i = 1; i < ys.length; i++) {
|
||||||
|
if ((ys[i-1] < 0 && ys[i] >= 0) || (ys[i-1] >= 0 && ys[i] < 0)) {
|
||||||
|
const be = xs[i-1] + (xs[i]-xs[i-1]) * (-ys[i-1]/(ys[i]-ys[i-1]));
|
||||||
|
breakevens.push(Math.round(be));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Net premium (positive = credit, negative = debit)
|
||||||
|
const netPremium = opts.reduce((s, p) => s + p.avg_price * p.netqty, 0);
|
||||||
|
|
||||||
|
// Stats
|
||||||
|
const dte = opts[0]._parsed.expiry ? Math.max(0, Math.ceil((opts[0]._parsed.expiry - new Date()) / 86400000)) : '?';
|
||||||
|
const rr = minY !== 0 ? (maxY / Math.abs(minY)).toFixed(2) : '∞';
|
||||||
|
|
||||||
|
const setVal = (id, html, cls='') => {
|
||||||
|
const el = document.querySelector('#'+id+' .pfs-val');
|
||||||
|
el.innerHTML = html;
|
||||||
|
if (cls) el.className = 'pfs-val ' + cls;
|
||||||
|
};
|
||||||
|
setVal('pfs-credit', (netPremium>=0?'+':'')+'₹'+Math.round(netPremium).toLocaleString('en-IN'), netPremium>=0?'up':'dn');
|
||||||
|
setVal('pfs-maxp', maxY > 1e6 ? '∞' : '+₹'+Math.round(maxY).toLocaleString('en-IN'), 'up');
|
||||||
|
setVal('pfs-maxl', minY < -1e6 ? '−∞' : '₹'+Math.round(minY).toLocaleString('en-IN'), 'dn');
|
||||||
|
setVal('pfs-rr', rr + 'x');
|
||||||
|
setVal('pfs-be', breakevens.length ? breakevens.map(b=>b.toLocaleString('en-IN')).join(', ') : 'None', '');
|
||||||
|
setVal('pfs-dte', dte + ' days');
|
||||||
|
|
||||||
|
document.getElementById('pf-underlying').textContent = underlying;
|
||||||
|
document.getElementById('pf-strategy').textContent = detectStrategy(opts);
|
||||||
|
|
||||||
|
// Build chart datasets: split into profit (green) and loss (red) segments
|
||||||
|
const dk = theme === 'dark';
|
||||||
|
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) }));
|
||||||
|
|
||||||
|
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)',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
options: {
|
||||||
|
responsive: true, maintainAspectRatio: false, animation: false,
|
||||||
|
interaction: { intersect: false, mode: 'index' },
|
||||||
|
plugins: {
|
||||||
|
legend: { display: false },
|
||||||
|
tooltip: {
|
||||||
|
backgroundColor: dk ? '#1C2330' : '#fff',
|
||||||
|
borderColor: dk ? 'rgba(255,255,255,.1)' : 'rgba(0,0,0,.1)',
|
||||||
|
borderWidth: 1, titleColor: tc, bodyColor: dk ? '#E6EDF3' : '#1A1410',
|
||||||
|
padding: 10, cornerRadius: 8,
|
||||||
|
callbacks: {
|
||||||
|
title: items => 'Spot: ₹' + parseInt(items[0].label).toLocaleString('en-IN'),
|
||||||
|
label: item => {
|
||||||
|
const v = item.raw;
|
||||||
|
return ' P&L: ' + (v >= 0 ? '+' : '') + '₹' + Math.round(v).toLocaleString('en-IN');
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
annotation: (window.ChartAnnotation && {
|
||||||
|
annotations,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
scales: {
|
||||||
|
x: {
|
||||||
|
type: 'linear',
|
||||||
|
ticks: {
|
||||||
|
color: tc, maxTicksLimit: 10, font: { size: 9, family: 'Geist Mono' },
|
||||||
|
callback: v => v.toLocaleString('en-IN'),
|
||||||
|
},
|
||||||
|
grid: { color: gc },
|
||||||
|
},
|
||||||
|
y: {
|
||||||
|
ticks: {
|
||||||
|
color: tc, font: { size: 9, family: 'Geist Mono' },
|
||||||
|
callback: v => (v >= 0 ? '+' : '') + '₹' + Math.round(v).toLocaleString('en-IN'),
|
||||||
|
},
|
||||||
|
grid: { color: gc },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// End payoff graph
|
||||||
|
|
||||||
loadConfig();refresh();
|
loadConfig();refresh();
|
||||||
setInterval(refresh,60000);
|
setInterval(refresh,60000);
|
||||||
setInterval(loadMarket,15000);
|
setInterval(loadMarket,15000);
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue