def _parse_trades(self, xml_tree: ET.ElementTree): for rec in xml_tree.findall('spot_main_deals_conclusion/item'): f = rec.attrib qnty = -1 * float(f['sell_qnty']) if 'sell_qnty' in f else float(f['buy_qnty']) assert float(int(qnty)) == qnty ticker = self._tickers.get( grn=f['security_grn_code'] if 'security_grn_code' in f else None, name=f['security_name'], ) if ticker.kind == TickerKind.Bond: price = Money(f['volume_currency'], Currency.parse(f['price_currency_code'])) / abs(int(qnty)) else: price = Money(f['price'], Currency.parse(f['price_currency_code'])) expected_volume = abs(int(qnty)) * price actual_volume = Money(f['volume_currency'], Currency.parse(f['price_currency_code'])) assert expected_volume == actual_volume, f'expected_volume({expected_volume} = {qnty} * {price}) != ({actual_volume}) for {f}' self._trades.append(Trade( ticker=ticker, datetime=_parse_datetime(f['conclusion_time']), settle_date=_parse_datetime(f['execution_date']), quantity=int(qnty), price=price, fee=Money(f['broker_commission'], Currency.parse(f['broker_commission_currency_code'])), ))
def _parse_dividends(self, f: Dict[str, str]): div_symbol, div_type = _parse_dividend_description(f['Description']) ticker = self._tickers.get_ticker(div_symbol, TickerKind.Stock) date = _parse_date(f['Date']) amount = Money(f['Amount'], Currency.parse(f['Currency'])) if amount.amount < 0: assert 'Reversal' in f[ 'Description'], f'unsupported dividend with negative amount: {f}' for i, v in enumerate(self._dividends): if v.dtype == div_type and v.ticker == ticker and v.date == date and v.amount == -1 * amount: self._dividends[i] = Dividend( dtype=div_type, ticker=ticker, date=date, amount=amount + v.amount, tax=v.tax, ) return assert amount.amount > 0, f'unsupported dividend with non positive amount: {f}' self._dividends.append( Dividend( dtype=div_type, ticker=ticker, date=date, amount=amount, tax=Money(0, amount.currency), ))
def _parse_trades(self, f: Dict[str, str]): ticker_kind = _parse_tickerkind(f['Asset Category']) if ticker_kind == TickerKind.Forex: logging.warning( f'Skipping FOREX trade (not supported yet), your final report may be incorrect! {f}' ) return ticker = self._tickers.get_ticker(f['Symbol'], ticker_kind) quantity_multiplier = self._tickers.get_multiplier(ticker) currency = Currency.parse(f['Currency']) dt = _parse_datetime(f['Date/Time']) settle_date = self._settle_dates.get((ticker.symbol, dt)) assert settle_date is not None self._trades.append( Trade( ticker=ticker, datetime=dt, settle_date=settle_date, quantity=int(f['Quantity']) * quantity_multiplier, price=Money(f['T. Price'], currency), fee=Money(f['Comm/Fee'], currency), ))
def _parse_withholding_tax(self, f: Dict[str, str]): div_symbol, div_type = _parse_dividend_description(f['Description']) ticker = self._tickers.get_ticker(div_symbol, TickerKind.Stock) date = _parse_date(f['Date']) tax_amount = Money(f['Amount'], Currency.parse(f['Currency'])) assert tax_amount.amount < 0 tax_amount *= -1 found = False for i, v in enumerate(self._dividends): if v.ticker == ticker and v.date == date and v.dtype == div_type: assert v.tax.amount == 0 assert v.amount.currency == tax_amount.currency self._dividends[i] = Dividend( dtype=v.dtype, ticker=v.ticker, date=v.date, amount=v.amount, tax=tax_amount, ) found = True break if not found: raise Exception(f'dividend not found for {ticker} on {date}')
def _parse_cash_report(self, f: Dict[str, str]): currency_code = f['Currency'] if currency_code != 'Base Currency Summary': currency = Currency.parse(currency_code) description = f['Currency Summary'] amount = Money(f['Total'], currency) self._cash.append(Cash(description, amount))
def _parse_withholding_tax(self, f: Dict[str, str]): div_symbol, div_type = _parse_dividend_description(f['Description']) ticker = self._tickers.get_ticker(div_symbol, TickerKind.Stock) date = _parse_date(f['Date']) tax_amount = Money(f['Amount'], Currency.parse(f['Currency'])) tax_amount *= -1 found = False for i, v in enumerate(self._dividends): # difference in reports for the same past year, but generated in different time # read more at https://github.com/cdump/investments/issues/17 cash_choice_hack = (v.dtype == 'Cash Dividend' and div_type == 'Choice Dividend') if v.ticker == ticker and v.date == date and (v.dtype == div_type or cash_choice_hack): assert v.amount.currency == tax_amount.currency self._dividends[i] = Dividend( dtype=v.dtype, ticker=v.ticker, date=v.date, amount=v.amount, tax=v.tax + tax_amount, ) found = True break if not found: raise Exception(f'dividend not found for {ticker} on {date}')
def _parse_non_trade_operations(self, xml_tree: ET.ElementTree): bonds_redemption = {} for rec in xml_tree.findall('spot_non_trade_security_operations/item'): f = rec.attrib if 'Снятие ЦБ с учета. Погашение облигаций' in f['comment']: ticker = self._tickers.get(grn=f['grn_code']) key = (ticker, _parse_datetime(f['operation_date'])) assert key not in bonds_redemption qnty = float(f['quantity']) assert float(int(qnty)) == qnty bonds_redemption[key] = int(qnty) elif '(Конвертация ЦБ)' in f['comment']: self._parse_cb_convertation(f) else: # print(f) # exit(1) pass for rec in xml_tree.findall('spot_non_trade_money_operations/item'): f = rec.attrib comment = f['comment'] if any(comment.startswith(p) for p in ('Поставлены на торги средства клиента', 'Перевод денежных средств с клиента')): self._deposits_and_withdrawals.append(( _parse_datetime(f['operation_date']), Money(f['amount'], Currency.parse(f['currency_code'])), )) continue if comment.startswith('Выплата дохода клиент'): self._parse_money_payment(f, bonds_redemption) continue known_prefixes = [ 'Комиссия ', 'Вознаграждение ', 'Ежегодная комиссия за', 'Возмещение за депозитарные услуги', 'Депозитарная комиссия за операции', 'Удержан налог на доход по дивидендам', 'Налог на доход за', 'Удержан налог на доход с клиента ', 'Перечисление дохода по акциям', 'Возврат ошибочно удержанного налога с клиента', 'Возврат излишне удержанного налога с клиента', 'Проценты по предоставленным займам ЦБ', 'Списаны средства клиента', ] if any(comment.startswith(p) for p in known_prefixes): continue raise Exception(f'unsupported description {f}') assert not bonds_redemption, 'not empty'
def _parse_dividends(self, f: Dict[str, str]): div_symbol, div_type = _parse_dividend_description(f['Description']) ticker = self._tickers.get_ticker(div_symbol, TickerKind.Stock) date = _parse_date(f['Date']) amount = Money(f['Amount'], Currency.parse(f['Currency'])) assert amount.amount > 0 self._dividends.append(Dividend( dtype=div_type, ticker=ticker, date=date, amount=amount, tax=Money(0, amount.currency), ))
def _parse_deposits(self, f: Dict[str, str]): currency = Currency.parse(f['Currency']) date = _parse_date(f['Settle Date']) amount = Money(f['Amount'], currency) self._deposits_and_withdrawals.append((date, amount))
def _parse_money_payment(self, f, bonds_redemption): comment = f['comment'] currency = Currency.parse(f['currency_code']) money_total = Money(f['amount'], currency) dt = _parse_datetime(f['operation_date']) m1 = re.match(r'^Выплата дохода клиент (\w+) дивиденды (?P<name>.*?) налог к удержанию (?P<tax>[0-9.]+) рублей$', comment) m2 = re.match(r'^Выплата дохода клиент (\w+) дивиденды (?P<name>.*?) налог 0.00 рублей удержан эмитентом$', comment) if m1 is not None or m2 is not None: if m1 is not None: name, tax = m1.group('name'), m1.group('tax') elif m2 is not None: name, tax = m2.group('name'), '0' self._dividends.append(Dividend( dtype='', ticker=self._tickers.get_by_dividend_name(name), date=dt.date(), amount=money_total, tax=Money(tax, currency), )) return m = re.match(r'^Выплата дохода клиент (\w+) \(Выкуп (?P<name>[^,]+), (?P<isin>\w+), количество (?P<quantity>\d+)\) налог не удерживается$', comment) if m is not None: isin, quantity = m.group('isin'), int(m.group('quantity')) self._trades.append(Trade( ticker=self._tickers.get(isin=isin), datetime=dt, settle_date=dt, quantity=-1 * quantity, price=money_total / quantity, fee=Money(0, currency), )) return m = re.match(r'^Выплата дохода клиент (\w+) \((?P<type>НКД \d+|Погашение) (?P<name>.*?)\) налог (к удержанию 0.00 рублей|не удерживается)$', comment) if m is not None: ticker = self._tickers.get(name=m.group('name')) if m.group('type').startswith('НКД'): # WARNING: do not use for tax calculation! for (price, quantity) in ((Money(0, currency), 1), (money_total, -1)): self._trades.append(Trade( ticker=ticker, datetime=dt, settle_date=dt, quantity=quantity, price=price, fee=Money(0, currency), )) return if m.group('type') == 'Погашение': key = (ticker, dt) qnty = bonds_redemption[key] self._trades.append(Trade( ticker=ticker, datetime=dt, settle_date=dt, quantity=qnty, price=-1 * money_total / int(qnty), fee=Money(0, currency), )) del bonds_redemption[key] return raise Exception(f'Unknown type {m.group("type")}') raise Exception(f'unsupported description {f}')
def test_parse_failure(): with pytest.raises(ValueError): assert Currency.parse('invalid')
def test_parse(search_value: str, res: Currency): assert Currency.parse(search_value) is res
def _parse_interests(self, f: Dict[str, str]): currency = Currency.parse(f['Currency']) date = _parse_date(f['Date']) amount = Money(f['Amount'], currency) description = f['Description'] self._interests.append(Interest(date, amount, description))
def _parse_fees(self, f: Dict[str, str]): currency = Currency.parse(f['Currency']) date = _parse_date(f['Date']) amount = Money(f['Amount'], currency) description = f"{f['Subtitle']} - {f['Description']}" self._fees.append(Fee(date, amount, description))