Files
edge_lab/examples/strategies.hpp
T

418 lines
15 KiB
C++

/**
* 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 <vector>
#include <numeric>
#include <cmath>
#include <algorithm>
#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