From 84e4d1ff752816447e20463140bd12cae50a7040 Mon Sep 17 00:00:00 2001 From: heyitsStylez Date: Sat, 16 May 2026 10:43:46 +0800 Subject: [PATCH] Extract buildDisplaySeries from rCpnlChart into 05b-pnl.js Moves the inline zero-baseline prepend, period-filter slice, and today-extension logic out of rCpnlChart into a pure, dual-exported function. rCpnlChart is now a single call site. Adds 4 unit tests. Updates architecture diagram. Closes #57. Co-Authored-By: Claude Sonnet 4.6 --- docs/architecture.md | 1 + src/js/05b-pnl.js | 35 +++++++++++++++++++++++++- src/js/07-render-charts.js | 37 ++------------------------- test/unit/pnl.test.js | 51 ++++++++++++++++++++++++++++++++++++++ 4 files changed, 88 insertions(+), 36 deletions(-) diff --git a/docs/architecture.md b/docs/architecture.md index 3dacc89..5147d35 100644 --- a/docs/architecture.md +++ b/docs/architecture.md @@ -63,6 +63,7 @@ flowchart TD subgraph PnL ["P&L — 05b-pnl.js"] CP["computePnl(trades, assetFilter, livePrices)\n→ { realised, unrealised, total,\n missingSpotAssets,\n realisedSeries, realisedByMonth }"] + BDS["buildDisplaySeries(series, period, today)\n→ [{date, val}]"] end subgraph Stats ["Stats — 05d-calc-stats.js"] diff --git a/src/js/05b-pnl.js b/src/js/05b-pnl.js index eb2017a..76b9935 100644 --- a/src/js/05b-pnl.js +++ b/src/js/05b-pnl.js @@ -80,6 +80,39 @@ function computePnl(trades, assetFilter, livePrices) { return { realised, unrealised, total, missingSpotAssets, realisedSeries, realisedByMonth }; } +function buildDisplaySeries(series, period, today) { + if (!series.length) return []; + + const allSeries = [{ date: series[0].date, val: 0 }, ...series]; + + let dispSeries; + if (period === 'ALL') { + dispSeries = allSeries; + } else { + const days = period === '1M' ? 30 : 90; + const cutDate = new Date(today + 'T12:00:00'); + cutDate.setDate(cutDate.getDate() - days); + const cutStr = cutDate.toISOString().slice(0, 10); + + const lastBefore = allSeries.filter(p => p.date < cutStr); + const baseline = lastBefore.length ? lastBefore[lastBefore.length - 1].val : 0; + const inPeriod = allSeries.filter(p => p.date >= cutStr); + + if (!inPeriod.length) { + dispSeries = [{ date: cutStr, val: baseline }, { date: today, val: baseline }]; + } else { + dispSeries = [{ date: cutStr, val: baseline }, ...inPeriod]; + } + } + + const last = dispSeries[dispSeries.length - 1]; + if (last.date < today) { + dispSeries = [...dispSeries, { date: today, val: last.val }]; + } + + return dispSeries; +} + if (typeof module !== 'undefined' && module.exports) { - module.exports = { computePnl }; + module.exports = { computePnl, buildDisplaySeries }; } diff --git a/src/js/07-render-charts.js b/src/js/07-render-charts.js index 369b559..eb44832 100644 --- a/src/js/07-render-charts.js +++ b/src/js/07-render-charts.js @@ -65,42 +65,9 @@ function rCpnlChart() { return; } - // Period filter cutoff - const today = new Date(); - let cutoff = null; - if (sCpnlPeriod === '1M') { - cutoff = new Date(today); cutoff.setDate(cutoff.getDate() - 30); - } else if (sCpnlPeriod === '3M') { - cutoff = new Date(today); cutoff.setDate(cutoff.getDate() - 90); - } - - // Cumulative series: prepend a zero-baseline so the first point starts at 0. - const allSeries = [{ date: realisedSeries[0].date, val: 0 }, ...realisedSeries]; + const todayStr = today(); const totalPnl = realisedSeries[realisedSeries.length - 1].val; - - // Filter series for display period - let dispSeries; - if (!cutoff) { - dispSeries = allSeries; - } else { - const cutStr = cutoff.toISOString().slice(0, 10); - let baseline = 0; - let lastBefore = allSeries.filter(p => p.date < cutStr); - if (lastBefore.length) baseline = lastBefore[lastBefore.length - 1].val; - const inPeriod = allSeries.filter(p => p.date >= cutStr); - if (!inPeriod.length) { - dispSeries = [{ date: cutStr, val: totalPnl }, { date: today.toISOString().slice(0, 10), val: totalPnl }]; - } else { - dispSeries = [{ date: cutStr, val: baseline }, ...inPeriod]; - } - } - - // Add today as the final point (carries last value forward) - const todayStr = today.toISOString().slice(0, 10); - const last = dispSeries[dispSeries.length - 1]; - if (last.date < todayStr) { - dispSeries = [...dispSeries, { date: todayStr, val: last.val }]; - } + const dispSeries = buildDisplaySeries(realisedSeries, sCpnlPeriod, todayStr); // Period change const periodStart = dispSeries[0].val; diff --git a/test/unit/pnl.test.js b/test/unit/pnl.test.js index dd0e34e..21f95f8 100644 --- a/test/unit/pnl.test.js +++ b/test/unit/pnl.test.js @@ -242,3 +242,54 @@ test('realisedSeries: CALLED event contributes premium AND capital gain at expir assert.strictEqual(last.date, '2026-02-15'); assert.strictEqual(last.val, 550); }); + +// buildDisplaySeries tests +const { buildDisplaySeries } = require('../../src/js/05b-pnl.js'); + +test('buildDisplaySeries: empty input returns empty', () => { + assert.deepStrictEqual(buildDisplaySeries([], 'ALL', '2026-05-16'), []); +}); + +test("buildDisplaySeries: ALL prepends zero-baseline and appends today", () => { + const series = [ + { date: '2026-01-15', val: 100 }, + { date: '2026-03-01', val: 250 }, + ]; + const result = buildDisplaySeries(series, 'ALL', '2026-05-16'); + assert.deepStrictEqual(result, [ + { date: '2026-01-15', val: 0 }, + { date: '2026-01-15', val: 100 }, + { date: '2026-03-01', val: 250 }, + { date: '2026-05-16', val: 250 }, + ]); +}); + +test("buildDisplaySeries: 1M with no in-range points returns flat [{cutoff,lastVal},{today,lastVal}]", () => { + // All trades before the 30-day window + const series = [{ date: '2026-01-15', val: 100 }]; + // today=2026-05-16 → cutoff=2026-04-16; series point is before cutoff + const result = buildDisplaySeries(series, '1M', '2026-05-16'); + assert.strictEqual(result.length, 2); + assert.strictEqual(result[0].date, '2026-04-16'); + assert.strictEqual(result[0].val, 100); + assert.strictEqual(result[1].date, '2026-05-16'); + assert.strictEqual(result[1].val, 100); +}); + +test("buildDisplaySeries: baseline carry-forward is last point before the cutoff", () => { + // today=2026-05-16 → 1M cutoff=2026-04-16 + // Two points before cutoff; baseline should be 250 (the later one) + // One point after cutoff at 300 + const series = [ + { date: '2026-01-15', val: 100 }, + { date: '2026-03-01', val: 250 }, + { date: '2026-04-20', val: 300 }, + ]; + const result = buildDisplaySeries(series, '1M', '2026-05-16'); + // dispSeries: [{cutoff, 250}, {2026-04-20, 300}], then today appended + assert.deepStrictEqual(result, [ + { date: '2026-04-16', val: 250 }, + { date: '2026-04-20', val: 300 }, + { date: '2026-05-16', val: 300 }, + ]); +});