From 897d674d81f9c5f8032fd29a1fe75b7f69d1b670 Mon Sep 17 00:00:00 2001 From: Ryan Watts Date: Tue, 29 Jul 2025 16:44:47 -0600 Subject: [PATCH] Improve asset price fetching and account value calculation Enhanced batch and individual asset price fetching logic for robustness and better error handling. Improved account value calculation to skip zero balances and provide detailed debug output. Updated UI overview update method with error handling and better timestamp formatting. Bumped program version to 2.1.4 and added a new account JSON file. --- resources/data/settings/settings.yaml | 2 +- .../subaccounts/data/accounts/account_2.json | 13 + src/apps/subaccounts/ui.py | 229 +++++++++++++----- 3 files changed, 186 insertions(+), 58 deletions(-) create mode 100644 src/apps/subaccounts/data/accounts/account_2.json diff --git a/resources/data/settings/settings.yaml b/resources/data/settings/settings.yaml index 61f5927..f69898b 100644 --- a/resources/data/settings/settings.yaml +++ b/resources/data/settings/settings.yaml @@ -1,4 +1,4 @@ program: name: "TradeByte" - version: "2.1.3" + version: "2.1.4" debug: false diff --git a/src/apps/subaccounts/data/accounts/account_2.json b/src/apps/subaccounts/data/accounts/account_2.json new file mode 100644 index 0000000..6745f0c --- /dev/null +++ b/src/apps/subaccounts/data/accounts/account_2.json @@ -0,0 +1,13 @@ +{ + "account_id": 2, + "nick_name": "poop", + "balance": { + "ZUSD": 0.0, + "ZEUR": 0.0 + }, + "created_date": "2025-07-29", + "active": true, + "balances": { + "ZUSD": 2939340.0 + } +} \ No newline at end of file diff --git a/src/apps/subaccounts/ui.py b/src/apps/subaccounts/ui.py index b14258f..b2658d9 100644 --- a/src/apps/subaccounts/ui.py +++ b/src/apps/subaccounts/ui.py @@ -12,8 +12,7 @@ try: from src.apps.subaccounts.account import SubAccount, AccountEdit except Exception as e: - from account import SubAccount, AccountEdit - print(f"C.ERROR: {e}") + print(f"C.Error: {e}") import datetime class SubAccountUI: @@ -99,27 +98,32 @@ def batch_update_prices(self, required_pairs: List[str]) -> Dict[str, float]: """Batch update prices for multiple pairs to reduce API calls""" current_time = time.time() - # If cache is still valid, return cached prices - if current_time - self.cache_timestamp < self.cache_duration: + # If cache is still valid and has all required pairs, return cached prices + if (current_time - self.cache_timestamp < self.cache_duration and + all(pair in self.price_cache for pair in required_pairs)): return {pair: self.price_cache.get(pair, 0.0) for pair in required_pairs} new_prices = {} - # USD pairs for common assets + # USD pairs for common assets - using correct Kraken pair names pair_mappings = { - 'ZUSD': ('ZUSDUSD', 1.0), # USD to USD = 1:1 - 'ZEUR': ('EURUSD', None), - 'XXBT': ('XXBTZUSD', None), - 'XETH': ('XETHZUSD', None), - 'ADA': ('ADAUSD', None), - 'DOT': ('DOTUSD', None), - 'LINK': ('LINKUSD', None), - 'LTC': ('LTCUSD', None), - 'XRP': ('XRPUSD', None) + 'ZUSD': ('ZUSDUSD', 1.0), # USD to USD = 1:1 + 'ZEUR': ('EURUSD', None), # This might not exist on Kraken, try EURFUSD + 'XXBT': ('XXBTZUSD', None), # Bitcoin to USD + 'XETH': ('XETHZUSD', None), # Ethereum to USD + 'ADA': ('ADAUSD', None), # Cardano to USD + 'DOT': ('DOTUSD', None), # Polkadot to USD + 'LINK': ('LINKUSD', None), # Chainlink to USD + 'LTC': ('LTCUSD', None), # Litecoin to USD + 'XRP': ('XRPUSD', None) # Ripple to USD } - # Batch API calls for efficiency - for asset in required_pairs: + # Get prices for assets that need updates + assets_to_update = [asset for asset in required_pairs + if asset not in self.price_cache or + current_time - self.cache_timestamp >= self.cache_duration] + + for asset in assets_to_update: try: if asset == 'ZUSD': new_prices[asset] = 1.0 @@ -128,29 +132,64 @@ def batch_update_prices(self, required_pairs: List[str]) -> Dict[str, float]: if fixed_price: new_prices[asset] = fixed_price else: - # Only make API call if not in cache + # Make API call with error handling try: ask = self.sub_account_manager.client.get_ask(pair) bid = self.sub_account_manager.client.get_bid(pair) - if ask and bid: - new_prices[asset] = (ask + bid) / 2 + + # Validate that we got numeric values + if ask and bid and isinstance(ask, (int, float)) and isinstance(bid, (int, float)): + new_prices[asset] = (float(ask) + float(bid)) / 2 else: + print(f"Invalid price data for {asset}: ask={ask}, bid={bid}") new_prices[asset] = 0.0 - except: + except Exception as api_error: + print(f"API error for {asset} ({pair}): {api_error}") new_prices[asset] = 0.0 else: - # For unknown assets, use fallback - new_prices[asset] = 0.0 + # For unknown assets, try multiple pair formats + price_found = False + possible_pairs = [ + f"{asset}USD", # Standard format + f"{asset}ZUSD", # Kraken Z-prefix format + f"X{asset}USD", # Kraken X-prefix format + f"X{asset}ZUSD" # Combined prefix format + ] + + for test_pair in possible_pairs: + try: + ask = self.sub_account_manager.client.get_ask(test_pair) + bid = self.sub_account_manager.client.get_bid(test_pair) + + if ask and bid and isinstance(ask, (int, float)) and isinstance(bid, (int, float)): + new_prices[asset] = (float(ask) + float(bid)) / 2 + price_found = True + print(f"Found price for {asset} using pair {test_pair}: ${new_prices[asset]:.2f}") + break + except: + continue + + if not price_found: + print(f"Could not find valid price for unknown asset: {asset}") + new_prices[asset] = 0.0 except Exception as e: - print(f"Error getting price for {asset}: {e}") + print(f"Error processing asset {asset}: {e}") new_prices[asset] = 0.0 - # Update cache + # Update cache with new prices self.price_cache.update(new_prices) self.cache_timestamp = current_time - return new_prices + # Return prices for all requested pairs (mix of cached and new) + result = {} + for pair in required_pairs: + if pair in new_prices: + result[pair] = new_prices[pair] + else: + result[pair] = self.price_cache.get(pair, 0.0) + + return result def calculate_account_value_fast(self, account_data: dict) -> float: """Fast calculation of account value using cached prices""" @@ -158,19 +197,75 @@ def calculate_account_value_fast(self, account_data: dict) -> float: if not balances: return 0.0 + # Filter out zero balances to reduce API calls + non_zero_balances = {asset: balance for asset, balance in balances.items() if balance != 0} + if not non_zero_balances: + return 0.0 + # Get all required assets - required_assets = list(balances.keys()) + required_assets = list(non_zero_balances.keys()) # Batch update prices - prices = self.batch_update_prices(required_assets) + try: + prices = self.batch_update_prices(required_assets) + except Exception as e: + print(f"Error batch updating prices: {e}") + # Fallback to individual price fetching + prices = {} + for asset in required_assets: + try: + if asset == 'ZUSD': + prices[asset] = 1.0 + else: + # Try to get individual price + price = self.get_individual_price(asset) + prices[asset] = price if price is not None else 0.0 + except: + prices[asset] = 0.0 total_value = 0.0 - for asset, balance in balances.items(): - if balance != 0: # Skip zero balances - price = prices.get(asset, 0.0) - total_value += balance * price + for asset, balance in non_zero_balances.items(): + price = prices.get(asset, 0.0) + asset_value = balance * price + total_value += asset_value + + # Debug output for troubleshooting + if price > 0: + print(f"Account value calc: {asset} = {balance:.6f} × ${price:.2f} = ${asset_value:.2f}") return total_value + + def get_individual_price(self, asset: str) -> Optional[float]: + """Get price for individual asset as fallback""" + try: + # Use same logic as batch update but for single asset + if asset == 'ZUSD': + return 1.0 + + # Try common pair formats + possible_pairs = [ + f"{asset}USD", + f"{asset}ZUSD", + f"X{asset}USD", + f"X{asset}ZUSD" + ] + + for pair in possible_pairs: + try: + ask = self.sub_account_manager.client.get_ask(pair) + bid = self.sub_account_manager.client.get_bid(pair) + + if ask and bid and isinstance(ask, (int, float)) and isinstance(bid, (int, float)): + return (float(ask) + float(bid)) / 2 + except: + continue + + print(f"Could not get price for asset: {asset}") + return 0.0 + + except Exception as e: + print(f"Error getting individual price for {asset}: {e}") + return 0.0 def async_load_accounts(self): """Load accounts asynchronously to prevent UI blocking""" @@ -335,34 +430,54 @@ def on_stats_complete(future): def _update_overview_ui(self, stats): """Update overview UI with calculated stats""" - # Update statistics labels - self.total_accounts_label.config(text=f"{stats['total_accounts']}") - self.total_value_label.config(text=f"${stats['total_value']:,.2f}") - self.active_accounts_label.config(text=f"{stats['active_accounts']}") - self.largest_account_label.config(text=f"{stats['largest_account']}" if stats['largest_account'] else "--") - self.avg_account_value_label.config(text=f"${stats['avg_account_value']:,.2f}") - self.last_activity_label.config(text=f"{stats['last_activity']}") - - # Update summary table - for item in self.summary_tree.get_children(): - self.summary_tree.delete(item) - - for i, (account_id, account_data) in enumerate(self.accounts_data.items()): - account_value = stats['account_values'][i] if i < len(stats['account_values']) else 0.0 + try: + # Update statistics labels + self.total_accounts_label.config(text=f"{stats['total_accounts']}") + self.total_value_label.config(text=f"${stats['total_value']:,.2f}") + self.active_accounts_label.config(text=f"{stats['active_accounts']}") + self.largest_account_label.config(text=f"{stats['largest_account']}" if stats['largest_account'] else "--") + self.avg_account_value_label.config(text=f"${stats['avg_account_value']:,.2f}") + self.last_activity_label.config(text=f"{stats['last_activity']}") - # Get last activity - trade_history = account_data.get('trade_history', []) - last_activity = "No activity" - if trade_history: - last_activity = trade_history[-1]['timestamp'] + # Update summary table + for item in self.summary_tree.get_children(): + self.summary_tree.delete(item) - self.summary_tree.insert('', tk.END, values=( - account_id, - account_data.get('nick_name', f'Account {account_id}'), - 'Active' if account_data.get('active', False) else 'Inactive', - f"${account_value:,.2f}", - last_activity - )) + for i, (account_id, account_data) in enumerate(self.accounts_data.items()): + account_value = stats['account_values'][i] if i < len(stats['account_values']) else 0.0 + + # Get last activity + trade_history = account_data.get('trade_history', []) + last_activity = "No activity" + if trade_history: + try: + last_activity = trade_history[-1].get('timestamp', 'No activity') + # Format timestamp if it's a datetime string + if isinstance(last_activity, str) and last_activity != 'No activity': + # Try to parse and format the datetime + try: + from datetime import datetime + dt = datetime.fromisoformat(last_activity.replace('Z', '+00:00')) + last_activity = dt.strftime('%Y-%m-%d %H:%M') + except: + pass # Keep original string if parsing fails + except: + last_activity = "No activity" + + self.summary_tree.insert('', tk.END, values=( + account_id, + account_data.get('nick_name', f'Account {account_id}'), + 'Active' if account_data.get('active', False) else 'Inactive', + f"${account_value:,.2f}", + last_activity + )) + + except Exception as e: + print(f"Error updating overview UI: {e}") + # Set default values if update fails + self.total_accounts_label.config(text="Error") + self.total_value_label.config(text="$0.00") + self.active_accounts_label.config(text="0") def create_widgets(self): """Create all UI widgets with modern dark theme"""