#!/usr/bin/env python3
# -*- coding: utf-8 -*-
# Copyright (C) 2023 Brendan Cody-Kenny
# This script is licensed under the GNU General Public License v3.0.
# See <https://www.gnu.org/licenses/gpl-3.0.en.html> for the full license text.
# Author: Brendan Cody-Kenny
# Email: codykenny@gmail.com
# LinkedIn: https://www.linkedin.com/in/brendancodykenny
"""
This script calculates gains or losses realized for each sale transaction
First-in-first-out (FIFO) is used to match purchases with each sale
Purchases that match a sale are listed before each sale
Transactions are read from a file called degiro_transactions.csv
The full list of all transactions are required to "resolve" all sales
Example usage: python3 degiro-gain-loss-calculator.py > gains-losses-all-time.csv
"""
# Dependencies: Standard Python Library (Python 3.11)
# Version: 1.0
# Date: 2023-10-22
import csv
import sys
from collections import deque
def round_floats_in_dict(data_dict, decimal_places=3):
"""Round float values in a dictionary to the specified number of decimal places."""
for key, value in data_dict.items():
if isinstance(value, float):
data_dict[key] = round(value, decimal_places)
return data_dict
def print_dicts_to_csv(dicts_list):
# Ensure the list isn't empty
if not dicts_list:
return
rounded_data = [round_floats_in_dict(d.copy()) for d in dicts_list]
# Create a CSV writer that writes to the console
writer = csv.DictWriter(sys.stdout, fieldnames=dicts_list[0].keys())
# Write the header (field names)
writer.writeheader()
# Write the rows
for d in rounded_data:
writer.writerow(d)
def compute_gain_or_loss(filename):
with open(filename, "r") as file:
reader = csv.DictReader(file)
purchases = {}
capital_gain_or_loss = 0.0
transactions_realized = []
for row in reversed(list(reader)):
transaction_year = int(row["Date"].split("-")[-1])
# Extract relevant fields
ISIN = row["ISIN"]
quantity = int(row["Quantity"])
price_per_stock_in_eur = abs(float(row["Total"]) / quantity)
t_date = row["Date"]
t_time = row["Time"]
if quantity > 0: # It's a purchase
if ISIN not in purchases:
purchases[ISIN] = deque()
purchases[ISIN].append(
(quantity, price_per_stock_in_eur, t_date, t_time)
)
else: # It's a sale
quantity = abs(quantity) # Make quantity positive for sales
sale_quantity = quantity
capital_gain_or_loss_this_sale = 0
while quantity > 0 and purchases.get(ISIN):
purchase_quantity, purchase_price, p_date, p_time = purchases[
ISIN
].popleft()
if purchase_quantity <= quantity:
capital_gain_or_loss_this_sale += (
price_per_stock_in_eur - purchase_price
) * purchase_quantity
quantity -= purchase_quantity
transactions_realized.append(
{
"type": "purchase",
"isin": ISIN,
"date": p_date,
"time": p_time,
"quantity": purchase_quantity,
"total": (purchase_price) * purchase_quantity,
"capital_derived_from_purchase": (
price_per_stock_in_eur - purchase_price
)
* purchase_quantity,
"total_realized_on_sale": "",
}
)
else:
capital_gain_or_loss_this_sale += (
price_per_stock_in_eur - purchase_price
) * quantity
remaining_purchase_quantity = purchase_quantity - quantity
purchases[ISIN].appendleft(
(
remaining_purchase_quantity,
purchase_price,
p_date,
p_time,
)
)
quantity = 0
transactions_realized.append(
{
"type": "purchase",
"isin": ISIN,
"date": p_date,
"time": p_time,
"quantity": purchase_quantity,
"total": (purchase_price) * purchase_quantity,
"capital_derived_from_purchase": (
price_per_stock_in_eur - purchase_price
)
* quantity,
"total_realized_on_sale": "",
}
)
capital_gain_or_loss += capital_gain_or_loss_this_sale
transactions_realized.append(
{
"type": "sale",
"isin": ISIN,
"date": t_date,
"time": t_time,
"quantity": sale_quantity,
"total": row["Total"],
"capital_derived_from_purchase": "",
"total_realized_on_sale": capital_gain_or_loss_this_sale,
}
)
print_dicts_to_csv(transactions_realized)
return capital_gain_or_loss
filename = "degiro_transactions.csv"
gain_or_loss = compute_gain_or_loss(filename)
print(
"Total realized capital "
+ ("gain" if gain_or_loss > 0 else "loss")
+ f" €{gain_or_loss:.2f}"
)