fa-quant 1.2.1__py3-none-any.whl
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.
- fa_quant-1.2.1.dist-info/METADATA +236 -0
- fa_quant-1.2.1.dist-info/RECORD +106 -0
- fa_quant-1.2.1.dist-info/WHEEL +4 -0
- fa_quant-1.2.1.dist-info/entry_points.txt +2 -0
- fa_quant-1.2.1.dist-info/licenses/LICENSE +21 -0
- financial_agent/__init__.py +1 -0
- financial_agent/agents/__init__.py +0 -0
- financial_agent/agents/claude/__init__.py +0 -0
- financial_agent/agents/gpt/__init__.py +0 -0
- financial_agent/agents/grok/__init__.py +0 -0
- financial_agent/dl/__init__.py +0 -0
- financial_agent/dl/models/__init__.py +0 -0
- financial_agent/dl/models/baseline_model.py +23 -0
- financial_agent/dl/preprocessing/__init__.py +0 -0
- financial_agent/dl/preprocessing/data_windowing.py +135 -0
- financial_agent/harness/__init__.py +73 -0
- financial_agent/harness/__main__.py +430 -0
- financial_agent/harness/_system/factor_scoring.py +175 -0
- financial_agent/harness/_system/settlement_ledger.py +1222 -0
- financial_agent/harness/audit.py +393 -0
- financial_agent/harness/equity_policy.toml +179 -0
- financial_agent/harness/fingerprint.py +55 -0
- financial_agent/harness/gates.py +522 -0
- financial_agent/harness/handlers.py +77 -0
- financial_agent/harness/helpers.py +47 -0
- financial_agent/harness/kernels.py +696 -0
- financial_agent/harness/ledger.py +325 -0
- financial_agent/harness/lifecycle.py +127 -0
- financial_agent/harness/manifest.py +113 -0
- financial_agent/harness/market_calendar.py +50 -0
- financial_agent/harness/mcp.py +505 -0
- financial_agent/harness/policy.py +248 -0
- financial_agent/harness/runner.py +275 -0
- financial_agent/harness/schema.py +451 -0
- financial_agent/harness/scoring.py +363 -0
- financial_agent/harness/skills.py +458 -0
- financial_agent/quant/__init__.py +1 -0
- financial_agent/quant/__main__.py +506 -0
- financial_agent/quant/backtest/__init__.py +16 -0
- financial_agent/quant/backtest/config.py +74 -0
- financial_agent/quant/backtest/costs.py +62 -0
- financial_agent/quant/backtest/engine.py +248 -0
- financial_agent/quant/backtest/metrics.py +102 -0
- financial_agent/quant/backtest/result.py +51 -0
- financial_agent/quant/data/__init__.py +21 -0
- financial_agent/quant/data/bars.py +103 -0
- financial_agent/quant/data/cache.py +168 -0
- financial_agent/quant/data/providers.py +138 -0
- financial_agent/quant/data/quality.py +104 -0
- financial_agent/quant/data/synthetic.py +67 -0
- financial_agent/quant/execution/__init__.py +24 -0
- financial_agent/quant/execution/alpaca.py +293 -0
- financial_agent/quant/execution/broker.py +98 -0
- financial_agent/quant/execution/ibkr.py +634 -0
- financial_agent/quant/execution/journal.py +139 -0
- financial_agent/quant/execution/paper.py +254 -0
- financial_agent/quant/execution/reconcile.py +35 -0
- financial_agent/quant/mcp.py +210 -0
- financial_agent/quant/portfolio.py +56 -0
- financial_agent/quant/quant_policy.toml +40 -0
- financial_agent/quant/risk/__init__.py +21 -0
- financial_agent/quant/risk/killswitch.py +61 -0
- financial_agent/quant/risk/limits.py +109 -0
- financial_agent/quant/risk/orders.py +91 -0
- financial_agent/quant/risk/pretrade.py +157 -0
- financial_agent/quant/risk/state.py +86 -0
- financial_agent/quant/runner/__init__.py +20 -0
- financial_agent/quant/runner/config.py +254 -0
- financial_agent/quant/runner/promotion.py +277 -0
- financial_agent/quant/runner/report.py +231 -0
- financial_agent/quant/runner/session.py +705 -0
- financial_agent/quant/strategies/__init__.py +19 -0
- financial_agent/quant/strategies/base.py +44 -0
- financial_agent/quant/strategies/benchmarks.py +54 -0
- financial_agent/quant/strategies/momentum.py +80 -0
- financial_agent/quant/strategies/registry.py +65 -0
- financial_agent/quant/strategies/research.py +226 -0
- financial_agent/quant/strategies/trend.py +91 -0
- financial_agent/quant/validation/__init__.py +41 -0
- financial_agent/quant/validation/bootstrap.py +39 -0
- financial_agent/quant/validation/certificate.py +189 -0
- financial_agent/quant/validation/gate.py +304 -0
- financial_agent/quant/validation/overfitting.py +148 -0
- financial_agent/quant/validation/stress.py +33 -0
- financial_agent/quant/validation/walkforward.py +157 -0
- financial_agent/rl/__init__.py +13 -0
- financial_agent/rl/agent.py +112 -0
- financial_agent/rl/environment.py +177 -0
- financial_agent/rl/trainer.py +142 -0
- financial_agent/trading/__init__.py +0 -0
- financial_agent/trading/autotrain/__init__.py +0 -0
- financial_agent/trading/autotrain/training.py +92 -0
- financial_agent/trading/backtest.py +118 -0
- financial_agent/trading/config/__init__.py +0 -0
- financial_agent/trading/config/config.py +1587 -0
- financial_agent/trading/marketdata/__init__.py +0 -0
- financial_agent/trading/marketdata/yahoodownloader.py +99 -0
- financial_agent/trading/model/__init__.py +0 -0
- financial_agent/trading/model/models.py +466 -0
- financial_agent/trading/preprocessing/__init__.py +0 -0
- financial_agent/trading/preprocessing/data.py +32 -0
- financial_agent/trading/preprocessing/preprocessors.py +162 -0
- financial_agent/trading/utils/__init__.py +0 -0
- financial_agent/trading/utils/gpu_config.py +213 -0
- financial_agent/trading/utils/main.py +52 -0
- financial_agent/trading/utils/setup_gpu_for_notebooks.py +130 -0
|
@@ -0,0 +1,430 @@
|
|
|
1
|
+
"""Harness CLI: the tool surface any agent runtime can call from a shell.
|
|
2
|
+
|
|
3
|
+
python -m financial_agent.harness skills validate
|
|
4
|
+
python -m financial_agent.harness skills catalog
|
|
5
|
+
python -m financial_agent.harness audit --output-dir agents/equity/output
|
|
6
|
+
python -m financial_agent.harness gate agents/equity/output/<model>-<date> [--json]
|
|
7
|
+
python -m financial_agent.harness replay --since 2026-07-31
|
|
8
|
+
python -m financial_agent.harness schema [<package>] [--json-schema]
|
|
9
|
+
python -m financial_agent.harness session <YYYY-MM-DD>
|
|
10
|
+
python -m financial_agent.harness reachability [--families tech_z macro_z]
|
|
11
|
+
python -m financial_agent.harness kernel <kind> --input FILE
|
|
12
|
+
(kind: equity, market, risk, portfolio, score)
|
|
13
|
+
python -m financial_agent.harness run --model <id> --date <YYYY-MM-DD>
|
|
14
|
+
python -m financial_agent.harness manifest agents/equity/output/<model>-<date>
|
|
15
|
+
python -m financial_agent.harness hash agents/equity/output/<model>-<date>
|
|
16
|
+
python -m financial_agent.harness clones --since 2026-07-31
|
|
17
|
+
python -m financial_agent.harness policy-check OLD.toml NEW.toml --settled-n 20
|
|
18
|
+
python -m financial_agent.harness mcp
|
|
19
|
+
|
|
20
|
+
Exit status is non-zero when a check fails, so a skill, a CI job, or a
|
|
21
|
+
PreToolUse hook can use the command as a gate.
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
from __future__ import annotations
|
|
25
|
+
|
|
26
|
+
import argparse
|
|
27
|
+
import json
|
|
28
|
+
import sys
|
|
29
|
+
from pathlib import Path
|
|
30
|
+
from typing import Any
|
|
31
|
+
|
|
32
|
+
from financial_agent.harness.audit import audit_output_dir
|
|
33
|
+
from financial_agent.harness.fingerprint import (
|
|
34
|
+
artifact_digests,
|
|
35
|
+
find_clones,
|
|
36
|
+
package_digest,
|
|
37
|
+
)
|
|
38
|
+
from financial_agent.harness.gates import (
|
|
39
|
+
PREDICTIONS_FILE,
|
|
40
|
+
go_reachability,
|
|
41
|
+
publish_gate,
|
|
42
|
+
replay_history,
|
|
43
|
+
replay_status,
|
|
44
|
+
)
|
|
45
|
+
from financial_agent.harness.kernels import (
|
|
46
|
+
EquityInputs,
|
|
47
|
+
Holding,
|
|
48
|
+
KernelError,
|
|
49
|
+
MarketInputs,
|
|
50
|
+
equity_record,
|
|
51
|
+
market_forecast_records,
|
|
52
|
+
portfolio_feasibility,
|
|
53
|
+
price_risk,
|
|
54
|
+
)
|
|
55
|
+
from financial_agent.harness.lifecycle import RunState
|
|
56
|
+
from financial_agent.harness.manifest import (
|
|
57
|
+
gate_report,
|
|
58
|
+
render_manifest_sections,
|
|
59
|
+
replay_lines,
|
|
60
|
+
)
|
|
61
|
+
from financial_agent.harness.market_calendar import session
|
|
62
|
+
from financial_agent.harness.policy import (
|
|
63
|
+
FAMILIES,
|
|
64
|
+
MutationEvidence,
|
|
65
|
+
check_mutation,
|
|
66
|
+
load_policy,
|
|
67
|
+
)
|
|
68
|
+
from financial_agent.harness.runner import RunLocked, default_handlers, run
|
|
69
|
+
from financial_agent.harness.schema import json_schema, validate_payload
|
|
70
|
+
from financial_agent.harness.scoring import score_request
|
|
71
|
+
from financial_agent.harness.skills import CHARS_PER_TOKEN, SkillRegistry
|
|
72
|
+
|
|
73
|
+
DEFAULT_SKILL_ROOTS = (
|
|
74
|
+
Path("skills"),
|
|
75
|
+
Path(".agents/skills"),
|
|
76
|
+
Path("agents/equity/turtle-trader"),
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def _registry(roots: list[Path] | None) -> SkillRegistry:
|
|
81
|
+
candidates = roots or [r for r in DEFAULT_SKILL_ROOTS if r.exists()]
|
|
82
|
+
return SkillRegistry.discover(
|
|
83
|
+
candidates,
|
|
84
|
+
known_stages=[s.value for s in RunState],
|
|
85
|
+
known_tools=tool_names(),
|
|
86
|
+
)
|
|
87
|
+
|
|
88
|
+
|
|
89
|
+
def tool_names() -> list[str]:
|
|
90
|
+
"""CLI subcommands: the names a skill's ``fa-tools`` may bind to."""
|
|
91
|
+
sub = next(
|
|
92
|
+
a for a in build_parser()._actions if isinstance(a, argparse._SubParsersAction)
|
|
93
|
+
)
|
|
94
|
+
return sorted(sub.choices)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
def _print_json(value: Any) -> None:
|
|
98
|
+
print(json.dumps(value, indent=2, sort_keys=False, default=str))
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def cmd_skills(args: argparse.Namespace) -> int:
|
|
102
|
+
registry = _registry(args.root)
|
|
103
|
+
if args.action == "catalog":
|
|
104
|
+
catalog = registry.catalog_xml()
|
|
105
|
+
print(catalog)
|
|
106
|
+
print(
|
|
107
|
+
f"<!-- {len(registry.skills)} skills, ~{len(catalog) // CHARS_PER_TOKEN} "
|
|
108
|
+
"tokens at level 1 -->"
|
|
109
|
+
)
|
|
110
|
+
return 0
|
|
111
|
+
failed = False
|
|
112
|
+
for result in registry.results:
|
|
113
|
+
status = "OK" if result.ok else "INVALID"
|
|
114
|
+
print(f"{status:8} {result.path}")
|
|
115
|
+
for error in result.errors:
|
|
116
|
+
print(f" error: {error}")
|
|
117
|
+
for warning in result.warnings:
|
|
118
|
+
print(f" warn: {warning}")
|
|
119
|
+
failed = failed or not result.ok
|
|
120
|
+
return 1 if failed else 0
|
|
121
|
+
|
|
122
|
+
|
|
123
|
+
def cmd_audit(args: argparse.Namespace) -> int:
|
|
124
|
+
report = audit_output_dir(
|
|
125
|
+
args.output_dir, load_policy(args.policy), as_of=args.as_of
|
|
126
|
+
)
|
|
127
|
+
print(report.to_markdown())
|
|
128
|
+
errors = [f for f in report.findings if f.severity == "error"]
|
|
129
|
+
return 1 if errors and args.strict else 0
|
|
130
|
+
|
|
131
|
+
|
|
132
|
+
def cmd_gate(args: argparse.Namespace) -> int:
|
|
133
|
+
policy = load_policy(args.policy)
|
|
134
|
+
if args.json:
|
|
135
|
+
report = gate_report(args.package, policy)
|
|
136
|
+
_print_json(report)
|
|
137
|
+
return 0 if report["pass"] else 1
|
|
138
|
+
failures = publish_gate(args.package, policy)
|
|
139
|
+
replay = replay_status(args.package, policy)
|
|
140
|
+
if replay is not None:
|
|
141
|
+
print(f"investable (recomputed): {len(replay.investable)} {replay.investable}")
|
|
142
|
+
print(
|
|
143
|
+
f"status replay: {replay.decision.status.value} "
|
|
144
|
+
f"({'; '.join(replay.decision.reasons)}); "
|
|
145
|
+
f"published: {replay.published}; Required inputs assumed grounded"
|
|
146
|
+
)
|
|
147
|
+
for line in replay_lines(replay):
|
|
148
|
+
print(line)
|
|
149
|
+
print(f"content hash: sha256:{package_digest(args.package)}")
|
|
150
|
+
for failure in failures:
|
|
151
|
+
print(f"GATE FAIL: {failure}")
|
|
152
|
+
print("publish gate:", "FAIL" if failures else "PASS")
|
|
153
|
+
return 1 if failures else 0
|
|
154
|
+
|
|
155
|
+
|
|
156
|
+
def cmd_replay(args: argparse.Namespace) -> int:
|
|
157
|
+
replays = replay_history(
|
|
158
|
+
args.output_dir, load_policy(args.policy), since=args.since, as_of=args.as_of
|
|
159
|
+
)
|
|
160
|
+
agree = [name for name, replay in replays if replay.agrees]
|
|
161
|
+
print("| package | session | replay | published | reasons |")
|
|
162
|
+
print("|---|---|---|---|---|")
|
|
163
|
+
for name, replay in replays:
|
|
164
|
+
if replay.agrees and not args.all:
|
|
165
|
+
continue
|
|
166
|
+
kind = replay.session.kind if replay.session else "-"
|
|
167
|
+
reasons = "; ".join(replay.decision.reasons).replace("|", "\\|")
|
|
168
|
+
print(
|
|
169
|
+
f"| {name} | {kind} | {replay.decision.status.value} | "
|
|
170
|
+
f"{replay.published} | {reasons} |"
|
|
171
|
+
)
|
|
172
|
+
print()
|
|
173
|
+
print(
|
|
174
|
+
f"{len(agree)} of {len(replays)} replayed packages agree with their "
|
|
175
|
+
"published status (Required inputs assumed grounded)."
|
|
176
|
+
)
|
|
177
|
+
return 1 if args.strict and len(agree) < len(replays) else 0
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def cmd_schema(args: argparse.Namespace) -> int:
|
|
181
|
+
policy = load_policy(args.policy)
|
|
182
|
+
if args.json_schema or args.package is None:
|
|
183
|
+
_print_json(json_schema(policy))
|
|
184
|
+
return 0
|
|
185
|
+
path = args.package / PREDICTIONS_FILE
|
|
186
|
+
if not path.exists():
|
|
187
|
+
print(f"{path}: not found")
|
|
188
|
+
return 1
|
|
189
|
+
payload = json.loads(path.read_text(encoding="utf-8"))
|
|
190
|
+
declared = isinstance(payload, dict) and "schema_version" in payload
|
|
191
|
+
findings = validate_payload(payload, policy)
|
|
192
|
+
for finding in findings:
|
|
193
|
+
print(f"v1: {finding}")
|
|
194
|
+
label = "declares" if declared else "legacy ledger, does not declare"
|
|
195
|
+
print(f"{path}: {label} schema_version; {len(findings)} v1 finding(s)")
|
|
196
|
+
return 1 if findings else 0
|
|
197
|
+
|
|
198
|
+
|
|
199
|
+
def cmd_session(args: argparse.Namespace) -> int:
|
|
200
|
+
day = session(args.date)
|
|
201
|
+
print(f"{day.kind} {day.describe()}")
|
|
202
|
+
return 0
|
|
203
|
+
|
|
204
|
+
|
|
205
|
+
def cmd_reachability(args: argparse.Namespace) -> int:
|
|
206
|
+
reach = go_reachability(
|
|
207
|
+
load_policy(args.policy),
|
|
208
|
+
available_families=args.families,
|
|
209
|
+
ranked_names=args.ranked_names,
|
|
210
|
+
)
|
|
211
|
+
print(f"gating families: {', '.join(reach.gating_families) or 'none'}")
|
|
212
|
+
for blocker in reach.blockers:
|
|
213
|
+
print(f"BLOCKER: {blocker}")
|
|
214
|
+
print("GO", "reachable" if reach.reachable else "unreachable")
|
|
215
|
+
return 0 if reach.reachable else 1
|
|
216
|
+
|
|
217
|
+
|
|
218
|
+
def run_kernel(kind: str, request: dict[str, Any], policy: Any) -> Any:
|
|
219
|
+
"""Shared by the CLI and the MCP server; raises ``KernelError``."""
|
|
220
|
+
if kind == "equity":
|
|
221
|
+
return [
|
|
222
|
+
equity_record(
|
|
223
|
+
request["run_date"],
|
|
224
|
+
request["model"],
|
|
225
|
+
EquityInputs.from_mapping(item),
|
|
226
|
+
policy,
|
|
227
|
+
)
|
|
228
|
+
for item in request["names"]
|
|
229
|
+
]
|
|
230
|
+
if kind == "market":
|
|
231
|
+
return market_forecast_records(
|
|
232
|
+
request["run_date"],
|
|
233
|
+
request["model"],
|
|
234
|
+
request["regime"],
|
|
235
|
+
[MarketInputs.from_mapping(item) for item in request["etfs"]],
|
|
236
|
+
policy,
|
|
237
|
+
)
|
|
238
|
+
if kind == "portfolio":
|
|
239
|
+
return vars(
|
|
240
|
+
portfolio_feasibility(
|
|
241
|
+
[Holding.from_mapping(item) for item in request["holdings"]],
|
|
242
|
+
request["spy_closes"],
|
|
243
|
+
policy,
|
|
244
|
+
)
|
|
245
|
+
)
|
|
246
|
+
if kind == "score":
|
|
247
|
+
return score_request(request, policy)
|
|
248
|
+
if kind == "risk":
|
|
249
|
+
return vars(
|
|
250
|
+
price_risk(
|
|
251
|
+
request["closes"],
|
|
252
|
+
request["spy_closes"],
|
|
253
|
+
tlt_closes=request.get("tlt_closes"),
|
|
254
|
+
)
|
|
255
|
+
)
|
|
256
|
+
raise KernelError(f"unknown kernel {kind!r}")
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
def cmd_kernel(args: argparse.Namespace) -> int:
|
|
260
|
+
text = sys.stdin.read() if str(args.input) == "-" else args.input.read_text()
|
|
261
|
+
try:
|
|
262
|
+
_print_json(run_kernel(args.kind, json.loads(text), load_policy(args.policy)))
|
|
263
|
+
except (KernelError, KeyError, TypeError, ValueError) as exc:
|
|
264
|
+
print(f"KERNEL ERROR: {type(exc).__name__}: {exc}")
|
|
265
|
+
return 1
|
|
266
|
+
return 0
|
|
267
|
+
|
|
268
|
+
|
|
269
|
+
def cmd_mcp(args: argparse.Namespace) -> int:
|
|
270
|
+
from financial_agent.harness.mcp import serve
|
|
271
|
+
|
|
272
|
+
serve(sys.stdin, sys.stdout, output_dir=args.output_dir)
|
|
273
|
+
return 0
|
|
274
|
+
|
|
275
|
+
|
|
276
|
+
def cmd_run(args: argparse.Namespace) -> int:
|
|
277
|
+
try:
|
|
278
|
+
report = run(args.model, args.date, args.output_dir, default_handlers())
|
|
279
|
+
except RunLocked as exc:
|
|
280
|
+
print(f"LOCKED: {exc}")
|
|
281
|
+
return 2
|
|
282
|
+
print(report.lifecycle.transcript())
|
|
283
|
+
for failure in report.gate_failures:
|
|
284
|
+
print(f"GATE FAIL: {failure}")
|
|
285
|
+
if report.published:
|
|
286
|
+
print(f"run {report.run_id}: {report.lifecycle.status}")
|
|
287
|
+
return 0
|
|
288
|
+
print(f"run {report.run_id}: HALTED: {report.halt_reason}")
|
|
289
|
+
return 1
|
|
290
|
+
|
|
291
|
+
|
|
292
|
+
def cmd_manifest(args: argparse.Namespace) -> int:
|
|
293
|
+
text, failures = render_manifest_sections(args.package, load_policy(args.policy))
|
|
294
|
+
print(text)
|
|
295
|
+
return 1 if failures else 0
|
|
296
|
+
|
|
297
|
+
|
|
298
|
+
def cmd_hash(args: argparse.Namespace) -> int:
|
|
299
|
+
for name, digest in artifact_digests(args.package).items():
|
|
300
|
+
print(f"{digest} {name}")
|
|
301
|
+
print(f"content hash: sha256:{package_digest(args.package)}")
|
|
302
|
+
return 0
|
|
303
|
+
|
|
304
|
+
|
|
305
|
+
def cmd_clones(args: argparse.Namespace) -> int:
|
|
306
|
+
clones = find_clones(args.output_dir, since=args.since)
|
|
307
|
+
for (name, digest), packages in sorted(clones.items()):
|
|
308
|
+
print(f"{name} {digest[:12]}: {', '.join(packages)}")
|
|
309
|
+
print(f"{len(clones)} artifact(s) shared between packages")
|
|
310
|
+
return 1 if clones and args.strict else 0
|
|
311
|
+
|
|
312
|
+
|
|
313
|
+
def cmd_policy_check(args: argparse.Namespace) -> int:
|
|
314
|
+
verdict = check_mutation(
|
|
315
|
+
load_policy(args.old),
|
|
316
|
+
load_policy(args.new),
|
|
317
|
+
MutationEvidence(
|
|
318
|
+
settled_n=args.settled_n,
|
|
319
|
+
eff_n=args.eff_n,
|
|
320
|
+
human_approved=args.human_approved,
|
|
321
|
+
),
|
|
322
|
+
)
|
|
323
|
+
print(f"decision: {verdict.decision}")
|
|
324
|
+
for key in verdict.changed:
|
|
325
|
+
print(f" changed: {key}")
|
|
326
|
+
for reason in verdict.reasons:
|
|
327
|
+
print(f" reason: {reason}")
|
|
328
|
+
return 0 if verdict.decision in {"NO_CHANGE", "ACCEPTABLE"} else 1
|
|
329
|
+
|
|
330
|
+
|
|
331
|
+
def build_parser() -> argparse.ArgumentParser:
|
|
332
|
+
parser = argparse.ArgumentParser(prog="python -m financial_agent.harness")
|
|
333
|
+
sub = parser.add_subparsers(dest="command", required=True)
|
|
334
|
+
|
|
335
|
+
skills = sub.add_parser("skills", help="validate skills or print the catalog")
|
|
336
|
+
skills.add_argument("action", choices=["validate", "catalog"])
|
|
337
|
+
skills.add_argument("--root", type=Path, action="append")
|
|
338
|
+
skills.set_defaults(func=cmd_skills)
|
|
339
|
+
|
|
340
|
+
audit = sub.add_parser("audit", help="conformance audit of prediction ledgers")
|
|
341
|
+
audit.add_argument("--output-dir", type=Path, default=Path("agents/equity/output"))
|
|
342
|
+
audit.add_argument("--as-of", help="only packages dated on or before YYYY-MM-DD")
|
|
343
|
+
audit.add_argument("--policy", type=Path)
|
|
344
|
+
audit.add_argument("--strict", action="store_true", help="exit 1 on any error")
|
|
345
|
+
audit.set_defaults(func=cmd_audit)
|
|
346
|
+
|
|
347
|
+
gate = sub.add_parser("gate", help="publish gate + status replay for a package")
|
|
348
|
+
gate.add_argument("package", type=Path)
|
|
349
|
+
gate.add_argument("--policy", type=Path)
|
|
350
|
+
gate.add_argument("--json", action="store_true", help="machine-readable report")
|
|
351
|
+
gate.set_defaults(func=cmd_gate)
|
|
352
|
+
|
|
353
|
+
replay = sub.add_parser("replay", help="status replay across the run history")
|
|
354
|
+
replay.add_argument("--output-dir", type=Path, default=Path("agents/equity/output"))
|
|
355
|
+
replay.add_argument("--since", help="only packages dated on or after YYYY-MM-DD")
|
|
356
|
+
replay.add_argument("--as-of", help="only packages dated on or before YYYY-MM-DD")
|
|
357
|
+
replay.add_argument("--all", action="store_true", help="list agreeing rows too")
|
|
358
|
+
replay.add_argument("--policy", type=Path)
|
|
359
|
+
replay.add_argument("--strict", action="store_true", help="exit 1 on a mismatch")
|
|
360
|
+
replay.set_defaults(func=cmd_replay)
|
|
361
|
+
|
|
362
|
+
schema = sub.add_parser("schema", help="validate a ledger against schema v1")
|
|
363
|
+
schema.add_argument("package", type=Path, nargs="?")
|
|
364
|
+
schema.add_argument("--json-schema", action="store_true", help="print the schema")
|
|
365
|
+
schema.add_argument("--policy", type=Path)
|
|
366
|
+
schema.set_defaults(func=cmd_schema)
|
|
367
|
+
|
|
368
|
+
day = sub.add_parser("session", help="NYSE session type for a run date")
|
|
369
|
+
day.add_argument("date", help="YYYY-MM-DD")
|
|
370
|
+
day.set_defaults(func=cmd_session)
|
|
371
|
+
|
|
372
|
+
reach = sub.add_parser("reachability", help="structural GO blockers")
|
|
373
|
+
reach.add_argument("--families", nargs="*", choices=FAMILIES)
|
|
374
|
+
reach.add_argument("--ranked-names", type=int)
|
|
375
|
+
reach.add_argument("--policy", type=Path)
|
|
376
|
+
reach.set_defaults(func=cmd_reachability)
|
|
377
|
+
|
|
378
|
+
kernel = sub.add_parser("kernel", help="compute records or risk from JSON input")
|
|
379
|
+
kernel.add_argument(
|
|
380
|
+
"kind", choices=["equity", "market", "risk", "portfolio", "score"]
|
|
381
|
+
)
|
|
382
|
+
kernel.add_argument("--input", type=Path, required=True, help="JSON file or -")
|
|
383
|
+
kernel.add_argument("--policy", type=Path)
|
|
384
|
+
kernel.set_defaults(func=cmd_kernel)
|
|
385
|
+
|
|
386
|
+
mcp = sub.add_parser("mcp", help="serve the read-only tools over MCP (stdio)")
|
|
387
|
+
mcp.add_argument("--output-dir", type=Path, default=Path("agents/equity/output"))
|
|
388
|
+
mcp.set_defaults(func=cmd_mcp)
|
|
389
|
+
|
|
390
|
+
run_parser = sub.add_parser("run", help="drive one run through the lifecycle")
|
|
391
|
+
run_parser.add_argument("--model", required=True)
|
|
392
|
+
run_parser.add_argument("--date", required=True, help="YYYY-MM-DD")
|
|
393
|
+
run_parser.add_argument(
|
|
394
|
+
"--output-dir", type=Path, default=Path("agents/equity/output")
|
|
395
|
+
)
|
|
396
|
+
run_parser.set_defaults(func=cmd_run)
|
|
397
|
+
|
|
398
|
+
manifest = sub.add_parser("manifest", help="generated manifest sections")
|
|
399
|
+
manifest.add_argument("package", type=Path)
|
|
400
|
+
manifest.add_argument("--policy", type=Path)
|
|
401
|
+
manifest.set_defaults(func=cmd_manifest)
|
|
402
|
+
|
|
403
|
+
digest = sub.add_parser("hash", help="per-artifact and package content hashes")
|
|
404
|
+
digest.add_argument("package", type=Path)
|
|
405
|
+
digest.set_defaults(func=cmd_hash)
|
|
406
|
+
|
|
407
|
+
clones = sub.add_parser("clones", help="artifacts shared between packages")
|
|
408
|
+
clones.add_argument("--output-dir", type=Path, default=Path("agents/equity/output"))
|
|
409
|
+
clones.add_argument("--since", help="only packages dated on or after YYYY-MM-DD")
|
|
410
|
+
clones.add_argument("--strict", action="store_true", help="exit 1 on any clone")
|
|
411
|
+
clones.set_defaults(func=cmd_clones)
|
|
412
|
+
|
|
413
|
+
check = sub.add_parser("policy-check", help="govern a proposed policy change")
|
|
414
|
+
check.add_argument("old", type=Path)
|
|
415
|
+
check.add_argument("new", type=Path)
|
|
416
|
+
check.add_argument("--settled-n", type=int, default=0)
|
|
417
|
+
check.add_argument("--eff-n", type=int, default=0)
|
|
418
|
+
check.add_argument("--human-approved", action="store_true")
|
|
419
|
+
check.set_defaults(func=cmd_policy_check)
|
|
420
|
+
return parser
|
|
421
|
+
|
|
422
|
+
|
|
423
|
+
def main(argv: list[str] | None = None) -> int:
|
|
424
|
+
args = build_parser().parse_args(argv)
|
|
425
|
+
result: int = args.func(args)
|
|
426
|
+
return result
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
if __name__ == "__main__":
|
|
430
|
+
sys.exit(main())
|
|
@@ -0,0 +1,175 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""Shared helpers for the SHADOW diagnostic scripts.
|
|
3
|
+
|
|
4
|
+
Used by `fundamental_diagnostics.py` and `sentiment_diagnostics.py`: fetch
|
|
5
|
+
plumbing (SSL context, JSON fetch, ticker-list parsing) and the
|
|
6
|
+
cross-sectional factor-scoring pipeline that turns per-name raw signals into
|
|
7
|
+
a family z-score, per rules.md's Family Aggregation rule -- convert
|
|
8
|
+
sourceable metrics into cross-sectional z-scores after winsorizing extreme
|
|
9
|
+
observations at the 5th and 95th percentiles, then equal-weight the
|
|
10
|
+
available signal z-scores.
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import datetime as dt
|
|
16
|
+
import json
|
|
17
|
+
import ssl
|
|
18
|
+
import urllib.request
|
|
19
|
+
from pathlib import Path
|
|
20
|
+
from typing import Any
|
|
21
|
+
|
|
22
|
+
SSL_CONTEXT: ssl.SSLContext | None = None
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def https_context() -> ssl.SSLContext | None:
|
|
26
|
+
global SSL_CONTEXT
|
|
27
|
+
if SSL_CONTEXT is not None:
|
|
28
|
+
return SSL_CONTEXT
|
|
29
|
+
try:
|
|
30
|
+
import certifi # type: ignore
|
|
31
|
+
|
|
32
|
+
SSL_CONTEXT = ssl.create_default_context(cafile=certifi.where())
|
|
33
|
+
except Exception:
|
|
34
|
+
SSL_CONTEXT = ssl.create_default_context()
|
|
35
|
+
return SSL_CONTEXT
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
def fetch_json(url: str, headers: dict[str, str], timeout: int = 20) -> Any:
|
|
39
|
+
req = urllib.request.Request(url, headers=headers)
|
|
40
|
+
with urllib.request.urlopen(req, timeout=timeout, context=https_context()) as resp:
|
|
41
|
+
result: Any = json.loads(resp.read())
|
|
42
|
+
return result
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def read_tickers(tickers: list[str], tickers_file: Path | None) -> list[str]:
|
|
46
|
+
"""Merge --tickers and --tickers-file into one de-duplicated, order-
|
|
47
|
+
preserving, upper-cased ticker list."""
|
|
48
|
+
out_tickers = list(tickers)
|
|
49
|
+
if tickers_file:
|
|
50
|
+
text = tickers_file.read_text()
|
|
51
|
+
out_tickers.extend(
|
|
52
|
+
t.strip().upper() for t in text.replace(",", "\n").splitlines() if t.strip()
|
|
53
|
+
)
|
|
54
|
+
seen = set()
|
|
55
|
+
deduped = []
|
|
56
|
+
for t in out_tickers:
|
|
57
|
+
if t not in seen:
|
|
58
|
+
seen.add(t)
|
|
59
|
+
deduped.append(t)
|
|
60
|
+
return deduped
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
# ---------------------------------------------------------------------------
|
|
64
|
+
# Cross-sectional z-scoring
|
|
65
|
+
# ---------------------------------------------------------------------------
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
def winsorize(
|
|
69
|
+
values: list[float], lo_pct: float = 0.05, hi_pct: float = 0.95
|
|
70
|
+
) -> list[float]:
|
|
71
|
+
if len(values) < 2:
|
|
72
|
+
return values
|
|
73
|
+
ordered = sorted(values)
|
|
74
|
+
lo = ordered[max(0, int(len(ordered) * lo_pct))]
|
|
75
|
+
hi = ordered[min(len(ordered) - 1, int(len(ordered) * hi_pct))]
|
|
76
|
+
return [min(max(v, lo), hi) for v in values]
|
|
77
|
+
|
|
78
|
+
|
|
79
|
+
def zscore_cross_sectionally(
|
|
80
|
+
per_ticker_values: dict[str, dict[str, float | None]],
|
|
81
|
+
signal_names: tuple[str, ...],
|
|
82
|
+
lower_is_better: set[str],
|
|
83
|
+
) -> dict[str, dict[str, float | None]]:
|
|
84
|
+
"""z-scores each named signal across the tickers that carry it; polarity
|
|
85
|
+
is normalized per `lower_is_better` so every returned z-score is
|
|
86
|
+
"higher is better", regardless of the raw signal's own polarity.
|
|
87
|
+
"""
|
|
88
|
+
z: dict[str, dict[str, float | None]] = {t: {} for t in per_ticker_values}
|
|
89
|
+
for signal in signal_names:
|
|
90
|
+
tickers = [t for t, v in per_ticker_values.items() if v.get(signal) is not None]
|
|
91
|
+
if len(tickers) < 2:
|
|
92
|
+
for t in per_ticker_values:
|
|
93
|
+
z[t][signal] = None
|
|
94
|
+
continue
|
|
95
|
+
raw = [per_ticker_values[t][signal] for t in tickers] # type: ignore[misc]
|
|
96
|
+
winsorized = winsorize(raw) # type: ignore[arg-type]
|
|
97
|
+
mean = sum(winsorized) / len(winsorized)
|
|
98
|
+
variance = sum((v - mean) ** 2 for v in winsorized) / len(winsorized)
|
|
99
|
+
stdev = variance**0.5
|
|
100
|
+
for t in per_ticker_values:
|
|
101
|
+
if t not in tickers:
|
|
102
|
+
z[t][signal] = None
|
|
103
|
+
continue
|
|
104
|
+
raw_v: float = per_ticker_values[t][signal] # type: ignore[assignment]
|
|
105
|
+
wv = min(max(raw_v, min(winsorized)), max(winsorized))
|
|
106
|
+
score = 0.0 if stdev == 0 else (wv - mean) / stdev
|
|
107
|
+
z[t][signal] = -score if signal in lower_is_better else score
|
|
108
|
+
return z
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
def composite_z(
|
|
112
|
+
z_signals: dict[str, float | None], min_signals: int = 2
|
|
113
|
+
) -> tuple[float | None, int]:
|
|
114
|
+
"""Equal-weight average of available signal z-scores, per rules.md's
|
|
115
|
+
"equal-weight available sourceable metric z-scores" rule. `composite` is
|
|
116
|
+
None when fewer than `min_signals` are available (mirrors rules.md's
|
|
117
|
+
"fewer than two sourceable metrics" -> family UNAVAILABLE rule)."""
|
|
118
|
+
available = [v for v in z_signals.values() if v is not None]
|
|
119
|
+
if len(available) < min_signals:
|
|
120
|
+
return None, len(available)
|
|
121
|
+
return sum(available) / len(available), len(available)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
# ---------------------------------------------------------------------------
|
|
125
|
+
# SHADOW output envelope (shared shape between both diagnostic scripts)
|
|
126
|
+
# ---------------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
|
|
129
|
+
def build_shadow_output(
|
|
130
|
+
tickers: list[str],
|
|
131
|
+
per_ticker_values: dict[str, dict[str, float | None]],
|
|
132
|
+
per_ticker_lineage: dict[str, Any],
|
|
133
|
+
fetch_failures: dict[str, str],
|
|
134
|
+
signal_names: tuple[str, ...],
|
|
135
|
+
lower_is_better: set[str],
|
|
136
|
+
composite_key: str,
|
|
137
|
+
family_label: str,
|
|
138
|
+
plan_path: str,
|
|
139
|
+
) -> dict[str, Any]:
|
|
140
|
+
"""Assemble the standard SHADOW diagnostic manifest: z-score every
|
|
141
|
+
signal, compute the family composite under `composite_key`, and wrap it
|
|
142
|
+
all in the disclosure envelope both scripts publish. Every record and
|
|
143
|
+
the envelope itself carry `"gating_status": "SHADOW"` -- this output is
|
|
144
|
+
never wired into rules.md's Evidence Thresholds or Adj Score.
|
|
145
|
+
"""
|
|
146
|
+
z_scores = zscore_cross_sectionally(
|
|
147
|
+
per_ticker_values, signal_names, lower_is_better
|
|
148
|
+
)
|
|
149
|
+
|
|
150
|
+
records = {}
|
|
151
|
+
for ticker in per_ticker_values:
|
|
152
|
+
composite, n_available = composite_z(z_scores[ticker])
|
|
153
|
+
records[ticker] = {
|
|
154
|
+
"gating_status": "SHADOW",
|
|
155
|
+
"raw_signals": per_ticker_values[ticker],
|
|
156
|
+
"z_signals": z_scores[ticker],
|
|
157
|
+
composite_key: composite,
|
|
158
|
+
"n_signals_available": n_available,
|
|
159
|
+
"sourceable": n_available >= 2,
|
|
160
|
+
"lineage": per_ticker_lineage[ticker],
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
return {
|
|
164
|
+
"generated_at": dt.datetime.now(dt.timezone.utc).isoformat(),
|
|
165
|
+
"gating_status": "SHADOW",
|
|
166
|
+
"note": f"Diagnostic only per {plan_path} Phase 1 -- {family_label} is NOT "
|
|
167
|
+
"wired into the 3-of-4 evidence-threshold gate or Adj Score until a "
|
|
168
|
+
"future run explicitly promotes it per the Evolution Policy "
|
|
169
|
+
"(HUMAN_REVIEW + logged shadow run).",
|
|
170
|
+
"universe_sourceable_pct": (
|
|
171
|
+
round(100 * len(per_ticker_values) / len(tickers), 1) if tickers else 0.0
|
|
172
|
+
),
|
|
173
|
+
"fetch_failures": fetch_failures,
|
|
174
|
+
"records": records,
|
|
175
|
+
}
|