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.
Files changed (105) hide show
  1. {sovai-0.2.64 → sovai-0.2.65}/PKG-INFO +1 -1
  2. {sovai-0.2.64 → sovai-0.2.65}/pyproject.toml +1 -1
  3. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/pandas_extensions.py +3 -3
  4. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/signal_evaluation.py +255 -49
  5. {sovai-0.2.64 → sovai-0.2.65}/LICENSE +0 -0
  6. {sovai-0.2.64 → sovai-0.2.65}/README.md +0 -0
  7. {sovai-0.2.64 → sovai-0.2.65}/data/codes.parq +0 -0
  8. {sovai-0.2.64 → sovai-0.2.65}/data/tickers.parq +0 -0
  9. {sovai-0.2.64 → sovai-0.2.65}/sovai/.DS_Store +0 -0
  10. {sovai-0.2.64 → sovai-0.2.65}/sovai/__init__.py +0 -0
  11. {sovai-0.2.64 → sovai-0.2.65}/sovai/api_config.py +0 -0
  12. {sovai-0.2.64 → sovai-0.2.65}/sovai/assets/dropdown.css +0 -0
  13. {sovai-0.2.64 → sovai-0.2.65}/sovai/assets/features_mapping.parq +0 -0
  14. {sovai-0.2.64 → sovai-0.2.65}/sovai/assets/tickers.parq +0 -0
  15. {sovai-0.2.64 → sovai-0.2.65}/sovai/basic_auth.py +0 -0
  16. {sovai-0.2.64 → sovai-0.2.65}/sovai/computations/__init__.py +0 -0
  17. {sovai-0.2.64 → sovai-0.2.65}/sovai/computations/functions.py +0 -0
  18. {sovai-0.2.64 → sovai-0.2.65}/sovai/errors/__init__.py +0 -0
  19. {sovai-0.2.64 → sovai-0.2.65}/sovai/errors/sovai_errors.py +0 -0
  20. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/__init__.py +0 -0
  21. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/anomalies.py +0 -0
  22. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/ask_df_llm.py +0 -0
  23. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/change_point_generator copy.py +0 -0
  24. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/change_point_generator.py +0 -0
  25. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/clustering.py +0 -0
  26. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/core_kshape.py +0 -0
  27. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/cum_sum_torch.py +0 -0
  28. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/cusum.py +0 -0
  29. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/dimensionality_reduction.py +0 -0
  30. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/feature_extraction.py +0 -0
  31. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/feature_importance.py +0 -0
  32. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/feature_neutralizer.py +0 -0
  33. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/filter_df.py +0 -0
  34. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/fractional_differencing.py +0 -0
  35. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/nowcasting.py +0 -0
  36. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/pairwise.py +0 -0
  37. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/parallel_functions.py +0 -0
  38. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/pfa_feature_selector.py +0 -0
  39. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/regime_change.py +0 -0
  40. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/regime_change_pca.py +0 -0
  41. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/shapley_global_importance.py +0 -0
  42. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/shapley_importance.py +0 -0
  43. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/technical_indicators.py +0 -0
  44. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/time_decomposition.py +0 -0
  45. {sovai-0.2.64 → sovai-0.2.65}/sovai/extensions/weight_optimization.py +0 -0
  46. {sovai-0.2.64 → sovai-0.2.65}/sovai/get_compute.py +0 -0
  47. {sovai-0.2.64 → sovai-0.2.65}/sovai/get_data copy 2.py +0 -0
  48. {sovai-0.2.64 → sovai-0.2.65}/sovai/get_data copy.py +0 -0
  49. {sovai-0.2.64 → sovai-0.2.65}/sovai/get_data.py +0 -0
  50. {sovai-0.2.64 → sovai-0.2.65}/sovai/get_plots.py +0 -0
  51. {sovai-0.2.64 → sovai-0.2.65}/sovai/get_reports.py +0 -0
  52. {sovai-0.2.64 → sovai-0.2.65}/sovai/get_tools.py +0 -0
  53. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/__init__.py +0 -0
  54. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/accounting/__init__.py +0 -0
  55. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/accounting/accounting_plots.py +0 -0
  56. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/allocation/__init__.py +0 -0
  57. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/allocation/allocation_plots.py +0 -0
  58. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/bankruptcy/__init__.py +0 -0
  59. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/bankruptcy/bankruptcy_plots.py +0 -0
  60. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/breakout/__init__.py +0 -0
  61. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/breakout/breakout_plots.py +0 -0
  62. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/corp_risk/__init__.py +0 -0
  63. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/corp_risk/corp_risk_plots.py +0 -0
  64. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/earnings_surprise/__init__.py +0 -0
  65. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/earnings_surprise/earnings_surprise_plots.py +0 -0
  66. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/insider/__init__.py +0 -0
  67. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/insider/insider_plots.py +0 -0
  68. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/institutional/__init__.py +0 -0
  69. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/institutional/institutional_plots.py +0 -0
  70. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/news/__init__.py +0 -0
  71. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/news/news_plots.py +0 -0
  72. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/ratios/__init__.py +0 -0
  73. {sovai-0.2.64 → sovai-0.2.65}/sovai/plots/ratios/ratios_plots.py +0 -0
  74. {sovai-0.2.64 → sovai-0.2.65}/sovai/reports/accounting/__init__.py +0 -0
  75. {sovai-0.2.64 → sovai-0.2.65}/sovai/reports/accounting/accounting_balance_sheet.py +0 -0
  76. {sovai-0.2.64 → sovai-0.2.65}/sovai/reports/bankruptcy/__init__.py +0 -0
  77. {sovai-0.2.64 → sovai-0.2.65}/sovai/reports/bankruptcy/bankruptcy_monthly_top.py +0 -0
  78. {sovai-0.2.64 → sovai-0.2.65}/sovai/reports/general/__init__.py +0 -0
  79. {sovai-0.2.64 → sovai-0.2.65}/sovai/reports/general/general_plots.py +0 -0
  80. {sovai-0.2.64 → sovai-0.2.65}/sovai/reports/news/__inti__.py +0 -0
  81. {sovai-0.2.64 → sovai-0.2.65}/sovai/reports/news/news_econometric_analysis.py +0 -0
  82. {sovai-0.2.64 → sovai-0.2.65}/sovai/studies/nowcasting.py +0 -0
  83. {sovai-0.2.64 → sovai-0.2.65}/sovai/token_auth.py +0 -0
  84. {sovai-0.2.64 → sovai-0.2.65}/sovai/tools/authentication/__init__.py +0 -0
  85. {sovai-0.2.64 → sovai-0.2.65}/sovai/tools/authentication/authentication.py +0 -0
  86. {sovai-0.2.64 → sovai-0.2.65}/sovai/tools/sec/__init__.py +0 -0
  87. {sovai-0.2.64 → sovai-0.2.65}/sovai/tools/sec/graphs.py +0 -0
  88. {sovai-0.2.64 → sovai-0.2.65}/sovai/tools/sec/llm_code_generator.py +0 -0
  89. {sovai-0.2.64 → sovai-0.2.65}/sovai/tools/sec/sec_10_k_8_k_filings.py +0 -0
  90. {sovai-0.2.64 → sovai-0.2.65}/sovai/tools/sec/sec_edgar_search.py +0 -0
  91. {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/__init__.py +0 -0
  92. {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/client_side.py +0 -0
  93. {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/client_side_s3.py +0 -0
  94. {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/client_side_s3_part_high.py +0 -0
  95. {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/converter.py +0 -0
  96. {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/datetime_formats.py +0 -0
  97. {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/file_management.py +0 -0
  98. {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/get_tickers.py +0 -0
  99. {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/helpers.py +0 -0
  100. {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/pandas_extensions copy.py +0 -0
  101. {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/plot.py +0 -0
  102. {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/port_manager copy.py +0 -0
  103. {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/port_manager.py +0 -0
  104. {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/stream.py +0 -0
  105. {sovai-0.2.64 → sovai-0.2.65}/sovai/utils/verbose_utils.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: sovai
3
- Version: 0.2.64
3
+ Version: 0.2.65
4
4
  Summary: python for asset management
5
5
  License: Proprietary License
6
6
  Author: Derek Snow
@@ -4,7 +4,7 @@ build-backend = "poetry.core.masonry.api"
4
4
 
5
5
  [project]
6
6
  name = "sovai"
7
- version = "0.2.64"
7
+ version = "0.2.65"
8
8
  description = "python for asset management"
9
9
  authors = [
10
10
  {name = "Derek Snow", email = "d.snow@outlook.com"}
@@ -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
- def __init__(self, df_factor):
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
- holdings_array[signal_array <= 10] = -1
1707
- holdings_array[signal_array >= 90] = 1
1708
- holdings_array[np.isnan(signal_array)] = np.nan
1709
- return pd.DataFrame(
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
- # Create the portfolio holdings DataFrame
1715
- df_holdings = self.positions
1716
-
1717
- # Replace NaNs with 2 (our temporary placeholder)
1718
- df_temp = df_holdings.fillna(2)
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
- # Create a 4-weekly rebalancing mask
1721
- rebalance_mask = pd.Series(False, index=df_temp.index)
1722
- rebalance_mask.iloc[0] = True # Always include the first row
1723
- rebalance_mask.iloc[4::4] = True # Then every 4th row
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
- # Create the portfolio holdings DataFrame
1728
- df_holdings = self.positions
1860
+ self._log("Running _real_holdings...")
1861
+ df_initial_holdings = self.positions
1729
1862
 
1730
- # Replace NaNs with 2 (our temporary placeholder)
1731
- df_temp = df_holdings.fillna(2)
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
- rebalance_mask = self.rebalance_mask
1734
-
1735
- # Apply the rebalancing mask
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
- # Calculate portfolio returns (sum across all assets for each date)
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
- # Calculate portfolio returns (sum across all assets for each date)
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
- return self.df_prices.pct_change(fill_method=None).clip(upper=3)
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
- return self.holdings * self.returns
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
- rebalance_mask = pd.Series(False, index=self.position_returns.index)
1770
- rebalance_mask.iloc[0] = True
1771
- rebalance_mask.iloc[4::4] = True
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 = (rebalance_mask.cumsum() - 1).ffill()
1777
- resampled_returns = self.position_returns.groupby(group_numbers).apply(
1778
- custom_resampler
1779
- )
1780
- resampled_returns.index = rebalance_mask[rebalance_mask == True].index
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
- return draw_down_statistics(self.position_returns.mean(axis=1))
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