feat(risk): pair-wise correlation exposure cap (PR-A11)#177
Merged
costajohnt merged 2 commits intomainfrom Apr 26, 2026
Merged
Conversation
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>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
The existing
check_sector_guardcaps 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:correlation_blocked={symbol: reason}threaded throughrun_candidate_filters.log_signal.--no-correlation-guardCLI flag opts out (default: on, threshold 0.7, 60-day window).tests/test_correlation.py(new): 16 tests.Behaviour
|r| >= threshold). A-0.85pair concentrates risk just like+0.85— short BEAR + long BULL is still a directional bet.lookback_daysargument if calling the function directly.Tests
🤖 Generated with Claude Code