#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
# QTPyLib: Quantitative Trading Python Library
# https://github.com/ranaroussi/qtpylib
#
# Copyright 2016-2018 Ran Aroussi
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
import argparse
import datetime
import hashlib
import logging
import sys
from time import time as timetime
from flask import Flask, jsonify, request, make_response, send_from_directory
from flask import render_template
from flask.json import JSONEncoder
from dateutil.parser import parse as parse_date
import numpy as np
import pandas as pd
import pymysql
from qtpylib import path, tools
from qtpylib.blotter import (
Blotter, load_blotter_args
)
# =============================================
# check min, python version
if sys.version_info < (3, 4):
raise SystemError("QTPyLib requires Python version >= 3.4")
# =============================================
class datetimeJSONEncoder(JSONEncoder):
def default(self, obj):
if isinstance(obj, datetime.datetime) | \
isinstance(obj, datetime.date) | \
isinstance(obj, datetime.time):
return int(timetime())
return JSONEncoder.default(self, obj)
app = Flask(__name__, template_folder=path['library'] + "/_webapp")
app.json_encoder = datetimeJSONEncoder
@app.template_filter('strftime')
def _jinja2_strftime(date, fmt='%Y-%m-%d %H:%M:%S%z'):
try:
date = parse_date(date)
except Exception as e:
pass
native = date.replace(tzinfo=None)
return native.strftime(fmt)
# =============================================
tools.createLogger(__name__)
# =============================================
[docs]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, **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, _ = 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, url_path):
return send_from_directory('_webapp/', url_path)
# ---------------------------------------
def login(self, password):
if self._password == password:
resp = make_response('yes')
resp.set_cookie('password', password)
return resp
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)
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)
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 not trades_where:
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)
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)
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)
return bars
# ---------------------------------------
def index(self, start=None, end=None):
if 'nopass' not in self.args:
if self._password != "" and self._password != request.cookies.get('password'):
return render_template('login.html')
return render_template('dashboard.html')
# ---------------------------------------
[docs] 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.
"""
# -----------------------------------
# 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/<url_path>', 'send_static',
view_func=self.send_static)
# let user know what the temp password is
if 'nopass' not in self.args and self._password != "":
print(" * Web app password is:", self._password)
# notice
# print(" * Running on http://"+ str(self.host) +":"+str(self.port)+"/ (CTRL+C to quit)")
# -----------------------------------
# run flask app
app.run(
debug=True,
host=str(self.host),
port=int(self.port)
)