QAway-to commited on
Commit
968376d
·
unverified ·
2 Parent(s): c33daaa 7e2057c

Merge pull request #1 from QAway-to/mainv1

Browse files
.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