feat: Streamlit dashboard with sidebar, detail, portfolio, and main app
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
93
dashboard/detail.py
Normal file
93
dashboard/detail.py
Normal file
@@ -0,0 +1,93 @@
|
||||
import streamlit as st
|
||||
import plotly.graph_objects as go
|
||||
from plotly.subplots import make_subplots
|
||||
import pandas as pd
|
||||
import ta
|
||||
|
||||
def render_detail(symbol: str, coin_data: dict, ohlcv_df: pd.DataFrame,
|
||||
news_articles: list, social_sentiment: dict, ai_summary: str):
|
||||
symbol_short = symbol.replace("USDT", "")
|
||||
signal = coin_data.get("signal", "HOLD")
|
||||
|
||||
st.header(f"{symbol_short}/USDT")
|
||||
cols = st.columns(5)
|
||||
cols[0].metric("Signal", signal)
|
||||
cols[1].metric("Score", f"{coin_data.get('composite', 0):.0f}/100")
|
||||
cols[2].metric("Technical", f"{coin_data.get('technical', 0):.0f}")
|
||||
cols[3].metric("News", f"{coin_data.get('news', 0):.0f}")
|
||||
cols[4].metric("Social / AI", f"{coin_data.get('social', 0):.0f} / {coin_data.get('ai', 0):.0f}")
|
||||
|
||||
tab_chart, tab_news, tab_social, tab_ai = st.tabs(["Chart", "News", "Social", "AI Analysis"])
|
||||
|
||||
with tab_chart:
|
||||
_render_chart(ohlcv_df, symbol_short)
|
||||
with tab_news:
|
||||
_render_news(news_articles)
|
||||
with tab_social:
|
||||
_render_social(social_sentiment)
|
||||
with tab_ai:
|
||||
_render_ai(ai_summary)
|
||||
|
||||
def _render_chart(df: pd.DataFrame, symbol: str):
|
||||
if df is None or df.empty:
|
||||
st.info("No chart data available")
|
||||
return
|
||||
df = df.copy()
|
||||
df["rsi"] = ta.momentum.RSIIndicator(df["close"]).rsi()
|
||||
macd = ta.trend.MACD(df["close"])
|
||||
df["macd"] = macd.macd()
|
||||
df["macd_signal"] = macd.macd_signal()
|
||||
bb = ta.volatility.BollingerBands(df["close"])
|
||||
df["bb_high"] = bb.bollinger_hband()
|
||||
df["bb_low"] = bb.bollinger_lband()
|
||||
df["sma20"] = df["close"].rolling(20).mean()
|
||||
|
||||
fig = make_subplots(rows=3, cols=1, shared_xaxes=True, vertical_spacing=0.05,
|
||||
row_heights=[0.6, 0.2, 0.2],
|
||||
subplot_titles=(f"{symbol} Price", "RSI", "MACD"))
|
||||
fig.add_trace(go.Candlestick(x=df["timestamp"], open=df["open"], high=df["high"],
|
||||
low=df["low"], close=df["close"], name="Price"), row=1, col=1)
|
||||
fig.add_trace(go.Scatter(x=df["timestamp"], y=df["bb_high"], line=dict(color="rgba(173,216,230,0.3)"), name="BB Upper"), row=1, col=1)
|
||||
fig.add_trace(go.Scatter(x=df["timestamp"], y=df["bb_low"], line=dict(color="rgba(173,216,230,0.3)"), fill="tonexty", name="BB Lower"), row=1, col=1)
|
||||
fig.add_trace(go.Scatter(x=df["timestamp"], y=df["sma20"], line=dict(color="orange", width=1), name="SMA20"), row=1, col=1)
|
||||
fig.add_trace(go.Scatter(x=df["timestamp"], y=df["rsi"], line=dict(color="purple"), name="RSI"), row=2, col=1)
|
||||
fig.add_hline(y=70, line_dash="dash", line_color="red", row=2, col=1)
|
||||
fig.add_hline(y=30, line_dash="dash", line_color="green", row=2, col=1)
|
||||
fig.add_trace(go.Scatter(x=df["timestamp"], y=df["macd"], line=dict(color="blue"), name="MACD"), row=3, col=1)
|
||||
fig.add_trace(go.Scatter(x=df["timestamp"], y=df["macd_signal"], line=dict(color="red"), name="Signal"), row=3, col=1)
|
||||
fig.update_layout(template="plotly_dark", height=700, showlegend=False, xaxis_rangeslider_visible=False)
|
||||
st.plotly_chart(fig, use_container_width=True)
|
||||
|
||||
def _render_news(articles: list):
|
||||
if not articles:
|
||||
st.info("No recent news for this coin")
|
||||
return
|
||||
for article in articles[:10]:
|
||||
votes = article.get("sentiment_votes", {})
|
||||
pos = votes.get("positive", 0)
|
||||
neg = votes.get("negative", 0)
|
||||
if pos > neg:
|
||||
icon = ":green_circle:"
|
||||
elif neg > pos:
|
||||
icon = ":red_circle:"
|
||||
else:
|
||||
icon = ":white_circle:"
|
||||
st.markdown(f"{icon} **{article['title']}**")
|
||||
st.caption(f"Published: {article.get('published_at', 'N/A')} | +{pos} -{neg}")
|
||||
st.divider()
|
||||
|
||||
def _render_social(sentiment: dict):
|
||||
if not sentiment or sentiment.get("total", 0) == 0:
|
||||
st.info("No social data available")
|
||||
return
|
||||
total = sentiment["total"]
|
||||
cols = st.columns(3)
|
||||
cols[0].metric("Positive", f"{sentiment['positive']}/{total}", f"{sentiment['positive']/total*100:.0f}%")
|
||||
cols[1].metric("Negative", f"{sentiment['negative']}/{total}", f"-{sentiment['negative']/total*100:.0f}%")
|
||||
cols[2].metric("Neutral", f"{sentiment['neutral']}/{total}")
|
||||
|
||||
def _render_ai(summary: str):
|
||||
if not summary:
|
||||
st.info("AI analysis not available")
|
||||
return
|
||||
st.markdown(summary)
|
||||
Reference in New Issue
Block a user