bleh
[mqlkit.git] / include / offline_charts.mqh
blobd44563e20a3a8c5111bc3c6993ded38a5d7dbe0b
1 //+------------------------------------------------------------------+\r
2 //|                                               offline_charts.mq4 |\r
3 //|                                                     Bernd Kreuss |\r
4 //|                                              Version 2010.9.12.1 |\r
5 //|                 paypal-donations go here -> mailto:7ibt@arcor.de |\r
6 //+------------------------------------------------------------------+\r
7 #property copyright "Bernd Kreuss"\r
8 #property link      "http://sites.google.com/site/prof7bit/"\r
9 \r
10 /** @file\r
11 * This file contains functions, needed to handle offline charts,\r
12 * the latest version of this file is available at \r
13 * http://sites.google.com/site/prof7bit/\r
14 *\r
15 * You can create or update offline charts in real time and also refresh\r
16 * the chart window associated with such an offline chart automatically, \r
17 *\r
18 * The main purpose of this library is to be able to produce\r
19 * equity curves resulting from traded strategies in real time, \r
20 * equity curves that would contain every drawdown that occured\r
21 * during the trades, not only closed profits. This is especially\r
22 * useful when used while backtesting an EA, unlike the built-in\r
23 * backtester equity plotter this one will record everything that\r
24 * happens while the positions are open and draw a much more \r
25 * realistic picture.\r
26 *\r
27 * To use the equity plotter feature simply include the .mqh file in \r
28 * your expert and then on every tick call the function recordEquity()\r
29 * For example if you have an EA with the name "foobazer" and want to\r
30 * record it's performance in an M15 chart you would issue the following\r
31 * call on every tick: \r
32 \r
33 *   recordEquity("foobazer", PERIOD_M15, magic_number)\r
34 *\r
35 \r
36 * !!! Use a different name for every chart the EA runs on !!!\r
37 * The period has nothing to do with the period the EA runs on, it is\r
38 * just meant to tell the function which timeframe the equity chart should have\r
39 * and can be every period you want. In the above example It will create and \r
40 * continually update an offline M15 chart with the name "foobazer" containing \r
41 * the performance of your EA in real time from this moment on. If you run your \r
42 * EA on backtester then an underscore will be prepended to the name of the chart,\r
43 * so it won't overwrite your live chart.\r
44 \r
45 * Names can only contain up to 12 characters. Make sure every EA on every \r
46 * chart will use a DIFFERENT NAME when calling this function or they will \r
47 * all write to the same chart and produce a complete mess.\r
48 */\r
50 #include <WinUser32.mqh>\r
52 #define OFFLINE_HEADER_SIZE 148 ///< LONG_SIZE + 64 + 12 + 4 * LONG_SIZE + 13 * LONG_SIZE \r
53 #define OFFLINE_RECORD_SIZE 44  ///< 5 * DOUBLE_SIZE + LONG_SIZE \r
55 int __chart_file = 0; ///< the cache for the file handle (only used in backtesting mode) \r
57 /**\r
58 * Record the equity curve for the specified filter critera into an offline chart.\r
59 * Call this function on every tick. It will produce a chart that will \r
60 * include every high and low of the total floating and realized profits.\r
61 \r
62 * You can filter by magic number and/or by the comment field.\r
63 * If magic is -1 then all magic numbers are allowed, if it is 0 then \r
64 * only manually opened trades are counted, if it is any other\r
65 * value then only trades with this particular number are counted. \r
66 * The second filter is comment, if it is "" then it is not filtered\r
67 * by comment, else only trades with this exact comment string are\r
68 * counted.\r
69 \r
70 * Offset is used to make sure the chart always is in positive terrotory.\r
71 * Since metatrader won't display charts with negative values and we are\r
72 * only summing up an individual strategie's profit (or loss!), not total \r
73 * equity, we need some imaginary positive starting capital for each chart.\r
74 */\r
75 void recordEquity(string name, int period, int magic=-1, string comment="", double offset=5000){\r
76    double equity;\r
77    \r
78    // don't do anything during optimization runs\r
79    if (IsOptimization()){\r
80       return(0);\r
81    }\r
82    \r
83    // This can happen shortly after a restart of metatrader. The order history\r
84    // is still empty. We do nothing in this case and wait for the next tick.\r
85    if (OrdersHistoryTotal() == 0){\r
86       return(0);\r
87    }\r
89    if (magic == -1 && comment == ""){\r
90       // If there is no filter we can simply use AccountEquity(), also we\r
91       // don't need a virtual starting balance in this case. \r
92       equity = AccountEquity();\r
93    }else{\r
94       // Otherwise calculate the partial profits and add 'offset' \r
95       // as virtual starting balance.\r
96       equity = getAllProfitFiltered(magic, comment) + offset;\r
97    }\r
98       \r
99    // when run in the strategy tester we add a _ to the chart name\r
100    // so it wont interfere with the same chart in live trading\r
101    if (IsTesting()){\r
102       name = "_" + name;\r
103    }\r
104    \r
105    // write it into the chart.\r
106    updateOfflineChart(name, period, 2, equity, 0); \r
109 /**\r
110 * Update the offline chart with a new price. \r
111 * This function will find the last bar in the offline chart file \r
112 * (create the chart if necessary) update the last bar (adjust the close, \r
113 * add to the volume and extend high or low if necessary) or start a new bar \r
114 * if current time is beyond the lifetime of the last bar in the chart.\r
115 * Note: if you want to make Renko or other range based charts you will need to\r
116 * write your own function similar to this but with a different algorithm to\r
117 * detect when a new bar must be started. This one is strictly time based.\r
118 */ \r
119 void updateOfflineChart(string symbol, int period, int digits, double price, double volume){\r
120    double o,h,l,c,v;\r
121    int t;\r
122    int time_current = iTime(NULL, period, 0); // FIXME! the starting time for the period's current bar\r
123    \r
124    // create the chart if it doesn't already exist\r
125    // or just update the header\r
126    writeOfflineHeader(symbol, period, digits);\r
127    \r
128    // read the last bar in the chart (if any)\r
129    if (!readOfflineBar(symbol, period, 1, t, o, h, l, c, v)){\r
130       // no bars in chart yet, so just make one\r
131       writeOfflineBar(symbol, period, 0, time_current, price, price, price, price, volume);\r
132       return(0);\r
133    }\r
134    \r
135    if (t > time_current){\r
136       // this is a very special case: the last bar in the chart is\r
137       // NEWER that the bar we just want to record. This can only\r
138       // happen if we backtest and there is already a chart left\r
139       // from a previous backtest. In this case the only reasonable\r
140       // thing we would want to do is completely empty the chart and\r
141       // start again.\r
142       if (IsTesting()){\r
143          // ONLY empty the chart if we REALLY are in the baktester, \r
144          // else we simply IGNORE it completely instead of accidently \r
145          // destroying a whole and possibly months old chart just \r
146          // because of one bad timestamp.\r
147          emptyOfflineChart(symbol, period, digits);   \r
148          writeOfflineBar(symbol, period, 0, time_current, price, price, price, price, volume);\r
149       }\r
150       return(0);\r
151    }\r
152    \r
153    if (t == time_current){\r
154       // the bar has the current time, so update it\r
155       if (price > h){\r
156          h = price;\r
157       }\r
158       if (price < l){\r
159          l = price;\r
160       }\r
161       c = price;\r
162       v += volume;\r
163       writeOfflineBar(symbol, period, 1, t, o, h, l, c, v);\r
164    }else{\r
165       // last bar is old, start a new one\r
166       writeOfflineBar(symbol, period, 0, time_current, price, price, price, price, volume);\r
167    }  \r
170 /**\r
171 * empty the chart, write a fresh header\r
172 */\r
173 void emptyOfflineChart(string symbol, int period, int digits){\r
174    // close it first (if we are in the backtester and the file is kept open)\r
175    forceFileClose();\r
176       \r
177    // open (and immediately close) the file in write only mode, \r
178    // this will truncate it to zero length, after that we write a fresh header\r
179    FileClose(FileOpenHistory(offlineFileName(symbol, period), FILE_WRITE | FILE_BIN));\r
180    writeOfflineHeader(symbol, period, digits);\r
183 /**\r
184 * write or update the header of an offline chart file,\r
185 * if the file does not yet exist create the file.\r
186 */\r
187 void writeOfflineHeader(string symbol, int period, int digits){\r
188    int    version = 400;\r
189    string c_copyright = "(C)opyright 2009-2010, Bernd Kreuss";\r
190    int    i_unused[13];\r
191    \r
192    int F = fileOpenEx(offlineFileName(symbol, period), FILE_BIN | FILE_READ | FILE_WRITE);\r
193    FileSeek(F, 0, SEEK_SET);\r
194    FileWriteInteger(F, version, LONG_VALUE);\r
195    FileWriteString(F, c_copyright, 64);\r
196    FileWriteString(F, symbol, 12);\r
197    FileWriteInteger(F, period, LONG_VALUE);\r
198    FileWriteInteger(F, digits, LONG_VALUE);\r
199    FileWriteInteger(F, 0, LONG_VALUE);       //timesign\r
200    FileWriteInteger(F, 0, LONG_VALUE);       //last_sync\r
201    FileWriteArray(F, i_unused, 0, 13);\r
203    fileCloseEx(F);\r
206 /**\r
207 * Write (or update) one bar in the offline chart file\r
208 * and refresh the chart window if it is currently open.\r
209 * The parameter bars_back is the offset counted from the end of\r
210 * the file: 0 means append a new bar, 1 means update the last bar\r
211 * 2 would be the second last bar and so on.\r
212 * The parameter time is the POSIX-Timestamp representing the \r
213 * beginning of that bar. It is the same value that would be \r
214 * returned from iTime() or Time[], namely the seconds that\r
215 * have passed since the UNIX-Epoch (00:00 a.m. of 1 January, 1970)\r
216 */\r
217 void writeOfflineBar(string symbol, int period, int bars_back, int time, double open, double high, double low, double close, double volume){\r
218    int F = fileOpenEx(offlineFileName(symbol, period), FILE_BIN | FILE_READ | FILE_WRITE);\r
219    \r
220    int position = bars_back * OFFLINE_RECORD_SIZE;   \r
221    FileSeek(F, -position, SEEK_END);\r
223    if (FileTell(F) >= OFFLINE_HEADER_SIZE){\r
224       FileWriteInteger(F, time, LONG_VALUE); \r
225       FileWriteDouble(F, open, DOUBLE_VALUE);\r
226       FileWriteDouble(F, low, DOUBLE_VALUE);\r
227       FileWriteDouble(F, high, DOUBLE_VALUE);\r
228       FileWriteDouble(F, close, DOUBLE_VALUE);\r
229       FileWriteDouble(F, volume, DOUBLE_VALUE);\r
230    \r
231       // refresh the chart window\r
232       // this won't work in backtesting mode\r
233       // and also don't do it while deinitializing or the\r
234       // WindowHandle() function will run into a deadlock\r
235       // for unknown reasons. (this took me a while to debug)\r
236       if (!IsStopped() && !IsTesting()){\r
237          int hwnd=WindowHandle(symbol, period);\r
238          if (hwnd != 0){\r
239             PostMessageA(hwnd, WM_COMMAND, 33324, 0);\r
240          }\r
241       }\r
242    }\r
243    fileCloseEx(F);\r
246 /**\r
247 * Read one bar out of the offline chart file and fill the\r
248 * "by reference"-parameters that were passed to the function.\r
249 * The function returns True if successful or False otherwise.\r
250 * The parameter bars_back is the offset counting from the end\r
251 * of the file: 0 makes no sense since it would be past the end,\r
252 * 1 means read the last bar in the file, 2 the second last, etc.\r
253 * If bars_back would point outside the file (beginning or end)\r
254 * the function will return False and do nothing, otherwise\r
255 * the read values will be filled into the supplied parameters\r
256 * and the function will return True\r
257 */ \r
258 bool readOfflineBar(string symbol, int period, int bars_back, int& time, double& open, double& high, double& low, double& close, double& volume){\r
259    int F = fileOpenEx(offlineFileName(symbol, period), FILE_BIN | FILE_READ | FILE_WRITE);\r
260    \r
261    int position = bars_back * OFFLINE_RECORD_SIZE;\r
262    FileSeek(F, -position, SEEK_END);\r
263    \r
264    if (FileSize(F) - FileTell(F) >= OFFLINE_RECORD_SIZE && FileTell(F) >= OFFLINE_HEADER_SIZE){\r
265       time = FileReadInteger(F, LONG_VALUE); \r
266       open = FileReadDouble(F, DOUBLE_VALUE);\r
267       low = FileReadDouble(F, DOUBLE_VALUE);\r
268       high = FileReadDouble(F, DOUBLE_VALUE);\r
269       close = FileReadDouble(F, DOUBLE_VALUE);\r
270       volume = FileReadDouble(F, DOUBLE_VALUE);\r
271       fileCloseEx(F);\r
272       return(True);\r
273    }else{\r
274       fileCloseEx(F);\r
275       return(False);\r
276    }\r
279 /**\r
280 * construct the file name for the chart file, truncate the \r
281 * symbol name to the maximum of 12 allowed characters.\r
282 */\r
283 string offlineFileName(string symbol, int period){\r
284    return(StringSubstr(symbol, 0, 12) + period + ".hst");\r
287 /**\r
288 * loop through all trades (historic and currently open) and\r
289 * sum up all profits (including swap and commission).\r
290 */\r
291 double getAllProfitFiltered(int magic=-1, string comment=""){\r
292    int cnt, total;\r
293    double floating = 0;\r
294    double realized = 0;\r
295    static int last_closed_ticket = 0;\r
296    string cache_name; \r
297    \r
298    // we need a unique name under which to cache the realized P&L later\r
299    if (IsTesting()){\r
300       cache_name = "profit_cache@@" + magic + "@" + comment;\r
301    }else{\r
302       cache_name = "profit_cache@" + magic + "@" + comment;\r
303    }\r
304    \r
305    // sum up the floating\r
306    total=OrdersTotal();\r
307    for(cnt=0; cnt<total; cnt++){\r
308       OrderSelect(cnt, SELECT_BY_POS, MODE_TRADES);\r
309       if ((magic == -1 || OrderMagicNumber() == magic) \r
310        && (comment == "" || StringFind(OrderComment(), comment, 0) != -1)\r
311       ){\r
312          floating += OrderProfit() + OrderSwap() + OrderCommission();\r
313       }\r
314    }\r
315    \r
316    // Now calculate the total realized profit.\r
317    // We first check if the order history has changed since \r
318    // the last tick by looking at the newest ticket number.\r
319    // If there was no change then we can assume that no trade\r
320    // has been closed and we can simply use the cached value\r
321    // of the previously calculated realized profit.\r
322    total=OrdersHistoryTotal();\r
323    OrderSelect(total-1, SELECT_BY_POS, MODE_HISTORY);\r
324    if (last_closed_ticket != OrderTicket()){\r
325    \r
326       // history is different from last time, so we must do \r
327       // the expensive loop and sum up all realized profit\r
328       last_closed_ticket = OrderTicket();\r
329       realized = 0;\r
330       for(cnt=0; cnt<total; cnt++){\r
331          OrderSelect(cnt, SELECT_BY_POS, MODE_HISTORY);\r
332          if ((magic == -1 || OrderMagicNumber() == magic) \r
333           && (comment == "" ||  StringFind(OrderComment(), comment, 0) != -1)\r
334          ){\r
335             realized += OrderProfit() + OrderSwap() + OrderCommission();\r
336          }\r
337       }\r
338       // remember it for the next call.\r
339       // We need to store it separately for every possible filter.\r
340       // We dont have hash tables in mql4, so we must abuse \r
341       // the global variables function to store name-value pairs.\r
342       GlobalVariableSet(cache_name, realized);      \r
343    }else{\r
344    \r
345       // history not changed. retrieve the cached value.\r
346       realized = GlobalVariableGet(cache_name);\r
347    }\r
348    \r
349    return (floating + realized);\r
352 /**\r
353 * Open the chart file. In testing mode this will open the file\r
354 * only when called for the first time and cache the file handle\r
355 * for subsequent calls to speed up things. The corresponding\r
356 * fileCloseEx() function will do nothing when in testing mode.\r
357 */\r
358 int fileOpenEx(string name, int mode){\r
359    if (IsTesting()){\r
360       if (__chart_file == 0){\r
361          __chart_file = FileOpenHistory(name, mode);\r
362       }\r
363       return(__chart_file);\r
364    }else{\r
365       return(FileOpenHistory(name, mode));\r
366    }\r
369 /**\r
370 * close the file. Keep the file open when in teting mode.\r
371 */\r
372 void fileCloseEx(int file){\r
373    if (!IsTesting()){\r
374       FileClose(file);\r
375    }else{\r
376       //FileFlush(file);\r
377    }\r
380 /**\r
381 * enforce closing of the file (in backtesting mode when it is held open)\r
382 */ \r
383 void forceFileClose(){\r
384    if(__chart_file != 0){\r
385       FileClose(__chart_file);\r
386       __chart_file = 0;\r
387    }\r