587 lines
21 KiB
MQL5
587 lines
21 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| PaperTrading.mqh |
| |||
//| Copyright 2023, MetaQuotes Software Corp. |
| |||
//| https://www.mql5.com |
| |||
//+------------------------------------------------------------------+
| |||
#property copyright "Copyright 2023, MetaQuotes Software Corp."
| |||
#property link "https://www.mql5.com"
| |||
#property version "1.00"
| |||
#property strict
| |||
| |||
#include <Arrays\ArrayObj.mqh>
| |||
#include <Trade\PositionInfo.mqh>
| |||
#include "TradeExecutor.mqh"
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Paper Trade Position Class |
| |||
//+------------------------------------------------------------------+
| |||
class CPosition : public CObject
| |||
{
| |||
public:
| |||
ulong ticket; // Position ticket
| |||
string symbol; // Symbol
| |||
ENUM_POSITION_TYPE type; // Position type
| |||
double volume; // Position volume
| |||
double openPrice; // Open price
| |||
double stopLoss; // Stop loss level
| |||
double takeProfit; // Take profit level
| |||
datetime openTime; // Open time
| |||
| |||
// Constructor
| |||
CPosition(ulong t, string s, ENUM_POSITION_TYPE pt, double v, double op,
| |||
double sl, double tp, datetime ot) :
| |||
ticket(t), symbol(s), type(pt), volume(v), openPrice(op),
| |||
stopLoss(sl), takeProfit(tp), openTime(ot) {}
| |||
| |||
// Check if position is closed
| |||
bool IsClosed() const { return volume <= 0; }
| |||
| |||
// Calculate current profit
| |||
double CalculateProfit(double currentBid, double currentAsk) const
| |||
{
| |||
if(IsClosed()) return 0.0;
| |||
| |||
double profit = 0.0;
| |||
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
| |||
| |||
if(type == POSITION_TYPE_BUY)
| |||
{
| |||
profit = (currentBid - openPrice) * volume * SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE) / point;
| |||
}
| |||
else if(type == POSITION_TYPE_SELL)
| |||
{
| |||
profit = (openPrice - currentAsk) * volume * SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE) / point;
| |||
}
| |||
| |||
return profit;
| |||
}
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Paper Trading Class |
| |||
//+------------------------------------------------------------------+
| |||
class CPaperTrading
| |||
{
| |||
private:
| |||
CArrayObj m_positions; // Array of open positions
| |||
double m_balance; // Paper trading balance
| |||
double m_equity; // Current equity
| |||
double m_margin; // Used margin
| |||
double m_freeMargin; // Free margin
| |||
| |||
// Simulation parameters
| |||
bool m_simulateSlippage; // Simulate slippage
| |||
bool m_simulatePartialFills; // Simulate partial fills
| |||
double m_maxSlippagePips; // Maximum slippage in pips
| |||
double m_fillProbability; // Probability of fill (0-1)
| |||
| |||
// Statistics
| |||
int m_totalTrades; // Total number of trades
| |||
int m_winningTrades; // Number of winning trades
| |||
int m_losingTrades; // Number of losing trades
| |||
double m_totalProfit; // Total profit
| |||
double m_totalLoss; // Total loss
| |||
| |||
// Objects
| |||
CSymbolInfo *m_symbol; // Pointer to symbol info
| |||
| |||
// Private methods
| |||
double ApplySlippage(double price, ENUM_ORDER_TYPE type);
| |||
double ApplyPartialFill(double requestedVolume);
| |||
void UpdateMetrics();
| |||
| |||
public:
| |||
CPaperTrading();
| |||
~CPaperTrading();
| |||
| |||
// Initialization
| |||
bool Initialize(CSymbolInfo *symbol, double initialBalance = 10000.0,
| |||
bool simulateSlippage = true, bool simulatePartialFills = true,
| |||
double maxSlippagePips = 1.0, double fillProbability = 0.95);
| |||
| |||
// Trading operations
| |||
bool OpenPosition(ENUM_ORDER_TYPE type, double lots, double price,
| |||
double sl = 0, double tp = 0);
| |||
bool ClosePosition(ulong ticket, double lots = 0);
| |||
bool ModifyPosition(ulong ticket, double sl, double tp);
| |||
| |||
// Position management
| |||
bool SelectPosition(ulong ticket, CPosition *&position);
| |||
int TotalPositions() const { return m_positions.Total(); }
| |||
| |||
// Account information
| |||
double GetBalance() const { return m_balance; }
| |||
double GetEquity() const { return m_equity; }
| |||
double GetMargin() const { return m_margin; }
| |||
double GetFreeMargin() const { return m_freeMargin; }
| |||
| |||
// Statistics
| |||
int GetTotalTrades() const { return m_totalTrades; }
| |||
int GetWinningTrades() const { return m_winningTrades; }
| |||
int GetLosingTrades() const { return m_losingTrades; }
| |||
double GetTotalProfit() const { return m_totalProfit; }
| |||
double GetTotalLoss() const { return m_totalLoss; }
| |||
double GetWinRate() const
| |||
{
| |||
return (m_totalTrades > 0) ? (double)m_winningTrades / m_totalTrades * 100.0 : 0.0;
| |||
}
| |||
| |||
// Visualization
| |||
void UpdateVisualization();
| |||
| |||
// Update method called on each tick
| |||
void Update();
| |||
| |||
// Check if live trading is allowed based on paper trading results
| |||
bool IsLiveTradingAllowed();
| |||
};
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Constructor |
| |||
//+------------------------------------------------------------------+
| |||
CPaperTrading::CPaperTrading() : m_balance(10000.0),
| |||
m_equity(10000.0),
| |||
m_margin(0.0),
| |||
m_freeMargin(10000.0),
| |||
m_simulateSlippage(true),
| |||
m_simulatePartialFills(true),
| |||
m_maxSlippagePips(1.0),
| |||
m_fillProbability(0.95),
| |||
m_totalTrades(0),
| |||
m_winningTrades(0),
| |||
m_losingTrades(0),
| |||
m_totalProfit(0.0),
| |||
m_totalLoss(0.0),
| |||
m_symbol(NULL)
| |||
{
| |||
m_positions.Clear();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Destructor |
| |||
//+------------------------------------------------------------------+
| |||
CPaperTrading::~CPaperTrading()
| |||
{
| |||
m_positions.Clear();
| |||
m_symbol = NULL;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Initialize paper trading |
| |||
//+------------------------------------------------------------------+
| |||
bool CPaperTrading::Initialize(CSymbolInfo *symbol, double initialBalance,
| |||
bool simulateSlippage, bool simulatePartialFills,
| |||
double maxSlippagePips, double fillProbability)
| |||
{
| |||
if(symbol == NULL || initialBalance <= 0)
| |||
{
| |||
Print("Error: Invalid parameters in PaperTrading initialization");
| |||
return false;
| |||
}
| |||
| |||
m_symbol = symbol;
| |||
m_balance = initialBalance;
| |||
m_equity = initialBalance;
| |||
m_freeMargin = initialBalance;
| |||
| |||
// Set simulation parameters
| |||
m_simulateSlippage = simulateSlippage;
| |||
m_simulatePartialFills = simulatePartialFills;
| |||
m_maxSlippagePips = maxSlippagePips;
| |||
m_fillProbability = MathMax(0.0, MathMin(1.0, fillProbability));
| |||
| |||
// Initialize statistics
| |||
m_totalTrades = 0;
| |||
m_winningTrades = 0;
| |||
m_losingTrades = 0;
| |||
m_totalProfit = 0.0;
| |||
m_totalLoss = 0.0;
| |||
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Open a new position |
| |||
//+------------------------------------------------------------------+
| |||
bool CPaperTrading::OpenPosition(ENUM_ORDER_TYPE type, double lots, double price,
| |||
double sl, double tp)
| |||
{
| |||
if(m_symbol == NULL || lots <= 0 || price <= 0)
| |||
{
| |||
Print("Error: Invalid parameters in OpenPosition");
| |||
return false;
| |||
}
| |||
| |||
// Apply slippage if enabled
| |||
double executionPrice = ApplySlippage(price, type);
| |||
| |||
// Apply partial fill if enabled
| |||
double filledLots = m_simulatePartialFills ?
| |||
ApplyPartialFill(lots) : lots;
| |||
| |||
if(filledLots <= 0)
| |||
{
| |||
Print("Order not filled (partial fill simulation)");
| |||
return false;
| |||
}
| |||
| |||
// Generate a unique ticket
| |||
static ulong lastTicket = 1000;
| |||
ulong ticket = ++lastTicket;
| |||
| |||
// Create and add the position
| |||
CPosition *position = new CPosition(ticket, m_symbol.Name(),
| |||
(type == ORDER_TYPE_BUY) ? POSITION_TYPE_BUY : POSITION_TYPE_SELL,
| |||
filledLots, executionPrice, sl, tp, TimeCurrent());
| |||
| |||
if(position == NULL)
| |||
{
| |||
Print("Error: Failed to create position object");
| |||
return false;
| |||
}
| |||
| |||
m_positions.Add(position);
| |||
| |||
// Update metrics
| |||
UpdateMetrics();
| |||
| |||
// Update statistics
| |||
m_totalTrades++;
| |||
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Close a position |
| |||
//+------------------------------------------------------------------+
| |||
bool CPaperTrading::ClosePosition(ulong ticket, double lots = 0)
| |||
{
| |||
CPosition *position = NULL;
| |||
| |||
// Find the position
| |||
for(int i = 0; i < m_positions.Total(); i++)
| |||
{
| |||
CPosition *pos = m_positions.At(i);
| |||
if(pos != NULL && pos.ticket == ticket && !pos.IsClosed())
| |||
{
| |||
position = pos;
| |||
break;
| |||
}
| |||
}
| |||
| |||
if(position == NULL)
| |||
{
| |||
Print("Error: Position not found or already closed");
| |||
return false;
| |||
}
| |||
| |||
// Calculate close price with slippage
| |||
double closePrice = ApplySlippage(
| |||
(position.type == POSITION_TYPE_BUY) ? m_symbol.Bid() : m_symbol.Ask(),
| |||
(position.type == POSITION_TYPE_BUY) ? ORDER_TYPE_SELL : ORDER_TYPE_BUY
| |||
);
| |||
| |||
// Calculate profit/loss
| |||
double profit = 0.0;
| |||
double point = m_symbol.Point();
| |||
double tickValue = m_symbol.TickValue();
| |||
| |||
if(position.type == POSITION_TYPE_BUY)
| |||
{
| |||
profit = (closePrice - position.openPrice) * position.volume * tickValue / point;
| |||
}
| |||
else // POSITION_TYPE_SELL
| |||
{
| |||
profit = (position.openPrice - closePrice) * position.volume * tickValue / point;
| |||
}
| |||
| |||
// Update balance and statistics
| |||
m_balance += profit;
| |||
| |||
if(profit >= 0)
| |||
{
| |||
m_winningTrades++;
| |||
m_totalProfit += profit;
| |||
}
| |||
else
| |||
{
| |||
m_losingTrades++;
| |||
m_totalLoss -= profit; // Loss is stored as positive value
| |||
}
| |||
| |||
// Close the position or reduce its volume
| |||
if(lots <= 0 || lots >= position.volume)
| |||
{
| |||
// Close the entire position
| |||
position.volume = 0;
| |||
}
| |||
else
| |||
{
| |||
// Partial close
| |||
position.volume -= lots;
| |||
}
| |||
| |||
// Update metrics
| |||
UpdateMetrics();
| |||
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Modify position's stop loss and take profit |
| |||
//+------------------------------------------------------------------+
| |||
bool CPaperTrading::ModifyPosition(ulong ticket, double sl, double tp)
| |||
{
| |||
CPosition *position = NULL;
| |||
| |||
// Find the position
| |||
for(int i = 0; i < m_positions.Total(); i++)
| |||
{
| |||
CPosition *pos = m_positions.At(i);
| |||
if(pos != NULL && pos.ticket == ticket && !pos.IsClosed())
| |||
{
| |||
position = pos;
| |||
break;
| |||
}
| |||
}
| |||
| |||
if(position == NULL)
| |||
{
| |||
Print("Error: Position not found or already closed");
| |||
return false;
| |||
}
| |||
| |||
// Update stop loss and take profit
| |||
position.stopLoss = sl;
| |||
position.takeProfit = tp;
| |||
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Select position by ticket |
| |||
//+------------------------------------------------------------------+
| |||
bool CPaperTrading::SelectPosition(ulong ticket, CPosition *&position)
| |||
{
| |||
for(int i = 0; i < m_positions.Total(); i++)
| |||
{
| |||
CPosition *pos = m_positions.At(i);
| |||
if(pos != NULL && pos.ticket == ticket && !pos.IsClosed())
| |||
{
| |||
position = pos;
| |||
return true;
| |||
}
| |||
}
| |||
| |||
position = NULL;
| |||
return false;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Apply slippage to price |
| |||
//+------------------------------------------------------------------+
| |||
double CPaperTrading::ApplySlippage(double price, ENUM_ORDER_TYPE type)
| |||
{
| |||
if(!m_simulateSlippage || m_maxSlippagePips <= 0)
| |||
return price;
| |||
| |||
// Generate random slippage (can be positive or negative)
| |||
double slippagePips = ((double)MathRand() / 32767.0 - 0.5) * 2.0 * m_maxSlippagePips;
| |||
double point = m_symbol.Point();
| |||
| |||
// For buy orders, slippage is added to the price (worse execution)
| |||
// For sell orders, slippage is subtracted from the price (worse execution)
| |||
if(type == ORDER_TYPE_BUY)
| |||
return price + slippagePips * point;
| |||
else
| |||
return price - slippagePips * point;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Apply partial fill to order |
| |||
//+------------------------------------------------------------------
| |||
double CPaperTrading::ApplyPartialFill(double requestedVolume)
| |||
{
| |||
if(!m_simulatePartialFills || m_fillProbability >= 1.0)
| |||
return requestedVolume;
| |||
| |||
// Check if order is filled based on probability
| |||
if(((double)MathRand() / 32767.0) > m_fillProbability)
| |||
return 0.0; // Order not filled
| |||
| |||
// For simplicity, we either fill the entire order or nothing
| |||
// In a more advanced simulation, you could implement partial fills
| |||
return requestedVolume;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Update account metrics |
| |||
//+------------------------------------------------------------------+
| |||
void CPaperTrading::UpdateMetrics()
| |||
{
| |||
if(m_symbol == NULL)
| |||
return;
| |||
| |||
// Update current prices
| |||
m_symbol.RefreshRates();
| |||
| |||
// Calculate equity and margin
| |||
double floatingProfit = 0.0;
| |||
m_margin = 0.0;
| |||
| |||
// Calculate floating P/L and used margin for all open positions
| |||
for(int i = 0; i < m_positions.Total(); i++)
| |||
{
| |||
CPosition *pos = m_positions.At(i);
| |||
if(pos != NULL && !pos.IsClosed())
| |||
{
| |||
// Calculate floating profit/loss
| |||
if(pos.type == POSITION_TYPE_BUY)
| |||
{
| |||
floatingProfit += (m_symbol.Bid() - pos.openPrice) * pos.volume *
| |||
m_symbol.TickValue() / m_symbol.Point();
| |||
}
| |||
else // POSITION_TYPE_SELL
| |||
{
| |||
floatingProfit += (pos.openPrice - m_symbol.Ask()) * pos.volume *
| |||
m_symbol.TickValue() / m_symbol.Point();
| |||
}
| |||
| |||
// Calculate margin (simplified)
| |||
m_margin += pos.volume * pos.openPrice / 100.0; // 1:100 leverage
| |||
}
| |||
}
| |||
| |||
// Update equity and free margin
| |||
m_equity = m_balance + floatingProfit;
| |||
m_freeMargin = m_equity - m_margin;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Update method called on each tick |
| |||
//+------------------------------------------------------------------+
| |||
void CPaperTrading::Update()
| |||
{
| |||
// Update metrics and visualization
| |||
UpdateMetrics();
| |||
UpdateVisualization();
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Check if live trading is allowed based on paper trading results |
| |||
//+------------------------------------------------------------------+
| |||
bool CPaperTrading::IsLiveTradingAllowed()
| |||
{
| |||
// Check if we have enough trades
| |||
if(m_totalTrades < 5) // Minimum number of trades required
| |||
return false;
| |||
| |||
// Check win rate (e.g., at least 60% win rate)
| |||
double winRate = GetWinRate();
| |||
if(winRate < 60.0)
| |||
return false;
| |||
| |||
// Check profit factor (e.g., at least 1.5)
| |||
double profitFactor = (m_totalLoss != 0) ? m_totalProfit / m_totalLoss : m_totalProfit;
| |||
if(profitFactor < 1.5)
| |||
return false;
| |||
| |||
// All conditions met, allow live trading
| |||
return true;
| |||
}
| |||
| |||
//+------------------------------------------------------------------+
| |||
//| Update visualization |
| |||
//+------------------------------------------------------------------+
| |||
void CPaperTrading::UpdateVisualization()
| |||
{
| |||
// Update metrics first
| |||
UpdateMetrics();
| |||
| |||
// Create or update panel
| |||
string panelName = "PaperTrading_Panel";
| |||
| |||
// Create panel background
| |||
if(ObjectFind(0, panelName) < 0)
| |||
{
| |||
ObjectCreate(0, panelName, OBJ_RECTANGLE_LABEL, 0, 0, 0);
| |||
ObjectSetInteger(0, panelName, OBJPROP_XDISTANCE, 10);
| |||
ObjectSetInteger(0, panelName, OBJPROP_YDISTANCE, 150);
| |||
ObjectSetInteger(0, panelName, OBJPROP_XSIZE, 200);
| |||
ObjectSetInteger(0, panelName, OBJPROP_YSIZE, 120);
| |||
ObjectSetInteger(0, panelName, OBJPROP_BGCOLOR, clrWhiteSmoke);
| |||
ObjectSetInteger(0, panelName, OBJPROP_BORDER_TYPE, BORDER_FLAT);
| |||
ObjectSetInteger(0, panelName, OBJPROP_CORNER, CORNER_RIGHT_UPPER);
| |||
ObjectSetInteger(0, panelName, OBJPROP_COLOR, clrDimGray);
| |||
ObjectSetInteger(0, panelName, OBJPROP_BACK, false);
| |||
}
| |||
| |||
// Create or update title
| |||
string titleName = panelName + "_Title";
| |||
if(ObjectFind(0, titleName) < 0)
| |||
{
| |||
ObjectCreate(0, titleName, OBJ_LABEL, 0, 0, 0);
| |||
ObjectSetInteger(0, titleName, OBJPROP_COLOR, clrBlack);
| |||
ObjectSetInteger(0, titleName, OBJPROP_CORNER, CORNER_RIGHT_UPPER);
| |||
ObjectSetInteger(0, titleName, OBJPROP_XDISTANCE, 20);
| |||
ObjectSetInteger(0, titleName, OBJPROP_YDISTANCE, 160);
| |||
ObjectSetInteger(0, titleName, OBJPROP_FONTSIZE, 10);
| |||
}
| |||
ObjectSetString(0, titleName, OBJPROP_TEXT, "PAPER TRADING");
| |||
| |||
// Create or update balance label
| |||
string balanceName = panelName + "_Balance";
| |||
if(ObjectFind(0, balanceName) < 0)
| |||
{
| |||
ObjectCreate(0, balanceName, OBJ_LABEL, 0, 0, 0);
| |||
ObjectSetInteger(0, balanceName, OBJPROP_COLOR, clrBlack);
| |||
ObjectSetInteger(0, balanceName, OBJPROP_CORNER, CORNER_RIGHT_UPPER);
| |||
ObjectSetInteger(0, balanceName, OBJPROP_XDISTANCE, 20);
| |||
ObjectSetInteger(0, balanceName, OBJPROP_YDISTANCE, 180);
| |||
ObjectSetInteger(0, balanceName, OBJPROP_FONTSIZE, 9);
| |||
}
| |||
ObjectSetString(0, balanceName, OBJPROP_TEXT, "Balance: " + DoubleToString(m_balance, 2));
| |||
| |||
// Create or update equity label
| |||
string equityName = panelName + "_Equity";
| |||
if(ObjectFind(0, equityName) < 0)
| |||
{
| |||
ObjectCreate(0, equityName, OBJ_LABEL, 0, 0, 0);
| |||
ObjectSetInteger(0, equityName, OBJPROP_COLOR, clrBlack);
| |||
ObjectSetInteger(0, equityName, OBJPROP_CORNER, CORNER_RIGHT_UPPER);
| |||
ObjectSetInteger(0, equityName, OBJPROP_XDISTANCE, 20);
| |||
ObjectSetInteger(0, equityName, OBJPROP_YDISTANCE, 200);
| |||
ObjectSetInteger(0, equityName, OBJPROP_FONTSIZE, 9);
| |||
}
| |||
ObjectSetString(0, equityName, OBJPROP_TEXT, "Equity: " + DoubleToString(m_equity, 2));
| |||
| |||
// Create or update margin label
| |||
string marginName = panelName + "_Margin";
| |||
if(ObjectFind(0, marginName) < 0)
| |||
{
| |||
ObjectCreate(0, marginName, OBJ_LABEL, 0, 0, 0);
| |||
ObjectSetInteger(0, marginName, OBJPROP_COLOR, clrBlack);
| |||
ObjectSetInteger(0, marginName, OBJPROP_CORNER, CORNER_RIGHT_UPPER);
| |||
ObjectSetInteger(0, marginName, OBJPROP_XDISTANCE, 20);
| |||
ObjectSetInteger(0, marginName, OBJPROP_YDISTANCE, 220);
| |||
ObjectSetInteger(0, marginName, OBJPROP_FONTSIZE, 9);
| |||
}
| |||
ObjectSetString(0, marginName, OBJPROP_TEXT, "Margin: " + DoubleToString(m_margin, 2));
| |||
| |||
// Create or update free margin label
| |||
string freeMarginName = panelName + "_FreeMargin";
| |||
if(ObjectFind(0, freeMarginName) < 0)
| |||
{
| |||
ObjectCreate(0, freeMarginName, OBJ_LABEL, 0, 0, 0);
| |||
ObjectSetInteger(0, freeMarginName, OBJPROP_COLOR, clrBlack);
| |||
ObjectSetInteger(0, freeMarginName, OBJPROP_CORNER, CORNER_RIGHT_UPPER);
| |||
ObjectSetInteger(0, freeMarginName, OBJPROP_XDISTANCE, 20);
| |||
ObjectSetInteger(0, freeMarginName, OBJPROP_YDISTANCE, 240);
| |||
ObjectSetInteger(0, freeMarginName, OBJPROP_FONTSIZE, 9);
| |||
}
| |||
ObjectSetString(0, freeMarginName, OBJPROP_TEXT, "Free Margin: " + DoubleToString(m_freeMargin, 2));
| |||
| |||
// Update the chart
| |||
ChartRedraw();
| |||
}
| |||
//+------------------------------------------------------------------+
|