Trader posture re-evaluation · overdue since 2026-07-04

The aggressive research posture asked for breadth and configured depth

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.

1. The drawdown is entirely post-posture

The account was flat for six weeks before the switch, then lost 10.7% in three weeks of trading.

DateEquityGross/equityOpenNote
2026-05-08$99,9940.4%2start
2026-06-19$100,0090.4%1pre-posture, +0.02%
2026-06-21posture switched (b74412e)
2026-06-30$102,391173.1%10peak
2026-07-01$94,308200.6%11−7.61% in one day, at the Reg-T wall
2026-07-09$89,31356.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.

2. The aggressive caps never bound. The sizing caps did.

CapLimitPeak actualBinding?
max_positions_concurrent7511No — 15% utilized
max_pct_gross_exposure600%200.6%No — broker Reg-T ~2× is the real wall
max_pct_per_position25%26.2%Yes — every entry sized to the ceiling
buying-power checkDoes not exist

The smoking gun

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

Why the same config behaves oppositely on the index bots

max_notional_usd is an absolute dollar cap, so it does not scale with account size:

TierCap% of $100k (equity bot)% of $1M (index bots)
live_high$60,00060.0%6.0%
live_medium$45,00045.0%4.5%
live_low$30,00030.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.

3. Did the research rig actually produce learning signal?

This is the only question the posture should be judged on — its charter says "paper-trade variance is free, learning signal is sample-bound."

MetricPrePostRead
allowlist candidates/day2.039.40+363% — pick-side worked
low/long picks0102thin bucket filled
short picks proposed017newly sampled
shorts actually filled00rig never harvested them
trader fills (total)~1314no breadth gain
too_few_samples share79.8%85.8%wrong direction
weight_tuner samples468700already cleared min_picks=15 before the change
Brier (90d)0.25550.2512flat, still worse than coinflip
Confound, stated plainly. The post window is contaminated twice over: the 2026-07-03 home reorg broke the venv and killed the crons for six days, and the IEX spread gate blocked essentially every candidate for the whole window. So "only 14 fills" is substantially a deployment failure, not proof the design fails. What survives the confound is the nine sessions of real trading (06-22 → 07-02), during which the account lost 7.9% on 14 fills at 200% gross — and the structural fact below.

The structural fact

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.

4. P0 The stop-bracket escalation is dead code

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.

5. Recommendation

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.

Keep the knobs that worked

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.

Change the sizing knobs

SettingNowProposedWhy
equity bot — config/trader.yaml paper_overrides
max_notional_usd.live_high$60,000$5,0005% 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_position25%5%the cap that actually bound
risk_per_trade_pct18%3%
max_pct_gross_exposure600%150%own gate binds before the broker's
index bots — trader.{nasdaq100,sp500}.yaml
max_notional_usd.live_high$60,000$25,0002.5% of $1M → 60 names fit
max_pct_gross_exposure600%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.

Also fix

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.

6. What I could not determine

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.