Пример #1
0
    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'])),
            ))
Пример #2
0
    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),
            ))
Пример #3
0
    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),
            ))
Пример #4
0
    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}')
Пример #5
0
 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))
Пример #6
0
    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}')
Пример #7
0
    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'
Пример #8
0
    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),
        ))
Пример #9
0
 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))
Пример #10
0
    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}')
Пример #11
0
def test_parse_failure():
    with pytest.raises(ValueError):
        assert Currency.parse('invalid')
Пример #12
0
def test_parse(search_value: str, res: Currency):
    assert Currency.parse(search_value) is res
Пример #13
0
 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))
Пример #14
0
 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))