diff --git a/investments/ibtax/ibtax.py b/investments/ibtax/ibtax.py index eedfa7b..18233f5 100644 --- a/investments/ibtax/ibtax.py +++ b/investments/ibtax/ibtax.py @@ -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 @@ -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' @@ -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, ) @@ -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 @@ -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 @@ -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 @@ -104,7 +107,7 @@ 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 @@ -112,56 +115,86 @@ 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') @@ -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: @@ -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 @@ -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): @@ -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) diff --git a/tests/ibdds/show_report.py b/tests/ibdds/show_report_test.py similarity index 100% rename from tests/ibdds/show_report.py rename to tests/ibdds/show_report_test.py diff --git a/tests/ibtax/prepare_trades_report.py b/tests/ibtax/prepare_trades_report.py deleted file mode 100644 index b142b9a..0000000 --- a/tests/ibtax/prepare_trades_report.py +++ /dev/null @@ -1,39 +0,0 @@ -import datetime - -from investments.currency import Currency -from investments.data_providers.cbr import ExchangeRatesRUB -from investments.ibtax.ibtax import prepare_trades_report -from investments.money import Money -from investments.ticker import Ticker, TickerKind -from investments.trades_fifo import FinishedTrade - - -def test_simple_trades(): - ticker = Ticker(symbol='VT', kind=TickerKind.Stock) - - trades = [ - FinishedTrade(N=1, ticker=ticker, trade_date=datetime.datetime(2020, 1, 30, 0, 0), settle_date=datetime.datetime(2020, 2, 3, 0, 0), quantity=7, - price=Money(80.62, Currency.USD), fee_per_piece=Money(-0.123375, Currency.USD)), - - FinishedTrade(N=1, ticker=ticker, trade_date=datetime.datetime(2020, 1, 31, 0, 0), settle_date=datetime.datetime(2020, 2, 4, 0, 0), quantity=-7, - price=Money(81.82, Currency.USD), fee_per_piece=Money('-0.1309628571428571428571428571', Currency.USD)), - - FinishedTrade(N=2, ticker=ticker, trade_date=datetime.datetime(2020, 1, 30, 0, 0), settle_date=datetime.datetime(2020, 2, 3, 0, 0), quantity=1, - price=Money(80.62, Currency.USD), fee_per_piece=Money('-0.123375', Currency.USD)), - - FinishedTrade(N=2, ticker=ticker, trade_date=datetime.datetime(2020, 1, 31, 0, 0), settle_date=datetime.datetime(2020, 2, 4, 0, 0), quantity=9, - price=Money(80.62, Currency.USD), fee_per_piece=Money(-0.1, Currency.USD)), - - FinishedTrade(N=2, ticker=ticker, trade_date=datetime.datetime(2020, 2, 10, 0, 0), settle_date=datetime.datetime(2020, 2, 12, 0, 0), quantity=-10, - price=Money(81.82, Currency.USD), fee_per_piece=Money('-0.101812674', Currency.USD)), - ] - cbr_client = ExchangeRatesRUB(Currency.USD) - - res: dict = prepare_trades_report(trades, cbr_client, False).to_dict() - - assert res['settle_rate'] == {0: Money(63.1385, Currency.RUB), 1: Money(63.9091, Currency.RUB), 2: Money(63.1385, Currency.RUB), 3: Money(63.9091, Currency.RUB), 4: Money(63.9490, Currency.RUB)} - assert res['fee_rate'] == {0: Money(62.3934, Currency.RUB), 1: Money(63.0359, Currency.RUB), 2: Money(62.3934, Currency.RUB), 3: Money(63.0359, Currency.RUB), 4: Money(63.4720, Currency.RUB)} - assert res['fee'] == {0: Money(-0.84, Currency.USD), 1: Money(-0.91, Currency.USD), 2: Money(-0.12, Currency.USD), 3: Money(-0.90, Currency.USD), 4: Money(-1.00, Currency.USD)} - assert res['total'] == {0: Money(-565.18, Currency.USD), 1: Money(571.83, Currency.USD), 2: Money(-80.74, Currency.USD), 3: Money(-726.48, Currency.USD), 4: Money(817.20, Currency.USD)} - assert res['total_rub'] == {0: Money(-35685.51, Currency.RUB), 1: Money(36545.46, Currency.RUB), 2: Money(-5097.93, Currency.RUB), 3: Money(-46427.85, Currency.RUB), 4: Money(52258.50, Currency.RUB)} - assert res['profit_rub'] == {0: Money(0, Currency.RUB), 1: Money(859.95, Currency.RUB), 2: Money(0, Currency.RUB), 3: Money(0, Currency.RUB), 4: Money(732.72, Currency.RUB)} diff --git a/tests/ibtax/prepare_trades_report_test.py b/tests/ibtax/prepare_trades_report_test.py new file mode 100644 index 0000000..27785ee --- /dev/null +++ b/tests/ibtax/prepare_trades_report_test.py @@ -0,0 +1,98 @@ +import datetime +from decimal import Decimal + +from investments.currency import Currency +from investments.data_providers.cbr import ExchangeRatesRUB +from investments.ibtax.ibtax import prepare_trades_report +from investments.money import Money +from investments.ticker import Ticker, TickerKind +from investments.trades_fifo import FinishedTrade +from tests.trades_fifo_test import test_trades_precision + + +def test_simple_trades(): + ticker = Ticker(symbol='VT', kind=TickerKind.Stock) + + trades = [ + FinishedTrade(N=1, ticker=ticker, trade_date=datetime.datetime(2020, 1, 30, 0, 0), settle_date=datetime.datetime(2020, 2, 3, 0, 0), quantity=7, + price=Money(80.62, Currency.USD), fee_per_piece=Money(-0.123375, Currency.USD)), + + FinishedTrade(N=1, ticker=ticker, trade_date=datetime.datetime(2020, 1, 31, 0, 0), settle_date=datetime.datetime(2020, 2, 4, 0, 0), quantity=-7, + price=Money(81.82, Currency.USD), fee_per_piece=Money('-0.1309628571428571428571428571', Currency.USD)), + + FinishedTrade(N=2, ticker=ticker, trade_date=datetime.datetime(2020, 1, 30, 0, 0), settle_date=datetime.datetime(2020, 2, 3, 0, 0), quantity=1, + price=Money(80.62, Currency.USD), fee_per_piece=Money('-0.123375', Currency.USD)), + + FinishedTrade(N=2, ticker=ticker, trade_date=datetime.datetime(2020, 1, 31, 0, 0), settle_date=datetime.datetime(2020, 2, 4, 0, 0), quantity=9, + price=Money(80.62, Currency.USD), fee_per_piece=Money(-0.1, Currency.USD)), + + FinishedTrade(N=2, ticker=ticker, trade_date=datetime.datetime(2020, 2, 10, 0, 0), settle_date=datetime.datetime(2020, 2, 12, 0, 0), quantity=-10, + price=Money(81.82, Currency.USD), fee_per_piece=Money('-0.101812674', Currency.USD)), + ] + cbr_client = ExchangeRatesRUB(Currency.USD) + + res: dict = prepare_trades_report(trades, cbr_client).to_dict() + + assert res['settle_rate'] == { + 0: Money(63.1385, Currency.RUB), + 1: Money(63.9091, Currency.RUB), + 2: Money(63.1385, Currency.RUB), + 3: Money(63.9091, Currency.RUB), + 4: Money(63.9490, Currency.RUB) + } + assert res['fee_rate'] == { + 0: Money(62.3934, Currency.RUB), + 1: Money(63.0359, Currency.RUB), + 2: Money(62.3934, Currency.RUB), + 3: Money(63.0359, Currency.RUB), + 4: Money(63.4720, Currency.RUB) + } + assert res['fee'] == { + 0: Money('-0.863625', Currency.USD), + 1: Money('-0.9167399999999999999999999997', Currency.USD), + 2: Money('-0.123375', Currency.USD), + 3: Money('-0.9', Currency.USD), + 4: Money('-1.018126740', Currency.USD) + } + assert res['total'] == { + 0: Money('-565.203625', Currency.USD), + 1: Money('571.82326', Currency.USD), + 2: Money('-80.743375', Currency.USD), + 3: Money('-726.48', Currency.USD), + 4: Money('817.181873260', Currency.USD) + } + assert res['total_rub'] == { + 0: Money('-35685.465590075', Currency.RUB), + 1: Money('36545.510403034', Currency.RUB), + 2: Money('-5097.923655725', Currency.RUB), + 3: Money('-46427.897088', Currency.RUB), + 4: Money('52258.44925955872', Currency.RUB) + } + assert res['profit_rub'] == { + 0: Money(0, Currency.RUB), + 1: Money('860.044812959', Currency.RUB), + 2: Money(0, Currency.RUB), + 3: Money(0, Currency.RUB), + 4: Money('732.62851583372', Currency.RUB) + } + + +def test_precision(): + """ + Отладка проблемы с потерей точности при расчётах финансового результата. + + """ + + test_case = test_trades_precision() + + res: dict = prepare_trades_report(test_case, ExchangeRatesRUB(Currency.USD)).to_dict() + + assert [x.amount for x in res['total_rub'].values()] == [ + Decimal('-51586.552320'), # Расход: (80.62 * 10 * 63.9091) + (0.1 * 10 * 63.0359) = 51586.55232₽ + Decimal('52258.4492595587200'), # Доход: (81.82 * 10 * 63.9490) - (0.101812674 * 10 * 63.4720) = 52258.4492595587200₽ + ] + + assert [x.amount for x in res['profit_rub'].values()] == [ + Decimal('0'), + Decimal('671.8969395587200'), # Финансовый результат: 52258.4492595587200 - 51586.552320 = 671.896939559₽ + ] diff --git a/tests/report_parsers/ib_test.py b/tests/report_parsers/ib_test.py index 072dfac..f60576b 100644 --- a/tests/report_parsers/ib_test.py +++ b/tests/report_parsers/ib_test.py @@ -1,6 +1,9 @@ import csv import datetime from decimal import Decimal +from typing import Any + +import pytest from investments.cash import Cash from investments.currency import Currency @@ -193,8 +196,7 @@ def test_parse_trades_with_fees(): Trades,Header,DataDiscriminator,Asset Category,Currency,Symbol,Date/Time,Quantity,T. Price,C. Price,Proceeds,Comm/Fee,Basis,Realized P/L,MTM P/L,Code Trades,Data,Order,Stocks,USD,VT,"2020-01-31, 09:30:00",10,80.62,79.73,-806.2,-1,807.2,0,-8.9,O Trades,Data,Order,Stocks,USD,VT,"2020-02-10, 09:38:00",-10,81.82,82.25,818.2,-1.01812674,-807.2,9.981873,-4.3,C -Trades,SubTotal,,Stocks,USD,VT,,0,,,12,-2.01812674,0,9.981873,-13.2, -Trades,Total,,Stocks,USD,,,,,,-57144.745,-25.563491343,57180.290364603,9.981873,-16.654,""" +Trades,SubTotal,,Stocks,USD,VT,,0,,,12,-2.01812674,0,9.981873,-13.2,""" lines = lines.split('\n') p._settle_dates = { @@ -232,3 +234,18 @@ def test_parse_trades_with_fees(): assert p.trades[1].fee.currency == Currency.USD assert p.trades[1].fee_per_piece.amount == Decimal('-0.101812674') assert p.trades[1].fee_per_piece.currency == Currency.USD + + +@pytest.mark.parametrize("case,expected", [ + ('2020-06-02', datetime.date(2020, 6, 2)), + ('', None), +]) +def test_parse_date(case: str, expected: Any): + if expected is None: + with pytest.raises(ValueError): + _parse_date(case) + + else: + res = _parse_date(case) + assert res == expected + diff --git a/tests/trades_fifo_test.py b/tests/trades_fifo_test.py index 372c290..542f4e2 100644 --- a/tests/trades_fifo_test.py +++ b/tests/trades_fifo_test.py @@ -1,5 +1,6 @@ import datetime from decimal import Decimal +from typing import List import pytest @@ -96,3 +97,38 @@ def test_trades_fees_simple(): assert sell_trade.quantity == -10 assert sell_trade.price.amount == Decimal('81.82') assert sell_trade.fee_per_piece.amount == Decimal('-0.101812674') + + +def test_trades_precision() -> List[FinishedTrade]: + ticker = Ticker(symbol='VT', kind=TickerKind.Stock) + test_case = [ + Trade( + ticker=ticker, + trade_date=datetime.datetime(2020, 1, 31, 9, 30), # 63,0359₽ + settle_date=datetime.date(2020, 2, 4), # 63,9091₽ + quantity=10, + price=Money('80.62', Currency.USD), + fee=Money('-1', Currency.USD) + ), + Trade( + ticker=ticker, + trade_date=datetime.datetime(2020, 2, 10, 9, 38), # 63,4720₽ + settle_date=datetime.date(2020, 2, 12), # 63,9490₽ + quantity=-10, + price=Money('81.82', Currency.USD), + fee=Money('-1.01812674', Currency.USD) + ) + ] + + finished_trades = TradesAnalyzer(test_case).finished_trades + + buy_trade: FinishedTrade = finished_trades[0] + assert buy_trade.price.amount == Decimal('80.62') + assert buy_trade.fee_per_piece.amount == Decimal('-0.1') + + sell_trade: FinishedTrade = finished_trades[1] + assert sell_trade.price.amount == Decimal('81.82') + assert sell_trade.fee_per_piece.amount == Decimal('-0.101812674') + + return finished_trades +