# -*- coding: utf-8 -*-
# 版权所有 2019 深圳米筐科技有限公司（下称“米筐科技”）
#
# 除非遵守当前许可，否则不得使用本软件。
#
#     * 非商业用途（非商业用途指个人出于非商业目的使用本软件，或者高校、研究所等非营利机构出于教育、科研等目的使用本软件）：
#         遵守 Apache License 2.0（下称“Apache 2.0 许可”），
#         您可以在以下位置获得 Apache 2.0 许可的副本：http://www.apache.org/licenses/LICENSE-2.0。
#         除非法律有要求或以书面形式达成协议，否则本软件分发时需保持当前许可“原样”不变，且不得附加任何条件。
#
#     * 商业用途（商业用途指个人出于任何商业目的使用本软件，或者法人或其他组织出于任何目的使用本软件）：
#         未经米筐科技授权，任何个人不得出于任何商业目的使用本软件（包括但不限于向第三方提供、销售、出租、出借、转让本软件、
#         本软件的衍生产品、引用或借鉴了本软件功能或源代码的产品或服务），任何法人或其他组织不得出于任何目的使用本软件，
#         否则米筐科技有权追究相应的知识产权侵权责任。
#         在此前提下，对本软件的使用同样需要遵守 Apache 2.0 许可，Apache 2.0 许可与本许可冲突之处，以本许可为准。
#         详细的授权流程，请联系 public@ricequant.com 获取。

import re
import copy
import datetime
from typing import Dict, Callable, Optional

import numpy as np
from dateutil.parser import parse

from rqalpha.environment import Environment
from rqalpha.const import INSTRUMENT_TYPE, POSITION_DIRECTION, DEFAULT_ACCOUNT_TYPE, EXCHANGE
from rqalpha.utils import TimeRange, INST_TYPE_IN_STOCK_ACCOUNT
from rqalpha.utils.repr import property_repr, PropertyReprMeta


class Instrument(metaclass=PropertyReprMeta):
    DEFAULT_DE_LISTED_DATE = datetime.datetime(2999, 12, 31)

    @staticmethod
    def _fix_date(ds, dflt=None) -> datetime:
        if isinstance(ds, datetime.datetime):
            return ds
        if ds == '0000-00-00' or ds is None:
            return dflt
        try:
            year, month, day = ds.split('-')
            return datetime.datetime(int(year), int(month), int(day))
        except:
            return parse(ds)

    __repr__ = property_repr

    def __init__(self, dic, future_tick_size_getter=None):
        # type: (Dict, Optional[Callable[[Instrument], float]]) -> None
        self.__dict__ = copy.copy(dic)
        self._future_tick_size_getter = future_tick_size_getter

        if "listed_date" in dic:
            self.__dict__["listed_date"] = self._fix_date(dic["listed_date"])
        if "de_listed_date" in dic:
            self.__dict__["de_listed_date"] = self._fix_date(dic["de_listed_date"], self.DEFAULT_DE_LISTED_DATE)
        if "maturity_date" in self.__dict__:
            self.__dict__["maturity_date"] = self._fix_date(dic["maturity_date"], self.DEFAULT_DE_LISTED_DATE)

        if 'contract_multiplier' in dic:
            if np.isnan(self.contract_multiplier):
                raise RuntimeError("Contract multiplier of {} is not supposed to be nan".format(self.order_book_id))

    @property
    def order_book_id(self):
        # type: () -> str
        """
        [str] 股票：证券代码，证券的独特的标识符。应以’.XSHG’或’.XSHE’结尾，前者代表上证，后者代表深证。
        期货：期货代码，期货的独特的标识符（郑商所期货合约数字部分进行了补齐。例如原有代码’ZC609’补齐之后变为’ZC1609’）。
        主力连续合约UnderlyingSymbol+88，例如’IF88’ ；指数连续合约命名规则为UnderlyingSymbol+99
        """
        return self.__dict__["order_book_id"]

    @property
    def symbol(self):
        # type: () -> str
        """
        [str] 股票：证券的简称，例如’平安银行’。期货：期货的简称，例如’沪深1005’。
        """
        return self.__dict__["symbol"]

    @property
    def round_lot(self) -> int:
        """
        [int] 股票：一手对应多少股，中国A股一手是100股。期货：一律为1。
        """
        if self.__dict__.get("type") == INSTRUMENT_TYPE.CS and self.__dict__["board_type"] == "KSH":
            return 1
        return int(self.__dict__["round_lot"])

    @property
    def listed_date(self) -> Optional[datetime.datetime]:
        """
        [datetime] 股票：该证券上市日期。期货：期货的上市日期，主力连续合约与指数连续合约都为 datetime(1990, 1, 1)。
        """
        return self.__dict__["listed_date"]

    @property
    def de_listed_date(self):
        # type: () -> datetime.datetime
        """
        [datetime] 股票：退市日期。期货：交割日期。
        """
        return self.__dict__["de_listed_date"]

    @property
    def type(self):
        # type: () -> str
        """
        [sty] 合约类型，目前支持的类型有: ‘CS’, ‘INDX’, ‘LOF’, ‘ETF’, ‘Future’
        """
        return INSTRUMENT_TYPE[self.__dict__["type"]]

    @property
    def exchange(self):
        # type: () -> EXCHANGE
        """
        [str] 交易所。股票：’XSHE’ - 深交所, ‘XSHG’ - 上交所。期货：’DCE’ - 大连商品交易所, ‘SHFE’ - 上海期货交易所，
        ’CFFEX’ - 中国金融期货交易所, ‘CZCE’- 郑州商品交易所
        """
        return self.__dict__["exchange"]

    @property
    def market_tplus(self):
        # type: () -> int
        """
        [int] 合约卖出和买入操作需要间隔的最小交易日数，如A股为 1
        公募基金的 market_tplus 默认0
        """
        return self.__dict__.get("market_tplus") or 0

    @property
    def sector_code(self):
        """
        [str] 板块缩写代码，全球通用标准定义（股票专用）
        """
        try:
            return self.__dict__["sector_code"]
        except (KeyError, ValueError):
            raise AttributeError(
                "Instrument(order_book_id={}) has no attribute 'sector_code' ".format(self.order_book_id)
            )

    @property
    def sector_code_name(self):
        """
        [str] 以当地语言为标准的板块代码名（股票专用）
        """
        try:
            return self.__dict__["sector_code_name"]
        except (KeyError, ValueError):
            raise AttributeError(
                "Instrument(order_book_id={}) has no attribute 'sector_code_name' ".format(self.order_book_id)
            )

    @property
    def industry_code(self):
        """
        [str] 国民经济行业分类代码，具体可参考“Industry列表” （股票专用）
        """
        try:
            return self.__dict__["industry_code"]
        except (KeyError, ValueError):
            raise AttributeError(
                "Instrument(order_book_id={}) has no attribute 'industry_code' ".format(self.order_book_id)
            )

    @property
    def industry_name(self):
        """
        [str] 国民经济行业分类名称（股票专用）
        """
        try:
            return self.__dict__["industry_name"]
        except (KeyError, ValueError):
            raise AttributeError(
                "Instrument(order_book_id={}) has no attribute 'industry_name' ".format(self.order_book_id)
            )

    @property
    def concept_names(self):
        """
        [str] 概念股分类，例如：’铁路基建’，’基金重仓’等（股票专用）
        """
        try:
            return self.__dict__["concept_names"]
        except (KeyError, ValueError):
            raise AttributeError(
                "Instrument(order_book_id={}) has no attribute 'concept_names' ".format(self.order_book_id)
            )

    @property
    def board_type(self):
        """
        [str] 板块类别，’MainBoard’ - 主板,’GEM’ - 创业板（股票专用）
        """
        try:
            return self.__dict__["board_type"]
        except (KeyError, ValueError):
            raise AttributeError(
                "Instrument(order_book_id={}) has no attribute 'board_type' ".format(self.order_book_id)
            )

    @property
    def status(self):
        """
        [str] 合约状态。’Active’ - 正常上市, ‘Delisted’ - 终止上市, ‘TemporarySuspended’ - 暂停上市,
        ‘PreIPO’ - 发行配售期间, ‘FailIPO’ - 发行失败（股票专用）
        """
        try:
            return self.__dict__["status"]
        except (KeyError, ValueError):
            raise AttributeError(
                "Instrument(order_book_id={}) has no attribute 'status' ".format(self.order_book_id)
            )

    @property
    def special_type(self):
        """
        [str] 特别处理状态。’Normal’ - 正常上市, ‘ST’ - ST处理, ‘StarST’ - *ST代表该股票正在接受退市警告,
        ‘PT’ - 代表该股票连续3年收入为负，将被暂停交易, ‘Other’ - 其他（股票专用）
        """
        try:
            return self.__dict__["special_type"]
        except (KeyError, ValueError):
            raise AttributeError(
                "Instrument(order_book_id={}) has no attribute 'special_type' ".format(self.order_book_id)
            )

    @property
    def contract_multiplier(self):
        """
        [float] 合约乘数，例如沪深300股指期货的乘数为300.0（期货专用）
        """
        return self.__dict__.get('contract_multiplier', 1)

    @property
    def margin_rate(self):
        """
        [float] 合约最低保证金率（期货专用）
        """
        return self.__dict__.get("margin_rate", 1)

    @property
    def underlying_order_book_id(self):
        """
        [str] 合约标的代码，目前除股指期货(IH, IF, IC)之外的期货合约，这一字段全部为’null’（期货专用）
        """
        try:
            return self.__dict__["underlying_order_book_id"]
        except (KeyError, ValueError):
            raise AttributeError(
                "Instrument(order_book_id={}) has no attribute 'underlying_order_book_id' ".format(self.order_book_id)
            )

    @property
    def underlying_symbol(self):
        """
        [str] 合约标的代码，目前除股指期货(IH, IF, IC)之外的期货合约，这一字段全部为’null’（期货专用）
        """
        try:
            return self.__dict__["underlying_symbol"]
        except (KeyError, ValueError):
            raise AttributeError(
                "Instrument(order_book_id={}) has no attribute 'underlying_symbol' ".format(self.order_book_id)
            )

    @property
    def maturity_date(self):
        # type: () -> datetime.datetime
        """
        [datetime] 到期日
        """
        try:
            return self.__dict__["maturity_date"]
        except (KeyError, ValueError):
            raise AttributeError(
                "Instrument(order_book_id={}) has no attribute 'maturity_date' ".format(self.order_book_id)
            )

    @property
    def settlement_method(self):
        """
        [str] 交割方式，’CashSettlementRequired’ - 现金交割, ‘PhysicalSettlementRequired’ - 实物交割（期货专用）
        """
        try:
            return self.__dict__["settlement_method"]
        except (KeyError, ValueError):
            raise AttributeError(
                "Instrument(order_book_id={}) has no attribute 'settlement_method' ".format(self.order_book_id)
            )

    @property
    def listing(self):
        """
        [bool] 该合约当前日期是否在交易
        """
        trading_dt = Environment.get_instance().trading_dt
        return self.listing_at(trading_dt)

    @property
    def listed(self):
        """
        [bool] 该合约当前交易日是否已上市
        """
        return self.listed_at(Environment.get_instance().trading_dt)

    @property
    def de_listed(self):
        """
        [bool] 该合约当前交易日是否已退市
        """
        return self.de_listed_at(Environment.get_instance().trading_dt)

    @property
    def account_type(self):
        if self.type in INST_TYPE_IN_STOCK_ACCOUNT:
            return DEFAULT_ACCOUNT_TYPE.STOCK
        elif self.type == INSTRUMENT_TYPE.FUTURE:
            return DEFAULT_ACCOUNT_TYPE.FUTURE
        else:
            raise NotImplementedError

    def listing_at(self, dt):
        """
        该合约在指定日期是否在交易
        :param dt: datetime.datetime
        :return: bool
        """
        return self.listed_at(dt) and not self.de_listed_at(dt)

    def listed_at(self, dt):
        """
        该合约在指定日期是否已上市
        :param dt: datetime.datetime
        :return: bool
        """
        return self.listed_date and self.listed_date <= dt

    def de_listed_at(self, dt):
        """
        该合约在指定日期是否已退市
        :param dt: datetime.datetime
        :return: bool
        """
        if self.type in (INSTRUMENT_TYPE.FUTURE, INSTRUMENT_TYPE.OPTION):
            return dt.date() > self.de_listed_date.date()
        else:
            return dt >= self.de_listed_date

    STOCK_TRADING_PERIOD = [
        TimeRange(start=datetime.time(9, 31), end=datetime.time(11, 30)),
        TimeRange(start=datetime.time(13, 1), end=datetime.time(15, 0)),
    ]

    @property
    def trading_hours(self):
        # trading_hours='09:31-11:30,13:01-15:00'
        try:
            trading_hours = self.__dict__["trading_hours"]
        except KeyError:
            if self.type in INST_TYPE_IN_STOCK_ACCOUNT:
                return self.STOCK_TRADING_PERIOD
            return None
        trading_period = []
        trading_hours = trading_hours.replace("-", ":")
        for time_range_str in trading_hours.split(","):
            start_h, start_m, end_h, end_m = (int(i) for i in time_range_str.split(":"))
            start, end = datetime.time(start_h, start_m), datetime.time(end_h, end_m)
            if start > end:
                trading_period.append(TimeRange(start, datetime.time(23, 59)))
                trading_period.append(TimeRange(datetime.time(0, 0), end))
            else:
                trading_period.append(TimeRange(start, end))
        return trading_period

    @property
    def trading_code(self):
        # type: () -> str
        try:
            return self.__dict__["trading_code"]
        except (KeyError, ValueError):
            raise AttributeError(
                "Instrument(order_book_id={}) has no attribute 'trading_code' ".format(self.order_book_id)
            )

    @property
    def trade_at_night(self):
        return any(r.start <= datetime.time(4, 0) or r.end >= datetime.time(19, 0) for r in (self.trading_hours or []))

    def during_call_auction(self, dt):
        """ 是否处于集合竞价交易时段 """
        # 当前的分钟数
        _minute = dt.hour * 60 + dt.minute

        if self.type in [INSTRUMENT_TYPE.CS, INSTRUMENT_TYPE.ETF]:
            # 9:30 前或 14:57 之后为集合竞价
            return _minute < 9 * 60 + 30 or _minute >= 14 * 60 + 57
        elif self.type == INSTRUMENT_TYPE.FUTURE:
            # 期货开盘时间
            start_time = self.trading_hours[0].start

            # -1 是因为获取到的时间都是开盘后1分钟，如 09:31
            start_minute = start_time.hour * 60 + start_time.minute - 1

            # 开盘集合竞价时间段为开盘前5分钟，期货无收盘集合竞价
            return start_minute - 5 <= _minute < start_minute
        else:
            # 其他品种由子类实现
            return False

    def days_from_listed(self):
        if not self.listed_date:
            return -1

        date = Environment.get_instance().trading_dt.date()
        if self.de_listed_date.date() < date:
            return -1

        ipo_days = (date - self.listed_date.date()).days
        return ipo_days if ipo_days >= 0 else -1

    def days_to_expire(self):
        if self.type != 'Future' or self.is_future_continuous_contract(self.order_book_id):
            return -1

        date = Environment.get_instance().trading_dt.date()
        days = (self.maturity_date.date() - date).days
        return -1 if days < 0 else days

    def tick_size(self):
        # type: () -> float
        if self.type in (INSTRUMENT_TYPE.CS, INSTRUMENT_TYPE.INDX):
            return 0.01
        elif self.type in ("ETF", "LOF"):
            return 0.001
        elif self.type == INSTRUMENT_TYPE.FUTURE:
            return self._future_tick_size_getter(self)
        else:
            raise NotImplementedError

    def calc_cash_occupation(self, price, quantity, direction):
        # type: (float, float, POSITION_DIRECTION) -> float
        if self.type in INST_TYPE_IN_STOCK_ACCOUNT:
            return price * quantity
        elif self.type == INSTRUMENT_TYPE.FUTURE:
            margin_multiplier = Environment.get_instance().config.base.margin_multiplier
            return price * quantity * self.contract_multiplier * self.margin_rate * margin_multiplier
        else:
            raise NotImplementedError

    FUTURE_CONTINUOUS_CONTRACT = re.compile(r"^[A-Z]+(88|888|99|889)([A-Z]\d+)?$")

    @classmethod
    def is_future_continuous_contract(cls, order_book_id):
        return re.match(cls.FUTURE_CONTINUOUS_CONTRACT, order_book_id)


class SectorCodeItem(object):
    def __init__(self, cn, en, name):
        self.__cn = cn
        self.__en = en
        self.__name = name

    @property
    def cn(self):
        return self.__cn

    @property
    def en(self):
        return self.__en

    @property
    def name(self):
        return self.__name

    def __repr__(self):
        return "{}: {}, {}".format(self.__name, self.__en, self.__cn)


class SectorCode(object):
    Energy = SectorCodeItem("能源", "energy", 'Energy')
    Materials = SectorCodeItem("原材料", "materials", 'Materials')
    ConsumerDiscretionary = SectorCodeItem("非必需消费品", "consumer discretionary", 'ConsumerDiscretionary')
    ConsumerStaples = SectorCodeItem("必需消费品", "consumer staples", 'ConsumerStaples')
    HealthCare = SectorCodeItem("医疗保健", "health care", 'HealthCare')
    Financials = SectorCodeItem("金融", "financials", 'Financials')
    InformationTechnology = SectorCodeItem("信息技术", "information technology", 'InformationTechnology')
    TelecommunicationServices = SectorCodeItem("电信服务", "telecommunication services", 'TelecommunicationServices')
    Utilities = SectorCodeItem("公共服务", "utilities", 'Utilities')
    Industrials = SectorCodeItem("工业", "industrials", "Industrials")


class IndustryCodeItem(object):
    def __init__(self, code, name):
        self.__code = code
        self.__name = name

    @property
    def code(self):
        return self.__code

    @property
    def name(self):
        return self.__name

    def __repr__(self):
        return "{0}:{1}".format(self.__code, self.__name)


class IndustryCode(object):
    A01 = IndustryCodeItem("A01", "农业")
    A02 = IndustryCodeItem("A02", "林业")
    A03 = IndustryCodeItem("A03", "畜牧业")
    A04 = IndustryCodeItem("A04", "渔业")
    A05 = IndustryCodeItem("A05", "农、林、牧、渔服务业")
    B06 = IndustryCodeItem("B06", "煤炭开采和洗选业")
    B07 = IndustryCodeItem("B07", "石油和天然气开采业")
    B08 = IndustryCodeItem("B08", "黑色金属矿采选业")
    B09 = IndustryCodeItem("B09", "有色金属矿采选业")
    B10 = IndustryCodeItem("B10", "非金属矿采选业")
    B11 = IndustryCodeItem("B11", "开采辅助活动")
    B12 = IndustryCodeItem("B12", "其他采矿业")
    C13 = IndustryCodeItem("C13", "农副食品加工业")
    C14 = IndustryCodeItem("C14", "食品制造业")
    C15 = IndustryCodeItem("C15", "酒、饮料和精制茶制造业")
    C16 = IndustryCodeItem("C16", "烟草制品业")
    C17 = IndustryCodeItem("C17", "纺织业")
    C18 = IndustryCodeItem("C18", "纺织服装、服饰业")
    C19 = IndustryCodeItem("C19", "皮革、毛皮、羽毛及其制品和制鞋业")
    C20 = IndustryCodeItem("C20", "木材加工及木、竹、藤、棕、草制品业")
    C21 = IndustryCodeItem("C21", "家具制造业")
    C22 = IndustryCodeItem("C22", "造纸及纸制品业")
    C23 = IndustryCodeItem("C23", "印刷和记录媒介复制业")
    C24 = IndustryCodeItem("C24", "文教、工美、体育和娱乐用品制造业")
    C25 = IndustryCodeItem("C25", "石油加工、炼焦及核燃料加工业")
    C26 = IndustryCodeItem("C26", "化学原料及化学制品制造业")
    C27 = IndustryCodeItem("C27", "医药制造业")
    C28 = IndustryCodeItem("C28", "化学纤维制造业")
    C29 = IndustryCodeItem("C29", "橡胶和塑料制品业")
    C30 = IndustryCodeItem("C30", "非金属矿物制品业")
    C31 = IndustryCodeItem("C31", "黑色金属冶炼及压延加工业")
    C32 = IndustryCodeItem("C32", "有色金属冶炼和压延加工业")
    C33 = IndustryCodeItem("C33", "金属制品业")
    C34 = IndustryCodeItem("C34", "通用设备制造业")
    C35 = IndustryCodeItem("C35", "专用设备制造业")
    C36 = IndustryCodeItem("C36", "汽车制造业")
    C37 = IndustryCodeItem("C37", "铁路、船舶、航空航天和其它运输设备制造业")
    C38 = IndustryCodeItem("C38", "电气机械及器材制造业")
    C39 = IndustryCodeItem("C39", "计算机、通信和其他电子设备制造业")
    C40 = IndustryCodeItem("C40", "仪器仪表制造业")
    C41 = IndustryCodeItem("C41", "其他制造业")
    C42 = IndustryCodeItem("C42", "废弃资源综合利用业")
    C43 = IndustryCodeItem("C43", "金属制品、机械和设备修理业")
    D44 = IndustryCodeItem("D44", "电力、热力生产和供应业")
    D45 = IndustryCodeItem("D45", "燃气生产和供应业")
    D46 = IndustryCodeItem("D46", "水的生产和供应业")
    E47 = IndustryCodeItem("E47", "房屋建筑业")
    E48 = IndustryCodeItem("E48", "土木工程建筑业")
    E49 = IndustryCodeItem("E49", "建筑安装业")
    E50 = IndustryCodeItem("E50", "建筑装饰和其他建筑业")
    F51 = IndustryCodeItem("F51", "批发业")
    F52 = IndustryCodeItem("F52", "零售业")
    G53 = IndustryCodeItem("G53", "铁路运输业")
    G54 = IndustryCodeItem("G54", "道路运输业")
    G55 = IndustryCodeItem("G55", "水上运输业")
    G56 = IndustryCodeItem("G56", "航空运输业")
    G57 = IndustryCodeItem("G57", "管道运输业")
    G58 = IndustryCodeItem("G58", "装卸搬运和运输代理业")
    G59 = IndustryCodeItem("G59", "仓储业")
    G60 = IndustryCodeItem("G60", "邮政业")
    H61 = IndustryCodeItem("H61", "住宿业")
    H62 = IndustryCodeItem("H62", "餐饮业")
    I63 = IndustryCodeItem("I63", "电信、广播电视和卫星传输服务")
    I64 = IndustryCodeItem("I64", "互联网和相关服务")
    I65 = IndustryCodeItem("I65", "软件和信息技术服务业")
    J66 = IndustryCodeItem("J66", "货币金融服务")
    J67 = IndustryCodeItem("J67", "资本市场服务")
    J68 = IndustryCodeItem("J68", "保险业")
    J69 = IndustryCodeItem("J69", "其他金融业")
    K70 = IndustryCodeItem("K70", "房地产业")
    L71 = IndustryCodeItem("L71", "租赁业")
    L72 = IndustryCodeItem("L72", "商务服务业")
    M73 = IndustryCodeItem("M73", "研究和试验发展")
    M74 = IndustryCodeItem("M74", "专业技术服务业")
    M75 = IndustryCodeItem("M75", "科技推广和应用服务业")
    N76 = IndustryCodeItem("N76", "水利管理业")
    N77 = IndustryCodeItem("N77", "生态保护和环境治理业")
    N78 = IndustryCodeItem("N78", "公共设施管理业")
    O79 = IndustryCodeItem("O79", "居民服务业")
    O80 = IndustryCodeItem("O80", "机动车、电子产品和日用产品修理业")
    O81 = IndustryCodeItem("O81", "其他服务业")
    P82 = IndustryCodeItem("P82", "教育")
    Q83 = IndustryCodeItem("Q83", "卫生")
    Q84 = IndustryCodeItem("Q84", "社会工作")
    R85 = IndustryCodeItem("R85", "新闻和出版业")
    R86 = IndustryCodeItem("R86", "广播、电视、电影和影视录音制作业")
    R87 = IndustryCodeItem("R87", "文化艺术业")
    R88 = IndustryCodeItem("R88", "体育")
    R89 = IndustryCodeItem("R89", "娱乐业")
    S90 = IndustryCodeItem("S90", "综合")
