alphacouncil-agent 1.0.1 → 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 (31) 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/CHANGELOG.md +37 -0
  5. package/data/authored/core-seats.mjs +3 -3
  6. package/data/authored/quant-seats.mjs +2 -2
  7. package/data/authored/value-seats.mjs +4 -4
  8. package/data/build-profile.v1.json +1 -1
  9. package/knowledge/ai-assisted-solo/experiments/runs/a.json +2 -2
  10. package/knowledge/ai-assisted-solo/experiments/runs/b.json +2 -2
  11. package/knowledge/ai-assisted-solo/experiments/runs/c.json +2 -2
  12. package/knowledge/ai-assisted-solo/experiments/runs/d13.json +15 -15
  13. package/knowledge/ai-assisted-solo/experiments/runs/d26.json +21 -21
  14. package/knowledge/ai-assisted-solo/experiments/runs/e-d13.json +18 -18
  15. package/knowledge/ai-assisted-solo/experiments/runs/e-d26.json +24 -24
  16. package/knowledge/ai-assisted-solo/experiments/runs/h_ai_reference.json +25 -25
  17. package/knowledge/ai-assisted-solo/experiments/simulation-input.json +4 -4
  18. package/knowledge/ai-assisted-solo/experiments/simulation-manifest.json +19 -19
  19. package/knowledge/solo-test/masters/master_graham/decision_policy.json +1 -1
  20. package/knowledge/solo-test/masters/master_klarman/decision_policy.json +1 -1
  21. package/knowledge/solo-test/masters/master_pabrai/decision_policy.json +1 -1
  22. package/mcp/lib/basket-news.mjs +181 -0
  23. package/mcp/lib/breadth.mjs +24 -0
  24. package/mcp/lib/cross-market.mjs +180 -0
  25. package/mcp/lib/grounding.mjs +34 -2
  26. package/mcp/lib/industry.mjs +23 -2
  27. package/mcp/lib/instrument-facts.mjs +3 -0
  28. package/mcp/lib/markdown.mjs +41 -8
  29. package/mcp/lib/personas-v3/grounding-adapter.mjs +139 -0
  30. package/package.json +1 -1
  31. package/scripts/lib/persona-v3-solo-formula-pipeline.mjs +11 -0
@@ -0,0 +1,180 @@
1
+ /**
2
+ * How a basket moves against other markets, and how its own sectors are moving apart.
3
+ *
4
+ * Every seat that reasons about crowding, reflexivity or position size needs to know what else
5
+ * a holding is a bet on. Dalio's own authored policy limits size by correlation; Soros needs a
6
+ * loop that runs between markets; Marks reads dispersion as where the cycle stands. None of
7
+ * them could ask, because nothing here compared one market with another.
8
+ *
9
+ * The inputs are daily closes, from the same keyless endpoint the breadth pass already uses,
10
+ * so a correlation costs one fetch per reference series and nothing per constituent.
11
+ *
12
+ * Two disciplines are enforced rather than assumed:
13
+ *
14
+ * 1. **Sessions are aligned by date, not by position.** Korea and the United States do not
15
+ * share a holiday calendar, and zipping two arrays by index silently compares a Tuesday
16
+ * with a Wednesday — which produces a correlation that is confidently wrong rather than
17
+ * obviously broken.
18
+ * 2. **A short overlap produces nothing.** A correlation over twenty paired sessions is
19
+ * noise with a decimal point on it.
20
+ */
21
+
22
+ import { LIMITS } from "./constants.mjs";
23
+ import { chartUrl, parseDatedCloses } from "./breadth.mjs";
24
+ import { fetchText } from "./quotes.mjs";
25
+
26
+ /** Windows the facts are published over. Both are stated on the fact, never implied. */
27
+ export const CORRELATION_WINDOW_DAYS = 60;
28
+
29
+ /** Below this many paired sessions a correlation is not reported at all. */
30
+ export const MIN_PAIRED_SESSIONS = 40;
31
+
32
+ /**
33
+ * Reference markets a US basket is worth being correlated against.
34
+ *
35
+ * Korea is here because it is the clearest listed read on the semiconductor and export cycle
36
+ * outside the United States: KOSPI's index weight is dominated by memory and display, so a
37
+ * semiconductor basket that has decoupled from it has decoupled from its own end demand.
38
+ */
39
+ export const REFERENCE_MARKETS = Object.freeze({
40
+ "^GSPC": { label: "S&P 500", why: "the broad US market a US basket is measured against" },
41
+ "^KS11": { label: "KOSPI", why: "Korea's main board, weighted toward memory and export manufacturing" },
42
+ "^KQ11": { label: "KOSDAQ", why: "Korea's growth board, a read on domestic risk appetite rather than exporters" },
43
+ "^SOX": { label: "PHLX Semiconductor", why: "the semiconductor cycle as a listed series" },
44
+ });
45
+
46
+ /** The eleven Select Sector SPDRs, which between them partition the S&P 500. */
47
+ export const SECTOR_SPDRS = Object.freeze(["XLK","XLF","XLE","XLV","XLI","XLY","XLP","XLU","XLB","XLRE","XLC"]);
48
+
49
+ const finite = (value) => typeof value === "number" && Number.isFinite(value);
50
+
51
+ /** Pearson correlation of two equal-length return series. */
52
+ export function correlation(left, right) {
53
+ if (!Array.isArray(left) || left.length !== right?.length || left.length < 2) return null;
54
+ const n = left.length;
55
+ const meanLeft = left.reduce((sum, value) => sum + value, 0) / n;
56
+ const meanRight = right.reduce((sum, value) => sum + value, 0) / n;
57
+ let covariance = 0;
58
+ let varianceLeft = 0;
59
+ let varianceRight = 0;
60
+ for (let index = 0; index < n; index += 1) {
61
+ const dl = left[index] - meanLeft;
62
+ const dr = right[index] - meanRight;
63
+ covariance += dl * dr;
64
+ varianceLeft += dl * dl;
65
+ varianceRight += dr * dr;
66
+ }
67
+ if (varianceLeft <= 0 || varianceRight <= 0) return null;
68
+ return covariance / Math.sqrt(varianceLeft * varianceRight);
69
+ }
70
+
71
+ /**
72
+ * Daily returns for the sessions two series actually share.
73
+ *
74
+ * Aligning on the date is the whole point. Two markets with different holidays produce arrays
75
+ * of different lengths whose positions do not correspond, and comparing them by position is
76
+ * the standard way a cross-market correlation ends up measuring nothing.
77
+ */
78
+ export function alignedReturns(left, right, windowDays = CORRELATION_WINDOW_DAYS) {
79
+ const rightByDate = new Map((right || []).map((row) => [row.date, row.close]));
80
+ const paired = [];
81
+ for (const row of left || []) {
82
+ const other = rightByDate.get(row.date);
83
+ if (finite(row.close) && finite(other)) paired.push({ date: row.date, left: row.close, right: other });
84
+ }
85
+ const window = paired.slice(-(windowDays + 1));
86
+ if (window.length < MIN_PAIRED_SESSIONS) return null;
87
+ const returns = { left: [], right: [], sessions: window.length - 1, from: window[0].date, to: window.at(-1).date };
88
+ for (let index = 1; index < window.length; index += 1) {
89
+ returns.left.push(window[index].left / window[index - 1].left - 1);
90
+ returns.right.push(window[index].right / window[index - 1].right - 1);
91
+ }
92
+ return returns;
93
+ }
94
+
95
+ async function datedCloses(symbol, signal) {
96
+ try {
97
+ return parseDatedCloses(JSON.parse(await fetchText(chartUrl(symbol), LIMITS.QUOTE_FETCH_MS, signal)));
98
+ } catch {
99
+ return null;
100
+ }
101
+ }
102
+
103
+ /**
104
+ * Correlation and relative strength of one symbol against each reference market.
105
+ *
106
+ * Relative strength travels with the correlation on purpose. A correlation of 0.9 says two
107
+ * markets move together and says nothing about which one is winning; a reader given only the
108
+ * first will supply the second from imagination.
109
+ */
110
+ export async function fetchCrossMarket(symbol, { signal, references = REFERENCE_MARKETS } = {}) {
111
+ const facts = [];
112
+ const unavailable = [];
113
+ const subject = await datedCloses(symbol, signal);
114
+ if (!subject?.length) {
115
+ return { facts, unavailable: [`cross-market: no daily closes for ${symbol}`] };
116
+ }
117
+ for (const [reference, meta] of Object.entries(references)) {
118
+ if (String(reference).toUpperCase() === String(symbol).toUpperCase()) continue;
119
+ const other = await datedCloses(reference, signal);
120
+ if (!other?.length) { unavailable.push(`cross-market ${reference}: no daily closes`); continue; }
121
+ const returns = alignedReturns(subject, other);
122
+ if (!returns) {
123
+ unavailable.push(
124
+ `cross-market ${reference}: fewer than ${MIN_PAIRED_SESSIONS} sessions shared with ${symbol};`
125
+ + " the two calendars do not overlap enough to compare",
126
+ );
127
+ continue;
128
+ }
129
+ const rho = correlation(returns.left, returns.right);
130
+ if (!finite(rho)) { unavailable.push(`cross-market ${reference}: a series did not move over the window`); continue; }
131
+ const total = (series) => series.reduce((compound, value) => compound * (1 + value), 1) - 1;
132
+ facts.push({
133
+ reference,
134
+ label: meta.label,
135
+ why: meta.why,
136
+ correlation: Number(rho.toFixed(4)),
137
+ relative_return: Number((total(returns.left) - total(returns.right)).toFixed(6)),
138
+ sessions: returns.sessions,
139
+ from: returns.from,
140
+ to: returns.to,
141
+ });
142
+ }
143
+ return { facts, unavailable };
144
+ }
145
+
146
+ /**
147
+ * How far apart a set of sector baskets has travelled over the window.
148
+ *
149
+ * One number, and it is a real one: the standard deviation of sector total returns. A market
150
+ * whose sectors all return the same thing is being repriced by one factor; a market whose
151
+ * sectors have separated is being repriced by many, and those are different regimes to own.
152
+ */
153
+ export async function fetchSectorDispersion(symbols, { signal } = {}) {
154
+ const returns = [];
155
+ const unavailable = [];
156
+ let window = null;
157
+ for (const symbol of symbols || []) {
158
+ const closes = await datedCloses(symbol, signal);
159
+ const tail = (closes || []).slice(-(CORRELATION_WINDOW_DAYS + 1));
160
+ if (tail.length < MIN_PAIRED_SESSIONS) { unavailable.push(`sector dispersion ${symbol}: too few sessions`); continue; }
161
+ returns.push({ symbol, value: tail.at(-1).close / tail[0].close - 1 });
162
+ window = { from: tail[0].date, to: tail.at(-1).date, sessions: tail.length - 1 };
163
+ }
164
+ if (returns.length < 3) {
165
+ return { available: false, unavailable: [...unavailable, "sector dispersion: fewer than three sectors priced"] };
166
+ }
167
+ const mean = returns.reduce((sum, row) => sum + row.value, 0) / returns.length;
168
+ const variance = returns.reduce((sum, row) => sum + (row.value - mean) ** 2, 0) / returns.length;
169
+ const sorted = [...returns].sort((left, right) => right.value - left.value);
170
+ return {
171
+ available: true,
172
+ dispersion: Number(Math.sqrt(variance).toFixed(6)),
173
+ mean_return: Number(mean.toFixed(6)),
174
+ leader: sorted[0],
175
+ laggard: sorted.at(-1),
176
+ measured: returns.length,
177
+ ...window,
178
+ unavailable,
179
+ };
180
+ }
@@ -5,6 +5,8 @@ import { fetchMacroSeries } from "./fred.mjs";
5
5
  import { fetchFundamentals } from "./fundamentals.mjs";
6
6
  import { fetchInsiderOwnership } from "./insider-ownership.mjs";
7
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";
8
10
  import { gatherInstrumentFacts, LOOK_THROUGH_FACT_IDS } from "./instrument-facts.mjs";
9
11
  import { fetchOptionsChain } from "./options.mjs";
10
12
  import { screenTicker } from "./screen.mjs";
@@ -200,6 +202,22 @@ export async function gatherGrounding({
200
202
  }));
201
203
  }
202
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
+
203
221
  if (cik) {
204
222
  if (snapshotPolicy.allowed) {
205
223
  // Resolve the registrant before deciding whether Company Facts applies. Scheduling
@@ -306,7 +324,7 @@ export async function gatherGrounding({
306
324
  // instead: published holdings, index-level valuation and the look-through aggregates that
307
325
  // let an operating-company method run against a basket at all.
308
326
  if (symbol && isFundOrIndex(out.instrument) && snapshotPolicy.allowed) {
309
- jobs.push(safely("instrument aggregate", () => gatherInstrumentFacts({
327
+ const instrumentJob = safely("instrument aggregate", () => gatherInstrumentFacts({
310
328
  symbol, instrument: out.instrument, asOf, signal,
311
329
  // The operating-company facts a basket can supply at all: everything the method seats
312
330
  // ask of a company, aggregated by weight across the constituents that publish it.
@@ -315,7 +333,21 @@ export async function gatherGrounding({
315
333
  if (!r.ok) { out.unavailable.push(r.error); return; }
316
334
  out.instrument_aggregate = r.value;
317
335
  out.unavailable.push(...(r.value.unavailable || []));
318
- }));
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
+ })());
319
351
  }
320
352
 
321
353
  if (symbol && isFundOrIndex(out.instrument)) {
@@ -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) {
@@ -839,6 +839,9 @@ export async function gatherInstrumentFacts({
839
839
  symbol,
840
840
  research_model: instrument?.research_model || "market_instrument",
841
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,
842
845
  facts,
843
846
  // Several facts have more than one route -- the implied ERP and the valuation percentile
844
847
  // are computed from the published workbook when the index feed cannot supply them -- and
@@ -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\u5728\u672c\u8f6e\u4e0d\u7ed9\u65b9\u5411\uff0c\u5404\u81ea\u7f3a\u7684\u662f\u65b9\u6cd5\u5fc5\u9700\u7684\u8f93\u5165\uff0c\u8fd9\u4e0d\u662f\u770b\u7a7a\u7968\uff1a`,
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 this round, each missing a method-critical input. These are not bearish votes:`,
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\u3044\u305a\u308c\u3082\u30e1\u30bd\u30c3\u30c9\u306b\u5fc5\u8981\u306a\u5165\u529b\u3092\u6b20\u3044\u3066\u304a\u308a\u3001\u5f31\u6c17\u7968\u3067\u306f\u3042\u308a\u307e\u305b\u3093\uff1a`,
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. \uac01\uac01 \ubc29\ubc95\ub860\uc5d0 \ud544\uc694\ud55c \uc785\ub825\uc774 \uc5c6\uc73c\uba70 \uc57d\uc138 \ud22c\ud45c\uac00 \uc544\ub2d9\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
- // One paragraph, not one row per seat. The stable IDs stay visible so the gate and the
187
- // reader can both account for every selected seat without twenty-five identical lines.
188
- const merged = abstained
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
- sections.push(`#### ${copy.abstained}`, `${copy.abstainLead(abstained.length)}\n\n${merged}`);
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 "";
@@ -571,6 +571,143 @@ function fundamentalFacts(grounding, context) {
571
571
  * across the newest ownership document per reporting owner, and the coverage limits of Section
572
572
  * 16 are real. Labelling it reported would claim a document that does not exist.
573
573
  */
574
+ /**
575
+ * What else a basket is a bet on, as typed facts.
576
+ *
577
+ * Correlation to the broad market decides how much diversification a position actually buys,
578
+ * which is the input Dalio's authored policy already asks for and could never get. Correlation
579
+ * to Korea is the semiconductor cycle read from outside the United States. Sector dispersion is
580
+ * whether one factor or many are repricing the market.
581
+ */
582
+ /**
583
+ * News as counts, never as content.
584
+ *
585
+ * A headline cannot reach a seat's arithmetic: the same symbol would answer differently
586
+ * depending on what was published that morning, and a frozen deterministic stance would stop
587
+ * being reproducible. What IS a fact is how much of the basket generated dated news in a stated
588
+ * window, and how many constituents filed an 8-K in it. Both are counts with a date, and both
589
+ * are the kind of thing an event-driven method legitimately asks for.
590
+ *
591
+ * The headlines themselves stay in the grounding for the report and for the voice worker that
592
+ * explains a stance the seat already reached.
593
+ */
594
+ function basketNewsFacts(grounding, context) {
595
+ const news = grounding?.basket_news;
596
+ if (!news?.available) return;
597
+ const publicAt = timestampAtOrBefore(grounding?.gathered_at || context.asOf, context.cutoff);
598
+ if (!publicAt) return;
599
+ const sourceIdValue = sourceId("news", "basket_window", news.window_days, publicAt.slice(0, 10));
600
+ if (!registerSource(context, {
601
+ source_id: sourceIdValue,
602
+ source_kind: "news_aggregate",
603
+ title: `dated items for the basket's largest holdings over ${news.window_days} days`,
604
+ url: "https://news.google.com/rss",
605
+ public_at: publicAt,
606
+ retrieved_at: grounding.gathered_at || publicAt,
607
+ locator: { window_days: news.window_days, constituents_read: news.constituents_read, industry: news.industry?.id || null },
608
+ })) return;
609
+ const shared = {
610
+ asOf: context.asOf,
611
+ publicAt,
612
+ sources: [sourceIdValue],
613
+ confidence: 0.6,
614
+ derivation: "rederived",
615
+ };
616
+ addUnique(context.facts, context.diagnostics, baseFact({
617
+ ...shared,
618
+ factId: "news.covered_weight",
619
+ valueKind: "ratio",
620
+ value: news.coverage_weight,
621
+ unit: "decimal",
622
+ ratioDenominator: `weight_of_${news.constituents_read}_largest_holdings`,
623
+ derivationToolId: `${ADAPTER_ID}:news:covered_weight`,
624
+ derivationInput: { window_days: news.window_days },
625
+ }));
626
+ addUnique(context.facts, context.diagnostics, baseFact({
627
+ ...shared,
628
+ factId: "news.filing_event_weight",
629
+ valueKind: "ratio",
630
+ value: news.filing_event_weight,
631
+ unit: "decimal",
632
+ ratioDenominator: `weight_of_${news.constituents_read}_largest_holdings`,
633
+ derivationToolId: `${ADAPTER_ID}:news:filing_event_weight`,
634
+ derivationInput: { window_days: news.window_days, filers: news.filing_event_count },
635
+ }));
636
+ }
637
+
638
+ function crossMarketFacts(grounding, context) {
639
+ const rows = grounding?.cross_market;
640
+ const stamp = grounding?.gathered_at || context.asOf;
641
+ const publicAt = timestampAtOrBefore(stamp, context.cutoff);
642
+ if (Array.isArray(rows) && publicAt) {
643
+ for (const row of rows) {
644
+ if (!finite(row?.correlation)) continue;
645
+ const factId = CROSS_MARKET_FACTS[row.reference];
646
+ if (!factId) continue;
647
+ const sourceIdValue = sourceId("market", "cross_correlation", row.reference, row.to);
648
+ if (!registerSource(context, {
649
+ source_id: sourceIdValue,
650
+ source_kind: "market_snapshot",
651
+ title: `daily closes for ${row.label}`,
652
+ url: `https://query1.finance.yahoo.com/v8/finance/chart/${encodeURIComponent(row.reference)}?range=1y&interval=1d`,
653
+ public_at: publicAt,
654
+ retrieved_at: stamp,
655
+ locator: { reference: row.reference, from: row.from, to: row.to, sessions: row.sessions },
656
+ })) continue;
657
+ addUnique(context.facts, context.diagnostics, baseFact({
658
+ factId,
659
+ valueKind: "ratio",
660
+ value: row.correlation,
661
+ unit: "decimal",
662
+ ratioDenominator: `pearson_over_${row.sessions}_paired_sessions`,
663
+ asOf: context.asOf,
664
+ publicAt,
665
+ sources: [sourceIdValue],
666
+ confidence: 0.8,
667
+ derivation: "rederived",
668
+ derivationToolId: `${ADAPTER_ID}:cross_market:${row.reference}`,
669
+ derivationInput: { reference: row.reference, from: row.from, to: row.to, relative_return: row.relative_return },
670
+ }));
671
+ }
672
+ }
673
+ const dispersion = grounding?.sector_dispersion;
674
+ if (dispersion?.available && finite(dispersion.dispersion) && publicAt) {
675
+ const sourceIdValue = sourceId("market", "sector_dispersion", dispersion.to);
676
+ if (registerSource(context, {
677
+ source_id: sourceIdValue,
678
+ source_kind: "market_snapshot",
679
+ title: "Select Sector SPDR daily closes",
680
+ url: "https://query1.finance.yahoo.com/v8/finance/chart/XLK?range=1y&interval=1d",
681
+ public_at: publicAt,
682
+ retrieved_at: stamp,
683
+ locator: { sectors: dispersion.measured, from: dispersion.from, to: dispersion.to },
684
+ })) {
685
+ addUnique(context.facts, context.diagnostics, baseFact({
686
+ factId: "market.sector_dispersion",
687
+ valueKind: "ratio",
688
+ value: dispersion.dispersion,
689
+ unit: "decimal",
690
+ ratioDenominator: `stdev_of_${dispersion.measured}_sector_total_returns`,
691
+ asOf: context.asOf,
692
+ publicAt,
693
+ sources: [sourceIdValue],
694
+ confidence: 0.8,
695
+ derivation: "rederived",
696
+ derivationToolId: `${ADAPTER_ID}:sector_dispersion`,
697
+ derivationInput: { leader: dispersion.leader?.symbol, laggard: dispersion.laggard?.symbol, from: dispersion.from, to: dispersion.to },
698
+ }));
699
+ }
700
+ }
701
+ }
702
+
703
+ /** Reference market -> the fact id its correlation is published under. */
704
+ const CROSS_MARKET_FACTS = Object.freeze({
705
+ "^GSPC": "market.correlation_to_broad_market",
706
+ "^KS11": "market.correlation_to_kospi",
707
+ "^KQ11": "market.correlation_to_kosdaq",
708
+ "^SOX": "market.correlation_to_semiconductors",
709
+ });
710
+
574
711
  function insiderOwnershipFacts(grounding, context) {
575
712
  const owned = grounding?.insider_ownership;
576
713
  if (!owned || !finite(owned.value)) return;
@@ -669,6 +806,8 @@ export function adaptGroundingToTypedFacts(grounding, { asOf, knowledgeAsOf = as
669
806
  fundamentalFacts(grounding, context);
670
807
  instrumentAggregateFacts(grounding, context);
671
808
  insiderOwnershipFacts(grounding, context);
809
+ crossMarketFacts(grounding, context);
810
+ basketNewsFacts(grounding, context);
672
811
  for (const family of ["screen", "macro", "market"]) {
673
812
  if (grounding?.[family] && !grounding[family].public_at) {
674
813
  if (family === "screen" && context.diagnostics.some((item) => String(item.source || "").startsWith("screen."))) continue;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "alphacouncil-agent",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "description": "Multi-agent company, ETF and market-index research workflow for Codex & Claude Code: sourced evidence, method seats, bull/bear debate and portfolio-manager synthesis.",
5
5
  "type": "module",
6
6
  "license": "MIT",
@@ -126,6 +126,17 @@ export const CANONICAL_SOLO_TEST_FACT_CONTRACTS = Object.freeze({
126
126
  "fund.aum": Object.freeze({ value_kind: "monetary", unit: "currency_units", period: INSTANT_AS_OF }),
127
127
  "fund.expense_ratio": Object.freeze({ value_kind: "ratio", unit: "decimal", period: INSTANT_AS_OF }),
128
128
  // Creations minus redemptions, priced. A crowding signal, never a valuation one.
129
+ // What else the holding is a bet on. Correlation decides what a position actually
130
+ // diversifies; dispersion says whether one factor or many are repricing the market.
131
+ "market.correlation_to_broad_market": Object.freeze({ value_kind: "ratio", unit: "decimal", period: INSTANT_AS_OF }),
132
+ "market.correlation_to_kospi": Object.freeze({ value_kind: "ratio", unit: "decimal", period: INSTANT_AS_OF }),
133
+ "market.correlation_to_kosdaq": Object.freeze({ value_kind: "ratio", unit: "decimal", period: INSTANT_AS_OF }),
134
+ "market.correlation_to_semiconductors": Object.freeze({ value_kind: "ratio", unit: "decimal", period: INSTANT_AS_OF }),
135
+ // News as counts. The headlines never enter a computation; how much of the basket is in the
136
+ // news, and how much of it filed, are dated quantities an event-driven method may read.
137
+ "news.covered_weight": Object.freeze({ value_kind: "ratio", unit: "decimal", period: INSTANT_AS_OF }),
138
+ "news.filing_event_weight": Object.freeze({ value_kind: "ratio", unit: "decimal", period: INSTANT_AS_OF }),
139
+ "market.sector_dispersion": Object.freeze({ value_kind: "ratio", unit: "decimal", period: INSTANT_AS_OF }),
129
140
  "fund.net_flow": Object.freeze({ value_kind: "monetary", unit: "currency_units", period: INSTANT_AS_OF }),
130
141
  "fund.net_flow_ratio": Object.freeze({ value_kind: "ratio", unit: "decimal", period: INSTANT_AS_OF }),
131
142