diff --git a/README.md b/README.md index df40df00..45cc834c 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,29 @@ -# Quantopian Backtesting Services +# qbt - Quantopian Backtesting Services + +qbt uses tornado to accept synchronous requests for backtesting sessions. +The client of a backtesting session first invokes the _backtest_ endpoint: +http://serverip/backtest?startdate=<>&enddate=<>... + +qbt will respond with a json object describing the session: +- backtest id, to be referenced in all further requests +- zeromq connection information for the event stream + +A backtesting session is comprised of: +- REST endpoint to request orders +- an event stream delivered via zeromq + +## Pre-requisites + +### MongoDB Server ### +QBT requires a running mongodb instance with a few collections: + +- user collection. See handlers.BaseHandler for code using this collection. Documents must have: + - email - standard email address + - encrypted_password - an sha2 hex digest of the password + - salt - a secret other than the password, which can be set in a secure cookie to maintain a session. if unset, will be set to sha2 hex of datetime.utcnow()--password + - _id - standard issue mongodb primary key + + + ## Requesting a Backtest diff --git a/qbt_server.py b/qbt_server.py new file mode 100644 index 00000000..ed8e39a9 --- /dev/null +++ b/qbt_server.py @@ -0,0 +1,109 @@ +import tornado.auth +import tornado.httpserver +import tornado.ioloop +from tornado.options import define, options +import tornado.web +import pymongo +import bson +import hashlib +import base64 +import uuid +import os +import logging + +logging.getLogger('qbt').setLevel(logging.DEBUG) +logger = logging.getLogger('qbt') + +define("port", default=8888, help="run the qbt on the given port", type=int) +define("mongodb_host", default="127.0.0.1", help="mongodb host address") +define("mongodb_port", default=27017, help="connect to the mongodb on the given port", type=int) +define("mongodb_dbname", default="blog", help="database name") +define("mongodb_user", default="blog", help="database user") +define("mongodb_password", default="blog", help="database password") + +HASH_ALGO = 'sha256' + +class Application(tornado.web.Application): + def __init__(self): + handlers = [ + (r"/", MainHandler), + (r"/login", LoginHandler), + ] + settings = dict( + template_path=os.path.join(os.path.dirname(__file__), "templates"), + static_path=os.path.join(os.path.dirname(__file__), "static"), + xsrf_cookies=False, + cookie_secret=base64.b64encode(uuid.uuid4().bytes + uuid.uuid4().bytes), + login_url="/login", + #autoescape=None, + debug=True, + ) + tornado.web.Application.__init__(self, handlers, **settings) + + # Have one global connection to the blog DB across all handlers + self.connection = pymongo.Connection(options.mongodb_host, options.mongodb_port) + self.db = self.connection[options.mongodb_dbname] + self.db.authenticate(options.mongodb_user, options.mongodb_password) + + +class BaseHandler(tornado.web.RequestHandler): + @property + def db(self): + return self.application.db + + def get_current_user(self): + user_id = self.get_secure_cookie(u"user_id") + logger.info("looking up user with id: {id}".format(id=user_id)) + if not user_id: return None + #get user record by id + users = self.db.users.find(spec={"_id":bson.ObjectId(user_id)}, limit=1) + if(users.count > 0): + return users[0] + return None + +class MainHandler(BaseHandler): + @tornado.web.authenticated + def get(self): + self.write("Hello, world") + +class LoginHandler(BaseHandler): + def get(self): + self.write('
' + 'Name: ' + 'pass: ' + '' + '
') + + def post(self): + self.authenticate(self.get_argument("user_name"),self.get_argument("password")) + self.redirect("/") + + def authenticate(self, username, password): + h = hashlib.new(HASH_ALGO) + #find user record by username. + users = self.db.users.find(spec={"email":username}, limit=1) + if(users.count > 0): + user_record = users[0] + else: + logger.debug("no user with name: {username}", username=username) + return + + + #calculate password hash + h.update(user_record['salt']+"--"+password) + encrypted_password = h.hexdigest() + logger.debug("checking {password} as {encrypted_password} against {record}".format(password=password, encrypted_password=encrypted_password, record=user_record['encrypted_password'])) + if (user_record['encrypted_password'] == encrypted_password): + #we have a match, so set the secure cookie to the salt + logger.debug("setting user_id cookie to {id}".format(id=user_record['_id'])) + self.set_secure_cookie(u"user_id", unicode(user_record['_id'])) + +def main(): + tornado.options.parse_command_line() + http_server = tornado.httpserver.HTTPServer(Application()) + http_server.listen(options.port) + tornado.ioloop.IOLoop.instance().start() + + +if __name__ == "__main__": + main()