"""
return html
def render_position_card(state: dict, current_price: float, symbol: str = 'BTC/USDT'):
"""Render current position card."""
position = state.get('position', 0)
clean_symbol = symbol.replace('/', '').lower() # For localStorage key
# SL/TP percentages (match live trading config)
SL_PCT = 0.015 # 1.5% (matches live_trading.py)
TP_PCT = 0.025 # 2.5% (matches live_trading.py)
if position == 0:
st.markdown(f"""
Current Position
No Position (FLAT)
Current Price: ${current_price:,.2f}
""", unsafe_allow_html=True)
else:
# DEBUG: Inspect state to find units key
# st.write(f"Debug State for P&L: {state}")
# logger.info(f"Debug State for P&L: {state}")
pass
is_long = position == 1
color = "#26a69a" if is_long else "#ef5350"
side = "LONG" if is_long else "SHORT"
icon = "๐" if is_long else "๐"
# Get entry price from state - check multiple field names for compatibility
# Priority: position_price > entry_price > price (last trade price fallback)
entry_price = state.get('position_price') or state.get('entry_price') or state.get('price', current_price)
# Validation: Entry price must be reasonable (within 50% of current price)
if entry_price > 0 and current_price > 0:
price_diff_pct = abs(entry_price - current_price) / current_price
if price_diff_pct > 0.5: # More than 50% difference is suspicious
logger.warning(f"Entry price ${entry_price:,.2f} is {price_diff_pct*100:.1f}% different from current ${current_price:,.2f} - using current price")
entry_price = current_price
# Get SL/TP from state (preferred) or calculate
sl_price = state.get('sl', 0)
tp_price = state.get('tp', 0)
if sl_price == 0 or tp_price == 0:
# Fallback to estimation
if is_long:
sl_price = entry_price * (1 - SL_PCT)
tp_price = entry_price * (1 + TP_PCT)
else:
sl_price = entry_price * (1 + SL_PCT)
tp_price = entry_price * (1 - TP_PCT)
# Calculate Unrealized PnL
units = state.get('position_size_units', state.get('position_units', state.get('units', 0)))
if is_long:
unrealized_pnl = (current_price - entry_price) * units
else:
unrealized_pnl = (entry_price - current_price) * units
pnl_color = "#26a69a" if unrealized_pnl >= 0 else "#ef5350"
pnl_sign = "+" if unrealized_pnl >= 0 else ""
st.markdown(f"""
Current Position{icon} {side}
Entry Price:${entry_price:,.2f}
Current Price:${current_price:,.2f}
Unrealized P&L:{pnl_sign}${unrealized_pnl:,.2f}
๐ Stop Loss:${sl_price:,.2f}
๐ฏ Take Profit:${tp_price:,.2f}
""", unsafe_allow_html=True)
def render_trade_history(trades: list):
"""Render real trade history."""
st.markdown('
Recent Trades
', unsafe_allow_html=True)
action_trades = [t for t in trades if 'action' in t and t['action'] != 'HOLD']
if not action_trades:
st.info("No trades yet")
return
for trade in reversed(action_trades[-10:]):
action = trade.get('action', '')
price = trade.get('price', 0)
pnl = trade.get('pnl', 0)
timestamp = trade.get('timestamp', '')
reason = trade.get('reason', 'model')
try:
ts = datetime.fromisoformat(timestamp)
time_str = ts.strftime('%m/%d %H:%M')
except:
time_str = ''
# Determine display based on action and reason
if 'OPEN_LONG' in action:
color = "#26a69a"
side = "LONG"
elif 'OPEN_SHORT' in action:
color = "#ef5350"
side = "SHORT"
elif 'CLOSE' in action:
if reason == 'stop_loss':
color = "#ff4444"
side = "SL"
elif reason == 'take_profit':
color = "#00ff88"
side = "TP"
else:
color = "#ffc107"
side = "EXIT"
else:
color = "#888"
side = action
pnl_color = "#26a69a" if pnl >= 0 else "#ef5350"
pnl_sign = "+" if pnl >= 0 else ""
pnl_display = f"{pnl_sign}${pnl:,.2f}" if pnl != 0 else ""
st.markdown(f"""
{side}${price:,.2f}{time_str}
{pnl_display}
""", unsafe_allow_html=True)
def load_real_market_data(symbol: str = 'BTC/USDT', timeframe: str = '1h') -> pd.DataFrame:
"""Load OHLCV candlestick data โ via /api/ohlcv or direct Binance public API."""
import requests as _mkt_requests
import logging as _log
_logger = _log.getLogger(__name__)
clean_symbol = symbol.replace("/", "")
def _parse_ohlcv_list(data: list) -> pd.DataFrame:
"""Parse list of {time,open,high,low,close,volume} dicts into DataFrame."""
df = pd.DataFrame(data)
df.index = pd.to_datetime(df['time'], unit='s')
df.index.name = None
return df[['open', 'high', 'low', 'close', 'volume']]
# Primary: /api/ohlcv via local Flask server
api_url = get_api_url()
try:
resp = _mkt_requests.get(
f'{api_url}/api/ohlcv',
params={'symbol': clean_symbol, 'interval': timeframe, 'limit': 500},
timeout=10
)
if resp.ok:
data = resp.json()
if data and isinstance(data, list) and len(data) > 0:
return _parse_ohlcv_list(data)
else:
_logger.warning(f"load_real_market_data: empty/invalid response from {api_url} for {clean_symbol} {timeframe}: {str(data)[:200]}")
else:
_logger.warning(f"load_real_market_data: HTTP {resp.status_code} from {api_url}/api/ohlcv for {clean_symbol} {timeframe}")
except Exception as e:
_logger.warning(f"load_real_market_data: Flask API unavailable ({api_url}): {e}")
# Fallback: Direct Binance public API (no auth required, works on HF)
try:
_logger.info(f"load_real_market_data: trying direct Binance API for {clean_symbol} {timeframe}")
binance_url = os.environ.get("BINANCE_FUTURES_URL", "https://data-api.binance.vision")
resp = _mkt_requests.get(
f"{binance_url}/api/v3/klines",
params={'symbol': clean_symbol, 'interval': timeframe, 'limit': 500},
timeout=15
)
if resp.ok:
raw = resp.json()
if isinstance(raw, list) and len(raw) > 0 and not (isinstance(raw, dict) and raw.get('code')):
candles = [
{
'time': int(row[0]) // 1000,
'open': float(row[1]),
'high': float(row[2]),
'low': float(row[3]),
'close': float(row[4]),
'volume': float(row[5]),
}
for row in raw
]
_logger.info(f"load_real_market_data: direct Binance returned {len(candles)} candles for {clean_symbol} {timeframe}")
return _parse_ohlcv_list(candles)
else:
_logger.warning(f"load_real_market_data: Binance direct API returned unexpected data: {str(raw)[:200]}")
else:
_logger.warning(f"load_real_market_data: Binance direct API HTTP {resp.status_code} for {clean_symbol} {timeframe}")
except Exception as e:
_logger.error(f"load_real_market_data: direct Binance fallback failed: {e}")
# Final fallback: BinanceHistoricalDataFetcher if backtest module available (local server only)
if _HAS_BACKTEST:
try:
fetcher = BinanceHistoricalDataFetcher()
end_date = datetime.now()
days = TIMEFRAMES.get(timeframe, {}).get('days', 7)
start_date = end_date - timedelta(days=days)
if "USDT" in symbol and "/" not in symbol:
symbol = symbol.replace("USDT", "/USDT")
df = fetcher.fetch_historical_data(
symbol=symbol, timeframe=timeframe,
start_date=start_date, end_date=end_date,
)
return df
except Exception as e:
_logger.error(f"load_real_market_data: BinanceHistoricalDataFetcher failed: {e}")
return pd.DataFrame()
return pd.DataFrame()
@st.fragment(run_every=60)
def render_sidebar_metrics_fragment():
"""Render sidebar portfolio metrics with auto-refresh."""
import requests
import logging
logger = logging.getLogger(__name__)
try:
# Fetch State
try:
state_resp = requests.get(f'{get_api_url()}/api/state', timeout=5)
if state_resp.status_code == 200:
api_state = state_resp.json()
# Update session state with API data (optional, but good for other parts)
if 'balance' in api_state:
st.session_state.portfolio_balance = api_state.get('balance', 0)
st.session_state.total_pnl = api_state.get('total_pnl', 0)
# Render
st.markdown(f"""
Portfolio Value
{f'${st.session_state["portfolio_balance"]:,.2f}' if st.session_state.get('portfolio_balance') is not None else 'โ'}
P&L: {'+' if float(st.session_state.get('total_pnl') or 0) >= 0 else ''}${float(st.session_state.get('total_pnl') or 0):,.2f}
""", unsafe_allow_html=True)
except Exception as e:
st.markdown(f"
Connection Error
", unsafe_allow_html=True)
except Exception as e:
logger.error(f"Sidebar data fetch error: {e}")
@st.fragment(run_every=120)
def render_market_analysis_fragment(symbol: str):
"""Render market analysis panel with auto-refresh."""
import requests
import logging
logger = logging.getLogger(__name__)
st.markdown("### ๐ Market Analysis")
# Fetch Market Analysis for current asset
market_data = {}
try:
api_symbol = symbol.replace('/', '').upper()
market_resp = requests.get(f'{get_api_url()}/api/market?symbol={api_symbol}', timeout=15)
if market_resp.status_code == 200:
market_data = market_resp.json()
else:
st.markdown(f"""
๐ Market Analysis
API error (HTTP {market_resp.status_code})
Server returned non-200 for /api/market
""", unsafe_allow_html=True)
return
except Exception as e:
st.markdown(f"""
๐ Market Analysis
Unable to load (API server offline?)
Error: {str(e)}
""", unsafe_allow_html=True)
return
# Whale Tracker
whale = market_data.get('whale', {})
if whale:
if whale.get('error'):
st.markdown(f"""
๐ Whale Signals
Data Error
{whale.get('error')}
""", unsafe_allow_html=True)
else:
whale_color = "#26a69a" if whale.get('score', 0) > 0 else "#ef5350" if whale.get('score', 0) < 0 else "#888"
whale_emoji = "๐ข" if whale.get('score', 0) > 0.1 else "๐ด" if whale.get('score', 0) < -0.1 else "โช"
# Format Flow Metrics
flow_metrics = whale.get('flow_metrics', {})
net_flow = flow_metrics.get('net_flow', 0)
flow_color = "#26a69a" if net_flow > 0 else "#ef5350"
flow_sign = "+" if net_flow > 0 else "-"
# Format to K or M
if abs(net_flow) > 1000000:
flow_str = f"{flow_sign}${abs(net_flow)/1000000:.1f}M"
elif abs(net_flow) > 1000:
flow_str = f"{flow_sign}${abs(net_flow)/1000:.0f}K"
else:
flow_str = "$0"
st.markdown(f"""
# """, unsafe_allow_html=True)
# else:
# # Show placeholder when news data is not available yet
# st.markdown(f"""
#
#
๐ฐ News Sentiment
#
Loading...
#
# Waiting for first news fetch (takes ~1-2 min)
#
#
# """, unsafe_allow_html=True)
# HMM Regime
regime_data = market_data.get('regime', {})
if regime_data and not regime_data.get('error'):
r_type = regime_data.get('type', 'UNKNOWN')
# Colors: Green for Bull, Red for Bear, Orange for Breakout, Blue for Range
r_color = "#26a69a" if "BULL" in r_type else "#ef5350" if "BEAR" in r_type else "#ffa726" if "BREAKOUT" in r_type else "#42a5f5"
st.markdown(f"""
""", unsafe_allow_html=True)
# Ensemble Confidence Engine
confidence = market_data.get('ensemble_confidence')
if confidence is not None:
conf_pct = min(100, max(0, int(confidence * 100)))
# Map 0-1.0 to 0.25x - 2.0x for UI display (matching the ConfidenceEngine logic roughly)
mult = 0.25 + 1.75 * confidence if confidence < 0.5 else 1.0 + 1.0 * (confidence - 0.5) * 2 # Approximate for UI
c_color = "#26a69a" if confidence > 0.6 else "#ffa726" if confidence > 0.35 else "#ef5350"
st.markdown(f"""
๐ง Ensemble Agreement
{conf_pct}% Alignment
Position Size Multiplier: ~{mult:.1f}x
""", unsafe_allow_html=True)
@st.fragment(run_every=30)
def render_position_fragment(symbol: str):
"""Render current position and portfolio status with auto-refresh."""
import requests
import os
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
# 1. Fetch Trading State
state = {}
try:
state_resp = requests.get(f'{get_api_url()}/api/state', timeout=5)
if state_resp.status_code == 200:
state = state_resp.json()
except Exception as e:
logger.error(f"State fetch error: {e}")
# 2. Fetch Live Price (Fast, from API or Fallback)
current_price = 0.0
try:
# Try to get price from market API first (faster)
clean_symbol = symbol.replace('/', '').upper()
market_resp = requests.get(f'{get_api_url()}/api/market?symbol={clean_symbol}', timeout=5)
if market_resp.status_code == 200:
m_data = market_resp.json()
if 'price' in m_data:
current_price = float(m_data['price'])
# Fallback if API didn't return price
if current_price == 0:
live_data = load_real_market_data(symbol, '1m')
if not live_data.empty:
current_price = float(live_data.iloc[-1]['close'])
else:
live_1h = load_real_market_data(symbol, '1h')
if not live_1h.empty:
current_price = float(live_1h.iloc[-1]['close'])
except Exception as e:
logger.error(f"Price fetch error: {e}")
# 3. Fetch ALL Trades early to calculate perfectly mathematically synced global Portfolio Value
all_trades = []
try:
trades_resp = requests.get(f'{get_api_url()}/api/trades', timeout=5)
if trades_resp.status_code == 200:
all_trades = trades_resp.json()
except Exception as e:
logger.error(f"Trades fetch error: {e}")
realized_pnl_total = sum(t.get('pnl', 0) for t in all_trades if 'CLOSE' in t.get('action', '').upper() or 'EXIT' in t.get('action', '').upper())
open_pnl_total = 0.0
raw_assets = state.get('raw_state', {}).get('assets', {})
for sym, asset_data in raw_assets.items():
if asset_data.get('position', 0) != 0:
open_pnl_total += asset_data.get('pnl', 0)
if all_trades or raw_assets:
total_pnl = realized_pnl_total + open_pnl_total
else:
total_pnl = state.get('total_pnl', state.get('realized_pnl', 0))
balance = state.get('total_balance', state.get('balance'))
pnl_class = "metric-delta-positive" if total_pnl >= 0 else "metric-delta-negative"
pnl_sign = "+" if total_pnl >= 0 else ""
st.markdown(f"""
Portfolio Value
{f'${balance:,.2f}' if balance is not None else 'โ'}
P&L: {pnl_sign}${(total_pnl or 0):,.2f}
""", unsafe_allow_html=True)
# 4. Render Position Card
# Extract specific asset state from global state
asset_state = {}
if 'assets' in state:
# Try exact match or cleaned match
clean_symbol = symbol.replace('/', '').upper()
if symbol in state['assets']:
asset_state = state['assets'][symbol]
elif clean_symbol in state['assets']:
asset_state = state['assets'][clean_symbol]
# If not found, fall back to global state (in case API returns single asset state)
if not asset_state and 'position' in state:
asset_state = state
# NORMALIZE STATE: Ensure position_price is set for P&L calc
# CRITICAL: entry_price is the actual entry price, price is the current price
# Must prioritize entry_price over price to avoid showing current price as entry
if asset_state:
if 'position_price' not in asset_state and 'entry_price' in asset_state:
asset_state['position_price'] = asset_state['entry_price']
elif 'position_price' not in asset_state and 'price' in asset_state:
# Only use 'price' if entry_price is not available (legacy compatibility)
asset_state['position_price'] = asset_state['price']
render_position_card(asset_state, current_price, symbol)
# 5. Render Trade History
# Filter trades for current symbol (already fetched above)
clean_symbol = symbol.replace('/', '').upper()
trades = [t for t in all_trades if t.get('symbol', '').replace('/', '').upper() == clean_symbol]
render_trade_history(trades)
@st.fragment(run_every=60)
def render_agent_status_fragment():
"""Render active agent status and model info with auto-refresh."""
import requests
import os
from datetime import datetime
import logging
logger = logging.getLogger(__name__)
# In client mode, use /api/model which returns pre-computed model stats
if IS_CLIENT_MODE:
try:
model_resp = requests.get(f'{get_api_url()}/api/model', timeout=5)
if model_resp.status_code == 200:
model_info = model_resp.json()
total_return = model_info.get('total_return', 0)
win_rate = model_info.get('win_rate', 0)
total_trades = model_info.get('total_trades', 0)
model_date = model_info.get('model_date', 'Remote')
model_exists = model_info.get('model_exists', True)
else:
total_return, win_rate, total_trades = 0, 0, 0
model_date, model_exists = 'API error', False
except Exception:
total_return, win_rate, total_trades = 0, 0, 0
model_date, model_exists = 'Connecting...', False
else:
# Local mode: check filesystem and compute from trades
project_root = Path(__file__).parent.parent.parent
model_path = project_root / 'data' / 'models' / 'ultimate_agent.zip'
model_exists = model_path.exists()
state = {}
try:
state_resp = requests.get(f'{get_api_url()}/api/state', timeout=5)
if state_resp.status_code == 200:
state = state_resp.json()
except Exception:
pass
all_trades = state.get('trades', [])
try:
trades_resp = requests.get(f'{get_api_url()}/api/trades', timeout=5)
if trades_resp.status_code == 200:
all_trades = trades_resp.json()
except Exception:
pass
realized_pnl = sum(t.get('pnl', 0) for t in all_trades if 'CLOSE' in t.get('action', '').upper() or 'EXIT' in t.get('action', '').upper())
raw_assets = state.get('raw_state', {}).get('assets', {})
open_pnl = 0.0
for sym, asset_data in raw_assets.items():
if asset_data.get('position', 0) != 0:
current_price = asset_data.get('price', 0)
units = asset_data.get('units', 0)
position = asset_data.get('position', 0)
entry_price = 0
sym_trades = [t for t in all_trades if t.get('symbol', '').upper() == sym.upper() or t.get('asset', '').upper() == sym.upper()]
for t in reversed(sorted(sym_trades, key=lambda x: x.get('timestamp', ''))):
if 'OPEN' in t.get('action', '').upper():
entry_price = t.get('price', 0)
break
if entry_price > 0 and units > 0 and current_price > 0:
if position > 0:
open_pnl += (current_price - entry_price) * units
else:
open_pnl += (entry_price - current_price) * units
total_pnl = realized_pnl + open_pnl
total_return = None # Cannot compute without knowing real initial capital
closed_trades = [t for t in all_trades if 'CLOSE' in t.get('action', '').upper() or 'EXIT' in t.get('action', '').upper()]
if closed_trades:
winning = sum(1 for t in closed_trades if t.get('pnl', 0) > 0)
win_rate = (winning / len(closed_trades) * 100)
else:
win_rate = 0
total_trades = len(all_trades)
if model_exists:
try:
model_mtime = datetime.fromtimestamp(os.path.getmtime(model_path))
model_date = model_mtime.strftime("%Y-%m-%d")
except Exception:
model_date = "Unknown"
else:
model_date = "Not found"
return_str = f"{'+' if total_return >= 0 else ''}{total_return:.2f}% Return" if total_return is not None else "N/A Return"
return_color = "#26a69a" if (total_return or 0) >= 0 else "#ef5350"
st.markdown(f"""
Active Model
Ultimate Agent (PPO)
{return_str} | {win_rate:.1f}% Win Rate
Trades: {total_trades} | Model: {model_date}
{'โ Model loaded' if model_exists else 'โ Model not found'}
""", unsafe_allow_html=True)
def on_asset_change():
"""Callback for asset selection change."""
# Clear stale market analysis to trigger fresh fetch in fragments
st.session_state.market_analysis = None
# Optional: Reset other asset-specific state if needed
def main():
"""Main application entry point."""
# Initialize session state for timeframe
if 'timeframe' not in st.session_state:
st.session_state.timeframe = '1h'
# Initialize session state for selected asset
if 'selected_asset' not in st.session_state:
st.session_state.selected_asset = 'BTCUSDT'
# Check for multi-asset state to populate selector
state_preview = get_trading_state()
available_assets = state_preview.get('available_assets', ['BTCUSDT'])
# Initialize session state for auto-refresh (kept for toggle state only)
if 'auto_refresh' not in st.session_state:
st.session_state.auto_refresh = True
# Sidebar
with st.sidebar:
st.markdown("### โ๏ธ Settings")
# Asset Selector
if len(available_assets) > 1:
st.session_state.selected_asset = st.selectbox(
"Select Asset",
available_assets,
index=available_assets.index(st.session_state.selected_asset) if st.session_state.selected_asset in available_assets else 0,
on_change=on_asset_change
)
else:
st.markdown(f"**Asset:** {st.session_state.selected_asset}")
st.divider()
st.markdown("### ๐ Debug")
if st.checkbox("Show Crash Log"):
log_path = project_root / "crash.log"
if log_path.exists():
st.error("โ ๏ธ Crash Log Found")
with open(log_path, "r") as f:
st.text_area("Log Content", f.read(), height=300)
else:
st.success("โ No crash log found")
if st.checkbox("Show Process Log (Stdout/Stderr)"):
proc_log = project_root / "process.log"
if proc_log.exists():
with open(proc_log, "r") as f:
st.text_area("Process Output", f.read(), height=300)
else:
st.warning("โ ๏ธ process.log not found (yet)")
if st.checkbox("Show API Server Log"):
api_log = project_root / "api_server.log"
if api_log.exists():
with open(api_log, "r") as f:
st.text_area("API Server Output", f.read(), height=300)
else:
st.warning("โ ๏ธ api_server.log not found (yet)")
# Storage path is server-side only; omitted from client UI
# Database Reset (Dev Only)
env = os.getenv("ENVIRONMENT", "production").lower()
if env in ["dev", "development"]:
st.divider()
st.markdown("### ๐ Database Reset")
st.warning("โ ๏ธ This will clear all trades and positions!")
if st.button("๐๏ธ Reset All Trades", type="primary"):
try:
import subprocess
reset_script = project_root / "reset_all_storage.py"
if reset_script.exists():
result = subprocess.run(
[sys.executable, str(reset_script)],
capture_output=True,
text=True,
cwd=str(project_root)
)
if result.returncode == 0:
st.success("โ Database reset successful!")
st.code(result.stdout)
st.info("๐ Refresh the page to see changes")
else:
st.error(f"โ Reset failed: {result.stderr}")
else:
st.error(f"โ Reset script not found at {reset_script}")
except Exception as e:
st.error(f"โ Error running reset: {e}")
if st.checkbox("Show System Inspector"):
st.markdown("#### ๐ต๏ธ System Inspector")
if st.button("List Processes (ps aux)"):
try:
import subprocess
# Use 'ps aux' for more details, or 'ps -ef'
res = subprocess.run(['ps', 'aux'], capture_output=True, text=True)
st.code(res.stdout if res.returncode == 0 else res.stderr)
except Exception as e:
st.error(f"Failed to run ps: {e}")
if st.button("List Files (ls -R)"):
try:
import subprocess
res = subprocess.run(['ls', '-R'], capture_output=True, text=True)
st.code(res.stdout if res.returncode == 0 else res.stderr)
except Exception as e:
st.error(f"Failed to run ls: {e}")
if st.button("Check Connectivity (ping google.com)"):
try:
import subprocess
res = subprocess.run(['ping', '-c', '3', 'google.com'], capture_output=True, text=True)
st.code(res.stdout if res.returncode == 0 else res.stderr)
except Exception as e:
st.error(f"Ping failed: {e}")
st.markdown("### ๐ API Status")
eth_key = os.environ.get("ETHERSCAN_API_KEY")
sol_key = os.environ.get("SOLSCAN_API_KEY")
xrp_key = os.environ.get("XRPSCAN_API_KEY")
st.caption(f"ETH: {'โ Set' if eth_key else 'โ Missing'}")
st.caption(f"SOL: {'โ Set' if sol_key else 'โ Missing'}")
st.caption(f"XRP: {'โ Set' if xrp_key else 'โช Optional (Public)'}")
# Header
col1, col2, col3 = st.columns([3, 1, 1])
with col1:
st.markdown(f"# ๐ค DRL Trading System - {st.session_state.selected_asset}")
with col2:
refresh_status = "๐ Auto (10s)" if st.session_state.auto_refresh else "โธ๏ธ Paused"
st.markdown(f"""
๐ข Connected {refresh_status}
""", unsafe_allow_html=True)
with col3:
# Auto-refresh toggle
st.session_state.auto_refresh = st.toggle("Auto Refresh", value=st.session_state.auto_refresh)
# Data fetching is now handled inside fragments (render_sidebar_metrics_fragment, render_market_analysis_fragment)
pass
# Render Sidebar Metrics using Fragment
with st.sidebar:
render_sidebar_metrics_fragment()
st.divider()
# Main layout
col_main, col_sidebar = st.columns([3, 1])
with col_main:
# Timeframe selector
st.markdown("#### Select Timeframe")
tf_cols = st.columns(7)
timeframes = ['1m', '5m', '15m', '30m', '1h', '4h', '1d']
for i, tf in enumerate(timeframes):
with tf_cols[i]:
label = TIMEFRAMES[tf]['label']
if st.button(label, key=f"tf_{tf}", use_container_width=True,
type="primary" if st.session_state.timeframe == tf else "secondary"):
st.session_state.timeframe = tf
st.rerun()
# Load data for selected timeframe and asset
with st.spinner(f"Loading {st.session_state.selected_asset} {st.session_state.timeframe} data..."):
df = load_real_market_data(st.session_state.selected_asset, st.session_state.timeframe)
state = get_trading_state(st.session_state.selected_asset)
current_price = float(df.iloc[-1]['close']) if not df.empty else 0
# Tabs
tab_chart, tab_live_portfolio, tab_performance, tab_whales, tab_testnet, tab_htf, tab_backtest = st.tabs([
"๐ Live Chart", "๐ผ Live Portfolio", "๐ Performance", "๐ On-Chain Whales", "๐งช Testnet", "๐ฎ HTF Agent", "๐ฌ Backtest"
])
with tab_chart:
# TradingView Chart with WebSocket
trades = state.get('trades', [])
chart_html = create_tradingview_chart_with_websocket(df, trades, st.session_state.timeframe, st.session_state.selected_asset)
# Append timestamp comment to force re-render since components.html doesn't support key
# Create a placeholder for the chart to force re-rendering
chart_placeholder = st.empty()
# Append timestamp comment to force re-render since components.html doesn't support key
current_time = time.time()
chart_html += f""
with chart_placeholder:
components.html(chart_html, height=600)
# Info about trade markers
num_trades = len([t for t in trades if 'OPEN' in t.get('action', '')])
st.caption(f"๐ {num_trades} trade signals on chart โข Switch timeframes to see trades at different intervals")
# Trading Controls Section
st.markdown("---")
st.markdown("### ๐ฎ Trading Controls")
if IS_CLIENT_MODE:
st.info("๐ **Client Mode** โ Trading bot is managed on the remote server. Use the server dashboard to start/stop the bot or place manual trades.")
if st.button("๐ Refresh Data", key="refresh_data", use_container_width=True):
st.rerun()
else:
# Bot status check (server-side only)
import subprocess
bot_running = False
try:
result = subprocess.run(['pgrep', '-f', 'live_trading'], capture_output=True, text=True)
bot_running = result.returncode == 0
except Exception:
pass
if bot_running:
st.success("๐ข **Trading Bot is RUNNING** (Multi-Asset Mode)")
else:
st.warning("๐ **Trading Bot is STOPPED**")
ctrl_col1, ctrl_col2, ctrl_col3, ctrl_col4 = st.columns(4)
with ctrl_col1:
if not bot_running:
if st.button("โถ๏ธ Start Trading", key="start_trading", use_container_width=True, type="primary"):
try:
with open(project_root / "process.log", "a") as log_file:
subprocess.Popen(
['./venv/bin/python', '-u', 'live_trading_multi.py',
'--assets', 'BTCUSDT', 'ETHUSDT', 'SOLUSDT', 'XRPUSDT',
'--balance', '5000'],
cwd=str(project_root),
stdout=log_file,
stderr=log_file,
)
st.success("โ Multi-Asset Bot started!")
time.sleep(2)
st.rerun()
except Exception as e:
st.error(f"Failed to start: {e}")
else:
if st.button("โน๏ธ Stop Trading", key="stop_trading", use_container_width=True, type="secondary"):
try:
subprocess.run(['pkill', '-f', 'live_trading'], check=False)
st.info("โ Trading bot stopped")
time.sleep(1)
st.rerun()
except Exception as e:
st.error(f"Failed to stop: {e}")
with ctrl_col2:
if st.button("๐ Open Long", key="open_long", use_container_width=True):
trade = {
'timestamp': datetime.now().isoformat(),
'action': 'OPEN_LONG',
'price': current_price,
'pnl': 0,
'balance': state.get('balance'),
'position': 1,
'reason': 'manual',
'symbol': st.session_state.selected_asset,
'asset': st.session_state.selected_asset,
}
storage.log_trade(trade)
st.success(f"โ Opened LONG @ ${current_price:,.2f}")
time.sleep(0.5)
st.rerun()
with ctrl_col3:
if st.button("๐ Open Short", key="open_short", use_container_width=True):
trade = {
'timestamp': datetime.now().isoformat(),
'action': 'OPEN_SHORT',
'price': current_price,
'pnl': 0,
'balance': state.get('balance'),
'position': -1,
'reason': 'manual',
'symbol': st.session_state.selected_asset,
'asset': st.session_state.selected_asset,
}
storage.log_trade(trade)
st.success(f"โ Opened SHORT @ ${current_price:,.2f}")
time.sleep(0.5)
st.rerun()
with ctrl_col4:
if st.button("๐ช Close Position", key="close_position", use_container_width=True):
position = state.get('position', 0)
if position != 0:
action = 'CLOSE_LONG' if position == 1 else 'CLOSE_SHORT'
trade = {
'timestamp': datetime.now().isoformat(),
'action': action,
'price': current_price,
'pnl': 0,
'balance': state.get('balance'),
'position': 0,
'reason': 'manual',
'symbol': st.session_state.selected_asset,
'asset': st.session_state.selected_asset,
}
storage.log_trade(trade)
st.success(f"โ Closed position @ ${current_price:,.2f}")
time.sleep(0.5)
st.rerun()
else:
st.info("No position to close")
st.markdown("")
action_col1, action_col2 = st.columns(2)
with action_col1:
if st.button("๐ Refresh Data", key="refresh_data", use_container_width=True):
st.rerun()
with action_col2:
if st.button("๐๏ธ Clear Trade Log", key="clear_log", use_container_width=True):
try:
log_file = project_root / 'logs' / 'trading_log.json'
state_file = project_root / 'logs' / 'trading_state.json'
log_file.write_text('')
if state_file.exists():
state_file.unlink()
st.success("โ Trade log cleared")
time.sleep(0.5)
st.rerun()
except Exception as e:
st.error(f"Failed to clear log: {e}")
with tab_live_portfolio:
# โโโ Compute portfolio metrics from trade data โโโ
all_trades_lp = []
try:
# Use API in client mode, local storage otherwise
all_trades_lp = load_trading_log()
if not IS_CLIENT_MODE:
# Apply reset filter (local mode only โ API already filters)
try:
lp_state = storage.load_state()
reset_ts = lp_state.get('reset_timestamp')
if reset_ts:
reset_dt = datetime.fromisoformat(reset_ts.replace('Z', '+00:00'))
all_trades_lp = [t for t in all_trades_lp if datetime.fromisoformat(t.get('timestamp', '2020-01-01').replace('Z', '+00:00')) >= reset_dt]
except:
pass
except:
pass
# Separate by symbol and compute per-asset metrics
assets_by_symbol = {}
for t in all_trades_lp:
sym = t.get('symbol', t.get('asset', 'UNKNOWN'))
sym = sym.replace('/', '').upper()
if sym not in assets_by_symbol:
assets_by_symbol[sym] = []
assets_by_symbol[sym].append(t)
# Compute closed P&L, open P&L, win rate
realized_pnl_total = 0.0
open_pnl_total = 0.0
total_closed_trades = 0
total_winning_trades = 0
total_open_trades = 0
equity_points = [0.0] # Start at 0%
asset_rows = []
# FIX: State structure is state['assets'], not state['raw_state']['assets']
raw_assets = state.get('assets', {})
for sym, trades_list in assets_by_symbol.items():
sorted_trades = sorted(trades_list, key=lambda x: x.get('timestamp', ''))
sym_realized = 0.0
sym_open_pnl = 0.0
sym_wins = 0
sym_closed = 0
sym_open = 0
sym_best = None
sym_worst = None
sym_status = 'FLAT'
for t in sorted_trades:
action = t.get('action', '').upper()
pnl = t.get('pnl', 0) or 0
if 'CLOSE' in action or 'EXIT' in action:
sym_realized += pnl
sym_closed += 1
if pnl > 0:
sym_wins += 1
equity_points.append(equity_points[-1] + pnl)
# Track best/worst
if sym_best is None or pnl > sym_best:
sym_best = pnl
if sym_worst is None or pnl < sym_worst:
sym_worst = pnl
elif 'OPEN_LONG' in action:
sym_status = 'LONG'
sym_open += 1
elif 'OPEN_SHORT' in action:
sym_status = 'SHORT'
sym_open += 1
# FIX: Determine final status from last trade (if it was a CLOSE, position is FLAT)
if sorted_trades:
last_trade = sorted_trades[-1]
last_action = last_trade.get('action', '').upper()
if 'CLOSE' in last_action or 'EXIT' in last_action:
sym_status = 'FLAT'
# Check current state for position status (this overrides trade-based status)
if sym in raw_assets:
asset_data = raw_assets[sym]
if asset_data.get('position', 0) != 0:
# Calculate unrealized P&L from entry price vs current price
current_price = asset_data.get('price', 0)
units = asset_data.get('units', 0)
position = asset_data.get('position', 0)
# Find entry price from last OPEN trade
entry_price = 0
for t in reversed(sorted_trades):
if 'OPEN' in t.get('action', '').upper():
entry_price = t.get('price', 0)
break
# Calculate unrealized P&L
if entry_price > 0 and units > 0 and current_price > 0:
if position > 0: # LONG
sym_open_pnl = (current_price - entry_price) * units
else: # SHORT
sym_open_pnl = (entry_price - current_price) * units
sym_status = 'LONG' if position > 0 else 'SHORT'
else:
sym_status = 'FLAT'
realized_pnl_total += sym_realized
open_pnl_total += sym_open_pnl
total_closed_trades += sym_closed
total_winning_trades += sym_wins
if sym_status != 'FLAT':
total_open_trades += 1
# Format display symbol
display_sym = sym
if sym.endswith('USDT'):
display_sym = sym[:-4] + ' /USDT'
# Get price / equity from raw state
sym_price = raw_assets.get(sym, {}).get('price', 0)
# Calculate True Equity mathematically instead of relying on historically corrupted bot.balance
sym_equity = 5000 + sym_realized + sym_open_pnl
asset_rows.append({
'symbol': display_sym,
'raw_symbol': sym,
'status': sym_status,
'price': sym_price,
'equity': sym_equity,
'pnl': sym_realized + sym_open_pnl,
'trades': sym_closed + (1 if sym_status != 'FLAT' else 0),
'open_trades': 1 if sym_status != 'FLAT' else 0,
'win_rate': (sym_wins / sym_closed * 100) if sym_closed > 0 else 0,
'wins': sym_wins,
'closed': sym_closed,
'best': sym_best,
'worst': sym_worst,
})
# Overall metrics
# Fixed: Always use 4 assets (BTCUSDT, ETHUSDT, SOLUSDT, XRPUSDT)
# Previously used len(assets_by_symbol) which only counted assets with trades
# Calculate from trades (single source of truth) - same logic as Agent Status sidebar
lp_grand_total_pnl = realized_pnl_total + open_pnl_total
lp_total_balance = state.get('total_balance', state.get('balance'))
overall_win_rate = (total_winning_trades / total_closed_trades * 100) if total_closed_trades > 0 else 0
total_trades_count = total_closed_trades + total_open_trades
lp_active_assets_count = len(asset_rows) if asset_rows else len(state.get('available_assets', []))
# System status (client mode: infer from recent trades; server mode: check process)
if IS_CLIENT_MODE:
is_online = bool(all_trades_lp and (datetime.now() - datetime.fromisoformat(
all_trades_lp[-1].get('timestamp', '2000-01-01').replace('Z', '+00:00').split('+')[0]
)).total_seconds() < 3600)
else:
is_online = check_process_running("live_trading_multi.py")
status_dot = '๐ข' if is_online else '๐ด'
status_text = 'Online' if is_online else 'Offline'
status_color = '#00e676' if is_online else '#ff5252'
# Color helpers
def pnl_color(val):
return '#00e676' if val >= 0 else '#ff5252'
def pnl_sign(val):
return '+' if val >= 0 else '-'
# โโโ Build the Live Portfolio HTML โโโ
# Equity curve data for SVG chart (pure inline, no CDN)
eq_pct = list(equity_points) # absolute PnL values; SVG normalizes by range
# Build SVG polyline points
svg_w = 900
svg_h = 160
n_points = len(eq_pct)
eq_min_val = min(eq_pct) if eq_pct else 0
eq_max_val = max(eq_pct) if eq_pct else 0
eq_range_val = max(abs(eq_min_val), abs(eq_max_val), 0.01)
padding_y = 20 # vertical padding
svg_points = []
svg_fill_points = []
for i, val in enumerate(eq_pct):
x = (i / max(n_points - 1, 1)) * svg_w
# Map value from [-range, +range] to [svg_h - padding, padding]
y = svg_h - padding_y - ((val + eq_range_val) / (2 * eq_range_val)) * (svg_h - 2 * padding_y)
svg_points.append(f"{x:.1f},{y:.1f}")
svg_fill_points.append(f"{x:.1f},{y:.1f}")
polyline_str = ' '.join(svg_points)
# Close the fill polygon at bottom
fill_points = svg_fill_points.copy()
if fill_points:
fill_points.append(f"{svg_w:.1f},{svg_h - padding_y:.1f}")
fill_points.append(f"0,{svg_h - padding_y:.1f}")
fill_str = ' '.join(fill_points)
last_eq = eq_pct[-1] if eq_pct else 0
line_color = '#00e676' if last_eq >= 0 else '#ff5252'
fill_color_start = 'rgba(0,230,118,0.3)' if last_eq >= 0 else 'rgba(255,82,82,0.3)'
fill_color_end = 'rgba(0,230,118,0.0)' if last_eq >= 0 else 'rgba(255,82,82,0.0)'
# Zero line Y position
zero_y = svg_h - padding_y - ((0 + eq_range_val) / (2 * eq_range_val)) * (svg_h - 2 * padding_y)
# Last point for dot
last_x = svg_w if n_points <= 1 else ((n_points - 1) / max(n_points - 1, 1)) * svg_w
last_y = svg_h - padding_y - ((last_eq + eq_range_val) / (2 * eq_range_val)) * (svg_h - 2 * padding_y)
# Y-axis labels
top_label = f"+{eq_range_val:.1f}%"
bot_label = f"-{eq_range_val:.1f}%"
svg_chart = f'''
'''
# Build asset rows HTML
asset_rows_html = ''
for row in asset_rows:
# Status badge
if row['status'] == 'LONG':
status_html = 'โ LONG'
elif row['status'] == 'SHORT':
status_html = 'โ SHORT'
else:
status_html = 'โ โ'
# PNL
pnl_val = row['pnl']
pnl_html = f'โ'
pnl_dollar_html = f'{pnl_sign(pnl_val)}${abs(pnl_val):,.2f}'
# Trades
trades_str = str(row['trades'])
if row['open_trades'] > 0:
trades_str += f' (+{row["open_trades"]})'
# Win rate bar
wr = row['win_rate']
bar_color = '#00e676' if wr >= 50 else '#ff9800' if wr > 0 else '#555'
wr_html = f'''
{wr:.0f}%
'''
# Best
if row['best'] is not None:
best_html = f'{pnl_sign(row["best"])}${abs(row["best"]):,.2f}'
else:
best_html = 'โ'
# Worst
if row['worst'] is not None:
worst_html = f'-${abs(row["worst"]):,.2f}'
else:
worst_html = 'โ'
asset_rows_html += f'''
{row['symbol']}
{status_html}
${row['price']:,.2f}
${row['equity']:,.2f}
{pnl_html}
{pnl_dollar_html}
{trades_str}
{wr_html}
{best_html}
{worst_html}
'''
if not asset_rows_html:
asset_rows_html = '''
No trades recorded yet. Start the trading bot to see portfolio data.
'''
portfolio_html = f'''
Live PortfolioLIVE TRADING{status_dot} {status_text}