mirror of
https://github.com/d0zingcat/deploy.git
synced 2026-05-23 07:26:53 +00:00
(feat) update pages
This commit is contained in:
120
pages/orchestration/archived_bots/README.md
Normal file
120
pages/orchestration/archived_bots/README.md
Normal 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
|
||||
1
pages/orchestration/archived_bots/__init__.py
Normal file
1
pages/orchestration/archived_bots/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Archived Bots Page Module
|
||||
1093
pages/orchestration/archived_bots/app.py
Normal file
1093
pages/orchestration/archived_bots/app.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -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()
|
||||
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
@@ -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.")
|
||||
|
||||
@@ -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.
|
||||
@@ -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()
|
||||
@@ -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
|
||||
@@ -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()
|
||||
|
||||
97
pages/orchestration/trading/README.md
Normal file
97
pages/orchestration/trading/README.md
Normal 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
|
||||
1
pages/orchestration/trading/__init__.py
Normal file
1
pages/orchestration/trading/__init__.py
Normal file
@@ -0,0 +1 @@
|
||||
# Trading page module
|
||||
1500
pages/orchestration/trading/app.py
Normal file
1500
pages/orchestration/trading/app.py
Normal file
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user