/*
	quantisan2 - JForex contest strategy
	version 2.22
	November 2010
	Copyright 2010 Paul at Quantisan.com
	
	Changelog:
	v2.22	- Nov 2010 commit
	v2.21	- update params.
	v2.20	- Oct 2010 commit
	v2.19	- refactored setSentiment()
			- update params.
			- re-added breakeven
			- readded setRiskAmt()/setRickPct()
	v2.18	- Sept commit
	v2.17	- fixed tradingAllowed check bug
			- fixed getLabel() parseInt index bug
			- removed goReverse()
			- update for new Sept rules
			- integrated stop & tp into order
			- emptied onTick()
			- shortened timeframes
	v2.16	- August commit
	v2.15	- fixed riskPct not initiated dynamically
			- added momentum check as entry condition
			- appended reverse trade upon closing
			- removed getRiskAdj()
	v2.14	- commit for July 2010 competition
			- optimized parameters
			- fixed sentiment on start
			- added regression position sizing
	v2.13	- name changed from ma_retrace2 to quantisan1
			- new rules in July
			- removed all @Configurable
			- ensure max. 1 position in acct
			- added position sizing
			- breakeven stop
			- two-tier time frame strategy
	v2.12	- commit for June 2010 competition
	v2.11	- fixed getLabel open position number overlap bug
	v2.10	- removed @Config modifying open position to adhere to contest rules
			- drawTrade() NP exception bug
			- added exit automation
			- added exit target and stop limit prices
	v2.0 	- renamed from ma_scalp1 (April 2010 contest)

*/
package jforex;

import com.dukascopy.api.*;

import java.util.*;
import java.awt.Color;

public class quantisan2 implements IStrategy {
	private final double version = 2.22;
	
	private IEngine engine;
	private IIndicators indicators;
	private IHistory history;
	private int tagCounter;
	private IConsole console;
	private IContext context;
	private IAccount account;

	private double[] maShr1 = new double[Instrument.values().length];
	private double[] maInt1 = new double[Instrument.values().length];
	private double[] atr = new double[Instrument.values().length];	
	
	private Sentiment[] sentiment = new Sentiment[Instrument.values().length];		
	
	// parameters	
	private final int SLIPPAGE = 3;
	private final int LOCKPIP = 3;
	
	private final Period PERIODSHR = Period.FIVE_MINS;
	private final Period PERIODINT = Period.THIRTY_MINS;
	
	private final Filter indFilter = Filter.ALL_FLATS;
	private final IIndicators.MaType maType = IIndicators.MaType.SMA;	
	
	// targets
	private double stopATR = 9.0;
	private double tpATR = 15.0;
	
	// indicators
	private int momoOB = 55;
	private final int lenShr = 13;	
	private final int lenInt = 55;

	private double beRatio = 0.5;

    // ** onBar used for entries **
    public void onBar(Instrument instrument, Period period, IBar askBar, IBar bidBar) throws JFException 
	{
		double maShr, maInt;
		double chanTop, chanBot;
		
		// higher time frame get sentiment
		if (period == PERIODINT)
		{				
			setSentiment(instrument);			
			return;			
		}
		
		if (period != PERIODSHR)	return;		// skip all other periods
				
		this.atr[instrument.ordinal()] = indicators.atr(instrument, PERIODSHR, 
			OfferSide.BID, lenShr, indFilter, 1, bidBar.getTime(), 0)[0];
		
		// moving averages
		maShr = indicators.ma(instrument, period,
			OfferSide.BID, IIndicators.AppliedPrice.MEDIAN_PRICE, 
			lenShr, maType, indFilter, 1, bidBar.getTime(), 0)[0];
			
		maInt = indicators.ma(instrument, period,
			OfferSide.BID, IIndicators.AppliedPrice.MEDIAN_PRICE, 
			lenInt, maType, indFilter, 1, bidBar.getTime(), 0)[0];	
		
		// if not enough bars
		if (this.maShr1[instrument.ordinal()] <= 0d || maShr <= 0d ||
			this.maInt1[instrument.ordinal()] <= 0d || maInt <= 0d || 
			this.atr[instrument.ordinal()] <= 0d) 
		{
			updateMA(instrument, maShr, maInt);
			return;
		}		
		
		boolean isLong;	
		double open, stop, diff, newStop, lockpip;
		
		for (IOrder order : engine.getOrders(instrument)) {
			if (order.getState() == IOrder.State.FILLED) {
				if (order.getProfitLossInAccountCurrency() <= 0d) 
					continue;					// skip if losing (use stop)
				
				isLong = order.isLong();
				open = order.getOpenPrice();
				stop = order.getStopLossPrice(); 
				diff = (stop - open);
				diff *= isLong ? 1 : -1;
				diff = roundPip(diff);
				lockpip = roundPip(instrument.getPipValue() * LOCKPIP);
				
// ********* BREAKEVEN *********************************************************		
				if (isLong && diff < lockpip && 
						askBar.getLow() > (open + Math.abs(diff)))
				{					
					if (beRatio > 0d) {
						order.close(roundLot(order.getAmount() * beRatio));	// close a portion
						print(order.getLabel() + ": Closed " + beRatio * 100 + "%");
					}

					newStop = open + (instrument.getPipValue() * LOCKPIP);
					newStop = roundPip(newStop);
					order.setStopLossPrice(newStop);
					order.waitForUpdate(500);
					newStop = order.getStopLossPrice();
					print(order.getLabel() + " Long Entry: " + open + ", New stop: " + newStop);
				}
				else if (!isLong && -diff < lockpip && 
						bidBar.getHigh() < (open - Math.abs(diff)))
				{					
					if (beRatio > 0d) {
						order.close(roundLot(order.getAmount() * beRatio));						
						print(order.getLabel() + ": Closed " + beRatio * 100 + "%");
					}
					
					newStop = open - (instrument.getPipValue() * LOCKPIP);
					newStop = roundPip(newStop);
					order.setStopLossPrice(newStop);
					order.waitForUpdate(500);
					newStop = order.getStopLossPrice();
					print(order.getLabel() + " Short Entry: " + open + ", New stop: " + newStop);
				}
			}
		}
		
		// *****   ENTRY SETUP   *******************************************************		
		
		if (isTradingAllowed())
		{
			double stopLoss, lot, takeProfit;
			
			chanTop = indicators.ma(instrument, period,
					OfferSide.ASK, IIndicators.AppliedPrice.HIGH, 
					lenInt, maType, indFilter, 1, bidBar.getTime(), 0)[0];
				
			chanBot = indicators.ma(instrument, period,
				OfferSide.BID, IIndicators.AppliedPrice.LOW, 
				lenInt, maType, indFilter, 1, bidBar.getTime(), 0)[0];	
			
			IBar prevBidBar = history.getBar(instrument, period, OfferSide.BID, 2);
			IBar prevAskBar = history.getBar(instrument, period, OfferSide.ASK, 2);		
			if (prevBidBar == null || prevAskBar == null)	return;		//not formed yet
			
			if (this.sentiment[instrument.ordinal()] == null)
				setSentiment(instrument);		// initialize
			
			// check setup
			if (this.sentiment[instrument.ordinal()] == Sentiment.BULL && 
				bidBar.getLow() > chanBot && prevBidBar.getLow() < chanBot && 
				bidBar.getLow() > prevBidBar.getLow() && bidBar.getHigh() > prevBidBar.getHigh() &&
				bidBar.getClose() > prevBidBar.getClose())
			{
				// go long
				stopLoss = bidBar.getLow() - (this.atr[instrument.ordinal()] * stopATR);
				stopLoss = roundPip(stopLoss);
				takeProfit = bidBar.getHigh() + (this.atr[instrument.ordinal()] * tpATR);
				takeProfit = roundPip(takeProfit);
				
				String label = getLabel(instrument);
				lot = getLot(instrument, bidBar.getClose());
				
				engine.submitOrder(label, instrument, IEngine.OrderCommand.BUY, 
					lot, 0, SLIPPAGE, stopLoss, takeProfit);						
				print(label + ": Long entry, lot = " + lot);			
			}
			else if (this.sentiment[instrument.ordinal()] == Sentiment.BEAR &&
				askBar.getHigh() < chanTop && prevAskBar.getHigh() > chanTop &&  				
				askBar.getLow() < prevAskBar.getLow() && askBar.getHigh() < prevAskBar.getHigh() &&
				askBar.getClose() < prevAskBar.getClose())
			{
				// go short
				stopLoss = askBar.getHigh() + (this.atr[instrument.ordinal()] * stopATR);
				stopLoss = roundPip(stopLoss);
				takeProfit = bidBar.getHigh() - (this.atr[instrument.ordinal()] * tpATR);
				takeProfit = roundPip(takeProfit);
				
				String label = getLabel(instrument);
				lot = getLot(instrument, askBar.getClose());
				
				engine.submitOrder(label, instrument, IEngine.OrderCommand.SELL, 
					lot, 0, SLIPPAGE, stopLoss, takeProfit);							
				print(label + ": Short entry, lot = " + lot);
			}
		}

	// ************ onBar clean up ************
		updateMA(instrument, maShr, maInt);
    }
	   
// ****************************************************************************

    // ** onTICK: **
    public void onTick(Instrument instrument, ITick tick) throws JFException {
    }
	
// *****  UTILS start  *********************************************************


	private double getLot(Instrument instrument, double price) throws JFException
	{
		double lotSize = 0;
		
		// Rule: fixed expected amount		
		lotSize = getRiskAmt() / (this.atr[instrument.ordinal()] * this.stopATR);
		
		lotSize /= 1e6;					// in millions
		return roundLot(lotSize);
	}

	private double roundLot(double lot)
	{	
		lot = (int)(lot * 1000) / (1000d);		// 1000 units min.
		return lot;
	}

	// keep prev MA instead of calc to improve performance
	private void updateMA(Instrument instrument, double maShr, double maInt) throws JFException
	{
		this.maShr1[instrument.ordinal()] = maShr;
		this.maInt1[instrument.ordinal()] = maInt;
	} 

	private double roundPip(double value) {
	// rounding to nearest half, 0, 0.5, or 1
		int pipsMultiplier = value <= 20 ? 10000 : 100;
		int rounded = (int) (value * pipsMultiplier * 10 + 0.5);
		rounded *= 2;
		rounded = (int) ((rounded) / 10d + 0.5d);
		value = (rounded) / 2d;
		value /= pipsMultiplier;
		return value;
    }

	private String getLabel(Instrument instrument) throws JFException {
		Calendar calendar = Calendar.getInstance(TimeZone.getTimeZone("GMT"));
		int day = calendar.get(Calendar.DAY_OF_MONTH);
		
		String label = instrument.name();
		label = label.substring(0, 3) + label.substring(3, 6);
		label = label + day + "d" + (this.tagCounter++);
		label = label.toLowerCase();
		return label;
   }
	
	public void print(String string) {
		this.console.getOut().println(string);
	}
	
	private void drawTrade(IOrder myOrder) {
		Instrument instrument;
		String label;
		boolean isLong;
		double openPrice, closePrice;
		IChart myChart;
		IChart.Type objType;
		IChartObject myObject;
		Color color;
	
		instrument = myOrder.getInstrument();
		isLong = myOrder.isLong();
		label = myOrder.getLabel();
		
		myChart = this.context.getChart(instrument);
		if (myChart == null)	return;		// no chart
		
		if (myOrder.getState() == IOrder.State.FILLED) 
		{
			objType = IChart.Type.PRICEMARKER;
			try {
				myObject = myChart.draw(label, objType, 
					myOrder.getFillTime(), myOrder.getOpenPrice());				
			} catch (NullPointerException e) {
				print(e.getMessage() + ": chart not available");
				return;
			}
			
			color = isLong ? Color.CYAN : Color.PINK;
		
			myObject.setColor(color);
		}
		else if (myOrder.getState() == IOrder.State.CLOSED) 
		{
			
			try {
				myChart.remove(label);			// remove entry marker
			} catch (NullPointerException e) {
				print(e.getMessage() + ": chart not available");
				return;
			}			
		
			objType = IChart.Type.SHORT_LINE;
			openPrice = myOrder.getOpenPrice();
			closePrice = myOrder.getClosePrice();
			try {
				myObject = myChart.draw(label, objType, 
					myOrder.getFillTime(), openPrice,
					myOrder.getCloseTime(), closePrice);			
			} catch (NullPointerException e) {
				print(e.getMessage() + ": chart not available");
				return;
			}
			
			if (isLong) {
				color = openPrice < closePrice ? Color.BLUE : Color.RED;
			}
			else {
				color = openPrice > closePrice ? Color.BLUE : Color.RED;
			}
			
			myObject.setColor(color);
			//myObject.setAttrInt(IChartObject.ATTR_INT.WIDTH, 1);
		}
	}
	
	private double getRiskAmt()
	{
		double amount;
		double x = account.getEquity();
		// regression
		amount = 4.666666666667e+04 - 1.666666666667e-01 * x;

		if (amount < 2000d)	amount = 2000d;
		print("Setting risk to $" + (int)amount);
		return amount;
	}
	
	private void setSentiment(Instrument instrument) throws JFException
	{
		double chanTop, chanBot, ult;
		
		IBar bidBar = this.history.getBar(instrument, PERIODINT, OfferSide.BID, 1);
		IBar askBar = this.history.getBar(instrument, PERIODINT, OfferSide.ASK, 1);
		
		chanTop = this.indicators.ma(instrument, PERIODINT,
			OfferSide.ASK, IIndicators.AppliedPrice.HIGH, 
			lenInt, maType, indFilter, 1, askBar.getTime(), 0)[0];
	
		chanBot = this.indicators.ma(instrument, PERIODINT,
			OfferSide.BID, IIndicators.AppliedPrice.LOW, 
			lenInt, maType, indFilter, 1, bidBar.getTime(), 0)[0];
				
		ult = this.indicators.ultOsc(instrument, PERIODINT,
			OfferSide.BID, 5, 13, 34, indFilter, 1, bidBar.getTime(), 0)[0];
	
		if (bidBar.getLow() > chanTop && ult < this.momoOB) {
			if (this.sentiment[instrument.ordinal()] != Sentiment.BULL)
				print(instrument.toString() + " Bullish");
			this.sentiment[instrument.ordinal()] = Sentiment.BULL;

		}
		else if (askBar.getHigh() < chanBot && ult > (100 - this.momoOB)) {
			if (this.sentiment[instrument.ordinal()] != Sentiment.BEAR)
				print(instrument.toString() + " Bearish");
			this.sentiment[instrument.ordinal()] = Sentiment.BEAR;

		}
		else {
			if (this.sentiment[instrument.ordinal()] != Sentiment.NEUTRAL)
				print(instrument.toString() + " Neutral");
			this.sentiment[instrument.ordinal()] = Sentiment.NEUTRAL;
		}
	}
	
	// Rule: max. one position only
	private boolean isTradingAllowed() {
		return (this.account.getUseOfLeverage() == 0d);
	}

// *****  MISC  ****************************************************************

	public enum Sentiment {
		NEUTRAL, BEAR, BULL 
	}


// *****  API  ****************************************************************
	public void onStart(IContext context) throws JFException {
		this.engine = context.getEngine();
		this.indicators = context.getIndicators();
		this.history = context.getHistory();
		this.console = context.getConsole();	// allows printing to console
		this.context = context;
		
		Set<Instrument> instSet = new HashSet<Instrument>();
		instSet.add(Instrument.EURUSD);
		context.setSubscribedInstruments(instSet);		
		
		this.console.getOut().print("--- Started "+quantisan2.class+" v." + version + " ---");
		this.console.getOut().print(Instrument.toStringSet(instSet));
		print(" ---");
		
		for (IOrder order : this.engine.getOrders()) 
		{
			 drawTrade(order);
		}

	}
	
	// Represents message sent from server to client application 
	public void onMessage(IMessage message) throws JFException {
		IMessage.Type msgType = message.getType();
		IOrder order = message.getOrder();
		
		// Draw trades on the chart
		if (msgType == IMessage.Type.ORDER_FILL_OK ||
			msgType == IMessage.Type.ORDER_CLOSE_OK) 
		{
			drawTrade(order);
		}	
    }
	
   public void onAccount(IAccount account) throws JFException {
		this.account = account;		
	}
	
	public void onStop() throws JFException {		
		print("---Stopped---");
   }
}
// ****************************************************************************

