Buy And Hold (BAH)
Relevant source files
The following files were used as context for generating this wiki page:
The Buy and Hold (BAH) strategy is a fundamental benchmark algorithm within the Universal Portfolios project. It represents a passive investment strategy where an investor purchases a set of stocks at the beginning of a period and holds them until the end, without any further rebalancing or trading. In this implementation, the strategy typically starts with uniform weights across all assets unless specific initial weights are provided.
Within the project's ecosystem, BAH serves as a critical baseline for evaluating the performance of more complex Online Portfolio Selection (OLPS) algorithms like Modern Portfolio Theory (MPT) or Universal Portfolios (UP). It is categorized as a benchmark alongside other strategies such as Constant Rebalanced Portfolios (CRP).
Sources: README.md:36-40, universal/algos/bah.py:10-12
Architecture and Class Hierarchy
The BAH strategy is implemented as a class that inherits from the base Algo class. This inheritance ensures that BAH integrates seamlessly with the project's standardized execution and result-tracking pipeline.
Class Structure
The BAH class overrides specific attributes and methods to implement its passive logic:
PRICE_TYPE: Set to"raw". Unlike many OLPS algorithms that operate on price ratios, BAH relies on absolute price levels to determine how the portfolio's weights evolve naturally over time as asset prices change.__init__(self, b=None): Initializes the strategy with optional starting weightsb. IfbisNone, weights default to a uniform distribution.weights(self, S): Overrides the standard weight calculation logic. While the baseAlgoclass typically uses astepmethod for sequential updates,BAHimplements a vectorizedweightsfunction for optimal performance.
The diagram above shows the inheritance relationship where BAH specializes the generic Algo framework.
Sources: universal/algos/bah.py:10-20, universal/algo.py:20-40
Core Logic and Data Flow
The logic of BAH is distinct because it does not "decide" new weights at each time step based on performance. Instead, the weights at any time $t$ are a function of the initial weights and the cumulative price change of the assets.
Weight Calculation Process
- Initialization: If no weights are provided, it identifies assets in the input DataFrame. It assigns 0 to "CASH" and 1 to other assets, then normalizes to create a uniform distribution.
- Price Movement: The weights are calculated as the product of the previous day's prices (
S.shift(1)) and the initial weightsb. - Normalization: The resulting values are divided by the total portfolio value at that time step to ensure weights always sum to 1.
- First Step: The weights for the very first time step are set to a uniform distribution ($1/N$).
The flow diagram illustrates the vectorized calculation of weights over the entire timeseries.
Sources: universal/algos/bah.py:22-38
Integration with Result Analysis
Once the BAH weights are computed, the run method in the base class returns an AlgoResult object. This object handles the calculation of performance metrics.
| Component | Description |
|---|---|
AlgoResult | Computes wealth, fees, Sharpe ratio, and drawdowns. |
total_wealth | The final product of all period returns. |
| Fee Handling | In BAH, fees are typically zero because no rebalancing occurs after the initial purchase. |
| Benchmark | BAH is often used as a comparison point in ListResult plots. |
Technical Note on Fees
Testing confirms that the total wealth of a BAH strategy should remain largely unaffected by transaction fees after the initial entry, as there is no turnover. This is verified in the project's test suite where wealth_no_fees is compared against wealth_with_fees.
Sources: universal/result.py:30-45, tests/test_algos.py:46-55
Implementation Example
The BAH implementation is concise, focusing on the relationship between raw prices and the hold-forever mandate.
# From universal/algos/bah.py:22-38
def weights(self, S):
"""Weights function optimized for performance."""
if self.b is None:
b = np.array([0 if s == "CASH" else 1 for s in S.columns])
b = b / b.sum()
else:
b = self.b
# weights are proportional to price times initial weights
w = S.shift(1) * b
# normalize
w = w.div(w.sum(axis=1), axis=0)
w.iloc[0] = 1.0 / S.shape[1]
return w
Sources: universal/algos/bah.py:22-38
Summary of Configuration
| Parameter | Type | Default | Description |
|---|---|---|---|
b | np.array or list | None | Initial portfolio weights. If None, uses uniform distribution. |
PRICE_TYPE | str | "raw" | Internal setting to use absolute prices for weight evolution. |
Sources: universal/algos/bah.py:15-20
The Buy and Hold strategy serves as the simplest form of portfolio management in the universal-portfolios package. By utilizing raw price data and avoiding periodic rebalancing, it provides a stable "zero-action" baseline that highlights the value (or lack thereof) generated by active OLPS algorithms. Its implementation within the Algo framework ensures that it can be tested, benchmarked, and plotted using the same unified tools as complex strategies like MPT or Kelly betting.
Sources: universal/algos/bah.py, universal/algo.py, README.md