// 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 // Centralized default strategy names #include // --- 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='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;ipeak) 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;i0) { 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;i30) 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; c0) 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; tadded_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;i0){ 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; s0){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=0) int uniq_tf[]; for(int i=0;i=0){ bool seen=false; for(int j=0;j0){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=0) { bool seen=false; for(int j=0;j0){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; y0){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 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); } }