import type { BrokerOperation, BrokerPosition } from '../../api/responses'; export type BrokerPositionGroup = 'shares' | 'bonds' | 'other'; export type BrokerOperationImpact = 'adds' | 'reduces' | 'neutral' | 'unknown'; type BrokerInstrumentLinkInput = { ticker: string | null; instrumentType: string | null; classCode: string | null; }; const STOCK_CLASS_CODES = new Set(['TQBR']); const BOND_CLASS_CODES = new Set(['TQOB', 'TQCB', 'TQIR']); const TRADE_TYPES = new Set([ 'OPERATION_TYPE_BUY', 'OPERATION_TYPE_BUY_CARD', 'OPERATION_TYPE_SELL', 'OPERATION_TYPE_SELL_CARD', 'OPERATION_TYPE_BUY_MARGIN', 'OPERATION_TYPE_SELL_MARGIN', 'OPERATION_TYPE_DELIVERY_BUY', 'OPERATION_TYPE_DELIVERY_SELL', ]); const BOND_REPAYMENT_TYPES = new Set([ 'OPERATION_TYPE_BOND_REPAYMENT', 'OPERATION_TYPE_BOND_REPAYMENT_FULL', ]); const INCOME_TYPES = new Set(['OPERATION_TYPE_COUPON', 'OPERATION_TYPE_DIVIDEND']); const TAX_TYPES = new Set([ 'OPERATION_TYPE_TAX', 'OPERATION_TYPE_BOND_TAX', 'OPERATION_TYPE_DIVIDEND_TAX', 'OPERATION_TYPE_TAX_CORRECTION', 'OPERATION_TYPE_TAX_CORRECTION_COUPON', ]); const FEE_TYPES = new Set([ 'OPERATION_TYPE_BROKER_FEE', 'OPERATION_TYPE_SERVICE_FEE', 'OPERATION_TYPE_MARGIN_FEE', 'OPERATION_TYPE_SUCCESS_FEE', ]); const TRANSFER_INPUT_TYPES = new Set([ 'OPERATION_TYPE_INPUT', 'OPERATION_TYPE_INPUT_SWIFT', 'OPERATION_TYPE_INPUT_ACQUIRING', 'OPERATION_TYPE_INP_MULTI', ]); const TRANSFER_OUTPUT_TYPES = new Set([ 'OPERATION_TYPE_OUTPUT', 'OPERATION_TYPE_OUTPUT_SWIFT', 'OPERATION_TYPE_OUTPUT_ACQUIRING', 'OPERATION_TYPE_OUT_MULTI', ]); const SECURITY_TRANSFER_TYPES = new Set([ 'OPERATION_TYPE_INPUT_SECURITIES', 'OPERATION_TYPE_OUTPUT_SECURITIES', 'OPERATION_TYPE_TRANS_IIS_BS', 'OPERATION_TYPE_TRANS_BS_BS', ]); const OPERATION_TYPE_LABELS: Record = { OPERATION_TYPE_BUY: 'Покупка', OPERATION_TYPE_BUY_CARD: 'Покупка', OPERATION_TYPE_SELL: 'Продажа', OPERATION_TYPE_SELL_CARD: 'Продажа', OPERATION_TYPE_BUY_MARGIN: 'Покупка с маржой', OPERATION_TYPE_SELL_MARGIN: 'Продажа с маржой', OPERATION_TYPE_DELIVERY_BUY: 'Поставка покупки', OPERATION_TYPE_DELIVERY_SELL: 'Поставка продажи', OPERATION_TYPE_COUPON: 'Выплата купона', OPERATION_TYPE_DIVIDEND: 'Дивиденды', OPERATION_TYPE_BOND_REPAYMENT: 'Погашение облигации', OPERATION_TYPE_BOND_REPAYMENT_FULL: 'Полное погашение облигации', OPERATION_TYPE_TAX: 'Налог', OPERATION_TYPE_BOND_TAX: 'Налог по облигациям', OPERATION_TYPE_DIVIDEND_TAX: 'Налог на дивиденды', OPERATION_TYPE_TAX_CORRECTION: 'Корректировка налога', OPERATION_TYPE_TAX_CORRECTION_COUPON: 'Корректировка налога по купону', OPERATION_TYPE_BROKER_FEE: 'Комиссия брокера', OPERATION_TYPE_SERVICE_FEE: 'Комиссия за обслуживание', OPERATION_TYPE_MARGIN_FEE: 'Комиссия за маржу', OPERATION_TYPE_SUCCESS_FEE: 'Комиссия за результат', OPERATION_TYPE_INPUT: 'Пополнение', OPERATION_TYPE_OUTPUT: 'Вывод средств', OPERATION_TYPE_INPUT_SECURITIES: 'Зачисление бумаг', OPERATION_TYPE_OUTPUT_SECURITIES: 'Списание бумаг', }; export const BROKER_OPERATION_TYPE_OPTIONS: ReadonlyArray< Readonly<{ value: string; label: string }> > = Object.freeze( Object.entries(OPERATION_TYPE_LABELS) .map(([value, label]) => Object.freeze({ value, label })) .sort((left, right) => left.label.localeCompare(right.label, 'ru')), ); const BROKER_OPERATION_TYPES = new Set(BROKER_OPERATION_TYPE_OPTIONS.map(({ value }) => value)); export function isBrokerOperationType(value: string | null): value is string { return value !== null && BROKER_OPERATION_TYPES.has(value); } export function getBrokerPositionGroup( position: Pick, ): BrokerPositionGroup { const instrumentType = position.instrumentType?.toLowerCase(); if (instrumentType === 'share') return 'shares'; if (instrumentType === 'bond') return 'bonds'; return 'other'; } export function getBrokerInstrumentPath(input: BrokerInstrumentLinkInput): string | null { const ticker = input.ticker?.trim().toUpperCase(); if (!ticker) return null; const instrumentType = input.instrumentType?.toLowerCase(); const classCode = input.classCode?.toUpperCase() ?? null; if (instrumentType === 'share') { return `/stocks/${encodeURIComponent(ticker)}`; } if (instrumentType === 'bond') { return `/bonds/${encodeURIComponent(ticker)}`; } if (instrumentType) return null; if (classCode && STOCK_CLASS_CODES.has(classCode)) return `/stocks/${encodeURIComponent(ticker)}`; if (classCode && BOND_CLASS_CODES.has(classCode)) return `/bonds/${encodeURIComponent(ticker)}`; return null; } export function getBrokerOperationTypeLabel( operation: Pick, ): string { const knownLabel = OPERATION_TYPE_LABELS[operation.type]; if (knownLabel) return knownLabel; if (operation.description) return operation.description; return operation.type .replace(/^OPERATION_TYPE_/, '') .replace(/_/g, ' ') .toLowerCase(); } export function getBrokerOperationImpact( operation: Pick, ): BrokerOperationImpact { if ( TRADE_TYPES.has(operation.type) || BOND_REPAYMENT_TYPES.has(operation.type) || SECURITY_TRANSFER_TYPES.has(operation.type) ) { return 'neutral'; } if (INCOME_TYPES.has(operation.type)) return 'adds'; if (TAX_TYPES.has(operation.type) || FEE_TYPES.has(operation.type)) return 'reduces'; if (TRANSFER_INPUT_TYPES.has(operation.type)) return 'adds'; if (TRANSFER_OUTPUT_TYPES.has(operation.type)) return 'reduces'; if (operation.category === 'tax' || operation.category === 'fee') return 'reduces'; if (operation.category === 'income' && (operation.payment?.value ?? 0) > 0) return 'adds'; return 'unknown'; }