mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-22 12:40:30 +08:00
Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
793b6e92d8 | ||
|
|
c0b9939580 | ||
|
|
e248831719 | ||
|
|
25f1f6e641 | ||
|
|
d2f9762fbf | ||
|
|
7a89cfc02b | ||
|
|
64e22ba27d | ||
|
|
8c58916bb1 | ||
|
|
18cdd51680 | ||
|
|
db7b7639a0 | ||
|
|
1ad26b0c39 | ||
|
|
e1abecb556 | ||
|
|
6e642f45d8 | ||
|
|
9f9bfc9df0 | ||
|
|
866b92910f | ||
|
|
e071f6ec8d | ||
|
|
3eff58fcf9 | ||
|
|
540358d0f9 | ||
|
|
0e3be98d24 | ||
|
|
c39e075766 | ||
|
|
54935dd6f6 | ||
|
|
093660ab1a | ||
|
|
fb65839032 | ||
|
|
461a5942fb | ||
|
|
eee2e1be88 | ||
|
|
2d43955abf | ||
|
|
c5bed6e8c4 | ||
|
|
2320a1432a | ||
|
|
5d74cd6f89 | ||
|
|
273408e9ed | ||
|
|
5ccddf6d21 | ||
|
|
d97890fc4e | ||
|
|
4c1a9b1dd7 | ||
|
|
2168fb0d5b | ||
|
|
3bc54a6c2e | ||
|
|
444fcbb2b3 | ||
|
|
881e3e5953 | ||
|
|
5889f4c74b | ||
|
|
ee93b16558 | ||
|
|
e94c9db2ec | ||
|
|
9f88e7a003 | ||
|
|
b9bedfda21 | ||
|
|
d1529afeb3 | ||
|
|
99dece953f | ||
|
|
9ef000e529 | ||
|
|
db4ee4c01e | ||
|
|
0decdf5aab | ||
|
|
e460290056 | ||
|
|
b9130fd968 | ||
|
|
37980f3580 | ||
|
|
56bd3db7a2 | ||
|
|
18123d7f7e | ||
|
|
a04a99373d | ||
|
|
e906315969 | ||
|
|
a360a5fe3a | ||
|
|
fcbdc131ec | ||
|
|
857a5d8a91 | ||
|
|
e914481325 | ||
|
|
c0ba8b2ebb | ||
|
|
69507d1b00 | ||
|
|
311e357451 | ||
|
|
d1cd95c492 | ||
|
|
0f55f0e9a6 | ||
|
|
c26d78cf05 | ||
|
|
c3662443e4 | ||
|
|
18c96303a6 | ||
|
|
5c236a65f7 | ||
|
|
7f021acb2e | ||
|
|
68faad4098 | ||
|
|
ff989ba524 | ||
|
|
fa60457a0f | ||
|
|
43737a9730 | ||
|
|
e6fb708c1c | ||
|
|
8ff8e6458e | ||
|
|
7d24433a42 | ||
|
|
7ae23e2340 | ||
|
|
940f625ec1 | ||
|
|
2ec9aa2ca9 | ||
|
|
c6dfd502d2 | ||
|
|
0aa8c91577 | ||
|
|
c09f53449a | ||
|
|
f34a66e9c6 | ||
|
|
04fed4140c | ||
|
|
76e183b5f7 | ||
|
|
911fb6e934 | ||
|
|
e8f98825e0 | ||
|
|
5619f6f451 | ||
|
|
8223d06d98 | ||
|
|
b47884a504 | ||
|
|
afa2af3014 | ||
|
|
55521b87b9 | ||
|
|
45978ab193 | ||
|
|
575ac4a36d | ||
|
|
db07ac0abb | ||
|
|
a754497d65 | ||
|
|
d669419d18 | ||
|
|
7d3c53dbef | ||
|
|
04a1513e73 | ||
|
|
03d2c0e306 | ||
|
|
853707dfb2 | ||
|
|
540dd97dbf | ||
|
|
9f372828b4 | ||
|
|
6a929b6e25 | ||
|
|
45f278ab15 | ||
|
|
a58fa21234 | ||
|
|
c8d6e07179 | ||
|
|
fdfc3f2ec3 | ||
|
|
3a321eb195 | ||
|
|
d21eb3b946 | ||
|
|
439b5404ae | ||
|
|
821f60897f | ||
|
|
5ff935723f | ||
|
|
51126fd7ae | ||
|
|
fec6a159e6 | ||
|
|
eee9a07f54 | ||
|
|
563fc433d5 | ||
|
|
53a54fde7c | ||
|
|
b0f2202b54 | ||
|
|
cf6c3bb76b | ||
|
|
772640e098 | ||
|
|
52d4ced37c | ||
|
|
3ed44f72ad | ||
|
|
7569f7eb7c | ||
|
|
6d28e289c4 | ||
|
|
22154f2337 | ||
|
|
df4dd0f5a4 | ||
|
|
5314d3e1f8 | ||
|
|
bb16975400 | ||
|
|
0ce624e6f6 | ||
|
|
50dc322230 | ||
|
|
fb36435231 | ||
|
|
e688783931 | ||
|
|
2f3dbeedcd | ||
|
|
cebb1cd6d4 | ||
|
|
020ec50258 | ||
|
|
df42cc9047 | ||
|
|
8a9b4e2df7 | ||
|
|
b9150aab79 | ||
|
|
d4efed0d3a | ||
|
|
db1ad9aac8 | ||
|
|
270c261203 | ||
|
|
3f974b1adf | ||
|
|
9eac88344d | ||
|
|
5d1f00bd19 | ||
|
|
8d3f3ba81d | ||
|
|
248299a725 | ||
|
|
e916283522 | ||
|
|
b7a32656d5 | ||
|
|
78eb5d9d64 | ||
|
|
14c7170159 | ||
|
|
415fceb11c | ||
|
|
05c8957c90 | ||
|
|
e4bacb169e | ||
|
|
608dd843d6 | ||
|
|
70b9fd8e03 | ||
|
|
21ad753fcf | ||
|
|
88aa7154db | ||
|
|
e0827fe4ad | ||
|
|
b1d63c9a5b | ||
|
|
3ea00e783c | ||
|
|
9361570895 | ||
|
|
50ded59c3c | ||
|
|
050fda1bdb | ||
|
|
713d487808 | ||
|
|
354e422be8 | ||
|
|
1e4af12e3d | ||
|
|
e0c8178932 | ||
|
|
93ebbf8b1f | ||
|
|
22a850ab14 | ||
|
|
aab501aa17 | ||
|
|
94d5b4a4d5 | ||
|
|
96b36c6614 |
@@ -40,6 +40,7 @@ develop-eggs
|
|||||||
coverage.xml
|
coverage.xml
|
||||||
htmlcov
|
htmlcov
|
||||||
nosetests.xml
|
nosetests.xml
|
||||||
|
.python-version
|
||||||
|
|
||||||
# C Extensions
|
# C Extensions
|
||||||
*.o
|
*.o
|
||||||
|
|||||||
+1
-1
@@ -1,4 +1,4 @@
|
|||||||
.. image:: https://s3.amazonaws.com/enigmaco-docs/enigma-catalyst.jpg
|
.. image:: https://s3.amazonaws.com/enigmaco-docs/enigma-catalyst.png
|
||||||
:target: https://enigmampc.github.io/catalyst
|
:target: https://enigmampc.github.io/catalyst
|
||||||
:align: center
|
:align: center
|
||||||
:alt: Enigma | Catalyst
|
:alt: Enigma | Catalyst
|
||||||
|
|||||||
+160
-12
@@ -3,8 +3,10 @@ import os
|
|||||||
from functools import wraps
|
from functools import wraps
|
||||||
|
|
||||||
import click
|
import click
|
||||||
|
import sys
|
||||||
import logbook
|
import logbook
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from catalyst.marketplace.marketplace import Marketplace
|
||||||
from six import text_type
|
from six import text_type
|
||||||
|
|
||||||
from catalyst.data import bundles as bundles_module
|
from catalyst.data import bundles as bundles_module
|
||||||
@@ -257,7 +259,7 @@ def run(ctx,
|
|||||||
if capital_base is None:
|
if capital_base is None:
|
||||||
ctx.fail("must specify a capital base with '--capital-base'")
|
ctx.fail("must specify a capital base with '--capital-base'")
|
||||||
|
|
||||||
click.echo('Running in backtesting mode.')
|
click.echo('Running in backtesting mode.', sys.stdout)
|
||||||
|
|
||||||
perf = _run(
|
perf = _run(
|
||||||
initialize=None,
|
initialize=None,
|
||||||
@@ -285,11 +287,12 @@ def run(ctx,
|
|||||||
analyze_live=None,
|
analyze_live=None,
|
||||||
live_graph=False,
|
live_graph=False,
|
||||||
simulate_orders=True,
|
simulate_orders=True,
|
||||||
|
auth_aliases=None,
|
||||||
stats_output=None,
|
stats_output=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if output == '-':
|
if output == '-':
|
||||||
click.echo(str(perf))
|
click.echo(str(perf), sys.stdout)
|
||||||
elif output != os.devnull: # make the catalyst magic not write any data
|
elif output != os.devnull: # make the catalyst magic not write any data
|
||||||
perf.to_pickle(output)
|
perf.to_pickle(output)
|
||||||
|
|
||||||
@@ -394,6 +397,12 @@ def catalyst_magic(line, cell=None):
|
|||||||
help='The base currency used to calculate statistics '
|
help='The base currency used to calculate statistics '
|
||||||
'(e.g. usd, btc, eth).',
|
'(e.g. usd, btc, eth).',
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
'-e',
|
||||||
|
'--end',
|
||||||
|
type=Date(tz='utc', as_timestamp=True),
|
||||||
|
help='An optional end date at which to stop the execution.',
|
||||||
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'--live-graph/--no-live-graph',
|
'--live-graph/--no-live-graph',
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
@@ -407,6 +416,15 @@ def catalyst_magic(line, cell=None):
|
|||||||
help='Simulating orders enable the paper trading mode. No orders will be '
|
help='Simulating orders enable the paper trading mode. No orders will be '
|
||||||
'sent to the exchange unless set to false.',
|
'sent to the exchange unless set to false.',
|
||||||
)
|
)
|
||||||
|
@click.option(
|
||||||
|
'--auth-aliases',
|
||||||
|
default=None,
|
||||||
|
help='Authentication file aliases for the specified exchanges. By default,'
|
||||||
|
'each exchange uses the "auth.json" file in the exchange folder. '
|
||||||
|
'Specifying an "auth2" alias would use "auth2.json". It should be '
|
||||||
|
'specified like this: "[exchange_name],[alias],..." For example, '
|
||||||
|
'"binance,auth2" or "binance,auth2,bittrex,auth2".',
|
||||||
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def live(ctx,
|
def live(ctx,
|
||||||
algofile,
|
algofile,
|
||||||
@@ -419,7 +437,9 @@ def live(ctx,
|
|||||||
exchange_name,
|
exchange_name,
|
||||||
algo_namespace,
|
algo_namespace,
|
||||||
base_currency,
|
base_currency,
|
||||||
|
end,
|
||||||
live_graph,
|
live_graph,
|
||||||
|
auth_aliases,
|
||||||
simulate_orders):
|
simulate_orders):
|
||||||
"""Trade live with the given algorithm.
|
"""Trade live with the given algorithm.
|
||||||
"""
|
"""
|
||||||
@@ -442,10 +462,10 @@ def live(ctx,
|
|||||||
ctx.fail("must specify a capital base with '--capital-base'")
|
ctx.fail("must specify a capital base with '--capital-base'")
|
||||||
|
|
||||||
if simulate_orders:
|
if simulate_orders:
|
||||||
click.echo('Running in paper trading mode.')
|
click.echo('Running in paper trading mode.', sys.stdout)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
click.echo('Running in live trading mode.')
|
click.echo('Running in live trading mode.', sys.stdout)
|
||||||
|
|
||||||
perf = _run(
|
perf = _run(
|
||||||
initialize=None,
|
initialize=None,
|
||||||
@@ -461,7 +481,7 @@ def live(ctx,
|
|||||||
bundle=None,
|
bundle=None,
|
||||||
bundle_timestamp=None,
|
bundle_timestamp=None,
|
||||||
start=None,
|
start=None,
|
||||||
end=None,
|
end=end,
|
||||||
output=output,
|
output=output,
|
||||||
print_algo=print_algo,
|
print_algo=print_algo,
|
||||||
local_namespace=local_namespace,
|
local_namespace=local_namespace,
|
||||||
@@ -473,11 +493,12 @@ def live(ctx,
|
|||||||
live_graph=live_graph,
|
live_graph=live_graph,
|
||||||
analyze_live=None,
|
analyze_live=None,
|
||||||
simulate_orders=simulate_orders,
|
simulate_orders=simulate_orders,
|
||||||
|
auth_aliases=auth_aliases,
|
||||||
stats_output=None,
|
stats_output=None,
|
||||||
)
|
)
|
||||||
|
|
||||||
if output == '-':
|
if output == '-':
|
||||||
click.echo(str(perf))
|
click.echo(str(perf), sys.stdout)
|
||||||
elif output != os.devnull: # make the catalyst magic not write any data
|
elif output != os.devnull: # make the catalyst magic not write any data
|
||||||
perf.to_pickle(output)
|
perf.to_pickle(output)
|
||||||
|
|
||||||
@@ -559,7 +580,8 @@ def ingest_exchange(ctx, exchange_name, data_frequency, start, end,
|
|||||||
|
|
||||||
exchange_bundle = ExchangeBundle(exchange_name)
|
exchange_bundle = ExchangeBundle(exchange_name)
|
||||||
|
|
||||||
click.echo('Ingesting exchange bundle {}...'.format(exchange_name))
|
click.echo('Ingesting exchange bundle {}...'.format(exchange_name),
|
||||||
|
sys.stdout)
|
||||||
exchange_bundle.ingest(
|
exchange_bundle.ingest(
|
||||||
data_frequency=data_frequency,
|
data_frequency=data_frequency,
|
||||||
include_symbols=include_symbols,
|
include_symbols=include_symbols,
|
||||||
@@ -582,10 +604,11 @@ def ingest_exchange(ctx, exchange_name, data_frequency, start, end,
|
|||||||
@click.pass_context
|
@click.pass_context
|
||||||
def clean_algo(ctx, algo_namespace):
|
def clean_algo(ctx, algo_namespace):
|
||||||
click.echo(
|
click.echo(
|
||||||
'Cleaning algo state: {}'.format(algo_namespace)
|
'Cleaning algo state: {}'.format(algo_namespace),
|
||||||
|
sys.stdout
|
||||||
)
|
)
|
||||||
delete_algo_folder(algo_namespace)
|
delete_algo_folder(algo_namespace)
|
||||||
click.echo('Done')
|
click.echo('Done', sys.stdout)
|
||||||
|
|
||||||
|
|
||||||
@main.command(name='clean-exchange')
|
@main.command(name='clean-exchange')
|
||||||
@@ -612,11 +635,12 @@ def clean_exchange(ctx, exchange_name, data_frequency):
|
|||||||
|
|
||||||
exchange_bundle = ExchangeBundle(exchange_name)
|
exchange_bundle = ExchangeBundle(exchange_name)
|
||||||
|
|
||||||
click.echo('Cleaning exchange bundle {}...'.format(exchange_name))
|
click.echo('Cleaning exchange bundle {}...'.format(exchange_name),
|
||||||
|
sys.stdout)
|
||||||
exchange_bundle.clean(
|
exchange_bundle.clean(
|
||||||
data_frequency=data_frequency,
|
data_frequency=data_frequency,
|
||||||
)
|
)
|
||||||
click.echo('Done')
|
click.echo('Done', sys.stdout)
|
||||||
|
|
||||||
|
|
||||||
@main.command()
|
@main.command()
|
||||||
@@ -737,7 +761,131 @@ def bundles():
|
|||||||
# because there were no entries, print a single message indicating that
|
# because there were no entries, print a single message indicating that
|
||||||
# no ingestions have yet been made.
|
# no ingestions have yet been made.
|
||||||
for timestamp in ingestions or ["<no ingestions>"]:
|
for timestamp in ingestions or ["<no ingestions>"]:
|
||||||
click.echo("%s %s" % (bundle, timestamp))
|
click.echo("%s %s" % (bundle, timestamp), sys.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
@main.group()
|
||||||
|
@click.pass_context
|
||||||
|
def marketplace(ctx):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@marketplace.command()
|
||||||
|
@click.pass_context
|
||||||
|
def ls(ctx):
|
||||||
|
click.echo('Listing of available data sources on the marketplace:',
|
||||||
|
sys.stdout)
|
||||||
|
marketplace = Marketplace()
|
||||||
|
marketplace.list()
|
||||||
|
|
||||||
|
|
||||||
|
@marketplace.command()
|
||||||
|
@click.option(
|
||||||
|
'--dataset',
|
||||||
|
default=None,
|
||||||
|
help='The name of the dataset to ingest from the Data Marketplace.',
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def subscribe(ctx, dataset):
|
||||||
|
if dataset is None:
|
||||||
|
ctx.fail("must specify a dataset to subscribe to with '--dataset'\n"
|
||||||
|
"List available dataset on the marketplace with "
|
||||||
|
"'catalyst marketplace ls'")
|
||||||
|
marketplace = Marketplace()
|
||||||
|
marketplace.subscribe(dataset)
|
||||||
|
|
||||||
|
|
||||||
|
@marketplace.command()
|
||||||
|
@click.option(
|
||||||
|
'--dataset',
|
||||||
|
default=None,
|
||||||
|
help='The name of the dataset to ingest from the Data Marketplace.',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'-f',
|
||||||
|
'--data-frequency',
|
||||||
|
type=click.Choice({'daily', 'minute', 'daily,minute', 'minute,daily'}),
|
||||||
|
default='daily',
|
||||||
|
show_default=True,
|
||||||
|
help='The data frequency of the desired OHLCV bars.',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'-s',
|
||||||
|
'--start',
|
||||||
|
default=None,
|
||||||
|
type=Date(tz='utc', as_timestamp=True),
|
||||||
|
help='The start date of the data range. (default: one year from end date)',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'-e',
|
||||||
|
'--end',
|
||||||
|
default=None,
|
||||||
|
type=Date(tz='utc', as_timestamp=True),
|
||||||
|
help='The end date of the data range. (default: today)',
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def ingest(ctx, dataset, data_frequency, start, end):
|
||||||
|
if dataset is None:
|
||||||
|
ctx.fail("must specify a dataset to clean with '--dataset'\n"
|
||||||
|
"List available dataset on the marketplace with "
|
||||||
|
"'catalyst marketplace ls'")
|
||||||
|
click.echo('Ingesting data: {}'.format(dataset), sys.stdout)
|
||||||
|
marketplace = Marketplace()
|
||||||
|
marketplace.ingest(dataset, data_frequency, start, end)
|
||||||
|
|
||||||
|
|
||||||
|
@marketplace.command()
|
||||||
|
@click.option(
|
||||||
|
'--dataset',
|
||||||
|
default=None,
|
||||||
|
help='The name of the dataset to ingest from the Data Marketplace.',
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def clean(ctx, dataset):
|
||||||
|
if dataset is None:
|
||||||
|
ctx.fail("must specify a dataset to ingest with '--dataset'\n"
|
||||||
|
"List available dataset on the marketplace with "
|
||||||
|
"'catalyst marketplace ls'")
|
||||||
|
click.echo('Cleaning data source: {}'.format(dataset), sys.stdout)
|
||||||
|
marketplace = Marketplace()
|
||||||
|
marketplace.clean(dataset)
|
||||||
|
click.echo('Done', sys.stdout)
|
||||||
|
|
||||||
|
|
||||||
|
@marketplace.command()
|
||||||
|
@click.pass_context
|
||||||
|
def register(ctx):
|
||||||
|
marketplace = Marketplace()
|
||||||
|
marketplace.register()
|
||||||
|
|
||||||
|
|
||||||
|
@marketplace.command()
|
||||||
|
@click.option(
|
||||||
|
'--dataset',
|
||||||
|
default=None,
|
||||||
|
help='The name of the Marketplace dataset to publish data for.',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--datadir',
|
||||||
|
default=None,
|
||||||
|
help='The folder that contains the CSV data files to publish.',
|
||||||
|
)
|
||||||
|
@click.option(
|
||||||
|
'--watch/--no-watch',
|
||||||
|
is_flag=True,
|
||||||
|
default=False,
|
||||||
|
help='Whether to watch the datadir for live data.',
|
||||||
|
)
|
||||||
|
@click.pass_context
|
||||||
|
def publish(ctx, dataset, datadir, watch):
|
||||||
|
marketplace = Marketplace()
|
||||||
|
if dataset is None:
|
||||||
|
ctx.fail("must specify a dataset to publish data for "
|
||||||
|
" with '--dataset'\n")
|
||||||
|
if datadir is None:
|
||||||
|
ctx.fail("must specify a datadir where to find the files to publish "
|
||||||
|
" with '--datadir'\n")
|
||||||
|
marketplace.publish(dataset, datadir, watch)
|
||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
+65
-8
@@ -34,6 +34,7 @@ def attach_pipeline(pipeline, name, chunks=None):
|
|||||||
:func:`catalyst.api.pipeline_output`
|
:func:`catalyst.api.pipeline_output`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def batch_market_order(share_counts):
|
def batch_market_order(share_counts):
|
||||||
"""Place a batch market order for multiple assets.
|
"""Place a batch market order for multiple assets.
|
||||||
|
|
||||||
@@ -48,6 +49,7 @@ def batch_market_order(share_counts):
|
|||||||
Index of ids for newly-created orders.
|
Index of ids for newly-created orders.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def cancel_order(order_param):
|
def cancel_order(order_param):
|
||||||
"""Cancel an open order.
|
"""Cancel an open order.
|
||||||
|
|
||||||
@@ -57,7 +59,9 @@ def cancel_order(order_param):
|
|||||||
The order_id or order object to cancel.
|
The order_id or order object to cancel.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def continuous_future(root_symbol_str, offset=0, roll='volume', adjustment='mul'):
|
|
||||||
|
def continuous_future(root_symbol_str, offset=0, roll='volume',
|
||||||
|
adjustment='mul'):
|
||||||
"""Create a specifier for a continuous contract.
|
"""Create a specifier for a continuous contract.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -81,7 +85,10 @@ def continuous_future(root_symbol_str, offset=0, roll='volume', adjustment='mul'
|
|||||||
The continuous future specifier.
|
The continuous future specifier.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def fetch_csv(url, pre_func=None, post_func=None, date_column='date', date_format=None, timezone='UTC', symbol=None, mask=True, symbol_column=None, special_params_checker=None, **kwargs):
|
|
||||||
|
def fetch_csv(url, pre_func=None, post_func=None, date_column='date',
|
||||||
|
date_format=None, timezone='UTC', symbol=None, mask=True,
|
||||||
|
symbol_column=None, special_params_checker=None, **kwargs):
|
||||||
"""Fetch a csv from a remote url and register the data so that it is
|
"""Fetch a csv from a remote url and register the data so that it is
|
||||||
queryable from the ``data`` object.
|
queryable from the ``data`` object.
|
||||||
|
|
||||||
@@ -125,6 +132,7 @@ def fetch_csv(url, pre_func=None, post_func=None, date_column='date', date_forma
|
|||||||
A requests source that will pull data from the url specified.
|
A requests source that will pull data from the url specified.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def future_symbol(symbol):
|
def future_symbol(symbol):
|
||||||
"""Lookup a futures contract with a given symbol.
|
"""Lookup a futures contract with a given symbol.
|
||||||
|
|
||||||
@@ -144,6 +152,7 @@ def future_symbol(symbol):
|
|||||||
Raised when no contract named 'symbol' is found.
|
Raised when no contract named 'symbol' is found.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_datetime(tz=None):
|
def get_datetime(tz=None):
|
||||||
"""
|
"""
|
||||||
Returns the current simulation datetime.
|
Returns the current simulation datetime.
|
||||||
@@ -159,6 +168,7 @@ dt : datetime
|
|||||||
The current simulation datetime converted to ``tz``.
|
The current simulation datetime converted to ``tz``.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_environment(field='platform'):
|
def get_environment(field='platform'):
|
||||||
"""Query the execution environment.
|
"""Query the execution environment.
|
||||||
|
|
||||||
@@ -198,6 +208,7 @@ def get_environment(field='platform'):
|
|||||||
Raised when ``field`` is not a valid option.
|
Raised when ``field`` is not a valid option.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def get_order(order_id):
|
def get_order(order_id):
|
||||||
"""Lookup an order based on the order id returned from one of the
|
"""Lookup an order based on the order id returned from one of the
|
||||||
order functions.
|
order functions.
|
||||||
@@ -213,10 +224,12 @@ def get_order(order_id):
|
|||||||
The order object.
|
The order object.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def history(bar_count, frequency, field, ffill=True):
|
def history(bar_count, frequency, field, ffill=True):
|
||||||
"""DEPRECATED: use ``data.history`` instead.
|
"""DEPRECATED: use ``data.history`` instead.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def order(asset, amount, limit_price=None, stop_price=None, style=None):
|
def order(asset, amount, limit_price=None, stop_price=None, style=None):
|
||||||
"""Place an order.
|
"""Place an order.
|
||||||
|
|
||||||
@@ -258,7 +271,9 @@ def order(asset, amount, limit_price=None, stop_price=None, style=None):
|
|||||||
:func:`catalyst.api.order_percent`
|
:func:`catalyst.api.order_percent`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def order_percent(asset, percent, limit_price=None, stop_price=None, style=None):
|
|
||||||
|
def order_percent(asset, percent, limit_price=None, stop_price=None,
|
||||||
|
style=None):
|
||||||
"""Place an order in the specified asset corresponding to the given
|
"""Place an order in the specified asset corresponding to the given
|
||||||
percent of the current portfolio value.
|
percent of the current portfolio value.
|
||||||
|
|
||||||
@@ -293,6 +308,7 @@ def order_percent(asset, percent, limit_price=None, stop_price=None, style=None)
|
|||||||
:func:`catalyst.api.order_value`
|
:func:`catalyst.api.order_value`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def order_target(asset, target, limit_price=None, stop_price=None, style=None):
|
def order_target(asset, target, limit_price=None, stop_price=None, style=None):
|
||||||
"""Place an order to adjust a position to a target number of shares. If
|
"""Place an order to adjust a position to a target number of shares. If
|
||||||
the position doesn't already exist, this is equivalent to placing a new
|
the position doesn't already exist, this is equivalent to placing a new
|
||||||
@@ -344,7 +360,9 @@ def order_target(asset, target, limit_price=None, stop_price=None, style=None):
|
|||||||
:func:`catalyst.api.order_target_value`
|
:func:`catalyst.api.order_target_value`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def order_target_percent(asset, target, limit_price=None, stop_price=None, style=None):
|
|
||||||
|
def order_target_percent(asset, target, limit_price=None, stop_price=None,
|
||||||
|
style=None):
|
||||||
"""Place an order to adjust a position to a target percent of the
|
"""Place an order to adjust a position to a target percent of the
|
||||||
current portfolio value. If the position doesn't already exist, this is
|
current portfolio value. If the position doesn't already exist, this is
|
||||||
equivalent to placing a new order. If the position does exist, this is
|
equivalent to placing a new order. If the position does exist, this is
|
||||||
@@ -396,7 +414,9 @@ def order_target_percent(asset, target, limit_price=None, stop_price=None, style
|
|||||||
:func:`catalyst.api.order_target_value`
|
:func:`catalyst.api.order_target_value`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def order_target_value(asset, target, limit_price=None, stop_price=None, style=None):
|
|
||||||
|
def order_target_value(asset, target, limit_price=None, stop_price=None,
|
||||||
|
style=None):
|
||||||
"""Place an order to adjust a position to a target value. If
|
"""Place an order to adjust a position to a target value. If
|
||||||
the position doesn't already exist, this is equivalent to placing a new
|
the position doesn't already exist, this is equivalent to placing a new
|
||||||
order. If the position does exist, this is equivalent to placing an
|
order. If the position does exist, this is equivalent to placing an
|
||||||
@@ -448,6 +468,7 @@ def order_target_value(asset, target, limit_price=None, stop_price=None, style=N
|
|||||||
:func:`catalyst.api.order_target_percent`
|
:func:`catalyst.api.order_target_percent`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def order_value(asset, value, limit_price=None, stop_price=None, style=None):
|
def order_value(asset, value, limit_price=None, stop_price=None, style=None):
|
||||||
"""Place an order by desired value rather than desired number of
|
"""Place an order by desired value rather than desired number of
|
||||||
shares.
|
shares.
|
||||||
@@ -488,6 +509,7 @@ def order_value(asset, value, limit_price=None, stop_price=None, style=None):
|
|||||||
:func:`catalyst.api.order_percent`
|
:func:`catalyst.api.order_percent`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def pipeline_output(name):
|
def pipeline_output(name):
|
||||||
"""Get the results of the pipeline that was attached with the name:
|
"""Get the results of the pipeline that was attached with the name:
|
||||||
``name``.
|
``name``.
|
||||||
@@ -514,6 +536,7 @@ def pipeline_output(name):
|
|||||||
:meth:`catalyst.pipeline.engine.PipelineEngine.run_pipeline`
|
:meth:`catalyst.pipeline.engine.PipelineEngine.run_pipeline`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def record(*args, **kwargs):
|
def record(*args, **kwargs):
|
||||||
"""Track and record values each day.
|
"""Track and record values each day.
|
||||||
|
|
||||||
@@ -529,7 +552,9 @@ def record(*args, **kwargs):
|
|||||||
:func:`~catalyst.run_algorithm`.
|
:func:`~catalyst.run_algorithm`.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def schedule_function(func, date_rule=None, time_rule=None, half_days=True, calendar=None):
|
|
||||||
|
def schedule_function(func, date_rule=None, time_rule=None, half_days=True,
|
||||||
|
calendar=None):
|
||||||
"""Schedules a function to be called according to some timed rules.
|
"""Schedules a function to be called according to some timed rules.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -549,6 +574,7 @@ def schedule_function(func, date_rule=None, time_rule=None, half_days=True, cale
|
|||||||
:class:`catalyst.api.time_rules`
|
:class:`catalyst.api.time_rules`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_asset_restrictions(restrictions, on_error='fail'):
|
def set_asset_restrictions(restrictions, on_error='fail'):
|
||||||
"""Set a restriction on which assets can be ordered.
|
"""Set a restriction on which assets can be ordered.
|
||||||
|
|
||||||
@@ -562,6 +588,7 @@ def set_asset_restrictions(restrictions, on_error='fail'):
|
|||||||
catalyst.finance.asset_restrictions.Restrictions
|
catalyst.finance.asset_restrictions.Restrictions
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_benchmark(benchmark):
|
def set_benchmark(benchmark):
|
||||||
"""Set the benchmark asset.
|
"""Set the benchmark asset.
|
||||||
|
|
||||||
@@ -576,6 +603,7 @@ def set_benchmark(benchmark):
|
|||||||
automatically reinvested.
|
automatically reinvested.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_cancel_policy(cancel_policy):
|
def set_cancel_policy(cancel_policy):
|
||||||
"""Sets the order cancellation policy for the simulation.
|
"""Sets the order cancellation policy for the simulation.
|
||||||
|
|
||||||
@@ -590,6 +618,7 @@ def set_cancel_policy(cancel_policy):
|
|||||||
:class:`catalyst.api.NeverCancel`
|
:class:`catalyst.api.NeverCancel`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_commission(commission):
|
def set_commission(commission):
|
||||||
"""Sets the commission model for the simulation.
|
"""Sets the commission model for the simulation.
|
||||||
|
|
||||||
@@ -605,6 +634,7 @@ def set_commission(commission):
|
|||||||
:class:`catalyst.finance.commission.PerDollar`
|
:class:`catalyst.finance.commission.PerDollar`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_do_not_order_list(restricted_list, on_error='fail'):
|
def set_do_not_order_list(restricted_list, on_error='fail'):
|
||||||
"""Set a restriction on which assets can be ordered.
|
"""Set a restriction on which assets can be ordered.
|
||||||
|
|
||||||
@@ -614,11 +644,13 @@ def set_do_not_order_list(restricted_list, on_error='fail'):
|
|||||||
The assets that cannot be ordered.
|
The assets that cannot be ordered.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_long_only(on_error='fail'):
|
def set_long_only(on_error='fail'):
|
||||||
"""Set a rule specifying that this algorithm cannot take short
|
"""Set a rule specifying that this algorithm cannot take short
|
||||||
positions.
|
positions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_max_leverage(max_leverage):
|
def set_max_leverage(max_leverage):
|
||||||
"""Set a limit on the maximum leverage of the algorithm.
|
"""Set a limit on the maximum leverage of the algorithm.
|
||||||
|
|
||||||
@@ -629,6 +661,7 @@ def set_max_leverage(max_leverage):
|
|||||||
be no maximum.
|
be no maximum.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_max_order_count(max_count, on_error='fail'):
|
def set_max_order_count(max_count, on_error='fail'):
|
||||||
"""Set a limit on the number of orders that can be placed in a single
|
"""Set a limit on the number of orders that can be placed in a single
|
||||||
day.
|
day.
|
||||||
@@ -639,7 +672,9 @@ def set_max_order_count(max_count, on_error='fail'):
|
|||||||
The maximum number of orders that can be placed on any single day.
|
The maximum number of orders that can be placed on any single day.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def set_max_order_size(asset=None, max_shares=None, max_notional=None, on_error='fail'):
|
|
||||||
|
def set_max_order_size(asset=None, max_shares=None, max_notional=None,
|
||||||
|
on_error='fail'):
|
||||||
"""Set a limit on the number of shares and/or dollar value of any single
|
"""Set a limit on the number of shares and/or dollar value of any single
|
||||||
order placed for sid. Limits are treated as absolute values and are
|
order placed for sid. Limits are treated as absolute values and are
|
||||||
enforced at the time that the algo attempts to place an order for sid.
|
enforced at the time that the algo attempts to place an order for sid.
|
||||||
@@ -658,7 +693,9 @@ def set_max_order_size(asset=None, max_shares=None, max_notional=None, on_error=
|
|||||||
The maximum value that can be ordered at one time.
|
The maximum value that can be ordered at one time.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def set_max_position_size(asset=None, max_shares=None, max_notional=None, on_error='fail'):
|
|
||||||
|
def set_max_position_size(asset=None, max_shares=None, max_notional=None,
|
||||||
|
on_error='fail'):
|
||||||
"""Set a limit on the number of shares and/or dollar value held for the
|
"""Set a limit on the number of shares and/or dollar value held for the
|
||||||
given sid. Limits are treated as absolute values and are enforced at
|
given sid. Limits are treated as absolute values and are enforced at
|
||||||
the time that the algo attempts to place an order for sid. This means
|
the time that the algo attempts to place an order for sid. This means
|
||||||
@@ -681,6 +718,7 @@ def set_max_position_size(asset=None, max_shares=None, max_notional=None, on_err
|
|||||||
The maximum value to hold for an asset.
|
The maximum value to hold for an asset.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_slippage(slippage):
|
def set_slippage(slippage):
|
||||||
"""Set the slippage model for the simulation.
|
"""Set the slippage model for the simulation.
|
||||||
|
|
||||||
@@ -694,6 +732,7 @@ def set_slippage(slippage):
|
|||||||
:class:`catalyst.finance.slippage.SlippageModel`
|
:class:`catalyst.finance.slippage.SlippageModel`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def set_symbol_lookup_date(dt):
|
def set_symbol_lookup_date(dt):
|
||||||
"""Set the date for which symbols will be resolved to their assets
|
"""Set the date for which symbols will be resolved to their assets
|
||||||
(symbols may map to different firms or underlying assets at
|
(symbols may map to different firms or underlying assets at
|
||||||
@@ -705,6 +744,7 @@ def set_symbol_lookup_date(dt):
|
|||||||
The new symbol lookup date.
|
The new symbol lookup date.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def sid(sid):
|
def sid(sid):
|
||||||
"""Lookup an Asset by its unique asset identifier.
|
"""Lookup an Asset by its unique asset identifier.
|
||||||
|
|
||||||
@@ -724,6 +764,7 @@ def sid(sid):
|
|||||||
When a requested ``sid`` does not map to any asset.
|
When a requested ``sid`` does not map to any asset.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def symbol(symbol_str):
|
def symbol(symbol_str):
|
||||||
"""Lookup an Equity by its ticker symbol.
|
"""Lookup an Equity by its ticker symbol.
|
||||||
|
|
||||||
@@ -748,6 +789,7 @@ def symbol(symbol_str):
|
|||||||
:func:`catalyst.api.set_symbol_lookup_date`
|
:func:`catalyst.api.set_symbol_lookup_date`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
def symbols(*args):
|
def symbols(*args):
|
||||||
"""Lookup multuple Equities as a list.
|
"""Lookup multuple Equities as a list.
|
||||||
|
|
||||||
@@ -773,3 +815,18 @@ def symbols(*args):
|
|||||||
:func:`catalyst.api.set_symbol_lookup_date`
|
:func:`catalyst.api.set_symbol_lookup_date`
|
||||||
"""
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def get_dataset(ds_name, start=None, end=None):
|
||||||
|
"""
|
||||||
|
Lookup a data source from the marketplace
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ds_name: str
|
||||||
|
start: pd.Timestamp
|
||||||
|
end: pd.Timestamp
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
|||||||
@@ -15,4 +15,32 @@ SYMBOLS_URL = 'https://s3.amazonaws.com/enigmaco/catalyst-exchanges/' \
|
|||||||
DATE_TIME_FORMAT = '%Y-%m-%d %H:%M'
|
DATE_TIME_FORMAT = '%Y-%m-%d %H:%M'
|
||||||
DATE_FORMAT = '%Y-%m-%d'
|
DATE_FORMAT = '%Y-%m-%d'
|
||||||
|
|
||||||
|
try:
|
||||||
|
ROOT_DIR = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
except Exception as e:
|
||||||
|
print('unable to get catalyst path: {}'.format(e))
|
||||||
|
|
||||||
AUTO_INGEST = False
|
AUTO_INGEST = False
|
||||||
|
|
||||||
|
AUTH_SERVER = 'https://data.enigma.co'
|
||||||
|
|
||||||
|
# TODO: switch to mainnet
|
||||||
|
ETH_REMOTE_NODE = 'https://ropsten.infura.io/'
|
||||||
|
|
||||||
|
# TODO: move to MASTER branch on github
|
||||||
|
MARKETPLACE_CONTRACT = 'https://raw.githubusercontent.com/enigmampc/' \
|
||||||
|
'catalyst/develop/catalyst/marketplace/' \
|
||||||
|
'contract_marketplace_address.txt'
|
||||||
|
|
||||||
|
MARKETPLACE_CONTRACT_ABI = 'https://raw.githubusercontent.com/enigmampc/' \
|
||||||
|
'catalyst/develop/catalyst/marketplace/' \
|
||||||
|
'contract_marketplace_abi.json'
|
||||||
|
|
||||||
|
# TODO: switch to mainnet
|
||||||
|
ENIGMA_CONTRACT = 'https://raw.githubusercontent.com/enigmampc/catalyst/' \
|
||||||
|
'develop/catalyst/marketplace/' \
|
||||||
|
'contract_enigma_address.txt'
|
||||||
|
|
||||||
|
ENIGMA_CONTRACT_ABI = 'https://raw.githubusercontent.com/enigmampc/' \
|
||||||
|
'catalyst/develop/catalyst/marketplace/' \
|
||||||
|
'contract_enigma_abi.json'
|
||||||
|
|||||||
@@ -88,11 +88,11 @@ class AssetDispatchBarReader(with_metaclass(ABCMeta)):
|
|||||||
if self._last_available_dt is not None:
|
if self._last_available_dt is not None:
|
||||||
return self._last_available_dt
|
return self._last_available_dt
|
||||||
else:
|
else:
|
||||||
return min(r.last_available_dt for r in self._readers.values())
|
return min(r.last_available_dt for r in list(self._readers.values()))
|
||||||
|
|
||||||
@lazyval
|
@lazyval
|
||||||
def first_trading_day(self):
|
def first_trading_day(self):
|
||||||
return max(r.first_trading_day for r in self._readers.values())
|
return max(r.first_trading_day for r in list(self._readers.values()))
|
||||||
|
|
||||||
def get_value(self, sid, dt, field):
|
def get_value(self, sid, dt, field):
|
||||||
asset = self._asset_finder.retrieve_asset(sid)
|
asset = self._asset_finder.retrieve_asset(sid)
|
||||||
|
|||||||
@@ -101,7 +101,7 @@ def load_crypto_market_data(trading_day=None, trading_days=None,
|
|||||||
trading_day = get_calendar('OPEN').trading_day
|
trading_day = get_calendar('OPEN').trading_day
|
||||||
|
|
||||||
# TODO: consider making configurable
|
# TODO: consider making configurable
|
||||||
bm_symbol = 'btc_usdt'
|
bm_symbol = 'btc_usd'
|
||||||
# if trading_days is None:
|
# if trading_days is None:
|
||||||
# trading_days = get_calendar('OPEN').schedule
|
# trading_days = get_calendar('OPEN').schedule
|
||||||
|
|
||||||
@@ -144,7 +144,7 @@ def load_crypto_market_data(trading_day=None, trading_days=None,
|
|||||||
# breaks things and it's only needed here
|
# breaks things and it's only needed here
|
||||||
from catalyst.exchange.utils.factory import get_exchange
|
from catalyst.exchange.utils.factory import get_exchange
|
||||||
exchange = get_exchange(
|
exchange = get_exchange(
|
||||||
exchange_name='poloniex', base_currency='usdt'
|
exchange_name='bitfinex', base_currency='usd'
|
||||||
)
|
)
|
||||||
exchange.init()
|
exchange.init()
|
||||||
|
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ from catalyst.api import (order_target_value, symbol, record,
|
|||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
context.ASSET_NAME = 'btc_usd'
|
context.ASSET_NAME = 'btc_usdt'
|
||||||
context.TARGET_HODL_RATIO = 0.8
|
context.TARGET_HODL_RATIO = 0.8
|
||||||
context.RESERVE_RATIO = 1.0 - context.TARGET_HODL_RATIO
|
context.RESERVE_RATIO = 1.0 - context.TARGET_HODL_RATIO
|
||||||
|
|
||||||
@@ -140,9 +140,9 @@ if __name__ == '__main__':
|
|||||||
initialize=initialize,
|
initialize=initialize,
|
||||||
handle_data=handle_data,
|
handle_data=handle_data,
|
||||||
analyze=analyze,
|
analyze=analyze,
|
||||||
exchange_name='bitfinex',
|
exchange_name='poloniex',
|
||||||
algo_namespace='buy_and_hodl',
|
algo_namespace='buy_and_hodl',
|
||||||
base_currency='usd',
|
base_currency='usdt',
|
||||||
start=pd.to_datetime('2015-03-01', utc=True),
|
start=pd.to_datetime('2015-03-01', utc=True),
|
||||||
end=pd.to_datetime('2017-10-31', utc=True),
|
end=pd.to_datetime('2017-10-31', utc=True),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import pandas as pd
|
|||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
context.asset = symbol('btc_usd')
|
context.asset = symbol('btc_usdt')
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
def handle_data(context, data):
|
||||||
@@ -41,9 +41,9 @@ if __name__ == '__main__':
|
|||||||
data_frequency='daily',
|
data_frequency='daily',
|
||||||
initialize=initialize,
|
initialize=initialize,
|
||||||
handle_data=handle_data,
|
handle_data=handle_data,
|
||||||
exchange_name='bitfinex',
|
exchange_name='poloniex',
|
||||||
algo_namespace='buy_and_hodl',
|
algo_namespace='buy_and_hodl',
|
||||||
base_currency='usd',
|
base_currency='usdt',
|
||||||
start=pd.to_datetime('2015-03-01', utc=True),
|
start=pd.to_datetime('2015-03-01', utc=True),
|
||||||
end=pd.to_datetime('2017-10-31', utc=True),
|
end=pd.to_datetime('2017-10-31', utc=True),
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -143,7 +143,7 @@ def analyze(context, stats):
|
|||||||
|
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
live = False
|
live = True
|
||||||
if live:
|
if live:
|
||||||
run_algorithm(
|
run_algorithm(
|
||||||
capital_base=0.001,
|
capital_base=0.001,
|
||||||
|
|||||||
@@ -84,7 +84,8 @@ def handle_data(context, data):
|
|||||||
def analyze(context, perf):
|
def analyze(context, perf):
|
||||||
|
|
||||||
# Get the base_currency that was passed as a parameter to the simulation
|
# Get the base_currency that was passed as a parameter to the simulation
|
||||||
base_currency = context.exchanges.values()[0].base_currency.upper()
|
exchange = list(context.exchanges.values())[0]
|
||||||
|
base_currency = exchange.base_currency.upper()
|
||||||
|
|
||||||
# First chart: Plot portfolio value using base_currency
|
# First chart: Plot portfolio value using base_currency
|
||||||
ax1 = plt.subplot(411)
|
ax1 = plt.subplot(411)
|
||||||
|
|||||||
@@ -0,0 +1,237 @@
|
|||||||
|
# For this example, we're going to write a simple momentum script. When the
|
||||||
|
# stock goes up quickly, we're going to buy; when it goes down quickly, we're
|
||||||
|
# going to sell. Hopefully we'll ride the waves.
|
||||||
|
import os
|
||||||
|
import tempfile
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import talib
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import symbol, record, order_target_percent, get_dataset
|
||||||
|
from catalyst.exchange.utils.stats_utils import set_print_settings, \
|
||||||
|
get_pretty_stats
|
||||||
|
# We give a name to the algorithm which Catalyst will use to persist its state.
|
||||||
|
# In this example, Catalyst will create the `.catalyst/data/live_algos`
|
||||||
|
# directory. If we stop and start the algorithm, Catalyst will resume its
|
||||||
|
# state using the files included in the folder.
|
||||||
|
from catalyst.utils.paths import ensure_directory
|
||||||
|
|
||||||
|
NAMESPACE = 'mean_reversion_simple'
|
||||||
|
log = Logger(NAMESPACE)
|
||||||
|
|
||||||
|
|
||||||
|
# To run an algorithm in Catalyst, you need two functions: initialize and
|
||||||
|
# handle_data.
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
# This initialize function sets any data or variables that you'll use in
|
||||||
|
# your algorithm. For instance, you'll want to define the trading pair (or
|
||||||
|
# trading pairs) you want to backtest. You'll also want to define any
|
||||||
|
# parameters or values you're going to use.
|
||||||
|
|
||||||
|
# In our example, we're looking at Neo in Ether.
|
||||||
|
df = get_dataset('testmarketcap2') # type: pd.DataFrame
|
||||||
|
|
||||||
|
# Picking a specific date in our DataFrame
|
||||||
|
first_dt = df.index.get_level_values(0)[0]
|
||||||
|
# Since we use a MultiIndex with date / symbol, picking a date will
|
||||||
|
# result in a new DataFrame for the selected date with a single
|
||||||
|
# symbol index
|
||||||
|
df = df.xs(first_dt, level=0)
|
||||||
|
# Keep only the top coins by market cap
|
||||||
|
df = df.loc[df['market_cap_usd'].isin(df['market_cap_usd'].nlargest(100))]
|
||||||
|
|
||||||
|
set_print_settings()
|
||||||
|
|
||||||
|
df.sort_values(by=['market_cap_usd'], ascending=True, inplace=True)
|
||||||
|
print('the marketplace data:\n{}'.format(df))
|
||||||
|
|
||||||
|
# Pick the 5 assets with the lowest market cap for trading
|
||||||
|
quote_currency = 'eth'
|
||||||
|
exchange = context.exchanges[next(iter(context.exchanges))]
|
||||||
|
symbols = [a.symbol for a in exchange.assets
|
||||||
|
if a.start_date < context.datetime]
|
||||||
|
context.assets = []
|
||||||
|
for currency, price in df['market_cap_usd'].iteritems():
|
||||||
|
if len(context.assets) >= 5:
|
||||||
|
break
|
||||||
|
|
||||||
|
s = '{}_{}'.format(currency.decode('utf-8'), quote_currency)
|
||||||
|
if s in symbols:
|
||||||
|
context.assets.append(symbol(s))
|
||||||
|
|
||||||
|
context.base_price = None
|
||||||
|
context.current_day = None
|
||||||
|
|
||||||
|
context.RSI_OVERSOLD = 55
|
||||||
|
context.RSI_OVERBOUGHT = 60
|
||||||
|
context.CANDLE_SIZE = '5T'
|
||||||
|
|
||||||
|
context.start_time = time.time()
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
# This handle_data function is where the real work is done. Our data is
|
||||||
|
# minute-level tick data, and each minute is called a frame. This function
|
||||||
|
# runs on each frame of the data.
|
||||||
|
|
||||||
|
# We flag the first period of each day.
|
||||||
|
# Since cryptocurrencies trade 24/7 the `before_trading_starts` handle
|
||||||
|
# would only execute once. This method works with minute and daily
|
||||||
|
# frequencies.
|
||||||
|
today = data.current_dt.floor('1D')
|
||||||
|
if today != context.current_day:
|
||||||
|
context.traded_today = dict()
|
||||||
|
context.current_day = today
|
||||||
|
|
||||||
|
# Preparing dictionaries for asset-level data points
|
||||||
|
volumes = dict()
|
||||||
|
rsis = dict()
|
||||||
|
price_values = dict()
|
||||||
|
cash = context.portfolio.cash
|
||||||
|
|
||||||
|
for asset in context.assets:
|
||||||
|
# We're computing the volume-weighted-average-price of the security
|
||||||
|
# defined above, in the context.assets variable. For this example,
|
||||||
|
# we're using three bars on the 15 min bars.
|
||||||
|
|
||||||
|
# The frequency attribute determine the bar size. We use this
|
||||||
|
# convention for the frequency alias:
|
||||||
|
# http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
|
||||||
|
prices = data.history(
|
||||||
|
asset,
|
||||||
|
fields='close',
|
||||||
|
bar_count=50,
|
||||||
|
frequency=context.CANDLE_SIZE
|
||||||
|
)
|
||||||
|
|
||||||
|
# Ta-lib calculates various technical indicator based on price and
|
||||||
|
# volume arrays.
|
||||||
|
|
||||||
|
# In this example, we are comp
|
||||||
|
rsi = talib.RSI(prices.values, timeperiod=14)
|
||||||
|
|
||||||
|
# We need a variable for the current price of the security to compare
|
||||||
|
# to the average. Since we are requesting two fields, data.current()
|
||||||
|
# returns a DataFrame with
|
||||||
|
current = data.current(asset, fields=['close', 'volume'])
|
||||||
|
price = current['close']
|
||||||
|
|
||||||
|
# If base_price is not set, we use the current value. This is the
|
||||||
|
# price at the first bar which we reference to calculate price_change.
|
||||||
|
# if asset not in context.base_price:
|
||||||
|
# context.base_price[asset] = price
|
||||||
|
#
|
||||||
|
# base_price = context.base_price[asset]
|
||||||
|
# price_change = (price - base_price) / base_price
|
||||||
|
|
||||||
|
# Tracking the relevant data
|
||||||
|
volumes[asset] = current['volume']
|
||||||
|
rsis[asset] = rsi[-1]
|
||||||
|
price_values[asset] = price
|
||||||
|
# price_changes[asset] = price_change
|
||||||
|
|
||||||
|
# We are trying to avoid over-trading by limiting our trades to
|
||||||
|
# one per day.
|
||||||
|
if asset in context.traded_today:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Exit if we cannot trade
|
||||||
|
if not data.can_trade(asset):
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Another powerful built-in feature of the Catalyst backtester is the
|
||||||
|
# portfolio object. The portfolio object tracks your positions, cash,
|
||||||
|
# cost basis of specific holdings, and more. In this line, we
|
||||||
|
# calculate how long or short our position is at this minute.
|
||||||
|
pos_amount = context.portfolio.positions[asset].amount
|
||||||
|
|
||||||
|
if rsi[-1] <= context.RSI_OVERSOLD and pos_amount == 0:
|
||||||
|
log.info(
|
||||||
|
'{}: buying - price: {}, rsi: {}'.format(
|
||||||
|
data.current_dt, price, rsi[-1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
# Set a style for limit orders,
|
||||||
|
limit_price = price * 1.005
|
||||||
|
target = 1.0 / len(context.assets)
|
||||||
|
order_target_percent(
|
||||||
|
asset, target, limit_price=limit_price
|
||||||
|
)
|
||||||
|
context.traded_today[asset] = True
|
||||||
|
|
||||||
|
elif rsi[-1] >= context.RSI_OVERBOUGHT and pos_amount > 0:
|
||||||
|
log.info(
|
||||||
|
'{}: selling - price: {}, rsi: {}'.format(
|
||||||
|
data.current_dt, price, rsi[-1]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
limit_price = price * 0.995
|
||||||
|
order_target_percent(
|
||||||
|
asset, 0, limit_price=limit_price
|
||||||
|
)
|
||||||
|
context.traded_today[asset] = True
|
||||||
|
|
||||||
|
# Now that we've collected all current data for this frame, we use
|
||||||
|
# the record() method to save it. This data will be available as
|
||||||
|
# a parameter of the analyze() function for further analysis.
|
||||||
|
record(
|
||||||
|
current_price=price_values,
|
||||||
|
volume=volumes,
|
||||||
|
rsi=rsis,
|
||||||
|
cash=cash,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context=None, perf=None):
|
||||||
|
stats = get_pretty_stats(perf)
|
||||||
|
print('the algo stats:\n{}'.format(stats))
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
# The execution mode: backtest or live
|
||||||
|
live = False
|
||||||
|
|
||||||
|
if live:
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=0.1,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
live=True,
|
||||||
|
algo_namespace=NAMESPACE,
|
||||||
|
base_currency='btc',
|
||||||
|
live_graph=False,
|
||||||
|
simulate_orders=False,
|
||||||
|
stats_output=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
folder = os.path.join(
|
||||||
|
tempfile.gettempdir(), 'catalyst', NAMESPACE
|
||||||
|
)
|
||||||
|
ensure_directory(folder)
|
||||||
|
|
||||||
|
timestr = time.strftime('%Y%m%d-%H%M%S')
|
||||||
|
out = os.path.join(folder, '{}.p'.format(timestr))
|
||||||
|
# catalyst run -f catalyst/examples/mean_reversion_simple.py \
|
||||||
|
# -x bitfinex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion \
|
||||||
|
# --data-frequency minute --capital-base 10000
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=100,
|
||||||
|
data_frequency='minute',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
algo_namespace=NAMESPACE,
|
||||||
|
base_currency='eth',
|
||||||
|
start=pd.to_datetime('2017-10-01', utc=True),
|
||||||
|
end=pd.to_datetime('2017-10-15', utc=True),
|
||||||
|
)
|
||||||
|
log.info('saved perf stats: {}'.format(out))
|
||||||
@@ -33,13 +33,13 @@ def initialize(context):
|
|||||||
# parameters or values you're going to use.
|
# parameters or values you're going to use.
|
||||||
|
|
||||||
# In our example, we're looking at Neo in Ether.
|
# In our example, we're looking at Neo in Ether.
|
||||||
context.market = symbol('eth_btc')
|
context.market = symbol('bnb_eth')
|
||||||
context.base_price = None
|
context.base_price = None
|
||||||
context.current_day = None
|
context.current_day = None
|
||||||
|
|
||||||
context.RSI_OVERSOLD = 55
|
context.RSI_OVERSOLD = 40
|
||||||
context.RSI_OVERBOUGHT = 60
|
context.RSI_OVERBOUGHT = 60
|
||||||
context.CANDLE_SIZE = '5T'
|
context.CANDLE_SIZE = '15T'
|
||||||
|
|
||||||
context.start_time = time.time()
|
context.start_time = time.time()
|
||||||
|
|
||||||
@@ -114,7 +114,7 @@ def handle_data(context, data):
|
|||||||
# TODO: retest with open orders
|
# TODO: retest with open orders
|
||||||
# Since we are using limit orders, some orders may not execute immediately
|
# Since we are using limit orders, some orders may not execute immediately
|
||||||
# we wait until all orders are executed before considering more trades.
|
# we wait until all orders are executed before considering more trades.
|
||||||
orders = get_open_orders(context.market)
|
orders = context.blotter.open_orders
|
||||||
if len(orders) > 0:
|
if len(orders) > 0:
|
||||||
log.info('exiting because orders are open: {}'.format(orders))
|
log.info('exiting because orders are open: {}'.format(orders))
|
||||||
return
|
return
|
||||||
@@ -161,7 +161,7 @@ def analyze(context=None, perf=None):
|
|||||||
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
# The base currency of the algo exchange
|
# The base currency of the algo exchange
|
||||||
base_currency = context.exchanges.values()[0].base_currency.upper()
|
base_currency = list(context.exchanges.values())[0].base_currency.upper()
|
||||||
|
|
||||||
# Plot the portfolio value over time.
|
# Plot the portfolio value over time.
|
||||||
ax1 = plt.subplot(611)
|
ax1 = plt.subplot(611)
|
||||||
@@ -244,7 +244,7 @@ def analyze(context=None, perf=None):
|
|||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
# The execution mode: backtest or live
|
# The execution mode: backtest or live
|
||||||
live = False
|
live = True
|
||||||
|
|
||||||
if live:
|
if live:
|
||||||
run_algorithm(
|
run_algorithm(
|
||||||
@@ -252,13 +252,14 @@ if __name__ == '__main__':
|
|||||||
initialize=initialize,
|
initialize=initialize,
|
||||||
handle_data=handle_data,
|
handle_data=handle_data,
|
||||||
analyze=analyze,
|
analyze=analyze,
|
||||||
exchange_name='poloniex',
|
exchange_name='binance',
|
||||||
live=True,
|
live=True,
|
||||||
algo_namespace=NAMESPACE,
|
algo_namespace=NAMESPACE,
|
||||||
base_currency='btc',
|
base_currency='eth',
|
||||||
live_graph=False,
|
live_graph=False,
|
||||||
simulate_orders=False,
|
simulate_orders=False,
|
||||||
stats_output=None,
|
stats_output=None,
|
||||||
|
# auth_aliases=dict(poloniex='auth2')
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -273,7 +274,7 @@ if __name__ == '__main__':
|
|||||||
# -x bitfinex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion \
|
# -x bitfinex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion \
|
||||||
# --data-frequency minute --capital-base 10000
|
# --data-frequency minute --capital-base 10000
|
||||||
run_algorithm(
|
run_algorithm(
|
||||||
capital_base=0.1,
|
capital_base=0.035,
|
||||||
data_frequency='minute',
|
data_frequency='minute',
|
||||||
initialize=initialize,
|
initialize=initialize,
|
||||||
handle_data=handle_data,
|
handle_data=handle_data,
|
||||||
|
|||||||
@@ -161,7 +161,7 @@ def analyze(context=None, perf=None):
|
|||||||
|
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
# The base currency of the algo exchange
|
# The base currency of the algo exchange
|
||||||
base_currency = context.exchanges.values()[0].base_currency.upper()
|
base_currency = list(context.exchanges.values())[0].base_currency.upper()
|
||||||
|
|
||||||
# Plot the portfolio value over time.
|
# Plot the portfolio value over time.
|
||||||
ax1 = plt.subplot(611)
|
ax1 = plt.subplot(611)
|
||||||
|
|||||||
@@ -175,7 +175,7 @@ def handle_data(context, data):
|
|||||||
def analyze(context=None, results=None):
|
def analyze(context=None, results=None):
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
base_currency = context.exchanges.values()[0].base_currency.upper()
|
base_currency = list(context.exchanges.values())[0].base_currency.upper()
|
||||||
# Plot the portfolio and asset data.
|
# Plot the portfolio and asset data.
|
||||||
ax1 = plt.subplot(611)
|
ax1 = plt.subplot(611)
|
||||||
results.loc[:, 'portfolio_value'].plot(ax=ax1)
|
results.loc[:, 'portfolio_value'].plot(ax=ax1)
|
||||||
|
|||||||
@@ -57,7 +57,7 @@ def analyze(context, perf):
|
|||||||
log.info('the stats: {}'.format(get_pretty_stats(perf)))
|
log.info('the stats: {}'.format(get_pretty_stats(perf)))
|
||||||
|
|
||||||
# The base currency of the algo exchange
|
# The base currency of the algo exchange
|
||||||
base_currency = context.exchanges.values()[0].base_currency.upper()
|
base_currency = list(context.exchanges.values())[0].base_currency.upper()
|
||||||
|
|
||||||
# Plot the portfolio value over time.
|
# Plot the portfolio value over time.
|
||||||
ax1 = plt.subplot(611)
|
ax1 = plt.subplot(611)
|
||||||
|
|||||||
@@ -41,8 +41,8 @@ from catalyst.exchange.utils.exchange_utils import get_exchange_symbols
|
|||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
context.i = -1 # minute counter
|
context.i = -1 # minute counter
|
||||||
context.exchange = context.exchanges.values()[0].name.lower()
|
context.exchange = list(context.exchanges.values())[0].name.lower()
|
||||||
context.base_currency = context.exchanges.values()[0].base_currency.lower()
|
context.base_currency = list(context.exchanges.values())[0].base_currency.lower()
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
def handle_data(context, data):
|
||||||
@@ -65,7 +65,7 @@ def handle_data(context, data):
|
|||||||
minutes = 30
|
minutes = 30
|
||||||
|
|
||||||
# get lookback_days of history data: that is 'lookback' number of bins
|
# get lookback_days of history data: that is 'lookback' number of bins
|
||||||
lookback = one_day_in_minutes / minutes * lookback_days
|
lookback = int(one_day_in_minutes / minutes * lookback_days)
|
||||||
if not context.i % minutes and context.universe:
|
if not context.i % minutes and context.universe:
|
||||||
# we iterate for every pair in the current universe
|
# we iterate for every pair in the current universe
|
||||||
for coin in context.coins:
|
for coin in context.coins:
|
||||||
|
|||||||
@@ -6,6 +6,11 @@ from collections import defaultdict
|
|||||||
import ccxt
|
import ccxt
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import six
|
import six
|
||||||
|
from ccxt import InvalidOrder, NetworkError, \
|
||||||
|
ExchangeError
|
||||||
|
from logbook import Logger
|
||||||
|
from six import string_types
|
||||||
|
|
||||||
from catalyst.algorithm import MarketOrder
|
from catalyst.algorithm import MarketOrder
|
||||||
from catalyst.assets._assets import TradingPair
|
from catalyst.assets._assets import TradingPair
|
||||||
from catalyst.constants import LOG_LEVEL
|
from catalyst.constants import LOG_LEVEL
|
||||||
@@ -13,16 +18,17 @@ from catalyst.exchange.exchange import Exchange
|
|||||||
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
||||||
from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \
|
from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \
|
||||||
ExchangeSymbolsNotFound, ExchangeRequestError, InvalidOrderStyle, \
|
ExchangeSymbolsNotFound, ExchangeRequestError, InvalidOrderStyle, \
|
||||||
ExchangeNotFoundError, CreateOrderError, InvalidHistoryTimeframeError
|
ExchangeNotFoundError, CreateOrderError, InvalidHistoryTimeframeError, \
|
||||||
|
UnsupportedHistoryFrequencyError
|
||||||
from catalyst.exchange.exchange_execution import ExchangeLimitOrder
|
from catalyst.exchange.exchange_execution import ExchangeLimitOrder
|
||||||
from catalyst.exchange.utils.exchange_utils import mixin_market_params, \
|
from catalyst.exchange.utils.exchange_utils import mixin_market_params, \
|
||||||
from_ms_timestamp, get_epoch, get_exchange_folder, get_catalyst_symbol, \
|
get_exchange_folder, get_catalyst_symbol, \
|
||||||
get_exchange_auth
|
get_exchange_auth
|
||||||
|
from catalyst.exchange.utils.datetime_utils import from_ms_timestamp, \
|
||||||
|
get_epoch, \
|
||||||
|
get_periods_range
|
||||||
from catalyst.finance.order import Order, ORDER_STATUS
|
from catalyst.finance.order import Order, ORDER_STATUS
|
||||||
from ccxt import InvalidOrder, NetworkError, \
|
from catalyst.finance.transaction import Transaction
|
||||||
ExchangeError
|
|
||||||
from logbook import Logger
|
|
||||||
from six import string_types
|
|
||||||
|
|
||||||
log = Logger('CCXT', level=LOG_LEVEL)
|
log = Logger('CCXT', level=LOG_LEVEL)
|
||||||
|
|
||||||
@@ -55,6 +61,7 @@ class CCXT(Exchange):
|
|||||||
'apiKey': key,
|
'apiKey': key,
|
||||||
'secret': secret,
|
'secret': secret,
|
||||||
})
|
})
|
||||||
|
self.api.enableRateLimit = True
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
raise ExchangeNotFoundError(exchange_name=exchange_name)
|
raise ExchangeNotFoundError(exchange_name=exchange_name)
|
||||||
@@ -70,6 +77,7 @@ class CCXT(Exchange):
|
|||||||
self.max_requests_per_minute = 60
|
self.max_requests_per_minute = 60
|
||||||
self.low_balance_threshold = 0.1
|
self.low_balance_threshold = 0.1
|
||||||
self.request_cpt = dict()
|
self.request_cpt = dict()
|
||||||
|
self._common_symbols = dict()
|
||||||
|
|
||||||
self.bundle = ExchangeBundle(self.name)
|
self.bundle = ExchangeBundle(self.name)
|
||||||
self.markets = None
|
self.markets = None
|
||||||
@@ -215,6 +223,21 @@ class CCXT(Exchange):
|
|||||||
)
|
)
|
||||||
return market
|
return market
|
||||||
|
|
||||||
|
def substitute_currency_code(self, currency, source='catalyst'):
|
||||||
|
if source == 'catalyst':
|
||||||
|
currency = currency.upper()
|
||||||
|
|
||||||
|
key = self.api.common_currency_code(currency)
|
||||||
|
self._common_symbols[key] = currency.lower()
|
||||||
|
return key
|
||||||
|
|
||||||
|
else:
|
||||||
|
if currency in self._common_symbols:
|
||||||
|
return self._common_symbols[currency]
|
||||||
|
|
||||||
|
else:
|
||||||
|
return currency.lower()
|
||||||
|
|
||||||
def get_symbol(self, asset_or_symbol, source='catalyst'):
|
def get_symbol(self, asset_or_symbol, source='catalyst'):
|
||||||
"""
|
"""
|
||||||
The CCXT symbol.
|
The CCXT symbol.
|
||||||
@@ -222,6 +245,7 @@ class CCXT(Exchange):
|
|||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
asset_or_symbol
|
asset_or_symbol
|
||||||
|
source
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -231,7 +255,13 @@ class CCXT(Exchange):
|
|||||||
if source == 'ccxt':
|
if source == 'ccxt':
|
||||||
if isinstance(asset_or_symbol, string_types):
|
if isinstance(asset_or_symbol, string_types):
|
||||||
parts = asset_or_symbol.split('/')
|
parts = asset_or_symbol.split('/')
|
||||||
return '{}_{}'.format(parts[0].lower(), parts[1].lower())
|
base_currency = self.substitute_currency_code(
|
||||||
|
parts[0], source
|
||||||
|
)
|
||||||
|
quote_currency = self.substitute_currency_code(
|
||||||
|
parts[1], source
|
||||||
|
)
|
||||||
|
return '{}_{}'.format(base_currency, quote_currency)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return asset_or_symbol.symbol
|
return asset_or_symbol.symbol
|
||||||
@@ -242,7 +272,13 @@ class CCXT(Exchange):
|
|||||||
) else asset_or_symbol.symbol
|
) else asset_or_symbol.symbol
|
||||||
|
|
||||||
parts = symbol.split('_')
|
parts = symbol.split('_')
|
||||||
return '{}/{}'.format(parts[0].upper(), parts[1].upper())
|
base_currency = self.substitute_currency_code(
|
||||||
|
parts[0], source
|
||||||
|
)
|
||||||
|
quote_currency = self.substitute_currency_code(
|
||||||
|
parts[1], source
|
||||||
|
)
|
||||||
|
return '{}/{}'.format(base_currency, quote_currency)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def map_frequency(value, source='ccxt', raise_error=True):
|
def map_frequency(value, source='ccxt', raise_error=True):
|
||||||
@@ -367,7 +403,7 @@ class CCXT(Exchange):
|
|||||||
timeframe, source='ccxt', raise_error=raise_error
|
timeframe, source='ccxt', raise_error=raise_error
|
||||||
)
|
)
|
||||||
|
|
||||||
def get_candles(self, freq, assets, bar_count=None, start_dt=None,
|
def get_candles(self, freq, assets, bar_count=1, start_dt=None,
|
||||||
end_dt=None):
|
end_dt=None):
|
||||||
is_single = (isinstance(assets, TradingPair))
|
is_single = (isinstance(assets, TradingPair))
|
||||||
if is_single:
|
if is_single:
|
||||||
@@ -376,17 +412,46 @@ class CCXT(Exchange):
|
|||||||
symbols = self.get_symbols(assets)
|
symbols = self.get_symbols(assets)
|
||||||
timeframe = CCXT.get_timeframe(freq)
|
timeframe = CCXT.get_timeframe(freq)
|
||||||
|
|
||||||
ms = None
|
if timeframe not in self.api.timeframes:
|
||||||
|
freqs = [CCXT.get_frequency(t) for t in self.api.timeframes]
|
||||||
|
raise UnsupportedHistoryFrequencyError(
|
||||||
|
exchange=self.name,
|
||||||
|
freq=freq,
|
||||||
|
freqs=freqs,
|
||||||
|
)
|
||||||
|
|
||||||
|
if start_dt is not None and end_dt is not None:
|
||||||
|
raise ValueError(
|
||||||
|
'Please provide either start_dt or end_dt, not both.'
|
||||||
|
)
|
||||||
|
|
||||||
|
elif end_dt is not None:
|
||||||
|
# Make sure that end_dt really wants data in the past
|
||||||
|
# if it's close to now, we skip the 'since' parameters to
|
||||||
|
# lower the probability of error
|
||||||
|
bars_to_now = pd.date_range(
|
||||||
|
end_dt, pd.Timestamp.utcnow(), freq=freq
|
||||||
|
)
|
||||||
|
# See: https://github.com/ccxt/ccxt/issues/1360
|
||||||
|
if len(bars_to_now) > 1 or self.name in ['poloniex']:
|
||||||
|
dt_range = get_periods_range(
|
||||||
|
end_dt=end_dt,
|
||||||
|
periods=bar_count,
|
||||||
|
freq=freq,
|
||||||
|
)
|
||||||
|
start_dt = dt_range[0]
|
||||||
|
|
||||||
|
since = None
|
||||||
if start_dt is not None:
|
if start_dt is not None:
|
||||||
delta = start_dt - get_epoch()
|
delta = start_dt - get_epoch()
|
||||||
ms = int(delta.total_seconds()) * 1000
|
since = int(delta.total_seconds()) * 1000
|
||||||
|
|
||||||
candles = dict()
|
candles = dict()
|
||||||
for asset in assets:
|
for index, asset in enumerate(assets):
|
||||||
ohlcvs = self.api.fetch_ohlcv(
|
ohlcvs = self.api.fetch_ohlcv(
|
||||||
symbol=symbols[0],
|
symbol=symbols[index],
|
||||||
timeframe=timeframe,
|
timeframe=timeframe,
|
||||||
since=ms,
|
since=since,
|
||||||
limit=bar_count,
|
limit=bar_count,
|
||||||
params={}
|
params={}
|
||||||
)
|
)
|
||||||
@@ -403,6 +468,9 @@ class CCXT(Exchange):
|
|||||||
close=ohlcv[4],
|
close=ohlcv[4],
|
||||||
volume=ohlcv[5]
|
volume=ohlcv[5]
|
||||||
))
|
))
|
||||||
|
candles[asset] = sorted(
|
||||||
|
candles[asset], key=lambda c: c['last_traded']
|
||||||
|
)
|
||||||
|
|
||||||
if is_single:
|
if is_single:
|
||||||
return six.next(six.itervalues(candles))
|
return six.next(six.itervalues(candles))
|
||||||
@@ -692,18 +760,23 @@ class CCXT(Exchange):
|
|||||||
|
|
||||||
side = 'buy' if amount > 0 else 'sell'
|
side = 'buy' if amount > 0 else 'sell'
|
||||||
if hasattr(self.api, 'amount_to_lots'):
|
if hasattr(self.api, 'amount_to_lots'):
|
||||||
adj_amount = self.api.amount_to_lots(
|
# TODO: is this right?
|
||||||
symbol=symbol,
|
if self.api.markets is None:
|
||||||
amount=abs(amount),
|
self.api.load_markets()
|
||||||
)
|
|
||||||
if adj_amount != abs(amount):
|
# https://github.com/ccxt/ccxt/issues/1483
|
||||||
log.info(
|
adj_amount = round(abs(amount), asset.decimals)
|
||||||
'adjusted order amount {} to {} based on lot size'.format(
|
market = self.api.markets[symbol]
|
||||||
abs(amount), adj_amount,
|
if 'lots' in market and market['lots'] > amount:
|
||||||
|
raise CreateOrderError(
|
||||||
|
exchange=self.name,
|
||||||
|
e='order amount lower than the smallest lot: {}'.format(
|
||||||
|
amount
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
adj_amount = abs(amount)
|
adj_amount = round(abs(amount), asset.decimals)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
result = self.api.create_order(
|
result = self.api.create_order(
|
||||||
@@ -725,6 +798,22 @@ class CCXT(Exchange):
|
|||||||
)
|
)
|
||||||
raise ExchangeRequestError(error=e)
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
exchange_amount = None
|
||||||
|
if 'amount' in result and result['amount'] != adj_amount:
|
||||||
|
exchange_amount = result['amount']
|
||||||
|
|
||||||
|
elif 'info' in result:
|
||||||
|
if 'origQty' in result['info']:
|
||||||
|
exchange_amount = float(result['info']['origQty'])
|
||||||
|
|
||||||
|
if exchange_amount:
|
||||||
|
log.info(
|
||||||
|
'order amount adjusted by {} from {} to {}'.format(
|
||||||
|
self.name, adj_amount, exchange_amount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
adj_amount = exchange_amount
|
||||||
|
|
||||||
if 'info' not in result:
|
if 'info' not in result:
|
||||||
raise ValueError('cannot use order without info attribute')
|
raise ValueError('cannot use order without info attribute')
|
||||||
|
|
||||||
@@ -759,13 +848,118 @@ class CCXT(Exchange):
|
|||||||
|
|
||||||
orders = []
|
orders = []
|
||||||
for order_status in result:
|
for order_status in result:
|
||||||
order, executed_price = self._create_order(order_status)
|
order, _ = self._create_order(order_status)
|
||||||
if asset is None or asset == order.sid:
|
if asset is None or asset == order.sid:
|
||||||
orders.append(order)
|
orders.append(order)
|
||||||
|
|
||||||
return orders
|
return orders
|
||||||
|
|
||||||
def get_order(self, order_id, asset_or_symbol=None):
|
def _process_order_fallback(self, order):
|
||||||
|
"""
|
||||||
|
Fallback method for exchanges which do not play nice with
|
||||||
|
fetch-my-trades. Apparently, about 60% of exchanges will return
|
||||||
|
the correct executed values with this method. Others will support
|
||||||
|
fetch-my-trades.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
order: Order
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
|
||||||
|
"""
|
||||||
|
exc_order, price = self.get_order(
|
||||||
|
order.id, order.asset, return_price=True
|
||||||
|
)
|
||||||
|
order.status = exc_order.status
|
||||||
|
order.commission = exc_order.commission
|
||||||
|
order.filled = exc_order.amount
|
||||||
|
|
||||||
|
transactions = []
|
||||||
|
if exc_order.status == ORDER_STATUS.FILLED:
|
||||||
|
if order.amount > exc_order.amount:
|
||||||
|
log.warn(
|
||||||
|
'executed order amount {} differs '
|
||||||
|
'from original'.format(
|
||||||
|
exc_order.amount, order.amount
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
order.check_triggers(
|
||||||
|
price=price,
|
||||||
|
dt=exc_order.dt,
|
||||||
|
)
|
||||||
|
transaction = Transaction(
|
||||||
|
asset=order.asset,
|
||||||
|
amount=order.amount,
|
||||||
|
dt=pd.Timestamp.utcnow(),
|
||||||
|
price=price,
|
||||||
|
order_id=order.id,
|
||||||
|
commission=order.commission,
|
||||||
|
)
|
||||||
|
transactions.append(transaction)
|
||||||
|
|
||||||
|
return transactions
|
||||||
|
|
||||||
|
def process_order(self, order):
|
||||||
|
# TODO: move to parent class after tracking features in the parent
|
||||||
|
if not self.api.has['fetchMyTrades']:
|
||||||
|
return self._process_order_fallback(order)
|
||||||
|
|
||||||
|
try:
|
||||||
|
all_trades = self.get_trades(order.asset)
|
||||||
|
except ExchangeRequestError as e:
|
||||||
|
log.warn(
|
||||||
|
'unable to fetch account trades, trying an alternate '
|
||||||
|
'method to find executed order {} / {}: {}'.format(
|
||||||
|
order.id, order.asset.symbol, e
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return self._process_order_fallback(order)
|
||||||
|
|
||||||
|
transactions = []
|
||||||
|
trades = [t for t in all_trades if t['order'] == order.id]
|
||||||
|
if not trades:
|
||||||
|
log.debug(
|
||||||
|
'order {} / {} not found in trades'.format(
|
||||||
|
order.id, order.asset.symbol
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return transactions
|
||||||
|
|
||||||
|
trades.sort(key=lambda t: t['timestamp'], reverse=False)
|
||||||
|
order.filled = 0
|
||||||
|
order.commission = 0
|
||||||
|
for trade in trades:
|
||||||
|
# status property will update automatically
|
||||||
|
filled = trade['amount'] * order.direction
|
||||||
|
order.filled += filled
|
||||||
|
|
||||||
|
commission = 0
|
||||||
|
if 'fee' in trade and 'cost' in trade['fee']:
|
||||||
|
commission = trade['fee']['cost']
|
||||||
|
order.commission += commission
|
||||||
|
|
||||||
|
order.check_triggers(
|
||||||
|
price=trade['price'],
|
||||||
|
dt=pd.to_datetime(trade['timestamp'], unit='ms', utc=True),
|
||||||
|
)
|
||||||
|
transaction = Transaction(
|
||||||
|
asset=order.asset,
|
||||||
|
amount=filled,
|
||||||
|
dt=pd.Timestamp.utcnow(),
|
||||||
|
price=trade['price'],
|
||||||
|
order_id=order.id,
|
||||||
|
commission=commission
|
||||||
|
)
|
||||||
|
transactions.append(transaction)
|
||||||
|
|
||||||
|
order.broker_order_id = ', '.join([t['id'] for t in trades])
|
||||||
|
return transactions
|
||||||
|
|
||||||
|
def get_order(self, order_id, asset_or_symbol=None, return_price=False):
|
||||||
if asset_or_symbol is None:
|
if asset_or_symbol is None:
|
||||||
log.debug(
|
log.debug(
|
||||||
'order not found in memory, the request might fail '
|
'order not found in memory, the request might fail '
|
||||||
@@ -777,6 +971,12 @@ class CCXT(Exchange):
|
|||||||
order_status = self.api.fetch_order(id=order_id, symbol=symbol)
|
order_status = self.api.fetch_order(id=order_id, symbol=symbol)
|
||||||
order, executed_price = self._create_order(order_status)
|
order, executed_price = self._create_order(order_status)
|
||||||
|
|
||||||
|
if return_price:
|
||||||
|
return order, executed_price
|
||||||
|
|
||||||
|
else:
|
||||||
|
return order
|
||||||
|
|
||||||
except (ExchangeError, NetworkError) as e:
|
except (ExchangeError, NetworkError) as e:
|
||||||
log.warn(
|
log.warn(
|
||||||
'unable to fetch order {} / {}: {}'.format(
|
'unable to fetch order {} / {}: {}'.format(
|
||||||
@@ -785,8 +985,6 @@ class CCXT(Exchange):
|
|||||||
)
|
)
|
||||||
raise ExchangeRequestError(error=e)
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
return order, executed_price
|
|
||||||
|
|
||||||
def cancel_order(self, order_param, asset_or_symbol=None):
|
def cancel_order(self, order_param, asset_or_symbol=None):
|
||||||
order_id = order_param.id \
|
order_id = order_param.id \
|
||||||
if isinstance(order_param, Order) else order_param
|
if isinstance(order_param, Order) else order_param
|
||||||
@@ -809,7 +1007,7 @@ class CCXT(Exchange):
|
|||||||
)
|
)
|
||||||
raise ExchangeRequestError(error=e)
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
def tickers(self, assets):
|
def tickers(self, assets, on_ticker_error='raise'):
|
||||||
"""
|
"""
|
||||||
Retrieve current tick data for the given assets
|
Retrieve current tick data for the given assets
|
||||||
|
|
||||||
@@ -822,48 +1020,70 @@ class CCXT(Exchange):
|
|||||||
list[dict[str, float]
|
list[dict[str, float]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
tickers = dict()
|
if len(assets) == 1:
|
||||||
try:
|
try:
|
||||||
for asset in assets:
|
symbol = self.get_symbol(assets[0])
|
||||||
symbol = self.get_symbol(asset)
|
log.debug('fetching single ticker: {}'.format(symbol))
|
||||||
# TODO: use fetch_tickers() for efficiency
|
results = dict()
|
||||||
# I tried using fetch_tickers() but noticed some
|
results[symbol] = self.api.fetch_ticker(symbol=symbol)
|
||||||
# inconsistencies, see issue:
|
|
||||||
# https://github.com/ccxt/ccxt/issues/870
|
|
||||||
ticker = self.api.fetch_ticker(symbol=symbol)
|
|
||||||
if not ticker:
|
|
||||||
log.warn('ticker not found for {} {}'.format(
|
|
||||||
self.name, symbol
|
|
||||||
))
|
|
||||||
continue
|
|
||||||
|
|
||||||
ticker['last_traded'] = from_ms_timestamp(ticker['timestamp'])
|
except (ExchangeError, NetworkError,) as e:
|
||||||
|
log.warn(
|
||||||
if 'last_price' not in ticker:
|
'unable to fetch ticker {} / {}: {}'.format(
|
||||||
# TODO: any more exceptions?
|
self.name, symbol, e
|
||||||
ticker['last_price'] = ticker['last']
|
)
|
||||||
|
|
||||||
if 'baseVolume' in ticker and ticker['baseVolume'] is not None:
|
|
||||||
# Using the volume represented in the base currency
|
|
||||||
ticker['volume'] = ticker['baseVolume']
|
|
||||||
|
|
||||||
elif 'info' in ticker and 'bidQty' in ticker['info'] \
|
|
||||||
and 'askQty' in ticker['info']:
|
|
||||||
ticker['volume'] = float(ticker['info']['bidQty']) + \
|
|
||||||
float(ticker['info']['askQty'])
|
|
||||||
|
|
||||||
else:
|
|
||||||
ticker['volume'] = 0
|
|
||||||
|
|
||||||
tickers[asset] = ticker
|
|
||||||
|
|
||||||
except (ExchangeError, NetworkError) as e:
|
|
||||||
log.warn(
|
|
||||||
'unable to fetch ticker {} / {}: {}'.format(
|
|
||||||
self.name, asset.symbol, e
|
|
||||||
)
|
)
|
||||||
)
|
raise ExchangeRequestError(error=e)
|
||||||
raise ExchangeRequestError(error=e)
|
|
||||||
|
elif len(assets) > 1:
|
||||||
|
symbols = self.get_symbols(assets)
|
||||||
|
try:
|
||||||
|
log.debug('fetching multiple tickers: {}'.format(symbols))
|
||||||
|
results = self.api.fetch_tickers(symbols=symbols)
|
||||||
|
|
||||||
|
except (ExchangeError, NetworkError) as e:
|
||||||
|
log.warn(
|
||||||
|
'unable to fetch tickers {} / {}: {}'.format(
|
||||||
|
self.name, symbols, e
|
||||||
|
)
|
||||||
|
)
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
else:
|
||||||
|
raise ValueError('Cannot request tickers with not assets.')
|
||||||
|
|
||||||
|
tickers = dict()
|
||||||
|
for asset in assets:
|
||||||
|
symbol = self.get_symbol(asset)
|
||||||
|
if symbol not in results:
|
||||||
|
msg = 'ticker not found {} / {}'.format(
|
||||||
|
self.name, symbol
|
||||||
|
)
|
||||||
|
log.warn(msg)
|
||||||
|
if on_ticker_error == 'warn':
|
||||||
|
continue
|
||||||
|
else:
|
||||||
|
raise ExchangeRequestError(error=msg)
|
||||||
|
|
||||||
|
ticker = results[symbol]
|
||||||
|
ticker['last_traded'] = from_ms_timestamp(ticker['timestamp'])
|
||||||
|
|
||||||
|
if 'last_price' not in ticker:
|
||||||
|
# TODO: any more exceptions?
|
||||||
|
ticker['last_price'] = ticker['last']
|
||||||
|
|
||||||
|
if 'baseVolume' in ticker and ticker['baseVolume'] is not None:
|
||||||
|
# Using the volume represented in the base currency
|
||||||
|
ticker['volume'] = ticker['baseVolume']
|
||||||
|
|
||||||
|
elif 'info' in ticker and 'bidQty' in ticker['info'] \
|
||||||
|
and 'askQty' in ticker['info']:
|
||||||
|
ticker['volume'] = float(ticker['info']['bidQty']) + \
|
||||||
|
float(ticker['info']['askQty'])
|
||||||
|
|
||||||
|
else:
|
||||||
|
ticker['volume'] = 0
|
||||||
|
|
||||||
|
tickers[asset] = ticker
|
||||||
|
|
||||||
return tickers
|
return tickers
|
||||||
|
|
||||||
@@ -893,3 +1113,27 @@ class CCXT(Exchange):
|
|||||||
))
|
))
|
||||||
|
|
||||||
return result
|
return result
|
||||||
|
|
||||||
|
def get_trades(self, asset, my_trades=True, start_dt=None, limit=100):
|
||||||
|
if not my_trades:
|
||||||
|
raise NotImplemented(
|
||||||
|
'get_trades only supports "my trades"'
|
||||||
|
)
|
||||||
|
|
||||||
|
# TODO: is it possible to sort this? Limit is useless otherwise.
|
||||||
|
ccxt_symbol = self.get_symbol(asset)
|
||||||
|
try:
|
||||||
|
trades = self.api.fetch_my_trades(
|
||||||
|
symbol=ccxt_symbol,
|
||||||
|
since=start_dt,
|
||||||
|
limit=limit,
|
||||||
|
)
|
||||||
|
except (ExchangeError, NetworkError) as e:
|
||||||
|
log.warn(
|
||||||
|
'unable to fetch trades {} / {}: {}'.format(
|
||||||
|
self.name, asset.symbol, e
|
||||||
|
)
|
||||||
|
)
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
return trades
|
||||||
|
|||||||
@@ -13,10 +13,11 @@ from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \
|
|||||||
PricingDataNotLoadedError, \
|
PricingDataNotLoadedError, \
|
||||||
NoDataAvailableOnExchange, NoValueForField, LastCandleTooEarlyError, \
|
NoDataAvailableOnExchange, NoValueForField, LastCandleTooEarlyError, \
|
||||||
TickerNotFoundError, NotEnoughCashError
|
TickerNotFoundError, NotEnoughCashError
|
||||||
from catalyst.exchange.utils.bundle_utils import get_start_dt, \
|
from catalyst.exchange.utils.datetime_utils import get_delta, \
|
||||||
get_delta, get_periods, get_periods_range
|
get_periods_range, \
|
||||||
|
get_periods, get_start_dt, get_frequency
|
||||||
from catalyst.exchange.utils.exchange_utils import get_exchange_symbols, \
|
from catalyst.exchange.utils.exchange_utils import get_exchange_symbols, \
|
||||||
get_frequency, resample_history_df, has_bundle
|
resample_history_df, has_bundle
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
log = Logger('Exchange', level=LOG_LEVEL)
|
log = Logger('Exchange', level=LOG_LEVEL)
|
||||||
@@ -177,6 +178,7 @@ class Exchange:
|
|||||||
if symbols is None:
|
if symbols is None:
|
||||||
# Make a distinct list of all symbols
|
# Make a distinct list of all symbols
|
||||||
symbols = list(set([asset.symbol for asset in self.assets]))
|
symbols = list(set([asset.symbol for asset in self.assets]))
|
||||||
|
symbols.sort()
|
||||||
|
|
||||||
if quote_currency is not None:
|
if quote_currency is not None:
|
||||||
for symbol in symbols[:]:
|
for symbol in symbols[:]:
|
||||||
@@ -233,11 +235,15 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
asset = None
|
asset = None
|
||||||
|
|
||||||
|
# TODO: temp mapping, fix to use a single symbol convention
|
||||||
|
og_symbol = symbol
|
||||||
|
symbol = self.get_symbol(symbol) if not is_exchange_symbol else symbol
|
||||||
log.debug(
|
log.debug(
|
||||||
'searching assets for: {} {}'.format(
|
'searching assets for: {} {}'.format(
|
||||||
self.name, symbol
|
self.name, symbol
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
# TODO: simplify and loose the loop
|
||||||
for a in self.assets:
|
for a in self.assets:
|
||||||
if asset is not None:
|
if asset is not None:
|
||||||
break
|
break
|
||||||
@@ -259,7 +265,8 @@ class Exchange:
|
|||||||
|
|
||||||
# The symbol provided may use the Catalyst or the exchange
|
# The symbol provided may use the Catalyst or the exchange
|
||||||
# convention
|
# convention
|
||||||
key = a.exchange_symbol if is_exchange_symbol else a.symbol
|
key = a.exchange_symbol if \
|
||||||
|
is_exchange_symbol else self.get_symbol(a)
|
||||||
if not asset and key.lower() == symbol.lower():
|
if not asset and key.lower() == symbol.lower():
|
||||||
if applies:
|
if applies:
|
||||||
asset = a
|
asset = a
|
||||||
@@ -275,7 +282,7 @@ class Exchange:
|
|||||||
supported_symbols = sorted([a.symbol for a in self.assets])
|
supported_symbols = sorted([a.symbol for a in self.assets])
|
||||||
|
|
||||||
raise SymbolNotFoundOnExchange(
|
raise SymbolNotFoundOnExchange(
|
||||||
symbol=symbol,
|
symbol=og_symbol,
|
||||||
exchange=self.name.title(),
|
exchange=self.name.title(),
|
||||||
supported_symbols=supported_symbols
|
supported_symbols=supported_symbols
|
||||||
)
|
)
|
||||||
@@ -433,7 +440,7 @@ class Exchange:
|
|||||||
series = pd.Series(values, index=dates)
|
series = pd.Series(values, index=dates)
|
||||||
|
|
||||||
periods = get_periods_range(
|
periods = get_periods_range(
|
||||||
start_dt, end_dt, data_frequency
|
start_dt=start_dt, end_dt=end_dt, freq=data_frequency
|
||||||
)
|
)
|
||||||
# TODO: ensure that this working as expected, if not use fillna
|
# TODO: ensure that this working as expected, if not use fillna
|
||||||
series = series.reindex(
|
series = series.reindex(
|
||||||
@@ -497,39 +504,37 @@ class Exchange:
|
|||||||
freq, candle_size, unit, data_frequency = get_frequency(
|
freq, candle_size, unit, data_frequency = get_frequency(
|
||||||
frequency, data_frequency
|
frequency, data_frequency
|
||||||
)
|
)
|
||||||
adj_bar_count = candle_size * bar_count
|
|
||||||
|
|
||||||
start_dt = get_start_dt(end_dt, adj_bar_count, data_frequency)
|
|
||||||
|
|
||||||
# The get_history method supports multiple asset
|
# The get_history method supports multiple asset
|
||||||
candles = self.get_candles(
|
candles = self.get_candles(
|
||||||
freq=freq,
|
freq=freq,
|
||||||
assets=assets,
|
assets=assets,
|
||||||
bar_count=bar_count,
|
bar_count=bar_count,
|
||||||
start_dt=start_dt if not is_current else None,
|
|
||||||
end_dt=end_dt if not is_current else None,
|
end_dt=end_dt if not is_current else None,
|
||||||
)
|
)
|
||||||
|
|
||||||
series = dict()
|
series = dict()
|
||||||
for asset in candles:
|
for asset in candles:
|
||||||
|
first_candle = candles[asset][0]
|
||||||
asset_series = self.get_series_from_candles(
|
asset_series = self.get_series_from_candles(
|
||||||
candles=candles[asset],
|
candles=candles[asset],
|
||||||
start_dt=start_dt,
|
start_dt=first_candle['last_traded'],
|
||||||
end_dt=end_dt,
|
end_dt=end_dt,
|
||||||
data_frequency=frequency,
|
data_frequency=frequency,
|
||||||
field=field,
|
field=field,
|
||||||
)
|
)
|
||||||
if end_dt is not None:
|
|
||||||
delta = get_delta(candle_size, data_frequency)
|
|
||||||
adj_end_dt = end_dt - delta
|
|
||||||
last_traded = asset_series.index[-1]
|
|
||||||
|
|
||||||
if last_traded < adj_end_dt:
|
# Checking to make sure that the dates match
|
||||||
raise LastCandleTooEarlyError(
|
delta = get_delta(candle_size, data_frequency)
|
||||||
last_traded=last_traded,
|
adj_end_dt = end_dt - delta
|
||||||
end_dt=adj_end_dt,
|
last_traded = asset_series.index[-1]
|
||||||
exchange=self.name,
|
|
||||||
)
|
if last_traded < adj_end_dt:
|
||||||
|
raise LastCandleTooEarlyError(
|
||||||
|
last_traded=last_traded,
|
||||||
|
end_dt=adj_end_dt,
|
||||||
|
exchange=self.name,
|
||||||
|
)
|
||||||
|
|
||||||
series[asset] = asset_series
|
series[asset] = asset_series
|
||||||
|
|
||||||
df = pd.DataFrame(series)
|
df = pd.DataFrame(series)
|
||||||
@@ -583,11 +588,11 @@ class Exchange:
|
|||||||
A dataframe containing the requested data.
|
A dataframe containing the requested data.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# TODO: this function needs some work, we're currently using it just for benchmark data
|
||||||
freq, candle_size, unit, data_frequency = get_frequency(
|
freq, candle_size, unit, data_frequency = get_frequency(
|
||||||
frequency, data_frequency
|
frequency, data_frequency
|
||||||
)
|
)
|
||||||
adj_bar_count = candle_size * bar_count
|
adj_bar_count = candle_size * bar_count
|
||||||
|
|
||||||
try:
|
try:
|
||||||
series = self.bundle.get_history_window_series_and_load(
|
series = self.bundle.get_history_window_series_and_load(
|
||||||
assets=assets,
|
assets=assets,
|
||||||
@@ -614,15 +619,14 @@ class Exchange:
|
|||||||
# The get_history method supports multiple asset
|
# The get_history method supports multiple asset
|
||||||
# Use the original frequency to let each api optimize
|
# Use the original frequency to let each api optimize
|
||||||
# the size of result sets
|
# the size of result sets
|
||||||
trailing_bar_count = get_periods(
|
trailing_bars = get_periods(
|
||||||
trailing_dt, end_dt, freq
|
trailing_dt, end_dt, freq
|
||||||
)
|
)
|
||||||
candles = self.get_candles(
|
candles = self.get_candles(
|
||||||
freq=freq,
|
freq=freq,
|
||||||
assets=asset,
|
assets=asset,
|
||||||
bar_count=trailing_bar_count,
|
end_dt=end_dt,
|
||||||
start_dt=start_dt,
|
bar_count=trailing_bars if trailing_bars < 500 else 500,
|
||||||
end_dt=end_dt
|
|
||||||
)
|
)
|
||||||
|
|
||||||
last_value = series[asset].iloc(0) if asset in series \
|
last_value = series[asset].iloc(0) if asset in series \
|
||||||
@@ -698,8 +702,8 @@ class Exchange:
|
|||||||
)
|
)
|
||||||
|
|
||||||
positions_value = 0.0
|
positions_value = 0.0
|
||||||
if positions is not None:
|
if positions:
|
||||||
assets = set([position.asset for position in positions])
|
assets = list(set([position.asset for position in positions]))
|
||||||
tickers = self.tickers(assets)
|
tickers = self.tickers(assets)
|
||||||
|
|
||||||
for position in positions:
|
for position in positions:
|
||||||
@@ -899,6 +903,22 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def process_order(self, order):
|
||||||
|
"""
|
||||||
|
Similar to get_order but looks only for executed orders.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
order: Order
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
float
|
||||||
|
Avg execution price
|
||||||
|
|
||||||
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def cancel_order(self, order_param, symbol_or_asset=None):
|
def cancel_order(self, order_param, symbol_or_asset=None):
|
||||||
"""Cancel an open order.
|
"""Cancel an open order.
|
||||||
@@ -913,8 +933,7 @@ class Exchange:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_candles(self, freq, assets, bar_count=None,
|
def get_candles(self, freq, assets, bar_count, start_dt=None, end_dt=None):
|
||||||
start_dt=None, end_dt=None):
|
|
||||||
"""
|
"""
|
||||||
Retrieve OHLCV candles for the given assets
|
Retrieve OHLCV candles for the given assets
|
||||||
|
|
||||||
@@ -954,13 +973,15 @@ class Exchange:
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def tickers(self, assets):
|
def tickers(self, assets, on_ticker_error='raise'):
|
||||||
"""
|
"""
|
||||||
Retrieve current tick data for the given assets
|
Retrieve current tick data for the given assets
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
assets: list[TradingPair]
|
assets: list[TradingPair]
|
||||||
|
on_ticker_error: str [raise|warn]
|
||||||
|
How to handle an error when retrieving a single ticker.
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -979,7 +1000,7 @@ class Exchange:
|
|||||||
@abc.abstractmethod
|
@abc.abstractmethod
|
||||||
def get_orderbook(self, asset, order_type, limit):
|
def get_orderbook(self, asset, order_type, limit):
|
||||||
"""
|
"""
|
||||||
Retrieve the the orderbook for the given trading pair.
|
Retrieve the orderbook for the given trading pair.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -993,3 +1014,20 @@ class Exchange:
|
|||||||
list[dict[str, float]
|
list[dict[str, float]
|
||||||
"""
|
"""
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_trades(self, asset, my_trades, start_dt, limit):
|
||||||
|
"""
|
||||||
|
Retrieve a list of trades.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
my_trades: bool
|
||||||
|
List only my trades.
|
||||||
|
start_dt
|
||||||
|
limit
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
|||||||
@@ -43,6 +43,7 @@ from catalyst.finance.execution import MarketOrder
|
|||||||
from catalyst.finance.performance import PerformanceTracker
|
from catalyst.finance.performance import PerformanceTracker
|
||||||
from catalyst.finance.performance.period import calc_period_stats
|
from catalyst.finance.performance.period import calc_period_stats
|
||||||
from catalyst.gens.tradesimulation import AlgorithmSimulator
|
from catalyst.gens.tradesimulation import AlgorithmSimulator
|
||||||
|
from catalyst.marketplace.marketplace import Marketplace
|
||||||
from catalyst.utils.api_support import api_method
|
from catalyst.utils.api_support import api_method
|
||||||
from catalyst.utils.input_validation import error_keywords, ensure_upper_case
|
from catalyst.utils.input_validation import error_keywords, ensure_upper_case
|
||||||
from catalyst.utils.math_utils import round_nearest
|
from catalyst.utils.math_utils import round_nearest
|
||||||
@@ -67,7 +68,7 @@ class ExchangeTradingAlgorithmBase(TradingAlgorithm):
|
|||||||
self.current_day = None
|
self.current_day = None
|
||||||
|
|
||||||
if self.simulate_orders is None \
|
if self.simulate_orders is None \
|
||||||
and self.sim_params.arena == 'backtest':
|
and self.sim_params.arena == 'backtest':
|
||||||
self.simulate_orders = True
|
self.simulate_orders = True
|
||||||
|
|
||||||
# Operations with retry features
|
# Operations with retry features
|
||||||
@@ -92,6 +93,8 @@ class ExchangeTradingAlgorithmBase(TradingAlgorithm):
|
|||||||
attempts=self.attempts,
|
attempts=self.attempts,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
self._marketplace = None
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def __convert_order_params_for_blotter(limit_price, stop_price, style):
|
def __convert_order_params_for_blotter(limit_price, stop_price, style):
|
||||||
"""
|
"""
|
||||||
@@ -115,7 +118,7 @@ class ExchangeTradingAlgorithmBase(TradingAlgorithm):
|
|||||||
# be in-line with CXXT and many exchanges. We'll consider
|
# be in-line with CXXT and many exchanges. We'll consider
|
||||||
# adding more order types in the future.
|
# adding more order types in the future.
|
||||||
if not isinstance(style, ExchangeLimitOrder) or \
|
if not isinstance(style, ExchangeLimitOrder) or \
|
||||||
not isinstance(style, MarketOrder):
|
not isinstance(style, MarketOrder):
|
||||||
raise OrderTypeNotSupported(
|
raise OrderTypeNotSupported(
|
||||||
order_type=style.__class__.__name__
|
order_type=style.__class__.__name__
|
||||||
)
|
)
|
||||||
@@ -167,6 +170,15 @@ class ExchangeTradingAlgorithmBase(TradingAlgorithm):
|
|||||||
"""
|
"""
|
||||||
return round_nearest(amount, asset.min_trade_size)
|
return round_nearest(amount, asset.min_trade_size)
|
||||||
|
|
||||||
|
@api_method
|
||||||
|
def get_dataset(self, data_source_name, start=None, end=None):
|
||||||
|
if self._marketplace is None:
|
||||||
|
self._marketplace = Marketplace()
|
||||||
|
|
||||||
|
return self._marketplace.get_dataset(
|
||||||
|
data_source_name, start, end,
|
||||||
|
)
|
||||||
|
|
||||||
@api_method
|
@api_method
|
||||||
@preprocess(symbol_str=ensure_upper_case)
|
@preprocess(symbol_str=ensure_upper_case)
|
||||||
def symbol(self, symbol_str, exchange_name=None):
|
def symbol(self, symbol_str, exchange_name=None):
|
||||||
@@ -303,6 +315,7 @@ class ExchangeTradingAlgorithmBacktest(ExchangeTradingAlgorithmBase):
|
|||||||
super(ExchangeTradingAlgorithmBacktest, self).__init__(*args, **kwargs)
|
super(ExchangeTradingAlgorithmBacktest, self).__init__(*args, **kwargs)
|
||||||
|
|
||||||
self.frame_stats = list()
|
self.frame_stats = list()
|
||||||
|
self.state = {}
|
||||||
log.info('initialized trading algorithm in backtest mode')
|
log.info('initialized trading algorithm in backtest mode')
|
||||||
|
|
||||||
def is_last_frame_of_day(self, data):
|
def is_last_frame_of_day(self, data):
|
||||||
@@ -350,6 +363,7 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
|||||||
self.live_graph = kwargs.pop('live_graph', None)
|
self.live_graph = kwargs.pop('live_graph', None)
|
||||||
self.stats_output = kwargs.pop('stats_output', None)
|
self.stats_output = kwargs.pop('stats_output', None)
|
||||||
self._analyze_live = kwargs.pop('analyze_live', None)
|
self._analyze_live = kwargs.pop('analyze_live', None)
|
||||||
|
self.end = kwargs.pop('end', None)
|
||||||
|
|
||||||
self._clock = None
|
self._clock = None
|
||||||
self.frame_stats = list()
|
self.frame_stats = list()
|
||||||
@@ -470,6 +484,13 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
|||||||
This allows us to stop/start algos without loosing their state.
|
This allows us to stop/start algos without loosing their state.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
self.state = get_algo_object(
|
||||||
|
algo_name=self.algo_namespace,
|
||||||
|
key='context.state',
|
||||||
|
)
|
||||||
|
if self.state is None:
|
||||||
|
self.state = {}
|
||||||
|
|
||||||
if self.perf_tracker is None:
|
if self.perf_tracker is None:
|
||||||
# Note from the Zipline dev:
|
# Note from the Zipline dev:
|
||||||
# HACK: When running with the `run` method, we set perf_tracker to
|
# HACK: When running with the `run` method, we set perf_tracker to
|
||||||
@@ -702,6 +723,10 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
|||||||
if not self.is_running:
|
if not self.is_running:
|
||||||
return
|
return
|
||||||
|
|
||||||
|
if self.end is not None and self.end < data.current_dt:
|
||||||
|
log.info('Algorithm has reached specified end time. Finishing...')
|
||||||
|
self.interrupt_algorithm()
|
||||||
|
|
||||||
# Resetting the frame stats every day to minimize memory footprint
|
# Resetting the frame stats every day to minimize memory footprint
|
||||||
today = data.current_dt.floor('1D')
|
today = data.current_dt.floor('1D')
|
||||||
if self.current_day is not None and today > self.current_day:
|
if self.current_day is not None and today > self.current_day:
|
||||||
@@ -765,6 +790,11 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
|||||||
obj=self.perf_tracker.todays_performance,
|
obj=self.perf_tracker.todays_performance,
|
||||||
rel_path='daily_performance'
|
rel_path='daily_performance'
|
||||||
)
|
)
|
||||||
|
log.debug('saving context.state object')
|
||||||
|
save_algo_object(
|
||||||
|
algo_name=self.algo_namespace,
|
||||||
|
key='context.state',
|
||||||
|
obj=self.state)
|
||||||
|
|
||||||
def _process_stats(self, data):
|
def _process_stats(self, data):
|
||||||
today = data.current_dt.floor('1D')
|
today = data.current_dt.floor('1D')
|
||||||
@@ -883,7 +913,8 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
|||||||
sleeptime=self.attempts['retry_sleeptime'],
|
sleeptime=self.attempts['retry_sleeptime'],
|
||||||
retry_exceptions=(ExchangeRequestError,),
|
retry_exceptions=(ExchangeRequestError,),
|
||||||
cleanup=lambda: log.warn('Fetching open orders again.'),
|
cleanup=lambda: log.warn('Fetching open orders again.'),
|
||||||
args=(asset,))
|
args=(asset,)
|
||||||
|
)
|
||||||
|
|
||||||
@api_method
|
@api_method
|
||||||
def get_order(self, order_id, exchange_name):
|
def get_order(self, order_id, exchange_name):
|
||||||
|
|||||||
@@ -1,4 +1,8 @@
|
|||||||
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from logbook import Logger
|
||||||
|
from redo import retry
|
||||||
|
|
||||||
from catalyst.assets._assets import TradingPair
|
from catalyst.assets._assets import TradingPair
|
||||||
from catalyst.constants import LOG_LEVEL
|
from catalyst.constants import LOG_LEVEL
|
||||||
from catalyst.exchange.exchange_errors import ExchangeRequestError
|
from catalyst.exchange.exchange_errors import ExchangeRequestError
|
||||||
@@ -8,8 +12,6 @@ from catalyst.finance.order import ORDER_STATUS
|
|||||||
from catalyst.finance.slippage import SlippageModel
|
from catalyst.finance.slippage import SlippageModel
|
||||||
from catalyst.finance.transaction import create_transaction, Transaction
|
from catalyst.finance.transaction import create_transaction, Transaction
|
||||||
from catalyst.utils.input_validation import expect_types
|
from catalyst.utils.input_validation import expect_types
|
||||||
from logbook import Logger
|
|
||||||
from redo import retry
|
|
||||||
|
|
||||||
log = Logger('exchange_blotter', level=LOG_LEVEL)
|
log = Logger('exchange_blotter', level=LOG_LEVEL)
|
||||||
|
|
||||||
@@ -93,7 +95,6 @@ class TradingPairFixedSlippage(SlippageModel):
|
|||||||
|
|
||||||
def simulate(self, data, asset, orders_for_asset):
|
def simulate(self, data, asset, orders_for_asset):
|
||||||
self._volume_for_bar = 0
|
self._volume_for_bar = 0
|
||||||
|
|
||||||
price = data.current(asset, 'close')
|
price = data.current(asset, 'close')
|
||||||
|
|
||||||
dt = data.current_dt
|
dt = data.current_dt
|
||||||
@@ -103,18 +104,20 @@ class TradingPairFixedSlippage(SlippageModel):
|
|||||||
|
|
||||||
order.check_triggers(price, dt)
|
order.check_triggers(price, dt)
|
||||||
if not order.triggered:
|
if not order.triggered:
|
||||||
log.debug('order has not reached the trigger at current '
|
log.info(
|
||||||
'price {}'.format(price))
|
'order has not reached the trigger at current '
|
||||||
|
'price {}'.format(price)
|
||||||
|
)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
execution_price, execution_volume = self.process_order(data, order)
|
execution_price, execution_volume = self.process_order(data, order)
|
||||||
|
if execution_price is not None:
|
||||||
|
transaction = create_transaction(
|
||||||
|
order, dt, execution_price, execution_volume
|
||||||
|
)
|
||||||
|
|
||||||
transaction = create_transaction(
|
self._volume_for_bar += abs(transaction.amount)
|
||||||
order, dt, execution_price, execution_volume
|
yield order, transaction
|
||||||
)
|
|
||||||
|
|
||||||
self._volume_for_bar += abs(transaction.amount)
|
|
||||||
yield order, transaction
|
|
||||||
|
|
||||||
def process_order(self, data, order):
|
def process_order(self, data, order):
|
||||||
price = data.current(order.asset, 'close')
|
price = data.current(order.asset, 'close')
|
||||||
@@ -205,34 +208,29 @@ class ExchangeBlotter(Blotter):
|
|||||||
for order in self.open_orders[asset]:
|
for order in self.open_orders[asset]:
|
||||||
log.debug('found open order: {}'.format(order.id))
|
log.debug('found open order: {}'.format(order.id))
|
||||||
|
|
||||||
new_order, executed_price = exchange.get_order(order.id, asset)
|
transactions = exchange.process_order(order)
|
||||||
log.debug(
|
# This is a temporary measure, we should really update all
|
||||||
'got updated order {} {}'.format(
|
# trades, not just when the order gets filled. I just think
|
||||||
new_order, executed_price
|
# that this is safer until we have a robust way to track
|
||||||
|
# the trades already processed by the algo. We can't loose
|
||||||
|
# them if the algo shuts down.
|
||||||
|
if transactions and order.status == ORDER_STATUS.FILLED:
|
||||||
|
avg_price = np.average(
|
||||||
|
a=[t.price for t in transactions],
|
||||||
|
weights=[t.amount for t in transactions],
|
||||||
)
|
)
|
||||||
)
|
ostatus = 'filled' if order.open_amount == 0 else 'partial'
|
||||||
order.status = new_order.status
|
log.info(
|
||||||
|
'{} order {} / {}: {}, avg price: {}'.format(
|
||||||
if order.status == ORDER_STATUS.FILLED:
|
ostatus,
|
||||||
order.commission = new_order.commission
|
order.id,
|
||||||
if order.amount != new_order.amount:
|
asset.symbol,
|
||||||
log.warn(
|
order.filled,
|
||||||
'executed order amount {} differs '
|
avg_price,
|
||||||
'from original'.format(
|
|
||||||
new_order.amount, order.amount
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
order.amount = new_order.amount
|
|
||||||
|
|
||||||
transaction = Transaction(
|
|
||||||
asset=order.asset,
|
|
||||||
amount=order.amount,
|
|
||||||
dt=pd.Timestamp.utcnow(),
|
|
||||||
price=executed_price,
|
|
||||||
order_id=order.id,
|
|
||||||
commission=order.commission
|
|
||||||
)
|
)
|
||||||
yield order, transaction
|
for transaction in transactions:
|
||||||
|
yield order, transaction
|
||||||
|
|
||||||
elif order.status == ORDER_STATUS.CANCELLED:
|
elif order.status == ORDER_STATUS.CANCELLED:
|
||||||
yield order, None
|
yield order, None
|
||||||
@@ -253,8 +251,6 @@ class ExchangeBlotter(Blotter):
|
|||||||
|
|
||||||
for order, txn in self.check_open_orders():
|
for order, txn in self.check_open_orders():
|
||||||
order.dt = txn.dt
|
order.dt = txn.dt
|
||||||
|
|
||||||
# TODO: is the commission already on the order object?
|
|
||||||
transactions.append(txn)
|
transactions.append(txn)
|
||||||
|
|
||||||
if not order.open:
|
if not order.open:
|
||||||
|
|||||||
@@ -21,9 +21,9 @@ from catalyst.exchange.exchange_errors import EmptyValuesInBundleError, \
|
|||||||
NoDataAvailableOnExchange, \
|
NoDataAvailableOnExchange, \
|
||||||
PricingDataNotLoadedError, DataCorruptionError, PricingDataValueError
|
PricingDataNotLoadedError, DataCorruptionError, PricingDataValueError
|
||||||
from catalyst.exchange.utils.bundle_utils import range_in_bundle, \
|
from catalyst.exchange.utils.bundle_utils import range_in_bundle, \
|
||||||
get_bcolz_chunk, get_month_start_end, \
|
get_bcolz_chunk, get_df_from_arrays, get_assets
|
||||||
get_year_start_end, get_df_from_arrays, get_start_dt, get_period_label, \
|
from catalyst.exchange.utils.datetime_utils import get_delta, get_start_dt, \
|
||||||
get_delta, get_assets
|
get_period_label, get_month_start_end, get_year_start_end
|
||||||
from catalyst.exchange.utils.exchange_utils import get_exchange_folder, \
|
from catalyst.exchange.utils.exchange_utils import get_exchange_folder, \
|
||||||
save_exchange_symbols, mixin_market_params, get_catalyst_symbol
|
save_exchange_symbols, mixin_market_params, get_catalyst_symbol
|
||||||
from catalyst.utils.cli import maybe_show_progress
|
from catalyst.utils.cli import maybe_show_progress
|
||||||
@@ -232,12 +232,12 @@ class ExchangeBundle:
|
|||||||
|
|
||||||
problem = '{name} ({start_dt} to {end_dt}) has empty ' \
|
problem = '{name} ({start_dt} to {end_dt}) has empty ' \
|
||||||
'periods: {dates}'.format(
|
'periods: {dates}'.format(
|
||||||
name=asset.symbol,
|
name=asset.symbol,
|
||||||
start_dt=asset.start_date.strftime(
|
start_dt=asset.start_date.strftime(
|
||||||
DATE_TIME_FORMAT),
|
DATE_TIME_FORMAT),
|
||||||
end_dt=end_dt.strftime(DATE_TIME_FORMAT),
|
end_dt=end_dt.strftime(DATE_TIME_FORMAT),
|
||||||
dates=[date.strftime(
|
dates=[date.strftime(
|
||||||
DATE_TIME_FORMAT) for date in dates])
|
DATE_TIME_FORMAT) for date in dates])
|
||||||
|
|
||||||
if empty_rows_behavior == 'warn':
|
if empty_rows_behavior == 'warn':
|
||||||
log.warn(problem)
|
log.warn(problem)
|
||||||
@@ -286,12 +286,12 @@ class ExchangeBundle:
|
|||||||
|
|
||||||
problem = '{name} ({start_dt} to {end_dt}) has {threshold} ' \
|
problem = '{name} ({start_dt} to {end_dt}) has {threshold} ' \
|
||||||
'identical close values on: {dates}'.format(
|
'identical close values on: {dates}'.format(
|
||||||
name=asset.symbol,
|
name=asset.symbol,
|
||||||
start_dt=asset.start_date.strftime(DATE_TIME_FORMAT),
|
start_dt=asset.start_date.strftime(DATE_TIME_FORMAT),
|
||||||
end_dt=end_dt.strftime(DATE_TIME_FORMAT),
|
end_dt=end_dt.strftime(DATE_TIME_FORMAT),
|
||||||
threshold=threshold,
|
threshold=threshold,
|
||||||
dates=[pd.to_datetime(date).strftime(DATE_TIME_FORMAT)
|
dates=[pd.to_datetime(date).strftime(DATE_TIME_FORMAT)
|
||||||
for date in dates])
|
for date in dates])
|
||||||
|
|
||||||
problems.append(problem)
|
problems.append(problem)
|
||||||
|
|
||||||
@@ -629,8 +629,8 @@ class ExchangeBundle:
|
|||||||
show_progress,
|
show_progress,
|
||||||
label='Ingesting {frequency} price data on '
|
label='Ingesting {frequency} price data on '
|
||||||
'{exchange}'.format(
|
'{exchange}'.format(
|
||||||
exchange=self.exchange_name,
|
exchange=self.exchange_name,
|
||||||
frequency=data_frequency,
|
frequency=data_frequency,
|
||||||
)) as it:
|
)) as it:
|
||||||
for chunk in it:
|
for chunk in it:
|
||||||
problems += self.ingest_ctable(
|
problems += self.ingest_ctable(
|
||||||
@@ -964,15 +964,15 @@ class ExchangeBundle:
|
|||||||
data_frequency,
|
data_frequency,
|
||||||
trailing_bar_count=None,
|
trailing_bar_count=None,
|
||||||
reset_reader=False):
|
reset_reader=False):
|
||||||
|
if trailing_bar_count:
|
||||||
|
delta = get_delta(trailing_bar_count, data_frequency)
|
||||||
|
end_dt += delta
|
||||||
|
|
||||||
start_dt = get_start_dt(end_dt, bar_count, data_frequency, False)
|
start_dt = get_start_dt(end_dt, bar_count, data_frequency, False)
|
||||||
start_dt, _ = self.get_adj_dates(
|
start_dt, _ = self.get_adj_dates(
|
||||||
start_dt, end_dt, assets, data_frequency
|
start_dt, end_dt, assets, data_frequency
|
||||||
)
|
)
|
||||||
|
|
||||||
if trailing_bar_count:
|
|
||||||
delta = get_delta(trailing_bar_count, data_frequency)
|
|
||||||
end_dt += delta
|
|
||||||
|
|
||||||
# This is an attempt to resolve some caching with the reader
|
# This is an attempt to resolve some caching with the reader
|
||||||
# when auto-ingesting data.
|
# when auto-ingesting data.
|
||||||
# TODO: needs more work
|
# TODO: needs more work
|
||||||
|
|||||||
@@ -9,8 +9,8 @@ from catalyst.exchange.exchange_bundle import ExchangeBundle
|
|||||||
from catalyst.exchange.exchange_errors import (
|
from catalyst.exchange.exchange_errors import (
|
||||||
ExchangeRequestError,
|
ExchangeRequestError,
|
||||||
PricingDataNotLoadedError)
|
PricingDataNotLoadedError)
|
||||||
from catalyst.exchange.utils.exchange_utils import get_frequency, \
|
from catalyst.exchange.utils.exchange_utils import resample_history_df, group_assets_by_exchange
|
||||||
resample_history_df, group_assets_by_exchange
|
from catalyst.exchange.utils.datetime_utils import get_frequency
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
from redo import retry
|
from redo import retry
|
||||||
|
|
||||||
@@ -291,6 +291,7 @@ class DataPortalExchangeBacktest(DataPortalExchangeBase):
|
|||||||
DataFrame
|
DataFrame
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
# TODO: verify that the exchange supports the timeframe
|
||||||
bundle = self.exchange_bundles[exchange_name] # type: ExchangeBundle
|
bundle = self.exchange_bundles[exchange_name] # type: ExchangeBundle
|
||||||
|
|
||||||
freq, candle_size, unit, adj_data_frequency = get_frequency(
|
freq, candle_size, unit, adj_data_frequency = get_frequency(
|
||||||
|
|||||||
@@ -100,6 +100,13 @@ class InvalidHistoryFrequencyError(ZiplineError):
|
|||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class UnsupportedHistoryFrequencyError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'{exchange} does not support candle frequency {freq}, please choose '
|
||||||
|
'from: {freqs}.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
class InvalidHistoryTimeframeError(ZiplineError):
|
class InvalidHistoryTimeframeError(ZiplineError):
|
||||||
msg = (
|
msg = (
|
||||||
'CCXT timeframe {timeframe} not supported by the exchange.'
|
'CCXT timeframe {timeframe} not supported by the exchange.'
|
||||||
|
|||||||
@@ -1,11 +1,19 @@
|
|||||||
import calendar
|
|
||||||
import os
|
import os
|
||||||
import tarfile
|
import tarfile
|
||||||
from datetime import timedelta, datetime, date
|
from datetime import datetime
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytz
|
|
||||||
|
from catalyst.data.bundles.core import download_without_progress
|
||||||
|
from catalyst.exchange.utils.exchange_utils import get_exchange_bundles_folder
|
||||||
|
import os
|
||||||
|
import tarfile
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
from catalyst.data.bundles.core import download_without_progress
|
from catalyst.data.bundles.core import download_without_progress
|
||||||
from catalyst.exchange.utils.exchange_utils import get_exchange_bundles_folder
|
from catalyst.exchange.utils.exchange_utils import get_exchange_bundles_folder
|
||||||
|
|
||||||
@@ -13,41 +21,6 @@ EXCHANGE_NAMES = ['bitfinex', 'bittrex', 'poloniex']
|
|||||||
API_URL = 'http://data.enigma.co/api/v1'
|
API_URL = 'http://data.enigma.co/api/v1'
|
||||||
|
|
||||||
|
|
||||||
def get_date_from_ms(ms):
|
|
||||||
"""
|
|
||||||
The date from the number of miliseconds from the epoch.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
ms: int
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
datetime
|
|
||||||
|
|
||||||
"""
|
|
||||||
return datetime.fromtimestamp(ms / 1000.0)
|
|
||||||
|
|
||||||
|
|
||||||
def get_seconds_from_date(date):
|
|
||||||
"""
|
|
||||||
The number of seconds from the epoch.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
date: datetime
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
int
|
|
||||||
|
|
||||||
"""
|
|
||||||
epoch = datetime.utcfromtimestamp(0)
|
|
||||||
epoch = epoch.replace(tzinfo=pytz.UTC)
|
|
||||||
|
|
||||||
return int((date - epoch).total_seconds())
|
|
||||||
|
|
||||||
|
|
||||||
def get_bcolz_chunk(exchange_name, symbol, data_frequency, period):
|
def get_bcolz_chunk(exchange_name, symbol, data_frequency, period):
|
||||||
"""
|
"""
|
||||||
Download and extract a bcolz bundle.
|
Download and extract a bcolz bundle.
|
||||||
@@ -77,8 +50,8 @@ def get_bcolz_chunk(exchange_name, symbol, data_frequency, period):
|
|||||||
if not os.path.isdir(path):
|
if not os.path.isdir(path):
|
||||||
url = 'https://s3.amazonaws.com/enigmaco/catalyst-bundles/' \
|
url = 'https://s3.amazonaws.com/enigmaco/catalyst-bundles/' \
|
||||||
'exchange-{exchange}/{name}.tar.gz'.format(
|
'exchange-{exchange}/{name}.tar.gz'.format(
|
||||||
exchange=exchange_name,
|
exchange=exchange_name,
|
||||||
name=name)
|
name=name)
|
||||||
|
|
||||||
bytes = download_without_progress(url)
|
bytes = download_without_progress(url)
|
||||||
with tarfile.open('r', fileobj=bytes) as tar:
|
with tarfile.open('r', fileobj=bytes) as tar:
|
||||||
@@ -87,178 +60,6 @@ def get_bcolz_chunk(exchange_name, symbol, data_frequency, period):
|
|||||||
return path
|
return path
|
||||||
|
|
||||||
|
|
||||||
def get_delta(periods, data_frequency):
|
|
||||||
"""
|
|
||||||
Get a time delta based on the specified data frequency.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
periods: int
|
|
||||||
data_frequency: str
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
timedelta
|
|
||||||
|
|
||||||
"""
|
|
||||||
return timedelta(minutes=periods) \
|
|
||||||
if data_frequency == 'minute' else timedelta(days=periods)
|
|
||||||
|
|
||||||
|
|
||||||
def get_periods_range(start_dt, end_dt, freq):
|
|
||||||
"""
|
|
||||||
Get a date range for the specified parameters.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
start_dt: datetime
|
|
||||||
end_dt: datetime
|
|
||||||
freq: str
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
DateTimeIndex
|
|
||||||
|
|
||||||
"""
|
|
||||||
if freq == 'minute':
|
|
||||||
freq = 'T'
|
|
||||||
|
|
||||||
elif freq == 'daily':
|
|
||||||
freq = 'D'
|
|
||||||
|
|
||||||
return pd.date_range(start_dt, end_dt, freq=freq)
|
|
||||||
|
|
||||||
|
|
||||||
def get_periods(start_dt, end_dt, freq):
|
|
||||||
"""
|
|
||||||
The number of periods in the specified range.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
start_dt: datetime
|
|
||||||
end_dt: datetime
|
|
||||||
freq: str
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
int
|
|
||||||
|
|
||||||
"""
|
|
||||||
return len(get_periods_range(start_dt, end_dt, freq))
|
|
||||||
|
|
||||||
|
|
||||||
def get_start_dt(end_dt, bar_count, data_frequency, include_first=True):
|
|
||||||
"""
|
|
||||||
The start date based on specified end date and data frequency.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
end_dt: datetime
|
|
||||||
bar_count: int
|
|
||||||
data_frequency: str
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
datetime
|
|
||||||
|
|
||||||
"""
|
|
||||||
periods = bar_count
|
|
||||||
if periods > 1:
|
|
||||||
delta = get_delta(periods, data_frequency)
|
|
||||||
start_dt = end_dt - delta
|
|
||||||
|
|
||||||
if not include_first:
|
|
||||||
start_dt += get_delta(1, data_frequency)
|
|
||||||
else:
|
|
||||||
start_dt = end_dt
|
|
||||||
|
|
||||||
return start_dt
|
|
||||||
|
|
||||||
|
|
||||||
def get_period_label(dt, data_frequency):
|
|
||||||
"""
|
|
||||||
The period label for the specified date and frequency.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
dt: datetime
|
|
||||||
data_frequency: str
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
str
|
|
||||||
|
|
||||||
"""
|
|
||||||
if data_frequency == 'minute':
|
|
||||||
return '{}-{:02d}'.format(dt.year, dt.month)
|
|
||||||
else:
|
|
||||||
return '{}'.format(dt.year)
|
|
||||||
|
|
||||||
|
|
||||||
def get_month_start_end(dt, first_day=None, last_day=None):
|
|
||||||
"""
|
|
||||||
The first and last day of the month for the specified date.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
dt: datetime
|
|
||||||
first_day: datetime
|
|
||||||
last_day: datetime
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
datetime, datetime
|
|
||||||
|
|
||||||
"""
|
|
||||||
month_range = calendar.monthrange(dt.year, dt.month)
|
|
||||||
|
|
||||||
if first_day:
|
|
||||||
month_start = first_day
|
|
||||||
else:
|
|
||||||
month_start = pd.to_datetime(datetime(
|
|
||||||
dt.year, dt.month, 1, 0, 0, 0, 0
|
|
||||||
), utc=True)
|
|
||||||
|
|
||||||
if last_day:
|
|
||||||
month_end = last_day
|
|
||||||
else:
|
|
||||||
month_end = pd.to_datetime(datetime(
|
|
||||||
dt.year, dt.month, month_range[1], 23, 59, 0, 0
|
|
||||||
), utc=True)
|
|
||||||
|
|
||||||
if month_end > pd.Timestamp.utcnow():
|
|
||||||
month_end = pd.Timestamp.utcnow().floor('1D')
|
|
||||||
|
|
||||||
return month_start, month_end
|
|
||||||
|
|
||||||
|
|
||||||
def get_year_start_end(dt, first_day=None, last_day=None):
|
|
||||||
"""
|
|
||||||
The first and last day of the year for the specified date.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
|
|
||||||
dt: datetime
|
|
||||||
first_day: datetime
|
|
||||||
last_day: datetime
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
datetime, datetime
|
|
||||||
|
|
||||||
"""
|
|
||||||
year_start = first_day if first_day \
|
|
||||||
else pd.to_datetime(date(dt.year, 1, 1), utc=True)
|
|
||||||
year_end = last_day if last_day \
|
|
||||||
else pd.to_datetime(date(dt.year, 12, 31), utc=True)
|
|
||||||
|
|
||||||
if year_end > pd.Timestamp.utcnow():
|
|
||||||
year_end = pd.Timestamp.utcnow().floor('1D')
|
|
||||||
|
|
||||||
return year_start, year_end
|
|
||||||
|
|
||||||
|
|
||||||
def get_df_from_arrays(arrays, periods):
|
def get_df_from_arrays(arrays, periods):
|
||||||
"""
|
"""
|
||||||
A DataFrame from the specified OHCLV arrays.
|
A DataFrame from the specified OHCLV arrays.
|
||||||
|
|||||||
@@ -0,0 +1,327 @@
|
|||||||
|
import calendar
|
||||||
|
import re
|
||||||
|
from datetime import datetime, timedelta, date
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
import pytz
|
||||||
|
|
||||||
|
from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \
|
||||||
|
InvalidHistoryFrequencyAlias
|
||||||
|
|
||||||
|
|
||||||
|
def get_date_from_ms(ms):
|
||||||
|
"""
|
||||||
|
The date from the number of miliseconds from the epoch.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ms: int
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
datetime
|
||||||
|
|
||||||
|
"""
|
||||||
|
return datetime.fromtimestamp(ms / 1000.0)
|
||||||
|
|
||||||
|
|
||||||
|
def get_seconds_from_date(date):
|
||||||
|
"""
|
||||||
|
The number of seconds from the epoch.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
date: datetime
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
|
||||||
|
"""
|
||||||
|
epoch = datetime.utcfromtimestamp(0)
|
||||||
|
epoch = epoch.replace(tzinfo=pytz.UTC)
|
||||||
|
|
||||||
|
return int((date - epoch).total_seconds())
|
||||||
|
|
||||||
|
|
||||||
|
def get_delta(periods, data_frequency):
|
||||||
|
"""
|
||||||
|
Get a time delta based on the specified data frequency.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
periods: int
|
||||||
|
data_frequency: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
timedelta
|
||||||
|
|
||||||
|
"""
|
||||||
|
return timedelta(minutes=periods) \
|
||||||
|
if data_frequency == 'minute' else timedelta(days=periods)
|
||||||
|
|
||||||
|
|
||||||
|
def get_periods_range(freq, start_dt=None, end_dt=None, periods=None):
|
||||||
|
"""
|
||||||
|
Get a date range for the specified parameters.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
start_dt: datetime
|
||||||
|
end_dt: datetime
|
||||||
|
freq: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
DateTimeIndex
|
||||||
|
|
||||||
|
"""
|
||||||
|
if freq == 'minute':
|
||||||
|
freq = 'T'
|
||||||
|
|
||||||
|
elif freq == 'daily':
|
||||||
|
freq = 'D'
|
||||||
|
|
||||||
|
if start_dt is not None and end_dt is not None and periods is None:
|
||||||
|
|
||||||
|
return pd.date_range(start_dt, end_dt, freq=freq)
|
||||||
|
|
||||||
|
elif periods is not None and (start_dt is not None or end_dt is not None):
|
||||||
|
_, unit_periods, unit, _ = get_frequency(freq)
|
||||||
|
adj_periods = periods * unit_periods
|
||||||
|
|
||||||
|
# TODO: standardize time aliases to avoid any mapping
|
||||||
|
unit = 'd' if unit == 'D' else 'm'
|
||||||
|
delta = pd.Timedelta(adj_periods, unit)
|
||||||
|
|
||||||
|
if start_dt is not None:
|
||||||
|
return pd.date_range(
|
||||||
|
start=start_dt,
|
||||||
|
end=start_dt + delta,
|
||||||
|
freq=freq,
|
||||||
|
closed='left',
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
return pd.date_range(
|
||||||
|
start=end_dt - delta,
|
||||||
|
end=end_dt,
|
||||||
|
freq=freq,
|
||||||
|
)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise ValueError(
|
||||||
|
'Choose only two parameters between start_dt, end_dt '
|
||||||
|
'and periods.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def get_periods(start_dt, end_dt, freq):
|
||||||
|
"""
|
||||||
|
The number of periods in the specified range.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
start_dt: datetime
|
||||||
|
end_dt: datetime
|
||||||
|
freq: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
|
||||||
|
"""
|
||||||
|
return len(get_periods_range(start_dt=start_dt, end_dt=end_dt, freq=freq))
|
||||||
|
|
||||||
|
|
||||||
|
def get_start_dt(end_dt, bar_count, data_frequency, include_first=True):
|
||||||
|
"""
|
||||||
|
The start date based on specified end date and data frequency.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
end_dt: datetime
|
||||||
|
bar_count: int
|
||||||
|
data_frequency: str
|
||||||
|
include_first
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
datetime
|
||||||
|
|
||||||
|
"""
|
||||||
|
periods = bar_count
|
||||||
|
if periods > 1:
|
||||||
|
delta = get_delta(periods, data_frequency)
|
||||||
|
start_dt = end_dt - delta
|
||||||
|
|
||||||
|
if not include_first:
|
||||||
|
start_dt += get_delta(1, data_frequency)
|
||||||
|
else:
|
||||||
|
start_dt = end_dt
|
||||||
|
|
||||||
|
return start_dt
|
||||||
|
|
||||||
|
|
||||||
|
def get_period_label(dt, data_frequency):
|
||||||
|
"""
|
||||||
|
The period label for the specified date and frequency.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
dt: datetime
|
||||||
|
data_frequency: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
if data_frequency == 'minute':
|
||||||
|
return '{}-{:02d}'.format(dt.year, dt.month)
|
||||||
|
else:
|
||||||
|
return '{}'.format(dt.year)
|
||||||
|
|
||||||
|
|
||||||
|
def get_month_start_end(dt, first_day=None, last_day=None):
|
||||||
|
"""
|
||||||
|
The first and last day of the month for the specified date.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
dt: datetime
|
||||||
|
first_day: datetime
|
||||||
|
last_day: datetime
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
datetime, datetime
|
||||||
|
|
||||||
|
"""
|
||||||
|
month_range = calendar.monthrange(dt.year, dt.month)
|
||||||
|
|
||||||
|
if first_day:
|
||||||
|
month_start = first_day
|
||||||
|
else:
|
||||||
|
month_start = pd.to_datetime(datetime(
|
||||||
|
dt.year, dt.month, 1, 0, 0, 0, 0
|
||||||
|
), utc=True)
|
||||||
|
|
||||||
|
if last_day:
|
||||||
|
month_end = last_day
|
||||||
|
else:
|
||||||
|
month_end = pd.to_datetime(datetime(
|
||||||
|
dt.year, dt.month, month_range[1], 23, 59, 0, 0
|
||||||
|
), utc=True)
|
||||||
|
|
||||||
|
if month_end > pd.Timestamp.utcnow():
|
||||||
|
month_end = pd.Timestamp.utcnow().floor('1D')
|
||||||
|
|
||||||
|
return month_start, month_end
|
||||||
|
|
||||||
|
|
||||||
|
def get_year_start_end(dt, first_day=None, last_day=None):
|
||||||
|
"""
|
||||||
|
The first and last day of the year for the specified date.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
|
||||||
|
dt: datetime
|
||||||
|
first_day: datetime
|
||||||
|
last_day: datetime
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
datetime, datetime
|
||||||
|
|
||||||
|
"""
|
||||||
|
year_start = first_day if first_day \
|
||||||
|
else pd.to_datetime(date(dt.year, 1, 1), utc=True)
|
||||||
|
year_end = last_day if last_day \
|
||||||
|
else pd.to_datetime(date(dt.year, 12, 31), utc=True)
|
||||||
|
|
||||||
|
if year_end > pd.Timestamp.utcnow():
|
||||||
|
year_end = pd.Timestamp.utcnow().floor('1D')
|
||||||
|
|
||||||
|
return year_start, year_end
|
||||||
|
|
||||||
|
|
||||||
|
def get_frequency(freq, data_frequency=None):
|
||||||
|
"""
|
||||||
|
Get the frequency parameters.
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
We're trying to use Pandas convention for frequency aliases.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
freq: str
|
||||||
|
data_frequency: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str, int, str, str
|
||||||
|
|
||||||
|
"""
|
||||||
|
if data_frequency is None:
|
||||||
|
data_frequency = 'daily' if freq.upper().endswith('D') else 'minute'
|
||||||
|
|
||||||
|
if freq == 'minute':
|
||||||
|
unit = 'T'
|
||||||
|
candle_size = 1
|
||||||
|
|
||||||
|
elif freq == 'daily':
|
||||||
|
unit = 'D'
|
||||||
|
candle_size = 1
|
||||||
|
|
||||||
|
else:
|
||||||
|
freq_match = re.match(r'([0-9].*)?(m|M|d|D|h|H|T)', freq, re.M | re.I)
|
||||||
|
if freq_match:
|
||||||
|
candle_size = int(freq_match.group(1)) if freq_match.group(1) \
|
||||||
|
else 1
|
||||||
|
unit = freq_match.group(2)
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise InvalidHistoryFrequencyError(frequency=freq)
|
||||||
|
|
||||||
|
# TODO: some exchanges support H and W frequencies but not bundles
|
||||||
|
# Find a way to pass-through these parameters to exchanges
|
||||||
|
# but resample from minute or daily in backtest mode
|
||||||
|
# see catalyst/exchange/ccxt/ccxt_exchange.py:242 for mapping between
|
||||||
|
# Pandas offet aliases (used by Catalyst) and the CCXT timeframes
|
||||||
|
if unit.lower() == 'd':
|
||||||
|
unit = 'D'
|
||||||
|
alias = '{}D'.format(candle_size)
|
||||||
|
|
||||||
|
if data_frequency == 'minute':
|
||||||
|
data_frequency = 'daily'
|
||||||
|
|
||||||
|
elif unit.lower() == 'm' or unit == 'T':
|
||||||
|
unit = 'T'
|
||||||
|
alias = '{}T'.format(candle_size)
|
||||||
|
|
||||||
|
if data_frequency == 'daily':
|
||||||
|
data_frequency = 'minute'
|
||||||
|
|
||||||
|
# elif unit.lower() == 'h':
|
||||||
|
# candle_size = candle_size * 60
|
||||||
|
#
|
||||||
|
# alias = '{}T'.format(candle_size)
|
||||||
|
# if data_frequency == 'daily':
|
||||||
|
# data_frequency = 'minute'
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise InvalidHistoryFrequencyAlias(freq=freq)
|
||||||
|
|
||||||
|
return alias, candle_size, unit, data_frequency
|
||||||
|
|
||||||
|
|
||||||
|
def from_ms_timestamp(ms):
|
||||||
|
return pd.to_datetime(ms, unit='ms', utc=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_epoch():
|
||||||
|
return pd.to_datetime('1970-1-1', utc=True)
|
||||||
@@ -2,21 +2,20 @@ import hashlib
|
|||||||
import json
|
import json
|
||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
import re
|
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from catalyst.assets._assets import TradingPair
|
from catalyst.assets._assets import TradingPair
|
||||||
|
from six import string_types
|
||||||
|
from six.moves.urllib import request
|
||||||
|
|
||||||
from catalyst.constants import DATE_FORMAT, SYMBOLS_URL
|
from catalyst.constants import DATE_FORMAT, SYMBOLS_URL
|
||||||
from catalyst.exchange.exchange_errors import ExchangeSymbolsNotFound, \
|
from catalyst.exchange.exchange_errors import ExchangeSymbolsNotFound
|
||||||
InvalidHistoryFrequencyError, InvalidHistoryFrequencyAlias
|
|
||||||
from catalyst.exchange.utils.serialization_utils import ExchangeJSONEncoder, \
|
from catalyst.exchange.utils.serialization_utils import ExchangeJSONEncoder, \
|
||||||
ExchangeJSONDecoder
|
ExchangeJSONDecoder
|
||||||
from catalyst.utils.paths import data_root, ensure_directory, \
|
from catalyst.utils.paths import data_root, ensure_directory, \
|
||||||
last_modified_time
|
last_modified_time
|
||||||
from six import string_types
|
|
||||||
from six.moves.urllib import request
|
|
||||||
|
|
||||||
|
|
||||||
def get_sid(symbol):
|
def get_sid(symbol):
|
||||||
@@ -129,7 +128,10 @@ def get_exchange_symbols(exchange_name, is_local=False, environ=None):
|
|||||||
if not is_local and (not os.path.isfile(filename) or pd.Timedelta(
|
if not is_local and (not os.path.isfile(filename) or pd.Timedelta(
|
||||||
pd.Timestamp('now', tz='UTC') - last_modified_time(
|
pd.Timestamp('now', tz='UTC') - last_modified_time(
|
||||||
filename)).days > 1):
|
filename)).days > 1):
|
||||||
download_exchange_symbols(exchange_name, environ)
|
try:
|
||||||
|
download_exchange_symbols(exchange_name, environ)
|
||||||
|
except Exception as e:
|
||||||
|
pass
|
||||||
|
|
||||||
if os.path.isfile(filename):
|
if os.path.isfile(filename):
|
||||||
with open(filename) as data_file:
|
with open(filename) as data_file:
|
||||||
@@ -189,7 +191,7 @@ def get_symbols_string(assets):
|
|||||||
return ', '.join([asset.symbol for asset in array])
|
return ', '.join([asset.symbol for asset in array])
|
||||||
|
|
||||||
|
|
||||||
def get_exchange_auth(exchange_name, environ=None):
|
def get_exchange_auth(exchange_name, alias=None, environ=None):
|
||||||
"""
|
"""
|
||||||
The de-serialized contend of the exchange's auth.json file.
|
The de-serialized contend of the exchange's auth.json file.
|
||||||
|
|
||||||
@@ -204,7 +206,8 @@ def get_exchange_auth(exchange_name, environ=None):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
exchange_folder = get_exchange_folder(exchange_name, environ)
|
exchange_folder = get_exchange_folder(exchange_name, environ)
|
||||||
filename = os.path.join(exchange_folder, 'auth.json')
|
name = 'auth' if alias is None else alias
|
||||||
|
filename = os.path.join(exchange_folder, '{}.json'.format(name))
|
||||||
|
|
||||||
if os.path.isfile(filename):
|
if os.path.isfile(filename):
|
||||||
with open(filename) as data_file:
|
with open(filename) as data_file:
|
||||||
@@ -509,72 +512,6 @@ def get_common_assets(exchanges):
|
|||||||
return assets
|
return assets
|
||||||
|
|
||||||
|
|
||||||
def get_frequency(freq, data_frequency):
|
|
||||||
"""
|
|
||||||
Get the frequency parameters.
|
|
||||||
|
|
||||||
Notes
|
|
||||||
-----
|
|
||||||
We're trying to use Pandas convention for frequency aliases.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
freq: str
|
|
||||||
data_frequency: str
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
str, int, str, str
|
|
||||||
|
|
||||||
"""
|
|
||||||
if freq == 'minute':
|
|
||||||
unit = 'T'
|
|
||||||
candle_size = 1
|
|
||||||
|
|
||||||
elif freq == 'daily':
|
|
||||||
unit = 'D'
|
|
||||||
candle_size = 1
|
|
||||||
|
|
||||||
else:
|
|
||||||
freq_match = re.match(r'([0-9].*)?(m|M|d|D|h|H|T)', freq, re.M | re.I)
|
|
||||||
if freq_match:
|
|
||||||
candle_size = int(freq_match.group(1)) if freq_match.group(1) \
|
|
||||||
else 1
|
|
||||||
unit = freq_match.group(2)
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise InvalidHistoryFrequencyError(frequency=freq)
|
|
||||||
|
|
||||||
# TODO: some exchanges support H and W frequencies but not bundles
|
|
||||||
# Find a way to pass-through these parameters to exchanges
|
|
||||||
# but resample from minute or daily in backtest mode
|
|
||||||
# see catalyst/exchange/ccxt/ccxt_exchange.py:242 for mapping between
|
|
||||||
# Pandas offet aliases (used by Catalyst) and the CCXT timeframes
|
|
||||||
if unit.lower() == 'd':
|
|
||||||
alias = '{}D'.format(candle_size)
|
|
||||||
|
|
||||||
if data_frequency == 'minute':
|
|
||||||
data_frequency = 'daily'
|
|
||||||
|
|
||||||
elif unit.lower() == 'm' or unit == 'T':
|
|
||||||
alias = '{}T'.format(candle_size)
|
|
||||||
|
|
||||||
if data_frequency == 'daily':
|
|
||||||
data_frequency = 'minute'
|
|
||||||
|
|
||||||
# elif unit.lower() == 'h':
|
|
||||||
# candle_size = candle_size * 60
|
|
||||||
#
|
|
||||||
# alias = '{}T'.format(candle_size)
|
|
||||||
# if data_frequency == 'daily':
|
|
||||||
# data_frequency = 'minute'
|
|
||||||
|
|
||||||
else:
|
|
||||||
raise InvalidHistoryFrequencyAlias(freq=freq)
|
|
||||||
|
|
||||||
return alias, candle_size, unit, data_frequency
|
|
||||||
|
|
||||||
|
|
||||||
def resample_history_df(df, freq, field):
|
def resample_history_df(df, freq, field):
|
||||||
"""
|
"""
|
||||||
Resample the OHCLV DataFrame using the specified frequency.
|
Resample the OHCLV DataFrame using the specified frequency.
|
||||||
@@ -648,14 +585,6 @@ def mixin_market_params(exchange_name, params, market):
|
|||||||
params['lot'] = params['min_trade_size']
|
params['lot'] = params['min_trade_size']
|
||||||
|
|
||||||
|
|
||||||
def from_ms_timestamp(ms):
|
|
||||||
return pd.to_datetime(ms, unit='ms', utc=True)
|
|
||||||
|
|
||||||
|
|
||||||
def get_epoch():
|
|
||||||
return pd.to_datetime('1970-1-1', utc=True)
|
|
||||||
|
|
||||||
|
|
||||||
def group_assets_by_exchange(assets):
|
def group_assets_by_exchange(assets):
|
||||||
exchange_assets = dict()
|
exchange_assets = dict()
|
||||||
for asset in assets:
|
for asset in assets:
|
||||||
|
|||||||
@@ -13,12 +13,12 @@ exchange_cache = dict()
|
|||||||
|
|
||||||
|
|
||||||
def get_exchange(exchange_name, base_currency=None, must_authenticate=False,
|
def get_exchange(exchange_name, base_currency=None, must_authenticate=False,
|
||||||
skip_init=False):
|
skip_init=False, auth_alias=None):
|
||||||
key = (exchange_name, base_currency)
|
key = (exchange_name, base_currency)
|
||||||
if key in exchange_cache:
|
if key in exchange_cache:
|
||||||
return exchange_cache[key]
|
return exchange_cache[key]
|
||||||
|
|
||||||
exchange_auth = get_exchange_auth(exchange_name)
|
exchange_auth = get_exchange_auth(exchange_name, alias=auth_alias)
|
||||||
|
|
||||||
has_auth = (exchange_auth['key'] != '' and exchange_auth['secret'] != '')
|
has_auth = (exchange_auth['key'] != '' and exchange_auth['secret'] != '')
|
||||||
if must_authenticate and not has_auth:
|
if must_authenticate and not has_auth:
|
||||||
|
|||||||
@@ -44,7 +44,7 @@ def crossover(source, target):
|
|||||||
"""
|
"""
|
||||||
if isinstance(target, numbers.Number):
|
if isinstance(target, numbers.Number):
|
||||||
if source[-1] is np.nan or source[-2] is np.nan \
|
if source[-1] is np.nan or source[-2] is np.nan \
|
||||||
or target is np.nan:
|
or target is np.nan:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if source[-1] >= target > source[-2]:
|
if source[-1] >= target > source[-2]:
|
||||||
@@ -54,7 +54,7 @@ def crossover(source, target):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
if source[-1] is np.nan or source[-2] is np.nan \
|
if source[-1] is np.nan or source[-2] is np.nan \
|
||||||
or target[-1] is np.nan or target[-2] is np.nan:
|
or target[-1] is np.nan or target[-2] is np.nan:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if source[-1] > target[-1] and source[-2] < target[-2]:
|
if source[-1] > target[-1] and source[-2] < target[-2]:
|
||||||
@@ -81,7 +81,7 @@ def crossunder(source, target):
|
|||||||
"""
|
"""
|
||||||
if isinstance(target, numbers.Number):
|
if isinstance(target, numbers.Number):
|
||||||
if source[-1] is np.nan or source[-2] is np.nan \
|
if source[-1] is np.nan or source[-2] is np.nan \
|
||||||
or target is np.nan:
|
or target is np.nan:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if source[-1] < target <= source[-2]:
|
if source[-1] < target <= source[-2]:
|
||||||
@@ -90,7 +90,7 @@ def crossunder(source, target):
|
|||||||
return False
|
return False
|
||||||
else:
|
else:
|
||||||
if source[-1] is np.nan or source[-2] is np.nan \
|
if source[-1] is np.nan or source[-2] is np.nan \
|
||||||
or target[-1] is np.nan or target[-2] is np.nan:
|
or target[-1] is np.nan or target[-2] is np.nan:
|
||||||
return False
|
return False
|
||||||
|
|
||||||
if source[-1] < target[-1] and source[-2] >= target[-2]:
|
if source[-1] < target[-1] and source[-2] >= target[-2]:
|
||||||
@@ -229,7 +229,10 @@ def prepare_stats(stats, recorded_cols=list()):
|
|||||||
asset_values)
|
asset_values)
|
||||||
|
|
||||||
df = pd.DataFrame(stats)
|
df = pd.DataFrame(stats)
|
||||||
|
df['orders'] = df['orders'].apply(lambda orders: len(orders))
|
||||||
|
df['transactions'] = df['transactions'].apply(
|
||||||
|
lambda transactions: len(transactions)
|
||||||
|
)
|
||||||
index_cols = [
|
index_cols = [
|
||||||
'period_close', 'starting_cash', 'ending_cash', 'portfolio_value',
|
'period_close', 'starting_cash', 'ending_cash', 'portfolio_value',
|
||||||
'pnl', 'long_exposure', 'short_exposure', 'orders', 'transactions',
|
'pnl', 'long_exposure', 'short_exposure', 'orders', 'transactions',
|
||||||
@@ -241,11 +244,6 @@ def prepare_stats(stats, recorded_cols=list()):
|
|||||||
for column in recorded_cols:
|
for column in recorded_cols:
|
||||||
index_cols.append(column)
|
index_cols.append(column)
|
||||||
|
|
||||||
df['orders'] = df['orders'].apply(lambda orders: len(orders))
|
|
||||||
df['transactions'] = df['transactions'].apply(
|
|
||||||
lambda transactions: len(transactions)
|
|
||||||
)
|
|
||||||
|
|
||||||
if asset_cols:
|
if asset_cols:
|
||||||
columns = asset_cols
|
columns = asset_cols
|
||||||
df.set_index(index_cols, drop=True, inplace=True)
|
df.set_index(index_cols, drop=True, inplace=True)
|
||||||
@@ -287,7 +285,7 @@ def get_pretty_stats(stats, recorded_cols=None, num_rows=10, show_tail=True):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if isinstance(stats, pd.DataFrame):
|
if isinstance(stats, pd.DataFrame):
|
||||||
stats = stats.T.to_dict().values()
|
stats = list(stats.T.to_dict().values())
|
||||||
stats.sort(key=itemgetter('period_close'))
|
stats.sort(key=itemgetter('period_close'))
|
||||||
|
|
||||||
if len(stats) > num_rows:
|
if len(stats) > num_rows:
|
||||||
@@ -359,9 +357,13 @@ def stats_to_s3(uri, stats, algo_namespace, recorded_cols=None,
|
|||||||
pid = os.getpid()
|
pid = os.getpid()
|
||||||
|
|
||||||
parts = uri.split('//')
|
parts = uri.split('//')
|
||||||
obj = s3.Object(parts[1], '{}/{}-{}-{}.csv'.format(
|
path = '{folder}/{algo}/{time}-{algo}-{pid}.csv'.format(
|
||||||
folder, timestr, algo_namespace, pid
|
folder=folder,
|
||||||
))
|
algo=algo_namespace,
|
||||||
|
time=timestr,
|
||||||
|
pid=pid,
|
||||||
|
)
|
||||||
|
obj = s3.Object(parts[1], path)
|
||||||
obj.put(Body=bytes_to_write)
|
obj.put(Body=bytes_to_write)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -62,14 +62,14 @@ def output_df(df, assets, name=None):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
if isinstance(assets, TradingPair):
|
if isinstance(assets, TradingPair):
|
||||||
exchange_folder = assets.exchange
|
asset_folder = '{}_{}'.format(assets.exchange, assets.symbol)
|
||||||
asset_folder = assets.symbol
|
|
||||||
else:
|
else:
|
||||||
exchange_folder = ','.join([asset.exchange for asset in assets])
|
asset_folder = ','.join(
|
||||||
asset_folder = ','.join([asset.symbol for asset in assets])
|
['{}_{}'.format(a.exchange, a.symbol) for a in assets]
|
||||||
|
)
|
||||||
|
|
||||||
folder = os.path.join(
|
folder = os.path.join(
|
||||||
tempfile.gettempdir(), 'catalyst', exchange_folder, asset_folder
|
tempfile.gettempdir(), 'catalyst', asset_folder
|
||||||
)
|
)
|
||||||
ensure_directory(folder)
|
ensure_directory(folder)
|
||||||
|
|
||||||
@@ -79,4 +79,4 @@ def output_df(df, assets, name=None):
|
|||||||
path = os.path.join(folder, '{}.csv'.format(name))
|
path = os.path.join(folder, '{}.csv'.format(name))
|
||||||
df.to_csv(path)
|
df.to_csv(path)
|
||||||
|
|
||||||
return path
|
return path, folder
|
||||||
|
|||||||
@@ -29,13 +29,15 @@ from .risk import check_entry
|
|||||||
from empyrical import (
|
from empyrical import (
|
||||||
alpha_beta_aligned,
|
alpha_beta_aligned,
|
||||||
annual_volatility,
|
annual_volatility,
|
||||||
cum_returns,
|
|
||||||
downside_risk,
|
downside_risk,
|
||||||
information_ratio,
|
information_ratio,
|
||||||
max_drawdown,
|
|
||||||
sharpe_ratio,
|
sharpe_ratio,
|
||||||
sortino_ratio
|
sortino_ratio
|
||||||
)
|
)
|
||||||
|
from catalyst.patches.stats import (
|
||||||
|
max_drawdown,
|
||||||
|
cum_returns,
|
||||||
|
)
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
from catalyst.constants import LOG_LEVEL
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,302 @@
|
|||||||
|
[
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "name",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_spender",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "approve",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "totalSupply",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_from",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_to",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "transferFrom",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "INITIAL_SUPPLY",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "decimals",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "uint8"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_spender",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_subtractedValue",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "decreaseApproval",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "success",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": false,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "getAfterApproveTest",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_owner",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "balanceOf",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "balance",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [],
|
||||||
|
"name": "symbol",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "string"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_to",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "transfer",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_spender",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_addedValue",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "increaseApproval",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "success",
|
||||||
|
"type": "bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"constant": true,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "_owner",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "_spender",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "allowance",
|
||||||
|
"outputs": [
|
||||||
|
{
|
||||||
|
"name": "",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "view",
|
||||||
|
"type": "function"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"name": "testValue",
|
||||||
|
"type": "address"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"payable": false,
|
||||||
|
"stateMutability": "nonpayable",
|
||||||
|
"type": "constructor"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"name": "owner",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"name": "spender",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"name": "value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "Approval",
|
||||||
|
"type": "event"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"anonymous": false,
|
||||||
|
"inputs": [
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"name": "from",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": true,
|
||||||
|
"name": "to",
|
||||||
|
"type": "address"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"indexed": false,
|
||||||
|
"name": "value",
|
||||||
|
"type": "uint256"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"name": "Transfer",
|
||||||
|
"type": "event"
|
||||||
|
}
|
||||||
|
]
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0x7fAec9aaE31BE428DeAAE1be8195dF609079Fd10
|
||||||
File diff suppressed because one or more lines are too long
@@ -0,0 +1 @@
|
|||||||
|
0x3985f5de8fddf2e8f7705cd360b498bf35ebfbc4
|
||||||
@@ -0,0 +1,705 @@
|
|||||||
|
from __future__ import print_function
|
||||||
|
|
||||||
|
import glob
|
||||||
|
import json
|
||||||
|
import os
|
||||||
|
import re
|
||||||
|
import shutil
|
||||||
|
import sys
|
||||||
|
import time
|
||||||
|
|
||||||
|
import bcolz
|
||||||
|
import logbook
|
||||||
|
import pandas as pd
|
||||||
|
import requests
|
||||||
|
from requests_toolbelt import MultipartDecoder
|
||||||
|
from requests_toolbelt.multipart.decoder import \
|
||||||
|
NonMultipartContentTypeException
|
||||||
|
|
||||||
|
from catalyst.constants import (
|
||||||
|
LOG_LEVEL, AUTH_SERVER, ETH_REMOTE_NODE, MARKETPLACE_CONTRACT,
|
||||||
|
MARKETPLACE_CONTRACT_ABI, ENIGMA_CONTRACT, ENIGMA_CONTRACT_ABI)
|
||||||
|
from catalyst.exchange.utils.stats_utils import set_print_settings
|
||||||
|
from catalyst.marketplace.marketplace_errors import (
|
||||||
|
MarketplacePubAddressEmpty, MarketplaceDatasetNotFound,
|
||||||
|
MarketplaceNoAddressMatch, MarketplaceHTTPRequest,
|
||||||
|
MarketplaceNoCSVFiles)
|
||||||
|
from catalyst.marketplace.utils.auth_utils import get_key_secret, \
|
||||||
|
get_signed_headers
|
||||||
|
from catalyst.marketplace.utils.bundle_utils import merge_bundles
|
||||||
|
from catalyst.marketplace.utils.eth_utils import bin_hex, from_grains, \
|
||||||
|
to_grains
|
||||||
|
from catalyst.marketplace.utils.path_utils import get_bundle_folder, \
|
||||||
|
get_data_source_folder, get_marketplace_folder, \
|
||||||
|
get_user_pubaddr, get_temp_bundles_folder, extract_bundle
|
||||||
|
|
||||||
|
if sys.version_info.major < 3:
|
||||||
|
import urllib
|
||||||
|
else:
|
||||||
|
import urllib.request as urllib
|
||||||
|
|
||||||
|
log = logbook.Logger('Marketplace', level=LOG_LEVEL)
|
||||||
|
|
||||||
|
|
||||||
|
class Marketplace:
|
||||||
|
def __init__(self):
|
||||||
|
global Web3
|
||||||
|
from web3 import Web3, HTTPProvider
|
||||||
|
|
||||||
|
self.addresses = get_user_pubaddr()
|
||||||
|
|
||||||
|
if self.addresses[0]['pubAddr'] == '':
|
||||||
|
raise MarketplacePubAddressEmpty(
|
||||||
|
filename=os.path.join(
|
||||||
|
get_marketplace_folder(), 'addresses.json')
|
||||||
|
)
|
||||||
|
self.default_account = self.addresses[0]['pubAddr']
|
||||||
|
|
||||||
|
self.web3 = Web3(HTTPProvider(ETH_REMOTE_NODE))
|
||||||
|
|
||||||
|
contract_url = urllib.urlopen(MARKETPLACE_CONTRACT)
|
||||||
|
|
||||||
|
self.mkt_contract_address = Web3.toChecksumAddress(
|
||||||
|
contract_url.readline().strip())
|
||||||
|
|
||||||
|
abi_url = urllib.urlopen(MARKETPLACE_CONTRACT_ABI)
|
||||||
|
abi = json.load(abi_url)
|
||||||
|
|
||||||
|
self.mkt_contract = self.web3.eth.contract(
|
||||||
|
self.mkt_contract_address,
|
||||||
|
abi=abi,
|
||||||
|
)
|
||||||
|
|
||||||
|
contract_url = urllib.urlopen(ENIGMA_CONTRACT)
|
||||||
|
|
||||||
|
self.eng_contract_address = Web3.toChecksumAddress(
|
||||||
|
contract_url.readline().strip())
|
||||||
|
|
||||||
|
abi_url = urllib.urlopen(ENIGMA_CONTRACT_ABI)
|
||||||
|
abi = json.load(abi_url)
|
||||||
|
|
||||||
|
self.eng_contract = self.web3.eth.contract(
|
||||||
|
self.eng_contract_address,
|
||||||
|
abi=abi,
|
||||||
|
)
|
||||||
|
|
||||||
|
# def get_data_sources_map(self):
|
||||||
|
# return [
|
||||||
|
# dict(
|
||||||
|
# name='Marketcap',
|
||||||
|
# desc='The marketcap value in USD.',
|
||||||
|
# start_date=pd.to_datetime('2017-01-01'),
|
||||||
|
# end_date=pd.to_datetime('2018-01-15'),
|
||||||
|
# data_frequencies=['daily'],
|
||||||
|
# ),
|
||||||
|
# dict(
|
||||||
|
# name='GitHub',
|
||||||
|
# desc='The rate of development activity on GitHub.',
|
||||||
|
# start_date=pd.to_datetime('2017-01-01'),
|
||||||
|
# end_date=pd.to_datetime('2018-01-15'),
|
||||||
|
# data_frequencies=['daily', 'hour'],
|
||||||
|
# ),
|
||||||
|
# dict(
|
||||||
|
# name='Influencers',
|
||||||
|
# desc='Tweets & related sentiments by selected influencers.',
|
||||||
|
# start_date=pd.to_datetime('2017-01-01'),
|
||||||
|
# end_date=pd.to_datetime('2018-01-15'),
|
||||||
|
# data_frequencies=['daily', 'hour', 'minute'],
|
||||||
|
# ),
|
||||||
|
# ]
|
||||||
|
|
||||||
|
def to_text(self, hex):
|
||||||
|
return Web3.toText(hex).rstrip('\0')
|
||||||
|
|
||||||
|
def choose_pubaddr(self):
|
||||||
|
if len(self.addresses) == 1:
|
||||||
|
address = self.addresses[0]['pubAddr']
|
||||||
|
address_i = 0
|
||||||
|
print('Using {} for this transaction.'.format(address))
|
||||||
|
else:
|
||||||
|
while True:
|
||||||
|
for i in range(0, len(self.addresses)):
|
||||||
|
print('{}\t{}\t{}'.format(
|
||||||
|
i,
|
||||||
|
self.addresses[i]['pubAddr'],
|
||||||
|
self.addresses[i]['desc'])
|
||||||
|
)
|
||||||
|
address_i = int(input('Choose your address associated with '
|
||||||
|
'this transaction: [default: 0] ') or 0)
|
||||||
|
if not (0 <= address_i < len(self.addresses)):
|
||||||
|
print('Please choose a number between 0 and {}\n'.format(
|
||||||
|
len(self.addresses) - 1))
|
||||||
|
else:
|
||||||
|
address = Web3.toChecksumAddress(
|
||||||
|
self.addresses[address_i]['pubAddr'])
|
||||||
|
break
|
||||||
|
|
||||||
|
return address, address_i
|
||||||
|
|
||||||
|
def sign_transaction(self, from_address, tx):
|
||||||
|
|
||||||
|
print('\nVisit https://www.myetherwallet.com/#offline-transaction and '
|
||||||
|
'enter the following parameters:\n\n'
|
||||||
|
'From Address:\t\t{_from}\n'
|
||||||
|
'\n\tClick the "Generate Information" button\n\n'
|
||||||
|
'To Address:\t\t{to}\n'
|
||||||
|
'Value / Amount to Send:\t{value}\n'
|
||||||
|
'Gas Limit:\t\t{gas}\n'
|
||||||
|
'Gas Price:\t\t[Accept the default value]\n'
|
||||||
|
'Nonce:\t\t\t{nonce}\n'
|
||||||
|
'Data:\t\t\t{data}\n'.format(
|
||||||
|
_from=from_address,
|
||||||
|
to=tx['to'],
|
||||||
|
value=tx['value'],
|
||||||
|
gas=tx['gas'],
|
||||||
|
nonce=tx['nonce'],
|
||||||
|
data=tx['data'], )
|
||||||
|
)
|
||||||
|
|
||||||
|
signed_tx = input('Copy and Paste the "Signed Transaction" '
|
||||||
|
'field here:\n')
|
||||||
|
|
||||||
|
if signed_tx.startswith('0x'):
|
||||||
|
signed_tx = signed_tx[2:]
|
||||||
|
|
||||||
|
return signed_tx
|
||||||
|
|
||||||
|
def check_transaction(self, tx_hash):
|
||||||
|
|
||||||
|
if 'ropsten' in ETH_REMOTE_NODE:
|
||||||
|
etherscan = 'https://ropsten.etherscan.io/tx/{}'.format(
|
||||||
|
tx_hash)
|
||||||
|
else:
|
||||||
|
etherscan = 'https://etherscan.io/tx/{}'.format(tx_hash)
|
||||||
|
|
||||||
|
print('\nYou can check the outcome of your transaction here:\n'
|
||||||
|
'{}\n\n'.format(etherscan))
|
||||||
|
|
||||||
|
def list(self):
|
||||||
|
|
||||||
|
data_sources = self.mkt_contract.functions.getAllProviders().call()
|
||||||
|
|
||||||
|
data = []
|
||||||
|
for index, data_source in enumerate(data_sources):
|
||||||
|
if index > 0:
|
||||||
|
if 'test' not in Web3.toText(data_source).lower():
|
||||||
|
data.append(
|
||||||
|
dict(
|
||||||
|
dataset=self.to_text(data_source)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
df = pd.DataFrame(data)
|
||||||
|
set_print_settings()
|
||||||
|
if df.empty:
|
||||||
|
print('There are no datasets available yet.')
|
||||||
|
else:
|
||||||
|
print(df)
|
||||||
|
|
||||||
|
def subscribe(self, dataset):
|
||||||
|
|
||||||
|
dataset = dataset.lower()
|
||||||
|
|
||||||
|
address = self.choose_pubaddr()[0]
|
||||||
|
provider_info = self.mkt_contract.functions.getDataProviderInfo(
|
||||||
|
Web3.toHex(dataset)
|
||||||
|
).call()
|
||||||
|
|
||||||
|
if not provider_info[4]:
|
||||||
|
print('The requested "{}" dataset is not registered in '
|
||||||
|
'the Data Marketplace.'.format(dataset))
|
||||||
|
return
|
||||||
|
|
||||||
|
grains = provider_info[1]
|
||||||
|
price = from_grains(grains)
|
||||||
|
|
||||||
|
subscribed = self.mkt_contract.functions.checkAddressSubscription(
|
||||||
|
address, Web3.toHex(dataset)
|
||||||
|
).call()
|
||||||
|
|
||||||
|
if subscribed[5]:
|
||||||
|
print(
|
||||||
|
'\nYou are already subscribed to the "{}" dataset.\n'
|
||||||
|
'Your subscription started on {} UTC, and is valid until '
|
||||||
|
'{} UTC.'.format(
|
||||||
|
dataset,
|
||||||
|
pd.to_datetime(subscribed[3], unit='s', utc=True),
|
||||||
|
pd.to_datetime(subscribed[4], unit='s', utc=True)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
print('\nThe price for a monthly subscription to this dataset is'
|
||||||
|
' {} ENG'.format(price))
|
||||||
|
|
||||||
|
print(
|
||||||
|
'Checking that the ENG balance in {} is greater than {} '
|
||||||
|
'ENG... '.format(address, price), end=''
|
||||||
|
)
|
||||||
|
|
||||||
|
wallet_address = address[2:]
|
||||||
|
balance = self.web3.eth.call({
|
||||||
|
'from': address,
|
||||||
|
'to': self.eng_contract_address,
|
||||||
|
'data': '0x70a08231000000000000000000000000{}'.format(
|
||||||
|
wallet_address
|
||||||
|
)
|
||||||
|
})
|
||||||
|
|
||||||
|
try:
|
||||||
|
balance = Web3.toInt(balance) # web3 >= 4.0.0b7
|
||||||
|
except TypeError:
|
||||||
|
balance = Web3.toInt(hexstr=balance) # web3 <= 4.0.0b6
|
||||||
|
|
||||||
|
if balance > grains:
|
||||||
|
print('OK.')
|
||||||
|
else:
|
||||||
|
print('FAIL.\n\nAddress {} balance is {} ENG,\nwhich is lower '
|
||||||
|
'than the price of the dataset that you are trying to\n'
|
||||||
|
'buy: {} ENG. Get enough ENG to cover the costs of the '
|
||||||
|
'monthly\nsubscription for what you are trying to buy, '
|
||||||
|
'and try again.'.format(
|
||||||
|
address, from_grains(balance), price))
|
||||||
|
return
|
||||||
|
|
||||||
|
while True:
|
||||||
|
agree_pay = input('Please confirm that you agree to pay {} ENG '
|
||||||
|
'for a monthly subscription to the dataset "{}" '
|
||||||
|
'starting today. [default: Y] '.format(
|
||||||
|
price, dataset)) or 'y'
|
||||||
|
if agree_pay.lower() not in ('y', 'n'):
|
||||||
|
print("Please answer Y or N.")
|
||||||
|
else:
|
||||||
|
if agree_pay.lower() == 'y':
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
return
|
||||||
|
|
||||||
|
print('Ready to subscribe to dataset {}.\n'.format(dataset))
|
||||||
|
print('In order to execute the subscription, you will need to sign '
|
||||||
|
'two different transactions:\n'
|
||||||
|
'1. First transaction is to authorize the Marketplace contract '
|
||||||
|
'to spend {} ENG on your behalf.\n'
|
||||||
|
'2. Second transaction is the actual subscription for the '
|
||||||
|
'desired dataset'.format(price))
|
||||||
|
|
||||||
|
tx = self.eng_contract.functions.approve(
|
||||||
|
self.mkt_contract_address,
|
||||||
|
grains,
|
||||||
|
).buildTransaction(
|
||||||
|
{'nonce': self.web3.eth.getTransactionCount(address)}
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'ropsten' in ETH_REMOTE_NODE:
|
||||||
|
tx['gas'] = min(int(tx['gas'] * 1.5), 4700000)
|
||||||
|
|
||||||
|
signed_tx = self.sign_transaction(address, tx)
|
||||||
|
try:
|
||||||
|
tx_hash = '0x{}'.format(
|
||||||
|
bin_hex(self.web3.eth.sendRawTransaction(signed_tx))
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
'\nThis is the TxHash for this transaction: {}'.format(tx_hash)
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print('Unable to subscribe to data source: {}'.format(e))
|
||||||
|
return
|
||||||
|
|
||||||
|
self.check_transaction(tx_hash)
|
||||||
|
|
||||||
|
print('Waiting for the first transaction to succeed...')
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
if self.web3.eth.getTransactionReceipt(tx_hash).status:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print('\nTransaction failed. Aborting...')
|
||||||
|
return
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
for i in range(0, 10):
|
||||||
|
print('.', end='', flush=True)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
print('\nFirst transaction successful!\n'
|
||||||
|
'Now processing second transaction.')
|
||||||
|
|
||||||
|
tx = self.mkt_contract.functions.subscribe(
|
||||||
|
Web3.toHex(dataset),
|
||||||
|
).buildTransaction(
|
||||||
|
{'nonce': self.web3.eth.getTransactionCount(address)})
|
||||||
|
|
||||||
|
if 'ropsten' in ETH_REMOTE_NODE:
|
||||||
|
tx['gas'] = min(int(tx['gas'] * 1.5), 4700000)
|
||||||
|
|
||||||
|
signed_tx = self.sign_transaction(address, tx)
|
||||||
|
|
||||||
|
try:
|
||||||
|
tx_hash = '0x{}'.format(bin_hex(
|
||||||
|
self.web3.eth.sendRawTransaction(signed_tx)))
|
||||||
|
print('\nThis is the TxHash for this transaction: '
|
||||||
|
'{}'.format(tx_hash))
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print('Unable to subscribe to data source: {}'.format(e))
|
||||||
|
return
|
||||||
|
|
||||||
|
self.check_transaction(tx_hash)
|
||||||
|
|
||||||
|
print('Waiting for the second transaction to succeed...')
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
if self.web3.eth.getTransactionReceipt(tx_hash).status:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print('\nTransaction failed. Aborting...')
|
||||||
|
return
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
for i in range(0, 10):
|
||||||
|
print('.', end='', flush=True)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
print('\nSecond transaction successful!\n'
|
||||||
|
'You have successfully subscribed to dataset {} with'
|
||||||
|
'address {}.\n'
|
||||||
|
'You can now ingest this dataset anytime during the '
|
||||||
|
'next month by running the following command:\n'
|
||||||
|
'catalyst marketplace ingest --dataset={}'.format(
|
||||||
|
dataset, address, dataset))
|
||||||
|
|
||||||
|
def process_temp_bundle(self, ds_name, path):
|
||||||
|
"""
|
||||||
|
Merge the temp bundle into the main bundle for the specified
|
||||||
|
data source.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ds_name
|
||||||
|
path
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
tmp_bundle = extract_bundle(path)
|
||||||
|
bundle_folder = get_data_source_folder(ds_name)
|
||||||
|
if os.listdir(bundle_folder):
|
||||||
|
zsource = bcolz.ctable(rootdir=tmp_bundle, mode='r')
|
||||||
|
ztarget = bcolz.ctable(rootdir=bundle_folder, mode='r')
|
||||||
|
merge_bundles(zsource, ztarget)
|
||||||
|
|
||||||
|
else:
|
||||||
|
os.rename(tmp_bundle, bundle_folder)
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
|
def ingest(self, ds_name, start=None, end=None, force_download=False):
|
||||||
|
|
||||||
|
# ds_name = ds_name.lower()
|
||||||
|
|
||||||
|
# TODO: catch error conditions
|
||||||
|
provider_info = self.mkt_contract.functions.getDataProviderInfo(
|
||||||
|
Web3.toHex(ds_name)
|
||||||
|
).call()
|
||||||
|
|
||||||
|
if not provider_info[4]:
|
||||||
|
print('The requested "{}" dataset is not registered in '
|
||||||
|
'the Data Marketplace.'.format(ds_name))
|
||||||
|
return
|
||||||
|
|
||||||
|
address, address_i = self.choose_pubaddr()
|
||||||
|
fns = self.mkt_contract.functions
|
||||||
|
check_sub = fns.checkAddressSubscription(
|
||||||
|
address, Web3.toHex(ds_name)
|
||||||
|
).call()
|
||||||
|
|
||||||
|
if check_sub[0] != address or self.to_text(check_sub[1]) != ds_name:
|
||||||
|
print('You are not subscribed to dataset "{}" with address {}. '
|
||||||
|
'Plese subscribe first.'.format(ds_name, address))
|
||||||
|
return
|
||||||
|
|
||||||
|
if not check_sub[5]:
|
||||||
|
print('Your subscription to dataset "{}" expired on {} UTC.'
|
||||||
|
'Please renew your subscription by running:\n'
|
||||||
|
'catalyst marketplace subscribe --dataset={}'.format(
|
||||||
|
ds_name,
|
||||||
|
pd.to_datetime(check_sub[4], unit='s', utc=True),
|
||||||
|
ds_name)
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'key' in self.addresses[address_i]:
|
||||||
|
key = self.addresses[address_i]['key']
|
||||||
|
secret = self.addresses[address_i]['secret']
|
||||||
|
else:
|
||||||
|
key, secret = get_key_secret(address)
|
||||||
|
|
||||||
|
headers = get_signed_headers(ds_name, key, secret)
|
||||||
|
log.debug('Starting download of dataset for ingestion...')
|
||||||
|
r = requests.post(
|
||||||
|
'{}/marketplace/ingest'.format(AUTH_SERVER),
|
||||||
|
headers=headers,
|
||||||
|
stream=True,
|
||||||
|
)
|
||||||
|
if r.status_code == 200:
|
||||||
|
target_path = get_temp_bundles_folder()
|
||||||
|
try:
|
||||||
|
decoder = MultipartDecoder.from_response(r)
|
||||||
|
for part in decoder.parts:
|
||||||
|
h = part.headers[b'Content-Disposition'].decode('utf-8')
|
||||||
|
# Extracting the filename from the header
|
||||||
|
name = re.search(r'filename="(.*)"', h).group(1)
|
||||||
|
|
||||||
|
filename = os.path.join(target_path, name)
|
||||||
|
with open(filename, 'wb') as f:
|
||||||
|
# for chunk in part.content.iter_content(
|
||||||
|
# chunk_size=1024):
|
||||||
|
# if chunk: # filter out keep-alive new chunks
|
||||||
|
# f.write(chunk)
|
||||||
|
f.write(part.content)
|
||||||
|
|
||||||
|
self.process_temp_bundle(ds_name, filename)
|
||||||
|
|
||||||
|
except NonMultipartContentTypeException:
|
||||||
|
response = r.json()
|
||||||
|
raise MarketplaceHTTPRequest(
|
||||||
|
request='ingest dataset',
|
||||||
|
error=response,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise MarketplaceHTTPRequest(
|
||||||
|
request='ingest dataset',
|
||||||
|
error=r.status_code,
|
||||||
|
)
|
||||||
|
|
||||||
|
log.info('{} ingested successfully'.format(ds_name))
|
||||||
|
|
||||||
|
def get_dataset(self, ds_name, start=None, end=None):
|
||||||
|
ds_name = ds_name.lower()
|
||||||
|
|
||||||
|
# TODO: filter ctable by start and end date
|
||||||
|
bundle_folder = get_data_source_folder(ds_name)
|
||||||
|
z = bcolz.ctable(rootdir=bundle_folder, mode='r')
|
||||||
|
|
||||||
|
df = z.todataframe() # type: pd.DataFrame
|
||||||
|
df.set_index(['date', 'symbol'], drop=True, inplace=True)
|
||||||
|
|
||||||
|
# TODO: implement the filter more carefully
|
||||||
|
# if start and end is None:
|
||||||
|
# df = df.xs(start, level=0)
|
||||||
|
|
||||||
|
return df
|
||||||
|
|
||||||
|
def clean(self, data_source_name, data_frequency=None):
|
||||||
|
data_source_name = data_source_name.lower()
|
||||||
|
|
||||||
|
if data_frequency is None:
|
||||||
|
folder = get_data_source_folder(data_source_name)
|
||||||
|
|
||||||
|
else:
|
||||||
|
folder = get_bundle_folder(data_source_name, data_frequency)
|
||||||
|
|
||||||
|
shutil.rmtree(folder)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def create_metadata(self, key, secret, ds_name, data_frequency, desc,
|
||||||
|
has_history=True, has_live=True):
|
||||||
|
"""
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
headers = get_signed_headers(ds_name, key, secret)
|
||||||
|
r = requests.post(
|
||||||
|
'{}/marketplace/register'.format(AUTH_SERVER),
|
||||||
|
json=dict(
|
||||||
|
ds_name=ds_name,
|
||||||
|
desc=desc,
|
||||||
|
data_frequency=data_frequency,
|
||||||
|
has_history=has_history,
|
||||||
|
has_live=has_live,
|
||||||
|
),
|
||||||
|
headers=headers,
|
||||||
|
)
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
raise MarketplaceHTTPRequest(
|
||||||
|
request='register', error=r.status_code
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'error' in r.json():
|
||||||
|
raise MarketplaceHTTPRequest(
|
||||||
|
request='upload file', error=r.json()['error']
|
||||||
|
)
|
||||||
|
|
||||||
|
def register(self):
|
||||||
|
while True:
|
||||||
|
desc = input('Enter the name of the dataset to register: ')
|
||||||
|
dataset = desc.lower()
|
||||||
|
provider_info = self.mkt_contract.functions.getDataProviderInfo(
|
||||||
|
Web3.toHex(dataset)
|
||||||
|
).call()
|
||||||
|
|
||||||
|
if provider_info[4]:
|
||||||
|
print('There is already a dataset registered under '
|
||||||
|
'the name "{}". Please choose a different '
|
||||||
|
'name.'.format(dataset))
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
price = int(
|
||||||
|
input(
|
||||||
|
'Enter the price for a monthly subscription to '
|
||||||
|
'this dataset in ENG: '
|
||||||
|
)
|
||||||
|
)
|
||||||
|
while True:
|
||||||
|
freq = input('Enter the data frequency [daily, hourly, minute]: ')
|
||||||
|
if freq.lower() not in ('daily', 'hourly', 'minute'):
|
||||||
|
print('Not a valid frequency.')
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
while True:
|
||||||
|
reg_pub = input(
|
||||||
|
'Does it include historical data? [default: Y]: '
|
||||||
|
) or 'y'
|
||||||
|
if reg_pub.lower() not in ('y', 'n'):
|
||||||
|
print('Please answer Y or N.')
|
||||||
|
else:
|
||||||
|
if reg_pub.lower() == 'y':
|
||||||
|
has_history = True
|
||||||
|
else:
|
||||||
|
has_history = False
|
||||||
|
break
|
||||||
|
|
||||||
|
while True:
|
||||||
|
reg_pub = input(
|
||||||
|
'Doest it include live data? [default: Y]: '
|
||||||
|
) or 'y'
|
||||||
|
if reg_pub.lower() not in ('y', 'n'):
|
||||||
|
print('Please answer Y or N.')
|
||||||
|
else:
|
||||||
|
if reg_pub.lower() == 'y':
|
||||||
|
has_live = True
|
||||||
|
else:
|
||||||
|
has_live = False
|
||||||
|
break
|
||||||
|
|
||||||
|
address, address_i = self.choose_pubaddr()
|
||||||
|
if 'key' in self.addresses[address_i]:
|
||||||
|
key = self.addresses[address_i]['key']
|
||||||
|
secret = self.addresses[address_i]['secret']
|
||||||
|
else:
|
||||||
|
key, secret = get_key_secret(address)
|
||||||
|
|
||||||
|
grains = to_grains(price)
|
||||||
|
|
||||||
|
tx = self.mkt_contract.functions.register(
|
||||||
|
Web3.toHex(dataset),
|
||||||
|
grains,
|
||||||
|
address,
|
||||||
|
).buildTransaction(
|
||||||
|
{'nonce': self.web3.eth.getTransactionCount(address)}
|
||||||
|
)
|
||||||
|
|
||||||
|
if 'ropsten' in ETH_REMOTE_NODE:
|
||||||
|
tx['gas'] = min(int(tx['gas'] * 1.5), 4700000)
|
||||||
|
|
||||||
|
signed_tx = self.sign_transaction(address, tx)
|
||||||
|
|
||||||
|
try:
|
||||||
|
tx_hash = '0x{}'.format(
|
||||||
|
bin_hex(self.web3.eth.sendRawTransaction(signed_tx))
|
||||||
|
)
|
||||||
|
print(
|
||||||
|
'\nThis is the TxHash for this transaction: {}'.format(tx_hash)
|
||||||
|
)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
print('Unable to subscribe to data source: {}'.format(e))
|
||||||
|
return
|
||||||
|
|
||||||
|
self.check_transaction(tx_hash)
|
||||||
|
|
||||||
|
print('Waiting for the transaction to succeed...')
|
||||||
|
|
||||||
|
while True:
|
||||||
|
try:
|
||||||
|
if self.web3.eth.getTransactionReceipt(tx_hash).status:
|
||||||
|
break
|
||||||
|
else:
|
||||||
|
print('\nTransaction failed. Aborting...')
|
||||||
|
return
|
||||||
|
except AttributeError:
|
||||||
|
pass
|
||||||
|
for i in range(0, 10):
|
||||||
|
print('.', end='', flush=True)
|
||||||
|
time.sleep(1)
|
||||||
|
|
||||||
|
print('\nWarming up the {} dataset'.format(dataset))
|
||||||
|
self.create_metadata(
|
||||||
|
key=key,
|
||||||
|
secret=secret,
|
||||||
|
ds_name=dataset,
|
||||||
|
data_frequency=freq,
|
||||||
|
desc=desc,
|
||||||
|
has_history=has_history,
|
||||||
|
has_live=has_live,
|
||||||
|
)
|
||||||
|
print('\n{} registered successfully'.format(dataset))
|
||||||
|
|
||||||
|
def publish(self, dataset, datadir, watch):
|
||||||
|
dataset = dataset.lower()
|
||||||
|
provider_info = self.mkt_contract.functions.getDataProviderInfo(
|
||||||
|
Web3.toHex(dataset)
|
||||||
|
).call()
|
||||||
|
|
||||||
|
if not provider_info[4]:
|
||||||
|
raise MarketplaceDatasetNotFound(dataset=dataset)
|
||||||
|
|
||||||
|
match = next(
|
||||||
|
(l for l in self.addresses if l['pubAddr'] == provider_info[0]),
|
||||||
|
None
|
||||||
|
)
|
||||||
|
if not match:
|
||||||
|
raise MarketplaceNoAddressMatch(
|
||||||
|
dataset=dataset,
|
||||||
|
address=provider_info[0])
|
||||||
|
|
||||||
|
print('Using address: {} to publish this dataset.'.format(
|
||||||
|
provider_info[0]))
|
||||||
|
|
||||||
|
if 'key' in match:
|
||||||
|
key = match['key']
|
||||||
|
secret = match['secret']
|
||||||
|
else:
|
||||||
|
key, secret = get_key_secret(provider_info[0])
|
||||||
|
|
||||||
|
headers = get_signed_headers(dataset, key, secret)
|
||||||
|
filenames = glob.glob(os.path.join(datadir, '*.csv'))
|
||||||
|
|
||||||
|
if not filenames:
|
||||||
|
raise MarketplaceNoCSVFiles(datadir=datadir)
|
||||||
|
|
||||||
|
files = []
|
||||||
|
for file in filenames:
|
||||||
|
files.append(('file', open(file, 'rb')))
|
||||||
|
|
||||||
|
r = requests.post('{}/marketplace/publish'.format(AUTH_SERVER),
|
||||||
|
files=files,
|
||||||
|
headers=headers)
|
||||||
|
|
||||||
|
if r.status_code != 200:
|
||||||
|
raise MarketplaceHTTPRequest(request='upload file',
|
||||||
|
error=r.status_code)
|
||||||
|
|
||||||
|
if 'error' in r.json():
|
||||||
|
raise MarketplaceHTTPRequest(request='upload file',
|
||||||
|
error=r.json()['error'])
|
||||||
|
|
||||||
|
print('Dataset {} uploaded successfully.'.format(dataset))
|
||||||
@@ -0,0 +1,88 @@
|
|||||||
|
import sys
|
||||||
|
import traceback
|
||||||
|
|
||||||
|
from catalyst.errors import ZiplineError
|
||||||
|
|
||||||
|
|
||||||
|
def silent_except_hook(exctype, excvalue, exctraceback):
|
||||||
|
if exctype in [MarketplacePubAddressEmpty, MarketplaceDatasetNotFound,
|
||||||
|
MarketplaceNoAddressMatch, MarketplaceHTTPRequest,
|
||||||
|
MarketplaceNoCSVFiles, MarketplaceContractDataNoMatch,
|
||||||
|
MarketplaceSubscriptionExpired, MarketplaceJSONError,
|
||||||
|
MarketplaceWalletNotSupported, MarketplaceEmptySignature]:
|
||||||
|
fn = traceback.extract_tb(exctraceback)[-1][0]
|
||||||
|
ln = traceback.extract_tb(exctraceback)[-1][1]
|
||||||
|
print("Error traceback: {1} (line {2})\n"
|
||||||
|
"{0.__name__}: {3}".format(exctype, fn, ln, excvalue))
|
||||||
|
else:
|
||||||
|
sys.__excepthook__(exctype, excvalue, exctraceback)
|
||||||
|
|
||||||
|
|
||||||
|
sys.excepthook = silent_except_hook
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplacePubAddressEmpty(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Please enter your public address to use in the Data Marketplace '
|
||||||
|
'in the following file: {filename}'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceDatasetNotFound(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'The dataset "{dataset}" is not registered in the Data Marketplace.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceNoAddressMatch(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'The address registered with the dataset {dataset}: {address} '
|
||||||
|
'does not match any of your addresses.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceHTTPRequest(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Request to remote server to {request} failed: {error}'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceNoCSVFiles(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'No CSV files found on {datadir} to upload.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceContractDataNoMatch(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'The information found on the contract does not match the '
|
||||||
|
'requested data:\n{params}.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceSubscriptionExpired(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Your subscription to dataset "{dataset}" expired on {date} '
|
||||||
|
'and is no longer active. You have to subscribe again running the '
|
||||||
|
'following command:\n'
|
||||||
|
'catalyst marketplace subscribe --dataset={dataset}'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceWalletNotSupported(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Wallet {wallet} is not supported.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceEmptySignature(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Signature cannot be empty.'
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class MarketplaceJSONError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'The configuration file {file} is malformed. Please correct '
|
||||||
|
'the following error:\n{error}'
|
||||||
|
)
|
||||||
@@ -0,0 +1,131 @@
|
|||||||
|
import hashlib
|
||||||
|
import hmac
|
||||||
|
|
||||||
|
import requests
|
||||||
|
import time
|
||||||
|
|
||||||
|
from catalyst.marketplace.marketplace_errors import (
|
||||||
|
MarketplaceHTTPRequest, MarketplaceWalletNotSupported,
|
||||||
|
MarketplaceEmptySignature)
|
||||||
|
from catalyst.marketplace.utils.path_utils import (
|
||||||
|
get_user_pubaddr, save_user_pubaddr)
|
||||||
|
from catalyst.constants import AUTH_SERVER
|
||||||
|
|
||||||
|
|
||||||
|
def get_key_secret(pubAddr, wallet='mew'):
|
||||||
|
"""
|
||||||
|
Obtain a new key/secret pair from authentication server
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
pubAddr: str
|
||||||
|
dataset: str
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
key: str
|
||||||
|
secret: str
|
||||||
|
|
||||||
|
"""
|
||||||
|
session = requests.Session()
|
||||||
|
response = session.get('{}/marketplace/getkeysecret'.format(AUTH_SERVER),
|
||||||
|
headers={
|
||||||
|
'Authorization': 'Digest username="{0}"'.format(
|
||||||
|
pubAddr)})
|
||||||
|
|
||||||
|
if response.status_code != 401:
|
||||||
|
raise MarketplaceHTTPRequest(request=str('obtain key/secret'),
|
||||||
|
error='Unexpected response code: '
|
||||||
|
'{}'.format(response.status_code))
|
||||||
|
|
||||||
|
header = response.headers.get('WWW-Authenticate')
|
||||||
|
auth_type, auth_info = header.split(None, 1)
|
||||||
|
d = requests.utils.parse_dict_header(auth_info)
|
||||||
|
|
||||||
|
nonce = '0x{}'.format(d['nonce'])
|
||||||
|
|
||||||
|
if wallet == 'mew':
|
||||||
|
print('\nObtaining a key/secret pair to streamline all future '
|
||||||
|
'requests with the authentication server.\n'
|
||||||
|
'Visit https://www.myetherwallet.com/signmsg.html and sign the'
|
||||||
|
'following message:\n{}'.format(nonce))
|
||||||
|
signature = input('Copy and Paste the "sig" field from '
|
||||||
|
'the signature here (without the double quotes, '
|
||||||
|
'only the HEX value:\n')
|
||||||
|
else:
|
||||||
|
raise MarketplaceWalletNotSupported(wallet=wallet)
|
||||||
|
|
||||||
|
if signature is None:
|
||||||
|
raise MarketplaceEmptySignature()
|
||||||
|
|
||||||
|
signature = signature[2:]
|
||||||
|
r = int(signature[0:64], base=16)
|
||||||
|
s = int(signature[64:128], base=16)
|
||||||
|
v = int(signature[128:130], base=16)
|
||||||
|
vrs = [v, r, s]
|
||||||
|
|
||||||
|
response = session.get('{}/marketplace/getkeysecret'.format(AUTH_SERVER),
|
||||||
|
headers={
|
||||||
|
'Authorization': 'Digest username="{0}",realm="{1}",'
|
||||||
|
'nonce="{2}",uri="/marketplace/getkeysecret",response="{3}",'
|
||||||
|
'opaque="{4}"'.format(pubAddr,
|
||||||
|
d['realm'],
|
||||||
|
d['nonce'],
|
||||||
|
','.join(str(e) for e in vrs+[wallet]),
|
||||||
|
d['opaque'])})
|
||||||
|
|
||||||
|
if response.status_code == 200:
|
||||||
|
|
||||||
|
if 'error' in response.json():
|
||||||
|
raise MarketplaceHTTPRequest(request=str('obtain key/secret'),
|
||||||
|
error=str(response.json()['error']))
|
||||||
|
else:
|
||||||
|
addresses = get_user_pubaddr()
|
||||||
|
|
||||||
|
match = next((l for l in addresses if
|
||||||
|
l['pubAddr'] == pubAddr), None)
|
||||||
|
match['key'] = response.json()['key']
|
||||||
|
match['secret'] = response.json()['secret']
|
||||||
|
|
||||||
|
addresses[addresses.index(match)] = match
|
||||||
|
|
||||||
|
save_user_pubaddr(addresses)
|
||||||
|
print('Key/secret pair retrieved successfully from server.')
|
||||||
|
|
||||||
|
return match['key'], match['secret']
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise MarketplaceHTTPRequest(request=str('obtain key/secret'),
|
||||||
|
error=response.status_code)
|
||||||
|
|
||||||
|
|
||||||
|
def get_signed_headers(ds_name, key, secret):
|
||||||
|
"""
|
||||||
|
Return a new request header including the key / secret signature
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ds_name
|
||||||
|
key
|
||||||
|
secret
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
nonce = str(int(time.time()))
|
||||||
|
|
||||||
|
signature = hmac.new(
|
||||||
|
secret.encode('utf-8'),
|
||||||
|
'{}{}'.format(ds_name, nonce).encode('utf-8'),
|
||||||
|
hashlib.sha512
|
||||||
|
).hexdigest()
|
||||||
|
|
||||||
|
headers = {
|
||||||
|
'Sign': signature,
|
||||||
|
'Key': key,
|
||||||
|
'Nonce': nonce,
|
||||||
|
'Dataset': ds_name,
|
||||||
|
}
|
||||||
|
|
||||||
|
return headers
|
||||||
@@ -0,0 +1,36 @@
|
|||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
|
||||||
|
import bcolz
|
||||||
|
|
||||||
|
|
||||||
|
def merge_bundles(zsource, ztarget):
|
||||||
|
"""
|
||||||
|
Merge
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
zsource
|
||||||
|
ztarget
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
# TODO: find a way to do this iteratively instead of in-memory
|
||||||
|
df_source = zsource.todataframe()
|
||||||
|
df_source.set_index('date', drop=False, inplace=True)
|
||||||
|
df_target = ztarget.todataframe()
|
||||||
|
df_target.set_index('date', drop=False, inplace=True)
|
||||||
|
|
||||||
|
df = df_target.merge(
|
||||||
|
right=df_source,
|
||||||
|
how='right',
|
||||||
|
) # type: pd.DataFrame
|
||||||
|
|
||||||
|
dirname = os.path.basename(ztarget.rootdir)
|
||||||
|
bak_dir = ztarget.rootdir.replace(dirname, '.{}'.format(dirname))
|
||||||
|
os.rename(ztarget.rootdir, bak_dir)
|
||||||
|
|
||||||
|
z = bcolz.ctable.fromdataframe(df=df, rootdir=ztarget.rootdir)
|
||||||
|
shutil.rmtree(bak_dir)
|
||||||
|
return z
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import binascii
|
||||||
|
|
||||||
|
|
||||||
|
# def bytes32(string):
|
||||||
|
# """
|
||||||
|
# Convert string to bytes32 data type for smart contract
|
||||||
|
|
||||||
|
# Parameters
|
||||||
|
# ----------
|
||||||
|
# string: str
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
# -------
|
||||||
|
# list
|
||||||
|
|
||||||
|
# """
|
||||||
|
# return binascii.hexlify(string.encode('utf-8'))
|
||||||
|
|
||||||
|
|
||||||
|
# def b32_str(bytes32):
|
||||||
|
# """
|
||||||
|
# Convert bytes32 to string
|
||||||
|
|
||||||
|
# Parameters
|
||||||
|
# ----------
|
||||||
|
# input: bytes object
|
||||||
|
|
||||||
|
# Returns
|
||||||
|
# -------
|
||||||
|
# str
|
||||||
|
|
||||||
|
# """
|
||||||
|
# return binascii.unhexlify(
|
||||||
|
# bytes32.decode('utf-8').rstrip('\0')).decode('ascii')
|
||||||
|
|
||||||
|
|
||||||
|
def bin_hex(binary):
|
||||||
|
"""
|
||||||
|
Convert bytes32 to string
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
input: bytes object
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
return binascii.hexlify(binary).decode('utf-8')
|
||||||
|
|
||||||
|
|
||||||
|
def from_grains(amount):
|
||||||
|
"""
|
||||||
|
Convert from grains to cryptocurrency
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
input: amount
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
|
||||||
|
"""
|
||||||
|
return amount // 10 ** 8
|
||||||
|
|
||||||
|
|
||||||
|
def to_grains(amount):
|
||||||
|
"""
|
||||||
|
Convert from cryptocurrency to grains
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
input: amount
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
|
||||||
|
"""
|
||||||
|
return amount * 10 ** 8
|
||||||
@@ -0,0 +1,166 @@
|
|||||||
|
import os
|
||||||
|
import json
|
||||||
|
import tarfile
|
||||||
|
|
||||||
|
from catalyst.utils.deprecate import deprecated
|
||||||
|
from catalyst.utils.paths import data_root, ensure_directory
|
||||||
|
from catalyst.marketplace.marketplace_errors import MarketplaceJSONError
|
||||||
|
|
||||||
|
|
||||||
|
def get_marketplace_folder(environ=None):
|
||||||
|
"""
|
||||||
|
The root path of the marketplace folder.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
environ:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not environ:
|
||||||
|
environ = os.environ
|
||||||
|
|
||||||
|
root = data_root(environ)
|
||||||
|
marketplace_folder = os.path.join(root, 'marketplace')
|
||||||
|
ensure_directory(marketplace_folder)
|
||||||
|
|
||||||
|
return marketplace_folder
|
||||||
|
|
||||||
|
|
||||||
|
def get_data_source_folder(data_source_name, environ=None):
|
||||||
|
"""
|
||||||
|
The root path of an data_source folder.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data_source_name: str
|
||||||
|
environ:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
if not environ:
|
||||||
|
environ = os.environ
|
||||||
|
|
||||||
|
root = data_root(environ)
|
||||||
|
data_source_folder = os.path.join(root, 'marketplace', data_source_name)
|
||||||
|
ensure_directory(data_source_folder)
|
||||||
|
|
||||||
|
return data_source_folder
|
||||||
|
|
||||||
|
|
||||||
|
@deprecated
|
||||||
|
def get_bundle_folder(data_source_name, data_frequency, environ=None):
|
||||||
|
data_source_folder = get_data_source_folder(data_source_name, environ)
|
||||||
|
|
||||||
|
bundle_folder = os.path.join(data_source_folder, data_frequency)
|
||||||
|
|
||||||
|
ensure_directory(bundle_folder)
|
||||||
|
|
||||||
|
return bundle_folder
|
||||||
|
|
||||||
|
|
||||||
|
def get_temp_bundles_folder(environ=None):
|
||||||
|
"""
|
||||||
|
The temp folder for bundle downloads by algo name.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ds_name: str
|
||||||
|
environ:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
root = data_root(environ)
|
||||||
|
folder = os.path.join(root, 'marketplace', 'temp_bundles')
|
||||||
|
ensure_directory(folder)
|
||||||
|
|
||||||
|
return folder
|
||||||
|
|
||||||
|
|
||||||
|
def extract_bundle(tar_filename):
|
||||||
|
"""
|
||||||
|
Extract a bcolz bundle.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
ds_name
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
str
|
||||||
|
|
||||||
|
"""
|
||||||
|
target_path = tar_filename.replace('.tar.gz', '')
|
||||||
|
with tarfile.open(tar_filename, 'r') as tar:
|
||||||
|
tar.extractall(target_path)
|
||||||
|
|
||||||
|
return target_path
|
||||||
|
|
||||||
|
|
||||||
|
def get_user_pubaddr(environ=None):
|
||||||
|
"""
|
||||||
|
The de-serialized contend of the user's addresses.json file.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
environ:
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
Object
|
||||||
|
|
||||||
|
"""
|
||||||
|
marketplace_folder = get_marketplace_folder(environ)
|
||||||
|
filename = os.path.join(marketplace_folder, 'addresses.json')
|
||||||
|
|
||||||
|
if os.path.isfile(filename):
|
||||||
|
with open(filename) as data_file:
|
||||||
|
try:
|
||||||
|
data = json.load(data_file)
|
||||||
|
except json.decoder.JSONDecodeError as e:
|
||||||
|
raise MarketplaceJSONError(file=filename, error=e)
|
||||||
|
try:
|
||||||
|
d = data[0]['pubAddr']
|
||||||
|
except Exception as e:
|
||||||
|
return [data, ]
|
||||||
|
return data
|
||||||
|
else:
|
||||||
|
data = []
|
||||||
|
data.append(dict(pubAddr='', desc=''))
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
json.dump(data, f, sort_keys=False, indent=2,
|
||||||
|
separators=(',', ':'))
|
||||||
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def save_user_pubaddr(data, environ=None):
|
||||||
|
"""
|
||||||
|
Saves the user's public addresses and their related metadata in
|
||||||
|
the corresponding addresses.json file.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
data: dict
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
True
|
||||||
|
|
||||||
|
"""
|
||||||
|
marketplace_folder = get_marketplace_folder(environ)
|
||||||
|
filename = os.path.join(marketplace_folder, 'addresses.json')
|
||||||
|
|
||||||
|
with open(filename, 'w') as f:
|
||||||
|
json.dump(data, f, sort_keys=False, indent=2,
|
||||||
|
separators=(',', ':'))
|
||||||
|
|
||||||
|
return True
|
||||||
@@ -142,7 +142,7 @@ class TermGraph(object):
|
|||||||
at the end of execution.
|
at the end of execution.
|
||||||
"""
|
"""
|
||||||
refcounts = self.graph.out_degree()
|
refcounts = self.graph.out_degree()
|
||||||
for t in self.outputs.values():
|
for t in list(self.outputs.values()):
|
||||||
refcounts[t] += 1
|
refcounts[t] += 1
|
||||||
|
|
||||||
for t in initial_terms:
|
for t in initial_terms:
|
||||||
@@ -238,7 +238,7 @@ class ExecutionPlan(TermGraph):
|
|||||||
min_extra_rows=0):
|
min_extra_rows=0):
|
||||||
super(ExecutionPlan, self).__init__(terms)
|
super(ExecutionPlan, self).__init__(terms)
|
||||||
|
|
||||||
for term in terms.values():
|
for term in list(terms.values()):
|
||||||
self.set_extra_rows(
|
self.set_extra_rows(
|
||||||
term,
|
term,
|
||||||
all_dates,
|
all_dates,
|
||||||
|
|||||||
@@ -144,7 +144,7 @@ class SpecificEquityTrades(object):
|
|||||||
for identifier in self.identifiers:
|
for identifier in self.identifiers:
|
||||||
assets_by_identifier[identifier] = env.asset_finder.\
|
assets_by_identifier[identifier] = env.asset_finder.\
|
||||||
lookup_generic(identifier, datetime.now())[0]
|
lookup_generic(identifier, datetime.now())[0]
|
||||||
self.sids = [asset.sid for asset in assets_by_identifier.values()]
|
self.sids = [asset.sid for asset in list(assets_by_identifier.values())]
|
||||||
for event in self.event_list:
|
for event in self.event_list:
|
||||||
event.sid = assets_by_identifier[event.sid].sid
|
event.sid = assets_by_identifier[event.sid].sid
|
||||||
|
|
||||||
@@ -167,7 +167,7 @@ class SpecificEquityTrades(object):
|
|||||||
for identifier in self.identifiers:
|
for identifier in self.identifiers:
|
||||||
assets_by_identifier[identifier] = env.asset_finder.\
|
assets_by_identifier[identifier] = env.asset_finder.\
|
||||||
lookup_generic(identifier, datetime.now())[0]
|
lookup_generic(identifier, datetime.now())[0]
|
||||||
self.sids = [asset.sid for asset in assets_by_identifier.values()]
|
self.sids = [asset.sid for asset in list(assets_by_identifier.values())]
|
||||||
|
|
||||||
# Hash_value for downstream sorting.
|
# Hash_value for downstream sorting.
|
||||||
self.arg_string = hash_args(*args, **kwargs)
|
self.arg_string = hash_args(*args, **kwargs)
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
context.i = -1 # counts the minutes
|
||||||
|
context.exchange = 'cryptopia'
|
||||||
|
context.base_currency = 'btc'
|
||||||
|
context.coins = context.exchanges[context.exchange].assets
|
||||||
|
context.coins = [c for c in context.coins if
|
||||||
|
c.quote_currency == context.base_currency]
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
# current date formatted into a string
|
||||||
|
today = data.current_dt
|
||||||
|
|
||||||
|
# update universe everyday
|
||||||
|
new_day = 60 * 24 # assuming data_frequency='minute'
|
||||||
|
if not context.i % new_day:
|
||||||
|
context.coins = context.exchanges[context.exchange].assets
|
||||||
|
context.coins = [c for c in context.coins if
|
||||||
|
c.quote_currency == context.base_currency]
|
||||||
|
|
||||||
|
# get data every 30 minutes
|
||||||
|
minutes = 1
|
||||||
|
if not context.i % minutes:
|
||||||
|
# we iterate for every pair in the current universe
|
||||||
|
for coin in context.coins:
|
||||||
|
pair = str(coin.symbol)
|
||||||
|
|
||||||
|
price = data.current(coin, 'price')
|
||||||
|
print(today, pair, price)
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context=None, results=None):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
start_date = pd.to_datetime('2018-01-17', utc=True)
|
||||||
|
end_date = pd.to_datetime('2018-01-18', utc=True)
|
||||||
|
|
||||||
|
performance = run_algorithm(
|
||||||
|
capital_base=1.0,
|
||||||
|
# amount of base_currency, not always in dollars unless usd
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='cryptopia',
|
||||||
|
data_frequency='minute',
|
||||||
|
base_currency='btc',
|
||||||
|
live=True,
|
||||||
|
live_graph=False,
|
||||||
|
simulate_orders=True,
|
||||||
|
algo_namespace='simple_universe'
|
||||||
|
)
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
import ccxt
|
||||||
|
|
||||||
|
bitfinex = ccxt.bitfinex()
|
||||||
|
bitfinex.verbose = True
|
||||||
|
ohlcvs = bitfinex.fetch_ohlcv('ETH/BTC', '30m', 1504224000000)
|
||||||
|
|
||||||
|
dt = bitfinex.iso8601(ohlcvs[0][0])
|
||||||
|
print(dt) # should print '2017-09-01T00:00:00.000Z'
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import symbol
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
context.asset1 = symbol('fct_btc')
|
||||||
|
context.asset2 = symbol('btc_usdt')
|
||||||
|
context.coins = [context.asset1, context.asset2]
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
df = data.history(context.coins,
|
||||||
|
'close',
|
||||||
|
bar_count=10,
|
||||||
|
frequency='5T',
|
||||||
|
)
|
||||||
|
print(df)
|
||||||
|
print(data.current(context.asset1, 'close'))
|
||||||
|
print(data.current(context.asset2, 'close'))
|
||||||
|
exit(0)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
LIVE = True
|
||||||
|
if LIVE:
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=1,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
algo_namespace='test_multi_assets',
|
||||||
|
base_currency='usdt',
|
||||||
|
live=True,
|
||||||
|
simulate_orders=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=1,
|
||||||
|
data_frequency='minute',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
algo_namespace='test_multi_assets',
|
||||||
|
base_currency='usdt',
|
||||||
|
live=False,
|
||||||
|
start=pd.to_datetime('2017-12-1', utc=True),
|
||||||
|
end=pd.to_datetime('2017-12-1', utc=True),
|
||||||
|
)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
from logbook import Logger
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import order_target_percent
|
||||||
|
|
||||||
|
NAMESPACE = 'goose7'
|
||||||
|
log = Logger(NAMESPACE)
|
||||||
|
|
||||||
|
from catalyst.api import record, symbol
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
context.asset = symbol('trx_btc')
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
price = data.current(context.asset, 'price')
|
||||||
|
record(btc=price)
|
||||||
|
|
||||||
|
# Only ordering if it does not have any position to avoid trying some
|
||||||
|
# tiny orders with the leftover btc
|
||||||
|
pos_amount = context.portfolio.positions[context.asset].amount
|
||||||
|
if pos_amount > 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
# Adding a limit price to workaround an issue with performance
|
||||||
|
# calculations of market orders
|
||||||
|
order_target_percent(
|
||||||
|
context.asset, 1, limit_price=price * 1.01
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=0.003,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
exchange_name='binance',
|
||||||
|
live=True,
|
||||||
|
algo_namespace=NAMESPACE,
|
||||||
|
base_currency='btc',
|
||||||
|
live_graph=False,
|
||||||
|
simulate_orders=False,
|
||||||
|
)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
from catalyst import run_algorithm
|
||||||
|
from catalyst.api import symbol
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
context.asset = symbol('btc_usdt')
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
df = data.history(context.asset,
|
||||||
|
'close',
|
||||||
|
bar_count=10,
|
||||||
|
frequency='5T',
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
LIVE = True
|
||||||
|
if LIVE:
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=1,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
algo_namespace='test_algo',
|
||||||
|
base_currency='usdt',
|
||||||
|
live=True,
|
||||||
|
simulate_orders=True,
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=1,
|
||||||
|
data_frequency='minute',
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
algo_namespace='test_algo',
|
||||||
|
base_currency='usdt',
|
||||||
|
live=False,
|
||||||
|
start=pd.to_datetime('2017-12-1', utc=True),
|
||||||
|
end=pd.to_datetime('2017-12-1', utc=True),
|
||||||
|
)
|
||||||
@@ -0,0 +1,44 @@
|
|||||||
|
import pandas as pd
|
||||||
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
from catalyst.api import symbol
|
||||||
|
from exchange.utils.stats_utils import set_print_settings
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
context.i = 0
|
||||||
|
context.data = []
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data(context, data):
|
||||||
|
prices = data.history(
|
||||||
|
symbol('xlm_eth'),
|
||||||
|
fields=['open', 'high', 'low', 'close'],
|
||||||
|
bar_count=50,
|
||||||
|
frequency='1T'
|
||||||
|
)
|
||||||
|
set_print_settings()
|
||||||
|
print(prices.tail(10))
|
||||||
|
context.data.append(prices)
|
||||||
|
|
||||||
|
context.i = context.i + 1
|
||||||
|
if context.i == 3:
|
||||||
|
context.interrupt_algorithm()
|
||||||
|
|
||||||
|
|
||||||
|
def analyze(context, prefs):
|
||||||
|
for dataset in context.data:
|
||||||
|
print(dataset[-2:])
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
run_algorithm(
|
||||||
|
capital_base=0.1,
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='binance',
|
||||||
|
algo_namespace='Test candles',
|
||||||
|
base_currency='eth',
|
||||||
|
data_frequency='minute',
|
||||||
|
live=True,
|
||||||
|
simulate_orders=True)
|
||||||
@@ -8,6 +8,8 @@ from time import sleep
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from six import string_types
|
||||||
|
|
||||||
from catalyst.data.bundles import load
|
from catalyst.data.bundles import load
|
||||||
from catalyst.data.data_portal import DataPortal
|
from catalyst.data.data_portal import DataPortal
|
||||||
from catalyst.exchange.exchange_pricing_loader import ExchangePricingLoader, \
|
from catalyst.exchange.exchange_pricing_loader import ExchangePricingLoader, \
|
||||||
@@ -91,11 +93,13 @@ def _run(handle_data,
|
|||||||
live_graph,
|
live_graph,
|
||||||
analyze_live,
|
analyze_live,
|
||||||
simulate_orders,
|
simulate_orders,
|
||||||
|
auth_aliases,
|
||||||
stats_output):
|
stats_output):
|
||||||
"""Run a backtest for the given algorithm.
|
"""Run a backtest for the given algorithm.
|
||||||
|
|
||||||
This is shared between the cli and :func:`catalyst.run_algo`.
|
This is shared between the cli and :func:`catalyst.run_algo`.
|
||||||
"""
|
"""
|
||||||
|
# TODO: refactor for more granularity
|
||||||
if algotext is not None:
|
if algotext is not None:
|
||||||
if local_namespace:
|
if local_namespace:
|
||||||
ip = get_ipython() # noqa
|
ip = get_ipython() # noqa
|
||||||
@@ -162,15 +166,31 @@ def _run(handle_data,
|
|||||||
if exchange_name is None:
|
if exchange_name is None:
|
||||||
raise ValueError('Please specify at least one exchange.')
|
raise ValueError('Please specify at least one exchange.')
|
||||||
|
|
||||||
exchange_list = [x.strip().lower() for x in exchange.split(',')]
|
if isinstance(auth_aliases, string_types):
|
||||||
|
aliases = auth_aliases.split(',')
|
||||||
|
if len(aliases) < 2 or len(aliases) % 2 != 0:
|
||||||
|
raise ValueError(
|
||||||
|
'the `auth_aliases` parameter must contain an even list '
|
||||||
|
'of comma-delimited values. For example, '
|
||||||
|
'"binance,auth2" or "binance,auth2,bittrex,auth2".'
|
||||||
|
)
|
||||||
|
|
||||||
|
auth_aliases = dict(zip(aliases[::2], aliases[1::2]))
|
||||||
|
|
||||||
|
exchange_list = [x.strip().lower() for x in exchange.split(',')]
|
||||||
exchanges = dict()
|
exchanges = dict()
|
||||||
for exchange_name in exchange_list:
|
for name in exchange_list:
|
||||||
exchanges[exchange_name] = get_exchange(
|
if auth_aliases is not None and name in auth_aliases:
|
||||||
exchange_name=exchange_name,
|
auth_alias = auth_aliases[name]
|
||||||
|
else:
|
||||||
|
auth_alias = None
|
||||||
|
|
||||||
|
exchanges[name] = get_exchange(
|
||||||
|
exchange_name=name,
|
||||||
base_currency=base_currency,
|
base_currency=base_currency,
|
||||||
must_authenticate=(live and not simulate_orders),
|
must_authenticate=(live and not simulate_orders),
|
||||||
skip_init=True,
|
skip_init=True,
|
||||||
|
auth_alias=auth_alias,
|
||||||
)
|
)
|
||||||
|
|
||||||
open_calendar = get_calendar('OPEN')
|
open_calendar = get_calendar('OPEN')
|
||||||
@@ -200,7 +220,8 @@ def _run(handle_data,
|
|||||||
start = pd.Timestamp.utcnow()
|
start = pd.Timestamp.utcnow()
|
||||||
|
|
||||||
# TODO: fix the end data.
|
# TODO: fix the end data.
|
||||||
end = start + timedelta(hours=8760)
|
if end is None:
|
||||||
|
end = start + timedelta(hours=8760)
|
||||||
|
|
||||||
data = DataPortalExchangeLive(
|
data = DataPortalExchangeLive(
|
||||||
exchanges=exchanges,
|
exchanges=exchanges,
|
||||||
@@ -228,6 +249,7 @@ def _run(handle_data,
|
|||||||
simulate_orders=simulate_orders,
|
simulate_orders=simulate_orders,
|
||||||
stats_output=stats_output,
|
stats_output=stats_output,
|
||||||
analyze_live=analyze_live,
|
analyze_live=analyze_live,
|
||||||
|
end=end,
|
||||||
)
|
)
|
||||||
elif exchanges:
|
elif exchanges:
|
||||||
# Removed the existing Poloniex fork to keep things simple
|
# Removed the existing Poloniex fork to keep things simple
|
||||||
@@ -391,6 +413,7 @@ def run_algorithm(initialize,
|
|||||||
live_graph=False,
|
live_graph=False,
|
||||||
analyze_live=None,
|
analyze_live=None,
|
||||||
simulate_orders=True,
|
simulate_orders=True,
|
||||||
|
auth_aliases=None,
|
||||||
stats_output=None,
|
stats_output=None,
|
||||||
output=os.devnull):
|
output=os.devnull):
|
||||||
"""Run a trading algorithm.
|
"""Run a trading algorithm.
|
||||||
@@ -524,5 +547,6 @@ def run_algorithm(initialize,
|
|||||||
live_graph=live_graph,
|
live_graph=live_graph,
|
||||||
analyze_live=analyze_live,
|
analyze_live=analyze_live,
|
||||||
simulate_orders=simulate_orders,
|
simulate_orders=simulate_orders,
|
||||||
|
auth_aliases=auth_aliases,
|
||||||
stats_output=stats_output
|
stats_output=stats_output
|
||||||
)
|
)
|
||||||
|
|||||||
+5
-5
@@ -23,7 +23,7 @@ I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
|||||||
|
|
||||||
help:
|
help:
|
||||||
@echo "Please use \`make <target>' where <target> is one of"
|
@echo "Please use \`make <target>' where <target> is one of"
|
||||||
@echo " build to build the C and Cython extensions for zipline"
|
@echo " build to build the C and Cython extensions for catalyst"
|
||||||
@echo " html to make standalone HTML files"
|
@echo " html to make standalone HTML files"
|
||||||
@echo " livehtml to run a persistent process that rebuilds the docs"
|
@echo " livehtml to run a persistent process that rebuilds the docs"
|
||||||
@echo " dirhtml to make HTML files named index.html in directories"
|
@echo " dirhtml to make HTML files named index.html in directories"
|
||||||
@@ -96,9 +96,9 @@ qthelp: build
|
|||||||
@echo
|
@echo
|
||||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||||
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
".qhcp project file in $(BUILDDIR)/qthelp, like this:"
|
||||||
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/zipline.qhcp"
|
@echo "# qcollectiongenerator $(BUILDDIR)/qthelp/catalyst.qhcp"
|
||||||
@echo "To view the help file:"
|
@echo "To view the help file:"
|
||||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/zipline.qhc"
|
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/catalyst.qhc"
|
||||||
|
|
||||||
applehelp: build
|
applehelp: build
|
||||||
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
||||||
@@ -113,8 +113,8 @@ devhelp: build
|
|||||||
@echo
|
@echo
|
||||||
@echo "Build finished."
|
@echo "Build finished."
|
||||||
@echo "To view the help file:"
|
@echo "To view the help file:"
|
||||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/zipline"
|
@echo "# mkdir -p $$HOME/.local/share/devhelp/catalyst"
|
||||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/zipline"
|
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/catalyst"
|
||||||
@echo "# devhelp"
|
@echo "# devhelp"
|
||||||
|
|
||||||
epub: build
|
epub: build
|
||||||
|
|||||||
+5
-5
@@ -8,8 +8,8 @@ from shutil import move, rmtree
|
|||||||
from subprocess import check_call
|
from subprocess import check_call
|
||||||
|
|
||||||
HERE = dirname(abspath(__file__))
|
HERE = dirname(abspath(__file__))
|
||||||
ZIPLINE_ROOT = dirname(HERE)
|
CATALYST_ROOT = dirname(HERE)
|
||||||
TEMP_LOCATION = '/tmp/zipline-doc'
|
TEMP_LOCATION = '/tmp/catalyst-doc'
|
||||||
TEMP_LOCATION_GLOB = TEMP_LOCATION + '/*'
|
TEMP_LOCATION_GLOB = TEMP_LOCATION + '/*'
|
||||||
|
|
||||||
|
|
||||||
@@ -46,8 +46,8 @@ def main():
|
|||||||
print("Copying built files to temp location.")
|
print("Copying built files to temp location.")
|
||||||
move('build/html', TEMP_LOCATION)
|
move('build/html', TEMP_LOCATION)
|
||||||
|
|
||||||
print("Moving to '%s'" % ZIPLINE_ROOT)
|
print("Moving to '%s'" % CATALYST_ROOT)
|
||||||
os.chdir(ZIPLINE_ROOT)
|
os.chdir(CATALYST_ROOT)
|
||||||
|
|
||||||
print("Checking out gh-pages branch.")
|
print("Checking out gh-pages branch.")
|
||||||
check_call(
|
check_call(
|
||||||
@@ -70,7 +70,7 @@ def main():
|
|||||||
os.chdir(old_dir)
|
os.chdir(old_dir)
|
||||||
|
|
||||||
print()
|
print()
|
||||||
print("Updated documentation branch in directory %s" % ZIPLINE_ROOT)
|
print("Updated documentation branch in directory %s" % CATALYST_ROOT)
|
||||||
print("If you are happy with these changes, commit and push to gh-pages.")
|
print("If you are happy with these changes, commit and push to gh-pages.")
|
||||||
|
|
||||||
if __name__ == '__main__':
|
if __name__ == '__main__':
|
||||||
|
|||||||
+2
-2
@@ -127,9 +127,9 @@ if "%1" == "qthelp" (
|
|||||||
echo.
|
echo.
|
||||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||||
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
.qhcp project file in %BUILDDIR%/qthelp, like this:
|
||||||
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\zipline.qhcp
|
echo.^> qcollectiongenerator %BUILDDIR%\qthelp\catalyst.qhcp
|
||||||
echo.To view the help file:
|
echo.To view the help file:
|
||||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\zipline.ghc
|
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\catalyst.ghc
|
||||||
goto end
|
goto end
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -483,7 +483,7 @@ bitcoin price.
|
|||||||
|
|
||||||
Now we will run the simulation again, but this time we extend our original
|
Now we will run the simulation again, but this time we extend our original
|
||||||
algorithm with the addition of the ``analyze()`` function. Somewhat analogously
|
algorithm with the addition of the ``analyze()`` function. Somewhat analogously
|
||||||
as how ``initialize()`` gets called once before the start of the algorith,
|
as how ``initialize()`` gets called once before the start of the algorithm,
|
||||||
``analyze()`` gets called once at the end of the algorithm, and receives two
|
``analyze()`` gets called once at the end of the algorithm, and receives two
|
||||||
variables: ``context``, which we discussed at the very beginning, and ``perf``,
|
variables: ``context``, which we discussed at the very beginning, and ``perf``,
|
||||||
which is the pandas dataframe containing the performance data for our algorithm
|
which is the pandas dataframe containing the performance data for our algorithm
|
||||||
@@ -589,7 +589,7 @@ the ``examples`` directory:
|
|||||||
from catalyst import run_algorithm
|
from catalyst import run_algorithm
|
||||||
from catalyst.api import (order, record, symbol, order_target_percent,
|
from catalyst.api import (order, record, symbol, order_target_percent,
|
||||||
get_open_orders)
|
get_open_orders)
|
||||||
from catalyst.exchange.stats_utils import extract_transactions
|
from catalyst.exchange.utils.stats_utils import extract_transactions
|
||||||
|
|
||||||
NAMESPACE = 'dual_moving_average'
|
NAMESPACE = 'dual_moving_average'
|
||||||
log = Logger(NAMESPACE)
|
log = Logger(NAMESPACE)
|
||||||
@@ -660,7 +660,8 @@ the ``examples`` directory:
|
|||||||
def analyze(context, perf):
|
def analyze(context, perf):
|
||||||
|
|
||||||
# Get the base_currency that was passed as a parameter to the simulation
|
# Get the base_currency that was passed as a parameter to the simulation
|
||||||
base_currency = context.exchanges.values()[0].base_currency.upper()
|
exchange = list(context.exchanges.values())[0]
|
||||||
|
base_currency = exchange.base_currency.upper()
|
||||||
|
|
||||||
# First chart: Plot portfolio value using base_currency
|
# First chart: Plot portfolio value using base_currency
|
||||||
ax1 = plt.subplot(411)
|
ax1 = plt.subplot(411)
|
||||||
|
|||||||
+2
-2
@@ -41,11 +41,11 @@ master_doc = 'index'
|
|||||||
|
|
||||||
# General information about the project.
|
# General information about the project.
|
||||||
project = u'Catalyst'
|
project = u'Catalyst'
|
||||||
copyright = u'2017, Enigma MPC, Inc.'
|
copyright = u'2018, Enigma MPC, Inc.'
|
||||||
|
|
||||||
# The full version, including alpha/beta/rc tags, but excluding the commit hash
|
# The full version, including alpha/beta/rc tags, but excluding the commit hash
|
||||||
#release = version.split('+', 1)[0]
|
#release = version.split('+', 1)[0]
|
||||||
release = '0.3'
|
release = '0.4'
|
||||||
|
|
||||||
# List of patterns, relative to source directory, that match files and
|
# List of patterns, relative to source directory, that match files and
|
||||||
# directories to ignore when looking for source files.
|
# directories to ignore when looking for source files.
|
||||||
|
|||||||
@@ -84,6 +84,25 @@ To build and view the docs locally, run:
|
|||||||
$ {BROWSER} build/html/index.html
|
$ {BROWSER} build/html/index.html
|
||||||
|
|
||||||
|
|
||||||
|
There is a `documented issue <https://github.com/sphinx-doc/sphinx/issues/3212>`_
|
||||||
|
with ``sphinx`` and ``docutils`` that causes the error below when trying to build
|
||||||
|
the docs.
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
Exception occurred:
|
||||||
|
File "(...)/env-c/lib/python2.7/site-packages/docutils/writers/_html_base.py", line 671, in depart_document
|
||||||
|
assert not self.context, 'len(context) = %s' % len(self.context)
|
||||||
|
AssertionError: len(context) = 3
|
||||||
|
|
||||||
|
If you get this error, you need to downgrade your version of ``docutils`` as
|
||||||
|
follows, and build the docs again:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ pip install docutils==0.12
|
||||||
|
|
||||||
|
|
||||||
Commit messages
|
Commit messages
|
||||||
---------------
|
---------------
|
||||||
|
|
||||||
|
|||||||
@@ -805,7 +805,7 @@ Credits: This code was originally submitted by `Abner Ayala-Acevedo
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from catalyst import run_algorithm
|
from catalyst import run_algorithm
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_symbols
|
from catalyst.exchange.utils.exchange_utils import get_exchange_symbols
|
||||||
from catalyst.api import (symbols, )
|
from catalyst.api import (symbols, )
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -44,11 +44,11 @@ For additional details on the functionality added on recent releases, see the
|
|||||||
Upcoming features
|
Upcoming features
|
||||||
~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
* Additional datasets beyond pricing data (Dec. 2017)
|
* Additional datasets beyond pricing data (Q1 2018)
|
||||||
* API documentation (Jan. 2017)
|
* API documentation (Q1 2018)
|
||||||
* Support for decentralized exchanges (Jan. 2017)
|
* Support for decentralized exchanges (Q1 2018)
|
||||||
* Support for data ingestion of community-contributed data sets (Jan. 2017)
|
* Support for data ingestion of community-contributed data sets (Q1 2018)
|
||||||
* Pipeline support (Jan. 2018)
|
* Pipeline support (Q1 2018)
|
||||||
* Web UI (Q2 2018)
|
* Web UI (Q2 2018)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
+70
-30
@@ -180,20 +180,6 @@ use a single tool to install Python and non-Python dependencies, or if you're
|
|||||||
already using `Anaconda <http://continuum.io/downloads>`_ as your Python
|
already using `Anaconda <http://continuum.io/downloads>`_ as your Python
|
||||||
distribution, refer to the :ref:`Installing with Conda <conda>` section.
|
distribution, refer to the :ref:`Installing with Conda <conda>` section.
|
||||||
|
|
||||||
Once you've installed the necessary additional dependencies for your system
|
|
||||||
(see below for your particular platform: :ref:`Linux`, :ref:`MacOS` or
|
|
||||||
:ref:`Windows`), you should be able to simply run
|
|
||||||
|
|
||||||
.. code-block:: bash
|
|
||||||
|
|
||||||
$ pip install enigma-catalyst matplotlib
|
|
||||||
|
|
||||||
Note that in the command above we install two different packages. The second
|
|
||||||
one, ``matplotlib`` is a visualization library. While it's not strictly
|
|
||||||
required to run catalyst simulations or live trading, it comes in very handy
|
|
||||||
to visualize the performance of your algorithms, and for this reason we
|
|
||||||
recommend you install it, as well.
|
|
||||||
|
|
||||||
If you use Python for anything other than Catalyst, we **strongly** recommend
|
If you use Python for anything other than Catalyst, we **strongly** recommend
|
||||||
that you install in a `virtualenv
|
that you install in a `virtualenv
|
||||||
<https://virtualenv.readthedocs.org/en/latest>`_. The `Hitchhiker's Guide to
|
<https://virtualenv.readthedocs.org/en/latest>`_. The `Hitchhiker's Guide to
|
||||||
@@ -206,8 +192,21 @@ summarized version:
|
|||||||
$ pip install virtualenv
|
$ pip install virtualenv
|
||||||
$ virtualenv catalyst-venv
|
$ virtualenv catalyst-venv
|
||||||
$ source ./catalyst-venv/bin/activate
|
$ source ./catalyst-venv/bin/activate
|
||||||
|
|
||||||
|
Once you've installed the necessary additional dependencies for your system
|
||||||
|
(:ref:`Linux`, :ref:`MacOS` or :ref:`Windows`) **and have activated your virtualenv**, you should be able to simply run
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
$ pip install enigma-catalyst matplotlib
|
$ pip install enigma-catalyst matplotlib
|
||||||
|
|
||||||
|
Note that in the command above we install two different packages. The second
|
||||||
|
one, ``matplotlib`` is a visualization library. While it's not strictly
|
||||||
|
required to run catalyst simulations or live trading, it comes in very handy
|
||||||
|
to visualize the performance of your algorithms, and for this reason we
|
||||||
|
recommend you install it, as well.
|
||||||
|
|
||||||
|
|
||||||
Troubleshooting ``pip`` Install
|
Troubleshooting ``pip`` Install
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
@@ -219,13 +218,13 @@ Troubleshooting ``pip`` Install
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pip install --upgrade pip
|
$ pip install --upgrade pip
|
||||||
|
|
||||||
On Windows, the recommended command is:
|
On Windows, the recommended command is:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
python -m pip install --upgrade pip
|
$ python -m pip install --upgrade pip
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -251,7 +250,7 @@ Troubleshooting ``pip`` Install
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pip install --pre enigma-catalyst
|
$ pip install --pre enigma-catalyst
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -263,7 +262,7 @@ Troubleshooting ``pip`` Install
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pip install --upgrade pip setuptools
|
$ pip install --upgrade pip setuptools
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -278,7 +277,7 @@ Troubleshooting ``pip`` Install
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pip install -r requirements.txt
|
$ pip install -r requirements.txt
|
||||||
|
|
||||||
----
|
----
|
||||||
|
|
||||||
@@ -294,7 +293,7 @@ Troubleshooting ``pip`` Install
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
sudo apt-get install python-dev
|
$ sudo apt-get install python-dev
|
||||||
|
|
||||||
.. _pipenv:
|
.. _pipenv:
|
||||||
|
|
||||||
@@ -376,14 +375,14 @@ outdated. Thus, you first need to run:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
pip install --upgrade pip setuptools
|
$ pip install --upgrade pip setuptools
|
||||||
|
|
||||||
The default installation is also missing the C and C++ compilers, which you
|
The default installation is also missing the C and C++ compilers, which you
|
||||||
install by:
|
install by:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
sudo yum install gcc gcc-c++
|
$ sudo yum install gcc gcc-c++
|
||||||
|
|
||||||
Then you should follow the regular installation instructions outlined at the
|
Then you should follow the regular installation instructions outlined at the
|
||||||
beginning of this page.
|
beginning of this page.
|
||||||
@@ -408,20 +407,34 @@ following brew packages:
|
|||||||
|
|
||||||
$ brew install freetype pkg-config gcc openssl
|
$ brew install freetype pkg-config gcc openssl
|
||||||
|
|
||||||
MacOS + virtualenv + matplotlib
|
MacOS + virtualenv/conda + matplotlib
|
||||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||||
|
|
||||||
A note about using matplotlib in virtual enviroments on MacOS: it may be
|
The first time that you try to run an algorithm that loads the ``matplotlib``
|
||||||
necessary to run
|
library, you may get the following error:
|
||||||
|
|
||||||
|
.. code-block:: text
|
||||||
|
|
||||||
|
RuntimeError: Python is not installed as a framework. The Mac OS X backend
|
||||||
|
will not be able to function correctly if Python is not installed as a
|
||||||
|
framework. See the Python documentation for more information on installing
|
||||||
|
Python as a framework on Mac OS X. Please either reinstall Python as a
|
||||||
|
framework, or try one of the other backends. If you are using (Ana)Conda
|
||||||
|
please install python.app and replace the use of 'python' with 'pythonw'.
|
||||||
|
See 'Working with Matplotlib on OSX' in the Matplotlib FAQ for more
|
||||||
|
information.
|
||||||
|
|
||||||
|
This is a ``matplotlib``-specific error, that will go away once you run the
|
||||||
|
following command:
|
||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
echo "backend: TkAgg" > ~/.matplotlib/matplotlibrc
|
$ echo "backend: TkAgg" > ~/.matplotlib/matplotlibrc
|
||||||
|
|
||||||
in order to override the default ``MacOS`` backend for your system, which
|
in order to override the default ``MacOS`` backend for your system, which
|
||||||
may not be accessible from inside the virtual environment. This will allow
|
may not be accessible from inside the virtual or conda environment. This will
|
||||||
Catalyst to open matplotlib charts from within a virtual environment, which
|
allow Catalyst to open matplotlib charts from within a virtual environment,
|
||||||
is useful for displaying the performance of your backtests. To learn more
|
which is useful for displaying the performance of your backtests. To learn more
|
||||||
about matplotlib backends, please refer to the
|
about matplotlib backends, please refer to the
|
||||||
`matplotlib backend documentation <https://matplotlib.org/faq/usage_faq.html#what-is-a-backend>`_.
|
`matplotlib backend documentation <https://matplotlib.org/faq/usage_faq.html#what-is-a-backend>`_.
|
||||||
|
|
||||||
@@ -475,6 +488,33 @@ mentioned above are as follows:
|
|||||||
- ``cd`` into the folder where you downloaded ``VCForPython27.msi``
|
- ``cd`` into the folder where you downloaded ``VCForPython27.msi``
|
||||||
- Run ``msiexec /i VCForPython27.msi``
|
- Run ``msiexec /i VCForPython27.msi``
|
||||||
|
|
||||||
|
Updating Catalyst
|
||||||
|
-----------------
|
||||||
|
|
||||||
|
Catalyst is currently in alpha and in under very active development. We release
|
||||||
|
new minor versions every few days in response to the thorough battle testing
|
||||||
|
that our user community puts Catalyst in. As a result, you should expect to
|
||||||
|
update Catalyst frequently. Once installed, Catalyst can easily be updated as a
|
||||||
|
``pip`` package regardless of the environemnt used for installation. Make sure
|
||||||
|
you activate your environment first as you did in your first install, and then
|
||||||
|
execute:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ pip uninstall enigma-catalyst
|
||||||
|
$ pip install enigma-catalyst
|
||||||
|
|
||||||
|
Alternatively, you could update Catalyst issuing the following command:
|
||||||
|
|
||||||
|
.. code-block:: bash
|
||||||
|
|
||||||
|
$ pip install -U enigma-catalyst
|
||||||
|
|
||||||
|
but this command will also upgrade all the Catalyst dependencies to the latest
|
||||||
|
versions available, and may have unexpected side effects if a newer version of a
|
||||||
|
dependency inadvertently breaks some functionality that Catalyst relies on.
|
||||||
|
Thus, the first method is the recommended one.
|
||||||
|
|
||||||
Getting Help
|
Getting Help
|
||||||
------------
|
------------
|
||||||
|
|
||||||
|
|||||||
@@ -4,11 +4,63 @@ This document explains how to get started with live trading.
|
|||||||
|
|
||||||
Supported Exchanges
|
Supported Exchanges
|
||||||
^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^
|
||||||
Catalyst can trade against these exchanges:
|
|
||||||
|
|
||||||
- Bitfinex, id= ``bitfinex``
|
Since version 0.4, Catalyst integrated with `CCXT <https://github.com/ccxt/ccxt>`_,
|
||||||
- Bittrex, id= ``bittrex``
|
a cryptocurrency trading library with support for more than 90 exchanges. The
|
||||||
- Poloniex, id= ``poloniex``
|
range of CCXT and Catalyst support for each of those exchanges varies greatly.
|
||||||
|
The most supported exchanges are as follows:
|
||||||
|
|
||||||
|
The exchanges available for backtesting are fully supported in live mode:
|
||||||
|
|
||||||
|
- Bitfinex, id = ``bitfinex``
|
||||||
|
- Bittrex, id = ``bittrex``
|
||||||
|
- Poloniex, id = ``poloniex``
|
||||||
|
|
||||||
|
Additionally, we have successfully tested the following exchanges:
|
||||||
|
|
||||||
|
- Binance, id = ``binance``
|
||||||
|
- Bitmex, id = ``bitmex``
|
||||||
|
- GDAX, id = ``gdax``
|
||||||
|
|
||||||
|
As Catalyst is currently in Alpha and in under active development, you are
|
||||||
|
encouraged to throughly test any exchange in *paper trading* mode before trading
|
||||||
|
*live* with it.
|
||||||
|
|
||||||
|
Paper Trading vs Live Trading modes
|
||||||
|
^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
|
||||||
|
|
||||||
|
Catalyst currently supports three different modes in which you can execute your
|
||||||
|
trading algorithm. The first is backtesting, which is covered extensively in the
|
||||||
|
tutorial, and uses historical data to run your algorithm. There is no
|
||||||
|
interaction with the exchange in backtesting mode, and this is the first mode
|
||||||
|
that you should test any new algorithm.
|
||||||
|
|
||||||
|
Once you are confident with the simulations that you have obtained with your
|
||||||
|
algorithm in backtesting, you may switch to live trading, where you have two
|
||||||
|
different modes:
|
||||||
|
* *Paper Trading*: The simulated algorithm runs in real time, and fetches
|
||||||
|
pricing data in real time from the exchange, but the orders never reach the
|
||||||
|
exchange, and are instead kept within Catalyst and simulated. No real currency
|
||||||
|
is bought or sold. Think of it as a `backtesting happening in real time`.
|
||||||
|
* *Live Trading*: This is the proper live trading mode in which an algorithm
|
||||||
|
runs in real time, fetching pricing data from live exchanges and placing orders
|
||||||
|
against the exchange. Real currency is transacted on the exchange driven by the
|
||||||
|
algorithm.
|
||||||
|
|
||||||
|
These three modes are controlled by the following variables:
|
||||||
|
|
||||||
|
+---------------+-------------------------+
|
||||||
|
| Mode | Parameters |
|
||||||
|
+ +-------+-----------------+
|
||||||
|
| | live | simulate_orders |
|
||||||
|
+---------------+-------+-----------------+
|
||||||
|
| backtesting | False | True (default) |
|
||||||
|
+---------------+-------+-----------------+
|
||||||
|
| paper trading | True | True |
|
||||||
|
+---------------+-------+-----------------+
|
||||||
|
| live trading | True | False |
|
||||||
|
+---------------+-------+-----------------+
|
||||||
|
|
||||||
|
|
||||||
Authentication
|
Authentication
|
||||||
^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^
|
||||||
@@ -75,7 +127,8 @@ Note that the trading pairs are always referenced in the same manner.
|
|||||||
However, not all trading pairs are available on all exchanges. An
|
However, not all trading pairs are available on all exchanges. An
|
||||||
error will occur if the specified trading pair is not trading
|
error will occur if the specified trading pair is not trading
|
||||||
on the exchange. To check which currency pairs are available on each
|
on the exchange. To check which currency pairs are available on each
|
||||||
of the supported exchanges, see `Catalyst Market Coverage <https://www.enigma.co/catalyst/status`_.
|
of the supported exchanges, see
|
||||||
|
`Catalyst Market Coverage <https://www.enigma.co/catalyst/status>`_.
|
||||||
|
|
||||||
Trading an Algorithm
|
Trading an Algorithm
|
||||||
^^^^^^^^^^^^^^^^^^^^
|
^^^^^^^^^^^^^^^^^^^^
|
||||||
@@ -105,20 +158,22 @@ What differs are the arguments provided to the catalyst client or
|
|||||||
|
|
||||||
Here is the breakdown of the new arguments:
|
Here is the breakdown of the new arguments:
|
||||||
|
|
||||||
- ``live``: Boolean flag which enables live trading.
|
- ``live``: Boolean flag which enables live trading. It defaults to ``False``.
|
||||||
- ``capital_base``: The amount of base_currency assigned to the strategy.
|
- ``capital_base``: The amount of base_currency assigned to the strategy.
|
||||||
It has to be lower or equal to the amount of base currency available for
|
It has to be lower or equal to the amount of base currency available for
|
||||||
trading on the exchange. For illustration, order_target_percent(asset, 1)
|
trading on the exchange. For illustration, order_target_percent(asset, 1)
|
||||||
will order the capital_base amount specified here of the specified asset.
|
will order the capital_base amount specified here of the specified asset.
|
||||||
- ``exchange_name``: The name of the targeted exchange
|
- ``exchange_name``: The name of the targeted exchange. See the
|
||||||
(supported values: *bitfinex*, *bittrex*).
|
`CCXT Supported Exchanges <https://github.com/ccxt/ccxt/wiki/Exchange-Markets>`_
|
||||||
|
for the full list.
|
||||||
- ``algo_namespace``: A arbitrary label assigned to your algorithm for
|
- ``algo_namespace``: A arbitrary label assigned to your algorithm for
|
||||||
data storage purposes.
|
data storage purposes.
|
||||||
- ``base_currency``: The base currency used to calculate the
|
- ``base_currency``: The base currency used to calculate the
|
||||||
statistics of your algorithm. Currently, the base currency of all
|
statistics of your algorithm. Currently, the base currency of all
|
||||||
trading pairs of your algorithm must match this value.
|
trading pairs of your algorithm must match this value.
|
||||||
- ``simulate_orders``: Enables the paper trading mode, in which orders are
|
- ``simulate_orders``: Enables the paper trading mode, in which orders are
|
||||||
simulated in Catalyst instead of processed on the exchange.
|
simulated in Catalyst instead of processed on the exchange. It defaults to
|
||||||
|
``True``.
|
||||||
|
|
||||||
Here is a complete algorithm for reference:
|
Here is a complete algorithm for reference:
|
||||||
`Buy Low and Sell High <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/buy_low_sell_high_live.py>`_
|
`Buy Low and Sell High <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/buy_low_sell_high_live.py>`_
|
||||||
|
|||||||
@@ -2,6 +2,64 @@
|
|||||||
Release Notes
|
Release Notes
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
Version 0.5.0
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
**Release Date**: 2018-02-07
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
~~~~~~~~~
|
||||||
|
- Fixed an issue with orders that stay open :issue:`211`
|
||||||
|
- Fixed Jupyter issues :issue:`179`
|
||||||
|
- Fetching multiple tickers in one call to minimize rate limit risks :issue:`174`
|
||||||
|
- Improved live state presentation :issue:`171`
|
||||||
|
|
||||||
|
|
||||||
|
Build
|
||||||
|
~~~~~
|
||||||
|
- Introducing the Enigma Marketplace
|
||||||
|
|
||||||
|
Version 0.4.7
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
**Release Date**: 2018-01-19
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
~~~~~~~~~
|
||||||
|
- Fixing issue :issue:`137` impacting the CLI
|
||||||
|
|
||||||
|
Build
|
||||||
|
~~~~~
|
||||||
|
- Implemented authentication aliases (:issue:`60`)
|
||||||
|
|
||||||
|
Version 0.4.6
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
**Release Date**: 2018-01-18
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
~~~~~~~~~
|
||||||
|
- Fixed some Python3 issues
|
||||||
|
- Reading the trade log to get executed order prices on exchanges like Binance (:issue:`151`)
|
||||||
|
- Fixed issue with market order executing price (:issue:`150` and :issue:`111`)
|
||||||
|
- Implemented standardized symbol mapping (:issue:`157`)
|
||||||
|
- Improved error handling for unsupported timeframes (:issue:`159`)
|
||||||
|
- Using Bitfinex instead of Poloniex to fetch btc_usdt benchmark (:issue:`161`)
|
||||||
|
|
||||||
|
|
||||||
|
Build
|
||||||
|
~~~~~
|
||||||
|
- Added a `context.state` dict to keep arbitrary state values between runs
|
||||||
|
- Added ability to stop live algo at specified end date
|
||||||
|
|
||||||
|
Version 0.4.5
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
**Release Date**: 2018-01-12
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
~~~~~~~~~
|
||||||
|
- Improved order execution for exchanges supporting trade lists (:issue:`151`)
|
||||||
|
- Fixed an issue where requesting history of multiple assets repeats values
|
||||||
|
- Raising an error for order amounts smaller than exchange lots
|
||||||
|
- Handling multiple req errors with tickers more gracefully (:issue:`160`)
|
||||||
|
|
||||||
Version 0.4.4
|
Version 0.4.4
|
||||||
^^^^^^^^^^^^^
|
^^^^^^^^^^^^^
|
||||||
**Release Date**: 2018-01-09
|
**Release Date**: 2018-01-09
|
||||||
|
|||||||
+1
-1
@@ -16,4 +16,4 @@ fi
|
|||||||
|
|
||||||
jupyter notebook -y --no-browser --notebook-dir=${PROJECT_DIR} \
|
jupyter notebook -y --no-browser --notebook-dir=${PROJECT_DIR} \
|
||||||
--certfile=${SSL_CERT_PEM} --keyfile=${SSL_CERT_KEY} --ip='*' \
|
--certfile=${SSL_CERT_PEM} --keyfile=${SSL_CERT_KEY} --ip='*' \
|
||||||
--config=${CONFIG_PATH}
|
--config=${CONFIG_PATH} --allow-root
|
||||||
|
|||||||
@@ -20,7 +20,9 @@ dependencies:
|
|||||||
- bcolz==0.12.1
|
- bcolz==0.12.1
|
||||||
- bottleneck==1.2.1
|
- bottleneck==1.2.1
|
||||||
- chardet==3.0.4
|
- chardet==3.0.4
|
||||||
- ccxt==1.10.565
|
- ccxt==1.10.1049
|
||||||
|
- web3==4.0.0b7
|
||||||
|
- requests-toolbelt==0.8.0
|
||||||
- click==6.7
|
- click==6.7
|
||||||
- contextlib2==0.5.5
|
- contextlib2==0.5.5
|
||||||
- cycler==0.10.0
|
- cycler==0.10.0
|
||||||
|
|||||||
@@ -81,6 +81,8 @@ empyrical==0.2.1
|
|||||||
tables==3.3.0
|
tables==3.3.0
|
||||||
|
|
||||||
#Catalyst dependencies
|
#Catalyst dependencies
|
||||||
ccxt==1.10.565
|
ccxt==1.10.1049
|
||||||
boto3==1.4.8
|
boto3==1.4.8
|
||||||
redo==1.6
|
redo==1.6
|
||||||
|
web3==4.0.0b7
|
||||||
|
requests-toolbelt==0.8.0
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
Sphinx>=1.3.2
|
Sphinx>=1.3.2
|
||||||
numpydoc>=0.5.0
|
numpydoc>=0.5.0
|
||||||
sphinx-autobuild==0.6.0
|
sphinx-autobuild==0.6.0
|
||||||
|
docutils==0.12
|
||||||
|
|||||||
@@ -0,0 +1,2 @@
|
|||||||
|
web3==4.0.0b7
|
||||||
|
requests-toolbelt==0.8.0
|
||||||
@@ -10,7 +10,8 @@ from catalyst.exchange.exchange_bcolz import BcolzExchangeBarReader, \
|
|||||||
from catalyst.exchange.exchange_bundle import ExchangeBundle, \
|
from catalyst.exchange.exchange_bundle import ExchangeBundle, \
|
||||||
BUNDLE_NAME_TEMPLATE
|
BUNDLE_NAME_TEMPLATE
|
||||||
from catalyst.exchange.utils.bundle_utils import get_bcolz_chunk, \
|
from catalyst.exchange.utils.bundle_utils import get_bcolz_chunk, \
|
||||||
get_start_dt, get_df_from_arrays
|
get_df_from_arrays
|
||||||
|
from exchange.utils.datetime_utils import get_start_dt
|
||||||
from catalyst.exchange.utils.exchange_utils import get_exchange_folder
|
from catalyst.exchange.utils.exchange_utils import get_exchange_folder
|
||||||
from catalyst.exchange.utils.factory import get_exchange
|
from catalyst.exchange.utils.factory import get_exchange
|
||||||
from catalyst.exchange.utils.stats_utils import df_to_string
|
from catalyst.exchange.utils.stats_utils import df_to_string
|
||||||
|
|||||||
@@ -1,7 +1,9 @@
|
|||||||
import pandas as pd
|
import pandas as pd
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
from base import BaseExchangeTestCase
|
from catalyst.testing import ZiplineTestCase
|
||||||
|
from catalyst.testing.fixtures import WithLogger
|
||||||
|
from .base import BaseExchangeTestCase
|
||||||
from catalyst.exchange.ccxt.ccxt_exchange import CCXT
|
from catalyst.exchange.ccxt.ccxt_exchange import CCXT
|
||||||
from catalyst.exchange.exchange_execution import ExchangeLimitOrder
|
from catalyst.exchange.exchange_execution import ExchangeLimitOrder
|
||||||
from catalyst.exchange.utils.exchange_utils import get_exchange_auth
|
from catalyst.exchange.utils.exchange_utils import get_exchange_auth
|
||||||
@@ -19,17 +21,17 @@ class TestCCXT(BaseExchangeTestCase):
|
|||||||
exchange_name=exchange_name,
|
exchange_name=exchange_name,
|
||||||
key=auth['key'],
|
key=auth['key'],
|
||||||
secret=auth['secret'],
|
secret=auth['secret'],
|
||||||
base_currency='bnb',
|
base_currency='usdt',
|
||||||
)
|
)
|
||||||
self.exchange.init()
|
self.exchange.init()
|
||||||
|
|
||||||
def test_order(self):
|
def test_order(self):
|
||||||
log.info('creating order')
|
log.info('creating order')
|
||||||
asset = self.exchange.get_asset('neo_bnb')
|
asset = self.exchange.get_asset('eth_usdt')
|
||||||
order_id = self.exchange.order(
|
order_id = self.exchange.order(
|
||||||
asset=asset,
|
asset=asset,
|
||||||
style=ExchangeLimitOrder(limit_price=10),
|
style=ExchangeLimitOrder(limit_price=1000),
|
||||||
amount=1,
|
amount=1.01,
|
||||||
)
|
)
|
||||||
log.info('order created {}'.format(order_id))
|
log.info('order created {}'.format(order_id))
|
||||||
assert order_id is not None
|
assert order_id is not None
|
||||||
@@ -56,10 +58,10 @@ class TestCCXT(BaseExchangeTestCase):
|
|||||||
def test_get_candles(self):
|
def test_get_candles(self):
|
||||||
log.info('retrieving candles')
|
log.info('retrieving candles')
|
||||||
candles = self.exchange.get_candles(
|
candles = self.exchange.get_candles(
|
||||||
freq='5T',
|
freq='30T',
|
||||||
assets=[self.exchange.get_asset('eth_btc')],
|
assets=[self.exchange.get_asset('eth_btc')],
|
||||||
bar_count=200,
|
bar_count=200,
|
||||||
start_dt=pd.to_datetime('2017-01-01', utc=True)
|
start_dt=pd.to_datetime('2017-09-01', utc=True)
|
||||||
)
|
)
|
||||||
|
|
||||||
for asset in candles:
|
for asset in candles:
|
||||||
@@ -70,10 +72,27 @@ class TestCCXT(BaseExchangeTestCase):
|
|||||||
def test_tickers(self):
|
def test_tickers(self):
|
||||||
log.info('retrieving tickers')
|
log.info('retrieving tickers')
|
||||||
assets = [
|
assets = [
|
||||||
self.exchange.get_asset('eng_eth'),
|
self.exchange.get_asset('ada_eth'),
|
||||||
|
self.exchange.get_asset('zrx_eth'),
|
||||||
]
|
]
|
||||||
tickers = self.exchange.tickers(assets)
|
tickers = self.exchange.tickers(assets)
|
||||||
assert len(tickers) == 1
|
assert len(tickers) == 2
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_my_trades(self):
|
||||||
|
asset = self.exchange.get_asset('dsh_btc')
|
||||||
|
|
||||||
|
trades = self.exchange.get_trades(asset)
|
||||||
|
assert trades
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_get_executed_order(self):
|
||||||
|
log.info('retrieving executed order')
|
||||||
|
asset = self.exchange.get_asset('eng_eth')
|
||||||
|
|
||||||
|
order = self.exchange.get_order('165784', asset)
|
||||||
|
transactions = self.exchange.process_order(order)
|
||||||
|
assert transactions
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_get_balances(self):
|
def test_get_balances(self):
|
||||||
|
|||||||
@@ -12,7 +12,13 @@ from logbook import TestHandler, WARNING
|
|||||||
from pathtools.path import listdir
|
from pathtools.path import listdir
|
||||||
|
|
||||||
filter_algos = [
|
filter_algos = [
|
||||||
'mean_reversion_simple_custom_fees.py',
|
'buy_and_hodl.py',
|
||||||
|
'buy_btc_simple.py',
|
||||||
|
'buy_low_sell_high.py',
|
||||||
|
'mean_reversion_simple.py',
|
||||||
|
'rsi_profit_target.py',
|
||||||
|
'simple_loop.py',
|
||||||
|
'simple_universe.py',
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
@@ -58,7 +64,7 @@ class TestSuiteAlgo(WithLogger, ZiplineTestCase):
|
|||||||
initialize=algo.initialize,
|
initialize=algo.initialize,
|
||||||
handle_data=algo.handle_data,
|
handle_data=algo.handle_data,
|
||||||
analyze=TestSuiteAlgo.analyze,
|
analyze=TestSuiteAlgo.analyze,
|
||||||
exchange_name='bitfinex',
|
exchange_name='poloniex',
|
||||||
algo_namespace='test_{}'.format(namespace),
|
algo_namespace='test_{}'.format(namespace),
|
||||||
base_currency='eth',
|
base_currency='eth',
|
||||||
start=pd.to_datetime('2017-10-01', utc=True),
|
start=pd.to_datetime('2017-10-01', utc=True),
|
||||||
@@ -67,6 +73,7 @@ class TestSuiteAlgo(WithLogger, ZiplineTestCase):
|
|||||||
)
|
)
|
||||||
warnings = [record for record in log_catcher.records if
|
warnings = [record for record in log_catcher.records if
|
||||||
record.level == WARNING]
|
record.level == WARNING]
|
||||||
self.assertEqual(0, len(warnings))
|
|
||||||
|
|
||||||
|
if len(warnings) > 0:
|
||||||
|
print('WARNINGS:\n{}'.format(warnings))
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import random
|
import random
|
||||||
|
|
||||||
|
import os
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from logbook import TestHandler
|
from logbook import TestHandler
|
||||||
from pandas.util.testing import assert_frame_equal
|
from pandas.util.testing import assert_frame_equal
|
||||||
@@ -11,7 +12,6 @@ from catalyst.exchange.utils.exchange_utils import get_candles_df
|
|||||||
from catalyst.exchange.utils.factory import get_exchange
|
from catalyst.exchange.utils.factory import get_exchange
|
||||||
from catalyst.exchange.utils.test_utils import output_df, \
|
from catalyst.exchange.utils.test_utils import output_df, \
|
||||||
select_random_assets
|
select_random_assets
|
||||||
from catalyst.testing.fixtures import WithLogger, ZiplineTestCase
|
|
||||||
|
|
||||||
pd.set_option('display.expand_frame_repr', False)
|
pd.set_option('display.expand_frame_repr', False)
|
||||||
pd.set_option('precision', 8)
|
pd.set_option('precision', 8)
|
||||||
@@ -19,12 +19,13 @@ pd.set_option('display.width', 1000)
|
|||||||
pd.set_option('display.max_colwidth', 1000)
|
pd.set_option('display.max_colwidth', 1000)
|
||||||
|
|
||||||
|
|
||||||
class TestSuiteBundle(WithLogger, ZiplineTestCase):
|
class TestSuiteBundle:
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def get_data_portal(exchange_names):
|
def get_data_portal(exchanges):
|
||||||
open_calendar = get_calendar('OPEN')
|
open_calendar = get_calendar('OPEN')
|
||||||
asset_finder = ExchangeAssetFinder()
|
asset_finder = ExchangeAssetFinder(exchanges)
|
||||||
|
|
||||||
|
exchange_names = [exchange.name for exchange in exchanges]
|
||||||
data_portal = DataPortalExchangeBacktest(
|
data_portal = DataPortalExchangeBacktest(
|
||||||
exchange_names=exchange_names,
|
exchange_names=exchange_names,
|
||||||
asset_finder=asset_finder,
|
asset_finder=asset_finder,
|
||||||
@@ -45,7 +46,9 @@ class TestSuiteBundle(WithLogger, ZiplineTestCase):
|
|||||||
assets
|
assets
|
||||||
end_dt
|
end_dt
|
||||||
bar_count
|
bar_count
|
||||||
sample_minutes
|
freq
|
||||||
|
data_frequency
|
||||||
|
data_portal
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
@@ -63,10 +66,6 @@ class TestSuiteBundle(WithLogger, ZiplineTestCase):
|
|||||||
field='close',
|
field='close',
|
||||||
data_frequency=data_frequency,
|
data_frequency=data_frequency,
|
||||||
)
|
)
|
||||||
print('bundle data:\n{}'.format(
|
|
||||||
data['bundle'].tail(10))
|
|
||||||
)
|
|
||||||
|
|
||||||
candles = exchange.get_candles(
|
candles = exchange.get_candles(
|
||||||
end_dt=end_dt,
|
end_dt=end_dt,
|
||||||
freq=freq,
|
freq=freq,
|
||||||
@@ -80,24 +79,37 @@ class TestSuiteBundle(WithLogger, ZiplineTestCase):
|
|||||||
bar_count=bar_count,
|
bar_count=bar_count,
|
||||||
end_dt=end_dt,
|
end_dt=end_dt,
|
||||||
)
|
)
|
||||||
print('exchange data:\n{}'.format(
|
|
||||||
data['exchange'].tail(10))
|
|
||||||
)
|
|
||||||
for source in data:
|
for source in data:
|
||||||
df = data[source]
|
df = data[source]
|
||||||
path = output_df(df, assets, '{}_{}'.format(freq, source))
|
path, folder = output_df(
|
||||||
print('saved {}:\n{}'.format(source, path))
|
df, assets, '{}_{}'.format(freq, source)
|
||||||
|
)
|
||||||
|
|
||||||
|
print('saved {} test results: {}'.format(end_dt, folder))
|
||||||
|
|
||||||
assert_frame_equal(
|
assert_frame_equal(
|
||||||
right=data['bundle'],
|
right=data['bundle'],
|
||||||
left=data['exchange'],
|
left=data['exchange'],
|
||||||
check_less_precise=True,
|
check_less_precise=1,
|
||||||
)
|
)
|
||||||
|
try:
|
||||||
|
assert_frame_equal(
|
||||||
|
right=data['bundle'],
|
||||||
|
left=data['exchange'],
|
||||||
|
check_less_precise=min([a.decimals for a in assets]),
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
print('Some differences were found within a 1 decimal point '
|
||||||
|
'interval of confidence: {}'.format(e))
|
||||||
|
with open(os.path.join(folder, 'compare.txt'), 'w+') as handle:
|
||||||
|
handle.write(e.args[0])
|
||||||
|
|
||||||
|
pass
|
||||||
|
|
||||||
def test_validate_bundles(self):
|
def test_validate_bundles(self):
|
||||||
# exchange_population = 3
|
# exchange_population = 3
|
||||||
asset_population = 3
|
asset_population = 3
|
||||||
data_frequency = random.choice(['minute', 'daily'])
|
data_frequency = random.choice(['minute'])
|
||||||
|
|
||||||
# bundle = 'dailyBundle' if data_frequency
|
# bundle = 'dailyBundle' if data_frequency
|
||||||
# == 'daily' else 'minuteBundle'
|
# == 'daily' else 'minuteBundle'
|
||||||
@@ -105,11 +117,9 @@ class TestSuiteBundle(WithLogger, ZiplineTestCase):
|
|||||||
# population=exchange_population,
|
# population=exchange_population,
|
||||||
# features=[bundle],
|
# features=[bundle],
|
||||||
# ) # Type: list[Exchange]
|
# ) # Type: list[Exchange]
|
||||||
exchanges = [get_exchange('bitfinex', skip_init=True)]
|
exchanges = [get_exchange('poloniex', skip_init=True)]
|
||||||
|
|
||||||
data_portal = TestSuiteBundle.get_data_portal(
|
data_portal = TestSuiteBundle.get_data_portal(exchanges)
|
||||||
[exchange.name for exchange in exchanges]
|
|
||||||
)
|
|
||||||
for exchange in exchanges:
|
for exchange in exchanges:
|
||||||
exchange.init()
|
exchange.init()
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ from catalyst.exchange.utils.test_utils import select_random_exchanges, \
|
|||||||
handle_exchange_error, select_random_assets
|
handle_exchange_error, select_random_assets
|
||||||
from catalyst.testing import ZiplineTestCase
|
from catalyst.testing import ZiplineTestCase
|
||||||
from catalyst.testing.fixtures import WithLogger
|
from catalyst.testing.fixtures import WithLogger
|
||||||
|
from catalyst.exchange.utils.factory import get_exchanges, get_exchange
|
||||||
|
|
||||||
log = Logger('TestSuiteExchange')
|
log = Logger('TestSuiteExchange')
|
||||||
|
|
||||||
@@ -83,12 +84,13 @@ class TestSuiteExchange(WithLogger, ZiplineTestCase):
|
|||||||
|
|
||||||
def test_tickers(self):
|
def test_tickers(self):
|
||||||
exchange_population = 3
|
exchange_population = 3
|
||||||
asset_population = 3
|
asset_population = 15
|
||||||
|
|
||||||
exchanges = select_random_exchanges(
|
# exchanges = select_random_exchanges(
|
||||||
exchange_population,
|
# exchange_population,
|
||||||
features=['fetchTickers'],
|
# features=['fetchTickers'],
|
||||||
) # Type: list[Exchange]
|
# ) # Type: list[Exchange]
|
||||||
|
exchanges = list(get_exchanges(['binance']).values())
|
||||||
for exchange in exchanges:
|
for exchange in exchanges:
|
||||||
exchange.init()
|
exchange.init()
|
||||||
|
|
||||||
@@ -111,10 +113,11 @@ class TestSuiteExchange(WithLogger, ZiplineTestCase):
|
|||||||
exchange_population = 3
|
exchange_population = 3
|
||||||
asset_population = 3
|
asset_population = 3
|
||||||
|
|
||||||
exchanges = select_random_exchanges(
|
# exchanges = select_random_exchanges(
|
||||||
population=exchange_population,
|
# population=exchange_population,
|
||||||
features=['fetchOHLCV'],
|
# features=['fetchOHLCV'],
|
||||||
) # Type: list[Exchange]
|
# ) # Type: list[Exchange]
|
||||||
|
exchanges = list(get_exchanges(['binance']).values())
|
||||||
for exchange in exchanges:
|
for exchange in exchanges:
|
||||||
exchange.init()
|
exchange.init()
|
||||||
|
|
||||||
@@ -136,7 +139,6 @@ class TestSuiteExchange(WithLogger, ZiplineTestCase):
|
|||||||
assets=assets,
|
assets=assets,
|
||||||
bar_count=bar_count,
|
bar_count=bar_count,
|
||||||
start_dt=dt_range[0],
|
start_dt=dt_range[0],
|
||||||
end_dt=dt_range[-1],
|
|
||||||
)
|
)
|
||||||
|
|
||||||
assert len(candles) == asset_population
|
assert len(candles) == asset_population
|
||||||
@@ -153,13 +155,20 @@ class TestSuiteExchange(WithLogger, ZiplineTestCase):
|
|||||||
quote_currency = 'eth'
|
quote_currency = 'eth'
|
||||||
order_amount = 0.1
|
order_amount = 0.1
|
||||||
|
|
||||||
exchanges = select_random_exchanges(
|
# exchanges = select_random_exchanges(
|
||||||
population=population,
|
# population=population,
|
||||||
features=['fetchOrder'],
|
# features=['fetchOrder'],
|
||||||
is_authenticated=True,
|
# is_authenticated=True,
|
||||||
base_currency=quote_currency,
|
# base_currency=quote_currency,
|
||||||
) # Type: list[Exchange]
|
# ) # Type: list[Exchange]
|
||||||
|
|
||||||
|
exchanges = [
|
||||||
|
get_exchange(
|
||||||
|
'binance',
|
||||||
|
base_currency=quote_currency,
|
||||||
|
must_authenticate=True,
|
||||||
|
)
|
||||||
|
]
|
||||||
log_catcher = TestHandler()
|
log_catcher = TestHandler()
|
||||||
with log_catcher:
|
with log_catcher:
|
||||||
for exchange in exchanges:
|
for exchange in exchanges:
|
||||||
@@ -184,13 +193,13 @@ class TestSuiteExchange(WithLogger, ZiplineTestCase):
|
|||||||
)
|
)
|
||||||
sleep(1)
|
sleep(1)
|
||||||
|
|
||||||
open_order, _ = exchange.get_order(order.id, asset)
|
open_order = exchange.get_order(order.id, asset)
|
||||||
self.assertEqual(0, open_order.status)
|
self.assertEqual(0, open_order.status)
|
||||||
|
|
||||||
exchange.cancel_order(open_order, asset)
|
exchange.cancel_order(open_order, asset)
|
||||||
sleep(1)
|
sleep(1)
|
||||||
|
|
||||||
canceled_order, _ = exchange.get_order(open_order.id, asset)
|
canceled_order = exchange.get_order(open_order.id, asset)
|
||||||
warnings = [record for record in log_catcher.records if
|
warnings = [record for record in log_catcher.records if
|
||||||
record.level == WARNING]
|
record.level == WARNING]
|
||||||
|
|
||||||
|
|||||||
@@ -0,0 +1,36 @@
|
|||||||
|
from catalyst.marketplace.marketplace import Marketplace
|
||||||
|
from catalyst.testing.fixtures import WithLogger, ZiplineTestCase
|
||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
|
class TestMarketplace(WithLogger, ZiplineTestCase):
|
||||||
|
def test_list(self):
|
||||||
|
marketplace = Marketplace()
|
||||||
|
marketplace.list()
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_register(self):
|
||||||
|
marketplace = Marketplace()
|
||||||
|
marketplace.register()
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_subscribe(self):
|
||||||
|
marketplace = Marketplace()
|
||||||
|
marketplace.subscribe('marketcap2222')
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_ingest(self):
|
||||||
|
marketplace = Marketplace()
|
||||||
|
ds_def = marketplace.ingest('marketcap1234')
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_publish(self):
|
||||||
|
marketplace = Marketplace()
|
||||||
|
datadir = '/Users/fredfortier/Downloads/marketcap_test_single'
|
||||||
|
marketplace.publish('marketcap1234', datadir, False)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_clean(self):
|
||||||
|
marketplace = Marketplace()
|
||||||
|
marketplace.clean('marketcap')
|
||||||
|
pass
|
||||||
Reference in New Issue
Block a user