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
1 change: 1 addition & 0 deletions docs/architecture.md
Original file line number Diff line number Diff line change
Expand Up @@ -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"]
Expand Down
35 changes: 34 additions & 1 deletion src/js/05b-pnl.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 };
}
37 changes: 2 additions & 35 deletions src/js/07-render-charts.js
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
51 changes: 51 additions & 0 deletions test/unit/pnl.test.js
Original file line number Diff line number Diff line change
Expand Up @@ -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 },
]);
});
Loading