Skip to content

Commit b29799e

Browse files
authored
fix(console): show real asset-history coverage ('历史仅 N 天') instead of implying multi-year (#616)
Co-authored-by: Pigbibi <pigbibi@users.noreply.github.com>
1 parent 40d843b commit b29799e

5 files changed

Lines changed: 50 additions & 7 deletions

File tree

‎tests/account_history_presentation_validation.mjs‎

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import { join } from "node:path";
44
import { fileURLToPath } from "node:url";
55

66
import {
7+
assetHistoryCoverage,
78
buildAssetChartGeometry,
89
buildBinanceWalletHistoryChartGeometry,
910
chartRangeEmptyNote,
@@ -59,6 +60,18 @@ const points = [
5960
const now = Date.parse("2026-09-04T12:00:00Z");
6061
assert.equal(filterAssetHistoryByRange(points, "all", now).length, 3);
6162
assert.deepEqual(filterAssetHistoryByRange(points, "3m", now).map((row) => row.observation_date), ["2026-09-01", "2026-09-02", "2026-09-04"]);
63+
// Coverage is measured from the first to the last real sample; a 3y/5y/10y/all
64+
// selection over 4 days of history must say so instead of implying more.
65+
assert.equal(assetHistoryCoverage([], "1y"), null);
66+
assert.equal(assetHistoryCoverage([{ observation_date: "bad" }], "1y"), null);
67+
assert.deepEqual(assetHistoryCoverage([...points].reverse(), "3y"), { firstDate: "2026-09-01", lastDate: "2026-09-04", days: 4, shorterThanRange: true });
68+
assert.equal(assetHistoryCoverage(points, "10y").shorterThanRange, true);
69+
assert.equal(assetHistoryCoverage(points, "all").shorterThanRange, true);
70+
assert.equal(assetHistoryCoverage([{ observation_date: "2026-09-04" }], "3m").days, 1);
71+
const longPoints = Array.from({ length: 120 }, (_, index) => ({ observation_date: new Date(Date.parse("2026-05-01T00:00:00Z") + index * 86400000).toISOString().slice(0, 10) }));
72+
assert.equal(assetHistoryCoverage(longPoints, "3m").shorterThanRange, false);
73+
assert.equal(assetHistoryCoverage(longPoints, "6m").shorterThanRange, true);
74+
6275

6376
const geometry = buildAssetChartGeometry(points);
6477
assert.equal(geometry.dots.length, 3);

‎web/strategy-switch-console/frontend/src/OverviewPage.tsx‎

Lines changed: 10 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import {
1313
chartRangeEmptyNote,
1414
chartUnavailable,
1515
filterAssetHistoryByRange,
16+
assetHistoryCoverage,
1617
formatOverviewInstant,
1718
formatOverviewShortInstant,
1819
accountNativeReadout,
@@ -511,11 +512,11 @@ export function OverviewPage({ accounts, accountFacts, accountOptionsRevision, o
511512
const selectedUpdatedAt = accountId === "all" ? null : accountFactsUpdatedAt(selectedFacts);
512513
const assetsMetricLabel = accountId === "all" ? "账户最新估值(按币种)" : selectedWalletValuation ? "钱包总资产" : "总资产";
513514
const selectedCashLabel = accountId === "all" ? "账户现金(按来源语义和币种)" : selectedAccount ? cashLabelForPlatform(selectedAccount.platformKey) : "可用现金";
514-
const filteredWalletPoints = walletChartSelected
515-
? filterAssetHistoryByRange(historyKey === requestedHistoryKey ? walletHistory?.points || [] : [], range) : [];
516-
const filteredAccountPoints = walletChartSelected
517-
? [] : filterAssetHistoryByRange(historyKey === requestedHistoryKey && accountHistory?.identity_mismatch !== true && accountHistory?.series.currency === currency
518-
? (accountHistory?.series.points || []).filter(point => point.currency === currency) : [], range);
515+
const storedWalletPoints = walletChartSelected && historyKey === requestedHistoryKey ? walletHistory?.points || [] : [];
516+
const storedAccountPoints = !walletChartSelected && historyKey === requestedHistoryKey && accountHistory?.identity_mismatch !== true && accountHistory?.series.currency === currency
517+
? (accountHistory?.series.points || []).filter(point => point.currency === currency) : [];
518+
const filteredWalletPoints = walletChartSelected ? filterAssetHistoryByRange(storedWalletPoints, range) : [];
519+
const filteredAccountPoints = walletChartSelected ? [] : filterAssetHistoryByRange(storedAccountPoints, range);
519520
const geometry = walletChartSelected
520521
? buildBinanceWalletHistoryChartGeometry(filteredWalletPoints)
521522
: buildAssetChartGeometry(filteredAccountPoints);
@@ -546,6 +547,9 @@ export function OverviewPage({ accounts, accountFacts, accountOptionsRevision, o
546547
break_before: point.break_before,
547548
})), range));
548549
const shownGeometry = accountId === "all" ? aggregateGeometry : geometry;
550+
const historyCoverage = assetHistoryCoverage(accountId === "all"
551+
? aggregateCurrencySeries
552+
: walletChartSelected ? storedWalletPoints : storedAccountPoints, range);
549553
const benchmarkSeries = RETURN_INDEX_LEGEND.map(name => presentMarketBenchmarkSeries(storedBenchmarkPoints, name));
550554
const primaryBenchmark = benchmarkSeries[0];
551555
const benchmarkGeometry = buildBenchmarkChartGeometry(primaryBenchmark.points);
@@ -809,6 +813,7 @@ export function OverviewPage({ accounts, accountFacts, accountOptionsRevision, o
809813
<div className="asset-chart-meta">
810814
<span>{shownGeometry.dots[0]?.date} → {shownGeometry.dots[shownGeometry.dots.length - 1]?.date}</span>
811815
<span>{currency} {shownGeometry.minLabel} – {shownGeometry.maxLabel}</span>
816+
{historyCoverage?.shorterThanRange ? <span className="asset-chart-coverage">{t("历史仅 {days} 天(自 {date} 起),不代表更长期表现。", { days: historyCoverage.days, date: historyCoverage.firstDate })}</span> : null}
812817
{accountId === "all" && aggregateSeries.omittedPartialDateCount > 0 ? <span>{t("有 {count} 个日期省略了缺观察的账户,线段在缺口处断开。", { count: aggregateSeries.omittedPartialDateCount })}</span> : null}
813818
</div>
814819
</div> : <div className="chart-empty">

‎web/strategy-switch-console/frontend/src/locales.ts‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -992,6 +992,7 @@ export const EN_COPY = {
992992
"请选择账户": "Select an account",
993993
"加载中…": "Loading…",
994994
"{range}内暂无资产记录": "No asset history in {range}",
995+
"历史仅 {days} 天(自 {date} 起),不代表更长期表现。": "Only {days} days of history (since {date}); this does not represent longer-term performance.",
995996
"只读演练": "Read-only rehearsal",
996997
"模拟观察": "Shadow observation",
997998
"验证": "Validation",

‎web/strategy-switch-console/frontend/src/presentation.ts‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,30 @@ export function filterAssetHistoryByRange<T extends { observation_date: string }
363363
return sorted.filter((item) => item.observation_date >= start && item.observation_date <= end);
364364
}
365365

366+
export type AssetHistoryCoverage = {
367+
firstDate: string;
368+
lastDate: string;
369+
days: number;
370+
shorterThanRange: boolean;
371+
};
372+
373+
/**
374+
* Real coverage of stored history, measured from the first to the last stored
375+
* sample (inclusive calendar days). `shorterThanRange` is true when the
376+
* selected range (or "all") is longer than what was actually stored, so the
377+
* chart must say how short the history is instead of implying multi-year data.
378+
*/
379+
export function assetHistoryCoverage(points: ReadonlyArray<{ observation_date: string }>, range: ChartRange): AssetHistoryCoverage | null {
380+
if (!Array.isArray(points) || !points.length) return null;
381+
const dates = points.map((item) => item?.observation_date).filter((date): date is string => typeof date === "string" && utcDayMs(date) !== null).sort();
382+
if (!dates.length) return null;
383+
const firstDate = dates[0];
384+
const lastDate = dates[dates.length - 1];
385+
const days = Math.round((utcDayMs(lastDate)! - utcDayMs(firstDate)!) / 86400000) + 1;
386+
const shorterThanRange = range === "all" ? true : days < CHART_RANGE_DAYS[range];
387+
return { firstDate, lastDate, days, shorterThanRange };
388+
}
389+
366390
export type AssetChartGeometry = {
367391
width: number;
368392
height: number;

‎web/strategy-switch-console/v2_asset_map.js‎

Lines changed: 2 additions & 2 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)