Calibrate a candidate weight vector with a frozen temporal split, compare Brier losses, and retain the base vector whenever holdout performance does not improve.
The decision this tutorial makes visible
Hand-set weights are transparent but may be poorly calibrated; unconstrained optimization can create negative or unstable weights and leak future outcomes.
The precise question is: How can sector component weights be adjusted under nonnegativity and sum-to-one constraints while temporal holdout performance—not training fit—governs adoption?
A practitioner needs to know what the diagnostic does and does not justify. A builder needs a contract that can be reproduced from the same point-in-time inputs in Python, TypeScript, a visual, and a browser lab.
Intuition before notation
Let the training window propose a reweighting, but make the later unseen window decide whether the proposal earns admission.
The result depends on the declared algorithm scope, input clocks, units, equality and rounding policies, and unsupported-state treatment. Change one of those and the output represents a different decision even when its field name is unchanged.
Scope and nearby methods
The teaching optimizer uses chronological binary outcomes, projected gradient steps on a nonnegative simplex, ridge shrinkage toward base weights, and one fixed temporal holdout. It is a compact lab, not a production validation protocol.
| Variant | Definition | Best use | Main limitation |
|---|---|---|---|
| Selected fixed temporal holdout | Earlier train, later validation | Deterministic teaching lab | High variance and weak evidence |
| Rolling-origin evaluation | Repeated expanding-window tests | Production time-series governance | More computation and reporting |
| Expert weights only | Governed qualitative weights | Sparse outcomes | May remain poorly calibrated |
What is sourced, selected, synthetic, and derived
| Role | Material claim | Evidence | Boundary |
|---|---|---|---|
| sourced fact | Brier loss is a proper probability scoring rule with interpretation caveats. | scikit-learn calibration documentation | Brier improvement alone does not prove economic utility. |
| sourced method | Time-series evaluation preserves temporal order. | Forecasting: Principles and Practice | This package uses only one fixed holdout. |
| package choice | The candidate fits b and w by training log loss plus ridge shrinkage, projects w to the simplex after every step, and is admitted only by later-sample Brier loss. | Repository contract and Python/TypeScript implementations | The optimizer, slope, split, learning rate, penalty, and equality rule are teaching choices, not a universal calibration recipe. |
| synthetic input | Features and labels are fabricated and ordered for instruction. | canonical-input.json | They contain no issuer outcomes. |
The authoritative sources support only the exact facts named in the claim ledger. They do not certify the synthetic numbers in this tutorial. The repository fixture is deliberately invented for auditability, and the displayed output is author-derived under the selected implementation choice.
Formula, symbols, and numerical policy
s_j = sum_i(w_i x_ji)/100; p_j = 1/(1 + exp(-(b + kappa(s_j - 0.5)))); minimize mean training log loss + (lambda/2)||w - w0||^2 subject to w_i >= 0 and sum_i(w_i) = 1; select the candidate only when validation Brier_candidate <= validation Brier_base.
| Symbol | Meaning | Unit | Policy |
|---|---|---|---|
| w | candidate component weights | simplex share | Each weight is nonnegative and the vector sums to one |
| w0 | governed base-weight vector | simplex share | Ridge shrinkage is measured from this frozen anchor |
| b | fitted candidate intercept | log-odds | Base comparison fixes b=0; the candidate estimates b on training rows |
| kappa | fixed logit slope | log-odds scale | Frozen before training and holdout evaluation |
| lambda | ridge penalty | penalty strength | Nonnegative and frozen before evaluation |
| eta | learning rate | step size | Positive and frozen before evaluation |
| BS | Brier score | mean squared probability error | Lower on the temporal holdout governs selection |
- Use IEEE-754 binary64 arithmetic without intermediate rounding.
- Treat percentages as decimal fractions and label percentage-point changes explicitly.
- Clamp only declared component transforms to [0,100]; never clamp raw regulatory or accounting inputs silently.
- Round only for presentation after the full component, penalty, and coverage ledger is stored.
Read the formula in the same order as the algorithm. Validate identity, ordering, units, and supported state first. Apply the selected equality and window rules second. Calculate with unrounded numeric values. Round only at the declared presentation boundary, and preserve null as a diagnostic rather than coercing it to zero.
Build the algorithm
- Freeze sector population, binary outcome label, observation clock, base weights w0, logit slope kappa, learning rate eta, and ridge penalty lambda.
- Sort chronologically and reserve the later observations as untouched validation.
- On each training row compute s_j = sum_i(w_i x_ji)/100 and p_j = sigmoid(b + kappa(s_j - 0.5)).
- Fit b and w by gradient steps on mean binary log loss plus (lambda/2)||w-w0||^2, projecting w to the nonnegative sum-to-one simplex after every step.
- Evaluate the unchanged base model (w0, b=0) and fitted candidate (w, b) on the same later holdout with mean Brier loss.
- Select the candidate only when its holdout Brier is no greater than the base holdout Brier; otherwise publish the base vector and zero intercept with all candidate diagnostics retained.
Production-minded operational checklist
- Resolve sector and framework before calculating.
- Freeze scoring and knowledge-cutoff timestamps.
- Reconcile each input to its filing, regulator, provider, and reporting perimeter.
- Inspect component scores, penalties, and coverage before the headline.
- Retain abstention and unsupported-scope reasons for the integrated router.
The checklist is intentionally strict: an explicit rejection is safer than a plausible output built from stale, malformed, or unsupported state.
Worked synthetic example
The canonical fixture is synthetic teaching data, not an observed control
event, customer order, or broker execution. Its primary author-derived output,
selected_validation_brier, is 0.101972489878. The complete input and output
are in datasets/canonical-input.json and datasets/expected-output.json.
The canonical fixture fits on the first twelve dated rows and evaluates the last four. It exposes base and candidate losses, weight shift, intercept, and the exact holdout decision instead of presenting fitted weights as automatically superior.
Counterfactual checkpoint
Corrupt temporal generalization. Alter only the four validation labels so the fitted relationship reverses. The output changes because Candidate training fit is unchanged, but holdout Brier can force the base weights to be retained.
The structured result retains state and diagnostics in addition to the primary number. That makes the calculation independently reviewable and prevents a partial, null, rejected, or venue-bounded outcome from being mistaken for an unqualified value.
Boundary and counterexample workbook
The playground computes every scenario at 61 deterministic parameter states.
The table uses the declared focus step and states whether that focus reproduces
the canonical fixture. The full state ledger and compressed transition
segments are in datasets/scenario-results.json.
| Scenario | Review focus | Purpose | State | Primary output | Diagnostic | Decision segments |
|---|---|---|---|---|---|---|
| Canonical driver sweep | Step 30 · canonical fixture | Move one primary causal driver through the canonical midpoint. | retain-base-weights | validation Brier 0.1020 | retain-base-weights · temporal-holdout-governs-selection | 2 |
| Adverse operating stress | Step 30 · comparison focus | Apply a controlled operating or earnings stress while retaining the framework. | retain-base-weights | validation Brier 0.0955 | retain-base-weights · temporal-holdout-governs-selection | 2 |
| Balance-sheet resilience | Step 30 · comparison focus | Vary a funding, leverage, capital, or liquidity channel. | retain-base-weights | validation Brier 0.1020 | retain-base-weights · temporal-holdout-governs-selection | 2 |
| Boundary crossing | Step 30 · comparison focus | Cross a declared review boundary and inspect equality behavior. | retain-base-weights | validation Brier 0.1020 | retain-base-weights · temporal-holdout-governs-selection | 2 |
| Evidence-quality stress | Step 30 · canonical fixture | Change evidence usability or freshness without hiding the diagnostic. | retain-base-weights | validation Brier 0.1020 | retain-base-weights · temporal-holdout-governs-selection | 2 |
| Concentration or mix | Step 30 · comparison focus | Vary concentration, mix, or component balance. | calibrated-improved | validation Brier 0.1027 | calibrated-improved · temporal-holdout-governs-selection | 2 |
| Recovery path | Step 30 · canonical fixture | Trace a recovery from an adverse state toward a resilient state. | retain-base-weights | validation Brier 0.1020 | retain-base-weights · temporal-holdout-governs-selection | 3 |
These rows are not backtest observations. They are controlled counterexamples that expose how one driver changes the state, output, or reason code while the rest of the contract stays fixed.
Visualize the boundary
Open this SVG at full size, or use the guided playground to compare the seven topic-specific canonical, boundary, policy, and failure scenarios.
The Mermaid flow answers where the selected calculation sits in the processing sequence. The SVG keeps the formula, output, decision boundary, and invariant visible together. The lab lets the reader step through the same structured states without changing the underlying definition.
Implementation walkthrough
The Python and TypeScript references begin with the same validation contract, reject malformed and unsupported state before calculation, preserve declared ordering and rounding policies, and return structured diagnostics rather than one context-free number.
The main implementation branches are:
- Candidate holdout Brier is no better — Retain base weights, because Training improvement alone does not justify adoption.
- Dates are unordered or random split is proposed — Reject the run, because Future leakage defeats the exercise.
- Sample, label, or sector regime changes — Recalibrate under a new version, because Weights are conditional on their evidence population.
Neither reference silently fetches data, mutates caller-owned inputs outside the declared engine behavior, guesses hidden state, or substitutes a provider default. Shared JSON fixtures make value, null, state, and reason-code drift visible across languages.
Testing and validation
Definition tests compare every canonical field, reject malformed state, and exercise the material boundary. Family validation recomputes every playground state from the reference function. Independent arithmetic is recorded beside the fixture rather than inferred only from implementation output.
The audit must preserve these invariants:
- framework and policy version
- point-in-time normalized input ledger
- component transform thresholds and scores
- weight vector and penalties
- coverage, state, reason, and final output
Passing the suite proves the selected package contract and Python/TypeScript parity; it does not prove empirical usefulness for a live issuer universe.
Failure modes and misuse
- The 0-100 teaching bands and default weights are package choices, not regulations, ratings, or market standards.
- Definition fidelity and code parity do not establish predictive validity, calibration, causality, fair value, or investment performance.
- Accounting frameworks, prudential regimes, business mixes, and reporting perimeters can make apparently identical ratios incomparable.
- A complete component ledger can still omit material qualitative, governance, legal, catastrophe, operational, or market risks.
Debugging order
When a result looks surprising, inspect the state in this order:
- Confirm framework and reporting perimeter
- Check units and knowledge dates
- Recalculate raw intermediates
- Inspect bounded component transforms
- Reconcile weights, penalties, coverage, and state
Evidence and historical boundary
Historical decision: not useful. A named Sector-Specific Weight Calibration case would introduce issuer identity, filing and regulator scope, restatement, data-license, and scoring-policy questions without teaching the deterministic mechanism better than a controlled synthetic record.
The primary sources are scikit-learn calibration, Time-series CV. They support the source roles listed in the research ledger, not a redistributable historical observation, a private participant decision, production conformance certification, execution-quality result, profitability claim, or prediction claim.
Summary and next topic
You can now compute and audit Sector-Specific Weight Calibration before passing its versioned output to the integrated scoring router. The learning flow is: Holding-Company Look-Through Score → Sector-Specific Weight Calibration → Unsupported-Scope and Coverage Decision. Carry the result forward only with its scope, clock, state, and evidence label.
Verified model contract — Level 1
- Selected calculation:
s_j = sum_i(w_i x_ji)/100; p_j = 1/(1 + exp(-(b + kappa(s_j - 0.5)))); minimize mean training log loss + (lambda/2)||w - w0||^2 subject to w_i >= 0 and sum_i(w_i) = 1; select the candidate only when validation Brier_candidate <= validation Brier_base. - Reproducibility boundary: the canonical fixture is synthetic; the stored input, intermediate ledger, output, and cross-language implementations define the executable example.
- Interpretation ceiling: Definition fidelity and code parity do not establish predictive validity, calibration, causality, fair value, or investment performance.
- Optimizer contract: fit the intercept and simplex-constrained weights on chronological training rows with mean binary log loss plus ridge shrinkage to the base vector; compare the fitted candidate and unchanged base model on the later holdout using mean Brier loss; adopt only when
candidate_validation_brier <= base_validation_brier.
Level 2 learning layer
How to choose this model or a nearby method
| Method | Best use | Assumption that must hold | Main limitation |
|---|---|---|---|
| Selected fixed temporal holdout | Deterministic teaching lab | One split is representative | High variance and weak evidence |
| Rolling-origin evaluation | Production time-series governance | Enough dated outcomes | More computation and reporting |
| Expert weights only | Sparse outcomes | Experts are accountable | May remain poorly calibrated |
The selected package is appropriate only when its framework tag, reporting perimeter, unit policy, and point-in-time evidence contract are all satisfied. If a stop condition in the topic decision matrix fires, use the named review, reroute, or abstention state instead of forcing a score.
Compact glossary
| Symbol | Meaning | Unit | Policy |
|---|---|---|---|
w | candidate component weights | simplex share | Each weight is nonnegative and the vector sums to one |
w0 | governed base-weight vector | simplex share | Ridge shrinkage is measured from this frozen anchor |
b | fitted candidate intercept | log-odds | Base comparison fixes b=0; the candidate estimates b on training rows |
kappa | fixed logit slope | log-odds scale | Frozen before training and holdout evaluation |
lambda | ridge penalty | penalty strength | Nonnegative and frozen before evaluation |
eta | learning rate | step size | Positive and frozen before evaluation |
BS | Brier score | mean squared probability error | Lower on the temporal holdout governs selection |
Visual asset map
| Asset | Learning job | Status |
|---|---|---|
| Article hero | See route → normalize → calculate → decide at a glance | Ready |
| System map | Connect formula, boundary, invariant, counterfactual, and output | Ready |
| Model anatomy | Follow the four-stage calculation pipeline | Ready |
| Component ledger | Reconstruct the headline from named dimensions | Ready |
| Decision boundary | Separate a computable result from a permissible interpretation | Ready |
| Evidence clock | Prevent point-in-time leakage | Ready |
| Mermaid flow | Inspect the text-native algorithm flow | Ready |
| Guided playground | Predict, reveal, sweep 427 calculations, and explain state changes | Ready |
Related concepts and continuation
- Previous family topic: D18-F10-A07
- Next family topic: D18-F10-A09
Deep visual atlas
The teaching sequence is route → normalize → calculate → decide. Use the component ledger to reconstruct the headline, the boundary map to stop unsupported interpretation, and the evidence clock to block hindsight.
Guided playground protocol
- Orient: identify the framework, active driver, and invariant.
- Predict: commit to decrease, stay flat, or increase before revealing the adjacent numeric result.
- Experiment: sweep all 61 computed states in each of seven scenarios and inspect discontinuities.
- Explain: reconcile
base and candidate train/validation Brier ledgerto the headline and apply this boundary: Unordered dates, leaked outcomes, unstable labels, or inadequate holdout evidence block adoption.
Rendered from the canonical Mermaid sources linked by this article.
Sector-Specific Weight Calibration calculation flow
This flow identifies the selected calculation stages and the structured output.
Takeaway: Calibration earns adoption on later data, not by winning its training sample.
ReferencesPrimary sources and evidence notesExpand the source trail, evidence role, and limitations behind the engineering choices.
Expand the source trail, evidence role, and limitations behind the engineering choices.
S1 — Probability calibration
- Organization or authors: scikit-learn maintainers
- Source type: Official maintained technical documentation
- Publication or effective date: Accessed 2026-08-08
- Version: 1.9 documentation
- URL or DOI: https://scikit-learn.org/stable/modules/calibration.html
- Accessed: 2026-08-09
- Jurisdiction: General statistical computing
- Supports: Brier loss is a strictly proper probabilistic scoring rule but combines reliability, resolution, and uncertainty effects.
- Limitations: The documentation does not validate this package's sector labels or constrained optimizer.
S2 — Forecasting: Principles and Practice — Time series cross-validation
- Organization or authors: Rob J Hyndman and George Athanasopoulos
- Source type: Authoritative academic textbook
- Publication or effective date: Online edition accessed 2026-08-08
- Version: 3rd edition
- URL or DOI: https://otexts.com/fpp3/tscv.html
- Accessed: 2026-08-09
- Jurisdiction: General forecasting
- Supports: Rolling-origin evaluation trains only on observations before each test observation.
- Limitations: The package uses one fixed temporal holdout for a compact deterministic teaching example.
Evidence boundary
Sources define sector measures, disclosure regimes, and methodological cautions. They do not endorse the repository's synthetic fixtures, weights, score bands, or investment use.
Full dependency-light reference implementations in both supported languages.
/** Deterministic TypeScript reference for D18-F10 sector-specific scoring. */
export type RecordValue = Record<string, any>;
const numberValue = (data: RecordValue, name: string): number => {
const value = data[name];
if (typeof value !== "number" || !Number.isFinite(value)) throw new TypeError(`${name} must be a finite number`);
return value;
};
const nonnegative = (data: RecordValue, name: string): number => {
const value = numberValue(data, name); if (value < 0) throw new RangeError(`${name} must be nonnegative`); return value;
};
const positive = (data: RecordValue, name: string): number => {
const value = numberValue(data, name); if (value <= 0) throw new RangeError(`${name} must be positive`); return value;
};
const textValue = (data: RecordValue, name: string): string => {
const value = data[name]; if (typeof value !== "string" || !value.trim()) throw new TypeError(`${name} must be a nonempty string`); return value.trim();
};
const dateValue = (value: unknown, name: string): Date => {
if (typeof value !== "string" || !/^\d{4}-\d{2}-\d{2}$/.test(value)) throw new TypeError(`${name} must be YYYY-MM-DD`);
const result = new Date(`${value}T00:00:00Z`); if (!Number.isFinite(result.getTime()) || result.toISOString().slice(0, 10) !== value) throw new RangeError(`${name} must be YYYY-MM-DD`); return result;
};
const requireFramework = (data: RecordValue, expected: string): void => { if (textValue(data, "framework") !== expected) throw new RangeError(`framework must be ${expected}`); };
const clamp = (value: number, low = 0, high = 100): number => Math.min(high, Math.max(low, value));
const higher = (value: number, weak: number, strong: number): number => { if (strong <= weak) throw new RangeError("strong threshold must exceed weak threshold"); return clamp(100 * (value - weak) / (strong - weak)); };
const lower = (value: number, strong: number, weak: number): number => { if (weak <= strong) throw new RangeError("weak threshold must exceed strong threshold"); return clamp(100 * (weak - value) / (weak - strong)); };
const centered = (value: number, center: number, fullDistance: number, zeroDistance: number): number => {
if (zeroDistance <= fullDistance) throw new RangeError("zero-score distance must exceed full-score distance");
return clamp(100 * (zeroDistance - Math.abs(value - center)) / (zeroDistance - fullDistance));
};
const weightsFor = (data: RecordValue, names: string[]): Record<string, number> => {
const raw = data.weights;
if (!raw || typeof raw !== "object" || Array.isArray(raw) || Object.keys(raw).sort().join("|") !== [...names].sort().join("|")) throw new RangeError("weights must contain exactly the declared component names");
const result = Object.fromEntries(names.map(name => [name, nonnegative(raw, name)]));
const total = Object.values(result).reduce((sum, value) => sum + value, 0);
if (Math.abs(total - 1) > 1e-9) throw new RangeError("weights must sum to 1");
return result;
};
const weightedScore = (components: Record<string, number>, weights: Record<string, number>): number => Object.entries(components).reduce((sum, [name, value]) => sum + value * weights[name], 0);
const scoreState = (score: number): string => score >= 75 ? "strong-review-band" : score >= 50 ? "mixed-review-band" : "weak-review-band";
function bankScore(data: RecordValue): RecordValue {
requireFramework(data, "basel-iii-teaching-v1");
const names = ["capital", "leverage", "short_liquidity", "stable_funding", "asset_quality", "coverage", "margin", "efficiency"];
const weights = weightsFor(data, names);
const values = {cet1_ratio: nonnegative(data,"cet1_ratio"), leverage_ratio: nonnegative(data,"leverage_ratio"), lcr: nonnegative(data,"lcr"), nsfr: nonnegative(data,"nsfr"), npl_ratio: nonnegative(data,"npl_ratio"), provision_coverage_ratio: nonnegative(data,"provision_coverage_ratio"), net_interest_margin: numberValue(data,"net_interest_margin"), cost_income_ratio: nonnegative(data,"cost_income_ratio")};
const components = {capital:higher(values.cet1_ratio,.07,.14), leverage:higher(values.leverage_ratio,.03,.06), short_liquidity:higher(values.lcr,1,1.4), stable_funding:higher(values.nsfr,1,1.3), asset_quality:lower(values.npl_ratio,.02,.08), coverage:higher(values.provision_coverage_ratio,.6,1.2), margin:higher(values.net_interest_margin,.01,.04), efficiency:lower(values.cost_income_ratio,.4,.7)};
const minimums = data.minimums; if (!minimums || typeof minimums !== "object" || Array.isArray(minimums)) throw new TypeError("minimums must be an object");
const checks: Record<string,number> = {cet1_ratio:nonnegative(minimums,"cet1_ratio"), leverage_ratio:nonnegative(minimums,"leverage_ratio"), lcr:nonnegative(minimums,"lcr"), nsfr:nonnegative(minimums,"nsfr")};
const breaches = Object.entries(checks).filter(([name, minimum]) => values[name as keyof typeof values] < minimum).map(([name])=>name);
const base = weightedScore(components, weights), penalty = 12.5 * breaches.length, score = clamp(base - penalty);
return {state:breaches.length?"prudential-floor-review":scoreState(score),method:"bank-sector-score-v1",component_scores:components,weights,base_score:base,floor_breaches:breaches,penalty,fundamental_score:score,coverage_ratio:1,reason:"package-bands-with-declared-prudential-minimums"};
}
function insuranceScore(data: RecordValue): RecordValue {
requireFramework(data,"solvency-ii-nonlife-teaching-v1");
const names=["solvency","minimum_capital","underwriting","reserve_quality","own_fund_quality","concentration","profitability"], weights=weightsFor(data,names);
const values={scr_coverage_ratio:nonnegative(data,"scr_coverage_ratio"),mcr_coverage_ratio:nonnegative(data,"mcr_coverage_ratio"),combined_ratio:nonnegative(data,"combined_ratio"),adverse_reserve_development_ratio:numberValue(data,"adverse_reserve_development_ratio"),tier1_own_funds_share:nonnegative(data,"tier1_own_funds_share"),investment_concentration_ratio:nonnegative(data,"investment_concentration_ratio"),return_on_equity:numberValue(data,"return_on_equity")};
const components={solvency:higher(values.scr_coverage_ratio,1,2),minimum_capital:higher(values.mcr_coverage_ratio,1,3),underwriting:lower(values.combined_ratio,.88,1.08),reserve_quality:lower(values.adverse_reserve_development_ratio,-.02,.08),own_fund_quality:higher(values.tier1_own_funds_share,.5,.9),concentration:lower(values.investment_concentration_ratio,.1,.4),profitability:higher(values.return_on_equity,.02,.15)};
const breaches=["scr_coverage_ratio","mcr_coverage_ratio"].filter(name=>values[name as keyof typeof values]<1),base=weightedScore(components,weights),penalty=20*breaches.length,score=clamp(base-penalty);
return {state:breaches.length?"capital-requirement-review":scoreState(score),method:"insurance-sector-score-v1",component_scores:components,weights,base_score:base,floor_breaches:breaches,penalty,fundamental_score:score,coverage_ratio:1,reason:"solvency-ii-tagged-nonlife-package-bands"};
}
function reitScore(data: RecordValue): RecordValue {
requireFramework(data,"nareit-equity-reit-teaching-v1");
const names=["distribution","leverage","coverage","occupancy","same_store_growth","liquidity"],weights=weightsFor(data,names);
const ffo=numberValue(data,"nareit_ffo"),capex=nonnegative(data,"recurring_capex"),rent=numberValue(data,"straight_line_rent_adjustment"),dividends=positive(data,"common_dividends"),ebitda=positive(data,"ebitda_re"),interest=positive(data,"interest_expense"),nearDebt=positive(data,"near_term_debt_maturities");
const affo=ffo-capex-rent,distribution=affo/dividends,leverage=nonnegative(data,"net_debt")/ebitda,interestCoverage=ebitda/interest,liquidity=nonnegative(data,"available_liquidity")/nearDebt;
const components={distribution:higher(distribution,.8,1.4),leverage:lower(leverage,3.5,8),coverage:higher(interestCoverage,1.5,5),occupancy:higher(nonnegative(data,"occupancy_ratio"),.8,.97),same_store_growth:higher(numberValue(data,"same_store_noi_growth"),-.05,.08),liquidity:higher(liquidity,.75,2)};
const score=weightedScore(components,weights);
return {state:affo<=0?"affo-proxy-deficit-review":scoreState(score),method:"reit-sector-score-v1",nareit_ffo:ffo,affo_proxy:affo,distribution_coverage:distribution,net_debt_to_ebitda_re:leverage,interest_coverage:interestCoverage,liquidity_coverage:liquidity,component_scores:components,weights,fundamental_score:score,coverage_ratio:1,reason:"nareit-ffo-plus-explicit-package-affo-proxy"};
}
function utilityScore(data: RecordValue): RecordValue {
requireFramework(data,"ferc-regulated-electric-teaching-v1");
const names=["earned_return","cash_debt","capital_structure","interest_coverage","capex_funding","regulatory_lag","rate_base_growth"],weights=weightsFor(data,names);
const allowed=positive(data,"allowed_roe"),earned=numberValue(data,"earned_roe"),debt=positive(data,"total_debt"),ffoDebt=numberValue(data,"funds_from_operations")/debt,interestCoverage=numberValue(data,"ebit")/positive(data,"interest_expense"),capexFunding=numberValue(data,"cash_from_operations")/positive(data,"capital_expenditure");
const components={earned_return:centered(earned-allowed,0,.005,.04),cash_debt:higher(ffoDebt,.08,.22),capital_structure:lower(nonnegative(data,"debt_to_capital"),.4,.65),interest_coverage:higher(interestCoverage,1.5,5),capex_funding:higher(capexFunding,.4,1),regulatory_lag:lower(nonnegative(data,"regulatory_lag_months"),3,18),rate_base_growth:centered(numberValue(data,"rate_base_growth"),.05,.01,.08)};
const score=weightedScore(components,weights);
return {state:scoreState(score),method:"utility-sector-score-v1",earned_allowed_roe_gap:earned-allowed,ffo_to_debt:ffoDebt,interest_coverage:interestCoverage,capex_funding_ratio:capexFunding,component_scores:components,weights,fundamental_score:score,coverage_ratio:1,reason:"ferc-tagged-package-bands"};
}
function earlyStageScore(data: RecordValue): RecordValue {
requireFramework(data,"early-stage-liquidity-teaching-v1");
const names=["runway","burn_trend","obligation_cover","revenue_cover"],weights=weightsFor(data,names),cashFlows=data.monthly_net_cash_flows,revenues=data.monthly_revenue;
if(!Array.isArray(cashFlows)||cashFlows.length<6)throw new RangeError("monthly_net_cash_flows must contain at least 6 observations");
if(!Array.isArray(revenues)||revenues.length!==cashFlows.length)throw new RangeError("monthly_revenue must match cash-flow history");
const flows=cashFlows.map((v:any)=>numberValue({v},"v")),revs=revenues.map((v:any)=>nonnegative({v},"v"));
const unrestricted=nonnegative(data,"unrestricted_cash"),investments=nonnegative(data,"liquid_investments"),facility=nonnegative(data,"unconditionally_committed_facility"),obligations=nonnegative(data,"near_term_obligations"),minimumCash=nonnegative(data,"minimum_operating_cash");
const available=unrestricted+investments+facility-obligations-minimumCash,latest=flows.slice(-6),burn=Math.max(0,-latest.reduce((a:number,b:number)=>a+b,0)/latest.length),prior=Math.max(0,-latest.slice(0,3).reduce((a:number,b:number)=>a+b,0)/3),current=Math.max(0,-latest.slice(3).reduce((a:number,b:number)=>a+b,0)/3),burnRatio=prior>0?current/prior:(current===0?0:2),gross=unrestricted+investments+facility,obligationCover=obligations>0?gross/obligations:10,latestRevenue=revs.slice(-3).reduce((a:number,b:number)=>a+b,0)/3,revenueCover=current>0?latestRevenue/current:10,runway=burn>0?available/burn:null;
const components={runway:runway===null?100:higher(runway,6,24),burn_trend:lower(burnRatio,.75,1.25),obligation_cover:higher(obligationCover,.75,2),revenue_cover:higher(revenueCover,0,1)},score=weightedScore(components,weights);
const state=available<0?"negative-available-liquidity":runway===null?"nonburning-observation":runway<12?"funding-window-under-12-months":scoreState(score);
return {state,method:"early-stage-runway-score-v1",available_liquidity:available,monthly_burn:burn,prior_three_month_burn:prior,latest_three_month_burn:current,burn_ratio:burnRatio,runway_months:runway,obligation_coverage:obligationCover,revenue_to_burn:revenueCover,component_scores:components,weights,runway_score:score,coverage_ratio:1,reason:"historical-burn-sensitivity-not-management-forecast"};
}
const medianValue=(values:number[]):number=>{const sorted=[...values].sort((a,b)=>a-b),middle=Math.floor(sorted.length/2);return sorted.length%2?sorted[middle]:(sorted[middle-1]+sorted[middle])/2;};
function cyclicalScore(data:RecordValue):RecordValue{
requireFramework(data,"cyclical-midcycle-teaching-v1");
const names=["normalized_margin","normalized_leverage","cash_consistency","cycle_balance"],weights=weightsFor(data,names),arrays:Record<string,number[]>={};
for(const name of ["realized_price_history","unit_cost_history","volume_history","free_cash_flow_history"]){const raw=data[name];if(!Array.isArray(raw)||raw.length<7)throw new RangeError(`${name} must contain at least 7 observations`);arrays[name]=raw.map((v:any)=>numberValue({v},"v"));}
if(new Set(Object.values(arrays).map(v=>v.length)).size!==1)throw new RangeError("cycle histories must have equal length");
const prices=arrays.realized_price_history,costs=arrays.unit_cost_history,volumes=arrays.volume_history;if([...prices,...costs,...volumes].some(v=>v<=0))throw new RangeError("price, cost, and volume histories must be positive");
const normalizedPrice=medianValue(prices),normalizedCost=medianValue(costs),normalizedVolume=medianValue(volumes),revenue=normalizedPrice*normalizedVolume,ebitda=(normalizedPrice-normalizedCost)*normalizedVolume-nonnegative(data,"fixed_costs"),margin=ebitda/revenue,netDebt=nonnegative(data,"net_debt"),leverage=ebitda>0?netDebt/ebitda:null,positiveFcf=arrays.free_cash_flow_history.filter(v=>v>0).length/arrays.free_cash_flow_history.length,currentPrice=positive(data,"current_realized_price"),percentile=prices.filter(v=>v<=currentPrice).length/prices.length;
const components={normalized_margin:higher(margin,0,.3),normalized_leverage:leverage===null?0:lower(leverage,.5,4),cash_consistency:higher(positiveFcf,.3,.9),cycle_balance:centered(percentile,.5,.1,.5)},score=weightedScore(components,weights);
return {state:ebitda<=0?"nonpositive-midcycle-ebitda-review":scoreState(score),method:"cyclical-midcycle-normalization-v1",normalized_price:normalizedPrice,normalized_unit_cost:normalizedCost,normalized_volume:normalizedVolume,normalized_revenue:revenue,normalized_ebitda:ebitda,normalized_margin:margin,normalized_net_leverage:leverage,positive_fcf_ratio:positiveFcf,cycle_percentile:percentile,component_scores:components,weights,normalized_score:score,coverage_ratio:1,reason:"point-in-time-median-cycle-window"};
}
function holdingCompanyScore(data:RecordValue):RecordValue{
requireFramework(data,"holding-company-lookthrough-teaching-v1");
const names=["nav_buffer","lookthrough_leverage","parent_coverage","diversification","freshness","listed_coverage"],weights=weightsFor(data,names),holdings=data.holdings;
if(!Array.isArray(holdings)||holdings.length<2)throw new RangeError("holdings must contain at least two records");
const ids=new Set<string>(),rows=holdings.map((raw:any)=>{if(!raw||typeof raw!=="object"||Array.isArray(raw))throw new TypeError("each holding must be an object");const id=textValue(raw,"id");if(ids.has(id))throw new RangeError("holding IDs must be unique");ids.add(id);const ownership=numberValue(raw,"ownership_ratio");if(!(ownership>0&&ownership<=1))throw new RangeError("ownership_ratio must be in (0, 1]");if(typeof raw.listed!=="boolean"||typeof raw.stale!=="boolean")throw new TypeError("listed and stale must be booleans");return{id,ownership_ratio:ownership,attributable_equity_value:ownership*nonnegative(raw,"equity_value"),attributable_debt:ownership*nonnegative(raw,"debt"),attributable_dividends:ownership*nonnegative(raw,"dividends_to_parent"),listed:raw.listed,stale:raw.stale};});
const parentCash=nonnegative(data,"parent_cash"),parentDebt=nonnegative(data,"parent_debt"),other=nonnegative(data,"other_parent_liabilities"),parentInterest=positive(data,"parent_interest_expense"),stake=rows.reduce((s,r)=>s+r.attributable_equity_value,0),gav=stake+parentCash;if(gav<=0)throw new RangeError("gross asset value must be positive");
const nav=gav-parentDebt-other,attributableDebt=rows.reduce((s,r)=>s+r.attributable_debt,0),enterprise=stake+attributableDebt+parentCash,leverage=enterprise>0?(parentDebt+attributableDebt)/enterprise:1,parentCoverage=rows.reduce((s,r)=>s+r.attributable_dividends,0)/parentInterest,shares=stake>0?rows.map(r=>r.attributable_equity_value/stake):[1],concentration=shares.reduce((s,v)=>s+v*v,0),freshness=stake>0?rows.filter(r=>!r.stale).reduce((s,r)=>s+r.attributable_equity_value,0)/stake:0,listed=stake>0?rows.filter(r=>r.listed).reduce((s,r)=>s+r.attributable_equity_value,0)/stake:0,navRatio=nav/gav;
const components={nav_buffer:higher(navRatio,.2,.8),lookthrough_leverage:lower(leverage,.2,.65),parent_coverage:higher(parentCoverage,1,4),diversification:lower(concentration,.25,.7),freshness:higher(freshness,.6,1),listed_coverage:higher(listed,.3,1)},score=weightedScore(components,weights),marketCap=data.parent_market_cap===undefined?null:nonnegative(data,"parent_market_cap"),discount=marketCap===null||nav<=0?null:1-marketCap/nav;
return {state:nav<=0?"nonpositive-nav-review":scoreState(score),method:"holding-company-lookthrough-score-v1",holding_ledger:rows,gross_asset_value:gav,net_asset_value:nav,attributable_subsidiary_debt:attributableDebt,lookthrough_leverage:leverage,parent_interest_coverage:parentCoverage,concentration_hhi:concentration,fresh_value_coverage:freshness,listed_value_coverage:listed,discount_to_nav:discount,component_scores:components,weights,look_through_score:score,coverage_ratio:1,reason:"attributable-stakes-with-parent-bridge"};
}
const sigmoid=(value:number):number=>value>=0?1/(1+Math.exp(-value)):Math.exp(value)/(1+Math.exp(value));
const projectSimplex=(values:number[]):number[]=>{const ordered=[...values].sort((a,b)=>b-a);let cumulative=0,rho=0;ordered.forEach((value,index)=>{cumulative+=value;if(value-(cumulative-1)/(index+1)>0)rho=index+1;});const theta=(ordered.slice(0,rho).reduce((a,b)=>a+b,0)-1)/rho,projected=values.map(v=>Math.max(v-theta,0)),total=projected.reduce((a,b)=>a+b,0);return projected.map(v=>v/total);};
const brier=(rows:number[][],labels:number[],weights:number[],intercept:number,slope:number):number=>rows.reduce((sum,row,index)=>{const score=row.reduce((s,v,j)=>s+weights[j]*v,0)/100,p=sigmoid(intercept+slope*(score-.5));return sum+(p-labels[index])**2;},0)/rows.length;
function calibrateWeights(data:RecordValue):RecordValue{
requireFramework(data,"sector-weight-calibration-teaching-v1");
const names=data.component_names,rowsRaw=data.feature_rows,labelsRaw=data.labels,datesRaw=data.observation_dates;
if(!Array.isArray(names)||names.length<2||new Set(names).size!==names.length||!names.every((n:any)=>typeof n==="string"&&n))throw new RangeError("component_names must be unique nonempty strings");
if(!Array.isArray(rowsRaw)||rowsRaw.length<12)throw new RangeError("feature_rows must contain at least 12 observations");if(!Array.isArray(labelsRaw)||labelsRaw.length!==rowsRaw.length)throw new RangeError("labels must match feature_rows");if(!Array.isArray(datesRaw)||datesRaw.length!==rowsRaw.length)throw new RangeError("observation_dates must match feature_rows");
const rows=rowsRaw.map((row:any)=>{if(!Array.isArray(row)||row.length!==names.length)throw new RangeError("each feature row must match component_names");const values=row.map((v:any)=>numberValue({v},"v"));if(values.some((v:number)=>v<0||v>100))throw new RangeError("feature scores must be within [0, 100]");return values;});
const labels=labelsRaw.map((v:any)=>{if(v!==0&&v!==1)throw new RangeError("labels must be binary 0/1 integers");return v;});const dates=datesRaw.map((v:any)=>dateValue(v,"observation_dates"));if(dates.some((v:Date,i:number)=>i<dates.length-1&&v.getTime()>=dates[i+1].getTime()))throw new RangeError("observation_dates must be strictly increasing");
const split=Math.trunc(numberValue(data,"train_end_index"));if(split<8||split>rows.length-4)throw new RangeError("train_end_index must leave at least 8 training and 4 validation rows");const baseMap=data.base_weights;if(!baseMap||typeof baseMap!=="object"||Array.isArray(baseMap)||Object.keys(baseMap).sort().join("|")!==[...names].sort().join("|"))throw new RangeError("base_weights must match component_names");const base=names.map((n:string)=>nonnegative(baseMap,n));if(Math.abs(base.reduce((a:number,b:number)=>a+b,0)-1)>1e-9)throw new RangeError("base_weights must sum to 1");
const learningRate=positive(data,"learning_rate"),ridge=nonnegative(data,"ridge_penalty"),slope=positive(data,"logit_slope"),iterations=Math.trunc(positive(data,"iterations"));if(iterations>5000)throw new RangeError("iterations must not exceed 5000");let weights=[...base],intercept=0;const trainRows=rows.slice(0,split),validationRows=rows.slice(split),trainLabels=labels.slice(0,split),validationLabels=labels.slice(split);
for(let iteration=0;iteration<iterations;iteration++){const probabilities=trainRows.map((row:number[])=>sigmoid(intercept+slope*(row.reduce((s:number,v:number,j:number)=>s+weights[j]*v,0)/100-.5))),residuals=probabilities.map((p:number,i:number)=>p-trainLabels[i]),gradIntercept=residuals.reduce((a:number,b:number)=>a+b,0)/residuals.length,gradWeights=names.map((_:string,column:number)=>residuals.reduce((sum:number,residual:number,i:number)=>sum+residual*slope*trainRows[i][column]/100,0)/trainRows.length+ridge*(weights[column]-base[column]));intercept-=learningRate*gradIntercept;weights=projectSimplex(weights.map((v:number,i:number)=>v-learningRate*gradWeights[i]));}
const baseTrain=brier(trainRows,trainLabels,base,0,slope),baseValidation=brier(validationRows,validationLabels,base,0,slope),candidateTrain=brier(trainRows,trainLabels,weights,intercept,slope),candidateValidation=brier(validationRows,validationLabels,weights,intercept,slope),improved=candidateValidation<=baseValidation,selected=improved?weights:base,selectedIntercept=improved?intercept:0,selectedValidation=improved?candidateValidation:baseValidation;
return {state:improved?"calibrated-improved":"retain-base-weights",method:"nonnegative-simplex-logloss-v1",component_names:names,base_weights:Object.fromEntries(names.map((n:string,i:number)=>[n,base[i]])),candidate_weights:Object.fromEntries(names.map((n:string,i:number)=>[n,weights[i]])),selected_weights:Object.fromEntries(names.map((n:string,i:number)=>[n,selected[i]])),candidate_intercept:intercept,selected_intercept:selectedIntercept,base_train_brier:baseTrain,base_validation_brier:baseValidation,candidate_train_brier:candidateTrain,candidate_validation_brier:candidateValidation,selected_validation_brier:selectedValidation,weight_shift_l1:weights.reduce((s:number,v:number,i:number)=>s+Math.abs(v-base[i]),0),train_rows:trainRows.length,validation_rows:validationRows.length,coverage_ratio:1,reason:"temporal-holdout-governs-selection"};
}
const ROUTES:Record<string,[string,string,string[]]>={bank:["D18-F10-A01","basel-iii-teaching-v1",["cet1_ratio","leverage_ratio","lcr","nsfr","npl_ratio","provision_coverage_ratio","net_interest_margin","cost_income_ratio"]],insurance:["D18-F10-A02","solvency-ii-nonlife-teaching-v1",["scr_coverage_ratio","mcr_coverage_ratio","combined_ratio","adverse_reserve_development_ratio","tier1_own_funds_share","investment_concentration_ratio","return_on_equity"]],reit:["D18-F10-A03","nareit-equity-reit-teaching-v1",["nareit_ffo","recurring_capex","common_dividends","net_debt","ebitda_re","interest_expense","occupancy_ratio","same_store_noi_growth"]],utility:["D18-F10-A04","ferc-regulated-electric-teaching-v1",["allowed_roe","earned_roe","funds_from_operations","total_debt","debt_to_capital","ebit","interest_expense","capital_expenditure"]],"early-stage":["D18-F10-A05","early-stage-liquidity-teaching-v1",["unrestricted_cash","liquid_investments","monthly_net_cash_flows","near_term_obligations"]],cyclical:["D18-F10-A06","cyclical-midcycle-teaching-v1",["realized_price_history","unit_cost_history","volume_history","free_cash_flow_history","net_debt"]],"holding-company":["D18-F10-A07","holding-company-lookthrough-teaching-v1",["holdings","parent_cash","parent_debt","parent_interest_expense"]]};
function coverageDecision(data:RecordValue):RecordValue{
requireFramework(data,"coverage-router-teaching-v1");const sector=textValue(data,"sector"),candidate=textValue(data,"candidate_framework"),asOf=dateValue(data.as_of,"as_of"),minimum=numberValue(data,"minimum_coverage"),maxAge=Math.trunc(positive(data,"max_age_days"));if(!(minimum>0&&minimum<=1))throw new RangeError("minimum_coverage must be in (0, 1]");if(!ROUTES[sector])return{state:"abstain",method:"sector-coverage-router-v1",selected_model:null,coverage_ratio:0,available_fields:[],missing_fields:[],stale_fields:[],future_fields:[],reasons:["unsupported-sector"],reason:"unsupported-sector"};
const [model,expected,required]=ROUTES[sector];if(candidate!==expected)return{state:"abstain",method:"sector-coverage-router-v1",selected_model:null,coverage_ratio:0,available_fields:[],missing_fields:[...required],stale_fields:[],future_fields:[],reasons:["framework-mismatch"],reason:"framework-mismatch"};const facts=data.facts;if(!Array.isArray(facts))throw new TypeError("facts must be an array");const ledger:Record<string,any>={};for(const raw of facts){if(!raw||typeof raw!=="object"||Array.isArray(raw))throw new TypeError("each fact must be an object");const name=textValue(raw,"name");if(ledger[name])throw new RangeError("fact names must be unique");if(typeof raw.value_present!=="boolean")throw new TypeError("value_present must be boolean");ledger[name]=raw;}
const available:string[]=[],missing:string[]=[],stale:string[]=[],future:string[]=[];for(const name of required){const fact=ledger[name];if(!fact||!fact.value_present){missing.push(name);continue;}const knowledge=dateValue(fact.knowledge_date,`${name}.knowledge_date`),periodEnd=dateValue(fact.period_end,`${name}.period_end`);if(knowledge.getTime()>asOf.getTime())future.push(name);else if((asOf.getTime()-periodEnd.getTime())/86400000>maxAge)stale.push(name);else available.push(name);}const coverage=available.length/required.length,reasons:string[]=[];if(future.length)reasons.push("future-evidence");if(stale.length)reasons.push("stale-evidence");if(missing.length)reasons.push("missing-required-fields");let state:string,selected:string|null;if(future.length){state="abstain";selected=null;}else if(coverage===1){state="supported";selected=model;}else if(coverage>=minimum){state="partial-review";selected=model;}else{state="abstain";selected=null;reasons.push("coverage-below-minimum");}if(!reasons.length)reasons.push("complete-current-coverage");return{state,method:"sector-coverage-router-v1",selected_model:selected,coverage_ratio:coverage,available_fields:available,missing_fields:missing,stale_fields:stale,future_fields:future,required_field_count:required.length,reasons,reason:reasons[0]};
}
export function calculate(topicId:string,data:RecordValue):RecordValue{
if(!data||typeof data!=="object"||Array.isArray(data))throw new TypeError("input must be an object");
const functions:Record<string,(value:RecordValue)=>RecordValue>={"D18-F10-A01":bankScore,"D18-F10-A02":insuranceScore,"D18-F10-A03":reitScore,"D18-F10-A04":utilityScore,"D18-F10-A05":earlyStageScore,"D18-F10-A06":cyclicalScore,"D18-F10-A07":holdingCompanyScore,"D18-F10-A08":calibrateWeights,"D18-F10-A09":coverageDecision};
if(!functions[topicId])throw new RangeError(`unsupported topic ID: ${topicId}`);return functions[topicId](data);
}
The embedded lab now expands to its full document height, keeping the article as the only scroll surface.