alphacouncil-agent 1.0.0 → 1.0.2
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-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/AGENTS.md +5 -6
- package/CHANGELOG.md +86 -0
- package/CLAUDE.md +5 -6
- package/README.ja.md +13 -12
- package/README.md +16 -16
- package/README.zh-CN.md +11 -11
- package/data/authored/core-seats.mjs +3 -3
- package/data/authored/quant-seats.mjs +2 -2
- package/data/authored/value-seats.mjs +4 -4
- package/data/build-profile.v1.json +1 -1
- package/docs/INSTALL.md +6 -10
- package/docs/plans/v1.1-basket-native-seats.md +267 -0
- package/knowledge/ai-assisted-solo/experiments/runs/a.json +10 -10
- 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 +52 -52
- package/knowledge/ai-assisted-solo/experiments/runs/d26.json +68 -68
- package/knowledge/ai-assisted-solo/experiments/runs/e-d13.json +55 -55
- package/knowledge/ai-assisted-solo/experiments/runs/e-d26.json +71 -71
- package/knowledge/ai-assisted-solo/experiments/runs/h_ai_reference.json +72 -72
- package/knowledge/ai-assisted-solo/experiments/simulation-input.json +51 -51
- package/knowledge/ai-assisted-solo/experiments/simulation-manifest.json +19 -19
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_ackman/reviews/market_capitalisation.ai-review.json +35 -35
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_ackman/reviews/owner_earnings_yield.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_aschenbrenner/reviews/market_capitalisation.ai-review.json +35 -35
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_aschenbrenner/reviews/price_implied_cash_yield.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_asness/reviews/nominal_earnings_yield_gap.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_asness/reviews/real_earnings_yield_gap.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_bogle/reviews/expected_return_over_long_bond.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_bogle/reviews/fundamental_expected_return.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_buffett/reviews/market_capitalisation.ai-review.json +35 -35
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_buffett/reviews/owner_earnings_yield.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_burry/reviews/net_current_asset_surplus_per_share.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_burry/reviews/net_current_asset_value_per_share.ai-review.json +35 -35
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_cathie_wood/reviews/current_cash_yield.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_cathie_wood/reviews/market_capitalisation.ai-review.json +35 -35
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_dalio/reviews/policy_real_rate.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_dalio/reviews/real_curve_slope.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_damodaran/reviews/implied_equity_risk_premium.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_damodaran/reviews/premium_versus_long_run_average.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_druckenmiller/reviews/liquidity_curve_impulse.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_druckenmiller/reviews/policy_real_rate.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_duan_yongping/reviews/five_year_opportunity_cost.ai-review.json +35 -35
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_duan_yongping/reviews/margin_retention.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_fisher/reviews/margin_retention.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_fisher/reviews/reinvestment_spread.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_forensic_short/reviews/cash_conversion_gap.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_forensic_short/reviews/cash_cover_of_leverage.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_graham/reviews/defensive_hurdle_base.ai-review.json +35 -35
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_graham/reviews/half_normalised_earnings_yield.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_jhunjhunwala/reviews/cash_quality_gap.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_jhunjhunwala/reviews/real_structural_growth.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_klarman/reviews/downside_asset_coverage.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_klarman/reviews/market_capitalisation.ai-review.json +35 -35
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_li_lu/reviews/cash_backed_decade_return.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_li_lu/reviews/decade_return_over_long_bond.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_lynch/reviews/growth_justified_capitalisation.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_lynch/reviews/market_capitalisation.ai-review.json +35 -35
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_marks/reviews/credit_cycle_position.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_marks/reviews/equity_over_corporate_debt.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_munger/reviews/debt_service_cushion.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_munger/reviews/reported_earnings_cash_gap.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_natenberg/reviews/normalised_skew.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_natenberg/reviews/round_trip_cost.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_pabrai/reviews/downside_floor_coverage.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_pabrai/reviews/market_capitalisation.ai-review.json +35 -35
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_simons/reviews/absolute_session_move.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_simons/reviews/session_move_over_implied.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_sinclair/reviews/round_trip_cost.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_sinclair/reviews/volatility_per_unit_of_width.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_soros/reviews/credit_cycle_position.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_soros/reviews/financing_burden.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_taleb/reviews/debt_service_cushion.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_taleb/reviews/normalised_skew.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_thorp/reviews/edge_per_unit_of_volatility.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_thorp/reviews/net_edge.ai-review.json +15 -15
- package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/review-manifest.json +164 -164
- package/knowledge/solo-test/masters/master_ackman/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_ackman/tools.json +6 -6
- package/knowledge/solo-test/masters/master_aschenbrenner/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_aschenbrenner/tools.json +6 -6
- package/knowledge/solo-test/masters/master_asness/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_bogle/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_buffett/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_buffett/tools.json +6 -6
- package/knowledge/solo-test/masters/master_burry/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_burry/tools.json +6 -6
- package/knowledge/solo-test/masters/master_cathie_wood/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_cathie_wood/tools.json +6 -6
- package/knowledge/solo-test/masters/master_dalio/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_damodaran/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_druckenmiller/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_duan_yongping/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_duan_yongping/tools.json +6 -6
- package/knowledge/solo-test/masters/master_fisher/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_forensic_short/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_graham/decision_policy.json +1 -1
- package/knowledge/solo-test/masters/master_graham/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_graham/tools.json +6 -6
- package/knowledge/solo-test/masters/master_jhunjhunwala/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_klarman/decision_policy.json +1 -1
- package/knowledge/solo-test/masters/master_klarman/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_klarman/tools.json +6 -6
- package/knowledge/solo-test/masters/master_li_lu/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_lynch/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_lynch/tools.json +6 -6
- package/knowledge/solo-test/masters/master_marks/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_munger/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_natenberg/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_pabrai/decision_policy.json +1 -1
- package/knowledge/solo-test/masters/master_pabrai/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_pabrai/tools.json +6 -6
- package/knowledge/solo-test/masters/master_simons/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_sinclair/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_soros/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_taleb/provisional-index.json +1 -1
- package/knowledge/solo-test/masters/master_thorp/provisional-index.json +1 -1
- package/knowledge/solo-test/persona-v3-solo-test-formulas/compilation-manifest.json +21 -21
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_ackman/components/tools.json +6 -6
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_ackman/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +3 -3
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_aschenbrenner/components/tools.json +6 -6
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_aschenbrenner/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +3 -3
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_buffett/components/tools.json +6 -6
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_buffett/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +3 -3
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_burry/components/tools.json +6 -6
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_burry/provisional-derivations/net_current_asset_value_per_share.derived-proxy-evidence.json +3 -3
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_cathie_wood/components/tools.json +6 -6
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_cathie_wood/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +3 -3
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_duan_yongping/components/tools.json +6 -6
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_duan_yongping/provisional-derivations/five_year_opportunity_cost.derived-proxy-evidence.json +3 -3
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_graham/components/tools.json +6 -6
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_graham/provisional-derivations/defensive_hurdle_base.derived-proxy-evidence.json +3 -3
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_klarman/components/tools.json +6 -6
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_klarman/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +3 -3
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_lynch/components/tools.json +6 -6
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_lynch/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +3 -3
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_pabrai/components/tools.json +6 -6
- package/knowledge/solo-test/persona-v3-solo-test-formulas/master_pabrai/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +3 -3
- package/mcp/lib/basket-news.mjs +181 -0
- package/mcp/lib/breadth.mjs +29 -1
- package/mcp/lib/cross-market.mjs +180 -0
- package/mcp/lib/fund-observations.mjs +152 -0
- package/mcp/lib/fundamentals.mjs +6 -1
- package/mcp/lib/funds.mjs +35 -0
- package/mcp/lib/grounding.mjs +55 -2
- package/mcp/lib/index-aggregate.mjs +16 -1
- package/mcp/lib/industry.mjs +23 -2
- package/mcp/lib/instrument-facts.mjs +318 -17
- package/mcp/lib/instruments.mjs +25 -0
- package/mcp/lib/markdown.mjs +41 -8
- package/mcp/lib/personas-v3/grounding-adapter.mjs +139 -0
- package/mcp/lib/xbrl-series.mjs +4 -2
- package/package.json +2 -2
- package/scripts/lib/persona-v3-solo-formula-pipeline.mjs +18 -1
|
@@ -13,12 +13,22 @@
|
|
|
13
13
|
*/
|
|
14
14
|
|
|
15
15
|
import { LIMITS } from "./constants.mjs";
|
|
16
|
-
import {
|
|
16
|
+
import {
|
|
17
|
+
MIN_LOOK_THROUGH_COVERAGE,
|
|
18
|
+
fetchFundHoldings,
|
|
19
|
+
fetchFundMetadata,
|
|
20
|
+
lookThroughAggregate,
|
|
21
|
+
topHoldingsCoverage,
|
|
22
|
+
} from "./funds.mjs";
|
|
17
23
|
import { fetchIndexAggregate, INDEX_PROXIES, normalizeIndexSymbol } from "./index-aggregate.mjs";
|
|
18
24
|
import { fetchImpliedErp } from "./damodaran.mjs";
|
|
19
|
-
import { fetchBasketBreadth } from "./breadth.mjs";
|
|
20
|
-
import {
|
|
21
|
-
import {
|
|
25
|
+
import { chartUrl, fetchBasketBreadth, parseDailyCloses } from "./breadth.mjs";
|
|
26
|
+
import { flowInputs, recordFundObservation } from "./fund-observations.mjs";
|
|
27
|
+
import { fundFlow } from "./funds.mjs";
|
|
28
|
+
import { fetchText } from "./quotes.mjs";
|
|
29
|
+
import { deriveFundamentals } from "./fundamentals.mjs";
|
|
30
|
+
import { evaluateRules } from "./screen.mjs";
|
|
31
|
+
import { fetchCompanyFacts, fetchUniverse } from "./sec.mjs";
|
|
22
32
|
|
|
23
33
|
/**
|
|
24
34
|
* How many constituents may be resolved for a look-through pass.
|
|
@@ -51,6 +61,15 @@ export const LOOK_THROUGH_TARGET_COVERAGE = 0.6;
|
|
|
51
61
|
export const LOOK_THROUGH_FACT_RULES = Object.freeze({
|
|
52
62
|
"financial.leverage": "leverage.debt_to_equity",
|
|
53
63
|
"valuation.revenue_growth": "growth.revenue_growth",
|
|
64
|
+
// Every one of these is a pure ratio that a weighted mean of constituents answers honestly,
|
|
65
|
+
// and every one of them was already legal to aggregate -- it simply had no line here, which
|
|
66
|
+
// is why eighteen seats stayed silent on a basket whose data was complete.
|
|
67
|
+
"accounting.cash_conversion": "accounting.cash_conversion",
|
|
68
|
+
"financial.gross_margin_5y": "profitability.gross_margin",
|
|
69
|
+
"financial.net_margin_5y": "profitability.net_margin",
|
|
70
|
+
"financial.return_on_equity_10y": "profitability.return_on_equity_10y",
|
|
71
|
+
"financial.incremental_return_on_capital": "profitability.incremental_return_on_capital",
|
|
72
|
+
"financial.interest_coverage": "coverage.interest_coverage",
|
|
54
73
|
});
|
|
55
74
|
|
|
56
75
|
/**
|
|
@@ -63,6 +82,29 @@ export const LOOK_THROUGH_FACT_RULES = Object.freeze({
|
|
|
63
82
|
* is real and worth doing; it is simply not this pass, and pretending otherwise by summing is
|
|
64
83
|
* the failure mode, not the shortcut.
|
|
65
84
|
*/
|
|
85
|
+
/**
|
|
86
|
+
* Absolute company figures a fund owns a real, computable dollar share of.
|
|
87
|
+
*
|
|
88
|
+
* `LOOK_THROUGH_BLOCKED` refuses the naive version of this and is right to: summing every
|
|
89
|
+
* constituent's owner earnings produces "the ETF's owner earnings" as though the fund owned
|
|
90
|
+
* each business outright, which it does not. What the fund does own is a stake — its position
|
|
91
|
+
* value over the company's market capitalisation — and that stake's claim on the figure is
|
|
92
|
+
* ordinary look-through accounting:
|
|
93
|
+
*
|
|
94
|
+
* fund's claim = AUM x SUM over i of ( weight_i / market_cap_i ) x figure_i
|
|
95
|
+
*
|
|
96
|
+
* The result is in dollars and is the fund's, not the index's. Because the fund's own market
|
|
97
|
+
* capitalisation is its AUM, every seat that divides one of these by market capitalisation
|
|
98
|
+
* gets the weighted look-through yield without a single change to its method.
|
|
99
|
+
*/
|
|
100
|
+
export const LOOK_THROUGH_CLAIM_FACTS = Object.freeze([
|
|
101
|
+
"financial.owner_earnings",
|
|
102
|
+
"financial.free_cash_flow_5y",
|
|
103
|
+
"financial.net_current_asset_value",
|
|
104
|
+
"valuation.downside_asset_value",
|
|
105
|
+
"valuation.downside_floor",
|
|
106
|
+
]);
|
|
107
|
+
|
|
66
108
|
export const LOOK_THROUGH_BLOCKED = Object.freeze({
|
|
67
109
|
"financial.owner_earnings": "absolute currency; needs a per-constituent market capitalisation to become a yield",
|
|
68
110
|
"financial.net_current_asset_value": "absolute currency; needs a per-constituent market capitalisation to become a ratio",
|
|
@@ -71,6 +113,35 @@ export const LOOK_THROUGH_BLOCKED = Object.freeze({
|
|
|
71
113
|
"capital_allocation.share_count": "a count, not a rate; it has no portfolio-level meaning",
|
|
72
114
|
});
|
|
73
115
|
|
|
116
|
+
/**
|
|
117
|
+
* Mechanical-screen rule id -> the pack fact id it supplies, mirroring the grounding adapter's
|
|
118
|
+
* own map. Percent rules publish 15 for 15%, and every pack ratio is a decimal.
|
|
119
|
+
*/
|
|
120
|
+
const SCREEN_RULE_FACTS = Object.freeze({
|
|
121
|
+
roe_10y: "financial.return_on_equity_10y",
|
|
122
|
+
interest_cover: "financial.interest_coverage",
|
|
123
|
+
gross_margin: "financial.gross_margin_5y",
|
|
124
|
+
ocf_over_ni: "accounting.cash_conversion",
|
|
125
|
+
net_margin: "financial.net_margin_5y",
|
|
126
|
+
// An absolute, and the only one the screen carries. It is not aggregated as a ratio -- it
|
|
127
|
+
// feeds the ownership-claim pass, where dollars are the right unit.
|
|
128
|
+
fcf_5y: "financial.free_cash_flow_5y",
|
|
129
|
+
});
|
|
130
|
+
const SCREEN_PERCENT_RULES = new Set(["roe_10y", "gross_margin", "net_margin"]);
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The unit a look-through fact must carry.
|
|
134
|
+
*
|
|
135
|
+
* The aggregate of a quantity IS that quantity, so it has to arrive under the same contract the
|
|
136
|
+
* single-company path declares. Emitting every aggregate as a bare `decimal` made the executor
|
|
137
|
+
* reject coverage and cash conversion, which are multiples -- the fact was present and unusable,
|
|
138
|
+
* which reads exactly like the fact being absent.
|
|
139
|
+
*/
|
|
140
|
+
const LOOK_THROUGH_UNITS = Object.freeze({
|
|
141
|
+
"financial.interest_coverage": "multiple",
|
|
142
|
+
"accounting.cash_conversion": "multiple",
|
|
143
|
+
});
|
|
144
|
+
|
|
74
145
|
export const LOOK_THROUGH_FACT_IDS = Object.freeze(Object.keys(LOOK_THROUGH_FACT_RULES));
|
|
75
146
|
|
|
76
147
|
const finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
@@ -241,11 +312,15 @@ export async function resolveConstituentFacts(holdings, { signal, asOf = null, c
|
|
|
241
312
|
// The aggregate inherits whatever span its inputs covered; without carrying it the basket
|
|
242
313
|
// fact arrives as a bare number that no duration contract can accept.
|
|
243
314
|
const perHoldingPeriods = new Map();
|
|
315
|
+
// A share count is not aggregable and is not aggregated. It is kept because a market
|
|
316
|
+
// capitalisation -- shares times price -- is what turns an absolute figure into the fund's
|
|
317
|
+
// own dollar claim on it.
|
|
318
|
+
const shareCounts = new Map();
|
|
244
319
|
const unavailable = [];
|
|
245
320
|
const ranked = [...holdings]
|
|
246
321
|
.sort((left, right) => (right.weight || 0) - (left.weight || 0))
|
|
247
322
|
.slice(0, LOOK_THROUGH_MAX_CONSTITUENTS);
|
|
248
|
-
if (!ranked.length) return { perHolding, perHoldingPeriods, unavailable, attempted: 0 };
|
|
323
|
+
if (!ranked.length) return { perHolding, perHoldingPeriods, shareCounts, unavailable, attempted: 0 };
|
|
249
324
|
|
|
250
325
|
let universe = null;
|
|
251
326
|
try {
|
|
@@ -255,6 +330,7 @@ export async function resolveConstituentFacts(holdings, { signal, asOf = null, c
|
|
|
255
330
|
return {
|
|
256
331
|
perHolding,
|
|
257
332
|
perHoldingPeriods,
|
|
333
|
+
shareCounts,
|
|
258
334
|
unavailable: [`look-through: SEC ticker universe unavailable (${String(error?.message || error)})`],
|
|
259
335
|
attempted: 0,
|
|
260
336
|
};
|
|
@@ -267,12 +343,29 @@ export async function resolveConstituentFacts(holdings, { signal, asOf = null, c
|
|
|
267
343
|
const cik = universe.get(ticker);
|
|
268
344
|
if (!cik) { unavailable.push(`look-through ${ticker}: no SEC registrant for this ticker`); continue; }
|
|
269
345
|
try {
|
|
270
|
-
|
|
346
|
+
// One document, both readers. The derived fundamentals and the mechanical screen read
|
|
347
|
+
// the same Company Facts filing, and fetching it twice per constituent would double
|
|
348
|
+
// the cost of a look-through for metrics that were already on disk. The screen is
|
|
349
|
+
// where the margins, cash conversion, ten-year ROE and interest coverage live -- which
|
|
350
|
+
// is why wiring only the fundamentals path left those aggregates permanently empty.
|
|
351
|
+
const companyFacts = await fetchCompanyFacts(cik, { signal });
|
|
352
|
+
const derived = deriveFundamentals({ companyFacts, asOf });
|
|
353
|
+
const screened = evaluateRules(companyFacts, { asOf });
|
|
271
354
|
const facts = {};
|
|
272
355
|
const periods = {};
|
|
356
|
+
for (const rule of screened.rules || []) {
|
|
357
|
+
const factId = SCREEN_RULE_FACTS[rule?.id];
|
|
358
|
+
if (!factId || rule.skipped || !Number.isFinite(rule.value)) continue;
|
|
359
|
+
// The screen publishes percentages as 15 for 15%; every pack ratio is a decimal.
|
|
360
|
+
facts[factId] = SCREEN_PERCENT_RULES.has(rule.id) ? rule.value / 100 : rule.value;
|
|
361
|
+
if (rule.period_start && rule.period_end) {
|
|
362
|
+
periods[factId] = { start: rule.period_start, end: rule.period_end };
|
|
363
|
+
}
|
|
364
|
+
}
|
|
273
365
|
for (const [factId, metric] of Object.entries(derived.metrics || {})) {
|
|
274
366
|
if (!metric || !Number.isFinite(metric.value)) continue;
|
|
275
367
|
facts[factId] = metric.value;
|
|
368
|
+
if (factId === "capital_allocation.share_count") shareCounts.set(ticker, metric.value);
|
|
276
369
|
if (metric.period_start && metric.period_end) {
|
|
277
370
|
periods[factId] = { start: metric.period_start, end: metric.period_end };
|
|
278
371
|
}
|
|
@@ -285,7 +378,7 @@ export async function resolveConstituentFacts(holdings, { signal, asOf = null, c
|
|
|
285
378
|
}
|
|
286
379
|
};
|
|
287
380
|
await Promise.all(Array.from({ length: Math.min(concurrency, ranked.length) }, worker));
|
|
288
|
-
return { perHolding, perHoldingPeriods, unavailable, attempted: ranked.length };
|
|
381
|
+
return { perHolding, perHoldingPeriods, shareCounts, unavailable, attempted: ranked.length };
|
|
289
382
|
}
|
|
290
383
|
|
|
291
384
|
/**
|
|
@@ -297,20 +390,134 @@ export async function resolveConstituentFacts(holdings, { signal, asOf = null, c
|
|
|
297
390
|
* correct outcome and not a failure.
|
|
298
391
|
*/
|
|
299
392
|
/**
|
|
300
|
-
* The interval a basket-level aggregate covers
|
|
301
|
-
*
|
|
302
|
-
*
|
|
393
|
+
* The interval a basket-level aggregate covers.
|
|
394
|
+
*
|
|
395
|
+
* The outer envelope -- earliest start to latest end -- is the wrong answer. Constituents keep
|
|
396
|
+
* different fiscal calendars, so the union of forty five-year spans is five years plus a year
|
|
397
|
+
* of calendar edges, and a fact that genuinely covers five years then fails a five-year
|
|
398
|
+
* contract for a reason that has nothing to do with its coverage.
|
|
399
|
+
*
|
|
400
|
+
* The median end date and the median span describe what the aggregate actually covers: a
|
|
401
|
+
* typical constituent's reporting window, which is the window the number was averaged over.
|
|
303
402
|
*/
|
|
304
403
|
function aggregatePeriod(perHoldingPeriods, factId, tickers) {
|
|
305
|
-
|
|
306
|
-
let end = null;
|
|
404
|
+
const spans = [];
|
|
307
405
|
for (const ticker of tickers) {
|
|
308
406
|
const span = perHoldingPeriods?.get(ticker)?.[factId];
|
|
309
407
|
if (!span?.start || !span?.end) continue;
|
|
310
|
-
|
|
311
|
-
|
|
408
|
+
const start = Date.parse(span.start);
|
|
409
|
+
const end = Date.parse(span.end);
|
|
410
|
+
if (!Number.isFinite(start) || !Number.isFinite(end) || end <= start) continue;
|
|
411
|
+
spans.push({ end, days: (end - start) / 86_400_000 });
|
|
412
|
+
}
|
|
413
|
+
if (!spans.length) return {};
|
|
414
|
+
const median = (values) => {
|
|
415
|
+
const sorted = [...values].sort((left, right) => left - right);
|
|
416
|
+
return sorted[Math.floor(sorted.length / 2)];
|
|
417
|
+
};
|
|
418
|
+
const end = median(spans.map((span) => span.end));
|
|
419
|
+
const days = median(spans.map((span) => span.days));
|
|
420
|
+
return {
|
|
421
|
+
period_start: new Date(end - days * 86_400_000).toISOString().slice(0, 10),
|
|
422
|
+
period_end: new Date(end).toISOString().slice(0, 10),
|
|
423
|
+
};
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* The dollar claim a fund has on an absolute company figure, summed across constituents.
|
|
428
|
+
*
|
|
429
|
+
* Requires a market capitalisation per constituent, which is its own filed share count times
|
|
430
|
+
* its last close — both already in hand from the fundamentals pass and the breadth pass. A
|
|
431
|
+
* constituent missing either is excluded and costs coverage rather than being assumed.
|
|
432
|
+
*/
|
|
433
|
+
/**
|
|
434
|
+
* A share count for a fund whose holdings file does not carry one.
|
|
435
|
+
*
|
|
436
|
+
* There are two routes and they are not equally good, which matters because a flow is a
|
|
437
|
+
* DIFFERENCE of two counts and therefore keeps only their error.
|
|
438
|
+
*
|
|
439
|
+
* The good route is the issuer's own assets over its own net asset value, on the same date
|
|
440
|
+
* from the same source: that ratio IS shares outstanding, an identity rather than an estimate,
|
|
441
|
+
* and it is trustworthy enough to difference.
|
|
442
|
+
*
|
|
443
|
+
* The weak route is the assets implied by disclosed positions over the fund's market price. It
|
|
444
|
+
* is fine for a market capitalisation -- the product is the size we already had -- and unfit
|
|
445
|
+
* for a flow, because a 95%-priced position sum and a premium to NAV each carry error that a
|
|
446
|
+
* subtraction leaves behind while cancelling the number. `flowInputs` refuses it by basis.
|
|
447
|
+
*/
|
|
448
|
+
async function impliedFundShares(symbol, netAssets, metadata, signal) {
|
|
449
|
+
if (finite(metadata?.nav) && metadata.nav > 0 && finite(metadata?.aum) && metadata.aum > 0) {
|
|
450
|
+
return { value: metadata.aum / metadata.nav, how: "issuer_aum_over_nav" };
|
|
451
|
+
}
|
|
452
|
+
if (!finite(netAssets) || netAssets <= 0) return null;
|
|
453
|
+
try {
|
|
454
|
+
const closes = parseDailyCloses(JSON.parse(await fetchText(chartUrl(symbol), LIMITS.QUOTE_FETCH_MS, signal)));
|
|
455
|
+
const close = closes?.at(-1);
|
|
456
|
+
if (!finite(close) || close <= 0) return null;
|
|
457
|
+
return { value: netAssets / close, how: "assets_from_disclosed_positions_over_market_price" };
|
|
458
|
+
} catch {
|
|
459
|
+
return null;
|
|
460
|
+
}
|
|
461
|
+
}
|
|
462
|
+
|
|
463
|
+
export function lookThroughClaims({ holdings, perHoldingFacts, perHoldingPeriods, shareCounts, closes, netAssets, holdingsMeta }) {
|
|
464
|
+
const facts = [];
|
|
465
|
+
const unavailable = [];
|
|
466
|
+
if (!finite(netAssets) || netAssets <= 0) {
|
|
467
|
+
unavailable.push("look-through claims: the fund's own size is unknown, so a dollar claim cannot be scaled");
|
|
468
|
+
return { facts, unavailable };
|
|
469
|
+
}
|
|
470
|
+
const priced = new Map();
|
|
471
|
+
for (const holding of holdings || []) {
|
|
472
|
+
const ticker = String(holding?.ticker || "").toUpperCase();
|
|
473
|
+
const shares = shareCounts?.get?.(ticker);
|
|
474
|
+
const close = closes?.[ticker];
|
|
475
|
+
if (!finite(holding?.weight) || !finite(shares) || shares <= 0 || !finite(close) || close <= 0) continue;
|
|
476
|
+
priced.set(ticker, { weight: holding.weight, marketCap: shares * close });
|
|
477
|
+
}
|
|
478
|
+
if (!priced.size) {
|
|
479
|
+
unavailable.push("look-through claims: no constituent supplied both a share count and a price");
|
|
480
|
+
return { facts, unavailable };
|
|
481
|
+
}
|
|
482
|
+
for (const factId of LOOK_THROUGH_CLAIM_FACTS) {
|
|
483
|
+
let claim = 0;
|
|
484
|
+
let covered = 0;
|
|
485
|
+
for (const [ticker, { weight, marketCap }] of priced) {
|
|
486
|
+
const value = perHoldingFacts?.get?.(ticker)?.[factId];
|
|
487
|
+
if (!finite(value)) continue;
|
|
488
|
+
claim += (weight / marketCap) * value;
|
|
489
|
+
covered += weight;
|
|
490
|
+
}
|
|
491
|
+
if (covered < MIN_LOOK_THROUGH_COVERAGE) {
|
|
492
|
+
unavailable.push(
|
|
493
|
+
`look-through claim ${factId}: ${(covered * 100).toFixed(1)}% of the basket by weight supplied it,`
|
|
494
|
+
+ ` below the ${(MIN_LOOK_THROUGH_COVERAGE * 100).toFixed(0)}% floor`,
|
|
495
|
+
);
|
|
496
|
+
continue;
|
|
497
|
+
}
|
|
498
|
+
facts.push({
|
|
499
|
+
fact_id: factId,
|
|
500
|
+
value: Number((claim * netAssets).toFixed(2)),
|
|
501
|
+
// A claim on a one-year figure covers a year and a claim on a five-year total covers
|
|
502
|
+
// five. It inherits the window its inputs reported, for the same reason the ratio
|
|
503
|
+
// aggregates do.
|
|
504
|
+
...aggregatePeriod(perHoldingPeriods, factId, [...priced.keys()]),
|
|
505
|
+
value_kind: "monetary",
|
|
506
|
+
unit: "currency_units",
|
|
507
|
+
currency: "USD",
|
|
508
|
+
scale: 1,
|
|
509
|
+
source_kind: "issuer_disclosure",
|
|
510
|
+
source_url: holdingsMeta?.source_url,
|
|
511
|
+
public_at: holdingsMeta?.public_at,
|
|
512
|
+
observation_date: holdingsMeta?.as_of,
|
|
513
|
+
confidence: 0.65,
|
|
514
|
+
derivation: "rederived",
|
|
515
|
+
method: "fund_ownership_share_of_constituent_figure",
|
|
516
|
+
coverage_weight: Number(covered.toFixed(6)),
|
|
517
|
+
title: `${holdingsMeta?.symbol || "fund"} look-through claim on ${factId}`,
|
|
518
|
+
});
|
|
312
519
|
}
|
|
313
|
-
return
|
|
520
|
+
return { facts, unavailable };
|
|
314
521
|
}
|
|
315
522
|
|
|
316
523
|
export function lookThroughFacts({ holdings, perHoldingFacts, perHoldingPeriods, factIds, holdingsMeta }) {
|
|
@@ -342,7 +549,7 @@ export function lookThroughFacts({ holdings, perHoldingFacts, perHoldingPeriods,
|
|
|
342
549
|
fact_id: factId,
|
|
343
550
|
value: aggregate.value,
|
|
344
551
|
value_kind: "ratio",
|
|
345
|
-
unit: "decimal",
|
|
552
|
+
unit: LOOK_THROUGH_UNITS[factId] || "decimal",
|
|
346
553
|
ratio_denominator: `weight_${aggregate.method}`,
|
|
347
554
|
source_kind: "issuer_disclosure",
|
|
348
555
|
source_url: holdingsMeta?.source_url,
|
|
@@ -456,12 +663,14 @@ export async function gatherInstrumentFacts({
|
|
|
456
663
|
}
|
|
457
664
|
unavailable.push(...(erp?.unavailable || []));
|
|
458
665
|
unavailable.push(...(aggregate?.unavailable || []));
|
|
666
|
+
let breadthResult = null;
|
|
459
667
|
if (holdings?.holdings?.length) {
|
|
460
668
|
facts.push(...structureFacts(holdings, metadata));
|
|
461
669
|
// Breadth is computed from the basket rather than bought: every free screener that
|
|
462
670
|
// publishes it forbids the query, and the holdings are already in hand.
|
|
463
|
-
|
|
671
|
+
breadthResult = await fetchBasketBreadth(holdings.holdings, { signal })
|
|
464
672
|
.catch((error) => ({ available: false, unavailable: [`breadth: ${String(error?.message || error)}`] }));
|
|
673
|
+
const breadth = breadthResult;
|
|
465
674
|
unavailable.push(...(breadth.unavailable || []));
|
|
466
675
|
if (breadth.available) {
|
|
467
676
|
const shared = {
|
|
@@ -497,6 +706,77 @@ export async function gatherInstrumentFacts({
|
|
|
497
706
|
title: `${holdings.symbol} assets from disclosed positions`,
|
|
498
707
|
});
|
|
499
708
|
}
|
|
709
|
+
// The fund's own shares outstanding. This is the fund as an issuer, not an aggregate of
|
|
710
|
+
// anything -- and it is what lets a seat build the fund's market capitalisation with the
|
|
711
|
+
// same arithmetic it uses on a company, which is why ten seats could not read a basket.
|
|
712
|
+
const fundShares = finite(holdings.shares_outstanding)
|
|
713
|
+
? { value: holdings.shares_outstanding, how: "issuer_disclosed_shares_outstanding" }
|
|
714
|
+
: await impliedFundShares(holdings.symbol, breadth.net_assets, metadata, signal);
|
|
715
|
+
if (fundShares) {
|
|
716
|
+
facts.push({
|
|
717
|
+
...shared,
|
|
718
|
+
fact_id: "capital_allocation.share_count",
|
|
719
|
+
value: Number(fundShares.value.toFixed(2)),
|
|
720
|
+
value_kind: "count",
|
|
721
|
+
unit: "shares",
|
|
722
|
+
ratio_denominator: undefined,
|
|
723
|
+
derivation: fundShares.how === "issuer_disclosed_shares_outstanding" ? "reported" : "rederived",
|
|
724
|
+
method: fundShares.how,
|
|
725
|
+
title: `${holdings.symbol} shares outstanding`,
|
|
726
|
+
});
|
|
727
|
+
}
|
|
728
|
+
// What this run saw about the fund's size, appended to its own ledger. No issuer serves
|
|
729
|
+
// a share-count history keylessly, so the history is built one run at a time and a flow
|
|
730
|
+
// becomes computable from the second observation onward.
|
|
731
|
+
const observed = recordFundObservation({
|
|
732
|
+
symbol: holdings.symbol,
|
|
733
|
+
asOf: holdings.as_of,
|
|
734
|
+
sharesOutstanding: fundShares?.value ?? null,
|
|
735
|
+
sharesBasis: fundShares?.how ?? null,
|
|
736
|
+
nav: finite(metadata?.nav) ? metadata.nav : null,
|
|
737
|
+
netAssets: finite(metadata?.aum) ? metadata.aum : breadth.net_assets,
|
|
738
|
+
sourceUrl: holdings.source_url,
|
|
739
|
+
});
|
|
740
|
+
const inputs = flowInputs(observed.observations);
|
|
741
|
+
const flow = inputs
|
|
742
|
+
? fundFlow({ sharesNow: inputs.sharesNow, sharesPrior: inputs.sharesPrior, nav: inputs.nav, issuer: holdings.issuer })
|
|
743
|
+
: null;
|
|
744
|
+
if (flow && finite(flow.value)) {
|
|
745
|
+
facts.push({
|
|
746
|
+
...shared,
|
|
747
|
+
fact_id: "fund.net_flow",
|
|
748
|
+
value: flow.value,
|
|
749
|
+
value_kind: "monetary",
|
|
750
|
+
unit: "currency_units",
|
|
751
|
+
currency: "USD",
|
|
752
|
+
scale: 1,
|
|
753
|
+
ratio_denominator: undefined,
|
|
754
|
+
derivation: "rederived",
|
|
755
|
+
observation_date: inputs.asOf,
|
|
756
|
+
method: `${flow.construction.formula}; ${inputs.priorAsOf} to ${inputs.asOf}, ${inputs.gapDays} days apart`,
|
|
757
|
+
title: `${holdings.symbol} net creation over ${inputs.gapDays} days`,
|
|
758
|
+
});
|
|
759
|
+
if (finite(inputs.netAssets) && inputs.netAssets > 0) {
|
|
760
|
+
facts.push({
|
|
761
|
+
...shared,
|
|
762
|
+
fact_id: "fund.net_flow_ratio",
|
|
763
|
+
value: Number((flow.value / inputs.netAssets).toFixed(6)),
|
|
764
|
+
ratio_denominator: "net_assets",
|
|
765
|
+
derivation: "rederived",
|
|
766
|
+
observation_date: inputs.asOf,
|
|
767
|
+
method: `net creation over assets; ${inputs.priorAsOf} to ${inputs.asOf}, ${inputs.gapDays} days apart`,
|
|
768
|
+
title: `${holdings.symbol} net creation as a share of assets`,
|
|
769
|
+
});
|
|
770
|
+
}
|
|
771
|
+
} else {
|
|
772
|
+
const filed = observed.observations.filter((row) => row.shares_basis === "issuer_disclosed_shares_outstanding").length;
|
|
773
|
+
unavailable.push(filed >= 1
|
|
774
|
+
? `fund flow for ${holdings.symbol}: ${filed} issuer-published share-count observation(s) on record;`
|
|
775
|
+
+ " a flow needs two, and no issuer serves a keyless history. The ledger grows one run at a time."
|
|
776
|
+
: `fund flow for ${holdings.symbol}: ${holdings.issuer} publishes no shares outstanding, and the count`
|
|
777
|
+
+ " reconstructed from assets over price may size the fund but must not be differenced into a flow --"
|
|
778
|
+
+ " the subtraction cancels the number and keeps the error.");
|
|
779
|
+
}
|
|
500
780
|
// The gap between weighted and counted breadth IS the concentration story: a
|
|
501
781
|
// cap-weighted basket can be above its average on weight while most members are below.
|
|
502
782
|
facts.push({
|
|
@@ -516,10 +796,12 @@ export async function gatherInstrumentFacts({
|
|
|
516
796
|
if (holdings?.holdings?.length && lookThroughFactIds.length) {
|
|
517
797
|
let resolved = perHoldingFacts;
|
|
518
798
|
let resolvedPeriods = null;
|
|
799
|
+
let resolvedShareCounts = null;
|
|
519
800
|
if (!resolved) {
|
|
520
801
|
const constituents = await resolveConstituentFacts(holdings.holdings, { signal, asOf });
|
|
521
802
|
resolved = constituents.perHolding;
|
|
522
803
|
resolvedPeriods = constituents.perHoldingPeriods;
|
|
804
|
+
resolvedShareCounts = constituents.shareCounts;
|
|
523
805
|
unavailable.push(...constituents.unavailable);
|
|
524
806
|
provenance.look_through = {
|
|
525
807
|
constituents_attempted: constituents.attempted,
|
|
@@ -535,12 +817,31 @@ export async function gatherInstrumentFacts({
|
|
|
535
817
|
});
|
|
536
818
|
facts.push(...through.facts);
|
|
537
819
|
unavailable.push(...through.unavailable);
|
|
820
|
+
|
|
821
|
+
// The fund's own size is what scales a per-dollar claim back into dollars. Prefer the
|
|
822
|
+
// issuer's published figure and fall back to the assets implied by the disclosed
|
|
823
|
+
// positions, which is the same number the size fact above is built from.
|
|
824
|
+
const netAssets = finite(metadata?.aum) ? metadata.aum : breadthResult?.net_assets;
|
|
825
|
+
const claims = lookThroughClaims({
|
|
826
|
+
holdings: holdings.holdings,
|
|
827
|
+
perHoldingFacts: resolved,
|
|
828
|
+
perHoldingPeriods: resolvedPeriods,
|
|
829
|
+
shareCounts: resolvedShareCounts,
|
|
830
|
+
closes: breadthResult?.closes,
|
|
831
|
+
netAssets,
|
|
832
|
+
holdingsMeta: holdings,
|
|
833
|
+
});
|
|
834
|
+
facts.push(...claims.facts);
|
|
835
|
+
unavailable.push(...claims.unavailable);
|
|
538
836
|
}
|
|
539
837
|
|
|
540
838
|
return {
|
|
541
839
|
symbol,
|
|
542
840
|
research_model: instrument?.research_model || "market_instrument",
|
|
543
841
|
provenance,
|
|
842
|
+
// The disclosed positions themselves, so a caller that needs the basket's identity rather
|
|
843
|
+
// than its aggregates -- news, for one -- does not fetch the holdings file a second time.
|
|
844
|
+
holdings: holdings?.holdings || null,
|
|
544
845
|
facts,
|
|
545
846
|
// Several facts have more than one route -- the implied ERP and the valuation percentile
|
|
546
847
|
// are computed from the published workbook when the index feed cannot supply them -- and
|
package/mcp/lib/instruments.mjs
CHANGED
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
11
|
import { localized } from "./lang.mjs";
|
|
12
|
+
import { FUND_REGISTRY } from "./funds.mjs";
|
|
12
13
|
|
|
13
14
|
export const ASSET_TYPES = Object.freeze([
|
|
14
15
|
"equity",
|
|
@@ -35,10 +36,24 @@ function normalizedRawType(value) {
|
|
|
35
36
|
return String(value || "").trim().toUpperCase().replace(/[\s-]+/gu, "_");
|
|
36
37
|
}
|
|
37
38
|
|
|
39
|
+
/**
|
|
40
|
+
* Cash indices people type without the caret.
|
|
41
|
+
*
|
|
42
|
+
* `^SOX` classified as an index and `SOX` did not, so a run on the name everyone actually uses
|
|
43
|
+
* produced no basket facts at all and every seat abstained for want of data it was never going
|
|
44
|
+
* to get. These are index names, not listed tickers -- deliberately a short curated set rather
|
|
45
|
+
* than the whole alias table, because an alias that collides with a real ticker would route a
|
|
46
|
+
* tradable security into the index path, which is the mirror image of the same defect.
|
|
47
|
+
*/
|
|
48
|
+
const BARE_INDEX_SYMBOLS = new Set([
|
|
49
|
+
"SOX", "SPX", "NDX", "DJIA", "DJI", "RUT", "VIX", "IXIC", "GSPC", "COMPQ", "NYA", "OEX", "XAX",
|
|
50
|
+
]);
|
|
51
|
+
|
|
38
52
|
function symbolHeuristic(symbol) {
|
|
39
53
|
const text = String(symbol || "").trim().toUpperCase();
|
|
40
54
|
if (!text) return null;
|
|
41
55
|
if (text.startsWith("^")) return "index";
|
|
56
|
+
if (BARE_INDEX_SYMBOLS.has(text)) return "index";
|
|
42
57
|
if (text.endsWith("=F")) return "future";
|
|
43
58
|
if (text.endsWith("=X")) return "fx";
|
|
44
59
|
if (/^[A-Z0-9]+-USD$/u.test(text)) return "crypto";
|
|
@@ -94,6 +109,16 @@ export function classifyInstrument({ symbol, quote, filer } = {}) {
|
|
|
94
109
|
classificationSource = "sec_filer_sic";
|
|
95
110
|
}
|
|
96
111
|
}
|
|
112
|
+
// A basket we hold an issuer holdings endpoint for is a basket, whatever a quote feed did or
|
|
113
|
+
// did not say. This is a lookup of something this repository states, not a guess from ticker
|
|
114
|
+
// shape, and it runs only after real evidence has had its turn so genuine metadata still
|
|
115
|
+
// wins and still reports its own provenance. It strengthens the fail-closed rule below
|
|
116
|
+
// rather than weakening it: the outcome is a fund, which is the one thing that can never be
|
|
117
|
+
// sent through an operating-company screen.
|
|
118
|
+
if (!assetType && Object.hasOwn(FUND_REGISTRY, String(symbol || quote?.symbol || "").trim().toUpperCase())) {
|
|
119
|
+
assetType = "etf";
|
|
120
|
+
classificationSource = "registered_fund_holdings_source";
|
|
121
|
+
}
|
|
97
122
|
// Deliberately fail closed. A bare ticker shape tells us nothing: SPY, QQQ and VOO all
|
|
98
123
|
// match it, and the previous `equity` fallback sent them into an operating-company Company
|
|
99
124
|
// Facts screen whenever Yahoo metadata was missing -- the exact QQQ defect 0.9.5 set out to
|
package/mcp/lib/markdown.mjs
CHANGED
|
@@ -118,25 +118,33 @@ const MASTER_STATEMENT_COPY = Object.freeze({
|
|
|
118
118
|
heading: "\u9010\u5e2d\u65b9\u6cd5\u8f93\u51fa", acted: "\u6709\u5224\u65ad\u7684\u5e2d\u4f4d", abstained: "\u8bf4\u8fd9\u4e0d\u5f52\u5b83\u7ba1\u7684\u5e2d\u4f4d",
|
|
119
119
|
stance: "\u7acb\u573a", intent: "\u610f\u5411", origin: "\u9648\u8bcd\u6765\u6e90", statement: "\u672c\u8f6e\u53d1\u8a00\uff08\u4e0d\u662f\u672c\u4eba\u5f15\u8bed\uff09",
|
|
120
120
|
findings: "\u5173\u952e\u53d1\u73b0", disagreements: "\u4e0e\u5206\u6790\u5e08\u5206\u6b67", change: "\u6539\u53d8\u5224\u65ad\u6761\u4ef6", sources: "\u6765\u6e90\u6216\u660e\u786e\u7f3a\u53e3",
|
|
121
|
-
abstainLead: (n) => `\u53e6\u6709 ${n} \u5e2d\
|
|
121
|
+
abstainLead: (n) => `\u53e6\u6709 ${n} \u5e2d\u672c\u8f6e\u672a\u80fd\u53d6\u5f97\u5176\u65b9\u6cd5\u5fc5\u9700\u7684\u8f93\u5165\uff0c\u56e0\u6b64\u6ca1\u6709\u7ed9\u51fa\u65b9\u5411\u3002\u8fd9\u662f\u6570\u636e\u7f3a\u53e3\uff0c\u4e0d\u662f\u770b\u7a7a\u7968\uff1a`,
|
|
122
|
+
declined: "\u770b\u8fc7\u4e4b\u540e\u51b3\u5b9a\u4e0d\u53c2\u4e0e\u7684\u5e2d\u4f4d",
|
|
123
|
+
declinedLead: (n) => `\u4ee5\u4e0b ${n} \u5e2d\u7684\u65b9\u6cd5\u8dd1\u5b8c\u4e86\uff0c\u5e76\u4e14\u5f97\u51fa\u4e86\u201c\u4e0d\u662f\u8fd9\u4e2a\u201d\u3002\u8fd9\u662f\u5224\u65ad\uff0c\u4e0d\u662f\u7f3a\u6570\u636e\uff1a`,
|
|
122
124
|
},
|
|
123
125
|
en: {
|
|
124
126
|
heading: "Method-Seat Outputs", acted: "Seats with a view", abstained: "Seats that say this is not theirs to call",
|
|
125
127
|
stance: "Stance", intent: "Intent", origin: "Statement source", statement: "Recorded statement (not a quote)",
|
|
126
128
|
findings: "Key findings", disagreements: "Disagreements", change: "What would change the view", sources: "Sources or explicit gaps",
|
|
127
|
-
abstainLead: (n) => `A further ${n} seat(s) issue no direction
|
|
129
|
+
abstainLead: (n) => `A further ${n} seat(s) issue no direction because a method-critical input did not arrive this round. This is a data gap, not a bearish vote:`,
|
|
130
|
+
declined: "Seats whose method examined this and declined",
|
|
131
|
+
declinedLead: (n) => `${n} seat(s) ran their method to completion and it returned "not this one". These are judgments, not missing data:`,
|
|
128
132
|
},
|
|
129
133
|
ja: {
|
|
130
134
|
heading: "\u30e1\u30bd\u30c3\u30c9\u5e2d\u3054\u3068\u306e\u51fa\u529b", acted: "\u5224\u65ad\u3092\u793a\u3057\u305f\u5e2d", abstained: "\u81ea\u5206\u306e\u62c5\u5f53\u3067\u306f\u306a\u3044\u3068\u3057\u305f\u5e2d",
|
|
131
135
|
stance: "\u30b9\u30bf\u30f3\u30b9", intent: "\u610f\u5411", origin: "\u898b\u89e3\u306e\u751f\u6210\u5143", statement: "\u4eca\u56de\u306e\u767a\u8a00\uff08\u672c\u4eba\u306e\u5f15\u7528\u3067\u306f\u3042\u308a\u307e\u305b\u3093\uff09",
|
|
132
136
|
findings: "\u4e3b\u306a\u6240\u898b", disagreements: "\u5206\u6790\u62c5\u5f53\u3068\u306e\u76f8\u9055", change: "\u5224\u65ad\u304c\u5909\u308f\u308b\u6761\u4ef6", sources: "\u51fa\u5178\u307e\u305f\u306f\u660e\u793a\u7684\u306a\u6b20\u843d",
|
|
133
|
-
abstainLead: (n) => `\u4ed6\u306b ${n} \u5e2d\u306f\u4eca\u56de\u65b9\u5411\u6027\u3092\u793a\u3057\u307e\u305b\u3093\u3002\
|
|
137
|
+
abstainLead: (n) => `\u4ed6\u306b ${n} \u5e2d\u306f\u3001\u30e1\u30bd\u30c3\u30c9\u306b\u5fc5\u8981\u306a\u5165\u529b\u304c\u4eca\u56de\u5c4a\u304b\u306a\u304b\u3063\u305f\u305f\u3081\u65b9\u5411\u6027\u3092\u793a\u3057\u307e\u305b\u3093\u3002\u30c7\u30fc\u30bf\u306e\u6b20\u843d\u3067\u3042\u308a\u3001\u5f31\u6c17\u7968\u3067\u306f\u3042\u308a\u307e\u305b\u3093\uff1a`,
|
|
138
|
+
declined: "\u691c\u8a0e\u3057\u305f\u4e0a\u3067\u898b\u9001\u3063\u305f\u5e2d",
|
|
139
|
+
declinedLead: (n) => `\u6b21\u306e ${n} \u5e2d\u306f\u30e1\u30bd\u30c3\u30c9\u3092\u6700\u5f8c\u307e\u3067\u5b9f\u884c\u3057\u3001\u300c\u3053\u308c\u3067\u306f\u306a\u3044\u300d\u3068\u7d50\u8ad6\u3057\u307e\u3057\u305f\u3002\u30c7\u30fc\u30bf\u4e0d\u8db3\u3067\u306f\u306a\u304f\u5224\u65ad\u3067\u3059\uff1a`,
|
|
134
140
|
},
|
|
135
141
|
ko: {
|
|
136
142
|
heading: "\ubc29\ubc95\ub860 \uc88c\uc11d\ubcc4 \ucd9c\ub825", acted: "\ud310\ub2e8\uc744 \ub0b8 \uc88c\uc11d", abstained: "\uc790\uae30 \uc18c\uad00\uc774 \uc544\ub2c8\ub77c\uace0 \ubc1d\ud78c \uc88c\uc11d",
|
|
137
143
|
stance: "\uc785\uc7a5", intent: "\uc758\ud5a5", origin: "\ubc1c\uc5b8 \ucd9c\ucc98", statement: "\uc774\ubc88 \ubc1c\uc5b8(\ubcf8\uc778 \uc778\uc6a9\uc774 \uc544\ub2d8)",
|
|
138
144
|
findings: "\ud575\uc2ec \ubc1c\uacac", disagreements: "\ubd84\uc11d\uac00\uc640\uc758 \uc774\uacac", change: "\ud310\ub2e8 \ubcc0\uacbd \uc870\uac74", sources: "\ucd9c\ucc98 \ub610\ub294 \uba85\uc2dc\uc801 \ub370\uc774\ud130 \uacf5\ubc31",
|
|
139
|
-
abstainLead: (n) => `\uadf8 \uc678 ${n}\uac1c \uc88c\uc11d\uc740 \uc774\ubc88 \ud68c\ucc28\uc5d0 \ubc29\ud5a5\uc744 \uc81c\uc2dc\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \
|
|
145
|
+
abstainLead: (n) => `\uadf8 \uc678 ${n}\uac1c \uc88c\uc11d\uc740 \ubc29\ubc95\ub860\uc5d0 \ud544\uc694\ud55c \uc785\ub825\uc774 \uc774\ubc88 \ud68c\ucc28\uc5d0 \ub3c4\ucc29\ud558\uc9c0 \uc54a\uc544 \ubc29\ud5a5\uc744 \uc81c\uc2dc\ud558\uc9c0 \uc54a\uc2b5\ub2c8\ub2e4. \ub370\uc774\ud130 \uacf5\ubc31\uc774\uba70 \uc57d\uc138 \ud22c\ud45c\uac00 \uc544\ub2d9\ub2c8\ub2e4:`,
|
|
146
|
+
declined: "\uac80\ud1a0 \ud6c4 \ucc38\uc5ec\ud558\uc9c0 \uc54a\uae30\ub85c \ud55c \uc88c\uc11d",
|
|
147
|
+
declinedLead: (n) => `\ub2e4\uc74c ${n}\uac1c \uc88c\uc11d\uc740 \ubc29\ubc95\ub860\uc744 \ub05d\uae4c\uc9c0 \uc218\ud589\ud588\uace0 "\uc774\uac83\uc740 \uc544\ub2c8\ub2e4"\ub77c\ub294 \uacb0\ub860\uc5d0 \ub3c4\ub2ec\ud588\uc2b5\ub2c8\ub2e4. \ub370\uc774\ud130 \ubd80\uc871\uc774 \uc544\ub2c8\ub77c \ud310\ub2e8\uc785\ub2c8\ub2e4:`,
|
|
140
148
|
},
|
|
141
149
|
});
|
|
142
150
|
|
|
@@ -183,12 +191,25 @@ function renderMasterStatements(run) {
|
|
|
183
191
|
const sections = [`### ${copy.heading}`, voiceDisclaimer(run.language)];
|
|
184
192
|
if (acted.length) sections.push(`#### ${copy.acted}`, acted.map(seatBlock).join("\n\n"));
|
|
185
193
|
if (abstained.length) {
|
|
186
|
-
//
|
|
187
|
-
//
|
|
188
|
-
|
|
194
|
+
// Two reasons wear the same word and they are not the same event. A seat whose method
|
|
195
|
+
// examined the subject and declined has ANSWERED -- Graham finding no asset floor, a
|
|
196
|
+
// volatility seat finding no testable observation -- and reporting that as "missing an
|
|
197
|
+
// input" tells a reader the system broke when the method spoke. A seat whose required
|
|
198
|
+
// fact never arrived genuinely is a gap. Rendering them together made every run read as
|
|
199
|
+
// the second kind, which is the complaint this split exists to answer.
|
|
200
|
+
// One paragraph each, not one row per seat: the stable IDs stay visible so the gate and
|
|
201
|
+
// the reader can both account for every selected seat.
|
|
202
|
+
const merged = (group) => group
|
|
189
203
|
.map((opinion) => `${masterTitle(opinion.master, run.language)} (\`${opinion.master}\`) \u2014 ${opinion.voice_statement}`)
|
|
190
204
|
.join(" ");
|
|
191
|
-
|
|
205
|
+
const declined = abstained.filter((opinion) => methodDeclined(opinion));
|
|
206
|
+
const ungrounded = abstained.filter((opinion) => !methodDeclined(opinion));
|
|
207
|
+
if (declined.length) {
|
|
208
|
+
sections.push(`#### ${copy.declined}`, `${copy.declinedLead(declined.length)}\n\n${merged(declined)}`);
|
|
209
|
+
}
|
|
210
|
+
if (ungrounded.length) {
|
|
211
|
+
sections.push(`#### ${copy.abstained}`, `${copy.abstainLead(ungrounded.length)}\n\n${merged(ungrounded)}`);
|
|
212
|
+
}
|
|
192
213
|
}
|
|
193
214
|
return sections.join("\n\n");
|
|
194
215
|
}
|
|
@@ -202,6 +223,18 @@ function renderMasterStatements(run) {
|
|
|
202
223
|
* weakest thing a council produces and the dissenting seat is the informative one. Stating
|
|
203
224
|
* this next to the opinions is the difference between a bench and a vote count.
|
|
204
225
|
*/
|
|
226
|
+
/**
|
|
227
|
+
* Did the seat's own method reach this, or did the data never arrive?
|
|
228
|
+
*
|
|
229
|
+
* A fired veto and a false eligibility condition are both the method running to completion and
|
|
230
|
+
* returning "not this one". Anything else -- an unmet required fact type, a coverage shortfall,
|
|
231
|
+
* an executor refusal -- is the run failing to give the method what it asked for.
|
|
232
|
+
*/
|
|
233
|
+
export function methodDeclined(opinion) {
|
|
234
|
+
const reason = String(opinion?.decision?.reason || opinion?.reason || "");
|
|
235
|
+
return reason === "veto" || reason === "eligibility";
|
|
236
|
+
}
|
|
237
|
+
|
|
205
238
|
export function masterCorrelationNote(run) {
|
|
206
239
|
const opinions = run?.master_opinions || [];
|
|
207
240
|
if (!opinions.length) return "";
|