Model Register

One entry per model: who owns it, what it assumes, what validates it, what monitors it, and when it was last reviewed. Shaped after OSFI Guideline E-23 and kept honest by a test — tests/parity/test_model_register.py compares every constant here against the engine's source and fails the build when they disagree, so this page cannot quietly drift from the code it describes.

It records what is not true, because that is the only reason a register is worth having. Independent validation, in the E-23 sense of a validator separate from the developer, is not available here: this is a single-developer product, and where a model is checked it is checked by an independent oracle — a test written from the statute or the stated law rather than from the implementation — never by an independent person.

Provenance is graded, not asserted. An earlier version of this page carried one free-text “source” field, and ten entries had prose beside them. A reader could not tell a citation from an excuse — one entry's “source” read “no citation”, and another pointed at a second constant that was itself ungrounded. Those ten are now graded, and six of them do not survive.
external source a named third party a reviewer can look up · derived / governed traceable to governed config · recorded decision a human choice with a rationale and a date — justified under E-23
NOT a source text that explains what the constant does, not why its value is that value · nothing written down
Only the first three count as justified. The figure below is computed from the grades, not from how many entries have words next to them.
14
models registered
67 / 75
assumptions with any justification at all
9 / 14
not monitored against outcomes
2
entry points with no law-stating gate

register version 2.0.0 · generated 2026-09-23 · grades: external 6 · derived 1 · decision 60 · mechanism 6 · none 2

Fair value (DCF + ensemble)

fair_value · owner vishalsinha · version 2.8.0 · last reviewed 2026-09-24 · valuation/models/dcf.py

Monitoring

NOT MONITORED on a schedule valuation/validation.confidence_calibration computes the realised median error by confidence tier on demand from stored scans (closing prices as stored, so a split between scans distorts it) and the Strategy advisor quotes it; no graded record is published

Metric: fair-value error vs realised price. Grader: none. Log: none.

Validation

invariant tests/invariants/test_dcf_discount_invariants.py (no price feedback into the discount rate), test_model_entry_point_invariants.py (WACC floor survives any leverage; a debt-free firm gets exactly its floored cost of equity), and test_ensemble_confidence_invariants.py for valuation/aggregate.blend, which is this model's aggregation step but lives outside its file so it is not listed above: the top tier now requires three corroborating families, and a fully price-rejected name suppresses rather than guesses

Assumptions

ConstantValueProvenanceReference & note
AFFO_RATIO
valuation/models/class_models.py
0.85recorded decisionAFFO = 0.85 x FFO for the REIT perpetuity: Nareit leaves the recurring-capex deduction to the filer and the feed carries no maintenance-capex line, so a sector-average 15% haircut stands in. A proxy, named as one; the P/FFO anchor (weight 2.0) does not use it (recorded 2026-09-16)
BOTTOM_UP_BETA_WEIGHT
valuation/models/dcf.py
0.67nothing written down0.67 on the sector-relevered beta against 0.33 on the firm's own regression beta. The split matches Blume's classic beta-adjustment weights exactly, but Blume shrinks a raw beta toward 1.0 - a DIFFERENT pair - so whether that is the real provenance or a coincidence is UNVERIFIED, and it stays ungraded. Asserting a citation I cannot confirm is the failure this register exists to prevent.
BOTTOM_UP_WEIGHT
valuation/discount.py
0.67recorded decisionthe equity-model beta is 0.67 x the Damodaran industry levered beta + 0.33 x the firm's regression beta — the same blend the FCFF path has used since audit M3, now applied where a raw single-stock beta used to stand alone (recorded 2026-09-16)
COST_OF_DEBT
valuation/models/dcf.py
0.05recorded decision5% pre-tax, used ONLY when interest coverage cannot be computed, so it is a fallback and not the primary path. Sits above a mid-single-digit risk-free rate by roughly an investment-grade spread. The better answer is a synthetic-rating table mapping coverage to a spread, recorded here as the improvement rather than pretended to be in place (recorded 2026-09-10)
ERP_MATURE
valuation/discount.py
0.0423external sourceDamodaran, Country Default Spreads and Risk Premiums, January 2026: mature-market implied ERP 4.23%; US total 4.46% (country premium 0.23%), Canada 4.23% (0.00%). pages.stern.nyu.edu/~adamodar/New_Home_Page/datafile/ctryprem.html The owner chose on 2026-09-15 to adopt the CURRENT implied premium plus a country premium in place of the 5.5% historical yardstick (see ERP), knowing it lowers most costs of equity by 100-200 bp and raises fair values universe-wide. It shipped behind VALUATION_DISCOUNT_RATE, dry-run on a prod copy first; that switch was RETIRED 2026-09-18 and this is now the only basis.
EV_REVENUE_MULTIPLE_HIGH
valuation/models/class_models.py
15.0recorded decisionupper bound of the same clamp; see EV_REVENUE_MULTIPLE_LOW (recorded 2026-09-16)
EV_REVENUE_MULTIPLE_LOW
valuation/models/class_models.py
1.0recorded decisionEV/Revenue = a + b x Rule-of-40, fitted inside the software cohort (>= 10 peers) else the published relation (2.7x for a name failing the rule, ~+1.0x per 10 points on an FCF basis; median 4.8x pass vs 2.7x fail); clamped to [1, 15]x so an outlier cohort cannot imply a multiple no software company has sustained (recorded 2026-09-16)
FFO_GROWTH_CAP
valuation/models/class_models.py
0.04recorded decisionFFO/share growth into the REIT perpetuity is capped at 4%: a REIT distributes ~90% of taxable income, so its internal growth is rent escalators plus retained ~10%, not the 6% a general DDM allows (recorded 2026-09-16)
HORIZON_YEARS
valuation/models/dcf.py
5recorded decisiona 5-year explicit forecast, the short end of the conventional 5-10 year window. Short deliberately: every additional projected year is a compounding guess, and a shorter explicit horizon pushes more value into the terminal term where the growth cap and the WACC floor both bind (recorded 2026-09-10)
JUSTIFIED_MULTIPLE_HIGH
valuation/models/class_models.py
3.0recorded decisionupper bound of the same clamp; see JUSTIFIED_MULTIPLE_LOW (recorded 2026-09-16)
JUSTIFIED_MULTIPLE_LOW
valuation/models/class_models.py
0.5recorded decisionthe justified P/TBV and P/B multiples — (ROE-g)/(k-g), Damodaran — are clamped to [0.5, 3.0]: the relation explodes as k -> g and goes negative below the cost of equity; the band is where listed banks and insurers have traded for two decades (JPM ~3.1x at 17% ROE, the median bank ~1.1x) (recorded 2026-09-16)
LEGACY_RISK_FREE
valuation/discount.py
0.043recorded decisionthe hard-coded 4.3% every valuation used until the 2026 basis is on, and the fallback when the macro cache is absent, stale or implausible (recorded 2026-09-16)
MACRO_MAX_AGE_HOURS
valuation/discount.py
72recorded decisionthe macro cache refreshes daily; a yield older than three days is stale and the fallback applies. Never fetched on a valuation path (recorded 2026-09-16)
MAX_FCF_GROWTH
valuation/models/dcf.py
0.12recorded decisiona 12% cap on projected free-cash-flow growth. Without it a single exceptional year extrapolates across the whole explicit horizon and then into the terminal value, which is the classic way a DCF produces an absurd number. 12% sustained over five years is already top-decile, so the cap binds exactly where extrapolation is least safe (recorded 2026-09-10)
RISK_FREE_CEILING
valuation/discount.py
0.07recorded decisionupper bound of the same plausibility band (recorded 2026-09-16)
RISK_FREE_FLOOR
valuation/discount.py
0.02recorded decisiona cached 10-year yield below 2% or above 7% is a feed error, not a rate: the fallback applies and the source says so (recorded 2026-09-16)
STATUTORY_TAX
valuation/models/dcf.py
0.25NOT a source'fallback when domicile is unknown' says what the constant is FOR, not why it is this rate. Sourceable: OECD / Finance Canada statutory rates.
TERMINAL_GROWTH
valuation/models/dcf.py
0.025recorded decision2.5% perpetuity growth, on the standard convention that a terminal rate may not exceed long-run nominal GDP - no firm outgrows its economy forever. The Bank of Canada's inflation target is 2%, so 2.5% nominal implies about 0.5% real perpetual growth, modest by construction. The convention is standard; the specific figure is ours (recorded 2026-09-10)
WACC_FLOOR
valuation/models/dcf.py
0.07recorded decisionDERIVED, not chosen: TERMINAL_GROWTH + WACC_FLOOR_SPREAD = 0.070. It was a bare 0.085 until 2026-09-18, which silently changed meaning when the equity risk premium underneath it moved — on the 2026 basis a beta-1.0 name prices near 0.0876, so the floor stopped guarding the tail and began clamping the quartile: 562 of 1896 priced rows on the 2026-09-17 production scan, 171 of them losing more than the whole 100 bp quality adjustment (median loss 56 bp, max 351 bp). Defining it against the quantity it protects is what stops the next premium change re-breaking it. (recorded 2026-09-10)
WACC_FLOOR_SPREAD
valuation/models/dcf.py
0.045recorded decision4.5 points over TERMINAL_GROWTH, so the Gordon terminal denominator is 0.045. Chosen against measurement AND against the record: the original comment says a 6% floor made terminal values explode, which would have left 0.035, so this is a full point wider than the rejected setting. At this spread 79 of 1896 priced rows (4.2%) still clamp — a tail again rather than the 29.6% the bare 0.085 clamped. Empirical; no external authority. (recorded 2026-09-10)

Limitations

Entry points & executable coverage

Entry pointSubstrate gate
true_waccinvariant / oracle
fcff_baseinvariant / oracle
_quality_adjinvariant / oracle

Deep Score (sector + size relative)

deep_score · owner vishalsinha · version 2.1.3 · last reviewed 2026-10-06 · deep_score_engine.py

Monitoring

WIRED, IMMATURE grades matured cohorts; none have matured

Metric: sector+size-relative excess forward return by score decile. Grader: scripts/deep_score_track_record.py. Log: winner_predictions.

Population test

partly certified 2026-10-06 scripts/backtest_deep_score_population.py · primary: 23,694 graded name-years, rank-IC +0.104, tercile spread +0.067, t 3.10, deflated Sharpe 0.992 (N=2), halves +0.073 / +0.110 -> certified; secondary: 22,184 graded name-years, rank-IC +0.106, tercile spread +0.073, t 2.89, deflated Sharpe 0.992, halves +0.065 / +0.130 -> not certified (scripts/backtest_deep_score_result.json; pre-registered df411c34, tag deep-score-preregistration-2026-10-06)

Scope: CERTIFIED: the 85-point fundamentals core (four business pillars) as a rank of the three-year sector+size-relative excess return over US 10-K filers FY2009-2022. NOT certified: the five-pillar variant (valuation on a public-float proxy, t 2.89). NOT tested: the Valuation pillar as shipped, PEG, the label cut-offs, TSX listings, the sizing bands, any probability.

Validation

population test (fundamentals core) + distribution invariant The 85-point fundamentals core's rank: certified by the pre-registered point-in-time population test, scripts/backtest_deep_score_population.py (result scripts/backtest_deep_score_result.json; 2.1.3) - the Valuation pillar, the label cut-offs and the bands are not covered by it. tests/invariants/test_deep_score_calibration_invariants.py — labels must land on the fractions their cliffs name; proven red against the pre-C5 behaviour. tests/invariants/test_deep_score_class_invariants.py — every class is scored on its own set in its own cohort, computes >= 70% of its factors on a six-class universe, the label mix still lands on the cliffs with every class on, classes-off is byte-identical, a thin class reverts to the default set; each gate proven red against its named injection (2026-09-16). tests/invariants/test_deep_score_bank_invariants.py — each balance-sheet class sits at its own pillar midpoint, one basis per cohort, a thin class reverts to the standard basis, every uncomputable substitute is None; each gate proven red against its named injection (2026-09-15)

Assumptions

ConstantValueProvenanceReference & note
CRITICAL_SCORE
deep_score_model.py
10recorded decisionthe worst decile of the universe, replacing a bare `score < 25` that meant 2.6% of names on the old bell-shaped scale and would have meant 23.8% on the percentile scale, flipping 6 of 39 real holdings into a SELL no rule intended (recorded 2026-09-10)
INVESTMENT_COMPANY_LEVERAGE_MAX
business_class.py
3.0recorded decisionbelow 3.0 dollars of assets per dollar of tangible common equity, a name that presents as a lender by income is holding a portfolio against its own capital rather than funding one with liabilities, and is UNSCORED rather than ranked. The line is statutory, not fitted: the Investment Company Act's asset-coverage rule caps a BDC at 2:1 debt to equity, so a US investment company cannot exceed ~3x. Measured on the 2026-09-17 scan's 250-name spread cohort, the 31 closed-end funds and BDCs run 1.02x to 2.38x (median 1.72x) against 11.64x median for the other 214, and the lowest bank-industry name is Banco Macro at 3.71x. Income alone cannot separate them (Ares Capital's net interest share is 96.1%, JPMorgan's 45.0%), which is why the income test needs a balance-sheet companion. The known boundary is Lufax at 2.98x, a real lender on a capital-light model, unscored on that scan for other reasons and named in data/class_review/ (recorded 2026-09-17)
NII_REVENUE_SHARE_MIN
business_class.py
10.0recorded decisionnet interest income at or above 10% of total revenue makes a Financial Services name a spread lender, scored on the balance-sheet basis inside its own peer group. Measured across 153 financials plus 30 non-financial controls: the class descends AXP 24.0, GS 23.3, STT 21.2, SF 19.9, RJF 15.5, MS 15.2 and the next name is IREN at 3.0 — a 12.2-point gap, the widest anywhere below 40%; every insurer is at or below -0.3% and every exchange or asset manager at or below 1.7%. The vendor industry string fails in both directions (Capital Markets holds GS and IBKR; Asset Management holds the custody banks STT and NTRS; Credit Services holds COF and V) and an equity multiple cannot classify at all (KO 25x, FHI 167x). 10 sits mid-gap with margin on both sides (recorded 2026-09-15)
PREMIUM_SCORE
deep_score_model.py
85recorded decisionmoved from 80 to 85 — the label ladder's top cliff — so it is no longer the one threshold sitting off the ladder. After C5 rescaled the composite, 80 meant the top 20% while STRONG BUY meant the top 15%, so a name could clear the premium-quality bar without clearing the ordinary verdict's. The premium screens go from 216 of 1007 names to 168: strictly more selective, which is the safe direction for a screen whose whole claim is selectivity. Pinned to LABEL_CLIFFS by tests/parity/test_deep_score_thresholds.py (recorded 2026-09-10)
_MIN_SECTOR_COHORT
deep_score_model.py
20recorded decision20 names minimum before a sector is scored against its own peers. Below that a percentile rank is largely an artefact of who happens to be in the cohort: at n=20 one name already moves the rank by 5 points, and finer resolution would be spurious precision. The code comment previously pointed at winner_model._MIN_UNIVERSE, which was itself ungrounded - a citation chain terminating in nothing (recorded 2026-09-10)

Limitations

Entry points & executable coverage

Entry pointSubstrate gate
score_cohortinvariant / oracle
compute_quadrantinvariant / oracle
legacy_fallbackinvariant / oracle

Winner Odds

winner · owner vishalsinha · version 1.16.1 · last reviewed 2026-10-06 · winner_engine.py

Monitoring

WIRED AND RUNNING verified against prod; earliest 1yr cohort matures ~2027-07-21

Metric: rank-IC, top-vs-bottom decile forward spread, winner hit rate. Grader: scripts/winner_track_record.py. Log: winner_predictions.

Population test

certified 2026-10-06 scripts/backtest_winner_population.py · re-run on the corrected history (1.16.1): rank-IC 0.234, tercile spread +0.203, t 4.52, deflated Sharpe 0.9997 over N=17, held-out halves 0.228 / 0.226 -> rank certified

Scope: the rank (ordering) over US 10-K filers FY2009-2022; not a probability (CALIBRATED False); TSX listings not tested

Validation

self-gating + invariant The rank: scripts/backtest_winner_population.py over every US 10-K filer FY2009-2022, judged by the one certify() rule (scripts/_winner_backtest_stats.py) — certified at 1.12.0 / 1.16.0. The probability: CALIBRATED=False until scripts/winner_calibration.py passes on held-out blocks (it failed 2026-09-28), and tests/invariants/test_remaining_entry_point_invariants.py ENFORCES that gate: no calibration parameter may be set while it is closed, and thin evidence must abstain rather than be ranked

Assumptions

ConstantValueProvenanceReference & note
ALTMAN_ZPP_DISTRESS
factor_helpers.py
1.1external sourceAltman, E. I., 'Evolution of the Altman Z-Score family of models' (docs/sources/altman-evolution-of-z-score-z-double-prime-zones.txt): the Z'' non-manufacturer model's distress line is 4.35 including its 3.25 constant; the stored column omits the constant, so 1.10. The gate is not applied to banks, insurers, REITs, regulated utilities or funds (winner_model.ALTMAN_EXEMPT_CLASSES). Until 2026-09-26 it was 1.8, the 1968 manufacturer-Z boundary applied to a Z'' value. utilities are a house exclusion, not Altman's; Altman excludes financials
BASE_RATE
winner_model.py
0.083recorded decision0.083 = the share of graded company-years on the SEC 10-K population (FY2009-2022, 14,149 graded) that gained >= +150% in three years, bounded 0.068-0.245 by the exits still unpriced (BASE_RATE_BOUNDS). MEASURED, not sourced: no external study states a 2.5x-in-3-years rate, and the 0.12 carried until 2026-09-27 cited studies never pinned. Adopted by the owner as a disclosed house figure; it completes the rank's certification and does NOT calibrate a probability (see 1.12.0) (recorded 2026-09-28)
CALIBRATED
winner_model.py
FalseNOT a sourcea gate flag, not a parameter — False keeps the engine rank-only until a positive out-of-fold Deflated Sharpe. Correct behaviour, but not a source.
TIER_BUFFER
winner_model.py
0.03external sourceMorningstar, 'Morningstar Quantitative Equity Ratings' methodology (QERFinal_V2, read 2026-09-26): a 3% buffer at each star-rating boundary so a rating does not flip on a small move. Adopted as a HOUSE constant for the same reason — the Strong list turned over 21 of 76 names between the 2026-08-18 and 2026-09-15 snapshots; a name within this distance of a cut keeps last scan's ADJACENT tier. the value is Morningstar's, not derived on this book
_MIN_CORE_FACTORS
winner_model.py
5recorded decision5 of the 7 CORE factors must be present or the name abstains - 71%, the nearest floor to the recorded three-quarters intent of 6-of-8 before reinvest_runway left the registry on 2026-09-27 (it was ROIC x retention, z-correlation 0.905 with ROIC). Scoring a name on a third of its inputs yields a confident-looking number backed by very little; abstaining says so instead (recorded 2026-09-27)
_MIN_UNIVERSE
winner_model.py
20recorded decision20 names minimum before the cross-section is ranked at all, for the same reason as deep_score._MIN_SECTOR_COHORT: at n=20 a single name moves any percentile by 5 points, so a rank over fewer is noise wearing a number's clothes (recorded 2026-09-10)

Limitations

Entry points & executable coverage

Entry pointSubstrate gate
score_universeinvariant / oracle
compute_momentsinvariant / oracle

Inflection Engine

inflection · owner vishalsinha · version 3.0.1 · last reviewed 2026-10-07 · inflection.py

Monitoring

WIRED, IMMATURE (2026-09-28) grades matured monthly snapshots against adjusted prices; none have matured

Metric: rank-IC of the live score with the forward return; forward return and wipe-out share per band; Emerging inside vs outside the Winner top tier. Grader: scripts/inflection_track_record.py. Log: winner_predictions (inflection_score + winner_tier columns).

Population test

not certified 2026-10-06 scripts/inflection_program.py · not certified (re-run 2026-10-06) on the corrected history (SIC map, 2023q1 data set): V7 rank-IC 0.163, spread +0.224, t 7.43, halves 0.159 / 0.136 - the ranks still separate - but deflated Sharpe 0.480 over N=13 against 0.95 (scripts/inflection_program_result.json)

Scope: variant V7 (shipped as engine 3.0.0): the rank over US 10-K filers FY2009-2022

Recorded 2026-09-29: certified · V7 rank-IC 0.169, spread +0.248, t 8.12, deflated Sharpe 0.973 over N=13, held-out halves 0.171 / 0.140

Open: the variant set: V5 and V6 are read on four held-out blocks, and V5's Sharpe (8.44) dominates the variance the deflation uses (3.89; 1.44 without V5 and V6) - whether they count on the same footing is under investigation

Validation

population test + invariant The rank: certified 2026-09-28 on the point-in-time population by scripts/inflection_program.py (variant V7, shipped as 3.0.0; weights measured on the same years); NOT CERTIFIED on the 2026-10-06 re-run under the corrected history (deflated Sharpe 0.480 over N=13 against 0.95; the ranks still separate, rank-IC 0.163; 3.0.1, population_test). The engine stays live and disclosed (owner, 2026-10-07). tests/invariants/test_remaining_entry_point_invariants.py — under 3.0.0 the score is the weight-averaged sector-peer percentile over ONLY the weighted factors a name has, re-based to the bands, so a data-poor name is neither penalised nor inflated; fewer than MIN_SCORED known factors abstains as 'Insufficient signal' rather than scoring zero

Assumptions

ConstantValueProvenanceReference & note
MIN_SCORED
inflection_factors.py
4recorded decision4 of the 6 weighted factors must be known or the name reads 'Insufficient signal' — two-thirds, the same proportion as winner_model._MIN_CORE_FACTORS; a percentile average over two inputs is a confident-looking number backed by very little (recorded 2026-09-28)
PEER_MIN
inflection_factors.py
20recorded decision20 names: a sector-year with fewer known values ranks against the whole universe instead — the same floor winner_model._MIN_UNIVERSE uses, for the same reason (at n=20 one name moves a percentile by 5 points; below that a within-sector rank is noise wearing a number's clothes) (recorded 2026-09-28)
_FWD_MIN_ANALYSTS
inflection.py
3recorded decision3 estimates before forward growth is used at all. One or two is a single analyst's opinion presented as a consensus, and the factor's whole claim is that it reflects one (recorded 2026-09-10)
_LEVEL_CAP
inflection.py
200.0recorded decisionwinsorisation cap on level factors. Uncapped, one company with a freak ratio dominates the cross-section and compresses every other name toward the middle. Winsorising is standard practice; the specific cap is ours (recorded 2026-09-10)
_MIN_MARKET_CAP
inflection.py
300000000.0recorded decisiona 300M floor. Below roughly this size the vendor's fundamentals thin out and the bid-ask spread starts to exceed the mispricing the screen is hunting, so a signal there is not actionable even when it is real. A judgement about DATA and LIQUIDITY, not about the companies (recorded 2026-09-10)
_MIN_POINTS
inflection.py
5recorded decision5 annual data points, which is exactly what _MIN_YOY=4 year-over-year comparisons requires. The two move together; neither is independently chosen (recorded 2026-09-10)
_MIN_REVENUE_B
inflection.py
0.05recorded decisiona 50M revenue floor. Percentage growth off a tiny base is arithmetic noise - a company going from 2M to 4M posts 100% growth and says nothing about an inflection (recorded 2026-09-10)
_MIN_YOY
inflection.py
4recorded decision4 year-over-year observations. This one is arithmetic rather than taste: an INFLECTION is a change in the RATE of change, so you need at least three growth rates to see one, and four year-on-year comparisons to be robust to a single odd year (recorded 2026-09-10)
_REBASE_KNOTS
inflection.py
((0.0, 0.0), (50.0, 35.0), (65.0, 50.0), (79.0, 65.0), (91.0, 80.0), (100.0, 100.0))recorded decisionthe weighted sector-percentile (3.0.0) is mapped piecewise-linearly onto the published band scale so that each label keeps the share of the 2026-09-28 book it had under engine 2.0.0 (knots derived on that book; recorded in the 3.0.0 limitation). Before 3.0.0 the same device re-based the earned-over-available ratio (raw 39.7 -> 35, 58.5 -> 50, 71.4 -> 65, 83.3 -> 80). Cutting growth acceleration to 10 points and zeroing insider buying lifted most raw ratios, and at the old cuts the Emerging-or-better set would have gone from 109 to 264 names; the bands are published on every surface, so the scale moved instead of the cuts. Monotone: no ordering changes. Re-derived whenever a weight moves (recorded 2026-09-28)
_ROIC_CAP
inflection.py
100.0recorded decisionwinsorisation cap on ROIC specifically, the factor most prone to an absurd reading: a near-zero invested-capital denominator produces a four-figure percentage that is a balance-sheet artefact, not a business fact (recorded 2026-09-10)

Limitations

Entry points & executable coverage

Entry pointSubstrate gate
compute_inflectioninvariant / oracle
score_inflection_universeinvariant / oracle
earnings_surpriseinvariant / oracle

ETF quality score

etf_score · owner vishalsinha · version 1.1.0 · last reviewed 2026-09-16 · etf_scoring.py

Monitoring

NOT MONITORED the mix was measured once on 2026-09-16

Metric: label mix per asset class after each ETF scan. Grader: none. Log: etf_fundamentals.

Population test

not yet tested 2026-10-06 · no point-in-time ETF history exists in this system (no archived categories, expense ratios or holdings by date), so no population test can be built today; the label mix was measured once (2026-09-16) and nothing grades the labels forward

Validation

example tests/test_etf_scoring.py — asset-class corpus over the real universe's hard cases, per-class ladder golden, unknown-window re-basing, scanner keeps a missing window None; each seen RED against its injection

Assumptions

ConstantValueProvenanceReference & note
_THRESH_10Y
etf_scoring.py
{'equity': [12, 8, 5, 2], 'fixed_income': [4, 3, 2, 1], 'cash': [2.5, 2, 1.5, 0.5], 'balanced': [8, 6, 4, 2]}recorded decisionten-year return ladder (%/yr) per asset class, same construction as the five-year ladder; commodity and alternative funds are judged on the equity ladder because no lower bar is defensible for them (recorded 2026-09-16)
_THRESH_5Y
etf_scoring.py
{'equity': [15, 10, 7, 4, 0], 'fixed_income': [5, 3.5, 2.5, 1.5, 0], 'cash': [3.5, 2.5, 2, 1, 0], 'balanced': [10, 7, 5, 3, 0]}recorded decisionfive-year return ladder (%/yr) PER ASSET CLASS. 1.1.0 (2026-09-16, owner decision from the universe verification): performance is judged within an asset class — equity, fixed income, cash, balanced, commodity, alternative — inferred from the vendor category or, where Yahoo carries none (every TSX listing: 138 of 140 Canadian funds), from the fund's name. The bar moves per class (fixed income 5%/yr is the top rung, cash 3.5%, balanced 10%; equity unchanged at 15%); the points per rung do not. A return window the fund is too young to have is unknown, never 0, and the score is re-based on the pillars it has. Dry run on the prod copy: fixed income median 57 -> 65, cash 48 -> 69, equity unchanged; CASH.TO, CBIL.TO and TCSH.TO carried a fabricated 0% five-year return and are now scored on cost, size and income. Every threshold is a judgement call sized to the 2015-2025 rate regime (recorded 2026-09-16)

Limitations

Entry points & executable coverage

Entry pointSubstrate gate
calculate_etf_scoreinvariant / oracle
etf_asset_classnot in the inventory

Position sizing

sizing · owner vishalsinha · version 1.1.1 · last reviewed 2026-10-06 · sizing.py

Monitoring

NOT MONITORED

Metric: realised concentration vs suggested band. Grader: none. Log: none.

Validation

invariant tests/invariants/test_model_entry_point_invariants.py — band/tier laws, one-tier valuation move, AVOID is never promoted, unknown quality is not sized as average

Assumptions

ConstantValueProvenanceReference & note
_MOS_UP_NOTCH
sizing.py
25.0recorded decisiona 25% margin of safety before valuation promotes a position one tier. Set high on purpose: the promotion is the only place valuation ADDS risk-taking, so it should require a discount big enough to survive the fair value being wrong. Below it the tier is quality alone (recorded 2026-09-10)
_WEIGHT_TOLERANCE
sizing.py
0.25recorded decision0.25 percentage points of slack before a holding is called off-target, so a 4.1% position is not nagged about a 2-4% band. More than an order of magnitude smaller than any band's width, so it softens the edges without moving them (recorded 2026-09-10)

Limitations

Entry points & executable coverage

Entry pointSubstrate gate
suggest_sizeinvariant / oracle
size_verdictinvariant / oracle

Decumulation plan

decumulation · owner vishalsinha · version 1.5.0 · last reviewed 2026-10-06 · decumulation.py

Monitoring

NOT MONITORED

Metric: plan versus actual withdrawal and balance. Grader: none. Log: none.

Validation

invariant + independent oracle tests/invariants/test_retirement_horizon.py holds the plan to the pot AT RETIREMENT (hand-checked: C$1M at 3% real for 20 years is C$1,806,111), proves an already-retired plan moves by nothing, and proves a past retirement age cannot DIVIDE the pot; tests/invariants/test_model_entry_point_invariants.py replays the withdrawal year by year — it must neither deplete early nor survive an extra year; test_remaining_entry_point_invariants.py pins the CPP/OAS break-even against an independent simulation and the withdrawal SEQUENCE against the order the docstring promises, including that being near the OAS clawback annotates the TFSA step without promoting it

Assumptions

ConstantValueProvenanceReference & note
DEFAULT_REAL_RETURN_PCT
config
—derived / governeddata/tax_rules.json via tax_config.default_real_return_pct(); cross-checked against the 2026 FP Canada / Institute of Financial Planning Projection Assumption Guidelines governed by the same owner-approved config flow as the CRA tax rules — the only assumption in the register with real change control. CROSS-CHECKED 2026-09-10: the 2026 PAG gives 2.1% inflation with nominal geometric returns of 3.2% fixed income and 6.3%/6.4% Canadian/US equities, implying a real return of 2.99% for a 60/40 mix — against our shipped 3.0%. That is a near-exact match to the Canadian planning standard. Two caveats: PAG figures are GROSS of investment fees and FP Canada directs planners to deduct them (a 1% fee makes 3.0% real gross about 2.0% net), and the constant is mix-blind while the PAG-implied figure is not.

Limitations

Entry points & executable coverage

Entry pointSubstrate gate
drawdown_orderinvariant / oracle
sustainable_spendinvariant / oracle
breakeven_ageinvariant / oracle
build_planinvariant / oracle
planning_horizoninvariant / oracle

Strategy Advisor — signal rule engine

signal_rules · owner vishalsinha · version 1.3.0 · last reviewed 2026-10-01 · signal_engine.py

Monitoring

NOT MONITORED

Metric: forward outcome of TRIM/SELL signals. Grader: none. Log: none.

Validation

invariant tests/invariants/test_advisor_signal_invariants.py + test_advisor_absence_invariants.py — soft-reason tax guard, verdict never silence, tax always stated, one decider, unknown is never a number

Assumptions

ConstantValueProvenanceReference & note
GUARDED_INPUTS
signal_engine.py
('score', 'beta', 'gain_pct', 'gain_cad', 'value_cad')recorded decisionthe five inputs whose absence stands a rule DOWN instead of becoming a plausible number: an unscored name used to be trimmed as 'Deep Score 50', and an unpriced holding's unknown weight read as 0% raised an ADD for being 'only 0.0% of portfolio' (2026-10-01) — so value_cad is one of them, declared by every rule that reads a weight or a value (recorded 2026-10-01)
SECTOR_ADD_CEILING_PCT
signal_engine.py
40recorded decision40% of the book in one sector, at which ADD rules stand down: five points below the trim ceiling so a sector stops growing before it is trimmed. Moves with a declared sector limit (declared minus 5). Was a literal default argument, and a second copy in advisor_engine; both now read this (recorded 2026-09-29)
SECTOR_CEILING_PCT
signal_engine.py
45recorded decision45% of the book in one sector before its holdings are trimmed; _regime_ceiling tightens it in late cycle and contraction. Above the 25-30% a private bank's policy would set (review B5). Measured against look-through sector weights, never against a bucket named Unknown. A limit the client DECLARES (max_sector_pct) replaces it, tighter or looser, and the regime does not tighten a declared limit; a looser one is honoured and stated as a departure from the house view whenever the sector is above 45% (owner decision 2026-09-29, CFA III(C)) (recorded 2026-09-29)
SINGLE_NAME_CEILING_PCT
signal_engine.py
25recorded decision25% of the book in one name, aggregated across accounts, before a TRIM fires. A limit the client DECLARES (max_single_position_pct) replaces it, tighter or looser; a looser one is honoured and stated as a departure from the house view whenever the name is above 25% (owner decision 2026-09-29, CFA III(C)). Chosen for a let-winners-run book and ABOVE private-bank policy, which sets a 10% soft and 15-20% hard single-name limit (2026-09-09 review, B5). The trim target is 70% of the ceiling so one trim lasts instead of whipsawing (recorded 2026-09-09)
USD_EXPOSURE_CEILING_PCT
signal_engine.py
45recorded decision45% of the book USD-priced before a registered USD position is trimmed. Deliberately a noise-limited rule: for a Canadian growth investor unhedged USD cushions CAD-terms drawdowns, so the rule is muted by a materiality floor rather than sharpened (review B4) (recorded 2026-09-10)

Limitations

Entry points & executable coverage

Entry pointSubstrate gate
run_signal_engineinvariant / oracle
_soft_reason_taxable_guardinvariant / oracle
concentration_limitinvariant / oracle
_trim_qtyinvariant / oracle
_tax_cost_noteinvariant / oracle
_value_stateinvariant / oracle

Strategy Advisor — orchestration, suitability, tax allocation, AI validation

strategy_advisor · owner vishalsinha · version 1.16.0 · last reviewed 2026-10-06 · advisor_engine.py

Monitoring

LOGGING picks recorded since 2026-09-10, none matured; not yet graded

Metric: forward return of diversifier picks vs SPY, by cohort. Grader: scripts/advisor_pick_track_record.py. Log: advisor_picks.

Population test

not yet tested 2026-10-06 · the diversifier picks are logged (advisor_picks, since 2026-09-10) and graded forward by scripts/advisor_pick_track_record.py; no cohort has matured, and the pick logic depends on each client's own book, so no retrospective population test exists

Validation

invariant tests/invariants/test_advisor_*.py, test_fund_lookthrough_invariants.py, test_stress_replay_invariants.py, test_risk_questionnaire_reaches_the_advisor.py, test_growth_and_concentration_disclosure.py — absence never a number, registered-first allocation, AI figures reconciled to the engine, fund fold-through on both spellings of absence, and the two engine-enforced disclosures: the threshold is the pillar's own weight share, an unmeasured share is silence and never 0%, the candidate whitelist carries the measurement through to the report, and no disclosure makes a forward claim the score has not been validated for (CFA V.A), each proven red by 16 entries in tests/parity/test_gate_injections.py

Assumptions

ConstantValueProvenanceReference & note
MINIMUM_SUITABILITY_FIELDS
advisor_engine.py
('objective', 'risk_tolerance', 'time_horizon', 'near_term_needs_cad', 'annual_contribution_cad', 'max_drawdown_pct')recorded decisionthe six fields a plan needs before it is more than a review draft: objective, risk tolerance (willingness), horizon (capacity), near-term liquidity, contribution capacity and the loss the client can bear — the elements CIRO's suitability rule and NI 31-103 list for a determination, minus KYC identity fields, which do not change the advice. A field satisfied by the risk questionnaire counts, with its provenance shown. Below the set every action loses its timing and is marked review-only; nothing is inferred (recorded 2026-09-23)
_AGGREGATE_TRIM_RULES
advisor_engine.py
('concentration_overweight', 'sector_overconcentration')recorded decisionthe two trim rules whose reduction is re-allocated across an aggregate position's account slices, registered accounts first: concentration and sector. Only these are portfolio-level; a per-slice rule's reduction belongs to its slice (recorded 2026-09-09)
_POLICY_REVIEW_DAYS
advisor_engine.py
365external sourceCFA Institute, Standards of Practice Handbook, 12th ed., Standard III(C) Suitability, 'Updating an Investment Policy': 'An IPS should be reviewed at least annually' (pinned T0) a policy last saved more than 365 days ago is due for review; saving it unchanged counts as one
_SINGLE_NAME_BUDGET_PCT
strategy_report.py
20.0recorded decision20% of the book held as INDIVIDUAL SECURITIES rather than funds, above which the Strategy report states the figure and its basis. The owner set the default and the client may set their own. DISCLOSED, NOT ENFORCED: measured on production 2026-09-24 all 6 of 6 books exceed it, from 62.9% to 100.0%, so a hard cap would reject every live portfolio rather than inform it. The evidence behind the line is one-sided — Bessembinder (JFE 2018) finds only 42.6% of ~26,000 US stocks since 1926 beat one-month T-bills over their lifetime with the best 4% of firms accounting for all net wealth creation, and Statman (JFQA 1987) with Domian, Louton & Racine (Financial Review 2007) put 40-50 names as the count needed to remove 90% of diversifiable risk — but the LEVEL is a house choice, not a figure any of those papers publishes, which is why it is graded a decision and not external (recorded 2026-09-24)
_SLEEVE_SUSPEND_AFTER_LOSING_COHORTS
advisor_engine.py
3recorded decisionhow many GRADED cohorts may underperform before the single-name screen stops being shown at all and the report reverts to allocation-level advice. A decision, NOT a fitted number: no cohort has matured (first grading 2027-01-15), so there is nothing to fit it to, and a threshold fitted later to the very data it judges would be circular. Three is slow enough that one bad quarter does not silence a screen on noise, and fast enough that a persistently losing screen does not run for years. REVISIT when the first cohorts mature (recorded 2026-09-24)
_SUITABILITY_INCOME_BETA
advisor_engine.py
1.3recorded decisionfor an Income or Capital-preservation objective, an ADD of a non-payer or of a name with beta above 1.3 becomes a watch item: 1.3 is where a holding is clearly more volatile than the market rather than roughly with it (recorded 2026-09-10)
_SUITABILITY_RISK_BETA
advisor_engine.py
1.0recorded decisionan ADD of a name with beta above 1.0 becomes HOLD when the bear scenario's modelled loss exceeds the drawdown the client declared. Market beta is the line because the conflict is with the market's own drawdown, not a stock-specific one (recorded 2026-09-10)
GOAL_BENCHMARK_CAGR
config
—recorded decisionthe 8% a goal is measured against ONLY when no policy, no questionnaire and no classifiable holdings exist; defined in portfolio_db.py, so not pinned here. Every other reader measures the goal against the expected return of the client's policy mix on FP Canada's 2026 projection assumptions (_policy_expected_return) and names the basis it used, including this fallback (recorded 2026-09-23)

Limitations

Entry points & executable coverage

Entry pointSubstrate gate
_load_portfolio_holdingsinvariant / oracle
_build_portfolio_summaryinvariant / oracle
book_fund_costsinvariant / oracle
declared_advisory_fee_pctinvariant / oracle
_book_feeinvariant / oracle
_sector_weights_with_coverageinvariant / oracle
_suitability_gateinvariant / oracle
_suitability_completenessinvariant / oracle
_policy_expected_returninvariant / oracle
_goal_feasibilityinvariant / oracle
_proposed_policy_from_questionnaireinvariant / oracle
apply_tax_and_allocationinvariant / oracle
_validate_narrativeinvariant / oracle
_historical_stressinvariant / oracle
_industry_concentrationinvariant / oracle
_apply_engine_disclosuresinvariant / oracle
_project_candidatesinvariant / oracle
_disclose_growth_pillar_dependenceinvariant / oracle
_policy_rebalance_tradeinvariant / oracle
_enforce_policy_firstinvariant / oracle
_annual_gain_budget_cadinvariant / oracle
_gain_staging_noteinvariant / oracle
_mark_single_names_as_researchinvariant / oracle
_research_track_recordinvariant / oracle
_disclose_diversification_basisinvariant / oracle
_diversification_deltainvariant / oracle
_sleeve_suspensioninvariant / oracle
_apply_sleeve_suspensioninvariant / oracle
_policy_reviewinvariant / oracle

Superficial-loss determination (ITA s.54 / s.251.1)

superficial_loss · owner vishalsinha · version 1.0.0 · last reviewed 2026-09-24 · portfolio_db.py

Monitoring

MEASURED 2026-09-24 - 0 of 4 taxable loss positions disagree; 0 declared spouses

Metric: harvest candidates where the Tax Centre and the signal engine disagree. Grader: manual prod probe (scripts/reach.py + the harvest funnel). Log: none.

Validation

invariant tests/invariants/test_harvest_superficial_one_decider.py, test_affiliated_person_invariants.py, tests/test_superficial_loss*.py - still-held, split rows, affiliated scope, settlement date, and failure withholding the all-clear; 3 entries in tests/parity/test_gate_injections.py prove them red

Assumptions

ConstantValueProvenanceReference & note
_SUPERFICIAL_WINDOW_DAYS
portfolio_db.py
30external sourceITA s.54 'superficial loss': the identical property must be reacquired by the taxpayer or a person affiliated with them during the period beginning 30 days BEFORE and ending 30 days AFTER the disposition, and still be held at the end of that period. The number is the statute's, not a house choice. Written inline in six places until 2026-09-24; one definition now, shared by the denial fraction, the ACB roll-in, the cross-account roll-in and the Tax Centre's prospective harvest check. https://laws-lois.justice.gc.ca/eng/acts/i-3.3/section-54.html

Limitations

Entry points & executable coverage

Entry pointSubstrate gate
_superficial_window_qtyinvariant / oracle
_superficial_fractioninvariant / oracle
affiliated_usernamesinvariant / oracle

Retirement Monte Carlo

retirement_monte_carlo · owner vishalsinha · version 1.5.0 · last reviewed 2026-10-06 · retirement_sim.py

Monitoring

NOT MONITORED

Metric: realised path vs simulated distribution. Grader: none. Log: none.

Validation

process invariant tests/invariants/test_retirement_mc_invariants.py — the drawn process must deliver the compound return the plan states, the conversion must be disclosed, and sigma=0 must still reduce exactly to the deterministic path; proven red against the pre-fix behaviour

Assumptions

ConstantValueProvenanceReference & note
MC_RUNS
retirement_sim.py
1000recorded decision1,000 paths, and this one is MEASURED rather than argued. Across 12 seeds on the shipped plan: at 250 paths the reported success probability wobbles with a standard deviation of 3.59 points and a 14.00-point spread; at 1,000 it is 0.99 and 3.60; at 4,000 it is 0.80 and 2.90. So 1,000 puts sampling noise under a percentage point on a figure shown to one decimal, and quadrupling the compute buys about 0.2 of a point (recorded 2026-09-10)
MC_SEED
retirement_sim.py
20260704NOT a source'fixed seed so the same inputs give the same answer' explains why a seed is fixed, not why it is this number. Any fixed value would do.
PAG_INFLATION_PCT
retirement_sim.py
2.1external sourceFP Canada Standards Council / Institute of Financial Planning, Projection Assumption Guidelines 2026 — inflation 2.1% the inflation the PAG real figures were derived with; added back, compounded, to measure a nominal-dollar goal against a nominal expected return
PAG_REAL_RETURN_PCT
retirement_sim.py
{'equity': 4.25, 'fixed_income': 1.1, 'cash': 0.3}recorded decisionreal return by asset class, derived from the 2026 PAG: nominal geometric 2.4% short-term, 3.2% fixed income, 6.3% Canadian and 6.4% US equities, less the Guidelines' own 2.1% inflation. Equity averages the Canadian and US figures, which is where this book sits. GROSS OF FEES as published; since 2026-10-06 every projection deducts the book's fees from it through return_basis, as FP Canada directs (recorded 2026-10-06)
VOL_CASH
retirement_sim.py
0.02recorded decision2% REAL against a nominal 1.70% (10y) / 1.58% (20y) in the PAG Addendum. Real cash volatility is dominated by inflation volatility, so if anything 2% is on the LOW side; the sleeve is small enough that it barely moves a blended sigma (recorded 2026-09-10)
VOL_EQUITY
retirement_sim.py
0.17recorded decision17% REAL. Benchmarked against FP Canada / Institute of Financial Planning, 2026 Projection Assumption Guidelines Addendum (dated 30 April 2026), sheet 'Correlation & Standard Dev.', read from the published .xlsx: historical NOMINAL standard deviation, short-term 1.70%/1.58%, fixed income 5.82%/4.79%, Canadian equities 13.49%/16.29%, US equities 13.68%/15.53% over 10y (2016-2025) / 20y (2006-2025). Held ABOVE the nominal figures deliberately and for a stated reason, not out of vagueness: this simulation runs in REAL terms, and real-return volatility exceeds nominal for every asset class because real = nominal minus inflation and inflation is itself variable. 17% sits just above the 20-year nominal Canadian figure of 16.29%, which is the closest published comparator. NOT graded external, because we are not adopting the Addendum's number — FP Canada publishes no standard-deviation GUIDELINE at all, only historical figures 'for information purposes' (recorded 2026-09-10)
VOL_FIXED_INCOME
retirement_sim.py
0.08recorded decision8% REAL against a nominal 5.82% (10y) / 4.79% (20y) in the PAG Addendum. Directionally right — inflation risk hits bonds hardest, so real bond volatility materially exceeds nominal — and the margin is now SIZED rather than merely flagged. Measured on the standard plan, moving from 8% to the Addendum's 20-year 4.79% changes success probability by +1.2pp at 60/40, +2.0pp at 40/50/10 and +3.7pp at a bond-heavy 20/70/10. So it is conservative in the safe direction at a bounded cost, largest for the most bond-heavy plan. Deriving the TRUE real sigma needs a standard deviation for inflation; the Addendum's inflation series could not be parsed reliably out of the workbook and a guessed figure would be worse than an acknowledged margin (recorded 2026-09-10)

Limitations

Entry points & executable coverage

Entry pointSubstrate gate
monte_carloinvariant / oracle
simulate_deterministicinvariant / oracle
implied_real_returninvariant / oracle
blended_sigmainvariant / oracle
nominal_from_real_pctinvariant / oracle
goal_success_probabilityinvariant / oracle
net_real_returninvariant / oracle
portfolio_feeinvariant / oracle
return_basisinvariant / oracle

Risk questionnaire → suggested mix

risk_profile_questionnaire · owner vishalsinha · version 1.0.0 · last reviewed 2026-09-30 · riskprofile.py

Monitoring

NOT MONITORED

Metric: suggested mix vs the mix the client adopts. Grader: none. Log: none.

Validation

property test tests/test_riskprofile.py — every profile's targets sum to 100 over all 1,024 answer combinations, and the horizon and capitulation caps are applied and named

Assumptions

ConstantValueProvenanceReference & note
PROFILES
riskprofile.py
[{'name': 'capital_preservation', 'label': 'Capital preservation', 'targets': {'equity': 30, 'fixed_income': 60, 'cash': 10}}, {'name': 'conservative', 'label': 'Conservative', 'targets': {'equity': 45, 'fixed_income': 50, 'cash': 5}}, {'name': 'balanced', 'label': 'Balanced', 'targets': {'equity': 60, 'fixed_income': 35, 'cash': 5}}, {'name': 'growth', 'label': 'Growth', 'targets': {'equity': 75, 'fixed_income': 20, 'cash': 5}}, {'name': 'aggressive_growth', 'label': 'Aggressive growth', 'targets': {'equity': 90, 'fixed_income': 5, 'cash': 5}}]nothing written downthe five target mixes (equity / fixed income / cash, 30/60/10 to 90/5/5) are house judgement calls; no external source or derivation is recorded for them
_CAPITULATION_CAP
riskprofile.py
2recorded decisiona client who says they would sell everything in a crash is capped at Balanced: the mix you can hold through a drawdown beats the one that looked best on paper (recorded 2026-07-04)
_HORIZON_CAPS
riskprofile.py
{0: 1, 1: 3}recorded decisioncapacity caps willingness: money needed within 3 years is capped at Conservative, within 10 years at Growth — bravery does not extend a time horizon (recorded 2026-07-04)

Limitations

Entry points & executable coverage

Entry pointSubstrate gate
assessnot in the inventory

Historical crisis replay + expected shortfall

historical_stress_replay · owner vishalsinha · version 1.0.0 · last reviewed 2026-09-30 · stress_replay.py

Monitoring

NOT MONITORED

Metric: none — a replay of history has no forward outcome to grade. Grader: none. Log: none.

Validation

invariant tests/invariants/test_stress_replay_invariants.py — today's weights on actual closes, coverage stated, expected shortfall <= VaR <= mean, dollars on the covered value only

Assumptions

ConstantValueProvenanceReference & note
CRISIS_WINDOWS
stress_replay.py
{'gfc_2008': ('2007-10-01', '2013-06-01', '2008 GFC'), 'covid_2020': ('2020-02-01', '2021-03-01', '2020 COVID'), 'drawdown_2022': ('2022-01-01', '2024-06-01', '2022 Rate Shock')}recorded decisionthree windows, each running through the recovery so a drawdown and its recovery are both inside it: the 2008 GFC, 2020 COVID and the 2022 rate shock (recorded 2026-09-10)
_MIN_CVAR_OBS
stress_replay.py
20NOT a sourcean expected shortfall over fewer daily observations than this is noise and is not computed
_MIN_POINTS
stress_replay.py
10NOT a sourcefewer closes than this inside a window is not a replay, so none is shown
_START_SLACK_DAYS
stress_replay.py
31NOT a sourcea holding must have a close within this many days of the window start to be replayed; one without is left out and coverage says so

Limitations

Entry points & executable coverage

Entry pointSubstrate gate
historical_scenariosinvariant / oracle
cvarinvariant / oracle
portfolio_cvarinvariant / oracle

Beta-adjusted downside table (Portfolio page)

beta_stress_table · owner vishalsinha · version 1.1.0 · last reviewed 2026-10-06 · advisor_engine.py

Monitoring

NOT MONITORED

Metric: none — the shocks are illustrative, not predictions. Grader: none. Log: none.

Validation

invariant tests/invariants/test_beta_stress_invariants.py — cash is never shocked, a holding with no beta is excluded and named (never given one), the report rows use the same decider, a covered CDR reads its underlying's beta, the clamp holds

Assumptions

ConstantValueProvenanceReference & note
STRESS_SHOCKS
advisor_engine.py
(('Mild pullback', 10), ('Correction', 20), ('Bear market', 30), ('GFC-scale', 50))recorded decisionfour illustrative index shocks — 10% mild pullback, 20% correction, 30% bear market, 50% GFC-scale — applied through each holding's beta; illustrative sizes, not forecasts (recorded 2026-09-30)

Limitations

Entry points & executable coverage

Entry pointSubstrate gate
beta_shock_tableinvariant / oracle