sac-capital 0.1.0__tar.gz
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.
- sac_capital-0.1.0/.gitignore +53 -0
- sac_capital-0.1.0/.python-version +1 -0
- sac_capital-0.1.0/.superpowers/sdd/.gitignore +1 -0
- sac_capital-0.1.0/.superpowers/sdd/progress.md +34 -0
- sac_capital-0.1.0/.superpowers/sdd/review-00b6b02..334acae.diff +77 -0
- sac_capital-0.1.0/.superpowers/sdd/review-0e16157..0e2922d.diff +269 -0
- sac_capital-0.1.0/.superpowers/sdd/review-0e16157..f9e5d04.diff +265 -0
- sac_capital-0.1.0/.superpowers/sdd/review-0e2922d..b45c9c0.diff +197 -0
- sac_capital-0.1.0/.superpowers/sdd/review-15630ac..551ed24.diff +119 -0
- sac_capital-0.1.0/.superpowers/sdd/review-1adb9d9..52050be.diff +61 -0
- sac_capital-0.1.0/.superpowers/sdd/review-1adb9d9..64fb6b2.diff +719 -0
- sac_capital-0.1.0/.superpowers/sdd/review-1adb9d9..b288e44.diff +62 -0
- sac_capital-0.1.0/.superpowers/sdd/review-20e38d5..4d25d58.diff +574 -0
- sac_capital-0.1.0/.superpowers/sdd/review-217aef0..5c63d71.diff +512 -0
- sac_capital-0.1.0/.superpowers/sdd/review-334acae..bb3cd71.diff +178 -0
- sac_capital-0.1.0/.superpowers/sdd/review-4d25d58..ab17e0b.diff +765 -0
- sac_capital-0.1.0/.superpowers/sdd/review-551ed24..217aef0.diff +173 -0
- sac_capital-0.1.0/.superpowers/sdd/review-5c63d71..d3ee12c.diff +406 -0
- sac_capital-0.1.0/.superpowers/sdd/review-5d0e5c2..7a94bb2.diff +70 -0
- sac_capital-0.1.0/.superpowers/sdd/review-7a94bb2..64fb6b2.diff +105 -0
- sac_capital-0.1.0/.superpowers/sdd/review-a50b159..df97918.diff +5214 -0
- sac_capital-0.1.0/.superpowers/sdd/review-ab17e0b..df97918.diff +1083 -0
- sac_capital-0.1.0/.superpowers/sdd/review-b288e44..dbd5bdd.diff +73 -0
- sac_capital-0.1.0/.superpowers/sdd/review-b45c9c0..efb6182.diff +223 -0
- sac_capital-0.1.0/.superpowers/sdd/review-bb3cd71..b18f4fd.diff +92 -0
- sac_capital-0.1.0/.superpowers/sdd/review-bb3cd71..ff12ec7.diff +106 -0
- sac_capital-0.1.0/.superpowers/sdd/review-d3ee12c..20e38d5.diff +839 -0
- sac_capital-0.1.0/.superpowers/sdd/review-dbd5bdd..00b6b02.diff +78 -0
- sac_capital-0.1.0/.superpowers/sdd/review-efb6182..15630ac.diff +153 -0
- sac_capital-0.1.0/.superpowers/sdd/review-ff12ec7..5d0e5c2.diff +182 -0
- sac_capital-0.1.0/.superpowers/sdd/task-1-brief.md +75 -0
- sac_capital-0.1.0/.superpowers/sdd/task-1-report.md +83 -0
- sac_capital-0.1.0/.superpowers/sdd/task-10-brief.md +11 -0
- sac_capital-0.1.0/.superpowers/sdd/task-10-report.md +125 -0
- sac_capital-0.1.0/.superpowers/sdd/task-11-brief.md +11 -0
- sac_capital-0.1.0/.superpowers/sdd/task-11-report.md +290 -0
- sac_capital-0.1.0/.superpowers/sdd/task-12-brief.md +26 -0
- sac_capital-0.1.0/.superpowers/sdd/task-12-report.md +208 -0
- sac_capital-0.1.0/.superpowers/sdd/task-2-brief.md +81 -0
- sac_capital-0.1.0/.superpowers/sdd/task-2-report.md +58 -0
- sac_capital-0.1.0/.superpowers/sdd/task-3-brief.md +74 -0
- sac_capital-0.1.0/.superpowers/sdd/task-3-report.md +127 -0
- sac_capital-0.1.0/.superpowers/sdd/task-4-brief.md +188 -0
- sac_capital-0.1.0/.superpowers/sdd/task-4-report.md +110 -0
- sac_capital-0.1.0/.superpowers/sdd/task-5-brief.md +177 -0
- sac_capital-0.1.0/.superpowers/sdd/task-5-report.md +53 -0
- sac_capital-0.1.0/.superpowers/sdd/task-6-brief.md +53 -0
- sac_capital-0.1.0/.superpowers/sdd/task-6-report.md +78 -0
- sac_capital-0.1.0/.superpowers/sdd/task-7-brief.md +92 -0
- sac_capital-0.1.0/.superpowers/sdd/task-7-report.md +22 -0
- sac_capital-0.1.0/.superpowers/sdd/task-8-brief.md +20 -0
- sac_capital-0.1.0/.superpowers/sdd/task-8-report.md +78 -0
- sac_capital-0.1.0/.superpowers/sdd/task-9-brief.md +12 -0
- sac_capital-0.1.0/.superpowers/sdd/task-9-report.md +115 -0
- sac_capital-0.1.0/.uv-cache/.gitignore +1 -0
- sac_capital-0.1.0/.uv-cache/.lock +0 -0
- sac_capital-0.1.0/.uv-cache/CACHEDIR.TAG +1 -0
- sac_capital-0.1.0/.uv-cache/interpreter-v4/ac57ab614d7f56de/b2553c0dc7360588.msgpack +0 -0
- sac_capital-0.1.0/.uv-cache/sdists-v9/.gitignore +0 -0
- sac_capital-0.1.0/AGENTS.md +85 -0
- sac_capital-0.1.0/CLAUDE.md +71 -0
- sac_capital-0.1.0/HACKATHON.md +171 -0
- sac_capital-0.1.0/INSTALL.md +57 -0
- sac_capital-0.1.0/PKG-INFO +19 -0
- sac_capital-0.1.0/README.md +218 -0
- sac_capital-0.1.0/cli/__init__.py +0 -0
- sac_capital-0.1.0/cli/banner.py +20 -0
- sac_capital-0.1.0/cli/main.py +100 -0
- sac_capital-0.1.0/cli/setup.py +109 -0
- sac_capital-0.1.0/cli/workdir.py +19 -0
- sac_capital-0.1.0/conftest.py +5 -0
- sac_capital-0.1.0/core/__init__.py +0 -0
- sac_capital-0.1.0/core/alerts/__init__.py +0 -0
- sac_capital-0.1.0/core/alerts/telegram.py +206 -0
- sac_capital-0.1.0/core/assets/__init__.py +0 -0
- sac_capital-0.1.0/core/assets/bar.py +28 -0
- sac_capital-0.1.0/core/assets/instrument.py +18 -0
- sac_capital-0.1.0/core/bot/__init__.py +0 -0
- sac_capital-0.1.0/core/bot/command_handler.py +455 -0
- sac_capital-0.1.0/core/claude_client.py +392 -0
- sac_capital-0.1.0/core/config.py +71 -0
- sac_capital-0.1.0/core/execution/__init__.py +0 -0
- sac_capital-0.1.0/core/execution/base.py +22 -0
- sac_capital-0.1.0/core/execution/paper.py +70 -0
- sac_capital-0.1.0/core/ledger.py +221 -0
- sac_capital-0.1.0/core/markets.py +26 -0
- sac_capital-0.1.0/core/probability/__init__.py +0 -0
- sac_capital-0.1.0/core/probability/bayes.py +29 -0
- sac_capital-0.1.0/core/sizing/__init__.py +0 -0
- sac_capital-0.1.0/core/sizing/kelly.py +42 -0
- sac_capital-0.1.0/core/strategy.py +23 -0
- sac_capital-0.1.0/deploy/README.md +90 -0
- sac_capital-0.1.0/deploy/com.polymarketbot.commands.plist +38 -0
- sac_capital-0.1.0/deploy/com.polymarketbot.equities.mark.plist +37 -0
- sac_capital-0.1.0/deploy/com.polymarketbot.equities.scan.plist +56 -0
- sac_capital-0.1.0/deploy/com.polymarketbot.nightly.plist +41 -0
- sac_capital-0.1.0/deploy/com.polymarketbot.resolve.plist +42 -0
- sac_capital-0.1.0/deploy/com.polymarketbot.runner.plist +31 -0
- sac_capital-0.1.0/deploy/com.polymarketbot.situational-digest.plist +41 -0
- sac_capital-0.1.0/equities/__init__.py +0 -0
- sac_capital-0.1.0/equities/analysis/__init__.py +0 -0
- sac_capital-0.1.0/equities/analysis/agents.py +134 -0
- sac_capital-0.1.0/equities/analysis/analyst.py +1003 -0
- sac_capital-0.1.0/equities/analysis/budget.py +38 -0
- sac_capital-0.1.0/equities/analysis/checkpoint.py +133 -0
- sac_capital-0.1.0/equities/analysis/core_analyst.py +137 -0
- sac_capital-0.1.0/equities/analysis/core_reviewers.py +161 -0
- sac_capital-0.1.0/equities/analysis/prompt.py +355 -0
- sac_capital-0.1.0/equities/analysis/schema.py +258 -0
- sac_capital-0.1.0/equities/analysis/sentiment_agent.py +33 -0
- sac_capital-0.1.0/equities/eval/__init__.py +1 -0
- sac_capital-0.1.0/equities/eval/citation_attribution.py +51 -0
- sac_capital-0.1.0/equities/eval/replay.py +349 -0
- sac_capital-0.1.0/equities/eval/report.py +65 -0
- sac_capital-0.1.0/equities/eval/situational_digest.py +147 -0
- sac_capital-0.1.0/equities/execution/__init__.py +1 -0
- sac_capital-0.1.0/equities/execution/alpaca.py +290 -0
- sac_capital-0.1.0/equities/execution/reconciler.py +147 -0
- sac_capital-0.1.0/equities/fund.py +13 -0
- sac_capital-0.1.0/equities/improve/__init__.py +0 -0
- sac_capital-0.1.0/equities/improve/promoter.py +108 -0
- sac_capital-0.1.0/equities/improve/tournament.py +60 -0
- sac_capital-0.1.0/equities/improve/variants.py +55 -0
- sac_capital-0.1.0/equities/killgate/__init__.py +0 -0
- sac_capital-0.1.0/equities/killgate/cost_model.py +62 -0
- sac_capital-0.1.0/equities/killgate/gate.py +81 -0
- sac_capital-0.1.0/equities/killgate/thesis_health.py +99 -0
- sac_capital-0.1.0/equities/killgate/tracker.py +172 -0
- sac_capital-0.1.0/equities/ledger_equity.py +326 -0
- sac_capital-0.1.0/equities/paper.py +104 -0
- sac_capital-0.1.0/equities/pit.py +59 -0
- sac_capital-0.1.0/equities/research/__init__.py +1 -0
- sac_capital-0.1.0/equities/research/artifacts.py +183 -0
- sac_capital-0.1.0/equities/research/backtest.py +369 -0
- sac_capital-0.1.0/equities/research/discovery_lag.py +39 -0
- sac_capital-0.1.0/equities/research/lag_rules.py +211 -0
- sac_capital-0.1.0/equities/research/memory.py +241 -0
- sac_capital-0.1.0/equities/research/narrative_gap.py +50 -0
- sac_capital-0.1.0/equities/research/run_manifest.py +100 -0
- sac_capital-0.1.0/equities/research/store.py +32 -0
- sac_capital-0.1.0/equities/research/supply_chain.py +85 -0
- sac_capital-0.1.0/equities/research/thesis_miner.py +129 -0
- sac_capital-0.1.0/equities/risk/__init__.py +0 -0
- sac_capital-0.1.0/equities/risk/exits.py +47 -0
- sac_capital-0.1.0/equities/risk/kernel.py +195 -0
- sac_capital-0.1.0/equities/risk/sizing.py +49 -0
- sac_capital-0.1.0/equities/risk/state.py +35 -0
- sac_capital-0.1.0/equities/risk/vol_target.py +64 -0
- sac_capital-0.1.0/equities/screen/__init__.py +0 -0
- sac_capital-0.1.0/equities/screen/event_screen.py +205 -0
- sac_capital-0.1.0/equities/screen/inflection_screen.py +89 -0
- sac_capital-0.1.0/equities/screen/politician_screen.py +89 -0
- sac_capital-0.1.0/equities/screen/quality_screen.py +128 -0
- sac_capital-0.1.0/equities/screen/relative_strength.py +197 -0
- sac_capital-0.1.0/equities/screen/supply_chain_lag_screen.py +276 -0
- sac_capital-0.1.0/equities/screen/thematic_monitor.py +30 -0
- sac_capital-0.1.0/equities/signal_stats.py +110 -0
- sac_capital-0.1.0/equities/strategy.py +40 -0
- sac_capital-0.1.0/foundry.toml +16 -0
- sac_capital-0.1.0/hackathon/__init__.py +2 -0
- sac_capital-0.1.0/hackathon/verifiability.py +145 -0
- sac_capital-0.1.0/harness/__init__.py +0 -0
- sac_capital-0.1.0/harness/approval.py +76 -0
- sac_capital-0.1.0/harness/learn/__init__.py +0 -0
- sac_capital-0.1.0/harness/learn/calibration.py +39 -0
- sac_capital-0.1.0/harness/learn/retune.py +47 -0
- sac_capital-0.1.0/harness/learn/weather_bias.py +22 -0
- sac_capital-0.1.0/harness/nightly.py +129 -0
- sac_capital-0.1.0/harness/obsidian.py +45 -0
- sac_capital-0.1.0/harness/params.py +148 -0
- sac_capital-0.1.0/orchestrator/__init__.py +0 -0
- sac_capital-0.1.0/orchestrator/allocator.py +45 -0
- sac_capital-0.1.0/orchestrator/performance.py +62 -0
- sac_capital-0.1.0/orchestrator/reconcile.py +45 -0
- sac_capital-0.1.0/orchestrator/report.py +44 -0
- sac_capital-0.1.0/orchestrator/risk.py +78 -0
- sac_capital-0.1.0/pyproject.toml +51 -0
- sac_capital-0.1.0/runner_equities.py +1308 -0
- sac_capital-0.1.0/runner_research.py +175 -0
- sac_capital-0.1.0/scripts/backfill_static_research_artifacts.py +60 -0
- sac_capital-0.1.0/scripts/export_mantle_commitments.py +61 -0
- sac_capital-0.1.0/scripts/find_markets.js +22 -0
- sac_capital-0.1.0/scripts/generate_frontend_data.py +258 -0
- sac_capital-0.1.0/scripts/preflight.py +124 -0
- sac_capital-0.1.0/scripts/run_equities_service.py +72 -0
- sac_capital-0.1.0/scripts/run_nightly.py +40 -0
- sac_capital-0.1.0/scripts/run_situational_digest.py +58 -0
- sac_capital-0.1.0/scripts/sizing_ab_report.py +243 -0
- sac_capital-0.1.0/scripts/submit_mantle_decisions.py +224 -0
- sac_capital-0.1.0/scripts/test_senate_local.py +39 -0
- sac_capital-0.1.0/strategies/__init__.py +0 -0
- sac_capital-0.1.0/uv.lock +3096 -0
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Virtual environment
|
|
2
|
+
.venv/
|
|
3
|
+
|
|
4
|
+
# Python bytecode
|
|
5
|
+
__pycache__/
|
|
6
|
+
*.pyc
|
|
7
|
+
*.pyo
|
|
8
|
+
*.pyd
|
|
9
|
+
|
|
10
|
+
# Environment
|
|
11
|
+
.env
|
|
12
|
+
|
|
13
|
+
# Data files
|
|
14
|
+
*.csv
|
|
15
|
+
*.jsonl
|
|
16
|
+
*.log
|
|
17
|
+
/data/
|
|
18
|
+
*.db
|
|
19
|
+
|
|
20
|
+
# Exploratory spikes
|
|
21
|
+
spike/
|
|
22
|
+
|
|
23
|
+
# Distribution / packaging
|
|
24
|
+
dist/
|
|
25
|
+
build/
|
|
26
|
+
*.egg-info/
|
|
27
|
+
.eggs/
|
|
28
|
+
frontend/dist/
|
|
29
|
+
frontend/node_modules/
|
|
30
|
+
frontend/*.tsbuildinfo
|
|
31
|
+
broadcast/
|
|
32
|
+
cache/
|
|
33
|
+
contracts/out/
|
|
34
|
+
|
|
35
|
+
# Testing / coverage
|
|
36
|
+
.pytest_cache/
|
|
37
|
+
.coverage
|
|
38
|
+
htmlcov/
|
|
39
|
+
|
|
40
|
+
# Type checking
|
|
41
|
+
.mypy_cache/
|
|
42
|
+
.ruff_cache/
|
|
43
|
+
|
|
44
|
+
# IDEs
|
|
45
|
+
.idea/
|
|
46
|
+
.vscode/
|
|
47
|
+
*.swp
|
|
48
|
+
*.swo
|
|
49
|
+
|
|
50
|
+
# OS
|
|
51
|
+
.DS_Store
|
|
52
|
+
Thumbs.db
|
|
53
|
+
.vercel
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
3.12
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
*
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# Progress — Plan 2026-07-03 hardening-and-alpha-improvements
|
|
2
|
+
|
|
3
|
+
Branch: hardening-2026-07
|
|
4
|
+
Base: 0e16157680c3c3aa5b4a3ed1946d57739a0f2726
|
|
5
|
+
|
|
6
|
+
Task 0 (secrets rotation): USER ACTION REQUIRED (Mantle key + Telegram token) — deferred
|
|
7
|
+
Task 1: complete (commits 0e16157..0e2922d, review clean)
|
|
8
|
+
Task 2: complete (commits 0e2922d..b45c9c0, review clean)
|
|
9
|
+
Task 3: complete (commits b45c9c0..efb6182, review clean)
|
|
10
|
+
Minor (deferred to final review): _has_active_broker_order now unused in runner_equities.py
|
|
11
|
+
Task 4: complete (commits efb6182..15630ac, review clean)
|
|
12
|
+
Task 5: complete (commits 15630ac..551ed24, review clean)
|
|
13
|
+
Task 6: complete (commits 551ed24..217aef0, review clean) — PHASE 1 DONE
|
|
14
|
+
Task 10: complete (commits 20e38d5..4d25d58, review clean)
|
|
15
|
+
Task 11: complete (commits 4d25d58..ab17e0b, review clean)
|
|
16
|
+
|
|
17
|
+
# Progress — Plan 2026-07-04 sac-capital-cli
|
|
18
|
+
|
|
19
|
+
Branch: feat/sac-cli
|
|
20
|
+
Base: 1adb9d9
|
|
21
|
+
Baseline: 4 pre-existing failures in equities/data/tests/test_senate_efd_disclosures.py (not ours)
|
|
22
|
+
|
|
23
|
+
Task 1: complete (commits 1adb9d9..b288e44, review clean after 1 fix)
|
|
24
|
+
Task 2: complete (commits b288e44..dbd5bdd, review clean)
|
|
25
|
+
Minor (deferred to final review): repo mode does not ensure data/ subdir exists
|
|
26
|
+
Task 3: complete (commits dbd5bdd..334acae, review clean after 1 fix)
|
|
27
|
+
Task 4: complete (commits 334acae..ff12ec7, review clean after 2 security fixes: env injection, 0600 perms)
|
|
28
|
+
Minor (deferred to final review): getpass echo for secrets; empty ALPACA_SECRET_KEY when key id given
|
|
29
|
+
Task 5: complete (commits ff12ec7..5d0e5c2, review clean; REMAINDER deviation verified necessary)
|
|
30
|
+
Minor (deferred to final review): dead return-2 lines cli/main.py:67,98; dict(_COMMANDS) rebuilt per call
|
|
31
|
+
Task 6: complete (commits 5d0e5c2..7a94bb2, review clean)
|
|
32
|
+
Minor (deferred to final review): broadcast in wheel packages list is inert config
|
|
33
|
+
Task 7: complete (commits 7a94bb2..64fb6b2, review clean)
|
|
34
|
+
Task 8: complete (full suite 786 pass/4 baseline, build+install smoke OK, final review READY TO MERGE, all 6 minors triaged fine-to-defer)
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
# Review package: 00b6b02..HEAD
|
|
2
|
+
|
|
3
|
+
## Commits
|
|
4
|
+
334acae test(llm): lock claude_cli subprocess contract; fix stale docstring
|
|
5
|
+
|
|
6
|
+
## Files changed
|
|
7
|
+
core/claude_client.py | 2 +-
|
|
8
|
+
tests/test_llm_provider.py | 24 ++++++++++++++++++++++++
|
|
9
|
+
2 files changed, 25 insertions(+), 1 deletion(-)
|
|
10
|
+
|
|
11
|
+
## Diff
|
|
12
|
+
diff --git a/core/claude_client.py b/core/claude_client.py
|
|
13
|
+
index 82da04b..bd2a545 100644
|
|
14
|
+
--- a/core/claude_client.py
|
|
15
|
+
+++ b/core/claude_client.py
|
|
16
|
+
@@ -246,21 +246,21 @@ class CodexCLIClient:
|
|
17
|
+
output_tokens=len(text) // 4,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class ClaudeCodeClient:
|
|
22
|
+
"""Backward-compatible client.
|
|
23
|
+
|
|
24
|
+
Provider selection:
|
|
25
|
+
- LLM_PROVIDER=codex -> local Codex CLI / ChatGPT login
|
|
26
|
+
- LLM_PROVIDER=openai -> OpenAI API (requires OPENAI_API_KEY)
|
|
27
|
+
- - LLM_PROVIDER=claude -> Claude CLI
|
|
28
|
+
+ - LLM_PROVIDER=claude_cli -> Claude CLI
|
|
29
|
+
- LLM_PROVIDER=anthropic -> Anthropic SDK
|
|
30
|
+
- blank / auto -> Codex CLI
|
|
31
|
+
|
|
32
|
+
Args:
|
|
33
|
+
timeout: Max seconds to wait for a response (default 60).
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
_MODEL_MAP = {
|
|
37
|
+
"claude-haiku-4-5-20251001": "claude-haiku-4-5-20251001",
|
|
38
|
+
"claude-sonnet-4-6": "claude-sonnet-4-6",
|
|
39
|
+
diff --git a/tests/test_llm_provider.py b/tests/test_llm_provider.py
|
|
40
|
+
index aa9519e..b556961 100644
|
|
41
|
+
--- a/tests/test_llm_provider.py
|
|
42
|
+
+++ b/tests/test_llm_provider.py
|
|
43
|
+
@@ -215,10 +215,34 @@ def test_claude_cli_provider_is_first_class(monkeypatch):
|
|
44
|
+
calls = {}
|
|
45
|
+
|
|
46
|
+
def fake_cli(system, user, model):
|
|
47
|
+
calls["model"] = model
|
|
48
|
+
return LLMResponse(content="ok", input_tokens=1, output_tokens=1)
|
|
49
|
+
|
|
50
|
+
client._complete_with_claude_cli = fake_cli
|
|
51
|
+
resp = client.complete("sys", "usr", "fast")
|
|
52
|
+
assert resp.content == "ok"
|
|
53
|
+
assert calls["model"] == "fast"
|
|
54
|
+
+
|
|
55
|
+
+
|
|
56
|
+
+def test_claude_cli_provider_invokes_claude_binary_via_subprocess(monkeypatch):
|
|
57
|
+
+ """LLM_PROVIDER=claude_cli must shell out to `claude -p --model <mapped>` via
|
|
58
|
+
+ subprocess, pass the prompt via `input`, and raise on nonzero returncode."""
|
|
59
|
+
+ monkeypatch.delenv("ANTHROPIC_API_KEY", raising=False)
|
|
60
|
+
+ monkeypatch.delenv("OPENAI_API_KEY", raising=False)
|
|
61
|
+
+ monkeypatch.setenv("LLM_PROVIDER", "claude_cli")
|
|
62
|
+
+
|
|
63
|
+
+ calls = {}
|
|
64
|
+
+
|
|
65
|
+
+ def fake_run(args, input, capture_output, text, timeout, start_new_session=False):
|
|
66
|
+
+ calls["args"] = args
|
|
67
|
+
+ calls["input"] = input
|
|
68
|
+
+ return SimpleNamespace(returncode=1, stderr="boom", stdout="")
|
|
69
|
+
+
|
|
70
|
+
+ monkeypatch.setattr("core.claude_client.subprocess.run", fake_run)
|
|
71
|
+
+ client = ClaudeCodeClient()
|
|
72
|
+
+
|
|
73
|
+
+ with pytest.raises(RuntimeError, match="boom"):
|
|
74
|
+
+ client.complete("sys", "usr", "strong")
|
|
75
|
+
+
|
|
76
|
+
+ assert calls["args"][:4] == ["claude", "-p", "--model", "claude-sonnet-4-6"]
|
|
77
|
+
+ assert "sys" in calls["input"] and "usr" in calls["input"]
|
|
@@ -0,0 +1,269 @@
|
|
|
1
|
+
# Review package: 0e16157..HEAD
|
|
2
|
+
|
|
3
|
+
## Commits
|
|
4
|
+
0e2922d fix(risk): only persist kernel state when it changes
|
|
5
|
+
f9e5d04 fix(risk): persist high-water mark and halt flag across runner restarts
|
|
6
|
+
|
|
7
|
+
## Files changed
|
|
8
|
+
.superpowers/sdd/progress.md | 12 +++-----
|
|
9
|
+
equities/risk/kernel.py | 19 +++++++++++++
|
|
10
|
+
equities/risk/state.py | 35 +++++++++++++++++++++++
|
|
11
|
+
runner_equities.py | 1 +
|
|
12
|
+
tests/equities/test_kernel_state.py | 55 +++++++++++++++++++++++++++++++++++++
|
|
13
|
+
5 files changed, 114 insertions(+), 8 deletions(-)
|
|
14
|
+
|
|
15
|
+
## Diff
|
|
16
|
+
diff --git a/.superpowers/sdd/progress.md b/.superpowers/sdd/progress.md
|
|
17
|
+
index 10f7e59..339d3bb 100644
|
|
18
|
+
--- a/.superpowers/sdd/progress.md
|
|
19
|
+
+++ b/.superpowers/sdd/progress.md
|
|
20
|
+
@@ -1,10 +1,6 @@
|
|
21
|
+
-# Progress — Plan 08 situational-awareness-additions
|
|
22
|
+
+# Progress — Plan 2026-07-03 hardening-and-alpha-improvements
|
|
23
|
+
|
|
24
|
+
-Branch: situational-awareness-additions
|
|
25
|
+
-Base: d5cb4d513d7207f4ebd3415714a112168b60aaf5
|
|
26
|
+
+Branch: hardening-2026-07
|
|
27
|
+
+Base: 0e16157680c3c3aa5b4a3ed1946d57739a0f2726
|
|
28
|
+
|
|
29
|
+
-Task 1 (Gap 4 preflight): complete (commits d5cb4d5..162e105, review clean)
|
|
30
|
+
-Task 2 (Gap 1 calibration): complete (commits 162e105..7b3b434, review clean)
|
|
31
|
+
-Task 3 (Gap 2 citation linkage): complete (commits 7b3b434..84d3fdc, review clean)
|
|
32
|
+
-Task 4 (Gap 3 run manifest): complete (commits 84d3fdc..2c1fa4d, review clean)
|
|
33
|
+
-Task 5 (Gap 5 nightly digest): complete (commits 2c1fa4d..21cfe78, review clean)
|
|
34
|
+
+Task 0 (secrets rotation): USER ACTION REQUIRED (Mantle key + Telegram token) — deferred
|
|
35
|
+
diff --git a/equities/risk/kernel.py b/equities/risk/kernel.py
|
|
36
|
+
index daef72d..5810cde 100644
|
|
37
|
+
--- a/equities/risk/kernel.py
|
|
38
|
+
+++ b/equities/risk/kernel.py
|
|
39
|
+
@@ -11,20 +11,21 @@ Fuses enforced:
|
|
40
|
+
- Per-name concentration: no single name > max_name_pct of swing sleeve
|
|
41
|
+
- Max daily loss: halts new entries for the day when breached
|
|
42
|
+
- Max total drawdown circuit-breaker: halts ALL trading
|
|
43
|
+
- Real-money promotion gate: LIVE flag absent by default
|
|
44
|
+
"""
|
|
45
|
+
from __future__ import annotations
|
|
46
|
+
|
|
47
|
+
import math
|
|
48
|
+
from dataclasses import dataclass
|
|
49
|
+
from datetime import date
|
|
50
|
+
+from pathlib import Path
|
|
51
|
+
from typing import Any
|
|
52
|
+
|
|
53
|
+
from equities.risk.sizing import size_shares, _DEFAULT_GAP_PCT
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
@dataclass(frozen=True)
|
|
57
|
+
class SizedRecommendation:
|
|
58
|
+
"""A Recommendation approved and sized by the risk kernel."""
|
|
59
|
+
|
|
60
|
+
# Forward-ref import avoided — using Any for the Recommendation type
|
|
61
|
+
@@ -52,37 +53,48 @@ class RiskKernel:
|
|
62
|
+
def __init__(
|
|
63
|
+
self,
|
|
64
|
+
capital: float,
|
|
65
|
+
risk_pct: float = 0.02,
|
|
66
|
+
max_positions: int = 4,
|
|
67
|
+
max_name_pct: float = 0.25,
|
|
68
|
+
max_sector_pct: float = 0.35,
|
|
69
|
+
daily_loss_limit_pct: float = 0.05,
|
|
70
|
+
drawdown_limit_pct: float = 0.15,
|
|
71
|
+
gap_pct: float = _DEFAULT_GAP_PCT,
|
|
72
|
+
+ state_path: Path | None = None,
|
|
73
|
+
) -> None:
|
|
74
|
+
if capital <= 0:
|
|
75
|
+
raise ValueError(f"RiskKernel capital must be positive, got {capital}")
|
|
76
|
+
self.capital = capital
|
|
77
|
+
self.risk_pct = risk_pct
|
|
78
|
+
self.max_positions = max_positions
|
|
79
|
+
self.max_name_pct = max_name_pct
|
|
80
|
+
self.max_sector_pct = max_sector_pct
|
|
81
|
+
self.daily_loss_limit_pct = daily_loss_limit_pct
|
|
82
|
+
self.drawdown_limit_pct = drawdown_limit_pct
|
|
83
|
+
self.gap_pct = gap_pct
|
|
84
|
+
|
|
85
|
+
+ self._state_path = state_path
|
|
86
|
+
self._high_water_mark = capital
|
|
87
|
+
self._today = date.today().isoformat()
|
|
88
|
+
self._daily_loss = 0.0
|
|
89
|
+
self._halted = False # circuit-breaker flag
|
|
90
|
+
|
|
91
|
+
+ # Load persisted state if available
|
|
92
|
+
+ if state_path is not None:
|
|
93
|
+
+ from equities.risk.state import load_kernel_state
|
|
94
|
+
+ saved = load_kernel_state(state_path)
|
|
95
|
+
+ if float(saved.get("capital", capital)) == capital:
|
|
96
|
+
+ self._high_water_mark = max(capital, float(saved.get("high_water_mark", capital)))
|
|
97
|
+
+ self._halted = bool(saved.get("halted", False))
|
|
98
|
+
+ # else: bankroll changed in config -> stale hwm/halt discarded, fresh baseline
|
|
99
|
+
+
|
|
100
|
+
# ------------------------------------------------------------------
|
|
101
|
+
# Public API
|
|
102
|
+
# ------------------------------------------------------------------
|
|
103
|
+
|
|
104
|
+
def approve(
|
|
105
|
+
self,
|
|
106
|
+
recommendation: Any,
|
|
107
|
+
open_positions: list[dict[str, Any]],
|
|
108
|
+
today_realized_loss: float = 0.0,
|
|
109
|
+
current_equity: float | None = None,
|
|
110
|
+
@@ -95,24 +107,31 @@ class RiskKernel:
|
|
111
|
+
open_positions: Current open positions (list of dicts from EquityLedger).
|
|
112
|
+
today_realized_loss: Sum of today's closed pnl (negative = loss, positive = gain).
|
|
113
|
+
current_equity: Current total equity for drawdown check (None = skip).
|
|
114
|
+
"""
|
|
115
|
+
# --- Circuit-breaker ---
|
|
116
|
+
if self._halted:
|
|
117
|
+
return SizedRecommendation(recommendation, 0.0, False, "circuit_breaker_tripped")
|
|
118
|
+
|
|
119
|
+
# --- Drawdown check ---
|
|
120
|
+
if current_equity is not None:
|
|
121
|
+
+ prior_hwm = self._high_water_mark
|
|
122
|
+
+ prior_halted = self._halted
|
|
123
|
+
self._high_water_mark = max(self._high_water_mark, current_equity)
|
|
124
|
+
drawdown = (self._high_water_mark - current_equity) / self._high_water_mark
|
|
125
|
+
if drawdown >= self.drawdown_limit_pct:
|
|
126
|
+
self._halted = True
|
|
127
|
+
+ # Save state only if it changed
|
|
128
|
+
+ if self._state_path is not None and (self._high_water_mark != prior_hwm or self._halted != prior_halted):
|
|
129
|
+
+ from equities.risk.state import save_kernel_state
|
|
130
|
+
+ save_kernel_state(self._state_path, self._high_water_mark, self._halted, self.capital)
|
|
131
|
+
+ if drawdown >= self.drawdown_limit_pct:
|
|
132
|
+
return SizedRecommendation(recommendation, 0.0, False, f"drawdown={drawdown:.1%}_exceeds_{self.drawdown_limit_pct:.0%}")
|
|
133
|
+
|
|
134
|
+
# --- Daily loss halt ---
|
|
135
|
+
if today_realized_loss < -(self.daily_loss_limit_pct * self.capital):
|
|
136
|
+
return SizedRecommendation(recommendation, 0.0, False, "daily_loss_limit_hit")
|
|
137
|
+
|
|
138
|
+
sleeve = getattr(recommendation, "sleeve", None)
|
|
139
|
+
is_core = sleeve is not None and str(sleeve.value) == "core"
|
|
140
|
+
|
|
141
|
+
# --- CORE DCA: fixed fractional sizing (no stop required) ---
|
|
142
|
+
diff --git a/equities/risk/state.py b/equities/risk/state.py
|
|
143
|
+
new file mode 100644
|
|
144
|
+
index 0000000..171ae7f
|
|
145
|
+
--- /dev/null
|
|
146
|
+
+++ b/equities/risk/state.py
|
|
147
|
+
@@ -0,0 +1,35 @@
|
|
148
|
+
+"""Kernel state persistence: high-water mark and halt flag."""
|
|
149
|
+
+import json
|
|
150
|
+
+from pathlib import Path
|
|
151
|
+
+
|
|
152
|
+
+
|
|
153
|
+
+def load_kernel_state(path: Path) -> dict:
|
|
154
|
+
+ """Load persisted kernel state from JSON file.
|
|
155
|
+
+
|
|
156
|
+
+ Args:
|
|
157
|
+
+ path: Path to state file.
|
|
158
|
+
+
|
|
159
|
+
+ Returns:
|
|
160
|
+
+ dict with keys: high_water_mark (float), halted (bool), capital (float).
|
|
161
|
+
+ Empty dict if file doesn't exist or is unreadable.
|
|
162
|
+
+ """
|
|
163
|
+
+ if not path.exists():
|
|
164
|
+
+ return {}
|
|
165
|
+
+ try:
|
|
166
|
+
+ return json.loads(path.read_text())
|
|
167
|
+
+ except (json.JSONDecodeError, OSError):
|
|
168
|
+
+ return {}
|
|
169
|
+
+
|
|
170
|
+
+
|
|
171
|
+
+def save_kernel_state(path: Path, hwm: float, halted: bool, capital: float) -> None:
|
|
172
|
+
+ """Persist kernel state to JSON file atomically.
|
|
173
|
+
+
|
|
174
|
+
+ Args:
|
|
175
|
+
+ path: Path to state file.
|
|
176
|
+
+ hwm: High-water mark (peak equity).
|
|
177
|
+
+ halted: Circuit-breaker flag.
|
|
178
|
+
+ capital: Current capital (for validation on reload).
|
|
179
|
+
+ """
|
|
180
|
+
+ tmp = path.with_suffix(".tmp")
|
|
181
|
+
+ tmp.write_text(json.dumps({"high_water_mark": hwm, "halted": halted, "capital": capital}))
|
|
182
|
+
+ tmp.replace(path)
|
|
183
|
+
diff --git a/runner_equities.py b/runner_equities.py
|
|
184
|
+
index ce5cb8e..0072ff7 100644
|
|
185
|
+
--- a/runner_equities.py
|
|
186
|
+
+++ b/runner_equities.py
|
|
187
|
+
@@ -892,20 +892,21 @@ async def run_once(
|
|
188
|
+
|
|
189
|
+
# --- Risk kernel + paper open ---
|
|
190
|
+
kernel = RiskKernel(
|
|
191
|
+
capital=settings.bankroll_usd,
|
|
192
|
+
risk_pct=settings.equity_risk_pct,
|
|
193
|
+
max_positions=settings.equity_max_positions,
|
|
194
|
+
max_name_pct=settings.equity_max_name_pct,
|
|
195
|
+
max_sector_pct=settings.equity_max_sector_pct,
|
|
196
|
+
daily_loss_limit_pct=settings.equity_daily_loss_limit_pct,
|
|
197
|
+
drawdown_limit_pct=settings.equity_drawdown_limit_pct,
|
|
198
|
+
+ state_path=Path("data/kernel_state.json"),
|
|
199
|
+
)
|
|
200
|
+
open_positions = equity_ledger.open_positions()
|
|
201
|
+
sector_lookup: dict[str, str] = {}
|
|
202
|
+
|
|
203
|
+
def sector_for(ticker: str) -> str:
|
|
204
|
+
if ticker in sector_lookup:
|
|
205
|
+
return sector_lookup[ticker]
|
|
206
|
+
try:
|
|
207
|
+
sector_lookup[ticker] = fundamentals_provider.fetch(ticker).sector
|
|
208
|
+
except (RuntimeError, TimeoutError):
|
|
209
|
+
diff --git a/tests/equities/test_kernel_state.py b/tests/equities/test_kernel_state.py
|
|
210
|
+
new file mode 100644
|
|
211
|
+
index 0000000..cd0b33b
|
|
212
|
+
--- /dev/null
|
|
213
|
+
+++ b/tests/equities/test_kernel_state.py
|
|
214
|
+
@@ -0,0 +1,55 @@
|
|
215
|
+
+"""Test kernel state persistence across restarts."""
|
|
216
|
+
+from pathlib import Path
|
|
217
|
+
+
|
|
218
|
+
+from equities.risk.kernel import RiskKernel
|
|
219
|
+
+
|
|
220
|
+
+
|
|
221
|
+
+class FakeRecommendation:
|
|
222
|
+
+ """Minimal recommendation stub for testing kernel approval."""
|
|
223
|
+
+
|
|
224
|
+
+ def __init__(self):
|
|
225
|
+
+ self.entry = 100.0
|
|
226
|
+
+ self.stop_loss = 95.0
|
|
227
|
+
+ self.size_pct = 0.02
|
|
228
|
+
+ self.sleeve = None
|
|
229
|
+
+
|
|
230
|
+
+ class Instrument:
|
|
231
|
+
+ ticker = "TEST"
|
|
232
|
+
+
|
|
233
|
+
+ instrument = Instrument()
|
|
234
|
+
+
|
|
235
|
+
+
|
|
236
|
+
+def test_high_water_mark_survives_restart(tmp_path: Path):
|
|
237
|
+
+ """Verify HWM persists when kernel is restarted."""
|
|
238
|
+
+ state = tmp_path / "kernel_state.json"
|
|
239
|
+
+ k1 = RiskKernel(capital=10_000, state_path=state)
|
|
240
|
+
+ # Simulate equity climbing to 12k via approve() with current_equity param
|
|
241
|
+
+ rec = FakeRecommendation()
|
|
242
|
+
+ k1.approve(rec, [], current_equity=12_000)
|
|
243
|
+
+
|
|
244
|
+
+ # Create new kernel with same state file — should restore HWM
|
|
245
|
+
+ k2 = RiskKernel(capital=10_000, state_path=state)
|
|
246
|
+
+ assert k2._high_water_mark == 12_000
|
|
247
|
+
+
|
|
248
|
+
+
|
|
249
|
+
+def test_halted_flag_survives_restart(tmp_path: Path):
|
|
250
|
+
+ """Verify halt flag persists when kernel is restarted."""
|
|
251
|
+
+ state = tmp_path / "kernel_state.json"
|
|
252
|
+
+ k1 = RiskKernel(capital=10_000, state_path=state)
|
|
253
|
+
+ rec = FakeRecommendation()
|
|
254
|
+
+
|
|
255
|
+
+ # Set HWM to 12k
|
|
256
|
+
+ k1.approve(rec, [], current_equity=12_000)
|
|
257
|
+
+
|
|
258
|
+
+ # Trigger halt: 10k / 12k = 16.7% drawdown (exceeds 15% default limit)
|
|
259
|
+
+ result = k1.approve(rec, [], current_equity=10_000)
|
|
260
|
+
+ assert not result.approved and "drawdown" in result.rejection_reason
|
|
261
|
+
+ assert k1._halted
|
|
262
|
+
+
|
|
263
|
+
+ # Create new kernel — should restore halted state
|
|
264
|
+
+ k2 = RiskKernel(capital=10_000, state_path=state)
|
|
265
|
+
+ assert k2._halted
|
|
266
|
+
+
|
|
267
|
+
+ # Verify new approval is rejected due to restored halt
|
|
268
|
+
+ result2 = k2.approve(rec, [], current_equity=10_000)
|
|
269
|
+
+ assert not result2.approved and "circuit_breaker" in result2.rejection_reason
|