Skip to content

Commit

Permalink
♻️ Move convert_to function from Money class to separate functions fo…
Browse files Browse the repository at this point in the history
…r compatible with multi-currency operations and manual compute trades profit
  • Loading branch information
esemi committed Oct 28, 2020
1 parent 3b543e8 commit 81f4848
Show file tree
Hide file tree
Showing 6 changed files with 255 additions and 162 deletions.
36 changes: 28 additions & 8 deletions investments/data_providers/cbr.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,10 @@
"""
Клиент к API ЦБ РФ с курсами валют относительно рубля.
Необходим для перевода сумм сделок в рубли по курсу ЦБ на дату поставки в соответствии с НК РФ
"""

import datetime
import logging
import xml.etree.ElementTree as ET # type: ignore
Expand All @@ -12,13 +19,19 @@


class ExchangeRatesRUB:
currency_codes = {
Currency.USD: 'R01235',
Currency.EUR: 'R01239',
}
currency: Currency
_df: pandas.DataFrame

def __init__(self, currency: Currency, year_from: int = 2000, cache_dir: str = None):
if currency == Currency.USD:
currency_code = 'R01235'
elif currency == Currency.EUR:
currency_code = 'R01239'
else:
raise NotImplementedError('only USD and EUR currencies supported')
self.currency = currency

currency_code = self.currency_codes.get(self.currency)
if not currency_code:
raise NotImplementedError(f'only USD and EUR currencies supported [{self.currency} requested]')

cache = DataFrameCache(cache_dir, f'cbrates_{currency_code}_since{year_from}.cache', datetime.timedelta(days=1))
df = cache.get()
Expand All @@ -27,7 +40,8 @@ def __init__(self, currency: Currency, year_from: int = 2000, cache_dir: str = N
self._df = df
return

r = requests.get(f'http://www.cbr.ru/scripts/XML_dynamic.asp?date_req1=01/01/{year_from}&date_req2=01/01/2030&VAL_NM_RQ={currency_code}')
end_date = (datetime.datetime.utcnow() + datetime.timedelta(days=1)).strftime('%d/%m/%Y')
r = requests.get(f'http://www.cbr.ru/scripts/XML_dynamic.asp?date_req1=01/01/{year_from}&date_req2={end_date}&VAL_NM_RQ={currency_code}')
tree = ET.fromstring(r.text)

rates_data: List[Tuple[datetime.date, Money]] = []
Expand All @@ -40,7 +54,6 @@ def __init__(self, currency: Currency, year_from: int = 2000, cache_dir: str = N

df = pandas.DataFrame(rates_data, columns=['date', 'rate'])
df.set_index(['date'], inplace=True)
# df = df.reindex(pandas.date_range(df.index.min(), df.index.max()))
today = datetime.datetime.utcnow().date()
df = df.reindex(pandas.date_range(df.index.min(), today))
df['rate'].fillna(method='pad', inplace=True)
Expand All @@ -54,3 +67,10 @@ def dataframe(self) -> pandas.DataFrame:

def get_rate(self, date: datetime.date) -> Money:
return self._df.loc[date].item()

def convert_to_rub(self, source: Money, rate_date: datetime.date) -> Money:
if source.currency == Currency.RUB:
return Money(source.amount, Currency.RUB)

rate = self.get_rate(rate_date)
return Money(source.amount * rate.amount, rate.currency)
87 changes: 48 additions & 39 deletions investments/ibtax/ibtax.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
import pandas # type: ignore

from investments.currency import Currency
from investments.data_providers.cbr import ExchangeRatesRUB
from investments.data_providers import cbr
from investments.dividend import Dividend
from investments.fees import Fee
from investments.interests import Interest
Expand All @@ -15,17 +15,17 @@
from investments.trades_fifo import PortfolioElement, TradesAnalyzer


def prepare_trades_report(df: pandas.DataFrame, usdrub_rates_df: pandas.DataFrame) -> pandas.DataFrame:
def prepare_trades_report(df: pandas.DataFrame, cbr_client_usd: cbr.ExchangeRatesRUB) -> pandas.DataFrame:
tax_date_column = 'settle_date'

df['date'] = df['datetime'].dt.normalize()
df['settle_date'] = pandas.to_datetime(df['settle_date'])
df[tax_date_column] = pandas.to_datetime(df[tax_date_column])

tax_years = df.groupby('N')[tax_date_column].max().map(lambda x: x.year).rename('tax_year')
df = df.join(tax_years, how='left', on='N')

df = df.join(usdrub_rates_df, how='left', on=tax_date_column)
df['total_rub'] = df.apply(lambda x: x['total'].convert_to(x['rate']).round(digits=2), axis=1)
df = df.join(cbr_client_usd.dataframe, how='left', on=tax_date_column)
df['total_rub'] = df.apply(lambda x: cbr_client_usd.convert_to_rub(x['total'], x[tax_date_column]).round(digits=2), axis=1)

df['profit_rub'] = df['total_rub']
df.loc[df['quantity'] >= 0, 'profit_rub'] *= -1
Expand All @@ -38,47 +38,52 @@ def prepare_trades_report(df: pandas.DataFrame, usdrub_rates_df: pandas.DataFram
return df


def prepare_dividends_report(dividends: List[Dividend], usdrub_rates_df: pandas.DataFrame, verbose: bool) -> pandas.DataFrame:
def prepare_dividends_report(dividends: List[Dividend], cbr_client_usd: cbr.ExchangeRatesRUB, verbose: bool) -> pandas.DataFrame:
operation_date_column = 'date'
if not verbose:
dividends = [x for x in dividends if x.amount.amount != 0 or x.tax.amount != 0] # remove reversed dividends

df_data = [(i + 1, x.ticker, pandas.to_datetime(x.date), x.amount, x.tax) for i, x in enumerate(dividends)]
df = pandas.DataFrame(df_data, columns=['N', 'ticker', 'date', 'amount', 'tax_paid'])
df['tax_year'] = df['date'].map(lambda x: x.year)

df = df.join(usdrub_rates_df, how='left', on='date')
df['tax_year'] = df[operation_date_column].map(lambda x: x.year)

df = df.join(cbr_client_usd.dataframe, how='left', on=operation_date_column)

df['amount_rub'] = df.apply(lambda x: cbr_client_usd.convert_to_rub(x['amount'], x[operation_date_column]).round(digits=2), axis=1)
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)

df['amount_rub'] = df.apply(lambda x: x['amount'].convert_to(x['rate']).round(digits=2), axis=1)
df['tax_paid_rub'] = df.apply(lambda x: x['tax_paid'].convert_to(x['rate']).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)

return df


def prepare_fees_report(fees: List[Fee], usdrub_rates_df: pandas.DataFrame) -> pandas.DataFrame:
def prepare_fees_report(fees: List[Fee], cbr_client_usd: cbr.ExchangeRatesRUB) -> pandas.DataFrame:
operation_date_column = 'date'
df_data = [
(i + 1, pandas.to_datetime(x.date), x.amount, x.description, x.date.year)
for i, x in enumerate(fees)
]
df = pandas.DataFrame(df_data, columns=['N', 'date', 'amount', 'description', 'tax_year'])

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

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


def prepare_interests_report(interests: List[Interest], usdrub_rates_df: pandas.DataFrame) -> pandas.DataFrame:
def prepare_interests_report(interests: List[Interest], cbr_client_usd: cbr.ExchangeRatesRUB) -> pandas.DataFrame:
operation_date_column = 'date'
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 = df.join(cbr_client_usd.dataframe, how='left', on=operation_date_column)

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


Expand Down Expand Up @@ -172,6 +177,27 @@ def csvs_in_dir(directory: str):
return ret


def parse_reports(activity_reports_dir: str, confirmation_reports_dir: str) -> InteractiveBrokersReportParser:
parser_object = InteractiveBrokersReportParser()

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

for apath in activity_reports:
logging.info('Activity report %s', apath)
for cpath in confirmation_reports:
logging.info('Confirmation report %s', cpath)

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}')

return parser_object


def main():
parser = argparse.ArgumentParser()
parser.add_argument('--activity-reports-dir', type=str, required=True, help='directory with InteractiveBrokers .csv activity reports')
Expand All @@ -188,24 +214,7 @@ def main():
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:
logging.info('Activity report %s', apath)
for cpath in confirmation_reports:
logging.info('Confirmation report %s', cpath)

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}')
parser_object = parse_reports(args.activity_reports_dir, args.confirmation_reports_dir)

trades = parser_object.trades
dividends = parser_object.dividends
Expand All @@ -218,19 +227,19 @@ def main():

# 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(currency=Currency.USD, year_from=first_year, cache_dir=args.cache_dir).dataframe
cbr_client_usd = cbr.ExchangeRatesRUB(currency=Currency.USD, year_from=first_year, cache_dir=args.cache_dir)

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
dividends_report = prepare_dividends_report(dividends, cbr_client_usd, args.verbose) if dividends else None
fees_report = prepare_fees_report(fees, cbr_client_usd) if fees else None
interests_report = prepare_interests_report(interests, cbr_client_usd) if interests else None

analyzer = TradesAnalyzer(trades)
finished_trades = analyzer.finished_trades
portfolio = analyzer.final_portfolio

if finished_trades:
finished_trades_df = pandas.DataFrame(finished_trades, columns=finished_trades[0]._fields) # noqa: WPS437
trades_report = prepare_trades_report(finished_trades_df, cbrates_df)
trades_report = prepare_trades_report(finished_trades_df, cbr_client_usd)
else:
trades_report = None

Expand Down
5 changes: 0 additions & 5 deletions investments/money.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,11 +17,6 @@ def currency(self) -> Currency:
def amount(self) -> Decimal:
return self._amount

def convert_to(self, rate: 'Money') -> 'Money':
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
Loading

0 comments on commit 81f4848

Please sign in to comment.