Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 2 additions & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -24,3 +24,5 @@ requirements.txt
!.github/workflows/
.ruff_cache/
.claude/
stats.json
stats.json.lock
4 changes: 2 additions & 2 deletions app_pages/0_Home.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,7 @@ def plot_total_value(df: pd.DataFrame):
height=400,
)

st.plotly_chart(fig, width="stretch")
st.plotly_chart(fig, use_container_width=True)


st.title("Crypto Update")
Expand Down Expand Up @@ -96,7 +96,7 @@ def plot_total_value(df: pd.DataFrame):
balance = (
0
if df_balance is None or df_balance.empty
else df_balance.iloc[-1, 1:].sum()
else df_balance.iloc[-1].sum()
)
balance = round(balance, 2)
st.metric("Total value", value=f"{balance} {currency_symbol}")
Expand Down
78 changes: 4 additions & 74 deletions app_pages/1_Portfolios.py
Original file line number Diff line number Diff line change
@@ -1,13 +1,10 @@
import logging
import traceback

import pandas as pd
import streamlit as st
import plotly.graph_objects as go

from modules.database.customdata import Customdata
from modules.database.portfolios import Portfolios
from modules.database.tokensdb import TokensDatabase
from modules.tools import (
create_portfolio_dataframe,
update,
Expand Down Expand Up @@ -105,7 +102,7 @@ def delete_token(portfolio_name: str):
)
if st.button("Submit"):
for token in tokens:
g_portfolios.delete_token_a(portfolio_name, token)
g_portfolios.delete_token_by_name(portfolio_name, token)
# Close dialog
st.rerun()

Expand Down Expand Up @@ -214,69 +211,6 @@ def load_portfolios(dbfile: str) -> Portfolios:
return Portfolios(dbfile)


def get_portfolio_history(portfolio_name: str, dbfile: str) -> pd.DataFrame:
"""Get historical value of a portfolio over time.

Args:
portfolio_name: Name of the portfolio
dbfile: Path to database file

Returns:
DataFrame with timestamp index and portfolio value
"""
logger.debug("Getting history for portfolio %s", portfolio_name)

# Get current portfolio tokens and amounts
portfolio = Portfolios(dbfile)
tokens_dict = portfolio.get_tokens(portfolio_name)

if not tokens_dict:
logger.warning("Portfolio %s is empty", portfolio_name)
return pd.DataFrame()

# Get historical prices from TokensDatabase
tokensdb = TokensDatabase(dbfile)

# Query all historical data for tokens in this portfolio
import sqlite3

with sqlite3.connect(dbfile) as con:
# Get tokens list
tokens_list = list(tokens_dict.keys())
tokens_placeholder = ",".join(["?" for _ in tokens_list])

query = f"""
SELECT timestamp, token, price
FROM TokensDatabase
WHERE token IN ({tokens_placeholder})
ORDER BY timestamp
"""
df = pd.read_sql_query(query, con, params=tokens_list)

if df.empty:
logger.warning("No historical data found for portfolio %s", portfolio_name)
return pd.DataFrame()

# Convert timestamp to datetime
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="s", utc=True)
df["timestamp"] = (
df["timestamp"].dt.tz_convert(tokensdb.local_timezone).dt.tz_localize(None)
)

# Calculate value for each token at each timestamp
df["amount"] = df["token"].map(lambda t: float(tokens_dict.get(t, 0)))
df["value"] = df["price"] * df["amount"]

# Group by timestamp and sum values
portfolio_value = df.groupby("timestamp")["value"].sum().reset_index()
portfolio_value.columns = ["Date", "Value"]
portfolio_value.set_index("Date", inplace=True)
portfolio_value.sort_index(inplace=True)

logger.debug("Portfolio history shape: %s", portfolio_value.shape)
return portfolio_value


@st.fragment
def execute_search():
"""Execute token search and display results.
Expand Down Expand Up @@ -368,13 +302,9 @@ def save_toggle_preference():

def is_portfolio_empty(pf_name):
pf = g_portfolios.get_portfolio(pf_name)
df = create_portfolio_dataframe(pf)
if df.empty:
return True
# Si tous les montants sont nuls ou absents
if (df["amount"].fillna(0) == 0).all():
if not pf:
return True
return False
return all((v.get("amount") or 0) == 0 for v in pf.values())


all_tabs = g_portfolios.get_portfolio_names()
Expand All @@ -393,4 +323,4 @@ def is_portfolio_empty(pf_name):
portfolio_ui(tabs)
except Exception as e:
st.error(f"UI Error: {str(e)}")
traceback.print_exc()
logger.exception("Portfolio UI rendering failed")
74 changes: 41 additions & 33 deletions app_pages/2_Graphs.py
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
from modules.database.portfolios import Portfolios
from modules.database.tokensdb import TokensDatabase
from modules.plotter import plot_as_pie
from modules.token_metadata import TokenMetadataManager
from modules.tools import (
create_portfolio_dataframe,
get_currency_symbol,
Expand Down Expand Up @@ -50,6 +51,7 @@ def fetch_api_crypto_market(
token_symbol: str,
from_ts: int,
to_ts: int,
coinid: int = None,
) -> pd.DataFrame:
"""Fetch cryptocurrency market data from API with Streamlit session cache.

Expand All @@ -60,13 +62,14 @@ def fetch_api_crypto_market(
token_symbol: Token symbol to fetch
from_ts: Unix timestamp for start date
to_ts: Unix timestamp for end date
coinid: Optional MarketRaccoon coin ID (prioritaire sur token_symbol)

Returns:
DataFrame with columns: Date (index), Price or None if empty
"""
api = ApiMarket(api_url, api_key=api_key, cache_file=cache_file)
return api.get_cryptocurrency_market_cached(
token_symbol=token_symbol, from_timestamp=from_ts, to_timestamp=to_ts
coinid=coinid, token_symbol=token_symbol, from_timestamp=from_ts, to_timestamp=to_ts
)


Expand Down Expand Up @@ -156,7 +159,7 @@ def plot_modern_graph(

fig.update_layout(**layout_config)

st.plotly_chart(fig, width="stretch")
st.plotly_chart(fig, use_container_width=True)


def plot_dual_axis_graph(df: pd.DataFrame, title: str = None, token: str = None):
Expand Down Expand Up @@ -254,7 +257,7 @@ def plot_dual_axis_graph(df: pd.DataFrame, title: str = None, token: str = None)
legend=dict(orientation="h", yanchor="bottom", y=1.02, xanchor="right", x=1),
)

st.plotly_chart(fig, width="stretch")
st.plotly_chart(fig, use_container_width=True)


def load_portfolios(dbfile: str) -> Portfolios:
Expand Down Expand Up @@ -323,6 +326,8 @@ def draw_tab_content(
token: str,
start_timestamp: int,
end_timestamp: int,
tokensdb: TokensDatabase,
marketdb: Market,
use_api: bool = False,
):
logger.debug("Draw tab content for token %s", token)
Expand All @@ -343,13 +348,16 @@ def draw_tab_content(
cache_file = os.path.join(
st.session_state.settings["data_path"], "api_cache.json"
)
metadata_manager = TokenMetadataManager(st.session_state.settings["dbfile"])
mr_id = metadata_manager.get_mr_id(token)
df_prices = fetch_api_crypto_market(
api_url,
api_key,
cache_file,
token,
start_timestamp,
end_timestamp,
coinid=mr_id,
)
if df_prices is None:
st.warning(
Expand Down Expand Up @@ -430,8 +438,11 @@ def draw_tab_content(
cache_file = os.path.join(
st.session_state.settings["data_path"], "api_cache.json"
)
metadata_manager = TokenMetadataManager(st.session_state.settings["dbfile"])
mr_id = metadata_manager.get_mr_id(token)
df_view = fetch_api_crypto_market(
api_url, api_key, cache_file, token, start_timestamp, end_timestamp
api_url, api_key, cache_file, token, start_timestamp, end_timestamp,
coinid=mr_id,
)

# Conversion USD → devise cible avec taux historiques
Expand All @@ -451,7 +462,7 @@ def draw_tab_content(
df_view = df_view.drop(columns=["source_currency"])
else:
# Use local SQLite database (données en EUR)
df_view = markgetdb.get_token_market(
df_view = marketdb.get_token_market(
token, start_timestamp, end_timestamp
)

Expand Down Expand Up @@ -522,7 +533,10 @@ def draw_tab_content(
"Timeframe Low",
value=f"{round(min_price, 2)} {currency_symbol}",
help=f"Date: {min_price_date[0]}",
delta=f"{round(((current_price - min_price) / min_price) * 100, 2)} %",
delta=(
f"{round(((current_price - min_price) / min_price) * 100, 2)} %"
if min_price != 0 else "∞ %"
),
)
with mcol4:
max_price = df_view[actual_column].max()
Expand All @@ -533,7 +547,10 @@ def draw_tab_content(
"Timeframe High",
value=f"{round(max_price, 2)} {currency_symbol}",
help=f"Date: {max_price_date[0]}",
delta=f"{round(((current_price - max_price) / max_price) * 100, 2)} %",
delta=(
f"{round(((current_price - max_price) / max_price) * 100, 2)} %"
if max_price != 0 else "∞ %"
),
)

col1, col2 = st.columns([3, 1])
Expand All @@ -559,6 +576,11 @@ def build_tabs(section: str = "Assets Balances", use_api: bool = False):
end_timestamp = toTimestamp_A(
st.session_state.enddate, pd.to_datetime("23:59:59").time()
)
tokensdb = TokensDatabase(st.session_state.settings["dbfile"])
marketdb = Market(
st.session_state.settings["dbfile"],
st.session_state.settings["coinmarketcap_token"],
)
if section == "Assets Balances":
# For Assets Balances, always use local tokens since we need count data
available_tokens = tokensdb.get_tokens()
Expand All @@ -577,9 +599,9 @@ def build_tabs(section: str = "Assets Balances", use_api: bool = False):
st.warning(
"Unable to fetch tokens from API, falling back to local database"
)
available_tokens = markgetdb.get_tokens()
available_tokens = marketdb.get_tokens()
else:
available_tokens = markgetdb.get_tokens()
available_tokens = marketdb.get_tokens()
else:
available_tokens = []
if not available_tokens:
Expand Down Expand Up @@ -614,6 +636,8 @@ def build_tabs(section: str = "Assets Balances", use_api: bool = False):
st.session_state.tokens[idx_token],
start_timestamp,
end_timestamp,
tokensdb,
marketdb,
use_api,
)
idx_token += 1
Expand All @@ -627,9 +651,9 @@ def build_price_tab(
st.info("No data available")
return
if st.session_state.startdate < st.session_state.enddate:
df_view = df.loc[df.index > str(st.session_state.startdate)]
df_view = df.loc[df.index > pd.Timestamp(st.session_state.startdate)]
df_view = df_view.loc[
df_view.index < str(st.session_state.enddate + pd.to_timedelta(1, unit="d"))
df_view.index < pd.Timestamp(st.session_state.enddate) + pd.to_timedelta(1, unit="d")
]
df_view = df_view.loc[:, ["price"]]
df_view = df_view.dropna()
Expand Down Expand Up @@ -723,6 +747,7 @@ def build_price_tab(
if st.button("Clear API Cache", help="Clear cached API responses"):
fetch_api_coins.clear()
fetch_api_crypto_market.clear()
fetch_api_fiat_rates.clear()
st.success("Cache cleared!")
st.rerun()
else:
Expand All @@ -738,20 +763,6 @@ def build_price_tab(
key="currency_direction_toggle",
)

tokensdb = TokensDatabase(st.session_state.settings["dbfile"])
markgetdb = Market(
st.session_state.settings["dbfile"],
st.session_state.settings["coinmarketcap_token"],
)

# Initialize ApiMarket for MarketRaccoon API access
cache_file = os.path.join(st.session_state.settings["data_path"], "api_cache.json")
apimarket = ApiMarket(
st.session_state.settings["marketraccoon_url"],
api_key=st.session_state.settings.get("marketraccoon_token"),
cache_file=cache_file,
)

if "tokens" not in st.session_state:
# Load saved token preferences from settings
st.session_state.tokens = st.session_state.settings.get(
Expand All @@ -763,17 +774,17 @@ def build_price_tab(
st.title("Global")
aggregater_ui()

if add_selectbox == "Assets Balances":
elif add_selectbox == "Assets Balances":
logger.debug("Assets Balances")
st.title("Assets Balances")
build_tabs(use_api=use_api_sidebar)

if add_selectbox == "Market":
elif add_selectbox == "Market":
logger.debug("Market")
st.title("Market")
build_tabs("Market", use_api=use_api_sidebar)

if add_selectbox == "Currency":
elif add_selectbox == "Currency":
# Determine currency direction from toggle
currency_inverted = st.session_state.get("currency_direction_toggle", True)

Expand Down Expand Up @@ -804,7 +815,7 @@ def build_price_tab(
# Invert prices if EUR/USD is selected
if currency_inverted and currency_data is not None and not currency_data.empty:
currency_data = currency_data.copy()
currency_data["price"] = 1 / currency_data["price"]
currency_data["price"] = 1 / currency_data["price"].replace(0, pd.NA)

build_price_tab(currency_data, chart_title=CHART_TITLE, chart_y_label=CHART_Y_LABEL)

Expand Down Expand Up @@ -860,8 +871,5 @@ def build_price_tab(
st.info(f"Exact value: {value:.6f} {UNIT}")
else:
st.warning("Value is 0.0")
elif (
st.session_state.interpolation_result is None
and "interpolation_result" in st.session_state
):
else:
st.info("No data available")
Loading
Loading