diff --git a/catalyst/__main__.py b/catalyst/__main__.py index 52920623..874cf4aa 100644 --- a/catalyst/__main__.py +++ b/catalyst/__main__.py @@ -506,6 +506,197 @@ def live(ctx, return perf +@main.command(name='serve') +@click.option( + '-f', + '--algofile', + default=None, + type=click.File('r'), + help='The file that contains the algorithm to run.', +) +@click.option( + '-t', + '--algotext', + help='The algorithm script to run.', +) +@click.option( + '-D', + '--define', + multiple=True, + help="Define a name to be bound in the namespace before executing" + " the algotext. For example '-Dname=value'. The value may be" + " any python expression. These are evaluated in order so they" + " may refer to previously defined names.", +) +@click.option( + '--data-frequency', + type=click.Choice({'daily', 'minute'}), + default='daily', + show_default=True, + help='The data frequency of the simulation.', +) +@click.option( + '--capital-base', + type=float, + show_default=True, + help='The starting capital for the simulation.', +) +@click.option( + '-b', + '--bundle', + default='poloniex', + metavar='BUNDLE-NAME', + show_default=True, + help='The data bundle to use for the simulation.', +) +@click.option( + '--bundle-timestamp', + type=Timestamp(), + default=pd.Timestamp.utcnow(), + show_default=False, + help='The date to lookup data on or before.\n' + '[default: ]' +) +@click.option( + '-s', + '--start', + type=Date(tz='utc', as_timestamp=True), + help='The start date of the simulation.', +) +@click.option( + '-e', + '--end', + type=Date(tz='utc', as_timestamp=True), + help='The end date of the simulation.', +) +@click.option( + '-o', + '--output', + default='-', + metavar='FILENAME', + show_default=True, + help="The location to write the perf data. If this is '-' the perf" + " will be written to stdout.", +) +@click.option( + '--print-algo/--no-print-algo', + is_flag=True, + default=False, + help='Print the algorithm to stdout.', +) +@ipython_only(click.option( + '--local-namespace/--no-local-namespace', + is_flag=True, + default=None, + help='Should the algorithm methods be resolved in the local namespace.' +)) +@click.option( + '-x', + '--exchange-name', + help='The name of the targeted exchange.', +) +@click.option( + '-n', + '--algo-namespace', + help='A label assigned to the algorithm for data storage purposes.' +) +@click.option( + '-c', + '--base-currency', + help='The base currency used to calculate statistics ' + '(e.g. usd, btc, eth).', +) +@click.pass_context +def run(ctx, + algofile, + algotext, + define, + data_frequency, + capital_base, + bundle, + bundle_timestamp, + start, + end, + output, + print_algo, + local_namespace, + exchange_name, + algo_namespace, + base_currency): + """Run a backtest for the given algorithm on the server. + """ + + if (algotext is not None) == (algofile is not None): + ctx.fail( + "must specify exactly one of '-f' / '--algofile' or" + " '-t' / '--algotext'", + ) + + # check that the start and end dates are passed correctly + if start is None and end is None: + # check both at the same time to avoid the case where a user + # does not pass either of these and then passes the first only + # to be told they need to pass the second argument also + ctx.fail( + "must specify dates with '-s' / '--start' and '-e' / '--end'" + " in backtest mode", + ) + if start is None: + ctx.fail("must specify a start date with '-s' / '--start'" + " in backtest mode") + if end is None: + ctx.fail("must specify an end date with '-e' / '--end'" + " in backtest mode") + + if exchange_name is None: + ctx.fail("must specify an exchange name '-x'") + + if base_currency is None: + ctx.fail("must specify a base currency with '-c' in backtest mode") + + if capital_base is None: + ctx.fail("must specify a capital base with '--capital-base'") + + click.echo('Running in backtesting mode.', sys.stdout) + + perf = run_server( + initialize=None, + handle_data=None, + before_trading_start=None, + analyze=None, + algofile=algofile, + algotext=algotext, + defines=define, + data_frequency=data_frequency, + capital_base=capital_base, + data=None, + bundle=bundle, + bundle_timestamp=bundle_timestamp, + start=start, + end=end, + output=output, + print_algo=print_algo, + local_namespace=local_namespace, + environ=os.environ, + live=False, + exchange=exchange_name, + algo_namespace=algo_namespace, + base_currency=base_currency, + analyze_live=None, + live_graph=False, + simulate_orders=True, + auth_aliases=None, + stats_output=None, + ) + + if output == '-': + click.echo(str(perf), sys.stdout) + elif output != os.devnull: # make the catalyst magic not write any data + perf.to_pickle(output) + + return perf + + @main.command(name='serve-live') @click.option( '-f', @@ -642,20 +833,33 @@ def serve_live(ctx, click.echo('Running in live trading mode.', sys.stdout) perf = run_server( - algofile=algofile, + initialize=None, + handle_data=None, + before_trading_start=None, + analyze=None, + algofile=algofile, algotext=algotext, - define=define, + defines=define, + data_frequency=None, capital_base=capital_base, + data=None, + bundle=None, + bundle_timestamp=None, + start=None, end=end, output=output, print_algo=print_algo, - live=False, + local_namespace=local_namespace, + environ=os.environ, + live=True, exchange=exchange_name, algo_namespace=algo_namespace, base_currency=base_currency, live_graph=live_graph, + analyze_live=None, simulate_orders=simulate_orders, auth_aliases=auth_aliases, + stats_output=None, ) if output == '-': diff --git a/catalyst/utils/run_server.py b/catalyst/utils/run_server.py index ff4d949d..413128e8 100644 --- a/catalyst/utils/run_server.py +++ b/catalyst/utils/run_server.py @@ -1,40 +1,181 @@ -import requests +#!flask/bin/python import base64 +import requests +import pandas as pd +import json +from flask import ( + Flask, jsonify, abort, + make_response, request, + url_for, + ) + +from catalyst.utils.run_algo import _run + + +def convert_date(date): + if isinstance(date, pd.Timestamp): + return date.__str__() + def run_server( + initialize, + handle_data, + before_trading_start, + analyze, algofile, algotext, - define, + defines, + data_frequency, capital_base, + data, + bundle, + bundle_timestamp, + start, end, output, print_algo, + local_namespace, + environ, live, exchange, algo_namespace, base_currency, live_graph, + analyze_live, simulate_orders, auth_aliases, - ): + stats_output, + ): + + if algotext: + algotext = base64.b64encode(algotext) + else: + algotext = base64.b64encode(algofile.read()) + algofile = None json_file = {'arguments': { - '--algofile': base64.b64encode(algofile.read()), - '--algotext': algotext, - '--define': define, - '--capital-base': capital_base, - '--end': end, - '--output': output, - 'print-algo': print_algo, - 'live': True, - '--exchange': exchange, - '--algo-namespace': algo_namespace, - '--base-currency': base_currency, - 'live-graph': live_graph, - 'simulate-orders': simulate_orders, - '--auth-aliases': auth_aliases, + 'initialize': initialize, + 'handle_data': handle_data, + 'before_trading_start': before_trading_start, + 'analyze': analyze, + 'algotext': algotext, + 'defines': defines, + 'data_frequency': data_frequency, + 'capital_base': capital_base, + 'data': data, + 'bundle': bundle, + 'bundle_timestamp': bundle_timestamp, + 'start': start, + 'end': end, + 'local_namespace': local_namespace, + 'environ': None, + 'analyze_live': analyze_live, + 'stats_output': stats_output, + 'algofile': algofile, + 'output': output, + 'print_algo': print_algo, + 'live': live, + 'exchange': exchange, + 'algo_namespace': algo_namespace, + 'base_currency': base_currency, + 'live_graph': live_graph, + 'simulate_orders': simulate_orders, + 'auth_aliases': auth_aliases, }} - url = 'http://127.0.0.1:5000/todo/api/v1.0/tasks' - response = requests.post(url, json=json_file) + url = 'http://127.0.0.1:5000/api/v1.0/tasks' + response = requests.post(url, json=json.dumps( + json_file, default=convert_date)) + + +app = Flask(__name__) + + +def exec_catalyst(arguments): + arguments['algotext'] = base64.b64decode(arguments['algotext']) + if arguments['start'] is not None: + arguments['start'] = pd.Timestamp(arguments['start']) + if arguments['end'] is not None: + arguments['end'] = pd.Timestamp(arguments['end']) + _run(**arguments) + + +def make_public_task(task): + new_task = {} + for field in task: + if field == 'id': + new_task['uri'] = url_for('get_task', task_id=task['id'], _external=True) + else: + new_task[field] = task[field] + return new_task + + +# @app.route('/') +# def index(): +# return "Hello, World!" + + +# @app.route('/api/v1.0/tasks/', methods=['GET']) +# def get_task(task_id): +# task = [task for task in tasks if task['id'] == task_id] +# if len(task) == 0: +# abort(404) +# return jsonify({'task': make_public_task(task[0])}) +# +# +# @app.route('/api/v1.0/tasks', methods=['GET']) +# def get_tasks(): +# return jsonify({'tasks': [make_public_task(task) for task in tasks]}) + + +@app.errorhandler(404) +def not_found(error): + return make_response(jsonify({'error': 'Not found'}), 400) + + +@app.route('/api/v1.0/tasks', methods=['POST']) +def create_task(): + if not request.json or 'arguments' not in request.json: + abort(400) + arguments = json.loads(request.json)['arguments'] + exec_catalyst(arguments) + # task = { + # 'id': tasks[-1]['id'] + 1, + # 'arguments': request.json['arguments'], + # 'done': False + # } + # tasks.append(task) + # + # return jsonify({'task': [make_public_task(task)]}), 201 + return jsonify({"success": "man"}), 201 + +# @app.route('/api/v1.0/tasks/', methods=['PUT']) +# def update_task(task_id): +# task = [task for task in tasks if task['id'] == task_id] +# if len(task) == 0: +# abort(404) +# if not request.json: +# abort(400) +# if 'arguments' in request.json and type(request.json['arguments']) != unicode: +# abort(400) +# if 'description' in request.json and type(request.json['description']) is not unicode: +# abort(400) +# if 'done' in request.json and type(request.json['done']) is not bool: +# abort(400) +# task[0]['arguments'] = request.json.get('arguments', task[0]['arguments']) +# task[0]['done'] = request.json.get('done', task[0]['done']) +# return jsonify({'task': [make_public_task(task[0])]}) +# +# +# @app.route('/api/v1.0/tasks/', methods=['DELETE']) +# def delete_task(task_id): +# task = [task for task in tasks if task['id'] == task_id] +# if len(task) == 0: +# abort(404) +# tasks.remove(task[0]) +# return jsonify({'result': True}) + + +if __name__ == '__main__': + app.run(debug=True, threaded=True)