AUTHOR: Tony Mudau
MAE Edge Autopsy — What the Numbers Actually Said
Date: 2026-07-02
Sources: mae_analytics_20260702_052945.xlsx, MT5 closed-trade export (Jun 2–10), live trade_history
Implementation: fx_sessions.py, trade_fingerprint_guard.py, technical + risk + orchestration gates
This note documents a forensic pass over Maximum Adverse/Favorable Excursion (MAE/MFE) analytics — not because the dashboard looked scary, but because half the workbook said "Terrible entry" and we needed to know whether the bot was trading badly or measuring badly.
Spoiler: both, but fixable.
The question we started with
Why do we have so many terrible entries?
The Excel export's Entry Quality column looked like a graveyard. Before changing strategy, we had to answer:
- Is the label measuring real entry timing failure?
- Is the data behind MFE/MAE trustworthy?
- Are specific pair + direction + session combos systematically bleeding edge?
How "Entry Quality" is defined (read this first)
Entry Quality is not about MAE. It does not mean "you lost" or "price went against you."
It comes from Entry Efficiency (R):
Entry Efficiency (R) = MFE (pips) ÷ SL distance (pips)
| Label | Threshold | Meaning |
|---|---|---|
| Terrible entry | < 0.2R |
Price barely moved in your favor relative to stop distance |
| Weak entry | 0.2 – 0.5R |
Some favorable move, but thin |
| Reasonable entry | 0.5 – 1.0R |
Decent opportunity before outcome |
| Strong entry | > 1.0R |
Meaningful favorable excursion vs risk |
Implementation: entry_efficiency_r() / entry_efficiency_label() in excursion_tracking.py.
Intuition: A 42-pip stop with only 4 pips of favorable movement is 0.10R → Terrible — even if you eventually scratch out a tiny win.
Workbook snapshot (91 exported rows)
| Entry Quality | Count | Win rate | Avg PnL |
|---|---|---|---|
| Terrible entry | 44 (48%) | 16% | -$3.14 |
| Strong entry | 13 | 77% | +$2.39 |
| Reasonable entry | 17 | 71% | +$0.68 |
| Weak entry | 17 | 59% | +$0.62 |
Summary sheet (100 closed trades):
- Win rate: 44%
- Avg MAE: 86.2 pips vs Avg SL: 36.8 pips → stops often inside noise
- 33% of trades had MAE ≥ SL
- Avg Entry Efficiency: 3.1R ← misleading (median in rows was 0.20R)
When the label works and MFE is recorded correctly, it correlates with outcomes. When MFE is missing or corrupted, Terrible becomes a catch-all.
Finding #1 — Two-thirds of "Terrible" is a measurement problem
29 of 44 Terrible rows had MFE = 0.0.
That alone forces Entry Efficiency = 0 → Terrible, regardless of what actually happened.
Smoking guns
| Trade | Outcome | Real move | Stored MFE | Label |
|---|---|---|---|---|
| EURJPY #13 | Win +$12.02 | ~37.7 pips favorable | 0.0 | Terrible |
| EURAUD #12 | Win +$7.24 | profitable sell | 0.0 | Terrible |
These are wins labeled as terrible entries. The metric is right in theory; the excursion tracker never got the data.
Why MFE goes missing
| Close reason | Terrible count |
|---|---|
mt5_history_sync |
33 |
major_news_risk |
6 |
llm_exit_management |
3 |
Most closes went through MT5 history sync rather than live tick-by-tick excursion updates. The repair job only backfills when mae_pips = 0, not when mfe_pips = 0 — so asymmetric gaps persist.
Bar backfill landmine
When the trade's OHLCV time window is empty, backfill falls back to the last 32 bars unrelated to the trade:
if window.empty:
window = df.tail(min(32, len(df)))
That produced absurd rows like:
| Trade | Actual adverse move | Stored MAE |
|---|---|---|
| GBPUSD buy (15 min) | ~3.3 pips | 255 pips |
| USDZAR sell | ~223 pips (wide SL) | 1,911 pips |
| EURUSD sell (win) | ~5.6 pips favorable | 382 pips MFE |
Takeaway: Treat Summary Avg MAE 86 pips and Avg Entry Eff 3.1R with skepticism on older mt5_history_sync rows. Use median efficiency and eyeball individual trades.
Finding #2 — When the data is real, Terrible means "never developed"
15 Terrible rows had MFE > 0 but still below 0.2R — genuine timing failure.
The clearest repeat offender:
USDCAD buys — wide stop, no follow-through
| Trade | MFE | SL | Efficiency | Outcome |
|---|---|---|---|---|
| #102 | 4.3 | 42.6 | 0.10R | Loss |
| #97 | 4.4 | 42.6 | 0.10R | Win (+$0.14) |
| #84 | 0.5 | 42.5 | 0.01R | Win (+$0.01) |
| #81 | 4.8 | 42.5 | 0.11R | Win (+$0.28) |
Same story four times: ~42-pip stop, never more than ~5 pips favorable. The market never confirmed the idea.
Range-position columns did not show chasing (mostly Mid-range / Pullback). Failure happened after entry on the price path.
Finding #3 — EURAUD sells were a structured leak
All 9 Terrible EURAUD rows were sells. Eight lost; one won with MFE = 0.
The duplicate blast (May 4)
Four sells opened within seconds of each other:
| ID | Entry | PnL | Opened |
|---|---|---|---|
| 10 | 1.63122 | -$7.01 | 18:31:09 |
| 9 | 1.63127 | -$7.01 | 18:31:23 |
| 7 | 1.63131 | -$7.01 | 18:31:28 |
| 8 | 1.63131 | -$7.01 | 18:31:28 |
Same idea, 4× risk, identical stop-out. The old fingerprint guard keyed on exact entry price — different fills slipped through.
MT5 history (Jun 2–10) — different picture for EURAUD
In the broker export for early June, EURAUD trades were buys (+$2.69, +$2.65). No EURAUD sells in that window. The pair isn't toxic in all directions — the sell setup was the leak.
Fix applied: hard block EURAUD sells via PAIR_BLOCKED_DIRECTIONS in fx_sessions.py.
Finding #4 — MT5 P&L story: stops kill, client exits survive
From 16 MT5 closed trades (Jun 2–10):
| Metric | Value |
|---|---|
| Net P&L | +$6.27 |
| Win rate | 75% (12W / 4L) |
| Avg win | +$4.45 |
| Avg loss | -$9.98 |
Every loss hit the broker stop
| Symbol | Dir | P&L | Closed by | Session hint |
|---|---|---|---|---|
| AUDUSD | sell | -$7.08 | Stop loss | ~03:00 UTC (Tokyo) |
| AUDJPY | sell | -$6.93 | Stop loss | ~03:00 UTC (Tokyo) |
| EURUSD | sell | -$12.66 | Stop loss | Jun 5 morning |
| USDJPY | buy | -$13.23 | Stop loss | overnight |
Client-managed exits: 11 wins, 1 small loss (-$0.94).
Stop losses: 0 wins, 4 losses.
The agent exits well when it manages the trade. Full SL hits are where edge goes to die — often in thin or wrong sessions.
EURUSD sell stacking (Jun 3)
Three sells opened within minutes (12:02, 12:05, 12:05 UTC), all closed together at 14:33 — +$3.99, +$4.32, +$6.12. Lucky outcome, bad process: stacked exposure on one thesis.
What we changed (Jul 2, 2026)
1. Pair + direction blocks (fx_sessions.py)
PAIR_BLOCKED_DIRECTIONS = {
"EURAUD": frozenset({"sell"}),
}
PAIR_BLOCKED_DIRECTION_SESSIONS = {
"AUDUSD": {"sell": frozenset({"tokyo"})},
"AUDJPY": {"sell": frozenset({"tokyo"})},
}
Gates:
pair_direction_allows_trade()— unconditional direction blockspair_direction_session_allows_trade()— session-specific direction blocks
Wired in technical_agent (proposal time) and risk_agent (sizing time).
2. Direction slot duplicate guard (trade_fingerprint_guard.py)
Old guard: same symbol + direction + rounded entry price → missed stacks at 1.16185 / 1.16135 / 1.16096.
New guard:
| Layer | Behavior |
|---|---|
| Direction slot | One in-flight reservation per pair+direction (6-hour TTL) |
| DB window | Max 1 open in trade_history per pair+direction per 6-hour window |
| Price fingerprint | Still blocks identical entry price retries |
Helper: same_direction_entry_blocked()
Enforced in: _entry_duplication_guard() (orchestration) + execute_trade() (execution agent).
New skip reasons in logs:
same_symbol_direction_slot_activesame_symbol_direction_window_limitreject_pair_direction_session_blocked
Gate flow (where trades die now)
run_cycle candidate
│
▼
technical_agent
├─ direction resolved
├─ pair_direction_allows_trade ← EURAUD sell blocked
├─ pair_direction_session_allows ← AUDUSD/AUDJPY sell @ tokyo blocked
├─ pair_session_allows_trade ← pair avoid sessions
└─ proposal built
│
▼
orchestration
└─ _entry_duplication_guard ← direction slot + window limit
│
▼
risk_agent
├─ pair direction / session recheck
└─ max_same_symbol_direction = 1
│
▼
execution_agent
└─ try_acquire_direction_slot ← atomic at broker send
Patterns worth remembering
1. Label ≠ truth when MFE = 0
Before tuning strategy from Excel, filter:
MFE = 0 → likely data gap, not necessarily bad entry
MFE > 0 and Eff < 0.2R → real timing failure
2. Cross pairs + Asia + short side = caution
EURAUD sells, AUDUSD/AUDJPY sells in Tokyo — both legs or commodity FX without London liquidity. The fix is session-aware direction blocking, not abandoning the pair entirely.
3. Stacked entries multiply regret
Duplicate guards must key on direction, not price. One thesis → one ticket.
4. Stop loss vs client close is the real P&L divider
Improve entry timing and avoid putting stops inside session noise — but the MT5 export shows exit path matters as much as entry label.
Open work (not yet implemented)
These came out of the same investigation; track separately:
| Item | Why |
|---|---|
Backfill when mfe_pips = 0 |
Repair job ignores half the Terrible labels |
Remove df.tail(32) backfill fallback |
Inflates MAE/MFE on synced trades |
finalize_trade_metrics_before_close on all close paths |
Including mt5_history_sync |
Exclude major_news_risk from entry-quality scoring |
Defensive flat ≠ bad entry |
| USDCAD 42-pip SL / 4-pip MFE pattern | Real timing issue; needs SL or entry logic review |
Files to read
| File | Role |
|---|---|
excursion_tracking.py |
MAE/MFE tracking, entry efficiency labels, Excel export |
entry_quality_metrics.py |
Range position, bars-since-signal for Excel columns |
fx_sessions.py |
Session + pair + direction gates |
trade_fingerprint_guard.py |
Direction slot + duplicate guard |
orchestration_agent/main.py |
_entry_duplication_guard() |
technical_agent/main.py |
Proposal-time session/direction gates |
risk_agent/main.py |
Second-layer direction/session block |
Related notes: SESSION_AWARENESS.md, trade_failure_analysis.md, TRADE_LIFECYCLE.md.
One-line summary
We looked terrible on paper because MFE was often zero, but the live leaks were real: EURAUD sells, Asia AUD shorts, and stacked same-direction entries. The bot now blocks those setups and refuses to stack multiple tickets on one morning idea — measure the rest after excursion data is fixed.