Inclustion of example EA
This commit is contained in:
@@ -1,282 +0,0 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| CalculatePositionData.mqh |
|
||||
//| xMattC |
|
||||
//+------------------------------------------------------------------+
|
||||
#property library
|
||||
#include <Trade/Trade.mqh>
|
||||
#include <MyLibs/TimeZones.mqh>
|
||||
#include <MyLibs/Myfunctions.mqh>
|
||||
|
||||
class CalculatePositionData : public CObject{
|
||||
|
||||
protected:
|
||||
CTrade trade;
|
||||
TimeZones tz;
|
||||
CPositionInfo position;
|
||||
MyFunctions mf;
|
||||
|
||||
bool check_lots(double &lots, string symbol);
|
||||
bool normalise_price(double price, double &normalizedPrice, string symbol);
|
||||
// double adjusted_point(string symbol);
|
||||
|
||||
public:
|
||||
|
||||
double calculate_stoploss(string symbol, double price, int order_side, string _sl_mode, double sl_var, ENUM_TIMEFRAMES atr_period);
|
||||
double calculate_take_profit(string symbol, double price, double stoploss, int order_side, string mode_tp, double tp_var, ENUM_TIMEFRAMES atr_period);
|
||||
double calculate_lots(string symbol, double sl_distance, double price, string mode_lot, double lot_var);
|
||||
double calculate_trading_cost(string symbol, ulong position_ticket);
|
||||
|
||||
};
|
||||
|
||||
double CalculatePositionData::calculate_stoploss(string symbol, double price, int order_side, string mode_sl, double sl_var, ENUM_TIMEFRAMES atr_period){
|
||||
// order_side int must be 1 for BUY or 2 for
|
||||
|
||||
double sl=0;
|
||||
|
||||
if(mode_sl=="NO_STOPLOSS"){
|
||||
sl=0;
|
||||
}
|
||||
|
||||
if(mode_sl=="SL_BREAKEVEN"){
|
||||
// https://www.youtube.com/watch?v=idPulZ3_iR0
|
||||
Alert("Not implemented yet yet");
|
||||
}
|
||||
|
||||
if(mode_sl=="SL_FIXED_PIPS"){
|
||||
// pips/poins = https://www.mql5.com/en/forum/187757
|
||||
double adj_point = mf.adjusted_point(symbol);
|
||||
|
||||
if(order_side == 1){
|
||||
sl = price - sl_var * adj_point;
|
||||
if(!normalise_price(sl,sl,symbol)){return false;}
|
||||
}
|
||||
if(order_side == 2){
|
||||
sl = price + sl_var * adj_point;
|
||||
if(!normalise_price(sl,sl,symbol)){return false;}
|
||||
}
|
||||
}
|
||||
|
||||
if(mode_sl=="SL_FIXED_PERCENT"){
|
||||
if(order_side == 1){
|
||||
sl = (-1.0 * sl_var * price / 100.00) + price;
|
||||
if(!normalise_price(sl,sl,symbol)){return false;}
|
||||
}
|
||||
if(order_side == 2){
|
||||
sl = sl_var * price / 100.00 + price;
|
||||
if(!normalise_price(sl,sl,symbol)){return false;}
|
||||
}
|
||||
}
|
||||
|
||||
if(mode_sl=="SL_ATR_MULTIPLE"){
|
||||
|
||||
int atr_handle = iATR(symbol,atr_period,14);
|
||||
double atr[];
|
||||
ArraySetAsSeries(atr,true);
|
||||
CopyBuffer(atr_handle,MAIN_LINE,1,1,atr);
|
||||
|
||||
if(order_side == 1){
|
||||
sl = price - (atr[0] * sl_var);
|
||||
if(!normalise_price(sl,sl,symbol)){return false;}
|
||||
|
||||
}
|
||||
if(order_side == 2){
|
||||
sl = price + (atr[0] * sl_var);
|
||||
if(!normalise_price(sl,sl,symbol)){return false;}
|
||||
}
|
||||
}
|
||||
|
||||
if(mode_sl=="SL_SPECIFIED_VALUE"){
|
||||
|
||||
double adj_point = mf.adjusted_point(symbol);
|
||||
|
||||
if(order_side == 1){
|
||||
|
||||
double pip_50_sl = price - 10 * adj_point;
|
||||
if(sl_var >= pip_50_sl){
|
||||
sl = pip_50_sl;
|
||||
}
|
||||
else sl = sl_var;
|
||||
|
||||
if(!normalise_price(sl,sl,symbol)){return false;}
|
||||
}
|
||||
if(order_side == 2){
|
||||
double pip_50_sl = price + 10 * adj_point;
|
||||
if(sl_var <= pip_50_sl){
|
||||
sl = pip_50_sl;
|
||||
}
|
||||
else sl = sl_var;
|
||||
|
||||
sl = sl = sl_var;
|
||||
if(!normalise_price(sl,sl,symbol)){return false;}
|
||||
}
|
||||
}
|
||||
|
||||
return sl;
|
||||
}
|
||||
|
||||
double CalculatePositionData::calculate_take_profit(string symbol, double price, double stoploss, int order_side, string mode_tp, double _tp_var, ENUM_TIMEFRAMES atr_period){
|
||||
// order_side int must be 1 for BUY or 2 for SELL
|
||||
|
||||
double tp=0;
|
||||
|
||||
if(mode_tp=="NO_TAKE_PROFIT"){
|
||||
tp=0;
|
||||
}
|
||||
|
||||
if(mode_tp=="TP_FIXED_PIPS"){
|
||||
|
||||
double adj_point = mf.adjusted_point(symbol);
|
||||
if(order_side == 1){
|
||||
tp = price + _tp_var * adj_point;
|
||||
if(!normalise_price(tp,tp,symbol)){return false;}
|
||||
}
|
||||
if(order_side == 2){
|
||||
tp = price - _tp_var * adj_point;
|
||||
if(!normalise_price(tp,tp,symbol)){return false;}
|
||||
}
|
||||
}
|
||||
|
||||
if(mode_tp=="TP_FIXED_PERCENT"){
|
||||
if(order_side == 1){
|
||||
tp = _tp_var * price / 100.00 + price;
|
||||
if(!normalise_price(tp,tp,symbol)){return false;}
|
||||
}
|
||||
if(order_side == 2){
|
||||
tp = (-1 * _tp_var * price / 100.00) + price;
|
||||
if(!normalise_price(tp,tp,symbol)){return false;}
|
||||
}
|
||||
}
|
||||
|
||||
if(mode_tp=="TP_ATR_MULTIPLE"){
|
||||
|
||||
int atr_handle = iATR(symbol,atr_period,14);
|
||||
double atr[];
|
||||
ArraySetAsSeries(atr,true);
|
||||
CopyBuffer(atr_handle,MAIN_LINE,1,1,atr);
|
||||
|
||||
if(order_side == 1){
|
||||
tp = price + (atr[0] * _tp_var);
|
||||
if(!normalise_price(tp,tp,symbol)){return false;}
|
||||
}
|
||||
if(order_side == 2){
|
||||
tp = price - (atr[0] * _tp_var);
|
||||
if(!normalise_price(tp,tp,symbol)){return false;}
|
||||
}
|
||||
}
|
||||
|
||||
if(mode_tp=="TP_SL_MULTIPLE"){
|
||||
if(order_side == 1){
|
||||
double sl_size = price - stoploss;
|
||||
tp = price + (_tp_var * sl_size);
|
||||
if(!normalise_price(tp,tp,symbol)){return false;}
|
||||
}
|
||||
if(order_side == 2){
|
||||
double sl_size = stoploss - price;
|
||||
tp = price - (_tp_var * sl_size);
|
||||
if(!normalise_price(tp,tp,symbol)){return false;}
|
||||
}
|
||||
}
|
||||
|
||||
if(mode_tp=="TP_SPECIFIED_VALUE"){
|
||||
|
||||
if(_tp_var!=0){
|
||||
double adj_point = mf.adjusted_point(symbol);
|
||||
|
||||
if(order_side == 1){
|
||||
double pip_limit = price + 10 * adj_point;
|
||||
if(_tp_var <= pip_limit){
|
||||
tp = pip_limit;
|
||||
}
|
||||
else tp = _tp_var;
|
||||
|
||||
if(!normalise_price(tp,tp,symbol)){return false;}
|
||||
}
|
||||
if(order_side == 2){
|
||||
double pip_limit = price - 10 * adj_point;
|
||||
if(_tp_var >= pip_limit){
|
||||
tp = pip_limit;
|
||||
}
|
||||
else tp = _tp_var;
|
||||
tp = tp = _tp_var;
|
||||
if(!normalise_price(tp,tp,symbol)){return false;}
|
||||
}
|
||||
}
|
||||
}
|
||||
return tp;
|
||||
|
||||
}
|
||||
|
||||
double CalculatePositionData::calculate_lots(string symbol, double sl_distance, double price, string mode_lot, double lot_var){
|
||||
|
||||
double lots = 0;
|
||||
double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
|
||||
double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
|
||||
double volume_step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
|
||||
|
||||
double account_value = fmin(fmin(AccountInfoDouble(ACCOUNT_EQUITY),AccountInfoDouble(ACCOUNT_BALANCE)),AccountInfoDouble(ACCOUNT_MARGIN_FREE));
|
||||
double risk_money = account_value * lot_var / 100;
|
||||
|
||||
if(mode_lot=="LOT_MODE_FIXED"){
|
||||
lots = lot_var;
|
||||
}
|
||||
|
||||
if(mode_lot=="LOT_MODE_PCT_RISK"){
|
||||
double money_lot_step = (sl_distance / tick_size) * tick_value * volume_step;
|
||||
lots = MathFloor(risk_money/money_lot_step) * volume_step;
|
||||
}
|
||||
|
||||
if(mode_lot=="LOT_MODE_PCT_ACCOUNT"){
|
||||
double money_lot_step = (price / tick_size) * tick_value * volume_step;
|
||||
lots = MathFloor(risk_money/money_lot_step) * volume_step;
|
||||
}
|
||||
|
||||
if(!check_lots(lots, symbol)){return false;}
|
||||
return lots;
|
||||
}
|
||||
|
||||
bool CalculatePositionData::check_lots(double &lots, string symbol){
|
||||
|
||||
double min = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MIN);
|
||||
double max = SymbolInfoDouble(symbol, SYMBOL_VOLUME_MAX);
|
||||
double step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
|
||||
|
||||
if(lots<min){
|
||||
Print("Lot size will be set to minimum allowed volume");
|
||||
lots = min;
|
||||
return true;
|
||||
}
|
||||
|
||||
if(lots>max){
|
||||
Print("Lot size greater than maximum allowed volume. lots:",lots,"max:",max);
|
||||
return false;
|
||||
}
|
||||
|
||||
lots = (int)MathFloor(lots/step) * step;
|
||||
return true;
|
||||
}
|
||||
|
||||
bool CalculatePositionData::normalise_price(double price, double &normalizedPrice, string symbol){
|
||||
double tickSize;
|
||||
if(!SymbolInfoDouble(symbol,SYMBOL_TRADE_TICK_SIZE,tickSize)){
|
||||
Print("Failed to get tick size");
|
||||
return false;
|
||||
}
|
||||
int symbol_digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
|
||||
normalizedPrice = NormalizeDouble(MathRound(price/tickSize)*tickSize, symbol_digits);
|
||||
return true;
|
||||
}
|
||||
|
||||
double CalculatePositionData::calculate_trading_cost(string symbol, ulong position_ticket){
|
||||
|
||||
position.SelectByTicket(position_ticket);
|
||||
|
||||
double swap = PositionGetDouble(POSITION_SWAP);
|
||||
double commission = PositionGetDouble(POSITION_COMMISSION);
|
||||
double tick_size = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_SIZE);
|
||||
double tick_value = SymbolInfoDouble(symbol, SYMBOL_TRADE_TICK_VALUE);
|
||||
double lot_step = SymbolInfoDouble(symbol, SYMBOL_VOLUME_STEP);
|
||||
double lots = PositionGetDouble(POSITION_VOLUME);
|
||||
double trading_cost = -1 * ((commission + swap) / tick_value * tick_size / lots);
|
||||
|
||||
return trading_cost;
|
||||
}
|
||||
@@ -1,209 +0,0 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| DrawdownControl.mqh |
|
||||
//| xMattC |
|
||||
//+------------------------------------------------------------------+
|
||||
#property library
|
||||
#include <Trade/Trade.mqh>
|
||||
#include <MyLibs/MyFunctions.mqh>
|
||||
|
||||
class DrawdownControl : public CObject {
|
||||
protected:
|
||||
CTrade trade;
|
||||
MyFunctions mf;
|
||||
|
||||
string data_file;
|
||||
double daily_max_dd_per;
|
||||
string daily_reset_time;
|
||||
bool print_statments;
|
||||
|
||||
double acc_max_dd_per;
|
||||
double equaty_control_high;
|
||||
double equaty_control_low;
|
||||
|
||||
|
||||
double daily_equity_start;
|
||||
double daily_max_dd_target;
|
||||
bool daily_dd_limit_reached;
|
||||
|
||||
bool write_global_var_data();
|
||||
bool print_messages();
|
||||
|
||||
public:
|
||||
void init_dd_control(string inp_data_file, double inp_acc_max_dd_per, double inp_daily_max_dd_per, string inp_daily_reset_time, bool inp_print_statments = true);
|
||||
bool determine_daily_dd_limit();
|
||||
double lot_correction_factor(double acc_equity_start, double min_lot_factor, double max_lot_factor, bool dynm_lot_factor=false, double dlf_trail_per=20);
|
||||
double lot_correction_dynamic(double acc_dd_percent, double min_lot_factor, double max_lot_factor);
|
||||
};
|
||||
|
||||
void DrawdownControl::init_dd_control(string inp_data_file, double inp_acc_max_dd_per, double inp_daily_max_dd_per, string inp_daily_reset_time, bool inp_print_statments = true) {
|
||||
|
||||
data_file = inp_data_file;
|
||||
acc_max_dd_per = inp_acc_max_dd_per;
|
||||
daily_max_dd_per = inp_daily_max_dd_per;
|
||||
daily_reset_time = inp_daily_reset_time;
|
||||
print_statments = inp_print_statments;
|
||||
|
||||
// If no data file exisits, create one and set global vairiables:
|
||||
if(FileIsExist(data_file) == false) {
|
||||
daily_equity_start = AccountInfoDouble(ACCOUNT_EQUITY);
|
||||
daily_max_dd_target = daily_equity_start - (daily_equity_start * (daily_max_dd_per / 100));
|
||||
daily_dd_limit_reached = false;
|
||||
equaty_control_high = 9999999;
|
||||
equaty_control_low = 0;
|
||||
write_global_var_data();
|
||||
}
|
||||
// If file exisits read file:
|
||||
if(FileIsExist(data_file) == true) {
|
||||
|
||||
int file_handle = FileOpen(data_file, FILE_READ | FILE_ANSI | FILE_TXT);
|
||||
if(file_handle == INVALID_HANDLE) {
|
||||
Print("Error opening file: ", data_file);
|
||||
}
|
||||
|
||||
// If data file is older than 24h 10min create a new file and reset global vars:
|
||||
long modifided_date = FileGetInteger(file_handle, FILE_MODIFY_DATE);
|
||||
long time_delta = ((long)TimeCurrent() - modifided_date) / 60;
|
||||
|
||||
if(time_delta >= 1450) {
|
||||
daily_equity_start = AccountInfoDouble(ACCOUNT_EQUITY);
|
||||
daily_max_dd_target = daily_equity_start - (daily_equity_start * (daily_max_dd_per / 100));
|
||||
daily_dd_limit_reached = false;
|
||||
equaty_control_high = equaty_control_high;
|
||||
equaty_control_low = equaty_control_low;
|
||||
write_global_var_data();
|
||||
Print(data_file, " is older than 24h and 10min; global vars reset!");
|
||||
}
|
||||
// If data file is younger than 24h+10 min read data and set global vars:
|
||||
else {
|
||||
daily_equity_start = (double)FileReadString(file_handle, 0);
|
||||
daily_max_dd_target = (double)FileReadString(file_handle, 1);
|
||||
daily_dd_limit_reached = FileReadBool(file_handle);
|
||||
equaty_control_high = (double)FileReadString(file_handle, 3);
|
||||
equaty_control_low = (double)FileReadString(file_handle, 4);;
|
||||
}
|
||||
FileClose(file_handle);
|
||||
}
|
||||
print_messages();
|
||||
}
|
||||
|
||||
bool DrawdownControl::determine_daily_dd_limit() {
|
||||
|
||||
// Reset max equity at the start of each day:
|
||||
string ct = TimeToString(TimeCurrent(), TIME_MINUTES);
|
||||
if(ct == daily_reset_time) {
|
||||
daily_equity_start = AccountInfoDouble(ACCOUNT_EQUITY);
|
||||
daily_max_dd_target = (daily_equity_start - (daily_equity_start * (daily_max_dd_per / 100)));
|
||||
daily_dd_limit_reached = false;
|
||||
write_global_var_data();
|
||||
print_messages();
|
||||
}
|
||||
|
||||
// If in drawdown close all positions and delete orders
|
||||
if(daily_dd_limit_reached || AccountInfoDouble(ACCOUNT_EQUITY) <= daily_max_dd_target) {
|
||||
|
||||
if(daily_dd_limit_reached == false) {
|
||||
daily_dd_limit_reached = true;
|
||||
write_global_var_data();
|
||||
print_messages();
|
||||
}
|
||||
|
||||
for(int i = PositionsTotal() - 1; i >= 0; i--) {
|
||||
ulong ticket = PositionGetTicket(i);
|
||||
trade.PositionClose(ticket);
|
||||
}
|
||||
|
||||
for(int i = OrdersTotal() - 1; i >= 0; i--) {
|
||||
ulong ticket = OrderGetTicket(i);
|
||||
trade.OrderDelete(ticket);
|
||||
}
|
||||
}
|
||||
return daily_dd_limit_reached;
|
||||
}
|
||||
|
||||
// Reduces lot size as account apporchaes max allowed drawdown limit.
|
||||
double DrawdownControl::lot_correction_factor(double acc_equity_start, double min_lot_factor, double max_lot_factor, bool dynm_lot_factor=false, double dlf_trail_per=20) {
|
||||
|
||||
double account_value = fmin(AccountInfoDouble(ACCOUNT_EQUITY), AccountInfoDouble(ACCOUNT_BALANCE));
|
||||
double lot_factor;
|
||||
|
||||
// Interpolate to find lot factor between given min and max values.
|
||||
if (account_value < acc_equity_start){
|
||||
|
||||
double acc_equity_min = acc_equity_start - (acc_equity_start * (acc_max_dd_per / 100));
|
||||
double y1 = min_lot_factor;
|
||||
double y2 = max_lot_factor;
|
||||
double x1 = acc_equity_min;
|
||||
double x = account_value;
|
||||
double x2 = acc_equity_start;
|
||||
lot_factor = y1 + (x - x1) * ((y2 - y1) / (x2 - x1));
|
||||
}
|
||||
|
||||
else if(account_value >= acc_equity_start) {
|
||||
|
||||
if(dynm_lot_factor=true){
|
||||
lot_factor = lot_correction_dynamic(dlf_trail_per, min_lot_factor, max_lot_factor);
|
||||
}
|
||||
|
||||
else {
|
||||
lot_factor = max_lot_factor;
|
||||
}
|
||||
}
|
||||
return max_lot_factor;
|
||||
}
|
||||
|
||||
|
||||
double DrawdownControl::lot_correction_dynamic(double acc_dd_percent, double min_lot_factor, double max_lot_factor) {
|
||||
|
||||
double account_value = fmin(AccountInfoDouble(ACCOUNT_EQUITY), AccountInfoDouble(ACCOUNT_BALANCE));
|
||||
double trail_point = account_value - (account_value * (acc_dd_percent / 100));
|
||||
|
||||
if(equaty_control_low < trail_point){
|
||||
equaty_control_low = trail_point;
|
||||
}
|
||||
|
||||
if(equaty_control_high < account_value){
|
||||
equaty_control_high = account_value;
|
||||
}
|
||||
|
||||
if(account_value < equaty_control_low){
|
||||
equaty_control_low = account_value;
|
||||
equaty_control_high = account_value + (account_value * (acc_dd_percent / 100));
|
||||
}
|
||||
|
||||
// back-up to file every hour:
|
||||
if(mf.is_new_bar(_Symbol, PERIOD_H1) == true){
|
||||
write_global_var_data();
|
||||
}
|
||||
|
||||
// Linear interpolation:
|
||||
double y1 = min_lot_factor;
|
||||
double y2 = max_lot_factor;
|
||||
double x1 = equaty_control_low;
|
||||
double x = account_value;
|
||||
double x2 = equaty_control_high;
|
||||
|
||||
double y = y1 + (x - x1) * ((y2 - y1) / (x2 - x1));
|
||||
|
||||
return y;
|
||||
}
|
||||
|
||||
bool DrawdownControl::write_global_var_data() {
|
||||
int file_handle = FileOpen(data_file, FILE_WRITE | FILE_ANSI | FILE_TXT);
|
||||
FileWrite(file_handle, daily_equity_start);
|
||||
FileWrite(file_handle, daily_max_dd_target);
|
||||
FileWrite(file_handle, daily_dd_limit_reached);
|
||||
FileClose(file_handle);
|
||||
Print(data_file, " written");
|
||||
return true;
|
||||
}
|
||||
|
||||
bool DrawdownControl::print_messages() {
|
||||
if(print_statments == true) {
|
||||
Print("TimeCurrent(): ", TimeToString(TimeCurrent()));
|
||||
Print("Daily Equity Start: ", (int)daily_equity_start);
|
||||
Print("Current Equity: ", (int)AccountInfoDouble(ACCOUNT_EQUITY));
|
||||
Print("Daily Drawdown Limit: ", (int)daily_max_dd_target, " (", daily_max_dd_per, "%) of DES");
|
||||
Print("Daily Drawdown Limit Hit: ", daily_dd_limit_reached);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
@@ -1,216 +0,0 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| MyFunctions.mqh |
|
||||
//| xMattC |
|
||||
//+------------------------------------------------------------------+
|
||||
#property library
|
||||
#include <Trade/Trade.mqh>
|
||||
#include <MyLibs/TradingWindow.mqh>
|
||||
#include <MyLibs/MyEnums.mqh>
|
||||
|
||||
class MyFunctions : public CObject{
|
||||
|
||||
protected:
|
||||
CTrade trade;
|
||||
TradingWindow tw;
|
||||
datetime previousTime;
|
||||
datetime bar_open_time;
|
||||
|
||||
public:
|
||||
void draw_line(double value, string name,color clr);
|
||||
bool check_indicator_handles(int &indicator_handles[]);
|
||||
double adjusted_point(string symbol);
|
||||
double get_bid_ask_price(string symbol, int price_side);
|
||||
bool is_new_bar(string symbol, ENUM_TIMEFRAMES time_frame, string daily_start_time="00:10");
|
||||
bool trade_window(string t1, string t2, string time_zone, bool plot_range_inp=true);
|
||||
bool in_test_period(MODE_SPLIT_DATA data_period);
|
||||
void get_white_list(MULTI_SYM_MODE mode, string& DataArray[]);
|
||||
|
||||
};
|
||||
|
||||
void MyFunctions::get_white_list(MULTI_SYM_MODE mode, string& DataArray[]){
|
||||
|
||||
if(mode==MULTI_SYM_CHART){
|
||||
string a[] = {_Symbol};
|
||||
ArrayResize(DataArray, ArraySize(a));
|
||||
for(int i = 0; i < ArraySize(DataArray); i++){
|
||||
DataArray[i]=a[i];
|
||||
}
|
||||
}
|
||||
if(mode==MULTI_SYM_FX_B5){
|
||||
string a[] = {"EURUSD", "AUDNZD", "EURGBP", "AUDCAD", "CHFJPY"};
|
||||
ArrayResize(DataArray, ArraySize(a));
|
||||
for(int i = 0; i < ArraySize(DataArray); i++){
|
||||
DataArray[i]=a[i];
|
||||
}
|
||||
}
|
||||
if(mode==MULTI_SYM_FX_28){
|
||||
string a[] = {"EURUSD","AUDNZD","AUDUSD","AUDJPY","EURCHF","EURGBP","EURJPY","GBPCHF","GBPJPY","GBPUSD","NZDUSD","USDCAD","USDCHF","USDJPY","CADJPY","EURAUD","CHFJPY","EURCAD","AUDCAD","AUDCHF","CADCHF","EURNZD","GBPAUD","GBPCAD","GBPNZD","NZDCAD","NZDCHF","NZDJPY",};
|
||||
ArrayResize(DataArray, ArraySize(a));
|
||||
for(int i = 0; i < ArraySize(DataArray); i++){
|
||||
DataArray[i]=a[i];
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MyFunctions::trade_window(string t1, string t2, string time_zone="Broker", bool plot_range_inp=true){
|
||||
bool in_window = tw.define_window(t1, t2, time_zone, plot_range_inp);
|
||||
return in_window;
|
||||
}
|
||||
|
||||
//if(!mf.is_new_bar(symbol, PERIOD_D1, "00:06")){return;}
|
||||
bool MyFunctions::is_new_bar(string symbol, ENUM_TIMEFRAMES time_frame, string daily_start_time="00:10"){
|
||||
|
||||
bar_open_time = iTime(symbol, time_frame, 0);
|
||||
if(previousTime!=bar_open_time){
|
||||
|
||||
if(PeriodSeconds(time_frame)==PeriodSeconds(PERIOD_D1)){
|
||||
if(TimeCurrent() > StringToTime(daily_start_time)){
|
||||
previousTime=bar_open_time;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
else{
|
||||
previousTime=bar_open_time;
|
||||
return true;
|
||||
}
|
||||
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
//if(!mf.in_test_period(data_split_method){return;}
|
||||
bool MyFunctions::in_test_period(MODE_SPLIT_DATA data_split_method){
|
||||
|
||||
string result[];
|
||||
string string_tc = TimeToString(TimeCurrent());
|
||||
ushort u_sep = StringGetCharacter(".",0);
|
||||
int split_string = StringSplit(string_tc, u_sep, result);
|
||||
bool odd_year = int(result[0]) % 2;
|
||||
bool odd_month = int(result[1]) % 2;
|
||||
|
||||
// get week of the year. rough estimate can be late the first week of jan:
|
||||
MqlDateTime dt;
|
||||
TimeToStruct(TimeCurrent(),dt);
|
||||
int iDay = (dt.day_of_week + 6 ) % 7 + 1; // convert day to standard index (1=Mon,...,7=Sun)
|
||||
int iWeek = (dt.day_of_year - iDay + 10 ) / 7; // calculate standard week number
|
||||
|
||||
bool odd_week = iWeek % 2;
|
||||
|
||||
|
||||
if(data_split_method==NO_SPLIT){
|
||||
return true;
|
||||
}
|
||||
|
||||
if(data_split_method==ODD_YEARS){
|
||||
if (odd_year){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if(data_split_method==EVEN_YEARS){
|
||||
if (!odd_year){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if(data_split_method==ODD_MONTHS){
|
||||
if (odd_month){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if(data_split_method==EVEN_MONTHS){
|
||||
|
||||
if (!odd_month){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if(data_split_method==ODD_WEEKS){
|
||||
if (odd_week){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
if(data_split_method==EVEN_WEEKS){
|
||||
|
||||
if (!odd_week){
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
void MyFunctions::draw_line(double value, string name,color clr=clrBlack){
|
||||
// EG:
|
||||
// ArrayResize(bar,1000);
|
||||
// ArraySetAsSeries(bar, true);
|
||||
// CopyRates(symbol,PERIOD_CURRENT,1,1000,bar);
|
||||
// double close = bar[0].close;
|
||||
// draw_line(close,"CLOSE",clrBlue);
|
||||
|
||||
if(ObjectFind(0,name)<0){
|
||||
ResetLastError();
|
||||
|
||||
if(!ObjectCreate(0,name,OBJ_HLINE,0,0,value)){
|
||||
Print(__FUNCTION__,": failed to create a horizontal line! Error code = ",GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
ObjectSetInteger(0,name,OBJPROP_COLOR,clr);
|
||||
ObjectSetInteger(0,name,OBJPROP_STYLE,STYLE_SOLID);
|
||||
ObjectSetInteger(0,name,OBJPROP_WIDTH,1);
|
||||
}
|
||||
|
||||
ResetLastError();
|
||||
|
||||
if(!ObjectMove(0,name,0,0,value)){
|
||||
Print(__FUNCTION__,": failed to move the horizontal line! Error code = ",GetLastError());
|
||||
return;
|
||||
}
|
||||
|
||||
ChartRedraw();
|
||||
}
|
||||
|
||||
double MyFunctions::adjusted_point(string symbol){
|
||||
|
||||
int symbol_digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
|
||||
int digits_adjust=1;
|
||||
if(symbol_digits==3 || symbol_digits==5){
|
||||
digits_adjust=10;
|
||||
}
|
||||
|
||||
double symbol_point_val = SymbolInfoDouble(symbol,SYMBOL_POINT);
|
||||
double m_adjusted_point;
|
||||
m_adjusted_point = symbol_point_val * digits_adjust;
|
||||
|
||||
return m_adjusted_point;
|
||||
|
||||
}
|
||||
// price side - 1 for the ask price and 2 for the bid price
|
||||
double MyFunctions::get_bid_ask_price(string symbol, int price_side){
|
||||
|
||||
int symbol_digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
|
||||
double symbol_point = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
||||
|
||||
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
||||
ask = NormalizeDouble(ask, symbol_digits);
|
||||
|
||||
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
||||
bid = NormalizeDouble(bid, symbol_digits);
|
||||
|
||||
double price = 0;
|
||||
|
||||
if(price_side==1){
|
||||
price = ask;
|
||||
}
|
||||
|
||||
else if(price_side==2){
|
||||
price = bid;
|
||||
}
|
||||
|
||||
return price;
|
||||
|
||||
}
|
||||
@@ -1,560 +0,0 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| OrderManagement.mqh |
|
||||
//| xMattC |
|
||||
//+------------------------------------------------------------------+
|
||||
#property library
|
||||
#include <Trade/Trade.mqh>
|
||||
#include <MyLibs/TimeZones.mqh>
|
||||
#include <MyLibs/MyEnums.mqh>
|
||||
#include <MyLibs/CalculatePositionData.mqh>
|
||||
#include <Trade/PositionInfo.mqh>
|
||||
#include <Trade/OrderInfo.mqh>
|
||||
#include <MyLibs/Myfunctions.mqh>
|
||||
|
||||
class OrderManagment : public CObject{
|
||||
|
||||
protected:
|
||||
CTrade trade;
|
||||
TimeZones tz;
|
||||
CalculatePositionData cpd;
|
||||
CPositionInfo m_position;
|
||||
COrderInfo m_order;
|
||||
|
||||
double stop_loss;
|
||||
double take_profit;
|
||||
ulong posTicket;
|
||||
int time_difference;
|
||||
int total_open_buy_orders;
|
||||
int total_open_sell_orders;
|
||||
double current_price;
|
||||
int total_pos;
|
||||
long position_open_time;
|
||||
long first_allowed_close_time;
|
||||
datetime current_bar_open_time;
|
||||
|
||||
public:
|
||||
bool open_buy_orders(string symbol, bool condition, ENUM_TIMEFRAMES atr_period, string _sl_mode, double sl_var, string _tp_mode, double tp_var, string _lot_mode, double lot_var,long magic_number);
|
||||
bool open_sell_orders(string symbol, bool condition, ENUM_TIMEFRAMES atr_period, string _sl_mode, double sl_var, string _tp_mode, double tp_var, string _lot_mode, double lot_var,long magic_number);
|
||||
bool open_nnfx_buy_orders(string symbol, bool condition, ENUM_TIMEFRAMES atr_period, string _sl_mode, double sl_var, string _tp_mode, double tp_var, string _lot_mode, double lot_var,long magic_number);
|
||||
bool open_nnfx_sell_orders(string symbol, bool condition, ENUM_TIMEFRAMES atr_period, string _sl_mode, double sl_var, string _tp_mode, double tp_var, string _lot_mode, double lot_var,long magic_number);
|
||||
|
||||
bool open_buy_stop_order(string symbol, bool condition, double entry_price, datetime experation, ENUM_TIMEFRAMES atr_period, string _sl_mode, double sl_var, string _tp_mode, double tp_var, string _lot_mode, double lot_var,long magic_number);
|
||||
bool open_sell_stop_order(string symbol, bool condition, double entry_price, datetime experation, ENUM_TIMEFRAMES atr_period, string _sl_mode, double sl_var, string _tp_mode, double tp_var, string _lot_mode, double lot_var,long magic_number);
|
||||
bool close_buy_orders(string symbol, bool buy_out, int close_bars, ENUM_TIMEFRAMES close_bar_period, long magic_number);
|
||||
bool close_sell_orders(string symbol, bool sell_out, int close_bars, ENUM_TIMEFRAMES close_bar_period, long magic_number);
|
||||
bool first_profitable_close_exit(string symbol, ENUM_TIMEFRAMES close_bar_period, long magic_number);
|
||||
bool daily_timed_exit(string symbol, datetime exit_time, int delay_days, long magic_number);
|
||||
bool daily_timed_profit_exit(string symbol, ENUM_TIMEFRAMES close_bar_period, string exit_time, string tz, int delay_days, long magic_number);
|
||||
int count_all_positions(string symbol, long magic_number);
|
||||
int count_pending_orders(string symbol, ENUM_ORDER_TYPE pendingType, long magic);
|
||||
double sl_specified_value_switch(string _sl_mode, double _inp_sl_var, double value);
|
||||
double tp_specified_value_switch(string _tp_mode, double _inp_tp_var, double value);
|
||||
int count_open_positions(string symbol,int order_side, long magic_number);
|
||||
void break_even_stop(string symbol, ulong magic_number, int be_trigger_points, int be_puffer);
|
||||
void nnfx_trailing_stop(string symbol, double sl_var, double tp_var, double atr_value, ulong magic_number);
|
||||
};
|
||||
|
||||
bool OrderManagment::open_buy_orders(string symbol, bool condition, ENUM_TIMEFRAMES atr_period, string _sl_mode, double sl_var, string _tp_mode, double tp_var, string _lot_mode, double lot_var, long magic_number){
|
||||
|
||||
if(condition == true){
|
||||
current_price = SymbolInfoDouble(symbol, SYMBOL_ASK); // ask for buy side
|
||||
|
||||
total_open_buy_orders = count_open_positions(symbol, 1, magic_number);
|
||||
if(total_open_buy_orders == 0){
|
||||
|
||||
stop_loss = cpd.calculate_stoploss(symbol, current_price, 1, _sl_mode, sl_var, atr_period);
|
||||
take_profit = cpd.calculate_take_profit(symbol, current_price, stop_loss, 1, _tp_mode, tp_var, atr_period);
|
||||
|
||||
double sl_distance = current_price-stop_loss;
|
||||
double lots = cpd.calculate_lots(symbol, sl_distance, current_price, _lot_mode, lot_var);
|
||||
|
||||
trade.SetExpertMagicNumber(magic_number);
|
||||
string comment = "Magic Number: " + IntegerToString(magic_number);
|
||||
trade.PositionOpen(symbol,ORDER_TYPE_BUY,lots,current_price,stop_loss,take_profit,comment);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool OrderManagment::open_sell_orders(string symbol, bool condition, ENUM_TIMEFRAMES atr_period, string _sl_mode, double sl_var, string _tp_mode, double tp_var, string _lot_mode, double lot_var,long magic_number){
|
||||
|
||||
if(condition == true){
|
||||
|
||||
// if(!SymbolInfoTick(symbol,currentTick)){Print("FAILED TO GET TICK:", symbol);return false;}
|
||||
current_price = SymbolInfoDouble(symbol, SYMBOL_BID); // bid for sell side
|
||||
|
||||
total_open_sell_orders = count_open_positions(symbol, 2, magic_number);
|
||||
if(total_open_sell_orders == 0){
|
||||
|
||||
stop_loss = cpd.calculate_stoploss(symbol, current_price, 2, _sl_mode, sl_var, atr_period);
|
||||
take_profit = cpd.calculate_take_profit(symbol, current_price, stop_loss, 2, _tp_mode, tp_var, atr_period);
|
||||
|
||||
double sl_distance = stop_loss-current_price;
|
||||
double lots = cpd.calculate_lots(symbol, sl_distance, current_price, _lot_mode, lot_var);
|
||||
|
||||
trade.SetExpertMagicNumber(magic_number);
|
||||
string comment = "Magic Number: " + IntegerToString(magic_number);
|
||||
trade.PositionOpen(symbol,ORDER_TYPE_SELL,lots,current_price,stop_loss,take_profit,comment);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OrderManagment::open_nnfx_buy_orders(string symbol, bool condition, ENUM_TIMEFRAMES atr_period, string _sl_mode, double sl_var, string _tp_mode, double tp_var, string _lot_mode, double lot_var, long magic_number){
|
||||
|
||||
if(condition == true){
|
||||
current_price = SymbolInfoDouble(symbol, SYMBOL_ASK); // ask for buy side
|
||||
|
||||
total_open_buy_orders = count_open_positions(symbol, 1, magic_number);
|
||||
if(total_open_buy_orders == 0){
|
||||
|
||||
stop_loss = cpd.calculate_stoploss(symbol, current_price, 1, _sl_mode, sl_var, atr_period);
|
||||
take_profit = cpd.calculate_take_profit(symbol, current_price, stop_loss, 1, _tp_mode, tp_var, atr_period);
|
||||
|
||||
double sl_distance = current_price-stop_loss;
|
||||
double lots = cpd.calculate_lots(symbol, sl_distance, current_price, _lot_mode, lot_var/2);
|
||||
|
||||
trade.SetExpertMagicNumber(magic_number);
|
||||
string comment = "Magic Number: " + IntegerToString(magic_number);
|
||||
trade.PositionOpen(symbol,ORDER_TYPE_BUY,lots,current_price,stop_loss,take_profit,comment);
|
||||
trade.PositionOpen(symbol,ORDER_TYPE_BUY,lots,current_price,stop_loss,0,comment);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool OrderManagment::open_nnfx_sell_orders(string symbol, bool condition, ENUM_TIMEFRAMES atr_period, string _sl_mode, double sl_var, string _tp_mode, double tp_var, string _lot_mode, double lot_var,long magic_number){
|
||||
|
||||
if(condition == true){
|
||||
|
||||
// if(!SymbolInfoTick(symbol,currentTick)){Print("FAILED TO GET TICK:", symbol);return false;}
|
||||
current_price = SymbolInfoDouble(symbol, SYMBOL_BID); // bid for sell side
|
||||
|
||||
total_open_sell_orders = count_open_positions(symbol, 2, magic_number);
|
||||
if(total_open_sell_orders == 0){
|
||||
|
||||
stop_loss = cpd.calculate_stoploss(symbol, current_price, 2, _sl_mode, sl_var, atr_period);
|
||||
take_profit = cpd.calculate_take_profit(symbol, current_price, stop_loss, 2, _tp_mode, tp_var, atr_period);
|
||||
|
||||
double sl_distance = stop_loss-current_price;
|
||||
double lots = cpd.calculate_lots(symbol, sl_distance, current_price, _lot_mode, lot_var);
|
||||
|
||||
trade.SetExpertMagicNumber(magic_number);
|
||||
string comment = "Magic Number: " + IntegerToString(magic_number);
|
||||
trade.PositionOpen(symbol,ORDER_TYPE_SELL,lots,current_price,stop_loss,take_profit,comment);
|
||||
trade.PositionOpen(symbol,ORDER_TYPE_SELL,lots,current_price,stop_loss,0,comment);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
// some usfull comment here
|
||||
bool OrderManagment::open_buy_stop_order(string symbol, bool condition, double entry_price, datetime experation, ENUM_TIMEFRAMES atr_period, string _sl_mode, double sl_var,string _tp_mode, double tp_var, string _lot_mode, double lot_var,long magic_number){
|
||||
|
||||
if(condition == true){
|
||||
|
||||
total_open_buy_orders = count_open_positions(symbol, 1, magic_number);
|
||||
if(total_open_buy_orders == 0){
|
||||
|
||||
stop_loss = cpd.calculate_stoploss(symbol, entry_price, 1, _sl_mode, sl_var, atr_period);
|
||||
take_profit = cpd.calculate_take_profit(symbol, entry_price, stop_loss, 1, _tp_mode, tp_var, atr_period);
|
||||
|
||||
double sl_distance = entry_price-stop_loss;
|
||||
double lots = cpd.calculate_lots(symbol, sl_distance, entry_price, _lot_mode, lot_var);
|
||||
|
||||
trade.SetExpertMagicNumber(magic_number);
|
||||
string comment = "Magic Number: " + IntegerToString(magic_number);
|
||||
trade.BuyStop(lots, entry_price, symbol, stop_loss, take_profit, ORDER_TIME_SPECIFIED, experation, comment);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool OrderManagment::open_sell_stop_order(string symbol, bool condition, double entry_price, datetime experation, ENUM_TIMEFRAMES atr_period, string _sl_mode, double sl_var, string _tp_mode, double tp_var, string _lot_mode, double lot_var,long magic_number){
|
||||
|
||||
if(condition == true){
|
||||
|
||||
total_open_sell_orders = count_open_positions(symbol, 2, magic_number);
|
||||
if(total_open_sell_orders == 0){
|
||||
|
||||
stop_loss = cpd.calculate_stoploss(symbol, entry_price, 2, _sl_mode, sl_var, atr_period);
|
||||
take_profit = cpd.calculate_take_profit(symbol, entry_price, stop_loss, 2, _tp_mode, tp_var, atr_period);
|
||||
|
||||
double sl_distance = stop_loss-entry_price;
|
||||
double lots = cpd.calculate_lots(symbol, sl_distance, entry_price, _lot_mode, lot_var);
|
||||
|
||||
trade.SetExpertMagicNumber(magic_number);
|
||||
string comment = "Magic Number: " + IntegerToString(magic_number);
|
||||
trade.SellStop(lots, entry_price, symbol, stop_loss, take_profit, ORDER_TIME_SPECIFIED, experation, comment);
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OrderManagment::close_buy_orders(string symbol, bool condition, int close_bars, ENUM_TIMEFRAMES close_bar_period, long magic_number){
|
||||
|
||||
for(int i = PositionsTotal()-1; i >=0; i--){
|
||||
posTicket = PositionGetTicket(i);
|
||||
|
||||
if(PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_MAGIC) == magic_number){
|
||||
|
||||
time_difference = Bars(symbol, close_bar_period, PositionGetInteger(POSITION_TIME), TimeCurrent()) - 1;
|
||||
|
||||
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){
|
||||
|
||||
if(condition){
|
||||
trade.PositionClose(posTicket);
|
||||
}
|
||||
|
||||
if(close_bars > 0){
|
||||
if(time_difference >= close_bars){
|
||||
trade.PositionClose(posTicket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OrderManagment::close_sell_orders(string symbol, bool condition, int close_bars, ENUM_TIMEFRAMES close_bar_period, long magic_number){
|
||||
|
||||
for(int i = PositionsTotal()-1; i >=0; i--){
|
||||
posTicket = PositionGetTicket(i);
|
||||
|
||||
if(PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_MAGIC) == magic_number){
|
||||
|
||||
time_difference = Bars(symbol, close_bar_period, PositionGetInteger(POSITION_TIME), TimeCurrent()) - 1;
|
||||
|
||||
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){
|
||||
|
||||
if(condition){trade.PositionClose(posTicket);}
|
||||
|
||||
if(close_bars > 0){
|
||||
if(time_difference >= close_bars){
|
||||
trade.PositionClose(posTicket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// order_side int must be 1 for BUY or 2 for SELL
|
||||
int OrderManagment::count_open_positions(string symbol,int order_side, long magic_number){
|
||||
|
||||
|
||||
int count = 0;
|
||||
bool match = (PositionGetInteger(POSITION_MAGIC)==magic_number);
|
||||
|
||||
for(int i = PositionsTotal()-1; i >=0; i--){
|
||||
ulong ticket = PositionGetTicket(i);
|
||||
|
||||
if(PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_MAGIC)==magic_number){
|
||||
|
||||
// Count only Buy orders:
|
||||
if(order_side == 1){
|
||||
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){
|
||||
count = count + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// Count only Sell orders:
|
||||
if(order_side == 2){
|
||||
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){
|
||||
count = count + 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
int OrderManagment::count_all_positions(string symbol, long magic_number){
|
||||
|
||||
int count = 0;
|
||||
for(int i = PositionsTotal()-1; i >=0; i--){
|
||||
ulong ticket = PositionGetTicket(i);
|
||||
|
||||
if(PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_MAGIC)==magic_number){
|
||||
count = count + 1;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
bool OrderManagment::daily_timed_exit(string symbol, datetime exit_time, int delay_days, long magic_number){
|
||||
|
||||
for(int i = PositionsTotal()-1; i >=0; i--){
|
||||
posTicket = PositionGetTicket(i);
|
||||
position_open_time = PositionGetInteger(POSITION_TIME);
|
||||
|
||||
if((int)position_open_time>0){
|
||||
|
||||
first_allowed_close_time = position_open_time + (delay_days * PeriodSeconds(PERIOD_D1));
|
||||
if(TimeCurrent() > first_allowed_close_time){
|
||||
|
||||
// datetime broker_close_time = tz.timezone_conversions(cw_tzone, StringToTime(exit_time), "Broker");
|
||||
if(TimeCurrent()>= exit_time){
|
||||
|
||||
if(PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_MAGIC) == magic_number){
|
||||
|
||||
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){
|
||||
trade.PositionClose(posTicket);
|
||||
}
|
||||
|
||||
// Sell orders:
|
||||
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){
|
||||
trade.PositionClose(posTicket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool OrderManagment::daily_timed_profit_exit(string symbol, ENUM_TIMEFRAMES close_bar_period, string exit_time, string cw_tzone, int delay_days, long magic_number){
|
||||
|
||||
// om.daily_timed_profit_exit(_Symbol, PERIOD_CURRENT, "16:45", "17:00", "NY", 1, inp_magic);
|
||||
|
||||
for(int i = PositionsTotal()-1; i >=0; i--){
|
||||
posTicket = PositionGetTicket(i);
|
||||
position_open_time = PositionGetInteger(POSITION_TIME);
|
||||
|
||||
if((int)position_open_time>0){
|
||||
|
||||
first_allowed_close_time = position_open_time + (delay_days * PeriodSeconds(PERIOD_D1));
|
||||
if(TimeCurrent() > first_allowed_close_time){
|
||||
|
||||
|
||||
datetime broker_close_time = tz.timezone_conversions(cw_tzone, StringToTime(exit_time), "Broker");
|
||||
if(TimeCurrent()>= broker_close_time){
|
||||
|
||||
if(PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_MAGIC) == magic_number){
|
||||
|
||||
double position_open_price = PositionGetDouble(POSITION_PRICE_OPEN);
|
||||
double spread = SymbolInfoDouble(symbol,SYMBOL_ASK) - SymbolInfoDouble(symbol,SYMBOL_BID);
|
||||
double bar_close = iClose(_Symbol, close_bar_period, 1); // shift 1 because 0 = live candle.
|
||||
double trading_cost = cpd.calculate_trading_cost(symbol, posTicket);
|
||||
|
||||
|
||||
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){
|
||||
if(bar_close > (position_open_price + spread + trading_cost)){
|
||||
trade.PositionClose(posTicket);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Sell orders:
|
||||
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){
|
||||
if(bar_close < position_open_price - spread - trading_cost){
|
||||
trade.PositionClose(posTicket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
bool OrderManagment::first_profitable_close_exit(string symbol, ENUM_TIMEFRAMES close_bar_period, long magic_number){
|
||||
// om.first_profitable_close_exit(_Symbol, PERIOD_CURRENT, inp_magic);
|
||||
|
||||
position_open_time = PositionGetInteger(POSITION_TIME);
|
||||
first_allowed_close_time = position_open_time + PeriodSeconds(close_bar_period);
|
||||
|
||||
if((int)position_open_time>0){
|
||||
|
||||
if(TimeCurrent() > first_allowed_close_time){
|
||||
for(int i = PositionsTotal()-1; i >=0; i--){
|
||||
posTicket = PositionGetTicket(i);
|
||||
|
||||
if(PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_MAGIC) == magic_number){
|
||||
|
||||
double position_open_price = PositionGetDouble(POSITION_PRICE_OPEN);
|
||||
double spread = SymbolInfoDouble(symbol,SYMBOL_ASK) - SymbolInfoDouble(symbol,SYMBOL_BID);
|
||||
double bar_close = iClose(_Symbol,close_bar_period, 1); // shift 1 because 0 = live candle.
|
||||
double trading_cost = cpd.calculate_trading_cost(symbol, posTicket);
|
||||
|
||||
|
||||
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_BUY){
|
||||
if(bar_close > (position_open_price + spread + trading_cost)){
|
||||
trade.PositionClose(posTicket);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
// Sell orders:
|
||||
if(PositionGetInteger(POSITION_TYPE) == POSITION_TYPE_SELL){
|
||||
if(bar_close < position_open_price - spread - trading_cost){
|
||||
trade.PositionClose(posTicket);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
// e.g. int buy_stop_count = om.count_pending_orders(symbol, ORDER_TYPE_BUY_STOP, inp_magic);
|
||||
// order types: ORDER_TYPE_BUY_LIMIT, ORDER_TYPE_SELL_LIMIT, ORDER_TYPE_BUY_STOP, ORDER_TYPE_SELL_STOP
|
||||
int OrderManagment::count_pending_orders(string symbol, ENUM_ORDER_TYPE order_type, long magic){
|
||||
int count = 0;
|
||||
|
||||
for(int i=OrdersTotal()-1;i>=0;i--) {
|
||||
|
||||
if(m_order.SelectByIndex(i)){
|
||||
if( OrderGetInteger(ORDER_MAGIC) == magic && OrderGetString(ORDER_SYMBOL) == symbol){
|
||||
|
||||
if(m_order.OrderType()==order_type){
|
||||
count++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return(count);
|
||||
}
|
||||
|
||||
void OrderManagment::break_even_stop(string symbol, ulong magic_number, int be_trigger_points, int be_puffer){
|
||||
|
||||
for(int i = PositionsTotal()-1; i >=0; i--){
|
||||
if(PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_MAGIC) == magic_number){
|
||||
|
||||
int symbol_digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
|
||||
double symbol_point = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
||||
|
||||
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
||||
ask = NormalizeDouble(ask, symbol_digits);
|
||||
|
||||
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
||||
bid = NormalizeDouble(bid, symbol_digits);
|
||||
|
||||
if(be_trigger_points !=0){
|
||||
|
||||
|
||||
ulong ticket = PositionGetTicket(i);
|
||||
if(PositionSelectByTicket(ticket)){
|
||||
|
||||
double position_open_price = PositionGetDouble(POSITION_PRICE_OPEN);
|
||||
double position_volume = PositionGetDouble(POSITION_VOLUME);
|
||||
double position_sl = PositionGetDouble(POSITION_SL);
|
||||
double position_tp = PositionGetDouble(POSITION_TP);
|
||||
ENUM_POSITION_TYPE position_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
|
||||
|
||||
if(position_type == POSITION_TYPE_BUY){
|
||||
|
||||
if(bid > position_open_price + be_trigger_points * symbol_point){
|
||||
|
||||
double sl = position_open_price + be_puffer * symbol_point;
|
||||
sl = NormalizeDouble(sl, symbol_digits);
|
||||
if(sl > position_sl){
|
||||
|
||||
if(trade.PositionModify(ticket, sl, position_tp)){
|
||||
Print("-----------------------------------Stop moved to break even");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
else if(position_type == POSITION_TYPE_SELL){
|
||||
|
||||
if(ask < position_open_price - be_trigger_points * symbol_point){
|
||||
|
||||
double sl = position_open_price - be_puffer * symbol_point;
|
||||
sl = NormalizeDouble(sl, symbol_digits);
|
||||
if(sl < position_sl){
|
||||
|
||||
if(trade.PositionModify(ticket, sl, position_tp)){
|
||||
Print("-----------------------------------Stop moved to break even");
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
void OrderManagment::nnfx_trailing_stop(string symbol, double sl_var, double tp_var, double atr_value, ulong magic_number){
|
||||
|
||||
MyFunctions mf3;
|
||||
|
||||
for(int i = PositionsTotal()-1; i >=0; i--){
|
||||
|
||||
ulong ticket = PositionGetTicket(i);
|
||||
if(PositionSelectByTicket(ticket)){
|
||||
|
||||
if(PositionGetString(POSITION_SYMBOL) == symbol && PositionGetInteger(POSITION_MAGIC) == magic_number){
|
||||
|
||||
int symbol_digits = (int)SymbolInfoInteger(symbol, SYMBOL_DIGITS);
|
||||
double symbol_point = SymbolInfoDouble(symbol, SYMBOL_POINT);
|
||||
|
||||
double ask = SymbolInfoDouble(symbol, SYMBOL_ASK);
|
||||
ask = NormalizeDouble(ask, symbol_digits);
|
||||
|
||||
double bid = SymbolInfoDouble(symbol, SYMBOL_BID);
|
||||
bid = NormalizeDouble(bid, symbol_digits);
|
||||
|
||||
double position_open_price = PositionGetDouble(POSITION_PRICE_OPEN);
|
||||
double position_sl = PositionGetDouble(POSITION_SL);
|
||||
double position_tp = PositionGetDouble(POSITION_TP);
|
||||
ENUM_POSITION_TYPE position_type = (ENUM_POSITION_TYPE)PositionGetInteger(POSITION_TYPE);
|
||||
|
||||
if(position_type == POSITION_TYPE_BUY){
|
||||
|
||||
if(bid > position_open_price + (atr_value * tp_var)){
|
||||
|
||||
double sl = bid - (atr_value * sl_var);
|
||||
sl = NormalizeDouble(sl, symbol_digits);
|
||||
if(sl > (position_sl + (atr_value * 0.5))){
|
||||
|
||||
if(trade.PositionModify(ticket, sl, position_tp)){
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
else if(position_type == POSITION_TYPE_SELL){
|
||||
|
||||
if(ask < position_open_price - (atr_value * tp_var)){
|
||||
|
||||
double sl = ask + (atr_value * sl_var);
|
||||
sl = NormalizeDouble(sl, symbol_digits);
|
||||
if(sl < (position_sl + (atr_value * 0.5))){
|
||||
|
||||
if(trade.PositionModify(ticket, sl, position_tp)){
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
double OrderManagment::sl_specified_value_switch(string _sl_mode, double _inp_sl_var, double value){
|
||||
double sl = 0;
|
||||
if(_sl_mode=="SL_SPECIFIED_VALUE"){sl = value;}
|
||||
if(_sl_mode!="SL_SPECIFIED_VALUE"){sl = _inp_sl_var;}
|
||||
return sl;
|
||||
}
|
||||
double OrderManagment::tp_specified_value_switch(string _tp_mode, double _inp_tp_var, double value){
|
||||
double tp = 0;
|
||||
if(_tp_mode=="SL_SPECIFIED_VALUE"){tp = value;}
|
||||
if(_tp_mode!="SL_SPECIFIED_VALUE"){tp = _inp_tp_var;}
|
||||
return tp;
|
||||
}
|
||||
@@ -1,148 +0,0 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| TimeZones.mqh |
|
||||
//| xMattC |
|
||||
//+------------------------------------------------------------------+
|
||||
#property library
|
||||
#include <Trade/Trade.mqh>
|
||||
#include <MyLibs/DealingWithTime.mqh>
|
||||
|
||||
class TimeZones: public CObject{
|
||||
|
||||
protected:
|
||||
string dt_s;
|
||||
int len;
|
||||
string dt_string;
|
||||
datetime tC, tGMT, tNY, tLon, tFfm, tMosc, tSyd, tTok;
|
||||
datetime tz_time;
|
||||
string tz_date;
|
||||
datetime time_start;
|
||||
datetime time_end;
|
||||
bool is_time;
|
||||
datetime tGIVEN;
|
||||
datetime tREQ;
|
||||
datetime tzt;
|
||||
datetime tz_req;
|
||||
double ny_daily_close_protected(string symbol, int shift_days, bool print_data=false);
|
||||
double required_close;
|
||||
|
||||
public:
|
||||
string get_date_string_from_datetime(datetime dt);
|
||||
datetime get_timezone_time(string time_zone, bool print_time);
|
||||
datetime timezone_conversions(string time_zone_known, datetime time_given, string time_zone_required);
|
||||
double ny_daily_close(string symbol, int shift_days, bool print_data=false);
|
||||
};
|
||||
|
||||
string TimeZones::get_date_string_from_datetime(datetime dt){
|
||||
dt_s = TimeToString(dt);
|
||||
len = StringLen(dt_s);
|
||||
dt_string = StringSubstr(dt_s, 0, len-5);
|
||||
return dt_string;
|
||||
}
|
||||
|
||||
|
||||
datetime TimeZones::get_timezone_time(string time_zone, bool print_time){
|
||||
// https://www.mql5.com/en/code/45287
|
||||
// https://www.mql5.com/en/articles/9926
|
||||
// https://www.mql5.com/en/articles/9929
|
||||
|
||||
checkTimeOffset(TimeCurrent()); // check changes of DST
|
||||
// cto();
|
||||
|
||||
tC = TimeCurrent();
|
||||
tGMT = TimeCurrent() + OffsetBroker.actOffset; // GMT
|
||||
tNY = tGMT - (NYShift+DST_USD); // time in New York (EST)
|
||||
tLon = tGMT - (LondonShift+DST_EUR); // time in London
|
||||
tFfm = tGMT - (FfmShift+DST_EUR); // time in Frankfurt
|
||||
tSyd = tGMT - (SidneyShift+DST_AUD); // time in Sidney
|
||||
tMosc = tGMT - (MoskwaShift+DST_RUS); // time in Moscow
|
||||
tTok = tGMT - (TokyoShift); // time in Tokyo - no DST
|
||||
|
||||
if(print_time==true){
|
||||
Print("----------------------------------");
|
||||
Print("Broker: ", tC);
|
||||
Print("GMT: ", tGMT);
|
||||
Print("time in New York: ", tNY);
|
||||
Print("time in London: ", tLon);
|
||||
Print("time in Frankfurt: ", tFfm);
|
||||
Print("time in Sidney: ", tSyd);
|
||||
Print("time in Moscow: ", tMosc);
|
||||
Print("time in Tokyo: ", tTok);
|
||||
}
|
||||
|
||||
if(time_zone=="NY"){return tNY;}
|
||||
if(time_zone=="Lon"){return tLon;}
|
||||
if(time_zone=="Ffm"){return tFfm;}
|
||||
if(time_zone=="Syd"){return tSyd;}
|
||||
if(time_zone=="Mosc"){return tMosc;}
|
||||
if(time_zone=="Tok"){return tTok;}
|
||||
|
||||
return NULL;
|
||||
}
|
||||
|
||||
|
||||
datetime TimeZones::timezone_conversions(string time_zone_known, datetime time_given, string time_zone_required){
|
||||
// https://www.mql5.com/en/code/45287
|
||||
// https://www.mql5.com/en/articles/9926
|
||||
// https://www.mql5.com/en/articles/9929
|
||||
|
||||
tGIVEN = time_given; //StringToTime(time_given);
|
||||
|
||||
checkTimeOffset(tGIVEN); // check changes of DST
|
||||
|
||||
// Get GMT:
|
||||
if(time_zone_known=="GMT" ){tGMT = tGIVEN;}
|
||||
if(time_zone_known=="Broker" ){tGMT = tGIVEN + OffsetBroker.actOffset;}
|
||||
if(time_zone_known=="NY" ){tGMT = tGIVEN + (NYShift+DST_USD);}
|
||||
if(time_zone_known=="Lon" ){tGMT = tGIVEN + (LondonShift+DST_EUR);}
|
||||
if(time_zone_known=="Ffm" ){tGMT = tGIVEN + (FfmShift+DST_EUR);}
|
||||
if(time_zone_known=="Syd" ){tGMT = tGIVEN + (SidneyShift+DST_AUD);}
|
||||
if(time_zone_known=="Mosc" ){tGMT = tGIVEN + (MoskwaShift+DST_RUS);}
|
||||
if(time_zone_known=="Tok" ){tGMT = tGIVEN + (TokyoShift);}
|
||||
|
||||
// define the required time:
|
||||
tREQ = NULL;
|
||||
if(time_zone_required=="GMT" ){tREQ = tGMT;}
|
||||
if(time_zone_required=="Broker" ){tREQ = tGMT - OffsetBroker.actOffset;}
|
||||
if(time_zone_required=="NY" ){tREQ = tGMT - (NYShift+DST_USD);}
|
||||
if(time_zone_required=="Lon" ){tREQ = tGMT - (LondonShift+DST_EUR);}
|
||||
if(time_zone_required=="Ffm" ){tREQ = tGMT - (FfmShift+DST_EUR);}
|
||||
if(time_zone_required=="Syd" ){tREQ = tGMT - (SidneyShift+DST_AUD) ;}
|
||||
if(time_zone_required=="Mosc" ){tREQ = tGMT - (MoskwaShift+DST_RUS);}
|
||||
if(time_zone_required=="Tok" ){tREQ = tGMT - (TokyoShift);}
|
||||
|
||||
return tREQ;
|
||||
}
|
||||
|
||||
// Calculte NY close time:
|
||||
double TimeZones::ny_daily_close(string symbol, int shift_days, bool print_data=false){
|
||||
required_close = ny_daily_close_protected(symbol, shift_days, print_data);
|
||||
return required_close;
|
||||
}
|
||||
double TimeZones::ny_daily_close_protected(string symbol, int shift_days, bool print_data=false){
|
||||
|
||||
// Get the brokers times for when NY openend today and tomorrow:
|
||||
datetime time_5pm = iTime(symbol, PERIOD_D1 , 0) - (PeriodSeconds(PERIOD_H1) * 7);
|
||||
datetime ny_close_in_brokers_time = timezone_conversions("NY", time_5pm, "Broker");
|
||||
datetime ny_close_time = ny_close_in_brokers_time + PeriodSeconds(PERIOD_D1); // ny close tomorrow
|
||||
|
||||
if(TimeCurrent()<ny_close_time){
|
||||
ny_close_time = ny_close_time - PeriodSeconds(PERIOD_D1); // ny close today
|
||||
}
|
||||
|
||||
// Get the number of hours since NY closed:
|
||||
int shift = iBarShift(symbol, PERIOD_H1, ny_close_time, false) + 1;
|
||||
shift = shift + (24 * (shift_days - 1)); // shift days if required:
|
||||
|
||||
double ny_close = iClose(symbol,PERIOD_H1, shift);
|
||||
double br_close = iClose(symbol,PERIOD_H1, 1);
|
||||
|
||||
if(print_data==true){
|
||||
Print("shift ",shift);
|
||||
Print("time_5pm ",time_5pm);
|
||||
Print("ny_close_in_brokers_time ",ny_close_in_brokers_time);
|
||||
Print("ny_close_time ",ny_close_time);
|
||||
Print("ny_close ", ny_close);
|
||||
Print("br_close ",br_close);
|
||||
}
|
||||
return ny_close;
|
||||
}
|
||||
@@ -1 +0,0 @@
|
||||
.8448951187990853
|
||||
@@ -1,183 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import logging
|
||||
from datetime import datetime, timedelta
|
||||
from time import sleep
|
||||
import pandas as pd
|
||||
from binance import Client
|
||||
from forex_python.converter import CurrencyRates
|
||||
import telegram
|
||||
import schedule
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(
|
||||
level=logging.INFO,
|
||||
format="%(asctime)s - %(levelname)s - %(message)s",
|
||||
)
|
||||
|
||||
# Environment variables for sensitive information
|
||||
API_KEY = os.getenv("BINANCE_API_KEY")
|
||||
API_SECRET = os.getenv("BINANCE_API_SECRET")
|
||||
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||
BOT_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
|
||||
|
||||
# Constants
|
||||
MAX_TRADES = 5
|
||||
RESET_STAKE_AMOUNT = 10 # minutes
|
||||
DATA_DIR = "data"
|
||||
|
||||
# Ensure data directory exists
|
||||
os.makedirs(DATA_DIR, exist_ok=True)
|
||||
|
||||
|
||||
def telegram_send_message(bot_token, bot_chat_id, message):
|
||||
"""
|
||||
Send a message via Telegram bot.
|
||||
"""
|
||||
try:
|
||||
bot = telegram.Bot(token=bot_token)
|
||||
bot.send_message(chat_id=bot_chat_id, text=message)
|
||||
except Exception as e:
|
||||
logging.error(f"Failed to send Telegram message: {e}")
|
||||
|
||||
|
||||
def update_df(gbp, usd, btc, exchange_rate):
|
||||
"""
|
||||
Update the HDF5 file with new balance data.
|
||||
"""
|
||||
try:
|
||||
file_path = os.path.join(DATA_DIR, "balances.h5")
|
||||
time_now = pd.to_datetime(datetime.now().replace(microsecond=0))
|
||||
new_data = pd.DataFrame({
|
||||
"date_time": [time_now],
|
||||
"£": [gbp],
|
||||
"$": [usd],
|
||||
"BTC": [btc],
|
||||
"Ex-rate": [exchange_rate],
|
||||
})
|
||||
|
||||
with pd.HDFStore(file_path) as store:
|
||||
if "df" in store:
|
||||
df = store["df"]
|
||||
df = pd.concat([df, new_data]).drop_duplicates(subset="date_time", keep="first")
|
||||
else:
|
||||
df = new_data.set_index("date_time")
|
||||
store["df"] = df
|
||||
|
||||
logging.info("Updated DataFrame successfully.")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"Could not update DataFrame: {e}")
|
||||
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, "Could not update DataFrame.")
|
||||
|
||||
|
||||
def trim_df():
|
||||
"""
|
||||
Trim the DataFrame to the last 24 hours.
|
||||
"""
|
||||
try:
|
||||
file_path = os.path.join(DATA_DIR, "balances.h5")
|
||||
cut_before = datetime.now() - timedelta(hours=25)
|
||||
|
||||
with pd.HDFStore(file_path) as store:
|
||||
if "df" in store:
|
||||
df = store["df"]
|
||||
df = df[df.index >= cut_before]
|
||||
store["df"] = df
|
||||
|
||||
logging.info("Trimmed DataFrame to the last 24 hours.")
|
||||
except Exception as e:
|
||||
logging.error(f"Could not trim DataFrame: {e}")
|
||||
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, "Could not trim DataFrame.")
|
||||
|
||||
|
||||
def update_exchange_rate():
|
||||
"""
|
||||
Fetch the latest USD to GBP exchange rate and store it.
|
||||
"""
|
||||
cr = CurrencyRates()
|
||||
file_path = os.path.join(DATA_DIR, "USDGBP_exchange_rate.txt")
|
||||
|
||||
try:
|
||||
exchange_rate = cr.get_rate("USD", "GBP")
|
||||
with open(file_path, "w") as f:
|
||||
f.write(str(exchange_rate))
|
||||
logging.info("Updated USD to GBP exchange rate.")
|
||||
except Exception as e:
|
||||
logging.error(f"Could not update exchange rate: {e}")
|
||||
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, "Could not update exchange rate.")
|
||||
|
||||
|
||||
def convert_usd_to_gbp(usd):
|
||||
"""
|
||||
Convert USD to GBP using the stored exchange rate.
|
||||
"""
|
||||
file_path = os.path.join(DATA_DIR, "USDGBP_exchange_rate.txt")
|
||||
|
||||
try:
|
||||
with open(file_path, "r") as f:
|
||||
exchange_rate = float(f.read())
|
||||
gbp = usd * exchange_rate
|
||||
return gbp, exchange_rate
|
||||
except Exception as e:
|
||||
logging.error(f"Could not convert USD to GBP: {e}")
|
||||
return usd, 1.0 # Fallback to 1:1 conversion
|
||||
|
||||
|
||||
def get_balance():
|
||||
"""
|
||||
Retrieve account balances from Binance and update the stake amount.
|
||||
"""
|
||||
try:
|
||||
client = Client(API_KEY, API_SECRET)
|
||||
account_info = client.get_account()
|
||||
balances = account_info["balances"]
|
||||
|
||||
usdt = 0.0
|
||||
for balance in balances:
|
||||
asset = balance["asset"]
|
||||
free = float(balance["free"])
|
||||
locked = float(balance["locked"])
|
||||
total = free + locked
|
||||
|
||||
if total > 0:
|
||||
if asset == "USDT":
|
||||
usdt += total
|
||||
else:
|
||||
try:
|
||||
price = float(client.get_symbol_ticker(symbol=f"{asset}USDT")["price"])
|
||||
usdt += total * price
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
btc_price = float(client.get_symbol_ticker(symbol="BTCUSDT")["price"])
|
||||
btc = usdt / btc_price
|
||||
gbp, exchange_rate = convert_usd_to_gbp(usdt)
|
||||
stake_amount = round(usdt / MAX_TRADES)
|
||||
|
||||
with open(os.path.join(DATA_DIR, "stake_amount.txt"), "w") as f:
|
||||
f.write(str(stake_amount))
|
||||
|
||||
update_df(gbp, usdt, btc, exchange_rate)
|
||||
except Exception as e:
|
||||
logging.error(f"Could not fetch balances: {e}")
|
||||
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, "Could not fetch Binance balances.")
|
||||
|
||||
|
||||
def run_code():
|
||||
"""
|
||||
Main function to schedule tasks and run the bot.
|
||||
"""
|
||||
update_exchange_rate()
|
||||
get_balance()
|
||||
schedule.every(RESET_STAKE_AMOUNT).minutes.do(get_balance)
|
||||
schedule.every().day.at("11:45").do(update_exchange_rate)
|
||||
schedule.every(4).hours.do(trim_df)
|
||||
|
||||
while True:
|
||||
schedule.run_pending()
|
||||
sleep(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
run_code()
|
||||
@@ -1,263 +0,0 @@
|
||||
#!/usr/bin/env python3
|
||||
import os
|
||||
import time
|
||||
import shutil
|
||||
import warnings
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt # pip install matplotlib
|
||||
import matplotlib.dates as mdates
|
||||
from datetime import datetime, timedelta, date
|
||||
import telegram
|
||||
import schedule # pip install schedule
|
||||
|
||||
warnings.simplefilter(action='ignore', category=FutureWarning)
|
||||
|
||||
# Constants
|
||||
LONG_PLOT_DAYS = -1 # 60 # -1 all days in df
|
||||
LONG_PLOT_CURRENCY = '£'
|
||||
PLOT_CURRENCY = '$'
|
||||
|
||||
# Environment variables for sensitive information
|
||||
API_KEY = os.getenv("BINANCE_API_KEY")
|
||||
API_SECRET = os.getenv("BINANCE_API_SECRET")
|
||||
BOT_TOKEN = os.getenv("TELEGRAM_BOT_TOKEN")
|
||||
BOT_CHAT_ID = os.getenv("TELEGRAM_CHAT_ID")
|
||||
|
||||
|
||||
date_today = date.today()
|
||||
TODAY = date_today.strftime("%Y_%m_%d")
|
||||
|
||||
|
||||
def telegram_send_image(bot_token, bot_chat_id, image_path):
|
||||
"""Send an image to the specified Telegram chat."""
|
||||
bot = telegram.Bot(token=bot_token)
|
||||
with open(image_path, 'rb') as photo:
|
||||
bot.send_photo(chat_id=bot_chat_id, photo=photo)
|
||||
return ()
|
||||
|
||||
|
||||
def telegram_send_message(bot_token, bot_chat_id, message):
|
||||
"""Send a message to the specified Telegram chat."""
|
||||
bot = telegram.Bot(token=bot_token)
|
||||
bot.send_message(chat_id=bot_chat_id, text=message)
|
||||
return ()
|
||||
|
||||
|
||||
def plot_and_send_image(df, currency, plot_title, file_name):
|
||||
"""Generate plot and send it as an image to Telegram."""
|
||||
plt.rcParams.update({'font.size': 14, 'font.family': 'STIXGeneral', 'mathtext.fontset': 'stix'})
|
||||
fig, axs = plt.subplots(figsize=(7, 4))
|
||||
axs.xaxis.set_major_formatter(mdates.DateFormatter("%d %b"))
|
||||
df[currency].plot.line(ax=axs, color="darkgreen", linewidth=1.50)
|
||||
delta_y = int(df[currency].max()) - int(df[currency].min())
|
||||
y_min = int(df[currency].min()) - (delta_y * 0.05)
|
||||
y_max = int(df[currency].max()) + (delta_y * 0.05)
|
||||
x_max = datetime.now()
|
||||
x_min = datetime.now() - timedelta(days=len(df))
|
||||
axs.set_title(plot_title)
|
||||
axs.set_ylim(y_min, y_max)
|
||||
axs.set_xlim(x_min, x_max)
|
||||
axs.set_ylabel("")
|
||||
axs.set_xlabel("")
|
||||
axs.grid(color='grey', alpha=0.5, linestyle='dashed', linewidth=0.5)
|
||||
axs.yaxis.set_major_formatter(f"{currency} {{x:1.0f}}")
|
||||
|
||||
plt.savefig(file_name)
|
||||
plt.cla()
|
||||
plt.close(fig)
|
||||
|
||||
# Send image to Telegram
|
||||
telegram_send_image(BOT_TOKEN, BOT_CHAT_ID, file_name)
|
||||
return ()
|
||||
|
||||
|
||||
def plot_long(period=LONG_PLOT_DAYS, currency=LONG_PLOT_CURRENCY):
|
||||
"""Plot long-term data."""
|
||||
try:
|
||||
if os.path.exists('balances_24h.h5'):
|
||||
balances_24h = pd.HDFStore('balances_24h.h5')
|
||||
df = balances_24h['df_24h'].iloc[1:, :]
|
||||
balances_24h.close()
|
||||
if period == -1:
|
||||
no_of_days = len(df)
|
||||
else:
|
||||
no_of_days = period
|
||||
df = df.tail(no_of_days)
|
||||
plot_and_send_image(df, currency, f"{no_of_days} days plot", "plot_long.png")
|
||||
else:
|
||||
message = "No balances_24h.h5 file"
|
||||
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
|
||||
except Exception:
|
||||
message = "Could not generate long plot"
|
||||
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
|
||||
return ()
|
||||
|
||||
|
||||
def plot_30days(currency=PLOT_CURRENCY):
|
||||
"""Plot 30 days data."""
|
||||
try:
|
||||
if os.path.exists('balances_4h.h5'):
|
||||
balances_4h = pd.HDFStore('balances_4h.h5')
|
||||
df = balances_4h['df_4h'].iloc[1:, :]
|
||||
balances_4h.close()
|
||||
df = df.tail(180)
|
||||
plot_and_send_image(df, currency, "30 Day Balances", "30_days.png")
|
||||
else:
|
||||
message = "No balances_4h.h5 file"
|
||||
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
|
||||
except Exception:
|
||||
message = "Could not generate 30 day plot"
|
||||
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
|
||||
return ()
|
||||
|
||||
|
||||
def plot_7days(currency=PLOT_CURRENCY):
|
||||
"""Plot 7 days data."""
|
||||
try:
|
||||
if os.path.exists('balances_1h.h5'):
|
||||
balances_1h = pd.HDFStore('balances_1h.h5')
|
||||
df = balances_1h['df_1h']
|
||||
end_date = datetime.now().replace(microsecond=0)
|
||||
cut_before_date = end_date - timedelta(days=7)
|
||||
df = df.loc[df.index >= cut_before_date]
|
||||
plot_and_send_image(df, currency, "7 Day Balances", "7_days.png")
|
||||
else:
|
||||
message = "No balances_1h.h5 file"
|
||||
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
|
||||
except Exception:
|
||||
message = "Could not generate 7 day plot"
|
||||
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
|
||||
return ()
|
||||
|
||||
|
||||
def plot_24h(currency=PLOT_CURRENCY):
|
||||
"""Plot 24 hours data."""
|
||||
try:
|
||||
if os.path.exists('balances.h5'):
|
||||
balances = pd.HDFStore('balances.h5')
|
||||
df = balances['df'].iloc[1:, :]
|
||||
df['date_time'] = pd.to_datetime(df['date_time'])
|
||||
balances.close()
|
||||
df = df.set_index('date_time')
|
||||
plot_and_send_image(df, currency, TODAY, "24_hour.png")
|
||||
else:
|
||||
message = "No balances.h5 file"
|
||||
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
|
||||
except Exception:
|
||||
message = "Could not generate 24h plot"
|
||||
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
|
||||
return ()
|
||||
|
||||
|
||||
def send_plots():
|
||||
"""Send all generated plots to Telegram."""
|
||||
try:
|
||||
balances = pd.HDFStore('balances.h5')
|
||||
df = balances['df']
|
||||
balances.close()
|
||||
GBP = df['£'].iloc[-1]
|
||||
USDT = df['$'].iloc[-1]
|
||||
BTC = df['BTC'].iloc[-1]
|
||||
|
||||
message = f"Balance:\n GBP £ {round(GBP, 2)}\n USD $ {round(USDT, 2)}\n BTC {round(BTC, 6)}"
|
||||
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
|
||||
|
||||
# Send all plots
|
||||
for file_name in ["plot_long.png", "30_days.png", "7_days.png", "24_hour.png"]:
|
||||
if os.path.exists(file_name):
|
||||
telegram_send_image(BOT_TOKEN, BOT_CHAT_ID, file_name)
|
||||
else:
|
||||
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, f"Could not find {file_name}")
|
||||
|
||||
print(f"{datetime.now().replace(microsecond=0)} - Sent plots to Telegram.")
|
||||
except Exception:
|
||||
message = "Could not send plots."
|
||||
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
|
||||
return ()
|
||||
|
||||
|
||||
def resample_data():
|
||||
"""Resample data to 1h, 4h, and 24h."""
|
||||
try:
|
||||
if not os.path.exists("balances_1h.h5"):
|
||||
balances = pd.HDFStore('balances.h5')
|
||||
df = balances['df']
|
||||
df['date_time'] = pd.to_datetime(df['date_time'])
|
||||
df = df.set_index('date_time')
|
||||
balances.close()
|
||||
|
||||
# Resample to 1h
|
||||
data_1h = df.resample("H").mean()
|
||||
balances_1h = pd.HDFStore('balances_1h.h5')
|
||||
balances_1h['df_1h'] = data_1h
|
||||
balances_1h.close()
|
||||
|
||||
if not os.path.exists("balances_4h.h5"):
|
||||
balances_1h = pd.HDFStore('balances_1h.h5')
|
||||
df_1h = balances_1h['df_1h']
|
||||
balances_1h.close()
|
||||
|
||||
# Resample to 4h
|
||||
data_4h = df_1h.resample("4H").mean()
|
||||
balances_4h = pd.HDFStore('balances_4h.h5')
|
||||
balances_4h['df_4h'] = data_4h
|
||||
balances_4h.close()
|
||||
|
||||
if not os.path.exists("balances_24h.h5"):
|
||||
balances_4h = pd.HDFStore('balances_4h.h5')
|
||||
df_4h = balances_4h['df_4h']
|
||||
balances_4h.close()
|
||||
|
||||
# Resample to 24h
|
||||
data_24h = df_4h.resample("24H").mean()
|
||||
balances_24h = pd.HDFStore('balances_24h.h5')
|
||||
balances_24h['df_24h'] = data_24h
|
||||
balances_24h.close()
|
||||
|
||||
except Exception:
|
||||
message = "Error in resampling data"
|
||||
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
|
||||
return ()
|
||||
|
||||
|
||||
def delete_old_files():
|
||||
"""Delete old files in the directory."""
|
||||
try:
|
||||
directory = "path_to_your_directory"
|
||||
for file_name in os.listdir(directory):
|
||||
file_path = os.path.join(directory, file_name)
|
||||
if os.path.getmtime(file_path) < time.time() - 7 * 86400:
|
||||
os.remove(file_path)
|
||||
except Exception:
|
||||
message = "Error in deleting old files"
|
||||
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
|
||||
return ()
|
||||
|
||||
|
||||
def archive_data():
|
||||
"""Archive old data to a zip file."""
|
||||
try:
|
||||
archive_name = f"archive_data_{TODAY}.zip"
|
||||
if not os.path.exists('archive_data'):
|
||||
os.makedirs('archive_data')
|
||||
shutil.make_archive(f'archive_data/{archive_name}', 'zip', 'path_to_your_directory')
|
||||
except Exception:
|
||||
message = "Error in archiving data"
|
||||
telegram_send_message(BOT_TOKEN, BOT_CHAT_ID, message)
|
||||
return ()
|
||||
|
||||
|
||||
# Scheduling tasks
|
||||
schedule.every().day.at("00:00").do(archive_data)
|
||||
schedule.every().day.at("01:00").do(delete_old_files)
|
||||
schedule.every().day.at("02:00").do(resample_data)
|
||||
schedule.every().day.at("02:30").do(plot_long)
|
||||
schedule.every().day.at("03:00").do(plot_30days)
|
||||
schedule.every().day.at("03:30").do(plot_7days)
|
||||
schedule.every().day.at("04:00").do(plot_24h)
|
||||
schedule.every().day.at("05:00").do(send_plots)
|
||||
|
||||
# Main loop
|
||||
while True:
|
||||
schedule.run_pending()
|
||||
time.sleep(1)
|
||||
@@ -1 +0,0 @@
|
||||
4164
|
||||
@@ -1,699 +0,0 @@
|
||||
"""
|
||||
MattC - 2025
|
||||
This code is a working prototype and is intended for initial testing and development purposes. Some Python
|
||||
standards, including but not limited to PEP 8 compliance, error handling, and code optimization, are yet to be fully
|
||||
implemented. Further refactoring and enhancements are planned to improve readability, maintainability,
|
||||
and efficiency.
|
||||
"""
|
||||
|
||||
import os
|
||||
from os import walk
|
||||
import json
|
||||
from datetime import datetime, timedelta
|
||||
import pandas as pd
|
||||
import shutil
|
||||
from pathlib import Path
|
||||
import logging
|
||||
import matplotlib.pyplot as plt # pip install matplotlib
|
||||
import matplotlib.dates as mdates
|
||||
import warnings
|
||||
import urllib
|
||||
from urllib.request import urlopen
|
||||
import time
|
||||
|
||||
warnings.simplefilter(action='ignore', category=FutureWarning)
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
# TODO sort out download data
|
||||
class WalkForward(object):
|
||||
|
||||
def __init__(self, strategy_path, strategy, config, output_dir, wf_start, wf_finish, anchored_start, min_trades,
|
||||
in_sample_days, out_sample_days, loss_function, cpu, epochs, wallet="2500", fee="0.002",
|
||||
pre_live=False, re_opt=False, re_opt_t="5m 1h 1d"):
|
||||
|
||||
self.in_sample_days = in_sample_days
|
||||
self.out_sample_days = out_sample_days
|
||||
self.loss_function = loss_function
|
||||
self.re_opt = re_opt
|
||||
self.output_dir = self.create_run_dir(output_dir)
|
||||
self.config = self.copy_input_config(config)
|
||||
self.strategy_path = strategy_path
|
||||
self.strategy_name = strategy
|
||||
self.strategy = self.copy_input_strategy(strategy_path, self.strategy_name)
|
||||
self.wf_start = wf_start
|
||||
self.wf_finish = wf_finish
|
||||
self.anchored_start = anchored_start
|
||||
self.is_start = self.in_sample_start()
|
||||
self.min_trades = min_trades
|
||||
self.cpu = cpu
|
||||
self.epochs = epochs
|
||||
self.wallet = wallet
|
||||
self.fee = fee
|
||||
self.pre_live = pre_live
|
||||
self.re_opt_t = re_opt_t
|
||||
self.start_log()
|
||||
|
||||
def run_walk_forward(self):
|
||||
|
||||
stages = self.generate_wf_stages()
|
||||
no_stages = len(stages)
|
||||
start_wallet = self.wallet
|
||||
|
||||
start_time = datetime.now()
|
||||
logger.info(f'Running Walk-forward for {no_stages} stages')
|
||||
|
||||
for count, value in enumerate(stages, 1):
|
||||
stage_start_time = datetime.now()
|
||||
logger.info(f'{"-" * 79}')
|
||||
hyperopt_time = value[0]
|
||||
backtest_time = value[1]
|
||||
full_time_period = f"{hyperopt_time.split('-')[0]}-{backtest_time.split('-')[1]}"
|
||||
logger.info(f'Walk-forward optimization for stage {count} of {no_stages}')
|
||||
stage_dir = self.create_stage_dir(self.output_dir, count, full_time_period)
|
||||
|
||||
# Run Hyperopt and process required data
|
||||
logger.info(f'Hyperopting for {hyperopt_time}')
|
||||
cpu = self.set_cpu(hyperopt_time)
|
||||
self.run_hyperopt(hyperopt_time, stage_dir, self.epochs, cpu)
|
||||
hy_start = datetime.strptime(hyperopt_time.split('-')[0], "%Y%m%d")
|
||||
hy_finish = datetime.strptime(hyperopt_time.split('-')[1], "%Y%m%d")
|
||||
hy_delta = hy_start - hy_finish
|
||||
hy_days = int(hy_delta.days)
|
||||
logger.info(f'Running Hyperopt Backtest for period {hyperopt_time} ({hy_days} days)')
|
||||
result_op, bt_file_op = self.run_backtest(hyperopt_time, "op_bt", start_wallet, stage_dir)
|
||||
self.save_bt_file_data(stage_dir, bt_file_op, "op_bt")
|
||||
df_op = self.update_df(result_op, "op_bt", count, hyperopt_time, start_wallet)
|
||||
|
||||
# Walk forward testing constant starting wallet:
|
||||
logger.info(f'Running WF Backtest for period {backtest_time} ({self.out_sample_days} days)')
|
||||
result_bt, bt_file_bt = self.run_backtest(backtest_time, "wf_bt", start_wallet, stage_dir)
|
||||
self.save_bt_file_data(stage_dir, bt_file_bt, "wf_bt")
|
||||
df_wf = self.update_df(result_bt, "wf_bt", count, backtest_time, start_wallet)
|
||||
plot_equity_curve(df_wf, self.output_dir, save_fig=True)
|
||||
|
||||
self.combine_data(hyperopt_time, df_op, df_wf)
|
||||
stage_run_time = str(datetime.now() - stage_start_time)
|
||||
logger.info(f'Stage {count} walk-forward analysis Duration: {stage_run_time.split(".")[0]}')
|
||||
|
||||
if self.pre_live:
|
||||
self.pre_live_optimise()
|
||||
|
||||
if self.re_opt:
|
||||
self.re_optimise()
|
||||
|
||||
run_time = str(datetime.now() - start_time)
|
||||
logger.info(f'Total walk-forward analysis Duration: {run_time.split(".")[0]}')
|
||||
|
||||
def re_optimise(self):
|
||||
# Download data
|
||||
# time_periods = "5m 15m 1h 4h 12h 1d"
|
||||
time_periods = self.re_opt_t
|
||||
self.download_data(time_periods)
|
||||
|
||||
end_date = datetime.strptime(self.wf_finish, "%Y%m%d")
|
||||
start_date = end_date - timedelta(int(self.in_sample_days))
|
||||
t1 = start_date.strftime("%Y%m%d")
|
||||
t2 = end_date.strftime("%Y%m%d")
|
||||
hyperopt_time = t1 + "-" + t2
|
||||
full_time_period = f"{hyperopt_time}"
|
||||
|
||||
stage_dir = self.create_stage_dir(self.output_dir, "re_optimise", full_time_period)
|
||||
epochs = f"{int(self.epochs)}"
|
||||
self.run_hyperopt(hyperopt_time, stage_dir, epochs, self.cpu)
|
||||
logger.info(f'Running Hyperopt Backtest for period {hyperopt_time} ({self.in_sample_days} days)')
|
||||
_ = self.run_backtest(hyperopt_time, "re_optimise", self.wallet, stage_dir)
|
||||
|
||||
def save_bt_file_data(self, stage_dir, bt_file_op, file_id):
|
||||
|
||||
with open(f'{stage_dir}/{bt_file_op}') as f:
|
||||
data1 = json.load(f)
|
||||
df1 = pd.DataFrame.from_dict(data1["strategy"][self.strategy_name]["results_per_pair"])
|
||||
csv_filepath = f"{stage_dir}/{file_id}_bt_results.csv"
|
||||
df1.to_csv(csv_filepath)
|
||||
|
||||
def update_df(self, result, file_id, wf_stage, time_range, start_wallet):
|
||||
|
||||
h5_string = f"{self.output_dir}/{file_id}.h5"
|
||||
if not os.path.exists(h5_string):
|
||||
cols = ["profit_mean", "profit_mean_pct", "profit_sum", "profit_sum_pct", "profit_total_abs",
|
||||
"profit_total_pct" "profit_total", "wins", "draws", "losses", "wf-stage", "bt_time_period",
|
||||
"start-balance", "final-balance", "%_profit_pa", "acc-start", "acc-finish"]
|
||||
df = pd.DataFrame(columns=cols)
|
||||
df.to_hdf(h5_string, 'data')
|
||||
|
||||
df = pd.read_hdf(h5_string, 'data')
|
||||
|
||||
if file_id == "op_bt":
|
||||
is_start = datetime.strptime(time_range.split('-')[0], "%Y%m%d")
|
||||
is_finish = datetime.strptime(time_range.split('-')[1], "%Y%m%d")
|
||||
delta = is_start - is_finish
|
||||
sample_days = int(delta.days)
|
||||
else:
|
||||
sample_days = self.out_sample_days
|
||||
|
||||
data = result[0]
|
||||
data.pop('key')
|
||||
data["%_profit_pa"] = data["profit_total_pct"] / float(sample_days) * 365 # percent profit year
|
||||
data["wf-stage"] = wf_stage
|
||||
data["start-balance"] = start_wallet
|
||||
data["final-balance"] = float(start_wallet) + float(data["profit_total_abs"])
|
||||
data["bt_time_period"] = time_range
|
||||
|
||||
if len(df) == 0:
|
||||
data["acc-start"] = float(self.wallet)
|
||||
else:
|
||||
data["acc-start"] = df["acc-finish"].iloc[-1]
|
||||
|
||||
data["acc-finish"] = (data["acc-start"] * data["profit_total_pct"] / 100) + data["acc-start"]
|
||||
df2 = pd.DataFrame(data, index=[0])
|
||||
df_new = df.copy()
|
||||
df_new = df_new.append([df2], ignore_index=True)
|
||||
|
||||
csv_filepath = f"{self.output_dir}/{file_id}.csv"
|
||||
df_new.to_csv(csv_filepath)
|
||||
df_new.to_hdf(h5_string, 'data')
|
||||
|
||||
return df_new
|
||||
|
||||
def pre_live_optimise(self):
|
||||
|
||||
end_date = datetime.strptime(self.wf_finish, "%Y%m%d")
|
||||
start_date = end_date - timedelta(int(self.in_sample_days))
|
||||
t1 = start_date.strftime("%Y%m%d")
|
||||
t2 = end_date.strftime("%Y%m%d")
|
||||
hyperopt_time = t1 + "-" + t2
|
||||
full_time_period = f"{hyperopt_time}"
|
||||
|
||||
stage_dir = self.create_stage_dir(self.output_dir, "pre_live", full_time_period)
|
||||
epochs = f"{int(self.epochs) * 2}"
|
||||
self.run_hyperopt(hyperopt_time, stage_dir, epochs, self.cpu)
|
||||
logger.info(f'Running Hyperopt Backtest for period {hyperopt_time} ({self.in_sample_days} days)')
|
||||
result_op = self.run_backtest(hyperopt_time, "pre_live", self.wallet, stage_dir)
|
||||
|
||||
def combine_data(self, hyp_time_range, df_op, df_wf):
|
||||
|
||||
is_start = datetime.strptime(hyp_time_range.split('-')[0], "%Y%m%d")
|
||||
is_finish = datetime.strptime(hyp_time_range.split('-')[1], "%Y%m%d")
|
||||
delta = is_start - is_finish
|
||||
days_in = int(delta.days)
|
||||
|
||||
days_out = int(self.out_sample_days)
|
||||
|
||||
try:
|
||||
a = pd.Series(df_op["profit_mean_pct"], name='op_profit_av')
|
||||
b = pd.Series(df_wf["profit_mean_pct"], name='wf_profit_av')
|
||||
c = pd.Series(df_op["wins"] / df_op["losses"], name='op_wl%')
|
||||
d = pd.Series(df_wf["wins"] / df_wf["losses"], name='wf_wl%')
|
||||
e = pd.Series(df_op['trades'].apply(lambda x: x / days_in), name='op_trades_per_day')
|
||||
f = pd.Series(df_wf['trades'].apply(lambda x: x / days_out), name='wf_trades_per_day')
|
||||
g = pd.Series(df_op["profit_total"].apply(lambda x: (x / days_in) * 365), name='op_ppa')
|
||||
h = pd.Series(df_wf["profit_total"].apply(lambda x: (x / days_out) * 365), name='wf_ppa')
|
||||
i = pd.Series(df_op["%_profit_pa"], name='op_%ppa')
|
||||
j = pd.Series(df_wf["%_profit_pa"], name='wf_%ppa')
|
||||
|
||||
df_combined = pd.concat([a, b, c, d, e, f, g, h, i, j], axis=1)
|
||||
filepath = f"{self.output_dir}/combined.csv"
|
||||
df_combined.to_csv(filepath)
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
return
|
||||
|
||||
def generate_wf_stages(self):
|
||||
|
||||
is_start = datetime.strptime(self.is_start, "%Y%m%d")
|
||||
oos_start = datetime.strptime(self.wf_start, "%Y%m%d")
|
||||
end_date = datetime.strptime(self.wf_finish, "%Y%m%d")
|
||||
oos_end = oos_start + timedelta(int(self.out_sample_days))
|
||||
stages = []
|
||||
while True:
|
||||
|
||||
t1 = is_start.strftime("%Y%m%d")
|
||||
t2 = oos_start.strftime("%Y%m%d")
|
||||
t3 = oos_end.strftime("%Y%m%d")
|
||||
|
||||
# define stage:
|
||||
insample_timframe = t1 + "-" + t2
|
||||
outsample_timframe = t2 + "-" + t3
|
||||
stage = [insample_timframe, outsample_timframe]
|
||||
stages.append(stage)
|
||||
|
||||
oos_start = oos_start + timedelta(int(self.out_sample_days))
|
||||
oos_end = oos_start + timedelta(int(self.out_sample_days))
|
||||
|
||||
if not self.in_sample_days == "anchored":
|
||||
is_start = is_start + timedelta(int(self.out_sample_days))
|
||||
|
||||
if oos_end > end_date:
|
||||
break
|
||||
|
||||
return stages
|
||||
|
||||
def run_hyperopt(self, time_range, stage_dir, epochs, cpu):
|
||||
"""
|
||||
:param stage_number:
|
||||
:param time_range:
|
||||
:param epochs:
|
||||
:param loss_function: SortinoHyperOptLoss,
|
||||
:param fee:
|
||||
:param cpu:
|
||||
:return:
|
||||
"""
|
||||
self.wait_for_internet_connection()
|
||||
start_time = datetime.now()
|
||||
os.system(
|
||||
"freqtrade hyperopt" +
|
||||
" --min-trades " + self.min_trades +
|
||||
" -j " + cpu +
|
||||
" -e " + epochs +
|
||||
" --spaces buy " +
|
||||
" --fee " + self.fee +
|
||||
" --logfile " + stage_dir + "/op_log" +
|
||||
" --timerange " + time_range +
|
||||
" --hyperopt-loss " + self.loss_function +
|
||||
" --strategy " + self.strategy +
|
||||
" --strategy-path " + self.output_dir +
|
||||
" --config " + self.config +
|
||||
" --dry-run-wallet " + self.wallet
|
||||
)
|
||||
|
||||
self.wait_for_internet_connection()
|
||||
os.system(
|
||||
"freqtrade hyperopt-list" +
|
||||
" --no-details " +
|
||||
" --export-csv " + stage_dir + "/op.csv"
|
||||
)
|
||||
|
||||
# # Copy the best optimisation results to "stage output directory":
|
||||
src = Path(f"{self.output_dir}/{self.strategy}.json")
|
||||
dst = f"{stage_dir}/op_result.json"
|
||||
shutil.copyfile(str(src), dst)
|
||||
|
||||
run_time = str(datetime.now() - start_time)
|
||||
logger.info(f'Hyperopt Duration: {run_time.split(".")[0]}')
|
||||
|
||||
with open(dst) as f:
|
||||
data = json.load(f)
|
||||
|
||||
results = data["params"]["buy"]
|
||||
for i in results:
|
||||
logger.info(f'Hyperopt result: {i}: {results[i]}')
|
||||
|
||||
def run_backtest(self, time_range, file_id, wallet, stage_dir):
|
||||
self.wait_for_internet_connection()
|
||||
start_time = datetime.now()
|
||||
os.system(
|
||||
"freqtrade backtesting" +
|
||||
" --export trades " +
|
||||
" --fee " + self.fee +
|
||||
f" --logfile {stage_dir}/{file_id}_log.txt"
|
||||
" --timerange " + time_range +
|
||||
" --strategy " + self.strategy +
|
||||
" --strategy-path " + self.output_dir +
|
||||
" --config " + self.config +
|
||||
" --dry-run-wallet " + wallet +
|
||||
f" --export-filename {stage_dir}/{file_id}_result.json"
|
||||
)
|
||||
|
||||
result, bt_file = self.get_backtest_data(stage_dir, file_id)
|
||||
run_time = str(datetime.now() - start_time)
|
||||
if not file_id == "wf_acc_bt":
|
||||
logger.info(f'Backtest Duration: {run_time.split(".")[0]}')
|
||||
|
||||
self.log_bt_results(time_range, result, wallet, file_id)
|
||||
self.plot_bt_profit(time_range, stage_dir, bt_file, file_id)
|
||||
|
||||
return result, bt_file
|
||||
|
||||
def download_data(self, time_periods, days="4000"):
|
||||
self.wait_for_internet_connection()
|
||||
logger.info(f'downloading data')
|
||||
os.system(
|
||||
"freqtrade download-data" +
|
||||
" -t " + time_periods +
|
||||
" --exchange binance " +
|
||||
" --pairs .*/USDT " +
|
||||
" --new-pairs-days " + days +
|
||||
" --include-inactive-pairs "
|
||||
)
|
||||
logger.info(f'finished downloading data')
|
||||
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def wait_for_internet_connection():
|
||||
start_time = datetime.now()
|
||||
switch = True
|
||||
while True:
|
||||
try:
|
||||
urlopen('https://www.google.com', timeout=1)
|
||||
if not switch:
|
||||
offline_time = str(datetime.now() - start_time)
|
||||
logger.warning(f"Disconnected time: {offline_time.split('.')[0]}")
|
||||
logger.warning("#############################")
|
||||
return
|
||||
|
||||
except urllib.error.URLError:
|
||||
|
||||
if switch:
|
||||
logger.warning("#############################")
|
||||
logger.warning("NO INTERNET")
|
||||
|
||||
switch = False
|
||||
time.sleep(2)
|
||||
|
||||
pass
|
||||
|
||||
def log_bt_results(self, time_range, result, wallet, file_id):
|
||||
|
||||
data = result[0].copy()
|
||||
|
||||
final_balance = float(wallet) + data["profit_total_abs"]
|
||||
w_start = round(float(wallet), 2)
|
||||
w_finish = round(final_balance, 2)
|
||||
percent_prof = round(data["profit_total_pct"], 1)
|
||||
|
||||
if file_id == "op_bt":
|
||||
|
||||
is_start = datetime.strptime(time_range.split('-')[0], "%Y%m%d")
|
||||
is_finish = datetime.strptime(time_range.split('-')[1], "%Y%m%d")
|
||||
delta = is_start - is_finish
|
||||
sample_days = int(delta.days)
|
||||
|
||||
else:
|
||||
sample_days = self.out_sample_days
|
||||
|
||||
pppa = round((percent_prof / float(sample_days) * 365), 2) # percent profit per year
|
||||
logger.info(f'Backtest result - Balance £{w_start} --> £{w_finish} ({percent_prof}%): {pppa} %profit pa')
|
||||
|
||||
# Trade stats
|
||||
win = data['wins']
|
||||
loss = data['losses']
|
||||
draw = data['draws']
|
||||
trades = data['trades']
|
||||
logger.info(f"Backtest result - Wins: {win}, Draws: {draw}, Losses: {loss}, trades: {trades}")
|
||||
|
||||
# Average tade profits:
|
||||
mean_p = round(float(data['profit_mean']), 2)
|
||||
mp_percent = round(float(data['profit_mean_pct']), 2)
|
||||
logger.info(f"Backtest result - mean trade profit £{mean_p}, {mp_percent}%")
|
||||
|
||||
# Account draw-down:
|
||||
dd_percent = round((data['max_drawdown_account'] * 100), 2)
|
||||
dd_abs = round(float(data['max_drawdown_abs']), 2)
|
||||
logger.info(f"Backtest result - Max dd: {dd_percent}%, £{dd_abs}")
|
||||
|
||||
def plot_bt_profit(self, time_range, stage_dir, bt_file, file_id):
|
||||
os.system(
|
||||
"freqtrade plot-profit "
|
||||
" --timeframe 1d "
|
||||
" --timerange " + time_range +
|
||||
" --strategy " + self.strategy +
|
||||
" --strategy-path " + self.output_dir +
|
||||
" --config " + self.config +
|
||||
f" --export-filename {stage_dir}/{bt_file}"
|
||||
)
|
||||
|
||||
# TODO relative path required:
|
||||
src = Path(f"/home/matt/freqtrade/user_data/plot/freqtrade-profit-plot.html")
|
||||
dst = f"{stage_dir}/{file_id}_profit-plot.html"
|
||||
shutil.copyfile(str(src), dst)
|
||||
|
||||
@staticmethod
|
||||
def get_backtest_data(stage_dir, file_id):
|
||||
f = []
|
||||
for (dirpath, dirnames, filenames) in walk(stage_dir):
|
||||
f.extend(filenames)
|
||||
break
|
||||
|
||||
# Find the backtest file for wf stage
|
||||
for file in f:
|
||||
|
||||
# not meta.json:
|
||||
if file[-9:-5] != "meta":
|
||||
|
||||
my_file = file_id + "_result"
|
||||
if file.split("-")[0] == my_file:
|
||||
# change to while open:
|
||||
f = open(stage_dir + "/" + file)
|
||||
data = json.load(f)
|
||||
result = data["strategy_comparison"]
|
||||
backtest_file = file
|
||||
|
||||
return result, backtest_file
|
||||
|
||||
def create_run_dir(self, path):
|
||||
|
||||
_time = datetime.now().strftime('%Y%m%d_%I:%M%p')
|
||||
if self.re_opt:
|
||||
directory = f"{path}/{_time}_{self.loss_function}_re-optimise_{self.in_sample_days}"
|
||||
else:
|
||||
directory = f"{path}/{_time}_{self.loss_function}_in_{self.in_sample_days}_out_{self.out_sample_days}"
|
||||
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
try:
|
||||
os.makedirs(directory)
|
||||
|
||||
except:
|
||||
pass
|
||||
|
||||
return directory
|
||||
|
||||
@staticmethod
|
||||
def create_stage_dir(path, stage, full_time_period):
|
||||
dir_string = f"{path}/stage_{stage}_{full_time_period}"
|
||||
if not os.path.exists(path):
|
||||
os.makedirs(path)
|
||||
|
||||
try:
|
||||
os.makedirs(dir_string)
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return dir_string
|
||||
|
||||
def copy_input_config(self, in_config):
|
||||
# Copy config:
|
||||
config = f"{in_config}"
|
||||
dst_config = f"{self.output_dir}/{config.split('/')[-1]}"
|
||||
shutil.copyfile(config, dst_config)
|
||||
return dst_config
|
||||
|
||||
def copy_input_strategy(self, in_strategy_path, strategy):
|
||||
# Copy Strategy
|
||||
src = f"{in_strategy_path}/{strategy}.py"
|
||||
dst_strategy = f"{self.output_dir}/{strategy}.py"
|
||||
shutil.copyfile(src, dst_strategy)
|
||||
return strategy
|
||||
|
||||
def in_sample_start(self):
|
||||
|
||||
oos_start = datetime.strptime(self.wf_start, "%Y%m%d")
|
||||
|
||||
if self.in_sample_days == "anchored":
|
||||
is_start = datetime.strptime(self.anchored_start, "%Y%m%d") # + oos days
|
||||
|
||||
else:
|
||||
is_start = oos_start - timedelta(int(self.in_sample_days))
|
||||
|
||||
is_start = is_start.strftime("%Y%m%d")
|
||||
|
||||
return is_start
|
||||
|
||||
def set_cpu(self, hyperopt_time):
|
||||
|
||||
is_start = datetime.strptime(hyperopt_time.split('-')[0], "%Y%m%d")
|
||||
is_finish = datetime.strptime(hyperopt_time.split('-')[1], "%Y%m%d")
|
||||
delta = is_finish - is_start
|
||||
days = int(delta.days)
|
||||
|
||||
if self.in_sample_days == "anchored":
|
||||
if days < 50:
|
||||
cpu = "-1"
|
||||
if days > 50:
|
||||
cpu = "-2"
|
||||
if days > 100:
|
||||
cpu = "-4"
|
||||
if days > 150:
|
||||
cpu = "-6"
|
||||
if days > 200:
|
||||
cpu = "-8"
|
||||
if days > 250:
|
||||
cpu = "-10"
|
||||
if days > 300:
|
||||
cpu = "-11"
|
||||
if days > 350:
|
||||
cpu = "-12"
|
||||
if days > 400:
|
||||
cpu = "-13"
|
||||
if days > 450:
|
||||
cpu = "-14"
|
||||
if days > 500:
|
||||
cpu = "-14"
|
||||
if days > 550:
|
||||
cpu = "-16"
|
||||
if days > 600:
|
||||
cpu = "-16"
|
||||
if days > 650:
|
||||
cpu = "-17"
|
||||
if days > 700:
|
||||
cpu = "-17"
|
||||
if days > 750:
|
||||
cpu = "-17"
|
||||
if days > 800:
|
||||
cpu = "-18"
|
||||
if days > 850:
|
||||
cpu = "-18"
|
||||
if days > 900:
|
||||
cpu = "-18"
|
||||
if days > 950:
|
||||
cpu = "-19"
|
||||
if days > 1000:
|
||||
cpu = "-19"
|
||||
if days > 1100:
|
||||
cpu = "-20"
|
||||
else:
|
||||
d1 = datetime.strptime("20220601", "%Y%m%d")
|
||||
d2 = datetime.strptime("20210101", "%Y%m%d")
|
||||
d3 = datetime.strptime("20200101", "%Y%m%d")
|
||||
d4 = datetime.strptime("20190101", "%Y%m%d")
|
||||
d5 = datetime.strptime("20180101", "%Y%m%d")
|
||||
|
||||
if is_finish > d1:
|
||||
cpu = self.cpu
|
||||
|
||||
if d2 < is_finish < d1:
|
||||
cpu = int(self.cpu) + 1
|
||||
|
||||
if d3 < is_finish < d2:
|
||||
cpu = int(self.cpu) + 2
|
||||
|
||||
if d4 < is_finish < d3:
|
||||
cpu = int(self.cpu) + 3
|
||||
|
||||
if d5 < is_finish < d4:
|
||||
cpu = int(self.cpu) + 4
|
||||
|
||||
if is_finish < d5:
|
||||
cpu = int(self.cpu) + 5
|
||||
|
||||
return str(cpu)
|
||||
|
||||
def start_log(self):
|
||||
# Set format and level:
|
||||
logger.setLevel(logging.INFO)
|
||||
formatter = logging.Formatter('%(asctime)s - WF - %(levelname)s - %(message)s')
|
||||
|
||||
# Remove old handlers:
|
||||
while logger.handlers:
|
||||
logger.handlers.pop()
|
||||
|
||||
# Define file handler:
|
||||
file_handler = logging.FileHandler(f'{self.output_dir}/wf.log')
|
||||
file_handler.setFormatter(formatter)
|
||||
logger.addHandler(file_handler)
|
||||
|
||||
# Define console handler:
|
||||
console_handler = logging.StreamHandler()
|
||||
console_handler.setFormatter(formatter)
|
||||
logger.addHandler(console_handler)
|
||||
|
||||
# Startup logs:
|
||||
_cpu = 20 + int(self.cpu) + 1
|
||||
start_date = datetime.strptime(self.is_start, '%Y%m%d').date()
|
||||
end_date = datetime.strptime(self.wf_finish, '%Y%m%d').date()
|
||||
run_time = str(end_date - start_date).split(",")[0]
|
||||
logger.info('Code Initiated')
|
||||
logger.info(f'Strategy:{self.strategy_path}/{self.strategy}')
|
||||
logger.info(f'Config:{self.config}')
|
||||
logger.info(f'Loss Function: {self.loss_function}')
|
||||
logger.info(f'Time Frame:{self.is_start}-{self.wf_finish} ({run_time})')
|
||||
logger.info(f'IS-days:{self.in_sample_days}, OOS-days:{self.out_sample_days}')
|
||||
logger.info(f'CPUs:{_cpu}, Epochs:{self.epochs}, Wallet:{"2500"}, Fee:{"0.002"}, Min-trades:{self.min_trades}')
|
||||
|
||||
|
||||
def plot_equity_curve(df, output_dir, save_fig=False):
|
||||
df = df.copy()
|
||||
pd.set_option('display.max_columns', None)
|
||||
fig, axs = plt.subplots(figsize=(7, 4))
|
||||
axs.xaxis.set_major_formatter(mdates.DateFormatter("%d %b"))
|
||||
df['dates'] = df["bt_time_period"].apply(lambda i: i.split("-")[1])
|
||||
df['dt'] = df['dates'].apply(lambda i: datetime.strptime(i, '%Y%m%d'))
|
||||
df.set_index('dt')
|
||||
df.plot(ax=axs, x="dt", y="acc-finish")
|
||||
dela_y = int(df["acc-finish"].max()) - int(df["acc-finish"].min())
|
||||
y_min = int(df["acc-finish"].min()) - (dela_y * 0.05)
|
||||
y_max = int(df["acc-finish"].max()) + (dela_y * 0.05)
|
||||
axs.set_title('Walk-Forward equity curve')
|
||||
axs.set_ylim(y_min, y_max)
|
||||
axs.set_ylabel("")
|
||||
axs.set_xlabel("")
|
||||
axs.grid(color='grey', alpha=0.5, linestyle='dashed', linewidth=0.5)
|
||||
axs.yaxis.set_major_formatter("£" + '{x:1.0f}')
|
||||
# plt.show()
|
||||
|
||||
if save_fig:
|
||||
try:
|
||||
plt.savefig(f"{output_dir}/wf_equity_curve.png")
|
||||
except:
|
||||
pass
|
||||
|
||||
|
||||
def walk_forward(path, strategy, config, output_dir, wf_start, wf_finish, anchored_start, pre_live=False, re_opt=False):
|
||||
is_list = ["730"]
|
||||
oos_list = ["30"]
|
||||
n_trades = ["100"]
|
||||
cpu_list = ["-15"]
|
||||
|
||||
ep = "100"
|
||||
loss_f = "SharpeHyperOptLoss"
|
||||
|
||||
for count, is_days in enumerate(is_list):
|
||||
oos_days = oos_list[count]
|
||||
nt = n_trades[count]
|
||||
cores = cpu_list[count]
|
||||
|
||||
wf = WalkForward(strategy_path=path, strategy=strategy, config=config, output_dir=output_dir,
|
||||
wf_start=wf_start, wf_finish=wf_finish, anchored_start=anchored_start, epochs=ep,
|
||||
loss_function=loss_f, in_sample_days=is_days, out_sample_days=oos_days, min_trades=nt,
|
||||
cpu=cores, pre_live=pre_live, re_opt=re_opt)
|
||||
|
||||
wf.run_walk_forward()
|
||||
return
|
||||
|
||||
|
||||
def re_optimise(path, strategy, config, output_dir, cpu="-19"):
|
||||
today = datetime.now().strftime("%Y%m%d")
|
||||
loss_f = "SortinoHyperOptLoss"
|
||||
in_sample_days = "730"
|
||||
n_trades = "100"
|
||||
ep = "200"
|
||||
download_data_t = "5m 1h 1d"
|
||||
|
||||
wf = WalkForward(strategy_path=path, strategy=strategy, config=config, output_dir=output_dir, wf_start=today,
|
||||
wf_finish=today, anchored_start=today, epochs=ep, loss_function=loss_f,
|
||||
in_sample_days=in_sample_days, out_sample_days="1", min_trades=n_trades, cpu=cpu,
|
||||
pre_live=False, re_opt=True, re_opt_t=download_data_t)
|
||||
|
||||
wf.re_optimise()
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
WF_START = "20200101" # 2023-03-25
|
||||
WF_END = "20230910"
|
||||
ANCHORED_START = "20200101" # only used if in_sample_days == "anchored"
|
||||
STRATEGY_PATH_THOR = "/home/matt/freqtrade/user_data/strategies/Thor"
|
||||
STRATEGY_THOR = "Optimise_Thor_BuySig_RiskReward"
|
||||
CONFIG_THOR = "/home/matt/freqtrade/user_data/strategies/Thor/config_Thor_WF.json"
|
||||
OUTPUT_DIR_THOR = "/home/matt/freqtrade/user_data/strategies/Thor/walk_forward"
|
||||
|
||||
# -------------------------------------------------------------
|
||||
walk_forward(STRATEGY_PATH_THOR, STRATEGY_THOR, CONFIG_THOR, OUTPUT_DIR_THOR, WF_START, WF_END, ANCHORED_START)
|
||||
|
||||
# -------------------------------------------------------------
|
||||
re_optimise(STRATEGY_PATH_THOR, STRATEGY_THOR, CONFIG_THOR, OUTPUT_DIR_THOR)
|
||||
@@ -1,55 +1,121 @@
|
||||
# MT5_Python_Strategy_Framework
|
||||
# MyLibs for MetaTrader 5
|
||||
|
||||
This project provides an experimental framework for integrating MetaTrader 5 (MT5) custom indicators and trading logic with Python-based data processing and strategy testing using [Freqtrade](https://www.freqtrade.io/).
|
||||
This is a modular MQL5 codebase containing reusable classes and utilities designed to accelerate EA development, backtesting, and live deployment for MetaTrader 5. It includes complete solutions for trade management, risk control, signal generation, time-based utilities, and more.
|
||||
|
||||
## Features
|
||||
|
||||
- 🧠 **Custom MT5 Libraries**: Modular `.mqh` files to handle position sizing, drawdown control, order management, and utility functions.
|
||||
- 🐍 **Python Scripts**:
|
||||
- `pre_process.py`: Prepares or cleans data before indicator processing.
|
||||
- `process_entry_indicators.py`: Extracts and processes entry signals.
|
||||
- `post_process_test.py`: Analyses backtest output or result data.
|
||||
- 📦 **Freqtrade-Compatible Module**: Python strategies and helpers located in `Python_freqtrade/` for integration with the Freqtrade framework.
|
||||
- 🛠️ **Project Structure Support**: Includes `.idea/` and `.vscode/` folders for JetBrains and VSCode IDE configurations.
|
||||
|
||||
## Project Structure
|
||||
> **Note**: To use this framework, the `MyLibs` folder must be placed inside your MetaTrader 5 **`Include/`** directory:
|
||||
>
|
||||
> ```
|
||||
> MQL5/Include/MyLibs/
|
||||
> ```
|
||||
|
||||
## Folder Structure
|
||||
```
|
||||
MT5_Python_Strategy_Framework/
|
||||
├── My_MQL5_Libs/ # Custom MQL5 include files
|
||||
├── Python_freqtrade/ # Freqtrade strategy components
|
||||
├── pre_process.py # Data pre-processing script
|
||||
├── process_entry_indicators.py # Entry signal extraction logic
|
||||
├── post_process_test.py # Backtest result post-processing
|
||||
├── .idea/, .vscode/ # IDE configs (optional)
|
||||
└── README.md # Project documentation
|
||||
MyLibs/
|
||||
├── BacktestUtils/
|
||||
│ ├── CustomMax.mqh # Defines custom optimization criteria for backtesting
|
||||
│ └── TestDataSplit.mqh # Supports splitting historical data into training/testing segments
|
||||
│
|
||||
├── Indicators/
|
||||
│ ├── AtrBands.mqh # Calculates and visualizes ATR-based dynamic bands
|
||||
│ └── TrendlineAnalyser.mqh # Detects trendline breaks and trend direction changes
|
||||
│
|
||||
├── Orders/
|
||||
│ ├── AdjustPosition.mqh # Manages stop loss adjustments, trailing stops, and breakevens
|
||||
│ ├── CalculatePositionData.mqh # Computes lot sizing and position metrics
|
||||
│ ├── EntryOrders.mqh # Handles logic for opening buy/sell orders
|
||||
│ ├── ExitOrders.mqh # Handles logic for closing trades under various conditions
|
||||
│ ├── OrderTracker.mqh # Tracks open orders and their metadata
|
||||
│ └── StopLogic.mqh # Provides rule-based SL/TP value resolution
|
||||
│
|
||||
├── RiskManagement/
|
||||
│ └── DrawdownControl.mqh # Implements drawdown-based risk controls
|
||||
│
|
||||
├── Strategy/
|
||||
│ └── RangeCalculator.mqh # Measures recent market range to inform signal logic
|
||||
│
|
||||
├── Utils/
|
||||
│ ├── AtrHandleManager.mqh # Efficiently manages and caches ATR indicator handles
|
||||
│ ├── ChartUtils.mqh # Utilities for drawing and annotating charts
|
||||
│ ├── DealingWithTime.mqh # Time conversion and formatting helpers
|
||||
│ ├── Enums.mqh # Enum declarations for inputs and logic flow
|
||||
│ ├── MarketDataUtils.mqh # Simplifies access to indicator buffers and price info
|
||||
│ ├── MultiSymbolSignalTracker.mqh # Tracks per-symbol signal state in multi-asset EAs
|
||||
│ ├── ResourceManager.mqh # Central manager for indicator handle cleanup
|
||||
│ ├── SignalStateTracker.mqh # Tracks signal timing (e.g., how many bars ago a trigger occurred)
|
||||
│ ├── TimeZones.mqh # Handles timezone conversion and time window logic
|
||||
│ └── TradeWindow.mqh # Defines and manages tradable time sessions
|
||||
```
|
||||
|
||||
## Getting Started
|
||||
## Included EA Example
|
||||
|
||||
### Requirements
|
||||
This repository also includes a full EA (`trend_following_ea.mq5`) that demonstrates how to use the MyLibs framework.
|
||||
|
||||
- MetaTrader 5 with access to `terminal64.exe`
|
||||
- Python 3.8+
|
||||
- Optional: Freqtrade installed (`pip install freqtrade`)
|
||||
### Features of the EA
|
||||
|
||||
### Running Scripts
|
||||
- Multi-symbol trading (`AUDNZD`, `EURGBP`, etc.)
|
||||
- Custom indicators and buffer signal handling
|
||||
- Virtual TP runner trades
|
||||
- ATR trailing stops and breakeven logic
|
||||
- Risk split between take-profit and runner entries
|
||||
- Support for data splitting during backtesting
|
||||
- Modular entry and exit strategies
|
||||
- Time-based filtering and session control
|
||||
|
||||
```bash
|
||||
python pre_process.py
|
||||
python process_entry_indicators.py
|
||||
python post_process_test.py
|
||||
```
|
||||
### EA Strategy Logic Overview
|
||||
|
||||
### MT5 Library Usage
|
||||
Signal generation is broken into modular components:
|
||||
|
||||
Place the `.mqh` files from `My_MQL5_Libs/` into your `MQL5/Include` folder to use them in your Expert Advisors or custom indicators.
|
||||
- **Trigger** (e.g., RSI cross)
|
||||
- **Trendline** (direction, breakout, pullback)
|
||||
- **Confirmation** (e.g., CCI alignment)
|
||||
- **Volume** (OBV trend confirmation)
|
||||
- **ATR Band Positioning** (via `AtrBands`)
|
||||
- **Exit Logic** (Stochastic crossover, trendline break)
|
||||
|
||||
## Notes
|
||||
### EA Dependencies
|
||||
|
||||
- This project is a scaffold for connecting MQL5 strategies to Python-based optimisation and analysis tools.
|
||||
- Actual EA logic, data formats, and strategy specifics should be customised to your use case.
|
||||
The EA uses standard MQL5 built-in indicators, but can be easily modified to use custom indicators if desired:
|
||||
|
||||
## License
|
||||
- `iMA` – Moving Average (used for trendline estimation)
|
||||
- `iRSI` – Relative Strength Index (used as a trigger)
|
||||
- `iCCI` – Commodity Channel Index (used for confirmation)
|
||||
- `iOBV` – On-Balance Volume (used for volume filtering)
|
||||
- `iStochastic` – Stochastic Oscillator (used for exit signals)
|
||||
|
||||
**Note:** These indicators were selected for simplicity and demonstration purposes only. They are **not optimized** for trading performance and should be replaced or tuned during real development and testing. The architecture is fully modular and allows for plugging in better-suited or proprietary indicators.
|
||||
|
||||
All indicator handles are dynamically created and released via the `ResourceManager` class, ensuring clean memory handling during runtime and backtests.
|
||||
|
||||
---
|
||||
|
||||
## Installation & Usage
|
||||
|
||||
1. Clone or download this repository.
|
||||
2. Copy the `MyLibs/` directory into your MT5 `Include/` folder:
|
||||
3. Place the example EA file into the `MQL5/Experts/` directory.
|
||||
4. Open **MetaEditor** and compile the EA.
|
||||
5. Attach it to a chart or run backtests.
|
||||
|
||||
---
|
||||
|
||||
## Backtesting & Optimization
|
||||
|
||||
The EA supports advanced backtesting scenarios via:
|
||||
|
||||
- **CustomMax.mqh** — Optimizable custom fitness criteria (e.g., win rate, net profit)
|
||||
- **TestDataSplit.mqh** — Supports train/test splits for more robust model evaluation
|
||||
|
||||
Use `OnTester()` to drive optimization scoring logic.
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
## Maintenance Notes
|
||||
|
||||
- Each helper class is standalone and can be reused or extended.
|
||||
- Use `MultiSymbolSignalTracker` to handle per-symbol logic in multi-asset EAs.
|
||||
- Time-related functions are consolidated in `TimeZones.mqh` and `TradeWindow.mqh`.
|
||||
|
||||
---
|
||||
|
||||
This project is provided for educational and prototyping purposes. Please adapt and extend as needed for production environments.
|
||||
|
||||
@@ -1,223 +0,0 @@
|
||||
import pandas as pd
|
||||
from pathlib import Path
|
||||
import logging
|
||||
from xml.sax import ContentHandler, parse
|
||||
from typing import List, Tuple
|
||||
|
||||
from pandas import DataFrame
|
||||
|
||||
# Configure logging
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
|
||||
class PostProcessData:
|
||||
"""
|
||||
Class for post-processing and combining results from multiple indicator testing files.
|
||||
"""
|
||||
|
||||
def __init__(self, results_dir: Path, print_outputs: bool = True, output_file: str = '1_combined_results.csv'):
|
||||
"""
|
||||
Initializes the PostProcessData class with the provided directory and output file.
|
||||
|
||||
Args:
|
||||
results_dir (Path): The directory where the result files are located.
|
||||
print_outputs (bool): Whether to print the final output.
|
||||
output_file (str): The name of the combined output CSV file.
|
||||
"""
|
||||
self.results_dir = results_dir
|
||||
self.print_outputs = print_outputs
|
||||
self.output_file = output_file
|
||||
self.run()
|
||||
|
||||
def run(self):
|
||||
"""
|
||||
Processes result files, calculates statistics, and saves the combined results.
|
||||
"""
|
||||
df_combined = self.process_results_files()
|
||||
|
||||
if df_combined is not None:
|
||||
# Save the combined results as a CSV file
|
||||
output_path = self.results_dir / self.output_file
|
||||
df_combined.to_csv(output_path, index=False)
|
||||
self.combine_opt_results()
|
||||
|
||||
if self.print_outputs:
|
||||
print(df_combined)
|
||||
else:
|
||||
logging.info("Results processing complete.")
|
||||
else:
|
||||
logging.warning("No valid results to process.")
|
||||
|
||||
def process_results_files(self):
|
||||
"""
|
||||
Processes each indicator result file (ins.xml and out.xml) and computes the statistics.
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: Combined results DataFrame.
|
||||
"""
|
||||
df_combined = []
|
||||
failed_post_process_list = []
|
||||
|
||||
for file in self.results_dir.iterdir():
|
||||
if file.suffix == ".xml" and file.name.endswith("ins.xml"):
|
||||
file_prefix = file.stem[:-4] # Remove '_ins' suffix
|
||||
try:
|
||||
df_combined.append(self.process_indicator_file(file_prefix))
|
||||
except Exception as e:
|
||||
failed_post_process_list.append(file_prefix)
|
||||
logging.error(f"Failed to process {file_prefix}: {e}")
|
||||
|
||||
# Return combined DataFrame
|
||||
if df_combined:
|
||||
return pd.DataFrame(df_combined)
|
||||
else:
|
||||
return
|
||||
|
||||
def process_indicator_file(self, file_prefix: str) -> dict:
|
||||
"""
|
||||
Processes a pair of `ins.xml` and `out.xml` files for an indicator, calculates statistics,
|
||||
and returns a dictionary of the results.
|
||||
|
||||
Args:
|
||||
file_prefix (str): The base name of the indicator files (without extensions).
|
||||
|
||||
Returns:
|
||||
dict: Dictionary of indicator statistics.
|
||||
"""
|
||||
# Load data from XML files
|
||||
result_in, p_fac_in, trades_in = self.load_xml_data(file_prefix, "ins")
|
||||
result_out, p_fac_out, trades_out = self.load_xml_data(file_prefix, "out")
|
||||
|
||||
# Calculate result statistics
|
||||
result_mean = (result_in + result_out) / 2
|
||||
pc_result = self.calc_percent_diff(result_in, result_out)
|
||||
pc_p_fac = self.calc_percent_diff(p_fac_in, p_fac_out)
|
||||
pc_trades = self.calc_percent_diff(trades_in, trades_out)
|
||||
|
||||
# Return a dictionary with computed data
|
||||
return {
|
||||
'Indicator': file_prefix,
|
||||
'R_ins': result_in,
|
||||
'R_outs': result_out,
|
||||
'R_dif': pc_result,
|
||||
'R_mean': result_mean,
|
||||
'P_fac_in': p_fac_in,
|
||||
'P_fac_out': p_fac_out,
|
||||
'P_fac_dif': pc_p_fac,
|
||||
'trades_in': trades_in,
|
||||
'trades_out': trades_out,
|
||||
'trades_dif': pc_trades
|
||||
}
|
||||
|
||||
def load_xml_data(self, file_prefix: str, file_type: str):
|
||||
"""
|
||||
Loads data from an XML file (either 'ins' or 'out'), and extracts the result, profit factor, and trades.
|
||||
|
||||
Args:
|
||||
file_prefix (str): The prefix of the file (without extension).
|
||||
file_type (str): The type of the file ('ins' or 'out').
|
||||
|
||||
Returns:
|
||||
tuple: Contains result, profit factor, and trades values as floats.
|
||||
"""
|
||||
try:
|
||||
df = self.load_data_from_xml(f"{file_prefix}_{file_type}.xml")
|
||||
return float(df["Result"][0]), float(df["Profit Factor"][0]), float(df["Trades"][0])
|
||||
except Exception as e:
|
||||
logging.error(f"Error loading {file_prefix}_{file_type}.xml: {e}")
|
||||
return
|
||||
|
||||
@staticmethod
|
||||
def load_data_from_xml(file: str) -> pd.DataFrame:
|
||||
"""
|
||||
Loads data from an XML file and converts it into a Pandas DataFrame.
|
||||
|
||||
Args:
|
||||
file (str): The name of the XML file to load.
|
||||
|
||||
Returns:
|
||||
pd.DataFrame: Data extracted from the XML file.
|
||||
"""
|
||||
excel_handler = ExcelHandler()
|
||||
parse(file, excel_handler)
|
||||
df = pd.DataFrame(excel_handler.tables[0][1:], columns=excel_handler.tables[0][0])
|
||||
return df
|
||||
|
||||
@staticmethod
|
||||
def calc_percent_diff(in_sample: float, out_sample: float) -> float:
|
||||
"""
|
||||
Calculates the percentage difference between two values.
|
||||
|
||||
Args:
|
||||
in_sample (float): The "in-sample" value.
|
||||
out_sample (float): The "out-sample" value.
|
||||
|
||||
Returns:
|
||||
float: The percentage difference between the two values.
|
||||
"""
|
||||
try:
|
||||
return round(abs(in_sample - out_sample) / out_sample * 100.0, 2)
|
||||
except ZeroDivisionError:
|
||||
return 0.0
|
||||
|
||||
def combine_opt_results(self):
|
||||
"""
|
||||
Combines all 'opt_results.txt' files in the results directory into one combined file.
|
||||
"""
|
||||
combined_file_path = self.results_dir / "2_combined_opt_results.txt"
|
||||
if combined_file_path.exists():
|
||||
combined_file_path.unlink()
|
||||
|
||||
opt_results_list = [file for file in self.results_dir.iterdir() if
|
||||
file.suffix == ".txt" and "opt_results" in file.name]
|
||||
new_lines = []
|
||||
for file_path in opt_results_list:
|
||||
new_lines.append(file_path.read_text())
|
||||
new_lines.append("\n\n")
|
||||
|
||||
combined_file_path.write_text("".join(new_lines))
|
||||
|
||||
|
||||
class ExcelHandler(ContentHandler):
|
||||
"""
|
||||
Custom handler to parse XML files and extract table data.
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
self.rows = None
|
||||
self.cells = None
|
||||
self.chars = []
|
||||
self.tables = []
|
||||
|
||||
def characters(self, content: str):
|
||||
"""
|
||||
Collects characters in XML elements.
|
||||
"""
|
||||
self.chars.append(content)
|
||||
|
||||
def start_element(self, name: str, attrs):
|
||||
"""
|
||||
Handle the start of XML elements.
|
||||
"""
|
||||
if name == "Table":
|
||||
self.rows = []
|
||||
elif name == "Row":
|
||||
self.cells = []
|
||||
elif name == "Data":
|
||||
self.chars = []
|
||||
|
||||
def end_element(self, name: str):
|
||||
"""
|
||||
Handle the end of XML elements.
|
||||
"""
|
||||
if name == "Table":
|
||||
self.tables.append(self.rows)
|
||||
elif name == "Row":
|
||||
self.rows.append(self.cells)
|
||||
elif name == "Data":
|
||||
self.cells.append("".join(self.chars))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Example usage with a results directory path
|
||||
post_processor = PostProcessData(Path(r'path_to_results'))
|
||||
@@ -1,81 +0,0 @@
|
||||
from pathlib import Path
|
||||
import os
|
||||
|
||||
DEFAULTS_DIR = Path(
|
||||
r'C:\Users\mkcor\AppData\Roaming\MetaQuotes\Terminal\49CDDEAA95A409ED22BD2287BB67CB9C\MQL5\Experts\My_Experts\NNFX\Entry_testing\default\indicators')
|
||||
OPTIMISE_DIR = Path(
|
||||
r'C:\Users\mkcor\AppData\Roaming\MetaQuotes\Terminal\49CDDEAA95A409ED22BD2287BB67CB9C\MQL5\Experts\My_Experts\NNFX\Entry_testing\optimise\indicators')
|
||||
MASTER_CONFIG_DIR = Path(
|
||||
r'C:\Users\mkcor\AppData\Roaming\MetaQuotes\Terminal\49CDDEAA95A409ED22BD2287BB67CB9C\MQL5\Experts\My_Experts\NNFX\Entry_testing\optimise\master_config_files')
|
||||
|
||||
|
||||
def pre_process_checks():
|
||||
"""
|
||||
Pre-process check that verifies the presence of valid indicator files and generates necessary configuration templates.
|
||||
|
||||
This function:
|
||||
- Checks if the indicator files in the default and optimizable directories have the correct suffixes.
|
||||
- Creates master configuration template files if required in the master config directory.
|
||||
"""
|
||||
print("---------- Checking: default/indicators ---------------")
|
||||
check_indicators_suffix(DEFAULTS_DIR)
|
||||
|
||||
print("---------- Checking: optimise/indicators --------------")
|
||||
check_indicators_suffix(OPTIMISE_DIR)
|
||||
|
||||
print("---------- Checking: optimise/master_config_files -----")
|
||||
create_cp_templates(OPTIMISE_DIR, MASTER_CONFIG_DIR)
|
||||
|
||||
|
||||
def check_indicators_suffix(dir):
|
||||
"""
|
||||
Verifies if the indicator files in the specified directory have valid suffixes.
|
||||
|
||||
Args:
|
||||
dir (Path): The directory containing the indicator files.
|
||||
|
||||
This function checks that all .mq5 and .ex5 files have one of the following suffixes:
|
||||
- "clc", "cbc", "clx", "hcc", "hlx", "lcc", "0lx", "2lx"
|
||||
|
||||
If a file does not have one of these suffixes, a warning is printed.
|
||||
"""
|
||||
suffex_list = ["clc", "cbc", "clx", "hcc", "hlx", "lcc", "0lx", "2lx"]
|
||||
for file in os.listdir(dir):
|
||||
filename, file_extension = os.path.splitext(file)
|
||||
|
||||
if file_extension in [".mq5", ".ex5"]:
|
||||
file_suffix = filename[-3:]
|
||||
if file_suffix not in suffex_list:
|
||||
print(f"{file} - INCORRECT FILE SUFFIX")
|
||||
print("Complete.")
|
||||
|
||||
|
||||
def create_cp_templates(opt_dir, master_config_dir):
|
||||
"""
|
||||
Creates master configuration template files in the specified master config directory.
|
||||
|
||||
Args:
|
||||
opt_dir (Path): The directory containing the optimizable indicator files.
|
||||
master_config_dir (Path): The directory where the master configuration files should be created.
|
||||
|
||||
This function generates a new template file for each .ex5 file in the optimizable directory,
|
||||
creating a template file with the same name in the master config directory. If a corresponding
|
||||
.ini file already exists, it skips the creation.
|
||||
"""
|
||||
for file in os.listdir(opt_dir):
|
||||
filename, file_extension = os.path.splitext(file)
|
||||
if file_extension == ".ex5":
|
||||
template_file = master_config_dir / filename
|
||||
ini_file = master_config_dir / f"{filename}.ini"
|
||||
|
||||
if ini_file.exists() and template_file.exists():
|
||||
template_file.unlink() # Remove existing template file if it exists
|
||||
else:
|
||||
with open(template_file, 'w') as f:
|
||||
f.write("Template content here") # Add content to the template
|
||||
print(f"Master config for required - {filename}")
|
||||
print("Complete.")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pre_process_checks()
|
||||
@@ -1,436 +0,0 @@
|
||||
"""
|
||||
MattC - 2025
|
||||
This code is a working prototype and is intended for initial testing and development purposes. Some Python
|
||||
standards, including but not limited to PEP 8 compliance, error handling, and code optimization, are yet to be fully
|
||||
implemented. Further refactoring and enhancements are planned to improve readability, maintainability,
|
||||
and efficiency.
|
||||
"""
|
||||
|
||||
from pathlib import Path
|
||||
import configparser
|
||||
import os
|
||||
import pandas as pd
|
||||
from subprocess import call
|
||||
from post_process_test import PostProcessData as ppd
|
||||
|
||||
MT5_TERM_EXE = Path(r'C:\Program Files\FTMO MetaTrader 5\terminal64.exe')
|
||||
MT5_DIRECTORY = Path(r"C:\Users\mkcor\AppData\Roaming\MetaQuotes\Terminal\49CDDEAA95A409ED22BD2287BB67CB9C")
|
||||
|
||||
|
||||
##
|
||||
|
||||
class TestParent:
|
||||
|
||||
def __init__(self, name, start_date, end_date, chart_period, custom_loss_function, symbol_mode, data_split):
|
||||
"""
|
||||
@param name: test name e.g. "name"
|
||||
@param start_date: backtest start e.g. "2010.10.01"
|
||||
@param end_date: backtest end e.g. "2010.10.01"
|
||||
@param chart_period: "Daily", "H4", "H1", "M15", etc
|
||||
@param custom_loss_function: "0"= W/L ratio, "1"= W percent, "2"=W percent (min 200 trades) ....
|
||||
@param symbol_mode: "0"=Chart sym only, "1"= Multi sym FX5, "2"=Multi sym 28FX pairs
|
||||
@param data_split: "year" or "month"
|
||||
"""
|
||||
self.name = name
|
||||
self.start_date = start_date
|
||||
self.end_date = end_date
|
||||
self.chart_period = chart_period
|
||||
self.custom_loss_function = custom_loss_function
|
||||
self.symbol_mode = symbol_mode
|
||||
self.data_split = data_split
|
||||
|
||||
self.mt5_term = MT5_TERM_EXE
|
||||
self.mt5_dir = MT5_DIRECTORY
|
||||
self.mq5_test_cash = Path.joinpath(self.mt5_dir, r"Tester\cache")
|
||||
self.test_folder = Path.joinpath(self.mt5_dir, r'MQL5\Experts\My_Experts\NNFX\Entry_testing')
|
||||
self.indi_dir = None
|
||||
self.indi_rel_path = None
|
||||
self.output_dir = None
|
||||
self.results_dir = None
|
||||
self.master_config_loc = None
|
||||
self.pct_risk = 2
|
||||
self.tp_atr = 1
|
||||
self.sl_atr = 1.5
|
||||
|
||||
def create_test_name(self):
|
||||
start = self.start_date.replace('.', '')
|
||||
end = self.end_date.replace('.', '')
|
||||
test_name = f'{self.name}_{start}_{end}_cl{self.custom_loss_function}_sm{self.symbol_mode}_ds-{self.data_split}_cp-{self.chart_period}'
|
||||
return test_name
|
||||
|
||||
def check_results_df(self, results_dir):
|
||||
|
||||
file_list = []
|
||||
for file in os.listdir(results_dir):
|
||||
file_list.append(file)
|
||||
|
||||
combined_resuts_file = '1_combined_results.csv'
|
||||
df_path = Path.joinpath(results_dir, combined_resuts_file)
|
||||
|
||||
if combined_resuts_file not in file_list:
|
||||
df = pd.DataFrame(
|
||||
columns=['Indicator', 'Type', 'R_ins', 'R_outs', 'R_dif', 'R_mean', 'P_fac_in', 'P_fac_out',
|
||||
'P_fac_dif', 'trades_in', 'trades_out', 'trades_dif'])
|
||||
df.to_csv(df_path, index=False)
|
||||
|
||||
return df_path
|
||||
|
||||
def create_indi_optimisation_ini(self, config_paser, indicator, config_files_dir, sample_data, force_optimisation,
|
||||
opt_os=False):
|
||||
|
||||
config = config_paser
|
||||
config['Tester']['Expert'] = str(Path.joinpath(self.indi_rel_path, indicator)) + ".ex5"
|
||||
config['Tester']['Symbol'] = "EURUSD"
|
||||
config['Tester']['Period'] = f"{self.chart_period}"
|
||||
config['Tester']['Optimization'] = "2"
|
||||
config['Tester']['Model'] = "1"
|
||||
config['Tester']['FromDate'] = f'{self.start_date}'
|
||||
config['Tester']['ToDate'] = f'{self.end_date}'
|
||||
config['Tester']['ForwardMode'] = "0"
|
||||
config['Tester']['Deposit'] = "100000"
|
||||
config['Tester']['Currency'] = "USD"
|
||||
config['Tester']['ProfitInPips'] = "0"
|
||||
config['Tester']['Leverage'] = "100"
|
||||
config['Tester']['ExecutionMode'] = "0"
|
||||
config['Tester']['OptimizationCriterion'] = "6"
|
||||
config['Tester']['Visual'] = "0"
|
||||
config['Tester']['ReplaceReport'] = "1"
|
||||
config['Tester']['ShutdownTerminal'] = "1"
|
||||
config['TesterInputs']['inp_lot_mode'] = "2||0||0||2||N"
|
||||
config['TesterInputs']['inp_lot_var'] = f"{self.pct_risk}||2.0||0.2||20||N"
|
||||
config['TesterInputs']['inp_sl_mode'] = "2||0||0||5||N"
|
||||
config['TesterInputs']['inp_sl_var'] = f"{self.sl_atr}||1.0||0.1||10||N"
|
||||
config['TesterInputs']['inp_tp_mode'] = "2||0||0||5||N"
|
||||
config['TesterInputs']['inp_tp_var'] = f"{self.tp_atr}||1.5||0.15||15||N"
|
||||
config['TesterInputs']['inp_custom_criteria'] = f"{self.custom_loss_function}||0||0||1||N"
|
||||
config['TesterInputs']['inp_sym_mode'] = f"{self.symbol_mode}||0||0||2||N"
|
||||
config['TesterInputs']['inp_force_opt'] = f"1||1||1||2||{force_optimisation}"
|
||||
|
||||
if sample_data == "in":
|
||||
|
||||
config['Tester']['report'] = f"{indicator}_ins"
|
||||
|
||||
if self.data_split == "year":
|
||||
config['TesterInputs']['inp_data_split_method'] = f"1||0||0||3||N"
|
||||
|
||||
if self.data_split == "month":
|
||||
config['TesterInputs']['inp_data_split_method'] = f"3||0||0||3||N"
|
||||
|
||||
if sample_data == "out":
|
||||
|
||||
config['Tester']['report'] = f"{indicator}_out"
|
||||
|
||||
if self.data_split == "year":
|
||||
config['TesterInputs']['inp_data_split_method'] = f"2||0||0||3||N"
|
||||
|
||||
if self.data_split == "month":
|
||||
config['TesterInputs']['inp_data_split_method'] = f"4||0||0||3||N"
|
||||
|
||||
if opt_os:
|
||||
|
||||
test_inp_list = list(config.items('TesterInputs'))
|
||||
test_inp_key = []
|
||||
for x in test_inp_list:
|
||||
test_inp_key.append(x[0])
|
||||
|
||||
removal_list = ['inp_lot_mode', 'inp_lot_var', 'inp_sl_mode', 'inp_sl_var', 'inp_tp_mode', 'inp_tp_var',
|
||||
'inp_custom_criteria', 'inp_sym_mode', 'inp_force_opt', 'inp_data_split_method']
|
||||
keys_to_mod = [x for x in test_inp_key if (x not in removal_list)]
|
||||
|
||||
opt_results = self.get_opt_results_from_xml(indicator)
|
||||
|
||||
lines = []
|
||||
lines2 = []
|
||||
|
||||
for key in keys_to_mod:
|
||||
|
||||
for j in test_inp_list:
|
||||
if key == j[0]:
|
||||
|
||||
string_value = j[1]
|
||||
string_value = string_value.split("||", 1)[
|
||||
1] # removes the first int from e.g f"2||0||0||3||N"
|
||||
|
||||
for k in opt_results:
|
||||
|
||||
if key == k[0]:
|
||||
result_val = k[1]
|
||||
string_value = f'{result_val}||{string_value}'
|
||||
string_value = string_value[:-1]
|
||||
string_value = f'{string_value}N'
|
||||
|
||||
config['TesterInputs'][key] = string_value
|
||||
|
||||
value = config['TesterInputs'][key].split("||", 1)[0]
|
||||
lines.append(f"{key}={value}, ")
|
||||
lines2.append(f"{value}, ")
|
||||
|
||||
self.save_in_sample_opt_results_to_file(indicator, lines, lines2)
|
||||
|
||||
new_file = indicator + ".ini"
|
||||
new_file_path = Path.joinpath(config_files_dir, new_file)
|
||||
|
||||
with open(new_file_path, 'w', encoding='utf-16') as configfile:
|
||||
config.write(configfile)
|
||||
|
||||
def save_in_sample_opt_results_to_file(self, indicator, lines, lines2):
|
||||
|
||||
file_path = Path.joinpath(self.results_dir, f"{indicator}_opt_results.txt")
|
||||
|
||||
if file_path.is_file():
|
||||
file_path.unlink() # delete old files.
|
||||
|
||||
lines_mod = ''
|
||||
for str in lines2:
|
||||
lines_mod = lines_mod + str
|
||||
|
||||
print(lines_mod[:-2])
|
||||
|
||||
f = open(file_path, "a")
|
||||
f.writelines(indicator + "\n")
|
||||
f.writelines(lines)
|
||||
f.writelines("\n, ")
|
||||
f.write(lines_mod[:-2])
|
||||
f.close()
|
||||
|
||||
def get_opt_results_from_xml(self, indicator):
|
||||
|
||||
ins_results = f"{indicator}_ins.xml"
|
||||
df = ppd.load_data_from_xml(self.results_dir)
|
||||
df = df.drop(['Pass', 'Result', 'Profit', 'Profit Factor', 'Custom', 'Expected Payoff', 'Recovery Factor',
|
||||
'Sharpe Ratio', 'Equity DD %', 'Trades'], axis=1)
|
||||
column_names = list(df.columns.values)
|
||||
|
||||
opt_result = []
|
||||
for count, value in enumerate(column_names):
|
||||
param_results = df[column_names[count]][0]
|
||||
tup = (value.lower(), param_results) # Convert string to lower case.
|
||||
opt_result.append(tup)
|
||||
|
||||
return opt_result
|
||||
|
||||
def create_indicator_list(self, df_path, indicator_dir, optimisation=False):
|
||||
|
||||
# Create list
|
||||
indi_list = []
|
||||
for file in os.listdir(os.fsencode(indicator_dir)):
|
||||
filename = os.fsdecode(file)
|
||||
if filename.endswith(".ex5"):
|
||||
indi_name = os.path.splitext(filename)[0]
|
||||
indi_list.append(str(indi_name))
|
||||
|
||||
# remove previously processed indicators:
|
||||
ti_list = []
|
||||
for i in indi_list:
|
||||
|
||||
df = pd.read_csv(df_path)
|
||||
if i in df["Indicator"].tolist():
|
||||
# ti_list.append(i)
|
||||
|
||||
print(f"Indicator - {i} - already processed")
|
||||
print("-" * 60)
|
||||
|
||||
indi_list2 = [x for x in indi_list if x not in ti_list]
|
||||
conf_ini_list = []
|
||||
if optimisation:
|
||||
|
||||
for file in os.listdir(os.fsencode(self.master_config_loc)):
|
||||
filename = os.fsdecode(file)
|
||||
if filename.endswith(".ini"):
|
||||
indi_name = os.path.splitext(filename)[0]
|
||||
conf_ini_list.append(str(indi_name))
|
||||
|
||||
removed_list = [x for x in indi_list2 if x not in conf_ini_list]
|
||||
for i in removed_list:
|
||||
print(f"Indicator - {i} - NO MASTER CONFIG!")
|
||||
print("-" * 60)
|
||||
|
||||
indi_list3 = [x for x in indi_list2 if x in conf_ini_list]
|
||||
|
||||
return_list = []
|
||||
if optimisation:
|
||||
return_list = indi_list3
|
||||
|
||||
else:
|
||||
return_list = indi_list2
|
||||
|
||||
for i in return_list:
|
||||
print(f"Indicator - {i} - To be tested.")
|
||||
|
||||
return return_list
|
||||
|
||||
def create_dir(self, dir_name):
|
||||
dir_string = Path.joinpath(self.output_dir, dir_name)
|
||||
if not os.path.exists(dir_string):
|
||||
os.makedirs(dir_string)
|
||||
|
||||
try:
|
||||
os.makedirs(dir_string)
|
||||
|
||||
except Exception as e:
|
||||
pass
|
||||
|
||||
return dir_string
|
||||
|
||||
@staticmethod
|
||||
def delete_files_in_directory(directory_path):
|
||||
try:
|
||||
files = os.listdir(directory_path)
|
||||
for file in files:
|
||||
file_path = os.path.join(directory_path, file)
|
||||
if os.path.isfile(file_path):
|
||||
os.remove(file_path)
|
||||
|
||||
except OSError:
|
||||
print("Error occurred while deleting files.")
|
||||
|
||||
|
||||
class TestIndicators(TestParent):
|
||||
|
||||
def __init__(self, name, start_date, end_date, chart_period, custom_loss_function, symbol_mode, data_split):
|
||||
super().__init__(name, start_date, end_date, chart_period, custom_loss_function, symbol_mode, data_split)
|
||||
|
||||
self.template_file = Path.joinpath(self.test_folder, "template.ini")
|
||||
self.indi_dir = Path.joinpath(self.test_folder, "default\indicators")
|
||||
self.indi_rel_path = Path(str(self.indi_dir).split(r'\MQL5\Experts')[1][1:])
|
||||
self.output_dir = Path.joinpath(Path(self.indi_dir).parents[0], f'Testing\{self.name}')
|
||||
self.results_dir = self.create_dir("results")
|
||||
self.run()
|
||||
|
||||
def run(self):
|
||||
print("~" * 80)
|
||||
print(f" DEFAULT INDICATOR TEST - {self.name}\n")
|
||||
|
||||
# Create in-sample dir/ delete its content:
|
||||
input_files_is_dir = self.create_dir("config_files_in_sample")
|
||||
self.delete_files_in_directory(input_files_is_dir)
|
||||
|
||||
# Create out sample dir/ delete its content:
|
||||
input_files_os_dir = self.create_dir("config_files_out_sample")
|
||||
self.delete_files_in_directory(input_files_os_dir)
|
||||
|
||||
df_path = self.check_results_df(self.results_dir)
|
||||
indicator_list = self.create_indicator_list(df_path, self.indi_dir)
|
||||
|
||||
print("-" * 25 + " STARTING TEST " + "-" * 25)
|
||||
for indicator in indicator_list:
|
||||
config_paser = self.load_config_paser()
|
||||
|
||||
# Create run input files:
|
||||
self.create_indi_optimisation_ini(config_paser, indicator, input_files_is_dir, "in", "Y")
|
||||
self.create_indi_optimisation_ini(config_paser, indicator, input_files_os_dir, "out", "Y")
|
||||
|
||||
# Delete MQL5 Tester Cash:
|
||||
line = f'del /F /Q {self.mq5_test_cash}'
|
||||
call(line, shell=True)
|
||||
|
||||
# Run the in-sample test:
|
||||
print(f"Running defalts in-sample test for {indicator}")
|
||||
line = f'"{self.mt5_term}" /config: {input_files_is_dir}\{indicator}.ini'
|
||||
call(line, shell=True)
|
||||
|
||||
# Copy output to run results dir:
|
||||
line = f'copy {self.mt5_dir}\{indicator}_ins.xml {self.results_dir}\{indicator}_ins.xml'
|
||||
call(line, shell=True)
|
||||
|
||||
# Run the out of sample test:
|
||||
print(f"Running defalts out-of-sample test for {indicator}")
|
||||
line = f'"{self.mt5_term}" /config: {input_files_os_dir}\{indicator}.ini'
|
||||
call(line, shell=True)
|
||||
|
||||
# Copy output to run results dir:
|
||||
line = f'copy {self.mt5_dir}\{indicator}_out.xml {self.results_dir}\{indicator}_out.xml'
|
||||
call(line, shell=True)
|
||||
|
||||
def load_config_paser(self):
|
||||
config_paser = configparser.ConfigParser()
|
||||
config_paser.read(self.template_file, encoding='utf-16')
|
||||
return config_paser
|
||||
|
||||
|
||||
class OptimiseIndicators(TestParent):
|
||||
|
||||
def __init__(self, name, start_date, end_date, chart_period, custom_loss_function, symbol_mode, data_split):
|
||||
super().__init__(name, start_date, end_date, chart_period, custom_loss_function, symbol_mode, data_split)
|
||||
|
||||
self.indi_dir = Path.joinpath(self.test_folder, "optimise\indicators")
|
||||
self.master_config_loc = Path.joinpath(self.test_folder, "optimise\master_config_files")
|
||||
self.indi_rel_path = Path(str(self.indi_dir).split(r'\MQL5\Experts')[1][1:])
|
||||
self.output_dir = Path.joinpath(Path(self.indi_dir).parents[0], f'Testing\{self.name}')
|
||||
self.results_dir = self.create_dir("results")
|
||||
self.run()
|
||||
|
||||
def run(self):
|
||||
print("~" * 80)
|
||||
print(f"\n INDICATOR OPTIMISATION - {self.name}\n")
|
||||
|
||||
# Create in-sample dir/ delete its content:
|
||||
input_files_is_dir = self.create_dir("config_files_in_sample")
|
||||
self.delete_files_in_directory(input_files_is_dir)
|
||||
|
||||
# Create out sample dir/ delete its content:
|
||||
input_files_os_dir = self.create_dir("config_files_out_sample")
|
||||
self.delete_files_in_directory(input_files_os_dir)
|
||||
|
||||
df_path = self.check_results_df(self.results_dir)
|
||||
indicator_list = self.create_indicator_list(df_path, self.indi_dir, True)
|
||||
|
||||
print("-" * 25 + " STARTING TEST " + "-" * 25)
|
||||
for indicator in indicator_list:
|
||||
# Delete MQL5 Tester Cash:
|
||||
line = f'del /F /Q {self.mq5_test_cash}'
|
||||
call(line, shell=True)
|
||||
|
||||
# Load the MQL5 .ini for the current indicator:
|
||||
config_paser = self.load_config_paser(indicator)
|
||||
|
||||
# Create in-sample input file:
|
||||
self.create_indi_optimisation_ini(config_paser, indicator, input_files_is_dir, "in", "N")
|
||||
|
||||
# Run the in-sample test:
|
||||
print(f"Running in-sample optimisation for {indicator}")
|
||||
line = f'"{self.mt5_term}" /config: {input_files_is_dir}\{indicator}.ini'
|
||||
call(line, shell=True)
|
||||
|
||||
# Copy output to run results dir:
|
||||
line = f'copy {self.mt5_dir}\{indicator}_ins.xml {self.results_dir}\{indicator}_ins.xml'
|
||||
call(line, shell=True)
|
||||
|
||||
# Create OOS test input file for optimisation results:
|
||||
self.create_indi_optimisation_ini(config_paser, indicator, input_files_os_dir, "out", "Y", opt_os=True)
|
||||
|
||||
# Run the out of sample test:
|
||||
print(f"Running out-of-sample optimisation for {indicator}")
|
||||
line = f'"{self.mt5_term}" /config: {input_files_os_dir}\{indicator}.ini'
|
||||
call(line, shell=True)
|
||||
|
||||
# Copy output to run results dir:
|
||||
line = f'copy {self.mt5_dir}\{indicator}_out.xml {self.results_dir}\{indicator}_out.xml'
|
||||
call(line, shell=True)
|
||||
|
||||
def load_config_paser(self, indicator):
|
||||
config_paser = configparser.ConfigParser()
|
||||
inp_file = f'{self.master_config_loc}\{indicator}.ini'
|
||||
config_paser.read(inp_file, encoding='utf-16')
|
||||
return config_paser
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Testing on 12 years data. in/out sample data split: year
|
||||
TestIndicators(name="Apollo-dftest",
|
||||
start_date="2012.01.01",
|
||||
end_date="2022.01.01",
|
||||
chart_period="Daily",
|
||||
custom_loss_function="1", # 1 = no trade limit
|
||||
symbol_mode="1",
|
||||
data_split="month"
|
||||
)
|
||||
|
||||
OptimiseIndicators(name="Apollo-opt",
|
||||
start_date="2012.01.01",
|
||||
end_date="2022.01.01",
|
||||
chart_period="Daily",
|
||||
custom_loss_function="4", # 400 trades min
|
||||
symbol_mode="1",
|
||||
data_split="month"
|
||||
)
|
||||
@@ -0,0 +1,361 @@
|
||||
//+------------------------------------------------------------------+
|
||||
//| Multi-Symbol Strategy EA using MyLibs Framework |
|
||||
//+------------------------------------------------------------------+
|
||||
// Description:
|
||||
// - This Expert Advisor (EA) implements a modular, multi-symbol trading
|
||||
// strategy using the MyLibs library for MetaTrader 5.
|
||||
// - The strategy combines RSI triggers, trendline analysis, ATR bands,
|
||||
// confirmation and volume indicators, and manages both TP and runner
|
||||
// trades with risk control.
|
||||
//
|
||||
// Features:
|
||||
// - Modular logic for entry/exit using signal structs
|
||||
// - Trailing stop and breakeven logic for runners
|
||||
// - Custom fitness criteria for optimization (via OnTester())
|
||||
// - Support for training/testing data splits
|
||||
// - Time filtering for bar-based updates
|
||||
//
|
||||
// Usage:
|
||||
// - Place the 'MyLibs' folder in MQL5/Include/
|
||||
// - Compile this EA and attach it to any chart
|
||||
// - Strategy supports multiple symbols simply modify the SymbolsArray
|
||||
// - Adjust inputs for lot size, SL/TP modes, ATR periods, etc. as needed
|
||||
//
|
||||
// Notes:
|
||||
// - All indicator handles are registered and released via ResourceManager
|
||||
// - Trade logic only executes once per bar per symbol
|
||||
// - Signals are tracked and time-decayed using MultiSymbolSignalTracker
|
||||
//+------------------------------------------------------------------+
|
||||
#include <MyLibs/BacktestUtils/CustomMax.mqh>
|
||||
#include <MyLibs/BacktestUtils/TestDataSplit.mqh>
|
||||
#include <MyLibs/Indicators/AtrBands.mqh>
|
||||
#include <MyLibs/Indicators/TrendlineAnalyser.mqh>
|
||||
#include <MyLibs/Orders/AdjustPosition.mqh>
|
||||
#include <MyLibs/Orders/EntryOrders.mqh>
|
||||
#include <MyLibs/Orders/ExitOrders.mqh>
|
||||
#include <MyLibs/Utils/Enums.mqh>
|
||||
#include <MyLibs/Utils/MarketDataUtils.mqh>
|
||||
#include <MyLibs/Utils/MultiSymbolSignalTracker.mqh>
|
||||
#include <MyLibs/Utils/ResourceManager.mqh>
|
||||
//--- classes
|
||||
AdjustPosition adjust_pos;
|
||||
AtrBands atr_bands;
|
||||
CustomMax c_max;
|
||||
EntryOrders entry_orders;
|
||||
ExitOrders exit_orders;
|
||||
MarketDataUtils m_utils;
|
||||
MultiSymbolSignalTracker track_trigger_signal;
|
||||
ResourceManager resource_manager;
|
||||
TestDataSplit data_split;
|
||||
TrendlineAnalyser tl_tools;
|
||||
//--- Inputs
|
||||
input LOT_MODE inp_lot_mode = LOT_MODE_PCT_RISK;
|
||||
string lot_mode = EnumToString(inp_lot_mode);
|
||||
input double inp_lot_var = 2;
|
||||
input SL_MODE inp_sl_mode = SL_ATR_MULTIPLE;
|
||||
string sl_mode = EnumToString(inp_sl_mode);
|
||||
input double inp_sl_var = 1.5;
|
||||
input TP_MODE inp_tp_mode = TP_ATR_MULTIPLE;
|
||||
string tp_mode = EnumToString(inp_tp_mode);
|
||||
input double inp_tp_var = 1;
|
||||
input CUSTOM_MAX_TYPE inp_custom_criteria = CM_WIN_PERCENT;
|
||||
input int inp_opt_min_trades = 0;
|
||||
input MODE_SPLIT_DATA inp_data_split_method = NO_SPLIT;
|
||||
input int inp_force_opt = 1;
|
||||
input group "-----------------------------------------";
|
||||
input int InpTrendlinePeriod = 100;
|
||||
input int InpTriggerPeriod = 14;
|
||||
input int InpConformationPeriod = 50;
|
||||
input int InpExitPeriod = 5;
|
||||
//--- Global vars/arrays
|
||||
int magic_number = 1000;
|
||||
int runner_magic = magic_number + 1;
|
||||
// string SymbolsArray[] = {_Symbol}; // Chart symbol only
|
||||
string SymbolsArray[] = {"AUDNZD", "EURGBP", "AUDCAD", "CHFJPY", "EURUSD"}; // Multi symbol trading
|
||||
int tl_handle[], trig_handle[], conf_handle[], vol_handle[], exit_handle[];
|
||||
|
||||
int OnInit() {
|
||||
ArrayResize(tl_handle, ArraySize(SymbolsArray));
|
||||
ArrayResize(trig_handle, ArraySize(SymbolsArray));
|
||||
ArrayResize(conf_handle, ArraySize(SymbolsArray));
|
||||
ArrayResize(vol_handle, ArraySize(SymbolsArray));
|
||||
ArrayResize(exit_handle, ArraySize(SymbolsArray));
|
||||
|
||||
for (int i = 0; i < ArraySize(SymbolsArray); i++) {
|
||||
// --- Create and register all indicator handles for this symbol
|
||||
tl_handle[i] = iMA(SymbolsArray[i], PERIOD_CURRENT, InpTrendlinePeriod, 0, MODE_SMA, PRICE_CLOSE);
|
||||
trig_handle[i] = iRSI(SymbolsArray[i], PERIOD_CURRENT, InpTriggerPeriod, PRICE_CLOSE);
|
||||
conf_handle[i] = iCCI(SymbolsArray[i], PERIOD_CURRENT, InpConformationPeriod, PRICE_TYPICAL);
|
||||
vol_handle[i] = iOBV(SymbolsArray[i], PERIOD_CURRENT, VOLUME_TICK);
|
||||
exit_handle[i] = iStochastic(SymbolsArray[i], PERIOD_CURRENT, InpExitPeriod, 3, 3, MODE_EMA, STO_CLOSECLOSE);
|
||||
resource_manager.register_handle(tl_handle[i]);
|
||||
resource_manager.register_handle(trig_handle[i]);
|
||||
resource_manager.register_handle(conf_handle[i]);
|
||||
resource_manager.register_handle(vol_handle[i]);
|
||||
resource_manager.register_handle(exit_handle[i]);
|
||||
}
|
||||
|
||||
EventSetTimer(60);
|
||||
return INIT_SUCCEEDED;
|
||||
}
|
||||
|
||||
void OnDeinit(const int reason) {
|
||||
EventKillTimer();
|
||||
resource_manager.release_all_handles();
|
||||
track_trigger_signal.clear();
|
||||
}
|
||||
|
||||
double OnTester() {
|
||||
return c_max.calculate_custom_criteria(inp_custom_criteria, inp_opt_min_trades);
|
||||
}
|
||||
|
||||
void OnTimer() {
|
||||
for (int i = 0; i < ArraySize(SymbolsArray); i++) {
|
||||
string symbol = SymbolsArray[i];
|
||||
run_trade_logic(symbol, tl_handle[i], trig_handle[i], conf_handle[i], vol_handle[i], exit_handle[i]);
|
||||
}
|
||||
}
|
||||
|
||||
void run_trade_logic(string symbol, int tl_hand, int trig_hand, int conf_hand, int vol_hand, int exit_hand) {
|
||||
|
||||
// --- Breakeven logic: if a profit target was reached, move SL to breakeven for runner trades (execution every time step)
|
||||
adjust_pos.set_breakeven_if_profit_target_hit(symbol, runner_magic);
|
||||
|
||||
// --- Only run strategy on a new bar to avoid repeated execution within the same candle
|
||||
if (!m_utils.is_new_bar(symbol, PERIOD_CURRENT, "00:05")) return;
|
||||
|
||||
// --- ATR-based trailing stop for runner trades only.
|
||||
// Updated each bar, calculation uses previous bar close. Trail by 1.5x ATR after 2x ATR in profit
|
||||
adjust_pos.trailing_stop_atr(symbol, runner_magic, PERIOD_CURRENT, 2.0, 1.5, 14, true);
|
||||
|
||||
// --- Construct signal data structures for long and short directions
|
||||
trading_signals long_signals = build_trading_signals(true, symbol, tl_hand, trig_hand, conf_hand, vol_hand, exit_hand);
|
||||
trading_signals short_signals = build_trading_signals(false, symbol, tl_hand, trig_hand, conf_hand, vol_hand, exit_hand);
|
||||
|
||||
// --- Determine whether to enter long:
|
||||
bool entry_long =
|
||||
standard_entry(long_signals)
|
||||
|| pullback_entry(long_signals)
|
||||
|| trendline_cross_entry(long_signals);
|
||||
|
||||
// --- Determine whether to enter short:
|
||||
bool entry_short =
|
||||
standard_entry(short_signals)
|
||||
|| pullback_entry(short_signals)
|
||||
|| trendline_cross_entry(short_signals);
|
||||
|
||||
// --- Determine whether exit long:
|
||||
bool exit_long =
|
||||
long_signals.exit_indi_sig
|
||||
|| long_signals.tl_cross_exit;
|
||||
|
||||
// --- Determine whether exit short:
|
||||
bool exit_short =
|
||||
short_signals.exit_indi_sig
|
||||
|| short_signals.tl_cross_exit;
|
||||
|
||||
// --- Centralised trade management:
|
||||
manage_orders(symbol, entry_long, entry_short, exit_long, exit_short);
|
||||
}
|
||||
|
||||
struct trading_signals {
|
||||
bool trigger_sig; int last_trigger_sig; bool trigger_agrees; bool trigger_agrees_prv; // Trigger signals
|
||||
bool conformation; // Comformation indicator signal
|
||||
bool tl_cross; bool tl_cross_prev; bool tl_agrees; bool tl_agrees_prev; // Trendline signals
|
||||
bool atr_b; bool atr_b_prev; bool atr_pullback; // Atr band signals
|
||||
bool volume; // Volume indicator signal
|
||||
bool exit_indi_sig; bool tl_cross_exit; // Exit signals
|
||||
};
|
||||
|
||||
trading_signals build_trading_signals(bool is_long, string symbol, int tl_hand, int trig_hand, int conf_hand, int vol_hand, int exit_hand) {
|
||||
|
||||
trading_signals long_sigs;
|
||||
trading_signals short_sigs;
|
||||
|
||||
// --- Trigger signals
|
||||
bool dummy1 = false, dummy2 = false;
|
||||
trigger(trig_hand, long_sigs.trigger_sig, short_sigs.trigger_sig, long_sigs.trigger_agrees, short_sigs.trigger_agrees, 1);
|
||||
trigger(trig_hand, dummy1, dummy2, long_sigs.trigger_agrees_prv, short_sigs.trigger_agrees_prv, 2);
|
||||
|
||||
track_trigger_signal.get_tracker(symbol).update_signal_tracker(long_sigs.trigger_sig, short_sigs.trigger_sig);
|
||||
long_sigs.last_trigger_sig = track_trigger_signal.get_tracker(symbol).get_long_signal();
|
||||
short_sigs.last_trigger_sig = track_trigger_signal.get_tracker(symbol).get_short_signal();
|
||||
|
||||
// --- Trendline crossover and direction signals
|
||||
tl_tools.detect_cross(symbol, tl_hand, long_sigs.tl_cross, short_sigs.tl_cross, 1);
|
||||
tl_tools.detect_cross(symbol, tl_hand, long_sigs.tl_cross_prev, short_sigs.tl_cross_prev, 2);
|
||||
tl_tools.trend_direction(symbol, tl_hand, long_sigs.tl_agrees, short_sigs.tl_agrees, 1);
|
||||
tl_tools.trend_direction(symbol, tl_hand, long_sigs.tl_agrees_prev, short_sigs.tl_agrees_prev, 2);
|
||||
|
||||
// --- Confirmation indicator signals
|
||||
conformation(conf_hand, long_sigs.conformation, short_sigs.conformation);
|
||||
|
||||
// --- Volume indicator signals
|
||||
volume_filter(vol_hand, long_sigs.volume, short_sigs.volume);
|
||||
|
||||
// --- ATR band signals
|
||||
// atr_bands.plot_bands(symbol, tl_hand, 14, PERIOD_CURRENT, 1.0, 1); // prit bands (for debugging)
|
||||
long_sigs.atr_b = atr_bands.inside_upper_band(symbol, tl_hand, 14, PERIOD_CURRENT, 1.0, 1);
|
||||
short_sigs.atr_b = atr_bands.inside_lower_band(symbol, tl_hand, 14, PERIOD_CURRENT, 1.0, 1);
|
||||
long_sigs.atr_b_prev = atr_bands.inside_upper_band(symbol, tl_hand, 14, PERIOD_CURRENT, 1.0, 2);
|
||||
short_sigs.atr_b_prev = atr_bands.inside_lower_band(symbol, tl_hand, 14, PERIOD_CURRENT, 1.0, 2);
|
||||
long_sigs.atr_pullback = atr_bands.crossed_below_upper_band(symbol, tl_hand, 14, PERIOD_CURRENT, 1.0, 1);
|
||||
short_sigs.atr_pullback = atr_bands.crossed_above_lower_band(symbol, tl_hand, 14, PERIOD_CURRENT, 1.0, 1);
|
||||
|
||||
// --- Exit signals
|
||||
exit_signal(exit_hand, long_sigs.exit_indi_sig, short_sigs.exit_indi_sig);
|
||||
long_sigs.tl_cross_exit = short_sigs.tl_cross; // Long exits when price crosses below the trendline
|
||||
short_sigs.tl_cross_exit = long_sigs.tl_cross; // Short exits when price crosses above the trendline
|
||||
|
||||
// --- Return side-specific struct
|
||||
return is_long ? long_sigs : short_sigs;
|
||||
}
|
||||
|
||||
|
||||
// A "standard" entry requires key conditions to be true: trigger, trendline agreement, ATR band position, confirmation, and volume.
|
||||
// A delayed entry is allowed one bar after the trigger, if confirmation and volume were not ready at the time.
|
||||
bool standard_entry(const trading_signals& data) {
|
||||
bool trigger_sig = data.trigger_sig; // Trigger signal occurred on the current candle
|
||||
bool trigger_sig_prv = (data.last_trigger_sig == 2); // Trigger signal occurred on the previous candle
|
||||
bool trigger_agrees = data.trigger_agrees; // Trigger direction still aligns on current candle
|
||||
bool tl_agree = data.tl_agrees; // Current price is on the correct side of the trendline
|
||||
bool tl_agree_prev = data.tl_agrees_prev; // Previous candle also agreed with trendline direction
|
||||
bool inside_atr_band = data.atr_b; // Price is within ATR band on current candle
|
||||
bool inside_atr_band_prev = data.atr_b_prev; // Price was within ATR band on previous candle
|
||||
bool confirmation = data.conformation; // Confirmation indicator aligns with trade direction
|
||||
bool volume = data.volume; // Volume supports entry
|
||||
|
||||
// --- Standard entry: all conditions met on the current candle
|
||||
bool current_entry_valid = trigger_sig && tl_agree && inside_atr_band && confirmation && volume;
|
||||
|
||||
// --- Delayed entry: previous candle had trigger + setup, current candle confirms with vol & confluence
|
||||
bool delayed_entry_valid = trigger_sig_prv && tl_agree_prev && inside_atr_band_prev && // Previous candle
|
||||
trigger_agrees && tl_agree && confirmation && volume; // Current candle
|
||||
|
||||
return current_entry_valid || delayed_entry_valid;
|
||||
}
|
||||
|
||||
// A "trendline cross" entry occurs after price crosses the trendline, provided other conditions (ATR band, confirmation, volume)
|
||||
// are met. Allows 1-bar delay if confirmation and volume were not ready at the time.
|
||||
bool trendline_cross_entry(const trading_signals& data) {
|
||||
bool tl_cross = data.tl_cross; // Trendline cross occurred on current candle **WORKS
|
||||
bool tl_cross_prev = data.tl_cross_prev; // Trendline cross occurred on previous candle **WORKS
|
||||
bool tl_agrees = data.tl_agrees; // Current price is on the correct side of the trendline
|
||||
bool trigger_agrees = data.trigger_agrees; // Trigger aligns with direction on current candle
|
||||
bool trigger_agrees_prev = data.trigger_agrees_prv; // Trigger aligned on previous candle
|
||||
bool trigger_sig_recent = (data.last_trigger_sig > 0 // Ensure the signal was actually triggered (not 0 or negative)
|
||||
&& data.last_trigger_sig <= 7 // Only consider signals within the last 7 bars
|
||||
&& data.last_trigger_sig != EMPTY_VALUE); // Avoid using uninitialized or invalid values
|
||||
bool inside_atr_band = data.atr_b; // Price is inside ATR band on current candle
|
||||
bool inside_atr_band_prev = data.atr_b_prev; // Price was inside ATR band on previous candle
|
||||
bool confirmation = data.conformation; // Confirmation indicator agrees with trade direction
|
||||
bool volume = data.volume; // Volume supports entry
|
||||
|
||||
// --- Entry on current candle: trendline cross with trigger agreement, ATR, confirmation and volume
|
||||
bool current_entry_valid =
|
||||
tl_cross && trigger_agrees && trigger_sig_recent && inside_atr_band && confirmation && volume;
|
||||
|
||||
// --- Delayed entry: trendline cross and alignment happened previously, confirmation and volume caught up now
|
||||
bool delayed_entry_valid =
|
||||
tl_cross_prev && trigger_agrees_prev && inside_atr_band_prev && // Previous candle
|
||||
trigger_sig_recent && trigger_agrees && tl_agrees && confirmation && volume; // Current candle
|
||||
|
||||
return current_entry_valid || delayed_entry_valid;
|
||||
}
|
||||
|
||||
// A "Pullback" entry expects price to pull back inside the ATR band 1 bar after a trendline cross.
|
||||
// The setup must have occurred on the previous candle, and all confirmations must align on the current candle.
|
||||
bool pullback_entry(const trading_signals& data) {
|
||||
bool tl_cross_prev = data.tl_cross_prev; // Trendline cross occurred on the previous candle
|
||||
bool trigger_agrees_prev = data.trigger_agrees_prv; // Trigger aligned on previous candle
|
||||
bool atr_pullback = data.atr_pullback; // Price has pulled back into ATR band on current candle
|
||||
bool tl_agree = data.tl_agrees; // Current price is on the correct side of the trendline
|
||||
bool trigger_agrees = data.trigger_agrees; // Trigger aligns with direction on current candle
|
||||
bool confirmation = data.conformation; // Confirmation indicator agrees with trade direction
|
||||
bool volume = data.volume; // Volume supports entry
|
||||
|
||||
// --- Pullback entry: setup on previous candle, pullback + confirmation on current
|
||||
return tl_cross_prev && trigger_agrees_prev && // Previous candle
|
||||
atr_pullback && tl_agree && trigger_agrees && confirmation && volume; // Current candle
|
||||
}
|
||||
|
||||
void manage_orders(string symbol, bool long_in, bool short_in, bool exit_long, bool exit_short) {
|
||||
// Split risk between TP and runner
|
||||
double lot_var = inp_lot_var / 2;
|
||||
|
||||
// --- Always manage open positions regardless of test window ---
|
||||
// Close TP positions if exit conditions met
|
||||
exit_orders.close_buy_orders(symbol, exit_long, 0, PERIOD_CURRENT, magic_number);
|
||||
exit_orders.close_sell_orders(symbol, exit_short, 0, PERIOD_CURRENT, magic_number);
|
||||
|
||||
// Close runner positions if exit triggered
|
||||
exit_orders.close_buy_orders(symbol, exit_long, 0, PERIOD_CURRENT, runner_magic);
|
||||
exit_orders.close_sell_orders(symbol, exit_short, 0, PERIOD_CURRENT, runner_magic);
|
||||
|
||||
// --- Only open new positions if in test window ---
|
||||
if (!data_split.in_test_period(inp_data_split_method)) return;
|
||||
|
||||
// --- Only open new positions if there are no open positions for symbol (regardless of side) ---
|
||||
if (entry_orders.count_open_positions(symbol, 0, magic_number) > 0) return;
|
||||
if (entry_orders.count_open_positions(symbol, 0, runner_magic) > 0) return;
|
||||
|
||||
// TP entries
|
||||
entry_orders.open_buy_orders(symbol, long_in, PERIOD_CURRENT, sl_mode, inp_sl_var, tp_mode, inp_tp_var, lot_mode, lot_var, magic_number);
|
||||
entry_orders.open_sell_orders(symbol, short_in, PERIOD_CURRENT, sl_mode, inp_sl_var, tp_mode, inp_tp_var, lot_mode, lot_var, magic_number);
|
||||
|
||||
// Runner entries
|
||||
entry_orders.open_runner_buy_order_with_virtual_tp(symbol, long_in, PERIOD_CURRENT, sl_mode, inp_sl_var, tp_mode, inp_tp_var, lot_mode, lot_var, runner_magic);
|
||||
entry_orders.open_runner_sell_order_with_virtual_tp(symbol, short_in, PERIOD_CURRENT, sl_mode, inp_sl_var, tp_mode, inp_tp_var, lot_mode, lot_var, runner_magic);
|
||||
}
|
||||
|
||||
// Trigger logic
|
||||
void trigger(int handle, bool& long_sig, bool& short_sig, bool& long_agree, bool& short_agree, int shift = 1) {
|
||||
|
||||
long_sig = false; short_sig = false; long_agree = false; short_agree = false;
|
||||
|
||||
double curr = m_utils.get_buffer_value(handle, shift);
|
||||
double prev = m_utils.get_buffer_value(handle, shift + 1);
|
||||
if (curr == EMPTY_VALUE || prev == EMPTY_VALUE) return;
|
||||
|
||||
long_sig = (prev < 50 && curr > 50);
|
||||
short_sig = (prev > 50 && curr < 50);
|
||||
long_agree = (curr > 50);
|
||||
short_agree = (curr < 50);
|
||||
}
|
||||
|
||||
// Confirmation logic
|
||||
void conformation(int handle, bool& long_ok, bool& short_ok) {
|
||||
|
||||
long_ok = false; short_ok = false;
|
||||
|
||||
double value = m_utils.get_buffer_value(handle, 1);
|
||||
if (value == EMPTY_VALUE) return;
|
||||
|
||||
long_ok = (value > 0);
|
||||
short_ok = (value < 0);
|
||||
}
|
||||
|
||||
// Volume filter
|
||||
void volume_filter(int handle, bool& long_ok, bool& short_ok) {
|
||||
long_ok = false; short_ok = false;
|
||||
|
||||
double curr = m_utils.get_buffer_value(handle, 1);
|
||||
double prev = m_utils.get_buffer_value(handle, 2);
|
||||
if (curr == EMPTY_VALUE || prev == EMPTY_VALUE) return;
|
||||
|
||||
long_ok = (curr > prev);
|
||||
short_ok = (curr < prev);
|
||||
}
|
||||
|
||||
// Exit signal
|
||||
void exit_signal(int handle, bool& exit_long, bool& exit_short) {
|
||||
exit_long = false; exit_short = false;
|
||||
|
||||
double curr = m_utils.get_buffer_value(handle, 1);
|
||||
double prev = m_utils.get_buffer_value(handle, 2);
|
||||
if (curr == EMPTY_VALUE || prev == EMPTY_VALUE) return;
|
||||
|
||||
exit_long = (prev > 50 && curr < 50);
|
||||
exit_short = (prev < 50 && curr > 50);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user