Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions src/js/05d-calc-stats.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
function calcPremiumStats(rows) {
let totalPrem = 0, totalNotional = 0, totalCount = 0;
let otmCount = 0, itmCount = 0, openCount = 0;
let aprWeightedSum = 0, aprWeightTotal = 0;

rows.forEach(r => {
if (r.type === 'HOLDING') return;
const net = (r.premium || 0) - (r.closeCost || 0);
const notional = (r.strike || 0) * (r.size || 0);
totalPrem += net;
totalNotional += notional;
totalCount++;
if (r.outcome === 'OPEN') { openCount++; }
else if (r.outcome === 'EXPIRED') { otmCount++; }
else if (r.outcome === 'ASSIGNED' || r.outcome === 'CALLED') { itmCount++; }
if (r.annual != null) {
aprWeightedSum += r.annual * notional;
aprWeightTotal += notional;
}
});

const settled = otmCount + itmCount;
const returnRate = settled > 0 ? otmCount / settled * 100 : null;
const portfolioAPR = aprWeightTotal > 0 ? aprWeightedSum / aprWeightTotal : null;

return { totalPrem, totalNotional, totalCount, otmCount, itmCount, openCount, settled, returnRate, portfolioAPR };
}

if (typeof module !== 'undefined' && module.exports) {
module.exports = { calcPremiumStats };
}
33 changes: 2 additions & 31 deletions src/js/07-render-charts.js
Original file line number Diff line number Diff line change
Expand Up @@ -405,35 +405,6 @@ function rCharts(displayRows, lots) {
const el = document.getElementById('ppnl-body');
if (!el) return;

function calcStats(rows) {
let totalPrem = 0, totalCount = 0;
let otmCount = 0, itmCount = 0;
let openCount = 0;
let aprWeightedSum = 0, aprWeightTotal = 0;
let assignmentLoss = 0, callAwayCredit = 0, totalNotional = 0;
rows.forEach(r => {
if (r.type === 'HOLDING') return;
const net = (r.premium || 0) - (r.closeCost || 0);
const notional = (r.strike || 0) * (r.size || 0);
totalPrem += net;
totalNotional += notional;
totalCount++;
if (r.outcome === 'OPEN') { openCount++; }
else if (r.outcome === 'EXPIRED') otmCount++;
else if (r.outcome === 'ASSIGNED') { itmCount++; assignmentLoss += notional; }
else if (r.outcome === 'CALLED') { itmCount++; callAwayCredit += notional; }
if (r.annual != null) {
aprWeightedSum += r.annual * notional;
aprWeightTotal += notional;
}
});
const settled = otmCount + itmCount;
const returnRate = settled > 0 ? otmCount / settled * 100 : null;
const portfolioAPR = aprWeightTotal > 0 ? aprWeightedSum / aprWeightTotal : null;
const netPnl = totalPrem - assignmentLoss + callAwayCredit;
return { totalPrem, totalCount, otmCount, itmCount, openCount, returnRate, settled, portfolioAPR, assignmentLoss, callAwayCredit, netPnl, totalNotional };
}

const pos = n => n === 1 ? '1 position' : n + ' positions';
const asgn = n => n === 1 ? '1 assignment' : n + ' assignments';
const dash = '—';
Expand All @@ -444,7 +415,7 @@ function rCharts(displayRows, lots) {
}

if (sPpnlTab === 'total') {
const s = calcStats(displayRows);
const s = calcPremiumStats(displayRows);
function tile(extraClass, label, main, sub, tip) {
const tipAttr = tip ? ' data-tip="' + tip.replace(/"/g, '"') + '"' : '';
const cls = 'ppnl-card' + (extraClass ? ' ' + extraClass : '') + (tip ? ' has-tip' : '');
Expand Down Expand Up @@ -497,7 +468,7 @@ function rCharts(displayRows, lots) {
}

const rows = months.map(ym => {
const s = calcStats(monthMap[ym]);
const s = calcPremiumStats(monthMap[ym]);
const rateClass = s.returnRate === null ? '' : s.returnRate >= 70 ? ' class="rate-hi"' : s.returnRate < 50 ? ' class="rate-lo"' : '';
const realisedM = realisedByMonth[ym];
const hasRealised = realisedM !== undefined;
Expand Down
77 changes: 77 additions & 0 deletions test/unit/calc-stats.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
const { describe, it, before } = require('node:test');
const assert = require('node:assert/strict');

// Wire up globals needed by the dual-export require chain
global.trades = [];
global.livePrices = {};

const { calcPremiumStats } = require('../../src/js/05d-calc-stats.js');

function makeRow(overrides) {
return Object.assign({
type: 'PUT',
outcome: 'EXPIRED',
premium: 100,
closeCost: 0,
strike: 1000,
size: 1,
annual: 50,
}, overrides);
}

describe('calcPremiumStats', () => {
it('excludes HOLDING trades from all counts', () => {
const rows = [
makeRow({ type: 'HOLDING', premium: 0, strike: 1000, size: 1 }),
];
const s = calcPremiumStats(rows);
assert.equal(s.totalCount, 0);
assert.equal(s.totalPrem, 0);
assert.equal(s.totalNotional, 0);
});

it('returns null returnRate when no settled trades', () => {
const rows = [makeRow({ outcome: 'OPEN' })];
const s = calcPremiumStats(rows);
assert.equal(s.returnRate, null);
assert.equal(s.settled, 0);
assert.equal(s.openCount, 1);
});

it('returns 100% returnRate when all settled trades expired OTM', () => {
const rows = [
makeRow({ outcome: 'EXPIRED' }),
makeRow({ outcome: 'EXPIRED' }),
];
const s = calcPremiumStats(rows);
assert.equal(s.returnRate, 100);
assert.equal(s.otmCount, 2);
assert.equal(s.itmCount, 0);
assert.equal(s.settled, 2);
});

it('computes portfolioAPR as notional-weighted average of annual', () => {
// notional = strike * size
// row1: notional=1000, annual=40 → weight contrib = 40000
// row2: notional=2000, annual=60 → weight contrib = 120000
// weighted avg = 160000 / 3000 ≈ 53.333...
const rows = [
makeRow({ outcome: 'EXPIRED', strike: 1000, size: 1, annual: 40 }),
makeRow({ outcome: 'EXPIRED', strike: 1000, size: 2, annual: 60 }),
];
const s = calcPremiumStats(rows);
assert.ok(Math.abs(s.portfolioAPR - (40 * 1000 + 60 * 2000) / 3000) < 0.001);
});

it('returns correct shape with expected keys', () => {
const s = calcPremiumStats([]);
const expected = ['totalPrem', 'totalNotional', 'totalCount', 'otmCount', 'itmCount', 'openCount', 'settled', 'returnRate', 'portfolioAPR'];
for (const k of expected) {
assert.ok(Object.prototype.hasOwnProperty.call(s, k), `missing key: ${k}`);
}
// dead fields must not be present
assert.ok(!Object.prototype.hasOwnProperty.call(s, 'assignmentLoss'));
assert.ok(!Object.prototype.hasOwnProperty.call(s, 'callAwayCredit'));
assert.ok(!Object.prototype.hasOwnProperty.call(s, 'netPnl'));
});
});
Loading