| """ |
| 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 |
|
|
| |
| 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 |
|
|
| |
| IS_CLIENT_MODE = bool(os.environ.get('API_SERVER_URL')) |
|
|
| |
| 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('/') |
|
|
| |
| st.set_page_config( |
| page_title="DRL Trading System", |
| page_icon="🤖", |
| layout="wide", |
| initial_sidebar_state="expanded", |
| ) |
|
|
| |
| @st.cache_resource |
| def get_app_storage(): |
| return get_storage() |
|
|
| storage = get_app_storage() |
|
|
| |
| st.markdown(""" |
| <style> |
| /* ═══ Foundation ═══ */ |
| .stApp { |
| background-color: #0d1117; |
| color: #e6edf3; |
| } |
| |
| /* ═══ Sidebar ═══ */ |
| div[data-testid="stSidebarContent"] { |
| background-color: #0d1117; |
| border-right: 1px solid #21262d; |
| } |
| div[data-testid="stSidebarContent"] .stMarkdown h3 { |
| color: #8b949e; |
| font-size: 14px; |
| font-weight: 600; |
| letter-spacing: 0.5px; |
| } |
| |
| /* ═══ Metric Cards (native st.metric) ═══ */ |
| div[data-testid="stMetric"] { |
| background: #151b23; |
| border: 1px solid #21262d; |
| border-radius: 8px; |
| padding: 16px 18px; |
| } |
| div[data-testid="stMetric"] label { |
| color: #8b949e !important; |
| font-size: 11px !important; |
| text-transform: uppercase; |
| letter-spacing: 0.8px; |
| } |
| div[data-testid="stMetric"] div[data-testid="stMetricValue"] { |
| color: #fff !important; |
| font-weight: 700; |
| } |
| div[data-testid="stMetricDelta"] svg { display: none; } |
| |
| /* ═══ Custom metric-card class (sidebar panels) ═══ */ |
| .metric-card { |
| background: #151b23; |
| border: 1px solid #21262d; |
| border-radius: 8px; |
| padding: 16px 18px; |
| margin-bottom: 12px; |
| } |
| .metric-label { |
| color: #8b949e; |
| font-size: 11px; |
| text-transform: uppercase; |
| letter-spacing: 0.8px; |
| margin-bottom: 6px; |
| } |
| .metric-value { |
| font-size: 24px; |
| font-weight: 700; |
| color: #fff; |
| } |
| .metric-delta-positive { color: #00e676; } |
| .metric-delta-negative { color: #ff5252; } |
| |
| /* ═══ Tabs ═══ */ |
| .stTabs [data-baseweb="tab-list"] { |
| gap: 8px; |
| border-bottom: 1px solid #21262d; |
| } |
| .stTabs [data-baseweb="tab"] { |
| background-color: transparent; |
| color: #8b949e; |
| border-radius: 6px 6px 0 0; |
| padding: 8px 16px; |
| font-size: 13px; |
| } |
| .stTabs [data-baseweb="tab"]:hover { |
| color: #e6edf3; |
| background-color: rgba(255,255,255,0.04); |
| } |
| .stTabs [aria-selected="true"] { |
| color: #fff !important; |
| font-weight: 600; |
| border-bottom: 2px solid #00e676; |
| } |
| .stTabs [data-baseweb="tab-highlight"] { |
| background-color: #00e676 !important; |
| } |
| .stTabs [data-baseweb="tab-border"] { |
| display: none; |
| } |
| |
| /* ═══ Buttons ═══ */ |
| .stButton > button { |
| background: #151b23; |
| border: 1px solid #21262d; |
| color: #e6edf3; |
| border-radius: 6px; |
| font-weight: 500; |
| transition: all 0.15s ease; |
| } |
| .stButton > button:hover { |
| background: #1c2333; |
| border-color: #388bfd; |
| color: #fff; |
| } |
| .stButton > button[kind="primary"], |
| .stButton > button[data-testid="stBaseButton-primary"] { |
| background: #1a6b3c; |
| border-color: #1a6b3c; |
| color: #00e676; |
| } |
| .stButton > button[kind="primary"]:hover, |
| .stButton > button[data-testid="stBaseButton-primary"]:hover { |
| background: #217a45; |
| border-color: #00e676; |
| } |
| |
| /* ═══ Inputs, Selects, Date Pickers ═══ */ |
| div[data-baseweb="select"] > div, |
| div[data-baseweb="input"] > div, |
| .stDateInput > div > div > input, |
| .stTextInput > div > div > input, |
| .stSelectbox > div > div { |
| background-color: #151b23 !important; |
| border-color: #21262d !important; |
| color: #e6edf3 !important; |
| } |
| |
| /* ═══ Text Areas ═══ */ |
| .stTextArea textarea { |
| background-color: #151b23 !important; |
| border-color: #21262d !important; |
| color: #e6edf3 !important; |
| border-radius: 6px; |
| } |
| |
| /* ═══ Code Blocks ═══ */ |
| .stCodeBlock, code, pre { |
| background-color: #151b23 !important; |
| border: 1px solid #21262d; |
| border-radius: 6px; |
| } |
| |
| /* ═══ Expanders ═══ */ |
| .streamlit-expanderHeader { |
| background: #151b23; |
| border: 1px solid #21262d; |
| border-radius: 6px; |
| color: #e6edf3; |
| } |
| details { |
| background: #151b23; |
| border: 1px solid #21262d; |
| border-radius: 8px; |
| } |
| |
| /* ═══ Dividers ═══ */ |
| hr { |
| border-color: #21262d !important; |
| } |
| |
| /* ═══ Checkboxes & Toggles ═══ */ |
| .stCheckbox label span { |
| color: #8b949e; |
| } |
| |
| /* ═══ Dataframes ═══ */ |
| .stDataFrame { |
| border: 1px solid #21262d; |
| border-radius: 8px; |
| overflow: hidden; |
| } |
| |
| /* ═══ Alerts ═══ */ |
| .stAlert { |
| background: #151b23; |
| border: 1px solid #21262d; |
| border-radius: 8px; |
| } |
| |
| /* ═══ Caption ═══ */ |
| .stCaption { |
| color: #8b949e !important; |
| } |
| |
| /* ═══ Scrollbar ═══ */ |
| ::-webkit-scrollbar { |
| width: 6px; |
| height: 6px; |
| } |
| ::-webkit-scrollbar-track { |
| background: #0d1117; |
| } |
| ::-webkit-scrollbar-thumb { |
| background: #21262d; |
| border-radius: 3px; |
| } |
| ::-webkit-scrollbar-thumb:hover { |
| background: #30363d; |
| } |
| |
| /* ═══ Hide defaults ═══ */ |
| #MainMenu {visibility: hidden;} |
| footer {visibility: hidden;} |
| |
| /* ═══ Timeframe buttons (custom) ═══ */ |
| .timeframe-btn { |
| background: #151b23; |
| border: 1px solid #21262d; |
| color: #8b949e; |
| padding: 5px 12px; |
| margin: 2px; |
| border-radius: 6px; |
| cursor: pointer; |
| font-size: 12px; |
| } |
| .timeframe-btn.active { |
| background: #1a6b3c; |
| color: #00e676; |
| border-color: #1a6b3c; |
| } |
| .timeframe-btn:hover { |
| background: #1c2333; |
| border-color: #388bfd; |
| } |
| </style> |
| """, unsafe_allow_html=True) |
|
|
|
|
| |
| 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 [] |
|
|
| |
| try: |
| all_trades = storage.get_trades(limit=1000) |
|
|
| |
| 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 |
| |
| res = subprocess.run(['ps', 'aux'], capture_output=True, text=True) |
| if res.returncode != 0: |
| return False |
| |
| |
| 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: |
| |
| 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 |
|
|
| |
| try: |
| state = storage.load_state() |
|
|
| if not state: |
| return {**_empty} |
|
|
| |
| 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), |
| } |
|
|
| |
| 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 "<div style='color: #888; text-align: center; padding: 50px;'>No market data available</div>" |
| |
| |
| 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, |
| }) |
| |
| |
| 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: |
| |
| 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 |
| |
| |
| last_candle = df.iloc[-1] |
| tf_label = TIMEFRAMES.get(timeframe, {}).get('label', timeframe.upper()) |
|
|
| |
| |
| clean_symbol = symbol.replace('/', '').lower() |
| ws_stream = f"{clean_symbol}@kline_{timeframe}" |
| chart_id = f"chart_{clean_symbol}_{timeframe}" |
| |
| html = f""" |
| <div id="tv-chart-container" style="width: 100%; height: 550px; position: relative; background: #131722;"> |
| <!-- OHLC and Price Display --> |
| <div id="chart-header" style=" |
| position: absolute; |
| top: 10px; |
| left: 10px; |
| z-index: 100; |
| font-family: -apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif; |
| "> |
| <div style="display: flex; align-items: center; gap: 15px;"> |
| <span style="color: white; font-size: 16px; font-weight: bold;">{symbol}</span> |
| <span style="color: #888; font-size: 13px;">{tf_label}</span> |
| <span id="live-indicator" style=" |
| display: inline-flex; |
| align-items: center; |
| gap: 5px; |
| color: #26a69a; |
| font-size: 11px; |
| "> |
| <span style=" |
| width: 8px; |
| height: 8px; |
| background: #26a69a; |
| border-radius: 50%; |
| animation: pulse 2s infinite; |
| "></span> |
| LIVE |
| </span> |
| </div> |
| <div id="ohlc-display" style=" |
| margin-top: 5px; |
| font-size: 12px; |
| color: #d1d4dc; |
| "> |
| <span style="color: #888;">O</span> <span id="o-val">{last_candle['open']:.2f}</span> |
| <span style="color: #888; margin-left: 10px;">H</span> <span id="h-val">{last_candle['high']:.2f}</span> |
| <span style="color: #888; margin-left: 10px;">L</span> <span id="l-val">{last_candle['low']:.2f}</span> |
| <span style="color: #888; margin-left: 10px;">C</span> <span id="c-val">{last_candle['close']:.2f}</span> |
| <span id="change-val" style="margin-left: 15px;"></span> |
| </div> |
| </div> |
| |
| <!-- Current Price Label --> |
| <div id="current-price" style=" |
| position: absolute; |
| top: 10px; |
| right: 10px; |
| z-index: 100; |
| text-align: right; |
| font-family: -apple-system, BlinkMacSystemFont, 'Trebuchet MS', Roboto, Ubuntu, sans-serif; |
| "> |
| <div id="price-value" style="font-size: 28px; font-weight: bold; color: white;"> |
| ${last_candle['close']:,.2f} |
| </div> |
| <div id="price-change" style="font-size: 14px; color: #26a69a;"></div> |
| </div> |
| |
| <div id="{chart_id}" style="width: 100%; height: 550px;"></div> |
| </div> |
| |
| <style> |
| @keyframes pulse {{ |
| 0% {{ opacity: 1; }} |
| 50% {{ opacity: 0.5; }} |
| 100% {{ opacity: 1; }} |
| }} |
| </style> |
| |
| <script src="https://unpkg.com/lightweight-charts@4.1.0/dist/lightweight-charts.standalone.production.js"></script> |
| <script> |
| (function() {{ |
| const container = document.getElementById('{chart_id}'); |
| |
| const chart = LightweightCharts.createChart(container, {{ |
| width: container.clientWidth, |
| height: 550, |
| layout: {{ |
| background: {{ type: 'solid', color: '#131722' }}, |
| textColor: '#d1d4dc', |
| }}, |
| grid: {{ |
| vertLines: {{ color: '#1e222d' }}, |
| horzLines: {{ color: '#1e222d' }}, |
| }}, |
| crosshair: {{ |
| mode: LightweightCharts.CrosshairMode.Normal, |
| vertLine: {{ |
| color: '#758696', |
| width: 1, |
| style: LightweightCharts.LineStyle.Dashed, |
| labelBackgroundColor: '#2962FF', |
| }}, |
| horzLine: {{ |
| color: '#758696', |
| width: 1, |
| style: LightweightCharts.LineStyle.Dashed, |
| labelBackgroundColor: '#2962FF', |
| }}, |
| }}, |
| rightPriceScale: {{ |
| borderColor: '#2a2e39', |
| scaleMargins: {{ |
| top: 0.1, |
| bottom: 0.2, |
| }}, |
| }}, |
| timeScale: {{ |
| borderColor: '#2a2e39', |
| timeVisible: true, |
| secondsVisible: false, |
| }}, |
| }}); |
| |
| // Candlestick series |
| const candlestickSeries = chart.addCandlestickSeries({{ |
| upColor: '#26a69a', |
| downColor: '#ef5350', |
| borderDownColor: '#ef5350', |
| borderUpColor: '#26a69a', |
| wickDownColor: '#ef5350', |
| wickUpColor: '#26a69a', |
| }}); |
| |
| let candleData = {json.dumps(candlestick_data)}; |
| candlestickSeries.setData(candleData); |
| |
| // Add markers for trades |
| const markers = {json.dumps(markers)}; |
| if (markers.length > 0) {{ |
| candlestickSeries.setMarkers(markers); |
| }} |
| |
| // Volume series |
| const volumeSeries = chart.addHistogramSeries({{ |
| priceFormat: {{ |
| type: 'volume', |
| }}, |
| priceScaleId: 'volume', |
| }}); |
| |
| chart.priceScale('volume').applyOptions({{ |
| scaleMargins: {{ |
| top: 0.85, |
| bottom: 0, |
| }}, |
| }}); |
| |
| let volumeData = {json.dumps(volume_data)}; |
| volumeSeries.setData(volumeData); |
| |
| // Track whether user is hovering over a specific candle |
| let isHoveringCandle = false; |
| let hoverTimeout = null; |
| |
| // Update OHLC on crosshair move |
| chart.subscribeCrosshairMove((param) => {{ |
| if (param.time) {{ |
| const data = param.seriesData.get(candlestickSeries); |
| if (data) {{ |
| // User is hovering over a candle |
| isHoveringCandle = true; |
| |
| // Clear any existing timeout |
| if (hoverTimeout) {{ |
| clearTimeout(hoverTimeout); |
| }} |
| |
| // Reset hover flag after 2 seconds of inactivity |
| hoverTimeout = setTimeout(() => {{ |
| isHoveringCandle = false; |
| }}, 2000); |
| |
| document.getElementById('o-val').textContent = data.open.toFixed(2); |
| document.getElementById('h-val').textContent = data.high.toFixed(2); |
| document.getElementById('l-val').textContent = data.low.toFixed(2); |
| document.getElementById('c-val').textContent = data.close.toFixed(2); |
| |
| const change = ((data.close - data.open) / data.open * 100).toFixed(2); |
| const changeEl = document.getElementById('change-val'); |
| changeEl.textContent = (change >= 0 ? '+' : '') + change + '%'; |
| changeEl.style.color = change >= 0 ? '#26a69a' : '#ef5350'; |
| }} |
| }} else {{ |
| // User moved cursor away from chart |
| isHoveringCandle = false; |
| if (hoverTimeout) {{ |
| clearTimeout(hoverTimeout); |
| hoverTimeout = null; |
| }} |
| }} |
| }}); |
| |
| // Fit content |
| chart.timeScale().fitContent(); |
| |
| // Resize handler |
| new ResizeObserver(entries => {{ |
| chart.applyOptions({{ width: entries[0].contentRect.width }}); |
| }}).observe(container); |
| |
| // WebSocket for live updates |
| let ws; |
| let reconnectInterval = 5000; |
| let lastCandle = candleData[candleData.length - 1]; |
| |
| function connectWebSocket() {{ |
| ws = new WebSocket('wss://data-stream.binance.vision/ws/{ws_stream}'); |
| |
| ws.onopen = function() {{ |
| console.log('WebSocket connected to Binance Vision cluster successfully'); |
| document.getElementById('live-indicator').style.display = 'inline-flex'; |
| }}; |
| |
| ws.onclose = function(event) {{ |
| console.log('WebSocket closed: code=' + event.code + ', reason=' + event.reason); |
| document.getElementById('live-indicator').style.display = 'none'; |
| setTimeout(connectWebSocket, reconnectInterval); |
| }}; |
| |
| ws.onerror = function(err) {{ |
| console.error('WebSocket encountered an error:', err); |
| ws.close(); |
| }}; |
| |
| ws.onmessage = function(event) {{ |
| const data = JSON.parse(event.data); |
| const kline = data.k; |
| |
| const candle = {{ |
| time: Math.floor(kline.t / 1000), |
| open: parseFloat(kline.o), |
| high: parseFloat(kline.h), |
| low: parseFloat(kline.l), |
| close: parseFloat(kline.c), |
| }}; |
| |
| // Update or add candle |
| candlestickSeries.update(candle); |
| |
| // Update volume |
| const volColor = candle.close >= candle.open ? '#26a69a80' : '#ef535080'; |
| volumeSeries.update({{ |
| time: candle.time, |
| value: parseFloat(kline.v), |
| color: volColor, |
| }}); |
| |
| // Update price display |
| const priceEl = document.getElementById('price-value'); |
| const changeEl = document.getElementById('price-change'); |
| |
| priceEl.textContent = '$' + candle.close.toLocaleString('en-US', {{ |
| minimumFractionDigits: 2, |
| maximumFractionDigits: 2 |
| }}); |
| |
| // Calculate 24h change (approximation from last candle) |
| if (lastCandle) {{ |
| const change = ((candle.close - lastCandle.open) / lastCandle.open * 100); |
| changeEl.textContent = (change >= 0 ? '+' : '') + change.toFixed(2) + '%'; |
| changeEl.style.color = change >= 0 ? '#26a69a' : '#ef5350'; |
| priceEl.style.color = change >= 0 ? '#26a69a' : '#ef5350'; |
| }} |
| |
| // Share price with sidebar via localStorage |
| localStorage.setItem('{clean_symbol}_live_price', candle.close.toFixed(2)); |
| |
| // Update OHLC display for current candle (only if user is not hovering over a historical candle) |
| if (!isHoveringCandle) {{ |
| document.getElementById('o-val').textContent = candle.open.toFixed(2); |
| document.getElementById('h-val').textContent = candle.high.toFixed(2); |
| document.getElementById('l-val').textContent = candle.low.toFixed(2); |
| document.getElementById('c-val').textContent = candle.close.toFixed(2); |
| |
| // Update change display as well |
| const change = ((candle.close - candle.open) / candle.open * 100).toFixed(2); |
| const changeEl = document.getElementById('change-val'); |
| changeEl.textContent = (change >= 0 ? '+' : '') + change + '%'; |
| changeEl.style.color = change >= 0 ? '#26a69a' : '#ef5350'; |
| }} |
| }}; |
| }} |
| |
| connectWebSocket(); |
| |
| // Cleanup on page unload |
| window.addEventListener('beforeunload', function() {{ |
| if (ws) ws.close(); |
| }}); |
| }})(); |
| </script> |
| """ |
| 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() |
| |
| |
| SL_PCT = 0.015 |
| TP_PCT = 0.025 |
| |
| if position == 0: |
| st.markdown(f""" |
| <div class="metric-card" style="text-align: center;"> |
| <div class="metric-label">Current Position</div> |
| <div style="font-size: 24px; color: #555; margin-top: 10px;">No Position (FLAT)</div> |
| <div style="font-size: 12px; color: #888; margin-top: 5px;">Current Price: ${current_price:,.2f}</div> |
| </div> |
| """, unsafe_allow_html=True) |
| else: |
| |
| |
| |
| pass |
| is_long = position == 1 |
| color = "#26a69a" if is_long else "#ef5350" |
| side = "LONG" if is_long else "SHORT" |
| icon = "📈" if is_long else "📉" |
| |
| |
| |
| entry_price = state.get('position_price') or state.get('entry_price') or state.get('price', 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: |
| 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 |
| |
| |
| sl_price = state.get('sl', 0) |
| tp_price = state.get('tp', 0) |
| |
| if sl_price == 0 or tp_price == 0: |
| |
| 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) |
| |
| |
| 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""" |
| <div class="metric-card" style="border: 1px solid {color};"> |
| <div style="display: flex; justify-content: space-between; align-items: center;"> |
| <span class="metric-label">Current Position</span> |
| <span style=" |
| background: {color}; |
| padding: 4px 12px; |
| border-radius: 4px; |
| color: white; |
| font-weight: bold; |
| font-size: 12px; |
| ">{icon} {side}</span> |
| </div> |
| <div style="margin-top: 15px;"> |
| <div style="display: flex; justify-content: space-between; margin-bottom: 5px;"> |
| <span style="color: #888;">Entry Price:</span> |
| <span style="color: white;">${entry_price:,.2f}</span> |
| </div> |
| <div style="display: flex; justify-content: space-between; margin-bottom: 5px;"> |
| <span style="color: #888;">Current Price:</span> |
| <span id="sidebar-current-price" style="color: white;">${current_price:,.2f}</span> |
| </div> |
| <div style="display: flex; justify-content: space-between; margin-bottom: 5px;"> |
| <span style="color: #888;">Unrealized P&L:</span> |
| <span id="sidebar-pnl" style="color: {pnl_color}; font-weight: bold;">{pnl_sign}${unrealized_pnl:,.2f}</span> |
| </div> |
| <div style="margin-top: 10px; padding-top: 10px; border-top: 1px solid #333;"> |
| <div style="display: flex; justify-content: space-between; margin-bottom: 5px;"> |
| <span style="color: #ef5350;">🛑 Stop Loss:</span> |
| <span style="color: #ef5350;">${sl_price:,.2f}</span> |
| </div> |
| <div style="display: flex; justify-content: space-between;"> |
| <span style="color: #26a69a;">🎯 Take Profit:</span> |
| <span style="color: #26a69a;">${tp_price:,.2f}</span> |
| </div> |
| </div> |
| </div> |
| </div> |
| <script> |
| // Real-time price update from WebSocket via localStorage |
| const entryPrice = {entry_price}; |
| const positionUnits = {state.get('position_size_units', 0)}; |
| const isLong = {'true' if is_long else 'false'}; |
| |
| function updateSidebarPrice() {{ |
| const livePrice = parseFloat(localStorage.getItem('{clean_symbol}_live_price')); |
| if (livePrice && livePrice > 0) {{ |
| // Update current price |
| const priceEl = document.getElementById('sidebar-current-price'); |
| if (priceEl) priceEl.textContent = '$' + livePrice.toLocaleString('en-US', {{minimumFractionDigits: 2}}); |
| |
| // Update unrealized P&L |
| let pnl = isLong ? (livePrice - entryPrice) * positionUnits : (entryPrice - livePrice) * positionUnits; |
| const pnlEl = document.getElementById('sidebar-pnl'); |
| if (pnlEl) {{ |
| pnlEl.textContent = (pnl >= 0 ? '+' : '') + '$' + pnl.toFixed(2); |
| pnlEl.style.color = pnl >= 0 ? '#26a69a' : '#ef5350'; |
| }} |
| }} |
| }} |
| |
| // Update every 500ms |
| setInterval(updateSidebarPrice, 500); |
| updateSidebarPrice(); |
| </script> |
| """, unsafe_allow_html=True) |
|
|
|
|
| def render_trade_history(trades: list): |
| """Render real trade history.""" |
| st.markdown('<div class="metric-label">Recent Trades</div>', 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 = '' |
| |
| |
| 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""" |
| <div style=" |
| background: #1e222d; |
| border-radius: 5px; |
| padding: 10px; |
| margin-bottom: 5px; |
| display: flex; |
| justify-content: space-between; |
| align-items: center; |
| "> |
| <div> |
| <span style="color: {color}; font-weight: bold;">{side}</span> |
| <span style="color: #888; font-size: 12px; margin-left: 10px;">${price:,.2f}</span> |
| <span style="color: #555; font-size: 10px; margin-left: 10px;">{time_str}</span> |
| </div> |
| <span style="color: {pnl_color}; font-weight: bold;">{pnl_display}</span> |
| </div> |
| """, 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']] |
|
|
| |
| 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}") |
|
|
| |
| 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}") |
|
|
| |
| 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: |
| |
| try: |
| state_resp = requests.get(f'{get_api_url()}/api/state', timeout=5) |
| if state_resp.status_code == 200: |
| api_state = state_resp.json() |
| |
| 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) |
| |
| |
| st.markdown(f""" |
| <div class="metric-card"> |
| <div class="metric-label">Portfolio Value</div> |
| <div class="metric-value">{f'${st.session_state["portfolio_balance"]:,.2f}' if st.session_state.get('portfolio_balance') is not None else '—'}</div> |
| <div class="metric-delta" style="color: {'#26a69a' if float(st.session_state.get('total_pnl') or 0) >= 0 else '#ef5350'}"> |
| P&L: {'+' if float(st.session_state.get('total_pnl') or 0) >= 0 else ''}${float(st.session_state.get('total_pnl') or 0):,.2f} |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| except Exception as e: |
| st.markdown(f"<div style='color: #ef5350'>Connection Error</div>", 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") |
| |
| |
| 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""" |
| <div class="metric-card"> |
| <div class="metric-label">📊 Market Analysis</div> |
| <div style="color: #ef5350; font-size: 12px;">API error (HTTP {market_resp.status_code})</div> |
| <div style="color: #888; font-size: 10px; margin-top:5px;">Server returned non-200 for /api/market</div> |
| </div> |
| """, unsafe_allow_html=True) |
| return |
| except Exception as e: |
| st.markdown(f""" |
| <div class="metric-card"> |
| <div class="metric-label">📊 Market Analysis</div> |
| <div style="color: #ef5350; font-size: 12px;">Unable to load (API server offline?)</div> |
| <div style="color: #888; font-size: 10px; margin-top:5px;">Error: {str(e)}</div> |
| </div> |
| """, unsafe_allow_html=True) |
| return |
|
|
| |
| whale = market_data.get('whale', {}) |
| if whale: |
| if whale.get('error'): |
| st.markdown(f""" |
| <div class="metric-card"> |
| <div class="metric-label">🐋 Whale Signals</div> |
| <div style="color: #ef5350; font-size: 12px;">Data Error</div> |
| <div style="color: #888; font-size: 10px;">{whale.get('error')}</div> |
| </div> |
| """, 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 "⚪" |
| |
| |
| 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 "-" |
| |
| |
| 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""" |
| <div class="metric-card"> |
| <div class="metric-label">🐋 Whale Signals</div> |
| <div style="color: {whale_color}; font-size: 14px;">{whale_emoji} {whale.get('direction', 'NEUTRAL')}</div> |
| <div style="color: #888; font-size: 11px;"> |
| Score: {whale.get('score', 0):.2f} | Conf: {whale.get('confidence', 0)}%<br> |
| Flow (1m): <span style="color: {flow_color}; font-weight: bold;">{flow_str}</span><br> |
| 🟢{whale.get('bullish', 0)} 🔴{whale.get('bearish', 0)} ⚪{whale.get('neutral', 0)} |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| |
| funding_data = market_data.get('funding', {}) |
| funding = funding_data.get('data', {}) |
| if funding_data and not funding_data.get('error'): |
| |
| rate = funding_data.get('rate', 0) |
| funding_color = "#26a69a" if rate > 0.0001 else "#ef5350" if rate < -0.0001 else "#888" |
| |
| st.markdown(f""" |
| <div class="metric-card"> |
| <div class="metric-label">💰 Funding Rate</div> |
| <div style="color: {funding_color}; font-size: 14px;">{rate:.4f}%</div> |
| <div style="color: #888; font-size: 11px;"> |
| Bias: {funding_data.get('bias', 'neutral')} | APR: {funding_data.get('annualized', 0):.1f}% |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| |
| 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" |
| |
| |
| 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""" |
| <div class="metric-card"> |
| <div class="metric-label">📊 Order Flow</div> |
| <div style="color: {of_color}; font-size: 14px;">{of_bias.upper()} ({(of_score or 0):+.2f})</div> |
| <div style="color: #888; font-size: 11px;"> |
| CVD: {cvd_trend} | Taker Buy: {taker_ratio:.0%}<br/> |
| Notable: B:{notable_buys} / S:{notable_sells} |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| |
| regime_data = market_data.get('regime', {}) |
| if regime_data and not regime_data.get('error'): |
| r_type = regime_data.get('type', 'UNKNOWN') |
| |
| 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""" |
| <div class="metric-card"> |
| <div class="metric-label">👑 Market Regime (HMM)</div> |
| <div style="color: {r_color}; font-size: 14px; font-weight: bold;">{r_type.replace('_', ' ')}</div> |
| <div style="color: #888; font-size: 11px;"> |
| ADX: {regime_data.get('adx', 0)} | Volatility: {regime_data.get('volatility', 1.0)}x |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| |
| 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""" |
| <div class="metric-card"> |
| <div class="metric-label">🚀 AI Price Forecast (TFT)</div> |
| <div style="color: {fc_color}; font-size: 14px;">4h: {fc_sign}{ret_4h}% | 12h: {forecast.get('return_12h', 0)}%</div> |
| <div style="color: #888; font-size: 11px;"> |
| Consensus: {forecast.get('consensus', 0):.2f} | Confidence: {forecast.get('confidence', 0):.2f} |
| </div> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
| |
| confidence = market_data.get('ensemble_confidence') |
| if confidence is not None: |
| conf_pct = min(100, max(0, int(confidence * 100))) |
| |
| mult = 0.25 + 1.75 * confidence if confidence < 0.5 else 1.0 + 1.0 * (confidence - 0.5) * 2 |
| c_color = "#26a69a" if confidence > 0.6 else "#ffa726" if confidence > 0.35 else "#ef5350" |
| |
| st.markdown(f""" |
| <div class="metric-card"> |
| <div class="metric-label">🧠 Ensemble Agreement</div> |
| <div style="color: {c_color}; font-size: 14px;">{conf_pct}% Alignment</div> |
| <div style="color: #888; font-size: 11px;"> |
| Position Size Multiplier: ~{mult:.1f}x |
| </div> |
| |
| <!-- Progress Bar --> |
| <div style="width: 100%; background-color: #333; height: 4px; border-radius: 2px; margin-top: 5px;"> |
| <div style="width: {conf_pct}%; background-color: {c_color}; height: 100%; border-radius: 2px;"></div> |
| </div> |
| </div> |
| """, 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__) |
| |
| |
| 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}") |
|
|
| |
| current_price = 0.0 |
| try: |
| |
| 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']) |
| |
| |
| 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}") |
|
|
| |
| 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""" |
| <div class="metric-card"> |
| <div class="metric-label">Portfolio Value</div> |
| <div class="metric-value">{f'${balance:,.2f}' if balance is not None else '—'}</div> |
| <div class="{pnl_class}">P&L: {pnl_sign}${(total_pnl or 0):,.2f}</div> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| |
| |
| asset_state = {} |
| if 'assets' in state: |
| |
| 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 asset_state and 'position' in state: |
| asset_state = state |
| |
| |
| |
| |
| 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: |
| |
| asset_state['position_price'] = asset_state['price'] |
| |
| render_position_card(asset_state, current_price, symbol) |
| |
| |
| |
| 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__) |
|
|
| |
| 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: |
| |
| 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 |
|
|
| 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""" |
| <div class="metric-card"> |
| <div class="metric-label">Active Model</div> |
| <div style="color: white; font-size: 14px; margin-top: 5px;">Ultimate Agent (PPO)</div> |
| <div style="color: {return_color}; font-size: 12px;">{return_str} | {win_rate:.1f}% Win Rate</div> |
| <div style="color: #888; font-size: 11px;">Trades: {total_trades} | Model: {model_date}</div> |
| <div style="color: {'#26a69a' if model_exists else '#ef5350'}; font-size: 11px;">{'✓ Model loaded' if model_exists else '✗ Model not found'}</div> |
| </div> |
| """, unsafe_allow_html=True) |
|
|
|
|
|
|
|
|
| def on_asset_change(): |
| """Callback for asset selection change.""" |
| |
| st.session_state.market_analysis = None |
| |
|
|
| def main(): |
| """Main application entry point.""" |
| |
| |
| if 'timeframe' not in st.session_state: |
| st.session_state.timeframe = '1h' |
| |
| |
| if 'selected_asset' not in st.session_state: |
| st.session_state.selected_asset = 'BTCUSDT' |
| |
| |
| state_preview = get_trading_state() |
| available_assets = state_preview.get('available_assets', ['BTCUSDT']) |
| |
| |
| if 'auto_refresh' not in st.session_state: |
| st.session_state.auto_refresh = True |
| |
| |
| with st.sidebar: |
| st.markdown("### ⚙️ Settings") |
| |
| |
| 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)") |
| |
| |
| |
| |
| 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 |
| |
| 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)'}") |
|
|
| |
| |
| 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""" |
| <div style="text-align: right; padding-top: 10px;"> |
| <span style="color: #00e676; font-size: 14px;">🟢 Connected</span><br> |
| <span style="color: #8b949e; font-size: 12px;">{refresh_status}</span> |
| </div> |
| """, unsafe_allow_html=True) |
| |
| with col3: |
| |
| st.session_state.auto_refresh = st.toggle("Auto Refresh", value=st.session_state.auto_refresh) |
| |
| |
| pass |
| |
| |
| with st.sidebar: |
| render_sidebar_metrics_fragment() |
| |
| st.divider() |
| |
| |
| col_main, col_sidebar = st.columns([3, 1]) |
| |
| with col_main: |
| |
| 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() |
| |
| |
| 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 |
| |
| |
| 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: |
| |
| trades = state.get('trades', []) |
| chart_html = create_tradingview_chart_with_websocket(df, trades, st.session_state.timeframe, st.session_state.selected_asset) |
| |
| |
| chart_placeholder = st.empty() |
| |
| |
| current_time = time.time() |
| chart_html += f"<!-- {current_time} -->" |
| |
| with chart_placeholder: |
| components.html(chart_html, height=600) |
| |
|
|
|
|
| |
| 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") |
| |
| |
| 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: |
| |
| 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: |
| |
| all_trades_lp = [] |
| try: |
| |
| all_trades_lp = load_trading_log() |
| if not IS_CLIENT_MODE: |
| |
| 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 |
| |
| |
| 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) |
| |
| |
| 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] |
| |
| asset_rows = [] |
|
|
| |
| 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) |
| |
| 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 |
|
|
| |
| 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' |
|
|
| |
| if sym in raw_assets: |
| asset_data = raw_assets[sym] |
| 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 |
| for t in reversed(sorted_trades): |
| 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: |
| sym_open_pnl = (current_price - entry_price) * units |
| else: |
| 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 |
| |
| |
| display_sym = sym |
| if sym.endswith('USDT'): |
| display_sym = sym[:-4] + ' /USDT' |
| |
| |
| sym_price = raw_assets.get(sym, {}).get('price', 0) |
| |
| 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, |
| }) |
| |
| |
| |
| |
| |
| 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', [])) |
| |
| |
| 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' |
| |
| |
| def pnl_color(val): |
| return '#00e676' if val >= 0 else '#ff5252' |
| |
| def pnl_sign(val): |
| return '+' if val >= 0 else '-' |
| |
| |
| |
| eq_pct = list(equity_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 |
| |
| svg_points = [] |
| svg_fill_points = [] |
| for i, val in enumerate(eq_pct): |
| x = (i / max(n_points - 1, 1)) * svg_w |
| |
| 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) |
| |
| 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_y = svg_h - padding_y - ((0 + eq_range_val) / (2 * eq_range_val)) * (svg_h - 2 * padding_y) |
| |
| |
| 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) |
| |
| |
| top_label = f"+{eq_range_val:.1f}%" |
| bot_label = f"-{eq_range_val:.1f}%" |
| |
| svg_chart = f''' |
| <svg width="100%" viewBox="0 0 {svg_w} {svg_h}" preserveAspectRatio="none" style="display:block;"> |
| <defs> |
| <linearGradient id="eqGrad" x1="0" y1="0" x2="0" y2="1"> |
| <stop offset="0%" stop-color="{fill_color_start}"/> |
| <stop offset="100%" stop-color="{fill_color_end}"/> |
| </linearGradient> |
| </defs> |
| <!-- Zero line --> |
| <line x1="0" y1="{zero_y:.1f}" x2="{svg_w}" y2="{zero_y:.1f}" stroke="rgba(255,255,255,0.08)" stroke-width="1" stroke-dasharray="4,4"/> |
| <!-- Fill area --> |
| <polygon points="{fill_str}" fill="url(#eqGrad)"/> |
| <!-- Line --> |
| <polyline points="{polyline_str}" fill="none" stroke="{line_color}" stroke-width="2.5" stroke-linecap="round" stroke-linejoin="round"/> |
| <!-- Last point dot --> |
| <circle cx="{last_x:.1f}" cy="{last_y:.1f}" r="4" fill="{line_color}" stroke="#fff" stroke-width="1.5"/> |
| <!-- Labels --> |
| <text x="{svg_w - 5}" y="{padding_y + 4}" fill="#8b949e" font-size="10" text-anchor="end" font-family="monospace">{top_label}</text> |
| <text x="{svg_w - 5}" y="{svg_h - padding_y + 12}" fill="#8b949e" font-size="10" text-anchor="end" font-family="monospace">{bot_label}</text> |
| <text x="5" y="{zero_y - 4:.1f}" fill="#555" font-size="9" font-family="monospace">0%</text> |
| </svg> |
| ''' |
| |
| |
| asset_rows_html = '' |
| for row in asset_rows: |
| |
| if row['status'] == 'LONG': |
| status_html = '<span style="background:#1b3a26;color:#00e676;padding:3px 10px;border-radius:4px;font-size:11px;font-weight:600;">● LONG</span>' |
| elif row['status'] == 'SHORT': |
| status_html = '<span style="background:#3a1b1b;color:#ff5252;padding:3px 10px;border-radius:4px;font-size:11px;font-weight:600;">● SHORT</span>' |
| else: |
| status_html = '<span style="background:#2a2e39;color:#888;padding:3px 10px;border-radius:4px;font-size:11px;">● —</span>' |
| |
| |
| pnl_val = row['pnl'] |
| pnl_html = f'<span style="color:{pnl_color(pnl_val)};font-weight:600;">—</span>' |
| pnl_dollar_html = f'<span style="color:{pnl_color(pnl_val)};font-weight:600;font-family:monospace;">{pnl_sign(pnl_val)}${abs(pnl_val):,.2f}</span>' |
| |
| |
| trades_str = str(row['trades']) |
| if row['open_trades'] > 0: |
| trades_str += f' <span style="color:#888;">(+{row["open_trades"]})</span>' |
| |
| |
| wr = row['win_rate'] |
| bar_color = '#00e676' if wr >= 50 else '#ff9800' if wr > 0 else '#555' |
| wr_html = f''' |
| <div style="display:flex;align-items:center;gap:8px;"> |
| <div style="flex:1;background:#1a1e2a;border-radius:4px;height:8px;overflow:hidden;min-width:60px;"> |
| <div style="width:{wr}%;height:100%;background:{bar_color};border-radius:4px;"></div> |
| </div> |
| <span style="color:#ccc;font-size:12px;min-width:35px;">{wr:.0f}%</span> |
| </div> |
| ''' |
| |
| |
| if row['best'] is not None: |
| best_html = f'<span style="color:#00e676;">{pnl_sign(row["best"])}${abs(row["best"]):,.2f}</span>' |
| else: |
| best_html = '<span style="color:#555;">—</span>' |
|
|
| |
| if row['worst'] is not None: |
| worst_html = f'<span style="color:#ff5252;">-${abs(row["worst"]):,.2f}</span>' |
| else: |
| worst_html = '<span style="color:#555;">—</span>' |
| |
| asset_rows_html += f''' |
| <tr style="border-bottom:1px solid #1a1e2a;"> |
| <td style="padding:14px 16px;font-weight:600;color:#fff;font-size:13px;"> |
| {row['symbol']} |
| </td> |
| <td style="padding:14px 16px;">{status_html}</td> |
| <td style="padding:14px 16px;text-align:right;color:#ccc;font-size:13px;font-family:monospace;">${row['price']:,.2f}</td> |
| <td style="padding:14px 16px;text-align:right;color:#ccc;font-size:13px;font-family:monospace;">${row['equity']:,.2f}</td> |
| <td style="padding:14px 16px;">{pnl_html}</td> |
| <td style="padding:14px 16px;">{pnl_dollar_html}</td> |
| <td style="padding:14px 16px;color:#ccc;font-size:13px;">{trades_str}</td> |
| <td style="padding:14px 16px;min-width:100px;">{wr_html}</td> |
| <td style="padding:14px 16px;">{best_html}</td> |
| <td style="padding:14px 16px;">{worst_html}</td> |
| </tr> |
| ''' |
| |
| if not asset_rows_html: |
| asset_rows_html = ''' |
| <tr> |
| <td colspan="10" style="padding:30px;text-align:center;color:#555;font-size:14px;"> |
| No trades recorded yet. Start the trading bot to see portfolio data. |
| </td> |
| </tr> |
| ''' |
| |
| portfolio_html = f''' |
| <div style=" |
| font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; |
| background: #0d1117; |
| color: #fff; |
| padding: 0; |
| "> |
| <!-- Header --> |
| <div style="display:flex;align-items:center;gap:12px;margin-bottom:20px;"> |
| <span style="font-size:22px;font-weight:700;color:#fff;">Live Portfolio</span> |
| <span style=" |
| background: #1a6b3c; |
| color: #00e676; |
| padding: 3px 10px; |
| border-radius: 4px; |
| font-size: 10px; |
| font-weight: 700; |
| letter-spacing: 1px; |
| text-transform: uppercase; |
| ">LIVE TRADING</span> |
| <span style=" |
| background: {'#1b3a26' if is_online else '#3a1b1b'}; |
| color: {status_color}; |
| padding: 3px 10px; |
| border-radius: 4px; |
| font-size: 10px; |
| font-weight: 700; |
| letter-spacing: 1px; |
| margin-left: 4px; |
| ">{status_dot} {status_text}</span> |
| </div> |
| |
| <!-- Metric Cards Row 1 --> |
| <div style="display:grid;grid-template-columns:repeat(4,1fr);gap:12px;margin-bottom:24px;"> |
| <div style="background:#151b23;border:1px solid #21262d;border-radius:8px;padding:18px 20px;"> |
| <div style="color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">Realized PNL</div> |
| <div style="font-size:26px;font-weight:700;color:{pnl_color(realized_pnl_total)};">{pnl_sign(realized_pnl_total)}${abs(realized_pnl_total):,.2f}</div> |
| </div> |
| <div style="background:#151b23;border:1px solid #21262d;border-radius:8px;padding:18px 20px;"> |
| <div style="color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">Open PNL</div> |
| <div style="font-size:26px;font-weight:700;color:{pnl_color(open_pnl_total)};">{pnl_sign(open_pnl_total)}${abs(open_pnl_total):,.2f}</div> |
| </div> |
| <div style="background:#151b23;border:1px solid #21262d;border-radius:8px;padding:18px 20px;"> |
| <div style="color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">Win Rate</div> |
| <div style="font-size:26px;font-weight:700;color:#fff;">{overall_win_rate:.0f}%</div> |
| <div style="color:#8b949e;font-size:11px;">{total_winning_trades}W / {total_closed_trades - total_winning_trades}L</div> |
| </div> |
| <div style="background:#151b23;border:1px solid #21262d;border-radius:8px;padding:18px 20px;"> |
| <div style="color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;margin-bottom:6px;">Trades</div> |
| <div style="font-size:26px;font-weight:700;color:#fff;">{total_trades_count}</div> |
| <div style="color:#8b949e;font-size:11px;">{total_open_trades} open · {total_closed_trades} closed</div> |
| </div> |
| </div> |
| |
| <!-- Metric Cards Row 2 (Dollar Values) --> |
| <div style="display:grid;grid-template-columns:repeat(3,1fr);gap:12px;margin-bottom:24px;"> |
| <div style="background:#151b23;border:1px solid #21262d;border-radius:8px;padding:14px 20px;"> |
| <div style="color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;margin-bottom:4px;">Portfolio Value</div> |
| <div style="font-size:22px;font-weight:700;color:#fff;">{f'${lp_total_balance:,.2f}' if lp_total_balance is not None else '—'}</div> |
| </div> |
| <div style="background:#151b23;border:1px solid #21262d;border-radius:8px;padding:14px 20px;"> |
| <div style="color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;margin-bottom:4px;">Total P&L</div> |
| <div style="font-size:22px;font-weight:700;color:{pnl_color(lp_grand_total_pnl)};">{pnl_sign(lp_grand_total_pnl)}${abs(lp_grand_total_pnl):,.2f}</div> |
| </div> |
| <div style="background:#151b23;border:1px solid #21262d;border-radius:8px;padding:14px 20px;"> |
| <div style="color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;margin-bottom:4px;">Active Assets</div> |
| <div style="font-size:22px;font-weight:700;color:#fff;">{lp_active_assets_count}</div> |
| </div> |
| </div> |
| |
| <!-- Equity Curve (Pure SVG — no external deps) --> |
| <div style="background:#151b23;border:1px solid #21262d;border-radius:8px;padding:20px;margin-bottom:24px;"> |
| <div style="font-size:15px;font-weight:600;color:#fff;margin-bottom:2px;">Equity Curve</div> |
| <div style="color:#8b949e;font-size:11px;margin-bottom:12px;">Cumulative P&L from closed trades</div> |
| {svg_chart} |
| </div> |
| |
| <!-- Asset Table --> |
| <div style="background:#151b23;border:1px solid #21262d;border-radius:8px;overflow-x:auto;overflow-y:hidden;"> |
| <table style="width:100%;min-width:1200px;border-collapse:collapse;"> |
| <thead> |
| <tr style="border-bottom:1px solid #21262d;"> |
| <th style="padding:12px 16px;text-align:left;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;font-weight:600;">Asset</th> |
| <th style="padding:12px 16px;text-align:left;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;font-weight:600;">Status</th> |
| <th style="padding:12px 16px;text-align:right;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;font-weight:600;">Price</th> |
| <th style="padding:12px 16px;text-align:right;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;font-weight:600;">Equity</th> |
| <th style="padding:12px 16px;text-align:left;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;font-weight:600;">PNL (%)</th> |
| <th style="padding:12px 16px;text-align:left;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;font-weight:600;">PNL ($)</th> |
| <th style="padding:12px 16px;text-align:left;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;font-weight:600;">Trades</th> |
| <th style="padding:12px 16px;text-align:left;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;font-weight:600;">Win Rate</th> |
| <th style="padding:12px 16px;text-align:left;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;font-weight:600;">Best</th> |
| <th style="padding:12px 16px;text-align:left;color:#8b949e;font-size:11px;text-transform:uppercase;letter-spacing:1px;font-weight:600;">Worst</th> |
| </tr> |
| </thead> |
| <tbody> |
| {asset_rows_html} |
| </tbody> |
| </table> |
| </div> |
| |
| <!-- Footer --> |
| <div style="text-align:center;color:#555;font-size:11px;margin-top:16px;"> |
| DRL Trading System · Signals from PPO + Composite Scoring · Connected to OKX |
| </div> |
| </div> |
| ''' |
| |
| 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') |
| |
| |
| |
| 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)") |
| |
| 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") |
| |
| 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: |
| |
| 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''' |
| <div style="background-color: rgba(255,255,255,0.05); padding: 15px; border-radius: 8px; border-left: 4px solid {color}; height: 140px; overflow: hidden;"> |
| <h4 style="margin: 0; padding: 0; color: #E2E8F0;">{chain}</h4> |
| <h5 style="margin: 5px 0 10px 0; color: {color};">{signal}</h5> |
| <p style="margin: 0; font-size: 0.85em; color: #94A3B8; line-height: 1.4; overflow: hidden; text-overflow: ellipsis; display: -webkit-box; -webkit-line-clamp: 3; -webkit-box-orient: vertical;">{reason}</p> |
| </div> |
| ''', 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.") |
|
|
| |
| import requests as _tn_requests |
|
|
| _api = get_api_url() |
|
|
| |
| 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 = {} |
|
|
| |
| 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'}") |
|
|
| |
| 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, |
| ) |
|
|
| |
| 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_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 = 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=( |
| '<b>%{x}</b><br>' |
| 'Cumulative PNL: $%{y:,.4f}<br>' |
| '<extra></extra>' |
| ), |
| )) |
| 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}") |
|
|
| |
| 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): |
| 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.") |
|
|
| |
| 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}") |
|
|
| |
| 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() |
|
|
| |
| 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}") |
|
|
| |
| if htf_status.get('position', 0) != 0: |
| st.markdown(f""" |
| <div style="background:#151b23;border:1px solid {pos_color};border-radius:8px;padding:14px 18px;margin:8px 0;"> |
| <b style="color:{pos_color};">{pos_label}</b> | |
| Entry: <b>${htf_status.get('position_price', 0):,.2f}</b> | |
| SL: <b style="color:#ff5252;">${htf_status.get('sl_price', 0):,.2f}</b> | |
| TP: <b style="color:#00e676;">${htf_status.get('tp_price', 0):,.2f}</b> | |
| Units: <b>{htf_status.get('position_units', 0):.5f}</b> |
| </div> |
| """, 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("---") |
|
|
| |
| 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("---") |
|
|
| |
| 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("---") |
|
|
| |
| 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") |
| |
| |
| state = get_trading_state(st.session_state.selected_asset) |
| |
| |
| |
| render_position_fragment(st.session_state.selected_asset) |
| |
| |
| render_market_analysis_fragment(st.session_state.selected_asset) |
| |
| |
| render_agent_status_fragment() |
|
|
| |
| |
| st.markdown("---") |
| st.markdown(f""" |
| <div style="text-align: center; color: #888; font-size: 12px;"> |
| DRL Trading System v2.1 | Advanced PPO Agent | |
| <span style="color: #00e676;">●</span> WebSocket Live Data | |
| Deployed: {datetime.now().strftime('%Y-%m-%d %H:%M')} UTC |
| </div> |
| """, unsafe_allow_html=True) |
|
|
|
|
| if __name__ == "__main__": |
| main() |
|
|