mql5/Include/Escape/PaperTrading.mqh

621 行
23 KiB
MQL5

2025-08-05 01:57:33 -04:00
//+------------------------------------------------------------------+
//| PaperTrading.mqh |
//| Copyright 2025, EscapeEA |
//| https://www.escapeea.com |
//+------------------------------------------------------------------+
#property copyright "Copyright 2025, EscapeEA"
#property link "https://www.escapeea.com"
#property version "2.00"
#property strict
#include <Trade\PositionInfo.mqh>
#include <Trade\DealInfo.mqh>
#include <Trade\AccountInfo.mqh>
#include <Trade\SymbolInfo.mqh>
#include <Arrays\ArrayObj.mqh>
// Forward declarations
class CRiskManager; // Forward declaration of RiskManager
// Include the actual header files after forward declarations
#include "RiskManager.mqh"
//+------------------------------------------------------------------+
//| Paper Trade Settings |
//+------------------------------------------------------------------+
input group "=== Paper Trading Settings ==="
input bool InpEnablePaperTrading = true; // Enable Paper Trading
input int InpPaperTradesRequired = 5; // Total Paper Trades
input int InpPaperWinsRequired = 3; // Required Wins for Live Trading
input double InpPaperInitialBalance = 10000.0; // Initial Paper Balance
input bool InpSimulateSlippage = true; // Simulate Slippage
input bool InpSimulatePartialFills = true; // Simulate Partial Fills
input double InpMaxSlippagePips = 1.0; // Max Slippage (pips)
input int InpFillProbability = 90; // Fill Probability (%)
//+------------------------------------------------------------------+
//| Paper Trade Structure |
//+------------------------------------------------------------------+
struct SPaperTrade {
ulong ticket; // Trade ticket
string symbol; // Symbol
ENUM_ORDER_TYPE type; // Order type
double volume; // Trade volume
double openPrice; // Open price
double stopLoss; // Stop loss
double takeProfit; // Take profit
datetime openTime; // Open time
datetime closeTime; // Close time
double closePrice; // Close price
double commission; // Commission
double swap; // Swap
double profit; // Profit/loss
bool isClosed; // Is trade closed
bool isWinner; // Is winning trade
// Constructor
SPaperTrade() {
ticket = 0;
volume = 0.0;
openPrice = 0.0;
stopLoss = 0.0;
takeProfit = 0.0;
closePrice = 0.0;
commission = 0.0;
swap = 0.0;
profit = 0.0;
isClosed = false;
isWinner = false;
}
};
//+------------------------------------------------------------------+
//| CPaperTrading class |
//+------------------------------------------------------------------+
class CPaperTrading
{
private:
// Trade management
CArrayObj m_trades; // Array of paper trades
double m_balance; // Current paper balance
double m_equity; // Current paper equity
int m_totalTrades; // Total paper trades
int m_winningTrades; // Number of winning trades
int m_losingTrades; // Number of losing trades
bool m_liveTrading; // Live trading enabled
// Simulation parameters
double m_slippage; // Current slippage
double m_spread; // Current spread
// External components
CSymbolInfo m_symbol; // Symbol info
CAccountInfo m_account; // Account info
CRiskManager *m_riskManager; // Risk manager (forward declared)
// Private methods
double CalculateCommission(double volume, double price);
double CalculateSwap(double volume, double price, ENUM_ORDER_TYPE type);
double CalculateSlippage();
bool SimulateFill();
void UpdateEquity();
public:
// Constructor/Destructor
CPaperTrading(CRiskManager *riskManager = NULL);
~CPaperTrading();
// Initialization
bool Initialize(CSymbolInfo *symbol, CAccountInfo *account,
CEnhancedRiskManager *riskManager);
void Deinitialize();
// Trading operations
bool OpenPosition(ENUM_ORDER_TYPE type, double volume,
double price, double sl, double tp,
string comment = "");
bool ClosePosition(ulong ticket, double price);
void CheckForExits();
// Getters
double GetBalance() const { return m_balance; }
double GetEquity() const { return m_equity; }
int GetTotalTrades() const { return m_totalTrades; }
int GetWinningTrades() const { return m_winningTrades; }
int GetLosingTrades() const { return m_losingTrades; }
double GetWinRate() const {
return (m_totalTrades > 0) ? (double)m_winningTrades / m_totalTrades * 100.0 : 0.0;
}
bool IsLiveTradingAllowed() const {
return m_liveTrading || (!InpEnablePaperTrading);
}
// State management
void Reset();
void Update();
// Chart display
void UpdateChart();
};
//+------------------------------------------------------------------+
//| Constructor |
//+------------------------------------------------------------------+
CPaperTrading::CPaperTrading() : m_balance(InpPaperInitialBalance),
m_equity(InpPaperInitialBalance),
m_totalTrades(0),
m_winningTrades(0),
m_losingTrades(0),
m_liveTrading(false),
m_slippage(0.0),
m_spread(0.0),
m_symbol(NULL),
m_account(NULL),
m_riskManager(NULL)
{
m_trades.Clear();
}
//+------------------------------------------------------------------+
//| Destructor |
//+------------------------------------------------------------------+
CPaperTrading::~CPaperTrading()
{
Deinitialize();
}
//+------------------------------------------------------------------+
//| Initialize paper trading system |
//+------------------------------------------------------------------+
bool CPaperTrading::Initialize(CSymbolInfo *symbol, CAccountInfo *account,
CEnhancedRiskManager *riskManager)
{
// Validate inputs
if(symbol == NULL || account == NULL || riskManager == NULL) {
Print("PaperTrading: Invalid initialization parameters");
return false;
}
// Store references
m_symbol = symbol;
m_account = account;
m_riskManager = riskManager;
// Reset state
Reset();
// Initialize chart objects
UpdateChart();
return true;
}
//+------------------------------------------------------------------+
//| Clean up resources |
//+------------------------------------------------------------------+
void CPaperTrading::Deinitialize()
{
// Clean up resources
m_trades.Clear();
// Remove chart objects
string prefix = "PaperTrade_";
ObjectsDeleteAll(0, prefix);
ChartRedraw();
// Reset references
m_symbol = NULL;
m_account = NULL;
m_riskManager = NULL;
}
//+------------------------------------------------------------------+
//| Open a new paper position |
//+------------------------------------------------------------------+
bool CPaperTrading::OpenPosition(ENUM_ORDER_TYPE type, double volume,
double price, double sl, double tp,
string comment = "")
{
// Check if paper trading is enabled
if(!InpEnablePaperTrading) {
m_liveTrading = true;
return true; // Allow live trading
}
// Check if we have enough wins to trade live
if(m_winningTrades >= InpPaperWinsRequired && m_totalTrades >= InpPaperTradesRequired) {
m_liveTrading = true;
return true; // Allow live trading
}
// Check if we've reached the maximum number of paper trades
if(m_totalTrades >= InpPaperTradesRequired) {
Print("PaperTrading: Maximum number of paper trades reached (", InpPaperTradesRequired, "). ",
m_winningTrades, " wins out of ", InpPaperWinsRequired, " required for live trading.");
return false;
}
// Check risk management
if(m_riskManager != NULL && !m_riskManager.IsTradingAllowed()) {
Print("PaperTrading: Risk management restricts trading");
return false;
}
// Check if we can simulate a fill
if(!SimulateFill()) {
Print("PaperTrading: Order fill simulation failed");
return false;
}
// Calculate slippage if enabled
double actualPrice = price;
if(InpSimulateSlippage) {
double slippage = CalculateSlippage();
if(type == ORDER_TYPE_BUY || type == ORDER_TYPE_BUY_LIMIT || type == ORDER_TYPE_BUY_STOP) {
actualPrice += slippage;
} else {
actualPrice -= slippage;
}
}
// Create new trade
SPaperTrade *trade = new SPaperTrade();
if(trade == NULL) {
Print("PaperTrading: Failed to create new trade");
return false;
}
// Initialize trade
trade.ticket = (ulong)(TimeCurrent() % 1000000 + m_trades.Total());
trade.symbol = m_symbol.Name();
trade.type = type;
trade.volume = volume;
trade.openPrice = actualPrice;
trade.stopLoss = sl;
trade.takeProfit = tp;
trade.openTime = TimeCurrent();
trade.commission = CalculateCommission(volume, actualPrice);
trade.swap = 0.0; // Will be updated on bar close
trade.isClosed = false;
trade.isWinner = false;
// Add to trades array
if(!m_trades.Add(trade)) {
delete trade;
Print("PaperTrading: Failed to add trade to array");
return false;
}
// Update statistics
m_totalTrades++;
// Update balance (subtract margin, will be added back on close)
double margin = m_riskManager != NULL ?
m_riskManager.CalculateMargin(trade.symbol, trade.volume, trade.type) :
(trade.volume * m_symbol.TickValue() * m_symbol.TickSize());
m_balance -= margin;
PrintFormat("PaperTrading: Opened %s position %.2f lots at %.5f (SL: %.5f, TP: %.5f)",
EnumToString(type), volume, actualPrice, sl, tp);
// Update chart
UpdateChart();
return true;
}
//+------------------------------------------------------------------+
//| Close a paper position |
//+------------------------------------------------------------------+
bool CPaperTrading::ClosePosition(ulong ticket, double price)
{
for(int i = 0; i < m_trades.Total(); i++) {
SPaperTrade *trade = m_trades.At(i);
if(trade == NULL || trade.isClosed || trade.ticket != ticket) continue;
// Calculate P/L
double priceDiff = price - trade.openPrice;
if(trade.type == ORDER_TYPE_SELL || trade.type == ORDER_TYPE_SELL_LIMIT || trade.type == ORDER_TYPE_SELL_STOP) {
priceDiff = -priceDiff;
}
double pips = priceDiff / m_symbol.Point();
double profit = pips * m_symbol.TickValue() * trade.volume / (m_symbol.Point() / m_symbol.TickSize());
// Update trade info
trade.closePrice = price;
trade.closeTime = TimeCurrent();
trade.profit = profit;
trade.isClosed = true;
// Update balance
m_balance += profit - trade.commission - trade.swap;
// Update statistics
if(profit > 0) {
m_winningTrades++;
} else {
m_losingTrades++;
}
// Update equity
UpdateEquity();
PrintFormat("PaperTrading: Closed position %d at %.5f, P/L: $%.2f",
ticket, price, profit - trade.commission - trade.swap);
// Update chart
UpdateChart();
return true;
}
return false;
}
//+------------------------------------------------------------------+
//| Check for exit conditions |
//+------------------------------------------------------------------+
void CPaperTrading::CheckForExits()
{
double bid = m_symbol.Bid();
double ask = m_symbol.Ask();
for(int i = m_trades.Total() - 1; i >= 0; i--) {
SPaperTrade *trade = m_trades.At(i);
if(trade == NULL || trade.isClosed) continue;
// Check if position should be closed by stop loss or take profit
double currentPrice = (trade.type == ORDER_TYPE_BUY || trade.type == ORDER_TYPE_BUY_LIMIT || trade.type == ORDER_TYPE_BUY_STOP) ? bid : ask;
// Check stop loss
if((trade.type == ORDER_TYPE_BUY || trade.type == ORDER_TYPE_BUY_LIMIT || trade.type == ORDER_TYPE_BUY_STOP) &&
trade.stopLoss > 0 && currentPrice <= trade.stopLoss) {
ClosePosition(trade.ticket, trade.stopLoss);
trade.isWinner = false;
continue;
}
if((trade.type == ORDER_TYPE_SELL || trade.type == ORDER_TYPE_SELL_LIMIT || trade.type == ORDER_TYPE_SELL_STOP) &&
trade.stopLoss > 0 && currentPrice >= trade.stopLoss) {
ClosePosition(trade.ticket, trade.stopLoss);
trade.isWinner = false;
continue;
}
// Check take profit
if((trade.type == ORDER_TYPE_BUY || trade.type == ORDER_TYPE_BUY_LIMIT || trade.type == ORDER_TYPE_BUY_STOP) &&
trade.takeProfit > 0 && currentPrice >= trade.takeProfit) {
ClosePosition(trade.ticket, trade.takeProfit);
trade.isWinner = true;
continue;
}
if((trade.type == ORDER_TYPE_SELL || trade.type == ORDER_TYPE_SELL_LIMIT || trade.type == ORDER_TYPE_SELL_STOP) &&
trade.takeProfit > 0 && currentPrice <= trade.takeProfit) {
ClosePosition(trade.ticket, trade.takeProfit);
trade.isWinner = true;
continue;
}
}
}
//+------------------------------------------------------------------+
//| Calculate commission for a trade |
//+------------------------------------------------------------------+
double CPaperTrading::CalculateCommission(double volume, double price)
{
// Default commission: $5 per lot
return 5.0 * volume;
}
//+------------------------------------------------------------------+
//| Calculate swap for an open position |
//+------------------------------------------------------------------+
double CPaperTrading::CalculateSwap(double volume, double price, ENUM_ORDER_TYPE type)
{
// Simulate swap based on position type and size
double swapRate = (type == ORDER_TYPE_BUY) ? -0.5 : 0.2; // pips per lot per day
return volume * swapRate * m_symbol.Point() * 10000.0;
}
//+------------------------------------------------------------------+
//| Calculate slippage for order execution |
//+------------------------------------------------------------------
double CPaperTrading::CalculateSlippage()
{
if(!InpSimulateSlippage) return 0.0;
// Random slippage up to max slippage
return MathRand() / 32767.0 * InpMaxSlippagePips * m_symbol.Point();
}
//+------------------------------------------------------------------+
//| Simulate order fill probability |
//+------------------------------------------------------------------+
bool CPaperTrading::SimulateFill()
{
if(!InpSimulatePartialFills) return true;
// Random chance of fill based on fill probability
return (MathRand() % 100) < InpFillProbability;
}
//+------------------------------------------------------------------+
//| Update equity based on open positions |
//+------------------------------------------------------------------+
void CPaperTrading::UpdateEquity()
{
double floatingPL = 0.0;
double bid = m_symbol.Bid();
double ask = m_symbol.Ask();
double pointValue = m_symbol.Point() * m_symbol.TradeTickValue() / m_symbol.TradeTickSize();
for(int i = 0; i < m_trades.Total(); i++) {
SPaperTrade *trade = m_trades.At(i);
if(trade == NULL || trade.isClosed) continue;
if(trade.type == ORDER_TYPE_BUY) {
floatingPL += (bid - trade.openPrice) * trade.volume * pointValue;
} else {
floatingPL += (trade.openPrice - ask) * trade.volume * pointValue;
}
}
m_equity = m_balance + floatingPL;
}
//+------------------------------------------------------------------+
//| Reset paper trading account |
//+------------------------------------------------------------------+
void CPaperTrading::Reset()
{
// Clear existing trades
m_trades.Clear();
// Reset metrics
m_balance = InpPaperInitialBalance;
m_equity = InpPaperInitialBalance;
m_totalTrades = 0;
m_winningTrades = 0;
m_losingTrades = 0;
m_liveTrading = false;
// Reset chart
UpdateChart();
Print("PaperTrading: Account reset");
}
//+------------------------------------------------------------------+
//| Update paper trading system |
//+------------------------------------------------------------------+
void CPaperTrading::Update()
{
// Update market data
m_symbol.RefreshRates();
// Check for exits
CheckForExits();
// Update equity
UpdateEquity();
// Update chart periodically
static datetime lastUpdate = 0;
if(TimeCurrent() - lastUpdate >= 1) {
UpdateChart();
lastUpdate = TimeCurrent();
}
}
//+------------------------------------------------------------------+
//| Update chart with paper trading information |
//+------------------------------------------------------------------+
void CPaperTrading::UpdateChart()
{
string prefix = "PaperTrade_";
// Create or update info panel
string objName = prefix + "Info";
// Create or update info text
string infoText = StringFormat("=== Paper Trading ===\n" +
"Balance: $%.2f\n" +
"Equity: $%.2f\n" +
"Trades: %d (%d wins, %d losses)\n" +
"Win Rate: %.1f%%\n" +
"Live Trading: %s",
m_balance, m_equity,
m_totalTrades, m_winningTrades, m_losingTrades,
GetWinRate(),
m_liveTrading ? "Enabled" : "Disabled");
// Create or update the info label
if(ObjectFind(0, objName) < 0) {
ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, 20);
ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, 20);
ObjectSetInteger(0, objName, OBJPROP_COLOR, clrWhite);
ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, clrNavy);
ObjectSetInteger(0, objName, OBJPROP_BACK, false);
ObjectSetInteger(0, objName, OBJPROP_HIDDEN, true);
ObjectSetInteger(0, objName, OBJPROP_SELECTABLE, false);
ObjectSetInteger(0, objName, OBJPROP_SELECTED, false);
ObjectSetInteger(0, objName, OBJPROP_ZORDER, 0);
}
ObjectSetString(0, objName, OBJPROP_TEXT, infoText);
ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, 8);
// Update trade markers on chart
for(int i = 0; i < m_trades.Total(); i++) {
SPaperTrade *trade = m_trades.At(i);
if(trade == NULL) continue;
string tradePrefix = prefix + IntegerToString(trade.ticket) + "_";
// Create or update entry marker
string entryName = tradePrefix + "Entry";
if(ObjectFind(0, entryName) < 0) {
ObjectCreate(0, entryName, OBJ_ARROW, 0, trade.openTime,
trade.type <= 1 ? trade.openPrice + 10 * m_symbol.Point() : trade.openPrice - 10 * m_symbol.Point());
ObjectSetInteger(0, entryName, OBJPROP_ARROWCODE, trade.type <= 1 ? 233 : 234);
ObjectSetInteger(0, entryName, OBJPROP_COLOR, trade.type <= 1 ? clrLime : clrRed);
ObjectSetInteger(0, entryName, OBJPROP_WIDTH, 2);
}
// Create or update exit marker if closed
if(trade.isClosed) {
string exitName = tradePrefix + "Exit";
if(ObjectFind(0, exitName) < 0) {
ObjectCreate(0, exitName, OBJ_ARROW, 0, trade.closeTime,
trade.type <= 1 ? trade.closePrice - 10 * m_symbol.Point() : trade.closePrice + 10 * m_symbol.Point());
ObjectSetInteger(0, exitName, OBJPROP_ARROWCODE, 251);
ObjectSetInteger(0, exitName, OBJPROP_COLOR, trade.isWinner ? clrLime : clrRed);
ObjectSetInteger(0, exitName, OBJPROP_WIDTH, 2);
}
}
}
// Redraw the chart
ChartRedraw();
if(ObjectFind(0, objName) < 0) {
ObjectCreate(0, objName, OBJ_RECTANGLE_LABEL, 0, 0, 0);
ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, 10);
ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, 10);
ObjectSetInteger(0, objName, OBJPROP_XSIZE, 200);
ObjectSetInteger(0, objName, OBJPROP_YSIZE, 120);
ObjectSetInteger(0, objName, OBJPROP_BGCOLOR, clrWhiteSmoke);
ObjectSetInteger(0, objName, OBJPROP_BORDER_TYPE, BORDER_FLAT);
ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, objName, OBJPROP_BACK, false);
}
// Update info text
string infoText = "=== Paper Trading ===\n";
infoText += StringFormat("Balance: %.2f %s\n", m_balance, m_account.Currency());
infoText += StringFormat("Equity: %.2f %s\n", m_equity, m_account.Currency());
infoText += StringFormat("Trades: %d (%dW/%dL)\n",
m_totalTrades, m_winningTrades, m_losingTrades);
infoText += StringFormat("Win Rate: %.1f%%\n", GetWinRate());
infoText += StringFormat("Status: %s",
m_liveTrading ? "LIVE TRADING" : "PAPER TRADING");
objName = prefix + "InfoText";
if(ObjectFind(0, objName) < 0) {
ObjectCreate(0, objName, OBJ_LABEL, 0, 0, 0);
ObjectSetInteger(0, objName, OBJPROP_XDISTANCE, 15);
ObjectSetInteger(0, objName, OBJPROP_YDISTANCE, 15);
ObjectSetInteger(0, objName, OBJPROP_CORNER, CORNER_LEFT_UPPER);
ObjectSetInteger(0, objName, OBJPROP_COLOR, clrBlack);
ObjectSetString(0, objName, OBJPROP_FONT, "Arial");
ObjectSetInteger(0, objName, OBJPROP_FONTSIZE, 8);
}
ObjectSetString(0, objName, OBJPROP_TEXT, infoText);
// Redraw chart
ChartRedraw();
}
//+------------------------------------------------------------------+