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

"""
Котировка
=========
X{Котировка} - это U{цена<http://ru.wikipedia.org/wiki/Цена>} товара. Так как
цена понятие всегда относительное, необходимо знать в каких единицах измеряется
эта цена (можно же подсчитать, сколько попугаев стоит один доллар:). Поэтому c
котировкой всегда связываются два товара: числитель и знаменатель котировки.

В зависимости от того, какой товар в котировке является "базовым", котировки
делятся на прямые и обратные (косвенные).

X{Прямая котировка} указывает цену единицы объекта торговли (1.20 доллар/1 литр
бензина) и цена единицы указывается в числителе котировки.

X{Обратная котировка} показывает - сколько единиц товара мы можем купить за
единицу денег (0,83 литра бензина/1 доллар). В числителе обратной котировке
указывается объем товара, который можно приорести за единицу денег.

Кроме того, котировки бывают односторонними и двусторонними.

X{Односторонняя котировка} - это объявление одной цены (покупки или продажи, L{Price}).

X{Двусторонняя котировка} - это объявление цены покупки и цены продажи дилером
(L{DualPrice}).

Вспомогательные объекты
-----------------------
С основным объектом - котировкой - связываются второстепенные: товар, единица
товара, данные котировки.

X{Товар} - это объект, участвующий в котировке (L{Item}). Это тот объект, который
указывается либо в числителе, либо в знаменателе. Товаром также считается и
валюта.

X{Единица товара} - определяет в какой размерности представлен объем товара в
котировке (например, котировка 10 Гривен/1 Рубль отражает стоимость 10 гривен
при покупке их за рубли. В терминологии системы 10 Гривен - это виртуальная
единица (L{Unit})).

X{Курс} - зафиксированная в определенное время цена товара
(L{Price}).

Данные котировки по отношению к котировке образуют связь многие к одной.

Кросс-котировки
---------------
X{Кроссовая котировка} отличается от обычной только методом расчета, но в
конечном счете сводится к модели числитель/знаменатель. Примером кроссовой
является котировке EUR/USD при расчете её через курс рубля.

Мелкие математические выкладки
------------------------------
Котировка дается в виде числа M{s}: M{s = x/y}, где M{x} - количество единиц
числителя, M{y} - количество единиц знаменателя.

Очень часто требуется ответить на вопросы:
    1. Сколько единиц знаменателя стоит одна единица числителя?
    2. Сколько единиц числителя стоит одна единица знаменателя (обратная задача)?

Для расчета цены числителя в единицах знаменателя (L{Price.numeratorCost})
используем формулу M{n = ys/x}.

Для расчета цены знаменателя в единицах числителя (L{Price.denominatorCost}) -
M{d = x/ys}.

Очевидно: M{n = 1/d}.

Дельты
------
Часто требуется вычислить величину изменения значения котировки. В таком
расчете участвуют два значения котировки - текущее и предыдущее.

X{Абсолютное изменение котировки} вычисляется просто (L{absolutePriceDelta}):
M{c = v - v'}, где M{v} - текущее значение котировки, M{v'} - предыдущее
значение котировки.

X{Относительное изменение котировки} вычисляется в процентах от предыдущего
значения (L{relativePriceDelta}): M{o = (c / v') * 100}.

Абсолютное изменение может быть вычислено всегда. Отнсительное - при условии
присутствия предыдущего I{ненулевого} значения (см. L{safeRelativePriceDelta}).

Используемые типы чисел
-----------------------
При вычислениях используется тип L{stocks_ft}. Для изменения этого типа следует
использовать L{set_stocks_ft}.

Управление точностью
--------------------
Модуль считает с той точностью, которая достижима при заданных входных.
значениях. Но, можно заставить модуль выдавать значения с той точностью,
которая необходима нам. Таким полезным свойством обладает класс
L{PrecisionController}. Если через него пропускать необходимые нам значения, то
они будут выданы с нужной точностью.

По умолчанию контроль точности не выполняется, то есть используется специальный
контроллер - L{pseudoPrecisionController}.

В качестве вспомогательного элемента используется декоратор
L{precision_control}, в который заворачиваются все функции, контролируемые по
точности.

Все классы, которые сами управляют точностью, являются потомками класса
L{Refined}.
"""
from datetime import datetime  # для assert'a


stocks_ft = float  #: Этот тип используется во всех расчетов в этом модуле.


def set_stocks_ft(float_type):
    global stocks_ft
    stocks_ft = float_type


class Stock(object):
    """
    Товар - любой объект, который имеет цену.
    """

    def __init__(self, stock_id):
        """
        Конструктор товара.

        @type stock_id: что угодно
        @param stock_id: Идентификатор товара.
        """
        self._stock_id = stock_id

    def _get_stockid(self): return self._stock_id

    stock_id = property(_get_stockid)

    def _get_scale(self): return stocks_ft(1.0)

    scale = property(_get_scale)


class Unit(object):
    """
    Единица измерения количества довара, например, доллар.
    """

    def __init__(self, unit_id, scale):
        """
        Конструктор единицы товара.

        @param unitId: Идентификатор единицы товара.
        @type scale: number
        @param scale: Показывает, сколько единиц товара содержится в нашей виртуальной единице.
        """
        assert scale != 0, "Zero unit scale."
        self._unitId = unit_id
        self._scale = stocks_ft(scale)

    def _get_unitid(self): return self._unitId

    unitId = property(_get_unitid)

    def _get_scale(self): return self._scale

    scale = property(_get_scale)


class StockWithUnit(Stock):
    """
    Товар с единицей измерения количества, например, бензин в литрах.
    """

    def __init__(self, stock_id, unit):
        """
        Создаем товар с единицей измерения количества.

        @type stock_id: Stock.stockId
        @param stock_id: См. Stock.stockId
        @type unit: L{Unit}
        @param unit: Единица измерения количества товара.
        """
        super().__init__(stock_id)
        assert isinstance(unit, Unit), "Expected Unit, but received: %s" % unit.__class__
        self.unit = unit

    def _get_scale(self): return self.unit.scale

    scale = property(_get_scale)


class NumeratorDenumenator:
    def __init__(self, config_dict=None, stock=None, scale=1):
        if config_dict is not None:
            self.stock = config_dict.get('stock', None)
            self.scale = config_dict.get('scale', 1)
        else:
            self.stock = stock
            self.scale = scale


class Quote(object):
    """
    Котировка - описывает отношение между ценами товаров.
    Котировка не содержит в себе никакого значения, она описывает участвующие в сравнении товары.
    """

    def __init__(self, quote_config):
        """
        Конструктор котировки. Так как в котировке указываются не только товары
        но и единицы измерения количества этих товаров, котировки USD1/EUR1 и
        USD10/EUR10 - разные котировки.

        В прямых котировках в числители всегда стоит единицы, в обратных единица всегда стоит в знаменателе.

        Кросскотировки рассчитываются через третий товар (например, EUR/USD через рубль).
        Поэтому - информации о числителе и знаменателе мало, чтобы идентифицировать котировку.
        """
        self._quote_config = quote_config
        self._quote_id = quote_config.get('id')
        self.name = quote_config.get("name")
        self.symbol = quote_config.get("symbol", None)
        self.stype = quote_config.get("stype", None)

        self.source = None

        # Параметры для вычисления флажка is_hot
        self.klimit = quote_config.get('ishot', {}).get('klimit', 0.0)
        # По умолчанию вычисляем klimit по значению relative.
        # Если установлен флаг klimit_percent, то вычисляем по значению relative.
        self.klimit_percent = quote_config.get('ishot', {}).get('percent', True)

        self._numerator = NumeratorDenumenator(config_dict=quote_config.get('numerator', {}))
        self._denominator = NumeratorDenumenator(config_dict=quote_config.get('denominator', {}))

        doublegraph_obj = quote_config.get("doublegraph", {})

        self.export = {
            "name": quote_config.get("export-name", ""),
            "news_title": quote_config.get("news-title", quote_config.get("export-name", "")),
            "page_title": quote_config.get("page-title", ""),
            "page_description": quote_config.get("page-description", ""),
            "index_title": quote_config.get("index-title", ""),
            "name_readable": quote_config.get("name-readable"),
            "group": quote_config.get("group", {}).get("text", ""),
            "group_order": quote_config.get("group", {}).get("order", 1),
            "doublegraph": tuple(doublegraph_obj.keys())[0] if len(doublegraph_obj.keys()) > 0 else 0,
            "doublegraph_region": tuple(doublegraph_obj.values())[0] if len(doublegraph_obj.values()) > 0 else 0,
            "history_type": quote_config.get("history_type", ""),
            "history_in_percentage": quote_config.get("history_in_percentage", ""),
        }
        self.is_dual = quote_config.get("is-dual", False)

        self.export_flags = quote_config.get('export-flags', {})

        self.alias_name = quote_config.get('alias') if 'alias' in quote_config else None
        self.alias_quote = None

    def _get_quoteid(self):
        return self._quote_id

    quote_id = property(_get_quoteid)

    def _get_numerator(self):
        return self._numerator

    numerator = property(_get_numerator)

    def _get_denominator(self):
        return self._denominator

    denominator = property(_get_denominator)

    def _get_timezone(self):
        return self.export.get('timezone', None)

    timezone = property(_get_timezone)

    def make_price(self, date, value, updated=None, pc=None, unconfirmed=False):
        """
        Синтаксический сахар. Эквивалентна вызову:
        C{Price(quote, date, value, pc)}.
        """
        return Price(self, date, value, updated, pc, unconfirmed)

    def make_dual_price(self, date, buy_value, sell_value, updated=None, pc=None, unconfirmed=False):
        buy = self.make_price(date, buy_value, updated, pc, unconfirmed)
        sell = self.make_price(date, sell_value, updated, pc, unconfirmed)
        return DualPrice(self, buy, sell, updated)

    def set_default_klimit(self, klimit):
        if self.klimit is None:
            self.klimit = klimit

    def make_aliases(self, all_quotes):
        if self.alias_name is not None:
            try:
                self.alias_quote = all_quotes[self.alias_name][1]
            except KeyError:
                raise RuntimeError("Unknown alias %s in %s" % (self.alias_name, self.quote_id))

    def has_flag(self, flagname):
        """
        Возвращает состояние флага flagname.
        """
        return self.export_flags.get(flagname, False)

    def has_flag_png(self):
        return self.has_flag('png')

    def attach_source(self, source):
        self.source = source


def absolutePriceDelta(current, previous):
    """
    Абсолютное изменение значения котировки.
    """
    return current - previous


def relativePriceDelta(absolute, previous):
    """
    Относительное изменения значения котировки.
    """
    return (absolute / previous) * stocks_ft(100.0)


class PseudoPrecisionController(object):
    """
    Контроллер точности, который ничего не делает :)

    FIXME: странный элемент
    """

    def __init__(self, precision=None):
        """
        Конструктор.

        @type precision: int
        @param precision: Точность вычислений.
        """
        self._precision = precision
        self._format_string = "%%.%sf" % self._precision

    def _get_precision(self): return self._precision

    precision = property(_get_precision)

    def __eq__(self, controller):
        return self.precision == controller.precision

    def __call__(self, value):
        return value

    def format_(self, value):
        """
        Форматирует числа с плавающей запятой для нужной точности.
        """
        assert type(value) == float, "format_ expected float value, but received %s" % value.__class__.__name__
        assert type(self._precision) == int, "Null precision"
        if value == 0:
            value = abs(value)
        return self._format_string % value


class PrecisionController(PseudoPrecisionController):
    """
    Пропускает через себя значения, изменяя их точность.
    """

    def __init__(self, precision):
        """
        Конструктор.

        См. L{PseudoPrecisionController}.
        """
        assert type(precision) == int and precision >= 0, "Precision must be positive integer."
        PseudoPrecisionController.__init__(self, precision)

    def __call__(self, value):
        return round(value, self.precision)


pseudoPrecisionController = PseudoPrecisionController()


class Refined(object):
    """
    От этого класса порождаются все классы, которые сами управляют точностью.
    """

    def __init__(self, pc):
        self._pc = pc if pc else pseudoPrecisionController

    def _get_pc(self): return self._pc

    pc = property(_get_pc)

    def _get_precision(self): return self._pc.precision if self._pc else None

    precision = property(_get_precision)

    def check_precision(self, otherData):
        if self.precision != otherData.precision:
            raise RuntimeError("Precision collision.")


def precision_control(func):
    """
    Декоратор, управляющий точностью.
    """

    def func_with_precision(self, *args):
        assert isinstance(self, Refined), "Precision control expected Refined class. Current: %s" % str(self.__class__)
        value = func(self, *args)
        return self.pc(value)

    func_with_precision.__name__ = func.__name__
    return func_with_precision


class AbstractPrice(object):
    def __init__(self, quote, date, updated=None, unconfirmed=False):
        assert isinstance(quote, Quote), "Quote must be Quote."
        assert isinstance(date, datetime), "Expected datetime, but received - %s" % date.__class__
        self._quote = quote
        self._date = date
        self._region = None
        self.tz = None
        self.unixtime = None  # Это поле вычисляется позже
        self.with_closetime = False
        # Этот флажок устанавливается при чтении из БД (L{S2DatabaseWithUnconfirmed.tuple_to_price}). Смотрим задачу
        # HOME-9475.
        self.unconfirmed = unconfirmed
        self._normalized = False
        self.updated = updated

    def _get_quote(self):
        return self._quote

    quote = property(_get_quote)

    def _get_date(self):
        return self._date

    date = property(_get_date)

    def get_date_date_frmt(self):
        return self._date.strftime("%Y-%m-%d")

    def get_date_time_frmt(self):
        return self._date.strftime("%H:%M")

    def _get_region(self):
        """
        Значение котировки может быть привязано к региону.
        Мы можем привязать значение к региону вызвав L{AbstractPrice.onlyForRegion}.
        """
        return self._region

    region = property(_get_region)

    def getRegion(self, default):
        """
        Для получения номера региона. Регион может быть не установлен и в этом
        случае возвращается default.
        """
        if self.region is None:
            return default
        else:
            return self.region

    def onlyForRegion(self, region):
        """
        Может так случиться, что данные актуальны только для какого-то
        определенного региона. Парсер может вызвать этот метод, чтобы известить
        остальные модули конвейера об этом.
        """
        assert isinstance(region, int), "Region must be integer."
        self._region = region

    def set_with_closetime(self, state):
        """
        Источник может пометить котировку флажком with_closetime.
        """
        self.with_closetime = state

    def _get_buy(self):
        """
        Поле сделано для совместимости между котировками (одиночными и
        двойными). Нужно для удобного формирования шаблона.
        """
        return self

    buy = property(_get_buy)

    def _get_is_dual_price(self):
        """
        Это свойство немного нарушает принцип наследования, но без него трудно
        генерировать вывод по шаблоном. Часто надо знать, односторонняя перед
        нами котировка или двусторонняя.
        """
        return False

    is_dual_price = property(_get_is_dual_price)

    def setTimezone(self, tz):
        """
        Может так случиться, что данные пришли в какой-то специфичной временной
        зоне. Тогда парсер может вызвать эту функцию, чтобы проинструктировать
        калькуляторы об этой особенности.
        """
        self.tz = tz

    @precision_control
    def _reverse(self, value):
        """
        Вспомогательная функция, контролирует точность.
        """
        return 1.0 / value

    def calcReversePrice(self, reverseQuote):
        """
        Вычисляет обратную котировки.
        """
        pass

    def normalize(self):
        self._normalized = True

    def is_normalized(self):
        return self._normalized

    def __mul__(self, k):
        pass

    def __div__(self, k):
        return self.__mul__(1.0 / k)

    def show(self):
        """
        Возвращает строчку, по которой можно понять, что это за котировка.
        """
        pass


class Price(Refined, AbstractPrice):
    """
    Значение котировки (курс, price) за какое-то число.
    """

    def __init__(self, quote, date, value, updated=None, pc=None, unconfirmed=False):
        """
        Конструктор значения котировки.

        @type quote: L{Qoute}
        @param quote: Котировка.
        @type date: datetime
        @param date: Дата и время зафиксированного значения котировки.
        @type value: float
        @param value: Значение котировки.
        @type pc: L{PrecisionController}
        @param pc: См. L{Refined}.
        """
        AbstractPrice.__init__(self, quote, date, updated, unconfirmed)
        Refined.__init__(self, pc)
        self._value = self._pc(stocks_ft(value))

    def _get_numerator(self):
        return self.quote.numerator

    numerator = property(_get_numerator)

    def _get_denominator(self):
        return self.quote.denominator

    denominator = property(_get_denominator)

    def _get_value(self):
        return self._value

    value = property(_get_value)

    @precision_control
    def numeratorCost(self):
        """
        Вычисляет цену числителя в единицах знаменателя.

        @returns:
            Цена числителя в единицах знаменателя.
        """
        # FIXME: при большом значении numerator.scale возможно падение в 0
        cost = (self.denominator.scale / self.numerator.scale) * self.value
        if cost == 0:
            raise RuntimeError("Numerator cost is Zero.")
        return cost

    @precision_control
    def denominatorCost(self):
        """
        Вычисляет цену знаменателя в единицах числителя.

        @returns:
            Цена знаменателя в единицах числителя.
        """
        return stocks_ft(1.0) / self.numeratorCost()

    @precision_control
    def absoluteDelta(self, previous):
        """
        Вычисляет абсолютное изменение котировки.

        @type previous: L{Price}
        @param previous: Предыдущее значение котировки. Должно быть в той же точности, что и self.
        """
        assert isinstance(previous, Price), "Price expected for absoluteDelta calculating."
        assert previous.quote.quote_id == self.quote.quote_id, "Defferent quotes."
        self.check_precision(previous)
        return absolutePriceDelta(self.value, previous.value)

    @precision_control
    def relativeDelta(self, previous):
        """
        Вычисляет относительное изменение котировки.

        @type previous: L{Price}
        @param previous: Предыдущее значение котировки.

        @exception ZeroDivisionError: Предыдущее значение нулевое.
        """
        assert isinstance(previous, Price), "Price expected for absoluteDelta calculating."
        assert previous.quote.quote_id == self.quote.quote_id, "Defferent quotes."
        self.check_precision(previous)
        absolute = self.absoluteDelta(previous)
        return relativePriceDelta(absolute, previous.value)

    def toPrecision(self, precision):
        """
        Если нам надо вычислять значения с определенной точностью, используем
        этот метод.

        @type precision: int
        @param precision: Требуемая точность.
        """
        cp = self.__class__(self.quote, self.date, self.value, self.updated, PrecisionController(precision))
        cp.unconfirmed = self.unconfirmed
        # FIXME: нужно что-то делать с этим unixtime
        cp.unixtime = self.unixtime
        return cp

    def calcReversePrice(self, reverseQuote):
        """
        @type reverseQuote: L{Quote}
        @param reverseQuote: Обратная котировка.
        """
        reverse_value = self._reverse(self.value)
        reverse = self.__class__(reverseQuote, self.date, reverse_value, self.pc)
        reverse.unconfirmed = self.unconfirmed
        # FIXME: опять unixtime
        reverse.unixtime = self.unixtime
        return reverse

    def normalize(self):
        """
        Выполняем нормализацию значения - вычисляем стоимость единицы товара.
        """
        if self.is_normalized():
            return
        AbstractPrice.normalize(self)
        self._value = self.numeratorCost()

    @precision_control
    def _mul(self, k):
        return self.value * k

    def __mul__(self, k):
        self._value = self._mul(k)
        return self

    def show(self):
        return "%s %s %s %s %s" % (self.region, self.quote.quote_id, self.date, self.unixtime, self.value)

    def __str__(self):
        return self.show()


class DualPrice(AbstractPrice):
    def __init__(self, quote, buy, sell, updated=None):
        """
        Котировка, включающая и цену покупки и цену продажи.

        @type quote: L{Qoute}
        @param quote: Котировка.
        @type buy: L{Price}
        @param buy: Цена покупки товара (для нас).
        @type sell: L{Price}
        @param sell: Цена продажи товара (для нас).
        """
        assert isinstance(sell, Price), "Sell must be Price."
        assert isinstance(buy, Price), "Buy must be Price."
        assert buy.pc == sell.pc, "Buy precision != Sell precision."
        AbstractPrice.__init__(self, quote, sell.date, updated)
        if quote != sell.quote or quote != buy.quote:
            raise TypeError("Difference quotes for sell and buy.")
        self._sell = sell
        self._buy = buy

    def _get_pc(self):
        return self.sell.pc

    pc = property(_get_pc)

    def _get_sell(self):
        return self._sell

    sell = property(_get_sell)

    def _get_buy(self):
        return self._buy

    buy = property(_get_buy)

    def _get_is_dual_price(self):
        return True

    is_dual_price = property(_get_is_dual_price)

    def toPrecision(self, precision):
        """
        Если нам надо вычислять значения с определенной точностью, используем
        этот метод.

        @type precision: int
        @param precision: Требуемая точность.
        """
        buy = self.buy.toPrecision(precision)
        sell = self.sell.toPrecision(precision)
        cp = self.__class__(self.quote, buy, sell)
        cp.unixtime = self.unixtime
        return cp

    def calcReversePrice(self, reverse_quote):
        """
        @type reverse_quote: L{Quote}
        @param reverse_quote: Обратная котировка.
        """
        reverse_buy_value = self._reverse(self.buy.value)
        reverse_sell_value = self._reverse(self.sell.value)
        reverse = self.__class__(reverse_quote, self.date, reverse_buy_value, reverse_sell_value, self.pc)
        # FIXME: опять unixtime
        reverse.unixtime = self.unixtime
        return reverse

    def normalize(self):
        if self.is_normalized():
            return
        AbstractPrice.normalize(self)
        self.buy.normalize()
        self.sell.normalize()

    def __mul__(self, k):
        self._buy = self._buy * k
        self._sell = self._sell * k
        return self

    def show(self):
        return "%s %s %s %s %s %s" % (
            self.region, self.quote.quote_id, self.date, self.unixtime, self.buy.value, self.sell.value)

    def __str__(self):
        return self.show()


# Functions
def safeRelativePriceDelta(current, previous, quantum_jump_value):
    """
    Расчет относительного изменения котировки в условиях возможного отсутствия
    предыдущего значения или нулевого предыдущего значения. Всегда возвращает
    значение. Исключение в этой функции - ненормальный результат.

    @type current: L{Price}
    @param current: Текущее значение котировки.
    @type previous: L{Price}
    @param previous: Предыдущее значение котировки.
    @type quantum_jump_value: float
    @param quantum_jump_value: Значение, которое вернется в качестве результата
                             при обнаружении кватового скачка :)
    """
    if not previous or previous.value == 0:
        # Квантовый скачок - нет предыдущего значения или значение нулевое
        if current.value == 0:
            return 0.0
        else:
            return quantum_jump_value
    return current.relativeDelta(previous)
