diff --git a/.gitignore b/.gitignore index 93f302e..a16bcaf 100644 --- a/.gitignore +++ b/.gitignore @@ -65,3 +65,5 @@ src/user-applications/notebook/main.ipynb # Sub-account data #src/apps/sub-accounts/data/accounts/ #src/apps/sub-accounts/data/trades/ +/src/logs +/data/accounts diff --git a/kraken_ws/account.py b/kraken_ws/account.py index da73be2..41f9761 100644 --- a/kraken_ws/account.py +++ b/kraken_ws/account.py @@ -43,6 +43,10 @@ def __init__(self, api_key: Optional[str] = None, api_secret: Optional[str] = No # Add handlers for private data streams self._private_handlers: Dict[str, List[Callable]] = {} self._subscriptions: Dict[str, Dict] = {} + + # Cancel on disconnect settings + self._cancel_on_disconnect_enabled = False + self._cancel_on_disconnect_timeout = None @classmethod async def create(cls, api_key: Optional[str] = None, api_secret: Optional[str] = None): @@ -239,6 +243,9 @@ async def _handle_ws_messages_v2(self): except websockets.exceptions.ConnectionClosed: logger.warning("WebSocket v2 connection closed.") + # Trigger cancel on disconnect if enabled + if self._cancel_on_disconnect_enabled: + logger.info("Connection lost - cancel_on_disconnect is enabled, orders should be cancelled automatically") except Exception as e: logger.error(f"Error in WebSocket v2 message handler: {e}", exc_info=True) finally: @@ -291,6 +298,158 @@ async def _send_subscription_v2(self, subscription: Dict): await self._ws_connection.send(json.dumps(subscription)) logger.debug(f"WS v2 Subscription Sent: {subscription}") + # --- Cancel All Orders After (Dead Man's Switch) Methods --- + + async def set_cancel_all_orders_after(self, timeout: int) -> Dict: + """ + Set up a "Dead Man's Switch" that will cancel all orders after the specified timeout. + This must be called periodically to reset the timer and prevent cancellation. + + Args: + timeout: Duration in seconds to set/extend the timer (max 86400 seconds). + Set to 0 to disable the mechanism. + + Returns: + Response from the server with currentTime and triggerTime. + """ + if timeout < 0 or timeout > 86400: + raise ValueError("Timeout must be between 0 and 86400 seconds") + + payload = { + "method": "cancel_all_orders_after", + "params": { + "timeout": timeout + } + } + + try: + result = await self._send_request_v2(payload) + + if timeout > 0: + self._cancel_on_disconnect_enabled = True + self._cancel_on_disconnect_timeout = timeout + logger.info(f"Cancel all orders after timer set to {timeout} seconds") + else: + self._cancel_on_disconnect_enabled = False + self._cancel_on_disconnect_timeout = None + logger.info("Cancel all orders after timer disabled") + + return result + + except Exception as e: + logger.error(f"Failed to set cancel all orders after: {e}") + raise + + async def enable_cancel_on_disconnect(self, timeout: int = 60) -> Dict: + """ + Enable the "Dead Man's Switch" mechanism with the specified timeout. + + Args: + timeout: Duration in seconds (recommended: 60). Must be > 0. + + Returns: + Response from the server. + """ + if timeout <= 0: + raise ValueError("Timeout must be greater than 0 to enable") + + return await self.set_cancel_all_orders_after(timeout) + + async def disable_cancel_on_disconnect(self) -> Dict: + """ + Disable the "Dead Man's Switch" mechanism. + + Returns: + Response from the server. + """ + return await self.set_cancel_all_orders_after(0) + + async def reset_cancel_timer(self, timeout: int = 60) -> Dict: + """ + Reset the cancel timer to prevent order cancellation. + Should be called periodically (every 15-30 seconds recommended). + + Args: + timeout: Duration in seconds to reset the timer to. + + Returns: + Response from the server. + """ + return await self.set_cancel_all_orders_after(timeout) + + async def get_cancel_on_disconnect_status(self) -> Dict: + """ + Get the current cancel on disconnect status. + Note: This returns local state. The actual timer status is on the server. + + Returns: + Dict containing enabled status and timeout (if applicable). + """ + return { + "enabled": self._cancel_on_disconnect_enabled, + "timeout": self._cancel_on_disconnect_timeout, + "note": "This shows local state. Call set_cancel_all_orders_after() to get server response with current/trigger times." + } + + async def set_cancel_on_disconnect(self, enabled: bool, timeout: int = 60) -> Dict: + """ + Convenience method to enable or disable the cancel mechanism. + + Args: + enabled: Whether to enable or disable the mechanism + timeout: Duration in seconds when enabling (ignored when disabling) + + Returns: + Response from the server. + """ + if enabled: + return await self.enable_cancel_on_disconnect(timeout) + else: + return await self.disable_cancel_on_disconnect() + + async def start_cancel_timer_task(self, timeout: int = 60, reset_interval: int = 30) -> asyncio.Task: + """ + Start a background task that automatically resets the cancel timer. + + Args: + timeout: Duration in seconds for the cancel timer + reset_interval: How often to reset the timer (in seconds) + + Returns: + The asyncio Task that can be cancelled to stop the auto-reset + """ + async def reset_timer_loop(): + try: + # Initial setup + await self.set_cancel_all_orders_after(timeout) + logger.info(f"Started auto-reset timer: {timeout}s timeout, reset every {reset_interval}s") + + while True: + await asyncio.sleep(reset_interval) + if self._ws_authenticated and self._ws_connection: + try: + result = await self.set_cancel_all_orders_after(timeout) + logger.debug(f"Timer reset successful: {result.get('result', {}).get('triggerTime', 'N/A')}") + except Exception as e: + logger.error(f"Failed to reset cancel timer: {e}") + else: + logger.warning("WebSocket not connected, skipping timer reset") + + except asyncio.CancelledError: + logger.info("Cancel timer auto-reset task cancelled") + # Try to disable the timer on cancellation + try: + if self._ws_authenticated and self._ws_connection: + await self.set_cancel_all_orders_after(0) + logger.info("Disabled cancel timer on task cancellation") + except: + pass + raise + except Exception as e: + logger.error(f"Error in cancel timer task: {e}") + + return asyncio.create_task(reset_timer_loop()) + # --- Private Data Subscriptions (v2 format) --- async def subscribe_own_trades(self, @@ -662,6 +821,11 @@ async def query_trades_info(self, txid: Union[str, List[str]]) -> Dict: async def close(self): """Closes the WebSocket connection and cleans up resources.""" + try: + self.cancel_all_orders() + except: + pass + if self._message_handler_task: self._message_handler_task.cancel() try: diff --git a/kraken_ws/kraken_ws.py b/kraken_ws/kraken_ws.py index ae71bcf..999ccc4 100644 --- a/kraken_ws/kraken_ws.py +++ b/kraken_ws/kraken_ws.py @@ -150,7 +150,7 @@ async def subscribe_own_trades(self, await self.account.connect_v2() await self.account.subscribe_own_trades( - snapshot=snapshot, + snap_trades=snapshot, consolidate_taker=consolidate_taker, handler=handler ) @@ -170,6 +170,109 @@ async def unsubscribe_open_orders(self): """Unsubscribe from open orders data stream.""" await self.account.unsubscribe_open_orders() + # --- Cancel All Orders After (Dead Man's Switch) Methods --- + + async def set_cancel_all_orders_after(self, timeout: int) -> Dict: + """ + Set up a "Dead Man's Switch" that will cancel all orders after the specified timeout. + This must be called periodically to reset the timer and prevent cancellation. + + Args: + timeout: Duration in seconds to set/extend the timer (max 86400 seconds). + Set to 0 to disable the mechanism. + + Returns: + Response from the server with currentTime and triggerTime. + """ + if not self.account.connected(): + await self.account.connect_v2() + + return await self.account.set_cancel_all_orders_after(timeout) + + async def enable_cancel_on_disconnect(self, timeout: int = 60) -> Dict: + """ + Enable the "Dead Man's Switch" mechanism with the specified timeout. + + Args: + timeout: Duration in seconds (recommended: 60). Must be > 0. + + Returns: + Response from the server. + """ + if not self.account.connected(): + await self.account.connect_v2() + + return await self.account.enable_cancel_on_disconnect(timeout) + + async def disable_cancel_on_disconnect(self) -> Dict: + """ + Disable the "Dead Man's Switch" mechanism. + + Returns: + Response from the server. + """ + if not self.account.connected(): + await self.account.connect_v2() + + return await self.account.disable_cancel_on_disconnect() + + async def reset_cancel_timer(self, timeout: int = 60) -> Dict: + """ + Reset the cancel timer to prevent order cancellation. + Should be called periodically (every 15-30 seconds recommended). + + Args: + timeout: Duration in seconds to reset the timer to. + + Returns: + Response from the server. + """ + if not self.account.connected(): + await self.account.connect_v2() + + return await self.account.reset_cancel_timer(timeout) + + async def get_cancel_on_disconnect_status(self) -> Dict: + """ + Get the current cancel on disconnect status. + + Returns: + Dict containing enabled status and timeout (if applicable). + """ + return await self.account.get_cancel_on_disconnect_status() + + async def set_cancel_on_disconnect(self, enabled: bool, timeout: int = 60) -> Dict: + """ + Convenience method to enable or disable the cancel mechanism. + + Args: + enabled: Whether to enable or disable the mechanism + timeout: Duration in seconds when enabling (ignored when disabling) + + Returns: + Response from the server. + """ + if not self.account.connected(): + await self.account.connect_v2() + + return await self.account.set_cancel_on_disconnect(enabled, timeout) + + async def start_cancel_timer_task(self, timeout: int = 60, reset_interval: int = 30) -> asyncio.Task: + """ + Start a background task that automatically resets the cancel timer. + + Args: + timeout: Duration in seconds for the cancel timer + reset_interval: How often to reset the timer (in seconds) + + Returns: + The asyncio Task that can be cancelled to stop the auto-reset + """ + if not self.account.connected(): + await self.account.connect_v2() + + return await self.account.start_cancel_timer_task(timeout, reset_interval) + # --- Enhanced Trading Methods (v2 API) --- async def add_order_v2(self, symbol: str, side: str, order_type: str, diff --git a/resources/data/settings/settings.yaml b/resources/data/settings/settings.yaml index 724d9d9..61f5927 100644 --- a/resources/data/settings/settings.yaml +++ b/resources/data/settings/settings.yaml @@ -1,4 +1,4 @@ program: name: "TradeByte" - version: "2.1.2" + version: "2.1.3" debug: false diff --git a/src/apps/subaccounts/ui.py b/src/apps/subaccounts/ui.py index a111558..b14258f 100644 --- a/src/apps/subaccounts/ui.py +++ b/src/apps/subaccounts/ui.py @@ -3,13 +3,17 @@ import json import os from typing import Optional, Dict, List +import threading +import time +from concurrent.futures import ThreadPoolExecutor, as_completed try: from account import SubAccount, AccountEdit except: try: from src.apps.subaccounts.account import SubAccount, AccountEdit except Exception as e: - print(f"C.Error: {e}") + from account import SubAccount, AccountEdit + print(f"C.ERROR: {e}") import datetime class SubAccountUI: @@ -23,16 +27,25 @@ def __init__(self, root): self.sub_account_manager = SubAccount() self.accounts_dir = os.path.join('data', 'accounts') - # Data storage + # Data storage with caching self.accounts_data = {} self.selected_account_id = None + self.price_cache = {} # Cache for API prices + self.cache_timestamp = 0 + self.cache_duration = 60 # Cache prices for 60 seconds + + # Threading for async operations + self.executor = ThreadPoolExecutor(max_workers=4) + self.loading_in_progress = False # Setup styles self.setup_styles() # Create UI components self.create_widgets() - self.load_accounts() + + # Load accounts asynchronously + self.async_load_accounts() def setup_styles(self): """Configure modern dark theme matching speedy.py""" @@ -69,7 +82,288 @@ def setup_styles(self): # Configure buttons style.configure('Action.TButton', font=('Arial', 11, 'bold')) style.configure('Primary.TButton', font=('Arial', 11, 'bold')) + + def get_cached_price(self, pair: str) -> Optional[float]: + """Get cached price or fetch new one if cache is stale""" + current_time = time.time() + + # Check if we have a cached price and it's still valid + if (pair in self.price_cache and + current_time - self.cache_timestamp < self.cache_duration): + return self.price_cache[pair] + + # Cache is stale or price not found, return None to trigger batch update + return None + + 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: + return {pair: self.price_cache.get(pair, 0.0) for pair in required_pairs} + + new_prices = {} + + # USD pairs for common assets + 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) + } + + # Batch API calls for efficiency + for asset in required_pairs: + try: + if asset == 'ZUSD': + new_prices[asset] = 1.0 + elif asset in pair_mappings: + pair, fixed_price = pair_mappings[asset] + if fixed_price: + new_prices[asset] = fixed_price + else: + # Only make API call if not in cache + 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 + else: + new_prices[asset] = 0.0 + except: + new_prices[asset] = 0.0 + else: + # For unknown assets, use fallback + new_prices[asset] = 0.0 + + except Exception as e: + print(f"Error getting price for {asset}: {e}") + new_prices[asset] = 0.0 + + # Update cache + self.price_cache.update(new_prices) + self.cache_timestamp = current_time + + return new_prices + + def calculate_account_value_fast(self, account_data: dict) -> float: + """Fast calculation of account value using cached prices""" + balances = account_data.get('balances', {}) + if not balances: + return 0.0 + + # Get all required assets + required_assets = list(balances.keys()) + + # Batch update prices + prices = self.batch_update_prices(required_assets) + + 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 + + return total_value + + def async_load_accounts(self): + """Load accounts asynchronously to prevent UI blocking""" + if self.loading_in_progress: + return + + self.loading_in_progress = True + self.status_label.config(text="Loading accounts...") + + def load_task(): + try: + new_accounts_data = {} + + if not os.path.exists(self.accounts_dir): + return new_accounts_data + + # Load all account files + account_files = [f for f in os.listdir(self.accounts_dir) if f.endswith('.json')] + + for filename in account_files: + try: + filepath = os.path.join(self.accounts_dir, filename) + with open(filepath, 'r') as f: + account_data = json.load(f) + account_id = account_data.get('account_id') + if account_id: + new_accounts_data[account_id] = account_data + except Exception as e: + print(f"Error loading account {filename}: {e}") + + return new_accounts_data + + except Exception as e: + print(f"Error in load_task: {e}") + return {} + + def on_complete(future): + try: + new_accounts_data = future.result() + # Update UI in main thread + self.root.after(0, self._update_ui_after_load, new_accounts_data) + except Exception as e: + print(f"Error completing load: {e}") + self.root.after(0, lambda: self.status_label.config(text="Error loading accounts")) + finally: + self.loading_in_progress = False + + future = self.executor.submit(load_task) + future.add_done_callback(on_complete) + + def _update_ui_after_load(self, new_accounts_data): + """Update UI components after accounts are loaded""" + self.accounts_data = new_accounts_data + + # Update account listbox + self.account_listbox.delete(0, tk.END) + for account_id, account_data in self.accounts_data.items(): + nickname = account_data.get('nick_name', f'Account {account_id}') + self.account_listbox.insert(tk.END, f"{account_id}: {nickname}") + + # Update combo boxes + account_list = [f"{id}: {data.get('nick_name', f'Account {id}')}" + for id, data in self.accounts_data.items()] + self.from_account_combo['values'] = account_list + self.to_account_combo['values'] = account_list + self.trade_account_combo['values'] = account_list + + # Update asset dropdown + self.update_asset_dropdown() + + # Update overview asynchronously + self.async_update_overview() + + self.status_label.config(text=f"Loaded {len(self.accounts_data)} accounts") + + def load_accounts(self): + """Public method to reload accounts""" + self.async_load_accounts() + + def update_asset_dropdown(self): + """Update the asset dropdown with all available assets from accounts""" + common_assets = ['ZUSD', 'ZEUR', 'XXBT', 'XETH', 'ADA', 'DOT', 'LINK', 'LTC', 'XRP'] + all_assets = set(common_assets) + # Add all assets from existing accounts + for account_data in self.accounts_data.values(): + balances = account_data.get('balances', {}) + all_assets.update(balances.keys()) + + all_assets = sorted(list(all_assets)) + + # Update the asset combo if it exists + if hasattr(self, 'asset_combo'): + self.asset_combo['values'] = all_assets + + def async_update_overview(self): + """Update overview statistics asynchronously""" + def calculate_stats(): + try: + total_accounts = len(self.accounts_data) + active_accounts = sum(1 for data in self.accounts_data.values() if data.get('active', False)) + + # Get all unique assets for batch price update + all_assets = set() + for account_data in self.accounts_data.values(): + balances = account_data.get('balances', {}) + all_assets.update(balances.keys()) + + # Batch update prices for all assets at once + if all_assets: + self.batch_update_prices(list(all_assets)) + + # Calculate portfolio stats + total_value = 0.0 + account_values = [] + largest_account = None + largest_value = 0.0 + last_activity = "No activity" + + for account_id, account_data in self.accounts_data.items(): + account_value = self.calculate_account_value_fast(account_data) + total_value += account_value + account_values.append(account_value) + + if account_value > largest_value: + largest_value = account_value + largest_account = account_data.get('nick_name', f'Account {account_id}') + + # Get last activity + trade_history = account_data.get('trade_history', []) + if trade_history: + last_trade = trade_history[-1]['timestamp'] + if last_activity == "No activity" or last_trade > last_activity: + last_activity = last_trade + + avg_account_value = total_value / total_accounts if total_accounts > 0 else 0.0 + + return { + 'total_accounts': total_accounts, + 'active_accounts': active_accounts, + 'total_value': total_value, + 'avg_account_value': avg_account_value, + 'largest_account': largest_account, + 'last_activity': last_activity, + 'account_values': account_values + } + + except Exception as e: + print(f"Error calculating stats: {e}") + return None + + def on_stats_complete(future): + try: + stats = future.result() + if stats: + self.root.after(0, self._update_overview_ui, stats) + except Exception as e: + print(f"Error in stats calculation: {e}") + + future = self.executor.submit(calculate_stats) + future.add_done_callback(on_stats_complete) + + 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 + + # Get last activity + trade_history = account_data.get('trade_history', []) + last_activity = "No activity" + if trade_history: + last_activity = trade_history[-1]['timestamp'] + + 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 + )) + def create_widgets(self): """Create all UI widgets with modern dark theme""" # Main container @@ -280,16 +574,8 @@ def create_transfers_tab(self): common_assets = ['ZUSD', 'ZEUR', 'XXBT', 'XETH', 'ADA', 'DOT', 'LINK', 'LTC', 'XRP'] self.asset_var = tk.StringVar() - # Get all assets from existing accounts for the dropdown - all_assets = set(common_assets) - for account_data in self.accounts_data.values(): - balances = account_data.get('balances', {}) - all_assets.update(balances.keys()) - - all_assets = sorted(list(all_assets)) - self.asset_combo = ttk.Combobox(asset_frame, textvariable=self.asset_var, - values=all_assets, + values=common_assets, state="readonly", width=15) self.asset_combo.pack(side="left", padx=(0, 10)) @@ -457,73 +743,35 @@ def create_overview_tab(self): summary_scrollbar.pack(side="right", fill="y") self.summary_tree.configure(yscrollcommand=summary_scrollbar.set) - def load_accounts(self): - """Load all accounts from JSON files""" - self.accounts_data.clear() - self.account_listbox.delete(0, tk.END) - - if not os.path.exists(self.accounts_dir): - return - - for filename in os.listdir(self.accounts_dir): - if filename.endswith('.json'): - try: - filepath = os.path.join(self.accounts_dir, filename) - with open(filepath, 'r') as f: - account_data = json.load(f) - account_id = account_data.get('account_id') - if account_id: - self.accounts_data[account_id] = account_data - nickname = account_data.get('nick_name', f'Account {account_id}') - self.account_listbox.insert(tk.END, f"{account_id}: {nickname}") - except Exception as e: - print(f"Error loading account {filename}: {e}") - - # Update combo boxes - account_list = [f"{id}: {data.get('nick_name', f'Account {id}')}" - for id, data in self.accounts_data.items()] - self.from_account_combo['values'] = account_list - self.to_account_combo['values'] = account_list - self.trade_account_combo['values'] = account_list - - # Update asset dropdown with all available assets - self.update_asset_dropdown() - - # Update overview - self.update_overview() - - def update_asset_dropdown(self): - """Update the asset dropdown with all available assets from accounts""" - common_assets = ['ZUSD', 'ZEUR', 'XXBT', 'XETH', 'ADA', 'DOT', 'LINK', 'LTC', 'XRP'] - all_assets = set(common_assets) - - # Add all assets from existing accounts - for account_data in self.accounts_data.values(): - balances = account_data.get('balances', {}) - all_assets.update(balances.keys()) - - all_assets = sorted(list(all_assets)) - - # Update the asset combo if it exists - if hasattr(self, 'asset_combo'): - self.asset_combo['values'] = all_assets - def on_account_select(self, event): - """Handle account selection""" + """Handle account selection with async detail loading""" selection = self.account_listbox.curselection() if selection: account_text = self.account_listbox.get(selection[0]) account_id = int(account_text.split(':')[0]) self.selected_account_id = account_id - self.display_account_details(account_id) - - def display_account_details(self, account_id: int): - """Display details for selected account""" - if account_id not in self.accounts_data: - return - - account_data = self.accounts_data[account_id] - + + # Load details asynchronously to prevent UI blocking + self.async_display_account_details(account_id) + + def async_display_account_details(self, account_id: int): + """Display account details asynchronously""" + def load_details(): + return self.accounts_data.get(account_id) + + def on_details_loaded(future): + try: + account_data = future.result() + if account_data: + self.root.after(0, self._update_account_details_ui, account_id, account_data) + except Exception as e: + print(f"Error loading account details: {e}") + + future = self.executor.submit(load_details) + future.add_done_callback(on_details_loaded) + + def _update_account_details_ui(self, account_id: int, account_data: dict): + """Update account details UI""" # Update labels self.account_id_label.config(text=f"Account ID: {account_id}") self.nickname_label.config(text=f"Nickname: {account_data.get('nick_name', 'N/A')}") @@ -537,6 +785,10 @@ def display_account_details(self, account_id: int): balances = account_data.get('balances', {}) for asset, balance in balances.items(): self.balances_tree.insert('', tk.END, values=(asset, f"{balance:.8f}")) + + def display_account_details(self, account_id: int): + """Legacy method for compatibility""" + self.async_display_account_details(account_id) def create_account_dialog(self): """Dialog to create a new account""" @@ -578,6 +830,7 @@ def create(): if filepath: messagebox.showinfo("Success", f"Account created successfully!\nSaved to: {filepath}") dialog.destroy() + self.load_accounts() # Refresh the account list else: messagebox.showerror("Error", "Failed to create account") except ValueError: @@ -597,6 +850,7 @@ def delete_account(self): if account_edit.delete_account(): messagebox.showinfo("Success", "Account deleted successfully") self.selected_account_id = None + self.load_accounts() # Refresh the account list else: messagebox.showerror("Error", "Failed to delete account") @@ -704,6 +958,8 @@ def save(): account_edit = AccountEdit(self.selected_account_id) if account_edit.edit_account_balance(account_data['balances']): messagebox.showinfo("Success", f"Balance updated: {asset} = {amount}") + # Update local cache + self.accounts_data[self.selected_account_id] = account_data self.display_account_details(self.selected_account_id) dialog.destroy() else: @@ -757,6 +1013,8 @@ def execute_transfer(self): self.asset_var.set('') self.custom_asset_var.set('') self.amount_var.set('') + # Refresh accounts to update balances + self.load_accounts() else: messagebox.showerror("Error", "Transfer failed") except (ValueError, IndexError): @@ -779,84 +1037,35 @@ def post_trade(self): self.pair_var.set('') self.quantity_var.set('') self.price_var.set('') + # Refresh accounts to update balances + self.load_accounts() else: messagebox.showerror("Error", "Trade posting failed") except (ValueError, IndexError): messagebox.showerror("Error", "Please fill all fields correctly") - + def update_overview(self): - """Update the overview tab with current data""" - total_accounts = len(self.accounts_data) - active_accounts = sum(1 for data in self.accounts_data.values() if data.get('active', False)) - - # Calculate total portfolio value and other stats using real-time prices - total_value = 0.0 - account_values = [] - largest_account = None - largest_value = 0.0 - last_activity = "No activity" - - for account_id, account_data in self.accounts_data.items(): - # Use the account_value method for real-time pricing - account_value = self.sub_account_manager.account_value(account_id) - if account_value is not None: - total_value += account_value - account_values.append(account_value) - - if account_value > largest_value: - largest_value = account_value - largest_account = account_data.get('nick_name', f'Account {account_id}') - else: - # Fallback to 0 if account_value fails - account_values.append(0.0) - - # Get last activity from trade history - trade_history = account_data.get('trade_history', []) - if trade_history: - last_trade = trade_history[-1]['timestamp'] - if last_activity == "No activity" or last_trade > last_activity: - last_activity = last_trade - - # Calculate average account value - avg_account_value = total_value / total_accounts if total_accounts > 0 else 0.0 - - # Update labels - self.total_accounts_label.config(text=f"{total_accounts}") - self.total_value_label.config(text=f"${total_value:,.2f}") - self.active_accounts_label.config(text=f"{active_accounts}") - self.largest_account_label.config(text=f"{largest_account}" if largest_account else "--") - self.avg_account_value_label.config(text=f"${avg_account_value:,.2f}") - self.last_activity_label.config(text=f"{last_activity}") - - # Update summary table - for item in self.summary_tree.get_children(): - self.summary_tree.delete(item) - - for account_id, account_data in self.accounts_data.items(): - # Use the account_value method for real-time pricing - account_value = self.sub_account_manager.account_value(account_id) - if account_value is None: - account_value = 0.0 - - # Get last activity from trade history - trade_history = account_data.get('trade_history', []) - last_activity = "No activity" - if trade_history: - last_activity = trade_history[-1]['timestamp'] - - 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 - )) + """Legacy method for compatibility - delegates to async version""" + self.async_update_overview() + + def __del__(self): + """Cleanup when object is destroyed""" + if hasattr(self, 'executor'): + self.executor.shutdown(wait=False) def main(): """Main function to run the UI""" root = tk.Tk() app = SubAccountUI(root) + + # Ensure proper cleanup on window close + def on_closing(): + if hasattr(app, 'executor'): + app.executor.shutdown(wait=False) + root.destroy() + + root.protocol("WM_DELETE_WINDOW", on_closing) root.mainloop() if __name__ == "__main__": - main() + main() \ No newline at end of file