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:
romysaputrasihananda
2026-06-12 00:48:45 +07:00
co-authored by Claude Sonnet 4.6
parent dbba172ed3
commit 272fca4f65
13 changed files with 431 additions and 103 deletions
+14
View File
@@ -41,6 +41,20 @@ BACKTEST_CANDLES=50000
# LIVE=true # LIVE=true
# LIVE_POLL_SECS=30 # LIVE_POLL_SECS=30
# ── Optimisations ─────────────────────────────────────────────────────────────
# Session filter: only take new entries when UTC hour in [FROM, TO).
# Leave empty to disable. Recommended: 8-13 (London) based on live data.
# SESSION_FROM_UTC=8
# SESSION_TO_UTC=13
# Breakeven: move SL to entry once price moves this multiple of SL-distance.
# 0 = disabled, 1.0 = move at 1:1 RR
# BREAKEVEN_AT_RR=1.0
# Daily loss limit: halt new entries if today's closed PnL < -(balance × pct).
# 0 = disabled, 0.03 = stop after -3% day
# DAILY_LOSS_LIMIT_PCT=0.03
# ── Telegram Notifications ─────────────────────────────────────────────────── # ── Telegram Notifications ───────────────────────────────────────────────────
# Get token from @BotFather, chat_id from @userinfobot or Telegram API. # Get token from @BotFather, chat_id from @userinfobot or Telegram API.
# All three are optional — omit to disable notifications. # All three are optional — omit to disable notifications.
+17
View File
@@ -97,6 +97,23 @@ impl Mt5Client {
Ok(w.data) Ok(w.data)
} }
pub async fn modify_position(
&self,
ticket: u64,
symbol: &str,
sl: f64,
tp: f64,
) -> Result<TradeResult, Mt5Error> {
let req = TradeRequest::modify_sltp(symbol, ticket, sl, tp);
#[derive(serde::Serialize)]
struct Body<'a> { request: &'a TradeRequest }
let url = format!("{}/order/send", self.base_url);
let text = self.fetch_text(self.http.post(&url).json(&Body { request: &req })).await?;
tracing::debug!(endpoint = %url, ticket, "modify position ok");
let w: DataOne<TradeResult> = serde_json::from_str(&text)?;
Ok(w.data)
}
pub async fn cancel_order(&self, ticket: u64, symbol: &str) -> Result<TradeResult, Mt5Error> { pub async fn cancel_order(&self, ticket: u64, symbol: &str) -> Result<TradeResult, Mt5Error> {
let req = TradeRequest::cancel(symbol, ticket); let req = TradeRequest::cancel(symbol, ticket);
#[derive(serde::Serialize)] #[derive(serde::Serialize)]
+23
View File
@@ -31,6 +31,9 @@ pub struct TradeRequest {
/// Ticket number — required for TRADE_ACTION_REMOVE (cancel pending order) /// Ticket number — required for TRADE_ACTION_REMOVE (cancel pending order)
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub order: Option<u64>, pub order: Option<u64>,
/// Position ticket — required for TRADE_ACTION_SLTP (modify SL/TP)
#[serde(skip_serializing_if = "Option::is_none")]
pub position: Option<u64>,
/// Allowed price deviation in points /// Allowed price deviation in points
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub deviation: Option<u32>, pub deviation: Option<u32>,
@@ -63,6 +66,25 @@ impl TradeRequest {
magic: Some(magic), magic: Some(magic),
comment: Some(comment.into()), comment: Some(comment.into()),
order: None, order: None,
position: None,
deviation: None,
}
}
pub fn modify_sltp(symbol: impl Into<String>, position: u64, sl: f64, tp: f64) -> Self {
// TRADE_ACTION_SLTP = 6
Self {
action: 6,
symbol: symbol.into(),
volume: None,
order_type: None,
price: None,
sl: Some(sl),
tp: Some(tp),
magic: None,
comment: None,
order: None,
position: Some(position),
deviation: None, deviation: None,
} }
} }
@@ -81,6 +103,7 @@ impl TradeRequest {
magic: None, magic: None,
comment: None, comment: None,
order: Some(ticket), order: Some(ticket),
position: None,
deviation: None, deviation: None,
} }
} }
+102 -25
View File
@@ -1,5 +1,5 @@
use anyhow::Result; use anyhow::Result;
use chrono::NaiveDate; use chrono::{NaiveDate, Timelike};
use domain::Side; use domain::Side;
use rust_decimal::Decimal; use rust_decimal::Decimal;
@@ -10,26 +10,35 @@ use crate::helpers::{actual_entry, actual_exit, fmt_price, fmt_pnl, rolling_ema,
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct BacktestConfig { pub struct BacktestConfig {
pub timeframe: domain::Timeframe, pub timeframe: domain::Timeframe,
pub candles: u32, pub candles: u32,
pub balance: Decimal, pub balance: Decimal,
pub risk_pct: Decimal, pub risk_pct: Decimal,
pub body_pct_min: Decimal, pub body_pct_min: Decimal,
pub close_pct_min: Decimal, pub close_pct_min: Decimal,
pub fvg_expiry: usize, pub fvg_expiry: usize,
pub min_fvg_pips: Decimal, pub min_fvg_pips: Decimal,
pub min_sl_pips: Decimal, pub min_sl_pips: Decimal,
pub sl_buffer: Decimal, pub sl_buffer: Decimal,
pub min_rr: Decimal, pub min_rr: Decimal,
pub timeout_candles: usize, pub timeout_candles: usize,
pub commission: Decimal, pub commission: Decimal,
pub slippage_points: Decimal, pub slippage_points: Decimal,
pub spread_override: Option<Decimal>, pub spread_override: Option<Decimal>,
pub ema_period: usize, pub ema_period: usize,
pub date_from: Option<NaiveDate>, pub date_from: Option<NaiveDate>,
pub date_to: Option<NaiveDate>, pub date_to: Option<NaiveDate>,
pub stop_out_pct: Decimal, pub stop_out_pct: Decimal,
pub tf_str: String, 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 ──────────────────────────────────────────────────────────────── // ── open trade ────────────────────────────────────────────────────────────────
@@ -43,6 +52,7 @@ struct OpenTrade {
tp: Decimal, tp: Decimal,
volume: Decimal, volume: Decimal,
open_candle_idx: usize, open_candle_idx: usize,
be_set: bool, // breakeven already applied
} }
// ── entry point ─────────────────────────────────────────────────────────────── // ── 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 max_consec = 0u32;
let mut cur_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 { 'outer: for i in 2..total {
let candle = &candles[i]; let candle = &candles[i];
let date = candle.time.date_naive(); 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 ──────────────────────────────────────────────── // ── 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 { if cfg.timeout_candles > 0 && (i - t.open_candle_idx) >= cfg.timeout_candles {
let t = open_trade.take().unwrap(); let t = open_trade.take().unwrap();
let exit_lvl = candle.close; let exit_lvl = candle.close;
@@ -132,6 +174,7 @@ pub async fn run(mt5: &mt5_client::Mt5Client, symbol: &str, cfg: &BacktestConfig
continue; continue;
} }
// stop-out
if cfg.stop_out_pct > Decimal::ZERO { if cfg.stop_out_pct > Decimal::ZERO {
let worst_price = match t.side { let worst_price = match t.side {
Side::Long => candle.low, 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; wins += 1; sum_wins += pnl; cur_consec = 0;
} }
trades += 1; total_pnl += pnl; total_friction += friction; trades += 1; total_pnl += pnl; total_friction += friction;
let be_tag = if t.be_set { " [BE]" } else { "" };
println!( 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, t.open_time, cfg.tf_str, symbol,
if t.side == Side::Long { "LONG " } else { "SHORT" }, 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, 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_from.is_some_and(|d| date < d) { continue; }
if cfg.date_to.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 ────────────────────────────────────────────────── // ── expire stale FVG ──────────────────────────────────────────────────
if pending_fvg.as_ref().is_some_and(|f| i >= f.expiry_idx) { if pending_fvg.as_ref().is_some_and(|f| i >= f.expiry_idx) {
missed_fills += 1; missed_fills += 1;
@@ -275,6 +339,7 @@ pub async fn run(mt5: &mt5_client::Mt5Client, symbol: &str, cfg: &BacktestConfig
actual_entry: ae, actual_entry: ae,
sl, tp, volume: v, sl, tp, volume: v,
open_candle_idx: i, open_candle_idx: i,
be_set: false,
}); });
pending_fvg = None; 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 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 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!("─────────────────────────────────────────");
println!("Ares Scalper: {} {} | {} candles", symbol, cfg.tf_str, total); 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() }; 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={}{}", 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); 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={}", println!("Friction : spread={} slip={} commission/lot={}",
fmt_price(spread_price, prec), fmt_price(slippage_price, prec), cfg.commission); fmt_price(spread_price, prec), fmt_price(slippage_price, prec), cfg.commission);
println!("─────────────────────────────────────────"); println!("─────────────────────────────────────────");
+127 -23
View File
@@ -1,5 +1,5 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use chrono::{DateTime, Utc}; use chrono::{DateTime, Timelike, Utc};
use domain::{Side, Timeframe}; use domain::{Side, Timeframe};
use futures_util::StreamExt; use futures_util::StreamExt;
use rust_decimal::Decimal; use rust_decimal::Decimal;
@@ -33,9 +33,14 @@ pub struct LiveConfig {
pub slippage_points: Decimal, pub slippage_points: Decimal,
pub spread_override: Option<Decimal>, pub spread_override: Option<Decimal>,
pub ema_period: usize, pub ema_period: usize,
pub poll_secs: u64, pub poll_secs: u64,
pub mt5_base_url: String, pub mt5_base_url: String,
pub telegram: Option<TelegramConfig>, 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 ─────────────────────────────────────────────────────── // ── pending order state ───────────────────────────────────────────────────────
@@ -78,6 +83,8 @@ struct PosState {
sl: f64, sl: f64,
tp: f64, tp: f64,
volume: f64, volume: f64,
#[serde(default)]
be_set: bool,
} }
impl PosState { impl PosState {
@@ -169,6 +176,27 @@ pub async fn run(mt5: &mt5_client::Mt5Client, cfg: &LiveConfig) -> Result<()> {
} }
let http = reqwest::Client::new(); 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)); let mut ticker = interval(Duration::from_secs(cfg.poll_secs));
loop { loop {
@@ -209,21 +237,67 @@ async fn tick(
if has_position { if has_position {
// ensure PosState exists so SSE task can find it on close // ensure PosState exists so SSE task can find it on close
if PosState::load(symbol).is_none() { let mut ps = if let Some(existing) = PosState::load(symbol) {
if let Some(pos) = positions.iter().find(|p| p.symbol == *symbol && p.magic == MAGIC) { existing
let tg_msg_id = State::load(symbol).and_then(|s| s.tg_message_id); } else if let Some(pos) = positions.iter().find(|p| p.symbol == *symbol && p.magic == MAGIC) {
let ps = PosState { let tg_msg_id = State::load(symbol).and_then(|s| s.tg_message_id);
ticket: pos.ticket, let ps = PosState {
tg_message_id: tg_msg_id, ticket: pos.ticket,
side: format!("{:?}", pos.side), tg_message_id: tg_msg_id,
entry: d2f(pos.price_open), side: format!("{:?}", pos.side),
sl: d2f(pos.sl), entry: d2f(pos.price_open),
tp: d2f(pos.tp), sl: d2f(pos.sl),
volume: d2f(pos.volume), tp: d2f(pos.tp),
}; volume: d2f(pos.volume),
let _ = ps.save(symbol); 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"); tracing::debug!(%symbol, "position open — skip");
return Ok(()); return Ok(());
} }
@@ -260,7 +334,36 @@ async fn tick(
return Ok(()); 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 let candles = mt5
.rates_from_pos(symbol, cfg.timeframe, 0, CANDLE_FETCH) .rates_from_pos(symbol, cfg.timeframe, 0, CANDLE_FETCH)
.await .await
@@ -273,7 +376,7 @@ async fn tick(
let impulse = &candles[n - 3]; let impulse = &candles[n - 3];
let post = &candles[n - 2]; 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 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(); let closes: Vec<Decimal> = candles.iter().map(|c| c.close).collect();
rolling_ema(&closes, cfg.ema_period)[n - 2] rolling_ema(&closes, cfg.ema_period)[n - 2]
@@ -281,7 +384,7 @@ async fn tick(
Some(Decimal::ZERO) Some(Decimal::ZERO)
}; };
// ── 5. detect FVG ───────────────────────────────────────────────────────── // ── 7. detect FVG ─────────────────────────────────────────────────────────
let fvg = match detector::detect( let fvg = match detector::detect(
pre, impulse, post, pre, impulse, post,
cfg.body_pct_min, cfg.close_pct_min, min_zone, cfg.body_pct_min, cfg.close_pct_min, min_zone,
@@ -300,7 +403,7 @@ async fn tick(
}; };
if !ema_ok { return Ok(()); } if !ema_ok { return Ok(()); }
// ── 6. SL / TP ──────────────────────────────────────────────────────────── // ── 8. SL / TP ────────────────────────────────────────────────────────────
let sl = match fvg.side { let sl = match fvg.side {
Side::Long => fvg.impulse_sl - cfg.sl_buffer, Side::Long => fvg.impulse_sl - cfg.sl_buffer,
Side::Short => 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, Side::Short => fvg.entry - sl_dist * cfg.min_rr,
}; };
// ── 7. position size ────────────────────────────────────────────────────── // ── 9. position size ──────────────────────────────────────────────────────
let acct = mt5.account().await.context("fetch account")?; let acct = mt5.account().await.context("fetch account")?;
let balance = Decimal::try_from(acct.balance).context("balance")?; let balance = Decimal::try_from(acct.balance).context("balance")?;
let value_per_lot = if profit_is_usd || post.close == Decimal::ZERO { let value_per_lot = if profit_is_usd || post.close == Decimal::ZERO {
@@ -328,7 +431,7 @@ async fn tick(
None => return Ok(()), None => return Ok(()),
}; };
// ── 8. place order ─────────────────────────────────────────────────────── // ── 10. place order ───────────────────────────────────────────────────────
let req = mt5_client::TradeRequest::limit( let req = mt5_client::TradeRequest::limit(
fvg.side, symbol.clone(), d2f(volume), d2f(fvg.entry), d2f(sl), d2f(tp), fvg.side, symbol.clone(), d2f(volume), d2f(fvg.entry), d2f(sl), d2f(tp),
MAGIC, format!("ares-{}", post.time.format("%m%d-%H%M")), 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, tg_message_id: tg_msg_id,
side: if kind == 0 { "Long".to_string() } else { "Short".to_string() }, side: if kind == 0 { "Long".to_string() } else { "Short".to_string() },
entry, sl, tp, volume, entry, sl, tp, volume,
be_set: false,
}; };
let _ = ps.save(symbol); let _ = ps.save(symbol);
State::clear(symbol); State::clear(symbol);
+37 -19
View File
@@ -78,18 +78,28 @@ async fn main() -> anyhow::Result<()> {
}; };
// ── shared config ───────────────────────────────────────────────────────── // ── shared config ─────────────────────────────────────────────────────────
let risk_pct = env_dec("RISK_PCT", "0.01")?; let risk_pct = env_dec("RISK_PCT", "0.01")?;
let body_pct_min = env_dec("BODY_PCT_MIN", "0.6")?; let body_pct_min = env_dec("BODY_PCT_MIN", "0.6")?;
let close_pct_min = env_dec("CLOSE_PCT_MIN", "0.8")?; let close_pct_min = env_dec("CLOSE_PCT_MIN", "0.8")?;
let fvg_expiry = env_usize("FVG_EXPIRY_CANDLES", "10")?; let fvg_expiry = env_usize("FVG_EXPIRY_CANDLES", "10")?;
let min_fvg_pips = env_dec("MIN_FVG_PIPS", "3")?; let min_fvg_pips = env_dec("MIN_FVG_PIPS", "3")?;
let min_sl_pips = env_dec("MIN_SL_PIPS", "5")?; let min_sl_pips = env_dec("MIN_SL_PIPS", "5")?;
let sl_buffer = env_dec("SL_BUFFER", "0")?; let sl_buffer = env_dec("SL_BUFFER", "0")?;
let min_rr = env_dec("MIN_RR", "1.5")?; let min_rr = env_dec("MIN_RR", "1.5")?;
let commission = env_dec("COMMISSION_PER_LOT", "0")?; let commission = env_dec("COMMISSION_PER_LOT", "0")?;
let slippage_points = env_dec("SLIPPAGE_POINTS", "5")?; let slippage_points = env_dec("SLIPPAGE_POINTS", "5")?;
let spread_override = env_spread_override()?; let spread_override = env_spread_override()?;
let ema_period = env_usize("EMA_PERIOD", "20")?; 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())); let mt5 = Arc::new(mt5_client::Mt5Client::new(mt5_base_url.clone()));
@@ -128,6 +138,10 @@ async fn main() -> anyhow::Result<()> {
poll_secs, poll_secs,
mt5_base_url: mt5_base_url.clone(), mt5_base_url: mt5_base_url.clone(),
telegram, telegram,
session_from_utc,
session_to_utc,
breakeven_at_rr,
daily_loss_limit_pct,
}; };
let mut handles = Vec::new(); let mut handles = Vec::new();
@@ -148,8 +162,8 @@ async fn main() -> anyhow::Result<()> {
// ── backtest mode ───────────────────────────────────────────────────────── // ── backtest mode ─────────────────────────────────────────────────────────
let cfg = backtest::BacktestConfig { let cfg = backtest::BacktestConfig {
timeframe, timeframe,
candles: env_u32("BACKTEST_CANDLES", "50000")?, candles: env_u32("BACKTEST_CANDLES", "50000")?,
balance: env_dec("BACKTEST_BALANCE", "600")?, balance: env_dec("BACKTEST_BALANCE", "600")?,
risk_pct, risk_pct,
body_pct_min, body_pct_min,
close_pct_min, close_pct_min,
@@ -158,15 +172,19 @@ async fn main() -> anyhow::Result<()> {
min_sl_pips, min_sl_pips,
sl_buffer, sl_buffer,
min_rr, min_rr,
timeout_candles: env_usize("TIMEOUT_CANDLES", "0")?, timeout_candles: env_usize("TIMEOUT_CANDLES", "0")?,
commission, commission,
slippage_points, slippage_points,
spread_override, spread_override,
ema_period, ema_period,
date_from: env_date("DATE_FROM")?, date_from: env_date("DATE_FROM")?,
date_to: env_date("DATE_TO")?, date_to: env_date("DATE_TO")?,
stop_out_pct: env_dec("STOP_OUT_PCT", "0.0")?, stop_out_pct: env_dec("STOP_OUT_PCT", "0.0")?,
tf_str: tf_str.clone(), tf_str: tf_str.clone(),
session_from_utc,
session_to_utc,
breakeven_at_rr,
daily_loss_limit_pct,
}; };
for symbol in &symbols { for symbol in &symbols {
+18 -11
View File
@@ -3,22 +3,27 @@ import Link from "next/link";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Backtest Results", title: "Backtest Results",
description: "ARES backtest results: M5 Momentum FVG scalper on XAUUSDm. Best run: PF 1.42, +43.2% net return, 11.5% max drawdown.", description: "ARES backtest results: M5 Momentum FVG scalper on XAUUSDm and BTCUSDm. London session filter, EMA-20 trend filter.",
}; };
const results = [ const results: Array<{
{ period: "1 Month", tf: "M5", risk: "1%", trades: 159, wr: 55.3, pf: 1.42, ret: 43.2, dd: -11.5, highlight: true }, symbol: string; period: string; tf: string; risk: string;
{ period: "1 Month", tf: "M5", risk: "5%", trades: 159, wr: 55.3, pf: 1.26, ret: 390, dd: -156, highlight: false }, trades: number | null; wr: number; pf: number | null; ret: number | null; dd: number | null;
{ period: "1 Week", tf: "M5", risk: "1%", trades: 34, wr: 47.1, pf: 0.94, ret: -6.2, dd: -8.1, highlight: false }, highlight: boolean; note?: string;
{ period: "Yesterday", tf: "M1", risk: "1%", trades: 36, wr: 47.2, pf: 1.05, ret: 6.6, dd: -51, highlight: false }, }> = [
{ period: "Yesterday", tf: "M5", risk: "1%", trades: 3, wr: 66.7, pf: null, ret: null, dd: null, highlight: false, note: "Too few trades" }, { symbol: "XAUUSDm", period: "1 Month", tf: "M5", risk: "1%", trades: 159, wr: 55.3, pf: 1.42, ret: 43.2, dd: -11.5, highlight: true },
{ symbol: "XAUUSDm", period: "50k bars", tf: "M5", risk: "5%", trades: 1421, wr: 50.3, pf: 1.17, ret: null, dd: null, highlight: false, note: "Session 0813 UTC" },
{ symbol: "BTCUSDm", period: "50k bars", tf: "M5", risk: "5%", trades: null, wr: 56.9, pf: 1.11, ret: null, dd: null, highlight: false, note: "Session 0813 UTC" },
{ symbol: "XAUUSDm", period: "1 Month", tf: "M5", risk: "5%", trades: 159, wr: 55.3, pf: 1.26, ret: 390, dd: -156, highlight: false },
{ symbol: "XAUUSDm", period: "1 Week", tf: "M5", risk: "1%", trades: 34, wr: 47.1, pf: 0.94, ret: -6.2, dd: -8.1, highlight: false },
]; ];
const params = [ const params = [
["Timeframe", "M5"], ["Timeframe", "M5"],
["Symbol", "XAUUSDm"], ["Symbols", "XAUUSDm · BTCUSDm"],
["Session", "08:0013:00 UTC"],
["EMA Period", "20"], ["EMA Period", "20"],
["Min FVG Pips", "3"], ["Min FVG Pips", "1"],
["Min SL Pips", "5"], ["Min SL Pips", "5"],
["Min RR", "1.5×"], ["Min RR", "1.5×"],
["FVG Expiry", "10 candles"], ["FVG Expiry", "10 candles"],
@@ -36,6 +41,7 @@ export default function BacktestPage() {
</h1> </h1>
<p className="text-[15px] text-ink-sub max-w-xl"> <p className="text-[15px] text-ink-sub max-w-xl">
Historical simulation on real MT5 tick data. Includes spread costs, commission, and slippage. Historical simulation on real MT5 tick data. Includes spread costs, commission, and slippage.
London session filter (0813 UTC) applied.
</p> </p>
</section> </section>
@@ -74,7 +80,7 @@ export default function BacktestPage() {
<table className="data-table"> <table className="data-table">
<thead> <thead>
<tr> <tr>
{["Period", "TF", "Risk", "Trades", "Win Rate", "Profit Factor", "Return", "Max DD", ""].map(h => ( {["Symbol", "Period", "TF", "Risk", "Trades", "Win Rate", "Profit Factor", "Return", "Max DD", ""].map(h => (
<th key={h}>{h}</th> <th key={h}>{h}</th>
))} ))}
</tr> </tr>
@@ -82,10 +88,11 @@ export default function BacktestPage() {
<tbody> <tbody>
{results.map((r, i) => ( {results.map((r, i) => (
<tr key={i} className={r.highlight ? "bg-s2" : ""}> <tr key={i} className={r.highlight ? "bg-s2" : ""}>
<td className="font-mono font-medium text-ink">{r.symbol}</td>
<td className="font-medium text-ink">{r.period}</td> <td className="font-medium text-ink">{r.period}</td>
<td className="font-mono text-ink-sub">{r.tf}</td> <td className="font-mono text-ink-sub">{r.tf}</td>
<td className="font-mono text-ink-sub">{r.risk}</td> <td className="font-mono text-ink-sub">{r.risk}</td>
<td className="font-mono text-ink-md">{r.trades}</td> <td className="font-mono text-ink-md">{r.trades ?? "—"}</td>
<td className="font-mono text-ink-md">{r.wr.toFixed(1)}%</td> <td className="font-mono text-ink-md">{r.wr.toFixed(1)}%</td>
<td className="font-mono font-medium"> <td className="font-mono font-medium">
{r.pf != null {r.pf != null
+36 -12
View File
@@ -1,21 +1,22 @@
import type { Metadata } from "next"; import type { Metadata } from "next";
import "./globals.css"; import "./globals.css";
import Nav from "@/components/Nav"; import Nav from "@/components/Nav";
import { getLatestVersion, GITHUB_URL } from "@/lib/github";
const SITE_URL = "https://ares.romys.my.id"; const SITE_URL = "https://ares.romys.my.id";
export const metadata: Metadata = { export const metadata: Metadata = {
metadataBase: new URL(SITE_URL), metadataBase: new URL(SITE_URL),
title: { title: {
default: "ARES — Automated Gold Trading Bot", default: "ARES — Algorithmic Trading Bot",
template: "%s · ARES", template: "%s · ARES",
}, },
description: description:
"Live forward-test results for ARES, an M5 Momentum FVG scalper built in Rust. Trades XAUUSDm with EMA-20 trend filter and automated risk management.", "Open-source algorithmic trading bot built in Rust. Momentum FVG scalper with EMA trend filter, automated risk management, and live forward-test results.",
keywords: [ keywords: [
"algorithmic trading", "gold trading bot", "XAUUSD EA", "algorithmic trading", "trading bot", "open source", "XAUUSD",
"MT5 expert advisor", "Rust trading bot", "FVG scalper", "MT5 expert advisor", "Rust trading bot", "FVG scalper",
"forex robot", "automated trading", "ICT strategy", "forex robot", "automated trading", "momentum strategy",
], ],
authors: [{ name: "Romy Saputra Sihananda" }], authors: [{ name: "Romy Saputra Sihananda" }],
creator: "Romy Saputra Sihananda", creator: "Romy Saputra Sihananda",
@@ -28,14 +29,14 @@ export const metadata: Metadata = {
type: "website", type: "website",
url: SITE_URL, url: SITE_URL,
siteName: "ARES Trading Bot", siteName: "ARES Trading Bot",
title: "ARES — Automated Gold Trading Bot", title: "ARES — Algorithmic Trading Bot",
description: "Live forward-test · M5 Momentum FVG scalper · XAUUSDm · Built in Rust", description: "Open-source Momentum FVG scalper built in Rust · Live forward-test · MT5",
locale: "en_US", locale: "en_US",
}, },
twitter: { twitter: {
card: "summary_large_image", card: "summary_large_image",
title: "ARES — Automated Gold Trading Bot", title: "ARES — Algorithmic Trading Bot",
description: "Live forward-test · M5 Momentum FVG scalper · XAUUSDm · Built in Rust", description: "Open-source Momentum FVG scalper built in Rust · Live forward-test · MT5",
}, },
icons: { icons: {
icon: "/favicon.svg", icon: "/favicon.svg",
@@ -46,11 +47,13 @@ export const metadata: Metadata = {
}, },
}; };
export default function RootLayout({ children }: { children: React.ReactNode }) { export default async function RootLayout({ children }: { children: React.ReactNode }) {
const version = await getLatestVersion();
return ( return (
<html lang="en"> <html lang="en">
<body className="min-h-screen antialiased"> <body className="min-h-screen antialiased">
<Nav /> <Nav version={version} />
<main className="max-w-5xl mx-auto px-4 sm:px-6 py-12"> <main className="max-w-5xl mx-auto px-4 sm:px-6 py-12">
{children} {children}
</main> </main>
@@ -64,10 +67,26 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<path d="M10.2 15.5 H15.8" stroke="white" strokeWidth="1.8" strokeLinecap="round"/> <path d="M10.2 15.5 H15.8" stroke="white" strokeWidth="1.8" strokeLinecap="round"/>
</svg> </svg>
<span className="font-mono text-sm font-semibold" style={{ letterSpacing: "0.18em", color: "var(--c-ink)" }}>ARES</span> <span className="font-mono text-sm font-semibold" style={{ letterSpacing: "0.18em", color: "var(--c-ink)" }}>ARES</span>
{version && (
<span className="text-[10px] font-mono text-ink-ter bg-s2 border border-hl px-1.5 py-0.5 rounded">
{version}
</span>
)}
</div> </div>
<p className="text-sm leading-relaxed" style={{ color: "var(--c-ink-sub)" }}> <p className="text-sm leading-relaxed mb-3" style={{ color: "var(--c-ink-sub)" }}>
M5 Momentum FVG Scalper<br />Built in Rust · XAUUSDm Open-source algorithmic trading bot<br />built in Rust · M5 Momentum FVG
</p> </p>
<a
href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
className="inline-flex items-center gap-1.5 text-xs text-ink-sub hover:text-ink transition-colors"
>
<svg width="14" height="14" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/>
</svg>
View on GitHub
</a>
</div> </div>
<div> <div>
<p className="eyebrow mb-4">Navigation</p> <p className="eyebrow mb-4">Navigation</p>
@@ -75,6 +94,11 @@ export default function RootLayout({ children }: { children: React.ReactNode })
<li><a href="/" className="hover:text-ink transition-colors">Dashboard</a></li> <li><a href="/" className="hover:text-ink transition-colors">Dashboard</a></li>
<li><a href="/trades" className="hover:text-ink transition-colors">Trades</a></li> <li><a href="/trades" className="hover:text-ink transition-colors">Trades</a></li>
<li><a href="/backtest" className="hover:text-ink transition-colors">Backtest</a></li> <li><a href="/backtest" className="hover:text-ink transition-colors">Backtest</a></li>
<li>
<a href={GITHUB_URL} target="_blank" rel="noopener noreferrer" className="hover:text-ink transition-colors">
GitHub
</a>
</li>
</ul> </ul>
</div> </div>
<div> <div>
+3 -2
View File
@@ -11,8 +11,9 @@ export default function DashboardPage() {
ARES Trading Bot ARES Trading Bot
</h1> </h1>
<p className="text-[18px] text-ink-md leading-relaxed max-w-2xl mb-8"> <p className="text-[18px] text-ink-md leading-relaxed max-w-2xl mb-8">
M5 Momentum FVG scalper built in Rust. Targets Fair Value Gaps on XAUUSDm Open-source algorithmic trading bot built in Rust. Momentum FVG scalper
with EMA-20 trend filter, automatic position sizing, and Telegram alerts. with EMA trend filter, configurable session window, automatic position
sizing, and Telegram alerts runs on any MT5 symbol.
</p> </p>
<div className="flex gap-3 flex-wrap"> <div className="flex gap-3 flex-wrap">
<Link href="/trades" className="btn-primary">View Trades</Link> <Link href="/trades" className="btn-primary">View Trades</Link>
+2 -2
View File
@@ -6,7 +6,7 @@ import TradesRefresher from "@/components/TradesRefresher";
export const metadata: Metadata = { export const metadata: Metadata = {
title: "Trade History", title: "Trade History",
description: "Live closed trades and equity curve for ARES — M5 Momentum FVG scalper on XAUUSDm.", description: "Live closed trades and equity curve for ARES — M5 Momentum FVG scalper on XAUUSDm and BTCUSDm.",
}; };
export const dynamic = "force-dynamic"; export const dynamic = "force-dynamic";
@@ -20,7 +20,7 @@ export default async function TradesPage() {
try { try {
[account, deals] = await Promise.all([ [account, deals] = await Promise.all([
getAccount(), getAccount(),
getAllDeals("2026-06-10T00:00:00"), getAllDeals(),
]); ]);
} catch { error = true; } } catch { error = true; }
+26 -7
View File
@@ -2,6 +2,7 @@
import Link from "next/link"; import Link from "next/link";
import { usePathname } from "next/navigation"; import { usePathname } from "next/navigation";
import clsx from "clsx"; import clsx from "clsx";
import { GITHUB_URL } from "@/lib/github";
const links = [ const links = [
{ href: "/", label: "Dashboard" }, { href: "/", label: "Dashboard" },
@@ -9,10 +10,10 @@ const links = [
{ href: "/backtest", label: "Backtest" }, { href: "/backtest", label: "Backtest" },
]; ];
export default function Nav() { export default function Nav({ version }: { version?: string | null }) {
const path = usePathname(); const path = usePathname();
return ( return (
<nav className="sticky top-0 z-50" style={{ backgroundColor: 'var(--c-canvas)', borderBottom: '1px solid var(--c-hl)' }}> <nav className="sticky top-0 z-50" style={{ backgroundColor: "var(--c-canvas)", borderBottom: "1px solid var(--c-hl)" }}>
<div className="max-w-5xl mx-auto px-4 sm:px-6 h-14 flex items-center gap-6"> <div className="max-w-5xl mx-auto px-4 sm:px-6 h-14 flex items-center gap-6">
{/* logo + wordmark */} {/* logo + wordmark */}
<Link href="/" className="flex items-center gap-2 group"> <Link href="/" className="flex items-center gap-2 group">
@@ -21,9 +22,14 @@ export default function Nav() {
<path d="M8 19 L13 8 L18 19" stroke="white" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/> <path d="M8 19 L13 8 L18 19" stroke="white" strokeWidth="1.8" strokeLinecap="round" strokeLinejoin="round"/>
<path d="M10.2 15.5 H15.8" stroke="white" strokeWidth="1.8" strokeLinecap="round"/> <path d="M10.2 15.5 H15.8" stroke="white" strokeWidth="1.8" strokeLinecap="round"/>
</svg> </svg>
<span className="font-mono text-sm font-semibold tracking-widest text-ink group-hover:text-accent transition-colors" style={{ letterSpacing: '0.18em' }}> <span className="font-mono text-sm font-semibold tracking-widest text-ink group-hover:text-accent transition-colors" style={{ letterSpacing: "0.18em" }}>
ARES ARES
</span> </span>
{version && (
<span className="hidden sm:inline text-[10px] font-mono text-ink-ter bg-s2 border border-hl px-1.5 py-0.5 rounded">
{version}
</span>
)}
</Link> </Link>
{/* nav links */} {/* nav links */}
@@ -44,10 +50,23 @@ export default function Nav() {
))} ))}
</div> </div>
{/* live indicator */} {/* right side: github + live */}
<div className="ml-auto flex items-center gap-2"> <div className="ml-auto flex items-center gap-4">
<span className="w-1.5 h-1.5 rounded-full bg-bull pulse-dot" /> <a
<span className="text-xs text-ink-sub font-medium tracking-eyebrow uppercase">Live</span> href={GITHUB_URL}
target="_blank"
rel="noopener noreferrer"
className="text-ink-ter hover:text-ink transition-colors"
aria-label="GitHub"
>
<svg width="18" height="18" viewBox="0 0 24 24" fill="currentColor">
<path d="M12 2C6.477 2 2 6.484 2 12.017c0 4.425 2.865 8.18 6.839 9.504.5.092.682-.217.682-.483 0-.237-.008-.868-.013-1.703-2.782.605-3.369-1.343-3.369-1.343-.454-1.158-1.11-1.466-1.11-1.466-.908-.62.069-.608.069-.608 1.003.07 1.531 1.032 1.531 1.032.892 1.53 2.341 1.088 2.91.832.092-.647.35-1.088.636-1.338-2.22-.253-4.555-1.113-4.555-4.951 0-1.093.39-1.988 1.029-2.688-.103-.253-.446-1.272.098-2.65 0 0 .84-.27 2.75 1.026A9.564 9.564 0 0112 6.844c.85.004 1.705.115 2.504.337 1.909-1.296 2.747-1.027 2.747-1.027.546 1.379.202 2.398.1 2.651.64.7 1.028 1.595 1.028 2.688 0 3.848-2.339 4.695-4.566 4.943.359.309.678.92.678 1.855 0 1.338-.012 2.419-.012 2.747 0 .268.18.58.688.482A10.019 10.019 0 0022 12.017C22 6.484 17.522 2 12 2z"/>
</svg>
</a>
<div className="flex items-center gap-2">
<span className="w-1.5 h-1.5 rounded-full bg-bull pulse-dot" />
<span className="text-xs text-ink-sub font-medium tracking-eyebrow uppercase">Live</span>
</div>
</div> </div>
</div> </div>
</nav> </nav>
+21
View File
@@ -0,0 +1,21 @@
const REPO = "RomySaputraSihananda/ares";
export async function getLatestVersion(): Promise<string | null> {
try {
const res = await fetch(
`https://api.github.com/repos/${REPO}/releases/latest`,
{
headers: { Accept: "application/vnd.github+json" },
next: { revalidate: 3600 },
}
);
if (!res.ok) return null;
const data = await res.json();
return (data.tag_name as string) ?? null;
} catch {
return null;
}
}
export const GITHUB_URL = `https://github.com/${REPO}`;
export const GITHUB_REPO = REPO;
+5 -2
View File
@@ -107,8 +107,11 @@ export async function getDeals(dateFrom: string, dateTo: string, symbol?: string
// Fetch all deals from startDate to now by querying one day at a time, // Fetch all deals from startDate to now by querying one day at a time,
// working around the MT5 bridge per-request deal limit. // working around the MT5 bridge per-request deal limit.
export async function getAllDeals(startDate: string): Promise<Deal[]> { // startDate defaults to 90 days ago if omitted.
const start = new Date(startDate); export async function getAllDeals(startDate?: string): Promise<Deal[]> {
const start = startDate
? new Date(startDate)
: (() => { const d = new Date(); d.setUTCDate(d.getUTCDate() - 90); return d; })();
const now = new Date(); const now = new Date();
const days: Array<[string, string]> = []; const days: Array<[string, string]> = [];