Skip to content

Membership Policy

Overview

The Membership Policy controls how assets enter and exit the portfolio during rebalancing events. It reduces portfolio churn and stabilizes holdings by applying configurable rules that override raw optimization results.

Purpose

Without membership control, portfolio optimization can lead to:

  • High turnover: Assets frequently entering/exiting increase transaction costs
  • Whipsaw trades: Assets bouncing in and out due to noisy signals
  • Tax inefficiency: Short holding periods trigger higher tax rates

Membership policy addresses these issues by applying deterministic rules to candidate portfolios.

Architecture

Core Components

from portfolio_management.portfolio import MembershipPolicy, apply_membership_policy

# Create policy
policy = MembershipPolicy(
    buffer_rank=5,              # Rank buffer for existing holdings
    min_holding_periods=3,      # Minimum rebalance periods to hold
    max_turnover=0.30,          # Maximum 30% turnover per rebalance
    max_new_assets=5,           # Maximum 5 new assets per rebalance
    max_removed_assets=3,       # Maximum 3 removals per rebalance
)

# Apply policy during rebalancing
filtered_assets = apply_membership_policy(
    candidate_assets=["AAPL", "MSFT", "GOOGL", "META", "TSLA"],
    current_holdings={"AAPL": 100, "AMZN": 150},
    holding_periods={"AAPL": 5, "AMZN": 2},
    policy=policy,
    candidate_rankings=pd.Series([1, 2, 3, 4, 5], index=["AAPL", "MSFT", ...]),
)

Policy Rules

The policy applies rules in a deterministic pipeline:

  1. Minimum Holding Periods (Step 1)

  2. Keep assets that haven't reached min_holding_periods

  3. Prevents premature exits
  4. Example: If min_holding_periods=3, asset must be held for at least 3 rebalances

  5. Buffer Rank Protection (Step 2)

  6. Give existing holdings a ranking advantage

  7. Keep holdings ranked within buffer_rank positions of cutoff
  8. Example: If buffer_rank=5 and portfolio size is 20, keep holdings ranked up to 25

  9. Max New Assets (Step 3)

  10. Limit number of new entries per rebalancing

  11. Prevents excessive portfolio changes
  12. Applied after removing candidates that violate other rules

  13. Max Removed Assets (Step 4)

  14. Limit number of exits per rebalancing

  15. Smooths portfolio transitions
  16. Keeps lowest-ranked removals if limit exceeded

Turnover Calculation

Turnover is calculated as:

turnover = (additions + removals) / portfolio_size

Where:

  • additions = number of new assets entering
  • removals = number of assets exiting
  • portfolio_size = number of current holdings

Example: If portfolio has 20 assets, adds 4, removes 2:

turnover = (4 + 2) / 20 = 0.30 (30%)

Usage

Command Line Interface

Enable membership policy via CLI flags:

# Basic usage with default policy
python scripts/run_backtest.py equal_weight \
    --membership-enabled \
    --start-date 2020-01-01 \
    --end-date 2023-12-31

# Custom policy parameters
python scripts/run_backtest.py risk_parity \
    --membership-enabled \
    --membership-buffer-rank 10 \
    --membership-min-hold 5 \
    --membership-max-turnover 0.25 \
    --membership-max-new 3 \
    --membership-max-removed 2

Available Flags

Flag Type Default Description
--membership-enabled flag False Enable membership policy
--membership-buffer-rank int 5 Rank buffer for existing holdings
--membership-min-hold int 3 Minimum holding periods
--membership-max-turnover float None Maximum turnover (0-1)
--membership-max-new int None Maximum new assets per rebalance
--membership-max-removed int None Maximum removals per rebalance

Programmatic Usage

from portfolio_management.portfolio import MembershipPolicy
from portfolio_management.backtesting import BacktestConfig, BacktestEngine

# Create policy
policy = MembershipPolicy(
    buffer_rank=5,
    min_holding_periods=3,
    max_turnover=0.30,
    max_new_assets=5,
    max_removed_assets=3,
)

# Add to backtest config
config = BacktestConfig(
    start_date=date(2020, 1, 1),
    end_date=date(2023, 12, 31),
    initial_capital=Decimal("100000"),
    rebalance_frequency=RebalanceFrequency.MONTHLY,
    membership_policy=policy,  # Add policy here
)

# Run backtest
engine = BacktestEngine(config=config, strategy=strategy, prices=prices, returns=returns)
equity_curve, metrics, events = engine.run()

Factory Methods

Default Policy - Balanced settings for moderate turnover control:

policy = MembershipPolicy.default()
# Equivalent to:
# MembershipPolicy(buffer_rank=5, min_holding_periods=3)

Disabled Policy - No membership restrictions:

policy = MembershipPolicy.disabled()
# Equivalent to:
# MembershipPolicy(buffer_rank=0, min_holding_periods=0)

Configuration in Universe YAML

Note: Universe YAML support is planned but not yet implemented.

Future syntax:

universes:
  my_universe:
    assets: [AAPL, MSFT, GOOGL, ...]
    membership_policy:
      buffer_rank: 5
      min_holding_periods: 3
      max_turnover: 0.30
      max_new_assets: 5
      max_removed_assets: 3

Design Decisions

Why Deterministic Pipeline?

The policy applies rules in a fixed order to ensure reproducible results. Alternative approaches (scoring, optimization) would add complexity and unpredictability.

Why Buffer Rank Instead of Absolute Threshold?

Buffer rank adapts to portfolio size and prevents cliff effects. An asset ranked 21 (just outside top 20) shouldn't be immediately removed if it was previously held.

Why Track Holding Periods?

Short holding periods often result from noise rather than signal. Forcing minimum holds reduces transaction costs and tax impacts.

Integration with Preselection

Membership policy requires ranked candidates, which depends on the Preselection feature (Issue #31). Until preselection is implemented:

  • Rankings can be derived from optimization weights
  • Simple rank-by-weight approach serves as placeholder
  • Full preselection will enable more sophisticated ranking strategies

Performance Impact

Membership policy has minimal computational overhead:

  • Time complexity: O(n) where n = number of candidates
  • Space complexity: O(n) for tracking holding periods
  • Typical execution: \<1ms per rebalance event

Testing

Comprehensive test coverage (45 tests) including extensive edge case validation:

# Run all membership policy tests
pytest tests/portfolio/test_membership.py -v

# Run specific edge case test categories
pytest tests/portfolio/test_membership.py::TestBufferZoneEdgeCases -v      # 7 tests
pytest tests/portfolio/test_membership.py::TestBoundaryConditions -v       # 6 tests
pytest tests/portfolio/test_membership.py::TestPolicyConstraintConflicts -v # 7 tests
pytest tests/portfolio/test_membership.py::TestSpecialScenarios -v         # 6 tests

Test Categories

Basic Functionality (19 tests):

  • Policy validation and creation
  • Basic policy application
  • Standard constraint enforcement

Buffer Zone Edge Cases (7 tests):

  • Assets entering buffer zone (ranks 31-50 with top_k=30, buffer=50)
  • Assets exiting buffer zone
  • Assets oscillating around buffer boundary
  • Multiple assets in buffer zone
  • Empty buffer zone scenarios
  • Buffer disabled scenarios

Boundary Conditions (6 tests):

  • All current holdings failing criteria
  • All holdings protected by min_holding_periods
  • Single asset portfolio
  • Exact boundary values (rank = top_k, rank = buffer_rank)
  • Equal rank scenarios (ties)

Policy Constraint Conflicts (7 tests):

  • min_holding_periods vs max_removed_assets conflict
  • max_new_assets vs top_k conflict
  • Buffer keeping more than top_k
  • Zero constraint values
  • Multiple policies at limits simultaneously

Special Scenarios (6 tests):

  • Missing assets in preselected_ranks (delisted assets)
  • All new candidates worse than holdings
  • Large buffer (buffer > universe size)
  • Multiple complex constraint interactions

See EDGE_CASE_TESTS_SUMMARY.md and TESTING_MEMBERSHIP_EDGE_CASES.md for detailed test documentation.

Limitations

  1. Requires Preselection: Currently depends on optimization weights for ranking
  2. No Universe YAML Support: Configuration must be done via CLI or programmatically
  3. No Backward Compatibility: Existing backtests will need explicit policy configuration

Future Enhancements

  • [ ] Universe YAML configuration support (Issue #35)
  • [ ] Integration with Preselection module (Issue #31)
  • [ ] Adaptive buffer rank based on market volatility
  • [ ] Time-decay for holding period requirements
  • [ ] Asset-specific membership rules (e.g., by sector)

See Also

  • Issue #35: Membership Policy Implementation
  • Issue #31: Preselection Strategy (dependency)
  • src/portfolio_management/portfolio/membership.py - Implementation
  • tests/portfolio/test_membership.py - Test suite