#
# Copyright 2016 Quantopian, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from datetime import timedelta, time
from itertools import chain

from nose_parameterized import parameterized
import numpy as np
from numpy import nan
from numpy.testing import assert_almost_equal
import pandas as pd
from toolz import concat
from trading_calendars import get_calendar
from trading_calendars.utils.pandas_utils import days_at_time

from zipline._protocol import handle_non_market_minutes

from zipline.finance.asset_restrictions import (
    Restriction,
    HistoricalRestrictions,
    RESTRICTION_STATES,
)
from zipline.testing import (
    MockDailyBarReader,
    create_daily_df_for_asset,
    create_minute_df_for_asset,
    str_to_seconds,
)
from zipline.testing.fixtures import (
    WithCreateBarData,
    WithDataPortal,
    ZiplineTestCase,
)

OHLC = ["open", "high", "low", "close"]
OHLCP = OHLC + ["price"]
ALL_FIELDS = OHLCP + ["volume", "last_traded"]

# offsets used in test data
field_info = {
    "open": 1,
    "high": 2,
    "low": -1,
    "close": 0
}


def str_to_ts(dt_str):
    return pd.Timestamp(dt_str, tz='UTC')


class WithBarDataChecks(object):
    def assert_same(self, val1, val2):
        try:
            self.assertEqual(val1, val2)
        except AssertionError:
            if val1 is pd.NaT:
                self.assertTrue(val2 is pd.NaT)
            elif np.isnan(val1):
                self.assertTrue(np.isnan(val2))
            else:
                raise

    def check_internal_consistency(self, bar_data):
        df = bar_data.current([self.ASSET1, self.ASSET2], ALL_FIELDS)

        asset1_multi_field = bar_data.current(self.ASSET1, ALL_FIELDS)
        asset2_multi_field = bar_data.current(self.ASSET2, ALL_FIELDS)

        for field in ALL_FIELDS:
            asset1_value = bar_data.current(self.ASSET1, field)
            asset2_value = bar_data.current(self.ASSET2, field)

            multi_asset_series = bar_data.current(
                [self.ASSET1, self.ASSET2], field
            )

            # make sure all the different query forms are internally
            # consistent
            self.assert_same(multi_asset_series.loc[self.ASSET1], asset1_value)
            self.assert_same(multi_asset_series.loc[self.ASSET2], asset2_value)

            self.assert_same(df.loc[self.ASSET1][field], asset1_value)
            self.assert_same(df.loc[self.ASSET2][field], asset2_value)

            self.assert_same(asset1_multi_field[field], asset1_value)
            self.assert_same(asset2_multi_field[field], asset2_value)

        # also verify that bar_data doesn't expose anything bad
        for field in ["data_portal", "simulation_dt_func", "data_frequency",
                      "_views", "_universe_func", "_last_calculated_universe",
                      "_universe_last_updatedat"]:
            with self.assertRaises(AttributeError):
                getattr(bar_data, field)


class TestMinuteBarData(WithCreateBarData,
                        WithBarDataChecks,
                        WithDataPortal,
                        ZiplineTestCase):
    START_DATE = pd.Timestamp('2016-01-05', tz='UTC')
    END_DATE = ASSET_FINDER_EQUITY_END_DATE = pd.Timestamp(
        '2016-01-07',
        tz='UTC',
    )

    ASSET_FINDER_EQUITY_SIDS = 1, 2, 3, 4, 5

    SPLIT_ASSET_SID = 3
    ILLIQUID_SPLIT_ASSET_SID = 4
    HILARIOUSLY_ILLIQUID_ASSET_SID = 5

    @classmethod
    def make_equity_minute_bar_data(cls):
        # asset1 has trades every minute
        # asset2 has trades every 10 minutes
        # split_asset trades every minute
        # illiquid_split_asset trades every 10 minutes
        for sid in (1, cls.SPLIT_ASSET_SID):
            yield sid, create_minute_df_for_asset(
                cls.trading_calendar,
                cls.equity_minute_bar_days[0],
                cls.equity_minute_bar_days[-1],
            )

        for sid in (2, cls.ILLIQUID_SPLIT_ASSET_SID):
            yield sid, create_minute_df_for_asset(
                cls.trading_calendar,
                cls.equity_minute_bar_days[0],
                cls.equity_minute_bar_days[-1],
                10,
            )

        yield cls.HILARIOUSLY_ILLIQUID_ASSET_SID, create_minute_df_for_asset(
            cls.trading_calendar,
            cls.equity_minute_bar_days[0],
            cls.equity_minute_bar_days[-1],
            50,
        )

    @classmethod
    def make_futures_info(cls):
        return pd.DataFrame.from_dict(
            {
                6: {
                    'symbol': 'CLG06',
                    'root_symbol': 'CL',
                    'start_date': pd.Timestamp('2005-12-01', tz='UTC'),
                    'notice_date': pd.Timestamp('2005-12-20', tz='UTC'),
                    'expiration_date': pd.Timestamp('2006-01-20', tz='UTC'),
                    'exchange': 'ICEUS',
                },
                7: {
                    'symbol': 'CLK06',
                    'root_symbol': 'CL',
                    'start_date': pd.Timestamp('2005-12-01', tz='UTC'),
                    'notice_date': pd.Timestamp('2006-03-20', tz='UTC'),
                    'expiration_date': pd.Timestamp('2006-04-20', tz='UTC'),
                    'exchange': 'ICEUS',
                },
            },
            orient='index',
        )

    @classmethod
    def make_splits_data(cls):
        return pd.DataFrame([
            {
                'effective_date': str_to_seconds("2016-01-06"),
                'ratio': 0.5,
                'sid': cls.SPLIT_ASSET_SID,
            },
            {
                'effective_date': str_to_seconds("2016-01-06"),
                'ratio': 0.5,
                'sid': cls.ILLIQUID_SPLIT_ASSET_SID,
            },
        ])

    @classmethod
    def init_class_fixtures(cls):
        super(TestMinuteBarData, cls).init_class_fixtures()

        cls.ASSET1 = cls.asset_finder.retrieve_asset(1)
        cls.ASSET2 = cls.asset_finder.retrieve_asset(2)
        cls.SPLIT_ASSET = cls.asset_finder.retrieve_asset(
            cls.SPLIT_ASSET_SID,
        )
        cls.ILLIQUID_SPLIT_ASSET = cls.asset_finder.retrieve_asset(
            cls.ILLIQUID_SPLIT_ASSET_SID,
        )
        cls.HILARIOUSLY_ILLIQUID_ASSET = cls.asset_finder.retrieve_asset(
            cls.HILARIOUSLY_ILLIQUID_ASSET_SID,
        )

        cls.ASSETS = [cls.ASSET1, cls.ASSET2]

    def test_current_session(self):
        regular_minutes = self.trading_calendar.minutes_for_sessions_in_range(
            self.equity_minute_bar_days[0],
            self.equity_minute_bar_days[-1]
        )

        bts_minutes = days_at_time(
            self.equity_minute_bar_days,
            time(8, 45),
            "US/Eastern"
        )

        # some other non-market-minute
        three_oh_six_am_minutes = days_at_time(
            self.equity_minute_bar_days,
            time(3, 6),
            "US/Eastern"
        )

        all_minutes = [regular_minutes, bts_minutes, three_oh_six_am_minutes]
        for minute in list(concat(all_minutes)):
            bar_data = self.create_bardata(lambda: minute)

            self.assertEqual(
                self.trading_calendar.minute_to_session_label(minute),
                bar_data.current_session
            )

    def test_current_session_minutes(self):
        first_day_minutes = self.trading_calendar.minutes_for_session(
            self.equity_minute_bar_days[0]
        )

        for minute in first_day_minutes:
            bar_data = self.create_bardata(lambda: minute)
            np.testing.assert_array_equal(
                first_day_minutes,
                bar_data.current_session_minutes
            )

    def test_minute_before_assets_trading(self):
        # grab minutes that include the day before the asset start
        minutes = self.trading_calendar.minutes_for_session(
            self.trading_calendar.previous_session_label(
                self.equity_minute_bar_days[0]
            )
        )

        # this entire day is before either asset has started trading
        for idx, minute in enumerate(minutes):
            bar_data = self.create_bardata(
                lambda: minute,
            )
            self.check_internal_consistency(bar_data)

            self.assertFalse(bar_data.can_trade(self.ASSET1))
            self.assertFalse(bar_data.can_trade(self.ASSET2))

            self.assertFalse(bar_data.is_stale(self.ASSET1))
            self.assertFalse(bar_data.is_stale(self.ASSET2))

            for field in ALL_FIELDS:
                for asset in self.ASSETS:
                    asset_value = bar_data.current(asset, field)

                    if field in OHLCP:
                        self.assertTrue(np.isnan(asset_value))
                    elif field == "volume":
                        self.assertEqual(0, asset_value)
                    elif field == "last_traded":
                        self.assertTrue(asset_value is pd.NaT)

    def test_regular_minute(self):
        minutes = self.trading_calendar.minutes_for_session(
            self.equity_minute_bar_days[0]
        )

        for idx, minute in enumerate(minutes):
            # day2 has prices
            # (every minute for asset1, every 10 minutes for asset2)

            # asset1:
            # opens: 2-391
            # high: 3-392
            # low: 0-389
            # close: 1-390
            # volume: 100-3900 (by 100)

            # asset2 is the same thing, but with only every 10th minute
            # populated.

            # this test covers the "IPO morning" case, because asset2 only
            # has data starting on the 10th minute.

            bar_data = self.create_bardata(
                lambda: minute,
            )
            self.check_internal_consistency(bar_data)
            asset2_has_data = (((idx + 1) % 10) == 0)

            self.assertTrue(bar_data.can_trade(self.ASSET1))
            self.assertFalse(bar_data.is_stale(self.ASSET1))

            if idx < 9:
                self.assertFalse(bar_data.can_trade(self.ASSET2))
                self.assertFalse(bar_data.is_stale(self.ASSET2))
            else:
                self.assertTrue(bar_data.can_trade(self.ASSET2))

                if asset2_has_data:
                    self.assertFalse(bar_data.is_stale(self.ASSET2))
                else:
                    self.assertTrue(bar_data.is_stale(self.ASSET2))

            for field in ALL_FIELDS:
                asset1_value = bar_data.current(self.ASSET1, field)
                asset2_value = bar_data.current(self.ASSET2, field)

                # now check the actual values
                if idx == 0 and field == "low":
                    # first low value is 0, which is interpreted as NaN
                    self.assertTrue(np.isnan(asset1_value))
                else:
                    if field in OHLC:
                        self.assertEqual(
                            idx + 1 + field_info[field],
                            asset1_value
                        )

                        if asset2_has_data:
                            self.assertEqual(
                                idx + 1 + field_info[field],
                                asset2_value
                            )
                        else:
                            self.assertTrue(np.isnan(asset2_value))
                    elif field == "volume":
                        self.assertEqual((idx + 1) * 100, asset1_value)

                        if asset2_has_data:
                            self.assertEqual((idx + 1) * 100, asset2_value)
                        else:
                            self.assertEqual(0, asset2_value)
                    elif field == "price":
                        self.assertEqual(idx + 1, asset1_value)

                        if asset2_has_data:
                            self.assertEqual(idx + 1, asset2_value)
                        elif idx < 9:
                            # no price to forward fill from
                            self.assertTrue(np.isnan(asset2_value))
                        else:
                            # forward-filled price
                            self.assertEqual((idx // 10) * 10, asset2_value)
                    elif field == "last_traded":
                        self.assertEqual(minute, asset1_value)

                        if idx < 9:
                            self.assertTrue(asset2_value is pd.NaT)
                        elif asset2_has_data:
                            self.assertEqual(minute, asset2_value)
                        else:
                            last_traded_minute = minutes[(idx // 10) * 10]
                            self.assertEqual(
                                last_traded_minute - timedelta(minutes=1),
                                asset2_value
                            )

    def test_minute_of_last_day(self):
        minutes = self.trading_calendar.minutes_for_session(
            self.equity_daily_bar_days[-1],
        )

        # this is the last day the assets exist
        for idx, minute in enumerate(minutes):
            bar_data = self.create_bardata(
                lambda: minute,
            )

            self.assertTrue(bar_data.can_trade(self.ASSET1))
            self.assertTrue(bar_data.can_trade(self.ASSET2))

    def test_minute_after_assets_stopped(self):
        minutes = self.trading_calendar.minutes_for_session(
            self.trading_calendar.next_session_label(
                self.equity_minute_bar_days[-1]
            )
        )

        last_trading_minute = self.trading_calendar.minutes_for_session(
            self.equity_minute_bar_days[-1]
        )[-1]

        # this entire day is after both assets have stopped trading
        for idx, minute in enumerate(minutes):
            bar_data = self.create_bardata(
                lambda: minute,
            )

            self.assertFalse(bar_data.can_trade(self.ASSET1))
            self.assertFalse(bar_data.can_trade(self.ASSET2))

            self.assertFalse(bar_data.is_stale(self.ASSET1))
            self.assertFalse(bar_data.is_stale(self.ASSET2))

            self.check_internal_consistency(bar_data)

            for field in ALL_FIELDS:
                for asset in self.ASSETS:
                    asset_value = bar_data.current(asset, field)

                    if field in OHLCP:
                        self.assertTrue(np.isnan(asset_value))
                    elif field == "volume":
                        self.assertEqual(0, asset_value)
                    elif field == "last_traded":
                        self.assertEqual(last_trading_minute, asset_value)

    def test_get_value_is_unadjusted(self):
        # verify there is a split for SPLIT_ASSET
        splits = self.adjustment_reader.get_adjustments_for_sid(
            "splits",
            self.SPLIT_ASSET.sid
        )

        self.assertEqual(1, len(splits))
        split = splits[0]
        self.assertEqual(
            split[0],
            pd.Timestamp("2016-01-06", tz='UTC')
        )

        # ... but that's it's not applied when using spot value
        minutes = self.trading_calendar.minutes_for_sessions_in_range(
            self.equity_minute_bar_days[0],
            self.equity_minute_bar_days[1]
        )

        for idx, minute in enumerate(minutes):
            bar_data = self.create_bardata(
                lambda: minute,
            )
            self.assertEqual(
                idx + 1,
                bar_data.current(self.SPLIT_ASSET, "price")
            )

    def test_get_value_is_adjusted_if_needed(self):
        # on cls.days[1], the first 9 minutes of ILLIQUID_SPLIT_ASSET are
        # missing. let's get them.
        day0_minutes = self.trading_calendar.minutes_for_session(
            self.equity_minute_bar_days[0]
        )
        day1_minutes = self.trading_calendar.minutes_for_session(
            self.equity_minute_bar_days[1]
        )

        for idx, minute in enumerate(day0_minutes[-10:-1]):
            bar_data = self.create_bardata(
                lambda: minute,
            )
            self.assertEqual(
                380,
                bar_data.current(self.ILLIQUID_SPLIT_ASSET, "price")
            )

        bar_data = self.create_bardata(
            lambda: day0_minutes[-1],
        )

        self.assertEqual(
            390,
            bar_data.current(self.ILLIQUID_SPLIT_ASSET, "price")
        )

        for idx, minute in enumerate(day1_minutes[0:9]):
            bar_data = self.create_bardata(
                lambda: minute,
            )

            # should be half of 390, due to the split
            self.assertEqual(
                195,
                bar_data.current(self.ILLIQUID_SPLIT_ASSET, "price")
            )

    def test_get_value_at_midnight(self):
        # make sure that if we try to get a minute price at a non-market
        # minute, we use the previous market close's timestamp
        day = self.equity_minute_bar_days[1]

        eight_fortyfive_am_eastern = \
            pd.Timestamp("{0}-{1}-{2} 8:45".format(
                day.year, day.month, day.day),
                tz='US/Eastern'
            )

        bar_data = self.create_bardata(
            lambda: day,
        )
        bar_data2 = self.create_bardata(
            lambda: eight_fortyfive_am_eastern,
        )

        with handle_non_market_minutes(bar_data), \
                handle_non_market_minutes(bar_data2):
            for bd in [bar_data, bar_data2]:
                for field in ["close", "price"]:
                    self.assertEqual(
                        390,
                        bd.current(self.ASSET1, field)
                    )

                # make sure that if the asset didn't trade at the previous
                # close, we properly ffill (or not ffill)
                self.assertEqual(
                    350,
                    bd.current(self.HILARIOUSLY_ILLIQUID_ASSET, "price")
                )

                self.assertTrue(
                    np.isnan(bd.current(self.HILARIOUSLY_ILLIQUID_ASSET,
                                        "high"))
                )

                self.assertEqual(
                    0,
                    bd.current(self.HILARIOUSLY_ILLIQUID_ASSET, "volume")
                )

    def test_get_value_during_non_market_hours(self):
        # make sure that if we try to get the OHLCV values of ASSET1 during
        # non-market hours, we don't get the previous market minute's values

        bar_data = self.create_bardata(
            simulation_dt_func=lambda:
            pd.Timestamp("2016-01-06 4:15", tz="US/Eastern"),
        )

        self.assertTrue(np.isnan(bar_data.current(self.ASSET1, "open")))
        self.assertTrue(np.isnan(bar_data.current(self.ASSET1, "high")))
        self.assertTrue(np.isnan(bar_data.current(self.ASSET1, "low")))
        self.assertTrue(np.isnan(bar_data.current(self.ASSET1, "close")))
        self.assertEqual(0, bar_data.current(self.ASSET1, "volume"))

        # price should still forward fill
        self.assertEqual(390, bar_data.current(self.ASSET1, "price"))

    def test_can_trade_equity_same_cal_outside_lifetime(self):

        # verify that can_trade returns False for the session before the
        # asset's first session
        session_before_asset1_start = \
            self.trading_calendar.previous_session_label(
                self.ASSET1.start_date
            )
        minutes_for_session = self.trading_calendar.minutes_for_session(
            session_before_asset1_start
        )

        # for good measure, check the minute before the session too
        minutes_to_check = chain(
            [minutes_for_session[0] - pd.Timedelta(minutes=1)],
            minutes_for_session
        )

        for minute in minutes_to_check:
            bar_data = self.create_bardata(
                simulation_dt_func=lambda: minute,
            )

            self.assertFalse(bar_data.can_trade(self.ASSET1))

        # after asset lifetime
        session_after_asset1_end = self.trading_calendar.next_session_label(
            self.ASSET1.end_date
        )
        bts_after_asset1_end = session_after_asset1_end.replace(
            hour=8, minute=45
        ).tz_convert(None).tz_localize("US/Eastern")

        minutes_to_check = chain(
            self.trading_calendar.minutes_for_session(
                session_after_asset1_end
            ),
            [bts_after_asset1_end]
        )

        for minute in minutes_to_check:
            bar_data = self.create_bardata(
                simulation_dt_func=lambda: minute,
            )

            self.assertFalse(bar_data.can_trade(self.ASSET1))

    def test_can_trade_equity_same_cal_exchange_closed(self):
        # verify that can_trade returns true for minutes that are
        # outside the asset's calendar (assuming the asset is alive and
        # there is a last price), because the asset is alive on the
        # next market minute.
        minutes = self.trading_calendar.minutes_for_sessions_in_range(
            self.ASSET1.start_date,
            self.ASSET1.end_date
        )

        for minute in minutes:
            bar_data = self.create_bardata(
                simulation_dt_func=lambda: minute,
            )

            self.assertTrue(bar_data.can_trade(self.ASSET1))

    def test_can_trade_equity_same_cal_no_last_price(self):
        # self.HILARIOUSLY_ILLIQUID_ASSET's first trade is at
        # 2016-01-05 15:20:00+00:00.  Make sure that can_trade returns false
        # for all minutes in that session before the first trade, and true
        # for all minutes afterwards.

        minutes_in_session = \
            self.trading_calendar.minutes_for_session(self.ASSET1.start_date)

        for minute in minutes_in_session[0:49]:
            bar_data = self.create_bardata(
                simulation_dt_func=lambda: minute,
            )

            self.assertFalse(bar_data.can_trade(
                self.HILARIOUSLY_ILLIQUID_ASSET)
            )

        for minute in minutes_in_session[50:]:
            bar_data = self.create_bardata(
                simulation_dt_func=lambda: minute,
            )

            self.assertTrue(bar_data.can_trade(
                self.HILARIOUSLY_ILLIQUID_ASSET)
            )

    def test_is_stale_during_non_market_hours(self):
        bar_data = self.create_bardata(
            lambda: self.equity_minute_bar_days[1],
        )

        with handle_non_market_minutes(bar_data):
            self.assertTrue(bar_data.is_stale(self.HILARIOUSLY_ILLIQUID_ASSET))

    def test_overnight_adjustments(self):
        # verify there is a split for SPLIT_ASSET
        splits = self.adjustment_reader.get_adjustments_for_sid(
            "splits",
            self.SPLIT_ASSET.sid
        )

        self.assertEqual(1, len(splits))
        split = splits[0]
        self.assertEqual(
            split[0],
            pd.Timestamp("2016-01-06", tz='UTC')
        )

        # Current day is 1/06/16
        day = self.equity_daily_bar_days[1]
        eight_fortyfive_am_eastern = \
            pd.Timestamp("{0}-{1}-{2} 8:45".format(
                day.year, day.month, day.day),
                tz='US/Eastern'
            )

        bar_data = self.create_bardata(
            lambda: eight_fortyfive_am_eastern,
        )

        expected = {
            'open': 391 / 2.0,
            'high': 392 / 2.0,
            'low': 389 / 2.0,
            'close': 390 / 2.0,
            'volume': 39000 * 2.0,
            'price': 390 / 2.0,
        }

        with handle_non_market_minutes(bar_data):
            for field in OHLCP + ['volume']:
                value = bar_data.current(self.SPLIT_ASSET, field)

                # Assert the price is adjusted for the overnight split
                self.assertEqual(value, expected[field])

    def test_can_trade_restricted(self):
        """
        Test that can_trade will return False for a sid if it is restricted
        on that dt
        """

        minutes_to_check = [
            (str_to_ts("2016-01-05 14:31"), False),
            (str_to_ts("2016-01-06 14:31"), False),
            (str_to_ts("2016-01-07 14:31"), True),
            (str_to_ts("2016-01-07 15:00"), False),
            (str_to_ts("2016-01-07 15:30"), True),
        ]

        rlm = HistoricalRestrictions([
            Restriction(1, str_to_ts('2016-01-05'),
                        RESTRICTION_STATES.FROZEN),
            Restriction(1, str_to_ts('2016-01-07'),
                        RESTRICTION_STATES.ALLOWED),
            Restriction(1, str_to_ts('2016-01-07 15:00'),
                        RESTRICTION_STATES.FROZEN),
            Restriction(1, str_to_ts('2016-01-07 15:30'),
                        RESTRICTION_STATES.ALLOWED),
        ])

        for info in minutes_to_check:
            bar_data = self.create_bardata(
                simulation_dt_func=lambda: info[0],
                restrictions=rlm,
            )
            self.assertEqual(bar_data.can_trade(self.ASSET1), info[1])


class TestMinuteBarDataFuturesCalendar(WithCreateBarData,
                                       WithBarDataChecks,
                                       ZiplineTestCase):

    START_DATE = pd.Timestamp('2016-01-05', tz='UTC')
    END_DATE = ASSET_FINDER_EQUITY_END_DATE = pd.Timestamp(
        '2016-01-07',
        tz='UTC',
    )

    ASSET_FINDER_EQUITY_SIDS = [1]

    @classmethod
    def make_equity_minute_bar_data(cls):
        # asset1 has trades every minute
        yield 1, create_minute_df_for_asset(
            cls.trading_calendar,
            cls.equity_minute_bar_days[0],
            cls.equity_minute_bar_days[-1],
        )

    @classmethod
    def make_futures_info(cls):
        return pd.DataFrame.from_dict(
            {
                6: {
                    'symbol': 'CLH16',
                    'root_symbol': 'CL',
                    'start_date': pd.Timestamp('2016-01-04', tz='UTC'),
                    'notice_date': pd.Timestamp('2016-01-19', tz='UTC'),
                    'expiration_date': pd.Timestamp('2016-02-19', tz='UTC'),
                    'exchange': 'ICEUS',
                },
                7: {
                    'symbol': 'FVH16',
                    'root_symbol': 'FV',
                    'start_date': pd.Timestamp('2016-01-04', tz='UTC'),
                    'notice_date': pd.Timestamp('2016-01-22', tz='UTC'),
                    'expiration_date': pd.Timestamp('2016-02-22', tz='UTC'),
                    'auto_close_date': pd.Timestamp('2016-01-20', tz='UTC'),
                    'exchange': 'CMES',
                },
            },
            orient='index',
        )

    @classmethod
    def init_class_fixtures(cls):
        super(TestMinuteBarDataFuturesCalendar, cls).init_class_fixtures()
        cls.trading_calendar = get_calendar('CMES')

    def test_can_trade_multiple_exchange_closed(self):
        nyse_asset = self.asset_finder.retrieve_asset(1)
        ice_asset = self.asset_finder.retrieve_asset(6)

        # minutes we're going to check (to verify that that the same bardata
        # can check multiple exchange calendars, all times Eastern):
        # 2016-01-05:
        # 20:00 (minute before ICE opens)
        # 20:01 (first minute of ICE session)
        # 20:02 (second minute of ICE session)
        # 00:00 (Cinderella's ride becomes a pumpkin)
        # 2016-01-06:
        # 9:30 (minute before NYSE opens)
        # 9:31 (first minute of NYSE session)
        # 9:32 (second minute of NYSE session)
        # 15:59 (second-to-last minute of NYSE session)
        # 16:00 (last minute of NYSE session)
        # 16:01 (minute after NYSE closed)
        # 17:59 (second-to-last minute of ICE session)
        # 18:00 (last minute of ICE session)
        # 18:01 (minute after ICE closed)

        # each row is dt, whether-nyse-is-open, whether-ice-is-open
        minutes_to_check = [
            (pd.Timestamp("2016-01-05 20:00", tz="US/Eastern"), False, False),
            (pd.Timestamp("2016-01-05 20:01", tz="US/Eastern"), False, True),
            (pd.Timestamp("2016-01-05 20:02", tz="US/Eastern"), False, True),
            (pd.Timestamp("2016-01-06 00:00", tz="US/Eastern"), False, True),
            (pd.Timestamp("2016-01-06 9:30", tz="US/Eastern"), False, True),
            (pd.Timestamp("2016-01-06 9:31", tz="US/Eastern"), True, True),
            (pd.Timestamp("2016-01-06 9:32", tz="US/Eastern"), True, True),
            (pd.Timestamp("2016-01-06 15:59", tz="US/Eastern"), True, True),
            (pd.Timestamp("2016-01-06 16:00", tz="US/Eastern"), True, True),
            (pd.Timestamp("2016-01-06 16:01", tz="US/Eastern"), False, True),
            (pd.Timestamp("2016-01-06 17:59", tz="US/Eastern"), False, True),
            (pd.Timestamp("2016-01-06 18:00", tz="US/Eastern"), False, True),
            (pd.Timestamp("2016-01-06 18:01", tz="US/Eastern"), False, False),
        ]

        for info in minutes_to_check:
            # use the CMES calendar, which covers 24 hours
            bar_data = self.create_bardata(
                simulation_dt_func=lambda: info[0],
            )

            series = bar_data.can_trade([nyse_asset, ice_asset])

            self.assertEqual(info[1], series.loc[nyse_asset])
            self.assertEqual(info[2], series.loc[ice_asset])

    def test_can_trade_delisted(self):
        """
        Test that can_trade returns False for an asset after its auto close
        date.
        """
        auto_closing_asset = self.asset_finder.retrieve_asset(7)

        # Our asset's auto close date is 2016-01-20, which means that as of the
        # market open for the 2016-01-21 session, `can_trade` should return
        # False.
        minutes_to_check = [
            (pd.Timestamp('2016-01-20 00:00:00', tz='UTC'), True),
            (pd.Timestamp('2016-01-20 23:00:00', tz='UTC'), True),
            (pd.Timestamp('2016-01-20 23:01:00', tz='UTC'), False),
            (pd.Timestamp('2016-01-20 23:59:00', tz='UTC'), False),
            (pd.Timestamp('2016-01-21 00:00:00', tz='UTC'), False),
            (pd.Timestamp('2016-01-21 00:01:00', tz='UTC'), False),
            (pd.Timestamp('2016-01-22 00:00:00', tz='UTC'), False),
        ]

        for info in minutes_to_check:
            bar_data = self.create_bardata(simulation_dt_func=lambda: info[0])
            self.assertEqual(bar_data.can_trade(auto_closing_asset), info[1])


class TestDailyBarData(WithCreateBarData,
                       WithBarDataChecks,
                       WithDataPortal,
                       ZiplineTestCase):
    START_DATE = pd.Timestamp('2016-01-05', tz='UTC')
    END_DATE = ASSET_FINDER_EQUITY_END_DATE = pd.Timestamp(
        '2016-01-11',
        tz='UTC',
    )
    CREATE_BARDATA_DATA_FREQUENCY = 'daily'

    ASSET_FINDER_EQUITY_SIDS = set(range(1, 9))

    SPLIT_ASSET_SID = 3
    ILLIQUID_SPLIT_ASSET_SID = 4
    MERGER_ASSET_SID = 5
    ILLIQUID_MERGER_ASSET_SID = 6
    DIVIDEND_ASSET_SID = 7
    ILLIQUID_DIVIDEND_ASSET_SID = 8

    @classmethod
    def make_equity_info(cls):
        frame = super(TestDailyBarData, cls).make_equity_info()
        frame.loc[[1, 2], 'end_date'] = pd.Timestamp('2016-01-08', tz='UTC')
        return frame

    @classmethod
    def make_splits_data(cls):
        return pd.DataFrame.from_records([
            {
                'effective_date': str_to_seconds("2016-01-06"),
                'ratio': 0.5,
                'sid': cls.SPLIT_ASSET_SID,
            },
            {
                'effective_date': str_to_seconds("2016-01-07"),
                'ratio': 0.5,
                'sid': cls.ILLIQUID_SPLIT_ASSET_SID,
            },
        ])

    @classmethod
    def make_mergers_data(cls):
        return pd.DataFrame.from_records([
            {
                'effective_date': str_to_seconds('2016-01-06'),
                'ratio': 0.5,
                'sid': cls.MERGER_ASSET_SID,
            },
            {
                'effective_date': str_to_seconds('2016-01-07'),
                'ratio': 0.6,
                'sid': cls.ILLIQUID_MERGER_ASSET_SID,
            }
        ])

    @classmethod
    def make_dividends_data(cls):
        return pd.DataFrame.from_records([
            {
                # only care about ex date, the other dates don't matter here
                'ex_date':
                    pd.Timestamp('2016-01-06', tz='UTC').to_datetime64(),
                'record_date':
                    pd.Timestamp('2016-01-06', tz='UTC').to_datetime64(),
                'declared_date':
                    pd.Timestamp('2016-01-06', tz='UTC').to_datetime64(),
                'pay_date':
                    pd.Timestamp('2016-01-06', tz='UTC').to_datetime64(),
                'amount': 2.0,
                'sid': cls.DIVIDEND_ASSET_SID,
            },
            {
                'ex_date':
                    pd.Timestamp('2016-01-07', tz='UTC').to_datetime64(),
                'record_date':
                    pd.Timestamp('2016-01-07', tz='UTC').to_datetime64(),
                'declared_date':
                    pd.Timestamp('2016-01-07', tz='UTC').to_datetime64(),
                'pay_date':
                    pd.Timestamp('2016-01-07', tz='UTC').to_datetime64(),
                'amount': 4.0,
                'sid': cls.ILLIQUID_DIVIDEND_ASSET_SID,
            }],
            columns=[
                'ex_date',
                'record_date',
                'declared_date',
                'pay_date',
                'amount',
                'sid',
            ]
        )

    @classmethod
    def make_adjustment_writer_equity_daily_bar_reader(cls):
        return MockDailyBarReader(
            dates=cls.trading_calendar.sessions_in_range(
                cls.START_DATE,
                cls.END_DATE,
            ),
        )

    @classmethod
    def make_equity_daily_bar_data(cls, country_code, sids):
        for sid in sids:
            asset = cls.asset_finder.retrieve_asset(sid)
            yield sid, create_daily_df_for_asset(
                cls.trading_calendar,
                asset.start_date,
                asset.end_date,
                interval=2 - sid % 2
            )

    @classmethod
    def init_class_fixtures(cls):
        super(TestDailyBarData, cls).init_class_fixtures()

        cls.ASSET1 = cls.asset_finder.retrieve_asset(1)
        cls.ASSET2 = cls.asset_finder.retrieve_asset(2)
        cls.SPLIT_ASSET = cls.asset_finder.retrieve_asset(
            cls.SPLIT_ASSET_SID,
        )
        cls.ILLIQUID_SPLIT_ASSET = cls.asset_finder.retrieve_asset(
            cls.ILLIQUID_SPLIT_ASSET_SID,
        )
        cls.MERGER_ASSET = cls.asset_finder.retrieve_asset(
            cls.MERGER_ASSET_SID,
        )
        cls.ILLIQUID_MERGER_ASSET = cls.asset_finder.retrieve_asset(
            cls.ILLIQUID_MERGER_ASSET_SID,
        )
        cls.DIVIDEND_ASSET = cls.asset_finder.retrieve_asset(
            cls.DIVIDEND_ASSET_SID,
        )
        cls.ILLIQUID_DIVIDEND_ASSET = cls.asset_finder.retrieve_asset(
            cls.ILLIQUID_DIVIDEND_ASSET_SID,
        )
        cls.ASSETS = [cls.ASSET1, cls.ASSET2]

    def get_last_minute_of_session(self, session_label):
        return self.trading_calendar.open_and_close_for_session(
            session_label
        )[1]

    def test_current_session(self):
        for session in self.trading_calendar.sessions_in_range(
            self.equity_daily_bar_days[0],
            self.equity_daily_bar_days[-1]
        ):
            bar_data = self.create_bardata(
                simulation_dt_func=lambda: self.get_last_minute_of_session(
                    session
                )
            )

            self.assertEqual(session, bar_data.current_session)

    def test_day_before_assets_trading(self):
        # use the day before self.bcolz_daily_bar_days[0]
        minute = self.get_last_minute_of_session(
            self.trading_calendar.previous_session_label(
                self.equity_daily_bar_days[0]
            )
        )

        bar_data = self.create_bardata(
            simulation_dt_func=lambda: minute,
        )
        self.check_internal_consistency(bar_data)

        self.assertFalse(bar_data.can_trade(self.ASSET1))
        self.assertFalse(bar_data.can_trade(self.ASSET2))

        self.assertFalse(bar_data.is_stale(self.ASSET1))
        self.assertFalse(bar_data.is_stale(self.ASSET2))

        for field in ALL_FIELDS:
            for asset in self.ASSETS:
                asset_value = bar_data.current(asset, field)

                if field in OHLCP:
                    self.assertTrue(np.isnan(asset_value))
                elif field == "volume":
                    self.assertEqual(0, asset_value)
                elif field == "last_traded":
                    self.assertTrue(asset_value is pd.NaT)

    def test_semi_active_day(self):
        # on self.equity_daily_bar_days[0], only asset1 has data
        bar_data = self.create_bardata(
            simulation_dt_func=lambda: self.get_last_minute_of_session(
                self.equity_daily_bar_days[0]
            ),
        )
        self.check_internal_consistency(bar_data)

        self.assertTrue(bar_data.can_trade(self.ASSET1))
        self.assertFalse(bar_data.can_trade(self.ASSET2))

        # because there is real data
        self.assertFalse(bar_data.is_stale(self.ASSET1))

        # because there has never been a trade bar yet
        self.assertFalse(bar_data.is_stale(self.ASSET2))

        self.assertEqual(3, bar_data.current(self.ASSET1, "open"))
        self.assertEqual(4, bar_data.current(self.ASSET1, "high"))
        self.assertEqual(1, bar_data.current(self.ASSET1, "low"))
        self.assertEqual(2, bar_data.current(self.ASSET1, "close"))
        self.assertEqual(200, bar_data.current(self.ASSET1, "volume"))
        self.assertEqual(2, bar_data.current(self.ASSET1, "price"))
        self.assertEqual(self.equity_daily_bar_days[0],
                         bar_data.current(self.ASSET1, "last_traded"))

        for field in OHLCP:
            self.assertTrue(np.isnan(bar_data.current(self.ASSET2, field)),
                            field)

        self.assertEqual(0, bar_data.current(self.ASSET2, "volume"))
        self.assertTrue(
            bar_data.current(self.ASSET2, "last_traded") is pd.NaT
        )

    def test_fully_active_day(self):
        bar_data = self.create_bardata(
            simulation_dt_func=lambda: self.get_last_minute_of_session(
                self.equity_daily_bar_days[1]
            ),
        )
        self.check_internal_consistency(bar_data)

        # on self.equity_daily_bar_days[1], both assets have data
        for asset in self.ASSETS:
            self.assertTrue(bar_data.can_trade(asset))
            self.assertFalse(bar_data.is_stale(asset))

            self.assertEqual(4, bar_data.current(asset, "open"))
            self.assertEqual(5, bar_data.current(asset, "high"))
            self.assertEqual(2, bar_data.current(asset, "low"))
            self.assertEqual(3, bar_data.current(asset, "close"))
            self.assertEqual(300, bar_data.current(asset, "volume"))
            self.assertEqual(3, bar_data.current(asset, "price"))
            self.assertEqual(
                self.equity_daily_bar_days[1],
                bar_data.current(asset, "last_traded")
            )

    def test_last_active_day(self):
        bar_data = self.create_bardata(
            simulation_dt_func=lambda: self.get_last_minute_of_session(
                self.equity_daily_bar_days[-1]
            ),
        )
        self.check_internal_consistency(bar_data)

        for asset in self.ASSETS:
            if asset in (1, 2):
                self.assertFalse(bar_data.can_trade(asset))
            else:
                self.assertTrue(bar_data.can_trade(asset))
            self.assertFalse(bar_data.is_stale(asset))

            if asset in (1, 2):
                assert_almost_equal(nan, bar_data.current(asset, "open"))
                assert_almost_equal(nan, bar_data.current(asset, "high"))
                assert_almost_equal(nan, bar_data.current(asset, "low"))
                assert_almost_equal(nan, bar_data.current(asset, "close"))
                assert_almost_equal(0, bar_data.current(asset, "volume"))
                assert_almost_equal(nan, bar_data.current(asset, "price"))
            else:
                self.assertEqual(6, bar_data.current(asset, "open"))
                self.assertEqual(7, bar_data.current(asset, "high"))
                self.assertEqual(4, bar_data.current(asset, "low"))
                self.assertEqual(5, bar_data.current(asset, "close"))
                self.assertEqual(500, bar_data.current(asset, "volume"))
                self.assertEqual(5, bar_data.current(asset, "price"))

    def test_after_assets_dead(self):
        session = self.END_DATE

        bar_data = self.create_bardata(
            simulation_dt_func=lambda: session,
        )
        self.check_internal_consistency(bar_data)

        for asset in self.ASSETS:
            self.assertFalse(bar_data.can_trade(asset))
            self.assertFalse(bar_data.is_stale(asset))

            for field in OHLCP:
                self.assertTrue(np.isnan(bar_data.current(asset, field)))

            self.assertEqual(0, bar_data.current(asset, "volume"))

            last_traded_dt = bar_data.current(asset, "last_traded")

            if asset in (self.ASSET1, self.ASSET2):
                self.assertEqual(self.equity_daily_bar_days[3],
                                 last_traded_dt)

    @parameterized.expand([
        ("split", 2, 3, 3, 1.5),
        ("merger", 2, 3, 3, 1.8),
        ("dividend", 2, 3, 3, 2.88)
    ])
    def test_get_value_adjustments(self,
                                   adjustment_type,
                                   liquid_day_0_price,
                                   liquid_day_1_price,
                                   illiquid_day_0_price,
                                   illiquid_day_1_price_adjusted):
        """Test the behaviour of spot prices during adjustments."""
        table_name = adjustment_type + 's'
        liquid_asset = getattr(self, (adjustment_type.upper() + "_ASSET"))
        illiquid_asset = getattr(
            self,
            ("ILLIQUID_" + adjustment_type.upper() + "_ASSET")
        )
        # verify there is an adjustment for liquid_asset
        adjustments = self.adjustment_reader.get_adjustments_for_sid(
            table_name,
            liquid_asset.sid
        )

        self.assertEqual(1, len(adjustments))
        adjustment = adjustments[0]
        self.assertEqual(
            adjustment[0],
            pd.Timestamp("2016-01-06", tz='UTC')
        )

        # ... but that's it's not applied when using spot value
        bar_data = self.create_bardata(
            simulation_dt_func=lambda: self.equity_daily_bar_days[0],
        )
        self.assertEqual(
            liquid_day_0_price,
            bar_data.current(liquid_asset, "price")
        )
        bar_data = self.create_bardata(
            simulation_dt_func=lambda: self.equity_daily_bar_days[1],
        )
        self.assertEqual(
            liquid_day_1_price,
            bar_data.current(liquid_asset, "price")
        )

        # ... except when we have to forward fill across a day boundary
        # ILLIQUID_ASSET has no data on days 0 and 2, and a split on day 2
        bar_data = self.create_bardata(
            simulation_dt_func=lambda: self.equity_daily_bar_days[1],
        )
        self.assertEqual(
            illiquid_day_0_price, bar_data.current(illiquid_asset, "price")
        )

        bar_data = self.create_bardata(
            simulation_dt_func=lambda: self.equity_daily_bar_days[2],
        )

        # 3 (price from previous day) * 0.5 (split ratio)
        self.assertAlmostEqual(
            illiquid_day_1_price_adjusted,
            bar_data.current(illiquid_asset, "price")
        )

    def test_can_trade_restricted(self):
        """
        Test that can_trade will return False for a sid if it is restricted
        on that dt
        """

        minutes_to_check = [
            (pd.Timestamp("2016-01-05", tz="UTC"), False),
            (pd.Timestamp("2016-01-06", tz="UTC"), False),
            (pd.Timestamp("2016-01-07", tz="UTC"), True),
        ]

        rlm = HistoricalRestrictions([
            Restriction(1, str_to_ts('2016-01-05'),
                        RESTRICTION_STATES.FROZEN),
            Restriction(1, str_to_ts('2016-01-07'),
                        RESTRICTION_STATES.ALLOWED),
        ])

        for info in minutes_to_check:
            bar_data = self.create_bardata(
                simulation_dt_func=lambda: info[0],
                restrictions=rlm
            )
            self.assertEqual(bar_data.can_trade(self.ASSET1), info[1])
