Skip to content

Commit 6c98e98

Browse files
heyitsStylezclaude
andcommitted
Remove APR duplication in renderExpiryTable — use enriched rows from compute()
renderExpiryTable now receives allRows (enriched by compute) via rStats instead of filtering raw trades[]. Uses r.annual directly and _liveDte for the DTE label, eliminating the duplicate inline APR formula. Removes the dead `else renderExpiryTable()` branch in fetchExpiryPrices. Closes #58. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
1 parent 7e3ade7 commit 6c98e98

5 files changed

Lines changed: 92 additions & 35 deletions

File tree

CLAUDE.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -56,7 +56,7 @@ globals, and 17-boot.js runs an IIFE last to bootstrap the app.
5656
| `05a-merge-open-lots.js` | 113 | `mergeOpenLots(trades, asset)``trades'`. Pure helper that merges all open lots for one asset (size-weighted `costBasis`, summed `lotPremiums`, earliest opener kept, CALL `lotNum` cleared). Prefers `lotEngine`, falls back to `compute` or a HOLDING/ASSIGNED heuristic for Node tests |
5757
| `05b-pnl.js` | 90 | `computePnl(trades, assetFilter, livePrices)``{ realised, unrealised, total, missingSpotAssets, realisedSeries, realisedByMonth }`. Cash-flow-lens P&L calculator. Realised: `Σ settled netPrem + Σ (strike − costBasis) × calledSize` (open contributions are zero). Unrealised: `Σ over open lots of (spot − costBasis) × size`, marked against raw `costBasis` (never `netCost`); assets missing spot are excluded from the sum and reported in `missingSpotAssets`. Total = Realised + Unrealised. HOLDING- and ASSIGNED-originated lots are treated symmetrically in both paths. Pure; dual-exported. ADR: `docs/adr/0003-pnl-cash-flow-lens.md` |
5858
| `05c-outcome-distribution.js` | 32 | `outcomeDistribution(trades, assetFilter)``[{outcome, count, premium}]`. Pure helper for the Position History outcome treemap. Excludes OPEN, orders EXPIRED/ASSIGNED/CALLED/CLOSED, nets `closeCost` from CLOSED premium (cash-flow lens). Dual-exported |
59-
| `06-render-table.js` | 452 | `sortOpen/sortHist`, `renderExpiryTable` (today badge + mobile cards), `fetchExpiryPrices` (CoinGecko, calls full `render()` on success), `rTable` (holdings cards, open & history tables, history filter application), `rStats` (just delegates to `renderExpiryTable`), `exportHistoryCSV` (downloads filtered history as CSV) |
59+
| `06-render-table.js` | 452 | `sortOpen/sortHist`, `renderExpiryTable(allRows)` (today badge + mobile cards — reads enriched rows from `compute`, uses `r.annual` and `_liveDte`; no longer reads raw `trades[]`), `fetchExpiryPrices` (CoinGecko, calls `render()` on success), `rTable` (holdings cards, open & history tables, history filter application), `rStats(streams, lots, allRows, displayRows)` (forwards `allRows` to `renderExpiryTable`), `exportHistoryCSV` (downloads filtered history as CSV) |
6060
| `06a-render-outcome-chart.js` | 75 | `rOutcomeChart()` — renders a horizontal treemap of `outcomeDistribution` into `#hist-outchart` when ≥10 settled trades; otherwise hides itself and shows `#hist-pills`. Each cell click toggles `setHistOutcome`. Cells coloured via CSS vars (EXPIRED/ASSIGNED/CALLED/CLOSED → green/red/orange/blue) |
6161
| `07-render-charts.js` | 640 | `setCpnlPeriod` (1M/3M/ALL), `rCpnlChart` (cumulative Realised P&L hero — sources `realisedSeries` from `computePnl` — plus secondary Realised sparkline), `rCharts` (Premium P&L total/monthly tabs — Total tab consumes `computePnl` for the Realised tile), `cOpts` (Chart.js options factory) |
6262
| `08-render.js` | 8 | `render()` — orchestrator: `compute → rStats → rTable → rOutcomeChart → rCharts` |

docs/architecture.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -80,7 +80,7 @@ flowchart TD
8080
LS -->|load on boot| Trades
8181
Trades -->|per-asset slice| LE
8282
LE -->|lots + accounting| CO
83-
CO -->|displayRows| RT
83+
CO -->|allRows + displayRows| RT
8484
CO -->|streams| RC
8585
Trades -->|all trades| CP
8686
CP -->|realisedSeries\nrealised + unrealised| RC

src/js/06-render-table.js

Lines changed: 26 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -133,11 +133,11 @@ function _histRow(r) {
133133
+ '</tr>';
134134
}
135135

136-
function rStats(streams, lots, displayRows) {
137-
renderExpiryTable();
136+
function rStats(streams, lots, allRows, displayRows) {
137+
renderExpiryTable(allRows);
138138
}
139139

140-
function renderExpiryTable() {
140+
function renderExpiryTable(allRows) {
141141
const wrap = document.getElementById('expiry-table-wrap');
142142
if (!wrap) return;
143143

@@ -146,12 +146,12 @@ function renderExpiryTable() {
146146
const weekOut = new Date(today);
147147
weekOut.setDate(weekOut.getDate() + 7);
148148

149-
const expiring = trades.filter(t => {
150-
if (t.outcome !== 'OPEN') return false;
151-
if (t.type !== 'PUT' && t.type !== 'CALL') return false;
152-
if (sFilter !== 'ALL' && t.asset !== sFilter) return false;
153-
if (!t.expiry) return false;
154-
const exp = new Date(t.expiry + 'T00:00:00');
149+
const expiring = (allRows || []).filter(r => {
150+
if (r.outcome !== 'OPEN') return false;
151+
if (r.type !== 'PUT' && r.type !== 'CALL') return false;
152+
if (sFilter !== 'ALL' && r.asset !== sFilter) return false;
153+
if (!r.expiry) return false;
154+
const exp = new Date(r.expiry + 'T00:00:00');
155155
return exp <= weekOut;
156156
}).sort((a, b) => new Date(a.expiry) - new Date(b.expiry) || a.asset.localeCompare(b.asset));
157157

@@ -177,38 +177,32 @@ function renderExpiryTable() {
177177

178178
const assetCol = { BTC: 'btc', ETH: 'eth', HYPE: 'hype', SOL: 'sol' };
179179

180-
const enriched = expiring.map(t => {
181-
const exp = new Date(t.expiry + 'T00:00:00');
182-
const daysLeft = Math.round((exp - today) / (1000 * 60 * 60 * 24));
183-
const dteLabel = daysLeft <= 0
184-
? '<span style="color:var(--red);font-weight:700">today</span>'
185-
: daysLeft + 'd';
186-
let aprHtml = '—';
187-
if (t.dte > 0 && t.strike > 0 && t.size > 0) {
188-
const ann = (t.premium / (t.strike * t.size)) * (365 / t.dte) * 100;
189-
aprHtml = ann.toFixed(1) + '%';
190-
}
180+
const enriched = expiring.map(r => {
181+
const exp = new Date(r.expiry + 'T00:00:00');
182+
const daysLeft = Math.round((exp - today) / 86400000);
183+
const dteLabel = _liveDte(r.expiry);
184+
const aprHtml = r.annual != null ? r.annual.toFixed(1) + '%' : '—';
191185
let statusHtml = '<span style="color:var(--mu)">—</span>';
192-
const spot = livePrices[t.asset];
186+
const spot = livePrices[r.asset];
193187
if (spot) {
194-
const isPut = t.type === 'PUT';
195-
const isOTM = isPut ? spot > t.strike : spot < t.strike;
196-
const pct = Math.abs((spot - t.strike) / spot * 100).toFixed(1);
188+
const isPut = r.type === 'PUT';
189+
const isOTM = isPut ? spot > r.strike : spot < r.strike;
190+
const pct = Math.abs((spot - r.strike) / spot * 100).toFixed(1);
197191
statusHtml = isOTM
198192
? '<span class="exp-otm">OTM ' + pct + '%</span>'
199193
: '<span class="exp-itm">ITM ' + pct + '%</span>';
200194
}
201-
const col = assetCol[t.asset] || 'mu2';
202-
const platBadge = (t.platform === 'HSFC')
195+
const col = assetCol[r.asset] || 'mu2';
196+
const platBadge = (r.platform === 'HSFC')
203197
? '<span class="bplat bplat-hsfc">HSFC</span>'
204198
: '<span class="bplat bplat-rysk">RYSK</span>';
205199
const actionsHtml = '<div class="row-actions">'
206-
+ '<button class="btn-qa btn-qa-exp" onclick="quickOutcome(' + t.id + ',\'EXPIRED\')" title="Mark expired">Exp \u2713</button>'
207-
+ (t.type === 'CALL'
208-
? '<button class="btn-qa btn-qa-cal" onclick="quickOutcome(' + t.id + ',\'CALLED\')" title="Mark called away">Called \u2191</button>'
209-
: '<button class="btn-qa btn-qa-asg" onclick="quickOutcome(' + t.id + ',\'ASSIGNED\')" title="Mark assigned">Asgn \u2193</button>')
200+
+ '<button class="btn-qa btn-qa-exp" onclick="quickOutcome(' + r.id + ',\'EXPIRED\')" title="Mark expired">Exp </button>'
201+
+ (r.type === 'CALL'
202+
? '<button class="btn-qa btn-qa-cal" onclick="quickOutcome(' + r.id + ',\'CALLED\')" title="Mark called away">Called </button>'
203+
: '<button class="btn-qa btn-qa-asg" onclick="quickOutcome(' + r.id + ',\'ASSIGNED\')" title="Mark assigned">Asgn </button>')
210204
+ '</div>';
211-
return { t, col, dteLabel, aprHtml, statusHtml, platBadge, actionsHtml, daysLeft };
205+
return { t: r, col, dteLabel, aprHtml, statusHtml, platBadge, actionsHtml, daysLeft };
212206
});
213207

214208
const rows = enriched.map(e => {
@@ -267,7 +261,7 @@ function fetchExpiryPrices() {
267261
const el = document.getElementById('expiry-last-refreshed');
268262
if (el) { const n = new Date(); el.textContent = 'refreshed ' + String(n.getUTCHours()).padStart(2,'0') + ':' + String(n.getUTCMinutes()).padStart(2,'0') + ' UTC'; }
269263
// Re-render whole page so holdings cards pick up live spot too
270-
if (typeof render === 'function') render(); else renderExpiryTable();
264+
render();
271265
})
272266
.catch(() => { /* silently fail — table shows — for status */ });
273267
}

src/js/08-render.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
// ── RENDER
22
function render() {
33
const { streams, lots, allRows, displayRows } = compute(sFilter);
4-
rStats(streams, lots, displayRows);
4+
rStats(streams, lots, allRows, displayRows);
55
rTable(displayRows, streams, lots);
66
rOutcomeChart();
77
rCharts(displayRows, lots);
Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
const test = require('node:test');
2+
const assert = require('node:assert');
3+
const { setupJsdom } = require('../helpers/setupJsdom');
4+
5+
function isoDaysFromToday(days) {
6+
const d = new Date();
7+
d.setHours(0, 0, 0, 0);
8+
d.setDate(d.getDate() + days);
9+
const yyyy = d.getFullYear();
10+
const mm = String(d.getMonth() + 1).padStart(2, '0');
11+
const dd = String(d.getDate()).padStart(2, '0');
12+
return yyyy + '-' + mm + '-' + dd;
13+
}
14+
15+
test('Expiring This Week APR matches Open Positions APR for the same trade', (t) => {
16+
// dte=21, premium=150, strike=50000, size=0.05
17+
// annual = (150 / (50000 * 0.05)) * (365 / 21) * 100 ≈ 104.8%
18+
const openPut = {
19+
id: 1, asset: 'BTC', type: 'PUT',
20+
date: '2026-01-01', expiry: isoDaysFromToday(3),
21+
dte: 21, strike: 50000, size: 0.05, premium: 150,
22+
outcome: 'OPEN', closeCost: 0, platform: 'RYSK',
23+
};
24+
const { window, teardown } = setupJsdom({ trades: [openPut] });
25+
t.after(teardown);
26+
27+
// Get APR from Open Positions table (column index 9 = APR: Asset,Platform,Date,Expiry,DTE,Type,Strike,Size,Premium,APR)
28+
const openCells = window.document.querySelectorAll('#ttbody-open tr td');
29+
const openApr = openCells[9].textContent.trim();
30+
31+
// Get APR from Expiring This Week table (column index 7 = APR)
32+
const expRows = window.document.querySelectorAll('.expiry-tbl tbody tr td');
33+
const expiryApr = expRows[7].textContent.trim();
34+
35+
assert.ok(openApr.length > 0, 'open positions APR should not be empty');
36+
assert.strictEqual(expiryApr, openApr, 'Expiring This Week APR must match Open Positions APR');
37+
});
38+
39+
test('Expiring This Week respects asset filter', (t) => {
40+
const btcPut = {
41+
id: 1, asset: 'BTC', type: 'PUT',
42+
date: '2026-01-01', expiry: isoDaysFromToday(3),
43+
dte: 21, strike: 50000, size: 0.05, premium: 100,
44+
outcome: 'OPEN', closeCost: 0, platform: 'RYSK',
45+
};
46+
const ethPut = {
47+
id: 2, asset: 'ETH', type: 'PUT',
48+
date: '2026-01-01', expiry: isoDaysFromToday(4),
49+
dte: 21, strike: 3000, size: 0.5, premium: 50,
50+
outcome: 'OPEN', closeCost: 0, platform: 'RYSK',
51+
};
52+
const { window, teardown } = setupJsdom({ trades: [btcPut, ethPut] });
53+
t.after(teardown);
54+
55+
// Apply ETH filter
56+
window.setFilter('ETH');
57+
window.render();
58+
59+
const rows = window.document.querySelectorAll('.expiry-tbl tbody tr');
60+
assert.strictEqual(rows.length, 1, 'only 1 row should appear with ETH filter');
61+
const assetBadge = rows[0].querySelector('.badge').textContent.trim();
62+
assert.strictEqual(assetBadge, 'ETH', 'the remaining row should be ETH');
63+
});

0 commit comments

Comments
 (0)