From adf298ddb0d8945498139e65482af27266eff11d Mon Sep 17 00:00:00 2001 From: oittaa Date: Fri, 19 Nov 2021 13:40:47 +0100 Subject: [PATCH] Python API examples (#64) --- README.md | 42 ++++++++++++++++++++++++++++++++++---- ibkr_report/definitions.py | 26 ++++++++--------------- ibkr_report/report.py | 20 +++++++++--------- ibkr_report/trade.py | 16 +++++++-------- test.py | 1 + 5 files changed, 64 insertions(+), 41 deletions(-) diff --git a/README.md b/README.md index 646cf83..318235b 100644 --- a/README.md +++ b/README.md @@ -13,13 +13,13 @@ Interactive Brokers (IBKR) Report Parser for MyTax (vero.fi) - not affiliated wi ## How to run locally ### Option 1: pip -``` +```shell pip install ibkr-report-parser ibkr-report-parser ``` ### Option 2: Docker -```` +````shell docker pull ghcr.io/oittaa/ibkr-report-parser docker run --rm -d -p 8080:8080 --name ibkr-report-parser ghcr.io/oittaa/ibkr-report-parser ```` @@ -27,7 +27,7 @@ docker run --rm -d -p 8080:8080 --name ibkr-report-parser ghcr.io/oittaa/ibkr-re ### Option 3: Build yourself #### Python -``` +```shell git clone https://github.com/oittaa/ibkr-report-parser.git cd ibkr-report-parser pip install -r requirements.txt @@ -35,7 +35,7 @@ python main.py ``` #### Docker -``` +```shell git clone https://github.com/oittaa/ibkr-report-parser.git cd ibkr-report-parser docker build -t ibkr-report-parser:latest . @@ -45,3 +45,37 @@ docker run --rm -d -p 8080:8080 --name ibkr-report-parser ibkr-report-parser ### Use the app Browse to http://127.0.0.1:8080/ + +## Python API + +```python +from ibkr_report.report import Report + +FILE_1 = "test-data/data_single_account.csv" +FILE_2 = "test-data/data_multi_account.csv" + +with open(FILE_1, "rb") as file: + report = Report(file=file, report_currency="EUR", use_deemed_acquisition_cost=True) + +with open(FILE_2, "rb") as file: + report.add_trades(file=file) + +print(f"Total selling prices: {report.prices}") +print(f"Total capital gains: {report.gains}") +print(f"Total capital losses: {report.losses}") + +for item in report.details: + print( + f"{item.symbol=}, {item.quantity=}, {item.buy_date=}, " + f"{item.sell_date=}, {item.price=}, {item.realized=}" + ) + +``` + +```python +from ibkr_report.exchangerates import ExchangeRates + +rates = ExchangeRates() +print(rates.get_rate("EUR", "USD", "2020-06-20")) +print(rates.get_rate("GBP", "SEK", "2015-12-31")) +``` diff --git a/ibkr_report/definitions.py b/ibkr_report/definitions.py index 5411883..1b901b2 100644 --- a/ibkr_report/definitions.py +++ b/ibkr_report/definitions.py @@ -3,7 +3,7 @@ import os from dataclasses import dataclass from decimal import Decimal -from enum import Enum, IntEnum, auto, unique +from enum import Enum, IntEnum, unique from typing import Dict TITLE = os.getenv("TITLE", "IBKR Report Parser") @@ -88,23 +88,13 @@ class FieldValue(StrEnum): HEADER = "Data" -@unique -class ReportOptions(Enum): - """Report options - - REPORT_CURRENCY: - The currency used in the output. - - DEEMED_ACQUISITION_COST: - The deemed acquisition cost is either 20% or 40% of the selling price. The - percentage is determined on the basis of how long you have owned the - property before selling it. The deemed acquisition cost is 20% of the - selling price if you have owned the property for less than 10 years. - """ - - REPORT_CURRENCY = auto() - DEEMED_ACQUISITION_COST = auto() - OFFSET = auto() +@dataclass +class ReportOptions: + """Report options""" + + report_currency: str + deemed_acquisition_cost: bool + offset: int @dataclass diff --git a/ibkr_report/report.py b/ibkr_report/report.py index 4de7a7c..d32e178 100644 --- a/ibkr_report/report.py +++ b/ibkr_report/report.py @@ -6,7 +6,7 @@ import csv from codecs import iterdecode from decimal import Decimal -from typing import Dict, Iterable, List, Optional, Tuple +from typing import Iterable, List, Optional, Tuple from ibkr_report.definitions import ( _FIELD_COUNT, @@ -35,14 +35,14 @@ class Report: gains (Decimal): Total capital gains losses (Decimal): Total capital losses details(List): Details from trades such as dates and quantities - options (Dict): Whether to use acquisition cost, report currency + options (ReportOptions): Whether to use acquisition cost, report currency """ prices: Decimal = Decimal(0) gains: Decimal = Decimal(0) losses: Decimal = Decimal(0) details: List[TradeDetails] - options: Dict + options: ReportOptions _trade: Optional[Trade] = None def __init__( @@ -52,11 +52,11 @@ def __init__( use_deemed_acquisition_cost: bool = True, ) -> None: self.details = [] - self.options = { - ReportOptions.REPORT_CURRENCY: report_currency, - ReportOptions.DEEMED_ACQUISITION_COST: use_deemed_acquisition_cost, - ReportOptions.OFFSET: 0, - } + self.options = ReportOptions( + report_currency=report_currency, + deemed_acquisition_cost=use_deemed_acquisition_cost, + offset=0, + ) if file: self.add_trades(file) @@ -67,7 +67,7 @@ def add_trades(self, file: Iterable[bytes]) -> None: items = tuple(items_list) offset = _OFFSET_DICT.get(items) if offset is not None: - self.options[ReportOptions.OFFSET] = offset + self.options.offset = offset self._trade = None continue if self.is_stock_or_options_trade(items): @@ -78,7 +78,7 @@ def add_trades(self, file: Iterable[bytes]) -> None: def is_stock_or_options_trade(self, items: Tuple[str, ...]) -> bool: """Checks whether the current row is part of a trade or not.""" if ( - len(items) == _FIELD_COUNT + self.options[ReportOptions.OFFSET] + len(items) == _FIELD_COUNT + self.options.offset and items[Field.TRADES] == FieldValue.TRADES and items[Field.HEADER] == FieldValue.HEADER and items[Field.DATA_DISCRIMINATOR] diff --git a/ibkr_report/trade.py b/ibkr_report/trade.py index a645b25..5a89fc1 100644 --- a/ibkr_report/trade.py +++ b/ibkr_report/trade.py @@ -3,7 +3,7 @@ import logging from datetime import datetime from decimal import Decimal -from typing import Dict, Tuple +from typing import Tuple from ibkr_report.definitions import ( _DATE, @@ -33,14 +33,14 @@ class Trade: closed_quantity: Decimal = Decimal(0) total_selling_price: Decimal = Decimal(0) data: RowData - options: Dict + options: ReportOptions - def __init__(self, items: Tuple[str, ...], options: Dict) -> None: + def __init__(self, items: Tuple[str, ...], options: ReportOptions) -> None: """Initializes the Trade and calculates the total selling price from it.""" self.options = options self.data = self._row_data(items) - offset = self.options[ReportOptions.OFFSET] + offset = self.options.offset self.fee = ( decimal_cleanup(items[Field.COMMISSION_AND_FEES + offset]) / self.data.rate ) @@ -92,7 +92,7 @@ def details_from_closed_lot(self, items: Tuple[str, ...]) -> TradeDetails: - lot_data.quantity * self.fee / self.data.quantity ) total_sell_price = abs(lot_data.quantity) * sell_price * multiplier - if self.options.get(ReportOptions.DEEMED_ACQUISITION_COST): + if self.options.deemed_acquisition_cost: realized = min( realized, self.deemed_profit(total_sell_price, buy_date, sell_date), @@ -120,7 +120,7 @@ def details_from_closed_lot(self, items: Tuple[str, ...]) -> TradeDetails: ) def _row_data(self, items: Tuple[str, ...]) -> RowData: - offset = self.options[ReportOptions.OFFSET] + offset = self.options.offset symbol = items[Field.SYMBOL + offset] date_str = items[Field.DATE_TIME + offset] rate = self.currency_rate( @@ -141,9 +141,7 @@ def currency_rate(self, currency: str, date_str: str) -> Decimal: rates = ExchangeRates() Cache.set(key=cache_key, value=rates) - return rates.get_rate( - self.options.get(ReportOptions.REPORT_CURRENCY), currency, date_str - ) + return rates.get_rate(self.options.report_currency, currency, date_str) @staticmethod def deemed_profit(sell_price: Decimal, buy_date: str, sell_date: str) -> Decimal: diff --git a/test.py b/test.py index d6d3496..d7130dd 100644 --- a/test.py +++ b/test.py @@ -72,6 +72,7 @@ def test_post_single_account_json(self): self.assertEqual(data_json["prices"], 8518.52) self.assertEqual(data_json["gains"], 5964.76) self.assertEqual(data_json["losses"], 0) + self.assertIsInstance(data_json["details"], list) def test_post_multi_account_json(self): data = {"file": open("test-data/data_multi_account.csv", "rb")}