sovai 0.2.74__tar.gz → 0.2.76__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {sovai-0.2.74 → sovai-0.2.76}/PKG-INFO +3 -1
- {sovai-0.2.74 → sovai-0.2.76}/pyproject.toml +5 -2
- {sovai-0.2.74 → sovai-0.2.76}/sovai/__init__.py +22 -1
- sovai-0.2.76/sovai/extensions/__init__.py +16 -0
- sovai-0.2.76/sovai/extensions/chart_explainer.py +537 -0
- sovai-0.2.76/sovai/extensions/overall_explainers.py +352 -0
- sovai-0.2.76/sovai/extensions/signal_evaluation copy.py +2720 -0
- sovai-0.2.76/sovai/extensions/signal_evaluation.py +2732 -0
- sovai-0.2.74/sovai/extensions/signal_evaluation.py → sovai-0.2.76/sovai/extensions/signal_evaluation_old.py +8 -8
- sovai-0.2.76/sovai/extensions/table_explainer.py +570 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/get_tools.py +5 -13
- sovai-0.2.76/sovai/tools/explainers.py +451 -0
- sovai-0.2.74/data/codes.parq +0 -0
- sovai-0.2.74/data/tickers.parq +0 -0
- sovai-0.2.74/sovai/assets/features_mapping.parq +0 -0
- sovai-0.2.74/sovai/assets/tickers.parq +0 -0
- sovai-0.2.74/sovai/utils/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/LICENSE +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/README.md +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/.DS_Store +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/api_config.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/assets/dropdown.css +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/basic_auth.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/computations/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/computations/functions.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/errors/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/errors/sovai_errors.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/anomalies.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/ask_df_llm.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/change_point_generator copy.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/change_point_generator.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/clustering.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/core_kshape.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/cum_sum_torch.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/cusum.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/dimensionality_reduction.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/feature_extraction.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/feature_importance.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/feature_neutralizer.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/filter_df.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/fractional_differencing.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/nowcasting.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/pairwise.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/pandas_extensions.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/parallel_functions.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/pfa_feature_selector.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/regime_change.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/regime_change_pca.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/shapley_global_importance.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/shapley_importance.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/technical_indicators.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/time_decomposition.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/weight_optimization.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/get_compute.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/get_data copy 2.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/get_data copy.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/get_data.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/get_plots.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/get_reports.py +0 -0
- {sovai-0.2.74/sovai/extensions → sovai-0.2.76/sovai/plots}/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/accounting/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/accounting/accounting_plots.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/allocation/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/allocation/allocation_plots.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/bankruptcy/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/bankruptcy/bankruptcy_plots.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/breakout/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/breakout/breakout_plots.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/corp_risk/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/corp_risk/corp_risk_plots.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/earnings_surprise/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/earnings_surprise/earnings_surprise_plots.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/insider/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/insider/insider_plots.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/institutional/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/institutional/institutional_plots.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/news/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/news/news_plots.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/ratios/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/ratios/ratios_plots.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/reports/accounting/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/reports/accounting/accounting_balance_sheet.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/reports/bankruptcy/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/reports/bankruptcy/bankruptcy_monthly_top.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/reports/general/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/reports/general/general_plots.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/reports/news/__inti__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/reports/news/news_econometric_analysis.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/studies/nowcasting.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/token_auth.py +0 -0
- {sovai-0.2.74/sovai/plots → sovai-0.2.76/sovai/tools/authentication}/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/tools/authentication/authentication.py +0 -0
- {sovai-0.2.74/sovai/tools/authentication → sovai-0.2.76/sovai/tools/sec}/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/tools/sec/graphs.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/tools/sec/llm_code_generator.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/tools/sec/sec_10_k_8_k_filings.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/tools/sec/sec_edgar_search.py +0 -0
- {sovai-0.2.74/sovai/tools/sec → sovai-0.2.76/sovai/utils}/__init__.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/client_side.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/client_side_s3.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/client_side_s3_part_high.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/converter.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/datetime_formats.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/file_management copy.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/file_management.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/get_tickers.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/helpers.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/pandas_extensions copy.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/plot.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/port_manager copy.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/port_manager.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/stream.py +0 -0
- {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/verbose_utils.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: sovai
|
|
3
|
-
Version: 0.2.
|
|
3
|
+
Version: 0.2.76
|
|
4
4
|
Summary: python for asset management
|
|
5
5
|
License: Proprietary License
|
|
6
6
|
License-File: LICENSE
|
|
@@ -16,6 +16,7 @@ Provides-Extra: full
|
|
|
16
16
|
Requires-Dist: aiobotocore (==2.21.1)
|
|
17
17
|
Requires-Dist: boto3 (==1.37.1)
|
|
18
18
|
Requires-Dist: botocore (==1.37.1)
|
|
19
|
+
Requires-Dist: brotli (>=1.1.0,<2.0.0)
|
|
19
20
|
Requires-Dist: dash ; extra == "full"
|
|
20
21
|
Requires-Dist: dash-bootstrap-components ; extra == "full"
|
|
21
22
|
Requires-Dist: edgar-tool (>=1.3.3)
|
|
@@ -26,6 +27,7 @@ Requires-Dist: gcsfs (==2024.10.0)
|
|
|
26
27
|
Requires-Dist: great-tables ; extra == "full"
|
|
27
28
|
Requires-Dist: ipywidgets ; extra == "full"
|
|
28
29
|
Requires-Dist: joblib (>=1.0)
|
|
30
|
+
Requires-Dist: kaleido (>=1.1.0,<2.0.0)
|
|
29
31
|
Requires-Dist: lightgbm ; extra == "full"
|
|
30
32
|
Requires-Dist: matplotlib ; extra == "full"
|
|
31
33
|
Requires-Dist: mfles ; extra == "full"
|
|
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
|
|
|
4
4
|
|
|
5
5
|
[project]
|
|
6
6
|
name = "sovai"
|
|
7
|
-
version = "0.2.
|
|
7
|
+
version = "0.2.76"
|
|
8
8
|
description = "python for asset management"
|
|
9
9
|
authors = [
|
|
10
10
|
{name = "Derek Snow", email = "d.snow@outlook.com"}
|
|
@@ -37,7 +37,9 @@ dependencies = [
|
|
|
37
37
|
"fastapi-cache>=0.1.0",
|
|
38
38
|
"fastapi-cache2[redis]>=0.2.1",
|
|
39
39
|
"tqdm (>=4.67.1,<5.0.0)",
|
|
40
|
-
"skfolio (>=0.9.0,<0.10.0)"
|
|
40
|
+
"skfolio (>=0.9.0,<0.10.0)",
|
|
41
|
+
"brotli (>=1.1.0,<2.0.0)",
|
|
42
|
+
"kaleido (>=1.1.0,<2.0.0)",
|
|
41
43
|
]
|
|
42
44
|
|
|
43
45
|
[tool.poetry]
|
|
@@ -114,6 +116,7 @@ fastapi-cache = "^0.1.0"
|
|
|
114
116
|
fastapi-cache2 = {extras = ["redis"], version = "^0.2.1"}
|
|
115
117
|
|
|
116
118
|
|
|
119
|
+
|
|
117
120
|
[project.optional-dependencies]
|
|
118
121
|
full = [
|
|
119
122
|
"scipy",
|
|
@@ -48,6 +48,7 @@ _sec_search_module = None
|
|
|
48
48
|
_sec_filing_module = None
|
|
49
49
|
_code_module = None
|
|
50
50
|
_sec_graph_module = None
|
|
51
|
+
_explain_module = None
|
|
51
52
|
|
|
52
53
|
try:
|
|
53
54
|
# Attempt to import a module from the full installation to check if it's installed
|
|
@@ -133,6 +134,24 @@ try:
|
|
|
133
134
|
_sec_graph_module = _loaded_sec_graph
|
|
134
135
|
return _sec_graph_module(*args, **kwargs)
|
|
135
136
|
|
|
137
|
+
def explain(*args, **kwargs):
|
|
138
|
+
"""
|
|
139
|
+
Unified explain function that intelligently routes based on input type.
|
|
140
|
+
|
|
141
|
+
Usage:
|
|
142
|
+
------
|
|
143
|
+
# Explain a single chart
|
|
144
|
+
sov.explain(fig)
|
|
145
|
+
|
|
146
|
+
# Explain overall category
|
|
147
|
+
sov.explain("signal_evaluation")
|
|
148
|
+
"""
|
|
149
|
+
global _explain_module
|
|
150
|
+
if _explain_module is None:
|
|
151
|
+
from .get_tools import explain as _loaded_explain
|
|
152
|
+
_explain_module = _loaded_explain
|
|
153
|
+
return _explain_module(*args, **kwargs)
|
|
154
|
+
|
|
136
155
|
|
|
137
156
|
except ImportError as e:
|
|
138
157
|
print("this is the lean installation, for full use sovai[full]")
|
|
@@ -158,6 +177,8 @@ except ImportError as e:
|
|
|
158
177
|
raise ImportError("SEC tools require the full installation. Install with 'pip install sovai[full]'")
|
|
159
178
|
def sec_graph(*args, **kwargs):
|
|
160
179
|
raise ImportError("SEC tools require the full installation. Install with 'pip install sovai[full]'")
|
|
180
|
+
def explain(*args, **kwargs):
|
|
181
|
+
raise ImportError("Chart explanation requires the full installation. Install with 'pip install sovai[full]'")
|
|
161
182
|
|
|
162
183
|
|
|
163
184
|
__all__ = [
|
|
@@ -170,5 +191,5 @@ __all__ = [
|
|
|
170
191
|
if HAS_FULL_INSTALL:
|
|
171
192
|
__all__ += [
|
|
172
193
|
'plot', 'report', 'compute', 'nowcast', 'extension',
|
|
173
|
-
'sec_search', 'sec_filing', 'code', 'sec_graph'
|
|
194
|
+
'sec_search', 'sec_filing', 'code', 'sec_graph', 'explain'
|
|
174
195
|
]
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Extensions module for SovAI.
|
|
3
|
+
|
|
4
|
+
This module provides extended functionality including:
|
|
5
|
+
- Chart explanation using Gemini AI
|
|
6
|
+
- Signal evaluation and backtesting
|
|
7
|
+
- Various data processing utilities
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from .chart_explainer import explain_chart, auto_explain_chart, get_ephemeral_token
|
|
11
|
+
|
|
12
|
+
__all__ = [
|
|
13
|
+
'explain_chart',
|
|
14
|
+
'auto_explain_chart',
|
|
15
|
+
'get_ephemeral_token',
|
|
16
|
+
]
|
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Chart Explanation Module using Gemini via Ephemeral Token Broker
|
|
3
|
+
|
|
4
|
+
This module provides functionality to automatically explain chart metadata
|
|
5
|
+
using Google's Gemini model through a secure ephemeral token broker.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import requests
|
|
9
|
+
import asyncio
|
|
10
|
+
import json
|
|
11
|
+
import os
|
|
12
|
+
from datetime import datetime
|
|
13
|
+
from typing import Dict, Any, Optional
|
|
14
|
+
from IPython.display import display, Markdown
|
|
15
|
+
from google import genai
|
|
16
|
+
from google.genai import types
|
|
17
|
+
|
|
18
|
+
# Configuration
|
|
19
|
+
BROKER_URL = "https://gemini-ephemeral-broker-y7o724zyfq-uc.a.run.app"
|
|
20
|
+
SOVAI_TOKEN = "22fd808d-0947-45f1-995a-82346b921f0f"
|
|
21
|
+
|
|
22
|
+
# Caching configuration
|
|
23
|
+
CACHE_DIR = os.path.join(os.path.dirname(__file__), "..", "..", "cache", "explanations")
|
|
24
|
+
DATA_CACHE_DIR = os.path.join(CACHE_DIR, "data")
|
|
25
|
+
EXPLANATION_CACHE_DIR = os.path.join(CACHE_DIR, "explanations")
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
def _ensure_cache_dirs():
|
|
29
|
+
"""Ensure cache directories exist."""
|
|
30
|
+
os.makedirs(DATA_CACHE_DIR, exist_ok=True)
|
|
31
|
+
os.makedirs(EXPLANATION_CACHE_DIR, exist_ok=True)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
def _get_cache_path(category: str, plot_name: str, data_type: str = "data") -> str:
|
|
35
|
+
"""
|
|
36
|
+
Get the cache path for a given category, plot name, and data type.
|
|
37
|
+
|
|
38
|
+
Parameters:
|
|
39
|
+
-----------
|
|
40
|
+
category : str
|
|
41
|
+
The category (e.g., "signal_evaluation")
|
|
42
|
+
plot_name : str
|
|
43
|
+
The name of the specific plot/analysis
|
|
44
|
+
data_type : str
|
|
45
|
+
Either "data" or "explanations"
|
|
46
|
+
|
|
47
|
+
Returns:
|
|
48
|
+
--------
|
|
49
|
+
str : Full path to the cache file
|
|
50
|
+
"""
|
|
51
|
+
if data_type == "data":
|
|
52
|
+
base_dir = DATA_CACHE_DIR
|
|
53
|
+
else:
|
|
54
|
+
base_dir = EXPLANATION_CACHE_DIR
|
|
55
|
+
|
|
56
|
+
category_dir = os.path.join(base_dir, category)
|
|
57
|
+
os.makedirs(category_dir, exist_ok=True)
|
|
58
|
+
|
|
59
|
+
return os.path.join(category_dir, f"{plot_name}.json")
|
|
60
|
+
|
|
61
|
+
|
|
62
|
+
def cache_chart_data(category: str, plot_name: str, chart_card: Dict[str, Any]) -> None:
|
|
63
|
+
"""
|
|
64
|
+
Cache chart data hierarchically.
|
|
65
|
+
|
|
66
|
+
Parameters:
|
|
67
|
+
-----------
|
|
68
|
+
category : str
|
|
69
|
+
The category (e.g., "signal_evaluation")
|
|
70
|
+
plot_name : str
|
|
71
|
+
The name of the specific plot/analysis
|
|
72
|
+
chart_card : dict
|
|
73
|
+
Chart metadata and statistics
|
|
74
|
+
"""
|
|
75
|
+
_ensure_cache_dirs()
|
|
76
|
+
cache_path = _get_cache_path(category, plot_name, "data")
|
|
77
|
+
|
|
78
|
+
cache_data = {
|
|
79
|
+
"category": category,
|
|
80
|
+
"plot_name": plot_name,
|
|
81
|
+
"timestamp": datetime.now().isoformat(),
|
|
82
|
+
"chart_card": chart_card
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try:
|
|
86
|
+
with open(cache_path, 'w') as f:
|
|
87
|
+
json.dump(cache_data, f, indent=2)
|
|
88
|
+
except Exception as e:
|
|
89
|
+
print(f"Error caching chart data: {e}")
|
|
90
|
+
|
|
91
|
+
|
|
92
|
+
def cache_explanation(category: str, plot_name: str, explanation: str) -> None:
|
|
93
|
+
"""
|
|
94
|
+
Cache explanation hierarchically.
|
|
95
|
+
|
|
96
|
+
Parameters:
|
|
97
|
+
-----------
|
|
98
|
+
category : str
|
|
99
|
+
The category (e.g., "signal_evaluation")
|
|
100
|
+
plot_name : str
|
|
101
|
+
The name of the specific plot/analysis
|
|
102
|
+
explanation : str
|
|
103
|
+
The generated explanation
|
|
104
|
+
"""
|
|
105
|
+
_ensure_cache_dirs()
|
|
106
|
+
cache_path = _get_cache_path(category, plot_name, "explanations")
|
|
107
|
+
|
|
108
|
+
cache_data = {
|
|
109
|
+
"category": category,
|
|
110
|
+
"plot_name": plot_name,
|
|
111
|
+
"timestamp": datetime.now().isoformat(),
|
|
112
|
+
"explanation": explanation
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
try:
|
|
116
|
+
with open(cache_path, 'w') as f:
|
|
117
|
+
json.dump(cache_data, f, indent=2)
|
|
118
|
+
except Exception as e:
|
|
119
|
+
print(f"Error caching explanation: {e}")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def get_cached_data(category: str = None, plot_name: str = None) -> Dict[str, Any]:
|
|
123
|
+
"""
|
|
124
|
+
Retrieve cached data and explanations.
|
|
125
|
+
|
|
126
|
+
Parameters:
|
|
127
|
+
-----------
|
|
128
|
+
category : str, optional
|
|
129
|
+
Filter by category. If None, returns all categories
|
|
130
|
+
plot_name : str, optional
|
|
131
|
+
Filter by plot name. If None, returns all plots in category
|
|
132
|
+
|
|
133
|
+
Returns:
|
|
134
|
+
--------
|
|
135
|
+
dict : Hierarchical dictionary of cached data and explanations
|
|
136
|
+
"""
|
|
137
|
+
_ensure_cache_dirs()
|
|
138
|
+
result = {"data": {}, "explanations": {}}
|
|
139
|
+
|
|
140
|
+
# Get data
|
|
141
|
+
if category is None:
|
|
142
|
+
# Get all categories
|
|
143
|
+
if os.path.exists(DATA_CACHE_DIR):
|
|
144
|
+
for cat in os.listdir(DATA_CACHE_DIR):
|
|
145
|
+
cat_dir = os.path.join(DATA_CACHE_DIR, cat)
|
|
146
|
+
if os.path.isdir(cat_dir):
|
|
147
|
+
result["data"][cat] = {}
|
|
148
|
+
for plot_file in os.listdir(cat_dir):
|
|
149
|
+
if plot_file.endswith('.json'):
|
|
150
|
+
plot_name_key = plot_file[:-5] # Remove .json
|
|
151
|
+
try:
|
|
152
|
+
with open(os.path.join(cat_dir, plot_file), 'r') as f:
|
|
153
|
+
cached = json.load(f)
|
|
154
|
+
result["data"][cat][plot_name_key] = cached
|
|
155
|
+
except Exception as e:
|
|
156
|
+
print(f"Error reading cached data {plot_file}: {e}")
|
|
157
|
+
else:
|
|
158
|
+
# Get specific category
|
|
159
|
+
cat_dir = os.path.join(DATA_CACHE_DIR, category)
|
|
160
|
+
if os.path.exists(cat_dir):
|
|
161
|
+
result["data"][category] = {}
|
|
162
|
+
for plot_file in os.listdir(cat_dir):
|
|
163
|
+
if plot_file.endswith('.json'):
|
|
164
|
+
plot_name_key = plot_file[:-5] # Remove .json
|
|
165
|
+
try:
|
|
166
|
+
with open(os.path.join(cat_dir, plot_file), 'r') as f:
|
|
167
|
+
cached = json.load(f)
|
|
168
|
+
if plot_name is None or cached["plot_name"] == plot_name_key:
|
|
169
|
+
result["data"][category][plot_name_key] = cached
|
|
170
|
+
except Exception as e:
|
|
171
|
+
print(f"Error reading cached data {plot_file}: {e}")
|
|
172
|
+
|
|
173
|
+
# Get explanations
|
|
174
|
+
if category is None:
|
|
175
|
+
# Get all categories
|
|
176
|
+
if os.path.exists(EXPLANATION_CACHE_DIR):
|
|
177
|
+
for cat in os.listdir(EXPLANATION_CACHE_DIR):
|
|
178
|
+
cat_dir = os.path.join(EXPLANATION_CACHE_DIR, cat)
|
|
179
|
+
if os.path.isdir(cat_dir):
|
|
180
|
+
result["explanations"][cat] = {}
|
|
181
|
+
for plot_file in os.listdir(cat_dir):
|
|
182
|
+
if plot_file.endswith('.json'):
|
|
183
|
+
plot_name_key = plot_file[:-5] # Remove .json
|
|
184
|
+
try:
|
|
185
|
+
with open(os.path.join(cat_dir, plot_file), 'r') as f:
|
|
186
|
+
cached = json.load(f)
|
|
187
|
+
result["explanations"][cat][plot_name_key] = cached
|
|
188
|
+
except Exception as e:
|
|
189
|
+
print(f"Error reading cached explanation {plot_file}: {e}")
|
|
190
|
+
else:
|
|
191
|
+
# Get specific category
|
|
192
|
+
cat_dir = os.path.join(EXPLANATION_CACHE_DIR, category)
|
|
193
|
+
if os.path.exists(cat_dir):
|
|
194
|
+
result["explanations"][category] = {}
|
|
195
|
+
for plot_file in os.listdir(cat_dir):
|
|
196
|
+
if plot_file.endswith('.json'):
|
|
197
|
+
plot_name_key = plot_file[:-5] # Remove .json
|
|
198
|
+
try:
|
|
199
|
+
with open(os.path.join(cat_dir, plot_file), 'r') as f:
|
|
200
|
+
cached = json.load(f)
|
|
201
|
+
if plot_name is None or cached["plot_name"] == plot_name_key:
|
|
202
|
+
result["explanations"][category][plot_name_key] = cached
|
|
203
|
+
except Exception as e:
|
|
204
|
+
print(f"Error reading cached explanation {plot_file}: {e}")
|
|
205
|
+
|
|
206
|
+
return result
|
|
207
|
+
|
|
208
|
+
|
|
209
|
+
def list_cached_categories() -> list:
|
|
210
|
+
"""
|
|
211
|
+
List all available categories in the cache.
|
|
212
|
+
|
|
213
|
+
Returns:
|
|
214
|
+
--------
|
|
215
|
+
list : List of category names
|
|
216
|
+
"""
|
|
217
|
+
_ensure_cache_dirs()
|
|
218
|
+
categories = set()
|
|
219
|
+
|
|
220
|
+
for cache_type in [DATA_CACHE_DIR, EXPLANATION_CACHE_DIR]:
|
|
221
|
+
if os.path.exists(cache_type):
|
|
222
|
+
categories.update(os.listdir(cache_type))
|
|
223
|
+
|
|
224
|
+
return sorted(list(categories))
|
|
225
|
+
|
|
226
|
+
|
|
227
|
+
def list_cached_plots(category: str) -> list:
|
|
228
|
+
"""
|
|
229
|
+
List all available plots for a given category.
|
|
230
|
+
|
|
231
|
+
Parameters:
|
|
232
|
+
-----------
|
|
233
|
+
category : str
|
|
234
|
+
The category to list plots for
|
|
235
|
+
|
|
236
|
+
Returns:
|
|
237
|
+
--------
|
|
238
|
+
list : List of plot names
|
|
239
|
+
"""
|
|
240
|
+
_ensure_cache_dirs()
|
|
241
|
+
plots = set()
|
|
242
|
+
|
|
243
|
+
for cache_type in [DATA_CACHE_DIR, EXPLANATION_CACHE_DIR]:
|
|
244
|
+
cat_dir = os.path.join(cache_type, category)
|
|
245
|
+
if os.path.exists(cat_dir):
|
|
246
|
+
for plot_file in os.listdir(cat_dir):
|
|
247
|
+
if plot_file.endswith('.json'):
|
|
248
|
+
plots.add(plot_file[:-5]) # Remove .json
|
|
249
|
+
|
|
250
|
+
return sorted(list(plots))
|
|
251
|
+
|
|
252
|
+
|
|
253
|
+
def get_ephemeral_token(sovai_token: str = SOVAI_TOKEN, broker_url: str = BROKER_URL) -> Optional[str]:
|
|
254
|
+
"""
|
|
255
|
+
Get ephemeral token from the broker using Sovai key.
|
|
256
|
+
|
|
257
|
+
Parameters:
|
|
258
|
+
-----------
|
|
259
|
+
sovai_token : str
|
|
260
|
+
The Sovai authentication token
|
|
261
|
+
broker_url : str
|
|
262
|
+
The broker endpoint URL
|
|
263
|
+
|
|
264
|
+
Returns:
|
|
265
|
+
--------
|
|
266
|
+
str or None : Ephemeral token if successful, None otherwise
|
|
267
|
+
"""
|
|
268
|
+
headers = {
|
|
269
|
+
"X-User-Token": sovai_token
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
try:
|
|
273
|
+
response = requests.get(
|
|
274
|
+
f"{broker_url}/token",
|
|
275
|
+
headers=headers,
|
|
276
|
+
timeout=10
|
|
277
|
+
)
|
|
278
|
+
|
|
279
|
+
if response.status_code == 200:
|
|
280
|
+
data = response.json()
|
|
281
|
+
return data['token']
|
|
282
|
+
else:
|
|
283
|
+
print(f"Failed to get ephemeral token: {response.status_code}")
|
|
284
|
+
print(f"Response: {response.text}")
|
|
285
|
+
return None
|
|
286
|
+
|
|
287
|
+
except Exception as e:
|
|
288
|
+
print(f"Error getting ephemeral token: {e}")
|
|
289
|
+
return None
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
async def explain_chart_async(
|
|
293
|
+
chart_card: Dict[str, Any],
|
|
294
|
+
key_stats: Dict[str, Any],
|
|
295
|
+
description: str,
|
|
296
|
+
ephemeral_token: str
|
|
297
|
+
) -> str:
|
|
298
|
+
"""
|
|
299
|
+
Use Gemini to explain the chart using metadata.
|
|
300
|
+
|
|
301
|
+
Parameters:
|
|
302
|
+
-----------
|
|
303
|
+
chart_card : dict
|
|
304
|
+
Chart metadata including title, axes, series info
|
|
305
|
+
key_stats : dict
|
|
306
|
+
Key statistics from the chart
|
|
307
|
+
description : str
|
|
308
|
+
Chart description
|
|
309
|
+
ephemeral_token : str
|
|
310
|
+
Ephemeral token for Gemini API
|
|
311
|
+
|
|
312
|
+
Returns:
|
|
313
|
+
--------
|
|
314
|
+
str : Markdown explanation of the chart
|
|
315
|
+
"""
|
|
316
|
+
try:
|
|
317
|
+
# Create client with ephemeral token
|
|
318
|
+
client = genai.Client(
|
|
319
|
+
api_key=ephemeral_token,
|
|
320
|
+
http_options=types.HttpOptions(api_version="v1alpha")
|
|
321
|
+
)
|
|
322
|
+
|
|
323
|
+
model = "gemini-2.5-flash"
|
|
324
|
+
config = {"response_modalities": ["TEXT"]}
|
|
325
|
+
|
|
326
|
+
# Construct prompt with comprehensive metadata
|
|
327
|
+
prompt = f"""You are a financial data analyst. Explain this chart in clear, concise language.
|
|
328
|
+
|
|
329
|
+
**Chart Title:** {chart_card.get('title', 'Untitled')}
|
|
330
|
+
|
|
331
|
+
**Description:** {description}
|
|
332
|
+
|
|
333
|
+
**Chart Details:**
|
|
334
|
+
- X-axis: {chart_card.get('x_axis', 'N/A')}
|
|
335
|
+
- Y-axis: {chart_card.get('y_axis', 'N/A')}"""
|
|
336
|
+
|
|
337
|
+
# Add secondary y-axis if present
|
|
338
|
+
if chart_card.get('y2_axis'):
|
|
339
|
+
prompt += f"\n- Secondary Y-axis: {chart_card.get('y2_axis')}"
|
|
340
|
+
|
|
341
|
+
# Add series information
|
|
342
|
+
prompt += f"\n- Number of data series: {chart_card.get('series_count', 0)}"
|
|
343
|
+
if chart_card.get('series_names'):
|
|
344
|
+
series_list = chart_card.get('series_names')[:10] # Limit to first 10
|
|
345
|
+
prompt += f"\n- Series: {', '.join(series_list)}"
|
|
346
|
+
|
|
347
|
+
prompt += f"""
|
|
348
|
+
|
|
349
|
+
**Key Statistics:**
|
|
350
|
+
{chr(10).join([f"- {k}: {v}" for k, v in key_stats.items()])}
|
|
351
|
+
|
|
352
|
+
**Date Range:** {chart_card.get('date_range', {}).get('start', 'N/A')} to {chart_card.get('date_range', {}).get('end', 'N/A')}"""
|
|
353
|
+
|
|
354
|
+
# Add total periods if available
|
|
355
|
+
if chart_card.get('date_range', {}).get('total_periods'):
|
|
356
|
+
prompt += f" ({chart_card['date_range']['total_periods']} periods)"
|
|
357
|
+
|
|
358
|
+
prompt += """
|
|
359
|
+
|
|
360
|
+
Provide a 2-3 paragraph explanation that:
|
|
361
|
+
1. Summarizes what the chart shows
|
|
362
|
+
2. Highlights the most important insights from the key statistics
|
|
363
|
+
3. Explains what patterns or trends are visible
|
|
364
|
+
4. Provides context for interpreting the results
|
|
365
|
+
|
|
366
|
+
Write in markdown format with appropriate formatting."""
|
|
367
|
+
|
|
368
|
+
async with client.aio.live.connect(model=model, config=config) as session:
|
|
369
|
+
await session.send(input=prompt, end_of_turn=True)
|
|
370
|
+
|
|
371
|
+
explanation = ""
|
|
372
|
+
async for response in session.receive():
|
|
373
|
+
if response.text:
|
|
374
|
+
explanation += response.text
|
|
375
|
+
|
|
376
|
+
return explanation
|
|
377
|
+
|
|
378
|
+
except Exception as e:
|
|
379
|
+
return f"**Error generating explanation:** {str(e)}"
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _run_async_in_sync(coro):
|
|
383
|
+
"""
|
|
384
|
+
Run an async coroutine in a sync context.
|
|
385
|
+
Handles both environments with and without existing event loops.
|
|
386
|
+
"""
|
|
387
|
+
# Apply nest_asyncio to handle nested event loops (e.g., in Jupyter)
|
|
388
|
+
try:
|
|
389
|
+
import nest_asyncio
|
|
390
|
+
nest_asyncio.apply()
|
|
391
|
+
except ImportError:
|
|
392
|
+
pass
|
|
393
|
+
|
|
394
|
+
# Try to get or create event loop
|
|
395
|
+
try:
|
|
396
|
+
loop = asyncio.get_event_loop()
|
|
397
|
+
if loop.is_running():
|
|
398
|
+
# Already have a running loop, just run the coroutine on it
|
|
399
|
+
return loop.run_until_complete(coro)
|
|
400
|
+
else:
|
|
401
|
+
# Have a loop but it's not running, run the coroutine
|
|
402
|
+
return loop.run_until_complete(coro)
|
|
403
|
+
except RuntimeError:
|
|
404
|
+
# No loop exists, create new one and run
|
|
405
|
+
loop = asyncio.new_event_loop()
|
|
406
|
+
asyncio.set_event_loop(loop)
|
|
407
|
+
try:
|
|
408
|
+
return loop.run_until_complete(coro)
|
|
409
|
+
finally:
|
|
410
|
+
loop.close()
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def explain_chart(
|
|
414
|
+
fig,
|
|
415
|
+
display_explanation: bool = True,
|
|
416
|
+
category: str = None,
|
|
417
|
+
plot_name: str = None
|
|
418
|
+
) -> Optional[str]:
|
|
419
|
+
"""
|
|
420
|
+
Synchronous wrapper to explain a chart with LLM metadata.
|
|
421
|
+
|
|
422
|
+
Parameters:
|
|
423
|
+
-----------
|
|
424
|
+
fig : plotly.graph_objects.Figure
|
|
425
|
+
The Plotly figure with LLM metadata
|
|
426
|
+
display_explanation : bool
|
|
427
|
+
Whether to display the explanation immediately
|
|
428
|
+
category : str, optional
|
|
429
|
+
Category for caching (e.g., "signal_evaluation")
|
|
430
|
+
plot_name : str, optional
|
|
431
|
+
Plot name for caching (e.g., "performance_metrics")
|
|
432
|
+
|
|
433
|
+
Returns:
|
|
434
|
+
--------
|
|
435
|
+
str or None : The explanation markdown, or None if failed
|
|
436
|
+
"""
|
|
437
|
+
# Check if figure has metadata
|
|
438
|
+
if not hasattr(fig.layout, 'meta') or not fig.layout.meta:
|
|
439
|
+
if display_explanation:
|
|
440
|
+
display(Markdown("⚠️ **Chart does not have metadata for explanation.**"))
|
|
441
|
+
return None
|
|
442
|
+
|
|
443
|
+
metadata = fig.layout.meta
|
|
444
|
+
|
|
445
|
+
# Extract metadata components
|
|
446
|
+
chart_card = metadata.get('chart_card', {})
|
|
447
|
+
key_stats = chart_card.get('key_statistics', {})
|
|
448
|
+
description = chart_card.get('description', '')
|
|
449
|
+
|
|
450
|
+
if not key_stats:
|
|
451
|
+
if display_explanation:
|
|
452
|
+
display(Markdown("⚠️ **No key statistics available for explanation.**"))
|
|
453
|
+
return None
|
|
454
|
+
|
|
455
|
+
# Auto-generate category and plot_name from metadata if not provided
|
|
456
|
+
if category is None or plot_name is None:
|
|
457
|
+
title = chart_card.get('title', 'untitled')
|
|
458
|
+
if category is None:
|
|
459
|
+
# First check if category is stored in figure metadata
|
|
460
|
+
if 'category' in metadata:
|
|
461
|
+
category = metadata['category']
|
|
462
|
+
else:
|
|
463
|
+
# Fallback: Try to detect category from title
|
|
464
|
+
title_lower = title.lower()
|
|
465
|
+
|
|
466
|
+
# Check for signal-related terms
|
|
467
|
+
if any(term in title_lower for term in ['signal', 'decile', 'sharpe', 'cumulative_returns', 'performance']):
|
|
468
|
+
category = "signal_evaluation"
|
|
469
|
+
# Check for other patterns
|
|
470
|
+
elif "_" in title:
|
|
471
|
+
parts = title.split("_")
|
|
472
|
+
if len(parts) >= 2:
|
|
473
|
+
category = parts[0]
|
|
474
|
+
else:
|
|
475
|
+
category = "general"
|
|
476
|
+
|
|
477
|
+
if plot_name is None:
|
|
478
|
+
# Use title as plot name, sanitize it
|
|
479
|
+
plot_name = title.lower().replace(" ", "_").replace("/", "_").replace("\\", "_").replace("(", "").replace(")", "").replace(",", "")
|
|
480
|
+
|
|
481
|
+
# Cache the chart data
|
|
482
|
+
cache_chart_data(category, plot_name, chart_card)
|
|
483
|
+
|
|
484
|
+
# Get ephemeral token
|
|
485
|
+
ephemeral_token = get_ephemeral_token()
|
|
486
|
+
|
|
487
|
+
if not ephemeral_token:
|
|
488
|
+
if display_explanation:
|
|
489
|
+
display(Markdown("❌ **Failed to get ephemeral token. Cannot generate explanation.**"))
|
|
490
|
+
return None
|
|
491
|
+
|
|
492
|
+
# Generate explanation
|
|
493
|
+
try:
|
|
494
|
+
# Run async function in sync context with proper event loop handling
|
|
495
|
+
explanation = _run_async_in_sync(explain_chart_async(
|
|
496
|
+
chart_card=chart_card,
|
|
497
|
+
key_stats=key_stats,
|
|
498
|
+
description=description,
|
|
499
|
+
ephemeral_token=ephemeral_token
|
|
500
|
+
))
|
|
501
|
+
|
|
502
|
+
# Cache the explanation
|
|
503
|
+
cache_explanation(category, plot_name, explanation)
|
|
504
|
+
|
|
505
|
+
if display_explanation:
|
|
506
|
+
display(Markdown("---"))
|
|
507
|
+
display(Markdown("## 🤖 Chart Explanation"))
|
|
508
|
+
display(Markdown(explanation))
|
|
509
|
+
display(Markdown("---"))
|
|
510
|
+
|
|
511
|
+
return explanation
|
|
512
|
+
|
|
513
|
+
except Exception as e:
|
|
514
|
+
error_msg = f"❌ **Error generating explanation:** {str(e)}"
|
|
515
|
+
if display_explanation:
|
|
516
|
+
display(Markdown(error_msg))
|
|
517
|
+
return None
|
|
518
|
+
|
|
519
|
+
|
|
520
|
+
def auto_explain_chart(fig):
|
|
521
|
+
"""
|
|
522
|
+
Automatically explain a chart after it's created.
|
|
523
|
+
|
|
524
|
+
This is a convenience function that can be called after
|
|
525
|
+
generating any plot to get an AI explanation.
|
|
526
|
+
|
|
527
|
+
Parameters:
|
|
528
|
+
-----------
|
|
529
|
+
fig : plotly.graph_objects.Figure
|
|
530
|
+
The Plotly figure to explain
|
|
531
|
+
|
|
532
|
+
Returns:
|
|
533
|
+
--------
|
|
534
|
+
plotly.graph_objects.Figure : The same figure (for chaining)
|
|
535
|
+
"""
|
|
536
|
+
explain_chart(fig, display_explanation=True)
|
|
537
|
+
return fig
|