MQL5 Cookbook: Trading strategy stress testing using custom symbols
Introduction
Some time ago a new feature was added in the MetaTrader 5 terminal: the possibility to create custom symbols. Now algo traders can act as brokers for themselves in some cases, while they do not need trade servers and can totally control the trading environment. However, "some cases" here only refer to the testing mode. Of course, orders for custom symbols cannot be executed by brokers. Nevertheless, this functionality enables algo traders to test their trading strategies more carefully.
In this article we will create conditions for such testing. Let us start with the custom symbol class.
1. The custom symbol class CiCustomSymbol
The Standard Library provides a class for simplified access to symbol properties. It is the CSymbolInfo class. In fact, the class executes an intermediary function: upon a user request, the class communicates with the server and receives from it a response in the form of a value of the requested property.
Our purpose is to create a similar class for a custom symbol. Moreover, this class functionality will be even wider, because we need to add creation, deletion and some other methods. On the other hand, we will not use methods associate with connection to the server. These include Refresh(), IsSynchronized() etc.
This class for creating a trading environment encapsulates standard features for working with custom symbols.
The class declaration structure is shown below.
//+------------------------------------------------------------------+ //| Class CiCustomSymbol. | //| Purpose: Base class for a custom symbol. | //+------------------------------------------------------------------+ class CiCustomSymbol : public CObject { //--- === Data members === --- private: string m_name; string m_path; MqlTick m_tick; ulong m_from_msc; ulong m_to_msc; uint m_batch_size; bool m_is_selected; //--- === Methods === --- public: //--- constructor/destructor void CiCustomSymbol(void); void ~CiCustomSymbol(void) {}; //--- create/delete int Create(const string _name,const string _path="",const string _origin_name=NULL, const uint _batch_size=1e6,const bool _is_selected=false); bool Delete(void); //--- methods of access to protected data string Name(void) const { return(m_name); } bool RefreshRates(void); //--- fast access methods to the integer symbol properties bool Select(void) const; bool Select(const bool select); //--- service methods bool Clone(const string _origin_symbol,const ulong _from_msc=0,const ulong _to_msc=0); bool LoadTicks(const string _src_file_name); bool ChangeSpread(const uint _spread_size,const uint _spread_markup=0, const ENUM_SPREAD_BASE _spread_base=SPREAD_BASE_BID); //--- API bool SetProperty(ENUM_SYMBOL_INFO_DOUBLE _property,double _val) const; bool SetProperty(ENUM_SYMBOL_INFO_INTEGER _property,long _val) const; bool SetProperty(ENUM_SYMBOL_INFO_STRING _property,string _val) const; double GetProperty(ENUM_SYMBOL_INFO_DOUBLE _property) const; long GetProperty(ENUM_SYMBOL_INFO_INTEGER _property) const; string GetProperty(ENUM_SYMBOL_INFO_STRING _property) const; bool SetSessionQuote(const ENUM_DAY_OF_WEEK _day_of_week,const uint _session_index, const datetime _from,const datetime _to); bool SetSessionTrade(const ENUM_DAY_OF_WEEK _day_of_week,const uint _session_index, const datetime _from,const datetime _to); int RatesDelete(const datetime _from,const datetime _to); int RatesReplace(const datetime _from,const datetime _to,const MqlRates &_rates[]); int RatesUpdate(const MqlRates &_rates[]) const; int TicksAdd(const MqlTick &_ticks[]) const; int TicksDelete(const long _from_msc,long _to_msc) const; int TicksReplace(const MqlTick &_ticks[]) const; //--- private: template<typename PT> bool CloneProperty(const string _origin_symbol,const PT _prop_type) const; int CloneTicks(const MqlTick &_ticks[]) const; int CloneTicks(const string _origin_symbol) const; }; //+------------------------------------------------------------------+
Let us begin with the methods which will make a full-featured custom symbol out of a selected symbol.
1.1 The CiCustomSymbol::Create() method
In order to use all custom symbol possibilities, we need to create it or make sure it has been created.
//+------------------------------------------------------------------+ //| Create a custom symbol | //| Codes: | //| -1 - failed to create; | //| 0 - a symbol exists, no need to create; | //| 1 - successfully created. | //+------------------------------------------------------------------+ int CiCustomSymbol::Create(const string _name,const string _path="",const string _origin_name=NULL, const uint _batch_size=1e6,const bool _is_selected=false) { int res_code=-1; m_name=m_path=NULL; if(_batch_size<1e2) { ::Print(__FUNCTION__+": a batch size must be greater than 100!"); } else { ::ResetLastError(); //--- attempt to create a custom symbol if(!::CustomSymbolCreate(_name,_path,_origin_name)) { if(::SymbolInfoInteger(_name,SYMBOL_CUSTOM)) { ::PrintFormat(__FUNCTION__+": a custom symbol \"%s\" already exists!",_name); res_code=0; } else { ::PrintFormat(__FUNCTION__+": failed to create a custom symbol. Error code: %d",::GetLastError()); } } else res_code=1; if(res_code>=0) { m_name=_name; m_path=_path; m_batch_size=_batch_size; //--- if the custom symbol must be selected in the "Market Watch" if(_is_selected) { if(!this.Select()) if(!this.Select(true)) { ::PrintFormat(__FUNCTION__+": failed to set the \"Market Watch\" symbol flag. Error code: %d",::GetLastError()); return false; } } else { if(this.Select()) if(!this.Select(false)) { ::PrintFormat(__FUNCTION__+": failed to unset the \"Market Watch\" symbol flag. Error code: %d",::GetLastError()); return false; } } m_is_selected=_is_selected; } } //--- return res_code; } //+------------------------------------------------------------------+
The method will return a value as a numerical code:
- -1 — error creating a symbol;
- 0 — the symbol was created earlier;
- 1 — the symbol has been successfully created during current method call.
Here are a few words about the _batch_size and _is_selected parameters.
The first parameter (_batch_size) sets the size of the batch, which will be used to load ticks. Ticks will be loaded in batches: data is first read to an auxiliary array; once the array has been filled, the data is loaded to the tick database of a custom symbol (tick history). On the one hand, with this approach you do not have to create a huge array, on the other hand, there is no need to update the tick database too often. The default size of the auxiliary tick array is 1 million.
The second parameter (_is_selected) determines whether we will write the ticks directly to the database, or they will first be added to the Market Watch window.
As an example, let us run the TestCreation.mql5 script, which creates a custom symbol.
The code returned by this method call will be shown in the Journal.
2019.08.11 12:34:08.055 TestCreation (EURUSD,M1) A custom symbol "EURUSD_1" creation has returned the code: 1
For more details related to custom symbol creation, please read the Documentation.
1.2 The CiCustomSymbol::Delete() method
This method will try to delete a custom symbol. Before deleting the symbol, the method will try to remove it from the Market Watch window. In case of failure, the deletion procedure will be interrupted.
//+------------------------------------------------------------------+ //| Delete | //+------------------------------------------------------------------+ bool CiCustomSymbol::Delete(void) { ::ResetLastError(); if(this.Select()) if(!this.Select(false)) { ::PrintFormat(__FUNCTION__+": failed to set the \"Market Watch\" symbol flag. Error code: %d",::GetLastError()); return false; } if(!::CustomSymbolDelete(m_name)) { ::PrintFormat(__FUNCTION__+": failed to delete the custom symbol \"%s\". Error code: %d",m_name,::GetLastError()); return false; } //--- return true; } //+------------------------------------------------------------------+
As an example, let us start a simple script TestDeletion.mql5, which deletes a custom symbol. If successful, a corresponding log will be added to the Journal.
2019.08.11 19:13:59.276 TestDeletion (EURUSD,M1) A custom symbol "EURUSD_1" has been successfully deleted.
1.3 The CiCustomSymbol::Clone() method
This method performs the cloning: based on the selected symbol, it determines properties for the current custom symbol. Simply put, we receive the original symbol's property value and copy it to another symbol. The user can also set the cloning of a tick history. To do this, you need to define the time interval.
//+------------------------------------------------------------------+ //| Clone a symbol | //+------------------------------------------------------------------+ bool CiCustomSymbol::Clone(const string _origin_symbol,const ulong _from_msc=0,const ulong _to_msc=0) { if(!::StringCompare(m_name,_origin_symbol)) { ::Print(__FUNCTION__+": the origin symbol name must be different!"); return false; } ::ResetLastError(); //--- if to load history if(_to_msc>0) { if(_to_msc<_from_msc) { ::Print(__FUNCTION__+": wrong settings for a time interval!"); return false; } m_from_msc=_from_msc; m_to_msc=_to_msc; } else m_from_msc=m_to_msc=0; //--- double ENUM_SYMBOL_INFO_DOUBLE dbl_props[]= { SYMBOL_MARGIN_HEDGED, SYMBOL_MARGIN_INITIAL, SYMBOL_MARGIN_MAINTENANCE, SYMBOL_OPTION_STRIKE, SYMBOL_POINT, SYMBOL_SESSION_PRICE_LIMIT_MAX, SYMBOL_SESSION_PRICE_LIMIT_MIN, SYMBOL_SESSION_PRICE_SETTLEMENT, SYMBOL_SWAP_LONG, SYMBOL_SWAP_SHORT, SYMBOL_TRADE_ACCRUED_INTEREST, SYMBOL_TRADE_CONTRACT_SIZE, SYMBOL_TRADE_FACE_VALUE, SYMBOL_TRADE_LIQUIDITY_RATE, SYMBOL_TRADE_TICK_SIZE, SYMBOL_TRADE_TICK_VALUE, SYMBOL_VOLUME_LIMIT, SYMBOL_VOLUME_MAX, SYMBOL_VOLUME_MIN, SYMBOL_VOLUME_STEP }; for(int prop_idx=0; prop_idx<::ArraySize(dbl_props); prop_idx++) { ENUM_SYMBOL_INFO_DOUBLE curr_property=dbl_props[prop_idx]; if(!this.CloneProperty(_origin_symbol,curr_property)) return false; } //--- integer ENUM_SYMBOL_INFO_INTEGER int_props[]= { SYMBOL_BACKGROUND_COLOR, SYMBOL_CHART_MODE, SYMBOL_DIGITS, SYMBOL_EXPIRATION_MODE, SYMBOL_EXPIRATION_TIME, SYMBOL_FILLING_MODE, SYMBOL_MARGIN_HEDGED_USE_LEG, SYMBOL_OPTION_MODE, SYMBOL_OPTION_RIGHT, SYMBOL_ORDER_GTC_MODE, SYMBOL_ORDER_MODE, SYMBOL_SPREAD, SYMBOL_SPREAD_FLOAT, SYMBOL_START_TIME, SYMBOL_SWAP_MODE, SYMBOL_SWAP_ROLLOVER3DAYS, SYMBOL_TICKS_BOOKDEPTH, SYMBOL_TRADE_CALC_MODE, SYMBOL_TRADE_EXEMODE, SYMBOL_TRADE_FREEZE_LEVEL, SYMBOL_TRADE_MODE, SYMBOL_TRADE_STOPS_LEVEL }; for(int prop_idx=0; prop_idx<::ArraySize(int_props); prop_idx++) { ENUM_SYMBOL_INFO_INTEGER curr_property=int_props[prop_idx]; if(!this.CloneProperty(_origin_symbol,curr_property)) return false; } //--- string ENUM_SYMBOL_INFO_STRING str_props[]= { SYMBOL_BASIS, SYMBOL_CURRENCY_BASE, SYMBOL_CURRENCY_MARGIN, SYMBOL_CURRENCY_PROFIT, SYMBOL_DESCRIPTION, SYMBOL_FORMULA, SYMBOL_ISIN, SYMBOL_PAGE, SYMBOL_PATH }; for(int prop_idx=0; prop_idx<::ArraySize(str_props); prop_idx++) { ENUM_SYMBOL_INFO_STRING curr_property=str_props[prop_idx]; if(!this.CloneProperty(_origin_symbol,curr_property)) return false; } //--- history if(_to_msc>0) { if(this.CloneTicks(_origin_symbol)==-1) return false; } //--- return true; } //+------------------------------------------------------------------+
Please note that not all properties can be copied, because some of them are set at the terminal level. Their values can be retrieved, but they cannot be controlled ( get-properties).
An attempt to set a get-property to a custom symbol will return error 5307 (ERR_CUSTOM_SYMBOL_PROPERTY_WRONG). Moreover, there is a separate group of errors for custom symbols under the "Runtime errors" section.
As an example, let us run a simple script TestClone.mql5, which clones a basic symbol. If the cloning attempt is successful, the following log will appear in the Journal.
2019.08.11 19:21:06.402 TestClone (EURUSD,M1) A base symbol "EURUSD" has been successfully cloned.
1.4 The CiCustomSymbol::LoadTicks() method
This method reads ticks from the file and loads them for further use. Note that the method previously deletes the existing tick database for this custom symbol.
//+------------------------------------------------------------------+ //| Load ticks | //+------------------------------------------------------------------+ bool CiCustomSymbol::LoadTicks(const string _src_file_name) { int symbol_digs=(int)this.GetProperty(SYMBOL_DIGITS);; //--- delete ticks if(this.TicksDelete(0,LONG_MAX)<0) return false; //--- open a file CFile curr_file; ::ResetLastError(); int file_ha=curr_file.Open(_src_file_name,FILE_READ|FILE_CSV,','); if(file_ha==INVALID_HANDLE) { ::PrintFormat(__FUNCTION__+": failed to open a %s file!",_src_file_name); return false; } curr_file.Seek(0,SEEK_SET); //--- read data from a file MqlTick batch_arr[]; if(::ArrayResize(batch_arr,m_batch_size)!=m_batch_size) { ::Print(__FUNCTION__+": failed to allocate memory for a batch array!"); return false; } ::ZeroMemory(batch_arr); uint tick_idx=0; bool is_file_ending=false; uint tick_cnt=0; do { is_file_ending=curr_file.IsEnding(); string dates_str[2]; if(!is_file_ending) { //--- time string time_str=::FileReadString(file_ha); if(::StringLen(time_str)<1) { ::Print(__FUNCTION__+": no datetime string - the current tick skipped!"); ::PrintFormat("The unprocessed string: %s",time_str); continue; } string sep="."; ushort u_sep; string result[]; u_sep=::StringGetCharacter(sep,0); int str_num=::StringSplit(time_str,u_sep,result); if(str_num!=4) { ::Print(__FUNCTION__+": no substrings - the current tick skipped!"); ::PrintFormat("The unprocessed string: %s",time_str); continue; } //--- datetime datetime date_time=::StringToTime(result[0]+"."+result[1]+"."+result[2]); long time_msc=(long)(1e3*date_time+::StringToInteger(result[3])); //--- bid double bid_val=::FileReadNumber(file_ha); if(bid_val<.0) { ::Print(__FUNCTION__+": no bid price - the current tick skipped!"); continue; } //--- ask double ask_val=::FileReadNumber(file_ha); if(ask_val<.0) { ::Print(__FUNCTION__+": no ask price - the current tick skipped!"); continue; } //--- volumes for(int jtx=0; jtx<2; jtx++) ::FileReadNumber(file_ha); //--- fill in the current tick MqlTick curr_tick= {0}; curr_tick.time=date_time; curr_tick.time_msc=(long)(1e3*date_time+::StringToInteger(result[3])); curr_tick.bid=::NormalizeDouble(bid_val,symbol_digs); curr_tick.ask=::NormalizeDouble(ask_val,symbol_digs); //--- flags if(m_tick.bid!=curr_tick.bid) curr_tick.flags|=TICK_FLAG_BID; if(m_tick.ask!=curr_tick.ask) curr_tick.flags|=TICK_FLAG_ASK; if(curr_tick.flags==0) curr_tick.flags=TICK_FLAG_BID|TICK_FLAG_ASK;; if(tick_idx==m_batch_size) { //--- add ticks to the custom symbol if(m_is_selected) { if(this.TicksAdd(batch_arr)!=m_batch_size) return false; } else { if(this.TicksReplace(batch_arr)!=m_batch_size) return false; } tick_cnt+=m_batch_size; //--- log for(uint idx=0,batch_idx=0; idx<::ArraySize(dates_str); idx++,batch_idx+=(m_batch_size-1)) dates_str[idx]=::TimeToString(batch_arr[batch_idx].time,TIME_DATE|TIME_SECONDS); ::PrintFormat("\nTicks loaded from %s to %s.",dates_str[0],dates_str[1]); //--- reset ::ZeroMemory(batch_arr); tick_idx=0; } batch_arr[tick_idx]=curr_tick; m_tick=curr_tick; tick_idx++; } //--- end of file else { uint new_size=tick_idx; if(new_size>0) { MqlTick last_batch_arr[]; if(::ArrayCopy(last_batch_arr,batch_arr,0,0,new_size)!=new_size) { ::Print(__FUNCTION__+": failed to copy a batch array!"); return false; } //--- add ticks to the custom symbol if(m_is_selected) { if(this.TicksAdd(last_batch_arr)!=new_size) return false; } else { if(this.TicksReplace(last_batch_arr)!=new_size) return false; } tick_cnt+=new_size; //--- log for(uint idx=0,batch_idx=0; idx<::ArraySize(dates_str); idx++,batch_idx+=(tick_idx-1)) dates_str[idx]=::TimeToString(batch_arr[batch_idx].time,TIME_DATE|TIME_SECONDS); ::PrintFormat("\nTicks loaded from %s to %s.",dates_str[0],dates_str[1]); } } } while(!is_file_ending && !::IsStopped()); ::PrintFormat("\nLoaded ticks number: %I32u",tick_cnt); curr_file.Close(); //--- MqlTick ticks_arr[]; if(::CopyTicks(m_name,ticks_arr,COPY_TICKS_INFO,1,1)!=1) { ::Print(__FUNCTION__+": failed to copy the first tick!"); return false; } m_from_msc=ticks_arr[0].time_msc; if(::CopyTicks(m_name,ticks_arr,COPY_TICKS_INFO,0,1)!=1) { ::Print(__FUNCTION__+": failed to copy the last tick!"); return false; } m_to_msc=ticks_arr[0].time_msc; //--- return true; } //+------------------------------------------------------------------+
In this variant, the following tick structure fields are filled:
struct MqlTick { datetime time; // Last price update time double bid; // Current Bid price double ask; // Current Ask price double last; // Current price of the last trade (Last) ulong volume; // Volume for the current Last price long time_msc; // Last price update time in milliseconds uint flags; // Tick flags double volume_real; // Volume for the current Last price };
The TICK_FLAG_BID|TICK_FLAG_ASK value is set for the first tick flag. Further value depends on which price (bid or ask) has changed. If none of the prices has changed, then they shall be processed as the first tick.
Starting with build 2085, a bar history can be formed by simply loading the tick history. If the history has been loaded, we can programmatically request the bar history .
As an example, let us run the simple script TestLoad.mql5, which loads ticks from a file. The data file must be located under the % MQL5/Files folder. In this example, the file is EURUSD1_tick.csv. It contains EURUSD ticks for August 1 and 2, 2019. Further we will consider tick data sources.
After running the script, the number of loaded ticks will be shown in the journal. In addition, we will double-check the number of available ticks by requesting data from the tick database of the terminal. 354,400 ticks have been copied. Thus, the numbers are equal. We also received 2,697 1-minute bars.
NO 0 15:52:50.149 TestLoad (EURUSD,H1) LN 0 15:52:50.150 TestLoad (EURUSD,H1) Ticks loaded from 2019.08.01 00:00:00 to 2019.08.02 20:59:56. FM 0 15:52:50.152 TestLoad (EURUSD,H1) RM 0 15:52:50.152 TestLoad (EURUSD,H1) Loaded ticks number: 354400 EJ 0 15:52:50.160 TestLoad (EURUSD,H1) Ticks from the file "EURUSD1_tick.csv" have been successfully loaded. DD 0 15:52:50.170 TestLoad (EURUSD,H1) Copied 1-minute rates number: 2697 GL 0 15:52:50.170 TestLoad (EURUSD,H1) The 1st rate time: 2019.08.01 00:00 EQ 0 15:52:50.170 TestLoad (EURUSD,H1) The last rate time: 2019.08.02 20:56 DJ 0 15:52:50.351 TestLoad (EURUSD,H1) Copied ticks number: 354400
Other methods belong to the group of API methods.
2. Tick data. Sources
Tick data form a price series, which has a very active life, in which supply and demand are fighting.
The nature of the price series has long been the subject of discussions of traders and experts. This series is the subject of analysis, the underlying basis for making decisions, etc.
The following ideas are applicable within the article context.
As you know, Forex is the over-the-counter market. Therefore, there are no benchmark quotes. As a result, there are no tick archives (tick history), which can be used for reference. But there are currency futures, most of which are traded on the Chicago Mercantile Exchange. Probably these quotes can serve as some kind of benchmark. However, the historic data cannot be obtained for free. In some cases, you can have a free testing period. But even in this case you will have to register on the exchange website and communicate with the sales manager. On the other hand there is competition between brokers. Therefore , quotes should not differ much between brokers. But usually brokers do not store tick archives and do not make them available for download.
One of the free sources is the Dukascopy Bank website.
After a simple registration, it allows downloading historical data, including ticks.
Rows in the file consist of 5 columns:
- Time
- Ask price
- Bid price
- Volume purchased
- Volume sold
Fig.1 Symbols for downloading on the Data tab of the Quant Data Manager app
The CiCustomSymbol::LoadTicks() method is adjusted to the csv file format used in the above app.
3. Stress testing of trading strategies
Trading strategy testing is a multi-aspect process. Most often "testing" refers to the execution of a trading algorithm using historic quotes (back-testing). But there are other methods to test a trading strategy.
One of them is Stress Testing.
Stress testing is a form of deliberately intense or thorough testing used to determine the stability of a given system or entity.
The idea is simple: create specific conditions for trading strategy operation, which will provide accelerated stress for the strategy. The ultimate goal of such conditions is to check the degree of trading system reliability and its resistance to conditions that have become or may become worse.
3.1 Spread change
The spread factor is extremely important for a trading strategy, as it determines the amount of additional costs. The strategies which aim short-term deals are particularly sensitive to the spread size. In some cases, the ratio of spread to return may exceed 100%.
Let's try to create a custom symbol, which will differ from the basic spread size. For this purpose, let's create a new method CiCustomSymbol::ChangeSpread().
//+------------------------------------------------------------------+ //| Change the initial spread | //| Input parameters: | //| 1) _spread_size - the new fixed value of the spread, pips. | //| If the value > 0 then the spread value is fixed. | //| 2) _spread_markup - a markup for the floating value of the | //| spread, pips. The value is added to the current spread if | //| _spread_size=0. | //| 3) _spread_base - a type of the price to which a markup is | //| added in case of the floating value. | //+------------------------------------------------------------------+ bool CiCustomSymbol::ChangeSpread(const uint _spread_size,const uint _spread_markup=0, const ENUM_SPREAD_BASE _spread_base=SPREAD_BASE_BID) { if(_spread_size==0) if(_spread_markup==0) { ::PrintFormat(__FUNCTION__+": neither the spread size nor the spread markup are set!", m_name,::GetLastError()); return false; } int symbol_digs=(int)this.GetProperty(SYMBOL_DIGITS); ::ZeroMemory(m_tick); //--- copy ticks int tick_idx=0; uint tick_cnt=0; ulong from=1; double curr_point=this.GetProperty(SYMBOL_POINT); int ticks_copied=0; MqlDateTime t1_time; TimeToStruct((int)(m_from_msc/1e3),t1_time); t1_time.hour=t1_time.min=t1_time.sec=0; datetime start_datetime,stop_datetime; start_datetime=::StructToTime(t1_time); stop_datetime=(int)(m_to_msc/1e3); do { MqlTick custom_symbol_ticks[]; ulong t1,t2; t1=(ulong)1e3*start_datetime; t2=(ulong)1e3*(start_datetime+PeriodSeconds(PERIOD_D1))-1; ::ResetLastError(); ticks_copied=::CopyTicksRange(m_name,custom_symbol_ticks,COPY_TICKS_INFO,t1,t2); if(ticks_copied<0) { ::PrintFormat(__FUNCTION__+": failed to copy ticks for a %s symbol! Error code: %d", m_name,::GetLastError()); return false; } //--- there are some ticks for the current day else if(ticks_copied>0) { for(int t_idx=0; t_idx<ticks_copied; t_idx++) { MqlTick curr_tick=custom_symbol_ticks[t_idx]; double curr_bid_pr=::NormalizeDouble(curr_tick.bid,symbol_digs); double curr_ask_pr=::NormalizeDouble(curr_tick.ask,symbol_digs); double curr_spread_pnt=0.; //--- if the spread is fixed if(_spread_size>0) { if(_spread_size>0) curr_spread_pnt=curr_point*_spread_size; } //--- if the spread is floating else { double spread_markup_pnt=0.; if(_spread_markup>0) spread_markup_pnt=curr_point*_spread_markup; curr_spread_pnt=curr_ask_pr-curr_bid_pr+spread_markup_pnt; } switch(_spread_base) { case SPREAD_BASE_BID: { curr_ask_pr=::NormalizeDouble(curr_bid_pr+curr_spread_pnt,symbol_digs); break; } case SPREAD_BASE_ASK: { curr_bid_pr=::NormalizeDouble(curr_ask_pr-curr_spread_pnt,symbol_digs); break; } case SPREAD_BASE_AVERAGE: { double curr_avg_pr=::NormalizeDouble((curr_bid_pr+curr_ask_pr)/2.,symbol_digs); curr_bid_pr=::NormalizeDouble(curr_avg_pr-curr_spread_pnt/2.,symbol_digs); curr_ask_pr=::NormalizeDouble(curr_bid_pr+curr_spread_pnt,symbol_digs); break; } } //--- new ticks curr_tick.bid=curr_bid_pr; curr_tick.ask=curr_ask_pr; //--- flags curr_tick.flags=0; if(m_tick.bid!=curr_tick.bid) curr_tick.flags|=TICK_FLAG_BID; if(m_tick.ask!=curr_tick.ask) curr_tick.flags|=TICK_FLAG_ASK; if(curr_tick.flags==0) curr_tick.flags=TICK_FLAG_BID|TICK_FLAG_ASK; custom_symbol_ticks[t_idx]=curr_tick; m_tick=curr_tick; } //--- replace ticks int ticks_replaced=0; for(int att=0; att<ATTEMTS; att++) { ticks_replaced=this.TicksReplace(custom_symbol_ticks); if(ticks_replaced==ticks_copied) break; ::Sleep(PAUSE); } if(ticks_replaced!=ticks_copied) { ::Print(__FUNCTION__+": failed to replace the refreshed ticks!"); return false; } tick_cnt+=ticks_replaced; } //--- next datetimes start_datetime=start_datetime+::PeriodSeconds(PERIOD_D1); } while(start_datetime<=stop_datetime && !::IsStopped()); ::PrintFormat("\nReplaced ticks number: %I32u",tick_cnt); //--- return true; } //+------------------------------------------------------------------+
How to change the spread size for a custom symbol?
Firstly, a fixed spread can be set. This can be done by specifying a positive value in the _spread_size parameter. Note that this part will work in the Tester, despite the following rule:
In the Strategy Tester, the spread is always considered floating. I.e. SymbolInfoInteger(symbol, SYMBOL_SPREAD_FLOAT) always returns true.
Secondly, a markup can be added to the available spread value. This can be done by defining the _spread_markup parameter.
Also, the method allows specifying the price, which will serve as the reference spread value. This is done by the ENUM_SPREAD_BASE enumeration.
//+------------------------------------------------------------------+ //| Spread calculation base | //+------------------------------------------------------------------+ enum ENUM_SPREAD_BASE { SPREAD_BASE_BID=0, // bid price SPREAD_BASE_ASK=1, // ask price SPREAD_BASE_AVERAGE=2,// average price }; //+------------------------------------------------------------------+
If we use the bid price (SPREAD_BASE_BID), then ask = bid + calculated spread. If we use the ask price (SPREAD_BASE_ASK), then bid = ask - calculated spread. If we use the average price (SPREAD_BASE_AVERAGE), then bid = average price - calculated spread/2.
The CiCustomSymbol::ChangeSpread() method does not change the value of a certain symbol property, but it changes the spread value at each tick. The updated ticks are stored in the tick base.
Check the operation of the method using the TestChangeSpread.mq5 spread. If the script runs fine, the following log will be added in the Journal:
2019.08.30 12:49:59.678 TestChangeSpread (EURUSD,M1) Replaced ticks number: 354400
It means that the tick size has been changed for all previously loaded ticks.
The below table shows my strategy testing results with different spread values, using EURUSD data (Table 1).
Value | Spread 1 (12-17 points) | Spread 2 (25 points) | Spread 2 (50 points) |
---|---|---|---|
Number of trades |
172 | 156 | 145 |
Net Profit, $ |
4 018.27 | 3 877.58 | 3 574.1 |
Max. Equity Drawdown, % |
11.79 | 9.65 | 8.29 |
Profit per Trade, $ |
23.36 | 24.86 | 24.65 |
Table 1. Testing results with different spread values
Column "Spread 1" reflects results with a real floating spread (12-17 points with five-digit quotes).
With a higher spread, the number of trades was less. This resulted in a decrease in drawdown. Moreover, the trade profitability has increased
in this case.
3.2 Changing the Stop and Freeze levels
Some strategies may depend on the stop level and the freeze level. Many brokers provide the stop level equal to spread and the zero freeze level. However, sometimes brokers can increase these levels. Usually this happens during periods of increased volatility or low liquidity. This information can be obtained from the broker's Trading Rules.
Here is an example of an extract from such Trading Rules:
"The minimum distance at which pending orders or T/P and S/L orders can be placed, is equal to the symbol spread. 10 minutes before the release of important macroeconomic statistics or economic news, the minimum distance for S/L orders can be increased to 10 spreads. 30 minutes before trading close time, this level for S/L orders increases to 25 spreads."
These levels (SYMBOL_TRADE_STOPS_LEVEL and SYMBOL_TRADE_FREEZE_LEVEL) can be configured using the CiCustomSymbol::SetProperty() method.
Please note that symbol properties cannot be changed dynamically in the Tester. In the current version, the Tester operates with pre-configured custom symbol parameters. The platform developer introduces significant . Therefore, this feature may possibly be added in future builds.
3.3 Changing margin requirements
Individual margin requirements can also be set for a custom symbol. The values for the following parameters can be set programmatically: SYMBOL_MARGIN_INITIAL, SYMBOL_MARGIN_MAINTENANCE, SYMBOL_MARGIN_HEDGED. Thus, we can define the margin level for the trading instrument.
There are also margin identifiers for separate types of positions and volumes (SYMBOL_MARGIN_LONG, SYMBOL_MARGIN_SHORT etc.). They are set in manual mode.
Leverage variations enable testing of the trading strategy in terms of resistance to drawdowns and ability to avoid stop outs.
Conclusion
This article highlights some aspects of trading strategy stress testing.
Custom symbol settings allow configuring parameters of your own symbols. Every algo trader can select a specific set of parameters, which are required for a specific trading strategy. One of the uncovered interesting options concerns the Market Depth of a custom symbol.
The archive contains a file with ticks, which were processed as part of the examples, as well as the source files of the script and the custom symbol class.
I would like to express my gratitude to fxsaber, to moderators Artyom Trishkin and Slava for interesting discussions in the "Custom symbols. Errors, bugs, questions & suggestions" thread (in Russian).
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/7166
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
Please lead me to know how can i decrease the Ask or Bid Value in to different time of every Bar( for example in 30 minutes time frame:A= Ask value in 10 minute minus from 20 minute, and A value shall be extract from chart to excel file.)
Tanks & Regards