mirror of
https://github.com/d0zingcat/deploy.git
synced 2026-05-17 07:26:45 +00:00
362 lines
14 KiB
Python
362 lines
14 KiB
Python
import pandas as pd
|
|
import plotly.express as px
|
|
import streamlit as st
|
|
|
|
from frontend.st_utils import get_backend_api_client, initialize_st_page
|
|
|
|
initialize_st_page(title="Portfolio", icon="💰")
|
|
|
|
# Page content
|
|
client = get_backend_api_client()
|
|
NUM_COLUMNS = 4
|
|
|
|
|
|
# Convert portfolio state to DataFrame for easier manipulation
|
|
def portfolio_state_to_df(portfolio_state):
|
|
data = []
|
|
for account, exchanges in portfolio_state.items():
|
|
for exchange, tokens_info in exchanges.items():
|
|
for info in tokens_info:
|
|
data.append({
|
|
"account": account,
|
|
"exchange": exchange,
|
|
"token": info["token"],
|
|
"price": info["price"],
|
|
"units": info["units"],
|
|
"value": info["value"],
|
|
"available_units": info["available_units"],
|
|
})
|
|
return pd.DataFrame(data)
|
|
|
|
|
|
# Convert historical portfolio states to DataFrame
|
|
def portfolio_history_to_df(history):
|
|
data = []
|
|
for record in history:
|
|
timestamp = record["timestamp"]
|
|
for account, exchanges in record["state"].items():
|
|
for exchange, tokens_info in exchanges.items():
|
|
for info in tokens_info:
|
|
data.append({
|
|
"timestamp": timestamp,
|
|
"account": account,
|
|
"exchange": exchange,
|
|
"token": info["token"],
|
|
"price": info["price"],
|
|
"units": info["units"],
|
|
"value": info["value"],
|
|
"available_units": info["available_units"],
|
|
})
|
|
return pd.DataFrame(data)
|
|
|
|
|
|
# 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()
|
|
|
|
|
|
@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:
|
|
# 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}') + ')'
|
|
|
|
fig = px.sunburst(portfolio_df,
|
|
path=['account', 'exchange', 'label'],
|
|
values='value',
|
|
hover_data={'% Allocation': ':.2f'},
|
|
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=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.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)
|
|
|
|
# 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=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()
|