Class PortfolioConstruction
double[] w = PortfolioConstruction.zScoreWeights(scores, 1.0, 0.05); w = PortfolioConstruction.sectorNeutralize(w, sectors); w = PortfolioConstruction.betaNeutralize(w, betas); w = PortfolioConstruction.inverseVolBudget(w, vols, 1.0);
All functions take and return weight arrays aligned with the
AlphaContext symbol order; NaN scores become zero weight. Inputs
are never mutated. Weights are fractions of equity (0.05 = 5%), signed
(short = negative), and each step documents what it preserves and what
it re-normalizes — neutralization steps change gross exposure, so gross
targeting is a final step or a re-application.
-
Nested Class Summary
Nested ClassesModifier and TypeClassDescriptionstatic final classA constructed book with what it actually achieved. -
Method Summary
Modifier and TypeMethodDescriptionstatic double[]betaNeutralize(double[] weights, double[] betas) Beta neutrality: removes the market-beta component by projecting the weight vector orthogonal to the beta vector,w − β · (w·β)/(β·β).static double[]capWeights(double[] weights, double maxWeight, double grossTarget) Caps each |weight| atmaxWeightand rescales the uncapped names so thatΣ|w|reachesgrossTarget, repeating until no further name breaches the cap (water-filling).static double[]inverseVolBudget(double[] weights, double[] vols, double grossTarget) Inverse-volatility risk budgeting: rescales each position by1/σᵢ(keeping its sign and relative signal strength), so every name contributes comparably to portfolio risk instead of the volatile names dominating — the first-order version of equal risk contribution, exact when correlations are equal.static double[]meanVarianceTilt(double[] alphas, double[][] covariance, double grossTarget) Unconstrained mean-variance tilt:w ∝ Σ⁻¹ α(the Markowitz solution up to scale), solved via Gaussian elimination and normalized togrossTarget.static double[]neutralize(double[] weights, double[][] constraints) Joint neutralisation: projectswonto the null space of all constraint rows at once, so thatΣ_i a_ki w_i = 0for every rowksimultaneously.static double[]sectorNeutralize(double[] weights, String[] sectors) Sector neutrality: demeans weights within each sector, so every sector's net weight is exactly zero and the book carries stock selection, not sector bets.static double[]sectorNeutralize(AlphaContext ctx, double[] weights, Map<String, String> sectorBySymbol) sectorNeutralize(double[], String[])with alignment by construction: sector labels come as a map keyed by symbol and are resolved against the context's frozen (sorted!)static double[][]sectorRows(String[] sectors) Sector dummy rows (one per distinct label) forneutralize(double[], double[][]).static double[]trailingBetas(AlphaContext ctx, int index, int lookback) Trailing OLS betas against a leave-one-out equal-weight market: for symbolithe market return is the mean of the other names' returns.static double[]trailingBetas(AlphaContext ctx, int index, int lookback, BarSeries market) Trailing OLS betas against an external market series aligned with the panel (same bar count and timestamps), for example an index.static double[]trailingVols(AlphaContext ctx, int index, int lookback) Trailing return volatilities per symbol atindex(sample sd of the close-to-close returns ending at barsindex−lookback+1 .. index), the standard input toinverseVolBudget(double[], double[], double).static PortfolioConstruction.BookzScoreBook(double[] scores, double grossTarget, double maxWeight) Z-score sizing: demean scores cross-sectionally, scale by their dispersion, clamp at ±3σ, then size the long and short sides separately togrossTarget/2each with an iterative water-filling cap atmaxWeight.static double[]zScoreWeights(double[] scores, double grossTarget) zScoreWeights(double[], double, double)without a per-name cap.static double[]zScoreWeights(double[] scores, double grossTarget, double maxWeight) The weights ofzScoreBook(double[], double, double).
-
Method Details
-
zScoreWeights
public static double[] zScoreWeights(double[] scores, double grossTarget) zScoreWeights(double[], double, double)without a per-name cap. -
zScoreWeights
public static double[] zScoreWeights(double[] scores, double grossTarget, double maxWeight) The weights ofzScoreBook(double[], double, double). -
zScoreBook
public static PortfolioConstruction.Book zScoreBook(double[] scores, double grossTarget, double maxWeight) Z-score sizing: demean scores cross-sectionally, scale by their dispersion, clamp at ±3σ, then size the long and short sides separately togrossTarget/2each with an iterative water-filling cap atmaxWeight.Sizing the sides separately keeps the book dollar-neutral after the clamp and the cap, both of which break
Σw = 0when applied to the whole vector. When neither binds the result equals plain proportional scaling of the z-scores. When one side cannot reachgrossTarget/2under the cap (fewer thangrossTarget/(2·maxWeight)names on it), both sides are sized to what the smaller side can carry, andPortfolioConstruction.Book.grossShortfall()says so: with the 5% default cap a book needs at least ten names per side to deploy gross 1.0. -
capWeights
public static double[] capWeights(double[] weights, double maxWeight, double grossTarget) Caps each |weight| atmaxWeightand rescales the uncapped names so thatΣ|w|reachesgrossTarget, repeating until no further name breaches the cap (water-filling). When the cap makes the target unreachable every non-zero name ends at the cap and the shortfall stays in cash. Signs are preserved; the net is not re-balanced (usezScoreBook(double[], double, double)for a dollar-neutral book). -
inverseVolBudget
public static double[] inverseVolBudget(double[] weights, double[] vols, double grossTarget) Inverse-volatility risk budgeting: rescales each position by1/σᵢ(keeping its sign and relative signal strength), so every name contributes comparably to portfolio risk instead of the volatile names dominating — the first-order version of equal risk contribution, exact when correlations are equal. Re-normalized togrossTarget.Related but different:
backtest.portfolio.PositionSizing. inverseVolatilityWeightsbuilds long-only weights from vols alone and silently equal-weights on a degenerate vol; this method rescales an existing signed book and throws on unusable vols — a flat (σ = 0) name inside a signal-weighted book is a data problem to surface, not to paper over. -
trailingVols
Trailing return volatilities per symbol atindex(sample sd of the close-to-close returns ending at barsindex−lookback+1 .. index), the standard input toinverseVolBudget(double[], double[], double). Synthetic (forward-filled) bars are skipped; NaN when fewer than two real bars remain. -
sectorNeutralize
public static double[] sectorNeutralize(AlphaContext ctx, double[] weights, Map<String, String> sectorBySymbol) sectorNeutralize(double[], String[])with alignment by construction: sector labels come as a map keyed by symbol and are resolved against the context's frozen (sorted!) symbol order —AlphaContext.ofre-sorts symbols, so a hand-built array in the caller's insertion order would silently demean against permuted labels. Symbols missing from the map keep their own singleton sector (i.e. they demean to zero). -
sectorNeutralize
Sector neutrality: demeans weights within each sector, so every sector's net weight is exactly zero and the book carries stock selection, not sector bets. Names with weight 0 stay 0 (they are not dragged in to fund their sector's offset). Gross exposure changes; re-target gross afterwards if it matters. Followed bybetaNeutralize(double[], double[])it is no longer sector-neutral: useneutralize(double[], double[][])for several constraints at once.- Parameters:
sectors- sector label per symbol, aligned with the weights — which followAlphaContext.symbols()order (sorted, not your input map's order); prefer thesectorNeutralize(AlphaContext, double[], java.util.Map)overload, which cannot misalign
-
betaNeutralize
public static double[] betaNeutralize(double[] weights, double[] betas) Beta neutrality: removes the market-beta component by projecting the weight vector orthogonal to the beta vector,w − β · (w·β)/(β·β). After the projectionΣ wᵢβᵢ = 0exactly and the book's P&L stops being a leveraged market bet. Gross changes; re-target afterwards if needed. -
trailingBetas
Trailing OLS betas against a leave-one-out equal-weight market: for symbolithe market return is the mean of the other names' returns. Including the name itself pulls every beta toward 1 on small panels (with N names the self-weight is 1/N). -
trailingBetas
Trailing OLS betas against an external market series aligned with the panel (same bar count and timestamps), for example an index. -
neutralize
public static double[] neutralize(double[] weights, double[][] constraints) Joint neutralisation: projectswonto the null space of all constraint rows at once, so thatΣ_i a_ki w_i = 0for every rowksimultaneously. ApplyingsectorNeutralize(AlphaContext, double[], Map)and thenbetaNeutralize(double[], double[])in sequence does not achieve this: the second projection undoes the first. Constraint rows are restricted to the book's support (names withw = 0stay at 0), orthonormalised by modified Gram-Schmidt, and rows that are linearly dependent on earlier ones (for example a dollar row next to a full set of sector dummies) are dropped. Gross changes; re-target afterwards.- Parameters:
constraints- constraint rows, each aligned withw(sector dummies, betas, a row of ones for dollar neutrality)
-
sectorRows
Sector dummy rows (one per distinct label) forneutralize(double[], double[][]). -
meanVarianceTilt
public static double[] meanVarianceTilt(double[] alphas, double[][] covariance, double grossTarget) Unconstrained mean-variance tilt:w ∝ Σ⁻¹ α(the Markowitz solution up to scale), solved via Gaussian elimination and normalized togrossTarget. Unlike z-score sizing this uses the correlation structure: two highly correlated names with the same alpha share one bet instead of doubling it. Feed a shrunk/regularized covariance — the raw sample matrix near-singular universe inverts into garbage, which is a data problem no solver fixes.- Parameters:
alphas- expected-return proxy per symbol (z-scored factor scores work); NaN = excludecovariance- per-bar return covariance, aligned both ways
-