functions.py 40 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134
  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. # ADD BENCHMARK-TICKER TO TICKER-DICT
  124. def add_benchmark_ticker(tickers, ticker_benchmarkt):
  125. tickers.append(ticker_benchmarkt)
  126. logging(logging_level="success")
  127. return tickers
  128. # CREATE BENCHMARK TRADES
  129. def create_benchmark_trades(trades, yf_data):
  130. # Prepertion
  131. benchmark_trades = {}
  132. i = 0
  133. # Creating benchmark trades
  134. try:
  135. for trade_id in trades:
  136. # Benchmark-id
  137. i = i+1
  138. benchmark_id = "benchmark" + str(i)
  139. # Copy raw trades
  140. benchmark_trades[benchmark_id] = trades[trade_id]
  141. benchmark_trades[benchmark_id]["ticker"] = config.ticker_benchmark
  142. # Calculate amount invested
  143. amount_invested = benchmark_trades[benchmark_id]["units"] * benchmark_trades[benchmark_id]["course_open"]
  144. # Change course-open for benchmark-ticker performance calculation
  145. success = False
  146. index_date = benchmark_trades[benchmark_id]["date_open"]
  147. while success == False:
  148. try:
  149. course_open_new = yf_data[config.ticker_benchmark].at[index_date, 'Close']
  150. success = True
  151. except:
  152. index_date = index_date + datetime.timedelta(days=1)
  153. benchmark_trades[benchmark_id]["course_open"] = course_open_new # type: ignore
  154. # Change amount for benchmark-ticker performance calculation
  155. benchmark_trades[benchmark_id]["units"] = amount_invested / course_open_new # type: ignore
  156. # Change course-open for benchmark-ticker performance calculation, if relevant
  157. if trades[trade_id]["date_close"] != 0:
  158. success = False
  159. index_date = benchmark_trades[benchmark_id]["date_close"]
  160. while success == False:
  161. try:
  162. course_close_new = yf_data[config.ticker_benchmark].at[index_date, 'Close']
  163. success = True
  164. except:
  165. index_date = index_date + datetime.timedelta(days=1)
  166. benchmark_trades[benchmark_id]["course_close"] = course_close_new # type: ignore
  167. # Logging
  168. logging(logging_level="success")
  169. return benchmark_trades
  170. except Exception as error_message:
  171. logging(logging_level="error")
  172. logging(logging_level="error", message=f"Failed with error: {error_message}")
  173. return False
  174. # MERGE BENCHMARK HISTORY & TICKER-HISTROY
  175. def merge_histories(history_per_ticker, benchmark_history):
  176. # Preperation
  177. benchmark_ticker = config.ticker_benchmark
  178. error_count = 0
  179. # Merging Data
  180. for index_date in history_per_ticker:
  181. try:
  182. history_per_ticker[index_date]["benchmark"] = {}
  183. history_per_ticker[index_date]["benchmark"]["current_invested"] = benchmark_history[index_date][benchmark_ticker]["current_invested"]
  184. history_per_ticker[index_date]["benchmark"]["total_dividends"] = benchmark_history[index_date][benchmark_ticker]["total_dividends"]
  185. history_per_ticker[index_date]["benchmark"]["current_value"] = benchmark_history[index_date][benchmark_ticker]["current_value"]
  186. history_per_ticker[index_date]["benchmark"]["current_irr"] = benchmark_history[index_date][benchmark_ticker]["current_irr"]
  187. history_per_ticker[index_date]["benchmark"]["total_performanance"] = benchmark_history[index_date][benchmark_ticker]["total_performanance"]
  188. except:
  189. error_count = error_count +1
  190. # Debugging
  191. if config.selected_logging_level == "debug":
  192. data = json.dumps(history_per_ticker, indent=2) # Converts a python-dictionary into a json
  193. with open("history_per_ticker_with_benchmark.json", "w") as f:
  194. f.write(data)
  195. # Main Logging
  196. if error_count == 0:
  197. logging(logging_level="success")
  198. return history_per_ticker
  199. else:
  200. logging(logging_level="warning")
  201. logging(logging_level="warning", message=f"Error merging benchmark-history into ticker-history in {error_count} cases")
  202. return False
  203. # -------------------------- #
  204. # NETWORK DOWNLOAD FUNCTIONS #
  205. # -------------------------- #
  206. # NOTION FETCH PAGES
  207. def notion_get_pages(db_id_trades, num_pages=None):
  208. try:
  209. # ------------------ FETCH THE FIRST 100 PAGES FROM A DB
  210. # Prepare Request
  211. url = f"https://api.notion.com/v1/databases/{db_id_trades}/query"
  212. get_all = num_pages is None # If num_pages is None, get all pages, otherwise just the defined number.
  213. page_size = 100 if get_all else num_pages
  214. payload = {"page_size": page_size}
  215. # Make Request
  216. raw_response = requests.post(url, json=payload, headers=config.notion_headers)
  217. # Process Reply
  218. parsed_response = raw_response.json()
  219. result = parsed_response["results"]
  220. # ------------------ FETCH 100 MORE PAGES AS OFTEN AS REQUIRED
  221. while parsed_response["has_more"] and get_all:
  222. # Prepare Request
  223. payload = {"page_size": page_size, "start_cursor": parsed_response["next_cursor"]}
  224. url = f"https://api.notion.com/v1/databases/{db_id_trades}/query"
  225. # Make Request
  226. raw_response = requests.post(url, json=payload, headers=config.notion_headers)
  227. # Process Reply
  228. parsed_response = raw_response.json()
  229. result.extend(parsed_response["results"])
  230. # Logging
  231. return result
  232. except Exception:
  233. return True # Return True when there was an error
  234. # NOTION FETCH & FORMAT TRADES
  235. def fetch_format_notion_trades(db_id_trades):
  236. trades = {}
  237. fetch_error = False
  238. format_errors = 0
  239. number_of_trades = 0
  240. error_message = ""
  241. # Download data from notion
  242. data = notion_get_pages(db_id_trades)
  243. # Check, if cuccessfull
  244. if data is True:
  245. fetch_error = True
  246. else:
  247. # Format the recieved data
  248. for i in data:
  249. # Count for stratistics
  250. number_of_trades = number_of_trades + 1
  251. # Each page is loaded as a dictionary
  252. notion_page = dict(i)
  253. # Handling desired missing entries
  254. try:
  255. date_close = notion_page["properties"]["Close"]["date"]
  256. date_close = date_close["start"]
  257. date_close = datetime.date(*map(int, date_close.split('-')))
  258. except:
  259. date_close = 0
  260. # Handeling non-desired missing entries (by skipping this trade)
  261. try:
  262. # Try extracting values
  263. trade = {}
  264. # Format date-open
  265. date_open = notion_page["properties"]["Open"]["date"]
  266. date_open = date_open["start"]
  267. date_open = datetime.date(*map(int, date_open.split('-')))
  268. # Combine data into json structure
  269. trade = {
  270. 'ticker' : notion_page["properties"]["Ticker"]["select"]["name"],
  271. 'date_open' : date_open,
  272. 'date_close' : date_close,
  273. 'course_open' : notion_page["properties"]["Open (€)"]["number"],
  274. 'course_close' : notion_page["properties"]["Close (€)"]["number"],
  275. 'course_current' : notion_page["properties"]["Current (€)"]["number"],
  276. 'irr' : notion_page["properties"]["IRR (%)"]["number"],
  277. 'units' : notion_page["properties"]["Units"]["number"],
  278. 'dividends' : notion_page["properties"]["Dividends (€)"]["number"]
  279. }
  280. # Save values
  281. notion_page_id = notion_page["id"] # Use as key for the dictionary
  282. trades[notion_page_id] = trade
  283. except Exception as e:
  284. format_errors = format_errors + 1
  285. error_message = e
  286. # Logging
  287. if fetch_error == True:
  288. logging(logging_level="error")
  289. logging(logging_level="error", message=f"Failed with error: {error_message}")
  290. return False
  291. else:
  292. # Writing Example File
  293. if config.selected_logging_level == "debug":
  294. with open("trades.json", "w") as f:
  295. f.write(str(trades))
  296. # Logging
  297. if format_errors == 0:
  298. logging(logging_level="success")
  299. logging(logging_level="info", message=f"{number_of_trades} trades recieved and formated")
  300. return trades
  301. else:
  302. logging(logging_level="warning")
  303. logging(logging_level="warning", message=f"{format_errors} trades out of {number_of_trades} skiped...maybe due to missing values?")
  304. return trades
  305. # NOTION FETCH & FORMAT INVESTMENT OVERVIEW
  306. def fetch_format_notion_investments(db_id_investments):
  307. investments = {}
  308. fetch_error = False
  309. format_errors = 0
  310. number_of_investments = 0
  311. # Download data & check for success
  312. data = notion_get_pages(db_id_investments)
  313. if data is True:
  314. error = True
  315. else:
  316. # Format recieved data
  317. for i in data:
  318. # Count up for statistics
  319. number_of_investments = number_of_investments + 1
  320. try:
  321. # Each page is loaded as a dictionary
  322. notion_page = dict(i)
  323. # Extract values
  324. notion_page_id = notion_page["id"] # Use as key for the dictionary
  325. investments[notion_page_id] = {}
  326. investments[notion_page_id]["ticker"] = notion_page["properties"]["Ticker"]["select"]["name"]
  327. investments[notion_page_id]["total_dividends"] = notion_page["properties"]["Dividends (€)"]["number"]
  328. investments[notion_page_id]["current_value"] = notion_page["properties"]["Current (€)"]["number"]
  329. investments[notion_page_id]["current_irr"] = notion_page["properties"]["IRR (%)"]["number"]
  330. investments[notion_page_id]["total_performanance"] = notion_page["properties"]["Performance (€)"]["number"]
  331. # Skip this entry, if errors show up
  332. except:
  333. format_errors = format_errors + 1
  334. # Main Logging
  335. if fetch_error == False & format_errors == 0:
  336. logging(logging_level="success")
  337. logging(logging_level="info", message=f"{number_of_investments} investments recieved and formated")
  338. return investments
  339. elif fetch_error == False & format_errors > 0:
  340. logging(logging_level="warning")
  341. logging(logging_level="warning", message=f"{format_errors} investments out of {number_of_investments} skiped...maybe due to missing values?")
  342. return investments
  343. else:
  344. logging(logging_level="error")
  345. return False
  346. # YFINANCE FETCH & FORMAT DATA
  347. def fetch_format_yf_data(tickers):
  348. yf_data = {}
  349. fetch_errors = 0
  350. format_errors = 0
  351. number_of_tickers = 0
  352. # Download data for each ticker seperately
  353. for i in tickers:
  354. number_of_tickers = number_of_tickers +1
  355. skip_formating = False # Helper varianbel (see flow logik)
  356. ticker = i
  357. # Catch errors during the download
  358. try:
  359. # Download data
  360. api = yf.Ticker(ticker)
  361. data = api.history(period="max", auto_adjust=False)
  362. except:
  363. # Store error for later logging
  364. fetch_errors = fetch_errors + 1
  365. data = True
  366. # If the download was successfull:
  367. if skip_formating == False:
  368. # Try formating the data
  369. try:
  370. # Convert to Pandas DataFrame
  371. data = pd.DataFrame(data) # type: ignore
  372. # Delete the columns "Stock Splits", "High", "Low" and "Open"
  373. del data['Open']
  374. del data['Low']
  375. del data['High']
  376. del data['Volume']
  377. # Delete these 2 columns, if they exist
  378. if 'Stock Splits' in data.columns:
  379. del data['Stock Splits']
  380. if 'Capital Gains' in data.columns:
  381. del data['Capital Gains']
  382. # Get the Number of rows in data
  383. data_rows = data.shape[0]
  384. # Create new index without the time from the existing datetime64-index
  385. old_index = data.index
  386. new_index = []
  387. x = 0
  388. while x < data_rows:
  389. date = pd.Timestamp.date(old_index[x]) # Converts the "Pandas Timestamp"-object to a "date" object
  390. new_index.append(date)
  391. x+=1
  392. # Add the new index to the dataframe and set it as the index
  393. data.insert(1, 'Date', new_index)
  394. data.set_index('Date', inplace=True)
  395. # Save the data-frame to the yf_data dict
  396. yf_data[ticker] = data
  397. # Handle formating errors
  398. except:
  399. format_errors = format_errors +1
  400. # in case of an error the entry never get's added to the yf_data object
  401. # Wait for the API to cool down
  402. print(".", end="", flush=True)
  403. time.sleep(config.api_cooldowm_time)
  404. # Main Logging
  405. print(" ", end="", flush=True)
  406. if fetch_errors == 0 & format_errors == 0:
  407. logging(logging_level="success")
  408. logging(logging_level="info", message=f"{number_of_tickers} tickers recieved and formated")
  409. return yf_data
  410. elif fetch_errors == 0 & format_errors > 0:
  411. logging(logging_level="warning")
  412. logging(logging_level="warning", message=f"{format_errors} tickers out of {number_of_tickers} skiped")
  413. return yf_data
  414. else:
  415. logging(logging_level="error")
  416. logging(logging_level="error", message=f"Failed with error: {number_of_tickers}")
  417. print("\n")
  418. return False
  419. # ------------------------ #
  420. # NETWORK UPLOAD FUNCTIONS #
  421. # ------------------------ #
  422. # NOTION UPDATE PAGES
  423. def notion_update_page(page_id: str, data: dict):
  424. url = f"https://api.notion.com/v1/pages/{page_id}"
  425. payload = {"properties": data}
  426. results = requests.patch(url, json=payload, headers=config.notion_headers)
  427. return results
  428. # UPDATE NOTION-TRADES-DATABASE
  429. def push_notion_trades_update(trades):
  430. # Logging
  431. error_count = 0
  432. number_of_uploads = 0
  433. for notion_page_id in trades:
  434. number_of_uploads = number_of_uploads+1
  435. try:
  436. # The irr is stored in the format 1.2534
  437. # Notion need the format 0,2534
  438. irr_notion = trades[notion_page_id]['irr'] - 1
  439. irr_notion = round(irr_notion, 4)
  440. # Construct Notion-Update-Object
  441. notion_update = {
  442. "Current (€)": {
  443. "number": trades[notion_page_id]['course_current']
  444. },
  445. "IRR (%)": {
  446. "number": irr_notion
  447. },
  448. "Dividends (€)": {
  449. "number": trades[notion_page_id]['dividends']
  450. }
  451. }
  452. # Update the properties of the corresponding notion-page
  453. notion_update_page(notion_page_id, notion_update)
  454. except:
  455. error_count = error_count + 1
  456. # Wait for the API to cool off
  457. print(".", end="", flush=True)
  458. time.sleep(config.api_cooldowm_time)
  459. # Logging
  460. print(" ", end="", flush=True)
  461. if error_count == 0:
  462. logging(logging_level="success")
  463. elif error_count < number_of_uploads:
  464. logging(logging_level="warning")
  465. logging(logging_level="success", message=f"Updating notion trades failed for {error_count} out of {number_of_uploads} entries")
  466. else:
  467. logging(logging_level="error")
  468. logging(logging_level="success", message=f"Updating notion trades failed for all {error_count} entries")
  469. # UPDATE NOTION-INVESTMENT-OVERVIEW
  470. def push_notion_investment_update(investments):
  471. # Logging
  472. error_count = 0
  473. number_of_uploads = 0
  474. for notion_page_id in investments:
  475. number_of_uploads = number_of_uploads+1
  476. # Try uploading an update
  477. try:
  478. # The irr is stored in the format 1.2534
  479. # Notion need the format 0,2534
  480. irr_notion = investments[notion_page_id]['current_irr'] - 1
  481. irr_notion = round(irr_notion, 4)
  482. # Construct Notion-Update-Object
  483. notion_update = {
  484. "Current (€)": {
  485. "number": investments[notion_page_id]['current_value']
  486. },
  487. "IRR (%)": {
  488. "number": irr_notion
  489. },
  490. "Performance (€)": {
  491. "number": investments[notion_page_id]['total_performanance']
  492. },
  493. "Dividends (€)": {
  494. "number": investments[notion_page_id]['total_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(config.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 investments 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 investments failed for all {error_count} entries")
  514. # TRMNL UPDATE DIAGRAMMS
  515. def push_trmnl_update_chart(trmnl_update_object, trmnl_url, trmnl_headers):
  516. # Send the data to TRMNL
  517. try:
  518. data = json.dumps(trmnl_update_object, indent=2) # Converts a python-dictionary into a json
  519. reply = requests.post(trmnl_url, data=data, headers=trmnl_headers)
  520. # Logging
  521. if reply.status_code == 200:
  522. logging(logging_level="success")
  523. elif reply.status_code == 429:
  524. logging_level="success"
  525. logging(logging_level="warning")
  526. logging(logging_level="warning", message="Exceeded TRMNL's API rate limits")
  527. logging(logging_level="warning", message="Waiting some time should work")
  528. elif reply.status_code == 422:
  529. logging(logging_level="warning")
  530. logging(logging_level="warning", message="Upload successful, but data cannot be displayed correctly")
  531. logging(logging_level="warning", message="The payload is probably to large in size")
  532. else:
  533. logging(logging_level="error")
  534. logging(logging_level="error", message=f"Failed pushing data to TRMNL with server reply code: {reply.status_code}")
  535. logging(logging_level="debug", message=f"Complete server reply message: {reply}")
  536. except Exception as e:
  537. logging(logging_level="error")
  538. logging(logging_level="error", message=f"Failed pushing data to TRMNL with error code: {e}")
  539. # ----------------------------- #
  540. # HISTORY CALCULATION FUNCTIONS #
  541. # ----------------------------- #
  542. # CALC HISTORY PER TRADE
  543. def calc_history_per_trade(trades, yf_data):
  544. # Create support variables
  545. history_per_trade = {}
  546. total_dividends = 0
  547. date_open_oldest_trade = get_date_open_oldest_trade(trades)
  548. # Logging & statistics
  549. missing_day_entrys = 0
  550. days_formated = 0
  551. number_of_trades = 0
  552. # As this history is so important, it is okay if this functions fails in total if errors araise
  553. try:
  554. # ------------------ LOOP OVER ALL TRADES
  555. for trade_id in trades:
  556. # Statistics
  557. number_of_trades = number_of_trades +1
  558. # ------------------ PREPARE FOR THE (NEXT) LOOP OVER ALL DAYS
  559. # Set / Reset the index-date to the oldest trade day
  560. # Resetting is required so that the calculations for the next trade start with day 1
  561. index_date = date_open_oldest_trade
  562. # Set the initial value for the course on the previous day to 0
  563. # Just in case the very first trade was made on a weekend somehow, where there is no yfinance data available
  564. previous_course = 0.0
  565. # Check, if the trade was closed already
  566. # If it was not, set the closure date to the future (Trick 17)
  567. if trades[trade_id]["date_close"] == 0:
  568. date_close = datetime.date.today() + datetime.timedelta(days=1)
  569. else:
  570. date_close = trades[trade_id]["date_close"]
  571. date_open = trades[trade_id]["date_open"]
  572. # Keep ticker for connecting performance later
  573. ticker = trades[trade_id]['ticker']
  574. # ------------------ DETERMINE THE COUSE PER DAY
  575. while index_date != datetime.date.today() + datetime.timedelta(days=1):
  576. # Statistics
  577. days_formated = days_formated +1
  578. # Fetch course for the day & eventual dividends from yf_data
  579. try:
  580. current_course = yf_data[ticker].at[index_date, 'Close']
  581. current_dividends_per_ticker = yf_data[ticker].at[index_date, 'Dividends']
  582. # Catch missing yf-data (eg. for weekends) by reusing course from previous day
  583. except:
  584. current_course = previous_course
  585. current_dividends_per_ticker = 0.0 # there are never dividends on non-trading days
  586. missing_day_entrys = missing_day_entrys +1 # Increase the warning count
  587. # Catch the special case of the day when the trade was closed
  588. # In this case, the current course needs to be overwritten with the sell-value
  589. if date_close == index_date:
  590. current_course = trades[trade_id]['course_close']
  591. # Save the result for the next iteration
  592. # This setup also makes it possible, that a previous course is passed down across mutiple days
  593. # This makes sense is case i.e. for a weekend
  594. previous_course = current_course
  595. # ------------------ CALCULATE PERFORMANCE IF REQUIRED
  596. if index_date >= date_open and index_date <= date_close:
  597. # Calculate performance values
  598. current_amount = trades[trade_id]['units']
  599. current_invested = current_amount * trades[trade_id]['course_open']
  600. total_dividends = total_dividends + current_amount * current_dividends_per_ticker
  601. current_value = current_amount * current_course
  602. current_value_with_dividends = current_value + total_dividends
  603. current_irr = calculate_irr(index_date, date_open, current_value_with_dividends, current_invested)
  604. total_performanance = current_value_with_dividends - current_invested
  605. if current_value_with_dividends == 0:
  606. print("0-value Error with ticker: {}".format(ticker))
  607. else:
  608. # Write 0, if trade is not relevant for current timeframe
  609. current_amount = 0
  610. current_invested = 0.00
  611. total_dividends = 0.00
  612. current_value = 0.00
  613. current_irr = 0.00
  614. total_performanance = 0.0
  615. # ------------------ STORE RESULTS
  616. index_date_iso = index_date.isoformat()
  617. # Store all values into a dict
  618. dict_a = {}
  619. dict_a['current_amount'] = current_amount
  620. dict_a['current_invested'] = current_invested
  621. dict_a['total_dividends'] = total_dividends
  622. dict_a['current_value'] = current_value
  623. dict_a['current_irr'] = current_irr
  624. dict_a['current_course'] = current_course
  625. dict_a['total_performanance'] = total_performanance
  626. # Check if the date is already present
  627. if index_date_iso in history_per_trade:
  628. dict_b = history_per_trade[index_date_iso]
  629. else:
  630. dict_b = {}
  631. # Add the values to the trade_id value-pair
  632. dict_b[trade_id] = dict_a
  633. # Update the hostory_per_trade
  634. history_per_trade.update({index_date_iso : dict_b})
  635. # ------------------ NEXT ITERATION
  636. index_date = index_date + datetime.timedelta(days=1)
  637. # ------------------ LOGGING & DEBUGING
  638. # Debug writing history to disk
  639. if config.selected_logging_level == "debug":
  640. data = json.dumps(history_per_trade, indent=2) # Converts a python-dictionary into a json
  641. with open("history_per_trade.json", "w") as f:
  642. f.write(data)
  643. # Logging logging
  644. if missing_day_entrys == 0:
  645. logging(logging_level="success")
  646. logging(logging_level="info", message=f"created a history with {days_formated} across all {number_of_trades} tickers o_O")
  647. else:
  648. logging(logging_level="warning")
  649. logging(logging_level="warning", message=f"No yf-data available in {missing_day_entrys} cases accross all {number_of_trades} tickers")
  650. logging(logging_level="warning", message="Probably reason is non-trading-days eg. weekends")
  651. logging(logging_level="warning", message="Used values from previous trade-day instead")
  652. # Return date
  653. return history_per_trade
  654. except Exception as error_message:
  655. logging(logging_level="error")
  656. logging(logging_level="error", message=f"Failed with error message: {error_message}")
  657. return False
  658. # CALC THE HISTORY PER TRADE & OVERALL
  659. def calc_history_per_ticker(history_per_trade, tickers, trades):
  660. # ------------------ CREATE JSON OBJECT
  661. # Create the json-dict
  662. history_per_ticker = {}
  663. # Logging & statistics
  664. missing_day_entrys = 0
  665. days_formated = 0
  666. # As this history is so important, it is okay if this functions fails in total if errors araise
  667. try:
  668. # Loop over each date entry in the history
  669. for date_entry in history_per_trade:
  670. # Statistics
  671. days_formated = days_formated +1
  672. # Create a dict to store the results per day and ticker
  673. dict_daily = {}
  674. for ticker in tickers:
  675. dict_daily[ticker] = {}
  676. dict_daily[ticker]["current_invested"] = 0
  677. dict_daily[ticker]["total_dividends"] = 0
  678. dict_daily[ticker]["current_value"] = 0
  679. dict_daily[ticker]["current_irr"] = 0
  680. dict_daily[ticker]["current_irr"] = 0
  681. dict_daily[ticker]["total_performanance"] = 0
  682. dict_daily[ticker]["current_amount"] = 0 # Added only for ticker entries, not for the "total" value
  683. dict_daily[ticker]["current_course"] = 0 # Added only for ticker entries, not for the "total" value
  684. dict_daily["total"] = {}
  685. dict_daily["total"]["current_invested"] = 0
  686. dict_daily["total"]["total_dividends"] = 0
  687. dict_daily["total"]["current_value"] = 0
  688. dict_daily["total"]["current_irr"] = 0
  689. dict_daily["total"]["total_performanance"] = 0
  690. # Loop over each trade-entry for that day
  691. for trade_id in history_per_trade[date_entry]:
  692. # Extract data from the history_per_trade
  693. trade_amount = history_per_trade[date_entry][trade_id]['current_amount']
  694. trade_invested = history_per_trade[date_entry][trade_id]['current_invested']
  695. trade_dividends = history_per_trade[date_entry][trade_id]['total_dividends']
  696. trade_value = history_per_trade[date_entry][trade_id]['current_value']
  697. trade_irr = history_per_trade[date_entry][trade_id]['current_irr']
  698. trade_course = history_per_trade[date_entry][trade_id]['current_course']
  699. trade_performanance = history_per_trade[date_entry][trade_id]['total_performanance']
  700. # Lookup the ticker by the trade-id
  701. ticker = trades[trade_id]["ticker"]
  702. # Extract data from the history_per_ticker
  703. ticker_amount = dict_daily[ticker]['current_amount']
  704. ticker_invested = dict_daily[ticker]['current_invested']
  705. ticker_dividends = dict_daily[ticker]['total_dividends']
  706. ticker_value = dict_daily[ticker]['current_value']
  707. ticker_irr = dict_daily[ticker]['current_irr']
  708. ticker_performanance = dict_daily[ticker]['total_performanance']
  709. # Overwrite the values in the history_per_ticker
  710. dict_daily[ticker]['current_amount'] = ticker_amount + trade_amount # Simple addition works
  711. dict_daily[ticker]['current_invested'] = ticker_invested + trade_invested
  712. dict_daily[ticker]['total_dividends'] = ticker_dividends + trade_dividends
  713. dict_daily[ticker]['current_value'] = ticker_value + trade_value
  714. dict_daily[ticker]['total_performanance'] = ticker_performanance + trade_performanance
  715. dict_daily[ticker]['current_course'] = trade_course # Simple overwrite is fine, as the course is the same for all trades
  716. if ticker_invested == 0 and trade_invested == 0:
  717. dict_daily[ticker]['current_irr'] = 0
  718. # Catch 0 values
  719. else:
  720. dict_daily[ticker]['current_irr'] = (ticker_irr * ticker_invested + trade_irr * trade_invested) / (ticker_invested + trade_invested)
  721. # --> 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
  722. # Calculate the "total" entry after finishing with all the trades
  723. for ticker in tickers:
  724. # Same logic as above, but shortended code
  725. dict_daily["total"]['total_dividends'] = dict_daily["total"]['total_dividends'] + dict_daily[ticker]['total_dividends']
  726. dict_daily["total"]['current_value'] = dict_daily["total"]['current_value'] + dict_daily[ticker]['current_value']
  727. dict_daily["total"]['total_performanance'] = dict_daily["total"]['total_performanance'] + dict_daily[ticker]['total_performanance']
  728. # Extracting the values before rewriting them, to preserve them for the IRR calculation
  729. total_invested = dict_daily["total"]['current_invested']
  730. ticker_invested = dict_daily[ticker]['current_invested']
  731. dict_daily["total"]['current_invested'] = total_invested + ticker_invested
  732. # Extracting the values before rewriting them, to preserve them for the IRR calculation
  733. if ticker_invested == 0 and total_invested == 0:
  734. dict_daily["total"]['current_irr'] = 0
  735. else:
  736. total_irr = dict_daily["total"]['current_irr']
  737. ticker_irr = dict_daily[ticker]['current_irr']
  738. dict_daily["total"]['current_irr'] = (total_irr * total_invested + ticker_irr * ticker_invested) / (total_invested + ticker_invested)
  739. # Finally, write the results for this day-entry to the history_per_ticker
  740. history_per_ticker[date_entry] = dict_daily
  741. # ------------------ LOGGING & DEBUGING
  742. # Debugging
  743. if config.selected_logging_level == "debug":
  744. data = json.dumps(history_per_ticker, indent=2) # Converts a python-dictionary into a json
  745. with open("history_per_ticker.json", "w") as f:
  746. f.write(data)
  747. # Success Logging
  748. logging(logging_level="success")
  749. logging(logging_level="info", message=f"created a history with {days_formated} days formated o_O")
  750. return history_per_ticker
  751. # Error Logging
  752. except Exception as error_message:
  753. logging(logging_level="error")
  754. logging(logging_level="error", message=f"Failed with error message: {error_message}")
  755. return False
  756. # --------------------------- #
  757. # HISTORY SELECTION FUNCTIONS #
  758. # --------------------------- #
  759. # FILTER ANY HISTORY OBJECT TO SELECTED DATES
  760. def filter_history_by_list(history, dates_list):
  761. filtered_history = {}
  762. try:
  763. # Loop over all days
  764. for index_date in history:
  765. # Check, if the history-date is in the filter-list
  766. if index_date in dates_list:
  767. # If so, add this date-entry to the filtered history object
  768. filtered_history[index_date] = history[index_date]
  769. # Main Logging
  770. logging(logging_level="success")
  771. return filtered_history
  772. except Exception as error_message:
  773. logging(logging_level="error")
  774. logging(logging_level="error", message=f"Failed with error: {error_message}")
  775. return False
  776. # SELECT CURRENT VALUES PER TRADE
  777. def select_current_value_per_trade(trades, history_per_trade):
  778. # Logging
  779. format_errors = 0
  780. # Loop over all trades
  781. for trade_id in trades:
  782. try:
  783. # Determine, what values to fetch based on whether the trade was closed already
  784. date_closed = trades[trade_id]["date_close"]
  785. if date_closed == 0:
  786. # If trade still open, use performance data from today
  787. index_date_iso = datetime.date.today().isoformat()
  788. else:
  789. # If trade closed, use performance data from close-date
  790. index_date_iso = date_closed.isoformat()
  791. # Fetch data from history and save for this trade
  792. trades[trade_id]["course_current"] = history_per_trade[index_date_iso][trade_id]['current_course']
  793. trades[trade_id]["irr"] = history_per_trade[index_date_iso][trade_id]['current_irr']
  794. trades[trade_id]["dividends"] = history_per_trade[index_date_iso][trade_id]['total_dividends']
  795. except:
  796. format_errors = format_errors + 1
  797. # Logging logging
  798. if format_errors == 0:
  799. logging(logging_level="success")
  800. else:
  801. logging(logging_level="warning")
  802. logging(logging_level="warning", message=f"Failed updating the current value per trade in {format_errors} cases")
  803. return trades
  804. # SELECT CURRENT VALUES PER TICKER
  805. def select_current_value_per_ticker(investments, history_per_ticker):
  806. # Logging
  807. format_errors = 0
  808. # Loop over all investments
  809. for investment_id in investments:
  810. try:
  811. # Generate the iso-date of today as the required index
  812. index_date_iso = datetime.date.today().isoformat()
  813. # Get the ticker corresponding to the investment
  814. ticker = investments[investment_id]["ticker"]
  815. # Select latest data from history and save for this investment
  816. investments[investment_id]["total_dividends"] = history_per_ticker[index_date_iso][ticker]['total_dividends']
  817. investments[investment_id]["current_value"] = history_per_ticker[index_date_iso][ticker]['current_value']
  818. investments[investment_id]["current_irr"] = history_per_ticker[index_date_iso][ticker]['current_irr']
  819. investments[investment_id]["total_performanance"] = history_per_ticker[index_date_iso][ticker]['total_performanance']
  820. except:
  821. format_errors = format_errors + 1
  822. # Logging
  823. if format_errors == 0:
  824. logging(logging_level="success")
  825. else:
  826. logging(logging_level="warning")
  827. logging(logging_level="warning", message=f"Failed updating the current value per ticker in {format_errors} cases")
  828. return investments
  829. # TRMNL CREATE IRR-UPDATE
  830. 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
  831. # Setup
  832. dict_big_numbers = {}
  833. charts_data = []
  834. chart_1 = {}
  835. chart_2 = {}
  836. try:
  837. # Fetch the latest date entry from the history
  838. index_date_iso = fetch_last_key_from_dict(history_to_show)
  839. # Select latest data from history for the big-numbers
  840. current_value = history_to_show[index_date_iso]["total"]["current_value"]
  841. total_performanance = history_to_show[index_date_iso]["total"]["total_performanance"]
  842. current_irr = history_to_show[index_date_iso]["total"]["current_irr"]
  843. current_irr = (current_irr -1) *100
  844. # Round the nubers
  845. dict_big_numbers["current_value"] = str(round(current_value, 0))
  846. dict_big_numbers["total_performanance"] = str(round(total_performanance, 0))
  847. dict_big_numbers["current_irr"] = str(round(current_irr, 2))
  848. # Catching false inputs for the series to show
  849. possible_series_to_show = list(history_to_show[index_date_iso].keys()) # Get a list of all the series values, that could be shown
  850. 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
  851. logging(logging_level="warning")
  852. logging(logging_level="warning", message="Selecting 'total' as the series to show, as the input was not valid")
  853. series_to_show_1 = "total"
  854. 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
  855. logging(logging_level="warning")
  856. logging(logging_level="warning", message="Selecting 'total' as the series to show, as the input was not valid")
  857. series_to_show_2 = "total"
  858. # Catching false inputs for the data to show
  859. possible_data_to_show = list(history_to_show[index_date_iso][series_to_show_1].keys())
  860. if data_to_show_1 not in possible_data_to_show:
  861. logging(logging_level="warning")
  862. logging(logging_level="warning", message="Selecting 'current invested' as chart data, as the input was not valid")
  863. data_to_show_1 = "current_value"
  864. possible_data_to_show = list(history_to_show[index_date_iso][series_to_show_2].keys())
  865. if data_to_show_2 not in possible_data_to_show:
  866. logging(logging_level="warning")
  867. logging(logging_level="warning", message="Selecting 'current invested' as chart data, as the input was not valid")
  868. data_to_show_2 = "current_value"
  869. # Create space for storing values
  870. chart_1["data"] = []
  871. chart_2["data"] = []
  872. # Format the chart data into the right data
  873. for date in history_to_show:
  874. # Extract the value to be stored
  875. value_to_show_1 = history_to_show[date][series_to_show_1][data_to_show_1]
  876. value_to_show_2 = history_to_show[date][series_to_show_2][data_to_show_2]
  877. # Catch the case irr and convert to percent
  878. if data_to_show_1 == "current_irr":
  879. value_to_show_1 = (value_to_show_1 -1) *100
  880. if data_to_show_2 == "current_irr":
  881. value_to_show_2 = (value_to_show_2 -1) *100
  882. # Round to 2 decimal values
  883. value_to_show_1 = round(value_to_show_1, 2)
  884. value_to_show_2 = round(value_to_show_2, 2)
  885. # Extend the date by a timestamp
  886. json_date = datetime.date.fromisoformat(date) # Convert ISO-String to python date-object
  887. json_date = datetime.datetime.combine(json_date, datetime.datetime.min.time()) # Combine the date with midnight (00:00:00) to create a datetime object
  888. json_date = json_date.isoformat() # Convert back to ISO-String, now including a time
  889. # Store the values together with the corresponding date
  890. value_1 = [json_date, value_to_show_1]
  891. value_2 = [json_date, value_to_show_2]
  892. # Add the value pair to the list of values for this chart
  893. chart_1["data"].append(value_1)
  894. chart_2["data"].append(value_2)
  895. # Add the two series to the list of series in the TRML object
  896. charts_data.append(chart_1)
  897. charts_data.append(chart_2)
  898. # Generating nicer series titels
  899. if series_to_show_1 == "total":
  900. series_to_show_1 = "Portfolio"
  901. if series_to_show_2 == "total":
  902. series_to_show_2 = "Portfolio"
  903. if series_to_show_1 == "benchmark":
  904. series_to_show_1 = "Benchmark: " + config.ticker_benchmark
  905. if series_to_show_2 == "benchmark":
  906. series_to_show_2 = "Benchmark: " + config.ticker_benchmark
  907. # Generating nicer data titels
  908. data_to_show_1 = data_to_show_1.replace("_", " ").capitalize()
  909. data_to_show_2 = data_to_show_2.replace("_", " ").capitalize()
  910. # Increase look of IRR even more
  911. # Funktioniert auch dann, wenn "irr" nicht vorkommt
  912. data_to_show_1 = data_to_show_1.replace("irr", "IRR")
  913. data_to_show_2 = data_to_show_2.replace("irr", "IRR")
  914. # Generate the chat names / desciptions
  915. chart_1["name"] = data_to_show_1 + " " + series_to_show_1
  916. chart_2["name"] = data_to_show_2 + " " + series_to_show_2
  917. # Construct the trmnl_object
  918. trmnl_update_object = {}
  919. trmnl_update_object["merge_variables"] = {}
  920. trmnl_update_object["merge_variables"]["big_numbers"] = dict_big_numbers
  921. trmnl_update_object["merge_variables"]["charts"] = charts_data
  922. # Debugging
  923. if config.selected_logging_level == "debug":
  924. data = json.dumps(trmnl_update_object, indent=2) # Converts a python-dictionary into a json
  925. with open("trmnl_update_object.json", "w") as f:
  926. f.write(data)
  927. # Main Logging
  928. logging(logging_level="success")
  929. return trmnl_update_object
  930. except Exception as error_message:
  931. logging(logging_level="error")
  932. logging(logging_level="error", message=f"Failed with error: {error_message}")
  933. return False