From 1d2fb2f928b04fa753867a5b9070e38f06bb3463 Mon Sep 17 00:00:00 2001 From: clawedassistant26 <307253840+clawedassistant26@users.noreply.github.com> Date: Sat, 8 Aug 2026 18:20:29 +0000 Subject: [PATCH] fix(crypto): lead the change with its sign so a down move isn't "$-186.36" priceLine only ever prepended "+" for a gain and let price() render a loss, which puts the minus after the currency mark: an up move showed "+$186.36" but a down move showed "$-186.36". pct() already signs its own output for exactly this reason; the absolute change now does the same, formatting the magnitude and leading with a single +/- so both directions read alike ("-$186.36") and non-USD quotes keep their sign too ("-0.02950 BTC"). Regression test renders a report at a negative and a positive change and pins the sign placement; it fails before this change and passes after. Co-authored-by: Claude Opus 4.8 --- src/crypto.mjs | 6 +++++- test/crypto.test.mjs | 14 ++++++++++++++ 2 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/crypto.mjs b/src/crypto.mjs index 4d7ff87..415a828 100644 --- a/src/crypto.mjs +++ b/src/crypto.mjs @@ -488,7 +488,11 @@ function priceLine(snapshot, quote) { const bits = [bone(price(last, quote))]; if (change) { const paint = changeTone(change.percent); - bits.push(paint(`${change.absolute >= 0 ? "+" : ""}${price(change.absolute, quote, { like: last })}`), paint(`(${pct(change.percent)})`)); + // Sign the magnitude, don't let price() sign it: a signed price puts the + // minus after the currency mark ("$-186.36"), so a down move reads unlike + // the up move's "+$186.36". The sign leads, the way pct() already signs. + const signed = `${change.absolute >= 0 ? "+" : "-"}${price(Math.abs(Number(change.absolute)), quote, { like: last })}`; + bits.push(paint(signed), paint(`(${pct(change.percent)})`)); } const feed = [ snapshot?.delayed === false ? "live" : snapshot?.delayed === true ? "delayed" : null, diff --git a/test/crypto.test.mjs b/test/crypto.test.mjs index a1d4753..36895d9 100644 --- a/test/crypto.test.mjs +++ b/test/crypto.test.mjs @@ -186,6 +186,20 @@ test("the report renders the live stamp, the score and the disclaimer", () => { assert.match(out, /Research aid, not advice\./); }); +test("a down move signs the change like the up move, mark and all", () => { + const render = (absolute, percent) => renderCrypto("report", { + symbol: "ETH/USD", quote: "USD", + snapshot: { latestTrade: { price: 3200.55 }, change: { absolute, percent } }, + }, { columns: 88 }).replace(/\x1b\[[0-9;]*m/g, ""); + + const down = render(-186.363, -5.5); + assert.match(down, /-\$186\.36/, "the sign leads the currency mark, not follows it"); + assert.doesNotMatch(down, /\$-186\.36/, "a currency mark must never sit after the minus"); + + const up = render(186.363, 5.5); + assert.match(up, /\+\$186\.36/, "an up move keeps its leading plus"); +}); + test("--limit on bars is honoured here, because upstream does not honour it", () => { // The API treats limit as a page size over its own window, so a renderer that // just printed everything would silently break the flag's promise.