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))
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
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) )
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))
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