Skip to content

Commit

Permalink
Python API examples (#64)
Browse files Browse the repository at this point in the history
  • Loading branch information
oittaa authored Nov 19, 2021
1 parent 6d9cc85 commit adf298d
Show file tree
Hide file tree
Showing 5 changed files with 64 additions and 41 deletions.
42 changes: 38 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,29 +13,29 @@ 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
````

### 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
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 .
Expand All @@ -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"))
```
26 changes: 8 additions & 18 deletions ibkr_report/definitions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down Expand Up @@ -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
Expand Down
20 changes: 10 additions & 10 deletions ibkr_report/report.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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__(
Expand All @@ -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)

Expand All @@ -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):
Expand All @@ -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]
Expand Down
16 changes: 7 additions & 9 deletions ibkr_report/trade.py
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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
)
Expand Down Expand Up @@ -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),
Expand Down Expand Up @@ -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(
Expand All @@ -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:
Expand Down
1 change: 1 addition & 0 deletions test.py
Original file line number Diff line number Diff line change
Expand Up @@ -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")}
Expand Down

0 comments on commit adf298d

Please sign in to comment.