605faf5310
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>
329 lines
11 KiB
Plaintext
329 lines
11 KiB
Plaintext
//+------------------------------------------------------------------+
|
|
//| DarvasBoxStrategy.mqh |
|
|
//+------------------------------------------------------------------+
|
|
// MQL5: no #if — use #ifdef only (no defined() / || in one #if)
|
|
#ifdef UNITED_V2_DYNAMIC_LOTS
|
|
extern double g_DB_LotSize;
|
|
#define DARVAS_TRADE_LOT (g_DB_LotSize)
|
|
#else
|
|
#ifdef CLUSTER0_ORCHESTRATOR
|
|
extern double g_DB_LotSize;
|
|
#define DARVAS_TRADE_LOT (g_DB_LotSize)
|
|
#else
|
|
#define DARVAS_TRADE_LOT 0.01
|
|
#endif
|
|
#endif
|
|
|
|
bool InitDarvasBox(string symbol)
|
|
{
|
|
dbData.symbol = symbol;
|
|
dbData.boxHigh = 0;
|
|
dbData.boxLow = 0;
|
|
dbData.boxFormed = false;
|
|
dbData.lastBoxTime = 0;
|
|
dbData.boxName = "DarvasBox_" + IntegerToString(DB_MagicNumber) + "_";
|
|
|
|
// Check if symbol exists
|
|
if(!SymbolSelect(symbol, true))
|
|
{
|
|
Print("DarvasBox: Symbol '", symbol, "' not available in Market Watch. Please add it to Market Watch or check symbol name.");
|
|
return false;
|
|
}
|
|
|
|
Sleep(100); // Wait for symbol to be ready
|
|
|
|
dbData.point = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
|
dbData.minStopLevel = SymbolInfoInteger(symbol, SYMBOL_TRADE_STOPS_LEVEL) * dbData.point;
|
|
|
|
dbData.maHandle = iMA(symbol, DB_TrendTimeframe, DB_MA_Period, 0, DB_MA_Method, DB_MA_Price);
|
|
// Same TF as box highs/lows (H1 loop in CalculateDarvasBox). PERIOD_CURRENT breaks when United EA
|
|
// runs on a chart timeframe other than H1 (volume/breakout no longer match the box).
|
|
dbData.volumeHandle = iVolumes(symbol, PERIOD_H1, VOLUME_TICK);
|
|
|
|
if(dbData.maHandle == INVALID_HANDLE || dbData.volumeHandle == INVALID_HANDLE)
|
|
{
|
|
Print("DarvasBox: Error creating indicators for '", symbol, "'");
|
|
return false;
|
|
}
|
|
|
|
dbData.trade.SetDeviationInPoints(10);
|
|
dbData.trade.SetTypeFilling(ORDER_FILLING_IOC);
|
|
dbData.trade.SetAsyncMode(false);
|
|
dbData.trade.SetExpertMagicNumber(DB_MagicNumber);
|
|
|
|
ObjectsDeleteAll(0, dbData.boxName);
|
|
dbData.isInitialized = true;
|
|
Print("DarvasBox: Successfully initialized for symbol '", symbol, "'");
|
|
return true;
|
|
}
|
|
|
|
void DeinitDarvasBox()
|
|
{
|
|
if(dbData.maHandle != INVALID_HANDLE) IndicatorRelease(dbData.maHandle);
|
|
if(dbData.volumeHandle != INVALID_HANDLE) IndicatorRelease(dbData.volumeHandle);
|
|
ObjectsDeleteAll(0, dbData.boxName);
|
|
}
|
|
|
|
void DrawDarvasBox()
|
|
{
|
|
if(!dbData.boxFormed) return;
|
|
|
|
datetime time1 = iTime(dbData.symbol, PERIOD_H1, DB_BoxPeriod);
|
|
datetime time2 = iTime(dbData.symbol, PERIOD_H1, 0);
|
|
|
|
ObjectsDeleteAll(0, dbData.boxName);
|
|
|
|
ObjectCreate(0, dbData.boxName + "Top", OBJ_TREND, 0, time1, dbData.boxHigh, time2, dbData.boxHigh);
|
|
ObjectCreate(0, dbData.boxName + "Bottom", OBJ_TREND, 0, time1, dbData.boxLow, time2, dbData.boxLow);
|
|
|
|
ObjectSetInteger(0, dbData.boxName + "Top", OBJPROP_COLOR, DB_BoxColor);
|
|
ObjectSetInteger(0, dbData.boxName + "Bottom", OBJPROP_COLOR, DB_BoxColor);
|
|
ObjectSetInteger(0, dbData.boxName + "Top", OBJPROP_WIDTH, DB_BoxWidth);
|
|
ObjectSetInteger(0, dbData.boxName + "Bottom", OBJPROP_WIDTH, DB_BoxWidth);
|
|
ObjectSetInteger(0, dbData.boxName + "Top", OBJPROP_RAY_RIGHT, true);
|
|
ObjectSetInteger(0, dbData.boxName + "Bottom", OBJPROP_RAY_RIGHT, true);
|
|
}
|
|
|
|
void CalculateDarvasBox()
|
|
{
|
|
double high = 0;
|
|
double low = DBL_MAX;
|
|
|
|
// Find highest high and lowest low in the period - EXACTLY like original
|
|
for(int i = 0; i < DB_BoxPeriod; i++)
|
|
{
|
|
high = MathMax(high, iHigh(dbData.symbol, PERIOD_H1, i));
|
|
low = MathMin(low, iLow(dbData.symbol, PERIOD_H1, i));
|
|
}
|
|
|
|
double range = high - low;
|
|
double allowedRange = DB_BoxDeviation * dbData.point; // Use dbData.point instead of _Point
|
|
|
|
if(DB_EnableLogging)
|
|
{
|
|
Print("DarvasBox: Box Calculation - High: ", high, " Low: ", low, " Range: ", range, " Allowed Range: ", allowedRange);
|
|
}
|
|
|
|
// Check if box is formed - EXACTLY like original
|
|
if(range <= allowedRange)
|
|
{
|
|
dbData.boxHigh = high;
|
|
dbData.boxLow = low;
|
|
dbData.boxFormed = true;
|
|
dbData.lastBoxTime = iTime(dbData.symbol, PERIOD_CURRENT, 0);
|
|
|
|
// Draw the box
|
|
DrawDarvasBox();
|
|
|
|
if(DB_EnableLogging)
|
|
Print("DarvasBox: Box Formed - High: ", dbData.boxHigh, " Low: ", dbData.boxLow, " Time: ", dbData.lastBoxTime);
|
|
}
|
|
else
|
|
{
|
|
dbData.boxFormed = false;
|
|
// Delete box if it exists
|
|
ObjectsDeleteAll(0, dbData.boxName);
|
|
}
|
|
}
|
|
|
|
bool ValidateStopLevels(double price, double &sl, double &tp, ENUM_ORDER_TYPE orderType)
|
|
{
|
|
double minSlDistance = MathMax(dbData.minStopLevel, DB_StopLoss * dbData.point);
|
|
double minTpDistance = MathMax(dbData.minStopLevel, DB_TakeProfit * dbData.point);
|
|
|
|
if(orderType == ORDER_TYPE_BUY)
|
|
{
|
|
sl = price - minSlDistance;
|
|
tp = price + minTpDistance;
|
|
}
|
|
else
|
|
{
|
|
sl = price + minSlDistance;
|
|
tp = price - minTpDistance;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool IsTrendFavorable(ENUM_ORDER_TYPE orderType)
|
|
{
|
|
if(!DB_UseTrendFilter)
|
|
return true;
|
|
|
|
double ma[];
|
|
ArraySetAsSeries(ma, true);
|
|
|
|
if(CopyBuffer(dbData.maHandle, 0, 0, 2, ma) <= 0)
|
|
return false;
|
|
|
|
double currentPrice = SymbolInfoDouble(dbData.symbol, SYMBOL_ASK);
|
|
double trendStrength = MathAbs(currentPrice - ma[0]) / dbData.point;
|
|
|
|
if(orderType == ORDER_TYPE_BUY)
|
|
return (currentPrice > ma[0] && trendStrength > DB_TrendThreshold);
|
|
else
|
|
return (currentPrice < ma[0] && trendStrength > DB_TrendThreshold);
|
|
}
|
|
|
|
bool CheckVolumeConditions()
|
|
{
|
|
if(!DB_UseVolumeSpikeFilter)
|
|
return true;
|
|
|
|
double volumes[];
|
|
ArraySetAsSeries(volumes, true);
|
|
|
|
if(CopyBuffer(dbData.volumeHandle, 0, 0, DB_VolumeMA_Period + 1, volumes) <= 0)
|
|
return false;
|
|
|
|
double volumeMA = 0;
|
|
for(int i = 1; i <= DB_VolumeMA_Period; i++)
|
|
volumeMA += volumes[i];
|
|
volumeMA /= DB_VolumeMA_Period;
|
|
|
|
double currentVolume = volumes[0];
|
|
if(volumeMA <= 0.0)
|
|
return (currentVolume > 0.0);
|
|
|
|
double volumeRatio = currentVolume / volumeMA;
|
|
return (volumeRatio > DB_VolumeThresholdMultiplier);
|
|
}
|
|
|
|
bool PlaceOrder(ENUM_ORDER_TYPE orderType, double price, double sl, double tp)
|
|
{
|
|
if(!ValidateStopLevels(price, sl, tp, orderType))
|
|
{
|
|
if(DB_EnableLogging)
|
|
Print("DarvasBox: Order rejected - Stop levels validation failed");
|
|
return false;
|
|
}
|
|
|
|
if(!IsTrendFavorable(orderType))
|
|
{
|
|
if(DB_EnableLogging)
|
|
Print("DarvasBox: Order rejected - Trend not favorable for ", EnumToString(orderType));
|
|
return false;
|
|
}
|
|
|
|
if(!CheckVolumeConditions())
|
|
{
|
|
if(DB_EnableLogging)
|
|
Print("DarvasBox: Order rejected - Volume conditions not met");
|
|
return false;
|
|
}
|
|
|
|
bool result = false;
|
|
const double lot = United_NormalizeVolume(dbData.symbol, DARVAS_TRADE_LOT);
|
|
if(lot <= 0.0)
|
|
{
|
|
Print("DarvasBox: Order rejected - invalid lot after normalize (raw=", DARVAS_TRADE_LOT, ")");
|
|
return false;
|
|
}
|
|
|
|
// Use market price (0) instead of explicit price - this ensures market order execution
|
|
// In backtesting, explicit price might fail if price has moved
|
|
if(orderType == ORDER_TYPE_BUY)
|
|
result = dbData.trade.Buy(lot, dbData.symbol, 0, sl, tp, "Darvas Box Breakout");
|
|
else
|
|
result = dbData.trade.Sell(lot, dbData.symbol, 0, sl, tp, "Darvas Box Breakdown");
|
|
|
|
// Always log errors, success only if logging enabled
|
|
if(result)
|
|
{
|
|
if(DB_EnableLogging)
|
|
Print("DarvasBox: ", (orderType == ORDER_TYPE_BUY ? "Buy" : "Sell"), " Order Placed Successfully");
|
|
}
|
|
else
|
|
{
|
|
// Always log failures with detailed info
|
|
uint retcode_uint = dbData.trade.ResultRetcode();
|
|
int retcode = (int)retcode_uint;
|
|
string desc = dbData.trade.ResultRetcodeDescription();
|
|
ulong deal = dbData.trade.ResultDeal();
|
|
ulong order = dbData.trade.ResultOrder();
|
|
Print("DarvasBox: ", (orderType == ORDER_TYPE_BUY ? "Buy" : "Sell"),
|
|
" Order Failed - Retcode: ", retcode,
|
|
", Description: ", desc,
|
|
", Deal: ", deal,
|
|
", Order: ", order,
|
|
", Symbol: ", dbData.symbol,
|
|
", Requested Price: ", price,
|
|
", SL: ", sl,
|
|
", TP: ", tp);
|
|
}
|
|
|
|
return result;
|
|
}
|
|
|
|
void ProcessDarvasBox(string symbol)
|
|
{
|
|
// Skip if not initialized (symbol not available)
|
|
if(!dbData.isInitialized)
|
|
return;
|
|
|
|
dbData.symbol = symbol; // Update symbol in case it changed
|
|
|
|
// Calculate new box levels - EXACTLY like original (called every tick)
|
|
CalculateDarvasBox();
|
|
|
|
// Check for trading signals - EXACTLY like original (checked every tick)
|
|
if(dbData.boxFormed)
|
|
{
|
|
double currentPrice = SymbolInfoDouble(dbData.symbol, SYMBOL_ASK);
|
|
long currentVolume_long = iVolume(dbData.symbol, PERIOD_H1, 0);
|
|
double currentVolume = (double)currentVolume_long;
|
|
|
|
if(DB_EnableLogging)
|
|
{
|
|
Print("DarvasBox: Current Price: ", currentPrice, " Box High: ", dbData.boxHigh, " Box Low: ", dbData.boxLow);
|
|
Print("DarvasBox: Current Volume: ", currentVolume, " Volume Threshold: ", DB_VolumeThreshold);
|
|
}
|
|
|
|
// Check for breakout above box - EXACTLY like original
|
|
if(currentPrice > dbData.boxHigh && currentVolume > DB_VolumeThreshold)
|
|
{
|
|
if(DB_EnableLogging)
|
|
Print("DarvasBox: Breakout Signal Detected - Price above box high");
|
|
|
|
// Buy signal
|
|
if(United_PrepareEntrySlot(dbData.trade, dbData.symbol, (ulong)DB_MagicNumber, DB_CloseUnprofitableOnNewSignal))
|
|
{
|
|
double sl = currentPrice - DB_StopLoss * dbData.point;
|
|
double tp = currentPrice + DB_TakeProfit * dbData.point;
|
|
|
|
if(DB_EnableLogging)
|
|
Print("DarvasBox: Preparing Buy Order - Price: ", currentPrice, " SL: ", sl, " TP: ", tp);
|
|
|
|
PlaceOrder(ORDER_TYPE_BUY, currentPrice, sl, tp);
|
|
}
|
|
else if(DB_EnableLogging)
|
|
Print("DarvasBox: Skipping Buy Signal - Position already exists");
|
|
}
|
|
|
|
// Check for breakdown below box - EXACTLY like original
|
|
if(currentPrice < dbData.boxLow && currentVolume > DB_VolumeThreshold)
|
|
{
|
|
if(DB_EnableLogging)
|
|
Print("DarvasBox: Breakdown Signal Detected - Price below box low");
|
|
|
|
// Sell signal
|
|
if(United_PrepareEntrySlot(dbData.trade, dbData.symbol, (ulong)DB_MagicNumber, DB_CloseUnprofitableOnNewSignal))
|
|
{
|
|
double sl = currentPrice + DB_StopLoss * dbData.point;
|
|
double tp = currentPrice - DB_TakeProfit * dbData.point;
|
|
|
|
if(DB_EnableLogging)
|
|
Print("DarvasBox: Preparing Sell Order - Price: ", currentPrice, " SL: ", sl, " TP: ", tp);
|
|
|
|
PlaceOrder(ORDER_TYPE_SELL, currentPrice, sl, tp);
|
|
}
|
|
else if(DB_EnableLogging)
|
|
Print("DarvasBox: Skipping Sell Signal - Position already exists");
|
|
}
|
|
}
|
|
else if(DB_EnableLogging)
|
|
Print("DarvasBox: No Box Formed - Waiting for consolidation");
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|