feat(performance): investment performance vs Moscow Exchange index (WealthLedger/main#54) (#200)
Backend verify / commits (push) Skipped
Backend verify / security (push) Successful in 1m7s
Backend verify / verify (push) Failing after 7m19s

This commit was merged in pull request #200.
This commit is contained in:
2026-11-08 16:45:29 +03:00
parent 84433ee207
commit fee61537b6
28 changed files with 1507 additions and 4 deletions
@@ -0,0 +1,41 @@
import { formatDecimal } from '#src/common/decimal';
import { candidateTransfers, readPerformanceOperations, sumFlows } from './performance-flows';
const at = (date: string) => new Date(date);
const op = (accountId: string, type: string, date: string, units: string, currency = 'rub') => ({
accountId, occurredAt: at(date), instrumentUid: null,
payload: { type, payment: { units, nano: 0, currency } },
});
describe('performance cash flows', () => {
it('includes only external cash and preserves dividends within the portfolio', () => {
const operations = [
op('A', 'OPERATION_TYPE_INPUT', '2025-01-01T15:00:00Z', '100000'),
op('A', 'OPERATION_TYPE_INPUT_ACQUIRING', '2025-02-01T15:00:00Z', '50000'),
op('A', 'OPERATION_TYPE_OUTPUT', '2025-03-01T15:00:00Z', '-20000'),
op('A', 'OPERATION_TYPE_DIVIDEND', '2025-04-01T15:00:00Z', '3000'),
];
const result = readPerformanceOperations(operations);
expect(formatDecimal(sumFlows(result.flows))).toBe('130000');
expect(result.flows).toHaveLength(3);
});
it('treats equal opposite cash movements as unconfirmed transfer candidates, not a proven pair', () => {
const result = readPerformanceOperations([
op('A', 'OPERATION_TYPE_OUTPUT', '2025-03-01T15:00:00Z', '-50000'),
op('B', 'OPERATION_TYPE_INPUT', '2025-03-01T15:01:00Z', '50000'),
]);
expect(candidateTransfers(result)).toEqual([{ accountIds: ['A', 'B'], type: 'cash' }]);
expect(result.flows).toHaveLength(2);
});
it('detects non-RUB operations and unvalued securities transfers', () => {
const result = readPerformanceOperations([
op('A', 'OPERATION_TYPE_INPUT', '2025-01-01T15:00:00Z', '10', 'usd'),
op('B', 'OPERATION_TYPE_INPUT_SECURITIES', '2025-01-01T15:00:00Z', '0'),
]);
expect([...result.nonRubAccounts]).toEqual(['A']);
expect([...result.unvaluedAccounts]).toEqual(['B']);
});
});