import datetime
import math
import unittest
from copy import deepcopy
from decimal import Decimal
from typing import Dict, List, Tuple

import numpy as np
import pandas as pd

from hummingbot.client import settings
from hummingbot.client.config.client_config_map import ClientConfigMap
from hummingbot.client.config.config_helpers import ClientConfigAdapter
from hummingbot.connector.exchange.paper_trade.paper_trade_exchange import QuantizationParams
from hummingbot.connector.test_support.mock_paper_exchange import MockPaperExchange
from hummingbot.core.clock import Clock, ClockMode
from hummingbot.core.data_type.common import OrderType, TradeType
from hummingbot.core.data_type.limit_order import LimitOrder
from hummingbot.core.data_type.order_book import OrderBook
from hummingbot.core.data_type.order_book_row import OrderBookRow
from hummingbot.core.data_type.trade_fee import AddedToCostTradeFee, TradeFeeSchema
from hummingbot.core.event.events import (
    BuyOrderCompletedEvent,
    MarketEvent,
    OrderBookTradeEvent,
    OrderFilledEvent,
    SellOrderCompletedEvent,
)
from hummingbot.strategy.__utils__.trailing_indicators.instant_volatility import InstantVolatilityIndicator
from hummingbot.strategy.__utils__.trailing_indicators.trading_intensity import TradingIntensityIndicator
from hummingbot.strategy.avellaneda_market_making import AvellanedaMarketMakingStrategy
from hummingbot.strategy.avellaneda_market_making.avellaneda_market_making_config_map_pydantic import (
    AvellanedaMarketMakingConfigMap,
    DailyBetweenTimesModel,
    InfiniteModel,
    MultiOrderLevelModel,
    TrackHangingOrdersModel,
)
from hummingbot.strategy.data_types import PriceSize, Proposal
from hummingbot.strategy.market_trading_pair_tuple import MarketTradingPairTuple
from hummingbot.strategy.order_book_asset_price_delegate import OrderBookAssetPriceDelegate

s_decimal_zero = Decimal(0)
s_decimal_one = Decimal(1)
s_decimal_nan = Decimal("NaN")
s_decimal_neg_one = Decimal(-1)


class AvellanedaMarketMakingUnitTests(unittest.TestCase):

    start: pd.Timestamp = pd.Timestamp("2019-01-01", tz="UTC")
    end: pd.Timestamp = pd.Timestamp("2019-01-01 01:00:00", tz="UTC")
    start_timestamp: float = start.timestamp()
    end_timestamp: float = end.timestamp()

    @classmethod
    def setUpClass(cls):
        super().setUpClass()
        cls.trading_pair: str = "COINALPHA-HBOT"
        cls.base_asset, cls.quote_asset = cls.trading_pair.split("-")
        cls.initial_mid_price: int = 100

        cls.clock_tick_size: int = 1

        # Testing Constants
        # Volatility as a percentage, not absolute as in instant volatility indicator
        cls.low_vol: Decimal = Decimal("0.05")
        cls.expected_low_vol: Decimal = Decimal("0.0501863845537047")
        cls.high_vol: Decimal = Decimal("5")
        cls.expected_high_vol: Decimal = Decimal("5.018622242594793")

        cls.low_liq_spread: Decimal = Decimal("10")
        cls.low_liq_amount: Decimal = Decimal("1")
        cls.high_liq_spread: Decimal = Decimal("0.1")
        cls.high_liq_amount: Decimal = Decimal("100")

        # Strategy Initial Configuration Parameters
        cls.order_amount: Decimal = Decimal("10")
        cls.inventory_target_base_pct: Decimal = Decimal("50")     # 50%
        cls.min_spread: Decimal = Decimal("0.0")                   # Default strategy value
        cls.risk_factor_finite: Decimal = Decimal("0.8")
        cls.risk_factor_infinite: Decimal = Decimal("1")

        cls.volatility_indicator_low_vol = None
        cls.volatility_indicator_high_vol = None

        cls.trading_intensity_indicator_low_liq = None
        cls.trading_intensity_indicator_high_liq = None

    def setUp(self):
        super().setUp()
        trade_fee_schema = TradeFeeSchema(
            maker_percent_fee_decimal=Decimal("0.25"), taker_percent_fee_decimal=Decimal("0.25")
        )
        self.market: MockPaperExchange = MockPaperExchange(
            client_config_map=ClientConfigAdapter(ClientConfigMap()),
            trade_fee_schema=trade_fee_schema)
        self.market_info: MarketTradingPairTuple = MarketTradingPairTuple(
            self.market, self.trading_pair, *self.trading_pair.split("-")
        )
        self.market.set_balanced_order_book(trading_pair=self.trading_pair,
                                            mid_price=self.initial_mid_price,
                                            min_price=1,
                                            max_price=200,
                                            price_step_size=1,
                                            volume_step_size=10)
        self.market.set_balance("COINALPHA", 1)
        self.market.set_balance("HBOT", 500)
        self.market.set_quantization_param(
            QuantizationParams(
                self.trading_pair.split("-")[0], 6, 6, 6, 6
            )
        )
        self._original_paper_trade_exchanges = settings.PAPER_TRADE_EXCHANGES
        settings.PAPER_TRADE_EXCHANGES.append("mock_paper_exchange")

        self.price_delegate = OrderBookAssetPriceDelegate(self.market_info.market, self.trading_pair)

        config_settings = self.get_default_map()
        self.config_map = ClientConfigAdapter(AvellanedaMarketMakingConfigMap(**config_settings))

        self.strategy: AvellanedaMarketMakingStrategy = AvellanedaMarketMakingStrategy()
        self.strategy.init_params(
            config_map=self.config_map,
            market_info=self.market_info,
        )

        self.avg_vol_indicator: InstantVolatilityIndicator = InstantVolatilityIndicator(sampling_length=100,
                                                                                        processing_length=1)

        self.trading_intensity_indicator: TradingIntensityIndicator = TradingIntensityIndicator(
            order_book=self.market_info.order_book,
            price_delegate=self.price_delegate,
            sampling_length=20)

        self.strategy.avg_vol = self.avg_vol_indicator

        self.clock: Clock = Clock(ClockMode.BACKTEST, self.clock_tick_size, self.start_timestamp, self.end_timestamp)

        self.clock.add_iterator(self.market)
        self.clock.add_iterator(self.strategy)
        self.strategy.start(self.clock, self.start_timestamp)

        self.strategy.trading_intensity = self.trading_intensity_indicator

        self.clock.backtest_til(self.start_timestamp)

    def tearDown(self) -> None:
        self.strategy.stop(self.clock)
        if self._original_paper_trade_exchanges is not None:
            settings.PAPER_TRADE_EXCHANGES = self._original_paper_trade_exchanges
        super().tearDown()

    def get_default_map(self) -> Dict[str, str]:
        config_settings = {
            "exchange": self.market.name,
            "market": self.trading_pair,
            "execution_timeframe_mode": "infinite",
            "order_amount": self.order_amount,
            "order_optimization_enabled": "yes",
            "min_spread": self.min_spread,
            "risk_factor": self.risk_factor_finite,
            "order_refresh_time": "30",
            "inventory_target_base_pct": self.inventory_target_base_pct,
            "add_transaction_costs": "yes",
        }
        return config_settings

    def simulate_low_volatility(self, strategy: AvellanedaMarketMakingStrategy):
        if self.volatility_indicator_low_vol is None:
            N_SAMPLES = 400
            INITIAL_RANDOM_SEED = 3141592653
            original_price = 100
            volatility = AvellanedaMarketMakingUnitTests.low_vol / Decimal("100")  # Assuming 0.5% volatility
            np.random.seed(INITIAL_RANDOM_SEED)     # Using this hardcoded random seed we guarantee random samples generated are always the same
            samples = np.random.normal(original_price, volatility * original_price, N_SAMPLES)

            # This replicates the same indicator Avellaneda uses if volatility_buffer_samples = 30
            volatility_indicator = strategy.avg_vol

            for sample in samples:
                volatility_indicator.add_sample(sample)

            self.volatility_indicator_low_vol = volatility_indicator

        # Note: Current Value of volatility is ~0.5%
        strategy.avg_vol = self.volatility_indicator_low_vol

        # Simulates change in mid price to reflect last sample added
        strategy.market_info.market.set_balanced_order_book(trading_pair=strategy.trading_pair,
                                                            mid_price=samples[-1],
                                                            min_price=1,
                                                            max_price=200,
                                                            price_step_size=1,
                                                            volume_step_size=10)

    def simulate_high_volatility(self, strategy: AvellanedaMarketMakingStrategy):
        if self.volatility_indicator_high_vol is None:
            N_SAMPLES = 400
            INITIAL_RANDOM_SEED = 3141592653
            original_price = 100
            volatility = AvellanedaMarketMakingUnitTests.high_vol / Decimal("100")  # Assuming 10% volatility
            np.random.seed(INITIAL_RANDOM_SEED)     # Using this hardcoded random seed we guarantee random samples generated are always the same
            samples = np.random.normal(original_price, volatility * original_price, N_SAMPLES)

            # This replicates the same indicator Avellaneda uses if volatility_buffer_samples = 30
            volatility_indicator = strategy.avg_vol

            for sample in samples:
                volatility_indicator.add_sample(sample)

            self.volatility_indicator_high_vol = volatility_indicator

        # Note: Current Value of volatility is ~5%
        strategy.avg_vol = self.volatility_indicator_high_vol

        # Simulates change in mid price to reflect last sample added
        strategy.market_info.market.set_balanced_order_book(trading_pair=strategy.trading_pair,
                                                            mid_price=samples[-1],
                                                            min_price=1,
                                                            max_price=200,
                                                            price_step_size=1,
                                                            volume_step_size=10)

    def simulate_low_liquidity(self, strategy: AvellanedaMarketMakingStrategy):
        if self.trading_intensity_indicator_low_liq is None:
            N_SAMPLES = 400
            INITIAL_RANDOM_SEED = 3141592653
            volatility = self.high_vol
            original_price_mid = 100
            original_spread = AvellanedaMarketMakingUnitTests.low_liq_spread
            volatility = volatility / Decimal("100")
            original_amount = AvellanedaMarketMakingUnitTests.low_liq_amount

            spread_stdev = original_spread * Decimal("0.01")
            amount_stdev = original_amount * Decimal("0.01")

            np.random.seed(INITIAL_RANDOM_SEED)     # Using this hardcoded random seed we guarantee random samples generated are always the same

            # Generate orderbooks for all ticks
            bids_df, asks_df = AvellanedaMarketMakingUnitTests.make_order_books(original_price_mid, original_spread, original_amount, volatility, spread_stdev, amount_stdev, N_SAMPLES)
            trades = AvellanedaMarketMakingUnitTests.make_trades(bids_df, asks_df)

            # This replicates the same indicator Avellaneda uses for trading intensity estimation
            trading_intensity_indicator = strategy.trading_intensity

            timestamp = self.start_timestamp
            for bid_df, ask_df, trades_tick in zip(bids_df, asks_df, trades):
                bid = bid_df["price"].iloc[0]
                ask = ask_df["price"].iloc[0]
                mid = (bid + ask) / 2
                for trade in trades_tick:
                    trading_intensity_indicator.register_trade(trade)
                trading_intensity_indicator.calculate(timestamp)
                trading_intensity_indicator.last_quotes = [{"timestamp": timestamp, "price": mid}] + trading_intensity_indicator.last_quotes
                timestamp += 1

            self.trading_intensity_indicator_low_liq = trading_intensity_indicator

        # Update the trading intensity indicator
        strategy.trading_intensity = self.trading_intensity_indicator_low_liq

    def simulate_high_liquidity(self, strategy: AvellanedaMarketMakingStrategy):
        if self.trading_intensity_indicator_high_liq is None:
            N_SAMPLES = 400
            INITIAL_RANDOM_SEED = 3141592653
            volatility = self.low_vol
            original_price_mid = 100
            original_spread = AvellanedaMarketMakingUnitTests.high_liq_spread
            volatility = volatility / Decimal("100")
            original_amount = AvellanedaMarketMakingUnitTests.high_liq_amount

            spread_stdev = original_spread * Decimal("0.01")
            amount_stdev = original_amount * Decimal("0.01")

            np.random.seed(INITIAL_RANDOM_SEED)     # Using this hardcoded random seed we guarantee random samples generated are always the same

            # Generate orderbooks for all ticks
            bids_df, asks_df = AvellanedaMarketMakingUnitTests.make_order_books(original_price_mid, original_spread, original_amount, volatility, spread_stdev, amount_stdev, N_SAMPLES)
            trades = AvellanedaMarketMakingUnitTests.make_trades(bids_df, asks_df)

            # This replicates the same indicator Avellaneda uses for trading intensity estimation
            trading_intensity_indicator = strategy.trading_intensity

            timestamp = self.start_timestamp
            for bid_df, ask_df, trades_tick in zip(bids_df, asks_df, trades):
                bid = bid_df["price"].iloc[0]
                ask = ask_df["price"].iloc[0]
                mid = (bid + ask) / 2
                for trade in trades_tick:
                    trading_intensity_indicator.register_trade(trade)
                trading_intensity_indicator.calculate(timestamp)
                trading_intensity_indicator.last_quotes = [{"timestamp": timestamp, "price": mid}] + trading_intensity_indicator.last_quotes
                timestamp += 1

            self.trading_intensity_indicator_high_liq = trading_intensity_indicator

        # Update the trading intensity indicator
        strategy.trading_intensity = self.trading_intensity_indicator_high_liq

    @staticmethod
    def make_order_books(original_price_mid, original_spread, original_amount, volatility, spread_stdev, amount_stdev, samples):
        # 0.1% quantization of prices in the orderbook
        PRICE_STEP_FRACTION = 0.01

        # Generate BBO quotes
        samples_mid = np.random.normal(original_price_mid, volatility * original_price_mid, samples)
        samples_spread = np.random.normal(original_spread, spread_stdev, samples)

        samples_price_bid = np.subtract(samples_mid, np.divide(samples_spread, 2))
        samples_price_ask = np.add(samples_mid, np.divide(samples_spread, 2))

        samples_amount_bid = np.random.normal(original_amount, amount_stdev, samples)
        samples_amount_ask = np.random.normal(original_amount, amount_stdev, samples)

        # A full orderbook is not necessary, only up to the BBO max deviation
        price_depth_max = max(max(samples_price_bid) - min(samples_price_bid), max(samples_price_ask) - min(samples_price_ask))

        bid_dfs = []
        ask_dfs = []

        # Generate an orderbook for every tick
        for price_bid, amount_bid, price_ask, amount_ask in zip(samples_price_bid, samples_amount_bid, samples_price_ask, samples_amount_ask):
            bid_df, ask_df = AvellanedaMarketMakingUnitTests.make_order_book(price_bid, amount_bid, price_ask, amount_ask, price_depth_max, original_price_mid * PRICE_STEP_FRACTION, amount_stdev)
            bid_dfs += [bid_df]
            ask_dfs += [ask_df]

        return bid_dfs, ask_dfs

    @staticmethod
    def make_order_book(price_bid, amount_bid, price_ask, amount_ask, price_depth, price_step, amount_stdev, ):

        prices_bid = np.linspace(price_bid, price_bid - price_depth, math.ceil(price_depth / price_step))
        amounts_bid = np.random.normal(amount_bid, amount_stdev, len(prices_bid))
        amounts_bid[0] = amount_bid

        prices_ask = np.linspace(price_ask, price_ask + price_depth, math.ceil(price_depth / price_step))
        amounts_ask = np.random.normal(amount_ask, amount_stdev, len(prices_ask))
        amounts_ask[0] = amount_ask

        data_bid = {'price': prices_bid, 'amount': amounts_bid}
        bid_df = pd.DataFrame(data=data_bid)

        data_ask = {'price': prices_ask, 'amount': amounts_ask}
        ask_df = pd.DataFrame(data=data_ask)

        return bid_df, ask_df

    @staticmethod
    def make_trades(bids_df, asks_df):
        # Estimate market orders that happened
        # Assume every movement in the BBO is caused by a market order and its size is the volume differential

        bid_df_prev = None
        ask_df_prev = None
        bid_prev = None
        ask_prev = None
        price_prev = None

        trades = []

        start = pd.Timestamp("2019-01-01", tz="UTC")
        start_timestamp = start.timestamp()
        timestamp = start_timestamp

        for bid_df, ask_df in zip(bids_df, asks_df):

            trades += [[]]

            bid = bid_df["price"].iloc[0]
            ask = ask_df["price"].iloc[0]

            if bid_prev is not None and ask_prev is not None and price_prev is not None:
                # Higher bids were filled - someone matched them - a determined seller
                # Equal bids - if amount lower - partially filled
                for index, row in bid_df_prev[bid_df_prev['price'] >= bid].iterrows():
                    if row['price'] == bid:
                        if bid_df["amount"].iloc[0] < row['amount']:
                            amount = row['amount'] - bid_df["amount"].iloc[0]
                            new_trade = OrderBookTradeEvent(
                                trading_pair="COINALPHAHBOT",
                                timestamp=timestamp,
                                price=row['price'],
                                amount=amount,
                                type=TradeType.SELL
                            )
                            trades[-1] += [new_trade]
                    else:
                        amount = row['amount']
                        new_trade = OrderBookTradeEvent(
                            trading_pair="COINALPHAHBOT",
                            timestamp=timestamp,
                            price=row['price'],
                            amount=amount,
                            type=TradeType.SELL
                        )
                        trades[-1] += [new_trade]

                # Lower asks were filled - someone matched them - a determined buyer
                # Equal asks - if amount lower - partially filled
                for index, row in ask_df_prev[ask_df_prev['price'] <= ask].iterrows():
                    if row['price'] == ask:
                        if ask_df["amount"].iloc[0] < row['amount']:
                            amount = row['amount'] - ask_df["amount"].iloc[0]
                            new_trade = OrderBookTradeEvent(
                                trading_pair="COINALPHAHBOT",
                                timestamp=timestamp,
                                price=row['price'],
                                amount=amount,
                                type=TradeType.BUY
                            )
                            trades[-1] += [new_trade]
                    else:
                        amount = row['amount']
                        new_trade = OrderBookTradeEvent(
                            trading_pair="COINALPHAHBOT",
                            timestamp=timestamp,
                            price=row['price'],
                            amount=amount,
                            type=TradeType.BUY
                        )
                        trades[-1] += [new_trade]

            # Store previous values
            bid_df_prev = bid_df
            ask_df_prev = ask_df
            bid_prev = bid_df["price"].iloc[0]
            ask_prev = ask_df["price"].iloc[0]
            price_prev = (bid_prev + ask_prev) / 2

            timestamp += 1

        return trades

    @staticmethod
    def simulate_place_limit_order(strategy: AvellanedaMarketMakingStrategy, market_info: MarketTradingPairTuple, order: LimitOrder):
        strategy.set_timers()
        if order.is_buy:
            return strategy.buy_with_specific_market(market_trading_pair_tuple=market_info,
                                                     order_type=OrderType.LIMIT,
                                                     price=order.price,
                                                     amount=order.quantity
                                                     )
        else:
            return strategy.sell_with_specific_market(market_trading_pair_tuple=market_info,
                                                      order_type=OrderType.LIMIT,
                                                      price=order.price,
                                                      amount=order.quantity)

    @staticmethod
    def simulate_cancelling_all_active_orders(strategy: AvellanedaMarketMakingStrategy):
        strategy.cancel_active_orders(None)

    @staticmethod
    def simulate_limit_order_fill(market: MockPaperExchange, limit_order: LimitOrder):
        quote_currency_traded: Decimal = limit_order.price * limit_order.quantity
        base_currency_traded: Decimal = limit_order.quantity
        quote_currency: str = limit_order.quote_currency
        base_currency: str = limit_order.base_currency

        if limit_order.is_buy:
            market.set_balance(quote_currency, market.get_balance(quote_currency) - quote_currency_traded)
            market.set_balance(base_currency, market.get_balance(base_currency) + base_currency_traded)
            market.trigger_event(MarketEvent.OrderFilled, OrderFilledEvent(
                market.current_timestamp,
                limit_order.client_order_id,
                limit_order.trading_pair,
                TradeType.BUY,
                OrderType.LIMIT,
                limit_order.price,
                limit_order.quantity,
                AddedToCostTradeFee(Decimal("0"))
            ))
            market.trigger_event(MarketEvent.BuyOrderCompleted, BuyOrderCompletedEvent(
                market.current_timestamp,
                limit_order.client_order_id,
                base_currency,
                quote_currency,
                base_currency_traded,
                quote_currency_traded,
                OrderType.LIMIT
            ))
        else:
            market.set_balance(quote_currency, market.get_balance(quote_currency) + quote_currency_traded)
            market.set_balance(base_currency, market.get_balance(base_currency) - base_currency_traded)
            market.trigger_event(MarketEvent.OrderFilled, OrderFilledEvent(
                market.current_timestamp,
                limit_order.client_order_id,
                limit_order.trading_pair,
                TradeType.SELL,
                OrderType.LIMIT,
                limit_order.price,
                limit_order.quantity,
                AddedToCostTradeFee(Decimal("0"))
            ))
            market.trigger_event(MarketEvent.SellOrderCompleted, SellOrderCompletedEvent(
                market.current_timestamp,
                limit_order.client_order_id,
                base_currency,
                quote_currency,
                base_currency_traded,
                quote_currency_traded,
                OrderType.LIMIT
            ))

    def test_all_markets_ready(self):
        self.assertTrue(self.strategy.all_markets_ready())

    def test_market_info(self):
        self.assertEqual(self.market_info, self.strategy.market_info)

    def test_base_asset(self):
        self.assertEqual(self.trading_pair.split("-")[0], self.strategy.base_asset)

    def test_quote_asset(self):
        self.assertEqual(self.trading_pair.split("-")[1], self.strategy.quote_asset)

    def test_trading_pair(self):
        self.assertEqual(self.trading_pair, self.strategy.trading_pair)

    def test_get_price(self):
        # Avellaneda Strategy get_price is simply a wrapper for MarketTradingPairTuple.get_mid_price()
        self.assertEqual(self.market_info.get_mid_price(), self.strategy.get_price())

    def test_get_mid_price(self):
        self.assertEqual(self.market_info.get_mid_price(), self.strategy.get_mid_price())

    def test_market_info_to_active_orders(self):
        order_tracker = self.strategy.order_tracker

        self.assertEqual(order_tracker.market_pair_to_active_orders, self.strategy.market_info_to_active_orders)

        # Simulate order being placed
        limit_order: LimitOrder = LimitOrder(client_order_id="test",
                                             trading_pair=self.trading_pair,
                                             is_buy=True,
                                             base_currency=self.trading_pair.split("-")[0],
                                             quote_currency=self.trading_pair.split("-")[1],
                                             price=Decimal("101.0"),
                                             quantity=Decimal("10"))

        self.simulate_place_limit_order(self.strategy, self.market_info, limit_order)

        self.assertEqual(1, len(self.strategy.market_info_to_active_orders))
        self.assertEqual(order_tracker.market_pair_to_active_orders, self.strategy.market_info_to_active_orders)

    def test_active_orders(self):
        self.assertEqual(0, len(self.strategy.active_orders))

        # Simulate order being placed
        limit_order: LimitOrder = LimitOrder(client_order_id="test",
                                             trading_pair=self.trading_pair,
                                             is_buy=True,
                                             base_currency=self.trading_pair.split("-")[0],
                                             quote_currency=self.trading_pair.split("-")[1],
                                             price=Decimal("101.0"),
                                             quantity=Decimal("10"))

        self.simulate_place_limit_order(self.strategy, self.market_info, limit_order)

        self.assertEqual(1, len(self.strategy.active_orders))
        # Note: The order_id created by the BacktestMarket class is as follows [buy/sell]://{trading_pair}/{random_uuid}
        self.assertTrue(f"buy://{self.trading_pair}" in self.strategy.active_orders[0].client_order_id)
        self.assertEqual(limit_order.price, self.strategy.active_orders[0].price)
        self.assertEqual(limit_order.quantity, self.strategy.active_orders[0].quantity)

    def test_active_buys(self):
        self.assertEqual(0, len(self.strategy.active_buys))

        # Simulate order being placed
        limit_order: LimitOrder = LimitOrder(client_order_id="test",
                                             trading_pair=self.trading_pair,
                                             is_buy=True,
                                             base_currency=self.trading_pair.split("-")[0],
                                             quote_currency=self.trading_pair.split("-")[1],
                                             price=Decimal("101.0"),
                                             quantity=Decimal("10"))

        self.simulate_place_limit_order(self.strategy, self.market_info, limit_order)

        self.assertEqual(1, len(self.strategy.active_buys))
        self.assertTrue(f"buy://{self.trading_pair}" in self.strategy.active_buys[0].client_order_id)
        self.assertEqual(limit_order.price, self.strategy.active_buys[0].price)
        self.assertEqual(limit_order.quantity, self.strategy.active_buys[0].quantity)

    def test_active_sells(self):
        self.assertEqual(0, len(self.strategy.active_sells))

        # Simulate order being placed
        limit_order: LimitOrder = LimitOrder(client_order_id="test",
                                             trading_pair=self.trading_pair,
                                             is_buy=False,
                                             base_currency=self.trading_pair.split("-")[0],
                                             quote_currency=self.trading_pair.split("-")[1],
                                             price=Decimal("101.0"),
                                             quantity=Decimal("0.5"))

        self.simulate_place_limit_order(self.strategy, self.market_info, limit_order)

        self.assertEqual(1, len(self.strategy.active_sells))
        self.assertTrue(f"sell://{self.trading_pair}" in self.strategy.active_sells[0].client_order_id)
        self.assertEqual(limit_order.price, self.strategy.active_sells[0].price)
        self.assertEqual(limit_order.quantity, self.strategy.active_sells[0].quantity)

    def test_logging_options(self):
        self.assertEqual(AvellanedaMarketMakingStrategy.OPTION_LOG_ALL, self.strategy.logging_options)

        # Test setter method
        self.strategy.logging_options = AvellanedaMarketMakingStrategy.OPTION_LOG_CREATE_ORDER

        self.assertEqual(AvellanedaMarketMakingStrategy.OPTION_LOG_CREATE_ORDER, self.strategy.logging_options)

    def test_execute_orders_proposal(self):
        self.assertEqual(0, len(self.strategy.active_orders))

        buys: List[PriceSize] = [PriceSize(price=Decimal("99"), size=Decimal("1"))]
        sells: List[PriceSize] = [PriceSize(price=Decimal("101"), size=Decimal("1"))]
        proposal: Proposal = Proposal(buys, sells)

        self.strategy.execute_orders_proposal(proposal)

        self.assertEqual(1, len(self.strategy.active_buys))
        self.assertEqual(1, len(self.strategy.active_sells))

        self.assertTrue(f"buy://{self.trading_pair}" in self.strategy.active_buys[0].client_order_id)
        self.assertEqual(buys[0].price, self.strategy.active_buys[0].price)
        self.assertEqual(buys[0].size, self.strategy.active_buys[0].quantity)

        self.assertTrue(f"sell://{self.trading_pair}" in self.strategy.active_sells[0].client_order_id)
        self.assertEqual(sells[0].price, self.strategy.active_sells[0].price)
        self.assertEqual(sells[0].size, self.strategy.active_sells[0].quantity)

    def test_cancel_order(self):
        self.assertEqual(0, len(self.strategy.active_orders))

        buys: List[PriceSize] = [PriceSize(price=Decimal("99"), size=Decimal("1"))]
        sells: List[PriceSize] = [PriceSize(price=Decimal("101"), size=Decimal("1"))]
        proposal: Proposal = Proposal(buys, sells)

        self.strategy.execute_orders_proposal(proposal)

        self.assertEqual(2, len(self.strategy.active_orders))

        for order in self.strategy.active_orders:
            self.strategy.cancel_order(order.client_order_id)

        self.assertEqual(0, len(self.strategy.active_orders))

    def test_is_algorithm_ready(self):
        self.assertFalse(self.strategy.is_algorithm_ready())

        self.simulate_high_volatility(self.strategy)
        self.simulate_low_liquidity(self.strategy)

        self.assertTrue(self.strategy.is_algorithm_ready())

    def test_get_spread(self):
        order_book: OrderBook = self.market.get_order_book(self.trading_pair)
        expected_spread = order_book.get_price(True) - order_book.get_price(False)

        self.assertEqual(expected_spread, self.strategy.get_spread())

    def test_get_volatility(self):
        # Initial Volatility
        self.assertTrue(math.isnan(self.strategy.get_volatility()))

        # Simulate volatility update
        self.simulate_low_volatility(self.strategy)

        # Check updated volatility
        self.assertAlmostEqual(self.expected_low_vol, self.strategy.get_volatility(), 1)

    def test_calculate_target_inventory(self):
        # Calculate expected quantize order amount
        current_price = self.market_info.get_mid_price()

        base_asset_amount = self.market.get_balance(self.trading_pair.split("-")[0])
        quote_asset_amount = self.market.get_balance(self.trading_pair.split("-")[1])
        base_value = base_asset_amount * current_price
        inventory_value = base_value + quote_asset_amount
        target_inventory_value = Decimal((inventory_value * self.inventory_target_base_pct / Decimal('100')) / current_price)

        expected_quantize_order_amount = self.market.quantize_order_amount(self.trading_pair, target_inventory_value)

        self.assertEqual(expected_quantize_order_amount, self.strategy.calculate_target_inventory())

    def test_liquidity_estimation(self):

        # Simulate high liquidity
        self.simulate_high_liquidity(self.strategy)

        alpha, kappa = self.strategy.trading_intensity.current_value

        self.assertAlmostEqual(118.53441791741469, alpha, 3)
        self.assertAlmostEqual(3.3607256761562003, kappa, 3)

        # Simulate high liquidity
        self.simulate_low_liquidity(self.strategy)

        alpha, kappa = self.strategy.trading_intensity.current_value

        self.assertAlmostEqual(118.45210662343376, alpha, 3)
        self.assertAlmostEqual(3.3468695409821243, kappa, 3)

    def test_calculate_reservation_price_and_optimal_spread_timeframe_constrained(self):
        # Init params
        start_time = (
            datetime.datetime.fromtimestamp(self.strategy.current_timestamp) - datetime.timedelta(minutes=30)
        ).time().strftime("%H:%M:%S")
        end_time = (
            datetime.datetime.fromtimestamp(self.strategy.current_timestamp) + datetime.timedelta(minutes=30)
        ).time().strftime("%H:%M:%S")
        self.config_map.execution_timeframe_mode = DailyBetweenTimesModel(start_time=start_time, end_time=end_time)

        # Simulate low volatility
        self.simulate_low_volatility(self.strategy)

        # Simulate high liquidity
        self.simulate_high_liquidity(self.strategy)

        # Execute measurements and calculations
        self.strategy.measure_order_book_liquidity()
        self.strategy.calculate_reservation_price_and_optimal_spread()

        # Check reservation_price, optimal_ask and optimal_bid
        self.assertAlmostEqual(Decimal("100.035"), self.strategy.reservation_price, 2)
        self.assertAlmostEqual(Decimal("0.592"), self.strategy.optimal_spread, 2)
        self.assertAlmostEqual(Decimal("100.331"), self.strategy.optimal_ask, 2)
        self.assertAlmostEqual(Decimal("99.739"), self.strategy.optimal_bid, 2)

    def test_calculate_reservation_price_and_optimal_spread_timeframe_infinite(self):
        # Init params
        self.config_map.execution_timeframe_mode = InfiniteModel()
        self.config_map.risk_factor = self.risk_factor_infinite

        # Simulate low volatility
        self.simulate_low_volatility(self.strategy)

        # Simulate high liquidity
        self.simulate_high_liquidity(self.strategy)

        # Execute measurements and calculations
        self.strategy.measure_order_book_liquidity()
        self.strategy.calculate_reservation_price_and_optimal_spread()

        # Check reservation_price, optimal_ask and optimal_bid
        self.assertAlmostEqual(Decimal("100.040"), self.strategy.reservation_price, 2)
        self.assertAlmostEqual(Decimal("0.594"), self.strategy.optimal_spread, 2)
        self.assertAlmostEqual(Decimal("100.337"), self.strategy.optimal_ask, 2)
        self.assertAlmostEqual(Decimal("99.743"), self.strategy.optimal_bid, 2)

    def test_create_proposal_based_on_order_override(self):
        # Initial check for empty order_override
        expected_output: Tuple[List, List] = ([], [])
        self.assertEqual(expected_output, self.strategy.create_proposal_based_on_order_override())

        order_override = {
            "order_1": ["sell", 2.5, 100],
            "order_2": ["buy", 0.5, 100]
        }

        # Re-configure strategy with order_ride configurations
        self.config_map.order_override = order_override

        expected_proposal = (list(), list())
        for order in order_override.values():
            list_to_append = expected_proposal[0] if order[0] == "buy" else expected_proposal[1]
            if "buy" == order[0]:
                price = self.strategy.get_price() * (Decimal("1") - Decimal(str(order[1])) / Decimal("100"))
            else:
                price = self.strategy.get_price() * (Decimal("1") + Decimal(str(order[1])) / Decimal("100"))

            price = self.market.quantize_order_price(self.trading_pair, price)
            size = self.market.quantize_order_amount(self.trading_pair, Decimal(str(order[2])))

            list_to_append.append(PriceSize(price, size))

        self.assertEqual(str(expected_proposal), str(self.strategy.create_proposal_based_on_order_override()))

    def test_get_level_spreads(self):
        order_levels_mode = MultiOrderLevelModel()
        order_levels_mode.order_levels = 4
        order_levels_mode.level_distances = 1

        config_settings = {
            "exchange": self.market.name,
            "market": self.trading_pair,
            "execution_timeframe_mode": "infinite",
            "order_amount": self.order_amount,
            "order_optimization_enabled": "yes",
            "order_levels_mode": order_levels_mode,
            "min_spread": self.min_spread,
            "risk_factor": self.risk_factor_infinite,
            "order_refresh_time": "60",
            "inventory_target_base_pct": self.inventory_target_base_pct,
        }
        config_map = ClientConfigAdapter(AvellanedaMarketMakingConfigMap(**config_settings))

        # Re-initialize strategy with order_level configurations
        self.strategy = AvellanedaMarketMakingStrategy()
        self.strategy.init_params(
            config_map=config_map,
            market_info=self.market_info,
        )

        # Create a new clock to start the strategy from scratch
        self.clock: Clock = Clock(ClockMode.BACKTEST, self.clock_tick_size, self.start_timestamp, self.end_timestamp)
        self.clock.add_iterator(self.market)
        self.clock.add_iterator(self.strategy)

        self.strategy.avg_vol = self.avg_vol_indicator
        self.clock.add_iterator(self.strategy)

        self.clock.backtest_til(self.start_timestamp + self.clock_tick_size)

        # Simulate low volatility.
        # Note: bid/ask_level_spreads Requires volatility, optimal_bid, optimal_ask to be defined
        self.simulate_low_volatility(self.strategy)

        # Simulate high liquidity
        self.simulate_high_liquidity(self.strategy)

        # Execute measurements and calculations
        self.strategy.measure_order_book_liquidity()
        self.strategy.calculate_reservation_price_and_optimal_spread()

        expected_bid_spreads = [Decimal('0E-28'), Decimal('0.03471008344015021195989165942'), Decimal('0.07680749440342730936221062482'), Decimal('0.1152112416051409640433159372')]
        expected_ask_spreads = [Decimal('0E-28'), Decimal('0.03471008344015021195989165942'), Decimal('0.07680749440342730936221062482'), Decimal('0.1152112416051409640433159372')]

        bid_level_spreads, ask_level_spreads = self.strategy._get_level_spreads()

        for i, spread in enumerate(bid_level_spreads):
            self.assertAlmostEqual(expected_bid_spreads[i], spread, 1)

        for i, spread in enumerate(ask_level_spreads):
            self.assertAlmostEqual(expected_ask_spreads[i], spread, 1)

        # Simulate high volatility. TODO: Find a better max_spread parameter to better illustrate bid levels
        # Note: bid/ask_level_spreads Requires volatility, optimal_bid, optimal_ask to be defined
        self.simulate_high_volatility(self.strategy)

        # Simulate high liquidity
        self.simulate_low_liquidity(self.strategy)

        # Execute measurements and calculations
        self.strategy.measure_order_book_liquidity()
        self.strategy.calculate_reservation_price_and_optimal_spread()

        expected_bid_spreads = [Decimal('0E-28'), Decimal('0.03909242377646942266258131591'), Decimal('0.07818484755293884532516263182'), Decimal('0.1172772713294082679877439477')]
        expected_ask_spreads = [Decimal('0E-28'), Decimal('0.03909242377646942266258131591'), Decimal('0.07818484755293884532516263182'), Decimal('0.1172772713294082679877439477')]

        bid_level_spreads, ask_level_spreads = self.strategy._get_level_spreads()

        for i, spread in enumerate(bid_level_spreads):
            self.assertAlmostEqual(expected_bid_spreads[i], spread, 1)

        for i, spread in enumerate(ask_level_spreads):
            self.assertAlmostEqual(expected_ask_spreads[i], spread, 1)

    def test_create_proposal_based_on_order_levels(self):
        # Simulate low volatility
        self.simulate_low_volatility(self.strategy)

        # Simulate high liquidity
        self.simulate_high_liquidity(self.strategy)

        # Prepare market variables and parameters for calculation
        self.strategy.measure_order_book_liquidity()
        self.strategy.calculate_reservation_price_and_optimal_spread()

        # Test(1) Check order_levels default = 0
        empty_proposal = ([], [])
        self.assertEqual(empty_proposal, self.strategy.create_proposal_based_on_order_levels())

        # Re-initialize strategy with order_level configurations
        order_levels_mode = MultiOrderLevelModel()
        order_levels_mode.order_levels = 2
        order_levels_mode.level_distances = 1
        self.config_map.order_levels_mode = order_levels_mode

        # Calculate order levels
        bid_level_spreads, ask_level_spreads = self.strategy._get_level_spreads()

        expected_buys = []
        expected_sells = []
        order_amount = self.market.quantize_order_amount(self.trading_pair, self.order_amount)
        for level in range(self.strategy.order_levels):
            bid_price = self.market.quantize_order_price(self.trading_pair,
                                                         self.strategy.optimal_bid - Decimal(str(bid_level_spreads[level])))
            ask_price = self.market.quantize_order_price(self.trading_pair,
                                                         self.strategy.optimal_ask + Decimal(str(ask_level_spreads[level])))

            expected_buys.append(PriceSize(bid_price, order_amount))
            expected_sells.append(PriceSize(ask_price, order_amount))

        expected_proposal = (expected_buys, expected_sells)

        self.assertEqual(str(expected_proposal), str(self.strategy.create_proposal_based_on_order_levels()))

    def test_create_basic_proposal(self):
        # Simulate low volatility
        self.simulate_low_volatility(self.strategy)

        # Simulate high liquidity
        self.simulate_high_liquidity(self.strategy)

        # Prepare market variables and parameters for calculation
        self.strategy.measure_order_book_liquidity()
        self.strategy.calculate_reservation_price_and_optimal_spread()

        expected_order_amount: Decimal = self.market.quantize_order_amount(self.trading_pair,
                                                                           self.order_amount)
        expected_bid_price: Decimal = self.market.quantize_order_price(self.trading_pair,
                                                                       self.strategy.optimal_bid)

        expected_ask_price: Decimal = self.market.quantize_order_price(self.trading_pair,
                                                                       self.strategy.optimal_ask)

        expected_proposal = ([PriceSize(expected_bid_price, expected_order_amount)],
                             [PriceSize(expected_ask_price, expected_order_amount)])

        self.assertEqual(str(expected_proposal), str(self.strategy.create_basic_proposal()))

    def test_create_base_proposal(self):
        # Simulate low volatility
        self.simulate_low_volatility(self.strategy)

        # Simulate high liquidity
        self.simulate_high_liquidity(self.strategy)

        # Prepare market variables and parameters for calculation
        self.strategy.measure_order_book_liquidity()
        self.strategy.calculate_reservation_price_and_optimal_spread()

        # (1) Default
        expected_order_amount: Decimal = self.market.quantize_order_amount(self.trading_pair,
                                                                           self.order_amount)
        expected_bid_price: Decimal = self.market.quantize_order_price(self.trading_pair,
                                                                       self.strategy.optimal_bid)

        expected_ask_price: Decimal = self.market.quantize_order_price(self.trading_pair,
                                                                       self.strategy.optimal_ask)

        expected_proposal: Proposal = Proposal([PriceSize(expected_bid_price, expected_order_amount)],
                                               [PriceSize(expected_ask_price, expected_order_amount)])

        self.assertEqual(str(expected_proposal), str(self.strategy.create_base_proposal()))

        # (2) With order_override
        order_override = {
            "order_1": ["sell", 2.5, 100],
            "order_2": ["buy", 0.5, 100]
        }

        # Re-configure strategy with order_ride configurations
        self.config_map.order_override = order_override

        expected_buys = []
        expected_sells = []
        for order in order_override.values():
            list_to_append = expected_buys if order[0] == "buy" else expected_sells
            if "buy" == order[0]:
                price = self.strategy.get_price() * (Decimal("1") - Decimal(str(order[1])) / Decimal("100"))
            else:
                price = self.strategy.get_price() * (Decimal("1") + Decimal(str(order[1])) / Decimal("100"))

            price = self.market.quantize_order_price(self.trading_pair, price)
            size = self.market.quantize_order_amount(self.trading_pair, Decimal(str(order[2])))

            list_to_append.append(PriceSize(price, size))

        expected_proposal: Proposal = Proposal(expected_buys, expected_sells)

        self.assertEqual(str(expected_proposal), str(self.strategy.create_base_proposal()))

        # Reset order_override configuration
        self.config_map.order_override = {}

        # (3) With order_levels
        order_levels_mode = MultiOrderLevelModel()
        order_levels_mode.order_levels = 2
        order_levels_mode.level_distances = 1
        self.config_map.order_levels_mode = order_levels_mode

        # Calculate order levels
        bid_level_spreads, ask_level_spreads = self.strategy._get_level_spreads()

        expected_buys = []
        expected_sells = []
        order_amount = self.market.quantize_order_amount(self.trading_pair, self.order_amount)
        for level in range(self.strategy.order_levels):
            bid_price = self.market.quantize_order_price(self.trading_pair,
                                                         self.strategy.optimal_bid - Decimal(str(bid_level_spreads[level])))
            ask_price = self.market.quantize_order_price(self.trading_pair,
                                                         self.strategy.optimal_ask + Decimal(str(ask_level_spreads[level])))

            expected_buys.append(PriceSize(bid_price, order_amount))
            expected_sells.append(PriceSize(ask_price, order_amount))

        expected_proposal: Proposal = Proposal(expected_buys, expected_sells)
        self.assertEqual(str(expected_proposal), str(self.strategy.create_base_proposal()))

    def test_get_adjusted_available_balance(self):
        expected_available_balance: Tuple[Decimal, Decimal] = (Decimal("1"), Decimal("500"))  # Initial asset balance
        self.assertEqual(expected_available_balance, self.strategy.get_adjusted_available_balance(self.strategy.active_orders))

        # Simulate order being placed
        limit_order: LimitOrder = LimitOrder(client_order_id="test",
                                             trading_pair=self.trading_pair,
                                             is_buy=True,
                                             base_currency=self.trading_pair.split("-")[0],
                                             quote_currency=self.trading_pair.split("-")[1],
                                             price=Decimal("101.0"),
                                             quantity=Decimal("1"))

        self.simulate_place_limit_order(self.strategy, self.market_info, limit_order)

        self.assertEqual(expected_available_balance, self.strategy.get_adjusted_available_balance(self.strategy.active_orders))

    def test_apply_order_optimization(self):
        # Simulate low volatility
        self.simulate_low_volatility(self.strategy)

        # Simulate high liquidity
        self.simulate_high_liquidity(self.strategy)

        # Prepare market variables and parameters for calculation
        self.strategy.measure_order_book_liquidity()
        self.strategy.calculate_reservation_price_and_optimal_spread()

        # Create a basic proposal.
        order_amount: Decimal = self.market.quantize_order_amount(self.trading_pair, self.order_amount)
        bid_price: Decimal = self.market.quantize_order_price(self.trading_pair, self.strategy.optimal_bid)
        ask_price: Decimal = self.market.quantize_order_price(self.trading_pair, self.strategy.optimal_ask)

        initial_proposal: Proposal = Proposal([PriceSize(bid_price, order_amount)], [PriceSize(ask_price, order_amount)])

        # Intentionally make top_bid/ask_price lower/higher respectively.
        ob_bids: List[OrderBookRow] = [OrderBookRow(bid_price * Decimal("0.5"), self.order_amount, 2)]
        ob_asks: List[OrderBookRow] = [OrderBookRow(ask_price * Decimal("1.5"), self.order_amount, 2)]
        self.market.order_books[self.trading_pair].apply_snapshot(ob_bids, ob_asks, 2)

        new_proposal: Proposal = deepcopy(initial_proposal)
        self.strategy.apply_order_price_modifiers(new_proposal)

        self.assertNotEqual(initial_proposal, new_proposal)

    def test_apply_add_transaction_costs(self):
        # Simulate low volatility
        self.simulate_low_volatility(self.strategy)

        # Simulate high liquidity
        self.simulate_high_liquidity(self.strategy)

        # Prepare market variables and parameters for calculation
        self.strategy.measure_order_book_liquidity()
        self.strategy.calculate_reservation_price_and_optimal_spread()

        # Create a basic proposal.
        order_amount: Decimal = self.market.quantize_order_amount(self.trading_pair, self.order_amount)
        bid_price: Decimal = self.market.quantize_order_price(self.trading_pair, self.strategy.optimal_bid)
        ask_price: Decimal = self.market.quantize_order_price(self.trading_pair, self.strategy.optimal_ask)

        initial_proposal: Proposal = Proposal([PriceSize(bid_price, order_amount)], [PriceSize(ask_price, order_amount)])

        # Set TradeFees
        # self.market.set_flat_fee(Decimal("0.25"))

        new_proposal: Proposal = deepcopy(initial_proposal)
        self.strategy.apply_order_price_modifiers(new_proposal)

        self.assertNotEqual(initial_proposal, new_proposal)

    def test_apply_order_price_modifiers(self):
        # >>>> Test Preparation Start
        # self.market.set_flat_fee(Decimal("0.25"))

        # Simulate low volatility
        self.simulate_low_volatility(self.strategy)

        # Simulate high liquidity
        self.simulate_high_liquidity(self.strategy)

        # Prepare market variables and parameters for calculation
        self.strategy.measure_order_book_liquidity()
        self.strategy.calculate_reservation_price_and_optimal_spread()

        # Create a basic proposal.
        order_amount: Decimal = self.market.quantize_order_amount(self.trading_pair, self.order_amount)
        bid_price: Decimal = self.market.quantize_order_price(self.trading_pair, self.strategy.optimal_bid)
        ask_price: Decimal = self.market.quantize_order_price(self.trading_pair, self.strategy.optimal_ask)

        initial_proposal: Proposal = Proposal([PriceSize(bid_price, order_amount)], [PriceSize(ask_price, order_amount)])
        # <<<<< Test Preparation End

        # (1) Default: order_optimization = True, add_transaction_costs_to_orders = False
        # self.strategy.add_transaction_costs_to_orders = True

        #   Intentionally make top_bid/ask_price lower/higher respectively & set TradeFees
        ob_bids: List[OrderBookRow] = [OrderBookRow(bid_price * Decimal("0.5"), self.order_amount, 2)]
        ob_asks: List[OrderBookRow] = [OrderBookRow(ask_price * Decimal("1.5"), self.order_amount, 2)]
        self.market.order_books[self.trading_pair].apply_snapshot(ob_bids, ob_asks, 2)

        expected_bid_price = self.market.quantize_order_price(
            self.trading_pair,
            bid_price * Decimal("0.5") * (Decimal("1") - Decimal("0.25")))

        expected_ask_price = self.market.quantize_order_price(
            self.trading_pair,
            ask_price * Decimal("1.5") * (Decimal("1") + Decimal("0.25")))

        new_proposal: Proposal = deepcopy(initial_proposal)
        self.strategy.apply_order_price_modifiers(new_proposal)

        self.assertNotEqual(str(initial_proposal), new_proposal)

        new_bid_price = new_proposal.buys[0].price
        new_ask_price = new_proposal.sells[0].price

        self.assertAlmostEqual(expected_bid_price, new_bid_price, 6)
        self.assertAlmostEqual(expected_ask_price, new_ask_price, 6)

        # (2) With none enabled
        self.config_map.order_optimization_enabled = self.config_map.add_transaction_costs = False

        new_proposal: Proposal = deepcopy(initial_proposal)
        self.strategy.apply_order_price_modifiers(new_proposal)

        self.assertEqual(str(initial_proposal), str(new_proposal))

    def test_apply_budget_constraint(self):
        # Simulate low volatility
        self.simulate_low_volatility(self.strategy)

        # Simulate high liquidity
        self.simulate_high_liquidity(self.strategy)

        # Prepare market variables and parameters for calculation
        self.strategy.measure_order_book_liquidity()
        self.strategy.calculate_reservation_price_and_optimal_spread()

        # Create a basic proposal.
        order_amount: Decimal = self.market.quantize_order_amount(self.trading_pair, self.order_amount)
        bid_price: Decimal = self.market.quantize_order_price(self.trading_pair, self.strategy.optimal_bid)
        ask_price: Decimal = self.market.quantize_order_price(self.trading_pair, self.strategy.optimal_ask)

        initial_proposal: Proposal = Proposal([PriceSize(bid_price, order_amount)], [PriceSize(ask_price, order_amount)])

        # Test (1) Base & Quote balance < Base & Quote sizes in Proposal

        # Modify base_balance and quote_balance
        self.market.set_balance("COINALPHA", int(self.order_amount * Decimal("0.5")))
        self.market.set_balance("HBOT", int(self.order_amount * Decimal("0.5")))

        # Calculate expected proposal
        proposal = deepcopy(initial_proposal)
        base_balance, quote_balance = self.strategy.get_adjusted_available_balance(self.strategy.active_orders)
        buy_fee: AddedToCostTradeFee = self.market.get_fee(self.base_asset,
                                                           self.quote_asset,
                                                           OrderType.LIMIT,
                                                           TradeType.BUY,
                                                           proposal.buys[0].size,
                                                           proposal.buys[0].price)
        buy_adjusted_amount: Decimal = quote_balance / (proposal.buys[0].price * (Decimal("1") + buy_fee.percent))
        expected_buy_amount: Decimal = self.market.quantize_order_amount(self.trading_pair, buy_adjusted_amount)
        expected_sell_amount = self.market.quantize_order_amount(self.trading_pair, base_balance)
        expected_proposal: Proposal = Proposal(
            [PriceSize(bid_price, expected_buy_amount)],
            [PriceSize(ask_price, expected_sell_amount)]
        )

        self.strategy.apply_budget_constraint(proposal)

        self.assertEqual(str(expected_proposal), str(proposal))

        # Test (2) Base & Quote balance = s_decimal_zero

        # Modify base_balance and quote_balance
        self.market.set_balance("COINALPHA", 0)
        self.market.set_balance("HBOT", 0)

        proposal = deepcopy(initial_proposal)
        self.strategy.apply_budget_constraint(proposal)

        expected_proposal: Proposal = Proposal([], [])

        self.assertEqual(str(expected_proposal), str(proposal))

    def test_apply_order_amount_eta_transformation(self):
        # Simulate low volatility
        self.simulate_low_volatility(self.strategy)

        # Simulate high liquidity
        self.simulate_high_liquidity(self.strategy)

        # Prepare market variables and parameters for calculation
        self.strategy.measure_order_book_liquidity()
        self.strategy.calculate_reservation_price_and_optimal_spread()

        # Create a basic proposal.
        order_amount: Decimal = self.market.quantize_order_amount(self.trading_pair, self.order_amount)
        bid_price: Decimal = self.market.quantize_order_price(self.trading_pair, self.strategy.optimal_bid)
        ask_price: Decimal = self.market.quantize_order_price(self.trading_pair, self.strategy.optimal_ask)

        initial_proposal: Proposal = Proposal(
            [PriceSize(bid_price, order_amount)],
            [PriceSize(ask_price, order_amount)]
        )

        # Test (1) Check proposal when order_override is NOT None
        proposal: Proposal = deepcopy(initial_proposal)

        order_override = {
            "order_1": ["sell", 2.5, 100],
            "order_2": ["buy", 0.5, 100]
        }

        # Re-configure strategy with order_ride configurations
        self.config_map.order_override = order_override

        self.strategy.apply_order_amount_eta_transformation(proposal)

        self.assertEqual(str(initial_proposal), str(proposal))

        # Test (2) Check proposal when order_override is None

        # Re-configure strategy with order_ride configurations
        self.config_map.order_override = None

        proposal: Proposal = deepcopy(initial_proposal)

        # Case(1): if q < 0
        eta: Decimal = self.strategy.eta
        q: Decimal = self.market.get_balance(self.base_asset) - self.strategy.calculate_target_inventory()

        expected_bid_amount: Decimal = proposal.buys[0].size
        expected_ask_amount: Decimal = self.market.quantize_order_amount(self.trading_pair,
                                                                         proposal.sells[0].size * Decimal.exp(eta * q))
        expected_proposal: Proposal = Proposal(
            [PriceSize(proposal.buys[0].price, expected_bid_amount)],
            [PriceSize(proposal.sells[0].price, expected_ask_amount)]
        )

        self.strategy.apply_order_amount_eta_transformation(proposal)
        self.assertEqual(str(expected_proposal), str(proposal))

        # Case(2): if q > 0
        self.market.set_balance("COINALPHA", 100)
        eta: Decimal = self.strategy.eta
        q: Decimal = self.market.get_balance(self.base_asset) - self.strategy.calculate_target_inventory()

        expected_bid_amount: Decimal = self.market.quantize_order_amount(self.trading_pair,
                                                                         proposal.buys[0].size * Decimal.exp(-eta * q))
        expected_ask_amount: Decimal = proposal.sells[0].size

        expected_proposal: Proposal = Proposal(
            [PriceSize(proposal.buys[0].price, expected_bid_amount)],
            [PriceSize(proposal.sells[0].price, expected_ask_amount)]
        )

        self.strategy.apply_order_amount_eta_transformation(proposal)
        self.assertEqual(str(expected_proposal), str(proposal))

    def test_is_within_tolerance(self):
        bid_price: Decimal = Decimal("99.5")
        ask_price: Decimal = Decimal("101.5")

        buy_prices: List[Decimal] = [bid_price]
        sell_prices: List[Decimal] = [ask_price]

        bid_price: Decimal = Decimal("98.5")
        ask_price: Decimal = Decimal("100.5")

        proposal: Proposal = Proposal(
            [PriceSize(bid_price, self.order_amount)],  # Bids
            [PriceSize(ask_price, self.order_amount)]   # Sells
        )
        proposal_buys = [buy.price for buy in proposal.buys]
        proposal_sells = [sell.price for sell in proposal.sells]

        # Default order_refresh_tolerance_pct is 0. So it will always NOT be within tolerance
        self.assertFalse(self.strategy.is_within_tolerance(buy_prices, proposal_buys))
        self.assertFalse(self.strategy.is_within_tolerance(sell_prices, proposal_sells))

        self.config_map.order_refresh_tolerance_pct = Decimal("10.0")

        self.assertTrue(self.strategy.is_within_tolerance(buy_prices, proposal_buys))
        self.assertTrue(self.strategy.is_within_tolerance(sell_prices, proposal_sells))

    def test_cancel_active_orders(self):

        bid_price: Decimal = Decimal("99.5")
        ask_price: Decimal = Decimal("101.5")
        proposal: Proposal = Proposal(
            [PriceSize(bid_price, self.order_amount)],  # Bids
            [PriceSize(ask_price, self.order_amount)]   # Sells
        )

        limit_buy_order: LimitOrder = LimitOrder(client_order_id="test",
                                                 trading_pair=self.trading_pair,
                                                 is_buy=True,
                                                 base_currency=self.trading_pair.split("-")[0],
                                                 quote_currency=self.trading_pair.split("-")[1],
                                                 price=bid_price,
                                                 quantity=self.order_amount)
        limit_sell_order: LimitOrder = LimitOrder(client_order_id="test",
                                                  trading_pair=self.trading_pair,
                                                  is_buy=False,
                                                  base_currency=self.trading_pair.split("-")[0],
                                                  quote_currency=self.trading_pair.split("-")[1],
                                                  price=ask_price,
                                                  quantity=self.order_amount)

        # Case (1): No orders to cancel
        self.strategy.cancel_active_orders(proposal)
        self.assertEqual(0, len(self.strategy.active_orders))

        # Case (2): Has active orders and within _order_refresh_tolerance_pct.
        # Note: Order will NOT be cancelled
        self.config_map.order_refresh_tolerance_pct = Decimal("10")
        self.simulate_place_limit_order(self.strategy, self.market_info, limit_buy_order)
        self.simulate_place_limit_order(self.strategy, self.market_info, limit_sell_order)
        self.assertEqual(2, len(self.strategy.active_orders))

        # Case (3a): Has active orders and EXCEED _order_refresh_tolerance_pct BUT cancel_timestamp > current_timestamp
        # Note: Orders will NOT be cancelled
        self.config_map.order_refresh_tolerance_pct = Decimal("0")
        bid_price: Decimal = Decimal("98.5")
        ask_price: Decimal = Decimal("100.5")
        proposal: Proposal = Proposal(
            [PriceSize(bid_price, self.order_amount)],  # Bids
            [PriceSize(ask_price, self.order_amount)]   # Sells
        )
        self.assertEqual(2, len(self.strategy.active_orders))

        self.strategy.cancel_active_orders(proposal)

        self.assertEqual(2, len(self.strategy.active_orders))

        # Case (3b): Has active orders and EXCEED _order_refresh_tolerance_pct AND cancel_timestamp <= current_timestamp
        # Note: Orders will be cancelled
        self.clock.backtest_til(self.strategy.current_timestamp + self.strategy.order_refresh_time + 1)

        self.strategy.cancel_active_orders(proposal)

        self.assertEqual(0, len(self.strategy.active_orders))

        # Case (4): Has active orders and within _order_refresh_tolerance_pct BUT cancel_timestamp > current_timestamp
        # Note: Order not cancelled
        self.config_map.order_refresh_tolerance_pct = Decimal("10")
        self.simulate_place_limit_order(self.strategy, self.market_info, limit_buy_order)
        self.simulate_place_limit_order(self.strategy, self.market_info, limit_sell_order)
        self.assertEqual(2, len(self.strategy.active_orders))

        self.strategy.cancel_active_orders(proposal)

        self.assertEqual(2, len(self.strategy.active_orders))

        # Case (5): Has active orders and within _order_refresh_tolerance_pct AND cancel_timestamp <= current_timestamp
        self.config_map.order_refresh_tolerance_pct = s_decimal_neg_one

        self.clock.backtest_til(self.strategy.current_timestamp + self.strategy.order_refresh_time + 1)

        self.strategy.cancel_active_orders(proposal)

        self.assertEqual(0, len(self.strategy.active_orders))

    def test_to_create_orders(self):
        # Simulate order being placed. Placing an order updates create_timestamp = next_cycle
        limit_buy_order: LimitOrder = LimitOrder(client_order_id="test",
                                                 trading_pair=self.trading_pair,
                                                 is_buy=True,
                                                 base_currency=self.trading_pair.split("-")[0],
                                                 quote_currency=self.trading_pair.split("-")[1],
                                                 price=Decimal("99"),
                                                 quantity=self.order_amount)
        limit_sell_order: LimitOrder = LimitOrder(client_order_id="test",
                                                  trading_pair=self.trading_pair,
                                                  is_buy=False,
                                                  base_currency=self.trading_pair.split("-")[0],
                                                  quote_currency=self.trading_pair.split("-")[1],
                                                  price=Decimal("101"),
                                                  quantity=self.order_amount)
        self.simulate_place_limit_order(self.strategy, self.market_info, limit_buy_order)
        self.simulate_place_limit_order(self.strategy, self.market_info, limit_sell_order)

        # Simulate new proposal being created
        bid_price: Decimal = Decimal("99.5")
        ask_price: Decimal = Decimal("101.5")
        proposal: Proposal = Proposal(
            [PriceSize(bid_price, self.order_amount)],  # Bids
            [PriceSize(ask_price, self.order_amount)]  # Sells
        )

        # Case (1) create_timestamp < current_timestamp
        self.assertFalse(self.strategy.to_create_orders(proposal))

        # Case (2) create_timestamp >= current_timestamp + order_refresh_time
        self.clock.backtest_til(self.start_timestamp + self.strategy.order_refresh_time + 1)
        self.simulate_cancelling_all_active_orders(self.strategy)

        self.assertTrue(self.strategy.to_create_orders(proposal))

    def test_existing_hanging_orders_are_included_in_budget_constraint(self):
        config_settings = {
            "exchange": self.market.name,
            "market": self.trading_pair,
            "execution_timeframe_mode": "infinite",
            "order_amount": self.order_amount,
            "min_spread": self.min_spread,
            "risk_factor": self.risk_factor_finite,
            "hanging_orders_mode": {"hanging_orders_cancel_pct": 99},
            "order_refresh_time": "60",
            "inventory_target_base_pct": self.inventory_target_base_pct,
            "filled_order_delay": 30,
        }
        config_map = ClientConfigAdapter(AvellanedaMarketMakingConfigMap(**config_settings))

        self.market.set_balance("COINALPHA", 100)
        self.market.set_balance("HBOT", 50000)

        # Create a new strategy, with hanging orders enabled
        self.strategy = AvellanedaMarketMakingStrategy()
        self.strategy.init_params(
            config_map=config_map,
            market_info=self.market_info,
        )

        # Create a new clock to start the strategy from scratch
        self.clock: Clock = Clock(ClockMode.BACKTEST, self.clock_tick_size, self.start_timestamp, self.end_timestamp)
        self.clock.add_iterator(self.market)
        self.clock.add_iterator(self.strategy)

        self.strategy.avg_vol = self.avg_vol_indicator
        self.clock.add_iterator(self.strategy)

        self.clock.backtest_til(self.start_timestamp + self.clock_tick_size)

        # Simulate low volatility
        self.simulate_low_volatility(self.strategy)

        # Simulate high liquidity
        self.simulate_high_liquidity(self.strategy)

        # Prepare market variables and parameters for calculation
        self.strategy.measure_order_book_liquidity()
        self.strategy.calculate_reservation_price_and_optimal_spread()

        self.clock.backtest_til(self.start_timestamp + self.clock_tick_size * 2)

        buy_order = self.strategy.active_buys[0]
        sell_order = self.strategy.active_sells[0]

        # Simulate market fill for limit sell.
        self.simulate_limit_order_fill(self.market, sell_order)

        # The buy order should turn into a hanging when it reaches its refresh time
        self.clock.backtest_til(self.start_timestamp + self.strategy.order_refresh_time + 2)
        self.assertEqual(1, len(self.strategy.hanging_orders_tracker.strategy_current_hanging_orders))
        self.assertEqual(buy_order.client_order_id,
                         list(self.strategy.hanging_orders_tracker.strategy_current_hanging_orders)[0].order_id)

        current_base_balance, current_quote_balance = self.strategy.adjusted_available_balance_for_orders_budget_constrain()
        expected_base_balance = (sum([order.quantity
                                      for order in self.strategy.active_non_hanging_orders
                                      if not order.is_buy])
                                 + self.market.get_available_balance(self.market_info.base_asset))
        expected_quote_balance = (sum([order.quantity * order.price
                                       for order in self.strategy.active_non_hanging_orders
                                       if order.is_buy])
                                  + self.market.get_available_balance(self.market_info.quote_asset))

        self.assertEqual(expected_base_balance, current_base_balance)
        self.assertEqual(expected_quote_balance, current_quote_balance)

    def test_not_filled_order_changed_to_hanging_order_after_refresh_time(self):
        hanging_orders_model = TrackHangingOrdersModel()
        hanging_orders_model.hanging_orders_cancel_pct = "99"

        # Refresh has to happend after filled_order_delay
        refresh_time = 80
        filled_extension_time = 60

        config_settings = {
            "exchange": self.market.name,
            "market": self.trading_pair,
            "execution_timeframe_mode": "infinite",
            "order_amount": self.order_amount,
            "order_optimization_enabled": "yes",
            "min_spread": self.min_spread,
            "risk_factor": self.risk_factor_finite,
            "order_refresh_time": refresh_time,
            "inventory_target_base_pct": self.inventory_target_base_pct,
            "hanging_orders_mode": hanging_orders_model,
            "filled_order_delay": filled_extension_time
        }
        config_map = ClientConfigAdapter(AvellanedaMarketMakingConfigMap(**config_settings))

        self.market.set_balance("COINALPHA", 100)
        self.market.set_balance("HBOT", 50000)

        # Create a new strategy, with hanging orders enabled
        self.strategy = AvellanedaMarketMakingStrategy()
        self.strategy.init_params(
            config_map=config_map,
            market_info=self.market_info,
        )

        # Create a new clock to start the strategy from scratch
        self.clock: Clock = Clock(ClockMode.BACKTEST, self.clock_tick_size, self.start_timestamp, self.end_timestamp)
        self.clock.add_iterator(self.market)
        self.clock.add_iterator(self.strategy)

        self.strategy.avg_vol = self.avg_vol_indicator

        self.clock.backtest_til(self.start_timestamp + self.clock_tick_size)

        # Simulate low volatility
        self.simulate_low_volatility(self.strategy)

        # Simulate high liquidity
        self.simulate_high_liquidity(self.strategy)

        # Prepare market variables and parameters for calculation
        self.strategy.measure_order_book_liquidity()
        self.strategy.calculate_reservation_price_and_optimal_spread()

        self.clock.backtest_til(self.start_timestamp + self.clock_tick_size * 2)

        buy_order = self.strategy.active_buys[0]
        sell_order = self.strategy.active_sells[0]

        orders_creation_timestamp = self.strategy.current_timestamp

        # Advance the clock some ticks and simulate market fill for limit sell
        self.clock.backtest_til(orders_creation_timestamp + 10)
        self.simulate_limit_order_fill(self.market, sell_order)
        self.assertEqual(buy_order.client_order_id,
                         self.strategy.active_non_hanging_orders[0].client_order_id)

        # The buy order should turn into a hanging when it reaches its refresh time
        self.clock.backtest_til(orders_creation_timestamp + refresh_time - 1)
        self.assertEqual(2, len(self.strategy.active_non_hanging_orders))

        # After refresh time the buy order that was candidate to hanging order should be turned into a hanging order
        self.clock.backtest_til(orders_creation_timestamp + refresh_time)
        # New orders get created
        self.assertEqual(2, len(self.strategy.active_non_hanging_orders))
        self.assertEqual(1, len(self.strategy.hanging_orders_tracker.strategy_current_hanging_orders))
        self.assertEqual(buy_order.client_order_id,
                         list(self.strategy.hanging_orders_tracker.strategy_current_hanging_orders)[0].order_id)

        # The new pair of orders should be created only after the fill delay time
        self.clock.backtest_til(orders_creation_timestamp + 10 + filled_extension_time - 1)
        # New orders get created
        self.assertEqual(2, len(self.strategy.active_non_hanging_orders))
        self.clock.backtest_til(orders_creation_timestamp + 10 + filled_extension_time + 1)
        self.assertEqual(2, len(self.strategy.active_non_hanging_orders))
        # The hanging order should still be present
        self.assertEqual(1, len(self.strategy.hanging_orders_tracker.strategy_current_hanging_orders))
        self.assertEqual(buy_order.client_order_id,
                         list(self.strategy.hanging_orders_tracker.strategy_current_hanging_orders)[0].order_id)

    def test_no_new_orders_created_until_previous_orders_cancellation_confirmed(self):

        refresh_time = self.strategy.order_refresh_time

        self.strategy.avg_vol = self.avg_vol_indicator
        self.config_map.order_refresh_tolerance_pct = -1

        # Simulate low volatility
        self.simulate_low_volatility(self.strategy)

        # Simulate high liquidity
        self.simulate_high_liquidity(self.strategy)

        # Prepare market variables and parameters for calculation
        self.strategy.measure_order_book_liquidity()
        self.strategy.calculate_reservation_price_and_optimal_spread()

        self.clock.backtest_til(self.start_timestamp + self.clock_tick_size)

        self.assertEqual(1, len(self.strategy.active_buys))
        self.assertEqual(1, len(self.strategy.active_sells))

        orders_creation_timestamp = self.strategy.current_timestamp

        # Add a fake in flight cancellation to simulate a confirmation has not arrived
        self.strategy._sb_order_tracker.in_flight_cancels["OID-99"] = self.strategy.current_timestamp

        # After refresh time the two real orders should be cancelled, but no new order should be created
        self.clock.backtest_til(orders_creation_timestamp + refresh_time)
        self.assertEqual(0, len(self.strategy.active_buys))
        self.assertEqual(0, len(self.strategy.active_sells))

        # After a second refresh time no new order should be created
        self.clock.backtest_til(orders_creation_timestamp + (2 * refresh_time))
        self.assertEqual(0, len(self.strategy.active_buys))
        self.assertEqual(0, len(self.strategy.active_sells))

        del self.strategy._sb_order_tracker.in_flight_cancels["OID-99"]

        # After removing the pending cancel, in the next tick the new orders should be created
        self.clock.backtest_til(self.strategy.current_timestamp + 1)
        self.assertEqual(1, len(self.strategy.active_buys))
        self.assertEqual(1, len(self.strategy.active_sells))

    def test_adjusted_available_balance_considers_in_flight_cancel_orders(self):
        base_balance = self.market.get_available_balance(self.base_asset)
        quote_balance = self.market.get_available_balance(self.quote_asset)

        self.strategy._sb_order_tracker.start_tracking_limit_order(
            market_pair=self.market_info,
            order_id="OID-1",
            is_buy=True,
            price=Decimal(1000),
            quantity=Decimal(1))
        self.strategy._sb_order_tracker.start_tracking_limit_order(
            market_pair=self.market_info,
            order_id="OID-2",
            is_buy=False,
            price=Decimal(2000),
            quantity=Decimal(2))

        self.strategy._sb_order_tracker.in_flight_cancels["OID-1"] = self.strategy.current_timestamp

        available_base_balance, available_quote_balance = self.strategy.adjusted_available_balance_for_orders_budget_constrain()

        self.assertEqual(available_base_balance, base_balance + Decimal(2))
        self.assertEqual(available_quote_balance, quote_balance + (Decimal(1) * Decimal(1000)))
