diff --git a/CHANGELOG.md b/CHANGELOG.md index f6e2e91..25de7f1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,7 +3,44 @@ All notable changes to RiskLabAI.py are documented here. Format: [Keep a Changelog](https://keepachangelog.com/en/1.1.0/); versioning: [SemVer](https://semver.org/). -## [Unreleased] +## [2.0.0] + +A **breaking** release that standardises the public API on PEP 8 names and makes +the core component registry the single way to construct cross-validators and +feature-importance strategies. **Every** renamed name keeps working with a +`DeprecationWarning` until 2.1.0, so existing code does not break on upgrade — +see `NAMING_CANON_2.0.0.md`. + +### Changed (BREAKING) +- `backtest.bet_sizing` functions renamed to snake_case: + `avgActiveSignals`→`avg_active_signals`, + `mpAvgActiveSignals`→`mp_avg_active_signals`, + `discreteSignal`→`discrete_signal`, `Signal`→`generate_signal`, + `betSize`→`bet_size_sigmoid`, `getW`→`compute_sigmoid_width`, + `TPos`→`target_position`, `inversePrice`→`inverse_price`, + `limitPrice`→`limit_price`. Their parameters are snake_case too (e.g. + `nThreads`→`n_threads`, `stepSize`→`step_size`, `acctualPrice`→`actual_price`, + `maximumPositionSize`→`maximum_position_size`). Keyword-argument callers must + update parameter names; positional calls are unaffected. +- Classes renamed: `pde.FBSNNolver`→`FBSNNSolver` (typo fix); + `optimization.MyPipeline`→`SampleWeightedPipeline`. +- Constants given ASCII identifiers — the string *values* are unchanged, so + stored data and internal dict keys are unaffected: `CUMULATIVE_θ`→ + `CUMULATIVE_THETA`, `CUMULATIVE_BUY_θ`→`CUMULATIVE_BUY_THETA`, + `CUMULATIVE_SELL_θ`→`CUMULATIVE_SELL_THETA`. +- The core registries (`RiskLabAI.core.CROSS_VALIDATORS`, `FEATURE_IMPORTANCE`) + are now the single way to construct those components. The bars stack imports + its column-name constants explicitly (no more `from ...constants import *`). + +### Deprecated +The following keep working until **2.1.0**, emitting a `DeprecationWarning` +that names the replacement: +- The old `bet_sizing` camelCase function names, `FBSNNolver`, `MyPipeline`, + and the `CUMULATIVE_*θ` constant identifiers (accessed via `RiskLabAI.utils`). +- `CrossValidatorFactory`, `CrossValidatorController`, + `FeatureImportanceFactory`, and `FeatureImportanceController` — use + `RiskLabAI.core.CROSS_VALIDATORS.create(...)` / + `RiskLabAI.core.FEATURE_IMPORTANCE.create(...)` instead. ### Added - `RiskLabAI.core`: a non-breaking extension layer that makes the library easier diff --git a/NAMING_CANON_2.0.0.md b/NAMING_CANON_2.0.0.md index b1879cc..dba275a 100644 --- a/NAMING_CANON_2.0.0.md +++ b/NAMING_CANON_2.0.0.md @@ -1,6 +1,8 @@ # RiskLabAI.py 2.0.0 — Naming Canon (proposal) -Status: **proposal, awaiting approval.** No code changes yet. This is the +Status: **approved and implemented in 2.0.0.** All §2 renames were approved +(remove aliases in 2.1.0; keep the `θ` string values; scope folded in the +star-import cleanup and the controller/factory→registry refactor). This is the breaking API cleanup from `IMPROVEMENT_PLAN.md` Phase 3, scoped to Python and written so that **no existing user breaks on upgrade** — every renamed name keeps working (with a `DeprecationWarning`) for one minor cycle before removal. diff --git a/RiskLabAI/_deprecation.py b/RiskLabAI/_deprecation.py new file mode 100644 index 0000000..b094271 --- /dev/null +++ b/RiskLabAI/_deprecation.py @@ -0,0 +1,123 @@ +""" +Internal helpers for deprecating public names without breaking callers. + +Used by the 2.0.0 naming canon (see ``NAMING_CANON_2.0.0.md``): every renamed +function, class, or constant keeps working throughout the 2.0.x series and emits +a :class:`DeprecationWarning` naming its replacement. The aliases are scheduled +for removal in 2.1.0. + +Three mechanisms, one per kind of object: + +- :func:`deprecated_alias` wraps a renamed **function**. +- :func:`deprecated_class` subclasses a renamed **class**. +- :func:`warn_deprecated_name` is called from a module ``__getattr__`` (PEP 562) + for renamed **module-level names** such as constants. +""" + +from __future__ import annotations + +import functools +import warnings +from typing import Any, Callable, TypeVar + +__all__ = ["deprecated_alias", "deprecated_class", "warn_deprecated_name"] + +_F = TypeVar("_F", bound=Callable[..., Any]) + + +def deprecated_alias(new_func: _F, old_name: str, *, removed_in: str) -> _F: + """ + Return a thin wrapper that calls ``new_func`` but warns under the old name. + + Parameters + ---------- + new_func : Callable + The renamed (canonical) function. + old_name : str + The deprecated public name being kept alive. + removed_in : str + Version in which the alias will be removed (e.g. ``"2.1.0"``). + + Returns + ------- + Callable + A wrapper with ``__name__`` set to ``old_name`` that emits a + :class:`DeprecationWarning` and delegates to ``new_func``. + """ + + @functools.wraps(new_func) + def wrapper(*args: Any, **kwargs: Any) -> Any: + warnings.warn( + f"{old_name}() is deprecated and will be removed in {removed_in}; " + f"use {new_func.__name__}() instead.", + DeprecationWarning, + stacklevel=2, + ) + return new_func(*args, **kwargs) + + wrapper.__name__ = old_name + wrapper.__qualname__ = old_name + wrapper.__doc__ = ( + f"Deprecated alias for :func:`{new_func.__name__}` " + f"(removed in {removed_in})." + ) + return wrapper # type: ignore[return-value] + + +def deprecated_class(new_cls: type, old_name: str, *, removed_in: str) -> type: + """ + Return a subclass of ``new_cls`` that warns when instantiated. + + The subclass behaves identically to ``new_cls`` but emits a + :class:`DeprecationWarning` on construction, naming the replacement. + + Parameters + ---------- + new_cls : type + The renamed (canonical) class. + old_name : str + The deprecated public class name being kept alive. + removed_in : str + Version in which the alias will be removed. + """ + + def __init__(self: Any, *args: Any, **kwargs: Any) -> None: + warnings.warn( + f"{old_name} is deprecated and will be removed in {removed_in}; " + f"use {new_cls.__name__} instead.", + DeprecationWarning, + stacklevel=2, + ) + new_cls.__init__(self, *args, **kwargs) + + shim = type(old_name, (new_cls,), {"__init__": __init__}) + shim.__doc__ = ( + f"Deprecated alias for :class:`{new_cls.__name__}` " + f"(removed in {removed_in})." + ) + shim.__module__ = new_cls.__module__ + return shim + + +def warn_deprecated_name(old_name: str, new_name: str, *, removed_in: str) -> None: + """ + Emit a deprecation warning for a renamed module-level name. + + Call this from a module's ``__getattr__`` (PEP 562) before returning the + value bound to ``new_name``. + + Parameters + ---------- + old_name : str + The deprecated name that was accessed. + new_name : str + The canonical replacement name. + removed_in : str + Version in which the old name will be removed. + """ + warnings.warn( + f"{old_name} is deprecated and will be removed in {removed_in}; " + f"use {new_name} instead.", + DeprecationWarning, + stacklevel=3, + ) diff --git a/RiskLabAI/backtest/__init__.py b/RiskLabAI/backtest/__init__.py index 8ad4c30..0a53a66 100644 --- a/RiskLabAI/backtest/__init__.py +++ b/RiskLabAI/backtest/__init__.py @@ -40,15 +40,24 @@ Signal, TPos, average_bet_sizes, + avg_active_signals, avgActiveSignals, + bet_size_sigmoid, betSize, + compute_sigmoid_width, + discrete_signal, discreteSignal, + generate_signal, getW, + inverse_price, inversePrice, + limit_price, limitPrice, + mp_avg_active_signals, mpAvgActiveSignals, probability_bet_size, strategy_bet_sizing, + target_position, ) from .probabilistic_sharpe_ratio import ( benchmark_sharpe_ratio, @@ -89,10 +98,20 @@ "compute_drawdowns_time_under_water", # from backtest_synthetic_data "synthetic_back_testing", - # from bet_sizing + # from bet_sizing (canonical 2.0.0 names) "probability_bet_size", "average_bet_sizes", "strategy_bet_sizing", + "avg_active_signals", + "mp_avg_active_signals", + "discrete_signal", + "generate_signal", + "bet_size_sigmoid", + "target_position", + "inverse_price", + "limit_price", + "compute_sigmoid_width", + # from bet_sizing (deprecated aliases, removed in 2.1.0) "avgActiveSignals", "mpAvgActiveSignals", "discreteSignal", diff --git a/RiskLabAI/backtest/backtest_overfitting_simulation.py b/RiskLabAI/backtest/backtest_overfitting_simulation.py index 732eaf9..db37eff 100644 --- a/RiskLabAI/backtest/backtest_overfitting_simulation.py +++ b/RiskLabAI/backtest/backtest_overfitting_simulation.py @@ -29,7 +29,7 @@ from sklearn.linear_model import LogisticRegression from tqdm import tqdm -from RiskLabAI.backtest.validation import CrossValidatorController +from RiskLabAI.core import CROSS_VALIDATORS from RiskLabAI.data.differentiation import fractionally_differentiated_log_price from RiskLabAI.data.labeling import ( cusum_filter_events_dynamic_threshold, @@ -49,6 +49,16 @@ from .probability_of_backtest_overfitting import probability_of_backtest_overfitting +def _create_cross_validator(validator_type, **kwargs): + """ + Build a cross-validator from the core registry (the 2.0.0 single source). + + Mirrors the old ``_create_cross_validator(...)`` behaviour, + including dropping keyword arguments a given validator does not accept. + """ + return CROSS_VALIDATORS.create(validator_type, filter_unknown_kwargs=True, **kwargs) + + def financial_features_backtest_overfitting_simulation( prices: pd.Series, noise_scale: float = 0.0, random_state: Optional[int] = None ) -> pd.DataFrame: @@ -357,20 +367,20 @@ def overall_backtest_overfitting_simulation( """ cross_validators = { - "Walk-Forward": CrossValidatorController( + "Walk-Forward": _create_cross_validator( "walkforward", n_splits=4, - ).cross_validator, - "K-Fold": CrossValidatorController( + ), + "K-Fold": _create_cross_validator( "kfold", n_splits=4, - ).cross_validator, - "Purged K-Fold": CrossValidatorController( + ), + "Purged K-Fold": _create_cross_validator( "purgedkfold", n_splits=4, times=None, embargo=0.02 - ).cross_validator, - "Combinatorial Purged": CrossValidatorController( + ), + "Combinatorial Purged": _create_cross_validator( "combinatorialpurged", n_splits=8, n_test_groups=2, times=None, embargo=0.02 - ).cross_validator, + ), } results = backtest_overfitting_simulation_results( @@ -432,20 +442,20 @@ def temporal_backtest_overfitting_simulation( Tuple[Dict[str, List[float]], Dict[str, List[float]]]: A tuple containing two dictionaries, one for the Probability of Backtest Overfitting (PBO) and the other for the Deflated Sharpe Ratio (DSR), for each cross-validation method tested. """ cross_validators = { - "Walk-Forward": CrossValidatorController( + "Walk-Forward": _create_cross_validator( "walkforward", n_splits=4, - ).cross_validator, - "K-Fold": CrossValidatorController( + ), + "K-Fold": _create_cross_validator( "kfold", n_splits=4, - ).cross_validator, - "Purged K-Fold": CrossValidatorController( + ), + "Purged K-Fold": _create_cross_validator( "purgedkfold", n_splits=4, times=None, embargo=0.02 - ).cross_validator, - "Combinatorial Purged": CrossValidatorController( + ), + "Combinatorial Purged": _create_cross_validator( "combinatorialpurged", n_splits=8, n_test_groups=2, times=None, embargo=0.02 - ).cross_validator, + ), } results = backtest_overfitting_simulation_results( @@ -506,20 +516,20 @@ def time_temporal_backtest_overfitting_simulation( Tuple[Dict[str, pd.Series], Dict[str, pd.Series]]: A tuple containing two dictionaries, one for the Probability of Backtest Overfitting (PBO) and the other for the Deflated Sharpe Ratio (DSR), for each cross-validation method tested, indexed by time. """ cross_validators = { - "Walk-Forward": CrossValidatorController( + "Walk-Forward": _create_cross_validator( "walkforward", n_splits=4, - ).cross_validator, - "K-Fold": CrossValidatorController( + ), + "K-Fold": _create_cross_validator( "kfold", n_splits=4, - ).cross_validator, - "Purged K-Fold": CrossValidatorController( + ), + "Purged K-Fold": _create_cross_validator( "purgedkfold", n_splits=4, times=None, embargo=0.02 - ).cross_validator, - "Combinatorial Purged": CrossValidatorController( + ), + "Combinatorial Purged": _create_cross_validator( "combinatorialpurged", n_splits=8, n_test_groups=2, times=None, embargo=0.02 - ).cross_validator, + ), } results = backtest_overfitting_simulation_results( @@ -602,16 +612,16 @@ def varying_embargo_backtest_overfitting_simulation( } cross_validators = { - "Purged K-Fold": CrossValidatorController( + "Purged K-Fold": _create_cross_validator( "purgedkfold", n_splits=4, times=None, embargo=embargo - ).cross_validator, - "Combinatorial Purged": CrossValidatorController( + ), + "Combinatorial Purged": _create_cross_validator( "combinatorialpurged", n_splits=8, n_test_groups=2, times=None, embargo=embargo, - ).cross_validator, + ), } # Iterate over each strategy parameter combination @@ -813,20 +823,20 @@ def backtest_overfitting_simulation_financial_metrics_rank_correlation( """ # Run the backtest overfitting simulation to get results cross_validators = { - "Walk-Forward": CrossValidatorController( + "Walk-Forward": _create_cross_validator( "walkforward", n_splits=4, - ).cross_validator, - "K-Fold": CrossValidatorController( + ), + "K-Fold": _create_cross_validator( "kfold", n_splits=4, - ).cross_validator, - "Purged K-Fold": CrossValidatorController( + ), + "Purged K-Fold": _create_cross_validator( "purgedkfold", n_splits=4, times=None, embargo=0.02 - ).cross_validator, - "Combinatorial Purged": CrossValidatorController( + ), + "Combinatorial Purged": _create_cross_validator( "combinatorialpurged", n_splits=8, n_test_groups=2, times=None, embargo=0.02 - ).cross_validator, + ), } results = backtest_overfitting_simulation_results( @@ -965,13 +975,13 @@ def backtest_overfitting_simulation_model_complexity( times = labels.loc[index]["End Time"] cross_validators = { - "Combinatorial Purged": CrossValidatorController( + "Combinatorial Purged": _create_cross_validator( "combinatorialpurged", n_splits=8, n_test_groups=2, times=times, embargo=0.02, - ).cross_validator, + ), } for cross_validator_type, cross_validator in cross_validators.items(): @@ -1119,24 +1129,24 @@ def overall_novel_methods_backtest_overfitting_simulation( """ cross_validators = { - "Combinatorial Purged": CrossValidatorController( + "Combinatorial Purged": _create_cross_validator( "combinatorialpurged", n_splits=8, n_test_groups=2, times=None, embargo=0.02 - ).cross_validator, - "Bagged Combinatorial Purged": CrossValidatorController( + ), + "Bagged Combinatorial Purged": _create_cross_validator( "baggedcombinatorialpurged", n_splits=8, n_test_groups=2, times=None, embargo=0.02, random_state=random_state, - ).cross_validator, - "Adaptive Combinatorial Purged": CrossValidatorController( + ), + "Adaptive Combinatorial Purged": _create_cross_validator( "adaptivecombinatorialpurged", n_splits=8, n_test_groups=2, times=None, embargo=0.02, - ).cross_validator, + ), } results = backtest_overfitting_simulation_results( @@ -1329,9 +1339,9 @@ def measure_cpcv_parallelization( model = LogisticRegression(penalty=None, solver="lbfgs", max_iter=10000) # Define the CPCV cross-validator - cpcv_cross_validator = CrossValidatorController( + cpcv_cross_validator = _create_cross_validator( "combinatorialpurged", n_splits=8, n_test_groups=2, times=times, embargo=0.02 - ).cross_validator + ) # Measure computational requirements with and without parallelization results = {} @@ -1383,13 +1393,13 @@ def measure_cpcv_scalability( data, target, weights, times = generate_random_data(n_samples, n_features) # Define the CPCV cross-validator - cpcv_cross_validator = CrossValidatorController( + cpcv_cross_validator = _create_cross_validator( "combinatorialpurged", n_splits=8, n_test_groups=2, times=times, embargo=0.02, - ).cross_validator + ) # Measure computational requirements result = measure_computational_requirements( diff --git a/RiskLabAI/backtest/bet_sizing.py b/RiskLabAI/backtest/bet_sizing.py index 329c380..e5f332c 100644 --- a/RiskLabAI/backtest/bet_sizing.py +++ b/RiskLabAI/backtest/bet_sizing.py @@ -10,6 +10,7 @@ from numba import jit from scipy.stats import norm +from RiskLabAI._deprecation import deprecated_alias from RiskLabAI.hpc import mp_pandas_obj @@ -143,11 +144,10 @@ def strategy_bet_sizing( return pd.Series(avg_bet_sizes_arr, index=price_timestamps) -# --- The following functions appear to be from de Prado (2018) --- -# --- Naming convention (camelCase) is preserved for reference. --- +# --- Bet-sizing functions from de Prado (2018), AFML Chapter 10. --- -def avgActiveSignals(signals: pd.DataFrame, nThreads: int) -> pd.DataFrame: +def avg_active_signals(signals: pd.DataFrame, n_threads: int) -> pd.DataFrame: """ Calculate the average signal among active signals using parallel processing. @@ -159,7 +159,7 @@ def avgActiveSignals(signals: pd.DataFrame, nThreads: int) -> pd.DataFrame: signals : pd.DataFrame DataFrame with signal start times as index, and columns 't1' (end time) and 'signal' (signal value). - nThreads : int + n_threads : int Number of threads to use for parallel execution via `mp_pandas_obj`. Returns @@ -168,24 +168,24 @@ def avgActiveSignals(signals: pd.DataFrame, nThreads: int) -> pd.DataFrame: DataFrame containing the average active signal at each time point. """ # 1) time points where signals change (either one starts or one ends) - timePoints = set(signals["t1"].dropna().values) - timePoints = timePoints.union(signals.index.values) - timePoints = list(timePoints) - timePoints.sort() + time_points = set(signals["t1"].dropna().values) + time_points = time_points.union(signals.index.values) + time_points = list(time_points) + time_points.sort() # 2) call parallel function out = mp_pandas_obj( - mpAvgActiveSignals, - ("molecule", timePoints), - nThreads, + mp_avg_active_signals, + ("molecule", time_points), + n_threads, signals=signals, ) return out -def mpAvgActiveSignals(signals: pd.DataFrame, molecule: list) -> pd.Series: +def mp_avg_active_signals(signals: pd.DataFrame, molecule: list) -> pd.Series: """ - Worker function for `avgActiveSignals`. + Worker function for `avg_active_signals`. At time `loc`, average signal among those still active. Signal is active if: @@ -246,7 +246,7 @@ def mpAvgActiveSignals(signals: pd.DataFrame, molecule: list) -> pd.Series: return pd.Series(averages, index=molecule_list) -def discreteSignal(signal: pd.Series, stepSize: float) -> pd.Series: +def discrete_signal(signal: pd.Series, step_size: float) -> pd.Series: """ Discretize a signal to a specific step size, capping at +/- 1. @@ -257,7 +257,7 @@ def discreteSignal(signal: pd.Series, stepSize: float) -> pd.Series: ---------- signal : pd.Series The continuous signal values (e.g., from -1 to 1). - stepSize : float + step_size : float The step size for discretization (e.g., 0.1). Returns @@ -265,19 +265,19 @@ def discreteSignal(signal: pd.Series, stepSize: float) -> pd.Series: pd.Series The discretized signal. """ - discretized = (signal / stepSize).round() * stepSize + discretized = (signal / step_size).round() * step_size discretized[discretized > 1] = 1.0 discretized[discretized < -1] = -1.0 return discretized -def Signal( +def generate_signal( events: pd.DataFrame, - stepSize: float, + step_size: float, probability: pd.Series, prediction: pd.Series, - nClasses: int, - nThreads: int, + n_classes: int, + n_threads: int, ) -> pd.Series: """ Generate a discretized, averaged signal from model predictions. @@ -290,16 +290,16 @@ def Signal( events : pd.DataFrame DataFrame of events, must include 't1' (end times) and optionally 'side' (for meta-labeling). - stepSize : float + step_size : float The step size for final signal discretization. probability : pd.Series Probability of class 1 (e.g., from `predict_proba`). prediction : pd.Series The predicted class (e.g., 1 or -1). - nClasses : int + n_classes : int Number of classes in the prediction. - nThreads : int - Number of threads for `avgActiveSignals`. + n_threads : int + Number of threads for `avg_active_signals`. Returns ------- @@ -311,7 +311,7 @@ def Signal( # 1) generate signals from multinomial classification (one-vs-rest, OvR) # t-value of OvR - t_value = (probability - 1.0 / nClasses) / ( + t_value = (probability - 1.0 / n_classes) / ( (probability * (1.0 - probability)) ** 0.5 ) signal = prediction * (2 * norm.cdf(t_value) - 1) # signal = side * size @@ -321,14 +321,14 @@ def Signal( # 2) compute average signal among those concurrently open signal_df = signal.to_frame("signal").join(events[["t1"]], how="left") - avg_signal = avgActiveSignals(signal_df, nThreads) + avg_signal = avg_active_signals(signal_df, n_threads) # 3) discretize signal - discretized_signal = discreteSignal(signal=avg_signal, stepSize=stepSize) + discretized_signal = discrete_signal(signal=avg_signal, step_size=step_size) return discretized_signal -def betSize(w: float, x: float) -> float: +def bet_size_sigmoid(w: float, x: float) -> float: """ Calculate bet size using a sigmoid function. @@ -350,7 +350,9 @@ def betSize(w: float, x: float) -> float: return x / np.sqrt(w + x**2) -def TPos(w: float, f: float, acctualPrice: float, maximumPositionSize: int) -> int: +def target_position( + w: float, f: float, actual_price: float, maximum_position_size: int +) -> int: """ Calculate the target position size. @@ -363,9 +365,9 @@ def TPos(w: float, f: float, acctualPrice: float, maximumPositionSize: int) -> i Coefficient regulating the sigmoid width. f : float Forecasted price. - acctualPrice : float + actual_price : float Actual (current) market price. - maximumPositionSize : int + maximum_position_size : int Maximum absolute position size. Returns @@ -373,10 +375,10 @@ def TPos(w: float, f: float, acctualPrice: float, maximumPositionSize: int) -> i int The target position size (integer). """ - return int(betSize(w, f - acctualPrice) * maximumPositionSize) + return int(bet_size_sigmoid(w, f - actual_price) * maximum_position_size) -def inversePrice(f: float, w: float, m: float) -> float: +def inverse_price(f: float, w: float, m: float) -> float: """ Calculates the inverse price given a bet size. @@ -402,12 +404,12 @@ def inversePrice(f: float, w: float, m: float) -> float: return f - m * np.sqrt(w / (1 - m**2)) -def limitPrice( - targetPositionSize: int, - cPosition: int, +def limit_price( + target_position_size: int, + current_position: int, f: float, w: float, - maximumPositionSize: int, + maximum_position_size: int, ) -> float: """ Calculate the limit price for adjusting position. @@ -417,15 +419,15 @@ def limitPrice( Parameters ---------- - targetPositionSize : int + target_position_size : int The target position size. - cPosition : int + current_position : int The current position size. f : float Forecasted price. w : float Coefficient regulating the sigmoid width. - maximumPositionSize : int + maximum_position_size : int Maximum absolute position size. Returns @@ -433,21 +435,21 @@ def limitPrice( float The average limit price. """ - if targetPositionSize == cPosition: + if target_position_size == current_position: return f # No change - sgn = np.sign(targetPositionSize - cPosition) - lP = 0.0 + sgn = np.sign(target_position_size - current_position) + limit = 0.0 # Average price from current to target position - for i in range(abs(cPosition + sgn), abs(targetPositionSize + sgn)): - lP += inversePrice(f, w, i / float(maximumPositionSize)) + for i in range(abs(current_position + sgn), abs(target_position_size + sgn)): + limit += inverse_price(f, w, i / float(maximum_position_size)) - lP /= abs(targetPositionSize - cPosition) - return lP + limit /= abs(target_position_size - current_position) + return limit -def getW(x: float, m: float) -> float: +def compute_sigmoid_width(x: float, m: float) -> float: """ Get the 'w' coefficient implied by a divergence and bet size. @@ -469,3 +471,23 @@ def getW(x: float, m: float) -> float: if m == 0.0 or m == 1.0 or m == -1.0: return np.inf # w is undefined return x**2 * ((1 / m**2) - 1) + + +# --------------------------------------------------------------------------- # +# Deprecated camelCase aliases (the historical AFML-style names). Each keeps +# working and emits a DeprecationWarning; scheduled for removal in 2.1.0. +# See NAMING_CANON_2.0.0.md. +# --------------------------------------------------------------------------- # +avgActiveSignals = deprecated_alias( + avg_active_signals, "avgActiveSignals", removed_in="2.1.0" +) +mpAvgActiveSignals = deprecated_alias( + mp_avg_active_signals, "mpAvgActiveSignals", removed_in="2.1.0" +) +discreteSignal = deprecated_alias(discrete_signal, "discreteSignal", removed_in="2.1.0") +Signal = deprecated_alias(generate_signal, "Signal", removed_in="2.1.0") +betSize = deprecated_alias(bet_size_sigmoid, "betSize", removed_in="2.1.0") +TPos = deprecated_alias(target_position, "TPos", removed_in="2.1.0") +inversePrice = deprecated_alias(inverse_price, "inversePrice", removed_in="2.1.0") +limitPrice = deprecated_alias(limit_price, "limitPrice", removed_in="2.1.0") +getW = deprecated_alias(compute_sigmoid_width, "getW", removed_in="2.1.0") diff --git a/RiskLabAI/backtest/validation/cross_validator_controller.py b/RiskLabAI/backtest/validation/cross_validator_controller.py index 08d5669..7c70742 100644 --- a/RiskLabAI/backtest/validation/cross_validator_controller.py +++ b/RiskLabAI/backtest/validation/cross_validator_controller.py @@ -1,45 +1,38 @@ -""" -Controller to simplify the creation and use of cross-validators. -""" - -from typing import Any - -from .cross_validator_factory import CrossValidatorFactory -from .cross_validator_interface import CrossValidator - - -class CrossValidatorController: - """ - Controller class to handle the cross-validation process. - - This class acts as a high-level interface, simplifying the - creation and access to a specific cross-validator using the factory. - """ - - def __init__(self, validator_type: str, **kwargs: Any): - """ - Initializes the CrossValidatorController. - - Parameters - ---------- - validator_type : str - Type of cross-validator to create (e.g., 'kfold', - 'combinatorialpurged'). This is passed to the factory. - **kwargs : Any - Additional keyword arguments to be passed to the - cross-validator's constructor. - """ - self.cross_validator: CrossValidator = ( - CrossValidatorFactory.create_cross_validator(validator_type, **kwargs) - ) - - def get_validator(self) -> CrossValidator: - """ - Get the created cross-validator instance. - - Returns - ------- - CrossValidator - The underlying cross-validator instance. - """ - return self.cross_validator +""" +Deprecated controller for creating and holding a cross-validator. + +Use the core component registry (:data:`RiskLabAI.core.CROSS_VALIDATORS`) +directly instead. This controller is retained for backward compatibility and +delegates to that registry; it is removed in 2.1.0. +""" + +import warnings +from typing import Any + +from .cross_validator_interface import CrossValidator + + +class CrossValidatorController: + """ + Deprecated. Use ``RiskLabAI.core.CROSS_VALIDATORS.create(...)`` instead. + + Removed in 2.1.0. + """ + + def __init__(self, validator_type: str, **kwargs: Any): + warnings.warn( + "CrossValidatorController is deprecated and will be removed in " + "2.1.0; use RiskLabAI.core.CROSS_VALIDATORS.create(validator_type, " + "...) instead.", + DeprecationWarning, + stacklevel=2, + ) + from RiskLabAI.core import CROSS_VALIDATORS + + self.cross_validator: CrossValidator = CROSS_VALIDATORS.create( + validator_type, filter_unknown_kwargs=True, **kwargs + ) + + def get_validator(self) -> CrossValidator: + """Return the created cross-validator instance.""" + return self.cross_validator diff --git a/RiskLabAI/backtest/validation/cross_validator_factory.py b/RiskLabAI/backtest/validation/cross_validator_factory.py index 79bdfef..2e2a53c 100644 --- a/RiskLabAI/backtest/validation/cross_validator_factory.py +++ b/RiskLabAI/backtest/validation/cross_validator_factory.py @@ -1,70 +1,74 @@ -""" -Factory for creating cross-validator instances. -""" - -import inspect -from typing import Any - -from .adaptive_combinatorial_purged import AdaptiveCombinatorialPurged -from .bagged_combinatorial_purged import BaggedCombinatorialPurged -from .combinatorial_purged import CombinatorialPurged -from .cross_validator_interface import CrossValidator -from .kfold import KFold -from .purged_kfold import PurgedKFold -from .walk_forward import WalkForward - - -class CrossValidatorFactory: - """ - Factory class for creating cross-validator objects. - - This class uses a static method to encapsulate the logic for - instantiating different cross-validator strategies. - """ - - VALIDATORS = { - "kfold": KFold, - "walkforward": WalkForward, - "purgedkfold": PurgedKFold, - "combinatorialpurged": CombinatorialPurged, - "baggedcombinatorialpurged": BaggedCombinatorialPurged, - "adaptivecombinatorialpurged": AdaptiveCombinatorialPurged, - } - - @staticmethod - def create_cross_validator(validator_type: str, **kwargs: Any) -> CrossValidator: - """ - Factory method to create and return an instance of a cross-validator. - - Parameters - ---------- - validator_type : str - Type of cross-validator to create. Must be one of: - 'kfold', 'walkforward', 'purgedkfold', 'combinatorialpurged', - 'baggedcombinatorialpurged', 'adaptivecombinatorialpurged'. - **kwargs : Any - Keyword arguments to be passed to the cross-validator's - constructor. - - Returns - ------- - CrossValidator - An instance of the specified cross-validator. - - Raises - ------ - ValueError - If an invalid `validator_type` is provided. - """ - validator_type = validator_type.lower() - validator_class = CrossValidatorFactory.VALIDATORS.get(validator_type) - - if validator_class: - sig = inspect.signature(validator_class.__init__) - valid_kwargs = {k: v for k, v in kwargs.items() if k in sig.parameters} - return validator_class(**valid_kwargs) - - raise ValueError( - f"Invalid validator_type: {validator_type}. " - f"Valid types are: {list(CrossValidatorFactory.VALIDATORS.keys())}" - ) +""" +Deprecated factory for creating cross-validator instances. + +The core component registry (:data:`RiskLabAI.core.CROSS_VALIDATORS`) is now the +single source of truth for creating cross-validators. This factory is retained +for backward compatibility and simply delegates to that registry; it is removed +in 2.1.0. +""" + +import warnings +from typing import Any + +from .adaptive_combinatorial_purged import AdaptiveCombinatorialPurged +from .bagged_combinatorial_purged import BaggedCombinatorialPurged +from .combinatorial_purged import CombinatorialPurged +from .cross_validator_interface import CrossValidator +from .kfold import KFold +from .purged_kfold import PurgedKFold +from .walk_forward import WalkForward + + +class CrossValidatorFactory: + """ + Deprecated. Use ``RiskLabAI.core.CROSS_VALIDATORS.create(...)`` instead. + + Retained for backward compatibility (removed in 2.1.0). ``VALIDATORS`` is + kept so existing introspection keeps working; ``create_cross_validator`` + now delegates to the core registry. + """ + + VALIDATORS = { + "kfold": KFold, + "walkforward": WalkForward, + "purgedkfold": PurgedKFold, + "combinatorialpurged": CombinatorialPurged, + "baggedcombinatorialpurged": BaggedCombinatorialPurged, + "adaptivecombinatorialpurged": AdaptiveCombinatorialPurged, + } + + @staticmethod + def create_cross_validator(validator_type: str, **kwargs: Any) -> CrossValidator: + """ + Deprecated. Create a cross-validator via the core registry. + + Parameters + ---------- + validator_type : str + One of the keys in :attr:`VALIDATORS` (case-insensitive). + **kwargs : Any + Forwarded to the validator's constructor; arguments it does not + accept are dropped (matching the historical behaviour). + + Raises + ------ + ValueError + If ``validator_type`` is not a known validator. + """ + warnings.warn( + "CrossValidatorFactory is deprecated and will be removed in 2.1.0; " + "use RiskLabAI.core.CROSS_VALIDATORS.create(validator_type, ...) " + "instead.", + DeprecationWarning, + stacklevel=2, + ) + from RiskLabAI.core import CROSS_VALIDATORS + + if validator_type.lower() not in CROSS_VALIDATORS: + raise ValueError( + f"Invalid validator_type: {validator_type}. " + f"Valid types are: {list(CrossValidatorFactory.VALIDATORS.keys())}" + ) + return CROSS_VALIDATORS.create( + validator_type, filter_unknown_kwargs=True, **kwargs + ) diff --git a/RiskLabAI/data/structures/abstract_bars.py b/RiskLabAI/data/structures/abstract_bars.py index 2bdfcd8..d71ea25 100644 --- a/RiskLabAI/data/structures/abstract_bars.py +++ b/RiskLabAI/data/structures/abstract_bars.py @@ -9,7 +9,14 @@ import numpy as np -from RiskLabAI.utils.constants import * +from RiskLabAI.utils.constants import ( + CUMULATIVE_BUY_VOLUME, + CUMULATIVE_DOLLAR, + CUMULATIVE_TICKS, + CUMULATIVE_VOLUME, + N_TICKS_ON_BAR_FORMATION, + PREVIOUS_TICK_RULE, +) # Type hint for a single tick: (datetime, price, volume) TickData = Union[list[Any], tuple[Any, ...], np.ndarray] diff --git a/RiskLabAI/data/structures/abstract_imbalance_bars.py b/RiskLabAI/data/structures/abstract_imbalance_bars.py index 307846a..2bd416e 100644 --- a/RiskLabAI/data/structures/abstract_imbalance_bars.py +++ b/RiskLabAI/data/structures/abstract_imbalance_bars.py @@ -12,7 +12,15 @@ from RiskLabAI.data.structures.abstract_information_driven_bars import ( AbstractInformationDrivenBars, ) -from RiskLabAI.utils.constants import * +from RiskLabAI.utils.constants import ( + CUMULATIVE_THETA, + CUMULATIVE_TICKS, + EXPECTED_IMBALANCE, + EXPECTED_IMBALANCE_WINDOW, + EXPECTED_TICKS_NUMBER, + PREVIOUS_BARS_N_TICKS_LIST, + PREVIOUS_TICK_IMBALANCES_LIST, +) class AbstractImbalanceBars(AbstractInformationDrivenBars): @@ -46,7 +54,7 @@ def __init__( ) self.imbalance_bars_statistics = { - CUMULATIVE_θ: 0.0, + CUMULATIVE_THETA: 0.0, EXPECTED_IMBALANCE: np.nan, PREVIOUS_BARS_N_TICKS_LIST: [], PREVIOUS_TICK_IMBALANCES_LIST: [], @@ -80,7 +88,7 @@ def construct_bars_from_data(self, data: Iterable[TickData]) -> list[list[Any]]: self.imbalance_bars_statistics[PREVIOUS_TICK_IMBALANCES_LIST].append( imbalance ) - self.imbalance_bars_statistics[CUMULATIVE_θ] += imbalance + self.imbalance_bars_statistics[CUMULATIVE_THETA] += imbalance # Warm-up E[b] if it's the first time if np.isnan(self.imbalance_bars_statistics[EXPECTED_IMBALANCE]): @@ -156,13 +164,13 @@ def _bar_construction_condition(self, threshold: float) -> bool: if np.isnan(threshold) or np.isinf(threshold): return False - cumulative_theta = self.imbalance_bars_statistics[CUMULATIVE_θ] + cumulative_theta = self.imbalance_bars_statistics[CUMULATIVE_THETA] return np.abs(cumulative_theta) >= threshold def _reset_cached_fields(self): """Reset base fields and cumulative theta.""" super()._reset_cached_fields() - self.imbalance_bars_statistics[CUMULATIVE_θ] = 0.0 + self.imbalance_bars_statistics[CUMULATIVE_THETA] = 0.0 @abstractmethod def _expected_number_of_ticks(self) -> float: diff --git a/RiskLabAI/data/structures/abstract_information_driven_bars.py b/RiskLabAI/data/structures/abstract_information_driven_bars.py index c95228d..63018f2 100644 --- a/RiskLabAI/data/structures/abstract_information_driven_bars.py +++ b/RiskLabAI/data/structures/abstract_information_driven_bars.py @@ -8,7 +8,10 @@ import numpy as np from RiskLabAI.data.structures.abstract_bars import AbstractBars -from RiskLabAI.utils.constants import * +from RiskLabAI.utils.constants import ( + EXPECTED_IMBALANCE_WINDOW, + EXPECTED_TICKS_NUMBER, +) from RiskLabAI.utils.ewma import ewma diff --git a/RiskLabAI/data/structures/abstract_run_bars.py b/RiskLabAI/data/structures/abstract_run_bars.py index df46a99..a74b8c0 100644 --- a/RiskLabAI/data/structures/abstract_run_bars.py +++ b/RiskLabAI/data/structures/abstract_run_bars.py @@ -12,7 +12,21 @@ from RiskLabAI.data.structures.abstract_information_driven_bars import ( AbstractInformationDrivenBars, ) -from RiskLabAI.utils.constants import * +from RiskLabAI.utils.constants import ( + BUY_TICKS_NUMBER, + CUMULATIVE_BUY_THETA, + CUMULATIVE_SELL_THETA, + CUMULATIVE_TICKS, + EXPECTED_BUY_IMBALANCE, + EXPECTED_BUY_TICKS_PROPORTION, + EXPECTED_IMBALANCE_WINDOW, + EXPECTED_SELL_IMBALANCE, + EXPECTED_TICKS_NUMBER, + PREVIOUS_BARS_BUY_TICKS_PROPORTIONS_LIST, + PREVIOUS_BARS_N_TICKS_LIST, + PREVIOUS_TICK_IMBALANCES_BUY_LIST, + PREVIOUS_TICK_IMBALANCES_SELL_LIST, +) from RiskLabAI.utils.ewma import ewma @@ -42,8 +56,8 @@ def __init__( ) self.run_bars_statistics = { - CUMULATIVE_BUY_θ: 0.0, - CUMULATIVE_SELL_θ: 0.0, + CUMULATIVE_BUY_THETA: 0.0, + CUMULATIVE_SELL_THETA: 0.0, EXPECTED_BUY_IMBALANCE: np.nan, EXPECTED_SELL_IMBALANCE: np.nan, EXPECTED_BUY_TICKS_PROPORTION: np.nan, @@ -84,13 +98,13 @@ def construct_bars_from_data(self, data: Iterable[TickData]) -> list[list[Any]]: self.run_bars_statistics[PREVIOUS_TICK_IMBALANCES_BUY_LIST].append( imbalance ) - self.run_bars_statistics[CUMULATIVE_BUY_θ] += imbalance + self.run_bars_statistics[CUMULATIVE_BUY_THETA] += imbalance self.run_bars_statistics[BUY_TICKS_NUMBER] += 1 elif imbalance < 0: self.run_bars_statistics[PREVIOUS_TICK_IMBALANCES_SELL_LIST].append( -imbalance ) - self.run_bars_statistics[CUMULATIVE_SELL_θ] += -imbalance + self.run_bars_statistics[CUMULATIVE_SELL_THETA] += -imbalance # Warm-up E[theta_buy], E[theta_sell], and P[buy] warm_up_stats = [ @@ -229,16 +243,16 @@ def _bar_construction_condition(self, threshold: float) -> bool: return False max_theta = max( - self.run_bars_statistics[CUMULATIVE_BUY_θ], - self.run_bars_statistics[CUMULATIVE_SELL_θ], + self.run_bars_statistics[CUMULATIVE_BUY_THETA], + self.run_bars_statistics[CUMULATIVE_SELL_THETA], ) return max_theta >= threshold def _reset_cached_fields(self): """Reset base fields and cumulative run counters.""" super()._reset_cached_fields() - self.run_bars_statistics[CUMULATIVE_BUY_θ] = 0.0 - self.run_bars_statistics[CUMULATIVE_SELL_θ] = 0.0 + self.run_bars_statistics[CUMULATIVE_BUY_THETA] = 0.0 + self.run_bars_statistics[CUMULATIVE_SELL_THETA] = 0.0 self.run_bars_statistics[BUY_TICKS_NUMBER] = 0 @abstractmethod diff --git a/RiskLabAI/data/structures/imbalance_bars.py b/RiskLabAI/data/structures/imbalance_bars.py index b6a6a44..749dee0 100644 --- a/RiskLabAI/data/structures/imbalance_bars.py +++ b/RiskLabAI/data/structures/imbalance_bars.py @@ -9,7 +9,10 @@ import numpy as np from RiskLabAI.data.structures.abstract_imbalance_bars import AbstractImbalanceBars -from RiskLabAI.utils.constants import * +from RiskLabAI.utils.constants import ( + EXPECTED_TICKS_NUMBER, + PREVIOUS_BARS_N_TICKS_LIST, +) from RiskLabAI.utils.ewma import ewma diff --git a/RiskLabAI/data/structures/run_bars.py b/RiskLabAI/data/structures/run_bars.py index 325afcb..ac1fb5c 100644 --- a/RiskLabAI/data/structures/run_bars.py +++ b/RiskLabAI/data/structures/run_bars.py @@ -9,7 +9,10 @@ import numpy as np from RiskLabAI.data.structures.abstract_run_bars import AbstractRunBars -from RiskLabAI.utils.constants import * +from RiskLabAI.utils.constants import ( + EXPECTED_TICKS_NUMBER, + PREVIOUS_BARS_N_TICKS_LIST, +) from RiskLabAI.utils.ewma import ewma diff --git a/RiskLabAI/features/feature_importance/feature_importance_controller.py b/RiskLabAI/features/feature_importance/feature_importance_controller.py index 2483b04..c34a4a7 100644 --- a/RiskLabAI/features/feature_importance/feature_importance_controller.py +++ b/RiskLabAI/features/feature_importance/feature_importance_controller.py @@ -1,75 +1,42 @@ -""" -Controller class to manage various feature importance strategies. -""" - -from typing import Any - -import pandas as pd - -from .feature_importance_factory import FeatureImportanceFactory -from .feature_importance_strategy import FeatureImportanceStrategy - - -class FeatureImportanceController: - """ - Controller class to manage and execute feature importance strategies. - - Example - ------- - .. code-block:: python - - from sklearn.ensemble import RandomForestClassifier - - my_classifier = RandomForestClassifier(n_estimators=10, seed=42) - my_clusters = {'cluster_0': ['feat_0', 'feat_1']} - - # Initialize the controller - controller = FeatureImportanceController( - 'ClusteredMDA', - classifier=my_classifier, - clusters=my_clusters, - n_splits=10 - ) - - # Calculate feature importance - result = controller.calculate_importance(my_x, my_y) - """ - - def __init__(self, strategy_type: str, **kwargs: Any): - """ - Initialize the controller with a specific feature importance strategy. - - Parameters - ---------- - strategy_type : str - The type of strategy to use (e.g., 'MDI', 'ClusteredMDA'). - **kwargs : Any - Configuration arguments to pass to the strategy's - constructor (e.g., `classifier`, `clusters`, `n_splits`). - """ - self.strategy_instance: FeatureImportanceStrategy = ( - FeatureImportanceFactory.create_feature_importance(strategy_type, **kwargs) - ) - - def calculate_importance( - self, x: pd.DataFrame, y: pd.Series, **kwargs: Any - ) -> pd.DataFrame: - """ - Calculate feature importance based on the initialized strategy. - - Parameters - ---------- - x : pd.DataFrame - Feature data. - y : pd.Series - Target data. - **kwargs : Any - Additional arguments to pass to the strategy's `compute` - method (e.g., `sample_weight`). - - Returns - ------- - pd.DataFrame - Feature importance results. - """ - return self.strategy_instance.compute(x, y, **kwargs) +""" +Deprecated controller for managing feature-importance strategies. + +Use the core component registry (:data:`RiskLabAI.core.FEATURE_IMPORTANCE`) +directly instead. This controller is retained for backward compatibility and +delegates to that registry; it is removed in 2.1.0. +""" + +import warnings +from typing import Any + +import pandas as pd + +from .feature_importance_strategy import FeatureImportanceStrategy + + +class FeatureImportanceController: + """ + Deprecated. Use ``RiskLabAI.core.FEATURE_IMPORTANCE.create(...)`` instead. + + Removed in 2.1.0. + """ + + def __init__(self, strategy_type: str, **kwargs: Any): + warnings.warn( + "FeatureImportanceController is deprecated and will be removed in " + "2.1.0; use RiskLabAI.core.FEATURE_IMPORTANCE.create(strategy_type, " + "...) instead.", + DeprecationWarning, + stacklevel=2, + ) + from RiskLabAI.core import FEATURE_IMPORTANCE + + self.strategy_instance: FeatureImportanceStrategy = FEATURE_IMPORTANCE.create( + strategy_type, filter_unknown_kwargs=True, **kwargs + ) + + def calculate_importance( + self, x: pd.DataFrame, y: pd.Series, **kwargs: Any + ) -> pd.DataFrame: + """Calculate feature importance using the configured strategy.""" + return self.strategy_instance.compute(x, y, **kwargs) diff --git a/RiskLabAI/features/feature_importance/feature_importance_factory.py b/RiskLabAI/features/feature_importance/feature_importance_factory.py index df1d622..a1a6f07 100644 --- a/RiskLabAI/features/feature_importance/feature_importance_factory.py +++ b/RiskLabAI/features/feature_importance/feature_importance_factory.py @@ -1,70 +1,50 @@ -""" -Factory class for creating feature importance strategy objects. -""" - -from typing import Any - -from .clustered_feature_importance_mda import ClusteredFeatureImportanceMDA -from .clustered_feature_importance_mdi import ClusteredFeatureImportanceMDI -from .feature_importance_mda import FeatureImportanceMDA -from .feature_importance_mdi import FeatureImportanceMDI -from .feature_importance_sfi import FeatureImportanceSFI -from .feature_importance_strategy import FeatureImportanceStrategy - - -class FeatureImportanceFactory: - """ - Factory class to create feature importance strategy instances. - """ - - @staticmethod - def create_feature_importance( - strategy_type: str, **kwargs: Any - ) -> FeatureImportanceStrategy: - """ - Factory method to create and return an instance of a feature - importance strategy. - - Parameters - ---------- - strategy_type : str - Type of strategy to create. Options include: - 'MDI', 'ClusteredMDI', 'MDA', 'ClusteredMDA', 'SFI'. - **kwargs : Any - Keyword arguments to be passed to the strategy's - constructor (e.g., `classifier`, `clusters`, `n_splits`). - - Returns - ------- - FeatureImportanceStrategy - An instance of the specified strategy. - - Raises - ------ - ValueError - If an invalid `strategy_type` is provided. - """ - - strategies: dict[str, type[FeatureImportanceStrategy]] = { - "MDI": FeatureImportanceMDI, - "ClusteredMDI": ClusteredFeatureImportanceMDI, - "MDA": FeatureImportanceMDA, - "ClusteredMDA": ClusteredFeatureImportanceMDA, - "SFI": FeatureImportanceSFI, - } - - strategy_class = strategies.get(strategy_type) - - if strategy_class: - # Pass only the relevant arguments to the constructor - # This uses introspection to be robust - import inspect - - sig = inspect.signature(strategy_class.__init__) - valid_kwargs = {k: v for k, v in kwargs.items() if k in sig.parameters} - return strategy_class(**valid_kwargs) - - raise ValueError( - f"Invalid strategy_type: {strategy_type}. " - f"Valid types are: {list(strategies.keys())}" - ) +""" +Deprecated factory for creating feature-importance strategy objects. + +The core component registry (:data:`RiskLabAI.core.FEATURE_IMPORTANCE`) is now +the single source of truth. This factory is retained for backward compatibility +and delegates to that registry; it is removed in 2.1.0. +""" + +import warnings +from typing import Any + +from .feature_importance_strategy import FeatureImportanceStrategy + + +class FeatureImportanceFactory: + """ + Deprecated. Use ``RiskLabAI.core.FEATURE_IMPORTANCE.create(...)`` instead. + + Removed in 2.1.0. + """ + + @staticmethod + def create_feature_importance( + strategy_type: str, **kwargs: Any + ) -> FeatureImportanceStrategy: + """ + Deprecated. Create a feature-importance strategy via the core registry. + + Raises + ------ + ValueError + If ``strategy_type`` is not a known strategy. + """ + warnings.warn( + "FeatureImportanceFactory is deprecated and will be removed in " + "2.1.0; use RiskLabAI.core.FEATURE_IMPORTANCE.create(strategy_type, " + "...) instead.", + DeprecationWarning, + stacklevel=2, + ) + from RiskLabAI.core import FEATURE_IMPORTANCE + + if strategy_type.lower() not in FEATURE_IMPORTANCE: + valid = list(FEATURE_IMPORTANCE.available()) + raise ValueError( + f"Invalid strategy_type: {strategy_type}. Valid types are: {valid}" + ) + return FEATURE_IMPORTANCE.create( + strategy_type, filter_unknown_kwargs=True, **kwargs + ) diff --git a/RiskLabAI/optimization/__init__.py b/RiskLabAI/optimization/__init__.py index f316602..c8effb2 100644 --- a/RiskLabAI/optimization/__init__.py +++ b/RiskLabAI/optimization/__init__.py @@ -18,7 +18,8 @@ recursive_bisection, ) from .hyper_parameter_tuning import ( - MyPipeline, + MyPipeline, # deprecated alias (removed in 2.1.0) + SampleWeightedPipeline, clf_hyper_fit, ) from .nco import ( @@ -41,6 +42,7 @@ # hedging.py "pca_weights", # hyper_parameter_tuning.py - "MyPipeline", + "SampleWeightedPipeline", + "MyPipeline", # deprecated alias (removed in 2.1.0) "clf_hyper_fit", ] diff --git a/RiskLabAI/optimization/hyper_parameter_tuning.py b/RiskLabAI/optimization/hyper_parameter_tuning.py index d797350..b7d9547 100644 --- a/RiskLabAI/optimization/hyper_parameter_tuning.py +++ b/RiskLabAI/optimization/hyper_parameter_tuning.py @@ -3,6 +3,7 @@ and the custom PurgedKFold cross-validators. """ +import warnings from typing import Any, Optional, Union import numpy as np @@ -11,11 +12,11 @@ from sklearn.model_selection import GridSearchCV, RandomizedSearchCV from sklearn.pipeline import Pipeline -# Import the controller from the refactored validation module -from RiskLabAI.backtest.validation import CrossValidatorController +# Cross-validators are created from the core registry (2.0.0 single source). +from RiskLabAI.core import CROSS_VALIDATORS -class MyPipeline(Pipeline): +class SampleWeightedPipeline(Pipeline): """ Custom pipeline class to correctly pass `sample_weight` to the final estimator's `fit` method. @@ -27,7 +28,7 @@ def fit( y: pd.Series, sample_weight: Optional[np.ndarray] = None, **fit_params, - ) -> "MyPipeline": + ) -> "SampleWeightedPipeline": """ Fit the pipeline, passing `sample_weight` to the final step. @@ -44,7 +45,7 @@ def fit( Returns ------- - MyPipeline + SampleWeightedPipeline The fitted pipeline. """ if sample_weight is not None: @@ -80,7 +81,7 @@ def clf_hyper_fit( times : pd.Series Series of event start and end times for purging. pipe_clf : Pipeline - The scikit-learn pipeline (or `MyPipeline`) to tune. + The scikit-learn pipeline (or `SampleWeightedPipeline`) to tune. param_grid : dict Parameter grid for the search. validator_type : str, default='purgedkfold' @@ -126,9 +127,9 @@ def clf_hyper_fit( validator_params["times"] = times # 1. Set up the custom cross-validator - inner_cv = CrossValidatorController( - validator_type, **validator_params - ).cross_validator + inner_cv = CROSS_VALIDATORS.create( + validator_type, filter_unknown_kwargs=True, **validator_params + ) # 2. Set up the hyperparameter search if rnd_search_iter == 0: @@ -157,7 +158,7 @@ def clf_hyper_fit( best_estimator = gs.best_estimator_ # Create a new pipeline with the best estimator's steps - bag_pipe = MyPipeline(best_estimator.steps) + bag_pipe = SampleWeightedPipeline(best_estimator.steps) bag_clf = BaggingClassifier( estimator=bag_pipe, @@ -175,3 +176,22 @@ def clf_hyper_fit( # 5. Return the best estimator found return gs.best_estimator_ + + +class MyPipeline(SampleWeightedPipeline): + """ + Deprecated alias for :class:`SampleWeightedPipeline` (removed in 2.1.0). + + Implemented as an explicit subclass rather than via ``deprecated_class`` so + that the scikit-learn ``__init__`` signature is preserved (estimators may + not use ``*args``/``**kwargs`` in their constructor). + """ + + def __init__(self, steps, *, memory=None, verbose=False): + warnings.warn( + "MyPipeline is deprecated and will be removed in 2.1.0; " + "use SampleWeightedPipeline instead.", + DeprecationWarning, + stacklevel=2, + ) + super().__init__(steps, memory=memory, verbose=verbose) diff --git a/RiskLabAI/pde/__init__.py b/RiskLabAI/pde/__init__.py index 8e943b1..b6a542e 100644 --- a/RiskLabAI/pde/__init__.py +++ b/RiskLabAI/pde/__init__.py @@ -42,7 +42,8 @@ ) from .solver import ( FBSDESolver, - FBSNNolver, # Note: Typo in original filename? + FBSNNolver, # deprecated alias (removed in 2.1.0) + FBSNNSolver, initialize_weights, ) @@ -69,5 +70,6 @@ # Solvers "initialize_weights", "FBSDESolver", - "FBSNNolver", + "FBSNNSolver", + "FBSNNolver", # deprecated alias (removed in 2.1.0) ] diff --git a/RiskLabAI/pde/solver.py b/RiskLabAI/pde/solver.py index 468e779..c28d596 100644 --- a/RiskLabAI/pde/solver.py +++ b/RiskLabAI/pde/solver.py @@ -9,6 +9,7 @@ import torch.autograd as autograd import torch.nn as nn +from RiskLabAI._deprecation import deprecated_class from RiskLabAI.pde.equation import Equation from RiskLabAI.pde.model import * @@ -243,7 +244,7 @@ def solve( return losses, inits -class FBSNNolver: +class FBSNNSolver: """ Solver for FBSNN (Forward-Backward Stochastic Neural Network). @@ -405,3 +406,7 @@ def solve( logger.info("Loss: %.4f, Y_0: %.4f", val_loss.item(), y0_mean) return losses, inits + + +# Deprecated alias for the misspelled original name; removed in 2.1.0. +FBSNNolver = deprecated_class(FBSNNSolver, "FBSNNolver", removed_in="2.1.0") diff --git a/RiskLabAI/utils/__init__.py b/RiskLabAI/utils/__init__.py index bf7c34a..315ca59 100644 --- a/RiskLabAI/utils/__init__.py +++ b/RiskLabAI/utils/__init__.py @@ -11,6 +11,8 @@ - Publication-quality Matplotlib plotting """ +from RiskLabAI._deprecation import warn_deprecated_name + from .constants import * from .ewma import ewma from .momentum_mean_reverting_strategy_sides import determine_strategy_side @@ -26,6 +28,16 @@ } +# Deprecated non-ASCII constant identifiers -> their ASCII replacements. +# Accessing these via RiskLabAI.utils warns and returns the new value; the +# identifiers are removed in 2.1.0 (string values are unchanged regardless). +_DEPRECATED_CONSTANTS = { + "CUMULATIVE_θ": "CUMULATIVE_THETA", + "CUMULATIVE_BUY_θ": "CUMULATIVE_BUY_THETA", + "CUMULATIVE_SELL_θ": "CUMULATIVE_SELL_THETA", +} + + def __getattr__(name): if name in _LAZY_PLOTTING: from importlib import import_module @@ -34,6 +46,10 @@ def __getattr__(name): value = getattr(import_module(f".{module_name}", __name__), attr) globals()[name] = value return value + if name in _DEPRECATED_CONSTANTS: + new = _DEPRECATED_CONSTANTS[name] + warn_deprecated_name(name, new, removed_in="2.1.0") + return globals()[new] raise AttributeError(f"module {__name__!r} has no attribute {name!r}") @@ -58,9 +74,9 @@ def __getattr__(name): "CUMULATIVE_VOLUME", "CUMULATIVE_BUY_VOLUME", "CUMULATIVE_SELL_VOLUME", - "CUMULATIVE_θ", - "CUMULATIVE_BUY_θ", - "CUMULATIVE_SELL_θ", + "CUMULATIVE_THETA", + "CUMULATIVE_BUY_THETA", + "CUMULATIVE_SELL_THETA", "EXPECTED_IMBALANCE", "EXPECTED_TICKS_NUMBER", "EXPECTED_BUY_IMBALANCE", diff --git a/RiskLabAI/utils/constants.py b/RiskLabAI/utils/constants.py index 1289705..b00492f 100644 --- a/RiskLabAI/utils/constants.py +++ b/RiskLabAI/utils/constants.py @@ -14,9 +14,17 @@ CUMULATIVE_BUY_VOLUME = "Cumulative Buy Volume" CUMULATIVE_SELL_VOLUME = "Cumulative Sell Volume" -CUMULATIVE_θ = "Cumulative θ" -CUMULATIVE_BUY_θ = "Cumulative Buy θ" -CUMULATIVE_SELL_θ = "Cumulative Sell θ" +CUMULATIVE_THETA = "Cumulative θ" +CUMULATIVE_BUY_THETA = "Cumulative Buy θ" +CUMULATIVE_SELL_THETA = "Cumulative Sell θ" + +# Deprecated non-ASCII aliases (removed in 2.1.0). The string *values* are +# unchanged, so stored data and internal dict keys are unaffected; only the +# Python identifier you import differs. These are intentionally excluded from +# ``__all__`` so ``RiskLabAI.utils`` can warn on access via its ``__getattr__``. +CUMULATIVE_θ = CUMULATIVE_THETA +CUMULATIVE_BUY_θ = CUMULATIVE_BUY_THETA +CUMULATIVE_SELL_θ = CUMULATIVE_SELL_THETA EXPECTED_IMBALANCE = "expected_imbalance" EXPECTED_TICKS_NUMBER = "exp_num_ticks" @@ -40,3 +48,40 @@ PREVIOUS_BARS_BUY_TICKS_PROPORTIONS_LIST = "List of previous bars buy ticks proportion" N_PREVIOUS_BARS_FOR_EXPECTED_N_TICKS_ESTIMATION = "Window size for E[T]" + +# Canonical public names. The deprecated non-ASCII θ aliases above are +# deliberately omitted so that `from .constants import *` does not bind them in +# RiskLabAI.utils (whose __getattr__ then warns on access). Removed in 2.1.0. +__all__ = [ + "DATE_TIME", + "TIMESTAMP", + "TICK_NUMBER", + "OPEN_PRICE", + "HIGH_PRICE", + "LOW_PRICE", + "CLOSE_PRICE", + "CUMULATIVE_TICKS", + "CUMULATIVE_DOLLAR", + "THRESHOLD", + "CUMULATIVE_VOLUME", + "CUMULATIVE_BUY_VOLUME", + "CUMULATIVE_SELL_VOLUME", + "CUMULATIVE_THETA", + "CUMULATIVE_BUY_THETA", + "CUMULATIVE_SELL_THETA", + "EXPECTED_IMBALANCE", + "EXPECTED_TICKS_NUMBER", + "EXPECTED_BUY_IMBALANCE", + "EXPECTED_SELL_IMBALANCE", + "EXPECTED_BUY_TICKS_PROPORTION", + "BUY_TICKS_NUMBER", + "N_TICKS_ON_BAR_FORMATION", + "PREVIOUS_TICK_RULE", + "EXPECTED_IMBALANCE_WINDOW", + "PREVIOUS_BARS_N_TICKS_LIST", + "PREVIOUS_TICK_IMBALANCES_LIST", + "PREVIOUS_TICK_IMBALANCES_BUY_LIST", + "PREVIOUS_TICK_IMBALANCES_SELL_LIST", + "PREVIOUS_BARS_BUY_TICKS_PROPORTIONS_LIST", + "N_PREVIOUS_BARS_FOR_EXPECTED_N_TICKS_ESTIMATION", +] diff --git a/pyproject.toml b/pyproject.toml index fe91058..4ae26ea 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "RiskLabAI" -version = "1.0.9" +version = "2.0.0" authors = [ { name = "RiskLab", email = "arian@risklab.ai" }, ] @@ -96,21 +96,12 @@ ignore = ["E501"] [tool.ruff.lint.per-file-ignores] # Package __init__ files exist to re-export their submodules' public API. "__init__.py" = ["F401", "F403", "F405"] -# The bars stack consumes the column-name constants via `from ...constants -# import *`. Replacing these with explicit imports is tracked for 2.0.0 -# (see NAMING_CANON_2.0.0.md, "Replace ... import * in the bars stack"). -"RiskLabAI/data/structures/abstract_bars.py" = ["F403", "F405"] -"RiskLabAI/data/structures/abstract_imbalance_bars.py" = ["F403", "F405"] -"RiskLabAI/data/structures/abstract_information_driven_bars.py" = ["F403", "F405"] -"RiskLabAI/data/structures/abstract_run_bars.py" = ["F403", "F405"] -"RiskLabAI/data/structures/imbalance_bars.py" = ["F403", "F405"] -"RiskLabAI/data/structures/run_bars.py" = ["F403", "F405"] +# pde/solver builds the networks from `from ...model import *`; the torch-only +# model module is intentionally left star-imported. "RiskLabAI/pde/solver.py" = ["F403", "F405"] # base.py re-exports the canonical interfaces lazily via module __getattr__, # so the names listed in __all__ are resolved on access, not at definition. "RiskLabAI/core/base.py" = ["F822"] -# Tests import the constants namespace wholesale for readability. -"test/data/structures/test_standard_bars.py" = ["F403", "F405"] # importorskip("torch") must execute before the torch-dependent imports below # it, so module-level imports cannot all sit at the top of the file. "test/pde/test_pde_solver.py" = ["E402"] diff --git a/test/backtest/test_bet_sizing.py b/test/backtest/test_bet_sizing.py index 7a1fdfc..6d258e1 100644 --- a/test/backtest/test_bet_sizing.py +++ b/test/backtest/test_bet_sizing.py @@ -4,22 +4,23 @@ import numpy as np import pandas as pd +import pytest from scipy.stats import norm from RiskLabAI.backtest.bet_sizing import ( - TPos, average_bet_sizes, - avgActiveSignals, - betSize, - getW, + avg_active_signals, + bet_size_sigmoid, + compute_sigmoid_width, probability_bet_size, strategy_bet_sizing, + target_position, ) def test_avg_active_signals(): """ - Regression test: avgActiveSignals used to silently return an empty + Regression test: avg_active_signals used to silently return an empty DataFrame because of a broken `mpPandasObj` import (the placeholder fallback always took over). Verifies real averaged values are returned. """ @@ -32,9 +33,9 @@ def test_avg_active_signals(): index=idx, ) - out = avgActiveSignals(signals, nThreads=1) + out = avg_active_signals(signals, n_threads=1) - assert len(out) > 0, "avgActiveSignals returned an empty result" + assert len(out) > 0, "avg_active_signals returned an empty result" out = out.sort_index() # At 2020-01-02: signals 1 (active, t1=01-03) and 2 (starts 01-02) -> (1.0+0.5)/2 assert np.isclose(out.loc[pd.Timestamp("2020-01-02")], 0.75) @@ -126,16 +127,40 @@ def test_desprado_bet_sizing_snippets(): """Test snippets 10.4 from de Prado.""" w = 1.0 x = 0.5 # divergence - m = betSize(w, x) + m = bet_size_sigmoid(w, x) # m = 0.5 / sqrt(1 + 0.25) = 0.5 / sqrt(1.25) = 0.4472 assert np.isclose(m, 0.44721359) - # Test getW - w_calc = getW(x, m) + # Test compute_sigmoid_width + w_calc = compute_sigmoid_width(x, m) assert np.isclose(w, w_calc) - # Test TPos - pos = TPos(w=1.0, f=10.5, acctualPrice=10.0, maximumPositionSize=100) + # Test target_position + pos = target_position(w=1.0, f=10.5, actual_price=10.0, maximum_position_size=100) # x = 0.5, m = 0.4472... # pos = int(0.4472 * 100) = 44 assert pos == 44 + + +def test_deprecated_bet_sizing_aliases_still_work_and_warn(): + """Every renamed bet_sizing function keeps a working, warning alias.""" + from RiskLabAI.backtest.bet_sizing import ( + TPos, + betSize, + getW, + ) + + w, x = 1.0, 0.5 + with pytest.warns(DeprecationWarning): + m = betSize(w, x) + assert np.isclose(m, bet_size_sigmoid(w, x)) + + with pytest.warns(DeprecationWarning): + w_calc = getW(x, m) + assert np.isclose(w_calc, compute_sigmoid_width(x, m)) + + # Parameter names were also snake_cased in 2.0.0, so the alias preserves + # positional calls (documented in the release notes). + with pytest.warns(DeprecationWarning): + pos = TPos(1.0, 10.5, 10.0, 100) + assert pos == target_position(1.0, 10.5, 10.0, 100) diff --git a/test/backtest/validation/test_cross_validator_controller.py b/test/backtest/validation/test_cross_validator_controller.py index 9df9727..306fe2d 100644 --- a/test/backtest/validation/test_cross_validator_controller.py +++ b/test/backtest/validation/test_cross_validator_controller.py @@ -51,3 +51,10 @@ def test_controller_public_attribute(dummy_args): validator_type="combinatorialpurged", **dummy_args ) assert isinstance(controller.cross_validator, CombinatorialPurged) + + +def test_controller_is_deprecated(): + """The controller is deprecated in 2.0.0 (delegates to the core registry).""" + with pytest.warns(DeprecationWarning, match="2.1.0"): + controller = CrossValidatorController(validator_type="kfold", n_splits=5) + assert isinstance(controller.get_validator(), KFold) diff --git a/test/backtest/validation/test_cross_validator_factory.py b/test/backtest/validation/test_cross_validator_factory.py index f4b7290..7008cd7 100644 --- a/test/backtest/validation/test_cross_validator_factory.py +++ b/test/backtest/validation/test_cross_validator_factory.py @@ -87,3 +87,10 @@ def test_factory_invalid_type(): with pytest.raises(ValueError) as exc_info: CrossValidatorFactory.create_cross_validator("invalid_type") assert "Invalid validator_type: invalid_type" in str(exc_info.value) + + +def test_factory_is_deprecated(): + """The factory is deprecated in 2.0.0 (delegates to the core registry).""" + with pytest.warns(DeprecationWarning, match="2.1.0"): + cv = CrossValidatorFactory.create_cross_validator("kfold", n_splits=5) + assert isinstance(cv, KFold) diff --git a/test/data/structures/test_standard_bars.py b/test/data/structures/test_standard_bars.py index 1564929..747e85e 100644 --- a/test/data/structures/test_standard_bars.py +++ b/test/data/structures/test_standard_bars.py @@ -6,7 +6,11 @@ import pytest from RiskLabAI.data.structures.standard_bars import StandardBars -from RiskLabAI.utils.constants import * +from RiskLabAI.utils.constants import ( + CUMULATIVE_DOLLAR, + CUMULATIVE_TICKS, + CUMULATIVE_VOLUME, +) @pytest.fixture diff --git a/test/features/feature_importance/test_feature_importance.py b/test/features/feature_importance/test_feature_importance.py index 597ad91..9d1c10e 100644 --- a/test/features/feature_importance/test_feature_importance.py +++ b/test/features/feature_importance/test_feature_importance.py @@ -42,6 +42,13 @@ def mock_data(): return X, y, classifier, clusters +def test_controller_is_deprecated(mock_data): + """The controller is deprecated in 2.0.0 (delegates to the core registry).""" + _, _, classifier, _ = mock_data + with pytest.warns(DeprecationWarning, match="2.1.0"): + FeatureImportanceController("MDI", classifier=classifier) + + def test_controller_mdi(mock_data): """Test MDI via the controller.""" X, y, classifier, _ = mock_data diff --git a/test/optimization/test_hyper_parameter_tuning.py b/test/optimization/test_hyper_parameter_tuning.py index 08a3b5c..a92955c 100644 --- a/test/optimization/test_hyper_parameter_tuning.py +++ b/test/optimization/test_hyper_parameter_tuning.py @@ -10,7 +10,11 @@ from sklearn.pipeline import Pipeline from sklearn.preprocessing import StandardScaler -from RiskLabAI.optimization.hyper_parameter_tuning import MyPipeline, clf_hyper_fit +from RiskLabAI.optimization.hyper_parameter_tuning import ( + MyPipeline, + SampleWeightedPipeline, + clf_hyper_fit, +) @pytest.fixture @@ -36,7 +40,7 @@ def fit(self, X, y, **kwargs): self.fit_kwargs = kwargs super().fit(X, y) - pipe = MyPipeline([("scaler", StandardScaler()), ("clf", MockLR())]) + pipe = SampleWeightedPipeline([("scaler", StandardScaler()), ("clf", MockLR())]) pipe.fit(X, y, sample_weight=sample_weight) @@ -46,11 +50,21 @@ def fit(self, X, y, **kwargs): ) +def test_my_pipeline_deprecated_alias_warns(): + """MyPipeline is a deprecated subclass of SampleWeightedPipeline.""" + assert issubclass(MyPipeline, SampleWeightedPipeline) + with pytest.warns(DeprecationWarning, match="MyPipeline.*2.1.0"): + pipe = MyPipeline([("clf", LogisticRegression())]) + assert isinstance(pipe, SampleWeightedPipeline) + + def test_clf_hyper_fit_gridsearch(mock_data): """Test the grid search functionality.""" X, y, times = mock_data - pipe_clf = MyPipeline([("scaler", StandardScaler()), ("clf", LogisticRegression())]) + pipe_clf = SampleWeightedPipeline( + [("scaler", StandardScaler()), ("clf", LogisticRegression())] + ) param_grid = {"clf__C": [0.1, 1.0]} @@ -77,7 +91,7 @@ def test_clf_hyper_fit_bagging(mock_data): """Test the bagging functionality.""" X, y, times = mock_data - pipe_clf = MyPipeline([("clf", LogisticRegression())]) + pipe_clf = SampleWeightedPipeline([("clf", LogisticRegression())]) param_grid = {"clf__C": [1.0]} validator_params = {"n_splits": 3} diff --git a/test/pde/test_pde_solver.py b/test/pde/test_pde_solver.py index 50a9791..93d057a 100644 --- a/test/pde/test_pde_solver.py +++ b/test/pde/test_pde_solver.py @@ -8,7 +8,13 @@ # Import the main components from RiskLabAI.pde.equation import HJBLQ -from RiskLabAI.pde.solver import FBSDESolver +from RiskLabAI.pde.solver import FBSDESolver, FBSNNolver, FBSNNSolver + + +def test_fbsnnolver_is_deprecated_alias(): + """FBSNNolver is a deprecated subclass of the renamed FBSNNSolver.""" + assert issubclass(FBSNNolver, FBSNNSolver) + assert FBSNNolver is not FBSNNSolver @pytest.fixture diff --git a/test/test_deprecation.py b/test/test_deprecation.py new file mode 100644 index 0000000..5e8ab67 --- /dev/null +++ b/test/test_deprecation.py @@ -0,0 +1,62 @@ +""" +Unit tests for the deprecation helpers in ``RiskLabAI._deprecation``. + +These guarantee the shim mechanism (used by the 2.0.0 naming canon) stays +intact: aliases keep working AND emit a ``DeprecationWarning`` naming the +replacement. +""" + +import pytest + +from RiskLabAI._deprecation import ( + deprecated_alias, + deprecated_class, + warn_deprecated_name, +) + + +def test_deprecated_alias_calls_through_and_warns(): + def new_func(a, b=2): + """Add two numbers.""" + return a + b + + old_func = deprecated_alias(new_func, "old_func", removed_in="2.1.0") + + with pytest.warns(DeprecationWarning, match="old_func.*2.1.0.*new_func"): + result = old_func(3, b=4) + + assert result == 7 + assert old_func.__name__ == "old_func" + + +def test_deprecated_class_is_subclass_and_warns_on_init(): + class New: + def __init__(self, x): + self.x = x + + Old = deprecated_class(New, "Old", removed_in="2.1.0") + + assert issubclass(Old, New) + assert Old is not New + + with pytest.warns(DeprecationWarning, match="Old.*2.1.0.*New"): + obj = Old(5) + + assert isinstance(obj, New) + assert obj.x == 5 + + +def test_warn_deprecated_name_emits_warning(): + with pytest.warns(DeprecationWarning, match="OLD_NAME.*2.1.0.*NEW_NAME"): + warn_deprecated_name("OLD_NAME", "NEW_NAME", removed_in="2.1.0") + + +def test_deprecated_theta_constant_alias_warns(): + """The non-ASCII θ constants warn (and return the ASCII value) via utils.""" + import RiskLabAI.utils as utils + + with pytest.warns(DeprecationWarning, match="2.1.0"): + value = utils.CUMULATIVE_θ + + # Identifier changed; the string value is intentionally unchanged. + assert value == utils.CUMULATIVE_THETA == "Cumulative θ" diff --git a/test/test_performance.py b/test/test_performance.py index ca6a207..11901e9 100644 --- a/test/test_performance.py +++ b/test/test_performance.py @@ -11,7 +11,7 @@ import numpy as np import pandas as pd -from RiskLabAI.backtest.bet_sizing import mpAvgActiveSignals +from RiskLabAI.backtest.bet_sizing import mp_avg_active_signals from RiskLabAI.data.differentiation.differentiation import ( calculate_weights_std, fractional_difference_std, @@ -94,7 +94,7 @@ def test_mp_avg_active_signals_matches_reference(): time_points = sorted(set(signals["t1"].dropna().values).union(signals.index.values)) - fast = mpAvgActiveSignals(signals, time_points) + fast = mp_avg_active_signals(signals, time_points) reference = _avg_active_reference(signals, time_points) assert np.allclose(fast.values, reference.values, atol=1e-12, equal_nan=True) @@ -105,7 +105,7 @@ def test_mp_avg_active_signals_empty_molecule(): {"t1": [pd.Timestamp("2020-01-02")], "signal": [1.0]}, index=[pd.Timestamp("2020-01-01")], ) - assert len(mpAvgActiveSignals(signals, [])) == 0 + assert len(mp_avg_active_signals(signals, [])) == 0 # --------------------------------------------------------------------------- #