From 94a08a86f15d535c5dc34c2165e0dbe80f364ec8 Mon Sep 17 00:00:00 2001 From: Koen Vossen Date: Fri, 24 Nov 2023 22:54:15 +0100 Subject: [PATCH 1/5] Add realtime data --- solarathon/pages/dashboard.py | 151 +++++++++++++++++++++++++--------- 1 file changed, 110 insertions(+), 41 deletions(-) diff --git a/solarathon/pages/dashboard.py b/solarathon/pages/dashboard.py index 705da26..471f906 100644 --- a/solarathon/pages/dashboard.py +++ b/solarathon/pages/dashboard.py @@ -13,14 +13,40 @@ #responsiveness #refactor """ +import threading +from threading import Event +from time import sleep import solara -from typing import Optional +from typing import Optional, cast import requests +import logging +import sys + +# root = logging.getLogger() +# root.setLevel(logging.DEBUG) + # some app state that outlives a single page +from pydantic import BaseModel, Field + +symbols_url = "https://api.binance.com/api/v3/exchangeInfo" +try: + symbols_response = requests.get(symbols_url) + symbols_data = symbols_response.json() + available_symbols = sorted(list(set([symbol['baseAsset'].lower() for symbol in symbols_data['symbols']]))) +except Exception as e: + available_symbols = [] + init_app_state = solara.reactive(["ada", "btc","bnb", "eth","doge", "xrp"]) + +class TickerData(BaseModel): + symbol: str + last_price: float = Field(..., alias="lastPrice") + price_change_percent: float = Field(..., alias="priceChangePercent") + + @solara.component def GeckoIcon (name: str, img: str): with solara.v.Html(tag="a", attributes={"href": f"https://www.binance.com/en/trade", "target": "_blank"}): @@ -34,7 +60,7 @@ def GeckoIcon (name: str, img: str): solara.v.ListItemTitle(children=[name], class_="v-list-item__title_avatar") def processColor(procentChange): - if procentChange.startswith("-"): + if procentChange < 0: return "red" else: return "green" @@ -48,13 +74,40 @@ def format_price(price): formatted_price = '{:,.0f}'.format(float(price)).replace(',', ' ') return formatted_price + +def get_binance_ticket(symbol: str) -> TickerData: + binance_url = f"https://api.binance.com/api/v3/ticker/24hr?symbol={symbol}" + return TickerData.model_validate_json( + json_data=requests.get(binance_url).content + ) + + @solara.component -def DashboardCard(price: str, procentChange: str, icon: solara.Element = None, market_cap: Optional[str] = None, market_cap_change_percentage: Optional[str] = None, pending: Optional[bool] = False): - if pending: - with solara.Card( - GeckoIcon('', '') - , style={"width":"330px", "min-width": "280px","max-width": "350px", "background-color": "#1B2028", "color": "#ffff", "border-radius": "16px", "padding": "20px", "box-shadow": "rgba(0, 0, 0, 0) 0px 0px, rgba(0, 0, 0, 0) 0px 0px, rgba(0, 0, 0, 0.2) 0px 4px 6px -1px, rgba(0, 0, 0, 0.14) 0px 2px 4px -1px"}, margin=0, classes=["my-2", "mx-auto",]): - +def DashboardCard(symbol: str, icon: solara.Element | str = None, market_cap: Optional[str] = None, market_cap_change_percentage: Optional[str] = None, pending: Optional[bool] = False): + ticker_data, set_ticker_data = solara.use_state( + cast(Optional[TickerData], None) + ) + + def fetch_data(event: threading.Event): + while True: + set_ticker_data(get_binance_ticket(symbol)) + if event.wait(10): + print(f"Stopping {symbol}") + return + + # run the render loop in a separate thread + result: solara.Result[bool] = solara.use_thread( + fetch_data, + intrusive_cancel=False + ) + if result.error: + raise result.error + + if not ticker_data: + with solara.Card( + GeckoIcon('', ''), + style={"width":"330px", "min-width": "280px","max-width": "350px", "background-color": "#1B2028", "color": "#ffff", "border-radius": "16px", "padding": "20px", "box-shadow": "rgba(0, 0, 0, 0) 0px 0px, rgba(0, 0, 0, 0) 0px 0px, rgba(0, 0, 0, 0.2) 0px 4px 6px -1px, rgba(0, 0, 0, 0.14) 0px 2px 4px -1px"}, margin=0, classes=["my-2", "mx-auto",]): + with solara.Div(): with solara.Div( style={ @@ -87,30 +140,19 @@ def DashboardCard(price: str, procentChange: str, icon: solara.Element = None, m }, ): with solara.GridFixed(columns=2, justify_items="space-between", align_items="baseline"): - solara.Text(str(f"{decimals(price)}$"), style={"font-size": "1.5rem", "font-weight": 500}) + solara.Text(str(f"{decimals(ticker_data.last_price)}$"), style={"font-size": "1.5rem", "font-weight": 500}) solara.Text(str("price"), style={"font-size": "0.6rem"}) if market_cap is not None: solara.Text(str(f"{format_price(decimals(market_cap))}$"), style={"font-weight": 400}) if market_cap is not None: solara.Text(str("market cap"), style={"font-size": "0.6rem"}) - solara.Text(str(procentChange + "%"), style={"color": processColor(procentChange), "font-weight": 500}) + solara.Text(f"{ticker_data.price_change_percent}%", style={"color": processColor(ticker_data.price_change_percent), "font-weight": 500}) solara.Text(str("24h change price"), style={"font-size": "0.6rem"}) - if market_cap_change_percentage is not None: solara.Text(str(market_cap_change_percentage) + "%", style={"color": processColor(procentChange), "font-weight": 500}) + if market_cap_change_percentage is not None: solara.Text(str(market_cap_change_percentage) + "%", style={"color": processColor(ticker_data.price_change_percent), "font-weight": 500}) if market_cap_change_percentage is not None: solara.Text(str("24h change market cap"), style={"font-size": "0.6rem"}) @solara.component def Page(): - loading, set_loading = solara.use_state(True) default_currency = "USDT" default_echange = "Binance" - symbols_url = "https://api.binance.com/api/v3/exchangeInfo" - coingecko_json_url = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc" - binance_url = "https://api.binance.com/api/v3/ticker/24hr?symbol=" - - try: - symbols_response = requests.get(symbols_url, verify=False) - symbols_data = symbols_response.json() - available_symbols = sorted(list(set([symbol['baseAsset'].lower() for symbol in symbols_data['symbols']]))) - except Exception as e: - available_symbols = [] all_tickers = list(init_app_state.value) + available_symbols @@ -143,25 +185,52 @@ def Page(): ) with solara.GridFixed(columns=3, align_items="end", justify_items="stretch"): - try: - coingecko_response = requests.get(coingecko_json_url, verify=False) - coingecko_data = coingecko_response.json() - for symbol in init_app_state.value: - coingecko_data_for_symbol = get_data_for_symbol(symbol, coingecko_data) - json_url = f"{binance_url}{symbol.upper()}{default_currency}" - response = requests.get(json_url, verify=False) - data = response.json() - - if coingecko_data_for_symbol: - set_loading(False) - DashboardCard(data['lastPrice'], data['priceChangePercent'], GeckoIcon(data['symbol'], coingecko_data_for_symbol['image']), coingecko_data_for_symbol['market_cap'], coingecko_data_for_symbol['market_cap_change_percentage_24h'], loading) - else: - set_loading(False) - DashboardCard(data['lastPrice'], data['priceChangePercent'], data['symbol'], loading) - - except Exception as e: - print(f"An error occurred: {e}") - solara.Error(f"Error {e}") + def get_coingecko_data(): + coingecko_json_url = "https://api.coingecko.com/api/v3/coins/markets?vs_currency=usd&order=market_cap_desc" + + coingecko_response = requests.get(coingecko_json_url) + return coingecko_response.json() + + coingecko_data, set_coingecko_data = solara.use_state(cast(Optional[dict], None)) + + def fetch_data(should_stop: Event): + while True: + set_coingecko_data(get_coingecko_data()) + if should_stop.wait(30): + return + + # run the render loop in a separate thread + result: solara.Result[bool] = solara.use_thread( + fetch_data, + intrusive_cancel=False + ) + if result.error: + raise result.error + + if not coingecko_data: + return + + if 'status' in coingecko_data: + solara.Error(f"Failed to retrieve data: {coingecko_data}") + else: + for symbol in init_app_state.value: + coingecko_data_for_symbol = get_data_for_symbol(symbol, coingecko_data) + + binance_symbol = symbol.upper() + default_currency + + if coingecko_data_for_symbol: + DashboardCard( + binance_symbol, + GeckoIcon(binance_symbol, coingecko_data_for_symbol['image']), + coingecko_data_for_symbol['market_cap'], + coingecko_data_for_symbol['market_cap_change_percentage_24h'], + ) + else: + DashboardCard( + binance_symbol, + binance_symbol + ) + def get_data_for_symbol(symbol, coingecko_list): for coin in coingecko_list: From 3870863821e9e1e78bef177d1f71a135482ec8a0 Mon Sep 17 00:00:00 2001 From: Koen Vossen Date: Fri, 24 Nov 2023 23:06:25 +0100 Subject: [PATCH 2/5] Make it stable --- solarathon/pages/dashboard.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/solarathon/pages/dashboard.py b/solarathon/pages/dashboard.py index 471f906..3cfc0a4 100644 --- a/solarathon/pages/dashboard.py +++ b/solarathon/pages/dashboard.py @@ -224,12 +224,12 @@ def fetch_data(should_stop: Event): GeckoIcon(binance_symbol, coingecko_data_for_symbol['image']), coingecko_data_for_symbol['market_cap'], coingecko_data_for_symbol['market_cap_change_percentage_24h'], - ) + ).key(symbol) else: DashboardCard( binance_symbol, binance_symbol - ) + ).key(symbol) def get_data_for_symbol(symbol, coingecko_list): From 5b2be6feeb498df3246b5e4a8b558e294f99d872 Mon Sep 17 00:00:00 2001 From: Koen Vossen Date: Fri, 24 Nov 2023 23:09:33 +0100 Subject: [PATCH 3/5] Refresh binance data every 5 seconds --- solarathon/pages/dashboard.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/solarathon/pages/dashboard.py b/solarathon/pages/dashboard.py index 3cfc0a4..6f57f10 100644 --- a/solarathon/pages/dashboard.py +++ b/solarathon/pages/dashboard.py @@ -91,7 +91,7 @@ def DashboardCard(symbol: str, icon: solara.Element | str = None, market_cap: Op def fetch_data(event: threading.Event): while True: set_ticker_data(get_binance_ticket(symbol)) - if event.wait(10): + if event.wait(5): print(f"Stopping {symbol}") return From d1c0efec37856b162fcc21d6066aae480b5353fc Mon Sep 17 00:00:00 2001 From: Koen Vossen Date: Fri, 24 Nov 2023 23:13:55 +0100 Subject: [PATCH 4/5] Minor refactor --- solarathon/pages/dashboard.py | 29 ++++++++++++++++++++++------- 1 file changed, 22 insertions(+), 7 deletions(-) diff --git a/solarathon/pages/dashboard.py b/solarathon/pages/dashboard.py index 6f57f10..c7abc5d 100644 --- a/solarathon/pages/dashboard.py +++ b/solarathon/pages/dashboard.py @@ -30,13 +30,6 @@ # some app state that outlives a single page from pydantic import BaseModel, Field -symbols_url = "https://api.binance.com/api/v3/exchangeInfo" -try: - symbols_response = requests.get(symbols_url) - symbols_data = symbols_response.json() - available_symbols = sorted(list(set([symbol['baseAsset'].lower() for symbol in symbols_data['symbols']]))) -except Exception as e: - available_symbols = [] init_app_state = solara.reactive(["ada", "btc","bnb", "eth","doge", "xrp"]) @@ -149,11 +142,33 @@ def fetch_data(event: threading.Event): if market_cap_change_percentage is not None: solara.Text(str(market_cap_change_percentage) + "%", style={"color": processColor(ticker_data.price_change_percent), "font-weight": 500}) if market_cap_change_percentage is not None: solara.Text(str("24h change market cap"), style={"font-size": "0.6rem"}) + +def get_available_symbols(): + symbols_url = "https://api.binance.com/api/v3/exchangeInfo" + try: + symbols_response = requests.get(symbols_url) + symbols_data = symbols_response.json() + available_symbols = sorted(list(set([symbol['baseAsset'].lower() for symbol in symbols_data['symbols']]))) + except Exception as e: + available_symbols = [] + + return available_symbols + + @solara.component def Page(): default_currency = "USDT" default_echange = "Binance" + # Store in state and make sure it won't be refetched when + # component is rerendered. + available_symbols, _ = solara.use_state( + solara.use_memo( + get_available_symbols, + dependencies=[] + ) + ) + all_tickers = list(init_app_state.value) + available_symbols solara.SelectMultiple(f"Tickers from {default_echange}", init_app_state, all_tickers) From 8f5c14c8ad77f103aa216c15c47a2d45749b0487 Mon Sep 17 00:00:00 2001 From: agora Date: Mon, 27 Nov 2023 18:55:23 +0100 Subject: [PATCH 5/5] Fix union type --- solarathon/pages/dashboard.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/solarathon/pages/dashboard.py b/solarathon/pages/dashboard.py index c7abc5d..d44f567 100644 --- a/solarathon/pages/dashboard.py +++ b/solarathon/pages/dashboard.py @@ -23,6 +23,7 @@ import logging import sys +from typing import Union # root = logging.getLogger() # root.setLevel(logging.DEBUG) @@ -76,7 +77,7 @@ def get_binance_ticket(symbol: str) -> TickerData: @solara.component -def DashboardCard(symbol: str, icon: solara.Element | str = None, market_cap: Optional[str] = None, market_cap_change_percentage: Optional[str] = None, pending: Optional[bool] = False): +def DashboardCard(symbol: str, icon: Union[solara.Element, str] = None, market_cap: Optional[str] = None, market_cap_change_percentage: Optional[str] = None, pending: Optional[bool] = False): ticker_data, set_ticker_data = solara.use_state( cast(Optional[TickerData], None) )