diff --git a/src/js/core/05d-calc-stats.js b/src/js/core/05d-calc-stats.js index e34038c..d0895b1 100644 --- a/src/js/core/05d-calc-stats.js +++ b/src/js/core/05d-calc-stats.js @@ -1,6 +1,6 @@ function calcPremiumStats(rows) { let totalPrem = 0, totalNotional = 0, totalCount = 0; - let otmCount = 0, itmCount = 0, openCount = 0; + let otmCount = 0, itmCount = 0, closedCount = 0, openCount = 0; let aprWeightedSum = 0, aprWeightTotal = 0; rows.forEach(r => { @@ -13,17 +13,19 @@ function calcPremiumStats(rows) { if (r.outcome === 'OPEN') { openCount++; } else if (r.outcome === 'EXPIRED') { otmCount++; } else if (r.outcome === 'ASSIGNED' || r.outcome === 'CALLED') { itmCount++; } + else if (r.outcome === 'CLOSED') { closedCount++; } if (r.annual != null) { aprWeightedSum += r.annual * notional; aprWeightTotal += notional; } }); - const settled = otmCount + itmCount; - const returnRate = settled > 0 ? otmCount / settled * 100 : null; + // CLOSED (buy-to-close) is a settled outcome: premium kept, no assignment/call-away. + const settled = otmCount + itmCount + closedCount; + const returnRate = settled > 0 ? (otmCount + closedCount) / settled * 100 : null; const portfolioAPR = aprWeightTotal > 0 ? aprWeightedSum / aprWeightTotal : null; - return { totalPrem, totalNotional, totalCount, otmCount, itmCount, openCount, settled, returnRate, portfolioAPR }; + return { totalPrem, totalNotional, totalCount, otmCount, itmCount, closedCount, openCount, settled, returnRate, portfolioAPR }; } if (typeof module !== 'undefined' && module.exports) { diff --git a/src/js/core/07-render-charts.js b/src/js/core/07-render-charts.js index 82cef2f..a5e9732 100644 --- a/src/js/core/07-render-charts.js +++ b/src/js/core/07-render-charts.js @@ -398,7 +398,7 @@ function rCharts(displayRows, lots) { 'Total Premium Collected', s.totalCount > 0 ? '$' + fmt(s.totalPrem) : dash, s.totalCount > 0 ? pos(s.settled) + ' settled' + (s.openCount > 0 ? ' · ' + s.openCount + ' open' : '') : '', - 'Sum of every option premium collected (gross of buy-to-close costs). Includes settled and open positions.') + + 'Sum of every option premium collected, net of buy-to-close costs. Includes settled and open positions.') + '
' + (_isTradfi() ? '' : tile('', 'Total Notional', s.totalNotional > 0 ? '$' + fmt(s.totalNotional) : dash, @@ -411,7 +411,7 @@ function rCharts(displayRows, lots) { tile('', 'Return Rate', s.returnRate !== null ? s.returnRate.toFixed(1) + '%' : dash, s.settled > 0 ? s.otmCount + ' / ' + s.settled + ' exp OTM' : '', - 'Share of settled options that expired OTM (premium kept, no assignment/call-away). Open options excluded.') + + 'Share of settled options that kept their premium with no assignment/call-away (expired OTM or bought back early). Open options excluded.') + '
'; } else { diff --git a/test/unit/calc-stats.test.js b/test/unit/calc-stats.test.js index 8b4e6a7..023a190 100644 --- a/test/unit/calc-stats.test.js +++ b/test/unit/calc-stats.test.js @@ -50,6 +50,19 @@ describe('calcPremiumStats', () => { assert.equal(s.settled, 2); }); + it('counts CLOSED (buy-to-close) as a settled outcome, net of closeCost', () => { + const rows = [ + makeRow({ outcome: 'CLOSED', premium: 45, closeCost: 37 }), + makeRow({ outcome: 'CLOSED', premium: 65, closeCost: 12 }), + ]; + const s = calcPremiumStats(rows); + assert.equal(s.closedCount, 2); + assert.equal(s.settled, 2); + assert.equal(s.totalPrem, (45 - 37) + (65 - 12)); + // closed-early with no assignment counts as premium kept + assert.equal(s.returnRate, 100); + }); + it('computes portfolioAPR as notional-weighted average of annual', () => { // notional = strike * size // row1: notional=1000, annual=40 → weight contrib = 40000 @@ -65,7 +78,7 @@ describe('calcPremiumStats', () => { it('returns correct shape with expected keys', () => { const s = calcPremiumStats([]); - const expected = ['totalPrem', 'totalNotional', 'totalCount', 'otmCount', 'itmCount', 'openCount', 'settled', 'returnRate', 'portfolioAPR']; + const expected = ['totalPrem', 'totalNotional', 'totalCount', 'otmCount', 'itmCount', 'closedCount', 'openCount', 'settled', 'returnRate', 'portfolioAPR']; for (const k of expected) { assert.ok(Object.prototype.hasOwnProperty.call(s, k), `missing key: ${k}`); }