sovai 0.2.64__tar.gz → 0.2.65__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.64 → sovai-0.2.65}/PKG-INFO +1 -1
- {sovai-0.2.64 → sovai-0.2.65}/pyproject.toml +1 -1
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/pandas_extensions.py +3 -3
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/signal_evaluation.py +255 -49
- {sovai-0.2.64 → sovai-0.2.65}/LICENSE +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/README.md +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/data/codes.parq +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/data/tickers.parq +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/.DS_Store +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/api_config.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/assets/dropdown.css +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/assets/features_mapping.parq +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/assets/tickers.parq +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/basic_auth.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/computations/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/computations/functions.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/errors/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/errors/sovai_errors.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/anomalies.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/ask_df_llm.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/change_point_generator copy.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/change_point_generator.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/clustering.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/core_kshape.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/cum_sum_torch.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/cusum.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/dimensionality_reduction.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/feature_extraction.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/feature_importance.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/feature_neutralizer.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/filter_df.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/fractional_differencing.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/nowcasting.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/pairwise.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/parallel_functions.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/pfa_feature_selector.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/regime_change.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/regime_change_pca.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/shapley_global_importance.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/shapley_importance.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/technical_indicators.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/time_decomposition.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/weight_optimization.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/get_compute.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/get_data copy 2.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/get_data copy.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/get_data.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/get_plots.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/get_reports.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/get_tools.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/accounting/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/accounting/accounting_plots.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/allocation/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/allocation/allocation_plots.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/bankruptcy/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/bankruptcy/bankruptcy_plots.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/breakout/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/breakout/breakout_plots.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/corp_risk/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/corp_risk/corp_risk_plots.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/earnings_surprise/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/earnings_surprise/earnings_surprise_plots.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/insider/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/insider/insider_plots.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/institutional/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/institutional/institutional_plots.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/news/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/news/news_plots.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/ratios/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/ratios/ratios_plots.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/reports/accounting/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/reports/accounting/accounting_balance_sheet.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/reports/bankruptcy/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/reports/bankruptcy/bankruptcy_monthly_top.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/reports/general/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/reports/general/general_plots.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/reports/news/__inti__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/reports/news/news_econometric_analysis.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/studies/nowcasting.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/token_auth.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/tools/authentication/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/tools/authentication/authentication.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/tools/sec/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/tools/sec/graphs.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/tools/sec/llm_code_generator.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/tools/sec/sec_10_k_8_k_filings.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/tools/sec/sec_edgar_search.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/__init__.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/client_side.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/client_side_s3.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/client_side_s3_part_high.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/converter.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/datetime_formats.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/file_management.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/get_tickers.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/helpers.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/pandas_extensions copy.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/plot.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/port_manager copy.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/port_manager.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/stream.py +0 -0
- {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/verbose_utils.py +0 -0
|
@@ -679,7 +679,7 @@ class CustomDataFrame(pd.DataFrame):
|
|
|
679
679
|
|
|
680
680
|
from sovai import data
|
|
681
681
|
|
|
682
|
-
df_closeadj = data("market/closeadj").rename({"closeadj": "price"}, axis=1)
|
|
682
|
+
df_closeadj = data("market/closeadj", full_history=True).rename({"closeadj": "price"}, axis=1)
|
|
683
683
|
|
|
684
684
|
df_merge = self.merge(
|
|
685
685
|
df_closeadj, left_index=True, right_index=True, how="left"
|
|
@@ -1005,7 +1005,7 @@ class CustomDataFrame(pd.DataFrame):
|
|
|
1005
1005
|
print("2024-07-22 0.022 -0.061 0.002 ...")
|
|
1006
1006
|
return None
|
|
1007
1007
|
|
|
1008
|
-
def signal_evaluator(self):
|
|
1008
|
+
def signal_evaluator(self, verbose=False):
|
|
1009
1009
|
"""
|
|
1010
1010
|
Perform weight optimization on the input multi-index DataFrame.
|
|
1011
1011
|
|
|
@@ -1034,7 +1034,7 @@ class CustomDataFrame(pd.DataFrame):
|
|
|
1034
1034
|
|
|
1035
1035
|
# If all checks pass, proceed with creating the SignalEvaluator object
|
|
1036
1036
|
try:
|
|
1037
|
-
optimized_evaluator = SignalEvaluator(self)
|
|
1037
|
+
optimized_evaluator = SignalEvaluator(self, verbose)
|
|
1038
1038
|
print("SignalEvaluator object created successfully with optimized weights.")
|
|
1039
1039
|
return optimized_evaluator
|
|
1040
1040
|
except Exception as e:
|
|
@@ -1673,9 +1673,21 @@ from joblib import Parallel, delayed
|
|
|
1673
1673
|
|
|
1674
1674
|
|
|
1675
1675
|
class SignalEvaluator:
|
|
1676
|
-
|
|
1676
|
+
# --- Initialization with verbose parameter ---
|
|
1677
|
+
def __init__(self, df_factor, verbose=False): # Added verbose parameter
|
|
1678
|
+
self.verbose = verbose # Store verbose state
|
|
1679
|
+
if self.verbose:
|
|
1680
|
+
print("--- SignalEvaluator Initialization ---")
|
|
1681
|
+
if isinstance(df_factor, (pd.DataFrame, pd.Series)):
|
|
1682
|
+
print(f"Input df_factor shape: {df_factor.shape}")
|
|
1683
|
+
# Use the new _log_df_preview for the initial df_factor print
|
|
1684
|
+
# Note: Using head() here as requested in previous refinement
|
|
1685
|
+
self._log_df_preview(df_factor, "Input df_factor head:")
|
|
1686
|
+
else:
|
|
1687
|
+
print(f"Input df_factor type: {type(df_factor)}")
|
|
1677
1688
|
|
|
1678
1689
|
self.df_factor = df_factor
|
|
1690
|
+
# The following attributes will be logged within their respective methods
|
|
1679
1691
|
self.df_signal, self.df_prices = self._signal_frame()
|
|
1680
1692
|
self.positions = self._create_portfolio_holdings()
|
|
1681
1693
|
self.rebalance_mask = self._calc_rebalanced_mask()
|
|
@@ -1686,102 +1698,296 @@ class SignalEvaluator:
|
|
|
1686
1698
|
self.portfolio_returns = self._portfolio_returns()
|
|
1687
1699
|
self.cumulative_returns = self._cummulative_returns()
|
|
1688
1700
|
|
|
1689
|
-
# self.statistics = self._calculate_statistics()
|
|
1690
|
-
|
|
1701
|
+
# self.statistics = self._calculate_statistics() # Keep commented if not used
|
|
1702
|
+
|
|
1703
|
+
if self.verbose:
|
|
1704
|
+
print("--- SignalEvaluator Initialization Complete ---")
|
|
1705
|
+
|
|
1706
|
+
# --- Logging Helper Methods ---
|
|
1707
|
+
def _log_df_preview(self, df_to_log, header_message="Preview:", max_cols_to_show=5, show_nan_summary=True):
|
|
1708
|
+
"""
|
|
1709
|
+
Prints a preview of a DataFrame or Series, intelligently slicing wide DataFrames.
|
|
1710
|
+
Uses .head() for the preview.
|
|
1711
|
+
"""
|
|
1712
|
+
if df_to_log is None:
|
|
1713
|
+
print(f"{header_message} (None)")
|
|
1714
|
+
return
|
|
1715
|
+
|
|
1716
|
+
print(f"{header_message} (Shape: {df_to_log.shape})")
|
|
1717
|
+
if isinstance(df_to_log, pd.DataFrame):
|
|
1718
|
+
if not df_to_log.empty:
|
|
1719
|
+
num_cols = df_to_log.shape[1]
|
|
1720
|
+
cols_to_display = min(num_cols, max_cols_to_show)
|
|
1721
|
+
# Display head of selected columns
|
|
1722
|
+
print(df_to_log.iloc[:, :cols_to_display].head())
|
|
1723
|
+
if show_nan_summary:
|
|
1724
|
+
print("NaNs count for displayed columns:")
|
|
1725
|
+
# Show NaN sum for the displayed columns
|
|
1726
|
+
print(df_to_log.iloc[:, :cols_to_display].isnull().sum())
|
|
1727
|
+
else:
|
|
1728
|
+
print("(DataFrame is empty)")
|
|
1729
|
+
elif isinstance(df_to_log, pd.Series):
|
|
1730
|
+
if not df_to_log.empty:
|
|
1731
|
+
print(df_to_log.head()) # Show head for Series
|
|
1732
|
+
if show_nan_summary:
|
|
1733
|
+
print(f"NaNs count: {df_to_log.isnull().sum()}")
|
|
1734
|
+
else:
|
|
1735
|
+
print("(Series is empty)")
|
|
1736
|
+
else:
|
|
1737
|
+
print(f"(Object is not a DataFrame or Series: {type(df_to_log)})")
|
|
1738
|
+
|
|
1739
|
+
def _log(self, message, df_to_preview=None, max_cols=5):
|
|
1740
|
+
"""Helper function for conditional logging. Uses _log_df_preview for DataFrames/Series."""
|
|
1741
|
+
if self.verbose:
|
|
1742
|
+
print(message)
|
|
1743
|
+
if df_to_preview is not None:
|
|
1744
|
+
# Pass an empty header as the main message is already printed
|
|
1745
|
+
self._log_df_preview(df_to_preview, header_message="Preview:", max_cols_to_show=max_cols)
|
|
1746
|
+
|
|
1747
|
+
|
|
1748
|
+
# --- Core Calculation Methods with Logging ---
|
|
1691
1749
|
def _signal_frame(self):
|
|
1750
|
+
self._log("Running _signal_frame...")
|
|
1692
1751
|
factor = self.df_factor
|
|
1752
|
+
# Ensure this column name matches your actual factor data
|
|
1753
|
+
if factor.columns.empty:
|
|
1754
|
+
self._log("Error: Input df_factor has no columns.")
|
|
1755
|
+
return pd.DataFrame(), pd.DataFrame() # Return empty to avoid index error
|
|
1693
1756
|
column_name = factor.columns[0]
|
|
1694
|
-
df_factor = factor.add_price()
|
|
1695
|
-
|
|
1696
|
-
df_signal = df_factor[column_name].unstack(level="ticker").shift(1)
|
|
1697
|
-
|
|
1698
|
-
# Create wide DataFrame for prices
|
|
1699
|
-
df_prices = df_factor["price"].unstack(level="ticker")
|
|
1700
1757
|
|
|
1758
|
+
# --- Crucial Area 1: Getting df_with_price ---
|
|
1759
|
+
# Verify that df_with_price correctly contains both the signal column
|
|
1760
|
+
# AND a valid numeric 'price' column, indexed correctly by date and ticker.
|
|
1761
|
+
if hasattr(factor, 'add_price') and callable(getattr(factor, 'add_price')):
|
|
1762
|
+
# Make sure factor.add_price() actually returns a DataFrame with 'price'
|
|
1763
|
+
df_with_price = factor.add_price()
|
|
1764
|
+
self._log("Preview of df_with_price AFTER add_price():", df_with_price)
|
|
1765
|
+
else:
|
|
1766
|
+
# Assumes 'price' column already exists in factor if add_price not used
|
|
1767
|
+
df_with_price = factor
|
|
1768
|
+
if 'price' not in df_with_price.columns:
|
|
1769
|
+
self._log("Warning: 'price' column not found in df_factor and add_price() was not used.")
|
|
1770
|
+
# Handle error or return empty DFs
|
|
1771
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
1772
|
+
self._log("Preview of df_with_price (assuming price exists):", df_with_price)
|
|
1773
|
+
|
|
1774
|
+
|
|
1775
|
+
# --- Crucial Area 2: Indexing Check ---
|
|
1776
|
+
# Ensure 'ticker' and 'date' are index levels for unstacking
|
|
1777
|
+
required_index_levels = ['date', 'ticker']
|
|
1778
|
+
if not all(level in df_with_price.index.names for level in required_index_levels):
|
|
1779
|
+
self._log(f"Warning: Index levels {required_index_levels} not found for unstacking. Current index: {df_with_price.index.names}", df_with_price.head())
|
|
1780
|
+
# Add logic here to set the index correctly if needed, e.g.:
|
|
1781
|
+
# if 'date' in df_with_price.columns and 'ticker' in df_with_price.columns:
|
|
1782
|
+
# self._log("Attempting to set index ['date', 'ticker']...")
|
|
1783
|
+
# try:
|
|
1784
|
+
# df_with_price = df_with_price.set_index(['date', 'ticker'])
|
|
1785
|
+
# except KeyError as e:
|
|
1786
|
+
# self._log(f"Failed to set index: {e}")
|
|
1787
|
+
# return pd.DataFrame(), pd.DataFrame() # Handle error
|
|
1788
|
+
# else:
|
|
1789
|
+
# self._log("Cannot set index - 'date' or 'ticker' columns missing.")
|
|
1790
|
+
# return pd.DataFrame(), pd.DataFrame() # Handle error
|
|
1791
|
+
|
|
1792
|
+
|
|
1793
|
+
# --- Crucial Area 3: The Unstack Operations ---
|
|
1794
|
+
try:
|
|
1795
|
+
# Check df_with_price[column_name] before unstacking
|
|
1796
|
+
self._log(f"Preview of signal data '{column_name}' before unstack:", df_with_price[column_name])
|
|
1797
|
+
df_signal = df_with_price[column_name].unstack(level="ticker").shift(1)
|
|
1798
|
+
|
|
1799
|
+
# **** FOCUS HERE: Check df_with_price['price'] ****
|
|
1800
|
+
self._log("Preview of 'price' data before unstack:", df_with_price['price'])
|
|
1801
|
+
df_prices = df_with_price["price"].unstack(level="ticker")
|
|
1802
|
+
|
|
1803
|
+
# **** Check if df_prices is all NaN ****
|
|
1804
|
+
if not df_prices.empty and df_prices.isnull().all().all():
|
|
1805
|
+
self._log("CRITICAL: df_prices is ALL NaN after unstacking! Check price data source and indexing.")
|
|
1806
|
+
|
|
1807
|
+
except Exception as e:
|
|
1808
|
+
self._log(f"Error during unstacking in _signal_frame: {e}")
|
|
1809
|
+
self._log("df_with_price index at time of error:", df_with_price.index.names)
|
|
1810
|
+
self._log("df_with_price columns at time of error:", df_with_price.columns)
|
|
1811
|
+
self._log("df_with_price head at time of error:", df_with_price.head())
|
|
1812
|
+
# Return empty DataFrames or re-raise to prevent further issues
|
|
1813
|
+
return pd.DataFrame(), pd.DataFrame()
|
|
1814
|
+
|
|
1815
|
+
|
|
1816
|
+
self._log("Result of _signal_frame - df_signal:", df_signal)
|
|
1817
|
+
self._log("Result of _signal_frame - df_prices:", df_prices)
|
|
1701
1818
|
return df_signal, df_prices
|
|
1702
1819
|
|
|
1703
1820
|
def _create_portfolio_holdings(self):
|
|
1821
|
+
self._log("Running _create_portfolio_holdings...")
|
|
1822
|
+
if self.df_signal.empty:
|
|
1823
|
+
self._log("df_signal is empty in _create_portfolio_holdings. Returning empty DataFrame.")
|
|
1824
|
+
return pd.DataFrame(index=self.df_signal.index, columns=self.df_signal.columns)
|
|
1825
|
+
|
|
1704
1826
|
signal_array = self.df_signal.values
|
|
1705
1827
|
holdings_array = np.zeros_like(signal_array)
|
|
1706
|
-
|
|
1707
|
-
|
|
1708
|
-
|
|
1709
|
-
|
|
1828
|
+
try:
|
|
1829
|
+
with np.errstate(invalid='ignore'): # Suppress warnings for NaN comparisons
|
|
1830
|
+
holdings_array[signal_array <= 10] = -1
|
|
1831
|
+
holdings_array[signal_array >= 90] = 1
|
|
1832
|
+
holdings_array[np.isnan(signal_array)] = np.nan # Set NaNs explicitly
|
|
1833
|
+
except TypeError as e:
|
|
1834
|
+
self._log(f"TypeError during holdings_array assignment: {e}. signal_array sample: {signal_array[:2, :2] if signal_array.ndim > 1 else signal_array[:2]}")
|
|
1835
|
+
# Fallback or re-raise
|
|
1836
|
+
pass
|
|
1837
|
+
|
|
1838
|
+
df_holdings = pd.DataFrame(
|
|
1710
1839
|
holdings_array, index=self.df_signal.index, columns=self.df_signal.columns
|
|
1711
1840
|
)
|
|
1841
|
+
self._log("Result of _create_portfolio_holdings - positions:", df_holdings)
|
|
1842
|
+
return df_holdings
|
|
1712
1843
|
|
|
1713
1844
|
def _calc_rebalanced_mask(self):
|
|
1714
|
-
|
|
1715
|
-
|
|
1716
|
-
|
|
1717
|
-
|
|
1718
|
-
|
|
1845
|
+
self._log("Running _calc_rebalanced_mask...")
|
|
1846
|
+
# Use the created positions index to create the mask
|
|
1847
|
+
if self.positions.empty:
|
|
1848
|
+
self._log("Positions DataFrame is empty in _calc_rebalanced_mask. Returning empty Series.")
|
|
1849
|
+
return pd.Series(dtype=bool)
|
|
1719
1850
|
|
|
1720
|
-
#
|
|
1721
|
-
rebalance_mask = pd.Series(False, index=
|
|
1722
|
-
rebalance_mask.
|
|
1723
|
-
|
|
1851
|
+
# No need for fillna(2) here, just use the index
|
|
1852
|
+
rebalance_mask = pd.Series(False, index=self.positions.index)
|
|
1853
|
+
if not rebalance_mask.empty:
|
|
1854
|
+
rebalance_mask.iloc[0] = True
|
|
1855
|
+
rebalance_mask.iloc[4::4] = True # Every 4th row after the first
|
|
1856
|
+
self._log("Result of _calc_rebalanced_mask - rebalance_mask (Series):", rebalance_mask)
|
|
1724
1857
|
return rebalance_mask
|
|
1725
1858
|
|
|
1726
1859
|
def _real_holdings(self):
|
|
1727
|
-
|
|
1728
|
-
|
|
1860
|
+
self._log("Running _real_holdings...")
|
|
1861
|
+
df_initial_holdings = self.positions
|
|
1729
1862
|
|
|
1730
|
-
|
|
1731
|
-
|
|
1863
|
+
if df_initial_holdings.empty or self.rebalance_mask.empty:
|
|
1864
|
+
self._log("Initial positions or rebalance_mask is empty in _real_holdings. Returning empty DataFrame.")
|
|
1865
|
+
return pd.DataFrame(index=df_initial_holdings.index, columns=df_initial_holdings.columns)
|
|
1732
1866
|
|
|
1733
|
-
|
|
1734
|
-
|
|
1735
|
-
|
|
1736
|
-
df_balance = df_temp.where(rebalance_mask, np.nan)
|
|
1737
|
-
|
|
1738
|
-
# Forward fill
|
|
1739
|
-
df_balance = df_balance.ffill()
|
|
1740
|
-
|
|
1741
|
-
# Replace 2 with NaN to restore original NaN structure
|
|
1742
|
-
df_balance = df_balance.replace(2, np.nan)
|
|
1867
|
+
# Apply the rebalancing mask and forward fill
|
|
1868
|
+
# No need for the intermediate fillna(2) step
|
|
1869
|
+
df_balance = df_initial_holdings.where(self.rebalance_mask).ffill()
|
|
1743
1870
|
|
|
1871
|
+
# Replace 0 with NaN *after* forward filling
|
|
1744
1872
|
df_balance = df_balance.replace(0, np.nan)
|
|
1745
1873
|
|
|
1874
|
+
self._log("Result of _real_holdings - final holdings (df_balance):", df_balance)
|
|
1746
1875
|
return df_balance
|
|
1747
1876
|
|
|
1748
1877
|
def _portfolio_returns(self):
|
|
1749
|
-
|
|
1878
|
+
self._log("Running _portfolio_returns...")
|
|
1879
|
+
if self.position_returns.empty:
|
|
1880
|
+
self._log("position_returns is empty. Returning empty Series for portfolio_returns.")
|
|
1881
|
+
return pd.Series(dtype=float)
|
|
1882
|
+
|
|
1883
|
+
# Calculate mean returns, ignoring NaNs by default
|
|
1750
1884
|
portfolio_returns = self.position_returns.mean(axis=1)
|
|
1751
1885
|
|
|
1886
|
+
self._log("Result of _portfolio_returns - portfolio_returns (Series):", portfolio_returns)
|
|
1752
1887
|
return portfolio_returns
|
|
1753
1888
|
|
|
1754
1889
|
def _cummulative_returns(self):
|
|
1755
|
-
|
|
1890
|
+
self._log("Running _cummulative_returns...")
|
|
1891
|
+
if self.portfolio_returns.empty or self.portfolio_returns.isnull().all():
|
|
1892
|
+
self._log("portfolio_returns is empty or all NaN. Returning empty/NaN Series for cumulative_returns.")
|
|
1893
|
+
return pd.Series(dtype=float, index=self.portfolio_returns.index) # Keep index if possible
|
|
1756
1894
|
|
|
1757
1895
|
# Calculate cumulative returns
|
|
1758
1896
|
cumulative_returns = (1 + self.portfolio_returns).cumprod()
|
|
1759
1897
|
|
|
1898
|
+
self._log("Result of _cummulative_returns - cumulative_returns (Series):", cumulative_returns)
|
|
1760
1899
|
return cumulative_returns
|
|
1761
1900
|
|
|
1762
1901
|
def _calculate_returns(self):
|
|
1763
|
-
|
|
1902
|
+
self._log("Running _calculate_returns (price percentage changes)...")
|
|
1903
|
+
if self.df_prices.empty:
|
|
1904
|
+
self._log("df_prices is empty. Returning empty DataFrame for asset_price_returns.")
|
|
1905
|
+
return pd.DataFrame(index=self.df_prices.index, columns=self.df_prices.columns)
|
|
1906
|
+
if self.df_prices.isnull().all().all():
|
|
1907
|
+
self._log("df_prices is ALL NaN. Returns calculation will result in NaNs.")
|
|
1908
|
+
|
|
1909
|
+
# fill_method=None is deprecated, default is None (no filling)
|
|
1910
|
+
asset_price_returns = self.df_prices.pct_change()
|
|
1911
|
+
asset_price_returns = asset_price_returns.clip(upper=3)
|
|
1912
|
+
|
|
1913
|
+
self._log("Result of _calculate_returns - asset_price_returns:", asset_price_returns)
|
|
1914
|
+
return asset_price_returns
|
|
1764
1915
|
|
|
1765
1916
|
def _calculate_position_returns(self):
|
|
1766
|
-
|
|
1917
|
+
self._log("Running _calculate_position_returns...")
|
|
1918
|
+
if self.holdings.empty or self.returns.empty:
|
|
1919
|
+
self._log("Holdings or returns are empty. Returning empty DataFrame for position_returns.")
|
|
1920
|
+
idx = self.holdings.index if not self.holdings.empty else self.returns.index
|
|
1921
|
+
cols = self.holdings.columns if not self.holdings.empty else self.returns.columns
|
|
1922
|
+
return pd.DataFrame(index=idx, columns=cols)
|
|
1923
|
+
if self.returns.isnull().all().all():
|
|
1924
|
+
self._log("Returns DataFrame is ALL NaN. Position returns will be NaN.")
|
|
1925
|
+
|
|
1926
|
+
# Ensure alignment (pandas usually handles this, but good to be aware)
|
|
1927
|
+
# Check if indices/columns match if issues arise
|
|
1928
|
+
if not self.holdings.index.equals(self.returns.index):
|
|
1929
|
+
self._log("Warning: Index mismatch between holdings and returns in _calculate_position_returns.")
|
|
1930
|
+
if not self.holdings.columns.equals(self.returns.columns):
|
|
1931
|
+
self._log("Warning: Column mismatch between holdings and returns in _calculate_position_returns.")
|
|
1932
|
+
|
|
1933
|
+
|
|
1934
|
+
position_returns = self.holdings * self.returns
|
|
1935
|
+
self._log("Result of _calculate_position_returns - position_returns:", position_returns)
|
|
1936
|
+
return position_returns
|
|
1767
1937
|
|
|
1768
1938
|
def _calculate_resampled_returns(self):
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1939
|
+
self._log("Running _calculate_resampled_returns...")
|
|
1940
|
+
if self.position_returns.empty:
|
|
1941
|
+
self._log("position_returns is empty in _calculate_resampled_returns. Returning empty DataFrame.")
|
|
1942
|
+
return pd.DataFrame(columns=self.position_returns.columns)
|
|
1943
|
+
|
|
1944
|
+
resample_group_mask = pd.Series(False, index=self.position_returns.index)
|
|
1945
|
+
if not resample_group_mask.empty:
|
|
1946
|
+
resample_group_mask.iloc[0] = True
|
|
1947
|
+
resample_group_mask.iloc[4::4] = True
|
|
1772
1948
|
|
|
1773
1949
|
def custom_resampler(x):
|
|
1950
|
+
# Sum ignores NaNs by default, so sum of all NaNs is 0.
|
|
1774
1951
|
return x.sum()
|
|
1775
1952
|
|
|
1776
|
-
group_numbers = (
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1780
|
-
|
|
1953
|
+
group_numbers = (resample_group_mask.cumsum() - 1).ffill()
|
|
1954
|
+
|
|
1955
|
+
if group_numbers.isnull().all():
|
|
1956
|
+
self._log("No valid groups for resampling. Returning empty DataFrame.")
|
|
1957
|
+
return pd.DataFrame(columns=self.position_returns.columns)
|
|
1958
|
+
|
|
1959
|
+
resampled_returns = self.position_returns.groupby(group_numbers).apply(custom_resampler)
|
|
1960
|
+
|
|
1961
|
+
# Align index with rebalance dates
|
|
1962
|
+
true_mask_indices = resample_group_mask[resample_group_mask].index
|
|
1963
|
+
if len(true_mask_indices) == len(resampled_returns):
|
|
1964
|
+
resampled_returns.index = true_mask_indices
|
|
1965
|
+
else:
|
|
1966
|
+
# This case might happen if the last period doesn't align perfectly
|
|
1967
|
+
# Or if groupby produces unexpected number of groups
|
|
1968
|
+
self._log(f"Warning: Mismatch in length for resampled_returns index. Resampled length: {len(resampled_returns)}, True mask indices: {len(true_mask_indices)}. Attempting partial index assignment.")
|
|
1969
|
+
common_len = min(len(true_mask_indices), len(resampled_returns))
|
|
1970
|
+
resampled_returns = resampled_returns.iloc[:common_len]
|
|
1971
|
+
resampled_returns.index = true_mask_indices[:common_len]
|
|
1972
|
+
|
|
1973
|
+
|
|
1974
|
+
self._log("Result of _calculate_resampled_returns - resampled_returns:", resampled_returns)
|
|
1781
1975
|
return resampled_returns
|
|
1782
1976
|
|
|
1783
1977
|
def _calculate_statistics(self):
|
|
1784
|
-
|
|
1978
|
+
# This method was previously commented out or missing implementation details
|
|
1979
|
+
self._log("Running _calculate_statistics...")
|
|
1980
|
+
if self.portfolio_returns.empty or self.portfolio_returns.isnull().all():
|
|
1981
|
+
self._log("Cannot calculate statistics: portfolio_returns is empty or all NaN.")
|
|
1982
|
+
return None # Or return an empty structure / specific message
|
|
1983
|
+
|
|
1984
|
+
# Assuming draw_down_statistics exists and takes a Series
|
|
1985
|
+
# stats = draw_down_statistics(self.portfolio_returns)
|
|
1986
|
+
# self._log("Statistics calculation complete.") # Add preview if needed
|
|
1987
|
+
# return stats
|
|
1988
|
+
self._log("Placeholder for statistics calculation.")
|
|
1989
|
+
return "Statistics Placeholder"
|
|
1990
|
+
|
|
1785
1991
|
|
|
1786
1992
|
@property
|
|
1787
1993
|
def performance_plot(self):
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|