Files
zhutoutoutousanandCursor 605faf5310 Prepare source-only public release for develop.
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>
2026-07-02 15:03:43 +02:00

561 lines
15 KiB
Plaintext
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//+------------------------------------------------------------------+
//| 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