import datetime
import time
import nest_asyncio
import pandas as pd
import plotly.graph_objects as go
import streamlit as st
from plotly.subplots import make_subplots
from frontend.st_utils import get_backend_api_client, initialize_st_page
# Enable nested async
nest_asyncio.apply()
initialize_st_page(
layout="wide",
show_readme=False
)
# Initialize backend client
backend_api_client = get_backend_api_client()
# Initialize session state
if "selected_account" not in st.session_state:
st.session_state.selected_account = None
if "selected_connector" not in st.session_state:
st.session_state.selected_connector = None
if "selected_market" not in st.session_state:
st.session_state.selected_market = {"connector": "binance_perpetual", "trading_pair": "BTC-USDT"}
if "candles_connector" not in st.session_state:
st.session_state.candles_connector = None
if "auto_refresh_enabled" not in st.session_state:
st.session_state.auto_refresh_enabled = False # Start with manual refresh
if "chart_interval" not in st.session_state:
st.session_state.chart_interval = "1m"
if "max_candles" not in st.session_state:
st.session_state.max_candles = 100 # Reduced for better performance
if "last_api_request" not in st.session_state:
st.session_state.last_api_request = 0 # Track last API request time
if "last_refresh_time" not in st.session_state:
st.session_state.last_refresh_time = 0 # Track last refresh time
# Trading form session state
if "trade_custom_price" not in st.session_state:
st.session_state.trade_custom_price = None # User's custom price input
if "trade_price_set_by_user" not in st.session_state:
st.session_state.trade_price_set_by_user = False # Track if user set custom price
if "last_order_type" not in st.session_state:
st.session_state.last_order_type = "market" # Track order type changes
# Set refresh interval for real-time updates
REFRESH_INTERVAL = 30 # seconds
def get_accounts_and_credentials():
"""Get available accounts and their credentials."""
try:
accounts_list = backend_api_client.accounts.list_accounts()
credentials_list = {}
for account in accounts_list:
credentials = backend_api_client.accounts.list_account_credentials(account_name=account)
credentials_list[account] = credentials
return accounts_list, credentials_list
except Exception as e:
st.error(f"Failed to fetch accounts: {e}")
return [], {}
def get_candles_connectors():
"""Get available candles feed connectors."""
try:
# For now, return a hardcoded list of known exchanges that provide candles
return ["binance", "binance_perpetual", "kucoin", "okx", "okx_perpetual", "gate_io"]
except Exception as e:
st.warning(f"Could not fetch candles feed connectors: {e}")
return []
def get_positions():
"""Get current positions."""
try:
response = backend_api_client.trading.get_positions(limit=100)
# Handle both response formats
if isinstance(response, list):
return response
elif isinstance(response, dict) and response.get("status") == "success":
return response.get("data", [])
elif isinstance(response, dict) and "data" in response:
# Handle the actual API response format
return response.get("data", [])
return []
except Exception as e:
st.error(f"Failed to fetch positions: {e}")
return []
def get_active_orders():
"""Get active orders."""
try:
response = backend_api_client.trading.get_active_orders(limit=100)
# Handle both response formats
if isinstance(response, list):
return response
elif isinstance(response, dict):
# Check for different response formats
if response.get("status") == "success":
return response.get("data", [])
elif "data" in response:
# Handle response format like {"data": [...], "pagination": {...}}
return response.get("data", [])
return []
except Exception as e:
st.error(f"Failed to fetch active orders: {e}")
return []
def get_order_history():
"""Get recent order history."""
try:
# Try to get orders instead of order_history since that method doesn't exist
response = backend_api_client.trading.search_orders(limit=50)
# Handle both response formats
if isinstance(response, list):
return response
elif isinstance(response, dict):
# Check for different response formats
if response.get("status") == "success":
return response.get("data", [])
elif "data" in response:
# Handle response format like {"data": [...], "pagination": {...}}
return response.get("data", [])
return []
except Exception:
# If get_orders doesn't exist either, just return empty list without warning
return []
def get_order_book(connector, trading_pair, depth=10):
"""Get order book data for the selected trading pair."""
try:
response = backend_api_client.market_data.get_order_book(
connector_name=connector,
trading_pair=trading_pair,
depth=depth
)
# Handle both response formats
if isinstance(response, dict):
if "status" in response and response.get("status") == "success":
return response.get("data", {})
elif "bids" in response and "asks" in response:
return response
return {}
except Exception as e:
st.warning(f"Could not fetch order book: {e}")
return {}
def get_funding_rate(connector, trading_pair):
"""Get funding rate for perpetual contracts."""
try:
# Only try to get funding rate for perpetual connectors
if "perpetual" in connector.lower():
response = backend_api_client.market_data.get_funding_info(
connector_name=connector,
trading_pair=trading_pair
)
# Handle both response formats
if isinstance(response, dict):
if "status" in response and response.get("status") == "success":
return response.get("data", {})
elif "funding_rate" in response:
return response
return {}
return {}
except Exception:
return {}
def get_trade_history(account_name, connector_name, trading_pair):
"""Get trade history for the selected account and trading pair."""
try:
# Try to get trades for this specific account/connector/pair
response = backend_api_client.trading.get_trades(
account_name=account_name,
connector_name=connector_name,
trading_pair=trading_pair,
limit=100
)
# Handle both response formats
if isinstance(response, list):
return response
elif isinstance(response, dict) and response.get("status") == "success":
return response.get("data", [])
return []
except Exception:
# If method doesn't exist, try alternative approach
try:
# Get all orders and filter for filled ones
orders = get_order_history()
trades = []
for order in orders:
if (order.get("status") == "FILLED" and
order.get("trading_pair") == trading_pair and
order.get("connector_name") == connector_name):
trades.append(order)
return trades
except Exception:
return []
def get_market_data(connector, trading_pair, interval="1m", max_records=100, candles_connector=None):
"""Get market data with proper error handling."""
start_time = time.time()
try:
# Get candles
candles = []
try:
# Use candles_connector if provided, otherwise use main connector
candles_conn = candles_connector if candles_connector else connector
candles_response = backend_api_client.market_data.get_candles(
connector_name=candles_conn,
trading_pair=trading_pair,
interval=interval,
max_records=max_records
)
# Handle both response formats
if isinstance(candles_response, list):
# Direct list response
candles = candles_response
elif isinstance(candles_response, dict) and candles_response.get("status") == "success":
# Response object with status and data
candles = candles_response.get("data", [])
except Exception as e:
st.warning(f"Could not fetch candles: {e}")
# Get current price
prices = {}
try:
price_response = backend_api_client.market_data.get_prices(
connector_name=connector,
trading_pairs=[trading_pair]
)
# Handle both response formats
if isinstance(price_response, dict):
if "status" in price_response and price_response.get("status") == "success":
prices = price_response.get("data", {})
elif "prices" in price_response:
# Response has a "prices" field containing the actual price data
prices = price_response.get("prices", {})
else:
# Direct dict response with prices
prices = price_response
elif isinstance(price_response, list):
# If it's a list, try to convert to dict
prices = {item.get("trading_pair", "unknown"): item.get("price", 0) for item in price_response if
isinstance(item, dict)}
except Exception as e:
st.warning(f"Could not fetch prices: {e}")
# Calculate fetch time for performance monitoring
fetch_time = (time.time() - start_time) * 1000
st.session_state["last_fetch_time"] = fetch_time
st.session_state["last_fetch_timestamp"] = time.time()
return candles, prices
except Exception as e:
st.error(f"Failed to fetch market data: {e}")
return [], {}
def place_order(order_data):
"""Place a trading order."""
try:
response = backend_api_client.trading.place_order(**order_data)
if response.get("status") == "submitted":
st.success(f"Order placed successfully! Order ID: {response.get('order_id')}")
return True
else:
st.error(f"Failed to place order: {response.get('message', 'Unknown error')}")
return False
except Exception as e:
st.error(f"Failed to place order: {e}")
return False
def cancel_order(account_name, connector_name, order_id):
"""Cancel an order."""
try:
response = backend_api_client.trading.cancel_order(
account_name=account_name,
connector_name=connector_name,
client_order_id=order_id
)
if response.get("status") == "success":
st.success(f"Order {order_id} cancelled successfully!")
return True
else:
st.error(f"Failed to cancel order: {response.get('message', 'Unknown error')}")
return False
except Exception as e:
st.error(f"Failed to cancel order: {e}")
return False
def get_default_layout(title=None, height=800, width=1100):
layout = {
"template": "plotly_dark",
"plot_bgcolor": 'rgba(0, 0, 0, 0)', # Transparent background
"paper_bgcolor": 'rgba(0, 0, 0, 0.1)', # Lighter shade for the paper
"font": {"color": 'white', "size": 12}, # Consistent font color and size
"height": height,
"width": width,
"margin": {"l": 20, "r": 20, "t": 50, "b": 20},
"xaxis_rangeslider_visible": False,
"hovermode": "x unified",
"showlegend": False,
}
if title:
layout["title"] = title
return layout
def create_candlestick_chart(candles_data, connector_name="", trading_pair="", interval="", trades_data=None):
"""Create a candlestick chart with custom theme, trade markers, and volume bars."""
if not candles_data:
fig = go.Figure()
fig.add_annotation(
text="No candle data available",
xref="paper", yref="paper",
x=0.5, y=0.5,
showarrow=False
)
fig.update_layout(**get_default_layout(height=800))
return fig
try:
# Convert candles data to DataFrame
df = pd.DataFrame(candles_data)
if df.empty:
return go.Figure()
# Convert timestamp to datetime for better display
if 'timestamp' in df.columns:
df['datetime'] = pd.to_datetime(df['timestamp'], unit='s')
# Calculate quote volume (volume * close price)
if 'volume' in df.columns and 'close' in df.columns:
df['quote_volume'] = df['volume'] * df['close']
else:
df['quote_volume'] = 0
# Create subplots with shared x-axis: candlestick chart on top, volume bars on bottom
fig = make_subplots(
rows=2, cols=1,
shared_xaxes=True,
vertical_spacing=0.01,
row_heights=[0.8, 0.2],
subplot_titles=(None, None) # No subplot titles
)
# Add candlestick trace to first subplot
fig.add_trace(
go.Candlestick(
x=df['datetime'] if 'datetime' in df.columns else df.index,
open=df['open'],
high=df['high'],
low=df['low'],
close=df['close'],
name="Candlesticks",
),
row=1, col=1
)
# Add volume bars to second subplot if volume data exists
if 'quote_volume' in df.columns and df['quote_volume'].sum() > 0:
# Color volume bars based on price movement (green for up, red for down)
colors = ['rgba(0, 255, 0, 0.5)' if close >= open_price else 'rgba(255, 0, 0, 0.5)'
for close, open_price in zip(df['close'], df['open'])]
fig.add_trace(
go.Bar(
x=df['datetime'] if 'datetime' in df.columns else df.index,
y=df['quote_volume'],
name='Volume',
marker=dict(color=colors),
yaxis='y2',
hovertemplate='Volume: $%{y:,.0f}
%{x}'
),
row=2, col=1
)
# Add trade markers if trade data is provided (add to first subplot)
if trades_data:
try:
trades_df = pd.DataFrame(trades_data)
if not trades_df.empty:
# Convert trade timestamps to datetime
if 'timestamp' in trades_df.columns:
trades_df['datetime'] = pd.to_datetime(trades_df['timestamp'], unit='s')
elif 'created_at' in trades_df.columns:
trades_df['datetime'] = pd.to_datetime(trades_df['created_at'])
elif 'execution_time' in trades_df.columns:
trades_df['datetime'] = pd.to_datetime(trades_df['execution_time'])
# Filter trades to chart time range if datetime column exists
if 'datetime' in trades_df.columns and 'datetime' in df.columns:
chart_start = df['datetime'].min()
chart_end = df['datetime'].max()
trades_in_range = trades_df[
(trades_df['datetime'] >= chart_start) &
(trades_df['datetime'] <= chart_end)
]
if not trades_in_range.empty:
# Separate buy and sell trades
buy_trades = trades_in_range[
trades_in_range.get('trade_type', trades_in_range.get('side', '')) == 'buy']
sell_trades = trades_in_range[
trades_in_range.get('trade_type', trades_in_range.get('side', '')) == 'sell']
# Add buy markers (green triangles pointing up) to first subplot
if not buy_trades.empty:
fig.add_trace(
go.Scatter(
x=buy_trades['datetime'],
y=buy_trades.get('price', buy_trades.get('avg_price', 0)),
mode='markers',
marker=dict(
symbol='triangle-up',
size=10,
line=dict(width=1, color='white')
),
name='Buy Trades',
hovertemplate='BUY
Price: $%{y:.4f}
Time: %{x}'
),
row=1, col=1
)
# Add sell markers (red triangles pointing down) to first subplot
if not sell_trades.empty:
fig.add_trace(
go.Scatter(
x=sell_trades['datetime'],
y=sell_trades.get('price', sell_trades.get('avg_price', 0)),
mode='markers',
marker=dict(
symbol='triangle-down',
size=10,
line=dict(width=1, color='white')
),
name='Sell Trades',
hovertemplate='SELL
Price: $%{y:.4f}
Time: %{x}'
),
row=1, col=1
)
except Exception:
# If trade markers fail, continue without them
pass
# Create title
title = f"{connector_name}: {trading_pair} ({interval})" if connector_name else "Price Chart"
# Get base layout and customize for subplots
layout = get_default_layout(title=title, height=700) # Increased height for two subplots
# Update specific layout options for subplots
layout.update({
"xaxis": {
"rangeslider": {"visible": False},
"showgrid": True,
"gridcolor": "rgba(255,255,255,0.1)",
"color": "white"
},
"yaxis": {
"title": "Price ($)",
"showgrid": True,
"gridcolor": "rgba(255,255,255,0.1)",
"color": "white"
},
"xaxis2": {
"showgrid": True,
"gridcolor": "rgba(255,255,255,0.1)",
"color": "white"
},
"yaxis2": {
"title": "Volume (Quote)",
"showgrid": True,
"gridcolor": "rgba(255,255,255,0.1)",
"color": "white"
}
})
fig.update_layout(**layout)
return fig
except Exception as e:
# Fallback chart with error message
fig = go.Figure()
fig.add_annotation(
text=f"Error creating chart: {str(e)}",
xref="paper", yref="paper",
x=0.5, y=0.5,
showarrow=False
)
fig.update_layout(**get_default_layout(height=600))
return fig
def create_order_book_chart(order_book_data, current_price=None, depth_percentage=1.0, trading_pair=""):
"""Create an order book histogram with price on Y-axis and volume on X-axis."""
if not order_book_data or not order_book_data.get("bids") or not order_book_data.get("asks"):
fig = go.Figure()
fig.add_annotation(
text="No order book data available",
xref="paper", yref="paper",
x=0.5, y=0.5,
showarrow=False
)
fig.update_layout(**get_default_layout(title="Order Book", height=600, width=300))
return fig, None, None
try:
bids = order_book_data.get("bids", [])
asks = order_book_data.get("asks", [])
if not bids or not asks:
fig = go.Figure()
fig.add_annotation(
text="Insufficient order book data",
xref="paper", yref="paper",
x=0.5, y=0.5,
showarrow=False
)
fig.update_layout(**get_default_layout(title="Order Book", height=600, width=300))
return fig, None, None
# Process bids and asks - they're already objects with price/amount keys
bids_df = pd.DataFrame(bids)
asks_df = pd.DataFrame(asks)
# Convert to float
bids_df['price'] = bids_df['price'].astype(float)
bids_df['amount'] = bids_df['amount'].astype(float)
asks_df['price'] = asks_df['price'].astype(float)
asks_df['amount'] = asks_df['amount'].astype(float)
# Convert amounts to quote asset (USDT) for better normalization
bids_df['quote_volume'] = bids_df['price'] * bids_df['amount']
asks_df['quote_volume'] = asks_df['price'] * asks_df['amount']
# Sort bids descending (highest price first) and asks ascending (lowest price first)
bids_df = bids_df.sort_values('price', ascending=False)
asks_df = asks_df.sort_values('price', ascending=True)
# Calculate cumulative volumes for better visualization
bids_df['cumulative_volume'] = bids_df['quote_volume'].cumsum()
asks_df['cumulative_volume'] = asks_df['quote_volume'].cumsum()
# Filter by depth percentage if current price is available
if current_price:
price_range = current_price * (depth_percentage / 100)
min_price = current_price - price_range
max_price = current_price + price_range
bids_df = bids_df[bids_df['price'] >= min_price]
asks_df = asks_df[asks_df['price'] <= max_price]
# Create order book chart
fig = go.Figure()
# Add bid bars (green, all positive values) - using cumulative volume
if not bids_df.empty:
fig.add_trace(
go.Bar(
x=bids_df['cumulative_volume'], # Using cumulative volume
y=bids_df['price'],
orientation='h',
name='Bids',
marker=dict(opacity=0.8),
hovertemplate='BID
Price: $%{y:.4f}
Cumulative Volume: $%{x:,.0f}
Level Volume: $%{customdata:,.0f}',
customdata=bids_df['quote_volume'], # Show individual level volume in hover
offsetgroup='bids'
)
)
# Add ask bars (red, all positive values) - using cumulative volume
if not asks_df.empty:
fig.add_trace(
go.Bar(
x=asks_df['cumulative_volume'], # Using cumulative volume
y=asks_df['price'],
orientation='h',
name='Asks',
marker=dict(opacity=0.8),
hovertemplate='ASK
Price: $%{y:.4f}
Cumulative Volume: $%{x:,.0f}
Level Volume: $%{customdata:,.0f}',
customdata=asks_df['quote_volume'], # Show individual level volume in hover
offsetgroup='asks'
)
)
# Update layout for histogram style
layout = get_default_layout(title="Order Book Depth", height=600, width=300)
layout.update({
"xaxis": {
"title": "Cumulative Volume (USDT)",
"color": "white",
"showgrid": True,
"gridcolor": "rgba(255,255,255,0.1)",
"zeroline": True,
"zerolinecolor": "rgba(255,255,255,0.3)",
"zerolinewidth": 1
},
"yaxis": {
"title": "Price ($)",
"color": "white",
"showgrid": True,
"gridcolor": "rgba(255,255,255,0.1)",
"type": "linear"
},
"bargap": 0.02,
"bargroupgap": 0.02,
"showlegend": False,
"hovermode": "closest"
})
fig.update_layout(**layout)
# Return price range for syncing with candles chart
price_min = None
price_max = None
if not bids_df.empty and not asks_df.empty:
price_min = min(bids_df['price'].min(), asks_df['price'].min())
price_max = max(bids_df['price'].max(), asks_df['price'].max())
elif not bids_df.empty:
price_min = price_max = bids_df['price'].min()
elif not asks_df.empty:
price_min = price_max = asks_df['price'].max()
return fig, price_min, price_max
except Exception as e:
# Fallback chart with error message
fig = go.Figure()
fig.add_annotation(
text=f"Error creating order book: {str(e)}",
xref="paper", yref="paper",
x=0.5, y=0.5,
showarrow=False
)
fig.update_layout(**get_default_layout(title="Order Book", height=600, width=300))
return fig, None, None
def render_positions_table(positions_data):
"""Render positions table with enhanced metrics and hedging information."""
if not positions_data:
st.info("No open positions found.")
return
# Convert to DataFrame for better display
df = pd.DataFrame(positions_data)
if df.empty:
st.info("No open positions found.")
return
# Calculate original value (amount * entry_price)
if 'amount' in df.columns and 'entry_price' in df.columns:
df['original_value'] = df['amount'] * df['entry_price']
st.subheader("🎯 Open Positions")
# Calculate and display summary metrics
col1, col2, col3, col4 = st.columns(4)
with col1:
total_unrealized_pnl = df['unrealized_pnl'].sum() if 'unrealized_pnl' in df.columns else 0
st.metric(
"Total Unrealized PnL",
f"${total_unrealized_pnl:,.2f}",
delta=None,
delta_color="normal" if total_unrealized_pnl >= 0 else "inverse"
)
with col2:
total_original_value = abs(df['original_value']).sum() if 'original_value' in df.columns else 0
st.metric(
"Total Position Amount",
f"${abs(total_original_value):,.2f}"
)
# Separate long and short positions for hedging analysis
long_positions = df[df['amount'] > 0] if 'amount' in df.columns else pd.DataFrame()
short_positions = df[df['amount'] < 0] if 'amount' in df.columns else pd.DataFrame()
with col3:
long_value = long_positions['original_value'].sum() if not long_positions.empty and 'original_value' in long_positions.columns else 0
st.metric(
"Long Exposure",
f"${abs(long_value):,.2f}",
help="Total value of long positions"
)
with col4:
short_value = short_positions['original_value'].sum() if not short_positions.empty and 'original_value' in short_positions.columns else 0
st.metric(
"Short Exposure",
f"${abs(short_value):,.2f}",
help="Total value of short positions"
)
# Calculate hedge ratio if we have both long and short positions
if long_value != 0 and short_value != 0:
hedge_ratio = min(abs(short_value), abs(long_value)) / max(abs(short_value), abs(long_value)) * 100
st.info(f"📊 **Hedge Ratio**: {hedge_ratio:.1f}% (Higher = More Hedged)")
elif long_value > 0 and short_value == 0:
st.warning("⚠️ **Portfolio is fully LONG** - No short hedging")
elif short_value > 0 and long_value == 0:
st.warning("⚠️ **Portfolio is fully SHORT** - No long hedging")
# Display positions table with enhanced formatting
st.dataframe(
df,
use_container_width=True,
hide_index=True,
column_config={
"amount": st.column_config.NumberColumn(
"Amount",
format="%.6f",
help="Positive = Long, Negative = Short"
),
"entry_price": st.column_config.NumberColumn(
"Entry Price",
format="$%.4f"
),
"original_value": st.column_config.NumberColumn(
"Original Value",
format="$%.2f",
help="Amount × Entry Price"
),
"mark_price": st.column_config.NumberColumn(
"Mark Price",
format="$%.4f"
),
"unrealized_pnl": st.column_config.NumberColumn(
"Unrealized PnL",
format="$%.2f"
)
}
)
# Show separate long/short breakdown if there are both types
if not long_positions.empty and not short_positions.empty:
st.divider()
col1, col2 = st.columns(2)
with col1:
st.subheader("🟢 Long Positions")
if not long_positions.empty:
long_pnl = long_positions['unrealized_pnl'].sum() if 'unrealized_pnl' in long_positions.columns else 0
st.caption(f"PnL: ${long_pnl:,.2f}")
st.dataframe(
long_positions,
use_container_width=True,
hide_index=True,
column_config={
"amount": st.column_config.NumberColumn("Amount", format="%.6f"),
"entry_price": st.column_config.NumberColumn("Entry Price", format="$%.4f"),
"unrealized_pnl": st.column_config.NumberColumn("PnL", format="$%.2f")
}
)
with col2:
st.subheader("🔴 Short Positions")
if not short_positions.empty:
short_pnl = short_positions['unrealized_pnl'].sum() if 'unrealized_pnl' in short_positions.columns else 0
st.caption(f"PnL: ${short_pnl:,.2f}")
st.dataframe(
short_positions,
use_container_width=True,
hide_index=True,
column_config={
"amount": st.column_config.NumberColumn("Amount", format="%.6f"),
"entry_price": st.column_config.NumberColumn("Entry Price", format="$%.4f"),
"unrealized_pnl": st.column_config.NumberColumn("PnL", format="$%.2f")
}
)
elif not long_positions.empty:
st.info("📈 All positions are LONG")
elif not short_positions.empty:
st.info("📉 All positions are SHORT")
def render_orders_table(orders_data):
"""Render active orders table."""
if not orders_data:
st.info("No active orders found.")
return
# Convert to DataFrame
df = pd.DataFrame(orders_data)
if df.empty:
st.info("No active orders found.")
return
st.subheader("📋 Active Orders")
# Add cancel column to dataframe
df_with_cancel = df.copy()
df_with_cancel["cancel"] = False
# Create column configurations based on what's available in the data
column_config = {
"cancel": st.column_config.CheckboxColumn(
"Cancel",
help="Select orders to cancel",
default=False,
),
"price": st.column_config.NumberColumn(
"Price",
format="$%.4f"
),
"amount": st.column_config.NumberColumn(
"Amount",
format="%.6f"
),
"executed_amount_base": st.column_config.NumberColumn(
"Executed (Base)",
format="%.6f"
),
"executed_amount_quote": st.column_config.NumberColumn(
"Executed (Quote)",
format="%.6f"
),
"last_update_timestamp": st.column_config.DatetimeColumn(
"Last Update",
format="DD/MM/YYYY HH:mm:ss"
)
}
# Add cancel button functionality
edited_df = st.data_editor(
df_with_cancel,
column_config=column_config,
disabled=[col for col in df_with_cancel.columns if col != "cancel"],
hide_index=True,
use_container_width=True,
key="orders_editor"
)
# Handle order cancellation
if "cancel" in edited_df.columns:
selected_orders = edited_df[edited_df["cancel"]]
if not selected_orders.empty and st.button(f"❌ Cancel Selected ({len(selected_orders)}) Orders",
type="secondary"):
with st.spinner("Cancelling orders..."):
for _, order in selected_orders.iterrows():
cancel_order(
order.get("account_name", ""),
order.get("connector_name", ""),
order.get("client_order_id", "")
)
st.rerun()
# Page Header
st.title("💹 Trading Hub")
st.caption("Execute trades, monitor positions, and analyze markets")
# Get accounts and credentials
accounts_list, credentials_dict = get_accounts_and_credentials()
candles_connectors = get_candles_connectors()
# Account and Trading Selection Section - Reorganized
selection_col, market_data_col = st.columns([1, 3])
with selection_col:
st.subheader("🏦 Account & Market")
# All selection in one column
if accounts_list:
# Default to first account if not set
if st.session_state.selected_account is None:
st.session_state.selected_account = accounts_list[0]
selected_account = st.selectbox(
"📱 Account",
accounts_list,
index=accounts_list.index(
st.session_state.selected_account) if st.session_state.selected_account in accounts_list else 0,
key="account_selector"
)
st.session_state.selected_account = selected_account
else:
st.error("No accounts found")
st.stop()
if selected_account and credentials_dict.get(selected_account):
credentials = credentials_dict[selected_account]
# Handle different credential formats
if isinstance(credentials, list) and credentials:
# If credentials is a list of strings (connector names)
if isinstance(credentials[0], str):
# Convert string list to dict format
credentials = [{"connector_name": cred} for cred in credentials]
# If credentials is already a list of dicts, use as is
elif isinstance(credentials[0], dict):
credentials = credentials
elif isinstance(credentials, dict):
# If credentials is a dict, convert to list of dicts
credentials = [{"connector_name": k, **v} for k, v in credentials.items()]
else:
credentials = []
# For simplicity, just use the first credential available
default_cred = credentials[0] if credentials else None
if default_cred and credentials:
connector = st.selectbox(
"📡 Exchange",
[cred["connector_name"] for cred in credentials],
index=0,
key="connector_selector"
)
st.session_state.selected_connector = connector
else:
st.error("No credentials found for this account")
connector = None
else:
st.error("No credentials available")
connector = None
trading_pair = st.text_input(
"💱 Trading Pair",
value="BTC-USDT",
key="trading_pair_input"
)
# Update selected market
if connector and trading_pair:
st.session_state.selected_market = {"connector": connector, "trading_pair": trading_pair}
with market_data_col:
st.subheader("📊 Market Data")
# Only show metrics if we have a selected market
if st.session_state.selected_market.get("connector") and st.session_state.selected_market.get("trading_pair"):
# Get market data for metrics
connector = st.session_state.selected_market["connector"]
trading_pair = st.session_state.selected_market["trading_pair"]
interval = st.session_state.chart_interval
max_candles = st.session_state.max_candles
candles_connector = st.session_state.candles_connector
# Create sub-columns for organized display
price_col, depth_col, funding_col, controls_col = st.columns([1, 1, 1, 1])
with price_col:
candles, prices = get_market_data(
connector, trading_pair, interval, max_candles, candles_connector
)
# Get order book data for bid/ask prices and volumes
order_book = get_order_book(connector, trading_pair, depth=1000)
if order_book and "bids" in order_book and "asks" in order_book:
bid_price = float(order_book["bids"][0]["price"]) if order_book["bids"] else 0
ask_price = float(order_book["asks"][0]["price"]) if order_book["asks"] else 0
mid_price = (bid_price + ask_price) / 2 if bid_price > 0 and ask_price > 0 else 0
st.metric(f"💰 {trading_pair}", f"${mid_price:.4f}")
st.metric("📈 Bid Price", f"${bid_price:.4f}")
st.metric("📉 Ask Price", f"${ask_price:.4f}")
else:
# Fallback to current price if no order book
if prices and trading_pair in prices:
current_price = prices[trading_pair]
st.metric(
f"💰 {trading_pair}",
f"${float(current_price):,.4f}"
)
else:
st.metric(f"💰 {trading_pair}", "Loading...")
with depth_col:
# Order book depth configuration
depth_percentage = st.number_input(
"📊 Depth ±%",
min_value=0.1,
max_value=10.0,
value=1.0,
step=0.1,
format="%.1f",
key="depth_percentage"
)
# Calculate depth using the actual API method
if order_book and "bids" in order_book and "asks" in order_book:
bid_price = float(order_book["bids"][0]["price"]) if order_book["bids"] else 0
ask_price = float(order_book["asks"][0]["price"]) if order_book["asks"] else 0
if bid_price > 0 and ask_price > 0:
# Calculate prices at depth percentage
depth_factor = depth_percentage / 100
buy_price = bid_price * (1 - depth_factor) # Price below current bid
sell_price = ask_price * (1 + depth_factor) # Price above current ask
try:
# Get buy depth (volume available when buying up to sell_price - hitting asks)
buy_response = backend_api_client.market_data.get_quote_volume_for_price(
connector_name=connector,
trading_pair=trading_pair,
price=sell_price, # Use sell_price for buying (hitting asks above current price)
is_buy=True
)
# Get sell depth (volume available when selling down to buy_price - hitting bids)
sell_response = backend_api_client.market_data.get_quote_volume_for_price(
connector_name=connector,
trading_pair=trading_pair,
price=buy_price, # Use buy_price for selling (hitting bids below current price)
is_buy=False
)
# Handle response format based on your example
buy_vol = 0
sell_vol = 0
if isinstance(buy_response, dict) and "result_quote_volume" in buy_response:
buy_vol = buy_response["result_quote_volume"]
# Handle NaN values more robustly
import math
if buy_vol is None or (isinstance(buy_vol, float) and math.isnan(buy_vol)) or str(buy_vol).lower() == 'nan':
buy_vol = 0
if isinstance(sell_response, dict) and "result_quote_volume" in sell_response:
sell_vol = sell_response["result_quote_volume"]
# Handle NaN values more robustly
import math
if sell_vol is None or (isinstance(sell_vol, float) and math.isnan(sell_vol)) or str(sell_vol).lower() == 'nan':
sell_vol = 0
st.metric(
"📊 Buy Depth (USDT)",
f"${float(buy_vol):,.0f}" if buy_vol != 0 else "N/A",
help="Volume available when buying (hitting asks)"
)
st.metric(
"📊 Sell Depth (USDT)",
f"${float(sell_vol):,.0f}" if sell_vol != 0 else "N/A",
help="Volume available when selling (hitting bids)"
)
except Exception:
# Fallback to simple calculation if API fails
total_bid_volume = sum(float(bid["amount"] * bid["price"]) for bid in order_book["bids"])
total_ask_volume = sum(float(ask["amount"] * ask["price"]) for ask in order_book["asks"])
st.metric(
"📊 Buy Depth (USDT)",
f"${total_ask_volume:,.0f}",
help="Total ask volume (for buying)"
)
st.metric(
"📊 Sell Depth (USDT)",
f"${total_bid_volume:,.0f}",
help="Total bid volume (for selling)"
)
else:
st.metric(f"📊 Depth ±{depth_percentage:.1f}%", "No data")
else:
st.metric(f"📊 Depth ±{depth_percentage:.1f}%", "No order book")
with funding_col:
# Funding rate for perpetual contracts
if "perpetual" in connector.lower():
funding_data = get_funding_rate(connector, trading_pair)
if funding_data and "funding_rate" in funding_data:
funding_rate = float(funding_data["funding_rate"]) * 100
st.metric(
"💸 Funding Rate",
f"{funding_rate:.4f}%"
)
else:
st.metric("💸 Funding Rate", "N/A")
else:
st.metric("💸 Funding Rate", "Spot")
with controls_col:
# Show fetch time and refresh button together
if "last_fetch_time" in st.session_state:
fetch_time = st.session_state["last_fetch_time"]
st.caption(f"⚡ Fetch: {fetch_time:.0f}ms")
# Auto-refresh toggle
auto_refresh = st.toggle(
"🔄 Auto-refresh",
value=st.session_state.auto_refresh_enabled,
help=f"Refresh data every {REFRESH_INTERVAL} seconds"
)
st.session_state.auto_refresh_enabled = auto_refresh
# Refresh button
if st.button("🔄 Refresh Now", use_container_width=True, type="primary"):
st.session_state.last_refresh_time = time.time()
st.rerun()
else:
st.info("Select account and pair to view extended market data")
# Main trading data display function
def show_trading_data():
"""Display trading data with chart controls."""
connector = st.session_state.selected_market.get("connector")
trading_pair = st.session_state.selected_market.get("trading_pair")
if not connector or not trading_pair:
st.warning("Please select an account and trading pair")
return
# Chart and Trade Execution section
st.divider()
chart_col, orderbook_col, trade_col = st.columns([3, 1, 1])
# Get market data first (needed for both charts)
candles, prices = get_market_data(
connector, trading_pair, st.session_state.chart_interval,
st.session_state.max_candles, st.session_state.candles_connector
)
# Get order book data
order_book = get_order_book(connector, trading_pair, depth=20)
# Get current price and depth percentage
current_price = 0.0
if prices and trading_pair in prices:
current_price = float(prices[trading_pair])
depth_percentage = st.session_state.get("depth_percentage", 1.0)
with chart_col:
st.subheader("📈 Price Chart")
# Chart controls in the same fragment
controls_col1, controls_col2, controls_col3 = st.columns([1, 1, 1])
with controls_col1:
interval = st.selectbox(
"⏱️ Chart Interval",
["1m", "3m", "5m", "15m", "1h", "4h", "1d"],
index=0,
key="chart_interval_selector"
)
st.session_state.chart_interval = interval
with controls_col2:
candles_connectors = get_candles_connectors()
if candles_connectors:
# Add option to use same connector as trading
candles_options = ["Same as trading"] + candles_connectors
selected_candles = st.selectbox(
"📊 Candles Source",
candles_options,
index=0,
key="chart_candles_connector_selector",
help="Some exchanges don't provide candles. Select an alternative source."
)
st.session_state.candles_connector = None if selected_candles == "Same as trading" else selected_candles
else:
st.session_state.candles_connector = None
with controls_col3:
max_candles = st.number_input(
"📈 Max Candles",
min_value=50,
max_value=500,
value=100,
step=50,
key="chart_max_candles_input"
)
st.session_state.max_candles = max_candles
# Get trade history for the selected account/connector/pair
trades = []
if st.session_state.selected_account and st.session_state.selected_connector:
trades = get_trade_history(
st.session_state.selected_account,
st.session_state.selected_connector,
trading_pair
)
# Add small gap before chart
st.write("")
# Create candlestick chart
candles_source = st.session_state.candles_connector if st.session_state.candles_connector else connector
candlestick_fig = create_candlestick_chart(candles, candles_source, trading_pair, interval, trades)
with orderbook_col:
st.subheader("📊 Order Book")
# Create and display order book chart
orderbook_fig, price_min, price_max = create_order_book_chart(
order_book, current_price, depth_percentage, trading_pair
)
# Display both charts
with chart_col:
st.plotly_chart(candlestick_fig, use_container_width=True)
# Show last update time
current_time = datetime.datetime.now().strftime("%H:%M:%S")
st.caption(f"🔄 Last updated: {current_time} (auto-refresh every 30s)")
with orderbook_col:
st.plotly_chart(orderbook_fig, use_container_width=True)
with trade_col:
st.subheader("💸 Execute Trade")
if st.session_state.selected_account and st.session_state.selected_connector:
# Get current price for calculations
current_price = 0.0
if prices and trading_pair in prices:
current_price = float(prices[trading_pair])
# Extract base and quote tokens from trading pair
base_token, quote_token = trading_pair.split('-')
# Order type selection
order_type = st.selectbox(
"Order Type",
["market", "limit"],
key="trade_order_type"
)
# Side selection
side = st.selectbox(
"Side",
["buy", "sell"],
key="trade_side"
)
# Position mode selection
position_action = st.selectbox(
"Position Mode",
["OPEN", "CLOSE"],
index=0, # Default to OPEN
key="trade_position_action",
help="OPEN creates new positions, CLOSE reduces existing positions"
)
# Amount input
amount = st.number_input(
"Amount",
min_value=0.0,
value=0.001,
format="%.6f",
key="trade_amount"
)
# Base/Quote toggle switch
is_quote = st.toggle(
f"Amount in {quote_token}",
value=False,
help=f"Toggle to enter amount in {quote_token} instead of {base_token}",
key="trade_is_quote"
)
# Show conversion line
if current_price > 0 and amount > 0:
if is_quote:
# User entered quote amount, show base equivalent
base_equivalent = amount / current_price
st.caption(f"≈ {base_equivalent:.6f} {base_token}")
else:
# User entered base amount, show quote equivalent
quote_equivalent = amount * current_price
st.caption(f"≈ {quote_equivalent:.2f} {quote_token}")
# Price input for limit orders
if order_type == "limit":
# Check if order type changed or if user hasn't set a custom price
if (st.session_state.last_order_type != order_type or
not st.session_state.trade_price_set_by_user or
st.session_state.trade_custom_price is None):
# Only set default price when switching to limit or no custom price set
if current_price > 0:
st.session_state.trade_custom_price = current_price
else:
st.session_state.trade_custom_price = 0.0
st.session_state.trade_price_set_by_user = False
# Update last order type
st.session_state.last_order_type = order_type
price = st.number_input(
"Price",
min_value=0.0,
value=st.session_state.trade_custom_price,
format="%.4f",
key="trade_price",
on_change=lambda: setattr(st.session_state, 'trade_price_set_by_user', True)
)
# Update custom price when user changes it
if price != st.session_state.trade_custom_price:
st.session_state.trade_custom_price = price
st.session_state.trade_price_set_by_user = True
# Show updated conversion for limit orders
if price > 0 and amount > 0:
if is_quote:
base_equivalent = amount / price
st.caption(f"At limit price: ≈ {base_equivalent:.6f} {base_token}")
else:
quote_equivalent = amount * price
st.caption(f"At limit price: ≈ {quote_equivalent:.2f} {quote_token}")
else:
price = None
# Submit button
st.write("")
if st.button("🚀 Place Order", type="primary", use_container_width=True, key="place_order_btn"):
if amount > 0:
# Convert amount to base if needed
final_amount = amount
conversion_price = price if order_type == "limit" and price else current_price
if is_quote and conversion_price > 0:
# Convert quote amount to base amount
final_amount = amount / conversion_price
st.success(f"Converting {amount} {quote_token} to {final_amount:.6f} {base_token}")
order_data = {
"account_name": st.session_state.selected_account,
"connector_name": st.session_state.selected_connector,
"trading_pair": st.session_state.selected_market["trading_pair"],
"order_type": order_type.upper(),
"trade_type": side.upper(),
"amount": final_amount,
"position_action": position_action
}
if order_type == "limit" and price:
order_data["price"] = price
with st.spinner("Placing order..."):
place_order(order_data)
else:
st.error("Please enter a valid amount")
st.write("")
st.info(f"🎯 {st.session_state.selected_connector}\n{st.session_state.selected_market['trading_pair']}")
else:
st.warning("Please select an account and exchange to execute trades")
# Data tables section
st.divider()
# Get positions, orders, and history
positions = get_positions()
orders = get_active_orders()
order_history = get_order_history()
# Display in tabs - Balances first
tab1, tab2, tab3, tab4 = st.tabs(["💰 Balances", "📊 Positions", "📋 Active Orders", "📜 Order History"])
with tab1:
render_balances_table()
with tab2:
render_positions_table(positions)
with tab3:
render_orders_table(orders)
with tab4:
render_order_history_table(order_history)
def render_order_history_table(order_history):
"""Render order history table."""
if not order_history:
st.info("No order history found.")
return
# Convert to DataFrame
df = pd.DataFrame(order_history)
if df.empty:
st.info("No order history found.")
return
st.subheader("📜 Order History")
st.dataframe(
df,
use_container_width=True,
hide_index=True,
column_config={
"price": st.column_config.NumberColumn(
"Price",
format="$%.4f"
),
"amount": st.column_config.NumberColumn(
"Amount",
format="%.6f"
),
"timestamp": st.column_config.DatetimeColumn(
"Time",
format="DD/MM/YYYY HH:mm:ss"
)
}
)
def get_balances():
"""Get account balances."""
try:
if not st.session_state.selected_account:
return []
# Get portfolio state for the selected account
portfolio_state = backend_api_client.portfolio.get_state(
account_names=[st.session_state.selected_account]
)
# Extract balances
balances = []
if st.session_state.selected_account in portfolio_state:
for exchange, tokens in portfolio_state[st.session_state.selected_account].items():
for token_info in tokens:
balances.append({
"exchange": exchange,
"token": token_info["token"],
"total": token_info["units"],
"available": token_info["available_units"],
"price": token_info["price"],
"value": token_info["value"]
})
return balances
except Exception as e:
st.error(f"Failed to fetch balances: {e}")
return []
def render_balances_table():
"""Render balances table."""
balances = get_balances()
if not balances:
st.info("No balances found.")
return
# Convert to DataFrame
df = pd.DataFrame(balances)
if df.empty:
st.info("No balances found.")
return
st.subheader(f"💰 Account Balances - {st.session_state.selected_account}")
# Calculate total value
total_value = df['value'].sum()
st.metric("Total Portfolio Value", f"${total_value:,.2f}")
st.dataframe(
df,
use_container_width=True,
hide_index=True,
column_config={
"total": st.column_config.NumberColumn(
"Total Balance",
format="%.6f"
),
"available": st.column_config.NumberColumn(
"Available",
format="%.6f"
),
"price": st.column_config.NumberColumn(
"Price",
format="$%.4f"
),
"value": st.column_config.NumberColumn(
"Value (USD)",
format="$%.2f"
)
}
)
# Auto-refresh logic - only if user is not actively trading
if st.session_state.auto_refresh_enabled and not st.session_state.trade_price_set_by_user:
# Check if it's time to refresh
current_time = time.time()
time_since_last_refresh = current_time - st.session_state.last_refresh_time
if time_since_last_refresh >= REFRESH_INTERVAL:
# Update last refresh time and rerun
st.session_state.last_refresh_time = current_time
time.sleep(0.1) # Small delay to prevent rapid refreshes
st.rerun()
# Display trading data
show_trading_data()