# D14-F04-A01 — Turnover-Constrained Optimization

> Author: Islam Baraka  
> Domain: D14 — Portfolio Construction  
> Family: D14-F04 — Practical Constraints  
> Status: canonical article-ready topic package; visitor publication is a separate gate

## What this package freezes

This package implements one explicit contract: a one-rebalance, long-only, normalized Markowitz allocation with a hard one-way turnover limit measured from validated current holdings.

Given expected simple returns `μ`, covariance `Σ`, risk aversion `λ ≥ 0`, current weights `w⁰`, and one-way turnover cap `τ ≥ 0`, solve

```text
maximize    μᵀw − λ wᵀΣw
subject to  w ≥ 0
            1ᵀw = 1
            ½ ||w − w⁰||₁ ≤ τ
```

The reported turnover is always recomputed directly as `½ Σᵢ |wᵢ−w⁰ᵢ|`. MathWorks’ `Portfolio.setTurnover` method documentation uses the same half-L1 “average turnover” convention; this package still defines its own units and validation. It is not the number of assets traded, final-holdings cardinality, a transaction-cost amount, or an absolute percentage change with a changing denominator.

The package deliberately does not infer a rebalance from prices, quantities, corporate actions, dividends, or FX. Those upstream states must already be validated and converted to the same base-currency valuation time.

## Reader question and learning objectives

How can an optimizer move toward a risk/return objective without trading more than a declared amount from the current portfolio?

After working through the package, a reader should be able to:

- distinguish one-way turnover from the two-way L1 norm, trade count, and final support;
- solve the general-`N` convex contract and inspect a solved fixture rather than only a two-asset picture;
- reject an invalid current portfolio, indefinite covariance, infeasible iterate, or unproved solver status;
- explain why split variables are useful for linear modeling but are not automatically equal to the direct absolute turnover when slack is present.

## Scope and nearby variants

The canonical scope is one valuation date, one rebalance, long-only normalized holdings, a fixed covariance estimate, and a declared per-rebalance turnover cap. `λ` is an objective-unit risk-aversion coefficient; it is not a universal investor preference or a calibrated probability.

The following are deliberately separate variants:

| Variant | Difference | Route |
|---|---|---|
| Turnover penalty | Adds an L1 term to the objective instead of imposing a hard cap | Nearby comparison; not equivalent for every target |
| Dollar turnover | Applies the same geometry to dollar holdings after explicit base-wealth conversion | Nearby implementation variant |
| Transaction-cost-aware allocation | Charges a cost schedule and reconciles cash | D14-F04-A02 |
| Trade cardinality | Limits nonzero `w−w⁰` coordinates | D14-F04-A03 comparison; never rename as final support |
| Final-holdings cardinality | Limits nonzero post-trade `w` coordinates | D14-F04-A03 canonical distinction |
| Taxes, dividends, FX, corporate actions | Stateful upstream accounting and policy | D14-F04-A05/data-contract adapters |

No hidden re-normalization, post-solve rounding, transaction cost, tax, borrow, leverage, margin, or dynamic multi-period schedule is included here. A one-way cap is not a claim about liquidity, market impact, tax consequence, or execution probability.

## Input contract

| Field | Shape/type | Units and validation |
|---|---|---|
| `mu` | finite real vector, length `N` | decimal simple return for the declared period; asset order is canonical |
| `covariance` | finite `N×N` matrix | squared-return units; symmetric PSD within scale-aware tolerance |
| `lambdaRisk` | finite real scalar | nonnegative objective coefficient |
| `w0` | finite real vector, length `N` | nonnegative current normalized holdings; sum must already be one within `1e-12`; no silent normalization |
| `tau` | finite real scalar | nonnegative one-way turnover cap in normalized portfolio units |

Input rows with missing values, nonfinite values, mismatched asset order/dimension, negative current weights, materially non-unit current sum, negative risk aversion, negative cap, asymmetric covariance, or indefinite covariance fail before optimization. The result exposes `inputCurrentSum` so a tolerated floating-point residue is not hidden.

### Upstream provenance boundary

The implementation accepts a validated weight state, not a historical market-data claim. The author-confirmed prior library includes split-invalid prices, suspect quantity imports, changing weights, and cross-currency holdings normalized to USD. Those are data-contract reasons to fail upstream validation; they are not synthetic observations used as this optimizer’s proof. A split must reconcile units and basis as applicable; a current mark is not an acquisition basis; FX quote direction/time must be explicit; and a cash dividend entitlement/receipt is not automatically a price return. The prior `1010.SR` anecdote has no verified date or basis and is not used here.

### Synthetic split/input-repair diagnostic

For a controlled lineage fixture, asset A has `10 × 120 = 1200`, then a
four-for-one split gives `40 × 30 = 1200`; asset B is marked at `800`. The
validated current weights are `[.60,.40]`. If a stale quantity of `10` is
combined with the split-adjusted mark `30`, the diagnostic values become
`[300,800]` and the invalid current weights become `[3/11,8/11]`. Holding the
same objective `mu=[.08,.04]`, `Sigma=diag(.04,.01)`, `lambdaRisk=1`, and
`tau=.10`, the reconciled branch returns `[.60,.40]` with turnover zero; the
invalid branch is cap-limited at `[41/110,69/110]` with turnover `.10`. A full
return to `[.60,.40]` from the invalid snapshot would require `18/55`, which
exceeds the cap. This is a synthetic invalid branch for the adapter contract,
not a historical account or security claim.

## Output contract

Successful output includes:

| Field | Meaning |
|---|---|
| `status` | `optimal` only when normalized projected-KKT residual and independent Frank–Wolfe gap satisfy tolerance; otherwise `max-iterations` |
| `weights` | returned post-trade normalized holdings in the input order |
| `objective` | `μᵀw−λwᵀΣw` in original objective units |
| `directTurnover` | direct `½||w−w⁰||₁` recomputation |
| `inputCurrentSum` | sum of the caller’s `w0`, before any operation; no hidden renormalization |
| `simplexResidual`, `minWeight` | feasibility diagnostics |
| `iterations` | projected-gradient iterations actually used |
| `frankWolfeGap` | original-unit independent linearization gap upper bound |
| `certificate` | scale, step, projected residual, normalized gap, tolerance, and certificate interpretation |

`max-iterations` is a useful feasible incumbent only; it is not an optimality claim. Validation and numerical exceptions carry a stable `code` such as `invalid-input`, `indefinite-covariance`, `feasibility-residual`, or `numerical-failure`. No numeric infinity is serialized.

## Mathematical and numerical contract

For PSD `Σ`, the objective is concave. The hard turnover set is convex because it is the intersection of the simplex and an L1 ball centered at `w⁰`. The solver uses deterministic projected-gradient ascent for general `N`:

1. Set `g(w)=μ−2λΣw`.
2. Use a conservative matrix-row-sum upper bound for the quadratic Lipschitz term and an objective scale based on the supplied mean/risk magnitudes. The step denominator is `max(L, objectiveScale)`, so a `1e−10` linear objective is not treated as zero by an absolute unit step.
3. Project `w + step*g(w)` onto the simplex ∩ L1 ball. The projection solves the simplex projection directly when the cap is inactive; otherwise it uses a scalar L1 proximal penalty with nested bisection. It never clips and then silently renormalizes.
4. Recompute direct turnover, simplex residual, and nonnegativity at the returned point.
5. Independently maximize the current gradient over the fixed feasible set by moving up to `τ` from the lowest-gradient current holdings into the highest-gradient receivers, skipping nonpositive gains. The resulting Frank–Wolfe gap is

   ```text
   gap(w) = max_{s in feasible set} g(w)ᵀ(s−w).
   ```

   For a concave objective, this original-unit gap upper-bounds the objective suboptimality. The package reports both this gap and its normalized value. `optimal` requires the normalized projected residual, normalized gap, and normalized objective change to be within the declared tolerance. This is a global certificate for the declared concave-QP contract; a timeout/max-iteration result is not.

Covariance PSD validation uses a converged scale-aware symmetric Jacobi eigen check. A negative eigenvalue is compared with `1e−10 × max(abs(matrix entries))`, rather than an absolute floor that would accept an indefinite matrix at a small numerical scale.

## Independently checkable fixtures

All fixtures are synthetic and stored in [`tests/fixtures/a01-turnover-fixtures.json`](./tests/fixtures/a01-turnover-fixtures.json).

### Canonical two-asset optimization oracle

```text
w⁰ = [0.60, 0.40]
μ  = [0.10, 0.04]
Σ  = diag(0.04, 0.01)
λ  = 1
τ  = 0.10
```

Writing `w=[x,1−x]`, the unconstrained stationary point is `x=0.8`; the turnover cap restricts `x∈[0.5,0.7]`. The exact constrained optimum is:

```text
w* = [0.70, 0.30]
objective = 0.0615
directTurnover = 0.10
```

This is an optimization oracle, not merely a feasibility example. A second equal-risk-covariance probe uses `μ=[0.10,0.05]` and returns the same boundary weights with objective `0.0618`.

### General-`N` fixture

```text
μ  = [0.09, 0.06, 0.03]
Σ  = [[0.04,0.005,0], [0.005,0.02,0.002], [0,0.002,0.01]]
λ  = 2
w⁰ = [0.50,0.30,0.20]
τ  = 0.12
```

The solver returns a three-asset optimum with direct turnover below the cap and a finite certificate. The test intentionally checks the general dimension, residuals, status, and Python/TypeScript parity rather than replacing the solver with a two-asset grid.

### Scale and boundary fixtures

- `τ=0` returns `w=w⁰` exactly within the input representation and reports zero direct turnover.
- `μ=[1e−10,0]`, `Σ=0`, `λ=0`, `w⁰=[.5,.5]`, `τ=.5` returns `[1,0]`. The normalized gap gate prevents the tiny objective from being mislabeled constant.
- A non-PSD random 20×20 covariance and a tiny-scale indefinite matrix both fail `indefinite-covariance`.
- Negative cap, non-unit current weights, malformed dimensions, and nonfinite parameters fail structured validation.

The split-variable diagnostic is intentionally separate from the solver fixture: for `w⁰=[.60,.40]`, `w=[.65,.35]`, the direct turnover is `.05`, while a permitted nonminimal `u⁺=[.07,.02]`, `u⁻=[.02,.07]` reports auxiliary half-sum `.09`. The API must not call `.09` the turnover without proving tightness.

## Lab brief

The guided lab should start with `w⁰`, show the L1 feasible diamond/simplex intersection, and move the target return/risk preference until the hard turnover boundary binds. It should display:

- current versus final weights and signed trades;
- direct one-way turnover, the cap, and the simplex residual;
- objective, solver status, normalized KKT residual, and original/normalized Frank–Wolfe gap;
- a deliberately nonminimal split-variable example that is rejected as a reported turnover;
- an invalid current sum, indefinite covariance, and max-iteration status as visible structured failures.

The lab must not show turnover as a trade count, a tax amount, or a transaction-cost dollar value. Any later cash-cost overlay belongs to A02 and must reconcile a separate budget.

## Acceptance tests

The paired Python/TypeScript suites under `tests/` must verify:

1. canonical and equal-risk two-asset optimal weights, objective, and cap boundary;
2. zero-turnover parity and a general three-asset result;
3. tiny-objective scaling, scale-aware PSD rejection, and direct turnover recomputation;
4. all invalid-input and indefinite-covariance codes;
5. nonminimal auxiliary turnover is not reported as direct turnover;
6. a one-iteration result is `max-iterations`, never `optimal`;
7. normalized stationarity and Frank–Wolfe gap gates, while preserving original-unit diagnostics;
8. Python/TypeScript weights, objective, turnover, status, and normalized gap agree within the shared tolerance.

## Commands

From this topic folder:

```powershell
python -X utf8 -B -m unittest discover -s tests/python -p 'test_*.py' -v
$env:A01_PYTHON_CLI_PATH = (Resolve-Path implementations/python/turnover_constrained_cli.py).Path
npx --yes tsx tests/typescript/turnover-constrained.test.ts
```

The TypeScript test invokes `turnover_constrained_cli.py` for direct parity. `npx` fetches the test runner if it is not installed locally; the package implementation itself has no runtime dependency on it.

## Evidence and historical-example decision

Primary mathematical and implementation references are recorded in [`REFERENCES.md`](./REFERENCES.md) and the family evidence record. The Google ledger is a partial, locale/date-specific discovery record; it is not treated as mathematical proof. No historical market example is included in this package. Real split/FX/dividend/current-weight cases remain upstream data-contract acceptance rows, not invented historical observations.

## Related catalog topics

- D14-F04-A02 — Transaction-Cost-Aware Optimization: adds explicit cost and cash horizon; do not fold it into this hard turnover constraint.
- D14-F04-A03 — Cardinality-Constrained Portfolio: final support binaries versus trade count.
- D14-F04-A05 — Tax-Aware Portfolio Optimization: lot basis/policy state and tax cash semantics.
- D14-F03-A03 — Robust Mean-Variance: parameter uncertainty, not a turnover constraint.
