Files
Notion-Performance-Tracker/functions.py
2025-11-30 00:22:09 +01:00

719 lines
24 KiB
Python

### -------------------- LIBARIES --------------------
import datetime
import time
import json
import yfinance as yf
import pandas as pd
import requests
import config
### -------------------- FUNCTIONS --------------------
# ------------------#
# LEVEL 1 FUNCTIONS #
# ------------------#
# CALCULATE THE IRR
def calculate_irr(date_now, date_open, value_now, value_open):
error = False
irr = 0.0
try:
# Count the number in days
a = date_now - date_open
a = a.days
# Am Tag des Kaufs selbst, liegt das Delta in Tagen bei 0
# Um dennoch einen IRR kalkulieren zu können, wird das Delta auf 1 gsetzt
if a == 0:
a = 1
a = a / 365 # Umrechnung auf Jahresanteil, um auch den Jahreszinssatz zu bekommen
b = value_now / value_open
# Catch negative IRRs
if b < 0:
b = b * (-1)
irr = b**(1/a) # matematisch identisch zur b-ten Wurzel von a
irr = irr * (-1)
else:
irr = b**(1/a) # matematisch identisch zur b-ten Wurzel von a
except:
error = True
# Return data if successful
if error == True:
print("[ERROR] Calculation of irr")
return error
else:
return irr
# GET THE DAY OF THE OLDEST TRADE
def get_date_open_oldest_trade(trades):
# Identify the open date for the oldest trade
date_open_oldest_trade = datetime.date.today()
for i in trades:
if trades[i]["date_open"] < date_open_oldest_trade:
date_open_oldest_trade = trades[i]["date_open"]
return date_open_oldest_trade
# CREATES LIST OF UNIQUE TICKERS
def filter_list_of_tickers(trades):
tickers = []
try:
for i in trades:
# Fetch ticker belonging to trade
ticker = trades[i]['ticker']
# Add ticker to list, if not already present
if ticker not in tickers:
tickers.append(ticker)
print("[SUCCESS] Creating a list of {} tickers".format(len(tickers)))
return tickers
except:
print("[ERROR] Creating a list of tickers")
return tickers
# NOTION FETCH PAGES
def notion_get_pages(db_id_trades, num_pages=None):
try:
# ------------------ FETCH THE FIRST 100 PAGES FROM A DB
# Prepare Request
url = f"https://api.notion.com/v1/databases/{db_id_trades}/query"
get_all = num_pages is None # If num_pages is None, get all pages, otherwise just the defined number.
page_size = 100 if get_all else num_pages
payload = {"page_size": page_size}
# Make Request
raw_response = requests.post(url, json=payload, headers=config.notion_headers)
# Process Reply
parsed_response = raw_response.json()
result = parsed_response["results"]
# ------------------ FETCH 100 MORE PAGES AS OFTEN AS REQUIRED
while parsed_response["has_more"] and get_all:
# Prepare Request
payload = {"page_size": page_size, "start_cursor": parsed_response["next_cursor"]}
url = f"https://api.notion.com/v1/databases/{db_id_trades}/query"
# Make Request
raw_response = requests.post(url, json=payload, headers=config.notion_headers)
# Process Reply
parsed_response = raw_response.json()
result.extend(parsed_response["results"])
# Logging
print("[SUCCESS] Fetching Data from Notion for database: {}".format(db_id_trades))
return result
except Exception:
print("[ERROR] Fetching Data from Notion with error: {}".format(Exception))
return True # Return True when there was an error
# NOTION UPDATE PAGES
def notion_update_page(page_id: str, data: dict):
url = f"https://api.notion.com/v1/pages/{page_id}"
payload = {"properties": data}
results = requests.patch(url, json=payload, headers=config.notion_headers)
return results
# TRMNL UPDATE DIAGRAMMS
'''
def push_trmnl_update_chart(wklydict_numbers, dict_chart, trmnl_page_id):
# --------------------------------------------------------------------------------- #
# REQUIREMENTS:
# The first-level-entry of dict_numbers needs be the "key" for referencing in TRMNL
# At least 3 keys are expected for numbers to fill the screen
# The json_chart data does not need a key
# Instead, each first-level-entry represents a series in the chart by name
# --------------------------------------------------------------------------------- #
# Create the data-structure for the chart-object
payload = {}
payload = dict_numbers
# Create the data-structure for the chart-object
chart = []
for entry in json_chart:
series = {}
name = entry[0]
dict_chart_values = entry[1]
series["name"] = name
series["data"] = dict_chart_values
chart.append(series)
# {"name" : name, "data" : [[date, value], [date, value]]}
try:
data = json.dumps(json_data, indent=2) # Converts a python-dictionary into a json
url = trmnl_base_url + trmnl_page_id
reply = requests.post(url, data=data, headers = trmnl_headers)
if reply.status_code == 200:
print("[SUCCESS] Pushing data to TRMNL")
elif reply.status_code == 429:
print("[WARNING] Exceeded TRMNL's API rate limits")
else:
print(f"[ERROR] Pushing data to TRMNL with server reply code: {reply.status_code}")
except Exception as e:
print("[ERROR] Pushing data to TRMNL with error code: {}".format(e))
'''
# ------------------#
# LEVEL 2 FUNCTIONS #
# ------------------#
# CREATE LIST OF WEEKLY DATES
def create_list_wkl_dates(trades):
index_date = get_date_open_oldest_trade(trades)
# Move start date from weekend to weekday
while index_date.weekday() > 5:
index_date = index_date + datetime.timedelta(days=1)
# Create list
list_wkl_dates = []
while index_date <= datetime.date.today():
list_wkl_dates.append(index_date.isoformat())
index_date = index_date + datetime.timedelta(days=7)
# Logging
print("[SUCCESS] Generating list of weekly dates with {} entries".format(len(list_wkl_dates)))
return list_wkl_dates
# NOTION FETCH & FORMAT TRADES
def fetch_format_notion_trades(db_id_trades):
trades = {}
error = False
data = notion_get_pages(db_id_trades)
if data is True:
error = True
else:
for i in data:
# Each page is loaded as a dictionary
notion_page = dict(i)
# Handling desired missing entries
try:
date_close = notion_page["properties"]["Close"]["date"]
date_close = date_close["start"]
date_close = datetime.date(*map(int, date_close.split('-')))
except:
date_close = 0
# Handeling non-desired missing entries (by skipping this trade)
try:
# Try extracting values
trade = {}
# Format date-open
date_open = notion_page["properties"]["Open"]["date"]
date_open = date_open["start"]
date_open = datetime.date(*map(int, date_open.split('-')))
# Combine data into json structure
trade = {
'ticker' : notion_page["properties"]["Ticker"]["select"]["name"],
'date_open' : date_open,
'date_close' : date_close,
'course_open' : notion_page["properties"]["Open (€)"]["number"],
'course_close' : notion_page["properties"]["Close (€)"]["number"],
'course_current' : notion_page["properties"]["Current (€)"]["number"],
'irr' : notion_page["properties"]["IRR (%)"]["number"],
'units' : notion_page["properties"]["Units"]["number"],
'dividends' : notion_page["properties"]["Dividends (€)"]["number"]
}
# Save values
notion_page_id = notion_page["id"] # Use as key for the dictionary
trades[notion_page_id] = trade
except:
print("[ERROR] Skipped an entry in the notion trades-db - Missing values?")
# Return data if successful
if error == True:
return error
else:
return trades
# NOTION FETCH & FORMAT INVESTMENT OVERVIEW
def fetch_format_notion_investments(db_id_investments):
investments = {}
error = False
data = notion_get_pages(db_id_investments)
if data is True:
error = True
else:
for i in data:
# Each page is loaded as a dictionary
notion_page = dict(i)
# Extract values
notion_page_id = notion_page["id"] # Use as key for the dictionary
investments[notion_page_id] = {}
investments[notion_page_id]["ticker"] = notion_page["properties"]["Ticker"]["select"]["name"]
investments[notion_page_id]["total_dividends"] = notion_page["properties"]["Dividends (€)"]["number"]
investments[notion_page_id]["current_value"] = notion_page["properties"]["Current (€)"]["number"]
investments[notion_page_id]["current_irr"] = notion_page["properties"]["IRR (%)"]["number"]
investments[notion_page_id]["total_performanance"] = notion_page["properties"]["Performance (€)"]["number"]
# Return data if successful
if error == True:
return error
else:
return investments
# YFINANCE FETCH & FORMAT DATA
def fetch_format_yf_data(tickers):
yf_data = {}
error_count = 0
for i in tickers:
ticker = i
try:
api = yf.Ticker(ticker)
data = api.history(period="max")
# Convert to Pandas DataFrame
data = pd.DataFrame(data)
# Delete the columns "Stock Splits", "High", "Low" and "Open"
del data['Open']
del data['Low']
del data['High']
del data['Volume']
try:
del data['Stock Splits']
del data['Capital Gains']
except:
time.sleep(0)
# Get the Number of rows in data
data_rows = data.shape[0]
# Create new index without the time from the existing datetime64-index
old_index = data.index
new_index = []
x = 0
while x < data_rows:
date = pd.Timestamp.date(old_index[x]) # Converts the "Pandas Timestamp"-object to a "date" object
new_index.append(date)
x+=1
# Add the new index to the dataframe and set it as the index
data.insert(1, 'Date', new_index)
data.set_index('Date', inplace=True)
# Save the data-frame to the yf_data dict
yf_data[ticker] = data
# Wait for the API to cool down
time.sleep(config.api_cooldowm_time)
except:
error_count = error_count + 1
print("[ERROR] Fetching data from yahoo-finance for ticker: {}".format(ticker))
if error_count == 0:
print("[SUCCESS] Fetching data from yahoo-finance")
else:
print("[ERROR] Fetching data from yahoo-finance in {} cases".format(error_count))
# Return data if successful
return yf_data
# UPDATE NOTION-TRADES-DATABASE
def push_notion_trades_update(trades):
# Logging
error_count = 0
for notion_page_id in trades:
try:
# The irr is stored in the format 1.2534
# Notion need the format 0,2534
irr_notion = trades[notion_page_id]['irr'] - 1
irr_notion = round(irr_notion, 4)
# Construct Notion-Update-Object
notion_update = {
"Current (€)": {
"number": trades[notion_page_id]['course_current']
},
"IRR (%)": {
"number": irr_notion
},
"Dividends (€)": {
"number": trades[notion_page_id]['dividends']
}
}
# Update the properties of the corresponding notion-page
notion_update_page(notion_page_id, notion_update)
except:
error_count = error_count + 1
# Wait for the API to cool off
time.sleep(config.api_cooldowm_time)
# Logging
if error_count == 0:
print ("[SUCCESS] Updating notion trades")
else:
print("[[SUCCESS] Updating notion trades in {} cases".format(error_count))
return trades
# UPDATE NOTION-INVESTMENT-OVERVIEW
def push_notion_investment_update(investments):
# Logging
error_count = 0
for notion_page_id in investments:
try:
# The irr is stored in the format 1.2534
# Notion need the format 0,2534
irr_notion = investments[notion_page_id]['current_irr'] - 1
irr_notion = round(irr_notion, 4)
# Construct Notion-Update-Object
notion_update = {
"Current (€)": {
"number": investments[notion_page_id]['current_value']
},
"IRR (%)": {
"number": irr_notion
},
"Performance (€)": {
"number": investments[notion_page_id]['total_performanance']
},
"Dividends (€)": {
"number": investments[notion_page_id]['total_dividends']
}
}
# Update the properties of the corresponding notion-page
notion_update_page(notion_page_id, notion_update)
except:
error_count = error_count + 1
# Wait for the API to cool off
time.sleep(config.api_cooldowm_time)
# Logging
if error_count == 0:
print ("[SUCCESS] Updating notion investments")
else:
print("[[SUCCESS] Updating notion investments in {} cases".format(error_count))
return investments
# SELECT CURRENT VALUES PER TRADE
def select_current_value_per_trade(trades, history_per_trade):
# Logging
error_count = 0
# Loop over all trades
for trade_id in trades:
try:
# Determine, what values to fetch based on whether the trade was closed already
date_closed = trades[trade_id]["date_close"]
if date_closed == 0:
# If trade still open, use performance data from today
index_date_iso = datetime.date.today().isoformat()
else:
# If trade closed, use performance data from close-date
index_date_iso = date_closed.isoformat()
# Fetch data from history and save for this trade
trades[trade_id]["course_current"] = history_per_trade[index_date_iso][trade_id]['current_course']
trades[trade_id]["irr"] = history_per_trade[index_date_iso][trade_id]['current_irr']
trades[trade_id]["dividends"] = history_per_trade[index_date_iso][trade_id]['total_dividends']
except:
error_count = error_count + 1
if error_count == 0:
print ("[SUCCESS] Selecting current value per trade")
else:
print("[ERROR] Selecting current value per trade for {} cases".format(error_count))
return trades
# SELECT CURRENT VALUES PER TRADE
def select_current_value_per_ticker(investments, history_per_ticker):
# Logging
error_count = 0
# Loop over all investments
for investment_id in investments:
try:
# Generate the iso-date of today as the required index
index_date_iso = datetime.date.today().isoformat()
# Get the ticker corresponding to the investment
ticker = investments[investment_id]["ticker"]
# Select latest data from history and save for this investment
investments[investment_id]["total_dividends"] = history_per_ticker[index_date_iso][ticker]['total_dividends']
investments[investment_id]["current_value"] = history_per_ticker[index_date_iso][ticker]['current_value']
investments[investment_id]["current_irr"] = history_per_ticker[index_date_iso][ticker]['current_irr']
investments[investment_id]["total_performanance"] = history_per_ticker[index_date_iso][ticker]['total_performanance']
except:
error_count = error_count + 1
if error_count == 0:
print ("[SUCCESS] Selecting current value per investment")
else:
print("[ERROR] Selecting current value per investment for {} cases".format(error_count))
return investments
# ------------------#
# LEVEL 3 FUNCTIONS #
# ------------------#
# FILTER ANY HISTORY OBJECT TO SELECTED DATES
def filter_history_by_list(history, dates_list):
filtered_history = {}
# Loop over all days
for index_date in history:
# Check, if the history-date is in the filter-list
if index_date in dates_list:
# If so, add this date-entry to the filtered history object
filtered_history[index_date] = history[index_date]
# Logging
print ("[SUCCESS] Filtering History")
return filtered_history
# CALC HISTORY PER TRADE
def calc_history_per_trade(trades, yf_data):
# Create support variables
history_per_trade = {}
total_dividends = 0
warning_count = 0
date_open_oldest_trade = get_date_open_oldest_trade(trades)
# ------------------ LOOP OVER ALL TRADES
for trade_id in trades:
# ------------------ PREPARE FOR THE (NEXT) LOOP OVER ALL DAYS
# Set / Reset the index-date to the oldest trade day
# Resetting is required so that the calculations for the next trade start with day 1
index_date = date_open_oldest_trade
# Set the initial value for the course on the previous day to 0
# Just in case the very first trade was made on a weekend somehow, where there is no yfinance data available
previous_course = 0.0
# Check, if the trade was closed already
# If it was not, set the closure date to the future (Trick 17)
if trades[trade_id]["date_close"] == 0:
date_close = datetime.date.today() + datetime.timedelta(days=1)
else:
date_close = trades[trade_id]["date_close"]
date_open = trades[trade_id]["date_open"]
# Keep ticker for connecting performance later
ticker = trades[trade_id]['ticker']
# ------------------ DETERMINE THE COUSE PER DAY
while index_date != datetime.date.today() + datetime.timedelta(days=1):
# Fetch course for the day & eventual dividends from yf_data
try:
current_course = yf_data[ticker].at[index_date, 'Close']
current_dividends_per_ticker = yf_data[ticker].at[index_date, 'Dividends']
# Catch missing yf-data (eg. for weekends) by reusing course from previous day
except:
current_course = previous_course
current_dividends_per_ticker = 0.0 # there are never dividends on non-trading days
warning_count = warning_count +1 # Increase the warning count
# Catch the special case of the day when the trade was closed
# In this case, the current course needs to be overwritten with the sell-value
if date_close == index_date:
current_course = trades[trade_id]['course_close']
# Save the result for the next iteration
# This setup also makes it possible, that a previous course is passed down across mutiple days
# This makes sense is case i.e. for a weekend
previous_course = current_course
# ------------------ CALCULATE PERFORMANCE IF REQUIRED
if index_date >= date_open and index_date <= date_close:
# Calculate performance values
current_amount = trades[trade_id]['units']
current_invested = current_amount * trades[trade_id]['course_open']
total_dividends = total_dividends + current_amount * current_dividends_per_ticker
current_value = current_amount * current_course
current_value_with_dividends = current_value + total_dividends
current_irr = calculate_irr(index_date, date_open, current_value_with_dividends, current_invested)
total_performanance = current_value_with_dividends - current_invested
if current_value_with_dividends == 0:
print("0-value Error with ticker: {}".format(ticker))
else:
# Write 0, if trade is not relevant for current timeframe
current_amount = 0
current_invested = 0.00
total_dividends = 0.00
current_value = 0.00
current_irr = 0.00
total_performanance = 0.0
# ------------------ STORE RESULTS
index_date_iso = index_date.isoformat()
# Store all values into a dict
dict_a = {}
dict_a['current_amount'] = current_amount
dict_a['current_invested'] = current_invested
dict_a['total_dividends'] = total_dividends
dict_a['current_value'] = current_value
dict_a['current_irr'] = current_irr
dict_a['current_course'] = current_course
dict_a['total_performanance'] = total_performanance
# Check if the date is already present
if index_date_iso in history_per_trade:
dict_b = history_per_trade[index_date_iso]
else:
dict_b = {}
# Add the values to the trade_id value-pair
dict_b[trade_id] = dict_a
# Update the hostory_per_trade
history_per_trade.update({index_date_iso : dict_b})
# ------------------ NEXT ITERATION
index_date = index_date + datetime.timedelta(days=1)
# ------------------ LOGGING
if warning_count > 0:
print("[WARNING] Calculating history for trade: {} with ticker: {}".format(trade_id, ticker))
print(" No yf-data available in {} cases of ticker & date".format(warning_count))
print(" Probably reason is non-trading-days eg. weekends")
print(" Used values from previous trade-day instead")
else:
print("[SUCCESS] Calculating history for trade: {} with ticker: {}".format(trade_id, ticker))
# Reset warning count for the next trade
warning_count = 0
data = json.dumps(history_per_trade, indent=2) # Converts a python-dictionary into a json
# Logging
print("[SUCCESS] Calculating history for trades")
with open("history_per_trade.json", "w") as f:
f.write(data)
return history_per_trade
# CALC THE HISTORY PER TRADE & OVERALL
def calc_history_per_ticker(history_per_trade, tickers, trades):
# ------------------ CREATE JSON OBJECT
# Create the json-dict
history_per_ticker = {}
# Loop over each date entry in the history
for date_entry in history_per_trade:
# Create a dict to store the results per day and ticker
dict_daily = {}
for ticker in tickers:
dict_daily[ticker] = {}
dict_daily[ticker]["current_invested"] = 0
dict_daily[ticker]["total_dividends"] = 0
dict_daily[ticker]["current_value"] = 0
dict_daily[ticker]["current_irr"] = 0
dict_daily[ticker]["current_irr"] = 0
dict_daily[ticker]["total_performanance"] = 0
dict_daily[ticker]["current_amount"] = 0 # Added only for ticker entries, not for the "total" value
dict_daily[ticker]["current_course"] = 0 # Added only for ticker entries, not for the "total" value
dict_daily["total"] = {}
dict_daily["total"]["current_invested"] = 0
dict_daily["total"]["total_dividends"] = 0
dict_daily["total"]["current_value"] = 0
dict_daily["total"]["current_irr"] = 0
dict_daily["total"]["current_irr"] = 0
dict_daily["total"]["total_performanance"] = 0
# Loop over each trade-entry for that day
for trade_id in history_per_trade[date_entry]:
# Extract data from the history_per_trade
trade_amount = history_per_trade[date_entry][trade_id]['current_amount']
trade_invested = history_per_trade[date_entry][trade_id]['current_invested']
trade_dividends = history_per_trade[date_entry][trade_id]['total_dividends']
trade_value = history_per_trade[date_entry][trade_id]['current_value']
trade_irr = history_per_trade[date_entry][trade_id]['current_irr']
trade_course = history_per_trade[date_entry][trade_id]['current_course']
trade_performanance = history_per_trade[date_entry][trade_id]['total_performanance']
# Lookup the ticker by the trade-id
ticker = trades[trade_id]["ticker"]
# Extract data from the history_per_ticker
ticker_amount = dict_daily[ticker]['current_amount']
ticker_invested = dict_daily[ticker]['current_invested']
ticker_dividends = dict_daily[ticker]['total_dividends']
ticker_value = dict_daily[ticker]['current_value']
ticker_irr = dict_daily[ticker]['current_irr']
ticker_performanance = dict_daily[ticker]['total_performanance']
# Overwrite the values in the history_per_ticker
dict_daily[ticker]['current_amount'] = ticker_amount + trade_amount # Simple addition works
dict_daily[ticker]['current_invested'] = ticker_invested + trade_invested
dict_daily[ticker]['total_dividends'] = ticker_dividends + trade_dividends
dict_daily[ticker]['current_value'] = ticker_value + trade_value
dict_daily[ticker]['total_performanance'] = ticker_performanance + trade_performanance
dict_daily[ticker]['current_course'] = trade_course # Simple overwrite is fine, as the course is the same for all trades
if ticker_invested == 0 and trade_invested == 0:
dict_daily[ticker]['current_irr'] = 0
# Catch 0 values
else:
dict_daily[ticker]['current_irr'] = (ticker_irr * ticker_invested + trade_irr * trade_invested) / (ticker_invested + trade_invested)
# --> IRR is adjusted based on the trade values. This way a trade of 25% of the initial trade volume has only a 25% influence on the irr
# Calculate the "total" entry after finishing with all the trades
for ticker in tickers:
# Same logic as above, but shortended code
dict_daily["total"]['total_dividends'] = dict_daily["total"]['total_dividends'] + dict_daily[ticker]['total_dividends']
dict_daily["total"]['current_value'] = dict_daily["total"]['current_value'] + dict_daily[ticker]['current_value']
dict_daily["total"]['total_performanance'] = dict_daily["total"]['total_performanance'] + dict_daily[ticker]['total_performanance']
# Extracting the values before rewriting them, to preserve them for the IRR calculation
total_invested = dict_daily["total"]['current_invested']
ticker_invested = dict_daily[ticker]['current_invested']
dict_daily["total"]['current_invested'] = total_invested + ticker_invested
# Extracting the values before rewriting them, to preserve them for the IRR calculation
if ticker_invested == 0 and total_invested == 0:
dict_daily["total"]['current_irr'] = 0
else:
total_irr = dict_daily["total"]['current_irr']
ticker_irr = dict_daily[ticker]['current_irr']
dict_daily["total"]['current_irr'] = (total_irr * total_invested + ticker_irr * ticker_invested) / (total_invested + ticker_invested)
# Finally, write the results for this day-entry to the history_per_ticker
history_per_ticker[date_entry] = dict_daily
# Logging
print("[SUCCESS] Calculating history per ticker")
data = json.dumps(history_per_ticker, indent=2) # Converts a python-dictionary into a json
with open("history_per_ticker.json", "w") as f:
f.write(data)
return history_per_ticker