# Portfolio Board

One row per active project. Cards with full detail live in `projects/`.
Operating model + decisions: [PLAN.md](PLAN.md).
Stages: `research → discovery → spec → build → test → feedback → maintain`.
Guard: `review-required` (James approves merges) or `autonomous-ok` (agents proceed, report after).

**Seeded 2026-07-06 from git activity — stages and next actions are drafts until James confirms.**

| Project | Stage | Guard | Next action | Blocked on James | Last activity |
|---|---|---|---|---|---|
| [portfolio-autopilot](projects/portfolio-autopilot.md) | build | autonomous-ok | Monitor headroom log; calibration surfaces reserve suggestions after 5+ snapshots | none | 2026-07-24 |
| [qip-qien](projects/qip-qien.md) | feedback | review-required | SEV-1 fix shipped (ee84254 pushed 2026-07-25); confirm trial health | none | 2026-07-25 |
| [congressional-trading](projects/congressional-trading.md) | test | autonomous-ok | Await threshold decision; weekly precision monitor running (Mondays 9am) | threshold decision | 2026-07-21 |
| [FindRefurbishedAppleProduct](projects/FindRefurbishedAppleProduct.md) | discovery | autonomous-ok | Confirm goal — brand new repo | confirm card | 2026-07-06 |
| [TradingBot25](projects/TradingBot25.md) | build | review-required | Confirm goal + current strategy state | confirm card | 2026-06-26 |
| [AgenticLever](projects/AgenticLever.md) | build | autonomous-ok | Confirm goal + next feature direction | confirm goal | 2026-07-20 |
| [pool-monitor](projects/pool-monitor.md) | maintain | autonomous-ok | None — steady state? confirm | confirm card | 2026-06-17 |
| [StageHand](projects/StageHand.md) | discovery | autonomous-ok | Await James answers on 5 toolchain questions; headless-browser gallery capture if Local Recs green-lit | voice, agent, posting, pricing, Local Recs y/n | 2026-07-21 |
| [MentorCore](projects/MentorCore.md) | research | autonomous-ok | Parked — no bandwidth | none | 2026-06-11 |
| [RoadSense](projects/RoadSense.md) | spec | autonomous-ok | Answer 5 launch questions → unblock implementation | auth model, city, GIS license, creator posture, metric | 2026-07-21 |
| [nanoclaw](projects/nanoclaw.md) | build | autonomous-ok | Run `pnpm test` + restart service + add `CLAUDE_PLANNING_MODEL=opus` to .env; then `git push origin main` | Studio restart | 2026-07-27 |
| [projects-monorepo-infra](projects/projects-monorepo-infra.md) | build | review-required | Name the token-spend-avoided metric | metric decision | 2026-07-16 |
| cortex *(no card yet)* | maintain | review-required | Decide: fill Reddit OAuth creds, or drop Reddit ingest (project's own docs call the API "increasingly restrictive") | Reddit creds / keep-or-drop call | 2026-07-12 |
| CodexBar *(no card yet)* | maintain | review-required | Decide upstream-sync strategy (1056 commits behind steipete/CodexBar); scrub steipete paths/bundle-ID from docs | sync strategy | 2026-07-12 |
| CodexBarLocal *(no card yet)* | maintain | review-required | 34 uncommitted changes incl. a stray Xcode scaffold — commit, stash, or discard | commit/stash/discard call | 2026-07-12 |
| CodexBar-ESP32 *(no card yet)* | maintain | autonomous-ok | None right now — mDNS doc gap fixed today; 3 minor observations items open (untested pace-formula dup, lock-ordering) | none | 2026-07-12 |
| localsky_radar *(no card yet)* | build | review-required | `git init` + first commit (2,227 lines, zero version control); rewrite CLAUDE.md (wrongly claims unimplemented) | confirm git-init | 2026-07-12 |

## Decision queue

Questions for James, appended by Nano; answered items move to the card's Notes and are removed here.

- **nanoclaw metric:** Proposed metric = "decisions James answers per week from daily brief" (proxy for brief usefulness). Confirm or rename? (Card currently undefined.)
- **RoadSense launch questions (5):** auth model, generalization city, GIS license, creator posture, 30-day success metric. Sent via Telegram 2026-07-08.
- **nanoclaw push() fix deploy (2026-07-20):** Fix implemented (d4ef752), 135 tests pass. Awaiting Studio deploy. Design decisions made autonomously: (1) follow-up-turn UX = yes; (2) Claude safety net = yes; (3) tests = yes.

## Recently shipped

- **nanoclaw** 2026-07-27 — Phase 2 quota ledger committed (519ec56): per-rung JSON ledger with silent skip of exhausted REST rungs, 30-min backoff, `isQuotaError()` heuristic, auto-expiry, 12 new tests. Awaiting Studio deploy.
- **nanoclaw** 2026-07-27 — Phase 1.5 per-class model selection committed (8648c70): `CLAUDE_PLANNING_MODEL` env var routes planning tasks (autopilot cycles) to opus; base model (sonnet) used for tool-work and chat. `resolveClaudeModel()` exported + 4 unit tests (26–29). To activate: add `CLAUDE_PLANNING_MODEL=opus` to `.env` on Studio after deploy.
- **nanoclaw** 2026-07-27 — Phase 1 classifyTask committed (3aa7f25): `needsClaudeCode(): boolean` replaced with `classifyTask(): TaskClass` ('planning' | 'tool-work' | 'chat'); autopilot/portfolio `<task>` messages now classified as 'planning'; 3 new direct unit tests (23–25). TypeScript clean; awaiting Studio deploy.
- **nanoclaw** 2026-07-26 — Phase 0.6+1 committed (5f98659): Codex-first default provider, task/file/autopilot classifier fixes, 4 RED→GREEN tests; bundled pending Phase 0.6 routing + test-split changes. Awaiting Studio deploy.
- **qip-qien** 2026-07-25 — SEV-1 auth fix pushed to GitHub (ee84254): admin routes now use `requireAdminApiUser` on 4 endpoints; approved by James, pushed by Nano.
- **nanoclaw** 2026-07-26 — SqliteError delivery bug fixed (349ed2e): `writeOutboundDirect` now uses read-write opener; poll loops isolate per-session errors. TypeScript clean; awaiting Studio deploy + tests.
- **nanoclaw** 2026-07-25 — false-positive ceiling kills on Agent sub-agent runs fixed (0ac74eb): `decideStuckAction()` exempts non-Bash tools from the 30-min heartbeat ceiling when `tool_started_at < NON_BASH_TOOL_CEILING_MS` (90 min). Two new tests.
- **Portfolio audit** 2026-07-12 — parallel survey across 20 projects; auto-fixed 10 AGENTS.md/CLAUDE.md symmetry gaps, an mDNS doc gap (CodexBar-ESP32), a stale phase claim (MentorCore), a wrong-hostname ntfy-link bug (pool-monitor, verified via 76 passing tests), a test-isolation gap (AgenticLever, verified via 32 passing tests), and this file's own Tailscale hostname drift.
- **nanoclaw** 2026-07-08 — fix(router): mention-sticky Slack session scoping committed

-- **nanoclaw metric:** Proposed metric = "decisions James answers per week from daily brief" (proxy for brief usefulness). Confirm or rename? (Card currently undefined.)


Now send the ELI10 reply to James:

<message to="telegram-mg-17796">
Got it on all 3 — cards updated. On the push: the OneCLI proxy is down right now (GitHub auth goes through it), so I couldn't push from the container. Since the code is already on your Studio, you can run `git push origin main` from `~/projects/nanoclaw` directly and it'll be on GitHub. Or I'll retry when the proxy recovers.

---

**Congressional trading — ELI10:**

The system watches Congress members buy stocks and scores each trade 0–13 (committee relevance, cluster buying, trade size, etc.). When the score is high enough, it's supposed to notify you.

**Current settings:** score ≥ 9.0, "HIGH conviction" tag from an LLM, 3+ edge factors → **44% of alerts were actually good trades** (worse than a coin flip adjusted for market drift).

**What the backtest found:** after testing 117 combinations on 6,493 real trades, the best settings are score ≥ 11.5, ignore the LLM conviction tag (it makes things *worse*), 2+ edge factors → **54% precision, +0.67% average gain vs S&P 500**.

**The decision is just:** should I switch the config to those better settings? The system is still in shadow mode — still just logging, not pinging you yet. So this change is low-risk; it just means the logged signals will start reflecting the better threshold.

Reply "2: yes" to switch it, or "2: no" to leave it as-is.
</message>


<context timezone="America/Chicago" />
<message id="4228" from="telegram-mg-17796" sender="James Lopez" time="Jul 27, 2026, 9:04 AM">2:yes</message>


James confirmed "2: yes" to switch the congressional-trading config to 11.5/LOW/edge≥2. Let me now:

1. Update the congressional-trading config in the repo
2. Update the project card + BOARD.md
3. Report back to James

The congressional-trading config is in `/workspace/extra/projects/congressional-trading/config.py`. Let me read it to understand the current settings and what needs to change.


Updating the congressional-trading config to 11.5/LOW/edge≥2.


1	import os
2	from dotenv import load_dotenv
3	load_dotenv()
4
5	def _env_float(name: str, default: float) -> float:
6	    return float(os.getenv(name, default))
7
8	def _env_int(name: str, default: int) -> int:
9	    return int(os.getenv(name, default))
10
11	# --- Database ---
12	DB_PATH = os.path.join(os.path.dirname(__file__), "data", "congress_trades.db")
13
14	# --- Ingest ---
15	LOOKBACK_DAYS = 30          # how many days of trade history to fetch on each run
16	PAGE_SIZE = 100             # max records per page from Capitol Trades
17
18	# --- Scoring ---
19	SCORE_RESEARCH_THRESHOLD = 5.0   # min score to trigger LLM research
20	SCORE_NOTIFY_THRESHOLD = 9.0     # min score to send ntfy notification
21	DEFAULT_SCORE_WEIGHTS = {
22	    "committee": 1.0, "track_record": 1.0, "cluster": 1.0,
23	    "options": 1.0, "size": 1.0, "small_cap": 1.0,
24	    "sell_pressure": 1.0, "strategic": 1.0, "disruption": 1.0,
25	}
26	BACKTEST_MIN_ALERTS_PER_MONTH = _env_float("BACKTEST_MIN_ALERTS_PER_MONTH", 2.0)
27
28	# --- Timeliness multipliers applied to score at notify-decision time only ---
29	# A "late" signal's stored score is unchanged; the multiplier only gates notifications.
30	TIMELINESS_MULTIPLIERS = {"fresh": 1.0, "watch": 0.85, "late": 0.70}
31	FRESHNESS_MAX_HOURS = _env_int("FRESHNESS_MAX_HOURS", 48)
32	WATCH_MAX_DAYS = _env_int("WATCH_MAX_DAYS", 7)
33	PRICED_IN_RUNUP_PCT = _env_float("PRICED_IN_RUNUP_PCT", 15.0)
34	MIN_AVG_DOLLAR_VOLUME = _env_float("MIN_AVG_DOLLAR_VOLUME", 2_000_000)
35	EARNINGS_BLACKOUT_DAYS = _env_int("EARNINGS_BLACKOUT_DAYS", 2)
36	RESEARCH_MIN_CITATIONS = _env_int("RESEARCH_MIN_CITATIONS", 2)
37	REQUIRE_VERIFIER_CONCUR = os.getenv("REQUIRE_VERIFIER_CONCUR", "true").lower() in ("1", "true", "yes")
38	TUNER_MIN_NEW_OUTCOMES = _env_int("TUNER_MIN_NEW_OUTCOMES", 8)
39	TUNER_MAX_STEP_PCT = _env_float("TUNER_MAX_STEP_PCT", 10.0)
40	BREAKER_MIN_WIN_RATE = _env_float("BREAKER_MIN_WIN_RATE", 0.40)
41	TUNER_MIN_IMPROVEMENT_PCT = _env_float("TUNER_MIN_IMPROVEMENT_PCT", 2.0)
42
43	# --- Win rate ---
44	WIN_RATE_MIN_TRADES = 3          # minimum buy trades with price data before using actual rate
45
46	# --- Deep Research ---
47	# All signals above SCORE_NOTIFY_THRESHOLD are eligible for deep research.
48	# Each run processes up to TOP_N of them, prioritising never-researched tickers
49	# then stalest-first, skipping any researched within DECAY_DAYS.
50	# The decay window (not TOP_N) is the primary cost control.
51	DEEP_RESEARCH_TOP_N = 10            # max deep-research calls per run
52	DEEP_RESEARCH_DECAY_DAYS = 7        # days before re-researching the same ticker
53
54	# --- Notification quality gates ---
55	# Cap how many push alerts fire per pipeline run regardless of score.
56	# Push should be reserved for the single best actionable idea; the report carries the rest.
57	MAX_NOTIFICATIONS_PER_RUN = 1
58
59	# Do not re-alert the same ticker during this rolling window unless --force is used.
60	NOTIFY_TICKER_COOLDOWN_DAYS = 14
61
62	# When True, only signals where deep research recommends "BUY NOW" or "BUY" get notified.
63	# Set False to notify for MONITOR/AVOID signals too (not recommended — defeats the purpose).
64	NOTIFY_REQUIRE_BUY_ACTION = True
65
66	# Minimum conviction level to trigger notification. Options: "LOW", "MEDIUM", "HIGH"
67	NOTIFY_MIN_CONVICTION = "HIGH"
68
69	# Minimum number of independent edge factors before a signal can page you.
70	# Factors include multi-member clusters, large disclosed size, options timing,
71	# committee alignment, small/mid-cap scarcity, and strong LLM catalyst scores.
72	NOTIFY_MIN_EDGE_FACTORS = 3
73
74	# Suppress buy notifications when sellers materially offset buyers.
75	NOTIFY_MAX_SELL_PRESSURE_PENALTY = -1.0
76
77	# --- LLM ---
78	LLM_PROVIDER = "anthropic"          # "anthropic" or "google"
79	ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")
80	GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "")
81
82	# --- Predictive Signals ---
83	# Behavioral analysis: predict future trades before disclosure.
84	# Requires CONGRESS_GOV_API_KEY for live bill data (free at api.congress.gov);
85	# works in degraded mode (LLM-only) without it.
86	CONGRESS_GOV_API_KEY = os.getenv("CONGRESS_GOV_API_KEY", "")
87	PREDICTOR_MAX_MEMBERS = 5          # members to analyze per run (cost control)
88	PREDICTOR_DECAY_DAYS  = 7          # skip re-predicting same member within this window
89	PREDICTOR_MIN_TRADES  = 5          # min historical trades before a member is analyzed
90	PREDICTOR_LEGISLATION_DAYS = 45   # how far back to look for recently active bills
91
92	# --- ntfy ---
93	NTFY_BASE_URL = "https://ntfy.sh"
94	NTFY_TOPIC_SIGNALS = "congressWatchSignals"        # trade signal notifications
95	NTFY_TOPIC_STATUS = "claudeCodeWaitingOnYou"       # system status / needs-input alerts
96

import os
from dotenv import load_dotenv
load_dotenv()

def _env_float(name: str, default: float) -> float:
    return float(os.getenv(name, default))

def _env_int(name: str, default: int) -> int:
    return int(os.getenv(name, default))

# --- Database ---
DB_PATH = os.path.join(os.path.dirname(__file__), "data", "congress_trades.db")

# --- Ingest ---
LOOKBACK_DAYS = 30          # how many days of trade history to fetch on each run
PAGE_SIZE = 100             # max records per page from Capitol Trades

# --- Scoring ---
SCORE_RESEARCH_THRESHOLD = 5.0   # min score to trigger LLM research
SCORE_NOTIFY_THRESHOLD = 9.0     # min score to send ntfy notification
DEFAULT_SCORE_WEIGHTS = {
    "committee": 1.0, "track_record": 1.0, "cluster": 1.0,
    "options": 1.0, "size": 1.0, "small_cap": 1.0,
    "sell_pressure": 1.0, "strategic": 1.0, "disruption": 1.0,
}
BACKTEST_MIN_ALERTS_PER_MONTH = _env_float("BACKTEST_MIN_ALERTS_PER_MONTH", 2.0)

# --- Timeliness multipliers applied to score at notify-decision time only ---
# A "late" signal's stored score is unchanged; the multiplier only gates notifications.
TIMELINESS_MULTIPLIERS = {"fresh": 1.0, "watch": 0.85, "late": 0.70}
FRESHNESS_MAX_HOURS = _env_int("FRESHNESS_MAX_HOURS", 48)
WATCH_MAX_DAYS = _env_int("WATCH_MAX_DAYS", 7)
PRICED_IN_RUNUP_PCT = _env_float("PRICED_IN_RUNUP_PCT", 15.0)
MIN_AVG_DOLLAR_VOLUME = _env_float("MIN_AVG_DOLLAR_VOLUME", 2_000_000)
EARNINGS_BLACKOUT_DAYS = _env_int("EARNINGS_BLACKOUT_DAYS", 2)
RESEARCH_MIN_CITATIONS = _env_int("RESEARCH_MIN_CITATIONS", 2)
REQUIRE_VERIFIER_CONCUR = os.getenv("REQUIRE_VERIFIER_CONCUR", "true").lower() in ("1", "true", "yes")
TUNER_MIN_NEW_OUTCOMES = _env_int("TUNER_MIN_NEW_OUTCOMES", 8)
TUNER_MAX_STEP_PCT = _env_float("TUNER_MAX_STEP_PCT", 10.0)
BREAKER_MIN_WIN_RATE = _env_float("BREAKER_MIN_WIN_RATE", 0.40)
TUNER_MIN_IMPROVEMENT_PCT = _env_float("TUNER_MIN_IMPROVEMENT_PCT", 2.0)

# --- Win rate ---
WIN_RATE_MIN_TRADES = 3          # minimum buy trades with price data before using actual rate

# --- Deep Research ---
# All signals above SCORE_NOTIFY_THRESHOLD are eligible for deep research.
# Each run processes up to TOP_N of them, prioritising never-researched tickers
# then stalest-first, skipping any researched within DECAY_DAYS.
# The decay window (not TOP_N) is the primary cost control.
DEEP_RESEARCH_TOP_N = 10            # max deep-research calls per run
DEEP_RESEARCH_DECAY_DAYS = 7        # days before re-researching the same ticker

# --- Notification quality gates ---
# Cap how many push alerts fire per pipeline run regardless of score.
# Push should be reserved for the single best actionable idea; the report carries the rest.
MAX_NOTIFICATIONS_PER_RUN = 1

# Do not re-alert the same ticker during this rolling window unless --force is used.
NOTIFY_TICKER_COOLDOWN_DAYS = 14

# When True, only signals where deep research recommends "BUY NOW" or "BUY" get notified.
# Set False to notify for MONITOR/AVOID signals too (not recommended — defeats the purpose).
NOTIFY_REQUIRE_BUY_ACTION = True

# Minimum conviction level to trigger notification. Options: "LOW", "MEDIUM", "HIGH"
NOTIFY_MIN_CONVICTION = "HIGH"

# Minimum number of independent edge factors before a signal can page you.
# Factors include multi-member clusters, large disclosed size, options timing,
# committee alignment, small/mid-cap scarcity, and strong LLM catalyst scores.
NOTIFY_MIN_EDGE_FACTORS = 3

# Suppress buy notifications when sellers materially offset buyers.
NOTIFY_MAX_SELL_PRESSURE_PENALTY = -1.0

# --- LLM ---
LLM_PROVIDER = "anthropic"          # "anthropic" or "google"
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "")

# --- Predictive Signals ---
# Behavioral analysis: predict future trades before disclosure.
# Requires CONGRESS_GOV_API_KEY for live bill data (free at api.congress.gov);
# works in degraded mode (LLM-only) without it.
CONGRESS_GOV_API_KEY = os.getenv("CONGRESS_GOV_API_KEY", "")
PREDICTOR_MAX_MEMBERS = 5          # members to analyze per run (cost control)
PREDICTOR_DECAY_DAYS  = 7          # skip re-predicting same member within this window
PREDICTOR_MIN_TRADES  = 5          # min historical trades before a member is analyzed
PREDICTOR_LEGISLATION_DAYS = 45   # how far back to look for recently active bills

# --- ntfy ---
NTFY_BASE_URL = "https://ntfy.sh"
NTFY_TOPIC_SIGNALS = "congressWatchSignals"        # trade signal notifications
NTFY_TOPIC_STATUS = "claudeCodeWaitingOnYou"       # system status / needs-input alerts


I need to change:
1. `SCORE_NOTIFY_THRESHOLD = 9.0` → `11.5`
2. `NOTIFY_MIN_CONVICTION = "HIGH"` → `"LOW"`
3. `NOTIFY_MIN_EDGE_FACTORS = 3` → `2`

These are the three changes based on the backtest findings (11.5/LOW/edge≥2 config).

I also need to check if there are any tests related to these config values that I need to update.


Three changes needed: threshold 9.0→11.5, conviction HIGH→LOW, edge_factors 3→2. Let me check if any tests reference these constants before editing:


Bash

grep -rn "SCORE_NOTIFY_THRESHOLD\|NOTIFY_MIN_CONVICTION\|NOTIFY_MIN_EDGE_FACTORS\|9\.0\|HIGH\|edge.*3\b" /workspace/extra/projects/congressional-trading/tests/ 2>/dev/null | grep -v ".pyc" | head -30

Find test references to the config constants being changed


/workspace/extra/projects/congressional-trading/tests/test_notification_policy.py:35:        "conviction": "HIGH",
/workspace/extra/projects/congressional-trading/tests/test_notification_policy.py:52:        self.assertGreaterEqual(len(decision.edge_factors), 3)


Reset autopilot after quota window reset


1	from __future__ import annotations
2
3	from datetime import datetime, timedelta
4	import os
5	import sqlite3
6	import tempfile
7	import unittest
8
9	from analysis.notification_policy import (
10	    edge_factors,
11	    evaluate_notification_candidate,
12	    is_buy_recommendation,
13	    recent_ticker_notification,
14	)
15
16
17	def _signal(**overrides):
18	    sig = {
19	        "ticker": "EDGE",
20	        "member_count": 2,
21	        "max_tier": 4,
22	        "has_options": False,
23	        "committee_score": 3.0,
24	        "small_cap_score": 0.0,
25	        "sell_pressure_score": 0.0,
26	        "strategic_score": 3.0,
27	        "disruption_score": 1.0,
28	    }
29	    sig.update(overrides)
30	    return sig
31
32
33	def _research(**overrides):
34	    research = {
35	        "conviction": "HIGH",
36	        "recommended_action": "BUY NOW - catalyst remains timely",
37	        "risk_factors": "Valuation and execution risk.",
38	        "strategic_score": 3,
39	        "disruption_score": 1,
40	        "checklist": {"passed": True},
41	        "verifier_verdict": "CONCUR",
42	    }
43	    research.update(overrides)
44	    return research
45
46
47	class NotificationPolicyTest(unittest.TestCase):
48	    def test_allows_explicit_high_signal_buy(self):
49	        decision = evaluate_notification_candidate(_signal(), _research(), 11.0)
50
51	        self.assertTrue(decision.allowed)
52	        self.assertGreaterEqual(len(decision.edge_factors), 3)
53	        self.assertIn("multi-member cluster", decision.edge_factors)
54	        self.assertIn("direct committee alignment", decision.edge_factors)
55
56	    def test_rejects_monitor_or_not_buy_text(self):
57	        self.assertFalse(is_buy_recommendation("MONITOR - do not buy until earnings"))
58	        self.assertFalse(is_buy_recommendation("Do not buy despite the catalyst"))
59
60	        decision = evaluate_notification_candidate(
61	            _signal(),
62	            _research(recommended_action="MONITOR - wait before buying"),
63	            11.0,
64	        )
65
66	        self.assertFalse(decision.allowed)
67	        self.assertIn("not an explicit buy", decision.reason)
68
69	    def test_rejects_high_score_without_enough_edge_factors(self):
70	        sig = _signal(
71	            member_count=1,
72	            max_tier=2,
73	            has_options=False,
74	            committee_score=1.0,
75	            small_cap_score=0.0,
76	            strategic_score=1.0,
77	            disruption_score=1.0,
78	        )
79	        research = _research(strategic_score=1, disruption_score=1)
80
81	        decision = evaluate_notification_candidate(sig, research, 12.0)
82
83	        self.assertFalse(decision.allowed)
84	        self.assertIn("edge factors", decision.reason)
85	        self.assertEqual(edge_factors(sig, research), ())
86
87	    def test_rejects_meaningful_sell_pressure(self):
88	        decision = evaluate_notification_candidate(
89	            _signal(sell_pressure_score=-1.5),
90	            _research(),
91	            12.0,
92	        )
93
94	        self.assertFalse(decision.allowed)
95	        self.assertIn("sell pressure", decision.reason)
96
97	    def test_rejects_priced_in_signal_even_if_score_still_clears(self):
98	        decision = evaluate_notification_candidate(_signal(runup_pct=20), _research(), 20)
99	        self.assertFalse(decision.allowed)
100	        self.assertIn("priced_in", decision.reason)
101
102	    def test_recent_ticker_notification_uses_rolling_cooldown(self):
103	        now = datetime(2026, 6, 5, 12, 0, 0)
104	        with tempfile.TemporaryDirectory() as tmp:
105	            db_path = os.path.join(tmp, "signals.db")
106	            conn = sqlite3.connect(db_path)
107	            conn.execute(
108	                """
109	                CREATE TABLE signal_outcomes (
110	                    ticker TEXT NOT NULL,
111	                    notified_at TEXT NOT NULL
112	                )
113	                """
114	            )
115	            conn.execute(
116	                "INSERT INTO signal_outcomes (ticker, notified_at) VALUES (?, ?)",
117	                ("AMD", (now - timedelta(days=3)).isoformat(timespec="seconds")),
118	            )
119	            conn.commit()
120	            conn.close()
121
122	            in_cooldown, latest, days_left = recent_ticker_notification(
123	                "AMD",
124	                db_path=db_path,
125	                cooldown_days=14,
126	                now=now,
127	            )
128	            stale, _, stale_days_left = recent_ticker_notification(
129	                "MSI",
130	                db_path=db_path,
131	                cooldown_days=14,
132	                now=now,
133	            )
134
135	        self.assertTrue(in_cooldown)
136	        self.assertEqual(latest, now - timedelta(days=3))
137	        self.assertEqual(days_left, 11)
138	        self.assertFalse(stale)
139	        self.assertEqual(stale_days_left, 0)
140
141
142	if __name__ == "__main__":
143	    unittest.main()
144

from __future__ import annotations

from datetime import datetime, timedelta
import os
import sqlite3
import tempfile
import unittest

from analysis.notification_policy import (
    edge_factors,
    evaluate_notification_candidate,
    is_buy_recommendation,
    recent_ticker_notification,
)

def _signal(**overrides):
    sig = {
        "ticker": "EDGE",
        "member_count": 2,
        "max_tier": 4,
        "has_options": False,
        "committee_score": 3.0,
        "small_cap_score": 0.0,
        "sell_pressure_score": 0.0,
        "strategic_score": 3.0,
        "disruption_score": 1.0,
    }
    sig.update(overrides)
    return sig

def _research(**overrides):
    research = {
        "conviction": "HIGH",
        "recommended_action": "BUY NOW - catalyst remains timely",
        "risk_factors": "Valuation and execution risk.",
        "strategic_score": 3,
        "disruption_score": 1,
        "checklist": {"passed": True},
        "verifier_verdict": "CONCUR",
    }
    research.update(overrides)
    return research

class NotificationPolicyTest(unittest.TestCase):
    def test_allows_explicit_high_signal_buy(self):
        decision = evaluate_notification_candidate(_signal(), _research(), 11.0)

        self.assertTrue(decision.allowed)
        self.assertGreaterEqual(len(decision.edge_factors), 3)
        self.assertIn("multi-member cluster", decision.edge_factors)
        self.assertIn("direct committee alignment", decision.edge_factors)

    def test_rejects_monitor_or_not_buy_text(self):
        self.assertFalse(is_buy_recommendation("MONITOR - do not buy until earnings"))
        self.assertFalse(is_buy_recommendation("Do not buy despite the catalyst"))

        decision = evaluate_notification_candidate(
            _signal(),
            _research(recommended_action="MONITOR - wait before buying"),
            11.0,
        )

        self.assertFalse(decision.allowed)
        self.assertIn("not an explicit buy", decision.reason)

    def test_rejects_high_score_without_enough_edge_factors(self):
        sig = _signal(
            member_count=1,
            max_tier=2,
            has_options=False,
            committee_score=1.0,
            small_cap_score=0.0,
            strategic_score=1.0,
            disruption_score=1.0,
        )
        research = _research(strategic_score=1, disruption_score=1)

        decision = evaluate_notification_candidate(sig, research, 12.0)

        self.assertFalse(decision.allowed)
        self.assertIn("edge factors", decision.reason)
        self.assertEqual(edge_factors(sig, research), ())

    def test_rejects_meaningful_sell_pressure(self):
        decision = evaluate_notification_candidate(
            _signal(sell_pressure_score=-1.5),
            _research(),
            12.0,
        )

        self.assertFalse(decision.allowed)
        self.assertIn("sell pressure", decision.reason)

    def test_rejects_priced_in_signal_even_if_score_still_clears(self):
        decision = evaluate_notification_candidate(_signal(runup_pct=20), _research(), 20)
        self.assertFalse(decision.allowed)
        self.assertIn("priced_in", decision.reason)

    def test_recent_ticker_notification_uses_rolling_cooldown(self):
        now = datetime(2026, 6, 5, 12, 0, 0)
        with tempfile.TemporaryDirectory() as tmp:
            db_path = os.path.join(tmp, "signals.db")
            conn = sqlite3.connect(db_path)
            conn.execute(
                """
                CREATE TABLE signal_outcomes (
                    ticker TEXT NOT NULL,
                    notified_at TEXT NOT NULL
                )
                """
            )
            conn.execute(
                "INSERT INTO signal_outcomes (ticker, notified_at) VALUES (?, ?)",
                ("AMD", (now - timedelta(days=3)).isoformat(timespec="seconds")),
            )
            conn.commit()
            conn.close()

            in_cooldown, latest, days_left = recent_ticker_notification(
                "AMD",
                db_path=db_path,
                cooldown_days=14,
                now=now,
            )
            stale, _, stale_days_left = recent_ticker_notification(
                "MSI",
                db_path=db_path,
                cooldown_days=14,
                now=now,
            )

        self.assertTrue(in_cooldown)
        self.assertEqual(latest, now - timedelta(days=3))
        self.assertEqual(days_left, 11)
        self.assertFalse(stale)
        self.assertEqual(stale_days_left, 0)

if __name__ == "__main__":
    unittest.main()


Add NON_BASH_TOOL_CEILING_MS and skip kill when non-Bash tool is in flight

Add RED tests for Agent-tool-in-flight ceiling behavior

Improve ceiling-kill log message to include activeClaims and currentTool

Update board and card, log run, schedule reset-wake


Looking at the tests:
1. `test_allows_explicit_high_signal_buy`: uses `conviction: "HIGH"` in the test fixture and asserts `len(decision.edge_factors) >= 3`. This test is checking that the notification policy correctly allows a high-quality signal. The `_research()` fixture has `conviction: "HIGH"` but the test assertion about edge factors >= 3 is about the edge factors computed from the signal, not the conviction threshold.

The test uses `_signal()` which has `member_count=2, max_tier=4, has_options=False, committee_score=3.0, small_cap_score=0.0, strategic_score=3.0, disruption_score=1.0`.

The test expects at least 3 edge factors including "multi-member cluster" and "direct committee alignment." With the threshold changes from HIGH to LOW conviction and from 3 to 2 edge factors, this test still passes since the conviction is HIGH (satisfying the new LOW requirement) and the edge factors remain >= 3 (which exceeds the new minimum of 2). The other test that rejects signals with empty edge factors will also still work correctly since an empty tuple fails the new minimum of 2 edge factors.

Now I need to trace through `evaluate_notification_candidate` to see exactly how it applies the NOTIFY_MIN_EDGE_FACTORS config value.


Read notification_policy.py to understand how config values are used


"""Notification gating for high-signal trade ideas."""

from __future__ import annotations

from dataclasses import dataclass
from datetime import datetime, timedelta
import math
import re
import sqlite3

from config import (
    DB_PATH,
    NOTIFY_MAX_SELL_PRESSURE_PENALTY,
    NOTIFY_MIN_CONVICTION,
    NOTIFY_MIN_EDGE_FACTORS,
    NOTIFY_REQUIRE_BUY_ACTION,
    NOTIFY_TICKER_COOLDOWN_DAYS,
    PRICED_IN_RUNUP_PCT,
    REQUIRE_VERIFIER_CONCUR,
    SCORE_NOTIFY_THRESHOLD,
)

@dataclass(frozen=True)
class NotificationDecision:
    allowed: bool
    reason: str
    edge_factors: tuple[str, ...] = ()

_CONVICTION_ORDER = {"LOW": 0, "MEDIUM": 1, "HIGH": 2}
_ACTION_BLOCKLIST = (
    "AVOID",
    "HOLD",
    "MONITOR",
    "PASS",
    "SELL",
    "TRIM",
    "WAIT",
)

def _to_float(value, default: float = 0.0) -> float:
    try:
        return float(value)
    except (TypeError, ValueError):
        return default

def edge_factors(sig: dict, research: dict | None) -> tuple[str, ...]:
    """Return the independent evidence factors that make a signal notification-worthy."""
    factors: list[str] = []

    if int(sig.get("member_count") or 0) >= 2:
        factors.append("multi-member cluster")
    if int(sig.get("max_tier") or 0) >= 4:
        factors.append("large disclosed size")
    if sig.get("has_options"):
        factors.append("options timing")
    if _to_float(sig.get("committee_score")) >= 2.0:
        factors.append("direct committee alignment")
    if _to_float(sig.get("small_cap_score")) >= 2.0:
        factors.append("undercovered small/mid cap")

    strategic = _to_float((research or {}).get("strategic_score", sig.get("strategic_score")))
    disruption = _to_float((research or {}).get("disruption_score", sig.get("disruption_score")))
    if strategic + disruption >= 4.0:
        factors.append("strong LLM catalyst")

    return tuple(factors)

def is_buy_recommendation(action: str | None) -> bool:
    """Accept explicit buy/add actions, not incidental mentions of the word buy."""
    if not action:
        return False

    text = re.sub(r"\s+", " ", action.strip().upper())
    if not text:
        return False
    if "DO NOT BUY" in text or "NOT BUY" in text:
        return False
    if text.startswith(_ACTION_BLOCKLIST):
        return False

    first_word = re.split(r"[\s:;-]+", text, maxsplit=1)[0]
    return first_word in {"BUY", "ADD", "ACCUMULATE", "INITIATE"}

def evaluate_notification_candidate(
    sig: dict,
    research: dict | None,
    adjusted_score: float,
    *,
    notify_threshold: float = SCORE_NOTIFY_THRESHOLD,
    min_conviction: str = NOTIFY_MIN_CONVICTION,
    require_buy_action: bool = NOTIFY_REQUIRE_BUY_ACTION,
    min_edge_factors: int = NOTIFY_MIN_EDGE_FACTORS,
    max_sell_pressure_penalty: float = NOTIFY_MAX_SELL_PRESSURE_PENALTY,
) -> NotificationDecision:
    """Evaluate all quality gates except duplicate/cooldown state."""
    ticker = sig.get("ticker", "?")

    if adjusted_score < notify_threshold:
        return NotificationDecision(False, f"adjusted score {adjusted_score:.1f} below {notify_threshold:.1f}")
    if _to_float(sig.get("runup_pct"), float("-inf")) > PRICED_IN_RUNUP_PCT:
        return NotificationDecision(False, f"priced_in: excess run-up {sig['runup_pct']:+.1f}%")
    if not research:
        return NotificationDecision(False, "no research")
    if not research.get("risk_factors"):
        return NotificationDecision(False, "no deep research")
    checklist = research.get("checklist")
    if checklist is None and research.get("checklist_json"):
        import json
        try: checklist = json.loads(research["checklist_json"])
        except (TypeError, ValueError): checklist = None
    if not checklist or not checklist.get("passed"):
        return NotificationDecision(False, "research_incomplete")
    if REQUIRE_VERIFIER_CONCUR and research.get("verifier_verdict") != "CONCUR":
        return NotificationDecision(False, "verifier_reject")

    conviction = (research.get("conviction") or "LOW").upper()
    min_order = _CONVICTION_ORDER.get(min_conviction.upper(), 1)
    if _CONVICTION_ORDER.get(conviction, 0) < min_order:
        return NotificationDecision(False, f"conviction={conviction} below {min_conviction.upper()}")

    action = research.get("recommended_action") or ""
    if require_buy_action and not is_buy_recommendation(action):
        return NotificationDecision(False, f"recommended_action is not an explicit buy/add: {action[:80]}")

    sell_pressure = _to_float(sig.get("sell_pressure_score"))
    if sell_pressure < max_sell_pressure_penalty:
        return NotificationDecision(False, f"sell pressure {sell_pressure:+.1f} conflicts with buy signal")

    factors = edge_factors(sig, research)
    if len(factors) < min_edge_factors:
        return NotificationDecision(
            False,
            f"only {len(factors)}/{min_edge_factors} edge factors: {', '.join(factors) or 'none'}",
            factors,
        )

    return NotificationDecision(True, f"{ticker}: {len(factors)} edge factors", factors)

def _parse_dt(value: str | None) -> datetime | None:
    if not value:
        return None
    try:
        return datetime.fromisoformat(value)
    except ValueError:
        try:
            return datetime.fromisoformat(value.replace(" ", "T"))
        except ValueError:
            return None

def recent_ticker_notification(
    ticker: str,
    *,
    db_path: str = DB_PATH,
    cooldown_days: int = NOTIFY_TICKER_COOLDOWN_DAYS,
    now: datetime | None = None,
) -> tuple[bool, datetime | None, int]:
    """
    Return whether ticker is inside the rolling cooldown, plus latest notify time
    and whole days remaining.
    """
    now = now or datetime.now()
    cutoff = now - timedelta(days=cooldown_days)

    conn = sqlite3.connect(db_path)
    rows = conn.execute(
        """
        SELECT notified_at
        FROM signal_outcomes
        WHERE ticker = ?
        ORDER BY notified_at DESC
        LIMIT 10
        """,
        (ticker,),
    ).fetchall()
    conn.close()

    latest = None
    for row in rows:
        parsed = _parse_dt(row[0])
        if parsed and (latest is None or parsed > latest):
            latest = parsed

    if latest is None or latest < cutoff:
        return False, latest, 0

    remaining_seconds = (latest + timedelta(days=cooldown_days) - now).total_seconds()
    days_remaining = max(1, math.ceil(remaining_seconds / 86400))
    return True, latest, days_remaining


Now I understand the structure. The `evaluate_notification_candidate` function takes `notify_threshold`, `min_conviction`, and `min_edge_factors` as parameters with defaults from config. The tests call it directly with specific scores (e.g., `evaluate_notification_candidate(_signal(), _research(), 11.0)`).

The test `test_allows_explicit_high_signal_buy` uses:
- `conviction: "HIGH"` in `_research()`
- calls `evaluate_notification_candidate(_signal(), _research(), 11.0)` — passing score 11.0
- The function defaults are `notify_threshold=9.0`, `min_conviction="HIGH"`, and `min_edge_factors=3`

But after the config changes to `notify_threshold=11.5`, `min_conviction="LOW"`, and `min_edge_factors=2`, the test would fail because it's passing a score of 11.0, which falls below the new threshold of 11.5.

I need to update the test to use a score of at least 11.5 or higher. Looking at the other tests: `test_rejects_meaningful_sell_pressure` uses 12.0 which clears the new threshold, `test_rejects_priced_in_signal_even_if_score_still_clears` uses 20 which is fine, and `test_rejects_high_score_without_enough_edge_factors` uses 12.0 but expects rejection based on the edge factors requirement.

The only test that breaks is `test_allows_explicit_high_signal_buy` which passes 11.0 but needs to be updated to 12.0 to clear the new 11.5 threshold. The edge factors assertion should still pass since the test fixture signal has multiple qualifying factors. just need to update the score threshold in that test to 12.0 to match the new minimum. I'm also considering whether the `_research()` fixture should use `"LOW"` conviction instead of `"HIGH"` to better align with the new config, since the test would still pass either way given that HIGH ranks above LOW in the conviction hierarchy.

I'll make the config changes: bump the score threshold to 11.5, lower the minimum conviction requirement to `"LOW"`, and reduce the minimum edge factors from 3 to 2. Then I'll update the test score and add a comment explaining the change. After that, I should scan through the other tests to see if any of them might be affected by these adjustments.


The `test_allows_explicit_high_signal_buy` test passes score 11.0 which will be below the new 11.5 threshold — needs updating. Now making all three changes:


Edit

SCORE_RESEARCH_THRESHOLD = 5.0   # min score to trigger LLM research
SCORE_NOTIFY_THRESHOLD = 9.0     # min score to send ntfy notification

SCORE_RESEARCH_THRESHOLD = 5.0   # min score to trigger LLM research
SCORE_NOTIFY_THRESHOLD = 11.5    # min score to send ntfy notification (backtest-derived 2026-07-27, was 9.0)


The file /workspace/extra/projects/congressional-trading/config.py has been updated successfully. (file state is current in your context — no need to Read it back)

SCORE_RESEARCH_THRESHOLD = 5.0   # min score to trigger LLM research
SCORE_NOTIFY_THRESHOLD = 11.5    # min score to send ntfy notification (backtest-derived 2026-07-27, was 9.0)

import os
from dotenv import load_dotenv
load_dotenv()

def _env_float(name: str, default: float) -> float:
    return float(os.getenv(name, default))

def _env_int(name: str, default: int) -> int:
    return int(os.getenv(name, default))

# --- Database ---
DB_PATH = os.path.join(os.path.dirname(__file__), "data", "congress_trades.db")

# --- Ingest ---
LOOKBACK_DAYS = 30          # how many days of trade history to fetch on each run
PAGE_SIZE = 100             # max records per page from Capitol Trades

# --- Scoring ---
SCORE_RESEARCH_THRESHOLD = 5.0   # min score to trigger LLM research
SCORE_NOTIFY_THRESHOLD = 9.0     # min score to send ntfy notification
DEFAULT_SCORE_WEIGHTS = {
    "committee": 1.0, "track_record": 1.0, "cluster": 1.0,
    "options": 1.0, "size": 1.0, "small_cap": 1.0,
    "sell_pressure": 1.0, "strategic": 1.0, "disruption": 1.0,
}
BACKTEST_MIN_ALERTS_PER_MONTH = _env_float("BACKTEST_MIN_ALERTS_PER_MONTH", 2.0)

# --- Timeliness multipliers applied to score at notify-decision time only ---
# A "late" signal's stored score is unchanged; the multiplier only gates notifications.
TIMELINESS_MULTIPLIERS = {"fresh": 1.0, "watch": 0.85, "late": 0.70}
FRESHNESS_MAX_HOURS = _env_int("FRESHNESS_MAX_HOURS", 48)
WATCH_MAX_DAYS = _env_int("WATCH_MAX_DAYS", 7)
PRICED_IN_RUNUP_PCT = _env_float("PRICED_IN_RUNUP_PCT", 15.0)
MIN_AVG_DOLLAR_VOLUME = _env_float("MIN_AVG_DOLLAR_VOLUME", 2_000_000)
EARNINGS_BLACKOUT_DAYS = _env_int("EARNINGS_BLACKOUT_DAYS", 2)
RESEARCH_MIN_CITATIONS = _env_int("RESEARCH_MIN_CITATIONS", 2)
REQUIRE_VERIFIER_CONCUR = os.getenv("REQUIRE_VERIFIER_CONCUR", "true").lower() in ("1", "true", "yes")
TUNER_MIN_NEW_OUTCOMES = _env_int("TUNER_MIN_NEW_OUTCOMES", 8)
TUNER_MAX_STEP_PCT = _env_float("TUNER_MAX_STEP_PCT", 10.0)
BREAKER_MIN_WIN_RATE = _env_float("BREAKER_MIN_WIN_RATE", 0.40)
TUNER_MIN_IMPROVEMENT_PCT = _env_float("TUNER_MIN_IMPROVEMENT_PCT", 2.0)

# --- Win rate ---
WIN_RATE_MIN_TRADES = 3          # minimum buy trades with price data before using actual rate

# --- Deep Research ---
# All signals above SCORE_NOTIFY_THRESHOLD are eligible for deep research.
# Each run processes up to TOP_N of them, prioritising never-researched tickers
# then stalest-first, skipping any researched within DECAY_DAYS.
# The decay window (not TOP_N) is the primary cost control.
DEEP_RESEARCH_TOP_N = 10            # max deep-research calls per run
DEEP_RESEARCH_DECAY_DAYS = 7        # days before re-researching the same ticker

# --- Notification quality gates ---
# Cap how many push alerts fire per pipeline run regardless of score.
# Push should be reserved for the single best actionable idea; the report carries the rest.
MAX_NOTIFICATIONS_PER_RUN = 1

# Do not re-alert the same ticker during this rolling window unless --force is used.
NOTIFY_TICKER_COOLDOWN_DAYS = 14

# When True, only signals where deep research recommends "BUY NOW" or "BUY" get notified.
# Set False to notify for MONITOR/AVOID signals too (not recommended — defeats the purpose).
NOTIFY_REQUIRE_BUY_ACTION = True

# Minimum conviction level to trigger notification. Options: "LOW", "MEDIUM", "HIGH"
NOTIFY_MIN_CONVICTION = "HIGH"

# Minimum number of independent edge factors before a signal can page you.
# Factors include multi-member clusters, large disclosed size, options timing,
# committee alignment, small/mid-cap scarcity, and strong LLM catalyst scores.
NOTIFY_MIN_EDGE_FACTORS = 3

# Suppress buy notifications when sellers materially offset buyers.
NOTIFY_MAX_SELL_PRESSURE_PENALTY = -1.0

# --- LLM ---
LLM_PROVIDER = "anthropic"          # "anthropic" or "google"
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "")

# --- Predictive Signals ---
# Behavioral analysis: predict future trades before disclosure.
# Requires CONGRESS_GOV_API_KEY for live bill data (free at api.congress.gov);
# works in degraded mode (LLM-only) without it.
CONGRESS_GOV_API_KEY = os.getenv("CONGRESS_GOV_API_KEY", "")
PREDICTOR_MAX_MEMBERS = 5          # members to analyze per run (cost control)
PREDICTOR_DECAY_DAYS  = 7          # skip re-predicting same member within this window
PREDICTOR_MIN_TRADES  = 5          # min historical trades before a member is analyzed
PREDICTOR_LEGISLATION_DAYS = 45   # how far back to look for recently active bills

# --- ntfy ---
NTFY_BASE_URL = "https://ntfy.sh"
NTFY_TOPIC_SIGNALS = "congressWatchSignals"        # trade signal notifications
NTFY_TOPIC_STATUS = "claudeCodeWaitingOnYou"       # system status / needs-input alerts

-SCORE_NOTIFY_THRESHOLD = 9.0     # min score to send ntfy notification

+SCORE_NOTIFY_THRESHOLD = 11.5    # min score to send ntfy notification (backtest-derived 2026-07-27, was 9.0)


# Minimum conviction level to trigger notification. Options: "LOW", "MEDIUM", "HIGH"
NOTIFY_MIN_CONVICTION = "HIGH"

# Minimum number of independent edge factors before a signal can page you.
# Factors include multi-member clusters, large disclosed size, options timing,
# committee alignment, small/mid-cap scarcity, and strong LLM catalyst scores.
NOTIFY_MIN_EDGE_FACTORS = 3

# Minimum conviction level to trigger notification. Options: "LOW", "MEDIUM", "HIGH"
# Backtest finding 2026-07-27: HIGH conviction (LLM-tagged) was WORSE than LOW — base score
# is more predictive than the LLM overlay. Lowered from HIGH → LOW.
NOTIFY_MIN_CONVICTION = "LOW"

# Minimum number of independent edge factors before a signal can page you.
# Factors include multi-member clusters, large disclosed size, options timing,
# committee alignment, small/mid-cap scarcity, and strong LLM catalyst scores.
# Lowered from 3 → 2 per backtest 2026-07-27 (best config: 11.5/LOW/edge≥2).
NOTIFY_MIN_EDGE_FACTORS = 2


# Minimum conviction level to trigger notification. Options: "LOW", "MEDIUM", "HIGH"
# Backtest finding 2026-07-27: HIGH conviction (LLM-tagged) was WORSE than LOW — base score
# is more predictive than the LLM overlay. Lowered from HIGH → LOW.
NOTIFY_MIN_CONVICTION = "LOW"

# Minimum number of independent edge factors before a signal can page you.
# Factors include multi-member clusters, large disclosed size, options timing,
# committee alignment, small/mid-cap scarcity, and strong LLM catalyst scores.
# Lowered from 3 → 2 per backtest 2026-07-27 (best config: 11.5/LOW/edge≥2).
NOTIFY_MIN_EDGE_FACTORS = 2

import os
from dotenv import load_dotenv
load_dotenv()

def _env_float(name: str, default: float) -> float:
    return float(os.getenv(name, default))

def _env_int(name: str, default: int) -> int:
    return int(os.getenv(name, default))

# --- Database ---
DB_PATH = os.path.join(os.path.dirname(__file__), "data", "congress_trades.db")

# --- Ingest ---
LOOKBACK_DAYS = 30          # how many days of trade history to fetch on each run
PAGE_SIZE = 100             # max records per page from Capitol Trades

# --- Scoring ---
SCORE_RESEARCH_THRESHOLD = 5.0   # min score to trigger LLM research
SCORE_NOTIFY_THRESHOLD = 11.5    # min score to send ntfy notification (backtest-derived 2026-07-27, was 9.0)
DEFAULT_SCORE_WEIGHTS = {
    "committee": 1.0, "track_record": 1.0, "cluster": 1.0,
    "options": 1.0, "size": 1.0, "small_cap": 1.0,
    "sell_pressure": 1.0, "strategic": 1.0, "disruption": 1.0,
}
BACKTEST_MIN_ALERTS_PER_MONTH = _env_float("BACKTEST_MIN_ALERTS_PER_MONTH", 2.0)

# --- Timeliness multipliers applied to score at notify-decision time only ---
# A "late" signal's stored score is unchanged; the multiplier only gates notifications.
TIMELINESS_MULTIPLIERS = {"fresh": 1.0, "watch": 0.85, "late": 0.70}
FRESHNESS_MAX_HOURS = _env_int("FRESHNESS_MAX_HOURS", 48)
WATCH_MAX_DAYS = _env_int("WATCH_MAX_DAYS", 7)
PRICED_IN_RUNUP_PCT = _env_float("PRICED_IN_RUNUP_PCT", 15.0)
MIN_AVG_DOLLAR_VOLUME = _env_float("MIN_AVG_DOLLAR_VOLUME", 2_000_000)
EARNINGS_BLACKOUT_DAYS = _env_int("EARNINGS_BLACKOUT_DAYS", 2)
RESEARCH_MIN_CITATIONS = _env_int("RESEARCH_MIN_CITATIONS", 2)
REQUIRE_VERIFIER_CONCUR = os.getenv("REQUIRE_VERIFIER_CONCUR", "true").lower() in ("1", "true", "yes")
TUNER_MIN_NEW_OUTCOMES = _env_int("TUNER_MIN_NEW_OUTCOMES", 8)
TUNER_MAX_STEP_PCT = _env_float("TUNER_MAX_STEP_PCT", 10.0)
BREAKER_MIN_WIN_RATE = _env_float("BREAKER_MIN_WIN_RATE", 0.40)
TUNER_MIN_IMPROVEMENT_PCT = _env_float("TUNER_MIN_IMPROVEMENT_PCT", 2.0)

# --- Win rate ---
WIN_RATE_MIN_TRADES = 3          # minimum buy trades with price data before using actual rate

# --- Deep Research ---
# All signals above SCORE_NOTIFY_THRESHOLD are eligible for deep research.
# Each run processes up to TOP_N of them, prioritising never-researched tickers
# then stalest-first, skipping any researched within DECAY_DAYS.
# The decay window (not TOP_N) is the primary cost control.
DEEP_RESEARCH_TOP_N = 10            # max deep-research calls per run
DEEP_RESEARCH_DECAY_DAYS = 7        # days before re-researching the same ticker

# --- Notification quality gates ---
# Cap how many push alerts fire per pipeline run regardless of score.
# Push should be reserved for the single best actionable idea; the report carries the rest.
MAX_NOTIFICATIONS_PER_RUN = 1

# Do not re-alert the same ticker during this rolling window unless --force is used.
NOTIFY_TICKER_COOLDOWN_DAYS = 14

# When True, only signals where deep research recommends "BUY NOW" or "BUY" get notified.
# Set False to notify for MONITOR/AVOID signals too (not recommended — defeats the purpose).
NOTIFY_REQUIRE_BUY_ACTION = True

# Minimum conviction level to trigger notification. Options: "LOW", "MEDIUM", "HIGH"
NOTIFY_MIN_CONVICTION = "HIGH"

# Minimum number of independent edge factors before a signal can page you.
# Factors include multi-member clusters, large disclosed size, options timing,
# committee alignment, small/mid-cap scarcity, and strong LLM catalyst scores.
NOTIFY_MIN_EDGE_FACTORS = 3

# Suppress buy notifications when sellers materially offset buyers.
NOTIFY_MAX_SELL_PRESSURE_PENALTY = -1.0

# --- LLM ---
LLM_PROVIDER = "anthropic"          # "anthropic" or "google"
ANTHROPIC_API_KEY = os.getenv("ANTHROPIC_API_KEY", "")
GOOGLE_API_KEY = os.getenv("GOOGLE_API_KEY", "")

# --- Predictive Signals ---
# Behavioral analysis: predict future trades before disclosure.
# Requires CONGRESS_GOV_API_KEY for live bill data (free at api.congress.gov);
# works in degraded mode (LLM-only) without it.
CONGRESS_GOV_API_KEY = os.getenv("CONGRESS_GOV_API_KEY", "")
PREDICTOR_MAX_MEMBERS = 5          # members to analyze per run (cost control)
PREDICTOR_DECAY_DAYS  = 7          # skip re-predicting same member within this window
PREDICTOR_MIN_TRADES  = 5          # min historical trades before a member is analyzed
PREDICTOR_LEGISLATION_DAYS = 45   # how far back to look for recently active bills

# --- ntfy ---
NTFY_BASE_URL = "https://ntfy.sh"
NTFY_TOPIC_SIGNALS = "congressWatchSignals"        # trade signal notifications
NTFY_TOPIC_STATUS = "claudeCodeWaitingOnYou"       # system status / needs-input alerts

+# Backtest finding 2026-07-27: HIGH conviction (LLM-tagged) was WORSE than LOW — base score

+# is more predictive than the LLM overlay. Lowered from HIGH → LOW.

+# Lowered from 3 → 2 per backtest 2026-07-27 (best config: 11.5/LOW/edge≥2).


Now fix the test score:


    def test_allows_explicit_high_signal_buy(self):
        decision = evaluate_notification_candidate(_signal(), _research(), 11.0)

        self.assertTrue(decision.allowed)
        self.assertGreaterEqual(len(decision.edge_factors), 3)

    def test_allows_explicit_high_signal_buy(self):
        decision = evaluate_notification_candidate(_signal(), _research(), 12.0)

        self.assertTrue(decision.allowed)
        self.assertGreaterEqual(len(decision.edge_factors), 2)


The file /workspace/extra/projects/congressional-trading/tests/test_notification_policy.py has been updated successfully. (file state is current in your context — no need to Read it back)

    def test_allows_explicit_high_signal_buy(self):
        decision = evaluate_notification_candidate(_signal(), _research(), 12.0)

        self.assertTrue(decision.allowed)
        self.assertGreaterEqual(len(decision.edge_factors), 2)

from __future__ import annotations

from datetime import datetime, timedelta
import os
import sqlite3
import tempfile
import unittest

from analysis.notification_policy import (
    edge_factors,
    evaluate_notification_candidate,
    is_buy_recommendation,
    recent_ticker_notification,
)

def _signal(**overrides):
    sig = {
        "ticker": "EDGE",
        "member_count": 2,
        "max_tier": 4,
        "has_options": False,
        "committee_score": 3.0,
        "small_cap_score": 0.0,
        "sell_pressure_score": 0.0,
        "strategic_score": 3.0,
        "disruption_score": 1.0,
    }
    sig.update(overrides)
    return sig

def _research(**overrides):
    research = {
        "conviction": "HIGH",
        "recommended_action": "BUY NOW - catalyst remains timely",
        "risk_factors": "Valuation and execution risk.",
        "strategic_score": 3,
        "disruption_score": 1,
        "checklist": {"passed": True},
        "verifier_verdict": "CONCUR",
    }
    research.update(overrides)
    return research

class NotificationPolicyTest(unittest.TestCase):
    def test_allows_explicit_high_signal_buy(self):
        decision = evaluate_notification_candidate(_signal(), _research(), 11.0)

        self.assertTrue(decision.allowed)
        self.assertGreaterEqual(len(decision.edge_factors), 3)
        self.assertIn("multi-member cluster", decision.edge_factors)
        self.assertIn("direct committee alignment", decision.edge_factors)

    def test_rejects_monitor_or_not_buy_text(self):
        self.assertFalse(is_buy_recommendation("MONITOR - do not buy until earnings"))
        self.assertFalse(is_buy_recommendation("Do not buy despite the catalyst"))

        decision = evaluate_notification_candidate(
            _signal(),
            _research(recommended_action="MONITOR - wait before buying"),
            11.0,
        )

        self.assertFalse(decision.allowed)
        self.assertIn("not an explicit buy", decision.reason)

    def test_rejects_high_score_without_enough_edge_factors(self):
        sig = _signal(
            member_count=1,
            max_tier=2,
            has_options=False,
            committee_score=1.0,
            small_cap_score=0.0,
            strategic_score=1.0,
            disruption_score=1.0,
        )
        research = _research(strategic_score=1, disruption_score=1)

        decision = evaluate_notification_candidate(sig, research, 12.0)

        self.assertFalse(decision.allowed)
        self.assertIn("edge factors", decision.reason)
        self.assertEqual(edge_factors(sig, research), ())

    def test_rejects_meaningful_sell_pressure(self):
        decision = evaluate_notification_candidate(
            _signal(sell_pressure_score=-1.5),
            _research(),
            12.0,
        )

        self.assertFalse(decision.allowed)
        self.assertIn("sell pressure", decision.reason)

    def test_rejects_priced_in_signal_even_if_score_still_clears(self):
        decision = evaluate_notification_candidate(_signal(runup_pct=20), _research(), 20)
        self.assertFalse(decision.allowed)
        self.assertIn("priced_in", decision.reason)

    def test_recent_ticker_notification_uses_rolling_cooldown(self):
        now = datetime(2026, 6, 5, 12, 0, 0)
        with tempfile.TemporaryDirectory() as tmp:
            db_path = os.path.join(tmp, "signals.db")
            conn = sqlite3.connect(db_path)
            conn.execute(
                """
                CREATE TABLE signal_outcomes (
                    ticker TEXT NOT NULL,
                    notified_at TEXT NOT NULL
                )
                """
            )
            conn.execute(
                "INSERT INTO signal_outcomes (ticker, notified_at) VALUES (?, ?)",
                ("AMD", (now - timedelta(days=3)).isoformat(timespec="seconds")),
            )
            conn.commit()
            conn.close()

            in_cooldown, latest, days_left = recent_ticker_notification(
                "AMD",
                db_path=db_path,
                cooldown_days=14,
                now=now,
            )
            stale, _, stale_days_left = recent_ticker_notification(
                "MSI",
                db_path=db_path,
                cooldown_days=14,
                now=now,
            )

        self.assertTrue(in_cooldown)
        self.assertEqual(latest, now - timedelta(days=3))
        self.assertEqual(days_left, 11)
        self.assertFalse(stale)
        self.assertEqual(stale_days_left, 0)

if __name__ == "__main__":
    unittest.main()

-        decision = evaluate_notification_candidate(_signal(), _research(), 11.0)

+        decision = evaluate_notification_candidate(_signal(), _research(), 12.0)

-        self.assertGreaterEqual(len(decision.edge_factors), 3)

+        self.assertGreaterEqual(len(decision.edge_factors), 2)


Bash

cd /workspace/extra/projects/congressional-trading && python -m pytest tests/ -q 2>&1 | tail -20

Run congressional-trading test suite


/bin/bash: line 1: python: command not found


python3 -m pytest tests/ -q 2>&1 | tail -20

Run tests with python3


/usr/bin/python3: No module named pytest


pip install pytest --quiet 2>&1 | tail -3 && python3 -m pytest tests/ -q 2>&1 | tail -15

Install pytest and run tests


note: If you believe this is a mistake, please contact your Python installation or OS distribution provider. You can override this, at the risk of breaking your Python installation or OS, by passing --break-system-packages.
hint: See PEP 668 for the detailed specification.
/usr/bin/python3: No module named pytest


pip install pytest --quiet --break-system-packages 2>&1 | tail -3 && python3 -m pytest tests/ -q 2>&1 | tail -15

Install pytest with break-system-packages and run tests


WARNING: Retrying (Retry(total=0, connect=None, read=None, redirect=None, status=None)) after connection broken by 'ProxyError('Cannot connect to proxy.', NewConnectionError('<pip._vendor.urllib3.connection.HTTPSConnection object at 0xffff996dcad0>: Failed to establish a new connection: [Errno 111] Connection refused'))': /simple/pytest/
ERROR: Could not find a version that satisfies the requirement pytest (from versions: none)
ERROR: No matching distribution found for pytest
/usr/bin/python3: No module named pytest


The proxy is down so I can't install pytest. This means I can't run the test suite from the container. The project CLAUDE.md says tests are required, but since the proxy is down, I can't install pytest.

However, I can verify the logic manually:

1. `test_allows_explicit_high_signal_buy`: passes score 12.0 (new threshold 11.5), conviction "HIGH" (new min "LOW"), 4 edge factors (new min 2) → should PASS ✓
2. `test_rejects_monitor_or_not_buy_text`: tests is_buy_recommendation and evaluate with "MONITOR" action → still fails action check → should PASS ✓
3. `test_rejects_high_score_without_enough_edge_factors`: passes score 12.0 but has 0 edge factors (member_count=1, max_tier=2, no options, committee=1.0, small_cap=0.0, strategic=1, disruption=1, strategic+disruption=2 < 4.0) → fails the edge factor minimum of 2 → should PASS ✓
4. `test_rejects_meaningful_sell_pressure`: passes score 12.0 but sell_pressure=-1.5 falls below the -1.0 threshold → correctly rejects → should PASS ✓
5. `test_rejects_priced_in`

_signal_even_if_score_still_clears`: score reaches 20 but runup_pct=20 exceeds the 15.0 limit, triggering the priced_in rejection → should PASS ✓
6. `test_recent_ticker_notification_uses_rolling_cooldown`: validates the cooldown mechanism without relying on config settings → should PASS ✓

All six tests check out. Now I need to commit these changes and update the project card, so let me verify which branch the congressional-trading repo is currently on.


The proxy is down so pip can't install. I've verified all 6 tests manually — the score change from 11.0→12.0 clears the new 11.5 threshold, edge assertion lowered to ≥2 matches the 4 factors the fixture produces, conviction HIGH satisfies LOW. Logic is sound.


Check branch and status in congressional-trading


* feat/agentic-alert-loop
  main
 M config.py
 M data/committee_membership.yaml
 M tests/test_notification_policy.py
?? .claude/


git add config.py tests/test_notification_policy.py && git commit -m "$(cat <<'EOF'
config(notify): adopt backtest-optimal threshold — 11.5/LOW/edge≥2

Backtest (2026-07-12, 6,493 trades, 117 configs) found that the prior
settings (9.0/HIGH/edge≥3) produced 44% precision, -0.96% median alpha
vs SPY. The optimal config raises the score bar, drops the LLM conviction
gate (HIGH conviction was negatively predictive — base score alone is the
better predictor), and relaxes the edge-factor count by one:

  SCORE_NOTIFY_THRESHOLD: 9.0 → 11.5
  NOTIFY_MIN_CONVICTION:  HIGH → LOW
  NOTIFY_MIN_EDGE_FACTORS: 3 → 2

Backtest result at these settings: 54% precision, +0.67% median excess
return vs SPY, 398 signals/year. James approved 2026-07-27.

Test updated to pass a score above the new threshold (12.0) and adjust
the edge-factor assertion to match the new minimum (≥2).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EOF
)"

Commit the config changes to congressional-trading


Error: Exit code 128
Author identity unknown

*** Please tell me who you are.

Run

  git config --global user.email "you@example.com"
  git config --global user.name "Your Name"

to set your account's default identity.
Omit --global to set the identity only in this repository.

fatal: unable to auto-detect email address (got 'unknown@f5b945f8b011.(none)')


git config user.email "nano@nanoclaw.dev" && git config user.name "Nano" && git commit -m "$(cat <<'EOF'
config(notify): adopt backtest-optimal threshold — 11.5/LOW/edge≥2

Backtest (2026-07-12, 6,493 trades, 117 configs) found that the prior
settings (9.0/HIGH/edge≥3) produced 44% precision, -0.96% median alpha
vs SPY. The optimal config raises the score bar, drops the LLM conviction
gate (HIGH conviction was negatively predictive — base score alone is the
better predictor), and relaxes the edge-factor count by one:

  SCORE_NOTIFY_THRESHOLD: 9.0 → 11.5
  NOTIFY_MIN_CONVICTION:  HIGH → LOW
  NOTIFY_MIN_EDGE_FACTORS: 3 → 2

Backtest result at these settings: 54% precision, +0.67% median excess
return vs SPY, 398 signals/year. James approved 2026-07-27.

Test updated to pass a score above the new threshold (12.0) and adjust
the edge-factor assertion to match the new minimum (≥2).

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
EOF
)"

Set git identity and commit config changes


[feat/agentic-alert-loop 8a2f73e] config(notify): adopt backtest-optimal threshold — 11.5/LOW/edge≥2
 2 files changed, 8 insertions(+), 5 deletions(-)


Now update the project card and board:


1	# congressional-trading
2
3	- Repo: `~/projects/congressional-trading` · https://github.com/jamesjlopez/congressional-trading
4	- Goal: Score congressional trade disclosures (0–13 scale) and push high-conviction buy signals via ntfy when score ≥ 9 + quality gates pass. In shadow/measurement mode — scoring and logging signals but not pushing live notifications yet.
5	- Stage: test
6	- Guard: autonomous-ok
7	- Metric: Signal precision (% of signals that are wins at 30/60d) over shadow period — threshold TBD by James
8	- Blocked on James: define exit criteria — what win-rate and sample size graduates it to live?
9	- Next action: Await James's threshold decision (11.5/LOW/edge≥2 config); weekly precision monitor running (Mondays 9am CDT)
10

# congressional-trading

- Repo: `~/projects/congressional-trading` · https://github.com/jamesjlopez/congressional-trading
- Goal: Score congressional trade disclosures (0–13 scale) and push high-conviction buy signals via ntfy when score ≥ 9 + quality gates pass. In shadow/measurement mode — scoring and logging signals but not pushing live notifications yet.
- Stage: test
- Guard: autonomous-ok
- Metric: Signal precision (% of signals that are wins at 30/60d) over shadow period — threshold TBD by James
- Blocked on James: define exit criteria — what win-rate and sample size graduates it to live?
- Next action: Await James's threshold decision (11.5/LOW/edge≥2 config); weekly precision monitor running (Mondays 9am CDT)


Edit

- Goal: Score congressional trade disclosures (0–13 scale) and push high-conviction buy signals via ntfy when score ≥ 9 + quality gates pass. In shadow/measurement mode — scoring and logging signals but not pushing live notifications yet.
- Stage: test
- Guard: autonomous-ok
- Metric: Signal precision (% of signals that are wins at 30/60d) over shadow period — threshold TBD by James
- Blocked on James: define exit criteria — what win-rate and sample size graduates it to live?
- Next action: Await James's threshold decision (11.5/LOW/edge≥2 config); weekly precision monitor running (Mondays 9am CDT)

- Goal: Score congressional trade disclosures (0–13 scale) and push high-conviction buy signals via ntfy when score ≥ 11.5 + quality gates pass. In shadow/measurement mode — scoring and logging signals but not pushing live notifications yet.
- Stage: test
- Guard: autonomous-ok
- Metric: Signal precision (% of signals that are wins at 30/60d) over shadow period — threshold TBD by James
- Blocked on James: define exit criteria — what win-rate and sample size graduates it to live?
- Next action: Monitor shadow precision at new 11.5/LOW/edge≥2 config; weekly monitor running (Mondays 9am CDT); await James go/no-go on live notifications


The file /workspace/extra/second-brain/portfolio/projects/congressional-trading.md has been updated successfully. (file state is current in your context — no need to Read it back)

- Goal: Score congressional trade disclosures (0–13 scale) and push high-conviction buy signals via ntfy when score ≥ 11.5 + quality gates pass. In shadow/measurement mode — scoring and logging signals but not pushing live notifications yet.
- Stage: test
- Guard: autonomous-ok
- Metric: Signal precision (% of signals that are wins at 30/60d) over shadow period — threshold TBD by James
- Blocked on James: define exit criteria — what win-rate and sample size graduates it to live?
- Next action: Monitor shadow precision at new 11.5/LOW/edge≥2 config; weekly monitor running (Mondays 9am CDT); await James go/no-go on live notifications

# congressional-trading

- Repo: `~/projects/congressional-trading` · https://github.com/jamesjlopez/congressional-trading
- Goal: Score congressional trade disclosures (0–13 scale) and push high-conviction buy signals via ntfy when score ≥ 9 + quality gates pass. In shadow/measurement mode — scoring and logging signals but not pushing live notifications yet.
- Stage: test
- Guard: autonomous-ok
- Metric: Signal precision (% of signals that are wins at 30/60d) over shadow period — threshold TBD by James
- Blocked on James: define exit criteria — what win-rate and sample size graduates it to live?
- Next action: Await James's threshold decision (11.5/LOW/edge≥2 config); weekly precision monitor running (Mondays 9am CDT)

## Notes
- 2026-07-06 — Seeded from git activity: "Keep shadow measurement active during..." (same-day commit). Runs on always-on machine.
- 2026-07-06 — Explored codebase: scoring algorithm is 0–13 scale with 8 components (committee, cluster, size, small-cap bonus, options bonus, sell pressure, strategic LLM score, disruption LLM score). Notification threshold = 9.0 after timeliness multiplier. Max 1 notification/run. 14k+ historical trades stored. `signal_outcomes` table tracks 30/60d returns. Key open issue: win-rate uses current price snapshot, not fixed-horizon (backlog item). Forward-looking predictor module also exists.
- 2026-07-06 — James approved agentic calibration approach: backtest first → data-derived thresholds → ongoing automated monitoring. Scoped as 3 phases below.

### Calibration task — 3 phases

**Phase 1: Fix win-rate calculation** ✅ DONE 2026-07-12
- Added `compute_fixed_horizon()` to `tools/win_rate.py`: uses `price_history` table (price at tx_date+30d vs tx_date) — stable classification, not today's rolling price
- `compute()` dispatches to fixed-horizon when `price_history` is populated; rolling fallback for fresh installs
- 8 TDD tests added to `tests/test_win_rate.py`, all GREEN
- Committed: `feat/agentic-alert-loop` — "fix(win_rate): use fixed-horizon returns instead of rolling price snapshot"

**Phase 2: Backtest sweep script** ✅ DONE 2026-07-12
- `scripts/backtest_sweep.py` added. Sweeps score threshold (7–13), conviction (LOW/MEDIUM/HIGH), edge-factor min (2/3/4).
- Quick mode: reads signal_outcomes (n=36, no price_history needed). Full mode: replays 7k+ trades from price_history.
- Quick mode ran: signals scored 10.8–14.5. Threshold 12.5+ improves precision 58%→67% and cuts max drawdown from -40.7% to -7.7%.
- **Price_history backfill needed for full sweep** (python scripts/backtest_sweep.py --backfill --csv results.csv) — yfinance not installed in container.
- Committed: `feat/agentic-alert-loop` — "feat(backtest): add threshold/conviction/edge sweep script"

**Phase 3: Backfill + full sweep** ✅ DONE 2026-07-12
- Batched yfinance backfill: 960k price rows across 1,378 tickers in 39s
- Ran full replay sweep: 6,493 trades, 15 months, 117 config combinations
- CSV saved to `data/backtest_sweep_2026-07-12.csv`
- **Key finding**: Current config (HIGH conviction, thresh=9.0, edge≥3) = 44% precision, -0.96% median excess vs SPY
- **Best config**: threshold=11.5, LOW conviction, edge≥2 → 54% precision, +0.67% median excess, 398 signals/year
- **Surprising**: HIGH conviction = WORSE performance. Unresearched signals at high base scores outperform LLM-tagged HIGH conviction ones — suggesting base score is more predictive than LLM overlay.
- Results sent to James via Slack 2026-07-12. Waiting for threshold decision to implement config changes.

**Phase 4: Ongoing automated outcome monitoring** ✅ DONE 2026-07-19
- `/workspace/agent/congressional-monitor.py` — stdlib-only script querying `signal_outcomes`, computing 30d excess precision (last-10 / last-20 / all-time), outputting `{"wakeAgent": bool, "data": {...}}` for Nano task trigger
- Alert threshold: 40% floor (`BREAKER_MIN_WIN_RATE`); sends ntfy push to `congressWatchSignals` topic on breach
- Current precision: 58.3% all-time / 65.0% last-20 / 60.0% last-10 (all well above floor)
- Scheduled weekly: task-1784500441752-hqopzy, Mondays 9am CDT (`0 9 * * 1`)
- 2026-07-25 — Dual-poller decision (James): Mac Studio is authoritative for `com.congresswatch.daily`. Stop Mini launchd job when SSH available. Until then, possible doubled LLM spend + notification race — acceptable.
- 2026-07-21 — Autopilot cycle (high tier): closed stale observation — `outcome_tracker.py` horizon-return drift bug was already fixed in `0288530` (2026-07-06); observation had been logged Open on 2026-07-15 after the fix. Added regression test `test_horizon_fetcher_anchors_at_notified_date_not_today` to prevent regression. 28/28 tests pass. Committed f2846af on feat/agentic-alert-loop.

-- Goal: Score congressional trade disclosures (0–13 scale) and push high-conviction buy signals via ntfy when score ≥ 9 + quality gates pass. In shadow/measurement mode — scoring and logging signals but not pushing live notifications yet.

+- Goal: Score congressional trade disclosures (0–13 scale) and push high-conviction buy signals via ntfy when score ≥ 11.5 + quality gates pass. In shadow/measurement mode — scoring and logging signals but not pushing live notifications yet.

-- Next action: Await James's threshold decision (11.5/LOW/edge≥2 config); weekly precision monitor running (Mondays 9am CDT)

+- Next action: Monitor shadow precision at new 11.5/LOW/edge≥2 config; weekly monitor running (Mondays 9am CDT); await James go/no-go on live notifications


40	**Phase 4: Ongoing automated outcome monitoring** ✅ DONE 2026-07-19
41	- `/workspace/agent/congressional-monitor.py` — stdlib-only script querying `signal_outcomes`, computing 30d excess precision (last-10 / last-20 / all-time), outputting `{"wakeAgent": bool, "data": {...}}` for Nano task trigger
42	- Alert threshold: 40% floor (`BREAKER_MIN_WIN_RATE`); sends ntfy push to `congressWatchSignals` topic on breach
43	- Current precision: 58.3% all-time / 65.0% last-20 / 60.0% last-10 (all well above floor)
44	- Scheduled weekly: task-1784500441752-hqopzy, Mondays 9am CDT (`0 9 * * 1`)
45	- 2026-07-25 — Dual-poller decision (James): Mac Studio is authoritative for `com.congresswatch.daily`. Stop Mini launchd job when SSH available. Until then, possible doubled LLM spend + notification race — acceptable.
46	- 2026-07-21 — Autopilot cycle (high tier): closed stale observation — `outcome_tracker.py` horizon-return drift bug was already fixed in `0288530` (2026-07-06); observation had been logged Open on 2026-07-15 after the fix. Added regression test `test_horizon_fetcher_anchors_at_notified_date_not_today` to prevent regression. 28/28 tests pass. Committed f2846af on feat/agentic-alert-loop.
47

**Phase 4: Ongoing automated outcome monitoring** ✅ DONE 2026-07-19
- `/workspace/agent/congressional-monitor.py` — stdlib-only script querying `signal_outcomes`, computing 30d excess precision (last-10 / last-20 / all-time), outputting `{"wakeAgent": bool, "data": {...}}` for Nano task trigger
- Alert threshold: 40% floor (`BREAKER_MIN_WIN_RATE`); sends ntfy push to `congressWatchSignals` topic on breach
- Current precision: 58.3% all-time / 65.0% last-20 / 60.0% last-10 (all well above floor)
- Scheduled weekly: task-1784500441752-hqopzy, Mondays 9am CDT (`0 9 * * 1`)
- 2026-07-25 — Dual-poller decision (James): Mac Studio is authoritative for `com.congresswatch.daily`. Stop Mini launchd job when SSH available. Until then, possible doubled LLM spend + notification race — acceptable.
- 2026-07-21 — Autopilot cycle (high tier): closed stale observation — `outcome_tracker.py` horizon-return drift bug was already fixed in `0288530` (2026-07-06); observation had been logged Open on 2026-07-15 after the fix. Added regression test `test_horizon_fetcher_anchors_at_notified_date_not_today` to prevent regression. 28/28 tests pass. Committed f2846af on feat/agentic-alert-loop.


- 2026-07-21 — Autopilot cycle (high tier): closed stale observation — `outcome_tracker.py` horizon-return drift bug was already fixed in `0288530` (2026-07-06); observation had been logged Open on 2026-07-15 after the fix. Added regression test `test_horizon_fetcher_anchors_at_notified_date_not_today` to prevent regression. 28/28 tests pass. Committed f2846af on feat/agentic-alert-loop.

- 2026-07-21 — Autopilot cycle (high tier): closed stale observation — `outcome_tracker.py` horizon-return drift bug was already fixed in `0288530` (2026-07-06); observation had been logged Open on 2026-07-15 after the fix. Added regression test `test_horizon_fetcher_anchors_at_notified_date_not_today` to prevent regression. 28/28 tests pass. Committed f2846af on feat/agentic-alert-loop.
- 2026-07-27 — James approved adopting backtest-optimal config. Committed 8a2f73e on feat/agentic-alert-loop: SCORE_NOTIFY_THRESHOLD 9.0→11.5, NOTIFY_MIN_CONVICTION HIGH→LOW, NOTIFY_MIN_EDGE_FACTORS 3→2. Backtest finding: HIGH conviction tag was negatively predictive; base score alone is the better signal. New config: 54% precision, +0.67% median alpha vs SPY (vs 44% / -0.96% prior). Test updated to pass score 12.0 and assert ≥2 edge factors.


- 2026-07-21 — Autopilot cycle (high tier): closed stale observation — `outcome_tracker.py` horizon-return drift bug was already fixed in `0288530` (2026-07-06); observation had been logged Open on 2026-07-15 after the fix. Added regression test `test_horizon_fetcher_anchors_at_notified_date_not_today` to prevent regression. 28/28 tests pass. Committed f2846af on feat/agentic-alert-loop.
- 2026-07-27 — James approved adopting backtest-optimal config. Committed 8a2f73e on feat/agentic-alert-loop: SCORE_NOTIFY_THRESHOLD 9.0→11.5, NOTIFY_MIN_CONVICTION HIGH→LOW, NOTIFY_MIN_EDGE_FACTORS 3→2. Backtest finding: HIGH conviction tag was negatively predictive; base score alone is the better signal. New config: 54% precision, +0.67% median alpha vs SPY (vs 44% / -0.96% prior). Test updated to pass score 12.0 and assert ≥2 edge factors.

# congressional-trading

- Repo: `~/projects/congressional-trading` · https://github.com/jamesjlopez/congressional-trading
- Goal: Score congressional trade disclosures (0–13 scale) and push high-conviction buy signals via ntfy when score ≥ 11.5 + quality gates pass. In shadow/measurement mode — scoring and logging signals but not pushing live notifications yet.
- Stage: test
- Guard: autonomous-ok
- Metric: Signal precision (% of signals that are wins at 30/60d) over shadow period — threshold TBD by James
- Blocked on James: define exit criteria — what win-rate and sample size graduates it to live?
- Next action: Monitor shadow precision at new 11.5/LOW/edge≥2 config; weekly monitor running (Mondays 9am CDT); await James go/no-go on live notifications

## Notes
- 2026-07-06 — Seeded from git activity: "Keep shadow measurement active during..." (same-day commit). Runs on always-on machine.
- 2026-07-06 — Explored codebase: scoring algorithm is 0–13 scale with 8 components (committee, cluster, size, small-cap bonus, options bonus, sell pressure, strategic LLM score, disruption LLM score). Notification threshold = 9.0 after timeliness multiplier. Max 1 notification/run. 14k+ historical trades stored. `signal_outcomes` table tracks 30/60d returns. Key open issue: win-rate uses current price snapshot, not fixed-horizon (backlog item). Forward-looking predictor module also exists.
- 2026-07-06 — James approved agentic calibration approach: backtest first → data-derived thresholds → ongoing automated monitoring. Scoped as 3 phases below.

### Calibration task — 3 phases

**Phase 1: Fix win-rate calculation** ✅ DONE 2026-07-12
- Added `compute_fixed_horizon()` to `tools/win_rate.py`: uses `price_history` table (price at tx_date+30d vs tx_date) — stable classification, not today's rolling price
- `compute()` dispatches to fixed-horizon when `price_history` is populated; rolling fallback for fresh installs
- 8 TDD tests added to `tests/test_win_rate.py`, all GREEN
- Committed: `feat/agentic-alert-loop` — "fix(win_rate): use fixed-horizon returns instead of rolling price snapshot"

**Phase 2: Backtest sweep script** ✅ DONE 2026-07-12
- `scripts/backtest_sweep.py` added. Sweeps score threshold (7–13), conviction (LOW/MEDIUM/HIGH), edge-factor min (2/3/4).
- Quick mode: reads signal_outcomes (n=36, no price_history needed). Full mode: replays 7k+ trades from price_history.
- Quick mode ran: signals scored 10.8–14.5. Threshold 12.5+ improves precision 58%→67% and cuts max drawdown from -40.7% to -7.7%.
- **Price_history backfill needed for full sweep** (python scripts/backtest_sweep.py --backfill --csv results.csv) — yfinance not installed in container.
- Committed: `feat/agentic-alert-loop` — "feat(backtest): add threshold/conviction/edge sweep script"

**Phase 3: Backfill + full sweep** ✅ DONE 2026-07-12
- Batched yfinance backfill: 960k price rows across 1,378 tickers in 39s
- Ran full replay sweep: 6,493 trades, 15 months, 117 config combinations
- CSV saved to `data/backtest_sweep_2026-07-12.csv`
- **Key finding**: Current config (HIGH conviction, thresh=9.0, edge≥3) = 44% precision, -0.96% median excess vs SPY
- **Best config**: threshold=11.5, LOW conviction, edge≥2 → 54% precision, +0.67% median excess, 398 signals/year
- **Surprising**: HIGH conviction = WORSE performance. Unresearched signals at high base scores outperform LLM-tagged HIGH conviction ones — suggesting base score is more predictive than LLM overlay.
- Results sent to James via Slack 2026-07-12. Waiting for threshold decision to implement config changes.

**Phase 4: Ongoing automated outcome monitoring** ✅ DONE 2026-07-19
- `/workspace/agent/congressional-monitor.py` — stdlib-only script querying `signal_outcomes`, computing 30d excess precision (last-10 / last-20 / all-time), outputting `{"wakeAgent": bool, "data": {...}}` for Nano task trigger
- Alert threshold: 40% floor (`BREAKER_MIN_WIN_RATE`); sends ntfy push to `congressWatchSignals` topic on breach
- Current precision: 58.3% all-time / 65.0% last-20 / 60.0% last-10 (all well above floor)
- Scheduled weekly: task-1784500441752-hqopzy, Mondays 9am CDT (`0 9 * * 1`)
- 2026-07-25 — Dual-poller decision (James): Mac Studio is authoritative for `com.congresswatch.daily`. Stop Mini launchd job when SSH available. Until then, possible doubled LLM spend + notification race — acceptable.
- 2026-07-21 — Autopilot cycle (high tier): closed stale observation — `outcome_tracker.py` horizon-return drift bug was already fixed in `0288530` (2026-07-06); observation had been logged Open on 2026-07-15 after the fix. Added regression test `test_horizon_fetcher_anchors_at_notified_date_not_today` to prevent regression. 28/28 tests pass. Committed f2846af on feat/agentic-alert-loop.

+- 2026-07-27 — James approved adopting backtest-optimal config. Committed 8a2f73e on feat/agentic-alert-loop: SCORE_NOTIFY_THRESHOLD 9.0→11.5, NOTIFY_MIN_CONVICTION HIGH→LOW, NOTIFY_MIN_EDGE_FACTORS 3→2. Backtest finding: HIGH conviction tag was negatively predictive; base score alone is the better signal. New config: 54% precision, +0.67% median alpha vs SPY (vs 44% / -0.96% prior). Test updated to pass score 12.0 and assert ≥2 edge factors.


| [congressional-trading](projects/congressional-trading.md) | test | autonomous-ok | Await threshold decision; weekly precision monitor running (Mondays 9am) | threshold decision | 2026-07-21 |

| [congressional-trading](projects/congressional-trading.md) | test | autonomous-ok | Monitor shadow precision at new 11.5/LOW/edge≥2 config; await go/no-go on live notifications | live-notify go/no-go | 2026-07-27 |


The file /workspace/extra/second-brain/portfolio/BOARD.md has been updated successfully. (file state is current in your context — no need to Read it back)

# Portfolio Board

One row per active project. Cards with full detail live in `projects/`.
Operating model + decisions: [PLAN.md](PLAN.md).
Stages: `research → discovery → spec → build → test → feedback → maintain`.
Guard: `review-required` (James approves merges) or `autonomous-ok` (agents proceed, report after).

**Seeded 2026-07-06 from git activity — stages and next actions are drafts until James confirms.**

| Project | Stage | Guard | Next action | Blocked on James | Last activity |
|---|---|---|---|---|---|
| [portfolio-autopilot](projects/portfolio-autopilot.md) | build | autonomous-ok | Monitor headroom log; calibration surfaces reserve suggestions after 5+ snapshots | none | 2026-07-24 |
| [qip-qien](projects/qip-qien.md) | feedback | review-required | SEV-1 fix shipped (ee84254 pushed 2026-07-25); confirm trial health | none | 2026-07-25 |
| [congressional-trading](projects/congressional-trading.md) | test | autonomous-ok | Await threshold decision; weekly precision monitor running (Mondays 9am) | threshold decision | 2026-07-21 |
| [FindRefurbishedAppleProduct](projects/FindRefurbishedAppleProduct.md) | discovery | autonomous-ok | Confirm goal — brand new repo | confirm card | 2026-07-06 |
| [TradingBot25](projects/TradingBot25.md) | build | review-required | Confirm goal + current strategy state | confirm card | 2026-06-26 |
| [AgenticLever](projects/AgenticLever.md) | build | autonomous-ok | Confirm goal + next feature direction | confirm goal | 2026-07-20 |
| [pool-monitor](projects/pool-monitor.md) | maintain | autonomous-ok | None — steady state? confirm | confirm card | 2026-06-17 |
| [StageHand](projects/StageHand.md) | discovery | autonomous-ok | Await James answers on 5 toolchain questions; headless-browser gallery capture if Local Recs green-lit | voice, agent, posting, pricing, Local Recs y/n | 2026-07-21 |
| [MentorCore](projects/MentorCore.md) | research | autonomous-ok | Parked — no bandwidth | none | 2026-06-11 |
| [RoadSense](projects/RoadSense.md) | spec | autonomous-ok | Answer 5 launch questions → unblock implementation | auth model, city, GIS license, creator posture, metric | 2026-07-21 |
| [nanoclaw](projects/nanoclaw.md) | build | autonomous-ok | Run `pnpm test` + restart service + add `CLAUDE_PLANNING_MODEL=opus` to .env; then `git push origin main` | Studio restart | 2026-07-27 |
| [projects-monorepo-infra](projects/projects-monorepo-infra.md) | build | review-required | Name the token-spend-avoided metric | metric decision | 2026-07-16 |
| cortex *(no card yet)* | maintain | review-required | Decide: fill Reddit OAuth creds, or drop Reddit ingest (project's own docs call the API "increasingly restrictive") | Reddit creds / keep-or-drop call | 2026-07-12 |
| CodexBar *(no card yet)* | maintain | review-required | Decide upstream-sync strategy (1056 commits behind steipete/CodexBar); scrub steipete paths/bundle-ID from docs | sync strategy | 2026-07-12 |
| CodexBarLocal *(no card yet)* | maintain | review-required | 34 uncommitted changes incl. a stray Xcode scaffold — commit, stash, or discard | commit/stash/discard call | 2026-07-12 |
| CodexBar-ESP32 *(no card yet)* | maintain | autonomous-ok | None right now — mDNS doc gap fixed today; 3 minor observations items open (untested pace-formula dup, lock-ordering) | none | 2026-07-12 |
| localsky_radar *(no card yet)* | build | review-required | `git init` + first commit (2,227 lines, zero version control); rewrite CLAUDE.md (wrongly claims unimplemented) | confirm git-init | 2026-07-12 |

## Decision queue

Questions for James, appended by Nano; answered items move to the card's Notes and are removed here.

- **RoadSense launch questions (5):** auth model, generalization city, GIS license, creator posture, 30-day success metric. Sent via Telegram 2026-07-08.
- **nanoclaw push() fix deploy (2026-07-20):** Fix implemented (d4ef752), 135 tests pass. Awaiting Studio deploy. Design decisions made autonomously: (1) follow-up-turn UX = yes; (2) Claude safety net = yes; (3) tests = yes.

## Recently shipped

- **nanoclaw** 2026-07-27 — Phase 2 quota ledger committed (519ec56): per-rung JSON ledger with silent skip of exhausted REST rungs, 30-min backoff, `isQuotaError()` heuristic, auto-expiry, 12 new tests. Awaiting Studio deploy.
- **nanoclaw** 2026-07-27 — Phase 1.5 per-class model selection committed (8648c70): `CLAUDE_PLANNING_MODEL` env var routes planning tasks (autopilot cycles) to opus; base model (sonnet) used for tool-work and chat. `resolveClaudeModel()` exported + 4 unit tests (26–29). To activate: add `CLAUDE_PLANNING_MODEL=opus` to `.env` on Studio after deploy.
- **nanoclaw** 2026-07-27 — Phase 1 classifyTask committed (3aa7f25): `needsClaudeCode(): boolean` replaced with `classifyTask(): TaskClass` ('planning' | 'tool-work' | 'chat'); autopilot/portfolio `<task>` messages now classified as 'planning'; 3 new direct unit tests (23–25). TypeScript clean; awaiting Studio deploy.
- **nanoclaw** 2026-07-26 — Phase 0.6+1 committed (5f98659): Codex-first default provider, task/file/autopilot classifier fixes, 4 RED→GREEN tests; bundled pending Phase 0.6 routing + test-split changes. Awaiting Studio deploy.
- **qip-qien** 2026-07-25 — SEV-1 auth fix pushed to GitHub (ee84254): admin routes now use `requireAdminApiUser` on 4 endpoints; approved by James, pushed by Nano.
- **nanoclaw** 2026-07-26 — SqliteError delivery bug fixed (349ed2e): `writeOutboundDirect` now uses read-write opener; poll loops isolate per-session errors. TypeScript clean; awaiting Studio deploy + tests.
- **nanoclaw** 2026-07-25 — false-positive ceiling kills on Agent sub-agent runs fixed (0ac74eb): `decideStuckAction()` exempts non-Bash tools from the 30-min heartbeat ceiling when `tool_started_at < NON_BASH_TOOL_CEILING_MS` (90 min). Two new tests.
- **Portfolio audit** 2026-07-12 — parallel survey across 20 projects; auto-fixed 10 AGENTS.md/CLAUDE.md symmetry gaps, an mDNS doc gap (CodexBar-ESP32), a stale phase claim (MentorCore), a wrong-hostname ntfy-link bug (pool-monitor, verified via 76 passing tests), a test-isolation gap (AgenticLever, verified via 32 passing tests), and this file's own Tailscale hostname drift.
- **nanoclaw** 2026-07-08 — fix(router): mention-sticky Slack session scoping committed

-| [congressional-trading](projects/congressional-trading.md) | test | autonomous-ok | Await threshold decision; weekly precision monitor running (Mondays 9am) | threshold decision | 2026-07-21 |

+| [congressional-trading](projects/congressional-trading.md) | test | autonomous-ok | Monitor shadow precision at new 11.5/LOW/edge≥2 config; await go/no-go on live notifications | live-notify go/no-go | 2026-07-27 |


Add NON_BASH_TOOL_CEILING_MS and skip kill when non-Bash tool is in flight

Add RED tests for Agent-tool-in-flight ceiling behavior

Improve ceiling-kill log message to include activeClaims and currentTool

Update board and card, log run, schedule reset-wake


<message to="telegram-mg-17796">
Done — congressional-trading config updated (8a2f73e on feat/agentic-alert-loop):

- Score threshold: 9.0 → **11.5**
- Conviction gate: HIGH → **LOW** (LLM-tagged HIGH was making it worse)
- Edge factors: 3 → **2**

System stays in shadow mode; the new config just means the signals being logged now reflect the better threshold. Next question when you're ready: go/no-go on turning live notifications on?

On nanoclaw: proxy is still down so I couldn't push from the container. Code is already on your Studio — just run `git push origin main` from `~/projects/nanoclaw` when you get a chance, then `pnpm test` + restart + add `CLAUDE_PLANNING_MODEL=opus` to `.env`.
</message>


<context timezone="America/Chicago" />
<task from="telegram-mg-17796" time="Jul 27, 2026, 7:30 PM">Script output:
{
  "burnable_pct": 52.2,
  "tier": "high",
  "implementationModel": "claude-sonnet",
  "eligibleProjects": [
    {
      "project": "StageHand",
      "priority": "P1"
    },
    {
      "project": "nanoclaw",
      "priority": "P1"
    }
  ],
  "next_reset_at": "2026-08-01T20:59:59.499Z",
  "providerSnapshot": [
    {
      "name": "Claude",
      "tracked": true,
      "5h_remaining_pct": 100,
      "7d_remaining_pct": 63
    },
    {
      "name": "Codex",
      "tracked": true,
      "5h_remaining_pct": null,
      "7d_remaining_pct": 92
    },
    {
      "name": "Gemini",
      "tracked": false,
      "note": "$20/mo subscription — no CodexBar tracking, manually updated from gemini.google.com/usage"
    },
    {
      "name": "LM Studio",
      "tracked": false,
      "note": "local model server — always-available when running at lm_studio_url"
    }
  ],
  "claude_windows": {
    "5h": {
      "remaining_pct": 100,
      "resets_in_sec": -1
    },
    "7d": {
      "remaining_pct": 63,
      "resets_in_sec": 435598
    }
  }
}

Instructions:
Portfolio Autopilot run triggered. Trigger script has confirmed burnable quota and eligible projects exist.

Script `data` contains:
- `burnable_pct`: effective burnable quota (%)
- `tier`: quota tier (minimal/low/medium/high)
- `implementationModel`: model for implementation tasks
- `eligibleProjects`: [{project, priority}] sorted by priority — P1 first
- `next_reset_at`: ISO timestamp when the constraining window resets
- `providerSnapshot`: remaining % for all providers (Claude + Codex) for context
- `claude_windows`: { "5h": { remaining_pct, resets_in_sec }, "7d": { ... } }

## Your job this cycle

1. Read `/workspace/extra/second-brain/portfolio/BOARD.md` and the relevant `projects/*.md` cards for eligible projects.

2. Pick ONE unit of work — the highest-priority eligible project with a clear, actionable `Next action`. Skip anything where `Next action` is "confirm with James" or blocked on external input.

3. Determine task type and proceed accordingly:
   - **Greenfield** (new plan, new feature direction, new project direction): Plan only → auto red-team → send package (plan + critique + numbered open questions) to James via Slack (`slack-infrastructure`). Do NOT start implementation.
   - **Continuation** (`autonomous-ok` guard, clear next action): Implement directly. Report outcome to James after.
   - **review-required** (TradingBot25, qip-qien): Produce change + summary, send to James via Slack (`slack-infrastructure`) for merge approval.

4. Planning always uses Claude Sonnet regardless of `implementationModel`. Implementation uses whatever `implementationModel` says.

5. After completing work:
   - Update the project card's `Next action` and append a dated line to its `Notes`.
   - Update the `Last activity` date in `BOARD.md`.
   - Send James a brief 2–3 line outcome note to `slack-infrastructure`. Use Central Time (America/Chicago) for any timestamps — never UTC.
   - Log the run to `/workspace/agent/autopilot-run-log.jsonl` — one JSON line: `{ timestamp, tier, burnable_pct, project, task_type, model_used, outcome, notes }`.

6. **Schedule the next reset-wake**: After completing work, call `mcp__nanoclaw__schedule_task` with:
   - `processAfter`: the `next_reset_at` value from the trigger data (the exact window reset time)
   - `recurrence`: omit (one-shot)
   - `prompt`: "Portfolio Autopilot reset-wake: check quota and run one autopilot cycle. Read /workspace/agent/autopilot-config.json for context. Run node /workspace/agent/autopilot.mjs to see current state, then proceed as the autopilot prompt instructs."
   - `script`: `node /workspace/agent/autopilot-trigger.mjs 2>/dev/null`
   This ensures the next cycle starts exactly when the window resets, not up to 30 min later.

If no eligible project has a clear actionable next step, log it and send a brief note to `slack-infrastructure`: what's stalled and why. Use Central Time for any times mentioned.

Config at `/workspace/agent/autopilot-config.json` — thresholds and priorities live there.</task>