devKit/positionManagement.mqh

342 lines
13 KiB
MQL5
Raw Permalink Normal View History

2026-07-26 19:35:52 +03:00
//+------------------------------------------------------------------+
//| 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 <Trade\Trade.mqh> // CTrade – used to modify SL
#include <Trade\PositionInfo.mqh> // 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
//+------------------------------------------------------------------+