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:
161
run.py
Normal file
161
run.py
Normal file
@@ -0,0 +1,161 @@
|
||||
import streamlit as st
|
||||
import logging
|
||||
from logging.handlers import RotatingFileHandler
|
||||
import os
|
||||
import json
|
||||
|
||||
from config import (
|
||||
BINANCE_API_KEY, BINANCE_SECRET, CRYPTOPANIC_API_KEY, NEWS_API_KEY,
|
||||
TWITTER_BEARER_TOKEN, REDDIT_CLIENT_ID, REDDIT_SECRET, REDDIT_USER_AGENT,
|
||||
ANTHROPIC_API_KEY, DB_PATH, LOG_DIR, TOP_N_COINS, INITIAL_CAPITAL,
|
||||
ANALYSIS_INTERVAL_MINUTES, DEFAULT_WEIGHTS, SURGE_VOLUME_MULTIPLIER,
|
||||
)
|
||||
from data.db import Database
|
||||
from data.binance_rest import BinanceRestClient
|
||||
from data.binance_ws import BinanceWSClient
|
||||
from data.news_client import NewsClient
|
||||
from data.social_client import SocialClient
|
||||
from agents.ai_analyst import AIAgent
|
||||
from engine.signal import SignalEngine
|
||||
from engine.surge import SurgeDetector
|
||||
from engine.portfolio import PortfolioManager
|
||||
from scheduler.jobs import AnalysisJob, create_scheduler
|
||||
from dashboard.sidebar import render_sidebar
|
||||
from dashboard.detail import render_detail
|
||||
from dashboard.portfolio_view import render_portfolio
|
||||
|
||||
os.makedirs(LOG_DIR, exist_ok=True)
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s [%(levelname)s] %(name)s: %(message)s",
|
||||
handlers=[
|
||||
RotatingFileHandler(os.path.join(LOG_DIR, "app.log"), maxBytes=10_000_000, backupCount=5),
|
||||
logging.StreamHandler(),
|
||||
],
|
||||
)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
st.set_page_config(page_title="Crypto Signal Dashboard", layout="wide", page_icon="📊")
|
||||
|
||||
if "initialized" not in st.session_state:
|
||||
st.session_state.initialized = False
|
||||
st.session_state.selected_coin = None
|
||||
|
||||
@st.cache_resource
|
||||
def init_services():
|
||||
db = Database(DB_PATH)
|
||||
db.init()
|
||||
binance_rest = BinanceRestClient(BINANCE_API_KEY, BINANCE_SECRET)
|
||||
binance_ws = BinanceWSClient(BINANCE_API_KEY, BINANCE_SECRET)
|
||||
news_client = NewsClient(CRYPTOPANIC_API_KEY, NEWS_API_KEY)
|
||||
social_client = SocialClient(REDDIT_CLIENT_ID, REDDIT_SECRET, REDDIT_USER_AGENT, TWITTER_BEARER_TOKEN)
|
||||
ai_agent = AIAgent(ANTHROPIC_API_KEY)
|
||||
signal_engine = SignalEngine()
|
||||
surge_detector = SurgeDetector(SURGE_VOLUME_MULTIPLIER)
|
||||
portfolio = PortfolioManager(INITIAL_CAPITAL)
|
||||
|
||||
saved_weights = db.load_setting("weights")
|
||||
if saved_weights:
|
||||
try:
|
||||
signal_engine.set_weights(json.loads(saved_weights))
|
||||
except (json.JSONDecodeError, ValueError):
|
||||
pass
|
||||
|
||||
try:
|
||||
monitored = binance_rest.get_top_coins(TOP_N_COINS)
|
||||
except Exception:
|
||||
monitored = ["BTCUSDT", "ETHUSDT", "BNBUSDT", "SOLUSDT", "XRPUSDT"]
|
||||
logger.warning("Failed to fetch top coins, using defaults")
|
||||
|
||||
job = AnalysisJob(
|
||||
binance=binance_rest, news_client=news_client,
|
||||
social_client=social_client, ai_agent=ai_agent,
|
||||
signal_engine=signal_engine, surge_detector=surge_detector,
|
||||
portfolio=portfolio, db=db, monitored_coins=monitored,
|
||||
)
|
||||
|
||||
scheduler = create_scheduler(job, ANALYSIS_INTERVAL_MINUTES)
|
||||
scheduler.start()
|
||||
|
||||
try:
|
||||
binance_ws.start(monitored[:20])
|
||||
except Exception as e:
|
||||
logger.warning(f"WebSocket start failed: {e}")
|
||||
|
||||
job.run_analysis()
|
||||
|
||||
return {
|
||||
"db": db, "binance_rest": binance_rest, "binance_ws": binance_ws,
|
||||
"news_client": news_client, "social_client": social_client,
|
||||
"signal_engine": signal_engine, "portfolio": portfolio,
|
||||
"job": job, "monitored": monitored,
|
||||
}
|
||||
|
||||
def main():
|
||||
services = init_services()
|
||||
db = services["db"]
|
||||
job = services["job"]
|
||||
portfolio = services["portfolio"]
|
||||
binance_rest = services["binance_rest"]
|
||||
binance_ws = services["binance_ws"]
|
||||
news_client = services["news_client"]
|
||||
social_client = services["social_client"]
|
||||
|
||||
current_prices = binance_ws.get_all_prices()
|
||||
if not current_prices:
|
||||
try:
|
||||
current_prices = binance_rest.get_all_prices()
|
||||
except Exception:
|
||||
current_prices = {}
|
||||
|
||||
page = render_sidebar(job.latest_results, db)
|
||||
|
||||
if page == "Signals":
|
||||
selected = st.session_state.get("selected_coin")
|
||||
if selected and selected in job.latest_results:
|
||||
coin_data = job.latest_results[selected]
|
||||
try:
|
||||
ohlcv = binance_rest.get_ohlcv(selected, interval="1h", limit=100)
|
||||
except Exception:
|
||||
ohlcv = None
|
||||
coin_news = news_client.filter_by_coin(news_client._cache, selected)
|
||||
coin_posts = social_client.filter_posts_by_coin(social_client._cache, selected)
|
||||
sentiment = social_client.simple_sentiment(coin_posts)
|
||||
ai_summary = job.ai_summaries.get(selected, "AI analysis not yet available.")
|
||||
render_detail(selected, coin_data, ohlcv, coin_news, sentiment, ai_summary)
|
||||
else:
|
||||
st.title("Crypto Signal Dashboard")
|
||||
st.info("Select a coin from the sidebar to view detailed analysis.")
|
||||
if job.latest_results:
|
||||
_render_overview(job.latest_results)
|
||||
elif page == "Portfolio":
|
||||
render_portfolio(portfolio, current_prices, db)
|
||||
|
||||
def _render_overview(results: dict):
|
||||
import pandas as pd
|
||||
st.subheader("Signal Overview")
|
||||
buy_coins = [r for r in results.values() if r["signal"] == "BUY"]
|
||||
sell_coins = [r for r in results.values() if r["signal"] == "SELL"]
|
||||
|
||||
col1, col2 = st.columns(2)
|
||||
with col1:
|
||||
st.markdown("### :green[BUY Signals]")
|
||||
if buy_coins:
|
||||
rows = [{"Coin": c["symbol"].replace("USDT",""), "Score": f"{c['composite']:.0f}",
|
||||
"Tech": f"{c['technical']:.0f}", "News": f"{c['news']:.0f}"}
|
||||
for c in sorted(buy_coins, key=lambda x: x["composite"], reverse=True)]
|
||||
st.dataframe(pd.DataFrame(rows), use_container_width=True, hide_index=True)
|
||||
else:
|
||||
st.info("No BUY signals currently")
|
||||
with col2:
|
||||
st.markdown("### :red[SELL Signals]")
|
||||
if sell_coins:
|
||||
rows = [{"Coin": c["symbol"].replace("USDT",""), "Score": f"{c['composite']:.0f}",
|
||||
"Tech": f"{c['technical']:.0f}", "News": f"{c['news']:.0f}"}
|
||||
for c in sorted(sell_coins, key=lambda x: x["composite"])]
|
||||
st.dataframe(pd.DataFrame(rows), use_container_width=True, hide_index=True)
|
||||
else:
|
||||
st.info("No SELL signals currently")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Reference in New Issue
Block a user