from logbook import Logger

from catalyst.api import (
    record,
    order,
    symbol,
    get_open_orders
)
from catalyst.exchange.utils.stats_utils import get_pretty_stats
from catalyst.utils.run_algo import run_algorithm

algo_namespace = 'arbitrage_eth_btc'
log = Logger(algo_namespace)


def initialize(context):
    log.info('initializing arbitrage algorithm')

    # The context contains a new "exchanges" attribute which is a dictionary
    # of exchange objects by exchange name. This allow easy access to the
    # exchanges.
    context.buying_exchange = context.exchanges['poloniex']
    context.selling_exchange = context.exchanges['bitfinex']

    context.trading_pair_symbol = 'eth_btc'
    context.trading_pairs = dict()

    # Note the second parameter of the symbol() method
    # Passing the exchange name here returns a TradingPair object including
    # the exchange information. This allow all other operations using
    # the TradingPair to target the correct exchange.
    context.trading_pairs[context.buying_exchange] = \
        symbol('eth_btc', context.buying_exchange.name)

    context.trading_pairs[context.selling_exchange] = \
        symbol(context.trading_pair_symbol, context.selling_exchange.name)

    context.entry_points = [
        dict(gap=0.03, amount=0.05),
        dict(gap=0.04, amount=0.1),
        dict(gap=0.05, amount=0.5),
    ]
    context.exit_points = [
        dict(gap=-0.02, amount=0.5),
    ]

    context.SLIPPAGE_ALLOWED = 0.02
    pass


def place_orders(context, amount, buying_price, selling_price, action):
    """
    This method will always place two orders of the same amount to keep
    the currency position the same as it moves between the two exchanges.

    :param context: TradingAlgorithm
    :param amount: float
        The trading pair amount to trade on both exchanges.
    :param buying_price: float
        The current trading pair price on the buying exchange.
    :param selling_price: float
        The current trading pair price on the selling exchange.
    :param action: string
        "enter": buys on the buying exchange and sells on the selling exchange
        "exit": buys on the selling exchange and sells on the buying exchange

    :return:
    """
    if action == 'enter':
        enter_exchange = context.buying_exchange
        entry_price = buying_price

        exit_exchange = context.selling_exchange
        exit_price = selling_price

    elif action == 'exit':
        enter_exchange = context.selling_exchange
        entry_price = selling_price

        exit_exchange = context.buying_exchange
        exit_price = buying_price

    else:
        raise ValueError('invalid order action')

    quote_currency = enter_exchange.quote_currency
    quote_currency_amount = enter_exchange.portfolio.cash

    exit_balances = exit_exchange.get_balances()
    exit_currency = context.trading_pairs[
        context.selling_exchange].quote_currency

    if exit_currency in exit_balances:
        quote_currency_amount = exit_balances[exit_currency]
    else:
        log.warn(
            'the selling exchange {exchange_name} does not hold '
            'currency {currency}'.format(
                exchange_name=exit_exchange.name,
                currency=exit_currency
            )
        )
        return

    if quote_currency_amount < (amount * entry_price):
        adj_amount = quote_currency_amount / entry_price
        log.warn(
            'not enough {quote_currency} ({quote_currency_amount}) to buy '
            '{amount}, adjusting the amount to {adj_amount}'.format(
                quote_currency=quote_currency,
                quote_currency_amount=quote_currency_amount,
                amount=amount,
                adj_amount=adj_amount
            )
        )
        amount = adj_amount

    elif quote_currency_amount < amount:
        log.warn(
            'not enough {currency} ({currency_amount}) to sell '
            '{amount}, aborting'.format(
                currency=exit_currency,
                currency_amount=quote_currency_amount,
                amount=amount
            )
        )
        return

    adj_buy_price = entry_price * (1 + context.SLIPPAGE_ALLOWED)
    log.info(
        'buying {amount} {trading_pair} on {exchange_name} with price '
        'limit {limit_price}'.format(
            amount=amount,
            trading_pair=context.trading_pair_symbol,
            exchange_name=enter_exchange.name,
            limit_price=adj_buy_price
        )
    )
    order(
        asset=context.trading_pairs[enter_exchange],
        amount=amount,
        limit_price=adj_buy_price
    )

    adj_sell_price = exit_price * (1 - context.SLIPPAGE_ALLOWED)
    log.info(
        'selling {amount} {trading_pair} on {exchange_name} with price '
        'limit {limit_price}'.format(
            amount=-amount,
            trading_pair=context.trading_pair_symbol,
            exchange_name=exit_exchange.name,
            limit_price=adj_sell_price
        )
    )
    order(
        asset=context.trading_pairs[exit_exchange],
        amount=-amount,
        limit_price=adj_sell_price
    )
    pass


def handle_data(context, data):
    log.info('handling bar {}'.format(data.current_dt))

    buying_price = data.current(
        context.trading_pairs[context.buying_exchange], 'price')

    log.info('price on buying exchange {exchange}: {price}'.format(
        exchange=context.buying_exchange.name.upper(),
        price=buying_price,
    ))

    selling_price = data.current(
        context.trading_pairs[context.selling_exchange], 'price')

    log.info('price on selling exchange {exchange}: {price}'.format(
        exchange=context.selling_exchange.name.upper(),
        price=selling_price,
    ))

    # If for example,
    #   selling price = 50
    #   buying price = 25
    #   expected gap = 1

    # If follows that,
    #   selling price - buying price / buying price
    #   50 - 25 / 25 = 1
    gap = (selling_price - buying_price) / buying_price
    log.info(
        'the price gap: {gap} ({gap_percent}%)'.format(
            gap=gap,
            gap_percent=gap * 100
        )
    )
    record(buying_price=buying_price, selling_price=selling_price, gap=gap)

    # Waiting for orders to close before initiating new ones
    for exchange in context.trading_pairs:
        asset = context.trading_pairs[exchange]

        orders = get_open_orders(asset)
        if orders:
            log.info(
                'found {order_count} open orders on {exchange_name} '
                'skipping bar until all open orders execute'.format(
                    order_count=len(orders),
                    exchange_name=exchange.name
                )
            )
            return

    # Consider the least ambitious entry point first
    # Override of wider gap is found
    entry_points = sorted(
        context.entry_points,
        key=lambda point: point['gap'],
    )

    buy_amount = None
    for entry_point in entry_points:
        if gap > entry_point['gap']:
            buy_amount = entry_point['amount']

    if buy_amount:
        log.info('found buy trigger for amount: {}'.format(buy_amount))
        place_orders(
            context=context,
            amount=buy_amount,
            buying_price=buying_price,
            selling_price=selling_price,
            action='enter'
        )

    else:
        # Consider the narrowest exit gap first
        # Override of wider gap is found
        exit_points = sorted(
            context.exit_points,
            key=lambda point: point['gap'],
            reverse=True
        )

        sell_amount = None
        for exit_point in exit_points:
            if gap < exit_point['gap']:
                sell_amount = exit_point['amount']

        if sell_amount:
            log.info('found sell trigger for amount: {}'.format(sell_amount))
            place_orders(
                context=context,
                amount=sell_amount,
                buying_price=buying_price,
                selling_price=selling_price,
                action='exit'
            )


def analyze(context, stats):
    log.info('the daily stats:\n{}'.format(get_pretty_stats(stats)))
    pass


if __name__ == '__main__':
    # The execution mode: backtest or live
    MODE = 'live'
    if MODE == 'live':
        run_algorithm(
            capital_base=0.1,
            initialize=initialize,
            handle_data=handle_data,
            analyze=analyze,
            exchange_name='poloniex,bitfinex',
            live=True,
            algo_namespace=algo_namespace,
            base_currency='btc',
            live_graph=False,
            simulate_orders=True,
            stats_output=None,
        )
