//+------------------------------------------------------------------+ //| 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 #include #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(); } //+------------------------------------------------------------------+