/*
 * Copyright (c) 2008-2009 OrangeSignal.com All rights reserved.
 * 
 * これは Apache ライセンス Version 2.0 (以下、このライセンスと記述) に
 * 従っています。このライセンスに準拠する場合以外、このファイルを使用
 * してはなりません。このライセンスのコピーは以下から入手できます。
 * 
 * http://www.apache.org/licenses/LICENSE-2.0.txt
 * 
 * 適用可能な法律がある、あるいは文書によって明記されている場合を除き、
 * このライセンスの下で配布されているソフトウェアは、明示的であるか暗黙の
 * うちであるかを問わず、「保証やあらゆる種類の条件を含んでおらず」、
 * 「あるがまま」の状態で提供されるものとします。
 * このライセンスが適用される特定の許諾と制限については、このライセンス
 * を参照してください。
 */

package jp.sf.orangesignal.trading;

import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedList;
import java.util.Map;

import jp.sf.orangesignal.ta.candle.Candlestick;
import jp.sf.orangesignal.ta.util.ArrayUtils;
import jp.sf.orangesignal.ta.util.Assert;
import jp.sf.orangesignal.trading.commission.Commission;
import jp.sf.orangesignal.trading.commission.FreeCommission;
import jp.sf.orangesignal.trading.data.Dataset;
import jp.sf.orangesignal.trading.order.LimitOrder;
import jp.sf.orangesignal.trading.order.MarketOrder;
import jp.sf.orangesignal.trading.order.Order;
import jp.sf.orangesignal.trading.order.StopOrder;

/**
 * 仮想売買の管理クラスを提供します。
 * 
 * @author 杉澤 浩二
 */
public class VirtualTrader implements Trader {

	/**
	 * デフォルトコンストラクタです。
	 */
	public VirtualTrader() {}

	/**
	 * <p>初期投資資金を指定して、このクラスを構築するコンストラクタです。</p>
	 * <p>デフォルトのデータセットは空のシンボル名で設定されます。</p>
	 * 
	 * @param dataset デフォルトのデータセット
	 * @param initCash 初期資金
	 * @throws IllegalArgumentException データセットに <code>null</code> が指定された場合
	 */
	public VirtualTrader(final Dataset dataset, final double initCash) {
		this(dataset, new VirtualAccount(initCash));
	}

	/**
	 * <p>手数料を無料として、このクラスを構築するコンストラクタです。</p>
	 * <p>デフォルトのデータセットは空のシンボル名で設定されます。</p>
	 * 
	 * @param dataset デフォルトのデータセット
	 * @param account 口座情報
	 * @throws IllegalArgumentException パラメーターに <code>null</code> が指定された場合
	 */
	public VirtualTrader(final Dataset dataset, final VirtualAccount account) {
		this(dataset, account, new FreeCommission());
	}

	/**
	 * <p>コンストラクタです。</p>
	 * <p>デフォルトのデータセットは空のシンボル名で設定されます。</p>
	 * 
	 * @param dataset デフォルトのデータセット
	 * @param account 口座情報
	 * @param commission 手数料情報
	 * @throws IllegalArgumentException パラメーターに <code>null</code> が指定された場合
	 */
	public VirtualTrader(final Dataset dataset, final VirtualAccount account, final Commission commission) {
		Assert.notNull(account, "Account must not be null");
		Assert.notNull(commission, "Commission must not be null");
		setDataset(null, dataset);
		setAccount(account);
		this.commission = commission;
	}

	/**
	 * 資金やポジションの状態をリセットします。
	 */
	public void reset() {
		if (account != null)
			account.setCash(initialCapital);
		currentPositions = new LinkedList<Position>();
		positions = new LinkedList<Position>();
		seq = 1;
	}

	// ----------------------------------------------------------------------

	/**
	 * 手数料情報を保持します。
	 */
	private Commission commission;

	@Override public Commission getCommission() { return commission; }

	/**
	 * 手数料情報を設定します。
	 * 
	 * @param commission 手数料情報
	 */
	public void setCommission(final Commission commission) { this.commission = commission; }

	/**
	 * スリッページを保持します。
	 */
	private double slippage;

	/**
	 * スリッページを返します。
	 * 
	 * @return スリッページ
	 */
	public double getSlippage() { return slippage; }

	/**
	 * スリッページを設定します。
	 * 
	 * @param slippage スリッページ
	 */
	public void setSlippage(final double slippage) { this.slippage = slippage; }

	/**
	 * 口座情報を保持します。
	 */
	private VirtualAccount account;

	@Override public VirtualAccount getAccount() { return account; }

	/**
	 * 口座情報を設定します。
	 * 
	 * @param account 口座情報
	 */
	public void setAccount(final VirtualAccount account) {
		this.initialCapital = account.getCash();
		this.account = account;
	}

	/**
	 * 初期資金を保持します。
	 */
	private double initialCapital;

	/**
	 * 初期資金を返します。
	 * 
	 * @return 初期資金
	 */
	public double getInitialCapital() { return initialCapital; }

	/**
	 * 買い増し及び売り増しの限度回数を保持します。
	 */
	private int positionLimit = 0;

	/**
	 * 買い増し及び売り増しの限度回数を返します。
	 * 初期値は <code>0</code> (買い増し及び売り増し不可能) です。
	 * 
	 * @return 買い増しおよび売り増しの限度回数
	 */
	public int getPositionLimit() { return positionLimit; }

	/**
	 * 買い増し及び売り増しの限度回数を設定します。
	 * 買い増し及び売り増しを可能にするには、正数を指定します。
	 * 
	 * @param positionLimit 買い増し及び売り増しの限度回数
	 */
	public void setPositionLimit(final int positionLimit) { this.positionLimit = positionLimit; }

	/**
	 * 既定の数量を保持します。
	 */
	private int defaultQuantity = 1;
	@Override public int getDefaultQuantity() { return defaultQuantity; }
	@Override public void setDefaultQuantity(final int defaultQuantity) { this.defaultQuantity = defaultQuantity; }

	// ----------------------------------------------------------------------

	/**
	 * トレードの種類を保持します。
	 */
	private TradeType tradeType = TradeType.LONG_AND_SHORT_AND_REVERSE;

	/**
	 * <p>トレードの種類を返します。</p>
	 * <p>デフォルトは {@link TradeType#LONG_AND_SHORT_AND_REVERSE} です。</p>
	 * 
	 * @return トレードの種類
	 */
	public TradeType getTradeType() { return tradeType; }

	/**
	 * トレードの種類を設定します。
	 * 
	 * @param tradeType トレードの種類
	 */
	public void setTradeType(final TradeType tradeType) { this.tradeType = tradeType; }

	// ----------------------------------------------------------------------
	// データセット

	/**
	 * シンボルをキーとしたデータセットのマップを保持します。
	 */
	private Map<String, Dataset> datasetMap = new HashMap<String, Dataset>();

	/**
	 * シンボルをキーとしたデータセットのマップを設定します。
	 * 指定されるデータセットはローソク足情報を返す必要があります。
	 * このメソッドを実行すると {@link #reset()} も呼出されます。
	 * 
	 * @param datasetMap シンボルをキーとしたデータセットのマップ
	 */
	public void setDatasetMap(final Map<String, Dataset> datasetMap) {
		this.datasetMap = datasetMap;
		reset();
	}

	/**
	 * 指定されたシンボルと一致するデータセットを返します。
	 * 一致するデータセットが見つからない場合は <code>null</code> を返します。
	 * 
	 * @param symbol シンボル
	 * @return データセット。又は <code>null</code>
	 */
	private Dataset getDataset(final String symbol) { return datasetMap.get(symbol); }

	/**
	 * データセットを設定します。
	 * 
	 * @param dataset データセット
	 * @throws IllegalArgumentException データセットに <code>null</code> を指定した場合
	 */
	public void setDataset(final String symbol, final Dataset dataset) {
		Assert.notNull(dataset, "Dataset must not be null");
		datasetMap = new HashMap<String, Dataset>(2);
		datasetMap.put(symbol == null ? "" : symbol, dataset);
	}

	// ----------------------------------------------------------------------
	// ポートフォリオ関連

	@Override
	public MarketPositionType getMarketPositionType(final String symbol) {
		final LinkedList<Position> positions = getPositionListBySymbol(currentPositions, symbol);
		boolean l = false, s = false;
		for (final Position p : positions) {
			l = l || p.getType() == PositionType.LONG;
			s = s || p.getType() == PositionType.SHORT;
		}
		return MarketPositionType.valueOf(l, s);
	}

	/**
	 * 未決済ポジションのリストを保持します。
	 */
	private LinkedList<Position> currentPositions = new LinkedList<Position>();

	@Override
	public Position getCurrentPosition(final String symbol) {
		final LinkedList<Position> positions = getPositionListBySymbol(currentPositions, symbol);
		return positions.isEmpty() ? null : positions.getLast();
	}

	@Override
	public LinkedList<Position> getCurrentPositions(final String symbol) {
		return getPositionListBySymbol(currentPositions, symbol);
	}

	/**
	 * 決済済みポジションのリストを保持します。
	 */
	private LinkedList<Position> positions = new LinkedList<Position>();
	@Override public LinkedList<Position> getPositions() { return positions; }
	@Override public LinkedList<Position> getPositionsBySymbol(final String symbol) { return getPositionListBySymbol(positions, symbol); }
	@Override public LinkedList<Position> getPositionsByEntryLabel(final String label) { return getPositionListByEntryLabel(positions, label); }
	@Override public LinkedList<Position> getPositionsByExitLabel(final String label) { return getPositionListByExitLabel(positions, label); }
	@Override public LinkedList<Position> getPositionsByLabel(final String label) { return getPositionListByLabel(positions, label); }

	/**
	 * 指定されたポジション情報のコレクションから、
	 * 指定されたシンボルと一致するポジション情報をリストにして返します。
	 * 
	 * @param positions ポジション情報
	 * @param symbol シンボル
	 * @return 指定されたシンボルと一致するポジション情報のリスト
	 */
	private LinkedList<Position> getPositionListBySymbol(final Collection<Position> positions, final String symbol) {
		final LinkedList<Position> results = new LinkedList<Position>();
		final String s = symbol == null ? "" : symbol;
		for (final Position p : positions) {
			if (s.equals(p.getSymbol()) /*|| s.equals(p.getExitLabel())*/)
				results.add(p);
		}
		return results;
	}

	private LinkedList<Position> getPositionListByEntryLabel(final Collection<Position> positions, final String label) {
		final LinkedList<Position> results = new LinkedList<Position>();
		final String s = label == null ? "" : label;
		for (final Position p : positions) {
			if (s.equals(p.getEntryLabel()))
				results.add(p);
		}
		return results;
	}

	private LinkedList<Position> getPositionListByExitLabel(final Collection<Position> positions, final String label) {
		final LinkedList<Position> results = new LinkedList<Position>();
		final String s = label == null ? "" : label;
		for (final Position p : positions) {
			if (s.equals(p.getExitLabel()))
				results.add(p);
		}
		return results;
	}

	private LinkedList<Position> getPositionListByLabel(final Collection<Position> positions, final String label) {
		final LinkedList<Position> results = new LinkedList<Position>();
		final String s = label == null ? "" : label;
		for (final Position p : positions) {
			if (s.equals(p.getEntryLabel()) || s.equals(p.getExitLabel()))
				results.add(p);
		}
		return results;
	}

	// ----------------------------------------------------------------------

	/**
	 * 内部的な注文情報を提供します。
	 */
	private class InternalOrder {

		/**
		 * シンボルを保持します。
		 */
		protected String symbol;

		/**
		 * ラベルを保持します。
		 */
		protected String label;

		/**
		 * 約定日時を保持します。
		 */
		protected Date date;

		/**
		 * 約定価格を保持します。
		 */
		protected double price;

		/**
		 * 注文数量を保持します。
		 */
		protected int quantity;

		/**
		 * スリッページを保持します。
		 */
		protected double slippage;

	}

	@Override
	public void buy(final Order order) {
		final InternalOrder o = contract(order, PositionType.LONG);
		if (o != null) {
			buy(o);
		}
	}

	@Override
	public void sellShort(final Order order) {
		final InternalOrder o = contract(order, PositionType.SHORT);
		if (o != null) {
			sell(o);
		}
	}

	@Override
	public void sell(final Order order) {
		final InternalOrder o = contract(order, PositionType.SHORT);
		if (o != null) {
			exitLong(o, order.getFindId(), order.getFindLabel());
		}
	}

	@Override
	public void buyToCover(final Order order) {
		final InternalOrder o = contract(order, PositionType.LONG);
		if (o != null) {
			exitShort(o, order.getFindId(), order.getFindLabel());
		}
	}

	// ----------------------------------------------------------------------
	// 約定

	/**
	 * 指定された注文情報から約定日時のローソク足情報を返します。
	 * 
	 * @param order 注文情報
	 * @return 約定日時のローソク足情報。又は <code>null</code>
	 */
	private Candlestick getTargetCandlestick(final Order order) {
		final Dataset dataset = getDataset(order.getSymbol());
		if (dataset == null)
			return null;
		int i = dataset.indexOf(order.getDate());
		if (i == ArrayUtils.INDEX_NOT_FOUND)
			return null;
		i = i + order.getPeriod();
		final Candlestick[] c = dataset.getCandlestick();
		if (c.length <= i)
			return null;
		return c[i];
	}

	/**
	 * 指定された注文情報から、約定情報を生成して返します。
	 * 約定できなかった場合は <code>null</code> を返します。
	 * 
	 * @param order 注文情報
	 * @param type ポジションの種類(指値及び逆指値注文時)
	 * @return 約定情報。又は <code>null</code>
	 */
	private InternalOrder contract(final Order order, final PositionType type) {
		final Candlestick c = getTargetCandlestick(order);
		if (c == null)
			return null;

		final InternalOrder result = new InternalOrder();
		result.symbol = order.getSymbol();
		result.label = order.getLabel();
		result.date = c.getDate();
		result.quantity = order.getQuantity();
		result.slippage = slippage;

		// 成行注文
		if (order instanceof MarketOrder) {
			final MarketOrder o = (MarketOrder) order;

			// 基本約定価格取得
			switch (o.getPriceType()) {
				case OPEN:
					result.price = c.getOpen();
					break;
				case HIGH:
					result.price = c.getHigh();
					break;
				case LOW:
					result.price = c.getLow();
					break;
				case CLOSE:
					result.price = c.getClose();
					break;
				default:
					return null;
			}
/*
			// スリッページ計算
			switch (order.getPositionType()) {
				case LONG:
					result.price = Math.min(result.price + slippage, c.getHigh());
					break;
				case SHORT:
					result.price = Math.max(result.price - slippage, c.getLow());
					break;
				default:
					return null;
			}
			result.slippage = slippage;
*/
		// 指値注文
		} else if (order instanceof LimitOrder) {
			final LimitOrder o = (LimitOrder) order;

			// 基本約定価格取得
			final double limit = o.getLimitPrice();
			if (c.contains(limit)) {
				result.price = limit;
			// 買いの場合にギャップダウンしていたら高値で約定とします。
			} else if (type == PositionType.LONG && c.getHigh() < limit) {
				result.price = c.getHigh();
			// 売りの場合にギャップアップしていたら安値で約定とします。
			} else if (type == PositionType.SHORT && c.getLow() > limit) {
				result.price = c.getLow();
			} else {
				return null;
			}
/*
			// スリッページ計算
			switch (order.getPositionType()) {
				case LONG:
					result.price  = result.price - slippage;
					break;
				case SHORT:
					result.price  = result.price + slippage;
					break;
				default:
					return null;
			}
			result.price = Math.max(Math.min(result.price, c.getHigh()), c.getLow());
			result.slippage = Math.abs(result.price - limit);
*/
		// 逆指値注文
		} else if (order instanceof StopOrder) {
			final StopOrder o = (StopOrder) order;

			// 基本約定価格取得
			final double stop = o.getStopPrice();
			if (c.contains(o.getStopPrice())) {
				result.price = o.getStopPrice();
			} else if (type == PositionType.SHORT && c.getHigh() < stop) {
				// 売りの場合にギャップダウンしていたら高値で約定とします。(安値つかみ)
				result.price = c.getHigh();
			} else if (type == PositionType.LONG && c.getLow() > stop) {
				// 買いの場合にギャップアップしていたら安値で約定とします。(高値つかみ)
				result.price = c.getLow();
			} else {
				return null;
			}
/*
			// スリッページ計算
			switch (order.getPositionType()) {
				case LONG:
					result.price  = result.price + slippage;
					break;
				case SHORT:
					result.price  = result.price - slippage;
					break;
				default:
					return null;
			}
			result.price = Math.max(Math.min(result.price, c.getHigh()), c.getLow());
			result.slippage = Math.abs(result.price - stop);
*/
		} else {
			return null;
		}

		return result;
	}

	// ----------------------------------------------------------------------
	// ポジション管理

	/**
	 * ポジションのエントリー/イグジット時の一意なシーケンシャルを保持します。
	 */
	private int seq = 1;

	/**
	 * 買いポジションをエントリーします。
	 * 買い増しが可能な場合は、買い増しエントリーします。
	 * 売りポジションがエントリー中の場合は、途転(ドテン)して新規に買いポジションをエントリーします。
	 * このメソッドは買い増しが出来ない場合や、残高の引出しが出来ない場合は何も行いません。
	 * 
	 * @param o 
	 */
	private void buy(final InternalOrder o) {
		// 途転(ドテン)可能かどうかを取得します。
		final MarketPositionType mp = getMarketPositionType(o.symbol);
		final boolean reverse = mp.isShort() && tradeType.isReverse();

		// 売りポジションを持っている場合は、途転(ドテン)させる為に全て決済します。
		if (mp.isShort())
			exitPosition(o, true, null, null);

		// 買いポジションを持っている場合は買い増ししない。
		if ((tradeType.isLong() && !mp.isShort()) || reverse) {
			if (!mp.isLong() || (positionLimit > 0 && (positionLimit + 1) > currentPositions.size())) {
				final int qty = o.quantity > 0 ? o.quantity : defaultQuantity;
				final double commission = this.commission.calcCommission(o.price, qty);
				if (account.withdraw(o.price * qty /*+ (commission + slippage)*/)) {
					currentPositions.add(new DefaultPosition(seq++, o.symbol, PositionType.LONG, o.label, o.date, o.price, qty, commission, slippage));
				}
			}
		}
	}

	/**
	 * 売りポジションをエントリーします。(空売り)
	 * 売り増しが可能な場合は、売り増しエントリーします。
	 * 買いポジションがエントリー中の場合は、途転(ドテン)して新規に売りポジションをエントリーします。
	 * このメソッドは売り増しが出来ない場合や、残高の引出しが出来ない場合は何も行いません。
	 * 
	 * @param o 
	 */
	private void sell(final InternalOrder o) {
		// 途転(ドテン)可能かどうかを取得します。
		final MarketPositionType mp = getMarketPositionType(o.symbol);
		final boolean reverse = mp.isLong() && tradeType.isReverse();

		// 買いポジションを持っている場合は、途転(ドテン)させる為に全て決済します。
		if (mp.isLong())
			exitPosition(o, true, null, null);

		// 売りポジションを持っている場合は売り増ししない。
		if ((tradeType.isShort() && !mp.isLong()) || reverse) {
			if (!mp.isShort() || (positionLimit > 0 && (positionLimit + 1) > currentPositions.size())) {
				final int qty = o.quantity > 0 ? o.quantity : defaultQuantity;
				final double commission = this.commission.calcCommission(o.price, qty);
				if (account.withdraw(o.price * qty /*+ (commission + slippage)*/)) {
					currentPositions.add(new DefaultPosition(seq++, o.symbol, PositionType.SHORT, o.label, o.date, o.price, qty, commission, slippage));
				}
			}
		}
	}

	/**
	 * 指定されたポジション名でエントリー中の買いポジションがある場合は決済します。
	 * ラベルが <code>null</code> 又はブランクの場合は、全てのエントリー中の買いポジションが対象になります。
	 * 数量に <code>0</code> 以下の値が指定された場合は、エントリー時の数量と同じ数量で決済します。
	 * エントリー時の数量より大きい数量が指定された場合は、残りの数量は無視されます。
	 * このメソッドはエントリー中の買いポジションがない場合は何も行いません。
	 * 
	 * @param o 
	 * @param findId
	 * @param findLabel
	 */
	private void exitLong(final InternalOrder o, final Integer findId, final String findLabel) {
		// 買いポジションを持っている場合は決済します。
		if (getMarketPositionType(o.symbol).isLong())
			exitPosition(o, false, findId, findLabel);
	}

	/**
	 * 指定された売買名でエントリー中の売りポジションがある場合は決済します。(買い戻し)
	 * ラベルが <code>null</code> 又はブランクの場合は、全てのエントリー中の売りポジションが対象になります。
	 * 数量に <code>0</code> 以下の値が指定された場合は、エントリー時の数量と同じ数量で決済します。
	 * エントリー時の数量より大きい数量が指定された場合は、残りの数量は無視されます。
	 * このメソッドはエントリー中の売りポジションがない場合は何も行いません。
	 * 
	 * @param o 
	 * @param findId
	 * @param findLabel
	 */
	private void exitShort(final InternalOrder o, final Integer findId, final String findLabel) {
		// 売りポジションを持っている場合は決済します。
		if (getMarketPositionType(o.symbol).isShort())
			exitPosition(o, false, findId, findLabel);
	}

	/**
	 * 指定された売買名でエントリー中のポジションがある場合は決済します。
	 * ラベルが <code>null</code> 又はブランクの場合は、全てのエントリー中のポジションが対象になります。
	 * 数量に <code>0</code> 以下の値が指定された場合は、エントリー時の数量と同じ数量で決済します。
	 * エントリー時の数量より大きい数量が指定された場合は、残りの数量は無視されます。
	 * 
	 * @param o 
	 * @param all 全て決済するかどうか
	 * @param findId ID
	 * @param findLabel
	 */
	private void exitPosition(final InternalOrder o, final boolean all, final Integer findId, final String findLabel) {
		// 決済数量の残り
		int count = o.quantity;

		int i = 0;
		while (i < currentPositions.size()) {
			// 決済数量の残りが 0 の場合は処理を終了します。
			if (o.quantity > 0 && count <= 0)
				break;

			final Position p = currentPositions.get(i);
			boolean match = false;
			if (o.symbol.equals(p.getSymbol())) {
				// 途転(ドテン)の場合、IDが一致する場合(途転(ドテン)を除く)、
				// ラベルが指定されていない場合、又は指定されたラベルと一致する場合
				if (all) {
					match = true;
				} else if (findId != null) {
					if (findId.intValue() == p.getId())
						match = true;
				} else if (findLabel == null || findLabel.isEmpty() || findLabel.equals(p.getEntryLabel())) {
					match = true;
				}
			}

			if (match) {
				final int entry = p.getEntryQuantity();
				final int qty;
				// 決済数量が指定されている場合は残りを計算する(ドテン決済時は全て決済するので残数を無視する)
				if (o.quantity > 0 && !all) {
					qty = Math.min(count, entry);
					count = count - qty;
				} else {
					qty = entry;
				}

				final double commission = this.commission.calcCommission(o.price, qty);
				//final Date[] d = getDataset(o.symbol).getDate();
				final int period = getDataset(o.symbol).getPeriod(p.getEntryDate(), o.date) - 1;//ArrayDataUtils.lastIndexOf(d, o.date) - ArrayDataUtils.indexOf(d, p.getEntryDate());
				final Position split = p.close(seq++, o.label, o.date, o.price, qty, commission, slippage, period);
				currentPositions.remove(i);
				positions.add(p);
				account.deposit(o.price * qty /*- (commission + slippage)*/);

				if (split != null) {
					currentPositions.add(i, split);
					i++;
				}
				continue;
			}
			i++;
		}
	}

}
