//+------------------------------------------------------------------+ //| XAUBot_PositionManager.mqh | //| Phase 2: Basket Management + Protect Positions | //| Inspired by: Gold 1 Minute Grid EA | //+------------------------------------------------------------------+ #property copyright "XAUBot AI" #property version "1.00" #property strict #include "XAUBot_Config.mqh" #include #include //+------------------------------------------------------------------+ //| Position Basket Structure | //+------------------------------------------------------------------+ struct PositionBasket { ulong tickets[]; datetime first_open_time; datetime last_open_time; double total_profit_usd; double total_lots; double avg_entry_price; int position_count; bool has_protect; ulong protect_ticket; }; //+------------------------------------------------------------------+ //| Protect Position Structure | //+------------------------------------------------------------------+ struct ProtectPosition { ulong parent_ticket; ulong protect_ticket; datetime created_time; double protect_lot; int layer; // 1, 2, or 3 }; //+------------------------------------------------------------------+ //| Position Manager Class | //+------------------------------------------------------------------+ class CPositionManager { private: CTrade m_trade; CPositionInfo m_position; PositionBasket m_baskets[]; ProtectPosition m_protects[]; // Basket parameters int m_basket_window_minutes; double m_basket_tp_usd; // Protect parameters bool m_enable_protect; double m_protect_trigger_pips; double m_protect_lot_multiplier; int m_max_protect_layers; // Helper functions void GroupPositionsIntoBaskets(int magic_number); double CalculateBasketProfit(const PositionBasket &basket); bool ShouldOpenProtect(ulong ticket, double &loss_pips); void UpdateProtectPositions(); public: CPositionManager(); ~CPositionManager(); bool Init(int magic_number); void Deinit(); // Basket management void UpdateBaskets(int magic_number); bool CheckBasketTP(int magic_number); void CloseBasket(const PositionBasket &basket); // Protect logic void CheckProtectTriggers(int magic_number); bool OpenProtectPosition(ulong parent_ticket, double loss_pips); // Getters int GetBasketCount() { return ArraySize(m_baskets); } int GetProtectCount() { return ArraySize(m_protects); } }; //+------------------------------------------------------------------+ //| Constructor | //+------------------------------------------------------------------+ CPositionManager::CPositionManager() { m_basket_window_minutes = 60; // Group positions within 1 hour m_basket_tp_usd = 50.0; // Close basket when total profit >= $50 m_enable_protect = true; m_protect_trigger_pips = 30.0; // Open protect after -30 pips loss m_protect_lot_multiplier = 0.5; // Protect size = 50% of original m_max_protect_layers = 1; // Max 1 protect per position (safe) } //+------------------------------------------------------------------+ //| Destructor | //+------------------------------------------------------------------+ CPositionManager::~CPositionManager() { Deinit(); } //+------------------------------------------------------------------+ //| Initialize | //+------------------------------------------------------------------+ bool CPositionManager::Init(int magic_number) { m_trade.SetExpertMagicNumber(magic_number); m_trade.SetDeviationInPoints(10); m_trade.SetTypeFilling(ORDER_FILLING_FOK); ArrayResize(m_baskets, 0); ArrayResize(m_protects, 0); Print("Position Manager Initialized"); Print(" Basket Window: ", m_basket_window_minutes, " minutes"); Print(" Basket TP: $", m_basket_tp_usd); Print(" Protect Logic: ", (m_enable_protect ? "ENABLED" : "DISABLED")); return true; } //+------------------------------------------------------------------+ //| Deinitialize | //+------------------------------------------------------------------+ void CPositionManager::Deinit() { ArrayFree(m_baskets); ArrayFree(m_protects); } //+------------------------------------------------------------------+ //| Update Baskets | //+------------------------------------------------------------------+ void CPositionManager::UpdateBaskets(int magic_number) { GroupPositionsIntoBaskets(magic_number); } //+------------------------------------------------------------------+ //| Group Positions into Baskets | //+------------------------------------------------------------------+ void CPositionManager::GroupPositionsIntoBaskets(int magic_number) { ArrayResize(m_baskets, 0); ulong all_tickets[]; datetime all_times[]; int count = 0; // Collect all positions for(int i = 0; i < PositionsTotal(); i++) { if(m_position.SelectByIndex(i)) { if(m_position.Magic() == magic_number) { ArrayResize(all_tickets, count + 1); ArrayResize(all_times, count + 1); all_tickets[count] = m_position.Ticket(); all_times[count] = m_position.Time(); count++; } } } if(count == 0) return; // Sort by time for(int i = 0; i < count - 1; i++) { for(int j = i + 1; j < count; j++) { if(all_times[i] > all_times[j]) { datetime temp_time = all_times[i]; all_times[i] = all_times[j]; all_times[j] = temp_time; ulong temp_ticket = all_tickets[i]; all_tickets[i] = all_tickets[j]; all_tickets[j] = temp_ticket; } } } // Group into baskets (positions within time window) int basket_count = 0; PositionBasket current_basket; ArrayResize(current_basket.tickets, 0); current_basket.first_open_time = 0; current_basket.has_protect = false; for(int i = 0; i < count; i++) { if(ArraySize(current_basket.tickets) == 0) { // Start new basket ArrayResize(current_basket.tickets, 1); current_basket.tickets[0] = all_tickets[i]; current_basket.first_open_time = all_times[i]; current_basket.last_open_time = all_times[i]; } else { // Check if within time window int time_diff_minutes = (int)((all_times[i] - current_basket.first_open_time) / 60); if(time_diff_minutes <= m_basket_window_minutes) { // Add to current basket int size = ArraySize(current_basket.tickets); ArrayResize(current_basket.tickets, size + 1); current_basket.tickets[size] = all_tickets[i]; current_basket.last_open_time = all_times[i]; } else { // Save current basket and start new one ArrayResize(m_baskets, basket_count + 1); m_baskets[basket_count] = current_basket; basket_count++; // Start new basket ArrayResize(current_basket.tickets, 1); current_basket.tickets[0] = all_tickets[i]; current_basket.first_open_time = all_times[i]; current_basket.last_open_time = all_times[i]; current_basket.has_protect = false; } } } // Save last basket if(ArraySize(current_basket.tickets) > 0) { ArrayResize(m_baskets, basket_count + 1); m_baskets[basket_count] = current_basket; } } //+------------------------------------------------------------------+ //| Check Basket TP | //+------------------------------------------------------------------+ bool CPositionManager::CheckBasketTP(int magic_number) { if(!g_config.use_basket_management) return false; UpdateBaskets(magic_number); for(int i = 0; i < ArraySize(m_baskets); i++) { double basket_profit = CalculateBasketProfit(m_baskets[i]); if(basket_profit >= m_basket_tp_usd) { Print("📦 BASKET TP HIT: $", basket_profit, " (target: $", m_basket_tp_usd, ")"); CloseBasket(m_baskets[i]); return true; } } return false; } //+------------------------------------------------------------------+ //| Calculate Basket Profit | //+------------------------------------------------------------------+ double CPositionManager::CalculateBasketProfit(const PositionBasket &basket) { double total_profit = 0; for(int i = 0; i < ArraySize(basket.tickets); i++) { if(m_position.SelectByTicket(basket.tickets[i])) { total_profit += m_position.Profit() + m_position.Swap() + m_position.Commission(); } } return total_profit; } //+------------------------------------------------------------------+ //| Close Basket | //+------------------------------------------------------------------+ void CPositionManager::CloseBasket(const PositionBasket &basket) { Print("Closing basket with ", ArraySize(basket.tickets), " positions..."); for(int i = 0; i < ArraySize(basket.tickets); i++) { if(m_position.SelectByTicket(basket.tickets[i])) { m_trade.PositionClose(basket.tickets[i]); Print(" ✅ Closed ticket ", basket.tickets[i]); } } } //+------------------------------------------------------------------+ //| Check Protect Triggers | //+------------------------------------------------------------------+ void CPositionManager::CheckProtectTriggers(int magic_number) { if(!m_enable_protect) return; for(int i = 0; i < PositionsTotal(); i++) { if(!m_position.SelectByIndex(i)) continue; if(m_position.Magic() != magic_number) continue; // Check if already has protect bool has_protect = false; for(int j = 0; j < ArraySize(m_protects); j++) { if(m_protects[j].parent_ticket == m_position.Ticket()) { has_protect = true; break; } } if(has_protect) continue; // Check if should open protect double loss_pips; if(ShouldOpenProtect(m_position.Ticket(), loss_pips)) { OpenProtectPosition(m_position.Ticket(), loss_pips); } } UpdateProtectPositions(); } //+------------------------------------------------------------------+ //| Should Open Protect | //+------------------------------------------------------------------+ bool CPositionManager::ShouldOpenProtect(ulong ticket, double &loss_pips) { if(!m_position.SelectByTicket(ticket)) return false; double open_price = m_position.PriceOpen(); double current_price = m_position.PriceCurrent(); double point = SymbolInfoDouble(m_position.Symbol(), SYMBOL_POINT); if(m_position.PositionType() == POSITION_TYPE_BUY) loss_pips = (open_price - current_price) / point / 10; // Loss in pips else loss_pips = (current_price - open_price) / point / 10; if(loss_pips >= m_protect_trigger_pips) { Print("⚠️ Protect trigger for ticket ", ticket, ": Loss ", loss_pips, " pips"); return true; } return false; } //+------------------------------------------------------------------+ //| Open Protect Position | //+------------------------------------------------------------------+ bool CPositionManager::OpenProtectPosition(ulong parent_ticket, double loss_pips) { if(!m_position.SelectByTicket(parent_ticket)) return false; // Calculate protect lot size (50% of original) double original_lot = m_position.Volume(); double protect_lot = original_lot * m_protect_lot_multiplier; // Normalize lot double min_lot = SymbolInfoDouble(m_position.Symbol(), SYMBOL_VOLUME_MIN); double lot_step = SymbolInfoDouble(m_position.Symbol(), SYMBOL_VOLUME_STEP); protect_lot = MathMax(protect_lot, min_lot); protect_lot = MathFloor(protect_lot / lot_step) * lot_step; // Get current price double entry_price; ENUM_POSITION_TYPE pos_type = m_position.PositionType(); if(pos_type == POSITION_TYPE_BUY) entry_price = SymbolInfoDouble(m_position.Symbol(), SYMBOL_ASK); else entry_price = SymbolInfoDouble(m_position.Symbol(), SYMBOL_BID); // Calculate SL/TP (same as parent) double sl = m_position.StopLoss(); double tp = m_position.TakeProfit(); // Open protect position (same direction, better price) bool result = false; if(pos_type == POSITION_TYPE_BUY) result = m_trade.Buy(protect_lot, m_position.Symbol(), entry_price, sl, tp, "PROTECT_" + IntegerToString(parent_ticket)); else result = m_trade.Sell(protect_lot, m_position.Symbol(), entry_price, sl, tp, "PROTECT_" + IntegerToString(parent_ticket)); if(result) { ProtectPosition protect; protect.parent_ticket = parent_ticket; protect.protect_ticket = m_trade.ResultOrder(); protect.created_time = TimeCurrent(); protect.protect_lot = protect_lot; protect.layer = 1; int size = ArraySize(m_protects); ArrayResize(m_protects, size + 1); m_protects[size] = protect; Print("🛡️ PROTECT OPENED: Parent=", parent_ticket, " Protect=", protect.protect_ticket, " Lot=", protect_lot, " Loss=", loss_pips, " pips"); return true; } Print("❌ Failed to open protect position: ", m_trade.ResultRetcodeDescription()); return false; } //+------------------------------------------------------------------+ //| Update Protect Positions | //+------------------------------------------------------------------+ void CPositionManager::UpdateProtectPositions() { // Remove protects if parent closed for(int i = ArraySize(m_protects) - 1; i >= 0; i--) { bool parent_exists = false; for(int j = 0; j < PositionsTotal(); j++) { if(m_position.SelectByIndex(j)) { if(m_position.Ticket() == m_protects[i].parent_ticket) { parent_exists = true; break; } } } if(!parent_exists) { // Parent closed, remove protect from array Print("Parent ", m_protects[i].parent_ticket, " closed, removing protect tracking"); // Shift array for(int k = i; k < ArraySize(m_protects) - 1; k++) { m_protects[k] = m_protects[k + 1]; } ArrayResize(m_protects, ArraySize(m_protects) - 1); } } } //+------------------------------------------------------------------+