forked from rosh/MQL5Book
279 lines
10 KiB
MQL5
279 lines
10 KiB
MQL5
//+------------------------------------------------------------------+
| |
//| CustomTester.mq5 |
| |
//| Copyright 2022, MetaQuotes Ltd. |
| |
//| https://www.mql5.com |
| |
//+------------------------------------------------------------------+
| |
#property description "Generates custom symbol from real Ticks of current chart's symbol.\n"
| |
#property description "Ticks are coming slowly, producing a kind of visual back test chart, but with full support of interactive events and up to the current day (that's not available in the standard tester).\n"
| |
| |
#include "..\..\Include\PRTF.mqh"
| |
#include "..\..\Include\Defines.mqh"
| |
#include <VirtualKeys.mqh>
| |
#include "..\..\Include\CustomSymbolMonitor.mqh"
| |
| |
#define EVENT_KEY 0xDED
| |
| |
//+------------------------------------------------------------------+
| |
//| Inputs |
| |
//+------------------------------------------------------------------+
| |
input string CustomPath = "MQL5Book\\Part7"; // Custom Symbol Folder
| |
input datetime _Start; // Start (default: 120 days back)
| |
input ENUM_TIMEFRAMES Timeframe = PERIOD_H1;
| |
| |
//+------------------------------------------------------------------+
| |
//| Globals (names in CamelCase) |
| |
//+------------------------------------------------------------------+
| |
string CustomSymbol = _Symbol + ".Tester";
| |
const uint DailySeconds = 60 * 60 * 24;
| |
datetime Start = _Start == 0 ? TimeCurrent() - DailySeconds * 120 : _Start;
| |
bool FirstCopy = true;
| |
// step back 1 day because without this new ticks will not update the chart
| |
datetime Cursor = (Start / DailySeconds - 1) * DailySeconds; // round up to a day boundary
| |
MqlTick Ticks[]; // ticks for current day
| |
int Index = 0; // position inside the day
| |
int Step = 32; // advancing by 32 Ticks at once (by default)
| |
int StepRestore = 0; // remember recent speed during pause
| |
long Chart = 0; // newly created chart with custom symbol
| |
bool InitDone = false;
| |
| |
//+------------------------------------------------------------------+
| |
//| Expert initialization function |
| |
//+------------------------------------------------------------------+
| |
void OnInit()
| |
{
| |
EventSetMillisecondTimer(100);
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Timer handler |
| |
//+------------------------------------------------------------------+
| |
void OnTimer()
| |
{
| |
if(!GenerateData())
| |
{
| |
EventKillTimer();
| |
}
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Helper function to speed up/slow down Ticks replay by keys |
| |
//+------------------------------------------------------------------+
| |
//| ATTENTION! Keyboard works only when the chart has focus, |
| |
//| so you need to click it by mouse for the keys to take effect. |
| |
//+------------------------------------------------------------------+
| |
void CheckKeys(const long key)
| |
{
| |
if(key == VK_DOWN)
| |
{
| |
Step /= 2;
| |
if(Step > 0)
| |
{
| |
Print("Slow down: ", Step);
| |
ChartSetString(Chart, CHART_COMMENT, "Speed: " + (string)Step);
| |
}
| |
else
| |
{
| |
Print("Paused");
| |
ChartSetString(Chart, CHART_COMMENT, "Paused");
| |
ChartRedraw(Chart);
| |
}
| |
}
| |
else if(key == VK_UP)
| |
{
| |
if(Step == 0)
| |
{
| |
Step = 1;
| |
Print("Resumed");
| |
ChartSetString(Chart, CHART_COMMENT, "Resumed");
| |
}
| |
else
| |
{
| |
Step *= 2;
| |
Print("Spead up: ", Step);
| |
ChartSetString(Chart, CHART_COMMENT, "Speed: " + (string)Step);
| |
}
| |
}
| |
else if(key == VK_PAUSE)
| |
{
| |
if(Step > 0)
| |
{
| |
StepRestore = Step;
| |
Step = 0;
| |
Print("Paused");
| |
ChartSetString(Chart, CHART_COMMENT, "Paused");
| |
ChartRedraw(Chart);
| |
}
| |
else
| |
{
| |
Step = StepRestore;
| |
Print("Resumed");
| |
ChartSetString(Chart, CHART_COMMENT, "Speed: " + (string)Step);
| |
}
| |
}
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Main function to produce the custom symbol and emit Ticks |
| |
//+------------------------------------------------------------------+
| |
bool GenerateData()
| |
{
| |
if(!InitDone)
| |
{
| |
bool custom = false;
| |
if(PRTF(SymbolExist(CustomSymbol, custom)) && custom)
| |
{
| |
if(IDYES == MessageBox(StringFormat("Clean up existing custom symbol '%s'?", CustomSymbol),
| |
"Please, confirm", MB_YESNO))
| |
{
| |
PRTF(CustomRatesDelete(CustomSymbol, 0, LONG_MAX));
| |
PRTF(CustomTicksDelete(CustomSymbol, 0, LONG_MAX));
| |
Sleep(1000);
| |
MqlRates rates[1];
| |
MqlTick tcks[];
| |
if(PRTF(CopyRates(CustomSymbol, PERIOD_M1, 0, 1, rates)) == 1
| |
|| PRTF(CopyTicks(CustomSymbol, tcks) > 0))
| |
{
| |
Alert("Can't delete rates and Ticks, internal error");
| |
ExpertRemove();
| |
}
| |
}
| |
else
| |
{
| |
return false;
| |
}
| |
}
| |
else
| |
if(!PRTF(CustomSymbolCreate(CustomSymbol, CustomPath, _Symbol)))
| |
{
| |
return false;
| |
}
| |
| |
// some properties, including very important ones can be not applied
| |
// right away after calling CustomSymbolCreate, so we need to check them
| |
// and try to apply "manually"
| |
SymbolMonitor sm;
| |
CustomSymbolMonitor csm(CustomSymbol, &sm);
| |
int props[] = {SYMBOL_TRADE_TICK_VALUE, SYMBOL_TRADE_TICK_SIZE};
| |
const int d1 = csm.verify(props);
| |
if(d1)
| |
{
| |
Print("Number of found descrepancies: ", d1);
| |
if(csm.verify(props)) // check again
| |
{
| |
Alert("Custom symbol can not be created, internal error!");
| |
return false;
| |
}
| |
Print("Fixed");
| |
}
| |
| |
Print(TimeToString(SymbolInfoInteger(CustomSymbol, SYMBOL_TIME)));
| |
SymbolSelect(CustomSymbol, true);
| |
Chart = ChartOpen(CustomSymbol, Timeframe);
| |
const int handle = iCustom(CustomSymbol, Timeframe, "MQL5Book/p7/KeyboardSpy", ChartID(), EVENT_KEY);
| |
ChartIndicatorAdd(Chart, 0, handle);
| |
ChartSetString(Chart, CHART_COMMENT, "Custom Tester");
| |
ChartSetInteger(Chart, CHART_SHOW_OBJECT_DESCR, true);
| |
ChartRedraw(Chart);
| |
| |
InitDone = true;
| |
}
| |
else
| |
{
| |
for(int i = 0; i <= (Step - 1) / 256; ++i)
| |
if(Step > 0 && !GenerateTicks())
| |
{
| |
return false;
| |
}
| |
}
| |
return true;
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Helper function to read real symbol Ticks by chunks day by day |
| |
//+------------------------------------------------------------------+
| |
bool FillTickBuffer()
| |
{
| |
int r;
| |
ArrayResize(Ticks, 0);
| |
do
| |
{
| |
r = PRTF(CopyTicksRange(_Symbol, Ticks, COPY_TICKS_ALL, Cursor * 1000L, (Cursor + DailySeconds) * 1000L - 1));
| |
if(r > 0 && FirstCopy)
| |
{
| |
// MQL5 bug workaround: this preliminary call is needed for the chart to leave "Waiting for Update" state
| |
PRTF(CustomTicksReplace(CustomSymbol, Cursor * 1000L, (Cursor + DailySeconds) * 1000L - 1, Ticks));
| |
FirstCopy = false;
| |
r = 0;
| |
}
| |
Cursor += DailySeconds;
| |
}
| |
while(r == 0 && Cursor < TimeCurrent()); // skip non-trading days
| |
Index = 0;
| |
return r > 0;
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Helper function to emit Ticks by small packets to custom symbol |
| |
//+------------------------------------------------------------------+
| |
bool GenerateTicks()
| |
{
| |
if(Index >= ArraySize(Ticks))
| |
{
| |
if(!FillTickBuffer()) return false;
| |
}
| |
| |
const int m = ArraySize(Ticks);
| |
MqlTick array[];
| |
const int n = ArrayCopy(array, Ticks, 0, Index, fmin(fmin(Step, 256), m));
| |
if(n <= 0) return false;
| |
| |
ResetLastError();
| |
if(CustomTicksAdd(CustomSymbol, array) != ArraySize(array) || _LastError != 0)
| |
{
| |
Print(_LastError); // in case we get ERR_CUSTOM_TICKS_WRONG_ORDER (5310)
| |
ExpertRemove();
| |
}
| |
| |
Comment("Spead: ", (string)Step, " / ", STR_TIME_MSC(array[n - 1].time_msc));
| |
Index += Step;
| |
| |
return true;
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Chart events handler: |
| |
//| - interactive keyboard presses (when the chart is active) |
| |
//| - remote keyboard events on dependent custom symbol chart |
| |
//+------------------------------------------------------------------+
| |
void OnChartEvent(const int id, const long &lparam, const double &dparam, const string &sparam)
| |
{
| |
if(id == CHARTEVENT_CUSTOM + EVENT_KEY) // this notification comes from dependent chart
| |
{
| |
// NB: MT5 limitation: since our MQL-program generates custom symbol
| |
// and normally does this in background (its chart is not an active chart),
| |
// TerminalInfoInteger(TERMINAL_KEYSTATE_) does not work,
| |
// that is it returns 0 always, hence we can't detect
| |
// Ctrl/Shift and other key states, and use only plain alphanumeric keys
| |
CheckKeys(lparam);
| |
}
| |
else if(id == CHARTEVENT_KEYDOWN) // this only fires when this chart is active
| |
{
| |
// when the chart is active, only at that mements we could
| |
// get meaningful data from TerminalInfoInteger(TERMINAL_KEYSTATE_),
| |
// but since it's unavailable via iCustom indicators, we don't use it here as well
| |
CheckKeys(lparam);
| |
}
| |
}
| |
| |
//+------------------------------------------------------------------+
| |
//| Finalization handler |
| |
//+------------------------------------------------------------------+
| |
void OnDeinit(const int)
| |
{
| |
if(Chart != 0)
| |
{
| |
ChartClose(Chart);
| |
}
| |
Comment("");
| |
}
| |
//+------------------------------------------------------------------+
|