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.
Files changed (192) hide show
  1. sac_capital-0.1.0/.gitignore +53 -0
  2. sac_capital-0.1.0/.python-version +1 -0
  3. sac_capital-0.1.0/.superpowers/sdd/.gitignore +1 -0
  4. sac_capital-0.1.0/.superpowers/sdd/progress.md +34 -0
  5. sac_capital-0.1.0/.superpowers/sdd/review-00b6b02..334acae.diff +77 -0
  6. sac_capital-0.1.0/.superpowers/sdd/review-0e16157..0e2922d.diff +269 -0
  7. sac_capital-0.1.0/.superpowers/sdd/review-0e16157..f9e5d04.diff +265 -0
  8. sac_capital-0.1.0/.superpowers/sdd/review-0e2922d..b45c9c0.diff +197 -0
  9. sac_capital-0.1.0/.superpowers/sdd/review-15630ac..551ed24.diff +119 -0
  10. sac_capital-0.1.0/.superpowers/sdd/review-1adb9d9..52050be.diff +61 -0
  11. sac_capital-0.1.0/.superpowers/sdd/review-1adb9d9..64fb6b2.diff +719 -0
  12. sac_capital-0.1.0/.superpowers/sdd/review-1adb9d9..b288e44.diff +62 -0
  13. sac_capital-0.1.0/.superpowers/sdd/review-20e38d5..4d25d58.diff +574 -0
  14. sac_capital-0.1.0/.superpowers/sdd/review-217aef0..5c63d71.diff +512 -0
  15. sac_capital-0.1.0/.superpowers/sdd/review-334acae..bb3cd71.diff +178 -0
  16. sac_capital-0.1.0/.superpowers/sdd/review-4d25d58..ab17e0b.diff +765 -0
  17. sac_capital-0.1.0/.superpowers/sdd/review-551ed24..217aef0.diff +173 -0
  18. sac_capital-0.1.0/.superpowers/sdd/review-5c63d71..d3ee12c.diff +406 -0
  19. sac_capital-0.1.0/.superpowers/sdd/review-5d0e5c2..7a94bb2.diff +70 -0
  20. sac_capital-0.1.0/.superpowers/sdd/review-7a94bb2..64fb6b2.diff +105 -0
  21. sac_capital-0.1.0/.superpowers/sdd/review-a50b159..df97918.diff +5214 -0
  22. sac_capital-0.1.0/.superpowers/sdd/review-ab17e0b..df97918.diff +1083 -0
  23. sac_capital-0.1.0/.superpowers/sdd/review-b288e44..dbd5bdd.diff +73 -0
  24. sac_capital-0.1.0/.superpowers/sdd/review-b45c9c0..efb6182.diff +223 -0
  25. sac_capital-0.1.0/.superpowers/sdd/review-bb3cd71..b18f4fd.diff +92 -0
  26. sac_capital-0.1.0/.superpowers/sdd/review-bb3cd71..ff12ec7.diff +106 -0
  27. sac_capital-0.1.0/.superpowers/sdd/review-d3ee12c..20e38d5.diff +839 -0
  28. sac_capital-0.1.0/.superpowers/sdd/review-dbd5bdd..00b6b02.diff +78 -0
  29. sac_capital-0.1.0/.superpowers/sdd/review-efb6182..15630ac.diff +153 -0
  30. sac_capital-0.1.0/.superpowers/sdd/review-ff12ec7..5d0e5c2.diff +182 -0
  31. sac_capital-0.1.0/.superpowers/sdd/task-1-brief.md +75 -0
  32. sac_capital-0.1.0/.superpowers/sdd/task-1-report.md +83 -0
  33. sac_capital-0.1.0/.superpowers/sdd/task-10-brief.md +11 -0
  34. sac_capital-0.1.0/.superpowers/sdd/task-10-report.md +125 -0
  35. sac_capital-0.1.0/.superpowers/sdd/task-11-brief.md +11 -0
  36. sac_capital-0.1.0/.superpowers/sdd/task-11-report.md +290 -0
  37. sac_capital-0.1.0/.superpowers/sdd/task-12-brief.md +26 -0
  38. sac_capital-0.1.0/.superpowers/sdd/task-12-report.md +208 -0
  39. sac_capital-0.1.0/.superpowers/sdd/task-2-brief.md +81 -0
  40. sac_capital-0.1.0/.superpowers/sdd/task-2-report.md +58 -0
  41. sac_capital-0.1.0/.superpowers/sdd/task-3-brief.md +74 -0
  42. sac_capital-0.1.0/.superpowers/sdd/task-3-report.md +127 -0
  43. sac_capital-0.1.0/.superpowers/sdd/task-4-brief.md +188 -0
  44. sac_capital-0.1.0/.superpowers/sdd/task-4-report.md +110 -0
  45. sac_capital-0.1.0/.superpowers/sdd/task-5-brief.md +177 -0
  46. sac_capital-0.1.0/.superpowers/sdd/task-5-report.md +53 -0
  47. sac_capital-0.1.0/.superpowers/sdd/task-6-brief.md +53 -0
  48. sac_capital-0.1.0/.superpowers/sdd/task-6-report.md +78 -0
  49. sac_capital-0.1.0/.superpowers/sdd/task-7-brief.md +92 -0
  50. sac_capital-0.1.0/.superpowers/sdd/task-7-report.md +22 -0
  51. sac_capital-0.1.0/.superpowers/sdd/task-8-brief.md +20 -0
  52. sac_capital-0.1.0/.superpowers/sdd/task-8-report.md +78 -0
  53. sac_capital-0.1.0/.superpowers/sdd/task-9-brief.md +12 -0
  54. sac_capital-0.1.0/.superpowers/sdd/task-9-report.md +115 -0
  55. sac_capital-0.1.0/.uv-cache/.gitignore +1 -0
  56. sac_capital-0.1.0/.uv-cache/.lock +0 -0
  57. sac_capital-0.1.0/.uv-cache/CACHEDIR.TAG +1 -0
  58. sac_capital-0.1.0/.uv-cache/interpreter-v4/ac57ab614d7f56de/b2553c0dc7360588.msgpack +0 -0
  59. sac_capital-0.1.0/.uv-cache/sdists-v9/.gitignore +0 -0
  60. sac_capital-0.1.0/AGENTS.md +85 -0
  61. sac_capital-0.1.0/CLAUDE.md +71 -0
  62. sac_capital-0.1.0/HACKATHON.md +171 -0
  63. sac_capital-0.1.0/INSTALL.md +57 -0
  64. sac_capital-0.1.0/PKG-INFO +19 -0
  65. sac_capital-0.1.0/README.md +218 -0
  66. sac_capital-0.1.0/cli/__init__.py +0 -0
  67. sac_capital-0.1.0/cli/banner.py +20 -0
  68. sac_capital-0.1.0/cli/main.py +100 -0
  69. sac_capital-0.1.0/cli/setup.py +109 -0
  70. sac_capital-0.1.0/cli/workdir.py +19 -0
  71. sac_capital-0.1.0/conftest.py +5 -0
  72. sac_capital-0.1.0/core/__init__.py +0 -0
  73. sac_capital-0.1.0/core/alerts/__init__.py +0 -0
  74. sac_capital-0.1.0/core/alerts/telegram.py +206 -0
  75. sac_capital-0.1.0/core/assets/__init__.py +0 -0
  76. sac_capital-0.1.0/core/assets/bar.py +28 -0
  77. sac_capital-0.1.0/core/assets/instrument.py +18 -0
  78. sac_capital-0.1.0/core/bot/__init__.py +0 -0
  79. sac_capital-0.1.0/core/bot/command_handler.py +455 -0
  80. sac_capital-0.1.0/core/claude_client.py +392 -0
  81. sac_capital-0.1.0/core/config.py +71 -0
  82. sac_capital-0.1.0/core/execution/__init__.py +0 -0
  83. sac_capital-0.1.0/core/execution/base.py +22 -0
  84. sac_capital-0.1.0/core/execution/paper.py +70 -0
  85. sac_capital-0.1.0/core/ledger.py +221 -0
  86. sac_capital-0.1.0/core/markets.py +26 -0
  87. sac_capital-0.1.0/core/probability/__init__.py +0 -0
  88. sac_capital-0.1.0/core/probability/bayes.py +29 -0
  89. sac_capital-0.1.0/core/sizing/__init__.py +0 -0
  90. sac_capital-0.1.0/core/sizing/kelly.py +42 -0
  91. sac_capital-0.1.0/core/strategy.py +23 -0
  92. sac_capital-0.1.0/deploy/README.md +90 -0
  93. sac_capital-0.1.0/deploy/com.polymarketbot.commands.plist +38 -0
  94. sac_capital-0.1.0/deploy/com.polymarketbot.equities.mark.plist +37 -0
  95. sac_capital-0.1.0/deploy/com.polymarketbot.equities.scan.plist +56 -0
  96. sac_capital-0.1.0/deploy/com.polymarketbot.nightly.plist +41 -0
  97. sac_capital-0.1.0/deploy/com.polymarketbot.resolve.plist +42 -0
  98. sac_capital-0.1.0/deploy/com.polymarketbot.runner.plist +31 -0
  99. sac_capital-0.1.0/deploy/com.polymarketbot.situational-digest.plist +41 -0
  100. sac_capital-0.1.0/equities/__init__.py +0 -0
  101. sac_capital-0.1.0/equities/analysis/__init__.py +0 -0
  102. sac_capital-0.1.0/equities/analysis/agents.py +134 -0
  103. sac_capital-0.1.0/equities/analysis/analyst.py +1003 -0
  104. sac_capital-0.1.0/equities/analysis/budget.py +38 -0
  105. sac_capital-0.1.0/equities/analysis/checkpoint.py +133 -0
  106. sac_capital-0.1.0/equities/analysis/core_analyst.py +137 -0
  107. sac_capital-0.1.0/equities/analysis/core_reviewers.py +161 -0
  108. sac_capital-0.1.0/equities/analysis/prompt.py +355 -0
  109. sac_capital-0.1.0/equities/analysis/schema.py +258 -0
  110. sac_capital-0.1.0/equities/analysis/sentiment_agent.py +33 -0
  111. sac_capital-0.1.0/equities/eval/__init__.py +1 -0
  112. sac_capital-0.1.0/equities/eval/citation_attribution.py +51 -0
  113. sac_capital-0.1.0/equities/eval/replay.py +349 -0
  114. sac_capital-0.1.0/equities/eval/report.py +65 -0
  115. sac_capital-0.1.0/equities/eval/situational_digest.py +147 -0
  116. sac_capital-0.1.0/equities/execution/__init__.py +1 -0
  117. sac_capital-0.1.0/equities/execution/alpaca.py +290 -0
  118. sac_capital-0.1.0/equities/execution/reconciler.py +147 -0
  119. sac_capital-0.1.0/equities/fund.py +13 -0
  120. sac_capital-0.1.0/equities/improve/__init__.py +0 -0
  121. sac_capital-0.1.0/equities/improve/promoter.py +108 -0
  122. sac_capital-0.1.0/equities/improve/tournament.py +60 -0
  123. sac_capital-0.1.0/equities/improve/variants.py +55 -0
  124. sac_capital-0.1.0/equities/killgate/__init__.py +0 -0
  125. sac_capital-0.1.0/equities/killgate/cost_model.py +62 -0
  126. sac_capital-0.1.0/equities/killgate/gate.py +81 -0
  127. sac_capital-0.1.0/equities/killgate/thesis_health.py +99 -0
  128. sac_capital-0.1.0/equities/killgate/tracker.py +172 -0
  129. sac_capital-0.1.0/equities/ledger_equity.py +326 -0
  130. sac_capital-0.1.0/equities/paper.py +104 -0
  131. sac_capital-0.1.0/equities/pit.py +59 -0
  132. sac_capital-0.1.0/equities/research/__init__.py +1 -0
  133. sac_capital-0.1.0/equities/research/artifacts.py +183 -0
  134. sac_capital-0.1.0/equities/research/backtest.py +369 -0
  135. sac_capital-0.1.0/equities/research/discovery_lag.py +39 -0
  136. sac_capital-0.1.0/equities/research/lag_rules.py +211 -0
  137. sac_capital-0.1.0/equities/research/memory.py +241 -0
  138. sac_capital-0.1.0/equities/research/narrative_gap.py +50 -0
  139. sac_capital-0.1.0/equities/research/run_manifest.py +100 -0
  140. sac_capital-0.1.0/equities/research/store.py +32 -0
  141. sac_capital-0.1.0/equities/research/supply_chain.py +85 -0
  142. sac_capital-0.1.0/equities/research/thesis_miner.py +129 -0
  143. sac_capital-0.1.0/equities/risk/__init__.py +0 -0
  144. sac_capital-0.1.0/equities/risk/exits.py +47 -0
  145. sac_capital-0.1.0/equities/risk/kernel.py +195 -0
  146. sac_capital-0.1.0/equities/risk/sizing.py +49 -0
  147. sac_capital-0.1.0/equities/risk/state.py +35 -0
  148. sac_capital-0.1.0/equities/risk/vol_target.py +64 -0
  149. sac_capital-0.1.0/equities/screen/__init__.py +0 -0
  150. sac_capital-0.1.0/equities/screen/event_screen.py +205 -0
  151. sac_capital-0.1.0/equities/screen/inflection_screen.py +89 -0
  152. sac_capital-0.1.0/equities/screen/politician_screen.py +89 -0
  153. sac_capital-0.1.0/equities/screen/quality_screen.py +128 -0
  154. sac_capital-0.1.0/equities/screen/relative_strength.py +197 -0
  155. sac_capital-0.1.0/equities/screen/supply_chain_lag_screen.py +276 -0
  156. sac_capital-0.1.0/equities/screen/thematic_monitor.py +30 -0
  157. sac_capital-0.1.0/equities/signal_stats.py +110 -0
  158. sac_capital-0.1.0/equities/strategy.py +40 -0
  159. sac_capital-0.1.0/foundry.toml +16 -0
  160. sac_capital-0.1.0/hackathon/__init__.py +2 -0
  161. sac_capital-0.1.0/hackathon/verifiability.py +145 -0
  162. sac_capital-0.1.0/harness/__init__.py +0 -0
  163. sac_capital-0.1.0/harness/approval.py +76 -0
  164. sac_capital-0.1.0/harness/learn/__init__.py +0 -0
  165. sac_capital-0.1.0/harness/learn/calibration.py +39 -0
  166. sac_capital-0.1.0/harness/learn/retune.py +47 -0
  167. sac_capital-0.1.0/harness/learn/weather_bias.py +22 -0
  168. sac_capital-0.1.0/harness/nightly.py +129 -0
  169. sac_capital-0.1.0/harness/obsidian.py +45 -0
  170. sac_capital-0.1.0/harness/params.py +148 -0
  171. sac_capital-0.1.0/orchestrator/__init__.py +0 -0
  172. sac_capital-0.1.0/orchestrator/allocator.py +45 -0
  173. sac_capital-0.1.0/orchestrator/performance.py +62 -0
  174. sac_capital-0.1.0/orchestrator/reconcile.py +45 -0
  175. sac_capital-0.1.0/orchestrator/report.py +44 -0
  176. sac_capital-0.1.0/orchestrator/risk.py +78 -0
  177. sac_capital-0.1.0/pyproject.toml +51 -0
  178. sac_capital-0.1.0/runner_equities.py +1308 -0
  179. sac_capital-0.1.0/runner_research.py +175 -0
  180. sac_capital-0.1.0/scripts/backfill_static_research_artifacts.py +60 -0
  181. sac_capital-0.1.0/scripts/export_mantle_commitments.py +61 -0
  182. sac_capital-0.1.0/scripts/find_markets.js +22 -0
  183. sac_capital-0.1.0/scripts/generate_frontend_data.py +258 -0
  184. sac_capital-0.1.0/scripts/preflight.py +124 -0
  185. sac_capital-0.1.0/scripts/run_equities_service.py +72 -0
  186. sac_capital-0.1.0/scripts/run_nightly.py +40 -0
  187. sac_capital-0.1.0/scripts/run_situational_digest.py +58 -0
  188. sac_capital-0.1.0/scripts/sizing_ab_report.py +243 -0
  189. sac_capital-0.1.0/scripts/submit_mantle_decisions.py +224 -0
  190. sac_capital-0.1.0/scripts/test_senate_local.py +39 -0
  191. sac_capital-0.1.0/strategies/__init__.py +0 -0
  192. 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