Files
Bell-PriceActionWithEma-EA/Experts/RSIForceStateEA/Trade.mqh
T
2026-05-09 23:16:50 +07:00

329 lines
12 KiB
Plaintext

#ifndef RSI_FORCE_STATE_EA__TRADE_MQH
#define RSI_FORCE_STATE_EA__TRADE_MQH
#include <Trade/Trade.mqh>
// CTrade wrapper used for all order/position operations.
// Defaults (magic, deviation) are set once in InitTradeOps() at OnInit.
CTrade g_TradeOps;
// ------------------------------------------------------------
// Lifecycle
// ------------------------------------------------------------
void InitTradeOps()
{
g_TradeOps.SetExpertMagicNumber(InpMagicNumber);
g_TradeOps.SetDeviationInPoints(10);
g_TradeOps.SetTypeFillingBySymbol(_Symbol);
}
// ------------------------------------------------------------
// Symbol primitives
// ------------------------------------------------------------
double NormalizePriceToTick(const double price)
{
return NormalizeDouble(price, (int)SymbolInfoInteger(_Symbol, SYMBOL_DIGITS));
}
double NormalizeVolumeToBroker(const double rawVolume)
{
const double volMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
const double volMax = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MAX);
const double volStep = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_STEP);
if (volStep <= 0.0) return 0.0;
double v = MathFloor(rawVolume / volStep) * volStep;
v = MathMax(v, volMin);
v = MathMin(v, volMax);
return NormalizeDouble(v, 2);
}
double GetStopsLevelPrice()
{
// Broker minimum distance for SL/TP/limit price from market.
const long stopsLevelPoints = SymbolInfoInteger(_Symbol, SYMBOL_TRADE_STOPS_LEVEL);
return (double)stopsLevelPoints * _Point;
}
// Position sizing from fixed % of balance and price distance entry->SL.
double CalcLotsForRisk(const double entryPrice, const double stopLossPrice)
{
const double balance = AccountInfoDouble(ACCOUNT_BALANCE);
const double riskMoney = balance * (InpRiskPercent / 100.0);
const double tickValue = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_VALUE);
const double tickSize = SymbolInfoDouble(_Symbol, SYMBOL_TRADE_TICK_SIZE);
const double slDistance = MathAbs(entryPrice - stopLossPrice);
if (slDistance <= 0.0 || tickValue <= 0.0 || tickSize <= 0.0) return 0.0;
const double moneyPerLot = (slDistance / tickSize) * tickValue;
if (moneyPerLot <= 0.0) return 0.0;
return NormalizeVolumeToBroker(riskMoney / moneyPerLot);
}
// ------------------------------------------------------------
// Swing helpers (used both for entry anchor and SL anchor)
// ------------------------------------------------------------
// Returns the raw extreme price of the nearest swing (no buffer added).
// Used to compute the entry midpoint = (close + swingExtreme) / 2.
double FindNearestSwingForEntry(const int direction, const int signalShift)
{
const int startShift = signalShift + 1;
const int lookback = MathMax(5, InpSwingLookbackBars);
if (direction > 0)
{
const int swingIdx = iLowest(_Symbol, _Period, MODE_LOW, lookback, startShift);
if (swingIdx <= 0) return 0.0;
return g_Bars[swingIdx].low;
}
const int swingIdx = iHighest(_Symbol, _Period, MODE_HIGH, lookback, startShift);
if (swingIdx <= 0) return 0.0;
return g_Bars[swingIdx].high;
}
// Returns the SL price anchored to the nearest swing extreme + safety buffer.
double FindNearestSwingForSL(const int direction, const int signalShift)
{
const int startShift = signalShift + 1;
const int lookback = MathMax(5, InpSwingLookbackBars);
if (direction > 0)
{
const int swingIdx = iLowest(_Symbol, _Period, MODE_LOW, lookback, startShift);
if (swingIdx <= 0) return 0.0;
return g_Bars[swingIdx].low - (InpSL_SwingBufferPoints * _Point);
}
const int swingIdx = iHighest(_Symbol, _Period, MODE_HIGH, lookback, startShift);
if (swingIdx <= 0) return 0.0;
return g_Bars[swingIdx].high + (InpSL_SwingBufferPoints * _Point);
}
// Compose final SL price honoring InpStopLossMode.
double ComputeStopLossPrice(const int direction, const int signalShift,
const double entryPrice, const double atrValue)
{
const double swingSL = FindNearestSwingForSL(direction, signalShift);
const double atrSL = (direction > 0)
? entryPrice - (atrValue * InpSL_ATRMult)
: entryPrice + (atrValue * InpSL_ATRMult);
if (InpStopLossMode == SL_SWING && swingSL > 0.0) return swingSL;
if (InpStopLossMode == SL_ATR) return atrSL;
if (swingSL <= 0.0) return atrSL;
// SL_HYBRID: pick the wider (safer) stop on the correct side.
return (direction > 0) ? MathMin(swingSL, atrSL) : MathMax(swingSL, atrSL);
}
// ------------------------------------------------------------
// Order placement / cancellation
// ------------------------------------------------------------
// Validates that the limit price + SL/TP respect the broker's stops level.
// For BUY LIMIT, entry must be below current Ask by at least stopsLevel.
// For SELL LIMIT, entry must be above current Bid by at least stopsLevel.
bool ValidateLimitPrices(const SignalSnapshot &plan)
{
const double stopsLevel = GetStopsLevelPrice();
const double askNow = SymbolInfoDouble(_Symbol, SYMBOL_ASK);
const double bidNow = SymbolInfoDouble(_Symbol, SYMBOL_BID);
if (plan.direction > 0)
{
if (plan.entryPrice > askNow - stopsLevel)
{
if (InpDebugLog)
PrintFormat("[ORDER] reject BUY LIMIT entry=%.5f too close to ask=%.5f (stops=%.5f)",
plan.entryPrice, askNow, stopsLevel);
return false;
}
if (plan.entryPrice - plan.stopLossPrice < stopsLevel
|| plan.takeProfitPrice - plan.entryPrice < stopsLevel)
{
if (InpDebugLog)
PrintFormat("[ORDER] reject BUY LIMIT SL/TP too close to entry (stops=%.5f)", stopsLevel);
return false;
}
}
else
{
if (plan.entryPrice < bidNow + stopsLevel)
{
if (InpDebugLog)
PrintFormat("[ORDER] reject SELL LIMIT entry=%.5f too close to bid=%.5f (stops=%.5f)",
plan.entryPrice, bidNow, stopsLevel);
return false;
}
if (plan.stopLossPrice - plan.entryPrice < stopsLevel
|| plan.entryPrice - plan.takeProfitPrice < stopsLevel)
{
if (InpDebugLog)
PrintFormat("[ORDER] reject SELL LIMIT SL/TP too close to entry (stops=%.5f)", stopsLevel);
return false;
}
}
return true;
}
bool PlaceLimitOrderFromPlan(const SignalSnapshot &plan, PendingContext &pendingCtx)
{
if (!ValidateLimitPrices(plan)) return false;
const double lots = CalcLotsForRisk(plan.entryPrice, plan.stopLossPrice);
if (lots <= 0.0)
{
if (InpDebugLog)
PrintFormat("[ORDER] reject: lots=%.4f (risk too small or symbol info missing)", lots);
return false;
}
const string comment = (plan.direction > 0) ? "RSIForce_BUY_LIMIT" : "RSIForce_SELL_LIMIT";
bool placed = false;
if (plan.direction > 0)
placed = g_TradeOps.BuyLimit(lots,
NormalizePriceToTick(plan.entryPrice), _Symbol,
NormalizePriceToTick(plan.stopLossPrice),
NormalizePriceToTick(plan.takeProfitPrice),
ORDER_TIME_GTC, 0, comment);
else
placed = g_TradeOps.SellLimit(lots,
NormalizePriceToTick(plan.entryPrice), _Symbol,
NormalizePriceToTick(plan.stopLossPrice),
NormalizePriceToTick(plan.takeProfitPrice),
ORDER_TIME_GTC, 0, comment);
if (!placed)
{
if (InpDebugLog)
PrintFormat("[ORDER] place fail: ret=%u msg=%s",
g_TradeOps.ResultRetcode(), g_TradeOps.ResultRetcodeDescription());
return false;
}
pendingCtx.orderTicket = g_TradeOps.ResultOrder();
pendingCtx.barsSincePlaced = 0;
pendingCtx.plan = plan;
if (InpDebugLog)
PrintFormat("[ORDER] placed %s lots=%.2f entry=%.5f SL=%.5f TP=%.5f ticket=%I64u",
comment, lots, plan.entryPrice, plan.stopLossPrice, plan.takeProfitPrice,
pendingCtx.orderTicket);
return (pendingCtx.orderTicket > 0);
}
bool CancelPendingOrder(PendingContext &pendingCtx)
{
if (pendingCtx.orderTicket == 0) return true;
if (!g_TradeOps.OrderDelete(pendingCtx.orderTicket))
{
if (InpDebugLog)
PrintFormat("[ORDER] delete fail: ret=%u", g_TradeOps.ResultRetcode());
return false;
}
pendingCtx.orderTicket = 0;
pendingCtx.barsSincePlaced = 0;
return true;
}
// ------------------------------------------------------------
// Broker queries (filtered by symbol + magic)
// ------------------------------------------------------------
bool HasOurOpenPosition(ulong &outTicket)
{
for (int i = PositionsTotal() - 1; i >= 0; i--)
{
const ulong posTicket = PositionGetTicket(i);
if (posTicket <= 0) continue;
if (PositionGetString(POSITION_SYMBOL) != _Symbol) continue;
if (PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) continue;
outTicket = posTicket;
return true;
}
return false;
}
bool HasOurPendingOrder(const ulong ticket)
{
if (ticket == 0) return false;
for (int i = OrdersTotal() - 1; i >= 0; i--)
{
if (OrderGetTicket(i) == ticket) return true;
}
return false;
}
// ------------------------------------------------------------
// Trade management: partial close at +R + move SL to BE
// ------------------------------------------------------------
bool ManagePartialAndBreakEven(TradeContext &openTrade)
{
if (!openTrade.isActive || openTrade.partialClosedDone) return true;
if (!PositionSelectByTicket(openTrade.positionTicket)) return false;
if (PositionGetInteger(POSITION_MAGIC) != InpMagicNumber) return false;
const long posType = PositionGetInteger(POSITION_TYPE);
const double openPrice = PositionGetDouble(POSITION_PRICE_OPEN);
const double slPrice = PositionGetDouble(POSITION_SL);
const double tpPrice = PositionGetDouble(POSITION_TP);
const double posVolume = PositionGetDouble(POSITION_VOLUME);
// Use the ACTUAL fill->SL distance as R, not the planned one
// (broker fill price may differ from planned entry price).
const double initRisk = MathAbs(openPrice - slPrice);
if (initRisk <= 0.0) return false;
const double priceNow = (posType == POSITION_TYPE_BUY)
? SymbolInfoDouble(_Symbol, SYMBOL_BID)
: SymbolInfoDouble(_Symbol, SYMBOL_ASK);
const double profitDist = (posType == POSITION_TYPE_BUY)
? (priceNow - openPrice)
: (openPrice - priceNow);
// Not yet at the partial trigger.
if (profitDist < (InpPartialCloseAtR * initRisk)) return true;
// Try to split off `InpPartialClosePercent`% but only if both sides
// remain >= volMin after split (otherwise just move BE).
const double volMin = SymbolInfoDouble(_Symbol, SYMBOL_VOLUME_MIN);
const double rawClose = posVolume * (InpPartialClosePercent / 100.0);
const double closeVol = NormalizeVolumeToBroker(rawClose);
const double remainVol = NormalizeVolumeToBroker(posVolume - closeVol);
const bool canSplit = (closeVol >= volMin)
&& (remainVol >= volMin)
&& (closeVol < posVolume);
if (canSplit)
{
if (!g_TradeOps.PositionClosePartial(openTrade.positionTicket, closeVol))
{
if (InpDebugLog)
PrintFormat("[TRADE] partial-close fail: ret=%u", g_TradeOps.ResultRetcode());
return false;
}
}
// Always move SL to the actual fill price (BE).
if (!g_TradeOps.PositionModify(openTrade.positionTicket,
NormalizePriceToTick(openPrice), tpPrice))
{
if (InpDebugLog)
PrintFormat("[TRADE] BE-move fail: ret=%u", g_TradeOps.ResultRetcode());
return false;
}
openTrade.partialClosedDone = true;
if (InpDebugLog)
PrintFormat("[TRADE] Partial=%s vol=%.2f -> SL moved to BE @ %.5f",
canSplit ? "yes" : "skipped(min vol)", closeVol, openPrice);
return true;
}
#endif