Cryptocurrency is a digital or virtual currency that uses cryptography to secure and verify transactions and our Cryptocurrency tracker is a tool that helps in monitoring the exchange rates of different currencies in the market.
The objective of this project is to develop a cryptocurrency tracker with an underlying stateless cache based on an sqlite3 database.
An API provides us the exchange rates of INR, EUR, GBP, DOGE and LTC against USD to compare the target exchange rates for DOGE and LTC with the current exchange rates to determine whether it is profitable to buy DOGE or LTC in your home currency.
When the program fetches the currency rates from an external API, it caches them in a sqlite3 database for future use.
The database is used as a stateless cache for the rates, so that the program doesn't have to request it from the API again every time it runs.
In this program, the caching is stateless, which means that the server hosting the API doesn't store the cached rates, instead we store it locally in an sqlite3 database This approach has a few advantages as it reduces the load on the API host by only making requests when necessary, it speeds up the program by avoiding unnecessary requests and database writes.
The project is developed in Python programming language and the following Python libraries have been used in our project:
import | application |
---|---|
sqlite3 | to create and manage the cache for exchange rates. |
requests | to send HTTPS requests to the API. |
prettytable | to display the exchange rates in a table. |
matplotlib | to plot the exchange rates in a graph. |
tkinter | bindings to the TK GUI toolkit |
A Cryptocurrency tracker using stateless cache has the following applications in various industries:
- Finance: The tracker can be used by financial analysts to monitor the exchange rates of different currencies and make informed decisions regarding investments.
- E-commerce: The tracker can be integrated with e-commerce websites to provide real-time exchange rates to customers and allow them to make purchases in different currencies.
- Travel: The tracker can be used by travelers to monitor the exchange rates of different currencies and make informed decisions regarding foreign currency exchange.
- Education: The tracker can be used as a learning tool to teach students about cryptocurrency and how it works. The tracker can also be used to teach students about exchange rates and how they fluctuate over time.
import datetime as dt
import sqlite3
import requests
import prettytable
import matplotlib.pyplot as plt
import numpy as np
class backend:
def __init__(self, homeCurrency: str, numOfDOGEToBuy: float, moneyToBuyDOGE: float, numOfLTCToBuy: float, moneyToBuyLTC: float) -> None:
self.homeCurrency: str = homeCurrency
self.numOfDOGEToBuy: float = numOfDOGEToBuy
self.moneyToBuyDOGE: float = moneyToBuyDOGE
self.numOfLTCToBuy: float = numOfLTCToBuy
self.moneyToBuyLTC: float = moneyToBuyLTC
print(f"constructed backend object with {homeCurrency}, {numOfDOGEToBuy}, {moneyToBuyDOGE}, {numOfLTCToBuy}, {moneyToBuyLTC}\n")
def fetchRates(self, date: str = "latest") -> dict[str, str | float]:
url: str = f"https://api.exchangerate.host/{date}"
response: requests.Response = requests.get(url, params={"base": "USD", "symbols": "INR,EUR,GBP", "places": 4}, timeout=10)
data: dict = response.json()
rates: dict[str, float] = data["rates"]
entry: dict[str, str | float] = {
"time": data["date"],
"INR": rates["INR"],
"EUR": rates["EUR"],
"GBP": rates["GBP"],
}
response = requests.get(url, params={"base": "USD", "source": "crypto", "symbols": "DOGE,LTC"}, timeout=10)
data = response.json()
rates = data["rates"]
entry["DOGE"] = rates["DOGE"]
entry["LTC"] = rates["LTC"]
print(f"\nGET: {url}: {entry}\n")
return entry
def connect2cache(self) -> tuple[sqlite3.Cursor, sqlite3.Connection]:
cachedRatesdb: sqlite3.Connection = sqlite3.connect("cachedRates.db")
cursor: sqlite3.Cursor = cachedRatesdb.cursor()
cursor.execute(
"""CREATE TABLE IF NOT EXISTS cache (
timestamp text,
INR real,
EUR real,
GBP real,
DOGE real,
LTC real
)"""
)
return cursor, cachedRatesdb
def compareTarget(self) -> dict[str, bool]:
# self.test(1)
cursor: sqlite3.Cursor
cachedRatesdb: sqlite3.Connection
cursor, cachedRatesdb = self.connect2cache()
today: str = str(dt.date.today() - dt.timedelta(days=1))
cursor.execute("SELECT timestamp FROM cache")
timestamps: list[str] = [row[0] for row in cursor.fetchall()]
if today in timestamps:
cursor.execute(f"SELECT * FROM cache WHERE timestamp = '{today}'")
row = cursor.fetchone()
rates = {"time": row[0], "INR": row[1], "EUR": row[2], "GBP": row[3], "DOGE": row[4], "LTC": row[5]}
print(f"rates for {today} already in sqlite3 cache {rates}")
else:
rates: dict[str, str | float] = self.fetchRates()
cursor.execute(f"INSERT INTO cache VALUES ('{rates['time']}', {rates['INR']}, {rates['EUR']}, {rates['GBP']}, {rates['DOGE']}, {rates['LTC']})")
print(f"cached {rates}")
res: dict[str, bool] = {"DOGE": False, "LTC": False}
if self.homeCurrency != "USD":
res["DOGE"] = self.moneyToBuyDOGE / (rates[self.homeCurrency] * self.numOfDOGEToBuy) >= rates["DOGE"]
res["LTC"] = self.moneyToBuyLTC / (rates[self.homeCurrency] * self.numOfLTCToBuy) >= rates["LTC"]
else:
res["DOGE"] = self.moneyToBuyDOGE / self.numOfDOGEToBuy >= rates["DOGE"]
res["LTC"] = self.moneyToBuyLTC / self.numOfLTCToBuy >= rates["LTC"]
cachedRatesdb.close()
return res
def ratesInThePast(self) -> list[dict[str, str | float]]:
cursor: sqlite3.Cursor
cachedRatesdb: sqlite3.Connection
cursor, cachedRatesdb = self.connect2cache()
today: dt.date = dt.date.today()
ratesThisWeekAsListOfDicts: list[dict[str, str | float]] = list(dict())
cursor.execute("SELECT timestamp FROM cache")
timestamps: list[str] = [row[0] for row in cursor.fetchall()]
for i in range(14, 0, -1):
date = str(today - dt.timedelta(days=i))
if date not in timestamps:
ratesThisWeekAsListOfDicts.append(self.fetchRates(date))
else:
print(f"rates for {date} already cached")
cachedRatesdb.close()
return ratesThisWeekAsListOfDicts
def dbHandler(self) -> None:
cursor: sqlite3.Cursor
cachedRatesdb: sqlite3.Connection
cursor, cachedRatesdb = self.connect2cache()
pastRates: list[dict[str, str | float]] = self.ratesInThePast()
for dc in pastRates:
cursor.execute(f"INSERT INTO cache VALUES ('{dc['time']}', {dc['INR']}, {dc['EUR']}, {dc['GBP']}, {dc['DOGE']}, {dc['LTC']})")
print(f"cached {dc}")
cachedRatesdb.commit()
cachedRatesdb.close()
self.printCACHE()
def printCACHE(self) -> None:
cursor: sqlite3.Cursor
cachedRatesdb: sqlite3.Connection
cursor, cachedRatesdb = self.connect2cache()
cursor.execute("SELECT * FROM cache")
table: prettytable.PrettyTable | None = prettytable.from_db_cursor(cursor)
print(table)
cachedRatesdb.close()
def plot(self, coin: str) -> None:
cursor: sqlite3.Cursor
cachedRatesdb: sqlite3.Connection
cursor, cachedRatesdb = self.connect2cache()
cursor.execute(f"SELECT timestamp, {coin} FROM cache")
result: list[tuple[str, float]] = cursor.fetchall()
timestamps: np.ndarray[str, np.dtype[np.string_]] = np.array([result[0] for result in result])
coinRates: np.ndarray[float, np.dtype[np.float64]] = np.array([result[1] for result in result])
cachedRatesdb.close()
plt.style.use("dark_background")
plt.plot(timestamps, coinRates, color="#a01bf2")
plt.title(f"Historical Exchange Rate Of {coin} in USD")
plt.xlabel("Timestamps (in days)")
plt.ylabel(f"{coin}'s exchange rate (in USD)")
figManager: plt.FigureManagerBase = plt.get_current_fig_manager()
figManager.window.state("normal")
plt.show()
def test(self, rowsTBDel: int) -> None:
if rowsTBDel < 1:
print("Bad Input: cache unchanged")
return
cursor: sqlite3.Cursor
cachedRatesdb: sqlite3.Connection
cursor, cachedRatesdb = self.connect2cache()
cursor.execute("SELECT COUNT(*) FROM cache")
num_rows: int = cursor.fetchone()[0]
if num_rows >= rowsTBDel:
cursor.execute(f"DELETE FROM cache WHERE ROWID IN (SELECT ROWID FROM cache ORDER BY ROWID DESC LIMIT {rowsTBDel})")
cachedRatesdb.commit()
print(f"Last {rowsTBDel} rows successfully deleted")
cachedRatesdb.commit()
self.printCACHE()
else:
print(f"There are not enough rows in cache to delete the last {rowsTBDel} rows")
cachedRatesdb.close()
if __name__ == "__main__":
instance = backend("INR", -1, -1, -1, -1)
backend.dbHandler(self=instance)
backend.compareTarget(self=instance)
- constructor for the backend class requires:
homeCurrency: str
can be"INR"
,"USD"
,"EUR"
,"GBP"
numOfDOGEToBuy: float
is the number of DOGE coins you aim to buy formoneyToBuyDOGE: float
money in your home currencynumOfLTCToBuy: float
is the number of DOGE coins you aim to buy formoneyToBuyLTC: float
money in your home currency
fetchRates(self, date: str = "latest") -> dict[str, str | float]
- returns the latest rates when called without any arguments otherwise can return rates for a specific date provided as a string in the format
"YYYY-MM-DD"
- from API hosted on the domain
https://api.exchangerate.host/<date>
- from API hosted on the domain
- sends 2 GET requests
- one asks for the rates of Fiat Currencies INR, EUR, GBP against USD
- another asks for rates of Crypto Currencies Dogecoin and Litecoin against USD
- it extracts the rates from the request object and appends it to a dictionary called
entry
that with a timestamp entry
is returned
- returns the latest rates when called without any arguments otherwise can return rates for a specific date provided as a string in the format
connect2cache(self) -> tuple[sqlite3.Cursor, sqlite3.Connection]
- creates the cache as a sqlite3 database if it doesn't exist already
- returns an opened connection to the cache and a cursor to the cache as objects
- decoupled to conform to the DRY principle
compareTarget(self) -> dict[str, bool]
- opens connection to the cache using
connect2cache(self) -> tuple[sqlite3.Cursor, sqlite3.Connection]
- extracts the entire column for timestamps from the cache and checks for the timestamp for the present day
- if the timestamp for the present day is not found, it calls
fetchRates(self, date: str = "latest") -> dict[str, str | float]
and caches the rates
- if the timestamp for the present day is not found, it calls
- then it makes precise calculations using both, the latest exchange rates between Crypto Currencies against USD and the exchange rates between your home currency against USD
- from these calculations it predicts if the current rate meets the requirement for buying a set number of tokens for a set amount of money passed during the creation of the object
- it returns a dictionary like
{"DOGE": True/False, "LTC": True/False}
after closing the connection to the cache
- opens connection to the cache using
ratesInThePast(self) -> list[dict[str, str | float]]
- opens connection to the cache using
connect2cache(self) -> tuple[sqlite3.Cursor, sqlite3.Connection]
- extracts the entire column for timestamps from the cache
- loops over the last 14 days using variable
date
- it calls
fetchRates(self, date: str = "latest") -> dict[str, str | float]
withstr = date
ifdate not in timestamps
- it calls
- it returns a list of rates that we fetched from
fetchRates()
(which is later cached bydbHandler(self) -> None
) after closing the connection to the cache
- opens connection to the cache using
dbHandler(self) -> None
- opens connection to the cache using
connect2cache(self) -> tuple[sqlite3.Cursor, sqlite3.Connection]
- stores the result of
ratesInThePast(self) -> list[dict[str, str | float]]
in variablepastRates
- caches the rates in
pastRates
- commits and closes the connection to the cache
- prints the entire cache in a tabular form using
printCACHE(self) -> None
- opens connection to the cache using
plot(self, coin: str) -> None
where coin can be either"DOGE"
or"LTC"
- opens connection to the cache using
connect2cache(self) -> tuple[sqlite3.Cursor, sqlite3.Connection]
- queries the cache for entire column of timestamps and the rates for
coin
and stores them into numpy arrays calledtimestamps
andcoinRates
respectively - close the connection to the cache
- plots a line chart showing fluctuations of prices of
coin
over time withtimestamps
on X-Axis andcoinRates
on Y-Axis
- opens connection to the cache using
test(self, rowsTBDel: int) -> None
whererowsTBDel
is the number of rows to be deleted- deletes
rowsTBDel
number of rows from cache from the bottom after validatingrowsTBDel
- used for debugging and forcefully demonstrating caching mechanism by test_cache.py
- if the last 2 rows are deleted, the rates for last 2 days are deleted
- when
dbHandler(self) -> None
is called it callsratesInThePast(self) -> list[dict[str, str | float]]
ratesInThePast(self) -> list[dict[str, str | float]]
will only fetch the rates for last 2 days as the rates before it are already cached
- deletes
import tkinter
import tkinter.messagebox
import socket
import customtkinter
from PIL import Image
import backend
customtkinter.set_appearance_mode("Dark")
customtkinter.set_default_color_theme("blue")
fg1 = "#a01bf2"
hvr = "#3f1369"
fg2 = "#6a16b7"
class App(customtkinter.CTk):
def __init__(self):
super().__init__()
fnt = customtkinter.CTkFont(family="HGGothicE", size=15)
self.backendObj = backend.backend("INR", -1, -1, -1, -1)
self.backendObj.dbHandler() # builds cache if missing as soon as the program starts
self.title("CryptoCached")
self.grid_columnconfigure(1, weight=1)
self.grid_columnconfigure((2, 3), weight=0)
self.grid_rowconfigure((0, 1, 2), weight=1)
# create sidebar frame with widgets
self.sidebar_frame = customtkinter.CTkFrame(self, width=250, corner_radius=0)
self.sidebar_frame.grid(row=0, column=0, rowspan=4, sticky="nsew")
self.sidebar_frame.grid_rowconfigure(4, weight=1)
my_image = customtkinter.CTkImage(light_image=Image.open("cryptologo.png"), dark_image=Image.open("cryptologo.png"), size=(200, 200))
self.logo_label = customtkinter.CTkLabel(self.sidebar_frame, text="", font=customtkinter.CTkFont(size=20, weight="bold"), image=my_image)
self.logo_label.grid(row=0, column=0, padx=20, pady=(20, 10))
self.Currency_choice_label = customtkinter.CTkLabel(self.sidebar_frame, text="Currency ", anchor="w", font=fnt)
self.Currency_choice_label.grid(row=5, column=0, padx=20, pady=(10, 0))
self.Currency_choice_optionemenu = customtkinter.CTkOptionMenu(self.sidebar_frame, values=["INR(₹) ", "USD($)", "EUR(€)", "GBP(£)"], fg_color=fg1, button_color=fg2, button_hover_color=hvr, width=180, dropdown_hover_color=fg2)
self.Currency_choice_optionemenu.grid(row=6, column=0, padx=20, pady=(10, 10))
self.Wifi_label = customtkinter.CTkLabel(self.sidebar_frame, text="Wi-Fi ", anchor="w", font=fnt)
self.Wifi_label.grid(row=7, column=0, padx=20, pady=(10, 0))
self.wifi_connect_label = customtkinter.CTkLabel(self.sidebar_frame, fg_color=fg1, width=180, corner_radius=8, text="Connected")
self.wifi_connect_label.grid(row=8, column=0, padx=20, pady=(10, 20))
self.main_frame = customtkinter.CTkFrame(self, width=1000, height=600, fg_color="transparent", border_color=fg2, border_width=4)
self.main_frame.grid(row=0, rowspan=10, column=1, padx=20, pady=(20, 0), sticky="nsew")
# initializes and displays necessary widgets when device is offline
if self.isConnected():
self.plot_frame = customtkinter.CTkFrame(self.main_frame, width=960, height=50, fg_color="transparent", corner_radius=8)
self.plot_frame.grid(row=0, column=0, padx=20, pady=(60, 20), sticky="ew")
self.disp_label = customtkinter.CTkLabel(self.plot_frame, fg_color=hvr, font=fnt, text="Enter target price and tokens in front of respective crypto currency and click check", width=650, corner_radius=8)
self.disp_label.pack(padx=(10, 5), pady=(10, 50), side="left")
self.ref_button = customtkinter.CTkButton(self.plot_frame, fg_color=fg1, hover_color=hvr, font=fnt, text="CHECK", width=80, command=self.cmpr)
self.ref_button.pack(padx=(5, 10), pady=(10, 50), side="left")
self.plot_button = customtkinter.CTkButton(self.plot_frame, fg_color=fg1, hover_color=hvr, font=fnt, text="PLOT", width=180, command=self.plt)
self.plot_button.pack(padx=10, pady=(10, 50), side="left")
self.crypt1_frame = customtkinter.CTkFrame(self.main_frame, width=960, height=200, fg_color=fg2, corner_radius=8)
self.crypt1_frame.grid(row=1, column=0, padx=20, pady=10)
self.crypt1_cb = customtkinter.CTkCheckBox(self.crypt1_frame, corner_radius=8, fg_color=hvr, border_color=hvr, hover_color=hvr, border_width=2, width=180, height=180, font=fnt, text="DOGE")
self.crypt1_cb.pack(padx=30, pady=10, side="left")
self.crypt1_ent1 = customtkinter.CTkEntry(self.crypt1_frame, fg_color=hvr, border_color=hvr, border_width=2, width=330, height=100, font=fnt, placeholder_text="Target Price")
self.crypt1_ent1.pack(side="left", padx=10, pady=50)
self.crypt1_ent1.bind("<FocusOut>", lambda event: self.validate_float(self.crypt1_ent1))
self.crypt1_ent2 = customtkinter.CTkEntry(self.crypt1_frame, fg_color=hvr, border_color=hvr, border_width=2, width=330, height=100, font=fnt, placeholder_text="Target Token Quantity")
self.crypt1_ent2.pack(side="left", padx=(10, 20), pady=50)
self.crypt1_ent2.bind("<FocusOut>", lambda event: self.validate_float(self.crypt1_ent2))
self.crypt2_frame = customtkinter.CTkFrame(self.main_frame, width=960, height=200, fg_color=fg2, corner_radius=8)
self.crypt2_frame.grid(row=2, column=0, padx=20, pady=10)
self.crypt2_cb = customtkinter.CTkCheckBox(self.crypt2_frame, corner_radius=8, fg_color=hvr, border_color=hvr, hover_color=hvr, border_width=2, width=180, height=180, font=fnt, text="LTC")
self.crypt2_cb.pack(padx=30, pady=10, side="left")
self.crypt2_ent1 = customtkinter.CTkEntry(self.crypt2_frame, fg_color=hvr, border_color=hvr, border_width=2, width=330, height=100, font=fnt, placeholder_text="Target Price")
self.crypt2_ent1.pack(side="left", padx=10, pady=50)
self.crypt2_ent1.bind("<FocusOut>", lambda event: self.validate_float(self.crypt2_ent1))
self.crypt2_ent2 = customtkinter.CTkEntry(self.crypt2_frame, fg_color=hvr, border_color=hvr, border_width=2, width=330, height=100, font=fnt, placeholder_text="Target Token Quantity")
self.crypt2_ent2.pack(side="left", padx=(10, 20), pady=50)
self.crypt2_ent2.bind("<FocusOut>", lambda event: self.validate_float(self.crypt2_ent2))
# initializes and displays necessary widgets when device is offline
else:
self.off_label = customtkinter.CTkLabel(self.main_frame, text="Please connect to Wi-Fi and restart, press 'OK' to close the Application", font=customtkinter.CTkFont(size=20, weight="bold"))
self.off_label.place(relx=0.5, rely=0.5, anchor=tkinter.CENTER)
self.re_button = customtkinter.CTkButton(self.main_frame, fg_color=fg1, hover_color=hvr, width=180, corner_radius=8, text="OK", command=self.restart_program)
self.re_button.place(relx=0.5, rely=0.6, anchor=tkinter.CENTER)
# displays error message on a pop-up message box
def error_box(self, txt):
window = customtkinter.CTkToplevel(self)
window.geometry("400x200+800+200")
customtkinter.CTkLabel(window, font=("HGGothicE", 15), width=380, height=180, text=txt).pack(padx=10, pady=10)
return 0
# checks if wifi is connected
def isConnected(self):
try:
s = socket.create_connection(("www.geeksforgeeks.org", 80))
if s is not None:
s.close()
return True
except OSError:
self.wifi_connect_label.configure(text="NOT CONNECTED", fg_color="#FF0000")
self.Currency_choice_optionemenu.configure(state="disabled")
return False
# destroys app window
def restart_program(self):
self.destroy()
# makes sure entryboxes can only store 6 decimal float values above zeros, rounds up or clears invalid values
def validate_float(self, entry):
try:
value = float(entry.get())
if value <= 0:
entry.select_clear()
entry.delete(0, "end")
return False
elif round(value, 6) == value:
return True
else:
entry.select_clear()
entry.delete(0, "end")
entry.insert(0, round(value, 6))
return False
except ValueError:
entry.select_clear()
entry.delete(0, "end")
return False
# displays comparison values
def cmpr_disp(self, cmprd):
c1 = self.crypt1_cb.get()
c2 = self.crypt2_cb.get()
if c1 and c2:
txt = "Target reached: DOGE: " + str(cmprd["DOGE"]) + " LTC: " + str(cmprd["LTC"])
elif c1:
txt = "Target reached: DOGE: " + str(cmprd["DOGE"])
elif c2:
txt = "Target reached: LTC: " + str(cmprd["LTC"])
else:
pass
self.disp_label.configure(text=txt)
# compares values entered in entry boxes with actual crypto rates, raises errors wherever necessary
def cmpr(self):
c1 = self.crypt1_cb.get()
c2 = self.crypt2_cb.get()
crypt1_box1 = 0
crypt1_box2 = 0
crypt2_box1 = 0
crypt2_box2 = 0
if c1 and c2:
if len(self.crypt1_ent1.get()) > 0:
crypt1_box1 = float(self.crypt1_ent1.get())
if len(self.crypt1_ent2.get()) > 0:
crypt1_box2 = float(self.crypt1_ent2.get())
if len(self.crypt2_ent1.get()) > 0:
crypt2_box1 = float(self.crypt2_ent1.get())
if len(self.crypt2_ent2.get()) > 0:
crypt2_box2 = float(self.crypt2_ent2.get())
else:
self.error_box("PLEASE FILL NECESSARY FIELDS")
else:
self.error_box("PLEASE FILL NECESSARY FIELDS")
else:
self.error_box("PLEASE FILL NECESSARY FIELDS")
else:
self.error_box("PLEASE FILL NECESSARY FIELDS")
elif c1:
if len(self.crypt1_ent1.get()) > 0:
crypt1_box1 = float(self.crypt1_ent1.get())
if len(self.crypt1_ent2.get()) > 0:
crypt1_box2 = float(self.crypt1_ent2.get())
else:
self.error_box("PLEASE FILL NECESSARY FIELDS")
else:
self.error_box("PLEASE FILL NECESSARY FIELDS")
crypt2_box1 = -1
crypt2_box2 = -1
elif c2:
crypt1_box1 = -1
crypt1_box2 = -1
if len(self.crypt2_ent1.get()) > 0:
crypt2_box1 = float(self.crypt2_ent1.get())
if len(self.crypt2_ent2.get()) > 0:
crypt2_box2 = float(self.crypt2_ent2.get())
else:
self.error_box("PLEASE FILL NECESSARY FIELDS")
else:
self.error_box("PLEASE FILL NECESSARY FIELDS")
else:
self.error_box("PLEASE SELECT ATLEAST ONE CRYPTO-CURRENCY")
if crypt1_box1 and crypt1_box2 and crypt2_box1 and crypt1_box2:
actual_backendObj = backend.backend(self.Currency_choice_optionemenu.get()[0:3], crypt1_box2, crypt1_box1, crypt2_box2, crypt2_box1)
self.cmpr_disp(actual_backendObj.compareTarget())
# checks cryptos selected and plots them, raises error_box wherever necessary
def plt(self):
c1 = self.crypt1_cb.get()
c2 = self.crypt2_cb.get()
if c1 and c2:
self.backendObj.plot("DOGE")
self.backendObj.plot("LTC")
elif c1:
self.backendObj.plot("DOGE")
elif c2:
self.backendObj.plot("LTC")
else:
self.error_box("PLEASE SELECT ATLEAST ONE CRYPTO-CURRENCY")
if __name__ == "__main__":
app = App()
app.state("normal")
app.mainloop()
error_box(self, txt)
displays error message on a pop-up message boxisConnected(self)
checks if wifi is connectedrestart_program(self)
destroys app windowvalidate_float(self, entry)
makes sure entryboxes can only store 6 decimal float values above zeros, rounds up or clears invalid valuescmpr_disp(self, cmprd)
displays comparison valuescmpr(self)
compares values entered in entry boxes with actual crypto rates, raises errors wherever necessaryplt(self)
checks cryptos selected and plots them, raiseserror_box
wherever necessary