FIN_ASSISTANT / app.py
QAway-to
Merge branch 'main' into codex/analyze-project-repository-rhgm87
3a2cb28 unverified
import gradio as gr
from application.metrics_table import show_metrics_table
from application.portfolio_analyzer import PortfolioAnalyzer
from application.portfolio_comparer import PortfolioComparer
from infrastructure.llm_client import llm_service
from core.news_digest import fetch_crypto_news, summarize_news
from presentation.components.crypto_dashboard import (
build_crypto_dashboard,
) # Plotly dashboard + KPI-line
from presentation.components.comparison_table import show_comparison_table
from presentation.components.visual_comparison import (
build_comparison_chart,
build_volatility_chart,
preload_pairs,
) # Interactive pair comparison
# === CSS loader ===
def load_css(path: str) -> str:
with open(path, "r", encoding="utf-8") as f:
return f.read()
# === Styles ===
base_css = load_css("presentation/styles/themes/base.css")
crypto_css = load_css("presentation/styles/themes/crypto_dashboard.css")
# === Model setup ===
MODEL_NAME = "meta-llama/Meta-Llama-3.1-8B-Instruct"
analyzer = PortfolioAnalyzer(llm_service, MODEL_NAME)
comparer = PortfolioComparer(llm_service, MODEL_NAME)
# === Main Interface ===
with gr.Blocks(css=base_css) as demo:
gr.Markdown("## Investment Portfolio Analyzer", elem_classes="page-title")
gr.Markdown(
"Professional AI-driven analytics for investment and crypto markets.",
elem_classes="subtitle",
)
with gr.Tabs(elem_id="main_tabs"):
# --- Analysis ---
with gr.TabItem("Portfolio Insights"):
portfolio_input = gr.Textbox(
label="Portfolio ID or Link",
placeholder="Enter portfolio ID (uuid)",
value="b1ef37aa-5b9a-41b4-9394-8823f2de36bb",
)
analyze_btn = gr.Button("Run Analysis", variant="primary")
analyze_out = gr.HTML(value="", elem_id="analysis_output")
analyze_btn.click(
fn=analyzer.run,
inputs=portfolio_input,
outputs=analyze_out,
show_progress="minimal",
)
# --- Comparison Table ---
with gr.TabItem("Comparison Table"):
with gr.Row():
pid_a = gr.Textbox(
label="Portfolio A",
value="3852a354-e66e-4bc5-97e9-55124e31e687",
scale=1,
)
pid_b = gr.Textbox(
label="Portfolio B",
value="b1ef37aa-5b9a-41b4-9394-8823f2de36bb",
scale=1,
)
compare_btn = gr.Button("Load Comparison", variant="primary")
comp_table = gr.Dataframe(
label="Comparative Metrics",
wrap=True,
elem_id="comparison_table",
)
comp_comment = gr.HTML(elem_id="llm_comment_box")
compare_btn.click(
fn=show_comparison_table,
inputs=[pid_a, pid_b],
outputs=[comp_table, comp_comment],
show_progress="minimal",
)
# --- Metrics Table ---
# with gr.TabItem("Metrics Table"):
# metrics_in = gr.Textbox(
# label="Portfolio ID",
# value="b1ef37aa-5b9a-41b4-9394-8823f2de36bb",
# )
# metrics_btn = gr.Button("Load Metrics", variant="primary")
# metrics_out = gr.Dataframe(label="Portfolio Metrics", wrap=True)
# metrics_btn.click(fn=show_metrics_table, inputs=metrics_in, outputs=metrics_out)
# --- Assistant (temporarily disabled; duplicate removed) ---
# with gr.TabItem("Assistant"):
# chat_in = gr.Textbox(label="Ask about investments or analysis")
# chat_btn = gr.Button("Send Question", variant="primary")
# chat_out = gr.Textbox(label="AI Response", lines=8, interactive=False)
# chat_btn.click(
# fn=chatbot.run,
# inputs=chat_in,
# outputs=chat_out,
# show_progress="minimal",
# )
# --- Crypto News Digest ---
with gr.TabItem("Crypto News Digest"):
gr.Markdown("### πŸ—žοΈ Latest Crypto News (via NewsData.io)")
headlines_box = gr.Markdown("Fetching latest headlines...", elem_id="news_headlines")
ai_summary_box = gr.Textbox(
label="AI Market Summary",
lines=10,
interactive=False,
)
refresh_btn = gr.Button("πŸ”„ Refresh News", variant="primary")
def run_news():
headlines, ok = fetch_crypto_news()
if not ok:
return headlines, "⚠️ Summary will appear once fresh headlines are available."
summary = summarize_news(headlines)
return headlines, summary
refresh_btn.click(
fn=run_news,
inputs=None,
outputs=[headlines_box, ai_summary_box],
show_progress="minimal",
)
demo.load(
fn=run_news,
inputs=None,
outputs=[headlines_box, ai_summary_box],
)
# --- Visual Comparison (Interactive Plotly Edition) ---
with gr.TabItem("Visual Comparison"):
gr.Markdown("### πŸ“Š Market Pair Comparison β€” Interactive Plotly Edition")
available_pairs = [
("bitcoin", "ethereum"),
("ethereum", "bnb"),
("solana", "avalanche-2"),
("litecoin", "bitcoin-cash"),
("dogecoin", "shiba-inu"),
]
def _format_pair(pair: tuple[str, str]) -> str:
def _label(asset: str) -> str:
return asset.replace("-", " ").title()
a, b = pair
return f"{_label(a)} vs {_label(b)}"
pair_map = {_format_pair(pair): pair for pair in available_pairs}
default_label = _format_pair(available_pairs[0])
with gr.Row():
pair_selector = gr.Dropdown(
label="Select Pair for Comparison",
choices=list(pair_map.keys()),
value=default_label,
interactive=True,
scale=3,
)
normalize_toggle = gr.Checkbox(
label="Normalized Mode (%)",
value=False,
interactive=True,
scale=1,
)
price_plot = gr.Plot(label="Price Comparison")
vol_plot = gr.Plot(label="Volatility Comparison")
def update_visuals(selected_pair: str, normalized: bool):
pair = pair_map.get(selected_pair, available_pairs[0])
return (
build_comparison_chart(pair, normalized=normalized),
build_volatility_chart(pair),
)
pair_selector.change(
fn=update_visuals,
inputs=[pair_selector, normalize_toggle],
outputs=[price_plot, vol_plot],
)
normalize_toggle.change(
fn=update_visuals,
inputs=[pair_selector, normalize_toggle],
outputs=[price_plot, vol_plot],
)
def init_visuals():
preload_pairs(available_pairs)
return update_visuals(default_label, False)
demo.load(fn=init_visuals, inputs=None, outputs=[price_plot, vol_plot])
# --- Crypto Intelligence Dashboard (Plotly Edition + KPI line, auto-load) ---
with gr.TabItem("Crypto Intelligence Dashboard"):
gr.HTML(f"<style>{crypto_css}</style>")
with gr.Row(elem_id="kpi_row"):
kpi_line = gr.HTML(elem_id="kpi_line")
with gr.Row(elem_id="controls_row"):
top_slider = gr.Slider(
label="Top N coins",
minimum=20, maximum=100, step=10, value=50, scale=100,
)
with gr.Row(equal_height=True, elem_id="main_charts_row"):
with gr.Column(scale=70):
treemap_plot = gr.Plot(label="Market Composition")
with gr.Column(scale=30):
ai_box = gr.Textbox(label="AI Market Summary", lines=18, elem_id="ai_summary_sidebar")
with gr.Row(equal_height=True, elem_id="bottom_charts_row"):
movers_plot = gr.Plot(label="Top Movers", scale=50)
scatter_plot = gr.Plot(label="Market Cap vs Volume", scale=50)
def run_dash(n):
fig_treemap, fig_bar, fig_bubble, ai_comment, kpi_text = build_crypto_dashboard(n)
return fig_treemap, fig_bar, fig_bubble, ai_comment, f"<div class='kpi-line'>{kpi_text}</div>"
top_slider.change(
fn=run_dash,
inputs=top_slider,
outputs=[treemap_plot, movers_plot, scatter_plot, ai_box, kpi_line],
show_progress="minimal",
)
def init_crypto():
return run_dash(50)
demo.load(
fn=init_crypto,
inputs=None,
outputs=[treemap_plot, movers_plot, scatter_plot, ai_box, kpi_line],
)
gr.Markdown(
"<center><small style='color:#6e7681;'>Developed with Featherless.ai β€’ Powered by OpenAI-compatible API</small></center>",
elem_classes="footer",
)
if __name__ == "__main__":
demo.launch()