When to Use
Use this skill in multi-strategy hedge fund platforms or multi-pod quantitative trading architectures to monitor PnL correlations across active strategies (e.g., Statistical Arbitrage, Trend Following, Options Volatility, Alt-Data Equity). Sub-strategy pods that appear independent during normal markets often converge during market stress, exhibiting high PnL correlation, and the diversification the book was sized on disappears before the drawdown shows up in PnL. This module computes a pairwise PnL correlation matrix — equally weighted over an explicit rolling window, or EWMA-weighted toward the latest observation for live recomputation — flags converged and redundant pairs, flags the portfolio when the average inter-pod correlation breaches its own threshold, computes the Portfolio Diversification Ratio ($DR = \frac{\sum w_i \sigma_i}{\sigma_{\text{portfolio}}}$, Choueifaty & Coignard 2008), and emits a shrunken, invertible covariance matrix for a downstream optimizer.
When NOT to Use
- As a stand-alone capital control. The report is an input to an allocation or de-risking process, not a limit engine. Pair it with
multi-strategy-capital-allocation-limitsandcorrelation-aware-exposure-limitsfor enforcement, andstrategy-level-kill-switch-vs-portfolio-level-kill-switchfor automated cuts. - As a tail-risk model. Pearson correlation — equal- or EWMA-weighted — is a linear, full-distribution measure; pods that decouple in normal markets and converge only in the left tail will show a benign $\rho$ right up to the drawdown. Use
tail-correlation-between-strategies-under-stressfor lower-tail dependence. - For cross-asset regime analysis. This engine correlates strategy PnL, not asset classes. Use
cross-asset-correlation-regime-shiftsfor regime shifts in the underlying markets. - With short-only or market-neutral weights. The Diversification Ratio is defined for long-only allocations; $DR \ge 1$ does not hold once a capital weight is negative, so the engine rejects negative weights. (Pod strategies may of course be short the market — this constrains capital allocation weights, not positions.)
- Without threshold calibration. 0.70 / 0.85 / 0.55 / 1.20 are internal policy defaults, not standards. Calibrate against the empirical distribution of your own pods' $\rho$ before automating capital cuts.
- On unsynchronized or gross PnL series. Misaligned timestamps and un-stripped market beta both produce correlations that describe the data pipeline, not the strategies.
Prerequisites
- Synchronized daily or hourly PnL return series for all active sub-strategies ($S_1, S_2, \dots, S_M$), $M \ge 2$, sharing one timestamp index, oldest row first.
- No non-finite values and no zero-variance (flat/stale/idle) pod column — the engine rejects both rather than imputing them.
- At least
min_observationsrows (default 30) in the evaluated window. The hard floor is 3; below that every off-diagonal correlation is algebraically $\pm 1$ regardless of the data. - Strategy capital allocation weights ($w_i \ge 0$); omit them for equal weighting. Weights are normalized to sum to 1.
- For live recomputation:
ewma_span$\ge 2$ (pandas convention $\alpha = 2/(\text{span}+1)$). For an optimizer feed: a shrinkage intensity $\delta \in [0, 1]$.
Workflow
- PnL Return Ingestion: Ingest PnL return matrix $R_{N, M}$ ($N$ timestamps, $M$ strategies), oldest first. Column order must match
strategy_names— the engine can verify the count, not the identity. - Window and Validation Gate: Set
lookback_window=Wto evaluate only the trailing $W$ rows (the rolling window); leave itNoneto window upstream. A history shorter than $W$ is not an error — it is used in full so a warming-up system still reports, provided it clearsmin_observations; checkobservations_usedon the report rather than assuming $W$ rows were available. The window is rejected before estimation if it is shorter thanmin_observations, holds non-finite values, or contains a zero-variance column. Decision point: an idle pod that traded nothing this window is a scope decision (exclude it), never an imputation — filling it with zeros reports a dead feed as a perfect diversifier. - Weighting Choice: leave
ewma_span=Nonefor the equally weighted rolling-window estimate (a stable, explicit "last $W$ observations" view), or set an integer span for EWMA weighting, $w_t \propto (1-\alpha)^{T-t}$ with $\alpha = 2/(\text{span}+1)$, when the point is to catch a live convergence within a handful of observations. Decision point: the span is a statistical-power knob, not a smoothing preference — it caps the effective sample size at roughly the span no matter how deep the history is. Readeffective_observations($N$ under equal weighting; the Kish size $1/\sum_t w_t^2$ under EWMA), never the row count: 10,000 rows at span 60 is a 60-observation estimator. - Correlation Matrix Computation: compute the covariance $S$ over the weighted window (the EWMA form is the debiased weighted second moment $S = \frac{1}{1 - \sum_t w_t^2}\sum_t w_t (x_t - \mu_w)(x_t - \mu_w)^\top$), then $C = D^{-1/2} S D^{-1/2}$ with $D = \operatorname{diag}(S)$. This unshrunk matrix is what every threshold is applied to.
- Correlation Pairwise Breach Audit (thresholds inclusive, one-sided, compared before rounding):
- $\rho_{i,j} \ge 0.70 \implies$
HIGH_CORRELATION. - $\rho_{i,j} \ge 0.85 \implies$
REDUNDANT_POD(takes precedence). - Negative $\rho$ is never a breach — it is diversification. Offsetting pods are a capital-efficiency question, out of scope here.
- $\rho_{i,j} \ge 0.70 \implies$
- Average-Correlation Breakdown: compute $\bar{\rho}$ as the mean of the $M(M-1)/2$ unique off-diagonal entries and flag the portfolio at $\bar{\rho} \ge$
max_avg_correlation_threshold(default 0.55). $\bar{\rho}$ is signed, so a $+0.9$ pair and a $-0.9$ pair average to zero — it is reported alongside the pair breaches, never instead of them. - Diversification Ratio (DR) Calculation:
- $DR = \frac{\sum_{i=1}^M w_i \sigma_i}{\sqrt{w^T \Sigma w}}$, with $\sigma$ and $\Sigma$ both taken from the same weighted estimate as the correlations.
- $DR = 1.0 \implies$ zero diversification benefit; $DR \ge 1$ always holds for $w \ge 0$; $M$ orthogonal equal-volatility pods at equal weight give exactly $DR = \sqrt{M}$.
- A perfectly offsetting portfolio (portfolio variance exactly zero) returns $DR = \infty$ — the limit of the ratio, and a signal to verify the inputs, not a healthy portfolio.
- Shrunken Covariance for Optimizers: $\hat{\Sigma} = \delta \operatorname{diag}(S) + (1-\delta) S$ — a fixed-intensity linear shrinkage toward the diagonal (zero-correlation) target, positive definite for $\delta > 0$, so it stays invertible even when the pod count exceeds the observation count and $S$ is singular. Decision point: never threshold this matrix. A diagonal target leaves variances untouched and scales every off-diagonal correlation by exactly $(1-\delta)$, so a 0.70 alert applied to it would only fire at a true $\rho \ge 0.70/(1-\delta) = 0.824$ at $\delta = 0.15$ — a silent, systematic under-report of the breakdown this engine exists to catch.
- Capital Re-Allocation Alert: every pair breach, any $\bar{\rho}$ breach and any DR shortfall is emitted in
recommendations; the three conditions are reported independently, so none can mask another.is_diversification_healthyis false if any of them fires.
Full procedure: see
references/workflows.md. Standards reference: seereferences/standards.md. Printable pre-flight checklist: seeassets/checklist.md.
Common Pitfalls
- Relying on Static Historical PnL Correlations: Using 3-year static PnL correlations that hide real-time correlation spikes during market sell-offs. Set
lookback_windowexplicitly, or switch toewma_span, rather than passing whatever history is on hand. - Thresholding a Shrunken Correlation Matrix: shrinkage toward a diagonal target multiplies every off-diagonal $\rho$ by $(1-\delta)$. Comparing 0.70 against that deflated number silently raises the real trigger to $0.70/(1-\delta)$, so a pair genuinely at $\rho = 0.80$ reports 0.68 and never alerts. Threshold the correlation estimate; shrink only the covariance you hand to an optimizer.
- Calling a Fixed Intensity "Ledoit-Wolf": the defining contribution of Ledoit-Wolf (and of Schäfer-Strimmer) is a shrinkage intensity estimated from the data, together with a specific target — single-index, constant-correlation, or scaled identity. A hard-coded $\delta$ against a diagonal target is neither, and inherits none of their optimality results. See
references/standards.md. - Shrinking Toward the Raw Identity Matrix: $\delta I + (1-\delta) S$ is dimensionally wrong for return data. Daily pod variances are on the order of $10^{-4}$, so a unit-variance target dominates the diagonal and collapses every correlation toward zero. If an identity target is wanted, it must be scaled, $\frac{\operatorname{tr}(S)}{M} I$.
- Imputing a Stale Pod: a flat PnL column has an undefined correlation to everything. Substituting 0.0 makes a dead or idle pod the portfolio's best diversifier, inflates $DR$ and drags $\bar{\rho}$ below its threshold. The engine raises instead; never bypass it by pre-filling zeros.
- Reading the Row Count as the Sample Size: under EWMA weighting the effective sample size is capped at roughly
ewma_spanregardless of history depth. Correlation estimates carry sampling error of roughly $1/\sqrt{N-3}$ on the Fisher-$z$ scale (Fisher, 1921) — at an effective $N=12$ a true $\rho = 0.4$ crosses 0.70 by chance regularly. Sizemin_observationsand the span to your tolerance instead of trusting the row count. - Reading $\bar{\rho}$ Alone: the average is signed, so a $+0.9$ pair and a $-0.9$ pair average to zero. Always read
high_correlation_breachesalongside it — that is why the health flag ORs the conditions. - Multiple-Testing Across Pairs: $M$ pods produce $M(M-1)/2$ simultaneous tests; at $M=10$ that is 45 pairs, so isolated threshold crossings are expected even under independence. Require persistence (N consecutive windows) before cutting capital.
- Ignoring Equal-Weighted Capital Fallacies: Assuming equal capital allocations guarantee zero PnL correlation across pods.
- Neglecting Un-hedged Factor Contamination: Failing to strip market beta or factor exposures before computing strategy PnL correlations — otherwise the monitor mostly rediscovers shared beta.
- Reading $DR$ as a Tail Metric: $DR$ is built from a covariance matrix and says nothing about co-movement in the left tail; a portfolio can hold $DR > 1.5$ and still have every pod lose together in a crisis.
Verification
- Build three pods from mutually orthogonal, zero-mean sign vectors (exact $\rho = 0$, equal variance) at equal weight: verify $DR = \sqrt{3} \approx 1.7321$, no breaches,
is_diversification_healthytrue. - Blend two orthogonal equal-variance columns as $z = \rho u + \sqrt{1-\rho^2} v$ with $\rho = 0.90$: verify the reported correlation is exactly 0.90 and severity is
REDUNDANT_POD. - Re-weight the orthogonal case to $(0.5, 0.25, 0.25)$: verify $DR = 1/\sqrt{0.375} \approx 1.6330$.
- Feed a pod with $\rho = 0.99$ against another so $DR < 1.20$: verify both the
REDUNDANT PODSand theDiversification Ratiorecommendations appear. - With $\rho = 0.60$ on a single pair and the DR floor relaxed: verify no pair breach fires but
AVERAGE CORRELATION BREACHdoes, and the portfolio is reported unhealthy. With columns $(u, u, -u)$: verify $\bar{\rho}$ nets below 0.55 while the converged pair still breaches. - Cross-check the EWMA matrix against
pandas.DataFrame.ewm(span=S).cov()— the two must agree to $10^{-10}$ relative. Build a pair with $\rho = 0.75$ exactly (Gram-Schmidt under the EWMA weights) and run it at $\delta = 0.30$: the reported correlation must be 0.75, not $0.70 \times 0.75 = 0.525$, and the 0.70 alert must fire. This is the regression test for the alert-deflation defect. - Confirm $\operatorname{diag}(\hat{\Sigma}) = \operatorname{diag}(S)$ and that $\hat{\Sigma}$'s implied off-diagonal correlation is $(1-\delta)\rho$; with 6 pods over 4 observations, confirm the unshrunk estimate is rank-deficient while $\hat{\Sigma}$ has strictly positive eigenvalues.
- With 5,000 rows at
ewma_span=60, confirmeffective_observations$\approx 60$; withewma_span=None, confirm it equals the row count. Confirm a fast span recovers a $\rho = 1$ tail regime that a very slow span still reports below 0.50. - Feed a flat (zero-variance) pod, a NaN, or a 2-row window: the engine must raise, not report a healthy portfolio or a redundant pair.
- Run
python -m unittest discover -s skills/cross-strategy-correlation-monitoring/scripts.