Codex Handoff: Pam Research Loop Strategy Fix
Status Update (as of 2026-03-28)
This file originally captured the March 17 failure state. That diagnosis was useful, but it is now partially obsolete. The strategy surface has since been improved and the handoff for the next iteration should start from the current reality on main.
What is already fixed on main
- Replay strategy recovered materially:
- score:
17.6(was-1.6) - hit rate:
0.8(was0.4) - action count:
10(was5) - avg realized bps:
110.0(was-40.0) - A replay-valid quantitative sleeve was added in
research/strategy.py: quantitative_eventful_reversal- it does not require
heuristic_direction - it uses
eventful_score, signedprice_move_bps, move magnitude, tradeability, and low live price research/candidate_review.pywas updated to mirror that sleeve and report live blocker diagnostics correctly.research/search.pywas reseeded around the new quantitative/eventful surface.research/autoresearch.pywas added and run; 50 local trials did not beat the current17.6replay baseline, so current params look like a local optimum.trade-candidatesno longer trusts stale gate data fromresearch/latest_state.json; it now uses current replay confidence.opportunity-scannow dedupes duplicate ticker rows before ranking.
Current persisted params
research/best_strategy_params.json currently represents the best known replay baseline:
{
"contrarian_enabled": false,
"economics_band_enabled": false,
"minimum_edge_bps": 100.0,
"politics_eventful_reversal_category": "Politics",
"politics_eventful_reversal_enabled": true,
"politics_eventful_reversal_max_live_price": 0.2,
"politics_eventful_reversal_min_abs_move_bps": 150.0,
"politics_eventful_reversal_min_tradeability": 0.9,
"politics_midprice_enabled": false,
"process_watch_enabled": false,
"quantitative_eventful_enabled": true,
"quantitative_eventful_max_live_price": 0.3,
"quantitative_eventful_min_abs_move_bps": 100.0,
"quantitative_eventful_min_eventful_score": 0.5,
"quantitative_eventful_min_tradeability": 0.8,
"quantitative_eventful_mode": "reversal"
}
Remaining issue
The replay strategy is now good enough to study, but the live operator path still does not produce deployable trades.
Current live situation after the latest fixes:
- python3 main.py opportunity-scan --limit 10 --json now returns unique tickers, but the scan still surfaces mostly watch rows.
- python3 main.py trade-candidates --limit 10 --json now shows review candidates instead of being suppressed by stale global gate data.
- There are still 0 deployable trades.
- The current global blocker is mostly stable_cycles<3.
- The leading local blocker on review candidates is live_edge_bps<100.0.
- The live heuristic still under-produces directional/actionable calls relative to what replay can support.
Next-phase task for Codex
- Read this file fully, but treat the March 17 diagnosis below as historical context, not the current state.
- Investigate the live heuristic path in the scanner/signal layer.
- Key file:
strategy/kalshi_opportunity_scanner.py- Understand whyopportunity-scanstill yields almost allwatchrows even after the replay-side quantitative sleeve started working. - Fix the live heuristic so the signal pipeline produces more directional/actionable calls.
- Goal: materially reduce the share of
neutral/watchscanner output. - Goal: maketrade-candidatessurface at least some realistic paths towarddeployable_trade, not justreview_candidate. - If the heuristic path is fundamentally too weak, add a quantitative-only live path that can bypass
heuristic_direction, analogous to how the replay-side quantitative sleeve bypasses it. - Re-run:
-
python3 main.py opportunity-scan --limit 20-python3 main.py trade-candidates --limit 20 --json - Verify whether any names become deployable. If not, document the exact remaining blocker mix and whether the bottleneck is: - live heuristic direction - live edge estimation - stable-cycles gate - sleeve matching / semantic mismatch
Key files for the next iteration
strategy/kalshi_opportunity_scanner.pyresearch/candidate_review.pyresearch/strategy.pyresearch/best_strategy_params.jsonmain.py
Branch note
The earlier handoff referenced codex/pam-research-loop, but that work has already been merged. The current source of truth is main. If you need an isolated branch for the next pass, branch from main.
Context
Pam’s research loop is stuck. The confidence gate correctly identifies that the current strategy doesn’t work. This handoff covers the diagnosis and three recommended fixes, in priority order.
Current State (as of 2026-03-17)
- Score: -1.6 (threshold: >= 5.0)
- Hit rate: 40% (threshold: >= 55%)
- Action count: 5 (threshold: >= 8)
- Unique sleeves: 1 (threshold: >= 2)
- Top sleeve share: 100% (threshold: <= 85%)
- Stable cycles: 0 (threshold: >= 3)
- Replay rows: 733 (sufficient data)
Root Cause Analysis
Problem 1: Signal starvation
16 of 18 live candidates have heuristic_direction=neutral. Every strategy sleeve in research/strategy.py gates on heuristic_direction before producing a buy signal. When 89% of inputs are neutral, the strategy can’t act.
Evidence: latest_state.json → candidate_review.summary.top_invalidation_reasons shows heuristic_direction=neutral as #1 invalidation reason (16 candidates).
Problem 2: Too few active sleeves
research/best_strategy_params.json has disabled 3 of 6 sleeves:
- contrarian_enabled: false
- process_watch_enabled: false
- politics_midprice_enabled: false
Only politics_eventful_reversal is firing (with threshold_review and economics_band enabled but rarely matching). This guarantees failure on unique_sleeves >= 2 and top_sleeve_share <= 0.85.
Problem 3: The active sleeve loses money
The 5 trades from politics_eventful_reversal averaged -40 bps realized. 40% hit rate. The contrarian logic (buying opposite to heuristic_direction on cheap politics markets after eventful moves) isn’t generating edge.
Recommended Fixes (priority order)
Fix 1: Add a quantitative sleeve that doesn’t require heuristic_direction
File: research/strategy.py
Location: Inside the decide() function, add a new sleeve after the existing ones.
Concept: A sleeve that trades on eventful_score + price movement alone, without needing a directional signal. For example:
- If eventful_score > 0.5 AND abs_move_bps > 150 AND live_price < 0.30
- Buy in the direction of the move (momentum) or against it (mean reversion)
- Test both approaches in replay
Why this helps: - Bypasses the signal starvation problem entirely - Adds a second unique sleeve (fixes diversity gates) - Increases action count (fixes action_count gate) - Uses data that’s already available in the observation dict
Implementation notes:
- The decide() function receives an obs dict. Check obs.get('eventful_score') availability in replay data — latest_state.json shows 50.8% of replay rows have eventful content.
- Add the sleeve name to the return tuple: return ("buy_yes", "quantitative_momentum") or similar
- Add corresponding params to DEFAULT_PARAMS dict at top of file
Fix 2: Re-enable disabled sleeves with relaxed parameters
File: research/best_strategy_params.json
The disabled sleeves were turned off based on ~5 total trades — far too small a sample for statistical significance. Re-enable them with slightly wider parameters:
{
"contrarian_enabled": true,
"contrarian_max_live_price": 0.05,
"process_watch_enabled": true,
"politics_midprice_enabled": true,
"politics_midprice_min_tradeability": 0.85,
"politics_eventful_reversal_enabled": true,
"politics_eventful_reversal_max_live_price": 0.20
}
Why this helps: - More sleeves firing = more actions = better statistical sample - Diversity gates (unique_sleeves, top_sleeve_share) start passing - The search loop can then properly evaluate which sleeves work
Fix 3: Investigate signal pipeline
Files: Look upstream of strategy.py for where heuristic_direction gets set.
- Check research/candidate_review.py — the _derive_heuristic_direction() or similar method
- Check research/prepare_replay.py — how replay rows get their heuristic_direction field
- Check core/signals.py or core/processing.py for the original signal generation
Question to answer: Why does 89% of the pipeline produce neutral? Is the heuristic too conservative? Is it missing input data? Is there a bug where a field isn’t being populated in replay rows?
This is the highest-impact fix long-term but may require more investigation. Fixes 1 and 2 are faster wins.
Success Criteria
After implementing these fixes, run the research loop:
cd /Users/joshua/Projects/Unicorn.Land/pam
source .venv/bin/activate
python3 main.py research-loop --until-confident
Target state: - Action count >= 8 - Unique sleeves >= 2 - Hit rate trending toward 55%+ - Score trending positive - Strategy should be firing on at least 2 different sleeve types
Key Files
| File | Purpose |
|---|---|
research/strategy.py |
The strategy surface — all trading logic lives here |
research/best_strategy_params.json |
Current tuned parameters |
research/continuous_loop.py |
The automated research lifecycle |
research/candidate_review.py |
Live market classification |
research/latest_state.json |
Current research state snapshot |
research/run_replay.py |
Replay evaluation harness |
research/prepare_replay.py |
Builds replay pairs from DB |
research/search.py |
Parameter grid search |
Branch
Current work is on codex/pam-research-loop. There are 15 modified files not yet committed — commit those first before making new changes.
Notes
- The confidence gate logic in
continuous_loop.pyis correct and should NOT be relaxed. The strategy needs to actually be good enough to pass it. - The replay harness (
run_replay.py) is frozen/deterministic — good for reproducible evaluation. - There’s an uncommitted
execute_trades.pyfile — leave it for now, it’s for later when the strategy passes confidence.
AutoResearch Pattern (recommended alongside the fixes above)
Inspired by Karpathy’s autoresearch repo. The core idea is dead simple: an AI agent modifies code, runs an experiment on a fixed time budget, checks a metric, keeps improvements and discards regressions, then repeats. You program the researcher, not the research.
This is a powerful reframe for Pam’s research loop, which currently has too many stages and gates between “try something” and “learn whether it worked.”
How to apply to Pam’s research loop
Map Pam onto the AutoResearch pattern with these choices:
| AutoResearch concept | Pam mapping |
|---|---|
| Single metric | composite_score = hit_rate * avg_realized_bps (or the existing confidence score — pick one number) |
| Single file to modify | research/best_strategy_params.json (the params that strategy.py reads) |
| Fixed evaluation budget | One full replay pass via run_replay.py (~30 seconds) |
| Keep-or-discard | Only persist params to best_strategy_params.json when score improves over baseline |
| Log everything | Append every trial (params, score, timestamp) to research/results.tsv |
The current continuous_loop.py pipeline is: backfill → prepare → replay → search → candidate review → confidence check. That’s six stages with multiple failure modes. AutoResearch says: just try things and measure. The confidence gates can be a graduation step AFTER you have a strategy that actually scores positive.
Concrete implementation: research/autoresearch.py
Create a single script that implements the loop:
#!/usr/bin/env python3
"""AutoResearch loop for Pam strategy params.
Pattern: mutate params → replay → compare to baseline → keep or discard → repeat.
Ref: github.com/karpathy/autoresearch
"""
import json, time, copy, random, csv, os
from pathlib import Path
from run_replay import run_replay # existing replay harness
PARAMS_FILE = Path("research/best_strategy_params.json")
RESULTS_FILE = Path("research/results.tsv")
def load_params():
return json.loads(PARAMS_FILE.read_text())
def save_params(params):
PARAMS_FILE.write_text(json.dumps(params, indent=2) + "\n")
def score(replay_result):
"""Single number: hit_rate * avg_realized_bps."""
hr = replay_result.get("hit_rate", 0)
bps = replay_result.get("avg_realized_bps", 0)
return hr * bps
def mutate(params):
"""Tweak one numeric param by ±10-30%."""
p = copy.deepcopy(params)
numeric_keys = [k for k, v in p.items() if isinstance(v, (int, float)) and not isinstance(v, bool)]
if not numeric_keys:
return p
key = random.choice(numeric_keys)
factor = random.uniform(0.7, 1.3)
p[key] = round(p[key] * factor, 6) if isinstance(p[key], float) else max(1, int(p[key] * factor))
return p
def log_trial(trial_num, params, result_score, kept):
"""Append to results.tsv."""
file_exists = RESULTS_FILE.exists()
with open(RESULTS_FILE, "a", newline="") as f:
w = csv.writer(f, delimiter="\t")
if not file_exists:
w.writerow(["trial", "timestamp", "score", "kept", "params"])
w.writerow([trial_num, time.strftime("%Y-%m-%dT%H:%M:%S"), result_score, kept, json.dumps(params)])
def main():
baseline_params = load_params()
baseline_result = run_replay(baseline_params)
baseline_score = score(baseline_result)
print(f"Baseline score: {baseline_score:.4f}")
trial = 0
while True:
trial += 1
candidate_params = mutate(baseline_params)
candidate_result = run_replay(candidate_params)
candidate_score = score(candidate_result)
kept = candidate_score > baseline_score
log_trial(trial, candidate_params, candidate_score, kept)
if kept:
print(f" Trial {trial}: {candidate_score:.4f} > {baseline_score:.4f} — KEEPING")
save_params(candidate_params)
baseline_params = candidate_params
baseline_score = candidate_score
else:
print(f" Trial {trial}: {candidate_score:.4f} <= {baseline_score:.4f} — discarding")
if __name__ == "__main__":
main()
This is a starting point — the run_replay import and score function will need to match Pam’s actual replay API. The key point is the structure: the entire loop is ~60 lines with zero ambiguity about what “better” means.
Key difference from the current approach
Current continuous_loop.py |
AutoResearch pattern |
|---|---|
| 6 stages (backfill, prepare, replay, search, candidate review, confidence) | 1 stage: mutate → evaluate → keep/discard |
| Multiple failure modes and intermediate state | Single failure mode: replay crashes (fix and retry) |
| Confidence gate blocks progress early | Confidence gate becomes a graduation check after positive score |
| Grid search explores params systematically | Random mutation explores params stochastically (simpler, surprisingly effective) |
Heavy orchestration in continuous_loop.py |
Self-contained script, runs until Ctrl+C |
The current approach isn’t wrong — it’s just front-loading quality gates before the strategy has had enough iterations to improve. AutoResearch inverts this: iterate fast with a single metric, graduate to quality gates once you have something worth gating.
What to tell Codex
When handing off to Codex, recommend the AutoResearch pattern as an alternative to debugging the existing continuous_loop.py pipeline:
Option A (existing fixes above): Fix signal starvation, re-enable sleeves, investigate heuristic_direction pipeline. Keep using
continuous_loop.py.Option B (AutoResearch pattern): Create
research/autoresearch.pyas described above. Bypass the multi-stage pipeline entirely. Iterate on params with a single score metric. Once the score is consistently positive, THEN run the confidence gate as a graduation check.Option B is faster to implement and more likely to find a working parameter set quickly. Option A addresses root causes that matter long-term. They’re complementary — do B first to get a working baseline, then A to understand why.