Skip to content

feat: opt-in no-forecast method= (min_variance / risk_parity / max_diversification) (fixes #7)#26

Merged
manan-tech merged 1 commit into
mainfrom
feat/7-no-forecast-method
Jul 1, 2026
Merged

feat: opt-in no-forecast method= (min_variance / risk_parity / max_diversification) (fixes #7)#26
manan-tech merged 1 commit into
mainfrom
feat/7-no-forecast-method

Conversation

@manan-tech

@manan-tech manan-tech commented Jul 1, 2026

Copy link
Copy Markdown
Owner

Closes #7.

What

Adds an opt-in method= to recommend_weights / recommend / walk_forward_recommendation selecting the allocator:

  • "optimize" (default) — objective-based, uses expected returns (current behavior)
  • "min_variance", "risk_parity", "max_diversification"no-forecast: allocate from the covariance alone, sidestepping the fragile expected-return estimate (TODO.md §1b: the mean is the dominant source of optimizer error)

Dispatched via a shared _optimize_weights helper, reusing the already-shipped engines (optimize_risk_parity, optimize_max_diversification, and objective_min_variance via the SLSQP path). Composes with cov_method= from #6.

The rationale plainly states the no-forecast assumption; evidence records method and a uses_return_forecast flag.

Default = "optimize" (deliberate)

The issue suggested considering a no-forecast default. I kept "optimize" as the default so the change is byte-for-byte behavior-preserving under the 2.x API/output-stability promise (a test asserts recommend_weights(prices) equals method="optimize" weight-for-weight). Flipping the default is a good candidate for a major version — easy to change if you'd prefer.

Constraint handling (documented)

min_variance honors bounds/constraints/profile via the shared SLSQP path. risk_parity and max_diversification use their own risk-based allocation and do not honor those constraints — documented in the docstring.

OOS comparison (acceptance criterion)

Walk-forward on the synthetic GBM fixture (AAA/BBB/CCC, ~5y, 1y window / 3m step, 15 windows):

method in-sample OOS Sharpe degradation
optimize (forecast) 0.783 0.460 41.2%
min_variance 0.272 0.305 0.0%
risk_parity 0.400 0.493 0.0%
max_diversification 0.397 0.492 0.0%

Even on synthetic GBM the thesis shows through: the forecast-based optimizer inflates in-sample (0.78) then degrades 41% out-of-sample, while the no-forecast allocators don't overfit — risk_parity/max_diversification reach a higher OOS Sharpe (~0.49) than the optimizer's OOS (0.46) with ~0% degradation. (Reproduced offline; not committed.)

Tests

test/test_recommend_method.py — default==optimize (behavior-preserving), each no-forecast method valid + flagged uses_return_forecast=False, rationale states the assumption, composes with cov_method, invalid method raises ValueError. Full suite: 423 passed, coverage 87.0%; ruff check src/ clean.

🤖 Generated with Claude Code

https://claude.ai/code/session_011a6EibcjDDv3Ff1Fb5esS1

Summary by Sourcery

Add an opt-in allocation method switch to the recommendation pipeline to support no-forecast allocators while preserving existing default behavior.

New Features:

  • Introduce a method parameter on recommend_weights, recommend, and walk_forward_recommendation to choose between objective-based optimization and no-forecast allocators (min_variance, risk_parity, max_diversification).

Enhancements:

  • Route allocation logic through a shared _optimize_weights helper and record the chosen method plus a uses_return_forecast flag in recommendation evidence and rationale.
  • Update recommendation and walk-forward docstrings and changelog to document allocator behavior, defaults, and constraint-handling nuances of each method.

Tests:

  • Add tests covering default vs explicit method behavior, validity and metadata of no-forecast allocators, interaction with robust covariance estimators, and error handling for invalid methods.

…versification)

recommend_weights, recommend, and walk_forward_recommendation gain an opt-in
method= — "optimize" (default, objective-based, uses expected returns) or the
no-forecast allocators min_variance / risk_parity / max_diversification that
allocate from the covariance alone, sidestepping the fragile expected-return
estimate (the dominant source of optimizer error). Dispatched via a shared
_optimize_weights helper. The default reproduces prior behavior; the chosen
method and a uses_return_forecast flag are recorded in evidence, and the
rationale states the no-forecast assumption plainly. risk_parity and
max_diversification use their own risk-based allocation and do not honor
bounds/constraints/profile (documented); min_variance honors them via the
shared SLSQP path.

Closes #7.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_011a6EibcjDDv3Ff1Fb5esS1
@sourcery-ai

sourcery-ai Bot commented Jul 1, 2026

Copy link
Copy Markdown

Reviewer's Guide

Adds an opt-in allocation method switch to the recommendation path, introducing no-forecast allocators (min_variance, risk_parity, max_diversification) wired through a shared helper, while preserving default behavior and enriching rationale/evidence metadata and tests.

Sequence diagram for allocation method selection in recommend

sequenceDiagram
    actor User
    participant recommend
    participant recommend_weights
    participant _optimize_weights
    participant find_optimal_portfolio
    participant optimize_risk_parity
    participant optimize_max_diversification

    User->>recommend: recommend(prices, method)
    recommend->>recommend_weights: recommend_weights(prices, method)
    recommend_weights->>_optimize_weights: _optimize_weights(method, objective, mean, cov, bounds, constraints, risk_free_rate)

    alt method == optimize
        _optimize_weights->>find_optimal_portfolio: find_optimal_portfolio(objective, mean, cov, bounds, constraints, risk_free_rate)
    else method == min_variance
        _optimize_weights->>find_optimal_portfolio: find_optimal_portfolio(objective_min_variance, mean, cov, bounds, constraints, risk_free_rate)
    else method == risk_parity
        _optimize_weights->>optimize_risk_parity: optimize_risk_parity(cov)
    else method == max_diversification
        _optimize_weights->>optimize_max_diversification: optimize_max_diversification(cov)
    else [unknown method]
        _optimize_weights-->>recommend_weights: ValueError
    end

    _optimize_weights-->>recommend_weights: weights
    recommend_weights-->>recommend: RecommendedWeights(weights, objective, rationale, evidence)
    recommend-->>User: Recommendation(target_weights, trades, alerts, verdict, validation)
Loading

File-Level Changes

Change Details Files
Introduce allocator method switch with shared optimization helper and no-forecast methods.
  • Add NO_FORECAST_METHODS/METHODS constants and _optimize_weights helper to route between optimize, min_variance, risk_parity, and max_diversification paths.
  • Wire min_variance to objective_min_variance via existing SLSQP-based find_optimal_portfolio; wire risk_parity and max_diversification to optimize_risk_parity and optimize_max_diversification using covariance only.
  • Validate method values and raise ValueError with the accepted set when an unknown method is provided.
src/quant_reporter/recommendation.py
Extend recommendation APIs to accept method parameter and propagate it through recommendation and walk-forward validation.
  • Add method="optimize" keyword argument to recommend_weights, recommend, and walk_forward_recommendation with docstring updates emphasizing default behavior preservation.
  • Update recommend to pass method through to recommend_weights and walk_forward_recommendation so validation uses the same allocator as the recommendation.
  • Update walk_forward_recommendation internal helpers (_rec, _cur) to call _optimize_weights instead of find_optimal_portfolio so that walk-forward simulations honor method.
src/quant_reporter/recommendation.py
Enhance rationale and evidence metadata to reflect allocator method and no-forecast stance.
  • Detect no-forecast methods and generate a rationale string that explicitly states returns are not forecast and allocation is covariance-only.
  • Include method, uses_return_forecast flag, and method-appropriate objective name in evidence while preserving existing stats fields.
  • Ensure RecommendedWeights.objective reflects either the objective name (optimize) or method name (no-forecast methods).
src/quant_reporter/recommendation.py
Document the new method switch and its constraint behavior in the changelog.
  • Describe the new method= switch, default behavior, recording of method/uses_return_forecast in evidence, and constraint-handling differences between methods in the Unreleased Added section.
CHANGELOG.md
Add tests validating default behavior preservation and correctness of no-forecast methods.
  • Create synthetic-price helper wrapper and weight-validity checker for tests.
  • Assert default recommend_weights(prices) matches method="optimize" output weight-for-weight and that uses_return_forecast is True.
  • Parametrize tests over min_variance/risk_parity/max_diversification to ensure valid weights, correct evidence flags, rationale explicitly stating the no-forecast assumption, composition with cov_method="ledoit_wolf", and ValueError on invalid method values.
test/test_recommend_method.py

Assessment against linked issues

Issue Objective Addressed Explanation
#7 Add a documented method= switch on recommend_weights that selects the no-forecast engines (min_variance, risk_parity, max_diversification) instead of relying solely on max-Sharpe / expected-return optimization.
#7 Provide a walk-forward comparison versus the baseline (forecast-based optimizer) and report the results in the PR.
#7 Update rationale and evidence text to explicitly state the no-forecast assumption and record it in the recommendation metadata.

Possibly linked issues


Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

@sourcery-ai sourcery-ai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've left some high level feedback:

  • For risk_parity and max_diversification, consider explicitly warning or erroring when bounds/constraints/profile are supplied, since they’re currently silently ignored and that may surprise callers.
  • The RecommendedWeights.objective field now carries method for no-forecast paths; if consumers expect this to represent the objective function, consider adding a separate allocator/method field on the dataclass and keeping objective strictly tied to the optimization objective.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- For `risk_parity` and `max_diversification`, consider explicitly warning or erroring when `bounds`/`constraints`/`profile` are supplied, since they’re currently silently ignored and that may surprise callers.
- The `RecommendedWeights.objective` field now carries `method` for no-forecast paths; if consumers expect this to represent the objective function, consider adding a separate `allocator`/`method` field on the dataclass and keeping `objective` strictly tied to the optimization objective.

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

@manan-tech manan-tech merged commit 66c7da4 into main Jul 1, 2026
10 checks passed
@manan-tech manan-tech deleted the feat/7-no-forecast-method branch July 1, 2026 10:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Add no-forecast default path (min_variance / risk_parity / max_diversification) to recommend_weights

1 participant