Files
deploy/pages/orchestration/trading/app.py
2025-07-11 02:57:57 +03:00

1501 lines
58 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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}<br>%{x}<extra></extra>'
),
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='<b>BUY</b><br>Price: $%{y:.4f}<br>Time: %{x}<extra></extra>'
),
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='<b>SELL</b><br>Price: $%{y:.4f}<br>Time: %{x}<extra></extra>'
),
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='<b>BID</b><br>Price: $%{y:.4f}<br>Cumulative Volume: $%{x:,.0f}<br>Level Volume: $%{customdata:,.0f}<extra></extra>',
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='<b>ASK</b><br>Price: $%{y:.4f}<br>Cumulative Volume: $%{x:,.0f}<br>Level Volume: $%{customdata:,.0f}<extra></extra>',
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()