sovai 0.2.78__tar.gz → 0.2.80__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.78 → sovai-0.2.80}/PKG-INFO +1 -1
- {sovai-0.2.78 → sovai-0.2.80}/pyproject.toml +1 -1
- sovai-0.2.80/sovai/compat.py +72 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/get_data.py +20 -4
- sovai-0.2.78/sovai/.DS_Store +0 -0
- sovai-0.2.78/sovai/extensions/change_point_generator copy.py +0 -446
- sovai-0.2.78/sovai/extensions/signal_evaluation copy.py +0 -2720
- sovai-0.2.78/sovai/get_data copy 2.py +0 -1688
- sovai-0.2.78/sovai/get_data copy.py +0 -1732
- sovai-0.2.78/sovai/utils/file_management copy.py +0 -79
- sovai-0.2.78/sovai/utils/pandas_extensions copy.py +0 -222
- sovai-0.2.78/sovai/utils/port_manager copy.py +0 -25
- {sovai-0.2.78 → sovai-0.2.80}/LICENSE +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/README.md +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/api_config.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/assets/dropdown.css +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/basic_auth.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/computations/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/computations/functions.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/errors/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/errors/sovai_errors.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/anomalies.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/ask_df_llm.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/change_point_generator.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/chart_explainer.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/clustering.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/core_kshape.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/cum_sum_torch.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/cusum.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/dimensionality_reduction.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/feature_extraction.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/feature_importance.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/feature_neutralizer.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/filter_df.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/fractional_differencing.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/nowcasting.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/overall_explainers.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/pairwise.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/pandas_extensions.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/parallel_functions.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/pfa_feature_selector.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/regime_change.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/regime_change_pca.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/shapley_global_importance.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/shapley_importance.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/signal_evaluation.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/signal_evaluation_old.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/table_explainer.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/technical_indicators.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/time_decomposition.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/extensions/weight_optimization.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/get_compute.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/get_plots.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/get_reports.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/get_tools.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/accounting/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/accounting/accounting_plots.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/allocation/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/allocation/allocation_plots.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/bankruptcy/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/bankruptcy/bankruptcy_plots.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/breakout/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/breakout/breakout_plots.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/corp_risk/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/corp_risk/corp_risk_plots.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/earnings_surprise/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/earnings_surprise/earnings_surprise_plots.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/insider/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/insider/insider_plots.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/institutional/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/institutional/institutional_plots.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/news/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/news/news_plots.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/ratios/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/plots/ratios/ratios_plots.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/reports/accounting/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/reports/accounting/accounting_balance_sheet.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/reports/bankruptcy/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/reports/bankruptcy/bankruptcy_monthly_top.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/reports/general/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/reports/general/general_plots.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/reports/news/__inti__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/reports/news/news_econometric_analysis.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/studies/nowcasting.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/token_auth.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/tools/authentication/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/tools/authentication/authentication.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/tools/explainers.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/tools/sec/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/tools/sec/graphs.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/tools/sec/llm_code_generator.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/tools/sec/sec_10_k_8_k_filings.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/tools/sec/sec_edgar_search.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/utils/__init__.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/utils/client_side.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/utils/client_side_s3.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/utils/client_side_s3_part_high.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/utils/converter.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/utils/datetime_formats.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/utils/file_management.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/utils/get_tickers.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/utils/helpers.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/utils/plot.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/utils/port_manager.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/utils/stream.py +0 -0
- {sovai-0.2.78 → sovai-0.2.80}/sovai/utils/verbose_utils.py +0 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
"""
|
|
2
|
+
SDK-side compatibility checker.
|
|
3
|
+
|
|
4
|
+
On first API call, reads server response headers to detect version deprecation
|
|
5
|
+
warnings. Never crashes -- degraded functionality is better than no functionality.
|
|
6
|
+
"""
|
|
7
|
+
from __future__ import annotations
|
|
8
|
+
|
|
9
|
+
import logging
|
|
10
|
+
import warnings
|
|
11
|
+
from typing import Optional
|
|
12
|
+
|
|
13
|
+
logger = logging.getLogger(__name__)
|
|
14
|
+
|
|
15
|
+
# Module-level flag so we warn at most once per process
|
|
16
|
+
_COMPAT_CHECKED: bool = False
|
|
17
|
+
_DEPRECATION_WARNING_SHOWN: bool = False
|
|
18
|
+
|
|
19
|
+
|
|
20
|
+
def get_sdk_version() -> str:
|
|
21
|
+
"""Return the installed SDK version, falling back to '0.0.0'."""
|
|
22
|
+
try:
|
|
23
|
+
from importlib.metadata import version as _pkg_version
|
|
24
|
+
return _pkg_version("sovai")
|
|
25
|
+
except Exception:
|
|
26
|
+
return "0.0.0"
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def check_response_compat(response_headers: dict[str, str]) -> None:
|
|
30
|
+
"""
|
|
31
|
+
Inspect server response headers for SDK compatibility signals.
|
|
32
|
+
|
|
33
|
+
Called after every API response. Emits at most one user-facing warning
|
|
34
|
+
per process lifetime to avoid log spam.
|
|
35
|
+
|
|
36
|
+
Headers checked:
|
|
37
|
+
X-SDK-Deprecation-Warning -- soft warning, SDK is below recommended
|
|
38
|
+
X-SDK-Upgrade-Required -- hard signal, SDK is critically outdated
|
|
39
|
+
"""
|
|
40
|
+
global _COMPAT_CHECKED, _DEPRECATION_WARNING_SHOWN
|
|
41
|
+
|
|
42
|
+
deprecation_msg: Optional[str] = response_headers.get("X-SDK-Deprecation-Warning")
|
|
43
|
+
upgrade_msg: Optional[str] = response_headers.get("X-SDK-Upgrade-Required")
|
|
44
|
+
|
|
45
|
+
if upgrade_msg and not _DEPRECATION_WARNING_SHOWN:
|
|
46
|
+
_DEPRECATION_WARNING_SHOWN = True
|
|
47
|
+
warnings.warn(
|
|
48
|
+
f"[sovai] SDK upgrade required: {upgrade_msg}. "
|
|
49
|
+
f"Run: pip install --upgrade sovai",
|
|
50
|
+
UserWarning,
|
|
51
|
+
stacklevel=3,
|
|
52
|
+
)
|
|
53
|
+
logger.warning("sdk.upgrade_required", extra={"detail": upgrade_msg})
|
|
54
|
+
|
|
55
|
+
elif deprecation_msg and not _DEPRECATION_WARNING_SHOWN:
|
|
56
|
+
_DEPRECATION_WARNING_SHOWN = True
|
|
57
|
+
warnings.warn(
|
|
58
|
+
f"[sovai] {deprecation_msg}. "
|
|
59
|
+
f"Run: pip install --upgrade sovai",
|
|
60
|
+
DeprecationWarning,
|
|
61
|
+
stacklevel=3,
|
|
62
|
+
)
|
|
63
|
+
logger.info("sdk.deprecation_warning", extra={"detail": deprecation_msg})
|
|
64
|
+
|
|
65
|
+
_COMPAT_CHECKED = True
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def reset_compat_state() -> None:
|
|
69
|
+
"""Reset module state -- useful for testing."""
|
|
70
|
+
global _COMPAT_CHECKED, _DEPRECATION_WARNING_SHOWN
|
|
71
|
+
_COMPAT_CHECKED = False
|
|
72
|
+
_DEPRECATION_WARNING_SHOWN = False
|
|
@@ -223,8 +223,16 @@ class ApiRequestHandler:
|
|
|
223
223
|
self.max_retries = max_retries
|
|
224
224
|
self.backoff_factor = backoff_factor
|
|
225
225
|
self.timeout = timeout
|
|
226
|
-
self.headers = {"Authorization": f"Bearer {token}"}
|
|
226
|
+
self.headers: Dict[str, str] = {"Authorization": f"Bearer {token}"}
|
|
227
227
|
self.logger = logger or logging.getLogger(__name__)
|
|
228
|
+
|
|
229
|
+
# Inject SDK version header for server-side compatibility checks
|
|
230
|
+
try:
|
|
231
|
+
from importlib.metadata import version as _pkg_version
|
|
232
|
+
sdk_version = _pkg_version("sovai")
|
|
233
|
+
except Exception:
|
|
234
|
+
sdk_version = "0.0.0"
|
|
235
|
+
self.headers["X-SDK-Version"] = sdk_version
|
|
228
236
|
|
|
229
237
|
def _should_retry(self, exception: Exception, attempt: int) -> bool:
|
|
230
238
|
"""
|
|
@@ -306,6 +314,14 @@ class ApiRequestHandler:
|
|
|
306
314
|
)
|
|
307
315
|
response.raise_for_status()
|
|
308
316
|
self.logger.debug(f"Request successful (status: {response.status_code})")
|
|
317
|
+
|
|
318
|
+
# Check server compatibility headers (warns at most once per process)
|
|
319
|
+
try:
|
|
320
|
+
from sovai.compat import check_response_compat
|
|
321
|
+
check_response_compat(dict(response.headers))
|
|
322
|
+
except Exception:
|
|
323
|
+
pass # Never let compat checking break data retrieval
|
|
324
|
+
|
|
309
325
|
return response
|
|
310
326
|
|
|
311
327
|
except Exception as e:
|
|
@@ -447,7 +463,7 @@ ENDPOINT_TO_TICKER = {
|
|
|
447
463
|
}
|
|
448
464
|
|
|
449
465
|
# Define endpoint sets for different processing methods
|
|
450
|
-
|
|
466
|
+
CLIENT_SIDE_ENDPOINTS_R2 = {
|
|
451
467
|
# "ratios/relative",
|
|
452
468
|
"market/prices",
|
|
453
469
|
"market/closeadj",
|
|
@@ -568,7 +584,7 @@ ENDPOINT_ALIASES = {
|
|
|
568
584
|
|
|
569
585
|
# Define handlers for different endpoint types
|
|
570
586
|
ENDPOINT_HANDLERS = [
|
|
571
|
-
(
|
|
587
|
+
(CLIENT_SIDE_ENDPOINTS_R2, client_side_frame, "Grabbing R2 client side"),
|
|
572
588
|
(CLIENT_SIDE_ENDPOINTS_S3, load_frame_s3, "Grabbing S3 client side"),
|
|
573
589
|
(CLIENT_SIDE_ENDPOINTS_S3_PART_HIGH, load_frame_s3_partitioned_high, "Grabbing S3 Partitioned High client side"),
|
|
574
590
|
]
|
|
@@ -1835,7 +1851,7 @@ This library is designed around several key components:
|
|
|
1835
1851
|
different parameters (tickers, date ranges, formats).
|
|
1836
1852
|
|
|
1837
1853
|
2. DATA SOURCE ABSTRACTION:
|
|
1838
|
-
The code abstracts away the complexities of different data sources (
|
|
1854
|
+
The code abstracts away the complexities of different data sources (R2, S3, API)
|
|
1839
1855
|
through specialized endpoint handlers, allowing for transparent access regardless
|
|
1840
1856
|
of where the data is stored.
|
|
1841
1857
|
|
sovai-0.2.78/sovai/.DS_Store
DELETED
|
Binary file
|
|
@@ -1,446 +0,0 @@
|
|
|
1
|
-
import numpy as np
|
|
2
|
-
import pandas as pd
|
|
3
|
-
from typing import Tuple
|
|
4
|
-
import plotly.graph_objects as go
|
|
5
|
-
from plotly.subplots import make_subplots
|
|
6
|
-
import dash
|
|
7
|
-
from dash import dcc, html
|
|
8
|
-
from dash.dependencies import Input, Output
|
|
9
|
-
import random
|
|
10
|
-
|
|
11
|
-
random.seed(42)
|
|
12
|
-
np.random.seed(42)
|
|
13
|
-
|
|
14
|
-
# Your code that uses NumPy's random functions
|
|
15
|
-
# Global dictionary to keep track of apps and their ports
|
|
16
|
-
app_ports = {}
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
def get_unique_port(app_name):
|
|
20
|
-
global app_ports
|
|
21
|
-
min_port = 8050 # define the range of ports you want to use
|
|
22
|
-
max_port = 8099
|
|
23
|
-
|
|
24
|
-
if app_name in app_ports:
|
|
25
|
-
# Return the previously assigned port for this app
|
|
26
|
-
return app_ports[app_name]
|
|
27
|
-
else:
|
|
28
|
-
# Generate a unique port for the new app
|
|
29
|
-
while True:
|
|
30
|
-
port = random.randint(min_port, max_port)
|
|
31
|
-
if (
|
|
32
|
-
port not in app_ports.values()
|
|
33
|
-
): # check if the port is not already in use
|
|
34
|
-
app_ports[app_name] = port
|
|
35
|
-
return port
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
class ImprovedCusumDetector:
|
|
39
|
-
def __init__(
|
|
40
|
-
self,
|
|
41
|
-
window_size_ratio: float = 0.05,
|
|
42
|
-
threshold_factor: float = 2.5,
|
|
43
|
-
drift_factor: float = 0.05,
|
|
44
|
-
min_distance: int = 30,
|
|
45
|
-
sideways_threshold: float = 0.03,
|
|
46
|
-
warm_up_period_ratio: float = 0.1,
|
|
47
|
-
):
|
|
48
|
-
self.window_size_ratio = window_size_ratio
|
|
49
|
-
self.threshold_factor = threshold_factor
|
|
50
|
-
self.drift_factor = drift_factor
|
|
51
|
-
self.min_distance = min_distance
|
|
52
|
-
self.sideways_threshold = sideways_threshold
|
|
53
|
-
self.warm_up_period_ratio = warm_up_period_ratio
|
|
54
|
-
|
|
55
|
-
def detect_changepoints(
|
|
56
|
-
self, signal: np.ndarray
|
|
57
|
-
) -> Tuple[np.ndarray, np.ndarray, np.ndarray, int]:
|
|
58
|
-
n = len(signal)
|
|
59
|
-
window_size = max(int(n * self.window_size_ratio), 8)
|
|
60
|
-
warm_up_period = int(n * self.warm_up_period_ratio)
|
|
61
|
-
|
|
62
|
-
ewma = self._calculate_ewma(signal, window_size)
|
|
63
|
-
z_scores = self._calculate_rolling_z_scores(signal, ewma, window_size)
|
|
64
|
-
scores, changepoints, trend_codes = self._calculate_two_sided_cusum(
|
|
65
|
-
signal,
|
|
66
|
-
z_scores,
|
|
67
|
-
self.drift_factor,
|
|
68
|
-
self.threshold_factor,
|
|
69
|
-
self.min_distance,
|
|
70
|
-
self.sideways_threshold,
|
|
71
|
-
warm_up_period,
|
|
72
|
-
)
|
|
73
|
-
|
|
74
|
-
trends = np.where(
|
|
75
|
-
trend_codes == 1,
|
|
76
|
-
"Increasing",
|
|
77
|
-
np.where(trend_codes == -1, "Decreasing", "Sideways"),
|
|
78
|
-
)
|
|
79
|
-
|
|
80
|
-
return changepoints, trends, scores, window_size
|
|
81
|
-
|
|
82
|
-
import numba
|
|
83
|
-
import numpy as np
|
|
84
|
-
|
|
85
|
-
@staticmethod
|
|
86
|
-
@numba.njit(cache=True)
|
|
87
|
-
def _calculate_ewma(signal, window_size):
|
|
88
|
-
alpha = 2 / (window_size + 1)
|
|
89
|
-
ewma = np.zeros_like(signal)
|
|
90
|
-
ewma[0] = signal[0]
|
|
91
|
-
for i in range(1, len(signal)):
|
|
92
|
-
ewma[i] = alpha * signal[i] + (1 - alpha) * ewma[i - 1]
|
|
93
|
-
return ewma
|
|
94
|
-
|
|
95
|
-
@staticmethod
|
|
96
|
-
@numba.njit(cache=True)
|
|
97
|
-
def _calculate_rolling_z_scores(signal, ewma, window_size):
|
|
98
|
-
rolling_var = np.zeros_like(signal)
|
|
99
|
-
for i in range(window_size, len(signal)):
|
|
100
|
-
rolling_var[i] = np.var(signal[i - window_size + 1 : i + 1])
|
|
101
|
-
rolling_std = np.sqrt(rolling_var)
|
|
102
|
-
z_scores = (signal - ewma) / (rolling_std + 1e-8)
|
|
103
|
-
return z_scores
|
|
104
|
-
|
|
105
|
-
@staticmethod
|
|
106
|
-
@numba.njit(cache=True)
|
|
107
|
-
def _calculate_two_sided_cusum(
|
|
108
|
-
signal,
|
|
109
|
-
z,
|
|
110
|
-
drift,
|
|
111
|
-
threshold_factor,
|
|
112
|
-
min_distance,
|
|
113
|
-
sideways_threshold,
|
|
114
|
-
warm_up_period,
|
|
115
|
-
):
|
|
116
|
-
cs_upper = np.zeros_like(z)
|
|
117
|
-
cs_lower = np.zeros_like(z)
|
|
118
|
-
changepoints = []
|
|
119
|
-
trend_codes = []
|
|
120
|
-
last_cp = -min_distance
|
|
121
|
-
for i in range(1, len(z)):
|
|
122
|
-
if i < warm_up_period:
|
|
123
|
-
continue
|
|
124
|
-
cs_upper[i] = max(0, cs_upper[i - 1] + z[i] - drift)
|
|
125
|
-
cs_lower[i] = min(0, cs_lower[i - 1] + z[i] + drift)
|
|
126
|
-
if i - last_cp >= min_distance:
|
|
127
|
-
if cs_upper[i] > threshold_factor * np.sqrt(i):
|
|
128
|
-
changepoints.append(i)
|
|
129
|
-
trend_codes.append(1)
|
|
130
|
-
cs_upper[i] = 0
|
|
131
|
-
cs_lower[i] = 0
|
|
132
|
-
last_cp = i
|
|
133
|
-
elif cs_lower[i] < -threshold_factor * np.sqrt(i):
|
|
134
|
-
changepoints.append(i)
|
|
135
|
-
trend_codes.append(-1)
|
|
136
|
-
cs_upper[i] = 0
|
|
137
|
-
cs_lower[i] = 0
|
|
138
|
-
last_cp = i
|
|
139
|
-
elif (
|
|
140
|
-
i > 0
|
|
141
|
-
and abs((signal[i] - signal[last_cp]) / signal[last_cp])
|
|
142
|
-
< sideways_threshold
|
|
143
|
-
):
|
|
144
|
-
changepoints.append(i)
|
|
145
|
-
trend_codes.append(0)
|
|
146
|
-
cs_upper[i] = 0
|
|
147
|
-
cs_lower[i] = 0
|
|
148
|
-
last_cp = i
|
|
149
|
-
return (
|
|
150
|
-
np.maximum(cs_upper, -cs_lower),
|
|
151
|
-
np.array(changepoints),
|
|
152
|
-
np.array(trend_codes),
|
|
153
|
-
)
|
|
154
|
-
|
|
155
|
-
def plot_cusum_results(
|
|
156
|
-
df_signal, signal_array, changepoints, trends, scores, ticker, feature
|
|
157
|
-
):
|
|
158
|
-
fig = make_subplots(
|
|
159
|
-
rows=2,
|
|
160
|
-
cols=1,
|
|
161
|
-
shared_xaxes=True,
|
|
162
|
-
vertical_spacing=0.1,
|
|
163
|
-
subplot_titles=(f"{feature} Over Time for {ticker}", "CUSUM Score"),
|
|
164
|
-
)
|
|
165
|
-
|
|
166
|
-
fig.add_trace(
|
|
167
|
-
go.Scatter(
|
|
168
|
-
x=df_signal.index,
|
|
169
|
-
y=signal_array,
|
|
170
|
-
mode="lines",
|
|
171
|
-
line=dict(color="gold", width=2),
|
|
172
|
-
name=feature,
|
|
173
|
-
),
|
|
174
|
-
row=1,
|
|
175
|
-
col=1,
|
|
176
|
-
)
|
|
177
|
-
|
|
178
|
-
fig.add_trace(
|
|
179
|
-
go.Scatter(
|
|
180
|
-
x=df_signal.index,
|
|
181
|
-
y=scores,
|
|
182
|
-
mode="lines",
|
|
183
|
-
line=dict(color="rgba(30, 144, 255, 0.7)", width=1.5),
|
|
184
|
-
name="CUSUM Score",
|
|
185
|
-
),
|
|
186
|
-
row=2,
|
|
187
|
-
col=1,
|
|
188
|
-
)
|
|
189
|
-
|
|
190
|
-
colors = {"Increasing": "lime", "Decreasing": "red", "Sideways": "yellow"}
|
|
191
|
-
symbols = {
|
|
192
|
-
"Increasing": "triangle-up",
|
|
193
|
-
"Decreasing": "triangle-down",
|
|
194
|
-
"Sideways": "circle",
|
|
195
|
-
}
|
|
196
|
-
|
|
197
|
-
for trend in set(trends):
|
|
198
|
-
mask = trends == trend
|
|
199
|
-
fig.add_trace(
|
|
200
|
-
go.Scatter(
|
|
201
|
-
x=df_signal.index[changepoints[mask]],
|
|
202
|
-
y=signal_array[changepoints[mask]],
|
|
203
|
-
mode="markers",
|
|
204
|
-
marker=dict(color=colors[trend], size=10, symbol=symbols[trend]),
|
|
205
|
-
name=f"{trend}",
|
|
206
|
-
),
|
|
207
|
-
row=1,
|
|
208
|
-
col=1,
|
|
209
|
-
)
|
|
210
|
-
|
|
211
|
-
fig.update_layout(
|
|
212
|
-
title=f"{feature} Change Point Detection for {ticker}",
|
|
213
|
-
plot_bgcolor="rgba(0,0,0,0.95)",
|
|
214
|
-
paper_bgcolor="rgba(0,0,0,0.95)",
|
|
215
|
-
font=dict(color="white", size=12),
|
|
216
|
-
autosize=True,
|
|
217
|
-
legend=dict(
|
|
218
|
-
orientation="h",
|
|
219
|
-
yanchor="bottom",
|
|
220
|
-
y=1.1,
|
|
221
|
-
xanchor="right",
|
|
222
|
-
x=1,
|
|
223
|
-
bgcolor="rgba(0,0,0,0.5)",
|
|
224
|
-
bordercolor="rgba(255,255,255,0.5)",
|
|
225
|
-
),
|
|
226
|
-
showlegend=True,
|
|
227
|
-
)
|
|
228
|
-
|
|
229
|
-
fig.update_xaxes(title_text="Date", row=2, col=1, gridcolor="rgba(255,255,255,0.2)")
|
|
230
|
-
fig.update_yaxes(
|
|
231
|
-
title_text=feature, type="log", row=1, col=1, gridcolor="rgba(255,255,255,0.2)"
|
|
232
|
-
)
|
|
233
|
-
fig.update_yaxes(
|
|
234
|
-
title_text="CUSUM Score", row=2, col=1, gridcolor="rgba(255,255,255,0.2)"
|
|
235
|
-
)
|
|
236
|
-
|
|
237
|
-
for i, (cp, trend) in enumerate(zip(changepoints, trends)):
|
|
238
|
-
fig.add_annotation(
|
|
239
|
-
x=df_signal.index[cp],
|
|
240
|
-
y=signal_array[cp],
|
|
241
|
-
text=f"{trend[0]} {i+1}",
|
|
242
|
-
showarrow=False,
|
|
243
|
-
yshift=10 if trend == "Increasing" else -10,
|
|
244
|
-
font=dict(color=colors[trend]),
|
|
245
|
-
row=1,
|
|
246
|
-
col=1,
|
|
247
|
-
)
|
|
248
|
-
|
|
249
|
-
return fig
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
def run_cusum_dashboard(df_accounting, ticker=None, feature=None):
|
|
253
|
-
if ticker is None:
|
|
254
|
-
ticker = df_accounting.index.get_level_values("ticker").unique()[0]
|
|
255
|
-
if feature is None:
|
|
256
|
-
feature = df_accounting.columns[0]
|
|
257
|
-
|
|
258
|
-
app = dash.Dash(__name__)
|
|
259
|
-
|
|
260
|
-
dark_mode_colors = {
|
|
261
|
-
"background": "#1E1E1E",
|
|
262
|
-
"text": "#FFFFFF",
|
|
263
|
-
"plot_bg": "#2B2B2B",
|
|
264
|
-
"plot_gridlines": "#3A3A3A",
|
|
265
|
-
}
|
|
266
|
-
|
|
267
|
-
app.layout = html.Div(
|
|
268
|
-
[
|
|
269
|
-
html.H3(
|
|
270
|
-
"Financial Metric Change Point Detection",
|
|
271
|
-
style={
|
|
272
|
-
"textAlign": "center",
|
|
273
|
-
"marginBottom": "20px",
|
|
274
|
-
"paddingTop": "10px",
|
|
275
|
-
"color": dark_mode_colors["text"],
|
|
276
|
-
},
|
|
277
|
-
),
|
|
278
|
-
html.Div(
|
|
279
|
-
[
|
|
280
|
-
html.Div(
|
|
281
|
-
[
|
|
282
|
-
html.Label(
|
|
283
|
-
"Select Ticker:",
|
|
284
|
-
style={"color": dark_mode_colors["text"]},
|
|
285
|
-
),
|
|
286
|
-
dcc.Dropdown(
|
|
287
|
-
id="ticker-dropdown",
|
|
288
|
-
options=[
|
|
289
|
-
{"label": t, "value": t}
|
|
290
|
-
for t in df_accounting.index.get_level_values(
|
|
291
|
-
"ticker"
|
|
292
|
-
).unique()
|
|
293
|
-
],
|
|
294
|
-
value=ticker,
|
|
295
|
-
),
|
|
296
|
-
],
|
|
297
|
-
style={"width": "48%", "display": "inline-block"},
|
|
298
|
-
),
|
|
299
|
-
html.Div(
|
|
300
|
-
[
|
|
301
|
-
html.Label(
|
|
302
|
-
"Select Feature:",
|
|
303
|
-
style={"color": dark_mode_colors["text"]},
|
|
304
|
-
),
|
|
305
|
-
dcc.Dropdown(
|
|
306
|
-
id="feature-dropdown",
|
|
307
|
-
options=[
|
|
308
|
-
{"label": col, "value": col}
|
|
309
|
-
for col in df_accounting.columns
|
|
310
|
-
],
|
|
311
|
-
value=feature,
|
|
312
|
-
),
|
|
313
|
-
],
|
|
314
|
-
style={
|
|
315
|
-
"width": "48%",
|
|
316
|
-
"float": "right",
|
|
317
|
-
"display": "inline-block",
|
|
318
|
-
},
|
|
319
|
-
),
|
|
320
|
-
],
|
|
321
|
-
style={"padding": "20px 0px"},
|
|
322
|
-
),
|
|
323
|
-
dcc.Graph(id="cusum-plot", style={"height": "80vh"}),
|
|
324
|
-
],
|
|
325
|
-
style={
|
|
326
|
-
"padding": "0 20px",
|
|
327
|
-
"margin": "0 auto",
|
|
328
|
-
"backgroundColor": dark_mode_colors["background"],
|
|
329
|
-
},
|
|
330
|
-
)
|
|
331
|
-
|
|
332
|
-
@app.callback(
|
|
333
|
-
Output("cusum-plot", "figure"),
|
|
334
|
-
[Input("ticker-dropdown", "value"), Input("feature-dropdown", "value")],
|
|
335
|
-
)
|
|
336
|
-
def update_graph(selected_ticker, selected_feature):
|
|
337
|
-
df_signal = (
|
|
338
|
-
df_accounting.query(f"ticker == '{selected_ticker}'")
|
|
339
|
-
.reset_index()
|
|
340
|
-
.set_index("date")[selected_feature]
|
|
341
|
-
)
|
|
342
|
-
signal_array = df_signal.values
|
|
343
|
-
detector = ImprovedCusumDetector()
|
|
344
|
-
changepoints, trends, scores, window_size = detector.detect_changepoints(
|
|
345
|
-
signal_array
|
|
346
|
-
)
|
|
347
|
-
fig = plot_cusum_results(
|
|
348
|
-
df_signal,
|
|
349
|
-
signal_array,
|
|
350
|
-
changepoints,
|
|
351
|
-
trends,
|
|
352
|
-
scores,
|
|
353
|
-
selected_ticker,
|
|
354
|
-
selected_feature,
|
|
355
|
-
)
|
|
356
|
-
fig.update_layout(
|
|
357
|
-
plot_bgcolor=dark_mode_colors["plot_bg"],
|
|
358
|
-
paper_bgcolor=dark_mode_colors["plot_bg"],
|
|
359
|
-
font_color=dark_mode_colors["text"],
|
|
360
|
-
)
|
|
361
|
-
fig.update_xaxes(gridcolor=dark_mode_colors["plot_gridlines"])
|
|
362
|
-
fig.update_yaxes(gridcolor=dark_mode_colors["plot_gridlines"])
|
|
363
|
-
return fig
|
|
364
|
-
|
|
365
|
-
app_name = "cumsum-app"
|
|
366
|
-
return app.run(debug=False, port=get_unique_port(app_name))
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
def perform_cusum_analysis(df_accounting, ticker=None, feature=None):
|
|
370
|
-
# Get the first ticker and feature if not provided
|
|
371
|
-
if ticker is None:
|
|
372
|
-
ticker = df_accounting.index.get_level_values("ticker").unique()[0]
|
|
373
|
-
if feature is None:
|
|
374
|
-
feature = df_accounting.columns[0]
|
|
375
|
-
|
|
376
|
-
# Extract the signal for the specified ticker and feature
|
|
377
|
-
df_signal = (
|
|
378
|
-
df_accounting.query(f"ticker == '{ticker}'")
|
|
379
|
-
.reset_index()
|
|
380
|
-
.set_index("date")[feature]
|
|
381
|
-
)
|
|
382
|
-
|
|
383
|
-
detector = ImprovedCusumDetector()
|
|
384
|
-
signal_array = df_signal.values
|
|
385
|
-
changepoints, trends, scores, window_size = detector.detect_changepoints(
|
|
386
|
-
signal_array
|
|
387
|
-
)
|
|
388
|
-
|
|
389
|
-
# Create main analysis DataFrame
|
|
390
|
-
analysis_df = pd.DataFrame(
|
|
391
|
-
{
|
|
392
|
-
"Date": df_signal.index[changepoints],
|
|
393
|
-
"Trend": trends,
|
|
394
|
-
"Signal_Value": signal_array[changepoints],
|
|
395
|
-
"CUSUM_Score": scores[
|
|
396
|
-
changepoints - 1
|
|
397
|
-
], # Take the score from just before the change point
|
|
398
|
-
}
|
|
399
|
-
)
|
|
400
|
-
|
|
401
|
-
# Calculate duration between change points
|
|
402
|
-
analysis_df["Duration"] = (
|
|
403
|
-
analysis_df["Date"].diff().shift(-1).fillna(pd.Timedelta(days=0))
|
|
404
|
-
)
|
|
405
|
-
analysis_df["Duration_Days"] = analysis_df["Duration"].dt.days
|
|
406
|
-
|
|
407
|
-
# Calculate percentage change between change points
|
|
408
|
-
analysis_df["Percent_Change"] = (
|
|
409
|
-
analysis_df["Signal_Value"].pct_change().shift(-1) * 100
|
|
410
|
-
).fillna(0)
|
|
411
|
-
|
|
412
|
-
# Overall statistics
|
|
413
|
-
overall_stats = pd.DataFrame(
|
|
414
|
-
{
|
|
415
|
-
"Statistic": [
|
|
416
|
-
"Ticker",
|
|
417
|
-
"Feature",
|
|
418
|
-
"Total_Change_Points",
|
|
419
|
-
"Average_Duration_Days",
|
|
420
|
-
"Average_Percent_Change",
|
|
421
|
-
"Increasing_Trends",
|
|
422
|
-
"Decreasing_Trends",
|
|
423
|
-
"Sideways_Trends",
|
|
424
|
-
"Max_CUSUM_Score",
|
|
425
|
-
"Min_CUSUM_Score",
|
|
426
|
-
"Window_Size",
|
|
427
|
-
],
|
|
428
|
-
"Value": [
|
|
429
|
-
ticker,
|
|
430
|
-
feature,
|
|
431
|
-
len(changepoints),
|
|
432
|
-
analysis_df["Duration_Days"].mean(),
|
|
433
|
-
analysis_df["Percent_Change"].mean(),
|
|
434
|
-
sum(trends == "Increasing"),
|
|
435
|
-
sum(trends == "Decreasing"),
|
|
436
|
-
sum(trends == "Sideways"),
|
|
437
|
-
scores.max(),
|
|
438
|
-
scores[scores > 0].min() if any(scores > 0) else 0,
|
|
439
|
-
window_size,
|
|
440
|
-
],
|
|
441
|
-
}
|
|
442
|
-
)
|
|
443
|
-
|
|
444
|
-
analysis_df.attrs["stats"] = overall_stats
|
|
445
|
-
|
|
446
|
-
return analysis_df
|