mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-21 12:30:16 +08:00
Compare commits
348
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6fc7dd6838 | ||
|
|
60e07c8a3c | ||
|
|
437e5c5b80 | ||
|
|
8bf639b01a | ||
|
|
8f7d678170 | ||
|
|
1de084a08f | ||
|
|
93ca4e990d | ||
|
|
4694372496 | ||
|
|
f0606b5ea4 | ||
|
|
b0dce13672 | ||
|
|
a99a4f4e85 | ||
|
|
fc9837b678 | ||
|
|
a1228a8ea2 | ||
|
|
2b0830bafc | ||
|
|
7ce382d52e | ||
|
|
42aac37f34 | ||
|
|
5de89549ee | ||
|
|
586d7f2954 | ||
|
|
218dc0bafd | ||
|
|
68f92dedd6 | ||
|
|
89c080fce7 | ||
|
|
a30d498abf | ||
|
|
ebdb9e423e | ||
|
|
4a794aa035 | ||
|
|
1f0c037d29 | ||
|
|
623ace1cbd | ||
|
|
a004825a09 | ||
|
|
cbc2ed2aaf | ||
|
|
1de881e17f | ||
|
|
6488ff5abe | ||
|
|
cb4668f093 | ||
|
|
6092471180 | ||
|
|
09068a4c37 | ||
|
|
ed406a30ff | ||
|
|
aa520d5a8b | ||
|
|
e59f46dfd6 | ||
|
|
0d051a9496 | ||
|
|
964c90176b | ||
|
|
5197ab6cc2 | ||
|
|
d7b6cb8490 | ||
|
|
fa0e9332bf | ||
|
|
d9d6a4e52d | ||
|
|
b4e5b699bd | ||
|
|
f990ecf14d | ||
|
|
40cfc65e02 | ||
|
|
abc48494c2 | ||
|
|
73faa87269 | ||
|
|
5d3a1c2f8b | ||
|
|
3159ec7dc2 | ||
|
|
8be0626fc9 | ||
|
|
868f17fd9d | ||
|
|
b95cf465fc | ||
|
|
c4b10bae39 | ||
|
|
6c4f7afaea | ||
|
|
b85219d5b4 | ||
|
|
c2f71cf852 | ||
|
|
082b342d02 | ||
|
|
37c1057ab6 | ||
|
|
fec829b82e | ||
|
|
25dc3ee737 | ||
|
|
5de67a5a61 | ||
|
|
46e1a87a3d | ||
|
|
cfafafb8fc | ||
|
|
b2f042e2c2 | ||
|
|
704c93dac9 | ||
|
|
478579ed8c | ||
|
|
c65a976b81 | ||
|
|
f9fa28c103 | ||
|
|
14c5ef3006 | ||
|
|
de2d3f6f54 | ||
|
|
b271b372d8 | ||
|
|
3f9a0727c0 | ||
|
|
497212383a | ||
|
|
e5870ea60a | ||
|
|
8587fee0ce | ||
|
|
388535b09c | ||
|
|
25e9f0f58f | ||
|
|
b4bd557273 | ||
|
|
2577b53518 | ||
|
|
8fe3ab344e | ||
|
|
bfd7e4b2dd | ||
|
|
50310576f9 | ||
|
|
fea2ed104e | ||
|
|
127878413e | ||
|
|
6a6ccf5595 | ||
|
|
d40585f56e | ||
|
|
4337abd60a | ||
|
|
92e0a7bb88 | ||
|
|
20f8a75f4a | ||
|
|
634d22fb06 | ||
|
|
7ee875ee70 | ||
|
|
271a51a393 | ||
|
|
9936b38e09 | ||
|
|
69153295f0 | ||
|
|
7e22674d40 | ||
|
|
ab644bd732 | ||
|
|
2becc4157a | ||
|
|
ec5fdecf91 | ||
|
|
bd88ba2277 | ||
|
|
a61857b37d | ||
|
|
866b0a215c | ||
|
|
2ba825db9b | ||
|
|
041665dee1 | ||
|
|
8072ded532 | ||
|
|
9956b5462d | ||
|
|
92d3b98448 | ||
|
|
46f34d64a0 | ||
|
|
d494de7551 | ||
|
|
129778945f | ||
|
|
f3183b267a | ||
|
|
6812aa0c5b | ||
|
|
89bf6742e3 | ||
|
|
f45d673d94 | ||
|
|
67400f048b | ||
|
|
d64d6f191f | ||
|
|
5345bfc43d | ||
|
|
1aed7c71f6 | ||
|
|
e9745be0e8 | ||
|
|
bc8bf6941d | ||
|
|
f35444fabc | ||
|
|
2c1f810163 | ||
|
|
77570f03eb | ||
|
|
a8cfcead70 | ||
|
|
00f579c729 | ||
|
|
49b6792399 | ||
|
|
6b1982274e | ||
|
|
bc58640a3a | ||
|
|
511af7946e | ||
|
|
dcdb3d9d7a | ||
|
|
262dffd4bf | ||
|
|
5c77cfc68d | ||
|
|
11f8e36ff0 | ||
|
|
44e4e66f5c | ||
|
|
3c4c6c3dfd | ||
|
|
bebebc46dc | ||
|
|
403d7f9c29 | ||
|
|
09ad397ee6 | ||
|
|
e56e1f8e21 | ||
|
|
00f232e2d7 | ||
|
|
a820f66bdc | ||
|
|
82d318a1c0 | ||
|
|
97afbf781e | ||
|
|
43a5f8858b | ||
|
|
dc04cd8781 | ||
|
|
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 | ||
|
|
62d21f1aca | ||
|
|
48a89ad521 | ||
|
|
2225c40b76 | ||
|
|
18c96303a6 | ||
|
|
5c236a65f7 | ||
|
|
1f29f7fdf1 | ||
|
|
cbae2465d2 | ||
|
|
4a4e32846b | ||
|
|
426fde40b1 | ||
|
|
e9a3bcf3e9 | ||
|
|
5b77f5eeba | ||
|
|
5417881a72 | ||
|
|
1cadaf1ed0 | ||
|
|
929e0a9a01 | ||
|
|
15aadbb101 | ||
|
|
7f021acb2e | ||
|
|
ad0bc5c41a | ||
|
|
2a239fd5bb | ||
|
|
68faad4098 | ||
|
|
6b3f59ff76 | ||
|
|
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 | ||
|
|
ff7d7c5256 | ||
|
|
b60b50e99a | ||
|
|
1e4af12e3d | ||
|
|
e0c8178932 | ||
|
|
93ebbf8b1f | ||
|
|
22a850ab14 | ||
|
|
aab501aa17 | ||
|
|
94d5b4a4d5 | ||
|
|
db37c9c6a7 | ||
|
|
a49cb55821 | ||
|
|
ac15413af8 | ||
|
|
141ee65c91 | ||
|
|
55d1fee82d | ||
|
|
33f94b3ef9 | ||
|
|
30448a65c5 | ||
|
|
9c33ee123c | ||
|
|
d88710501f | ||
|
|
ba0208910f | ||
|
|
5d9708901d | ||
|
|
96b36c6614 | ||
|
|
e872b1fc82 | ||
|
|
13de3e69ef | ||
|
|
215de33c35 |
@@ -40,6 +40,7 @@ develop-eggs
|
||||
coverage.xml
|
||||
htmlcov
|
||||
nosetests.xml
|
||||
.python-version
|
||||
|
||||
# C Extensions
|
||||
*.o
|
||||
|
||||
+2
-4
@@ -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
|
||||
:align: center
|
||||
:alt: Enigma | Catalyst
|
||||
@@ -17,9 +17,7 @@ insights regarding a particular strategy's performance. Catalyst also supports
|
||||
live-trading of crypto-assets starting with three exchanges (Bitfinex, Bittrex,
|
||||
and Poloniex) with more being added over time. Catalyst empowers users to share
|
||||
and curate data and build profitable, data-driven investment strategies. Please
|
||||
visit `enigma.co <https://www.enigma.co>`_ to learn more about Catalyst, or
|
||||
refer to the `whitepaper <https://www.enigma.co/enigma_catalyst.pdf>`_ for
|
||||
further technical details.
|
||||
visit `enigma.co <https://www.enigma.co>`_ to learn more about Catalyst.
|
||||
|
||||
Catalyst builds on top of the well-established
|
||||
`Zipline <https://github.com/quantopian/zipline>`_ project. We did our best to
|
||||
|
||||
+161
-12
@@ -3,8 +3,10 @@ import os
|
||||
from functools import wraps
|
||||
|
||||
import click
|
||||
import sys
|
||||
import logbook
|
||||
import pandas as pd
|
||||
from catalyst.marketplace.marketplace import Marketplace
|
||||
from six import text_type
|
||||
|
||||
from catalyst.data import bundles as bundles_module
|
||||
@@ -257,7 +259,7 @@ def run(ctx,
|
||||
if capital_base is None:
|
||||
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(
|
||||
initialize=None,
|
||||
@@ -285,11 +287,12 @@ def run(ctx,
|
||||
analyze_live=None,
|
||||
live_graph=False,
|
||||
simulate_orders=True,
|
||||
auth_aliases=None,
|
||||
stats_output=None,
|
||||
)
|
||||
|
||||
if output == '-':
|
||||
click.echo(str(perf))
|
||||
click.echo(str(perf), sys.stdout)
|
||||
elif output != os.devnull: # make the catalyst magic not write any data
|
||||
perf.to_pickle(output)
|
||||
|
||||
@@ -394,6 +397,12 @@ def catalyst_magic(line, cell=None):
|
||||
help='The base currency used to calculate statistics '
|
||||
'(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(
|
||||
'--live-graph/--no-live-graph',
|
||||
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 '
|
||||
'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
|
||||
def live(ctx,
|
||||
algofile,
|
||||
@@ -419,7 +437,9 @@ def live(ctx,
|
||||
exchange_name,
|
||||
algo_namespace,
|
||||
base_currency,
|
||||
end,
|
||||
live_graph,
|
||||
auth_aliases,
|
||||
simulate_orders):
|
||||
"""Trade live with the given algorithm.
|
||||
"""
|
||||
@@ -442,10 +462,10 @@ def live(ctx,
|
||||
ctx.fail("must specify a capital base with '--capital-base'")
|
||||
|
||||
if simulate_orders:
|
||||
click.echo('Running in paper trading mode.')
|
||||
click.echo('Running in paper trading mode.', sys.stdout)
|
||||
|
||||
else:
|
||||
click.echo('Running in live trading mode.')
|
||||
click.echo('Running in live trading mode.', sys.stdout)
|
||||
|
||||
perf = _run(
|
||||
initialize=None,
|
||||
@@ -461,7 +481,7 @@ def live(ctx,
|
||||
bundle=None,
|
||||
bundle_timestamp=None,
|
||||
start=None,
|
||||
end=None,
|
||||
end=end,
|
||||
output=output,
|
||||
print_algo=print_algo,
|
||||
local_namespace=local_namespace,
|
||||
@@ -473,11 +493,12 @@ def live(ctx,
|
||||
live_graph=live_graph,
|
||||
analyze_live=None,
|
||||
simulate_orders=simulate_orders,
|
||||
auth_aliases=auth_aliases,
|
||||
stats_output=None,
|
||||
)
|
||||
|
||||
if output == '-':
|
||||
click.echo(str(perf))
|
||||
click.echo(str(perf), sys.stdout)
|
||||
elif output != os.devnull: # make the catalyst magic not write any data
|
||||
perf.to_pickle(output)
|
||||
|
||||
@@ -559,7 +580,8 @@ def ingest_exchange(ctx, exchange_name, data_frequency, start, end,
|
||||
|
||||
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(
|
||||
data_frequency=data_frequency,
|
||||
include_symbols=include_symbols,
|
||||
@@ -582,10 +604,11 @@ def ingest_exchange(ctx, exchange_name, data_frequency, start, end,
|
||||
@click.pass_context
|
||||
def clean_algo(ctx, algo_namespace):
|
||||
click.echo(
|
||||
'Cleaning algo state: {}'.format(algo_namespace)
|
||||
'Cleaning algo state: {}'.format(algo_namespace),
|
||||
sys.stdout
|
||||
)
|
||||
delete_algo_folder(algo_namespace)
|
||||
click.echo('Done')
|
||||
click.echo('Done', sys.stdout)
|
||||
|
||||
|
||||
@main.command(name='clean-exchange')
|
||||
@@ -612,11 +635,12 @@ def clean_exchange(ctx, exchange_name, data_frequency):
|
||||
|
||||
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(
|
||||
data_frequency=data_frequency,
|
||||
)
|
||||
click.echo('Done')
|
||||
click.echo('Done', sys.stdout)
|
||||
|
||||
|
||||
@main.command()
|
||||
@@ -737,7 +761,132 @@ def bundles():
|
||||
# because there were no entries, print a single message indicating that
|
||||
# no ingestions have yet been made.
|
||||
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):
|
||||
"""Access the Enigma Data Marketplace to:\n
|
||||
- Register and Publish new datasets (seller-side)\n
|
||||
- Subscribe and Ingest premium datasets (buyer-side)\n
|
||||
"""
|
||||
pass
|
||||
|
||||
|
||||
@marketplace.command()
|
||||
@click.pass_context
|
||||
def ls(ctx):
|
||||
"""List all available datasets.
|
||||
"""
|
||||
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):
|
||||
"""Subscribe to an exisiting dataset.
|
||||
"""
|
||||
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):
|
||||
"""Ingest a dataset (requires subscription).
|
||||
"""
|
||||
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):
|
||||
"""Clean/Remove local data for a given dataset.
|
||||
"""
|
||||
marketplace = Marketplace()
|
||||
marketplace.clean(dataset)
|
||||
|
||||
|
||||
@marketplace.command()
|
||||
@click.pass_context
|
||||
def register(ctx):
|
||||
"""Register a new dataset.
|
||||
"""
|
||||
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):
|
||||
"""Publish data for a registered dataset.
|
||||
"""
|
||||
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__':
|
||||
|
||||
@@ -939,7 +939,7 @@ class TradingAlgorithm(object):
|
||||
The field to query. The options have the following meanings:
|
||||
arena : str
|
||||
The arena from the simulation parameters. This will normally
|
||||
be ``'backtest'`` but some systems may use this distinguish
|
||||
be ``backtest`` but some systems may use this distinguish
|
||||
live trading from backtesting.
|
||||
data_frequency : {'daily', 'minute'}
|
||||
data_frequency tells the algorithm if it is running with
|
||||
@@ -954,7 +954,7 @@ class TradingAlgorithm(object):
|
||||
The platform that the code is running on. By default this
|
||||
will be the string 'catalyst'. This can allow algorithms to
|
||||
know if they are running on the Quantopian platform instead.
|
||||
* : dict[str -> any]
|
||||
\* : dict[str -> any]
|
||||
Returns all of the fields in a dictionary.
|
||||
|
||||
Returns
|
||||
@@ -1032,7 +1032,7 @@ class TradingAlgorithm(object):
|
||||
argument is the name of the column in the preprocessed dataframe
|
||||
containing the symbols. This will be used along with the date
|
||||
information to map the sids in the asset finder.
|
||||
**kwargs
|
||||
\*\*kwargs
|
||||
Forwarded to :func:`pandas.read_csv`.
|
||||
|
||||
Returns
|
||||
@@ -1156,7 +1156,7 @@ class TradingAlgorithm(object):
|
||||
|
||||
Parameters
|
||||
----------
|
||||
**kwargs
|
||||
\*\*kwargs
|
||||
The names and values to record.
|
||||
|
||||
Notes
|
||||
@@ -1273,7 +1273,7 @@ class TradingAlgorithm(object):
|
||||
|
||||
Parameters
|
||||
----------
|
||||
*args : iterable[str]
|
||||
\*args : iterable[str]
|
||||
The ticker symbols to lookup.
|
||||
|
||||
Returns
|
||||
|
||||
+65
-8
@@ -34,6 +34,7 @@ def attach_pipeline(pipeline, name, chunks=None):
|
||||
:func:`catalyst.api.pipeline_output`
|
||||
"""
|
||||
|
||||
|
||||
def batch_market_order(share_counts):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
|
||||
def cancel_order(order_param):
|
||||
"""Cancel an open order.
|
||||
|
||||
@@ -57,7 +59,9 @@ def cancel_order(order_param):
|
||||
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.
|
||||
|
||||
Parameters
|
||||
@@ -81,7 +85,10 @@ def continuous_future(root_symbol_str, offset=0, roll='volume', adjustment='mul'
|
||||
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
|
||||
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.
|
||||
"""
|
||||
|
||||
|
||||
def future_symbol(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.
|
||||
"""
|
||||
|
||||
|
||||
def get_datetime(tz=None):
|
||||
"""
|
||||
Returns the current simulation datetime.
|
||||
@@ -159,6 +168,7 @@ dt : datetime
|
||||
The current simulation datetime converted to ``tz``.
|
||||
"""
|
||||
|
||||
|
||||
def get_environment(field='platform'):
|
||||
"""Query the execution environment.
|
||||
|
||||
@@ -198,6 +208,7 @@ def get_environment(field='platform'):
|
||||
Raised when ``field`` is not a valid option.
|
||||
"""
|
||||
|
||||
|
||||
def get_order(order_id):
|
||||
"""Lookup an order based on the order id returned from one of the
|
||||
order functions.
|
||||
@@ -213,10 +224,12 @@ def get_order(order_id):
|
||||
The order object.
|
||||
"""
|
||||
|
||||
|
||||
def history(bar_count, frequency, field, ffill=True):
|
||||
"""DEPRECATED: use ``data.history`` instead.
|
||||
"""
|
||||
|
||||
|
||||
def order(asset, amount, limit_price=None, stop_price=None, style=None):
|
||||
"""Place an order.
|
||||
|
||||
@@ -258,7 +271,9 @@ def order(asset, amount, limit_price=None, stop_price=None, style=None):
|
||||
: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
|
||||
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`
|
||||
"""
|
||||
|
||||
|
||||
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
|
||||
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`
|
||||
"""
|
||||
|
||||
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
|
||||
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
|
||||
@@ -396,7 +414,9 @@ def order_target_percent(asset, target, limit_price=None, stop_price=None, style
|
||||
: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
|
||||
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
|
||||
@@ -448,6 +468,7 @@ def order_target_value(asset, target, limit_price=None, stop_price=None, style=N
|
||||
:func:`catalyst.api.order_target_percent`
|
||||
"""
|
||||
|
||||
|
||||
def order_value(asset, value, limit_price=None, stop_price=None, style=None):
|
||||
"""Place an order by desired value rather than desired number of
|
||||
shares.
|
||||
@@ -488,6 +509,7 @@ def order_value(asset, value, limit_price=None, stop_price=None, style=None):
|
||||
:func:`catalyst.api.order_percent`
|
||||
"""
|
||||
|
||||
|
||||
def pipeline_output(name):
|
||||
"""Get the results of the pipeline that was attached with the name:
|
||||
``name``.
|
||||
@@ -514,6 +536,7 @@ def pipeline_output(name):
|
||||
:meth:`catalyst.pipeline.engine.PipelineEngine.run_pipeline`
|
||||
"""
|
||||
|
||||
|
||||
def record(*args, **kwargs):
|
||||
"""Track and record values each day.
|
||||
|
||||
@@ -529,7 +552,9 @@ def record(*args, **kwargs):
|
||||
: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.
|
||||
|
||||
Parameters
|
||||
@@ -549,6 +574,7 @@ def schedule_function(func, date_rule=None, time_rule=None, half_days=True, cale
|
||||
:class:`catalyst.api.time_rules`
|
||||
"""
|
||||
|
||||
|
||||
def set_asset_restrictions(restrictions, on_error='fail'):
|
||||
"""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
|
||||
"""
|
||||
|
||||
|
||||
def set_benchmark(benchmark):
|
||||
"""Set the benchmark asset.
|
||||
|
||||
@@ -576,6 +603,7 @@ def set_benchmark(benchmark):
|
||||
automatically reinvested.
|
||||
"""
|
||||
|
||||
|
||||
def set_cancel_policy(cancel_policy):
|
||||
"""Sets the order cancellation policy for the simulation.
|
||||
|
||||
@@ -590,6 +618,7 @@ def set_cancel_policy(cancel_policy):
|
||||
:class:`catalyst.api.NeverCancel`
|
||||
"""
|
||||
|
||||
|
||||
def set_commission(commission):
|
||||
"""Sets the commission model for the simulation.
|
||||
|
||||
@@ -605,6 +634,7 @@ def set_commission(commission):
|
||||
:class:`catalyst.finance.commission.PerDollar`
|
||||
"""
|
||||
|
||||
|
||||
def set_do_not_order_list(restricted_list, on_error='fail'):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
|
||||
def set_long_only(on_error='fail'):
|
||||
"""Set a rule specifying that this algorithm cannot take short
|
||||
positions.
|
||||
"""
|
||||
|
||||
|
||||
def set_max_leverage(max_leverage):
|
||||
"""Set a limit on the maximum leverage of the algorithm.
|
||||
|
||||
@@ -629,6 +661,7 @@ def set_max_leverage(max_leverage):
|
||||
be no maximum.
|
||||
"""
|
||||
|
||||
|
||||
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
|
||||
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.
|
||||
"""
|
||||
|
||||
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
|
||||
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.
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
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
|
||||
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
|
||||
@@ -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.
|
||||
"""
|
||||
|
||||
|
||||
def set_slippage(slippage):
|
||||
"""Set the slippage model for the simulation.
|
||||
|
||||
@@ -694,6 +732,7 @@ def set_slippage(slippage):
|
||||
:class:`catalyst.finance.slippage.SlippageModel`
|
||||
"""
|
||||
|
||||
|
||||
def set_symbol_lookup_date(dt):
|
||||
"""Set the date for which symbols will be resolved to their assets
|
||||
(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.
|
||||
"""
|
||||
|
||||
|
||||
def sid(sid):
|
||||
"""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.
|
||||
"""
|
||||
|
||||
|
||||
def symbol(symbol_str):
|
||||
"""Lookup an Equity by its ticker symbol.
|
||||
|
||||
@@ -748,6 +789,7 @@ def symbol(symbol_str):
|
||||
:func:`catalyst.api.set_symbol_lookup_date`
|
||||
"""
|
||||
|
||||
|
||||
def symbols(*args):
|
||||
"""Lookup multuple Equities as a list.
|
||||
|
||||
@@ -773,3 +815,18 @@ def symbols(*args):
|
||||
: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
|
||||
-------
|
||||
|
||||
"""
|
||||
|
||||
+59
-10
@@ -433,7 +433,7 @@ cdef class TradingPair(Asset):
|
||||
'taker',
|
||||
'trading_state',
|
||||
'data_source',
|
||||
'decimals'
|
||||
'decimals',
|
||||
})
|
||||
def __init__(self,
|
||||
object symbol,
|
||||
@@ -455,7 +455,7 @@ cdef class TradingPair(Asset):
|
||||
float taker=0.0025,
|
||||
float lot=0,
|
||||
int decimals = 8,
|
||||
int trading_state=0,
|
||||
int trading_state=1,
|
||||
object data_source='catalyst'):
|
||||
"""
|
||||
Replicates the Asset constructor with some built-in conventions
|
||||
@@ -600,14 +600,51 @@ cdef class TradingPair(Asset):
|
||||
cpdef to_dict(self):
|
||||
"""
|
||||
Convert to a python dict.
|
||||
|
||||
Repeat constructor params:
|
||||
object symbol,
|
||||
object exchange,
|
||||
object start_date=None,
|
||||
object asset_name=None,
|
||||
int sid=0,
|
||||
float leverage=1.0,
|
||||
object end_daily=None,
|
||||
object end_minute=None,
|
||||
object end_date=None,
|
||||
object exchange_symbol=None,
|
||||
object first_traded=None,
|
||||
object auto_close_date=None,
|
||||
object exchange_full=None,
|
||||
float min_trade_size=0.0001,
|
||||
float max_trade_size=1000000,
|
||||
float maker=0.0015,
|
||||
float taker=0.0025,
|
||||
float lot=0,
|
||||
int decimals = 8,
|
||||
int trading_state=1,
|
||||
object data_source='catalyst',
|
||||
"""
|
||||
#TODO: missing fields
|
||||
super_dict = super(TradingPair, self).to_dict()
|
||||
super_dict['end_daily'] = self.end_daily
|
||||
super_dict['end_minute'] = self.end_minute
|
||||
super_dict['leverage'] = self.leverage
|
||||
super_dict['min_trade_size'] = self.min_trade_size
|
||||
return super_dict
|
||||
trading_pair_dict = dict(
|
||||
symbol=self.symbol,
|
||||
exchange=self.exchange,
|
||||
start_date=self.start_date,
|
||||
asset_name=self.asset_name,
|
||||
leverage=self.leverage,
|
||||
end_daily=self.end_daily,
|
||||
end_minute=self.end_minute,
|
||||
end_date=self.end_date,
|
||||
exchange_symbol=self.exchange_symbol,
|
||||
exchange_full=self.exchange_full,
|
||||
min_trade_size=self.min_trade_size,
|
||||
max_trade_size=self.max_trade_size,
|
||||
maker=self.maker,
|
||||
taker=self.taker,
|
||||
lot=self.lot,
|
||||
decimals=self.decimals,
|
||||
trading_state=self.trading_state,
|
||||
data_source=self.data_source,
|
||||
)
|
||||
return trading_pair_dict
|
||||
|
||||
def is_exchange_open(self, dt_minute):
|
||||
"""
|
||||
@@ -623,6 +660,16 @@ cdef class TradingPair(Asset):
|
||||
#TODO: make more dymanic to catch holds
|
||||
return True
|
||||
|
||||
def set_end_date(self, dt, data_frequency):
|
||||
if data_frequency == 'minute':
|
||||
self.end_minute = dt
|
||||
|
||||
else:
|
||||
self.end_daily = dt
|
||||
|
||||
def set_start_date(self, dt):
|
||||
self.start_date = dt
|
||||
|
||||
cpdef __reduce__(self):
|
||||
"""
|
||||
Function used by pickle to determine how to serialize/deserialize this
|
||||
@@ -646,7 +693,9 @@ cdef class TradingPair(Asset):
|
||||
self.lot,
|
||||
self.decimals,
|
||||
self.taker,
|
||||
self.maker))
|
||||
self.maker,
|
||||
self.trading_state,
|
||||
self.data_source))
|
||||
|
||||
def make_asset_array(int size, Asset asset):
|
||||
cdef np.ndarray out = np.empty([size], dtype=object)
|
||||
|
||||
+32
-1
@@ -11,8 +11,39 @@ LOG_LEVEL = int(os.environ.get('CATALYST_LOG_LEVEL', logbook.INFO))
|
||||
|
||||
SYMBOLS_URL = 'https://s3.amazonaws.com/enigmaco/catalyst-exchanges/' \
|
||||
'{exchange}/symbols.json'
|
||||
|
||||
EXCHANGE_CONFIG_URL = 'https://s3.amazonaws.com/enigmaco/ohlcv/' \
|
||||
'{exchange}/config.json'
|
||||
BUNDLE_URL = 'https://s3.amazonaws.com/enigmaco/ohlcv/' \
|
||||
'{exchange}/{data_frequency}/{name}.tar.gz'
|
||||
DATE_TIME_FORMAT = '%Y-%m-%d %H:%M'
|
||||
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
|
||||
|
||||
AUTH_SERVER = 'https://data.enigma.co'
|
||||
|
||||
# TODO: switch to mainnet
|
||||
ETH_REMOTE_NODE = 'https://ropsten.infura.io/'
|
||||
|
||||
|
||||
MARKETPLACE_CONTRACT = 'https://raw.githubusercontent.com/enigmampc/' \
|
||||
'catalyst/master/catalyst/marketplace/' \
|
||||
'contract_marketplace_address.txt'
|
||||
|
||||
MARKETPLACE_CONTRACT_ABI = 'https://raw.githubusercontent.com/enigmampc/' \
|
||||
'catalyst/master/catalyst/marketplace/' \
|
||||
'contract_marketplace_abi.json'
|
||||
|
||||
# TODO: switch to mainnet
|
||||
ENIGMA_CONTRACT = 'https://raw.githubusercontent.com/enigmampc/catalyst/' \
|
||||
'master/catalyst/marketplace/' \
|
||||
'contract_enigma_address.txt'
|
||||
|
||||
ENIGMA_CONTRACT_ABI = 'https://raw.githubusercontent.com/enigmampc/' \
|
||||
'catalyst/master/catalyst/marketplace/' \
|
||||
'contract_enigma_abi.json'
|
||||
|
||||
@@ -88,11 +88,11 @@ class AssetDispatchBarReader(with_metaclass(ABCMeta)):
|
||||
if self._last_available_dt is not None:
|
||||
return self._last_available_dt
|
||||
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
|
||||
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):
|
||||
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
|
||||
|
||||
# TODO: consider making configurable
|
||||
bm_symbol = 'btc_usdt'
|
||||
bm_symbol = 'btc_usd'
|
||||
# if trading_days is None:
|
||||
# trading_days = get_calendar('OPEN').schedule
|
||||
|
||||
@@ -144,8 +144,9 @@ def load_crypto_market_data(trading_day=None, trading_days=None,
|
||||
# breaks things and it's only needed here
|
||||
from catalyst.exchange.utils.factory import get_exchange
|
||||
exchange = get_exchange(
|
||||
exchange_name='poloniex', base_currency='usdt'
|
||||
exchange_name='bitfinex', base_currency='usd'
|
||||
)
|
||||
exchange.init()
|
||||
|
||||
benchmark_asset = exchange.get_asset(bm_symbol)
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ from catalyst.api import (order_target_value, symbol, record,
|
||||
|
||||
|
||||
def initialize(context):
|
||||
context.ASSET_NAME = 'btc_usd'
|
||||
context.ASSET_NAME = 'btc_usdt'
|
||||
context.TARGET_HODL_RATIO = 0.8
|
||||
context.RESERVE_RATIO = 1.0 - context.TARGET_HODL_RATIO
|
||||
|
||||
@@ -140,9 +140,9 @@ if __name__ == '__main__':
|
||||
initialize=initialize,
|
||||
handle_data=handle_data,
|
||||
analyze=analyze,
|
||||
exchange_name='bitfinex',
|
||||
exchange_name='poloniex',
|
||||
algo_namespace='buy_and_hodl',
|
||||
base_currency='usd',
|
||||
base_currency='usdt',
|
||||
start=pd.to_datetime('2015-03-01', utc=True),
|
||||
end=pd.to_datetime('2017-10-31', utc=True),
|
||||
)
|
||||
|
||||
@@ -27,7 +27,7 @@ import pandas as pd
|
||||
|
||||
|
||||
def initialize(context):
|
||||
context.asset = symbol('btc_usd')
|
||||
context.asset = symbol('btc_usdt')
|
||||
|
||||
|
||||
def handle_data(context, data):
|
||||
@@ -41,9 +41,9 @@ if __name__ == '__main__':
|
||||
data_frequency='daily',
|
||||
initialize=initialize,
|
||||
handle_data=handle_data,
|
||||
exchange_name='bitfinex',
|
||||
exchange_name='poloniex',
|
||||
algo_namespace='buy_and_hodl',
|
||||
base_currency='usd',
|
||||
base_currency='usdt',
|
||||
start=pd.to_datetime('2015-03-01', utc=True),
|
||||
end=pd.to_datetime('2017-10-31', utc=True),
|
||||
)
|
||||
|
||||
@@ -7,7 +7,6 @@ from catalyst.api import (
|
||||
order_target_percent,
|
||||
symbol,
|
||||
record,
|
||||
get_open_orders,
|
||||
)
|
||||
from catalyst.exchange.utils.stats_utils import get_pretty_stats
|
||||
from catalyst.utils.run_algo import run_algorithm
|
||||
@@ -60,7 +59,7 @@ def _handle_data(context, data):
|
||||
rsi=rsi,
|
||||
)
|
||||
|
||||
orders = get_open_orders(context.asset)
|
||||
orders = context.blotter.open_orders
|
||||
if orders:
|
||||
log.info('skipping bar until all open orders execute')
|
||||
return
|
||||
@@ -143,14 +142,14 @@ def analyze(context, stats):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
live = False
|
||||
live = True
|
||||
if live:
|
||||
run_algorithm(
|
||||
capital_base=0.001,
|
||||
capital_base=1000,
|
||||
initialize=initialize,
|
||||
handle_data=handle_data,
|
||||
analyze=analyze,
|
||||
exchange_name='binance',
|
||||
exchange_name='bittrex',
|
||||
live=True,
|
||||
algo_namespace=algo_namespace,
|
||||
base_currency='btc',
|
||||
|
||||
@@ -4,8 +4,7 @@ import pandas as pd
|
||||
from logbook import Logger
|
||||
|
||||
from catalyst import run_algorithm
|
||||
from catalyst.api import (record, symbol, order_target_percent,
|
||||
get_open_orders)
|
||||
from catalyst.api import (record, symbol, order_target_percent,)
|
||||
from catalyst.exchange.utils.stats_utils import extract_transactions
|
||||
|
||||
NAMESPACE = 'dual_moving_average'
|
||||
@@ -32,16 +31,18 @@ def handle_data(context, data):
|
||||
# moving average with the appropriate parameters. We choose to use
|
||||
# minute bars for this simulation -> freq="1m"
|
||||
# Returns a pandas dataframe.
|
||||
short_mavg = data.history(context.asset,
|
||||
short_data = data.history(context.asset,
|
||||
'price',
|
||||
bar_count=short_window,
|
||||
frequency="1m",
|
||||
).mean()
|
||||
long_mavg = data.history(context.asset,
|
||||
frequency="1T",
|
||||
)
|
||||
short_mavg = short_data.mean()
|
||||
long_data = data.history(context.asset,
|
||||
'price',
|
||||
bar_count=long_window,
|
||||
frequency="1m",
|
||||
).mean()
|
||||
frequency="1T",
|
||||
)
|
||||
long_mavg = long_data.mean()
|
||||
|
||||
# Let's keep the price of our asset in a more handy variable
|
||||
price = data.current(context.asset, 'price')
|
||||
@@ -61,7 +62,7 @@ def handle_data(context, data):
|
||||
|
||||
# Since we are using limit orders, some orders may not execute immediately
|
||||
# we wait until all orders are executed before considering more trades.
|
||||
orders = get_open_orders(context.asset)
|
||||
orders = context.blotter.open_orders
|
||||
if len(orders) > 0:
|
||||
return
|
||||
|
||||
@@ -82,9 +83,9 @@ def handle_data(context, data):
|
||||
|
||||
|
||||
def analyze(context, perf):
|
||||
|
||||
# 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
|
||||
ax1 = plt.subplot(411)
|
||||
@@ -92,7 +93,7 @@ def analyze(context, perf):
|
||||
ax1.legend_.remove()
|
||||
ax1.set_ylabel('Portfolio Value\n({})'.format(base_currency))
|
||||
start, end = ax1.get_ylim()
|
||||
ax1.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
|
||||
ax1.yaxis.set_ticks(np.arange(start, end, (end - start) / 5))
|
||||
|
||||
# Second chart: Plot asset price, moving averages and buys/sells
|
||||
ax2 = plt.subplot(412, sharex=ax1)
|
||||
@@ -103,9 +104,9 @@ def analyze(context, perf):
|
||||
ax2.set_ylabel('{asset}\n({base})'.format(
|
||||
asset=context.asset.symbol,
|
||||
base=base_currency
|
||||
))
|
||||
))
|
||||
start, end = ax2.get_ylim()
|
||||
ax2.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
|
||||
ax2.yaxis.set_ticks(np.arange(start, end, (end - start) / 5))
|
||||
|
||||
transaction_df = extract_transactions(perf)
|
||||
if not transaction_df.empty:
|
||||
@@ -135,19 +136,20 @@ def analyze(context, perf):
|
||||
ax3.legend_.remove()
|
||||
ax3.set_ylabel('Percent Change')
|
||||
start, end = ax3.get_ylim()
|
||||
ax3.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
|
||||
ax3.yaxis.set_ticks(np.arange(start, end, (end - start) / 5))
|
||||
|
||||
# Fourth chart: Plot our cash
|
||||
ax4 = plt.subplot(414, sharex=ax1)
|
||||
perf.cash.plot(ax=ax4)
|
||||
ax4.set_ylabel('Cash\n({})'.format(base_currency))
|
||||
start, end = ax4.get_ylim()
|
||||
ax4.yaxis.set_ticks(np.arange(0, end, end/5))
|
||||
ax4.yaxis.set_ticks(np.arange(0, end, end / 5))
|
||||
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
run_algorithm(
|
||||
capital_base=1000,
|
||||
data_frequency='minute',
|
||||
|
||||
@@ -0,0 +1,70 @@
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from catalyst import run_algorithm
|
||||
from catalyst.api import symbol, get_dataset
|
||||
|
||||
START = '2017-01-01'
|
||||
END = '2017-12-31'
|
||||
|
||||
|
||||
def initialize(context):
|
||||
pass
|
||||
|
||||
|
||||
def handle_data(context, data):
|
||||
context.github = get_dataset('github')
|
||||
context.github.sort_index(level=0, inplace=True)
|
||||
|
||||
context.zec = data.history(symbol('zec_usdt'),
|
||||
['price', ],
|
||||
bar_count=365,
|
||||
frequency="1d")
|
||||
context.xmr = data.history(symbol('xmr_usdt'),
|
||||
['price', ],
|
||||
bar_count=365,
|
||||
frequency="1d")
|
||||
|
||||
|
||||
def analyze(context=None, results=None):
|
||||
ax1 = plt.subplot(211)
|
||||
idx = pd.IndexSlice
|
||||
df = context.github.loc[START:END].loc[
|
||||
idx[:, [b'ZEC']], ['commits']].reset_index(
|
||||
level='symbol', drop=True)
|
||||
df.plot(ax=ax1, color='blue')
|
||||
ax1.legend(loc=2)
|
||||
ax1.set_title('Zcash')
|
||||
ax2 = ax1.twinx()
|
||||
context.zec['price'].loc[START:END].plot(ax=ax2, color='green')
|
||||
ax2.legend(loc=1)
|
||||
|
||||
ax3 = plt.subplot(212)
|
||||
idx = pd.IndexSlice
|
||||
df = context.github.loc[START:END].loc[
|
||||
idx[:, [b'XMR']], ['commits']].reset_index(
|
||||
level='symbol', drop=True)
|
||||
df.plot(ax=ax3, color='blue')
|
||||
ax3.legend(loc=2)
|
||||
ax3.set_title('Monero')
|
||||
ax4 = ax3.twinx()
|
||||
context.xmr['price'].loc[START:END].plot(ax=ax4, color='green')
|
||||
ax4.legend(loc=1)
|
||||
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_algorithm(
|
||||
capital_base=1000,
|
||||
data_frequency='daily',
|
||||
initialize=initialize,
|
||||
handle_data=handle_data,
|
||||
analyze=analyze,
|
||||
exchange_name='poloniex',
|
||||
algo_namespace='algo-github',
|
||||
base_currency='usdt',
|
||||
live=False,
|
||||
start=pd.to_datetime(END, utc=True),
|
||||
end=pd.to_datetime(END, utc=True),
|
||||
)
|
||||
@@ -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))
|
||||
@@ -38,13 +38,13 @@ def initialize(context):
|
||||
context.current_day = None
|
||||
|
||||
context.RSI_OVERSOLD = 55
|
||||
context.RSI_OVERBOUGHT = 65
|
||||
context.CANDLE_SIZE = '5T'
|
||||
context.RSI_OVERBOUGHT = 60
|
||||
context.CANDLE_SIZE = '15T'
|
||||
|
||||
context.start_time = time.time()
|
||||
|
||||
# context.set_commission(maker=0.1, taker=0.2)
|
||||
context.set_slippage(spread=0.0001)
|
||||
context.set_commission(maker=0.001, taker=0.002)
|
||||
context.set_slippage(spread=0.001)
|
||||
|
||||
|
||||
def handle_data(context, data):
|
||||
@@ -114,7 +114,7 @@ def handle_data(context, data):
|
||||
# TODO: retest with open orders
|
||||
# Since we are using limit orders, some orders may not execute immediately
|
||||
# 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:
|
||||
log.info('exiting because orders are open: {}'.format(orders))
|
||||
return
|
||||
@@ -161,7 +161,7 @@ def analyze(context=None, perf=None):
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
# 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.
|
||||
ax1 = plt.subplot(611)
|
||||
@@ -259,6 +259,7 @@ if __name__ == '__main__':
|
||||
live_graph=False,
|
||||
simulate_orders=False,
|
||||
stats_output=None,
|
||||
# auth_aliases=dict(poloniex='auth2')
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -280,7 +281,7 @@ if __name__ == '__main__':
|
||||
analyze=analyze,
|
||||
exchange_name='bitfinex',
|
||||
algo_namespace=NAMESPACE,
|
||||
base_currency='eth',
|
||||
base_currency='btc',
|
||||
start=pd.to_datetime('2017-10-01', utc=True),
|
||||
end=pd.to_datetime('2017-11-10', utc=True),
|
||||
output=out
|
||||
|
||||
@@ -0,0 +1,288 @@
|
||||
# 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 numpy as np
|
||||
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_open_orders
|
||||
from catalyst.exchange.utils.stats_utils import extract_transactions
|
||||
# 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.
|
||||
context.market = symbol('eth_btc')
|
||||
context.base_price = None
|
||||
context.current_day = None
|
||||
|
||||
context.RSI_OVERSOLD = 50
|
||||
context.RSI_OVERBOUGHT = 60
|
||||
context.CANDLE_SIZE = '5T'
|
||||
|
||||
context.start_time = time.time()
|
||||
|
||||
context.set_commission(maker=0.001, taker=0.002)
|
||||
# context.set_slippage(spread=0.001)
|
||||
|
||||
|
||||
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 = False
|
||||
context.current_day = today
|
||||
|
||||
# We're computing the volume-weighted-average-price of the security
|
||||
# defined above, in the context.market 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(
|
||||
context.market,
|
||||
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(context.market, 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 context.base_price is None:
|
||||
context.base_price = price
|
||||
|
||||
price_change = (price - context.base_price) / context.base_price
|
||||
cash = context.portfolio.cash
|
||||
|
||||
# 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(
|
||||
volume=current['volume'],
|
||||
price=price,
|
||||
price_change=price_change,
|
||||
rsi=rsi[-1],
|
||||
cash=cash
|
||||
)
|
||||
# We are trying to avoid over-trading by limiting our trades to
|
||||
# one per day.
|
||||
if context.traded_today:
|
||||
return
|
||||
|
||||
# TODO: retest with open orders
|
||||
# Since we are using limit orders, some orders may not execute immediately
|
||||
# we wait until all orders are executed before considering more trades.
|
||||
orders = get_open_orders(context.market)
|
||||
if len(orders) > 0:
|
||||
log.info('exiting because orders are open: {}'.format(orders))
|
||||
return
|
||||
|
||||
# Exit if we cannot trade
|
||||
if not data.can_trade(context.market):
|
||||
return
|
||||
|
||||
# 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[context.market].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
|
||||
order_target_percent(
|
||||
context.market, 1, limit_price=limit_price
|
||||
)
|
||||
context.traded_today = 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(
|
||||
context.market, 0, limit_price=limit_price
|
||||
)
|
||||
context.traded_today = True
|
||||
|
||||
|
||||
def analyze(context=None, perf=None):
|
||||
end = time.time()
|
||||
log.info('elapsed time: {}'.format(end - context.start_time))
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
# The base currency of the algo exchange
|
||||
base_currency = list(context.exchanges.values())[0].base_currency.upper()
|
||||
|
||||
# Plot the portfolio value over time.
|
||||
ax1 = plt.subplot(611)
|
||||
perf.loc[:, 'portfolio_value'].plot(ax=ax1)
|
||||
ax1.set_ylabel('Portfolio\nValue\n({})'.format(base_currency))
|
||||
|
||||
# Plot the price increase or decrease over time.
|
||||
ax2 = plt.subplot(612, sharex=ax1)
|
||||
perf.loc[:, 'price'].plot(ax=ax2, label='Price')
|
||||
|
||||
ax2.set_ylabel('{asset}\n({base})'.format(
|
||||
asset=context.market.symbol, base=base_currency
|
||||
))
|
||||
|
||||
transaction_df = extract_transactions(perf)
|
||||
if not transaction_df.empty:
|
||||
buy_df = transaction_df[transaction_df['amount'] > 0]
|
||||
sell_df = transaction_df[transaction_df['amount'] < 0]
|
||||
ax2.scatter(
|
||||
buy_df.index.to_pydatetime(),
|
||||
perf.loc[buy_df.index.floor('1 min'), 'price'],
|
||||
marker='^',
|
||||
s=100,
|
||||
c='green',
|
||||
label=''
|
||||
)
|
||||
ax2.scatter(
|
||||
sell_df.index.to_pydatetime(),
|
||||
perf.loc[sell_df.index.floor('1 min'), 'price'],
|
||||
marker='v',
|
||||
s=100,
|
||||
c='red',
|
||||
label=''
|
||||
)
|
||||
|
||||
ax4 = plt.subplot(613, sharex=ax1)
|
||||
perf.loc[:, 'cash'].plot(
|
||||
ax=ax4, label='Base Currency ({})'.format(base_currency)
|
||||
)
|
||||
ax4.set_ylabel('Cash\n({})'.format(base_currency))
|
||||
|
||||
perf['algorithm'] = perf.loc[:, 'algorithm_period_return']
|
||||
|
||||
ax5 = plt.subplot(614, sharex=ax1)
|
||||
perf.loc[:, ['algorithm', 'price_change']].plot(ax=ax5)
|
||||
ax5.set_ylabel('Percent\nChange')
|
||||
|
||||
ax6 = plt.subplot(615, sharex=ax1)
|
||||
perf.loc[:, 'rsi'].plot(ax=ax6, label='RSI')
|
||||
ax6.set_ylabel('RSI')
|
||||
ax6.axhline(context.RSI_OVERBOUGHT, color='darkgoldenrod')
|
||||
ax6.axhline(context.RSI_OVERSOLD, color='darkgoldenrod')
|
||||
|
||||
if not transaction_df.empty:
|
||||
ax6.scatter(
|
||||
buy_df.index.to_pydatetime(),
|
||||
perf.loc[buy_df.index.floor('1 min'), 'rsi'],
|
||||
marker='^',
|
||||
s=100,
|
||||
c='green',
|
||||
label=''
|
||||
)
|
||||
ax6.scatter(
|
||||
sell_df.index.to_pydatetime(),
|
||||
perf.loc[sell_df.index.floor('1 min'), 'rsi'],
|
||||
marker='v',
|
||||
s=100,
|
||||
c='red',
|
||||
label=''
|
||||
)
|
||||
plt.legend(loc=3)
|
||||
start, end = ax6.get_ylim()
|
||||
ax6.yaxis.set_ticks(np.arange(0, end, end / 5))
|
||||
|
||||
# Show the plot.
|
||||
plt.gcf().set_size_inches(18, 8)
|
||||
plt.show()
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# The execution mode: backtest or live
|
||||
live = False
|
||||
|
||||
if live:
|
||||
run_algorithm(
|
||||
capital_base=0.025,
|
||||
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=0.1,
|
||||
data_frequency='minute',
|
||||
initialize=initialize,
|
||||
handle_data=handle_data,
|
||||
analyze=analyze,
|
||||
exchange_name='bitfinex',
|
||||
algo_namespace=NAMESPACE,
|
||||
base_currency='eth',
|
||||
start=pd.to_datetime('2017-10-01', utc=True),
|
||||
end=pd.to_datetime('2017-11-10', utc=True),
|
||||
output=out
|
||||
)
|
||||
log.info('saved perf stats: {}'.format(out))
|
||||
@@ -66,7 +66,7 @@ def handle_data(context, data):
|
||||
# Define portfolio optimization parameters
|
||||
n_portfolios = 50000
|
||||
results_array = np.zeros((3 + context.nassets, n_portfolios))
|
||||
for p in xrange(n_portfolios):
|
||||
for p in range(n_portfolios):
|
||||
weights = np.random.random(context.nassets)
|
||||
weights /= np.sum(weights)
|
||||
w = np.asmatrix(weights)
|
||||
@@ -146,4 +146,5 @@ if __name__ == '__main__':
|
||||
start=start,
|
||||
end=end,
|
||||
exchange_name='poloniex',
|
||||
capital_base=100000, )
|
||||
capital_base=100000,
|
||||
base_currency='usdt', )
|
||||
|
||||
@@ -175,7 +175,7 @@ def handle_data(context, data):
|
||||
def analyze(context=None, results=None):
|
||||
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.
|
||||
ax1 = plt.subplot(611)
|
||||
results.loc[:, 'portfolio_value'].plot(ax=ax1)
|
||||
|
||||
@@ -26,7 +26,7 @@ def handle_data(context, data):
|
||||
context.asset,
|
||||
fields='price',
|
||||
bar_count=20,
|
||||
frequency='30T'
|
||||
frequency='2H'
|
||||
)
|
||||
last_traded = prices.index[-1]
|
||||
log.info('last candle date: {}'.format(last_traded))
|
||||
@@ -57,7 +57,7 @@ def analyze(context, perf):
|
||||
log.info('the stats: {}'.format(get_pretty_stats(perf)))
|
||||
|
||||
# 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.
|
||||
ax1 = plt.subplot(611)
|
||||
@@ -114,7 +114,7 @@ def analyze(context, perf):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
mode = 'backtest'
|
||||
mode = 'live'
|
||||
|
||||
if mode == 'backtest':
|
||||
run_algorithm(
|
||||
|
||||
@@ -41,8 +41,8 @@ from catalyst.exchange.utils.exchange_utils import get_exchange_symbols
|
||||
|
||||
def initialize(context):
|
||||
context.i = -1 # minute counter
|
||||
context.exchange = context.exchanges.values()[0].name.lower()
|
||||
context.base_currency = context.exchanges.values()[0].base_currency.lower()
|
||||
context.exchange = list(context.exchanges.values())[0].name.lower()
|
||||
context.base_currency = list(context.exchanges.values())[0].base_currency.lower()
|
||||
|
||||
|
||||
def handle_data(context, data):
|
||||
@@ -65,7 +65,7 @@ def handle_data(context, data):
|
||||
minutes = 30
|
||||
|
||||
# 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:
|
||||
# we iterate for every pair in the current universe
|
||||
for coin in context.coins:
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
@@ -7,9 +5,7 @@ import ccxt
|
||||
import pandas as pd
|
||||
import six
|
||||
from catalyst.assets._assets import TradingPair
|
||||
from ccxt import ExchangeNotAvailable, InvalidOrder
|
||||
from logbook import Logger
|
||||
from six import string_types
|
||||
from redo import retry
|
||||
|
||||
from catalyst.algorithm import MarketOrder
|
||||
from catalyst.constants import LOG_LEVEL
|
||||
@@ -17,12 +13,21 @@ from catalyst.exchange.exchange import Exchange
|
||||
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
||||
from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \
|
||||
ExchangeSymbolsNotFound, ExchangeRequestError, InvalidOrderStyle, \
|
||||
ExchangeNotFoundError, CreateOrderError, InvalidHistoryTimeframeError
|
||||
UnsupportedHistoryFrequencyError, \
|
||||
ExchangeNotFoundError, CreateOrderError, InvalidHistoryTimeframeError, \
|
||||
MarketsNotFoundError, InvalidMarketError
|
||||
from catalyst.exchange.exchange_execution import ExchangeLimitOrder
|
||||
from catalyst.exchange.utils.exchange_utils import mixin_market_params, \
|
||||
from_ms_timestamp, get_epoch, get_exchange_folder, get_catalyst_symbol, \
|
||||
get_exchange_auth
|
||||
from catalyst.exchange.utils.ccxt_utils import get_exchange_config
|
||||
from catalyst.exchange.utils.datetime_utils import from_ms_timestamp, \
|
||||
get_epoch, \
|
||||
get_periods_range
|
||||
from catalyst.exchange.utils.exchange_utils import get_catalyst_symbol
|
||||
from catalyst.finance.order import Order, ORDER_STATUS
|
||||
from catalyst.finance.transaction import Transaction
|
||||
from ccxt import InvalidOrder, NetworkError, \
|
||||
ExchangeError
|
||||
from logbook import Logger
|
||||
from six import string_types
|
||||
|
||||
log = Logger('CCXT', level=LOG_LEVEL)
|
||||
|
||||
@@ -37,7 +42,8 @@ SUPPORTED_EXCHANGES = dict(
|
||||
|
||||
|
||||
class CCXT(Exchange):
|
||||
def __init__(self, exchange_name, key, secret, base_currency):
|
||||
def __init__(self, exchange_name, key,
|
||||
secret, password, base_currency, config=None):
|
||||
log.debug(
|
||||
'finding {} in CCXT exchanges:\n{}'.format(
|
||||
exchange_name, ccxt.exchanges
|
||||
@@ -54,7 +60,11 @@ class CCXT(Exchange):
|
||||
self.api = exchange_attr({
|
||||
'apiKey': key,
|
||||
'secret': secret,
|
||||
'password': password,
|
||||
})
|
||||
self.api.enableRateLimit = True
|
||||
self.has = self.api.has
|
||||
self.fees = self.api.fees
|
||||
|
||||
except Exception:
|
||||
raise ExchangeNotFoundError(exchange_name=exchange_name)
|
||||
@@ -62,6 +72,7 @@ class CCXT(Exchange):
|
||||
self._symbol_maps = [None, None]
|
||||
|
||||
self.name = exchange_name
|
||||
self.assets = []
|
||||
|
||||
self.base_currency = base_currency
|
||||
self.transactions = defaultdict(list)
|
||||
@@ -70,94 +81,126 @@ class CCXT(Exchange):
|
||||
self.max_requests_per_minute = 60
|
||||
self.low_balance_threshold = 0.1
|
||||
self.request_cpt = dict()
|
||||
self._common_symbols = dict()
|
||||
|
||||
self.bundle = ExchangeBundle(self.name)
|
||||
self.markets = None
|
||||
self._is_init = False
|
||||
self._config = config
|
||||
|
||||
def init(self):
|
||||
if self._is_init:
|
||||
return
|
||||
|
||||
exchange_folder = get_exchange_folder(self.name)
|
||||
filename = os.path.join(exchange_folder, 'cctx_markets.json')
|
||||
|
||||
if os.path.exists(filename):
|
||||
timestamp = os.path.getmtime(filename)
|
||||
dt = pd.to_datetime(timestamp, unit='s', utc=True)
|
||||
|
||||
if dt >= pd.Timestamp.utcnow().floor('1D'):
|
||||
with open(filename) as f:
|
||||
self.markets = json.load(f)
|
||||
|
||||
log.debug('loaded markets for {}'.format(self.name))
|
||||
|
||||
if self.markets is None:
|
||||
try:
|
||||
markets_symbols = self.api.load_markets()
|
||||
log.debug(
|
||||
'fetching {} markets:\n{}'.format(
|
||||
self.name, markets_symbols
|
||||
)
|
||||
if self._config is None:
|
||||
self._config = get_exchange_config(self.name)
|
||||
log.debug(
|
||||
'got exchange config {}:\n{}'.format(
|
||||
self.name, self._config
|
||||
)
|
||||
|
||||
self.markets = self.api.fetch_markets()
|
||||
with open(filename, 'w+') as f:
|
||||
json.dump(self.markets, f, indent=4)
|
||||
|
||||
except ExchangeNotAvailable as e:
|
||||
raise ExchangeRequestError(error=e)
|
||||
)
|
||||
|
||||
self.load_assets()
|
||||
self._is_init = True
|
||||
|
||||
@staticmethod
|
||||
def find_exchanges(features=None, is_authenticated=False):
|
||||
ccxt_features = []
|
||||
if features is not None:
|
||||
for feature in features:
|
||||
if not feature.endswith('Bundle'):
|
||||
ccxt_features.append(feature)
|
||||
def load_assets(self):
|
||||
if self._config is None:
|
||||
raise ValueError('Exchange config not available.')
|
||||
|
||||
exchange_names = []
|
||||
for exchange_name in ccxt.exchanges:
|
||||
if is_authenticated:
|
||||
exchange_auth = get_exchange_auth(exchange_name)
|
||||
self.assets = []
|
||||
for asset_dict in self._config['assets']:
|
||||
asset = TradingPair(**asset_dict)
|
||||
self.assets.append(asset)
|
||||
|
||||
has_auth = (exchange_auth['key'] != ''
|
||||
and exchange_auth['secret'] != '')
|
||||
def _fetch_markets(self):
|
||||
markets_symbols = self.api.load_markets()
|
||||
log.debug(
|
||||
'fetching {} markets:\n{}'.format(
|
||||
self.name, markets_symbols
|
||||
)
|
||||
)
|
||||
try:
|
||||
markets = self.api.fetch_markets()
|
||||
|
||||
if not has_auth:
|
||||
continue
|
||||
except NetworkError as e:
|
||||
raise ExchangeRequestError(error=e)
|
||||
|
||||
log.debug('loading exchange: {}'.format(exchange_name))
|
||||
exchange = getattr(ccxt, exchange_name)()
|
||||
if not markets:
|
||||
raise MarketsNotFoundError(
|
||||
exchange=self.name,
|
||||
)
|
||||
|
||||
if ccxt_features is None:
|
||||
has_feature = True
|
||||
for market in markets:
|
||||
if 'id' not in market:
|
||||
raise InvalidMarketError(
|
||||
exchange=self.name,
|
||||
market=market,
|
||||
)
|
||||
return markets
|
||||
|
||||
else:
|
||||
try:
|
||||
has_feature = all(
|
||||
[exchange.has[feature] for feature in ccxt_features]
|
||||
)
|
||||
def create_exchange_config(self):
|
||||
config = dict(
|
||||
name=self.name,
|
||||
features=[feature for feature in self.has if self.has[feature]]
|
||||
)
|
||||
markets = retry(
|
||||
action=self._fetch_markets,
|
||||
attempts=5,
|
||||
sleeptime=5,
|
||||
retry_exceptions=(ExchangeRequestError,),
|
||||
cleanup=lambda: log.warn(
|
||||
'fetching markets again for {}'.format(self.name)
|
||||
),
|
||||
)
|
||||
|
||||
except Exception:
|
||||
has_feature = False
|
||||
config['assets'] = []
|
||||
for market in markets:
|
||||
asset = self.create_trading_pair(market=market)
|
||||
config['assets'].append(asset)
|
||||
|
||||
if has_feature:
|
||||
try:
|
||||
log.info('initializing {}'.format(exchange_name))
|
||||
exchange_names.append(exchange_name)
|
||||
return config
|
||||
|
||||
except Exception as e:
|
||||
log.warn(
|
||||
'unable to initialize exchange {}: {}'.format(
|
||||
exchange_name, e
|
||||
)
|
||||
)
|
||||
def create_trading_pair(self, market, start_dt=None, end_dt=None,
|
||||
leverage=1, end_daily=None, end_minute=None):
|
||||
"""
|
||||
Creating a TradingPair from market and asset data.
|
||||
|
||||
return exchange_names
|
||||
Parameters
|
||||
----------
|
||||
market: dict[str, Object]
|
||||
start_dt
|
||||
end_dt
|
||||
leverage
|
||||
end_daily
|
||||
end_minute
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
"""
|
||||
params = dict(
|
||||
exchange=self.name,
|
||||
data_source='catalyst',
|
||||
exchange_symbol=market['id'],
|
||||
symbol=get_catalyst_symbol(market),
|
||||
start_date=start_dt,
|
||||
end_date=end_dt,
|
||||
leverage=leverage,
|
||||
asset_name=market['symbol'],
|
||||
end_daily=end_daily,
|
||||
end_minute=end_minute,
|
||||
)
|
||||
self.apply_conditional_market_params(params, market)
|
||||
|
||||
return TradingPair(**params)
|
||||
|
||||
def load_assets(self):
|
||||
if self._config is None or 'error' in self._config:
|
||||
raise ValueError('Exchange config not available.')
|
||||
|
||||
self.assets = []
|
||||
for asset_dict in self._config['assets']:
|
||||
asset = TradingPair(**asset_dict)
|
||||
self.assets.append(asset)
|
||||
|
||||
def account(self):
|
||||
return None
|
||||
@@ -189,26 +232,20 @@ class CCXT(Exchange):
|
||||
|
||||
return frequencies
|
||||
|
||||
def get_market(self, symbol):
|
||||
"""
|
||||
The CCXT market.
|
||||
def substitute_currency_code(self, currency, source='catalyst'):
|
||||
if source == 'catalyst':
|
||||
currency = currency.upper()
|
||||
|
||||
Parameters
|
||||
----------
|
||||
symbol:
|
||||
The CCXT symbol.
|
||||
key = self.api.common_currency_code(currency).lower()
|
||||
self._common_symbols[key] = currency.lower()
|
||||
return key
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict[str, Object]
|
||||
else:
|
||||
if currency in self._common_symbols:
|
||||
return self._common_symbols[currency]
|
||||
|
||||
"""
|
||||
s = self.get_symbol(symbol)
|
||||
market = next(
|
||||
(market for market in self.markets if market['symbol'] == s),
|
||||
None,
|
||||
)
|
||||
return market
|
||||
else:
|
||||
return currency.lower()
|
||||
|
||||
def get_symbol(self, asset_or_symbol, source='catalyst'):
|
||||
"""
|
||||
@@ -217,6 +254,7 @@ class CCXT(Exchange):
|
||||
Parameters
|
||||
----------
|
||||
asset_or_symbol
|
||||
source
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -362,8 +400,8 @@ class CCXT(Exchange):
|
||||
timeframe, source='ccxt', raise_error=raise_error
|
||||
)
|
||||
|
||||
def get_candles(self, freq, assets, bar_count=None, start_dt=None,
|
||||
end_dt=None):
|
||||
def get_candles(self, freq, assets, bar_count=1, start_dt=None,
|
||||
end_dt=None, floor_dates=True):
|
||||
is_single = (isinstance(assets, TradingPair))
|
||||
if is_single:
|
||||
assets = [assets]
|
||||
@@ -371,33 +409,62 @@ class CCXT(Exchange):
|
||||
symbols = self.get_symbols(assets)
|
||||
timeframe = CCXT.get_timeframe(freq)
|
||||
|
||||
ms = None
|
||||
if start_dt is not None:
|
||||
delta = start_dt - get_epoch()
|
||||
ms = int(delta.total_seconds()) * 1000
|
||||
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.'
|
||||
)
|
||||
|
||||
if start_dt is None:
|
||||
if end_dt is None:
|
||||
end_dt = pd.Timestamp.utcnow()
|
||||
|
||||
dt_range = get_periods_range(
|
||||
end_dt=end_dt,
|
||||
periods=bar_count,
|
||||
freq=freq,
|
||||
)
|
||||
start_dt = dt_range[0]
|
||||
|
||||
delta = start_dt - get_epoch()
|
||||
since = int(delta.total_seconds()) * 1000
|
||||
|
||||
candles = dict()
|
||||
for asset in assets:
|
||||
for index, asset in enumerate(assets):
|
||||
ohlcvs = self.api.fetch_ohlcv(
|
||||
symbol=symbols[0],
|
||||
symbol=symbols[index],
|
||||
timeframe=timeframe,
|
||||
since=ms,
|
||||
since=since,
|
||||
limit=bar_count,
|
||||
params={}
|
||||
)
|
||||
|
||||
candles[asset] = []
|
||||
for ohlcv in ohlcvs:
|
||||
candles[asset].append(dict(
|
||||
last_traded=pd.to_datetime(
|
||||
ohlcv[0], unit='ms', utc=True
|
||||
),
|
||||
open=ohlcv[1],
|
||||
high=ohlcv[2],
|
||||
low=ohlcv[3],
|
||||
close=ohlcv[4],
|
||||
volume=ohlcv[5]
|
||||
))
|
||||
dt = pd.to_datetime(ohlcv[0], unit='ms', utc=True)
|
||||
if floor_dates:
|
||||
dt = dt.floor('1T')
|
||||
|
||||
candles[asset].append(
|
||||
dict(
|
||||
last_traded=dt,
|
||||
open=ohlcv[1],
|
||||
high=ohlcv[2],
|
||||
low=ohlcv[3],
|
||||
close=ohlcv[4],
|
||||
volume=ohlcv[5],
|
||||
)
|
||||
)
|
||||
candles[asset] = sorted(
|
||||
candles[asset], key=lambda c: c['last_traded']
|
||||
)
|
||||
|
||||
if is_single:
|
||||
return six.next(six.itervalues(candles))
|
||||
@@ -408,147 +475,57 @@ class CCXT(Exchange):
|
||||
def _fetch_symbol_map(self, is_local):
|
||||
try:
|
||||
return self.fetch_symbol_map(is_local)
|
||||
|
||||
except ExchangeSymbolsNotFound:
|
||||
return None
|
||||
|
||||
def get_asset_defs(self, market):
|
||||
def apply_conditional_market_params(self, params, market):
|
||||
"""
|
||||
The local and Catalyst definitions of the specified market.
|
||||
Applies a CCXT market dict to parameters of TradingPair init.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
market: dict[str, Object]
|
||||
The CCXT market dicts.
|
||||
params: dict[Object]
|
||||
market: dict[Object]
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict[str, Object]
|
||||
The asset definition.
|
||||
|
||||
"""
|
||||
asset_defs = []
|
||||
|
||||
for is_local in (False, True):
|
||||
asset_def = self.get_asset_def(market, is_local)
|
||||
asset_defs.append((asset_def, is_local))
|
||||
|
||||
return asset_defs
|
||||
|
||||
def get_asset_def(self, market, is_local=False):
|
||||
"""
|
||||
The asset definition (in symbols.json files) corresponding
|
||||
to the the specified market.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
market: dict[str, Object]
|
||||
The CCXT market dict.
|
||||
is_local
|
||||
Whether to search in local or Catalyst asset definitions.
|
||||
|
||||
Returns
|
||||
-------
|
||||
dict[str, Object]
|
||||
The asset definition.
|
||||
|
||||
"""
|
||||
exchange_symbol = market['id']
|
||||
|
||||
symbol_map = self._fetch_symbol_map(is_local)
|
||||
if symbol_map is not None:
|
||||
assets_lower = {k.lower(): v for k, v in symbol_map.items()}
|
||||
key = exchange_symbol.lower()
|
||||
|
||||
asset = assets_lower[key] if key in assets_lower else None
|
||||
if asset is not None:
|
||||
return asset
|
||||
|
||||
else:
|
||||
return None
|
||||
# TODO: make this more externalized / configurable
|
||||
# Consider representing in some type of JSON structure
|
||||
if 'active' in market:
|
||||
params['trading_state'] = 1 if market['active'] else 0
|
||||
|
||||
else:
|
||||
return None
|
||||
params['trading_state'] = 1
|
||||
|
||||
def create_trading_pair(self, market, asset_def=None, is_local=False):
|
||||
"""
|
||||
Creating a TradingPair from market and asset data.
|
||||
if 'lot' in market:
|
||||
params['min_trade_size'] = market['lot']
|
||||
params['lot'] = market['lot']
|
||||
|
||||
Parameters
|
||||
----------
|
||||
market: dict[str, Object]
|
||||
asset_def: dict[str, Object]
|
||||
is_local: bool
|
||||
if self.name == 'bitfinex':
|
||||
params['maker'] = 0.001
|
||||
params['taker'] = 0.002
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
"""
|
||||
data_source = 'local' if is_local else 'catalyst'
|
||||
params = dict(
|
||||
exchange=self.name,
|
||||
data_source=data_source,
|
||||
exchange_symbol=market['id'],
|
||||
)
|
||||
mixin_market_params(self.name, params, market)
|
||||
|
||||
if asset_def is not None:
|
||||
params['symbol'] = asset_def['symbol']
|
||||
|
||||
params['start_date'] = asset_def['start_date'] \
|
||||
if 'start_date' in asset_def else None
|
||||
|
||||
params['end_date'] = asset_def['end_date'] \
|
||||
if 'end_date' in asset_def else None
|
||||
|
||||
params['leverage'] = asset_def['leverage'] \
|
||||
if 'leverage' in asset_def else 1.0
|
||||
|
||||
params['asset_name'] = asset_def['asset_name'] \
|
||||
if 'asset_name' in asset_def else None
|
||||
|
||||
params['end_daily'] = asset_def['end_daily'] \
|
||||
if 'end_daily' in asset_def \
|
||||
and asset_def['end_daily'] != 'N/A' else None
|
||||
|
||||
params['end_minute'] = asset_def['end_minute'] \
|
||||
if 'end_minute' in asset_def \
|
||||
and asset_def['end_minute'] != 'N/A' else None
|
||||
elif 'maker' in market and 'taker' in market \
|
||||
and market['maker'] is not None \
|
||||
and market['taker'] is not None:
|
||||
params['maker'] = market['maker']
|
||||
params['taker'] = market['taker']
|
||||
|
||||
else:
|
||||
params['symbol'] = get_catalyst_symbol(market)
|
||||
# TODO: add as an optional column
|
||||
params['leverage'] = 1.0
|
||||
# TODO: default commission, make configurable
|
||||
params['maker'] = 0.0015
|
||||
params['taker'] = 0.0025
|
||||
|
||||
return TradingPair(**params)
|
||||
info = market['info'] if 'info' in market else None
|
||||
if info:
|
||||
if 'minimum_order_size' in info:
|
||||
params['min_trade_size'] = float(info['minimum_order_size'])
|
||||
|
||||
def load_assets(self):
|
||||
log.debug('loading assets for {}'.format(self.name))
|
||||
self.assets = []
|
||||
|
||||
for market in self.markets:
|
||||
if 'id' not in market:
|
||||
log.warn('invalid market: {}'.format(market))
|
||||
continue
|
||||
|
||||
asset_defs = self.get_asset_defs(market)
|
||||
|
||||
asset = None
|
||||
for asset_def in asset_defs:
|
||||
if asset_def[0] is not None or not asset_defs[1]:
|
||||
try:
|
||||
asset = self.create_trading_pair(
|
||||
market=market,
|
||||
asset_def=asset_def[0],
|
||||
is_local=asset_def[1]
|
||||
)
|
||||
self.assets.append(asset)
|
||||
|
||||
except TypeError as e:
|
||||
log.warn('unable to add asset: {}'.format(e))
|
||||
|
||||
if asset is None:
|
||||
asset = self.create_trading_pair(market=market)
|
||||
self.assets.append(asset)
|
||||
if 'lot' not in params:
|
||||
params['lot'] = params['min_trade_size']
|
||||
|
||||
def get_balances(self):
|
||||
try:
|
||||
@@ -559,8 +536,12 @@ class CCXT(Exchange):
|
||||
for key in balances:
|
||||
balances_lower[key.lower()] = balances[key]
|
||||
|
||||
except Exception as e:
|
||||
log.debug('error retrieving balances: {}', e)
|
||||
except (ExchangeError, NetworkError) as e:
|
||||
log.warn(
|
||||
'unable to fetch balance {}: {}'.format(
|
||||
self.name, e
|
||||
)
|
||||
)
|
||||
raise ExchangeRequestError(error=e)
|
||||
|
||||
return balances_lower
|
||||
@@ -692,8 +673,9 @@ class CCXT(Exchange):
|
||||
abs(amount), adj_amount,
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
adj_amount = abs(amount)
|
||||
adj_amount = round(abs(amount), asset.decimals)
|
||||
|
||||
try:
|
||||
result = self.api.create_order(
|
||||
@@ -703,14 +685,34 @@ class CCXT(Exchange):
|
||||
amount=adj_amount,
|
||||
price=price
|
||||
)
|
||||
except ExchangeNotAvailable as e:
|
||||
log.debug('unable to create order: {}'.format(e))
|
||||
raise ExchangeRequestError(error=e)
|
||||
|
||||
except InvalidOrder as e:
|
||||
log.warn('the exchange rejected the order: {}'.format(e))
|
||||
raise CreateOrderError(exchange=self.name, error=e)
|
||||
|
||||
except (ExchangeError, NetworkError) as e:
|
||||
log.warn(
|
||||
'unable to create order {} / {}: {}'.format(
|
||||
self.name, symbol, 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:
|
||||
raise ValueError('cannot use order without info attribute')
|
||||
|
||||
@@ -735,18 +737,128 @@ class CCXT(Exchange):
|
||||
limit=None,
|
||||
params=dict()
|
||||
)
|
||||
except Exception as e:
|
||||
except (ExchangeError, NetworkError) as e:
|
||||
log.warn(
|
||||
'unable to fetch open orders {} / {}: {}'.format(
|
||||
self.name, asset.symbol, e
|
||||
)
|
||||
)
|
||||
raise ExchangeRequestError(error=e)
|
||||
|
||||
orders = []
|
||||
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:
|
||||
orders.append(order)
|
||||
|
||||
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:
|
||||
log.debug(
|
||||
'order not found in memory, the request might fail '
|
||||
@@ -758,12 +870,22 @@ class CCXT(Exchange):
|
||||
order_status = self.api.fetch_order(id=order_id, symbol=symbol)
|
||||
order, executed_price = self._create_order(order_status)
|
||||
|
||||
except Exception as e:
|
||||
if return_price:
|
||||
return order, executed_price
|
||||
|
||||
else:
|
||||
return order
|
||||
|
||||
except (ExchangeError, NetworkError) as e:
|
||||
log.warn(
|
||||
'unable to fetch order {} / {}: {}'.format(
|
||||
self.name, order_id, 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, params={}):
|
||||
order_id = order_param.id \
|
||||
if isinstance(order_param, Order) else order_param
|
||||
|
||||
@@ -775,12 +897,18 @@ class CCXT(Exchange):
|
||||
try:
|
||||
symbol = self.get_symbol(asset_or_symbol) \
|
||||
if asset_or_symbol is not None else None
|
||||
self.api.cancel_order(id=order_id, symbol=symbol)
|
||||
self.api.cancel_order(id=order_id,
|
||||
symbol=symbol, params=params)
|
||||
|
||||
except Exception as e:
|
||||
except (ExchangeError, NetworkError) as e:
|
||||
log.warn(
|
||||
'unable to cancel order {} / {}: {}'.format(
|
||||
self.name, order_id, 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
|
||||
|
||||
@@ -793,48 +921,70 @@ class CCXT(Exchange):
|
||||
list[dict[str, float]
|
||||
|
||||
"""
|
||||
tickers = dict()
|
||||
try:
|
||||
for asset in assets:
|
||||
symbol = self.get_symbol(asset)
|
||||
# TODO: use fetch_tickers() for efficiency
|
||||
# I tried using fetch_tickers() but noticed some
|
||||
# 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
|
||||
if len(assets) == 1:
|
||||
try:
|
||||
symbol = self.get_symbol(assets[0])
|
||||
log.debug('fetching single ticker: {}'.format(symbol))
|
||||
results = dict()
|
||||
results[symbol] = self.api.fetch_ticker(symbol=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
|
||||
|
||||
except ExchangeNotAvailable as e:
|
||||
log.warn(
|
||||
'unable to fetch ticker: {} {}'.format(
|
||||
self.name, asset.symbol
|
||||
except (ExchangeError, NetworkError,) as e:
|
||||
log.warn(
|
||||
'unable to fetch ticker {} / {}: {}'.format(
|
||||
self.name, 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
|
||||
|
||||
@@ -864,3 +1014,35 @@ class CCXT(Exchange):
|
||||
))
|
||||
|
||||
return result
|
||||
|
||||
def get_trades(self, asset, my_trades=True, start_dt=None, limit=100):
|
||||
# TODO: is it possible to sort this? Limit is useless otherwise.
|
||||
ccxt_symbol = self.get_symbol(asset)
|
||||
if start_dt:
|
||||
delta = start_dt - get_epoch()
|
||||
since = int(delta.total_seconds()) * 1000
|
||||
else:
|
||||
since = None
|
||||
|
||||
try:
|
||||
if my_trades:
|
||||
trades = self.api.fetch_my_trades(
|
||||
symbol=ccxt_symbol,
|
||||
since=since,
|
||||
limit=limit,
|
||||
)
|
||||
else:
|
||||
trades = self.api.fetch_trades(
|
||||
symbol=ccxt_symbol,
|
||||
since=since,
|
||||
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
|
||||
|
||||
+113
-77
@@ -13,12 +13,14 @@ from catalyst.exchange.exchange_bundle import ExchangeBundle
|
||||
from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \
|
||||
SymbolNotFoundOnExchange, \
|
||||
PricingDataNotLoadedError, \
|
||||
NoDataAvailableOnExchange, NoValueForField, LastCandleTooEarlyError, \
|
||||
NoDataAvailableOnExchange, NoValueForField, \
|
||||
NoCandlesReceivedFromExchange, \
|
||||
TickerNotFoundError, NotEnoughCashError
|
||||
from catalyst.exchange.utils.bundle_utils import get_start_dt, \
|
||||
get_delta, get_periods, get_periods_range
|
||||
from catalyst.exchange.utils.exchange_utils import get_exchange_symbols, \
|
||||
get_frequency, resample_history_df, has_bundle
|
||||
from catalyst.exchange.utils.datetime_utils import get_delta, \
|
||||
get_periods_range, \
|
||||
get_periods, get_start_dt, get_frequency
|
||||
from catalyst.exchange.utils.exchange_utils import \
|
||||
resample_history_df, has_bundle
|
||||
|
||||
log = Logger('Exchange', level=LOG_LEVEL)
|
||||
|
||||
@@ -178,6 +180,7 @@ class Exchange:
|
||||
if symbols is None:
|
||||
# Make a distinct list of all symbols
|
||||
symbols = list(set([asset.symbol for asset in self.assets]))
|
||||
symbols.sort()
|
||||
|
||||
if quote_currency is not None:
|
||||
for symbol in symbols[:]:
|
||||
@@ -234,11 +237,15 @@ class Exchange:
|
||||
"""
|
||||
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(
|
||||
'searching assets for: {} {}'.format(
|
||||
self.name, symbol
|
||||
)
|
||||
)
|
||||
# TODO: simplify and loose the loop
|
||||
for a in self.assets:
|
||||
if asset is not None:
|
||||
break
|
||||
@@ -250,9 +257,10 @@ class Exchange:
|
||||
elif data_frequency is not None:
|
||||
applies = (
|
||||
(
|
||||
data_frequency == 'minute' and a.end_minute is not None)
|
||||
or (
|
||||
data_frequency == 'daily' and a.end_daily is not None)
|
||||
data_frequency == 'minute' and a.end_minute is not None
|
||||
) or (
|
||||
data_frequency == 'daily' and a.end_daily is not None
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
@@ -260,7 +268,8 @@ class Exchange:
|
||||
|
||||
# The symbol provided may use the Catalyst or the exchange
|
||||
# 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 applies:
|
||||
asset = a
|
||||
@@ -276,7 +285,7 @@ class Exchange:
|
||||
supported_symbols = sorted([a.symbol for a in self.assets])
|
||||
|
||||
raise SymbolNotFoundOnExchange(
|
||||
symbol=symbol,
|
||||
symbol=og_symbol,
|
||||
exchange=self.name.title(),
|
||||
supported_symbols=supported_symbols
|
||||
)
|
||||
@@ -284,16 +293,6 @@ class Exchange:
|
||||
log.debug('found asset: {}'.format(asset))
|
||||
return asset
|
||||
|
||||
def fetch_symbol_map(self, is_local=False):
|
||||
index = 1 if is_local else 0
|
||||
if self._symbol_maps[index] is not None:
|
||||
return self._symbol_maps[index]
|
||||
|
||||
else:
|
||||
symbol_map = get_exchange_symbols(self.name, is_local)
|
||||
self._symbol_maps[index] = symbol_map
|
||||
return symbol_map
|
||||
|
||||
@abstractmethod
|
||||
def init(self):
|
||||
"""
|
||||
@@ -305,24 +304,13 @@ class Exchange:
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def load_assets(self, is_local=False):
|
||||
def create_exchange_config(self):
|
||||
"""
|
||||
Populate the 'assets' attribute with a dictionary of Assets.
|
||||
The key of the resulting dictionary is the exchange specific
|
||||
currency pair symbol. The universal symbol is contained in the
|
||||
'symbol' attribute of each asset.
|
||||
|
||||
Notes
|
||||
-----
|
||||
The sid of each asset is calculated based on a numeric hash of the
|
||||
universal symbol. This simple approach avoids maintaining a mapping
|
||||
of sids.
|
||||
|
||||
This method can be omerridden if an exchange offers equivalent data
|
||||
via its api.
|
||||
Fetch the exchange market data and generate a config object
|
||||
Returns
|
||||
-------
|
||||
|
||||
"""
|
||||
pass
|
||||
|
||||
def get_spot_value(self, assets, field, dt=None, data_frequency='minute'):
|
||||
"""
|
||||
@@ -434,7 +422,7 @@ class Exchange:
|
||||
series = pd.Series(values, index=dates)
|
||||
|
||||
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
|
||||
series = series.reindex(
|
||||
@@ -496,47 +484,54 @@ class Exchange:
|
||||
|
||||
"""
|
||||
freq, candle_size, unit, data_frequency = get_frequency(
|
||||
frequency, data_frequency
|
||||
frequency, data_frequency, supported_freqs=['T', 'D', 'H']
|
||||
)
|
||||
adj_bar_count = candle_size * bar_count
|
||||
|
||||
start_dt = get_start_dt(end_dt, adj_bar_count, data_frequency)
|
||||
|
||||
# we want to avoid receiving empty candles
|
||||
# so we request more than needed
|
||||
# TODO: consider defining a const per asset
|
||||
# and/or some retry mechanism (in each iteration request more data)
|
||||
requested_bar_count = bar_count + 30
|
||||
# The get_history method supports multiple asset
|
||||
candles = self.get_candles(
|
||||
freq=freq,
|
||||
assets=assets,
|
||||
bar_count=bar_count,
|
||||
start_dt=start_dt if not is_current else None,
|
||||
bar_count=requested_bar_count,
|
||||
end_dt=end_dt if not is_current else None,
|
||||
)
|
||||
|
||||
series = dict()
|
||||
# candles sanity check - verify no empty candles were received:
|
||||
for asset in candles:
|
||||
asset_series = self.get_series_from_candles(
|
||||
candles=candles[asset],
|
||||
start_dt=start_dt,
|
||||
end_dt=end_dt,
|
||||
data_frequency=frequency,
|
||||
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 not candles[asset]:
|
||||
raise NoCandlesReceivedFromExchange(
|
||||
bar_count=requested_bar_count,
|
||||
end_dt=end_dt,
|
||||
asset=asset,
|
||||
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 = get_candles_df(candles=candles,
|
||||
field=field,
|
||||
freq=frequency,
|
||||
bar_count=requested_bar_count,
|
||||
end_dt=end_dt)
|
||||
|
||||
# TODO: consider how to approach this edge case
|
||||
# delta_candle_size = candle_size * 60 if unit == 'H' else candle_size
|
||||
# Checking to make sure that the dates match
|
||||
# delta = get_delta(delta_candle_size, data_frequency)
|
||||
# adj_end_dt = end_dt - delta
|
||||
# last_traded = asset_series.index[-1]
|
||||
# if last_traded < adj_end_dt:
|
||||
# raise LastCandleTooEarlyError(
|
||||
# last_traded=last_traded,
|
||||
# end_dt=adj_end_dt,
|
||||
# exchange=self.name,
|
||||
# )
|
||||
|
||||
df = pd.DataFrame(series)
|
||||
df.dropna(inplace=True)
|
||||
|
||||
return df
|
||||
return df.tail(bar_count)
|
||||
|
||||
def get_history_window_with_bundle(self,
|
||||
assets,
|
||||
@@ -584,11 +579,12 @@ class Exchange:
|
||||
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(
|
||||
frequency, data_frequency
|
||||
)
|
||||
adj_bar_count = candle_size * bar_count
|
||||
|
||||
try:
|
||||
series = self.bundle.get_history_window_series_and_load(
|
||||
assets=assets,
|
||||
@@ -615,15 +611,14 @@ class Exchange:
|
||||
# The get_history method supports multiple asset
|
||||
# Use the original frequency to let each api optimize
|
||||
# the size of result sets
|
||||
trailing_bar_count = get_periods(
|
||||
trailing_bars = get_periods(
|
||||
trailing_dt, end_dt, freq
|
||||
)
|
||||
candles = self.get_candles(
|
||||
freq=freq,
|
||||
assets=asset,
|
||||
bar_count=trailing_bar_count,
|
||||
start_dt=start_dt,
|
||||
end_dt=end_dt
|
||||
end_dt=end_dt,
|
||||
bar_count=trailing_bars if trailing_bars < 500 else 500,
|
||||
)
|
||||
|
||||
last_value = series[asset].iloc(0) if asset in series \
|
||||
@@ -652,16 +647,21 @@ class Exchange:
|
||||
|
||||
return df
|
||||
|
||||
def _check_low_balance(self, currency, balances, amount):
|
||||
def _check_low_balance(self, currency, balances, amount, open_orders=None):
|
||||
free = balances[currency]['free'] if currency in balances else 0.0
|
||||
|
||||
if open_orders:
|
||||
# TODO: make sure that this works
|
||||
free += sum([order.amount for order in open_orders])
|
||||
|
||||
if free < amount:
|
||||
return free, True
|
||||
|
||||
else:
|
||||
return free, False
|
||||
|
||||
def sync_positions(self, positions, cash=None, check_balances=False):
|
||||
def sync_positions(self, positions, open_orders=None, cash=None,
|
||||
check_balances=False):
|
||||
"""
|
||||
Update the portfolio cash and position balances based on the
|
||||
latest ticker prices.
|
||||
@@ -690,7 +690,7 @@ class Exchange:
|
||||
balances=balances,
|
||||
amount=cash,
|
||||
)
|
||||
if is_lower:
|
||||
if is_lower and not open_orders:
|
||||
raise NotEnoughCashError(
|
||||
currency=self.base_currency,
|
||||
exchange=self.name,
|
||||
@@ -699,8 +699,8 @@ class Exchange:
|
||||
)
|
||||
|
||||
positions_value = 0.0
|
||||
if positions is not None:
|
||||
assets = set([position.asset for position in positions])
|
||||
if positions:
|
||||
assets = list(set([position.asset for position in positions]))
|
||||
tickers = self.tickers(assets)
|
||||
|
||||
for position in positions:
|
||||
@@ -901,7 +901,24 @@ class Exchange:
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cancel_order(self, order_param, symbol_or_asset=None):
|
||||
def process_order(self, order):
|
||||
"""
|
||||
Similar to get_order but looks only for executed orders.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
order: Order
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
Avg execution price
|
||||
|
||||
"""
|
||||
|
||||
@abstractmethod
|
||||
def cancel_order(self, order_param,
|
||||
symbol_or_asset=None, params={}):
|
||||
"""Cancel an open order.
|
||||
|
||||
Parameters
|
||||
@@ -910,12 +927,12 @@ class Exchange:
|
||||
The order_id or order object to cancel.
|
||||
symbol_or_asset: str|TradingPair
|
||||
The catalyst symbol, some exchanges need this
|
||||
params:
|
||||
"""
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_candles(self, freq, assets, bar_count=None,
|
||||
start_dt=None, end_dt=None):
|
||||
def get_candles(self, freq, assets, bar_count, start_dt=None, end_dt=None):
|
||||
"""
|
||||
Retrieve OHLCV candles for the given assets
|
||||
|
||||
@@ -955,13 +972,15 @@ class Exchange:
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def tickers(self, assets):
|
||||
def tickers(self, assets, on_ticker_error='raise'):
|
||||
"""
|
||||
Retrieve current tick data for the given assets
|
||||
|
||||
Parameters
|
||||
----------
|
||||
assets: list[TradingPair]
|
||||
on_ticker_error: str [raise|warn]
|
||||
How to handle an error when retrieving a single ticker.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -980,7 +999,7 @@ class Exchange:
|
||||
@abc.abstractmethod
|
||||
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
|
||||
----------
|
||||
@@ -994,3 +1013,20 @@ class Exchange:
|
||||
list[dict[str, float]
|
||||
"""
|
||||
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
|
||||
-------
|
||||
|
||||
"""
|
||||
|
||||
@@ -16,7 +16,7 @@ import signal
|
||||
import sys
|
||||
from datetime import timedelta
|
||||
from os import listdir
|
||||
from os.path import isfile, join
|
||||
from os.path import isfile, join, exists
|
||||
|
||||
import logbook
|
||||
import pandas as pd
|
||||
@@ -38,13 +38,16 @@ from catalyst.exchange.utils.exchange_utils import (
|
||||
get_algo_folder,
|
||||
get_algo_df,
|
||||
save_algo_df,
|
||||
clear_frame_stats_directory,
|
||||
remove_old_files,
|
||||
group_assets_by_exchange, )
|
||||
from catalyst.exchange.utils.stats_utils import get_pretty_stats, stats_to_s3, \
|
||||
stats_to_algo_folder
|
||||
from catalyst.exchange.utils.stats_utils import \
|
||||
get_pretty_stats, stats_to_s3, stats_to_algo_folder
|
||||
from catalyst.finance.execution import MarketOrder
|
||||
from catalyst.finance.performance import PerformanceTracker
|
||||
from catalyst.finance.performance.period import calc_period_stats
|
||||
from catalyst.gens.tradesimulation import AlgorithmSimulator
|
||||
from catalyst.marketplace.marketplace import Marketplace
|
||||
from catalyst.utils.api_support import api_method
|
||||
from catalyst.utils.input_validation import error_keywords, ensure_upper_case
|
||||
from catalyst.utils.math_utils import round_nearest
|
||||
@@ -67,8 +70,8 @@ class ExchangeTradingAlgorithmBase(TradingAlgorithm):
|
||||
|
||||
self.current_day = None
|
||||
|
||||
if self.simulate_orders is None \
|
||||
and self.sim_params.arena == 'backtest':
|
||||
if self.simulate_orders is None and \
|
||||
self.sim_params.arena == 'backtest':
|
||||
self.simulate_orders = True
|
||||
|
||||
# Operations with retry features
|
||||
@@ -93,6 +96,8 @@ class ExchangeTradingAlgorithmBase(TradingAlgorithm):
|
||||
attempts=self.attempts,
|
||||
)
|
||||
|
||||
self._marketplace = None
|
||||
|
||||
@staticmethod
|
||||
def __convert_order_params_for_blotter(limit_price, stop_price, style):
|
||||
"""
|
||||
@@ -130,7 +135,7 @@ class ExchangeTradingAlgorithmBase(TradingAlgorithm):
|
||||
|
||||
@api_method
|
||||
def set_commission(self, maker=None, taker=None):
|
||||
key = self.blotter.commission_models.keys()[0]
|
||||
key = list(self.blotter.commission_models.keys())[0]
|
||||
if maker is not None:
|
||||
self.blotter.commission_models[key].maker = maker
|
||||
|
||||
@@ -139,7 +144,7 @@ class ExchangeTradingAlgorithmBase(TradingAlgorithm):
|
||||
|
||||
@api_method
|
||||
def set_slippage(self, spread=None):
|
||||
key = self.blotter.slippage_models.keys()[0]
|
||||
key = list(self.blotter.slippage_models.keys())[0]
|
||||
if spread is not None:
|
||||
self.blotter.slippage_models[key].spread = spread
|
||||
|
||||
@@ -159,6 +164,25 @@ class ExchangeTradingAlgorithmBase(TradingAlgorithm):
|
||||
style)
|
||||
return amount, style
|
||||
|
||||
def _calculate_order_target_amount(self, asset, target):
|
||||
"""
|
||||
removes order amounts so we won't run into issues
|
||||
when two orders are placed one after the other.
|
||||
it then proceeds to removing positions amount at TradingAlgorithm
|
||||
:param asset:
|
||||
:param target:
|
||||
:return: target
|
||||
"""
|
||||
if asset in self.blotter.open_orders:
|
||||
for open_order in self.blotter.open_orders[asset]:
|
||||
current_amount = open_order.amount
|
||||
target -= current_amount
|
||||
|
||||
target = super(ExchangeTradingAlgorithmBase, self). \
|
||||
_calculate_order_target_amount(asset, target)
|
||||
|
||||
return target
|
||||
|
||||
def round_order(self, amount, asset):
|
||||
"""
|
||||
We need fractions with cryptocurrencies
|
||||
@@ -168,6 +192,15 @@ class ExchangeTradingAlgorithmBase(TradingAlgorithm):
|
||||
"""
|
||||
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
|
||||
@preprocess(symbol_str=ensure_upper_case)
|
||||
def symbol(self, symbol_str, exchange_name=None):
|
||||
@@ -271,9 +304,9 @@ class ExchangeTradingAlgorithmBase(TradingAlgorithm):
|
||||
# Merging latest recorded variables
|
||||
stats.update(self.recorded_vars)
|
||||
|
||||
stats['positions'] = cum.position_tracker.get_positions_list()
|
||||
|
||||
period = tracker.todays_performance
|
||||
stats['positions'] = period.position_tracker.get_positions_list()
|
||||
|
||||
# we want the key to be absent, not just empty
|
||||
# Only include transactions for given dt
|
||||
stats['transactions'] = []
|
||||
@@ -304,6 +337,7 @@ class ExchangeTradingAlgorithmBacktest(ExchangeTradingAlgorithmBase):
|
||||
super(ExchangeTradingAlgorithmBacktest, self).__init__(*args, **kwargs)
|
||||
|
||||
self.frame_stats = list()
|
||||
self.state = {}
|
||||
log.info('initialized trading algorithm in backtest mode')
|
||||
|
||||
def is_last_frame_of_day(self, data):
|
||||
@@ -351,23 +385,40 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
self.live_graph = kwargs.pop('live_graph', None)
|
||||
self.stats_output = kwargs.pop('stats_output', None)
|
||||
self._analyze_live = kwargs.pop('analyze_live', None)
|
||||
self.end = kwargs.pop('end', None)
|
||||
|
||||
self._clock = None
|
||||
self.frame_stats = list()
|
||||
|
||||
self.pnl_stats = get_algo_df(self.algo_namespace, 'pnl_stats')
|
||||
# erase the frame_stats folder to avoid overloading the disk
|
||||
error = clear_frame_stats_directory(self.algo_namespace)
|
||||
if error:
|
||||
log.warning(error)
|
||||
|
||||
self.custom_signals_stats = \
|
||||
get_algo_df(self.algo_namespace, 'custom_signals_stats')
|
||||
# in order to save paper & live files separately
|
||||
self.mode_name = 'paper' if kwargs['simulate_orders'] else 'live'
|
||||
|
||||
self.exposure_stats = \
|
||||
get_algo_df(self.algo_namespace, 'exposure_stats')
|
||||
self.pnl_stats = get_algo_df(
|
||||
self.algo_namespace,
|
||||
'pnl_stats_{}'.format(self.mode_name),
|
||||
)
|
||||
|
||||
self.custom_signals_stats = get_algo_df(
|
||||
self.algo_namespace,
|
||||
'custom_signals_stats_{}'.format(self.mode_name)
|
||||
)
|
||||
|
||||
self.exposure_stats = get_algo_df(
|
||||
self.algo_namespace,
|
||||
'exposure_stats_{}'.format(self.mode_name)
|
||||
)
|
||||
|
||||
self.is_running = True
|
||||
|
||||
self.stats_minutes = 1
|
||||
|
||||
self._last_orders = []
|
||||
self._last_open_orders = []
|
||||
self.trading_client = None
|
||||
|
||||
super(ExchangeTradingAlgorithmLive, self).__init__(*args, **kwargs)
|
||||
@@ -378,9 +429,20 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
log.warn("Can't initialize signal handler inside another thread."
|
||||
"Exit should be handled by the user.")
|
||||
|
||||
log.info('initialized trading algorithm in live mode')
|
||||
|
||||
def interrupt_algorithm(self):
|
||||
"""
|
||||
|
||||
when algorithm comes to an end this function is called.
|
||||
extracts the stats and calls analyze.
|
||||
after finishing, it exits the run.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
"""
|
||||
self.is_running = False
|
||||
|
||||
if self._analyze is None:
|
||||
@@ -390,17 +452,31 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
log.info('Exiting the algorithm. Calling `analyze()` '
|
||||
'before exiting the algorithm.')
|
||||
|
||||
# add the last day stats which is not saved in the directory
|
||||
current_stats = pd.DataFrame(self.frame_stats)
|
||||
current_stats.set_index('period_close', drop=False, inplace=True)
|
||||
|
||||
# get the location of the directory
|
||||
algo_folder = get_algo_folder(self.algo_namespace)
|
||||
folder = join(algo_folder, 'daily_perf')
|
||||
files = [f for f in listdir(folder) if isfile(join(folder, f))]
|
||||
folder = join(algo_folder, 'frame_stats')
|
||||
|
||||
daily_perf_list = []
|
||||
for item in files:
|
||||
filename = join(folder, item)
|
||||
with open(filename, 'rb') as handle:
|
||||
daily_perf_list.append(pickle.load(handle))
|
||||
if exists(folder):
|
||||
files = [f for f in listdir(folder) if isfile(join(folder, f))]
|
||||
|
||||
stats = pd.DataFrame(daily_perf_list)
|
||||
period_stats_list = []
|
||||
for item in files:
|
||||
filename = join(folder, item)
|
||||
|
||||
with open(filename, 'rb') as handle:
|
||||
perf_period = pickle.load(handle)
|
||||
period_stats_list.extend(perf_period)
|
||||
|
||||
stats = pd.DataFrame(period_stats_list)
|
||||
stats.set_index('period_close', drop=False, inplace=True)
|
||||
|
||||
stats = pd.concat([stats, current_stats])
|
||||
else:
|
||||
stats = current_stats
|
||||
|
||||
self.analyze(stats)
|
||||
|
||||
@@ -460,43 +536,69 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
|
||||
return self._clock
|
||||
|
||||
def get_generator(self):
|
||||
if self.trading_client is not None:
|
||||
return self.trading_client.transform()
|
||||
def _init_trading_client(self):
|
||||
"""
|
||||
This replaces Ziplines `_create_generator` method. The main difference
|
||||
is that we are restoring performance tracker objects if available.
|
||||
This allows us to stop/start algos without loosing their state.
|
||||
|
||||
"""
|
||||
self.state = get_algo_object(
|
||||
algo_name=self.algo_namespace,
|
||||
key='context.state_{}'.format(self.mode_name),
|
||||
)
|
||||
if self.state is None:
|
||||
self.state = {}
|
||||
|
||||
perf = None
|
||||
if self.perf_tracker is None:
|
||||
# Note from the Zipline dev:
|
||||
# HACK: When running with the `run` method, we set perf_tracker to
|
||||
# None so that it will be overwritten here.
|
||||
tracker = self.perf_tracker = PerformanceTracker(
|
||||
sim_params=self.sim_params,
|
||||
trading_calendar=self.trading_calendar,
|
||||
env=self.trading_environment,
|
||||
)
|
||||
|
||||
# Set the dt initially to the period start by forcing it to change.
|
||||
self.on_dt_changed(self.sim_params.start_session)
|
||||
|
||||
new_position_tracker = tracker.position_tracker
|
||||
tracker.position_tracker = None
|
||||
|
||||
# Unpacking the perf_tracker and positions if available
|
||||
perf = get_algo_object(
|
||||
cum_perf = get_algo_object(
|
||||
algo_name=self.algo_namespace,
|
||||
key='cumulative_performance',
|
||||
key='cumulative_performance_{}'.format(self.mode_name),
|
||||
)
|
||||
if cum_perf is not None:
|
||||
tracker.cumulative_performance = cum_perf
|
||||
# Ensure single common position tracker
|
||||
tracker.position_tracker = cum_perf.position_tracker
|
||||
|
||||
today = pd.Timestamp.utcnow().floor('1D')
|
||||
todays_perf = get_algo_object(
|
||||
algo_name=self.algo_namespace,
|
||||
key=today.strftime('%Y-%m-%d'),
|
||||
rel_path='daily_performance_{}'.format(self.mode_name),
|
||||
)
|
||||
if todays_perf is not None:
|
||||
# Ensure single common position tracker
|
||||
if tracker.position_tracker is not None:
|
||||
todays_perf.position_tracker = tracker.position_tracker
|
||||
else:
|
||||
tracker.position_tracker = todays_perf.position_tracker
|
||||
|
||||
tracker.todays_performance = todays_perf
|
||||
|
||||
if tracker.position_tracker is None:
|
||||
# Use a new position_tracker if not is found in the state
|
||||
tracker.position_tracker = new_position_tracker
|
||||
|
||||
if not self.initialized:
|
||||
# Calls the initialize function of the algorithm
|
||||
self.initialize(*self.initialize_args, **self.initialize_kwargs)
|
||||
self.initialized = True
|
||||
|
||||
# Call the simulation trading algorithm for side-effects:
|
||||
# it creates the perf tracker
|
||||
# TradingAlgorithm._create_generator(self, self.sim_params)
|
||||
if perf is not None:
|
||||
tracker.cumulative_performance = perf
|
||||
|
||||
period = self.perf_tracker.todays_performance
|
||||
period.starting_cash = perf.ending_cash
|
||||
period.starting_exposure = perf.ending_exposure
|
||||
period.starting_value = perf.ending_value
|
||||
period.position_tracker = perf.position_tracker
|
||||
|
||||
self.trading_client = ExchangeAlgorithmExecutor(
|
||||
algo=self,
|
||||
sim_params=self.sim_params,
|
||||
@@ -506,6 +608,11 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
restrictions=self.restrictions,
|
||||
universe_func=self._calculate_universe,
|
||||
)
|
||||
|
||||
def get_generator(self):
|
||||
if self.trading_client is None:
|
||||
self._init_trading_client()
|
||||
|
||||
return self.trading_client.transform()
|
||||
|
||||
def updated_portfolio(self):
|
||||
@@ -523,10 +630,6 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
positions, returning the available cash, and raising error
|
||||
if the data goes out of sync.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
attempt_index: int
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
@@ -559,10 +662,18 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
if base_currency is None:
|
||||
base_currency = exchange.base_currency
|
||||
|
||||
orders = []
|
||||
for asset in self.blotter.open_orders:
|
||||
asset_orders = self.blotter.open_orders[asset]
|
||||
if asset_orders:
|
||||
orders += asset_orders
|
||||
|
||||
required_cash = self.portfolio.cash if not orders else None
|
||||
cash, positions_value = exchange.sync_positions(
|
||||
positions=exchange_positions,
|
||||
open_orders=orders,
|
||||
check_balances=check_balances,
|
||||
cash=self.portfolio.cash,
|
||||
cash=required_cash,
|
||||
)
|
||||
total_cash += cash
|
||||
total_positions_value += positions_value
|
||||
@@ -606,7 +717,11 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
)
|
||||
self.pnl_stats = pd.concat([self.pnl_stats, df])
|
||||
|
||||
save_algo_df(self.algo_namespace, 'pnl_stats', self.pnl_stats)
|
||||
save_algo_df(
|
||||
self.algo_namespace,
|
||||
'pnl_stats_{}'.format(self.mode_name),
|
||||
self.pnl_stats,
|
||||
)
|
||||
|
||||
def add_custom_signals_stats(self, period_stats):
|
||||
"""
|
||||
@@ -627,8 +742,11 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
)
|
||||
self.custom_signals_stats = pd.concat([self.custom_signals_stats, df])
|
||||
|
||||
save_algo_df(self.algo_namespace, 'custom_signals_stats',
|
||||
self.custom_signals_stats)
|
||||
save_algo_df(
|
||||
self.algo_namespace,
|
||||
'custom_signals_stats_{}'.format(self.mode_name),
|
||||
self.custom_signals_stats,
|
||||
)
|
||||
|
||||
def add_exposure_stats(self, period_stats):
|
||||
"""
|
||||
@@ -655,9 +773,43 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
self.exposure_stats = pd.concat([self.exposure_stats, df])
|
||||
|
||||
save_algo_df(
|
||||
self.algo_namespace, 'exposure_stats', self.exposure_stats
|
||||
self.algo_namespace,
|
||||
'exposure_stats_{}'.format(self.mode_name),
|
||||
self.exposure_stats
|
||||
)
|
||||
|
||||
def nullify_frame_stats(self, now):
|
||||
"""
|
||||
|
||||
Save all period_stats to local directory
|
||||
erase old files from the folder and nullify
|
||||
self.frame_stats
|
||||
|
||||
Parameters
|
||||
----------
|
||||
now: Timestamp
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
"""
|
||||
save_algo_object(
|
||||
algo_name=self.algo_namespace,
|
||||
key=now.floor('1D').strftime('%Y-%m-%d'),
|
||||
obj=self.frame_stats,
|
||||
rel_path='frame_stats'
|
||||
)
|
||||
|
||||
error = remove_old_files(
|
||||
algo_name=self.algo_namespace,
|
||||
today=now,
|
||||
rel_path='frame_stats'
|
||||
)
|
||||
if error:
|
||||
log.warning(error)
|
||||
|
||||
self.frame_stats = list()
|
||||
|
||||
def handle_data(self, data):
|
||||
"""
|
||||
Wrapper around the handle_data method of each algo.
|
||||
@@ -670,17 +822,27 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
if not self.is_running:
|
||||
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
|
||||
today = data.current_dt.floor('1D')
|
||||
if self.current_day is not None and today > self.current_day:
|
||||
self.frame_stats = list()
|
||||
self.nullify_frame_stats(now=data.current_dt)
|
||||
|
||||
self.performance_needs_update = False
|
||||
new_orders = self.perf_tracker.todays_performance.orders_by_id.keys()
|
||||
if new_orders != self._last_orders:
|
||||
last_orders_list = list(self.blotter.orders.keys())
|
||||
open_orders_list = list(self.blotter.open_orders.keys())
|
||||
|
||||
if last_orders_list != self._last_orders or \
|
||||
open_orders_list != self._last_open_orders:
|
||||
self.performance_needs_update = True
|
||||
|
||||
self._last_orders = copy.deepcopy(new_orders)
|
||||
# Saving current order positions
|
||||
# to detect changes in the next frame
|
||||
self._last_orders = copy.deepcopy(last_orders_list)
|
||||
self._last_open_orders = copy.deepcopy(open_orders_list)
|
||||
|
||||
if self.performance_needs_update:
|
||||
self.perf_tracker.update_performance()
|
||||
@@ -697,7 +859,7 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
self.portfolio_needs_update = False
|
||||
|
||||
log.info(
|
||||
'got totals from exchanges, cash: {} positions: {}'.format(
|
||||
'portfolio balances, cash: {}, positions: {}'.format(
|
||||
cash, positions_value
|
||||
)
|
||||
)
|
||||
@@ -709,18 +871,34 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
# every bar no matter if the algorithm places an order or not.
|
||||
self.validate_account_controls()
|
||||
|
||||
self._save_algo_state(data)
|
||||
self.current_day = data.current_dt.floor('1D')
|
||||
|
||||
def _save_algo_state(self, data):
|
||||
today = data.current_dt.floor('1D')
|
||||
try:
|
||||
self._save_stats_csv(self._process_stats(data))
|
||||
except Exception as e:
|
||||
log.warn('unable to calculate performance: {}'.format(e))
|
||||
|
||||
log.debug('saving cumulative performance object')
|
||||
save_algo_object(
|
||||
algo_name=self.algo_namespace,
|
||||
key='cumulative_performance',
|
||||
key='cumulative_performance_{}'.format(self.mode_name),
|
||||
obj=self.perf_tracker.cumulative_performance,
|
||||
)
|
||||
|
||||
self.current_day = data.current_dt.floor('1D')
|
||||
log.debug('saving todays performance object')
|
||||
save_algo_object(
|
||||
algo_name=self.algo_namespace,
|
||||
key=today.strftime('%Y-%m-%d'),
|
||||
obj=self.perf_tracker.todays_performance,
|
||||
rel_path='daily_performance_{}'.format(self.mode_name)
|
||||
)
|
||||
log.debug('saving context.state object')
|
||||
save_algo_object(
|
||||
algo_name=self.algo_namespace,
|
||||
key='context.state_{}'.format(self.mode_name),
|
||||
obj=self.state)
|
||||
|
||||
def _process_stats(self, data):
|
||||
today = data.current_dt.floor('1D')
|
||||
@@ -736,6 +914,8 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
# Saving the last hour in memory
|
||||
self.frame_stats.append(frame_stats)
|
||||
|
||||
# creating and saving the pnl_stats into the local
|
||||
# directory
|
||||
self.add_pnl_stats(frame_stats)
|
||||
if self.recorded_vars:
|
||||
self.add_custom_signals_stats(frame_stats)
|
||||
@@ -763,12 +943,6 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
start_dt=today,
|
||||
end_dt=data.current_dt
|
||||
)
|
||||
save_algo_object(
|
||||
algo_name=self.algo_namespace,
|
||||
key=today.strftime('%Y-%m-%d'),
|
||||
obj=daily_stats,
|
||||
rel_path='daily_perf'
|
||||
)
|
||||
|
||||
return recorded_cols
|
||||
|
||||
@@ -779,6 +953,7 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
csv_bytes = stats_to_algo_folder(
|
||||
stats=self.frame_stats,
|
||||
algo_namespace=self.algo_namespace,
|
||||
folder_name='stats_{}'.format(self.mode_name),
|
||||
recorded_cols=recorded_cols,
|
||||
)
|
||||
except Exception as e:
|
||||
@@ -806,6 +981,13 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
raise NotImplementedError()
|
||||
|
||||
def _get_open_orders(self, asset=None):
|
||||
if self.simulate_orders:
|
||||
raise ValueError(
|
||||
'The get_open_orders() method only works in live mode. '
|
||||
'The purpose is to list open orders on the exchange '
|
||||
'regardless who placed them. To list the open orders of '
|
||||
'this algo, use `context.blotter.open_orders`.'
|
||||
)
|
||||
if asset:
|
||||
exchange = self.exchanges[asset.exchange]
|
||||
return exchange.get_open_orders(asset)
|
||||
@@ -839,13 +1021,15 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
If an asset is passed then this will return a list of the open
|
||||
orders for this asset.
|
||||
"""
|
||||
# TODO: should this be a shortcut to the open orders in the blotter?
|
||||
return retry(
|
||||
action=self._get_open_orders,
|
||||
attempts=self.attempts['get_open_orders_attempts'],
|
||||
sleeptime=self.attempts['retry_sleeptime'],
|
||||
retry_exceptions=(ExchangeRequestError,),
|
||||
cleanup=lambda: log.warn('Fetching open orders again.'),
|
||||
args=(asset,))
|
||||
args=(asset,)
|
||||
)
|
||||
|
||||
@api_method
|
||||
def get_order(self, order_id, exchange_name):
|
||||
@@ -874,13 +1058,19 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
args=(order_id,))
|
||||
|
||||
@api_method
|
||||
def cancel_order(self, order_param, exchange_name):
|
||||
def cancel_order(self, order_param, exchange_name,
|
||||
symbol=None, params={}):
|
||||
"""Cancel an open order.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
order_param : str or Order
|
||||
The order_id or order object to cancel.
|
||||
|
||||
exchange_name: name of exchange from
|
||||
which you want to cancel the order
|
||||
symbol:
|
||||
params:
|
||||
"""
|
||||
exchange = self.exchanges[exchange_name]
|
||||
|
||||
@@ -894,4 +1084,4 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
||||
sleeptime=self.attempts['retry_sleeptime'],
|
||||
retry_exceptions=(ExchangeRequestError,),
|
||||
cleanup=lambda: log.warn('cancelling order again.'),
|
||||
args=(order_id,))
|
||||
args=(order_id, symbol, params))
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import pandas as pd
|
||||
from logbook import Logger
|
||||
|
||||
from catalyst.constants import LOG_LEVEL
|
||||
from catalyst.exchange.utils.factory import find_exchanges
|
||||
from logbook import Logger
|
||||
|
||||
log = Logger('ExchangeAssetFinder', level=LOG_LEVEL)
|
||||
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from catalyst.assets._assets import TradingPair
|
||||
from logbook import Logger
|
||||
from redo import retry
|
||||
|
||||
from catalyst.assets._assets import TradingPair
|
||||
from catalyst.constants import LOG_LEVEL
|
||||
from catalyst.exchange.exchange_errors import ExchangeRequestError
|
||||
from catalyst.finance.blotter import Blotter
|
||||
@@ -42,6 +43,11 @@ class TradingPairFeeSchedule(CommissionModel):
|
||||
)
|
||||
)
|
||||
|
||||
def get_maker_taker(self, asset):
|
||||
maker = self.maker if self.maker is not None else asset.maker
|
||||
taker = self.taker if self.taker is not None else asset.taker
|
||||
return maker, taker
|
||||
|
||||
def calculate(self, order, transaction):
|
||||
"""
|
||||
Calculate the final fee based on the order parameters.
|
||||
@@ -55,15 +61,14 @@ class TradingPairFeeSchedule(CommissionModel):
|
||||
cost = abs(transaction.amount) * transaction.price
|
||||
|
||||
asset = order.asset
|
||||
maker = self.maker if self.maker is not None else asset.maker
|
||||
taker = self.taker if self.taker is not None else asset.taker
|
||||
maker, taker = self.get_maker_taker(asset)
|
||||
|
||||
multiplier = taker
|
||||
if order.limit is not None:
|
||||
multiplier = maker \
|
||||
if ((order.amount > 0 and order.limit < transaction.price)
|
||||
or (order.amount < 0 and order.limit > transaction.price)) \
|
||||
and order.limit_reached else taker
|
||||
and order.limit_reached else taker
|
||||
|
||||
fee = cost * multiplier
|
||||
return fee
|
||||
@@ -90,7 +95,6 @@ class TradingPairFixedSlippage(SlippageModel):
|
||||
|
||||
def simulate(self, data, asset, orders_for_asset):
|
||||
self._volume_for_bar = 0
|
||||
|
||||
price = data.current(asset, 'close')
|
||||
|
||||
dt = data.current_dt
|
||||
@@ -100,18 +104,20 @@ class TradingPairFixedSlippage(SlippageModel):
|
||||
|
||||
order.check_triggers(price, dt)
|
||||
if not order.triggered:
|
||||
log.debug('order has not reached the trigger at current '
|
||||
'price {}'.format(price))
|
||||
log.info(
|
||||
'order has not reached the trigger at current '
|
||||
'price {}'.format(price)
|
||||
)
|
||||
continue
|
||||
|
||||
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(
|
||||
order, dt, execution_price, execution_volume
|
||||
)
|
||||
|
||||
self._volume_for_bar += abs(transaction.amount)
|
||||
yield order, transaction
|
||||
self._volume_for_bar += abs(transaction.amount)
|
||||
yield order, transaction
|
||||
|
||||
def process_order(self, data, order):
|
||||
price = data.current(order.asset, 'close')
|
||||
@@ -202,34 +208,29 @@ class ExchangeBlotter(Blotter):
|
||||
for order in self.open_orders[asset]:
|
||||
log.debug('found open order: {}'.format(order.id))
|
||||
|
||||
new_order, executed_price = exchange.get_order(order.id, asset)
|
||||
log.debug(
|
||||
'got updated order {} {}'.format(
|
||||
new_order, executed_price
|
||||
transactions = exchange.process_order(order)
|
||||
# This is a temporary measure, we should really update all
|
||||
# trades, not just when the order gets filled. I just think
|
||||
# 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],
|
||||
)
|
||||
)
|
||||
order.status = new_order.status
|
||||
|
||||
if order.status == ORDER_STATUS.FILLED:
|
||||
order.commission = new_order.commission
|
||||
if order.amount != new_order.amount:
|
||||
log.warn(
|
||||
'executed order amount {} differs '
|
||||
'from original'.format(
|
||||
new_order.amount, order.amount
|
||||
)
|
||||
ostatus = 'filled' if order.open_amount == 0 else 'partial'
|
||||
log.info(
|
||||
'{} order {} / {}: {}, avg price: {}'.format(
|
||||
ostatus,
|
||||
order.id,
|
||||
asset.symbol,
|
||||
order.filled,
|
||||
avg_price,
|
||||
)
|
||||
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:
|
||||
yield order, None
|
||||
@@ -237,9 +238,12 @@ class ExchangeBlotter(Blotter):
|
||||
else:
|
||||
delta = pd.Timestamp.utcnow() - order.dt
|
||||
log.info(
|
||||
'order {order_id} still open after {delta}'.format(
|
||||
'{exchange} order {order_id} for {symbol} still open '
|
||||
'after {delta}'.format(
|
||||
exchange=exchange.name,
|
||||
order_id=order.id,
|
||||
delta=delta
|
||||
delta=delta,
|
||||
symbol=order.asset.symbol,
|
||||
)
|
||||
)
|
||||
|
||||
@@ -250,7 +254,6 @@ class ExchangeBlotter(Blotter):
|
||||
|
||||
for order, txn in self.check_open_orders():
|
||||
order.dt = txn.dt
|
||||
|
||||
transactions.append(txn)
|
||||
|
||||
if not order.open:
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import copy
|
||||
import os
|
||||
import shutil
|
||||
from datetime import timedelta
|
||||
@@ -25,11 +26,11 @@ from catalyst.exchange.exchange_errors import EmptyValuesInBundleError, \
|
||||
NoDataAvailableOnExchange, \
|
||||
PricingDataNotLoadedError, DataCorruptionError, PricingDataValueError
|
||||
from catalyst.exchange.utils.bundle_utils import range_in_bundle, \
|
||||
get_bcolz_chunk, get_month_start_end, \
|
||||
get_year_start_end, get_df_from_arrays, get_start_dt, get_period_label, \
|
||||
get_delta, get_assets
|
||||
from catalyst.exchange.utils.exchange_utils import get_exchange_folder, \
|
||||
save_exchange_symbols, mixin_market_params, get_catalyst_symbol
|
||||
get_bcolz_chunk, get_df_from_arrays, get_assets
|
||||
from catalyst.exchange.utils.datetime_utils import get_start_dt, \
|
||||
get_period_label, get_month_start_end, get_year_start_end, get_period, \
|
||||
timestr_to_dt
|
||||
from catalyst.exchange.utils.exchange_utils import get_exchange_folder
|
||||
from catalyst.utils.cli import maybe_show_progress
|
||||
from catalyst.utils.paths import ensure_directory
|
||||
|
||||
@@ -233,12 +234,12 @@ class ExchangeBundle:
|
||||
|
||||
problem = '{name} ({start_dt} to {end_dt}) has empty ' \
|
||||
'periods: {dates}'.format(
|
||||
name=asset.symbol,
|
||||
start_dt=asset.start_date.strftime(
|
||||
DATE_TIME_FORMAT),
|
||||
end_dt=end_dt.strftime(DATE_TIME_FORMAT),
|
||||
dates=[date.strftime(
|
||||
DATE_TIME_FORMAT) for date in dates])
|
||||
name=asset.symbol,
|
||||
start_dt=asset.start_date.strftime(
|
||||
DATE_TIME_FORMAT),
|
||||
end_dt=end_dt.strftime(DATE_TIME_FORMAT),
|
||||
dates=[date.strftime(
|
||||
DATE_TIME_FORMAT) for date in dates])
|
||||
|
||||
if empty_rows_behavior == 'warn':
|
||||
log.warn(problem)
|
||||
@@ -287,12 +288,12 @@ class ExchangeBundle:
|
||||
|
||||
problem = '{name} ({start_dt} to {end_dt}) has {threshold} ' \
|
||||
'identical close values on: {dates}'.format(
|
||||
name=asset.symbol,
|
||||
start_dt=asset.start_date.strftime(DATE_TIME_FORMAT),
|
||||
end_dt=end_dt.strftime(DATE_TIME_FORMAT),
|
||||
threshold=threshold,
|
||||
dates=[pd.to_datetime(date).strftime(DATE_TIME_FORMAT)
|
||||
for date in dates])
|
||||
name=asset.symbol,
|
||||
start_dt=asset.start_date.strftime(DATE_TIME_FORMAT),
|
||||
end_dt=end_dt.strftime(DATE_TIME_FORMAT),
|
||||
threshold=threshold,
|
||||
dates=[pd.to_datetime(date).strftime(DATE_TIME_FORMAT)
|
||||
for date in dates])
|
||||
|
||||
problems.append(problem)
|
||||
|
||||
@@ -459,7 +460,7 @@ class ExchangeBundle:
|
||||
last_entry = None
|
||||
|
||||
if start is None or \
|
||||
(earliest_trade is not None and earliest_trade > start):
|
||||
(earliest_trade is not None and earliest_trade > start):
|
||||
start = earliest_trade
|
||||
|
||||
if last_entry is not None and (end is None or end > last_entry):
|
||||
@@ -513,8 +514,8 @@ class ExchangeBundle:
|
||||
continue
|
||||
|
||||
dates = pd.date_range(
|
||||
start=get_period_label(adj_start, data_frequency),
|
||||
end=get_period_label(adj_end, data_frequency),
|
||||
start=get_period(adj_start, data_frequency),
|
||||
end=get_period(adj_end, data_frequency),
|
||||
freq='MS' if data_frequency == 'minute' else 'AS',
|
||||
tz=UTC
|
||||
)
|
||||
@@ -553,7 +554,9 @@ class ExchangeBundle:
|
||||
|
||||
# We sort the chunks by end date to ingest most recent data first
|
||||
chunks[asset].sort(
|
||||
key=lambda chunk: pd.to_datetime(chunk['period'])
|
||||
key=lambda chunk: timestr_to_dt(
|
||||
chunk['period'], data_frequency
|
||||
)
|
||||
)
|
||||
|
||||
return chunks
|
||||
@@ -601,14 +604,15 @@ class ExchangeBundle:
|
||||
if show_breakdown:
|
||||
for asset in chunks:
|
||||
with maybe_show_progress(
|
||||
chunks[asset],
|
||||
show_progress,
|
||||
label='Ingesting {frequency} price data for '
|
||||
'{symbol} on {exchange}'.format(
|
||||
exchange=self.exchange_name,
|
||||
frequency=data_frequency,
|
||||
symbol=asset.symbol
|
||||
)) as it:
|
||||
chunks[asset],
|
||||
show_progress,
|
||||
label='Ingesting {frequency} price data for '
|
||||
'{symbol} on {exchange}'.format(
|
||||
exchange=self.exchange_name,
|
||||
frequency=data_frequency,
|
||||
symbol=asset.symbol
|
||||
)
|
||||
) as it:
|
||||
for chunk in it:
|
||||
problems += self.ingest_ctable(
|
||||
asset=chunk['asset'],
|
||||
@@ -623,16 +627,19 @@ class ExchangeBundle:
|
||||
|
||||
# We sort the chunks by end date to ingest most recent data first
|
||||
all_chunks.sort(
|
||||
key=lambda chunk: pd.to_datetime(chunk['period'])
|
||||
key=lambda chunk: timestr_to_dt(
|
||||
chunk['period'], data_frequency
|
||||
)
|
||||
)
|
||||
with maybe_show_progress(
|
||||
all_chunks,
|
||||
show_progress,
|
||||
label='Ingesting {frequency} price data on '
|
||||
'{exchange}'.format(
|
||||
exchange=self.exchange_name,
|
||||
frequency=data_frequency,
|
||||
)) as it:
|
||||
all_chunks,
|
||||
show_progress,
|
||||
label='Ingesting {frequency} price data on '
|
||||
'{exchange}'.format(
|
||||
exchange=self.exchange_name,
|
||||
frequency=data_frequency,
|
||||
)
|
||||
) as it:
|
||||
for chunk in it:
|
||||
problems += self.ingest_ctable(
|
||||
asset=chunk['asset'],
|
||||
@@ -701,42 +708,36 @@ class ExchangeBundle:
|
||||
for symbol in symbols:
|
||||
start_dt = df.index.get_level_values(1).min()
|
||||
end_dt = df.index.get_level_values(1).max()
|
||||
end_dt_key = 'end_{}'.format(data_frequency)
|
||||
|
||||
market = self.exchange.get_market(symbol)
|
||||
if market is None:
|
||||
raise ValueError('symbol not available in the exchange.')
|
||||
try:
|
||||
asset = self.exchange.get_asset(symbol, is_local=True)
|
||||
except:
|
||||
asset = copy.deepcopy(self.exchange.get_asset(symbol))
|
||||
|
||||
params = dict(
|
||||
exchange=self.exchange.name,
|
||||
data_source='local',
|
||||
exchange_symbol=market['id'],
|
||||
)
|
||||
mixin_market_params(self.exchange_name, params, market)
|
||||
if asset.data_source == 'local':
|
||||
asset.start_date = asset.start_date \
|
||||
if asset.start_date < start_dt else start_dt
|
||||
|
||||
asset_def = self.exchange.get_asset_def(market, True)
|
||||
if asset_def is not None:
|
||||
params['symbol'] = asset_def['symbol']
|
||||
if data_frequency == 'daily':
|
||||
asset.end_date = asset.end_daily = asset.end_daily \
|
||||
if asset.end_daily > end_dt else end_dt
|
||||
|
||||
params['start_date'] = asset_def['start_date'] \
|
||||
if asset_def['start_date'] < start_dt else start_dt
|
||||
|
||||
params['end_date'] = asset_def[end_dt_key] \
|
||||
if asset_def[end_dt_key] > end_dt else end_dt
|
||||
|
||||
params['end_daily'] = end_dt \
|
||||
if data_frequency == 'daily' else asset_def['end_daily']
|
||||
|
||||
params['end_minute'] = end_dt \
|
||||
if data_frequency == 'minute' else asset_def['end_minute']
|
||||
else:
|
||||
asset.end_date = asset.end_minute = asset.end_minute \
|
||||
if asset.end_minute > end_dt else end_dt
|
||||
|
||||
else:
|
||||
params['symbol'] = get_catalyst_symbol(market)
|
||||
asset.data_source = 'local'
|
||||
asset.start_date = start_dt
|
||||
asset.end_dt = end_dt
|
||||
|
||||
params['end_daily'] = end_dt \
|
||||
if data_frequency == 'daily' else 'N/A'
|
||||
params['end_minute'] = end_dt \
|
||||
if data_frequency == 'minute' else 'N/A'
|
||||
if data_frequency == 'daily':
|
||||
asset.end_daily = end_dt
|
||||
asset.end_minute = None
|
||||
|
||||
else:
|
||||
asset.end_daily = None
|
||||
asset.end_minute = end_dt
|
||||
|
||||
if min_start_dt is None or start_dt < min_start_dt:
|
||||
min_start_dt = start_dt
|
||||
@@ -744,11 +745,9 @@ class ExchangeBundle:
|
||||
if max_end_dt is None or end_dt > max_end_dt:
|
||||
max_end_dt = end_dt
|
||||
|
||||
asset = TradingPair(**params)
|
||||
assets[market['id']] = asset
|
||||
|
||||
save_exchange_symbols(self.exchange_name, assets, True)
|
||||
assets[symbol] = asset
|
||||
|
||||
# TODO: update config.json
|
||||
writer = self.get_writer(
|
||||
start_dt=min_start_dt.replace(hour=00, minute=00),
|
||||
end_dt=max_end_dt.replace(hour=23, minute=59),
|
||||
@@ -831,7 +830,6 @@ class ExchangeBundle:
|
||||
field,
|
||||
data_frequency,
|
||||
algo_end_dt=None,
|
||||
trailing_bar_count=None,
|
||||
force_auto_ingest=False
|
||||
):
|
||||
"""
|
||||
@@ -859,7 +857,6 @@ class ExchangeBundle:
|
||||
bar_count=bar_count,
|
||||
field=field,
|
||||
data_frequency=data_frequency,
|
||||
trailing_bar_count=trailing_bar_count,
|
||||
)
|
||||
return pd.DataFrame(series)
|
||||
|
||||
@@ -888,7 +885,6 @@ class ExchangeBundle:
|
||||
field=field,
|
||||
data_frequency=data_frequency,
|
||||
reset_reader=True,
|
||||
trailing_bar_count=trailing_bar_count,
|
||||
)
|
||||
return series
|
||||
|
||||
@@ -899,7 +895,6 @@ class ExchangeBundle:
|
||||
bar_count=bar_count,
|
||||
field=field,
|
||||
data_frequency=data_frequency,
|
||||
trailing_bar_count=trailing_bar_count,
|
||||
)
|
||||
return pd.DataFrame(series)
|
||||
|
||||
@@ -963,17 +958,12 @@ class ExchangeBundle:
|
||||
bar_count,
|
||||
field,
|
||||
data_frequency,
|
||||
trailing_bar_count=None,
|
||||
reset_reader=False):
|
||||
start_dt = get_start_dt(end_dt, bar_count, data_frequency, False)
|
||||
start_dt, _ = self.get_adj_dates(
|
||||
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
|
||||
# when auto-ingesting data.
|
||||
# TODO: needs more work
|
||||
|
||||
@@ -3,17 +3,17 @@ import abc
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from catalyst.assets._assets import TradingPair
|
||||
from logbook import Logger
|
||||
from redo import retry
|
||||
|
||||
from catalyst.constants import LOG_LEVEL, AUTO_INGEST
|
||||
from catalyst.data.data_portal import DataPortal
|
||||
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
||||
from catalyst.exchange.exchange_errors import (
|
||||
ExchangeRequestError,
|
||||
PricingDataNotLoadedError)
|
||||
from catalyst.exchange.utils.exchange_utils import get_frequency, \
|
||||
resample_history_df, group_assets_by_exchange
|
||||
from catalyst.exchange.utils.exchange_utils import resample_history_df, \
|
||||
group_assets_by_exchange
|
||||
from catalyst.exchange.utils.datetime_utils import get_frequency, get_start_dt
|
||||
from logbook import Logger
|
||||
from redo import retry
|
||||
|
||||
log = Logger('DataPortalExchange', level=LOG_LEVEL)
|
||||
|
||||
@@ -292,13 +292,13 @@ class DataPortalExchangeBacktest(DataPortalExchangeBase):
|
||||
DataFrame
|
||||
|
||||
"""
|
||||
# TODO: verify that the exchange supports the timeframe
|
||||
bundle = self.exchange_bundles[exchange_name] # type: ExchangeBundle
|
||||
|
||||
freq, candle_size, unit, adj_data_frequency = get_frequency(
|
||||
frequency, data_frequency
|
||||
)
|
||||
adj_bar_count = candle_size * bar_count
|
||||
trailing_bar_count = candle_size - 1
|
||||
|
||||
if data_frequency == 'minute' and adj_data_frequency == 'daily':
|
||||
end_dt = end_dt.floor('1D')
|
||||
@@ -310,10 +310,10 @@ class DataPortalExchangeBacktest(DataPortalExchangeBase):
|
||||
field=field,
|
||||
data_frequency=adj_data_frequency,
|
||||
algo_end_dt=self._last_available_session,
|
||||
trailing_bar_count=trailing_bar_count,
|
||||
)
|
||||
|
||||
df = resample_history_df(pd.DataFrame(series), freq, field)
|
||||
start_dt = get_start_dt(end_dt, adj_bar_count, data_frequency)
|
||||
df = resample_history_df(pd.DataFrame(series), freq, field, start_dt)
|
||||
return df
|
||||
|
||||
def get_exchange_spot_value(self,
|
||||
|
||||
@@ -100,6 +100,13 @@ class InvalidHistoryFrequencyError(ZiplineError):
|
||||
).strip()
|
||||
|
||||
|
||||
class UnsupportedHistoryFrequencyError(ZiplineError):
|
||||
msg = (
|
||||
'{exchange} does not support candle frequency {freq}, please choose '
|
||||
'from: {freqs}.'
|
||||
).strip()
|
||||
|
||||
|
||||
class InvalidHistoryTimeframeError(ZiplineError):
|
||||
msg = (
|
||||
'CCXT timeframe {timeframe} not supported by the exchange.'
|
||||
@@ -315,3 +322,24 @@ class BalanceTooLowError(ZiplineError):
|
||||
'add positions to hold a free amount greater than {amount}, or clean '
|
||||
'the state of this algo and restart.'
|
||||
).strip()
|
||||
|
||||
|
||||
class NoCandlesReceivedFromExchange(ZiplineError):
|
||||
msg = (
|
||||
'Although requesting {bar_count} candles until {end_dt} of asset {asset}, '
|
||||
'an empty list of candles was received for {exchange}.'
|
||||
).strip()
|
||||
|
||||
|
||||
class MarketsNotFoundError(ZiplineError):
|
||||
msg = (
|
||||
'Exchange {exchange} contains no valid market so it is unusable in '
|
||||
'Catalyst.'
|
||||
).strip()
|
||||
|
||||
|
||||
class InvalidMarketError(ZiplineError):
|
||||
msg = (
|
||||
'Exchange {exchange} contains at least one incorrectly structured '
|
||||
'market: {market}, so it is unusable in Catalyst.'
|
||||
).strip()
|
||||
|
||||
@@ -1,8 +1,7 @@
|
||||
import numpy as np
|
||||
from logbook import Logger
|
||||
|
||||
from catalyst.constants import LOG_LEVEL
|
||||
from catalyst.protocol import Portfolio, Positions, Position
|
||||
from logbook import Logger
|
||||
|
||||
log = Logger('ExchangePortfolio', level=LOG_LEVEL)
|
||||
|
||||
|
||||
@@ -11,12 +11,6 @@
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
from logbook import Logger
|
||||
from numpy import (
|
||||
iinfo,
|
||||
uint32,
|
||||
)
|
||||
|
||||
from catalyst.constants import LOG_LEVEL
|
||||
from catalyst.data.us_equity_pricing import BcolzDailyBarReader
|
||||
from catalyst.errors import NoFurtherDataError
|
||||
@@ -26,6 +20,11 @@ from catalyst.pipeline.data import DataSet, Column
|
||||
from catalyst.pipeline.loaders.base import PipelineLoader
|
||||
from catalyst.utils.calendars import get_calendar
|
||||
from catalyst.utils.numpy_utils import float64_dtype
|
||||
from logbook import Logger
|
||||
from numpy import (
|
||||
iinfo,
|
||||
uint32,
|
||||
)
|
||||
|
||||
UINT32_MAX = iinfo(uint32).max
|
||||
|
||||
|
||||
@@ -1,13 +1,12 @@
|
||||
import pandas as pd
|
||||
from catalyst.constants import LOG_LEVEL
|
||||
from catalyst.exchange.utils.stats_utils import prepare_stats
|
||||
from catalyst.gens.sim_engine import (
|
||||
BAR,
|
||||
SESSION_START
|
||||
)
|
||||
from logbook import Logger
|
||||
|
||||
from catalyst.constants import LOG_LEVEL
|
||||
from catalyst.exchange.utils.stats_utils import prepare_stats
|
||||
|
||||
log = Logger('LiveGraphClock', level=LOG_LEVEL)
|
||||
|
||||
|
||||
|
||||
@@ -14,14 +14,13 @@
|
||||
from time import sleep
|
||||
|
||||
import pandas as pd
|
||||
from catalyst.constants import LOG_LEVEL
|
||||
from catalyst.gens.sim_engine import (
|
||||
BAR,
|
||||
SESSION_START
|
||||
)
|
||||
from logbook import Logger
|
||||
|
||||
from catalyst.constants import LOG_LEVEL
|
||||
|
||||
log = Logger('ExchangeClock', level=LOG_LEVEL)
|
||||
|
||||
|
||||
|
||||
@@ -1,11 +1,19 @@
|
||||
import calendar
|
||||
import os
|
||||
import tarfile
|
||||
from datetime import timedelta, datetime, date
|
||||
from datetime import datetime
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from catalyst.constants import BUNDLE_URL
|
||||
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
|
||||
import pytz
|
||||
|
||||
from catalyst.data.bundles.core import download_without_progress
|
||||
from catalyst.exchange.utils.exchange_utils import get_exchange_bundles_folder
|
||||
@@ -14,41 +22,6 @@ EXCHANGE_NAMES = ['bitfinex', 'bittrex', 'poloniex']
|
||||
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):
|
||||
"""
|
||||
Download and extract a bcolz bundle.
|
||||
@@ -76,10 +49,11 @@ def get_bcolz_chunk(exchange_name, symbol, data_frequency, period):
|
||||
path = os.path.join(root, name)
|
||||
|
||||
if not os.path.isdir(path):
|
||||
url = 'https://s3.amazonaws.com/enigmaco/catalyst-bundles/' \
|
||||
'exchange-{exchange}/{name}.tar.gz'.format(
|
||||
exchange=exchange_name,
|
||||
name=name)
|
||||
url = BUNDLE_URL.format(
|
||||
exchange=exchange_name,
|
||||
data_frequency=data_frequency,
|
||||
name=name,
|
||||
)
|
||||
|
||||
bytes = download_without_progress(url)
|
||||
with tarfile.open('r', fileobj=bytes) as tar:
|
||||
@@ -88,178 +62,6 @@ def get_bcolz_chunk(exchange_name, symbol, data_frequency, period):
|
||||
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):
|
||||
"""
|
||||
A DataFrame from the specified OHCLV arrays.
|
||||
@@ -275,14 +77,14 @@ def get_df_from_arrays(arrays, periods):
|
||||
|
||||
"""
|
||||
ohlcv = dict()
|
||||
for index, field in enumerate(
|
||||
['open', 'high', 'low', 'close', 'volume']):
|
||||
for index, field in enumerate(['open', 'high', 'low', 'close', 'volume']):
|
||||
ohlcv[field] = arrays[index].flatten()
|
||||
|
||||
df = pd.DataFrame(
|
||||
data=ohlcv,
|
||||
index=periods
|
||||
)
|
||||
df.index.name = 'last_traded'
|
||||
return df
|
||||
|
||||
|
||||
|
||||
@@ -0,0 +1,307 @@
|
||||
import json
|
||||
import os
|
||||
import pandas as pd
|
||||
from six.moves.urllib import request
|
||||
|
||||
from catalyst.assets._assets import TradingPair
|
||||
from ccxt import NetworkError
|
||||
from catalyst.constants import LOG_LEVEL, EXCHANGE_CONFIG_URL
|
||||
from catalyst.exchange.exchange_errors import MarketsNotFoundError, \
|
||||
InvalidMarketError
|
||||
from catalyst.exchange.utils.exchange_utils import get_catalyst_symbol, \
|
||||
get_exchange_folder, get_exchange_auth
|
||||
from catalyst.exchange.utils.serialization_utils import ExchangeJSONDecoder, \
|
||||
ExchangeJSONEncoder
|
||||
from logbook import Logger
|
||||
from redo import retry
|
||||
from ccxt.base.exchange import Exchange
|
||||
from catalyst.utils.paths import last_modified_time, data_root, \
|
||||
ensure_directory
|
||||
import ccxt
|
||||
|
||||
log = Logger('ccxt_utils', level=LOG_LEVEL)
|
||||
|
||||
|
||||
def scan_exchange_configs(features=None, history=None, is_authenticated=False,
|
||||
path=None):
|
||||
"""
|
||||
Finding exchanges from their config files
|
||||
|
||||
Parameters
|
||||
----------
|
||||
features
|
||||
is_authenticated
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
"""
|
||||
for exchange_name in ccxt.exchanges:
|
||||
config = get_exchange_config(exchange_name, path)
|
||||
if not config or 'error' in config:
|
||||
log.info(
|
||||
'skipping invalid exchange {}'.format(exchange_name)
|
||||
)
|
||||
|
||||
# Check if the exchange has an auth.json file
|
||||
if is_authenticated:
|
||||
exchange_auth = get_exchange_auth(exchange_name)
|
||||
has_auth = (exchange_auth['key'] != ''
|
||||
and exchange_auth['secret'] != '')
|
||||
|
||||
if not has_auth:
|
||||
continue
|
||||
|
||||
if features is None:
|
||||
has_features = True
|
||||
|
||||
else:
|
||||
try:
|
||||
supported_features = [
|
||||
feature for feature in features if
|
||||
feature in config['features']
|
||||
]
|
||||
has_features = len(supported_features) > 0
|
||||
except Exception:
|
||||
has_features = False
|
||||
|
||||
# TODO: filter by history
|
||||
if has_features:
|
||||
yield config
|
||||
|
||||
|
||||
def get_exchange_config(exchange_name, path=None, environ=None,
|
||||
expiry='1H'):
|
||||
"""
|
||||
The de-serialized content of the exchange's config.json.
|
||||
Parameters
|
||||
----------
|
||||
exchange_name: str
|
||||
The exchange name
|
||||
filename: str
|
||||
The target file
|
||||
environ:
|
||||
|
||||
Returns
|
||||
-------
|
||||
config: dict[srt, Object]
|
||||
The config dictionary.
|
||||
|
||||
"""
|
||||
try:
|
||||
if path is None:
|
||||
root = data_root(environ)
|
||||
path = os.path.join(root, 'exchanges')
|
||||
|
||||
folder = os.path.join(path, exchange_name)
|
||||
ensure_directory(folder)
|
||||
|
||||
filename = os.path.join(folder, 'config.json')
|
||||
url = EXCHANGE_CONFIG_URL.format(exchange=exchange_name)
|
||||
if os.path.isfile(filename):
|
||||
# If the file exists, only update periodically to avoid
|
||||
# unnecessary calls
|
||||
now = pd.Timestamp.utcnow()
|
||||
limit = pd.Timedelta(expiry)
|
||||
if pd.Timedelta(now - last_modified_time(filename)) > limit:
|
||||
try:
|
||||
request.urlretrieve(url=url, filename=filename)
|
||||
except Exception as e:
|
||||
log.warn(
|
||||
'unable to update config {} => {}: {}'.format(
|
||||
url, filename, e
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
request.urlretrieve(url=url, filename=filename)
|
||||
|
||||
with open(filename) as data_file:
|
||||
data = json.load(data_file, cls=ExchangeJSONDecoder)
|
||||
return data
|
||||
|
||||
except Exception as e:
|
||||
log.warn(
|
||||
'unable to download {} config: {}'.format(
|
||||
exchange_name, e
|
||||
)
|
||||
)
|
||||
return dict(error=e)
|
||||
|
||||
|
||||
def save_exchange_config(config, filename=None, environ=None):
|
||||
"""
|
||||
Save assets into an exchange_config file.
|
||||
Parameters
|
||||
----------
|
||||
exchange_name: str
|
||||
config
|
||||
environ
|
||||
Returns
|
||||
-------
|
||||
"""
|
||||
if filename is None:
|
||||
name = 'config.json'
|
||||
exchange_folder = get_exchange_folder(config['id'], environ)
|
||||
filename = os.path.join(exchange_folder, name)
|
||||
|
||||
with open(filename, 'w+') as handle:
|
||||
json.dump(config, handle, indent=4, cls=ExchangeJSONEncoder)
|
||||
|
||||
|
||||
def fetch_markets(ccxt_exchange):
|
||||
"""
|
||||
Fetches CCXT market objects.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ccxt_exchange: Exchange
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
"""
|
||||
markets_symbols = ccxt_exchange.load_markets()
|
||||
log.debug(
|
||||
'fetching {} markets:\n{}'.format(
|
||||
ccxt_exchange.name, markets_symbols
|
||||
)
|
||||
)
|
||||
markets = ccxt_exchange.fetch_markets()
|
||||
|
||||
if not markets:
|
||||
raise MarketsNotFoundError(
|
||||
exchange=ccxt_exchange.name,
|
||||
)
|
||||
|
||||
for market in markets:
|
||||
if 'id' not in market:
|
||||
raise InvalidMarketError(
|
||||
exchange=ccxt_exchange.name,
|
||||
market=market,
|
||||
)
|
||||
return markets
|
||||
|
||||
|
||||
def create_exchange_config(ccxt_exchange):
|
||||
"""
|
||||
Creates an exchange config structure.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
ccxt_exchange: Exchange
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
"""
|
||||
exchange_name = ccxt_exchange.__class__.__name__
|
||||
config = dict(
|
||||
id=exchange_name,
|
||||
name=ccxt_exchange.name,
|
||||
features=[
|
||||
feature for feature in ccxt_exchange.has if
|
||||
ccxt_exchange.has[feature]
|
||||
]
|
||||
)
|
||||
markets = retry(
|
||||
action=fetch_markets,
|
||||
attempts=5,
|
||||
sleeptime=5,
|
||||
retry_exceptions=(NetworkError,),
|
||||
cleanup=lambda: log.warn(
|
||||
'fetching markets again for {}'.format(exchange_name)
|
||||
),
|
||||
args=(ccxt_exchange,)
|
||||
)
|
||||
|
||||
config['assets'] = []
|
||||
for market in markets:
|
||||
asset = create_trading_pair(exchange_name, market)
|
||||
config['assets'].append(asset)
|
||||
|
||||
return config
|
||||
|
||||
|
||||
def create_trading_pair(exchange_name, market, start_dt=None, end_dt=None,
|
||||
leverage=1, end_daily=None, end_minute=None):
|
||||
"""
|
||||
Creating a TradingPair from market and asset data.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
market: dict[str, Object]
|
||||
start_dt
|
||||
end_dt
|
||||
leverage
|
||||
end_daily
|
||||
end_minute
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
"""
|
||||
params = dict(
|
||||
exchange=exchange_name,
|
||||
data_source='catalyst',
|
||||
exchange_symbol=market['id'],
|
||||
symbol=get_catalyst_symbol(market),
|
||||
start_date=start_dt,
|
||||
end_date=end_dt,
|
||||
leverage=leverage,
|
||||
asset_name=market['symbol'],
|
||||
end_daily=end_daily,
|
||||
end_minute=end_minute,
|
||||
)
|
||||
apply_conditional_market_params(exchange_name, params, market)
|
||||
|
||||
return TradingPair(**params)
|
||||
|
||||
|
||||
def apply_conditional_market_params(exchange_name, params, market):
|
||||
"""
|
||||
Applies a CCXT market dict to parameters of TradingPair init.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
params: dict[Object]
|
||||
market: dict[Object]
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
"""
|
||||
# TODO: make this more externalized / configurable
|
||||
# Consider representing in some type of JSON structure
|
||||
if 'active' in market:
|
||||
params['trading_state'] = 1 if market['active'] else 0
|
||||
|
||||
else:
|
||||
params['trading_state'] = 1
|
||||
|
||||
if 'lot' in market:
|
||||
params['min_trade_size'] = market['lot']
|
||||
params['lot'] = market['lot']
|
||||
|
||||
if exchange_name == 'bitfinex':
|
||||
params['maker'] = 0.001
|
||||
params['taker'] = 0.002
|
||||
|
||||
elif 'maker' in market and 'taker' in market \
|
||||
and market['maker'] is not None \
|
||||
and market['taker'] is not None:
|
||||
params['maker'] = market['maker']
|
||||
params['taker'] = market['taker']
|
||||
|
||||
else:
|
||||
# TODO: default commission, make configurable
|
||||
params['maker'] = 0.0015
|
||||
params['taker'] = 0.0025
|
||||
|
||||
info = market['info'] if 'info' in market else None
|
||||
if info:
|
||||
if 'minimum_order_size' in info:
|
||||
params['min_trade_size'] = float(info['minimum_order_size'])
|
||||
|
||||
if 'lot' not in params:
|
||||
params['lot'] = params['min_trade_size']
|
||||
@@ -0,0 +1,354 @@
|
||||
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 'h' if unit == 'H' 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 timestr_to_dt(timestr, data_frequency):
|
||||
dt_format = '%Y' if data_frequency == 'daily' else '%Y%m'
|
||||
dt = pd.to_datetime(timestr, format=dt_format, utc=True)
|
||||
return 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_period(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, supported_freqs=['D', 'T']):
|
||||
"""
|
||||
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)
|
||||
data_frequency = 'minute'
|
||||
|
||||
elif unit.lower() == 'h':
|
||||
if 'H' in supported_freqs:
|
||||
unit = 'H'
|
||||
alias = '{}H'.format(candle_size)
|
||||
|
||||
else:
|
||||
candle_size = candle_size * 60
|
||||
alias = '{}T'.format(candle_size)
|
||||
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)
|
||||
@@ -1,21 +1,19 @@
|
||||
import hashlib
|
||||
import json
|
||||
import os
|
||||
import pickle
|
||||
import re
|
||||
import shutil
|
||||
from datetime import date, datetime
|
||||
|
||||
import json
|
||||
import pandas as pd
|
||||
import pickle
|
||||
from catalyst.assets._assets import TradingPair
|
||||
from datetime import date, datetime
|
||||
from six import string_types
|
||||
from six.moves.urllib import request
|
||||
|
||||
from catalyst.constants import DATE_FORMAT, SYMBOLS_URL
|
||||
from catalyst.exchange.exchange_errors import ExchangeSymbolsNotFound, \
|
||||
InvalidHistoryFrequencyError, InvalidHistoryFrequencyAlias
|
||||
from catalyst.constants import EXCHANGE_CONFIG_URL
|
||||
from catalyst.exchange.utils.serialization_utils import ExchangeJSONEncoder, \
|
||||
ExchangeJSONDecoder
|
||||
ExchangeJSONDecoder, ConfigJSONEncoder
|
||||
from catalyst.utils.deprecate import deprecated
|
||||
from catalyst.utils.paths import data_root, ensure_directory, \
|
||||
last_modified_time
|
||||
|
||||
@@ -71,7 +69,7 @@ def is_blacklist(exchange_name, environ=None):
|
||||
return os.path.exists(filename)
|
||||
|
||||
|
||||
def get_exchange_symbols_filename(exchange_name, is_local=False, environ=None):
|
||||
def get_exchange_config_filename(exchange_name, environ=None):
|
||||
"""
|
||||
The absolute path of the exchange's symbol.json file.
|
||||
|
||||
@@ -85,12 +83,12 @@ def get_exchange_symbols_filename(exchange_name, is_local=False, environ=None):
|
||||
str
|
||||
|
||||
"""
|
||||
name = 'symbols.json' if not is_local else 'symbols_local.json'
|
||||
name = 'config.json'
|
||||
exchange_folder = get_exchange_folder(exchange_name, environ)
|
||||
return os.path.join(exchange_folder, name)
|
||||
|
||||
|
||||
def download_exchange_symbols(exchange_name, environ=None):
|
||||
def download_exchange_config(exchange_name, filename, environ=None):
|
||||
"""
|
||||
Downloads the exchange's symbols.json from the repository.
|
||||
|
||||
@@ -104,15 +102,14 @@ def download_exchange_symbols(exchange_name, environ=None):
|
||||
str
|
||||
|
||||
"""
|
||||
filename = get_exchange_symbols_filename(exchange_name)
|
||||
url = SYMBOLS_URL.format(exchange=exchange_name)
|
||||
response = request.urlretrieve(url=url, filename=filename)
|
||||
return response
|
||||
url = EXCHANGE_CONFIG_URL.format(exchange=exchange_name)
|
||||
request.urlretrieve(url=url, filename=filename)
|
||||
|
||||
|
||||
def get_exchange_symbols(exchange_name, is_local=False, environ=None):
|
||||
@deprecated
|
||||
def get_exchange_config(exchange_name, filename=None, environ=None):
|
||||
"""
|
||||
The de-serialized content of the exchange's symbols.json.
|
||||
The de-serialized content of the exchange's config.json.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -125,52 +122,48 @@ def get_exchange_symbols(exchange_name, is_local=False, environ=None):
|
||||
Object
|
||||
|
||||
"""
|
||||
filename = get_exchange_symbols_filename(exchange_name, is_local)
|
||||
|
||||
if not is_local and (not os.path.isfile(filename) or pd.Timedelta(
|
||||
pd.Timestamp('now', tz='UTC') - last_modified_time(
|
||||
filename)).days > 1):
|
||||
download_exchange_symbols(exchange_name, environ)
|
||||
if filename is None:
|
||||
filename = get_exchange_config_filename(exchange_name)
|
||||
|
||||
if os.path.isfile(filename):
|
||||
with open(filename) as data_file:
|
||||
try:
|
||||
data = json.load(data_file, cls=ExchangeJSONDecoder)
|
||||
return data
|
||||
now = pd.Timestamp.utcnow()
|
||||
limit = pd.Timedelta('2H')
|
||||
if pd.Timedelta(now - last_modified_time(filename)) > limit:
|
||||
download_exchange_config(exchange_name, filename, environ)
|
||||
|
||||
except ValueError:
|
||||
return dict()
|
||||
else:
|
||||
raise ExchangeSymbolsNotFound(
|
||||
exchange=exchange_name,
|
||||
filename=filename
|
||||
)
|
||||
download_exchange_config(exchange_name, filename, environ)
|
||||
|
||||
with open(filename) as data_file:
|
||||
try:
|
||||
data = json.load(data_file, cls=ExchangeJSONDecoder)
|
||||
return data
|
||||
|
||||
except ValueError:
|
||||
return dict()
|
||||
|
||||
|
||||
def save_exchange_symbols(exchange_name, assets, is_local=False, environ=None):
|
||||
def save_exchange_config(exchange_name, config, filename=None, environ=None):
|
||||
"""
|
||||
Save assets into an exchange_symbols file.
|
||||
Save assets into an exchange_config file.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exchange_name: str
|
||||
assets: list[dict[str, object]]
|
||||
is_local: bool
|
||||
config
|
||||
environ
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
"""
|
||||
asset_dicts = dict()
|
||||
for symbol in assets:
|
||||
asset_dicts[symbol] = assets[symbol].to_dict()
|
||||
if filename is None:
|
||||
name = 'config.json'
|
||||
exchange_folder = get_exchange_folder(exchange_name, environ)
|
||||
filename = os.path.join(exchange_folder, name)
|
||||
|
||||
filename = get_exchange_symbols_filename(
|
||||
exchange_name, is_local, environ
|
||||
)
|
||||
with open(filename, 'wt') as handle:
|
||||
json.dump(asset_dicts, handle, indent=4, default=symbols_serial)
|
||||
with open(filename, 'w+') as handle:
|
||||
json.dump(config, handle, indent=4, cls=ConfigJSONEncoder)
|
||||
|
||||
|
||||
def get_symbols_string(assets):
|
||||
@@ -190,7 +183,7 @@ def get_symbols_string(assets):
|
||||
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.
|
||||
|
||||
@@ -205,7 +198,8 @@ def get_exchange_auth(exchange_name, environ=None):
|
||||
|
||||
"""
|
||||
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):
|
||||
with open(filename) as data_file:
|
||||
@@ -271,6 +265,7 @@ def get_algo_object(algo_name, key, environ=None, rel_path=None, how='pickle'):
|
||||
key: str
|
||||
environ:
|
||||
rel_path: str
|
||||
how: str
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -314,6 +309,7 @@ def save_algo_object(algo_name, key, obj, environ=None, rel_path=None,
|
||||
obj: Object
|
||||
environ:
|
||||
rel_path: str
|
||||
how: str
|
||||
|
||||
"""
|
||||
folder = get_algo_folder(algo_name, environ)
|
||||
@@ -390,6 +386,71 @@ def save_algo_df(algo_name, key, df, environ=None, rel_path=None):
|
||||
df.to_csv(handle, encoding='UTF_8')
|
||||
|
||||
|
||||
def clear_frame_stats_directory(algo_name):
|
||||
"""
|
||||
remove the outdated directory
|
||||
to avoid overloading the disk
|
||||
|
||||
Parameters
|
||||
----------
|
||||
algo_name: str
|
||||
|
||||
Returns
|
||||
-------
|
||||
error: str
|
||||
|
||||
"""
|
||||
error = None
|
||||
algo_folder = get_algo_folder(algo_name)
|
||||
folder = os.path.join(algo_folder, 'frame_stats')
|
||||
if os.path.exists(folder):
|
||||
try:
|
||||
shutil.rmtree(folder)
|
||||
except OSError:
|
||||
error = 'unable to remove {}, the analyze ' \
|
||||
'data will be inconsistent'.format(folder)
|
||||
return error
|
||||
|
||||
|
||||
def remove_old_files(algo_name, today, rel_path, environ=None):
|
||||
"""
|
||||
remove old files from a directory
|
||||
to avoid overloading the disk
|
||||
|
||||
Parameters
|
||||
----------
|
||||
algo_name: str
|
||||
today: Timestamp
|
||||
rel_path: str
|
||||
environ:
|
||||
|
||||
Returns
|
||||
-------
|
||||
error: str
|
||||
|
||||
"""
|
||||
|
||||
error = None
|
||||
algo_folder = get_algo_folder(algo_name, environ)
|
||||
folder = os.path.join(algo_folder, rel_path)
|
||||
ensure_directory(folder)
|
||||
|
||||
# run on all files in the folder
|
||||
for f in os.listdir(folder):
|
||||
try:
|
||||
file_path = os.path.join(folder, f)
|
||||
creation_unix = os.path.getctime(file_path)
|
||||
creation_time = pd.to_datetime(creation_unix, unit='s', utc=True)
|
||||
|
||||
# if the file is older than 30 days erase it
|
||||
if today - pd.DateOffset(30) > creation_time:
|
||||
os.unlink(file_path)
|
||||
except OSError:
|
||||
error = 'unable to erase files in {}'.format(folder)
|
||||
|
||||
return error
|
||||
|
||||
|
||||
def get_exchange_minute_writer_root(exchange_name, environ=None):
|
||||
"""
|
||||
The minute writer folder for the exchange.
|
||||
@@ -443,25 +504,6 @@ def has_bundle(exchange_name, data_frequency, environ=None):
|
||||
return os.path.isdir(folder)
|
||||
|
||||
|
||||
def symbols_serial(obj):
|
||||
"""
|
||||
JSON serializer for objects not serializable by default json code
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj: Object
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
|
||||
"""
|
||||
if isinstance(obj, (datetime, date)):
|
||||
return obj.floor('1D').strftime(DATE_FORMAT)
|
||||
|
||||
raise TypeError("Type %s not serializable" % type(obj))
|
||||
|
||||
|
||||
def perf_serial(obj):
|
||||
"""
|
||||
JSON serializer for objects not serializable by default json code
|
||||
@@ -510,73 +552,7 @@ def get_common_assets(exchanges):
|
||||
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, start_dt=None):
|
||||
"""
|
||||
Resample the OHCLV DataFrame using the specified frequency.
|
||||
|
||||
@@ -604,51 +580,19 @@ def resample_history_df(df, freq, field):
|
||||
else:
|
||||
raise ValueError('Invalid field.')
|
||||
|
||||
resampled_df = df.resample(freq).agg(agg)
|
||||
resampled_df = df.resample(
|
||||
freq, closed='left', label='left'
|
||||
).agg(agg) # type: pd.DataFrame
|
||||
|
||||
# Because the samples are closed left, we get one more candle at
|
||||
# the beginning then the requested number for bars. Removing this
|
||||
# candle to avoid confusion.
|
||||
if start_dt and not resampled_df.empty:
|
||||
resampled_df = resampled_df[resampled_df.index >= start_dt]
|
||||
|
||||
return resampled_df
|
||||
|
||||
|
||||
def mixin_market_params(exchange_name, params, market):
|
||||
"""
|
||||
Applies a CCXT market dict to parameters of TradingPair init.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
params: dict[Object]
|
||||
market: dict[Object]
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
"""
|
||||
# TODO: make this more externalized / configurable
|
||||
if 'lot' in market:
|
||||
params['min_trade_size'] = market['lot']
|
||||
params['lot'] = market['lot']
|
||||
|
||||
if exchange_name == 'bitfinex':
|
||||
params['maker'] = 0.001
|
||||
params['taker'] = 0.002
|
||||
|
||||
elif 'maker' in market and 'taker' in market \
|
||||
and market['maker'] is not None and market['taker'] is not None:
|
||||
params['maker'] = market['maker']
|
||||
params['taker'] = market['taker']
|
||||
|
||||
else:
|
||||
# TODO: default commission, make configurable
|
||||
params['maker'] = 0.0015
|
||||
params['taker'] = 0.0025
|
||||
|
||||
info = market['info'] if 'info' in market else None
|
||||
if info:
|
||||
if 'minimum_order_size' in info:
|
||||
params['min_trade_size'] = float(info['minimum_order_size'])
|
||||
|
||||
if 'lot' not in params:
|
||||
params['lot'] = params['min_trade_size']
|
||||
|
||||
|
||||
def from_ms_timestamp(ms):
|
||||
return pd.to_datetime(ms, unit='ms', utc=True)
|
||||
|
||||
@@ -711,25 +655,69 @@ def save_asset_data(folder, df, decimals=8):
|
||||
)
|
||||
|
||||
|
||||
def get_candles_df(candles, field, freq, bar_count, end_dt,
|
||||
previous_value=None):
|
||||
def forward_fill_df_if_needed(df, periods):
|
||||
df = df.reindex(periods)
|
||||
# volume should always be 0 (if there were no trades in this interval)
|
||||
df['volume'] = df['volume'].fillna(0.0)
|
||||
# ie pull the last close into this close
|
||||
df['close'] = df.fillna(method='pad')
|
||||
# now copy the close that was pulled down from the last timestep
|
||||
# into this row, across into o/h/l
|
||||
df['open'] = df['open'].fillna(df['close'])
|
||||
df['low'] = df['low'].fillna(df['close'])
|
||||
df['high'] = df['high'].fillna(df['close'])
|
||||
return df
|
||||
|
||||
|
||||
def transform_candles_to_df(candles):
|
||||
return pd.DataFrame(candles).set_index('last_traded')
|
||||
|
||||
|
||||
def get_candles_df(candles, field, freq, bar_count, end_dt):
|
||||
all_series = dict()
|
||||
|
||||
for asset in candles:
|
||||
periods = pd.date_range(end=end_dt, periods=bar_count, freq=freq)
|
||||
asset_df = transform_candles_to_df(candles[asset])
|
||||
rounded_end_dt = end_dt.floor(freq)
|
||||
periods = pd.date_range(end=rounded_end_dt,
|
||||
periods=bar_count,
|
||||
freq=freq)
|
||||
asset_df = forward_fill_df_if_needed(asset_df, periods)
|
||||
|
||||
dates = [candle['last_traded'] for candle in candles[asset]]
|
||||
values = [candle[field] for candle in candles[asset]]
|
||||
series = pd.Series(values, index=dates)
|
||||
|
||||
series = series.reindex(
|
||||
periods,
|
||||
method='ffill',
|
||||
fill_value=previous_value,
|
||||
)
|
||||
series.sort_index(inplace=True)
|
||||
all_series[asset] = series
|
||||
all_series[asset] = pd.Series(asset_df[field])
|
||||
|
||||
df = pd.DataFrame(all_series)
|
||||
|
||||
df.dropna(inplace=True)
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def get_trades_df(trades):
|
||||
df = pd.DataFrame(trades)
|
||||
df.index = pd.to_datetime(df.pop('datetime'))
|
||||
df.index = df.index.tz_localize('UTC')
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def candles_from_trades(trades_df, freq):
|
||||
"""
|
||||
Calculate OHLCV from candles.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
trades_df
|
||||
freq
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
"""
|
||||
df = trades_df['price'].resample(freq).ohlc() # type: pd.DataFrame
|
||||
df['volume'] = trades_df['amount'].resample(freq).sum()
|
||||
|
||||
df.dropna(axis=0, how='all', inplace=True)
|
||||
df.sort_index(inplace=True, ascending=False)
|
||||
|
||||
return df
|
||||
|
||||
@@ -1,25 +1,28 @@
|
||||
import os
|
||||
|
||||
from logbook import Logger
|
||||
|
||||
from catalyst.constants import LOG_LEVEL
|
||||
from catalyst.exchange.ccxt.ccxt_exchange import CCXT
|
||||
from catalyst.exchange.exchange import Exchange
|
||||
from catalyst.exchange.exchange_errors import ExchangeAuthEmpty
|
||||
from catalyst.exchange.utils.ccxt_utils import scan_exchange_configs
|
||||
from catalyst.exchange.utils.exchange_utils import get_exchange_auth, \
|
||||
get_exchange_folder, is_blacklist
|
||||
get_exchange_folder
|
||||
from logbook import Logger
|
||||
|
||||
log = Logger('factory', level=LOG_LEVEL)
|
||||
exchange_cache = dict()
|
||||
|
||||
|
||||
def get_exchange(exchange_name, base_currency=None, must_authenticate=False,
|
||||
skip_init=False):
|
||||
skip_init=False, auth_alias=None, config=None):
|
||||
key = (exchange_name, base_currency)
|
||||
if key in exchange_cache:
|
||||
if not skip_init:
|
||||
exchange_cache[key].init()
|
||||
|
||||
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'] != '')
|
||||
if must_authenticate and not has_auth:
|
||||
@@ -34,7 +37,10 @@ def get_exchange(exchange_name, base_currency=None, must_authenticate=False,
|
||||
exchange_name=exchange_name,
|
||||
key=exchange_auth['key'],
|
||||
secret=exchange_auth['secret'],
|
||||
password=exchange_auth['password'] if 'password'
|
||||
in exchange_auth.keys() else '',
|
||||
base_currency=base_currency,
|
||||
config=config,
|
||||
)
|
||||
exchange_cache[key] = exchange
|
||||
|
||||
@@ -52,8 +58,8 @@ def get_exchanges(exchange_names):
|
||||
return exchanges
|
||||
|
||||
|
||||
def find_exchanges(features=None, skip_blacklist=True, is_authenticated=False,
|
||||
base_currency=None):
|
||||
def find_exchanges(features=None, history=None, skip_blacklist=True, path=None,
|
||||
is_authenticated=False, base_currency=None):
|
||||
"""
|
||||
Find exchanges filtered by a list of feature.
|
||||
|
||||
@@ -71,28 +77,33 @@ def find_exchanges(features=None, skip_blacklist=True, is_authenticated=False,
|
||||
list[Exchange]
|
||||
|
||||
"""
|
||||
exchange_names = CCXT.find_exchanges(features, is_authenticated)
|
||||
|
||||
exchanges = []
|
||||
for exchange_name in exchange_names:
|
||||
if skip_blacklist and is_blacklist(exchange_name):
|
||||
return list(
|
||||
scan_exchanges(
|
||||
features,
|
||||
history,
|
||||
skip_blacklist,
|
||||
path,
|
||||
is_authenticated,
|
||||
base_currency
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
def scan_exchanges(features=None, history=None, skip_blacklist=True, path=None,
|
||||
is_authenticated=False, base_currency=None):
|
||||
for config in scan_exchange_configs(
|
||||
features=features,
|
||||
history=history,
|
||||
is_authenticated=is_authenticated,
|
||||
path=path,
|
||||
):
|
||||
if skip_blacklist and (config is None or 'error' in config):
|
||||
continue
|
||||
|
||||
exchange = get_exchange(
|
||||
exchange_name=exchange_name,
|
||||
yield get_exchange(
|
||||
exchange_name=config['id'],
|
||||
skip_init=True,
|
||||
base_currency=base_currency,
|
||||
config=config,
|
||||
)
|
||||
|
||||
if features is not None:
|
||||
if 'dailyBundle' in features \
|
||||
and not exchange.has_bundle('daily'):
|
||||
continue
|
||||
|
||||
elif 'minuteBundle' in features \
|
||||
and not exchange.has_bundle('minute'):
|
||||
continue
|
||||
|
||||
exchanges.append(exchange)
|
||||
|
||||
return exchanges
|
||||
|
||||
@@ -5,7 +5,30 @@ from json import JSONEncoder
|
||||
import pandas as pd
|
||||
from six import string_types
|
||||
|
||||
from catalyst.constants import DATE_TIME_FORMAT
|
||||
from datetime import date, datetime
|
||||
from catalyst.constants import DATE_TIME_FORMAT, DATE_FORMAT
|
||||
from catalyst.assets._assets import TradingPair
|
||||
|
||||
|
||||
class ConfigJSONEncoder(json.JSONEncoder):
|
||||
def default(self, obj):
|
||||
"""
|
||||
JSON serializer for objects not serializable by default json code
|
||||
|
||||
Parameters
|
||||
----------
|
||||
obj: Object
|
||||
|
||||
Returns
|
||||
-------
|
||||
str
|
||||
|
||||
"""
|
||||
if isinstance(obj, (datetime, date)):
|
||||
return obj.floor('1D').strftime(DATE_FORMAT)
|
||||
|
||||
elif isinstance(obj, TradingPair):
|
||||
return obj.to_dict()
|
||||
|
||||
|
||||
class ExchangeJSONEncoder(json.JSONEncoder):
|
||||
@@ -13,6 +36,15 @@ class ExchangeJSONEncoder(json.JSONEncoder):
|
||||
if isinstance(obj, pd.Timestamp):
|
||||
return obj.strftime(DATE_TIME_FORMAT)
|
||||
|
||||
elif isinstance(obj, TradingPair):
|
||||
asset = obj.to_dict()
|
||||
asset['maker'] = round(asset['maker'], asset['decimals'])
|
||||
asset['taker'] = round(asset['taker'], asset['decimals'])
|
||||
asset['lot'] = round(asset['lot'], 4)
|
||||
asset['min_trade_size'] = round(asset['min_trade_size'], 4)
|
||||
asset['max_trade_size'] = round(asset['max_trade_size'], 4)
|
||||
return asset
|
||||
|
||||
# Let the base class default method raise the TypeError
|
||||
return JSONEncoder.default(self, obj)
|
||||
|
||||
|
||||
@@ -8,9 +8,9 @@ import time
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from catalyst.assets._assets import TradingPair
|
||||
|
||||
from catalyst.exchange.utils.exchange_utils import get_algo_folder
|
||||
from catalyst.utils.paths import data_root, ensure_directory
|
||||
from operator import itemgetter
|
||||
|
||||
s3_conn = []
|
||||
mailgun = []
|
||||
@@ -44,7 +44,7 @@ def crossover(source, target):
|
||||
"""
|
||||
if isinstance(target, numbers.Number):
|
||||
if source[-1] is np.nan or source[-2] is np.nan \
|
||||
or target is np.nan:
|
||||
or target is np.nan:
|
||||
return False
|
||||
|
||||
if source[-1] >= target > source[-2]:
|
||||
@@ -54,7 +54,7 @@ def crossover(source, target):
|
||||
|
||||
else:
|
||||
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
|
||||
|
||||
if source[-1] > target[-1] and source[-2] < target[-2]:
|
||||
@@ -81,7 +81,7 @@ def crossunder(source, target):
|
||||
"""
|
||||
if isinstance(target, numbers.Number):
|
||||
if source[-1] is np.nan or source[-2] is np.nan \
|
||||
or target is np.nan:
|
||||
or target is np.nan:
|
||||
return False
|
||||
|
||||
if source[-1] < target <= source[-2]:
|
||||
@@ -90,7 +90,7 @@ def crossunder(source, target):
|
||||
return False
|
||||
else:
|
||||
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
|
||||
|
||||
if source[-1] < target[-1] and source[-2] >= target[-2]:
|
||||
@@ -229,7 +229,10 @@ def prepare_stats(stats, recorded_cols=list()):
|
||||
asset_values)
|
||||
|
||||
df = pd.DataFrame(stats)
|
||||
|
||||
df['orders'] = df['orders'].apply(lambda orders: len(orders))
|
||||
df['transactions'] = df['transactions'].apply(
|
||||
lambda transactions: len(transactions)
|
||||
)
|
||||
index_cols = [
|
||||
'period_close', 'starting_cash', 'ending_cash', 'portfolio_value',
|
||||
'pnl', 'long_exposure', 'short_exposure', 'orders', 'transactions',
|
||||
@@ -241,11 +244,6 @@ def prepare_stats(stats, recorded_cols=list()):
|
||||
for column in recorded_cols:
|
||||
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:
|
||||
columns = asset_cols
|
||||
df.set_index(index_cols, drop=True, inplace=True)
|
||||
@@ -261,7 +259,14 @@ def prepare_stats(stats, recorded_cols=list()):
|
||||
return df, columns
|
||||
|
||||
|
||||
def get_pretty_stats(stats, recorded_cols=None, num_rows=10):
|
||||
def set_print_settings():
|
||||
pd.set_option('display.expand_frame_repr', False)
|
||||
pd.set_option('precision', 8)
|
||||
pd.set_option('display.width', 1000)
|
||||
pd.set_option('display.max_colwidth', 1000)
|
||||
|
||||
|
||||
def get_pretty_stats(stats, recorded_cols=None, num_rows=10, show_tail=True):
|
||||
"""
|
||||
Format and print the last few rows of a statistics DataFrame.
|
||||
See the pyfolio project for the data structure.
|
||||
@@ -280,18 +285,18 @@ def get_pretty_stats(stats, recorded_cols=None, num_rows=10):
|
||||
|
||||
"""
|
||||
if isinstance(stats, pd.DataFrame):
|
||||
stats = stats.T.to_dict().values()
|
||||
stats = list(stats.T.to_dict().values())
|
||||
stats.sort(key=itemgetter('period_close'))
|
||||
|
||||
if len(stats) > num_rows:
|
||||
display_stats = stats[-num_rows:] if show_tail else stats[0:num_rows]
|
||||
else:
|
||||
display_stats = stats
|
||||
|
||||
display_stats = stats[-num_rows:] if len(stats) > num_rows else stats
|
||||
df, columns = prepare_stats(
|
||||
display_stats, recorded_cols=recorded_cols
|
||||
)
|
||||
|
||||
pd.set_option('display.expand_frame_repr', False)
|
||||
pd.set_option('precision', 8)
|
||||
pd.set_option('display.width', 1000)
|
||||
pd.set_option('display.max_colwidth', 1000)
|
||||
|
||||
set_print_settings()
|
||||
return df.to_string(columns=columns)
|
||||
|
||||
|
||||
@@ -352,9 +357,13 @@ def stats_to_s3(uri, stats, algo_namespace, recorded_cols=None,
|
||||
pid = os.getpid()
|
||||
|
||||
parts = uri.split('//')
|
||||
obj = s3.Object(parts[1], '{}/{}-{}-{}.csv'.format(
|
||||
folder, timestr, algo_namespace, pid
|
||||
))
|
||||
path = '{folder}/{algo}/{time}-{algo}-{pid}.csv'.format(
|
||||
folder=folder,
|
||||
algo=algo_namespace,
|
||||
time=timestr,
|
||||
pid=pid,
|
||||
)
|
||||
obj = s3.Object(parts[1], path)
|
||||
obj.put(Body=bytes_to_write)
|
||||
|
||||
|
||||
@@ -387,7 +396,8 @@ def email_error(algo_name, dt, e, environ=None):
|
||||
)})
|
||||
|
||||
|
||||
def stats_to_algo_folder(stats, algo_namespace, recorded_cols=None):
|
||||
def stats_to_algo_folder(stats, algo_namespace,
|
||||
folder_name, recorded_cols=None):
|
||||
"""
|
||||
Saves the performance stats to the algo local folder.
|
||||
|
||||
@@ -395,6 +405,7 @@ def stats_to_algo_folder(stats, algo_namespace, recorded_cols=None):
|
||||
----------
|
||||
stats: list[Object]
|
||||
algo_namespace: str
|
||||
folder_name: str
|
||||
recorded_cols: list[str]
|
||||
|
||||
Returns
|
||||
@@ -407,7 +418,7 @@ def stats_to_algo_folder(stats, algo_namespace, recorded_cols=None):
|
||||
timestr = time.strftime('%Y%m%d')
|
||||
folder = get_algo_folder(algo_namespace)
|
||||
|
||||
stats_folder = os.path.join(folder, 'stats')
|
||||
stats_folder = os.path.join(folder, folder_name)
|
||||
ensure_directory(stats_folder)
|
||||
|
||||
filename = os.path.join(stats_folder, '{}.csv'.format(timestr))
|
||||
@@ -439,6 +450,17 @@ def df_to_string(df):
|
||||
return df.to_string()
|
||||
|
||||
|
||||
def extract_orders(perf):
|
||||
order_list = perf.orders.values
|
||||
all_orders = [t for sublist in order_list for t in sublist]
|
||||
all_orders.sort(key=lambda o: o['dt'])
|
||||
|
||||
orders = pd.DataFrame(all_orders)
|
||||
if not orders.empty:
|
||||
orders.set_index('dt', inplace=True, drop=True)
|
||||
return orders
|
||||
|
||||
|
||||
def extract_transactions(perf):
|
||||
"""
|
||||
Compute indexes for buy and sell transactions
|
||||
|
||||
@@ -3,7 +3,6 @@ import random
|
||||
import tempfile
|
||||
|
||||
from catalyst.assets._assets import TradingPair
|
||||
|
||||
from catalyst.exchange.utils.exchange_utils import get_exchange_folder
|
||||
from catalyst.exchange.utils.factory import find_exchanges
|
||||
from catalyst.utils.paths import ensure_directory
|
||||
@@ -63,14 +62,14 @@ def output_df(df, assets, name=None):
|
||||
|
||||
"""
|
||||
if isinstance(assets, TradingPair):
|
||||
exchange_folder = assets.exchange
|
||||
asset_folder = assets.symbol
|
||||
asset_folder = '{}_{}'.format(assets.exchange, assets.symbol)
|
||||
else:
|
||||
exchange_folder = ','.join([asset.exchange for asset in assets])
|
||||
asset_folder = ','.join([asset.symbol for asset in assets])
|
||||
asset_folder = ','.join(
|
||||
['{}_{}'.format(a.exchange, a.symbol) for a in assets]
|
||||
)
|
||||
|
||||
folder = os.path.join(
|
||||
tempfile.gettempdir(), 'catalyst', exchange_folder, asset_folder
|
||||
tempfile.gettempdir(), 'catalyst', asset_folder
|
||||
)
|
||||
ensure_directory(folder)
|
||||
|
||||
@@ -80,4 +79,4 @@ def output_df(df, assets, name=None):
|
||||
path = os.path.join(folder, '{}.csv'.format(name))
|
||||
df.to_csv(path)
|
||||
|
||||
return path
|
||||
return path, folder
|
||||
|
||||
@@ -29,13 +29,15 @@ from .risk import check_entry
|
||||
from empyrical import (
|
||||
alpha_beta_aligned,
|
||||
annual_volatility,
|
||||
cum_returns,
|
||||
downside_risk,
|
||||
information_ratio,
|
||||
max_drawdown,
|
||||
sharpe_ratio,
|
||||
sortino_ratio
|
||||
)
|
||||
from catalyst.patches.stats import (
|
||||
max_drawdown,
|
||||
cum_returns,
|
||||
)
|
||||
|
||||
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,799 @@
|
||||
from __future__ import print_function
|
||||
|
||||
import glob
|
||||
import json
|
||||
import os
|
||||
import re
|
||||
import shutil
|
||||
import sys
|
||||
import time
|
||||
import webbrowser
|
||||
|
||||
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, MarketplaceRequiresPython3)
|
||||
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
|
||||
from catalyst.utils.paths import ensure_directory
|
||||
|
||||
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
|
||||
try:
|
||||
from web3 import Web3, HTTPProvider
|
||||
except ImportError:
|
||||
raise MarketplaceRequiresPython3()
|
||||
|
||||
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().decode(
|
||||
contract_url.info().get_content_charset()).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().decode(
|
||||
contract_url.info().get_content_charset()).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, tx):
|
||||
|
||||
url = 'https://www.myetherwallet.com/#offline-transaction'
|
||||
print('\nVisit {url} 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(
|
||||
url=url,
|
||||
_from=tx['from'],
|
||||
to=tx['to'],
|
||||
value=tx['value'],
|
||||
gas=tx['gas'],
|
||||
nonce=tx['nonce'],
|
||||
data=tx['data'], )
|
||||
)
|
||||
|
||||
webbrowser.open_new(url)
|
||||
|
||||
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)
|
||||
)
|
||||
)
|
||||
return pd.DataFrame(data)
|
||||
|
||||
def list(self):
|
||||
df = self._list()
|
||||
|
||||
set_print_settings()
|
||||
if df.empty:
|
||||
print('There are no datasets available yet.')
|
||||
else:
|
||||
print(df)
|
||||
|
||||
def subscribe(self, dataset=None):
|
||||
|
||||
if dataset is None:
|
||||
|
||||
df_sets = self._list()
|
||||
if df_sets.empty:
|
||||
print('There are no datasets available yet.')
|
||||
return
|
||||
|
||||
set_print_settings()
|
||||
while True:
|
||||
print(df_sets)
|
||||
dataset_num = input('Choose the dataset you want to '
|
||||
'subscribe to [0..{}]: '.format(
|
||||
df_sets.size - 1))
|
||||
try:
|
||||
dataset_num = int(dataset_num)
|
||||
except ValueError:
|
||||
print('Enter a number between 0 and {}'.format(
|
||||
df_sets.size - 1))
|
||||
else:
|
||||
if dataset_num not in range(0, df_sets.size):
|
||||
print('Enter a number between 0 and {}'.format(
|
||||
df_sets.size - 1))
|
||||
else:
|
||||
dataset = df_sets.iloc[dataset_num]['dataset']
|
||||
break
|
||||
|
||||
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(
|
||||
{'from': address,
|
||||
'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(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({
|
||||
'from': address,
|
||||
'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(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)
|
||||
ensure_directory(bundle_folder)
|
||||
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=None, start=None, end=None, force_download=False):
|
||||
|
||||
if ds_name is None:
|
||||
|
||||
df_sets = self._list()
|
||||
if df_sets.empty:
|
||||
print('There are no datasets available yet.')
|
||||
return
|
||||
|
||||
set_print_settings()
|
||||
while True:
|
||||
print(df_sets)
|
||||
dataset_num = input('Choose the dataset you want to '
|
||||
'ingest [0..{}]: '.format(
|
||||
df_sets.size - 1))
|
||||
try:
|
||||
dataset_num = int(dataset_num)
|
||||
except ValueError:
|
||||
print('Enter a number between 0 and {}'.format(
|
||||
df_sets.size - 1))
|
||||
else:
|
||||
if dataset_num not in range(0, df_sets.size):
|
||||
print('Enter a number between 0 and {}'.format(
|
||||
df_sets.size - 1))
|
||||
else:
|
||||
ds_name = df_sets.iloc[dataset_num]['dataset']
|
||||
break
|
||||
|
||||
# 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, ds_name=None, data_frequency=None):
|
||||
|
||||
if ds_name is None:
|
||||
mktplace_root = get_marketplace_folder()
|
||||
folders = [os.path.basename(f.rstrip('/'))
|
||||
for f in glob.glob('{}/*/'.format(mktplace_root))
|
||||
if 'temp_bundles' not in f]
|
||||
|
||||
while True:
|
||||
for idx, f in enumerate(folders):
|
||||
print('{}\t{}'.format(idx, f))
|
||||
dataset_num = input('Choose the dataset you want to '
|
||||
'clean [0..{}]: '.format(
|
||||
len(folders) - 1))
|
||||
try:
|
||||
dataset_num = int(dataset_num)
|
||||
except ValueError:
|
||||
print('Enter a number between 0 and {}'.format(
|
||||
len(folders) - 1))
|
||||
else:
|
||||
if dataset_num not in range(0, len(folders)):
|
||||
print('Enter a number between 0 and {}'.format(
|
||||
len(folders) - 1))
|
||||
else:
|
||||
ds_name = folders[dataset_num]
|
||||
break
|
||||
|
||||
ds_name = ds_name.lower()
|
||||
|
||||
if data_frequency is None:
|
||||
folder = get_data_source_folder(ds_name)
|
||||
|
||||
else:
|
||||
folder = get_bundle_folder(ds_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(
|
||||
{'from': address,
|
||||
'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(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 register the requested dataset: {}'.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,97 @@
|
||||
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,
|
||||
MarketplaceRequiresPython3]:
|
||||
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}'
|
||||
)
|
||||
|
||||
|
||||
class MarketplaceRequiresPython3(ZiplineError):
|
||||
msg = (
|
||||
'\nCatalyst requires Python3 to access the Enigma Data Marketplace.\n'
|
||||
'If you want to use the Data Marketplace, you need to reinstall '
|
||||
'Catalyst\nwith Python3. See the documentation website for additional '
|
||||
'information.')
|
||||
@@ -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,94 @@
|
||||
import os
|
||||
import random
|
||||
import re
|
||||
import shutil
|
||||
|
||||
import bcolz
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from six import string_types
|
||||
|
||||
|
||||
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_target = ztarget.todataframe()
|
||||
|
||||
df = pd.concat(
|
||||
[df_source, df_target], ignore_index=True
|
||||
) # type: pd.DataFrame
|
||||
df.drop_duplicates(inplace=True)
|
||||
df.set_index(['date', 'symbol'], drop=False, inplace=True)
|
||||
|
||||
sanitize_df(df)
|
||||
|
||||
dirname = os.path.basename(ztarget.rootdir)
|
||||
bak_dir = ztarget.rootdir.replace(dirname, '.{}'.format(dirname))
|
||||
shutil.move(ztarget.rootdir, bak_dir)
|
||||
|
||||
z = bcolz.ctable.fromdataframe(df=df, rootdir=ztarget.rootdir)
|
||||
shutil.rmtree(bak_dir)
|
||||
return z
|
||||
|
||||
|
||||
def sanitize_df(df):
|
||||
# Using a sampling method to identify dates for efficiency with
|
||||
# large datasets
|
||||
if len(df) > 100:
|
||||
indexes = random.sample(range(0, len(df) - 1), 100)
|
||||
elif len(df) > 1:
|
||||
indexes = range(0, len(df) - 1)
|
||||
else:
|
||||
indexes = [0, ]
|
||||
|
||||
for column in df.columns:
|
||||
is_date = False
|
||||
for index in indexes:
|
||||
value = df[column].iloc[index]
|
||||
if not isinstance(value, string_types):
|
||||
continue
|
||||
|
||||
# TODO: assuming that the date is at least daily
|
||||
exp = re.compile(r'^\d{4}-\d{2}-\d{2}.*$')
|
||||
matches = exp.findall(value)
|
||||
|
||||
if matches:
|
||||
is_date = True
|
||||
break
|
||||
|
||||
if is_date:
|
||||
df[column] = pd.to_datetime(df[column])
|
||||
|
||||
else:
|
||||
try:
|
||||
ser = safely_reduce_dtype(df[column])
|
||||
df[column] = ser
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def safely_reduce_dtype(ser): # pandas.Series or numpy.array
|
||||
orig_dtype = "".join(
|
||||
[x for x in ser.dtype.name if x.isalpha()]) # float/int
|
||||
mx = 1
|
||||
for val in ser.values:
|
||||
new_itemsize = np.min_scalar_type(val).itemsize
|
||||
if mx < new_itemsize:
|
||||
mx = new_itemsize
|
||||
if orig_dtype == 'int':
|
||||
mx = max(mx, 4)
|
||||
new_dtype = orig_dtype + str(mx * 8)
|
||||
return ser.astype(new_dtype)
|
||||
@@ -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.
|
||||
"""
|
||||
refcounts = self.graph.out_degree()
|
||||
for t in self.outputs.values():
|
||||
for t in list(self.outputs.values()):
|
||||
refcounts[t] += 1
|
||||
|
||||
for t in initial_terms:
|
||||
@@ -238,7 +238,7 @@ class ExecutionPlan(TermGraph):
|
||||
min_extra_rows=0):
|
||||
super(ExecutionPlan, self).__init__(terms)
|
||||
|
||||
for term in terms.values():
|
||||
for term in list(terms.values()):
|
||||
self.set_extra_rows(
|
||||
term,
|
||||
all_dates,
|
||||
|
||||
@@ -144,7 +144,7 @@ class SpecificEquityTrades(object):
|
||||
for identifier in self.identifiers:
|
||||
assets_by_identifier[identifier] = env.asset_finder.\
|
||||
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:
|
||||
event.sid = assets_by_identifier[event.sid].sid
|
||||
|
||||
@@ -167,7 +167,7 @@ class SpecificEquityTrades(object):
|
||||
for identifier in self.identifiers:
|
||||
assets_by_identifier[identifier] = env.asset_finder.\
|
||||
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.
|
||||
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)
|
||||
@@ -0,0 +1,376 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# !/usr/bin/env python2
|
||||
|
||||
import sys
|
||||
import os
|
||||
import pandas as pd
|
||||
import signal
|
||||
# import talib
|
||||
|
||||
from logbook import Logger
|
||||
|
||||
from catalyst import run_algorithm
|
||||
from catalyst.api import (
|
||||
symbol,
|
||||
record,
|
||||
order,
|
||||
order_target,
|
||||
order_target_percent,
|
||||
get_open_orders
|
||||
)
|
||||
from catalyst.finance import commission
|
||||
|
||||
|
||||
# from base.telegrambot import TelegramBot
|
||||
|
||||
|
||||
class GracefulKiller:
|
||||
# Source: https://stackoverflow.com/a/31464349
|
||||
def __init__(self, context):
|
||||
self.kill_now = False
|
||||
self.signal = 0
|
||||
self.context = context
|
||||
signal.signal(signal.SIGINT, self.exit_gracefully)
|
||||
|
||||
def exit_gracefully(self, signum, frame):
|
||||
self.kill_now = True
|
||||
self.signal = signum
|
||||
if hasattr(self.context,
|
||||
'telegram_bot') and self.context.telegram_bot is not None:
|
||||
self.context.telegram_bot.updater.stop()
|
||||
sys.exit(0)
|
||||
|
||||
def exit(self):
|
||||
return self.kill_now
|
||||
|
||||
|
||||
class SimulationParameters:
|
||||
MODE = 'paper'
|
||||
CAPITAL_BASE = 1000
|
||||
"""
|
||||
Capital base used on this simulation
|
||||
"""
|
||||
|
||||
DATA_FREQUECY = 'minute'
|
||||
|
||||
EXCHANGE_NAME = 'bitfinex'
|
||||
# EXCHANGE_NAME = 'binance'
|
||||
"""
|
||||
Exchange used on this simulation
|
||||
"""
|
||||
|
||||
DATA_DIR = '/home/av/Dropbox/simulations/data'
|
||||
ALGO_NAMESPACE = os.path.basename(__file__).split('.')[0]
|
||||
ALGO_NAMESPACE_IMAGE = '{}/{}/{}.png'.format(DATA_DIR, 'images',
|
||||
ALGO_NAMESPACE)
|
||||
ALGO_NAMESPACE_RESULTS_TABLE = '{}/{}/{}.csv'.format(DATA_DIR, 'tables',
|
||||
ALGO_NAMESPACE + '_results')
|
||||
ALGO_NAMESPACE_TRANSACTIONS_TABLE = '{}/{}/{}.csv'.format(DATA_DIR,
|
||||
'tables',
|
||||
ALGO_NAMESPACE + '_transactions')
|
||||
BASE_CURRENCY = 'usd'
|
||||
# BASE_CURRENCY = 'usdt'
|
||||
|
||||
# SHORT PERIOD
|
||||
START_DATE = '2017-09-07'
|
||||
"""
|
||||
Start date used on this simulation
|
||||
"""
|
||||
END_DATE = '2017-12-12'
|
||||
"""
|
||||
End date used on this simulation
|
||||
"""
|
||||
|
||||
SKIP_FIRST_CANDLES = 0
|
||||
|
||||
# CANDLES_SAMPLE_RATE = 60
|
||||
# CANDLES_SAMPLE_RATE = 30
|
||||
CANDLES_SAMPLE_RATE = 1
|
||||
"""
|
||||
Candle interval used on this simulation (in minutes)
|
||||
"""
|
||||
|
||||
# http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
|
||||
# 30 minute interval ohlcv data (the standard data required for candlestick or
|
||||
# indicators/signals)
|
||||
# 30T means 30 minutes re-sampling of one minute data.
|
||||
# CANDLES_FREQUENCY = '60T'
|
||||
# CANDLES_FREQUENCY = '30T'
|
||||
CANDLES_FREQUENCY = '1T'
|
||||
CANDLES_BUFFER_SIZE = 48
|
||||
COIN_PAIR = 'btc_usd'
|
||||
# COIN_PAIR = 'btc_usdt'
|
||||
"""
|
||||
Coin pair used on this simulation
|
||||
"""
|
||||
|
||||
# TRANSACTIONS
|
||||
COMMISSION_FEE = 0.0030
|
||||
BUY_MIN_AMOUNT = 5 # i.e: USD
|
||||
SELL_MIN_AMOUNT = 0.001 # i.e: USD
|
||||
BUY_SELL_PERCENTAGE = 1 # 0.50
|
||||
BUY_PERCENTAGE = BUY_SELL_PERCENTAGE
|
||||
SELL_PERCENTAGE = BUY_SELL_PERCENTAGE
|
||||
|
||||
BASE_PRICE = 'close'
|
||||
"""
|
||||
Base price used (close / Heiken Ashi)
|
||||
"""
|
||||
|
||||
|
||||
log = None
|
||||
parameters = None
|
||||
|
||||
|
||||
def print_facts(context):
|
||||
context.log.info("""
|
||||
Index: {}
|
||||
Date: {}
|
||||
Candle:
|
||||
O: {}
|
||||
H: {}
|
||||
L: {}
|
||||
C: {}
|
||||
V: {}
|
||||
Metrics:
|
||||
...
|
||||
Portfolio:
|
||||
Base price: {}
|
||||
Base coin (coin2/usd): {}
|
||||
Amount (coin1/btc): {}
|
||||
""".format(
|
||||
# Facts
|
||||
context.i,
|
||||
context.curr_minute,
|
||||
context.candles_open[-1],
|
||||
context.candles_high[-1],
|
||||
context.candles_low[-1],
|
||||
context.candles_close[-1],
|
||||
context.candles_volume[-1],
|
||||
# Metrics
|
||||
# ...
|
||||
# Portfolio
|
||||
context.curr_base_price,
|
||||
context.portfolio.cash,
|
||||
context.portfolio.positions[context.coin_pair].amount,
|
||||
))
|
||||
|
||||
|
||||
def print_facts_telegram(context):
|
||||
price = context.curr_base_price
|
||||
amount = context.portfolio.positions[context.coin_pair].amount
|
||||
pnl = context.portfolio.pnl
|
||||
capital_used = context.portfolio.capital_used
|
||||
portfolio_value = context.portfolio.portfolio_value
|
||||
portfolio_returns = context.portfolio.returns
|
||||
starting_cash = context.portfolio.starting_cash
|
||||
cash = context.portfolio.cash
|
||||
|
||||
msg = """
|
||||
Status...
|
||||
Price: {}
|
||||
Starting cash: {}
|
||||
Cash: {}
|
||||
Capital used: {}
|
||||
Amount: {}
|
||||
Portfolio value: {}
|
||||
Returns: {}
|
||||
PnL: {}
|
||||
""".format(
|
||||
price,
|
||||
starting_cash,
|
||||
cash,
|
||||
capital_used,
|
||||
amount,
|
||||
portfolio_value,
|
||||
portfolio_returns,
|
||||
pnl,
|
||||
)
|
||||
if hasattr(context, 'telegram_bot') and context.telegram_bot is not None:
|
||||
context.telegram_bot.msg(msg)
|
||||
|
||||
|
||||
def default_initialize(context):
|
||||
# FIXME: set_benchmark
|
||||
# set_benchmark(symbol(context.parameters.COIN_PAIR))
|
||||
|
||||
context.coin_pair = symbol(context.parameters.COIN_PAIR)
|
||||
context.base_price = None
|
||||
context.current_day = None
|
||||
context.counter = -1
|
||||
context.i = 0
|
||||
|
||||
context.candles_sample_rate = context.parameters.CANDLES_SAMPLE_RATE
|
||||
context.candles_frequency = context.parameters.CANDLES_FREQUENCY
|
||||
context.candles_buffer_size = context.parameters.CANDLES_BUFFER_SIZE
|
||||
context.set_commission(
|
||||
commission.PerShare(cost=context.parameters.COMMISSION_FEE))
|
||||
|
||||
|
||||
def default_handle_data(context, data):
|
||||
context.curr_minute = data.current_dt
|
||||
context.counter += 1
|
||||
|
||||
if context.candles_sample_rate == 1:
|
||||
context.i += 1
|
||||
elif context.counter % context.candles_sample_rate != 0:
|
||||
context.i += 1
|
||||
return
|
||||
|
||||
if context.i < context.parameters.SKIP_FIRST_CANDLES:
|
||||
return
|
||||
|
||||
context.candles_open = data.history(
|
||||
context.coin_pair,
|
||||
'open',
|
||||
bar_count=context.candles_buffer_size,
|
||||
frequency=context.candles_frequency)
|
||||
context.candles_high = data.history(
|
||||
context.coin_pair,
|
||||
'high',
|
||||
bar_count=context.candles_buffer_size,
|
||||
frequency=context.candles_frequency)
|
||||
context.candles_low = data.history(
|
||||
context.coin_pair,
|
||||
'low',
|
||||
bar_count=context.candles_buffer_size,
|
||||
frequency=context.candles_frequency)
|
||||
context.candles_close = data.history(
|
||||
context.coin_pair,
|
||||
'price',
|
||||
bar_count=context.candles_buffer_size,
|
||||
frequency=context.candles_frequency)
|
||||
context.candles_volume = data.history(
|
||||
context.coin_pair,
|
||||
'volume',
|
||||
bar_count=context.candles_buffer_size,
|
||||
frequency=context.candles_frequency)
|
||||
|
||||
# FIXME: Here is the error!
|
||||
# The candles_close frame shows more or less always a value of 94, while
|
||||
# bitcoin price is very different from that
|
||||
print(context.candles_close)
|
||||
|
||||
context.base_prices = context.candles_close
|
||||
cash = context.portfolio.cash
|
||||
amount = context.portfolio.positions[context.coin_pair].amount
|
||||
price = data.current(context.coin_pair, 'price')
|
||||
order_id = None
|
||||
context.last_base_price = context.base_prices[-2]
|
||||
context.curr_base_price = context.base_prices[-1]
|
||||
|
||||
# TA calculations
|
||||
# ...
|
||||
|
||||
# Sanity checks
|
||||
# assert cash >= 0
|
||||
if cash < 0:
|
||||
import ipdb;
|
||||
ipdb.set_trace() # BREAKPOINT
|
||||
|
||||
print_facts(context)
|
||||
print_facts_telegram(context)
|
||||
|
||||
# Order management
|
||||
net_shares = 0
|
||||
if context.counter == 2:
|
||||
brute_shares = (cash / price) * context.parameters.BUY_PERCENTAGE
|
||||
share_commission_fee = brute_shares * context.parameters.COMMISSION_FEE
|
||||
net_shares = brute_shares - share_commission_fee
|
||||
buy_order_id = order(context.coin_pair, net_shares)
|
||||
|
||||
if context.counter == 3:
|
||||
brute_shares = amount * context.parameters.SELL_PERCENTAGE
|
||||
share_commission_fee = brute_shares * context.parameters.COMMISSION_FEE
|
||||
net_shares = -(brute_shares - share_commission_fee)
|
||||
sell_order_id = order(context.coin_pair, net_shares)
|
||||
|
||||
# Record
|
||||
record(
|
||||
price=price,
|
||||
foo='bar',
|
||||
# volume=current['volume'],
|
||||
# price_change=price_change,
|
||||
# Metrics
|
||||
cash=cash,
|
||||
# buy=context.buy,
|
||||
# sell=context.sell
|
||||
)
|
||||
|
||||
|
||||
def default_analyze(context=None, perf=None):
|
||||
pass
|
||||
|
||||
|
||||
def initialize(context):
|
||||
global log
|
||||
context.parameters = parameters
|
||||
context.log = Logger(context.parameters.ALGO_NAMESPACE)
|
||||
log = context.log
|
||||
default_initialize(context)
|
||||
context.killer = GracefulKiller(context)
|
||||
context.telegram_bot = None
|
||||
|
||||
# TELEGRAM_TOKEN='token'
|
||||
# context.telegram_bot = TelegramBot()
|
||||
# context.telegram_bot.initialize(TELEGRAM_TOKEN, context)
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# Parameters:
|
||||
parameters = SimulationParameters()
|
||||
start_date = pd.to_datetime(parameters.START_DATE, utc=True)
|
||||
end_date = pd.to_datetime(parameters.END_DATE, utc=True)
|
||||
|
||||
if parameters.MODE == 'backtest':
|
||||
results = run_algorithm(
|
||||
capital_base=parameters.CAPITAL_BASE,
|
||||
data_frequency=parameters.DATA_FREQUECY,
|
||||
initialize=initialize,
|
||||
handle_data=default_handle_data,
|
||||
analyze=default_analyze,
|
||||
exchange_name=parameters.EXCHANGE_NAME,
|
||||
algo_namespace=parameters.ALGO_NAMESPACE,
|
||||
base_currency=parameters.BASE_CURRENCY,
|
||||
start=start_date,
|
||||
end=end_date,
|
||||
live=False,
|
||||
live_graph=False
|
||||
)
|
||||
|
||||
returns_daily = results
|
||||
results.to_csv('{}'.format(parameters.ALGO_NAMESPACE_RESULTS_TABLE))
|
||||
|
||||
# returns_daily = returns_minutely.add(1).groupby(pd.TimeGrouper('24H')).prod().add(-1)
|
||||
|
||||
# FIXME: pyfolio integration
|
||||
# pf_data = pyfolio.utils.extract_rets_pos_txn_from_zipline(results)
|
||||
# pf_data = pyfolio.utils.extract_rets_pos_txn_from_zipline(results[:'2017-01-01'])
|
||||
# pyfolio.create_full_tear_sheet(*pf_data)
|
||||
|
||||
elif parameters.MODE == 'paper':
|
||||
results = run_algorithm(
|
||||
capital_base=parameters.CAPITAL_BASE,
|
||||
data_frequency=parameters.DATA_FREQUECY,
|
||||
initialize=initialize,
|
||||
handle_data=default_handle_data,
|
||||
analyze=default_analyze,
|
||||
exchange_name=parameters.EXCHANGE_NAME,
|
||||
algo_namespace=parameters.ALGO_NAMESPACE,
|
||||
base_currency=parameters.BASE_CURRENCY,
|
||||
live=True,
|
||||
simulate_orders=True,
|
||||
live_graph=False
|
||||
)
|
||||
|
||||
elif parameters.MODE == 'live':
|
||||
results = run_algorithm(
|
||||
initialize=initialize,
|
||||
handle_data=default_handle_data,
|
||||
analyze=default_analyze,
|
||||
exchange_name=parameters.EXCHANGE_NAME,
|
||||
algo_namespace=parameters.ALGO_NAMESPACE,
|
||||
base_currency=parameters.BASE_CURRENCY,
|
||||
live=True,
|
||||
live_graph=True
|
||||
)
|
||||
@@ -0,0 +1,34 @@
|
||||
from catalyst.api import symbol
|
||||
from catalyst.utils.run_algo import run_algorithm
|
||||
|
||||
coins = ['dash', 'btc', 'dash', 'etc', 'eth', 'ltc', 'nxt', 'rep', 'str',
|
||||
'xmr', 'xrp', 'zec']
|
||||
symbols = None
|
||||
|
||||
|
||||
def initialize(context):
|
||||
pass
|
||||
|
||||
|
||||
def _handle_data(context, data):
|
||||
global symbols
|
||||
if symbols is None: symbols = [symbol(c + '_usdt') for c in coins]
|
||||
|
||||
print('getting history for: %s' % [s.symbol for s in symbols])
|
||||
history = data.history(symbols,
|
||||
['close', 'volume'],
|
||||
bar_count=1, # EXCEPTION, Change to 2
|
||||
frequency='5T')
|
||||
# print 'history: %s' % history.shape
|
||||
|
||||
|
||||
run_algorithm(initialize=initialize,
|
||||
handle_data=_handle_data,
|
||||
analyze=lambda _, results: True,
|
||||
exchange_name='poloniex',
|
||||
base_currency='usdt',
|
||||
algo_namespace='issue-236',
|
||||
live=True,
|
||||
data_frequency='minute',
|
||||
capital_base=3000,
|
||||
simulate_orders=True)
|
||||
+63
-80
@@ -8,13 +8,15 @@ from time import sleep
|
||||
|
||||
import click
|
||||
import pandas as pd
|
||||
from logbook import Logger
|
||||
from six import string_types
|
||||
|
||||
import catalyst
|
||||
from catalyst.data.bundles import load
|
||||
from catalyst.data.data_portal import DataPortal
|
||||
from catalyst.exchange.exchange_pricing_loader import ExchangePricingLoader, \
|
||||
TradingPairPricing
|
||||
from catalyst.exchange.utils.factory import get_exchange
|
||||
from logbook import Logger
|
||||
|
||||
try:
|
||||
from pygments import highlight
|
||||
@@ -22,7 +24,7 @@ try:
|
||||
from pygments.formatters import TerminalFormatter
|
||||
|
||||
PYGMENTS = True
|
||||
except:
|
||||
except ImportError:
|
||||
PYGMENTS = False
|
||||
from toolz import valfilter, concatv
|
||||
from functools import partial
|
||||
@@ -40,9 +42,6 @@ from catalyst.exchange.exchange_algorithm import (
|
||||
from catalyst.exchange.exchange_data_portal import DataPortalExchangeLive, \
|
||||
DataPortalExchangeBacktest
|
||||
from catalyst.exchange.exchange_asset_finder import ExchangeAssetFinder
|
||||
from catalyst.exchange.exchange_errors import (
|
||||
ExchangeRequestError, ExchangeRequestErrorTooManyAttempts,
|
||||
BaseCurrencyNotFoundError, NotEnoughCapitalError)
|
||||
|
||||
from catalyst.constants import LOG_LEVEL
|
||||
|
||||
@@ -57,6 +56,7 @@ class _RunAlgoError(click.ClickException, ValueError):
|
||||
----------
|
||||
pyfunc_msg : str
|
||||
The message that will be shown when called as a python function.
|
||||
|
||||
cmdline_msg : str
|
||||
The message that will be shown on the command line.
|
||||
"""
|
||||
@@ -95,11 +95,13 @@ def _run(handle_data,
|
||||
live_graph,
|
||||
analyze_live,
|
||||
simulate_orders,
|
||||
auth_aliases,
|
||||
stats_output):
|
||||
"""Run a backtest for the given algorithm.
|
||||
|
||||
This is shared between the cli and :func:`catalyst.run_algo`.
|
||||
"""
|
||||
# TODO: refactor for more granularity
|
||||
if algotext is not None:
|
||||
if local_namespace:
|
||||
ip = get_ipython() # noqa
|
||||
@@ -144,23 +146,54 @@ def _run(handle_data,
|
||||
else:
|
||||
click.echo(algotext)
|
||||
|
||||
mode = 'paper-trading' if simulate_orders else 'live-trading' \
|
||||
if live else 'backtest'
|
||||
log.warn(
|
||||
'Catalyst is currently in ALPHA. It is going through rapid '
|
||||
'development and it is subject to errors. Please use carefully. '
|
||||
'We encourage you to report any issue on GitHub: '
|
||||
'https://github.com/enigmampc/catalyst/issues'
|
||||
)
|
||||
log.info('Catalyst version {}'.format(catalyst.__version__))
|
||||
sleep(3)
|
||||
|
||||
if live:
|
||||
if simulate_orders:
|
||||
mode = 'paper-trading'
|
||||
else:
|
||||
mode = 'live-trading'
|
||||
else:
|
||||
mode = 'backtest'
|
||||
|
||||
log.info('running algo in {mode} mode'.format(mode=mode))
|
||||
|
||||
exchange_name = exchange
|
||||
if exchange_name is None:
|
||||
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()
|
||||
for exchange_name in exchange_list:
|
||||
exchanges[exchange_name] = get_exchange(
|
||||
exchange_name=exchange_name,
|
||||
for name in exchange_list:
|
||||
if auth_aliases is not None and name in auth_aliases:
|
||||
auth_alias = auth_aliases[name]
|
||||
else:
|
||||
auth_alias = None
|
||||
|
||||
exchanges[name] = get_exchange(
|
||||
exchange_name=name,
|
||||
base_currency=base_currency,
|
||||
must_authenticate=(live and not simulate_orders),
|
||||
skip_init=True,
|
||||
auth_alias=auth_alias,
|
||||
)
|
||||
|
||||
open_calendar = get_calendar('OPEN')
|
||||
@@ -190,7 +223,8 @@ def _run(handle_data,
|
||||
start = pd.Timestamp.utcnow()
|
||||
|
||||
# TODO: fix the end data.
|
||||
end = start + timedelta(hours=8760)
|
||||
if end is None:
|
||||
end = start + timedelta(hours=8760)
|
||||
|
||||
data = DataPortalExchangeLive(
|
||||
exchanges=exchanges,
|
||||
@@ -199,66 +233,6 @@ def _run(handle_data,
|
||||
first_trading_day=pd.to_datetime('today', utc=True)
|
||||
)
|
||||
|
||||
def fetch_capital_base(exchange, attempt_index=0):
|
||||
"""
|
||||
Fetch the base currency amount required to bootstrap
|
||||
the algorithm against the exchange.
|
||||
|
||||
The algorithm cannot continue without this value.
|
||||
|
||||
:param exchange: the targeted exchange
|
||||
:param attempt_index:
|
||||
:return capital_base: the amount of base currency available for
|
||||
trading
|
||||
"""
|
||||
try:
|
||||
log.debug('retrieving capital base in {} to bootstrap '
|
||||
'exchange {}'.format(base_currency, exchange_name))
|
||||
balances = exchange.get_balances()
|
||||
except ExchangeRequestError as e:
|
||||
if attempt_index < 20:
|
||||
log.warn(
|
||||
'could not retrieve balances on {}: {}'.format(
|
||||
exchange.name, e
|
||||
)
|
||||
)
|
||||
sleep(5)
|
||||
return fetch_capital_base(exchange, attempt_index + 1)
|
||||
|
||||
else:
|
||||
raise ExchangeRequestErrorTooManyAttempts(
|
||||
attempts=attempt_index,
|
||||
error=e
|
||||
)
|
||||
|
||||
if base_currency in balances:
|
||||
base_currency_available = balances[base_currency]['free']
|
||||
log.info(
|
||||
'base currency available in the account: {} {}'.format(
|
||||
base_currency_available, base_currency
|
||||
)
|
||||
)
|
||||
|
||||
return base_currency_available
|
||||
else:
|
||||
raise BaseCurrencyNotFoundError(
|
||||
base_currency=base_currency,
|
||||
exchange=exchange_name
|
||||
)
|
||||
|
||||
if not simulate_orders:
|
||||
for exchange_name in exchanges:
|
||||
exchange = exchanges[exchange_name]
|
||||
balance = fetch_capital_base(exchange)
|
||||
|
||||
if balance < capital_base:
|
||||
raise NotEnoughCapitalError(
|
||||
exchange=exchange_name,
|
||||
base_currency=base_currency,
|
||||
balance=balance,
|
||||
capital_base=capital_base,
|
||||
)
|
||||
|
||||
sim_params = create_simulation_parameters(
|
||||
start=start,
|
||||
end=end,
|
||||
@@ -278,6 +252,7 @@ def _run(handle_data,
|
||||
simulate_orders=simulate_orders,
|
||||
stats_output=stats_output,
|
||||
analyze_live=analyze_live,
|
||||
end=end,
|
||||
)
|
||||
elif exchanges:
|
||||
# Removed the existing Poloniex fork to keep things simple
|
||||
@@ -288,6 +263,15 @@ def _run(handle_data,
|
||||
# We still need to support bundles for other misc data, but we
|
||||
# can handle this later.
|
||||
|
||||
if start != pd.tslib.normalize_date(start) or \
|
||||
end != pd.tslib.normalize_date(end):
|
||||
# todo: add to Sim_Params the option to start & end at specific times
|
||||
log.warn(
|
||||
"Catalyst currently starts and ends on the start and "
|
||||
"end of the dates specified, respectively. We hope to "
|
||||
"Modify this and support specific times in a future release."
|
||||
)
|
||||
|
||||
data = DataPortalExchangeBacktest(
|
||||
exchange_names=[exchange_name for exchange_name in exchanges],
|
||||
asset_finder=None,
|
||||
@@ -441,9 +425,11 @@ def run_algorithm(initialize,
|
||||
live_graph=False,
|
||||
analyze_live=None,
|
||||
simulate_orders=True,
|
||||
auth_aliases=None,
|
||||
stats_output=None,
|
||||
output=os.devnull):
|
||||
"""Run a trading algorithm.
|
||||
"""
|
||||
Run a trading algorithm.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
@@ -485,7 +471,7 @@ def run_algorithm(initialize,
|
||||
This argument is mutually exclusive with ``data``.
|
||||
default_extension : bool, optional
|
||||
Should the default catalyst extension be loaded. This is found at
|
||||
``$ZIPLINE_ROOT/extension.py``
|
||||
``$CATALYST_ROOT/extension.py``
|
||||
extensions : iterable[str], optional
|
||||
The names of any other extensions to load. Each element may either be
|
||||
a dotted module path like ``a.b.c`` or a path to a python file ending
|
||||
@@ -496,12 +482,8 @@ def run_algorithm(initialize,
|
||||
environ : mapping[str -> str], optional
|
||||
The os environment to use. Many extensions use this to get parameters.
|
||||
This defaults to ``os.environ``.
|
||||
live: execute live trading
|
||||
exchange_conn: The exchange connection parameters
|
||||
|
||||
Supported Exchanges
|
||||
-------------------
|
||||
bitfinex
|
||||
live : bool, optional
|
||||
Execute algorithm in live trading mode.
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -574,5 +556,6 @@ def run_algorithm(initialize,
|
||||
live_graph=live_graph,
|
||||
analyze_live=analyze_live,
|
||||
simulate_orders=simulate_orders,
|
||||
auth_aliases=auth_aliases,
|
||||
stats_output=stats_output
|
||||
)
|
||||
|
||||
+5
-5
@@ -23,7 +23,7 @@ I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) source
|
||||
|
||||
help:
|
||||
@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 " livehtml to run a persistent process that rebuilds the docs"
|
||||
@echo " dirhtml to make HTML files named index.html in directories"
|
||||
@@ -96,9 +96,9 @@ qthelp: build
|
||||
@echo
|
||||
@echo "Build finished; now you can run "qcollectiongenerator" with the" \
|
||||
".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 "# assistant -collectionFile $(BUILDDIR)/qthelp/zipline.qhc"
|
||||
@echo "# assistant -collectionFile $(BUILDDIR)/qthelp/catalyst.qhc"
|
||||
|
||||
applehelp: build
|
||||
$(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
|
||||
@@ -113,8 +113,8 @@ devhelp: build
|
||||
@echo
|
||||
@echo "Build finished."
|
||||
@echo "To view the help file:"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/zipline"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/zipline"
|
||||
@echo "# mkdir -p $$HOME/.local/share/devhelp/catalyst"
|
||||
@echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/catalyst"
|
||||
@echo "# devhelp"
|
||||
|
||||
epub: build
|
||||
|
||||
+5
-5
@@ -8,8 +8,8 @@ from shutil import move, rmtree
|
||||
from subprocess import check_call
|
||||
|
||||
HERE = dirname(abspath(__file__))
|
||||
ZIPLINE_ROOT = dirname(HERE)
|
||||
TEMP_LOCATION = '/tmp/zipline-doc'
|
||||
CATALYST_ROOT = dirname(HERE)
|
||||
TEMP_LOCATION = '/tmp/catalyst-doc'
|
||||
TEMP_LOCATION_GLOB = TEMP_LOCATION + '/*'
|
||||
|
||||
|
||||
@@ -46,8 +46,8 @@ def main():
|
||||
print("Copying built files to temp location.")
|
||||
move('build/html', TEMP_LOCATION)
|
||||
|
||||
print("Moving to '%s'" % ZIPLINE_ROOT)
|
||||
os.chdir(ZIPLINE_ROOT)
|
||||
print("Moving to '%s'" % CATALYST_ROOT)
|
||||
os.chdir(CATALYST_ROOT)
|
||||
|
||||
print("Checking out gh-pages branch.")
|
||||
check_call(
|
||||
@@ -70,7 +70,7 @@ def main():
|
||||
os.chdir(old_dir)
|
||||
|
||||
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.")
|
||||
|
||||
if __name__ == '__main__':
|
||||
|
||||
+2
-2
@@ -127,9 +127,9 @@ if "%1" == "qthelp" (
|
||||
echo.
|
||||
echo.Build finished; now you can run "qcollectiongenerator" with the ^
|
||||
.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.^> assistant -collectionFile %BUILDDIR%\qthelp\zipline.ghc
|
||||
echo.^> assistant -collectionFile %BUILDDIR%\qthelp\catalyst.ghc
|
||||
goto end
|
||||
)
|
||||
|
||||
|
||||
+173
-179
@@ -4,7 +4,7 @@ API Reference
|
||||
Running a Backtest
|
||||
~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autofunction:: zipline.run_algorithm(...)
|
||||
.. autofunction:: catalyst.run_algorithm(...)
|
||||
|
||||
Algorithm API
|
||||
~~~~~~~~~~~~~
|
||||
@@ -18,341 +18,335 @@ currently-executing :class:`~zipline.algorithm.TradingAlgorithm` instance.
|
||||
Data Object
|
||||
```````````
|
||||
|
||||
.. autoclass:: zipline.protocol.BarData
|
||||
.. autoclass:: catalyst.protocol.BarData
|
||||
:members:
|
||||
|
||||
Scheduling Functions
|
||||
````````````````````
|
||||
|
||||
.. autofunction:: zipline.api.schedule_function
|
||||
.. autofunction:: catalyst.api.schedule_function
|
||||
|
||||
.. autoclass:: zipline.api.date_rules
|
||||
.. autoclass:: catalyst.api.date_rules
|
||||
:members:
|
||||
:undoc-members:
|
||||
|
||||
.. autoclass:: zipline.api.time_rules
|
||||
.. autoclass:: catalyst.api.time_rules
|
||||
:members:
|
||||
|
||||
Orders
|
||||
``````
|
||||
|
||||
.. autofunction:: zipline.api.order
|
||||
.. autofunction:: catalyst.api.order
|
||||
|
||||
.. autofunction:: zipline.api.order_value
|
||||
.. autofunction:: catalyst.api.order_value
|
||||
|
||||
.. autofunction:: zipline.api.order_percent
|
||||
.. autofunction:: catalyst.api.order_percent
|
||||
|
||||
.. autofunction:: zipline.api.order_target
|
||||
.. autofunction:: catalyst.api.order_target
|
||||
|
||||
.. autofunction:: zipline.api.order_target_value
|
||||
.. autofunction:: catalyst.api.order_target_value
|
||||
|
||||
.. autofunction:: zipline.api.order_target_percent
|
||||
.. autofunction:: catalyst.api.order_target_percent
|
||||
|
||||
.. autoclass:: zipline.finance.execution.ExecutionStyle
|
||||
.. autoclass:: catalyst.finance.execution.ExecutionStyle
|
||||
:members:
|
||||
|
||||
.. autoclass:: zipline.finance.execution.MarketOrder
|
||||
.. autoclass:: catalyst.finance.execution.MarketOrder
|
||||
|
||||
.. autoclass:: zipline.finance.execution.LimitOrder
|
||||
.. autoclass:: catalyst.finance.execution.LimitOrder
|
||||
|
||||
.. autoclass:: zipline.finance.execution.StopOrder
|
||||
.. autoclass:: catalyst.finance.execution.StopOrder
|
||||
|
||||
.. autoclass:: zipline.finance.execution.StopLimitOrder
|
||||
.. autoclass:: catalyst.finance.execution.StopLimitOrder
|
||||
|
||||
.. autofunction:: zipline.api.get_order
|
||||
.. autofunction:: catalyst.api.get_order
|
||||
|
||||
.. autofunction:: zipline.api.get_open_orders
|
||||
.. autofunction:: catalyst.api.get_open_orders
|
||||
|
||||
.. autofunction:: zipline.api.cancel_order
|
||||
.. autofunction:: catalyst.api.cancel_order
|
||||
|
||||
Order Cancellation Policies
|
||||
'''''''''''''''''''''''''''
|
||||
|
||||
.. autofunction:: zipline.api.set_cancel_policy
|
||||
.. autofunction:: catalyst.api.set_cancel_policy
|
||||
|
||||
.. autoclass:: zipline.finance.cancel_policy.CancelPolicy
|
||||
.. autoclass:: catalyst.finance.cancel_policy.CancelPolicy
|
||||
:members:
|
||||
|
||||
.. autofunction:: zipline.api.EODCancel
|
||||
.. autofunction:: catalyst.api.EODCancel
|
||||
|
||||
.. autofunction:: zipline.api.NeverCancel
|
||||
.. autofunction:: catalyst.api.NeverCancel
|
||||
|
||||
|
||||
Assets
|
||||
``````
|
||||
|
||||
.. autofunction:: zipline.api.symbol
|
||||
.. autofunction:: catalyst.api.symbol
|
||||
|
||||
.. autofunction:: zipline.api.symbols
|
||||
.. autofunction:: catalyst.api.symbols
|
||||
|
||||
.. autofunction:: zipline.api.future_symbol
|
||||
.. autofunction:: catalyst.api.set_symbol_lookup_date
|
||||
|
||||
.. autofunction:: zipline.api.set_symbol_lookup_date
|
||||
|
||||
.. autofunction:: zipline.api.sid
|
||||
.. autofunction:: catalyst.api.sid
|
||||
|
||||
|
||||
Trading Controls
|
||||
````````````````
|
||||
|
||||
Zipline provides trading controls to help ensure that the algorithm is
|
||||
zipline provides trading controls to help ensure that the algorithm is
|
||||
performing as expected. The functions help protect the algorithm from certian
|
||||
bugs that could cause undesirable behavior when trading with real money.
|
||||
|
||||
.. autofunction:: zipline.api.set_do_not_order_list
|
||||
.. autofunction:: catalyst.api.set_do_not_order_list
|
||||
|
||||
.. autofunction:: zipline.api.set_long_only
|
||||
.. autofunction:: catalyst.api.set_long_only
|
||||
|
||||
.. autofunction:: zipline.api.set_max_leverage
|
||||
.. autofunction:: catalyst.api.set_max_leverage
|
||||
|
||||
.. autofunction:: zipline.api.set_max_order_count
|
||||
.. autofunction:: catalyst.api.set_max_order_count
|
||||
|
||||
.. autofunction:: zipline.api.set_max_order_size
|
||||
.. autofunction:: catalyst.api.set_max_order_size
|
||||
|
||||
.. autofunction:: zipline.api.set_max_position_size
|
||||
.. autofunction:: catalyst.api.set_max_position_size
|
||||
|
||||
|
||||
Simulation Parameters
|
||||
`````````````````````
|
||||
|
||||
.. autofunction:: zipline.api.set_benchmark
|
||||
.. autofunction:: catalyst.api.set_benchmark
|
||||
|
||||
Commission Models
|
||||
'''''''''''''''''
|
||||
|
||||
.. autofunction:: zipline.api.set_commission
|
||||
.. autofunction:: catalyst.api.set_commission
|
||||
|
||||
.. autoclass:: zipline.finance.commission.CommissionModel
|
||||
.. autoclass:: catalyst.finance.commission.CommissionModel
|
||||
:members:
|
||||
|
||||
.. autoclass:: zipline.finance.commission.PerShare
|
||||
.. autoclass:: catalyst.finance.commission.PerShare
|
||||
|
||||
.. autoclass:: zipline.finance.commission.PerTrade
|
||||
.. autoclass:: catalyst.finance.commission.PerTrade
|
||||
|
||||
.. autoclass:: zipline.finance.commission.PerDollar
|
||||
.. autoclass:: catalyst.finance.commission.PerDollar
|
||||
|
||||
Slippage Models
|
||||
'''''''''''''''
|
||||
|
||||
.. autofunction:: zipline.api.set_slippage
|
||||
.. autofunction:: catalyst.api.set_slippage
|
||||
|
||||
.. autoclass:: zipline.finance.slippage.SlippageModel
|
||||
.. autoclass:: catalyst.finance.slippage.SlippageModel
|
||||
:members:
|
||||
|
||||
.. autoclass:: zipline.finance.slippage.FixedSlippage
|
||||
.. autoclass:: catalyst.finance.slippage.FixedSlippage
|
||||
|
||||
.. autoclass:: zipline.finance.slippage.VolumeShareSlippage
|
||||
.. autoclass:: catalyst.finance.slippage.VolumeShareSlippage
|
||||
|
||||
Pipeline
|
||||
````````
|
||||
|
||||
For more information, see :ref:`pipeline-api`
|
||||
Not supported yet.
|
||||
|
||||
.. autofunction:: zipline.api.attach_pipeline
|
||||
.. For more information, see :ref:`pipeline-api`
|
||||
|
||||
.. autofunction:: zipline.api.pipeline_output
|
||||
.. .. autofunction:: catalyst.api.attach_pipeline
|
||||
|
||||
.. .. autofunction:: catalyst.api.pipeline_output
|
||||
|
||||
|
||||
Miscellaneous
|
||||
`````````````
|
||||
|
||||
.. autofunction:: zipline.api.record
|
||||
.. autofunction:: catalyst.api.record
|
||||
|
||||
.. autofunction:: zipline.api.get_environment
|
||||
.. autofunction:: catalyst.api.get_environment
|
||||
|
||||
.. autofunction:: zipline.api.fetch_csv
|
||||
.. autofunction:: catalyst.api.fetch_csv
|
||||
|
||||
|
||||
.. _pipeline-api:
|
||||
|
||||
Pipeline API
|
||||
~~~~~~~~~~~~
|
||||
.. Pipeline API
|
||||
.. ~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: zipline.pipeline.Pipeline
|
||||
:members:
|
||||
:member-order: groupwise
|
||||
.. .. autoclass:: zipline.pipeline.Pipeline
|
||||
.. :members:
|
||||
.. :member-order: groupwise
|
||||
|
||||
.. autoclass:: zipline.pipeline.CustomFactor
|
||||
:members:
|
||||
:member-order: groupwise
|
||||
.. .. autoclass:: zipline.pipeline.CustomFactor
|
||||
.. :members:
|
||||
.. :member-order: groupwise
|
||||
|
||||
.. autoclass:: zipline.pipeline.filters.Filter
|
||||
:members: __and__, __or__
|
||||
:exclude-members: dtype
|
||||
.. .. autoclass:: zipline.pipeline.filters.Filter
|
||||
.. :members: __and__, __or__
|
||||
.. :exclude-members: dtype
|
||||
|
||||
.. autoclass:: zipline.pipeline.factors.Factor
|
||||
:members: bottom, deciles, demean, linear_regression, pearsonr,
|
||||
percentile_between, quantiles, quartiles, quintiles, rank,
|
||||
spearmanr, top, winsorize, zscore, isnan, notnan, isfinite, eq,
|
||||
__add__, __sub__, __mul__, __div__, __mod__, __pow__, __lt__,
|
||||
__le__, __ne__, __ge__, __gt__
|
||||
:exclude-members: dtype
|
||||
:member-order: bysource
|
||||
.. .. autoclass:: zipline.pipeline.factors.Factor
|
||||
.. :members: bottom, deciles, demean, linear_regression, pearsonr,
|
||||
.. percentile_between, quantiles, quartiles, quintiles, rank,
|
||||
.. spearmanr, top, winsorize, zscore, isnan, notnan, isfinite, eq,
|
||||
.. \__add__, \__sub__, \__mul__, \__div__, \__mod__, \__pow__,
|
||||
.. \__lt__, \__le__, \__ne__, \__ge__, \__gt__
|
||||
.. :exclude-members: dtype
|
||||
.. :member-order: bysource
|
||||
|
||||
.. autoclass:: zipline.pipeline.term.Term
|
||||
:members:
|
||||
:exclude-members: compute_extra_rows, dependencies, inputs, mask, windowed
|
||||
.. .. autoclass:: zipline.pipeline.term.Term
|
||||
.. :members:
|
||||
.. :exclude-members: compute_extra_rows, dependencies, inputs, mask, windowed
|
||||
|
||||
.. autoclass:: zipline.pipeline.data.USEquityPricing
|
||||
:members: open, high, low, close, volume
|
||||
:undoc-members:
|
||||
.. .. autoclass:: zipline.pipeline.data.USEquityPricing
|
||||
.. :members: open, high, low, close, volume
|
||||
.. :undoc-members:
|
||||
|
||||
Built-in Factors
|
||||
````````````````
|
||||
.. Built-in Factors
|
||||
.. ````````````````
|
||||
|
||||
.. autoclass:: zipline.pipeline.factors.AverageDollarVolume
|
||||
:members:
|
||||
.. .. autoclass:: zipline.pipeline.factors.AverageDollarVolume
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.pipeline.factors.BollingerBands
|
||||
:members:
|
||||
.. .. autoclass:: zipline.pipeline.factors.BollingerBands
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.pipeline.factors.BusinessDaysSincePreviousEvent
|
||||
:members:
|
||||
.. .. autoclass:: zipline.pipeline.factors.BusinessDaysSincePreviousEvent
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.pipeline.factors.BusinessDaysUntilNextEvent
|
||||
:members:
|
||||
.. .. autoclass:: zipline.pipeline.factors.BusinessDaysUntilNextEvent
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.pipeline.factors.ExponentialWeightedMovingAverage
|
||||
:members:
|
||||
.. .. autoclass:: zipline.pipeline.factors.ExponentialWeightedMovingAverage
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.pipeline.factors.ExponentialWeightedMovingStdDev
|
||||
:members:
|
||||
.. .. autoclass:: zipline.pipeline.factors.ExponentialWeightedMovingStdDev
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.pipeline.factors.Latest
|
||||
:members:
|
||||
.. .. autoclass:: zipline.pipeline.factors.Latest
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.pipeline.factors.MaxDrawdown
|
||||
:members:
|
||||
.. .. autoclass:: zipline.pipeline.factors.MaxDrawdown
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.pipeline.factors.Returns
|
||||
:members:
|
||||
.. .. autoclass:: zipline.pipeline.factors.Returns
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.pipeline.factors.RollingLinearRegressionOfReturns
|
||||
:members:
|
||||
.. .. autoclass:: zipline.pipeline.factors.RollingLinearRegressionOfReturns
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.pipeline.factors.RollingPearsonOfReturns
|
||||
:members:
|
||||
.. .. autoclass:: zipline.pipeline.factors.RollingPearsonOfReturns
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.pipeline.factors.RollingSpearmanOfReturns
|
||||
:members:
|
||||
.. .. autoclass:: zipline.pipeline.factors.RollingSpearmanOfReturns
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.pipeline.factors.RSI
|
||||
:members:
|
||||
.. .. autoclass:: zipline.pipeline.factors.RSI
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.pipeline.factors.SimpleMovingAverage
|
||||
:members:
|
||||
.. .. autoclass:: zipline.pipeline.factors.SimpleMovingAverage
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.pipeline.factors.VWAP
|
||||
:members:
|
||||
.. .. autoclass:: zipline.pipeline.factors.VWAP
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.pipeline.factors.WeightedAverageValue
|
||||
:members:
|
||||
.. .. autoclass:: zipline.pipeline.factors.WeightedAverageValue
|
||||
.. :members:
|
||||
|
||||
Pipeline Engine
|
||||
```````````````
|
||||
.. Pipeline Engine
|
||||
.. ```````````````
|
||||
|
||||
.. autoclass:: zipline.pipeline.engine.PipelineEngine
|
||||
:members: run_pipeline, run_chunked_pipeline
|
||||
:member-order: bysource
|
||||
.. .. autoclass:: zipline.pipeline.engine.PipelineEngine
|
||||
.. :members: run_pipeline, run_chunked_pipeline
|
||||
.. :member-order: bysource
|
||||
|
||||
.. autoclass:: zipline.pipeline.engine.SimplePipelineEngine
|
||||
:members: __init__, run_pipeline, run_chunked_pipeline
|
||||
:member-order: bysource
|
||||
.. .. autoclass:: zipline.pipeline.engine.SimplePipelineEngine
|
||||
.. :members: __init__, run_pipeline, run_chunked_pipeline
|
||||
.. :member-order: bysource
|
||||
|
||||
.. autofunction:: zipline.pipeline.engine.default_populate_initial_workspace
|
||||
.. .. autofunction:: zipline.pipeline.engine.default_populate_initial_workspace
|
||||
|
||||
Data Loaders
|
||||
````````````
|
||||
.. Data Loaders
|
||||
.. ````````````
|
||||
|
||||
.. autoclass:: zipline.pipeline.loaders.equity_pricing_loader.USEquityPricingLoader
|
||||
:members: __init__, from_files, load_adjusted_array
|
||||
:member-order: bysource
|
||||
.. .. autoclass:: zipline.pipeline.loaders.equity_pricing_loader.USEquityPricingLoader
|
||||
.. :members: __init__, from_files, load_adjusted_array
|
||||
.. :member-order: bysource
|
||||
|
||||
Asset Metadata
|
||||
~~~~~~~~~~~~~~
|
||||
|
||||
.. autoclass:: zipline.assets.Asset
|
||||
.. autoclass:: catalyst.assets.Asset
|
||||
:members:
|
||||
|
||||
.. autoclass:: zipline.assets.Equity
|
||||
:members:
|
||||
|
||||
.. autoclass:: zipline.assets.Future
|
||||
:members:
|
||||
|
||||
.. autoclass:: zipline.assets.AssetConvertible
|
||||
.. autoclass:: catalyst.assets.AssetConvertible
|
||||
:members:
|
||||
|
||||
|
||||
Trading Calendar API
|
||||
~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
.. autofunction:: zipline.utils.calendars.get_calendar
|
||||
.. autofunction:: catalyst.utils.calendars.get_calendar
|
||||
|
||||
.. autoclass:: zipline.utils.calendars.TradingCalendar
|
||||
.. autoclass:: catalyst.utils.calendars.TradingCalendar
|
||||
:members:
|
||||
|
||||
.. autofunction:: zipline.utils.calendars.register_calendar
|
||||
.. autofunction:: catalyst.utils.calendars.register_calendar
|
||||
|
||||
.. autofunction:: zipline.utils.calendars.register_calendar_type
|
||||
.. autofunction:: catalyst.utils.calendars.register_calendar_type
|
||||
|
||||
.. autofunction:: zipline.utils.calendars.deregister_calendar
|
||||
.. autofunction:: catalyst.utils.calendars.deregister_calendar
|
||||
|
||||
.. autofunction:: zipline.utils.calendars.clear_calendars
|
||||
.. autofunction:: catalyst.utils.calendars.clear_calendars
|
||||
|
||||
|
||||
Data API
|
||||
~~~~~~~~
|
||||
|
||||
Writers
|
||||
```````
|
||||
.. autoclass:: zipline.data.minute_bars.BcolzMinuteBarWriter
|
||||
:members:
|
||||
.. Writers
|
||||
.. ```````
|
||||
.. .. autoclass:: zipline.data.minute_bars.BcolzMinuteBarWriter
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.data.us_equity_pricing.BcolzDailyBarWriter
|
||||
:members:
|
||||
.. .. autoclass:: zipline.data.us_equity_pricing.BcolzDailyBarWriter
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.data.us_equity_pricing.SQLiteAdjustmentWriter
|
||||
:members:
|
||||
.. .. autoclass:: zipline.data.us_equity_pricing.SQLiteAdjustmentWriter
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.assets.AssetDBWriter
|
||||
:members:
|
||||
.. .. autoclass:: zipline.assets.AssetDBWriter
|
||||
.. :members:
|
||||
|
||||
Readers
|
||||
```````
|
||||
.. autoclass:: zipline.data.minute_bars.BcolzMinuteBarReader
|
||||
:members:
|
||||
.. Readers
|
||||
.. ```````
|
||||
.. .. autoclass:: zipline.data.minute_bars.BcolzMinuteBarReader
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.data.us_equity_pricing.BcolzDailyBarReader
|
||||
:members:
|
||||
.. .. autoclass:: zipline.data.us_equity_pricing.BcolzDailyBarReader
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.data.us_equity_pricing.SQLiteAdjustmentReader
|
||||
:members:
|
||||
.. .. autoclass:: zipline.data.us_equity_pricing.SQLiteAdjustmentReader
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.assets.AssetFinder
|
||||
:members:
|
||||
.. .. autoclass:: zipline.assets.AssetFinder
|
||||
.. :members:
|
||||
|
||||
.. autoclass:: zipline.data.data_portal.DataPortal
|
||||
:members:
|
||||
.. .. autoclass:: zipline.data.data_portal.DataPortal
|
||||
.. :members:
|
||||
|
||||
Bundles
|
||||
```````
|
||||
.. autofunction:: zipline.data.bundles.register
|
||||
.. Bundles
|
||||
.. ```````
|
||||
.. .. autofunction:: zipline.data.bundles.register
|
||||
|
||||
.. autofunction:: zipline.data.bundles.ingest(name, environ=os.environ, date=None, show_progress=True)
|
||||
.. .. autofunction:: zipline.data.bundles.ingest(name, environ=os.environ, date=None, show_progress=True)
|
||||
|
||||
.. autofunction:: zipline.data.bundles.load(name, environ=os.environ, date=None)
|
||||
.. .. autofunction:: zipline.data.bundles.load(name, environ=os.environ, date=None)
|
||||
|
||||
.. autofunction:: zipline.data.bundles.unregister
|
||||
.. .. autofunction:: zipline.data.bundles.unregister
|
||||
|
||||
.. data:: zipline.data.bundles.bundles
|
||||
.. .. data:: zipline.data.bundles.bundles
|
||||
|
||||
The bundles that have been registered as a mapping from bundle name to bundle
|
||||
data. This mapping is immutable and should only be updated through
|
||||
:func:`~zipline.data.bundles.register` or
|
||||
:func:`~zipline.data.bundles.unregister`.
|
||||
.. The bundles that have been registered as a mapping from bundle name to bundle
|
||||
.. data. This mapping is immutable and should only be updated through
|
||||
.. :func:`~zipline.data.bundles.register` or
|
||||
.. :func:`~zipline.data.bundles.unregister`.
|
||||
|
||||
.. autofunction:: zipline.data.bundles.yahoo_equities
|
||||
.. .. autofunction:: zipline.data.bundles.yahoo_equities
|
||||
|
||||
|
||||
|
||||
@@ -362,16 +356,16 @@ Utilities
|
||||
Caching
|
||||
```````
|
||||
|
||||
.. autoclass:: zipline.utils.cache.CachedObject
|
||||
.. autoclass:: catalyst.utils.cache.CachedObject
|
||||
|
||||
.. autoclass:: zipline.utils.cache.ExpiringCache
|
||||
.. autoclass:: catalyst.utils.cache.ExpiringCache
|
||||
|
||||
.. autoclass:: zipline.utils.cache.dataframe_cache
|
||||
.. autoclass:: catalyst.utils.cache.dataframe_cache
|
||||
|
||||
.. autoclass:: zipline.utils.cache.working_file
|
||||
.. autoclass:: catalyst.utils.cache.working_file
|
||||
|
||||
.. autoclass:: zipline.utils.cache.working_dir
|
||||
.. autoclass:: catalyst.utils.cache.working_dir
|
||||
|
||||
Command Line
|
||||
````````````
|
||||
.. autofunction:: zipline.utils.cli.maybe_show_progress
|
||||
.. autofunction:: catalyst.utils.cli.maybe_show_progress
|
||||
|
||||
@@ -168,7 +168,7 @@ We'll start with the CLI, and introduce the ``run_algorithm()`` in the last
|
||||
example of this tutorial. Some of the :doc:`example algorithms <example-algos>`
|
||||
provide instructions on how to run them both from the CLI, and using the
|
||||
:func:`~catalyst.run_algorithm` function. For the third method, refer to the
|
||||
corresponding section on :doc:`Catalyst & Jupyter Notebook <jupyter>` after you
|
||||
corresponding section on :ref:`Catalyst & Jupyter Notebook <jupyter>` after you
|
||||
have assimilated the contents of this tutorial.
|
||||
|
||||
Command line interface
|
||||
@@ -473,6 +473,7 @@ Which we execute by running:
|
||||
</div>
|
||||
|
||||
|
|
||||
|
||||
There is a row for each trading day, starting on the first day of our
|
||||
simulation Jan 1st, 2016. In the columns you can find various
|
||||
information about the state of your algorithm. The column
|
||||
@@ -483,7 +484,7 @@ bitcoin price.
|
||||
|
||||
Now we will run the simulation again, but this time we extend our original
|
||||
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
|
||||
variables: ``context``, which we discussed at the very beginning, and ``perf``,
|
||||
which is the pandas dataframe containing the performance data for our algorithm
|
||||
@@ -518,7 +519,7 @@ alongside enigma-catalyst (with the exception of the ``Conda`` install, where it
|
||||
was included by default inside the conda environment we created). If for any
|
||||
reason you don't have it installed, you can add it by running:
|
||||
|
||||
.. code-block:: python
|
||||
.. code-block:: bash
|
||||
|
||||
(catalyst)$ pip install matplotlib
|
||||
|
||||
@@ -579,161 +580,8 @@ which you can skim through for now. A copy of this algorithm is available in
|
||||
the ``examples`` directory:
|
||||
`dual_moving_average.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/dual_moving_average.py>`_.
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from logbook import Logger
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from catalyst import run_algorithm
|
||||
from catalyst.api import (order, record, symbol, order_target_percent,
|
||||
get_open_orders)
|
||||
from catalyst.exchange.stats_utils import extract_transactions
|
||||
|
||||
NAMESPACE = 'dual_moving_average'
|
||||
log = Logger(NAMESPACE)
|
||||
|
||||
def initialize(context):
|
||||
context.i = 0
|
||||
context.asset = symbol('ltc_usd')
|
||||
context.base_price = None
|
||||
|
||||
|
||||
def handle_data(context, data):
|
||||
# define the windows for the moving averages
|
||||
short_window = 50
|
||||
long_window = 200
|
||||
|
||||
# Skip as many bars as long_window to properly compute the average
|
||||
context.i += 1
|
||||
if context.i < long_window:
|
||||
return
|
||||
|
||||
# Compute moving averages calling data.history() for each
|
||||
# moving average with the appropriate parameters. We choose to use
|
||||
# minute bars for this simulation -> freq="1m"
|
||||
# Returns a pandas dataframe.
|
||||
short_mavg = data.history(context.asset, 'price',
|
||||
bar_count=short_window, frequency="1m").mean()
|
||||
long_mavg = data.history(context.asset, 'price',
|
||||
bar_count=long_window, frequency="1m").mean()
|
||||
|
||||
# Let's keep the price of our asset in a more handy variable
|
||||
price = data.current(context.asset, 'price')
|
||||
|
||||
# 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 context.base_price is None:
|
||||
context.base_price = price
|
||||
price_change = (price - context.base_price) / context.base_price
|
||||
|
||||
# Save values for later inspection
|
||||
record(price=price,
|
||||
cash=context.portfolio.cash,
|
||||
price_change=price_change,
|
||||
short_mavg=short_mavg,
|
||||
long_mavg=long_mavg)
|
||||
|
||||
# Since we are using limit orders, some orders may not execute immediately
|
||||
# we wait until all orders are executed before considering more trades.
|
||||
orders = get_open_orders(context.asset)
|
||||
if len(orders) > 0:
|
||||
return
|
||||
|
||||
# Exit if we cannot trade
|
||||
if not data.can_trade(context.asset):
|
||||
return
|
||||
|
||||
# We check what's our position on our portfolio and trade accordingly
|
||||
pos_amount = context.portfolio.positions[context.asset].amount
|
||||
|
||||
# Trading logic
|
||||
if short_mavg > long_mavg and pos_amount == 0:
|
||||
# we buy 100% of our portfolio for this asset
|
||||
order_target_percent(context.asset, 1)
|
||||
elif short_mavg < long_mavg and pos_amount > 0:
|
||||
# we sell all our positions for this asset
|
||||
order_target_percent(context.asset, 0)
|
||||
|
||||
|
||||
def analyze(context, perf):
|
||||
|
||||
# Get the base_currency that was passed as a parameter to the simulation
|
||||
base_currency = context.exchanges.values()[0].base_currency.upper()
|
||||
|
||||
# First chart: Plot portfolio value using base_currency
|
||||
ax1 = plt.subplot(411)
|
||||
perf.loc[:, ['portfolio_value']].plot(ax=ax1)
|
||||
ax1.legend_.remove()
|
||||
ax1.set_ylabel('Portfolio Value\n({})'.format(base_currency))
|
||||
start, end = ax1.get_ylim()
|
||||
ax1.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
|
||||
|
||||
# Second chart: Plot asset price, moving averages and buys/sells
|
||||
ax2 = plt.subplot(412, sharex=ax1)
|
||||
perf.loc[:, ['price','short_mavg','long_mavg']].plot(ax=ax2, label='Price')
|
||||
ax2.legend_.remove()
|
||||
ax2.set_ylabel('{asset}\n({base})'.format(
|
||||
asset = context.asset.symbol,
|
||||
base = base_currency
|
||||
))
|
||||
start, end = ax2.get_ylim()
|
||||
ax2.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
|
||||
|
||||
transaction_df = extract_transactions(perf)
|
||||
if not transaction_df.empty:
|
||||
buy_df = transaction_df[transaction_df['amount'] > 0]
|
||||
sell_df = transaction_df[transaction_df['amount'] < 0]
|
||||
ax2.scatter(
|
||||
buy_df.index.to_pydatetime(),
|
||||
perf.loc[buy_df.index, 'price'],
|
||||
marker='^',
|
||||
s=100,
|
||||
c='green',
|
||||
label=''
|
||||
)
|
||||
ax2.scatter(
|
||||
sell_df.index.to_pydatetime(),
|
||||
perf.loc[sell_df.index, 'price'],
|
||||
marker='v',
|
||||
s=100,
|
||||
c='red',
|
||||
label=''
|
||||
)
|
||||
|
||||
# Third chart: Compare percentage change between our portfolio
|
||||
# and the price of the asset
|
||||
ax3 = plt.subplot(413, sharex=ax1)
|
||||
perf.loc[:, ['algorithm_period_return', 'price_change']].plot(ax=ax3)
|
||||
ax3.legend_.remove()
|
||||
ax3.set_ylabel('Percent Change')
|
||||
start, end = ax3.get_ylim()
|
||||
ax3.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
|
||||
|
||||
# Fourth chart: Plot our cash
|
||||
ax4 = plt.subplot(414, sharex=ax1)
|
||||
perf.cash.plot(ax=ax4)
|
||||
ax4.set_ylabel('Cash\n({})'.format(base_currency))
|
||||
start, end = ax4.get_ylim()
|
||||
ax4.yaxis.set_ticks(np.arange(0, end, end/5))
|
||||
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_algorithm(
|
||||
capital_base=1000,
|
||||
data_frequency='minute',
|
||||
initialize=initialize,
|
||||
handle_data=handle_data,
|
||||
analyze=analyze,
|
||||
exchange_name='bitfinex',
|
||||
algo_namespace=NAMESPACE,
|
||||
base_currency='usd',
|
||||
start=pd.to_datetime('2017-9-22', utc=True),
|
||||
end=pd.to_datetime('2017-9-23', utc=True),
|
||||
)
|
||||
.. literalinclude:: ../../catalyst/examples/dual_moving_average.py
|
||||
:language: python
|
||||
|
||||
In order to run the code above, you have to ingest the needed data first:
|
||||
|
||||
@@ -805,6 +653,7 @@ the ``scikit-learn`` functions require ``numpy.ndarray``\ s rather than
|
||||
``pandas.DataFrame``\ s, so you can simply pass the underlying
|
||||
``ndarray`` of a ``DataFrame`` via ``.values``).
|
||||
|
||||
.. _jupyter:
|
||||
|
||||
Jupyter Notebook
|
||||
~~~~~~~~~~~~~~~~
|
||||
@@ -825,13 +674,13 @@ In order to use Jupyter Notebook, you first have to install it inside your
|
||||
environment. It's available as ``pip`` package, so regardless of how you
|
||||
installed Catalyst, go inside your catalyst environemnt and run:
|
||||
|
||||
.. code:: bash
|
||||
.. code-block:: bash
|
||||
|
||||
(catalyst)$ pip install jupyter
|
||||
|
||||
Once you have Jupyter Notebook installed, every time you want to use it run:
|
||||
|
||||
.. code:: bash
|
||||
.. code-block:: bash
|
||||
|
||||
(catalyst)$ jupyter notebook
|
||||
|
||||
@@ -845,7 +694,7 @@ Before running your algorithms inside the Jupyter Notebook, remember to ingest
|
||||
the data from the command line interface (CLI). In the example below, you would
|
||||
need to run first:
|
||||
|
||||
.. code:: bash
|
||||
.. code-block:: bash
|
||||
|
||||
catalyst ingest-exchange -x bitfinex -i btc_usd
|
||||
|
||||
@@ -16606,7 +16455,49 @@ NaN
|
||||
|
||||
</div>
|
||||
|
||||
PyCharm IDE
|
||||
~~~~~~~~~~~
|
||||
|
||||
PyCharm is an Integrated Development Environment (IDE) used in computer
|
||||
programming, specifically for the Python language. It streamlines the continuos
|
||||
development of Python code, and among other things includes a debugger that
|
||||
comes in handy to see the inner workings of Catalyst, and your trading
|
||||
algorithms.
|
||||
|
||||
Install
|
||||
^^^^^^^
|
||||
Install PyCharm from their `Website <https://www.jetbrains.com/pycharm/download/>`__.
|
||||
There is a free and open-source **Community** version.
|
||||
|
||||
Setup
|
||||
^^^^^
|
||||
|
||||
1. When creating a new project in PyCharm, right under you specify the Location,
|
||||
click on **Project Interpreter** to display a drop down menu
|
||||
|
||||
2. Select **Existing interpreter**, click the gear box right next to it and
|
||||
select 'add local'. Depending on your installation, select either
|
||||
"*Virtual Environemnt*" or "*Conda Environment" and click the '...' button to
|
||||
navigate to your catalyst env and select the Python binary file:
|
||||
``bin/python`` for Linux/MacOS installations or 'python.exe' for Windows
|
||||
installs (for example: 'C:\\Users\\user\\Anaconda2\\envs\\catalyst\\python.exe').
|
||||
Select OK. You may want to click on *Make available to all projects* for your
|
||||
future reference. Click OK again, and create your new environment using the
|
||||
set up of your virtual environment.
|
||||
|
||||
Alternatively, if you already have your project created, in Windows do:
|
||||
|
||||
1. File -> Default Settings -> Project Interpreter. Click the gear box next to
|
||||
the project interpreter and select ‘add local’, and follow the steps from the
|
||||
second step above.
|
||||
|
||||
On MacOS:
|
||||
|
||||
1. PyCharm -> Preferences -> Settings -> Project:’NAME_OF_PROJECT’ ->
|
||||
Project Interpreter. Click the gear box next to the project interpreter
|
||||
and select ‘add local’, and follow the steps from the second step above.
|
||||
|
||||
You should now be able to run your project/scripts in PyCharm.
|
||||
|
||||
Next steps
|
||||
~~~~~~~~~~
|
||||
|
||||
+7
-4
@@ -27,8 +27,8 @@ extlinks = {
|
||||
|
||||
# -- Docstrings ---------------------------------------------------------------
|
||||
|
||||
#extensions += ['numpydoc']
|
||||
#numpydoc_show_class_members = False
|
||||
extensions += ['numpydoc']
|
||||
numpydoc_show_class_members = False
|
||||
|
||||
# Add any paths that contain templates here, relative to this directory.
|
||||
templates_path = ['.templates']
|
||||
@@ -41,11 +41,11 @@ master_doc = 'index'
|
||||
|
||||
# General information about the project.
|
||||
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
|
||||
#release = version.split('+', 1)[0]
|
||||
release = '0.3'
|
||||
release = '0.4'
|
||||
|
||||
# List of patterns, relative to source directory, that match files and
|
||||
# directories to ignore when looking for source files.
|
||||
@@ -97,3 +97,6 @@ intersphinx_mapping = {
|
||||
doctest_global_setup = "import catalyst"
|
||||
|
||||
todo_include_todos = True
|
||||
|
||||
suppress_warnings = ['image.nonlocal_uri']
|
||||
|
||||
|
||||
@@ -36,25 +36,15 @@ Finally, you can build the C extensions by running:
|
||||
|
||||
$ python setup.py build_ext --inplace
|
||||
|
||||
.. To finish, make sure `tests`__ pass.
|
||||
Development with Docker
|
||||
-----------------------
|
||||
|
||||
.. __ #style-guide-running-tests
|
||||
If you want to work with zipline using a `Docker`__ container, you'll need to
|
||||
build the ``Dockerfile`` in the Zipline root directory, and then build
|
||||
``Dockerfile-dev``. Instructions for building both containers can be found in
|
||||
``Dockerfile`` and ``Dockerfile-dev``, respectively.
|
||||
|
||||
.. If you get an error running nosetests after setting up a fresh virtualenv, please try running
|
||||
|
||||
.. code-block
|
||||
|
||||
.. # where zipline is the name of your virtualenv
|
||||
.. $ deactivate zipline
|
||||
.. $ workon zipline
|
||||
|
||||
|
||||
.. Development with Docker
|
||||
.. -----------------------
|
||||
|
||||
..If you want to work with zipline using a `Docker`__ container, you'll need to build the ``Dockerfile`` in the Zipline root directory, and then build ``Dockerfile-dev``. Instructions for building both containers can be found in ``Dockerfile`` and ``Dockerfile-dev``, respectively.
|
||||
|
||||
.. __ https://docs.docker.com/get-started/
|
||||
__ https://docs.docker.com/get-started/
|
||||
|
||||
Git Branching Structure
|
||||
-----------------------
|
||||
@@ -84,6 +74,25 @@ To build and view the docs locally, run:
|
||||
$ {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
|
||||
---------------
|
||||
|
||||
|
||||
+18
-881
@@ -1,4 +1,5 @@
|
||||
|
|
||||
|
||||
Example Algorithms
|
||||
==================
|
||||
|
||||
@@ -51,35 +52,8 @@ Buy BTC Simple Algorithm
|
||||
|
||||
Source code: `examples/buy_btc_simple.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/buy_btc_simple.py>`_
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
'''
|
||||
Run this example, by executing the following from your terminal:
|
||||
catalyst ingest-exchange -x bitfinex -f daily -i btc_usdt
|
||||
catalyst run -f buy_btc_simple.py -x bitfinex --start 2016-1-1 --end 2017-9-30 -o buy_btc_simple_out.pickle
|
||||
|
||||
If you want to run this code using another exchange, make sure that
|
||||
the asset is available on that exchange. For example, if you were to run
|
||||
it for exchange Poloniex, you would need to edit the following line:
|
||||
|
||||
context.asset = symbol('btc_usdt') # note 'usdt' instead of 'usd'
|
||||
|
||||
and specify exchange poloniex as follows:
|
||||
catalyst ingest-exchange -x poloniex -f daily -i btc_usdt
|
||||
catalyst run -f buy_btc_simple.py -x poloniex --start 2016-1-1 --end 2017-9-30 -o buy_btc_simple_out.pickle
|
||||
|
||||
To see which assets are available on each exchange, visit:
|
||||
https://www.enigma.co/catalyst/status
|
||||
'''
|
||||
|
||||
from catalyst.api import order, record, symbol
|
||||
|
||||
def initialize(context):
|
||||
context.asset = symbol('btc_usd')
|
||||
|
||||
def handle_data(context, data):
|
||||
order(context.asset, 1)
|
||||
record(btc = data.current(context.asset, 'price'))
|
||||
.. literalinclude:: ../../catalyst/examples/buy_btc_simple.py
|
||||
:language: python
|
||||
|
||||
This simple algorithm does not produce any output nor displays any chart.
|
||||
|
||||
@@ -89,8 +63,6 @@ This simple algorithm does not produce any output nor displays any chart.
|
||||
Buy and Hodl Algorithm
|
||||
~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Source code: `examples/buy_and_hodl.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/buy_and_hodl.py>`_
|
||||
|
||||
First ingest the historical pricing data needed to run this algorithm:
|
||||
|
||||
.. code-block:: bash
|
||||
@@ -118,157 +90,10 @@ that 2015-3-1 is the earliest date that Catalyst supports (if you choose an
|
||||
earlier date, you'll get an error), and the most recent date you can choose is
|
||||
one day prior to the current date.
|
||||
|
||||
Source code: `examples/buy_and_hodl.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/buy_and_hodl.py>`_
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
#!/usr/bin/env python
|
||||
#
|
||||
# Copyright 2017 Enigma MPC, Inc.
|
||||
# Copyright 2015 Quantopian, Inc.
|
||||
#
|
||||
# Licensed under the Apache License, Version 2.0 (the "License");
|
||||
# you may not use this file except in compliance with the License.
|
||||
# You may obtain a copy of the License at
|
||||
#
|
||||
# http://www.apache.org/licenses/LICENSE-2.0
|
||||
#
|
||||
# Unless required by applicable law or agreed to in writing, software
|
||||
# distributed under the License is distributed on an "AS IS" BASIS,
|
||||
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
# See the License for the specific language governing permissions and
|
||||
# limitations under the License.
|
||||
import pandas as pd
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from catalyst import run_algorithm
|
||||
from catalyst.api import (order_target_value, symbol, record,
|
||||
cancel_order, get_open_orders, )
|
||||
|
||||
|
||||
def initialize(context):
|
||||
context.ASSET_NAME = 'btc_usd'
|
||||
context.TARGET_HODL_RATIO = 0.8
|
||||
context.RESERVE_RATIO = 1.0 - context.TARGET_HODL_RATIO
|
||||
|
||||
context.is_buying = True
|
||||
context.asset = symbol(context.ASSET_NAME)
|
||||
|
||||
context.i = 0
|
||||
|
||||
|
||||
def handle_data(context, data):
|
||||
context.i += 1
|
||||
|
||||
starting_cash = context.portfolio.starting_cash
|
||||
target_hodl_value = context.TARGET_HODL_RATIO * starting_cash
|
||||
reserve_value = context.RESERVE_RATIO * starting_cash
|
||||
|
||||
# Cancel any outstanding orders
|
||||
orders = get_open_orders(context.asset) or []
|
||||
for order in orders:
|
||||
cancel_order(order)
|
||||
|
||||
# Stop buying after passing the reserve threshold
|
||||
cash = context.portfolio.cash
|
||||
if cash <= reserve_value:
|
||||
context.is_buying = False
|
||||
|
||||
# Retrieve current asset price from pricing data
|
||||
price = data.current(context.asset, 'price')
|
||||
|
||||
# Check if still buying and could (approximately) afford another purchase
|
||||
if context.is_buying and cash > price:
|
||||
print('buying')
|
||||
# Place order to make position in asset equal to target_hodl_value
|
||||
order_target_value(
|
||||
context.asset,
|
||||
target_hodl_value,
|
||||
limit_price=price * 1.1,
|
||||
)
|
||||
|
||||
record(
|
||||
price=price,
|
||||
volume=data.current(context.asset, 'volume'),
|
||||
cash=cash,
|
||||
starting_cash=context.portfolio.starting_cash,
|
||||
leverage=context.account.leverage,
|
||||
)
|
||||
|
||||
|
||||
def analyze(context=None, results=None):
|
||||
|
||||
# Plot the portfolio and asset data.
|
||||
ax1 = plt.subplot(611)
|
||||
results[['portfolio_value']].plot(ax=ax1)
|
||||
ax1.set_ylabel('Portfolio Value (USD)')
|
||||
|
||||
ax2 = plt.subplot(612, sharex=ax1)
|
||||
ax2.set_ylabel('{asset} (USD)'.format(asset=context.ASSET_NAME))
|
||||
results[['price']].plot(ax=ax2)
|
||||
|
||||
trans = results.ix[[t != [] for t in results.transactions]]
|
||||
buys = trans.ix[
|
||||
[t[0]['amount'] > 0 for t in trans.transactions]
|
||||
]
|
||||
ax2.scatter(
|
||||
buys.index.to_pydatetime(),
|
||||
results.price[buys.index],
|
||||
marker='^',
|
||||
s=100,
|
||||
c='g',
|
||||
label=''
|
||||
)
|
||||
|
||||
ax3 = plt.subplot(613, sharex=ax1)
|
||||
results[['leverage', 'alpha', 'beta']].plot(ax=ax3)
|
||||
ax3.set_ylabel('Leverage ')
|
||||
|
||||
ax4 = plt.subplot(614, sharex=ax1)
|
||||
results[['starting_cash', 'cash']].plot(ax=ax4)
|
||||
ax4.set_ylabel('Cash (USD)')
|
||||
|
||||
results[[
|
||||
'treasury',
|
||||
'algorithm',
|
||||
'benchmark',
|
||||
]] = results[[
|
||||
'treasury_period_return',
|
||||
'algorithm_period_return',
|
||||
'benchmark_period_return',
|
||||
]]
|
||||
|
||||
ax5 = plt.subplot(615, sharex=ax1)
|
||||
results[[
|
||||
'treasury',
|
||||
'algorithm',
|
||||
'benchmark',
|
||||
]].plot(ax=ax5)
|
||||
ax5.set_ylabel('Percent Change')
|
||||
|
||||
ax6 = plt.subplot(616, sharex=ax1)
|
||||
results[['volume']].plot(ax=ax6)
|
||||
ax6.set_ylabel('Volume (mCoins/5min)')
|
||||
|
||||
plt.legend(loc=3)
|
||||
|
||||
# Show the plot.
|
||||
plt.gcf().set_size_inches(18, 8)
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_algorithm(
|
||||
capital_base=10000,
|
||||
data_frequency='daily',
|
||||
initialize=initialize,
|
||||
handle_data=handle_data,
|
||||
analyze=analyze,
|
||||
exchange_name='bitfinex',
|
||||
algo_namespace='buy_and_hodl',
|
||||
base_currency='usd',
|
||||
start=pd.to_datetime('2015-03-01', utc=True),
|
||||
end=pd.to_datetime('2017-10-31', utc=True),
|
||||
)
|
||||
.. literalinclude:: ../../catalyst/examples/buy_and_hodl.py
|
||||
:language: python
|
||||
|
||||
.. image:: https://s3.amazonaws.com/enigmaco-docs/github.io/example_buy_and_hodl.png
|
||||
|
||||
@@ -277,166 +102,13 @@ one day prior to the current date.
|
||||
Dual Moving Average Crossover
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Source Code: `examples/dual_moving_average.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/dual_moving_average.py>`_
|
||||
|
||||
This strategy is covered in detail in the last part of
|
||||
`this tutorial <beginner-tutorial.html#history>`_.
|
||||
|
||||
.. code-block:: python
|
||||
Source Code: `examples/dual_moving_average.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/dual_moving_average.py>`_
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from logbook import Logger
|
||||
import matplotlib.pyplot as plt
|
||||
|
||||
from catalyst import run_algorithm
|
||||
from catalyst.api import (order, record, symbol, order_target_percent,
|
||||
get_open_orders)
|
||||
from catalyst.exchange.stats_utils import extract_transactions
|
||||
|
||||
NAMESPACE = 'dual_moving_average'
|
||||
log = Logger(NAMESPACE)
|
||||
|
||||
def initialize(context):
|
||||
context.i = 0
|
||||
context.asset = symbol('ltc_usd')
|
||||
context.base_price = None
|
||||
|
||||
|
||||
def handle_data(context, data):
|
||||
# define the windows for the moving averages
|
||||
short_window = 50
|
||||
long_window = 200
|
||||
|
||||
# Skip as many bars as long_window to properly compute the average
|
||||
context.i += 1
|
||||
if context.i < long_window:
|
||||
return
|
||||
|
||||
# Compute moving averages calling data.history() for each
|
||||
# moving average with the appropriate parameters. We choose to use
|
||||
# minute bars for this simulation -> freq="1m"
|
||||
# Returns a pandas dataframe.
|
||||
short_mavg = data.history(context.asset, 'price',
|
||||
bar_count=short_window, frequency="1m").mean()
|
||||
long_mavg = data.history(context.asset, 'price',
|
||||
bar_count=long_window, frequency="1m").mean()
|
||||
|
||||
# Let's keep the price of our asset in a more handy variable
|
||||
price = data.current(context.asset, 'price')
|
||||
|
||||
# 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 context.base_price is None:
|
||||
context.base_price = price
|
||||
price_change = (price - context.base_price) / context.base_price
|
||||
|
||||
# Save values for later inspection
|
||||
record(price=price,
|
||||
cash=context.portfolio.cash,
|
||||
price_change=price_change,
|
||||
short_mavg=short_mavg,
|
||||
long_mavg=long_mavg)
|
||||
|
||||
# Since we are using limit orders, some orders may not execute immediately
|
||||
# we wait until all orders are executed before considering more trades.
|
||||
orders = get_open_orders(context.asset)
|
||||
if len(orders) > 0:
|
||||
return
|
||||
|
||||
# Exit if we cannot trade
|
||||
if not data.can_trade(context.asset):
|
||||
return
|
||||
|
||||
# We check what's our position on our portfolio and trade accordingly
|
||||
pos_amount = context.portfolio.positions[context.asset].amount
|
||||
|
||||
# Trading logic
|
||||
if short_mavg > long_mavg and pos_amount == 0:
|
||||
# we buy 100% of our portfolio for this asset
|
||||
order_target_percent(context.asset, 1)
|
||||
elif short_mavg < long_mavg and pos_amount > 0:
|
||||
# we sell all our positions for this asset
|
||||
order_target_percent(context.asset, 0)
|
||||
|
||||
|
||||
def analyze(context, perf):
|
||||
|
||||
# Get the base_currency that was passed as a parameter to the simulation
|
||||
base_currency = context.exchanges.values()[0].base_currency.upper()
|
||||
|
||||
# First chart: Plot portfolio value using base_currency
|
||||
ax1 = plt.subplot(411)
|
||||
perf.loc[:, ['portfolio_value']].plot(ax=ax1)
|
||||
ax1.legend_.remove()
|
||||
ax1.set_ylabel('Portfolio Value\n({})'.format(base_currency))
|
||||
start, end = ax1.get_ylim()
|
||||
ax1.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
|
||||
|
||||
# Second chart: Plot asset price, moving averages and buys/sells
|
||||
ax2 = plt.subplot(412, sharex=ax1)
|
||||
perf.loc[:, ['price','short_mavg','long_mavg']].plot(ax=ax2, label='Price')
|
||||
ax2.legend_.remove()
|
||||
ax2.set_ylabel('{asset}\n({base})'.format(
|
||||
asset = context.asset.symbol,
|
||||
base = base_currency
|
||||
))
|
||||
start, end = ax2.get_ylim()
|
||||
ax2.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
|
||||
|
||||
transaction_df = extract_transactions(perf)
|
||||
if not transaction_df.empty:
|
||||
buy_df = transaction_df[transaction_df['amount'] > 0]
|
||||
sell_df = transaction_df[transaction_df['amount'] < 0]
|
||||
ax2.scatter(
|
||||
buy_df.index.to_pydatetime(),
|
||||
perf.loc[buy_df.index, 'price'],
|
||||
marker='^',
|
||||
s=100,
|
||||
c='green',
|
||||
label=''
|
||||
)
|
||||
ax2.scatter(
|
||||
sell_df.index.to_pydatetime(),
|
||||
perf.loc[sell_df.index, 'price'],
|
||||
marker='v',
|
||||
s=100,
|
||||
c='red',
|
||||
label=''
|
||||
)
|
||||
|
||||
# Third chart: Compare percentage change between our portfolio
|
||||
# and the price of the asset
|
||||
ax3 = plt.subplot(413, sharex=ax1)
|
||||
perf.loc[:, ['algorithm_period_return', 'price_change']].plot(ax=ax3)
|
||||
ax3.legend_.remove()
|
||||
ax3.set_ylabel('Percent Change')
|
||||
start, end = ax3.get_ylim()
|
||||
ax3.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
|
||||
|
||||
# Fourth chart: Plot our cash
|
||||
ax4 = plt.subplot(414, sharex=ax1)
|
||||
perf.cash.plot(ax=ax4)
|
||||
ax4.set_ylabel('Cash\n({})'.format(base_currency))
|
||||
start, end = ax4.get_ylim()
|
||||
ax4.yaxis.set_ticks(np.arange(0, end, end/5))
|
||||
|
||||
plt.show()
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
run_algorithm(
|
||||
capital_base=1000,
|
||||
data_frequency='minute',
|
||||
initialize=initialize,
|
||||
handle_data=handle_data,
|
||||
analyze=analyze,
|
||||
exchange_name='bitfinex',
|
||||
algo_namespace=NAMESPACE,
|
||||
base_currency='usd',
|
||||
start=pd.to_datetime('2017-9-22', utc=True),
|
||||
end=pd.to_datetime('2017-9-23', utc=True),
|
||||
)
|
||||
.. literalinclude:: ../../catalyst/examples/dual_moving_average.py
|
||||
:language: python
|
||||
|
||||
.. image:: https://s3.amazonaws.com/enigmaco-docs/github.io/tutorial_dual_moving_average.png
|
||||
|
||||
@@ -446,8 +118,6 @@ This strategy is covered in detail in the last part of
|
||||
Mean Reversion Algorithm
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Source code: `examples/mean_reversion_simple.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/mean_reversion_simple.py>`_
|
||||
|
||||
This algorithm is based on a simple momentum strategy. When the cryptoasset goes
|
||||
up quickly, we're going to buy; when it goes down quickly, we're going to sell.
|
||||
Hopefully, we'll ride the waves.
|
||||
@@ -468,284 +138,10 @@ lines 218-245, so in order to run the algorithm we just type:
|
||||
|
||||
python mean_reversion_simple.py
|
||||
|
||||
.. code-block:: python
|
||||
Source code: `examples/mean_reversion_simple.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/mean_reversion_simple.py>`_
|
||||
|
||||
import os
|
||||
import tempfile
|
||||
import time
|
||||
|
||||
import numpy as np
|
||||
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_open_orders
|
||||
from catalyst.exchange.stats_utils import extract_transactions
|
||||
# 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 USD.
|
||||
context.neo_eth = symbol('neo_usd')
|
||||
context.base_price = None
|
||||
context.current_day = None
|
||||
|
||||
context.RSI_OVERSOLD = 30
|
||||
context.RSI_OVERBOUGHT = 80
|
||||
context.CANDLE_SIZE = '15T'
|
||||
|
||||
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 = False
|
||||
context.current_day = today
|
||||
|
||||
# We're computing the volume-weighted-average-price of the security
|
||||
# defined above, in the context.neo_eth 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(
|
||||
context.neo_eth,
|
||||
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(context.neo_eth, 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 context.base_price is None:
|
||||
context.base_price = price
|
||||
|
||||
price_change = (price - context.base_price) / context.base_price
|
||||
cash = context.portfolio.cash
|
||||
|
||||
# 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(
|
||||
price=price,
|
||||
volume=current['volume'],
|
||||
price_change=price_change,
|
||||
rsi=rsi[-1],
|
||||
cash=cash
|
||||
)
|
||||
|
||||
# We are trying to avoid over-trading by limiting our trades to
|
||||
# one per day.
|
||||
if context.traded_today:
|
||||
return
|
||||
|
||||
# Since we are using limit orders, some orders may not execute immediately
|
||||
# we wait until all orders are executed before considering more trades.
|
||||
orders = get_open_orders(context.neo_eth)
|
||||
if len(orders) > 0:
|
||||
return
|
||||
|
||||
# Exit if we cannot trade
|
||||
if not data.can_trade(context.neo_eth):
|
||||
return
|
||||
|
||||
# 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[context.neo_eth].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
|
||||
order_target_percent(
|
||||
context.neo_eth, 1, limit_price=limit_price
|
||||
)
|
||||
context.traded_today = 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(
|
||||
context.neo_eth, 0, limit_price=limit_price
|
||||
)
|
||||
context.traded_today = True
|
||||
|
||||
|
||||
def analyze(context=None, perf=None):
|
||||
end = time.time()
|
||||
log.info('elapsed time: {}'.format(end - context.start_time))
|
||||
|
||||
import matplotlib.pyplot as plt
|
||||
# The base currency of the algo exchange
|
||||
base_currency = context.exchanges.values()[0].base_currency.upper()
|
||||
|
||||
# Plot the portfolio value over time.
|
||||
ax1 = plt.subplot(611)
|
||||
perf.loc[:, 'portfolio_value'].plot(ax=ax1)
|
||||
ax1.set_ylabel('Portfolio\nValue\n({})'.format(base_currency))
|
||||
|
||||
# Plot the price increase or decrease over time.
|
||||
ax2 = plt.subplot(612, sharex=ax1)
|
||||
perf.loc[:, 'price'].plot(ax=ax2, label='Price')
|
||||
|
||||
ax2.set_ylabel('{asset}\n({base})'.format(
|
||||
asset=context.neo_eth.symbol, base=base_currency
|
||||
))
|
||||
|
||||
transaction_df = extract_transactions(perf)
|
||||
if not transaction_df.empty:
|
||||
buy_df = transaction_df[transaction_df['amount'] > 0]
|
||||
sell_df = transaction_df[transaction_df['amount'] < 0]
|
||||
ax2.scatter(
|
||||
buy_df.index.to_pydatetime(),
|
||||
perf.loc[buy_df.index.floor('1 min'), 'price'],
|
||||
marker='^',
|
||||
s=100,
|
||||
c='green',
|
||||
label=''
|
||||
)
|
||||
ax2.scatter(
|
||||
sell_df.index.to_pydatetime(),
|
||||
perf.loc[sell_df.index.floor('1 min'), 'price'],
|
||||
marker='v',
|
||||
s=100,
|
||||
c='red',
|
||||
label=''
|
||||
)
|
||||
|
||||
ax4 = plt.subplot(613, sharex=ax1)
|
||||
perf.loc[:, 'cash'].plot(
|
||||
ax=ax4, label='Base Currency ({})'.format(base_currency)
|
||||
)
|
||||
ax4.set_ylabel('Cash\n({})'.format(base_currency))
|
||||
|
||||
perf['algorithm'] = perf.loc[:, 'algorithm_period_return']
|
||||
|
||||
ax5 = plt.subplot(614, sharex=ax1)
|
||||
perf.loc[:, ['algorithm', 'price_change']].plot(ax=ax5)
|
||||
ax5.set_ylabel('Percent\nChange')
|
||||
|
||||
ax6 = plt.subplot(615, sharex=ax1)
|
||||
perf.loc[:, 'rsi'].plot(ax=ax6, label='RSI')
|
||||
ax6.set_ylabel('RSI')
|
||||
ax6.axhline(context.RSI_OVERBOUGHT, color='darkgoldenrod')
|
||||
ax6.axhline(context.RSI_OVERSOLD, color='darkgoldenrod')
|
||||
|
||||
if not transaction_df.empty:
|
||||
ax6.scatter(
|
||||
buy_df.index.to_pydatetime(),
|
||||
perf.loc[buy_df.index.floor('1 min'), 'rsi'],
|
||||
marker='^',
|
||||
s=100,
|
||||
c='green',
|
||||
label=''
|
||||
)
|
||||
ax6.scatter(
|
||||
sell_df.index.to_pydatetime(),
|
||||
perf.loc[sell_df.index.floor('1 min'), 'rsi'],
|
||||
marker='v',
|
||||
s=100,
|
||||
c='red',
|
||||
label=''
|
||||
)
|
||||
plt.legend(loc=3)
|
||||
start, end = ax6.get_ylim()
|
||||
ax6.yaxis.set_ticks(np.arange(0, end, end/5))
|
||||
|
||||
# Show the plot.
|
||||
plt.gcf().set_size_inches(18, 8)
|
||||
plt.show()
|
||||
pass
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
# The execution mode: backtest or live
|
||||
MODE = 'backtest'
|
||||
|
||||
if MODE == 'backtest':
|
||||
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=10000,
|
||||
data_frequency='minute',
|
||||
initialize=initialize,
|
||||
handle_data=handle_data,
|
||||
analyze=analyze,
|
||||
exchange_name='bitfinex',
|
||||
algo_namespace=NAMESPACE,
|
||||
base_currency='usd',
|
||||
start=pd.to_datetime('2017-10-01', utc=True),
|
||||
end=pd.to_datetime('2017-11-10', utc=True),
|
||||
output=out
|
||||
)
|
||||
log.info('saved perf stats: {}'.format(out))
|
||||
|
||||
elif MODE == 'live':
|
||||
run_algorithm(
|
||||
capital_base=0.5,
|
||||
initialize=initialize,
|
||||
handle_data=handle_data,
|
||||
analyze=analyze,
|
||||
exchange_name='bittrex',
|
||||
live=True,
|
||||
algo_namespace=NAMESPACE,
|
||||
base_currency='usd',
|
||||
live_graph=False
|
||||
)
|
||||
.. literalinclude:: ../../catalyst/examples/mean_reversion_simple.py
|
||||
:language: python
|
||||
|
||||
.. image:: https://s3.amazonaws.com/enigmaco-docs/github.io/example_mean_reversion_simple.png
|
||||
|
||||
@@ -762,8 +158,6 @@ strategy.
|
||||
Simple Universe
|
||||
~~~~~~~~~~~~~~~
|
||||
|
||||
Source code: `examples/simple_universe.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/simple_universe.py>`_
|
||||
|
||||
This example aims to provide an easy way for users to learn how to
|
||||
collect data from any given exchange and select a subset of the available
|
||||
currency pairs for trading. You simply need to specify the exchange and
|
||||
@@ -790,142 +184,10 @@ of the file:
|
||||
|
||||
catalyst ingest-exchange -x bitfinex -f minute
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python simple_universe.py
|
||||
|
||||
Credits: This code was originally submitted by `Abner Ayala-Acevedo
|
||||
<https://github.com/abnera>`_. Thank you!
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
from datetime import timedelta
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
|
||||
from catalyst import run_algorithm
|
||||
from catalyst.exchange.exchange_utils import get_exchange_symbols
|
||||
from catalyst.api import (symbols, )
|
||||
|
||||
|
||||
def initialize(context):
|
||||
context.i = -1 # minute counter
|
||||
context.exchange = context.exchanges.values()[0].name.lower()
|
||||
context.base_currency = context.exchanges.values()[0].base_currency.lower()
|
||||
|
||||
|
||||
def handle_data(context, data):
|
||||
context.i += 1
|
||||
lookback_days = 7 # 7 days
|
||||
|
||||
# current date & time in each iteration formatted into a string
|
||||
now = data.current_dt
|
||||
date, time = now.strftime('%Y-%m-%d %H:%M:%S').split(' ')
|
||||
lookback_date = now - timedelta(days=lookback_days)
|
||||
# keep only the date as a string, discard the time
|
||||
lookback_date = lookback_date.strftime('%Y-%m-%d %H:%M:%S').split(' ')[0]
|
||||
|
||||
one_day_in_minutes = 1440 # 60 * 24 assumes data_frequency='minute'
|
||||
# update universe everyday at midnight
|
||||
if not context.i % one_day_in_minutes:
|
||||
context.universe = universe(context, lookback_date, date)
|
||||
|
||||
# get data every 30 minutes
|
||||
minutes = 30
|
||||
# get lookback_days of history data: that is 'lookback' number of bins
|
||||
lookback = one_day_in_minutes / minutes * lookback_days
|
||||
if not context.i % minutes and context.universe:
|
||||
# we iterate for every pair in the current universe
|
||||
for coin in context.coins:
|
||||
pair = str(coin.symbol)
|
||||
|
||||
# Get 30 minute interval OHLCV data. This is the standard data
|
||||
# required for candlestick or indicators/signals. Return Pandas
|
||||
# DataFrames. 30T means 30-minute re-sampling of one minute data.
|
||||
# Adjust it to your desired time interval as needed.
|
||||
opened = fill(data.history(coin, 'open',
|
||||
bar_count=lookback, frequency='30T')).values
|
||||
high = fill(data.history(coin, 'high',
|
||||
bar_count=lookback, frequency='30T')).values
|
||||
low = fill(data.history(coin, 'low',
|
||||
bar_count=lookback, frequency='30T')).values
|
||||
close = fill(data.history(coin, 'price',
|
||||
bar_count=lookback, frequency='30T')).values
|
||||
volume = fill(data.history(coin, 'volume',
|
||||
bar_count=lookback, frequency='30T')).values
|
||||
|
||||
# close[-1] is the last value in the set, which is the equivalent
|
||||
# to current price (as in the most recent value)
|
||||
# displays the minute price for each pair every 30 minutes
|
||||
print('{now}: {pair} -\tO:{o},\tH:{h},\tL:{c},\tC{c},\tV:{v}'.format(
|
||||
now=now,
|
||||
pair=pair,
|
||||
o=opened[-1],
|
||||
h=high[-1],
|
||||
l=low[-1],
|
||||
c=close[-1],
|
||||
v=volume[-1],
|
||||
))
|
||||
|
||||
# -------------------------------------------------------------
|
||||
# --------------- Insert Your Strategy Here -------------------
|
||||
# -------------------------------------------------------------
|
||||
|
||||
|
||||
def analyze(context=None, results=None):
|
||||
pass
|
||||
|
||||
|
||||
# Get the universe for a given exchange and a given base_currency market
|
||||
# Example: Poloniex BTC Market
|
||||
def universe(context, lookback_date, current_date):
|
||||
# get all the pairs for the given exchange
|
||||
json_symbols = get_exchange_symbols(context.exchange)
|
||||
# convert into a DataFrame for easier processing
|
||||
df = pd.DataFrame.from_dict(json_symbols).transpose().astype(str)
|
||||
df['base_currency'] = df.apply(lambda row: row.symbol.split('_')[1],axis=1)
|
||||
df['market_currency'] = df.apply(lambda row: row.symbol.split('_')[0],axis=1)
|
||||
|
||||
# Filter all the pairs to get only the ones for a given base_currency
|
||||
df = df[df['base_currency'] == context.base_currency]
|
||||
|
||||
# Filter all the pairs to ensure that pair existed in the current date range
|
||||
df = df[df.start_date < lookback_date]
|
||||
df = df[df.end_daily >= current_date]
|
||||
context.coins = symbols(*df.symbol) # convert all the pairs to symbols
|
||||
|
||||
return df.symbol.tolist()
|
||||
|
||||
|
||||
# Replace all NA, NAN or infinite values with its nearest value
|
||||
def fill(series):
|
||||
if isinstance(series, pd.Series):
|
||||
return series.replace([np.inf, -np.inf], np.nan).ffill().bfill()
|
||||
elif isinstance(series, np.ndarray):
|
||||
return pd.Series(series).replace(
|
||||
[np.inf, -np.inf], np.nan
|
||||
).ffill().bfill().values
|
||||
else:
|
||||
return series
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
start_date = pd.to_datetime('2017-11-10', utc=True)
|
||||
end_date = pd.to_datetime('2017-11-13', utc=True)
|
||||
|
||||
performance = run_algorithm(start=start_date, end=end_date,
|
||||
capital_base=100.0, # amount of base_currency
|
||||
initialize=initialize,
|
||||
handle_data=handle_data,
|
||||
analyze=analyze,
|
||||
exchange_name='bitfinex',
|
||||
data_frequency='minute',
|
||||
base_currency='btc',
|
||||
live=False,
|
||||
live_graph=False,
|
||||
algo_namespace='simple_universe')
|
||||
Source code: `examples/simple_universe.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/simple_universe.py>`_
|
||||
|
||||
.. literalinclude:: ../../catalyst/examples/simple_universe.py
|
||||
:language: python
|
||||
|
||||
|
||||
.. _portfolio_optimization:
|
||||
@@ -939,135 +201,10 @@ use 180 days of historical data and rebalance every 30 days. This code was used
|
||||
in writting the following article:
|
||||
`Markowitz Portfolio Optimization for Cryptocurrencies <https://blog.enigma.co/markowitz-portfolio-optimization-for-cryptocurrencies-in-catalyst-b23c38652556>`_.
|
||||
|
||||
.. code-block:: python
|
||||
Source code: `examples/simple_universe.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/portfolio_optimization.py>`_
|
||||
|
||||
'''
|
||||
You can run this code using the Python interpreter:
|
||||
|
||||
$ python portfolio_optimization.py
|
||||
'''
|
||||
|
||||
from __future__ import division
|
||||
import os
|
||||
import pytz
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from scipy.optimize import minimize
|
||||
import matplotlib.pyplot as plt
|
||||
from datetime import datetime
|
||||
|
||||
from catalyst.api import record, symbol, symbols, order_target_percent
|
||||
from catalyst.utils.run_algo import run_algorithm
|
||||
|
||||
np.set_printoptions(threshold='nan', suppress=True)
|
||||
|
||||
|
||||
def initialize(context):
|
||||
# Portfolio assets list
|
||||
context.assets = symbols('btc_usdt', 'eth_usdt', 'ltc_usdt', 'dash_usdt',
|
||||
'xmr_usdt')
|
||||
context.nassets = len(context.assets)
|
||||
# Set the time window that will be used to compute expected return
|
||||
# and asset correlations
|
||||
context.window = 180
|
||||
# Set the number of days between each portfolio rebalancing
|
||||
context.rebalance_period = 30
|
||||
context.i = 0
|
||||
|
||||
|
||||
def handle_data(context, data):
|
||||
# Only rebalance at the beggining of the algorithm execution and
|
||||
# every multiple of the rebalance period
|
||||
if context.i == 0 or context.i%context.rebalance_period == 0:
|
||||
n = context.window
|
||||
prices = data.history(context.assets, fields='price',
|
||||
bar_count=n+1, frequency='1d')
|
||||
pr = np.asmatrix(prices)
|
||||
t_prices = prices.iloc[1:n+1]
|
||||
t_val = t_prices.values
|
||||
tminus_prices = prices.iloc[0:n]
|
||||
tminus_val = tminus_prices.values
|
||||
# Compute daily returns (r)
|
||||
r = np.asmatrix(t_val/tminus_val-1)
|
||||
# Compute the expected returns of each asset with the average
|
||||
# daily return for the selected time window
|
||||
m = np.asmatrix(np.mean(r, axis=0))
|
||||
# ###
|
||||
stds = np.std(r, axis=0)
|
||||
# Compute excess returns matrix (xr)
|
||||
xr = r - m
|
||||
# Matrix algebra to get variance-covariance matrix
|
||||
cov_m = np.dot(np.transpose(xr),xr)/n
|
||||
# Compute asset correlation matrix (informative only)
|
||||
corr_m = cov_m/np.dot(np.transpose(stds),stds)
|
||||
|
||||
# Define portfolio optimization parameters
|
||||
n_portfolios = 50000
|
||||
results_array = np.zeros((3+context.nassets,n_portfolios))
|
||||
for p in xrange(n_portfolios):
|
||||
weights = np.random.random(context.nassets)
|
||||
weights /= np.sum(weights)
|
||||
w = np.asmatrix(weights)
|
||||
p_r = np.sum(np.dot(w,np.transpose(m)))*365
|
||||
p_std = np.sqrt(np.dot(np.dot(w,cov_m),np.transpose(w)))*np.sqrt(365)
|
||||
|
||||
#store results in results array
|
||||
results_array[0,p] = p_r
|
||||
results_array[1,p] = p_std
|
||||
#store Sharpe Ratio (return / volatility) - risk free rate element
|
||||
#excluded for simplicity
|
||||
results_array[2,p] = results_array[0,p] / results_array[1,p]
|
||||
i = 0
|
||||
for iw in weights:
|
||||
results_array[3+i,p] = weights[i]
|
||||
i += 1
|
||||
|
||||
#convert results array to Pandas DataFrame
|
||||
results_frame = pd.DataFrame(np.transpose(results_array),
|
||||
columns=['r','stdev','sharpe']+context.assets)
|
||||
#locate position of portfolio with highest Sharpe Ratio
|
||||
max_sharpe_port = results_frame.iloc[results_frame['sharpe'].idxmax()]
|
||||
#locate positon of portfolio with minimum standard deviation
|
||||
min_vol_port = results_frame.iloc[results_frame['stdev'].idxmin()]
|
||||
|
||||
#order optimal weights for each asset
|
||||
for asset in context.assets:
|
||||
if data.can_trade(asset):
|
||||
order_target_percent(asset, max_sharpe_port[asset])
|
||||
|
||||
#create scatter plot coloured by Sharpe Ratio
|
||||
plt.scatter(results_frame.stdev,results_frame.r,c=results_frame.sharpe,cmap='RdYlGn')
|
||||
plt.xlabel('Volatility')
|
||||
plt.ylabel('Returns')
|
||||
plt.colorbar()
|
||||
#plot red star to highlight position of portfolio with highest Sharpe Ratio
|
||||
plt.scatter(max_sharpe_port[1],max_sharpe_port[0],marker='o',color='b',s=200)
|
||||
#plot green star to highlight position of minimum variance portfolio
|
||||
plt.show()
|
||||
print(max_sharpe_port)
|
||||
record(pr=pr,r=r, m=m, stds=stds ,max_sharpe_port=max_sharpe_port, corr_m=corr_m)
|
||||
context.i += 1
|
||||
|
||||
|
||||
def analyze(context=None, results=None):
|
||||
# Form DataFrame with selected data
|
||||
data = results[['pr','r','m','stds','max_sharpe_port','corr_m','portfolio_value']]
|
||||
|
||||
# Save results in CSV file
|
||||
filename = os.path.splitext(os.path.basename(__file__))[0]
|
||||
data.to_csv(filename + '.csv')
|
||||
|
||||
|
||||
# Bitcoin data is available from 2015-3-2. Dates vary for other tokens.
|
||||
start = datetime(2017, 1, 1, 0, 0, 0, 0, pytz.utc)
|
||||
end = datetime(2017, 8, 16, 0, 0, 0, 0, pytz.utc)
|
||||
results = run_algorithm(initialize=initialize,
|
||||
handle_data=handle_data,
|
||||
analyze=analyze,
|
||||
start=start,
|
||||
end=end,
|
||||
exchange_name='poloniex',
|
||||
capital_base=100000, )
|
||||
.. literalinclude:: ../../catalyst/examples/portfolio_optimization.py
|
||||
:language: python
|
||||
|
||||
.. image:: https://cdn-images-1.medium.com/max/1600/0*EjjiKZHlYF3sn7yQ.
|
||||
:align: center
|
||||
|
||||
@@ -44,11 +44,11 @@ For additional details on the functionality added on recent releases, see the
|
||||
Upcoming features
|
||||
~~~~~~~~~~~~~~~~~
|
||||
|
||||
* Additional datasets beyond pricing data (Dec. 2017)
|
||||
* API documentation (Jan. 2017)
|
||||
* Support for decentralized exchanges (Jan. 2017)
|
||||
* Support for data ingestion of community-contributed data sets (Jan. 2017)
|
||||
* Pipeline support (Jan. 2018)
|
||||
* Additional datasets beyond pricing data (Q1 2018)
|
||||
* API documentation (Q1 2018)
|
||||
* Support for decentralized exchanges (Q1 2018)
|
||||
* Support for data ingestion of community-contributed data sets (Q1 2018)
|
||||
* Pipeline support (Q1 2018)
|
||||
* Web UI (Q2 2018)
|
||||
|
||||
|
||||
|
||||
@@ -1,6 +1,8 @@
|
||||
.. include:: ../../README.rst
|
||||
|
||||
|
|
||||
|
|
||||
|
||||
Table of Contents
|
||||
-----------------
|
||||
|
||||
|
||||
+112
-42
@@ -47,8 +47,10 @@ you can install MiniConda, which is a smaller footprint (fewer packages and
|
||||
smaller size) than its big brother Anaconda, but it still contains all the
|
||||
main packages needed. To install MiniConda, you can follow these steps:
|
||||
|
||||
1. Download `MiniConda <https://conda.io/miniconda.html>`_. Select Python 2.7
|
||||
for your Operating System.
|
||||
1. Download `MiniConda <https://conda.io/miniconda.html>`_. Select either
|
||||
Python 3.6 (recommended) or Python 2.7 for your Operating System. The
|
||||
`Enigma Data Marketplace <https://enigmampc.github.io/marketplace/>`_ will
|
||||
require Python3, that's why we are recommending to opt for the newer version.
|
||||
2. Install MiniConda. See the `Installation Instructions
|
||||
<https://conda.io/docs/user-guide/install/index.html>`_ if you need help.
|
||||
3. Ensure the correct installation by running ``conda list`` in a Terminal
|
||||
@@ -64,18 +66,27 @@ main packages needed. To install MiniConda, you can follow these steps:
|
||||
|
||||
Once either Conda or MiniConda has been set up you can install Catalyst:
|
||||
|
||||
1. Download the file `python2.7-environment.yml
|
||||
<https://github.com/enigmampc/catalyst/blob/master/etc/python2.7-environment.yml>`_.
|
||||
1. Download the file `python3.6-environment.yml
|
||||
<https://github.com/enigmampc/catalyst/blob/master/etc/python3.6-environment.yml>`_
|
||||
(recommended) or `python2.7-environment.yml
|
||||
<https://github.com/enigmampc/catalyst/blob/master/etc/python2.7-environment.yml>`_
|
||||
matching your Conda installation from step #1 above.
|
||||
|
||||
To download, simply click on the 'Raw' button and save the file locally
|
||||
to a folder you can remember. Make sure that the file gets saved with the
|
||||
``.yml`` extension, and nothing like a ``.txt`` file or anything else.
|
||||
|
||||
2. Open a Terminal window and enter [``cd/dir``] into the directory where you
|
||||
saved the above ``python2.7-environment.yml`` file.
|
||||
saved the above ``.yml`` file.
|
||||
|
||||
3. Install using this file. This step can take about 5-10 minutes to install.
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
conda env create -f python3.6-environment.yml
|
||||
|
||||
or
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
conda env create -f python2.7-environment.yml
|
||||
@@ -122,6 +133,14 @@ with the following steps:
|
||||
|
||||
2. Create the environment:
|
||||
|
||||
for python 2.7:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
conda create --name catalyst python=2.7 scipy zlib
|
||||
|
||||
or for python 3.6:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
conda create --name catalyst python=2.7 scipy zlib
|
||||
@@ -180,20 +199,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
|
||||
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
|
||||
that you install in a `virtualenv
|
||||
<https://virtualenv.readthedocs.org/en/latest>`_. The `Hitchhiker's Guide to
|
||||
@@ -206,8 +211,21 @@ summarized version:
|
||||
$ pip install virtualenv
|
||||
$ virtualenv catalyst-venv
|
||||
$ 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
|
||||
|
||||
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
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
@@ -219,13 +237,13 @@ Troubleshooting ``pip`` Install
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install --upgrade pip
|
||||
$ pip install --upgrade pip
|
||||
|
||||
On Windows, the recommended command is:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
python -m pip install --upgrade pip
|
||||
$ python -m pip install --upgrade pip
|
||||
|
||||
----
|
||||
|
||||
@@ -251,7 +269,7 @@ Troubleshooting ``pip`` Install
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install --pre enigma-catalyst
|
||||
$ pip install --pre enigma-catalyst
|
||||
|
||||
----
|
||||
|
||||
@@ -263,7 +281,7 @@ Troubleshooting ``pip`` Install
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install --upgrade pip setuptools
|
||||
$ pip install --upgrade pip setuptools
|
||||
|
||||
----
|
||||
|
||||
@@ -278,7 +296,7 @@ Troubleshooting ``pip`` Install
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
pip install -r requirements.txt
|
||||
$ pip install -r requirements.txt
|
||||
|
||||
----
|
||||
|
||||
@@ -294,12 +312,12 @@ Troubleshooting ``pip`` Install
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
sudo apt-get install python-dev
|
||||
$ sudo apt-get install python-dev
|
||||
|
||||
.. _pipenv:
|
||||
|
||||
Installing with ``pipenv``
|
||||
-------------------------
|
||||
--------------------------
|
||||
|
||||
Installing Catalyst via ``pipenv`` is perhaps easier that installing it via
|
||||
``pip`` itself but you need to install ``pipenv`` first via ``pip``.
|
||||
@@ -376,14 +394,14 @@ outdated. Thus, you first need to run:
|
||||
|
||||
.. 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
|
||||
install by:
|
||||
|
||||
.. 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
|
||||
beginning of this page.
|
||||
@@ -408,20 +426,34 @@ following brew packages:
|
||||
|
||||
$ 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
|
||||
necessary to run
|
||||
The first time that you try to run an algorithm that loads the ``matplotlib``
|
||||
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
|
||||
|
||||
echo "backend: TkAgg" > ~/.matplotlib/matplotlibrc
|
||||
$ echo "backend: TkAgg" > ~/.matplotlib/matplotlibrc
|
||||
|
||||
in order to override the default ``MacOS`` backend for your system, which
|
||||
may not be accessible from inside the virtual environment. This will allow
|
||||
Catalyst to open matplotlib charts from within a virtual environment, which
|
||||
is useful for displaying the performance of your backtests. To learn more
|
||||
may not be accessible from inside the virtual or conda environment. This will
|
||||
allow Catalyst to open matplotlib charts from within a virtual environment,
|
||||
which is useful for displaying the performance of your backtests. To learn more
|
||||
about matplotlib backends, please refer to the
|
||||
`matplotlib backend documentation <https://matplotlib.org/faq/usage_faq.html#what-is-a-backend>`_.
|
||||
|
||||
@@ -430,12 +462,22 @@ about matplotlib backends, please refer to the
|
||||
Windows Requirements
|
||||
--------------------
|
||||
|
||||
In Windows, you will first need to install the `Microsoft Visual C++ Compiler
|
||||
for Python 2.7
|
||||
<https://www.microsoft.com/en-us/download/details.aspx?id=44266>`_. This
|
||||
package contains the compiler and the set of system headers necessary for
|
||||
producing binary wheels for Python 2.7 packages. If it's not already in your
|
||||
system, download it and install it before proceeding to the next step.
|
||||
In Windows, you will first need to install the Microsoft Visual C++ Compiler,
|
||||
which is different depending on the version of Python that you plan to use:
|
||||
|
||||
* Python 3.5, 3.6: `Visual C++ 2015 Build Tools
|
||||
<http://landinghub.visualstudio.com/visual-cpp-build-tools>`_,
|
||||
which installs Visual C++ version 14.0. **This is the recommended version**
|
||||
|
||||
* Python 2.7: `Microsoft Visual C++ Compiler for Python 2.7
|
||||
<https://www.microsoft.com/en-us/download/details.aspx?id=44266>`_, which
|
||||
installs version Visual C++ version 9.0
|
||||
|
||||
This package contains the compiler and the set of system headers necessary for
|
||||
producing binary wheels for Python packages. If it's not already in your
|
||||
system, download it and install it before proceeding to the next step. If you
|
||||
need additional help, or are looking for other versions of Visual C++ for
|
||||
Windows (only advanced users), follow `this link <https://wiki.python.org/moin/WindowsCompilers>`_.
|
||||
|
||||
Once you have the above compiler installed, the easiest and best supported way
|
||||
to install Catalyst in Windows is to use :ref:`Conda <conda>`. If you didn't
|
||||
@@ -463,6 +505,7 @@ mentioned above are as follows:
|
||||
default you get 0 as the Value Data)
|
||||
|
||||
|
|
||||
|
||||
- **The installer has encountered an unexpected error installing this package.
|
||||
This may indicate a problem with this package. The error code is 2503.**
|
||||
|
||||
@@ -475,6 +518,33 @@ mentioned above are as follows:
|
||||
- ``cd`` into the folder where you downloaded ``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
|
||||
------------
|
||||
|
||||
|
||||
@@ -4,11 +4,65 @@ This document explains how to get started with live trading.
|
||||
|
||||
Supported Exchanges
|
||||
^^^^^^^^^^^^^^^^^^^
|
||||
Catalyst can trade against these exchanges:
|
||||
|
||||
- Bitfinex, id= ``bitfinex``
|
||||
- Bittrex, id= ``bittrex``
|
||||
- Poloniex, id= ``poloniex``
|
||||
Since version 0.4, Catalyst integrated with `CCXT <https://github.com/ccxt/ccxt>`_,
|
||||
a cryptocurrency trading library with support for more than 90 exchanges. The
|
||||
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
|
||||
^^^^^^^^^^^^^^
|
||||
@@ -61,7 +115,7 @@ Currency symbols (e.g. btc, eth, ltc) follow the Bittrex convention.
|
||||
|
||||
Here are some examples:
|
||||
|
||||
.. code-block:: json
|
||||
.. code:: python
|
||||
|
||||
# With Bitfinex
|
||||
bitcoin_usd_asset = symbol('btc_usd')
|
||||
@@ -75,7 +129,8 @@ Note that the trading pairs are always referenced in the same manner.
|
||||
However, not all trading pairs are available on all exchanges. An
|
||||
error will occur if the specified trading pair is not trading
|
||||
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
|
||||
^^^^^^^^^^^^^^^^^^^^
|
||||
@@ -105,20 +160,38 @@ What differs are the arguments provided to the catalyst client or
|
||||
|
||||
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.
|
||||
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)
|
||||
will order the capital_base amount specified here of the specified asset.
|
||||
- ``exchange_name``: The name of the targeted exchange
|
||||
(supported values: *bitfinex*, *bittrex*).
|
||||
- ``exchange_name``: The name of the targeted exchange. See the
|
||||
`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
|
||||
data storage purposes.
|
||||
- ``base_currency``: The base currency used to calculate the
|
||||
statistics of your algorithm. Currently, the base currency of all
|
||||
trading pairs of your algorithm must match this value.
|
||||
- ``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``.
|
||||
- ``end_date``: When setting the end_date to a time in the **future**,
|
||||
it will schedule the live algo to finish gracefully at the specified date.
|
||||
- ``start_date``: (**Will be implemented in the future**)
|
||||
The live algo starts by default in the present, as mentioned above.
|
||||
by setting the start_date to a time in the future, the algorithm would
|
||||
essentially sleep and when the predefined time comes, it would start executing.
|
||||
|
||||
|
||||
|
||||
The `catalyst live` command offers additional parameters.
|
||||
You can learn more by running the following from the command line:
|
||||
|
||||
.. code-block:: bash
|
||||
|
||||
catalyst live --help
|
||||
|
||||
|
||||
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>`_
|
||||
|
||||
@@ -2,9 +2,93 @@
|
||||
Release Notes
|
||||
=============
|
||||
|
||||
Version 0.5.3
|
||||
^^^^^^^^^^^^^
|
||||
**Release Date**: 2018-02-09
|
||||
|
||||
Bug Fixes
|
||||
~~~~~~~~~
|
||||
- Fixed an issue with last candle in backtesting :issue:`219`
|
||||
|
||||
Version 0.5.2
|
||||
^^^^^^^^^^^^^
|
||||
**Release Date**: 2018-02-08
|
||||
|
||||
Bug Fixes
|
||||
~~~~~~~~~
|
||||
- Fixed an issue with live candle values :issue:`216` and :issue:`199`
|
||||
|
||||
Version 0.5.1
|
||||
^^^^^^^^^^^^^
|
||||
**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
|
||||
^^^^^^^^^^^^^
|
||||
**Release Date**: 2018-01-09
|
||||
|
||||
Bug Fixes
|
||||
~~~~~~~~~
|
||||
- Removed redundant capital_base validation (:issue:`142`)
|
||||
- Fixed portfolio update issue with restored state (:issue:`111`)
|
||||
- Skipping cash validation where there are open orders (:issue:`144`)
|
||||
|
||||
Version 0.4.3
|
||||
^^^^^^^^^^^^^
|
||||
**Release Date**: 2017-01-05
|
||||
**Release Date**: 2018-01-05
|
||||
|
||||
Bug Fixes
|
||||
~~~~~~~~~
|
||||
@@ -13,7 +97,7 @@ Bug Fixes
|
||||
|
||||
Version 0.4.2
|
||||
^^^^^^^^^^^^^
|
||||
**Release Date**: 2017-01-03
|
||||
**Release Date**: 2018-01-03
|
||||
|
||||
Bug Fixes
|
||||
~~~~~~~~~
|
||||
|
||||
@@ -11,6 +11,7 @@ Installation: MacOS
|
||||
|
||||
|
|
||||
|
|
||||
|
||||
Installation: Windows
|
||||
---------------------
|
||||
|
||||
@@ -21,6 +22,7 @@ Where things go smoothly:
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/H8HqcEbZmkk" frameborder="0" allowfullscreen></iframe>
|
||||
|
||||
|
|
||||
|
||||
Where things don't:
|
||||
|
||||
.. raw:: html
|
||||
@@ -29,6 +31,7 @@ Where things don't:
|
||||
|
||||
|
|
||||
|
|
||||
|
||||
Backtesting a Strategy
|
||||
----------------------
|
||||
|
||||
@@ -44,6 +47,7 @@ sell. Hopefully, we’ll ride the waves.
|
||||
|
||||
|
|
||||
|
|
||||
|
||||
Live Trading a Strategy
|
||||
-----------------------
|
||||
|
||||
@@ -54,5 +58,6 @@ in the previous video, we now take it to trade live against the Bittrex exchange
|
||||
.. raw:: html
|
||||
|
||||
<iframe width="560" height="315" src="https://www.youtube.com/embed/NupiE-Xuglw" frameborder="0" allowfullscreen></iframe>
|
||||
|
||||
|
|
||||
|
|
||||
|
|
||||
+1
-1
@@ -16,4 +16,4 @@ fi
|
||||
|
||||
jupyter notebook -y --no-browser --notebook-dir=${PROJECT_DIR} \
|
||||
--certfile=${SSL_CERT_PEM} --keyfile=${SSL_CERT_KEY} --ip='*' \
|
||||
--config=${CONFIG_PATH}
|
||||
--config=${CONFIG_PATH} --allow-root
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
name: catalyst
|
||||
channels:
|
||||
- defaults
|
||||
- conda-forge
|
||||
dependencies:
|
||||
- certifi=2016.2.28=py27_0
|
||||
- mkl=2017.0.3
|
||||
- matplotlib=2.1.2=py36_0
|
||||
- numpy=1.13.1=py27_0
|
||||
- openssl=1.0.2l
|
||||
- pip=9.0.1=py27_1
|
||||
@@ -20,7 +22,11 @@ dependencies:
|
||||
- bcolz==0.12.1
|
||||
- bottleneck==1.2.1
|
||||
- chardet==3.0.4
|
||||
- ccxt==1.10.565
|
||||
- ccxt==1.11.22
|
||||
# The Enigma Data Marketplace requires Python3 because it depends on
|
||||
# web3, which requires Python3, as building its dependencies breaks in Python2
|
||||
# - web3==4.0.0b7
|
||||
- requests-toolbelt==0.8.0
|
||||
- click==6.7
|
||||
- contextlib2==0.5.5
|
||||
- cycler==0.10.0
|
||||
@@ -57,4 +63,4 @@ dependencies:
|
||||
- tables==3.4.2
|
||||
- toolz==0.8.2
|
||||
- urllib3==1.22
|
||||
- enigma-catalyst>=0.3
|
||||
- enigma-catalyst>=0.5
|
||||
|
||||
@@ -0,0 +1,90 @@
|
||||
name: catalyst
|
||||
channels:
|
||||
- defaults
|
||||
- conda-forge
|
||||
dependencies:
|
||||
- ca-certificates=2017.08.26
|
||||
- certifi=2018.1.18
|
||||
- intel-openmp=2018.0.0
|
||||
- mkl=2018.0.1
|
||||
- numpy=1.14.0
|
||||
- openssl=1.0.2n
|
||||
- matplotlib=2.1.2=py36_0
|
||||
- pip=9.0.1
|
||||
- python=3.6.4
|
||||
- scipy=1.0.0
|
||||
- setuptools=38.4.0=py36_0
|
||||
- sqlite=3.22.0
|
||||
- tk=8.6.7
|
||||
- wheel=0.30.0
|
||||
- xz=5.2.3
|
||||
- zlib=1.2.11
|
||||
- pip:
|
||||
- aiodns==1.1.1
|
||||
- aiohttp==3.0.1
|
||||
- alembic==0.9.7
|
||||
- async-timeout==2.0.0
|
||||
- attrdict==2.0.0
|
||||
- attrs==17.4.0
|
||||
- bcolz==0.12.1
|
||||
- boto3==1.5.27
|
||||
- botocore==1.8.41
|
||||
- bottleneck==1.2.1
|
||||
- cchardet==2.1.1
|
||||
- ccxt==1.11.22
|
||||
- chardet==3.0.4
|
||||
- click==6.7
|
||||
- contextlib2==0.5.5
|
||||
- cyordereddict==1.0.0
|
||||
- cython==0.27.3
|
||||
- cytoolz==0.9.0
|
||||
- decorator==4.2.1
|
||||
- docutils==0.14
|
||||
- empyrical==0.2.1
|
||||
- enigma-catalyst>=0.5.3
|
||||
- eth-abi==1.0.0b0
|
||||
- eth-account==0.1.0a2
|
||||
- eth-keyfile==0.5.1
|
||||
- eth-keys==0.2.0b1
|
||||
- eth-rlp==0.1.0a2
|
||||
- eth-utils==1.0.0b1
|
||||
- hexbytes==0.1.0b0
|
||||
- idna==2.6
|
||||
- idna-ssl==1.0.0
|
||||
- intervaltree==2.1.0
|
||||
- jmespath==0.9.3
|
||||
- logbook==1.2.1
|
||||
- lru-dict==1.1.6
|
||||
- lxml==4.1.1
|
||||
- mako==1.0.7
|
||||
- markupsafe==1.0
|
||||
- multidict==4.1.0
|
||||
- multipledispatch==0.4.9
|
||||
- networkx==2.1
|
||||
- numexpr==2.6.4
|
||||
- pandas==0.19.2
|
||||
- pandas-datareader==0.6.0
|
||||
- patsy==0.5.0
|
||||
- pycares==2.3.0
|
||||
- pycryptodome==3.4.11
|
||||
- pysha3==1.0.2
|
||||
- python-dateutil==2.6.1
|
||||
- python-editor==1.0.3
|
||||
- pytz==2018.3
|
||||
- redo==1.6
|
||||
- requests==2.18.4
|
||||
- requests-file==1.4.3
|
||||
- requests-ftp==0.3.1
|
||||
- requests-toolbelt==0.8.0
|
||||
- rlp==0.6.0
|
||||
- s3transfer==0.1.12
|
||||
- six==1.11.0
|
||||
- sortedcontainers==1.5.9
|
||||
- sqlalchemy==1.2.2
|
||||
- statsmodels==0.8.0
|
||||
- tables==3.4.2
|
||||
- toolz==0.9.0
|
||||
- urllib3==1.22
|
||||
- web3==4.0.0b9
|
||||
- wrapt==1.10.11
|
||||
- yarl==1.1.0
|
||||
@@ -81,6 +81,8 @@ empyrical==0.2.1
|
||||
tables==3.3.0
|
||||
|
||||
#Catalyst dependencies
|
||||
ccxt==1.10.565
|
||||
ccxt==1.11.22
|
||||
boto3==1.4.8
|
||||
redo==1.6
|
||||
web3==4.0.0b11; python_version > '3.4'
|
||||
requests-toolbelt==0.8.0
|
||||
|
||||
@@ -16,7 +16,7 @@ babel==1.3
|
||||
docutils==0.12
|
||||
snowballstemmer==1.2.0
|
||||
sphinx-rtd-theme==0.1.8
|
||||
sphinx==1.3.4
|
||||
sphinx==1.6.7
|
||||
pbr==1.10.0
|
||||
|
||||
mock==2.0.0
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
Sphinx>=1.3.2
|
||||
Sphinx==1.6.7
|
||||
numpydoc>=0.5.0
|
||||
sphinx-autobuild==0.6.0
|
||||
docutils==0.12
|
||||
|
||||
@@ -10,10 +10,12 @@ from catalyst.exchange.exchange_bcolz import BcolzExchangeBarReader, \
|
||||
from catalyst.exchange.exchange_bundle import ExchangeBundle, \
|
||||
BUNDLE_NAME_TEMPLATE
|
||||
from catalyst.exchange.utils.bundle_utils import get_bcolz_chunk, \
|
||||
get_start_dt, get_df_from_arrays
|
||||
get_df_from_arrays
|
||||
from catalyst.exchange.utils.datetime_utils import get_start_dt
|
||||
from catalyst.exchange.utils.exchange_utils import get_exchange_folder
|
||||
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, \
|
||||
set_print_settings
|
||||
from catalyst.utils.paths import ensure_directory
|
||||
|
||||
log = getLogger('test_exchange_bundle')
|
||||
@@ -41,16 +43,16 @@ class TestExchangeBundle:
|
||||
|
||||
def test_ingest_minute(self):
|
||||
data_frequency = 'minute'
|
||||
exchange_name = 'poloniex'
|
||||
exchange_name = 'binance'
|
||||
|
||||
exchange = get_exchange(exchange_name)
|
||||
exchange_bundle = ExchangeBundle(exchange)
|
||||
exchange_bundle = ExchangeBundle(exchange_name)
|
||||
assets = [
|
||||
exchange.get_asset('eth_btc')
|
||||
exchange.get_asset('bch_eth')
|
||||
]
|
||||
|
||||
start = pd.to_datetime('2016-03-01', utc=True)
|
||||
end = pd.to_datetime('2017-11-1', utc=True)
|
||||
start = pd.to_datetime('2018-03-01', utc=True)
|
||||
end = pd.to_datetime('2018-03-8', utc=True)
|
||||
|
||||
log.info('ingesting exchange bundle {}'.format(exchange_name))
|
||||
exchange_bundle.ingest(
|
||||
@@ -60,7 +62,8 @@ class TestExchangeBundle:
|
||||
exclude_symbols=None,
|
||||
start=start,
|
||||
end=end,
|
||||
show_progress=True
|
||||
show_progress=False,
|
||||
show_breakdown=False
|
||||
)
|
||||
|
||||
reader = exchange_bundle.get_reader(data_frequency)
|
||||
@@ -71,9 +74,15 @@ class TestExchangeBundle:
|
||||
start_dt=start,
|
||||
end_dt=end
|
||||
)
|
||||
print('found {} rows for {} ingestion\n{}'.format(
|
||||
len(arrays[0]), asset.symbol, arrays[0])
|
||||
periods = exchange_bundle.get_calendar_periods_range(
|
||||
start, end, data_frequency
|
||||
)
|
||||
|
||||
dx = get_df_from_arrays(arrays[0], periods)
|
||||
set_print_settings()
|
||||
print('found {} rows for last ingestion:\n{}\n{}'.format(
|
||||
len(dx), dx.head(10), dx.tail(10)
|
||||
))
|
||||
pass
|
||||
|
||||
def test_ingest_minute_all(self):
|
||||
@@ -100,7 +109,7 @@ class TestExchangeBundle:
|
||||
# data_frequency = 'daily'
|
||||
# include_symbols = 'neo_btc,bch_btc,eth_btc'
|
||||
|
||||
exchange_name = 'bitfinex'
|
||||
exchange_name = 'binance'
|
||||
data_frequency = 'minute'
|
||||
|
||||
exchange = get_exchange(exchange_name)
|
||||
@@ -221,9 +230,14 @@ class TestExchangeBundle:
|
||||
start_dt=start,
|
||||
end_dt=end
|
||||
)
|
||||
print('found {} rows for {} ingestion\n{}'.format(
|
||||
len(arrays[0]), asset.symbol, arrays[0])
|
||||
periods = exchange_bundle.get_calendar_periods_range(
|
||||
start, end, data_frequency
|
||||
)
|
||||
|
||||
dx = get_df_from_arrays(arrays, periods)
|
||||
print('found {} rows for last ingestion'.format(
|
||||
len(dx)
|
||||
))
|
||||
pass
|
||||
|
||||
def test_daily_data_to_minute_table(self):
|
||||
@@ -289,17 +303,22 @@ class TestExchangeBundle:
|
||||
for asset in assets:
|
||||
sid = asset.sid
|
||||
|
||||
daily_values = reader.load_raw_arrays(
|
||||
arrays = reader.load_raw_arrays(
|
||||
fields=['open', 'high', 'low', 'close', 'volume'],
|
||||
start_dt=start,
|
||||
end_dt=end,
|
||||
sids=[sid],
|
||||
)
|
||||
|
||||
print('found {} rows for last ingestion'.format(
|
||||
len(daily_values[0]))
|
||||
periods = exchange_bundle.get_calendar_periods_range(
|
||||
start, end, data_frequency
|
||||
)
|
||||
pass
|
||||
|
||||
dx = get_df_from_arrays(arrays, periods)
|
||||
print('found {} rows for last ingestion'.format(
|
||||
len(dx)
|
||||
))
|
||||
pass
|
||||
|
||||
def test_minute_bundle(self):
|
||||
# exchange_name = 'poloniex'
|
||||
|
||||
+67
-11
@@ -1,10 +1,12 @@
|
||||
import pandas as pd
|
||||
from logbook import Logger
|
||||
|
||||
from base import BaseExchangeTestCase
|
||||
from catalyst.exchange.utils.stats_utils import set_print_settings
|
||||
from .base import BaseExchangeTestCase
|
||||
from catalyst.exchange.ccxt.ccxt_exchange import CCXT
|
||||
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, \
|
||||
get_trades_df, candles_from_trades
|
||||
from catalyst.finance.order import Order
|
||||
|
||||
log = Logger('test_ccxt')
|
||||
@@ -19,17 +21,18 @@ class TestCCXT(BaseExchangeTestCase):
|
||||
exchange_name=exchange_name,
|
||||
key=auth['key'],
|
||||
secret=auth['secret'],
|
||||
base_currency='bnb',
|
||||
password=None,
|
||||
base_currency='usdt',
|
||||
)
|
||||
self.exchange.init()
|
||||
|
||||
def test_order(self):
|
||||
log.info('creating order')
|
||||
asset = self.exchange.get_asset('neo_bnb')
|
||||
asset = self.exchange.get_asset('eth_usdt')
|
||||
order_id = self.exchange.order(
|
||||
asset=asset,
|
||||
style=ExchangeLimitOrder(limit_price=10),
|
||||
amount=1,
|
||||
style=ExchangeLimitOrder(limit_price=1000),
|
||||
amount=1.01,
|
||||
)
|
||||
log.info('order created {}'.format(order_id))
|
||||
assert order_id is not None
|
||||
@@ -56,24 +59,77 @@ class TestCCXT(BaseExchangeTestCase):
|
||||
def test_get_candles(self):
|
||||
log.info('retrieving candles')
|
||||
candles = self.exchange.get_candles(
|
||||
freq='5T',
|
||||
assets=[self.exchange.get_asset('eth_btc')],
|
||||
freq='1T',
|
||||
assets=[self.exchange.get_asset('eng_eth')],
|
||||
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:
|
||||
df = pd.DataFrame(candles[asset])
|
||||
df.set_index('last_traded', drop=True, inplace=True)
|
||||
|
||||
set_print_settings()
|
||||
print('got {} candles'.format(len(df)))
|
||||
print(df.head(10))
|
||||
print(df.tail(10))
|
||||
pass
|
||||
|
||||
def test_tickers(self):
|
||||
log.info('retrieving tickers')
|
||||
assets = [
|
||||
self.exchange.get_asset('eng_eth'),
|
||||
self.exchange.get_asset('ada_eth'),
|
||||
self.exchange.get_asset('zrx_eth'),
|
||||
]
|
||||
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_validate_volume(self):
|
||||
asset = self.exchange.get_asset('eng_eth')
|
||||
candles = self.exchange.get_candles(
|
||||
freq='1T',
|
||||
assets=[asset],
|
||||
bar_count=10,
|
||||
)
|
||||
df = pd.DataFrame(candles[asset])
|
||||
df.set_index('last_traded', drop=True, inplace=True)
|
||||
|
||||
df.drop_duplicates()
|
||||
df.sort_index(inplace=True, ascending=False)
|
||||
assert candles
|
||||
|
||||
start_dt = df.index[-1]
|
||||
trades = self.exchange.get_trades(
|
||||
asset, start_dt=start_dt, my_trades=False
|
||||
)
|
||||
assert trades
|
||||
|
||||
trades_df = get_trades_df(trades)
|
||||
df2 = candles_from_trades(trades_df, '1T')
|
||||
|
||||
set_print_settings()
|
||||
log.info(
|
||||
'comparing candles / resampled trades:\n{}\n{}'.format(
|
||||
df, df2
|
||||
)
|
||||
)
|
||||
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
|
||||
|
||||
def test_get_balances(self):
|
||||
|
||||
@@ -0,0 +1,8 @@
|
||||
from catalyst.exchange.utils.factory import get_exchange
|
||||
|
||||
|
||||
class TestConfig:
|
||||
def test_create_config(self):
|
||||
exchange = get_exchange('binance', skip_init=True)
|
||||
config = exchange.create_exchange_config()
|
||||
pass
|
||||
@@ -9,7 +9,7 @@ from catalyst.exchange.exchange_data_portal import (
|
||||
)
|
||||
from catalyst.exchange.utils.exchange_utils import get_common_assets
|
||||
from catalyst.exchange.utils.factory import get_exchanges
|
||||
from test_utils import rnd_history_date_days, rnd_bar_count
|
||||
from .test_utils import rnd_history_date_days, rnd_bar_count
|
||||
|
||||
log = Logger('test_bitfinex')
|
||||
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
from catalyst.exchange.utils.exchange_utils import transform_candles_to_df, \
|
||||
forward_fill_df_if_needed, get_candles_df
|
||||
|
||||
from catalyst.testing.fixtures import WithLogger, ZiplineTestCase
|
||||
from datetime import timedelta
|
||||
from pandas import Timestamp, DataFrame, concat
|
||||
|
||||
import numpy as np
|
||||
|
||||
|
||||
class TestExchangeUtils(WithLogger, ZiplineTestCase):
|
||||
@classmethod
|
||||
def get_specific_field_from_df(cls, df, field, asset):
|
||||
new_df = DataFrame(df[field])
|
||||
new_df.columns = [asset]
|
||||
new_df.index.name = None
|
||||
return new_df
|
||||
|
||||
@classmethod
|
||||
def verify_forward_fill_df_if_needed(cls, candles, periods, expected_df):
|
||||
observed_df = forward_fill_df_if_needed(
|
||||
transform_candles_to_df(candles),
|
||||
periods)
|
||||
assert (expected_df.equals(observed_df))
|
||||
|
||||
@classmethod
|
||||
def verify_get_candles_df(cls, assets, candles, end_fixed_dt,
|
||||
expected_df, check_next_candle=False):
|
||||
# run on all the fields
|
||||
for field in ['volume', 'open', 'close', 'high', 'low']:
|
||||
|
||||
field_dt = cls.get_specific_field_from_df(expected_df,
|
||||
field,
|
||||
assets[0])
|
||||
# run on several timestamps
|
||||
for delta in range(5):
|
||||
end_dt = end_fixed_dt + timedelta(minutes=delta)
|
||||
assert (field_dt.equals(get_candles_df({assets[0]: candles},
|
||||
field, '5T', 3,
|
||||
end_dt=end_dt)))
|
||||
|
||||
field_dt_a1 = cls.get_specific_field_from_df(expected_df,
|
||||
field,
|
||||
assets[0])
|
||||
field_dt_a2 = cls.get_specific_field_from_df(expected_df,
|
||||
field,
|
||||
assets[1])
|
||||
observed_df = get_candles_df({assets[0]: candles,
|
||||
assets[1]: candles},
|
||||
field, '5T', 3,
|
||||
end_dt=end_dt)
|
||||
|
||||
assert (observed_df.equals(concat([field_dt_a1, field_dt_a2],
|
||||
axis=1)))
|
||||
|
||||
if check_next_candle:
|
||||
# one candle forward
|
||||
end_dt = end_fixed_dt + timedelta(minutes=6)
|
||||
observed_df = get_candles_df({assets[0]: candles,
|
||||
assets[1]: candles},
|
||||
field, '5T', 3,
|
||||
end_dt=end_dt)
|
||||
|
||||
assert (not observed_df.equals(concat([field_dt_a1,
|
||||
field_dt_a2],
|
||||
axis=1)))
|
||||
assert (concat([field_dt_a1, field_dt_a2],
|
||||
axis=1)[1:].equals(observed_df[:-1]))
|
||||
|
||||
def test_get_candles_df(self):
|
||||
assets = ['btc_usdt', 'eth_usdt']
|
||||
|
||||
# test forward fill in the end
|
||||
candles = [{'high': 595, 'volume': 10, 'low': 594,
|
||||
'close': 595, 'open': 594,
|
||||
'last_traded': Timestamp('2018-03-01 09:45:00+0000',
|
||||
tz='UTC')
|
||||
},
|
||||
{'high': 594, 'volume': 108, 'low': 592,
|
||||
'close': 593, 'open': 592,
|
||||
'last_traded': Timestamp('2018-03-01 09:50:00+0000',
|
||||
tz='UTC')
|
||||
}]
|
||||
|
||||
expected = [{'high': 595.0, 'volume': 10.0, 'low': 594.0,
|
||||
'close': 595.0, 'open': 594.0,
|
||||
'last_traded': Timestamp('2018-03-01 09:45:00+0000',
|
||||
tz='UTC')
|
||||
},
|
||||
{'high': 594.0, 'volume': 108.0, 'low': 592.0,
|
||||
'close': 593.0, 'open': 592.0,
|
||||
'last_traded': Timestamp('2018-03-01 09:50:00+0000',
|
||||
tz='UTC')
|
||||
},
|
||||
{'high': 593.0, 'volume': 0.0, 'low': 593.0,
|
||||
'close': 593.0, 'open': 593.0,
|
||||
'last_traded': Timestamp('2018-03-01 09:55:00+0000',
|
||||
tz='UTC')
|
||||
}]
|
||||
|
||||
periods = [Timestamp('2018-03-01 09:45:00+0000', tz='UTC'),
|
||||
Timestamp('2018-03-01 09:50:00+0000', tz='UTC'),
|
||||
Timestamp('2018-03-01 09:55:00+0000', tz='UTC')]
|
||||
|
||||
expected_df = transform_candles_to_df(expected)
|
||||
|
||||
self.verify_forward_fill_df_if_needed(candles, periods,
|
||||
expected_df)
|
||||
self.verify_get_candles_df(assets, candles, periods[2],
|
||||
expected_df, True)
|
||||
|
||||
# test forward fill in the middle
|
||||
candles = [{'high': 595, 'volume': 10, 'low': 594,
|
||||
'close': 595, 'open': 594,
|
||||
'last_traded': Timestamp('2018-03-01 09:45:00+0000',
|
||||
tz='UTC')
|
||||
},
|
||||
{'high': 594, 'volume': 108, 'low': 592,
|
||||
'close': 593, 'open': 592,
|
||||
'last_traded': Timestamp('2018-03-01 09:55:00+0000',
|
||||
tz='UTC')
|
||||
}]
|
||||
|
||||
expected = [{'high': 595.0, 'volume': 10.0, 'low': 594.0,
|
||||
'close': 595.0, 'open': 594.0,
|
||||
'last_traded': Timestamp('2018-03-01 09:45:00+0000',
|
||||
tz='UTC')
|
||||
},
|
||||
{'high': 595.0, 'volume': 0.0, 'low': 595.0,
|
||||
'close': 595.0, 'open': 595.0,
|
||||
'last_traded': Timestamp('2018-03-01 09:50:00+0000',
|
||||
tz='UTC')
|
||||
},
|
||||
{'high': 594.0, 'volume': 108.0, 'low': 592.0,
|
||||
'close': 593.0, 'open': 592.0,
|
||||
'last_traded': Timestamp('2018-03-01 09:55:00+0000',
|
||||
tz='UTC')
|
||||
}]
|
||||
|
||||
expected_df = transform_candles_to_df(expected)
|
||||
self.verify_forward_fill_df_if_needed(candles, periods, expected_df)
|
||||
self.verify_get_candles_df(assets, candles, periods[2], expected_df)
|
||||
|
||||
# test "forward fill" at the beginning
|
||||
candles = [{'high': 595, 'volume': 10, 'low': 594,
|
||||
'close': 595, 'open': 594,
|
||||
'last_traded': Timestamp('2018-03-01 09:50:00+0000',
|
||||
tz='UTC')
|
||||
},
|
||||
{'high': 594, 'volume': 108, 'low': 592,
|
||||
'close': 593, 'open': 592,
|
||||
'last_traded': Timestamp('2018-03-01 09:55:00+0000',
|
||||
tz='UTC')
|
||||
}]
|
||||
|
||||
expected = [{'high': np.NaN, 'volume': 0.0, 'low': np.NaN,
|
||||
'close': np.NaN, 'open': np.NaN,
|
||||
'last_traded': Timestamp('2018-03-01 09:45:00+0000',
|
||||
tz='UTC')
|
||||
},
|
||||
{'high': 595, 'volume': 10, 'low': 594,
|
||||
'close': 595, 'open': 594,
|
||||
'last_traded': Timestamp('2018-03-01 09:50:00+0000',
|
||||
tz='UTC')
|
||||
},
|
||||
{'high': 594, 'volume': 108, 'low': 592,
|
||||
'close': 593, 'open': 592,
|
||||
'last_traded': Timestamp('2018-03-01 09:55:00+0000',
|
||||
tz='UTC')
|
||||
}]
|
||||
|
||||
expected_df = transform_candles_to_df(expected)
|
||||
self.verify_forward_fill_df_if_needed(candles, periods, expected_df)
|
||||
# Not the same due to dropna - commenting out for now
|
||||
# self.verify_get_candles_df(assets, candles, periods[2], expected_df)
|
||||
@@ -0,0 +1,79 @@
|
||||
import importlib
|
||||
from os.path import join, isfile
|
||||
|
||||
import pandas as pd
|
||||
import os
|
||||
|
||||
from catalyst import run_algorithm
|
||||
from catalyst.exchange.utils.stats_utils import get_pretty_stats, \
|
||||
extract_transactions, set_print_settings, extract_orders
|
||||
from catalyst.testing.fixtures import WithLogger, ZiplineTestCase
|
||||
from logbook import TestHandler, WARNING
|
||||
from pathtools.path import listdir
|
||||
|
||||
filter_algos = [
|
||||
'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',
|
||||
]
|
||||
|
||||
|
||||
class TestSuiteAlgo(WithLogger, ZiplineTestCase):
|
||||
@staticmethod
|
||||
def analyze(context, perf):
|
||||
set_print_settings()
|
||||
|
||||
transaction_df = extract_transactions(perf)
|
||||
print('the transactions:\n{}'.format(transaction_df))
|
||||
|
||||
orders_df = extract_orders(perf)
|
||||
print('the orders:\n{}'.format(orders_df))
|
||||
|
||||
stats = get_pretty_stats(perf, show_tail=False, num_rows=5)
|
||||
print('the stats:\n{}'.format(stats))
|
||||
pass
|
||||
|
||||
def test_run_examples(self):
|
||||
folder = join('..', '..', '..', 'catalyst', 'examples')
|
||||
files = [f for f in listdir(folder) if isfile(join(folder, f))]
|
||||
|
||||
algo_list = []
|
||||
for filename in files:
|
||||
name = os.path.basename(filename)
|
||||
if filter_algos and name not in filter_algos:
|
||||
continue
|
||||
|
||||
module_name = 'catalyst.examples.{}'.format(
|
||||
name.replace('.py', '')
|
||||
)
|
||||
algo_list.append(module_name)
|
||||
|
||||
for module_name in algo_list:
|
||||
algo = importlib.import_module(module_name)
|
||||
namespace = module_name.replace('.', '_')
|
||||
|
||||
log_catcher = TestHandler()
|
||||
with log_catcher:
|
||||
run_algorithm(
|
||||
capital_base=0.1,
|
||||
data_frequency='minute',
|
||||
initialize=algo.initialize,
|
||||
handle_data=algo.handle_data,
|
||||
analyze=TestSuiteAlgo.analyze,
|
||||
exchange_name='poloniex',
|
||||
algo_namespace='test_{}'.format(namespace),
|
||||
base_currency='eth',
|
||||
start=pd.to_datetime('2017-10-01', utc=True),
|
||||
end=pd.to_datetime('2017-10-02', utc=True),
|
||||
# output=out
|
||||
)
|
||||
warnings = [record for record in log_catcher.records if
|
||||
record.level == WARNING]
|
||||
|
||||
if len(warnings) > 0:
|
||||
print('WARNINGS:\n{}'.format(warnings))
|
||||
pass
|
||||
@@ -1,6 +1,8 @@
|
||||
import random
|
||||
|
||||
import os
|
||||
import pandas as pd
|
||||
from datetime import timedelta
|
||||
from logbook import TestHandler
|
||||
from pandas.util.testing import assert_frame_equal
|
||||
|
||||
@@ -11,7 +13,7 @@ from catalyst.exchange.utils.exchange_utils import get_candles_df
|
||||
from catalyst.exchange.utils.factory import get_exchange
|
||||
from catalyst.exchange.utils.test_utils import output_df, \
|
||||
select_random_assets
|
||||
from catalyst.testing.fixtures import WithLogger, ZiplineTestCase
|
||||
from catalyst.exchange.utils.stats_utils import set_print_settings
|
||||
|
||||
pd.set_option('display.expand_frame_repr', False)
|
||||
pd.set_option('precision', 8)
|
||||
@@ -19,12 +21,13 @@ pd.set_option('display.width', 1000)
|
||||
pd.set_option('display.max_colwidth', 1000)
|
||||
|
||||
|
||||
class TestSuiteBundle(WithLogger, ZiplineTestCase):
|
||||
class TestSuiteBundle:
|
||||
@staticmethod
|
||||
def get_data_portal(exchange_names):
|
||||
def get_data_portal(exchanges):
|
||||
open_calendar = get_calendar('OPEN')
|
||||
asset_finder = ExchangeAssetFinder()
|
||||
asset_finder = ExchangeAssetFinder(exchanges)
|
||||
|
||||
exchange_names = [exchange.name for exchange in exchanges]
|
||||
data_portal = DataPortalExchangeBacktest(
|
||||
exchange_names=exchange_names,
|
||||
asset_finder=asset_finder,
|
||||
@@ -34,7 +37,7 @@ class TestSuiteBundle(WithLogger, ZiplineTestCase):
|
||||
return data_portal
|
||||
|
||||
def compare_bundle_with_exchange(self, exchange, assets, end_dt, bar_count,
|
||||
freq, data_frequency, data_portal):
|
||||
freq, data_frequency, data_portal, field):
|
||||
"""
|
||||
Creates DataFrames from the bundle and exchange for the specified
|
||||
data set.
|
||||
@@ -45,7 +48,9 @@ class TestSuiteBundle(WithLogger, ZiplineTestCase):
|
||||
assets
|
||||
end_dt
|
||||
bar_count
|
||||
sample_minutes
|
||||
freq
|
||||
data_frequency
|
||||
data_portal
|
||||
|
||||
Returns
|
||||
-------
|
||||
@@ -55,18 +60,26 @@ class TestSuiteBundle(WithLogger, ZiplineTestCase):
|
||||
|
||||
log_catcher = TestHandler()
|
||||
with log_catcher:
|
||||
symbols = [asset.symbol for asset in assets]
|
||||
print(
|
||||
'comparing {} for {}/{} with {} timeframe until {}'.format(
|
||||
field, exchange.name, symbols, freq, end_dt
|
||||
)
|
||||
)
|
||||
data['bundle'] = data_portal.get_history_window(
|
||||
assets=assets,
|
||||
end_dt=end_dt,
|
||||
bar_count=bar_count,
|
||||
frequency=freq,
|
||||
field='close',
|
||||
field=field,
|
||||
data_frequency=data_frequency,
|
||||
)
|
||||
print('bundle data:\n{}'.format(
|
||||
data['bundle'].tail(10))
|
||||
set_print_settings()
|
||||
print(
|
||||
'the bundle data:\n{}'.format(
|
||||
data['bundle']
|
||||
)
|
||||
)
|
||||
|
||||
candles = exchange.get_candles(
|
||||
end_dt=end_dt,
|
||||
freq=freq,
|
||||
@@ -75,29 +88,108 @@ class TestSuiteBundle(WithLogger, ZiplineTestCase):
|
||||
)
|
||||
data['exchange'] = get_candles_df(
|
||||
candles=candles,
|
||||
field='close',
|
||||
field=field,
|
||||
freq=freq,
|
||||
bar_count=bar_count,
|
||||
end_dt=end_dt,
|
||||
)
|
||||
print('exchange data:\n{}'.format(
|
||||
data['exchange'].tail(10))
|
||||
print(
|
||||
'the exchange data:\n{}'.format(
|
||||
data['exchange']
|
||||
)
|
||||
)
|
||||
for source in data:
|
||||
df = data[source]
|
||||
path = output_df(df, assets, '{}_{}'.format(freq, source))
|
||||
print('saved {}:\n{}'.format(source, path))
|
||||
path, folder = output_df(
|
||||
df, assets, '{}_{}'.format(freq, source)
|
||||
)
|
||||
|
||||
print('saved {} test results: {}'.format(end_dt, folder))
|
||||
|
||||
assert_frame_equal(
|
||||
right=data['bundle'],
|
||||
left=data['exchange'],
|
||||
check_less_precise=True,
|
||||
right=data['bundle'][:-1],
|
||||
left=data['exchange'][:-1],
|
||||
check_less_precise=1,
|
||||
)
|
||||
try:
|
||||
assert_frame_equal(
|
||||
right=data['bundle'][:-1],
|
||||
left=data['exchange'][:-1],
|
||||
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 compare_current_with_last_candle(self, exchange, assets, end_dt,
|
||||
freq, data_frequency, data_portal):
|
||||
"""
|
||||
Creates DataFrames from the bundle and exchange for the specified
|
||||
data set.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
exchange: Exchange
|
||||
assets
|
||||
end_dt
|
||||
bar_count
|
||||
freq
|
||||
data_frequency
|
||||
data_portal
|
||||
|
||||
Returns
|
||||
-------
|
||||
|
||||
"""
|
||||
data = dict()
|
||||
|
||||
assets = sorted(assets, key=lambda a: a.symbol)
|
||||
log_catcher = TestHandler()
|
||||
with log_catcher:
|
||||
symbols = [asset.symbol for asset in assets]
|
||||
print(
|
||||
'comparing data for {}/{} with {} timeframe on {}'.format(
|
||||
exchange.name, symbols, freq, end_dt
|
||||
)
|
||||
)
|
||||
data['candle'] = data_portal.get_history_window(
|
||||
assets=assets,
|
||||
end_dt=end_dt,
|
||||
bar_count=1,
|
||||
frequency=freq,
|
||||
field='close',
|
||||
data_frequency=data_frequency,
|
||||
)
|
||||
set_print_settings()
|
||||
print(
|
||||
'the bundle first / last row:\n{}'.format(
|
||||
data['candle'].iloc[[-1]]
|
||||
)
|
||||
)
|
||||
current = data_portal.get_spot_value(
|
||||
assets=assets,
|
||||
field='close',
|
||||
dt=end_dt,
|
||||
data_frequency=data_frequency,
|
||||
)
|
||||
data['current'] = pd.Series(data=current, index=assets)
|
||||
print(
|
||||
'the current price:\n{}'.format(
|
||||
data['current']
|
||||
)
|
||||
)
|
||||
pass
|
||||
|
||||
def test_validate_bundles(self):
|
||||
# exchange_population = 3
|
||||
asset_population = 3
|
||||
data_frequency = random.choice(['minute', 'daily'])
|
||||
data_frequency = random.choice(['minute'])
|
||||
|
||||
# bundle = 'dailyBundle' if data_frequency
|
||||
# == 'daily' else 'minuteBundle'
|
||||
@@ -105,19 +197,71 @@ class TestSuiteBundle(WithLogger, ZiplineTestCase):
|
||||
# population=exchange_population,
|
||||
# features=[bundle],
|
||||
# ) # Type: list[Exchange]
|
||||
exchanges = [get_exchange('bitfinex', skip_init=True)]
|
||||
# TODO: currently focusing on Binance, try other exchanges
|
||||
exchanges = [get_exchange('poloniex', skip_init=True)]
|
||||
|
||||
data_portal = TestSuiteBundle.get_data_portal(
|
||||
[exchange.name for exchange in exchanges]
|
||||
)
|
||||
data_portal = TestSuiteBundle.get_data_portal(exchanges)
|
||||
for exchange in exchanges:
|
||||
exchange.init()
|
||||
|
||||
frequencies = exchange.get_candle_frequencies(data_frequency)
|
||||
# freq = random.sample(frequencies, 1)[0]
|
||||
freq = '5T'
|
||||
rnd = random.SystemRandom()
|
||||
# field = rnd.choice(['open', 'high', 'low', 'close', 'volume'])
|
||||
field = rnd.choice(['close'])
|
||||
|
||||
# bar_count = random.randint(3, 6)
|
||||
bar_count = 5
|
||||
|
||||
# assets = select_random_assets(
|
||||
# exchange.assets, asset_population
|
||||
# )
|
||||
assets = [exchange.get_asset('bch_eth')]
|
||||
end_dt = pd.to_datetime('2018-03-01', utc=True)
|
||||
for asset in assets:
|
||||
attribute = 'end_{}'.format(data_frequency)
|
||||
asset_end_dt = getattr(asset, attribute)
|
||||
|
||||
if end_dt is None or asset_end_dt < end_dt:
|
||||
end_dt = asset_end_dt
|
||||
|
||||
end_dt = end_dt + timedelta(minutes=3)
|
||||
dt_range = pd.date_range(
|
||||
end=end_dt, periods=bar_count, freq=freq
|
||||
)
|
||||
self.compare_bundle_with_exchange(
|
||||
exchange=exchange,
|
||||
assets=assets,
|
||||
end_dt=dt_range[-1],
|
||||
bar_count=bar_count,
|
||||
freq=freq,
|
||||
data_frequency=data_frequency,
|
||||
data_portal=data_portal,
|
||||
field=field,
|
||||
)
|
||||
pass
|
||||
|
||||
def test_validate_last_candle(self):
|
||||
# exchange_population = 3
|
||||
asset_population = 3
|
||||
data_frequency = random.choice(['minute'])
|
||||
|
||||
# bundle = 'dailyBundle' if data_frequency
|
||||
# == 'daily' else 'minuteBundle'
|
||||
# exchanges = select_random_exchanges(
|
||||
# population=exchange_population,
|
||||
# features=[bundle],
|
||||
# ) # Type: list[Exchange]
|
||||
exchanges = [get_exchange('poloniex', skip_init=True)]
|
||||
|
||||
data_portal = TestSuiteBundle.get_data_portal(exchanges)
|
||||
for exchange in exchanges:
|
||||
exchange.init()
|
||||
|
||||
frequencies = exchange.get_candle_frequencies(data_frequency)
|
||||
freq = random.sample(frequencies, 1)[0]
|
||||
|
||||
bar_count = random.randint(1, 10)
|
||||
|
||||
assets = select_random_assets(
|
||||
exchange.assets, asset_population
|
||||
)
|
||||
@@ -129,14 +273,11 @@ class TestSuiteBundle(WithLogger, ZiplineTestCase):
|
||||
if end_dt is None or asset_end_dt < end_dt:
|
||||
end_dt = asset_end_dt
|
||||
|
||||
dt_range = pd.date_range(
|
||||
end=end_dt, periods=bar_count, freq=freq
|
||||
)
|
||||
self.compare_bundle_with_exchange(
|
||||
end_dt = end_dt + timedelta(minutes=3)
|
||||
self.compare_current_with_last_candle(
|
||||
exchange=exchange,
|
||||
assets=assets,
|
||||
end_dt=dt_range[-1],
|
||||
bar_count=bar_count,
|
||||
end_dt=end_dt,
|
||||
freq=freq,
|
||||
data_frequency=data_frequency,
|
||||
data_portal=data_portal,
|
||||
|
||||
@@ -5,14 +5,14 @@ from logging import Logger, WARNING
|
||||
from time import sleep
|
||||
|
||||
import pandas as pd
|
||||
from catalyst.assets._assets import TradingPair
|
||||
from logbook import TestHandler
|
||||
|
||||
from catalyst.exchange.exchange_errors import ExchangeRequestError
|
||||
from catalyst.assets._assets import TradingPair
|
||||
from catalyst.exchange.exchange_execution import ExchangeLimitOrder
|
||||
from catalyst.exchange.utils.exchange_utils import get_exchange_folder
|
||||
from catalyst.exchange.utils.factory import get_exchanges, get_exchange
|
||||
from catalyst.exchange.utils.test_utils import select_random_exchanges, \
|
||||
handle_exchange_error, select_random_assets
|
||||
select_random_assets
|
||||
from catalyst.testing import ZiplineTestCase
|
||||
from catalyst.testing.fixtures import WithLogger
|
||||
|
||||
@@ -20,47 +20,13 @@ log = Logger('TestSuiteExchange')
|
||||
|
||||
|
||||
class TestSuiteExchange(WithLogger, ZiplineTestCase):
|
||||
def _test_markets_exchange(self, exchange, attempts=0):
|
||||
assets = None
|
||||
try:
|
||||
exchange.init()
|
||||
|
||||
# Verify that the assets and markets are populated
|
||||
if not exchange.markets:
|
||||
raise ValueError(
|
||||
'no markets found'
|
||||
)
|
||||
if not exchange.assets:
|
||||
raise ValueError(
|
||||
'no assets derived from markets'
|
||||
)
|
||||
assets = exchange.assets
|
||||
|
||||
except ExchangeRequestError as e:
|
||||
sleep(5)
|
||||
|
||||
if attempts > 5:
|
||||
handle_exchange_error(exchange, e)
|
||||
|
||||
else:
|
||||
print(
|
||||
're-trying an exchange request {} {}'.format(
|
||||
exchange.name, attempts
|
||||
)
|
||||
)
|
||||
self._test_markets_exchange(exchange, attempts + 1)
|
||||
|
||||
except Exception as e:
|
||||
handle_exchange_error(exchange, e)
|
||||
|
||||
return assets
|
||||
|
||||
def test_markets(self):
|
||||
population = 3
|
||||
results = dict()
|
||||
|
||||
exchanges = select_random_exchanges(population) # Type: list[Exchange]
|
||||
for exchange in exchanges:
|
||||
exchange.init()
|
||||
assets = self._test_markets_exchange(exchange)
|
||||
|
||||
if assets is not None:
|
||||
@@ -83,12 +49,13 @@ class TestSuiteExchange(WithLogger, ZiplineTestCase):
|
||||
|
||||
def test_tickers(self):
|
||||
exchange_population = 3
|
||||
asset_population = 3
|
||||
asset_population = 15
|
||||
|
||||
exchanges = select_random_exchanges(
|
||||
exchange_population,
|
||||
features=['fetchTickers'],
|
||||
) # Type: list[Exchange]
|
||||
# exchanges = select_random_exchanges(
|
||||
# exchange_population,
|
||||
# features=['fetchTickers'],
|
||||
# ) # Type: list[Exchange]
|
||||
exchanges = list(get_exchanges(['binance']).values())
|
||||
for exchange in exchanges:
|
||||
exchange.init()
|
||||
|
||||
@@ -111,10 +78,11 @@ class TestSuiteExchange(WithLogger, ZiplineTestCase):
|
||||
exchange_population = 3
|
||||
asset_population = 3
|
||||
|
||||
exchanges = select_random_exchanges(
|
||||
population=exchange_population,
|
||||
features=['fetchOHLCV'],
|
||||
) # Type: list[Exchange]
|
||||
# exchanges = select_random_exchanges(
|
||||
# population=exchange_population,
|
||||
# features=['fetchOHLCV'],
|
||||
# ) # Type: list[Exchange]
|
||||
exchanges = list(get_exchanges(['binance']).values())
|
||||
for exchange in exchanges:
|
||||
exchange.init()
|
||||
|
||||
@@ -136,7 +104,6 @@ class TestSuiteExchange(WithLogger, ZiplineTestCase):
|
||||
assets=assets,
|
||||
bar_count=bar_count,
|
||||
start_dt=dt_range[0],
|
||||
end_dt=dt_range[-1],
|
||||
)
|
||||
|
||||
assert len(candles) == asset_population
|
||||
@@ -153,13 +120,20 @@ class TestSuiteExchange(WithLogger, ZiplineTestCase):
|
||||
quote_currency = 'eth'
|
||||
order_amount = 0.1
|
||||
|
||||
exchanges = select_random_exchanges(
|
||||
population=population,
|
||||
features=['fetchOrder'],
|
||||
is_authenticated=True,
|
||||
base_currency=quote_currency,
|
||||
) # Type: list[Exchange]
|
||||
# exchanges = select_random_exchanges(
|
||||
# population=population,
|
||||
# features=['fetchOrder'],
|
||||
# is_authenticated=True,
|
||||
# base_currency=quote_currency,
|
||||
# ) # Type: list[Exchange]
|
||||
|
||||
exchanges = [
|
||||
get_exchange(
|
||||
'binance',
|
||||
base_currency=quote_currency,
|
||||
must_authenticate=True,
|
||||
)
|
||||
]
|
||||
log_catcher = TestHandler()
|
||||
with log_catcher:
|
||||
for exchange in exchanges:
|
||||
@@ -184,13 +158,13 @@ class TestSuiteExchange(WithLogger, ZiplineTestCase):
|
||||
)
|
||||
sleep(1)
|
||||
|
||||
open_order, _ = exchange.get_order(order.id, asset)
|
||||
open_order = exchange.get_order(order.id, asset)
|
||||
self.assertEqual(0, open_order.status)
|
||||
|
||||
exchange.cancel_order(open_order, asset)
|
||||
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
|
||||
record.level == WARNING]
|
||||
|
||||
|
||||
@@ -0,0 +1,35 @@
|
||||
from catalyst.marketplace.marketplace import Marketplace
|
||||
from catalyst.testing.fixtures import WithLogger, ZiplineTestCase
|
||||
|
||||
|
||||
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('marketcap')
|
||||
pass
|
||||
|
||||
def test_ingest(self):
|
||||
marketplace = Marketplace()
|
||||
ds_def = marketplace.ingest('marketcap')
|
||||
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