/*
*/

#property copyright "ForexHackedMod 004.02"
#property link      "http://forum.tradelikeapro.ru"

#include <stderror.mqh>

extern bool Allow1Buy = true;
extern bool Allow1Sell = true;
extern bool Order2Booster = true;
extern bool UseIndicators = true;
extern bool AllowExtraOrders = false;
extern bool UseBuyGridParametersOnly = false;

extern double BuyLots = 0.02;
extern double BuyBooster = 1.65;
extern double BuyPipStarter = 37.2;
extern double BuyTakeProfit = 49.0;
extern int BuyMaxOrders = 9;

extern double SellLots = 0.02;
extern double SellBooster = 1.65;
extern double SellPipStarter = 37.2;
extern double SellTakeProfit = 49.0;
extern int SellMaxOrders = 9;

// minimal allowed TP for price gap order
extern double PriceGapMinimalTP = 3.0;
// distance between price gap TP and normal pyramid order
extern double PriceGapOrderDistance = 0;

extern bool AllowiStopLoss = true;
extern double iStopLoss = 295;
extern int Slippage = 2;

extern double StopLossPct = 100;
extern double TakeProfitPct = 100;

extern int StartHour = 0;
extern int StartMinute = 0;
extern int StopHour = 0;
extern int StopMinute = 0;
extern int StartingTradeDay = 0;
extern int EndingTradeDay = 7;

extern string MagicNumberInfo = "Magic Number Must be UNIQUE for each chart!";
extern int MagicNumber = 1002401;
extern string TradeChartInfo = "TradeChart = name of currencies pair used for trading";
extern string TradeChart = "GBPUSD";
extern int MAPeriod = 11;
extern double SARStep = 0.0636;
extern string EA_Name = "ForexHackedMod 004.02";

bool
gIsNotOptimization,
gIsNotReady = false,
gLowBalance = false,
gIsNotTesting,
gModifiedTP[2],
gUseGapTP = false;

int
gMAShift = 0,
gMAMethod = MODE_LWMA,
gMAPrice = PRICE_WEIGHTED,
gLotStepDigits,
gFileHandler = 0,
gStartMinutes,
gStopMinutes,
gGapOrderTicket[2],
gLastTicket[2],
gOrders[2],
gLevel[2],
gTickets[2][20];

double
gSARMaximum = 0.2,
gLotMargin,
gPriceValue,
gTicksPerPrice,
gBaseLotMargin,
gBalanceNeeded,
gSlippage,
gLots[2],
gBooster[2],
gPipStarter[2],
gTakeProfit[2],
gPipStarterInv[2],
gStopLevel,
gSumLots[2],
gSumPriceLots[2],
gMeanPrice[2],
gLastTP[2],
gMinimalGap;

string gMutexName = "FHMod";

double iif(bool condition, double ifTrue, double ifFalse) {
  if (condition) return (ifTrue);
  return (ifFalse);
}

void Log(string text) {
  if (gFileHandler > 0) FileWrite(gFileHandler, TimeToStr(TimeCurrent(), TIME_DATE | TIME_SECONDS) + ": " + text);
}

int deinit() {
  if (gFileHandler > 0) FileClose(gFileHandler);
  return (0);
}

void CalcLevel(int type) {
  double booster = gBooster[type];
//   gLevel[type] = MathRound(MathLog(1 + (booster - 1) * gSumLots[type] / gLots[type]) / MathLog(booster));
  gLevel[type] = 1 + MathRound(MathLog(OrderLots() / gLots[type]) / MathLog(booster));
}

void AnalyzeOrders() {
  int i, n = OrdersTotal(), type;
  double tmp;
  for (i = 0; i < 2; i++) {
    gLevel[i] = 0;
    gOrders[i] = 0;
    gLastTP[i] = 0;
    gLastTicket[i] = 0;
    gGapOrderTicket[i] = 0;
    gModifiedTP[i] = false;
    gSumLots[i] = 0;
    gSumPriceLots[i] = 0;
  }
  // find orders tickets
  for (i = 0; i < n; i++) {
    if (!OrderSelect(i, SELECT_BY_POS, MODE_TRADES) || OrderMagicNumber() != MagicNumber) continue;
    type = OrderType();
    if (type > 1) continue;
    gTickets[type][gOrders[type]] = OrderTicket();
    gOrders[type]++;
    tmp = OrderLots();
    gSumLots[type] += tmp;
    gSumPriceLots[type] += tmp * OrderOpenPrice();
  }
  // find last takeprofit
  for (i = 0; i < 2; i++) {
    if (gOrders[i] < 1) continue;
    gMeanPrice[i] = gSumPriceLots[i] / gSumLots[i];
    gLastTicket[i] = gTickets[i][gOrders[i] - 1];
    OrderSelect(gLastTicket[i], SELECT_BY_TICKET);
    gLevel[i] = 1;
    if (gOrders[i] < 2) continue;
    CalcLevel(i);
    gModifiedTP[i] = true;
    gLastTP[i] = OrderTakeProfit();
    tmp = OrderOpenPrice();
    OrderSelect(gTickets[i][gOrders[i] - 2], SELECT_BY_TICKET);
    tmp -= OrderOpenPrice();
    if (tmp < 0) tmp = -tmp;
    tmp -= gPipStarter[i];
    if (tmp >= gMinimalGap) {
      gGapOrderTicket[i] = gLastTicket[i];
      gLastTP[i] = OrderTakeProfit();
    }
  }
}

double PipToPrice(double x) {
  return (NormalizeDouble(x * Point, Digits));
}

void LimitPipToPrice(double &limit) {
  limit *= Point;
  limit = iif(limit < gStopLevel, gStopLevel, NormalizeDouble(limit, Digits));
}

int init() {
  if (TradeChart != Symbol()) {
    string msg = "EA is OFF! TradeChart differs from chart symbol: " + TradeChart + " != " + Symbol();
    Alert(msg);
    Log(msg);
    gIsNotReady = true;
    return (1);
  }

  gIsNotOptimization = !IsOptimization();
  gIsNotTesting = !IsTesting() && gIsNotOptimization;
  if (gIsNotTesting && !GlobalVariableCheck(gMutexName)) GlobalVariableSet(gMutexName, 0);
  if (gIsNotOptimization) {
    gFileHandler = FileOpen(WindowExpertName() + "_" + Time[0] + "_" + TradeChart + "_" + MagicNumber + ".log", FILE_WRITE);
  }
  gLotStepDigits = MathRound((-MathLog(MarketInfo(TradeChart, MODE_LOTSTEP))) / MathLog(10));
  gStopLevel = MarketInfo(TradeChart, MODE_STOPLEVEL) * Point;

  gLots[0] = BuyLots;
  gBooster[0] = BuyBooster;
  gPipStarter[0] = BuyPipStarter;
  gTakeProfit[0] = BuyTakeProfit;
  if (UseBuyGridParametersOnly) {
    gLots[1] = gLots[0];
    gBooster[1] = gBooster[0];
    gPipStarter[1] = gPipStarter[0];
    gTakeProfit[1] = gTakeProfit[0];
  } else {
    gLots[1] = SellLots;
    gBooster[1] = SellBooster;
    gPipStarter[1] = SellPipStarter;
    gTakeProfit[1] = SellTakeProfit;
  }

  int i;
  if (Digits == 5 || Digits == 3) {
    for (i = 0; i < 2; i++) {
      gPipStarter[i] *= 10.0;
      gTakeProfit[i] *= 10.0;
    }
    iStopLoss *= 10.0;
    PriceGapMinimalTP *= 10.0;
    PriceGapOrderDistance *= 10.0;
  }

  LimitPipToPrice(iStopLoss);
  LimitPipToPrice(PriceGapMinimalTP);
  PriceGapOrderDistance = PipToPrice(PriceGapOrderDistance);
  gMinimalGap = PriceGapMinimalTP + PriceGapOrderDistance;
  gUseGapTP = PriceGapOrderDistance > 0;

  for (i = 0; i < 2; i++) {
    gPipStarter[i] = PipToPrice(gPipStarter[i]);
    gTakeProfit[i] = PipToPrice(gTakeProfit[i]);
    gPipStarterInv[i] = 1.0 / ((gBooster[i] - 1.0) * gTakeProfit[i]);
    gSumLots[i] = 0;
  }
  AnalyzeOrders();

  // for "trade time" check
  gStartMinutes = 60 * StartHour + StartMinute;
  gStopMinutes = 60 * StopHour + StopMinute;
  if (gStartMinutes > gStopMinutes) {
    int tmp = gStartMinutes;
    gStartMinutes = gStopMinutes;
    gStopMinutes = tmp;
  }

  // for "close by percentage" check
  StopLossPct = 1.0 - 0.01 * StopLossPct;
  TakeProfitPct = 1.0 + 0.01 * TakeProfitPct;

  // for "no money" check
  gTicksPerPrice = 1.0 / MarketInfo(TradeChart, MODE_TICKSIZE);
  gPriceValue = MarketInfo(TradeChart, MODE_TICKVALUE) * gTicksPerPrice;
  gLotMargin = MarketInfo(TradeChart, MODE_MARGINREQUIRED);
  gBaseLotMargin = MarketInfo(TradeChart, MODE_LOTSIZE) / AccountLeverage();

  // for lot size correction, if gap > slippage
  gSlippage = Point * Slippage;

  if (gIsNotOptimization) Log("Initialization finished. TradeChart = " + TradeChart);
  else EA_Name = "";
  return (0);
}

bool IsTradeTime() {
  int day = DayOfWeek();
  if (day < StartingTradeDay || day > EndingTradeDay) return (false);
  if (gStartMinutes == gStopMinutes) return (true);
  datetime now = TimeCurrent();
  int minuntes = 60 * TimeHour(now) + TimeMinute(now);
  return (gStartMinutes <= minuntes && minuntes < gStopMinutes);
}

double RequiredBalance(double new_lot, double sl, double lots_opened = 0) {
  return (new_lot * gLotMargin + (new_lot + lots_opened) * sl * gPriceValue);
}

int StrategySignal() {
  double sar = iSAR(TradeChart, PERIOD_H1, SARStep, gSARMaximum, 0);
  double ma = iMA(TradeChart, PERIOD_H1, MAPeriod, gMAShift, gMAMethod, gMAPrice, 0);
  if (sar > ma) return (-1);
  if (sar < ma) return (1);
  return (0);
}

// set order limits, return true in case of error
bool SetOrderLimits(double takeprofit = 0, double stoploss = 0) {
  if (OrderCloseTime() != 0) return (false);

  bool skip_modify = true;
  double tp = OrderTakeProfit(), sl = OrderStopLoss();
  if (takeprofit > 0 && tp != takeprofit) {
    skip_modify = false;
    tp = takeprofit;
  }
  if (stoploss > 0 && sl != stoploss) {
    skip_modify = false;
    sl = stoploss;
  }
  if (skip_modify) return (false);

  skip_modify = !OrderModify(OrderTicket(), OrderOpenPrice(), sl, tp, 0, Pink) && GetLastError() > ERR_NO_RESULT;
  if (skip_modify && gIsNotOptimization) {
    Log("OrderModify error! Ticket #" + OrderTicket() +
        ", OpenPrice: " + DoubleToStr(OrderOpenPrice(), Digits) +
        ", TP: " + DoubleToStr(tp, Digits) +
        ", SL: " + DoubleToStr(sl, Digits));
  }
  return (skip_modify);
}

// return true in case of error
bool SetOrdersTakeProfit(int type, double takeprofit) {
  double price;
  if (type == OP_BUY) price = Bid;
  else price = Ask;
  if (MathAbs(takeprofit - price) < gStopLevel) return (true);
  bool has_error = false;
  int pos = gOrders[type] - 1;
  if (gLastTicket[type] == gGapOrderTicket[type]) pos--;
  for (; pos >= 0; pos--) {
    if (!OrderSelect(gTickets[type][pos], SELECT_BY_TICKET) || OrderTakeProfit() == takeprofit) continue;
    if (SetOrderLimits(takeprofit, 0)) has_error = true;
  }
  return (has_error);
}

bool OpenDeal(int type, double last_open_price, double gap) {
  int new_ticket, level = gLevel[type];
  double tp, sl, price, lots = gLots[type], booster = gBooster[type];
  bool is_opened = true;
  color arrow_color;
  string msg;

  if (type == OP_BUY) {
    price = Ask;
    arrow_color = Green;
    msg = "BUY";
  } else {
    price = Bid;
    arrow_color = Pink;
    msg = "SELL";
  }

  // calculate order lots
  if (level > 0) {
    if (level > 1) lots *= MathPow(booster, level);
    else if (Order2Booster || gOrders[type] > 1) lots *= booster;
    // price gap case
    if (gap > gSlippage && (gap < gMinimalGap || !gUseGapTP)) lots *= (1.0 + gap * gPipStarterInv[type]);
  }
  lots = NormalizeDouble(lots, gLotStepDigits);

  // check for "no money"
  tp = RequiredBalance(lots, gPipStarter[type], gSumLots[type]);
  if (AccountFreeMargin() < tp) {
    if (!gLowBalance) {
      gLowBalance = true;
      gBalanceNeeded = AccountMargin() + tp;
      if (gIsNotOptimization) Log("ERROR! " + msg + " order canceled! Balance < " + gBalanceNeeded);
    }
    return (false);
  }
  gLowBalance = false;

  new_ticket = OrderSend(TradeChart, type, lots, price, Slippage, 0, 0, EA_Name, MagicNumber, 0, arrow_color);
  if (new_ticket < 0) {
    if (gIsNotOptimization) Log("FAILED " + msg + ", lots: " + lots + ", sl: " + sl + ", tp: " + tp);
    is_opened = false;
  } else {
    gTickets[type][gOrders[type]] = new_ticket;
    gLastTicket[type] = new_ticket;
    gOrders[type]++;
    gGapOrderTicket[type] = 0;
    if (OrderSelect(new_ticket, SELECT_BY_TICKET)) {
      price = OrderOpenPrice();
      tp = gTakeProfit[type];
      if (gap >= gMinimalGap && gUseGapTP) {
        gap = last_open_price - price;
        if (gap < 0) gap = -gap;
        gap -= gPipStarter[type];
        if (gap >= gMinimalGap) {
          gGapOrderTicket[type] = new_ticket;
          tp = gap - PriceGapOrderDistance;
        }
      }
      if (type == OP_BUY) {
        tp = price + tp;
        sl = price - iStopLoss;
      } else {
        tp = price - tp;
        sl = price + iStopLoss;
      }
      if (!AllowiStopLoss) sl = 0;
    }
    gSumLots[type] += lots;
    gSumPriceLots[type] += lots * price;
    if (gOrders[type] > 1) {
      gMeanPrice[type] = gSumPriceLots[type] / gSumLots[type];
      CalcLevel(type);
    } else {
      gMeanPrice[type] = price;
      gLevel[type] = 1;
    }
    OrderModify(new_ticket, price, sl, tp, 0, Black);
    if (gGapOrderTicket[type] == 0) {
      gLastTP[type] = tp;
      gModifiedTP[type] = (gOrders[type] > 1);
    }
    if (gIsNotOptimization) {
      Log(msg + " #" + new_ticket);
    }
  }
  return (is_opened);
}

void ManageDeals(int type) {
  if (gIsNotTesting && !GlobalVariableSetOnCondition(gMutexName, MagicNumber, 0)) return;

  bool allow_first_order, has_open_order = false;
  double dd, price;
  int n_max, pos;

  // check that pyramid is open
  for (pos = gOrders[type] - 1; pos >= 0; pos--) {
    if (!OrderSelect(gTickets[type][pos], SELECT_BY_TICKET)) continue;
    if (OrderCloseTime() == 0) {
      has_open_order = true;
      break;
    } else {
      dd = OrderLots();
      gSumLots[type] -= dd;
      gSumPriceLots[type] -= dd * OrderOpenPrice();
    }
  }

  if (has_open_order) {
    price = OrderOpenPrice();
    // some orders were closed
    if (gOrders[type] != pos + 1) {
      if (gLastTicket[type] != gGapOrderTicket[type]) {
        gLastTP[type] = OrderTakeProfit();
        gModifiedTP[type] = true;
      }
      gLastTicket[type] = gTickets[type][pos];
      gOrders[type] = pos + 1;
      CalcLevel(type);
      if (pos > 0) gMeanPrice[type] = gSumPriceLots[type] / gSumLots[type];
      else gMeanPrice[type] = price;
    }

    // manage open deals
    if (type == OP_BUY) {
      n_max = BuyMaxOrders;
      dd = price - Ask;
    } else {
      n_max = SellMaxOrders;
      dd = Bid - price;
    }
    if (dd > gPipStarter[type] && (gLevel[type] < n_max || AllowExtraOrders)) {
      dd -= gPipStarter[type];
      OpenDeal(type, price, dd);
    } else if (gModifiedTP[type]) {
      gModifiedTP[type] = SetOrdersTakeProfit(type, gLastTP[type]);
    }
  } else {
    gOrders[type] = 0;

    // try to open first order
    if (type == OP_BUY) {
      pos = 1;
      allow_first_order = Allow1Buy;
    } else {
      pos = -1;
      allow_first_order = Allow1Sell;
    }
    if ((!UseIndicators || StrategySignal() == pos) && IsTradeTime() && allow_first_order) {
      gLevel[type] = 0;
      gSumLots[type] = 0;
      gSumPriceLots[type] = 0;
      OpenDeal(type, 0, 0);
    }
  }
  if (gIsNotTesting) GlobalVariableSet(gMutexName, 0);
}

double BalanceDrawdown() {
  double dd = 100.0 * (1.0 - AccountEquity() / AccountBalance());
  if (dd < 0.01) dd = 0;
  return (dd);
}

void ChartComment() {
  string msg =
    EA_Name + " Loaded Successfully" +
    "\nAccount Leverage:  1 : " + AccountLeverage() +
    "\nAccount Type:  " + AccountServer() +
    "\nServer Time:  " + TimeToStr(TimeCurrent(), TIME_SECONDS) +
    "\nAccount Equity:  " + DoubleToStr(AccountEquity(), 2) +
    "\nFree Margin:  " + DoubleToStr(AccountFreeMargin(), 2) +
    "\nDrawdown:  " + DoubleToStr(BalanceDrawdown(), 2) + "%";
  if (gLowBalance) {
    msg = msg + "\n\nLOW BALANCE! Needed:  " + DoubleToStr(gBalanceNeeded, 2);
  }
  Comment(msg);
}

void CloseByPercentage() {
  bool need_close = false;
  if (AccountEquity() < StopLossPct * AccountBalance()) {
    if (gIsNotOptimization) Log("Percentage Stop Loss: Balance = " + AccountBalance() + ", Equity = " + AccountEquity());
    need_close = true;
  }
  if (AccountEquity() > TakeProfitPct * AccountBalance()) {
    if (gIsNotOptimization) Log("Percentage Take Profit: Balance = " + AccountBalance() + ", Equity = " + AccountEquity());
    need_close = true;
  }
  int slippage;
  if (need_close) slippage = MarketInfo(TradeChart, MODE_SPREAD);
  while (need_close) {
    need_close = false;
    for (int pos = OrdersTotal() - 1; pos >= 0; pos--) {
      if (!OrderSelect(pos, SELECT_BY_POS) || OrderMagicNumber() != MagicNumber) continue;
      if (gIsNotOptimization) Log("Close by percentage #" + OrderTicket());
      if (!OrderClose(OrderTicket(), OrderLots(), OrderClosePrice(), slippage, White)) {
        if (gIsNotOptimization) Log("Close error #" + OrderTicket());
        need_close = true;
      }
    }
  }
}

int start() {
  if (gIsNotReady) return (1);
  if (gIsNotOptimization) {
    ChartComment();
    CloseByPercentage();
  }
  if (gIsNotTesting) {
    gPriceValue = MarketInfo(TradeChart, MODE_TICKVALUE) * gTicksPerPrice;
    gLotMargin = MarketInfo(TradeChart, MODE_MARGINREQUIRED);
    gStopLevel = MarketInfo(TradeChart, MODE_STOPLEVEL) * Point;
  } else {
    gLotMargin = gBaseLotMargin * Ask;
  }
  ManageDeals(OP_BUY);
  ManageDeals(OP_SELL);
  return (0);
}
