#property copyright "Mark K Cowan"
#property link      "http://hackology.co.uk"

// Equity interval at which to adjust the "bail out" threshold
extern int bailInterval = 200;

// Bail out threshold (at bailInterval, bailStopLoss = equity - bailThreshold)
extern int bailThreshold = 100;

// Limits on the lifetime of an order
extern int orderCloseMaxMinutes = 9000;
extern int orderCloseMinMinutes = 2000;

// Size of buffer for moving average
extern int movingAverageLength = 5;

// Banned months bitmask (lowest bit not used as Month starts from 1)
extern int bannedMonths = 4098;

// Close half an order when mean profit is reached
extern int closeHalfOnMeanProfit = 1;

// Set stop loss to break even after mean profit is reached
extern int breakEvenStopLossOnMeanProfit = 1;

// Max consecutive losses before switching from buy to sell to buy (0 for random buy/sell)
extern int maxConsecutiveLossesToSwitch = 2;

// Max equity loss (before trader aborts)
extern double maxEquityLoss = 50;

// Delay when switching between buy/sell mode
extern int switchDelay = 15;

// Delay between close and Open
extern int delayCloseToOpen = 5;

// Lots per initial order
extern double lotsPerOrder = 0.02;

// Fatal error occurred;
bool died = false;

// Time of last tick (used to identify start of new bar)
datetime lastTickTime = 0;

// Remaining delay before next open
int delay = 0;

// Total orders closed
int closed = 0;

// Total profit
double netProfit = 0;

// Buying (1) or selling (0)?  (ignored iff randomBuyOrSell)
bool buysell = 1;

// Current consecutive lossy orders
int consecutiveLosses = 0;

// Threshold at which to abort trading
int bailStopLoss = 0;

// Moving averages / gradients
double ask[100], bid[100];
double askGradient = 0, bidGradient = 0;
int maIdx = 0;

// Initial balance
double initialBalance;

// Minimum equity permissable
double minEquity;

// Minimum stop loss gap
int minTrail;

/*
 * Initialisation
 */
int init()
{
	initialBalance = AccountBalance();
	minEquity = AccountEquity() - maxEquityLoss;
	minTrail = (MarketInfo(Symbol(), MODE_STOPLEVEL) + 1) * Point;
	Print("Simple EA initiated");
	Print("Initial balance = ", initialBalance);
	Print("Initial equity = ", AccountEquity());
	Print("Equity floor level = ", minTrail);
	Print("Stop level = ", minTrail);
	updateBailStopLoss();
	return (0);
}

/*
 * Finalisation
 */
int deinit()
{
	Print("Simple EA terminated");
	return (0);
}

/*
 * Fuzzy floating-point comparison
 */
bool equal(double a, double b)
{
	return (NormalizeDouble(a - b, Digits) == 0);
}

/*
 * Updates the delay
 */
void updateDelay(int value)
{
	if (value > delay) {
		delay = value;
	}
}

/*
 * Closes half of an order
 */
void closeHalf()
{
	bool success = OrderClose(OrderTicket(), NormalizeDouble(OrderLots() / 2, Digits), OrderClosePrice(), 3);
	errorCheck(success, "Failed half-closing order!");
}

/*
 * Modifies the stop loss of a (assumed to be profiting)
 * order to ensure that it will break even
 */
void stopLossBreakEven()
{
	if (equal(OrderStopLoss(), OrderOpenPrice())) {
		return;
	}
	double stopLoss;
	if (OrderType() == OP_BUY) {
		stopLoss = OrderOpenPrice() - minTrail;
		if (MathAbs(stopLoss - Bid) < minTrail) {
			stopLoss = Bid - minTrail;
		}
	} else if (OrderType() == OP_SELL) {
		stopLoss = OrderOpenPrice() + minTrail;
		if (MathAbs(stopLoss - Ask) < minTrail) {
			stopLoss = Ask + minTrail;
		}
	}
	if (MathAbs(OrderOpenPrice() - OrderClosePrice()) <= minTrail) {
		return;
	}
	bool success = OrderModify(OrderTicket(), OrderOpenPrice(), OrderOpenPrice(), 0, 0);
	bool error = GetLastError();
	errorCheck(success || error == 1 || error == 130, "Failed stop-lossing order!");
}

/*
 * Executed on each tick
 *
 * Updates pending orders, implementing the following:
 *  + break even stop loss 
 *  + close half on mean profit
 */
void updatePendingOrders()
{
	for (int i = 0; i < OrdersTotal(); i++) {
		bool success = OrderSelect(i, SELECT_BY_POS);
		errorCheck(success, "Failed to select order!");
		if (closed > 0 && netProfit > 0) {
			double meanProfit = netProfit / closed;
			if (breakEvenStopLossOnMeanProfit && OrderProfit() >= meanProfit) {
				stopLossBreakEven();
			}
			if (closeHalfOnMeanProfit && OrderProfit() >= meanProfit && equal(OrderLots(), lotsPerOrder)) {
				closeHalf();
			}
		}
	}
}

/*
 * Executed on each tick
 *
 * Opens an order
 */
void openOrder()
{
	int ticket;
	bool randomBuyOrSell = maxConsecutiveLossesToSwitch == 0;
	if ((buysell && !randomBuyOrSell) || ((MathRand() % 2) && randomBuyOrSell)) {
		if (askGradient >= 0) {
			ticket = OrderSend(Symbol(), OP_BUY, lotsPerOrder, Ask, Bid - minTrail, 0, 0);
			errorCheck(ticket != -1, "Failed sending buy order!");
		}
	} else {
		if (bidGradient <= 0) {
			ticket = OrderSend(Symbol(), OP_SELL, lotsPerOrder, Bid, Ask + minTrail, 0, 0);
			errorCheck(ticket != -1, "Failed sending sell order!");
		}
	}
}

/*
 * Executed on each tick
 *
 * Closes an order if relevant assertions and
 * limits are valid
 *
 * Only orders closed via this function will
 * contribute to the profit accumulator
 */
void closeOrder()
{
	int minutesSinceOrder = (Time[0] - OrderOpenTime()) / 60;
	int randomMinutes = MathRand() % (orderCloseMaxMinutes - orderCloseMinMinutes) + orderCloseMinMinutes;
	if (minutesSinceOrder < randomMinutes) {
		return;
	}
	if (OrderProfit() > 0) {
		if (OrderType() == OP_BUY && bidGradient > 0 && randomMinutes < orderCloseMaxMinutes) {
			return;
		}
		if (OrderType() == OP_SELL && askGradient < 0 && randomMinutes < orderCloseMaxMinutes) {
			return;
		}
	}
	bool success = OrderClose(OrderTicket(), OrderLots(), OrderClosePrice(), 3);
	errorCheck(success, "Failed closing order!");
	closed++;
	updateProfit(OrderProfit());
}

/*
 * Executed on each tick
 *
 * Keeps track of consecutive losses
 */
void trackLosses(bool loss)
{
	if (loss) {
		consecutiveLosses++;
		if (consecutiveLosses > maxConsecutiveLossesToSwitch) {
			buysell = !buysell;
			consecutiveLosses = 0;
			updateDelay(switchDelay);
		}
	} else {
		consecutiveLosses = 0;
	}
}

/*
 * Executed on each tick
 *
 * Updates the bail stop loss bar
 */
void updateBailStopLoss()
{
	double equity = AccountEquity();
	errorCheck(equity >= bailStopLoss, "Equity stop loss bar hit, bailing!");
	double oldBailStopLoss = bailStopLoss;
	double newBailStopLoss = MathFloor(equity / bailInterval) * bailInterval - bailThreshold;
	if (newBailStopLoss < oldBailStopLoss) {
		return;
	}
	if (newBailStopLoss < initialBalance - bailThreshold) {
		newBailStopLoss = initialBalance - bailThreshold;
	}
	if (!equal(oldBailStopLoss, newBailStopLoss)) {
		Print("Equity=" + equity + "; Bail bar moved from ", oldBailStopLoss, " to ", newBailStopLoss);
		bailStopLoss = newBailStopLoss;
	}
}

/*
 * Closes all orders immediately
 */
void closeAllOrders()
{
	if (OrdersTotal() == 0) {
		return;
	}
	bool success;
	Print("Closing all (", OrdersTotal(), ") orders");
	for (int i = OrdersTotal() - 1; i >= 0; i--) {
		errorCheck(OrderSelect(i, SELECT_BY_POS), "Failed to select order!");
		errorCheck(OrderClose(OrderTicket(), OrderLots(), OrderClosePrice(), 5), "Failed to close order!");
	}
}

/*
 * Executed on each tick
 *
 * Updates the net profit accumulator
 * Triggers immediate closure of orders and termination
 * of the expert assistant if the bail limit is hit.
 */
void updateProfit(double delta)
{
	netProfit += delta;
	updateDelay(delayCloseToOpen);
	trackLosses(delta < 0);
}

/*
 * Executed on each tick
 *
 * Updates the cyclic history buffers used to
 * calculate moving averages and gradients
 */
void updateMovingAverage()
{
	ask[maIdx] = Ask;
	bid[maIdx] = Bid;
	int prevIdx = (maIdx + movingAverageLength - 1) % movingAverageLength;
	askGradient = (ask[maIdx] - ask[prevIdx]) / movingAverageLength;
	bidGradient = (bid[maIdx] - bid[prevIdx]) / movingAverageLength;
	maIdx = (maIdx + 1) % movingAverageLength;
}

/*
 * Tests the condtion, if false the trader is terminated
 * and all orders are closed.
 */
void errorCheck(bool condition, string msg)
{
	if (!condition) {
		Print("ABORT: " + msg);
		/*
		 * The following prevents a recursive errorCheck
		 * in the event that orders cannot be closed
		 */
		if (!died) {
			died = true;
			closeAllOrders();
		}
	}
}

/*
 * Returns true if we're at the start of a new bar
 */
bool isNewBar()
{
	bool isNew = Time[0] != lastTickTime;
	lastTickTime = Time[0];
	return (isNew);
}

/*
 * Executed on each tick
 *
 * Returns immediately unless a new bar has started
 *
 * If no orders are open, it opens an order.
 * Otherwise, it closes the last order in the list,
 * assuming relevant delays have expired.
 * 
 */
int start()
{
	if (died || !isNewBar() || ((Month() << 1) & bannedMonths)) {
		return (0);
	}
	errorCheck(AccountEquity() > minEquity, "Equity floor reached, bailing!");
	updateBailStopLoss();
	if (movingAverageLength > 1) {
		updateMovingAverage();
	}
	updatePendingOrders();
	if (OrdersTotal() == 0) {
		if (delay > 0) {
			delay--;
		}
		if (delay == 0) {
			openOrder();
		}
	} else {
		errorCheck(OrderSelect(OrdersTotal() - 1, SELECT_BY_POS), "Failed to select order");
		closeOrder();
	}
	return (0);
}