Entry, SL, TP với Orderblock

This commit is contained in:
Bell
2025-12-12 22:06:06 +07:00
parent f3b87bcf90
commit 44ca020ec5
2 changed files with 328 additions and 147 deletions
+25 -1
View File
@@ -15,4 +15,28 @@ Bước 3: Vào lệnh pullback khi có xác nhận tín hiệu tại khung m5 k
- Tính toán lot size để đặt SL:
+ Nếu buy limit: Tính toán lot size vào lệnh sao cho khoảng cách từ entry tới đáy thấp nhất của sóng hồi m5 vừa bằng 1% equity
+ Nếu sell limit: Tính toán lot size vào lệnh sao cho khoảng cách từ entry tới đỉnh cao nhất của sóng hồi m5 vừa bằng 1% equity
- R:R = 1:3
- R:R = 1:3
======
v2 issues:
1. FindInternalFVG -> đang trả về found (boolean) -> nên trả về list FVGs(struct[])
2. Cách tính sl lại: đặt dưới OB
======
Result test 12/12/25
1. Tháng 02/2025:
- 1:3 -> 8%
- 1:4 -> 10%
- 1:5 -> 13%
- 1:6 -> 23%
- 1:7 -> 17%
- 1:8 -> 3.5%
2. Cả năm 2025:
- 1:5 -> 56.7%
- 1:6 -> 59.4%
+303 -146
View File
@@ -10,8 +10,9 @@
// --- Only Entry logs (user requested) ---
bool PrintEntryLog = true; // nếu true -> in log chỉ liên quan tới entry
input double RishPercent = 1.0; // % vốn rủi ro cho mỗi lệnh
// --- Cấu hình Risk:Reward ---
input double RiskRewardRatio = 3.0; // tỉ lệ R:R mặc định (TP = entry ± RiskRewardRatio * |entry - SL|)
input double RiskRewardRatio = 5.0; // tỉ lệ R:R mặc định (TP = entry ± RiskRewardRatio * |entry - SL|)
// Struct pending entry (single slot)
struct PendingEntry
@@ -25,6 +26,8 @@ struct PendingEntry
int source_slot; // slot nơi phát hiện MSS (HTF/MTF/LTF)
double sl_price; // giá SL (dựa trên swing gần nhất)
double tp_price; // giá TP (dựa trên swing gần nhất)
double lotSize; // kích thước lot tính toán
ulong orderTicket; // ticket lệnh đã mở (0 = chưa mở)
};
// global pending entry variable
@@ -806,7 +809,7 @@ int FindInternalFVG(string symbol, ENUM_TIMEFRAMES timeframe, int lookback,
for(int i = 1; i <= maxScan; i++)
{
int idxA = i + 2;
int idxB = i + 1; // thực ra không cần B trong phép tính nhưng vẫn để đúng mô hình
int idxB = i + 1;
int idxC = i;
// kiểm tra A không vượt tổng số bar
@@ -1741,7 +1744,7 @@ void DetectMSSOnTimeframe(string sym, ENUM_TIMEFRAMES tf, int slot, bool enabled
watchingFVGIndex = -1;
watchingFVGDir = 0;
// SetUpPendingEntryForMSS(mss, slot);
SetUpPendingEntryForMSS(mss, slot);
}
}
@@ -1937,15 +1940,194 @@ void DrawPendingEntryVisuals(string symbol)
}
}
// Set up pending entry when MSS confirmed and direction matches watched FVG
// CalculateLotSizeForRisk:
// - symbol: symbol
// - entryPrice, slPrice: điểm entry và sl (giá thực tế)
// - riskPercent: ví dụ 1.0 cho 1% equity
// Trả về lotsize phù hợp (đã được clamp theo min/max/step)
double CalculateLotSizeForRisk(string symbol, double entryPrice, double slPrice, double riskPercent)
{
double pipValue = GetPipSize(symbol); // 1 pip in price units
double distance = MathAbs(entryPrice - slPrice); // in price units
if(distance <= 0.0) return 0.0;
// Use account equity as base
double equity = AccountInfoDouble(ACCOUNT_EQUITY);
if(equity <= 0.0) equity = AccountInfoDouble(ACCOUNT_BALANCE);
if(equity <= 0.0) return 0.0;
double riskAmount = equity * (riskPercent / 100.0);
// Obtain tick size/value info from symbol
double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
// Defensive fallback: if tick_size/tick_value unavailable, try point-based approximation
double valuePerPointPerLot = 0.0;
if(tick_size > 0.0 && tick_value > 0.0)
{
valuePerPointPerLot = tick_value / tick_size;
}
else
{
// approximate: use point and contract size
double point = SymbolInfoDouble(symbol, SYMBOL_POINT);
// assume valuePerPointPerLot ~ 10 (very rough) -> but better to abort
// safer: abort by returning 0 if we don't have reliable tick info
return 0.0;
}
// value risk per lot = distance (price units) * valuePerPointPerLot
double valueRiskPerLot = distance * valuePerPointPerLot;
if(valueRiskPerLot <= 0.0) return 0.0;
double lots = riskAmount / valueRiskPerLot;
// clamp to symbol lot limits and step
double minLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
double maxLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
double stepLot = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
if(minLot <= 0.0 || stepLot <= 0.0)
{
// fallback defaults if broker info missing
minLot = 0.01;
stepLot = 0.01;
maxLot = 100.0;
}
// Normalize lots to nearest step
double stepInv = MathRound(lots / stepLot);
double lotsNorm = stepInv * stepLot;
// ensure within min/max
if(lotsNorm < minLot) lotsNorm = minLot;
if(lotsNorm > maxLot) lotsNorm = maxLot;
// final safety: round to allowed decimals (step determines decimals)
int stepDigits = 0;
{
double tmp = stepLot;
while(tmp < 1.0 && stepDigits < 8)
{
tmp *= 10.0;
stepDigits++;
}
}
lotsNorm = NormalizeDouble(lotsNorm, stepDigits);
return lotsNorm;
}
// PlacePendingOrderFromPendingEntry:
// - Gọi DrawPendingEntryVisuals để vẽ entry/sl/tp
// - Gửi pending order (BUY_LIMIT nếu pendingEntry.direction==1, SELL_LIMIT nếu -1)
// - Lưu ticket vào pendingEntry.order_ticket nếu thành công
// - Trả về true nếu order đặt thành công, false nếu lỗi
bool PlacePendingOrderFromPendingEntry()
{
string sym = Symbol();
if(!pendingEntry.active)
{
if(PrintEntryLog) Print("PlacePendingOrderFromPendingEntry: abort - pendingEntry.active == false");
return false;
}
if(pendingEntry.price <= 0.0 || pendingEntry.lotSize <= 0.0)
{
if(PrintEntryLog) PrintFormat("PlacePendingOrderFromPendingEntry: abort - invalid price/lotsize (price=%.10f lots=%.4f)", pendingEntry.price, pendingEntry.lotSize);
return false;
}
// 1) Vẽ visuals trước (đảm bảo compositeName cập nhật)
DrawPendingEntryVisuals(sym);
// 2) Chuẩn bị trade request
MqlTradeRequest request;
MqlTradeResult result;
ZeroMemory(request);
ZeroMemory(result);
request.action = TRADE_ACTION_PENDING; // đặt lệnh pending
request.symbol = sym;
request.volume = pendingEntry.lotSize;
request.deviation= 10; // acceptable slippage in points (bạn chỉnh nếu muốn)
request.magic = 123456; // chỉnh magic number nếu bạn dùng khác
request.comment = "PEND_BY_MSS_OB";
// normalize prices
int digs = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS);
double price = NormalizeDouble(pendingEntry.price, digs);
double sl = (pendingEntry.sl_price != 0.0) ? NormalizeDouble(pendingEntry.sl_price, digs) : 0.0;
double tp = (pendingEntry.tp_price != 0.0) ? NormalizeDouble(pendingEntry.tp_price, digs) : 0.0;
if(pendingEntry.direction == 1)
{
request.type = ORDER_TYPE_BUY_LIMIT;
request.price = price;
}
else
{
request.type = ORDER_TYPE_SELL_LIMIT;
request.price = price;
}
// set stoploss/takeprofit as absolute prices
request.sl = sl;
request.tp = tp;
// optional: set expiration (0 = good till canceled)
request.expiration = 0;
// 3) Send request
if(!OrderSend(request, result))
{
// OrderSend failed to execute (interface error)
if(PrintEntryLog) PrintFormat("PlacePendingOrderFromPendingEntry: OrderSend() returned false. result.retcode=%d retcode_external=%d", result.retcode, result.retcode_external);
return false;
}
// 4) Check result.retcode for success codes (10009 etc.)
// Success for pending order is typically TRADE_RETCODE_DONE (10008) or TRADE_RETCODE_DONE_REMAINDER etc.
if(result.retcode == TRADE_RETCODE_DONE || result.retcode == TRADE_RETCODE_PLACED || result.retcode == 10006 || result.retcode == 10008 || result.retcode == 10009)
{
// store ticket
pendingEntry.orderTicket = result.order;
if(PrintEntryLog) PrintFormat("PlacePendingOrderFromPendingEntry: SUCCESS ticket=%I64u (retcode=%d) entry=%.10f sl=%.10f tp=%.10f lots=%.4f",
pendingEntry.orderTicket, result.retcode, price, sl, tp, pendingEntry.lotSize);
return true;
}
else
{
// failure; log reason
if(PrintEntryLog)
{
PrintFormat("PlacePendingOrderFromPendingEntry: FAILED retcode=%d retval=%d result_comment=%s",
result.retcode, result.retcode, result.comment);
}
return false;
}
}
// SetUpPendingEntryForMSS: new implementation using OrderBlock (last opposite-color candle before break)
// - mss: MSSInfo found
// - slot: slot where MSS was detected
// - slot: slot where MSS detected
// Logic:
// 1) Find OrderBlock (OB) = first opposite-color candle when scanning backward from mss.break_time (exclusive).
// - bullish MSS (mss.direction == 1): find first bearish candle (close < open) when scanning backward from break_time
// - bearish MSS (mss.direction == -1): find first bullish candle (close > open) when scanning backward from break_time
// 2) Entry = OB edge (high for bearish candle (bullish OB), low for bullish candle (bearish OB))
// 3) SL = Entry +/- 300 pips (bullish: SL = entry - 300 pips; bearish: SL = entry + 300 pips)
// 4) TP computed using RiskRewardRatio as before (TP = entry ± R:R*|entry-sl|)
// 5) Calculate lotsize such that risk = 1% equity (use CalculateLotSizeForRisk)
// 6) Store into pendingEntry and draw visuals
void SetUpPendingEntryForMSS(const MSSInfo &mss, int slot)
{
string sym = Symbol();
if(PrintEntryLog)
PrintFormat("==> SetUpPendingEntryForMSS START: mss.found=%d dir=%d sweep_time=%d sweep_price=%.10f break_time=%d break_price=%.10f",
PrintFormat("==> SetUpPendingEntryForMSS (NEW) START: mss.found=%d dir=%d sweep_time=%d sweep_price=%.10f break_time=%d break_price=%.10f",
mss.found ? 1 : 0, mss.direction, (int)mss.sweep_time, mss.sweep_price, (int)mss.break_time, mss.break_price);
if(!mss.found)
@@ -1953,178 +2135,153 @@ void SetUpPendingEntryForMSS(const MSSInfo &mss, int slot)
if(PrintEntryLog) Print("-> abort: mss.found == false");
return;
}
if(mss.sweep_time == 0 || mss.break_time == 0 || mss.sweep_time >= mss.break_time)
if(mss.break_time == 0)
{
if(PrintEntryLog) PrintFormat("-> abort: invalid times (sweep_time=%d break_time=%d)", (int)mss.sweep_time, (int)mss.break_time);
if(PrintEntryLog) Print("-> abort: invalid break_time");
return;
}
double lfgtop[]; double lfgbottom[];
datetime lfgA[]; datetime lfgC[];
int lfgtype[];
// 1) Find OrderBlock (scan backward on LowTF from break_time - 1 bar)
int idxStart = iBarShift(sym, LowTF, mss.break_time, false);
if(idxStart == -1) idxStart = 0;
int cnt = FindInternalFVG(sym, LowTF, FVGLookback, lfgtop, lfgbottom, lfgA, lfgC, lfgtype);
if(PrintEntryLog) PrintFormat("-> FindInternalFVG returned cnt=%d (FVGLookback=%d)", cnt, FVGLookback);
int foundIdx = -1;
double obEdgePrice = 0.0; // entry price (edge)
int obDirection = 0; // 1 = bullish candle (close>open), -1 = bearish candle (close<open)
if(cnt <= 0)
// Start scanning previous bars strictly before break_time -> start at idxStart (bar whose time == break_time) then go idxStart+1 ???
// iBarShift returns index (0 = current), bar with time==break_time likely index > 0. We want bars older than break_time so begin idx = idxStart (if that bar equals break_time) then idx = idxStart
// Safer: we want bars with time < break_time, so shift to idx = idxStart (if that index's time == break_time then idx++), but iBarShift(...,false) returns exact index.
// We'll start scanning from idx = idxStart (which should point to the bar with time==break_time) and step forward (older) idx+1, idx+2...
int scanIdx = idxStart;
// move one bar older to ensure strictly before break_time
scanIdx = scanIdx + 1;
int maxScan = 50; // cap scanning to avoid infinite loops (you can adjust)
int scanned = 0;
for(int idx = scanIdx; idx < iBars(sym, LowTF) && scanned < maxScan; idx++, scanned++)
{
if(PrintEntryLog) Print("-> abort: no internal LTF FVG found");
double o = iOpen(sym, LowTF, idx);
double c = iClose(sym, LowTF, idx);
if(o == 0.0 || c == 0.0) continue;
if(mss.direction == 1)
{
// bullish MSS: find first bearish candle going backward => candle where close < open (bearish)
if(c < o)
{
foundIdx = idx;
obDirection = -1;
obEdgePrice = iHigh(sym, LowTF, idx); // use high of that bearish candle as OB edge
break;
}
}
else if(mss.direction == -1)
{
// bearish MSS: find first bullish candle (close > open)
if(c > o)
{
foundIdx = idx;
obDirection = 1;
obEdgePrice = iLow(sym, LowTF, idx); // use low of that bullish candle as OB edge
break;
}
}
}
if(foundIdx == -1)
{
if(PrintEntryLog) Print("-> abort: no suitable OrderBlock (opposite-color candle) found before break_time on LowTF");
return;
}
// Log all FVGs found for debug
double point = SymbolInfoDouble(sym, SYMBOL_POINT);
double tol = (point > 0.0) ? point * 0.5 : 0.0;
for(int i=0; i<cnt; i++)
{
string timestrA = TimeToString(lfgA[i], TIME_DATE|TIME_MINUTES);
string timestrC = TimeToString(lfgC[i], TIME_DATE|TIME_MINUTES);
if(PrintEntryLog)
PrintFormat(" FVG[%d]: type=%d top=%.10f bottom=%.10f timeA=%s timeC=%s", i, lfgtype[i], lfgtop[i], lfgbottom[i], timestrA, timestrC);
}
double pmin = MathMin(mss.sweep_price, mss.break_price);
double pmax = MathMax(mss.sweep_price, mss.break_price);
if(PrintEntryLog) PrintFormat("-> Price window between sweep & break: pmin=%.10f pmax=%.10f tol=%.10g", pmin, pmax, tol);
int chosenIdx = -1;
for(int i=0; i<cnt; i++)
{
datetime tc = lfgC[i];
// ensure candidate FVG C bar is strictly between sweep_time and break_time
if(tc <= mss.sweep_time)
{
if(PrintEntryLog) PrintFormat(" skip FVG[%d] because timeC(%s) <= sweep_time(%s)", i, TimeToString(tc, TIME_DATE|TIME_MINUTES), TimeToString(mss.sweep_time, TIME_DATE|TIME_MINUTES));
continue;
}
if(tc >= mss.break_time)
{
if(PrintEntryLog) PrintFormat(" skip FVG[%d] because timeC(%s) >= break_time(%s)", i, TimeToString(tc, TIME_DATE|TIME_MINUTES), TimeToString(mss.break_time, TIME_DATE|TIME_MINUTES));
continue;
}
double top = lfgtop[i];
double bottom = lfgbottom[i];
// check overlap: bottom..top overlap with sweep..break price window
if(bottom + tol >= pmin && top - tol <= pmax)
{
chosenIdx = i;
if(PrintEntryLog) PrintFormat("-> chosen FVG index=%d (type=%d top=%.10f bottom=%.10f)", chosenIdx, lfgtype[i], top, bottom);
break;
}
else
{
if(PrintEntryLog) PrintFormat(" FVG[%d] does not overlap window: bottom+tol=%.10f top-tol=%.10f (need bottom+tol >= pmin && top-tol <= pmax)", i, bottom+tol, top-tol);
}
}
if(chosenIdx == -1)
{
if(PrintEntryLog) Print("-> abort: no matching LTF internal FVG found between sweep and break (chosenIdx == -1)");
return;
}
// Determine entry price from chosen FVG
double entPrice = 0.0;
int fvgDir = lfgtype[chosenIdx];
if(fvgDir == 1)
entPrice = lfgbottom[chosenIdx];
else
entPrice = lfgtop[chosenIdx];
// Normalize entry to symbol digits
// Normalize entry price to symbol digits
int digs = (int)SymbolInfoInteger(sym, SYMBOL_DIGITS);
double entPriceNorm = NormalizeDouble(entPrice, digs);
double entryPrice = NormalizeDouble(obEdgePrice, digs);
if(PrintEntryLog)
PrintFormat("-> Found OB at index=%d time=%s dir=%d edge=%.10f (normalized=%.10f)",
foundIdx, TimeToString(iTime(sym, LowTF, foundIdx), TIME_DATE|TIME_MINUTES), obDirection, obEdgePrice, entryPrice);
// 2) Set SL = entry +/- 300 pips
double pip = GetPipSize(sym);
double sl;
double slDistancePoints = 300.0 * pip;
if(mss.direction == 1)
{
PrintFormat("-> entry raw=%.10f normalized=%.10f digits=%d fvgDir=%d", entPrice, entPriceNorm, digs, fvgDir);
// bullish MSS -> we will BUY at entry=OB(high) -> SL below OB
sl = entryPrice - slDistancePoints;
}
else
{
// bearish MSS -> SELL at entry=OB(low) -> SL above OB
sl = entryPrice + slDistancePoints;
}
sl = NormalizeDouble(sl, digs);
// 3) Compute TP using RiskRewardRatio (existing input)
double tp = 0.0;
double diff = MathAbs(entryPrice - sl);
if(diff > 0.0)
{
if(mss.direction == 1)
tp = entryPrice + RiskRewardRatio * diff;
else
tp = entryPrice - RiskRewardRatio * diff;
tp = NormalizeDouble(tp, digs);
}
else
{
if(PrintEntryLog) Print("-> abort: computed diff==0 between entry and SL");
return;
}
// Clear previous pending
// 4) Calculate lotsize based on 1% equity risk (hard-coded 1% per your request)
double lots = CalculateLotSizeForRisk(sym, entryPrice, sl, 1.0); // 1% equity
if(lots <= 0.0)
{
if(PrintEntryLog) Print("-> abort: CalculateLotSizeForRisk returned 0.0 (cannot determine lotsize)");
return;
}
// 5) Clear previous pending and populate new pendingEntry
ClearPendingEntry();
// Fill pendingEntry fields
pendingEntry.active = true;
pendingEntry.direction = (mss.direction == 1) ? 1 : -1;
pendingEntry.price = entPriceNorm;
pendingEntry.fvgIndex = chosenIdx;
pendingEntry.price = entryPrice;
pendingEntry.fvgIndex = -1; // not using FVG for this method
pendingEntry.created_time = TimeCurrent();
pendingEntry.compositeName = "";
pendingEntry.source_slot = slot;
pendingEntry.sl_price = sl;
pendingEntry.tp_price = tp;
pendingEntry.lotSize = lots;
pendingEntry.orderTicket = 0;
// Use MSS sweep price as SL (as before)
pendingEntry.sl_price = mss.sweep_price;
// attempt to place pending order immediately
if(PrintEntryLog) Print("Attempting to place pending order from pendingEntry...");
bool ok = PlacePendingOrderFromPendingEntry();
if(!ok)
Print("Failed to place pending order (check broker settings, tick/tickvalue availability, volume limits).");
// compute TP using RiskRewardRatio
double entry = pendingEntry.price;
double sl = pendingEntry.sl_price;
pendingEntry.tp_price = 0.0;
if(sl == 0.0)
if(PrintEntryLog)
{
if(PrintEntryLog) PrintFormat("-> warning: SL==0. TP not set (entry=%.10f)", entry);
PrintFormat("-> PendingEntry populated (OB method): dir=%d entry=%.10f sl=%.10f tp=%.10f lots=%.4f created=%s",
pendingEntry.direction, pendingEntry.price, pendingEntry.sl_price, pendingEntry.tp_price,
pendingEntry.lotSize, TimeToString(pendingEntry.created_time, TIME_DATE|TIME_SECONDS));
}
else
if(PrintEntryLog)
{
double diff = MathAbs(entry - sl);
if(diff <= 0.0)
{
if(PrintEntryLog) PrintFormat("-> warning: diff==0 (entry=%.10f sl=%.10f) TP not set", entry, sl);
}
if(StringLen(pendingEntry.compositeName) > 0)
PrintFormat("-> compositeName after draw: (%s)", pendingEntry.compositeName);
else
{
double tp = (pendingEntry.direction == 1) ? (entry + RiskRewardRatio * diff)
: (entry - RiskRewardRatio * diff);
pendingEntry.tp_price = NormalizeDouble(tp, digs);
if(PrintEntryLog)
PrintFormat("-> computed TP: entry=%.10f sl=%.10f diff=%.10f R:R=%.2f tp=%.10f", entry, sl, diff, RiskRewardRatio, pendingEntry.tp_price);
}
Print("-> Warning: compositeName empty after DrawPendingEntryVisuals");
}
if(PrintEntryLog) PrintFormat("-> PendingEntry populated: dir=%d entry=%.10f sl=%.10f tp=%.10f fvgIndex=%d created=%s",
pendingEntry.direction, pendingEntry.price, pendingEntry.sl_price, pendingEntry.tp_price, pendingEntry.fvgIndex, TimeToString(pendingEntry.created_time, TIME_DATE|TIME_SECONDS));
// Draw visuals
DrawPendingEntryVisuals(sym);
// After drawing, log compositeName and check each object exists
if(StringLen(pendingEntry.compositeName) > 0)
{
if(PrintEntryLog) PrintFormat("-> DrawPendingEntryVisuals set compositeName=(%s)", pendingEntry.compositeName);
string parts[];
int n = StringSplit(pendingEntry.compositeName, '|', parts);
for(int i=0; i<n; i++)
{
string nm = parts[i];
if(StringLen(nm) == 0)
{
if(PrintEntryLog) PrintFormat(" part[%d] is empty", i);
continue;
}
int found = ObjectFind(0, nm);
if(found >= 0)
PrintFormat(" OBJECT FOUND: part[%d] name='%s' ObjectFind returned=%d", i, nm, found);
else
PrintFormat(" OBJECT MISSING: part[%d] name='%s' ObjectFind returned=%d", i, nm, found);
}
}
else
{
if(PrintEntryLog) Print("-> WARNING: pendingEntry.compositeName is empty AFTER DrawPendingEntryVisuals()");
// As extra debug, attempt to reconstruct expected entry object names using created_time
int timeStamp = (int)pendingEntry.created_time;
string base = SwingObjPrefix + "PEND_";
string expect_line = base + "LINE_E_" + IntegerToString(timeStamp);
string expect_lbl = base + "LBL_E_" + IntegerToString(timeStamp);
int found_line = ObjectFind(0, expect_line);
int found_lbl = ObjectFind(0, expect_lbl);
if(PrintEntryLog) PrintFormat("-> Reconstructed expected names: %s(found=%d), %s(found=%d)", expect_line, found_line, expect_lbl, found_lbl);
}
if(PrintEntryLog) Print("==> SetUpPendingEntryForMSS END");
if(PrintEntryLog) Print("==> SetUpPendingEntryForMSS (NEW) END");
}
void OnTick()