Spaces:
Running
Running
Merge pull request #1 from QAway-to/mainv1
Browse files- .gitattributes +35 -0
- .idea/.gitignore +3 -0
- .idea/TradeLinkAI.iml +15 -0
- .idea/inspectionProfiles/Project_Default.xml +13 -0
- .idea/inspectionProfiles/profiles_settings.xml +6 -0
- .idea/misc.xml +7 -0
- .idea/modules.xml +8 -0
- .idea/vcs.xml +6 -0
- README.md +13 -0
- app.py +154 -0
- config.py +20 -0
- core/__init__.py +2 -0
- core/analyzer.py +57 -0
- core/chat.py +35 -0
- core/comparer.py +70 -0
- core/comparison_table.py +70 -0
- core/crypto_dashboard.py +126 -0
- core/data_binance.py +22 -0
- core/data_coinlore.py +13 -0
- core/data_yfinance.py +16 -0
- core/metrics.py +34 -0
- core/multi_charts.py +0 -0
- core/styles/base.css +18 -0
- core/styles/crypto_dashboard.css +132 -0
- core/styles/multi_charts.css +4 -0
- core/ui_style.css +135 -0
- core/visual_comparison.py +94 -0
- core/visualization.py +58 -0
- prompts/__init__.py +2 -0
- prompts/reference_templates.py +39 -0
- prompts/system_prompts.py +54 -0
- requirements.txt +12 -0
- services/__init__.py +2 -0
- services/llm_client.py +39 -0
- services/output_api.py +76 -0
.gitattributes
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
*.7z filter=lfs diff=lfs merge=lfs -text
|
| 2 |
+
*.arrow filter=lfs diff=lfs merge=lfs -text
|
| 3 |
+
*.bin filter=lfs diff=lfs merge=lfs -text
|
| 4 |
+
*.bz2 filter=lfs diff=lfs merge=lfs -text
|
| 5 |
+
*.ckpt filter=lfs diff=lfs merge=lfs -text
|
| 6 |
+
*.ftz filter=lfs diff=lfs merge=lfs -text
|
| 7 |
+
*.gz filter=lfs diff=lfs merge=lfs -text
|
| 8 |
+
*.h5 filter=lfs diff=lfs merge=lfs -text
|
| 9 |
+
*.joblib filter=lfs diff=lfs merge=lfs -text
|
| 10 |
+
*.lfs.* filter=lfs diff=lfs merge=lfs -text
|
| 11 |
+
*.mlmodel filter=lfs diff=lfs merge=lfs -text
|
| 12 |
+
*.model filter=lfs diff=lfs merge=lfs -text
|
| 13 |
+
*.msgpack filter=lfs diff=lfs merge=lfs -text
|
| 14 |
+
*.npy filter=lfs diff=lfs merge=lfs -text
|
| 15 |
+
*.npz filter=lfs diff=lfs merge=lfs -text
|
| 16 |
+
*.onnx filter=lfs diff=lfs merge=lfs -text
|
| 17 |
+
*.ot filter=lfs diff=lfs merge=lfs -text
|
| 18 |
+
*.parquet filter=lfs diff=lfs merge=lfs -text
|
| 19 |
+
*.pb filter=lfs diff=lfs merge=lfs -text
|
| 20 |
+
*.pickle filter=lfs diff=lfs merge=lfs -text
|
| 21 |
+
*.pkl filter=lfs diff=lfs merge=lfs -text
|
| 22 |
+
*.pt filter=lfs diff=lfs merge=lfs -text
|
| 23 |
+
*.pth filter=lfs diff=lfs merge=lfs -text
|
| 24 |
+
*.rar filter=lfs diff=lfs merge=lfs -text
|
| 25 |
+
*.safetensors filter=lfs diff=lfs merge=lfs -text
|
| 26 |
+
saved_model/**/* filter=lfs diff=lfs merge=lfs -text
|
| 27 |
+
*.tar.* filter=lfs diff=lfs merge=lfs -text
|
| 28 |
+
*.tar filter=lfs diff=lfs merge=lfs -text
|
| 29 |
+
*.tflite filter=lfs diff=lfs merge=lfs -text
|
| 30 |
+
*.tgz filter=lfs diff=lfs merge=lfs -text
|
| 31 |
+
*.wasm filter=lfs diff=lfs merge=lfs -text
|
| 32 |
+
*.xz filter=lfs diff=lfs merge=lfs -text
|
| 33 |
+
*.zip filter=lfs diff=lfs merge=lfs -text
|
| 34 |
+
*.zst filter=lfs diff=lfs merge=lfs -text
|
| 35 |
+
*tfevents* filter=lfs diff=lfs merge=lfs -text
|
.idea/.gitignore
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Default ignored files
|
| 2 |
+
/shelf/
|
| 3 |
+
/workspace.xml
|
.idea/TradeLinkAI.iml
ADDED
|
@@ -0,0 +1,15 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
+
<module type="PYTHON_MODULE" version="4">
|
| 3 |
+
<component name="NewModuleRootManager">
|
| 4 |
+
<content url="file://$MODULE_DIR$" />
|
| 5 |
+
<orderEntry type="inheritedJdk" />
|
| 6 |
+
<orderEntry type="sourceFolder" forTests="false" />
|
| 7 |
+
</component>
|
| 8 |
+
<component name="PyDocumentationSettings">
|
| 9 |
+
<option name="format" value="PLAIN" />
|
| 10 |
+
<option name="myDocStringFormat" value="Plain" />
|
| 11 |
+
</component>
|
| 12 |
+
<component name="TestRunnerService">
|
| 13 |
+
<option name="PROJECT_TEST_RUNNER" value="py.test" />
|
| 14 |
+
</component>
|
| 15 |
+
</module>
|
.idea/inspectionProfiles/Project_Default.xml
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<component name="InspectionProjectProfileManager">
|
| 2 |
+
<profile version="1.0">
|
| 3 |
+
<option name="myName" value="Project Default" />
|
| 4 |
+
<inspection_tool class="MarkdownIncorrectTableFormatting" enabled="false" level="WEAK WARNING" enabled_by_default="false" />
|
| 5 |
+
<inspection_tool class="MarkdownIncorrectlyNumberedListItem" enabled="false" level="WARNING" enabled_by_default="false" />
|
| 6 |
+
<inspection_tool class="MarkdownLinkDestinationWithSpaces" enabled="false" level="WARNING" enabled_by_default="false" />
|
| 7 |
+
<inspection_tool class="MarkdownNoTableBorders" enabled="false" level="WARNING" enabled_by_default="false" />
|
| 8 |
+
<inspection_tool class="MarkdownOutdatedTableOfContents" enabled="false" level="WARNING" enabled_by_default="false" />
|
| 9 |
+
<inspection_tool class="MarkdownUnresolvedFileReference" enabled="false" level="WARNING" enabled_by_default="false" />
|
| 10 |
+
<inspection_tool class="MarkdownUnresolvedHeaderReference" enabled="false" level="WARNING" enabled_by_default="false" />
|
| 11 |
+
<inspection_tool class="MarkdownUnresolvedLinkLabel" enabled="false" level="WARNING" enabled_by_default="false" />
|
| 12 |
+
</profile>
|
| 13 |
+
</component>
|
.idea/inspectionProfiles/profiles_settings.xml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<component name="InspectionProjectProfileManager">
|
| 2 |
+
<settings>
|
| 3 |
+
<option name="USE_PROJECT_PROFILE" value="false" />
|
| 4 |
+
<version value="1.0" />
|
| 5 |
+
</settings>
|
| 6 |
+
</component>
|
.idea/misc.xml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
+
<project version="4">
|
| 3 |
+
<component name="Black">
|
| 4 |
+
<option name="sdkName" value="Python 3.12 (sdfs11)" />
|
| 5 |
+
</component>
|
| 6 |
+
<component name="ProjectRootManager" version="2" project-jdk-name="Python 3.13 (TradeLinkAI)" project-jdk-type="Python SDK" />
|
| 7 |
+
</project>
|
.idea/modules.xml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
+
<project version="4">
|
| 3 |
+
<component name="ProjectModuleManager">
|
| 4 |
+
<modules>
|
| 5 |
+
<module fileurl="file://$PROJECT_DIR$/.idea/TradeLinkAI.iml" filepath="$PROJECT_DIR$/.idea/TradeLinkAI.iml" />
|
| 6 |
+
</modules>
|
| 7 |
+
</component>
|
| 8 |
+
</project>
|
.idea/vcs.xml
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<?xml version="1.0" encoding="UTF-8"?>
|
| 2 |
+
<project version="4">
|
| 3 |
+
<component name="VcsDirectoryMappings">
|
| 4 |
+
<mapping directory="" vcs="Git" />
|
| 5 |
+
</component>
|
| 6 |
+
</project>
|
README.md
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
---
|
| 2 |
+
title: Finacial Assistance
|
| 3 |
+
emoji: 📊
|
| 4 |
+
colorFrom: blue
|
| 5 |
+
colorTo: red
|
| 6 |
+
sdk: gradio
|
| 7 |
+
sdk_version: 5.36.2
|
| 8 |
+
app_file: app.py
|
| 9 |
+
pinned: false
|
| 10 |
+
license: apache-2.0
|
| 11 |
+
---
|
| 12 |
+
|
| 13 |
+
Check out the configuration reference at https://huggingface.co/docs/hub/spaces-config-reference
|
app.py
ADDED
|
@@ -0,0 +1,154 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import gradio as gr
|
| 2 |
+
from services.llm_client import llm_service
|
| 3 |
+
from core.analyzer import PortfolioAnalyzer
|
| 4 |
+
from core.comparer import PortfolioComparer
|
| 5 |
+
from core.chat import ChatAssistant
|
| 6 |
+
from core.metrics import show_metrics_table
|
| 7 |
+
from core.crypto_dashboard import build_crypto_dashboard # Plotly dashboard + KPI-line
|
| 8 |
+
from core.visual_comparison import build_price_chart, build_volatility_chart # Interactive pair comparison
|
| 9 |
+
|
| 10 |
+
# === CSS loader ===
|
| 11 |
+
def load_css(path: str) -> str:
|
| 12 |
+
with open(path, "r", encoding="utf-8") as f:
|
| 13 |
+
return f.read()
|
| 14 |
+
|
| 15 |
+
# === Styles ===
|
| 16 |
+
base_css = load_css("core/styles/base.css")
|
| 17 |
+
crypto_css = load_css("core/styles/crypto_dashboard.css")
|
| 18 |
+
|
| 19 |
+
# === Model setup ===
|
| 20 |
+
MODEL_NAME = "meta-llama/Meta-Llama-3.1-8B-Instruct"
|
| 21 |
+
analyzer = PortfolioAnalyzer(llm_service, MODEL_NAME)
|
| 22 |
+
comparer = PortfolioComparer(llm_service, MODEL_NAME)
|
| 23 |
+
chatbot = ChatAssistant(llm_service, MODEL_NAME)
|
| 24 |
+
|
| 25 |
+
# === Main Interface ===
|
| 26 |
+
with gr.Blocks(css=base_css) as demo:
|
| 27 |
+
gr.Markdown("## Investment Portfolio Analyzer")
|
| 28 |
+
gr.Markdown(
|
| 29 |
+
"Professional AI-driven analytics for investment and crypto markets.",
|
| 30 |
+
elem_classes="subtitle",
|
| 31 |
+
)
|
| 32 |
+
|
| 33 |
+
with gr.Tabs():
|
| 34 |
+
# --- Analysis ---
|
| 35 |
+
with gr.TabItem("Analysis"):
|
| 36 |
+
portfolio_input = gr.Textbox(
|
| 37 |
+
label="Portfolio ID or Link",
|
| 38 |
+
placeholder="Enter portfolio ID (uuid)",
|
| 39 |
+
value="b1ef37aa-5b9a-41b4-9394-8823f2de36bb",
|
| 40 |
+
)
|
| 41 |
+
analyze_btn = gr.Button("Run Analysis", variant="primary")
|
| 42 |
+
analyze_out = gr.Textbox(label="Analysis Result", lines=15, elem_id="analysis_output")
|
| 43 |
+
analyze_btn.click(fn=analyzer.run, inputs=portfolio_input, outputs=analyze_out)
|
| 44 |
+
|
| 45 |
+
# --- Comparison Table ---
|
| 46 |
+
with gr.TabItem("Comparison Table"):
|
| 47 |
+
from core.comparison_table import show_comparison_table
|
| 48 |
+
pid_a = gr.Textbox(label="Portfolio A", value="3852a354-e66e-4bc5-97e9-55124e31e687")
|
| 49 |
+
pid_b = gr.Textbox(label="Portfolio B", value="b1ef37aa-5b9a-41b4-8823f2de36bb")
|
| 50 |
+
compare_btn = gr.Button("Load Comparison", variant="primary")
|
| 51 |
+
comp_table = gr.Dataframe(label="Comparative Metrics", wrap=True)
|
| 52 |
+
comp_comment = gr.Textbox(label="AI Commentary", lines=14, elem_id="llm_comment_box")
|
| 53 |
+
compare_btn.click(fn=show_comparison_table, inputs=[pid_a, pid_b], outputs=[comp_table, comp_comment])
|
| 54 |
+
|
| 55 |
+
# --- Assistant ---
|
| 56 |
+
with gr.TabItem("Assistant"):
|
| 57 |
+
chat_in = gr.Textbox(label="Ask about investments or analysis")
|
| 58 |
+
chat_btn = gr.Button("Send Question", variant="primary")
|
| 59 |
+
chat_out = gr.Textbox(label="AI Response", lines=8)
|
| 60 |
+
chat_btn.click(fn=chatbot.run, inputs=chat_in, outputs=chat_out)
|
| 61 |
+
|
| 62 |
+
# --- Metrics Table ---
|
| 63 |
+
with gr.TabItem("Metrics Table"):
|
| 64 |
+
metrics_in = gr.Textbox(label="Portfolio ID", value="b1ef37aa-5b9a-41b4-9394-8823f2de36bb")
|
| 65 |
+
metrics_btn = gr.Button("Load Metrics", variant="primary")
|
| 66 |
+
metrics_out = gr.Dataframe(label="Portfolio Metrics", wrap=True)
|
| 67 |
+
metrics_btn.click(fn=show_metrics_table, inputs=metrics_in, outputs=metrics_out)
|
| 68 |
+
|
| 69 |
+
# --- Visual Comparison (Interactive Plotly Edition) ---
|
| 70 |
+
with gr.TabItem("Visual Comparison"):
|
| 71 |
+
gr.Markdown("### 📊 Market Pair Comparison — Interactive Plotly Edition")
|
| 72 |
+
|
| 73 |
+
available_pairs = [
|
| 74 |
+
("bitcoin", "ethereum"),
|
| 75 |
+
("bitcoin", "solana"),
|
| 76 |
+
("ethereum", "bnb"),
|
| 77 |
+
("solana", "dogecoin"),
|
| 78 |
+
("bitcoin", "toncoin"),
|
| 79 |
+
]
|
| 80 |
+
|
| 81 |
+
pair_selector = gr.Dropdown(
|
| 82 |
+
label="Select Pair for Comparison",
|
| 83 |
+
choices=[f"{a.capitalize()} vs {b.capitalize()}" for a, b in available_pairs],
|
| 84 |
+
value="Bitcoin vs Ethereum",
|
| 85 |
+
interactive=True,
|
| 86 |
+
)
|
| 87 |
+
|
| 88 |
+
price_plot = gr.Plot(label="Price Comparison")
|
| 89 |
+
vol_plot = gr.Plot(label="Volatility Comparison")
|
| 90 |
+
|
| 91 |
+
def update_visuals(selected_pair: str):
|
| 92 |
+
for a, b in available_pairs:
|
| 93 |
+
if selected_pair.lower() == f"{a} vs {b}":
|
| 94 |
+
return build_price_chart((a, b)), build_volatility_chart((a, b))
|
| 95 |
+
return build_price_chart(("bitcoin", "ethereum")), build_volatility_chart(("bitcoin", "ethereum"))
|
| 96 |
+
|
| 97 |
+
pair_selector.change(fn=update_visuals, inputs=pair_selector, outputs=[price_plot, vol_plot])
|
| 98 |
+
|
| 99 |
+
def init_visuals():
|
| 100 |
+
return update_visuals("Bitcoin vs Ethereum")
|
| 101 |
+
|
| 102 |
+
demo.load(fn=init_visuals, inputs=None, outputs=[price_plot, vol_plot])
|
| 103 |
+
|
| 104 |
+
# --- Crypto Intelligence Dashboard (Plotly Edition + KPI line, auto-load) ---
|
| 105 |
+
with gr.TabItem("Crypto Intelligence Dashboard"):
|
| 106 |
+
gr.HTML(f"<style>{crypto_css}</style>")
|
| 107 |
+
|
| 108 |
+
with gr.Row(elem_id="kpi_row"):
|
| 109 |
+
kpi_line = gr.HTML(elem_id="kpi_line")
|
| 110 |
+
|
| 111 |
+
with gr.Row(elem_id="controls_row"):
|
| 112 |
+
top_slider = gr.Slider(
|
| 113 |
+
label="Top N coins",
|
| 114 |
+
minimum=20, maximum=100, step=10, value=50, scale=100,
|
| 115 |
+
)
|
| 116 |
+
|
| 117 |
+
with gr.Row(equal_height=True, elem_id="main_charts_row"):
|
| 118 |
+
with gr.Column(scale=70):
|
| 119 |
+
treemap_plot = gr.Plot(label="Market Composition")
|
| 120 |
+
with gr.Column(scale=30):
|
| 121 |
+
ai_box = gr.Textbox(label="AI Market Summary", lines=18, elem_id="ai_summary_sidebar")
|
| 122 |
+
|
| 123 |
+
with gr.Row(equal_height=True, elem_id="bottom_charts_row"):
|
| 124 |
+
movers_plot = gr.Plot(label="Top Movers", scale=50)
|
| 125 |
+
scatter_plot = gr.Plot(label="Market Cap vs Volume", scale=50)
|
| 126 |
+
|
| 127 |
+
def run_dash(n):
|
| 128 |
+
fig_treemap, fig_bar, fig_bubble, ai_comment, kpi_text = build_crypto_dashboard(n)
|
| 129 |
+
return fig_treemap, fig_bar, fig_bubble, ai_comment, f"<div class='kpi-line'>{kpi_text}</div>"
|
| 130 |
+
|
| 131 |
+
top_slider.change(
|
| 132 |
+
fn=run_dash,
|
| 133 |
+
inputs=top_slider,
|
| 134 |
+
outputs=[treemap_plot, movers_plot, scatter_plot, ai_box, kpi_line],
|
| 135 |
+
show_progress="minimal",
|
| 136 |
+
)
|
| 137 |
+
|
| 138 |
+
def init_crypto():
|
| 139 |
+
return run_dash(50)
|
| 140 |
+
|
| 141 |
+
demo.load(
|
| 142 |
+
fn=init_crypto,
|
| 143 |
+
inputs=None,
|
| 144 |
+
outputs=[treemap_plot, movers_plot, scatter_plot, ai_box, kpi_line],
|
| 145 |
+
)
|
| 146 |
+
|
| 147 |
+
gr.Markdown("---")
|
| 148 |
+
gr.Markdown(
|
| 149 |
+
"<center><small style='color:#6e7681;'>Developed with Featherless.ai • Powered by OpenAI-compatible API</small></center>",
|
| 150 |
+
elem_classes="footer",
|
| 151 |
+
)
|
| 152 |
+
|
| 153 |
+
if __name__ == "__main__":
|
| 154 |
+
demo.launch()
|
config.py
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
🇬🇧 Module: config.py
|
| 3 |
+
Purpose: Central configuration for environment variables and constants.
|
| 4 |
+
|
| 5 |
+
🇷🇺 Модуль: config.py
|
| 6 |
+
Назначение: централизованная конфигурация переменных окружения и констант проекта.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
|
| 11 |
+
# === Featherless.ai Configuration ===
|
| 12 |
+
FEATHERLESS_API_KEY = os.getenv("featherless")
|
| 13 |
+
FEATHERLESS_MODEL = "meta-llama/Meta-Llama-3.1-8B-Instruct"
|
| 14 |
+
|
| 15 |
+
# === External API Configuration ===
|
| 16 |
+
EXTERNAL_API_URL = os.getenv("EXTERNAL_API_URL")
|
| 17 |
+
|
| 18 |
+
# === Request / Connection Settings ===
|
| 19 |
+
REQUEST_TIMEOUT = 15
|
| 20 |
+
DEBUG = os.getenv("DEBUG", "false").lower() == "true"
|
core/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# __init__.py
|
| 2 |
+
# Marks this directory as a Python package.
|
core/analyzer.py
ADDED
|
@@ -0,0 +1,57 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
🇬🇧 Module: analyzer.py()
|
| 3 |
+
Purpose: Handles single-portfolio analysis using LLM. Fetches metrics, builds prompt, streams reasoning.
|
| 4 |
+
|
| 5 |
+
🇷🇺 Модуль: analyzer.py
|
| 6 |
+
Назначение: анализ одного инвестиционного портфеля с использованием LLM. Получает метрики, формирует промпт, возвращает потоковый ответ.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import asyncio
|
| 10 |
+
from typing import Generator
|
| 11 |
+
from services.output_api import extract_portfolio_id, fetch_metrics_async
|
| 12 |
+
from services.llm_client import llm_service
|
| 13 |
+
from prompts.system_prompts import ANALYSIS_SYSTEM_PROMPT
|
| 14 |
+
from prompts.reference_templates import REFERENCE_PROMPT
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class PortfolioAnalyzer:
|
| 18 |
+
"""Main use-case class for analyzing a single portfolio."""
|
| 19 |
+
|
| 20 |
+
def __init__(self, llm=llm_service, model_name: str = "meta-llama/Meta-Llama-3.1-8B-Instruct"):
|
| 21 |
+
self.llm = llm
|
| 22 |
+
self.model_name = model_name
|
| 23 |
+
|
| 24 |
+
def run(self, text: str) -> Generator[str, None, None]:
|
| 25 |
+
"""Stream analysis result step by step."""
|
| 26 |
+
portfolio_id = extract_portfolio_id(text)
|
| 27 |
+
if not portfolio_id:
|
| 28 |
+
yield "❗ Please enter a portfolio ID."
|
| 29 |
+
return
|
| 30 |
+
|
| 31 |
+
yield "⏳ Working..."
|
| 32 |
+
try:
|
| 33 |
+
metrics = asyncio.run(fetch_metrics_async(portfolio_id))
|
| 34 |
+
except Exception as e:
|
| 35 |
+
yield f"❌ Fail to collect metrics: {e}"
|
| 36 |
+
return
|
| 37 |
+
|
| 38 |
+
if not metrics:
|
| 39 |
+
yield "❗ Metrics can't be collected."
|
| 40 |
+
return
|
| 41 |
+
|
| 42 |
+
metrics_text = ", ".join(f"{k}: {v}" for k, v in metrics.items())
|
| 43 |
+
prompt = f"{REFERENCE_PROMPT}\n\nИспользуй эти данные для анализа:\n{metrics_text}"
|
| 44 |
+
|
| 45 |
+
try:
|
| 46 |
+
messages = [
|
| 47 |
+
{"role": "system", "content": ANALYSIS_SYSTEM_PROMPT},
|
| 48 |
+
{"role": "user", "content": prompt},
|
| 49 |
+
]
|
| 50 |
+
|
| 51 |
+
partial = ""
|
| 52 |
+
for delta in self.llm.stream_chat(messages=messages, model=self.model_name):
|
| 53 |
+
partial += delta
|
| 54 |
+
yield partial
|
| 55 |
+
|
| 56 |
+
except Exception as e:
|
| 57 |
+
yield f"❌ LLM's error: {e}"
|
core/chat.py
ADDED
|
@@ -0,0 +1,35 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
🇬🇧 Module: chat.py
|
| 3 |
+
Purpose: General chat interface for user questions about investments or portfolios.
|
| 4 |
+
|
| 5 |
+
🇷🇺 Модуль: chat.py
|
| 6 |
+
Назначение: общий чат-помощник для ответов на вопросы об инвестициях и портфелях.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
from typing import Generator
|
| 10 |
+
from services.llm_client import llm_service
|
| 11 |
+
from prompts.system_prompts import GENERAL_CONTEXT
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
class ChatAssistant:
|
| 15 |
+
"""Handles general user dialogue via LLM."""
|
| 16 |
+
|
| 17 |
+
def __init__(self, llm=llm_service, model_name: str = "meta-llama/Meta-Llama-3.1-8B-Instruct"):
|
| 18 |
+
self.llm = llm
|
| 19 |
+
self.model_name = model_name
|
| 20 |
+
|
| 21 |
+
def run(self, user_input: str) -> Generator[str, None, None]:
|
| 22 |
+
"""Stream chat responses."""
|
| 23 |
+
messages = [
|
| 24 |
+
{"role": "system", "content": GENERAL_CONTEXT},
|
| 25 |
+
{"role": "user", "content": user_input},
|
| 26 |
+
]
|
| 27 |
+
|
| 28 |
+
try:
|
| 29 |
+
partial = ""
|
| 30 |
+
for delta in self.llm.stream_chat(messages=messages, model=self.model_name):
|
| 31 |
+
partial += delta
|
| 32 |
+
yield partial
|
| 33 |
+
|
| 34 |
+
except Exception as e:
|
| 35 |
+
yield f"❌ Ошибка при генерации ответа: {e}"
|
core/comparer.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
🇬🇧 Module: comparer.py()
|
| 3 |
+
Purpose: Compares two portfolios using LLM. Fetches metrics for both and builds a unified comparison prompt.
|
| 4 |
+
|
| 5 |
+
🇷🇺 Модуль: comparer.py
|
| 6 |
+
Назначение: сравнение двух инвестиционных портфелей с помощью LLM. Получает метрики обоих портфелей, формирует промпт и возвращает потоковый результат.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import asyncio
|
| 10 |
+
from typing import Generator
|
| 11 |
+
from services.output_api import extract_portfolio_id, fetch_metrics_async
|
| 12 |
+
from services.llm_client import llm_service
|
| 13 |
+
from prompts.system_prompts import COMPARISON_SYSTEM_PROMPT
|
| 14 |
+
from prompts.reference_templates import REFERENCE_COMPARISON_PROMPT
|
| 15 |
+
|
| 16 |
+
|
| 17 |
+
class PortfolioComparer:
|
| 18 |
+
"""Main use-case class for comparing two portfolios."""
|
| 19 |
+
|
| 20 |
+
def __init__(self, llm=llm_service, model_name: str = "meta-llama/Meta-Llama-3.1-8B-Instruct"):
|
| 21 |
+
self.llm = llm
|
| 22 |
+
self.model_name = model_name
|
| 23 |
+
|
| 24 |
+
def run(self, text1: str, text2: str) -> Generator[str, None, None]:
|
| 25 |
+
"""Stream comparison results between two portfolios."""
|
| 26 |
+
id1 = extract_portfolio_id(text1)
|
| 27 |
+
id2 = extract_portfolio_id(text2)
|
| 28 |
+
|
| 29 |
+
if text1 == text2:
|
| 30 |
+
yield "❗ Please, give me a two difference portfolio ID."
|
| 31 |
+
return
|
| 32 |
+
if not id1 or not id2:
|
| 33 |
+
yield "❗ One of two portfolios is empty or incorrect."
|
| 34 |
+
return
|
| 35 |
+
|
| 36 |
+
yield "⏳ Working..."
|
| 37 |
+
try:
|
| 38 |
+
m1 = asyncio.run(fetch_metrics_async(id1))
|
| 39 |
+
m2 = asyncio.run(fetch_metrics_async(id2))
|
| 40 |
+
except Exception as e:
|
| 41 |
+
yield f"❌ There are issue via collecting data: {e}"
|
| 42 |
+
return
|
| 43 |
+
|
| 44 |
+
if not m1 or not m2:
|
| 45 |
+
yield "❗ One of two portfolios is empty or has incorrect data."
|
| 46 |
+
return
|
| 47 |
+
|
| 48 |
+
m1_text = ", ".join(f"{k}: {v}" for k, v in m1.items())
|
| 49 |
+
m2_text = ", ".join(f"{k}: {v}" for k, v in m2.items())
|
| 50 |
+
|
| 51 |
+
prompt = (
|
| 52 |
+
f"{REFERENCE_COMPARISON_PROMPT}\n"
|
| 53 |
+
f"Используй эти данные для сравнения:\n"
|
| 54 |
+
f"Портфель A: {m1_text}\n"
|
| 55 |
+
f"Портфель B: {m2_text}"
|
| 56 |
+
)
|
| 57 |
+
|
| 58 |
+
try:
|
| 59 |
+
messages = [
|
| 60 |
+
{"role": "system", "content": COMPARISON_SYSTEM_PROMPT},
|
| 61 |
+
{"role": "user", "content": prompt},
|
| 62 |
+
]
|
| 63 |
+
|
| 64 |
+
partial = ""
|
| 65 |
+
for delta in self.llm.stream_chat(messages=messages, model=self.model_name):
|
| 66 |
+
partial += delta
|
| 67 |
+
yield partial
|
| 68 |
+
|
| 69 |
+
except Exception as e:
|
| 70 |
+
yield f"❌ Ошибка при сравнении портфелей через LLM: {e}"
|
core/comparison_table.py
ADDED
|
@@ -0,0 +1,70 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
🇬🇧 Module: comparison_table.py
|
| 3 |
+
Purpose: Generates comparative DataFrame for two portfolios and an LLM commentary.
|
| 4 |
+
|
| 5 |
+
🇷🇺 Модуль: comparison_table.py
|
| 6 |
+
Назначение: создаёт сравнительную таблицу метрик двух портфелей и комментарий LLM.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import pandas as pd
|
| 10 |
+
import asyncio
|
| 11 |
+
from services.output_api import fetch_metrics_async, extract_portfolio_id
|
| 12 |
+
from services.llm_client import llm_service
|
| 13 |
+
from prompts.system_prompts import COMPARISON_SYSTEM_PROMPT
|
| 14 |
+
|
| 15 |
+
|
| 16 |
+
def show_comparison_table(portfolio_a: str, portfolio_b: str):
|
| 17 |
+
"""Public Gradio entry: returns both a DataFrame and LLM commentary."""
|
| 18 |
+
pid_a = extract_portfolio_id(portfolio_a)
|
| 19 |
+
pid_b = extract_portfolio_id(portfolio_b)
|
| 20 |
+
if not pid_a or not pid_b:
|
| 21 |
+
return "❌ Invalid portfolio IDs.", "No commentary available."
|
| 22 |
+
|
| 23 |
+
try:
|
| 24 |
+
df, commentary = asyncio.run(_build_comparison_with_comment(pid_a, pid_b))
|
| 25 |
+
return df, commentary
|
| 26 |
+
except Exception as e:
|
| 27 |
+
return f"❌ Error building comparison table: {e}", "❌ LLM analysis failed."
|
| 28 |
+
|
| 29 |
+
|
| 30 |
+
async def _build_comparison_with_comment(p1: str, p2: str):
|
| 31 |
+
"""Async helper: builds table and gets commentary."""
|
| 32 |
+
m1 = await fetch_metrics_async(p1)
|
| 33 |
+
m2 = await fetch_metrics_async(p2)
|
| 34 |
+
if not m1 or not m2:
|
| 35 |
+
raise ValueError("Metrics unavailable for one or both portfolios.")
|
| 36 |
+
|
| 37 |
+
all_keys = sorted(set(m1.keys()) | set(m2.keys()))
|
| 38 |
+
rows = []
|
| 39 |
+
for k in all_keys:
|
| 40 |
+
v1 = m1.get(k, 0)
|
| 41 |
+
v2 = m2.get(k, 0)
|
| 42 |
+
diff = v1 - v2
|
| 43 |
+
symbol = "▲" if diff > 0 else "▼" if diff < 0 else "—"
|
| 44 |
+
rows.append({
|
| 45 |
+
"Metric": k,
|
| 46 |
+
"Portfolio A": round(v1, 3),
|
| 47 |
+
"Portfolio B": round(v2, 3),
|
| 48 |
+
"Δ Difference": f"{symbol} {diff:+.3f}"
|
| 49 |
+
})
|
| 50 |
+
df = pd.DataFrame(rows, columns=["Metric", "Portfolio A", "Portfolio B", "Δ Difference"])
|
| 51 |
+
|
| 52 |
+
# Generate LLM commentary
|
| 53 |
+
summary = "\n".join(f"{r['Metric']}: {r['Δ Difference']}" for r in rows)
|
| 54 |
+
prompt = (
|
| 55 |
+
f"{COMPARISON_SYSTEM_PROMPT}\n"
|
| 56 |
+
f"Compare and explain the differences between Portfolio A and B:\n{summary}\n"
|
| 57 |
+
f"Write your insights as a concise professional commentary."
|
| 58 |
+
)
|
| 59 |
+
|
| 60 |
+
commentary = ""
|
| 61 |
+
for delta in llm_service.stream_chat(
|
| 62 |
+
messages=[
|
| 63 |
+
{"role": "system", "content": "You are an investment portfolio analyst."},
|
| 64 |
+
{"role": "user", "content": prompt},
|
| 65 |
+
],
|
| 66 |
+
model="meta-llama/Meta-Llama-3.1-8B-Instruct",
|
| 67 |
+
):
|
| 68 |
+
commentary += delta
|
| 69 |
+
|
| 70 |
+
return df, commentary
|
core/crypto_dashboard.py
ADDED
|
@@ -0,0 +1,126 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Crypto Dashboard — Plotly Edition (clean layout)
|
| 3 |
+
• убраны colorbar заголовки (percent_change_*)
|
| 4 |
+
• уменьшены отступы KPI
|
| 5 |
+
• без глобального Markdown-заголовка
|
| 6 |
+
"""
|
| 7 |
+
import requests
|
| 8 |
+
import pandas as pd
|
| 9 |
+
import plotly.express as px
|
| 10 |
+
from services.llm_client import llm_service
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def fetch_coinlore_data(limit=100):
|
| 14 |
+
url = "https://api.coinlore.net/api/tickers/"
|
| 15 |
+
data = requests.get(url).json()["data"]
|
| 16 |
+
df = pd.DataFrame(data)
|
| 17 |
+
for col in ["price_usd", "market_cap_usd", "volume24",
|
| 18 |
+
"percent_change_1h", "percent_change_24h", "percent_change_7d"]:
|
| 19 |
+
df[col] = pd.to_numeric(df[col], errors="coerce")
|
| 20 |
+
return df.head(limit)
|
| 21 |
+
|
| 22 |
+
|
| 23 |
+
def _kpi_line(df) -> str:
|
| 24 |
+
"""Формирует компактную KPI-строку без лишних пробелов"""
|
| 25 |
+
tracked = ["BTC", "ETH", "SOL", "DOGE"]
|
| 26 |
+
parts = []
|
| 27 |
+
for sym in tracked:
|
| 28 |
+
row = df[df["symbol"] == sym]
|
| 29 |
+
if row.empty:
|
| 30 |
+
continue
|
| 31 |
+
price = float(row["price_usd"])
|
| 32 |
+
ch = float(row["percent_change_24h"])
|
| 33 |
+
arrow = "↑" if ch > 0 else "↓"
|
| 34 |
+
color = "#4ade80" if ch > 0 else "#f87171"
|
| 35 |
+
parts.append(
|
| 36 |
+
f"<b>{sym}</b> ${price:,.0f} "
|
| 37 |
+
f"<span style='color:{color}'>{arrow} {abs(ch):.2f}%</span>"
|
| 38 |
+
)
|
| 39 |
+
return " , ".join(parts)
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
def build_crypto_dashboard(top_n=50):
|
| 43 |
+
df = fetch_coinlore_data(top_n)
|
| 44 |
+
|
| 45 |
+
# === Treemap ===
|
| 46 |
+
fig_treemap = px.treemap(
|
| 47 |
+
df,
|
| 48 |
+
path=["symbol"],
|
| 49 |
+
values="market_cap_usd",
|
| 50 |
+
color="percent_change_24h",
|
| 51 |
+
color_continuous_scale="RdYlGn",
|
| 52 |
+
height=420,
|
| 53 |
+
)
|
| 54 |
+
fig_treemap.update_layout(
|
| 55 |
+
title=None,
|
| 56 |
+
template="plotly_dark",
|
| 57 |
+
coloraxis_colorbar=dict(title=None), # 🔹 убираем надпись percent_change_24h
|
| 58 |
+
margin=dict(l=5, r=5, t=5, b=5),
|
| 59 |
+
paper_bgcolor="rgba(0,0,0,0)",
|
| 60 |
+
plot_bgcolor="rgba(0,0,0,0)",
|
| 61 |
+
)
|
| 62 |
+
|
| 63 |
+
# === Bar chart (Top gainers) ===
|
| 64 |
+
top = df.sort_values("percent_change_24h", ascending=False).head(12)
|
| 65 |
+
fig_bar = px.bar(
|
| 66 |
+
top,
|
| 67 |
+
x="percent_change_24h",
|
| 68 |
+
y="symbol",
|
| 69 |
+
orientation="h",
|
| 70 |
+
color="percent_change_24h",
|
| 71 |
+
color_continuous_scale="Blues",
|
| 72 |
+
height=320,
|
| 73 |
+
)
|
| 74 |
+
fig_bar.update_layout(
|
| 75 |
+
title=None,
|
| 76 |
+
template="plotly_dark",
|
| 77 |
+
coloraxis_colorbar=dict(title=None), # 🔹 убираем надпись percent_change_24h
|
| 78 |
+
margin=dict(l=40, r=10, t=5, b=18),
|
| 79 |
+
paper_bgcolor="rgba(0,0,0,0)",
|
| 80 |
+
plot_bgcolor="rgba(0,0,0,0)",
|
| 81 |
+
)
|
| 82 |
+
|
| 83 |
+
# === Scatter (Market Cap vs Volume) ===
|
| 84 |
+
fig_bubble = px.scatter(
|
| 85 |
+
df.head(60),
|
| 86 |
+
x="market_cap_usd",
|
| 87 |
+
y="volume24",
|
| 88 |
+
size="price_usd",
|
| 89 |
+
color="percent_change_7d",
|
| 90 |
+
hover_name="symbol",
|
| 91 |
+
log_x=True,
|
| 92 |
+
log_y=True,
|
| 93 |
+
color_continuous_scale="RdYlGn",
|
| 94 |
+
height=320,
|
| 95 |
+
)
|
| 96 |
+
fig_bubble.update_layout(
|
| 97 |
+
title=None,
|
| 98 |
+
template="plotly_dark",
|
| 99 |
+
coloraxis_colorbar=dict(title=None), # 🔹 убираем надпись percent_change_7d
|
| 100 |
+
margin=dict(l=36, r=10, t=5, b=18),
|
| 101 |
+
paper_bgcolor="rgba(0,0,0,0)",
|
| 102 |
+
plot_bgcolor="rgba(0,0,0,0)",
|
| 103 |
+
)
|
| 104 |
+
|
| 105 |
+
# === LLM summary ===
|
| 106 |
+
summary = _ai_summary(df)
|
| 107 |
+
kpi_text = _kpi_line(df)
|
| 108 |
+
return fig_treemap, fig_bar, fig_bubble, summary, kpi_text
|
| 109 |
+
|
| 110 |
+
|
| 111 |
+
def _ai_summary(df):
|
| 112 |
+
leaders = df.sort_values("percent_change_24h", ascending=False).head(3)["symbol"].tolist()
|
| 113 |
+
laggards = df.sort_values("percent_change_24h").head(3)["symbol"].tolist()
|
| 114 |
+
prompt = f"""
|
| 115 |
+
Summarize today's crypto market based on Coinlore data.
|
| 116 |
+
Top gainers: {', '.join(leaders)}.
|
| 117 |
+
Top losers: {', '.join(laggards)}.
|
| 118 |
+
Include: overall sentiment, volatility/liquidity, short-term outlook.
|
| 119 |
+
"""
|
| 120 |
+
text = ""
|
| 121 |
+
for delta in llm_service.stream_chat(
|
| 122 |
+
messages=[{"role": "user", "content": prompt}],
|
| 123 |
+
model="meta-llama/Meta-Llama-3.1-8B-Instruct",
|
| 124 |
+
):
|
| 125 |
+
text += delta
|
| 126 |
+
return text
|
core/data_binance.py
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Binance API Handler – public OHLC data
|
| 3 |
+
"""
|
| 4 |
+
import requests, pandas as pd
|
| 5 |
+
|
| 6 |
+
BINANCE = "https://api.binance.com/api/v3"
|
| 7 |
+
|
| 8 |
+
def get_binance_data(symbols=None, limit=500):
|
| 9 |
+
if symbols is None:
|
| 10 |
+
symbols = ["BTCUSDT", "ETHUSDT", "BNBUSDT"]
|
| 11 |
+
frames = []
|
| 12 |
+
for s in symbols:
|
| 13 |
+
url = f"{BINANCE}/klines?symbol={s}&interval=1d&limit={limit}"
|
| 14 |
+
r = requests.get(url).json()
|
| 15 |
+
df = pd.DataFrame(r, columns=[
|
| 16 |
+
"t","o","h","l","c","v","ct","qv","tr","tb","tbv","ig"])
|
| 17 |
+
df = df[["t","c"]].rename(columns={"t":"timestamp","c":"close"})
|
| 18 |
+
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms")
|
| 19 |
+
df["close"] = df["close"].astype(float)
|
| 20 |
+
df["symbol"] = s
|
| 21 |
+
frames.append(df)
|
| 22 |
+
return pd.concat(frames)
|
core/data_coinlore.py
ADDED
|
@@ -0,0 +1,13 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Coinlore API Handler – market overview
|
| 3 |
+
"""
|
| 4 |
+
import requests, pandas as pd
|
| 5 |
+
|
| 6 |
+
def get_coinlore_data():
|
| 7 |
+
url = "https://api.coinlore.net/api/tickers/"
|
| 8 |
+
data = requests.get(url).json()["data"]
|
| 9 |
+
df = pd.DataFrame(data)
|
| 10 |
+
df["price_usd"] = df["price_usd"].astype(float)
|
| 11 |
+
df["market_cap_usd"] = df["market_cap_usd"].astype(float)
|
| 12 |
+
df["percent_change_24h"] = df["percent_change_24h"].astype(float)
|
| 13 |
+
return df
|
core/data_yfinance.py
ADDED
|
@@ -0,0 +1,16 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Yahoo Finance API Handler – historical 5-10 years
|
| 3 |
+
"""
|
| 4 |
+
import yfinance as yf, pandas as pd
|
| 5 |
+
|
| 6 |
+
def get_yf_history(symbols=None, period="2y"):
|
| 7 |
+
if symbols is None:
|
| 8 |
+
symbols = ["BTC-USD","ETH-USD","BNB-USD"]
|
| 9 |
+
frames = []
|
| 10 |
+
for s in symbols:
|
| 11 |
+
df = yf.download(s, period=period, interval="1d", progress=False)
|
| 12 |
+
df = df.reset_index()[["Date","Close"]]
|
| 13 |
+
df.columns = ["timestamp","close"]
|
| 14 |
+
df["symbol"] = s.replace("-USD","USDT")
|
| 15 |
+
frames.append(df)
|
| 16 |
+
return pd.concat(frames)
|
core/metrics.py
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
🇬🇧 Module: metrics.py
|
| 3 |
+
Purpose: Provides async utilities to fetch and display portfolio metrics as a DataFrame.
|
| 4 |
+
|
| 5 |
+
🇷🇺 Модуль: metrics.py
|
| 6 |
+
Назначение: предоставляет асинхронные функции для получения и отображения метрик портфеля в виде DataFrame.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import pandas as pd
|
| 10 |
+
import asyncio
|
| 11 |
+
from services.output_api import extract_portfolio_id, fetch_metrics_async
|
| 12 |
+
|
| 13 |
+
|
| 14 |
+
def show_metrics_table(portfolio_input: str):
|
| 15 |
+
"""Fetch portfolio metrics and return them as a DataFrame for Gradio."""
|
| 16 |
+
pid = extract_portfolio_id(portfolio_input)
|
| 17 |
+
if not pid:
|
| 18 |
+
return "❌ Invalid portfolioId format."
|
| 19 |
+
|
| 20 |
+
try:
|
| 21 |
+
df = asyncio.run(_get_metrics_df(pid))
|
| 22 |
+
return df
|
| 23 |
+
except Exception as e:
|
| 24 |
+
return f"❌ Error fetching metrics: {e}"
|
| 25 |
+
|
| 26 |
+
|
| 27 |
+
async def _get_metrics_df(portfolio_id: str) -> pd.DataFrame:
|
| 28 |
+
"""Internal helper to asynchronously get metrics."""
|
| 29 |
+
metrics = await fetch_metrics_async(portfolio_id)
|
| 30 |
+
if not metrics:
|
| 31 |
+
raise ValueError("No metrics found for given portfolio.")
|
| 32 |
+
|
| 33 |
+
df = pd.DataFrame(list(metrics.items()), columns=["Metric", "Value"])
|
| 34 |
+
return df
|
core/multi_charts.py
ADDED
|
File without changes
|
core/styles/base.css
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* === Global Layout === */
|
| 2 |
+
.gradio-container { font-family: 'Inter', sans-serif; background:#0d1117 !important; }
|
| 3 |
+
[data-testid="block-container"] { max-width:1180px !important; margin:auto !important; }
|
| 4 |
+
h2, h3, .gr-markdown { color:#f0f6fc !important; font-weight:600 !important; }
|
| 5 |
+
|
| 6 |
+
/* buttons / slider */
|
| 7 |
+
.gr-button {
|
| 8 |
+
border-radius:6px !important; font-weight:600 !important; height:52px !important;
|
| 9 |
+
background:linear-gradient(90deg,#4f46e5,#6366f1) !important; border:none !important;
|
| 10 |
+
box-shadow:0 2px 4px rgba(0,0,0,.25);
|
| 11 |
+
}
|
| 12 |
+
.gr-slider { height:52px !important; }
|
| 13 |
+
.gr-slider input[type=range]::-webkit-slider-thumb { background:#6366f1 !important; }
|
| 14 |
+
|
| 15 |
+
/* tables */
|
| 16 |
+
.gr-dataframe table { width:100% !important; color:#c9d1d9 !important; background:#161b22 !important; }
|
| 17 |
+
.gr-dataframe th { background:#21262d !important; color:#f0f6fc !important; border-bottom:1px solid #30363d !important; }
|
| 18 |
+
.gr-dataframe td { border-top:1px solid #30363d !important; padding:8px !important; }
|
core/styles/crypto_dashboard.css
ADDED
|
@@ -0,0 +1,132 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* === Убираем отступ между вкладками и контентом === */
|
| 2 |
+
[data-testid="tabitem"] {
|
| 3 |
+
margin-top: 0 !important;
|
| 4 |
+
padding-top: 0 !important;
|
| 5 |
+
}
|
| 6 |
+
|
| 7 |
+
/* === KPI fixed line (строка с метриками) === */
|
| 8 |
+
#kpi_row {
|
| 9 |
+
margin-top: 0 !important;
|
| 10 |
+
margin-bottom: 0 !important;
|
| 11 |
+
padding: 0 !important;
|
| 12 |
+
}
|
| 13 |
+
#kpi_line {
|
| 14 |
+
width: 100%;
|
| 15 |
+
margin: 0 !important;
|
| 16 |
+
padding: 2px 6px !important;
|
| 17 |
+
background: transparent;
|
| 18 |
+
color: #f0f6fc;
|
| 19 |
+
font-family: 'JetBrains Mono', monospace;
|
| 20 |
+
font-size: 14px;
|
| 21 |
+
line-height: 1.2;
|
| 22 |
+
white-space: nowrap;
|
| 23 |
+
overflow: hidden;
|
| 24 |
+
text-overflow: ellipsis;
|
| 25 |
+
display: flex;
|
| 26 |
+
align-items: center;
|
| 27 |
+
justify-content: flex-start;
|
| 28 |
+
min-height: 24px;
|
| 29 |
+
border-bottom: 1px solid #30363d;
|
| 30 |
+
transition: all 0.2s ease-in-out;
|
| 31 |
+
}
|
| 32 |
+
|
| 33 |
+
/* KPI items */
|
| 34 |
+
.kpi-item {
|
| 35 |
+
margin-right: 14px;
|
| 36 |
+
letter-spacing: 0.2px;
|
| 37 |
+
transition: opacity 0.2s ease;
|
| 38 |
+
}
|
| 39 |
+
.kpi-item:hover {
|
| 40 |
+
opacity: 0.85;
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
/* === Controls (slider + button) — минимальные отступы === */
|
| 44 |
+
#controls_row {
|
| 45 |
+
margin-top: 2px !important;
|
| 46 |
+
margin-bottom: 2px !important;
|
| 47 |
+
padding: 0 !important;
|
| 48 |
+
}
|
| 49 |
+
#controls_row .gr-button,
|
| 50 |
+
#controls_row .gr-slider {
|
| 51 |
+
margin-top: 0 !important;
|
| 52 |
+
margin-bottom: 0 !important;
|
| 53 |
+
}
|
| 54 |
+
|
| 55 |
+
/* === Plot containers — плотное выравнивание === */
|
| 56 |
+
[data-testid="plot-container"] {
|
| 57 |
+
width: 100% !important;
|
| 58 |
+
margin: 0 !important;
|
| 59 |
+
padding: 0 !important;
|
| 60 |
+
background: transparent !important;
|
| 61 |
+
border: none !important;
|
| 62 |
+
}
|
| 63 |
+
[data-testid="plot-container"] .wrap {
|
| 64 |
+
margin: 0 !important;
|
| 65 |
+
padding: 0 !important;
|
| 66 |
+
background: transparent !important;
|
| 67 |
+
box-shadow: none !important;
|
| 68 |
+
}
|
| 69 |
+
[data-testid="plot-container"] svg {
|
| 70 |
+
display: none !important;
|
| 71 |
+
}
|
| 72 |
+
|
| 73 |
+
/* === Скрываем только встроенные заголовки Plotly и подписи colorbar === */
|
| 74 |
+
.js-plotly-plot .plotly .gtitle,
|
| 75 |
+
.js-plotly-plot .plotly .g-xtitle,
|
| 76 |
+
.js-plotly-plot .plotly .g-ytitle,
|
| 77 |
+
.js-plotly-plot .colorbar-title,
|
| 78 |
+
.js-plotly-plot .colorbar .cbtitle,
|
| 79 |
+
.js-plotly-plot .colorbar .cbTitle {
|
| 80 |
+
display: none !important;
|
| 81 |
+
visibility: hidden !important;
|
| 82 |
+
}
|
| 83 |
+
|
| 84 |
+
/* === Явно оставляем видимыми подписи осей, тики, легенду и аннотации === */
|
| 85 |
+
.js-plotly-plot .legend text,
|
| 86 |
+
.js-plotly-plot .xtick text,
|
| 87 |
+
.js-plotly-plot .ytick text,
|
| 88 |
+
.js-plotly-plot .annotation text,
|
| 89 |
+
.js-plotly-plot .colorbar .tick text {
|
| 90 |
+
display: block !important;
|
| 91 |
+
visibility: visible !important;
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
/* === Plot canvas плотные высоты === */
|
| 95 |
+
#root [label="Market Composition"] canvas {
|
| 96 |
+
height: 440px !important;
|
| 97 |
+
margin: 0 !important;
|
| 98 |
+
}
|
| 99 |
+
#root [label="Top Movers"] canvas,
|
| 100 |
+
#root [label="Market Cap vs Volume"] canvas {
|
| 101 |
+
height: 340px !important;
|
| 102 |
+
margin: 0 !important;
|
| 103 |
+
}
|
| 104 |
+
|
| 105 |
+
/* === Убираем возможные системные бордеры блоков Gradio === */
|
| 106 |
+
[data-testid="block"] {
|
| 107 |
+
border: none !important;
|
| 108 |
+
box-shadow: none !important;
|
| 109 |
+
background: transparent !important;
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
/* === AI sidebar === */
|
| 113 |
+
#ai_summary_sidebar textarea {
|
| 114 |
+
height: 440px !important;
|
| 115 |
+
background: #161b22 !important;
|
| 116 |
+
color: #f0f6fc !important;
|
| 117 |
+
border: 1px solid #30363d !important;
|
| 118 |
+
border-radius: 6px !important;
|
| 119 |
+
font-family: 'JetBrains Mono', monospace !important;
|
| 120 |
+
font-size: 13.5px !important;
|
| 121 |
+
line-height: 1.55 !important;
|
| 122 |
+
padding: 12px !important;
|
| 123 |
+
resize: none !important;
|
| 124 |
+
box-shadow: inset 0 0 3px rgba(0,0,0,0.25);
|
| 125 |
+
}
|
| 126 |
+
|
| 127 |
+
/* === Ряды между графиками === */
|
| 128 |
+
.gr-row {
|
| 129 |
+
gap: 10px !important;
|
| 130 |
+
margin-top: 2px !important;
|
| 131 |
+
margin-bottom: 2px !important;
|
| 132 |
+
}
|
core/styles/multi_charts.css
ADDED
|
@@ -0,0 +1,4 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
[data-testid="html"]{
|
| 2 |
+
width:100% !important; background:#0d1117 !important; border:1px solid #30363d !important;
|
| 3 |
+
border-radius:6px !important; overflow:hidden !important; min-height: 360px;
|
| 4 |
+
}
|
core/ui_style.css
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
/* === Global Layout === */
|
| 2 |
+
.gradio-container {
|
| 3 |
+
font-family: 'Inter', sans-serif;
|
| 4 |
+
background-color: #0d1117 !important;
|
| 5 |
+
}
|
| 6 |
+
[data-testid="block-container"] {
|
| 7 |
+
max-width: 1180px !important;
|
| 8 |
+
margin: auto !important;
|
| 9 |
+
}
|
| 10 |
+
|
| 11 |
+
/* === Typography === */
|
| 12 |
+
h2, h3, .gr-markdown {
|
| 13 |
+
color: #f0f6fc !important;
|
| 14 |
+
font-weight: 600 !important;
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
/* === KPI Panel === */
|
| 18 |
+
#kpi_panel {
|
| 19 |
+
display: flex;
|
| 20 |
+
justify-content: space-between;
|
| 21 |
+
align-items: center;
|
| 22 |
+
margin: 12px 0 4px 0;
|
| 23 |
+
}
|
| 24 |
+
.kpi-card {
|
| 25 |
+
background-color: #161b22;
|
| 26 |
+
border-radius: 6px;
|
| 27 |
+
padding: 10px 14px;
|
| 28 |
+
width: 24%;
|
| 29 |
+
box-shadow: 0 1px 3px rgba(0,0,0,0.3);
|
| 30 |
+
display: flex;
|
| 31 |
+
flex-direction: column;
|
| 32 |
+
}
|
| 33 |
+
.kpi-symbol {
|
| 34 |
+
font-weight: 700;
|
| 35 |
+
font-size: 15px;
|
| 36 |
+
color: #f0f6fc;
|
| 37 |
+
}
|
| 38 |
+
.kpi-price {
|
| 39 |
+
font-size: 20px;
|
| 40 |
+
font-weight: 600;
|
| 41 |
+
color: #f9fafb;
|
| 42 |
+
}
|
| 43 |
+
.kpi-change {
|
| 44 |
+
font-size: 13px;
|
| 45 |
+
margin-top: 2px;
|
| 46 |
+
}
|
| 47 |
+
|
| 48 |
+
/* === Chart Embed (ECharts + Highcharts) === */
|
| 49 |
+
[data-testid="html"] {
|
| 50 |
+
width: 100% !important;
|
| 51 |
+
background: #0d1117 !important;
|
| 52 |
+
border: 1px solid #30363d !important;
|
| 53 |
+
border-radius: 6px !important;
|
| 54 |
+
overflow: hidden !important;
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
/* === Remove Gray Placeholder Icons === */
|
| 58 |
+
[data-testid="plot-container"] svg {
|
| 59 |
+
display: none !important;
|
| 60 |
+
}
|
| 61 |
+
[data-testid="plot-container"] .wrap {
|
| 62 |
+
background: transparent !important;
|
| 63 |
+
box-shadow: none !important;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
/* === Plot Layout === */
|
| 67 |
+
[data-testid="plot-container"] {
|
| 68 |
+
width: 100% !important;
|
| 69 |
+
margin: 0 auto 16px auto !important;
|
| 70 |
+
}
|
| 71 |
+
[data-testid="plot-container"] canvas {
|
| 72 |
+
width: 100% !important;
|
| 73 |
+
height: auto !important;
|
| 74 |
+
}
|
| 75 |
+
|
| 76 |
+
/* === Chart Heights === */
|
| 77 |
+
#root [label="Market Composition"] canvas { height: 360px !important; }
|
| 78 |
+
#root [label="Top Movers"] canvas,
|
| 79 |
+
#root [label="Market Cap vs Volume"] canvas { height: 320px !important; }
|
| 80 |
+
|
| 81 |
+
/* === Sidebar === */
|
| 82 |
+
#ai_summary_sidebar textarea {
|
| 83 |
+
height: 360px !important;
|
| 84 |
+
background-color: #161b22 !important;
|
| 85 |
+
color: #f0f6fc !important;
|
| 86 |
+
border: 1px solid #30363d !important;
|
| 87 |
+
border-radius: 6px !important;
|
| 88 |
+
font-family: 'JetBrains Mono', monospace !important;
|
| 89 |
+
font-size: 13.5px !important;
|
| 90 |
+
line-height: 1.5 !important;
|
| 91 |
+
padding: 12px !important;
|
| 92 |
+
resize: none !important;
|
| 93 |
+
}
|
| 94 |
+
|
| 95 |
+
/* === Controls === */
|
| 96 |
+
.gr-button {
|
| 97 |
+
border-radius: 6px !important;
|
| 98 |
+
font-weight: 600 !important;
|
| 99 |
+
letter-spacing: 0.3px;
|
| 100 |
+
height: 52px !important;
|
| 101 |
+
background: linear-gradient(90deg, #4f46e5, #6366f1) !important;
|
| 102 |
+
border: none !important;
|
| 103 |
+
box-shadow: 0 2px 4px rgba(0,0,0,0.25);
|
| 104 |
+
transition: all 0.2s ease-in-out;
|
| 105 |
+
}
|
| 106 |
+
.gr-button:hover {
|
| 107 |
+
filter: brightness(1.08);
|
| 108 |
+
transform: translateY(-1px);
|
| 109 |
+
}
|
| 110 |
+
.gr-slider {
|
| 111 |
+
height: 52px !important;
|
| 112 |
+
}
|
| 113 |
+
.gr-slider input[type=range]::-webkit-slider-thumb {
|
| 114 |
+
background: #6366f1 !important;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
/* === Table / Row spacing === */
|
| 118 |
+
.gr-dataframe table {
|
| 119 |
+
width: 100% !important;
|
| 120 |
+
color: #c9d1d9 !important;
|
| 121 |
+
background: #161b22 !important;
|
| 122 |
+
}
|
| 123 |
+
.gr-dataframe th {
|
| 124 |
+
background-color: #21262d !important;
|
| 125 |
+
color: #f0f6fc !important;
|
| 126 |
+
border-bottom: 1px solid #30363d !important;
|
| 127 |
+
text-transform: uppercase;
|
| 128 |
+
font-weight: 600 !important;
|
| 129 |
+
}
|
| 130 |
+
.gr-dataframe td {
|
| 131 |
+
border-top: 1px solid #30363d !important;
|
| 132 |
+
padding: 8px !important;
|
| 133 |
+
}
|
| 134 |
+
.gr-row { gap: 16px !important; }
|
| 135 |
+
|
core/visual_comparison.py
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
Module: visual_comparison.py
|
| 3 |
+
Purpose: Interactive crypto pair comparison (Plotly + CoinGecko)
|
| 4 |
+
"""
|
| 5 |
+
|
| 6 |
+
import requests
|
| 7 |
+
import pandas as pd
|
| 8 |
+
import plotly.graph_objects as go
|
| 9 |
+
|
| 10 |
+
COINGECKO_API = "https://api.coingecko.com/api/v3"
|
| 11 |
+
|
| 12 |
+
|
| 13 |
+
def get_coin_history(coin_id: str, days: int = 180):
|
| 14 |
+
"""Fetch historical market data for given coin from CoinGecko API."""
|
| 15 |
+
url = f"{COINGECKO_API}/coins/{coin_id}/market_chart?vs_currency=usd&days={days}"
|
| 16 |
+
r = requests.get(url)
|
| 17 |
+
r.raise_for_status()
|
| 18 |
+
data = r.json()
|
| 19 |
+
df = pd.DataFrame(data["prices"], columns=["timestamp", "price"])
|
| 20 |
+
df["timestamp"] = pd.to_datetime(df["timestamp"], unit="ms")
|
| 21 |
+
return df
|
| 22 |
+
|
| 23 |
+
|
| 24 |
+
def build_price_chart(pair: tuple[str, str], days: int = 180):
|
| 25 |
+
"""Build comparative price chart for selected pair."""
|
| 26 |
+
coin_a, coin_b = pair
|
| 27 |
+
|
| 28 |
+
df_a = get_coin_history(coin_a, days)
|
| 29 |
+
df_b = get_coin_history(coin_b, days)
|
| 30 |
+
|
| 31 |
+
fig = go.Figure()
|
| 32 |
+
fig.add_trace(go.Scatter(
|
| 33 |
+
x=df_a["timestamp"],
|
| 34 |
+
y=df_a["price"],
|
| 35 |
+
name=f"{coin_a.capitalize()} / USD",
|
| 36 |
+
line=dict(width=2),
|
| 37 |
+
))
|
| 38 |
+
fig.add_trace(go.Scatter(
|
| 39 |
+
x=df_b["timestamp"],
|
| 40 |
+
y=df_b["price"],
|
| 41 |
+
name=f"{coin_b.capitalize()} / USD",
|
| 42 |
+
line=dict(width=2),
|
| 43 |
+
))
|
| 44 |
+
|
| 45 |
+
fig.update_layout(
|
| 46 |
+
template="plotly_dark",
|
| 47 |
+
height=480,
|
| 48 |
+
margin=dict(l=40, r=20, t=30, b=40),
|
| 49 |
+
xaxis_title="Date",
|
| 50 |
+
yaxis_title="Price (USD)",
|
| 51 |
+
legend_title="Asset",
|
| 52 |
+
hovermode="x unified",
|
| 53 |
+
)
|
| 54 |
+
|
| 55 |
+
return fig
|
| 56 |
+
|
| 57 |
+
|
| 58 |
+
def build_volatility_chart(pair: tuple[str, str], days: int = 180):
|
| 59 |
+
"""Build comparative volatility chart for selected pair."""
|
| 60 |
+
coin_a, coin_b = pair
|
| 61 |
+
|
| 62 |
+
df_a = get_coin_history(coin_a, days)
|
| 63 |
+
df_b = get_coin_history(coin_b, days)
|
| 64 |
+
|
| 65 |
+
df_a["returns"] = df_a["price"].pct_change() * 100
|
| 66 |
+
df_b["returns"] = df_b["price"].pct_change() * 100
|
| 67 |
+
|
| 68 |
+
fig = go.Figure()
|
| 69 |
+
fig.add_trace(go.Scatter(
|
| 70 |
+
x=df_a["timestamp"],
|
| 71 |
+
y=df_a["returns"],
|
| 72 |
+
name=f"{coin_a.upper()} Daily Change (%)",
|
| 73 |
+
mode="lines",
|
| 74 |
+
line=dict(width=1.6),
|
| 75 |
+
))
|
| 76 |
+
fig.add_trace(go.Scatter(
|
| 77 |
+
x=df_b["timestamp"],
|
| 78 |
+
y=df_b["returns"],
|
| 79 |
+
name=f"{coin_b.upper()} Daily Change (%)",
|
| 80 |
+
mode="lines",
|
| 81 |
+
line=dict(width=1.6),
|
| 82 |
+
))
|
| 83 |
+
|
| 84 |
+
fig.update_layout(
|
| 85 |
+
template="plotly_dark",
|
| 86 |
+
height=400,
|
| 87 |
+
margin=dict(l=40, r=20, t=30, b=40),
|
| 88 |
+
xaxis_title="Date",
|
| 89 |
+
yaxis_title="Daily Change (%)",
|
| 90 |
+
legend_title="Volatility",
|
| 91 |
+
hovermode="x unified",
|
| 92 |
+
)
|
| 93 |
+
|
| 94 |
+
return fig
|
core/visualization.py
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
🇬🇧 Module: visualization.py
|
| 3 |
+
Purpose: Contains visualization utilities such as plotting charts for portfolio analytics (e.g., Alpha vs BTC).
|
| 4 |
+
|
| 5 |
+
🇷🇺 Модуль: visualization.py
|
| 6 |
+
Назначение: содержит функции визуализации данных, включая построение графиков портфелей (например, Alpha к BTC).
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import matplotlib.pyplot as plt
|
| 10 |
+
import requests
|
| 11 |
+
from typing import Optional
|
| 12 |
+
from config import EXTERNAL_API_URL, DEBUG
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
def build_alpha_chart(portfolio_id: str) -> Optional[plt.Figure]:
|
| 16 |
+
"""Fetch alphaBTC series and build a matplotlib figure."""
|
| 17 |
+
url = (
|
| 18 |
+
f"{EXTERNAL_API_URL}/portfolio/get?"
|
| 19 |
+
f"portfolioId={portfolio_id}&extended=1&declaration=1&step=day&lang=en&incViews=1"
|
| 20 |
+
)
|
| 21 |
+
headers = {"User-Agent": "Mozilla/5.0", "Accept": "application/json"}
|
| 22 |
+
|
| 23 |
+
try:
|
| 24 |
+
response = requests.get(url, headers=headers)
|
| 25 |
+
if DEBUG:
|
| 26 |
+
print(f"[DEBUG] GET {url} -> {response.status_code}")
|
| 27 |
+
print(f"[DEBUG] Response preview: {response.text[:200]}...")
|
| 28 |
+
|
| 29 |
+
response.raise_for_status()
|
| 30 |
+
data = response.json()
|
| 31 |
+
extended = data.get("data", {}).get("extended", {})
|
| 32 |
+
alpha_data = extended.get("alphaBTC", [])
|
| 33 |
+
|
| 34 |
+
if not alpha_data or not isinstance(alpha_data, list):
|
| 35 |
+
print("[WARN] No alphaBTC data found.")
|
| 36 |
+
return None
|
| 37 |
+
|
| 38 |
+
values = [item.get("value", 0) for item in alpha_data]
|
| 39 |
+
indices = list(range(len(values)))
|
| 40 |
+
|
| 41 |
+
# Create plot
|
| 42 |
+
fig, ax = plt.subplots(figsize=(12, 6))
|
| 43 |
+
ax.plot(indices, values, color="blue", label="alphaBTC")
|
| 44 |
+
ax.axhline(0, color="black", linewidth=1)
|
| 45 |
+
ax.set_title("Alpha vs BTC", fontsize=14)
|
| 46 |
+
ax.set_xlabel("Index")
|
| 47 |
+
ax.set_ylabel("Alpha")
|
| 48 |
+
ax.grid(True, linestyle="--", alpha=0.5)
|
| 49 |
+
ax.legend()
|
| 50 |
+
plt.tight_layout()
|
| 51 |
+
|
| 52 |
+
if DEBUG:
|
| 53 |
+
print("[DEBUG] Chart built successfully ✅")
|
| 54 |
+
return fig
|
| 55 |
+
|
| 56 |
+
except Exception as e:
|
| 57 |
+
print(f"[ERROR] Exception while building chart: {e}")
|
| 58 |
+
return None
|
prompts/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# __init__.py
|
| 2 |
+
# Marks this directory as a Python package.
|
prompts/reference_templates.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
🇬🇧 Module: reference_templates.py
|
| 3 |
+
Purpose: Defines reusable prompt templates for portfolio analysis and comparison.
|
| 4 |
+
|
| 5 |
+
🇷🇺 Модуль: reference_templates.py
|
| 6 |
+
Назначение: содержит шаблоны промптов для анализа и сравнения портфелей.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
REFERENCE_PROMPT = (
|
| 10 |
+
"**Analysis**\n\n"
|
| 11 |
+
"For each key metric, provide value and brief interpretation:\n"
|
| 12 |
+
"- Annualized Return: {value1}\n"
|
| 13 |
+
"- Max Drawdown: {value2}\n"
|
| 14 |
+
"- Sharpe Ratio: {value3}\n"
|
| 15 |
+
"- Sortino Ratio: {value4}\n"
|
| 16 |
+
"- Calmar Ratio: {value5}\n"
|
| 17 |
+
"- Beta: {value6}\n"
|
| 18 |
+
"- Volatility: {value7}\n\n"
|
| 19 |
+
"**Interpretation**\n"
|
| 20 |
+
"Summarize the overall strategy characteristics, risk level, and stability.\n\n"
|
| 21 |
+
"**Recommendation**\n"
|
| 22 |
+
"Provide a brief conclusion about the portfolio’s viability."
|
| 23 |
+
)
|
| 24 |
+
|
| 25 |
+
REFERENCE_COMPARISON_PROMPT = (
|
| 26 |
+
"**Comparison**\n\n"
|
| 27 |
+
"Portfolio A:\n"
|
| 28 |
+
"- Annualized Return: {a1}\n"
|
| 29 |
+
"- Max Drawdown: {a2}\n"
|
| 30 |
+
"- Sharpe Ratio: {a3}\n"
|
| 31 |
+
"- Sortino Ratio: {a4}\n\n"
|
| 32 |
+
"Portfolio B:\n"
|
| 33 |
+
"- Annualized Return: {b1}\n"
|
| 34 |
+
"- Max Drawdown: {b2}\n"
|
| 35 |
+
"- Sharpe Ratio: {b3}\n"
|
| 36 |
+
"- Sortino Ratio: {b4}\n\n"
|
| 37 |
+
"**Summary**\n"
|
| 38 |
+
"Highlight main differences, strengths, and investor suitability."
|
| 39 |
+
)
|
prompts/system_prompts.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
🇬🇧 Module: system_prompts.py
|
| 3 |
+
Purpose: Stores system-level instructions for LLM.
|
| 4 |
+
|
| 5 |
+
🇷🇺 Модуль: system_prompts.py
|
| 6 |
+
Назначение: хранит системные инструкции для LLM.
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
ANALYSIS_SYSTEM_PROMPT = """
|
| 10 |
+
You are a quantitative portfolio analyst.
|
| 11 |
+
|
| 12 |
+
Given a single portfolio’s metrics, provide:
|
| 13 |
+
1️⃣ **Objective evaluation** of performance (returns, alpha, Sharpe ratio).
|
| 14 |
+
2️⃣ **Risk assessment** (volatility, max drawdown).
|
| 15 |
+
3️⃣ **Interpretation of efficiency** (how well risk and return balance).
|
| 16 |
+
4️⃣ **Concise recommendation** — state whether the portfolio is performing *well, neutrally, or poorly*, and what could be improved.
|
| 17 |
+
|
| 18 |
+
Avoid general statements. Use data context.
|
| 19 |
+
Your tone: professional, precise, analytical.
|
| 20 |
+
"""
|
| 21 |
+
|
| 22 |
+
COMPARISON_SYSTEM_PROMPT = """
|
| 23 |
+
|
| 24 |
+
You are a financial analysis model specializing in quantitative portfolio comparison.
|
| 25 |
+
|
| 26 |
+
Your goal is to compare two portfolios **objectively** using their metrics.
|
| 27 |
+
|
| 28 |
+
When analyzing:
|
| 29 |
+
- Do not make emotional or vague statements.
|
| 30 |
+
- Base conclusions strictly on the provided numerical data.
|
| 31 |
+
- Identify *which portfolio is statistically stronger* based on performance, risk, and stability.
|
| 32 |
+
|
| 33 |
+
Your output must be concise and structured in the following format:
|
| 34 |
+
|
| 35 |
+
Comparative Analysis
|
| 36 |
+
- Discuss which portfolio shows **better performance** (higher return / alpha).
|
| 37 |
+
- Discuss **risk-adjusted efficiency** (Sharpe / Sortino ratio, volatility, maxDD).
|
| 38 |
+
- Highlight **consistency** and **downside protection** if metrics imply it.
|
| 39 |
+
|
| 40 |
+
Recommendation
|
| 41 |
+
Provide an actionable insight:
|
| 42 |
+
- If one portfolio is clearly better, state which one and why.
|
| 43 |
+
- If both are weak or over-risked — say it explicitly.
|
| 44 |
+
- If data is incomplete or contradictory — mention this clearly.
|
| 45 |
+
|
| 46 |
+
Avoid phrases like “depends on the investor” or “might be better for risk-taking investors”.
|
| 47 |
+
You are the quantitative analyst — give a judgment based on data, not preference.
|
| 48 |
+
"""
|
| 49 |
+
|
| 50 |
+
|
| 51 |
+
GENERAL_CONTEXT = (
|
| 52 |
+
"You are an intelligent assistant specializing in investment portfolios, finance, "
|
| 53 |
+
"and market analysis. Answer clearly, concisely, and professionally."
|
| 54 |
+
)
|
requirements.txt
ADDED
|
@@ -0,0 +1,12 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
gradio>=4.29.0
|
| 2 |
+
openai>=1.30.1
|
| 3 |
+
requests
|
| 4 |
+
httpx
|
| 5 |
+
pandas
|
| 6 |
+
matplotlib
|
| 7 |
+
plotly
|
| 8 |
+
yfinance>=0.2.43
|
| 9 |
+
plotly>=6.3.1
|
| 10 |
+
altair
|
| 11 |
+
pyecharts
|
| 12 |
+
jinja2
|
services/__init__.py
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# __init__.py
|
| 2 |
+
# Marks this directory as a Python package.
|
services/llm_client.py
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
🇬🇧 Module: llm_client.py
|
| 3 |
+
Purpose: Adapter for Featherless.ai (OpenAI-compatible API).
|
| 4 |
+
|
| 5 |
+
🇷🇺 Модуль: llm_client.py
|
| 6 |
+
Назначение: адаптер для LLM-инференса через Featherless.ai (совместимо с OpenAI API).
|
| 7 |
+
"""
|
| 8 |
+
|
| 9 |
+
import os
|
| 10 |
+
from typing import List, Dict, Generator
|
| 11 |
+
from openai import OpenAI
|
| 12 |
+
from config import FEATHERLESS_API_KEY, FEATHERLESS_MODEL
|
| 13 |
+
|
| 14 |
+
|
| 15 |
+
class FeatherlessLLM:
|
| 16 |
+
"""Wrapper for Featherless.ai LLM inference."""
|
| 17 |
+
|
| 18 |
+
def __init__(self, api_key: str = FEATHERLESS_API_KEY, model: str = FEATHERLESS_MODEL):
|
| 19 |
+
if not api_key:
|
| 20 |
+
raise RuntimeError("❌ Environment variable 'featherless' (API key) is missing.")
|
| 21 |
+
self.client = OpenAI(base_url="https://api.featherless.ai/v1", api_key=api_key)
|
| 22 |
+
self.model = model
|
| 23 |
+
|
| 24 |
+
def stream_chat(self, *, messages: List[Dict], model: str = None) -> Generator[str, None, None]:
|
| 25 |
+
"""Stream chat completion using Featherless.ai."""
|
| 26 |
+
used_model = model or self.model
|
| 27 |
+
response = self.client.chat.completions.create(
|
| 28 |
+
model=used_model,
|
| 29 |
+
messages=messages,
|
| 30 |
+
stream=True,
|
| 31 |
+
)
|
| 32 |
+
for chunk in response:
|
| 33 |
+
delta = chunk.choices[0].delta.content
|
| 34 |
+
if delta:
|
| 35 |
+
yield delta
|
| 36 |
+
|
| 37 |
+
|
| 38 |
+
# === Global singleton instance ===
|
| 39 |
+
llm_service = FeatherlessLLM()
|
services/output_api.py
ADDED
|
@@ -0,0 +1,76 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
"""
|
| 2 |
+
🇬🇧 Module: api_client.py
|
| 3 |
+
Purpose: Provides async API client for external portfolio analytics service.
|
| 4 |
+
Handles fetching metrics, alphaBTC data, and other portfolio information.
|
| 5 |
+
|
| 6 |
+
🇷🇺 Модуль: api_client.py
|
| 7 |
+
Назначение: асинхронный клиент для внешнего API аналитики портфелей,
|
| 8 |
+
выполняющий запросы к метрикам, данным alphaBTC и другой информации о портфелях.
|
| 9 |
+
"""
|
| 10 |
+
|
| 11 |
+
import re
|
| 12 |
+
import httpx
|
| 13 |
+
from typing import Any, Dict, List, Optional
|
| 14 |
+
from config import EXTERNAL_API_URL, REQUEST_TIMEOUT, DEBUG
|
| 15 |
+
|
| 16 |
+
# === UUID detection ===
|
| 17 |
+
UUID_PATTERN = re.compile(
|
| 18 |
+
r"[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}"
|
| 19 |
+
)
|
| 20 |
+
|
| 21 |
+
|
| 22 |
+
def extract_portfolio_id(text: str) -> Optional[str]:
|
| 23 |
+
"""Extract a portfolio UUID from text or URL."""
|
| 24 |
+
match = UUID_PATTERN.search(text or "")
|
| 25 |
+
return match.group(0) if match else None
|
| 26 |
+
|
| 27 |
+
|
| 28 |
+
async def _get_json(url: str) -> Dict[str, Any]:
|
| 29 |
+
"""Generic helper to send GET request and parse JSON."""
|
| 30 |
+
if DEBUG:
|
| 31 |
+
print(f"[DEBUG] Requesting URL: {url}")
|
| 32 |
+
|
| 33 |
+
async with httpx.AsyncClient(timeout=REQUEST_TIMEOUT) as client:
|
| 34 |
+
r = await client.get(url, headers={"User-Agent": "Mozilla/5.0", "Accept": "application/json"})
|
| 35 |
+
r.raise_for_status()
|
| 36 |
+
return r.json()
|
| 37 |
+
|
| 38 |
+
|
| 39 |
+
async def fetch_metrics_async(portfolio_id: str) -> Optional[Dict[str, Any]]:
|
| 40 |
+
"""Fetch portfolio metrics (extended data)."""
|
| 41 |
+
url = f"{EXTERNAL_API_URL}/portfolio/get?portfolioId={portfolio_id}&extended=1"
|
| 42 |
+
try:
|
| 43 |
+
data = await _get_json(url)
|
| 44 |
+
extended = data.get("data", {}).get("extended", {})
|
| 45 |
+
result = {}
|
| 46 |
+
for k, v in extended.items():
|
| 47 |
+
if isinstance(v, (int, float)):
|
| 48 |
+
# Convert some fields to percentages for readability
|
| 49 |
+
if k in {"cagr", "alphaRatio", "volatility", "maxDD"}:
|
| 50 |
+
result[k] = v * 100
|
| 51 |
+
else:
|
| 52 |
+
result[k] = v
|
| 53 |
+
if DEBUG:
|
| 54 |
+
print(f"[DEBUG] Metrics fetched for {portfolio_id}: {result}")
|
| 55 |
+
return result
|
| 56 |
+
except Exception as e:
|
| 57 |
+
if DEBUG:
|
| 58 |
+
print(f"[ERROR] fetch_metrics_async: {e}")
|
| 59 |
+
return None
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
async def fetch_absolute_pnl_async(portfolio_id: str) -> Optional[List[Dict[str, Any]]]:
|
| 63 |
+
"""Fetch absolutePnL daily data."""
|
| 64 |
+
url = f"{EXTERNAL_API_URL}/portfolio/get?portfolioId={portfolio_id}&extended=1&step=day"
|
| 65 |
+
try:
|
| 66 |
+
data = await _get_json(url)
|
| 67 |
+
pnl = data.get("data", {}).get("extended", {}).get("absolutePnL", [])
|
| 68 |
+
if not isinstance(pnl, list):
|
| 69 |
+
if DEBUG:
|
| 70 |
+
print("[ERROR] absolutePnL is not a list or missing")
|
| 71 |
+
return None
|
| 72 |
+
return pnl
|
| 73 |
+
except Exception as e:
|
| 74 |
+
if DEBUG:
|
| 75 |
+
print(f"[ERROR] fetch_absolute_pnl_async: {e}")
|
| 76 |
+
return None
|