diff --git a/src/quantlib_st/cli/corr_cmd.py b/src/quantlib_st/cli/corr_cmd.py index 1d085b7..5e9b80e 100644 --- a/src/quantlib_st/cli/corr_cmd.py +++ b/src/quantlib_st/cli/corr_cmd.py @@ -85,6 +85,15 @@ def add_corr_subcommand(subparsers: argparse._SubParsersAction) -> None: default=False, help="If true, treat input as prices. If false (default), treat as returns.", ) + parser.add_argument( + "--signed-log-transform", + action=argparse.BooleanOptionalAction, + default=True, + help=( + "If true (default), apply signed-log transform when --is-price-series " + "to allow non-positive prices." + ), + ) parser.add_argument( "--index-col", @@ -121,6 +130,7 @@ def run_corr(args: argparse.Namespace) -> int: frequency=args.frequency, forward_fill_price_index=args.forward_fill_price_index, is_price_series=args.is_price_series, + signed_log_transform=args.signed_log_transform, date_method=args.date_method, rollyears=args.rollyears, interval_frequency=args.interval_frequency, diff --git a/src/quantlib_st/correlation/README.md b/src/quantlib_st/correlation/README.md index 37f171b..c299c7e 100644 --- a/src/quantlib_st/correlation/README.md +++ b/src/quantlib_st/correlation/README.md @@ -19,6 +19,9 @@ cat sample_data/returns_10x4.csv | ./dist/quantlib corr - `--interval-frequency 12M`: Controls how often a new correlation matrix is emitted. - `--ew-lookback 250` and `--min-periods 20`: Parameters for EWMA estimation. - `--is-price-series`: Treat input data as price series and convert to lognormal returns. +- `--signed-log-transform / --no-signed-log-transform`: When using `--is-price-series`, + apply a signed-log transform ($\mathrm{sign}(x)\cdot\log(1+|x|)$) to allow + non-positive prices (default: enabled). ### Key concepts diff --git a/src/quantlib_st/correlation/correlation_over_time.py b/src/quantlib_st/correlation/correlation_over_time.py index 6cabd2a..521bf68 100644 --- a/src/quantlib_st/correlation/correlation_over_time.py +++ b/src/quantlib_st/correlation/correlation_over_time.py @@ -60,6 +60,7 @@ def correlation_over_time( frequency: str = "D", forward_fill_price_index: bool = True, is_price_series: bool = False, + signed_log_transform: bool = True, **kwargs, ) -> CorrelationList: """Construct a CorrelationList from a DataFrame of returns or prices. @@ -86,17 +87,29 @@ def correlation_over_time( is_price_series : bool, optional If True, treat input as a price index and log-transform it. If False (default), treat input as log-normal returns. + signed_log_transform : bool, optional + If True (default), apply a signed-log transform when ``is_price_series`` + to allow non-positive prices (e.g., spreads). If False, use ``np.log`` + which requires strictly positive prices. **kwargs : All other keyword arguments are forwarded to :func:`compute_correlation_over_time`. """ # 1. Determine the (log) Price Index if is_price_series: # Input is Price P; we want ln(P) so that diff() yields log-returns. - index_prices_for_correlation = pd.DataFrame( - np.log(data_for_correlation), - index=data_for_correlation.index, - columns=data_for_correlation.columns, - ) + if signed_log_transform: + # Allow non-positive prices: sign(x) * log(1 + |x|). + index_prices_for_correlation = pd.DataFrame( + np.sign(data_for_correlation) * np.log1p(np.abs(data_for_correlation)), + index=data_for_correlation.index, + columns=data_for_correlation.columns, + ) + else: + index_prices_for_correlation = pd.DataFrame( + np.log(data_for_correlation), + index=data_for_correlation.index, + columns=data_for_correlation.columns, + ) else: # Input is log-normal returns; cumsum yields the log-price index. index_prices_for_correlation = data_for_correlation.cumsum()