Ejemplo n.º 1
0
class Reports():
    """Reports class initilizer

    :Optional:

        blotter : str
            Log trades to MySQL server used by this Blotter (default is "auto detect")
        port : int
            HTTP port to use (default: 5000)
        host : string
            Host to bind the http process to (defaults 0.0.0.0)
        password : string
            Password for logging in to the web app (auto-generated by default). Use "" for no password.
    """

    # ---------------------------------------
    def __init__(self,
                 blotter=None,
                 port=5000,
                 host="0.0.0.0",
                 password=None,
                 nopass=False,
                 **kwargs):
        # return
        self._password = password if password is not None else hashlib.sha1(
            str(datetime.datetime.now()).encode()).hexdigest()[:6]

        # initilize class logger
        self.log = logging.getLogger(__name__)

        # override args with any (non-default) command-line args
        self.args = {
            arg: val
            for arg, val in locals().items()
            if arg not in ('__class__', 'self', 'kwargs')
        }
        self.args.update(kwargs)
        self.args.update(self.load_cli_args())

        self.dbconn = None
        self.dbcurr = None

        self.host = self.args['host'] if self.args['host'] is not None else host
        self.port = self.args['port'] if self.args['port'] is not None else port

        # blotter / db connection
        self.blotter_name = self.args['blotter'] if self.args[
            'blotter'] is not None else blotter
        self.blotter_args = load_blotter_args(self.blotter_name)
        self.blotter = Blotter(**self.blotter_args)

        # connect to mysql using blotter's settings
        self.dbconn = pymysql.connect(host=str(self.blotter_args['dbhost']),
                                      port=int(self.blotter_args['dbport']),
                                      user=str(self.blotter_args['dbuser']),
                                      passwd=str(self.blotter_args['dbpass']),
                                      db=str(self.blotter_args['dbname']),
                                      autocommit=True)
        self.dbcurr = self.dbconn.cursor()

    # ---------------------------------------
    def load_cli_args(self):
        """
        Parse command line arguments and return only the non-default ones

        :Retruns: dict
            a dict of any non-default args passed on the command-line.
        """
        parser = argparse.ArgumentParser(
            description='QTPyLib Reporting',
            formatter_class=argparse.ArgumentDefaultsHelpFormatter)

        parser.add_argument('--port',
                            default=self.args["port"],
                            help='HTTP port to use',
                            type=int)
        parser.add_argument('--host',
                            default=self.args["host"],
                            help='Host to bind the http process to')
        parser.add_argument('--blotter',
                            help='Use this Blotter\'s MySQL server settings')
        parser.add_argument('--nopass',
                            help='Skip password for web app (flag)',
                            action='store_true')

        # only return non-default cmd line args
        # (meaning only those actually given)
        cmd_args, unknown = parser.parse_known_args()
        args = {
            arg: val
            for arg, val in vars(cmd_args).items()
            if val != parser.get_default(arg)
        }
        return args

    # ---------------------------------------
    def send_static(self, path):
        return send_from_directory('_webapp/', path)

    # ---------------------------------------
    def login(self, password):
        if self._password == password:
            resp = make_response('yes')
            resp.set_cookie('password', password)
            return resp
        else:
            return make_response("no")

    # ---------------------------------------
    def algos(self, json=True):

        algos = pd.read_sql("SELECT DISTINCT algo FROM trades",
                            self.dbconn).to_dict(orient="records")

        if json:
            return jsonify(algos)
        else:
            return algos

    # ---------------------------------------
    def symbols(self, json=True):

        symbols = pd.read_sql("SELECT * FROM symbols",
                              self.dbconn).to_dict(orient="records")

        if json:
            return jsonify(symbols)
        else:
            return symbols

    # ---------------------------------------
    def trades(self, start=None, end=None, algo_id=None, json=True):

        if algo_id is not None:
            algo_id = algo_id.replace('/', '')
        if start is not None:
            start = start.replace('/', '')
        if end is not None:
            end = end.replace('/', '')

        if start is None:
            start = tools.backdate("7D", date=None, as_datetime=False)

        trades_query = "SELECT * FROM trades WHERE exit_time IS NOT NULL"
        trades_where = []

        if isinstance(start, str):
            trades_where.append("entry_time>='" + start + "'")
        if isinstance(end, str):
            trades_where.append("exit_time<='" + end + "'")
        if algo_id is not None:
            trades_where.append("algo='" + algo_id + "'")

        if len(trades_where) > 0:
            trades_query += " AND " + " AND ".join(trades_where)

        trades = pd.read_sql(trades_query, self.dbconn)
        trades['exit_time'].fillna(0, inplace=True)

        trades['slippage'] = abs(trades['entry_price'] -
                                 trades['market_price'])

        trades['slippage'] = np.where(
            ((trades['direction'] == "LONG") &
             (trades['entry_price'] > trades['market_price'])) |
            ((trades['direction'] == "SHORT") &
             (trades['entry_price'] < trades['market_price'])),
            -trades['slippage'], trades['slippage'])

        trades = trades.sort_values(['exit_time', 'entry_time'],
                                    ascending=[False, False])

        trades = trades.to_dict(orient="records")
        if json:
            return jsonify(trades)
        else:
            return trades

    # ---------------------------------------
    def positions(self, algo_id=None, json=True):

        if algo_id is not None:
            algo_id = algo_id.replace('/', '')

        trades_query = "SELECT * FROM trades WHERE exit_time IS NULL"
        if algo_id is not None:
            trades_query += " AND algo='" + algo_id + "'"

        trades = pd.read_sql(trades_query, self.dbconn)

        last_query = "SELECT s.id, s.symbol, max(t.last) as last_price FROM ticks t LEFT JOIN symbols s ON (s.id=t.symbol_id) GROUP BY s.id"
        last_prices = pd.read_sql(last_query, self.dbconn)

        trades = trades.merge(last_prices, on=['symbol'])

        trades['unrealized_pnl'] = np.where(
            trades['direction'] == "SHORT",
            trades['entry_price'] - trades['last_price'],
            trades['last_price'] - trades['entry_price'])

        trades['slippage'] = abs(trades['entry_price'] -
                                 trades['market_price'])
        trades['slippage'] = np.where(
            ((trades['direction'] == "LONG") &
             (trades['entry_price'] > trades['market_price'])) |
            ((trades['direction'] == "SHORT") &
             (trades['entry_price'] < trades['market_price'])),
            -trades['slippage'], trades['slippage'])

        trades = trades.sort_values(['entry_time'], ascending=[False])

        trades = trades.to_dict(orient="records")
        if json:
            return jsonify(trades)
        else:
            return trades

    # ---------------------------------------
    def trades_by_algo(self, algo_id=None, start=None, end=None):
        trades = self.trades(start, end, algo_id=algo_id, json=False)
        return jsonify(trades)

    # ---------------------------------------
    def bars(self, resolution, symbol, start=None, end=None, json=True):

        if start is not None:
            start = start.replace('/', '')
        if end is not None:
            end = end.replace('/', '')

        if start is None:
            start = tools.backdate("7D", date=None, as_datetime=False)

        bars = self.blotter.history(symbols=symbol,
                                    start=start,
                                    end=end,
                                    resolution=resolution)

        bars['datetime'] = bars.index

        bars = bars.to_dict(orient="records")
        if json:
            return jsonify(bars)
        else:
            return bars

    # ---------------------------------------
    def index(self, start=None, end=None):
        if not self.args['nopass']:
            if self._password != "" and self._password != request.cookies.get(
                    'password'):
                return render_template('login.html')

        return render_template('dashboard.html')

    # ---------------------------------------
    def run(self):
        """Starts the reporting module

        Makes the dashboard web app available via localhost:port, and exposes
        a REST API for trade information, open positions and market data.
        """

        global app

        # -----------------------------------
        # assign view
        app.add_url_rule('/', 'index', view_func=self.index)
        app.add_url_rule('/<path:start>', 'index', view_func=self.index)
        app.add_url_rule('/<start>/<path:end>', 'index', view_func=self.index)

        app.add_url_rule('/algos', 'algos', view_func=self.algos)
        app.add_url_rule('/symbols', 'symbols', view_func=self.symbols)

        app.add_url_rule('/positions', 'positions', view_func=self.positions)
        app.add_url_rule('/positions/<path:algo_id>',
                         'positions',
                         view_func=self.positions)

        app.add_url_rule('/algo/<path:algo_id>',
                         'trades_by_algo',
                         view_func=self.trades_by_algo)
        app.add_url_rule('/algo/<algo_id>/<path:start>',
                         'trades_by_algo',
                         view_func=self.trades_by_algo)
        app.add_url_rule('/algo/<algo_id>/<start>/<path:end>',
                         'trades_by_algo',
                         view_func=self.trades_by_algo)

        app.add_url_rule('/bars/<resolution>/<symbol>',
                         'bars',
                         view_func=self.bars)
        app.add_url_rule('/bars/<resolution>/<symbol>/<path:start>',
                         'bars',
                         view_func=self.bars)
        app.add_url_rule('/bars/<resolution>/<symbol>/<start>/<path:end>',
                         'bars',
                         view_func=self.bars)

        app.add_url_rule('/trades', 'trades', view_func=self.trades)
        app.add_url_rule('/trades/<path:start>',
                         'trades',
                         view_func=self.trades)
        app.add_url_rule('/trades/<start>/<path:end>',
                         'trades',
                         view_func=self.trades)
        app.add_url_rule('/login/<password>', 'login', view_func=self.login)
        app.add_url_rule('/static/<path>',
                         'send_static',
                         view_func=self.send_static)

        # let user know what the temp password is
        if not self.args['nopass'] and self._password != "":
            print(" * Web app password is:", self._password)

        # notice
        # print(" * Running on http://"+ str(self.host) +":"+str(self.port)+"/ (Press CTRL+C to quit)")

        # -----------------------------------
        # run flask app
        app.run(debug=True, host=str(self.host), port=int(self.port))
Ejemplo n.º 2
0
class Algo(Broker):
    """Algo class initilizer (sub-class of Broker)

    :Parameters:

        instruments : list
            List of IB contract tuples
        resolution : str
            Desired bar resolution (using pandas resolution: 1T, 1H, etc). Use K for tick bars.
        tick_window : int
            Length of tick lookback window to keep. Defaults to 1
        bar_window : int
            Length of bar lookback window to keep. Defaults to 100
        timezone : str
            Convert IB timestamps to this timezone (eg. US/Central). Defaults to UTC
        preload : str
            Preload history when starting algo (using pandas resolution: 1H, 1D, etc). Use K for tick bars.
        continuous : bool
            Tells preloader to construct continuous Futures contracts (default is True)
        blotter : str
            Log trades to MySQL server used by this Blotter (default is "auto detect")
        force_resolution : bool
            Force new bar on every ``resolution`` even if no new ticks received (default is False)
    """

    __metaclass__ = ABCMeta

    def __init__(self, instruments, resolution,
        tick_window=1, bar_window=100, timezone="UTC", preload=None,
        continuous=True, blotter=None, force_resolution=False, **kwargs):

        self.name = str(self.__class__).split('.')[-1].split("'")[0]

        # ----------------------------------------------------
        # default args
        self.args = kwargs.copy()
        cli_args  = self.load_cli_args()

        # override kwargs args with cli args
        for arg in cli_args:
            if arg not in self.args or ( arg in self.args and cli_args[arg] is not None ):
                self.args[arg] = cli_args[arg]

        # fix flag args (no value)
        for arg in ["backtest"]:
            if arg in kwargs and "--"+str(arg) not in sys.argv:
                self.args[arg] = kwargs["backtest"]
        # ----------------------------------------------------


        # assign algo params
        self.bars           = pd.DataFrame()
        self.ticks          = pd.DataFrame()
        self.quotes         = {}
        self.books          = {}
        self.tick_count     = 0
        self.tick_bar_count = 0
        self.bar_count      = 0
        self.bar_hash       = 0

        self.tick_window    = tick_window if tick_window > 0 else 1
        if "V" in resolution:
            self.tick_window = 1000
        self.bar_window     = bar_window if bar_window > 0 else 100
        self.resolution     = resolution.replace("MIN", "T")
        self.timezone       = timezone
        self.preload        = preload
        self.continuous     = continuous

        self.backtest       = self.args["backtest"]
        self.backtest_start = self.args["start"]
        self.backtest_end   = self.args["end"]

        # -----------------------------------
        self.sms_numbers    = [] if self.args["sms"] is None else self.args["sms"]
        self.trade_log_dir  = self.args["log"]
        self.blotter_name   = self.args["blotter"] if self.args["blotter"] is not None else blotter
        self.record_output  = self.args["output"]

        # -----------------------------------
        # load blotter settings && initilize Blotter
        self.load_blotter_args(self.args["blotter"])
        self.blotter = Blotter(**self.blotter_args)

        # -----------------------------------
        # initiate broker/order manager
        super().__init__(instruments, ibclient=int(self.args["ibclient"]),
            ibport=int(self.args["ibport"]), ibserver=str(self.args["ibserver"]))

        # -----------------------------------
        # signal collector
        self.signals = {}
        for sym in self.symbols:
            self.signals[sym] = pd.DataFrame()

        # -----------------------------------
        # initilize output file
        self.record_ts = None
        if self.record_output:
            self.datastore = tools.DataStore(self.args["output"])

        # -----------------------------------
        # initiate strategy
        self.on_start()

        # ---------------------------------------
        # add stale ticks to allow for interval-based bars
        if force_resolution and self.resolution[-1] not in ("K", "V"):
            self.bar_timer = tools.RecurringTask(
                self.add_stale_tick, interval_sec=1, init_sec=1, daemon=True)

    # ---------------------------------------
    def add_stale_tick(self):
        if len(self.ticks) > 0:
            tick = self.ticks[-1:].to_dict(orient='records')[0]
            tick['timestamp'] = datetime.utcnow()

            tick = pd.DataFrame(index=[0], data=tick)
            tick.set_index('timestamp', inplace=True)
            tick = tools.set_timezone(tick, tz=self.timezone)

            self._tick_handler(tick, stale_tick=True)


    # ---------------------------------------
    def load_cli_args(self):
        parser = argparse.ArgumentParser(description='QTPy Algo Framework')

        parser.add_argument('--ibport', default='4001', help='IB TWS/GW Port to use (default: 4001)', required=False)
        parser.add_argument('--ibclient', default='998', help='IB TWS/GW Client ID (default: 998)', required=False)
        parser.add_argument('--ibserver', default='localhost', help='IB TWS/GW Server hostname (default: localhost)', required=False)
        parser.add_argument('--sms', nargs='+', help='Numbers to text orders', required=False)
        parser.add_argument('--log', default=None, help='Path to store trade data (default: ~/qpy/trades/)', required=False)

        parser.add_argument('--backtest', help='Work in Backtest mode', action='store_true', required=False)
        parser.add_argument('--start', help='Backtest start date', required=False)
        parser.add_argument('--end', help='Backtest end date', required=False)
        parser.add_argument('--output', help='Path to save the recorded data', required=False)

        parser.add_argument('--blotter', help='Log trades to the MySQL server used by this Blotter', required=False)

        args, unknown = parser.parse_known_args()

        return args.__dict__

    # ---------------------------------------
    def run(self):
        """Starts the algo

        Connects to the Blotter, processes market data and passes
        tick data to the ``on_tick`` function and bar data to the
        ``on_bar`` methods.
        """

        # -----------------------------------
        # backtest mode?
        if self.backtest:
            if self.record_output is None:
                logging.error("Must provide an output file for Backtest mode")
                sys.exit(0)
            if self.backtest_start is None:
                logging.error("Must provide start date for Backtest mode")
                sys.exit(0)
            if self.backtest_end is None:
                self.backtest_end = datetime.now().strftime('%Y-%m-%d %H:%M:%S.%f')

            # backtest history
            self.blotter.drip(
                symbols       = self.symbols,
                start         = self.backtest_start,
                end           = self.backtest_end,
                resolution    = self.resolution,
                tz            = self.timezone,
                continuous    = self.continuous,
                quote_handler = self._quote_handler,
                tick_handler  = self._tick_handler,
                bar_handler   = self._bar_handler,
                book_handler  = self._book_handler
            )

        # -----------------------------------
        # live data mode
        else:
            # preload history
            if self.preload is not None:
                try: # dbskip may be active
                    self.bars = self.blotter.history(
                        symbols    = self.symbols,
                        start      = tools.backdate(self.preload),
                        resolution = self.resolution,
                        tz         = self.timezone,
                        continuous = self.continuous
                    )
                except:
                    pass
                # print(self.bars)

            # add instruments to blotter in case they do not exist
            self.blotter.register(self.instruments)

            # listen for RT data
            self.blotter.listen(
                symbols       = self.symbols,
                tz            = self.timezone,
                quote_handler = self._quote_handler,
                tick_handler  = self._tick_handler,
                bar_handler   = self._bar_handler,
                book_handler  = self._book_handler
            )

    # ---------------------------------------
    @abstractmethod
    def on_start(self):
        """
        Invoked once when algo starts. Used for when the strategy
        needs to initialize parameters upon starting.

        """
        # raise NotImplementedError("Should implement on_start()")
        pass

    # ---------------------------------------
    @abstractmethod
    def on_quote(self, instrument):
        """
        Invoked on every quote captured for the selected instrument.
        This is where you'll write your strategy logic for quote events.

        :Parameters:

            symbol : string
                `Instruments Object <#instrument-api>`_

        """
        # raise NotImplementedError("Should implement on_quote()")
        pass

    # ---------------------------------------
    @abstractmethod
    def on_tick(self, instrument):
        """
        Invoked on every tick captured for the selected instrument.
        This is where you'll write your strategy logic for tick events.

        :Parameters:

            symbol : string
                `Instruments Object <#instrument-api>`_

        """
        # raise NotImplementedError("Should implement on_tick()")
        pass

    # ---------------------------------------
    @abstractmethod
    def on_bar(self, instrument):
        """
        Invoked on every tick captured for the selected instrument.
        This is where you'll write your strategy logic for tick events.

        :Parameters:

            instrument : object
                `Instruments Object <#instrument-api>`_

        """
        # raise NotImplementedError("Should implement on_bar()")
        pass

    # ---------------------------------------
    @abstractmethod
    def on_orderbook(self, instrument):
        """
        Invoked on every change to the orderbook for the selected instrument.
        This is where you'll write your strategy logic for orderbook changes events.

        :Parameters:

            symbol : string
                `Instruments Object <#instrument-api>`_

        """
        # raise NotImplementedError("Should implement on_orderbook()")
        pass

    # ---------------------------------------
    @abstractmethod
    def on_fill(self, instrument, order):
        """
        Invoked on every order fill for the selected instrument.
        This is where you'll write your strategy logic for fill events.

        :Parameters:

            instrument : object
                `Instruments Object <#instrument-api>`_
            order : object
                Filled order data

        """
        # raise NotImplementedError("Should implement on_fill()")
        pass

    # ---------------------------------------
    def get_instrument(self, symbol):
        """
        A string subclass that provides easy access to misc
        symbol-related methods and information using shorthand.
        Refer to the `Instruments API <#instrument-api>`_
        for available methods and properties

        Call from within your strategy:
        ``instrument = self.get_instrument("SYMBOL")``

        :Parameters:

            symbol : string
                instrument symbol

        """
        instrument = Instrument(self._getsymbol_(symbol))
        instrument._set_parent(self)
        return instrument

    # ---------------------------------------
    def get_history(self, symbols, start, end=None, resolution="1T", tz="UTC"):
        """Get historical market data.
        Connects to Blotter and gets historical data from storage

        :Parameters:
            symbols : list
                List of symbols to fetch history for
            start : datetime / string
                History time period start date (datetime or YYYY-MM-DD[ HH:MM[:SS]] string)

        :Optional:
            end : datetime / string
                History time period end date (datetime or YYYY-MM-DD[ HH:MM[:SS]] string)
            resolution : string
                History resoluton (Pandas resample, defaults to 1T/1min)
            tz : string
                History timezone (defaults to UTC)

        :Returns:
            history : pd.DataFrame
                Pandas DataFrame object with historical data for all symbols
        """
        return self.blotter.history(symbols, start, end, resolution, tz)


    # ---------------------------------------
    # shortcuts to broker._create_order
    # ---------------------------------------
    def order(self, signal, symbol, quantity=0, **kwargs):
        """ Send an order for the selected instrument

        :Parameters:

            direction : string
                Order Type (BUY/SELL, EXIT/FLATTEN)
            symbol : string
                instrument symbol
            quantity : int
                Order quantiry

        :Optional:

            limit_price : float
                In case of a LIMIT order, this is the LIMIT PRICE
            expiry : int
                Cancel this order if not filled after *n* seconds (default 60 seconds)
            order_type : string
                Type of order: Market (default), LIMIT (default when limit_price is passed), MODIFY (required passing or orderId)
            orderId : int
                If modifying an order, the order id of the modified order
            target : float
                target (exit) price
            initial_stop : float
                price to set hard stop
            stop_limit: bool
                Flag to indicate if the stop should be STOP or STOP LIMIT (default False=STOP)
            trail_stop_at : float
                price at which to start trailing the stop
            trail_stop_by : float
                % of trailing stop distance from current price
            fillorkill: bool
                fill entire quantiry or none at all
            iceberg: bool
                is this an iceberg (hidden) order
            tif: str
                time in force (DAY, GTC, IOC, GTD). default is ``DAY``
        """
        if signal.upper() == "EXIT" or signal.upper() == "FLATTEN":
            position = self.get_positions(symbol)
            if position['position'] == 0:
                return

            kwargs['symbol']    = symbol
            kwargs['quantity']  = abs(position['position'])
            kwargs['direction'] = "BUY" if position['position'] < 0 else "SELL"

            # print("EXIT", kwargs)

            try: self.record(position=0)
            except: pass

            if not self.backtest:
                self._create_order(**kwargs)

        else:
            if quantity == 0:
                return

            kwargs['symbol']    = symbol
            kwargs['quantity']  = abs(quantity)
            kwargs['direction'] = signal.upper()

            # print(signal.upper(), kwargs)

            # record
            try:
                quantity = -quantity if kwargs['direction'] == "BUY" else quantity
                self.record(position=quantity)
            except:
                pass

            if not self.backtest:
                self._create_order(**kwargs)


    # ---------------------------------------
    def cancel_order(self, orderId):
        """ Cancels a un-filled order

        Parameters:
            orderId : int
                Order ID
        """
        self._cancel_order(orderId)

    # ---------------------------------------
    def record(self, *args, **kwargs):
        """Records data for later analysis.
        Values will be logged to the file specified via
        ``--output [file]`` (along with bar data) as
        csv/pickle/h5 file.

        Call from within your strategy:
        ``self.record(key=value)``

        :Parameters:
            ** kwargs : mixed
                The names and values to record

        """
        if self.record_output:
            try: self.datastore.record(self.record_ts, *args, **kwargs)
            except: pass


    # ---------------------------------------
    def sms(self, text):
        """Sends an SMS message.
        Relies on properly setting up an SMS provider (refer to the
        SMS section of the documentation for more information about this)

        Call from within your strategy:
        ``self.sms("message text")``

        :Parameters:
            text : string
                The body of the SMS message to send

        """
        logging.info("SMS: "+str(text))
        sms.send_text(self.name +': '+ str(text), self.sms_numbers)

    # ---------------------------------------
    def _caller(self, caller):
        stack = [x[3] for x in inspect.stack()][1:-1]
        return caller in stack

    # ---------------------------------------
    def _book_handler(self, book):
        symbol = book['symbol']
        del book['symbol']
        del book['kind']

        self.books[symbol] = book
        self.on_orderbook(self.get_instrument(symbol))


    # ---------------------------------------
    def _quote_handler(self, quote):
        del quote['kind']
        self.quotes[quote['symbol']] = quote
        self.on_quote(self.get_instrument(quote))

    # ---------------------------------------
    def _tick_handler(self, tick, stale_tick=False):
        self._cancel_expired_pending_orders()

        # initial value
        if self.record_ts is None:
            self.record_ts = tick.index[0]

        if self.resolution[-1] not in ("S", "K", "V"):
            self.ticks = self._update_window(self.ticks, tick, window=self.tick_window)
        else:
            self.ticks = self._update_window(self.ticks, tick)
            bars = tools.resample(self.ticks, self.resolution)

            if len(bars.index) > self.tick_bar_count > 0 or stale_tick:
                self.record_ts = tick.index[0]
                self._bar_handler(bars)

                periods = int("".join([s for s in self.resolution if s.isdigit()]))
                self.ticks = self.ticks[-periods:]

            self.tick_bar_count = len(bars)

            # record tick bars
            self.record(bars[-1:])

        if not stale_tick:
            self.on_tick(self.get_instrument(tick))


    # ---------------------------------------
    def _bar_handler(self, bar):

        is_tick_or_volume_bar = False
        handle_bar  = True

        if self.resolution[-1] in ("S", "K", "V"):
            is_tick_or_volume_bar = True
            handle_bar = self._caller("_tick_handler")

        # drip is also ok
        handle_bar = handle_bar or self._caller("drip")

        if is_tick_or_volume_bar:
            # just add a bar (used by tick bar bandler)
            self.bars = self._update_window(self.bars, bar, window=self.bar_window)
        else:
            # add the bar and resample to resolution
            self.bars = self._update_window(self.bars, bar, window=self.bar_window, resolution=self.resolution)

        # new bar?
        this_bar_hash = abs(hash(str(self.bars.index.values[-1]))) % (10 ** 8)
        newbar = (self.bar_hash != this_bar_hash)
        self.bar_hash = this_bar_hash

        if newbar and handle_bar:
            self.record_ts = bar.index[0]
            self.on_bar(self.get_instrument(bar))

            if self.resolution[-1] not in ("S", "K", "V"):
                self.record(bar)


    # ---------------------------------------
    def _update_window(self, df, data, window=None, resolution=None):
        if df is None:
            df = data
        else:
            df = df.append(data)

        if resolution is not None:
            try:
                tz = str(df.index.tz)
            except:
                tz = None
            df = tools.resample(df, resolution=resolution, tz=tz)

        if window is None:
            return df

        return df[-window:]

    # ---------------------------------------
    # signal logging methods
    # ---------------------------------------
    def _add_signal_history(self, df, symbol):
        """ Initilize signal history """
        if symbol not in self.signals.keys() or len(self.signals[symbol]) == 0:
            self.signals[symbol] = [nan]*len(df)
        else:
            self.signals[symbol].append(nan)

        self.signals[symbol] = self.signals[symbol][-len(df):]
        df.loc[-len(self.signals[symbol]):, 'signal'] = self.signals[symbol]

        return df

    def _log_signal(self, symbol, signal):
        """ Log signal

        :Parameters:
            symbol : string
                instruments symbol
            signal : integer
                signal identifier (1, 0, -1)

        """
        self.signals[symbol][-1] = signal
Ejemplo n.º 3
0
class Reports():
    """Reports class initilizer

    :Optional:

        blotter : str
            Log trades to MySQL server used by this Blotter (default is "auto detect")
        port : int
            HTTP port to use (default: 5000)
        host : string
            Host to bind the http process to (defaults 0.0.0.0)
        password : string
            Password for logging in to the web app (auto-generated by default). Use "" for no password.
    """

    # ---------------------------------------
    def __init__(self, blotter=None, port=5000, host="0.0.0.0", password=None, nopass=False, **kwargs):
        # return
        self._password = password if password is not None else hashlib.sha1(
            str(datetime.datetime.now()).encode()).hexdigest()[:6]

        # initilize class logger
        self.log = logging.getLogger(__name__)

        # override args with any (non-default) command-line args
        self.args = {arg: val for arg, val in locals().items(
            ) if arg not in ('__class__', 'self', 'kwargs')}
        self.args.update(kwargs)
        self.args.update(self.load_cli_args())

        self.dbconn = None
        self.dbcurr = None

        self.host = self.args['host'] if self.args['host'] is not None else host
        self.port = self.args['port'] if self.args['port'] is not None else port

        # blotter / db connection
        self.blotter_name = self.args['blotter'] if self.args['blotter'] is not None else blotter
        self.blotter_args = load_blotter_args(self.blotter_name)
        self.blotter = Blotter(**self.blotter_args)

        # connect to mysql using blotter's settings
        self.dbconn = pymysql.connect(
            host   = str(self.blotter_args['dbhost']),
            port   = int(self.blotter_args['dbport']),
            user   = str(self.blotter_args['dbuser']),
            passwd = str(self.blotter_args['dbpass']),
            db     = str(self.blotter_args['dbname']),
            autocommit = True
        )
        self.dbcurr = self.dbconn.cursor()

    # ---------------------------------------
    def load_cli_args(self):
        """
        Parse command line arguments and return only the non-default ones

        :Retruns: dict
            a dict of any non-default args passed on the command-line.
        """
        parser = argparse.ArgumentParser(description='QTPyLib Reporting',
                            formatter_class=argparse.ArgumentDefaultsHelpFormatter)

        parser.add_argument('--port', default=self.args["port"],
                            help='HTTP port to use', type=int)
        parser.add_argument('--host', default=self.args["host"],
                            help='Host to bind the http process to')
        parser.add_argument('--blotter',
                            help='Use this Blotter\'s MySQL server settings')
        parser.add_argument('--nopass',
                            help='Skip password for web app (flag)',
                            action='store_true')

        # only return non-default cmd line args
        # (meaning only those actually given)
        cmd_args, unknown = parser.parse_known_args()
        args = {arg: val for arg, val in vars(cmd_args).items() if val != parser.get_default(arg)}
        return args

    # ---------------------------------------
    def send_static(self, path):
        return send_from_directory('_webapp/', path)

    # ---------------------------------------
    def login(self, password):
        if self._password == password:
            resp = make_response('yes')
            resp.set_cookie('password', password)
            return resp
        else:
            return make_response("no")

    # ---------------------------------------
    def algos(self, json=True):

        algos = pd.read_sql("SELECT DISTINCT algo FROM trades",
            self.dbconn).to_dict(orient="records")

        if json:
            return jsonify(algos)
        else:
            return algos

    # ---------------------------------------
    def symbols(self, json=True):

        symbols = pd.read_sql("SELECT * FROM symbols", self.dbconn).to_dict(orient="records")

        if json:
            return jsonify(symbols)
        else:
            return symbols

    # ---------------------------------------
    def trades(self, start=None, end=None, algo_id=None, json=True):

        if algo_id is not None:
            algo_id = algo_id.replace('/', '')
        if start is not None:
            start = start.replace('/', '')
        if end is not None:
            end = end.replace('/', '')

        if start is None:
            start = tools.backdate("7D", date=None, as_datetime=False)

        trades_query = "SELECT * FROM trades WHERE exit_time IS NOT NULL"
        trades_where = []

        if isinstance(start, str):
            trades_where.append("entry_time>='" + start + "'")
        if isinstance(end, str):
            trades_where.append("exit_time<='" + end + "'")
        if algo_id is not None:
            trades_where.append("algo='" + algo_id + "'")

        if len(trades_where) > 0:
            trades_query += " AND " + " AND ".join(trades_where)

        trades = pd.read_sql(trades_query, self.dbconn)
        trades['exit_time'].fillna(0, inplace=True)

        trades['slippage'] = abs(trades['entry_price'] - trades['market_price'])

        trades['slippage'] = np.where(
            ((trades['direction'] == "LONG") & (trades['entry_price'] > trades['market_price'])) |
            ((trades['direction'] == "SHORT") & (trades['entry_price'] < trades['market_price'])),
            -trades['slippage'], trades['slippage'])

        trades = trades.sort_values(['exit_time', 'entry_time'], ascending=[False, False])

        trades = trades.to_dict(orient="records")
        if json:
            return jsonify(trades)
        else:
            return trades

    # ---------------------------------------
    def positions(self, algo_id=None, json=True):

        if algo_id is not None:
            algo_id = algo_id.replace('/', '')

        trades_query = "SELECT * FROM trades WHERE exit_time IS NULL"
        if algo_id is not None:
            trades_query += " AND algo='" + algo_id + "'"

        trades = pd.read_sql(trades_query, self.dbconn)

        last_query = "SELECT s.id, s.symbol, max(t.last) as last_price FROM ticks t LEFT JOIN symbols s ON (s.id=t.symbol_id) GROUP BY s.id"
        last_prices = pd.read_sql(last_query, self.dbconn)

        trades = trades.merge(last_prices, on=['symbol'])

        trades['unrealized_pnl'] = np.where(
                trades['direction']=="SHORT",
                trades['entry_price']-trades['last_price'],
                trades['last_price']-trades['entry_price'])

        trades['slippage'] = abs(trades['entry_price'] - trades['market_price'])
        trades['slippage'] = np.where(
            ((trades['direction'] == "LONG") & (trades['entry_price'] > trades['market_price'])) |
            ((trades['direction'] == "SHORT") & (trades['entry_price'] < trades['market_price'])),
            -trades['slippage'], trades['slippage'])

        trades = trades.sort_values(['entry_time'], ascending=[False])

        trades = trades.to_dict(orient="records")
        if json:
            return jsonify(trades)
        else:
            return trades

    # ---------------------------------------
    def trades_by_algo(self, algo_id=None, start=None, end=None):
        trades = self.trades(start, end, algo_id=algo_id, json=False)
        return jsonify(trades)

    # ---------------------------------------
    def bars(self, resolution, symbol, start=None, end=None, json=True):

        if start is not None:
            start = start.replace('/', '')
        if end is not None:
            end = end.replace('/', '')

        if start is None:
            start = tools.backdate("7D", date=None, as_datetime=False)

        bars = self.blotter.history(
            symbols    = symbol,
            start      = start,
            end        = end,
            resolution = resolution
        )

        bars['datetime'] = bars.index

        bars = bars.to_dict(orient="records")
        if json:
            return jsonify(bars)
        else:
            return bars

    # ---------------------------------------
    def index(self, start=None, end=None):
        if not self.args['nopass']:
            if self._password != "" and self._password != request.cookies.get('password'):
                return render_template('login.html')

        return render_template('dashboard.html')

    # ---------------------------------------
    def run(self):
        """Starts the reporting module

        Makes the dashboard web app available via localhost:port, and exposes
        a REST API for trade information, open positions and market data.
        """

        global app

        # -----------------------------------
        # assign view
        app.add_url_rule('/', 'index', view_func=self.index)
        app.add_url_rule('/<path:start>', 'index', view_func=self.index)
        app.add_url_rule('/<start>/<path:end>', 'index', view_func=self.index)

        app.add_url_rule('/algos', 'algos', view_func=self.algos)
        app.add_url_rule('/symbols', 'symbols', view_func=self.symbols)

        app.add_url_rule('/positions', 'positions', view_func=self.positions)
        app.add_url_rule('/positions/<path:algo_id>', 'positions', view_func=self.positions)

        app.add_url_rule('/algo/<path:algo_id>', 'trades_by_algo', view_func=self.trades_by_algo)
        app.add_url_rule('/algo/<algo_id>/<path:start>',
                         'trades_by_algo', view_func=self.trades_by_algo)
        app.add_url_rule('/algo/<algo_id>/<start>/<path:end>',
                         'trades_by_algo', view_func=self.trades_by_algo)

        app.add_url_rule('/bars/<resolution>/<symbol>', 'bars', view_func=self.bars)
        app.add_url_rule('/bars/<resolution>/<symbol>/<path:start>', 'bars', view_func=self.bars)
        app.add_url_rule('/bars/<resolution>/<symbol>/<start>/<path:end>',
                         'bars', view_func=self.bars)

        app.add_url_rule('/trades', 'trades', view_func=self.trades)
        app.add_url_rule('/trades/<path:start>', 'trades', view_func=self.trades)
        app.add_url_rule('/trades/<start>/<path:end>', 'trades', view_func=self.trades)
        app.add_url_rule('/login/<password>', 'login', view_func=self.login)
        app.add_url_rule('/static/<path>', 'send_static', view_func=self.send_static)

        # let user know what the temp password is
        if not self.args['nopass'] and self._password != "":
            print(" * Web app password is:", self._password)

        # notice
        # print(" * Running on http://"+ str(self.host) +":"+str(self.port)+"/ (Press CTRL+C to quit)")

        # -----------------------------------
        # run flask app
        app.run(
            debug=True,
            host=str(self.host),
            port=int(self.port)
            )
Ejemplo n.º 4
0
class Reports():
    """Reports class initilizer

    :Optional:

        blotter : str
            Log trades to MySQL server used by this Blotter (default is "auto detect")
        port : int
            HTTP port to use (default: 5000)
        host : string
            Host to bind the http process to (defaults 0.0.0.0)
        password : string
            Password for logging in to the web app (auto-generated by default). Use "" for no password.
    """

    # ---------------------------------------
    def __init__(self, blotter=None, port=5000, host="0.0.0.0", password=None):
        # return
        self._password = password if password is not None else hashlib.sha1(
            str(datetime.datetime.now()).encode()).hexdigest()[:6]

        self.dbconn = None
        self.dbcurr = None

        self.host = args.host if args.host is not None else host
        self.port = args.port if args.port is not None else port

        self.blotter_name = args.blotter if args.blotter is not None else blotter
        self.load_blotter_args(self.blotter_name)

        self.blotter = Blotter(**self.blotter_args)

    # ---------------------------------------
    def load_blotter_args(self, name=None):
        if name is not None:
            self.blotter_name = name

        # find specific name
        if self.blotter_name is not None:  # and self.blotter_name != 'auto-detect':
            args_cache_file = tempfile.gettempdir(
            ) + "/" + self.blotter_name.lower() + ".ezq"
            if not os.path.exists(args_cache_file):
                print("[ERROR] Cannot connect to running Blotter [%s]" %
                      (self.blotter_name))
                sys.exit(0)

        # no name provided - connect to last running
        else:
            blotter_files = sorted(glob.glob(tempfile.gettempdir() + "/*.ezq"),
                                   key=os.path.getmtime)
            if len(blotter_files) == 0:
                print("[ERROR] Cannot connect to running Blotter [%s]" %
                      (self.blotter_name))
                sys.exit(0)

            args_cache_file = blotter_files[-1]

        args = pickle.load(open(args_cache_file, "rb"))
        args['as_client'] = True

        if args:
            # connect to mysql
            self.dbconn = pymysql.connect(host=str(args['dbhost']),
                                          port=int(args['dbport']),
                                          user=str(args['dbuser']),
                                          passwd=str(args['dbpass']),
                                          db=str(args['dbname']),
                                          autocommit=True)
            self.dbcurr = self.dbconn.cursor()

        self.blotter_args = args

    # ---------------------------------------
    def send_static(self, path):
        return send_from_directory('_webapp/', path)

    # ---------------------------------------
    def login(self, password):
        if self._password == password:
            resp = make_response('yes')
            resp.set_cookie('password', password)
            return resp
        else:
            return make_response("no")

    # ---------------------------------------
    def algos(self, json=True):

        algos = pd.read_sql("SELECT DISTINCT algo FROM trades",
                            self.dbconn).to_dict(orient="records")

        if json:
            return jsonify(algos)
        else:
            return algos

    # ---------------------------------------
    def symbols(self, json=True):

        symbols = pd.read_sql("SELECT * FROM symbols",
                              self.dbconn).to_dict(orient="records")

        if json:
            return jsonify(symbols)
        else:
            return symbols

    # ---------------------------------------
    def trades(self, start=None, end=None, algo_id=None, json=True):

        if algo_id is not None:
            algo_id = algo_id.replace('/', '')
        if start is not None:
            start = start.replace('/', '')
        if end is not None:
            end = end.replace('/', '')

        if start is None:
            start = tools.backdate("7D", date=None, as_datetime=False)

        trades_query = "SELECT * FROM trades WHERE exit_time IS NOT NULL"
        trades_where = []

        if isinstance(start, str):
            trades_where.append("entry_time>='" + start + "'")
        if isinstance(end, str):
            trades_where.append("exit_time<='" + end + "'")
        if algo_id is not None:
            trades_where.append("algo='" + algo_id + "'")

        if len(trades_where) > 0:
            trades_query += " AND " + " AND ".join(trades_where)

        trades = pd.read_sql(trades_query, self.dbconn)
        trades['exit_time'].fillna(0, inplace=True)

        trades['slippage'] = abs(trades['entry_price'] -
                                 trades['market_price'])

        trades['slippage'] = np.where(
            ((trades['direction'] == "LONG") &
             (trades['entry_price'] > trades['market_price'])) |
            ((trades['direction'] == "SHORT") &
             (trades['entry_price'] < trades['market_price'])),
            -trades['slippage'], trades['slippage'])

        trades = trades.sort_values(['exit_time', 'entry_time'],
                                    ascending=[False, False])

        trades = trades.to_dict(orient="records")
        if json:
            return jsonify(trades)
        else:
            return trades

    # ---------------------------------------
    def positions(self, algo_id=None, json=True):

        if algo_id is not None:
            algo_id = algo_id.replace('/', '')

        trades_query = "SELECT * FROM trades WHERE exit_time IS NULL"
        if algo_id is not None:
            trades_query += " AND algo='" + algo_id + "'"

        trades = pd.read_sql(trades_query, self.dbconn)

        last_query = "SELECT s.id, s.symbol, max(t.last) as last_price FROM ticks t LEFT JOIN symbols s ON (s.id=t.symbol_id) GROUP BY s.id"
        last_prices = pd.read_sql(last_query, self.dbconn)

        trades = trades.merge(last_prices, on=['symbol'])

        trades['unrealized_pnl'] = np.where(
            trades['direction'] == "SHORT",
            trades['entry_price'] - trades['last_price'],
            trades['last_price'] - trades['entry_price'])

        trades['slippage'] = abs(trades['entry_price'] -
                                 trades['market_price'])
        trades['slippage'] = np.where(
            ((trades['direction'] == "LONG") &
             (trades['entry_price'] > trades['market_price'])) |
            ((trades['direction'] == "SHORT") &
             (trades['entry_price'] < trades['market_price'])),
            -trades['slippage'], trades['slippage'])

        trades = trades.sort_values(['entry_time'], ascending=[False])

        trades = trades.to_dict(orient="records")
        if json:
            return jsonify(trades)
        else:
            return trades

    # ---------------------------------------
    def trades_by_algo(self, algo_id=None, start=None, end=None):
        trades = self.trades(start, end, algo_id=algo_id, json=False)
        return jsonify(trades)

    # ---------------------------------------
    def bars(self, resolution, symbol, start=None, end=None, json=True):

        if start is not None:
            start = start.replace('/', '')
        if end is not None:
            end = end.replace('/', '')

        if start is None:
            start = tools.backdate("7D", date=None, as_datetime=False)

        bars = self.blotter.history(symbols=symbol,
                                    start=start,
                                    end=end,
                                    resolution=resolution)

        bars['datetime'] = bars.index

        bars = bars.to_dict(orient="records")
        if json:
            return jsonify(bars)
        else:
            return bars

    # ---------------------------------------
    def index(self, start=None, end=None):
        if not args.nopass:
            if self._password != "" and self._password != request.cookies.get(
                    'password'):
                return render_template('login.html')

        return render_template('dashboard.html')

    # ---------------------------------------
    def run(self):
        """Starts the reporting module

        Makes the dashboard web app available via localhost:port, and exposes
        a REST API for trade information, open positions and market data.
        """

        global app

        # -----------------------------------
        # assign view
        app.add_url_rule('/', 'index', view_func=self.index)
        app.add_url_rule('/<path:start>', 'index', view_func=self.index)
        app.add_url_rule('/<start>/<path:end>', 'index', view_func=self.index)

        app.add_url_rule('/algos', 'algos', view_func=self.algos)
        app.add_url_rule('/symbols', 'symbols', view_func=self.symbols)

        app.add_url_rule('/positions', 'positions', view_func=self.positions)
        app.add_url_rule('/positions/<path:algo_id>',
                         'positions',
                         view_func=self.positions)

        app.add_url_rule('/algo/<path:algo_id>',
                         'trades_by_algo',
                         view_func=self.trades_by_algo)
        app.add_url_rule('/algo/<algo_id>/<path:start>',
                         'trades_by_algo',
                         view_func=self.trades_by_algo)
        app.add_url_rule('/algo/<algo_id>/<start>/<path:end>',
                         'trades_by_algo',
                         view_func=self.trades_by_algo)

        app.add_url_rule('/bars/<resolution>/<symbol>',
                         'bars',
                         view_func=self.bars)
        app.add_url_rule('/bars/<resolution>/<symbol>/<path:start>',
                         'bars',
                         view_func=self.bars)
        app.add_url_rule('/bars/<resolution>/<symbol>/<start>/<path:end>',
                         'bars',
                         view_func=self.bars)

        app.add_url_rule('/trades', 'trades', view_func=self.trades)
        app.add_url_rule('/trades/<path:start>',
                         'trades',
                         view_func=self.trades)
        app.add_url_rule('/trades/<start>/<path:end>',
                         'trades',
                         view_func=self.trades)
        app.add_url_rule('/login/<password>', 'login', view_func=self.login)
        app.add_url_rule('/static/<path>',
                         'send_static',
                         view_func=self.send_static)

        # let user know what the temp password is
        if not args.nopass and self._password != "":
            print(" * Web app password is:", self._password)

        # notice
        print(" * Running on http://" + str(self.host) + ":" + str(self.port) +
              "/ (Press CTRL+C to quit)")

        # -----------------------------------
        # run flask app
        app.run(
            # debug = True,
            host=str(self.host),
            port=int(self.port))
Ejemplo n.º 5
0
class Algo(Broker):
    """Algo class initilizer (sub-class of Broker)

    :Parameters:

        instruments : list
            List of IB contract tuples
        resolution : str
            Desired bar resolution (using pandas resolution: 1T, 1H, etc). Use K for tick bars.
        tick_window : int
            Length of tick lookback window to keep. Defaults to 1
        bar_window : int
            Length of bar lookback window to keep. Defaults to 100
        timezone : str
            Convert IB timestamps to this timezone (eg. US/Central). Defaults to UTC
        preload : str
            Preload history when starting algo (using pandas resolution: 1H, 1D, etc). Use K for tick bars.
        blotter : str
            Log trades to MySQL server used by this Blotter (default is "auto detect")

    """

    __metaclass__ = ABCMeta

    def __init__(self, instruments, resolution, \
        tick_window=1, bar_window=100, timezone="UTC", preload=None, \
        blotter=None, **kwargs):

        self.name = str(self.__class__).split('.')[-1].split("'")[0]

        # assign algo params
        self.bars = pd.DataFrame()
        self.ticks = pd.DataFrame()
        self.quotes = {}
        self.tick_count = 0
        self.tick_bar_count = 0
        self.bar_count = 0
        self.bar_hash = 0

        self.tick_window = tick_window if tick_window > 0 else 1
        self.bar_window = bar_window if bar_window > 0 else 100
        self.resolution = resolution.replace("MIN", "T")
        self.timezone = timezone
        self.preload = preload

        self.backtest = args.backtest
        self.backtest_start = args.start
        self.backtest_end = args.end

        # -----------------------------------
        self.sms_numbers = [] if args.sms is None else args.sms
        self.trade_log_dir = args.log
        self.blotter_name = args.blotter if args.blotter is not None else blotter
        self.record_output = args.output

        # -----------------------------------
        # load blotter settings && initilize Blotter
        self.load_blotter_args(args.blotter)
        self.blotter = Blotter(**self.blotter_args)

        # -----------------------------------
        # initiate broker/order manager
        super().__init__(instruments, ibclient=int(args.ibclient), \
            ibport=int(args.ibport), ibserver=str(args.ibserver))

        # -----------------------------------
        # signal collector
        self.signals = {}
        for sym in self.symbols:
            self.signals[sym] = pd.DataFrame()

        # -----------------------------------
        # initilize output file
        self.record_ts = None
        if self.record_output:
            self.datastore = tools.DataStore(args.output)

        # -----------------------------------
        # initiate strategy
        self.on_start()

    # ---------------------------------------
    def run(self):
        """Starts the algo

        Connects to the Blotter, processes market data and passes
        tick data to the ``on_tick`` function and bar data to the
        ``on_bar`` methods.
        """

        # -----------------------------------
        # backtest mode?
        if self.backtest:
            if self.output is None:
                print(
                    "ERROR: Must provide an output file for backtesting mode")
                sys.exit(0)
            if self.backtest_start is None:
                print("ERROR: Must provide start date for backtesting mode")
                sys.exit(0)
            if self.backtest_end is None:
                self.backtest_end = datetime.now().strftime(
                    '%Y-%m-%d %H:%M:%S.%f')

            # backtest history
            self.blotter.drip(symbols=self.symbols,
                              start=self.backtest_start,
                              end=self.backtest_end,
                              resolution=self.resolution,
                              tz=self.timezone,
                              quote_handler=self._quote_handler,
                              tick_handler=self._tick_handler,
                              bar_handler=self._bar_handler)

        # -----------------------------------
        # live data mode
        else:
            # preload history
            if self.preload is not None:
                try:  # dbskip may be active
                    self.bars = self.blotter.history(
                        symbols=self.symbols,
                        start=tools.backdate(self.preload),
                        resolution=self.resolution,
                        tz=self.timezone)
                except:
                    pass
                # print(self.bars)

            # add instruments to blotter in case they do not exist
            self.blotter.register(self.instruments)

            # listen for RT data
            self.blotter.listen(symbols=self.symbols,
                                tz=self.timezone,
                                quote_handler=self._quote_handler,
                                tick_handler=self._tick_handler,
                                bar_handler=self._bar_handler)

    # ---------------------------------------
    @abstractmethod
    def on_start(self):
        """
        Invoked once when algo starts. Used for when the strategy
        needs to initialize parameters upon starting.

        """
        # raise NotImplementedError("Should implement on_start()")
        pass

    # ---------------------------------------
    @abstractmethod
    def on_quote(self, instrument):
        """
        Invoked on every quote captured for the selected instrument.
        This is where you'll write your strategy logic for quote events.

        :Parameters:

            symbol : string
                `Instruments Object <#instrument-api>`_

        """
        # raise NotImplementedError("Should implement on_quote()")
        pass

    # ---------------------------------------
    @abstractmethod
    def on_tick(self, instrument):
        """
        Invoked on every tick captured for the selected instrument.
        This is where you'll write your strategy logic for tick events.

        :Parameters:

            symbol : string
                `Instruments Object <#instrument-api>`_

        """
        # raise NotImplementedError("Should implement on_tick()")
        pass

    # ---------------------------------------
    @abstractmethod
    def on_bar(self, instrument):
        """
        Invoked on every tick captured for the selected instrument.
        This is where you'll write your strategy logic for tick events.

        :Parameters:

            instrument : object
                `Instruments Object <#instrument-api>`_

        """
        # raise NotImplementedError("Should implement on_bar()")
        pass

    # ---------------------------------------
    @abstractmethod
    def on_fill(self, instrument, order):
        """
        Invoked on every order fill for the selected instrument.
        This is where you'll write your strategy logic for fill events.

        :Parameters:

            instrument : object
                `Instruments Object <#instrument-api>`_
            order : object
                Filled order data

        """
        # raise NotImplementedError("Should implement on_fill()")
        pass

    # ---------------------------------------
    def get_instrument(self, symbol):
        """
        A string subclass that provides easy access to misc
        symbol-related methods and information using shorthand.
        Refer to the `Instruments API <#instrument-api>`_
        for available methods and properties

        Call from within your strategy:
        ``instrument = self.get_instrument("SYMBOL")``

        :Parameters:

            symbol : string
                instrument symbol

        """
        instrument = Instrument(self._getsymbol_(symbol))
        instrument._set_parent(self)
        return instrument

    # ---------------------------------------
    def get_history(self, symbols, start, end=None, resolution="1T", tz="UTC"):
        """Get historical market data.
        Connects to Blotter and gets historical data from storage

        :Parameters:
            symbols : list
                List of symbols to fetch history for
            start : datetime / string
                History time period start date (datetime or YYYY-MM-DD[ HH:MM[:SS]] string)

        :Optional:
            end : datetime / string
                History time period end date (datetime or YYYY-MM-DD[ HH:MM[:SS]] string)
            resolution : string
                History resoluton (Pandas resample, defaults to 1T/1min)
            tz : string
                History timezone (defaults to UTC)

        :Returns:
            history : pd.DataFrame
                Pandas DataFrame object with historical data for all symbols
        """
        return self.blotter.history(symbols, start, end, resolution, tz)

    # ---------------------------------------
    # shortcuts to broker._create_order
    # ---------------------------------------
    def order(self, signal, symbol, quantity=0, **kwargs):
        """ Send an order for the selected instrument

        :Parameters:

            direction : string
                Order Type (BUY/SELL, EXIT/FLATTEN)
            symbol : string
                instrument symbol
            quantity : int
                Order quantiry

        :Optional:

            limit_price : float
                In case of a LIMIT order, this is the LIMIT PRICE
            expiry : int
                Cancel this order if not filled after *n* seconds (default 60 seconds)
            order_type : string
                Type of order: Market (default), LIMIT (default when limit_price is passed), MODIFY (required passing or orderId)
            orderId : int
                If modifying an order, the order id of the modified order
            target : float
                target (exit) price
            initial_stop : float
                price to set hard stop
            trail_stop_at : float
                price at which to start trailing the stop
            trail_stop_by : float
                % of trailing stop distance from current price
            ticksize : float
                If using traling stop, pass the tick size for the instruments so you won't try to buy ES at 2200.128230 :)
            fillorkill: bool
                fill entire quantiry or none at all
            iceberg: bool
                is this an iceberg (hidden) order
        """
        if signal.upper() == "EXIT" or signal.upper() == "FLATTEN":
            position = self.get_positions(symbol)
            if position['position'] == 0:
                return

            kwargs['symbol'] = symbol
            kwargs['quantity'] = abs(position['position'])
            kwargs['direction'] = "BUY" if position['position'] < 0 else "SELL"

            # print("EXIT", kwargs)

            try:
                self.record(position=0)
            except:
                pass

            if not self.backtest:
                self._create_order(**kwargs)

        else:
            if quantity == 0:
                return

            kwargs['symbol'] = symbol
            kwargs['quantity'] = abs(quantity)
            kwargs['direction'] = signal.upper()

            # print(signal.upper(), kwargs)

            # record
            try:
                quantity = -quantity if kwargs[
                    'direction'] == "BUY" else quantity
                self.record(position=quantity)
            except:
                pass

            if not self.backtest:
                self._create_order(**kwargs)

    # ---------------------------------------
    def cancel_order(self, orderId):
        """ Cancels a un-filled order

        Parameters:
            orderId : int
                Order ID
        """
        self._cancel_order(orderId)

    # ---------------------------------------
    def record(self, *args, **kwargs):
        """Records data for later analysis.
        Values will be logged to the file specified via
        ``--output [file]`` (along with bar data) as
        csv/pickle/h5 file.

        Call from within your strategy:
        ``self.record(key=value)``

        :Parameters:
            ** kwargs : mixed
                The names and values to record

        """
        if self.record_output:
            self.datastore.record(self.record_ts, *args, **kwargs)

    # ---------------------------------------
    def sms(self, text):
        """Sends an SMS message.
        Relies on properly setting up an SMS provider (refer to the
        SMS section of the documentation for more information about this)

        Call from within your strategy:
        ``self.sms("message text")``

        :Parameters:
            text : string
                The body of the SMS message to send

        """
        logging.info("SMS: " + str(text))
        sms.send_text(self.name + ': ' + str(text), self.sms_numbers)

    # ---------------------------------------
    def _caller(self, caller):
        stack = [x[3] for x in inspect.stack()][1:-1]
        return caller in stack

    # ---------------------------------------
    def _quote_handler(self, quote):
        # self._cancel_expired_pending_orders()
        self.quotes[quote['symbol']] = quote
        self.on_quote(self.get_instrument(quote))

    # ---------------------------------------
    def _tick_handler(self, tick):
        self._cancel_expired_pending_orders()

        if "K" not in self.resolution:
            self.ticks = self._update_window(self.ticks,
                                             tick,
                                             window=self.tick_window)
        else:
            self.ticks = self._update_window(self.ticks, tick)
            bar = tools.resample(self.ticks, self.resolution)
            if len(bar) > self.tick_bar_count > 0:
                self.record_ts = tick.index[0]
                self._bar_handler(bar)

                periods = int("".join(
                    [s for s in self.resolution if s.isdigit()]))
                self.ticks = self.ticks[-periods:]

            self.tick_bar_count = len(bar)

            # record tick bar
            self.record(bar)

        self.on_tick(self.get_instrument(tick))

    # ---------------------------------------
    def _bar_handler(self, bar):

        is_tick_bar = False
        handle_bar = True

        if "K" in self.resolution:
            if self._caller("_tick_handler"):
                is_tick_bar = True
                handle_bar = True
            else:
                is_tick_bar = True
                handle_bar = False

        if is_tick_bar:
            # just add a bar (used by tick bar bandler)
            self.bars = self._update_window(self.bars,
                                            bar,
                                            window=self.bar_window)
        else:
            # add the bar and resample to resolution
            self.bars = self._update_window(self.bars,
                                            bar,
                                            window=self.bar_window,
                                            resolution=self.resolution)

        # new bar?
        this_bar_hash = abs(hash(str(self.bars.index.values[-1]))) % (10**8)
        newbar = (self.bar_hash != this_bar_hash)
        self.bar_hash = this_bar_hash

        if newbar & handle_bar:
            self.record_ts = bar.index[0]
            self.on_bar(self.get_instrument(bar))

            if "K" not in self.resolution:
                self.record(bar)

    # ---------------------------------------
    def _update_window(self, df, data, window=None, resolution=None):
        if df is None:
            df = data
        else:
            df = df.append(data)

        if resolution is not None:
            try:
                tz = str(df.index.tz)
            except:
                tz = None
            df = tools.resample(df, resolution=resolution, tz=tz)

        if window is None:
            return df

        return df[-window:]

    # ---------------------------------------
    # signal logging methods
    # ---------------------------------------
    def _add_signal_history(self, df, symbol):
        """ Initilize signal history """
        if symbol not in self.signals.keys() or len(self.signals[symbol]) == 0:
            self.signals[symbol] = [nan] * len(df)
        else:
            self.signals[symbol].append(nan)

        self.signals[symbol] = self.signals[symbol][-len(df):]
        df.loc[-len(self.signals[symbol]):, 'signal'] = self.signals[symbol]

        return df

    def _log_signal(self, symbol, signal):
        """ Log signal

        :Parameters:
            symbol : string
                instruments symbol
            signal : integer
                signal identifier (1, 0, -1)

        """
        self.signals[symbol][-1] = signal