performance_tracker.py 57 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031103210331034103510361037103810391040104110421043104410451046104710481049105010511052105310541055105610571058105910601061106210631064106510661067106810691070107110721073107410751076107710781079108010811082108310841085108610871088108910901091109210931094109510961097109810991100110111021103110411051106110711081109111011111112111311141115111611171118111911201121112211231124112511261127112811291130113111321133113411351136113711381139114011411142114311441145114611471148114911501151115211531154115511561157115811591160116111621163116411651166116711681169117011711172117311741175117611771178117911801181118211831184118511861187118811891190119111921193119411951196119711981199120012011202120312041205120612071208120912101211121212131214121512161217121812191220122112221223122412251226122712281229123012311232123312341235123612371238123912401241124212431244124512461247124812491250125112521253125412551256125712581259126012611262126312641265126612671268126912701271127212731274127512761277127812791280128112821283128412851286128712881289129012911292129312941295129612971298129913001301130213031304130513061307130813091310131113121313131413151316131713181319132013211322132313241325132613271328132913301331133213331334133513361337133813391340134113421343134413451346134713481349135013511352135313541355135613571358135913601361136213631364136513661367136813691370137113721373137413751376137713781379138013811382138313841385138613871388138913901391139213931394139513961397139813991400140114021403140414051406140714081409141014111412141314141415141614171418141914201421142214231424142514261427142814291430143114321433143414351436143714381439144014411442144314441445144614471448144914501451145214531454145514561457145814591460146114621463146414651466146714681469147014711472147314741475147614771478147914801481148214831484148514861487148814891490149114921493149414951496149714981499
  1. ### --------------------------------------------- ---------------------- --------------------------------------------- ###
  2. ### --------------------------------------------- PROGRAMM CONFIGUARTION --------------------------------------------- ###
  3. ### --------------------------------------------- ---------------------- --------------------------------------------- ###
  4. # Program Functionality Switch
  5. update_notion = True
  6. update_TRMNL = True
  7. calculate_benchmark = True
  8. log_coloring = False
  9. # Program Functionality Configuration
  10. programm_cooldown_time = 15 # Programm cooldown timer in minutes
  11. api_cooldowm_time = 0.1 # API cooldown timer in minutes
  12. trmnl_granularity = 80 # Days in between two data points in the TRMNL chart
  13. ticker_benchmark = "VGWL.DE" # Ticker to benchmark the trades against
  14. # Programm Execution Configuration
  15. selected_logging_level = "warning" # must be one from the list below
  16. logging_levels = ("none", "error", "success", "warning", "info", "debug") # ordered by amount of logs
  17. class log_colors:
  18. error = '\033[91m'
  19. warning = '\033[93m'
  20. success = '\033[92m'
  21. info = '\033[90m'
  22. debug = '\033[4m'
  23. endcode = '\033[0m'
  24. ### --------- API CONFIGURATION
  25. # NOTION
  26. notion_token = "secret_b7PiPL2FqC9QEikqkAEWOht7LmzPMIJMWTzUPWwbw4H"
  27. notion_headers = {
  28. "Authorization": "Bearer " + notion_token,
  29. "Content-Type": "application/json",
  30. "Notion-Version": "2022-02-22"
  31. }
  32. notion_db_id_trades = "95f7a2b697a249d4892d60d855d31bda"
  33. notion_db_id_investments = "2ba10a5f51bd8160ab9ee982bbef8cc3"
  34. notion_db_id_performance = "1c010a5f51bd806f90d8e76a1286cfd4"
  35. # TRMNL
  36. trmnl_headers = {"Content-Type": "application/json"}
  37. trmnl_url_chart_1 = "https://usetrmnl.com/api/custom_plugins/334ea2ed-1f20-459a-bea5-dca2c8cf7714"
  38. trmnl_url_chart_2 = "https://usetrmnl.com/api/custom_plugins/72950759-38df-49eb-99fb-b3e2e67c385e"
  39. trmnl_url_chart_3 = "https://usetrmnl.com/api/custom_plugins/a975543a-51dc-4793-b7fa-d6a101dc4025"
  40. ### -------------------- LIBARIES
  41. import datetime
  42. import time
  43. import json
  44. import yfinance as yf
  45. import pandas as pd
  46. import requests
  47. ### ---------------------------------------------------- --------- ---------------------------------------------------- ###
  48. ### ---------------------------------------------------- FUNCTIONS ---------------------------------------------------- ###
  49. ### ---------------------------------------------------- --------- ---------------------------------------------------- ###
  50. # ---------------- #
  51. # HELPER FUNCTIONS #
  52. # ---------------- #
  53. # LOGGING / PRINTING TO TERMINAL
  54. def logging(message = "", logging_level = "", new_line = True):
  55. # Take the selected logging level in the config file
  56. # Look this up in the list of all available logging levels in the config file
  57. # Return the index number
  58. config_logging_level = logging_levels.index(selected_logging_level)
  59. try:
  60. # Take the logging level of the text to print
  61. # Look this up in the list of all available logging levels in the config file
  62. # Return the index number
  63. message_logging_level = logging_levels.index(logging_level)
  64. except:
  65. # Fallback to the least important logging level
  66. # Solved by checking the lenght of the available logging levels
  67. message_logging_level = len(logging_levels)
  68. # Check for false new_line entries
  69. if new_line is not bool:
  70. new_line = True
  71. # Check if the warning should be printed
  72. if message_logging_level <= config_logging_level:
  73. # Check, if colored logs are switched on
  74. if log_coloring == True:
  75. # Geting the log color
  76. log_color = getattr(log_colors, logging_level)
  77. # Construct the logging-text incl. color
  78. log_text = str(log_color + "[" + logging_level + "] " + log_colors.endcode + message)
  79. else:
  80. # Construct the logging-text incl. color
  81. log_text = str("[" + logging_level + "] " + message)
  82. # Check if the warning should end with a new-line
  83. # Printing the text
  84. if new_line == True:
  85. print(log_text)
  86. else:
  87. print(log_text, end=" ", flush=True)
  88. # CALCULATE THE IRR
  89. def calculate_irr(date_now, date_open, value_now, value_open):
  90. error = False
  91. irr = 0.0
  92. try:
  93. # Count the number in days
  94. a = date_now - date_open
  95. a = a.days
  96. # Am Tag des Kaufs selbst, liegt das Delta in Tagen bei 0
  97. # Um dennoch einen IRR kalkulieren zu können, wird das Delta auf 1 gsetzt
  98. if a == 0:
  99. a = 1
  100. a = a / 365 # Umrechnung auf Jahresanteil, um auch den Jahreszinssatz zu bekommen
  101. b = value_now / value_open
  102. # Catch negative IRRs
  103. if b < 0:
  104. b = b * (-1)
  105. irr = b**(1/a) # matematisch identisch zur b-ten Wurzel von a
  106. irr = irr * (-1)
  107. else:
  108. irr = b**(1/a) # matematisch identisch zur b-ten Wurzel von a
  109. except:
  110. error = True
  111. # Return data if successful
  112. if error == True:
  113. print("[ERROR] Calculation of irr")
  114. return error
  115. else:
  116. return irr
  117. # GET THE DAY OF THE OLDEST TRADE
  118. def get_date_open_oldest_trade(trades):
  119. # Identify the open date for the oldest trade
  120. date_open_oldest_trade = datetime.date.today()
  121. for i in trades:
  122. if trades[i]["date_open"] < date_open_oldest_trade:
  123. date_open_oldest_trade = trades[i]["date_open"]
  124. return date_open_oldest_trade
  125. # CREATES LIST OF UNIQUE TICKERS
  126. def filter_list_of_tickers(trades):
  127. tickers = []
  128. try:
  129. for i in trades:
  130. # Fetch ticker belonging to trade
  131. ticker = trades[i]['ticker']
  132. # Add ticker to list, if not already present
  133. if ticker not in tickers:
  134. tickers.append(ticker)
  135. # Main Logging
  136. logging(logging_level="success")
  137. logging(logging_level="info", message=f"{len(tickers)} tickers found")
  138. return tickers
  139. except Exception as error_message:
  140. logging(logging_level="error")
  141. logging(logging_level="error", message=f"Failed with error: {error_message}")
  142. return False
  143. # CREATE LIST OF WEEKLY DATES
  144. def create_list_filtered_dates(trades, days_seperation):
  145. stop_date = get_date_open_oldest_trade(trades)
  146. index_date = datetime.date.today()
  147. try:
  148. # Create reversed list (1st entry is today going back in time)
  149. list_filtered_dates = []
  150. while index_date >= stop_date:
  151. list_filtered_dates.append(index_date.isoformat())
  152. index_date = index_date - datetime.timedelta(days=days_seperation)
  153. # Reverse the list, so that the frist entry is the oldest one
  154. list_filtered_dates.reverse()
  155. # Main Logging
  156. logging(logging_level="success")
  157. logging(logging_level="info", message=f"{len(list_filtered_dates)} dates in weekly list")
  158. return list_filtered_dates
  159. except Exception as error_message:
  160. logging(logging_level="error")
  161. logging(logging_level="error", message=f"Failed with error: {error_message}")
  162. return False
  163. # FETCH THE LAST INDEX FROM A DICT
  164. def fetch_last_key_from_dict(dict):
  165. key_list = list(dict.keys()) # Extract the keys and convert them to a list
  166. last_key = key_list[-1] # select the last entry from the list as it is the most current entry
  167. return last_key
  168. # ADD BENCHMARK-TICKER TO TICKER-DICT
  169. def add_benchmark_ticker(tickers, ticker_benchmarkt):
  170. tickers.append(ticker_benchmarkt)
  171. logging(logging_level="success")
  172. return tickers
  173. # CREATE BENCHMARK TRADES
  174. def create_benchmark_trades(trades, yf_data):
  175. # Prepertion
  176. benchmark_trades = {}
  177. i = 0
  178. # Creating benchmark trades
  179. try:
  180. for trade_id in trades:
  181. # Benchmark-id
  182. i = i+1
  183. benchmark_id = "benchmark" + str(i)
  184. # Copy raw trades
  185. benchmark_trades[benchmark_id] = trades[trade_id]
  186. benchmark_trades[benchmark_id]["ticker"] = ticker_benchmark
  187. # Calculate amount invested
  188. amount_invested = benchmark_trades[benchmark_id]["units"] * benchmark_trades[benchmark_id]["course_open"]
  189. # Change course-open for benchmark-ticker performance calculation
  190. success = False
  191. index_date = benchmark_trades[benchmark_id]["date_open"]
  192. while success == False:
  193. try:
  194. course_open_new = yf_data[ticker_benchmark].at[index_date, 'Close']
  195. success = True
  196. except:
  197. index_date = index_date + datetime.timedelta(days=1)
  198. benchmark_trades[benchmark_id]["course_open"] = course_open_new # type: ignore
  199. # Change amount for benchmark-ticker performance calculation
  200. benchmark_trades[benchmark_id]["units"] = amount_invested / course_open_new # type: ignore
  201. # Change course-open for benchmark-ticker performance calculation, if relevant
  202. if trades[trade_id]["date_close"] != 0:
  203. success = False
  204. index_date = benchmark_trades[benchmark_id]["date_close"]
  205. while success == False:
  206. try:
  207. course_close_new = yf_data[ticker_benchmark].at[index_date, 'Close']
  208. success = True
  209. except:
  210. index_date = index_date + datetime.timedelta(days=1)
  211. benchmark_trades[benchmark_id]["course_close"] = course_close_new # type: ignore
  212. # Logging
  213. logging(logging_level="success")
  214. return benchmark_trades
  215. except Exception as error_message:
  216. logging(logging_level="error")
  217. logging(logging_level="error", message=f"Failed with error: {error_message}")
  218. return False
  219. # MERGE BENCHMARK HISTORY & TICKER-HISTROY
  220. def merge_histories(history_per_ticker, benchmark_history):
  221. # Preperation
  222. benchmark_ticker = ticker_benchmark
  223. error_count = 0
  224. # Merging Data
  225. for index_date in history_per_ticker:
  226. try:
  227. history_per_ticker[index_date]["benchmark"] = {}
  228. history_per_ticker[index_date]["benchmark"]["current_invested"] = benchmark_history[index_date][benchmark_ticker]["current_invested"]
  229. history_per_ticker[index_date]["benchmark"]["total_dividends"] = benchmark_history[index_date][benchmark_ticker]["total_dividends"]
  230. history_per_ticker[index_date]["benchmark"]["current_value"] = benchmark_history[index_date][benchmark_ticker]["current_value"]
  231. history_per_ticker[index_date]["benchmark"]["current_irr"] = benchmark_history[index_date][benchmark_ticker]["current_irr"]
  232. history_per_ticker[index_date]["benchmark"]["total_performanance"] = benchmark_history[index_date][benchmark_ticker]["total_performanance"]
  233. except:
  234. error_count = error_count +1
  235. # Debugging
  236. if selected_logging_level == "debug":
  237. data = json.dumps(history_per_ticker, indent=2) # Converts a python-dictionary into a json
  238. with open("history_per_ticker_with_benchmark.json", "w") as f:
  239. f.write(data)
  240. # Main Logging
  241. if error_count == 0:
  242. logging(logging_level="success")
  243. return history_per_ticker
  244. else:
  245. logging(logging_level="warning")
  246. logging(logging_level="warning", message=f"Error merging benchmark-history into ticker-history in {error_count} cases")
  247. return False
  248. # -------------------------- #
  249. # NETWORK DOWNLOAD FUNCTIONS #
  250. # -------------------------- #
  251. # NOTION FETCH PAGES
  252. def notion_get_pages(db_id_trades, num_pages=None):
  253. try:
  254. # ------------------ FETCH THE FIRST 100 PAGES FROM A DB
  255. # Prepare Request
  256. url = f"https://api.notion.com/v1/databases/{db_id_trades}/query"
  257. get_all = num_pages is None # If num_pages is None, get all pages, otherwise just the defined number.
  258. page_size = 100 if get_all else num_pages
  259. payload = {"page_size": page_size}
  260. # Make Request
  261. raw_response = requests.post(url, json=payload, headers=notion_headers)
  262. # Process Reply
  263. parsed_response = raw_response.json()
  264. result = parsed_response["results"]
  265. # ------------------ FETCH 100 MORE PAGES AS OFTEN AS REQUIRED
  266. while parsed_response["has_more"] and get_all:
  267. # Prepare Request
  268. payload = {"page_size": page_size, "start_cursor": parsed_response["next_cursor"]}
  269. url = f"https://api.notion.com/v1/databases/{db_id_trades}/query"
  270. # Make Request
  271. raw_response = requests.post(url, json=payload, headers=notion_headers)
  272. # Process Reply
  273. parsed_response = raw_response.json()
  274. result.extend(parsed_response["results"])
  275. # Logging
  276. return result
  277. except Exception:
  278. return True # Return True when there was an error
  279. # NOTION FETCH & FORMAT TRADES
  280. def fetch_format_notion_trades(db_id_trades):
  281. trades = {}
  282. fetch_error = False
  283. format_errors = 0
  284. number_of_trades = 0
  285. error_message = ""
  286. # Download data from notion
  287. data = notion_get_pages(db_id_trades)
  288. # Check, if cuccessfull
  289. if data is True:
  290. fetch_error = True
  291. else:
  292. # Format the recieved data
  293. for i in data:
  294. # Count for stratistics
  295. number_of_trades = number_of_trades + 1
  296. # Each page is loaded as a dictionary
  297. notion_page = dict(i)
  298. # Handling desired missing entries
  299. try:
  300. date_close = notion_page["properties"]["Close"]["date"]
  301. date_close = date_close["start"]
  302. date_close = datetime.date(*map(int, date_close.split('-')))
  303. except:
  304. date_close = 0
  305. # Handeling non-desired missing entries (by skipping this trade)
  306. try:
  307. # Try extracting values
  308. trade = {}
  309. # Format date-open
  310. date_open = notion_page["properties"]["Open"]["date"]
  311. date_open = date_open["start"]
  312. date_open = datetime.date(*map(int, date_open.split('-')))
  313. # Combine data into json structure
  314. trade = {
  315. 'ticker' : notion_page["properties"]["Ticker"]["select"]["name"],
  316. 'date_open' : date_open,
  317. 'date_close' : date_close,
  318. 'course_open' : notion_page["properties"]["Open (€)"]["number"],
  319. 'course_close' : notion_page["properties"]["Close (€)"]["number"],
  320. 'course_current' : notion_page["properties"]["Current (€)"]["number"],
  321. 'irr' : notion_page["properties"]["IRR (%)"]["number"],
  322. 'units' : notion_page["properties"]["Units"]["number"],
  323. 'dividends' : notion_page["properties"]["Dividends (€)"]["number"]
  324. }
  325. # Save values
  326. notion_page_id = notion_page["id"] # Use as key for the dictionary
  327. trades[notion_page_id] = trade
  328. except Exception as e:
  329. format_errors = format_errors + 1
  330. error_message = e
  331. # Logging
  332. if fetch_error == True:
  333. logging(logging_level="error")
  334. logging(logging_level="error", message=f"Failed with error: {error_message}")
  335. return False
  336. else:
  337. # Writing Example File
  338. if selected_logging_level == "debug":
  339. with open("trades.json", "w") as f:
  340. f.write(str(trades))
  341. # Logging
  342. if format_errors == 0:
  343. logging(logging_level="success")
  344. logging(logging_level="info", message=f"{number_of_trades} trades recieved and formated")
  345. return trades
  346. else:
  347. logging(logging_level="warning")
  348. logging(logging_level="warning", message=f"{format_errors} trades out of {number_of_trades} skiped...maybe due to missing values?")
  349. return trades
  350. # NOTION FETCH & FORMAT INVESTMENT OVERVIEW
  351. def fetch_format_notion_investments(db_id_investments):
  352. investments = {}
  353. fetch_error = False
  354. format_errors = 0
  355. number_of_investments = 0
  356. # Download data & check for success
  357. data = notion_get_pages(db_id_investments)
  358. if data is True:
  359. error = True
  360. else:
  361. # Format recieved data
  362. for i in data:
  363. # Count up for statistics
  364. number_of_investments = number_of_investments + 1
  365. try:
  366. # Each page is loaded as a dictionary
  367. notion_page = dict(i)
  368. # Extract values
  369. notion_page_id = notion_page["id"] # Use as key for the dictionary
  370. investments[notion_page_id] = {}
  371. investments[notion_page_id]["ticker"] = notion_page["properties"]["Ticker"]["select"]["name"]
  372. investments[notion_page_id]["total_dividends"] = notion_page["properties"]["Dividends (€)"]["number"]
  373. investments[notion_page_id]["current_value"] = notion_page["properties"]["Current (€)"]["number"]
  374. investments[notion_page_id]["current_irr"] = notion_page["properties"]["IRR (%)"]["number"]
  375. investments[notion_page_id]["total_performanance"] = notion_page["properties"]["Performance (€)"]["number"]
  376. # Skip this entry, if errors show up
  377. except:
  378. format_errors = format_errors + 1
  379. # Main Logging
  380. if fetch_error == False & format_errors == 0:
  381. logging(logging_level="success")
  382. logging(logging_level="info", message=f"{number_of_investments} investments recieved and formated")
  383. return investments
  384. elif fetch_error == False & format_errors > 0:
  385. logging(logging_level="warning")
  386. logging(logging_level="warning", message=f"{format_errors} investments out of {number_of_investments} skiped...maybe due to missing values?")
  387. return investments
  388. else:
  389. logging(logging_level="error")
  390. return False
  391. # YFINANCE FETCH & FORMAT DATA
  392. def fetch_format_yf_data(tickers):
  393. yf_data = {}
  394. fetch_errors = 0
  395. format_errors = 0
  396. number_of_tickers = 0
  397. # Download data for each ticker seperately
  398. for i in tickers:
  399. number_of_tickers = number_of_tickers +1
  400. skip_formating = False # Helper varianbel (see flow logik)
  401. ticker = i
  402. # Catch errors during the download
  403. try:
  404. # Download data
  405. api = yf.Ticker(ticker)
  406. data = api.history(period="max", auto_adjust=False)
  407. except:
  408. # Store error for later logging
  409. fetch_errors = fetch_errors + 1
  410. data = True
  411. # If the download was successfull:
  412. if skip_formating == False:
  413. # Try formating the data
  414. try:
  415. # Convert to Pandas DataFrame
  416. data = pd.DataFrame(data) # type: ignore
  417. # Delete the columns "Stock Splits", "High", "Low" and "Open"
  418. del data['Open']
  419. del data['Low']
  420. del data['High']
  421. del data['Volume']
  422. # Delete these 2 columns, if they exist
  423. if 'Stock Splits' in data.columns:
  424. del data['Stock Splits']
  425. if 'Capital Gains' in data.columns:
  426. del data['Capital Gains']
  427. # Get the Number of rows in data
  428. data_rows = data.shape[0]
  429. # Create new index without the time from the existing datetime64-index
  430. old_index = data.index
  431. new_index = []
  432. x = 0
  433. while x < data_rows:
  434. date = pd.Timestamp.date(old_index[x]) # Converts the "Pandas Timestamp"-object to a "date" object
  435. new_index.append(date)
  436. x+=1
  437. # Add the new index to the dataframe and set it as the index
  438. data.insert(1, 'Date', new_index)
  439. data.set_index('Date', inplace=True)
  440. # Save the data-frame to the yf_data dict
  441. yf_data[ticker] = data
  442. # Handle formating errors
  443. except:
  444. format_errors = format_errors +1
  445. # in case of an error the entry never get's added to the yf_data object
  446. # Wait for the API to cool down
  447. print(".", end="", flush=True)
  448. time.sleep(api_cooldowm_time)
  449. # Main Logging
  450. print(" ", end="", flush=True)
  451. if fetch_errors == 0 & format_errors == 0:
  452. logging(logging_level="success")
  453. logging(logging_level="info", message=f"{number_of_tickers} tickers recieved and formated")
  454. return yf_data
  455. elif fetch_errors == 0 & format_errors > 0:
  456. logging(logging_level="warning")
  457. logging(logging_level="warning", message=f"{format_errors} tickers out of {number_of_tickers} skiped")
  458. return yf_data
  459. else:
  460. logging(logging_level="error")
  461. logging(logging_level="error", message=f"Failed with error: {number_of_tickers}")
  462. print("\n")
  463. return False
  464. # ------------------------ #
  465. # NETWORK UPLOAD FUNCTIONS #
  466. # ------------------------ #
  467. # NOTION UPDATE PAGES
  468. def notion_update_page(page_id: str, data: dict):
  469. url = f"https://api.notion.com/v1/pages/{page_id}"
  470. payload = {"properties": data}
  471. results = requests.patch(url, json=payload, headers=notion_headers)
  472. return results
  473. # UPDATE NOTION-TRADES-DATABASE
  474. def push_notion_trades_update(trades):
  475. # Logging
  476. error_count = 0
  477. number_of_uploads = 0
  478. for notion_page_id in trades:
  479. number_of_uploads = number_of_uploads+1
  480. try:
  481. # The irr is stored in the format 1.2534
  482. # Notion need the format 0,2534
  483. irr_notion = trades[notion_page_id]['irr'] - 1
  484. irr_notion = round(irr_notion, 4)
  485. # Construct Notion-Update-Object
  486. notion_update = {
  487. "Current (€)": {
  488. "number": trades[notion_page_id]['course_current']
  489. },
  490. "IRR (%)": {
  491. "number": irr_notion
  492. },
  493. "Dividends (€)": {
  494. "number": trades[notion_page_id]['dividends']
  495. }
  496. }
  497. # Update the properties of the corresponding notion-page
  498. notion_update_page(notion_page_id, notion_update)
  499. except:
  500. error_count = error_count + 1
  501. # Wait for the API to cool off
  502. print(".", end="", flush=True)
  503. time.sleep(api_cooldowm_time)
  504. # Logging
  505. print(" ", end="", flush=True)
  506. if error_count == 0:
  507. logging(logging_level="success")
  508. elif error_count < number_of_uploads:
  509. logging(logging_level="warning")
  510. logging(logging_level="success", message=f"Updating notion trades failed for {error_count} out of {number_of_uploads} entries")
  511. else:
  512. logging(logging_level="error")
  513. logging(logging_level="success", message=f"Updating notion trades failed for all {error_count} entries")
  514. # UPDATE NOTION-INVESTMENT-OVERVIEW
  515. def push_notion_investment_update(investments):
  516. # Logging
  517. error_count = 0
  518. number_of_uploads = 0
  519. for notion_page_id in investments:
  520. number_of_uploads = number_of_uploads+1
  521. # Try uploading an update
  522. try:
  523. # The irr is stored in the format 1.2534
  524. # Notion need the format 0,2534
  525. irr_notion = investments[notion_page_id]['current_irr'] - 1
  526. irr_notion = round(irr_notion, 4)
  527. # Construct Notion-Update-Object
  528. notion_update = {
  529. "Current (€)": {
  530. "number": investments[notion_page_id]['current_value']
  531. },
  532. "IRR (%)": {
  533. "number": irr_notion
  534. },
  535. "Performance (€)": {
  536. "number": investments[notion_page_id]['total_performanance']
  537. },
  538. "Dividends (€)": {
  539. "number": investments[notion_page_id]['total_dividends']
  540. }
  541. }
  542. # Update the properties of the corresponding notion-page
  543. notion_update_page(notion_page_id, notion_update)
  544. except:
  545. error_count = error_count + 1
  546. # Wait for the API to cool off
  547. print(".", end="", flush=True)
  548. time.sleep(api_cooldowm_time)
  549. # Logging
  550. print(" ", end="", flush=True)
  551. if error_count == 0:
  552. logging(logging_level="success")
  553. elif error_count < number_of_uploads:
  554. logging(logging_level="warning")
  555. logging(logging_level="success", message=f"Updating notion investments failed for {error_count} out of {number_of_uploads} entries")
  556. else:
  557. logging(logging_level="error")
  558. logging(logging_level="success", message=f"Updating notion investments failed for all {error_count} entries")
  559. # TRMNL UPDATE DIAGRAMMS
  560. def push_trmnl_update_chart(trmnl_update_object, trmnl_url, trmnl_headers):
  561. # Send the data to TRMNL
  562. try:
  563. data = json.dumps(trmnl_update_object, indent=2) # Converts a python-dictionary into a json
  564. reply = requests.post(trmnl_url, data=data, headers=trmnl_headers)
  565. # Logging
  566. if reply.status_code == 200:
  567. logging(logging_level="success")
  568. elif reply.status_code == 429:
  569. logging_level="success"
  570. logging(logging_level="warning")
  571. logging(logging_level="warning", message="Exceeded TRMNL's API rate limits")
  572. logging(logging_level="warning", message="Waiting some time should work")
  573. elif reply.status_code == 422:
  574. logging(logging_level="warning")
  575. logging(logging_level="warning", message="Upload successful, but data cannot be displayed correctly")
  576. logging(logging_level="warning", message="The payload is probably to large in size")
  577. else:
  578. logging(logging_level="error")
  579. logging(logging_level="error", message=f"Failed pushing data to TRMNL with server reply code: {reply.status_code}")
  580. logging(logging_level="debug", message=f"Complete server reply message: {reply}")
  581. except Exception as e:
  582. logging(logging_level="error")
  583. logging(logging_level="error", message=f"Failed pushing data to TRMNL with error code: {e}")
  584. # ----------------------------- #
  585. # HISTORY CALCULATION FUNCTIONS #
  586. # ----------------------------- #
  587. # CALC HISTORY PER TRADE
  588. def calc_history_per_trade(trades, yf_data):
  589. # Create support variables
  590. history_per_trade = {}
  591. total_dividends = 0
  592. date_open_oldest_trade = get_date_open_oldest_trade(trades)
  593. # Logging & statistics
  594. missing_day_entrys = 0
  595. days_formated = 0
  596. number_of_trades = 0
  597. # As this history is so important, it is okay if this functions fails in total if errors araise
  598. try:
  599. # ------------------ LOOP OVER ALL TRADES
  600. for trade_id in trades:
  601. # Statistics
  602. number_of_trades = number_of_trades +1
  603. # ------------------ PREPARE FOR THE (NEXT) LOOP OVER ALL DAYS
  604. # Set / Reset the index-date to the oldest trade day
  605. # Resetting is required so that the calculations for the next trade start with day 1
  606. index_date = date_open_oldest_trade
  607. # Set the initial value for the course on the previous day to 0
  608. # Just in case the very first trade was made on a weekend somehow, where there is no yfinance data available
  609. previous_course = 0.0
  610. # Check, if the trade was closed already
  611. # If it was not, set the closure date to the future (Trick 17)
  612. if trades[trade_id]["date_close"] == 0:
  613. date_close = datetime.date.today() + datetime.timedelta(days=1)
  614. else:
  615. date_close = trades[trade_id]["date_close"]
  616. date_open = trades[trade_id]["date_open"]
  617. # Keep ticker for connecting performance later
  618. ticker = trades[trade_id]['ticker']
  619. # ------------------ DETERMINE THE COUSE PER DAY
  620. while index_date != datetime.date.today() + datetime.timedelta(days=1):
  621. # Statistics
  622. days_formated = days_formated +1
  623. # Fetch course for the day & eventual dividends from yf_data
  624. try:
  625. current_course = yf_data[ticker].at[index_date, 'Close']
  626. current_dividends_per_ticker = yf_data[ticker].at[index_date, 'Dividends']
  627. # Catch missing yf-data (eg. for weekends) by reusing course from previous day
  628. except:
  629. current_course = previous_course
  630. current_dividends_per_ticker = 0.0 # there are never dividends on non-trading days
  631. missing_day_entrys = missing_day_entrys +1 # Increase the warning count
  632. # Catch the special case of the day when the trade was closed
  633. # In this case, the current course needs to be overwritten with the sell-value
  634. if date_close == index_date:
  635. current_course = trades[trade_id]['course_close']
  636. # Save the result for the next iteration
  637. # This setup also makes it possible, that a previous course is passed down across mutiple days
  638. # This makes sense is case i.e. for a weekend
  639. previous_course = current_course
  640. # ------------------ CALCULATE PERFORMANCE IF REQUIRED
  641. if index_date >= date_open and index_date <= date_close:
  642. # Calculate performance values
  643. current_amount = trades[trade_id]['units']
  644. current_invested = current_amount * trades[trade_id]['course_open']
  645. total_dividends = total_dividends + current_amount * current_dividends_per_ticker
  646. current_value = current_amount * current_course
  647. current_value_with_dividends = current_value + total_dividends
  648. current_irr = calculate_irr(index_date, date_open, current_value_with_dividends, current_invested)
  649. total_performanance = current_value_with_dividends - current_invested
  650. if current_value_with_dividends == 0:
  651. print("0-value Error with ticker: {}".format(ticker))
  652. else:
  653. # Write 0, if trade is not relevant for current timeframe
  654. current_amount = 0
  655. current_invested = 0.00
  656. total_dividends = 0.00
  657. current_value = 0.00
  658. current_irr = 0.00
  659. total_performanance = 0.0
  660. # ------------------ STORE RESULTS
  661. index_date_iso = index_date.isoformat()
  662. # Store all values into a dict
  663. dict_a = {}
  664. dict_a['current_amount'] = current_amount
  665. dict_a['current_invested'] = current_invested
  666. dict_a['total_dividends'] = total_dividends
  667. dict_a['current_value'] = current_value
  668. dict_a['current_irr'] = current_irr
  669. dict_a['current_course'] = current_course
  670. dict_a['total_performanance'] = total_performanance
  671. # Check if the date is already present
  672. if index_date_iso in history_per_trade:
  673. dict_b = history_per_trade[index_date_iso]
  674. else:
  675. dict_b = {}
  676. # Add the values to the trade_id value-pair
  677. dict_b[trade_id] = dict_a
  678. # Update the hostory_per_trade
  679. history_per_trade.update({index_date_iso : dict_b})
  680. # ------------------ NEXT ITERATION
  681. index_date = index_date + datetime.timedelta(days=1)
  682. # ------------------ LOGGING & DEBUGING
  683. # Debug writing history to disk
  684. if selected_logging_level == "debug":
  685. data = json.dumps(history_per_trade, indent=2) # Converts a python-dictionary into a json
  686. with open("history_per_trade.json", "w") as f:
  687. f.write(data)
  688. # Logging logging
  689. if missing_day_entrys == 0:
  690. logging(logging_level="success")
  691. logging(logging_level="info", message=f"created a history with {days_formated} across all {number_of_trades} tickers o_O")
  692. else:
  693. logging(logging_level="warning")
  694. logging(logging_level="warning", message=f"No yf-data available in {missing_day_entrys} cases accross all {number_of_trades} tickers")
  695. logging(logging_level="warning", message="Probably reason is non-trading-days eg. weekends")
  696. logging(logging_level="warning", message="Used values from previous trade-day instead")
  697. # Return date
  698. return history_per_trade
  699. except Exception as error_message:
  700. logging(logging_level="error")
  701. logging(logging_level="error", message=f"Failed with error message: {error_message}")
  702. return False
  703. # CALC THE HISTORY PER TRADE & OVERALL
  704. def calc_history_per_ticker(history_per_trade, tickers, trades):
  705. # ------------------ CREATE JSON OBJECT
  706. # Create the json-dict
  707. history_per_ticker = {}
  708. # Logging & statistics
  709. missing_day_entrys = 0
  710. days_formated = 0
  711. # As this history is so important, it is okay if this functions fails in total if errors araise
  712. try:
  713. # Loop over each date entry in the history
  714. for date_entry in history_per_trade:
  715. # Statistics
  716. days_formated = days_formated +1
  717. # Create a dict to store the results per day and ticker
  718. dict_daily = {}
  719. for ticker in tickers:
  720. dict_daily[ticker] = {}
  721. dict_daily[ticker]["current_invested"] = 0
  722. dict_daily[ticker]["total_dividends"] = 0
  723. dict_daily[ticker]["current_value"] = 0
  724. dict_daily[ticker]["current_irr"] = 0
  725. dict_daily[ticker]["current_irr"] = 0
  726. dict_daily[ticker]["total_performanance"] = 0
  727. dict_daily[ticker]["current_amount"] = 0 # Added only for ticker entries, not for the "total" value
  728. dict_daily[ticker]["current_course"] = 0 # Added only for ticker entries, not for the "total" value
  729. dict_daily["total"] = {}
  730. dict_daily["total"]["current_invested"] = 0
  731. dict_daily["total"]["total_dividends"] = 0
  732. dict_daily["total"]["current_value"] = 0
  733. dict_daily["total"]["current_irr"] = 0
  734. dict_daily["total"]["total_performanance"] = 0
  735. # Loop over each trade-entry for that day
  736. for trade_id in history_per_trade[date_entry]:
  737. # Extract data from the history_per_trade
  738. trade_amount = history_per_trade[date_entry][trade_id]['current_amount']
  739. trade_invested = history_per_trade[date_entry][trade_id]['current_invested']
  740. trade_dividends = history_per_trade[date_entry][trade_id]['total_dividends']
  741. trade_value = history_per_trade[date_entry][trade_id]['current_value']
  742. trade_irr = history_per_trade[date_entry][trade_id]['current_irr']
  743. trade_course = history_per_trade[date_entry][trade_id]['current_course']
  744. trade_performanance = history_per_trade[date_entry][trade_id]['total_performanance']
  745. # Lookup the ticker by the trade-id
  746. ticker = trades[trade_id]["ticker"]
  747. # Extract data from the history_per_ticker
  748. ticker_amount = dict_daily[ticker]['current_amount']
  749. ticker_invested = dict_daily[ticker]['current_invested']
  750. ticker_dividends = dict_daily[ticker]['total_dividends']
  751. ticker_value = dict_daily[ticker]['current_value']
  752. ticker_irr = dict_daily[ticker]['current_irr']
  753. ticker_performanance = dict_daily[ticker]['total_performanance']
  754. # Overwrite the values in the history_per_ticker
  755. dict_daily[ticker]['current_amount'] = ticker_amount + trade_amount # Simple addition works
  756. dict_daily[ticker]['current_invested'] = ticker_invested + trade_invested
  757. dict_daily[ticker]['total_dividends'] = ticker_dividends + trade_dividends
  758. dict_daily[ticker]['current_value'] = ticker_value + trade_value
  759. dict_daily[ticker]['total_performanance'] = ticker_performanance + trade_performanance
  760. dict_daily[ticker]['current_course'] = trade_course # Simple overwrite is fine, as the course is the same for all trades
  761. if ticker_invested == 0 and trade_invested == 0:
  762. dict_daily[ticker]['current_irr'] = 0
  763. # Catch 0 values
  764. else:
  765. dict_daily[ticker]['current_irr'] = (ticker_irr * ticker_invested + trade_irr * trade_invested) / (ticker_invested + trade_invested)
  766. # --> 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
  767. # Calculate the "total" entry after finishing with all the trades
  768. for ticker in tickers:
  769. # Same logic as above, but shortended code
  770. dict_daily["total"]['total_dividends'] = dict_daily["total"]['total_dividends'] + dict_daily[ticker]['total_dividends']
  771. dict_daily["total"]['current_value'] = dict_daily["total"]['current_value'] + dict_daily[ticker]['current_value']
  772. dict_daily["total"]['total_performanance'] = dict_daily["total"]['total_performanance'] + dict_daily[ticker]['total_performanance']
  773. # Extracting the values before rewriting them, to preserve them for the IRR calculation
  774. total_invested = dict_daily["total"]['current_invested']
  775. ticker_invested = dict_daily[ticker]['current_invested']
  776. dict_daily["total"]['current_invested'] = total_invested + ticker_invested
  777. # Extracting the values before rewriting them, to preserve them for the IRR calculation
  778. if ticker_invested == 0 and total_invested == 0:
  779. dict_daily["total"]['current_irr'] = 0
  780. else:
  781. total_irr = dict_daily["total"]['current_irr']
  782. ticker_irr = dict_daily[ticker]['current_irr']
  783. dict_daily["total"]['current_irr'] = (total_irr * total_invested + ticker_irr * ticker_invested) / (total_invested + ticker_invested)
  784. # Finally, write the results for this day-entry to the history_per_ticker
  785. history_per_ticker[date_entry] = dict_daily
  786. # ------------------ LOGGING & DEBUGING
  787. # Debugging
  788. if selected_logging_level == "debug":
  789. data = json.dumps(history_per_ticker, indent=2) # Converts a python-dictionary into a json
  790. with open("history_per_ticker.json", "w") as f:
  791. f.write(data)
  792. # Success Logging
  793. logging(logging_level="success")
  794. logging(logging_level="info", message=f"created a history with {days_formated} days formated o_O")
  795. return history_per_ticker
  796. # Error Logging
  797. except Exception as error_message:
  798. logging(logging_level="error")
  799. logging(logging_level="error", message=f"Failed with error message: {error_message}")
  800. return False
  801. # --------------------------- #
  802. # HISTORY SELECTION FUNCTIONS #
  803. # --------------------------- #
  804. # FILTER ANY HISTORY OBJECT TO SELECTED DATES (With Weighted Averaging & Outlier Removal)
  805. def filter_history_by_list(history, dates_list):
  806. filtered_history = {}
  807. try:
  808. # Sort inputs to ensure chronologically correct processing
  809. sorted_target_dates = sorted(dates_list)
  810. all_history_dates = sorted(history.keys())
  811. last_history_idx = 0
  812. for i, target_date in enumerate(sorted_target_dates):
  813. # Check if this is the last date (Today)
  814. is_last_date = (i == len(sorted_target_dates) - 1)
  815. if is_last_date:
  816. # --- LOGIC FOR TODAY (Last Entry) ---
  817. # Do NOT average. Grab the specific data for this date.
  818. # Try to find exact match
  819. data_to_use = history.get(target_date)
  820. # If not found (e.g. running on weekend/holiday), use the very last entry available
  821. if not data_to_use and len(all_history_dates) > 0:
  822. last_available_date = all_history_dates[-1]
  823. data_to_use = history.get(last_available_date)
  824. if data_to_use:
  825. # Deep copy using JSON to avoid reference issues
  826. filtered_history[target_date] = json.loads(json.dumps(data_to_use))
  827. else:
  828. # --- LOGIC FOR PAST DATES (Weighted Interval Averaging) ---
  829. current_history_idx = -1
  830. # Find the index of the target date (or the closest date BEFORE it)
  831. for j in range(last_history_idx, len(all_history_dates)):
  832. h_date = all_history_dates[j]
  833. if h_date <= target_date:
  834. current_history_idx = j
  835. else:
  836. break # Optimization: stop searching once we pass the date
  837. if current_history_idx != -1 and current_history_idx >= last_history_idx:
  838. # Get interval dates (Python slice is exclusive at the end, so +1)
  839. interval_dates = all_history_dates[last_history_idx : current_history_idx + 1]
  840. if len(interval_dates) > 0:
  841. # Initialize Aggregation Dictionary
  842. aggregation = {}
  843. # Helper to init ticker if missing
  844. def get_agg(t):
  845. if t not in aggregation:
  846. aggregation[t] = {
  847. 'wSumInvested': 0.0, 'wSumValue': 0.0, 'wSumPerf': 0.0, 'wSumDiv': 0.0,
  848. 'totalWeight': 0.0,
  849. 'wSumIrr': 0.0, 'irrTotalWeight': 0.0,
  850. 'bSumInvested': 0.0, 'bSumValue': 0.0, 'bSumPerf': 0.0, 'bSumDiv': 0.0,
  851. 'bTotalWeight': 0.0,
  852. 'bSumIrr': 0.0, 'bIrrTotalWeight': 0.0
  853. }
  854. return aggregation[t]
  855. for date_key in interval_dates:
  856. day_data = history.get(date_key)
  857. if not day_data: continue
  858. for ticker, entry in day_data.items():
  859. agg = get_agg(ticker)
  860. # Access properties safely
  861. current_invested = entry.get('current_invested', 0)
  862. current_value = entry.get('current_value', 0)
  863. current_irr = entry.get('current_irr', 0)
  864. # Note: Keeping original typo 'performanance' to match your Notion keys
  865. total_performance = entry.get('total_performanance', 0)
  866. total_dividends = entry.get('total_dividends', 0)
  867. # Weighting: If invested < 1, treat as 0 to avoid dust-multiplication
  868. w = current_invested if current_invested > 1 else 0
  869. # Outlier Detection: Ignore IRRs > 500% (5.0)
  870. is_outlier = abs(current_irr) > 5.0
  871. if w > 0:
  872. agg['wSumInvested'] += current_invested * w
  873. agg['wSumValue'] += current_value * w
  874. agg['wSumPerf'] += total_performance * w
  875. agg['wSumDiv'] += total_dividends * w
  876. agg['totalWeight'] += w
  877. # Only add IRR if sane
  878. if not is_outlier:
  879. agg['wSumIrr'] += current_irr * w
  880. agg['irrTotalWeight'] += w
  881. # Benchmark Logic
  882. benchmark_entry = entry.get('benchmark')
  883. if benchmark_entry:
  884. b_invested = benchmark_entry.get('current_invested', 0)
  885. b_value = benchmark_entry.get('current_value', 0)
  886. b_irr = benchmark_entry.get('current_irr', 0)
  887. b_perf = benchmark_entry.get('total_performanance', 0)
  888. b_div = benchmark_entry.get('total_dividends', 0)
  889. bw = b_invested if b_invested > 1 else 0
  890. is_bench_outlier = abs(b_irr) > 5.0
  891. if bw > 0:
  892. agg['bSumInvested'] += b_invested * bw
  893. agg['bSumValue'] += b_value * bw
  894. agg['bSumPerf'] += b_perf * bw
  895. agg['bSumDiv'] += b_div * bw
  896. agg['bTotalWeight'] += bw
  897. if not is_bench_outlier:
  898. agg['bSumIrr'] += b_irr * bw
  899. agg['bIrrTotalWeight'] += bw
  900. # Construct Result for this Date
  901. result_daily = {}
  902. for ticker, agg in aggregation.items():
  903. def safe_div(n, d):
  904. return n / d if d != 0 else 0
  905. entry_result = {
  906. 'current_invested': safe_div(agg['wSumInvested'], agg['totalWeight']),
  907. 'current_value': safe_div(agg['wSumValue'], agg['totalWeight']),
  908. 'current_irr': safe_div(agg['wSumIrr'], agg['irrTotalWeight']),
  909. 'total_performanance': safe_div(agg['wSumPerf'], agg['totalWeight']),
  910. 'total_dividends': safe_div(agg['wSumDiv'], agg['totalWeight']),
  911. 'current_amount': 0,
  912. 'current_course': 0
  913. }
  914. if agg['bTotalWeight'] > 0:
  915. entry_result['benchmark'] = {
  916. 'current_invested': safe_div(agg['bSumInvested'], agg['bTotalWeight']),
  917. 'current_value': safe_div(agg['bSumValue'], agg['bTotalWeight']),
  918. 'current_irr': safe_div(agg['bSumIrr'], agg['bIrrTotalWeight']),
  919. 'total_performanance': safe_div(agg['bSumPerf'], agg['bTotalWeight']),
  920. 'total_dividends': safe_div(agg['bSumDiv'], agg['bTotalWeight']),
  921. }
  922. result_daily[ticker] = entry_result
  923. filtered_history[target_date] = result_daily
  924. # Update start index for next iteration
  925. last_history_idx = current_history_idx + 1
  926. # Main Logging
  927. logging(logging_level="success")
  928. return filtered_history
  929. except Exception as error_message:
  930. logging(logging_level="error")
  931. logging(logging_level="error", message=f"Failed with error: {error_message}")
  932. return False
  933. # SELECT CURRENT VALUES PER TRADE
  934. def select_current_value_per_trade(trades, history_per_trade):
  935. # Logging
  936. format_errors = 0
  937. # Loop over all trades
  938. for trade_id in trades:
  939. try:
  940. # Determine, what values to fetch based on whether the trade was closed already
  941. date_closed = trades[trade_id]["date_close"]
  942. if date_closed == 0:
  943. # If trade still open, use performance data from today
  944. index_date_iso = datetime.date.today().isoformat()
  945. else:
  946. # If trade closed, use performance data from close-date
  947. index_date_iso = date_closed.isoformat()
  948. # Fetch data from history and save for this trade
  949. trades[trade_id]["course_current"] = history_per_trade[index_date_iso][trade_id]['current_course']
  950. trades[trade_id]["irr"] = history_per_trade[index_date_iso][trade_id]['current_irr']
  951. trades[trade_id]["dividends"] = history_per_trade[index_date_iso][trade_id]['total_dividends']
  952. except:
  953. format_errors = format_errors + 1
  954. # Logging logging
  955. if format_errors == 0:
  956. logging(logging_level="success")
  957. else:
  958. logging(logging_level="warning")
  959. logging(logging_level="warning", message=f"Failed updating the current value per trade in {format_errors} cases")
  960. return trades
  961. # SELECT CURRENT VALUES PER TICKER
  962. def select_current_value_per_ticker(investments, history_per_ticker):
  963. # Logging
  964. format_errors = 0
  965. # Loop over all investments
  966. for investment_id in investments:
  967. try:
  968. # Generate the iso-date of today as the required index
  969. index_date_iso = datetime.date.today().isoformat()
  970. # Get the ticker corresponding to the investment
  971. ticker = investments[investment_id]["ticker"]
  972. # Select latest data from history and save for this investment
  973. investments[investment_id]["total_dividends"] = history_per_ticker[index_date_iso][ticker]['total_dividends']
  974. investments[investment_id]["current_value"] = history_per_ticker[index_date_iso][ticker]['current_value']
  975. investments[investment_id]["current_irr"] = history_per_ticker[index_date_iso][ticker]['current_irr']
  976. investments[investment_id]["total_performanance"] = history_per_ticker[index_date_iso][ticker]['total_performanance']
  977. except:
  978. format_errors = format_errors + 1
  979. # Logging
  980. if format_errors == 0:
  981. logging(logging_level="success")
  982. else:
  983. logging(logging_level="warning")
  984. logging(logging_level="warning", message=f"Failed updating the current value per ticker in {format_errors} cases")
  985. return investments
  986. # TRMNL CREATE IRR-UPDATE
  987. def prep_trmnl_chart_udpate(history_to_show, series_to_show_1 = "total", data_to_show_1 = "current_value", series_to_show_2 = "bechnmark", data_to_show_2 = "current_value"): # default value = current invested
  988. # Setup
  989. dict_big_numbers = {}
  990. charts_data = []
  991. chart_1 = {}
  992. chart_2 = {}
  993. try:
  994. # Fetch the latest date entry from the history
  995. index_date_iso = fetch_last_key_from_dict(history_to_show)
  996. # Select latest data from history for the big-numbers
  997. current_value = history_to_show[index_date_iso]["total"]["current_value"]
  998. total_performanance = history_to_show[index_date_iso]["total"]["total_performanance"]
  999. current_irr = history_to_show[index_date_iso]["total"]["current_irr"]
  1000. current_irr = (current_irr -1) *100
  1001. # Round the nubers
  1002. dict_big_numbers["current_value"] = str(round(current_value, 0))
  1003. dict_big_numbers["total_performanance"] = str(round(total_performanance, 0))
  1004. dict_big_numbers["current_irr"] = str(round(current_irr, 2))
  1005. # Catching false inputs for the series to show
  1006. possible_series_to_show = list(history_to_show[index_date_iso].keys()) # Get a list of all the series values, that could be shown
  1007. if series_to_show_1 not in possible_series_to_show: # checks, if the selected series is not part of the history-object sent to the function
  1008. logging(logging_level="warning")
  1009. logging(logging_level="warning", message="Selecting 'total' as the series to show, as the input was not valid")
  1010. series_to_show_1 = "total"
  1011. if series_to_show_2 not in possible_series_to_show: # checks, if the selected series is not part of the history-object sent to the function
  1012. logging(logging_level="warning")
  1013. logging(logging_level="warning", message="Selecting 'total' as the series to show, as the input was not valid")
  1014. series_to_show_2 = "total"
  1015. # Catching false inputs for the data to show
  1016. possible_data_to_show = list(history_to_show[index_date_iso][series_to_show_1].keys())
  1017. if data_to_show_1 not in possible_data_to_show:
  1018. logging(logging_level="warning")
  1019. logging(logging_level="warning", message="Selecting 'current invested' as chart data, as the input was not valid")
  1020. data_to_show_1 = "current_value"
  1021. possible_data_to_show = list(history_to_show[index_date_iso][series_to_show_2].keys())
  1022. if data_to_show_2 not in possible_data_to_show:
  1023. logging(logging_level="warning")
  1024. logging(logging_level="warning", message="Selecting 'current invested' as chart data, as the input was not valid")
  1025. data_to_show_2 = "current_value"
  1026. # Create space for storing values
  1027. chart_1["data"] = []
  1028. chart_2["data"] = []
  1029. # Format the chart data into the right data
  1030. for date in history_to_show:
  1031. # Extract the value to be stored
  1032. value_to_show_1 = history_to_show[date][series_to_show_1][data_to_show_1]
  1033. value_to_show_2 = history_to_show[date][series_to_show_2][data_to_show_2]
  1034. # Catch the case irr and convert to percent
  1035. if data_to_show_1 == "current_irr":
  1036. value_to_show_1 = (value_to_show_1 -1) *100
  1037. if data_to_show_2 == "current_irr":
  1038. value_to_show_2 = (value_to_show_2 -1) *100
  1039. # Round to 2 decimal values
  1040. value_to_show_1 = round(value_to_show_1, 2)
  1041. value_to_show_2 = round(value_to_show_2, 2)
  1042. # Extend the date by a timestamp
  1043. json_date = datetime.date.fromisoformat(date) # Convert ISO-String to python date-object
  1044. json_date = datetime.datetime.combine(json_date, datetime.datetime.min.time()) # Combine the date with midnight (00:00:00) to create a datetime object
  1045. json_date = json_date.isoformat() # Convert back to ISO-String, now including a time
  1046. # Store the values together with the corresponding date
  1047. value_1 = [json_date, value_to_show_1]
  1048. value_2 = [json_date, value_to_show_2]
  1049. # Add the value pair to the list of values for this chart
  1050. chart_1["data"].append(value_1)
  1051. chart_2["data"].append(value_2)
  1052. # Add the two series to the list of series in the TRML object
  1053. charts_data.append(chart_1)
  1054. charts_data.append(chart_2)
  1055. # Generating nicer series titels
  1056. if series_to_show_1 == "total":
  1057. series_to_show_1 = "Portfolio"
  1058. if series_to_show_2 == "total":
  1059. series_to_show_2 = "Portfolio"
  1060. if series_to_show_1 == "benchmark":
  1061. series_to_show_1 = "Benchmark: " + ticker_benchmark
  1062. if series_to_show_2 == "benchmark":
  1063. series_to_show_2 = "Benchmark: " + ticker_benchmark
  1064. # Generating nicer data titels
  1065. data_to_show_1 = data_to_show_1.replace("_", " ").capitalize()
  1066. data_to_show_2 = data_to_show_2.replace("_", " ").capitalize()
  1067. # Increase look of IRR even more
  1068. # Funktioniert auch dann, wenn "irr" nicht vorkommt
  1069. data_to_show_1 = data_to_show_1.replace("irr", "IRR")
  1070. data_to_show_2 = data_to_show_2.replace("irr", "IRR")
  1071. # Generate the chat names / desciptions
  1072. chart_1["name"] = data_to_show_1 + " " + series_to_show_1
  1073. chart_2["name"] = data_to_show_2 + " " + series_to_show_2
  1074. # Construct the trmnl_object
  1075. trmnl_update_object = {}
  1076. trmnl_update_object["merge_variables"] = {}
  1077. trmnl_update_object["merge_variables"]["big_numbers"] = dict_big_numbers
  1078. trmnl_update_object["merge_variables"]["charts"] = charts_data
  1079. # Debugging
  1080. if selected_logging_level == "debug":
  1081. data = json.dumps(trmnl_update_object, indent=2) # Converts a python-dictionary into a json
  1082. with open("trmnl_update_object.json", "w") as f:
  1083. f.write(data)
  1084. # Main Logging
  1085. logging(logging_level="success")
  1086. return trmnl_update_object
  1087. except Exception as error_message:
  1088. logging(logging_level="error")
  1089. logging(logging_level="error", message=f"Failed with error: {error_message}")
  1090. return False
  1091. ### ---------------------------------------------------- --------- ---------------------------------------------------- ###
  1092. ### ---------------------------------------------------- MAIN CODE ---------------------------------------------------- ###
  1093. ### ---------------------------------------------------- --------- ---------------------------------------------------- ###
  1094. # ------------------------------------------- #
  1095. # PART 1: Updating the notion trades database #
  1096. # ------------------------------------------- #
  1097. # Fetches the list of all trades stored in notion
  1098. print("Fetching Data from Notion...", end=" ", flush=True)
  1099. trades = fetch_format_notion_trades(notion_db_id_trades)
  1100. # Generates a list with unique tickers and no duplicates to reduce workload for the yfinance api
  1101. print("Creating a list of unique tickers...", end=" ", flush=True)
  1102. tickers = filter_list_of_tickers(trades)
  1103. # Configuration dependent execution:
  1104. if calculate_benchmark == True:
  1105. # Add the benchmark-ticker to the list of tickers to download data from yfinance from
  1106. print("Adding benchmark-ticker...", end="", flush=True)
  1107. tickers = add_benchmark_ticker(tickers, ticker_benchmark)
  1108. # Fetches & formats the complete history per ticker from yfinance
  1109. print("Fetching & formating yfinance data", end="", flush=True)
  1110. yf_data = fetch_format_yf_data(tickers)
  1111. # Calculates & stores a history per trade
  1112. print("Calculating the history per trade...", end=" ", flush=True)
  1113. history_per_trade = calc_history_per_trade(trades, yf_data)
  1114. # Configuration dependent execution:
  1115. if update_notion == True:
  1116. # Selects the most current values from the history per trade and overwrites them in the "trades" feteched from notion
  1117. print("Selecting the most current values...", end=" ", flush=True)
  1118. trades = select_current_value_per_trade(trades, history_per_trade)
  1119. # Updates the values in the notion database
  1120. print("Updating the notion trades database", end="", flush=True)
  1121. push_notion_trades_update(trades)
  1122. # ------------------------------------------------ #
  1123. # PART 2: Updating the notion investments database #
  1124. # ------------------------------------------------ #
  1125. # Fetches the list of entries in the investment-overview database stored in notion
  1126. print("Fetching & formating notion investments...", end=" ", flush=True)
  1127. investments = fetch_format_notion_investments(notion_db_id_investments)
  1128. # Calculates & stores a history per ticker AND a total across all tickers indexed by the ticker name
  1129. print("Calculating history per ticker...", end=" ", flush=True)
  1130. history_per_ticker = calc_history_per_ticker(history_per_trade, tickers, trades)
  1131. # Configuration dependent execution:
  1132. if update_notion == True:
  1133. # Selects the most current values from the history per ticker and overwrites them in the "investments" feteched from notion
  1134. print("Calculating current value per ticker...", end=" ", flush=True)
  1135. investments = select_current_value_per_ticker(investments, history_per_ticker)
  1136. # Updates the values in the notion database
  1137. print("Updating the notion ticker database", end="", flush=True)
  1138. push_notion_investment_update(investments)
  1139. # ----------------------------------------- #
  1140. # PART 3: Calculating Benchmark performance #
  1141. # ----------------------------------------- #
  1142. # Configuration dependent execution:
  1143. if calculate_benchmark == True:
  1144. # Creating benchmark trades
  1145. print("Creating 'benchmark trades'...", end="", flush=True)
  1146. benchmark_trades = create_benchmark_trades(trades, yf_data)
  1147. # Calculating benchmark trades
  1148. print("Calculating the history per benchmark-trade...", end=" ", flush=True)
  1149. history_per_benchmark_trade = calc_history_per_trade(benchmark_trades, yf_data)
  1150. # Calculates & stores a history for the benchmark
  1151. print("Calculating benchmark-history overall...", end=" ", flush=True)
  1152. history_benchmark = calc_history_per_ticker(history_per_benchmark_trade, tickers, benchmark_trades)
  1153. # Merging the benchmark_history into the ticker_history
  1154. print("Merging the benchmark-history into the ticker-history...", end=" ", flush=True)
  1155. history_per_ticker = merge_histories(history_per_ticker, history_benchmark)
  1156. # --------------------------------- #
  1157. # PART 4: Updating the TRMNL Screen #
  1158. # --------------------------------- #
  1159. # Configuration dependent execution:
  1160. if update_TRMNL == True:
  1161. # Creates a list containing one date per week
  1162. print("Creating a list with one entry per week...", end=" ", flush=True)
  1163. list_filtered_dates = create_list_filtered_dates(trades, trmnl_granularity)
  1164. # Filter a weekly snapshot from the history per ticker
  1165. print("Filtering the history per ticker to weekly values...", end=" ", flush=True)
  1166. history_per_ticker_filtered = filter_history_by_list(history_per_ticker, list_filtered_dates)
  1167. # Prepare a new TRMNL update
  1168. print("Constructing a TERMNL update object...", end=" ", flush=True)
  1169. trmnl_update_object = prep_trmnl_chart_udpate(
  1170. history_per_ticker_filtered,
  1171. series_to_show_1="total",
  1172. data_to_show_1="current_value",
  1173. series_to_show_2="benchmark",
  1174. data_to_show_2="current_value"
  1175. )
  1176. # Push the update to TRMNL
  1177. print("Updating a TERMNL screen...", end=" ", flush=True)
  1178. push_trmnl_update_chart(trmnl_update_object, trmnl_url_chart_1, trmnl_headers)
  1179. # Prepare a new TRMNL update
  1180. print("Constructing a TERMNL update object...", end=" ", flush=True)
  1181. trmnl_update_object = prep_trmnl_chart_udpate(
  1182. history_per_ticker_filtered,
  1183. series_to_show_1="total",
  1184. data_to_show_1="current_irr",
  1185. series_to_show_2="benchmark",
  1186. data_to_show_2="current_irr"
  1187. )
  1188. # Push the update to TRMNL
  1189. print("Updating a TERMNL screen...", end=" ", flush=True)
  1190. push_trmnl_update_chart(trmnl_update_object, trmnl_url_chart_2, trmnl_headers)
  1191. # Prepare a new TRMNL update
  1192. print("Constructing a TERMNL update object...", end=" ", flush=True)
  1193. trmnl_update_object = prep_trmnl_chart_udpate(
  1194. history_per_ticker_filtered,
  1195. series_to_show_1="total",
  1196. data_to_show_1="total_performanance",
  1197. series_to_show_2="benchmark",
  1198. data_to_show_2="total_performanance"
  1199. )
  1200. # Push the update to TRMNL
  1201. print("Updating a TERMNL screen...", end=" ", flush=True)
  1202. push_trmnl_update_chart(trmnl_update_object, trmnl_url_chart_3, trmnl_headers)
  1203. # ----------------- #
  1204. # PART 5: Finsch up #
  1205. # ----------------- #
  1206. # Logging
  1207. print("--------------------------- SUCCESS! ---------------------------")
  1208. print("Completed cycle at: {}".format(datetime.datetime.now()))
  1209. print("--------------------------- SUCCESS! ---------------------------")