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.
Files changed (157) hide show
  1. package/.claude-plugin/marketplace.json +2 -2
  2. package/.claude-plugin/plugin.json +1 -1
  3. package/.codex-plugin/plugin.json +1 -1
  4. package/AGENTS.md +5 -6
  5. package/CHANGELOG.md +86 -0
  6. package/CLAUDE.md +5 -6
  7. package/README.ja.md +13 -12
  8. package/README.md +16 -16
  9. package/README.zh-CN.md +11 -11
  10. package/data/authored/core-seats.mjs +3 -3
  11. package/data/authored/quant-seats.mjs +2 -2
  12. package/data/authored/value-seats.mjs +4 -4
  13. package/data/build-profile.v1.json +1 -1
  14. package/docs/INSTALL.md +6 -10
  15. package/docs/plans/v1.1-basket-native-seats.md +267 -0
  16. package/knowledge/ai-assisted-solo/experiments/runs/a.json +10 -10
  17. package/knowledge/ai-assisted-solo/experiments/runs/b.json +2 -2
  18. package/knowledge/ai-assisted-solo/experiments/runs/c.json +2 -2
  19. package/knowledge/ai-assisted-solo/experiments/runs/d13.json +52 -52
  20. package/knowledge/ai-assisted-solo/experiments/runs/d26.json +68 -68
  21. package/knowledge/ai-assisted-solo/experiments/runs/e-d13.json +55 -55
  22. package/knowledge/ai-assisted-solo/experiments/runs/e-d26.json +71 -71
  23. package/knowledge/ai-assisted-solo/experiments/runs/h_ai_reference.json +72 -72
  24. package/knowledge/ai-assisted-solo/experiments/simulation-input.json +51 -51
  25. package/knowledge/ai-assisted-solo/experiments/simulation-manifest.json +19 -19
  26. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_ackman/reviews/market_capitalisation.ai-review.json +35 -35
  27. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_ackman/reviews/owner_earnings_yield.ai-review.json +15 -15
  28. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_aschenbrenner/reviews/market_capitalisation.ai-review.json +35 -35
  29. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_aschenbrenner/reviews/price_implied_cash_yield.ai-review.json +15 -15
  30. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_asness/reviews/nominal_earnings_yield_gap.ai-review.json +15 -15
  31. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_asness/reviews/real_earnings_yield_gap.ai-review.json +15 -15
  32. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_bogle/reviews/expected_return_over_long_bond.ai-review.json +15 -15
  33. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_bogle/reviews/fundamental_expected_return.ai-review.json +15 -15
  34. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_buffett/reviews/market_capitalisation.ai-review.json +35 -35
  35. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_buffett/reviews/owner_earnings_yield.ai-review.json +15 -15
  36. 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
  37. 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
  38. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_cathie_wood/reviews/current_cash_yield.ai-review.json +15 -15
  39. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_cathie_wood/reviews/market_capitalisation.ai-review.json +35 -35
  40. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_dalio/reviews/policy_real_rate.ai-review.json +15 -15
  41. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_dalio/reviews/real_curve_slope.ai-review.json +15 -15
  42. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_damodaran/reviews/implied_equity_risk_premium.ai-review.json +15 -15
  43. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_damodaran/reviews/premium_versus_long_run_average.ai-review.json +15 -15
  44. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_druckenmiller/reviews/liquidity_curve_impulse.ai-review.json +15 -15
  45. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_druckenmiller/reviews/policy_real_rate.ai-review.json +15 -15
  46. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_duan_yongping/reviews/five_year_opportunity_cost.ai-review.json +35 -35
  47. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_duan_yongping/reviews/margin_retention.ai-review.json +15 -15
  48. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_fisher/reviews/margin_retention.ai-review.json +15 -15
  49. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_fisher/reviews/reinvestment_spread.ai-review.json +15 -15
  50. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_forensic_short/reviews/cash_conversion_gap.ai-review.json +15 -15
  51. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_forensic_short/reviews/cash_cover_of_leverage.ai-review.json +15 -15
  52. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_graham/reviews/defensive_hurdle_base.ai-review.json +35 -35
  53. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_graham/reviews/half_normalised_earnings_yield.ai-review.json +15 -15
  54. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_jhunjhunwala/reviews/cash_quality_gap.ai-review.json +15 -15
  55. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_jhunjhunwala/reviews/real_structural_growth.ai-review.json +15 -15
  56. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_klarman/reviews/downside_asset_coverage.ai-review.json +15 -15
  57. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_klarman/reviews/market_capitalisation.ai-review.json +35 -35
  58. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_li_lu/reviews/cash_backed_decade_return.ai-review.json +15 -15
  59. 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
  60. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_lynch/reviews/growth_justified_capitalisation.ai-review.json +15 -15
  61. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_lynch/reviews/market_capitalisation.ai-review.json +35 -35
  62. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_marks/reviews/credit_cycle_position.ai-review.json +15 -15
  63. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_marks/reviews/equity_over_corporate_debt.ai-review.json +15 -15
  64. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_munger/reviews/debt_service_cushion.ai-review.json +15 -15
  65. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_munger/reviews/reported_earnings_cash_gap.ai-review.json +15 -15
  66. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_natenberg/reviews/normalised_skew.ai-review.json +15 -15
  67. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_natenberg/reviews/round_trip_cost.ai-review.json +15 -15
  68. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_pabrai/reviews/downside_floor_coverage.ai-review.json +15 -15
  69. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_pabrai/reviews/market_capitalisation.ai-review.json +35 -35
  70. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_simons/reviews/absolute_session_move.ai-review.json +15 -15
  71. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_simons/reviews/session_move_over_implied.ai-review.json +15 -15
  72. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_sinclair/reviews/round_trip_cost.ai-review.json +15 -15
  73. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_sinclair/reviews/volatility_per_unit_of_width.ai-review.json +15 -15
  74. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_soros/reviews/credit_cycle_position.ai-review.json +15 -15
  75. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_soros/reviews/financing_burden.ai-review.json +15 -15
  76. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_taleb/reviews/debt_service_cushion.ai-review.json +15 -15
  77. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_taleb/reviews/normalised_skew.ai-review.json +15 -15
  78. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_thorp/reviews/edge_per_unit_of_volatility.ai-review.json +15 -15
  79. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/master_thorp/reviews/net_edge.ai-review.json +15 -15
  80. package/knowledge/ai-assisted-solo/reviews/persona-v3-ai-formula-reviews/review-manifest.json +164 -164
  81. package/knowledge/solo-test/masters/master_ackman/provisional-index.json +1 -1
  82. package/knowledge/solo-test/masters/master_ackman/tools.json +6 -6
  83. package/knowledge/solo-test/masters/master_aschenbrenner/provisional-index.json +1 -1
  84. package/knowledge/solo-test/masters/master_aschenbrenner/tools.json +6 -6
  85. package/knowledge/solo-test/masters/master_asness/provisional-index.json +1 -1
  86. package/knowledge/solo-test/masters/master_bogle/provisional-index.json +1 -1
  87. package/knowledge/solo-test/masters/master_buffett/provisional-index.json +1 -1
  88. package/knowledge/solo-test/masters/master_buffett/tools.json +6 -6
  89. package/knowledge/solo-test/masters/master_burry/provisional-index.json +1 -1
  90. package/knowledge/solo-test/masters/master_burry/tools.json +6 -6
  91. package/knowledge/solo-test/masters/master_cathie_wood/provisional-index.json +1 -1
  92. package/knowledge/solo-test/masters/master_cathie_wood/tools.json +6 -6
  93. package/knowledge/solo-test/masters/master_dalio/provisional-index.json +1 -1
  94. package/knowledge/solo-test/masters/master_damodaran/provisional-index.json +1 -1
  95. package/knowledge/solo-test/masters/master_druckenmiller/provisional-index.json +1 -1
  96. package/knowledge/solo-test/masters/master_duan_yongping/provisional-index.json +1 -1
  97. package/knowledge/solo-test/masters/master_duan_yongping/tools.json +6 -6
  98. package/knowledge/solo-test/masters/master_fisher/provisional-index.json +1 -1
  99. package/knowledge/solo-test/masters/master_forensic_short/provisional-index.json +1 -1
  100. package/knowledge/solo-test/masters/master_graham/decision_policy.json +1 -1
  101. package/knowledge/solo-test/masters/master_graham/provisional-index.json +1 -1
  102. package/knowledge/solo-test/masters/master_graham/tools.json +6 -6
  103. package/knowledge/solo-test/masters/master_jhunjhunwala/provisional-index.json +1 -1
  104. package/knowledge/solo-test/masters/master_klarman/decision_policy.json +1 -1
  105. package/knowledge/solo-test/masters/master_klarman/provisional-index.json +1 -1
  106. package/knowledge/solo-test/masters/master_klarman/tools.json +6 -6
  107. package/knowledge/solo-test/masters/master_li_lu/provisional-index.json +1 -1
  108. package/knowledge/solo-test/masters/master_lynch/provisional-index.json +1 -1
  109. package/knowledge/solo-test/masters/master_lynch/tools.json +6 -6
  110. package/knowledge/solo-test/masters/master_marks/provisional-index.json +1 -1
  111. package/knowledge/solo-test/masters/master_munger/provisional-index.json +1 -1
  112. package/knowledge/solo-test/masters/master_natenberg/provisional-index.json +1 -1
  113. package/knowledge/solo-test/masters/master_pabrai/decision_policy.json +1 -1
  114. package/knowledge/solo-test/masters/master_pabrai/provisional-index.json +1 -1
  115. package/knowledge/solo-test/masters/master_pabrai/tools.json +6 -6
  116. package/knowledge/solo-test/masters/master_simons/provisional-index.json +1 -1
  117. package/knowledge/solo-test/masters/master_sinclair/provisional-index.json +1 -1
  118. package/knowledge/solo-test/masters/master_soros/provisional-index.json +1 -1
  119. package/knowledge/solo-test/masters/master_taleb/provisional-index.json +1 -1
  120. package/knowledge/solo-test/masters/master_thorp/provisional-index.json +1 -1
  121. package/knowledge/solo-test/persona-v3-solo-test-formulas/compilation-manifest.json +21 -21
  122. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_ackman/components/tools.json +6 -6
  123. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_ackman/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +3 -3
  124. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_aschenbrenner/components/tools.json +6 -6
  125. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_aschenbrenner/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +3 -3
  126. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_buffett/components/tools.json +6 -6
  127. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_buffett/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +3 -3
  128. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_burry/components/tools.json +6 -6
  129. 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
  130. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_cathie_wood/components/tools.json +6 -6
  131. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_cathie_wood/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +3 -3
  132. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_duan_yongping/components/tools.json +6 -6
  133. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_duan_yongping/provisional-derivations/five_year_opportunity_cost.derived-proxy-evidence.json +3 -3
  134. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_graham/components/tools.json +6 -6
  135. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_graham/provisional-derivations/defensive_hurdle_base.derived-proxy-evidence.json +3 -3
  136. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_klarman/components/tools.json +6 -6
  137. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_klarman/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +3 -3
  138. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_lynch/components/tools.json +6 -6
  139. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_lynch/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +3 -3
  140. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_pabrai/components/tools.json +6 -6
  141. package/knowledge/solo-test/persona-v3-solo-test-formulas/master_pabrai/provisional-derivations/market_capitalisation.derived-proxy-evidence.json +3 -3
  142. package/mcp/lib/basket-news.mjs +181 -0
  143. package/mcp/lib/breadth.mjs +29 -1
  144. package/mcp/lib/cross-market.mjs +180 -0
  145. package/mcp/lib/fund-observations.mjs +152 -0
  146. package/mcp/lib/fundamentals.mjs +6 -1
  147. package/mcp/lib/funds.mjs +35 -0
  148. package/mcp/lib/grounding.mjs +55 -2
  149. package/mcp/lib/index-aggregate.mjs +16 -1
  150. package/mcp/lib/industry.mjs +23 -2
  151. package/mcp/lib/instrument-facts.mjs +318 -17
  152. package/mcp/lib/instruments.mjs +25 -0
  153. package/mcp/lib/markdown.mjs +41 -8
  154. package/mcp/lib/personas-v3/grounding-adapter.mjs +139 -0
  155. package/mcp/lib/xbrl-series.mjs +4 -2
  156. package/package.json +2 -2
  157. package/scripts/lib/persona-v3-solo-formula-pipeline.mjs +18 -1
@@ -0,0 +1,152 @@
1
+ /**
2
+ * An append-only ledger of what each run saw about a fund's size.
3
+ *
4
+ * A flow is shares created minus shares redeemed, priced. Two dated share counts are the whole
5
+ * input, and no issuer serves a history of them keylessly: iShares publishes today's count in
6
+ * the holdings file and answers 404 for a dated one, Invesco and Vanguard publish none at all.
7
+ *
8
+ * So the history is built rather than fetched. Every run already retrieves the holdings file;
9
+ * recording what it said costs nothing and makes the second run onward able to compute a flow
10
+ * exactly, by the arithmetic `fundFlow` already implements.
11
+ *
12
+ * This bootstraps, it does not backfill, and that is the honest trade. A user who looks at SOXX
13
+ * weekly has weekly flows from the second week. It is not a vendor flow series and the fact
14
+ * built from it says so.
15
+ *
16
+ * What this deliberately does NOT do is derive a flow from a change in assets. Assets move with
17
+ * the market and with creations, and reporting the sum as the second is how this number gets
18
+ * faked — in the direction that makes a rally look like conviction.
19
+ */
20
+
21
+ import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
22
+ import { dirname, join, resolve, sep } from "node:path";
23
+
24
+ import { DATA_DIR } from "./constants.mjs";
25
+
26
+ const LEDGER_DIRNAME = "fund-observations";
27
+
28
+ /** Keep a bounded history per fund; a flow only ever reads the two most recent rows. */
29
+ export const MAX_OBSERVATIONS_PER_FUND = 400;
30
+
31
+ const SYMBOL = /^[A-Z0-9][A-Z0-9.\-^]{0,15}$/u;
32
+
33
+ export function observationsRoot(dataDir = DATA_DIR) {
34
+ return join(dataDir, LEDGER_DIRNAME);
35
+ }
36
+
37
+ /**
38
+ * The ledger file for one symbol.
39
+ *
40
+ * A symbol arrives from a caller and becomes a path, so it is validated against a shape rather
41
+ * than escaped: anything with a separator or a traversal segment is refused outright.
42
+ */
43
+ export function observationFile(symbol, dataDir = DATA_DIR) {
44
+ const upper = String(symbol || "").trim().toUpperCase();
45
+ if (!SYMBOL.test(upper)) throw new Error(`unsafe fund symbol for a ledger path: ${JSON.stringify(symbol)}`);
46
+ const root = observationsRoot(dataDir);
47
+ const file = resolve(root, `${upper.replace(/[^A-Z0-9]/gu, "_")}.json`);
48
+ if (dirname(file) !== resolve(root)) throw new Error(`ledger path escaped its root: ${file}`);
49
+ return file;
50
+ }
51
+
52
+ function readLedger(file) {
53
+ if (!existsSync(file)) return [];
54
+ try {
55
+ const parsed = JSON.parse(readFileSync(file, "utf8"));
56
+ return Array.isArray(parsed?.observations) ? parsed.observations : [];
57
+ } catch {
58
+ // A corrupt ledger is not a reason to fail a research run. It is a reason to start a new
59
+ // one, and to lose the history rather than to report a flow computed from garbage.
60
+ return [];
61
+ }
62
+ }
63
+
64
+ function finite(value) {
65
+ return typeof value === "number" && Number.isFinite(value);
66
+ }
67
+
68
+ /**
69
+ * Record one dated observation and return the ledger.
70
+ *
71
+ * Idempotent per as-of date: the same trading day seen twice does not create two rows, because
72
+ * a flow between two rows of the same date is zero and would read as a real zero.
73
+ */
74
+ export function recordFundObservation({
75
+ symbol,
76
+ asOf,
77
+ sharesOutstanding = null,
78
+ sharesBasis = null,
79
+ nav = null,
80
+ netAssets = null,
81
+ sourceUrl = null,
82
+ dataDir = DATA_DIR,
83
+ } = {}) {
84
+ if (!asOf || !/^\d{4}-\d{2}-\d{2}$/u.test(String(asOf))) return { observations: [], recorded: false };
85
+ if (!finite(sharesOutstanding) && !finite(netAssets)) return { observations: [], recorded: false };
86
+ const file = observationFile(symbol, dataDir);
87
+ const existing = readLedger(file);
88
+ if (existing.some((row) => row.as_of === asOf)) return { observations: existing, recorded: false };
89
+ const observations = [...existing, {
90
+ as_of: asOf,
91
+ shares_outstanding: finite(sharesOutstanding) ? sharesOutstanding : null,
92
+ // How the count was obtained decides whether it may price a flow. See `flowInputs`.
93
+ shares_basis: sharesBasis || null,
94
+ nav: finite(nav) ? nav : null,
95
+ net_assets: finite(netAssets) ? netAssets : null,
96
+ source_url: sourceUrl || null,
97
+ }]
98
+ .sort((left, right) => left.as_of.localeCompare(right.as_of))
99
+ .slice(-MAX_OBSERVATIONS_PER_FUND);
100
+ mkdirSync(dirname(file), { recursive: true });
101
+ writeFileSync(file, `${JSON.stringify({ schema_version: 1, symbol: String(symbol).toUpperCase(), observations }, null, 2)}\n`, "utf8");
102
+ return { observations, recorded: true };
103
+ }
104
+
105
+ /**
106
+ * Share counts an issuer actually published. Nothing else may price a flow.
107
+ *
108
+ * Where an issuer publishes no count this repository reconstructs one from disclosed assets
109
+ * over price, which is fine for a market capitalisation -- the product is the size we already
110
+ * had. It is NOT fine for a flow. A flow is a DIFFERENCE of two counts, so it subtracts away
111
+ * the number and leaves the noise: assets carry the error of a 95%-priced position sum, price
112
+ * carries the fund's premium or discount, and half a percent at each end of a $300bn fund
113
+ * manufactures $1.5bn of flow that never happened. Worse, that error is not centred on zero
114
+ * from a reader's point of view -- it appears as conviction exactly when the market moved.
115
+ */
116
+ const FILED_SHARE_BASES = new Set([
117
+ "issuer_disclosed_shares_outstanding",
118
+ // Assets and net asset value published by the same issuer on the same date. Their ratio is
119
+ // shares outstanding by definition, not an approximation of it, so differencing two of them
120
+ // is differencing two share counts.
121
+ "issuer_aum_over_nav",
122
+ ]);
123
+
124
+ /**
125
+ * The two most recent observations that can price a flow, and how far apart they are.
126
+ *
127
+ * The gap matters and is returned rather than hidden: a flow measured across forty days is not
128
+ * a daily flow, and a reader who cannot see the gap will read it as one.
129
+ */
130
+ export function flowInputs(observations) {
131
+ const priced = (observations || []).filter((row) => (
132
+ finite(row?.shares_outstanding) && FILED_SHARE_BASES.has(row?.shares_basis)
133
+ ));
134
+ if (priced.length < 2) return null;
135
+ const prior = priced.at(-2);
136
+ const latest = priced.at(-1);
137
+ const nav = finite(latest.nav)
138
+ ? latest.nav
139
+ : (finite(latest.net_assets) && latest.shares_outstanding > 0
140
+ ? latest.net_assets / latest.shares_outstanding
141
+ : null);
142
+ if (!finite(nav) || nav <= 0) return null;
143
+ return {
144
+ sharesNow: latest.shares_outstanding,
145
+ sharesPrior: prior.shares_outstanding,
146
+ nav,
147
+ asOf: latest.as_of,
148
+ priorAsOf: prior.as_of,
149
+ gapDays: Math.round((Date.parse(latest.as_of) - Date.parse(prior.as_of)) / 86_400_000),
150
+ netAssets: finite(latest.net_assets) ? latest.net_assets : latest.shares_outstanding * nav,
151
+ };
152
+ }
@@ -717,7 +717,12 @@ function shareCount(series, cik, gaps) {
717
717
  valueKind: "count",
718
718
  value: Math.round(entry.value),
719
719
  unit: "shares",
720
- alignment,
720
+ // A count is carried as a point-in-time quantity rather than as a span. It exists to be a
721
+ // denominator under a market capitalisation, where what matters is how many shares there
722
+ // are and not which window they were averaged over -- and a fund's share count, which is
723
+ // a genuine instant, could satisfy no duration contract at all. The averaging basis stays
724
+ // stated in `assumptions` rather than being implied by an interval.
725
+ alignment: { ...alignment, suppressInterval: true },
721
726
  cik,
722
727
  derivation: "reported",
723
728
  inputs: { diluted_shares: entry.value, tag: entry.tag },
package/mcp/lib/funds.mjs CHANGED
@@ -54,11 +54,36 @@ import { fetchText } from "./quotes.mjs";
54
54
  export const FUND_REGISTRY = Object.freeze({
55
55
  IVV: { issuer: "ishares", name: "iShares Core S&P 500 ETF", product_id: "239726", cusip: "464287200" },
56
56
  IWM: { issuer: "ishares", name: "iShares Russell 2000 ETF", product_id: "239710", cusip: "464287655" },
57
+ SOXX: { issuer: "ishares", name: "iShares Semiconductor ETF", product_id: "239705", cusip: "464287523" },
58
+ IYW: { issuer: "ishares", name: "iShares U.S. Technology ETF", product_id: "239522", cusip: "464287721" },
59
+ IYF: { issuer: "ishares", name: "iShares U.S. Financials ETF", product_id: "239508", cusip: "464287788" },
60
+ IYH: { issuer: "ishares", name: "iShares U.S. Healthcare ETF", product_id: "239511", cusip: "464287762" },
61
+ IYE: { issuer: "ishares", name: "iShares U.S. Energy ETF", product_id: "239507", cusip: "464287796" },
62
+ IBB: { issuer: "ishares", name: "iShares Biotechnology ETF", product_id: "239699", cusip: "464287556" },
63
+ IGV: { issuer: "ishares", name: "iShares Expanded Tech-Software Sector ETF", product_id: "239771", cusip: "464287515" },
64
+ ITB: { issuer: "ishares", name: "iShares U.S. Home Construction ETF", product_id: "239512", cusip: "464288752" },
65
+ IWF: { issuer: "ishares", name: "iShares Russell 1000 Growth ETF", product_id: "239706", cusip: "464287614" },
66
+ IWD: { issuer: "ishares", name: "iShares Russell 1000 Value ETF", product_id: "239708", cusip: "464287598" },
67
+ IJH: { issuer: "ishares", name: "iShares Core S&P Mid-Cap ETF", product_id: "239763", cusip: "464287507" },
68
+ IJR: { issuer: "ishares", name: "iShares Core S&P Small-Cap ETF", product_id: "239774", cusip: "464287804" },
69
+ IEFA: { issuer: "ishares", name: "iShares Core MSCI EAFE ETF", product_id: "244049", cusip: "46432F842" },
70
+ EEM: { issuer: "ishares", name: "iShares MSCI Emerging Markets ETF", product_id: "239637", cusip: "464287234" },
57
71
  EFA: { issuer: "ishares", name: "iShares MSCI EAFE ETF", product_id: "239623", cusip: "464287465" },
58
72
  AGG: { issuer: "ishares", name: "iShares Core U.S. Aggregate Bond ETF", product_id: "239458", cusip: "464287226" },
59
73
  TLT: { issuer: "ishares", name: "iShares 20+ Year Treasury Bond ETF", product_id: "239454", cusip: "464287432" },
60
74
  HYG: { issuer: "ishares", name: "iShares iBoxx $ High Yield Corporate Bond ETF", product_id: "239565", cusip: "464288513" },
61
75
  SPY: { issuer: "ssga", name: "SPDR S&P 500 ETF Trust", slug: "spy", cusip: "78462F103" },
76
+ XLK: { issuer: "ssga", name: "Technology Select Sector SPDR Fund", slug: "xlk" },
77
+ XLF: { issuer: "ssga", name: "Financial Select Sector SPDR Fund", slug: "xlf" },
78
+ XLE: { issuer: "ssga", name: "Energy Select Sector SPDR Fund", slug: "xle" },
79
+ XLV: { issuer: "ssga", name: "Health Care Select Sector SPDR Fund", slug: "xlv" },
80
+ XLI: { issuer: "ssga", name: "Industrial Select Sector SPDR Fund", slug: "xli" },
81
+ XLY: { issuer: "ssga", name: "Consumer Discretionary Select Sector SPDR Fund", slug: "xly" },
82
+ XLP: { issuer: "ssga", name: "Consumer Staples Select Sector SPDR Fund", slug: "xlp" },
83
+ XLU: { issuer: "ssga", name: "Utilities Select Sector SPDR Fund", slug: "xlu" },
84
+ XLB: { issuer: "ssga", name: "Materials Select Sector SPDR Fund", slug: "xlb" },
85
+ XLRE: { issuer: "ssga", name: "Real Estate Select Sector SPDR Fund", slug: "xlre" },
86
+ XLC: { issuer: "ssga", name: "Communication Services Select Sector SPDR Fund", slug: "xlc" },
62
87
  QQQ: { issuer: "invesco", name: "Invesco QQQ Trust Series 1", cusip: "46090E103" },
63
88
  QQQM: { issuer: "invesco", name: "Invesco NASDAQ 100 ETF", cusip: "46138G649" },
64
89
  VOO: { issuer: "vanguard", name: "Vanguard S&P 500 ETF", ticker: "VOO", cusip: "922908363" },
@@ -462,7 +487,17 @@ export const LOOK_THROUGH_METRICS = Object.freeze({
462
487
  "growth.revenue_growth": { kind: "rate", aggregation: "weighted_arithmetic_mean", reciprocal_of: null },
463
488
  "growth.earnings_growth": { kind: "rate", aggregation: "weighted_arithmetic_mean", reciprocal_of: null },
464
489
  "leverage.debt_to_equity": { kind: "ratio", aggregation: "weighted_arithmetic_mean", reciprocal_of: null },
490
+ // Cash conversion sits around one and is bounded in practice, so the weighted mean of the
491
+ // ratios is the portfolio ratio to within the usual dispersion caveat.
492
+ "accounting.cash_conversion": { kind: "ratio", aggregation: "weighted_arithmetic_mean", reciprocal_of: null },
493
+ "profitability.incremental_return_on_capital": { kind: "rate", aggregation: "weighted_arithmetic_mean", reciprocal_of: null },
494
+ // Interest coverage is a MULTIPLE, and an arithmetic mean of multiples is dominated by
495
+ // whichever constituent carries almost no debt -- one company at 900x drags a basket of
496
+ // thinly covered names to "comfortably covered". The harmonic mean averages the thing that
497
+ // actually adds up across a portfolio, which is interest expense per unit of profit.
498
+ "coverage.interest_coverage": { kind: "price_multiple", aggregation: "weighted_harmonic_mean", reciprocal_of: null },
465
499
  "leverage.net_debt_to_ebitda": { kind: "ratio", aggregation: "weighted_arithmetic_mean", reciprocal_of: null },
500
+ "profitability.return_on_equity_10y": { kind: "rate", aggregation: "weighted_arithmetic_mean", reciprocal_of: null },
466
501
  });
467
502
 
468
503
  /**
@@ -4,6 +4,9 @@ import { getMacroSnapshot } from "./macro.mjs";
4
4
  import { fetchMacroSeries } from "./fred.mjs";
5
5
  import { fetchFundamentals } from "./fundamentals.mjs";
6
6
  import { fetchInsiderOwnership } from "./insider-ownership.mjs";
7
+ import { INDEX_PROXIES, normalizeIndexSymbol } from "./index-aggregate.mjs";
8
+ import { SECTOR_SPDRS, fetchCrossMarket, fetchSectorDispersion } from "./cross-market.mjs";
9
+ import { fetchBasketNews } from "./basket-news.mjs";
7
10
  import { gatherInstrumentFacts, LOOK_THROUGH_FACT_IDS } from "./instrument-facts.mjs";
8
11
  import { fetchOptionsChain } from "./options.mjs";
9
12
  import { screenTicker } from "./screen.mjs";
@@ -77,6 +80,11 @@ async function safely(label, fn) {
77
80
  * @param {boolean} [options.options] include the delayed CBOE option-chain digest for US listings
78
81
  * @param {string} [options.asOf] only use filings filed by this date
79
82
  */
83
+ /** The tracking ETF whose listed chain stands in for a cash index. */
84
+ function indexProxyEtf(symbol) {
85
+ return INDEX_PROXIES[normalizeIndexSymbol(symbol)]?.etf || null;
86
+ }
87
+
80
88
  export async function gatherGrounding({
81
89
  symbol,
82
90
  cik,
@@ -170,6 +178,21 @@ export async function gatherGrounding({
170
178
  }));
171
179
  } else if (symbol && options && symbolMarket?.id === "US" && !out.instrument.index_like) {
172
180
  out.unavailable.push("options chain: historical cutoff requires an archived chain; current CBOE snapshot was not fetched");
181
+ } else if (symbol && options && out.instrument.index_like && indexProxyEtf(symbol) && snapshotPolicy.allowed) {
182
+ // A cash index has no chain of its own on this adapter, and its tracking ETF does. The
183
+ // holdings path already answers "the index itself is licensed, so read the tracker and say
184
+ // so"; a volatility surface is the same problem and gets the same answer. What must not
185
+ // happen is the substitution going unlabelled -- an ETF's implied volatility is not the
186
+ // index's, and a reader comparing them needs to know which one this is.
187
+ const proxy = indexProxyEtf(symbol);
188
+ jobs.push(safely("options chain", () => fetchOptionsChain(proxy, { asOf, signal })).then((r) => {
189
+ if (r.ok && r.value?.available) {
190
+ out.options = { ...r.value, proxy_for: normalizeIndexSymbol(symbol), is_proxy: true,
191
+ proxy_note: `${proxy} option chain used as an explicit proxy; the cash index has no chain on this adapter` };
192
+ } else {
193
+ out.unavailable.push(r.ok ? `options chain via ${proxy}: ${r.value?.reason || "unavailable"}` : r.error);
194
+ }
195
+ }));
173
196
  } else if (symbol && options && out.instrument.index_like) {
174
197
  out.not_applicable.push(localizedText(language, {
175
198
  en: "CBOE equity/ETF option-chain adapter: direct cash-index symbol is not supported; use the appropriate listed derivative or ETF proxy explicitly",
@@ -179,6 +202,22 @@ export async function gatherGrounding({
179
202
  }));
180
203
  }
181
204
 
205
+ // What else is this a bet on. Only for baskets: a single company's correlation to KOSPI is a
206
+ // fact about its sector, not about the company, and the seats that reason about crowding and
207
+ // position size are asking about the basket.
208
+ if (symbol && isFundOrIndex(out.instrument)) {
209
+ jobs.push(safely("cross-market", () => fetchCrossMarket(symbol, { signal })).then((r) => {
210
+ if (!r.ok) { out.unavailable.push(r.error); return; }
211
+ if (r.value.facts.length) out.cross_market = r.value.facts;
212
+ out.unavailable.push(...r.value.unavailable);
213
+ }));
214
+ jobs.push(safely("sector dispersion", () => fetchSectorDispersion(SECTOR_SPDRS, { signal })).then((r) => {
215
+ if (!r.ok) { out.unavailable.push(r.error); return; }
216
+ if (r.value.available) out.sector_dispersion = r.value;
217
+ out.unavailable.push(...(r.value.unavailable || []));
218
+ }));
219
+ }
220
+
182
221
  if (cik) {
183
222
  if (snapshotPolicy.allowed) {
184
223
  // Resolve the registrant before deciding whether Company Facts applies. Scheduling
@@ -285,7 +324,7 @@ export async function gatherGrounding({
285
324
  // instead: published holdings, index-level valuation and the look-through aggregates that
286
325
  // let an operating-company method run against a basket at all.
287
326
  if (symbol && isFundOrIndex(out.instrument) && snapshotPolicy.allowed) {
288
- jobs.push(safely("instrument aggregate", () => gatherInstrumentFacts({
327
+ const instrumentJob = safely("instrument aggregate", () => gatherInstrumentFacts({
289
328
  symbol, instrument: out.instrument, asOf, signal,
290
329
  // The operating-company facts a basket can supply at all: everything the method seats
291
330
  // ask of a company, aggregated by weight across the constituents that publish it.
@@ -294,7 +333,21 @@ export async function gatherGrounding({
294
333
  if (!r.ok) { out.unavailable.push(r.error); return; }
295
334
  out.instrument_aggregate = r.value;
296
335
  out.unavailable.push(...(r.value.unavailable || []));
297
- }));
336
+ });
337
+ jobs.push(instrumentJob);
338
+ // A basket has no press office and files nothing, so its news comes from what it holds.
339
+ // Chained after the instrument pass rather than run beside it, because the holdings are
340
+ // what name the industry -- fetching them twice to avoid the wait would cost more than it
341
+ // saves.
342
+ jobs.push((async () => {
343
+ await instrumentJob;
344
+ const holdings = out.instrument_aggregate?.holdings;
345
+ if (!holdings?.length) return;
346
+ const news = await safely("basket news", () => fetchBasketNews(holdings, { asOf, signal }));
347
+ if (!news.ok) { out.unavailable.push(news.error); return; }
348
+ if (news.value.available) out.basket_news = news.value;
349
+ out.unavailable.push(...(news.value.unavailable || []));
350
+ })());
298
351
  }
299
352
 
300
353
  if (symbol && isFundOrIndex(out.instrument)) {
@@ -124,6 +124,20 @@ export const INDEX_PROXIES = Object.freeze({
124
124
  licensor: "Nasdaq, Inc.",
125
125
  why: "Nasdaq publishes NDX membership itself, but weights still come from the tracking ETF, so the same proxy labelling applies",
126
126
  }),
127
+ "^SOX": Object.freeze({
128
+ index_name: "PHLX Semiconductor Sector",
129
+ etf: "SOXX",
130
+ alternates: Object.freeze(["SMH", "SOXQ"]),
131
+ licensor: "Nasdaq, Inc.",
132
+ why: "PHLX Semiconductor membership and its modified-cap weights are a licensed Nasdaq asset; the free path is a tracking ETF's published holdings, labelled as a proxy",
133
+ }),
134
+ "^RUT": Object.freeze({
135
+ index_name: "Russell 2000",
136
+ etf: "IWM",
137
+ alternates: Object.freeze(["VTWO"]),
138
+ licensor: "FTSE Russell",
139
+ why: "Russell 2000 membership and weights are a licensed FTSE Russell asset; the free path is a tracking ETF's published holdings, labelled as a proxy",
140
+ }),
127
141
  "^DJI": Object.freeze({
128
142
  index_name: "Dow Jones Industrial Average",
129
143
  etf: "DIA",
@@ -137,7 +151,8 @@ const INDEX_ALIASES = Object.freeze({
137
151
  GSPC: "^GSPC", "^GSPC": "^GSPC", SPX: "^GSPC", "^SPX": "^GSPC", SP500: "^GSPC",
138
152
  NDX: "^NDX", "^NDX": "^NDX", NASDAQ100: "^NDX",
139
153
  DJI: "^DJI", "^DJI": "^DJI", DJIA: "^DJI",
140
- RUT: "^RUT", "^RUT": "^RUT",
154
+ RUT: "^RUT", "^RUT": "^RUT", RUSSELL2000: "^RUT",
155
+ SOX: "^SOX", "^SOX": "^SOX", SOXX: "^SOX", PHLXSEMI: "^SOX",
141
156
  IXIC: "^IXIC", "^IXIC": "^IXIC",
142
157
  });
143
158
 
@@ -119,24 +119,45 @@ export const SIC_GROUPS = [
119
119
  { id: "mining_energy", range: [1000, 1499], title: { zh: "采矿与能源", en: "Mining and energy" } },
120
120
  { id: "construction", range: [1500, 1799], title: { zh: "建筑", en: "Construction" } },
121
121
  { id: "food_beverage", range: [2000, 2199], title: { zh: "食品饮料", en: "Food and beverage" } },
122
+ { id: "textiles_apparel", range: [2200, 2399], title: { zh: "纺织服装", en: "Textiles and apparel" } },
123
+ { id: "paper_packaging", range: [2400, 2679], title: { zh: "造纸与包装", en: "Paper and packaging" } },
124
+ { id: "publishing", range: [2700, 2799], title: { zh: "出版印刷", en: "Publishing and printing" } },
122
125
  { id: "chemicals", range: [2800, 2899], title: { zh: "化工", en: "Chemicals" } },
123
126
  { id: "pharma_biotech", range: [2833, 2836], title: { zh: "医药与生物科技", en: "Pharma and biotech" } },
124
127
  { id: "energy_refining", range: [2900, 2999], title: { zh: "炼化", en: "Refining" } },
128
+ { id: "rubber_plastics", range: [3000, 3199], title: { zh: "橡胶塑料", en: "Rubber and plastics" } },
129
+ { id: "building_materials", range: [3200, 3299], title: { zh: "建材", en: "Building materials" } },
125
130
  { id: "metals", range: [3300, 3399], title: { zh: "金属", en: "Metals" } },
131
+ { id: "fabricated_metal", range: [3400, 3499], title: { zh: "金属制品", en: "Fabricated metal products" } },
126
132
  { id: "machinery", range: [3500, 3569], title: { zh: "机械", en: "Machinery" } },
127
133
  { id: "computers_hardware", range: [3570, 3579], title: { zh: "计算机硬件", en: "Computers and hardware" } },
134
+ { id: "industrial_equipment", range: [3580, 3599], title: { zh: "工业设备", en: "Industrial equipment" } },
128
135
  { id: "electronics", range: [3600, 3673], title: { zh: "电子", en: "Electronics" } },
129
136
  { id: "semiconductors", range: [3674, 3674], title: { zh: "半导体", en: "Semiconductors" } },
130
- { id: "instruments_medical", range: [3820, 3873], title: { zh: "仪器与医疗器械", en: "Instruments and medical devices" } },
137
+ { id: "electrical_equipment", range: [3675, 3699], title: { zh: "电气设备", en: "Electrical equipment" } },
138
+ { id: "autos", range: [3700, 3719], title: { zh: "汽车", en: "Automobiles" } },
139
+ { id: "aerospace_defense", range: [3720, 3799], title: { zh: "航空航天与国防", en: "Aerospace and defense" } },
140
+ { id: "instruments_medical", range: [3800, 3879], title: { zh: "仪器与医疗器械", en: "Instruments and medical devices" } },
141
+ { id: "manufacturing_other", range: [3880, 3999], title: { zh: "其他制造", en: "Other manufacturing" } },
131
142
  { id: "transportation", range: [4000, 4799], title: { zh: "运输", en: "Transportation" } },
132
143
  { id: "telecom", range: [4800, 4899], title: { zh: "电信", en: "Telecom" } },
133
144
  { id: "utilities", range: [4900, 4999], title: { zh: "公用事业", en: "Utilities" } },
145
+ { id: "wholesale", range: [5000, 5199], title: { zh: "批发分销", en: "Wholesale and distribution" } },
134
146
  { id: "retail", range: [5200, 5999], title: { zh: "零售", en: "Retail" } },
135
- { id: "banks", range: [6020, 6199], title: { zh: "银行", en: "Banks" } },
147
+ { id: "banks", range: [6000, 6199], title: { zh: "银行", en: "Banks" } },
148
+ { id: "brokers_asset_managers", range: [6200, 6299], title: { zh: "券商与资产管理", en: "Brokers and asset managers" } },
136
149
  { id: "insurance", range: [6300, 6411], title: { zh: "保险", en: "Insurance" } },
150
+ { id: "financial_services", range: [6412, 6499], title: { zh: "金融服务", en: "Financial services" } },
137
151
  { id: "real_estate", range: [6500, 6799], title: { zh: "房地产", en: "Real estate" } },
152
+ { id: "hospitality_leisure", range: [7000, 7099], title: { zh: "酒店与休闲", en: "Hospitality and leisure" } },
153
+ { id: "business_services", range: [7100, 7369], title: { zh: "商业服务", en: "Business services" } },
138
154
  { id: "software", range: [7370, 7379], title: { zh: "软件与IT服务", en: "Software and IT services" } },
155
+ { id: "consumer_services", range: [7380, 7799], title: { zh: "消费服务", en: "Consumer services" } },
156
+ { id: "media_entertainment", range: [7800, 7999], title: { zh: "影视与娱乐", en: "Media and entertainment" } },
139
157
  { id: "healthcare_services", range: [8000, 8099], title: { zh: "医疗服务", en: "Healthcare services" } },
158
+ { id: "professional_services", range: [8100, 8730], title: { zh: "专业服务", en: "Professional services" } },
159
+ { id: "research_services", range: [8731, 8734], title: { zh: "研究服务", en: "Research services" } },
160
+ { id: "other_services", range: [8735, 8999], title: { zh: "其他服务", en: "Other services" } },
140
161
  ];
141
162
 
142
163
  export function sicGroupFor(sic) {