Skip to content

feat(risk): pair-wise correlation exposure cap (PR-A11)#177

Merged
costajohnt merged 2 commits intomainfrom
feat/correlation-exposure-cap
Apr 26, 2026
Merged

feat(risk): pair-wise correlation exposure cap (PR-A11)#177
costajohnt merged 2 commits intomainfrom
feat/correlation-exposure-cap

Conversation

@costajohnt
Copy link
Copy Markdown
Owner

Summary

The existing check_sector_guard caps positions per static GICS-style label, but two tickers in different sectors can still move together over short windows (AAPL/MSFT often correlate ~0.85). This PR adds a return-correlation guard that blocks candidates whose daily-return correlation with an already-held position is >= 0.7 |r|.

Pattern adapted (re-implemented, no code copy) from virattt/ai-hedge-fund's risk_manager.py.

Files

  • strategies/correlation.py (new): compute_position_correlations, too_correlated_pairs, candidates_blocked_by_correlation. Pulls daily closes via Alpaca, computes Pearson on returns. Defensive on fetch / import / partial-data failures.
  • scripts/orchestrator.py:
    • One-shot precompute after candidates are known.
    • correlation_blocked={symbol: reason} threaded through run_candidate_filters.
    • Filter checks right after the sector guard. Veto logged + audit-trail recorded via log_signal.
    • --no-correlation-guard CLI flag opts out (default: on, threshold 0.7, 60-day window).
  • tests/test_correlation.py (new): 16 tests.

Behaviour

  • Default: enabled, threshold 0.7, 60-day lookback.
  • On Alpaca fetch failure: WARNING logged, blocked set empty → sector guard remains the safety floor. No false positives that would block all candidates.
  • Anti-correlation: counts (|r| >= threshold). A -0.85 pair concentrates risk just like +0.85 — short BEAR + long BULL is still a directional bet.
  • Mid-window changes: lookback is 60 trading days; correlations during regime shifts may lag — operator can shorten via the lookback_days argument if calling the function directly.

Tests

$ uv run python -m pytest tests/test_correlation.py -q
16 passed

$ uv run python -m pytest tests/ -q
2816 passed (was 2800; +16)
Total coverage: 95.52%
Per-file coverage floors OK (69 files; default 85%)

🤖 Generated with Claude Code

costajohnt and others added 2 commits April 25, 2026 19:31
Pattern adapted (re-implemented, no code copy) from
virattt/ai-hedge-fund's risk_manager.py. The existing sector guard
caps positions per static GICS-style label, but two tickers in
*different* sectors can still move together over short windows
(AAPL/MSFT often correlate ~0.85). When a candidate's daily-return
correlation with an already-held position is >= 0.7 |r|, the new
guard blocks it.

New strategies/correlation.py:
  - compute_position_correlations(symbols, data_client, lookback_days=60)
  - too_correlated_pairs(corr, threshold=0.7)
  - candidates_blocked_by_correlation(candidates, held, corr, threshold)

Wire-in: scripts/orchestrator.py:
  - One-shot precompute after candidates are known.
  - correlation_blocked={symbol: reason} threaded through
    run_candidate_filters, checked right after the sector guard.
  - Veto reason logged at INFO and recorded via log_signal.
  - --no-correlation-guard CLI flag opts out (default: on).

Defensive: any failure in the correlation fetch / matrix build is
logged at WARNING and returns an empty blocked set — sector guard
remains the safety floor.

Tests: 16 new in test_correlation.py covering threshold semantics,
diagonal exclusion, NaN handling, anti-correlation, fetch-failure
no-op, and integration with synthetic correlated price series.

Coverage: 95.52% total, all per-file floors pass.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Three reviewer pickups:

1. Replace per-symbol StockBarsRequest loop with a single batched
   request (alpaca-py accepts symbol_or_symbols=List[str]). For ~13
   symbols this is 1 API call vs 13 — meaningful for momentum_trade
   where candidate count can run higher. Test mocks updated to
   produce a multi-index combined DataFrame matching the batch
   response shape.

2. Threshold and lookback are now constants
   (CORRELATION_THRESHOLD=0.7, CORRELATION_LOOKBACK_DAYS=60) in
   strategies/constants.py rather than hardcoded in orchestrator.py.
   Aligns with the project's "tunable parameters in constants.py"
   convention; agent can adjust if it ever decides to.

3. Added two TestRunCandidateFilters cases — the correlation_blocked
   SKIP branch was untested; now covered:
     - test_veto_correlation_blocked: symbol in blocked dict triggers
       early veto with the right log_signal payload.
     - test_no_correlation_block_when_dict_empty: empty dict is
       no-op; sentiment/fundamentals still run.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
@costajohnt costajohnt merged commit b6aad34 into main Apr 26, 2026
7 checks passed
@costajohnt costajohnt deleted the feat/correlation-exposure-cap branch April 26, 2026 02:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant