#ifndef NN_DATA_LOGGER_MQH #define NN_DATA_LOGGER_MQH //+------------------------------------------------------------------+ //| NNDataLogger.mqh | //| Real-trade labeling via MT5 deal history. | //| Phase 1 — NN_CaptureEntry(): snapshot features at trade open. | //| Phase 2 — NN_LabelFromDeal(): write labeled row when trade closes.| //| | //| Replaces the old speculative 180-bar lookahead approach. | //+------------------------------------------------------------------+ #property strict #include #include "NNFeatures.mqh" #include "InputParams.mqh" // ─── Pending-entry store ───────────────────────────────────────────────────── struct NNPendingEntry { ulong position_id; bool is_buy; double entry_price; datetime entry_time; string setup_name; string session_name; string session_phase; string symbol_profile; double cluster_strength; double conflict_score; int category; // 0 = continuation, 1 = counter double spread_points; double free_margin; double equity; double balance; double margin_required; double risk_amount; double features[12]; int n_features; }; #define NN_MAX_PENDING 60 // safely above Max_Open_Trades (20) with headroom NNPendingEntry g_nn_pending[NN_MAX_PENDING]; int g_nn_pending_count = 0; // ─── Phase 1: capture at trade open ───────────────────────────────────────── // // Call this immediately after a successful Trade.Buy() / Trade.Sell(). // position_id — from HistoryDealGetInteger(entry_deal, DEAL_POSITION_ID) // is_buy — true for long, false for short // setup — reason string passed to Trade.Buy/Sell // cluster_strength, conflict_score — from Coordinator globals // category — 0 = continuation, 1 = counter void NN_CaptureEntry(ulong position_id, bool is_buy, const string setup, const double entry_price, const string session_name, const string session_phase, const string symbol_profile, double cluster_strength, double conflict_score, int category, double spread_points, double free_margin, double equity, double balance, double margin_required, double risk_amount) { if(!NN_LogData) return; if(conflict_score > NN_MaxConflictToLog) return; if(g_nn_pending_count >= NN_MAX_PENDING) return; double feat[]; int n = BuildNNFeatures(feat); if(n <= 0) return; int idx = g_nn_pending_count; g_nn_pending[idx].position_id = position_id; g_nn_pending[idx].is_buy = is_buy; g_nn_pending[idx].entry_price = entry_price; g_nn_pending[idx].entry_time = TimeCurrent(); g_nn_pending[idx].setup_name = setup; g_nn_pending[idx].session_name = session_name; g_nn_pending[idx].session_phase = session_phase; g_nn_pending[idx].symbol_profile = symbol_profile; g_nn_pending[idx].cluster_strength = cluster_strength; g_nn_pending[idx].conflict_score = conflict_score; g_nn_pending[idx].category = category; g_nn_pending[idx].spread_points = spread_points; g_nn_pending[idx].free_margin = free_margin; g_nn_pending[idx].equity = equity; g_nn_pending[idx].balance = balance; g_nn_pending[idx].margin_required = margin_required; g_nn_pending[idx].risk_amount = risk_amount; g_nn_pending[idx].n_features = MathMin(n, 12); for(int i = 0; i < g_nn_pending[idx].n_features; i++) g_nn_pending[idx].features[i] = feat[i]; g_nn_pending_count++; } void NN_LogRejectedDecision(const bool is_buy, const string setup, const string reason, const double lots, const double entry_price, const string session_name, const string session_phase, const string symbol_profile, const double cluster_strength, const double conflict_score, const int category, const double spread_points, const double free_margin, const double equity, const double balance, const double margin_required, const double risk_amount) { if(!NN_LogData) return; double feat[]; int n = BuildNNFeatures(feat); if(n <= 0) return; int h = FileOpen(NN_LogFileName, FILE_READ|FILE_WRITE|FILE_CSV|FILE_ANSI); if(h == INVALID_HANDLE) return; FileSeek(h, 0, SEEK_END); if(FileSize(h) == 0) { FileWrite(h, "status", "entry_time", "exit_time", "symbol", "tf", "session", "phase", "symbol_profile", "is_buy", "category", "setup", "reason", "cluster_strength", "conflict_score", "entry_price", "exit_price", "spread_points", "free_margin", "equity", "balance", "margin_required", "risk_amount", "lots", "label", "duration_min", "n_features", "features_json" ); } string fjson = "["; for(int k = 0; k < MathMin(n, 12); k++) { fjson += DoubleToString(feat[k], 8); if(k < MathMin(n, 12) - 1) fjson += ","; } fjson += "]"; FileWrite(h, "rejected", (string)TimeCurrent(), "", _Symbol, (string)Period(), session_name, session_phase, symbol_profile, (is_buy ? 1 : 0), category, setup, reason, DoubleToString(cluster_strength, 2), DoubleToString(conflict_score, 2), DoubleToString(entry_price, _Digits), "", DoubleToString(spread_points, 1), DoubleToString(free_margin, 2), DoubleToString(equity, 2), DoubleToString(balance, 2), DoubleToString(margin_required, 2), DoubleToString(risk_amount, 2), DoubleToString(lots, 2), 0, 0, MathMin(n, 12), fjson ); FileClose(h); } // ─── Phase 2: label and write at trade close ───────────────────────────────── // // Call from OnTradeTransaction when entry == DEAL_ENTRY_OUT. // deal_ticket must already be selected with HistoryDealSelect(). void NN_LabelFromDeal(ulong deal_ticket) { if(!NN_LogData) return; ulong pos_id = (ulong)HistoryDealGetInteger(deal_ticket, DEAL_POSITION_ID); // Find the matching pending entry int idx = -1; for(int i = 0; i < g_nn_pending_count; i++) { if(g_nn_pending[i].position_id == pos_id) { idx = i; break; } } if(idx < 0) return; // trade wasn't captured (manual trade or already labeled) double profit = HistoryDealGetDouble(deal_ticket, DEAL_PROFIT); double exit_price = HistoryDealGetDouble(deal_ticket, DEAL_PRICE); datetime exit_time = (datetime)HistoryDealGetInteger(deal_ticket, DEAL_TIME); int dur_min = (int)((exit_time - g_nn_pending[idx].entry_time) / 60); int label = (profit > 0.0) ? 1 : 0; // 1 = win, 0 = loss/breakeven // Write CSV row int h = FileOpen(NN_LogFileName, FILE_READ|FILE_WRITE|FILE_CSV|FILE_ANSI); if(h != INVALID_HANDLE) { FileSeek(h, 0, SEEK_END); if(FileSize(h) == 0) { FileWrite(h, "status", "entry_time", "exit_time", "symbol", "tf", "session", "phase", "symbol_profile", "is_buy", "category", "setup", "cluster_strength", "conflict_score", "entry_price", "exit_price", "spread_points", "free_margin", "equity", "balance", "margin_required", "risk_amount", "profit", "label", "duration_min", "n_features", "features_json" ); } string fjson = "["; for(int k = 0; k < g_nn_pending[idx].n_features; k++) { fjson += DoubleToString(g_nn_pending[idx].features[k], 8); if(k < g_nn_pending[idx].n_features - 1) fjson += ","; } fjson += "]"; FileWrite(h, "closed", (string)g_nn_pending[idx].entry_time, (string)exit_time, _Symbol, (string)Period(), g_nn_pending[idx].session_name, g_nn_pending[idx].session_phase, g_nn_pending[idx].symbol_profile, (g_nn_pending[idx].is_buy ? 1 : 0), g_nn_pending[idx].category, g_nn_pending[idx].setup_name, DoubleToString(g_nn_pending[idx].cluster_strength, 2), DoubleToString(g_nn_pending[idx].conflict_score, 2), DoubleToString(g_nn_pending[idx].entry_price, _Digits), DoubleToString(exit_price, _Digits), DoubleToString(g_nn_pending[idx].spread_points, 1), DoubleToString(g_nn_pending[idx].free_margin, 2), DoubleToString(g_nn_pending[idx].equity, 2), DoubleToString(g_nn_pending[idx].balance, 2), DoubleToString(g_nn_pending[idx].margin_required, 2), DoubleToString(g_nn_pending[idx].risk_amount, 2), DoubleToString(profit, 2), label, dur_min, g_nn_pending[idx].n_features, fjson ); FileClose(h); } UpdateSymbolPolicyFeedback(g_nn_pending[idx].symbol_profile, profit, g_nn_pending[idx].spread_points, (g_nn_pending[idx].equity > 0.0) ? ((g_nn_pending[idx].free_margin / g_nn_pending[idx].equity) * 100.0) : 0.0, (g_nn_pending[idx].equity > 0.0) ? ((g_nn_pending[idx].risk_amount / g_nn_pending[idx].equity) * 100.0) : 0.0, g_nn_pending[idx].category); // Remove from pending array (shift remaining entries down) for(int i = idx; i < g_nn_pending_count - 1; i++) g_nn_pending[i] = g_nn_pending[i + 1]; g_nn_pending_count--; } #endif