Tired of the psychological stress and time-sink that came with traditional trading but not willing to give up on the potential gains, I decided to translate my trading strategies into code.
The main advantage of this will be excecution of strategy with surgical precision and with far less variance due to the absence of human emotions. At the same time it transforms the active income of trading into a passive one
Developed using the robust MQL4 language and optimized for the renowned MT4 platform, this system has consistently demonstrated remarkable performance since its inception in January 2020. The core design philosophy prioritized safety, recognizing the system's mandate to manage real capital. It further emphasized precision over frequency, resulting in a high win rate with a selective number of trades. Through rigorous filtering of potential trades, the system averages a judicious rate of one trade every two weeks.
While the trade frequency initially raised concerns, the system's impressive win rate of 70% swiftly alleviated those apprehensions. In its debut year, it reported a commendable 18% profit, realized over 20-35 well-curated trades.
Here are some performance metrics for 2020 pulled from Oanda's systems:
Due to the occurrence of alpha decay (in which trading algorithms lose their competitive advantage over time due to the saturation of similar strategies), I won't be sharing the complete codebase. Doing so could likely expedite this decay. Instead, I will provide a more general explanation of how the algorithm operates, excluding the exact parameters used.
In MQL4 the OnTick() function executes on every market tick. Therefore, all our code that needs to be executed in real-time will be contained within it.
void OnTick(){//code that will be executed on every market tick}
Since all decisions start with information, the first order of business is to allow Acedia to receive real-time information about the instrument it will be trading. Acedia uses the closing price of each tick as well as a few technical indicators. In the example below I show how the EMAs (Exponential Moving Averages) can be obtained.
double EMA_50_0 = iMA(NULL,0,50,0,MODE_EMA,PRICE_CLOSE,0); // current 50 day EMA
double EMA_50_1 = iMA(NULL,0,50,0,MODE_EMA,PRICE_CLOSE,1); // 1 bar ago 50 day EMA
double EMA_100_0 = iMA(NULL,0,100,0,MODE_EMA,PRICE_CLOSE,3); // current 100 day EMA
double EMA_100_1 = iMA(NULL,0,100,0,MODE_EMA,PRICE_CLOSE,4); // 1 bar ago 100 day EMA
...
Note: there are many indicators available for you to use, this is just one example. If you want to find out more, the official documentation is here.
Next, comes the many Boolean operations that monitor for certain market conditions. The example code below checks for uptrends in the EMA indicator. For the sake of the example, an uptrend is detected when the EMA_50 is above the EMA_100 for 3 consequetive time periods.
EMA_UpTrend = false;
if(EMA_50_0 > EMA_100_0 && EMA_50_1 > EMA_100_1 && EMA_50_2 > EMA_100_2){
EMAUpTrend = true;
}
else EMAUpTrend = false;
if(EMA_50_0 < EMA_100_0 && EMA_50_1 < EMA_100_1 && EMA_50_2 < EMA_100_2){
EMADownTrend = true;
}
else EMADownTrend = false;
It looks simple, and in a way it is since its just regular if-else checking after all. However, the difficulty comes from tweaking these conditions endlessly until you get a check that is actually useful for Acedia to take into account. For reference: the code I wrote for these Boolean Operations alone come in at around 150 lines.
Following this is Order Management. Assuming that we have obtained the information we need, Acedia needs a way to make a decision based on that information. However, since we are still currently inside the OnTick() function, if we were to write code that places a trade, it would be executed multiple times almost instantaneously. What we want is to allow only one trade to be executed at a time.
To do this, we wrap the subsequent lines of code in an if statement to make them execute only once per bar (i.e once every 15mins/30mins/etc.).
//Outside OnTick()
datetime = LastActionTime; // Current Time
//Back Inside OnTick()
if (LastActionTime != Time[0]){ //Time[0] is the starting time for each bar
//code here executes only once per bar
}
Since we only want one trade to be open at a time, before we open any other orders, we should first check if there are any orders currently open. To do this we use the OrdersTotal() function which returns the number of market and pending orders in an if statement (i.e if there are any orders currently open).
if(OrdersTotal() < 1){ // i.e if there are 0 orders open
//code here will only execute if there are no open orders
}
To execute an order we will use the OrderSend() function. More information about it available here.
// To execute a BUY
OrderSend(NULL,OP_BUY,Lotsize1,Ask,Slippage,Ask-(StopLoss1*nDigits),Ask+(TakeProfit1*nDigits),NULL,1,0,Green)
// To execute a SELL
OrderSend(NULL,OP_SELL,Lotsize1,Bid,Slippage,Bid+(StopLoss1*nDigits),Bid-(TakeProfit1*nDigits),NULL,1,0,Red
To decide when to make a trade, we simply use an if statement to trigger it like so:
if (EMAUptrend == false){// BUY Conditions
if(OrderSend(NULL,OP_BUY,Lotsize,Ask,Slippage,Ask-(StopLoss*nDigits),Ask+(TakeProfit*nDigits),NULL,1,0,Green)){
Alert("Open Buy - V.High MACD");
}
else{
Alert("Failed to Open Buy - V.High MACD due to : ", GetLastError());
}
}
else if (EMAUpTrend == true){// SELL Conditions
if(OrderSend(NULL,OP_SELL,Lotsize,Bid,Slippage,Bid+(StopLoss*nDigits),Bid-(TakeProfit*nDigits),NULL,1,0,Red)){
Alert("Open Sell - V.High MACD");
}
else{
Alert("Failed to Open Sell - V.High MACD due to : ", GetLastError());
}
}
Note: The actual OrderSend() function is itself wrapped inside an if statement to check if the trade was executed properly, and report it. There are also variables used in as the function's arguments that I will define later on (Lotsize, Ask, Slippage, Stoploss, nDigits, TakeProfit).
Now that Acedia can place trades, we must then enable it to close trades as well. To do this we must first use OrdersTotal() to check if there are any open orders. If so, we then iterate through the orders using a for loop and the OrderSelect() function. Once we've selected an open trade, we use the OrderType() function to check if they are a BUY or a SELL. Then we further narrow down on the order using the OrderMagicNumber() function which returns a unique number assigned to a trade (this number is specified in our code that sends an order). Finally, we use a boolean statement to check for the closing conditions and then execute the close using the OrderClose() function.
if(OrdersTotal()>0){ // checks if there are any open orders
for(int i=OrdersTotal()-1; i >= 0; i--){ // iterates through the open orders
if(OrderSelect(i,SELECT_BY_POS,MODE_TRADES)){ //selects the order according to the for loop
if(OrderType() == OP_BUY){// checks for type of order
if(OrderMagicNumber() == 1){// checks for the magic number assigned to the trade
if((EMA_50_1 < EMA_50_0)){ // checks closing conditions
OrderClose(OrderTicket(),OrderLots(),Bid,Slippage,Green); // closes the order
}
}
}
}
}
}
Note: the magic number is a number assigned to each trade to identify them. Since we are only allowing 1 trade to be open at all times, it can effectively be used to determine which opening conditions triggered the trade in the first place. This gives us more precision in tailoring our close conditions to our opening conditions. The example code above can should be repeated for each different opening condition specified with its own unique close condition.
Now we get to the most important part, risk management. To be added...
This was my first coding project and I am really proud of it. It contains 680 lines of code and does not require expert-level programming to pull-off. The difficulty came in having to do excruciating amounts of research and backtesting. In total, it took me about 3 months to write the first version and another 6 months of live-testing and tweaking before I arrived at the current version.
Moving forward, I would like to incorporate some form of Reinforcement Learning into Acedia. But currently, I have not found the time or patience to do so since I would need to redesign the entire codebase in Python and that sounds very boring. Since it is working well, I have decided to put this off till a later date and work on other projects in the meantime.