diff --git a/CLAUDE.md b/CLAUDE.md index d294b7a..aeb6b98 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -48,7 +48,7 @@ globals, and 17-boot.js runs an IIFE last to bootstrap the app. | `04b-lot-engine.js` | 140 | `lotNetCost(costBasis, lotPremiums, size)` and `lotEngine(assetTrades)` → `{lots, portfolioPnl, portfolioPremiums, putOnlyPnl, tradeAccounting}`. **Single source of truth** for wheel invariants (see Lot model below). Pure; dual-exported for Node. **Key invariant:** assigned-PUT premium IS credited to the new lot's `lotPremiums` | | `05-compute.js` | 89 | `compute(assetFilter)` → `{streams, lots, allRows, displayRows}`. Cross-asset orchestrator: per-asset grouping, calls `lotEngine`, applies asset filter, sorts, assigns idx, derives display fields (`returnPct`, `monthly`, `annual`, `lotPnl`). Dual-exports `compute` for Node tests (reads `trades`/`lotEngine` from globals — set them before `require`) | | `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 | -| `05b-pnl.js` | 90 | `computePnl(trades, assetFilter, livePrices)` → `{ realised, unrealised, total, missingSpotAssets, realisedSeries }`. 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` | +| `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` | | `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) | | `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) | | `08-render.js` | 7 | `render()` — orchestrator: `compute → rStats → rTable → rCharts` | diff --git a/src/js/05b-pnl.js b/src/js/05b-pnl.js index 4272765..eb2017a 100644 --- a/src/js/05b-pnl.js +++ b/src/js/05b-pnl.js @@ -26,6 +26,7 @@ function computePnl(trades, assetFilter, livePrices) { let unrealised = 0; const missingSpotAssets = []; const events = []; + const realisedByMonth = {}; Object.keys(byAsset).forEach(asset => { const assetTrades = byAsset[asset]; @@ -58,7 +59,10 @@ function computePnl(trades, assetFilter, livePrices) { delta += gain; } } - events.push({ date: t.expiry || t.date, delta }); + const evDate = t.expiry || t.date; + events.push({ date: evDate, delta }); + const ym = (evDate || '').slice(0, 7); + if (ym) realisedByMonth[ym] = (realisedByMonth[ym] || 0) + delta; }); }); @@ -73,7 +77,7 @@ function computePnl(trades, assetFilter, livePrices) { }); const total = realised + unrealised; - return { realised, unrealised, total, missingSpotAssets, realisedSeries }; + return { realised, unrealised, total, missingSpotAssets, realisedSeries, realisedByMonth }; } if (typeof module !== 'undefined' && module.exports) { diff --git a/src/js/07-render-charts.js b/src/js/07-render-charts.js index ec892f2..ec86680 100644 --- a/src/js/07-render-charts.js +++ b/src/js/07-render-charts.js @@ -627,6 +627,7 @@ function rCharts(displayRows, lots) { monthMap[ym].push(r); }); const months = Object.keys(monthMap).sort().reverse(); + const { realisedByMonth } = computePnl(trades, sFilter, livePrices); el.className = 'ppnl-mtbl-wrap'; if (!months.length) { @@ -637,9 +638,11 @@ function rCharts(displayRows, lots) { const rows = months.map(ym => { const s = calcStats(monthMap[ym]); const rateClass = s.returnRate === null ? '' : s.returnRate >= 70 ? ' class="rate-hi"' : s.returnRate < 50 ? ' class="rate-lo"' : ''; - const mnColor = s.netPnl >= 0 ? 'var(--green)' : 'var(--red)'; - const mnStr = s.totalCount > 0 - ? '' + (s.netPnl >= 0 ? '+$' : '-$') + fmt(Math.abs(s.netPnl)) + '' + const realisedM = realisedByMonth[ym]; + const hasRealised = realisedM !== undefined; + const mnColor = hasRealised && realisedM < 0 ? 'var(--red)' : 'var(--green)'; + const mnStr = hasRealised + ? '' + (realisedM >= 0 ? '+$' : '-$') + fmt(Math.abs(realisedM)) + '' : dash; return '