/*
	quantisan2 - JForex contest strategy
	version 2.24
	December 2010
	Copyright 2010 Paul at Quantisan.com
	
	Changelog:
	v2.24	- Dec 2010
	v2.23	- implement javadoc
			- fix getLot()
			- changed regression
			- removed drawTrade()
			- add trailing step
			- removed breakeven algo
			- changed parameters			
	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.*;

public class quantisan2 implements IStrategy {
	private final double version = 2.24;
	
	private IEngine engine;
	private IIndicators indicators;
	private IHistory history;
	private int tagCounter;
	private IConsole console;
	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 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;
	private double trailRatio = 0.5;
	// indicators
	private int momoOB = 55;
	private final int lenShr = 21;	
	private final int lenInt = 34;

    // ** 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;
		}		
		
		// *****   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
			
			IOrder order;
			double trailStep = 
				(this.atr[instrument.ordinal()] * stopATR) * this.trailRatio *
						Math.pow(10, instrument.getPipScale());
			
			// 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);
				
				order = engine.submitOrder(label, instrument, IEngine.OrderCommand.BUY, 
					lot, 0, SLIPPAGE, stopLoss, takeProfit);						
				print(label + ": Long entry, lot = " + lot);
				order.waitForUpdate(800);
				order.setStopLossPrice(stopLoss, OfferSide.BID, trailStep);
			}
			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);
				
				order = engine.submitOrder(label, instrument, IEngine.OrderCommand.SELL, 
					lot, 0, SLIPPAGE, stopLoss, takeProfit);							
				print(label + ": Short entry, lot = " + lot);
				order.waitForUpdate(800);
				order.setStopLossPrice(stopLoss, OfferSide.BID, trailStep);
			}
		}

	// ************ onBar clean up ************
		updateMA(instrument, maShr, maInt);
    }
	   
// ****************************************************************************

    // ** onTICK: **
    public void onTick(Instrument instrument, ITick tick) throws JFException {
    }
	
// *****  UTILS start  *********************************************************


    /**
     * @param instrument instrument to trade
     * @return a lot size
     */
	private double getLot(Instrument instrument) throws JFException
	{
		double lotSize = 0;
		
		// Rule: fixed expected amount		
		lotSize = getRiskAmt() / (this.atr[instrument.ordinal()] * this.stopATR);

		lotSize /= 1e6;					// in millions
		return roundLot(lotSize);
	}

	/**
	 * @param lot
	 * @return rounded lot to min. 1000 units
	 */
	private double roundLot(double lot)
	{	
		lot = (int)(lot * 1000) / (1000d);		// 1000 units min.
		return lot;
	}

	/**
	 * keep prev MA instead of calculating
	 * @param instrument
	 * @param maShr
	 * @param maInt
	 * @throws JFException
	 */
	private void updateMA(Instrument instrument, double maShr, double maInt) throws JFException
	{
		this.maShr1[instrument.ordinal()] = maShr;
		this.maInt1[instrument.ordinal()] = maInt;
	} 

	/**
	 * 
	 * @param value pip value
	 * @return rounded pip value
	 */
	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;
    }

	/**
	 * @param instrument
	 * @return a label
	 * @throws JFException
	 */
	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;
   }
	
	/**
	 * Print string to console
	 * @param string
	 */
	public void print(String string) {
		this.console.getOut().println(string);
	}
	
	/**
	 * 
	 * @return risk amount in USD
	 */
	private double getRiskAmt()
	{
		double amount;
		double x = account.getEquity();
		// regression
		amount = 3.033333333333e+04 - 8.333333333333e-02 * x;
		if (amount < 2000d)	amount = 2000d;
		print("Setting risk to $" + (int)amount);
		return amount;
	}
	
	/**
	 * Identify sentiment based on price channel and overbought/sold condition
	 * @param instrument
	 * @throws JFException
	 */
	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;
		}
	}
	
	/**
	 * Max. 1 position
	 * @return is trading allowed?
	 */
	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
		
		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(" ---");
	}
	
	// Message sent from server to client application 
	public void onMessage(IMessage message) throws JFException {
    }
	
   public void onAccount(IAccount account) throws JFException {
		this.account = account;		
	}
	
	public void onStop() throws JFException {		
		print("---Stopped---");
   }
}
// ****************************************************************************
