diff --git a/Experts/EA_ICT.txt b/Experts/EA_ICT.txt index 4cea920..c4fe8f1 100644 --- a/Experts/EA_ICT.txt +++ b/Experts/EA_ICT.txt @@ -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 \ No newline at end of file +- 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% \ No newline at end of file diff --git a/Experts/EA_ICT_v2.mq5 b/Experts/EA_ICT_v2.mq5 index 0fe6052..c0a8343 100644 --- a/Experts/EA_ICT_v2.mq5 +++ b/Experts/EA_ICT_v2.mq5 @@ -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 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 Price window between sweep & break: pmin=%.10f pmax=%.10f tol=%.10g", pmin, pmax, tol); - - int chosenIdx = -1; - for(int i=0; i= 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= 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()