feat: expose tbank broker portfolio
This commit is contained in:
parent
ddeab01b9e
commit
bd6b2589c0
@ -1,5 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { BrokerAccountResponseDto } from './broker-account-response.dto';
|
||||
import { BrokerPortfolioResponseDto } from './broker-portfolio-response.dto';
|
||||
|
||||
export class BrokerResponseMetaDto {
|
||||
@ApiProperty({ nullable: true })
|
||||
@ -16,3 +17,11 @@ export class BrokerAccountsEnvelopeDto {
|
||||
@ApiProperty({ type: BrokerResponseMetaDto })
|
||||
meta!: BrokerResponseMetaDto;
|
||||
}
|
||||
|
||||
export class BrokerPortfolioEnvelopeDto {
|
||||
@ApiProperty({ type: BrokerPortfolioResponseDto })
|
||||
data!: BrokerPortfolioResponseDto;
|
||||
|
||||
@ApiProperty({ type: BrokerResponseMetaDto })
|
||||
meta!: BrokerResponseMetaDto;
|
||||
}
|
||||
|
||||
15
apps/backend/src/modules/tbank/dto/broker-money.dto.ts
Normal file
15
apps/backend/src/modules/tbank/dto/broker-money.dto.ts
Normal file
@ -0,0 +1,15 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export class BrokerMoneyDto {
|
||||
@ApiProperty()
|
||||
currency!: string;
|
||||
|
||||
@ApiProperty()
|
||||
units!: string;
|
||||
|
||||
@ApiProperty()
|
||||
nano!: number;
|
||||
|
||||
@ApiProperty()
|
||||
value!: number;
|
||||
}
|
||||
@ -0,0 +1,110 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { BrokerAccountResponseDto } from './broker-account-response.dto';
|
||||
import { BrokerMoneyDto } from './broker-money.dto';
|
||||
|
||||
export class BrokerPositionResponseDto {
|
||||
@ApiProperty({ nullable: true })
|
||||
figi!: string | null;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
instrumentUid!: string | null;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
positionUid!: string | null;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
ticker!: string | null;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
classCode!: string | null;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
instrumentType!: string | null;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
name!: string | null;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
quantity!: number | null;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
blockedLots!: number | null;
|
||||
|
||||
@ApiProperty({ type: BrokerMoneyDto, nullable: true })
|
||||
currentPrice!: BrokerMoneyDto | null;
|
||||
|
||||
@ApiProperty({ type: BrokerMoneyDto, nullable: true })
|
||||
currentValue!: BrokerMoneyDto | null;
|
||||
|
||||
@ApiProperty({ type: BrokerMoneyDto, nullable: true })
|
||||
averagePositionPrice!: BrokerMoneyDto | null;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
expectedYieldPercent!: number | null;
|
||||
|
||||
@ApiProperty({ type: BrokerMoneyDto, nullable: true })
|
||||
dailyYield!: BrokerMoneyDto | null;
|
||||
}
|
||||
|
||||
export class BrokerPortfolioTotalsDto {
|
||||
@ApiProperty({ type: BrokerMoneyDto, nullable: true })
|
||||
shares!: BrokerMoneyDto | null;
|
||||
|
||||
@ApiProperty({ type: BrokerMoneyDto, nullable: true })
|
||||
bonds!: BrokerMoneyDto | null;
|
||||
|
||||
@ApiProperty({ type: BrokerMoneyDto, nullable: true })
|
||||
etf!: BrokerMoneyDto | null;
|
||||
|
||||
@ApiProperty({ type: BrokerMoneyDto, nullable: true })
|
||||
currencies!: BrokerMoneyDto | null;
|
||||
|
||||
@ApiProperty({ type: BrokerMoneyDto, nullable: true })
|
||||
futures!: BrokerMoneyDto | null;
|
||||
|
||||
@ApiProperty({ type: BrokerMoneyDto, nullable: true })
|
||||
options!: BrokerMoneyDto | null;
|
||||
|
||||
@ApiProperty({ type: BrokerMoneyDto, nullable: true })
|
||||
structuredProducts!: BrokerMoneyDto | null;
|
||||
|
||||
@ApiProperty({ type: BrokerMoneyDto, nullable: true })
|
||||
dfa!: BrokerMoneyDto | null;
|
||||
|
||||
@ApiProperty({ type: BrokerMoneyDto, nullable: true })
|
||||
portfolio!: BrokerMoneyDto | null;
|
||||
}
|
||||
|
||||
export class BrokerPortfolioYieldsDto {
|
||||
@ApiProperty({ nullable: true })
|
||||
expectedPercent!: number | null;
|
||||
|
||||
@ApiProperty({ type: BrokerMoneyDto, nullable: true })
|
||||
daily!: BrokerMoneyDto | null;
|
||||
|
||||
@ApiProperty({ nullable: true })
|
||||
dailyPercent!: number | null;
|
||||
}
|
||||
|
||||
export class BrokerPortfolioResponseDto {
|
||||
@ApiProperty({ type: BrokerAccountResponseDto })
|
||||
account!: BrokerAccountResponseDto;
|
||||
|
||||
@ApiProperty({ type: BrokerPortfolioTotalsDto })
|
||||
totals!: BrokerPortfolioTotalsDto;
|
||||
|
||||
@ApiProperty({ type: BrokerPortfolioYieldsDto })
|
||||
yields!: BrokerPortfolioYieldsDto;
|
||||
|
||||
@ApiProperty({ type: [BrokerMoneyDto] })
|
||||
cash!: BrokerMoneyDto[];
|
||||
|
||||
@ApiProperty({ type: [BrokerMoneyDto] })
|
||||
blockedCash!: BrokerMoneyDto[];
|
||||
|
||||
@ApiProperty({ type: [BrokerPositionResponseDto] })
|
||||
positions!: BrokerPositionResponseDto[];
|
||||
|
||||
@ApiProperty()
|
||||
asOf!: string;
|
||||
}
|
||||
@ -0,0 +1,54 @@
|
||||
import { mapBrokerPortfolio } from './portfolio.mapper';
|
||||
import type { BrokerAccount } from '../types/broker.types';
|
||||
|
||||
describe('portfolio.mapper', () => {
|
||||
const account: BrokerAccount = {
|
||||
id: 'acc-1',
|
||||
type: 'brokerage',
|
||||
name: 'Broker',
|
||||
status: 'ACCOUNT_STATUS_OPEN',
|
||||
openedAt: null,
|
||||
accessLevel: 'ACCOUNT_ACCESS_LEVEL_FULL_ACCESS',
|
||||
};
|
||||
|
||||
it('combines portfolio totals, cash, and enriched positions', () => {
|
||||
const result = mapBrokerPortfolio({
|
||||
account,
|
||||
portfolio: {
|
||||
accountId: 'acc-1',
|
||||
totalAmountShares: { currency: 'rub', units: '1000', nano: 0 },
|
||||
totalAmountPortfolio: { currency: 'rub', units: '1500', nano: 0 },
|
||||
expectedYield: { units: '10', nano: 500000000 },
|
||||
positions: [
|
||||
{
|
||||
figi: 'BBG004730N88',
|
||||
instrumentUid: 'uid-1',
|
||||
ticker: 'SBER',
|
||||
classCode: 'TQBR',
|
||||
instrumentType: 'share',
|
||||
quantity: { units: '10', nano: 0 },
|
||||
currentPrice: { currency: 'rub', units: '250', nano: 0 },
|
||||
averagePositionPrice: { currency: 'rub', units: '200', nano: 0 },
|
||||
},
|
||||
],
|
||||
},
|
||||
positions: {
|
||||
money: [{ currency: 'rub', units: '500', nano: 0 }],
|
||||
blocked: [{ currency: 'rub', units: '10', nano: 0 }],
|
||||
securities: [],
|
||||
},
|
||||
instruments: new Map([['uid-1', { name: 'Sberbank', ticker: 'SBER' }]]),
|
||||
});
|
||||
|
||||
expect(result.account.id).toBe('acc-1');
|
||||
expect(result.totals.shares?.value).toBe(1000);
|
||||
expect(result.cash[0].value).toBe(500);
|
||||
expect(result.blockedCash[0].value).toBe(10);
|
||||
expect(result.positions[0]).toMatchObject({
|
||||
ticker: 'SBER',
|
||||
name: 'Sberbank',
|
||||
quantity: 10,
|
||||
currentValue: { value: 2500 },
|
||||
});
|
||||
});
|
||||
});
|
||||
84
apps/backend/src/modules/tbank/mappers/portfolio.mapper.ts
Normal file
84
apps/backend/src/modules/tbank/mappers/portfolio.mapper.ts
Normal file
@ -0,0 +1,84 @@
|
||||
import type {
|
||||
BrokerAccount,
|
||||
BrokerMoney,
|
||||
BrokerPortfolio,
|
||||
BrokerPosition,
|
||||
} from '../types/broker.types';
|
||||
import type {
|
||||
TBankInstrument,
|
||||
TBankPortfolioResponse,
|
||||
TBankPositionsResponse,
|
||||
} from '../types/tbank-proto.types';
|
||||
import { mapMoneyValue, mapQuotationToNumber } from './money.mapper';
|
||||
|
||||
type MapBrokerPortfolioInput = {
|
||||
account: BrokerAccount;
|
||||
portfolio: TBankPortfolioResponse;
|
||||
positions: TBankPositionsResponse;
|
||||
instruments: Map<string, Partial<TBankInstrument>>;
|
||||
};
|
||||
|
||||
function isBrokerMoney(value: BrokerMoney | null): value is BrokerMoney {
|
||||
return value !== null;
|
||||
}
|
||||
|
||||
export function mapBrokerPortfolio(input: MapBrokerPortfolioInput): BrokerPortfolio {
|
||||
const mappedPositions = (input.portfolio.positions ?? []).map<BrokerPosition>((position) => {
|
||||
const quantity = mapQuotationToNumber(position.quantity);
|
||||
const currentPrice = mapMoneyValue(position.currentPrice);
|
||||
const currentValue =
|
||||
currentPrice && quantity !== null
|
||||
? {
|
||||
...currentPrice,
|
||||
units: String(Math.trunc(currentPrice.value * quantity)),
|
||||
nano: 0,
|
||||
value: Number((currentPrice.value * quantity).toFixed(9)),
|
||||
}
|
||||
: null;
|
||||
const instrument =
|
||||
(position.instrumentUid && input.instruments.get(position.instrumentUid)) ||
|
||||
(position.positionUid && input.instruments.get(position.positionUid)) ||
|
||||
undefined;
|
||||
|
||||
return {
|
||||
figi: position.figi ?? null,
|
||||
instrumentUid: position.instrumentUid ?? null,
|
||||
positionUid: position.positionUid ?? null,
|
||||
ticker: position.ticker || instrument?.ticker || null,
|
||||
classCode: position.classCode || instrument?.classCode || null,
|
||||
instrumentType: position.instrumentType || instrument?.instrumentType || null,
|
||||
name: instrument?.name ?? null,
|
||||
quantity,
|
||||
blockedLots: mapQuotationToNumber(position.blockedLots),
|
||||
currentPrice,
|
||||
currentValue,
|
||||
averagePositionPrice: mapMoneyValue(position.averagePositionPrice),
|
||||
expectedYieldPercent: mapQuotationToNumber(position.expectedYield),
|
||||
dailyYield: mapMoneyValue(position.dailyYield),
|
||||
};
|
||||
});
|
||||
|
||||
return {
|
||||
account: input.account,
|
||||
totals: {
|
||||
shares: mapMoneyValue(input.portfolio.totalAmountShares),
|
||||
bonds: mapMoneyValue(input.portfolio.totalAmountBonds),
|
||||
etf: mapMoneyValue(input.portfolio.totalAmountEtf),
|
||||
currencies: mapMoneyValue(input.portfolio.totalAmountCurrencies),
|
||||
futures: mapMoneyValue(input.portfolio.totalAmountFutures),
|
||||
options: mapMoneyValue(input.portfolio.totalAmountOptions),
|
||||
structuredProducts: mapMoneyValue(input.portfolio.totalAmountSp),
|
||||
dfa: mapMoneyValue(input.portfolio.totalAmountDfa),
|
||||
portfolio: mapMoneyValue(input.portfolio.totalAmountPortfolio),
|
||||
},
|
||||
yields: {
|
||||
expectedPercent: mapQuotationToNumber(input.portfolio.expectedYield),
|
||||
daily: mapMoneyValue(input.portfolio.dailyYield),
|
||||
dailyPercent: mapQuotationToNumber(input.portfolio.dailyYieldRelative),
|
||||
},
|
||||
cash: (input.positions.money ?? []).map(mapMoneyValue).filter(isBrokerMoney),
|
||||
blockedCash: (input.positions.blocked ?? []).map(mapMoneyValue).filter(isBrokerMoney),
|
||||
positions: mappedPositions,
|
||||
asOf: new Date().toISOString(),
|
||||
};
|
||||
}
|
||||
@ -0,0 +1,38 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { CacheService } from '../../cache/cache.service';
|
||||
import { TBANK_CACHE_KEYS } from '../tbank.config';
|
||||
import type { TBankInstrument, TBankInstrumentResponse } from '../types/tbank-proto.types';
|
||||
import { TBankClientService } from './tbank-client.service';
|
||||
|
||||
@Injectable()
|
||||
export class BrokerInstrumentsService {
|
||||
constructor(
|
||||
private readonly tbankClient: TBankClientService,
|
||||
private readonly cacheService: CacheService,
|
||||
) {}
|
||||
|
||||
async findByInstrumentUid(instrumentUid: string): Promise<TBankInstrument | null> {
|
||||
const result = await this.cacheService.getOrFetch(
|
||||
TBANK_CACHE_KEYS.instrument,
|
||||
[instrumentUid],
|
||||
() => this.fetchByUid(instrumentUid),
|
||||
'tbankInstrumentTtl',
|
||||
);
|
||||
|
||||
return result.data;
|
||||
}
|
||||
|
||||
private async fetchByUid(instrumentUid: string): Promise<TBankInstrument | null> {
|
||||
const instrumentsClient = this.tbankClient.getServiceClient('InstrumentsService') as any;
|
||||
const response = await this.tbankClient.callUnary<
|
||||
{ idType: string; id: string },
|
||||
TBankInstrumentResponse
|
||||
>(
|
||||
'InstrumentsService/GetInstrumentBy',
|
||||
instrumentsClient.getInstrumentBy.bind(instrumentsClient),
|
||||
{ idType: 'INSTRUMENT_ID_TYPE_UID', id: instrumentUid },
|
||||
);
|
||||
|
||||
return response.instrument ?? null;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,70 @@
|
||||
import { NotFoundException } from '@nestjs/common';
|
||||
import { CacheService } from '../../cache/cache.service';
|
||||
import { BrokerAccountsService } from './broker-accounts.service';
|
||||
import { BrokerInstrumentsService } from './broker-instruments.service';
|
||||
import { BrokerPortfolioService } from './broker-portfolio.service';
|
||||
import { TBankClientService } from './tbank-client.service';
|
||||
|
||||
describe('BrokerPortfolioService', () => {
|
||||
const accounts = { findById: vi.fn() } as unknown as BrokerAccountsService;
|
||||
const instruments = { findByInstrumentUid: vi.fn() } as unknown as BrokerInstrumentsService;
|
||||
const client = { getServiceClient: vi.fn(), callUnary: vi.fn() } as unknown as TBankClientService;
|
||||
const cache = { getOrFetch: vi.fn() } as unknown as CacheService;
|
||||
|
||||
beforeEach(() => {
|
||||
vi.clearAllMocks();
|
||||
});
|
||||
|
||||
it('throws 404 for excluded or missing account', async () => {
|
||||
vi.mocked(accounts.findById).mockResolvedValue(null);
|
||||
const service = new BrokerPortfolioService(accounts, instruments, client, cache);
|
||||
|
||||
await expect(service.getPortfolio('missing')).rejects.toThrow(NotFoundException);
|
||||
});
|
||||
|
||||
it('fetches portfolio and positions through cache', async () => {
|
||||
vi.mocked(accounts.findById).mockResolvedValue({
|
||||
id: 'acc-1',
|
||||
type: 'brokerage',
|
||||
name: 'Broker',
|
||||
status: 'ACCOUNT_STATUS_OPEN',
|
||||
openedAt: null,
|
||||
accessLevel: null,
|
||||
});
|
||||
vi.mocked(cache.getOrFetch).mockImplementation(
|
||||
async (_prefix: string, _parts: string[], fetchFn: () => Promise<unknown>) => ({
|
||||
data: await fetchFn(),
|
||||
fromCache: false,
|
||||
cachedAt: null,
|
||||
}),
|
||||
);
|
||||
vi.mocked(client.getServiceClient).mockReturnValue({
|
||||
getPortfolio: vi.fn(),
|
||||
getPositions: vi.fn(),
|
||||
} as any);
|
||||
vi.mocked(client.callUnary)
|
||||
.mockResolvedValueOnce({
|
||||
accountId: 'acc-1',
|
||||
totalAmountPortfolio: { currency: 'rub', units: '1000', nano: 0 },
|
||||
positions: [],
|
||||
})
|
||||
.mockResolvedValueOnce({
|
||||
accountId: 'acc-1',
|
||||
money: [{ currency: 'rub', units: '1000', nano: 0 }],
|
||||
blocked: [],
|
||||
securities: [],
|
||||
});
|
||||
|
||||
const service = new BrokerPortfolioService(accounts, instruments, client, cache);
|
||||
const result = await service.getPortfolio('acc-1');
|
||||
|
||||
expect(result.data.account.id).toBe('acc-1');
|
||||
expect(result.data.cash[0].value).toBe(1000);
|
||||
expect(cache.getOrFetch).toHaveBeenCalledWith(
|
||||
'tbank:portfolio',
|
||||
['acc-1'],
|
||||
expect.any(Function),
|
||||
'tbankPortfolioTtl',
|
||||
);
|
||||
});
|
||||
});
|
||||
@ -0,0 +1,81 @@
|
||||
import { Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { CacheService } from '../../cache/cache.service';
|
||||
import { mapBrokerPortfolio } from '../mappers/portfolio.mapper';
|
||||
import { TBANK_CACHE_KEYS } from '../tbank.config';
|
||||
import type { BrokerPortfolio } from '../types/broker.types';
|
||||
import type {
|
||||
TBankInstrument,
|
||||
TBankPortfolioResponse,
|
||||
TBankPositionsResponse,
|
||||
} from '../types/tbank-proto.types';
|
||||
import { BrokerAccountsService } from './broker-accounts.service';
|
||||
import { BrokerInstrumentsService } from './broker-instruments.service';
|
||||
import { TBankClientService } from './tbank-client.service';
|
||||
|
||||
@Injectable()
|
||||
export class BrokerPortfolioService {
|
||||
constructor(
|
||||
private readonly accountsService: BrokerAccountsService,
|
||||
private readonly instrumentsService: BrokerInstrumentsService,
|
||||
private readonly tbankClient: TBankClientService,
|
||||
private readonly cacheService: CacheService,
|
||||
) {}
|
||||
|
||||
async getPortfolio(accountId: string): Promise<{
|
||||
data: BrokerPortfolio;
|
||||
meta: { fromCache: boolean; cachedAt: string | null };
|
||||
}> {
|
||||
const account = await this.accountsService.findById(accountId);
|
||||
if (!account) throw new NotFoundException('Broker account not found');
|
||||
|
||||
const result = await this.cacheService.getOrFetch(
|
||||
TBANK_CACHE_KEYS.portfolio,
|
||||
[accountId],
|
||||
async () => {
|
||||
const operationsClient = this.tbankClient.getServiceClient('OperationsService') as any;
|
||||
const [portfolio, positions] = await Promise.all([
|
||||
this.tbankClient.callUnary<
|
||||
{ accountId: string; currency: string },
|
||||
TBankPortfolioResponse
|
||||
>(
|
||||
'OperationsService/GetPortfolio',
|
||||
operationsClient.getPortfolio.bind(operationsClient),
|
||||
{ accountId, currency: 'RUB' },
|
||||
),
|
||||
this.tbankClient.callUnary<{ accountId: string }, TBankPositionsResponse>(
|
||||
'OperationsService/GetPositions',
|
||||
operationsClient.getPositions.bind(operationsClient),
|
||||
{ accountId },
|
||||
),
|
||||
]);
|
||||
|
||||
const instrumentMap = await this.buildInstrumentMap(portfolio);
|
||||
|
||||
return mapBrokerPortfolio({ account, portfolio, positions, instruments: instrumentMap });
|
||||
},
|
||||
'tbankPortfolioTtl',
|
||||
);
|
||||
|
||||
return {
|
||||
data: result.data,
|
||||
meta: { fromCache: result.fromCache, cachedAt: result.cachedAt },
|
||||
};
|
||||
}
|
||||
|
||||
private async buildInstrumentMap(
|
||||
portfolio: TBankPortfolioResponse,
|
||||
): Promise<Map<string, Partial<TBankInstrument>>> {
|
||||
const ids = Array.from(
|
||||
new Set(
|
||||
(portfolio.positions ?? []).map((position) => position.instrumentUid).filter(Boolean),
|
||||
),
|
||||
) as string[];
|
||||
const entries = await Promise.all(
|
||||
ids.map(async (id) => [id, await this.instrumentsService.findByInstrumentUid(id)] as const),
|
||||
);
|
||||
|
||||
return new Map(
|
||||
entries.filter((entry): entry is readonly [string, TBankInstrument] => entry[1] !== null),
|
||||
);
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,17 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { Controller, Get, Param } from '@nestjs/common';
|
||||
import { ApiBearerAuth, ApiOkResponse, ApiOperation, ApiTags } from '@nestjs/swagger';
|
||||
import { BrokerAccountsEnvelopeDto } from './dto/broker-envelope.dto';
|
||||
import { BrokerAccountsEnvelopeDto, BrokerPortfolioEnvelopeDto } from './dto/broker-envelope.dto';
|
||||
import { BrokerAccountsService } from './services/broker-accounts.service';
|
||||
import { BrokerPortfolioService } from './services/broker-portfolio.service';
|
||||
|
||||
@ApiTags('Broker')
|
||||
@ApiBearerAuth()
|
||||
@Controller('broker')
|
||||
export class TBankController {
|
||||
constructor(private readonly brokerAccountsService: BrokerAccountsService) {}
|
||||
constructor(
|
||||
private readonly brokerAccountsService: BrokerAccountsService,
|
||||
private readonly brokerPortfolioService: BrokerPortfolioService,
|
||||
) {}
|
||||
|
||||
@Get('accounts')
|
||||
@ApiOperation({ summary: 'Get open T-Bank brokerage and IIS accounts' })
|
||||
@ -15,4 +19,11 @@ export class TBankController {
|
||||
async getAccounts() {
|
||||
return this.brokerAccountsService.findAll();
|
||||
}
|
||||
|
||||
@Get('accounts/:accountId/portfolio')
|
||||
@ApiOperation({ summary: 'Get T-Bank broker account portfolio with cash and positions' })
|
||||
@ApiOkResponse({ type: BrokerPortfolioEnvelopeDto })
|
||||
async getPortfolio(@Param('accountId') accountId: string) {
|
||||
return this.brokerPortfolioService.getPortfolio(accountId);
|
||||
}
|
||||
}
|
||||
|
||||
@ -1,11 +1,23 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TBankController } from './tbank.controller';
|
||||
import { BrokerAccountsService } from './services/broker-accounts.service';
|
||||
import { BrokerInstrumentsService } from './services/broker-instruments.service';
|
||||
import { BrokerPortfolioService } from './services/broker-portfolio.service';
|
||||
import { TBankClientService } from './services/tbank-client.service';
|
||||
|
||||
@Module({
|
||||
controllers: [TBankController],
|
||||
providers: [TBankClientService, BrokerAccountsService],
|
||||
exports: [TBankClientService, BrokerAccountsService],
|
||||
providers: [
|
||||
TBankClientService,
|
||||
BrokerAccountsService,
|
||||
BrokerInstrumentsService,
|
||||
BrokerPortfolioService,
|
||||
],
|
||||
exports: [
|
||||
TBankClientService,
|
||||
BrokerAccountsService,
|
||||
BrokerInstrumentsService,
|
||||
BrokerPortfolioService,
|
||||
],
|
||||
})
|
||||
export class TBankModule {}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user