347 lines
13 KiB
C++
347 lines
13 KiB
C++
/**
|
|
* EdgeEngine - Open-Source MT5 Strategy Tester Replica
|
|
* strategy_base.hpp
|
|
*
|
|
* Abstract Strategy interface mirroring the MQL5 EA lifecycle:
|
|
* OnInit() - Called once at start of simulation
|
|
* OnDeinit(reason) - Called once at end of simulation
|
|
* OnTick(tick) - Called on every synthetic or real tick
|
|
* OnBar(rates, idx) - Called on every new bar close (optional, convenience)
|
|
* OnTradeTransaction() - Called after each trade/order event
|
|
*
|
|
* TradeContext class mirrors MQL5's CTrade helper:
|
|
* Buy(), Sell(), BuyLimit(), SellLimit(), BuyStop(), SellStop()
|
|
* PositionClose(), PositionModify(), OrderModify(), OrderDelete()
|
|
*
|
|
* Strategies subclass Strategy and override whichever hooks they need.
|
|
* The Tester class drives the lifecycle and injects dependencies.
|
|
*/
|
|
#pragma once
|
|
|
|
#include <string>
|
|
#include <vector>
|
|
#include <functional>
|
|
#include <cmath>
|
|
#include <stdexcept>
|
|
#include "mql_types.hpp"
|
|
#include "account.hpp"
|
|
#include "order_book.hpp"
|
|
#include "types.hpp"
|
|
|
|
namespace edge {
|
|
|
|
// ============================================================
|
|
// StrategyContext - runtime state injected into a strategy
|
|
// Provides access to account, symbol, and current bar data
|
|
// ============================================================
|
|
struct StrategyContext {
|
|
Account* account = nullptr;
|
|
OrderBook* order_book = nullptr;
|
|
SymbolSpec symbol;
|
|
MqlTick current_tick;
|
|
int32_t current_bar = 0; // 0-indexed bar position in simulation
|
|
int32_t total_bars = 0; // Total bars in simulation
|
|
const MqlRates* rates = nullptr; // Full price history up to current bar
|
|
const double* bid_array = nullptr;
|
|
const double* ask_array = nullptr;
|
|
TickMode tick_mode = TickMode::OPEN_PRICES;
|
|
};
|
|
|
|
|
|
// ============================================================
|
|
// TradeContext - MQL5 CTrade equivalent
|
|
// Thin wrapper around OrderBook for ergonomic EA-style calls
|
|
// ============================================================
|
|
class TradeContext {
|
|
public:
|
|
explicit TradeContext(StrategyContext& ctx) : ctx_(ctx) {}
|
|
|
|
// --- Market Orders ---
|
|
|
|
// Buy at current market Ask price
|
|
bool Buy(double volume, double sl = 0.0, double tp = 0.0,
|
|
const std::string& comment = "", uint64_t magic = 0)
|
|
{
|
|
MqlTradeRequest req{};
|
|
req.action = TRADE_ACTION_DEAL;
|
|
req.symbol = ctx_.symbol.name;
|
|
req.type = ORDER_TYPE_BUY;
|
|
req.volume = volume;
|
|
req.price = ctx_.current_tick.ask;
|
|
req.sl = ctx_.symbol.normalize_price(sl);
|
|
req.tp = ctx_.symbol.normalize_price(tp);
|
|
req.comment = comment;
|
|
req.magic = magic;
|
|
return ctx_.order_book->order_send(req, last_result_);
|
|
}
|
|
|
|
// Sell at current market Bid price
|
|
bool Sell(double volume, double sl = 0.0, double tp = 0.0,
|
|
const std::string& comment = "", uint64_t magic = 0)
|
|
{
|
|
MqlTradeRequest req{};
|
|
req.action = TRADE_ACTION_DEAL;
|
|
req.symbol = ctx_.symbol.name;
|
|
req.type = ORDER_TYPE_SELL;
|
|
req.volume = volume;
|
|
req.price = ctx_.current_tick.bid;
|
|
req.sl = ctx_.symbol.normalize_price(sl);
|
|
req.tp = ctx_.symbol.normalize_price(tp);
|
|
req.comment = comment;
|
|
req.magic = magic;
|
|
return ctx_.order_book->order_send(req, last_result_);
|
|
}
|
|
|
|
// --- Pending Orders ---
|
|
|
|
bool BuyLimit(double volume, double price, double sl = 0.0, double tp = 0.0,
|
|
int64_t expiry = 0, const std::string& comment = "", uint64_t magic = 0)
|
|
{
|
|
MqlTradeRequest req{};
|
|
req.action = TRADE_ACTION_PENDING;
|
|
req.symbol = ctx_.symbol.name;
|
|
req.type = ORDER_TYPE_BUY_LIMIT;
|
|
req.volume = volume;
|
|
req.price = ctx_.symbol.normalize_price(price);
|
|
req.sl = ctx_.symbol.normalize_price(sl);
|
|
req.tp = ctx_.symbol.normalize_price(tp);
|
|
req.expiration = expiry;
|
|
req.comment = comment;
|
|
req.magic = magic;
|
|
return ctx_.order_book->order_send(req, last_result_);
|
|
}
|
|
|
|
bool SellLimit(double volume, double price, double sl = 0.0, double tp = 0.0,
|
|
int64_t expiry = 0, const std::string& comment = "", uint64_t magic = 0)
|
|
{
|
|
MqlTradeRequest req{};
|
|
req.action = TRADE_ACTION_PENDING;
|
|
req.symbol = ctx_.symbol.name;
|
|
req.type = ORDER_TYPE_SELL_LIMIT;
|
|
req.volume = volume;
|
|
req.price = ctx_.symbol.normalize_price(price);
|
|
req.sl = ctx_.symbol.normalize_price(sl);
|
|
req.tp = ctx_.symbol.normalize_price(tp);
|
|
req.expiration = expiry;
|
|
req.comment = comment;
|
|
req.magic = magic;
|
|
return ctx_.order_book->order_send(req, last_result_);
|
|
}
|
|
|
|
bool BuyStop(double volume, double price, double sl = 0.0, double tp = 0.0,
|
|
int64_t expiry = 0, const std::string& comment = "", uint64_t magic = 0)
|
|
{
|
|
MqlTradeRequest req{};
|
|
req.action = TRADE_ACTION_PENDING;
|
|
req.symbol = ctx_.symbol.name;
|
|
req.type = ORDER_TYPE_BUY_STOP;
|
|
req.volume = volume;
|
|
req.price = ctx_.symbol.normalize_price(price);
|
|
req.sl = ctx_.symbol.normalize_price(sl);
|
|
req.tp = ctx_.symbol.normalize_price(tp);
|
|
req.expiration = expiry;
|
|
req.comment = comment;
|
|
req.magic = magic;
|
|
return ctx_.order_book->order_send(req, last_result_);
|
|
}
|
|
|
|
bool SellStop(double volume, double price, double sl = 0.0, double tp = 0.0,
|
|
int64_t expiry = 0, const std::string& comment = "", uint64_t magic = 0)
|
|
{
|
|
MqlTradeRequest req{};
|
|
req.action = TRADE_ACTION_PENDING;
|
|
req.symbol = ctx_.symbol.name;
|
|
req.type = ORDER_TYPE_SELL_STOP;
|
|
req.volume = volume;
|
|
req.price = ctx_.symbol.normalize_price(price);
|
|
req.sl = ctx_.symbol.normalize_price(sl);
|
|
req.tp = ctx_.symbol.normalize_price(tp);
|
|
req.expiration = expiry;
|
|
req.comment = comment;
|
|
req.magic = magic;
|
|
return ctx_.order_book->order_send(req, last_result_);
|
|
}
|
|
|
|
// --- Position Management ---
|
|
|
|
// Close position by ticket (full or partial)
|
|
bool PositionClose(uint64_t ticket, double volume = 0.0) {
|
|
return ctx_.order_book->position_close(ticket, volume);
|
|
}
|
|
|
|
// Modify SL/TP on an open position
|
|
bool PositionModify(uint64_t ticket, double sl, double tp) {
|
|
MqlTradeRequest req{};
|
|
req.action = TRADE_ACTION_SLTP;
|
|
req.position = ticket;
|
|
req.symbol = ctx_.symbol.name;
|
|
req.sl = ctx_.symbol.normalize_price(sl);
|
|
req.tp = ctx_.symbol.normalize_price(tp);
|
|
return ctx_.order_book->order_send(req, last_result_);
|
|
}
|
|
|
|
// --- Order Management ---
|
|
|
|
bool OrderModify(uint64_t ticket, double price, double sl, double tp,
|
|
ENUM_ORDER_TYPE_TIME time_type = ORDER_TIME_GTC, int64_t expiry = 0)
|
|
{
|
|
MqlTradeRequest req{};
|
|
req.action = TRADE_ACTION_MODIFY;
|
|
req.order = ticket;
|
|
req.symbol = ctx_.symbol.name;
|
|
req.price = ctx_.symbol.normalize_price(price);
|
|
req.sl = ctx_.symbol.normalize_price(sl);
|
|
req.tp = ctx_.symbol.normalize_price(tp);
|
|
req.type_time = time_type;
|
|
req.expiration = expiry;
|
|
return ctx_.order_book->order_send(req, last_result_);
|
|
}
|
|
|
|
bool OrderDelete(uint64_t ticket) {
|
|
MqlTradeRequest req{};
|
|
req.action = TRADE_ACTION_REMOVE;
|
|
req.order = ticket;
|
|
return ctx_.order_book->order_send(req, last_result_);
|
|
}
|
|
|
|
// --- Result Inspection ---
|
|
const MqlTradeResult& ResultLast() const { return last_result_; }
|
|
bool ResultSuccess() const { return last_result_.retcode == TRADE_RETCODE_DONE || last_result_.retcode == TRADE_RETCODE_PLACED; }
|
|
|
|
// --- Account / Positions Helpers ---
|
|
double AccountBalance() const { return ctx_.account->balance(); }
|
|
double AccountEquity() const { return ctx_.account->equity(); }
|
|
double AccountFreeMargin() const { return ctx_.account->free_margin(); }
|
|
double AccountMarginLevel() const { return ctx_.account->margin_level(); }
|
|
int32_t PositionsTotal() const { return ctx_.order_book->positions_total(); }
|
|
int32_t OrdersTotal() const { return ctx_.order_book->orders_total(); }
|
|
const std::vector<PositionEntity>& Positions() const { return ctx_.order_book->active_positions(); }
|
|
const std::vector<Order>& Orders() const { return ctx_.order_book->active_orders(); }
|
|
|
|
// --- Pricing Helpers ---
|
|
double Ask() const { return ctx_.current_tick.ask; }
|
|
double Bid() const { return ctx_.current_tick.bid; }
|
|
double Spread() const { return ctx_.current_tick.ask - ctx_.current_tick.bid; }
|
|
|
|
// --- Historical Data Access ---
|
|
// iOpen, iHigh, iLow, iClose in MQL5 use shift from current bar (0 = current, 1 = previous)
|
|
double iOpen(int shift = 0) const {
|
|
int idx = ctx_.current_bar - shift;
|
|
if (idx < 0 || !ctx_.rates) return 0.0;
|
|
return ctx_.rates[idx].open;
|
|
}
|
|
double iHigh(int shift = 0) const {
|
|
int idx = ctx_.current_bar - shift;
|
|
if (idx < 0 || !ctx_.rates) return 0.0;
|
|
return ctx_.rates[idx].high;
|
|
}
|
|
double iLow(int shift = 0) const {
|
|
int idx = ctx_.current_bar - shift;
|
|
if (idx < 0 || !ctx_.rates) return 0.0;
|
|
return ctx_.rates[idx].low;
|
|
}
|
|
double iClose(int shift = 0) const {
|
|
int idx = ctx_.current_bar - shift;
|
|
if (idx < 0 || !ctx_.rates) return 0.0;
|
|
return ctx_.rates[idx].close;
|
|
}
|
|
int64_t iTime(int shift = 0) const {
|
|
int idx = ctx_.current_bar - shift;
|
|
if (idx < 0 || !ctx_.rates) return 0;
|
|
return ctx_.rates[idx].time;
|
|
}
|
|
int64_t iVolume(int shift = 0) const {
|
|
int idx = ctx_.current_bar - shift;
|
|
if (idx < 0 || !ctx_.rates) return 0;
|
|
return ctx_.rates[idx].tick_volume;
|
|
}
|
|
|
|
// Number of bars available to this point in the simulation
|
|
int BarsCount() const { return ctx_.current_bar + 1; }
|
|
|
|
private:
|
|
StrategyContext& ctx_;
|
|
MqlTradeResult last_result_{};
|
|
};
|
|
|
|
|
|
// ============================================================
|
|
// INIT_SUCCEEDED / INIT_FAILED codes (matching MQL5)
|
|
// ============================================================
|
|
constexpr int INIT_SUCCEEDED = 0;
|
|
constexpr int INIT_FAILED = 1;
|
|
|
|
// DEINIT reason codes (matching MQL5)
|
|
enum ENUM_DEINIT_REASON : int32_t {
|
|
REASON_PROGRAM = 0,
|
|
REASON_REMOVE = 1,
|
|
REASON_RECOMPILE = 2,
|
|
REASON_CHARTCHANGE = 3,
|
|
REASON_CHARTCLOSE = 4,
|
|
REASON_PARAMETERS = 5,
|
|
REASON_ACCOUNT = 6,
|
|
REASON_TEMPLATE = 7,
|
|
REASON_INITFAILED = 8,
|
|
REASON_CLOSE = 9
|
|
};
|
|
|
|
|
|
// ============================================================
|
|
// Strategy - Abstract base class
|
|
// Subclass this to implement any trading strategy.
|
|
// The Tester drives the lifecycle and provides StrategyContext.
|
|
// ============================================================
|
|
class Strategy {
|
|
public:
|
|
virtual ~Strategy() = default;
|
|
|
|
// Called once before the simulation begins.
|
|
// Return INIT_SUCCEEDED (0) to allow simulation, INIT_FAILED (1) to abort.
|
|
virtual int OnInit() { return INIT_SUCCEEDED; }
|
|
|
|
// Called once after the simulation ends.
|
|
virtual void OnDeinit(ENUM_DEINIT_REASON reason) {}
|
|
|
|
// Called on every tick (EVERY_TICK mode) or bar open (OPEN_PRICES mode).
|
|
// This is the primary entry point for strategy logic.
|
|
virtual void OnTick() {}
|
|
|
|
// Called after every completed bar (bar close event). Convenience hook.
|
|
// In OPEN_PRICES mode this fires at the same time as OnTick (bar open).
|
|
virtual void OnBar() {}
|
|
|
|
// Called after every trade transaction (order fill, position open/close, etc.).
|
|
virtual void OnTradeTransaction(
|
|
const MqlTradeTransaction& trans,
|
|
const MqlTradeRequest& req,
|
|
const MqlTradeResult& res) {}
|
|
|
|
// Called after each full bar evaluation to report indicator values.
|
|
// Override to populate external arrays (for chart display or logging).
|
|
virtual void OnCalculate(int bars_calculated) {}
|
|
|
|
// Called at simulation completion during optimization to compute custom fitness.
|
|
// Equivalent to MQL5's double OnTester(void).
|
|
virtual double OnTester() { return 0.0; }
|
|
|
|
// ---------------------------------------------------------
|
|
// Injected state available to strategy implementation
|
|
// ---------------------------------------------------------
|
|
StrategyContext ctx; // Set by Tester before any callbacks
|
|
TradeContext* Trade = nullptr; // Set by Tester before any callbacks
|
|
|
|
protected:
|
|
// Helper shortcut: calculate a simple EMA in-strategy
|
|
static double EMA(const double* src, int n, int period, double k) {
|
|
if (n < period) return src[n > 0 ? n-1 : 0];
|
|
double val = src[0];
|
|
for (int i = 1; i < n; ++i) val = val * (1.0 - k) + src[i] * k;
|
|
return val;
|
|
}
|
|
};
|
|
|
|
// Shared-pointer alias for ergonomic use
|
|
using StrategyPtr = std::shared_ptr<Strategy>;
|
|
|
|
} // namespace edge
|