Thêm thống kê cho bot

This commit is contained in:
Bell
2026-05-25 23:24:30 +07:00
parent f966c5ba2c
commit b3132b500b
7 changed files with 398 additions and 5 deletions
+39 -3
View File
@@ -3,7 +3,7 @@
//| BOS = Continue | CHoCH = Reversal |
//+------------------------------------------------------------------+
#property copyright "ICT 2026"
#property version "1.138"
#property version "1.142"
#property description "ICT2026 MSS limit entry + SL swing CHoCH + TP intraday"
#include <ICT2026/Config.mqh>
@@ -27,15 +27,18 @@ int OnInit()
if(!lowOk)
Print("[ICT2026] LowTF/FVG: chưa đủ dữ liệu ", EnumToString(InpFvgTf));
IctMssStats_Recompute(_Symbol, InpMssMagic);
if(biasOk || intraOk)
{
IctEaState_Refresh(_Symbol);
PrintFormat("[ICT2026] Init — EA=%s | Bias=%s | H1=%s | Allow=%s | FVG=%d",
PrintFormat("[ICT2026] Init — EA=%s | Bias=%s | H1=%s | Allow=%s | FVG=%d | Stats=%d trades",
IctEaState_Code(IctEaState_Current()),
IctBiasDisplayShort(g_ictDailyBias.bias),
IctTrendDisplayShort(g_ictIntraday.trend),
g_ictIntraday.isAllowTrade ? "true" : "false",
g_ictLowTf.availableCount);
g_ictLowTf.availableCount,
g_ictMssStats.total);
IctPanel_Render(_Symbol);
IctDraw_Render(_Symbol);
}
@@ -87,6 +90,39 @@ void OnChartEvent(const int id, const long &lparam, const double &dparam, const
}
}
//+------------------------------------------------------------------+
//| Bắt close (SL/TP/manual) → mark M5 FVG Used + reset MSS pipeline |
//+------------------------------------------------------------------+
void OnTradeTransaction(const MqlTradeTransaction &trans,
const MqlTradeRequest &request,
const MqlTradeResult &result)
{
if(trans.type != TRADE_TRANSACTION_DEAL_ADD)
return;
const ulong deal = trans.deal;
if(deal == 0 || !HistoryDealSelect(deal))
return;
if(HistoryDealGetString(deal, DEAL_SYMBOL) != _Symbol)
return;
if((ulong)HistoryDealGetInteger(deal, DEAL_MAGIC) != InpMssMagic)
return;
if(HistoryDealGetInteger(deal, DEAL_ENTRY) != DEAL_ENTRY_OUT)
return;
const long reason = HistoryDealGetInteger(deal, DEAL_REASON);
const double net = HistoryDealGetDouble(deal, DEAL_PROFIT)
+ HistoryDealGetDouble(deal, DEAL_SWAP)
+ HistoryDealGetDouble(deal, DEAL_COMMISSION);
IctMss_OnPositionClosed(_Symbol, reason, net);
IctMssStats_Recompute(_Symbol, InpMssMagic);
IctEaState_Refresh(_Symbol);
IctPanel_Render(_Symbol);
IctMssDraw_Render(_Symbol);
IctFvgDraw_Render(_Symbol);
}
//+------------------------------------------------------------------+
ENUM_ICT_BIAS ICT2026_GetDailyBias() { return g_ictDailyBias.bias; }
+3
View File
@@ -81,6 +81,9 @@ input double InpMssSlAtrMult = 0.5; // SL: buffer ngo
input double InpMssMinRR = 2.0; // TP tối thiểu (× risk entry→SL)
input int InpMssPendingExpireHours = 24; // Hết hạn pending (giờ, 0=không)
input bool InpMssOnePosition = true; // Một position/pending MSS
input bool InpMssCancelPendingEod = true; // Hủy pending cuối phiên Mỹ (nếu chưa khớp)
input int InpMssEodHour = 23; // Giờ EOD theo SERVER time (24h, vd EET broker = 23h ≈ 16:00 ET DST)
input int InpMssEodMinute = 0; // Phút EOD
input group "══ Debug ══"
input bool InpDebug = true;
+123 -1
View File
@@ -136,7 +136,7 @@ bool IctMssEntry_ComputeLevels(const string sym, const IctFvgZone &m5Zone,
return true;
}
bool IctMssEntry_HasOpenExposure(const string sym)
bool IctMssEntry_HasOpenPositionMagic(const string sym)
{
for(int i = PositionsTotal() - 1; i >= 0; i--)
{
@@ -148,6 +148,13 @@ bool IctMssEntry_HasOpenExposure(const string sym)
if((ulong)PositionGetInteger(POSITION_MAGIC) == InpMssMagic)
return true;
}
return false;
}
bool IctMssEntry_HasOpenExposure(const string sym)
{
if(IctMssEntry_HasOpenPositionMagic(sym))
return true;
for(int i = OrdersTotal() - 1; i >= 0; i--)
{
@@ -165,6 +172,109 @@ bool IctMssEntry_HasOpenExposure(const string sym)
return false;
}
string IctMssEntry_DealReasonText(const long r)
{
switch((ENUM_DEAL_REASON)r)
{
case DEAL_REASON_SL: return "SL";
case DEAL_REASON_TP: return "TP";
case DEAL_REASON_SO: return "Stop Out";
case DEAL_REASON_EXPERT: return "Expert close";
case DEAL_REASON_CLIENT: return "Manual";
case DEAL_REASON_MOBILE: return "Mobile";
case DEAL_REASON_WEB: return "Web";
default: return (r < 0) ? "Detected" : "Unknown";
}
}
void IctMssEntry_MarkM5FvgUsed(const string sym, const ulong fvgId)
{
if(fvgId == 0)
return;
const int idx = IctConfirmFvg_FindById(fvgId);
if(idx < 0)
return;
g_ictConfirmFvgZones[idx].state = ICT_FVG_USED;
if(g_ictConfirmFvgZones[idx].fvgUsedTime == 0)
g_ictConfirmFvgZones[idx].fvgUsedTime = iTime(sym, InpConfirmTf, 0);
}
void IctMss_OnEodCancel(const string sym, const ulong ticket)
{
const ulong m5Id = g_ictLowTf.mss.m5FvgId;
const ulong h1Id = g_ictLowTf.mss.h1FvgId;
if(ticket > 0 && OrderSelect(ticket))
g_ictMssTrade.OrderDelete(ticket);
IctMss_ResetState();
g_ictLowTf.mss.displayReason = StringFormat(
"EOD cancel pending #%I64u | H1 #%I64u — reset, chờ phiên mới",
ticket, h1Id);
if(InpMssLogJournal)
PrintFormat("[ICT2026/MSS] EOD cancel limit #%I64u | H1 #%I64u | M5 #%I64u | reset → WAIT_FVG_TOUCH",
ticket, h1Id, m5Id);
}
void IctMssEntry_CheckEodCancel(const string sym)
{
if(!InpMssCancelPendingEod)
return;
if(g_ictLowTf.mss.pendingTicket == 0)
return;
if(IctMssEntry_HasOpenPositionMagic(sym))
return;
const ulong ticket = g_ictLowTf.mss.pendingTicket;
if(!OrderSelect(ticket))
{
g_ictLowTf.mss.pendingTicket = 0;
return;
}
MqlDateTime mdt;
TimeToStruct(TimeCurrent(), mdt);
const int nowMinutes = mdt.hour * 60 + mdt.min;
const int eodMinutes = InpMssEodHour * 60 + InpMssEodMinute;
if(nowMinutes < eodMinutes)
return;
MqlDateTime d0;
d0.year = mdt.year;
d0.mon = mdt.mon;
d0.day = mdt.day;
d0.hour = 0;
d0.min = 0;
d0.sec = 0;
const datetime dayStart = StructToTime(d0);
static datetime s_lastEodHandledDate = 0;
if(s_lastEodHandledDate == dayStart)
return;
s_lastEodHandledDate = dayStart;
IctMss_OnEodCancel(sym, ticket);
}
void IctMss_OnPositionClosed(const string sym, const long reason, const double netProfit)
{
const ulong m5Id = g_ictLowTf.mss.m5FvgId;
const ulong h1Id = g_ictLowTf.mss.h1FvgId;
const string rt = IctMssEntry_DealReasonText(reason);
IctMssEntry_MarkM5FvgUsed(sym, m5Id);
IctMss_ResetState();
g_ictLowTf.mss.displayReason = StringFormat(
"Đóng %s (net %.2f) | M5 FVG #%I64u → Used | chờ POI mới",
rt, netProfit, m5Id);
if(InpMssLogJournal)
PrintFormat("[ICT2026/MSS] Close (%s, net=%.2f) | H1 #%I64u | M5 #%I64u → Used | reset → WAIT_FVG_TOUCH",
rt, netProfit, h1Id, m5Id);
}
bool IctMssEntry_CancelTicket(const ulong ticket)
{
if(ticket == 0)
@@ -216,6 +326,18 @@ void IctMssEntry_Init()
void IctMssEntry_Update(const string sym)
{
static bool s_hadPosition = false;
const bool nowPosition = IctMssEntry_HasOpenPositionMagic(sym);
if(s_hadPosition && !nowPosition && g_ictLowTf.mss.phase != ICT_MSS_IDLE)
{
// Fallback nếu OnTradeTransaction miss (reload, disconnect, …)
IctMss_OnPositionClosed(sym, -1L, 0.0);
}
s_hadPosition = nowPosition;
IctMssEntry_CheckEodCancel(sym);
if(g_ictLowTf.mss.phase == ICT_MSS_IDLE && g_ictLowTf.mss.pendingTicket > 0)
{
IctMssEntry_CancelTicket(g_ictLowTf.mss.pendingTicket);
+21 -1
View File
@@ -10,10 +10,11 @@
#include <ICT2026/IntradayStructure.mqh>
#include <ICT2026/LowTfTrend.mqh>
#include <ICT2026/EaState.mqh>
#include <ICT2026/Stats.mqh>
const string ICT26_PANEL_PFX = "ICT26_PNL_";
const string ICT26_PANEL_LEGACY = "ICT26_BIAS_PANEL";
const int ICT26_PANEL_MAXLINE = 16;
const int ICT26_PANEL_MAXLINE = 20;
const color ICT_PANEL_CLR_UP = clrLime;
const color ICT_PANEL_CLR_DOWN = clrOrangeRed;
@@ -197,6 +198,25 @@ void IctPanel_Render(const string sym)
lines[n] = StringFormat("H1 FVG: %s", g_ictLowTf.displayReason);
colors[n++] = (g_ictLowTf.availableCount > 0) ? ICT_PANEL_CLR_ALLOW : ICT_PANEL_CLR_MUTED;
ArrayResize(lines, n + 1);
ArrayResize(colors, n + 1);
lines[n] = "-------------------------";
colors[n++] = ICT_PANEL_CLR_NEUTRAL;
const color clrStatsCounts = (g_ictMssStats.total > 0) ? ICT_PANEL_CLR_ALLOW : ICT_PANEL_CLR_MUTED;
const color clrStatsPerf = (g_ictMssStats.netProfit > 0.0) ? ICT_PANEL_CLR_UP :
(g_ictMssStats.netProfit < 0.0) ? ICT_PANEL_CLR_DOWN : ICT_PANEL_CLR_MUTED;
ArrayResize(lines, n + 1);
ArrayResize(colors, n + 1);
lines[n] = IctMssStats_LineCounts();
colors[n++] = clrStatsCounts;
ArrayResize(lines, n + 1);
ArrayResize(colors, n + 1);
lines[n] = IctMssStats_LinePerf();
colors[n++] = clrStatsPerf;
for(int i = 0; i < n; i++)
IctPanel_SetLine(ch, i, i * lh, lines[i], colors[i]);
+30
View File
@@ -566,6 +566,36 @@ ENUM_ICT_BIAS ICT2026_GetDailyBias();
## Changelog
### v1.142 — Fix Stats đếm thiếu lệnh
- Bug v1.141: `IctMssStats_ComputeR` gọi `HistorySelectByPosition` **bên trong vòng quét history toàn cục**
→ selection bị thay → các index sau đọc từ history của 1 position → chỉ đếm được lệnh đầu tiên
- Fix: 2-pass — collect ticket/reason/net trước (selection toàn cục còn nguyên), tính R sau
- Thêm log journal `[ICT2026/Stats] Recompute …` để verify mỗi lần quét
### v1.141 — Stats lệnh MSS trên panel
- `Stats.mqh`: `IctMssStats_Recompute` quét HistoryDeals theo magic
- Tính: total, TP, SL, khác, win, loss, sumR, netProfit
- R thực = (close - entry)/(entry - SL) lấy từ deal IN + order SL
- WR = TP / (TP+SL); Ravg = sumR/total
- Hiển thị 2 dòng cuối panel: counts + WR/Ravg/Net
- Recompute trên `OnInit` và sau mỗi `OnTradeTransaction` close
### v1.140 — Hủy pending cuối phiên Mỹ (EOD)
- Input: `InpMssCancelPendingEod` (true), `InpMssEodHour` (23), `InpMssEodMinute` (0) — **server time**
- Mỗi tick: nếu có `pendingTicket` chưa fill + thời gian ≥ EOD → `OrderDelete` + `IctMss_ResetState`
- Idempotent theo ngày: dùng `s_lastEodHandledDate` để không lặp
- Bỏ qua nếu đang có position magic (chỉ hủy pending, không động vào lệnh đã khớp)
### v1.139 — Reset MSS pipeline sau SL/TP/close (option A)
- `OnTradeTransaction` lọc `DEAL_ENTRY_OUT` + magic → `IctMss_OnPositionClosed`
- Mark **M5 FVG → Used**, `IctMss_ResetState`, journal lý do (SL/TP/Manual/…)
- Fallback: detect transition `hadPosition → false` trong `IctMssEntry_Update` (miss OnTradeTransaction)
- Sau close: EaState = `WAIT_FVG_TOUCH``SelectNearestH1Poi` (H1 cũ đã Used từ v1.136 → bỏ qua)
### v1.138 — POI H1 = FVG chưa Used **gần giá nhất**
- `IctMss_SelectNearestH1Poi`: khoảng cách bid → FVG; bỏ qua Used; tie → FVG mới hơn
+182
View File
@@ -0,0 +1,182 @@
//+------------------------------------------------------------------+
//| Stats.mqh — thống kê lệnh MSS từ history (theo magic) |
//+------------------------------------------------------------------+
#ifndef ICT2026_STATS_MQH
#define ICT2026_STATS_MQH
#include <ICT2026/Config.mqh>
struct IctMssStats
{
int total;
int tpCount;
int slCount;
int otherCount; // manual / expert / mobile / web
int winCount; // profit > 0
int lossCount; // profit < 0
double sumR; // tổng R đạt được (đã quy chuẩn theo risk lúc vào lệnh)
double netProfit;
void Clear()
{
total = tpCount = slCount = otherCount = 0;
winCount = lossCount = 0;
sumR = 0.0;
netProfit = 0.0;
}
};
IctMssStats g_ictMssStats;
static datetime g_ictMssStatsLastScan = 0;
double IctMssStats_ComputeR(const ulong outDeal)
{
const ulong posId = (ulong)HistoryDealGetInteger(outDeal, DEAL_POSITION_ID);
const double closePrice = HistoryDealGetDouble(outDeal, DEAL_PRICE);
if(posId == 0 || closePrice <= 0.0)
return 0.0;
if(!HistorySelectByPosition(posId))
return 0.0;
double entryPrice = 0.0;
double slPrice = 0.0;
bool isBuy = false;
const int n = HistoryDealsTotal();
for(int i = 0; i < n; i++)
{
const ulong d = HistoryDealGetTicket(i);
if(d == 0)
continue;
if((ulong)HistoryDealGetInteger(d, DEAL_POSITION_ID) != posId)
continue;
if(HistoryDealGetInteger(d, DEAL_ENTRY) != DEAL_ENTRY_IN)
continue;
entryPrice = HistoryDealGetDouble(d, DEAL_PRICE);
isBuy = (HistoryDealGetInteger(d, DEAL_TYPE) == DEAL_TYPE_BUY);
const ulong orderTicket = (ulong)HistoryDealGetInteger(d, DEAL_ORDER);
if(orderTicket > 0 && HistoryOrderSelect(orderTicket))
slPrice = HistoryOrderGetDouble(orderTicket, ORDER_SL);
break;
}
if(entryPrice <= 0.0 || slPrice <= 0.0)
return 0.0;
const double risk = isBuy ? (entryPrice - slPrice) : (slPrice - entryPrice);
if(risk <= _Point)
return 0.0;
const double move = isBuy ? (closePrice - entryPrice) : (entryPrice - closePrice);
return move / risk;
}
void IctMssStats_Recompute(const string sym, const ulong magic)
{
g_ictMssStats.Clear();
if(!HistorySelect(0, TimeCurrent()))
return;
// Pass 1: thu thập ticket + dữ liệu cơ bản trong khi global history selection còn nguyên.
// Lưu ý: nếu gọi HistorySelectByPosition (trong ComputeR) ở đây → selection bị thay,
// các index sau sẽ đọc từ history của 1 position → đếm thiếu lệnh.
ulong outTickets[];
long reasons[];
double nets[];
int cached = 0;
const int n = HistoryDealsTotal();
ArrayResize(outTickets, n);
ArrayResize(reasons, n);
ArrayResize(nets, n);
for(int i = 0; i < n; i++)
{
const ulong d = HistoryDealGetTicket(i);
if(d == 0)
continue;
if(HistoryDealGetString(d, DEAL_SYMBOL) != sym)
continue;
if((ulong)HistoryDealGetInteger(d, DEAL_MAGIC) != magic)
continue;
if(HistoryDealGetInteger(d, DEAL_ENTRY) != DEAL_ENTRY_OUT)
continue;
outTickets[cached] = d;
reasons[cached] = HistoryDealGetInteger(d, DEAL_REASON);
nets[cached] = HistoryDealGetDouble(d, DEAL_PROFIT)
+ HistoryDealGetDouble(d, DEAL_SWAP)
+ HistoryDealGetDouble(d, DEAL_COMMISSION);
cached++;
}
// Pass 2: tính R cho từng deal (mỗi lần gọi sẽ thay selection — không ảnh hưởng vì đã cache xong).
for(int k = 0; k < cached; k++)
{
const double r = IctMssStats_ComputeR(outTickets[k]);
g_ictMssStats.total++;
g_ictMssStats.netProfit += nets[k];
g_ictMssStats.sumR += r;
const ENUM_DEAL_REASON rs = (ENUM_DEAL_REASON)reasons[k];
if(rs == DEAL_REASON_TP)
g_ictMssStats.tpCount++;
else if(rs == DEAL_REASON_SL)
g_ictMssStats.slCount++;
else
g_ictMssStats.otherCount++;
if(nets[k] > 0.0)
g_ictMssStats.winCount++;
else if(nets[k] < 0.0)
g_ictMssStats.lossCount++;
}
g_ictMssStatsLastScan = TimeCurrent();
if(InpMssLogJournal)
PrintFormat("[ICT2026/Stats] Recompute %s magic=%I64u | total=%d TP=%d SL=%d khác=%d | net=%.2f sumR=%.2f",
sym, magic,
g_ictMssStats.total, g_ictMssStats.tpCount,
g_ictMssStats.slCount, g_ictMssStats.otherCount,
g_ictMssStats.netProfit, g_ictMssStats.sumR);
}
double IctMssStats_WinratePct()
{
const int decided = g_ictMssStats.tpCount + g_ictMssStats.slCount;
if(decided == 0)
return 0.0;
return 100.0 * (double)g_ictMssStats.tpCount / (double)decided;
}
double IctMssStats_AvgR()
{
if(g_ictMssStats.total <= 0)
return 0.0;
return g_ictMssStats.sumR / (double)g_ictMssStats.total;
}
string IctMssStats_LineCounts()
{
return StringFormat("Stats: %d lệnh | TP %d | SL %d | khác %d",
g_ictMssStats.total,
g_ictMssStats.tpCount,
g_ictMssStats.slCount,
g_ictMssStats.otherCount);
}
string IctMssStats_LinePerf()
{
const double wr = IctMssStats_WinratePct();
const double ar = IctMssStats_AvgR();
return StringFormat("WR %.1f%% | Ravg %+.2fR | Net %+.2f",
wr, ar, g_ictMssStats.netProfit);
}
#endif
BIN
View File
Binary file not shown.