//+------------------------------------------------------------------+ //| News Database.mqh | //| Copyright 2026, Allan Munene Mutiiria. | //| https://t.me/Forex_Algo_Trader | //+------------------------------------------------------------------+ #property copyright "Copyright 2026, Allan Munene Mutiiria." #property link "https://t.me/Forex_Algo_Trader" //--- Include guard #ifndef NEWS_DATABASE_MQH #define NEWS_DATABASE_MQH //--- Include core data definitions and state #include "News Core.mqh" //--- Declare debugLogging extern when not compiled from main (Logic.mqh uses same guard) #ifndef NEWS_COMPILED_FROM_MAIN extern bool debugLogging; // Print debug info to journal #endif //+------------------------------------------------------------------+ //| Database State | //+------------------------------------------------------------------+ int g_news_db = INVALID_HANDLE; // SQLite database handle (-1 when not open) //--- Database stored in the COMMON terminal folder so both live chart //--- and strategy tester agents can access the same file #define NEWS_DB_PATH "Database\\NewsCalendarEA.db" // Path relative to Common/Files/ //+------------------------------------------------------------------+ //| Open or create database and ensure required tables exist | //+------------------------------------------------------------------+ bool News_DbOpen() { //--- Return early if database is already open if(g_news_db != INVALID_HANDLE) return true; //--- Open from Common/Files/ folder for shared live and tester access g_news_db = DatabaseOpen(NEWS_DB_PATH, DATABASE_OPEN_READWRITE | DATABASE_OPEN_CREATE | DATABASE_OPEN_COMMON); if(g_news_db == INVALID_HANDLE) { Print("News_DbOpen: failed to open '", NEWS_DB_PATH, "' error=", GetLastError()); return false; } //--- Create events table with all calendar value columns if(!DatabaseExecute(g_news_db, "CREATE TABLE IF NOT EXISTS events (" " value_id INTEGER PRIMARY KEY," " event_id INTEGER NOT NULL," " event_time INTEGER NOT NULL," " currency TEXT NOT NULL," " event_name TEXT NOT NULL," " importance TEXT NOT NULL," " has_actual INTEGER NOT NULL DEFAULT 0," " actual REAL NOT NULL DEFAULT 0," " has_forecast INTEGER NOT NULL DEFAULT 0," " forecast REAL NOT NULL DEFAULT 0," " has_previous INTEGER NOT NULL DEFAULT 0," " previous REAL NOT NULL DEFAULT 0," " has_revised INTEGER NOT NULL DEFAULT 0," " revised_prev REAL NOT NULL DEFAULT 0," " unit INTEGER NOT NULL DEFAULT 0," " multiplier INTEGER NOT NULL DEFAULT 0," " digits INTEGER NOT NULL DEFAULT 0," " updated_at INTEGER NOT NULL DEFAULT 0" ");")) { Print("News_DbOpen: create events table failed: ", GetLastError()); DatabaseClose(g_news_db); g_news_db = INVALID_HANDLE; return false; } //--- Create index on event_time for fast time-window queries DatabaseExecute(g_news_db, "CREATE INDEX IF NOT EXISTS idx_events_time ON events(event_time);"); //--- Create triggered table for trade deduplication across restarts if(!DatabaseExecute(g_news_db, "CREATE TABLE IF NOT EXISTS triggered (" " event_id INTEGER PRIMARY KEY," " triggered_at INTEGER NOT NULL" ");")) { Print("News_DbOpen: create triggered table failed: ", GetLastError()); DatabaseClose(g_news_db); g_news_db = INVALID_HANDLE; return false; } if(debugLogging) Print("News_DbOpen: ready at '", NEWS_DB_PATH, "'"); return true; } //+------------------------------------------------------------------+ //| Close the database cleanly | //+------------------------------------------------------------------+ void News_DbClose() { if(g_news_db == INVALID_HANDLE) { Print(">>> News DB: Close requested but database was not open"); return; } DatabaseClose(g_news_db); g_news_db = INVALID_HANDLE; Print(">>> News DB: Database closed cleanly"); } //+------------------------------------------------------------------+ //| Upsert a single event row into the events table | //+------------------------------------------------------------------+ bool News_DbUpsertEvent(long valueId, const NewsEvent &ev, datetime updatedAt) { //--- Abort if database is not open if(g_news_db == INVALID_HANDLE) return false; //--- Prepare INSERT OR REPLACE statement with all event columns int stmt = DatabasePrepare(g_news_db, "INSERT OR REPLACE INTO events (" " value_id,event_id,event_time,currency,event_name,importance," " has_actual,actual,has_forecast,forecast," " has_previous,previous,has_revised,revised_prev," " unit,multiplier,digits,updated_at" ") VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?);"); if(stmt == INVALID_HANDLE) return false; //--- Bind all parameter values to statement slots DatabaseBind(stmt, 0, valueId); DatabaseBind(stmt, 1, ev.eventId); DatabaseBind(stmt, 2, (long)ev.eventDateTime); DatabaseBind(stmt, 3, ev.currency); DatabaseBind(stmt, 4, ev.event); DatabaseBind(stmt, 5, ev.importance); DatabaseBind(stmt, 6, (int)ev.hasActual); DatabaseBind(stmt, 7, ev.actual); DatabaseBind(stmt, 8, (int)ev.hasForecast); DatabaseBind(stmt, 9, ev.forecast); DatabaseBind(stmt, 10, (int)ev.hasPrevious); DatabaseBind(stmt, 11, ev.previous); DatabaseBind(stmt, 12, (int)ev.hasRevised); DatabaseBind(stmt, 13, ev.revisedPrevious); DatabaseBind(stmt, 14, ev.unit); DatabaseBind(stmt, 15, ev.multiplier); DatabaseBind(stmt, 16, ev.digits); DatabaseBind(stmt, 17, (long)updatedAt); //--- Execute and finalize the prepared statement const bool ok = DatabaseRead(stmt); DatabaseFinalize(stmt); return ok; } //+------------------------------------------------------------------+ //| Load events for a time window from DB into g_news_allEvents[] | //+------------------------------------------------------------------+ int News_DbLoadEventsForWindow(datetime startDt, datetime endDt) { //--- Abort if database is not open if(g_news_db == INVALID_HANDLE) return 0; //--- Prepare time-window query ordered ascending by event time int stmt = DatabasePrepare(g_news_db, "SELECT event_id,event_time,currency,event_name,importance," " has_actual,actual,has_forecast,forecast," " has_previous,previous,has_revised,revised_prev," " unit,multiplier,digits" " FROM events" " WHERE event_time >= ? AND event_time <= ?" " ORDER BY event_time ASC;"); if(stmt == INVALID_HANDLE) return 0; //--- Bind time window bounds DatabaseBind(stmt, 0, (long)startDt); DatabaseBind(stmt, 1, (long)endDt); //--- Reset event array and read each row into it ArrayResize(g_news_allEvents, 0); int idx = 0; while(DatabaseRead(stmt)) { //--- Declare temporary locals; MQL5 forbids struct refs from arrays NewsEvent e; long evId = 0, evTime = 0; int hasA = 0, hasF = 0, hasP = 0, hasR = 0; int unitV = 0, multV = 0, digV = 0; string cur = "", evName = "", imp = ""; double act = 0, fcast = 0, prev = 0, revPrev = 0; //--- Read each column from the current row DatabaseColumnLong (stmt, 0, evId); DatabaseColumnLong (stmt, 1, evTime); DatabaseColumnText (stmt, 2, cur); DatabaseColumnText (stmt, 3, evName); DatabaseColumnText (stmt, 4, imp); DatabaseColumnInteger(stmt, 5, hasA); DatabaseColumnDouble (stmt, 6, act); DatabaseColumnInteger(stmt, 7, hasF); DatabaseColumnDouble (stmt, 8, fcast); DatabaseColumnInteger(stmt, 9, hasP); DatabaseColumnDouble (stmt, 10, prev); DatabaseColumnInteger(stmt, 11, hasR); DatabaseColumnDouble (stmt, 12, revPrev); DatabaseColumnInteger(stmt, 13, unitV); DatabaseColumnInteger(stmt, 14, multV); DatabaseColumnInteger(stmt, 15, digV); //--- Populate event struct from column values e.eventId = evId; e.eventDateTime = (datetime)evTime; e.eventDate = TimeToString((datetime)evTime, TIME_DATE); e.eventTime = TimeToString((datetime)evTime, TIME_MINUTES); e.currency = cur; e.event = evName; e.importance = imp; e.hasActual = (hasA != 0); e.actual = act; e.hasForecast = (hasF != 0); e.forecast = fcast; e.hasPrevious = (hasP != 0); e.previous = prev; e.hasRevised = (hasR != 0); e.revisedPrevious = revPrev; e.unit = unitV; e.multiplier = multV; e.digits = digV; //--- Append populated struct to the event array ArrayResize(g_news_allEvents, idx + 1); g_news_allEvents[idx] = e; idx++; } DatabaseFinalize(stmt); if(debugLogging) Print("News_DbLoadEventsForWindow: loaded ", idx, " events"); return idx; } //+------------------------------------------------------------------+ //| Prune events older than keepDays to prevent database bloat | //+------------------------------------------------------------------+ void News_DbPruneOldEvents(int keepDays = 7) { //--- Abort if database is not open if(g_news_db == INVALID_HANDLE) return; //--- Compute cutoff timestamp and delete rows older than it const long cutoff = (long)TimeCurrent() - (long)keepDays * 86400; int stmt = DatabasePrepare(g_news_db, "DELETE FROM events WHERE event_time < ?;"); if(stmt == INVALID_HANDLE) return; DatabaseBind(stmt, 0, cutoff); DatabaseRead(stmt); DatabaseFinalize(stmt); } //+------------------------------------------------------------------+ //| Persist a triggered event ID to DB for restart safety | //+------------------------------------------------------------------+ bool News_DbMarkTriggered(long evId) { //--- Abort if database is not open if(g_news_db == INVALID_HANDLE) return false; //--- Insert event ID with current timestamp; ignore duplicate int stmt = DatabasePrepare(g_news_db, "INSERT OR IGNORE INTO triggered (event_id,triggered_at) VALUES (?,?);"); if(stmt == INVALID_HANDLE) return false; DatabaseBind(stmt, 0, evId); DatabaseBind(stmt, 1, (long)TimeCurrent()); const bool ok = DatabaseRead(stmt); DatabaseFinalize(stmt); return ok; } //+------------------------------------------------------------------+ //| Load triggered event IDs from last 24h into g_news_triggeredIds[]| //+------------------------------------------------------------------+ void News_DbLoadTriggered() { //--- Abort if database is not open if(g_news_db == INVALID_HANDLE) return; //--- Query triggered IDs from the past 24 hours const long cutoff = (long)TimeCurrent() - 86400; int stmt = DatabasePrepare(g_news_db, "SELECT event_id FROM triggered WHERE triggered_at >= ?;"); if(stmt == INVALID_HANDLE) return; DatabaseBind(stmt, 0, cutoff); //--- Populate g_news_triggeredIds array from query results ArrayResize(g_news_triggeredIds, 0); int n = 0; while(DatabaseRead(stmt)) { long id = 0; DatabaseColumnLong(stmt, 0, id); ArrayResize(g_news_triggeredIds, n + 1); g_news_triggeredIds[n++] = id; } DatabaseFinalize(stmt); if(n > 0 && debugLogging) Print("News_DbLoadTriggered: restored ", n, " triggered IDs"); } //+------------------------------------------------------------------+ //| Check if event ID was already triggered (memory array fast path) | //+------------------------------------------------------------------+ bool News_DbAlreadyTriggered(long evId) { //--- Linear scan of the in-memory triggered IDs array const int n = ArraySize(g_news_triggeredIds); for(int i = 0; i < n; i++) if(g_news_triggeredIds[i] == evId) return true; return false; } //+------------------------------------------------------------------+ //| Check if database already has events for a given time window | //+------------------------------------------------------------------+ bool News_DbHasDataForWindow(datetime startDt, datetime endDt) { //--- Abort if database is not open if(g_news_db == INVALID_HANDLE) return false; //--- Count rows in the requested window int stmt = DatabasePrepare(g_news_db, "SELECT COUNT(*) FROM events WHERE event_time >= ? AND event_time <= ?;"); if(stmt == INVALID_HANDLE) return false; DatabaseBind(stmt, 0, (long)startDt); DatabaseBind(stmt, 1, (long)endDt); int cnt = 0; if(DatabaseRead(stmt)) DatabaseColumnInteger(stmt, 0, cnt); DatabaseFinalize(stmt); return (cnt > 0); } //+------------------------------------------------------------------+ //| Draw one frame of the download progress bar onto canvas | //+------------------------------------------------------------------+ void News_DbDrawProgressBar(CCanvas &canv, int pbW, int pbH, int pct, int saved, int total) { //--- Clear canvas to transparent canv.Erase(0x00000000); //--- Draw outer rounded pill background using theme colors News_FillRoundRect(canv, 0, 0, pbW, pbH, 10, ColorToARGB(g_news_bg, 255)); News_DrawRoundRectBorder(canv, 0, 0, pbW, pbH, 10, 1, ColorToARGB(g_news_borderAccent, 255)); //--- Stamp centered title text with current progress counts canv.FontSet("Arial Bold", -100); canv.TextOut(pbW / 2, 7, "Downloading News Events: " + IntegerToString(pct) + "% (" + IntegerToString(saved) + " / " + IntegerToString(total) + ")", ColorToARGB(g_news_titleText, 255), TA_CENTER | TA_TOP); //--- Draw the track capsule background below the title const int padX = 14; const int trackL = padX; const int trackR = pbW - padX; const int trackW = trackR - trackL; const int trackY = pbH - 18; const int barH = 10; News_FillRoundRect(canv, trackL, trackY, trackW, barH, barH / 2, ColorToARGB(g_news_panelAlt, 255)); News_DrawRoundRectBorder(canv, trackL, trackY, trackW, barH, barH / 2, 1, ColorToARGB(g_news_border, 255)); //--- Fill the track from center outward proportional to percent complete const int fillW = (int)(trackW * pct / 100.0); const int fillL = trackL + (trackW - fillW) / 2; if(fillW > 0) News_FillRoundRect(canv, fillL, trackY, fillW, barH, barH / 2, ColorToARGB(g_news_accent, 255)); //--- Push canvas frame to chart canv.Update(); ChartRedraw(0); } //+------------------------------------------------------------------+ //| Download calendar data for date range and save to database | //+------------------------------------------------------------------+ void News_DbDownloadAndSave(datetime startDt, datetime endDt) { //--- Abort if database is not open if(g_news_db == INVALID_HANDLE) { Print("News_DbDownloadAndSave: DB not open - aborting"); return; } Print(">>> News DB: Fetching calendar events from ", TimeToString(startDt), " to ", TimeToString(endDt), " ..."); //--- Query all raw calendar values in the requested time window MqlCalendarValue values[]; const int total = CalendarValueHistory(values, startDt, endDt, NULL, NULL); if(total <= 0) { Print(">>> News DB: No events found for specified range"); return; } Print(">>> News DB: ", total, " raw calendar records found - saving to DB..."); //--- Create progress bar canvas centered at the top of the chart const int chartW = (int)ChartGetInteger(0, CHART_WIDTH_IN_PIXELS); const int PB_W = 420; const int PB_H = 46; const int PB_X = (chartW - PB_W) / 2; const int PB_Y = 20; const string PB_NAME = "NewsDB_ProgressCanvas"; CCanvas pbCanv; if(pbCanv.CreateBitmapLabel(PB_NAME, PB_X, PB_Y, PB_W, PB_H, COLOR_FORMAT_ARGB_NORMALIZE)) { //--- Configure progress bar canvas object properties ObjectSetInteger(0, PB_NAME, OBJPROP_SELECTABLE, false); ObjectSetInteger(0, PB_NAME, OBJPROP_HIDDEN, true); ObjectSetInteger(0, PB_NAME, OBJPROP_ZORDER, 2000); } //--- Draw initial 0% progress frame News_DbDrawProgressBar(pbCanv, PB_W, PB_H, 0, 0, total); //--- Begin transaction, iterate all values, upsert each event record DatabaseTransactionBegin(g_news_db); const datetime updatedAt = TimeCurrent(); int saved = 0; for(int i = 0; i < total; i++) { //--- Fetch associated event metadata and country info MqlCalendarEvent ev; if(!CalendarEventById(values[i].event_id, ev)) continue; MqlCalendarCountry ctry; CalendarCountryById(ev.country_id, ctry); MqlCalendarValue val; CalendarValueById(values[i].id, val); //--- Populate NewsEvent struct from calendar API data NewsEvent ne; ne.eventDate = TimeToString(values[i].time, TIME_DATE); ne.eventTime = TimeToString(values[i].time, TIME_MINUTES); ne.currency = ctry.currency; ne.event = ev.name; ne.importance = News_GetImpactLabel(ev.importance); ne.hasActual = val.HasActualValue(); ne.hasForecast = val.HasForecastValue(); ne.hasPrevious = val.HasPreviousValue(); ne.hasRevised = val.HasRevisedValue(); ne.actual = val.HasActualValue() ? val.GetActualValue() : 0.0; ne.forecast = val.HasForecastValue() ? val.GetForecastValue() : 0.0; ne.previous = val.HasPreviousValue() ? val.GetPreviousValue() : 0.0; ne.revisedPrevious = val.HasRevisedValue() ? val.GetRevisedValue() : 0.0; ne.unit = (int)ev.unit; ne.multiplier = (int)ev.multiplier; ne.digits = (int)ev.digits; ne.eventDateTime = values[i].time; ne.eventId = (long)values[i].event_id; //--- Upsert the event into the database News_DbUpsertEvent((long)values[i].id, ne, updatedAt); saved++; //--- Refresh progress bar every 5 records to reduce redraw overhead if(i % 5 == 0 || i == total - 1) { const int pct = (int)((i + 1) * 100.0 / total); News_DbDrawProgressBar(pbCanv, PB_W, PB_H, pct, saved, total); } } //--- Commit all upserts as a single transaction DatabaseTransactionCommit(g_news_db); //--- Hold at 100% briefly so the user sees completion before canvas disappears News_DbDrawProgressBar(pbCanv, PB_W, PB_H, 100, saved, total); Sleep(800); //--- Destroy progress bar canvas and clean up the chart object pbCanv.Destroy(); ObjectDelete(0, PB_NAME); ChartRedraw(0); Print(">>> News DB: Download complete. ", saved, " events saved to ", NEWS_DB_PATH, " for range ", TimeToString(startDt), " to ", TimeToString(endDt), ". Use it for offline testing..."); //--- Open the DB file to log its current size on disk ResetLastError(); int fh = FileOpen(NEWS_DB_PATH, FILE_READ | FILE_BIN | FILE_COMMON | FILE_SHARE_READ | FILE_SHARE_WRITE); if(fh == INVALID_HANDLE) { Print(">>> News DB: Could not open DB file to read size (error ", GetLastError(), ")"); } else { //--- Format byte count into KB or MB for readability const ulong dbBytes = FileSize(fh); FileClose(fh); string sizeStr; if(dbBytes >= 1048576) sizeStr = DoubleToString((double)dbBytes / 1048576.0, 2) + " MB"; else if(dbBytes >= 1024) sizeStr = DoubleToString((double)dbBytes / 1024.0, 1) + " KB"; else sizeStr = IntegerToString((long)dbBytes) + " bytes"; Print(">>> News DB: Database file size: ", dbBytes, " bytes (", sizeStr, ")..."); } } #endif // NEWS_DATABASE_MQH