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
15 changes: 12 additions & 3 deletions src/js/06-render-table.js
Original file line number Diff line number Diff line change
Expand Up @@ -46,19 +46,28 @@ function _th(label, col, s, fn) {
function _openHeaders() {
const s = tSortOpen, fn = 'sortOpen';
return _th('Asset','asset',s,fn) + _th('Platform','platform',s,fn) + _th('Date','date',s,fn)
+ _th('Expiry','expiry',s,fn) + _th('DTE','dte',s,fn) + _th('Type','type',s,fn)
+ _th('Expiry','expiry',s,fn) + _th('DTE','expiry',s,fn) + _th('Type','type',s,fn)
+ _th('Strike','strike',s,fn) + _th('Size','size',s,fn) + _th('Premium','premium',s,fn)
+ _th('APR','annual',s,fn) + '<th></th>';
}

function _histHeaders() {
const s = tSortHist, fn = 'sortHist';
return _th('Asset','asset',s,fn) + _th('Platform','platform',s,fn) + _th('Date','date',s,fn)
+ _th('Expiry','expiry',s,fn) + _th('DTE','dte',s,fn) + _th('Type','type',s,fn)
+ _th('Expiry','expiry',s,fn) + _th('Term','dte',s,fn) + _th('Type','type',s,fn)
+ _th('Strike','strike',s,fn) + _th('Size','size',s,fn) + _th('Premium','premium',s,fn)
+ _th('APR','annual',s,fn) + _th('Outcome','outcome',s,fn) + '<th></th>';
}

function _liveDte(expiry) {
if (!expiry) return '&mdash;';
const today = new Date(); today.setHours(0, 0, 0, 0);
const exp = new Date(expiry + 'T00:00:00');
const days = Math.round((exp - today) / 86400000);
if (days <= 0) return '<span style="color:var(--red);font-weight:700">today</span>';
return days + 'd';
}

function _openRow(r) {
const assetCls = { BTC:'bbtc', ETH:'beth', HYPE:'bhype', SOL:'bsol' }[r.asset] || 'bbtc';
const isHolding = r.type === 'HOLDING';
Expand All @@ -81,7 +90,7 @@ function _openRow(r) {
+ '<td>' + platBadge + '</td>'
+ '<td class="mu" style="font-size:.72rem">' + r.date + '</td>'
+ '<td class="mu" style="font-size:.72rem">' + (isHolding ? '&mdash;' : (r.expiry || '&mdash;')) + '</td>'
+ '<td class="mu">' + (isHolding ? '&mdash;' : (r.dte || '&mdash;')) + '</td>'
+ '<td class="mu">' + (isHolding ? '&mdash;' : _liveDte(r.expiry)) + '</td>'
+ '<td>' + typeBadge + '</td>'
+ '<td>$' + fmt(r.strike) + (isHolding ? '<br><span style="font-size:.65rem;color:var(--mu)">cost basis</span>' : '') + '</td>'
+ '<td class="mu">' + r.size + ' ' + r.asset + '</td>'
Expand Down
91 changes: 91 additions & 0 deletions test/integration/dte-term-split.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
const test = require('node:test');
const assert = require('node:assert');
const { setupJsdom } = require('../helpers/setupJsdom');

function isoDaysFromToday(days) {
// Local-date string (not UTC) so it round-trips with `new Date(s + 'T00:00:00')`.
const d = new Date();
d.setHours(0, 0, 0, 0);
d.setDate(d.getDate() + days);
const yyyy = d.getFullYear();
const mm = String(d.getMonth() + 1).padStart(2, '0');
const dd = String(d.getDate()).padStart(2, '0').slice(0, 2);
return yyyy + '-' + mm + '-' + dd;
}

// Find the <th> labelled `label` in the given thead element and invoke its onclick.
function clickHeader(window, theadId, label) {
const ths = window.document.querySelectorAll('#' + theadId + ' th');
for (const th of ths) {
if (th.textContent.trim().startsWith(label)) {
const m = /(\w+)\('([^']+)'\)/.exec(th.getAttribute('onclick') || '');
if (m) window[m[1]](m[2]);
return;
}
}
throw new Error('header not found: ' + label);
}

test('Open Positions DTE column shows live countdown, not stored t.dte', (t) => {
// Stored dte=99 is the original term at open; expiry is 3d away — live should win.
const openPut = {
id: 1, asset: 'BTC', type: 'PUT',
date: '2026-01-01', expiry: isoDaysFromToday(3),
dte: 99, strike: 50000, size: 0.05, premium: 100,
outcome: 'OPEN', closeCost: 0, platform: 'RYSK',
};
const { window, teardown } = setupJsdom({ trades: [openPut] });
t.after(teardown);

const cells = window.document.querySelectorAll('#ttbody-open tr td');
// Column index 4 is the DTE cell (Asset, Platform, Date, Expiry, DTE, ...).
const dteCellText = cells[4].textContent;
assert.ok(/\b3d\b|\b3\b/.test(dteCellText), 'expected live "3" countdown, got: ' + dteCellText);
assert.ok(!/99/.test(dteCellText), 'should not render stored term 99 in live DTE column');
});

test('Position History column header reads "Term"; cell shows stored t.dte', (t) => {
const settled = {
id: 2, asset: 'BTC', type: 'PUT',
date: '2026-01-01', expiry: '2026-01-22',
dte: 21, strike: 50000, size: 0.05, premium: 100,
outcome: 'EXPIRED', closeCost: 0, platform: 'RYSK',
};
const { window, teardown } = setupJsdom({ trades: [settled] });
t.after(teardown);

const histHdr = window.document.getElementById('hist-hdr');
assert.ok(/Term/.test(histHdr.textContent), 'history header should contain "Term"');
assert.ok(!/\bDTE\b/.test(histHdr.textContent), 'history header should not contain "DTE"');

const cells = window.document.querySelectorAll('#ttbody-hist tr td');
// Same column position (4) — stored 21.
assert.strictEqual(cells[4].textContent.trim(), '21');
});

test('Open Positions DTE column sorts by expiry', (t) => {
// Two opens: stored dte order (5, 10) is opposite to expiry order.
// After clicking DTE, ascending order should be by expiry (soonest first).
// All three trades share the same stored dte (20). Old behavior would tie on
// dte and fall back to insertion order — BTC first regardless of direction.
// New behavior sorts by expiry, so ETH (3d) or HYPE (20d) leads, never BTC.
const trades = [
{ id: 1, asset: 'BTC', type: 'PUT', date: '2026-01-01', expiry: isoDaysFromToday(10),
dte: 20, strike: 50000, size: 0.05, premium: 100, outcome: 'OPEN', closeCost: 0, platform: 'RYSK' },
{ id: 2, asset: 'ETH', type: 'PUT', date: '2026-01-01', expiry: isoDaysFromToday(3),
dte: 20, strike: 3000, size: 0.5, premium: 50, outcome: 'OPEN', closeCost: 0, platform: 'RYSK' },
{ id: 3, asset: 'HYPE', type: 'PUT', date: '2026-01-01', expiry: isoDaysFromToday(20),
dte: 20, strike: 20, size: 50, premium: 30, outcome: 'OPEN', closeCost: 0, platform: 'RYSK' },
];
const { window, teardown } = setupJsdom({ trades });
t.after(teardown);

clickHeader(window, 'open-hdr', 'DTE');
const firstA = window.document.querySelector('#ttbody-open tr td').textContent.trim();
clickHeader(window, 'open-hdr', 'DTE');
const firstB = window.document.querySelector('#ttbody-open tr td').textContent.trim();

const seen = new Set([firstA, firstB]);
assert.deepStrictEqual([...seen].sort(), ['ETH', 'HYPE'],
'expected ETH (3d) and HYPE (20d) at the ends across two clicks; BTC must not lead');
});
Loading