""" DRL Trading System - Streamlit Dashboard Real-time monitoring with TradingView charts, WebSocket live data, and timeframe switching. """ import streamlit as st import streamlit.components.v1 as components import pandas as pd import numpy as np import json import time from datetime import datetime, timedelta from pathlib import Path import sys import os # Add project root to path project_root = Path(__file__).parent.parent.parent sys.path.insert(0, str(project_root)) try: from src.backtest.data_loader import DataLoader, BinanceHistoricalDataFetcher _HAS_BACKTEST = True except ImportError: _HAS_BACKTEST = False from src.data.storage import get_storage, JsonFileStorage # True when running as a client-only HF Space (API_SERVER_URL points at remote server) IS_CLIENT_MODE = bool(os.environ.get('API_SERVER_URL')) # API server URL โ€” configurable for remote (local server) or local deployments def get_api_url() -> str: """Return the base URL of the Flask API server. Set API_SERVER_URL env var to point at a remote local server (e.g. https://abc123.ngrok.io). Defaults to localhost:5001. """ return os.environ.get('API_SERVER_URL', 'http://127.0.0.1:5001').rstrip('/') # Page configuration โ€” MUST be first Streamlit command st.set_page_config( page_title="DRL Trading System", page_icon="๐Ÿค–", layout="wide", initial_sidebar_state="expanded", ) # Initialize storage with caching (must be after set_page_config) @st.cache_resource def get_app_storage(): return get_storage() storage = get_app_storage() # Custom CSS โ€” Premium Dark Theme (matches Live Portfolio aesthetic) st.markdown(""" """, unsafe_allow_html=True) # Timeframe options TIMEFRAMES = { '1m': {'binance': '1m', 'label': '1m', 'days': 1}, '5m': {'binance': '5m', 'label': '5m', 'days': 2}, '15m': {'binance': '15m', 'label': '15m', 'days': 5}, '30m': {'binance': '30m', 'label': '30m', 'days': 7}, '1h': {'binance': '1h', 'label': '1H', 'days': 14}, '4h': {'binance': '4h', 'label': '4H', 'days': 30}, '1d': {'binance': '1d', 'label': '1D', 'days': 180}, } def load_trading_log(symbol: str = None) -> list: """Load real trading data โ€” via API in client mode, local storage otherwise.""" import requests as _r def _filter_by_symbol(trades, symbol): if not symbol: return trades s1 = symbol.replace('/', '').upper() return [t for t in trades if s1 in t.get('symbol', t.get('asset', '')).replace('/', '').upper() or t.get('symbol', t.get('asset', '')).replace('/', '').upper() in s1] if IS_CLIENT_MODE: try: resp = _r.get(f'{get_api_url()}/api/trades', timeout=10) if resp.ok: return _filter_by_symbol(resp.json(), symbol) except Exception: pass return [] # Local storage mode try: all_trades = storage.get_trades(limit=1000) # Filter by reset_timestamp if available (hide pre-reset trades) try: state = storage.load_state() reset_ts = state.get('reset_timestamp') if reset_ts: reset_dt = datetime.fromisoformat(reset_ts.replace('Z', '+00:00')) filtered_by_time = [] for trade in all_trades: try: trade_ts = trade.get('timestamp', '') trade_dt = datetime.fromisoformat(trade_ts.replace('Z', '+00:00')) if trade_dt >= reset_dt: filtered_by_time.append(trade) except: filtered_by_time.append(trade) all_trades = filtered_by_time except: pass return _filter_by_symbol(all_trades, symbol) except Exception as e: st.error(f"Failed to load trades: {e}") return [] def check_pid_running(pid: int) -> bool: """Check if a process with the given PID is running.""" if not pid: return False try: os.kill(int(pid), 0) return True except OSError: return False def check_process_running(process_name_substr: str) -> bool: """Check if a process is running by parsing ps aux output.""" try: import subprocess # Run ps aux res = subprocess.run(['ps', 'aux'], capture_output=True, text=True) if res.returncode != 0: return False # Check if process name is in output for line in res.stdout.splitlines(): if process_name_substr in line and "grep" not in line: return True return False except: return False def get_last_logs(log_path: Path, lines: int = 50) -> str: """Read last N lines of a log file.""" if not log_path.exists(): return f"Log file not found: {log_path}" try: # Use simple file reading for portability content = log_path.read_text().splitlines() return "\n".join(content[-lines:]) except Exception as e: return f"Error reading logs: {e}" def get_trading_state(selected_asset: str = None) -> dict: """Get current trading state โ€” via API in client mode, local storage otherwise.""" import requests as _r _empty = {'balance': 0, 'realized_pnl': 0, 'multi_asset': True, 'whale_alerts': [], 'assets': {}, 'available_assets': []} if IS_CLIENT_MODE: try: state_resp = _r.get(f'{get_api_url()}/api/state', timeout=10) state = state_resp.json() if state_resp.ok else {} trades_resp = _r.get(f'{get_api_url()}/api/trades', timeout=10) all_trades = trades_resp.json() if trades_resp.ok else [] raw_assets = state.get('assets', {}) if selected_asset: s1 = selected_asset.replace('/', '').upper() asset_trades = [t for t in all_trades if s1 in t.get('symbol', t.get('asset', '')).replace('/', '').upper()] asset_state = raw_assets.get(selected_asset, raw_assets.get(s1, {})) return { 'balance': state.get('balance', state.get('total_balance', 0)), 'total_balance': state.get('total_balance', state.get('balance', 0)), 'asset_balance': asset_state.get('balance', 0), 'position': asset_state.get('position', 0), 'realized_pnl': state.get('realized_pnl', state.get('total_pnl', 0)), 'total_pnl': state.get('total_pnl', state.get('realized_pnl', 0)), 'asset_pnl': asset_state.get('pnl', 0), 'trades': asset_trades, 'total_trades': len([t for t in asset_trades if 'OPEN' in t.get('action', '')]), 'position_price': asset_state.get('price', 0), 'position_size_units': asset_state.get('units', 0), 'price': asset_state.get('price', 0), 'timestamp': state.get('timestamp'), 'multi_asset': True, 'available_assets': state.get('available_assets') or list(raw_assets.keys()) or ['BTCUSDT'], 'whale_alerts': state.get('whale_alerts', []), 'raw_state': state, 'assets': raw_assets, 'sl': asset_state.get('sl', 0), 'tp': asset_state.get('tp', 0), } else: return { 'balance': state.get('balance', state.get('total_balance', 0)), 'total_balance': state.get('total_balance', state.get('balance', 0)), 'realized_pnl': state.get('realized_pnl', state.get('total_pnl', 0)), 'total_pnl': state.get('total_pnl', state.get('realized_pnl', 0)), 'multi_asset': True, 'available_assets': state.get('available_assets') or list(raw_assets.keys()) or ['BTCUSDT'], 'whale_alerts': state.get('whale_alerts', []), 'raw_state': state, 'assets': raw_assets, } except Exception: pass return _empty # โ”€โ”€ Local storage mode (server-side only) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ try: state = storage.load_state() if not state: return {**_empty} # If specific asset selected, return its details mixed with global if selected_asset and 'assets' in state and selected_asset in state['assets']: asset_state = state['assets'][selected_asset] asset_trades = load_trading_log(symbol=selected_asset) all_trades = load_trading_log() realized_pnl = sum(t.get('pnl', 0) for t in all_trades if 'CLOSE' in t.get('action', '').upper() or 'EXIT' in t.get('action', '').upper()) raw_assets = state.get('assets', {}) open_pnl = sum(a.get('pnl', 0) for a in raw_assets.values() if a.get('position', 0) != 0) total_pnl = realized_pnl + open_pnl total_balance = state.get('total_balance', state.get('balance')) whale_alerts = _load_whale_alerts_local() state['whale_alerts'] = whale_alerts return { 'balance': total_balance, 'total_balance': total_balance, 'asset_balance': asset_state.get('balance', 0), 'position': asset_state.get('position', 0), 'realized_pnl': total_pnl, 'total_pnl': total_pnl, 'asset_pnl': asset_state.get('pnl', 0), 'trades': asset_trades, 'total_trades': len([t for t in asset_trades if 'OPEN' in t.get('action', '')]), 'position_price': asset_state.get('price', 0), 'position_size_units': asset_state.get('units', 0), 'price': asset_state.get('price', 0), 'timestamp': state.get('timestamp'), 'multi_asset': True, 'available_assets': state.get('available_assets') or list(state.get('assets', {}).keys()) or ['BTCUSDT'], 'whale_alerts': whale_alerts, 'raw_state': state, 'assets': raw_assets, 'sl': asset_state.get('sl', 0), 'tp': asset_state.get('tp', 0), } # Global view all_trades = load_trading_log() realized_pnl = sum(t.get('pnl', 0) for t in all_trades if 'CLOSE' in t.get('action', '').upper() or 'EXIT' in t.get('action', '').upper()) raw_assets = state.get('assets', {}) open_pnl = sum(a.get('pnl', 0) for a in raw_assets.values() if a.get('position', 0) != 0) total_pnl = realized_pnl + open_pnl total_balance = state.get('total_balance', state.get('balance')) whale_alerts = _load_whale_alerts_local() state['whale_alerts'] = whale_alerts return { 'balance': total_balance, 'total_balance': total_balance, 'realized_pnl': total_pnl, 'total_pnl': total_pnl, 'multi_asset': True, 'available_assets': state.get('available_assets') or list(state.get('assets', {}).keys()) or ['BTCUSDT'], 'whale_alerts': whale_alerts, 'raw_state': state, 'assets': raw_assets, } except Exception: return {**_empty} def _load_whale_alerts_local() -> list: """Load whale alerts from local wallet files (server-side only). Returns [] on HF.""" import json as _json, time as _time whale_alerts = [] try: whale_dir = Path(__file__).parent.parent.parent / "data" / "whale_wallets" if not whale_dir.exists(): return [] try: from src.features.whale_wallet_registry import get_wallets_by_chain as _gwbc except ImportError: return [] for chain_dir in whale_dir.iterdir(): if not chain_dir.is_dir(): continue chain = chain_dir.name.upper() for wallet_file in chain_dir.glob("*.json"): try: with open(wallet_file, "r") as f: w_data = _json.load(f) addr = w_data.get("address", "") chain_wallets = _gwbc(chain) wallet = next((w for w in chain_wallets if w.address.lower() == addr.lower()), None) w_label = wallet.label if wallet else f"Unknown {chain} Whale" w_type = wallet.wallet_type if wallet else "unknown" price_map = {'BTC': 70000, 'ETH': 3500, 'SOL': 150, 'XRP': 0.6} for tx in w_data.get("transactions", [])[-10:]: val = float(tx.get('value', 0)) if val * price_map.get(chain, 1) > 50000: whale_alerts.append({ 'chain': chain, 'value': val, 'currency': tx.get('asset', chain), 'timestamp': tx.get('timestamp', int(_time.time())), 'link': tx.get('link', '#'), 'wallet_label': w_label, 'wallet_type': w_type, 'wallet_address': addr, }) except Exception: pass whale_alerts = sorted(whale_alerts, key=lambda x: x.get('timestamp', 0), reverse=True)[:50] except Exception: pass return whale_alerts def create_tradingview_chart_with_websocket(df: pd.DataFrame, trades: list, timeframe: str = '1h', symbol: str = 'BTC/USDT') -> str: """Create TradingView Lightweight Charts HTML with WebSocket live updates.""" if df.empty: return "
No market data available
" # Convert data to the format expected by Lightweight Charts candlestick_data = [] for idx, row in df.iterrows(): candlestick_data.append({ 'time': int(idx.timestamp()), 'open': float(row['open']), 'high': float(row['high']), 'low': float(row['low']), 'close': float(row['close']), }) volume_data = [] for idx, row in df.iterrows(): color = '#26a69a80' if row['close'] >= row['open'] else '#ef535080' volume_data.append({ 'time': int(idx.timestamp()), 'value': float(row['volume']), 'color': color, }) # Create markers for trades markers = [] for trade in trades: if 'price' in trade and 'timestamp' in trade: try: ts = datetime.fromisoformat(trade['timestamp'].replace('Z', '+00:00')) action = trade.get('action', '') reason = trade.get('reason', 'model') if 'OPEN_LONG' in action: markers.append({ 'time': int(ts.timestamp()), 'position': 'belowBar', 'color': '#26a69a', 'shape': 'arrowUp', 'text': 'LONG', }) elif 'OPEN_SHORT' in action: markers.append({ 'time': int(ts.timestamp()), 'position': 'aboveBar', 'color': '#ef5350', 'shape': 'arrowDown', 'text': 'SHORT', }) elif 'CLOSE' in action: # Differentiate exit types if reason == 'stop_loss': markers.append({ 'time': int(ts.timestamp()), 'position': 'aboveBar', 'color': '#ff4444', 'shape': 'square', 'text': 'SL', }) elif reason == 'take_profit': markers.append({ 'time': int(ts.timestamp()), 'position': 'aboveBar', 'color': '#00ff88', 'shape': 'square', 'text': 'TP', }) else: markers.append({ 'time': int(ts.timestamp()), 'position': 'aboveBar', 'color': '#ffc107', 'shape': 'circle', 'text': 'EXIT', }) except: pass # Get OHLC for display last_candle = df.iloc[-1] tf_label = TIMEFRAMES.get(timeframe, {}).get('label', timeframe.upper()) # WebSocket stream name for Binance # Symbol needs to be lowercase and without / clean_symbol = symbol.replace('/', '').lower() ws_stream = f"{clean_symbol}@kline_{timeframe}" chart_id = f"chart_{clean_symbol}_{timeframe}" html = f"""
{symbol} {tf_label} LIVE
O {last_candle['open']:.2f} H {last_candle['high']:.2f} L {last_candle['low']:.2f} C {last_candle['close']:.2f}
${last_candle['close']:,.2f}
""" return html def render_position_card(state: dict, current_price: float, symbol: str = 'BTC/USDT'): """Render current position card.""" position = state.get('position', 0) clean_symbol = symbol.replace('/', '').lower() # For localStorage key # SL/TP percentages (match live trading config) SL_PCT = 0.015 # 1.5% (matches live_trading.py) TP_PCT = 0.025 # 2.5% (matches live_trading.py) if position == 0: st.markdown(f"""
Current Position
No Position (FLAT)
Current Price: ${current_price:,.2f}
""", unsafe_allow_html=True) else: # DEBUG: Inspect state to find units key # st.write(f"Debug State for P&L: {state}") # logger.info(f"Debug State for P&L: {state}") pass is_long = position == 1 color = "#26a69a" if is_long else "#ef5350" side = "LONG" if is_long else "SHORT" icon = "๐Ÿ“ˆ" if is_long else "๐Ÿ“‰" # Get entry price from state - check multiple field names for compatibility # Priority: position_price > entry_price > price (last trade price fallback) entry_price = state.get('position_price') or state.get('entry_price') or state.get('price', current_price) # Validation: Entry price must be reasonable (within 50% of current price) if entry_price > 0 and current_price > 0: price_diff_pct = abs(entry_price - current_price) / current_price if price_diff_pct > 0.5: # More than 50% difference is suspicious logger.warning(f"Entry price ${entry_price:,.2f} is {price_diff_pct*100:.1f}% different from current ${current_price:,.2f} - using current price") entry_price = current_price # Get SL/TP from state (preferred) or calculate sl_price = state.get('sl', 0) tp_price = state.get('tp', 0) if sl_price == 0 or tp_price == 0: # Fallback to estimation if is_long: sl_price = entry_price * (1 - SL_PCT) tp_price = entry_price * (1 + TP_PCT) else: sl_price = entry_price * (1 + SL_PCT) tp_price = entry_price * (1 - TP_PCT) # Calculate Unrealized PnL units = state.get('position_size_units', state.get('position_units', state.get('units', 0))) if is_long: unrealized_pnl = (current_price - entry_price) * units else: unrealized_pnl = (entry_price - current_price) * units pnl_color = "#26a69a" if unrealized_pnl >= 0 else "#ef5350" pnl_sign = "+" if unrealized_pnl >= 0 else "" st.markdown(f"""
Current Position {icon} {side}
Entry Price: ${entry_price:,.2f}
Current Price: ${current_price:,.2f}
Unrealized P&L: {pnl_sign}${unrealized_pnl:,.2f}
๐Ÿ›‘ Stop Loss: ${sl_price:,.2f}
๐ŸŽฏ Take Profit: ${tp_price:,.2f}
""", unsafe_allow_html=True) def render_trade_history(trades: list): """Render real trade history.""" st.markdown('
Recent Trades
', unsafe_allow_html=True) action_trades = [t for t in trades if 'action' in t and t['action'] != 'HOLD'] if not action_trades: st.info("No trades yet") return for trade in reversed(action_trades[-10:]): action = trade.get('action', '') price = trade.get('price', 0) pnl = trade.get('pnl', 0) timestamp = trade.get('timestamp', '') reason = trade.get('reason', 'model') try: ts = datetime.fromisoformat(timestamp) time_str = ts.strftime('%m/%d %H:%M') except: time_str = '' # Determine display based on action and reason if 'OPEN_LONG' in action: color = "#26a69a" side = "LONG" elif 'OPEN_SHORT' in action: color = "#ef5350" side = "SHORT" elif 'CLOSE' in action: if reason == 'stop_loss': color = "#ff4444" side = "SL" elif reason == 'take_profit': color = "#00ff88" side = "TP" else: color = "#ffc107" side = "EXIT" else: color = "#888" side = action pnl_color = "#26a69a" if pnl >= 0 else "#ef5350" pnl_sign = "+" if pnl >= 0 else "" pnl_display = f"{pnl_sign}${pnl:,.2f}" if pnl != 0 else "" st.markdown(f"""
{side} ${price:,.2f} {time_str}
{pnl_display}
""", unsafe_allow_html=True) def load_real_market_data(symbol: str = 'BTC/USDT', timeframe: str = '1h') -> pd.DataFrame: """Load OHLCV candlestick data โ€” via /api/ohlcv or direct Binance public API.""" import requests as _mkt_requests import logging as _log _logger = _log.getLogger(__name__) clean_symbol = symbol.replace("/", "") def _parse_ohlcv_list(data: list) -> pd.DataFrame: """Parse list of {time,open,high,low,close,volume} dicts into DataFrame.""" df = pd.DataFrame(data) df.index = pd.to_datetime(df['time'], unit='s') df.index.name = None return df[['open', 'high', 'low', 'close', 'volume']] # Primary: /api/ohlcv via local Flask server api_url = get_api_url() try: resp = _mkt_requests.get( f'{api_url}/api/ohlcv', params={'symbol': clean_symbol, 'interval': timeframe, 'limit': 500}, timeout=10 ) if resp.ok: data = resp.json() if data and isinstance(data, list) and len(data) > 0: return _parse_ohlcv_list(data) else: _logger.warning(f"load_real_market_data: empty/invalid response from {api_url} for {clean_symbol} {timeframe}: {str(data)[:200]}") else: _logger.warning(f"load_real_market_data: HTTP {resp.status_code} from {api_url}/api/ohlcv for {clean_symbol} {timeframe}") except Exception as e: _logger.warning(f"load_real_market_data: Flask API unavailable ({api_url}): {e}") # Fallback: Direct Binance public API (no auth required, works on HF) try: _logger.info(f"load_real_market_data: trying direct Binance API for {clean_symbol} {timeframe}") binance_url = os.environ.get("BINANCE_FUTURES_URL", "https://data-api.binance.vision") resp = _mkt_requests.get( f"{binance_url}/api/v3/klines", params={'symbol': clean_symbol, 'interval': timeframe, 'limit': 500}, timeout=15 ) if resp.ok: raw = resp.json() if isinstance(raw, list) and len(raw) > 0 and not (isinstance(raw, dict) and raw.get('code')): candles = [ { 'time': int(row[0]) // 1000, 'open': float(row[1]), 'high': float(row[2]), 'low': float(row[3]), 'close': float(row[4]), 'volume': float(row[5]), } for row in raw ] _logger.info(f"load_real_market_data: direct Binance returned {len(candles)} candles for {clean_symbol} {timeframe}") return _parse_ohlcv_list(candles) else: _logger.warning(f"load_real_market_data: Binance direct API returned unexpected data: {str(raw)[:200]}") else: _logger.warning(f"load_real_market_data: Binance direct API HTTP {resp.status_code} for {clean_symbol} {timeframe}") except Exception as e: _logger.error(f"load_real_market_data: direct Binance fallback failed: {e}") # Final fallback: BinanceHistoricalDataFetcher if backtest module available (local server only) if _HAS_BACKTEST: try: fetcher = BinanceHistoricalDataFetcher() end_date = datetime.now() days = TIMEFRAMES.get(timeframe, {}).get('days', 7) start_date = end_date - timedelta(days=days) if "USDT" in symbol and "/" not in symbol: symbol = symbol.replace("USDT", "/USDT") df = fetcher.fetch_historical_data( symbol=symbol, timeframe=timeframe, start_date=start_date, end_date=end_date, ) return df except Exception as e: _logger.error(f"load_real_market_data: BinanceHistoricalDataFetcher failed: {e}") return pd.DataFrame() return pd.DataFrame() @st.fragment(run_every=60) def render_sidebar_metrics_fragment(): """Render sidebar portfolio metrics with auto-refresh.""" import requests import logging logger = logging.getLogger(__name__) try: # Fetch State try: state_resp = requests.get(f'{get_api_url()}/api/state', timeout=5) if state_resp.status_code == 200: api_state = state_resp.json() # Update session state with API data (optional, but good for other parts) if 'balance' in api_state: st.session_state.portfolio_balance = api_state.get('balance', 0) st.session_state.total_pnl = api_state.get('total_pnl', 0) # Render st.markdown(f"""
Portfolio Value
{f'${st.session_state["portfolio_balance"]:,.2f}' if st.session_state.get('portfolio_balance') is not None else 'โ€”'}
P&L: {'+' if float(st.session_state.get('total_pnl') or 0) >= 0 else ''}${float(st.session_state.get('total_pnl') or 0):,.2f}
""", unsafe_allow_html=True) except Exception as e: st.markdown(f"
Connection Error
", unsafe_allow_html=True) except Exception as e: logger.error(f"Sidebar data fetch error: {e}") @st.fragment(run_every=120) def render_market_analysis_fragment(symbol: str): """Render market analysis panel with auto-refresh.""" import requests import logging logger = logging.getLogger(__name__) st.markdown("### ๐Ÿ“Š Market Analysis") # Fetch Market Analysis for current asset market_data = {} try: api_symbol = symbol.replace('/', '').upper() market_resp = requests.get(f'{get_api_url()}/api/market?symbol={api_symbol}', timeout=15) if market_resp.status_code == 200: market_data = market_resp.json() else: st.markdown(f"""
๐Ÿ“Š Market Analysis
API error (HTTP {market_resp.status_code})
Server returned non-200 for /api/market
""", unsafe_allow_html=True) return except Exception as e: st.markdown(f"""
๐Ÿ“Š Market Analysis
Unable to load (API server offline?)
Error: {str(e)}
""", unsafe_allow_html=True) return # Whale Tracker whale = market_data.get('whale', {}) if whale: if whale.get('error'): st.markdown(f"""
๐Ÿ‹ Whale Signals
Data Error
{whale.get('error')}
""", unsafe_allow_html=True) else: whale_color = "#26a69a" if whale.get('score', 0) > 0 else "#ef5350" if whale.get('score', 0) < 0 else "#888" whale_emoji = "๐ŸŸข" if whale.get('score', 0) > 0.1 else "๐Ÿ”ด" if whale.get('score', 0) < -0.1 else "โšช" # Format Flow Metrics flow_metrics = whale.get('flow_metrics', {}) net_flow = flow_metrics.get('net_flow', 0) flow_color = "#26a69a" if net_flow > 0 else "#ef5350" flow_sign = "+" if net_flow > 0 else "-" # Format to K or M if abs(net_flow) > 1000000: flow_str = f"{flow_sign}${abs(net_flow)/1000000:.1f}M" elif abs(net_flow) > 1000: flow_str = f"{flow_sign}${abs(net_flow)/1000:.0f}K" else: flow_str = "$0" st.markdown(f"""
๐Ÿ‹ Whale Signals
{whale_emoji} {whale.get('direction', 'NEUTRAL')}
Score: {whale.get('score', 0):.2f} | Conf: {whale.get('confidence', 0)}%
Flow (1m): {flow_str}
๐ŸŸข{whale.get('bullish', 0)} ๐Ÿ”ด{whale.get('bearish', 0)} โšช{whale.get('neutral', 0)}
""", unsafe_allow_html=True) # Funding funding_data = market_data.get('funding', {}) funding = funding_data.get('data', {}) # structure varies, being safe if funding_data and not funding_data.get('error'): # Extract funding rate rate = funding_data.get('rate', 0) funding_color = "#26a69a" if rate > 0.0001 else "#ef5350" if rate < -0.0001 else "#888" st.markdown(f"""
๐Ÿ’ฐ Funding Rate
{rate:.4f}%
Bias: {funding_data.get('bias', 'neutral')} | APR: {funding_data.get('annualized', 0):.1f}%
""", unsafe_allow_html=True) # Order Flow (enhanced 3-layer) order_flow = market_data.get('order_flow', {}) if order_flow and not order_flow.get('error'): of_bias = order_flow.get('bias', 'neutral') of_score = order_flow.get('score', 0) of_color = "#26a69a" if of_bias == 'bullish' else "#ef5350" if of_bias == 'bearish' else "#888" # Layer details cvd_data = order_flow.get('cvd', {}) taker_data = order_flow.get('taker', {}) notable_data = order_flow.get('notable', {}) cvd_trend = cvd_data.get('trend', 'n/a') taker_ratio = taker_data.get('ratio', 0.5) notable_buys = notable_data.get('large_buys', order_flow.get('large_buys', 0)) notable_sells = notable_data.get('large_sells', order_flow.get('large_sells', 0)) st.markdown(f"""
๐Ÿ“Š Order Flow
{of_bias.upper()} ({(of_score or 0):+.2f})
CVD: {cvd_trend} | Taker Buy: {taker_ratio:.0%}
Notable: B:{notable_buys} / S:{notable_sells}
""", unsafe_allow_html=True) # News Sentiment - DISABLED (not reliable, removed per user request) # Commented out - news sentiment disabled in trading logic # news_data = market_data.get('news') # if news_data is not None and isinstance(news_data, dict): # news_sentiment = news_data.get('sentiment', 0) # news_conf = news_data.get('confidence', 0) # news_trend = news_data.get('trend', 'unknown') # news_sources = news_data.get('sources', 0) # # # Sentiment color and emoji # news_color = "#26a69a" if news_sentiment > 0.1 else "#ef5350" if news_sentiment < -0.1 else "#888" # news_emoji = "๐ŸŸข" if news_sentiment > 0.1 else "๐Ÿ”ด" if news_sentiment < -0.1 else "โšช" # sentiment_label = "Bullish" if news_sentiment > 0.1 else "Bearish" if news_sentiment < -0.1 else "Neutral" # # # Trend indicator # trend_emoji = "๐Ÿ“ˆ" if news_trend == "improving" else "๐Ÿ“‰" if news_trend == "deteriorating" else "โžก๏ธ" # # st.markdown(f""" #
#
๐Ÿ“ฐ News Sentiment
#
{news_emoji} {sentiment_label} ({news_sentiment:+.2f})
#
# Confidence: {news_conf:.0%} | Trend: {trend_emoji} {news_trend}
# Sources: {news_sources}/3 (CryptoCompare) #
#
# """, unsafe_allow_html=True) # else: # # Show placeholder when news data is not available yet # st.markdown(f""" #
#
๐Ÿ“ฐ News Sentiment
#
Loading...
#
# Waiting for first news fetch (takes ~1-2 min) #
#
# """, unsafe_allow_html=True) # HMM Regime regime_data = market_data.get('regime', {}) if regime_data and not regime_data.get('error'): r_type = regime_data.get('type', 'UNKNOWN') # Colors: Green for Bull, Red for Bear, Orange for Breakout, Blue for Range r_color = "#26a69a" if "BULL" in r_type else "#ef5350" if "BEAR" in r_type else "#ffa726" if "BREAKOUT" in r_type else "#42a5f5" st.markdown(f"""
๐Ÿ‘‘ Market Regime (HMM)
{r_type.replace('_', ' ')}
ADX: {regime_data.get('adx', 0)} | Volatility: {regime_data.get('volatility', 1.0)}x
""", unsafe_allow_html=True) # TFT Forecast forecast = market_data.get('forecast') if forecast: ret_4h = forecast.get('return_4h', 0) fc_color = "#26a69a" if ret_4h > 0 else "#ef5350" if ret_4h < 0 else "#888" fc_sign = "+" if ret_4h > 0 else "" st.markdown(f"""
๐Ÿš€ AI Price Forecast (TFT)
4h: {fc_sign}{ret_4h}% | 12h: {forecast.get('return_12h', 0)}%
Consensus: {forecast.get('consensus', 0):.2f} | Confidence: {forecast.get('confidence', 0):.2f}
""", unsafe_allow_html=True) # Ensemble Confidence Engine confidence = market_data.get('ensemble_confidence') if confidence is not None: conf_pct = min(100, max(0, int(confidence * 100))) # Map 0-1.0 to 0.25x - 2.0x for UI display (matching the ConfidenceEngine logic roughly) mult = 0.25 + 1.75 * confidence if confidence < 0.5 else 1.0 + 1.0 * (confidence - 0.5) * 2 # Approximate for UI c_color = "#26a69a" if confidence > 0.6 else "#ffa726" if confidence > 0.35 else "#ef5350" st.markdown(f"""
๐Ÿง  Ensemble Agreement
{conf_pct}% Alignment
Position Size Multiplier: ~{mult:.1f}x
""", unsafe_allow_html=True) @st.fragment(run_every=30) def render_position_fragment(symbol: str): """Render current position and portfolio status with auto-refresh.""" import requests import os from datetime import datetime import logging logger = logging.getLogger(__name__) # 1. Fetch Trading State state = {} try: state_resp = requests.get(f'{get_api_url()}/api/state', timeout=5) if state_resp.status_code == 200: state = state_resp.json() except Exception as e: logger.error(f"State fetch error: {e}") # 2. Fetch Live Price (Fast, from API or Fallback) current_price = 0.0 try: # Try to get price from market API first (faster) clean_symbol = symbol.replace('/', '').upper() market_resp = requests.get(f'{get_api_url()}/api/market?symbol={clean_symbol}', timeout=5) if market_resp.status_code == 200: m_data = market_resp.json() if 'price' in m_data: current_price = float(m_data['price']) # Fallback if API didn't return price if current_price == 0: live_data = load_real_market_data(symbol, '1m') if not live_data.empty: current_price = float(live_data.iloc[-1]['close']) else: live_1h = load_real_market_data(symbol, '1h') if not live_1h.empty: current_price = float(live_1h.iloc[-1]['close']) except Exception as e: logger.error(f"Price fetch error: {e}") # 3. Fetch ALL Trades early to calculate perfectly mathematically synced global Portfolio Value all_trades = [] try: trades_resp = requests.get(f'{get_api_url()}/api/trades', timeout=5) if trades_resp.status_code == 200: all_trades = trades_resp.json() except Exception as e: logger.error(f"Trades fetch error: {e}") realized_pnl_total = sum(t.get('pnl', 0) for t in all_trades if 'CLOSE' in t.get('action', '').upper() or 'EXIT' in t.get('action', '').upper()) open_pnl_total = 0.0 raw_assets = state.get('raw_state', {}).get('assets', {}) for sym, asset_data in raw_assets.items(): if asset_data.get('position', 0) != 0: open_pnl_total += asset_data.get('pnl', 0) if all_trades or raw_assets: total_pnl = realized_pnl_total + open_pnl_total else: total_pnl = state.get('total_pnl', state.get('realized_pnl', 0)) balance = state.get('total_balance', state.get('balance')) pnl_class = "metric-delta-positive" if total_pnl >= 0 else "metric-delta-negative" pnl_sign = "+" if total_pnl >= 0 else "" st.markdown(f"""
Portfolio Value
{f'${balance:,.2f}' if balance is not None else 'โ€”'}
P&L: {pnl_sign}${(total_pnl or 0):,.2f}
""", unsafe_allow_html=True) # 4. Render Position Card # Extract specific asset state from global state asset_state = {} if 'assets' in state: # Try exact match or cleaned match clean_symbol = symbol.replace('/', '').upper() if symbol in state['assets']: asset_state = state['assets'][symbol] elif clean_symbol in state['assets']: asset_state = state['assets'][clean_symbol] # If not found, fall back to global state (in case API returns single asset state) if not asset_state and 'position' in state: asset_state = state # NORMALIZE STATE: Ensure position_price is set for P&L calc # CRITICAL: entry_price is the actual entry price, price is the current price # Must prioritize entry_price over price to avoid showing current price as entry if asset_state: if 'position_price' not in asset_state and 'entry_price' in asset_state: asset_state['position_price'] = asset_state['entry_price'] elif 'position_price' not in asset_state and 'price' in asset_state: # Only use 'price' if entry_price is not available (legacy compatibility) asset_state['position_price'] = asset_state['price'] render_position_card(asset_state, current_price, symbol) # 5. Render Trade History # Filter trades for current symbol (already fetched above) clean_symbol = symbol.replace('/', '').upper() trades = [t for t in all_trades if t.get('symbol', '').replace('/', '').upper() == clean_symbol] render_trade_history(trades) @st.fragment(run_every=60) def render_agent_status_fragment(): """Render active agent status and model info with auto-refresh.""" import requests import os from datetime import datetime import logging logger = logging.getLogger(__name__) # In client mode, use /api/model which returns pre-computed model stats if IS_CLIENT_MODE: try: model_resp = requests.get(f'{get_api_url()}/api/model', timeout=5) if model_resp.status_code == 200: model_info = model_resp.json() total_return = model_info.get('total_return', 0) win_rate = model_info.get('win_rate', 0) total_trades = model_info.get('total_trades', 0) model_date = model_info.get('model_date', 'Remote') model_exists = model_info.get('model_exists', True) else: total_return, win_rate, total_trades = 0, 0, 0 model_date, model_exists = 'API error', False except Exception: total_return, win_rate, total_trades = 0, 0, 0 model_date, model_exists = 'Connecting...', False else: # Local mode: check filesystem and compute from trades project_root = Path(__file__).parent.parent.parent model_path = project_root / 'data' / 'models' / 'ultimate_agent.zip' model_exists = model_path.exists() state = {} try: state_resp = requests.get(f'{get_api_url()}/api/state', timeout=5) if state_resp.status_code == 200: state = state_resp.json() except Exception: pass all_trades = state.get('trades', []) try: trades_resp = requests.get(f'{get_api_url()}/api/trades', timeout=5) if trades_resp.status_code == 200: all_trades = trades_resp.json() except Exception: pass realized_pnl = sum(t.get('pnl', 0) for t in all_trades if 'CLOSE' in t.get('action', '').upper() or 'EXIT' in t.get('action', '').upper()) raw_assets = state.get('raw_state', {}).get('assets', {}) open_pnl = 0.0 for sym, asset_data in raw_assets.items(): if asset_data.get('position', 0) != 0: current_price = asset_data.get('price', 0) units = asset_data.get('units', 0) position = asset_data.get('position', 0) entry_price = 0 sym_trades = [t for t in all_trades if t.get('symbol', '').upper() == sym.upper() or t.get('asset', '').upper() == sym.upper()] for t in reversed(sorted(sym_trades, key=lambda x: x.get('timestamp', ''))): if 'OPEN' in t.get('action', '').upper(): entry_price = t.get('price', 0) break if entry_price > 0 and units > 0 and current_price > 0: if position > 0: open_pnl += (current_price - entry_price) * units else: open_pnl += (entry_price - current_price) * units total_pnl = realized_pnl + open_pnl total_return = None # Cannot compute without knowing real initial capital closed_trades = [t for t in all_trades if 'CLOSE' in t.get('action', '').upper() or 'EXIT' in t.get('action', '').upper()] if closed_trades: winning = sum(1 for t in closed_trades if t.get('pnl', 0) > 0) win_rate = (winning / len(closed_trades) * 100) else: win_rate = 0 total_trades = len(all_trades) if model_exists: try: model_mtime = datetime.fromtimestamp(os.path.getmtime(model_path)) model_date = model_mtime.strftime("%Y-%m-%d") except Exception: model_date = "Unknown" else: model_date = "Not found" return_str = f"{'+' if total_return >= 0 else ''}{total_return:.2f}% Return" if total_return is not None else "N/A Return" return_color = "#26a69a" if (total_return or 0) >= 0 else "#ef5350" st.markdown(f"""
Active Model
Ultimate Agent (PPO)
{return_str} | {win_rate:.1f}% Win Rate
Trades: {total_trades} | Model: {model_date}
{'โœ“ Model loaded' if model_exists else 'โœ— Model not found'}
""", unsafe_allow_html=True) def on_asset_change(): """Callback for asset selection change.""" # Clear stale market analysis to trigger fresh fetch in fragments st.session_state.market_analysis = None # Optional: Reset other asset-specific state if needed def main(): """Main application entry point.""" # Initialize session state for timeframe if 'timeframe' not in st.session_state: st.session_state.timeframe = '1h' # Initialize session state for selected asset if 'selected_asset' not in st.session_state: st.session_state.selected_asset = 'BTCUSDT' # Check for multi-asset state to populate selector state_preview = get_trading_state() available_assets = state_preview.get('available_assets', ['BTCUSDT']) # Initialize session state for auto-refresh (kept for toggle state only) if 'auto_refresh' not in st.session_state: st.session_state.auto_refresh = True # Sidebar with st.sidebar: st.markdown("### โš™๏ธ Settings") # Asset Selector if len(available_assets) > 1: st.session_state.selected_asset = st.selectbox( "Select Asset", available_assets, index=available_assets.index(st.session_state.selected_asset) if st.session_state.selected_asset in available_assets else 0, on_change=on_asset_change ) else: st.markdown(f"**Asset:** {st.session_state.selected_asset}") st.divider() st.markdown("### ๐Ÿž Debug") if st.checkbox("Show Crash Log"): log_path = project_root / "crash.log" if log_path.exists(): st.error("โš ๏ธ Crash Log Found") with open(log_path, "r") as f: st.text_area("Log Content", f.read(), height=300) else: st.success("โœ… No crash log found") if st.checkbox("Show Process Log (Stdout/Stderr)"): proc_log = project_root / "process.log" if proc_log.exists(): with open(proc_log, "r") as f: st.text_area("Process Output", f.read(), height=300) else: st.warning("โš ๏ธ process.log not found (yet)") if st.checkbox("Show API Server Log"): api_log = project_root / "api_server.log" if api_log.exists(): with open(api_log, "r") as f: st.text_area("API Server Output", f.read(), height=300) else: st.warning("โš ๏ธ api_server.log not found (yet)") # Storage path is server-side only; omitted from client UI # Database Reset (Dev Only) env = os.getenv("ENVIRONMENT", "production").lower() if env in ["dev", "development"]: st.divider() st.markdown("### ๐Ÿ”„ Database Reset") st.warning("โš ๏ธ This will clear all trades and positions!") if st.button("๐Ÿ—‘๏ธ Reset All Trades", type="primary"): try: import subprocess reset_script = project_root / "reset_all_storage.py" if reset_script.exists(): result = subprocess.run( [sys.executable, str(reset_script)], capture_output=True, text=True, cwd=str(project_root) ) if result.returncode == 0: st.success("โœ… Database reset successful!") st.code(result.stdout) st.info("๐Ÿ”„ Refresh the page to see changes") else: st.error(f"โŒ Reset failed: {result.stderr}") else: st.error(f"โŒ Reset script not found at {reset_script}") except Exception as e: st.error(f"โŒ Error running reset: {e}") if st.checkbox("Show System Inspector"): st.markdown("#### ๐Ÿ•ต๏ธ System Inspector") if st.button("List Processes (ps aux)"): try: import subprocess # Use 'ps aux' for more details, or 'ps -ef' res = subprocess.run(['ps', 'aux'], capture_output=True, text=True) st.code(res.stdout if res.returncode == 0 else res.stderr) except Exception as e: st.error(f"Failed to run ps: {e}") if st.button("List Files (ls -R)"): try: import subprocess res = subprocess.run(['ls', '-R'], capture_output=True, text=True) st.code(res.stdout if res.returncode == 0 else res.stderr) except Exception as e: st.error(f"Failed to run ls: {e}") if st.button("Check Connectivity (ping google.com)"): try: import subprocess res = subprocess.run(['ping', '-c', '3', 'google.com'], capture_output=True, text=True) st.code(res.stdout if res.returncode == 0 else res.stderr) except Exception as e: st.error(f"Ping failed: {e}") st.markdown("### ๐Ÿ”‘ API Status") eth_key = os.environ.get("ETHERSCAN_API_KEY") sol_key = os.environ.get("SOLSCAN_API_KEY") xrp_key = os.environ.get("XRPSCAN_API_KEY") st.caption(f"ETH: {'โœ… Set' if eth_key else 'โŒ Missing'}") st.caption(f"SOL: {'โœ… Set' if sol_key else 'โŒ Missing'}") st.caption(f"XRP: {'โœ… Set' if xrp_key else 'โšช Optional (Public)'}") # Header col1, col2, col3 = st.columns([3, 1, 1]) with col1: st.markdown(f"# ๐Ÿค– DRL Trading System - {st.session_state.selected_asset}") with col2: refresh_status = "๐Ÿ”„ Auto (10s)" if st.session_state.auto_refresh else "โธ๏ธ Paused" st.markdown(f"""
๐ŸŸข Connected
{refresh_status}
""", unsafe_allow_html=True) with col3: # Auto-refresh toggle st.session_state.auto_refresh = st.toggle("Auto Refresh", value=st.session_state.auto_refresh) # Data fetching is now handled inside fragments (render_sidebar_metrics_fragment, render_market_analysis_fragment) pass # Render Sidebar Metrics using Fragment with st.sidebar: render_sidebar_metrics_fragment() st.divider() # Main layout col_main, col_sidebar = st.columns([3, 1]) with col_main: # Timeframe selector st.markdown("#### Select Timeframe") tf_cols = st.columns(7) timeframes = ['1m', '5m', '15m', '30m', '1h', '4h', '1d'] for i, tf in enumerate(timeframes): with tf_cols[i]: label = TIMEFRAMES[tf]['label'] if st.button(label, key=f"tf_{tf}", use_container_width=True, type="primary" if st.session_state.timeframe == tf else "secondary"): st.session_state.timeframe = tf st.rerun() # Load data for selected timeframe and asset with st.spinner(f"Loading {st.session_state.selected_asset} {st.session_state.timeframe} data..."): df = load_real_market_data(st.session_state.selected_asset, st.session_state.timeframe) state = get_trading_state(st.session_state.selected_asset) current_price = float(df.iloc[-1]['close']) if not df.empty else 0 # Tabs tab_chart, tab_live_portfolio, tab_performance, tab_whales, tab_testnet, tab_htf, tab_backtest = st.tabs([ "๐Ÿ“Š Live Chart", "๐Ÿ’ผ Live Portfolio", "๐Ÿ“ˆ Performance", "๐Ÿ‹ On-Chain Whales", "๐Ÿงช Testnet", "๐Ÿ”ฎ HTF Agent", "๐Ÿ”ฌ Backtest" ]) with tab_chart: # TradingView Chart with WebSocket trades = state.get('trades', []) chart_html = create_tradingview_chart_with_websocket(df, trades, st.session_state.timeframe, st.session_state.selected_asset) # Append timestamp comment to force re-render since components.html doesn't support key # Create a placeholder for the chart to force re-rendering chart_placeholder = st.empty() # Append timestamp comment to force re-render since components.html doesn't support key current_time = time.time() chart_html += f"" with chart_placeholder: components.html(chart_html, height=600) # Info about trade markers num_trades = len([t for t in trades if 'OPEN' in t.get('action', '')]) st.caption(f"๐Ÿ“ {num_trades} trade signals on chart โ€ข Switch timeframes to see trades at different intervals") # Trading Controls Section st.markdown("---") st.markdown("### ๐ŸŽฎ Trading Controls") if IS_CLIENT_MODE: st.info("๐ŸŒ **Client Mode** โ€” Trading bot is managed on the remote server. Use the server dashboard to start/stop the bot or place manual trades.") if st.button("๐Ÿ”„ Refresh Data", key="refresh_data", use_container_width=True): st.rerun() else: # Bot status check (server-side only) import subprocess bot_running = False try: result = subprocess.run(['pgrep', '-f', 'live_trading'], capture_output=True, text=True) bot_running = result.returncode == 0 except Exception: pass if bot_running: st.success("๐ŸŸข **Trading Bot is RUNNING** (Multi-Asset Mode)") else: st.warning("๐ŸŸ  **Trading Bot is STOPPED**") ctrl_col1, ctrl_col2, ctrl_col3, ctrl_col4 = st.columns(4) with ctrl_col1: if not bot_running: if st.button("โ–ถ๏ธ Start Trading", key="start_trading", use_container_width=True, type="primary"): try: with open(project_root / "process.log", "a") as log_file: subprocess.Popen( ['./venv/bin/python', '-u', 'live_trading_multi.py', '--assets', 'BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'XRPUSDT', '--balance', '5000'], cwd=str(project_root), stdout=log_file, stderr=log_file, ) st.success("โœ“ Multi-Asset Bot started!") time.sleep(2) st.rerun() except Exception as e: st.error(f"Failed to start: {e}") else: if st.button("โน๏ธ Stop Trading", key="stop_trading", use_container_width=True, type="secondary"): try: subprocess.run(['pkill', '-f', 'live_trading'], check=False) st.info("โœ“ Trading bot stopped") time.sleep(1) st.rerun() except Exception as e: st.error(f"Failed to stop: {e}") with ctrl_col2: if st.button("๐Ÿ“ˆ Open Long", key="open_long", use_container_width=True): trade = { 'timestamp': datetime.now().isoformat(), 'action': 'OPEN_LONG', 'price': current_price, 'pnl': 0, 'balance': state.get('balance'), 'position': 1, 'reason': 'manual', 'symbol': st.session_state.selected_asset, 'asset': st.session_state.selected_asset, } storage.log_trade(trade) st.success(f"โœ“ Opened LONG @ ${current_price:,.2f}") time.sleep(0.5) st.rerun() with ctrl_col3: if st.button("๐Ÿ“‰ Open Short", key="open_short", use_container_width=True): trade = { 'timestamp': datetime.now().isoformat(), 'action': 'OPEN_SHORT', 'price': current_price, 'pnl': 0, 'balance': state.get('balance'), 'position': -1, 'reason': 'manual', 'symbol': st.session_state.selected_asset, 'asset': st.session_state.selected_asset, } storage.log_trade(trade) st.success(f"โœ“ Opened SHORT @ ${current_price:,.2f}") time.sleep(0.5) st.rerun() with ctrl_col4: if st.button("๐Ÿšช Close Position", key="close_position", use_container_width=True): position = state.get('position', 0) if position != 0: action = 'CLOSE_LONG' if position == 1 else 'CLOSE_SHORT' trade = { 'timestamp': datetime.now().isoformat(), 'action': action, 'price': current_price, 'pnl': 0, 'balance': state.get('balance'), 'position': 0, 'reason': 'manual', 'symbol': st.session_state.selected_asset, 'asset': st.session_state.selected_asset, } storage.log_trade(trade) st.success(f"โœ“ Closed position @ ${current_price:,.2f}") time.sleep(0.5) st.rerun() else: st.info("No position to close") st.markdown("") action_col1, action_col2 = st.columns(2) with action_col1: if st.button("๐Ÿ”„ Refresh Data", key="refresh_data", use_container_width=True): st.rerun() with action_col2: if st.button("๐Ÿ—‘๏ธ Clear Trade Log", key="clear_log", use_container_width=True): try: log_file = project_root / 'logs' / 'trading_log.json' state_file = project_root / 'logs' / 'trading_state.json' log_file.write_text('') if state_file.exists(): state_file.unlink() st.success("โœ“ Trade log cleared") time.sleep(0.5) st.rerun() except Exception as e: st.error(f"Failed to clear log: {e}") with tab_live_portfolio: # โ”€โ”€โ”€ Compute portfolio metrics from trade data โ”€โ”€โ”€ all_trades_lp = [] try: # Use API in client mode, local storage otherwise all_trades_lp = load_trading_log() if not IS_CLIENT_MODE: # Apply reset filter (local mode only โ€” API already filters) try: lp_state = storage.load_state() reset_ts = lp_state.get('reset_timestamp') if reset_ts: reset_dt = datetime.fromisoformat(reset_ts.replace('Z', '+00:00')) all_trades_lp = [t for t in all_trades_lp if datetime.fromisoformat(t.get('timestamp', '2020-01-01').replace('Z', '+00:00')) >= reset_dt] except: pass except: pass # Separate by symbol and compute per-asset metrics assets_by_symbol = {} for t in all_trades_lp: sym = t.get('symbol', t.get('asset', 'UNKNOWN')) sym = sym.replace('/', '').upper() if sym not in assets_by_symbol: assets_by_symbol[sym] = [] assets_by_symbol[sym].append(t) # Compute closed P&L, open P&L, win rate realized_pnl_total = 0.0 open_pnl_total = 0.0 total_closed_trades = 0 total_winning_trades = 0 total_open_trades = 0 equity_points = [0.0] # Start at 0% asset_rows = [] # FIX: State structure is state['assets'], not state['raw_state']['assets'] raw_assets = state.get('assets', {}) for sym, trades_list in assets_by_symbol.items(): sorted_trades = sorted(trades_list, key=lambda x: x.get('timestamp', '')) sym_realized = 0.0 sym_open_pnl = 0.0 sym_wins = 0 sym_closed = 0 sym_open = 0 sym_best = None sym_worst = None sym_status = 'FLAT' for t in sorted_trades: action = t.get('action', '').upper() pnl = t.get('pnl', 0) or 0 if 'CLOSE' in action or 'EXIT' in action: sym_realized += pnl sym_closed += 1 if pnl > 0: sym_wins += 1 equity_points.append(equity_points[-1] + pnl) # Track best/worst if sym_best is None or pnl > sym_best: sym_best = pnl if sym_worst is None or pnl < sym_worst: sym_worst = pnl elif 'OPEN_LONG' in action: sym_status = 'LONG' sym_open += 1 elif 'OPEN_SHORT' in action: sym_status = 'SHORT' sym_open += 1 # FIX: Determine final status from last trade (if it was a CLOSE, position is FLAT) if sorted_trades: last_trade = sorted_trades[-1] last_action = last_trade.get('action', '').upper() if 'CLOSE' in last_action or 'EXIT' in last_action: sym_status = 'FLAT' # Check current state for position status (this overrides trade-based status) if sym in raw_assets: asset_data = raw_assets[sym] if asset_data.get('position', 0) != 0: # Calculate unrealized P&L from entry price vs current price current_price = asset_data.get('price', 0) units = asset_data.get('units', 0) position = asset_data.get('position', 0) # Find entry price from last OPEN trade entry_price = 0 for t in reversed(sorted_trades): if 'OPEN' in t.get('action', '').upper(): entry_price = t.get('price', 0) break # Calculate unrealized P&L if entry_price > 0 and units > 0 and current_price > 0: if position > 0: # LONG sym_open_pnl = (current_price - entry_price) * units else: # SHORT sym_open_pnl = (entry_price - current_price) * units sym_status = 'LONG' if position > 0 else 'SHORT' else: sym_status = 'FLAT' realized_pnl_total += sym_realized open_pnl_total += sym_open_pnl total_closed_trades += sym_closed total_winning_trades += sym_wins if sym_status != 'FLAT': total_open_trades += 1 # Format display symbol display_sym = sym if sym.endswith('USDT'): display_sym = sym[:-4] + ' /USDT' # Get price / equity from raw state sym_price = raw_assets.get(sym, {}).get('price', 0) # Calculate True Equity mathematically instead of relying on historically corrupted bot.balance sym_equity = 5000 + sym_realized + sym_open_pnl asset_rows.append({ 'symbol': display_sym, 'raw_symbol': sym, 'status': sym_status, 'price': sym_price, 'equity': sym_equity, 'pnl': sym_realized + sym_open_pnl, 'trades': sym_closed + (1 if sym_status != 'FLAT' else 0), 'open_trades': 1 if sym_status != 'FLAT' else 0, 'win_rate': (sym_wins / sym_closed * 100) if sym_closed > 0 else 0, 'wins': sym_wins, 'closed': sym_closed, 'best': sym_best, 'worst': sym_worst, }) # Overall metrics # Fixed: Always use 4 assets (BTCUSDT, ETHUSDT, SOLUSDT, XRPUSDT) # Previously used len(assets_by_symbol) which only counted assets with trades # Calculate from trades (single source of truth) - same logic as Agent Status sidebar lp_grand_total_pnl = realized_pnl_total + open_pnl_total lp_total_balance = state.get('total_balance', state.get('balance')) overall_win_rate = (total_winning_trades / total_closed_trades * 100) if total_closed_trades > 0 else 0 total_trades_count = total_closed_trades + total_open_trades lp_active_assets_count = len(asset_rows) if asset_rows else len(state.get('available_assets', [])) # System status (client mode: infer from recent trades; server mode: check process) if IS_CLIENT_MODE: is_online = bool(all_trades_lp and (datetime.now() - datetime.fromisoformat( all_trades_lp[-1].get('timestamp', '2000-01-01').replace('Z', '+00:00').split('+')[0] )).total_seconds() < 3600) else: is_online = check_process_running("live_trading_multi.py") status_dot = '๐ŸŸข' if is_online else '๐Ÿ”ด' status_text = 'Online' if is_online else 'Offline' status_color = '#00e676' if is_online else '#ff5252' # Color helpers def pnl_color(val): return '#00e676' if val >= 0 else '#ff5252' def pnl_sign(val): return '+' if val >= 0 else '-' # โ”€โ”€โ”€ Build the Live Portfolio HTML โ”€โ”€โ”€ # Equity curve data for SVG chart (pure inline, no CDN) eq_pct = list(equity_points) # absolute PnL values; SVG normalizes by range # Build SVG polyline points svg_w = 900 svg_h = 160 n_points = len(eq_pct) eq_min_val = min(eq_pct) if eq_pct else 0 eq_max_val = max(eq_pct) if eq_pct else 0 eq_range_val = max(abs(eq_min_val), abs(eq_max_val), 0.01) padding_y = 20 # vertical padding svg_points = [] svg_fill_points = [] for i, val in enumerate(eq_pct): x = (i / max(n_points - 1, 1)) * svg_w # Map value from [-range, +range] to [svg_h - padding, padding] y = svg_h - padding_y - ((val + eq_range_val) / (2 * eq_range_val)) * (svg_h - 2 * padding_y) svg_points.append(f"{x:.1f},{y:.1f}") svg_fill_points.append(f"{x:.1f},{y:.1f}") polyline_str = ' '.join(svg_points) # Close the fill polygon at bottom fill_points = svg_fill_points.copy() if fill_points: fill_points.append(f"{svg_w:.1f},{svg_h - padding_y:.1f}") fill_points.append(f"0,{svg_h - padding_y:.1f}") fill_str = ' '.join(fill_points) last_eq = eq_pct[-1] if eq_pct else 0 line_color = '#00e676' if last_eq >= 0 else '#ff5252' fill_color_start = 'rgba(0,230,118,0.3)' if last_eq >= 0 else 'rgba(255,82,82,0.3)' fill_color_end = 'rgba(0,230,118,0.0)' if last_eq >= 0 else 'rgba(255,82,82,0.0)' # Zero line Y position zero_y = svg_h - padding_y - ((0 + eq_range_val) / (2 * eq_range_val)) * (svg_h - 2 * padding_y) # Last point for dot last_x = svg_w if n_points <= 1 else ((n_points - 1) / max(n_points - 1, 1)) * svg_w last_y = svg_h - padding_y - ((last_eq + eq_range_val) / (2 * eq_range_val)) * (svg_h - 2 * padding_y) # Y-axis labels top_label = f"+{eq_range_val:.1f}%" bot_label = f"-{eq_range_val:.1f}%" svg_chart = f''' {top_label} {bot_label} 0% ''' # Build asset rows HTML asset_rows_html = '' for row in asset_rows: # Status badge if row['status'] == 'LONG': status_html = 'โ— LONG' elif row['status'] == 'SHORT': status_html = 'โ— SHORT' else: status_html = 'โ— โ€”' # PNL pnl_val = row['pnl'] pnl_html = f'โ€”' pnl_dollar_html = f'{pnl_sign(pnl_val)}${abs(pnl_val):,.2f}' # Trades trades_str = str(row['trades']) if row['open_trades'] > 0: trades_str += f' (+{row["open_trades"]})' # Win rate bar wr = row['win_rate'] bar_color = '#00e676' if wr >= 50 else '#ff9800' if wr > 0 else '#555' wr_html = f'''
{wr:.0f}%
''' # Best if row['best'] is not None: best_html = f'{pnl_sign(row["best"])}${abs(row["best"]):,.2f}' else: best_html = 'โ€”' # Worst if row['worst'] is not None: worst_html = f'-${abs(row["worst"]):,.2f}' else: worst_html = 'โ€”' asset_rows_html += f''' {row['symbol']} {status_html} ${row['price']:,.2f} ${row['equity']:,.2f} {pnl_html} {pnl_dollar_html} {trades_str} {wr_html} {best_html} {worst_html} ''' if not asset_rows_html: asset_rows_html = ''' No trades recorded yet. Start the trading bot to see portfolio data. ''' portfolio_html = f'''
Live Portfolio LIVE TRADING {status_dot} {status_text}
Realized PNL
{pnl_sign(realized_pnl_total)}${abs(realized_pnl_total):,.2f}
Open PNL
{pnl_sign(open_pnl_total)}${abs(open_pnl_total):,.2f}
Win Rate
{overall_win_rate:.0f}%
{total_winning_trades}W / {total_closed_trades - total_winning_trades}L
Trades
{total_trades_count}
{total_open_trades} open ยท {total_closed_trades} closed
Portfolio Value
{f'${lp_total_balance:,.2f}' if lp_total_balance is not None else 'โ€”'}
Total P&L
{pnl_sign(lp_grand_total_pnl)}${abs(lp_grand_total_pnl):,.2f}
Active Assets
{lp_active_assets_count}
Equity Curve
Cumulative P&L from closed trades
{svg_chart}
{asset_rows_html}
Asset Status Price Equity PNL (%) PNL ($) Trades Win Rate Best Worst
DRL Trading System ยท Signals from PPO + Composite Scoring ยท Connected to OKX
''' components.html(portfolio_html, height=950, scrolling=True) with tab_performance: total_pnl = state.get('realized_pnl', 0) total_trades = state.get('total_trades', 0) balance = state.get('balance', state.get('total_balance')) col1, col2, col3, col4 = st.columns(4) with col1: st.metric( label="Total Return", value="N/A", delta=f"${total_pnl:.2f}" ) with col2: st.metric( label="Portfolio Value", value=f"${balance:,.2f}" if balance is not None else "โ€”", ) with col3: st.metric( label="Total Trades", value=f"{total_trades}", ) with col4: st.metric( label="Realized P&L", value=f"${(total_pnl or 0):,.2f}", ) backtest_file = project_root / 'data' / 'backtest_report.txt' if backtest_file.exists(): st.markdown("### Backtest Results") with open(backtest_file, 'r') as f: st.code(f.read()) with tab_whales: st.markdown("### ๐Ÿ‹ On-Chain Whale Analytics") whale_alerts = state.get('whale_alerts', []) if whale_alerts: import pandas as pd import plotly.express as px df = pd.DataFrame(whale_alerts) df['datetime'] = pd.to_datetime(df['timestamp'], unit='s') # Approximate USD prices for aggregation (since we only have raw crypto values) # This allows us to "tell the story" of total USD economic volume moved price_map = {'BTC': 70000, 'ETH': 3500, 'SOL': 150, 'XRP': 0.6} df['usd_value'] = df.apply(lambda row: row['value'] * price_map.get(row['chain'], 1), axis=1) total_usd = df['usd_value'].sum() top_chain = df.groupby('chain')['usd_value'].sum().idxmax() if not df.empty else "N/A" col1, col2, col3, col4 = st.columns(4) col1.metric("Recent Alerts", len(df)) col2.metric("Trailing Vol (USD)", f"${total_usd/1e6:.1f}M") col3.metric("Most Active Chain", top_chain) eth_whales = len(df[df['chain'] == 'ETH']) xrp_whales = len(df[df['chain'] == 'XRP']) sol_whales = len(df[df['chain'] == 'SOL']) btc_whales = len(df[df['chain'] == 'BTC']) col4.metric("Network Activity", f"BTC:{btc_whales} ETH:{eth_whales} SOL:{sol_whales} XRP:{xrp_whales}") st.divider() chart_col, table_col = st.columns([1.2, 1]) with chart_col: st.markdown("#### ๐Ÿ“Š Whale Volume by Chain (USD)") # Group by chain and enforce order chain_vol = df.groupby('chain')['usd_value'].sum().reset_index() all_chains = pd.DataFrame({'chain': ['BTC', 'ETH', 'SOL', 'XRP']}) chain_vol = pd.merge(all_chains, chain_vol, on='chain', how='left').fillna(0) fig = px.bar( chain_vol, x='chain', y='usd_value', color='chain', text_auto='.2s', color_discrete_map={'BTC': '#F7931A', 'ETH': '#627EEA', 'SOL': '#14F195', 'XRP': '#00AAE4'}, labels={'usd_value': 'Estimated USD Volume', 'chain': 'Network'} ) fig.update_layout( plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', font=dict(color='#8b949e'), height=250, margin=dict(l=0, r=0, t=30, b=0), showlegend=False, xaxis={'categoryorder':'array', 'categoryarray':['BTC','ETH','SOL','XRP']} ) st.plotly_chart(fig, use_container_width=True) st.markdown("#### ๐Ÿ› Volume by Entity Type") if 'wallet_type' in df.columns: type_vol = df.groupby('wallet_type')['usd_value'].sum().reset_index() fig_type = px.pie( type_vol, values='usd_value', names='wallet_type', hole=0.4, color_discrete_sequence=['#F7931A', '#627EEA', '#14F195', '#00AAE4', '#888888'] ) fig_type.update_layout( plot_bgcolor='rgba(0,0,0,0)', paper_bgcolor='rgba(0,0,0,0)', font=dict(color='#8b949e'), height=250, margin=dict(l=0, r=0, t=30, b=0), showlegend=True ) st.plotly_chart(fig_type, use_container_width=True) with table_col: st.markdown("#### ๐Ÿ“ Latest Transaction Feed") # Format table cols_to_keep = ['datetime', 'chain', 'wallet_label', 'wallet_type', 'value', 'usd_value', 'link'] exist_cols = [c for c in cols_to_keep if c in df.columns] display_df = df[exist_cols].copy() display_df = display_df.sort_values('datetime', ascending=False) display_df['datetime'] = display_df['datetime'].dt.strftime('%H:%M:%S') display_df['value'] = display_df.apply(lambda r: f"{r['value']:,.0f} {r['chain']}" if r['value'] >= 1000 else (f"{r['value']:,.2f} {r['chain']}" if r['value'] >= 1 else f"{r['value']:,.4f} {r['chain']}"), axis=1) display_df['usd_value'] = display_df['usd_value'].apply(lambda x: f"${x/1e6:,.1f}M" if x >= 1e6 else f"${x/1000:,.0f}k") rename_map = { 'datetime': 'Time', 'chain': 'Net', 'wallet_label': 'Entity', 'wallet_type': 'Type', 'value': 'Amount', 'usd_value': 'Est. USD', 'link': 'Explorer' } display_df.rename(columns=rename_map, inplace=True) try: st.dataframe( display_df, column_config={ "Explorer": st.column_config.LinkColumn("Explorer", display_text="View TX โ†—") }, use_container_width=True, hide_index=True, height=400 ) except Exception: # Fallback for older Streamlit versions without column_config st.dataframe(display_df.drop(columns=['Explorer']), use_container_width=True, height=400) st.divider() st.markdown("#### ๐Ÿค– AI Momentum Predictions") st.caption("Real-time directional predictions based on institutional flow and wallet behavioral analysis.") pred_cols = st.columns(4) idx = 0 for chain in ['BTC', 'ETH', 'SOL', 'XRP']: chain_df = df[df['chain'] == chain] signal, reason, color = "๐ŸŸก STANDBY", f"Insufficient whale data for {chain}.", "#888888" if not chain_df.empty and 'wallet_type' in chain_df.columns: c_vol = chain_df['usd_value'].sum() if c_vol > 0: types_vol = chain_df.groupby('wallet_type')['usd_value'].sum() acc_vol = types_vol.get('accumulator', 0) exc_vol = types_vol.get('exchange', 0) if acc_vol / c_vol > 0.5: signal, color = "๐ŸŸข BULLISH", "#14F195" reason = f"Supply Shock: {acc_vol/c_vol*100:.0f}% of volume moving to Accumulators." elif exc_vol / c_vol > 0.6: signal, color = "๐Ÿ”ด BEARISH", "#FF4B4B" reason = f"Sell Wall: {exc_vol/c_vol*100:.0f}% of volume flowing into Exchanges." else: signal, color = "๐ŸŸก STANDBY", "#F7931A" reason = "Mixed flows. No clear imbalance." with pred_cols[idx]: st.markdown(f'''

{chain}

{signal}

{reason}

''', unsafe_allow_html=True) idx += 1 else: st.info("๐ŸŒŠ No whale alerts detected yet. Monitoring blockchain for large movements...") with tab_testnet: st.markdown("### ๐Ÿงช Binance Testnet Trading") st.markdown("Real orders on Binance Testnet โ€” bot decisions mirrored live.") # All testnet calls go through the API server (client-mode compatible) import requests as _tn_requests _api = get_api_url() # โ”€โ”€ Fetch all data in parallel โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ tn_data, tn_positions_data, tn_pnl_data, tn_trades_data = {}, {}, {}, {} try: tn_resp = _tn_requests.get(f'{_api}/api/testnet/status', timeout=15) tn_data = tn_resp.json() if tn_resp.status_code == 200 else {} except Exception as _e: st.error(f"โŒ Cannot reach API server: {_e}") try: _pos_resp = _tn_requests.get(f'{_api}/api/testnet/positions', timeout=20) tn_positions_data = _pos_resp.json() if _pos_resp.status_code == 200 else {} except Exception: tn_positions_data = {} try: _pnl_resp = _tn_requests.get(f'{_api}/api/testnet/pnl', timeout=20) tn_pnl_data = _pnl_resp.json() if _pnl_resp.status_code == 200 else {} except Exception: tn_pnl_data = {} try: _trades_resp = _tn_requests.get(f'{_api}/api/testnet/trades?limit=200', timeout=15) tn_trades_data = _trades_resp.json() if _trades_resp.status_code == 200 else {} except Exception: tn_trades_data = {} # โ”€โ”€ Connection status โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ if not tn_data.get('configured', True) or (tn_data.get('error') and not tn_data.get('connected')): st.error(f"โš ๏ธ {tn_data.get('error', 'Testnet not configured on server')}") st.info("Set `BINANCE_TESTNET_API_KEY` and `BINANCE_TESTNET_API_SECRET` in server environment.") else: _key_pfx = tn_data.get('api_key_prefix', '') _connected = tn_data.get('connected', False) status_cols = st.columns([2, 2, 2]) with status_cols[0]: if _connected: st.success(f"โœ… Connected to Binance Testnet") else: st.warning("โš ๏ธ Testnet connection failed") with status_cols[1]: if _key_pfx: st.info(f"๐Ÿ”‘ Key: `{_key_pfx}`") with status_cols[2]: mirror_active = bool(tn_data.get('connected')) st.info(f"๐Ÿค– Auto-Mirror: {'ON (set TESTNET_MIRROR=true)' if mirror_active else 'Enable via TESTNET_MIRROR=true'}") # โ”€โ”€ PNL Summary metrics โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown("---") st.markdown("### ๐Ÿ’ฐ Portfolio & PNL Summary") portfolio_value = float(tn_data.get('portfolio_value', 0) or 0) usdt_balance = float(tn_data.get('usdt_balance', 0) or 0) realized_pnl = float(tn_pnl_data.get('realized_pnl', 0) or 0) unrealized_pnl = float(tn_pnl_data.get('unrealized_pnl', 0) or 0) total_pnl = float(tn_pnl_data.get('total_pnl', 0) or 0) total_trades = int(tn_pnl_data.get('total_trades', 0) or 0) closed_trades = int(tn_pnl_data.get('closed_trades', 0) or 0) win_rate = float(tn_pnl_data.get('win_rate', 0) or 0) winning_trades = int(tn_pnl_data.get('winning_trades', 0) or 0) m1, m2, m3, m4 = st.columns(4) with m1: st.metric( "๐Ÿ’ฐ Portfolio Value", f"${portfolio_value:,.2f}" if portfolio_value is not None else "โ€”", ) with m2: st.metric( "๐Ÿ’ต USDT Balance", f"${usdt_balance:,.2f}" if usdt_balance is not None else "โ€”", ) with m3: st.metric( "๐Ÿ“ˆ Realized PNL", f"${realized_pnl:+,.2f}" if realized_pnl is not None else "โ€”", delta=f"${unrealized_pnl:+,.2f} unrealized" if unrealized_pnl else None, ) with m4: wr_str = f"{win_rate * 100:.1f}%" if win_rate is not None else "โ€”" st.metric( "๐ŸŽฏ Win Rate", wr_str, delta=f"{winning_trades}/{closed_trades} closed" if closed_trades > 0 else None, ) # โ”€โ”€ Bot-Mirrored Open Positions โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown("---") st.markdown("### ๐Ÿ“Š Open Positions (Bot-Mirrored)") bot_positions = tn_positions_data.get('positions', []) if bot_positions: pos_rows = [] for p in bot_positions: sym = p.get('symbol', '') side = p.get('side', '') entry = float(p.get('entry_price', 0) or 0) curr = float(p.get('current_price', 0) or 0) amt = float(p.get('amount', 0) or 0) upnl = float(p.get('unrealized_pnl', 0) or 0) upnl_pct = float(p.get('unrealized_pnl_pct', 0) or 0) sl_p = float(p.get('sl', 0) or 0) tp_p = float(p.get('tp', 0) or 0) conf = float(p.get('confidence', 0) or 0) sim = bool(p.get('simulated', False)) side_display = f"{side} {'(sim)' if sim else ''}" pos_rows.append({ 'Symbol': sym, 'Side': side_display, 'Entry': f"${entry:,.4f}" if entry else "โ€”", 'Current': f"${curr:,.4f}" if curr else "โ€”", 'Amount': f"{amt:.6f}", 'Unreal. PNL': f"${upnl:+,.4f} ({upnl_pct:+.2f}%)" if curr else "โ€”", 'SL': f"${sl_p:,.4f}" if sl_p else "โ€”", 'TP': f"${tp_p:,.4f}" if tp_p else "โ€”", 'Confidence': f"{conf:.2f}" if conf else "โ€”", }) st.dataframe(pd.DataFrame(pos_rows), use_container_width=True, hide_index=True) else: st.info("No open bot-mirrored positions.") # Spot wallet positions from status endpoint spot_positions = tn_data.get('positions', []) if spot_positions: st.markdown("**Spot Wallet Holdings:**") spot_rows = [{ 'Asset': p.get('asset', ''), 'Amount': f"{float(p.get('amount', 0) or 0):.6f}", 'Price': f"${float(p.get('price', 0) or 0):,.2f}", 'Value (USDT)': f"${float(p.get('value_usdt', 0) or 0):,.2f}", } for p in spot_positions] st.dataframe(pd.DataFrame(spot_rows), use_container_width=True, hide_index=True) # โ”€โ”€ Equity Curve โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ equity_curve = tn_pnl_data.get('equity_curve', []) if equity_curve: st.markdown("---") st.markdown("### ๐Ÿ“ˆ Equity Curve (Cumulative PNL)") try: eq_df = pd.DataFrame(equity_curve) eq_df['timestamp'] = pd.to_datetime(eq_df['timestamp'], errors='coerce') eq_df = eq_df.dropna(subset=['timestamp']) if not eq_df.empty: import plotly.graph_objects as go fig_eq = go.Figure() fig_eq.add_trace(go.Scatter( x=eq_df['timestamp'], y=eq_df['cumulative_pnl'], mode='lines+markers', name='Cumulative PNL', line=dict(color='#00e676', width=2), marker=dict(size=6), hovertemplate=( '%{x}
' 'Cumulative PNL: $%{y:,.4f}
' '' ), )) fig_eq.add_hline(y=0, line_dash='dash', line_color='#666') fig_eq.update_layout( height=280, margin=dict(l=0, r=0, t=20, b=0), paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)', font=dict(color='#E2E8F0'), xaxis=dict(gridcolor='rgba(255,255,255,0.1)'), yaxis=dict(gridcolor='rgba(255,255,255,0.1)', tickprefix='$'), ) st.plotly_chart(fig_eq, use_container_width=True) except Exception as _eq_e: st.warning(f"Equity curve render failed: {_eq_e}") # โ”€โ”€ Trade History โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown("---") st.markdown("### ๐Ÿ“‹ Trade History (Testnet Executions)") all_trades = tn_trades_data.get('trades', []) if all_trades: trade_rows = [] for t in reversed(all_trades): # newest first ts = t.get('timestamp', '')[:19].replace('T', ' ') if t.get('timestamp') else 'โ€”' sym = t.get('symbol', 'โ€”') action = t.get('action', 'โ€”') price_v = float(t.get('filled_price') or t.get('price', 0) or 0) amt = float(t.get('amount', 0) or 0) pnl_v = t.get('pnl') pnl_str = f"${float(pnl_v):+,.4f}" if pnl_v is not None else "โ€”" oid = str(t.get('order_id', '') or 'โ€”')[:16] executed = 'โœ…' if t.get('executed') else 'โŒ' err = t.get('error', '') trade_rows.append({ 'Time': ts, 'Symbol': sym, 'Action': action, 'Price': f"${price_v:,.4f}" if price_v else "โ€”", 'Amount': f"{amt:.6f}" if amt else "โ€”", 'PNL': pnl_str, 'Order ID': oid, 'OK': executed, 'Error': err if err else '', }) st.dataframe( pd.DataFrame(trade_rows), use_container_width=True, hide_index=True, height=320, ) else: st.info("No testnet trades recorded yet. Enable `TESTNET_MIRROR=true` to auto-mirror bot decisions.") # โ”€โ”€ Live Order Book (open orders) โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown("---") st.markdown("### ๐Ÿ“– Live Open Orders") ord_col1, ord_col2 = st.columns([3, 1]) with ord_col2: if st.button("๐Ÿ”„ Refresh Orders", key="testnet_refresh_orders", use_container_width=True): st.rerun() try: orders_resp = _tn_requests.get(f'{_api}/api/testnet/orders', timeout=15) open_orders = orders_resp.json().get('orders', []) if orders_resp.status_code == 200 else [] if open_orders: ord_rows = [] for o in open_orders: ord_rows.append({ 'Order ID': str(o.get('orderId', o.get('id', 'โ€”')))[:16], 'Symbol': o.get('symbol', 'โ€”'), 'Side': o.get('side', 'โ€”'), 'Type': o.get('type', 'โ€”'), 'Price': f"${float(o.get('price', 0) or 0):,.4f}", 'Qty': f"{float(o.get('origQty', o.get('amount', 0)) or 0):.6f}", 'Status': o.get('status', 'โ€”'), }) st.dataframe(pd.DataFrame(ord_rows), use_container_width=True, hide_index=True) else: st.info("No open orders on testnet.") except Exception as _oe: st.warning(f"Could not fetch open orders: {_oe}") # โ”€โ”€ Manual Trading Controls โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€ st.markdown("---") st.markdown("### ๐ŸŽฎ Manual Trading Controls") col1, col2 = st.columns(2) with col1: trade_symbol = st.selectbox( "Select Pair", ['BTC/USDT', 'ETH/USDT', 'SOL/USDT', 'XRP/USDT'], key="testnet_symbol" ) with col2: trade_amount = st.number_input( "Amount (USDT)", min_value=10.0, max_value=float(usdt_balance) if usdt_balance > 10 else 10000.0, value=100.0, step=10.0, key="testnet_amount" ) btn_cols = st.columns(3) if btn_cols[0].button("๐ŸŸข BUY (Market)", key="testnet_buy", use_container_width=True): try: order_resp = _tn_requests.post( f'{_api}/api/testnet/order', json={'symbol': trade_symbol, 'side': 'buy', 'amount_usdt': trade_amount}, timeout=20 ) result = order_resp.json() if result.get('success'): _amt = float(result.get('amount', 0) or 0) _pr = float(result.get('price', 0) or 0) st.success(f"โœ… BUY: {_amt:.6f} {trade_symbol.split('/')[0]} @ ${_pr:,.2f}") st.rerun() else: st.error(f"โŒ Order failed: {result.get('error', 'Unknown error')}") except Exception as e: st.error(f"โŒ Order failed: {e}") if btn_cols[1].button("๐Ÿ”ด SELL (Market)", key="testnet_sell", use_container_width=True): try: order_resp = _tn_requests.post( f'{_api}/api/testnet/order', json={'symbol': trade_symbol, 'side': 'sell', 'amount_usdt': 0}, timeout=20 ) result = order_resp.json() if result.get('success'): _amt = float(result.get('amount', 0) or 0) _sym = trade_symbol.split('/')[0] st.success(f"โœ… SELL: {_amt:.6f} {_sym}") st.rerun() else: st.error(f"โŒ Order failed: {result.get('error', 'Unknown error')}") except Exception as e: st.error(f"โŒ Order failed: {e}") if btn_cols[2].button("๐Ÿงช Mirror Bot Trade", key="testnet_mirror_btn", use_container_width=True): try: _sym_raw = trade_symbol.replace('/', '') exec_resp = _tn_requests.post( f'{_api}/api/testnet/execute', json={'action': 'OPEN_LONG_SPLIT', 'symbol': f"{_sym_raw}USDT" if 'USDT' not in _sym_raw else _sym_raw, 'confidence': 0.65}, timeout=25 ) result = exec_resp.json() if result.get('success'): t = result.get('trade', {}) or {} _pr = float(t.get('price', 0) or 0) st.success(f"โœ… Testnet mirror executed: OPEN_LONG @ ${_pr:,.2f}") st.rerun() else: st.error(f"โŒ Mirror failed: {result.get('error', 'Unknown')}") except Exception as e: st.error(f"โŒ Mirror failed: {e}") st.markdown("---") _ic1, _ic2 = st.columns(2) with _ic1: st.info(""" **Bot Auto-Mirror (TESTNET_MIRROR=true):** - Set env var to enable real-time mirroring - Every bot decision โ†’ real testnet order - LONG = real BUY order (50% market + 50% limit) - SHORT = conceptual (spot testnet only) - Trades logged to `logs/testnet_trades.json` """) with _ic2: st.warning(""" **Testnet Notes:** - Zero real money risk (testnet.binance.vision) - Testnet funds reset periodically - SHORT positions tracked conceptually (spot exchange) - SL/TP managed by bot logic (no exchange OCO orders) """) with tab_htf: st.markdown("### ๐Ÿ”ฎ HTF Agent โ€” Hierarchical Multi-Timeframe Trader") st.caption("4-timeframe cascade: 1D โ†’ 4H โ†’ 1H โ†’ 15M | PPO | Walk-forward validated (Avg Sharpe 3.85, +14.8%/2mo)") api_base = get_api_url() # โ”€โ”€ Status โ”€โ”€ try: htf_status_resp = __import__('requests').get(f"{api_base}/api/htf/status", timeout=8) htf_status = htf_status_resp.json() if htf_status_resp.ok else {} except Exception: htf_status = {} if not htf_status.get('running'): st.warning( "**HTF bot is not running.** Start it with:\n" "```bash\npython live_trading_htf.py --interval 15\n```\n" "Add `--live` to enable real execution. Default is dry-run (paper trading)." ) else: dry_tag = " *(dry-run)*" if htf_status.get('dry_run') else " *(LIVE)*" col1, col2, col3, col4 = st.columns(4) pos_label = htf_status.get('position_label', 'FLAT') pos_color = {"LONG": "#00e676", "SHORT": "#ff5252", "FLAT": "#8b949e"}.get(pos_label, "#8b949e") col1.metric("Position", pos_label) col2.metric("Balance", f"${htf_status.get('balance', 0):,.2f}" if htf_status.get('balance') else "โ€”") col3.metric("Realized PnL", f"${htf_status.get('realized_pnl', 0):+,.2f}") col4.metric("Unrealized PnL", f"${htf_status.get('unrealized_pnl', 0):+,.2f}") # Position details if htf_status.get('position', 0) != 0: st.markdown(f"""
{pos_label}  |  Entry: ${htf_status.get('position_price', 0):,.2f}  |  SL: ${htf_status.get('sl_price', 0):,.2f}  |  TP: ${htf_status.get('tp_price', 0):,.2f}  |  Units: {htf_status.get('position_units', 0):.5f}
""", unsafe_allow_html=True) agent_cols = st.columns(3) agent_cols[0].info(f"**Win Rate:** {htf_status.get('win_rate', 0)*100:.1f}%") agent_cols[1].info(f"**Trades:** {htf_status.get('trade_count', 0)}") agent_cols[2].info(f"**Mode:** HTF PPO{dry_tag}") model_path = htf_status.get('model_path') or 'Not loaded' st.caption(f"Model: `{Path(model_path).name if model_path else 'โ€”'}` | " f"Started: {htf_status.get('start_time', 'โ€”')[:19] if htf_status.get('start_time') else 'โ€”'}") st.markdown("---") # โ”€โ”€ Performance Metrics โ”€โ”€ st.markdown("#### ๐Ÿ“ˆ Performance Metrics") try: perf_resp = __import__('requests').get(f"{api_base}/api/htf/performance", timeout=8) perf = perf_resp.json() if perf_resp.ok else {} except Exception: perf = {} if perf and not perf.get('error') and perf.get('total_trades', 0) > 0: pm1, pm2, pm3, pm4, pm5 = st.columns(5) pm1.metric("Total Trades", perf.get('total_trades', 0)) pm2.metric("Win Rate", f"{perf.get('win_rate', 0)*100:.1f}%") pm3.metric("Total PnL", f"${perf.get('total_pnl', 0):+,.2f}") pm4.metric("Sharpe Ratio", f"{perf.get('sharpe', 0):.2f}") pm5.metric("Max Drawdown", f"{perf.get('max_drawdown', 0):.1f}%") pm6, pm7, pm8 = st.columns(3) pm6.metric("Return", f"{perf.get('return_pct', 0):+.1f}%") pm7.metric("Best Trade", f"${perf.get('best_trade', 0):+,.2f}") pm8.metric("Worst Trade", f"${perf.get('worst_trade', 0):+,.2f}") else: st.info(perf.get('message', 'No closed trades yet โ€” metrics will appear after first completed trade.')) st.markdown("---") # โ”€โ”€ Trade History โ”€โ”€ st.markdown("#### ๐Ÿ“‹ Trade History") try: trades_resp = __import__('requests').get(f"{api_base}/api/htf/trades?limit=100", timeout=8) htf_trades = trades_resp.json().get('trades', []) if trades_resp.ok else [] except Exception: htf_trades = [] if htf_trades: close_trades = [t for t in reversed(htf_trades) if 'CLOSE' in t.get('action', '').upper()] open_trades = [t for t in reversed(htf_trades) if 'OPEN' in t.get('action', '').upper()] if close_trades: rows = [] for t in close_trades[:50]: pnl = t.get('pnl', 0) rows.append({ 'Time': t.get('timestamp', '')[:19], 'Action': t.get('action', ''), 'Entry': f"${t.get('entry_price', 0):,.2f}", 'Exit': f"${t.get('exit_price', 0):,.2f}", 'PnL': f"${pnl:+,.2f}", 'Reason': t.get('reason', ''), }) df_trades = pd.DataFrame(rows) def _color_pnl(val): if isinstance(val, str) and val.startswith('$'): try: v = float(val.replace('$', '').replace(',', '').replace('+', '')) return 'color: #00e676' if v > 0 else 'color: #ff5252' except Exception: pass return '' st.dataframe( df_trades.style.applymap(_color_pnl, subset=['PnL']), use_container_width=True, hide_index=True, ) else: st.info("No closed trades yet.") if open_trades: st.markdown("**Open Trades**") for t in open_trades[:5]: st.markdown( f"- `{t.get('action','')}` @ **${t.get('price', 0):,.2f}** " f"| conf: {t.get('confidence', 0):.2f} " f"| {t.get('timestamp', '')[:19]}" ) else: st.info("No HTF trades recorded yet. Bot will begin trading on next cycle.") st.markdown("---") # โ”€โ”€ Architecture Info โ”€โ”€ with st.expander("๐Ÿ— HTF Agent Architecture"): st.markdown(""" **Observation Space:** 117 dimensions across 4 timeframes | Block | Dims | Features | |-------|------|---------| | 1D | 20 | Macro trend, regime, HTF structure | | 4H | 25 | Swing structure, Smart Money Concepts (BOS, CHoCH, OB, FVG) | | 1H | 30 | Momentum, RSI divergence, MACD, Stochastic | | 15M | 35 | Micro entry triggers, candle patterns, Wyckoff phase | | Align | 4 | Cross-TF cascade hierarchy signals | | Pos | 3 | Position, unrealized PnL, balance ratio | **Agent:** PPO with `[512, 256, 128]` network, VecNormalize, curriculum training **Training:** Walk-forward validation (8 folds, 50% position size) **Validated:** Avg Sharpe 3.85 ยท +14.8% / 2 months ยท Max Drawdown 5.95% **Risk:** SL 1.5% ยท TP 3.0% ยท Fee 0.04% ยท Min hold 1h ยท Cooldown 30min after loss """) with tab_backtest: st.markdown("### ๐Ÿ”ฌ Backtest") if IS_CLIENT_MODE: st.info("๐ŸŒ **Backtest is not available in client mode.** Run the trading server locally and access backtesting from the server dashboard.") else: col1, col2 = st.columns(2) with col1: start_date = st.date_input( "Start Date", value=datetime.now() - timedelta(days=365) ) with col2: end_date = st.date_input( "End Date", value=datetime.now() ) if st.button("๐Ÿš€ Run Backtest", key="run_backtest"): st.info("To run backtest, execute in terminal:") st.code("python train_advanced.py --evaluate ./data/models/advanced_agent.zip") with col_sidebar: st.markdown("### ๐ŸŽฏ Agent Status") # Load state for sidebar state = get_trading_state(st.session_state.selected_asset) # Fetch real-time price using 1m data for accuracy # Position & Portfolio Fragment (Live 15s updates) render_position_fragment(st.session_state.selected_asset) # Market Analysis Fragment (Live 15s updates) render_market_analysis_fragment(st.session_state.selected_asset) # Agent Status Fragment (Live 15s updates) render_agent_status_fragment() # Footer st.markdown("---") st.markdown(f"""
DRL Trading System v2.1 | Advanced PPO Agent | โ— WebSocket Live Data | Deployed: {datetime.now().strftime('%Y-%m-%d %H:%M')} UTC
""", unsafe_allow_html=True) if __name__ == "__main__": main()