171 lines
4.4 KiB
TypeScript
171 lines
4.4 KiB
TypeScript
import { formatShare, type Portfolio } from "@/entities/portfolio";
|
|
import { formatMoney, instrumentLogoUrl } from "@/shared/lib";
|
|
|
|
type AccountPagePosition = Portfolio["positions"][number] & {
|
|
instrumentTo?: string;
|
|
logoSrc: null | string;
|
|
returnText: null | string;
|
|
returnTone?: AccountPageReturnTone;
|
|
shareText: null | string;
|
|
};
|
|
|
|
type AccountPagePositionGroup = {
|
|
key: string;
|
|
positions: [AccountPagePosition, ...AccountPagePosition[]];
|
|
};
|
|
|
|
type AccountPagePositionSection = {
|
|
groups: AccountPagePositionGroup[];
|
|
kind: PositionSectionKind;
|
|
};
|
|
|
|
type AccountPageReturnTone = "negative" | "positive";
|
|
|
|
type PositionSectionKind = "bonds"
|
|
| "other"
|
|
| "stocks";
|
|
|
|
function groupPositions(
|
|
positions: AccountPagePosition[],
|
|
): AccountPagePositionGroup[] {
|
|
const groups = new Map<string, AccountPagePositionGroup>();
|
|
|
|
for (const [index, position] of positions.entries()) {
|
|
const identity = positionIdentity(position);
|
|
const rowKey = JSON.stringify(["row", position.account?.id, index]);
|
|
|
|
const key
|
|
= identity && position.account
|
|
? JSON.stringify([position.type, ...identity])
|
|
: rowKey;
|
|
|
|
const group = groups.get(key);
|
|
|
|
if (
|
|
group
|
|
&& group.positions.every(
|
|
item => item.account?.id !== position.account?.id,
|
|
)
|
|
) {
|
|
group.positions.push(position);
|
|
}
|
|
else {
|
|
const uniqueKey = group ? rowKey : key;
|
|
groups.set(uniqueKey, { key: uniqueKey, positions: [position] });
|
|
}
|
|
}
|
|
|
|
// eslint-disable-next-line unicorn/prefer-iterator-to-array -- Production targets ES2023; Iterator helpers require a newer browser baseline.
|
|
return [...groups.values()];
|
|
}
|
|
|
|
function positionIdentity(position: AccountPagePosition) {
|
|
if (position.instrumentUid?.trim())
|
|
return ["uid", position.instrumentUid.trim()];
|
|
if (position.figi?.trim()) return ["figi", position.figi.trim()];
|
|
|
|
return position.isin?.trim() ? ["isin", position.isin.trim()] : undefined;
|
|
}
|
|
|
|
function toAccountPagePositionSections(
|
|
portfolio: Portfolio,
|
|
): AccountPagePositionSection[] {
|
|
const denominators = new Map<string, number>();
|
|
|
|
for (const position of portfolio.positions) {
|
|
if (!position.currentValue) continue;
|
|
const currency = position.currentValue.currency;
|
|
denominators.set(
|
|
currency,
|
|
(denominators.get(currency) ?? 0) + Number(position.currentValue.value),
|
|
);
|
|
}
|
|
|
|
const stocks = toSection(
|
|
"stocks",
|
|
portfolio,
|
|
denominators,
|
|
);
|
|
|
|
const bonds = toSection("bonds", portfolio, denominators);
|
|
const other = toSection("other", portfolio, denominators);
|
|
|
|
return [stocks, bonds, other].filter(section => section.groups.length > 0);
|
|
}
|
|
|
|
function toPosition(
|
|
position: Portfolio["positions"][number],
|
|
denominators: Map<string, number>,
|
|
kind: PositionSectionKind,
|
|
): AccountPagePosition {
|
|
const isExpanded = kind !== "other";
|
|
|
|
const returnValue = position.absoluteReturn
|
|
? Number(position.absoluteReturn.value)
|
|
: null;
|
|
|
|
let returnTone: AccountPageReturnTone | undefined;
|
|
if (returnValue !== null && returnValue !== 0)
|
|
returnTone = returnValue > 0 ? "positive" : "negative";
|
|
|
|
const returnPrefix
|
|
= isExpanded && returnValue !== null && returnValue > 0 ? "+" : "";
|
|
|
|
return {
|
|
...position,
|
|
instrumentTo:
|
|
(kind === "stocks" || kind === "bonds") && position.instrumentUid
|
|
? `/${kind}/${encodeURIComponent(position.instrumentUid)}`
|
|
: undefined,
|
|
logoSrc: isExpanded
|
|
? instrumentLogoUrl(position.logoName)
|
|
: null,
|
|
returnText: position.absoluteReturn
|
|
? `${returnPrefix}${formatMoney(position.absoluteReturn)}`
|
|
: null,
|
|
returnTone,
|
|
shareText: position.currentValue?.currency
|
|
? formatShare(position.currentValue, {
|
|
currency: position.currentValue.currency,
|
|
value: String(denominators.get(position.currentValue.currency) ?? 0),
|
|
})
|
|
: null,
|
|
};
|
|
}
|
|
|
|
function toSection(
|
|
kind: PositionSectionKind,
|
|
portfolio: Portfolio,
|
|
denominators: Map<string, number>,
|
|
): AccountPagePositionSection {
|
|
return {
|
|
groups: groupPositions(
|
|
portfolio.positions
|
|
.filter((position) => {
|
|
if (kind === "stocks") return position.type === "STOCK";
|
|
|
|
return kind === "bonds"
|
|
? position.type === "BOND"
|
|
: position.type !== "STOCK"
|
|
&& position.type !== "BOND"
|
|
&& position.type !== "CURRENCY";
|
|
})
|
|
.map(position =>
|
|
toPosition(
|
|
position,
|
|
denominators,
|
|
kind,
|
|
),
|
|
),
|
|
),
|
|
kind,
|
|
};
|
|
}
|
|
|
|
export {
|
|
type AccountPagePosition,
|
|
type AccountPagePositionGroup,
|
|
type AccountPagePositionSection,
|
|
toAccountPagePositionSections,
|
|
};
|