//+------------------------------------------------------------------+ //| positionManagement.mqh | //| Breakeven SL Manager (devKit) | //| | //| PURPOSE | //| ------- | //| Moves the stop-loss of an open position to break-even (entry ± | //| a spread-buffer) once the current price has reached a | //| user-defined fraction of the original Risk-to-Reward target. | //| | //| KEY CONCEPTS | //| ----------- | //| • RR Trigger – integer percentage of RR to reach before BE | //| fires. E.g. 50 = "move SL to BE once price is halfway to | //| TP." Range 1-100. | //| • Spread buffer – a multiple of the current Ask-Bid spread is | //| added beyond entry so the SL sits safely past the spread and | //| doesn't get tagged by normal re-quotes. | //| • The class is stateful per ticket – once BE has been applied | //| to a ticket it will never try to move it again. | //| | //| QUICK-START | //| ----------- | //| #include "../devKit/positionManagement.mqh" | //| | //| // Inputs | //| input int InpRRTriggerPct = 50; // % of RR to trigger BE | //| input double InpSpreadMult = 1.5; // spread buffer multiplier | //| | //| CPositionManager posMgr(InpRRTriggerPct, InpSpreadMult); | //| | //| void OnTick() { | //| posMgr.ManageAll(); | //| } | //+------------------------------------------------------------------+ #pragma once #include // CTrade – used to modify SL #include // CPositionInfo – position access //------------------------------------------------------------------- // Internal record for one tracked ticket //------------------------------------------------------------------- struct BERecord { ulong ticket; // position ticket bool be_done; // true once BE has been applied }; //------------------------------------------------------------------- // CPositionManager //------------------------------------------------------------------- class CPositionManager { private: //--- user configuration int m_rr_trigger_pct; // 1-100 : % of RR before BE fires double m_spread_multiplier; // safety spread buffer (e.g. 1.5) //--- internal state BERecord m_records[]; int m_record_count; CTrade m_trade; CPositionInfo m_pos; //--- helpers --------------------------------------------------- //--- Find record index for a ticket; -1 if not found int FindRecord(ulong ticket) const { for(int i = 0; i < m_record_count; i++) if(m_records[i].ticket == ticket) return i; return -1; } //--- Ensure a record exists; return its index int EnsureRecord(ulong ticket) { int idx = FindRecord(ticket); if(idx != -1) return idx; ArrayResize(m_records, m_record_count + 1); m_records[m_record_count].ticket = ticket; m_records[m_record_count].be_done = false; return m_record_count++; } //--- Purge records whose tickets are no longer open void PurgeStaleRecords() { for(int i = m_record_count - 1; i >= 0; i--) { if(!PositionSelectByTicket(m_records[i].ticket)) { // Remove by swapping with the last element m_records[i] = m_records[m_record_count - 1]; m_record_count--; ArrayResize(m_records, m_record_count); } } } //--- Calculate the BE stop level for a position // Returns EMPTY_VALUE on error. double CalcBEStop(ENUM_POSITION_TYPE posType, const string symbol, double entryPrice) const { double spread = SymbolInfoDouble(symbol, SYMBOL_ASK) - SymbolInfoDouble(symbol, SYMBOL_BID); double buffer = spread * m_spread_multiplier; double point = SymbolInfoDouble(symbol, SYMBOL_POINT); int stop_level_pts = (int)SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL); double min_dist = stop_level_pts * point; double be_sl; if(posType == POSITION_TYPE_BUY) { // For a BUY: SL moves above entry by the buffer so it is // protected from spread but not so high it's in profit territory be_sl = entryPrice + buffer; // Respect broker's minimum stop distance from Ask double ask = SymbolInfoDouble(symbol, SYMBOL_ASK); if((ask - be_sl) < min_dist) be_sl = ask - min_dist; } else // SELL { // For a SELL: SL moves below entry by the buffer be_sl = entryPrice - buffer; // Respect broker's minimum stop distance from Bid double bid = SymbolInfoDouble(symbol, SYMBOL_BID); if((be_sl - bid) < min_dist) be_sl = bid + min_dist; } // Normalise to symbol digits int digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS); be_sl = NormalizeDouble(be_sl, digits); return be_sl; } //--- Returns true when current price has covered >= m_rr_trigger_pct // of the distance between entry and TP bool RRTriggerReached(ENUM_POSITION_TYPE posType, const string symbol, double entryPrice, double tp) const { // No TP defined → cannot compute RR fraction if(tp == 0.0) return false; double totalRR = MathAbs(tp - entryPrice); if(totalRR <= 0) return false; double currentPrice; double covered; if(posType == POSITION_TYPE_BUY) { currentPrice = SymbolInfoDouble(symbol, SYMBOL_BID); // bid is exit for buy covered = currentPrice - entryPrice; } else { currentPrice = SymbolInfoDouble(symbol, SYMBOL_ASK); // ask is exit for sell covered = entryPrice - currentPrice; } if(covered <= 0) return false; // price hasn't moved in the right direction double pct = (covered / totalRR) * 100.0; return (pct >= (double)m_rr_trigger_pct); } //--- Attempt to move SL of a single position to break-even // Returns true on success bool ApplyBE(ulong ticket) { if(!m_pos.SelectByTicket(ticket)) { PrintFormat("CPositionManager::ApplyBE – cannot select ticket #%I64u", ticket); return false; } ENUM_POSITION_TYPE posType = m_pos.PositionType(); string symbol = m_pos.Symbol(); double entryPrice = m_pos.PriceOpen(); double tp = m_pos.TakeProfit(); double currentSL = m_pos.StopLoss(); double be_sl = CalcBEStop(posType, symbol, entryPrice); if(be_sl == EMPTY_VALUE) { Print("CPositionManager::ApplyBE – CalcBEStop returned EMPTY_VALUE for #", ticket); return false; } // Safety: never move SL in the wrong direction if(posType == POSITION_TYPE_BUY && be_sl <= currentSL) return false; if(posType == POSITION_TYPE_SELL && currentSL > 0 && be_sl >= currentSL) return false; bool ok = m_trade.PositionModify(ticket, be_sl, tp); if(ok) PrintFormat("CPositionManager – BE applied to #%I64u | entry=%.5f | new SL=%.5f", ticket, entryPrice, be_sl); else PrintFormat("CPositionManager – SL modify FAILED for #%I64u | error=%d", ticket, GetLastError()); return ok; } public: //--- Constructor // rr_trigger_pct : 1-100. Price must cover this % of the entry→TP // distance before BE fires. Default 50 (half-RR). // spread_mult : how many spreads to add as a buffer beyond entry. // Default 1.5. CPositionManager(int rr_trigger_pct = 50, double spread_mult = 1.5) { m_rr_trigger_pct = MathMax(1, MathMin(100, rr_trigger_pct)); m_spread_multiplier = MathMax(0.0, spread_mult); m_record_count = 0; ArrayResize(m_records, 0); } ~CPositionManager() {} //--- Optional: change the RR trigger percentage at runtime void SetRRTrigger(int pct) { m_rr_trigger_pct = MathMax(1, MathMin(100, pct)); } //--- Optional: change the spread multiplier at runtime void SetSpreadMultiplier(double mult) { m_spread_multiplier = MathMax(0.0, mult); } //================================================================ // MAIN ENTRY POINT – call once per OnTick() //================================================================ //--- Iterate every open position on the account and apply BE logic. // Positions without a TP are skipped (RR cannot be computed). void ManageAll() { PurgeStaleRecords(); int total = PositionsTotal(); for(int i = 0; i < total; i++) { ulong ticket = PositionGetTicket(i); if(ticket == 0) continue; int idx = EnsureRecord(ticket); if(m_records[idx].be_done) continue; // already moved for this ticket if(!m_pos.SelectByTicket(ticket)) continue; ENUM_POSITION_TYPE posType = m_pos.PositionType(); string symbol = m_pos.Symbol(); double entryPrice = m_pos.PriceOpen(); double tp = m_pos.TakeProfit(); if(tp == 0.0) continue; // no TP → skip if(RRTriggerReached(posType, symbol, entryPrice, tp)) { if(ApplyBE(ticket)) m_records[idx].be_done = true; } } } //--- Manage only a specific ticket instead of all positions. // Useful when your EA tracks its own ticket directly. void ManageTicket(ulong ticket) { if(ticket == 0) return; PurgeStaleRecords(); int idx = EnsureRecord(ticket); if(m_records[idx].be_done) return; if(!m_pos.SelectByTicket(ticket)) return; ENUM_POSITION_TYPE posType = m_pos.PositionType(); string symbol = m_pos.Symbol(); double entryPrice = m_pos.PriceOpen(); double tp = m_pos.TakeProfit(); if(tp == 0.0) return; if(RRTriggerReached(posType, symbol, entryPrice, tp)) { if(ApplyBE(ticket)) m_records[idx].be_done = true; } } //--- Reset the BE record for a ticket so it can be evaluated again. // Use this if you reopen a position and want fresh BE tracking. void ResetTicket(ulong ticket) { int idx = FindRecord(ticket); if(idx != -1) m_records[idx].be_done = false; } //--- Query: has BE already been applied to this ticket? bool IsBEDone(ulong ticket) const { int idx = FindRecord(ticket); return (idx != -1) ? m_records[idx].be_done : false; } //--- Diagnostics: dump all tracked records to the Experts log void PrintStatus() const { PrintFormat("--- CPositionManager | RR trigger=%d%% | spread mult=%.2f | records=%d ---", m_rr_trigger_pct, m_spread_multiplier, m_record_count); for(int i = 0; i < m_record_count; i++) PrintFormat(" [%d] ticket=#%I64u | BE done=%s", i, m_records[i].ticket, (m_records[i].be_done ? "YES" : "no")); Print("-----------------------------------------------------------"); } }; //+------------------------------------------------------------------+ // END OF positionManagement.mqh //+------------------------------------------------------------------+