AUTHOR: Tony Mudau
Trade Discipline Rules — System Hardening (April 25 2026)
Purpose: This document explains the filter rules added to technical_agent and
orchestration_agent on April 25 2026. It is written for someone who understands
basic trading concepts but is still learning how algorithmic systems translate market
intuition into code.
The Core Problem We Solved
Before this change, the system worked like a competition with no entry requirements.
Every symbol was scored, and the symbol with the highest score was automatically selected — even if all scores were terrible. There was always a "winner", even when the market offered nothing worth trading.
The analogy: imagine a tennis tournament where the rules say someone must win even if all players are injured. The result is a bad match. Our system was doing the same thing with trades.
Before (broken)
Loop symbols → Score each one → Pick highest score → Execute
After (fixed)
Loop symbols → FILTER (must pass hard rules) → Score survivors → Pick highest → Execute
↑
Most cycles end here with "no_trade" when conditions are poor
This is the difference between a ranking system and a filter-then-rank system.
Financial Concepts You Need to Know First
Volatility
Volatility is how much a price moves over a period. High volatility = big swings. Low volatility = price barely moving.
- Why it matters for trading: Trend strategies need movement to work. If a market is frozen, there is no trend to follow. Entering a trend trade in a frozen market is like trying to surf when there are no waves.
ATR (Average True Range)
A technical indicator that measures average price movement per candle. It is the most common way to measure short-term volatility.
- Low ATR → market is quiet → low volatility
- High ATR → market is active → high volatility
Bollinger Band Width
Bollinger Bands are two lines drawn around price, spaced by standard deviation. When the bands are narrow (squeezed), the market is in compression — consolidating with no clear direction. When they expand, the market is breaking out.
Volatility Regime
The system classifies the market into three regimes based on ATR percentile and Bollinger Band Width percentile:
| Regime | Meaning | Trade implication |
|---|---|---|
expansion |
Market is moving strongly, above the 82nd percentile | Good for trend trades |
normal |
Market moving at average pace | Neutral |
compression |
Market very quiet, below the 25th percentile | Avoid trend trades; only mean-reversion is suitable |
Strategy Modes
The system chooses one of three strategies per symbol per cycle:
| Mode | What it does | Best in |
|---|---|---|
trend |
Follows the existing direction — buys in an uptrend, sells in a downtrend | Expansion / normal volatility |
htf_pullback |
Enters a short-term counter-move within a larger trend (buy a dip in an uptrend) | Normal volatility, when higher TF trend is clear |
mean_reversion |
Fades extremes — buys when oversold, sells when overbought | Compression / quiet markets |
EMA Trend Strength
EMA 50 and EMA 200 are moving averages over 50 and 200 candles respectively. When they are far apart, it confirms a strong trend. When they are very close together, the market has no direction.
trend_strength = |EMA_50 - EMA_200| / price
A value below 0.0008 (0.08% of price) means the two averages are almost touching —
no meaningful trend exists.
RSI (Relative Strength Index)
RSI measures whether a market is overbought (above 70) or oversold (below 30). These are "exhaustion zones" — the market has moved so far that a reversal is likely.
- Buying into an overbought market is chasing the top of a move that is about to reverse — a classic mistake.
- Selling into an oversold market is shorting the bottom of a move that is about to bounce.
Correlation Pressure
If the portfolio already holds multiple trades in the same underlying currency, adding
another one increases concentration risk. All those positions will win or lose
together. This is measured as same_side_pressure — the combined lot exposure on the
same side across correlated pairs.
Trading Sessions
Forex markets have three main sessions: Asia, London, and New York (NY). Each has different liquidity and volatility characteristics.
- The most volatile and highest-volume time is London/NY overlap (afternoon European time).
- Late New York (especially Fridays after 3 PM NY time) is a "dead zone" — traders are closing positions for the week, volume collapses, and false moves are common. Entering trends in this window is a recurring cause of losses.
Confidence Score
A number from 0.0 to 1.0 that summarises how aligned all the signals are. Higher is better. The formula blends pattern win-rate, trend alignment, MTF alignment, trend strength, and volatility adjustments.
The Rules — What They Do and Why
RULE 1 — Block Trend/HTF-Pullback in Volatility Compression
In code (technical_agent):
if vol_regime == "compression" and strategy_mode != "mean_reversion":
return None
What it means:
When the market is in compression (Bollinger Bands squeezed, ATR at its lowest percentile), the system used to still attempt trend trades. This is fundamentally wrong.
A trend trade requires directional price movement. In compression, price is coiling — it moves sideways with no conviction. Entering a trend trade here is like pressing the accelerator when the engine is off.
The only valid strategy during compression is mean-reversion — fading the small oscillations within the tight range. Everything else gets rejected.
Learner analogy: Imagine trying to ride a wave on a lake with no wind. The wave is not there. You sit down and wait.
RULE 2 — Block Trend Trades with No EMA Momentum
In code (technical_agent):
if strategy_mode == "trend" and trend_strength < 0.0008:
return None
What it means:
Even if volatility looks normal, the EMA spread could be tiny — meaning the short-term and long-term moving averages are almost identical. This means the market has no confirmed directional bias at the macro level.
Trend following works because the market has inertia — a ball rolling downhill keeps rolling. If the ball is barely moving, there is no inertia to follow.
0.0008 means EMA 50 and EMA 200 are less than 0.08% apart. At this level, the
"trend" signal is statistical noise, not a real trend.
RULE 4 — Hard Minimum Confidence (0.62 Floor)
In code (technical_agent):
if confidence < 0.62:
return None
What it means:
Before this fix, the confidence formula could produce values like 0.58 or 0.60, and those signals would still be passed to the orchestration agent. The orchestration agent would score them and potentially execute them because they were the "least bad" option.
The problem: 0.60 confidence does not mean "a good trade". It means "slightly better than a coin flip".
By adding a hard floor at 0.62 inside the technical agent itself, we prevent mediocre signals from even reaching the scoring stage. The signal is killed at the source.
Why 0.62? The confidence formula starts at a base of 0.42, then gets lifted by pattern win-rate, alignment, trend strength, and MTF alignment. Getting above 0.62 requires meaningful confirmation from multiple indicators simultaneously — not just one or two.
Learner analogy: You don't just need green lights — you need all the important ones to be green at the same time.
RULE 5 — Block Trend Trades in Low Volatility Markets
In code (technical_agent):
if context_volatility == "low" and strategy_mode == "trend":
return None
What it means:
context_volatility comes from the ATR ratio (ATR divided by price). When this ratio
is in the lowest bucket ("low"), the market's daily range is tiny. There is not enough
movement to:
- Justify the spread cost of entering a trade
- Reach a meaningful take-profit target before reversing
- Show a clean trend signal
This rule is complementary to RULE 1. RULE 1 catches compression at the percentile/regime level. RULE 5 catches low ATR ratio at the absolute level. Together they provide two independent checks for the same underlying problem: the market is not moving.
RULE 6 — Block New York Session Dead Zone Trades
In code (technical_agent):
if session_label == "new_york" and vol_regime != "expansion" and trend_strength < 0.001:
return None
What it means:
The New York session is not uniformly active. The first half (9:30 AM – 1 PM NY time) overlaps with London and is the highest-volume period. The second half, and especially Friday afternoons, is the worst time to enter new trend trades:
- Institutional desks are closing positions for the week
- Volume drops sharply
- Price moves are choppy and random — fake breakouts are common
- Spreads can widen
This rule only fires when all three conditions align:
- We are in the New York session
- The regime is not showing true expansion (no strong directional move)
- EMA trend strength is below 0.001 (barely any trend)
When all three are true, the system refuses to enter. It waits.
This directly addresses a real observation: the previous system was noted to be losing money on Friday NY session trades in quiet markets. This rule stops that pattern.
RULE 7 — No Market Orders in Dead Markets
In code (technical_agent):
if execution_type == "market" and context_volatility == "low":
return None
What it means:
There are two ways to enter a trade:
- Market order: Enter immediately at the current price (whatever it is)
- Pending order (limit/stop): Place an order at a specific price level, and only enter if price reaches that level
In a low volatility market, a market order is dangerous because:
- The spread (difference between buy and sell price) is a proportionally larger cost relative to the tiny expected move
- There is no "thrust" to carry the trade toward profit quickly
- Price may drift back immediately, stopping you out at a loss
Pending orders are safer in quiet markets because they require price to move intentionally to your entry level — which filters out dead, drifting markets naturally.
This rule says: never submit an immediate market order when the market is quiet. If the entry type is a limit or stop order, that is fine — the pending order will simply not be filled if the market doesn't move.
RULE 8 — Stricter Correlation Block (2.0 instead of 3.4)
In code (technical_agent → correlation_overlay):
# Changed from:
if same_side_pressure >= 3.4:
block = True
# To:
if same_side_pressure >= 2.0:
block = True
What it means:
In forex, many currency pairs share the same base or quote currency. For example:
EURUSDandGBPUSDboth use USD as the quote. If USD weakens, both pairs go up.- Holding 1 lot BUY on EURUSD and 1 lot BUY on GBPUSD is not diversification — it is a doubled USD bet.
same_side_pressure is the measure of how much total correlated exposure the portfolio
already has on the intended side.
Previously the block only triggered at 3.4 — meaning the system was allowing very
concentrated correlated positions before refusing. This was a significant risk
concentration issue.
Lowering the block to 2.0 means: once the combined exposure on the same side exceeds
2 lots of equivalent pressure, no new correlated trades are added. This enforces
proper position diversification.
Orchestration Agent Changes
The orchestration agent is the "conductor" — it loops through candidate symbols, collects signals from the technical agent, scores each one, and selects a trade.
The problem was: it always selected something.
Hard Quality Gate — Before Scoring
if not technical_signal or direction not in {"buy", "sell"}:
rejection = "no_direction"
elif confidence < 0.65:
rejection = "low_confidence"
elif strategy_mode == "mean_reversion":
rejection = "mean_reversion_blocked"
elif vol_regime == "compression":
rejection = "low_volatility_compression"
What it means:
Before any scoring happens, each symbol must pass four checks:
- Has a direction — the signal must say BUY or SELL. No direction = no trade.
- Confidence ≥ 0.65 — higher bar than the technical agent's 0.62, because even a signal that squeaked past the technical filter must prove itself again at the orchestration level. This is double-verification.
- Not mean-reversion — the orchestration agent makes a blanket decision: no mean-reversion trades. Mean-reversion is a legitimate strategy but it requires different risk sizing and timing than what this system's execution pipeline is calibrated for. Rather than execute it incorrectly, it is blocked entirely.
- Not compression regime — catches any compression-regime signals that somehow passed the technical agent filter (defensive programming).
Rejected symbols are stored in filtered_out_symbols with their rejection reason,
visible in every cycle trace and the Telegram notification.
Minimum Confidence Raised: 0.52 → 0.65
Even after passing the quality gate, a proposal is only allowed into the selection competition if its effective confidence (after MTF and reversal adjustments) is at or above 0.65.
Previously the bar was 0.52 — this was dangerously low. A 52% confidence trade is barely better than random. The new 65% threshold means the combined signal alignment must be genuinely meaningful.
Minimum Combined Score Gate: 0.6
After all signals are combined into a weighted score (technical confidence, market sentiment, precheck conditions, MTF alignment), if the combined score is below 0.6, the symbol is filtered out even if it has a proposal.
combined_score = (tech_conf × 0.46) + (market_conf × 0.2) + (precheck × 0.14) + (mtf × 0.2) + alignment_bonus
This prevents a symbol with a strong technical signal but terrible precheck conditions (wide spread, no liquidity, poor timing) from being selected.
The combined score ensures holistic quality — not just technical quality.
Execution Guard — Last Line of Defence
if proposal.confidence < 0.65:
proposal = None # block execution
Even after the portfolio agent and risk agent have shaped the proposal, if the confidence has been adjusted down below 0.65, the trade is not executed. This prevents cases where portfolio or risk adjustments reduce confidence but the system still proceeds.
Think of this as the bouncer at the door — even if you got through all the previous checks, you still need to show your credentials at the entrance.
No-Trade Return — Explicit System Decision
When no symbol survives all the filters, the system now explicitly returns:
{
"status": "no_trade",
"reason": "No valid high-quality setups found",
"filtered_out_symbols": [
{"symbol": "EURUSD", "rejected": true, "reason": "low_confidence"},
{"symbol": "GBPUSD", "rejected": true, "reason": "low_volatility_compression"},
{"symbol": "USDJPY", "rejected": true, "reason": "mean_reversion_blocked"}
],
"final_candidate_count": 0
}
This is a first-class decision, not an error or a fallback. The system chose not to trade, which is the correct choice when conditions are poor.
The Telegram notification also reports which symbols were filtered and why, so a human can verify the system's reasoning without reading logs.
The Big Picture — Why This Matters Financially
Before: Slow Bleed
The old system made many low-quality trades. Each one had a small edge at best, or a random outcome at worst. Over time:
- Win rate hovers around 50%
- Losses and wins roughly cancel
- Spreads and swap costs create a net negative drift
- Capital slowly erodes
After: Asymmetric Discipline
The new system trades rarely, but only when multiple conditions align. The expected outcome:
| Metric | Before | After (expected) |
|---|---|---|
| Trade frequency | High | Low |
| Average confidence | ~0.60 | ≥0.65 |
| Win rate | ~48–52% | Higher |
| Drawdown per cycle | Variable (often negative) | Reduced |
| Capital preservation | Poor | Improved |
The Professional Analogy
A professional trader does not trade every hour. They sit at their desk, watch the market, and wait for the one setup that checks every box. Then they size in with conviction.
The old system was like a trader who places a bet every five minutes out of boredom. The new system waits. It skips. And when it trades, it trades with a reason.
The edge is not in finding the best of what is available. The edge is in refusing to trade when nothing good is available.
Summary Table
| Change | Location | What changed | Financial principle |
|---|---|---|---|
| Compression blocks trend | technical_agent |
Returns None when regime = compression + strategy = trend | No trend trade without momentum |
| EMA spread minimum | technical_agent |
Blocks trend when trend_strength < 0.0008 | Trend requires confirmed directional bias |
| Confidence floor 0.62 | technical_agent |
Kills signals below 0.62 at source | Only high-conviction setups enter pipeline |
| Low volatility blocks trend | technical_agent |
Blocks trend when context_volatility = low | Dead markets cannot sustain directional trades |
| NY dead zone filter | technical_agent |
Blocks NY + non-expansion + weak trend | Avoid end-of-week/session liquidity traps |
| No market orders in low vol | technical_agent |
Blocks market-type entry when volatility = low | Spread cost disproportionate; no thrust to profit |
| Correlation block 3.4→2.0 | technical_agent |
Stricter FX exposure concentration cap | Prevents correlated position doubling risk |
| Quality gate in orchestration | orchestration_agent |
Filters before scoring, logs rejections | Only competitive setups enter ranking |
| Confidence raised 0.52→0.65 | orchestration_agent |
Higher bar for proposal acceptance | Removes "slightly above coin flip" trades |
| Combined score gate 0.6 | orchestration_agent |
Filters low holistic quality proposals | Holistic quality, not just technical signal |
| Execution guard 0.65 | orchestration_agent |
Last confidence check before live execution | Prevents degraded proposals from executing |
| No-trade as explicit decision | orchestration_agent |
Returns status + reason + filtered symbols | Transparency; no-trade is a valid outcome |
Written: 2026-04-25 | Applies to: TFA Agents v1 backend