Skip to content

portfolio_balancing: trace the risk-return frontier with solver shadow prices#83

Draft
chriscoey wants to merge 2 commits into
mainfrom
coey-portfolio-sensitivity-frontier
Draft

portfolio_balancing: trace the risk-return frontier with solver shadow prices#83
chriscoey wants to merge 2 commits into
mainfrom
coey-portfolio-sensitivity-frontier

Conversation

@chriscoey

Copy link
Copy Markdown
Member

Why

The portfolio_balancing template walks the risk/return efficient frontier. It previously sampled the frontier blindly — evenly spaced return targets, each an independent solve, with no signal about where the curve actually bends. Now that the prescriptive solver returns sensitivity information, the dual of each return-floor constraint is the local slope of the frontier (envelope theorem: shadow_price = d(min variance) / d(return target)). That turns frontier tracing from guesswork into a guided search, and gives the template a concrete, teachable use of solver duals.

What changed

Stage 3 now solves with sensitivity=True and uses the per-scenario shadow prices to trace the frontier three ways, compared at the same solve budget:

  • grid — evenly spaced returns (baseline, blind to the curve's shape)
  • adaptive — each step sized by the current shadow price, so points land evenly in variance space
  • dichotomic — repeatedly split the interval with the largest chord-to-tangent gap

The run prints an approximation-quality table (max chord-gap per driver — dichotomic is tightest at equal budget), a shadow-price-vs-secant table showing each dual matches the finite-difference slope, and the existing base-vs-crisis stress test (per-scenario frontier with exact dual marginals and the knee point).

Supporting changes:

  • Solver switched from Ipopt to HiGHS — HiGHS returns sensitivity duals, and the QP is convex (PSD-preserving covariance).
  • SDK pin bumped to 1.9.0 (sensitivity support), matching the other sensitivity templates.
  • README rewritten: shadow-price sign-convention callout, how to read the three new tables, refreshed expected-output block, and updated troubleshooting.

How to validate

Run python portfolio_balancing.py against a prescriptive-capable engine (sensitivity support required). The README's expected-output block matches a fresh run end-to-end:

  • driver gaps: grid 557.93 > adaptive 415.17 > dichotomic 202.30
  • shadow-price table: duals bracket the finite-difference secants and rise monotonically
  • base/crisis volatility table: crisis vol exceeds base at every point, gap peaking mid-frontier

I ran this end-to-end on a current engine; output is numerically stable across runs.

chriscoey and others added 2 commits June 8, 2026 14:15
…w prices

Extend the prescriptive stage to use solver sensitivity information (constraint
duals) to trace the bi-objective risk vs return efficient frontier efficiently.

- Read the return-floor constraint's dual via solve(sensitivity=True) on HiGHS.
  By the envelope theorem that dual is the frontier's local slope
  d(variance)/d(return), so each solve yields a Pareto point and its slope with
  no finite differencing.
- Add three frontier drivers compared at equal solve budget: grid (blind even
  spacing), adaptive (step sized by the current shadow price), and dichotomic
  (split the interval with the largest chord-vs-tangent gap, sampling at the
  tangent crossover). The dual-guided drivers approximate the frontier far more
  tightly than blind spacing.
- Materialize the chosen frontier as a FrontierPoint Concept with integrity
  constraints requiring that neither return nor risk decreases along it, a
  relational statement of Pareto-efficiency (non-decreasing with a small
  relative tolerance so near-equal crisis-scenario points do not spuriously
  trip it).
- Switch the QP from Ipopt to HiGHS (HiGHS returns the sensitivity duals the
  frontier search relies on); bump relationalai to 1.9.0.
- Rewrite the README for the new flow: shadow-price sign convention, the
  three-driver comparison, the max chord-gap quality metric, and refreshed
  expected output.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
… docs

A solve can report OPTIMAL yet return no primal rows (e.g. an unstable or
timed-out reasoner handing back a hollow result). Treat that as a non-result
in solve_epsilon so the frontier drivers fall back through the same path as
infeasible, instead of KeyError-ing on the missing "scenario" column in
_extract_allocations. Expand the solve_at SystemExit message to name this case.

Also clarify surrounding comments and docs: the module-scope rules-in-loop
suppression, POSITION_LIMIT feasibility note, the LOCALLY_SOLVED back-end note,
the pair_gap tangent-crossover docstring, the frontier_adaptive anchors-only
guard, and the README's Anchor 2 per-scenario output and infeasibility note.

Co-Authored-By: Claude Opus 4.8 <[email protected]>
@github-actions

github-actions Bot commented Jun 8, 2026

Copy link
Copy Markdown

The docs preview for this pull request has been deployed to Vercel!

✅ Preview: https://relationalai-docs-2tryymy5d-relationalai.vercel.app/build/templates
🔍 Inspect: https://vercel.com/relationalai/relationalai-docs/FubBbFRixyLxHzMA7tXRxmbnp37K

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant