Skip to content

Commit

Permalink
Increase precision for trades income/expenses/profit values (#37)
Browse files Browse the repository at this point in the history
  • Loading branch information
esemi committed Jan 27, 2021
1 parent 007ff95 commit bc1f5ab
Show file tree
Hide file tree
Showing 6 changed files with 238 additions and 93 deletions.
137 changes: 85 additions & 52 deletions investments/ibtax/ibtax.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import argparse
import logging
import os
from typing import List, Optional
from typing import Iterable, List, Optional

import pandas # type: ignore

Expand All @@ -16,8 +16,21 @@
from investments.trades_fifo import TradesAnalyzer, FinishedTrade, PortfolioElement # noqa: I001


def prepare_trades_report(finished_trades: List[FinishedTrade], cbr_client_usd: cbr.ExchangeRatesRUB, verbose: bool) -> pandas.DataFrame:
fee_round_digits = 4
def apply_round_for_dataframe(source: pandas.DataFrame, columns: Iterable, digits: int = 2) -> pandas.DataFrame:
source[list(columns)] = source[list(columns)].applymap(
lambda x: x.round(digits=digits) if isinstance(x, Money) else round(x, digits),
)
return source


def prepare_trades_report(finished_trades: List[FinishedTrade], cbr_client_usd: cbr.ExchangeRatesRUB) -> pandas.DataFrame:
"""
Расчёт расхода/дохода и финансового результата по закрытым сделкам.
Общая методика расчёта расхода/дохода по сделке:
[сумма сделки] * [курс валюты на дату поставки] +/- [сумма комиссии] * [курс валюты на дату сделки]
"""
trade_date_column = 'trade_date'
tax_date_column = 'settle_date'

Expand All @@ -29,18 +42,16 @@ def prepare_trades_report(finished_trades: List[FinishedTrade], cbr_client_usd:
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['price_rub'] = df.apply(lambda x: cbr_client_usd.convert_to_rub(x['price'], x[tax_date_column]).round(digits=2), axis=1)
df['fee_per_piece_rub'] = df.apply(lambda x: cbr_client_usd.convert_to_rub(x['fee_per_piece'], x[trade_date_column]).round(digits=fee_round_digits), axis=1)

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['price_rub'] = df.apply(lambda x: cbr_client_usd.convert_to_rub(x['price'], x[tax_date_column]), axis=1)
df['fee_per_piece_rub'] = df.apply(lambda x: cbr_client_usd.convert_to_rub(x['fee_per_piece'], x[trade_date_column]), axis=1)
df['fee'] = df.apply(lambda x: (x['fee_per_piece'] * abs(x['quantity'])), axis=1)

df['total'] = df.apply(
lambda x: compute_total_cost(x['quantity'], x['price'], x['fee_per_piece']).round(digits=2),
lambda x: compute_total_cost(x['quantity'], x['price'], x['fee_per_piece']),
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),
lambda x: compute_total_cost(x['quantity'], x['price_rub'], x['fee_per_piece_rub']),
axis=1,
)

Expand All @@ -49,13 +60,10 @@ def prepare_trades_report(finished_trades: List[FinishedTrade], cbr_client_usd:
df['profit_rub'] = df['total_rub']

profit = df.groupby('N')['profit_rub'].sum().reset_index().set_index('N')
df = df.join(profit, how='left', on='N', lsuffix='del')
df.drop(columns=['profit_rubdel'], axis=0, inplace=True)
df = df.join(profit, how='left', on='N', lsuffix='_delete')
df.drop(columns=['profit_rub_delete'], axis=0, inplace=True)
df.loc[~df.index.isin(df.groupby('N')[trade_date_column].idxmax()), 'profit_rub'] = Money(0, Currency.RUB)

if not verbose:
df = df.drop(columns=['fee_per_piece', 'fee_per_piece_rub', 'price_rub'])

return df


Expand All @@ -71,14 +79,9 @@ def prepare_dividends_report(dividends: List[Dividend], cbr_client_usd: cbr.Exch

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)

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

return df

Expand All @@ -92,7 +95,7 @@ def prepare_fees_report(fees: List[Fee], cbr_client_usd: cbr.ExchangeRatesRUB) -
df = pandas.DataFrame(df_data, columns=['N', operation_date_column, 'amount', 'description', 'tax_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['amount_rub'] = df.apply(lambda x: cbr_client_usd.convert_to_rub(x['amount'], x[operation_date_column]), axis=1)
return df


Expand All @@ -104,64 +107,94 @@ def prepare_interests_report(interests: List[Interest], cbr_client_usd: cbr.Exch
]
df = pandas.DataFrame(df_data, columns=['N', operation_date_column, 'amount', 'description', 'tax_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['amount_rub'] = df.apply(lambda x: cbr_client_usd.convert_to_rub(x['amount'], x[operation_date_column]), axis=1)
return df


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


def _show_fees_report(fees: pandas.DataFrame, year: int):
def _show_fees_report(fees: pandas.DataFrame, year: int, verbose: bool):
fees_by_year = fees[fees['tax_year'] == year].drop(columns=['tax_year'])
if fees_by_year.empty:
return

feed_presenter = fees_by_year.copy(deep=True).set_index(['N', 'date'])
if not verbose:
apply_round_for_dataframe(feed_presenter, {'rate'}, 4)
apply_round_for_dataframe(feed_presenter, {'amount', 'amount_rub'}, 2)

_show_header('OTHER FEES')
print(fees_by_year.set_index(['N', 'date']).to_string())
print('\nTOTAL:\t', fees_by_year['amount_rub'].sum())
print(feed_presenter.to_string())
print('\nTOTAL:\t', feed_presenter['amount_rub'].sum())
print('\n\n')


def _show_interests_report(interests: pandas.DataFrame, year: int):
def _show_interests_report(interests: pandas.DataFrame, year: int, verbose: bool):
interests_by_year = interests[interests['tax_year'] == year].drop(columns=['tax_year'])
if interests_by_year.empty:
return

interests_presenter = interests_by_year.copy(deep=True).set_index(['N', 'date'])
if not verbose:
apply_round_for_dataframe(interests_presenter, {'rate'}, 4)
apply_round_for_dataframe(interests_presenter, {'amount', 'amount_rub'}, 2)

_show_header('INTERESTS')
print(interests_by_year.set_index(['N', 'date']).to_string())
print(interests_presenter.to_string())
print('\n\n')


def _show_dividends_report(dividends: pandas.DataFrame, year: int):
def _show_dividends_report(dividends: pandas.DataFrame, year: int, verbose: bool):
dividends_by_year = dividends[dividends['tax_year'] == year].drop(columns=['tax_year'])
if dividends_by_year.empty:
return

dividends_by_year['N'] -= dividends_by_year['N'].iloc[0] - 1

dividends_presenter = dividends_by_year.copy(deep=True).set_index(['N', 'ticker', 'date'])
if not verbose:
apply_round_for_dataframe(dividends_presenter, {'rate'}, 4)
apply_round_for_dataframe(dividends_presenter, {'amount', 'amount_rub', 'tax_paid', 'tax_paid_rub'}, 2)
dividends_presenter = dividends_presenter.drop(columns=['tax_rate'])

_show_header('DIVIDENDS')
print(dividends_by_year.set_index(['N', 'ticker', 'date']).to_string())
print(dividends_presenter.to_string())
print('\n\n')


def _show_trades_report(trades: pandas.DataFrame, year: int):
def _show_trades_report(trades: pandas.DataFrame, year: int, verbose: bool):
trades_by_year = trades[trades['tax_year'] == year].drop(columns=['tax_year'])
if trades_by_year.empty:
return

trades_by_year['N'] -= trades_by_year['N'].iloc[0] - 1

_show_header('TRADES')
print(trades_by_year.set_index(['N', 'ticker', 'trade_date']).to_string())
trades_presenter = trades_by_year.copy(deep=True).set_index(['N', 'ticker', 'trade_date'])
if not verbose:
apply_round_for_dataframe(trades_presenter, {'price', 'total', 'total_rub', 'profit_rub'}, 2)
apply_round_for_dataframe(trades_presenter, {'fee', 'settle_rate', 'fee_rate'}, 4)
trades_presenter = trades_presenter.drop(columns=['fee_per_piece', 'fee_per_piece_rub', 'price_rub'])

print(trades_presenter.to_string())
print('\n\n')

_show_header('TRADES RESULTS BEFORE TAXES')
tp = trades_by_year.groupby(lambda idx: (
trades_summary_presenter = trades_by_year.copy(deep=True).groupby(lambda idx: (
trades_by_year.loc[idx, 'ticker'].kind,
'expenses' if trades_by_year.loc[idx, 'quantity'] > 0 else 'income',
))['total_rub'].sum().reset_index()
tp = tp['index'].apply(pandas.Series).join(tp).pivot(index=0, columns=1, values='total_rub')
tp.index.name = ''
tp.columns.name = ''
tp['profit'] = tp['income'] + tp['expenses']
print(tp.reset_index().to_string())
trades_summary_presenter = trades_summary_presenter['index'].apply(pandas.Series).join(trades_summary_presenter).pivot(index=0, columns=1, values='total_rub')
trades_summary_presenter.index.name = ''
trades_summary_presenter.columns.name = ''
trades_summary_presenter['profit'] = trades_summary_presenter['income'] + trades_summary_presenter['expenses']

if not verbose:
apply_round_for_dataframe(trades_summary_presenter, {'expenses', 'income', 'profit'}, 2)

print(trades_summary_presenter.reset_index().to_string())
print('\n\n')


Expand All @@ -174,7 +207,7 @@ def show_portfolio_report(portfolio: List[PortfolioElement]):

def show_report(trades: Optional[pandas.DataFrame], dividends: Optional[pandas.DataFrame],
fees: Optional[pandas.DataFrame], interests: Optional[pandas.DataFrame],
filter_years: List[int]): # noqa: WPS318,WPS319
filter_years: List[int], verbose: bool): # noqa: WPS318,WPS319
years = set()
for report in (trades, dividends, fees, interests):
if report is not None:
Expand All @@ -186,28 +219,28 @@ def show_report(trades: Optional[pandas.DataFrame], dividends: Optional[pandas.D
print('\n', '______' * 8, f' {year} ', '______' * 8, '\n')

if dividends is not None:
_show_dividends_report(dividends, year)
_show_dividends_report(dividends, year, verbose)

if trades is not None:
_show_trades_report(trades, year)
_show_trades_report(trades, year, verbose)

if fees is not None:
_show_fees_report(fees, year)
_show_fees_report(fees, year, verbose)

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

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


def csvs_in_dir(directory: str):
ret = []
for fname in os.scandir(directory):
if not fname.is_file():
for filename in os.scandir(directory):
if not filename.is_file():
continue
if not fname.name.lower().endswith('.csv'):
if not filename.name.lower().endswith('.csv'):
continue
ret.append(fname.path)
ret.append(filename.path)
return ret


Expand Down Expand Up @@ -238,7 +271,7 @@ def main():
parser.add_argument('--confirmation-reports-dir', type=str, required=True, help='directory with InteractiveBrokers .csv confirmation reports')
parser.add_argument('--cache-dir', type=str, default='.', help='directory for caching (CBR RUB exchange rates)')
parser.add_argument('--years', type=lambda x: [int(v.strip()) for v in x.split(',')], default=[], help='comma separated years for final report, omit for all')
parser.add_argument('--verbose', nargs='?', default=False, const=True, help='do not "prune" reversed dividends, show dividens tax percent, etc.')
parser.add_argument('--verbose', nargs='?', default=False, const=True, help='do not "prune" reversed dividends, show dividends tax percent, etc.')
args = parser.parse_args()

if os.path.abspath(args.activity_reports_dir) == os.path.abspath(args.confirmation_reports_dir):
Expand Down Expand Up @@ -271,9 +304,9 @@ def main():
finished_trades = analyzer.finished_trades
portfolio = analyzer.final_portfolio

trades_report = prepare_trades_report(finished_trades, cbr_client_usd, args.verbose) if finished_trades else None
trades_report = prepare_trades_report(finished_trades, cbr_client_usd) if finished_trades else None

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


Expand Down
File renamed without changes.
39 changes: 0 additions & 39 deletions tests/ibtax/prepare_trades_report.py

This file was deleted.

0 comments on commit bc1f5ab

Please sign in to comment.