Skip to content

Commit

Permalink
Добавлена утилита ibdds: помошник для подготовки отчёта о движении де…
Browse files Browse the repository at this point in the history
…нежных средств по счёту в IB (#33)
  • Loading branch information
esemi authored Jan 17, 2021
1 parent fd8e64c commit eb1c60c
Show file tree
Hide file tree
Showing 13 changed files with 526 additions and 274 deletions.
2 changes: 1 addition & 1 deletion .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ max-cognitive-score = 24
max-cognitive-average = 24

# magic methods includes
max-methods = 18
max-methods = 20

# function arguments
max-arguments = 8
Expand Down
25 changes: 20 additions & 5 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# Investments
Библиотека для анализа брокерских отчетов + утилита для подготовки налоговой отчетности
Библиотека для анализа брокерских отчетов + утилиты для подготовки налоговой отчетности

![Tests status](https://github.com/cdump/investments/workflows/tests/badge.svg)

Expand All @@ -24,16 +24,31 @@ $ pip install investments --upgrade --user
*Пример отчета:*
![ibtax report example](./images/ibtax_2020.jpg)


### Запуск
Запустить `ibtax` указав в `--activity-reports-dir` и `--confirmation-reports-dir` директории отчетами в формате `.csv` (см. *Подготовка отчетов Interactive Brokers*)

Важно, чтобы csv-отчеты `activity` и `confirmation` были в разных директориях!

### Подготовка отчетов Interactive Brokers

## Утилита ibdds
Утилита для подготовки отчёта о движении денежных средств по счетам у брокера Interactive Brokers (USA) для резидентов РФ

- выводит отчёт по каждой валюте счёта отдельно
- вывод максимально приближен к форме отчёта о ДДС

*Пример отчета:*
![ibdds report example](./images/ibdds_2020.png)

### Запуск
Запустить `ibdds` указав в `--activity-report-filepath` путь до отчёта о активности по счёту в формате `.csv` (см. *Подготовка отчетов Interactive Brokers*)

Важно: утилита не проверяет период отчёта `activity` и для корректной подготовки налоговой отчётности необходимо указать передать путь до отчёта за один год.


## Подготовка отчетов Interactive Brokers
Для работы нужно выгрузить из [личного кабинета](https://www.interactivebrokers.co.uk/sso/Login) два типа отчетов: *Activity statement* (сделки, дивиденды, информация по инструментам и т.п.) и *Trade Confirmation* (settlement date, необходимая для правильной конвертации сумм по курсу ЦБ)

#### Activity statement
### Activity statement
Для загрузки нужно перейти в **Reports / Tax Docs** > **Default Statements** > **Activity**

Выбрать `Format: CSV` и скачать данные за все доступное время (`Perioid: Annual` для прошлых лет + `Period: Year to Date` для текущего года)
Expand All @@ -42,7 +57,7 @@ $ pip install investments --upgrade --user

![Activity Statement](./images/ib_report_activity.jpg)

#### Trade Confirmation
### Trade Confirmation

Для загрузки нужно перейти в **Reports / Tax Docs** > **Flex Queries** > **Trade Confirmation Flex Query** и создать новый тип отчетов, выбрав в **Sections** > **Trade Confirmation** все пункты в группе **Executions**, остальные настройки - как на скриншоте:

Expand Down
Binary file added images/ibdds_2020.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
18 changes: 18 additions & 0 deletions investments/cash.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
from typing import NamedTuple

from investments.money import Money


class Cash(NamedTuple):
"""
Движения денежных средств по счёту.
По итогам года нужно отчитаться о всех зачислениях/списаниях по счёту у иностранного брокера в соответствии со статьёй 12 173-ФЗ «О валютном регулировании»
"""

description: str
amount: Money

def __str__(self):
return f'Cash ({self.amount} {self.description})'
18 changes: 18 additions & 0 deletions investments/currency.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,21 @@ def __str__(self):
elif self == Currency.EUR:
return '€'
return self.__repr__(self)

def iso_numeric_code(self) -> str:
"""
Код валюты в соответствии с общероссийским классификатором валют (ОК (МК (ИСО 4217) 003-97) 014-2000).
see https://classifikators.ru/okv
Raises:
ValueError: if currency is unsupported
"""
if self == Currency.USD:
return '840'
elif self == Currency.RUB:
return '643'
elif self == Currency.EUR:
return '978'
raise ValueError(self)
3 changes: 3 additions & 0 deletions investments/ibdds/__main__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from investments.ibdds.ibdds import main

main()
95 changes: 95 additions & 0 deletions investments/ibdds/ibdds.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
"""
Утилита для подготовки отчёта о движении денежных средств для брокера Interactive Brokers (USA).
Актуальна на 15.01.2021
Отчёт нужно подавать каждый год до 1 июня или в течение месяца после закрытия счёта.
В будущем (уже в отчёте за 2021 год) обещают обновить требования в пользу большей детализации, но пока так.
@see статью 12 173-ФЗ «О валютном регулировании»
"""

import argparse
import csv
import logging
from pathlib import Path
from typing import List

from tabulate import tabulate

from investments.cash import Cash
from investments.money import Money
from investments.report_parsers.ib import InteractiveBrokersReportParser


class InteractiveBrokersCashReportParser(InteractiveBrokersReportParser):
def parse_csv(self, *, activity_report_filepath: Path, **kwargs):
with open(activity_report_filepath, newline='') as activity_fh:
self._real_parse_activity_csv(csv.reader(activity_fh, delimiter=','), {
'Cash Report': self._parse_cash_report,
})


def parse_reports(activity_report_filepath: str) -> InteractiveBrokersCashReportParser:
parser_object = InteractiveBrokersCashReportParser()

activity_report = Path(activity_report_filepath)
logging.info(f'Activity report {activity_report}')

logging.info('start reports parse')
parser_object.parse_csv(activity_report_filepath=activity_report)
logging.info(f'end reports parse {parser_object}')

return parser_object


def dds_specific_round(source_amount: Money) -> Money:
return (source_amount / 1000).round(3)


def show_report(cash: List[Cash]):
currencies = set(map(lambda x: x.amount.currency, cash))
logging.info(f'currency={currencies}')

for currency in currencies:
operations = [op for op in cash if op.amount.currency == currency]
begin_amount = dds_specific_round([op.amount for op in operations if op.description == 'Starting Cash'][0])
end_amount = dds_specific_round([op.amount for op in operations if op.description == 'Ending Cash'][0])

deposits = [op.amount for op in operations if 'Cash' not in op.description and op.amount > Money(0, op.amount.currency)]
deposits_amount = dds_specific_round(sum(deposits) if deposits else Money(0, currency))

withdrawals = [op.amount for op in operations if 'Cash' not in op.description and op.amount < Money(0, op.amount.currency)]
withdrawals_amount = dds_specific_round(sum(withdrawals) if withdrawals else Money(0, currency))

report = [
[f'{currency.name} {currency.iso_numeric_code()}', 'Сумма в тысячах единиц'],
['Остаток денежных средств на счете на начало отчетного периода', begin_amount],
['Зачислено денежных средств за отчетный период', deposits_amount],
['Списано денежных средств за отчетный период', abs(withdrawals_amount)],
['Остаток денежных средств на счете на конец отчетного периода', end_amount],
]

print('\n')
print(tabulate(report, headers='firstrow', tablefmt='presto', colalign=('right', 'decimal')))
print('\n')


def main():
parser = argparse.ArgumentParser()
parser.add_argument('--activity-report-filepath', type=str, required=True, help='InteractiveBrokers .csv activity report file path')
parser.add_argument('--verbose', nargs='?', default=False, const=True, help='details mode')
args = parser.parse_args()

if args.verbose:
logging.basicConfig(level=logging.INFO)

parser_object = parse_reports(args.activity_report_filepath)

cash_report = parser_object.cash
logging.info(f'cash report={cash_report}')

show_report(cash_report)


if __name__ == '__main__':
main()
15 changes: 12 additions & 3 deletions investments/ibtax/ibtax.py
Original file line number Diff line number Diff line change
Expand Up @@ -35,8 +35,14 @@ def prepare_trades_report(finished_trades: List[FinishedTrade], cbr_client_usd:
df['fee_per_piece'] = df.apply(lambda x: x['fee_per_piece'].round(digits=fee_round_digits), axis=1)
df['fee'] = df.apply(lambda x: (x['fee_per_piece'] * abs(x['quantity'])).round(digits=fee_round_digits), axis=1)

df['total'] = df.apply(lambda x: compute_total_cost(x['quantity'], x['price'], x['fee_per_piece']).round(digits=2), axis=1)
df['total_rub'] = df.apply(lambda x: compute_total_cost(x['quantity'], x['price_rub'], x['fee_per_piece_rub']).round(digits=2), axis=1)
df['total'] = df.apply(
lambda x: compute_total_cost(x['quantity'], x['price'], x['fee_per_piece']).round(digits=2),
axis=1,
)
df['total_rub'] = df.apply(
lambda x: compute_total_cost(x['quantity'], x['price_rub'], x['fee_per_piece_rub']).round(digits=2),
axis=1,
)

df = df.join(cbr_client_usd.dataframe['rate'].rename('settle_rate'), how='left', on=tax_date_column)
df = df.join(cbr_client_usd.dataframe['rate'].rename('fee_rate'), how='left', on=trade_date_column)
Expand Down Expand Up @@ -69,7 +75,10 @@ def prepare_dividends_report(dividends: List[Dividend], cbr_client_usd: cbr.Exch
df['tax_paid_rub'] = df.apply(lambda x: cbr_client_usd.convert_to_rub(x['tax_paid'], x[operation_date_column]).round(digits=2), axis=1)

if verbose:
df['tax_rate'] = df.apply(lambda x: round(x['tax_paid'].amount * 100 / x['amount'].amount, 2), axis=1)
df['tax_rate'] = df.apply(
lambda x: round(x['tax_paid'].amount * 100 / x['amount'].amount, 2),
axis=1,
)

return df

Expand Down
15 changes: 15 additions & 0 deletions investments/report_parsers/ib.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import re
from typing import Dict, Iterator, List, Tuple

from investments.cash import Cash
from investments.currency import Currency
from investments.dividend import Dividend
from investments.fees import Fee
Expand Down Expand Up @@ -102,6 +103,7 @@ def __init__(self):
self._dividends = []
self._fees: List[Fee] = []
self._interests: List[Interest] = []
self._cash: List[Cash] = []
self._deposits_and_withdrawals = []
self._tickers = TickersStorage()
self._settle_dates = {}
Expand Down Expand Up @@ -129,6 +131,10 @@ def fees(self) -> List[Fee]:
def interests(self) -> List[Interest]:
return self._interests

@property
def cash(self) -> List[Cash]:
return self._cash

def parse_csv(self, *, activity_csvs: List[str], trade_confirmation_csvs: List[str]):
# 1. parse tickers info
for ac_fname in activity_csvs:
Expand Down Expand Up @@ -158,6 +164,7 @@ def parse_csv(self, *, activity_csvs: List[str], trade_confirmation_csvs: List[s
# 'Mark-to-Market Performance Summary',
# 'Net Asset Value', 'Notes/Legal Notes', 'Open Positions', 'Realized & Unrealized Performance Summary',
# 'Statement', '\ufeffStatement', 'Total P/L for Statement Period', 'Transaction Fees',
'Cash Report': self._parse_cash_report,
})

# 4. sort
Expand Down Expand Up @@ -313,3 +320,11 @@ def _parse_interests(self, f: Dict[str, str]):
amount = Money(f['Amount'], currency)
description = f['Description']
self._interests.append(Interest(date, amount, description))

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))
Loading

0 comments on commit eb1c60c

Please sign in to comment.