139 lines
7 KiB
MQL5
139 lines
7 KiB
MQL5
//+------------------------------------------------------------------+
| |||
//| ImportDukascopyBars.mq5 |
| |||
//| AnimateDread |
| |||
//| https://www.mql5.com |
| |||
//+------------------------------------------------------------------+
| |||
#property copyright "AnimateDread"
| |||
#property link "https://www.mql5.com"
| |||
#property version "1.00"
| |||
#property script_show_inputs
| |||
#property description "Imports a Dukascopy M1 CSV (the SQX customdata export) as an MT5 custom"
| |||
#property description "symbol, so the same EA config can be tested against a SECOND data vendor."
| |||
| |||
//--- WHY THIS EXISTS
| |||
//---
| |||
//--- Every result this project has ever produced came from one broker's history. That is not a
| |||
//--- validation, it is a single sample: a result that survives only on Five Percent Online's bars
| |||
//--- is a result about Five Percent Online. The SQX install ships Dukascopy M1 exports for the
| |||
//--- same instruments, which makes a genuine like-for-like cross-vendor check possible - same EA,
| |||
//--- same window, same settings, independent prices.
| |||
//---
| |||
//--- WHAT THE FILES ACTUALLY CONTAIN (measured 2026-09-11, all 11 CSVs, counted by year):
| |||
//--- * The header says M1 and the filename says 1990, but the early years are DAILY bars, one
| |||
//--- per trading day stamped 00:00. EURUSD/USDJPY carry 257-261 bars/yr from 1990-1998 and
| |||
//--- then jump to ~300k/yr from 1999: a clean handover, no overlap, no mixed band.
| |||
//--- * The INDICES are not like that and must not be treated as if they were. SP500 is daily
| |||
//--- 2008-2016, then a PARTIAL intraday band 2017-2021 - 2017 holds 1,472 bars, about six per
| |||
//--- session - and only becomes true M1 in 2022. Those middle years would still build D1
| |||
//--- bars, from a few minutes of each session, so their highs and lows are systematically
| |||
//--- compressed: understated range, understated ATR, stops too tight. NAS100, UK100 and
| |||
//--- XTIUSD share the shape. Import the indices only for 2022+, or as daily-era data, never
| |||
//--- across the join.
| |||
//--- * <SPREAD> is synthetic and drifts with the vendor's era - 50 points in 1999, 40 in 2004,
| |||
//--- 13 in 2010, 7 in 2015, 0 by 2023. It is NOT a cost model and is deliberately ignored
| |||
//--- here; the custom symbol inherits its costs from the broker symbol it is cloned from.
| |||
//---
| |||
//--- HOW IT IMPORTS. CustomSymbolCreate() with an `origin` clones the broker symbol's contract
| |||
//--- specification - digits, tick size, tick value, contract size, margin - so a backtest on the
| |||
//--- custom symbol is priced like the real one and only the BARS differ. That is the whole point:
| |||
//--- if the two disagree, the disagreement is the data, not the instrument definition.
| |||
//+------------------------------------------------------------------+
| |||
| |||
input string InpCsv = "EURUSD_M1_dukascopy.csv"; // CSV in MQL5\Files (tab-separated MT5 export)
| |||
input string InpOrigin = "EURUSD"; // Broker symbol to clone the spec from
| |||
input string InpName = "EURUSD.dk"; // Name for the custom symbol
| |||
input string InpFrom = "2016.01.01"; // Import bars on/after this date ("" = all)
| |||
input string InpTo = ""; // Import bars before this date ("" = all)
| |||
| |||
#define BATCH 50000 // bars per CustomRatesUpdate call
| |||
| |||
//+------------------------------------------------------------------+
| |||
void OnStart(void)
| |||
{
| |||
const datetime from = (InpFrom == "") ? 0 : StringToTime(InpFrom);
| |||
const datetime to = (InpTo == "") ? 0 : StringToTime(InpTo);
| |||
| |||
//--- THE SYMBOL. Created once and then reused: deleting and recreating on every run would drop
| |||
//--- any bars already imported, and this file takes minutes to walk.
| |||
if(!SymbolSelect(InpName, true))
| |||
{
| |||
if(!CustomSymbolCreate(InpName, "Custom\\Dukascopy", InpOrigin))
| |||
{
| |||
PrintFormat("CustomSymbolCreate(%s, origin=%s) failed: %d - is '%s' in Market Watch?",
| |||
InpName, InpOrigin, GetLastError(), InpOrigin);
| |||
return;
| |||
}
| |||
SymbolSelect(InpName, true);
| |||
PrintFormat("created custom symbol %s cloned from %s", InpName, InpOrigin);
| |||
}
| |||
| |||
const int h = FileOpen(InpCsv, FILE_READ | FILE_TXT | FILE_ANSI);
| |||
if(h == INVALID_HANDLE)
| |||
{
| |||
PrintFormat("cannot open MQL5\\Files\\%s: %d", InpCsv, GetLastError());
| |||
return;
| |||
}
| |||
| |||
MqlRates r[];
| |||
ArrayResize(r, BATCH);
| |||
int n = 0;
| |||
long read = 0, kept = 0, bad = 0;
| |||
datetime firstKept = 0, lastKept = 0;
| |||
string parts[];
| |||
| |||
while(!FileIsEnding(h))
| |||
{
| |||
const string line = FileReadString(h);
| |||
read++;
| |||
if(StringLen(line) < 20 || StringGetCharacter(line, 0) == '<')
| |||
continue; // header, or a truncated tail line
| |||
if(StringSplit(line, '\t', parts) < 6)
| |||
{ bad++; continue; }
| |||
| |||
//--- "1990.01.02" + " " + "00:00:00" is exactly what StringToTime expects.
| |||
const datetime t = StringToTime(parts[0] + " " + parts[1]);
| |||
if(t == 0)
| |||
{ bad++; continue; }
| |||
if(from > 0 && t < from) continue;
| |||
if(to > 0 && t >= to) continue;
| |||
| |||
r[n].time = t;
| |||
r[n].open = StringToDouble(parts[2]);
| |||
r[n].high = StringToDouble(parts[3]);
| |||
r[n].low = StringToDouble(parts[4]);
| |||
r[n].close = StringToDouble(parts[5]);
| |||
r[n].tick_volume = (ArraySize(parts) > 6) ? (long)StringToInteger(parts[6]) : 0;
| |||
r[n].real_volume = 0;
| |||
//--- The vendor's spread column is era-dependent fiction (see the note above). Left at 0 so
| |||
//--- the tester prices from the cloned symbol's own spread rather than from it.
| |||
r[n].spread = 0;
| |||
| |||
//--- A zero or inverted bar would import as a valid bar and then quietly poison every ATR
| |||
//--- and range feature built from it. Refuse it here, where it can still be counted.
| |||
if(r[n].open <= 0.0 || r[n].high < r[n].low || r[n].high <= 0.0)
| |||
{ bad++; continue; }
| |||
| |||
if(firstKept == 0) firstKept = t;
| |||
lastKept = t;
| |||
kept++;
| |||
if(++n >= BATCH)
| |||
{
| |||
if(CustomRatesUpdate(InpName, r, n) < 0)
| |||
PrintFormat("CustomRatesUpdate failed at %s: %d", TimeToString(t), GetLastError());
| |||
n = 0;
| |||
if(kept % 1000000 == 0)
| |||
PrintFormat(" ... %d bars imported (at %s)", kept, TimeToString(t));
| |||
}
| |||
}
| |||
if(n > 0 && CustomRatesUpdate(InpName, r, n) < 0)
| |||
PrintFormat("final CustomRatesUpdate failed: %d", GetLastError());
| |||
FileClose(h);
| |||
| |||
PrintFormat("%s: read %d line(s), imported %d bar(s), rejected %d; range %s .. %s",
| |||
InpName, read, kept, bad,
| |||
TimeToString(firstKept, TIME_DATE | TIME_MINUTES),
| |||
TimeToString(lastKept, TIME_DATE | TIME_MINUTES));
| |||
PrintFormat("Bars(%s, M1) now reports %d; D1 reports %d.",
| |||
InpName, Bars(InpName, PERIOD_M1), Bars(InpName, PERIOD_D1));
| |||
}
| |||
//+------------------------------------------------------------------+
|