diff --git a/.env.example b/.env.example
index 5e6abad..5b5140e 100644
--- a/.env.example
+++ b/.env.example
@@ -55,6 +55,18 @@ BACKTEST_CANDLES=50000
# 0 = disabled, 0.03 = stop after -3% day
# DAILY_LOSS_LIMIT_PCT=0.03
+# ATR quality filters (backtest + live): reject weak FVGs/impulses.
+# Backtest shows ATR filter improves PF from 1.12 → 1.29 on XAUUSDm 20k candles.
+# MIN_FVG_ATR_RATIO=0.3 # FVG zone must be >= 0.3× ATR(14)
+# MIN_IMPULSE_ATR_RATIO=1.0 # Impulse candle body must be >= 1.0× ATR(14)
+
+# Pre-fill invalidation: cancel pending FVG if price closes past impulse SL before fill.
+# Combined with ATR filter: PF 1.32, lowest DD. Recommended.
+# PRE_FILL_INVALIDATE=true
+
+# Entry zone within FVG: 0.0 = zone edge (aggressive), 0.5 = midpoint (default), 1.0 = far edge (conservative).
+# ENTRY_ZONE_PCT=0.5
+
# ── Telegram Notifications ───────────────────────────────────────────────────
# Get token from @BotFather, chat_id from @userinfobot or Telegram API.
# All three are optional — omit to disable notifications.
diff --git a/src/backtest.rs b/src/backtest.rs
index 1a6a838..e69c4c0 100644
--- a/src/backtest.rs
+++ b/src/backtest.rs
@@ -4,7 +4,7 @@ use domain::Side;
use rust_decimal::Decimal;
use crate::detector;
-use crate::helpers::{actual_entry, actual_exit, fmt_price, fmt_pnl, rolling_ema, size_position};
+use crate::helpers::{actual_entry, actual_exit, fmt_price, fmt_pnl, rolling_atr, rolling_ema, size_position};
// ── config ────────────────────────────────────────────────────────────────────
@@ -39,6 +39,18 @@ pub struct BacktestConfig {
pub breakeven_at_rr: Decimal,
/// Stop trading today when realised PnL < -(balance × this). 0 = disabled.
pub daily_loss_limit_pct: Decimal,
+ /// Close this fraction of position at TP1 then run remainder to TP2. 0 = disabled.
+ pub partial_close_pct: Decimal,
+ /// RR multiple for TP2 (only used when partial_close_pct > 0).
+ pub tp2_rr: Decimal,
+ /// Cancel pending FVG if price closes beyond impulse SL before fill.
+ pub pre_fill_invalidate: bool,
+ /// Min FVG zone width as multiple of ATR(14). 0 = disabled.
+ pub min_fvg_atr_ratio: Decimal,
+ /// Min impulse candle body as multiple of ATR(14). 0 = disabled.
+ pub min_impulse_atr_ratio: Decimal,
+ /// Entry position within FVG zone: 0.0 = zone edge (aggressive), 0.5 = midpoint, 1.0 = far edge (conservative).
+ pub entry_zone_pct: Decimal,
}
// ── open trade ────────────────────────────────────────────────────────────────
@@ -50,9 +62,13 @@ struct OpenTrade {
actual_entry: Decimal,
sl: Decimal,
tp: Decimal,
+ tp2: Decimal,
volume: Decimal,
+ volume_initial: Decimal,
open_candle_idx: usize,
- be_set: bool, // breakeven already applied
+ be_set: bool,
+ partial_done: bool,
+ partial_pnl: Decimal,
}
// ── entry point ───────────────────────────────────────────────────────────────
@@ -83,6 +99,7 @@ pub async fn run(mt5: &mt5_client::Mt5Client, symbol: &str, cfg: &BacktestConfig
} else {
vec![None; total]
};
+ let atr_vals: Vec