342 lines
13 KiB
MQL5
342 lines
13 KiB
MQL5
//+------------------------------------------------------------------+
|
|
//| 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
|
|
//+------------------------------------------------------------------+
|