Asset Filtering
Relevant source files
The following files were used as context for generating this wiki page:
Asset Filtering is a specialized module within the universal-portfolios library designed to enhance Online Portfolio Selection (OLPS) by dynamically excluding assets based on their past performance metrics. The primary objective is to improve a strategy's overall risk-adjusted returns by identifying and removing assets that negatively contribute to the portfolio's Sharpe ratio or other performance indicators.
This feature operates by intercepting the standard execution flow of portfolio algorithms (such as those defined in universal.algos). It utilizes statistical methods, including bootstrapping and Normal CDF evaluations, to determine which assets should be assigned zero weights during a specific trading period.
Sources: universal/asset_filters.py:1-93, universal/result.py:1-537
Architecture and Components
The filtering system is built around the AssetFilter class and a high-level filter_result utility. It integrates with the library's core results processing and algorithm execution logic.
The AssetFilter Class
The AssetFilter class serves as the core logic engine for determining asset exclusion. It calculates the impact of each asset on the portfolio's total Sharpe ratio and uses a statistical threshold to decide which assets to drop.
- Windowing: Allows filtering based on a specific look-back period of log returns.
- Thresholding: A configurable sensitivity parameter (default 0.3) used to evaluate the Cumulative Distribution Function (CDF) of Sharpe ratio differences.
- Statistical Analysis: Uses
scipy.stats.normto perform inference on bootstrapped samples.
Sources: universal/asset_filters.py:11-47, universal/tools.py:382-411
Data Flow Diagram
The following diagram illustrates how performance data is transformed into a set of excluded assets.
Dynamic Algorithm Patching
Asset filtering is implemented using a "monkey-patching" approach via the filter_result function. This allows the filter to be applied to any existing algorithm without modifying the algorithm's internal code.
Execution Sequence
- Initial Run: The algorithm is first run on the full dataset to generate a baseline
AlgoResult. - Step Interception: The
stepmethod of the algorithm is replaced with a wrapper that callsAssetFilter.fit. - Filtered Weights: During each time step, the filter identifies assets to remove, and the algorithm calculates weights for the remaining assets.
- Re-indexing: The filtered weights are re-aligned with the original asset index, filling removed assets with
0.0.
Key Configurations and Metrics
The asset filtering process relies on specific hyperparameters and statistical methods to ensure robustness.
| Parameter | Type | Default | Description |
|---|---|---|---|
window | int | None | The number of past periods to consider for filtering. If None, uses full history. |
threshold | float | 0.3 | The significance level for removing assets; lower values are more aggressive. |
SAMPLES | int | 50 | Number of bootstrap samples used to estimate the distribution of Sharpe differences. |
alpha | float | 0.00001 | Regularization parameter used in Sharpe ratio calculations. |
Sources: universal/asset_filters.py:12-23, universal/tools.py:537-562
Performance Evaluation
The results of filtering are encapsulated in the AlgoResult class, which provides metrics to compare the filtered strategy against the baseline. Relevant metrics include:
- Sharpe Ratio: Annualized log returns divided by volatility.
- Information Ratio: Performance relative to the uniform CRP benchmark.
- Turnover: The frequency of rebalancing, which may increase due to assets being toggled on/off by the filter.
Sources: universal/result.py:136-155, universal/result.py:207-228
Implementation Example
The following snippet demonstrates how AssetFilter is utilized within asset_filters.py to identify underperforming components:
def _filter(self, R):
SAMPLES = 50
sh = []
for _ in range(SAMPLES):
# get bootstrap sample
R_sample = R.sample(n=len(R), replace=True)
sh.append(
{col: tools.sharpe(R_sample[col], alpha=0.00001) for col in R_sample}
)
sh = pd.DataFrame(sh)
sh_diff = sh.subtract(sh["full"], 0)
cdf = stats.norm.cdf(
0.0, loc=sh_diff.mean(), scale=0.01 + sh_diff.std() / np.sqrt(len(sh_diff))
)
to_remove = sh_diff.columns[cdf < self.threshold]
return to_remove
Sources: universal/asset_filters.py:17-34
Summary
Asset Filtering provides a statistical wrapper for portfolio algorithms, allowing them to adapt to market conditions by pruning assets that fail to meet risk-adjusted performance thresholds. By combining bootstrapping with dynamic method patching, the module offers a flexible way to improve the performance of strategies like MPT or UP without requiring internal changes to their optimization logic.
Sources: universal/asset_filters.py:50-81, universal/algos/mpt.py:112-145