Back to articles
DocumentationSource: backend/agents/notes/mae_edge_autopsy_20260702.md

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:

  1. Is the label measuring real entry timing failure?
  2. Is the data behind MFE/MAE trustworthy?
  3. 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.1Rmisleading (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 blocks
  • pair_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_active
  • same_symbol_direction_window_limit
  • reject_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.