From 8aea56cbbfebe5187c5943e0c99b885836107882 Mon Sep 17 00:00:00 2001 From: Sergey Krylov Date: Thu, 18 Jun 2026 23:15:32 +0300 Subject: [PATCH] feat: expose broker position counts --- .../dto/broker-portfolio-response.dto.ts | 17 ++++++++++++++++ .../tbank/mappers/portfolio.mapper.spec.ts | 7 +++++++ .../modules/tbank/mappers/portfolio.mapper.ts | 20 +++++++++++++++++++ .../services/broker-portfolio.service.spec.ts | 4 +++- .../src/modules/tbank/types/broker.types.ts | 6 ++++++ 5 files changed, 53 insertions(+), 1 deletion(-) diff --git a/apps/backend/src/modules/tbank/dto/broker-portfolio-response.dto.ts b/apps/backend/src/modules/tbank/dto/broker-portfolio-response.dto.ts index c06b5cc..964dcbe 100644 --- a/apps/backend/src/modules/tbank/dto/broker-portfolio-response.dto.ts +++ b/apps/backend/src/modules/tbank/dto/broker-portfolio-response.dto.ts @@ -42,10 +42,27 @@ export class BrokerPortfolioYieldsDto { dailyPercent!: number | null; } +export class BrokerPortfolioPositionCountsDto { + @ApiProperty({ minimum: 0 }) + shares!: number; + + @ApiProperty({ minimum: 0 }) + bonds!: number; + + @ApiProperty({ minimum: 0 }) + etf!: number; + + @ApiProperty({ minimum: 0 }) + other!: number; +} + export class BrokerPortfolioResponseDto { @ApiProperty({ type: BrokerAccountResponseDto }) account!: BrokerAccountResponseDto; + @ApiProperty({ type: BrokerPortfolioPositionCountsDto }) + positionCounts!: BrokerPortfolioPositionCountsDto; + @ApiProperty({ type: BrokerPortfolioTotalsDto }) totals!: BrokerPortfolioTotalsDto; diff --git a/apps/backend/src/modules/tbank/mappers/portfolio.mapper.spec.ts b/apps/backend/src/modules/tbank/mappers/portfolio.mapper.spec.ts index b939fa9..6e06e26 100644 --- a/apps/backend/src/modules/tbank/mappers/portfolio.mapper.spec.ts +++ b/apps/backend/src/modules/tbank/mappers/portfolio.mapper.spec.ts @@ -30,6 +30,12 @@ describe('portfolio.mapper', () => { currentPrice: { currency: 'rub', units: '250', nano: 0 }, averagePositionPrice: { currency: 'rub', units: '200', nano: 0 }, }, + { instrumentType: 'SHARE' }, + { instrumentType: 'bond' }, + { instrumentType: 'etf' }, + { instrumentType: 'fund' }, + { instrumentType: 'future' }, + {}, ], }, positions: { @@ -43,5 +49,6 @@ describe('portfolio.mapper', () => { expect(result.totals.shares?.value).toBe(1000); expect(result.cash[0].value).toBe(500); expect(result.blockedCash[0].value).toBe(10); + expect(result.positionCounts).toEqual({ shares: 2, bonds: 1, etf: 2, other: 2 }); }); }); diff --git a/apps/backend/src/modules/tbank/mappers/portfolio.mapper.ts b/apps/backend/src/modules/tbank/mappers/portfolio.mapper.ts index 4248ae7..d0b3303 100644 --- a/apps/backend/src/modules/tbank/mappers/portfolio.mapper.ts +++ b/apps/backend/src/modules/tbank/mappers/portfolio.mapper.ts @@ -74,8 +74,28 @@ export function mapBrokerPosition(input: { } export function mapBrokerPortfolio(input: MapBrokerPortfolioInput): BrokerPortfolio { + const positionCounts = { shares: 0, bonds: 0, etf: 0, other: 0 }; + + for (const position of input.portfolio.positions ?? []) { + switch (position.instrumentType?.toLowerCase()) { + case 'share': + positionCounts.shares += 1; + break; + case 'bond': + positionCounts.bonds += 1; + break; + case 'etf': + case 'fund': + positionCounts.etf += 1; + break; + default: + positionCounts.other += 1; + } + } + return { account: input.account, + positionCounts, totals: { shares: mapMoneyValue(input.portfolio.totalAmountShares), bonds: mapMoneyValue(input.portfolio.totalAmountBonds), diff --git a/apps/backend/src/modules/tbank/services/broker-portfolio.service.spec.ts b/apps/backend/src/modules/tbank/services/broker-portfolio.service.spec.ts index 6a1caa4..338eae5 100644 --- a/apps/backend/src/modules/tbank/services/broker-portfolio.service.spec.ts +++ b/apps/backend/src/modules/tbank/services/broker-portfolio.service.spec.ts @@ -46,7 +46,7 @@ describe('BrokerPortfolioService', () => { .mockResolvedValueOnce({ accountId: 'acc-1', totalAmountPortfolio: { currency: 'rub', units: '1000', nano: 0 }, - positions: [], + positions: [{ instrumentType: 'share' }, { instrumentType: 'bond' }], }) .mockResolvedValueOnce({ accountId: 'acc-1', @@ -60,7 +60,9 @@ describe('BrokerPortfolioService', () => { expect(result.data.account.id).toBe('acc-1'); expect(result.data.cash[0].value).toBe(1000); + expect(result.data.positionCounts).toEqual({ shares: 1, bonds: 1, etf: 0, other: 0 }); expect('positions' in result.data).toBe(false); + expect(client.callUnary).toHaveBeenCalledTimes(2); expect(cache.getOrFetch).toHaveBeenCalledWith( 'tbank:portfolio', ['acc-1'], diff --git a/apps/backend/src/modules/tbank/types/broker.types.ts b/apps/backend/src/modules/tbank/types/broker.types.ts index cd74fa0..aff00e7 100644 --- a/apps/backend/src/modules/tbank/types/broker.types.ts +++ b/apps/backend/src/modules/tbank/types/broker.types.ts @@ -33,6 +33,12 @@ export type BrokerPosition = { export type BrokerPortfolio = { account: BrokerAccount; + positionCounts: { + shares: number; + bonds: number; + etf: number; + other: number; + }; totals: { shares: BrokerMoney | null; bonds: BrokerMoney | null;