Esempio n. 1
0
    def _layout(self):
        """ Core of the Commandline User Interface. Defines the layout of the interface.
        """

        # Manual tiling
        (maxy, maxx) = self.stdscr.getmaxyx()
        (self.maxy, self.maxx) = self.stdscr.getmaxyx()

        if self.auto_scale:
            splitx = int(maxx * .3)
            splity = int(maxy * .7)
        else:
            splitx = 30
            splity = maxy - 13

        # Title height
        title_height = 7

        # initialize windows
        # specify Upper left corner, size, title, color scheme and border/no-border
        self.main_border = StringWindow((0, 0), (maxx, maxy), ' FincLab Ver 0.1 ', TITLE_INACTIVE)
        self.pane_output = StringWindow((splitx, title_height + 1), (maxx - splitx - 1, splity - title_height - 2), 'Output', TITLE_INACTIVE)
        self.pane_menu = MenuWindow((1, title_height + 1), (splitx - 1, splity - title_height - 2), 'Menu', TITLE_INACTIVE)
        self.pane_command = EditorWindow((splitx, splity - 1), (maxx - splitx - 1, maxy - splity - 1), 'Strategy description (or type in commands)', palette=TITLE_INACTIVE, callback=self.pane_output.add_str)
        self.pane_status = Window((1, splity - 1), (splitx - 1, maxy - splity - 1), 'Status', palette=TITLE_INACTIVE)

        self.lines_in_output_pane = splity - title_height - 3

        self.update_progress_bar(0, 100)

        # Set menu options with corrisponding callbacks
        menu_actions = [
            MenuTuple("Say 'Hi'", (self.pane_output.add_str, 'Hello from Mars!', MENU_MESSAGE)),
            MenuTuple('Colour - Default', (curses.init_pair, TITLE_INACTIVE, COLOR_WHITE, COLOR_BLACK)),
            MenuTuple('Colour - Cyan', (curses.init_pair, TITLE_INACTIVE, COLOR_CYAN, COLOR_BLACK)),
            MenuTuple('Colour - Green', (curses.init_pair, TITLE_INACTIVE, COLOR_GREEN, COLOR_BLACK))]
        self.pane_menu.set_menu(menu_actions)

        # Put all the windows in a list so they can be updated together
        self.windows = [self.main_border, self.pane_output, self.pane_menu, self.pane_command, self.pane_status]

        # create input window cycling an input window must have a process_key(key) method
        self.input_windows = cycle([self.pane_menu, self.pane_command])
        self.active_window = next(self.input_windows)
        self.active_window.draw_border(TITLE_ACTIVE)

        # Add titles and other texts
        self.main_border.add_str("  FincLab Event-Driven Live-Trading / Backtesting Engine")
        self.main_border.add_str(" ")
        self.main_border.add_str("  Author: Peter Lee ([email protected])")
        self.main_border.add_str(" ")
        self.main_border.add_str("  Instrctions:")
        self.main_border.add_str('      - Please configure program settings in the "config.ini"')
        self.main_border.add_str("      - End-of-day data will be downloaded automatically if not found.")
Esempio n. 2
0
class CommandLineInterface():
    """
    Command line user interface for FincLab.

    Notes
    -----
        Trigger .run() to execute.

        Use self.stream.write() to post strings to the output pane.
    """

    def __init__(self, log_queue, config=config):
        """
        Initialise the interface.

        Parameters
        ----------
            log_queue : queue.Queue()
                The event queue for the event-driven system. Log (and message) events will be read from the queue and printed to the User Interface.
        """
        self.log_queue = log_queue
        self.sleep_time = float(config['ui']['refresh_time'])
        self.auto_scale = config.getboolean('ui', 'auto')
        self.stream = StdOutWrapper()
        self.config = config
        self.logger = logging.getLogger("FincLab.UI")

    def start_log_listener(self):
        """ Start the logger listener to take log events from the queue.
        Only starts after initialising the UI.
        """
        # Log listener
        handler = logging.StreamHandler(self.stream)
        formatter = logging.Formatter(fmt="%(name)-18s: %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
        handler.setFormatter(formatter)
        self.listener = logging.handlers.QueueListener(self.log_queue, handler)
        self.listener.start()

    def _refresh(self):
        """
        Refreshes the screen.using a loop, and captures key pressings to trigger different behaviours.
        """
        while True:
            # asynchronously try to get the key the user pressed
            key = self.stdscr.getch()
            if key == curses.ERR:
                # no key was pressed.  Do house-keeping
                dirtied = 0
                for win in self.windows:
                    dirtied += win.dirty
                    win.update()
                # if dirtied:
                self.stdscr.refresh()

                # Refresh output pane
                if self.stream.new_updates:
                    self.pane_output.clear()
                    output = self.stream.get_last_rows(self.lines_in_output_pane)
                    self.pane_output.refresh_pane(output)
                    self.pane_output.draw_border()
                    self.pane_output.dirty = True
                    self.stream.new_updates = False

                # Refresh app status
                if self.stream.new_status:
                    self.pane_status.clear()
                    # Engine tag
                    if self.stream.status.get("Engine"):
                        self.pane_status._addstr(1, 1, "Engine: {}".format(self.stream.status.get("Engine", 'n/a')))
                    else:
                        self.pane_status._addstr(1, 1, "Countdown: {} secs".format(self.stream.status.get("countdown", 'n/a')))
                    # Display datetime of the processing bar
                    if self.stream.status.get("current_datetime") and self.stream.status.get("Engine", 'n/a') == "Backtesting":
                        self.pane_status._addstr(2, 1, "Processing: {}".format(self.stream.status.get("current_datetime", 'n/a')))
                    # Components
                    self.pane_status._addstr(4, 1, "Components:")
                    self.pane_status._addstr(5, 1, "  {}".format(self.config['components']['strategy']))
                    self.pane_status._addstr(6, 1, "  {}".format(self.config['components']['data_handler']))
                    self.pane_status._addstr(7, 1, "  {}".format(self.config['components']['execution_handler']))
                    self.pane_status.draw_border()
                    self.pane_status.dirty = True
                    self.stream.new_status = False

                # Refresh strategy description
                if self.stream.new_description:
                    self.pane_command.clear()
                    self.pane_command._addstr(1, 1, self.stream.description)
                    self.pane_command.draw_border()
                    self.pane_command.dirty = True
                    self.stream.new_description = False

                # Refresh progress bar
                if self.stream.new_progress_bar:
                    self.update_progress_bar(0, 100)  # To clear outputs
                    self.update_progress_bar(self.stream.progress[0], self.stream.progress[1])  # To display correct outputs

                # Refresh time
                sleep(self.sleep_time)  # Refresh rate = 10 times per second

            elif key == KEY_TAB:
                # cycle input window
                self.active_window.draw_border()  # uses window default
                self.active_window = next(self.input_windows)
                self.active_window.draw_border(TITLE_ACTIVE)
            elif key == 27:  # ESC or ALT
                # Exit
                break
            else:
                # every other key gets processed by the active input window
                self.active_window.process_key(key)

    def run(self, stdscr):
        """
        Launch the interface.
        """
        # Set up screen.  Try/except to make sure the terminal gets put back together no matter what happens
        try:

            # sys.stdout = self.stream
            # sys.stderr = self.stream

            curses.cbreak()  # enable key press asynch
            # self.stdscr = curses.initscr()
            self.stdscr = stdscr
            curses.start_color()
            curses.noecho()  # let input windows handle drawing characters to the screen
            self.stdscr.nodelay(1)  # enable immediate time out (don't wait for keys at all)
            self.stdscr.keypad(1)  # enable enter, tab, and other keys

            # Set initial palette
            curses.init_pair(TITLE_ACTIVE, COLOR_BLUE, COLOR_BLACK)
            curses.init_pair(TITLE_INACTIVE, COLOR_WHITE, COLOR_BLACK)
            curses.init_pair(MENU_MESSAGE, COLOR_MAGENTA, COLOR_BLACK)

            # run while wrapped in this try/except
            self._layout()

            # Creates an internal logger
            self._create_logger()

            # Starts the logger listener
            self.start_log_listener()  # start the listener to take log events from the queue (only after the UI has initialised)

            # Ininite loop to refresh the interface
            self._refresh()

        except Exception:
            # put the terminal back in it's normal mode before raising the error
            curses.nocbreak()
            self.stdscr.keypad(0)
            curses.echo()
            curses.endwin()
            raise
        finally:
            curses.nocbreak()
            self.stdscr.keypad(0)
            curses.echo()
            curses.endwin()

            # sys.stdout = sys.__stdout__
            # sys.stderr = sys.__stderr__
            # sys.stdout.write(self.stream.get_text())

    def create_progress_bar(self, value, max_value, bar_length=50):
        # Bar
        if value <= 0:
            percent = float(0)
        else:
            percent = float(value + 1) / max_value
        hashes = '#' * int(round(percent * bar_length))
        spaces = ' ' * (bar_length - len(hashes))

        return ("[{}] {:>5.1f}%".format(hashes + spaces, percent * 100))

    def update_progress_bar(self, value, max_value):
        """ Update the progress bar which is located at the bottom of the UI

        Parameters
        ----------
            value : float
                the value of the current progress
            max_value : float
                the value when the bar reaches 100%
        """
        # Define progress bar (width of screen is (maxx - 2), height of screen is (maxy -2)
        progress_title = "Progress "
        bar = self.create_progress_bar(value, max_value, self.maxx - len(progress_title) - 4 - 9)  # 4->borders 9->digits at right end of the bar
        self.main_border._addstr(self.maxy - 2, 2, progress_title + bar)
        self.main_border.dirty = True

    def _layout(self):
        """ Core of the Commandline User Interface. Defines the layout of the interface.
        """

        # Manual tiling
        (maxy, maxx) = self.stdscr.getmaxyx()
        (self.maxy, self.maxx) = self.stdscr.getmaxyx()

        if self.auto_scale:
            splitx = int(maxx * .3)
            splity = int(maxy * .7)
        else:
            splitx = 30
            splity = maxy - 13

        # Title height
        title_height = 7

        # initialize windows
        # specify Upper left corner, size, title, color scheme and border/no-border
        self.main_border = StringWindow((0, 0), (maxx, maxy), ' FincLab Ver 0.1 ', TITLE_INACTIVE)
        self.pane_output = StringWindow((splitx, title_height + 1), (maxx - splitx - 1, splity - title_height - 2), 'Output', TITLE_INACTIVE)
        self.pane_menu = MenuWindow((1, title_height + 1), (splitx - 1, splity - title_height - 2), 'Menu', TITLE_INACTIVE)
        self.pane_command = EditorWindow((splitx, splity - 1), (maxx - splitx - 1, maxy - splity - 1), 'Strategy description (or type in commands)', palette=TITLE_INACTIVE, callback=self.pane_output.add_str)
        self.pane_status = Window((1, splity - 1), (splitx - 1, maxy - splity - 1), 'Status', palette=TITLE_INACTIVE)

        self.lines_in_output_pane = splity - title_height - 3

        self.update_progress_bar(0, 100)

        # Set menu options with corrisponding callbacks
        menu_actions = [
            MenuTuple("Say 'Hi'", (self.pane_output.add_str, 'Hello from Mars!', MENU_MESSAGE)),
            MenuTuple('Colour - Default', (curses.init_pair, TITLE_INACTIVE, COLOR_WHITE, COLOR_BLACK)),
            MenuTuple('Colour - Cyan', (curses.init_pair, TITLE_INACTIVE, COLOR_CYAN, COLOR_BLACK)),
            MenuTuple('Colour - Green', (curses.init_pair, TITLE_INACTIVE, COLOR_GREEN, COLOR_BLACK))]
        self.pane_menu.set_menu(menu_actions)

        # Put all the windows in a list so they can be updated together
        self.windows = [self.main_border, self.pane_output, self.pane_menu, self.pane_command, self.pane_status]

        # create input window cycling an input window must have a process_key(key) method
        self.input_windows = cycle([self.pane_menu, self.pane_command])
        self.active_window = next(self.input_windows)
        self.active_window.draw_border(TITLE_ACTIVE)

        # Add titles and other texts
        self.main_border.add_str("  FincLab Event-Driven Live-Trading / Backtesting Engine")
        self.main_border.add_str(" ")
        self.main_border.add_str("  Author: Peter Lee ([email protected])")
        self.main_border.add_str(" ")
        self.main_border.add_str("  Instrctions:")
        self.main_border.add_str('      - Please configure program settings in the "config.ini"')
        self.main_border.add_str("      - End-of-day data will be downloaded automatically if not found.")

    def _create_logger(self):
        """
        Logger that utilises the queue handler.
        """
        level = logging.DEBUG
        self.logger = logging.getLogger("FincLab.UI")
        self.logger.setLevel(level)
        formatter = logging.Formatter(fmt="%(levelname)-8s %(message)s")
        formatter = logging.Formatter(fmt="%(asctime)s %(name)-12s %(levelname)-8s %(message)s", datefmt="%Y-%m-%d %H:%M:%S")
        # queue handler
        queue_handler = logging.handlers.QueueHandler(self.log_queue)
        queue_handler.setLevel(logging.DEBUG)
        queue_handler.setFormatter(formatter)
        self.logger.addHandler(queue_handler)