Skip to content

Commit

Permalink
✨ Parse interests for every year and show report
Browse files Browse the repository at this point in the history
  • Loading branch information
esemi committed Oct 8, 2020
1 parent 456835f commit 1893d0f
Show file tree
Hide file tree
Showing 9 changed files with 138 additions and 16 deletions.
8 changes: 6 additions & 2 deletions .flake8
Original file line number Diff line number Diff line change
Expand Up @@ -4,20 +4,22 @@ line_length = 79
[flake8]
max-line-length = 200

max-imports = 16

min-name-length = 1

max-line-complexity = 24

max-string-usages = 16

max-local-variables = 18
max-local-variables = 20

max-cognitive-score = 24

max-cognitive-average = 24

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

# function arguments
max-arguments = 8
Expand All @@ -38,10 +40,12 @@ ignore =
WPS115, # upper-case constant in a class
WPS202, # too many module members
WPS218, # too many asserts, useful for tests/
WPS226, # Found string constant over-use
WPS301, # dotted raw import
WPS305, # f-strings
WPS306, # class without a base class
WPS317, # multilines
WPS318, # Found extra indentation
WPS323, # '%' strings, too many false-positives (strptime, ...)
WPS420, # allow 'pass' (but also 'global','local' & 'del')
WPS421, # allow 'print()' function
Expand Down
4 changes: 2 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,8 +15,8 @@ $ pip install investments --upgrade --user
- расчет сделок по методу ФИФО, учет даты расчетов (settle date)
- конвертация по курсу ЦБ
- раздельный результат сделок по акциям и опционам + дивиденды
- учёт начисленных процентов на остаток по счету
- пока **НЕ** учитывает комисии по сделкам (т.е. налог будет немного больше, в пользу налоговой)
- пока **НЕ** учитываются проценты на остаток по счету
- пока **НЕ** поддерживаются сплиты
- пока **НЕ** поддерживаются сделки Forex, сделка пропускается и выводится сообщение о том, что это может повлиять на итоговый отчет

Expand Down Expand Up @@ -65,7 +65,7 @@ $ cd investments
$ poetry install
$ poetry run ibtax
usage: ibtax [-h] --activity-reports-dir ACTIVITY_REPORTS_DIR --confirmation-reports-dir CONFIRMATION_REPORTS_DIR [--cache-dir CACHE_DIR] [--years YEARS]
usage: ibtax [-h] --activity-reports-dir ACTIVITY_REPORTS_DIR --confirmation-reports-dir CONFIRMATION_REPORTS_DIR [--cache-dir CACHE_DIR] [--years YEARS] [--verbose]
ibtax: error: the following arguments are required: --activity-reports-dir, --confirmation-reports-dir
$ vim investments/ibtax/ibtax.py # edit main file for example
Expand Down
3 changes: 3 additions & 0 deletions investments/data_providers/cbr.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import datetime
import logging
import xml.etree.ElementTree as ET # type: ignore
from typing import List, Tuple

Expand All @@ -15,6 +16,7 @@ def __init__(self, year_from: int = 2000, cache_dir: str = None):
cache = DataFrameCache(cache_dir, f'cbrates_since{year_from}.cache', datetime.timedelta(days=1))
df = cache.get()
if df is not None:
logging.info('CBR cache hit')
self._df = df
return

Expand All @@ -39,6 +41,7 @@ def __init__(self, year_from: int = 2000, cache_dir: str = None):
cache.put(df)
self._df = df

@property
def dataframe(self) -> pandas.DataFrame:
return self._df

Expand Down
52 changes: 43 additions & 9 deletions investments/ibtax/ibtax.py
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import argparse
import logging
import os
from typing import List, Optional

Expand All @@ -8,6 +9,7 @@
from investments.data_providers.cbr import ExchangeRatesRUB
from investments.dividend import Dividend
from investments.fees import Fee
from investments.interests import Interest
from investments.money import Money
from investments.report_parsers.ib import InteractiveBrokersReportParser
from investments.trades_fifo import analyze_trades_fifo
Expand Down Expand Up @@ -67,6 +69,19 @@ def prepare_fees_report(fees: List[Fee], usdrub_rates_df: pandas.DataFrame) -> p
return df


def prepare_interests_report(interests: List[Interest], usdrub_rates_df: pandas.DataFrame) -> pandas.DataFrame:
df_data = [
(i + 1, pandas.to_datetime(x.date), x.amount, x.description, x.date.year)
for i, x in enumerate(interests)
]
df = pandas.DataFrame(df_data, columns=['N', 'date', 'amount', 'description', 'tax_year'])

df = df.join(usdrub_rates_df, how='left', on='date')

df['amount_rub'] = df.apply(lambda x: x['amount'].convert_to(x['rate']).round(digits=2), axis=1)
return df


def _show_header(msg: str):
print(f'>>> {msg} <<<')

Expand All @@ -79,9 +94,18 @@ def _show_fees_report(fees: pandas.DataFrame, year: int):
print('\n\n')


def show_report(trades: Optional[pandas.DataFrame], dividends: Optional[pandas.DataFrame], fees: Optional[pandas.DataFrame], portfolio, filter_years: List[int]):
def _show_interests_report(interests: pandas.DataFrame, year: int):
interests_year = interests[interests['tax_year'] == year].drop(columns=['tax_year'])
_show_header('INTERESTS')
print(interests_year.set_index(['N', 'date']).to_string())
print('\n\n')


def show_report(trades: Optional[pandas.DataFrame], dividends: Optional[pandas.DataFrame],
fees: Optional[pandas.DataFrame], interests: Optional[pandas.DataFrame], portfolio,
filter_years: List[int]): # noqa: WPS318,WPS319
years = set()
for report in (trades, dividends, fees):
for report in (trades, dividends, fees, interests):
if report is not None:
years |= set(report['tax_year'].unique())

Expand Down Expand Up @@ -122,6 +146,9 @@ def show_report(trades: Optional[pandas.DataFrame], dividends: Optional[pandas.D
if fees is not None:
_show_fees_report(fees, year)

if interests is not None:
_show_interests_report(interests, year)

print('______' * 8, f'EOF {year}', '______' * 8, '\n\n\n')

print('>>> PORTFOLIO <<<')
Expand Down Expand Up @@ -153,37 +180,44 @@ def main():
print('--activity-reports-dir and --confirmation-reports-dir MUST be different directories')
return

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

parser_object = InteractiveBrokersReportParser()

activity_reports = csvs_in_dir(args.activity_reports_dir)
confirmation_reports = csvs_in_dir(args.confirmation_reports_dir)

for apath in activity_reports:
print(f'[*] Activity report {apath}')
logging.info('Activity report %s', apath)
for cpath in confirmation_reports:
print(f'[*] Confirmation report {cpath}')
logging.info('Confirmation report %s', cpath)

print('========' * 8)
print('')
logging.info('========' * 8)

logging.info('start reports parse')
parser_object.parse_csv(
activity_csvs=activity_reports,
trade_confirmation_csvs=confirmation_reports,
)
logging.info(f'end reports parse {parser_object}')

trades = parser_object.trades
dividends = parser_object.dividends
fees = parser_object.fees
interests = parser_object.interests

if not trades:
print('no trades found')
logging.warning('no trades found')
return

# fixme first_year without dividends
first_year = min(trades[0].datetime.year, dividends[0].date.year) if dividends else trades[0].datetime.year
cbrates_df = ExchangeRatesRUB(year_from=first_year, cache_dir=args.cache_dir).dataframe()
cbrates_df = ExchangeRatesRUB(year_from=first_year, cache_dir=args.cache_dir).dataframe

dividends_report = prepare_dividends_report(dividends, cbrates_df, args.verbose) if dividends else None
fees_report = prepare_fees_report(fees, cbrates_df) if fees else None
interests_report = prepare_interests_report(interests, cbrates_df) if interests else None

portfolio, finished_trades = analyze_trades_fifo(trades)
if finished_trades:
Expand All @@ -192,7 +226,7 @@ def main():
else:
trades_report = None

show_report(trades_report, dividends_report, fees_report, portfolio, args.years)
show_report(trades_report, dividends_report, fees_report, interests_report, portfolio, args.years)


if __name__ == '__main__':
Expand Down
20 changes: 20 additions & 0 deletions investments/interests.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
import datetime
from typing import NamedTuple

from investments.money import Money


class Interest(NamedTuple):
"""
Начисления процентов на кеш.
По итогам года нужно заплатить с данных начислений обычные 13%
"""

date: datetime.date
amount: Money
description: str

def __str__(self):
return f'{self.date} ({self.amount} {self.description})'
4 changes: 3 additions & 1 deletion investments/money.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,9 @@ def amount(self) -> Decimal:
return self._amount

def convert_to(self, rate: 'Money') -> 'Money':
return Money(self._amount * rate.amount, rate.currency)
if self.currency == rate.currency:
return Money(self.amount, self.currency)
return Money(self.amount * rate.amount, rate.currency)

def round(self, digits=0) -> 'Money': # noqa: WPS125
return Money(round(self._amount, digits), self._currency)
Expand Down
23 changes: 21 additions & 2 deletions investments/report_parsers/ib.py
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
from investments.currency import Currency
from investments.dividend import Dividend
from investments.fees import Fee
from investments.interests import Interest
from investments.money import Money
from investments.ticker import Ticker, TickerKind
from investments.trade import Trade
Expand Down Expand Up @@ -100,10 +101,14 @@ def __init__(self):
self._trades = []
self._dividends = []
self._fees: List[Fee] = []
self._interests: List[Interest] = []
self._deposits_and_withdrawals = []
self._tickers = TickersStorage()
self._settle_dates = {}

def __repr__(self):
return f'IbParser(trades={len(self.trades)}, dividends={len(self.dividends)}, fees={len(self.fees)}, interests={len(self.interests)})' # noqa: WPS221

@property
def trades(self) -> List:
return self._trades
Expand All @@ -117,9 +122,13 @@ def deposits_and_withdrawals(self) -> List:
return self._deposits_and_withdrawals

@property
def fees(self) -> List:
def fees(self) -> List[Fee]:
return self._fees

@property
def interests(self) -> List[Interest]:
return self._interests

def parse_csv(self, *, activity_csvs: List[str], trade_confirmation_csvs: List[str]):
# 1. parse tickers info
for ac_fname in activity_csvs:
Expand All @@ -144,14 +153,17 @@ def parse_csv(self, *, activity_csvs: List[str], trade_confirmation_csvs: List[s
# 'Account Information', 'Cash Report', 'Change in Dividend Accruals', 'Change in NAV',
# 'Codes',
'Fees': self._parse_fees,
# 'Interest Accruals', 'Interest', 'Mark-to-Market Performance Summary',
# 'Interest Accruals',
'Interest': self._parse_interests,
# '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',
})

# 4. sort
self._trades.sort(key=lambda x: x.datetime)
self._dividends.sort(key=lambda x: x.date)
self._interests.sort(key=lambda x: x.date)
self._deposits_and_withdrawals.sort(key=lambda x: x[0])

def _parse_trade_confirmation_csv(self, csv_reader: Iterator[List[str]]):
Expand Down Expand Up @@ -293,3 +305,10 @@ def _parse_fees(self, f: Dict[str, str]):
amount = Money(f['Amount'], currency)
description = f"{f['Subtitle']} - {f['Description']}"
self._fees.append(Fee(date, amount, description))

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))
16 changes: 16 additions & 0 deletions tests/money_test.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from decimal import Decimal

import pytest

from investments.currency import Currency
Expand Down Expand Up @@ -79,3 +81,17 @@ def test_money_float():

msum = m + m + m
assert msum.amount == m_expect.amount


def test_convert_to():
usdrub_rate = Money(78.17, Currency.RUB)

test_usd = Money(10.98, Currency.USD)
res = test_usd.convert_to(usdrub_rate)
assert res.amount == Decimal('858.3066')
assert res.currency == Currency.RUB

test_rub = Money(Decimal('858.3066'), Currency.RUB)
res = test_rub.convert_to(usdrub_rate)
assert res.amount == Decimal('858.3066')
assert res.currency == Currency.RUB
24 changes: 24 additions & 0 deletions tests/report_parsers/ib_test.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@

from investments.currency import Currency
from investments.fees import Fee
from investments.interests import Interest
from investments.money import Money
from investments.report_parsers.ib import InteractiveBrokersReportParser
from investments.ticker import TickerKind
Expand Down Expand Up @@ -114,3 +115,26 @@ def test_parse_fees():
description='Other Fees - E*****42:GLOBAL SNAPSHOT FOR JAN 2020')
assert p.fees[5] == Fee(date=datetime.date(2020, 9, 3), amount=Money(-10., Currency.USD),
description='Other Fees - Balance of Monthly Minimum Fee for Aug 2020')


def test_parse_interests():
p = InteractiveBrokersReportParser()

lines = """Interest,Header,Currency,Date,Description,Amount
Interest,Data,RUB,2020-03-04,RUB Credit Interest for Feb-2020,3.21
Interest,Data,Total,,,3.21
Interest,Data,Total in USD,,,0.04844211
Interest,Data,USD,2020-03-04,USD Credit Interest for Feb-2020,0.09
Interest,Data,Total,,,0.09
Interest,Data,Total Interest in USD,,,0.13844211"""

lines = lines.split('\n')
p._real_parse_activity_csv(csv.reader(lines, delimiter=','), {
'Interest': p._parse_interests,
})

assert len(p.interests) == 2
assert p.interests[0] == Interest(date=datetime.date(2020, 3, 4), amount=Money(3.21, Currency.RUB),
description='RUB Credit Interest for Feb-2020')
assert p.interests[1] == Interest(date=datetime.date(2020, 3, 4), amount=Money(0.09, Currency.USD),
description='USD Credit Interest for Feb-2020')

0 comments on commit 1893d0f

Please sign in to comment.