alphacouncil-agent 0.9.2 → 1.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude/commands/alpha.md +22 -3
- package/.claude-plugin/marketplace.json +3 -3
- package/.claude-plugin/plugin.json +2 -2
- package/.codex-plugin/plugin.json +4 -4
- package/.grok/commands/alpha.md +22 -3
- package/.opencode/command/alpha.md +22 -3
- package/AGENTS.md +42 -4
- package/CHANGELOG.md +164 -0
- package/CLAUDE.md +59 -7
- package/README.ja.md +25 -11
- package/README.md +44 -12
- package/README.zh-CN.md +37 -12
- package/commands/alpha.md +22 -3
- package/data/authored/core-seats.mjs +808 -0
- package/data/authored/growth-seats.mjs +581 -0
- package/data/authored/index.mjs +114 -0
- package/data/authored/quant-seats.mjs +564 -0
- package/data/authored/value-seats.mjs +663 -0
- package/data/build-profile.v1.json +2 -1
- package/data/council-evaluation-protocol.v1.json +7 -4
- package/data/master-selector-method-locales.v1.mjs +296 -0
- package/data/persona-v3-ai-assisted-solo-profile.v1.json +2 -2
- package/data/persona-v3-build-specs.v1.mjs +60 -3
- package/docs/INSTALL.md +43 -6
- package/docs/personas.md +2 -1
- package/docs/plans/v1.0-roadmap.md +487 -0
- package/docs/releases/v0.9.3.md +86 -0
- package/docs/releases/v0.9.4.md +79 -0
- package/docs/releases/v0.9.5.md +96 -0
- package/docs/releases/v1.0.0.md +164 -0
- package/docs/report-contract.md +75 -2
- package/knowledge/ai-assisted-solo/experiments/runs/a.json +13 -13
- package/knowledge/ai-assisted-solo/experiments/runs/b.json +2 -2
- package/knowledge/ai-assisted-solo/experiments/runs/c.json +2 -2
- package/knowledge/ai-assisted-solo/experiments/runs/d13.json +126 -126
- package/knowledge/ai-assisted-solo/experiments/runs/d26.json +274 -254
- package/knowledge/ai-assisted-solo/experiments/runs/e-d13.json +129 -129
- package/knowledge/ai-assisted-solo/experiments/runs/e-d26.json +277 -257
- package/knowledge/ai-assisted-solo/experiments/runs/h_ai_reference.json +278 -258
- package/knowledge/ai-assisted-solo/experiments/simulation-input.json +7278 -556
- package/knowledge/ai-assisted-solo/experiments/simulation-manifest.json +22 -22
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_ackman/reviews/{change_value_bridge.ai-review.json → market_capitalisation.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_ackman/reviews/{power_map.ai-review.json → owner_earnings_yield.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_aschenbrenner/reviews/{compute_power_bridge.ai-review.json → market_capitalisation.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_aschenbrenner/reviews/{timeline_reverse_valuation.ai-review.json → price_implied_cash_yield.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_asness/reviews/{crowding_cost_stress.ai-review.json → nominal_earnings_yield_gap.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_asness/reviews/{factor_decomposer.ai-review.json → real_earnings_yield_gap.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_bogle/reviews/expected_return_over_long_bond.ai-review.json +277 -0
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_bogle/reviews/fundamental_expected_return.ai-review.json +277 -0
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_buffett/reviews/{incremental_returns.ai-review.json → market_capitalisation.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_buffett/reviews/{owner_earnings_rebuilder.ai-review.json → owner_earnings_yield.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_burry/reviews/{mispricing_carry.ai-review.json → net_current_asset_surplus_per_share.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_burry/reviews/{capital_structure_reader.ai-review.json → net_current_asset_value_per_share.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_cathie_wood/reviews/{cost_adoption_curve.ai-review.json → current_cash_yield.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_cathie_wood/reviews/{capture_valuation_bridge.ai-review.json → market_capitalisation.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_dalio/reviews/{debt_driver_stress.ai-review.json → policy_real_rate.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_dalio/reviews/{regime_classifier.ai-review.json → real_curve_slope.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_damodaran/reviews/{reverse_valuation.ai-review.json → implied_equity_risk_premium.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_damodaran/reviews/{story_dcf.ai-review.json → premium_versus_long_run_average.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_druckenmiller/reviews/{liquidity_revision_map.ai-review.json → liquidity_curve_impulse.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_druckenmiller/reviews/{inflection_payoff.ai-review.json → policy_real_rate.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_duan_yongping/reviews/{opportunity_cost.ai-review.json → five_year_opportunity_cost.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_duan_yongping/reviews/{user_value_evidence.ai-review.json → margin_retention.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_fisher/reviews/{research_productivity.ai-review.json → margin_retention.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_fisher/reviews/{scuttlebutt_graph.ai-review.json → reinvestment_spread.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_forensic_short/reviews/{borrow_catalyst.ai-review.json → cash_conversion_gap.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_forensic_short/reviews/{accounting_rebuilder.ai-review.json → cash_cover_of_leverage.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_graham/reviews/{asset_floor.ai-review.json → defensive_hurdle_base.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_graham/reviews/{normalized_earnings.ai-review.json → half_normalised_earnings_yield.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_jhunjhunwala/reviews/{penetration_liquidity.ai-review.json → cash_quality_gap.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_jhunjhunwala/reviews/{promoter_governance.ai-review.json → real_structural_growth.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_klarman/reviews/{recovery_waterfall.ai-review.json → downside_asset_coverage.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_klarman/reviews/{cash_catalyst_compare.ai-review.json → market_capitalisation.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_li_lu/reviews/{promise_integrity_ledger.ai-review.json → cash_backed_decade_return.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_li_lu/reviews/{ten_year_return_bridge.ai-review.json → decade_return_over_long_bond.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_lynch/reviews/{story_numbers_check.ai-review.json → growth_justified_capitalisation.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_lynch/reviews/{category_classifier.ai-review.json → market_capitalisation.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_marks/reviews/{cycle_temperature.ai-review.json → credit_cycle_position.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_marks/reviews/{implied_expectations.ai-review.json → equity_over_corporate_debt.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_munger/reviews/{failure_path_graph.ai-review.json → debt_service_cushion.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_munger/reviews/{incentive_map.ai-review.json → reported_earnings_cash_gap.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_natenberg/reviews/{surface_builder.ai-review.json → normalised_skew.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_natenberg/reviews/{greeks_payoff.ai-review.json → round_trip_cost.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_pabrai/reviews/{downside_floor.ai-review.json → downside_floor_coverage.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_pabrai/reviews/{discrete_payoff.ai-review.json → market_capitalisation.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_simons/reviews/{oos_cost_test.ai-review.json → absolute_session_move.ai-review.json} +42 -42
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_simons/reviews/{leakage_audit.ai-review.json → session_move_over_implied.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_sinclair/reviews/{realized_vol_forecast.ai-review.json → round_trip_cost.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_sinclair/reviews/{net_edge_sizer.ai-review.json → volatility_per_unit_of_width.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_soros/reviews/{reversal_monitor.ai-review.json → credit_cycle_position.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_soros/reviews/{reflexivity_graph.ai-review.json → financing_burden.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_taleb/reviews/{tail_friction.ai-review.json → debt_service_cushion.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_taleb/reviews/{payoff_ruin.ai-review.json → normalised_skew.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_thorp/reviews/{edge_recalculator.ai-review.json → edge_per_unit_of_volatility.ai-review.json} +46 -46
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_thorp/reviews/{fractional_kelly.ai-review.json → net_edge.ai-review.json} +49 -49
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/review-manifest.json +288 -270
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-semantic-adjudications/index.json +2 -2
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-semantic-extractions/index.json +2 -2
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-semantic-skeptic-reviews/index.json +2 -2
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-source-prereviews/index.json +6 -2
- package/knowledge/solo-test/masters/master_ackman/decision_policy.json +122 -11
- package/knowledge/solo-test/masters/master_ackman/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_ackman/manifest.json +27 -10
- package/knowledge/solo-test/masters/master_ackman/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_ackman/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_ackman/tools.json +56 -30
- package/knowledge/solo-test/masters/master_aschenbrenner/decision_policy.json +101 -10
- package/knowledge/solo-test/masters/master_aschenbrenner/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_aschenbrenner/manifest.json +26 -8
- package/knowledge/solo-test/masters/master_aschenbrenner/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_aschenbrenner/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_aschenbrenner/tools.json +56 -30
- package/knowledge/solo-test/masters/master_asness/decision_policy.json +89 -10
- package/knowledge/solo-test/masters/master_asness/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_asness/manifest.json +25 -8
- package/knowledge/solo-test/masters/master_asness/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_asness/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_asness/tools.json +56 -30
- package/knowledge/solo-test/masters/master_bogle/counterfactuals.jsonl +0 -0
- package/knowledge/solo-test/masters/master_bogle/decision_cases.jsonl +0 -0
- package/knowledge/solo-test/masters/master_bogle/decision_policy.json +170 -0
- package/knowledge/solo-test/masters/master_bogle/doctrine.jsonl +14 -0
- package/knowledge/solo-test/masters/master_bogle/evaluation/calibration_cases.jsonl +0 -0
- package/knowledge/solo-test/masters/master_bogle/evaluation/experiments.json +5 -0
- package/knowledge/solo-test/masters/master_bogle/evaluation/golden_cases.jsonl +0 -0
- package/knowledge/solo-test/masters/master_bogle/evaluation/pairwise_cases.jsonl +0 -0
- package/knowledge/solo-test/masters/master_bogle/failures.jsonl +0 -0
- package/knowledge/solo-test/masters/master_bogle/manifest.json +141 -0
- package/knowledge/solo-test/masters/master_bogle/memory_policy.json +5 -0
- package/knowledge/solo-test/masters/master_bogle/provisional-index.json +41 -0
- package/knowledge/solo-test/masters/master_bogle/research_policy.json +11 -0
- package/knowledge/solo-test/masters/master_bogle/sources.jsonl +1 -0
- package/knowledge/solo-test/masters/master_bogle/tools.json +120 -0
- package/knowledge/solo-test/masters/master_bogle/voice.en.md +7 -0
- package/knowledge/solo-test/masters/master_bogle/voice.zh.md +7 -0
- package/knowledge/solo-test/masters/master_buffett/decision_policy.json +102 -11
- package/knowledge/solo-test/masters/master_buffett/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_buffett/manifest.json +24 -8
- package/knowledge/solo-test/masters/master_buffett/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_buffett/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_buffett/tools.json +55 -29
- package/knowledge/solo-test/masters/master_burry/decision_policy.json +102 -12
- package/knowledge/solo-test/masters/master_burry/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_burry/manifest.json +26 -10
- package/knowledge/solo-test/masters/master_burry/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_burry/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_burry/tools.json +56 -30
- package/knowledge/solo-test/masters/master_cathie_wood/decision_policy.json +74 -12
- package/knowledge/solo-test/masters/master_cathie_wood/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_cathie_wood/manifest.json +26 -10
- package/knowledge/solo-test/masters/master_cathie_wood/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_cathie_wood/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_cathie_wood/tools.json +56 -30
- package/knowledge/solo-test/masters/master_dalio/decision_policy.json +123 -16
- package/knowledge/solo-test/masters/master_dalio/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_dalio/manifest.json +24 -8
- package/knowledge/solo-test/masters/master_dalio/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_dalio/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_dalio/tools.json +56 -30
- package/knowledge/solo-test/masters/master_damodaran/decision_policy.json +58 -10
- package/knowledge/solo-test/masters/master_damodaran/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_damodaran/manifest.json +23 -8
- package/knowledge/solo-test/masters/master_damodaran/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_damodaran/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_damodaran/tools.json +54 -28
- package/knowledge/solo-test/masters/master_druckenmiller/decision_policy.json +89 -10
- package/knowledge/solo-test/masters/master_druckenmiller/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_druckenmiller/manifest.json +25 -8
- package/knowledge/solo-test/masters/master_druckenmiller/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_druckenmiller/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_druckenmiller/tools.json +55 -29
- package/knowledge/solo-test/masters/master_duan_yongping/decision_policy.json +138 -13
- package/knowledge/solo-test/masters/master_duan_yongping/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_duan_yongping/manifest.json +31 -11
- package/knowledge/solo-test/masters/master_duan_yongping/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_duan_yongping/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_duan_yongping/tools.json +82 -30
- package/knowledge/solo-test/masters/master_fisher/decision_policy.json +102 -11
- package/knowledge/solo-test/masters/master_fisher/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_fisher/manifest.json +27 -10
- package/knowledge/solo-test/masters/master_fisher/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_fisher/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_fisher/tools.json +58 -32
- package/knowledge/solo-test/masters/master_forensic_short/decision_policy.json +93 -14
- package/knowledge/solo-test/masters/master_forensic_short/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_forensic_short/manifest.json +20 -8
- package/knowledge/solo-test/masters/master_forensic_short/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_forensic_short/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_forensic_short/tools.json +53 -27
- package/knowledge/solo-test/masters/master_graham/decision_policy.json +103 -12
- package/knowledge/solo-test/masters/master_graham/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_graham/manifest.json +26 -8
- package/knowledge/solo-test/masters/master_graham/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_graham/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_graham/tools.json +67 -28
- package/knowledge/solo-test/masters/master_jhunjhunwala/decision_policy.json +77 -12
- package/knowledge/solo-test/masters/master_jhunjhunwala/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_jhunjhunwala/manifest.json +25 -10
- package/knowledge/solo-test/masters/master_jhunjhunwala/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_jhunjhunwala/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_jhunjhunwala/tools.json +54 -28
- package/knowledge/solo-test/masters/master_klarman/decision_policy.json +105 -14
- package/knowledge/solo-test/masters/master_klarman/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_klarman/manifest.json +22 -6
- package/knowledge/solo-test/masters/master_klarman/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_klarman/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_klarman/tools.json +55 -29
- package/knowledge/solo-test/masters/master_li_lu/decision_policy.json +101 -10
- package/knowledge/solo-test/masters/master_li_lu/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_li_lu/manifest.json +27 -10
- package/knowledge/solo-test/masters/master_li_lu/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_li_lu/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_li_lu/tools.json +58 -32
- package/knowledge/solo-test/masters/master_lynch/decision_policy.json +127 -16
- package/knowledge/solo-test/masters/master_lynch/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_lynch/manifest.json +28 -10
- package/knowledge/solo-test/masters/master_lynch/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_lynch/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_lynch/tools.json +67 -28
- package/knowledge/solo-test/masters/master_marks/decision_policy.json +71 -9
- package/knowledge/solo-test/masters/master_marks/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_marks/manifest.json +25 -10
- package/knowledge/solo-test/masters/master_marks/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_marks/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_marks/tools.json +54 -28
- package/knowledge/solo-test/masters/master_munger/decision_policy.json +141 -16
- package/knowledge/solo-test/masters/master_munger/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_munger/manifest.json +24 -10
- package/knowledge/solo-test/masters/master_munger/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_munger/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_munger/tools.json +52 -26
- package/knowledge/solo-test/masters/master_natenberg/decision_policy.json +59 -11
- package/knowledge/solo-test/masters/master_natenberg/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_natenberg/manifest.json +23 -10
- package/knowledge/solo-test/masters/master_natenberg/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_natenberg/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_natenberg/tools.json +50 -24
- package/knowledge/solo-test/masters/master_pabrai/decision_policy.json +125 -14
- package/knowledge/solo-test/masters/master_pabrai/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_pabrai/manifest.json +22 -6
- package/knowledge/solo-test/masters/master_pabrai/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_pabrai/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_pabrai/tools.json +55 -29
- package/knowledge/solo-test/masters/master_simons/decision_policy.json +59 -11
- package/knowledge/solo-test/masters/master_simons/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_simons/manifest.json +22 -8
- package/knowledge/solo-test/masters/master_simons/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_simons/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_simons/tools.json +43 -30
- package/knowledge/solo-test/masters/master_sinclair/decision_policy.json +59 -11
- package/knowledge/solo-test/masters/master_sinclair/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_sinclair/manifest.json +22 -10
- package/knowledge/solo-test/masters/master_sinclair/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_sinclair/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_sinclair/tools.json +54 -28
- package/knowledge/solo-test/masters/master_soros/decision_policy.json +98 -10
- package/knowledge/solo-test/masters/master_soros/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_soros/manifest.json +24 -11
- package/knowledge/solo-test/masters/master_soros/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_soros/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_soros/tools.json +54 -28
- package/knowledge/solo-test/masters/master_taleb/decision_policy.json +124 -13
- package/knowledge/solo-test/masters/master_taleb/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_taleb/manifest.json +23 -9
- package/knowledge/solo-test/masters/master_taleb/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_taleb/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_taleb/tools.json +50 -24
- package/knowledge/solo-test/masters/master_thorp/decision_policy.json +61 -13
- package/knowledge/solo-test/masters/master_thorp/doctrine.jsonl +2 -2
- package/knowledge/solo-test/masters/master_thorp/manifest.json +25 -10
- package/knowledge/solo-test/masters/master_thorp/provisional-index.json +3 -3
- package/knowledge/solo-test/masters/master_thorp/sources.jsonl +1 -1
- package/knowledge/solo-test/masters/master_thorp/tools.json +56 -30
- package/knowledge/solo-test/persona-v3-solo-test-formulas/compilation-manifest.json +290 -264
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_ackman/components/tools.json +58 -32
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_ackman/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_ackman/provisional-derivations/owner_earnings_yield.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_aschenbrenner/components/tools.json +58 -32
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_aschenbrenner/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_aschenbrenner/provisional-derivations/price_implied_cash_yield.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_asness/components/tools.json +58 -32
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_asness/provisional-derivations/nominal_earnings_yield_gap.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_asness/provisional-derivations/real_earnings_yield_gap.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_bogle/components/tools.json +120 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_bogle/provisional-derivations/expected_return_over_long_bond.derived-proxy-evidence.json +77 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_bogle/provisional-derivations/fundamental_expected_return.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_buffett/components/tools.json +57 -31
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_buffett/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_buffett/provisional-derivations/owner_earnings_yield.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_burry/components/tools.json +58 -32
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_burry/provisional-derivations/net_current_asset_surplus_per_share.derived-proxy-evidence.json +77 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_burry/provisional-derivations/net_current_asset_value_per_share.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_cathie_wood/components/tools.json +58 -32
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_cathie_wood/provisional-derivations/current_cash_yield.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_cathie_wood/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_dalio/components/tools.json +58 -32
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_dalio/provisional-derivations/policy_real_rate.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_dalio/provisional-derivations/real_curve_slope.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_damodaran/components/tools.json +56 -30
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_damodaran/provisional-derivations/implied_equity_risk_premium.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_damodaran/provisional-derivations/{reverse_valuation.derived-proxy-evidence.json → premium_versus_long_run_average.derived-proxy-evidence.json} +29 -16
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_druckenmiller/components/tools.json +57 -31
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_druckenmiller/provisional-derivations/liquidity_curve_impulse.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_druckenmiller/provisional-derivations/policy_real_rate.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_duan_yongping/components/tools.json +84 -32
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_duan_yongping/provisional-derivations/five_year_opportunity_cost.derived-proxy-evidence.json +102 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_duan_yongping/provisional-derivations/margin_retention.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_fisher/components/tools.json +60 -34
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_fisher/provisional-derivations/margin_retention.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_fisher/provisional-derivations/reinvestment_spread.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_forensic_short/components/tools.json +55 -29
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_forensic_short/provisional-derivations/{accounting_rebuilder.derived-proxy-evidence.json → cash_conversion_gap.derived-proxy-evidence.json} +28 -15
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_forensic_short/provisional-derivations/cash_cover_of_leverage.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_graham/components/tools.json +69 -30
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_graham/provisional-derivations/defensive_hurdle_base.derived-proxy-evidence.json +89 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_graham/provisional-derivations/half_normalised_earnings_yield.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_jhunjhunwala/components/tools.json +56 -30
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_jhunjhunwala/provisional-derivations/cash_quality_gap.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_jhunjhunwala/provisional-derivations/real_structural_growth.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_klarman/components/tools.json +57 -31
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_klarman/provisional-derivations/downside_asset_coverage.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_klarman/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_li_lu/components/tools.json +60 -34
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_li_lu/provisional-derivations/cash_backed_decade_return.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_li_lu/provisional-derivations/decade_return_over_long_bond.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_lynch/components/tools.json +69 -30
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_lynch/provisional-derivations/growth_justified_capitalisation.derived-proxy-evidence.json +89 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_lynch/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_marks/components/tools.json +56 -30
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_marks/provisional-derivations/credit_cycle_position.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_marks/provisional-derivations/equity_over_corporate_debt.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_munger/components/tools.json +54 -28
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_munger/provisional-derivations/debt_service_cushion.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_munger/provisional-derivations/reported_earnings_cash_gap.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_natenberg/components/tools.json +52 -26
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_natenberg/provisional-derivations/{surface_builder.derived-proxy-evidence.json → normalised_skew.derived-proxy-evidence.json} +27 -14
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_natenberg/provisional-derivations/round_trip_cost.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_pabrai/components/tools.json +57 -31
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_pabrai/provisional-derivations/downside_floor_coverage.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_pabrai/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_simons/components/tools.json +45 -32
- package/knowledge/solo-test/persona-v3-solo-test-formulas/{master_natenberg/provisional-derivations/greeks_payoff.derived-proxy-evidence.json → master_simons/provisional-derivations/absolute_session_move.derived-proxy-evidence.json} +17 -17
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_simons/provisional-derivations/session_move_over_implied.derived-proxy-evidence.json +77 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_sinclair/components/tools.json +56 -30
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_sinclair/provisional-derivations/round_trip_cost.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_sinclair/provisional-derivations/volatility_per_unit_of_width.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_soros/components/tools.json +56 -30
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_soros/provisional-derivations/credit_cycle_position.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_soros/provisional-derivations/financing_burden.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_taleb/components/tools.json +52 -26
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_taleb/provisional-derivations/debt_service_cushion.derived-proxy-evidence.json +76 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_taleb/provisional-derivations/{tail_friction.derived-proxy-evidence.json → normalised_skew.derived-proxy-evidence.json} +27 -14
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_thorp/components/tools.json +58 -32
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_thorp/provisional-derivations/edge_per_unit_of_volatility.derived-proxy-evidence.json +77 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_thorp/provisional-derivations/net_edge.derived-proxy-evidence.json +76 -0
- package/mcp/lib/breadth.mjs +122 -0
- package/mcp/lib/codex.mjs +3 -3
- package/mcp/lib/constants.mjs +76 -25
- package/mcp/lib/council-options.mjs +95 -40
- package/mcp/lib/council-selection.mjs +95 -9
- package/mcp/lib/damodaran.mjs +119 -0
- package/mcp/lib/fred.mjs +246 -0
- package/mcp/lib/fund-holdings-parsers.mjs +297 -0
- package/mcp/lib/fundamentals.mjs +801 -0
- package/mcp/lib/funds.mjs +750 -0
- package/mcp/lib/gates.mjs +149 -15
- package/mcp/lib/grounding.mjs +188 -36
- package/mcp/lib/index-aggregate.mjs +718 -0
- package/mcp/lib/insider-ownership.mjs +149 -0
- package/mcp/lib/instrument-facts.mjs +557 -0
- package/mcp/lib/instruments.mjs +191 -0
- package/mcp/lib/lang.mjs +109 -2
- package/mcp/lib/markdown.mjs +620 -138
- package/mcp/lib/master-catalog.mjs +32 -3
- package/mcp/lib/orchestrator.mjs +1121 -290
- package/mcp/lib/packets.mjs +227 -21
- package/mcp/lib/personas/engine.mjs +44 -31
- package/mcp/lib/personas-v3/compiler.mjs +2 -0
- package/mcp/lib/personas-v3/deterministic-executor.mjs +13 -2
- package/mcp/lib/personas-v3/ga-external-evidence.mjs +3 -2
- package/mcp/lib/personas-v3/ga-gate.mjs +2 -1
- package/mcp/lib/personas-v3/ga-package-evidence.mjs +5 -4
- package/mcp/lib/personas-v3/grounding-adapter.mjs +309 -2
- package/mcp/lib/personas-v3/loader.mjs +10 -7
- package/mcp/lib/personas-v3/production-root.mjs +5 -4
- package/mcp/lib/personas-v3/release-formula-evidence.mjs +14 -7
- package/mcp/lib/personas-v3/release-source-evidence.mjs +2 -1
- package/mcp/lib/personas-v3/releases.mjs +5 -4
- package/mcp/lib/personas-v3/runtime-release-integrity.mjs +6 -5
- package/mcp/lib/personas-v3/semantic-source-adjudication.mjs +2 -1
- package/mcp/lib/personas-v3/semantic-source-extraction.mjs +2 -1
- package/mcp/lib/personas-v3/semantic-source-skeptic-review.mjs +2 -1
- package/mcp/lib/personas-v3/source-acquisition.mjs +10 -3
- package/mcp/lib/personas-v3/source-review-operations.mjs +3 -2
- package/mcp/lib/personas-v3/staging.mjs +22 -3
- package/mcp/lib/prompts.mjs +119 -2
- package/mcp/lib/quotes.mjs +5 -0
- package/mcp/lib/rpc.mjs +101 -42
- package/mcp/lib/run-store.mjs +28 -3
- package/mcp/lib/sec.mjs +32 -0
- package/mcp/lib/tables.mjs +11 -0
- package/mcp/lib/voice.mjs +122 -0
- package/mcp/lib/xbrl-aggregates.mjs +153 -0
- package/mcp/lib/xbrl-series.mjs +339 -0
- package/mcp/lib/xlsx.mjs +155 -0
- package/package.json +5 -5
- package/personas/masters/masters-value/bogle.md +145 -0
- package/schemas/council-evaluation-protocol-v1.schema.json +2 -2
- package/schemas/persona-v3-ai-assisted-solo-profile-v1.schema.json +2 -2
- package/schemas/persona-v3-ai-machine-simulation-v1.schema.json +1 -1
- package/schemas/persona-v3-build-spec-v1.schema.json +3 -3
- package/schemas/persona-v3-release-formula-review-evidence-v1.schema.json +3 -3
- package/schemas/persona-v3-release-manifest-v1.schema.json +6 -6
- package/schemas/persona-v3-release-source-review-evidence-v1.schema.json +3 -3
- package/schemas/persona-v3-tool-graph-v1.schema.json +1 -1
- package/schemas/persona-v3.schema.json +37 -2
- package/schemas/source-review-batch-v1.schema.json +9 -9
- package/scripts/assemble-persona-v3-release.mjs +3 -2
- package/scripts/build-persona-v3-solo-test-packs.mjs +6 -8
- package/scripts/check-persona-v3-ga.mjs +1 -1
- package/scripts/check-persona-v3-production-candidates.mjs +1 -1
- package/scripts/compile-persona-v3-formulas.mjs +19 -18
- package/scripts/doctor.mjs +11 -9
- package/scripts/export-persona-source-review-batch.mjs +1 -1
- package/scripts/generate-persona-v3-drafts.mjs +2 -2
- package/scripts/lib/build-profile.mjs +24 -0
- package/scripts/lib/council-evaluation-protocol.mjs +7 -6
- package/scripts/lib/host-capabilities.mjs +2 -1
- package/scripts/lib/packaged-host-parity.mjs +63 -8
- package/scripts/lib/persona-v3-ai-assisted-solo-status.mjs +6 -4
- package/scripts/lib/persona-v3-ai-formula-review.mjs +6 -5
- package/scripts/lib/persona-v3-ai-machine-simulations.mjs +32 -4
- package/scripts/lib/persona-v3-build-specs.mjs +3 -2
- package/scripts/lib/persona-v3-draft-packs.mjs +2 -2
- package/scripts/lib/persona-v3-formula-pipeline.mjs +11 -10
- package/scripts/lib/persona-v3-production-candidates.mjs +10 -9
- package/scripts/lib/persona-v3-solo-formula-pipeline.mjs +182 -16
- package/scripts/lib/persona-v3-solo-test-packs.mjs +143 -16
- package/scripts/package-smoke.mjs +32 -12
- package/scripts/report-persona-corpus-gaps.mjs +4 -1
- package/scripts/review-persona-v3-solo-formulas.mjs +12 -8
- package/scripts/run-tests.mjs +23 -2
- package/scripts/scaffold-persona-v3-staging.mjs +1 -1
- package/skills/agent-skills-governance/SKILL.md +8 -2
- package/skills/alphacouncil-agent/SKILL.md +60 -9
- package/skills/public-equity-investing/SKILL.md +17 -0
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_ackman/provisional-derivations/change_value_bridge.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_ackman/provisional-derivations/power_map.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_aschenbrenner/provisional-derivations/compute_power_bridge.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_aschenbrenner/provisional-derivations/timeline_reverse_valuation.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_asness/provisional-derivations/crowding_cost_stress.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_asness/provisional-derivations/factor_decomposer.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_buffett/provisional-derivations/incremental_returns.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_buffett/provisional-derivations/owner_earnings_rebuilder.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_burry/provisional-derivations/capital_structure_reader.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_burry/provisional-derivations/mispricing_carry.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_cathie_wood/provisional-derivations/capture_valuation_bridge.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_cathie_wood/provisional-derivations/cost_adoption_curve.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_dalio/provisional-derivations/debt_driver_stress.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_dalio/provisional-derivations/regime_classifier.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_damodaran/provisional-derivations/story_dcf.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_druckenmiller/provisional-derivations/inflection_payoff.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_druckenmiller/provisional-derivations/liquidity_revision_map.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_duan_yongping/provisional-derivations/opportunity_cost.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_duan_yongping/provisional-derivations/user_value_evidence.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_fisher/provisional-derivations/research_productivity.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_fisher/provisional-derivations/scuttlebutt_graph.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_forensic_short/provisional-derivations/borrow_catalyst.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_graham/provisional-derivations/asset_floor.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_graham/provisional-derivations/normalized_earnings.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_jhunjhunwala/provisional-derivations/penetration_liquidity.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_jhunjhunwala/provisional-derivations/promoter_governance.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_klarman/provisional-derivations/cash_catalyst_compare.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_klarman/provisional-derivations/recovery_waterfall.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_li_lu/provisional-derivations/promise_integrity_ledger.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_li_lu/provisional-derivations/ten_year_return_bridge.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_lynch/provisional-derivations/category_classifier.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_lynch/provisional-derivations/story_numbers_check.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_marks/provisional-derivations/cycle_temperature.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_marks/provisional-derivations/implied_expectations.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_munger/provisional-derivations/failure_path_graph.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_munger/provisional-derivations/incentive_map.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_pabrai/provisional-derivations/discrete_payoff.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_pabrai/provisional-derivations/downside_floor.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_simons/provisional-derivations/leakage_audit.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_simons/provisional-derivations/oos_cost_test.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_sinclair/provisional-derivations/net_edge_sizer.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_sinclair/provisional-derivations/realized_vol_forecast.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_soros/provisional-derivations/reflexivity_graph.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_soros/provisional-derivations/reversal_monitor.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_taleb/provisional-derivations/payoff_ruin.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_thorp/provisional-derivations/edge_recalculator.derived-proxy-evidence.json +0 -64
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_thorp/provisional-derivations/fractional_kelly.derived-proxy-evidence.json +0 -64
|
@@ -0,0 +1,750 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* ETF holdings, and look-through aggregation of the operating companies inside them.
|
|
3
|
+
*
|
|
4
|
+
* `instruments.mjs` already refuses to send an ETF through an operating-company screen: QQQ
|
|
5
|
+
* has no revenue, no EPS and no Form 4. That correctly stops the wrong answer, but it leaves
|
|
6
|
+
* a fund with nothing to say. This module supplies the right answer instead of the missing
|
|
7
|
+
* one -- the fund's own facts, and the weighted fundamentals of what it actually owns.
|
|
8
|
+
*
|
|
9
|
+
* The justification for the second half is Buffett's 1990 letter: Berkshire's reported
|
|
10
|
+
* earnings omitted the undistributed earnings of the companies it part-owned, so he reported
|
|
11
|
+
* "look-through earnings" alongside them. An index fund is the same problem at 500x. The
|
|
12
|
+
* portfolio-level number is a WEIGHTED AGGREGATE of constituent ratios, and it is only worth
|
|
13
|
+
* anything if the coverage is stated with it.
|
|
14
|
+
*
|
|
15
|
+
* Two rules are enforced in code here rather than asked for in a prompt, because both have
|
|
16
|
+
* already produced confidently wrong output elsewhere in this pipeline:
|
|
17
|
+
*
|
|
18
|
+
* 1. Absolute currency amounts can NOT be aggregated. "ETF revenue" and "ETF EPS" are
|
|
19
|
+
* category errors -- a fund does not earn its holdings' earnings, it owns a slice of
|
|
20
|
+
* them. `lookThroughAggregate` accepts a closed whitelist of ratio metrics, so summing
|
|
21
|
+
* revenue is not a discouraged call, it is an unrepresentable one.
|
|
22
|
+
* 2. Below 50% coverage there is no aggregate, only a sample. The function returns null and
|
|
23
|
+
* a diagnostic rather than a number that reads as if it described the fund.
|
|
24
|
+
*
|
|
25
|
+
* Every issuer adapter validates the CONTENT it received rather than the status code, because
|
|
26
|
+
* three of the four endpoints here return HTTP 200 while lying (see the trap notes below).
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import { Buffer } from "node:buffer";
|
|
30
|
+
|
|
31
|
+
import { LIMITS } from "./constants.mjs";
|
|
32
|
+
import { columnIndex, unzip, xlsxRows, xmlText } from "./xlsx.mjs";
|
|
33
|
+
import { numeric } from "./fund-holdings-parsers.mjs";
|
|
34
|
+
import { linkedAbort } from "./abort.mjs";
|
|
35
|
+
import { fetchText } from "./quotes.mjs";
|
|
36
|
+
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
// Registry
|
|
39
|
+
// ---------------------------------------------------------------------------
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Ticker -> issuer + the identifier that issuer's holdings endpoint actually keys on.
|
|
43
|
+
*
|
|
44
|
+
* Each issuer keys on something different and the wrong key fails in a different way, so the
|
|
45
|
+
* identifier is stored per issuer rather than guessed from the ticker:
|
|
46
|
+
* - iShares keys on a numeric productId. The slug in the URL path is decorative.
|
|
47
|
+
* - SSGA keys on a lowercase ticker slug.
|
|
48
|
+
* - Invesco keys on CUSIP. `idType=ticker` works for QQQ alone; QQQM returns HTTP 500.
|
|
49
|
+
* - Vanguard keys on the ticker.
|
|
50
|
+
*
|
|
51
|
+
* Every CUSIP and productId below was read back from the issuer's own response, not inferred.
|
|
52
|
+
* An unknown ticker is an explicit unsupported result -- never a constructed URL.
|
|
53
|
+
*/
|
|
54
|
+
export const FUND_REGISTRY = Object.freeze({
|
|
55
|
+
IVV: { issuer: "ishares", name: "iShares Core S&P 500 ETF", product_id: "239726", cusip: "464287200" },
|
|
56
|
+
IWM: { issuer: "ishares", name: "iShares Russell 2000 ETF", product_id: "239710", cusip: "464287655" },
|
|
57
|
+
EFA: { issuer: "ishares", name: "iShares MSCI EAFE ETF", product_id: "239623", cusip: "464287465" },
|
|
58
|
+
AGG: { issuer: "ishares", name: "iShares Core U.S. Aggregate Bond ETF", product_id: "239458", cusip: "464287226" },
|
|
59
|
+
TLT: { issuer: "ishares", name: "iShares 20+ Year Treasury Bond ETF", product_id: "239454", cusip: "464287432" },
|
|
60
|
+
HYG: { issuer: "ishares", name: "iShares iBoxx $ High Yield Corporate Bond ETF", product_id: "239565", cusip: "464288513" },
|
|
61
|
+
SPY: { issuer: "ssga", name: "SPDR S&P 500 ETF Trust", slug: "spy", cusip: "78462F103" },
|
|
62
|
+
QQQ: { issuer: "invesco", name: "Invesco QQQ Trust Series 1", cusip: "46090E103" },
|
|
63
|
+
QQQM: { issuer: "invesco", name: "Invesco NASDAQ 100 ETF", cusip: "46138G649" },
|
|
64
|
+
VOO: { issuer: "vanguard", name: "Vanguard S&P 500 ETF", ticker: "VOO", cusip: "922908363" },
|
|
65
|
+
VTI: { issuer: "vanguard", name: "Vanguard Total Stock Market ETF", ticker: "VTI" },
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Issuers that publish shares outstanding daily, which is the only honest input to a flow.
|
|
70
|
+
*
|
|
71
|
+
* Vanguard and Invesco publish neither shares outstanding nor AUM on any keyless endpoint
|
|
72
|
+
* (Invesco's /nav and /profile both answer HTTP 501). A flow for those two is a gap. It is
|
|
73
|
+
* NOT a modelling problem to be solved with an estimate -- see `fundFlow`.
|
|
74
|
+
*/
|
|
75
|
+
// Imported, not merely re-exported: this module calls all six. A bare `export ... from` makes
|
|
76
|
+
// the names part of the public surface without binding them in this scope, so every fetch path
|
|
77
|
+
// threw `not defined` at runtime while the parser unit tests -- which import them directly --
|
|
78
|
+
// stayed green.
|
|
79
|
+
import {
|
|
80
|
+
checkWeightSum,
|
|
81
|
+
isoDate,
|
|
82
|
+
parseIsharesHoldingsCsv,
|
|
83
|
+
parseInvescoHoldings,
|
|
84
|
+
parseSsgaHoldingsXlsx,
|
|
85
|
+
parseVanguardHoldings,
|
|
86
|
+
} from "./fund-holdings-parsers.mjs";
|
|
87
|
+
|
|
88
|
+
export {
|
|
89
|
+
checkWeightSum,
|
|
90
|
+
isoDate,
|
|
91
|
+
parseIsharesHoldingsCsv,
|
|
92
|
+
parseInvescoHoldings,
|
|
93
|
+
parseSsgaHoldingsXlsx,
|
|
94
|
+
parseVanguardHoldings,
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
export const DAILY_SHARES_ISSUERS = Object.freeze(["ishares", "ssga"]);
|
|
98
|
+
|
|
99
|
+
/** Weights are published rounded, so the sum is never exactly 1. IVV lands at 1.0005. */
|
|
100
|
+
|
|
101
|
+
/** Below this, an aggregate describes a sample rather than the fund. Hard floor, not advice. */
|
|
102
|
+
export const MIN_LOOK_THROUGH_COVERAGE = 0.5;
|
|
103
|
+
|
|
104
|
+
/** Unknown ticker -> an explicit unsupported result. Never a guessed identifier. */
|
|
105
|
+
export function resolveFund(symbol) {
|
|
106
|
+
const key = String(symbol || "").trim().toUpperCase();
|
|
107
|
+
if (!key) return { supported: false, symbol: key, reason: "empty symbol" };
|
|
108
|
+
if (!Object.prototype.hasOwnProperty.call(FUND_REGISTRY, key)) {
|
|
109
|
+
return {
|
|
110
|
+
supported: false,
|
|
111
|
+
symbol: key,
|
|
112
|
+
reason: `${key} is not in the fund registry; holdings would have to be guessed`,
|
|
113
|
+
supported_symbols: Object.keys(FUND_REGISTRY),
|
|
114
|
+
};
|
|
115
|
+
}
|
|
116
|
+
return { supported: true, symbol: key, ...FUND_REGISTRY[key] };
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ---------------------------------------------------------------------------
|
|
120
|
+
// URL builders -- the query string is part of the correctness contract
|
|
121
|
+
// ---------------------------------------------------------------------------
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* The NEW iShares holdings path. Only the numeric productId matters; the slug segment is
|
|
125
|
+
* arbitrary and is written as `x` so nobody maintains a table of marketing slugs.
|
|
126
|
+
*
|
|
127
|
+
* The old `.../<id>.ajax?fileType=csv` path is deliberately not built anywhere in this file.
|
|
128
|
+
* It still answers HTTP 200 with `Content-Type: text/csv` and a 2.2MB HTML product page as
|
|
129
|
+
* the body, for every User-Agent tried. See `parseIsharesHoldingsCsv`.
|
|
130
|
+
*/
|
|
131
|
+
export function isharesHoldingsUrl(productId) {
|
|
132
|
+
return `https://www.ishares.com/us/products/${encodeURIComponent(productId)}/x/latest-holdings.csv`;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export function ssgaHoldingsUrl(slug) {
|
|
136
|
+
return `https://www.ssga.com/us/en/intermediary/library-content/products/fund-data/etfs/us/holdings-daily-us-en-${encodeURIComponent(slug)}.xlsx`;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Invesco's cache API. Both query parameters are load-bearing and both fail SILENTLY:
|
|
141
|
+
* - without `interval=daily` the response can serve a stale cached effectiveDate;
|
|
142
|
+
* - with `loadType=initial` it returns the first 10 holdings while `totalNumberOfHoldings`
|
|
143
|
+
* still reports the true count, so the truncation is invisible unless you compare them.
|
|
144
|
+
* `assertInvescoQuery` re-checks both so a later edit to this string cannot quietly regress.
|
|
145
|
+
*
|
|
146
|
+
* Host choice is also deliberate. `www.invesco.com` answers HTTP 406 with a zero-byte body to
|
|
147
|
+
* any Mozilla-prefixed User-Agent -- which is exactly what `fetchText` sends -- while
|
|
148
|
+
* `dng-api.invesco.com` accepts every User-Agent tested, including a full Chrome one.
|
|
149
|
+
*
|
|
150
|
+
* `dng-api` does rate-limit by IP, and it signals that with the SAME zero-byte 406. Measured:
|
|
151
|
+
* roughly eight requests in quick succession locked it out for about two minutes, for every
|
|
152
|
+
* User-Agent at once, then it recovered on its own. That collision matters because a 406 here
|
|
153
|
+
* reads like a UA problem and is not one -- see `invescoFetchHint`. Changing the UA in
|
|
154
|
+
* response to it accomplishes nothing except hiding the need to back off.
|
|
155
|
+
*/
|
|
156
|
+
export function invescoHoldingsUrl(cusip) {
|
|
157
|
+
return "https://dng-api.invesco.com/cache/v1/accounts/en_US/shareclasses/"
|
|
158
|
+
+ `${encodeURIComponent(cusip)}/holdings/fund?idType=cusip&interval=daily&productType=ETF`;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/**
|
|
162
|
+
* Turn Invesco's ambiguous 406 into the diagnosis that is actually true, so the next reader
|
|
163
|
+
* backs off instead of cycling User-Agents.
|
|
164
|
+
*/
|
|
165
|
+
export function invescoFetchHint(error) {
|
|
166
|
+
const message = String(error?.message || error);
|
|
167
|
+
if (!/HTTP 406/u.test(message)) return message;
|
|
168
|
+
return `${message} -- dng-api.invesco.com returns a zero-byte 406 when rate limiting by IP, not because of the User-Agent (it accepts every UA tested). Back off ~2 minutes and retry.`;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
export function assertInvescoQuery(url) {
|
|
172
|
+
const text = String(url || "");
|
|
173
|
+
if (!/[?&]interval=daily(&|$)/u.test(text)) {
|
|
174
|
+
throw new Error("invesco holdings URL must carry interval=daily or it may serve a stale cached date");
|
|
175
|
+
}
|
|
176
|
+
if (/[?&]loadType=/u.test(text)) {
|
|
177
|
+
throw new Error("invesco holdings URL must not carry loadType; it silently truncates to 10 holdings");
|
|
178
|
+
}
|
|
179
|
+
if (!/[?&]idType=cusip(&|$)/u.test(text)) {
|
|
180
|
+
throw new Error("invesco holdings URL must key on CUSIP; idType=ticker resolves QQQ only");
|
|
181
|
+
}
|
|
182
|
+
return text;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
const VANGUARD_PAGE_SIZE = 500;
|
|
186
|
+
const VANGUARD_MAX_PAGES = 40; // ~20k holdings; a runaway pager is a bug, not a big fund
|
|
187
|
+
|
|
188
|
+
/**
|
|
189
|
+
* Vanguard month-end holdings, paged 500 at a time.
|
|
190
|
+
*
|
|
191
|
+
* The payload's own `next` link points at `api.vanguard.com`, which is dead, so following it
|
|
192
|
+
* yields nothing and looks like a short fund. Pagination is done here against the live host.
|
|
193
|
+
*/
|
|
194
|
+
export function vanguardHoldingsUrl(ticker, start = 1, count = VANGUARD_PAGE_SIZE) {
|
|
195
|
+
const base = `https://investor.vanguard.com/investment-products/etfs/profile/api/${encodeURIComponent(ticker)}/portfolio-holding/stock`;
|
|
196
|
+
return start > 1 ? `${base}?start=${start}&count=${count}` : base;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ---------------------------------------------------------------------------
|
|
200
|
+
// Small shared helpers
|
|
201
|
+
// ---------------------------------------------------------------------------
|
|
202
|
+
|
|
203
|
+
|
|
204
|
+
// ---------------------------------------------------------------------------
|
|
205
|
+
// iShares
|
|
206
|
+
// ---------------------------------------------------------------------------
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------------------
|
|
209
|
+
// Invesco
|
|
210
|
+
// ---------------------------------------------------------------------------
|
|
211
|
+
|
|
212
|
+
// ---------------------------------------------------------------------------
|
|
213
|
+
// Vanguard
|
|
214
|
+
// ---------------------------------------------------------------------------
|
|
215
|
+
|
|
216
|
+
// ---------------------------------------------------------------------------
|
|
217
|
+
// Fetch
|
|
218
|
+
// ---------------------------------------------------------------------------
|
|
219
|
+
|
|
220
|
+
/**
|
|
221
|
+
* `fetchText` decodes the body as UTF-8, which is irreversibly lossy for a ZIP container --
|
|
222
|
+
* measured on SPY's workbook, 54,450 bytes of xlsx come back as 96,860 bytes of replacement
|
|
223
|
+
* characters. The xlsx issuer therefore needs bytes, not text. Same abort discipline.
|
|
224
|
+
*/
|
|
225
|
+
async function fetchBinary(url, timeoutMs = LIMITS.QUOTE_FETCH_MS * 2, upstreamSignal) {
|
|
226
|
+
const abort = linkedAbort(timeoutMs, upstreamSignal);
|
|
227
|
+
try {
|
|
228
|
+
const res = await fetch(url, {
|
|
229
|
+
signal: abort.signal,
|
|
230
|
+
redirect: "follow", // SSGA answers 301 before serving the workbook
|
|
231
|
+
headers: { "User-Agent": "Mozilla/5.0 (AlphaCouncil)" },
|
|
232
|
+
});
|
|
233
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
234
|
+
return Buffer.from(await res.arrayBuffer());
|
|
235
|
+
} finally {
|
|
236
|
+
abort.cleanup();
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
async function fetchVanguardPages(ticker, signal) {
|
|
241
|
+
const pages = [];
|
|
242
|
+
const urls = [];
|
|
243
|
+
let start = 1;
|
|
244
|
+
for (let i = 0; i < VANGUARD_MAX_PAGES; i += 1) {
|
|
245
|
+
const url = vanguardHoldingsUrl(ticker, start);
|
|
246
|
+
urls.push(url);
|
|
247
|
+
const page = JSON.parse(await fetchText(url, LIMITS.QUOTE_FETCH_MS * 2, signal));
|
|
248
|
+
pages.push(page);
|
|
249
|
+
const collected = pages.reduce((total, one) => total + (one?.fund?.entity?.length || 0), 0);
|
|
250
|
+
const declared = Number(page?.size);
|
|
251
|
+
if (!Number.isFinite(declared) || collected >= declared) break;
|
|
252
|
+
start += VANGUARD_PAGE_SIZE;
|
|
253
|
+
}
|
|
254
|
+
return { pages, urls };
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Fund holdings from the issuer, with the issuer's own stated date.
|
|
259
|
+
*
|
|
260
|
+
* `as_of` and `public_at` are the issuer's date, never the run time: a fund file is a dated
|
|
261
|
+
* publication, and stamping it with "now" is exactly how a month-end Vanguard file starts
|
|
262
|
+
* passing for today's portfolio.
|
|
263
|
+
*/
|
|
264
|
+
export async function fetchFundHoldings(symbol, { signal } = {}) {
|
|
265
|
+
const fund = resolveFund(symbol);
|
|
266
|
+
if (!fund.supported) {
|
|
267
|
+
return Object.freeze({
|
|
268
|
+
symbol: fund.symbol,
|
|
269
|
+
issuer: null,
|
|
270
|
+
as_of: null,
|
|
271
|
+
holdings: [],
|
|
272
|
+
source_url: null,
|
|
273
|
+
public_at: null,
|
|
274
|
+
unavailable: [`holdings unsupported: ${fund.reason}`],
|
|
275
|
+
});
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
const unavailable = [];
|
|
279
|
+
let parsed = null;
|
|
280
|
+
let sourceUrl = null;
|
|
281
|
+
try {
|
|
282
|
+
if (fund.issuer === "ishares") {
|
|
283
|
+
sourceUrl = isharesHoldingsUrl(fund.product_id);
|
|
284
|
+
parsed = parseIsharesHoldingsCsv(await fetchText(sourceUrl, LIMITS.QUOTE_FETCH_MS * 2, signal), { symbol: fund.symbol });
|
|
285
|
+
} else if (fund.issuer === "invesco") {
|
|
286
|
+
sourceUrl = assertInvescoQuery(invescoHoldingsUrl(fund.cusip));
|
|
287
|
+
parsed = parseInvescoHoldings(await fetchText(sourceUrl, LIMITS.QUOTE_FETCH_MS * 2, signal), { symbol: fund.symbol });
|
|
288
|
+
} else if (fund.issuer === "ssga") {
|
|
289
|
+
sourceUrl = ssgaHoldingsUrl(fund.slug);
|
|
290
|
+
parsed = parseSsgaHoldingsXlsx(await fetchBinary(sourceUrl, LIMITS.QUOTE_FETCH_MS * 2, signal), { symbol: fund.symbol });
|
|
291
|
+
} else if (fund.issuer === "vanguard") {
|
|
292
|
+
const walk = await fetchVanguardPages(fund.ticker, signal);
|
|
293
|
+
sourceUrl = walk.urls;
|
|
294
|
+
parsed = parseVanguardHoldings(walk.pages, { symbol: fund.symbol });
|
|
295
|
+
} else {
|
|
296
|
+
throw new Error(`no holdings adapter for issuer ${fund.issuer}`);
|
|
297
|
+
}
|
|
298
|
+
} catch (error) {
|
|
299
|
+
const detail = fund.issuer === "invesco" ? invescoFetchHint(error) : String(error?.message || error);
|
|
300
|
+
return Object.freeze({
|
|
301
|
+
symbol: fund.symbol,
|
|
302
|
+
issuer: fund.issuer,
|
|
303
|
+
as_of: null,
|
|
304
|
+
holdings: [],
|
|
305
|
+
source_url: sourceUrl,
|
|
306
|
+
public_at: null,
|
|
307
|
+
unavailable: [`${fund.issuer} holdings: ${detail}`],
|
|
308
|
+
});
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
const sumCheck = checkWeightSum(parsed.holdings);
|
|
312
|
+
if (!sumCheck.ok) unavailable.push(`${fund.issuer} ${fund.symbol}: ${sumCheck.gap}`);
|
|
313
|
+
if (!parsed.as_of) unavailable.push(`${fund.issuer} ${fund.symbol}: issuer stated no holdings date`);
|
|
314
|
+
if (parsed.shares_outstanding === null && DAILY_SHARES_ISSUERS.includes(fund.issuer)) {
|
|
315
|
+
unavailable.push(`${fund.issuer} ${fund.symbol}: shares outstanding absent from the holdings file, so no flow can be computed`);
|
|
316
|
+
}
|
|
317
|
+
if (!DAILY_SHARES_ISSUERS.includes(fund.issuer)) {
|
|
318
|
+
unavailable.push(`${fund.issuer} publishes no keyless shares-outstanding endpoint; fund flow is a gap for ${fund.symbol}`);
|
|
319
|
+
}
|
|
320
|
+
if (parsed.cadence === "month_end") {
|
|
321
|
+
unavailable.push(`${fund.issuer} ${fund.symbol}: holdings are month-end only, not daily (stated date ${parsed.as_of})`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return Object.freeze({
|
|
325
|
+
symbol: fund.symbol,
|
|
326
|
+
issuer: fund.issuer,
|
|
327
|
+
fund_name: parsed.fund_name || fund.name,
|
|
328
|
+
as_of: parsed.as_of,
|
|
329
|
+
cadence: parsed.cadence || "daily",
|
|
330
|
+
holdings: Object.freeze(parsed.holdings.map((row) => Object.freeze(row))),
|
|
331
|
+
holdings_count: parsed.holdings.length,
|
|
332
|
+
weight_sum: sumCheck.sum,
|
|
333
|
+
shares_outstanding: parsed.shares_outstanding,
|
|
334
|
+
source_url: sourceUrl,
|
|
335
|
+
// The issuer's stated date IS the publication anchor, exactly as with a FRED observation.
|
|
336
|
+
public_at: parsed.as_of ? `${parsed.as_of}T00:00:00.000Z` : null,
|
|
337
|
+
observation_date: parsed.as_of,
|
|
338
|
+
unavailable: Object.freeze(unavailable),
|
|
339
|
+
});
|
|
340
|
+
}
|
|
341
|
+
|
|
342
|
+
// ---------------------------------------------------------------------------
|
|
343
|
+
// Flow
|
|
344
|
+
// ---------------------------------------------------------------------------
|
|
345
|
+
|
|
346
|
+
/**
|
|
347
|
+
* Creation/redemption flow: the change in shares outstanding priced at NAV.
|
|
348
|
+
*
|
|
349
|
+
* Only iShares and SSGA publish shares outstanding daily. For Vanguard and Invesco this must
|
|
350
|
+
* return null. There is a tempting substitute -- infer flow from AUM change -- and it is
|
|
351
|
+
* wrong, because AUM moves with the market as well as with creations, so it reports a flow on
|
|
352
|
+
* a day with none. The gap is named instead.
|
|
353
|
+
*/
|
|
354
|
+
export function fundFlow({ sharesNow, sharesPrior, nav, issuer = null } = {}) {
|
|
355
|
+
const construction = {
|
|
356
|
+
formula: "(shares_outstanding_now - shares_outstanding_prior) * nav",
|
|
357
|
+
unit: "usd",
|
|
358
|
+
inputs: { shares_now: sharesNow ?? null, shares_prior: sharesPrior ?? null, nav: nav ?? null },
|
|
359
|
+
issuer,
|
|
360
|
+
};
|
|
361
|
+
|
|
362
|
+
if (issuer && !DAILY_SHARES_ISSUERS.includes(issuer)) {
|
|
363
|
+
return {
|
|
364
|
+
value: null,
|
|
365
|
+
construction,
|
|
366
|
+
unavailable: [`${issuer} publishes no daily shares outstanding; fund flow is unavailable and must not be estimated from AUM change`],
|
|
367
|
+
};
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const now = numeric(sharesNow);
|
|
371
|
+
const prior = numeric(sharesPrior);
|
|
372
|
+
const price = numeric(nav);
|
|
373
|
+
const missing = [];
|
|
374
|
+
if (now === null) missing.push("shares_outstanding_now");
|
|
375
|
+
if (prior === null) missing.push("shares_outstanding_prior");
|
|
376
|
+
if (price === null) missing.push("nav");
|
|
377
|
+
if (missing.length) {
|
|
378
|
+
return { value: null, construction, unavailable: [`fund flow needs ${missing.join(", ")}; no estimate is substituted`] };
|
|
379
|
+
}
|
|
380
|
+
|
|
381
|
+
return {
|
|
382
|
+
value: Number(((now - prior) * price).toFixed(2)),
|
|
383
|
+
construction: { ...construction, share_change: now - prior },
|
|
384
|
+
unavailable: [],
|
|
385
|
+
};
|
|
386
|
+
}
|
|
387
|
+
|
|
388
|
+
// ---------------------------------------------------------------------------
|
|
389
|
+
// Coverage
|
|
390
|
+
// ---------------------------------------------------------------------------
|
|
391
|
+
|
|
392
|
+
/**
|
|
393
|
+
* The shortest run of largest holdings that reaches `minWeight` of the fund.
|
|
394
|
+
*
|
|
395
|
+
* Returns what it actually reached, not what was asked for. A fund whose whole book is 0.4 of
|
|
396
|
+
* itself -- a truncated file, or a top-ten sample -- returns `reached: false` and 0.4, so the
|
|
397
|
+
* caller can decline rather than describe 40% of a portfolio as the portfolio.
|
|
398
|
+
*/
|
|
399
|
+
export function topHoldingsCoverage(holdings, { minWeight = 0.6 } = {}) {
|
|
400
|
+
const ranked = [...(holdings || [])]
|
|
401
|
+
.filter((row) => Number.isFinite(Number(row?.weight)))
|
|
402
|
+
.sort((a, b) => Number(b.weight) - Number(a.weight));
|
|
403
|
+
|
|
404
|
+
const prefix = [];
|
|
405
|
+
let coverage = 0;
|
|
406
|
+
for (const row of ranked) {
|
|
407
|
+
prefix.push(row);
|
|
408
|
+
coverage += Number(row.weight);
|
|
409
|
+
if (coverage >= minWeight) break;
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
const reached = coverage >= minWeight;
|
|
413
|
+
return {
|
|
414
|
+
holdings: prefix,
|
|
415
|
+
count: prefix.length,
|
|
416
|
+
coverage_weight: Number(coverage.toFixed(6)),
|
|
417
|
+
min_weight: minWeight,
|
|
418
|
+
reached,
|
|
419
|
+
unavailable: reached
|
|
420
|
+
? []
|
|
421
|
+
: [`top holdings reach only ${coverage.toFixed(4)} of the fund, short of the requested ${minWeight}`],
|
|
422
|
+
};
|
|
423
|
+
}
|
|
424
|
+
|
|
425
|
+
// ---------------------------------------------------------------------------
|
|
426
|
+
// Look-through aggregation
|
|
427
|
+
// ---------------------------------------------------------------------------
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* The closed set of metrics that may be look-through aggregated.
|
|
431
|
+
*
|
|
432
|
+
* Membership is the whole enforcement mechanism for "no absolute currency amounts": revenue,
|
|
433
|
+
* EPS, net income and market cap are not listed, so `lookThroughAggregate` cannot express
|
|
434
|
+
* them. There is no flag to turn that off.
|
|
435
|
+
*
|
|
436
|
+
* `aggregation` is not cosmetic. A price-per-unit-of-fundamental ratio (P/E, P/B, P/S) must be
|
|
437
|
+
* aggregated HARMONICALLY, because the portfolio ratio is total price over total earnings:
|
|
438
|
+
*
|
|
439
|
+
* portfolio P/E = sum(w_i * P_i) / sum(w_i * E_i) = 1 / sum(w_i / PE_i)
|
|
440
|
+
*
|
|
441
|
+
* which is the weighted harmonic mean of the constituent P/Es, equivalently the reciprocal of
|
|
442
|
+
* the weighted arithmetic mean of their earnings yields. Taking the arithmetic mean of P/Es
|
|
443
|
+
* instead is the classic error that produces an index P/E in the hundreds: a near-zero-
|
|
444
|
+
* earnings constituent contributes an enormous P/E to an arithmetic average, when in the real
|
|
445
|
+
* aggregate it contributes almost no earnings and a normal amount of price.
|
|
446
|
+
*
|
|
447
|
+
* Metrics that are ALREADY a rate -- a yield, a margin, a growth rate -- aggregate
|
|
448
|
+
* arithmetically, because their denominators are the weights themselves.
|
|
449
|
+
*/
|
|
450
|
+
export const LOOK_THROUGH_METRICS = Object.freeze({
|
|
451
|
+
"valuation.pe_ratio": { kind: "price_multiple", aggregation: "weighted_harmonic_mean", reciprocal_of: "valuation.earnings_yield" },
|
|
452
|
+
"valuation.pb_ratio": { kind: "price_multiple", aggregation: "weighted_harmonic_mean", reciprocal_of: "valuation.book_yield" },
|
|
453
|
+
"valuation.ps_ratio": { kind: "price_multiple", aggregation: "weighted_harmonic_mean", reciprocal_of: "valuation.sales_yield" },
|
|
454
|
+
"valuation.ev_ebitda": { kind: "price_multiple", aggregation: "weighted_harmonic_mean", reciprocal_of: null },
|
|
455
|
+
"valuation.earnings_yield": { kind: "rate", aggregation: "weighted_arithmetic_mean", reciprocal_of: null },
|
|
456
|
+
"valuation.dividend_yield": { kind: "rate", aggregation: "weighted_arithmetic_mean", reciprocal_of: null },
|
|
457
|
+
"valuation.free_cash_flow_yield": { kind: "rate", aggregation: "weighted_arithmetic_mean", reciprocal_of: null },
|
|
458
|
+
"profitability.net_margin": { kind: "rate", aggregation: "weighted_arithmetic_mean", reciprocal_of: null },
|
|
459
|
+
"profitability.gross_margin": { kind: "rate", aggregation: "weighted_arithmetic_mean", reciprocal_of: null },
|
|
460
|
+
"profitability.operating_margin": { kind: "rate", aggregation: "weighted_arithmetic_mean", reciprocal_of: null },
|
|
461
|
+
"profitability.return_on_equity": { kind: "rate", aggregation: "weighted_arithmetic_mean", reciprocal_of: null },
|
|
462
|
+
"growth.revenue_growth": { kind: "rate", aggregation: "weighted_arithmetic_mean", reciprocal_of: null },
|
|
463
|
+
"growth.earnings_growth": { kind: "rate", aggregation: "weighted_arithmetic_mean", reciprocal_of: null },
|
|
464
|
+
"leverage.debt_to_equity": { kind: "ratio", aggregation: "weighted_arithmetic_mean", reciprocal_of: null },
|
|
465
|
+
"leverage.net_debt_to_ebitda": { kind: "ratio", aggregation: "weighted_arithmetic_mean", reciprocal_of: null },
|
|
466
|
+
});
|
|
467
|
+
|
|
468
|
+
/**
|
|
469
|
+
* Named only so the refusal explains itself. These are the aggregations people actually try,
|
|
470
|
+
* and each one produces a number that reads like a fact about the fund and is not one.
|
|
471
|
+
*/
|
|
472
|
+
const ABSOLUTE_FACT_HINTS = Object.freeze([
|
|
473
|
+
"revenue", "sales", "eps", "earnings_per_share", "net_income", "profit", "ebitda",
|
|
474
|
+
"free_cash_flow", "fcf", "market_cap", "assets", "equity", "debt", "capex", "book_value",
|
|
475
|
+
]);
|
|
476
|
+
|
|
477
|
+
const looksAbsolute = (factId) => {
|
|
478
|
+
const id = String(factId || "").toLowerCase();
|
|
479
|
+
if (Object.prototype.hasOwnProperty.call(LOOK_THROUGH_METRICS, factId)) return false;
|
|
480
|
+
return ABSOLUTE_FACT_HINTS.some((hint) => id.includes(hint));
|
|
481
|
+
};
|
|
482
|
+
|
|
483
|
+
const refusal = (reason, extra = {}) => ({
|
|
484
|
+
value: null,
|
|
485
|
+
coverage_weight: null,
|
|
486
|
+
method: null,
|
|
487
|
+
constituent_count: 0,
|
|
488
|
+
inputs: [],
|
|
489
|
+
unavailable: [reason],
|
|
490
|
+
...extra,
|
|
491
|
+
});
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Weight-aggregate one ratio metric across a fund's holdings into a portfolio-level value.
|
|
495
|
+
*
|
|
496
|
+
* This is the Buffett look-through step: the fund has no fundamentals, the companies inside it
|
|
497
|
+
* do, and the portfolio number is their weighted aggregate. Four rules hold:
|
|
498
|
+
*
|
|
499
|
+
* (a) below `MIN_LOOK_THROUGH_COVERAGE` of achieved weight there is no value, only a
|
|
500
|
+
* diagnostic -- the caller gets null and the reason;
|
|
501
|
+
* (b) only whitelisted ratio metrics exist; an absolute currency amount is unrepresentable;
|
|
502
|
+
* (c) `inputs` carries every constituent that contributed, so the arithmetic is checkable
|
|
503
|
+
* by hand rather than trusted;
|
|
504
|
+
* (d) `method` states which mean was taken, because harmonic and arithmetic disagree
|
|
505
|
+
* violently on exactly the multiples people care about.
|
|
506
|
+
*
|
|
507
|
+
* Achieved coverage is computed from the constituents that actually supplied a usable fact --
|
|
508
|
+
* not from the length of the holdings list, and not from the caller's `coverageWeight`, which
|
|
509
|
+
* is recorded and cross-checked but never trusted as the answer.
|
|
510
|
+
*/
|
|
511
|
+
export function lookThroughAggregate({ holdings, perHoldingFacts, factId, coverageWeight = null } = {}) {
|
|
512
|
+
const metric = Object.prototype.hasOwnProperty.call(LOOK_THROUGH_METRICS, factId)
|
|
513
|
+
? LOOK_THROUGH_METRICS[factId]
|
|
514
|
+
: null;
|
|
515
|
+
|
|
516
|
+
if (!metric) {
|
|
517
|
+
const reason = looksAbsolute(factId)
|
|
518
|
+
? `refused: ${factId} is an absolute amount, not a ratio. A fund does not earn its holdings' earnings, so it cannot be weight-aggregated into a fund-level figure. Aggregate a ratio instead.`
|
|
519
|
+
: `refused: ${factId} is not an aggregatable ratio metric`;
|
|
520
|
+
return refusal(reason, { allowed_metrics: Object.keys(LOOK_THROUGH_METRICS) });
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
const facts = perHoldingFacts instanceof Map
|
|
524
|
+
? perHoldingFacts
|
|
525
|
+
: new Map(Object.entries(perHoldingFacts || {}));
|
|
526
|
+
|
|
527
|
+
const inputs = [];
|
|
528
|
+
const skipped = [];
|
|
529
|
+
let achieved = 0;
|
|
530
|
+
for (const row of holdings || []) {
|
|
531
|
+
const ticker = row?.ticker ? String(row.ticker).toUpperCase() : null;
|
|
532
|
+
const weight = Number(row?.weight);
|
|
533
|
+
if (!ticker || !Number.isFinite(weight) || weight <= 0) continue;
|
|
534
|
+
const value = numeric(facts.get(ticker));
|
|
535
|
+
if (value === null) continue;
|
|
536
|
+
// A zero price multiple has no reciprocal, so it cannot enter a harmonic mean at all.
|
|
537
|
+
// Negative multiples DO enter it: a loss-making constituent should subtract earnings from
|
|
538
|
+
// the aggregate, which is precisely what the reciprocal does.
|
|
539
|
+
if (metric.aggregation === "weighted_harmonic_mean" && value === 0) {
|
|
540
|
+
skipped.push(`${ticker}: ${factId} is 0 and has no reciprocal`);
|
|
541
|
+
continue;
|
|
542
|
+
}
|
|
543
|
+
inputs.push({ ticker, weight: Number(weight.toFixed(6)), value });
|
|
544
|
+
achieved += weight;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
achieved = Number(achieved.toFixed(6));
|
|
548
|
+
const declared = Number.isFinite(Number(coverageWeight)) ? Number(coverageWeight) : null;
|
|
549
|
+
|
|
550
|
+
if (!inputs.length) {
|
|
551
|
+
return refusal(`no constituent supplied ${factId}; nothing to aggregate`, { coverage_weight: 0 });
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
if (achieved < MIN_LOOK_THROUGH_COVERAGE) {
|
|
555
|
+
return refusal(
|
|
556
|
+
`look-through coverage ${achieved.toFixed(4)} is below the ${MIN_LOOK_THROUGH_COVERAGE} floor; ${inputs.length} constituents describe a sample, not the fund`,
|
|
557
|
+
{ coverage_weight: achieved, constituent_count: inputs.length, declared_coverage_weight: declared },
|
|
558
|
+
);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
const unavailable = skipped.slice();
|
|
562
|
+
if (declared !== null && achieved > declared + 1e-6) {
|
|
563
|
+
unavailable.push(`declared coverage ${declared} is below the achieved ${achieved}; the caller's coverage figure is stale`);
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
// Both means are weighted by the constituent weights and normalised by the weight actually
|
|
567
|
+
// present, so a partial-coverage aggregate is the average over what was covered rather than
|
|
568
|
+
// a value diluted toward zero by the constituents that were missing.
|
|
569
|
+
const totalWeight = inputs.reduce((sum, row) => sum + row.weight, 0);
|
|
570
|
+
const value = metric.aggregation === "weighted_harmonic_mean"
|
|
571
|
+
? totalWeight / inputs.reduce((sum, row) => sum + (row.weight / row.value), 0)
|
|
572
|
+
: inputs.reduce((sum, row) => sum + (row.weight * row.value), 0) / totalWeight;
|
|
573
|
+
|
|
574
|
+
if (!Number.isFinite(value)) {
|
|
575
|
+
return refusal(`aggregating ${factId} produced a non-finite value; the constituent set is degenerate`, {
|
|
576
|
+
coverage_weight: achieved,
|
|
577
|
+
constituent_count: inputs.length,
|
|
578
|
+
});
|
|
579
|
+
}
|
|
580
|
+
|
|
581
|
+
return {
|
|
582
|
+
value: Number(value.toFixed(6)),
|
|
583
|
+
coverage_weight: achieved,
|
|
584
|
+
declared_coverage_weight: declared,
|
|
585
|
+
method: metric.aggregation,
|
|
586
|
+
metric_kind: metric.kind,
|
|
587
|
+
fact_id: factId,
|
|
588
|
+
constituent_count: inputs.length,
|
|
589
|
+
inputs,
|
|
590
|
+
unavailable,
|
|
591
|
+
};
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
/**
|
|
595
|
+
* The reciprocal view of a price multiple, e.g. P/E -> earnings yield.
|
|
596
|
+
*
|
|
597
|
+
* Kept separate from `lookThroughAggregate` so the ordering is explicit: the harmonic mean is
|
|
598
|
+
* taken over the MULTIPLES first and inverted after. Inverting each constituent and then
|
|
599
|
+
* averaging arithmetically gives the same number, but doing it in the other order -- average
|
|
600
|
+
* the multiples arithmetically, then invert -- does not, and that is the mistake this
|
|
601
|
+
* separation exists to make visible.
|
|
602
|
+
*/
|
|
603
|
+
export function asYield(aggregate) {
|
|
604
|
+
if (!aggregate || aggregate.value === null || !aggregate.value) return null;
|
|
605
|
+
const metric = LOOK_THROUGH_METRICS[aggregate.fact_id];
|
|
606
|
+
if (!metric || metric.aggregation !== "weighted_harmonic_mean") return null;
|
|
607
|
+
return {
|
|
608
|
+
value: Number((1 / aggregate.value).toFixed(6)),
|
|
609
|
+
fact_id: metric.reciprocal_of,
|
|
610
|
+
method: `reciprocal_of_${aggregate.method}`,
|
|
611
|
+
coverage_weight: aggregate.coverage_weight,
|
|
612
|
+
constituent_count: aggregate.constituent_count,
|
|
613
|
+
};
|
|
614
|
+
}
|
|
615
|
+
|
|
616
|
+
// ---------------------------------------------------------------------------
|
|
617
|
+
// Metadata (fees / NAV / AUM)
|
|
618
|
+
// ---------------------------------------------------------------------------
|
|
619
|
+
|
|
620
|
+
export const ISHARES_SCREENER_URL = "https://www.ishares.com/us/product-screener/product-screener-v3.1.jsn"
|
|
621
|
+
+ "?dcrPath=/templatedata/config/product-screener-v3/data/en/us-ishares/ishares-product-screener-backend-config"
|
|
622
|
+
+ "&siteEntryPassthrough=true";
|
|
623
|
+
|
|
624
|
+
export const SSGA_FUNDFINDER_URL = "https://www.ssga.com/bin/v1/ssmp/fund/fundfinder"
|
|
625
|
+
+ "?country=us&language=en&role=intermediary&product=etfs&ui=fund-finder";
|
|
626
|
+
|
|
627
|
+
/**
|
|
628
|
+
* The two metadata feeds wrap every value, in two different shapes, and neither is a number:
|
|
629
|
+
* iShares sends `{d: "0.03", r: 0.03}` and SSGA sends `["0.0945%", 0.0945]`. Both carry a
|
|
630
|
+
* formatted display string beside the raw value; the raw member is the one to read, because
|
|
631
|
+
* the display string is localised and suffixed.
|
|
632
|
+
*/
|
|
633
|
+
const screenerNumber = (cell) => {
|
|
634
|
+
if (Array.isArray(cell)) return numeric(cell[1]);
|
|
635
|
+
if (cell && typeof cell === "object") return numeric(cell.r ?? cell.d);
|
|
636
|
+
return numeric(cell);
|
|
637
|
+
};
|
|
638
|
+
|
|
639
|
+
/** The raw member, uncoerced -- SSGA's dates live there already in ISO form. */
|
|
640
|
+
const screenerRaw = (cell) => {
|
|
641
|
+
if (Array.isArray(cell)) return cell[1] ?? cell[0] ?? null;
|
|
642
|
+
if (cell && typeof cell === "object") return cell.d ?? cell.r ?? null;
|
|
643
|
+
return cell ?? null;
|
|
644
|
+
};
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* AUM units are NOT the same across the two issuers and mixing them is a factor-of-a-million
|
|
648
|
+
* error: iShares reports total net assets in absolute dollars (IVV: 8.67e11) while SSGA
|
|
649
|
+
* reports millions (SPY: 785173.06, displayed "$785,173.06 M" -- all 180 SSGA funds use that
|
|
650
|
+
* suffix). Both are converted to absolute USD here and the unit is declared on the way out,
|
|
651
|
+
* for the same reason FRED's millions/billions mismatch is converted explicitly rather than
|
|
652
|
+
* assumed away.
|
|
653
|
+
*/
|
|
654
|
+
const SSGA_AUM_SCALE = 1e6;
|
|
655
|
+
|
|
656
|
+
export function parseIsharesScreener(payload, symbol) {
|
|
657
|
+
const data = typeof payload === "string" ? JSON.parse(payload) : payload;
|
|
658
|
+
const wanted = String(symbol || "").toUpperCase();
|
|
659
|
+
for (const [productId, row] of Object.entries(data || {})) {
|
|
660
|
+
if (String(row?.localExchangeTicker || "").toUpperCase() !== wanted) continue;
|
|
661
|
+
return {
|
|
662
|
+
issuer: "ishares",
|
|
663
|
+
product_id: productId,
|
|
664
|
+
fund_name: row.fundName || null,
|
|
665
|
+
cusip: row.cusip || null,
|
|
666
|
+
expense_ratio: screenerNumber(row.fees ?? row.ter),
|
|
667
|
+
expense_ratio_unit: "percent",
|
|
668
|
+
nav: screenerNumber(row.navAmount),
|
|
669
|
+
aum: screenerNumber(row.totalNetAssets),
|
|
670
|
+
aum_unit: "usd",
|
|
671
|
+
// iShares sends {d: "Jul 27, 2026", r: 20260727}; the display member is the parseable one.
|
|
672
|
+
as_of: isoDate(screenerRaw(row.navAmountAsOf)),
|
|
673
|
+
};
|
|
674
|
+
}
|
|
675
|
+
return null;
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
export function parseSsgaFundFinder(payload, symbol) {
|
|
679
|
+
const data = typeof payload === "string" ? JSON.parse(payload) : payload;
|
|
680
|
+
const rows = data?.data?.funds?.etfs?.datas;
|
|
681
|
+
if (!Array.isArray(rows)) return null;
|
|
682
|
+
const wanted = String(symbol || "").toUpperCase();
|
|
683
|
+
const row = rows.find((entry) => String(entry?.fundTicker || "").toUpperCase() === wanted);
|
|
684
|
+
if (!row) return null;
|
|
685
|
+
const aumMillions = screenerNumber(row.aum);
|
|
686
|
+
return {
|
|
687
|
+
issuer: "ssga",
|
|
688
|
+
fund_name: row.fundName || null,
|
|
689
|
+
expense_ratio: screenerNumber(row.ter),
|
|
690
|
+
expense_ratio_unit: "percent",
|
|
691
|
+
nav: screenerNumber(row.nav),
|
|
692
|
+
aum: aumMillions === null ? null : aumMillions * SSGA_AUM_SCALE,
|
|
693
|
+
aum_unit: "usd",
|
|
694
|
+
// SSGA's asOfDate arrives as ["Jul 27 2026", "2026-07-27"]: the raw member is already ISO.
|
|
695
|
+
as_of: isoDate(screenerRaw(row.asOfDate)),
|
|
696
|
+
};
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/**
|
|
700
|
+
* Fees, NAV and AUM where the issuer publishes them keylessly.
|
|
701
|
+
*
|
|
702
|
+
* Vanguard and Invesco do not: Invesco's /nav and /profile both answer HTTP 501, and neither
|
|
703
|
+
* issuer exposes shares outstanding. Those two return a named gap rather than a figure sourced
|
|
704
|
+
* from somewhere the number cannot be traced back to.
|
|
705
|
+
*/
|
|
706
|
+
export async function fetchFundMetadata(symbol, { signal } = {}) {
|
|
707
|
+
const fund = resolveFund(symbol);
|
|
708
|
+
if (!fund.supported) {
|
|
709
|
+
return Object.freeze({ symbol: fund.symbol, issuer: null, unavailable: [`metadata unsupported: ${fund.reason}`] });
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const gap = (reason) => Object.freeze({
|
|
713
|
+
symbol: fund.symbol,
|
|
714
|
+
issuer: fund.issuer,
|
|
715
|
+
expense_ratio: null,
|
|
716
|
+
nav: null,
|
|
717
|
+
aum: null,
|
|
718
|
+
source_url: null,
|
|
719
|
+
public_at: null,
|
|
720
|
+
unavailable: [reason],
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
if (fund.issuer === "vanguard" || fund.issuer === "invesco") {
|
|
724
|
+
// Fund size is recovered from the disclosed positions instead, so the missing figure here
|
|
725
|
+
// is the SHARE COUNT: two dated share counts are what a flow needs, and no arithmetic over
|
|
726
|
+
// one day's positions can reconstruct yesterday's.
|
|
727
|
+
return gap(`${fund.issuer} publishes no keyless shares-outstanding endpoint for ${fund.symbol}`
|
|
728
|
+
+ (fund.issuer === "invesco" ? " (its /nav and /profile both answer HTTP 501)" : "")
|
|
729
|
+
+ "; fund size is derived from disclosed positions, but flow needs two dated share counts");
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const url = fund.issuer === "ishares" ? ISHARES_SCREENER_URL : SSGA_FUNDFINDER_URL;
|
|
733
|
+
try {
|
|
734
|
+
const body = await fetchText(url, LIMITS.QUOTE_FETCH_MS * 3, signal);
|
|
735
|
+
const parsed = fund.issuer === "ishares"
|
|
736
|
+
? parseIsharesScreener(body, fund.symbol)
|
|
737
|
+
: parseSsgaFundFinder(body, fund.symbol);
|
|
738
|
+
if (!parsed) return gap(`${fund.issuer} metadata feed did not list ${fund.symbol}`);
|
|
739
|
+
return Object.freeze({
|
|
740
|
+
symbol: fund.symbol,
|
|
741
|
+
...parsed,
|
|
742
|
+
source_url: url,
|
|
743
|
+
public_at: parsed.as_of ? `${parsed.as_of}T00:00:00.000Z` : null,
|
|
744
|
+
observation_date: parsed.as_of,
|
|
745
|
+
unavailable: parsed.as_of ? [] : [`${fund.issuer} ${fund.symbol}: metadata carried no stated date`],
|
|
746
|
+
});
|
|
747
|
+
} catch (error) {
|
|
748
|
+
return gap(`${fund.issuer} metadata: ${String(error?.message || error)}`);
|
|
749
|
+
}
|
|
750
|
+
}
|