/** * EdgeEngine - Example Strategies * * Demonstrates the general-purpose MT5 Strategy Tester API. * Each class is a self-contained strategy - none of these know about or * depend on any indicator code in the engine itself. * * Example strategies: * 1. SmaCrossStrategy - Classic SMA crossover (fast/slow EMA) * 2. RSIStrategy - RSI mean-reversion (buy oversold, sell overbought) * 3. BollingerBandStrategy - Bollinger band breakout + reversal * 4. GridStrategy - Fixed-interval grid trading * * Usage from C++: * #include "tester.hpp" * #include "examples/strategies.hpp" * * SmaCrossStrategy strat; * strat.fast_period = 10; * strat.slow_period = 30; * * Tester tester; * TesterResult res = tester.run(&strat, bars, n, sym, cfg); */ #pragma once #include #include #include #include #include "../core/include/strategy_base.hpp" namespace edge { namespace examples { // ============================================================ // Utility: Compute SMA over last `period` bars from current bar // ============================================================ inline double sma_close(const TradeContext& T, int period) { if (T.BarsCount() < period) return 0.0; double sum = 0.0; for (int i = 0; i < period; ++i) sum += T.iClose(i); return sum / period; } // Compute EMA using Wilder / standard multiplier inline double ema_close(const TradeContext& T, int period, double& prev_ema, bool& initialized) { double price = T.iClose(0); if (!initialized) { // Seed with SMA if enough bars if (T.BarsCount() < period) return price; double sum = 0.0; for (int i = 0; i < period; ++i) sum += T.iClose(i); prev_ema = sum / period; initialized = true; return prev_ema; } double k = 2.0 / (period + 1.0); prev_ema = prev_ema + k * (price - prev_ema); return prev_ema; } // Compute ATR over last `period` bars inline double atr(const TradeContext& T, int period) { if (T.BarsCount() < period + 1) return 0.0; double sum = 0.0; for (int i = 0; i < period; ++i) { double h = T.iHigh(i); double l = T.iLow(i); double pc = T.iClose(i + 1); double tr = std::max({h - l, std::abs(h - pc), std::abs(l - pc)}); sum += tr; } return sum / period; } // ============================================================ // 1. SmaCrossStrategy // Buy when fast EMA crosses above slow EMA. // Sell (short) when fast EMA crosses below slow EMA. // SL = ATR * sl_atr_mult from entry price. // ============================================================ class SmaCrossStrategy : public Strategy { public: int fast_period = 10; int slow_period = 30; int atr_period = 14; double sl_atr_mult = 2.0; double risk_pct = 1.0; // % of balance risked per trade bool allow_short = true; int OnInit() override { fast_ema_ = 0.0; slow_ema_ = 0.0; fast_init_ = slow_init_ = false; return INIT_SUCCEEDED; } void OnBar() override { if (!Trade || Trade->BarsCount() < slow_period + 2) return; double fast = ema_close(*Trade, fast_period, fast_ema_, fast_init_); double slow = ema_close(*Trade, slow_period, slow_ema_, slow_init_); if (!fast_init_ || !slow_init_) return; double prev_close = Trade->iClose(1); double cur_close = Trade->iClose(0); double cur_atr = atr(*Trade, atr_period); if (cur_atr <= 0.0) return; // Detect crossover: compare current and previous bar's EMA diff sign // Use close-based proxy since we only store one EMA value bool cross_up = (cur_close > fast) && (prev_close <= fast) && (fast > slow); bool cross_down = (cur_close < fast) && (prev_close >= fast) && (fast < slow); int pos_count = Trade->PositionsTotal(); // Close any open positions on opposite signal if (cross_up || cross_down) { for (const auto& pos : Trade->Positions()) { bool is_buy = (pos.type == POSITION_TYPE_BUY); if (cross_up && !is_buy) Trade->PositionClose(pos.ticket); if (cross_down && is_buy) Trade->PositionClose(pos.ticket); } } // Entry double sl_dist = cur_atr * sl_atr_mult; double lots = _risk_lots(sl_dist); if (lots <= 0.0) return; if (cross_up && Trade->PositionsTotal() == 0) { double sl = Trade->Ask() - sl_dist; Trade->Buy(lots, sl, 0.0, "SmaCross_Buy"); } else if (cross_down && allow_short && Trade->PositionsTotal() == 0) { double sl = Trade->Bid() + sl_dist; Trade->Sell(lots, sl, 0.0, "SmaCross_Sell"); } } private: double fast_ema_, slow_ema_; bool fast_init_, slow_init_; double _risk_lots(double sl_dist) const { if (sl_dist <= 1e-10) return ctx.symbol.volume_min; double balance = ctx.account->balance(); double risk_amt = balance * (risk_pct / 100.0); double pts_sl = sl_dist / ctx.symbol.point; double val_pt = ctx.symbol.trade_tick_value * (ctx.symbol.point / ctx.symbol.trade_tick_size); if (pts_sl * val_pt <= 0.0) return ctx.symbol.volume_min; double raw = risk_amt / (pts_sl * val_pt); return ctx.symbol.lot_normalize(raw); } }; // ============================================================ // 2. RSIStrategy // RSI-based mean-reversion: // Buy when RSI crosses above oversold_level from below. // Sell when RSI crosses below overbought_level from above. // ============================================================ class RSIStrategy : public Strategy { public: int rsi_period = 14; double oversold_level = 30.0; double overbought_level = 70.0; int atr_period = 14; double sl_atr_mult = 1.5; double tp_atr_mult = 3.0; double risk_pct = 1.0; bool allow_short = true; int OnInit() override { avg_gain_ = avg_loss_ = 0.0; initialized_ = false; prev_rsi_ = 50.0; return INIT_SUCCEEDED; } void OnBar() override { if (!Trade || Trade->BarsCount() < rsi_period + 2) return; double cur_rsi = _compute_rsi(); double cur_atr = atr(*Trade, atr_period); if (cur_atr <= 0.0) { prev_rsi_ = cur_rsi; return; } bool buy_signal = (prev_rsi_ < oversold_level) && (cur_rsi >= oversold_level); bool sell_signal = (prev_rsi_ > overbought_level) && (cur_rsi <= overbought_level); // Close opposing positions for (const auto& pos : Trade->Positions()) { bool is_buy = (pos.type == POSITION_TYPE_BUY); if (sell_signal && is_buy) Trade->PositionClose(pos.ticket); if (buy_signal && !is_buy) Trade->PositionClose(pos.ticket); } double sl_dist = cur_atr * sl_atr_mult; double tp_dist = cur_atr * tp_atr_mult; double lots = _risk_lots(sl_dist); if (buy_signal && Trade->PositionsTotal() == 0) { Trade->Buy(lots, Trade->Ask() - sl_dist, Trade->Ask() + tp_dist, "RSI_Buy"); } else if (sell_signal && allow_short && Trade->PositionsTotal() == 0) { Trade->Sell(lots, Trade->Bid() + sl_dist, Trade->Bid() - tp_dist, "RSI_Sell"); } prev_rsi_ = cur_rsi; } private: double avg_gain_, avg_loss_, prev_rsi_; bool initialized_; double _compute_rsi() { int n = Trade->BarsCount(); if (n < rsi_period + 1) return 50.0; if (!initialized_) { avg_gain_ = avg_loss_ = 0.0; for (int i = 0; i < rsi_period; ++i) { double diff = Trade->iClose(i) - Trade->iClose(i + 1); if (diff > 0) avg_gain_ += diff; else avg_loss_ -= diff; } avg_gain_ /= rsi_period; avg_loss_ /= rsi_period; initialized_ = true; } else { double diff = Trade->iClose(0) - Trade->iClose(1); double gain = diff > 0 ? diff : 0.0; double loss = diff < 0 ? -diff : 0.0; avg_gain_ = (avg_gain_ * (rsi_period - 1) + gain) / rsi_period; avg_loss_ = (avg_loss_ * (rsi_period - 1) + loss) / rsi_period; } if (avg_loss_ < 1e-10) return 100.0; double rs = avg_gain_ / avg_loss_; return 100.0 - 100.0 / (1.0 + rs); } double _risk_lots(double sl_dist) const { if (sl_dist <= 1e-10) return ctx.symbol.volume_min; double balance = ctx.account->balance(); double risk_amt = balance * (risk_pct / 100.0); double pts_sl = sl_dist / ctx.symbol.point; double val_pt = ctx.symbol.trade_tick_value * (ctx.symbol.point / ctx.symbol.trade_tick_size); if (pts_sl * val_pt <= 0.0) return ctx.symbol.volume_min; return ctx.symbol.lot_normalize(risk_amt / (pts_sl * val_pt)); } }; // ============================================================ // 3. BollingerBandStrategy // Breakout/reversal using Bollinger Bands (20-period, 2 std dev). // Breakout mode: buy close above upper band, sell below lower. // Reversal mode: buy close below lower band, sell above upper. // ============================================================ class BollingerBandStrategy : public Strategy { public: int period = 20; double std_dev_mult = 2.0; bool reversal_mode = true; // true=mean-reversion, false=breakout int atr_period = 14; double sl_atr_mult = 2.0; double tp_band_pct = 0.5; // TP at 50% of band width from entry double risk_pct = 1.0; bool allow_short = true; int OnInit() override { return INIT_SUCCEEDED; } void OnBar() override { if (!Trade || Trade->BarsCount() < period + 1) return; // Compute Bollinger Bands double sma = sma_close(*Trade, period); double sq_sum = 0.0; for (int i = 0; i < period; ++i) { double d = Trade->iClose(i) - sma; sq_sum += d * d; } double sd = std::sqrt(sq_sum / period); double upper = sma + std_dev_mult * sd; double lower = sma - std_dev_mult * sd; double band_width = upper - lower; double cur_close = Trade->iClose(0); double prev_close = Trade->iClose(1); double cur_atr = atr(*Trade, atr_period); if (cur_atr <= 0.0) return; bool above_upper = cur_close > upper && prev_close <= upper; bool below_lower = cur_close < lower && prev_close >= lower; for (const auto& pos : Trade->Positions()) { bool is_buy = (pos.type == POSITION_TYPE_BUY); if (reversal_mode) { // Close longs when price reaches SMA (mean) if (is_buy && cur_close >= sma) Trade->PositionClose(pos.ticket); if (!is_buy && cur_close <= sma) Trade->PositionClose(pos.ticket); } else { // Close breakout longs when price crosses back inside band if (is_buy && cur_close < upper) Trade->PositionClose(pos.ticket); if (!is_buy && cur_close > lower) Trade->PositionClose(pos.ticket); } } if (Trade->PositionsTotal() > 0) return; double sl_dist = cur_atr * sl_atr_mult; double tp_dist = band_width * tp_band_pct; double lots = _risk_lots(sl_dist); if (reversal_mode) { if (below_lower) { Trade->Buy(lots, Trade->Ask() - sl_dist, Trade->Ask() + tp_dist, "BB_Rev_Buy"); } else if (above_upper && allow_short) { Trade->Sell(lots, Trade->Bid() + sl_dist, Trade->Bid() - tp_dist, "BB_Rev_Sell"); } } else { if (above_upper) { Trade->Buy(lots, Trade->Ask() - sl_dist, Trade->Ask() + tp_dist, "BB_Break_Buy"); } else if (below_lower && allow_short) { Trade->Sell(lots, Trade->Bid() + sl_dist, Trade->Bid() - tp_dist, "BB_Break_Sell"); } } } private: double _risk_lots(double sl_dist) const { if (sl_dist <= 1e-10) return ctx.symbol.volume_min; double balance = ctx.account->balance(); double risk_amt = balance * (risk_pct / 100.0); double pts_sl = sl_dist / ctx.symbol.point; double val_pt = ctx.symbol.trade_tick_value * (ctx.symbol.point / ctx.symbol.trade_tick_size); if (pts_sl * val_pt <= 0.0) return ctx.symbol.volume_min; return ctx.symbol.lot_normalize(risk_amt / (pts_sl * val_pt)); } }; // ============================================================ // 4. GridStrategy // Fixed-interval grid: place buy stops and sell stops at // regular intervals around current price. Classic grid EA. // ============================================================ class GridStrategy : public Strategy { public: double grid_step_pts = 100.0; // Grid spacing in points double lot_size = 0.01; int max_orders = 5; // Max pending orders per side double tp_pts = 150.0; // TP per order in points int OnInit() override { grid_initialized_ = false; return INIT_SUCCEEDED; } void OnBar() override { if (!Trade || Trade->BarsCount() < 2) return; double cur_price = (Trade->Ask() + Trade->Bid()) * 0.5; double step = grid_step_pts * ctx.symbol.point; double tp = tp_pts * ctx.symbol.point; double lots = ctx.symbol.lot_normalize(lot_size); if (!grid_initialized_) { grid_base_ = cur_price; grid_initialized_ = true; } // Manage orders: ensure we have `max_orders` on each side int buy_orders = 0; int sell_orders = 0; for (const auto& ord : Trade->Orders()) { if (ord.type == ORDER_TYPE_BUY_STOP) buy_orders++; if (ord.type == ORDER_TYPE_SELL_STOP) sell_orders++; } // Place missing buy stops above current price for (int i = 1; i <= max_orders && buy_orders < max_orders; ++i, ++buy_orders) { double price = ctx.symbol.normalize_price(cur_price + i * step); double order_tp = ctx.symbol.normalize_price(price + tp); Trade->BuyStop(lots, price, 0.0, order_tp, 0, "Grid_Buy"); } // Place missing sell stops below current price for (int i = 1; i <= max_orders && sell_orders < max_orders; ++i, ++sell_orders) { double price = ctx.symbol.normalize_price(cur_price - i * step); double order_tp = ctx.symbol.normalize_price(price - tp); if (order_tp < ctx.symbol.point) continue; Trade->SellStop(lots, price, 0.0, order_tp, 0, "Grid_Sell"); } } void OnDeinit(ENUM_DEINIT_REASON) override { if (!Trade) return; // Cancel all pending grid orders for (const auto& ord : Trade->Orders()) { Trade->OrderDelete(ord.ticket); } } private: bool grid_initialized_ = false; double grid_base_ = 0.0; }; } // namespace examples } // namespace edge