VizionAI-Trading-EA/NNDataLogger.mqh

298 righe
11 KiB
MQL5

#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 <Files\File.mqh>
#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