/*
 * Copyright (c) 2006-2009 OrangeSignal.com All rights reserved.
 */

package jp.sourceforge.orangesignal.trading.backtest;

import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;

import jp.sourceforge.orangesignal.ta.dataset.StandardDataset;
import jp.sourceforge.orangesignal.ta.dataset.loader.DatasetLoader;
import jp.sourceforge.orangesignal.trading.VirtualTrader;
import jp.sourceforge.orangesignal.trading.order.OrderTiming;
import jp.sourceforge.orangesignal.trading.stats.Stats;
import jp.sourceforge.orangesignal.trading.stats.Summary;
import jp.sourceforge.orangesignal.trading.strategy.TradingStrategy;
import jp.sourceforge.orangesignal.trading.strategy.TradingStrategyContext;
import jp.sourceforge.orangesignal.trading.strategy.TradingStrategyContextAware;

/**
 * <p>バックテスト実行クラスを提供します。</p>
 * 
 * @author 杉澤 浩二
 */
public class Backtester {

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

	// ----------------------------------------------------------------------
	// フィールド(メンバー)変数

	/**
	 * データセットローダーを保持します。
	 */
	private DatasetLoader datasetLoader;

	/**
	 * データセットローダーを返します。
	 * 
	 * @return データセットローダー
	 */
	public DatasetLoader getDatasetLoader() { return datasetLoader; }

	/**
	 * データセットローダーを設定します。
	 * 
	 * @param datasetLoader データセットローダー
	 */
	public void setDatasetLoader(final DatasetLoader datasetLoader) { this.datasetLoader = datasetLoader; }

	/**
	 * 検証開始日時を保持します。
	 */
	private Date startDate;

	/**
	 * 検証開始日時を返します。
	 * 
	 * @return 検証開始日時。又は <code>null</code>
	 */
	public Date getStartDate() { return startDate; }

	/**
	 * 検証開始日時を設定します。
	 * 
	 * @param startDate 検証開始日時
	 */
	public void setStartDate(final Date startDate) { this.startDate = startDate; }

	/**
	 * 検証終了日時を保持します。
	 */
	private Date endDate;

	/**
	 * 検証終了日時を返します。
	 * 
	 * @return 検証終了日時。又は <code>null</code>
	 */
	public Date getEndDate() { return endDate; }

	/**
	 * 検証終了日時を設定します。
	 * 
	 * @param endDate 検証終了日時
	 */
	public void setEndDate(Date endDate) { this.endDate = endDate; }

	/**
	 * シンボルとシンボル名のマップを保持します。
	 */
	private Map<String, String> symbols;

	/**
	 * シンボルとシンボル名のマップを返します。
	 * 
	 * @return シンボルとシンボル名のマップ
	 */
	public Map<String, String> getSymbols() { return symbols; }

	/**
	 * シンボルとシンボル名のマップを設定します。
	 * 
	 * @param symbolMap シンボルとシンボル名のマップ
	 */
	public void setSymbols(final Map<String, String> symbolMap) { this.symbols = symbolMap; }

	/**
	 * ストラテジーのリストを保持します。
	 */
	private List<TradingStrategy> strategies;

	/**
	 * ストラテジーのリストを返します。
	 * 
	 * @return ストラテジーのリスト
	 */
	public List<TradingStrategy> getStrategies() { return strategies; }

	/**
	 * ストラテジーのリストを設定します。
	 * 
	 * @param strategies ストラテジーのリスト
	 */
	public void setStrategies(final List<TradingStrategy> strategies) { this.strategies = strategies; }

	/**
	 * 売買管理オブジェクトを保持します。
	 */
	private VirtualTrader trader;

	/**
	 * 売買管理オブジェクトを返します。
	 * 
	 * @return 売買管理オブジェクト
	 */
	public VirtualTrader getTrader() { return trader; }

	/**
	 * 売買管理オブジェクトを設定します。
	 * 
	 * @param trader 売買管理オブジェクト
	 */
	public void setTrader(final VirtualTrader trader) { this.trader = trader; }

	/**
	 * デフォルトの注文方法を保持します。
	 */
	private OrderTiming defaultOrderTiming = OrderTiming.NEXT_OPEN;

	/**
	 * デフォルトの注文方法を返します。
	 * 
	 * @return デフォルトの注文方法
	 */
	public OrderTiming getDefaultOrderTiming() { return defaultOrderTiming; }

	/**
	 * デフォルトの注文方法を設定します。
	 * 
	 * @param defaultOrderTiming デフォルトの注文方法
	 */
	public void setDefaultOrderTiming(final OrderTiming defaultOrderTiming) { this.defaultOrderTiming = defaultOrderTiming; }

	// ----------------------------------------------------------------------
	// リスナー

	/**
	 * {@link BacktesterListener} のリストを保持します。
	 */
	private List<BacktesterListener> listeners = new ArrayList<BacktesterListener>(1);

	/**
	 * {@link BacktesterListener} をリスナーのリストに追加します。
	 * 
	 * @param listener 追加する {@link BacktesterListener}
	 */
	public void addBacktesterListener(final BacktesterListener listener) { listeners.add(listener); }

	/**
	 * {@link BacktesterListener} をリスナーのリストから削除します。
	 * 
	 * @param listener 削除する {@link BacktesterListener}
	 */
	public void removeBacktesterListener(final BacktesterListener listener) { listeners.remove(listener); }

	// ----------------------------------------------------------------------
	// メソッド

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

	/**
	 * バックテストを実行してトレードパフォーマンス情報を返します。
	 * 
	 * @return トレードパフォーマンス情報
	 * @throws BacktestException 
	 */
	public Summary backtest() throws BacktestException {
		// イベント情報用変数群
		final int max = symbols.size();
		int count = 0;
		BacktesterEvent event;

		// データセット毎にストラテジーを実行します。
		final Map<String, Stats> statsMap = new LinkedHashMap<String, Stats>(max + 1, 1);
		for (final Map.Entry<String, String> entry : symbols.entrySet()) {
			final String symbol = entry.getKey();

			// データセットマップを構築します。
			datasetLoader.setSymbol(symbol);
			final StandardDataset dataset = new StandardDataset(datasetLoader.load());
			dataset.setSymbolName(entry.getValue());
			final Map<String, StandardDataset> datasetMap = new HashMap<String, StandardDataset>(2, 1);
			datasetMap.put(symbol, dataset);
			// 売買管理オブジェクトへデータセットマップを設定します。
			trader.setDatasetMap(datasetMap);

			count++;
			event = new BacktesterEvent(this, max, count, symbol, dataset.getSymbolName(), null);
			for (final BacktesterListener listener : listeners)
				listener.backtestStart(event);

			trader.reset();

			if (strategies != null) {
				final int start = dataset.defaultIndexOf(startDate);
				final int end = dataset.defaultLastIndexOf(endDate);
				final TradingStrategyContext context = new TradingStrategyContextImpl();

				// prepare
				for (final TradingStrategy strategy : strategies) {
					strategy.setTrader(trader);
					strategy.setDefaultOrderTiming(defaultOrderTiming);
					strategy.setSymbol(symbol);
					strategy.setDataset(dataset);
					strategy.setStartDataIndex(start);
					strategy.setEndDataIndex(end);
					if (strategy instanceof TradingStrategyContextAware)
						((TradingStrategyContextAware) strategy).setTradingStrategyContext(context);
					strategy.prepare();
				}

				// execute
				final Date[] date = dataset.getDate();
				for (int i = start; i <= end; i++) {
					for (final TradingStrategy strategy : strategies) {
						strategy.setCurrentDataIndex(i);
						strategy.setDate(date[i]);
						try {
							if (strategy.execute())
								break;
						} catch (Exception e) {
							throw new BacktestException(e.getMessage(), e);
						}
					}
				}

				// close
				for (final TradingStrategy strategy : strategies) {
					strategy.close();
					if (strategy instanceof TradingStrategyContextAware)
						((TradingStrategyContextAware) strategy).setTradingStrategyContext(null);
					strategy.setDataset(null);
					strategy.setSymbol(null);
					strategy.setDefaultOrderTiming(null);
					strategy.setTrader(null);
				}
				context.clear();

				// データセット毎の統計情報を生成します。
				final Stats stats = new Stats(trader.getPositions(), trader.getInitialCapital(), dataset, start, end); 
				statsMap.put(symbol, stats);

				event = new BacktesterEvent(this, max, count, symbol, dataset.getSymbolName(), stats);
				for (final BacktesterListener listener : listeners)
					listener.backtestProcessed(event);
			}
		}

		return new Summary(statsMap, this);
	}

}
