functions.py 36 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182838485868788899091929394959697989910010110210310410510610710810911011111211311411511611711811912012112212312412512612712812913013113213313413513613713813914014114214314414514614714814915015115215315415515615715815916016116216316416516616716816917017117217317417517617717817918018118218318418518618718818919019119219319419519619719819920020120220320420520620720820921021121221321421521621721821922022122222322422522622722822923023123223323423523623723823924024124224324424524624724824925025125225325425525625725825926026126226326426526626726826927027127227327427527627727827928028128228328428528628728828929029129229329429529629729829930030130230330430530630730830931031131231331431531631731831932032132232332432532632732832933033133233333433533633733833934034134234334434534634734834935035135235335435535635735835936036136236336436536636736836937037137237337437537637737837938038138238338438538638738838939039139239339439539639739839940040140240340440540640740840941041141241341441541641741841942042142242342442542642742842943043143243343443543643743843944044144244344444544644744844945045145245345445545645745845946046146246346446546646746846947047147247347447547647747847948048148248348448548648748848949049149249349449549649749849950050150250350450550650750850951051151251351451551651751851952052152252352452552652752852953053153253353453553653753853954054154254354454554654754854955055155255355455555655755855956056156256356456556656756856957057157257357457557657757857958058158258358458558658758858959059159259359459559659759859960060160260360460560660760860961061161261361461561661761861962062162262362462562662762862963063163263363463563663763863964064164264364464564664764864965065165265365465565665765865966066166266366466566666766866967067167267367467567667767867968068168268368468568668768868969069169269369469569669769869970070170270370470570670770870971071171271371471571671771871972072172272372472572672772872973073173273373473573673773873974074174274374474574674774874975075175275375475575675775875976076176276376476576676776876977077177277377477577677777877978078178278378478578678778878979079179279379479579679779879980080180280380480580680780880981081181281381481581681781881982082182282382482582682782882983083183283383483583683783883984084184284384484584684784884985085185285385485585685785885986086186286386486586686786886987087187287387487587687787887988088188288388488588688788888989089189289389489589689789889990090190290390490590690790890991091191291391491591691791891992092192292392492592692792892993093193293393493593693793893994094194294394494594694794894995095195295395495595695795895996096196296396496596696796896997097197297397497597697797897998098198298398498598698798898999099199299399499599699799899910001001100210031004100510061007100810091010101110121013101410151016101710181019102010211022102310241025102610271028102910301031
  1. ### -------------------- LIBARIES --------------------
  2. import datetime
  3. import time
  4. import json
  5. import yfinance as yf
  6. import pandas as pd
  7. import requests
  8. import config
  9. ### -------------------- FUNCTIONS --------------------
  10. # ---------------- #
  11. # HELPER FUNCTIONS #
  12. # ---------------- #
  13. # LOGGING / PRINTING TO TERMINAL
  14. def logging(message = "", logging_level = "", new_line = True):
  15. # Take the selected logging level in the config file
  16. # Look this up in the list of all available logging levels in the config file
  17. # Return the index number
  18. config_logging_level = config.logging_levels.index(config.selected_logging_level)
  19. try:
  20. # Take the logging level of the text to print
  21. # Look this up in the list of all available logging levels in the config file
  22. # Return the index number
  23. message_logging_level = config.logging_levels.index(logging_level)
  24. except:
  25. # Fallback to the least important logging level
  26. # Solved by checking the lenght of the available logging levels
  27. message_logging_level = len(config.logging_levels)
  28. # Check for false new_line entries
  29. if new_line is not bool:
  30. new_line = True
  31. # Check if the warning should be printed
  32. if message_logging_level <= config_logging_level:
  33. # Geting the log color
  34. log_color = getattr(config.log_colors, logging_level)
  35. # Construct the logging-text incl. color
  36. log_text = str(log_color + "[" + logging_level + "] " + config.log_colors.endcode + message)
  37. # Check if the warning should end with a new-line
  38. # Printing the text
  39. if new_line == True:
  40. print(log_text)
  41. else:
  42. print(log_text, end=" ", flush=True)
  43. # CALCULATE THE IRR
  44. def calculate_irr(date_now, date_open, value_now, value_open):
  45. error = False
  46. irr = 0.0
  47. try:
  48. # Count the number in days
  49. a = date_now - date_open
  50. a = a.days
  51. # Am Tag des Kaufs selbst, liegt das Delta in Tagen bei 0
  52. # Um dennoch einen IRR kalkulieren zu können, wird das Delta auf 1 gsetzt
  53. if a == 0:
  54. a = 1
  55. a = a / 365 # Umrechnung auf Jahresanteil, um auch den Jahreszinssatz zu bekommen
  56. b = value_now / value_open
  57. # Catch negative IRRs
  58. if b < 0:
  59. b = b * (-1)
  60. irr = b**(1/a) # matematisch identisch zur b-ten Wurzel von a
  61. irr = irr * (-1)
  62. else:
  63. irr = b**(1/a) # matematisch identisch zur b-ten Wurzel von a
  64. except:
  65. error = True
  66. # Return data if successful
  67. if error == True:
  68. print("[ERROR] Calculation of irr")
  69. return error
  70. else:
  71. return irr
  72. # GET THE DAY OF THE OLDEST TRADE
  73. def get_date_open_oldest_trade(trades):
  74. # Identify the open date for the oldest trade
  75. date_open_oldest_trade = datetime.date.today()
  76. for i in trades:
  77. if trades[i]["date_open"] < date_open_oldest_trade:
  78. date_open_oldest_trade = trades[i]["date_open"]
  79. return date_open_oldest_trade
  80. # CREATES LIST OF UNIQUE TICKERS
  81. def filter_list_of_tickers(trades):
  82. tickers = []
  83. try:
  84. for i in trades:
  85. # Fetch ticker belonging to trade
  86. ticker = trades[i]['ticker']
  87. # Add ticker to list, if not already present
  88. if ticker not in tickers:
  89. tickers.append(ticker)
  90. # Main Logging
  91. logging(logging_level="success")
  92. logging(logging_level="info", message=f"{len(tickers)} tickers found")
  93. return tickers
  94. except Exception as error_message:
  95. logging(logging_level="error")
  96. logging(logging_level="error", message=f"Failed with error: {error_message}")
  97. return False
  98. # CREATE LIST OF WEEKLY DATES
  99. def create_list_filtered_dates(trades, days_seperation):
  100. stop_date = get_date_open_oldest_trade(trades)
  101. index_date = datetime.date.today()
  102. try:
  103. # Create reversed list (1st entry is today going back in time)
  104. list_filtered_dates = []
  105. while index_date >= stop_date:
  106. list_filtered_dates.append(index_date.isoformat())
  107. index_date = index_date - datetime.timedelta(days=days_seperation)
  108. # Reverse the list, so that the frist entry is the oldest one
  109. list_filtered_dates.reverse()
  110. # Main Logging
  111. logging(logging_level="success")
  112. logging(logging_level="info", message=f"{len(list_filtered_dates)} dates in weekly list")
  113. return list_filtered_dates
  114. except Exception as error_message:
  115. logging(logging_level="error")
  116. logging(logging_level="error", message=f"Failed with error: {error_message}")
  117. return False
  118. # FETCH THE LAST INDEX FROM A DICT
  119. def fetch_last_key_from_dict(dict):
  120. key_list = list(dict.keys()) # Extract the keys and convert them to a list
  121. last_key = key_list[-1] # select the last entry from the list as it is the most current entry
  122. return last_key
  123. # -------------------------- #
  124. # NETWORK DOWNLOAD FUNCTIONS #
  125. # -------------------------- #
  126. # NOTION FETCH PAGES
  127. def notion_get_pages(db_id_trades, num_pages=None):
  128. try:
  129. # ------------------ FETCH THE FIRST 100 PAGES FROM A DB
  130. # Prepare Request
  131. url = f"https://api.notion.com/v1/databases/{db_id_trades}/query"
  132. get_all = num_pages is None # If num_pages is None, get all pages, otherwise just the defined number.
  133. page_size = 100 if get_all else num_pages
  134. payload = {"page_size": page_size}
  135. # Make Request
  136. raw_response = requests.post(url, json=payload, headers=config.notion_headers)
  137. # Process Reply
  138. parsed_response = raw_response.json()
  139. result = parsed_response["results"]
  140. # ------------------ FETCH 100 MORE PAGES AS OFTEN AS REQUIRED
  141. while parsed_response["has_more"] and get_all:
  142. # Prepare Request
  143. payload = {"page_size": page_size, "start_cursor": parsed_response["next_cursor"]}
  144. url = f"https://api.notion.com/v1/databases/{db_id_trades}/query"
  145. # Make Request
  146. raw_response = requests.post(url, json=payload, headers=config.notion_headers)
  147. # Process Reply
  148. parsed_response = raw_response.json()
  149. result.extend(parsed_response["results"])
  150. # Logging
  151. return result
  152. except Exception:
  153. return True # Return True when there was an error
  154. # NOTION FETCH & FORMAT TRADES
  155. def fetch_format_notion_trades(db_id_trades):
  156. trades = {}
  157. fetch_error = False
  158. format_errors = 0
  159. number_of_trades = 0
  160. error_message = ""
  161. # Download data from notion
  162. data = notion_get_pages(db_id_trades)
  163. # Check, if cuccessfull
  164. if data is True:
  165. fetch_error = True
  166. else:
  167. # Format the recieved data
  168. for i in data:
  169. # Count for stratistics
  170. number_of_trades = number_of_trades + 1
  171. # Each page is loaded as a dictionary
  172. notion_page = dict(i)
  173. # Handling desired missing entries
  174. try:
  175. date_close = notion_page["properties"]["Close"]["date"]
  176. date_close = date_close["start"]
  177. date_close = datetime.date(*map(int, date_close.split('-')))
  178. except:
  179. date_close = 0
  180. # Handeling non-desired missing entries (by skipping this trade)
  181. try:
  182. # Try extracting values
  183. trade = {}
  184. # Format date-open
  185. date_open = notion_page["properties"]["Open"]["date"]
  186. date_open = date_open["start"]
  187. date_open = datetime.date(*map(int, date_open.split('-')))
  188. # Combine data into json structure
  189. trade = {
  190. 'ticker' : notion_page["properties"]["Ticker"]["select"]["name"],
  191. 'date_open' : date_open,
  192. 'date_close' : date_close,
  193. 'course_open' : notion_page["properties"]["Open (€)"]["number"],
  194. 'course_close' : notion_page["properties"]["Close (€)"]["number"],
  195. 'course_current' : notion_page["properties"]["Current (€)"]["number"],
  196. 'irr' : notion_page["properties"]["IRR (%)"]["number"],
  197. 'units' : notion_page["properties"]["Units"]["number"],
  198. 'dividends' : notion_page["properties"]["Dividends (€)"]["number"]
  199. }
  200. # Save values
  201. notion_page_id = notion_page["id"] # Use as key for the dictionary
  202. trades[notion_page_id] = trade
  203. except Exception as e:
  204. format_errors = format_errors + 1
  205. error_message = e
  206. # Main Logging
  207. if fetch_error == False & format_errors == 0:
  208. logging(logging_level="success")
  209. logging(logging_level="info", message=f"{number_of_trades} trades recieved and formated")
  210. return trades
  211. elif fetch_error == False & format_errors > 0:
  212. logging(logging_level="warning")
  213. logging(logging_level="warning", message=f"{format_errors} trades out of {number_of_trades} skiped...maybe due to missing values?")
  214. return trades
  215. else:
  216. logging(logging_level="error")
  217. logging(logging_level="error", message=f"Failed with error: {error_message}")
  218. return False
  219. # NOTION FETCH & FORMAT INVESTMENT OVERVIEW
  220. def fetch_format_notion_investments(db_id_investments):
  221. investments = {}
  222. fetch_error = False
  223. format_errors = 0
  224. number_of_investments = 0
  225. # Download data & check for success
  226. data = notion_get_pages(db_id_investments)
  227. if data is True:
  228. error = True
  229. else:
  230. # Format recieved data
  231. for i in data:
  232. # Count up for statistics
  233. number_of_investments = number_of_investments + 1
  234. try:
  235. # Each page is loaded as a dictionary
  236. notion_page = dict(i)
  237. # Extract values
  238. notion_page_id = notion_page["id"] # Use as key for the dictionary
  239. investments[notion_page_id] = {}
  240. investments[notion_page_id]["ticker"] = notion_page["properties"]["Ticker"]["select"]["name"]
  241. investments[notion_page_id]["total_dividends"] = notion_page["properties"]["Dividends (€)"]["number"]
  242. investments[notion_page_id]["current_value"] = notion_page["properties"]["Current (€)"]["number"]
  243. investments[notion_page_id]["current_irr"] = notion_page["properties"]["IRR (%)"]["number"]
  244. investments[notion_page_id]["total_performanance"] = notion_page["properties"]["Performance (€)"]["number"]
  245. # Skip this entry, if errors show up
  246. except:
  247. format_errors = format_errors + 1
  248. # Main Logging
  249. if fetch_error == False & format_errors == 0:
  250. logging(logging_level="success")
  251. logging(logging_level="info", message=f"{number_of_investments} trades recieved and formated")
  252. return investments
  253. elif fetch_error == False & format_errors > 0:
  254. logging(logging_level="warning")
  255. logging(logging_level="warning", message=f"{format_errors} trades out of {number_of_investments} skiped...maybe due to missing values?")
  256. return investments
  257. else:
  258. logging(logging_level="error")
  259. return False
  260. # YFINANCE FETCH & FORMAT DATA
  261. def fetch_format_yf_data(tickers):
  262. yf_data = {}
  263. fetch_errors = 0
  264. format_errors = 0
  265. number_of_tickers = 0
  266. # Download data for each ticker seperately
  267. for i in tickers:
  268. number_of_tickers = number_of_tickers +1
  269. skip_formating = False # Helper varianbel (see flow logik)
  270. ticker = i
  271. # Catch errors during the download
  272. try:
  273. # Download data
  274. api = yf.Ticker(ticker)
  275. data = api.history(period="max")
  276. except:
  277. # Store error for later logging
  278. fetch_errors = fetch_errors + 1
  279. skip_formating = True
  280. # If the download was successfull:
  281. if skip_formating == False:
  282. # Try formating the data
  283. try:
  284. # Convert to Pandas DataFrame
  285. data = pd.DataFrame(data)
  286. # Delete the columns "Stock Splits", "High", "Low" and "Open"
  287. del data['Open']
  288. del data['Low']
  289. del data['High']
  290. del data['Volume']
  291. # Delete these 2 columns, if they exist
  292. if 'Stock Splits' in data.columns:
  293. del data['Stock Splits']
  294. if 'Capital Gains' in data.columns:
  295. del data['Capital Gains']
  296. # Get the Number of rows in data
  297. data_rows = data.shape[0]
  298. # Create new index without the time from the existing datetime64-index
  299. old_index = data.index
  300. new_index = []
  301. x = 0
  302. while x < data_rows:
  303. date = pd.Timestamp.date(old_index[x]) # Converts the "Pandas Timestamp"-object to a "date" object
  304. new_index.append(date)
  305. x+=1
  306. # Add the new index to the dataframe and set it as the index
  307. data.insert(1, 'Date', new_index)
  308. data.set_index('Date', inplace=True)
  309. # Save the data-frame to the yf_data dict
  310. yf_data[ticker] = data
  311. # Handle formating errors
  312. except:
  313. format_errors = format_errors +1
  314. # in case of an error the entry never get's added to the yf_data object
  315. # Wait for the API to cool down
  316. print(".", end="", flush=True)
  317. time.sleep(config.api_cooldowm_time)
  318. # Main Logging
  319. print(" ", end="", flush=True)
  320. if fetch_errors == 0 & format_errors == 0:
  321. logging(logging_level="success")
  322. logging(logging_level="info", message=f"{number_of_tickers} tickers recieved and formated")
  323. return yf_data
  324. elif fetch_errors == 0 & format_errors > 0:
  325. logging(logging_level="warning")
  326. logging(logging_level="warning", message=f"{format_errors} tickers out of {number_of_tickers} skiped")
  327. return yf_data
  328. else:
  329. logging(logging_level="error")
  330. logging(logging_level="error", message=f"Failed with error: {number_of_tickers}")
  331. print("\n")
  332. return False
  333. # ------------------------ #
  334. # NETWORK UPLOAD FUNCTIONS #
  335. # ------------------------ #
  336. # NOTION UPDATE PAGES
  337. def notion_update_page(page_id: str, data: dict):
  338. url = f"https://api.notion.com/v1/pages/{page_id}"
  339. payload = {"properties": data}
  340. results = requests.patch(url, json=payload, headers=config.notion_headers)
  341. return results
  342. # UPDATE NOTION-TRADES-DATABASE
  343. def push_notion_trades_update(trades):
  344. # Logging
  345. error_count = 0
  346. number_of_uploads = 0
  347. for notion_page_id in trades:
  348. number_of_uploads = number_of_uploads+1
  349. try:
  350. # The irr is stored in the format 1.2534
  351. # Notion need the format 0,2534
  352. irr_notion = trades[notion_page_id]['irr'] - 1
  353. irr_notion = round(irr_notion, 4)
  354. # Construct Notion-Update-Object
  355. notion_update = {
  356. "Current (€)": {
  357. "number": trades[notion_page_id]['course_current']
  358. },
  359. "IRR (%)": {
  360. "number": irr_notion
  361. },
  362. "Dividends (€)": {
  363. "number": trades[notion_page_id]['dividends']
  364. }
  365. }
  366. # Update the properties of the corresponding notion-page
  367. notion_update_page(notion_page_id, notion_update)
  368. except:
  369. error_count = error_count + 1
  370. # Wait for the API to cool off
  371. print(".", end="", flush=True)
  372. time.sleep(config.api_cooldowm_time)
  373. # Logging
  374. print(" ", end="", flush=True)
  375. if error_count == 0:
  376. logging(logging_level="success")
  377. elif error_count < number_of_uploads:
  378. logging(logging_level="warning")
  379. logging(logging_level="success", message=f"Updating notion trades failed for {error_count} out of {number_of_uploads} entries")
  380. else:
  381. logging(logging_level="error")
  382. logging(logging_level="success", message=f"Updating notion trades failed for all {error_count} entries")
  383. # UPDATE NOTION-INVESTMENT-OVERVIEW
  384. def push_notion_investment_update(investments):
  385. # Logging
  386. error_count = 0
  387. number_of_uploads = 0
  388. for notion_page_id in investments:
  389. number_of_uploads = number_of_uploads+1
  390. # Try uploading an update
  391. try:
  392. # The irr is stored in the format 1.2534
  393. # Notion need the format 0,2534
  394. irr_notion = investments[notion_page_id]['current_irr'] - 1
  395. irr_notion = round(irr_notion, 4)
  396. # Construct Notion-Update-Object
  397. notion_update = {
  398. "Current (€)": {
  399. "number": investments[notion_page_id]['current_value']
  400. },
  401. "IRR (%)": {
  402. "number": irr_notion
  403. },
  404. "Performance (€)": {
  405. "number": investments[notion_page_id]['total_performanance']
  406. },
  407. "Dividends (€)": {
  408. "number": investments[notion_page_id]['total_dividends']
  409. }
  410. }
  411. # Update the properties of the corresponding notion-page
  412. notion_update_page(notion_page_id, notion_update)
  413. except:
  414. error_count = error_count + 1
  415. # Wait for the API to cool off
  416. print(".", end="", flush=True)
  417. time.sleep(config.api_cooldowm_time)
  418. # Logging
  419. print(" ", end="", flush=True)
  420. if error_count == 0:
  421. logging(logging_level="success")
  422. elif error_count < number_of_uploads:
  423. logging(logging_level="warning")
  424. logging(logging_level="success", message=f"Updating notion investments failed for {error_count} out of {number_of_uploads} entries")
  425. else:
  426. logging(logging_level="error")
  427. logging(logging_level="success", message=f"Updating notion investments failed for all {error_count} entries")
  428. # TRMNL UPDATE DIAGRAMMS
  429. def push_trmnl_update_chart(trmnl_update_object, trmnl_url, trmnl_headers):
  430. # Send the data to TRMNL
  431. try:
  432. data = json.dumps(trmnl_update_object, indent=2) # Converts a python-dictionary into a json
  433. reply = requests.post(trmnl_url, data=data, headers=trmnl_headers)
  434. # Logging
  435. if reply.status_code == 200:
  436. logging(logging_level="success")
  437. elif reply.status_code == 429:
  438. logging_level="success"
  439. logging(logging_level="warning")
  440. logging(logging_level="warning", message="Exceeded TRMNL's API rate limits")
  441. logging(logging_level="warning", message="Waiting some time should work")
  442. elif reply.status_code == 422:
  443. logging(logging_level="warning")
  444. logging(logging_level="warning", message="Upload successful, but data cannot be displayed correctly")
  445. logging(logging_level="warning", message="The payload is probably to large in size")
  446. else:
  447. logging(logging_level="error")
  448. logging(logging_level="error", message=f"Failed pushing data to TRMNL with server reply code: {reply.status_code}")
  449. logging(logging_level="debug", message=f"Complete server reply message: {reply}")
  450. except Exception as e:
  451. logging(logging_level="error")
  452. logging(logging_level="error", message=f"Failed pushing data to TRMNL with error code: {e}")
  453. # ----------------------------- #
  454. # HISTORY CALCULATION FUNCTIONS #
  455. # ----------------------------- #
  456. # CALC HISTORY PER TRADE
  457. def calc_history_per_trade(trades, yf_data):
  458. # Create support variables
  459. history_per_trade = {}
  460. total_dividends = 0
  461. date_open_oldest_trade = get_date_open_oldest_trade(trades)
  462. # Logging & statistics
  463. missing_day_entrys = 0
  464. days_formated = 0
  465. number_of_trades = 0
  466. # As this history is so important, it is okay if this functions fails in total if errors araise
  467. try:
  468. # ------------------ LOOP OVER ALL TRADES
  469. for trade_id in trades:
  470. # Statistics
  471. number_of_trades = number_of_trades +1
  472. # ------------------ PREPARE FOR THE (NEXT) LOOP OVER ALL DAYS
  473. # Set / Reset the index-date to the oldest trade day
  474. # Resetting is required so that the calculations for the next trade start with day 1
  475. index_date = date_open_oldest_trade
  476. # Set the initial value for the course on the previous day to 0
  477. # Just in case the very first trade was made on a weekend somehow, where there is no yfinance data available
  478. previous_course = 0.0
  479. # Check, if the trade was closed already
  480. # If it was not, set the closure date to the future (Trick 17)
  481. if trades[trade_id]["date_close"] == 0:
  482. date_close = datetime.date.today() + datetime.timedelta(days=1)
  483. else:
  484. date_close = trades[trade_id]["date_close"]
  485. date_open = trades[trade_id]["date_open"]
  486. # Keep ticker for connecting performance later
  487. ticker = trades[trade_id]['ticker']
  488. # ------------------ DETERMINE THE COUSE PER DAY
  489. while index_date != datetime.date.today() + datetime.timedelta(days=1):
  490. # Statistics
  491. days_formated = days_formated +1
  492. # Fetch course for the day & eventual dividends from yf_data
  493. try:
  494. current_course = yf_data[ticker].at[index_date, 'Close']
  495. current_dividends_per_ticker = yf_data[ticker].at[index_date, 'Dividends']
  496. # Catch missing yf-data (eg. for weekends) by reusing course from previous day
  497. except:
  498. current_course = previous_course
  499. current_dividends_per_ticker = 0.0 # there are never dividends on non-trading days
  500. missing_day_entrys = missing_day_entrys +1 # Increase the warning count
  501. # Catch the special case of the day when the trade was closed
  502. # In this case, the current course needs to be overwritten with the sell-value
  503. if date_close == index_date:
  504. current_course = trades[trade_id]['course_close']
  505. # Save the result for the next iteration
  506. # This setup also makes it possible, that a previous course is passed down across mutiple days
  507. # This makes sense is case i.e. for a weekend
  508. previous_course = current_course
  509. # ------------------ CALCULATE PERFORMANCE IF REQUIRED
  510. if index_date >= date_open and index_date <= date_close:
  511. # Calculate performance values
  512. current_amount = trades[trade_id]['units']
  513. current_invested = current_amount * trades[trade_id]['course_open']
  514. total_dividends = total_dividends + current_amount * current_dividends_per_ticker
  515. current_value = current_amount * current_course
  516. current_value_with_dividends = current_value + total_dividends
  517. current_irr = calculate_irr(index_date, date_open, current_value_with_dividends, current_invested)
  518. total_performanance = current_value_with_dividends - current_invested
  519. if current_value_with_dividends == 0:
  520. print("0-value Error with ticker: {}".format(ticker))
  521. else:
  522. # Write 0, if trade is not relevant for current timeframe
  523. current_amount = 0
  524. current_invested = 0.00
  525. total_dividends = 0.00
  526. current_value = 0.00
  527. current_irr = 0.00
  528. total_performanance = 0.0
  529. # ------------------ STORE RESULTS
  530. index_date_iso = index_date.isoformat()
  531. # Store all values into a dict
  532. dict_a = {}
  533. dict_a['current_amount'] = current_amount
  534. dict_a['current_invested'] = current_invested
  535. dict_a['total_dividends'] = total_dividends
  536. dict_a['current_value'] = current_value
  537. dict_a['current_irr'] = current_irr
  538. dict_a['current_course'] = current_course
  539. dict_a['total_performanance'] = total_performanance
  540. # Check if the date is already present
  541. if index_date_iso in history_per_trade:
  542. dict_b = history_per_trade[index_date_iso]
  543. else:
  544. dict_b = {}
  545. # Add the values to the trade_id value-pair
  546. dict_b[trade_id] = dict_a
  547. # Update the hostory_per_trade
  548. history_per_trade.update({index_date_iso : dict_b})
  549. # ------------------ NEXT ITERATION
  550. index_date = index_date + datetime.timedelta(days=1)
  551. # ------------------ LOGGING & DEBUGING
  552. # Debug writing history to disk
  553. if config.selected_logging_level == "debug":
  554. data = json.dumps(history_per_trade, indent=2) # Converts a python-dictionary into a json
  555. with open("history_per_trade.json", "w") as f:
  556. f.write(data)
  557. # Logging logging
  558. if missing_day_entrys == 0:
  559. logging(logging_level="success")
  560. logging(logging_level="info", message=f"created a history with {days_formated} across all {number_of_trades} tickers o_O")
  561. else:
  562. logging(logging_level="warning")
  563. logging(logging_level="warning", message=f"No yf-data available in {missing_day_entrys} cases accross all {number_of_trades} tickers")
  564. logging(logging_level="warning", message="Probably reason is non-trading-days eg. weekends")
  565. logging(logging_level="warning", message="Used values from previous trade-day instead")
  566. # Return date
  567. return history_per_trade
  568. except Exception as error_message:
  569. logging(logging_level="error")
  570. logging(logging_level="error", message=f"Failed with error message: {error_message}")
  571. return False
  572. # CALC THE HISTORY PER TRADE & OVERALL
  573. def calc_history_per_ticker(history_per_trade, tickers, trades):
  574. # ------------------ CREATE JSON OBJECT
  575. # Create the json-dict
  576. history_per_ticker = {}
  577. # Logging & statistics
  578. missing_day_entrys = 0
  579. days_formated = 0
  580. # As this history is so important, it is okay if this functions fails in total if errors araise
  581. try:
  582. # Loop over each date entry in the history
  583. for date_entry in history_per_trade:
  584. # Statistics
  585. days_formated = days_formated +1
  586. # Create a dict to store the results per day and ticker
  587. dict_daily = {}
  588. for ticker in tickers:
  589. dict_daily[ticker] = {}
  590. dict_daily[ticker]["current_invested"] = 0
  591. dict_daily[ticker]["total_dividends"] = 0
  592. dict_daily[ticker]["current_value"] = 0
  593. dict_daily[ticker]["current_irr"] = 0
  594. dict_daily[ticker]["current_irr"] = 0
  595. dict_daily[ticker]["total_performanance"] = 0
  596. dict_daily[ticker]["current_amount"] = 0 # Added only for ticker entries, not for the "total" value
  597. dict_daily[ticker]["current_course"] = 0 # Added only for ticker entries, not for the "total" value
  598. dict_daily["total"] = {}
  599. dict_daily["total"]["current_invested"] = 0
  600. dict_daily["total"]["total_dividends"] = 0
  601. dict_daily["total"]["current_value"] = 0
  602. dict_daily["total"]["current_irr"] = 0
  603. dict_daily["total"]["current_irr"] = 0
  604. dict_daily["total"]["total_performanance"] = 0
  605. # Loop over each trade-entry for that day
  606. for trade_id in history_per_trade[date_entry]:
  607. # Extract data from the history_per_trade
  608. trade_amount = history_per_trade[date_entry][trade_id]['current_amount']
  609. trade_invested = history_per_trade[date_entry][trade_id]['current_invested']
  610. trade_dividends = history_per_trade[date_entry][trade_id]['total_dividends']
  611. trade_value = history_per_trade[date_entry][trade_id]['current_value']
  612. trade_irr = history_per_trade[date_entry][trade_id]['current_irr']
  613. trade_course = history_per_trade[date_entry][trade_id]['current_course']
  614. trade_performanance = history_per_trade[date_entry][trade_id]['total_performanance']
  615. # Lookup the ticker by the trade-id
  616. ticker = trades[trade_id]["ticker"]
  617. # Extract data from the history_per_ticker
  618. ticker_amount = dict_daily[ticker]['current_amount']
  619. ticker_invested = dict_daily[ticker]['current_invested']
  620. ticker_dividends = dict_daily[ticker]['total_dividends']
  621. ticker_value = dict_daily[ticker]['current_value']
  622. ticker_irr = dict_daily[ticker]['current_irr']
  623. ticker_performanance = dict_daily[ticker]['total_performanance']
  624. # Overwrite the values in the history_per_ticker
  625. dict_daily[ticker]['current_amount'] = ticker_amount + trade_amount # Simple addition works
  626. dict_daily[ticker]['current_invested'] = ticker_invested + trade_invested
  627. dict_daily[ticker]['total_dividends'] = ticker_dividends + trade_dividends
  628. dict_daily[ticker]['current_value'] = ticker_value + trade_value
  629. dict_daily[ticker]['total_performanance'] = ticker_performanance + trade_performanance
  630. dict_daily[ticker]['current_course'] = trade_course # Simple overwrite is fine, as the course is the same for all trades
  631. if ticker_invested == 0 and trade_invested == 0:
  632. dict_daily[ticker]['current_irr'] = 0
  633. # Catch 0 values
  634. else:
  635. dict_daily[ticker]['current_irr'] = (ticker_irr * ticker_invested + trade_irr * trade_invested) / (ticker_invested + trade_invested)
  636. # --> 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
  637. # Calculate the "total" entry after finishing with all the trades
  638. for ticker in tickers:
  639. # Same logic as above, but shortended code
  640. dict_daily["total"]['total_dividends'] = dict_daily["total"]['total_dividends'] + dict_daily[ticker]['total_dividends']
  641. dict_daily["total"]['current_value'] = dict_daily["total"]['current_value'] + dict_daily[ticker]['current_value']
  642. dict_daily["total"]['total_performanance'] = dict_daily["total"]['total_performanance'] + dict_daily[ticker]['total_performanance']
  643. # Extracting the values before rewriting them, to preserve them for the IRR calculation
  644. total_invested = dict_daily["total"]['current_invested']
  645. ticker_invested = dict_daily[ticker]['current_invested']
  646. dict_daily["total"]['current_invested'] = total_invested + ticker_invested
  647. # Extracting the values before rewriting them, to preserve them for the IRR calculation
  648. if ticker_invested == 0 and total_invested == 0:
  649. dict_daily["total"]['current_irr'] = 0
  650. else:
  651. total_irr = dict_daily["total"]['current_irr']
  652. ticker_irr = dict_daily[ticker]['current_irr']
  653. dict_daily["total"]['current_irr'] = (total_irr * total_invested + ticker_irr * ticker_invested) / (total_invested + ticker_invested)
  654. # Finally, write the results for this day-entry to the history_per_ticker
  655. history_per_ticker[date_entry] = dict_daily
  656. # ------------------ LOGGING & DEBUGING
  657. # Debugging
  658. if config.selected_logging_level == "debug":
  659. data = json.dumps(history_per_ticker, indent=2) # Converts a python-dictionary into a json
  660. with open("history_per_ticker.json", "w") as f:
  661. f.write(data)
  662. # Success Logging
  663. logging(logging_level="success")
  664. logging(logging_level="info", message=f"created a history with {days_formated} days formated o_O")
  665. return history_per_ticker
  666. # Error Logging
  667. except Exception as error_message:
  668. logging(logging_level="error")
  669. logging(logging_level="error", message=f"Failed with error message: {error_message}")
  670. return False
  671. # --------------------------- #
  672. # HISTORY SELECTION FUNCTIONS #
  673. # --------------------------- #
  674. # FILTER ANY HISTORY OBJECT TO SELECTED DATES
  675. def filter_history_by_list(history, dates_list):
  676. filtered_history = {}
  677. try:
  678. # Loop over all days
  679. for index_date in history:
  680. # Check, if the history-date is in the filter-list
  681. if index_date in dates_list:
  682. # If so, add this date-entry to the filtered history object
  683. filtered_history[index_date] = history[index_date]
  684. # Main Logging
  685. logging(logging_level="success")
  686. return filtered_history
  687. except Exception as error_message:
  688. logging(logging_level="error")
  689. logging(logging_level="error", message=f"Failed with error: {error_message}")
  690. return False
  691. # SELECT CURRENT VALUES PER TRADE
  692. def select_current_value_per_trade(trades, history_per_trade):
  693. # Logging
  694. format_errors = 0
  695. # Loop over all trades
  696. for trade_id in trades:
  697. try:
  698. # Determine, what values to fetch based on whether the trade was closed already
  699. date_closed = trades[trade_id]["date_close"]
  700. if date_closed == 0:
  701. # If trade still open, use performance data from today
  702. index_date_iso = datetime.date.today().isoformat()
  703. else:
  704. # If trade closed, use performance data from close-date
  705. index_date_iso = date_closed.isoformat()
  706. # Fetch data from history and save for this trade
  707. trades[trade_id]["course_current"] = history_per_trade[index_date_iso][trade_id]['current_course']
  708. trades[trade_id]["irr"] = history_per_trade[index_date_iso][trade_id]['current_irr']
  709. trades[trade_id]["dividends"] = history_per_trade[index_date_iso][trade_id]['total_dividends']
  710. except:
  711. format_errors = format_errors + 1
  712. # Logging logging
  713. if format_errors == 0:
  714. logging(logging_level="success")
  715. else:
  716. logging(logging_level="warning")
  717. logging(logging_level="warning", message=f"Failed updating the current value per trade in {format_errors} cases")
  718. return trades
  719. # SELECT CURRENT VALUES PER TICKER
  720. def select_current_value_per_ticker(investments, history_per_ticker):
  721. # Logging
  722. format_errors = 0
  723. # Loop over all investments
  724. for investment_id in investments:
  725. try:
  726. # Generate the iso-date of today as the required index
  727. index_date_iso = datetime.date.today().isoformat()
  728. # Get the ticker corresponding to the investment
  729. ticker = investments[investment_id]["ticker"]
  730. # Select latest data from history and save for this investment
  731. investments[investment_id]["total_dividends"] = history_per_ticker[index_date_iso][ticker]['total_dividends']
  732. investments[investment_id]["current_value"] = history_per_ticker[index_date_iso][ticker]['current_value']
  733. investments[investment_id]["current_irr"] = history_per_ticker[index_date_iso][ticker]['current_irr']
  734. investments[investment_id]["total_performanance"] = history_per_ticker[index_date_iso][ticker]['total_performanance']
  735. except:
  736. format_errors = format_errors + 1
  737. # Logging
  738. if format_errors == 0:
  739. logging(logging_level="success")
  740. else:
  741. logging(logging_level="warning")
  742. logging(logging_level="warning", message=f"Failed updating the current value per ticker in {format_errors} cases")
  743. return investments
  744. # TRMNL CREATE IRR-UPDATE
  745. 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
  746. # Setup
  747. dict_big_numbers = {}
  748. charts_data = []
  749. chart_1 = {}
  750. chart_2 = {}
  751. try:
  752. # Fetch the latest date entry from the history
  753. index_date_iso = fetch_last_key_from_dict(history_to_show)
  754. # Select latest data from history for the big-numbers
  755. current_value = history_to_show[index_date_iso]["total"]["current_value"]
  756. total_performanance = history_to_show[index_date_iso]["total"]["total_performanance"]
  757. current_irr = history_to_show[index_date_iso]["total"]["current_irr"]
  758. current_irr = (current_irr -1) *100
  759. # Round the nubers
  760. dict_big_numbers["current_value"] = str(round(current_value, 0))
  761. dict_big_numbers["total_performanance"] = str(round(total_performanance, 0))
  762. dict_big_numbers["current_irr"] = str(round(current_irr, 2))
  763. # Catching false inputs for the series to show
  764. possible_series_to_show = list(history_to_show[index_date_iso].keys()) # Get a list of all the series values, that could be shown
  765. 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
  766. logging(logging_level="warning")
  767. logging(logging_level="warning", message="Selecting 'total' as the series to show, as the input was not valid")
  768. series_to_show_1 = "total"
  769. 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
  770. logging(logging_level="warning")
  771. logging(logging_level="warning", message="Selecting 'total' as the series to show, as the input was not valid")
  772. series_to_show_2 = "total"
  773. # Catching false inputs for the data to show
  774. possible_data_to_show = list(history_to_show[index_date_iso][series_to_show_1].keys())
  775. if data_to_show_1 not in possible_data_to_show:
  776. logging(logging_level="warning")
  777. logging(logging_level="warning", message="Selecting 'current invested' as chart data, as the input was not valid")
  778. data_to_show_1 = "current_value"
  779. possible_data_to_show = list(history_to_show[index_date_iso][series_to_show_2].keys())
  780. if data_to_show_2 not in possible_data_to_show:
  781. logging(logging_level="warning")
  782. logging(logging_level="warning", message="Selecting 'current invested' as chart data, as the input was not valid")
  783. data_to_show_2 = "current_value"
  784. # Create space for storing values
  785. chart_1["data"] = []
  786. chart_2["data"] = []
  787. # Format the chart data into the right data
  788. for date in history_to_show:
  789. # Extract the value to be stored
  790. value_to_show_1 = history_to_show[date][series_to_show_1][data_to_show_1]
  791. value_to_show_2 = history_to_show[date][series_to_show_2][data_to_show_2]
  792. # Catch the case irr and convert to percent
  793. if data_to_show_1 == "current_irr":
  794. value_to_show_1 = (value_to_show_1 -1) *100
  795. if data_to_show_2 == "current_irr":
  796. value_to_show_2 = (value_to_show_2 -1) *100
  797. # Round to 2 decimal values
  798. value_to_show_1 = round(value_to_show_1, 2)
  799. value_to_show_2 = round(value_to_show_2, 2)
  800. # Extend the date by a timestamp
  801. json_date = datetime.date.fromisoformat(date) # Convert ISO-String to python date-object
  802. json_date = datetime.datetime.combine(json_date, datetime.datetime.min.time()) # Combine the date with midnight (00:00:00) to create a datetime object
  803. json_date = json_date.isoformat() # Convert back to ISO-String, now including a time
  804. # Store the values together with the corresponding date
  805. value_1 = [json_date, value_to_show_1]
  806. value_2 = [json_date, value_to_show_2]
  807. # Add the value pair to the list of values for this chart
  808. chart_1["data"].append(value_1)
  809. chart_2["data"].append(value_2)
  810. # Add the two series to the list of series in the TRML object
  811. charts_data.append(chart_1)
  812. charts_data.append(chart_2)
  813. # Generating nicer series titels
  814. if series_to_show_1 == "total":
  815. series_to_show_1 = "Portfolio"
  816. if series_to_show_2 == "total":
  817. series_to_show_2 = "Portfolio"
  818. # Generating nicer data titels
  819. data_to_show_1 = data_to_show_1.replace("_", " ").capitalize()
  820. data_to_show_2 = data_to_show_2.replace("_", " ").capitalize()
  821. # Increase look of IRR even more
  822. # Funktioniert auch dann, wenn "irr" nicht vorkommt
  823. data_to_show_1 = data_to_show_1.replace("irr", "IRR")
  824. data_to_show_2 = data_to_show_2.replace("irr", "IRR")
  825. # Generate the chat names / desciptions
  826. chart_1["name"] = data_to_show_1 + " " + series_to_show_1
  827. chart_2["name"] = data_to_show_2 + " " + series_to_show_2
  828. # Construct the trmnl_object
  829. trmnl_update_object = {}
  830. trmnl_update_object["merge_variables"] = {}
  831. trmnl_update_object["merge_variables"]["big_numbers"] = dict_big_numbers
  832. trmnl_update_object["merge_variables"]["charts"] = charts_data
  833. # Debugging
  834. if config.selected_logging_level == "debug":
  835. data = json.dumps(trmnl_update_object, indent=2) # Converts a python-dictionary into a json
  836. with open("trmnl_update_object.json", "w") as f:
  837. f.write(data)
  838. # Main Logging
  839. logging(logging_level="success")
  840. return trmnl_update_object
  841. except Exception as error_message:
  842. logging(logging_level="error")
  843. logging(logging_level="error", message=f"Failed with error: {error_message}")
  844. return False