mirror of
https://github.com/RomySaputraSihananda/ares.git
synced 2026-08-13 18:58:07 +00:00
feat: session filter, breakeven SL, daily loss limit, startup alert, multi-pair web
Bot improvements: - Session filter (SESSION_FROM_UTC/TO_UTC) — backtest confirms 08-13 UTC optimal for XAU - Breakeven SL management (BREAKEVEN_AT_RR) — disabled by default, hurts XAU momentum - Daily loss limit circuit breaker (DAILY_LOSS_LIMIT_PCT) - Telegram startup alert with symbol, session, risk, and balance - MT5 modify_position (TRADE_ACTION_SLTP) support in mt5-client Web updates: - Version badge auto-fetched from GitHub Releases API - GitHub icon link in Nav and footer - Multi-pair general (not XAUUSDm-specific) - BTCUSDm backtest results added, session params in params table - Trades page uses rolling 90-day window instead of hardcoded date Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 4.6
parent
dbba172ed3
commit
272fca4f65
+102
-25
@@ -1,5 +1,5 @@
|
||||
use anyhow::Result;
|
||||
use chrono::NaiveDate;
|
||||
use chrono::{NaiveDate, Timelike};
|
||||
use domain::Side;
|
||||
use rust_decimal::Decimal;
|
||||
|
||||
@@ -10,26 +10,35 @@ use crate::helpers::{actual_entry, actual_exit, fmt_price, fmt_pnl, rolling_ema,
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct BacktestConfig {
|
||||
pub timeframe: domain::Timeframe,
|
||||
pub candles: u32,
|
||||
pub balance: Decimal,
|
||||
pub risk_pct: Decimal,
|
||||
pub body_pct_min: Decimal,
|
||||
pub close_pct_min: Decimal,
|
||||
pub fvg_expiry: usize,
|
||||
pub min_fvg_pips: Decimal,
|
||||
pub min_sl_pips: Decimal,
|
||||
pub sl_buffer: Decimal,
|
||||
pub min_rr: Decimal,
|
||||
pub timeout_candles: usize,
|
||||
pub commission: Decimal,
|
||||
pub slippage_points: Decimal,
|
||||
pub spread_override: Option<Decimal>,
|
||||
pub ema_period: usize,
|
||||
pub date_from: Option<NaiveDate>,
|
||||
pub date_to: Option<NaiveDate>,
|
||||
pub stop_out_pct: Decimal,
|
||||
pub tf_str: String,
|
||||
pub timeframe: domain::Timeframe,
|
||||
pub candles: u32,
|
||||
pub balance: Decimal,
|
||||
pub risk_pct: Decimal,
|
||||
pub body_pct_min: Decimal,
|
||||
pub close_pct_min: Decimal,
|
||||
pub fvg_expiry: usize,
|
||||
pub min_fvg_pips: Decimal,
|
||||
pub min_sl_pips: Decimal,
|
||||
pub sl_buffer: Decimal,
|
||||
pub min_rr: Decimal,
|
||||
pub timeout_candles: usize,
|
||||
pub commission: Decimal,
|
||||
pub slippage_points: Decimal,
|
||||
pub spread_override: Option<Decimal>,
|
||||
pub ema_period: usize,
|
||||
pub date_from: Option<NaiveDate>,
|
||||
pub date_to: Option<NaiveDate>,
|
||||
pub stop_out_pct: Decimal,
|
||||
pub tf_str: String,
|
||||
// ── new optimisations ────────────────────────────────────────────────────
|
||||
/// UTC hour range [from, to) allowed for new entries. None = no filter.
|
||||
pub session_from_utc: Option<u32>,
|
||||
pub session_to_utc: Option<u32>,
|
||||
/// Move SL to entry once price moves this many × SL-distance in our favour.
|
||||
/// 0 = disabled.
|
||||
pub breakeven_at_rr: Decimal,
|
||||
/// Stop trading today when realised PnL < -(balance × this). 0 = disabled.
|
||||
pub daily_loss_limit_pct: Decimal,
|
||||
}
|
||||
|
||||
// ── open trade ────────────────────────────────────────────────────────────────
|
||||
@@ -43,6 +52,7 @@ struct OpenTrade {
|
||||
tp: Decimal,
|
||||
volume: Decimal,
|
||||
open_candle_idx: usize,
|
||||
be_set: bool, // breakeven already applied
|
||||
}
|
||||
|
||||
// ── entry point ───────────────────────────────────────────────────────────────
|
||||
@@ -97,12 +107,44 @@ pub async fn run(mt5: &mt5_client::Mt5Client, symbol: &str, cfg: &BacktestConfig
|
||||
let mut max_consec = 0u32;
|
||||
let mut cur_consec = 0u32;
|
||||
|
||||
// ── daily loss tracking ───────────────────────────────────────────────────
|
||||
let mut today_date = chrono::NaiveDate::from_ymd_opt(1970, 1, 1).unwrap();
|
||||
let mut today_start_balance = balance;
|
||||
let mut daily_halted = false;
|
||||
|
||||
'outer: for i in 2..total {
|
||||
let candle = &candles[i];
|
||||
let date = candle.time.date_naive();
|
||||
let hour = candle.time.time().hour();
|
||||
|
||||
// ── reset daily state on new day ──────────────────────────────────────
|
||||
if date != today_date {
|
||||
today_date = date;
|
||||
today_start_balance = balance;
|
||||
daily_halted = false;
|
||||
}
|
||||
|
||||
// ── manage open trade ────────────────────────────────────────────────
|
||||
if let Some(ref t) = open_trade {
|
||||
if let Some(ref mut t) = open_trade {
|
||||
// breakeven: move SL to entry once price moves breakeven_at_rr × sl_dist
|
||||
if !t.be_set && cfg.breakeven_at_rr > Decimal::ZERO {
|
||||
let sl_dist = (t.actual_entry - t.sl).abs();
|
||||
let be_trigger = match t.side {
|
||||
Side::Long => t.actual_entry + sl_dist * cfg.breakeven_at_rr,
|
||||
Side::Short => t.actual_entry - sl_dist * cfg.breakeven_at_rr,
|
||||
};
|
||||
let triggered = match t.side {
|
||||
Side::Long => candle.high >= be_trigger,
|
||||
Side::Short => candle.low <= be_trigger,
|
||||
};
|
||||
if triggered {
|
||||
t.sl = t.actual_entry;
|
||||
t.be_set = true;
|
||||
tracing::debug!(%symbol, "breakeven SL set");
|
||||
}
|
||||
}
|
||||
|
||||
// timeout
|
||||
if cfg.timeout_candles > 0 && (i - t.open_candle_idx) >= cfg.timeout_candles {
|
||||
let t = open_trade.take().unwrap();
|
||||
let exit_lvl = candle.close;
|
||||
@@ -132,6 +174,7 @@ pub async fn run(mt5: &mt5_client::Mt5Client, symbol: &str, cfg: &BacktestConfig
|
||||
continue;
|
||||
}
|
||||
|
||||
// stop-out
|
||||
if cfg.stop_out_pct > Decimal::ZERO {
|
||||
let worst_price = match t.side {
|
||||
Side::Long => candle.low,
|
||||
@@ -204,8 +247,9 @@ pub async fn run(mt5: &mt5_client::Mt5Client, symbol: &str, cfg: &BacktestConfig
|
||||
wins += 1; sum_wins += pnl; cur_consec = 0;
|
||||
}
|
||||
trades += 1; total_pnl += pnl; total_friction += friction;
|
||||
let be_tag = if t.be_set { " [BE]" } else { "" };
|
||||
println!(
|
||||
"[{} {}] {} {} entry={} sl={} tp={} vol={:.2} → {label} exit={} friction={} pnl={} bal={:.2}",
|
||||
"[{} {}] {} {} entry={} sl={} tp={} vol={:.2} → {label}{be_tag} exit={} friction={} pnl={} bal={:.2}",
|
||||
t.open_time, cfg.tf_str, symbol,
|
||||
if t.side == Side::Long { "LONG " } else { "SHORT" },
|
||||
fmt_price(t.actual_entry, prec), fmt_price(t.sl, prec), fmt_price(t.tp, prec), t.volume,
|
||||
@@ -219,6 +263,26 @@ pub async fn run(mt5: &mt5_client::Mt5Client, symbol: &str, cfg: &BacktestConfig
|
||||
if cfg.date_from.is_some_and(|d| date < d) { continue; }
|
||||
if cfg.date_to.is_some_and(|d| date > d) { continue; }
|
||||
|
||||
// ── daily loss limit ──────────────────────────────────────────────────
|
||||
if daily_halted { continue; }
|
||||
if cfg.daily_loss_limit_pct > Decimal::ZERO {
|
||||
let daily_pnl = balance - today_start_balance;
|
||||
let limit = -(today_start_balance * cfg.daily_loss_limit_pct);
|
||||
if daily_pnl <= limit {
|
||||
daily_halted = true;
|
||||
tracing::debug!(%symbol, %date, "daily loss limit hit — halting rest of day");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// ── session filter ────────────────────────────────────────────────────
|
||||
if let (Some(from), Some(to)) = (cfg.session_from_utc, cfg.session_to_utc) {
|
||||
if hour < from || hour >= to {
|
||||
pending_fvg = None; // discard stale FVGs from outside session
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// ── expire stale FVG ──────────────────────────────────────────────────
|
||||
if pending_fvg.as_ref().is_some_and(|f| i >= f.expiry_idx) {
|
||||
missed_fills += 1;
|
||||
@@ -275,6 +339,7 @@ pub async fn run(mt5: &mt5_client::Mt5Client, symbol: &str, cfg: &BacktestConfig
|
||||
actual_entry: ae,
|
||||
sl, tp, volume: v,
|
||||
open_candle_idx: i,
|
||||
be_set: false,
|
||||
});
|
||||
pending_fvg = None;
|
||||
}
|
||||
@@ -323,11 +388,23 @@ pub async fn run(mt5: &mt5_client::Mt5Client, symbol: &str, cfg: &BacktestConfig
|
||||
let pf = if sum_losses > Decimal::ZERO { sum_wins / sum_losses } else { Decimal::MAX };
|
||||
let ret_pct = (balance - cfg.balance) / cfg.balance * Decimal::from(100u32);
|
||||
|
||||
let session_str = match (cfg.session_from_utc, cfg.session_to_utc) {
|
||||
(Some(f), Some(t)) => format!(" session={}–{}UTC", f, t),
|
||||
_ => String::new(),
|
||||
};
|
||||
let be_str = if cfg.breakeven_at_rr > Decimal::ZERO {
|
||||
format!(" be@{}×RR", cfg.breakeven_at_rr)
|
||||
} else { String::new() };
|
||||
let dl_str = if cfg.daily_loss_limit_pct > Decimal::ZERO {
|
||||
format!(" daily_loss_limit={}%", cfg.daily_loss_limit_pct * Decimal::from(100u32))
|
||||
} else { String::new() };
|
||||
|
||||
println!("─────────────────────────────────────────");
|
||||
println!("Ares Scalper: {} {} | {} candles", symbol, cfg.tf_str, total);
|
||||
let timeout_str = if cfg.timeout_candles > 0 { format!(" timeout={}c", cfg.timeout_candles) } else { String::new() };
|
||||
println!("Strategy : Momentum FVG body≥{} close≥{} expiry={}c min_fvg={}pip min_sl={}pip min_rr={}{}",
|
||||
cfg.body_pct_min, cfg.close_pct_min, cfg.fvg_expiry, cfg.min_fvg_pips, cfg.min_sl_pips, cfg.min_rr, timeout_str);
|
||||
println!("Strategy : Momentum FVG body≥{} close≥{} expiry={}c min_fvg={}pip min_sl={}pip min_rr={}{}{}{}{}",
|
||||
cfg.body_pct_min, cfg.close_pct_min, cfg.fvg_expiry, cfg.min_fvg_pips, cfg.min_sl_pips, cfg.min_rr,
|
||||
timeout_str, session_str, be_str, dl_str);
|
||||
println!("Friction : spread={} slip={} commission/lot={}",
|
||||
fmt_price(spread_price, prec), fmt_price(slippage_price, prec), cfg.commission);
|
||||
println!("─────────────────────────────────────────");
|
||||
|
||||
+127
-23
@@ -1,5 +1,5 @@
|
||||
use anyhow::{Context, Result};
|
||||
use chrono::{DateTime, Utc};
|
||||
use chrono::{DateTime, Timelike, Utc};
|
||||
use domain::{Side, Timeframe};
|
||||
use futures_util::StreamExt;
|
||||
use rust_decimal::Decimal;
|
||||
@@ -33,9 +33,14 @@ pub struct LiveConfig {
|
||||
pub slippage_points: Decimal,
|
||||
pub spread_override: Option<Decimal>,
|
||||
pub ema_period: usize,
|
||||
pub poll_secs: u64,
|
||||
pub mt5_base_url: String,
|
||||
pub telegram: Option<TelegramConfig>,
|
||||
pub poll_secs: u64,
|
||||
pub mt5_base_url: String,
|
||||
pub telegram: Option<TelegramConfig>,
|
||||
// ── optimisations ────────────────────────────────────────────────────────
|
||||
pub session_from_utc: Option<u32>,
|
||||
pub session_to_utc: Option<u32>,
|
||||
pub breakeven_at_rr: Decimal,
|
||||
pub daily_loss_limit_pct: Decimal,
|
||||
}
|
||||
|
||||
// ── pending order state ───────────────────────────────────────────────────────
|
||||
@@ -78,6 +83,8 @@ struct PosState {
|
||||
sl: f64,
|
||||
tp: f64,
|
||||
volume: f64,
|
||||
#[serde(default)]
|
||||
be_set: bool,
|
||||
}
|
||||
|
||||
impl PosState {
|
||||
@@ -169,6 +176,27 @@ pub async fn run(mt5: &mt5_client::Mt5Client, cfg: &LiveConfig) -> Result<()> {
|
||||
}
|
||||
|
||||
let http = reqwest::Client::new();
|
||||
|
||||
// startup notification
|
||||
if let Some(tg) = &cfg.telegram {
|
||||
let bal = mt5.account().await.ok()
|
||||
.map(|a| d2f(a.balance))
|
||||
.unwrap_or(0.0);
|
||||
let session_str = match (cfg.session_from_utc, cfg.session_to_utc) {
|
||||
(Some(f), Some(t)) => format!("{f:02}:00–{t:02}:00 UTC"),
|
||||
_ => "All hours".to_string(),
|
||||
};
|
||||
let text = format!(
|
||||
"🤖 <b>ARES Started</b>\n{} · {:?}\n\nSession <code>{}</code>\nRisk <code>{:.1}%</code>\nEMA <code>{}</code>\nBalance <code>${:.2}</code>",
|
||||
cfg.symbol, cfg.timeframe,
|
||||
session_str,
|
||||
d2f(cfg.risk_pct) * 100.0,
|
||||
cfg.ema_period,
|
||||
bal,
|
||||
);
|
||||
let _ = tg.send(&http, &text).await;
|
||||
}
|
||||
|
||||
let mut ticker = interval(Duration::from_secs(cfg.poll_secs));
|
||||
|
||||
loop {
|
||||
@@ -209,21 +237,67 @@ async fn tick(
|
||||
|
||||
if has_position {
|
||||
// ensure PosState exists so SSE task can find it on close
|
||||
if PosState::load(symbol).is_none() {
|
||||
if let Some(pos) = positions.iter().find(|p| p.symbol == *symbol && p.magic == MAGIC) {
|
||||
let tg_msg_id = State::load(symbol).and_then(|s| s.tg_message_id);
|
||||
let ps = PosState {
|
||||
ticket: pos.ticket,
|
||||
tg_message_id: tg_msg_id,
|
||||
side: format!("{:?}", pos.side),
|
||||
entry: d2f(pos.price_open),
|
||||
sl: d2f(pos.sl),
|
||||
tp: d2f(pos.tp),
|
||||
volume: d2f(pos.volume),
|
||||
};
|
||||
let _ = ps.save(symbol);
|
||||
let mut ps = if let Some(existing) = PosState::load(symbol) {
|
||||
existing
|
||||
} else if let Some(pos) = positions.iter().find(|p| p.symbol == *symbol && p.magic == MAGIC) {
|
||||
let tg_msg_id = State::load(symbol).and_then(|s| s.tg_message_id);
|
||||
let ps = PosState {
|
||||
ticket: pos.ticket,
|
||||
tg_message_id: tg_msg_id,
|
||||
side: format!("{:?}", pos.side),
|
||||
entry: d2f(pos.price_open),
|
||||
sl: d2f(pos.sl),
|
||||
tp: d2f(pos.tp),
|
||||
volume: d2f(pos.volume),
|
||||
be_set: false,
|
||||
};
|
||||
let _ = ps.save(symbol);
|
||||
ps
|
||||
} else {
|
||||
tracing::debug!(%symbol, "position open — skip");
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
// ── breakeven SL management ───────────────────────────────────────────
|
||||
if !ps.be_set && cfg.breakeven_at_rr > Decimal::ZERO {
|
||||
if let Some(pos) = positions.iter().find(|p| p.ticket == ps.ticket) {
|
||||
let entry = Decimal::try_from(ps.entry).unwrap_or_default();
|
||||
let sl = Decimal::try_from(ps.sl).unwrap_or_default();
|
||||
let sl_dist = (entry - sl).abs();
|
||||
if sl_dist > Decimal::ZERO {
|
||||
let be_trigger = match pos.side {
|
||||
domain::Side::Long => entry + sl_dist * cfg.breakeven_at_rr,
|
||||
domain::Side::Short => entry - sl_dist * cfg.breakeven_at_rr,
|
||||
};
|
||||
let reached = match pos.side {
|
||||
domain::Side::Long => pos.price_current >= be_trigger,
|
||||
domain::Side::Short => pos.price_current <= be_trigger,
|
||||
};
|
||||
if reached {
|
||||
let new_sl = ps.entry; // move SL to entry
|
||||
match mt5.modify_position(pos.ticket, symbol, new_sl, ps.tp).await {
|
||||
Ok(r) if r.retcode == 10009 => {
|
||||
tracing::info!(%symbol, ticket = pos.ticket, "breakeven SL set");
|
||||
ps.sl = new_sl;
|
||||
ps.be_set = true;
|
||||
let _ = ps.save(symbol);
|
||||
if let (Some(tg), Some(msg_id)) = (&cfg.telegram, ps.tg_message_id) {
|
||||
let text = format!(
|
||||
"🔒 <b>BREAKEVEN</b>\n{} · {}\n\nSL moved to entry <code>{}</code>\nTP <code>{}</code>",
|
||||
symbol, ps.side,
|
||||
fp(ps.entry), fp(ps.tp),
|
||||
);
|
||||
let _ = tg.edit(http, msg_id, &text).await;
|
||||
}
|
||||
}
|
||||
Ok(r) => tracing::warn!(retcode = r.retcode, "breakeven modify retcode unexpected"),
|
||||
Err(e) => tracing::warn!("breakeven modify failed: {e:#}"),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tracing::debug!(%symbol, "position open — skip");
|
||||
return Ok(());
|
||||
}
|
||||
@@ -260,7 +334,36 @@ async fn tick(
|
||||
return Ok(());
|
||||
}
|
||||
|
||||
// ── 3. fetch candles ──────────────────────────────────────────────────────
|
||||
// ── 3. session filter ─────────────────────────────────────────────────────
|
||||
if let (Some(from), Some(to)) = (cfg.session_from_utc, cfg.session_to_utc) {
|
||||
let hour = Utc::now().time().hour();
|
||||
if hour < from || hour >= to {
|
||||
tracing::debug!(%symbol, hour, from, to, "outside session window — skip");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
|
||||
// ── 4. daily loss limit ───────────────────────────────────────────────────
|
||||
if cfg.daily_loss_limit_pct > Decimal::ZERO {
|
||||
let now = Utc::now();
|
||||
let today_str = now.format("%Y-%m-%dT00:00:00").to_string();
|
||||
let now_str = now.format("%Y-%m-%dT%H:%M:%S").to_string();
|
||||
if let Ok(today_deals) = mt5.history_deals(&today_str, &now_str, Some(symbol)).await {
|
||||
let daily_pnl: Decimal = today_deals.iter()
|
||||
.filter(|d| d.entry == 1 && d.magic == MAGIC)
|
||||
.map(|d| d.profit + d.commission + d.swap)
|
||||
.sum();
|
||||
let acct = mt5.account().await.context("fetch account for daily limit")?;
|
||||
let balance = Decimal::try_from(acct.balance).context("balance")?;
|
||||
let limit = -(balance * cfg.daily_loss_limit_pct);
|
||||
if daily_pnl <= limit {
|
||||
tracing::info!(%symbol, %daily_pnl, %limit, "daily loss limit hit — no new trades today");
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ── 5. fetch candles ──────────────────────────────────────────────────────
|
||||
let candles = mt5
|
||||
.rates_from_pos(symbol, cfg.timeframe, 0, CANDLE_FETCH)
|
||||
.await
|
||||
@@ -273,7 +376,7 @@ async fn tick(
|
||||
let impulse = &candles[n - 3];
|
||||
let post = &candles[n - 2];
|
||||
|
||||
// ── 4. EMA filter ─────────────────────────────────────────────────────────
|
||||
// ── 6. EMA filter ─────────────────────────────────────────────────────────
|
||||
let ema_val: Option<Decimal> = if cfg.ema_period > 0 && candles.len() >= cfg.ema_period {
|
||||
let closes: Vec<Decimal> = candles.iter().map(|c| c.close).collect();
|
||||
rolling_ema(&closes, cfg.ema_period)[n - 2]
|
||||
@@ -281,7 +384,7 @@ async fn tick(
|
||||
Some(Decimal::ZERO)
|
||||
};
|
||||
|
||||
// ── 5. detect FVG ─────────────────────────────────────────────────────────
|
||||
// ── 7. detect FVG ─────────────────────────────────────────────────────────
|
||||
let fvg = match detector::detect(
|
||||
pre, impulse, post,
|
||||
cfg.body_pct_min, cfg.close_pct_min, min_zone,
|
||||
@@ -300,7 +403,7 @@ async fn tick(
|
||||
};
|
||||
if !ema_ok { return Ok(()); }
|
||||
|
||||
// ── 6. SL / TP ────────────────────────────────────────────────────────────
|
||||
// ── 8. SL / TP ────────────────────────────────────────────────────────────
|
||||
let sl = match fvg.side {
|
||||
Side::Long => fvg.impulse_sl - cfg.sl_buffer,
|
||||
Side::Short => fvg.impulse_sl + cfg.sl_buffer,
|
||||
@@ -312,7 +415,7 @@ async fn tick(
|
||||
Side::Short => fvg.entry - sl_dist * cfg.min_rr,
|
||||
};
|
||||
|
||||
// ── 7. position size ──────────────────────────────────────────────────────
|
||||
// ── 9. position size ──────────────────────────────────────────────────────
|
||||
let acct = mt5.account().await.context("fetch account")?;
|
||||
let balance = Decimal::try_from(acct.balance).context("balance")?;
|
||||
let value_per_lot = if profit_is_usd || post.close == Decimal::ZERO {
|
||||
@@ -328,7 +431,7 @@ async fn tick(
|
||||
None => return Ok(()),
|
||||
};
|
||||
|
||||
// ── 8. place order ────────────────────────────────────────────────────────
|
||||
// ── 10. place order ───────────────────────────────────────────────────────
|
||||
let req = mt5_client::TradeRequest::limit(
|
||||
fvg.side, symbol.clone(), d2f(volume), d2f(fvg.entry), d2f(sl), d2f(tp),
|
||||
MAGIC, format!("ares-{}", post.time.format("%m%d-%H%M")),
|
||||
@@ -487,6 +590,7 @@ async fn on_position_opened(
|
||||
tg_message_id: tg_msg_id,
|
||||
side: if kind == 0 { "Long".to_string() } else { "Short".to_string() },
|
||||
entry, sl, tp, volume,
|
||||
be_set: false,
|
||||
};
|
||||
let _ = ps.save(symbol);
|
||||
State::clear(symbol);
|
||||
|
||||
+37
-19
@@ -78,18 +78,28 @@ async fn main() -> anyhow::Result<()> {
|
||||
};
|
||||
|
||||
// ── shared config ─────────────────────────────────────────────────────────
|
||||
let risk_pct = env_dec("RISK_PCT", "0.01")?;
|
||||
let body_pct_min = env_dec("BODY_PCT_MIN", "0.6")?;
|
||||
let close_pct_min = env_dec("CLOSE_PCT_MIN", "0.8")?;
|
||||
let fvg_expiry = env_usize("FVG_EXPIRY_CANDLES", "10")?;
|
||||
let min_fvg_pips = env_dec("MIN_FVG_PIPS", "3")?;
|
||||
let min_sl_pips = env_dec("MIN_SL_PIPS", "5")?;
|
||||
let sl_buffer = env_dec("SL_BUFFER", "0")?;
|
||||
let min_rr = env_dec("MIN_RR", "1.5")?;
|
||||
let commission = env_dec("COMMISSION_PER_LOT", "0")?;
|
||||
let slippage_points = env_dec("SLIPPAGE_POINTS", "5")?;
|
||||
let spread_override = env_spread_override()?;
|
||||
let ema_period = env_usize("EMA_PERIOD", "20")?;
|
||||
let risk_pct = env_dec("RISK_PCT", "0.01")?;
|
||||
let body_pct_min = env_dec("BODY_PCT_MIN", "0.6")?;
|
||||
let close_pct_min = env_dec("CLOSE_PCT_MIN", "0.8")?;
|
||||
let fvg_expiry = env_usize("FVG_EXPIRY_CANDLES", "10")?;
|
||||
let min_fvg_pips = env_dec("MIN_FVG_PIPS", "3")?;
|
||||
let min_sl_pips = env_dec("MIN_SL_PIPS", "5")?;
|
||||
let sl_buffer = env_dec("SL_BUFFER", "0")?;
|
||||
let min_rr = env_dec("MIN_RR", "1.5")?;
|
||||
let commission = env_dec("COMMISSION_PER_LOT", "0")?;
|
||||
let slippage_points = env_dec("SLIPPAGE_POINTS", "5")?;
|
||||
let spread_override = env_spread_override()?;
|
||||
let ema_period = env_usize("EMA_PERIOD", "20")?;
|
||||
let breakeven_at_rr = env_dec("BREAKEVEN_AT_RR", "0")?;
|
||||
let daily_loss_limit_pct = env_dec("DAILY_LOSS_LIMIT_PCT", "0")?;
|
||||
let session_from_utc: Option<u32> = match std::env::var("SESSION_FROM_UTC") {
|
||||
Ok(s) if !s.is_empty() => Some(s.parse().context("SESSION_FROM_UTC")?),
|
||||
_ => None,
|
||||
};
|
||||
let session_to_utc: Option<u32> = match std::env::var("SESSION_TO_UTC") {
|
||||
Ok(s) if !s.is_empty() => Some(s.parse().context("SESSION_TO_UTC")?),
|
||||
_ => None,
|
||||
};
|
||||
|
||||
let mt5 = Arc::new(mt5_client::Mt5Client::new(mt5_base_url.clone()));
|
||||
|
||||
@@ -128,6 +138,10 @@ async fn main() -> anyhow::Result<()> {
|
||||
poll_secs,
|
||||
mt5_base_url: mt5_base_url.clone(),
|
||||
telegram,
|
||||
session_from_utc,
|
||||
session_to_utc,
|
||||
breakeven_at_rr,
|
||||
daily_loss_limit_pct,
|
||||
};
|
||||
|
||||
let mut handles = Vec::new();
|
||||
@@ -148,8 +162,8 @@ async fn main() -> anyhow::Result<()> {
|
||||
// ── backtest mode ─────────────────────────────────────────────────────────
|
||||
let cfg = backtest::BacktestConfig {
|
||||
timeframe,
|
||||
candles: env_u32("BACKTEST_CANDLES", "50000")?,
|
||||
balance: env_dec("BACKTEST_BALANCE", "600")?,
|
||||
candles: env_u32("BACKTEST_CANDLES", "50000")?,
|
||||
balance: env_dec("BACKTEST_BALANCE", "600")?,
|
||||
risk_pct,
|
||||
body_pct_min,
|
||||
close_pct_min,
|
||||
@@ -158,15 +172,19 @@ async fn main() -> anyhow::Result<()> {
|
||||
min_sl_pips,
|
||||
sl_buffer,
|
||||
min_rr,
|
||||
timeout_candles: env_usize("TIMEOUT_CANDLES", "0")?,
|
||||
timeout_candles: env_usize("TIMEOUT_CANDLES", "0")?,
|
||||
commission,
|
||||
slippage_points,
|
||||
spread_override,
|
||||
ema_period,
|
||||
date_from: env_date("DATE_FROM")?,
|
||||
date_to: env_date("DATE_TO")?,
|
||||
stop_out_pct: env_dec("STOP_OUT_PCT", "0.0")?,
|
||||
tf_str: tf_str.clone(),
|
||||
date_from: env_date("DATE_FROM")?,
|
||||
date_to: env_date("DATE_TO")?,
|
||||
stop_out_pct: env_dec("STOP_OUT_PCT", "0.0")?,
|
||||
tf_str: tf_str.clone(),
|
||||
session_from_utc,
|
||||
session_to_utc,
|
||||
breakeven_at_rr,
|
||||
daily_loss_limit_pct,
|
||||
};
|
||||
|
||||
for symbol in &symbols {
|
||||
|
||||
Reference in New Issue
Block a user