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.
- package/.claude-plugin/marketplace.json +2 -2
- package/.claude-plugin/plugin.json +1 -1
- package/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +37 -0
- 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/knowledge/ai-assisted-solo/experiments/runs/a.json +2 -2
- 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 +15 -15
- package/knowledge/ai-assisted-solo/experiments/runs/d26.json +21 -21
- package/knowledge/ai-assisted-solo/experiments/runs/e-d13.json +18 -18
- package/knowledge/ai-assisted-solo/experiments/runs/e-d26.json +24 -24
- package/knowledge/ai-assisted-solo/experiments/runs/h_ai_reference.json +25 -25
- package/knowledge/ai-assisted-solo/experiments/simulation-input.json +4 -4
- package/knowledge/ai-assisted-solo/experiments/simulation-manifest.json +19 -19
- package/knowledge/solo-test/masters/master_graham/decision_policy.json +1 -1
- package/knowledge/solo-test/masters/master_klarman/decision_policy.json +1 -1
- package/knowledge/solo-test/masters/master_pabrai/decision_policy.json +1 -1
- package/mcp/lib/basket-news.mjs +181 -0
- package/mcp/lib/breadth.mjs +24 -0
- package/mcp/lib/cross-market.mjs +180 -0
- package/mcp/lib/grounding.mjs +34 -2
- package/mcp/lib/industry.mjs +23 -2
- package/mcp/lib/instrument-facts.mjs +3 -0
- package/mcp/lib/markdown.mjs +41 -8
- package/mcp/lib/personas-v3/grounding-adapter.mjs +139 -0
- package/package.json +1 -1
- package/scripts/lib/persona-v3-solo-formula-pipeline.mjs +11 -0
|
@@ -44,12 +44,12 @@
|
|
|
44
44
|
"formal_h_satisfied": false,
|
|
45
45
|
"human_reference": false,
|
|
46
46
|
"input_binding": {
|
|
47
|
-
"input_hash": "sha256:
|
|
47
|
+
"input_hash": "sha256:db8f31c70e4b4589561a53127b16985a8b9d09000ba536df3467eacf857d2d0c",
|
|
48
48
|
"path": "simulation-input.json"
|
|
49
49
|
},
|
|
50
50
|
"network_allowed": false,
|
|
51
51
|
"production_effect": "none",
|
|
52
|
-
"result_hash": "sha256:
|
|
52
|
+
"result_hash": "sha256:f910df4d8cc64bb5e25f0a238d23a6b785699d45863ca092b120afc4d4e2c4b6",
|
|
53
53
|
"results": {
|
|
54
54
|
"blocked_fail_closed_count": 1,
|
|
55
55
|
"completion_status": "completed_machine_simulation",
|
|
@@ -81,18 +81,18 @@
|
|
|
81
81
|
"structured_decision_hash": "sha256:1f311deaad734cbe73028eeefe08e2a967ad680a182cc415469bde700f01b490"
|
|
82
82
|
},
|
|
83
83
|
{
|
|
84
|
-
"anonymous_method_hash": "sha256:
|
|
85
|
-
"deterministic_core_hash": "sha256:
|
|
84
|
+
"anonymous_method_hash": "sha256:9f75bea5d4df5ca9568a1f23bb8d85664f07f752b69235b101929e064135f75f",
|
|
85
|
+
"deterministic_core_hash": "sha256:a20f6d80d48d021a5c7b72fef1a74e42f528027710015c962d4d556ac1e6c4da",
|
|
86
86
|
"fact_pack_hash": "sha256:f69c424fb4ab9bb147ca933702857366ea492f8e988bfb2aeeb4e64df2c32706",
|
|
87
|
-
"frozen_decision_hash": "sha256:
|
|
87
|
+
"frozen_decision_hash": "sha256:234e552565bac7e446160dcf086dfde730ede400f03238d57d50d6e231de2ab8",
|
|
88
88
|
"native_state": "provisional_reject",
|
|
89
|
-
"pack_hash": "sha256:
|
|
89
|
+
"pack_hash": "sha256:f960a6c7061ae791935590e8d045478a65ea451335aceaed5c275894a9b8c9d7",
|
|
90
90
|
"persona_id": "master_graham",
|
|
91
|
-
"policy_execution_hash": "sha256:
|
|
91
|
+
"policy_execution_hash": "sha256:d3412e038f48794eaf1822ffbe7492c1756877ba6f9e9a735ae84631c5f0a3be",
|
|
92
92
|
"score_ratio": 0.6666666666666666,
|
|
93
93
|
"stance": "opposed",
|
|
94
94
|
"status": "executed",
|
|
95
|
-
"structured_decision_hash": "sha256:
|
|
95
|
+
"structured_decision_hash": "sha256:50c4dc6b37d568f3ef9fb44d5e6df96934647dc64830e8b76b3751039e962908"
|
|
96
96
|
},
|
|
97
97
|
{
|
|
98
98
|
"anonymous_method_hash": "sha256:8b5e33a808364beb9174b2c54d21b88c86b36a4428a066136db4987b26c1a828",
|
|
@@ -319,32 +319,32 @@
|
|
|
319
319
|
"structured_decision_hash": "sha256:681107df21fe8be65cc2e1ed044625466f9d0ac900b28f8eaaa79614d87d37f7"
|
|
320
320
|
},
|
|
321
321
|
{
|
|
322
|
-
"anonymous_method_hash": "sha256:
|
|
323
|
-
"deterministic_core_hash": "sha256:
|
|
322
|
+
"anonymous_method_hash": "sha256:47bf9d68671a5328275480eea9fea1ca3712ef761bcb23bf76f85643c9116e4f",
|
|
323
|
+
"deterministic_core_hash": "sha256:d899dad2f478d8465e55acf5854a7872484fa53dcad0b310c6cdc4f51c2f133b",
|
|
324
324
|
"fact_pack_hash": "sha256:0b481c9b13e34ec6465e5007d364473d71c462e3940726744fa6f91b9646f4d3",
|
|
325
|
-
"frozen_decision_hash": "sha256:
|
|
325
|
+
"frozen_decision_hash": "sha256:40c289c8d230547674eb3a04cc029b91ad3b4781093cb135152cc7ef6a8a3ef6",
|
|
326
326
|
"native_state": "provisional_absolute_return_candidate",
|
|
327
|
-
"pack_hash": "sha256:
|
|
327
|
+
"pack_hash": "sha256:a09fec122e5d4f43d8885a5b00e354d259768c7f4648c9f0eebb358cf0461134",
|
|
328
328
|
"persona_id": "master_klarman",
|
|
329
|
-
"policy_execution_hash": "sha256:
|
|
329
|
+
"policy_execution_hash": "sha256:67d06b995c5ef1fa3ce84e344cb0c2d30a703a3682144e2a89d4f10314624109",
|
|
330
330
|
"score_ratio": 1,
|
|
331
331
|
"stance": "constructive",
|
|
332
332
|
"status": "executed",
|
|
333
|
-
"structured_decision_hash": "sha256:
|
|
333
|
+
"structured_decision_hash": "sha256:095f5331acb2dcbbe883f49592ff723eb27495f82a9e1fcf74336e6301b9f08f"
|
|
334
334
|
},
|
|
335
335
|
{
|
|
336
|
-
"anonymous_method_hash": "sha256:
|
|
337
|
-
"deterministic_core_hash": "sha256:
|
|
336
|
+
"anonymous_method_hash": "sha256:992b5268a4549c4e5723c6dceb0b65a0aa1f03dbe906dca95da34e5b0469e344",
|
|
337
|
+
"deterministic_core_hash": "sha256:8c2b1adcaab9d55ff5bab83b7ed62fa4250dd65acd3988b21571c4b6d71c1d8a",
|
|
338
338
|
"fact_pack_hash": "sha256:6735e9705be48f6b9c19db94360b13ad6727db15e288c4e1225664ddd1d1a5a1",
|
|
339
|
-
"frozen_decision_hash": "sha256:
|
|
339
|
+
"frozen_decision_hash": "sha256:779b9a62a801452bc1b48e809692f25d10348c43978a44d9e860b2706d84dbfe",
|
|
340
340
|
"native_state": "provisional_watch",
|
|
341
|
-
"pack_hash": "sha256:
|
|
341
|
+
"pack_hash": "sha256:8ee9e13deaadaeef3065feb7bb5b39382181338cf1055b769b153cad4c308ce4",
|
|
342
342
|
"persona_id": "master_pabrai",
|
|
343
|
-
"policy_execution_hash": "sha256:
|
|
343
|
+
"policy_execution_hash": "sha256:87a2a8034242271ddea852302c001d0e0ef35de1b75cd01c5041d436693da2b0",
|
|
344
344
|
"score_ratio": 0.6666666666666666,
|
|
345
345
|
"stance": "cautious",
|
|
346
346
|
"status": "executed",
|
|
347
|
-
"structured_decision_hash": "sha256:
|
|
347
|
+
"structured_decision_hash": "sha256:200220879e34db72003a0570b0408c74da25aaa505bb72f015efed0cf974fc97"
|
|
348
348
|
},
|
|
349
349
|
{
|
|
350
350
|
"anonymous_method_hash": "sha256:897de3a4d8083b8a41f2203cc3e6b22c19ba01418d957d8c7272d39f41e1b9ba",
|
|
@@ -434,7 +434,7 @@
|
|
|
434
434
|
"deterministic_execution_count": 27,
|
|
435
435
|
"executed_count": 26,
|
|
436
436
|
"formal_results": null,
|
|
437
|
-
"machine_reference_digest": "sha256:
|
|
437
|
+
"machine_reference_digest": "sha256:dbb01a7bdcaccf19f2f022ca64586dd0866fe1ce4f3d5c33ffad61efccc8e997",
|
|
438
438
|
"metric_claims": [],
|
|
439
439
|
"network_call_count": 0,
|
|
440
440
|
"provider_cost_usd": 0,
|
|
@@ -449,13 +449,13 @@
|
|
|
449
449
|
"verifier_checks": [
|
|
450
450
|
{
|
|
451
451
|
"check_id": "physical_input_hash_binding",
|
|
452
|
-
"input_hash": "sha256:
|
|
452
|
+
"input_hash": "sha256:db8f31c70e4b4589561a53127b16985a8b9d09000ba536df3467eacf857d2d0c",
|
|
453
453
|
"status": "pass"
|
|
454
454
|
},
|
|
455
455
|
{
|
|
456
456
|
"check_id": "deterministic_rederivation",
|
|
457
|
-
"first_result_hash": "sha256:
|
|
458
|
-
"repeated_result_hash": "sha256:
|
|
457
|
+
"first_result_hash": "sha256:dbb01a7bdcaccf19f2f022ca64586dd0866fe1ce4f3d5c33ffad61efccc8e997",
|
|
458
|
+
"repeated_result_hash": "sha256:dbb01a7bdcaccf19f2f022ca64586dd0866fe1ce4f3d5c33ffad61efccc8e997",
|
|
459
459
|
"status": "pass"
|
|
460
460
|
},
|
|
461
461
|
{
|
|
@@ -466,7 +466,7 @@
|
|
|
466
466
|
]
|
|
467
467
|
},
|
|
468
468
|
"reviewer_kind": "ai",
|
|
469
|
-
"run_artifact_hash": "sha256:
|
|
469
|
+
"run_artifact_hash": "sha256:c383e1ed38d866258fd55e7ad1521df91487c4f14a3cf60e6cdae3fb49bcaa4d",
|
|
470
470
|
"run_id": "H_ai_reference",
|
|
471
471
|
"schema_version": 1,
|
|
472
472
|
"simulation_scope": "AI deterministic reference digest only; no human analysts or blinded human adjudicator"
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
"formal_ga_effect": "none",
|
|
19
19
|
"formal_h_satisfied": false,
|
|
20
20
|
"human_reference": false,
|
|
21
|
-
"input_hash": "sha256:
|
|
21
|
+
"input_hash": "sha256:db8f31c70e4b4589561a53127b16985a8b9d09000ba536df3467eacf857d2d0c",
|
|
22
22
|
"network_allowed": false,
|
|
23
23
|
"production_effect": "none",
|
|
24
24
|
"protocol_binding": {
|
|
@@ -1008,7 +1008,7 @@
|
|
|
1008
1008
|
"schema_version": 1
|
|
1009
1009
|
},
|
|
1010
1010
|
"fact_pack_hash": "sha256:f69c424fb4ab9bb147ca933702857366ea492f8e988bfb2aeeb4e64df2c32706",
|
|
1011
|
-
"pack_hash": "sha256:
|
|
1011
|
+
"pack_hash": "sha256:f960a6c7061ae791935590e8d045478a65ea451335aceaed5c275894a9b8c9d7",
|
|
1012
1012
|
"pack_manifest_binding": {
|
|
1013
1013
|
"byte_length": 5436,
|
|
1014
1014
|
"path": "knowledge/solo-test/masters/master_graham/manifest.json",
|
|
@@ -6322,7 +6322,7 @@
|
|
|
6322
6322
|
"schema_version": 1
|
|
6323
6323
|
},
|
|
6324
6324
|
"fact_pack_hash": "sha256:0b481c9b13e34ec6465e5007d364473d71c462e3940726744fa6f91b9646f4d3",
|
|
6325
|
-
"pack_hash": "sha256:
|
|
6325
|
+
"pack_hash": "sha256:a09fec122e5d4f43d8885a5b00e354d259768c7f4648c9f0eebb358cf0461134",
|
|
6326
6326
|
"pack_manifest_binding": {
|
|
6327
6327
|
"byte_length": 5399,
|
|
6328
6328
|
"path": "knowledge/solo-test/masters/master_klarman/manifest.json",
|
|
@@ -6634,7 +6634,7 @@
|
|
|
6634
6634
|
"schema_version": 1
|
|
6635
6635
|
},
|
|
6636
6636
|
"fact_pack_hash": "sha256:6735e9705be48f6b9c19db94360b13ad6727db15e288c4e1225664ddd1d1a5a1",
|
|
6637
|
-
"pack_hash": "sha256:
|
|
6637
|
+
"pack_hash": "sha256:8ee9e13deaadaeef3065feb7bb5b39382181338cf1055b769b153cad4c308ce4",
|
|
6638
6638
|
"pack_manifest_binding": {
|
|
6639
6639
|
"byte_length": 5594,
|
|
6640
6640
|
"path": "knowledge/solo-test/masters/master_pabrai/manifest.json",
|
|
@@ -6,11 +6,11 @@
|
|
|
6
6
|
"formal_h_satisfied": false,
|
|
7
7
|
"human_reference": false,
|
|
8
8
|
"input_binding": {
|
|
9
|
-
"content_hash": "sha256:
|
|
10
|
-
"input_hash": "sha256:
|
|
9
|
+
"content_hash": "sha256:cdbc6e5a916b1aca475748ce06158fc9ae8caaf7ab7650337baa9d74af9fc828",
|
|
10
|
+
"input_hash": "sha256:db8f31c70e4b4589561a53127b16985a8b9d09000ba536df3467eacf857d2d0c",
|
|
11
11
|
"path": "simulation-input.json"
|
|
12
12
|
},
|
|
13
|
-
"manifest_hash": "sha256:
|
|
13
|
+
"manifest_hash": "sha256:6f23223d57531653456b1b5c67e4b1ceedce6f263d334bdcd4865c5ba05ae1d3",
|
|
14
14
|
"n_eff_disclosure_binding": {
|
|
15
15
|
"artifact_hash": "sha256:42ddb52fb8c5cc6335a0c90bbb4989fcfcc0a626ea1a32c87697ba4335ec94ab",
|
|
16
16
|
"content_hash": "sha256:18f4efc086ffa86322eeb9b442960b291f41811d92b7fc3f59db72dfb5cbc305",
|
|
@@ -32,66 +32,66 @@
|
|
|
32
32
|
"run_bindings": [
|
|
33
33
|
{
|
|
34
34
|
"blocked_fail_closed_count": 0,
|
|
35
|
-
"content_hash": "sha256:
|
|
35
|
+
"content_hash": "sha256:2a77e9c2c0593f2a827e07d3814088228118b1a83d75567f3378fc73556387ac",
|
|
36
36
|
"executed_count": 1,
|
|
37
37
|
"path": "runs/a.json",
|
|
38
|
-
"run_artifact_hash": "sha256:
|
|
38
|
+
"run_artifact_hash": "sha256:a0161522586f9a9b2ee80b68081279ec2b9399692e8e9e1ec56aef00379d27db",
|
|
39
39
|
"run_id": "A"
|
|
40
40
|
},
|
|
41
41
|
{
|
|
42
42
|
"blocked_fail_closed_count": 0,
|
|
43
|
-
"content_hash": "sha256:
|
|
43
|
+
"content_hash": "sha256:e7546d43a7fedcdcf33e04f444d876d8c587d1e76d1e558ca9fea1bf9a5f91fa",
|
|
44
44
|
"executed_count": 0,
|
|
45
45
|
"path": "runs/b.json",
|
|
46
|
-
"run_artifact_hash": "sha256:
|
|
46
|
+
"run_artifact_hash": "sha256:133ca81d8703085824a374f545ca30f83e90e4af45b8bdab17ed5aaa0e39adb1",
|
|
47
47
|
"run_id": "B"
|
|
48
48
|
},
|
|
49
49
|
{
|
|
50
50
|
"blocked_fail_closed_count": 0,
|
|
51
|
-
"content_hash": "sha256:
|
|
51
|
+
"content_hash": "sha256:eddda5b2d8702a9f4abe535f01a4a3f2a4cdd03585afa0f9656ab6fffb98d900",
|
|
52
52
|
"executed_count": 0,
|
|
53
53
|
"path": "runs/c.json",
|
|
54
|
-
"run_artifact_hash": "sha256:
|
|
54
|
+
"run_artifact_hash": "sha256:0760f289508bcbed0222e48531f3b0f8c394243fca4d7f6ad0a8de0b53fbb1b9",
|
|
55
55
|
"run_id": "C"
|
|
56
56
|
},
|
|
57
57
|
{
|
|
58
58
|
"blocked_fail_closed_count": 0,
|
|
59
|
-
"content_hash": "sha256:
|
|
59
|
+
"content_hash": "sha256:57d6781678d68417bec0cf6d2ff645234238f389e6d2f2a533b99edb28009170",
|
|
60
60
|
"executed_count": 13,
|
|
61
61
|
"path": "runs/d13.json",
|
|
62
|
-
"run_artifact_hash": "sha256:
|
|
62
|
+
"run_artifact_hash": "sha256:6e5ae1711c012a4a7bb670090485ed071f5fdb66f33145e3adf9fb394c2231df",
|
|
63
63
|
"run_id": "D13"
|
|
64
64
|
},
|
|
65
65
|
{
|
|
66
66
|
"blocked_fail_closed_count": 1,
|
|
67
|
-
"content_hash": "sha256:
|
|
67
|
+
"content_hash": "sha256:e50ac873b27e99b5459b75929b0a73986fc8cc64464f6d297f5460c58b047ccf",
|
|
68
68
|
"executed_count": 26,
|
|
69
69
|
"path": "runs/d26.json",
|
|
70
|
-
"run_artifact_hash": "sha256:
|
|
70
|
+
"run_artifact_hash": "sha256:184b9acfc9670ced5e8a41f346a3f6fc90d3e7f5db5f6093f61e4425d267d527",
|
|
71
71
|
"run_id": "D26"
|
|
72
72
|
},
|
|
73
73
|
{
|
|
74
74
|
"blocked_fail_closed_count": 0,
|
|
75
|
-
"content_hash": "sha256:
|
|
75
|
+
"content_hash": "sha256:ed0b970d7de787d9cbf37745cfb8f3f1488375b5e1904764f2a1a4bccf4b70a2",
|
|
76
76
|
"executed_count": 13,
|
|
77
77
|
"path": "runs/e-d13.json",
|
|
78
|
-
"run_artifact_hash": "sha256:
|
|
78
|
+
"run_artifact_hash": "sha256:76d61211082a3e02ac4e9196b5732183d6ca6c529e9e990622a228bc0e599bd5",
|
|
79
79
|
"run_id": "E:D13"
|
|
80
80
|
},
|
|
81
81
|
{
|
|
82
82
|
"blocked_fail_closed_count": 1,
|
|
83
|
-
"content_hash": "sha256:
|
|
83
|
+
"content_hash": "sha256:07cd8c9528cb72a9993c8121b353ea91a964f6dab6183e22881a3779fdc67dfd",
|
|
84
84
|
"executed_count": 26,
|
|
85
85
|
"path": "runs/e-d26.json",
|
|
86
|
-
"run_artifact_hash": "sha256:
|
|
86
|
+
"run_artifact_hash": "sha256:8120ff805d85265449c057f713f2684f06f409b5d0c288de3e3e620c220ec531",
|
|
87
87
|
"run_id": "E:D26"
|
|
88
88
|
},
|
|
89
89
|
{
|
|
90
90
|
"blocked_fail_closed_count": 1,
|
|
91
|
-
"content_hash": "sha256:
|
|
91
|
+
"content_hash": "sha256:0e57e34f69dff242a233522562a80d10d24390fb7837ce167e17a39cf2e87be5",
|
|
92
92
|
"executed_count": 26,
|
|
93
93
|
"path": "runs/h_ai_reference.json",
|
|
94
|
-
"run_artifact_hash": "sha256:
|
|
94
|
+
"run_artifact_hash": "sha256:c383e1ed38d866258fd55e7ad1521df91487c4f14a3cf60e6cdae3fb49bcaa4d",
|
|
95
95
|
"run_id": "H_ai_reference"
|
|
96
96
|
}
|
|
97
97
|
],
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* News for something that is not a company.
|
|
3
|
+
*
|
|
4
|
+
* `SOX` has no press office, files nothing, and returns nothing from a ticker news feed. Asked
|
|
5
|
+
* for "recent semiconductor news" the pipeline had no way to turn the symbol into that subject,
|
|
6
|
+
* so a basket run carried market-wide headlines and no industry at all.
|
|
7
|
+
*
|
|
8
|
+
* A basket's news identity comes from what it holds, not from a table someone maintains:
|
|
9
|
+
*
|
|
10
|
+
* - **The industry** is the modal SIC group of its largest holdings. SOXX resolves to
|
|
11
|
+
* semiconductors because its holdings are semiconductor registrants, and XLE resolves to
|
|
12
|
+
* energy for the same reason. A hand-written map would drift the first time an index
|
|
13
|
+
* rebalanced; this cannot.
|
|
14
|
+
* - **The constituents** supply their own dated company news and their own 8-K filings,
|
|
15
|
+
* weighted by how much of the basket each one is. A headline about 8% of the fund is worth
|
|
16
|
+
* more than one about 0.3% of it, and the weight travels with the item so a reader can see
|
|
17
|
+
* which it was.
|
|
18
|
+
*
|
|
19
|
+
* What this deliberately does NOT do is let a headline reach a seat's arithmetic. A frozen
|
|
20
|
+
* deterministic stance stays frozen; news informs what a seat SAYS about a stance it already
|
|
21
|
+
* reached, and supplies counts that are themselves dated facts. Anything else would make the
|
|
22
|
+
* same symbol answer differently depending on what was published that morning, which is the
|
|
23
|
+
* property this whole runtime exists to avoid.
|
|
24
|
+
*/
|
|
25
|
+
|
|
26
|
+
import { applyRecencyGate, fetchFeed, filingsFeed, queryNewsFeed, tickerNewsFeed } from "./feeds.mjs";
|
|
27
|
+
import { sicGroupFor } from "./industry.mjs";
|
|
28
|
+
import { fetchSubmissions, fetchUniverse } from "./sec.mjs";
|
|
29
|
+
|
|
30
|
+
/** How many of the largest holdings are read for news. Weight decays; attention should too. */
|
|
31
|
+
export const NEWS_CONSTITUENT_LIMIT = 10;
|
|
32
|
+
|
|
33
|
+
/** Recency gate for a basket, in days. Wider than a single company's because a sector moves slower. */
|
|
34
|
+
export const BASKET_NEWS_DAYS = 14;
|
|
35
|
+
|
|
36
|
+
const finite = (value) => typeof value === "number" && Number.isFinite(value);
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* The industry a basket is, derived from the registrants it holds.
|
|
40
|
+
*
|
|
41
|
+
* Weighted rather than counted: an index with forty small biotech names and two enormous
|
|
42
|
+
* semiconductor ones is a semiconductor bet, and a headcount would say otherwise.
|
|
43
|
+
*/
|
|
44
|
+
export function dominantIndustry(sicByTicker, holdings) {
|
|
45
|
+
return industryMix(sicByTicker, holdings)[0] || null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* The industries a basket actually is, largest first, until they account for most of it.
|
|
50
|
+
*
|
|
51
|
+
* A single label is right for SOXX, where semiconductors are half the fund, and wrong for a
|
|
52
|
+
* broad industrial basket whose largest group is eleven percent of it -- calling that one
|
|
53
|
+
* "electronics" would query the wrong news and state it with more confidence than the holdings
|
|
54
|
+
* support. Taking groups until they cover half the read weight gives an industrial fund its
|
|
55
|
+
* aerospace, machinery and transport news instead of one misleading query.
|
|
56
|
+
*/
|
|
57
|
+
export function industryMix(sicByTicker, holdings, { cover = 0.5, max = 3 } = {}) {
|
|
58
|
+
const weightByGroup = new Map();
|
|
59
|
+
for (const holding of holdings || []) {
|
|
60
|
+
const ticker = String(holding?.ticker || "").toUpperCase();
|
|
61
|
+
const sic = sicByTicker?.get?.(ticker);
|
|
62
|
+
const group = sic ? sicGroupFor(sic) : null;
|
|
63
|
+
if (!group || !finite(holding?.weight)) continue;
|
|
64
|
+
const current = weightByGroup.get(group.id) || { group, weight: 0, members: 0 };
|
|
65
|
+
weightByGroup.set(group.id, { group, weight: current.weight + holding.weight, members: current.members + 1 });
|
|
66
|
+
}
|
|
67
|
+
if (!weightByGroup.size) return [];
|
|
68
|
+
const ranked = [...weightByGroup.values()].sort((left, right) => right.weight - left.weight);
|
|
69
|
+
const chosen = [];
|
|
70
|
+
let cumulative = 0;
|
|
71
|
+
for (const entry of ranked) {
|
|
72
|
+
if (chosen.length >= max || (chosen.length && cumulative >= cover)) break;
|
|
73
|
+
chosen.push({
|
|
74
|
+
id: entry.group.id,
|
|
75
|
+
title: entry.group.title,
|
|
76
|
+
weight: Number(entry.weight.toFixed(6)),
|
|
77
|
+
members: entry.members,
|
|
78
|
+
// A basket whose largest industry is a third of it is not "a semiconductor fund", and
|
|
79
|
+
// the share is reported so a reader can judge rather than being told a single label.
|
|
80
|
+
is_concentrated: entry.weight >= 0.5,
|
|
81
|
+
});
|
|
82
|
+
cumulative += entry.weight;
|
|
83
|
+
}
|
|
84
|
+
return chosen;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Industry and constituent news for a basket, every item carrying its own publication date.
|
|
89
|
+
*
|
|
90
|
+
* `asOf` is honoured by the recency gate, so a historical run cannot pick up items published
|
|
91
|
+
* after the date it claims to be reasoning at.
|
|
92
|
+
*/
|
|
93
|
+
export async function fetchBasketNews(holdings, { asOf = null, days = BASKET_NEWS_DAYS, signal, limit = NEWS_CONSTITUENT_LIMIT } = {}) {
|
|
94
|
+
const unavailable = [];
|
|
95
|
+
const ranked = [...(holdings || [])]
|
|
96
|
+
.filter((holding) => holding?.ticker && finite(holding?.weight))
|
|
97
|
+
.sort((left, right) => right.weight - left.weight)
|
|
98
|
+
.slice(0, limit);
|
|
99
|
+
if (!ranked.length) return { available: false, unavailable: ["basket news: the basket published no weighted holdings"] };
|
|
100
|
+
|
|
101
|
+
let universe = null;
|
|
102
|
+
try {
|
|
103
|
+
universe = new Map((await fetchUniverse({ signal })).map((row) => [String(row.ticker).toUpperCase(), row.cik]));
|
|
104
|
+
} catch (error) {
|
|
105
|
+
unavailable.push(`basket news: SEC ticker universe unavailable (${String(error?.message || error)})`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// One submissions call per constituent buys its SIC, which is what the industry is derived
|
|
109
|
+
// from and what a filings feed needs anyway.
|
|
110
|
+
const sicByTicker = new Map();
|
|
111
|
+
const cikByTicker = new Map();
|
|
112
|
+
for (const holding of ranked) {
|
|
113
|
+
const ticker = String(holding.ticker).toUpperCase();
|
|
114
|
+
const cik = universe?.get(ticker);
|
|
115
|
+
if (!cik) { unavailable.push(`basket news ${ticker}: no SEC registrant`); continue; }
|
|
116
|
+
cikByTicker.set(ticker, cik);
|
|
117
|
+
try {
|
|
118
|
+
const profile = await fetchSubmissions(cik, { signal });
|
|
119
|
+
if (profile?.sic) sicByTicker.set(ticker, profile.sic);
|
|
120
|
+
} catch (error) {
|
|
121
|
+
unavailable.push(`basket news ${ticker}: ${String(error?.message || error)}`);
|
|
122
|
+
}
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
const industries = industryMix(sicByTicker, ranked);
|
|
126
|
+
const industry = industries[0] || null;
|
|
127
|
+
const specs = [];
|
|
128
|
+
if (industries.length) {
|
|
129
|
+
// Each industry's own name is a query. Deriving them from holdings rather than from a map
|
|
130
|
+
// is what makes this survive a rebalance.
|
|
131
|
+
for (const group of industries) {
|
|
132
|
+
specs.push({ ...queryNewsFeed(`${group.title.en} industry`), kind: "industry", scope: "industry", industry: group.id });
|
|
133
|
+
}
|
|
134
|
+
} else {
|
|
135
|
+
unavailable.push("basket news: no holding resolved to a SIC group, so no industry could be named");
|
|
136
|
+
}
|
|
137
|
+
for (const holding of ranked) {
|
|
138
|
+
const ticker = String(holding.ticker).toUpperCase();
|
|
139
|
+
specs.push({ ...tickerNewsFeed(ticker), kind: "company", scope: "constituent", ticker, weight: holding.weight });
|
|
140
|
+
const cik = cikByTicker.get(ticker);
|
|
141
|
+
if (cik) specs.push({ ...filingsFeed(cik, "8-K"), kind: "filing", scope: "constituent", ticker, weight: holding.weight });
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// Fetched per spec rather than through `fetchFeeds`, which returns a merged item list. The
|
|
145
|
+
// whole point here is that an item keeps which constituent it belongs to and how much of the
|
|
146
|
+
// basket that constituent is; a merged list has already thrown that away.
|
|
147
|
+
const fetched = await Promise.all(specs.map((spec) => fetchFeed(spec.url, { source: spec.source })));
|
|
148
|
+
const items = [];
|
|
149
|
+
fetched.forEach((feed, index) => {
|
|
150
|
+
const spec = specs[index];
|
|
151
|
+
if (!feed?.ok) { unavailable.push(`basket news ${spec.source}: ${feed?.reason || "unreachable"}`); return; }
|
|
152
|
+
for (const item of feed.items || []) {
|
|
153
|
+
items.push({ ...item, source: feed.source, kind: spec.kind, scope: spec.scope, ticker: spec.ticker || null, weight: spec.weight ?? null, industry: spec.industry || null });
|
|
154
|
+
}
|
|
155
|
+
});
|
|
156
|
+
const { included: dated } = applyRecencyGate(items, { days, asOf });
|
|
157
|
+
|
|
158
|
+
// How much of the basket actually generated dated news, and how many filed. Both are counts
|
|
159
|
+
// with a window, which is what makes them usable as facts rather than as impressions.
|
|
160
|
+
const covered = new Set(dated.filter((item) => item.ticker).map((item) => item.ticker));
|
|
161
|
+
const coverageWeight = ranked
|
|
162
|
+
.filter((holding) => covered.has(String(holding.ticker).toUpperCase()))
|
|
163
|
+
.reduce((sum, holding) => sum + holding.weight, 0);
|
|
164
|
+
const filers = new Set(dated.filter((item) => item.kind === "filing" && item.ticker).map((item) => item.ticker));
|
|
165
|
+
|
|
166
|
+
return {
|
|
167
|
+
available: dated.length > 0,
|
|
168
|
+
industry,
|
|
169
|
+
industries,
|
|
170
|
+
items: dated,
|
|
171
|
+
window_days: days,
|
|
172
|
+
constituents_read: ranked.length,
|
|
173
|
+
coverage_weight: Number(coverageWeight.toFixed(6)),
|
|
174
|
+
filing_event_count: filers.size,
|
|
175
|
+
filing_event_weight: Number(ranked
|
|
176
|
+
.filter((holding) => filers.has(String(holding.ticker).toUpperCase()))
|
|
177
|
+
.reduce((sum, holding) => sum + holding.weight, 0)
|
|
178
|
+
.toFixed(6)),
|
|
179
|
+
unavailable,
|
|
180
|
+
};
|
|
181
|
+
}
|
package/mcp/lib/breadth.mjs
CHANGED
|
@@ -40,6 +40,30 @@ export function parseDailyCloses(json) {
|
|
|
40
40
|
return clean.length ? clean : null;
|
|
41
41
|
}
|
|
42
42
|
|
|
43
|
+
/**
|
|
44
|
+
* Closes paired with the session they belong to.
|
|
45
|
+
*
|
|
46
|
+
* `parseDailyCloses` drops the timestamps, which is fine for a moving average over one series
|
|
47
|
+
* and wrong the moment two series are compared: Korea and the United States keep different
|
|
48
|
+
* holidays, so aligning by array position compares a Tuesday with a Wednesday and produces a
|
|
49
|
+
* correlation that is confidently wrong rather than visibly broken.
|
|
50
|
+
*/
|
|
51
|
+
export function parseDatedCloses(json) {
|
|
52
|
+
const result = json?.chart?.result?.[0];
|
|
53
|
+
const closes = result?.indicators?.quote?.[0]?.close;
|
|
54
|
+
const stamps = result?.timestamp;
|
|
55
|
+
if (!Array.isArray(closes) || !Array.isArray(stamps) || closes.length !== stamps.length) return null;
|
|
56
|
+
const rows = [];
|
|
57
|
+
for (let index = 0; index < closes.length; index += 1) {
|
|
58
|
+
const close = closes[index];
|
|
59
|
+
const stamp = stamps[index];
|
|
60
|
+
if (typeof close !== "number" || !Number.isFinite(close) || close <= 0) continue;
|
|
61
|
+
if (typeof stamp !== "number" || !Number.isFinite(stamp)) continue;
|
|
62
|
+
rows.push({ date: new Date(stamp * 1000).toISOString().slice(0, 10), close });
|
|
63
|
+
}
|
|
64
|
+
return rows.length ? rows : null;
|
|
65
|
+
}
|
|
66
|
+
|
|
43
67
|
/** Simple moving average of the last `window` closes, or null when there is not enough history. */
|
|
44
68
|
export function movingAverage(closes, window = BREADTH_WINDOW_DAYS) {
|
|
45
69
|
if (!Array.isArray(closes) || closes.length < window) return null;
|