Trader posture re-evaluation · overdue since 2026-07-04
Verdict on strategy_version: aggressive-research-2026-06-21, dad's $100k equity paper trader, plus first-day exposure for the two $1M index bots.
Verdict: half the posture worked, half caused the entire drawdown, and the two halves are separable.
The pick-side knobs (wider confidence buckets, more allowlist rows, shorts routed live) delivered a +363% increase in candidates and filled buckets that were previously empty. The execution-side knobs (80× notional cap, 18% risk/trade, 25% max position) produced 14 fills, drove the book to 200.6% gross, and account for 100% of the −10.7%. The learning loop does not consume fills, so that risk bought almost nothing.
The account was flat for six weeks before the switch, then lost 10.7% in three weeks of trading.
| Date | Equity | Gross/equity | Open | Note |
|---|---|---|---|---|
| 2026-05-08 | $99,994 | 0.4% | 2 | start |
| 2026-06-19 | $100,009 | 0.4% | 1 | pre-posture, +0.02% |
| 2026-06-21 | — | — | — | posture switched (b74412e) |
| 2026-06-30 | $102,391 | 173.1% | 10 | peak |
| 2026-07-01 | $94,308 | 200.6% | 11 | −7.61% in one day, at the Reg-T wall |
| 2026-07-09 | $89,313 | 56.8% | 3 | −10.70% during posture |
Realized P&L is roughly −$11,507; the three still-open positions are net +$820. The loss is closed trades, not bad marks. The 07-01 crash was spread across AMD, CAT, GEV, KLAC, TER and LRCX simultaneously — death by a thousand cuts at 2× leverage, not a single blow-up.
| Cap | Limit | Peak actual | Binding? |
|---|---|---|---|
| max_positions_concurrent | 75 | 11 | No — 15% utilized |
| max_pct_gross_exposure | 600% | 200.6% | No — broker Reg-T ~2× is the real wall |
| max_pct_per_position | 25% | 26.2% | Yes — every entry sized to the ceiling |
| buying-power check | — | — | Does not exist |
Commit b74412e, titled "aggressive research posture — maximise paper fills incl. losers", is the same commit that raised max_notional_usd.live_high from $750 to $60,000 — an 80× jump on a $100k account.
At $750/name the bot could hold ~130 names at 1× leverage. That is "count > leverage." After the change, sizing falls through to the 25% percentage cap → $25k/name → ~8 names → the Reg-T wall. The one knob meant to buy breadth is the knob that destroyed it.
From the same config block, four lines above the change: "the real lever for more trades is COUNT + turnover … not higher leverage. Don't raise this expecting more fills." — config/trader.yaml:147
max_notional_usd is an absolute dollar cap, so it does not scale with account size:
| Tier | Cap | % of $100k (equity bot) | % of $1M (index bots) |
|---|---|---|---|
| live_high | $60,000 | 60.0% | 6.0% |
| live_medium | $45,000 | 45.0% | 4.5% |
| live_low | $30,000 | 30.0% | 3.0% |
On the equity bot the percentage cap binds and yields ~8 huge names. On the index bots the dollar cap binds and yields up to 60 small ones. Identical config, opposite strategy.
This is the only question the posture should be judged on — its charter says "paper-trade variance is free, learning signal is sample-bound."
| Metric | Pre | Post | Read |
|---|---|---|---|
| allowlist candidates/day | 2.03 | 9.40 | +363% — pick-side worked |
| low/long picks | 0 | 102 | thin bucket filled |
| short picks proposed | 0 | 17 | newly sampled |
| shorts actually filled | 0 | 0 | rig never harvested them |
| trader fills (total) | ~13 | 14 | no breadth gain |
| too_few_samples share | 79.8% | 85.8% | wrong direction |
| weight_tuner samples | 468 | 700 | already cleared min_picks=15 before the change |
| Brier (90d) | 0.2555 | 0.2512 | flat, still worse than coinflip |
src/pick_grader.py grades picks from fetch_ohlcv_batch market data. It never reads fills.jsonl or orders.jsonl. Neither does calibration.py, learning_grader.py, or weight_tuner.py. The closed-loop learning signal does not depend on the trader filling anything. The incremental value of actually executing is execution realism — slippage lessons, real stop behaviour — which is genuine but produced 24 fills in the system's entire lifetime.
Leverage was therefore spent on a hypothesis the architecture does not support.
src/trader/loop.py:697 falls back to a single-leg stop when the OCO submit raises BrokerError, wrapped in except Exception as stop_exc which logs CRITICAL "position is LIVE WITHOUT PROTECTION" and fires a Telegram alert.
But _submit_bracket (loop.py:750-754) catches BrokerError itself, logs a warning, and returns. So for the realistic failure — the broker rejecting both orders — the escalation can never fire.
Result: two WARNING lines, no alert, and a filled, margined position with no stop, exiting only via manage_until (14–60 days). A manual repair exists (scripts/patch_position_brackets.py) but nothing detects the condition or tells anyone to run it. With 4× margin and no drawdown kill-switch, this is the only path to an unbounded loss.
Not "be less aggressive." Be aggressive in the dimension the charter actually asked for — count — and stop paying for leverage the learning loop cannot use.
picker.max_picks: 60 · allowed_confidences: [high, medium, low] · max_per_sector: 4 · shorts routed to the live tier · min_price_usd: 1.0 · min_avg_volume: 100000. These produced the +363% candidate breadth and the newly-sampled thin buckets. This is the research rig doing its job.
| Setting | Now | Proposed | Why |
|---|---|---|---|
| equity bot — config/trader.yaml paper_overrides | |||
| max_notional_usd.live_high | $60,000 | $5,000 | 5% of equity, not 60% |
| max_notional_usd.live_medium | $45,000 | $3,500 | |
| max_notional_usd.live_low | $30,000 | $2,500 | |
| max_pct_per_position | 25% | 5% | the cap that actually bound |
| risk_per_trade_pct | 18% | 3% | |
| max_pct_gross_exposure | 600% | 150% | own gate binds before the broker's |
| index bots — trader.{nasdaq100,sp500}.yaml | |||
| max_notional_usd.live_high | $60,000 | $25,000 | 2.5% of $1M → 60 names fit |
| max_pct_gross_exposure | 600% | 150% | real BP unknown (2× or 4×); bind below the lower bound |
Equity bot outcome: ~30 concurrent names at $5k each = 150% gross. Roughly 3× the breadth, one-fifth the per-name risk, and more fills per week — which is what the charter wanted. This is not risk-aversion; a 600% gross cap is fictional, because the broker rejects above ~200%. It hides the binding constraint rather than expressing one.
P0. The dead-code stop escalation above.
P1. Account.buying_power is fetched at broker.py:555 and never enters TraderState or any gate. No gate checks real buying power.
P1. Zero shorts have ever filled despite 17 proposed — the rig is not harvesting the failures it was built to harvest. Worth a root-cause.
P2. Journal rotation stopped after 2026-07-02, so stop-discipline over the worst week is unverifiable. That audit gap is itself a finding.
Whether the index paper accounts carry 2× or 4× buying power. The equity account demonstrably walled at 200.6%; project memory records 4× for the new accounts. Nothing on disk settles it, and I did not call the broker API. The proposed 150% gross cap is chosen to be safe under either.
Whether stops fired as designed during 07-03 → 07-09. trader_state.json drops closed rows and journal rotation had stopped. Positions did fall 9 → 3 with cash recovering from −$73.5k to +$38.6k, which is consistent with broker-side OCO brackets executing while the bot was down — but that is inference, not proof.