fix(strategy-ui): correct rankings, currency, injection view and staleness

- MaxDD ranks shallower drawdowns first (values are negative); ruined
  results are badged and never ranked as best.
- Metrics render '—' for null/non-finite values instead of crashing.
- Amounts use the market's currency (CNY for CN) instead of hard-coded USD.
- Injection scenarios show invested / final value / profit / return on
  invested from capitalFlows and label return metrics as time-weighted.
- Precomputed results show their data end date and warn when older than 7
  days; live strategies warn when not updated for 4 days, show start date,
  price as-of, stale holdings, and same-window benchmark with excess return.
- Unknown precomputed scenario returns 400 instead of silently serving the
  first scenario; the client uses the scenarioId it receives.

Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-25 01:22:07 +08:00
parent c2882ae4cf
commit eefd879a93
5 changed files with 441 additions and 119 deletions

View File

@@ -172,6 +172,7 @@ function resolveScenarios(raw: { scenarios?: Record<string, unknown>; markets?:
label: string;
description?: string;
capital: number;
capitalByMarket?: Record<string, number>;
injectionSchedule: unknown;
markets: Record<string, unknown>;
}>;
@@ -191,6 +192,7 @@ function resolveScenarios(raw: { scenarios?: Record<string, unknown>; markets?:
label: string;
description?: string;
capital: number;
capitalByMarket?: Record<string, number>;
injectionSchedule: unknown;
markets: Record<string, unknown>;
}>;
@@ -207,6 +209,7 @@ app.get("/api/strategies/precomputed/index", (_request, response, next) => {
label: sc.label,
description: sc.description,
capital: sc.capital,
capitalByMarket: sc.capitalByMarket,
injectionSchedule: sc.injectionSchedule ?? null,
availableMarkets: Object.keys(sc.markets ?? {}),
}));
@@ -223,9 +226,12 @@ app.get("/api/strategies/precomputed", (_request, response, next) => {
const raw = loadPrecomputedFile();
const scenarios = resolveScenarios(raw);
const scenarioIds = Object.keys(scenarios);
const scenarioId = requestedScenario && scenarios[requestedScenario]
? requestedScenario
: scenarioIds[0];
// An explicit but unknown scenario is a client error; silently serving
// another scenario would mislabel the results.
if (requestedScenario && !Object.hasOwn(scenarios, requestedScenario)) {
throw new Error(`Unknown pre-computed scenario: ${requestedScenario}`);
}
const scenarioId = requestedScenario ?? scenarioIds[0];
const scenario = scenarios[scenarioId];
if (!scenario) {
throw new Error("No pre-computed scenarios available.");