Class PortfolioConstruction

java.lang.Object
com.quantfinlib.alpha.PortfolioConstruction

public final class PortfolioConstruction extends Object
Turns raw factor scores into tradeable weight vectors, as a chain of small, composable, pure functions so a construction pipeline reads as what it does:
  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 Classes
    Modifier and Type
    Class
    Description
    static final class 
    A constructed book with what it actually achieved.
  • Method Summary

    Modifier and Type
    Method
    Description
    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·β)/(β·β).
    static double[]
    capWeights(double[] weights, double maxWeight, double grossTarget)
    Caps each |weight| at maxWeight and rescales the uncapped names so that Σ|w| reaches grossTarget, 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 by 1/σᵢ (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 to grossTarget.
    static double[]
    neutralize(double[] weights, double[][] constraints)
    Joint neutralisation: projects w onto the null space of all constraint rows at once, so that Σ_i a_ki w_i = 0 for every row k simultaneously.
    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) for neutralize(double[], double[][]).
    static double[]
    trailingBetas(AlphaContext ctx, int index, int lookback)
    Trailing OLS betas against a leave-one-out equal-weight market: for symbol i the 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 at index (sample sd of the close-to-close returns ending at bars index−lookback+1 .. index), the standard input to inverseVolBudget(double[], double[], double).
    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 to grossTarget/2 each with an iterative water-filling cap at maxWeight.
    static double[]
    zScoreWeights(double[] scores, double grossTarget)
    static double[]
    zScoreWeights(double[] scores, double grossTarget, double maxWeight)

    Methods inherited from class Object

    clone, equals, finalize, getClass, hashCode, notify, notifyAll, toString, wait, wait, wait
  • Method Details

    • zScoreWeights

      public static double[] zScoreWeights(double[] scores, double grossTarget)
    • zScoreWeights

      public static double[] zScoreWeights(double[] scores, double grossTarget, double maxWeight)
    • 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 to grossTarget/2 each with an iterative water-filling cap at maxWeight.

      Sizing the sides separately keeps the book dollar-neutral after the clamp and the cap, both of which break Σw = 0 when applied to the whole vector. When neither binds the result equals plain proportional scaling of the z-scores. When one side cannot reach grossTarget/2 under the cap (fewer than grossTarget/(2·maxWeight) names on it), both sides are sized to what the smaller side can carry, and PortfolioConstruction.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| at maxWeight and rescales the uncapped names so that Σ|w| reaches grossTarget, 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 (use zScoreBook(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 by 1/σᵢ (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 to grossTarget.

      Related but different: backtest.portfolio.PositionSizing. inverseVolatilityWeights builds 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

      public static double[] trailingVols(AlphaContext ctx, int index, int lookback)
      Trailing return volatilities per symbol at index (sample sd of the close-to-close returns ending at bars index−lookback+1 .. index), the standard input to inverseVolBudget(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.of re-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

      public 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. 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 by betaNeutralize(double[], double[]) it is no longer sector-neutral: use neutralize(double[], double[][]) for several constraints at once.
      Parameters:
      sectors - sector label per symbol, aligned with the weights — which follow AlphaContext.symbols() order (sorted, not your input map's order); prefer the sectorNeutralize(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ᵢβᵢ = 0 exactly and the book's P&L stops being a leveraged market bet. Gross changes; re-target afterwards if needed.
    • trailingBetas

      public static double[] trailingBetas(AlphaContext ctx, int index, int lookback)
      Trailing OLS betas against a leave-one-out equal-weight market: for symbol i the 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

      public 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.
    • neutralize

      public static double[] neutralize(double[] weights, double[][] constraints)
      Joint neutralisation: projects w onto the null space of all constraint rows at once, so that Σ_i a_ki w_i = 0 for every row k simultaneously. Applying sectorNeutralize(AlphaContext, double[], Map) and then betaNeutralize(double[], double[]) in sequence does not achieve this: the second projection undoes the first. Constraint rows are restricted to the book's support (names with w = 0 stay 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 with w (sector dummies, betas, a row of ones for dollar neutrality)
    • sectorRows

      public static double[][] sectorRows(String[] sectors)
      Sector dummy rows (one per distinct label) for neutralize(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 to grossTarget. 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 = exclude
      covariance - per-bar return covariance, aligned both ways