//--- Inputs (visible in Inputs tab) input string InpSymbol1 = "BTCUSD"; // Symbol 1 input string InpSymbol2 = "ETHUSD"; // Symbol 2 input int InpMaxLogs = 100; // Max logs to keep (mean/std/z-score when reached) //+------------------------------------------------------------------+ //| Expert initialization function | //+------------------------------------------------------------------+ int OnInit() { ENUM_TIMEFRAMES tf = Period(); int seconds = PeriodSeconds(_Period); EventSetTimer(seconds); Comment("meanSpreadReversion started. Chart timeframe: ", EnumToString(tf), " (log every ", seconds, " sec)"); return(INIT_SUCCEEDED); } // Parse "ask-spread" and "bid-spread" from a JSON line; return mid spread (average). Returns false if parse fails. bool ParseSpreadFromLine(const string &line, double &outAskSpread, double &outBidSpread) { int pa = StringFind(line, "\"ask-spread\":"); int pb = StringFind(line, "\"bid-spread\":"); if(pa < 0 || pb < 0) return false; // Skip "\"ask-spread\":" (13 chars) to start at the number int startA = pa + 13; int endA = StringFind(line, ",", startA); if(endA < 0) endA = StringLen(line); int startB = pb + 13; int endB = StringFind(line, "}", startB); if(endB < 0) endB = StringLen(line); outAskSpread = StringToDouble(StringSubstr(line, startA, endA - startA)); outBidSpread = StringToDouble(StringSubstr(line, startB, endB - startB)); return true; } void SavePriceToJSON(string symbol1, string symbol2) { double sbl1_bid = SymbolInfoDouble(symbol1, SYMBOL_BID); double sbl1_ask = SymbolInfoDouble(symbol1, SYMBOL_ASK); double sbl2_bid = SymbolInfoDouble(symbol2, SYMBOL_BID); double sbl2_ask = SymbolInfoDouble(symbol2, SYMBOL_ASK); double ask_spread = MathLog(sbl1_ask) - MathLog(sbl2_ask); double bid_spread = MathLog(sbl1_bid) - MathLog(sbl2_bid); double current_spread = (ask_spread + bid_spread) * 0.5; ENUM_TIMEFRAMES tf = Period(); string tfStr = EnumToString(tf); string json = "{"; json += "\"time\":\"" + TimeToString(TimeCurrent(), TIME_DATE | TIME_MINUTES) + "\","; json += "\"timeframe\":\"" + tfStr + "\","; json += "\"symbol1\":\"" + symbol1 + "\","; json += "\"symbol2\":\"" + symbol2 + "\","; json += "\"ask1\":" + DoubleToString(sbl1_ask, 5) + ","; json += "\"bid1\":" + DoubleToString(sbl1_bid, 5) + ","; json += "\"ask2\":" + DoubleToString(sbl2_ask, 5) + ","; json += "\"bid2\":" + DoubleToString(sbl2_bid, 5) + ","; json += "\"ask-spread\":" + DoubleToString(ask_spread, 8) + ","; json += "\"bid-spread\":" + DoubleToString(bid_spread, 8) + "}"; string filename = "prices_" + tfStr + ".json"; // Read existing lines string lines[]; ArrayResize(lines, 0); int file = FileOpen(filename, FILE_READ | FILE_TXT | FILE_ANSI | FILE_COMMON); if(file != INVALID_HANDLE) { while(!FileIsEnding(file)) { string ln = FileReadString(file); StringTrimRight(ln); StringTrimLeft(ln); if(ln != "") { int n = ArraySize(lines); ArrayResize(lines, n + 1); lines[n] = ln; } } FileClose(file); } // Cap: if we have >= InpMaxLogs, keep only last (InpMaxLogs-1), then add new -> exactly InpMaxLogs if(ArraySize(lines) >= InpMaxLogs) { int keep = InpMaxLogs - 1; int start = ArraySize(lines) - keep; string trimmed[]; ArrayResize(trimmed, keep); for(int i = 0; i < keep; i++) trimmed[i] = lines[start + i]; ArrayResize(lines, keep); for(int i = 0; i < keep; i++) lines[i] = trimmed[i]; } int n = ArraySize(lines); ArrayResize(lines, n + 1); lines[n] = json; // Write file (overwrite with exactly lines[]) file = FileOpen(filename, FILE_WRITE | FILE_TXT | FILE_ANSI | FILE_COMMON); if(file == INVALID_HANDLE) { Print("FileOpen failed for ", filename, ", error ", GetLastError()); return; } for(int i = 0; i < ArraySize(lines); i++) { if(i > 0) FileWriteString(file, "\n"); FileWriteString(file, lines[i]); } FileClose(file); Print("Logged prices to Common\\Files\\", filename, " (", ArraySize(lines), " logs)"); // When we have >= InpMaxLogs, compute mean, standard deviation, z-score if(ArraySize(lines) >= InpMaxLogs) { double spreads[]; ArrayResize(spreads, ArraySize(lines)); int count = 0; for(int i = 0; i < ArraySize(lines); i++) { double a = 0, b = 0; if(ParseSpreadFromLine(lines[i], a, b)) { spreads[count++] = (a + b) * 0.5; } } if(count < 2) return; double sum = 0; for(int i = 0; i < count; i++) sum += spreads[i]; double mean = sum / count; double sumSq = 0; for(int i = 0; i < count; i++) sumSq += (spreads[i] - mean) * (spreads[i] - mean); double variance = sumSq / count; double std = MathSqrt(variance); double zScore = (std > 0) ? (current_spread - mean) / std : 0; Print("Spread: mean=", DoubleToString(mean, 8), " std=", DoubleToString(std, 8), " current=", DoubleToString(current_spread, 8), " z-score=", DoubleToString(zScore, 4)); } } //+------------------------------------------------------------------+ //| Expert deinitialization function | //+------------------------------------------------------------------+ void OnDeinit(const int reason) { EventKillTimer(); Print("meanSpreadReversion stopped. Reason: ", reason); } //+------------------------------------------------------------------+ //| Expert tick function | //+------------------------------------------------------------------+ void OnTick() { // Log to file once per bar — frequency = chart timeframe (e.g. M5 -> every 5 min) static datetime lastBar = 0; datetime thisBar = iTime(_Symbol, _Period, 0); if(thisBar != lastBar) { lastBar = thisBar; SavePriceToJSON(InpSymbol1, InpSymbol2); } }