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
11 changes: 8 additions & 3 deletions src/css/styles.css
Original file line number Diff line number Diff line change
Expand Up @@ -286,8 +286,14 @@ tr.lot-child.la-sol:hover > td{background:rgba(153,69,255,.08)}
.ppnl-tabs{display:flex;gap:4px}
.ppnl-tab{font-family:var(--mono);font-size:.65rem;letter-spacing:1px;cursor:pointer;background:transparent;border:1px solid var(--bd);color:var(--mu);padding:4px 10px;transition:all .15s}
.ppnl-tab.active{border-color:var(--green);color:var(--green)}
.ppnl-cards{padding:16px 20px 20px;display:grid;grid-template-columns:repeat(3,minmax(0,340px));gap:12px;justify-content:center}
.ppnl-layout{padding:16px 20px 20px;display:grid;grid-template-columns:1.4fr 1fr;gap:14px;align-items:stretch}
.ppnl-card{background:var(--s2);border:1px solid var(--bd);border-radius:10px;padding:14px 16px;position:relative}
.ppnl-trio{display:flex;flex-direction:column;gap:10px}
.ppnl-trio .ppnl-card{flex:1;display:flex;flex-direction:column;justify-content:center}
.ppnl-hero{background:linear-gradient(135deg,var(--gd),transparent 65%),var(--s2);border-color:var(--gb);padding:24px 28px;display:flex;flex-direction:column;justify-content:center}
.ppnl-hero .ppnl-lbl{font-size:.65rem;margin-bottom:14px}
.ppnl-hero .ppnl-main{font-size:2.2rem;letter-spacing:-1px;color:var(--green)}
.ppnl-hero .ppnl-sub{font-size:.72rem;margin-top:8px}

/* Generic styled-popover tooltip. Opt in by adding class="has-tip" + data-tip="…" */
.has-tip{position:relative;cursor:help}
Expand All @@ -308,8 +314,7 @@ tr.lot-child.la-sol:hover > td{background:rgba(153,69,255,.08)}
.ppnl-mtbl tr:last-child td{border-bottom:none}
.ppnl-mtbl .rate-hi{color:var(--green)}
.ppnl-mtbl .rate-lo{color:var(--red)}
@media(max-width:600px){.ppnl-cards{grid-template-columns:1fr 1fr}}
@media(max-width:460px){.ppnl-cards{grid-template-columns:1fr}}
@media(max-width:600px){.ppnl-layout{grid-template-columns:1fr}.ppnl-hero .ppnl-main{font-size:1.8rem}}

/* EMPTY */
.empty{text-align:center;padding:64px 24px;display:flex;flex-direction:column;align-items:center;gap:16px}
Expand Down
41 changes: 22 additions & 19 deletions src/js/07-render-charts.js
Original file line number Diff line number Diff line change
Expand Up @@ -445,33 +445,36 @@ function rCharts(displayRows, lots) {

if (sPpnlTab === 'total') {
const s = calcStats(displayRows);
function card(label, main, sub, tip) {
function tile(extraClass, label, main, sub, tip) {
const tipAttr = tip ? ' data-tip="' + tip.replace(/"/g, '"') + '"' : '';
return '<div class="ppnl-card' + (tip ? ' has-tip' : '') + '"' + tipAttr + '>' +
const cls = 'ppnl-card' + (extraClass ? ' ' + extraClass : '') + (tip ? ' has-tip' : '');
return '<div class="' + cls + '"' + tipAttr + '>' +
'<div class="ppnl-lbl">' + label + (tip ? ' <span class="ppnl-tip-ico" aria-hidden="true">&#9432;</span>' : '') + '</div>' +
'<div class="ppnl-main">' + main + '</div>' +
(sub ? '<div class="ppnl-sub">' + sub + '</div>' : '') +
'</div>';
}
el.className = 'ppnl-cards';
el.innerHTML = [
card('Total Premium Collected',
el.className = 'ppnl-layout';
el.innerHTML =
tile('ppnl-hero',
'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.'),
card('Total Notional',
s.totalNotional > 0 ? '$' + fmt(s.totalNotional) : dash,
s.totalCount > 0 ? pos(s.totalCount) + (s.openCount > 0 ? ' · ' + s.openCount + ' open' : '') : '',
'Total Notional = Σ strike × size across every option (settled and open). The capital you would tie up if every put were assigned at strike.'),
card('Portfolio APR',
s.portfolioAPR !== null ? s.portfolioAPR.toFixed(1) + '%' : dash,
s.settled > 0 ? 'notional-weighted · ' + s.settled + ' settled' : '',
'Notional-weighted average APR of settled options. Per-option APR = (netPrem / collateral) / DTE × 365. Open options excluded.'),
card('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.'),
].join('');
'Sum of every option premium collected (gross of buy-to-close costs). Includes settled and open positions.') +
'<div class="ppnl-trio">' +
tile('', 'Total Notional',
s.totalNotional > 0 ? '$' + fmt(s.totalNotional) : dash,
s.totalCount > 0 ? pos(s.totalCount) + (s.openCount > 0 ? ' · ' + s.openCount + ' open' : '') : '',
'Total Notional = Σ strike × size across every option (settled and open). The capital you would tie up if every put were assigned at strike.') +
tile('', 'Portfolio APR',
s.portfolioAPR !== null ? s.portfolioAPR.toFixed(1) + '%' : dash,
s.settled > 0 ? 'notional-weighted · ' + s.settled + ' settled' : '',
'Notional-weighted average APR of settled options. Per-option APR = (netPrem / collateral) / DTE × 365. Open options excluded.') +
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.') +
'</div>';

} else {
// Group trades by month — OPEN trades by open date, settled by expiry date
Expand Down
29 changes: 29 additions & 0 deletions test/integration/pnl-tiles.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -72,6 +72,35 @@ test('Return Rate tile has tooltip + ⓘ glyph', (t) => {
assertHasTooltip(findCard(window, /Return Rate/i), /OTM|expired/i);
});

test('Total tab uses hero + supporting trio layout (issue #42)', (t) => {
// Total Premium Collected is the hero (focal point); the other three live in a trio container.
const trades = [
{ id: 1, asset: 'BTC', type: 'PUT', date: '2026-01-01', expiry: '2026-01-15',
dte: 14, strike: 50000, size: 0.1, premium: 120, outcome: 'EXPIRED',
closeCost: 0, platform: 'RYSK' },
];
const { window, teardown } = setupJsdom({ trades });
t.after(teardown);

const hero = window.document.querySelector('.ppnl-hero');
assert.ok(hero, 'expected a .ppnl-hero element on the Total tab');
const heroLbl = hero.querySelector('.ppnl-lbl');
assert.match(heroLbl.textContent, /Total Premium Collected/i,
`hero should be Total Premium Collected, got "${heroLbl.textContent}"`);

const trio = window.document.querySelector('.ppnl-trio');
assert.ok(trio, 'expected a .ppnl-trio container');
const trioLabels = Array.from(trio.querySelectorAll('.ppnl-lbl')).map(l => l.textContent.trim());
assert.strictEqual(trioLabels.length, 3, `trio should hold 3 tiles, got ${trioLabels.length}`);
assert.ok(trioLabels.some(l => /Total Notional/i.test(l)));
assert.ok(trioLabels.some(l => /Portfolio APR/i.test(l)));
assert.ok(trioLabels.some(l => /Return Rate/i.test(l)));

// Hero retains its tooltip + ⓘ glyph affordance.
assert.ok(hero.classList.contains('has-tip'), 'hero should keep has-tip styling');
assert.ok(hero.querySelector('.ppnl-tip-ico'), 'hero should keep the ⓘ glyph');
});

test('Total tab no longer renders Realised/Unrealised/Total P&L cards (issue #40)', (t) => {
// Per #40, the hero band is the canonical Realised/Unrealised/Total surface.
// The Total tab must not duplicate them.
Expand Down
Loading