906 lines
45 KiB
MQL5
906 lines
45 KiB
MQL5
// KnowledgeBase.mqh
|
|
// Shared functions for reading from and writing to the knowledge base.
|
|
|
|
#property copyright "2025, Windsurf Engineering"
|
|
#property link "https://www.windsurf.ai"
|
|
|
|
#include <Files\File.mqh>
|
|
// Centralized default strategy names
|
|
#include <Strategies\Registry.mqh>
|
|
|
|
// --- Defines the structure for a single trade record
|
|
struct TradeRecord
|
|
{
|
|
datetime timestamp; // Trade execution timestamp
|
|
string symbol; // Trading symbol
|
|
ENUM_ORDER_TYPE type; // Order type (e.g., ORDER_TYPE_BUY, ORDER_TYPE_SELL)
|
|
double entry_price; // Price at which the trade was opened
|
|
double stop_loss; // Stop loss level
|
|
double take_profit; // Take profit level
|
|
double close_price; // Price at which the trade was closed
|
|
double profit; // Profit or loss from the trade
|
|
string strategy_id; // ID of the strategy that generated the trade
|
|
};
|
|
// Uncomment to enable verbose parsing diagnostics
|
|
//#define INSIGHTS_DEBUG
|
|
|
|
// --- Class to manage knowledge base operations
|
|
class CKnowledgeBase
|
|
{
|
|
private:
|
|
string m_file_path; // Path to the knowledge base file
|
|
int m_file_handle; // File handle
|
|
string m_csv_delimiter; // CSV delimiter
|
|
string m_lock_path; // Path to lock file in Common Files
|
|
int m_lock_handle; // Lock file handle
|
|
|
|
public:
|
|
CKnowledgeBase(string file_name="DualEA\\knowledge_base.csv", string delimiter=",");
|
|
~CKnowledgeBase();
|
|
|
|
// --- Methods for data handling
|
|
bool WriteRecord(const TradeRecord &record);
|
|
bool LogTrade(const string strategy_name, const int retcode, const ulong deal, const ulong order_id);
|
|
|
|
private:
|
|
bool OpenFile(int flags=FILE_WRITE|FILE_READ|FILE_CSV);
|
|
void CloseFile();
|
|
bool AcquireLock(const int timeout_ms=3000);
|
|
void ReleaseLock();
|
|
};
|
|
|
|
// --- Class to manage features.csv logging (long format: one feature per row)
|
|
class CFeaturesKB
|
|
{
|
|
private:
|
|
string m_file_path;
|
|
int m_file_handle;
|
|
string m_csv_delim;
|
|
void EnsureHeader()
|
|
{
|
|
// Ensure folder exists and header is present
|
|
FolderCreate("DualEA", FILE_COMMON);
|
|
int h = FileOpen(m_file_path, FILE_READ|FILE_CSV|FILE_COMMON);
|
|
if(h==INVALID_HANDLE)
|
|
{
|
|
h = FileOpen(m_file_path, FILE_WRITE|FILE_CSV|FILE_COMMON);
|
|
if(h!=INVALID_HANDLE)
|
|
{
|
|
FileWriteString(h, "timestamp,symbol,strategy,feature,value\n");
|
|
FileClose(h);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if(FileSize(h)==0)
|
|
{
|
|
FileClose(h);
|
|
h = FileOpen(m_file_path, FILE_WRITE|FILE_CSV|FILE_COMMON);
|
|
if(h!=INVALID_HANDLE)
|
|
FileWriteString(h, "timestamp,symbol,strategy,feature,value\n");
|
|
}
|
|
if(h!=INVALID_HANDLE) FileClose(h);
|
|
}
|
|
string full = TerminalInfoString(TERMINAL_COMMONDATA_PATH) + "\\Files\\" + m_file_path;
|
|
PrintFormat("KnowledgeBase: writing features CSV to Common Files: %s", full);
|
|
}
|
|
public:
|
|
CFeaturesKB(string file_name="DualEA\\features.csv", string delimiter=",")
|
|
{
|
|
m_file_path = file_name;
|
|
m_csv_delim = delimiter;
|
|
m_file_handle = INVALID_HANDLE;
|
|
EnsureHeader();
|
|
}
|
|
~CFeaturesKB() {}
|
|
bool WriteKV(const datetime ts, const string symbol, const string strategy, const string feature, const double value)
|
|
{
|
|
int h = FileOpen(m_file_path, FILE_READ|FILE_WRITE|FILE_CSV|FILE_SHARE_WRITE|FILE_COMMON);
|
|
if(h==INVALID_HANDLE)
|
|
{
|
|
PrintFormat("Error opening features file '%s'. Error: %d", m_file_path, GetLastError());
|
|
return false;
|
|
}
|
|
if(FileSize(h)==0)
|
|
FileWriteString(h, "timestamp,symbol,strategy,feature,value\n");
|
|
FileSeek(h, 0, SEEK_END);
|
|
string line = TimeToString(ts) + m_csv_delim + symbol + m_csv_delim + strategy + m_csv_delim + feature + m_csv_delim + DoubleToString(value, 8);
|
|
FileWriteString(h, line + "\n");
|
|
FileClose(h);
|
|
return true;
|
|
}
|
|
};
|
|
|
|
// --- Insights builder: reads KB and emits insights.json (basic counts scaffold)
|
|
class CInsightsBuilder
|
|
{
|
|
private:
|
|
string m_kb_path;
|
|
string m_features_path;
|
|
string m_out_path;
|
|
string m_delim;
|
|
// --- helpers
|
|
string Trim(string s)
|
|
{
|
|
StringTrimLeft(s);
|
|
StringTrimRight(s);
|
|
return s;
|
|
}
|
|
string StripQuotes(string s)
|
|
{
|
|
s = Trim(s);
|
|
// strip BOM if present (U+FEFF or UTF-8 BOM 0xEF 0xBB 0xBF)
|
|
if(StringLen(s)>0)
|
|
{
|
|
int c0 = StringGetCharacter(s,0);
|
|
// U+FEFF
|
|
if(c0==65279)
|
|
s = StringSubstr(s,1);
|
|
else if(StringLen(s)>=3)
|
|
{
|
|
int b0 = c0;
|
|
int b1 = StringGetCharacter(s,1);
|
|
int b2 = StringGetCharacter(s,2);
|
|
if(b0==239 && b1==187 && b2==191)
|
|
s = StringSubstr(s,3);
|
|
}
|
|
}
|
|
// remove surrounding quotes
|
|
if(StringLen(s)>=2 && StringSubstr(s,0,1)=="\"" && StringSubstr(s,StringLen(s)-1,1)=="\"")
|
|
s = StringSubstr(s,1,StringLen(s)-2);
|
|
// remove stray quotes
|
|
StringReplace(s, "\"", "");
|
|
// remove CR/LF and NBSP
|
|
StringReplace(s, "\r", "");
|
|
StringReplace(s, "\n", "");
|
|
// remove non-breaking space (char code 160)
|
|
for(int i=0;i<StringLen(s);++i){ if(StringGetCharacter(s,i)==160){ s = StringSubstr(s,0,i)+StringSubstr(s,i+1); i--; } }
|
|
return s;
|
|
}
|
|
string NormalizeToken(string s)
|
|
{
|
|
s = StripQuotes(s);
|
|
// StringToLower modifies in place; do not assign its return value
|
|
StringToLower(s);
|
|
string out="";
|
|
for(int i=0;i<StringLen(s);++i)
|
|
{
|
|
int ch = StringGetCharacter(s,i);
|
|
bool ok = (ch>='a' && ch<='z') || (ch>='0' && ch<='9') || (ch=='_');
|
|
if(ok)
|
|
out = out + StringSubstr(s,i,1);
|
|
}
|
|
return out;
|
|
}
|
|
int FindIndexString(string &arr[], const string key)
|
|
{
|
|
for(int i=0;i<ArraySize(arr);++i)
|
|
if(arr[i]==key) return i;
|
|
return -1;
|
|
}
|
|
void PushUnique(string &arr[], const string key)
|
|
{
|
|
if(FindIndexString(arr,key)<0)
|
|
{
|
|
int n = ArraySize(arr);
|
|
ArrayResize(arr, n+1);
|
|
arr[n] = key;
|
|
}
|
|
}
|
|
double SafeDiv(const double a, const double b)
|
|
{
|
|
if(b==0.0) return 0.0;
|
|
return a/b;
|
|
}
|
|
double Median(double &values[])
|
|
{
|
|
int n = ArraySize(values);
|
|
if(n==0) return 0.0;
|
|
// Work on a copy to avoid side effects
|
|
double tmp[];
|
|
ArrayResize(tmp, n);
|
|
for(int i=0;i<n;++i) tmp[i]=values[i];
|
|
ArraySort(tmp);
|
|
if((n % 2)==1)
|
|
return tmp[n/2];
|
|
return 0.5*(tmp[n/2-1] + tmp[n/2]);
|
|
}
|
|
double MaxDrawdownR(double &r_series[])
|
|
{
|
|
// Computes max peak-to-trough drawdown on cumulative R
|
|
int n = ArraySize(r_series);
|
|
if(n==0) return 0.0;
|
|
double cum=0.0, peak=0.0, maxdd=0.0;
|
|
for(int i=0;i<n;++i)
|
|
{
|
|
cum += r_series[i];
|
|
if(cum>peak) peak=cum;
|
|
double dd = peak - cum; // in R
|
|
if(dd>maxdd) maxdd=dd;
|
|
}
|
|
return maxdd;
|
|
}
|
|
public:
|
|
CInsightsBuilder(string kb_path="DualEA\\knowledge_base.csv", string features_path="DualEA\\features.csv", string out_path="DualEA\\insights.json", string delim=",")
|
|
{
|
|
m_kb_path = kb_path;
|
|
m_features_path = features_path;
|
|
m_out_path = out_path;
|
|
m_delim = delim;
|
|
}
|
|
bool Build()
|
|
{
|
|
// Build insights from features.csv using r_multiple rows
|
|
// Open features.csv with explicit comma delimiter
|
|
int hf = FileOpen(m_features_path, FILE_READ|FILE_CSV|FILE_SHARE_READ|FILE_COMMON, (ushort)',' );
|
|
if(hf==INVALID_HANDLE)
|
|
{
|
|
PrintFormat("InsightsBuilder: cannot open features '%s'. Err=%d", m_features_path, GetLastError());
|
|
return false;
|
|
}
|
|
|
|
// Storage of r-multiples with metadata (plus optional timeframe)
|
|
datetime ts_list[]; string sym_list[]; string strat_list[]; double r_list[]; int tf_list[];
|
|
// Map (symbol|strategy) -> timeframe (last seen)
|
|
string tf_keys[]; int tf_vals[];
|
|
// Collect all observed (symbol|strategy) pairs to support synthesis when timeframe is absent
|
|
string all_keys[];
|
|
|
|
// Robust CSV scan: 5 fields per row, skip header if present
|
|
bool header_checked=false;
|
|
int parsed_rows=0;
|
|
while(!FileIsEnding(hf))
|
|
{
|
|
string ts = FileReadString(hf);
|
|
if(ts=="" && FileIsEnding(hf)) break;
|
|
string sym = FileReadString(hf);
|
|
string strat = FileReadString(hf);
|
|
string feat = FileReadString(hf);
|
|
string val = FileReadString(hf);
|
|
|
|
// Trim all fields to avoid CR/whitespace issues
|
|
ts = StripQuotes(ts);
|
|
sym = StripQuotes(sym);
|
|
strat = StripQuotes(strat);
|
|
feat = StripQuotes(feat);
|
|
val = StripQuotes(val);
|
|
|
|
if(feat=="") { continue; }
|
|
if(!header_checked)
|
|
{
|
|
header_checked=true;
|
|
string ts_norm = NormalizeToken(ts);
|
|
string feat_norm_dbg = NormalizeToken(feat);
|
|
int c0 = (StringLen(ts)>0? StringGetCharacter(ts,0) : -1);
|
|
#ifdef INSIGHTS_DEBUG
|
|
PrintFormat("InsightsBuilder: header-check ts_raw='%s' ts_norm='%s' c0=%d feat_raw='%s' feat_norm='%s'", ts, ts_norm, c0, feat, feat_norm_dbg);
|
|
#endif
|
|
if(ts_norm=="timestamp")
|
|
continue; // skip header row
|
|
}
|
|
|
|
// Count this parsed data row (post header)
|
|
parsed_rows++;
|
|
if(parsed_rows<=5)
|
|
{
|
|
string feat_norm = NormalizeToken(feat);
|
|
#ifdef INSIGHTS_DEBUG
|
|
PrintFormat("InsightsBuilder: sample row %d feat='%s' feat_norm='%s' val='%s'", parsed_rows-1, feat, feat_norm, val);
|
|
#endif
|
|
}
|
|
|
|
// Track (symbol|strategy) pairs from any feature rows
|
|
if(sym!="" && strat!="")
|
|
{
|
|
string key_any = sym + "|" + strat;
|
|
PushUnique(all_keys, key_any);
|
|
}
|
|
|
|
string feat_norm_main = NormalizeToken(feat);
|
|
if(feat_norm_main=="timeframe")
|
|
{
|
|
// record latest timeframe for (symbol,strategy)
|
|
int tfv = (int)StringToInteger(val);
|
|
string key = sym+"|"+strat;
|
|
bool updated=false;
|
|
for(int i=0;i<ArraySize(tf_keys);++i){ if(tf_keys[i]==key){ tf_vals[i]=tfv; updated=true; break; } }
|
|
if(!updated){ int k=ArraySize(tf_keys); ArrayResize(tf_keys,k+1); ArrayResize(tf_vals,k+1); tf_keys[k]=key; tf_vals[k]=tfv; }
|
|
}
|
|
else if(feat_norm_main=="r_multiple")
|
|
{
|
|
int n = ArraySize(r_list);
|
|
ArrayResize(ts_list, n+1);
|
|
ArrayResize(sym_list, n+1);
|
|
ArrayResize(strat_list, n+1);
|
|
ArrayResize(r_list, n+1);
|
|
ArrayResize(tf_list, n+1);
|
|
ts_list[n] = StringToTime(ts);
|
|
sym_list[n] = sym;
|
|
strat_list[n] = strat;
|
|
r_list[n] = StringToDouble(val);
|
|
// lookup timeframe if available
|
|
int tfv=-1; string key2 = sym_list[n]+"|"+strat_list[n];
|
|
for(int j=0;j<ArraySize(tf_keys);++j){ if(tf_keys[j]==key2){ tfv=tf_vals[j]; break; } }
|
|
tf_list[n] = tfv;
|
|
#ifdef INSIGHTS_DEBUG
|
|
if(n<5)
|
|
PrintFormat("InsightsBuilder: captured r_multiple row ts=%s sym=%s strat=%s r=%f", ts, sym, strat, r_list[n]);
|
|
#endif
|
|
}
|
|
}
|
|
FileClose(hf);
|
|
|
|
int total = ArraySize(r_list);
|
|
PrintFormat("InsightsBuilder: scanned rows=%d, r_multiple rows=%d from features '%s'", parsed_rows, total, m_features_path);
|
|
// --- Fallback: if no r_multiple labels were found, derive metrics from knowledge_base.csv using profit as surrogate
|
|
if(total==0)
|
|
{
|
|
// Secondary fallback: parse features.csv as raw text lines and split by comma
|
|
int ht = FileOpen(m_features_path, FILE_READ|FILE_COMMON|FILE_ANSI);
|
|
if(ht!=INVALID_HANDLE)
|
|
{
|
|
int added=0; bool header_seen=false;
|
|
while(!FileIsEnding(ht))
|
|
{
|
|
string line = FileReadString(ht);
|
|
line = Trim(line);
|
|
if(line=="") continue;
|
|
string parts[];
|
|
ushort sep = (ushort)StringGetCharacter(",",0);
|
|
int cnt = StringSplit(line, sep, parts);
|
|
if(cnt<5) continue;
|
|
// parts: 0 ts,1 sym,2 strat,3 feat,4 val
|
|
string ts_s = StripQuotes(parts[0]);
|
|
if(!header_seen)
|
|
{
|
|
header_seen=true;
|
|
if(NormalizeToken(ts_s)=="timestamp") continue; // skip header
|
|
}
|
|
// Track (symbol|strategy) pairs from any feature rows (text fallback)
|
|
string sy0 = StripQuotes(parts[1]);
|
|
string st0 = StripQuotes(parts[2]);
|
|
if(sy0!="" && st0!="") PushUnique(all_keys, sy0+"|"+st0);
|
|
string f_s = StripQuotes(parts[3]);
|
|
string f_norm = NormalizeToken(f_s);
|
|
if(f_norm=="timeframe")
|
|
{
|
|
int tfv = (int)StringToInteger(StripQuotes(parts[4]));
|
|
string sy = StripQuotes(parts[1]); string st = StripQuotes(parts[2]);
|
|
string key = sy+"|"+st; bool updated=false;
|
|
for(int i2=0;i2<ArraySize(tf_keys);++i2){ if(tf_keys[i2]==key){ tf_vals[i2]=tfv; updated=true; break; } }
|
|
if(!updated){ int k2=ArraySize(tf_keys); ArrayResize(tf_keys,k2+1); ArrayResize(tf_vals,k2+1); tf_keys[k2]=key; tf_vals[k2]=tfv; }
|
|
}
|
|
else if(f_norm=="r_multiple")
|
|
{
|
|
int n = ArraySize(r_list);
|
|
ArrayResize(ts_list, n+1);
|
|
ArrayResize(sym_list, n+1);
|
|
ArrayResize(strat_list, n+1);
|
|
ArrayResize(r_list, n+1);
|
|
ArrayResize(tf_list, n+1);
|
|
ts_list[n] = StringToTime(ts_s);
|
|
sym_list[n] = StripQuotes(parts[1]);
|
|
strat_list[n] = StripQuotes(parts[2]);
|
|
r_list[n] = StringToDouble(StripQuotes(parts[4]));
|
|
// lookup timeframe if available
|
|
int tfv=-1; string key2 = sym_list[n]+"|"+strat_list[n];
|
|
for(int j2=0;j2<ArraySize(tf_keys);++j2){ if(tf_keys[j2]==key2){ tfv=tf_vals[j2]; break; } }
|
|
tf_list[n] = tfv;
|
|
added++;
|
|
}
|
|
}
|
|
FileClose(ht);
|
|
if(added>0)
|
|
{
|
|
total = ArraySize(r_list);
|
|
PrintFormat("InsightsBuilder: text-fallback collected %d r_multiple rows from features.csv", added);
|
|
}
|
|
}
|
|
|
|
Print("InsightsBuilder: no r_multiple rows found in features.csv; falling back to knowledge_base.csv using profit as surrogate");
|
|
// Open knowledge_base.csv with explicit comma delimiter
|
|
int hk = FileOpen(m_kb_path, FILE_READ|FILE_CSV|FILE_SHARE_READ|FILE_COMMON, (ushort)',' );
|
|
if(hk!=INVALID_HANDLE)
|
|
{
|
|
// No header expected in knowledge_base.csv
|
|
// Columns: ts,symbol,type,entry,sl,tp,close,profit,strategy
|
|
string sym_list2[]; string strat_list2[]; double r_list2[]; // use profit as R substitute
|
|
while(!FileIsEnding(hk))
|
|
{
|
|
string ts = FileReadString(hk); if(ts=="" && FileIsEnding(hk)) break;
|
|
string sym = FileReadString(hk);
|
|
string type= FileReadString(hk);
|
|
string entry=FileReadString(hk);
|
|
string sl = FileReadString(hk);
|
|
string tp = FileReadString(hk);
|
|
string close=FileReadString(hk);
|
|
string profit=FileReadString(hk);
|
|
string strat = FileReadString(hk);
|
|
|
|
// Trim fields
|
|
ts=Trim(ts); sym=Trim(sym); type=Trim(type); entry=Trim(entry); sl=Trim(sl); tp=Trim(tp); close=Trim(close); profit=Trim(profit); strat=Trim(strat);
|
|
if(profit=="") continue;
|
|
// consider only rows that look like closures (close price present OR profit non-zero)
|
|
double pr = StringToDouble(profit);
|
|
double cp = StringToDouble(close);
|
|
if(cp>0.0 || pr!=0.0)
|
|
{
|
|
int n = ArraySize(r_list2);
|
|
ArrayResize(r_list2, n+1);
|
|
ArrayResize(sym_list2, n+1);
|
|
ArrayResize(strat_list2, n+1);
|
|
r_list2[n] = pr;
|
|
sym_list2[n] = sym;
|
|
strat_list2[n] = strat;
|
|
}
|
|
}
|
|
FileClose(hk);
|
|
// replace primary arrays for aggregation below
|
|
ArraySwap(r_list, r_list2);
|
|
ArraySwap(strat_list, strat_list2);
|
|
ArraySwap(sym_list, sym_list2);
|
|
total = ArraySize(r_list);
|
|
PrintFormat("InsightsBuilder: fallback collected %d rows from knowledge_base.csv", total);
|
|
// Initialize tf_list for KB-fallback rows using timeframe map from features
|
|
ArrayResize(tf_list, total);
|
|
for(int i3=0;i3<total;++i3)
|
|
{
|
|
tf_list[i3] = -1;
|
|
string key3 = sym_list[i3] + "|" + strat_list[i3];
|
|
for(int j3=0;j3<ArraySize(tf_keys);++j3)
|
|
{
|
|
if(tf_keys[j3]==key3){ tf_list[i3]=tf_vals[j3]; break; }
|
|
}
|
|
}
|
|
// If still no rows after KB fallback, synthesize dummy rows from timeframe features
|
|
if(total==0)
|
|
{
|
|
int combos = ArraySize(tf_keys);
|
|
for(int c=0;c<combos;++c)
|
|
{
|
|
int sep = StringFind(tf_keys[c], "|", 0);
|
|
if(sep<0) continue;
|
|
int n2 = ArraySize(r_list);
|
|
ArrayResize(r_list, n2+1);
|
|
ArrayResize(strat_list, n2+1);
|
|
ArrayResize(sym_list, n2+1);
|
|
ArrayResize(tf_list, n2+1);
|
|
sym_list[n2] = StringSubstr(tf_keys[c], 0, sep);
|
|
strat_list[n2] = StringSubstr(tf_keys[c], sep+1);
|
|
r_list[n2] = 0.0; // scaffold count only
|
|
tf_list[n2] = tf_vals[c];
|
|
}
|
|
total = ArraySize(r_list);
|
|
if(total>0)
|
|
PrintFormat("InsightsBuilder: synthesized %d dummy rows from timeframe features (no r_multiple/profit found)", total);
|
|
}
|
|
}
|
|
}
|
|
// Final synthesis: if still no rows and no timeframe entries, synthesize from symbol/strategy pairs using default chart timeframe
|
|
if(total==0 && ArraySize(all_keys)>0)
|
|
{
|
|
int tf_def = (int)Period();
|
|
for(int c=0; c<ArraySize(all_keys); ++c)
|
|
{
|
|
int sep = StringFind(all_keys[c], "|", 0);
|
|
if(sep<0) continue;
|
|
int n2 = ArraySize(r_list);
|
|
ArrayResize(r_list, n2+1);
|
|
ArrayResize(strat_list, n2+1);
|
|
ArrayResize(sym_list, n2+1);
|
|
ArrayResize(tf_list, n2+1);
|
|
sym_list[n2] = StringSubstr(all_keys[c], 0, sep);
|
|
strat_list[n2] = StringSubstr(all_keys[c], sep+1);
|
|
r_list[n2] = 0.0; // scaffold count only
|
|
tf_list[n2] = tf_def;
|
|
}
|
|
total = ArraySize(r_list);
|
|
if(total>0)
|
|
PrintFormat("InsightsBuilder: synthesized %d dummy rows from symbol/strategy pairs (default timeframe=%d)", total, tf_def);
|
|
}
|
|
// Ensure general coverage on current chart symbol across multiple timeframes for all default strategies
|
|
{
|
|
string def_strats[]; GetDefaultStrategyNames(def_strats);
|
|
string csym = Symbol();
|
|
int ctf = (int)Period();
|
|
// Small TF set including current
|
|
int tf_opts[]; ArrayResize(tf_opts, 5);
|
|
tf_opts[0]=ctf; tf_opts[1]=PERIOD_M5; tf_opts[2]=PERIOD_M10; tf_opts[3]=PERIOD_M15; tf_opts[4]=PERIOD_M30;
|
|
int added_before = ArraySize(r_list);
|
|
for(int t=0; t<ArraySize(tf_opts); ++t)
|
|
{
|
|
int tfc = tf_opts[t];
|
|
for(int ds=0; ds<ArraySize(def_strats); ++ds)
|
|
{
|
|
bool exists=false;
|
|
for(int i=0;i<total;++i)
|
|
{
|
|
if(sym_list[i]==csym && strat_list[i]==def_strats[ds] && tf_list[i]==tfc)
|
|
{ exists=true; break; }
|
|
}
|
|
if(!exists)
|
|
{
|
|
int n3 = ArraySize(r_list);
|
|
ArrayResize(r_list, n3+1);
|
|
ArrayResize(strat_list, n3+1);
|
|
ArrayResize(sym_list, n3+1);
|
|
ArrayResize(tf_list, n3+1);
|
|
sym_list[n3] = csym;
|
|
strat_list[n3] = def_strats[ds];
|
|
r_list[n3] = 0.0;
|
|
tf_list[n3] = tfc;
|
|
}
|
|
}
|
|
}
|
|
int added_after = ArraySize(r_list);
|
|
if(added_after>added_before)
|
|
PrintFormat("InsightsBuilder: ensured multi-TF chart coverage by adding %d rows for symbol=%s across %d TFs and %d strategies", added_after-added_before, csym, ArraySize(tf_opts), ArraySize(def_strats));
|
|
}
|
|
// Unique strategies and symbols
|
|
string uniq_strats[]; string uniq_syms[];
|
|
for(int i=0;i<total;++i){ PushUnique(uniq_strats, strat_list[i]); PushUnique(uniq_syms, sym_list[i]); }
|
|
|
|
// Overall aggregates
|
|
double wins=0.0, sumR=0.0, gp=0.0, gl=0.0;
|
|
double r_all[];
|
|
ArrayResize(r_all, total);
|
|
for(int i=0;i<total;++i)
|
|
{
|
|
double r = r_list[i];
|
|
r_all[i]=r;
|
|
sumR += r;
|
|
if(r>0){ wins+=1.0; gp+=r; }
|
|
else if(r<0){ gl += -r; }
|
|
}
|
|
double win_rate = SafeDiv(wins, (double)total);
|
|
double avg_R = SafeDiv(sumR, (double)total);
|
|
double pf = (gl>0.0)? gp/gl : (gp>0.0? 9999.0 : 0.0);
|
|
double median_R = Median(r_all);
|
|
double maxdd_R = MaxDrawdownR(r_all);
|
|
|
|
// Prepare output
|
|
FolderCreate("DualEA", FILE_COMMON);
|
|
int out = FileOpen(m_out_path, FILE_WRITE|FILE_COMMON|FILE_TXT|FILE_ANSI);
|
|
if(out==INVALID_HANDLE)
|
|
{
|
|
PrintFormat("InsightsBuilder: cannot open output '%s'. Err=%d", m_out_path, GetLastError());
|
|
return false;
|
|
}
|
|
string now = TimeToString(TimeCurrent(), TIME_DATE|TIME_MINUTES|TIME_SECONDS);
|
|
FileWriteString(out, "{\n");
|
|
FileWriteString(out, " \"schema_version\": \"1.0\",\n");
|
|
FileWriteString(out, " \"generated_at\": \"" + now + "\",\n");
|
|
FileWriteString(out, " \"totals\": {\n");
|
|
FileWriteString(out, " \"trade_count\": " + IntegerToString(total) + ",\n");
|
|
FileWriteString(out, " \"win_rate\": " + DoubleToString(win_rate, 6) + ",\n");
|
|
FileWriteString(out, " \"avg_R\": " + DoubleToString(avg_R, 6) + ",\n");
|
|
FileWriteString(out, " \"median_R\": " + DoubleToString(median_R, 6) + ",\n");
|
|
FileWriteString(out, " \"profit_factor\": " + DoubleToString(pf, 6) + ",\n");
|
|
FileWriteString(out, " \"expectancy\": " + DoubleToString(avg_R, 6) + ",\n");
|
|
FileWriteString(out, " \"max_drawdown_R\": " + DoubleToString(maxdd_R, 6) + "\n");
|
|
|
|
FileWriteString(out, " },\n");
|
|
|
|
// by_strategy breakdown
|
|
FileWriteString(out, " \"by_strategy\": [\n");
|
|
for(int s=0; s<ArraySize(uniq_strats); ++s)
|
|
{
|
|
string sname = uniq_strats[s];
|
|
// collect r for this strategy in file order
|
|
double rs[]; int cnt=0; double s_sum=0.0, s_wins=0.0, s_gp=0.0, s_gl=0.0;
|
|
for(int i=0;i<total;++i)
|
|
if(strat_list[i]==sname)
|
|
{
|
|
int k = ArraySize(rs); ArrayResize(rs, k+1); rs[k]=r_list[i];
|
|
double rr = r_list[i]; s_sum+=rr; if(rr>0){s_wins+=1.0; s_gp+=rr;} else if(rr<0){s_gl+=-rr;}
|
|
cnt++;
|
|
}
|
|
double s_wr = SafeDiv(s_wins, (double)cnt);
|
|
double s_avg= SafeDiv(s_sum, (double)cnt);
|
|
double s_pf = (s_gl>0.0)? s_gp/s_gl : (s_gp>0.0? 9999.0 : 0.0);
|
|
double s_med= Median(rs);
|
|
double s_dd = MaxDrawdownR(rs);
|
|
string comma = (s<ArraySize(uniq_strats)-1)?",":"";
|
|
FileWriteString(out, StringFormat(" {\"strategy\": \"%s\", \"trade_count\": %d, \"win_rate\": %.6f, \"avg_R\": %.6f, \"median_R\": %.6f, \"profit_factor\": %.6f, \"expectancy\": %.6f, \"max_drawdown_R\": %.6f}%s\n",
|
|
sname, cnt, s_wr, s_avg, s_med, s_pf, s_avg, s_dd, comma));
|
|
}
|
|
FileWriteString(out, " ],\n");
|
|
|
|
// by_timeframe breakdown (if available)
|
|
// collect unique timeframes present in tf_list (>=0)
|
|
int uniq_tf[]; for(int i=0;i<total;++i){ if(tf_list[i]>=0){ bool seen=false; for(int j=0;j<ArraySize(uniq_tf);++j){ if(uniq_tf[j]==tf_list[i]){seen=true;break;} } if(!seen){ int k=ArraySize(uniq_tf); ArrayResize(uniq_tf,k+1); uniq_tf[k]=tf_list[i]; } } }
|
|
FileWriteString(out, " \"by_timeframe\": [\n");
|
|
for(int t=0;t<ArraySize(uniq_tf);++t)
|
|
{
|
|
int tfv = uniq_tf[t];
|
|
double rs[]; int cnt=0; double s_sum=0.0, s_wins=0.0, s_gp=0.0, s_gl=0.0;
|
|
for(int i=0;i<total;++i)
|
|
if(tf_list[i]==tfv)
|
|
{ int k=ArraySize(rs); ArrayResize(rs,k+1); rs[k]=r_list[i]; double rr=r_list[i]; s_sum+=rr; if(rr>0){s_wins+=1.0; s_gp+=rr;} else if(rr<0){s_gl+=-rr;} cnt++; }
|
|
double wr = (cnt>0? s_wins/cnt : 0.0);
|
|
double avg = (cnt>0? s_sum/cnt : 0.0);
|
|
double pf = (s_gl>0.0? s_gp/s_gl : (s_gp>0.0? 9999.0 : 0.0));
|
|
double med= Median(rs);
|
|
double dd = MaxDrawdownR(rs);
|
|
string comma = (t<ArraySize(uniq_tf)-1)?",":"";
|
|
FileWriteString(out, StringFormat(" {\"timeframe\": %d, \"trade_count\": %d, \"win_rate\": %.6f, \"avg_R\": %.6f, \"median_R\": %.6f, \"profit_factor\": %.6f, \"expectancy\": %.6f, \"max_drawdown_R\": %.6f}%s\n", tfv, cnt, wr, avg, med, pf, avg, dd, comma));
|
|
}
|
|
FileWriteString(out, " ],\n");
|
|
|
|
// by_symbol_strategy_timeframe breakdown
|
|
FileWriteString(out, " \"by_symbol_strategy_timeframe\": [\n");
|
|
for(int s=0; s<ArraySize(uniq_strats); ++s)
|
|
{
|
|
string sname = uniq_strats[s];
|
|
for(int y=0; y<ArraySize(uniq_syms); ++y)
|
|
{
|
|
string yname = uniq_syms[y];
|
|
// collect unique tf for this (s,y)
|
|
int tf_set_sy[];
|
|
for(int i=0;i<total;++i) if(strat_list[i]==sname && sym_list[i]==yname && tf_list[i]>=0)
|
|
{ bool seen=false; for(int j=0;j<ArraySize(tf_set_sy);++j){ if(tf_set_sy[j]==tf_list[i]){seen=true;break;} } if(!seen){ int k=ArraySize(tf_set_sy); ArrayResize(tf_set_sy,k+1); tf_set_sy[k]=tf_list[i]; } }
|
|
for(int t=0;t<ArraySize(tf_set_sy);++t)
|
|
{
|
|
int tfv = tf_set_sy[t];
|
|
double rs[]; int cnt=0; double s_sum=0.0, s_wins=0.0, s_gp=0.0, s_gl=0.0;
|
|
for(int i=0;i<total;++i)
|
|
if(strat_list[i]==sname && sym_list[i]==yname && tf_list[i]==tfv)
|
|
{ int k=ArraySize(rs); ArrayResize(rs,k+1); rs[k]=r_list[i]; double rr=r_list[i]; s_sum+=rr; if(rr>0){s_wins+=1.0; s_gp+=rr;} else if(rr<0){s_gl+=-rr;} cnt++; }
|
|
double wr = (cnt>0? s_wins/cnt : 0.0);
|
|
double avg = (cnt>0? s_sum/cnt : 0.0);
|
|
double pf = (s_gl>0.0? s_gp/s_gl : (s_gp>0.0? 9999.0 : 0.0));
|
|
double med= Median(rs);
|
|
double dd = MaxDrawdownR(rs);
|
|
// determine trailing comma: last item overall
|
|
bool lastBlock = (s==ArraySize(uniq_strats)-1) && (y==ArraySize(uniq_syms)-1) && (t==ArraySize(tf_set_sy)-1);
|
|
string comma = (lastBlock?"":",");
|
|
FileWriteString(out, StringFormat(" {\"strategy\": \"%s\", \"symbol\": \"%s\", \"timeframe\": %d, \"trade_count\": %d, \"win_rate\": %.6f, \"avg_R\": %.6f, \"median_R\": %.6f, \"profit_factor\": %.6f, \"expectancy\": %.6f, \"max_drawdown_R\": %.6f}%s\n",
|
|
sname, yname, tfv, cnt, wr, avg, med, pf, avg, dd, comma));
|
|
}
|
|
}
|
|
}
|
|
FileWriteString(out, " ],\n");
|
|
|
|
// by_symbol breakdown
|
|
FileWriteString(out, " \"by_symbol\": [\n");
|
|
for(int y=0; y<ArraySize(uniq_syms); ++y)
|
|
{
|
|
string yname = uniq_syms[y];
|
|
double rs2[]; int cnt2=0; double s_sum2=0.0, s_wins2=0.0, s_gp2=0.0, s_gl2=0.0;
|
|
for(int i=0;i<total;++i)
|
|
if(sym_list[i]==yname)
|
|
{
|
|
int k = ArraySize(rs2); ArrayResize(rs2, k+1); rs2[k]=r_list[i];
|
|
double rr = r_list[i]; s_sum2+=rr; if(rr>0){s_wins2+=1.0; s_gp2+=rr;} else if(rr<0){s_gl2+=-rr;}
|
|
cnt2++;
|
|
}
|
|
double y_wr = SafeDiv(s_wins2, (double)cnt2);
|
|
double y_avg= SafeDiv(s_sum2, (double)cnt2);
|
|
double y_pf = (s_gl2>0.0)? s_gp2/s_gl2 : (s_gp2>0.0? 9999.0 : 0.0);
|
|
double y_med= Median(rs2);
|
|
double y_dd = MaxDrawdownR(rs2);
|
|
string comma2 = (y<ArraySize(uniq_syms)-1)?",":"";
|
|
FileWriteString(out, StringFormat(" {\"symbol\": \"%s\", \"trade_count\": %d, \"win_rate\": %.6f, \"avg_R\": %.6f, \"median_R\": %.6f, \"profit_factor\": %.6f, \"expectancy\": %.6f, \"max_drawdown_R\": %.6f}%s\n",
|
|
yname, cnt2, y_wr, y_avg, y_med, y_pf, y_avg, y_dd, comma2));
|
|
}
|
|
FileWriteString(out, " ]\n");
|
|
|
|
FileWriteString(out, "}\n");
|
|
FileClose(out);
|
|
string full = TerminalInfoString(TERMINAL_COMMONDATA_PATH) + "\\Files\\" + m_out_path;
|
|
PrintFormat("InsightsBuilder: wrote %s", full);
|
|
return true;
|
|
}
|
|
};
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Constructor |
|
|
//+------------------------------------------------------------------+
|
|
CKnowledgeBase::CKnowledgeBase(string file_name="DualEA\\knowledge_base.csv", string delimiter=",")
|
|
{
|
|
m_csv_delimiter = delimiter;
|
|
m_file_path = file_name; // Use subfolder under Common Files: DualEA\
|
|
m_lock_handle = INVALID_HANDLE;
|
|
// Derive a lock file path next to the KB file (e.g., DualEA\\knowledge_base.lock)
|
|
string lp = m_file_path;
|
|
int dot_lp = StringFind(lp, ".", 0);
|
|
if(dot_lp > 0)
|
|
m_lock_path = StringSubstr(lp, 0, dot_lp) + ".lock";
|
|
else
|
|
m_lock_path = lp + ".lock";
|
|
|
|
// Ensure target subfolder exists in Common files
|
|
// Common files base: TerminalInfoString(TERMINAL_COMMONDATA_PATH) + "\\Files"
|
|
// Create "DualEA" if missing
|
|
FolderCreate("DualEA", FILE_COMMON);
|
|
|
|
// Debug: show resolved common file path
|
|
string full_main_path = TerminalInfoString(TERMINAL_COMMONDATA_PATH) + "\\Files\\" + m_file_path;
|
|
PrintFormat("KnowledgeBase: writing main CSV to Common Files: %s", full_main_path);
|
|
|
|
// --- Ensure the header exists (use direct FileOpen to avoid noisy error on first run)
|
|
int h = FileOpen(m_file_path, FILE_READ|FILE_CSV|FILE_COMMON);
|
|
if(h == INVALID_HANDLE)
|
|
{
|
|
// Create new file with header
|
|
h = FileOpen(m_file_path, FILE_WRITE|FILE_CSV|FILE_COMMON);
|
|
if(h != INVALID_HANDLE)
|
|
{
|
|
FileWriteString(h, "timestamp,symbol,type,entry_price,stop_loss,take_profit,close_price,profit,strategy_id\n");
|
|
FileClose(h);
|
|
}
|
|
}
|
|
else
|
|
{
|
|
// File exists; if empty, write header
|
|
if(FileSize(h) == 0)
|
|
{
|
|
FileClose(h);
|
|
h = FileOpen(m_file_path, FILE_WRITE|FILE_CSV|FILE_COMMON);
|
|
if(h != INVALID_HANDLE)
|
|
{
|
|
FileWriteString(h, "timestamp,symbol,type,entry_price,stop_loss,take_profit,close_price,profit,strategy_id\n");
|
|
}
|
|
}
|
|
if(h != INVALID_HANDLE)
|
|
FileClose(h);
|
|
}
|
|
}
|
|
//+------------------------------------------------------------------+
|
|
//| Destructor |
|
|
//+------------------------------------------------------------------+
|
|
CKnowledgeBase::~CKnowledgeBase()
|
|
{
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Writes a trade record to the CSV file |
|
|
//+------------------------------------------------------------------+
|
|
bool CKnowledgeBase::WriteRecord(const TradeRecord &record)
|
|
{
|
|
// Acquire cross-terminal lock to serialize writers
|
|
if(!AcquireLock(3000))
|
|
return(false);
|
|
|
|
// Use READ|WRITE to avoid truncation; FILE_WRITE alone can clear the file
|
|
if(!OpenFile(FILE_READ|FILE_WRITE|FILE_CSV))
|
|
{
|
|
ReleaseLock();
|
|
return(false);
|
|
}
|
|
|
|
FileSeek(m_file_handle, 0, SEEK_END);
|
|
|
|
string record_string = TimeToString(record.timestamp) + m_csv_delimiter +
|
|
record.symbol + m_csv_delimiter +
|
|
IntegerToString(record.type) + m_csv_delimiter +
|
|
DoubleToString(record.entry_price, _Digits) + m_csv_delimiter +
|
|
DoubleToString(record.stop_loss, _Digits) + m_csv_delimiter +
|
|
DoubleToString(record.take_profit, _Digits) + m_csv_delimiter +
|
|
DoubleToString(record.close_price, _Digits) + m_csv_delimiter +
|
|
DoubleToString(record.profit, 2) + m_csv_delimiter +
|
|
record.strategy_id;
|
|
|
|
FileWriteString(m_file_handle, record_string + "\n");
|
|
|
|
CloseFile();
|
|
ReleaseLock();
|
|
return(true);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Logs a trade event with basic MT5 result info |
|
|
//+------------------------------------------------------------------+
|
|
bool CKnowledgeBase::LogTrade(const string strategy_name, const int retcode, const ulong deal, const ulong order_id)
|
|
{
|
|
// Derive an events file from the main path, e.g., knowledge_base_events.csv
|
|
string events_path = m_file_path;
|
|
int dot = StringFind(events_path, ".", 0);
|
|
if(dot > 0)
|
|
events_path = StringSubstr(events_path, 0, dot) + "_events.csv";
|
|
else
|
|
events_path = events_path + "_events.csv";
|
|
|
|
// Ensure folder exists in Common files
|
|
FolderCreate("DualEA", FILE_COMMON);
|
|
|
|
// Debug: show resolved events file path
|
|
string full_events_path = TerminalInfoString(TERMINAL_COMMONDATA_PATH) + "\\Files\\" + events_path;
|
|
PrintFormat("KnowledgeBase: writing events CSV to Common Files: %s", full_events_path);
|
|
|
|
int handle = FileOpen(events_path, FILE_READ|FILE_WRITE|FILE_CSV|FILE_SHARE_WRITE|FILE_COMMON);
|
|
if(handle == INVALID_HANDLE)
|
|
{
|
|
PrintFormat("Error opening knowledge base events file '%s'. Error code: %d", events_path, GetLastError());
|
|
return(false);
|
|
}
|
|
|
|
// Write header if file is empty
|
|
if(FileSize(handle) == 0)
|
|
{
|
|
FileWriteString(handle, "timestamp,strategy,retcode,deal,order\n");
|
|
}
|
|
|
|
FileSeek(handle, 0, SEEK_END);
|
|
string line = TimeToString(TimeCurrent()) + m_csv_delimiter +
|
|
strategy_name + m_csv_delimiter +
|
|
IntegerToString(retcode) + m_csv_delimiter +
|
|
IntegerToString((int)deal) + m_csv_delimiter +
|
|
IntegerToString((int)order_id);
|
|
FileWriteString(handle, line + "\n");
|
|
FileClose(handle);
|
|
return(true);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Opens the file |
|
|
//+------------------------------------------------------------------+
|
|
bool CKnowledgeBase::OpenFile(int flags=FILE_WRITE|FILE_READ|FILE_CSV)
|
|
{
|
|
// Always target the Common files area so results are shared across Tester, Paper, and Live
|
|
// The path m_file_path should include the subfolder, e.g. "DualEA\\knowledge_base.csv"
|
|
m_file_handle = INVALID_HANDLE;
|
|
// Retry with read/write sharing to mitigate transient locks
|
|
for(int attempt=0; attempt<10 && m_file_handle==INVALID_HANDLE; ++attempt)
|
|
{
|
|
m_file_handle = FileOpen(m_file_path, (flags | FILE_SHARE_READ | FILE_SHARE_WRITE | FILE_COMMON));
|
|
if(m_file_handle==INVALID_HANDLE)
|
|
Sleep(25);
|
|
}
|
|
if(m_file_handle == INVALID_HANDLE)
|
|
{
|
|
PrintFormat("Error opening knowledge base file '%s'. Error code: %d", m_file_path, GetLastError());
|
|
return(false);
|
|
}
|
|
return(true);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Closes the file |
|
|
//+------------------------------------------------------------------+
|
|
void CKnowledgeBase::CloseFile()
|
|
{
|
|
if(m_file_handle != INVALID_HANDLE)
|
|
FileClose(m_file_handle);
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Acquire exclusive lock via a temporary lock file |
|
|
//+------------------------------------------------------------------+
|
|
bool CKnowledgeBase::AcquireLock(const int timeout_ms)
|
|
{
|
|
m_lock_handle = INVALID_HANDLE;
|
|
int waited = 0;
|
|
// Do not specify share flags to request exclusive access on the lock file
|
|
while(waited < timeout_ms && m_lock_handle==INVALID_HANDLE)
|
|
{
|
|
m_lock_handle = FileOpen(m_lock_path, FILE_WRITE|FILE_COMMON);
|
|
if(m_lock_handle==INVALID_HANDLE)
|
|
{
|
|
Sleep(25);
|
|
waited += 25;
|
|
}
|
|
}
|
|
if(m_lock_handle==INVALID_HANDLE)
|
|
{
|
|
PrintFormat("KnowledgeBase: lock acquire timeout for %s (err=%d)", m_lock_path, GetLastError());
|
|
return false;
|
|
}
|
|
// Optional: write small token so file exists for observers
|
|
FileWriteString(m_lock_handle, "lock\n");
|
|
return true;
|
|
}
|
|
|
|
//+------------------------------------------------------------------+
|
|
//| Release the lock |
|
|
//+------------------------------------------------------------------+
|
|
void CKnowledgeBase::ReleaseLock()
|
|
{
|
|
if(m_lock_handle!=INVALID_HANDLE)
|
|
{
|
|
FileClose(m_lock_handle);
|
|
m_lock_handle = INVALID_HANDLE;
|
|
// Best-effort delete to avoid stale locks
|
|
FileDelete(m_lock_path, FILE_COMMON);
|
|
}
|
|
}
|