Add cluster audit pipeline, united EA updates, brochure generators, and publication hygiene (gitignore, MT5 path desensitization, pre-upload scan). Remove tracked reports, models, and binary artifacts from the repo. Co-authored-by: Cursor <cursoragent@cursor.com>
561 lines
15 KiB
Plaintext
561 lines
15 KiB
Plaintext
//+------------------------------------------------------------------+
|
||
//| USDJPYBusterStrategy.mqh |
|
||
//| Ian-style USDJPY Asian range breakout for United EA cluster |
|
||
//+------------------------------------------------------------------+
|
||
#ifndef USDJPY_BUSTER_STRATEGY_MQH
|
||
#define USDJPY_BUSTER_STRATEGY_MQH
|
||
|
||
enum ENUM_UB_RISK_MODE
|
||
{
|
||
UB_RISK_FIXED_MONEY = 0,
|
||
UB_RISK_PERCENT = 1,
|
||
UB_RISK_FIXED_LOTS = 2
|
||
};
|
||
|
||
struct USDJPYBusterData
|
||
{
|
||
string symbol;
|
||
bool isInitialized;
|
||
CTrade trade;
|
||
|
||
int rangeStartHour;
|
||
int rangeEndHour;
|
||
int closeHour;
|
||
ENUM_TIMEFRAMES rangeTF;
|
||
int minRangePoints;
|
||
double orderBufferPoints;
|
||
|
||
bool firstTradeOnly;
|
||
bool allowLong;
|
||
bool allowShort;
|
||
bool useTakeProfit;
|
||
double takeProfitPoints;
|
||
|
||
ENUM_UB_RISK_MODE riskMode;
|
||
double fixedRiskMoney;
|
||
double riskPercent;
|
||
double fixedLots;
|
||
|
||
int magic;
|
||
int slippage;
|
||
int maxSpreadPoints;
|
||
bool drawRange;
|
||
bool debugLog;
|
||
bool closeUnprofitableOnNewSignal;
|
||
|
||
int dayKey;
|
||
double rangeHigh;
|
||
double rangeLow;
|
||
bool rangeBuilt;
|
||
bool rangeSkipDay;
|
||
bool ordersPlaced;
|
||
bool dayClosed;
|
||
bool firstFillDone;
|
||
int entriesToday;
|
||
int lastPosCount;
|
||
};
|
||
|
||
//+------------------------------------------------------------------+
|
||
int UB_DayKey(const datetime t)
|
||
{
|
||
MqlDateTime dt;
|
||
TimeToStruct(t, dt);
|
||
return dt.year * 10000 + dt.mon * 100 + dt.day;
|
||
}
|
||
|
||
datetime UB_DayStart(const datetime t)
|
||
{
|
||
MqlDateTime dt;
|
||
TimeToStruct(t, dt);
|
||
dt.hour = 0;
|
||
dt.min = 0;
|
||
dt.sec = 0;
|
||
return StructToTime(dt);
|
||
}
|
||
|
||
void UB_ResetDayState(USDJPYBusterData &d)
|
||
{
|
||
d.rangeHigh = 0.0;
|
||
d.rangeLow = 0.0;
|
||
d.rangeBuilt = false;
|
||
d.rangeSkipDay = false;
|
||
d.ordersPlaced = false;
|
||
d.dayClosed = false;
|
||
d.firstFillDone = false;
|
||
d.entriesToday = 0;
|
||
d.lastPosCount = 0;
|
||
}
|
||
|
||
void UB_Dbg(USDJPYBusterData &d, const string msg)
|
||
{
|
||
if(d.debugLog)
|
||
Print("USDJPYBuster: ", msg);
|
||
}
|
||
|
||
double UB_NormalizeLots(const string sym, double lots)
|
||
{
|
||
const double mn = SymbolInfoDouble(sym, SYMBOL_VOLUME_MIN);
|
||
const double mx = SymbolInfoDouble(sym, SYMBOL_VOLUME_MAX);
|
||
const double st = SymbolInfoDouble(sym, SYMBOL_VOLUME_STEP);
|
||
if(st > 0.0)
|
||
lots = MathFloor(lots / st) * st;
|
||
if(lots < mn)
|
||
lots = mn;
|
||
if(lots > mx)
|
||
lots = mx;
|
||
return lots;
|
||
}
|
||
|
||
double UB_NormalizePrice(const string sym, const double price)
|
||
{
|
||
const int dg = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS);
|
||
return NormalizeDouble(price, dg);
|
||
}
|
||
|
||
double UB_MinStopDistance(const string sym)
|
||
{
|
||
const double pt = SymbolInfoDouble(sym, SYMBOL_POINT);
|
||
const long lvl = SymbolInfoInteger(sym, SYMBOL_TRADE_STOPS_LEVEL);
|
||
return MathMax((double)lvl * pt, pt);
|
||
}
|
||
|
||
bool UB_SpreadOk(const USDJPYBusterData &d)
|
||
{
|
||
return ((double)SymbolInfoInteger(d.symbol, SYMBOL_SPREAD) <= (double)d.maxSpreadPoints);
|
||
}
|
||
|
||
bool UB_MoneyPerLotAtSl(const string sym, const ENUM_ORDER_TYPE type,
|
||
const double openPrice, const double slPrice, double &lossPerLot)
|
||
{
|
||
lossPerLot = 0.0;
|
||
double p = 0.0;
|
||
if(!OrderCalcProfit(type, sym, 1.0, openPrice, slPrice, p))
|
||
return false;
|
||
lossPerLot = MathAbs(p);
|
||
return (lossPerLot > 0.0);
|
||
}
|
||
|
||
double UB_LotsForOrder(USDJPYBusterData &d, const ENUM_ORDER_TYPE type,
|
||
const double entry, const double sl, const double scaledFixedLots)
|
||
{
|
||
if(d.riskMode == UB_RISK_FIXED_LOTS)
|
||
return UB_NormalizeLots(d.symbol, scaledFixedLots);
|
||
|
||
double perLotLoss = 0.0;
|
||
if(!UB_MoneyPerLotAtSl(d.symbol, type, entry, sl, perLotLoss) || perLotLoss <= 0.0)
|
||
return UB_NormalizeLots(d.symbol, scaledFixedLots);
|
||
|
||
double riskMoney = d.fixedRiskMoney;
|
||
if(d.riskMode == UB_RISK_PERCENT)
|
||
riskMoney = AccountInfoDouble(ACCOUNT_BALANCE) * (d.riskPercent / 100.0);
|
||
|
||
if(riskMoney <= 0.0)
|
||
return UB_NormalizeLots(d.symbol, scaledFixedLots);
|
||
|
||
return UB_NormalizeLots(d.symbol, riskMoney / perLotLoss);
|
||
}
|
||
|
||
bool UB_BuildRange(USDJPYBusterData &d, const datetime serverNow, double &hi, double &lo)
|
||
{
|
||
hi = -DBL_MAX;
|
||
lo = DBL_MAX;
|
||
|
||
const datetime day0 = UB_DayStart(serverNow);
|
||
const datetime tStart = day0 + (datetime)d.rangeStartHour * 3600;
|
||
const datetime tEnd = day0 + (datetime)d.rangeEndHour * 3600;
|
||
if(tEnd <= tStart)
|
||
return false;
|
||
|
||
MqlRates rates[];
|
||
const int copied = CopyRates(d.symbol, d.rangeTF, tStart, tEnd, rates);
|
||
if(copied <= 0)
|
||
return false;
|
||
|
||
for(int i = 0; i < copied; i++)
|
||
{
|
||
if(rates[i].time < tStart || rates[i].time >= tEnd)
|
||
continue;
|
||
hi = MathMax(hi, rates[i].high);
|
||
lo = MathMin(lo, rates[i].low);
|
||
}
|
||
|
||
if(hi <= -DBL_MAX || lo >= DBL_MAX || hi <= lo)
|
||
return false;
|
||
|
||
const double pt = SymbolInfoDouble(d.symbol, SYMBOL_POINT);
|
||
if((hi - lo) / pt < (double)d.minRangePoints)
|
||
return false;
|
||
|
||
hi = UB_NormalizePrice(d.symbol, hi);
|
||
lo = UB_NormalizePrice(d.symbol, lo);
|
||
return true;
|
||
}
|
||
|
||
bool UB_AdjustStopsForBroker(USDJPYBusterData &d, const ENUM_ORDER_TYPE type,
|
||
const double entry, double &sl, double &tp)
|
||
{
|
||
const double minD = UB_MinStopDistance(d.symbol);
|
||
if(minD <= 0.0)
|
||
return true;
|
||
|
||
if(type == ORDER_TYPE_BUY || type == ORDER_TYPE_BUY_STOP)
|
||
{
|
||
if(entry - sl < minD)
|
||
sl = entry - minD;
|
||
if(d.useTakeProfit && tp > 0.0 && tp - entry < minD)
|
||
tp = entry + minD;
|
||
}
|
||
else
|
||
{
|
||
if(sl - entry < minD)
|
||
sl = entry + minD;
|
||
if(d.useTakeProfit && tp > 0.0 && entry - tp < minD)
|
||
tp = entry - minD;
|
||
}
|
||
sl = UB_NormalizePrice(d.symbol, sl);
|
||
if(d.useTakeProfit)
|
||
tp = UB_NormalizePrice(d.symbol, tp);
|
||
return true;
|
||
}
|
||
|
||
bool UB_BuyStopValid(const string sym, const double buyStopPrice)
|
||
{
|
||
MqlTick tick;
|
||
if(!SymbolInfoTick(sym, tick))
|
||
return false;
|
||
return (buyStopPrice > tick.ask + UB_MinStopDistance(sym));
|
||
}
|
||
|
||
bool UB_SellStopValid(const string sym, const double sellStopPrice)
|
||
{
|
||
MqlTick tick;
|
||
if(!SymbolInfoTick(sym, tick))
|
||
return false;
|
||
return (sellStopPrice < tick.bid - UB_MinStopDistance(sym));
|
||
}
|
||
|
||
int UB_CountMagicPositions(USDJPYBusterData &d)
|
||
{
|
||
int n = 0;
|
||
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
||
{
|
||
const ulong t = PositionGetTicket(i);
|
||
if(t == 0 || !PositionSelectByTicket(t))
|
||
continue;
|
||
if(PositionGetString(POSITION_SYMBOL) != d.symbol)
|
||
continue;
|
||
if((int)PositionGetInteger(POSITION_MAGIC) != d.magic)
|
||
continue;
|
||
n++;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
int UB_CountMagicPendings(USDJPYBusterData &d)
|
||
{
|
||
int n = 0;
|
||
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
||
{
|
||
const ulong ticket = OrderGetTicket(i);
|
||
if(ticket == 0 || !OrderSelect(ticket))
|
||
continue;
|
||
if(OrderGetString(ORDER_SYMBOL) != d.symbol)
|
||
continue;
|
||
if((int)OrderGetInteger(ORDER_MAGIC) != d.magic)
|
||
continue;
|
||
n++;
|
||
}
|
||
return n;
|
||
}
|
||
|
||
void UB_DeleteAllMagicPendings(USDJPYBusterData &d)
|
||
{
|
||
for(int i = OrdersTotal() - 1; i >= 0; i--)
|
||
{
|
||
const ulong ticket = OrderGetTicket(i);
|
||
if(ticket == 0 || !OrderSelect(ticket))
|
||
continue;
|
||
if(OrderGetString(ORDER_SYMBOL) != d.symbol)
|
||
continue;
|
||
if((int)OrderGetInteger(ORDER_MAGIC) != d.magic)
|
||
continue;
|
||
d.trade.OrderDelete(ticket);
|
||
}
|
||
}
|
||
|
||
void UB_CloseAllMagicPositions(USDJPYBusterData &d)
|
||
{
|
||
for(int i = PositionsTotal() - 1; i >= 0; i--)
|
||
{
|
||
const ulong t = PositionGetTicket(i);
|
||
if(t == 0 || !PositionSelectByTicket(t))
|
||
continue;
|
||
if(PositionGetString(POSITION_SYMBOL) != d.symbol)
|
||
continue;
|
||
if((int)PositionGetInteger(POSITION_MAGIC) != d.magic)
|
||
continue;
|
||
d.trade.PositionClose(t);
|
||
}
|
||
}
|
||
|
||
void UB_EndOfDayClose(USDJPYBusterData &d)
|
||
{
|
||
UB_CloseAllMagicPositions(d);
|
||
UB_DeleteAllMagicPendings(d);
|
||
d.dayClosed = true;
|
||
d.ordersPlaced = false;
|
||
}
|
||
|
||
int UB_MaxEntriesPerDay(const USDJPYBusterData &d)
|
||
{
|
||
return (d.firstTradeOnly ? 1 : 2);
|
||
}
|
||
|
||
void UB_TrackEntries(USDJPYBusterData &d)
|
||
{
|
||
const int pc = UB_CountMagicPositions(d);
|
||
if(pc > d.lastPosCount)
|
||
d.entriesToday += (pc - d.lastPosCount);
|
||
d.lastPosCount = pc;
|
||
|
||
if(d.entriesToday >= UB_MaxEntriesPerDay(d))
|
||
UB_DeleteAllMagicPendings(d);
|
||
}
|
||
|
||
bool UB_PlaceBreakoutOrders(USDJPYBusterData &d, const double scaledFixedLots, const double riskScale)
|
||
{
|
||
if(!UB_SpreadOk(d))
|
||
{
|
||
UB_Dbg(d, "spread too wide — retry later");
|
||
return false;
|
||
}
|
||
|
||
if(AccountInfoDouble(ACCOUNT_MARGIN_FREE) <= 0.0)
|
||
{
|
||
UB_Dbg(d, "no free margin — skip placement");
|
||
return false;
|
||
}
|
||
|
||
const double pt = SymbolInfoDouble(d.symbol, SYMBOL_POINT);
|
||
const double buf = d.orderBufferPoints * pt;
|
||
|
||
const double buyPrice = UB_NormalizePrice(d.symbol, d.rangeHigh + buf);
|
||
const double sellPrice = UB_NormalizePrice(d.symbol, d.rangeLow - buf);
|
||
const double buySl = UB_NormalizePrice(d.symbol, d.rangeLow);
|
||
const double sellSl = UB_NormalizePrice(d.symbol, d.rangeHigh);
|
||
|
||
double buyTp = 0.0, sellTp = 0.0;
|
||
if(d.useTakeProfit && d.takeProfitPoints > 0.0)
|
||
{
|
||
buyTp = UB_NormalizePrice(d.symbol, buyPrice + d.takeProfitPoints * pt);
|
||
sellTp = UB_NormalizePrice(d.symbol, sellPrice - d.takeProfitPoints * pt);
|
||
}
|
||
|
||
double buySlAdj = buySl, sellSlAdj = sellSl;
|
||
double buyTpAdj = buyTp, sellTpAdj = sellTp;
|
||
UB_AdjustStopsForBroker(d, ORDER_TYPE_BUY_STOP, buyPrice, buySlAdj, buyTpAdj);
|
||
UB_AdjustStopsForBroker(d, ORDER_TYPE_SELL_STOP, sellPrice, sellSlAdj, sellTpAdj);
|
||
|
||
const double savedFixed = d.fixedRiskMoney;
|
||
if(d.riskMode == UB_RISK_FIXED_MONEY && riskScale > 0.0)
|
||
d.fixedRiskMoney = savedFixed * riskScale;
|
||
|
||
int placed = 0;
|
||
|
||
if(d.allowLong && UB_BuyStopValid(d.symbol, buyPrice))
|
||
{
|
||
const double lots = UB_LotsForOrder(d, ORDER_TYPE_BUY, buyPrice, buySlAdj, scaledFixedLots);
|
||
if(d.trade.BuyStop(lots, buyPrice, d.symbol, buySlAdj, buyTpAdj, ORDER_TIME_DAY, 0, "UB range up"))
|
||
placed++;
|
||
else
|
||
Print("USDJPYBuster BuyStop failed ", d.trade.ResultRetcode(), " ", d.trade.ResultRetcodeDescription());
|
||
}
|
||
else if(d.allowLong)
|
||
UB_Dbg(d, "buy stop skipped — price already at/above range high");
|
||
|
||
if(d.allowShort && UB_SellStopValid(d.symbol, sellPrice))
|
||
{
|
||
const double lots = UB_LotsForOrder(d, ORDER_TYPE_SELL, sellPrice, sellSlAdj, scaledFixedLots);
|
||
if(d.trade.SellStop(lots, sellPrice, d.symbol, sellSlAdj, sellTpAdj, ORDER_TIME_DAY, 0, "UB range dn"))
|
||
placed++;
|
||
else
|
||
Print("USDJPYBuster SellStop failed ", d.trade.ResultRetcode(), " ", d.trade.ResultRetcodeDescription());
|
||
}
|
||
else if(d.allowShort)
|
||
UB_Dbg(d, "sell stop skipped — price already at/below range low");
|
||
|
||
d.fixedRiskMoney = savedFixed;
|
||
return (placed > 0);
|
||
}
|
||
|
||
void UB_HandleFirstFillRule(USDJPYBusterData &d)
|
||
{
|
||
if(!d.firstTradeOnly || d.firstFillDone)
|
||
return;
|
||
if(UB_CountMagicPositions(d) <= 0)
|
||
return;
|
||
UB_DeleteAllMagicPendings(d);
|
||
d.firstFillDone = true;
|
||
}
|
||
|
||
//+------------------------------------------------------------------+
|
||
bool InitUSDJPYBuster(USDJPYBusterData &d,
|
||
const string symbol,
|
||
const int rangeStartHour,
|
||
const int rangeEndHour,
|
||
const int closeHour,
|
||
const ENUM_TIMEFRAMES rangeTF,
|
||
const int minRangePoints,
|
||
const double orderBufferPoints,
|
||
const bool firstTradeOnly,
|
||
const bool allowLong,
|
||
const bool allowShort,
|
||
const bool useTakeProfit,
|
||
const double takeProfitPoints,
|
||
const ENUM_UB_RISK_MODE riskMode,
|
||
const double fixedRiskMoney,
|
||
const double riskPercent,
|
||
const double fixedLots,
|
||
const int magic,
|
||
const int slippage,
|
||
const int maxSpreadPoints,
|
||
const bool drawRange,
|
||
const bool debugLog = false)
|
||
{
|
||
d.symbol = symbol;
|
||
d.rangeStartHour = rangeStartHour;
|
||
d.rangeEndHour = rangeEndHour;
|
||
d.closeHour = closeHour;
|
||
d.rangeTF = rangeTF;
|
||
d.minRangePoints = minRangePoints;
|
||
d.orderBufferPoints = orderBufferPoints;
|
||
d.firstTradeOnly = firstTradeOnly;
|
||
d.allowLong = allowLong;
|
||
d.allowShort = allowShort;
|
||
d.useTakeProfit = useTakeProfit;
|
||
d.takeProfitPoints = takeProfitPoints;
|
||
d.riskMode = riskMode;
|
||
d.fixedRiskMoney = fixedRiskMoney;
|
||
d.riskPercent = riskPercent;
|
||
d.fixedLots = fixedLots;
|
||
d.magic = magic;
|
||
d.slippage = slippage;
|
||
d.maxSpreadPoints = maxSpreadPoints;
|
||
d.drawRange = drawRange;
|
||
d.debugLog = debugLog;
|
||
|
||
if(!SymbolSelect(symbol, true))
|
||
{
|
||
Print("USDJPYBuster: symbol not available: ", symbol);
|
||
d.isInitialized = false;
|
||
return false;
|
||
}
|
||
|
||
if(rangeEndHour <= rangeStartHour)
|
||
{
|
||
Print("USDJPYBuster: rangeEndHour must be > rangeStartHour");
|
||
d.isInitialized = false;
|
||
return false;
|
||
}
|
||
|
||
d.trade.SetExpertMagicNumber(magic);
|
||
d.trade.SetDeviationInPoints(slippage);
|
||
d.trade.SetTypeFillingBySymbol(symbol);
|
||
|
||
d.dayKey = UB_DayKey(TimeTradeServer());
|
||
UB_ResetDayState(d);
|
||
d.isInitialized = true;
|
||
|
||
Print("USDJPYBuster: ", symbol,
|
||
" range ", rangeStartHour, ":00–", rangeEndHour, ":00",
|
||
" place@", rangeEndHour, ":00 close@", closeHour, ":00",
|
||
" firstOnly=", (firstTradeOnly ? "Y" : "N"));
|
||
return true;
|
||
}
|
||
|
||
void DeinitUSDJPYBuster(USDJPYBusterData &d)
|
||
{
|
||
if(!d.isInitialized)
|
||
return;
|
||
d.isInitialized = false;
|
||
}
|
||
|
||
void ProcessUSDJPYBuster(USDJPYBusterData &d, const double scaledFixedLots, const double riskScale)
|
||
{
|
||
if(!d.isInitialized)
|
||
return;
|
||
|
||
const datetime now = TimeTradeServer();
|
||
MqlDateTime dt;
|
||
TimeToStruct(now, dt);
|
||
|
||
const int today = UB_DayKey(now);
|
||
if(today != d.dayKey)
|
||
{
|
||
d.dayKey = today;
|
||
UB_ResetDayState(d);
|
||
}
|
||
|
||
if(dt.hour >= d.closeHour && !d.dayClosed)
|
||
{
|
||
UB_EndOfDayClose(d);
|
||
return;
|
||
}
|
||
|
||
if(d.dayClosed)
|
||
return;
|
||
|
||
UB_TrackEntries(d);
|
||
UB_HandleFirstFillRule(d);
|
||
|
||
if(d.entriesToday >= UB_MaxEntriesPerDay(d))
|
||
{
|
||
UB_DeleteAllMagicPendings(d);
|
||
d.ordersPlaced = true;
|
||
return;
|
||
}
|
||
|
||
if(dt.hour < d.rangeEndHour || dt.hour >= d.closeHour)
|
||
return;
|
||
|
||
if(d.ordersPlaced || d.rangeSkipDay)
|
||
return;
|
||
|
||
if(UB_CountMagicPendings(d) > 0)
|
||
{
|
||
d.ordersPlaced = true;
|
||
return;
|
||
}
|
||
|
||
if(UB_CountMagicPositions(d) > 0)
|
||
{
|
||
if(!d.closeUnprofitableOnNewSignal
|
||
|| !United_PrepareEntrySlot(d.trade, d.symbol, (ulong)d.magic, d.closeUnprofitableOnNewSignal))
|
||
{
|
||
d.ordersPlaced = true;
|
||
return;
|
||
}
|
||
}
|
||
|
||
if(!d.rangeBuilt)
|
||
{
|
||
if(!UB_BuildRange(d, now, d.rangeHigh, d.rangeLow))
|
||
{
|
||
UB_Dbg(d, "range not ready or too narrow — retry until " + IntegerToString(d.closeHour) + ":00");
|
||
return;
|
||
}
|
||
d.rangeBuilt = true;
|
||
}
|
||
|
||
if(UB_PlaceBreakoutOrders(d, scaledFixedLots, riskScale))
|
||
{
|
||
d.ordersPlaced = true;
|
||
return;
|
||
}
|
||
|
||
if(!d.allowLong && !d.allowShort)
|
||
d.rangeSkipDay = true;
|
||
}
|
||
|
||
#endif
|