import java.util.Iterator;
import java.io.FileWriter;
import java.io.IOException;
import com.inforeach.util.Configurator;
import com.inforeach.util.value.Value;
import com.inforeach.recorddata.IRDRecord;
import com.inforeach.recorddata.IRDRecordWithHistory;

import com.inforeach.eltrader.tms.TMSystem;
import com.inforeach.eltrader.tms.TMSException;
import com.inforeach.eltrader.tms.api.ITMSOrder;
import com.inforeach.eltrader.tms.api.ITMSConstants;
import com.inforeach.eltrader.tms.api.ITMSMarketTarget;
import com.inforeach.eltrader.tms.api.TMSNewOrderMessage;
import com.inforeach.eltrader.tms.api.TMSModifyOrderMessage;
import com.inforeach.eltrader.tms.domain.portfolio.api.TMSUtils;
import com.inforeach.eltrader.tms.domain.portfolio.api.ITMSAutoTrader;
import com.inforeach.eltrader.tms.domain.portfolio.autotrader.modules.TMSAbstractAnalyticModule;
import com.inforeach.eltrader.tms.domain.portfolio.autotrader.modules.TMSFieldListAnalyticModuleSpec;

/**
    Sample Analytic Module.

    Implements the following strategy:

    - For "Buy" targets every time the bid price changes for the target's instrument:

         Cancel any outstanding buy orders.
         Send a buy order for n shares at the bid.

    - For "Sell" targets every time the ask price changes for the target's instrument:

         Cancel any outstanding sell orders.
         Send a sell order for n shares at the ask.

    - When a complete fill for an order is received, send out a new order of same kind (buy->bid, sell->ask)

    - If an order has received no partial fills after m seconds, replace the price with the mid.

    - Every minute, log the symbol, bid, ask, and current position to a file

    - The slice size, the max. stale period, and the output file name are entered by users.
*/

public class SampleAnalytic extends TMSAbstractAnalyticModule
{
    // these CFG strings have the same value as strings used in the GUI editor configuration
    // file AnalyticModulesMetaData.xml
    public static final String CFG_MAX_SECONDS_SINCE_LAST_CHANGE = "maxSecondsSinceLastChange";
    public static final String CFG_SLICE_SIZE                    = "sliceSize";
    public static final String CFG_FILE_NAME                     = "outputFileName";

    private int maxSecondsSinceLastChange_;
    private int sliceSize_;
    private FileWriter outputFile_;

    private long startTradingTimerId_;
    private long dumpRecordIntoFileTimerId_;
    private long staleOrderCheckerTimerId_;

    public SampleAnalytic(ITMSAutoTrader autoTrader)
    {
        super(autoTrader);
    }

    public void onAutoTraderCreated()
    {
        // this method will be called initially when the AutoTrader is instantated.
        // The call to setup() will do all the neccessary initialization and subscription.

        try
        {
            sendAuditRecord("AutoTrader starting.");

            // this call performs all necessary initialization
            setup();
        }
        catch (Throwable ex)
        {
            TMSUtils.auditException(ex, getAutoTrader(), this);
        }
    }

    public void onAutoTraderRecovered()
    {
        // this method will be called when the AutoTrader is recovers after portfolio satellite restart.
        // The call to setup() will do all the neccessary initialization and subscription.

        try
        {
            sendAuditRecord("AutoTrader recovering.");

            // this call performs all necessary initialization
            setup();
        }
        catch (Throwable ex)
        {
            TMSUtils.auditException(ex, getAutoTrader(), this);
        }
    }

    protected void setup()
    {
        // this is main initialization method called from onAutoTraderCreated()
        // and onAutoTraderRecovered()

        // first read some parameters from the spec (entered by users in the GUI)
        maxSecondsSinceLastChange_  = getSpec().getFieldValue(CFG_MAX_SECONDS_SINCE_LAST_CHANGE).intValue();
        sliceSize_  = getSpec().getFieldValue(CFG_SLICE_SIZE).intValue();
        String outputFileName  = getSpec().getFieldValue(CFG_FILE_NAME).stringValue();

        // open a file
        try
        {
            outputFile_ = new FileWriter(outputFileName, true);
        }
        catch (IOException ex)
        {
            TMSUtils.auditException(ex, getAutoTrader(), this);
        }

        try
        {
            // Subscribe for order data. This analytic will be notified of any change in any order
            // sliced from this portfolio's target in the onOrderUpdate() method
            getAutoTrader().subscribeForOrderData();

            // Subscribe for target data. This analytic will be notified of any change in any of
            // this portfolio's target in the onTargetUpdate() method
            getAutoTrader().subscribeForTargetData();

            // Subscribe for market data for all existing targets.
            subscribeForMarketDataForExistingTargets();
        }
        catch (TMSException ex)
        {
            TMSUtils.auditException(ex, getAutoTrader(), this);
        }

        // also schedule the initial sweep of all targets to release initial orders
        // in this case we give a 10 sec. delay from NOW. Look for onTimer()
        //  implementation for more details.
        startTradingTimerId_ = getAutoTrader().scheduleNonRepeatingTimer(10);

        // also schedule the 1 min. timer to record info into the file
        // (starting 20 sec. from now do it every 1 min.). Look for onTimer()
        //  implementation for more details.
        dumpRecordIntoFileTimerId_ = getAutoTrader().scheduleRepeatingTimer(20, 60);

        // also schedule the timer that will trigger the checking of the order age
        // since the last change.
        staleOrderCheckerTimerId_ = getAutoTrader().scheduleRepeatingTimer(maxSecondsSinceLastChange_, maxSecondsSinceLastChange_);
    }

    private void subscribeForMarketDataForExistingTargets()
    {
        Iterator it = getAutoTrader().getTargets();
        while (it.hasNext())
        {
            ITMSMarketTarget target = (ITMSMarketTarget)it.next();
            try
            {
                getAutoTrader().subscribeForMarketData(target.getInstrumentId());
            }
            catch (TMSException ex)
            {
                TMSUtils.auditException(ex, getAutoTrader(), target, this);
            }
        }
    }

    // is called when some timer triggers
    public void onTimer(long timerId)
    {
        if (!getAutoTrader().isExecuting()) // users could Pause/Terminate AutoTrader
            return;

        if (timerId == startTradingTimerId_)
            startTrading();
        else
        if (timerId == staleOrderCheckerTimerId_)
            evaluateAllTargetsAndPerformNecessaryActions();
        else
        if (timerId == dumpRecordIntoFileTimerId_)
            dumpRecords();
    }

    private void startTrading()
    {
        // run through all targets and release initial order based on market price

        sendAuditRecord("Start trading timer triggered -- releasing initial orders");

        Iterator targetIterator = getAutoTrader().getTargets();
        ITMSMarketTarget target;
        while (targetIterator.hasNext())
        {
            target = (ITMSMarketTarget)targetIterator.next();

            if (!target.isActive())
                continue;
            else
                releaseOrderForTargetAtMarketPrice(target);
        }
    }

    private void releaseOrderForTargetAtMarketPrice(ITMSMarketTarget target)
    {
        IRDRecord marketDataRecord = getAutoTrader().getMarketDataRecord(target.getInstrumentId());
        boolean isTargetBuy =  (target.getSideType() == ITMSConstants.SIDETYPE_BUY);

        if (isTargetBuy)
        {
            double bidPx = marketDataRecord.getDoubleFieldValue(ITMSConstants.RDFLDTAG_BID_PRICE);

            if (!Double.isNaN(bidPx)) // can be NaN if subscription has not yet produced any quotes or symbol is incorrect
            {
                // prepare and send order
                TMSNewOrderMessage message = new TMSNewOrderMessage(target);
                message.setFieldValue(ITMSConstants.MSGFLDTAG_ORDER_QTY, sliceSize_);
                message.setFieldValue(ITMSConstants.MSGFLDTAG_PRICE, bidPx);
                message.setFieldValue(ITMSConstants.MSGFLDTAG_ORD_TYPE, ITMSConstants.ORDTYPE_LIMIT);

                try
                {
                    getAutoTrader().sendOrder(target, message);
                    sendAuditRecord(target, "Releasing new order at " + bidPx, getAutoTrader().AUDIT_SEVERITY_ACTIVITY_DETAILS_LOW);
                }
                catch (TMSException ex)
                {
                    TMSUtils.auditException(ex, getAutoTrader(), target, this);
                }
            }
            else
            {
                sendAuditRecord(target, "Not releasing new order --- could not obtain price", getAutoTrader().AUDIT_SEVERITY_ACTIVITY_DETAILS_LOW);
            }
        }
        else
        {
            double askPx = marketDataRecord.getDoubleFieldValue(ITMSConstants.RDFLDTAG_ASK_PRICE);

            if (!Double.isNaN(askPx)) // can be NaN if subscription has not yet produced any quotes or symbol is incorrect
            {
                // prepare and send order
                TMSNewOrderMessage message = new TMSNewOrderMessage(target);
                message.setFieldValue(ITMSConstants.MSGFLDTAG_ORDER_QTY, sliceSize_);
                message.setFieldValue(ITMSConstants.MSGFLDTAG_PRICE, askPx);
                message.setFieldValue(ITMSConstants.MSGFLDTAG_ORD_TYPE, ITMSConstants.ORDTYPE_LIMIT);

                try
                {
                    getAutoTrader().sendOrder(target, message);
                    sendAuditRecord(target, "Releasing new order at " + askPx, getAutoTrader().AUDIT_SEVERITY_ACTIVITY_DETAILS_LOW);
                }
                catch (TMSException ex)
                {
                    TMSUtils.auditException(ex, getAutoTrader(), target, this);
                }
            }
            else
            {
                sendAuditRecord(target, "Not releasing new order --- could not obtain price", getAutoTrader().AUDIT_SEVERITY_ACTIVITY_DETAILS_LOW);
            }
        }
    }

    private void evaluateAllTargetsAndPerformNecessaryActions()
    {
        // run through all targets and evaluate

        Iterator it = getAutoTrader().getTargets();
        while (it.hasNext())
        {
            ITMSMarketTarget target = (ITMSMarketTarget)it.next();

            if (!target.isActive())
                continue;

            Iterator ordersIterator = getAutoTrader().getOpenOrdersForTarget(target, false);

            if (ordersIterator == null || !ordersIterator.hasNext())
            {
                // this target has no open orders --- release one.
                // this can happen if there was no market price initially
                // because of subscription latency
                releaseOrderForTargetAtMarketPrice(target);
            }
            else
            {
                // run through all open orders and see if they are stale
                long currentTime = System.currentTimeMillis();
                long lastFillTime;
                long orderTime;
                while (ordersIterator != null && ordersIterator.hasNext())
                {
                    ITMSOrder order = (ITMSOrder) ordersIterator.next();

                    lastFillTime = order.getLastFillTime();

                    if (lastFillTime <= 0)
                    {
                        // order was never  filled --- compare with order time
                        orderTime = order.getOrderTime();
                        if ((currentTime - orderTime) / 1000 < maxSecondsSinceLastChange_)
                        {
                            // modify order to mid
                            modifyOrderToMidPx(target, order);
                        }
                    }
                    else
                    {
                        if ((currentTime - lastFillTime) / 1000 < maxSecondsSinceLastChange_)
                        {
                            // modify order to mid
                            modifyOrderToMidPx(target, order);
                        }
                    }
                }
            }
        }
    }

    private void modifyOrderToMidPx(ITMSMarketTarget target, ITMSOrder order)
    {
        IRDRecord marketDataRecord = getAutoTrader().getMarketDataRecord(target.getInstrumentId());

        double midPx = marketDataRecord.getDoubleFieldValue(ITMSConstants.RDFLDTAG_MID_PRICE);

        if (!Double.isNaN(midPx) && (midPx != order.getPrice())) // can be NaN if subscription has not yet produced any quotes or symbol is incorrect
        {
            if ((order.getTransitionState() != ITMSConstants.TRANSSTATE_CANCEL_PENDING) &&
                (order.getTransitionState() != ITMSConstants.TRANSSTATE_MODIFY_PENDING))
           {
               // prepare and send modify request only if there is no cancel or modify already pending
               TMSModifyOrderMessage modifyMsg = new TMSModifyOrderMessage(target);
               modifyMsg.setFieldValue(ITMSConstants.MSGFLDTAG_PRICE, midPx);

               try
               {
                   getAutoTrader().modifyOrder(order, modifyMsg);
                   sendAuditRecord(target, "Modifying order to mid " + midPx,
                                   getAutoTrader().
                                   AUDIT_SEVERITY_ACTIVITY_DETAILS_LOW);
               }
               catch (TMSException ex)
               {
                   TMSUtils.auditException(ex, getAutoTrader(), target, this);
               }
           }
        }
        else
        {
            sendAuditRecord(target, "Not modifying  order --- could not obtain price", getAutoTrader().AUDIT_SEVERITY_ACTIVITY_DETAILS_LOW);
        }
    }

    /**
    // This callback method is called whenever status of the order changes
     */
    public void onOrderUpdate(ITMSOrder order)
    {
        if (!getAutoTrader().isExecuting()) // users could Pause/Terminate autotrader
            return;

        // find a parent target for this order
        ITMSMarketTarget target = getAutoTrader().getTargetForOrder(order);

        if (!target.isActive()) // users could Pause/Suspend target
            return;

        if (!order.isOpen())
        {
            char ordStatus = order.getStatus();
            if (ordStatus == ORDSTATUS_REJECTED)
            {
                // order got rejected -- pause the whole target
                try
                {
                    getAutoTrader().pauseTarget(target, "Order got rejected", false);
                    sendAuditRecord(target, "Pausing target. Order was rejected.", getAutoTrader().AUDIT_SEVERITY_ACTIVITY_DETAILS_LOW);
                }
                catch (TMSException ex)
                {
                    TMSUtils.auditException(ex, getAutoTrader(), target, this);
                }
            }
            else
            {
                // order is either canceled or filled
                // so release new order for the target (if there is no other open order already)
                Iterator ordersIterator = getAutoTrader().getOpenOrdersForTarget(target, false);

                if (ordersIterator == null || !ordersIterator.hasNext())
                    releaseOrderForTargetAtMarketPrice(target);
            }
        }
    }

    /**
    // This callback method is called whenever the quote changes for some target instrument
     */
    public void onMarketDataUpdate(String instrumentId, IRDRecord record)
    {
        if (!getAutoTrader().isExecuting()) // users could Pause/Terminate autotrader
            return;
        
        double bidPrice, askPrice;        
        if (record instanceof IRDRecordWithHistory)
        {
            // calculate ask and bid prices as simple average of prices from history 
            IRDRecordWithHistory recordWithHistory = (IRDRecordWithHistory)record;
            int historySize = recordWithHistory.getRecordCount();
            double bidPriceSum = 0, askPriceSum = 0;
            for (int i = 0; i < historySize; i++)
            { 
                IRDRecord historyRecord = recordWithHistory.getRecordAt(i);
                bidPriceSum += historyRecord.getDoubleFieldValue(ITMSConstants.RDFLDTAG_BID_PRICE);
                askPriceSum += historyRecord.getDoubleFieldValue(ITMSConstants.RDFLDTAG_ASK_PRICE);
            }
            bidPrice = bidPriceSum / historySize;
            askPrice = askPriceSum / historySize;            
        }
        else
        {
            // simply use last ask and bid price values
            bidPrice = record.getDoubleFieldValue(ITMSConstants.RDFLDTAG_BID_PRICE);
            askPrice = record.getDoubleFieldValue(ITMSConstants.RDFLDTAG_ASK_PRICE);
        }

        // find all targets in this portoflio for this instrumentId
        Iterator targetIterator = getAutoTrader().getTargetsForInstrument(instrumentId);
        ITMSMarketTarget target;
        while (targetIterator.hasNext())
        {
            target = (ITMSMarketTarget)targetIterator.next();
            if (!target.isActive()) // users could Pause/Suspend target
                continue;

            // for each open order of the target see if need to cancel
            Iterator openOrdersIterator = getAutoTrader().getOpenOrdersForTarget(target, false);
            while (openOrdersIterator.hasNext())
            {
                ITMSOrder order = (ITMSOrder)openOrdersIterator.next();
                
                double marketPrice = order.getSideType() == ITMSConstants.SIDETYPE_BUY? bidPrice: askPrice;                 

                double ordPx = order.getPrice();
                if (ordPx != marketPrice)
                {
                    try
                    {
                        // cancel order only if there is no cancel or modify already pending
                        if ((order.getTransitionState() != ITMSConstants.TRANSSTATE_CANCEL_PENDING) &&
                            (order.getTransitionState() != ITMSConstants.TRANSSTATE_MODIFY_PENDING))
                        {
                             getAutoTrader().cancelOrder(order, null);
                             sendAuditRecord(target, "Canceling order because mktPx " + marketPrice +
                                            " is different from ordPx " + ordPx, getAutoTrader().AUDIT_SEVERITY_ACTIVITY_DETAILS_LOW);

                            // after we cancel the order we do not release new one right away.
                            // Rather we wait for ordser status to change to Caceled --- after
                            // the cancel report is received from the broker. This happens
                            // in the callback method onPrderUpdate()
                        }
                    }
                    catch (TMSException ex)
                    {
                        TMSUtils.auditException(ex, getAutoTrader(), target, this);
                    }
                }
            }
        }
    }

    private void dumpRecords()
    {
        // run through all targets and dump the info

        sendAuditRecord("Dumping records into file");

        Iterator targetIterator = getAutoTrader().getTargets();
        ITMSMarketTarget target;
        IRDRecord marketDataRecord;
        double askPx;
        double bidPx;
        String instrument;
        double fillQty;
        double fillValue;

        StringBuffer nextRecord;

        while (targetIterator.hasNext())
        {
            target = (ITMSMarketTarget) targetIterator.next();
            marketDataRecord = getAutoTrader().getMarketDataRecord(target.getInstrumentId());

            askPx = marketDataRecord.getDoubleFieldValue(ITMSConstants.RDFLDTAG_ASK_PRICE);
            bidPx = marketDataRecord.getDoubleFieldValue(ITMSConstants.RDFLDTAG_BID_PRICE);
            instrument = target.getInstrumentId();
            fillQty = Math.abs(target.getNetFillQty());
            fillValue = Math.abs(target.getNetFillVal());

            nextRecord = new StringBuffer(50);
            nextRecord.append(instrument).append('\t').
                       append(askPx).append('\t').
                       append(bidPx).append('\t').
                       append(fillQty).append('\t').
                       append(fillValue).append("\t\n");

            try
            {
                outputFile_.write(nextRecord.toString());
                outputFile_.flush();
            }
            catch(IOException ex)
            {
                TMSUtils.auditException(ex, getAutoTrader(), target, this);
            }
        }

    }

    /**
    // Action to be performed when a new target is added.
     */
    public void onTargetAdded(ITMSMarketTarget target)
    {
        sendAuditRecord(target, "Target added.", getAutoTrader().AUDIT_SEVERITY_ACTIVITY_DETAILS_LOW);

        try
        {
            // every time a target is added need to subscribe for market data
            getAutoTrader().subscribeForMarketData(target.getInstrumentId());
        }
        catch (TMSException ex)
        {
            TMSUtils.auditException(ex, getAutoTrader(), target, this);
        }
    }

// some action handlers that in this sample do nothing

    /**
    // Action to be performed when a new target is changed.
     */
    public void onTargetUpdate(ITMSMarketTarget target)
    {
        sendAuditRecord(target, "Target updated. ", getAutoTrader().AUDIT_SEVERITY_ACTIVITY_DETAILS_LOW);
    }

    /**
    // Action to be performed when a new target is paused.
     */
    public void onTargetPaused(ITMSMarketTarget target)
    {
        sendAuditRecord(target, "Target paused.", getAutoTrader().AUDIT_SEVERITY_ACTIVITY_DETAILS_LOW);
    }

    /**
    // Action to be performed when a new target is resumed.
     */
    public void onTargetResumed(ITMSMarketTarget target)
    {
        sendAuditRecord(target, "Target resumed.", getAutoTrader().AUDIT_SEVERITY_ACTIVITY_DETAILS_LOW);
    }

    public void onAutoTraderModified()
    {
        try
        {
            sendAuditRecord("AutoTrader modified.", getAutoTrader().AUDIT_SEVERITY_ACTIVITY_DETAILS_LOW);

            setup();
        }
        catch (Throwable ex)
        {
            TMSUtils.auditException(ex, getAutoTrader(), this);
        }
    }

    public void onAutoTraderAction(Configurator params)
    {
        if (null != params)
        {
            String action = params.getPropertyStringValue(ITMSConstants.AT_ACTION_COMMAND, null);

            sendAuditRecord("AutoTrader action: " + action, getAutoTrader().AUDIT_SEVERITY_ACTIVITY_DETAILS_LOW);

            if (ITMSConstants.AT_ACTION_MANUAL_TRIGGER.equals(action))
                evaluateAllTargetsAndPerformNecessaryActions();
        }
    }

// convenience methods

    private TMSFieldListAnalyticModuleSpec getSpec()
    {
        return (TMSFieldListAnalyticModuleSpec)getAutoTrader().getSpec().getAnalyticModuleSpec();
    }

// auxiliary auditing methods

    void sendAuditRecord(String message)
    {
        sendAuditRecord(message, getAutoTrader().AUDIT_SEVERITY_ACTIVITY_DETAILS_LOW);
    }

    void sendAuditRecord(ITMSMarketTarget target, String message)
    {
        sendAuditRecord(target, message, getAutoTrader().AUDIT_SEVERITY_ACTIVITY_DETAILS_LOW);
    }

    void sendAuditRecord(String message, int level)
    {
        try
        {
            getAutoTrader().sendAuditRecord(message, level);
        }
        catch (TMSException ex)
        {
            TMSystem.getLogFacility().error().write(this, ex);
        }
    }

    void sendAuditRecord(ITMSMarketTarget target, String message, int level)
    {
        try
        {
            getAutoTrader().sendAuditRecord(target, message, level);
        }
        catch (TMSException ex)
        {
            TMSystem.getLogFacility().error().write(this, ex);
        }
    }
}