(feat) update pages

This commit is contained in:
cardosofede
2025-07-11 02:57:57 +03:00
parent 91cd3d0365
commit e44fa89283
39 changed files with 6036 additions and 780 deletions

View File

@@ -0,0 +1,120 @@
# Archived Bots
## Overview
The Archived Bots page provides comprehensive access to historical bot database files, enabling users to analyze past trading performance, review strategies, and extract insights from archived bot data.
## Key Features
### Database Management
- **Database Discovery**: Automatically lists all available database files in the system
- **Database Status**: Shows connection status and basic information for each database
- **Database Summary**: Provides overview statistics and metadata for each database
### Historical Data Analysis
- **Performance Metrics**: Detailed trade-based performance analysis including PnL, win/loss ratios, and key statistics
- **Trade History**: Complete record of all trades with filtering and pagination
- **Order History**: Comprehensive order book data with status filtering
- **Position Tracking**: Historical position data with timeline analysis
### Strategy Insights
- **Executor Analysis**: Review strategy executor performance and configuration
- **Controller Data**: Access to controller configurations and their historical performance
- **Strategy Comparison**: Compare different strategy implementations and their results
### Data Export & Visualization
- **Export Functionality**: Download historical data in various formats (CSV, JSON)
- **Performance Charts**: Interactive visualizations of trading performance over time
- **Comparative Analysis**: Side-by-side comparison of different archived strategies
## Usage Instructions
### 1. Database Selection
- View the list of available archived databases
- Select a database to explore its contents
- Check database status and connection health
### 2. Performance Analysis
- Navigate to the Performance tab to view trading metrics
- Review key performance indicators (KPIs)
- Analyze profit/loss trends and trading patterns
### 3. Historical Data Review
- Browse trade history with pagination controls
- Filter orders by status, date range, or trading pair
- Review position data and timeline
### 4. Strategy Analysis
- Examine executor configurations and performance
- Review controller settings and their impact
- Compare different strategy implementations
### 5. Data Export
- Select desired data range and format
- Export historical data for external analysis
- Download performance reports
## Technical Implementation
### Architecture
- **Async API Integration**: Uses nest_asyncio for async database operations
- **Database Connections**: Manages multiple database connections efficiently
- **Pagination**: Implements efficient pagination for large datasets
- **Error Handling**: Comprehensive error handling for database operations
### Components
- **Database Browser**: Interactive database selection and status display
- **Performance Dashboard**: Real-time performance metrics and charts
- **Data Grid**: Efficient display of large datasets with filtering
- **Export Manager**: Handles data export in multiple formats
### State Management
- **Database Selection**: Tracks currently selected database
- **Filter States**: Maintains filter settings across page navigation
- **Pagination State**: Manages pagination across different data views
- **Export Settings**: Remembers export preferences
### API Integration
- **ArchivedBotsRouter**: Async router for database operations
- **Batch Operations**: Efficient bulk data retrieval
- **Connection Pooling**: Manages database connections efficiently
- **Error Recovery**: Automatic retry mechanisms for failed operations
## Best Practices
### Performance Optimization
- Use pagination for large datasets
- Implement efficient filtering on the backend
- Cache frequently accessed data
- Use async operations for database queries
### User Experience
- Provide clear status indicators
- Show loading states for long operations
- Implement progressive data loading
- Offer keyboard shortcuts for navigation
### Data Integrity
- Validate database connections before operations
- Handle missing or corrupted data gracefully
- Provide clear error messages
- Implement data consistency checks
## File Structure
```
archived_bots/
├── __init__.py
├── README.md
├── app.py # Main application file
├── utils.py # Utility functions
└── components/ # Page-specific components
├── database_browser.py
├── performance_dashboard.py
├── data_grid.py
└── export_manager.py
```
## Dependencies
- **Backend**: ArchivedBotsRouter from hummingbot-api-client
- **Frontend**: Streamlit components, plotly for visualization
- **Utils**: nest_asyncio for async operations, pandas for data manipulation
- **Components**: Custom styling components for consistent UI

View File

@@ -0,0 +1 @@
# Archived Bots Page Module

File diff suppressed because it is too large Load Diff

View File

@@ -1,7 +1,10 @@
import nest_asyncio
import streamlit as st
from frontend.st_utils import get_backend_api_client, initialize_st_page
nest_asyncio.apply()
initialize_st_page(title="Credentials", icon="🔑")
# Page content
@@ -9,102 +12,188 @@ client = get_backend_api_client()
NUM_COLUMNS = 4
@st.cache_data
def get_all_connectors_config_map():
return client.get_all_connectors_config_map()
# Get fresh client instance inside cached function
connectors = client.connectors.list_connectors()
config_map_dict = {}
for connector_name in connectors:
try:
config_map = client.connectors.get_config_map(connector_name=connector_name)
config_map_dict[connector_name] = config_map
except Exception as e:
st.warning(f"Could not get config map for {connector_name}: {e}")
config_map_dict[connector_name] = []
return config_map_dict
# Section to display available accounts and credentials
accounts = client.get_accounts()
all_connector_config_map = get_all_connectors_config_map()
st.header("Available Accounts and Credentials")
if accounts:
n_accounts = len(accounts)
accounts.remove("master_account")
accounts.insert(0, "master_account")
for i in range(0, n_accounts, NUM_COLUMNS):
cols = st.columns(NUM_COLUMNS)
for j, account in enumerate(accounts[i:i + NUM_COLUMNS]):
with cols[j]:
st.subheader(f"🏦 {account}")
credentials = client.get_credentials(account)
st.json(credentials)
else:
st.write("No accounts available.")
@st.fragment
def accounts_section():
# Get fresh accounts list
accounts = client.accounts.list_accounts()
if accounts:
n_accounts = len(accounts)
# Ensure master_account is first, but handle if it doesn't exist
if "master_account" in accounts:
accounts.remove("master_account")
accounts.insert(0, "master_account")
for i in range(0, n_accounts, NUM_COLUMNS):
cols = st.columns(NUM_COLUMNS)
for j, account in enumerate(accounts[i:i + NUM_COLUMNS]):
with cols[j]:
st.subheader(f"🏦 {account}")
credentials = client.accounts.list_account_credentials(account)
st.json(credentials)
else:
st.write("No accounts available.")
st.markdown("---")
# Account management actions
c1, c2, c3 = st.columns([1, 1, 1])
with c1:
# Section to create a new account
st.header("Create a New Account")
new_account_name = st.text_input("New Account Name")
if st.button("Create Account"):
new_account_name = new_account_name.replace(" ", "_")
if new_account_name:
if new_account_name in accounts:
st.warning(f"Account {new_account_name} already exists.")
st.stop()
elif new_account_name == "" or all(char == "_" for char in new_account_name):
st.warning("Please enter a valid account name.")
st.stop()
response = client.accounts.add_account(new_account_name)
st.write(response)
try:
st.rerun(scope="fragment")
except Exception:
st.rerun()
else:
st.write("Please enter an account name.")
with c2:
# Section to delete an existing account
st.header("Delete an Account")
delete_account_name = st.selectbox("Select Account to Delete",
options=accounts if accounts else ["No accounts available"], )
if st.button("Delete Account"):
if delete_account_name and delete_account_name != "No accounts available":
response = client.accounts.delete_account(delete_account_name)
st.warning(response)
try:
st.rerun(scope="fragment")
except Exception:
st.rerun()
else:
st.write("Please select a valid account.")
with c3:
# Section to delete a credential from an existing account
st.header("Delete Credential")
delete_account_cred_name = st.selectbox("Select the credentials account",
options=accounts if accounts else ["No accounts available"], )
credentials_data = client.accounts.list_account_credentials(delete_account_cred_name)
# Handle different possible return formats
if isinstance(credentials_data, list):
# If it's a list of strings in format "connector.key"
if credentials_data and isinstance(credentials_data[0], str):
creds_for_account = [credential.split(".")[0] for credential in credentials_data]
# If it's a list of dicts, extract connector names
elif credentials_data and isinstance(credentials_data[0], dict):
creds_for_account = list(
set([cred.get('connector', cred.get('connector_name', '')) for cred in credentials_data if
cred.get('connector') or cred.get('connector_name')]))
else:
creds_for_account = []
elif isinstance(credentials_data, dict):
# If it's a dict with connectors as keys
creds_for_account = list(credentials_data.keys())
else:
creds_for_account = []
delete_cred_name = st.selectbox("Select a Credential to Delete",
options=creds_for_account if creds_for_account else [
"No credentials available"])
if st.button("Delete Credential"):
if (delete_account_cred_name and delete_account_cred_name != "No accounts available") and \
(delete_cred_name and delete_cred_name != "No credentials available"):
response = client.accounts.delete_credential(delete_account_cred_name, delete_cred_name)
st.warning(response)
try:
st.rerun(scope="fragment")
except Exception:
st.rerun()
else:
st.write("Please select a valid account.")
return accounts
accounts = accounts_section()
st.markdown("---")
c1, c2, c3 = st.columns([1, 1, 1])
with c1:
# Section to create a new account
st.header("Create a New Account")
new_account_name = st.text_input("New Account Name")
if st.button("Create Account"):
new_account_name = new_account_name.replace(" ", "_")
if new_account_name:
if new_account_name in accounts:
st.warning(f"Account {new_account_name} already exists.")
st.stop()
elif new_account_name == "" or all(char == "_" for char in new_account_name):
st.warning("Please enter a valid account name.")
st.stop()
response = client.add_account(new_account_name)
st.write(response)
else:
st.write("Please enter an account name.")
with c2:
# Section to delete an existing account
st.header("Delete an Account")
delete_account_name = st.selectbox("Select Account to Delete",
options=accounts if accounts else ["No accounts available"], )
if st.button("Delete Account"):
if delete_account_name and delete_account_name != "No accounts available":
response = client.delete_account(delete_account_name)
st.warning(response)
else:
st.write("Please select a valid account.")
with c3:
# Section to delete a credential from an existing account
st.header("Delete Credential")
delete_account_cred_name = st.selectbox("Select the credentials account",
options=accounts if accounts else ["No accounts available"], )
creds_for_account = [credential.split(".")[0] for credential in client.get_credentials(delete_account_cred_name)]
delete_cred_name = st.selectbox("Select a Credential to Delete",
options=creds_for_account if creds_for_account else ["No credentials available"])
if st.button("Delete Credential"):
if (delete_account_cred_name and delete_account_cred_name != "No accounts available") and \
(delete_cred_name and delete_cred_name != "No credentials available"):
response = client.delete_credential(delete_account_cred_name, delete_cred_name)
st.warning(response)
else:
st.write("Please select a valid account.")
st.markdown("---")
# Section to add credentials
st.header("Add Credentials")
c1, c2 = st.columns([1, 1])
with c1:
account_name = st.selectbox("Select Account", options=accounts if accounts else ["No accounts available"])
with c2:
all_connectors = list(all_connector_config_map.keys())
binance_perpetual_index = all_connectors.index(
"binance_perpetual") if "binance_perpetual" in all_connectors else None
connector_name = st.selectbox("Select Connector", options=all_connectors, index=binance_perpetual_index)
config_map = all_connector_config_map[connector_name]
@st.fragment
def add_credentials_section():
st.header("Add Credentials")
c1, c2 = st.columns([1, 1])
with c1:
account_name = st.selectbox("Select Account", options=accounts if accounts else ["No accounts available"])
with c2:
all_connectors = list(all_connector_config_map.keys())
binance_perpetual_index = all_connectors.index(
"binance_perpetual") if "binance_perpetual" in all_connectors else None
connector_name = st.selectbox("Select Connector", options=all_connectors, index=binance_perpetual_index)
config_map = all_connector_config_map.get(connector_name, [])
st.write(f"Configuration Map for {connector_name}:")
config_inputs = {}
cols = st.columns(NUM_COLUMNS)
for i, config in enumerate(config_map):
with cols[i % (NUM_COLUMNS - 1)]:
config_inputs[config] = st.text_input(config, type="password", key=f"{connector_name}_{config}")
st.write(f"Configuration Map for {connector_name}:")
config_inputs = {}
with cols[-1]:
if st.button("Submit Credentials"):
response = client.add_connector_keys(account_name, connector_name, config_inputs)
if response:
st.success(response)
# Custom logic for XRPL connector
if connector_name == "xrpl":
# Define custom XRPL fields with default values
xrpl_fields = {
"xrpl_secret_key": "",
"wss_node_urls": "wss://xrplcluster.com,wss://s1.ripple.com,wss://s2.ripple.com",
}
# Display XRPL-specific fields
for field, default_value in xrpl_fields.items():
if field == "xrpl_secret_key":
config_inputs[field] = st.text_input(field, type="password", key=f"{connector_name}_{field}")
else:
config_inputs[field] = st.text_input(field, value=default_value, key=f"{connector_name}_{field}")
if st.button("Submit Credentials"):
response = client.accounts.add_credential(account_name, connector_name, config_inputs)
if response:
st.success(response)
try:
st.rerun(scope="fragment")
except Exception:
st.rerun()
else:
# Default behavior for other connectors
cols = st.columns(NUM_COLUMNS)
for i, config in enumerate(config_map):
with cols[i % (NUM_COLUMNS - 1)]:
config_inputs[config] = st.text_input(config, type="password", key=f"{connector_name}_{config}")
with cols[-1]:
if st.button("Submit Credentials"):
response = client.accounts.add_credential(account_name, connector_name, config_inputs)
if response:
st.success(response)
try:
st.rerun(scope="fragment")
except Exception:
st.rerun()
add_credentials_section()

View File

@@ -1,19 +1,137 @@
### Description
# Bot Instances Management
This page helps you deploy and manage Hummingbot instances:
The Bot Instances page provides centralized control for deploying, managing, and monitoring Hummingbot trading bot instances across your infrastructure.
- Starting and stopping Hummingbot Broker
- Creating, starting and stopping bot instances
- Managing strategy and script files that instances run
- Fetching status of running instances
## Features
### Maintainers
### 🤖 Instance Management
- **Create Bot Instances**: Deploy new Hummingbot instances with custom configurations
- **Start/Stop Control**: Manage instance lifecycle with one-click controls
- **Status Monitoring**: Real-time health checks and status updates
- **Multi-Instance Support**: Manage multiple bots running different strategies simultaneously
This page is maintained by Hummingbot Foundation as a template other pages:
### 📁 Configuration Management
- **Strategy File Upload**: Deploy strategy Python files to instances
- **Script Management**: Upload and manage custom scripts
- **Configuration Templates**: Save and reuse bot configurations
- **Hot Reload**: Update strategies without restarting instances
* [cardosfede](https://github.com/cardosfede)
* [fengtality](https://github.com/fengtality)
### 🔧 Broker Management
- **Hummingbot Broker**: Start and stop the broker service
- **Connection Status**: Monitor broker health and connectivity
- **Resource Usage**: Track CPU and memory consumption
- **Log Access**: View broker logs for debugging
### Wiki
### 📊 Instance Monitoring
- **Performance Metrics**: Real-time P&L, trade count, and volume
- **Active Orders**: View open orders across all instances
- **Error Tracking**: Centralized error logs and alerts
- **Resource Monitoring**: CPU, memory, and network usage per instance
See the [wiki](https://github.com/hummingbot/dashboard/wiki/%F0%9F%90%99-Bot-Orchestration) for more information.
## Usage Instructions
### 1. Start Hummingbot Broker
- Click "Start Broker" to initialize the Hummingbot broker service
- Wait for the broker to reach "Running" status
- Verify connection by checking the status indicator
### 2. Create Bot Instance
- Click "Create New Instance" button
- Configure instance settings:
- **Instance Name**: Unique identifier for the bot
- **Image**: Select Hummingbot version/image
- **Strategy**: Choose strategy file to run
- **Credentials**: Select API keys to use
- Click "Create" to deploy the instance
### 3. Manage Strategies
- **Upload Strategy**: Use the file uploader to add new strategy files
- **Select Active Strategy**: Choose which strategy the instance should run
- **Edit Strategy**: Modify strategy parameters through the editor
- **Version Control**: Track strategy changes and rollback if needed
### 4. Control Instances
- **Start**: Launch a stopped instance
- **Stop**: Gracefully shutdown a running instance
- **Restart**: Stop and start an instance
- **Delete**: Remove an instance and its configuration
### 5. Monitor Performance
- View real-time status in the instances table
- Click on an instance for detailed metrics
- Access logs for troubleshooting
- Export performance data for analysis
## Technical Notes
### Architecture
- **Docker-based**: Each instance runs in an isolated Docker container
- **RESTful API**: Communication via Backend API Client
- **WebSocket Updates**: Real-time status updates
- **Persistent Storage**: Configurations and logs stored on disk
### Instance Lifecycle
1. **Created**: Instance configured but not running
2. **Starting**: Docker container launching
3. **Running**: Bot actively trading
4. **Stopping**: Graceful shutdown in progress
5. **Stopped**: Instance halted but configuration preserved
6. **Error**: Instance encountered fatal error
### Resource Management
- **CPU Limits**: Configurable CPU allocation per instance
- **Memory Limits**: Set maximum memory usage
- **Network Isolation**: Instances communicate only through broker
- **Storage Quotas**: Limit log and data storage per instance
## Component Structure
```
instances/
├── app.py # Main instances management page
├── components/
│ ├── instance_table.py # Instance list and status display
│ ├── instance_controls.py # Start/stop/delete controls
│ ├── broker_panel.py # Broker management interface
│ └── strategy_uploader.py # Strategy file management
└── utils/
├── docker_manager.py # Docker container operations
├── instance_monitor.py # Status polling and updates
└── resource_tracker.py # Resource usage monitoring
```
## Best Practices
### Instance Naming
- Use descriptive names (e.g., "btc_market_maker_01")
- Include strategy type in the name
- Add exchange identifier if running multiple exchanges
- Use consistent naming conventions
### Strategy Management
- Test strategies in paper trading first
- Keep backups of working configurations
- Document strategy parameters
- Use version control for strategy files
### Performance Optimization
- Limit instances per broker (recommended: 5-10)
- Monitor resource usage regularly
- Restart instances weekly for stability
- Clear old logs to save disk space
## Error Handling
The instances page handles various error scenarios:
- **Broker Connection Lost**: Automatic reconnection attempts
- **Instance Crashes**: Auto-restart with configurable retry limits
- **Resource Exhaustion**: Graceful degradation and alerts
- **Strategy Errors**: Detailed error logs and stack traces
- **Network Issues**: Offline mode with cached status
## Security Considerations
- **API Key Isolation**: Each instance has access only to assigned credentials
- **Network Segmentation**: Instances cannot communicate directly
- **Resource Limits**: Prevent runaway processes from affecting system
- **Audit Logging**: All actions are logged for compliance

View File

@@ -1,76 +1,384 @@
import time
from types import SimpleNamespace
import pandas as pd
import streamlit as st
from streamlit_elements import elements, mui
from frontend.components.bot_performance_card import BotPerformanceCardV2
from frontend.components.dashboard import Dashboard
from frontend.st_utils import get_backend_api_client, initialize_st_page
# Constants for UI layout
CARD_WIDTH = 12
CARD_HEIGHT = 4
NUM_CARD_COLS = 1
initialize_st_page(icon="🦅", show_readme=False)
# Initialize backend client
backend_api_client = get_backend_api_client()
# Initialize session state for auto-refresh
if "auto_refresh_enabled" not in st.session_state:
st.session_state.auto_refresh_enabled = True
# Set refresh interval
REFRESH_INTERVAL = 10 # seconds
def get_grid_positions(n_cards: int, cols: int = NUM_CARD_COLS, card_width: int = CARD_WIDTH, card_height: int = CARD_HEIGHT):
rows = n_cards // cols + 1
x_y = [(x * card_width, y * card_height) for x in range(cols) for y in range(rows)]
return sorted(x_y, key=lambda x: (x[1], x[0]))
def stop_bot(bot_name):
"""Stop a running bot."""
try:
backend_api_client.bot_orchestration.stop_and_archive_bot(bot_name)
st.success(f"Bot {bot_name} stopped and archived successfully")
time.sleep(2) # Give time for the backend to process
except Exception as e:
st.error(f"Failed to stop bot {bot_name}: {e}")
def update_active_bots(api_client):
active_bots_response = api_client.get_active_bots_status()
if active_bots_response.get("status") == "success":
current_active_bots = active_bots_response.get("data")
stored_bots = {card[1]: card for card in st.session_state.active_instances_board.bot_cards}
new_bots = set(current_active_bots.keys()) - set(stored_bots.keys())
removed_bots = set(stored_bots.keys()) - set(current_active_bots.keys())
for bot in removed_bots:
st.session_state.active_instances_board.bot_cards = [card for card in
st.session_state.active_instances_board.bot_cards
if card[1] != bot]
positions = get_grid_positions(len(current_active_bots), NUM_CARD_COLS, CARD_WIDTH, CARD_HEIGHT)
for bot, (x, y) in zip(new_bots, positions[:len(new_bots)]):
card = BotPerformanceCardV2(st.session_state.active_instances_board.dashboard, x, y, CARD_WIDTH, CARD_HEIGHT)
st.session_state.active_instances_board.bot_cards.append((card, bot))
def archive_bot(bot_name):
"""Archive a stopped bot."""
try:
backend_api_client.docker.stop_container(bot_name)
backend_api_client.docker.remove_container(bot_name)
st.success(f"Bot {bot_name} archived successfully")
time.sleep(1)
except Exception as e:
st.error(f"Failed to archive bot {bot_name}: {e}")
initialize_st_page(title="Instances", icon="🦅")
api_client = get_backend_api_client()
def stop_controllers(bot_name, controllers):
"""Stop selected controllers."""
success_count = 0
for controller in controllers:
try:
backend_api_client.controllers.update_bot_controller_config(
bot_name,
controller,
{"manual_kill_switch": True}
)
success_count += 1
except Exception as e:
st.error(f"Failed to stop controller {controller}: {e}")
if not api_client.is_docker_running():
st.warning("Docker is not running. Please start Docker and refresh the page.")
st.stop()
if success_count > 0:
st.success(f"Successfully stopped {success_count} controller(s)")
# Temporarily disable auto-refresh to prevent immediate state reset
st.session_state.auto_refresh_enabled = False
if "active_instances_board" not in st.session_state:
active_bots_response = api_client.get_active_bots_status()
bot_cards = []
board = Dashboard()
st.session_state.active_instances_board = SimpleNamespace(
dashboard=board,
bot_cards=bot_cards,
)
active_bots = active_bots_response.get("data")
number_of_bots = len(active_bots)
if number_of_bots > 0:
positions = get_grid_positions(number_of_bots, NUM_CARD_COLS, CARD_WIDTH, CARD_HEIGHT)
for (bot, bot_info), (x, y) in zip(active_bots.items(), positions):
bot_status = api_client.get_bot_status(bot)
card = BotPerformanceCardV2(board, x, y, CARD_WIDTH, CARD_HEIGHT)
st.session_state.active_instances_board.bot_cards.append((card, bot))
else:
update_active_bots(api_client)
return success_count > 0
with elements("active_instances_board"):
with mui.Paper(sx={"padding": "2rem"}, variant="outlined"):
mui.Typography("🏠 Local Instances", variant="h5")
for card, bot in st.session_state.active_instances_board.bot_cards:
with st.session_state.active_instances_board.dashboard():
card(bot)
while True:
time.sleep(10)
st.rerun()
def start_controllers(bot_name, controllers):
"""Start selected controllers."""
success_count = 0
for controller in controllers:
try:
backend_api_client.controllers.update_bot_controller_config(
bot_name,
controller,
{"manual_kill_switch": False}
)
success_count += 1
except Exception as e:
st.error(f"Failed to start controller {controller}: {e}")
if success_count > 0:
st.success(f"Successfully started {success_count} controller(s)")
# Temporarily disable auto-refresh to prevent immediate state reset
st.session_state.auto_refresh_enabled = False
return success_count > 0
def render_bot_card(bot_name):
"""Render a bot performance card using native Streamlit components."""
try:
# Get bot status first
bot_status = backend_api_client.bot_orchestration.get_bot_status(bot_name)
# Only try to get controller configs if bot exists and is running
controller_configs = []
if bot_status.get("status") == "success":
bot_data = bot_status.get("data", {})
is_running = bot_data.get("status") == "running"
if is_running:
try:
controller_configs = backend_api_client.controllers.get_bot_controller_configs(bot_name)
controller_configs = controller_configs if controller_configs else []
except Exception as e:
# If controller configs fail, continue without them
st.warning(f"Could not fetch controller configs for {bot_name}: {e}")
controller_configs = []
with st.container(border=True):
if bot_status.get("status") == "error":
# Error state
col1, col2 = st.columns([3, 1])
with col1:
st.error(f"🤖 **{bot_name}** - Not Available")
st.error(f"An error occurred while fetching bot status of {bot_name}. Please check the bot client.")
else:
bot_data = bot_status.get("data", {})
is_running = bot_data.get("status") == "running"
performance = bot_data.get("performance", {})
error_logs = bot_data.get("error_logs", [])
general_logs = bot_data.get("general_logs", [])
# Bot header
col1, col2, col3 = st.columns([2, 1, 1])
with col1:
if is_running:
st.success(f"🤖 **{bot_name}** - Running")
else:
st.warning(f"🤖 **{bot_name}** - Stopped")
with col3:
if is_running:
if st.button("⏹️ Stop", key=f"stop_{bot_name}", use_container_width=True):
stop_bot(bot_name)
else:
if st.button("📦 Archive", key=f"archive_{bot_name}", use_container_width=True):
archive_bot(bot_name)
if is_running:
# Calculate totals
active_controllers = []
stopped_controllers = []
error_controllers = []
total_global_pnl_quote = 0
total_volume_traded = 0
total_unrealized_pnl_quote = 0
for controller, inner_dict in performance.items():
controller_status = inner_dict.get("status")
if controller_status == "error":
error_controllers.append({
"Controller": controller,
"Error": inner_dict.get("error", "Unknown error")
})
continue
controller_performance = inner_dict.get("performance", {})
controller_config = next(
(config for config in controller_configs if config.get("id") == controller), {}
)
controller_name = controller_config.get("controller_name", controller)
connector_name = controller_config.get("connector_name", "N/A")
trading_pair = controller_config.get("trading_pair", "N/A")
kill_switch_status = controller_config.get("manual_kill_switch", False)
realized_pnl_quote = controller_performance.get("realized_pnl_quote", 0)
unrealized_pnl_quote = controller_performance.get("unrealized_pnl_quote", 0)
global_pnl_quote = controller_performance.get("global_pnl_quote", 0)
volume_traded = controller_performance.get("volume_traded", 0)
close_types = controller_performance.get("close_type_counts", {})
tp = close_types.get("CloseType.TAKE_PROFIT", 0)
sl = close_types.get("CloseType.STOP_LOSS", 0)
time_limit = close_types.get("CloseType.TIME_LIMIT", 0)
ts = close_types.get("CloseType.TRAILING_STOP", 0)
refreshed = close_types.get("CloseType.EARLY_STOP", 0)
failed = close_types.get("CloseType.FAILED", 0)
close_types_str = f"TP: {tp} | SL: {sl} | TS: {ts} | TL: {time_limit} | ES: {refreshed} | F: {failed}"
controller_info = {
"Select": False,
"ID": controller_config.get("id"),
"Controller": controller_name,
"Connector": connector_name,
"Trading Pair": trading_pair,
"Realized PNL ($)": round(realized_pnl_quote, 2),
"Unrealized PNL ($)": round(unrealized_pnl_quote, 2),
"NET PNL ($)": round(global_pnl_quote, 2),
"Volume ($)": round(volume_traded, 2),
"Close Types": close_types_str,
"_controller_id": controller
}
if kill_switch_status:
stopped_controllers.append(controller_info)
else:
active_controllers.append(controller_info)
total_global_pnl_quote += global_pnl_quote
total_volume_traded += volume_traded
total_unrealized_pnl_quote += unrealized_pnl_quote
total_global_pnl_pct = total_global_pnl_quote / total_volume_traded if total_volume_traded > 0 else 0
# Display metrics
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("🏦 NET PNL", f"${total_global_pnl_quote:.2f}")
with col2:
st.metric("💹 Unrealized PNL", f"${total_unrealized_pnl_quote:.2f}")
with col3:
st.metric("📊 NET PNL (%)", f"{total_global_pnl_pct:.2%}")
with col4:
st.metric("💸 Volume Traded", f"${total_volume_traded:.2f}")
# Active Controllers
if active_controllers:
st.success("🚀 **Active Controllers:** Controllers currently running and trading")
active_df = pd.DataFrame(active_controllers)
edited_active_df = st.data_editor(
active_df,
column_config={
"Select": st.column_config.CheckboxColumn(
"Select",
help="Select controllers to stop",
default=False,
),
"_controller_id": None, # Hide this column
},
disabled=[col for col in active_df.columns if col != "Select"],
hide_index=True,
use_container_width=True,
key=f"active_table_{bot_name}"
)
selected_active = [
row["_controller_id"]
for _, row in edited_active_df.iterrows()
if row["Select"]
]
if selected_active:
if st.button(f"⏹️ Stop Selected ({len(selected_active)})",
key=f"stop_active_{bot_name}",
type="secondary"):
with st.spinner(f"Stopping {len(selected_active)} controller(s)..."):
stop_controllers(bot_name, selected_active)
time.sleep(1)
# Stopped Controllers
if stopped_controllers:
st.warning("💤 **Stopped Controllers:** Controllers that are paused or stopped")
stopped_df = pd.DataFrame(stopped_controllers)
edited_stopped_df = st.data_editor(
stopped_df,
column_config={
"Select": st.column_config.CheckboxColumn(
"Select",
help="Select controllers to start",
default=False,
),
"_controller_id": None, # Hide this column
},
disabled=[col for col in stopped_df.columns if col != "Select"],
hide_index=True,
use_container_width=True,
key=f"stopped_table_{bot_name}"
)
selected_stopped = [
row["_controller_id"]
for _, row in edited_stopped_df.iterrows()
if row["Select"]
]
if selected_stopped:
if st.button(f"▶️ Start Selected ({len(selected_stopped)})",
key=f"start_stopped_{bot_name}",
type="primary"):
with st.spinner(f"Starting {len(selected_stopped)} controller(s)..."):
start_controllers(bot_name, selected_stopped)
time.sleep(1)
# Error Controllers
if error_controllers:
st.error("💀 **Controllers with Errors:** Controllers that encountered errors")
error_df = pd.DataFrame(error_controllers)
st.dataframe(error_df, use_container_width=True, hide_index=True)
# Logs sections
with st.expander("📋 Error Logs"):
if error_logs:
for log in error_logs[:50]:
timestamp = log.get("timestamp", "")
message = log.get("msg", "")
logger_name = log.get("logger_name", "")
st.text(f"{timestamp} - {logger_name}: {message}")
else:
st.info("No error logs available.")
with st.expander("📝 General Logs"):
if general_logs:
for log in general_logs[:50]:
timestamp = pd.to_datetime(int(log.get("timestamp", 0)), unit="s")
message = log.get("msg", "")
logger_name = log.get("logger_name", "")
st.text(f"{timestamp} - {logger_name}: {message}")
else:
st.info("No general logs available.")
except Exception as e:
with st.container(border=True):
st.error(f"🤖 **{bot_name}** - Error")
st.error(f"An error occurred while fetching bot status: {str(e)}")
# Page Header
st.title("🦅 Hummingbot Instances")
# Auto-refresh controls
col1, col2, col3 = st.columns([3, 1, 1])
# Create placeholder for status message
status_placeholder = col1.empty()
with col2:
if st.button("▶️ Start Auto-refresh" if not st.session_state.auto_refresh_enabled else "⏸️ Stop Auto-refresh",
use_container_width=True):
st.session_state.auto_refresh_enabled = not st.session_state.auto_refresh_enabled
with col3:
if st.button("🔄 Refresh Now", use_container_width=True):
# Re-enable auto-refresh if it was temporarily disabled
if not st.session_state.auto_refresh_enabled:
st.session_state.auto_refresh_enabled = True
pass
@st.fragment(run_every=REFRESH_INTERVAL if st.session_state.auto_refresh_enabled else None)
def show_bot_instances():
"""Fragment to display bot instances with auto-refresh."""
try:
active_bots_response = backend_api_client.bot_orchestration.get_active_bots_status()
if active_bots_response.get("status") == "success":
active_bots = active_bots_response.get("data", {})
# Filter out any bots that might be in transitional state
truly_active_bots = {}
for bot_name, bot_info in active_bots.items():
try:
bot_status = backend_api_client.bot_orchestration.get_bot_status(bot_name)
if bot_status.get("status") == "success":
bot_data = bot_status.get("data", {})
if bot_data.get("status") in ["running", "stopped"]:
truly_active_bots[bot_name] = bot_info
except Exception:
continue
if truly_active_bots:
# Show refresh status
if st.session_state.auto_refresh_enabled:
status_placeholder.info(f"🔄 Auto-refreshing every {REFRESH_INTERVAL} seconds")
else:
status_placeholder.warning("⏸️ Auto-refresh paused. Click 'Refresh Now' to resume.")
# Render each bot
for bot_name in truly_active_bots.keys():
render_bot_card(bot_name)
else:
status_placeholder.info("No active bot instances found. Deploy a bot to see it here.")
else:
st.error("Failed to fetch active bots status.")
except Exception as e:
st.error(f"Failed to connect to backend: {e}")
st.info("Please make sure the backend is running and accessible.")
# Call the fragment
show_bot_instances()

View File

@@ -1,31 +1,296 @@
from types import SimpleNamespace
import re
import time
import pandas as pd
import streamlit as st
from streamlit_elements import elements, mui
from frontend.components.dashboard import Dashboard
from frontend.components.launch_strategy_v2 import LaunchStrategyV2
from frontend.st_utils import initialize_st_page
from frontend.st_utils import get_backend_api_client, initialize_st_page
CARD_WIDTH = 6
CARD_HEIGHT = 3
NUM_CARD_COLS = 2
initialize_st_page(icon="🙌", show_readme=False)
initialize_st_page(title="Launch Bot", icon="🙌")
if "launch_bots_board" not in st.session_state:
board = Dashboard()
launch_bots_board = SimpleNamespace(
dashboard=board,
launch_bot=LaunchStrategyV2(board, 0, 0, 12, 10),
)
st.session_state.launch_bots_board = launch_bots_board
else:
launch_bots_board = st.session_state.launch_bots_board
# Initialize backend client
backend_api_client = get_backend_api_client()
with elements("create_bot"):
with mui.Paper(elevation=3, style={"padding": "2rem"}, spacing=[2, 2], container=True):
with launch_bots_board.dashboard():
launch_bots_board.launch_bot()
def get_controller_configs():
"""Get all controller configurations using the new API."""
try:
return backend_api_client.controllers.list_controller_configs()
except Exception as e:
st.error(f"Failed to fetch controller configs: {e}")
return []
def filter_hummingbot_images(images):
"""Filter images to only show Hummingbot-related ones."""
hummingbot_images = []
pattern = r'.+/hummingbot:'
for image in images:
try:
if re.match(pattern, image):
hummingbot_images.append(image)
except Exception:
continue
return hummingbot_images
def launch_new_bot(bot_name, image_name, credentials, selected_controllers, max_global_drawdown,
max_controller_drawdown):
"""Launch a new bot with the selected configuration."""
if not bot_name:
st.warning("You need to define the bot name.")
return False
if not image_name:
st.warning("You need to select the hummingbot image.")
return False
if not selected_controllers:
st.warning("You need to select the controllers configs. Please select at least one controller "
"config by clicking on the checkbox.")
return False
start_time_str = time.strftime("%Y%m%d-%H%M")
full_bot_name = f"{bot_name}-{start_time_str}"
try:
# Use the new deploy_v2_controllers method
deploy_config = {
"instance_name": full_bot_name,
"credentials_profile": credentials,
"controllers_config": [config.replace(".yml", "") for config in selected_controllers],
"image": image_name,
}
# Add optional drawdown parameters if set
if max_global_drawdown is not None and max_global_drawdown > 0:
deploy_config["max_global_drawdown_quote"] = max_global_drawdown
if max_controller_drawdown is not None and max_controller_drawdown > 0:
deploy_config["max_controller_drawdown_quote"] = max_controller_drawdown
backend_api_client.bot_orchestration.deploy_v2_controllers(**deploy_config)
st.success(f"Successfully deployed bot: {full_bot_name}")
time.sleep(3)
return True
except Exception as e:
st.error(f"Failed to deploy bot: {e}")
return False
def delete_selected_configs(selected_controllers):
"""Delete selected controller configurations."""
if selected_controllers:
try:
for config in selected_controllers:
# Remove .yml extension if present
config_name = config.replace(".yml", "")
response = backend_api_client.controllers.delete_controller_config(config_name)
st.success(f"Deleted {config_name}")
return True
except Exception as e:
st.error(f"Failed to delete configs: {e}")
return False
else:
st.warning("You need to select the controllers configs that you want to delete.")
return False
# Page Header
st.title("🚀 Deploy Trading Bot")
st.subheader("Configure and deploy your automated trading strategy")
# Bot Configuration Section
with st.container(border=True):
st.info("🤖 **Bot Configuration:** Set up your bot instance with basic configuration")
# Create three columns for the configuration inputs
col1, col2, col3 = st.columns(3)
with col1:
bot_name = st.text_input(
"Instance Name",
placeholder="Enter a unique name for your bot instance",
key="bot_name_input"
)
with col2:
try:
available_credentials = backend_api_client.accounts.list_accounts()
credentials = st.selectbox(
"Credentials Profile",
options=available_credentials,
index=0,
key="credentials_select"
)
except Exception as e:
st.error(f"Failed to fetch credentials: {e}")
credentials = st.text_input(
"Credentials Profile",
value="master_account",
key="credentials_input"
)
with col3:
try:
all_images = backend_api_client.docker.get_available_images("hummingbot")
available_images = filter_hummingbot_images(all_images)
if not available_images:
# Fallback to default if no hummingbot images found
available_images = ["hummingbot/hummingbot:latest"]
# Ensure default image is in the list
default_image = "hummingbot/hummingbot:latest"
if default_image not in available_images:
available_images.insert(0, default_image)
image_name = st.selectbox(
"Hummingbot Image",
options=available_images,
index=0,
key="image_select"
)
except Exception as e:
st.error(f"Failed to fetch available images: {e}")
image_name = st.text_input(
"Hummingbot Image",
value="hummingbot/hummingbot:latest",
key="image_input"
)
# Risk Management Section
with st.container(border=True):
st.warning("⚠️ **Risk Management:** Set maximum drawdown limits in USDT to protect your capital")
col1, col2 = st.columns(2)
with col1:
max_global_drawdown = st.number_input(
"Max Global Drawdown (USDT)",
min_value=0.0,
value=0.0,
step=100.0,
format="%.2f",
help="Maximum allowed drawdown across all controllers",
key="global_drawdown_input"
)
with col2:
max_controller_drawdown = st.number_input(
"Max Controller Drawdown (USDT)",
min_value=0.0,
value=0.0,
step=100.0,
format="%.2f",
help="Maximum allowed drawdown per controller",
key="controller_drawdown_input"
)
# Controllers Section
with st.container(border=True):
st.success("🎛️ **Controller Selection:** Select the trading controllers you want to deploy with this bot instance")
# Get controller configs
all_controllers_config = get_controller_configs()
# Prepare data for the table
data = []
for config in all_controllers_config:
# Handle case where config might be a string instead of dict
if isinstance(config, str):
st.warning(f"Unexpected config format: {config}. Expected a dictionary.")
continue
# Handle both old and new config format
config_name = config.get("config_name", config.get("id", "Unknown"))
config_data = config.get("config", config) # New format has config nested
connector_name = config_data.get("connector_name", "Unknown")
trading_pair = config_data.get("trading_pair", "Unknown")
total_amount_quote = float(config_data.get("total_amount_quote", 0))
# Extract controller info
controller_name = config_data.get("controller_name", config_name)
controller_type = config_data.get("controller_type", "generic")
# Fix config base and version splitting
config_parts = config_name.split("_")
if len(config_parts) > 1:
version = config_parts[-1]
config_base = "_".join(config_parts[:-1])
else:
config_base = config_name
version = "NaN"
data.append({
"Select": False, # Checkbox column
"Config Base": config_base,
"Version": version,
"Controller Name": controller_name,
"Controller Type": controller_type,
"Connector": connector_name,
"Trading Pair": trading_pair,
"Amount (USDT)": f"${total_amount_quote:,.2f}",
"_config_name": config_name # Hidden column for reference
})
# Display info and action buttons
if data:
# Create DataFrame
df = pd.DataFrame(data)
# Use data_editor with checkbox column for selection
edited_df = st.data_editor(
df,
column_config={
"Select": st.column_config.CheckboxColumn(
"Select",
help="Select controllers to deploy or delete",
default=False,
),
"_config_name": None, # Hide this column
},
disabled=[col for col in df.columns if col != "Select"], # Only allow editing the Select column
hide_index=True,
use_container_width=True,
key="controller_table"
)
# Get selected controllers from the edited dataframe
selected_controllers = [
row["_config_name"]
for _, row in edited_df.iterrows()
if row["Select"]
]
# Display selected count
if selected_controllers:
st.success(f"{len(selected_controllers)} controller(s) selected for deployment")
# Display action buttons
st.divider()
col1, col2 = st.columns(2)
with col1:
if st.button("🗑️ Delete Selected", type="secondary", use_container_width=True):
if selected_controllers:
if delete_selected_configs(selected_controllers):
st.rerun()
else:
st.warning("Please select at least one controller to delete")
with col2:
deploy_button_style = "primary" if selected_controllers else "secondary"
if st.button("🚀 Deploy Bot", type=deploy_button_style, use_container_width=True):
if selected_controllers:
with st.spinner('🚀 Starting Bot... This process may take a few seconds'):
if launch_new_bot(bot_name, image_name, credentials, selected_controllers,
max_global_drawdown, max_controller_drawdown):
st.rerun()
else:
st.warning("Please select at least one controller to deploy")
else:
st.warning("⚠️ No controller configurations available. Please create some configurations first.")

View File

@@ -1,19 +0,0 @@
### Description
This page helps you deploy and manage Hummingbot instances:
- Starting and stopping Hummingbot Broker
- Creating, starting and stopping bot instances
- Managing strategy and script files that instances run
- Fetching status of running instances
### Maintainers
This page is maintained by Hummingbot Foundation as a template other pages:
* [cardosfede](https://github.com/cardosfede)
* [fengtality](https://github.com/fengtality)
### Wiki
See the [wiki](https://github.com/hummingbot/dashboard/wiki/%F0%9F%90%99-Bot-Orchestration) for more information.

View File

@@ -1,8 +0,0 @@
from frontend.components.deploy_v2_with_controllers import LaunchV2WithControllers
from frontend.st_utils import initialize_st_page
initialize_st_page(title="Launch Bot ST", icon="🙌")
launcher = LaunchV2WithControllers()
launcher()

View File

@@ -1,19 +1,149 @@
### Description
# Portfolio Management
This page helps you deploy and manage Hummingbot instances:
The Portfolio Management page provides comprehensive oversight of your trading portfolio across multiple exchanges, accounts, and strategies.
- Starting and stopping Hummingbot Broker
- Creating, starting and stopping bot instances
- Managing strategy and script files that instances run
- Fetching status of running instances
## Features
### Maintainers
### 💰 Multi-Exchange Portfolio
- **Unified Balance View**: Aggregate holdings across all connected exchanges
- **Real-time Valuation**: Live portfolio value updates in USD and BTC
- **Asset Distribution**: Visual breakdown of holdings by asset and exchange
- **Historical Performance**: Track portfolio value over time
This page is maintained by Hummingbot Foundation as a template other pages:
### 📊 Position Tracking
- **Open Positions**: Monitor all active positions across exchanges
- **P&L Analysis**: Real-time and realized profit/loss calculations
- **Risk Metrics**: Position sizing, leverage, and exposure analysis
- **Position History**: Complete record of closed positions
* [cardosfede](https://github.com/cardosfede)
* [fengtality](https://github.com/fengtality)
### 🔄 Performance Analytics
- **ROI Calculation**: Return on investment by strategy and timeframe
- **Sharpe Ratio**: Risk-adjusted performance metrics
- **Win Rate Analysis**: Success rate of trades by strategy
- **Drawdown Tracking**: Maximum and current drawdown monitoring
### Wiki
### 🎯 Risk Management
- **Exposure Limits**: Set and monitor position size limits
- **Correlation Analysis**: Identify correlated positions
- **VaR Calculation**: Value at Risk across the portfolio
- **Alert System**: Notifications for risk threshold breaches
See the [wiki](https://github.com/hummingbot/dashboard/wiki/%F0%9F%90%99-Bot-Orchestration) for more information.
## Usage Instructions
### 1. Connect Exchanges
- Navigate to the Credentials page to add exchange API keys
- Ensure API keys have read permissions for balances and positions
- Verify successful connection in the portfolio overview
### 2. Portfolio Overview
- **Total Value**: View aggregate portfolio value in preferred currency
- **Asset Allocation**: Pie chart showing distribution across assets
- **Exchange Distribution**: Breakdown of holdings by exchange
- **24h Performance**: Daily change in portfolio value
### 3. Position Management
- **Active Positions Tab**: Current open positions with live P&L
- **Position Details**: Click any position for detailed metrics
- **Quick Actions**: Close positions or adjust sizes
- **Export Data**: Download position data for external analysis
### 4. Performance Analysis
- **Time Range Selection**: Choose analysis period (1D, 1W, 1M, 3M, 1Y)
- **Strategy Breakdown**: Performance attribution by strategy
- **Benchmark Comparison**: Compare against BTC or market indices
- **Custom Reports**: Generate detailed performance reports
### 5. Risk Monitoring
- **Risk Dashboard**: Overview of key risk metrics
- **Position Sizing**: Ensure positions align with risk limits
- **Correlation Matrix**: Visualize position correlations
- **Stress Testing**: Simulate portfolio under various scenarios
## Technical Notes
### Data Architecture
- **Real-time Updates**: WebSocket connections for live data
- **Data Aggregation**: Efficient cross-exchange data consolidation
- **Historical Storage**: Time-series database for performance tracking
- **Cache Layer**: Redis caching for improved performance
### Calculation Methods
- **Portfolio Value**: Sum of all holdings at current market prices
- **Unrealized P&L**: (Current Price - Entry Price) × Position Size
- **Realized P&L**: Actual profits from closed positions
- **ROI**: (Current Value - Initial Value) / Initial Value × 100
### Performance Optimization
- **Incremental Updates**: Only fetch changed data
- **Batch Processing**: Aggregate API calls across exchanges
- **Smart Caching**: Cache static data with TTL
- **Lazy Loading**: Load detailed data on demand
## Component Structure
```
portfolio/
├── app.py # Main portfolio page
├── components/
│ ├── portfolio_overview.py # Summary cards and charts
│ ├── position_table.py # Active positions display
│ ├── performance_charts.py # Performance visualization
│ └── risk_dashboard.py # Risk metrics and alerts
├── services/
│ ├── balance_aggregator.py # Multi-exchange balance fetching
│ ├── position_tracker.py # Position monitoring service
│ └── performance_calc.py # Performance calculations
└── utils/
├── currency_converter.py # FX rate conversions
├── risk_metrics.py # Risk calculation functions
└── data_export.py # Export functionality
```
## Key Metrics Explained
### Portfolio Metrics
- **Total Value**: Sum of all assets converted to base currency
- **Daily Change**: 24-hour change in portfolio value
- **All-Time P&L**: Total profit/loss since inception
- **Asset Count**: Number of unique assets held
### Position Metrics
- **Entry Price**: Average price of position entry
- **Mark Price**: Current market price
- **Unrealized P&L**: Paper profit/loss on open position
- **ROI %**: Return on investment percentage
### Risk Metrics
- **Sharpe Ratio**: Risk-adjusted return metric
- **Maximum Drawdown**: Largest peak-to-trough decline
- **Value at Risk (VaR)**: Potential loss at confidence level
- **Exposure**: Total position size relative to portfolio
## Best Practices
### Portfolio Management
- Diversify across multiple assets and strategies
- Set position size limits based on risk tolerance
- Regular rebalancing to maintain target allocations
- Monitor correlation between positions
### Performance Tracking
- Record all trades for accurate P&L calculation
- Include fees in performance calculations
- Compare performance against relevant benchmarks
- Regular performance attribution analysis
### Risk Control
- Set stop-loss levels for all positions
- Monitor leverage usage across accounts
- Regular stress testing of portfolio
- Maintain cash reserves for opportunities
## Error Handling
The portfolio page includes robust error handling:
- **API Failures**: Graceful degradation with cached data
- **Rate Limiting**: Intelligent request throttling
- **Data Inconsistencies**: Reconciliation mechanisms
- **Connection Issues**: Automatic reconnection with exponential backoff
- **Calculation Errors**: Fallback values with warning indicators

View File

@@ -11,10 +11,10 @@ client = get_backend_api_client()
NUM_COLUMNS = 4
# Convert balances to a DataFrame for easier manipulation
def account_state_to_df(account_state):
# Convert portfolio state to DataFrame for easier manipulation
def portfolio_state_to_df(portfolio_state):
data = []
for account, exchanges in account_state.items():
for account, exchanges in portfolio_state.items():
for exchange, tokens_info in exchanges.items():
for info in tokens_info:
data.append({
@@ -29,8 +29,8 @@ def account_state_to_df(account_state):
return pd.DataFrame(data)
# Convert historical account states to a DataFrame
def account_history_to_df(history):
# Convert historical portfolio states to DataFrame
def portfolio_history_to_df(history):
data = []
for record in history:
timestamp = record["timestamp"]
@@ -50,108 +50,312 @@ def account_history_to_df(history):
return pd.DataFrame(data)
# Fetch account state from the backend
account_state = client.get_accounts_state()
account_history = client.get_account_state_history()
if len(account_state) == 0:
st.warning("No accounts found.")
# Aggregate portfolio history by grouping nearby timestamps
def aggregate_portfolio_history(history_df, time_window_seconds=10):
"""
Aggregate portfolio history by grouping timestamps within a time window.
This solves the issue where different exchanges are logged at slightly different times.
"""
if len(history_df) == 0:
return history_df
# Convert timestamp to pandas datetime if not already
history_df['timestamp'] = pd.to_datetime(history_df['timestamp'])
# Sort by timestamp
history_df = history_df.sort_values('timestamp')
# Create time groups by rounding timestamps to the nearest time window
history_df['time_group'] = history_df['timestamp'].dt.floor(f'{time_window_seconds}s')
# For each time group, aggregate the data
aggregated_data = []
for time_group in history_df['time_group'].unique():
group_data = history_df[history_df['time_group'] == time_group]
# Aggregate by account, exchange, token within this time group
agg_group = group_data.groupby(['account', 'exchange', 'token']).agg({
'value': 'sum',
'units': 'sum',
'available_units': 'sum',
'price': 'mean' # Use mean price for the time group
}).reset_index()
# Add the time group as timestamp
agg_group['timestamp'] = time_group
aggregated_data.append(agg_group)
if aggregated_data:
return pd.concat(aggregated_data, ignore_index=True)
else:
return pd.DataFrame()
# Global filters (outside fragments to avoid duplication)
def get_portfolio_filters():
"""Get portfolio filters that are shared between fragments"""
# Get available accounts
try:
accounts_list = client.accounts.list_accounts()
except Exception as e:
st.error(f"Failed to fetch accounts: {e}")
return None, None, None
if len(accounts_list) == 0:
st.warning("No accounts found.")
return None, None, None
# Account selection
selected_accounts = st.multiselect("Select Accounts", accounts_list, accounts_list, key="main_accounts")
if len(selected_accounts) == 0:
st.warning("Please select at least one account.")
return None, None, None
# Get portfolio state for available exchanges and tokens
try:
portfolio_state = client.portfolio.get_state(account_names=selected_accounts)
except Exception as e:
st.error(f"Failed to fetch portfolio state: {e}")
return None, None, None
# Extract available exchanges
exchanges_available = []
for account in selected_accounts:
if account in portfolio_state:
exchanges_available.extend(portfolio_state[account].keys())
exchanges_available = list(set(exchanges_available))
if len(exchanges_available) == 0:
st.warning("No exchanges found for selected accounts.")
return None, None, None
selected_exchanges = st.multiselect("Select Exchanges", exchanges_available, exchanges_available, key="main_exchanges")
# Extract available tokens
tokens_available = []
for account in selected_accounts:
if account in portfolio_state:
for exchange in selected_exchanges:
if exchange in portfolio_state[account]:
tokens_available.extend([info["token"] for info in portfolio_state[account][exchange]])
tokens_available = list(set(tokens_available))
selected_tokens = st.multiselect("Select Tokens", tokens_available, tokens_available, key="main_tokens")
return selected_accounts, selected_exchanges, selected_tokens
# Get filters once at the top level
st.header("Portfolio Filters")
selected_accounts, selected_exchanges, selected_tokens = get_portfolio_filters()
if not selected_accounts:
st.stop()
# Display the accounts available
accounts = st.multiselect("Select Accounts", list(account_state.keys()), list(account_state.keys()))
if len(accounts) == 0:
st.warning("Please select an account.")
st.stop()
# Display the exchanges available
exchanges_available = []
for account in accounts:
exchanges_available += account_state[account].keys()
if len(exchanges_available) == 0:
st.warning("No exchanges found.")
st.stop()
exchanges = st.multiselect("Select Exchanges", exchanges_available, exchanges_available)
# Display the tokens available
tokens_available = []
for account in accounts:
for exchange in exchanges:
if exchange in account_state[account]:
tokens_available += [info["token"] for info in account_state[account][exchange]]
token_options = set(tokens_available)
tokens_available = st.multiselect("Select Tokens", token_options, token_options)
st.write("---")
filtered_account_state = {}
for account in accounts:
filtered_account_state[account] = {}
for exchange in exchanges:
if exchange in account_state[account]:
filtered_account_state[account][exchange] = [token_info for token_info in account_state[account][exchange]
if token_info["token"] in tokens_available]
filtered_account_history = []
for record in account_history:
filtered_record = {"timestamp": record["timestamp"], "state": {}}
for account in accounts:
if account in record["state"]:
filtered_record["state"][account] = {}
for exchange in exchanges:
if exchange in record["state"][account]:
filtered_record["state"][account][exchange] = [token_info for token_info in
record["state"][account][exchange] if
token_info["token"] in tokens_available]
filtered_account_history.append(filtered_record)
if len(filtered_account_state) > 0:
account_state_df = account_state_to_df(filtered_account_state)
total_balance_usd = round(account_state_df["value"].sum(), 2)
c1, c2 = st.columns([1, 5])
@st.fragment
def portfolio_overview():
"""Fragment for portfolio overview and metrics"""
st.markdown("---")
# Get portfolio state and summary
try:
portfolio_state = client.portfolio.get_state(account_names=selected_accounts)
portfolio_summary = client.portfolio.get_portfolio_summary()
except Exception as e:
st.error(f"Failed to fetch portfolio data: {e}")
return
# Filter portfolio state
filtered_portfolio_state = {}
for account in selected_accounts:
if account in portfolio_state:
filtered_portfolio_state[account] = {}
for exchange in selected_exchanges:
if exchange in portfolio_state[account]:
filtered_portfolio_state[account][exchange] = [
token_info for token_info in portfolio_state[account][exchange]
if token_info["token"] in selected_tokens
]
if len(filtered_portfolio_state) == 0:
st.warning("No data available for selected filters.")
return
# Convert to DataFrame
portfolio_df = portfolio_state_to_df(filtered_portfolio_state)
total_balance_usd = round(portfolio_df["value"].sum(), 2)
# Display metrics
col1, col2, col3, col4 = st.columns(4)
with col1:
st.metric("Total Balance (USD)", f"${total_balance_usd:,.2f}")
with col2:
st.metric("Accounts", len(selected_accounts))
with col3:
st.metric("Exchanges", len(selected_exchanges))
with col4:
st.metric("Tokens", len(selected_tokens))
# Create visualizations
c1, c2 = st.columns([1, 1])
with c1:
st.metric("Total Balance (USD)", total_balance_usd)
with c2:
account_state_df['% Allocation'] = (account_state_df['value'] / total_balance_usd) * 100
account_state_df['label'] = account_state_df['token'] + ' ($' + account_state_df['value'].apply(
# Portfolio allocation pie chart
portfolio_df['% Allocation'] = (portfolio_df['value'] / total_balance_usd) * 100
portfolio_df['label'] = portfolio_df['token'] + ' ($' + portfolio_df['value'].apply(
lambda x: f'{x:,.2f}') + ')'
# Create a sunburst chart with Plotly Express
fig = px.sunburst(account_state_df,
fig = px.sunburst(portfolio_df,
path=['account', 'exchange', 'label'],
values='value',
hover_data={'% Allocation': ':.2f'},
title='% Allocation by Account, Exchange, and Token',
title='Portfolio Allocation',
color='account',
color_discrete_sequence=px.colors.qualitative.Vivid)
fig.update_traces(textinfo='label+percent entry')
fig.update_layout(margin=dict(t=0, l=0, r=0, b=0), height=800, title_x=0.01, title_y=1,)
fig.update_layout(margin=dict(t=50, l=0, r=0, b=0), height=600)
st.plotly_chart(fig, use_container_width=True)
with c2:
# Token distribution
token_distribution = portfolio_df.groupby('token')['value'].sum().reset_index()
token_distribution = token_distribution.sort_values('value', ascending=False)
fig = px.bar(token_distribution, x='token', y='value',
title='Token Distribution',
color='value',
color_continuous_scale='Blues')
fig.update_layout(xaxis_title='Token', yaxis_title='Value (USD)', height=600)
st.plotly_chart(fig, use_container_width=True)
# Portfolio details table
st.subheader("Portfolio Details")
st.dataframe(
portfolio_df[['account', 'exchange', 'token', 'units', 'price', 'value', 'available_units']],
use_container_width=True
)
st.dataframe(account_state_df[['exchange', 'token', 'units', 'price', 'value', 'available_units']], width=1800,
height=600)
# Plot the evolution of the portfolio over time
if len(filtered_account_history) > 0:
account_history_df = account_history_to_df(filtered_account_history)
account_history_df['timestamp'] = pd.to_datetime(account_history_df['timestamp'])
# Aggregate the value of the portfolio over time
portfolio_evolution_df = account_history_df.groupby('timestamp')['value'].sum().reset_index()
fig = px.line(portfolio_evolution_df, x='timestamp', y='value', title='Portfolio Evolution Over Time')
fig.update_layout(xaxis_title='Time', yaxis_title='Total Value (USD)', height=600)
@st.fragment
def portfolio_history():
"""Fragment for portfolio history and charts"""
st.markdown("---")
st.subheader("Portfolio History")
# Date range selection
col1, col2, col3 = st.columns(3)
with col1:
days_back = st.selectbox("Time Period", [7, 30, 90, 180, 365], index=1, key="history_days")
with col2:
limit = st.number_input("Max Records", min_value=10, max_value=1000, value=100, key="history_limit")
with col3:
time_window = st.selectbox("Time Aggregation Window", [5, 10, 30, 60, 300], index=1, key="time_window",
help="Seconds to group nearby timestamps (fixes exchange timing differences)")
# Get portfolio history
try:
from datetime import datetime, timezone, timedelta
# Calculate start time for filtering
start_time = datetime.now(timezone.utc) - timedelta(days=days_back)
response = client.portfolio.get_history(
selected_accounts, # account_names
None, # connector_names
limit, # limit
None, # cursor
int(start_time.timestamp()), # start_time
None # end_time
)
# Extract data from response
history_data = response.get("data", [])
except Exception as e:
st.error(f"Failed to fetch portfolio history: {e}")
return
if not history_data or len(history_data) == 0:
st.warning("No historical data available.")
return
# Convert to DataFrame
history_df = portfolio_history_to_df(history_data)
history_df['timestamp'] = pd.to_datetime(history_df['timestamp'], format='ISO8601')
# Filter by selected exchanges and tokens
history_df = history_df[
(history_df['exchange'].isin(selected_exchanges)) &
(history_df['token'].isin(selected_tokens))
]
# Aggregate timestamps to solve the "electrocardiogram" issue
history_df = aggregate_portfolio_history(history_df, time_window_seconds=time_window)
if len(history_df) == 0:
st.warning("No historical data available for selected filters.")
return
# Portfolio evolution by account (area chart)
st.subheader("Portfolio Evolution by Account")
account_evolution_df = history_df.groupby(['timestamp', 'account'])['value'].sum().reset_index()
account_evolution_df = account_evolution_df.sort_values('timestamp')
fig = px.area(account_evolution_df, x='timestamp', y='value', color='account',
title='Portfolio Value Evolution by Account',
color_discrete_sequence=px.colors.qualitative.Set3)
fig.update_layout(xaxis_title='Time', yaxis_title='Value (USD)', height=400)
st.plotly_chart(fig, use_container_width=True)
# Plot the evolution of each token's value over time
token_evolution_df = account_history_df.groupby(['timestamp', 'token'])['value'].sum().reset_index()
fig = px.area(token_evolution_df, x='timestamp', y='value', color='token', title='Token Value Evolution Over Time',
# Portfolio evolution by token (area chart)
st.subheader("Portfolio Evolution by Token")
token_evolution_df = history_df.groupby(['timestamp', 'token'])['value'].sum().reset_index()
token_evolution_df = token_evolution_df.sort_values('timestamp')
# Show only top 10 tokens by average value to avoid clutter
top_tokens = token_evolution_df.groupby('token')['value'].mean().sort_values(ascending=False).head(10).index
token_evolution_filtered = token_evolution_df[token_evolution_df['token'].isin(top_tokens)]
fig = px.area(token_evolution_filtered, x='timestamp', y='value', color='token',
title='Portfolio Value Evolution by Token (Top 10)',
color_discrete_sequence=px.colors.qualitative.Vivid)
fig.update_layout(xaxis_title='Time', yaxis_title='Value (USD)', height=600)
fig.update_layout(xaxis_title='Time', yaxis_title='Value (USD)', height=400)
st.plotly_chart(fig, use_container_width=True)
# Portfolio evolution by exchange (area chart)
st.subheader("Portfolio Evolution by Exchange")
exchange_evolution_df = history_df.groupby(['timestamp', 'exchange'])['value'].sum().reset_index()
exchange_evolution_df = exchange_evolution_df.sort_values('timestamp')
fig = px.area(exchange_evolution_df, x='timestamp', y='value', color='exchange',
title='Portfolio Value Evolution by Exchange',
color_discrete_sequence=px.colors.qualitative.Pastel)
fig.update_layout(xaxis_title='Time', yaxis_title='Value (USD)', height=400)
st.plotly_chart(fig, use_container_width=True)
# Portfolio evolution table - total values
st.subheader("Portfolio Total Value Over Time")
total_evolution_df = history_df.groupby('timestamp')['value'].sum().reset_index()
total_evolution_df = total_evolution_df.sort_values('timestamp')
evolution_table = total_evolution_df.copy()
evolution_table['timestamp'] = evolution_table['timestamp'].dt.strftime('%Y-%m-%d %H:%M:%S')
evolution_table['value'] = evolution_table['value'].round(2)
evolution_table = evolution_table.rename(columns={'timestamp': 'Time', 'value': 'Total Value (USD)'})
st.dataframe(evolution_table, use_container_width=True)
# Main portfolio page
st.header("Portfolio Overview")
portfolio_overview()
st.header("Portfolio History")
portfolio_history()

View File

@@ -0,0 +1,97 @@
# Trading Hub
The Trading Hub provides a comprehensive interface for executing trades, monitoring positions, and analyzing markets in real-time.
## Features
### 🎯 Real-time Market Data
- **OHLC Candlestick Chart**: 5-minute interval price action with volume overlay
- **Live Order Book**: Real-time bid/ask levels with configurable depth (10-100 levels)
- **Current Price Display**: Live price updates with auto-refresh capability
- **Volume Analysis**: Trading volume visualization
### ⚡ Quick Trading
- **Market Orders**: Instant buy/sell execution at current market price
- **Limit Orders**: Set specific price levels for order execution
- **Position Management**: Open/close positions for perpetual contracts
- **Multi-Exchange Support**: Trade across Binance, KuCoin, OKX, and more
### 📊 Portfolio Monitoring
- **Open Positions**: Real-time P&L tracking with entry/mark prices
- **Active Orders**: Monitor pending orders with one-click cancellation
- **Account Overview**: Multi-account position and order management
### 🔄 Real-time Performance
- **Memory-Cached Candles**: Ultra-fast updates from backend memory cache (typically <100ms)
- **Configurable Intervals**: 2-second auto-refresh for real-time trading experience
- **Performance Monitoring**: Live display of data fetch times
- **Optimized Updates**: Efficient data streaming for minimal latency
## How to Use
### Market Selection
1. **Choose Exchange**: Select from available connectors (binance_perpetual, binance, kucoin, okx_perpetual)
2. **Select Trading Pair**: Enter the trading pair (e.g., BTC-USDT, ETH-USDT)
3. **Set Order Book Depth**: Choose how many price levels to display (10-100)
### Placing Orders
1. **Account Setup**: Specify the account name (default: master_account)
2. **Order Configuration**:
- **Side**: Choose BUY or SELL
- **Order Type**: Select MARKET, LIMIT, or LIMIT_MAKER
- **Amount**: Enter the quantity to trade
- **Price**: Set price for limit orders (auto-filled for market orders)
- **Position Action**: Choose OPEN or CLOSE for perpetual contracts
### Managing Positions
- **View Open Positions**: Monitor unrealized P&L, entry prices, and position sizes
- **Track Performance**: Real-time updates of mark prices and P&L calculations
- **Multi-Account Support**: View positions across different trading accounts
### Order Management
- **Active Orders**: View all pending orders with real-time status
- **Bulk Cancellation**: Select multiple orders for batch cancellation
- **Order History**: Track order execution and fill status
## Technical Features
### Market Data Integration
- **Memory-Cached Candles**: Real-time OHLC data from backend memory (1m, 3m, 5m, 15m, 1h intervals)
- **Ultra-Fast Updates**: Sub-100ms data fetching from cached candle streams
- **Order Book Depth**: Configurable bid/ask level display (10-100 levels)
- **Live Price Feeds**: Real-time price updates across multiple exchanges
- **Performance Metrics**: Live monitoring of data fetch speeds
### Chart Visualization
- **Candlestick Chart**: Interactive price action with zoom and pan
- **Order Book Overlay**: Visualized bid/ask levels on the chart
- **Volume Bars**: Trading volume display below price chart
- **Dark Theme**: Futuristic styling optimized for trading environments
### Auto-Refresh System
- **Streamlit Fragments**: Efficient real-time updates without full page refresh
- **Configurable Intervals**: Adjustable refresh rates (default: 5 seconds)
- **Manual Control**: Start/stop auto-refresh as needed
- **Error Handling**: Graceful handling of connection issues
## Supported Exchanges
- **Binance Spot**: Standard spot trading
- **Binance Perpetual**: Futures and perpetual contracts
- **KuCoin**: Spot and margin trading
- **OKX Perpetual**: Futures and perpetual contracts
## Error Handling
The trading interface includes comprehensive error handling:
- **Connection Errors**: Graceful handling of backend connectivity issues
- **Order Errors**: Clear error messages for failed order placement
- **Data Errors**: Fallback displays when market data is unavailable
- **Validation**: Input validation for trading parameters
## Security Considerations
- **Account Isolation**: Each account's positions and orders are tracked separately
- **Order Validation**: Server-side validation of all trading parameters
- **Error Recovery**: Automatic retry mechanisms for transient failures
- **Safe Defaults**: Conservative default values for trading parameters

View File

@@ -0,0 +1 @@
# Trading page module

File diff suppressed because it is too large Load Diff