feat(account): add performance tab vs Moscow Exchange index (WealthLedger/main#54) (#167)
This commit was merged in pull request #167.
This commit is contained in:
@@ -0,0 +1,105 @@
|
||||
import type { Performance } from "@/entities/performance";
|
||||
|
||||
import { formatDate, formatMoney, formatPercentagePoints, formatSignedPercent } from "@/shared/lib";
|
||||
|
||||
type TextValue = { tone: Tone; value: string };
|
||||
|
||||
type Tone
|
||||
= | "negative"
|
||||
| "neutral"
|
||||
| "positive";
|
||||
|
||||
const reasonLabels: Record<string, string> = {
|
||||
ACCOUNT_VALUATION_DATES_DIFFER: "Оценки счетов сделаны в разные дни",
|
||||
AMBIGUOUS_RETURN: "У расчёта доходности несколько возможных решений",
|
||||
HISTORY_START_UNKNOWN: "Начало истории счёта неизвестно",
|
||||
INDEX_HISTORY_UNAVAILABLE: "История индекса временно недоступна",
|
||||
NO_SOLUTION: "Расчёт доходности не сходится",
|
||||
NON_RUB_ACTIVITY: "Есть операции или позиции не в рублях",
|
||||
OPERATIONS_HISTORY_INCOMPLETE: "История операций ещё не получена полностью",
|
||||
PORTFOLIO_VALUATION_INCOMPLETE: "Нет полной оценки портфеля",
|
||||
UNCONFIRMED_TRANSFER: "Перевод между счетами не удалось подтвердить",
|
||||
UNVALUED_SECURITIES_TRANSFER: "Бумаги переведены без денежной оценки",
|
||||
};
|
||||
|
||||
const unknownReason = "Причину недоступности не удалось определить";
|
||||
|
||||
function performanceVm(data: Performance) {
|
||||
return {
|
||||
basis: data.return.basis,
|
||||
comparison: data.outperformance
|
||||
? {
|
||||
money: signedMoney(data.outperformance.money),
|
||||
points: valueText(
|
||||
data.outperformance.percentagePoints,
|
||||
formatPercentagePoints,
|
||||
),
|
||||
}
|
||||
: null,
|
||||
current: formatMoney(data.currentValue),
|
||||
currentPoint: data.series.length > 0
|
||||
? { date: formatDate(`${data.series.at(-1)?.date ?? data.period.to}T00:00:00Z`) }
|
||||
: null,
|
||||
days: data.period.days,
|
||||
excluded: data.excludedAccounts.map(item => ({
|
||||
id: item.accountId,
|
||||
name: item.name,
|
||||
reason: reasonText(item.reason),
|
||||
})),
|
||||
from: data.period.from,
|
||||
indexReason: data.benchmark.unavailableReason
|
||||
? reasonText(data.benchmark.unavailableReason)
|
||||
: null,
|
||||
indexReturn: valueText(data.benchmark.return.value, formatSignedPercent),
|
||||
indexValue: data.benchmark.value ? formatMoney(data.benchmark.value) : "—",
|
||||
invested: formatMoney(data.netInvested),
|
||||
profit: signedMoney(data.profit),
|
||||
returnReason: data.return.unavailableReason
|
||||
? reasonText(data.return.unavailableReason)
|
||||
: null,
|
||||
returnValue: valueText(data.return.value, formatSignedPercent),
|
||||
series: data.series.map(item => ({
|
||||
benchmarkValue: Number(item.benchmarkValue),
|
||||
date: item.date,
|
||||
label: formatDate(`${item.date}T00:00:00Z`),
|
||||
netInvested: Number(item.netInvested),
|
||||
referenceValue: item.referenceValue === null
|
||||
? null
|
||||
: Number(item.referenceValue),
|
||||
})),
|
||||
to: data.period.to,
|
||||
};
|
||||
}
|
||||
|
||||
function reasonText(reason: string): string {
|
||||
// eslint-disable-next-line security/detect-object-injection -- Lookup accepts only an API reason, with a safe fallback.
|
||||
return reasonLabels[reason] ?? unknownReason;
|
||||
}
|
||||
|
||||
function signedMoney(value: Performance["profit"]): TextValue {
|
||||
return {
|
||||
tone: tone(value.value),
|
||||
value: formatMoney(value, { withSign: true }),
|
||||
};
|
||||
}
|
||||
|
||||
function tone(value: string): Tone {
|
||||
const number = Number(value);
|
||||
if (number > 0) return "positive";
|
||||
|
||||
return number < 0 ? "negative" : "neutral";
|
||||
}
|
||||
|
||||
// Tone follows the displayed text, so "0,0 %" is never coloured.
|
||||
function valueText(
|
||||
value: null | string,
|
||||
format: (value: string) => string,
|
||||
): TextValue {
|
||||
if (value === null) return { tone: "neutral", value: "—" };
|
||||
const text = format(value);
|
||||
if (text.startsWith("+")) return { tone: "positive", value: text };
|
||||
|
||||
return { tone: text.startsWith("−") ? "negative" : "neutral", value: text };
|
||||
}
|
||||
|
||||
export { performanceVm, reasonText };
|
||||
Reference in New Issue
Block a user