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.
Files changed (113) hide show
  1. {sovai-0.2.74 → sovai-0.2.76}/PKG-INFO +3 -1
  2. {sovai-0.2.74 → sovai-0.2.76}/pyproject.toml +5 -2
  3. {sovai-0.2.74 → sovai-0.2.76}/sovai/__init__.py +22 -1
  4. sovai-0.2.76/sovai/extensions/__init__.py +16 -0
  5. sovai-0.2.76/sovai/extensions/chart_explainer.py +537 -0
  6. sovai-0.2.76/sovai/extensions/overall_explainers.py +352 -0
  7. sovai-0.2.76/sovai/extensions/signal_evaluation copy.py +2720 -0
  8. sovai-0.2.76/sovai/extensions/signal_evaluation.py +2732 -0
  9. sovai-0.2.74/sovai/extensions/signal_evaluation.py → sovai-0.2.76/sovai/extensions/signal_evaluation_old.py +8 -8
  10. sovai-0.2.76/sovai/extensions/table_explainer.py +570 -0
  11. {sovai-0.2.74 → sovai-0.2.76}/sovai/get_tools.py +5 -13
  12. sovai-0.2.76/sovai/tools/explainers.py +451 -0
  13. sovai-0.2.74/data/codes.parq +0 -0
  14. sovai-0.2.74/data/tickers.parq +0 -0
  15. sovai-0.2.74/sovai/assets/features_mapping.parq +0 -0
  16. sovai-0.2.74/sovai/assets/tickers.parq +0 -0
  17. sovai-0.2.74/sovai/utils/__init__.py +0 -0
  18. {sovai-0.2.74 → sovai-0.2.76}/LICENSE +0 -0
  19. {sovai-0.2.74 → sovai-0.2.76}/README.md +0 -0
  20. {sovai-0.2.74 → sovai-0.2.76}/sovai/.DS_Store +0 -0
  21. {sovai-0.2.74 → sovai-0.2.76}/sovai/api_config.py +0 -0
  22. {sovai-0.2.74 → sovai-0.2.76}/sovai/assets/dropdown.css +0 -0
  23. {sovai-0.2.74 → sovai-0.2.76}/sovai/basic_auth.py +0 -0
  24. {sovai-0.2.74 → sovai-0.2.76}/sovai/computations/__init__.py +0 -0
  25. {sovai-0.2.74 → sovai-0.2.76}/sovai/computations/functions.py +0 -0
  26. {sovai-0.2.74 → sovai-0.2.76}/sovai/errors/__init__.py +0 -0
  27. {sovai-0.2.74 → sovai-0.2.76}/sovai/errors/sovai_errors.py +0 -0
  28. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/anomalies.py +0 -0
  29. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/ask_df_llm.py +0 -0
  30. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/change_point_generator copy.py +0 -0
  31. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/change_point_generator.py +0 -0
  32. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/clustering.py +0 -0
  33. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/core_kshape.py +0 -0
  34. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/cum_sum_torch.py +0 -0
  35. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/cusum.py +0 -0
  36. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/dimensionality_reduction.py +0 -0
  37. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/feature_extraction.py +0 -0
  38. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/feature_importance.py +0 -0
  39. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/feature_neutralizer.py +0 -0
  40. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/filter_df.py +0 -0
  41. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/fractional_differencing.py +0 -0
  42. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/nowcasting.py +0 -0
  43. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/pairwise.py +0 -0
  44. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/pandas_extensions.py +0 -0
  45. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/parallel_functions.py +0 -0
  46. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/pfa_feature_selector.py +0 -0
  47. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/regime_change.py +0 -0
  48. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/regime_change_pca.py +0 -0
  49. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/shapley_global_importance.py +0 -0
  50. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/shapley_importance.py +0 -0
  51. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/technical_indicators.py +0 -0
  52. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/time_decomposition.py +0 -0
  53. {sovai-0.2.74 → sovai-0.2.76}/sovai/extensions/weight_optimization.py +0 -0
  54. {sovai-0.2.74 → sovai-0.2.76}/sovai/get_compute.py +0 -0
  55. {sovai-0.2.74 → sovai-0.2.76}/sovai/get_data copy 2.py +0 -0
  56. {sovai-0.2.74 → sovai-0.2.76}/sovai/get_data copy.py +0 -0
  57. {sovai-0.2.74 → sovai-0.2.76}/sovai/get_data.py +0 -0
  58. {sovai-0.2.74 → sovai-0.2.76}/sovai/get_plots.py +0 -0
  59. {sovai-0.2.74 → sovai-0.2.76}/sovai/get_reports.py +0 -0
  60. {sovai-0.2.74/sovai/extensions → sovai-0.2.76/sovai/plots}/__init__.py +0 -0
  61. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/accounting/__init__.py +0 -0
  62. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/accounting/accounting_plots.py +0 -0
  63. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/allocation/__init__.py +0 -0
  64. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/allocation/allocation_plots.py +0 -0
  65. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/bankruptcy/__init__.py +0 -0
  66. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/bankruptcy/bankruptcy_plots.py +0 -0
  67. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/breakout/__init__.py +0 -0
  68. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/breakout/breakout_plots.py +0 -0
  69. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/corp_risk/__init__.py +0 -0
  70. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/corp_risk/corp_risk_plots.py +0 -0
  71. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/earnings_surprise/__init__.py +0 -0
  72. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/earnings_surprise/earnings_surprise_plots.py +0 -0
  73. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/insider/__init__.py +0 -0
  74. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/insider/insider_plots.py +0 -0
  75. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/institutional/__init__.py +0 -0
  76. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/institutional/institutional_plots.py +0 -0
  77. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/news/__init__.py +0 -0
  78. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/news/news_plots.py +0 -0
  79. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/ratios/__init__.py +0 -0
  80. {sovai-0.2.74 → sovai-0.2.76}/sovai/plots/ratios/ratios_plots.py +0 -0
  81. {sovai-0.2.74 → sovai-0.2.76}/sovai/reports/accounting/__init__.py +0 -0
  82. {sovai-0.2.74 → sovai-0.2.76}/sovai/reports/accounting/accounting_balance_sheet.py +0 -0
  83. {sovai-0.2.74 → sovai-0.2.76}/sovai/reports/bankruptcy/__init__.py +0 -0
  84. {sovai-0.2.74 → sovai-0.2.76}/sovai/reports/bankruptcy/bankruptcy_monthly_top.py +0 -0
  85. {sovai-0.2.74 → sovai-0.2.76}/sovai/reports/general/__init__.py +0 -0
  86. {sovai-0.2.74 → sovai-0.2.76}/sovai/reports/general/general_plots.py +0 -0
  87. {sovai-0.2.74 → sovai-0.2.76}/sovai/reports/news/__inti__.py +0 -0
  88. {sovai-0.2.74 → sovai-0.2.76}/sovai/reports/news/news_econometric_analysis.py +0 -0
  89. {sovai-0.2.74 → sovai-0.2.76}/sovai/studies/nowcasting.py +0 -0
  90. {sovai-0.2.74 → sovai-0.2.76}/sovai/token_auth.py +0 -0
  91. {sovai-0.2.74/sovai/plots → sovai-0.2.76/sovai/tools/authentication}/__init__.py +0 -0
  92. {sovai-0.2.74 → sovai-0.2.76}/sovai/tools/authentication/authentication.py +0 -0
  93. {sovai-0.2.74/sovai/tools/authentication → sovai-0.2.76/sovai/tools/sec}/__init__.py +0 -0
  94. {sovai-0.2.74 → sovai-0.2.76}/sovai/tools/sec/graphs.py +0 -0
  95. {sovai-0.2.74 → sovai-0.2.76}/sovai/tools/sec/llm_code_generator.py +0 -0
  96. {sovai-0.2.74 → sovai-0.2.76}/sovai/tools/sec/sec_10_k_8_k_filings.py +0 -0
  97. {sovai-0.2.74 → sovai-0.2.76}/sovai/tools/sec/sec_edgar_search.py +0 -0
  98. {sovai-0.2.74/sovai/tools/sec → sovai-0.2.76/sovai/utils}/__init__.py +0 -0
  99. {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/client_side.py +0 -0
  100. {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/client_side_s3.py +0 -0
  101. {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/client_side_s3_part_high.py +0 -0
  102. {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/converter.py +0 -0
  103. {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/datetime_formats.py +0 -0
  104. {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/file_management copy.py +0 -0
  105. {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/file_management.py +0 -0
  106. {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/get_tickers.py +0 -0
  107. {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/helpers.py +0 -0
  108. {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/pandas_extensions copy.py +0 -0
  109. {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/plot.py +0 -0
  110. {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/port_manager copy.py +0 -0
  111. {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/port_manager.py +0 -0
  112. {sovai-0.2.74 → sovai-0.2.76}/sovai/utils/stream.py +0 -0
  113. {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.74
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.74"
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