#!/usr/bin/env python2
# -*- coding: utf-8 -*-
"""
Tool to display live market info and
framework for experimenting with trading bots
"""
#  Copyright (c) 2013 Bernd Kreuss <prof7bit@gmail.com>
#
#  This program is free software; you can redistribute it and/or modify
#  it under the terms of the GNU General Public License as published by
#  the Free Software Foundation; either version 3 of the License, or
#  (at your option) any later version.
#
#  This program is distributed in the hope that it will be useful,
#  but WITHOUT ANY WARRANTY; without even the implied warranty of
#  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
#  GNU General Public License for more details.
#
#  You should have received a copy of the GNU General Public License
#  along with this program; if not, write to the Free Software
#  Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston,
#  MA 02110-1301, USA.

import argparse
import curses
import curses.panel
import curses.textpad
import api
import logging
import locale
import math
import os
import sys
import time
import textwrap
import traceback
import threading

sys_out = sys.stdout

#
# curses user interface
#

HEIGHT_STATUS = 2
HEIGHT_CON = 20
WIDTH_ORDERBOOK = 40

COLORS = [["con_text", curses.COLOR_BLACK, curses.COLOR_WHITE],
          ["con_text_buy", curses.COLOR_BLACK, curses.COLOR_GREEN],
          ["con_text_sell", curses.COLOR_BLACK, curses.COLOR_RED],
          ["con_separator", curses.COLOR_BLUE, curses.COLOR_WHITE],
          ["status_text", curses.COLOR_BLACK, curses.COLOR_WHITE],

          ["book_text", curses.COLOR_BLACK, curses.COLOR_CYAN],
          ["book_bid", curses.COLOR_BLACK, curses.COLOR_GREEN],
          ["book_ask", curses.COLOR_BLACK, curses.COLOR_RED],
          ["book_own", curses.COLOR_BLACK, curses.COLOR_YELLOW],
          ["book_vol", curses.COLOR_BLACK, curses.COLOR_CYAN],

          ["chart_text", curses.COLOR_BLACK, curses.COLOR_WHITE],
          ["chart_up", curses.COLOR_BLACK, curses.COLOR_GREEN],
          ["chart_down", curses.COLOR_BLACK, curses.COLOR_RED],
          ["order_pending", curses.COLOR_BLACK, curses.COLOR_RED],

          ["dialog_text", curses.COLOR_BLUE, curses.COLOR_CYAN],
          ["dialog_sel", curses.COLOR_CYAN, curses.COLOR_BLUE],
          ["dialog_sel_text", curses.COLOR_BLUE, curses.COLOR_YELLOW],
          ["dialog_sel_sel", curses.COLOR_YELLOW, curses.COLOR_BLUE],
          ["dialog_bid_text", curses.COLOR_GREEN, curses.COLOR_BLACK],
          ["dialog_ask_text", curses.COLOR_RED, curses.COLOR_WHITE]]

INI_DEFAULTS = [["pytrader", "exchange", "kraken"],
                ["pytrader", "set_xterm_title", "True"],
                ["pytrader", "dont_truncate_logfile", "False"],
                ["pytrader", "show_orderbook_stats", "True"],
                ["pytrader", "highlight_changes", "True"],
                ["pytrader", "orderbook_group", "0"],
                ["pytrader", "orderbook_sum_total", "False"],
                ["pytrader", "display_right", "history_chart"],
                ["pytrader", "depth_chart_group", "0.00001"],
                ["pytrader", "depth_chart_sum_total", "True"],
                ["pytrader", "show_ticker", "True"],
                ["pytrader", "show_depth", "True"],
                ["pytrader", "show_trade", "True"],
                ["pytrader", "show_trade_own", "True"]]

COLOR_PAIR = {}

def init_colors():
    """initialize curses color pairs and give them names. The color pair
    can then later quickly be retrieved from the COLOR_PAIR[] dict"""
    index = 1
    for (name, back, fore) in COLORS:
        if curses.has_colors():
            curses.init_pair(index, fore, back)
            COLOR_PAIR[name] = curses.color_pair(index)
        else:
            COLOR_PAIR[name] = 0
        index += 1

def dump_all_stacks():
    """dump a stack trace for all running threads for debugging purpose"""

    def get_name(thread_id):
        """return the human readable name that was assigned to a thread"""
        for thread in threading.enumerate():
            if thread.ident == thread_id:
                return thread.name

    ret = "\n# Full stack trace of all running threads:\n"
    for thread_id, stack in sys._current_frames().items():
        ret += "\n# %s (%s)\n" % (get_name(thread_id), thread_id)
        for filename, lineno, name, line in traceback.extract_stack(stack):
            ret += 'File: "%s", line %d, in %s\n' % (filename, lineno, name)
            if line:
                ret += "  %s\n" % (line.strip())
    return ret

def try_get_lock_or_break_open():
    """this is an ugly hack to workaround possible deadlock problems.
    It is used during shutdown to make sure we can properly exit even when
    some slot is stuck (due to a programming error) and won't release the lock.
    If we can't acquire it within 5 seconds we just break it open forcefully."""
    time_end = time.time() + 5
    while time.time() < time_end:
        if api.Signal._lock.acquire(False):
            return
        time.sleep(0.001)

    # something keeps holding the lock, apparently some slot is stuck
    # in an infinite loop. In order to be able to shut down anyways
    # we just throw away that lock and replace it with a new one
    lock = threading.RLock()
    lock.acquire()
    api.Signal._lock = lock
    print "### could not acquire signal lock, frozen slot somewhere?"
    print "### please see the stacktrace log to determine the cause."

class Win:
    """represents a curses window"""

    def __init__(self, stdscr):
        """create and initialize the window. This will also subsequently
        call the paint() method."""
        self.stdscr = stdscr
        self.posx = 0
        self.posy = 0
        self.width = 10
        self.height = 10
        self.termwidth = 10
        self.termheight = 10
        self.win = None
        self.panel = None
        self.__create_win()

    def __del__(self):
        del self.panel
        del self.win
        curses.panel.update_panels()
        curses.doupdate()

    def calc_size(self):
        """override this method to change posx, posy, width, height.
        It will be called before window creation and on resize."""
        pass

    def do_paint(self):
        """call this if you want the window to repaint itself"""
        curses.curs_set(0)
        if self.win:
            self.paint()
            self.done_paint()

    # method could be a function
    def done_paint(self):
        """update the sreen after paint operations, this will invoke all
        necessary stuff to refresh all (possibly overlapping) windows in
        the right order and then push it to the screen"""
        curses.panel.update_panels()
        curses.doupdate()

    def paint(self):
        """paint the window. Override this with your own implementation.
        This method must paint the entire window contents from scratch.
        It is automatically called after the window has been initially
        created and also after every resize. Call it explicitly when
        your data has changed and must be displayed"""
        pass

    def resize(self):
        """You must call this method from your main loop when the
        terminal has been resized. It will subsequently make it
        recalculate its own new size and then call its paint() method"""
        del self.win
        self.__create_win()

    def addstr(self, *args):
        """drop-in replacement for addstr that will never raise exceptions
        and that will cut off at end of line instead of wrapping"""
        if len(args) > 0:
            line, col = self.win.getyx()
            string = args[0]
            attr = 0
        if len(args) > 1:
            attr = args[1]
        if len(args) > 2:
            line, col, string = args[:3]
            attr = 0
        if len(args) > 3:
            attr = args[3]
        if line >= self.height:
            return
        space_left = self.width - col - 1  # always omit last column, avoids problems.
        if space_left <= 0:
            return
        self.win.addstr(line, col, string[:space_left], attr)

    def addch(self, posy, posx, character, color_pair):
        """place a character but don't throw error in lower right corner"""
        if posy < 0 or posy > self.height - 1:
            return
        if posx < 0 or posx > self.width - 1:
            return
        if posx == self.width - 1 and posy == self.height - 1:
            return
        self.win.addch(posy, posx, character, color_pair)

    def __create_win(self):
        """create the window. This will also be called on every resize,
        windows won't be moved, they will be deleted and recreated."""
        self.__calc_size()
        try:
            self.win = curses.newwin(self.height, self.width, self.posy, self.posx)
            self.panel = curses.panel.new_panel(self.win)
            self.win.scrollok(True)
            self.win.keypad(1)
            self.do_paint()
        except Exception:
            self.win = None
            self.panel = None

    def __calc_size(self):
        """calculate the default values for positionand size. By default
        this will result in a window covering the entire terminal.
        Implement the calc_size() method (which will be called afterwards)
        to change (some of) these values according to your needs."""
        maxyx = self.stdscr.getmaxyx()
        self.termwidth = maxyx[1]
        self.termheight = maxyx[0]
        self.posx = 0
        self.posy = 0
        self.width = self.termwidth
        self.height = self.termheight
        self.calc_size()


class WinConsole(Win):
    """The console window at the bottom"""
    def __init__(self, stdscr, instance):
        """create the console window and connect it to the instance's debug
        callback function"""
        self.instance = instance
        instance.signal_debug.connect(self.slot_debug)
        Win.__init__(self, stdscr)

    def paint(self):
        """just empty the window after resize (I am lazy)"""
        self.win.bkgd(" ", COLOR_PAIR["con_text"])

    def resize(self):
        """resize and print a log message. Old messages will have been
        lost after resize because of my dumb paint() implementation, so
        at least print a message indicating that fact into the
        otherwise now empty console window"""
        Win.resize(self)
        self.write("### console has been resized")

    def calc_size(self):
        """put it at the bottom of the screen"""
        self.height = HEIGHT_CON
        self.width = self.termwidth - int(self.termwidth / 2) - 2
        self.posy = self.termheight - self.height

    def slot_debug(self, dummy_instance, (txt)):
        """this slot will be connected to all debug signals."""
        if txt.startswith('[c]'):
            self.write("\n   ".join(textwrap.wrap(txt.replace('[c]', ''), self.width - 3)))
        elif not txt.startswith('[s]'):
            self.write(textwrap.fill(txt, self.width))

    def write(self, txt):
        """write a line of text, scroll if needed"""
        if not self.win:
            return

        # This code would break if the format of
        # the log messages would ever change!
        if " tick:" in txt:
            if not self.instance.config.get_bool("pytrader", "show_ticker"):
                return
        if "depth:" in txt:
            if not self.instance.config.get_bool("pytrader", "show_depth"):
                return
        if "trade:" in txt:
            if "own order" in txt:
                if not self.instance.config.get_bool("pytrader", "show_trade_own"):
                    return
            else:
                if not self.instance.config.get_bool("pytrader", "show_trade"):
                    return

        col = COLOR_PAIR["con_text"]
        if "trade: bid:" in txt:
            col = COLOR_PAIR["con_text_buy"] + curses.A_BOLD
        if "trade: ask:" in txt:
            col = COLOR_PAIR["con_text_sell"] + curses.A_BOLD
        self.win.addstr("\n" + txt.encode('utf-8'), col)
        self.done_paint()

class PluginConsole(Win):
    """The console window at the bottom"""
    def __init__(self, stdscr, instance):
        """create the console window and connect it to the instance's debug
        callback function"""
        self.instance = instance
        instance.signal_debug.connect(self.slot_debug)
        Win.__init__(self, stdscr)

    def paint(self):
        """just empty the window after resize (I am lazy)"""
        self.win.bkgd(" ", COLOR_PAIR["con_text"])
        for i in range(HEIGHT_CON):
            self.win.addstr("\n ", COLOR_PAIR["con_separator"])

    def resize(self):
        """resize and print a log message. Old messages will have been
        lost after resize because of my dumb paint() implementation, so
        at least print a message indicating that fact into the
        otherwise now empty console window"""
        Win.resize(self)
        self.write("### console has been resized")

    def calc_size(self):
        """put it at the bottom of the screen"""
        self.height = HEIGHT_CON
        self.width = self.termwidth - int(self.termwidth / 2) - 1
        self.posy = self.termheight - self.height
        self.posx = self.termwidth - int(self.termwidth / 2) + 1

    def slot_debug(self, dummy_instance, (txt)):
        """this slot will be connected to all plugin debug signals."""
        if (txt.startswith('[s]')):
            self.write(textwrap.fill(txt.replace('[s]', ' '), self.width))

    def write(self, txt):
        """write a line of text, scroll if needed"""
        self.win.addstr("\n ", COLOR_PAIR["con_separator"])
        self.win.addstr(txt, COLOR_PAIR["con_text"])
        self.done_paint()


class WinOrderBook(Win):
    """the orderbook window"""

    def __init__(self, stdscr, instance):
        """create the orderbook window and connect it to the
        onChanged callback of the instance.orderbook instance"""
        self.instance = instance
        instance.orderbook.signal_changed.connect(self.slot_changed)
        Win.__init__(self, stdscr)

    def calc_size(self):
        """put it into the middle left side"""
        self.height = self.termheight - HEIGHT_CON - HEIGHT_STATUS
        self.posy = HEIGHT_STATUS
        self.width = WIDTH_ORDERBOOK

    def paint(self):
        """paint the visible portion of the orderbook"""

        def paint_row(pos, price, vol, ownvol, color, changevol):
            """paint a row in the orderbook (bid or ask)"""
            if changevol > 0:
                col2 = col_bid + curses.A_BOLD
            elif changevol < 0:
                col2 = col_ask + curses.A_BOLD
            else:
                col2 = col_vol
            self.addstr(pos, 0, str(price), color)
            self.addstr(pos, 12, str(vol), col2)
            # if ownvol:
            self.addstr(pos, 28, str(ownvol), col_own)

        self.win.bkgd(" ", COLOR_PAIR["book_text"])
        self.win.erase()

        instance = self.instance
        book = instance.orderbook

        mid = self.height / 2
        col_bid = COLOR_PAIR["book_bid"]
        col_ask = COLOR_PAIR["book_ask"]
        col_vol = COLOR_PAIR["book_vol"]
        col_own = COLOR_PAIR["book_own"]

        sum_total = instance.config.get_bool("pytrader", "orderbook_sum_total")
        group = instance.config.get_float("pytrader", "orderbook_group")
        if group == 0:
            group = 1

        #
        # paint the asks (first we put them into bins[] then we paint them)
        #
        if len(book.asks):
            i = 0
            bins = []
            pos = mid - 1
            vol = 0
            prev_vol = 0

            # no grouping, bins can be created in one simple and fast loop
            if group == 1:
                cnt = len(book.asks)
                while pos >= 0 and i < cnt:
                    level = book.asks[i]
                    price = level.price
                    if sum_total:
                        vol += level.volume
                    else:
                        vol = level.volume
                    ownvol = level.own_volume
                    bins.append([pos, price, vol, ownvol, 0])
                    pos -= 1
                    i += 1

            # with grouping its a bit more complicated
            else:
                # first bin is exact lowest ask price
                price = book.asks[0].price
                vol = book.asks[0].volume
                bins.append([pos, price, vol, 0, 0])
                prev_vol = vol
                pos -= 1

                # now all following bins
                bin_price = math.ceil(price / group) * group
                if bin_price == price:
                    # first level was exact bin price already, skip to next bin
                    bin_price += group
                while pos >= 0 and bin_price < book.asks[-1].price + group:
                    vol, _vol_quote = book.get_total_up_to(bin_price, True)  # 01 freeze
                    if vol > prev_vol:
                        # append only non-empty bins
                        if sum_total:
                            bins.append([pos, bin_price, vol, 0, 0])
                        else:
                            bins.append([pos, bin_price, vol - prev_vol, 0, 0])
                        prev_vol = vol
                        pos -= 1
                    bin_price += group
                    bin_price = math.ceil(bin_price / group) * group

                # now add the own volumes to their bins
                for order in book.owns:
                    if order.typ == "ask" and order.price > 0:
                        order_bin_price = math.ceil(float(order.price) / group) * group
                        for abin in bins:
                            if abin[1] == order.price:
                                abin[3] += order.volume
                                break
                            if abin[1] == order_bin_price:
                                abin[3] += order.volume
                                break

            # mark the level where change took place (optional)
            if instance.config.get_bool("pytrader", "highlight_changes"):
                if book.last_change_type == "ask":
                    change_bin_price = math.ceil(float(book.last_change_price) / group) * group
                    for abin in bins:
                        if abin[1] == book.last_change_price:
                            abin[4] = book.last_change_volume
                            break
                        if abin[1] == change_bin_price:
                            abin[4] = book.last_change_volume
                            break

            # now finally paint the asks
            for pos, price, vol, ownvol, changevol in bins:
                paint_row(pos, str(price), str(vol), str(ownvol), col_ask, changevol)

        #
        # paint the bids (first we put them into bins[] then we paint them)
        #
        if len(book.bids):
            i = 0
            bins = []
            pos = mid + 1
            vol = 0
            prev_vol = 0

            # no grouping, bins can be created in one simple and fast loop
            if group == 1:
                cnt = len(book.bids)
                while pos < self.height and i < cnt:
                    level = book.bids[i]
                    price = level.price
                    if sum_total:
                        vol += level.volume
                    else:
                        vol = level.volume
                    ownvol = level.own_volume
                    bins.append([pos, price, vol, ownvol, 0])
                    prev_vol = vol
                    pos += 1
                    i += 1

            # with gouping its a bit more complicated
            else:
                # first bin is exact lowest ask price
                price = book.bids[0].price
                vol = book.bids[0].volume
                bins.append([pos, price, vol, 0, 0])
                prev_vol = vol
                pos += 1

                # now all following bins
                bin_price = math.floor(float(price) / group) * group
                if bin_price == price:
                    # first level was exact bin price already, skip to next bin
                    bin_price -= group
                while pos < self.height and bin_price >= 0:
                    vol, _vol_quote = book.get_total_up_to(bin_price, False)
                    if vol > prev_vol:
                        # append only non-empty bins
                        if sum_total:
                            bins.append([pos, bin_price, vol, 0, 0])
                        else:
                            bins.append([pos, bin_price, vol - prev_vol, 0, 0])
                        prev_vol = vol
                        pos += 1
                    bin_price -= group
                    bin_price = math.floor(bin_price / group) * group

                # now add the own volumes to their bins
                for order in book.owns:
                    if order.typ == "bid" and order.price > 0:
                        order_bin_price = math.floor(float(order.price) / group) * group
                        for abin in bins:
                            if abin[1] == order.price:
                                abin[3] += order.volume
                                break
                            if abin[1] == order_bin_price:
                                abin[3] += order.volume
                                break

            # mark the level where change took place (optional)
            if instance.config.get_bool("pytrader", "highlight_changes"):
                if book.last_change_type == "bid":
                    change_bin_price = math.floor(float(book.last_change_price) / group) * group
                    for abin in bins:
                        if abin[1] == book.last_change_price:
                            abin[4] = book.last_change_volume
                            break
                        if abin[1] == change_bin_price:
                            abin[4] = book.last_change_volume
                            break

            # now finally paint the bids
            for pos, price, vol, ownvol, changevol in bins:
                paint_row(pos, price, vol, ownvol, col_bid, changevol)

        # update the xterm title bar
        if self.instance.config.get_bool("pytrader", "set_xterm_title"):
            last_candle = self.instance.history.last_candle()
            if last_candle:
                title = str(last_candle.cls)
                title += " - PyTrader -"
                title += " bid:" + str(book.bid)
                title += " ask:" + str(book.ask)

                term = os.environ["TERM"]
                # the following is incomplete but better safe than sorry
                # if you know more terminals then please provide a patch
                if "xterm" in term or "rxvt" in term:
                    sys_out.write("\x1b]0;%s\x07" % title)
                    sys_out.flush()

    def slot_changed(self, _book, _dummy):
        """Slot for orderbook.signal_changed"""
        self.do_paint()


TYPE_HISTORY = 1
TYPE_ORDERBOOK = 2

class WinChart(Win):
    """the chart window"""

    def __init__(self, stdscr, instance):
        self.instance = instance
        self.pmin = 0
        self.pmax = 0
        self.change_type = None
        instance.history.signal_changed.connect(self.slot_history_changed)
        instance.orderbook.signal_changed.connect(self.slot_orderbook_changed)

        # some terminals do not support reverse video
        # so we cannot use reverse space for candle bodies
        if curses.A_REVERSE & curses.termattrs():
            self.body_char = " "
            self.body_attr = curses.A_REVERSE
        else:
            self.body_char = curses.ACS_CKBOARD
            self.body_attr = 0

        Win.__init__(self, stdscr)

    def calc_size(self):
        """position in the middle, right to the orderbook"""
        self.posx = WIDTH_ORDERBOOK
        self.posy = HEIGHT_STATUS
        self.width = self.termwidth - WIDTH_ORDERBOOK
        self.height = self.termheight - HEIGHT_CON - HEIGHT_STATUS

    def is_in_range(self, price):
        """is this price in the currently visible range?"""
        return price <= self.pmax and price >= self.pmin

    def get_optimal_step(self, num_min):
        """return optimal step size for painting y-axis labels so that the
        range will be divided into at least num_min steps"""
        if self.pmax <= self.pmin:
            return None
        stepex = float(self.pmax - self.pmin) / num_min
        step1 = math.pow(10, math.floor(math.log(stepex, 10)))
        step2 = step1 * 2
        step5 = step1 * 5
        if step5 <= stepex:
            return step5
        if step2 <= stepex:
            return step2
        return step1

    def price_to_screen(self, price):
        """convert price into screen coordinates (y=0 is at the top!)"""
        relative_from_bottom = float(price - self.pmin) / float(self.pmax - self.pmin)
        screen_from_bottom = relative_from_bottom * self.height
        return int(self.height - screen_from_bottom)

    def paint_y_label(self, posy, posx, price):
        """paint the y label of the history chart, formats the number
        so that it needs not more room than necessary but it also uses
        pmax to determine how many digits are needed so that all numbers
        will be nicely aligned at the decimal point"""

        fprice = float(price)
        labelstr = ("%f" % fprice).rstrip("0").rstrip(".")

        # look at pmax to determine the max number of digits before the decimal
        # and then pad all smaller prices with spaces to make them align nicely.
        need_digits = int(math.log10(float(self.pmax))) + 1
        have_digits = len(str(int(fprice)))
        if have_digits < need_digits:
            padding = " " * (need_digits - have_digits)
            labelstr = padding + labelstr

        self.addstr(
            posy, posx,
            labelstr,
            COLOR_PAIR["chart_text"]
        )

    def paint_candle(self, posx, candle):
        """paint a single candle"""

        sopen = self.price_to_screen(candle.opn)
        shigh = self.price_to_screen(candle.hig)
        slow = self.price_to_screen(candle.low)
        sclose = self.price_to_screen(candle.cls)

        for posy in range(self.height):
            if posy >= shigh and posy < sopen and posy < sclose:
                # upper wick
                self.addch(posy, posx, curses.ACS_VLINE, COLOR_PAIR["chart_text"])
            if posy >= sopen and posy < sclose:
                # red body
                self.addch(posy, posx, self.body_char, self.body_attr + COLOR_PAIR["chart_down"])
            if posy >= sclose and posy < sopen:
                # green body
                self.addch(posy, posx, self.body_char, self.body_attr + COLOR_PAIR["chart_up"])
            if posy >= sopen and posy >= sclose and posy < slow:
                # lower wick
                self.addch(posy, posx, curses.ACS_VLINE, COLOR_PAIR["chart_text"])

    def paint(self):
        typ = self.instance.config.get_string("pytrader", "display_right")
        if typ == "history_chart":
            self.paint_history_chart()
        elif typ == "depth_chart":
            self.paint_depth_chart()
        else:
            self.paint_history_chart()

    def paint_depth_chart(self):
        """paint a depth chart"""

        BAR_LEFT_EDGE = 14
        FORMAT_STRING = "%7.8f"

        def paint_depth(pos, price, vol, own, col_price, change):
            """paint one row of the depth chart"""
            # self.instance.debug("pos: %s, change: %s, own: %s, price: %s, col_price: %s, vol: %s" % (pos, change, own, price, col_price, vol))
            if change > 0:
                col = col_bid + curses.A_BOLD
            elif change < 0:
                col = col_ask + curses.A_BOLD
            else:
                col = col_bar
            pricestr = FORMAT_STRING % price
            self.addstr(pos, 0, pricestr, col_price)
            length = int(vol * mult_x)
            self.win.hline(pos, BAR_LEFT_EDGE, curses.ACS_CKBOARD, length, col)
            if own:
                self.addstr(pos, length + BAR_LEFT_EDGE, "o", col_own)

        self.win.bkgd(" ", COLOR_PAIR["chart_text"])
        self.win.erase()

        book = self.instance.orderbook
        if not (book.bid and book.ask and len(book.bids) and len(book.asks)):
            # orderbook is not initialized yet, paint nothing
            return

        col_bar = COLOR_PAIR["book_vol"]
        col_bid = COLOR_PAIR["book_bid"]
        col_ask = COLOR_PAIR["book_ask"]
        col_own = COLOR_PAIR["book_own"]

        group = self.instance.config.get_float("pytrader", "depth_chart_group")
        if group == 0:
            group = 0.00000001

        max_vol_ask = 0
        max_vol_bid = 0
        bin_asks = []
        bin_bids = []
        mid = self.height / 2
        sum_total = self.instance.config.get_bool("pytrader", "depth_chart_sum_total")

        #
        # bin the asks
        #
        pos = mid - 1
        prev_vol = 0
        bin_price = math.ceil(book.asks[0].price / group) * group
        while pos >= 0 and bin_price < book.asks[-1].price + group:
            bin_vol, _bin_vol_quote = book.get_total_up_to(bin_price, True)
            if bin_vol > prev_vol:
                # add only non-empty bins
                if sum_total:
                    bin_asks.append([pos, bin_price, bin_vol, 0, 0])
                    max_vol_ask = max(bin_vol, max_vol_ask)
                else:
                    bin_asks.append([pos, bin_price, bin_vol - prev_vol, 0, 0])
                    max_vol_ask = max(bin_vol - prev_vol, max_vol_ask)
                prev_vol = bin_vol
                pos -= 1
            bin_price += group
            bin_price = math.ceil(bin_price / group) * group

        #
        # bin the bids
        #
        pos = mid + 1
        prev_vol = 0
        bin_price = math.floor(book.bids[0].price / group) * group
        while pos < self.height and bin_price >= 0:
            _bin_vol_base, bin_vol_quote = book.get_total_up_to(bin_price, False)
            bin_vol = float(bin_vol_quote / book.bid)
            if bin_vol > prev_vol:
                # add only non-empty bins
                if sum_total:
                    bin_bids.append([pos, bin_price, bin_vol, 0, 0])
                    max_vol_bid = max(bin_vol, max_vol_bid)
                else:
                    bin_bids.append([pos, bin_price, bin_vol - prev_vol, 0, 0])
                    max_vol_bid = max(bin_vol - prev_vol, max_vol_bid)
                prev_vol = bin_vol
                pos += 1
            bin_price -= group
            bin_price = math.floor(bin_price / group) * group

        max_vol_tot = max(max_vol_ask, max_vol_bid)
        if not max_vol_tot:
            return
        mult_x = float(self.width - BAR_LEFT_EDGE - 2) / max_vol_tot

        # add the own volume to the bins
        for order in book.owns:
            if order.price > 0:
                if order.typ == "ask":
                    bin_price = math.ceil(order.price / group) * group
                    for abin in bin_asks:
                        if abin[1] == bin_price:
                            abin[3] += order.volume
                            break
                else:
                    bin_price = math.floor(order.price / group) * group
                    for abin in bin_bids:
                        if abin[1] == bin_price:
                            abin[3] += order.volume
                            break

        # highlight the relative change (optional)
        if self.instance.config.get_bool("pytrader", "highlight_changes"):
            price = book.last_change_price
            if book.last_change_type == "ask":
                bin_price = math.ceil(price / group) * group
                for abin in bin_asks:
                    if abin[1] == bin_price:
                        abin[4] = book.last_change_volume
                        break
            if book.last_change_type == "bid":
                bin_price = math.floor(price / group) * group
                for abin in bin_bids:
                    if abin[1] == bin_price:
                        abin[4] = book.last_change_volume
                        break

        # paint the asks
        for pos, price, vol, own, change in bin_asks:
            paint_depth(pos, price, vol, own, col_ask, change)

        # paint the bids
        for pos, price, vol, own, change in bin_bids:
            paint_depth(pos, price, vol, own, col_bid, change)

    def paint_history_chart(self):
        """paint a history candlestick chart"""

        if self.change_type == TYPE_ORDERBOOK:
            # erase only the rightmost column to redraw bid/ask and orders
            # beause we won't redraw the chart, its only an orderbook change
            self.win.vline(0, self.width - 1, " ", self.height, COLOR_PAIR["chart_text"])
        else:
            self.win.bkgd(" ", COLOR_PAIR["chart_text"])
            self.win.erase()

        hist = self.instance.history
        book = self.instance.orderbook

        self.pmax = 0
        self.pmin = 9999999999

        # determine y range
        posx = self.width - 2
        index = 0
        while index < hist.length() and posx >= 0:
            candle = hist.candles[index]
            if self.pmax < candle.hig:
                self.pmax = candle.hig
            if self.pmin > candle.low:
                self.pmin = candle.low
            index += 1
            posx -= 1

        if self.pmax == self.pmin:
            return

        # paint the candlestick chart.
        # We won't paint it if it was triggered from an orderbook change
        # signal because that would be redundant and only waste CPU.
        # In that case we only repaint the bid/ask markers (see below)
        if self.change_type != TYPE_ORDERBOOK:
            # paint the candles
            posx = self.width - 2
            index = 0
            while index < hist.length() and posx >= 0:
                candle = hist.candles[index]
                self.paint_candle(posx, candle)
                index += 1
                posx -= 1

            # paint the y-axis labels
            posx = 0
            step = self.get_optimal_step(4)
            if step:
                labelprice = self.pmin / step
                while not labelprice > self.pmax:
                    posy = self.price_to_screen(labelprice)
                    if posy < self.height - 1:
                        self.paint_y_label(posy, posx, labelprice)
                    labelprice += step

        # paint bid, ask, own orders
        posx = self.width - 1
        for order in book.owns:
            if self.is_in_range(order.price):
                posy = self.price_to_screen(order.price)
                if order.status == "pending":
                    self.addch(posy, posx, ord("p"), COLOR_PAIR["order_pending"])
                else:
                    self.addch(posy, posx, ord("o"), COLOR_PAIR["book_own"])

        if self.is_in_range(book.bid):
            posy = self.price_to_screen(book.bid)
            self.addch(posy, posx, curses.ACS_HLINE, COLOR_PAIR["chart_up"])

        if self.is_in_range(book.ask):
            posy = self.price_to_screen(book.ask)
            self.addch(posy, posx, curses.ACS_HLINE, COLOR_PAIR["chart_down"])

    def slot_history_changed(self, _sender, _data):
        """Slot for history changed"""
        self.change_type = TYPE_HISTORY
        self.do_paint()
        self.change_type = None

    def slot_orderbook_changed(self, _sender, _data):
        """Slot for orderbook changed"""
        self.change_type = TYPE_ORDERBOOK
        self.do_paint()
        self.change_type = None


class WinStatus(Win):
    """the status window at the top"""

    def __init__(self, stdscr, instance):
        """create the status window and connect the needed callbacks"""
        self.instance = instance
        self.order_lag = 0
        self.order_lag_txt = ""
        self.sorted_currency_list = []
        instance.signal_orderlag.connect(self.slot_orderlag)
        instance.signal_wallet.connect(self.slot_changed)
        instance.orderbook.signal_changed.connect(self.slot_changed)
        Win.__init__(self, stdscr)

    def calc_size(self):
        """place it at the top of the terminal"""
        self.height = HEIGHT_STATUS

    def sort_currency_list_if_changed(self):
        """sort the currency list in the wallet for better display,
        sort it only if it has changed, otherwise leave it as it is"""
        currency_list = self.instance.wallet.keys()
        if len(currency_list) == len(self.sorted_currency_list):
            return

        # now we will bring base and quote currency to the front and sort the
        # the rest of the list of names by acount balance in descending order
        if self.instance.curr_base in currency_list:
            currency_list.remove(self.instance.curr_base)
        if self.instance.curr_quote in currency_list:
            currency_list.remove(self.instance.curr_quote)
        currency_list.sort(key=lambda name: -self.instance.wallet[name])
        currency_list.insert(0, self.instance.curr_quote)
        currency_list.insert(0, self.instance.curr_base)
        self.sorted_currency_list = currency_list

    def paint(self):
        """paint the complete status"""
        cbase = self.instance.curr_base
        cquote = self.instance.curr_quote
        self.sort_currency_list_if_changed()
        self.win.bkgd(" ", COLOR_PAIR["status_text"])
        self.win.erase()

        #
        # first line
        #
        self.addstr(0, 0, "Price: ", COLOR_PAIR["status_text"])
        self.addstr("%f" % float(self.instance.orderbook.bid), COLOR_PAIR["status_text"] + curses.A_BOLD)
        self.addstr(" - ", COLOR_PAIR["status_text"])
        self.addstr("%f" % float(self.instance.orderbook.ask), COLOR_PAIR["status_text"] + curses.A_BOLD)

        self.addstr(" | Market: ", COLOR_PAIR["status_text"])
        self.addstr("%s%s" % (cbase, cquote), COLOR_PAIR["status_text"] + curses.A_BOLD)

        self.addstr(" | Account: ", COLOR_PAIR["status_text"])
        if len(self.sorted_currency_list):
            own_currencies = []
            total_base = 0
            total_quote = 0
            for currency in self.sorted_currency_list:
                if currency in self.instance.wallet:
                    own_currencies.append(currency)
            for c, own_currency in enumerate(own_currencies):
                # self.instance.debug("%s: %s" % (own_currency, self.instance.wallet[own_currency]))
                self.addstr("%f %s" % (self.instance.wallet[own_currency], own_currency), COLOR_PAIR["status_text"] + curses.A_BOLD)
                if own_currency == cbase and self.instance.wallet and self.instance.orderbook.ask:
                    total_base += self.instance.wallet[own_currency]
                    total_quote += self.instance.wallet[own_currency] * self.instance.orderbook.ask
                elif own_currency == cquote and self.instance.wallet and self.instance.orderbook.bid:
                    total_quote += self.instance.wallet[own_currency]
                    total_base += self.instance.wallet[own_currency] / self.instance.orderbook.bid
                if (c + 1 != len(own_currencies)):
                    self.addstr(" + ", COLOR_PAIR["status_text"])
            self.addstr(" | Totals: ", COLOR_PAIR["status_text"])
            self.addstr("%f %s" % (total_base, cbase), COLOR_PAIR["status_text"] + curses.A_BOLD)
            self.addstr(" / ", COLOR_PAIR["status_text"])
            self.addstr("%f %s" % (float(total_quote), cquote), COLOR_PAIR["status_text"] + curses.A_BOLD)
            self.addstr(" | %s order(s)" % len(self.instance.orderbook.owns))
            self.addstr(" | Volume: %s %s" % (self.instance.monthly_volume, self.instance.currency))
            self.addstr(" | Fee: ", COLOR_PAIR["status_text"])
            self.addstr("%s" % self.instance.trade_fee, COLOR_PAIR["status_text"] + curses.A_BOLD)
            self.addstr(" %", COLOR_PAIR["status_text"])
        else:
            self.addstr("No info (yet)", COLOR_PAIR["status_text"] + curses.A_BOLD)

        #
        # second line
        #
        line2 = ""
        if self.instance.config.get_bool("pytrader", "show_orderbook_stats"):
            str_btc = locale.format('%d', self.instance.orderbook.total_ask, 1)
            str_fiat = locale.format('%d', self.instance.orderbook.total_bid, 1)
            if self.instance.orderbook.total_ask:
                ratio = (self.instance.orderbook.total_bid / self.instance.orderbook.ask) / self.instance.orderbook.total_ask
                str_ratio = locale.format('%1.5f', ratio, 1)
            else:
                str_ratio = "-"

            line2 += "sum_bid: %s %s | " % (str_fiat, cquote)
            line2 += "sum_ask: %s %s | " % (str_btc, cbase)
            line2 += "ratio: %s %s/%s | " % (str_ratio, cquote, cbase)

        line2 += "lag: %s" % self.order_lag_txt
        if self.instance.socket_lag:
            line2 += " %.3f s " % (self.instance.socket_lag / 1e6)
            line2 += "(order / socket)"
        line2 += " | "
        line2 += "depth: %s / " % self.instance.orderbook.depth_updated
        line2 += "orders: %s" % self.instance.orderbook.orders_updated

        # self.addstr(0, 0, line1, COLOR_PAIR["status_text"])
        self.addstr(1, 0, line2, COLOR_PAIR["status_text"])

    def slot_changed(self, dummy_sender, dummy_data):
        """the callback funtion called by the Api() instance"""
        self.do_paint()

    def slot_orderlag(self, dummy_sender, (usec, text)):
        """slot for order_lag mesages"""
        self.order_lag = usec
        self.order_lag_txt = text
        self.do_paint()


class DlgListItems(Win):
    """dialog with a scrollable list of items"""
    def __init__(self, stdscr, width, title, hlp, keys):
        self.items = []
        self.selected = []
        self.item_top = 0
        self.item_sel = 0
        self.dlg_width = width
        self.dlg_title = title
        self.dlg_hlp = hlp
        self.dlg_keys = keys
        self.reserved_lines = 5  # how many lines NOT used for order list
        self.init_items()
        Win.__init__(self, stdscr)

    def init_items(self):
        """initialize the items list, must override and implement this"""
        raise NotImplementedError()

    def calc_size(self):
        maxh = self.termheight - 4
        self.height = len(self.items) + self.reserved_lines
        if self.height > maxh:
            self.height = maxh
        self.posy = (self.termheight - self.height) / 2

        self.width = self.dlg_width
        self.posx = (self.termwidth - self.width) / 2

    def paint_item(self, posx, index):
        """paint the item. Must override and implement this"""
        raise NotImplementedError()

    def paint(self):
        try:
            self.win.bkgd(" ", COLOR_PAIR["dialog_text"])
            self.win.erase()
            self.win.border()
            self.addstr(0, 1, " %s " % self.dlg_title, COLOR_PAIR["dialog_text"])
            index = self.item_top
            posy = 2
            while posy < self.height - 3 and index < len(self.items):
                self.paint_item(posy, index)
                index += 1
                posy += 1

            self.win.move(self.height - 2, 2)
            for key, desc in self.dlg_hlp:
                self.addstr(key + " ", COLOR_PAIR["dialog_sel"])
                self.addstr(desc + " ", COLOR_PAIR["dialog_text"])
        except Exception:
            self.instance.debug(traceback.format_exc())

    def down(self, num):
        """move the cursor down (or up)"""
        if not len(self.items):
            return
        self.item_sel += num
        if self.item_sel < 0:
            self.item_sel = 0
        if self.item_sel > len(self.items) - 1:
            self.item_sel = len(self.items) - 1

        last_line = self.height - 1 - self.reserved_lines
        if self.item_sel < self.item_top:
            self.item_top = self.item_sel
        if self.item_sel - self.item_top > last_line:
            self.item_top = self.item_sel - last_line

        self.do_paint()

    def toggle_select(self):
        """toggle selection under cursor"""
        if not len(self.items):
            return
        item = self.items[self.item_sel]
        if item in self.selected:
            self.selected.remove(item)
        else:
            self.selected.append(item)
        self.do_paint()

    def modal(self):
        """run the modal getch-loop for this dialog"""
        if self.win:
            done = False
            while not done:
                key_pressed = self.win.getch()
                if key_pressed in [27, ord("q"), curses.KEY_F10]:
                    done = True
                if key_pressed == curses.KEY_DOWN:
                    self.down(1)
                if key_pressed == curses.KEY_UP:
                    self.down(-1)
                if key_pressed in [curses.KEY_IC, ord("=")]:
                    self.toggle_select()
                    self.down(1)

                for key, func in self.dlg_keys:
                    if key == key_pressed:
                        func()
                        done = True

        # help the garbage collector clean up circular references
        # to make sure __del__() will be called to close the dialog
        del self.dlg_keys


class DlgCancelOrders(DlgListItems):
    """modal dialog to cancel orders"""
    def __init__(self, stdscr, instance):
        self.instance = instance
        hlp = [("INS / =", "select"), ("F8", "cancel selected"), ("F10", "exit")]
        keys = [(curses.KEY_F8, self._do_cancel)]
        DlgListItems.__init__(self, stdscr, 45, "Cancel order(s)", hlp, keys)

    def init_items(self):
        for order in self.instance.orderbook.owns:
            # self.instance.debug("oid: %s, typ: %s, price: %s, volume: %s" % (order.oid, order.typ, order.price, order.volume))
            self.items.append(order)
        self.items.sort(key=lambda o: -o.price)
        # self.instance.debug("items: %s" % self.items)

    def paint_item(self, posy, index):
        """paint one single order"""
        order = self.items[index]
        # self.instance.debug("order: %s, selected: %s" % (order, self.selected))
        if order in self.selected:
            marker = "*"
            if index == self.item_sel:
                attr = COLOR_PAIR["dialog_sel_sel"]
            else:
                attr = COLOR_PAIR["dialog_sel_text"] + curses.A_BOLD
        else:
            marker = ""
            if index == self.item_sel:
                attr = COLOR_PAIR["dialog_sel"]
            else:
                attr = COLOR_PAIR["dialog_text"]

        self.addstr(posy, 2, marker, attr)
        self.addstr(posy, 5, order.typ, attr)
        self.addstr(posy, 9, str(order.price), attr)
        self.addstr(posy, 22, str(order.volume), attr)

    def _do_cancel(self):
        """cancel all selected orders (or the order under cursor if empty)"""

        def do_cancel(order):
            """cancel a single order"""
            self.instance.cancel(order.oid)

        if not len(self.items):
            return
        if not len(self.selected):
            order = self.items[self.item_sel]
            do_cancel(order)
        else:
            for order in self.selected:
                do_cancel(order)


class TextBox():
    """wrapper for curses.textpad.Textbox"""

    def __init__(self, dlg, posy, posx, length):
        self.dlg = dlg
        self.win = dlg.win.derwin(1, length, posy, posx)
        self.win.keypad(1)
        self.box = curses.textpad.Textbox(self.win, insert_mode=True)
        self.value = ""
        self.result = None
        self.editing = False

    def __del__(self):
        self.box = None
        self.win = None

    def modal(self):
        """enter te edit box modal loop"""
        self.win.move(0, 0)
        self.editing = True
        api.start_thread(self.cursor_placement_thread, "TextBox cursor placement")
        self.value = self.box.edit(self.validator)
        self.editing = False
        return self.result

    def validator(self, char):
        """here we tweak the behavior slightly, especially we want to
        end modal editing mode immediately on arrow up/down and on enter
        and we also want to catch ESC and F10, to abort the entire dialog"""
        if curses.ascii.isprint(char):
            return char
        if char == curses.ascii.TAB:
            char = curses.KEY_DOWN
        if char in [curses.KEY_DOWN, curses.KEY_UP]:
            self.result = char
            return curses.ascii.BEL
        if char in [10, 13, curses.KEY_ENTER, curses.ascii.BEL]:
            self.result = 10
            return curses.ascii.BEL
        if char == 127:
            char = curses.KEY_BACKSPACE
        if char in [27, curses.KEY_F10]:
            self.result = -1
            return curses.ascii.BEL
        return char

    def cursor_placement_thread(self):
        """this is the most ugly hack of the entire program. During the
        signals hat are fired while we are editing there will be many repaints
        of other other panels below this dialog and when curses is done
        repainting everything the blinking cursor is not in the correct
        position. This is only a cosmetic problem but very annnoying. Try to
        force it into the edit field by repainting it very often."""
        while self.editing:
            with api.Signal._lock:
                curses.curs_set(2)
                self.win.touchwin()
                self.win.refresh()
            time.sleep(0.1)
        curses.curs_set(0)


class NumberBox(TextBox):
    """TextBox that only accepts numbers"""
    def __init__(self, dlg, posy, posx, length):
        TextBox.__init__(self, dlg, posy, posx, length)

    def validator(self, char):
        """allow only numbers to be entered"""
        if char == ord("q"):
            char = curses.KEY_F10
        if curses.ascii.isprint(char):
            if chr(char) not in "0123456789.":
                char = 0
        return TextBox.validator(self, char)


class DlgNewOrder(Win):
    """abtract base class for entering new orders"""
    def __init__(self, stdscr, instance, color, title):
        self.instance = instance
        self.color = color
        self.title = title
        self.edit_price = None
        self.edit_volume = None
        Win.__init__(self, stdscr)

    def calc_size(self):
        Win.calc_size(self)
        self.width = 35
        self.height = 8
        self.posx = (self.termwidth - self.width) / 2
        self.posy = (self.termheight - self.height) / 2

    def paint(self):
        self.win.bkgd(" ", self.color)
        self.win.border()
        self.addstr(0, 1, " %s " % self.title, self.color)
        self.addstr(2, 2, " price", self.color)
        self.addstr(2, 30, self.instance.curr_quote)
        self.addstr(4, 2, "volume", self.color)
        self.addstr(4, 30, self.instance.curr_base)
        self.addstr(6, 2, "F10 ", self.color + curses.A_REVERSE)
        self.addstr("cancel ", self.color)
        self.addstr("Enter ", self.color + curses.A_REVERSE)
        self.addstr("submit ", self.color)
        self.edit_price = NumberBox(self, 2, 10, 20)
        self.edit_volume = NumberBox(self, 4, 10, 20)

    def do_submit(self, price_float, volume_float):
        """sumit the order. implementating class will do eiter buy or sell"""
        raise NotImplementedError()

    def modal(self):
        """enter the modal getch() loop of this dialog"""
        if self.win:
            focus = 1
            # next time I am going to use some higher level
            # wrapper on top of curses, i promise...
            while True:
                if focus == 1:
                    res = self.edit_price.modal()
                    if res == -1:
                        break  # cancel entire dialog
                    if res in [10, curses.KEY_DOWN, curses.KEY_UP]:
                        try:
                            price_float = float(self.edit_price.value)
                            focus = 2
                        except ValueError:
                            pass  # can't move down until this is a valid number

                if focus == 2:
                    res = self.edit_volume.modal()
                    if res == -1:
                        break  # cancel entire dialog
                    if res in [curses.KEY_UP, curses.KEY_DOWN]:
                        focus = 1
                    if res == 10:
                        try:
                            volume_float = float(self.edit_volume.value)
                            break  # have both values now, can submit order
                        except ValueError:
                            pass  # no float number, stay in this edit field

            if res == -1:
                # user has hit f10. just end here, do nothing
                pass
            if res == 10:
                self.do_submit(price_float, volume_float)

        # make sure all cyclic references are garbage collected or
        # otherwise the curses window won't disappear
        self.edit_price = None
        self.edit_volume = None


class DlgNewOrderBid(DlgNewOrder):
    """Modal dialog for new buy order"""
    def __init__(self, stdscr, instance):
        DlgNewOrder.__init__(self, stdscr, instance, COLOR_PAIR["dialog_bid_text"], "New buy order")

    def do_submit(self, price, volume):
        price = float(price)
        volume = float(volume)
        self.instance.buy(price, volume)


class DlgNewOrderAsk(DlgNewOrder):
    """Modal dialog for new sell order"""
    def __init__(self, stdscr, instance):
        DlgNewOrder.__init__(self, stdscr, instance, COLOR_PAIR["dialog_ask_text"], "New sell order")

    def do_submit(self, price, volume):
        price = float(price)
        volume = float(volume)
        self.instance.sell(price, volume)


#
# logging, printing, etc...
#

class LogWriter():
    """connects to api.signal_debug and logs it all to the logfile"""
    def __init__(self, instance):
        self.instance = instance
        if self.instance.config.get_bool("pytrader", "dont_truncate_logfile"):
            logfilemode = 'a'
        else:
            logfilemode = 'w'

        logging.basicConfig(filename='%s.log' % self.instance.config.filename[:-4],
                            filemode=logfilemode,
                            format='%(asctime)s:%(levelname)s:%(message)s',
                            level=logging.DEBUG)
        self.instance.signal_debug.connect(self.slot_debug)

    def close(self):
        """stop logging"""
        # not needed
        pass

    def slot_debug(self, sender, (msg)):
        """handler for signal_debug signals"""
        name = "%s.%s" % (sender.__class__.__module__, sender.__class__.__name__)
        logging.debug("%s:%s", name, msg)


class PrintHook():
    """intercept stdout/stderr and send it all to instance.signal_debug instead"""
    def __init__(self, instance):
        self.instance = instance
        self.stdout = sys.stdout
        self.stderr = sys.stderr
        sys.stdout = self
        sys.stderr = self

    def close(self):
        """restore normal stdio"""
        sys.stdout = self.stdout
        sys.stderr = self.stderr

    def write(self, string):
        """called when someone uses print(), send it to instance"""
        string = string.strip()
        if string != "":
            self.instance.signal_debug(self, string)


#
# dynamically (re)loadable strategy module
#

class StrategyManager():
    """load the strategy module"""

    def __init__(self, instance, strategy_name_list):
        self.strategy_object_list = []
        self.strategy_name_list = strategy_name_list
        self.instance = instance
        self.reload()

    def unload(self):
        """unload the strategy, will trigger its the __del__ method"""
        self.instance.signal_strategy_unload(self, None)
        self.strategy_object_list = []

    def reload(self):
        """reload and re-initialize the strategy module"""
        self.unload()
        for name in self.strategy_name_list:
            name = name.replace(".py", "").strip()

            try:
                strategy_module = __import__(name)
                try:
                    reload(strategy_module)
                    strategy_object = strategy_module.Strategy(self.instance)
                    self.strategy_object_list.append(strategy_object)
                    if hasattr(strategy_object, "name"):
                        self.instance.strategies[strategy_object.name] = strategy_object

                except Exception:
                    self.instance.debug("### error while loading strategy %s.py, traceback follows:" % name)
                    self.instance.debug(traceback.format_exc())

            except ImportError:
                self.instance.debug("### could not import %s.py, traceback follows:" % name)
                self.instance.debug(traceback.format_exc())


def toggle_setting(instance, alternatives, option_name, direction):
    """toggle a setting in the ini file"""
    with api.Signal._lock:
        setting = instance.config.get_string("pytrader", option_name)
        try:
            newindex = (alternatives.index(setting) + direction) % len(alternatives)
        except ValueError:
            newindex = 0
        instance.config.set("pytrader", option_name, alternatives[newindex])
        instance.config.save()

def toggle_depth_group(instance, direction):
    """toggle the step width of the depth chart"""
    alt = ["0.00000001", "0.00000005", "0.0000001", "0.0000005", "0.000001", "0.000005", "0.00001", "0.00005", "0.0001", "0.0005",
           "0.001", "0.005", "0.01", "0.05", "0.1", "0.5", "1", "5", "10", "20", "50", "100"]
    toggle_setting(instance, alt, "depth_chart_group", direction)
    instance.orderbook.signal_changed(instance.orderbook, None)

def toggle_orderbook_group(instance, direction):
    """toggle the group width of the orderbook"""
    alt = ["0", "0.00000001", "0.00000005", "0.0000001", "0.0000005", "0.000001", "0.000005", "0.00001", "0.00005", "0.0001", "0.0005",
           "0.001", "0.005", "0.01", "0.05", "0.1", "0.5", "1", "5", "10", "20", "50", "100"]
    toggle_setting(instance, alt, "orderbook_group", direction)
    instance.orderbook.signal_changed(instance.orderbook, None)

def toggle_orderbook_sum(instance):
    """toggle the summing in the orderbook on and off"""
    alt = ["False", "True"]
    toggle_setting(instance, alt, "orderbook_sum_total", 1)
    instance.orderbook.signal_changed(instance.orderbook, None)

def toggle_depth_sum(instance):
    """toggle the summing in the depth chart on and off"""
    alt = ["False", "True"]
    toggle_setting(instance, alt, "depth_chart_sum_total", 1)
    instance.orderbook.signal_changed(instance.orderbook, None)

def set_ini(instance, setting, value, signal, signal_sender, signal_params):
    """set the ini value and then send a signal"""
    with api.Signal._lock:
        instance.config.set("pytrader", setting, value)
        instance.config.save()
    signal(signal_sender, signal_params)


#
#
# main program
#

def main():
    """main funtion, called at the start of the program"""
    debug_tb = []

    def curses_loop(stdscr):
        """Only the code inside this function runs within the curses wrapper"""

        # this function may under no circumstancs raise an exception, so I'm
        # wrapping everything into try/except (should actually never happen
        # anyways but when it happens during coding or debugging it would
        # leave the terminal in an unusable state and this must be avoded).
        # We have a list debug_tb[] where we can append tracebacks and
        # after curses uninitialized properly and the terminal is restored
        # we can print them.
        try:
            init_colors()

            instance = api.Api(secret, config)

            logwriter = LogWriter(instance)
            printhook = PrintHook(instance)

            conwin = WinConsole(stdscr, instance)
            plugwin = PluginConsole(stdscr, instance)
            bookwin = WinOrderBook(stdscr, instance)
            statuswin = WinStatus(stdscr, instance)
            chartwin = WinChart(stdscr, instance)

            strategy_manager = StrategyManager(instance, strat_mod_list)

            instance.start()

            while True:
                key = stdscr.getch()
                if key == ord("q"):
                    break
                elif key == curses.KEY_F4:
                    DlgNewOrderBid(stdscr, instance).modal()
                elif key == curses.KEY_F5:
                    DlgNewOrderAsk(stdscr, instance).modal()
                elif key == curses.KEY_F6:
                    DlgCancelOrders(stdscr, instance).modal()
                elif key == curses.KEY_RESIZE:
                    with api.Signal._lock:
                        stdscr.erase()
                        stdscr.refresh()
                        conwin.resize()
                        plugwin.resize()
                        bookwin.resize()
                        chartwin.resize()
                        statuswin.resize()
                elif key == ord("l"):
                    strategy_manager.reload()

                # which chart to show on the right side
                elif key == ord("H"):
                    set_ini(instance, "display_right", "history_chart", instance.history.signal_changed, instance.history, None)
                elif key == ord("D"):
                    set_ini(instance, "display_right", "depth_chart", instance.orderbook.signal_changed, instance.orderbook, None)

                #  depth chart step
                elif key == ord(","):  # zoom out
                    toggle_depth_group(instance, +1)
                elif key == ord("."):  # zoom in
                    toggle_depth_group(instance, -1)

                # orderbook grouping step
                elif key == ord("-"):  # zoom out (larger step)
                    toggle_orderbook_group(instance, +1)
                elif key == ord("+"):  # zoom in (smaller step)
                    toggle_orderbook_group(instance, -1)

                elif key == ord("S"):
                    toggle_orderbook_sum(instance)

                elif key == ord("T"):
                    toggle_depth_sum(instance)

                # lowercase keys go to the strategy module
                elif key >= ord("a") and key <= ord("z"):
                    instance.signal_keypress(instance, (key))
                else:
                    instance.debug("key pressed: key=%i" % key)

        except KeyboardInterrupt:
            # Ctrl+C has been pressed
            pass

        except Exception:
            debug_tb.append(traceback.format_exc())

        # We are here because shutdown was requested.
        # Before we do anything we dump stacktraces of all currently running
        # threads to a separate logfile because this helps debugging freezes
        # and deadlocks that might occur if things went totally wrong.

        try:
            with open("%s.stacktrace.log" % config.filename[:-4], "w") as stacklog:
                stacklog.write(dump_all_stacks())
        except Exception as exc:
            print("Failed to write stacktrace logs:", exc)

        # we need the signal lock to be able to shut down. And we cannot
        # wait for any frozen slot to return, so try really hard to get
        # the lock and if that fails then unlock it forcefully.
        try:
            try_get_lock_or_break_open()
        except Exception as exc:
            print("Failed to shut down locks:", exc)

        # Now trying to shutdown everything in an orderly manner.it in the
        # Since we are still inside curses but we don't know whether
        # the printhook or the logwriter was initialized properly already
        # or whether it crashed earlier we cannot print here and we also
        # cannot log, so we put all tracebacks into the debug_tb list to
        # print them later once the terminal is properly restored again.
        try:
            strategy_manager.unload()
        except Exception:
            debug_tb.append(traceback.format_exc())

        try:
            instance.stop()
        except Exception:
            debug_tb.append(traceback.format_exc())

        try:
            printhook.close()
        except Exception:
            debug_tb.append(traceback.format_exc())

        try:
            logwriter.close()
        except Exception:
            debug_tb.append(traceback.format_exc())

        time.sleep(1)
        try:
            with open("%s.leftovers.log" % config.filename[:-4], "w") as stacklog:
                stacklog.write(dump_all_stacks())
        except Exception as exc:
            print("Failed to write leftover stacktrace logs:", exc)
        # curses_loop() ends here, we must reach this point under all circumstances.
        # Now curses will restore the terminal back to cooked (normal) mode.

    # Here it begins. The very first thing is to always set US or GB locale
    # to have always the same well defined behavior for number formatting.
    for loc in ["en_US.UTF8", "en_GB.UTF8", "en_EN", "en_GB", "C"]:
        try:
            locale.setlocale(locale.LC_NUMERIC, loc)
            break
        except locale.Error:
            continue

    # before we can finally start the curses UI we might need to do some user
    # interaction on the command line, regarding the encrypted secret
    argp = argparse.ArgumentParser(
        description='Live market data monitor and trading bot experimentation framework')
    argp.add_argument('--config',
                      default="pytrader.ini",
                      help="Use different config file (default: %(default)s)")
    argp.add_argument('--add-secret', action="store_true",
                      help="prompt for API secret, encrypt it and then exit")
    argp.add_argument('--strategy', action="store", default="strategy.py",
                      help="name of strategy module files, comma separated list (default: %(default)s)")
    argp.add_argument('--protocol', action="store", default="",
                      help="force protocol (socketio, websocket or pubnub), ignore setting in .ini")
    argp.add_argument('--no-fulldepth', action="store_true", default=False,
                      help="do not download full depth (useful for debugging)")
    argp.add_argument('--no-depth', action="store_true", default=False,
                      help="do not request depth messages (implies no-fulldeph), useful for low traffic")
    argp.add_argument('--no-lag', action="store_true", default=False,
                      help="do not request order-lag updates, useful for low traffic")
    argp.add_argument('--no-history', action="store_true", default=False,
                      help="do not download full history (useful for debugging)")
    argp.add_argument('--use-http', action="store_true", default=False,
                      help="use http api for trading (more reliable, recommended")
    argp.add_argument('--no-http', action="store_true", default=False,
                      help="use streaming api for trading (problematic when streaming api disconnects often)")
    argp.add_argument('--password', action="store", default=None,
                      help="password for decryption of stored key. This is a dangerous option "
                      + "because the password might end up being stored in the history file "
                      + "of your shell, for example in ~/.bash_history. Use this only when "
                      + "starting it from within a script and then of course you need to "
                      + "keep this start script in a secure place!")
    args = argp.parse_args()

    config = api.ApiConfig(args.config)
    config.init_defaults(INI_DEFAULTS)
    config.filename = args.config
    secret = api.Secret(config)
    secret.password_from_commandline_option = args.password
    if args.add_secret:
        # prompt for secret, encrypt, write to .ini and then exit the program
        secret.prompt_encrypt()
    else:
        strat_mod_list = args.strategy.split(",")
        api.FORCE_PROTOCOL = args.protocol
        api.FORCE_NO_FULLDEPTH = args.no_fulldepth
        api.FORCE_NO_DEPTH = args.no_depth
        api.FORCE_NO_LAG = args.no_lag
        api.FORCE_NO_HISTORY = args.no_history
        api.FORCE_HTTP_API = args.use_http
        api.FORCE_NO_HTTP_API = args.no_http
        if api.FORCE_NO_DEPTH:
            api.FORCE_NO_FULLDEPTH = True

        # if its ok then we can finally enter the curses main loop
        if secret.prompt_decrypt() != secret.S_FAIL_FATAL:
            # Use curses wrapper
            curses.wrapper(curses_loop)
            # curses ended, terminal should be back in normal (cooked) mode

            if len(debug_tb):
                print "\n\n*** error(s) in curses_loop() that caused unclean shutdown:\n"
                for trb in debug_tb:
                    print trb
            else:
                print
                print "***************************************************************"
                print "*  Please donate!                                             *"
                print "*    caktux: 0xf05b7f96ac8b607fe62bf77b8aaf926d719d4294 (ETH) *"
                print "*            1EMtjvaxCGwFrLa8LHPwqa8xrxnj2VXFL5 (BTC)         *"
                print "***************************************************************"

if __name__ == "__main__":
    main()