# -*- coding: utf-8 -*-
"""

    Name: Generic Strategy for Fz
    Version: 0.0.1

"""

import math
import time
from datetime import datetime

from arquants import Strategy, Order
from arquants.enums import Req, Statistics, Side


class FzStrategy(Strategy):

    def __init__(self, strategies, states, md, debug=True):

        super(FzStrategy, self).__init__()

        # Parameters
        self.strategies = strategies  # enum with sub-strategies
        self.states = states  # list order by sub-strategy id with enum of sub-strategies states
        self.md_info = md  # Dictionary by sub-strategy with a list of list of market data to check
        self.debug = debug  # used for logging

        self.rejected_counter = dict() # Dictionary by strategy and index with number of rejected orders.
        self.rejected_limit = dict()    # Dictionary by strategy and index with number of rejected orders.
        self.rejected = dict()  # Dictionary by order_id with counter of rejected request for the order.
        self.idx_to_process = dict()  # Dictionary by strategy with a set of index to process when md arrives.
        self.idx_data = dict()  # Dictionary by index with a list of instrument associated to the index
        self.idx_by_inst = dict()  # Dictionary by instrument's data with list of index associated to the instrument
        self.symbol_data = dict()  # Dictionary by strategy and index with list of data associated
        self.signals = dict()  # Dictionary by strategy and index with signals generated by the strategy
        self.strategy_state = dict()  # Dictionary by strategy and index with the state of the strategy
        self.last_md = dict()  # Dictionary by strategy and index with last market data received
        self.trades_to_process = dict()  # Dictionary by strategy and index of number of trades to process
        self.my_orders = dict()  # Dictionary by strategy and index of orders by order_id
        self.index_of_orders = dict()  # Dictionary by strategy and order_id with index of the order
        self.amount = dict()  # Dictionary by strategy, index and instrument of CumQty and AvgPx

        # List of dictionaries by strategy, index and order_id
        # where we store information about new request sent to the market (0=new, 1=replace, 2=cancel)
        self.ack = [dict(), dict(), dict()]

        self.log_string = ""    # log string

        self.init_data()
        self.log_index_info()

    def next(self):
        self.generate_signals()

        # process signals
        for strategy in self.strategies:
            for index, signals in self.signals[strategy.value].items():
                getattr(self, 'process_signal_%s' % strategy.name.lower())(index, signals)

        # clear processed signals
        for strategy in self.strategies:
            self.signals[strategy.value].clear()

    def new_order(self, strategy, index, side, symbol_id, px, qty):
        if side == Order.Buy:
            order = self.buy(data=self.symbol_data[strategy.value][index][symbol_id],
                             price=px, size=qty, exectype=Order.Limit)
        elif side == Order.Sell:
            order = self.sell(data=self.symbol_data[strategy.value][index][symbol_id],
                              price=px, size=qty, exectype=Order.Limit)

        self.index_of_orders[strategy.value][order.m_orderId] = index
        self.my_orders[strategy.value][index][order.m_orderId] = order
        self.ack[strategy.value][index][Req.NEW.value][order.m_orderId] = order

    def replace_order(self, strategy, index, order_to_replace, new_px, new_qty):
        replaced_order = self.replace(size=new_qty, price=new_px, order=order_to_replace)

        self.index_of_orders[strategy.value][replaced_order.m_orderId] = index
        self.my_orders[strategy.value][index][replaced_order.m_orderId] = replaced_order
        self.ack[strategy.value][index][Req.REPLACE.value][replaced_order.m_orderId] = order_to_replace

    def cancel_order(self, strategy, index, order_to_cancel):
        self.cancel(order_to_cancel)
        self.ack[strategy.value][index][Req.CANCEL.value][order_to_cancel.m_orderId] = order_to_cancel

    def notify_order(self, order):
        # check strategy that send the order
        for strategy in self.strategies:
            if order.m_orderId in self.index_of_orders[strategy.value].keys():
                if order.status not in (Order.Created, Order.Submitted):
                    index = self.index_of_orders[strategy.value][order.m_orderId]
                    self.log_order_info(strategy.name, index, order)
                    if order.status in (Order.Completed, Order.Canceled):
                        self.update_amount(strategy.value, index, order)
                    if order.status is Order.Rejected:
                        self.rejected_counter[strategy.value][index] += 1
                    if order.m_orderId in self.ack[strategy.value][index][Req.NEW.value].keys():
                        del self.ack[strategy.value][index][Req.NEW.value][order.m_orderId]
                    elif order.m_orderId in self.ack[strategy.value][index][Req.REPLACE.value].keys():
                        replaced = self.ack[strategy.value][index][Req.REPLACE.value][order.m_orderId]
                        if order.status is Order.Rejected:
                            if replaced.m_orderId in self.rejected.keys():
                                self.cancel_order(strategy, index, replaced)
                            else:
                                self.rejected[replaced.m_orderId] = 1 # one order only could be rejected ones
                        del self.ack[strategy.value][index][Req.REPLACE.value][order.m_orderId]
                    elif order.m_orderId in self.ack[strategy.value][index][Req.CANCEL.value].keys():
                        ord_to_cancel = self.ack[strategy.value][index][Req.CANCEL.value][order.m_orderId]
                        if order.status is Order.Rejected:
                            if ord_to_cancel.m_orderId in self.rejected.keys():
                                del self.my_orders[strategy.value][index][ord_to_cancel.m_orderId]
                                del self.index_of_orders[strategy.value][index][ord_to_cancel.m_orderId]
                                del self.rejected[ord_to_cancel.m_orderId]
                                self.logs("ERROR: Orden no existe mas en el mercado. ID: %s" % ord_to_cancel.m_orderId)
                            else:
                                self.rejected[ord_to_cancel.m_orderId] = 1
                        else:
                            if ord_to_cancel.m_orderId in self.rejected.keys():
                                self.logs("ERROR: Orden cancelada por reemplazos rejectados. ID: %s" % ord_to_cancel.m_orderId)
                        del self.ack[strategy.value][index][Req.CANCEL.value][order.m_orderId]
                    if order.status in (Order.Completed, Order.Rejected, Order.Canceled):
                        del self.my_orders[strategy.value][index][order.m_orderId]
                        del self.index_of_orders[strategy.value][order.m_orderId]
                    getattr(self, 'notify_order_%s' % strategy.name.lower())(index, order)
                break
            else:
                self.log_order_not_expected(strategy, None, order)

    def generate_signals(self):
        self.update_market_data()
        for strategy in self.strategies:
            for idx in self.idx_to_process[strategy.value]:
                # for every sub-strategy we call generate signals for every index in which market data changed
                getattr(self, 'generate_signal_%s' % strategy.name.lower())(idx, self.symbol_data[strategy.value][idx])
            self.idx_to_process[strategy.value].clear()

    def update_market_data(self):
        """
        Update market data of every instrument and identify index of every strategy that need to be process.
        """
        for instr in self.last_md.keys():
            data = getattr(self, instr)
            for md in self.last_md[instr].keys():
                self.last_md[instr][md][0] = self.last_md[instr][md][1]
                self.last_md[instr][md][1] = self.get_data_value(data, md)
                if self.last_md[instr][md][0] != self.last_md[instr][md][1]:
                    for strategy in self.strategies:
                        if instr in self.idx_by_inst[strategy.value].keys() and \
                                md in self.idx_by_inst[strategy.value][instr].keys():
                            for idx in self.idx_by_inst[strategy.value][instr][md]:
                                self.idx_to_process[strategy.value].add(idx)

    def on_pause(self):
        self.logs("Se apreto pausa, cancelando órdenes")
        self.cancel_all()

    def on_error(self):
        self.logs("Ocurrio un error, cancelando órdenes")
        self.cancel_all()

    def cancel_all(self):
        # Esperamos 1seg para evitar limite de logueo
        time.sleep(1)
        for strategy in self.strategies:
            for index in self.my_orders[strategy.value].keys():
                for order in self.my_orders[strategy.value][index].values():
                    self.logs("Cancelling order: strategy: %s - index: %s - id: %s" %
                              (strategy.name, index, order.m_orderId))
                    self.cancel(order)
            # volvemos a setaer estados iniciales
            for index in self.strategy_state[strategy.value].keys():
                self.change_state(index, strategy.value, 0)

    def init_data(self):
        for strategy in self.strategies:
            i = 0
            while True:
                if hasattr(self, 'data' + str(i)):
                    self.last_md['data' + str(i)] = dict()
                else:
                    break
                i += 1
            self.amount[strategy.value] = dict()
            self.idx_by_inst[strategy.value] = dict()
            self.idx_to_process[strategy.value] = set()
            self.signals[strategy.value] = dict()
            self.strategy_state[strategy.value] = dict()
            self.trades_to_process[strategy.value] = dict()
            self.my_orders[strategy.value] = dict()
            self.index_of_orders[strategy.value] = dict()
            self.ack[strategy.value] = dict()
            self.idx_data[strategy.value] = dict()
            self.symbol_data[strategy.value] = dict()
        for strategy in self.strategies:
            result = getattr(self, 'generate_index_%s' % strategy.name.lower())()
            for idx in result.keys():
                i = 0
                self.amount[strategy.value][idx] = dict()
                self.amount[strategy.value][Statistics.Amount.value] = 0
                self.symbol_data[strategy.value][idx] = []
                self.idx_data[strategy.value][idx] = []
                for inst in result[idx]:
                    self.amount[strategy.value][idx][inst] = dict()
                    self.amount[strategy.value][idx][Statistics.Amount.value] = 0
                    self.amount[strategy.value][idx][inst][Side.BUY.value] = dict()
                    self.amount[strategy.value][idx][inst][Side.SELL.value] = dict()
                    self.amount[strategy.value][idx][inst][Statistics.Amount.value] = 0
                    self.amount[strategy.value][idx][inst][Side.BUY.value][Statistics.CumQty.value] = 0
                    self.amount[strategy.value][idx][inst][Side.BUY.value][Statistics.AvgPx.value] = 0
                    self.amount[strategy.value][idx][inst][Side.BUY.value][Statistics.Amount.value] = 0
                    self.amount[strategy.value][idx][inst][Side.SELL.value][Statistics.CumQty.value] = 0
                    self.amount[strategy.value][idx][inst][Side.SELL.value][Statistics.AvgPx.value] = 0
                    self.amount[strategy.value][idx][inst][Side.SELL.value][Statistics.Amount.value] = 0
                    self.symbol_data[strategy.value][idx].append(getattr(self, inst))
                    self.idx_data[strategy.value][idx].append(inst)
                    if inst not in self.idx_by_inst[strategy.value].keys():
                        self.idx_by_inst[strategy.value][inst] = dict()
                        # self.idx_by_inst[strategy.value][inst] = set()
                    for md in self.md_info[strategy.value][i]:
                        self.last_md[inst][md] = [None, None]
                        if md not in self.idx_by_inst[strategy.value][inst].keys():
                            self.idx_by_inst[strategy.value][inst][md] = set()
                        self.idx_by_inst[strategy.value][inst][md].add(idx)
                    i += 1
                self.ack[strategy.value][idx] = [None, None, None]
                self.ack[strategy.value][idx][Req.NEW.value] = dict()
                self.ack[strategy.value][idx][Req.REPLACE.value] = dict()
                self.ack[strategy.value][idx][Req.CANCEL.value] = dict()
                self.trades_to_process[strategy.value][idx] = 0
                self.strategy_state[strategy.value][idx] = 0
                self.my_orders[strategy.value][idx] = dict()

    ####################
    # HELPER FUNCTIONS #
    ####################

    def change_state(self, index, strategy_id, new_state_id):
        if self.debug:
            self.logs("Strategy: %s - Index: %s - Changing State from '%s' to '%s'" %
                      (strategy_id, index, self.strategy_state[strategy_id][index], new_state_id))
        self.strategy_state[strategy_id][index] = new_state_id

    @staticmethod
    def my_compare_float(float1, float2, prec=6):
        """ Comparamos 2 float con un minimos de precision.
        True si son igual - else false
        """
        if float1 and float2:
            return abs(float1 - float2) <= (1 / 10 ** prec)
        else:
            return float1 == float2

    @staticmethod
    def get_data_value(data, level):
        value = None
        if hasattr(data, level):
            value = getattr(data, level)[0]
            if not value or math.isnan(value):
                value = None
            else:
                value = round(value, 6)
        return value

    def get_strategy_state(self, strategy_id, index):
        return self.states[strategy_id](self.strategy_state[strategy_id][index])

    def log_md(self, strategy, index):
        if self.debug:
            line = "Strategy: %s - Index: %s - State: %s. Market Data: " % \
                   (strategy.name, index, self.get_strategy_state(strategy.value, index).name)
            i = 0
            for data_name in self.idx_data[strategy.value][index]:
                data = getattr(self, data_name)
                line = line + "%s: " % data.tradecontract
                for md in self.md_info[strategy.value][i]:
                    line = line + " %s@%s " % (md, self.last_md[data_name][md][1])
                i += 1
            self.logs(line)

    def log_order_not_expected(self, strategy, index, order):
        """
        Logging unexpected order.
        """
        state = None if not index else self.strategy_state[strategy.value][index].name
        self.logs("Strategy: %s - Index: %s - State: %s. Unexpected order: id: %s - status: %s" %
                  (strategy.name, index, state, order.m_orderId, order.status))

    def log_order_info(self, strategy_name, index, order):
        if order.status is Order.Created:
            self.logs_if("Strategy: %s - Index: %s - order Created: %s" % (strategy_name, index, order.m_orderId))
        elif order.status is Order.Submitted:
            self.logs_if("Strategy: %s - Index: %s  order Submitted: %s" % (strategy_name, index, order.m_orderId))
        elif order.status is Order.Accepted:
            self.logs_if("Strategy: %s - Index: %s  order Accepted: %s" % (strategy_name, index, order.m_orderId))
        elif order.status is Order.Partial:
            self.logs_if("Strategy: %s - Index: %s  order Partial: %s" % (strategy_name, index, order.m_orderId))
        elif order.status is Order.Completed:
            self.logs_if("Strategy: %s - Index: %s - order Completed: %s" % (strategy_name, index, order.m_orderId))
        elif order.status is Order.Canceled:
            self.logs_if("Strategy: %s - Index: %s  order Canceled: %s" % (strategy_name, index, order.m_orderId))
        elif order.status is Order.Expired:
            self.logs_if("Strategy: %s - Index: %s  order Expired: %s" % (strategy_name, index, order.m_orderId))
        elif order.status is Order.Margin:
            self.logs_if("Strategy: %s - Index: %s  order Margin: %s" % (strategy_name, index, order.m_orderId))
        elif order.status is Order.Rejected:
            self.logs_if("Strategy: %s - Index: %s  order Rejected: %s" % (strategy_name, index, order.m_orderId))
        else:
            self.logs_if("Strategy: %s - Index: %s  order with not knowing status: %s" % (strategy_name, index,
                                                                                          order.m_orderId))

    def log_index_info(self):
        for strategy in self.strategies:
            for idx in self.idx_data[strategy.value].keys():
                i = 0
                line = "Strategy: %s - Index: %s - \n" % (strategy.name, idx)
                for data in self.symbol_data[strategy.value][idx]:
                    line = line + "%s: %s - minsize: %s\n" % (i, data.tradecontract, data.contractsize)
                    i += 1
                self.logs_if(line)

    def update_amount(self, strategy_id, idx, order):
        last = list(order.executed.exbits)[-1]
        avgpx = ((self.amount[strategy_id][idx][order.data][order.size][Statistics.AvgPx.value] * self.amount[strategy_id][idx][order.data][order.size][Statistics.CumQty.value]) + (abs(last.size) * abs(last.price))) / (self.amount[strategy_id][idx][order.data][order.size][Statistics.CumQty.value] + abs(last.size))
        self.amount[strategy_id][idx][order.data][order.size][Statistics.CumQty.value] += abs(last.size)
        self.amount[strategy_id][idx][order.data][order.size][Statistics.AvgPx.value] = avgpx

        self.amount[strategy_id][idx][order.data][Statistics.Amount.value] +=  (last.size * last.price / order.data)
        self.amount[strategy_id][Statistics.Amount.value]
        self.amount[strategy_id][idx][Statistics.Amount.value]


        self.amount[strategy_id][idx][order.data][order.size][Statistics.Amount.value]
        pass

    def logs_if(self, msg):
        if self.debug:
            self.log_string += str(datetime.now()) + " {" + msg + "}|"

    def flush_logs(self):
        self.logs(self.log_string)
        self.log_string = ""

