Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Bank cash removal, TWR computation #3

Open
wants to merge 1 commit into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ DGPC is a small and simple utility that parses CSV-exports from [DeGiro](degiro.
The tool displays:
* In green, the total value of the DeGiro account, including current stock prices, dividend, fees, cash, etc. This is the sum of the cash balance (in red, see below) and the current value of stock/ETF (not shown).
* In magenta, the total nominal account value: money transferred in to the DeGiro account, not taking into account losses or profits.
* In red, the cash balance: cash on the DeGiro account plus optional 'bank cash' if money is transferred out. Note that if cash is booked in and invested as stocks on the same day, it will not show up in the graph.
* In red, the cash balance: cash on the DeGiro account. Note that if cash is booked in and invested as stocks on the same day, it will not show up in the graph.
* In orange, a benchmark (default IWDA ETF) assuming all invested money was invested in this instead at time of availability.
* In blue, the same benchmark, but now assuming all available money was invested on day 0.

Expand Down
Binary file modified doc/example_graph.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
52 changes: 23 additions & 29 deletions src/degiro.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
import csv
import datetime
from pathlib import Path
from typing import Dict, Sequence, Tuple, List
from typing import NamedTuple, Sequence, Tuple, List

import numpy as np

Expand All @@ -17,6 +17,15 @@
# ... ano others, not complete of course


class ParsedData(NamedTuple):
"""Tuple holding all data parsed from account data"""
invested: np.ndarray
cash: np.ndarray
deposits: np.ndarray
shares_value: np.ndarray
total_account: np.ndarray


def read_account(account_csv: Path) -> Tuple[List[List[str]], datetime.date]:
"""Opens a DeGiro 'Account.csv' file and returns the contents as well as the first date"""
csv_data = list(csv.reader(account_csv.open()))
Expand All @@ -28,7 +37,7 @@ def read_account(account_csv: Path) -> Tuple[List[List[str]], datetime.date]:


def parse_single_row(row: List[str], dates: Sequence[datetime.date], date_index: int,
invested: np.ndarray, cash: np.ndarray, shares_value: np.ndarray, bank_cash: np.ndarray) -> None:
invested: np.ndarray, cash: np.ndarray, deposits: np.ndarray, shares_value: np.ndarray) -> None:
"""Parses a single row of the CSV data, updating all the NumPy arrays (they are both input and output)."""
# pylint: disable=too-many-locals,too-many-arguments,too-many-statements,too-many-branches

Expand All @@ -39,15 +48,14 @@ def parse_single_row(row: List[str], dates: Sequence[datetime.date], date_index:
# ----- Cash in and out -----

if description in ("iDEAL storting", "Storting"):
if bank_cash[date_index] > mutation:
bank_cash[date_index:] -= mutation
else:
invested[date_index:] += (mutation - bank_cash[date_index])
cash[date_index:] += (mutation - bank_cash[date_index])
bank_cash[date_index:] = 0
deposits[date_index] = mutation
invested[date_index:] += mutation
cash[date_index:] += mutation

elif description in ("Terugstorting",):
bank_cash[date_index:] -= mutation
deposits[date_index] = mutation
invested[date_index:] += mutation
cash[date_index:] += mutation

# ----- Buying and selling -----

Expand Down Expand Up @@ -118,22 +126,17 @@ def parse_single_row(row: List[str], dates: Sequence[datetime.date], date_index:
print(row)


def parse_account(csv_data: List[List[str]], dates: List[datetime.date]) -> Tuple[Dict[str, np.ndarray],
Dict[str, np.ndarray]]:
"""Parses the csv-data and constructs NumPy arrays for the given date range with cash value, total account value,
and total invested."""
def parse_account(csv_data: List[List[str]], dates: List[datetime.date]) -> ParsedData:
"""Parses the csv-data and constructs NumPy arrays for the given date range with invested money, cash value, the
deposits, the shares value and the total account value."""

# Initial values
num_days = len(dates)
invested = np.zeros(shape=num_days)
cash = np.zeros(shape=num_days)
deposits = np.zeros(shape=num_days)
shares_value = np.zeros(shape=num_days)

# We make the assumption that any money going out of the DeGiro account is still on a bank and thus counted here
# as cash. This value holds the amount of money on the bank at a given time while parsing, with future cash
# deposits reducing this value.
bank_cash = np.zeros(shape=num_days)

# Parse the CSV data
date_index = 0
stop_parsing = False
Expand All @@ -156,16 +159,7 @@ def parse_account(csv_data: List[List[str]], dates: List[datetime.date]) -> Tupl
break

parse_single_row(row, tuple(dates), date_index,
invested, cash, shares_value, bank_cash)
invested, cash, deposits, shares_value)

# Set the absolute value metrics
total_account = shares_value + cash
absolutes = {"nominal account (without profit/loss)": invested,
"cash in DeGiro account": cash,
"total account value": total_account}

# Set the relative metrics
performance = np.divide(total_account, invested, out=np.zeros_like(invested), where=invested != 0)

relatives = {"account performance": performance}
return absolutes, relatives
return ParsedData(invested, cash, deposits, shares_value, total_account)
41 changes: 40 additions & 1 deletion src/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,41 @@ def parse_arguments() -> Any:
return vars(parser.parse_args())


def compute_absolutes(data: degiro.ParsedData) -> Dict[str, np.ndarray]:
"""Sets all the absolute values we want to plot."""
return {
"nominal account (without profit/loss)": data.invested,
"cash in DeGiro account": data.cash,
"total account value": data.total_account,
}


def compute_relatives(data: degiro.ParsedData) -> Dict[str, np.ndarray]:
"""Sets all the relative values we want to plot. For the TWR, this follows the computation from:
https://www.fool.com/about/how-to-calculate-investment-returns/"""
num_days = data.invested.shape[0] # or any other array - they are all the same size

# Set the relative metrics
performance = np.divide(data.total_account, data.invested,
out=np.zeros_like(data.invested), where=data.invested != 0) - 1

# Computes the daily returns adjusted for deposits
returns = (data.total_account[1:] / (data.total_account[:-1] + data.deposits[1:])) - 1
returns = np.concatenate([[0], returns])

# Computes the time-weighted-returns
running_twr = 1
twr = np.zeros(num_days)
for i in range(num_days):
running_twr *= (1 + returns[i])
twr[i] = running_twr - 1

return {
"account performance": performance,
"time-weighted-return": twr
}


def compute_reference_invested(reference: np.ndarray, invested: np.ndarray) -> np.ndarray:
"""Given some amount of cash investment over time, compute the reference stock/ETF's value given that all the
invested cash was used to buy the reference stock/ETF at the time when it was available. Assumes partial shares
Expand Down Expand Up @@ -83,7 +118,11 @@ def dgpc(input_file: Path, output_png: Path, output_csv: Path, end_date: datetim

# Parse the DeGiro account data
print(f"[DGPC] Parsing DeGiro data with {len(csv_data)} rows from {dates[0]} till {dates[-1]}")
absolute_data, relative_data = degiro.parse_account(csv_data, dates)
parsed_account_data = degiro.parse_account(csv_data, dates)

# Compute the values we want to plot
absolute_data = compute_absolutes(parsed_account_data)
relative_data = compute_relatives(parsed_account_data)

# Filter out all values before the chosen 'start_date' (default: today)
if start_date in dates:
Expand Down
4 changes: 2 additions & 2 deletions src/plot.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ def get_colour(label: str) -> Optional[str]:
return "blue"
if "cash" in label:
return "red"
if "nominal account" in label:
if "nominal account" in label or "time-weighted-return" in label:
return "magenta"
if "account" in label:
return "green"
Expand Down Expand Up @@ -58,7 +58,7 @@ def plot(dates: List[datetime.date], absolute_data: Dict[str, np.ndarray],
plt.subplot(212)
axis = plt.gca()
for name, values in relative_data.items():
plt.plot(x_values, 100 * values - 100, label=name, color=get_colour(name))
plt.plot(x_values, values * 100, label=name, color=get_colour(name))
plt.ylabel("Performance (%)")
plt.xticks(x_values[::x_label_freq], labels=dates[::x_label_freq], rotation=45)
axis.set_xlim(xmin=0, xmax=len(x_values))
Expand Down
22 changes: 11 additions & 11 deletions tests/test_degiro.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,9 +18,9 @@ def test_parse_cash_addition() -> None:
]))
dates = [datetime.date(2020, 3, 9) + datetime.timedelta(days=days) for days in range(0, 4)]

abs_data, _ = degiro.parse_account(csv_data, dates)
np.testing.assert_equal(abs_data["nominal account (without profit/loss)"], abs_data["cash in DeGiro account"])
np.testing.assert_equal(abs_data["nominal account (without profit/loss)"], [0, 500, 500, 1500])
data = degiro.parse_account(csv_data, dates)
np.testing.assert_equal(data.invested, data.cash)
np.testing.assert_equal(data.invested, [0, 500, 500, 1500])


def test_parse_buy_and_sell() -> None:
Expand All @@ -34,10 +34,10 @@ def test_parse_buy_and_sell() -> None:
]))
dates = [datetime.date(2017, 7, 10) + datetime.timedelta(days=days) for days in range(0, 5)]

abs_data, _ = degiro.parse_account(csv_data, dates)
np.testing.assert_allclose(abs_data["nominal account (without profit/loss)"], [0, 500, 500, 500, 500])
np.testing.assert_allclose(abs_data["cash in DeGiro account"], [0, 402.816779, 402.816779, 632.661502, 632.661502])
np.testing.assert_allclose(abs_data["total account value"], [0, 499.720938, 502.992033, 632.661502, 632.661502])
data = degiro.parse_account(csv_data, dates)
np.testing.assert_allclose(data.invested, [0, 500, 500, 500, 500])
np.testing.assert_allclose(data.cash, [0, 402.816779, 402.816779, 632.661502, 632.661502])
np.testing.assert_allclose(data.total_account, [0, 499.720938, 502.992033, 632.661502, 632.661502])


def test_parse_transaction_costs() -> None:
Expand All @@ -51,7 +51,7 @@ def test_parse_transaction_costs() -> None:
]))
dates = [datetime.date(2017, 7, 10) + datetime.timedelta(days=days) for days in range(0, 3)]

abs_data, _ = degiro.parse_account(csv_data, dates)
np.testing.assert_allclose(abs_data["nominal account (without profit/loss)"], [0, 500, 500])
np.testing.assert_allclose(abs_data["cash in DeGiro account"], [0, 500, 497.38])
np.testing.assert_allclose(abs_data["total account value"], [0, 500, 497.38])
data = degiro.parse_account(csv_data, dates)
np.testing.assert_allclose(data.invested, [0, 500, 500])
np.testing.assert_allclose(data.cash, [0, 500, 497.38])
np.testing.assert_allclose(data.total_account, [0, 500, 497.38])