531 lines
18 KiB
Plaintext
531 lines
18 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| PartialCloseManager.mqh |
|
|
//| Universal Buffer Reader EA v2.0 |
|
|
//+------------------------------------------------------------------+
|
|
#property copyright "Copyright 2025"
|
|
#property link ""
|
|
#property version "1.00"
|
|
#property strict
|
|
|
|
#include <Trade\Trade.mqh>
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| CPartialCloseManager - Manages multiple TPs and partial closes |
|
|
//+------------------------------------------------------------------+
|
|
class CPartialCloseManager
|
|
{
|
|
private:
|
|
bool m_enable_partial_close;
|
|
bool m_use_equal_division;
|
|
double m_partial_close_percentages[];
|
|
|
|
int m_magic_number;
|
|
string m_symbol;
|
|
CTrade *m_trade;
|
|
|
|
// TP tracking to prevent duplicate closes
|
|
struct TPTracking
|
|
{
|
|
ulong ticket;
|
|
int last_closed_tp;
|
|
datetime last_check_time;
|
|
};
|
|
|
|
TPTracking m_tp_tracking[];
|
|
int m_max_tracking_records;
|
|
|
|
// Logging
|
|
bool m_enable_debug;
|
|
|
|
public:
|
|
//+------------------------------------------------------------------+
|
|
//| TP lot allocation structure |
|
|
//+------------------------------------------------------------------+
|
|
struct TPLotAllocation
|
|
{
|
|
double lots;
|
|
double percentage;
|
|
};
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Constructor |
|
|
//+------------------------------------------------------------------+
|
|
CPartialCloseManager()
|
|
{
|
|
m_enable_partial_close = true;
|
|
m_use_equal_division = true;
|
|
|
|
ArrayResize(m_partial_close_percentages, 0);
|
|
|
|
m_magic_number = 0;
|
|
m_symbol = "";
|
|
m_enable_debug = false;
|
|
m_max_tracking_records = 100;
|
|
|
|
ArrayResize(m_tp_tracking, 0);
|
|
|
|
m_trade = new CTrade();
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Destructor |
|
|
//+------------------------------------------------------------------+
|
|
~CPartialCloseManager()
|
|
{
|
|
if(m_trade != NULL)
|
|
{
|
|
delete m_trade;
|
|
m_trade = NULL;
|
|
}
|
|
|
|
ArrayResize(m_partial_close_percentages, 0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Set parameters |
|
|
//+------------------------------------------------------------------+
|
|
void SetParameters(
|
|
bool enable_partial_close,
|
|
bool use_equal_division,
|
|
double &partial_close_percentages[],
|
|
int magic_number,
|
|
string symbol,
|
|
bool enable_debug = false
|
|
)
|
|
{
|
|
m_enable_partial_close = enable_partial_close;
|
|
m_use_equal_division = use_equal_division;
|
|
|
|
// Validate and copy percentages
|
|
int pct_count = ArraySize(partial_close_percentages);
|
|
ArrayResize(m_partial_close_percentages, pct_count);
|
|
|
|
double total_pct = 0;
|
|
for(int i = 0; i < pct_count; i++)
|
|
{
|
|
if(partial_close_percentages[i] <= 0)
|
|
{
|
|
Print("[WARNING] Invalid partial close percentage at index ", i, ": ",
|
|
partial_close_percentages[i], ". Using 0%");
|
|
m_partial_close_percentages[i] = 0;
|
|
}
|
|
else if(partial_close_percentages[i] > 100)
|
|
{
|
|
Print("[WARNING] Partial close percentage > 100% at index ", i, ": ",
|
|
partial_close_percentages[i], ". Using 100%");
|
|
m_partial_close_percentages[i] = 100;
|
|
}
|
|
else
|
|
{
|
|
m_partial_close_percentages[i] = partial_close_percentages[i];
|
|
}
|
|
|
|
total_pct += m_partial_close_percentages[i];
|
|
}
|
|
|
|
if(!m_use_equal_division && MathAbs(total_pct - 100.0) > 0.01)
|
|
{
|
|
Print("[WARNING] Partial close percentages don't sum to 100%: ", total_pct, "%");
|
|
}
|
|
|
|
m_magic_number = magic_number;
|
|
m_symbol = symbol;
|
|
m_enable_debug = enable_debug;
|
|
|
|
m_trade.SetExpertMagicNumber(m_magic_number);
|
|
|
|
if(m_enable_debug)
|
|
{
|
|
Print("[PartialCloseManager] Parameters set:");
|
|
Print(" Enable partial close: ", m_enable_partial_close);
|
|
Print(" Use equal division: ", m_use_equal_division);
|
|
Print(" Partial close percentages: ", pct_count, " levels");
|
|
if(!m_use_equal_division)
|
|
{
|
|
for(int i = 0; i < pct_count; i++)
|
|
{
|
|
Print(" TP", (i + 1), ": ", m_partial_close_percentages[i], "%");
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Set debug mode |
|
|
//+------------------------------------------------------------------+
|
|
void SetDebugMode(bool enable_debug)
|
|
{
|
|
m_enable_debug = enable_debug;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check if any TP has been reached and execute partial close |
|
|
//+------------------------------------------------------------------+
|
|
void CheckAndExecutePartialCloses()
|
|
{
|
|
if(!m_enable_partial_close)
|
|
{
|
|
if(m_enable_debug)
|
|
{
|
|
Print("[PartialCloseManager] Partial close disabled");
|
|
}
|
|
return;
|
|
}
|
|
|
|
// Iterate through all positions
|
|
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
|
{
|
|
if(!PositionSelectByTicket(PositionGetTicket(i))) continue;
|
|
if(PositionGetString(POSITION_SYMBOL) != m_symbol) continue;
|
|
if(PositionGetInteger(POSITION_MAGIC) != m_magic_number) continue;
|
|
|
|
ulong ticket = PositionGetInteger(POSITION_TICKET);
|
|
double total_lots = PositionGetDouble(POSITION_VOLUME);
|
|
|
|
if(m_enable_debug)
|
|
{
|
|
Print("[PartialCloseManager] Checking position #", ticket, " (", total_lots, " lots)");
|
|
}
|
|
|
|
// Get TP prices from comment
|
|
double tp_prices[];
|
|
int tp_count = 0;
|
|
if(!GetTPPricesFromComment(ticket, tp_prices, tp_count))
|
|
{
|
|
if(m_enable_debug)
|
|
{
|
|
Print("[PartialCloseManager] No TPs found in comment for ticket #", ticket);
|
|
}
|
|
continue;
|
|
}
|
|
|
|
if(m_enable_debug)
|
|
{
|
|
Print("[PartialCloseManager] Found ", tp_count, " TP levels for ticket #", ticket);
|
|
}
|
|
|
|
// Get last closed TP for this ticket
|
|
int last_closed_tp = GetLastClosedTP(ticket);
|
|
|
|
// Check each TP level
|
|
for(int tp_index = last_closed_tp; tp_index < tp_count; tp_index++)
|
|
{
|
|
if(IsTPReached(ticket, tp_index, tp_prices))
|
|
{
|
|
// Calculate close lots
|
|
TPLotAllocation allocation = CalculateTPLotAllocation(total_lots, tp_index, tp_count);
|
|
|
|
if(m_enable_debug)
|
|
{
|
|
Print("[PartialCloseManager] TP", (tp_index + 1), " reached for ticket #", ticket);
|
|
Print(" Close lots: ", allocation.lots, " (", allocation.percentage, "%)");
|
|
}
|
|
|
|
// Execute partial close
|
|
if(allocation.lots > 0)
|
|
{
|
|
if(ExecutePartialClose(ticket, allocation.lots, tp_index))
|
|
{
|
|
// Update tracking
|
|
UpdateTPTracking(ticket, tp_index + 1);
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate lot size for each TP level |
|
|
//+------------------------------------------------------------------+
|
|
TPLotAllocation CalculateTPLotAllocation(double total_lots, int tp_index, int total_tp_count)
|
|
{
|
|
TPLotAllocation result;
|
|
result.lots = 0;
|
|
result.percentage = 0;
|
|
|
|
if(m_use_equal_division)
|
|
{
|
|
result.lots = CalculateEqualDivisionLots(total_lots, tp_index, total_tp_count);
|
|
result.percentage = 100.0 / total_tp_count;
|
|
}
|
|
else
|
|
{
|
|
result.lots = CalculateCustomPercentageLots(total_lots, tp_index);
|
|
if(tp_index < ArraySize(m_partial_close_percentages))
|
|
{
|
|
result.percentage = m_partial_close_percentages[tp_index];
|
|
}
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
private:
|
|
//+------------------------------------------------------------------+
|
|
//| Get TP prices from position comment |
|
|
//+------------------------------------------------------------------+
|
|
bool GetTPPricesFromComment(ulong ticket, double &tp_prices[], int &tp_count)
|
|
{
|
|
if(!PositionSelectByTicket(ticket)) return false;
|
|
|
|
string comment = PositionGetString(POSITION_COMMENT);
|
|
tp_count = 0;
|
|
ArrayResize(tp_prices, 0);
|
|
|
|
// Parse comment for TP values
|
|
// Format: "UnivBufEA_24680_H1;TP1=1.2530;TP2=1.2560;TP3=1.2600;..."
|
|
|
|
int tp_index = 1;
|
|
while(true)
|
|
{
|
|
string search_str = ";TP" + IntegerToString(tp_index) + "=";
|
|
int pos = StringFind(comment, search_str);
|
|
|
|
if(pos == -1) break;
|
|
|
|
// Extract TP value
|
|
int start_pos = pos + StringLen(search_str);
|
|
int end_pos = StringFind(comment, ";", start_pos);
|
|
|
|
if(end_pos == -1) end_pos = StringLen(comment);
|
|
|
|
string tp_str = StringSubstr(comment, start_pos, end_pos - start_pos);
|
|
double tp_value = StringToDouble(tp_str);
|
|
|
|
ArrayResize(tp_prices, tp_count + 1);
|
|
tp_prices[tp_count] = tp_value;
|
|
tp_count++;
|
|
|
|
tp_index++;
|
|
}
|
|
|
|
return (tp_count > 0);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Check if specific TP level has been reached |
|
|
//+------------------------------------------------------------------+
|
|
bool IsTPReached(ulong ticket, int tp_index, double &tp_prices[])
|
|
{
|
|
if(!PositionSelectByTicket(ticket)) return false;
|
|
if(tp_index >= ArraySize(tp_prices)) return false;
|
|
|
|
double tp_price = tp_prices[tp_index];
|
|
if(tp_price == 0) return false;
|
|
|
|
ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
|
|
double current_price = (pos_type == POSITION_TYPE_BUY) ?
|
|
SymbolInfoDouble(m_symbol, SYMBOL_BID) :
|
|
SymbolInfoDouble(m_symbol, SYMBOL_ASK);
|
|
|
|
// Check if TP has been reached
|
|
if(pos_type == POSITION_TYPE_BUY)
|
|
{
|
|
return (current_price >= tp_price);
|
|
}
|
|
else
|
|
{
|
|
return (current_price <= tp_price);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Execute partial close for specific TP level |
|
|
//+------------------------------------------------------------------+
|
|
bool ExecutePartialClose(ulong ticket, double close_lots, int tp_index)
|
|
{
|
|
if(!PositionSelectByTicket(ticket))
|
|
{
|
|
Print("[ERROR] Failed to select position #", ticket);
|
|
return false;
|
|
}
|
|
|
|
double current_lots = PositionGetDouble(POSITION_VOLUME);
|
|
|
|
if(close_lots <= 0)
|
|
{
|
|
Print("[ERROR] Invalid close lots: ", close_lots, " for ticket #", ticket);
|
|
return false;
|
|
}
|
|
|
|
if(close_lots > current_lots)
|
|
{
|
|
Print("[WARNING] Close lots (", close_lots, ") > current lots (", current_lots,
|
|
") for ticket #", ticket, ". Adjusting to current lots.");
|
|
close_lots = current_lots;
|
|
}
|
|
|
|
ENUM_POSITION_TYPE pos_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
|
|
|
|
bool result = false;
|
|
if(pos_type == POSITION_TYPE_BUY)
|
|
{
|
|
result = m_trade.Sell(close_lots, m_symbol, 0, 0, 0, "Partial Close TP" + IntegerToString(tp_index + 1));
|
|
}
|
|
else
|
|
{
|
|
result = m_trade.Buy(close_lots, m_symbol, 0, 0, 0, "Partial Close TP" + IntegerToString(tp_index + 1));
|
|
}
|
|
|
|
if(result)
|
|
{
|
|
Print("[PartialCloseManager] Partial close executed: ", close_lots, " lots at TP", (tp_index + 1),
|
|
" for ticket #", ticket);
|
|
UpdateCommentAfterPartialClose(ticket, tp_index + 1);
|
|
}
|
|
else
|
|
{
|
|
Print("[ERROR] Failed to execute partial close for ticket #", ticket,
|
|
". Error: ", m_trade.ResultRetcodeDescription());
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Update position comment after partial close |
|
|
//+------------------------------------------------------------------+
|
|
bool UpdateCommentAfterPartialClose(ulong ticket, int tp_index)
|
|
{
|
|
if(!PositionSelectByTicket(ticket)) return false;
|
|
|
|
string comment = PositionGetString(POSITION_COMMENT);
|
|
|
|
// Update BE flag if TP1 was closed
|
|
if(tp_index == 1)
|
|
{
|
|
// Replace BE=0 with BE=1
|
|
string be_old = ";BE=0";
|
|
string be_new = ";BE=1";
|
|
comment = StringReplace(comment, be_old, be_new);
|
|
}
|
|
|
|
// Update TS flag if all TPs closed
|
|
// Check if this was the last TP
|
|
double tp_prices[];
|
|
int tp_count = 0;
|
|
if(GetTPPricesFromComment(ticket, tp_prices, tp_count))
|
|
{
|
|
if(tp_index >= tp_count)
|
|
{
|
|
// All TPs closed, activate trailing
|
|
string ts_old = ";TS=0";
|
|
string ts_new = ";TS=ACTIVE";
|
|
comment = StringReplace(comment, ts_old, ts_new);
|
|
}
|
|
}
|
|
|
|
// Update comment
|
|
if(m_trade.PositionModify(ticket, PositionGetDouble(POSITION_SL), PositionGetDouble(POSITION_TP)))
|
|
{
|
|
if(m_enable_debug)
|
|
{
|
|
Print("[PartialCloseManager] Comment updated for ticket #", ticket, ": ", comment);
|
|
}
|
|
return true;
|
|
}
|
|
else
|
|
{
|
|
Print("[ERROR] Failed to update comment for ticket #", ticket);
|
|
return false;
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Get last closed TP for ticket |
|
|
//+------------------------------------------------------------------+
|
|
int GetLastClosedTP(ulong ticket)
|
|
{
|
|
for(int i = 0; i < ArraySize(m_tp_tracking); i++)
|
|
{
|
|
if(m_tp_tracking[i].ticket == ticket)
|
|
{
|
|
return m_tp_tracking[i].last_closed_tp;
|
|
}
|
|
}
|
|
return 0;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Update TP tracking |
|
|
//+------------------------------------------------------------------+
|
|
void UpdateTPTracking(ulong ticket, int tp_level)
|
|
{
|
|
// Check if ticket already exists in tracking
|
|
for(int i = 0; i < ArraySize(m_tp_tracking); i++)
|
|
{
|
|
if(m_tp_tracking[i].ticket == ticket)
|
|
{
|
|
m_tp_tracking[i].last_closed_tp = tp_level;
|
|
m_tp_tracking[i].last_check_time = TimeCurrent();
|
|
return;
|
|
}
|
|
}
|
|
|
|
// Add new tracking record
|
|
int size = ArraySize(m_tp_tracking);
|
|
if(size >= m_max_tracking_records)
|
|
{
|
|
// Remove oldest record
|
|
ArrayCopy(m_tp_tracking, m_tp_tracking, 0, 1, size - 1);
|
|
size = m_max_tracking_records - 1;
|
|
}
|
|
|
|
ArrayResize(m_tp_tracking, size + 1);
|
|
m_tp_tracking[size].ticket = ticket;
|
|
m_tp_tracking[size].last_closed_tp = tp_level;
|
|
m_tp_tracking[size].last_check_time = TimeCurrent();
|
|
|
|
if(m_enable_debug)
|
|
{
|
|
Print("[PartialCloseManager] Added tracking for ticket #", ticket, ", TP level: ", tp_level);
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Clear TP tracking for ticket |
|
|
//+------------------------------------------------------------------+
|
|
void ClearTPTracking(ulong ticket)
|
|
{
|
|
for(int i = 0; i < ArraySize(m_tp_tracking); i++)
|
|
{
|
|
if(m_tp_tracking[i].ticket == ticket)
|
|
{
|
|
ArrayCopy(m_tp_tracking, m_tp_tracking, i, i + 1, ArraySize(m_tp_tracking) - i - 1);
|
|
ArrayResize(m_tp_tracking, ArraySize(m_tp_tracking) - 1);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate equal division lots |
|
|
//+------------------------------------------------------------------+
|
|
double CalculateEqualDivisionLots(double total_lots, int tp_index, int total_tp_count)
|
|
{
|
|
if(total_tp_count == 0) return 0;
|
|
|
|
double equal_lots = total_lots / total_tp_count;
|
|
|
|
// Last TP gets remaining lots
|
|
if(tp_index == total_tp_count - 1)
|
|
{
|
|
return total_lots - (equal_lots * (total_tp_count - 1));
|
|
}
|
|
|
|
return equal_lots;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Calculate custom percentage lots |
|
|
//+------------------------------------------------------------------+
|
|
double CalculateCustomPercentageLots(double total_lots, int tp_index)
|
|
{
|
|
if(tp_index >= ArraySize(m_partial_close_percentages)) return 0;
|
|
|
|
double percentage = m_partial_close_percentages[tp_index];
|
|
return total_lots * (percentage / 100.0);
|
|
}
|
|
};
|
|
//+------------------------------------------------------------------+ |