Files
backend/src/modules/investments/instruments/bonds/application/bond.spec.ts
T
codex-bot 07cfd08df9
Backend verify / commits (push) Skipped
Backend verify / security (push) Successful in 1m6s
Backend verify / verify (push) Failing after 6m44s
refactor: replace deep imports with native node aliases (WealthLedger/backend#165) (#193)
2026-11-04 23:22:46 +03:00

179 lines
5.1 KiB
TypeScript

import type { PortfolioDto } from '#src/modules/investments/portfolio/portfolio.api';
import { instrumentPosition } from '../../application/instrument-position';
import { bondCoupons, bondMetrics } from './bond-income.service';
import { bondQuote } from './bond-market.service';
jest.mock('#src/prisma/prisma.service', () => ({
PrismaService: class {},
}));
const time = (value: string) => ({ seconds: String(Date.parse(value) / 1000) });
const money = (units: string, currency = 'rub') => ({ units, currency });
const bond = {
uid: 'uid',
ticker: 'BOND',
isin: 'ISIN',
instrumentKind: 'INSTRUMENT_TYPE_BOND',
nominal: money('500'),
initialNominal: money('1000'),
aciValue: money('12'),
amortizationFlag: true,
first_1dayCandleDate: time('2024-01-01'),
};
describe('bond financial projections', () => {
beforeEach(() =>
jest.useFakeTimers().setSystemTime(new Date('2026-09-10T10:00:00Z')),
);
afterEach(() => jest.useRealTimers());
it('estimates only future unfixed payouts from exact UID holdings', () => {
const holding = {
instrumentUid: 'uid',
type: 'BOND',
quantity: '2',
averagePurchasePrice: null,
currentValue: { value: '981.25', currency: 'RUB' },
absoluteReturn: null,
};
const position = instrumentPosition(
{
positions: [
holding,
{ ...holding, instrumentUid: 'other', quantity: '99' },
{ ...holding, type: 'STOCK', quantity: '999' },
],
lastSuccessfulAt: '2026-09-10T00:00:00Z',
isStale: true,
} as PortfolioDto,
'uid',
'BOND',
);
const result = bondCoupons(
[
{
couponNumber: '1',
couponDate: time('2026-10-01'),
fixDate: time('2026-09-29'),
payOneBond: money('10'),
},
{
couponNumber: '2',
couponDate: time('2026-10-01'),
fixDate: time('2026-09-09'),
payOneBond: money('10'),
},
{
couponNumber: '3',
couponDate: time('2026-10-01'),
payOneBond: money('10'),
},
{
couponNumber: '4',
couponDate: time('2026-10-01'),
fixDate: time('2026-09-29'),
},
],
position,
{ from: '2021-09-10', to: '2027-09-10', today: '2026-09-10' },
'source-time',
false,
);
expect(result.coupons[0].estimatedAmount).toEqual({
value: '20',
currency: 'RUB',
});
expect(
result.coupons.slice(1).every((row) => row.estimatedAmount === null),
).toBe(true);
expect(result.coupons[3].perBond).toBeNull();
expect(result.positionIsStale).toBe(true);
expect(position.quantity).toBe('2');
});
it('preserves amortized nominal, ACI, percent quotes and annualized coupon yield', () => {
const values = {
instrument: bond,
price: { price: { units: '98', nano: 125000000 } },
candle: { close: { units: '99' } },
coupons: [
{
couponDate: time('2026-10-10'),
payOneBond: money('30'),
couponPeriod: 90,
},
],
};
expect(bondQuote(values, [], 'time')).toMatchObject({
pricePercent: '98.125',
cleanPrice: { currency: 'RUB', value: '490.625' },
dirtyPrice: { currency: 'RUB', value: '502.625' },
changePercentagePoints: '-0.875',
});
expect(bondMetrics(values, [], 'time')).toMatchObject({
couponDate: '2026-10-10',
couponYieldPercent: '24.7983',
creditRating: null,
});
});
it.each(['unknown', 'currency', 'period', 'zero-price'])(
'does not invent coupon yield with %s inputs',
(scenario) => {
const result = bondMetrics(
{
instrument: bond,
price: { price: { units: scenario === 'zero-price' ? '0' : '100' } },
coupons: [
{
couponDate: time('2026-10-10'),
payOneBond:
scenario === 'unknown'
? undefined
: money('30', scenario === 'currency' ? 'usd' : 'rub'),
couponPeriod: scenario === 'period' ? 0 : 90,
},
],
},
[],
'time',
);
expect(result.couponYieldPercent).toBeNull();
},
);
it('keeps absent and mismatched ACI unknown and never substitutes risk level for a rating', () => {
expect(
bondQuote(
{
instrument: { ...bond, aciValue: money('1', 'usd') },
price: { price: { units: '100' } },
},
[],
'time',
).dirtyPrice,
).toBeNull();
expect(
bondQuote(
{
instrument: { ...bond, aciValue: undefined },
price: { price: { units: '100' } },
},
[],
'time',
).dirtyPrice,
).toBeNull();
expect(
bondMetrics(
{
instrument: {
...bond,
riskLevel: 'RISK_LEVEL_LOW',
floatingCouponFlag: true,
},
},
['yield'],
'time',
),
).toMatchObject({
creditRating: null,
couponType: 'COUPON_TYPE_FLOATING',
couponYieldPercent: null,
});
});
});