AUTHOR: Tony Mudau
Trade lifecycle: entries, exits, and SL/TP management
This document describes how the TFA Agents backend decides where to put stops and targets, when to open or close, and how open positions are managed over time. It maps behavior to the main modules: technical agent, orchestration agent, trade_lifecycle, execution agent, execution_guards, and app.config.
1. Architecture overview
The system is a pipeline, not a single monolith:
| Stage | Responsibility |
|---|---|
| Technical agent | Produces a directional hypothesis: entry level, stop_loss, take_profit, execution style (market vs pending), and pattern-level confidence, including pattern_win_rate. |
| Orchestration agent | Applies quality gates, entry validation (R:R, S/R, chasing, EV), portfolio / risk shaping, duplication checks, and drives exit management (lifecycle + LLM + time stop + hard TP). |
trade_lifecycle |
Pure rules for R:R, EV, entry checks, time-in-trade, in-R profit, and post-open breakeven / trail / partial close. |
| Execution agent | Sends orders to MT5; does not recompute strategy SL/TP—it enforces execution guards first. |
execution_guards |
Last-mile safety before the broker: news risk, bar freshness, slippage vs entry, spread, intrabar range vs ATR. |
Important: The orchestration layer no longer fabricates SL/TP if the technical layer omits them. Invalid or zero SL/TP → the candidate is rejected (invalid_sl_tp_from_technical).
2. How SL and TP are set (before a trade is open)
2.1 Technical agent (source of truth)
In generate_trade_hypothesis (see technical_agent/main.py):
- Entry is chosen by
calculate_entry_price(e.g. pullback vs breakout vs market) from OHLC and context. - Risk distance is based on ATR and a small minimum relative to price:
stop_distance = max(atr * 1.5, price * 0.001).
- Take profit is a multiple of that distance (
tp_multfrom strategy mode, e.g. default vshtf_pullback). - For a buy:
stop_loss = entry - stop_distance,take_profit = entry + stop_distance * tp_mult. - For a sell: the mirror.
So the first SL/TP is always anchored to the technical plan (ATR + entry), not to orchestration heuristics.
The hypothesis also exposes:
pattern_win_rate— from pattern memory, used for EV and confidence floor later.
2.2 Orchestration (enrichment, not override)
In the per-symbol worker (_evaluate_symbol_worker):
- Entry uses the technical
entryif positive; otherwise the current tick (ask for buy, bid for sell). - If either
stop_lossortake_profitis≤ 0→ reject; there is no fallback to a % of price. - Risk/reward is computed in price space (same as
trade_lifecycle.risk_reward_ratio). - EV gate (
ev_gate_allows_entry): usespattern_win_rateand RR. IfENTRY_REQUIRE_POSITIVE_EVis true, strongly negative expected R (below a small buffer) blocks the trade. - Entry validation (
validate_entry_proposal): see §3. - Confidence is adjusted for MTF/reversal, then blended with EV (
blend_confidence_with_ev), and compared to a floor that can rise whenpattern_win_rateis weak (effective_entry_confidence_floor).
Orchestration does not replace technical SL/TP with ad-hoc percentages. It may filter a trade; it does not silently rewrite stops/targets to arbitrary deltas.
2.3 Risk / portfolio agents
Risk and portfolio agents can change size (e.g. lot) and other constraints; the intended design is that per-unit risk geometry (SL/TP prices from the technical hypothesis) remain the reference for that signal unless a future layer explicitly recomputes them (not the old % fallback).
3. Entry validation (why a signal might be dropped)
Module: trade_lifecycle.py — validate_entry_proposal.
Checks (configurable in app/config.py):
| Check | Meaning |
|---|---|
| Minimum R:R | reward / risk ≥ ENTRY_MIN_RISK_REWARD (default 1.5), using the proposal entry, SL, and TP. |
| Near resistance (buy) / support (sell) | Uses build_market_context’s recent_range (high/low) and ATR. A buffer is max(2% of range, ATR * ENTRY_SR_ATR_BUFFER_MULT). If a buy entry sits within that buffer of the local high, or a sell near the local low, the trade is rejected. |
| Chasing | Compares the active side price (ask for buy, bid for sell) to the position within the recent high–low range. Buys in the top ENTRY_CHASE_RANGE_TOP (default 0.85) of the range or sells in the bottom ENTRY_CHASE_RANGE_BOTTOM (default 0.15) are rejected. |
| Entry vs market | ` |
Rejection reasons are traced (e.g. risk_reward_below_minimum, entry_too_close_to_resistance, chasing_buy_near_range_top, entry_too_far_from_current_price).
3.1 Expected value (not only raw confidence)
- Per-unit EV in R:
w * RR − (1 − w)wherew = pattern_win_rate(clamped) andRRis the reward/risk ratio. - If
ENTRY_REQUIRE_POSITIVE_EVand EV is below about −0.02, the entry is blocked (expected_value_negative). blend_confidence_with_evdown-weights confidence when EV is negative; small positive adjustments when EV is healthy.
This ties “confidence” to a rough economic plausibility given stored pattern win rate—still not a guarantee of real-world calibration, but stricter than a single ≥ 0.65 threshold alone.
4. Execution path: when the order actually fires
4.1 Execution agent
- Market orders:
MT5Tools.execute_marketwith the proposal’s SL and TP attached to the deal. - Pending orders:
execute_pendingwith entry, SL, and TP. execute_tradebranches onexecution_type(market vs pending) then runs guards.
4.2 evaluate_execution_guards (order of checks)
News does not block execution; it still informs orchestration (confidence, filters, LLM). Guards only check freshness and microstructure.
-
Bar age (signal freshness)
- Last closed bar on the request timeframe must be younger than
bar_age_multiplier ×bar length (seeEXEC_GUARD_BAR_AGE_MULT).
- Last closed bar on the request timeframe must be younger than
-
Optional micro-trend vs direction (if
EXEC_GUARD_REQUIRE_TECH_AGREEMENT). -
Slippage vs proposed entry
|ref − entry| / entryvsEXEC_GUARD_MAX_SLIPPAGE_PCT(ref = ask for buy, bid for sell).
-
Spread
(ask − bid) / midmust not exceed **EXEC_MAX_SPREAD_RATIO`.
-
Volatility spike
- Last bar’s range divided by ATR(14) must not exceed EXEC_VOL_ATR_SPIKE_MULT (abnormal single-bar extension).
5. Open positions: SL/TP on the server and in memory
- MT5 returns positions with floating
slandtp. The backend normalizes these inparse_positions(common.py) so downstream logic can readslandtpon each open position. - Initial SL/TP on the order are what the technical layer intended; the trade management layer can tighten stops (breakeven, trail) or reduce size (partial) via new MT5 calls.
6. Holding positions: in-trade management (breakeven, trail, partial)
Function: apply_open_position_lifecycle in trade_lifecycle.py, called from exit management in the orchestration when evaluating each open position (after loading OHLC for that symbol).
Definitions:
- 1R in price uses the current open SL vs open price:
- Buy:
one_risk_price = price_open − sl(whenslis a valid buy stop). - Sell:
sl − price_openwhenslis above entry.
- Buy:
- Profit in R (
profit_in_r): unrealized move along the trade divided by that 1R distance.
When enabled (defaults in app/config.py):
| Phase | Default trigger | Action |
|---|---|---|
| Breakeven | TRADE_MGMT_BREAKEVEN_R (default 1.0 R) |
Move SL to roughly entry (small offset in points to satisfy broker min distance). |
| ATR trail | From TRADE_MGMT_TRAIL_START_R (default 1.5 R) |
For buys: pull SL up toward bid − TRADE_MGMT_TRAIL_ATR_MULT * ATR(14); for sells, mirror below ask. |
| Partial close | TRADE_MGMT_PARTIAL_AT_R (default 1.2 R) |
Close TRADE_MGMT_PARTIAL_VOLUME_FRACTION of volume (e.g. 0.5) once, tracked per ticket in process memory for idempotence. |
Implementation uses MT5Tools.modify_position_sltp and close_position_volume.
Caveats:
- If initial SL is 0 or invalid, 1R cannot be computed—management steps are skipped for that position.
- Partial-close deduplication is in-process; after restart, a duplicate partial is unlikely but possible if tickets repeat in edge cases.
- Tightening stops can increase stop-out rate in chop; tune
TRADE_MGMT_*and ATR multiplier for your pairs.
7. Exits: strategies beyond broker TP/SL
Exit decisions are merged in this order of concern (see _run_exit_management):
-
In-trade lifecycle (above) — adjusts SL/TP or size before discretionary exit logic.
-
LLM + rules exit (
llm_position_exit_decisionincommon.py)- Heuristics: e.g. technical direction opposes the position with sufficient confidence, or very negative floating P&L.
- If OpenAI is configured, an LLM may output hold vs close with confidence.
-
Hard take-profit (account money)
- From risk settings: if
hard_take_profit_enabledand floating profit ≥hard_take_profit_amount(account currency) → close (overrides as a full exit).
- From risk settings: if
-
Time stop (bar-based)
time_stop_candlessets a max hold in bars per chart timeframe (e.g. M15: 72 bars ≈ 18h at 15m; M30: 36 bars; H1: 18 bars; short TFs: longer bar counts).position_age_barsusesopened_atand timeframe bar length.- If age ≥ max and profit in R is still very low (scratch / drift), a time-based close is recommended.
- A stricter case extends hold slightly (1.2×) with still weak R-multiple.
- Executed closes can record
close_reasontime_stopin trade history when applicable.
-
Execution
ExecutionAgent.close_open_positionsends a market close for the full remaining volume at the ticket.
Broker-level TP/SL still apply automatically on the server; the stack adds strategic flattening, risk caps, and time limits.
8. Configuration reference (app/config.py)
| Setting | Role |
|---|---|
ENTRY_MIN_RISK_REWARD |
Minimum R:R for validated entries. |
ENTRY_SR_ATR_BUFFER_MULT, ENTRY_CHASE_RANGE_TOP/BOTTOM, ENTRY_MAX_PCT_DEVIATION_FROM_MID |
Entry quality filters. |
ENTRY_REQUIRE_POSITIVE_EV |
Enable EV gate using pattern_win_rate. |
EXEC_GUARD_* |
Slippage, bar age, optional tech agreement. |
EXEC_MAX_SPREAD_RATIO, EXEC_VOL_ATR_SPIKE_MULT |
Last-mile liquidity / spike filters. |
TRADE_MGMT_BREAKEVEN_R, TRADE_MGMT_TRAIL_START_R, TRADE_MGMT_TRAIL_ATR_MULT, TRADE_MGMT_PARTIAL_* |
In-trade management. |
Tune spread and vol thresholds per symbol and broker (commissions, typical spreads).
9. End-to-end mental model
- Technical builds entry + SL + TP (ATR-based) and
pattern_win_rate. - Orchestration rejects bad geometry (missing SL/TP, R:R, EV, S/R, chase, deviance), then other agents and gates.
- Execution guards block bad timing and conditions (stale bar, slippage, wide spread, vol spike; optional micro-trend).
- MT5 holds the order with the passed SL/TP.
- Lifecycle can move SL, trail, and partially close using R and ATR.
- Exit manager can close on LLM/risk, hard P&L, or time stop, in addition to normal TP/SL hits.
This turns the system from a signal executor into a trade lifecycle with explicit entry quality, execution quality, and position management layers—each configurable and traceable in logs and execution_guard_events / traces.
10. File map
| File | Content |
|---|---|
agents/technical_agent/main.py |
Hypothesis, entry / stop_loss / take_profit, pattern_win_rate |
agents/orchestration_agent/main.py |
Worker gates, run_cycle execute path, _run_exit_management |
agents/trade_lifecycle.py |
R:R, EV, validate_entry_proposal, time stops, apply_open_position_lifecycle |
agents/execution_guards.py |
evaluate_execution_guards |
agents/execution_agent/main.py |
execute_trade, execute_market_order, close_open_position |
agents/common.py |
MT5Tools, parse_positions, llm_position_exit_decision |
app/config.py |
All tunable constants above |
Last updated to match the trade-lifecycle implementation in the repository (entries, execution guards, post-open management, and time-based exits).