/*
*/

#property copyright "ForexHackedMod 004.09"
#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 bool     CloseAllBuy = false;
extern bool     CloseAllSell = false;
extern bool     MoveBuyTP = false;
extern bool     MoveSellTP = false;
extern double   MoveTP = 1.0;
extern int      BreakEvenLevel = 0;

extern double   BuyLots = 0.18;
extern double   BuyBooster = 1.986;
extern double   BuyPipStarter = 8.0;
extern double   BuyTakeProfit = 21.6;
extern int      BuyMaxOrders = 8;

extern double   SellLots = 0.18;
extern double   SellBooster = 1.986;
extern double   SellPipStarter = 7.5;
extern double   SellTakeProfit = 22.8;
extern int      SellMaxOrders = 8;

// 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 = 70;
extern int      Slippage = 1;

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 string   EA_Name = "ForexHackedMod 004.09";

extern int      bb_period = 5;
extern int      bb_deviation = 1;
extern int      stoch_k = 3;
extern int      stoch_d = 1;
extern int      stoch_slowing = 2;
extern double   stoch_lo_level = 34;
extern double   stoch_up_level = 86;
extern int      rsi_period = 20;
extern double   rsi_lower = 29;
extern double   rsi_upper = 75;

bool
gIsNotOptimization,
gIsNotReady = false,
gLowBalance = false,
gIsNotTesting,
gModifiedTP[2],
gUseGapTP = false,
gMoveTPMode[2],
gCloseAllMode[2];

int
gSignalBuy = 1,
gSignalSell = -1,
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,
sMaxTotalLots;

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) {
    if (sMaxTotalLots > 0) Log("Maximum of lots total: " + DoubleToStr(sMaxTotalLots, gLotStepDigits));
    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));
}

bool CloseAll(int type) {
  double price;
  bool repeat_needed = false;
  if (gIsNotTesting) RefreshRates();
  if (type == OP_BUY) price = Bid;
  else price = Ask;
  for (int pos = gOrders[type] - 1; pos >= 0; pos--) {
    if (!OrderSelect(gTickets[type][pos], SELECT_BY_TICKET) || OrderCloseTime() != 0) continue;
    if (gIsNotTesting && RefreshRates()) {
      if (type == OP_BUY) price = Bid;
      else price = Ask;
    }
    if (!OrderClose(OrderTicket(), OrderLots(), price, Slippage)) repeat_needed = true;
  }
  return (repeat_needed);
}

void AnalyzeOrders() {
  int i, n = OrdersTotal(), type;
  double tmp, tp;
  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] = NormalizeDouble(gSumPriceLots[i] / gSumLots[i], Digits);
    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();
    }
  }

  // move TP close to breakeven level
  for (i = 0; i < 2; i++) {
    if (!gMoveTPMode[i]) continue;
    if (BreakEvenLevel <= 0) gMoveTPMode[i] = false;
    if (gOrders[i] < 1) continue;
    gModifiedTP[i] = true;
    if (i == OP_BUY) tmp = MoveTP;
    else tmp = -MoveTP;
    gLastTP[i] = gMeanPrice[i] + tmp;
  }
}

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;

  gMoveTPMode[0] = MoveBuyTP;
  gMoveTPMode[1] = MoveSellTP;
  gCloseAllMode[0] = CloseAllBuy;
  gCloseAllMode[1] = CloseAllSell;

  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;
    MoveTP *= 10.0;
  }

  LimitPipToPrice(iStopLoss);
  LimitPipToPrice(PriceGapMinimalTP);
  PriceGapOrderDistance = PipToPrice(PriceGapOrderDistance);
  gMinimalGap = PriceGapMinimalTP + PriceGapOrderDistance;
  gUseGapTP = PriceGapOrderDistance > 0;

  MoveTP = PipToPrice(MoveTP);

  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 = "";

  sMaxTotalLots = 0;
  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() {
  int bb_shift = 0;
  int stoch_price_field = 0;
  int stoch_shift = 0;
  int rsi_shift = 0;
  double upBB = iBands(TradeChart, 0, bb_period, bb_deviation, 0, PRICE_CLOSE, MODE_UPPER, bb_shift);
  double loBB = iBands(TradeChart, 0, bb_period, bb_deviation, 0, PRICE_CLOSE, MODE_LOWER, bb_shift);
  double stoch = iStochastic(TradeChart, 0, stoch_k, stoch_d, stoch_slowing, MODE_SMA, stoch_price_field, MODE_SIGNAL, stoch_shift);
  double rsi = iRSI(TradeChart, 0, rsi_period, PRICE_CLOSE, rsi_shift);
  if (High[bb_shift] > upBB && stoch > stoch_up_level && rsi > rsi_upper) return (gSignalBuy);
  if (Low[bb_shift] < loBB && stoch < stoch_lo_level && rsi < rsi_lower)   return (gSignalSell);
  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, if repeat is needed
bool SetOrdersTakeProfit(int type, double takeprofit) {
  bool repeat_needed = false;
  double price, tp;
  if (gIsNotTesting) RefreshRates();
  if (type == OP_BUY) price = Bid;
  else price = Ask;
  if (MathAbs(takeprofit - price) < gStopLevel) {
    repeat_needed = true;
    if (type == OP_BUY) tp = price + gStopLevel;
    else tp = price - gStopLevel;
  } else tp = takeprofit;
  int pos = gOrders[type] - 1;
  if (gLastTicket[type] == gGapOrderTicket[type] && !gMoveTPMode[type]) pos--;
  for (; pos >= 0; pos--) {
    if (!OrderSelect(gTickets[type][pos], SELECT_BY_TICKET) || OrderTakeProfit() == takeprofit) continue;
    if (gIsNotTesting && RefreshRates()) {
      if (type == OP_BUY) price = Bid;
      else price = Ask;
      if (MathAbs(takeprofit - price) < gStopLevel) {
        repeat_needed = true;
        if (type == OP_BUY) tp = price + gStopLevel;
        else tp = price - gStopLevel;
      } else tp = takeprofit;
    }
    if (SetOrderLimits(tp, 0)) repeat_needed = true;
  }
  return (repeat_needed);
}

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] = NormalizeDouble(gSumPriceLots[type] / gSumLots[type], Digits);
      CalcLevel(type);
    } else {
      gMeanPrice[type] = price;
      gLevel[type] = 1;
    }
    if (gMoveTPMode[type] && gLevel[type] >= BreakEvenLevel) {
      tp = gMeanPrice[type];
      if (type == OP_BUY) tp += MoveTP;
      else tp -= MoveTP;
    }
    OrderModify(new_ticket, price, sl, tp, 0, Black);
    if (gGapOrderTicket[type] == 0 || gMoveTPMode[type]) {
      gLastTP[type] = tp;
      gModifiedTP[type] = (gOrders[type] > 1);
    }
    if (gIsNotOptimization) {
      lots = gSumLots[0] + gSumLots[1];
      if (sMaxTotalLots < lots) sMaxTotalLots = lots;
    }
  }
  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) {
      gLastTicket[type] = gTickets[type][pos];
      gOrders[type] = pos + 1;
      CalcLevel(type);
      if (pos > 0) gMeanPrice[type] = NormalizeDouble(gSumPriceLots[type] / gSumLots[type], Digits);
      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 = gSignalBuy;
      allow_first_order = Allow1Buy;
    } else {
      pos = gSignalSell;
      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;
  }
  if (need_close) {
    gCloseAllMode[0] = true;
    gCloseAllMode[1] = true;
  }
}

int start() {
  if (gIsNotReady) return (1);
  if (gIsNotOptimization) {
    ChartComment();
    CloseByPercentage();
    for (int i = 0; i < 2; i++) if (gCloseAllMode[i]) gCloseAllMode[i] = CloseAll(i);
    if (gCloseAllMode[0] || gCloseAllMode[1]) return (2);
  }
  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);
}