mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-22 12:40:30 +08:00
Compare commits
44
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
2fbb6f5134 | ||
|
|
af7b211035 | ||
|
|
4eaec97614 | ||
|
|
c29b1ef3c1 | ||
|
|
17f9906df4 | ||
|
|
af85ee31c9 | ||
|
|
132dffd239 | ||
|
|
887a7cc825 | ||
|
|
22249506e6 | ||
|
|
b979ffd123 | ||
|
|
0db9950347 | ||
|
|
7c4467d800 | ||
|
|
30dbeaa5fb | ||
|
|
f5cb6e38d6 | ||
|
|
c768b207bc | ||
|
|
e18686d5c5 | ||
|
|
b7779cf363 | ||
|
|
28819b8a32 | ||
|
|
a56d7f34c7 | ||
|
|
9f0b3303f1 | ||
|
|
9d7a35658b | ||
|
|
c58cebd1eb | ||
|
|
d223529100 | ||
|
|
2a97ade68e | ||
|
|
9648767e9a | ||
|
|
98449b2088 | ||
|
|
91d16aba3b | ||
|
|
9eb649371b | ||
|
|
7f2ded65bc | ||
|
|
b76b4458cb | ||
|
|
decbdbf6ea | ||
|
|
685ce25b85 | ||
|
|
0d77854782 | ||
|
|
4cb8d54d97 | ||
|
|
7b796a4276 | ||
|
|
41a4c7072f | ||
|
|
11302b3af9 | ||
|
|
5bb7eed072 | ||
|
|
dbf3b6e6b2 | ||
|
|
3e69449a6b | ||
|
|
69731b653d | ||
|
|
127d779eb1 | ||
|
|
8d86a5548f | ||
|
|
0a37cdec5b |
+9
-3
@@ -5,6 +5,7 @@
|
|||||||
|
|
||||||
|version tag|
|
|version tag|
|
||||||
|version status|
|
|version status|
|
||||||
|
|forum|
|
||||||
|discord|
|
|discord|
|
||||||
|twitter|
|
|twitter|
|
||||||
|
|
||||||
@@ -22,9 +23,11 @@ visit `enigma.co <https://www.enigma.co>`_ to learn more about Catalyst.
|
|||||||
Catalyst builds on top of the well-established
|
Catalyst builds on top of the well-established
|
||||||
`Zipline <https://github.com/quantopian/zipline>`_ project. We did our best to
|
`Zipline <https://github.com/quantopian/zipline>`_ project. We did our best to
|
||||||
minimize structural changes to the general API to maximize compatibility with
|
minimize structural changes to the general API to maximize compatibility with
|
||||||
existing trading algorithms, developer knowledge, and tutorials. Join us on
|
existing trading algorithms, developer knowledge, and tutorials. Join us on the
|
||||||
`Discord <https://discord.gg/SJK32GY>`_ where we have a *#catalyst_dev* channel
|
`Catalyst Forum <https://catalyst.enigma.co/>`_ for questions around Catalyst,
|
||||||
for questions around Catalyst, algorithmic trading and technical support.
|
algorithmic trading and technical support. We also have a
|
||||||
|
`Discord <https://discord.gg/SJK32GY>`_ group with the *#catalyst_dev* and
|
||||||
|
*#catalyst_setup* dedicated channels.
|
||||||
|
|
||||||
Overview
|
Overview
|
||||||
========
|
========
|
||||||
@@ -61,6 +64,9 @@ Go to our `Documentation Website <https://enigmampc.github.io/catalyst/>`_.
|
|||||||
.. |version status| image:: https://img.shields.io/pypi/pyversions/enigma-catalyst.svg
|
.. |version status| image:: https://img.shields.io/pypi/pyversions/enigma-catalyst.svg
|
||||||
:target: https://pypi.python.org/pypi/enigma-catalyst
|
:target: https://pypi.python.org/pypi/enigma-catalyst
|
||||||
|
|
||||||
|
.. |forum| image:: https://img.shields.io/badge/forum-join-green.svg
|
||||||
|
:target: https://catalyst.enigma.co/
|
||||||
|
|
||||||
.. |discord| image:: https://img.shields.io/badge/discord-join%20chat-green.svg
|
.. |discord| image:: https://img.shields.io/badge/discord-join%20chat-green.svg
|
||||||
:target: https://discordapp.com/invite/SJK32GY
|
:target: https://discordapp.com/invite/SJK32GY
|
||||||
|
|
||||||
|
|||||||
@@ -580,7 +580,7 @@ def ingest_exchange(ctx, exchange_name, data_frequency, start, end,
|
|||||||
|
|
||||||
exchange_bundle = ExchangeBundle(exchange_name)
|
exchange_bundle = ExchangeBundle(exchange_name)
|
||||||
|
|
||||||
click.echo('Ingesting exchange bundle {}...'.format(exchange_name),
|
click.echo('Trying to ingest exchange bundle {}...'.format(exchange_name),
|
||||||
sys.stdout)
|
sys.stdout)
|
||||||
exchange_bundle.ingest(
|
exchange_bundle.ingest(
|
||||||
data_frequency=data_frequency,
|
data_frequency=data_frequency,
|
||||||
@@ -793,7 +793,7 @@ def ls(ctx):
|
|||||||
)
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
def subscribe(ctx, dataset):
|
def subscribe(ctx, dataset):
|
||||||
"""Subscribe to an exisiting dataset.
|
"""Subscribe to an existing dataset.
|
||||||
"""
|
"""
|
||||||
marketplace = Marketplace()
|
marketplace = Marketplace()
|
||||||
marketplace.subscribe(dataset)
|
marketplace.subscribe(dataset)
|
||||||
|
|||||||
+15
-59
@@ -433,7 +433,7 @@ cdef class TradingPair(Asset):
|
|||||||
'taker',
|
'taker',
|
||||||
'trading_state',
|
'trading_state',
|
||||||
'data_source',
|
'data_source',
|
||||||
'decimals',
|
'decimals'
|
||||||
})
|
})
|
||||||
def __init__(self,
|
def __init__(self,
|
||||||
object symbol,
|
object symbol,
|
||||||
@@ -455,7 +455,7 @@ cdef class TradingPair(Asset):
|
|||||||
float taker=0.0025,
|
float taker=0.0025,
|
||||||
float lot=0,
|
float lot=0,
|
||||||
int decimals = 8,
|
int decimals = 8,
|
||||||
int trading_state=1,
|
int trading_state=0,
|
||||||
object data_source='catalyst'):
|
object data_source='catalyst'):
|
||||||
"""
|
"""
|
||||||
Replicates the Asset constructor with some built-in conventions
|
Replicates the Asset constructor with some built-in conventions
|
||||||
@@ -600,51 +600,14 @@ cdef class TradingPair(Asset):
|
|||||||
cpdef to_dict(self):
|
cpdef to_dict(self):
|
||||||
"""
|
"""
|
||||||
Convert to a python dict.
|
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',
|
|
||||||
"""
|
"""
|
||||||
trading_pair_dict = dict(
|
#TODO: missing fields
|
||||||
symbol=self.symbol,
|
super_dict = super(TradingPair, self).to_dict()
|
||||||
exchange=self.exchange,
|
super_dict['end_daily'] = self.end_daily
|
||||||
start_date=self.start_date,
|
super_dict['end_minute'] = self.end_minute
|
||||||
asset_name=self.asset_name,
|
super_dict['leverage'] = self.leverage
|
||||||
leverage=self.leverage,
|
super_dict['min_trade_size'] = self.min_trade_size
|
||||||
end_daily=self.end_daily,
|
return super_dict
|
||||||
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):
|
def is_exchange_open(self, dt_minute):
|
||||||
"""
|
"""
|
||||||
@@ -660,16 +623,6 @@ cdef class TradingPair(Asset):
|
|||||||
#TODO: make more dymanic to catch holds
|
#TODO: make more dymanic to catch holds
|
||||||
return True
|
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):
|
cpdef __reduce__(self):
|
||||||
"""
|
"""
|
||||||
Function used by pickle to determine how to serialize/deserialize this
|
Function used by pickle to determine how to serialize/deserialize this
|
||||||
@@ -677,23 +630,26 @@ cdef class TradingPair(Asset):
|
|||||||
and whose second element is a tuple of all the attributes that should
|
and whose second element is a tuple of all the attributes that should
|
||||||
be serialized/deserialized during pickling.
|
be serialized/deserialized during pickling.
|
||||||
"""
|
"""
|
||||||
#TODO: make sure that all fields set there
|
# added arguments for catalyst
|
||||||
return (self.__class__, (self.symbol,
|
return (self.__class__, (self.symbol,
|
||||||
self.exchange,
|
self.exchange,
|
||||||
self.start_date,
|
self.start_date,
|
||||||
self.asset_name,
|
self.asset_name,
|
||||||
self.sid,
|
self.sid,
|
||||||
self.leverage,
|
self.leverage,
|
||||||
|
self.end_daily,
|
||||||
|
self.end_minute,
|
||||||
self.end_date,
|
self.end_date,
|
||||||
|
self.exchange_symbol,
|
||||||
self.first_traded,
|
self.first_traded,
|
||||||
self.auto_close_date,
|
self.auto_close_date,
|
||||||
self.exchange_full,
|
self.exchange_full,
|
||||||
self.min_trade_size,
|
self.min_trade_size,
|
||||||
self.max_trade_size,
|
self.max_trade_size,
|
||||||
|
self.maker,
|
||||||
|
self.taker,
|
||||||
self.lot,
|
self.lot,
|
||||||
self.decimals,
|
self.decimals,
|
||||||
self.taker,
|
|
||||||
self.maker,
|
|
||||||
self.trading_state,
|
self.trading_state,
|
||||||
self.data_source))
|
self.data_source))
|
||||||
|
|
||||||
|
|||||||
@@ -11,10 +11,7 @@ LOG_LEVEL = int(os.environ.get('CATALYST_LOG_LEVEL', logbook.INFO))
|
|||||||
|
|
||||||
SYMBOLS_URL = 'https://s3.amazonaws.com/enigmaco/catalyst-exchanges/' \
|
SYMBOLS_URL = 'https://s3.amazonaws.com/enigmaco/catalyst-exchanges/' \
|
||||||
'{exchange}/symbols.json'
|
'{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_TIME_FORMAT = '%Y-%m-%d %H:%M'
|
||||||
DATE_FORMAT = '%Y-%m-%d'
|
DATE_FORMAT = '%Y-%m-%d'
|
||||||
|
|
||||||
@@ -28,8 +25,7 @@ AUTO_INGEST = False
|
|||||||
AUTH_SERVER = 'https://data.enigma.co'
|
AUTH_SERVER = 'https://data.enigma.co'
|
||||||
|
|
||||||
# TODO: switch to mainnet
|
# TODO: switch to mainnet
|
||||||
ETH_REMOTE_NODE = 'https://ropsten.infura.io/'
|
ETH_REMOTE_NODE = 'https://rinkeby.infura.io/'
|
||||||
|
|
||||||
|
|
||||||
MARKETPLACE_CONTRACT = 'https://raw.githubusercontent.com/enigmampc/' \
|
MARKETPLACE_CONTRACT = 'https://raw.githubusercontent.com/enigmampc/' \
|
||||||
'catalyst/master/catalyst/marketplace/' \
|
'catalyst/master/catalyst/marketplace/' \
|
||||||
@@ -40,10 +36,13 @@ MARKETPLACE_CONTRACT_ABI = 'https://raw.githubusercontent.com/enigmampc/' \
|
|||||||
'contract_marketplace_abi.json'
|
'contract_marketplace_abi.json'
|
||||||
|
|
||||||
# TODO: switch to mainnet
|
# TODO: switch to mainnet
|
||||||
ENIGMA_CONTRACT = 'https://raw.githubusercontent.com/enigmampc/catalyst/' \
|
ENIGMA_CONTRACT = 'https://raw.githubusercontent.com/enigmampc/' \
|
||||||
'master/catalyst/marketplace/' \
|
'catalyst/master/catalyst/marketplace/' \
|
||||||
'contract_enigma_address.txt'
|
'contract_enigma_address.txt'
|
||||||
|
|
||||||
ENIGMA_CONTRACT_ABI = 'https://raw.githubusercontent.com/enigmampc/' \
|
ENIGMA_CONTRACT_ABI = 'https://raw.githubusercontent.com/enigmampc/' \
|
||||||
'catalyst/master/catalyst/marketplace/' \
|
'catalyst/master/catalyst/marketplace/' \
|
||||||
'contract_enigma_abi.json'
|
'contract_enigma_abi.json'
|
||||||
|
|
||||||
|
SUPPORTED_WALLETS = ['metamask', 'ledger', 'trezor', 'bitbox', 'keystore',
|
||||||
|
'key']
|
||||||
|
|||||||
@@ -33,12 +33,12 @@ def initialize(context):
|
|||||||
# parameters or values you're going to use.
|
# parameters or values you're going to use.
|
||||||
|
|
||||||
# In our example, we're looking at Neo in Ether.
|
# In our example, we're looking at Neo in Ether.
|
||||||
context.market = symbol('eth_btc')
|
context.market = symbol('bnb_eth')
|
||||||
context.base_price = None
|
context.base_price = None
|
||||||
context.current_day = None
|
context.current_day = None
|
||||||
|
|
||||||
context.RSI_OVERSOLD = 55
|
context.RSI_OVERSOLD = 60
|
||||||
context.RSI_OVERBOUGHT = 60
|
context.RSI_OVERBOUGHT = 70
|
||||||
context.CANDLE_SIZE = '15T'
|
context.CANDLE_SIZE = '15T'
|
||||||
|
|
||||||
context.start_time = time.time()
|
context.start_time = time.time()
|
||||||
@@ -248,14 +248,14 @@ if __name__ == '__main__':
|
|||||||
|
|
||||||
if live:
|
if live:
|
||||||
run_algorithm(
|
run_algorithm(
|
||||||
capital_base=0.03,
|
capital_base=0.1,
|
||||||
initialize=initialize,
|
initialize=initialize,
|
||||||
handle_data=handle_data,
|
handle_data=handle_data,
|
||||||
analyze=analyze,
|
analyze=analyze,
|
||||||
exchange_name='poloniex',
|
exchange_name='binance',
|
||||||
live=True,
|
live=True,
|
||||||
algo_namespace=NAMESPACE,
|
algo_namespace=NAMESPACE,
|
||||||
base_currency='btc',
|
base_currency='eth',
|
||||||
live_graph=False,
|
live_graph=False,
|
||||||
simulate_orders=False,
|
simulate_orders=False,
|
||||||
stats_output=None,
|
stats_output=None,
|
||||||
@@ -274,7 +274,7 @@ if __name__ == '__main__':
|
|||||||
# -x bitfinex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion \
|
# -x bitfinex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion \
|
||||||
# --data-frequency minute --capital-base 10000
|
# --data-frequency minute --capital-base 10000
|
||||||
run_algorithm(
|
run_algorithm(
|
||||||
capital_base=0.1,
|
capital_base=0.035,
|
||||||
data_frequency='minute',
|
data_frequency='minute',
|
||||||
initialize=initialize,
|
initialize=initialize,
|
||||||
handle_data=handle_data,
|
handle_data=handle_data,
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ def handle_data(context, data):
|
|||||||
context.asset,
|
context.asset,
|
||||||
fields='price',
|
fields='price',
|
||||||
bar_count=20,
|
bar_count=20,
|
||||||
frequency='2H'
|
frequency='30T'
|
||||||
)
|
)
|
||||||
last_traded = prices.index[-1]
|
last_traded = prices.index[-1]
|
||||||
log.info('last candle date: {}'.format(last_traded))
|
log.info('last candle date: {}'.format(last_traded))
|
||||||
|
|||||||
@@ -1,33 +1,34 @@
|
|||||||
|
import json
|
||||||
|
import os
|
||||||
import re
|
import re
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
|
||||||
import ccxt
|
import ccxt
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import six
|
import six
|
||||||
from catalyst.assets._assets import TradingPair
|
from ccxt import InvalidOrder, NetworkError, \
|
||||||
from redo import retry
|
ExchangeError
|
||||||
|
from logbook import Logger
|
||||||
|
from six import string_types
|
||||||
|
|
||||||
from catalyst.algorithm import MarketOrder
|
from catalyst.algorithm import MarketOrder
|
||||||
|
from catalyst.assets._assets import TradingPair
|
||||||
from catalyst.constants import LOG_LEVEL
|
from catalyst.constants import LOG_LEVEL
|
||||||
from catalyst.exchange.exchange import Exchange
|
from catalyst.exchange.exchange import Exchange
|
||||||
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
||||||
from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \
|
from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \
|
||||||
ExchangeSymbolsNotFound, ExchangeRequestError, InvalidOrderStyle, \
|
ExchangeSymbolsNotFound, ExchangeRequestError, InvalidOrderStyle, \
|
||||||
UnsupportedHistoryFrequencyError, \
|
|
||||||
ExchangeNotFoundError, CreateOrderError, InvalidHistoryTimeframeError, \
|
ExchangeNotFoundError, CreateOrderError, InvalidHistoryTimeframeError, \
|
||||||
MarketsNotFoundError, InvalidMarketError
|
UnsupportedHistoryFrequencyError
|
||||||
from catalyst.exchange.exchange_execution import ExchangeLimitOrder
|
from catalyst.exchange.exchange_execution import ExchangeLimitOrder
|
||||||
from catalyst.exchange.utils.ccxt_utils import get_exchange_config
|
from catalyst.exchange.utils.exchange_utils import mixin_market_params, \
|
||||||
|
get_exchange_folder, get_catalyst_symbol, \
|
||||||
|
get_exchange_auth
|
||||||
from catalyst.exchange.utils.datetime_utils import from_ms_timestamp, \
|
from catalyst.exchange.utils.datetime_utils import from_ms_timestamp, \
|
||||||
get_epoch, \
|
get_epoch, \
|
||||||
get_periods_range
|
get_periods_range
|
||||||
from catalyst.exchange.utils.exchange_utils import get_catalyst_symbol
|
|
||||||
from catalyst.finance.order import Order, ORDER_STATUS
|
from catalyst.finance.order import Order, ORDER_STATUS
|
||||||
from catalyst.finance.transaction import Transaction
|
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)
|
log = Logger('CCXT', level=LOG_LEVEL)
|
||||||
|
|
||||||
@@ -43,7 +44,7 @@ SUPPORTED_EXCHANGES = dict(
|
|||||||
|
|
||||||
class CCXT(Exchange):
|
class CCXT(Exchange):
|
||||||
def __init__(self, exchange_name, key,
|
def __init__(self, exchange_name, key,
|
||||||
secret, password, base_currency, config=None):
|
secret, password, base_currency):
|
||||||
log.debug(
|
log.debug(
|
||||||
'finding {} in CCXT exchanges:\n{}'.format(
|
'finding {} in CCXT exchanges:\n{}'.format(
|
||||||
exchange_name, ccxt.exchanges
|
exchange_name, ccxt.exchanges
|
||||||
@@ -63,8 +64,6 @@ class CCXT(Exchange):
|
|||||||
'password': password,
|
'password': password,
|
||||||
})
|
})
|
||||||
self.api.enableRateLimit = True
|
self.api.enableRateLimit = True
|
||||||
self.has = self.api.has
|
|
||||||
self.fees = self.api.fees
|
|
||||||
|
|
||||||
except Exception:
|
except Exception:
|
||||||
raise ExchangeNotFoundError(exchange_name=exchange_name)
|
raise ExchangeNotFoundError(exchange_name=exchange_name)
|
||||||
@@ -72,7 +71,6 @@ class CCXT(Exchange):
|
|||||||
self._symbol_maps = [None, None]
|
self._symbol_maps = [None, None]
|
||||||
|
|
||||||
self.name = exchange_name
|
self.name = exchange_name
|
||||||
self.assets = []
|
|
||||||
|
|
||||||
self.base_currency = base_currency
|
self.base_currency = base_currency
|
||||||
self.transactions = defaultdict(list)
|
self.transactions = defaultdict(list)
|
||||||
@@ -84,123 +82,97 @@ class CCXT(Exchange):
|
|||||||
self._common_symbols = dict()
|
self._common_symbols = dict()
|
||||||
|
|
||||||
self.bundle = ExchangeBundle(self.name)
|
self.bundle = ExchangeBundle(self.name)
|
||||||
|
self.markets = None
|
||||||
self._is_init = False
|
self._is_init = False
|
||||||
self._config = config
|
|
||||||
|
|
||||||
def init(self):
|
def init(self):
|
||||||
if self._is_init:
|
if self._is_init:
|
||||||
return
|
return
|
||||||
|
|
||||||
if self._config is None:
|
exchange_folder = get_exchange_folder(self.name)
|
||||||
self._config = get_exchange_config(self.name)
|
filename = os.path.join(exchange_folder, 'cctx_markets.json')
|
||||||
log.debug(
|
|
||||||
'got exchange config {}:\n{}'.format(
|
|
||||||
self.name, self._config
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
self.load_assets()
|
if os.path.exists(filename):
|
||||||
self._is_init = True
|
timestamp = os.path.getmtime(filename)
|
||||||
|
dt = pd.to_datetime(timestamp, unit='s', utc=True)
|
||||||
|
|
||||||
def load_assets(self):
|
if dt >= pd.Timestamp.utcnow().floor('1D'):
|
||||||
if self._config is None:
|
with open(filename) as f:
|
||||||
raise ValueError('Exchange config not available.')
|
self.markets = json.load(f)
|
||||||
|
|
||||||
self.assets = []
|
log.debug('loaded markets for {}'.format(self.name))
|
||||||
for asset_dict in self._config['assets']:
|
|
||||||
asset = TradingPair(**asset_dict)
|
|
||||||
self.assets.append(asset)
|
|
||||||
|
|
||||||
def _fetch_markets(self):
|
if self.markets is None:
|
||||||
|
try:
|
||||||
markets_symbols = self.api.load_markets()
|
markets_symbols = self.api.load_markets()
|
||||||
log.debug(
|
log.debug(
|
||||||
'fetching {} markets:\n{}'.format(
|
'fetching {} markets:\n{}'.format(
|
||||||
self.name, markets_symbols
|
self.name, markets_symbols
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
try:
|
|
||||||
markets = self.api.fetch_markets()
|
|
||||||
|
|
||||||
except NetworkError as e:
|
self.markets = self.api.fetch_markets()
|
||||||
|
with open(filename, 'w+') as f:
|
||||||
|
json.dump(self.markets, f, indent=4)
|
||||||
|
|
||||||
|
except (ExchangeError, NetworkError) as e:
|
||||||
|
log.warn(
|
||||||
|
'unable to fetch markets {}: {}'.format(
|
||||||
|
self.name, e
|
||||||
|
)
|
||||||
|
)
|
||||||
raise ExchangeRequestError(error=e)
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
if not markets:
|
self.load_assets()
|
||||||
raise MarketsNotFoundError(
|
self._is_init = True
|
||||||
exchange=self.name,
|
|
||||||
|
@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)
|
||||||
|
|
||||||
|
exchange_names = []
|
||||||
|
for exchange_name in ccxt.exchanges:
|
||||||
|
if is_authenticated:
|
||||||
|
exchange_auth = get_exchange_auth(exchange_name)
|
||||||
|
|
||||||
|
has_auth = (exchange_auth['key'] != ''
|
||||||
|
and exchange_auth['secret'] != '')
|
||||||
|
|
||||||
|
if not has_auth:
|
||||||
|
continue
|
||||||
|
|
||||||
|
log.debug('loading exchange: {}'.format(exchange_name))
|
||||||
|
exchange = getattr(ccxt, exchange_name)()
|
||||||
|
|
||||||
|
if ccxt_features is None:
|
||||||
|
has_feature = True
|
||||||
|
|
||||||
|
else:
|
||||||
|
try:
|
||||||
|
has_feature = all(
|
||||||
|
[exchange.has[feature] for feature in ccxt_features]
|
||||||
)
|
)
|
||||||
|
|
||||||
for market in markets:
|
except Exception:
|
||||||
if 'id' not in market:
|
has_feature = False
|
||||||
raise InvalidMarketError(
|
|
||||||
exchange=self.name,
|
|
||||||
market=market,
|
|
||||||
)
|
|
||||||
return markets
|
|
||||||
|
|
||||||
def create_exchange_config(self):
|
if has_feature:
|
||||||
config = dict(
|
try:
|
||||||
name=self.name,
|
log.info('initializing {}'.format(exchange_name))
|
||||||
features=[feature for feature in self.has if self.has[feature]]
|
exchange_names.append(exchange_name)
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
log.warn(
|
||||||
|
'unable to initialize exchange {}: {}'.format(
|
||||||
|
exchange_name, e
|
||||||
)
|
)
|
||||||
markets = retry(
|
|
||||||
action=self._fetch_markets,
|
|
||||||
attempts=5,
|
|
||||||
sleeptime=5,
|
|
||||||
retry_exceptions=(ExchangeRequestError,),
|
|
||||||
cleanup=lambda: log.warn(
|
|
||||||
'fetching markets again for {}'.format(self.name)
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
|
|
||||||
config['assets'] = []
|
return exchange_names
|
||||||
for market in markets:
|
|
||||||
asset = self.create_trading_pair(market=market)
|
|
||||||
config['assets'].append(asset)
|
|
||||||
|
|
||||||
return config
|
|
||||||
|
|
||||||
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.
|
|
||||||
|
|
||||||
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):
|
def account(self):
|
||||||
return None
|
return None
|
||||||
@@ -218,6 +190,9 @@ class CCXT(Exchange):
|
|||||||
if data_frequency == 'minute' and not freq.endswith('T'):
|
if data_frequency == 'minute' and not freq.endswith('T'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
|
elif data_frequency == 'hourly' and not freq.endswith('D'):
|
||||||
|
continue
|
||||||
|
|
||||||
elif data_frequency == 'daily' and not freq.endswith('D'):
|
elif data_frequency == 'daily' and not freq.endswith('D'):
|
||||||
continue
|
continue
|
||||||
|
|
||||||
@@ -232,11 +207,32 @@ class CCXT(Exchange):
|
|||||||
|
|
||||||
return frequencies
|
return frequencies
|
||||||
|
|
||||||
|
def get_market(self, symbol):
|
||||||
|
"""
|
||||||
|
The CCXT market.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
symbol:
|
||||||
|
The CCXT symbol.
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
dict[str, Object]
|
||||||
|
|
||||||
|
"""
|
||||||
|
s = self.get_symbol(symbol)
|
||||||
|
market = next(
|
||||||
|
(market for market in self.markets if market['symbol'] == s),
|
||||||
|
None,
|
||||||
|
)
|
||||||
|
return market
|
||||||
|
|
||||||
def substitute_currency_code(self, currency, source='catalyst'):
|
def substitute_currency_code(self, currency, source='catalyst'):
|
||||||
if source == 'catalyst':
|
if source == 'catalyst':
|
||||||
currency = currency.upper()
|
currency = currency.upper()
|
||||||
|
|
||||||
key = self.api.common_currency_code(currency).lower()
|
key = self.api.common_currency_code(currency)
|
||||||
self._common_symbols[key] = currency.lower()
|
self._common_symbols[key] = currency.lower()
|
||||||
return key
|
return key
|
||||||
|
|
||||||
@@ -264,7 +260,13 @@ class CCXT(Exchange):
|
|||||||
if source == 'ccxt':
|
if source == 'ccxt':
|
||||||
if isinstance(asset_or_symbol, string_types):
|
if isinstance(asset_or_symbol, string_types):
|
||||||
parts = asset_or_symbol.split('/')
|
parts = asset_or_symbol.split('/')
|
||||||
return '{}_{}'.format(parts[0].lower(), parts[1].lower())
|
base_currency = self.substitute_currency_code(
|
||||||
|
parts[0], source
|
||||||
|
)
|
||||||
|
quote_currency = self.substitute_currency_code(
|
||||||
|
parts[1], source
|
||||||
|
)
|
||||||
|
return '{}_{}'.format(base_currency, quote_currency)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return asset_or_symbol.symbol
|
return asset_or_symbol.symbol
|
||||||
@@ -275,7 +277,13 @@ class CCXT(Exchange):
|
|||||||
) else asset_or_symbol.symbol
|
) else asset_or_symbol.symbol
|
||||||
|
|
||||||
parts = symbol.split('_')
|
parts = symbol.split('_')
|
||||||
return '{}/{}'.format(parts[0].upper(), parts[1].upper())
|
base_currency = self.substitute_currency_code(
|
||||||
|
parts[0], source
|
||||||
|
)
|
||||||
|
quote_currency = self.substitute_currency_code(
|
||||||
|
parts[1], source
|
||||||
|
)
|
||||||
|
return '{}/{}'.format(base_currency, quote_currency)
|
||||||
|
|
||||||
@staticmethod
|
@staticmethod
|
||||||
def map_frequency(value, source='ccxt', raise_error=True):
|
def map_frequency(value, source='ccxt', raise_error=True):
|
||||||
@@ -401,7 +409,7 @@ class CCXT(Exchange):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def get_candles(self, freq, assets, bar_count=1, start_dt=None,
|
def get_candles(self, freq, assets, bar_count=1, start_dt=None,
|
||||||
end_dt=None, floor_dates=True):
|
end_dt=None):
|
||||||
is_single = (isinstance(assets, TradingPair))
|
is_single = (isinstance(assets, TradingPair))
|
||||||
if is_single:
|
if is_single:
|
||||||
assets = [assets]
|
assets = [assets]
|
||||||
@@ -448,20 +456,16 @@ class CCXT(Exchange):
|
|||||||
|
|
||||||
candles[asset] = []
|
candles[asset] = []
|
||||||
for ohlcv in ohlcvs:
|
for ohlcv in ohlcvs:
|
||||||
dt = pd.to_datetime(ohlcv[0], unit='ms', utc=True)
|
candles[asset].append(dict(
|
||||||
if floor_dates:
|
last_traded=pd.to_datetime(
|
||||||
dt = dt.floor('1T')
|
ohlcv[0], unit='ms', utc=True
|
||||||
|
),
|
||||||
candles[asset].append(
|
|
||||||
dict(
|
|
||||||
last_traded=dt,
|
|
||||||
open=ohlcv[1],
|
open=ohlcv[1],
|
||||||
high=ohlcv[2],
|
high=ohlcv[2],
|
||||||
low=ohlcv[3],
|
low=ohlcv[3],
|
||||||
close=ohlcv[4],
|
close=ohlcv[4],
|
||||||
volume=ohlcv[5],
|
volume=ohlcv[5]
|
||||||
)
|
))
|
||||||
)
|
|
||||||
candles[asset] = sorted(
|
candles[asset] = sorted(
|
||||||
candles[asset], key=lambda c: c['last_traded']
|
candles[asset], key=lambda c: c['last_traded']
|
||||||
)
|
)
|
||||||
@@ -479,53 +483,144 @@ class CCXT(Exchange):
|
|||||||
except ExchangeSymbolsNotFound:
|
except ExchangeSymbolsNotFound:
|
||||||
return None
|
return None
|
||||||
|
|
||||||
def apply_conditional_market_params(self, params, market):
|
def get_asset_defs(self, market):
|
||||||
"""
|
"""
|
||||||
Applies a CCXT market dict to parameters of TradingPair init.
|
The local and Catalyst definitions of the specified market.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
params: dict[Object]
|
market: dict[str, Object]
|
||||||
market: dict[Object]
|
The CCXT market dicts.
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
else:
|
||||||
|
return None
|
||||||
|
|
||||||
|
def create_trading_pair(self, market, asset_def=None, is_local=False):
|
||||||
|
"""
|
||||||
|
Creating a TradingPair from market and asset data.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
market: dict[str, Object]
|
||||||
|
asset_def: dict[str, Object]
|
||||||
|
is_local: bool
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
|
||||||
"""
|
"""
|
||||||
# TODO: make this more externalized / configurable
|
data_source = 'local' if is_local else 'catalyst'
|
||||||
# Consider representing in some type of JSON structure
|
params = dict(
|
||||||
if 'active' in market:
|
exchange=self.name,
|
||||||
params['trading_state'] = 1 if market['active'] else 0
|
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
|
||||||
|
|
||||||
else:
|
else:
|
||||||
params['trading_state'] = 1
|
params['symbol'] = get_catalyst_symbol(market)
|
||||||
|
# TODO: add as an optional column
|
||||||
|
params['leverage'] = 1.0
|
||||||
|
|
||||||
if 'lot' in market:
|
return TradingPair(**params)
|
||||||
params['min_trade_size'] = market['lot']
|
|
||||||
params['lot'] = market['lot']
|
|
||||||
|
|
||||||
if self.name == 'bitfinex':
|
def load_assets(self):
|
||||||
params['maker'] = 0.001
|
log.debug('loading assets for {}'.format(self.name))
|
||||||
params['taker'] = 0.002
|
self.assets = []
|
||||||
|
|
||||||
elif 'maker' in market and 'taker' in market \
|
for market in self.markets:
|
||||||
and market['maker'] is not None \
|
if 'id' not in market:
|
||||||
and market['taker'] is not None:
|
log.warn('invalid market: {}'.format(market))
|
||||||
params['maker'] = market['maker']
|
continue
|
||||||
params['taker'] = market['taker']
|
|
||||||
|
|
||||||
else:
|
asset_defs = self.get_asset_defs(market)
|
||||||
# TODO: default commission, make configurable
|
|
||||||
params['maker'] = 0.0015
|
|
||||||
params['taker'] = 0.0025
|
|
||||||
|
|
||||||
info = market['info'] if 'info' in market else None
|
asset = None
|
||||||
if info:
|
for asset_def in asset_defs:
|
||||||
if 'minimum_order_size' in info:
|
if asset_def[0] is not None or not asset_defs[1]:
|
||||||
params['min_trade_size'] = float(info['minimum_order_size'])
|
try:
|
||||||
|
asset = self.create_trading_pair(
|
||||||
|
market=market,
|
||||||
|
asset_def=asset_def[0],
|
||||||
|
is_local=asset_def[1]
|
||||||
|
)
|
||||||
|
self.assets.append(asset)
|
||||||
|
|
||||||
if 'lot' not in params:
|
except TypeError as e:
|
||||||
params['lot'] = params['min_trade_size']
|
log.warn('unable to add asset: {}'.format(e))
|
||||||
|
|
||||||
|
if asset is None:
|
||||||
|
asset = self.create_trading_pair(market=market)
|
||||||
|
self.assets.append(asset)
|
||||||
|
|
||||||
def get_balances(self):
|
def get_balances(self):
|
||||||
try:
|
try:
|
||||||
@@ -663,14 +758,18 @@ class CCXT(Exchange):
|
|||||||
|
|
||||||
side = 'buy' if amount > 0 else 'sell'
|
side = 'buy' if amount > 0 else 'sell'
|
||||||
if hasattr(self.api, 'amount_to_lots'):
|
if hasattr(self.api, 'amount_to_lots'):
|
||||||
adj_amount = self.api.amount_to_lots(
|
# TODO: is this right?
|
||||||
symbol=symbol,
|
if self.api.markets is None:
|
||||||
amount=abs(amount),
|
self.api.load_markets()
|
||||||
)
|
|
||||||
if adj_amount != abs(amount):
|
# https://github.com/ccxt/ccxt/issues/1483
|
||||||
log.info(
|
adj_amount = round(abs(amount), asset.decimals)
|
||||||
'adjusted order amount {} to {} based on lot size'.format(
|
market = self.api.markets[symbol]
|
||||||
abs(amount), adj_amount,
|
if 'lots' in market and market['lots'] > amount:
|
||||||
|
raise CreateOrderError(
|
||||||
|
exchange=self.name,
|
||||||
|
e='order amount lower than the smallest lot: {}'.format(
|
||||||
|
amount
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1016,25 +1115,17 @@ class CCXT(Exchange):
|
|||||||
return result
|
return result
|
||||||
|
|
||||||
def get_trades(self, asset, my_trades=True, start_dt=None, limit=100):
|
def get_trades(self, asset, my_trades=True, start_dt=None, limit=100):
|
||||||
|
if not my_trades:
|
||||||
|
raise NotImplemented(
|
||||||
|
'get_trades only supports "my trades"'
|
||||||
|
)
|
||||||
|
|
||||||
# TODO: is it possible to sort this? Limit is useless otherwise.
|
# TODO: is it possible to sort this? Limit is useless otherwise.
|
||||||
ccxt_symbol = self.get_symbol(asset)
|
ccxt_symbol = self.get_symbol(asset)
|
||||||
if start_dt:
|
|
||||||
delta = start_dt - get_epoch()
|
|
||||||
since = int(delta.total_seconds()) * 1000
|
|
||||||
else:
|
|
||||||
since = None
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
if my_trades:
|
|
||||||
trades = self.api.fetch_my_trades(
|
trades = self.api.fetch_my_trades(
|
||||||
symbol=ccxt_symbol,
|
symbol=ccxt_symbol,
|
||||||
since=since,
|
since=start_dt,
|
||||||
limit=limit,
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
trades = self.api.fetch_trades(
|
|
||||||
symbol=ccxt_symbol,
|
|
||||||
since=since,
|
|
||||||
limit=limit,
|
limit=limit,
|
||||||
)
|
)
|
||||||
except (ExchangeError, NetworkError) as e:
|
except (ExchangeError, NetworkError) as e:
|
||||||
|
|||||||
@@ -5,8 +5,6 @@ from time import sleep
|
|||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from logbook import Logger
|
|
||||||
|
|
||||||
from catalyst.constants import LOG_LEVEL
|
from catalyst.constants import LOG_LEVEL
|
||||||
from catalyst.data.data_portal import BASE_FIELDS
|
from catalyst.data.data_portal import BASE_FIELDS
|
||||||
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
from catalyst.exchange.exchange_bundle import ExchangeBundle
|
||||||
@@ -18,9 +16,11 @@ from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \
|
|||||||
TickerNotFoundError, NotEnoughCashError
|
TickerNotFoundError, NotEnoughCashError
|
||||||
from catalyst.exchange.utils.datetime_utils import get_delta, \
|
from catalyst.exchange.utils.datetime_utils import get_delta, \
|
||||||
get_periods_range, \
|
get_periods_range, \
|
||||||
get_periods, get_start_dt, get_frequency
|
get_periods, get_start_dt, get_frequency, \
|
||||||
from catalyst.exchange.utils.exchange_utils import \
|
get_candles_number_from_minutes
|
||||||
resample_history_df, has_bundle
|
from catalyst.exchange.utils.exchange_utils import get_exchange_symbols, \
|
||||||
|
resample_history_df, has_bundle, get_candles_df
|
||||||
|
from logbook import Logger
|
||||||
|
|
||||||
log = Logger('Exchange', level=LOG_LEVEL)
|
log = Logger('Exchange', level=LOG_LEVEL)
|
||||||
|
|
||||||
@@ -199,12 +199,8 @@ class Exchange:
|
|||||||
)
|
)
|
||||||
assets.append(asset)
|
assets.append(asset)
|
||||||
|
|
||||||
except SymbolNotFoundOnExchange:
|
except SymbolNotFoundOnExchange as e:
|
||||||
log.debug(
|
log.warn(e)
|
||||||
'skipping non-existent market {} {}'.format(
|
|
||||||
self.name, symbol
|
|
||||||
)
|
|
||||||
)
|
|
||||||
return assets
|
return assets
|
||||||
|
|
||||||
def get_asset(self, symbol, data_frequency=None, is_exchange_symbol=False,
|
def get_asset(self, symbol, data_frequency=None, is_exchange_symbol=False,
|
||||||
@@ -257,10 +253,10 @@ class Exchange:
|
|||||||
elif data_frequency is not None:
|
elif data_frequency is not None:
|
||||||
applies = (
|
applies = (
|
||||||
(
|
(
|
||||||
data_frequency == 'minute' and a.end_minute is not None
|
data_frequency == 'minute' and
|
||||||
) or (
|
a.end_minute is not None)
|
||||||
data_frequency == 'daily' and a.end_daily is not None
|
or (
|
||||||
)
|
data_frequency == 'daily' and a.end_daily is not None)
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
@@ -293,6 +289,16 @@ class Exchange:
|
|||||||
log.debug('found asset: {}'.format(asset))
|
log.debug('found asset: {}'.format(asset))
|
||||||
return 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
|
@abstractmethod
|
||||||
def init(self):
|
def init(self):
|
||||||
"""
|
"""
|
||||||
@@ -304,13 +310,24 @@ class Exchange:
|
|||||||
"""
|
"""
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def create_exchange_config(self):
|
def load_assets(self, is_local=False):
|
||||||
"""
|
"""
|
||||||
Fetch the exchange market data and generate a config object
|
Populate the 'assets' attribute with a dictionary of Assets.
|
||||||
Returns
|
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.
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
def get_spot_value(self, assets, field, dt=None, data_frequency='minute'):
|
def get_spot_value(self, assets, field, dt=None, data_frequency='minute'):
|
||||||
"""
|
"""
|
||||||
@@ -491,7 +508,12 @@ class Exchange:
|
|||||||
# so we request more than needed
|
# so we request more than needed
|
||||||
# TODO: consider defining a const per asset
|
# TODO: consider defining a const per asset
|
||||||
# and/or some retry mechanism (in each iteration request more data)
|
# and/or some retry mechanism (in each iteration request more data)
|
||||||
requested_bar_count = bar_count + 30
|
kExtra_minutes_candles = 150
|
||||||
|
requested_bar_count = bar_count + \
|
||||||
|
get_candles_number_from_minutes(unit,
|
||||||
|
candle_size,
|
||||||
|
kExtra_minutes_candles)
|
||||||
|
|
||||||
# The get_history method supports multiple asset
|
# The get_history method supports multiple asset
|
||||||
candles = self.get_candles(
|
candles = self.get_candles(
|
||||||
freq=freq,
|
freq=freq,
|
||||||
@@ -509,11 +531,14 @@ class Exchange:
|
|||||||
asset=asset,
|
asset=asset,
|
||||||
exchange=self.name)
|
exchange=self.name)
|
||||||
|
|
||||||
|
# for avoiding unnecessary forward fill end_dt is taken back one second
|
||||||
|
forward_fill_till_dt = end_dt - timedelta(seconds=1)
|
||||||
|
|
||||||
series = get_candles_df(candles=candles,
|
series = get_candles_df(candles=candles,
|
||||||
field=field,
|
field=field,
|
||||||
freq=frequency,
|
freq=frequency,
|
||||||
bar_count=requested_bar_count,
|
bar_count=requested_bar_count,
|
||||||
end_dt=end_dt)
|
end_dt=forward_fill_till_dt)
|
||||||
|
|
||||||
# TODO: consider how to approach this edge case
|
# TODO: consider how to approach this edge case
|
||||||
# delta_candle_size = candle_size * 60 if unit == 'H' else candle_size
|
# delta_candle_size = candle_size * 60 if unit == 'H' else candle_size
|
||||||
@@ -582,7 +607,7 @@ class Exchange:
|
|||||||
# TODO: this function needs some work,
|
# TODO: this function needs some work,
|
||||||
# we're currently using it just for benchmark data
|
# we're currently using it just for benchmark data
|
||||||
freq, candle_size, unit, data_frequency = get_frequency(
|
freq, candle_size, unit, data_frequency = get_frequency(
|
||||||
frequency, data_frequency
|
frequency, data_frequency, supported_freqs=['T', 'D']
|
||||||
)
|
)
|
||||||
adj_bar_count = candle_size * bar_count
|
adj_bar_count = candle_size * bar_count
|
||||||
try:
|
try:
|
||||||
@@ -647,20 +672,16 @@ class Exchange:
|
|||||||
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
def _check_low_balance(self, currency, balances, amount, open_orders=None):
|
def _check_low_balance(self, currency, balances, amount):
|
||||||
free = balances[currency]['free'] if currency in balances else 0.0
|
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:
|
if free < amount:
|
||||||
return free, True
|
return free, True
|
||||||
|
|
||||||
else:
|
else:
|
||||||
return free, False
|
return free, False
|
||||||
|
|
||||||
def sync_positions(self, positions, open_orders=None, cash=None,
|
def sync_positions(self, positions, cash=None,
|
||||||
check_balances=False):
|
check_balances=False):
|
||||||
"""
|
"""
|
||||||
Update the portfolio cash and position balances based on the
|
Update the portfolio cash and position balances based on the
|
||||||
@@ -690,7 +711,7 @@ class Exchange:
|
|||||||
balances=balances,
|
balances=balances,
|
||||||
amount=cash,
|
amount=cash,
|
||||||
)
|
)
|
||||||
if is_lower and not open_orders:
|
if is_lower:
|
||||||
raise NotEnoughCashError(
|
raise NotEnoughCashError(
|
||||||
currency=self.base_currency,
|
currency=self.base_currency,
|
||||||
exchange=self.name,
|
exchange=self.name,
|
||||||
|
|||||||
@@ -18,11 +18,9 @@ from datetime import timedelta
|
|||||||
from os import listdir
|
from os import listdir
|
||||||
from os.path import isfile, join, exists
|
from os.path import isfile, join, exists
|
||||||
|
|
||||||
|
import catalyst.protocol as zp
|
||||||
import logbook
|
import logbook
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from redo import retry
|
|
||||||
|
|
||||||
import catalyst.protocol as zp
|
|
||||||
from catalyst.algorithm import TradingAlgorithm
|
from catalyst.algorithm import TradingAlgorithm
|
||||||
from catalyst.constants import LOG_LEVEL
|
from catalyst.constants import LOG_LEVEL
|
||||||
from catalyst.exchange.exchange_blotter import ExchangeBlotter
|
from catalyst.exchange.exchange_blotter import ExchangeBlotter
|
||||||
@@ -52,6 +50,7 @@ from catalyst.utils.api_support import api_method
|
|||||||
from catalyst.utils.input_validation import error_keywords, ensure_upper_case
|
from catalyst.utils.input_validation import error_keywords, ensure_upper_case
|
||||||
from catalyst.utils.math_utils import round_nearest
|
from catalyst.utils.math_utils import round_nearest
|
||||||
from catalyst.utils.preprocess import preprocess
|
from catalyst.utils.preprocess import preprocess
|
||||||
|
from redo import retry
|
||||||
|
|
||||||
log = logbook.Logger('exchange_algorithm', level=LOG_LEVEL)
|
log = logbook.Logger('exchange_algorithm', level=LOG_LEVEL)
|
||||||
|
|
||||||
@@ -671,7 +670,6 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
|
|||||||
required_cash = self.portfolio.cash if not orders else None
|
required_cash = self.portfolio.cash if not orders else None
|
||||||
cash, positions_value = exchange.sync_positions(
|
cash, positions_value = exchange.sync_positions(
|
||||||
positions=exchange_positions,
|
positions=exchange_positions,
|
||||||
open_orders=orders,
|
|
||||||
check_balances=check_balances,
|
check_balances=check_balances,
|
||||||
cash=required_cash,
|
cash=required_cash,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import copy
|
|
||||||
import os
|
import os
|
||||||
import shutil
|
import shutil
|
||||||
from datetime import timedelta
|
from datetime import timedelta
|
||||||
@@ -9,12 +8,8 @@ from operator import is_not
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytz
|
import pytz
|
||||||
from catalyst.assets._assets import TradingPair
|
|
||||||
from logbook import Logger
|
|
||||||
from pytz import UTC
|
|
||||||
from six import itervalues
|
|
||||||
|
|
||||||
from catalyst import get_calendar
|
from catalyst import get_calendar
|
||||||
|
from catalyst.assets._assets import TradingPair
|
||||||
from catalyst.constants import DATE_TIME_FORMAT, AUTO_INGEST
|
from catalyst.constants import DATE_TIME_FORMAT, AUTO_INGEST
|
||||||
from catalyst.constants import LOG_LEVEL
|
from catalyst.constants import LOG_LEVEL
|
||||||
from catalyst.data.minute_bars import BcolzMinuteOverlappingData, \
|
from catalyst.data.minute_bars import BcolzMinuteOverlappingData, \
|
||||||
@@ -28,11 +23,14 @@ from catalyst.exchange.exchange_errors import EmptyValuesInBundleError, \
|
|||||||
from catalyst.exchange.utils.bundle_utils import range_in_bundle, \
|
from catalyst.exchange.utils.bundle_utils import range_in_bundle, \
|
||||||
get_bcolz_chunk, get_df_from_arrays, get_assets
|
get_bcolz_chunk, get_df_from_arrays, get_assets
|
||||||
from catalyst.exchange.utils.datetime_utils import get_start_dt, \
|
from catalyst.exchange.utils.datetime_utils import get_start_dt, \
|
||||||
get_period_label, get_month_start_end, get_year_start_end, get_period, \
|
get_period_label, get_month_start_end, get_year_start_end
|
||||||
timestr_to_dt
|
from catalyst.exchange.utils.exchange_utils import get_exchange_folder, \
|
||||||
from catalyst.exchange.utils.exchange_utils import get_exchange_folder
|
save_exchange_symbols, mixin_market_params, get_catalyst_symbol
|
||||||
from catalyst.utils.cli import maybe_show_progress
|
from catalyst.utils.cli import maybe_show_progress
|
||||||
from catalyst.utils.paths import ensure_directory
|
from catalyst.utils.paths import ensure_directory
|
||||||
|
from logbook import Logger
|
||||||
|
from pytz import UTC
|
||||||
|
from six import itervalues
|
||||||
|
|
||||||
log = Logger('exchange_bundle', level=LOG_LEVEL)
|
log = Logger('exchange_bundle', level=LOG_LEVEL)
|
||||||
|
|
||||||
@@ -514,8 +512,8 @@ class ExchangeBundle:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
dates = pd.date_range(
|
dates = pd.date_range(
|
||||||
start=get_period(adj_start, data_frequency),
|
start=get_period_label(adj_start, data_frequency),
|
||||||
end=get_period(adj_end, data_frequency),
|
end=get_period_label(adj_end, data_frequency),
|
||||||
freq='MS' if data_frequency == 'minute' else 'AS',
|
freq='MS' if data_frequency == 'minute' else 'AS',
|
||||||
tz=UTC
|
tz=UTC
|
||||||
)
|
)
|
||||||
@@ -554,9 +552,7 @@ class ExchangeBundle:
|
|||||||
|
|
||||||
# We sort the chunks by end date to ingest most recent data first
|
# We sort the chunks by end date to ingest most recent data first
|
||||||
chunks[asset].sort(
|
chunks[asset].sort(
|
||||||
key=lambda chunk: timestr_to_dt(
|
key=lambda chunk: pd.to_datetime(chunk['period'])
|
||||||
chunk['period'], data_frequency
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
|
|
||||||
return chunks
|
return chunks
|
||||||
@@ -602,6 +598,7 @@ class ExchangeBundle:
|
|||||||
# we want to give an end_date far in time
|
# we want to give an end_date far in time
|
||||||
writer = self.get_writer(start_dt, end_dt, data_frequency)
|
writer = self.get_writer(start_dt, end_dt, data_frequency)
|
||||||
if show_breakdown:
|
if show_breakdown:
|
||||||
|
if chunks:
|
||||||
for asset in chunks:
|
for asset in chunks:
|
||||||
with maybe_show_progress(
|
with maybe_show_progress(
|
||||||
chunks[asset],
|
chunks[asset],
|
||||||
@@ -611,8 +608,7 @@ class ExchangeBundle:
|
|||||||
exchange=self.exchange_name,
|
exchange=self.exchange_name,
|
||||||
frequency=data_frequency,
|
frequency=data_frequency,
|
||||||
symbol=asset.symbol
|
symbol=asset.symbol
|
||||||
)
|
)) as it:
|
||||||
) as it:
|
|
||||||
for chunk in it:
|
for chunk in it:
|
||||||
problems += self.ingest_ctable(
|
problems += self.ingest_ctable(
|
||||||
asset=chunk['asset'],
|
asset=chunk['asset'],
|
||||||
@@ -624,12 +620,10 @@ class ExchangeBundle:
|
|||||||
)
|
)
|
||||||
else:
|
else:
|
||||||
all_chunks = list(chain.from_iterable(itervalues(chunks)))
|
all_chunks = list(chain.from_iterable(itervalues(chunks)))
|
||||||
|
|
||||||
# We sort the chunks by end date to ingest most recent data first
|
# We sort the chunks by end date to ingest most recent data first
|
||||||
|
if all_chunks:
|
||||||
all_chunks.sort(
|
all_chunks.sort(
|
||||||
key=lambda chunk: timestr_to_dt(
|
key=lambda chunk: pd.to_datetime(chunk['period'])
|
||||||
chunk['period'], data_frequency
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
with maybe_show_progress(
|
with maybe_show_progress(
|
||||||
all_chunks,
|
all_chunks,
|
||||||
@@ -638,8 +632,7 @@ class ExchangeBundle:
|
|||||||
'{exchange}'.format(
|
'{exchange}'.format(
|
||||||
exchange=self.exchange_name,
|
exchange=self.exchange_name,
|
||||||
frequency=data_frequency,
|
frequency=data_frequency,
|
||||||
)
|
)) as it:
|
||||||
) as it:
|
|
||||||
for chunk in it:
|
for chunk in it:
|
||||||
problems += self.ingest_ctable(
|
problems += self.ingest_ctable(
|
||||||
asset=chunk['asset'],
|
asset=chunk['asset'],
|
||||||
@@ -708,36 +701,42 @@ class ExchangeBundle:
|
|||||||
for symbol in symbols:
|
for symbol in symbols:
|
||||||
start_dt = df.index.get_level_values(1).min()
|
start_dt = df.index.get_level_values(1).min()
|
||||||
end_dt = df.index.get_level_values(1).max()
|
end_dt = df.index.get_level_values(1).max()
|
||||||
|
end_dt_key = 'end_{}'.format(data_frequency)
|
||||||
|
|
||||||
try:
|
market = self.exchange.get_market(symbol)
|
||||||
asset = self.exchange.get_asset(symbol, is_local=True)
|
if market is None:
|
||||||
except:
|
raise ValueError('symbol not available in the exchange.')
|
||||||
asset = copy.deepcopy(self.exchange.get_asset(symbol))
|
|
||||||
|
|
||||||
if asset.data_source == 'local':
|
params = dict(
|
||||||
asset.start_date = asset.start_date \
|
exchange=self.exchange.name,
|
||||||
if asset.start_date < start_dt else start_dt
|
data_source='local',
|
||||||
|
exchange_symbol=market['id'],
|
||||||
|
)
|
||||||
|
mixin_market_params(self.exchange_name, params, market)
|
||||||
|
|
||||||
if data_frequency == 'daily':
|
asset_def = self.exchange.get_asset_def(market, True)
|
||||||
asset.end_date = asset.end_daily = asset.end_daily \
|
if asset_def is not None:
|
||||||
if asset.end_daily > end_dt else end_dt
|
params['symbol'] = asset_def['symbol']
|
||||||
|
|
||||||
|
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:
|
else:
|
||||||
asset.end_date = asset.end_minute = asset.end_minute \
|
params['symbol'] = get_catalyst_symbol(market)
|
||||||
if asset.end_minute > end_dt else end_dt
|
|
||||||
|
|
||||||
else:
|
params['end_daily'] = end_dt \
|
||||||
asset.data_source = 'local'
|
if data_frequency == 'daily' else 'N/A'
|
||||||
asset.start_date = start_dt
|
params['end_minute'] = end_dt \
|
||||||
asset.end_dt = 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:
|
if min_start_dt is None or start_dt < min_start_dt:
|
||||||
min_start_dt = start_dt
|
min_start_dt = start_dt
|
||||||
@@ -745,9 +744,11 @@ class ExchangeBundle:
|
|||||||
if max_end_dt is None or end_dt > max_end_dt:
|
if max_end_dt is None or end_dt > max_end_dt:
|
||||||
max_end_dt = end_dt
|
max_end_dt = end_dt
|
||||||
|
|
||||||
assets[symbol] = asset
|
asset = TradingPair(**params)
|
||||||
|
assets[market['id']] = asset
|
||||||
|
|
||||||
|
save_exchange_symbols(self.exchange_name, assets, True)
|
||||||
|
|
||||||
# TODO: update config.json
|
|
||||||
writer = self.get_writer(
|
writer = self.get_writer(
|
||||||
start_dt=min_start_dt.replace(hour=00, minute=00),
|
start_dt=min_start_dt.replace(hour=00, minute=00),
|
||||||
end_dt=max_end_dt.replace(hour=23, minute=59),
|
end_dt=max_end_dt.replace(hour=23, minute=59),
|
||||||
|
|||||||
@@ -1,4 +1,5 @@
|
|||||||
import abc
|
import abc
|
||||||
|
import datetime
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -296,24 +297,38 @@ class DataPortalExchangeBacktest(DataPortalExchangeBase):
|
|||||||
bundle = self.exchange_bundles[exchange_name] # type: ExchangeBundle
|
bundle = self.exchange_bundles[exchange_name] # type: ExchangeBundle
|
||||||
|
|
||||||
freq, candle_size, unit, adj_data_frequency = get_frequency(
|
freq, candle_size, unit, adj_data_frequency = get_frequency(
|
||||||
frequency, data_frequency
|
frequency, data_frequency, supported_freqs=['T', 'D']
|
||||||
)
|
)
|
||||||
adj_bar_count = candle_size * bar_count
|
adj_bar_count = candle_size * bar_count
|
||||||
|
|
||||||
if data_frequency == 'minute' and adj_data_frequency == 'daily':
|
if data_frequency == "minute":
|
||||||
end_dt = end_dt.floor('1D')
|
# for minute frequency always request data until the
|
||||||
|
# current minute (do not include the current minute)
|
||||||
|
last_dt_for_series = end_dt - datetime.timedelta(minutes=1)
|
||||||
|
|
||||||
|
# read the minute bundles for daily frequency to
|
||||||
|
# support last partial candle
|
||||||
|
# TODO: optimize this by applying this logic only for the last day
|
||||||
|
if adj_data_frequency == 'daily':
|
||||||
|
adj_data_frequency = 'minute'
|
||||||
|
adj_bar_count = adj_bar_count * 1440
|
||||||
|
|
||||||
|
else: # data_frequency == "daily":
|
||||||
|
last_dt_for_series = end_dt
|
||||||
|
|
||||||
series = bundle.get_history_window_series_and_load(
|
series = bundle.get_history_window_series_and_load(
|
||||||
assets=assets,
|
assets=assets,
|
||||||
end_dt=end_dt,
|
end_dt=last_dt_for_series,
|
||||||
bar_count=adj_bar_count,
|
bar_count=adj_bar_count,
|
||||||
field=field,
|
field=field,
|
||||||
data_frequency=adj_data_frequency,
|
data_frequency=adj_data_frequency,
|
||||||
algo_end_dt=self._last_available_session,
|
algo_end_dt=self._last_available_session,
|
||||||
)
|
)
|
||||||
|
|
||||||
start_dt = get_start_dt(end_dt, adj_bar_count, data_frequency)
|
start_dt = get_start_dt(last_dt_for_series, adj_bar_count,
|
||||||
|
adj_data_frequency, False)
|
||||||
df = resample_history_df(pd.DataFrame(series), freq, field, start_dt)
|
df = resample_history_df(pd.DataFrame(series), freq, field, start_dt)
|
||||||
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
def get_exchange_spot_value(self,
|
def get_exchange_spot_value(self,
|
||||||
|
|||||||
@@ -329,17 +329,3 @@ class NoCandlesReceivedFromExchange(ZiplineError):
|
|||||||
'Although requesting {bar_count} candles until {end_dt} of asset {asset}, '
|
'Although requesting {bar_count} candles until {end_dt} of asset {asset}, '
|
||||||
'an empty list of candles was received for {exchange}.'
|
'an empty list of candles was received for {exchange}.'
|
||||||
).strip()
|
).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()
|
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ from datetime import datetime
|
|||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
|
||||||
from catalyst.constants import BUNDLE_URL
|
|
||||||
from catalyst.data.bundles.core import download_without_progress
|
from catalyst.data.bundles.core import download_without_progress
|
||||||
from catalyst.exchange.utils.exchange_utils import get_exchange_bundles_folder
|
from catalyst.exchange.utils.exchange_utils import get_exchange_bundles_folder
|
||||||
import os
|
import os
|
||||||
@@ -49,11 +48,10 @@ def get_bcolz_chunk(exchange_name, symbol, data_frequency, period):
|
|||||||
path = os.path.join(root, name)
|
path = os.path.join(root, name)
|
||||||
|
|
||||||
if not os.path.isdir(path):
|
if not os.path.isdir(path):
|
||||||
url = BUNDLE_URL.format(
|
url = 'https://s3.amazonaws.com/enigmaco/catalyst-bundles/' \
|
||||||
|
'exchange-{exchange}/{name}.tar.gz'.format(
|
||||||
exchange=exchange_name,
|
exchange=exchange_name,
|
||||||
data_frequency=data_frequency,
|
name=name)
|
||||||
name=name,
|
|
||||||
)
|
|
||||||
|
|
||||||
bytes = download_without_progress(url)
|
bytes = download_without_progress(url)
|
||||||
with tarfile.open('r', fileobj=bytes) as tar:
|
with tarfile.open('r', fileobj=bytes) as tar:
|
||||||
@@ -77,14 +75,14 @@ def get_df_from_arrays(arrays, periods):
|
|||||||
|
|
||||||
"""
|
"""
|
||||||
ohlcv = dict()
|
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()
|
ohlcv[field] = arrays[index].flatten()
|
||||||
|
|
||||||
df = pd.DataFrame(
|
df = pd.DataFrame(
|
||||||
data=ohlcv,
|
data=ohlcv,
|
||||||
index=periods
|
index=periods
|
||||||
)
|
)
|
||||||
df.index.name = 'last_traded'
|
|
||||||
return df
|
return df
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,307 +0,0 @@
|
|||||||
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']
|
|
||||||
@@ -1,4 +1,5 @@
|
|||||||
import calendar
|
import calendar
|
||||||
|
import math
|
||||||
import re
|
import re
|
||||||
from datetime import datetime, timedelta, date
|
from datetime import datetime, timedelta, date
|
||||||
|
|
||||||
@@ -164,12 +165,6 @@ def get_start_dt(end_dt, bar_count, data_frequency, include_first=True):
|
|||||||
return start_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):
|
def get_period_label(dt, data_frequency):
|
||||||
"""
|
"""
|
||||||
The period label for the specified date and frequency.
|
The period label for the specified date and frequency.
|
||||||
@@ -183,26 +178,6 @@ def get_period_label(dt, data_frequency):
|
|||||||
-------
|
-------
|
||||||
str
|
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':
|
if data_frequency == 'minute':
|
||||||
return '{}-{:02d}'.format(dt.year, dt.month)
|
return '{}-{:02d}'.format(dt.year, dt.month)
|
||||||
@@ -274,9 +249,12 @@ def get_year_start_end(dt, first_day=None, last_day=None):
|
|||||||
return year_start, year_end
|
return year_start, year_end
|
||||||
|
|
||||||
|
|
||||||
def get_frequency(freq, data_frequency=None, supported_freqs=['D', 'T']):
|
def get_frequency(freq, data_frequency=None, supported_freqs=['D', 'H', 'T']):
|
||||||
"""
|
"""
|
||||||
Get the frequency parameters.
|
Takes an arbitrary candle size (e.g. 15T) and converts to the lowest
|
||||||
|
common denominator supported by the data bundles (e.g. 1T). The data
|
||||||
|
bundles only support 1T and 1D frequencies. If another frequency
|
||||||
|
is requested, Catalyst must request the underlying data and resample.
|
||||||
|
|
||||||
Notes
|
Notes
|
||||||
-----
|
-----
|
||||||
@@ -331,14 +309,14 @@ def get_frequency(freq, data_frequency=None, supported_freqs=['D', 'T']):
|
|||||||
data_frequency = 'minute'
|
data_frequency = 'minute'
|
||||||
|
|
||||||
elif unit.lower() == 'h':
|
elif unit.lower() == 'h':
|
||||||
|
data_frequency = 'minute'
|
||||||
|
|
||||||
if 'H' in supported_freqs:
|
if 'H' in supported_freqs:
|
||||||
unit = 'H'
|
unit = 'H'
|
||||||
alias = '{}H'.format(candle_size)
|
alias = '{}H'.format(candle_size)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
candle_size = candle_size * 60
|
candle_size = candle_size * 60
|
||||||
alias = '{}T'.format(candle_size)
|
alias = '{}T'.format(candle_size)
|
||||||
data_frequency = 'minute'
|
|
||||||
|
|
||||||
else:
|
else:
|
||||||
raise InvalidHistoryFrequencyAlias(freq=freq)
|
raise InvalidHistoryFrequencyAlias(freq=freq)
|
||||||
@@ -352,3 +330,33 @@ def from_ms_timestamp(ms):
|
|||||||
|
|
||||||
def get_epoch():
|
def get_epoch():
|
||||||
return pd.to_datetime('1970-1-1', utc=True)
|
return pd.to_datetime('1970-1-1', utc=True)
|
||||||
|
|
||||||
|
|
||||||
|
def get_candles_number_from_minutes(unit, candle_size, minutes):
|
||||||
|
"""
|
||||||
|
Get the number of bars needed for the given time interval
|
||||||
|
in minutes.
|
||||||
|
|
||||||
|
Notes
|
||||||
|
-----
|
||||||
|
Supports only "T", "D" and "H" units
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
unit: str
|
||||||
|
candle_size : int
|
||||||
|
minutes: int
|
||||||
|
|
||||||
|
Returns
|
||||||
|
-------
|
||||||
|
int
|
||||||
|
|
||||||
|
"""
|
||||||
|
if unit == "T":
|
||||||
|
res = (float(minutes) / candle_size)
|
||||||
|
elif unit == "H":
|
||||||
|
res = (minutes / 60.0) / candle_size
|
||||||
|
else: # unit == "D"
|
||||||
|
res = (minutes / 1440.0) / candle_size
|
||||||
|
|
||||||
|
return int(math.ceil(res))
|
||||||
|
|||||||
@@ -1,19 +1,19 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import os
|
|
||||||
import shutil
|
|
||||||
|
|
||||||
import json
|
import json
|
||||||
import pandas as pd
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
from catalyst.assets._assets import TradingPair
|
import shutil
|
||||||
from datetime import date, datetime
|
from datetime import date, datetime
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from catalyst.assets._assets import TradingPair
|
||||||
from six import string_types
|
from six import string_types
|
||||||
from six.moves.urllib import request
|
from six.moves.urllib import request
|
||||||
|
|
||||||
from catalyst.constants import EXCHANGE_CONFIG_URL
|
from catalyst.constants import DATE_FORMAT, SYMBOLS_URL
|
||||||
|
from catalyst.exchange.exchange_errors import ExchangeSymbolsNotFound
|
||||||
from catalyst.exchange.utils.serialization_utils import ExchangeJSONEncoder, \
|
from catalyst.exchange.utils.serialization_utils import ExchangeJSONEncoder, \
|
||||||
ExchangeJSONDecoder, ConfigJSONEncoder
|
ExchangeJSONDecoder
|
||||||
from catalyst.utils.deprecate import deprecated
|
|
||||||
from catalyst.utils.paths import data_root, ensure_directory, \
|
from catalyst.utils.paths import data_root, ensure_directory, \
|
||||||
last_modified_time
|
last_modified_time
|
||||||
|
|
||||||
@@ -69,7 +69,7 @@ def is_blacklist(exchange_name, environ=None):
|
|||||||
return os.path.exists(filename)
|
return os.path.exists(filename)
|
||||||
|
|
||||||
|
|
||||||
def get_exchange_config_filename(exchange_name, environ=None):
|
def get_exchange_symbols_filename(exchange_name, is_local=False, environ=None):
|
||||||
"""
|
"""
|
||||||
The absolute path of the exchange's symbol.json file.
|
The absolute path of the exchange's symbol.json file.
|
||||||
|
|
||||||
@@ -83,12 +83,12 @@ def get_exchange_config_filename(exchange_name, environ=None):
|
|||||||
str
|
str
|
||||||
|
|
||||||
"""
|
"""
|
||||||
name = 'config.json'
|
name = 'symbols.json' if not is_local else 'symbols_local.json'
|
||||||
exchange_folder = get_exchange_folder(exchange_name, environ)
|
exchange_folder = get_exchange_folder(exchange_name, environ)
|
||||||
return os.path.join(exchange_folder, name)
|
return os.path.join(exchange_folder, name)
|
||||||
|
|
||||||
|
|
||||||
def download_exchange_config(exchange_name, filename, environ=None):
|
def download_exchange_symbols(exchange_name, environ=None):
|
||||||
"""
|
"""
|
||||||
Downloads the exchange's symbols.json from the repository.
|
Downloads the exchange's symbols.json from the repository.
|
||||||
|
|
||||||
@@ -102,14 +102,15 @@ def download_exchange_config(exchange_name, filename, environ=None):
|
|||||||
str
|
str
|
||||||
|
|
||||||
"""
|
"""
|
||||||
url = EXCHANGE_CONFIG_URL.format(exchange=exchange_name)
|
filename = get_exchange_symbols_filename(exchange_name)
|
||||||
request.urlretrieve(url=url, filename=filename)
|
url = SYMBOLS_URL.format(exchange=exchange_name)
|
||||||
|
response = request.urlretrieve(url=url, filename=filename)
|
||||||
|
return response
|
||||||
|
|
||||||
|
|
||||||
@deprecated
|
def get_exchange_symbols(exchange_name, is_local=False, environ=None):
|
||||||
def get_exchange_config(exchange_name, filename=None, environ=None):
|
|
||||||
"""
|
"""
|
||||||
The de-serialized content of the exchange's config.json.
|
The de-serialized content of the exchange's symbols.json.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
@@ -122,18 +123,17 @@ def get_exchange_config(exchange_name, filename=None, environ=None):
|
|||||||
Object
|
Object
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if filename is None:
|
filename = get_exchange_symbols_filename(exchange_name, is_local)
|
||||||
filename = get_exchange_config_filename(exchange_name)
|
|
||||||
|
if not is_local and (not os.path.isfile(filename) or pd.Timedelta(
|
||||||
|
pd.Timestamp('now', tz='UTC') - last_modified_time(
|
||||||
|
filename)).days > 1):
|
||||||
|
try:
|
||||||
|
download_exchange_symbols(exchange_name, environ)
|
||||||
|
except Exception:
|
||||||
|
pass
|
||||||
|
|
||||||
if os.path.isfile(filename):
|
if os.path.isfile(filename):
|
||||||
now = pd.Timestamp.utcnow()
|
|
||||||
limit = pd.Timedelta('2H')
|
|
||||||
if pd.Timedelta(now - last_modified_time(filename)) > limit:
|
|
||||||
download_exchange_config(exchange_name, filename, environ)
|
|
||||||
|
|
||||||
else:
|
|
||||||
download_exchange_config(exchange_name, filename, environ)
|
|
||||||
|
|
||||||
with open(filename) as data_file:
|
with open(filename) as data_file:
|
||||||
try:
|
try:
|
||||||
data = json.load(data_file, cls=ExchangeJSONDecoder)
|
data = json.load(data_file, cls=ExchangeJSONDecoder)
|
||||||
@@ -141,29 +141,37 @@ def get_exchange_config(exchange_name, filename=None, environ=None):
|
|||||||
|
|
||||||
except ValueError:
|
except ValueError:
|
||||||
return dict()
|
return dict()
|
||||||
|
else:
|
||||||
|
raise ExchangeSymbolsNotFound(
|
||||||
|
exchange=exchange_name,
|
||||||
|
filename=filename
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
def save_exchange_config(exchange_name, config, filename=None, environ=None):
|
def save_exchange_symbols(exchange_name, assets, is_local=False, environ=None):
|
||||||
"""
|
"""
|
||||||
Save assets into an exchange_config file.
|
Save assets into an exchange_symbols file.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
----------
|
----------
|
||||||
exchange_name: str
|
exchange_name: str
|
||||||
config
|
assets: list[dict[str, object]]
|
||||||
|
is_local: bool
|
||||||
environ
|
environ
|
||||||
|
|
||||||
Returns
|
Returns
|
||||||
-------
|
-------
|
||||||
|
|
||||||
"""
|
"""
|
||||||
if filename is None:
|
asset_dicts = dict()
|
||||||
name = 'config.json'
|
for symbol in assets:
|
||||||
exchange_folder = get_exchange_folder(exchange_name, environ)
|
asset_dicts[symbol] = assets[symbol].to_dict()
|
||||||
filename = os.path.join(exchange_folder, name)
|
|
||||||
|
|
||||||
with open(filename, 'w+') as handle:
|
filename = get_exchange_symbols_filename(
|
||||||
json.dump(config, handle, indent=4, cls=ConfigJSONEncoder)
|
exchange_name, is_local, environ
|
||||||
|
)
|
||||||
|
with open(filename, 'wt') as handle:
|
||||||
|
json.dump(asset_dicts, handle, indent=4, default=symbols_serial)
|
||||||
|
|
||||||
|
|
||||||
def get_symbols_string(assets):
|
def get_symbols_string(assets):
|
||||||
@@ -504,6 +512,25 @@ def has_bundle(exchange_name, data_frequency, environ=None):
|
|||||||
return os.path.isdir(folder)
|
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):
|
def perf_serial(obj):
|
||||||
"""
|
"""
|
||||||
JSON serializer for objects not serializable by default json code
|
JSON serializer for objects not serializable by default json code
|
||||||
@@ -593,12 +620,46 @@ def resample_history_df(df, freq, field, start_dt=None):
|
|||||||
return resampled_df
|
return resampled_df
|
||||||
|
|
||||||
|
|
||||||
def from_ms_timestamp(ms):
|
def mixin_market_params(exchange_name, params, market):
|
||||||
return pd.to_datetime(ms, unit='ms', utc=True)
|
"""
|
||||||
|
Applies a CCXT market dict to parameters of TradingPair init.
|
||||||
|
|
||||||
|
Parameters
|
||||||
|
----------
|
||||||
|
params: dict[Object]
|
||||||
|
market: dict[Object]
|
||||||
|
|
||||||
def get_epoch():
|
Returns
|
||||||
return pd.to_datetime('1970-1-1', utc=True)
|
-------
|
||||||
|
|
||||||
|
"""
|
||||||
|
# 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 group_assets_by_exchange(assets):
|
def group_assets_by_exchange(assets):
|
||||||
@@ -687,37 +748,6 @@ def get_candles_df(candles, field, freq, bar_count, end_dt):
|
|||||||
all_series[asset] = pd.Series(asset_df[field])
|
all_series[asset] = pd.Series(asset_df[field])
|
||||||
|
|
||||||
df = pd.DataFrame(all_series)
|
df = pd.DataFrame(all_series)
|
||||||
|
|
||||||
df.dropna(inplace=True)
|
df.dropna(inplace=True)
|
||||||
|
|
||||||
return df
|
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
|
|
||||||
|
|||||||
@@ -4,9 +4,8 @@ from catalyst.constants import LOG_LEVEL
|
|||||||
from catalyst.exchange.ccxt.ccxt_exchange import CCXT
|
from catalyst.exchange.ccxt.ccxt_exchange import CCXT
|
||||||
from catalyst.exchange.exchange import Exchange
|
from catalyst.exchange.exchange import Exchange
|
||||||
from catalyst.exchange.exchange_errors import ExchangeAuthEmpty
|
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, \
|
from catalyst.exchange.utils.exchange_utils import get_exchange_auth, \
|
||||||
get_exchange_folder
|
get_exchange_folder, is_blacklist
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
log = Logger('factory', level=LOG_LEVEL)
|
log = Logger('factory', level=LOG_LEVEL)
|
||||||
@@ -14,12 +13,9 @@ exchange_cache = dict()
|
|||||||
|
|
||||||
|
|
||||||
def get_exchange(exchange_name, base_currency=None, must_authenticate=False,
|
def get_exchange(exchange_name, base_currency=None, must_authenticate=False,
|
||||||
skip_init=False, auth_alias=None, config=None):
|
skip_init=False, auth_alias=None):
|
||||||
key = (exchange_name, base_currency)
|
key = (exchange_name, base_currency)
|
||||||
if key in exchange_cache:
|
if key in exchange_cache:
|
||||||
if not skip_init:
|
|
||||||
exchange_cache[key].init()
|
|
||||||
|
|
||||||
return exchange_cache[key]
|
return exchange_cache[key]
|
||||||
|
|
||||||
exchange_auth = get_exchange_auth(exchange_name, alias=auth_alias)
|
exchange_auth = get_exchange_auth(exchange_name, alias=auth_alias)
|
||||||
@@ -40,7 +36,6 @@ def get_exchange(exchange_name, base_currency=None, must_authenticate=False,
|
|||||||
password=exchange_auth['password'] if 'password'
|
password=exchange_auth['password'] if 'password'
|
||||||
in exchange_auth.keys() else '',
|
in exchange_auth.keys() else '',
|
||||||
base_currency=base_currency,
|
base_currency=base_currency,
|
||||||
config=config,
|
|
||||||
)
|
)
|
||||||
exchange_cache[key] = exchange
|
exchange_cache[key] = exchange
|
||||||
|
|
||||||
@@ -58,8 +53,8 @@ def get_exchanges(exchange_names):
|
|||||||
return exchanges
|
return exchanges
|
||||||
|
|
||||||
|
|
||||||
def find_exchanges(features=None, history=None, skip_blacklist=True, path=None,
|
def find_exchanges(features=None, skip_blacklist=True, is_authenticated=False,
|
||||||
is_authenticated=False, base_currency=None):
|
base_currency=None):
|
||||||
"""
|
"""
|
||||||
Find exchanges filtered by a list of feature.
|
Find exchanges filtered by a list of feature.
|
||||||
|
|
||||||
@@ -77,33 +72,28 @@ def find_exchanges(features=None, history=None, skip_blacklist=True, path=None,
|
|||||||
list[Exchange]
|
list[Exchange]
|
||||||
|
|
||||||
"""
|
"""
|
||||||
|
exchange_names = CCXT.find_exchanges(features, is_authenticated)
|
||||||
|
|
||||||
return list(
|
exchanges = []
|
||||||
scan_exchanges(
|
for exchange_name in exchange_names:
|
||||||
features,
|
if skip_blacklist and is_blacklist(exchange_name):
|
||||||
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
|
continue
|
||||||
|
|
||||||
yield get_exchange(
|
exchange = get_exchange(
|
||||||
exchange_name=config['id'],
|
exchange_name=exchange_name,
|
||||||
skip_init=True,
|
skip_init=True,
|
||||||
base_currency=base_currency,
|
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
|
||||||
|
|||||||
@@ -3,48 +3,15 @@ import re
|
|||||||
from json import JSONEncoder
|
from json import JSONEncoder
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from catalyst.constants import DATE_TIME_FORMAT
|
||||||
from six import string_types
|
from six import string_types
|
||||||
|
|
||||||
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):
|
class ExchangeJSONEncoder(json.JSONEncoder):
|
||||||
def default(self, obj):
|
def default(self, obj):
|
||||||
if isinstance(obj, pd.Timestamp):
|
if isinstance(obj, pd.Timestamp):
|
||||||
return obj.strftime(DATE_TIME_FORMAT)
|
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
|
# Let the base class default method raise the TypeError
|
||||||
return JSONEncoder.default(self, obj)
|
return JSONEncoder.default(self, obj)
|
||||||
|
|
||||||
|
|||||||
@@ -95,11 +95,24 @@ class TradingEnvironment(object):
|
|||||||
if not trading_calendar:
|
if not trading_calendar:
|
||||||
trading_calendar = get_calendar("NYSE")
|
trading_calendar = get_calendar("NYSE")
|
||||||
|
|
||||||
self.benchmark_returns, self.treasury_curves = load(
|
# todo: uncomment and add a well defined benchmark
|
||||||
trading_calendar.day,
|
# self.benchmark_returns, self.treasury_curves = load(
|
||||||
trading_calendar.schedule.index,
|
# trading_calendar.day,
|
||||||
self.bm_symbol,
|
# trading_calendar.schedule.index,
|
||||||
)
|
# self.bm_symbol,
|
||||||
|
# exchange=exchange,
|
||||||
|
# )
|
||||||
|
|
||||||
|
start_data = get_calendar('OPEN').first_trading_session
|
||||||
|
end_data = pd.Timestamp.utcnow()
|
||||||
|
treasure_cols = ['1month', '3month', '6month', '1year', '2year',
|
||||||
|
'3year', '5year', '7year', '10year', '20year', '30year']
|
||||||
|
self.benchmark_returns = pd.DataFrame(data=0.001,
|
||||||
|
index=pd.date_range(start_data, end_data),
|
||||||
|
columns=['close'])
|
||||||
|
self.treasury_curves = pd.DataFrame(data=0.001,
|
||||||
|
index=pd.date_range(start_data, end_data),
|
||||||
|
columns=treasure_cols)
|
||||||
|
|
||||||
self.exchange_tz = exchange_tz
|
self.exchange_tz = exchange_tz
|
||||||
|
|
||||||
|
|||||||
@@ -1 +1 @@
|
|||||||
0x7fAec9aaE31BE428DeAAE1be8195dF609079Fd10
|
0xf0ee6b27b759c9893ce4f094b49ad28fd15a23e4
|
||||||
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
|||||||
0x3985f5de8fddf2e8f7705cd360b498bf35ebfbc4
|
0xa64927358a82254be92eb1f1cb01de68d1787004
|
||||||
@@ -69,7 +69,10 @@ class Marketplace:
|
|||||||
contract_url.info().get_content_charset()).strip())
|
contract_url.info().get_content_charset()).strip())
|
||||||
|
|
||||||
abi_url = urllib.urlopen(MARKETPLACE_CONTRACT_ABI)
|
abi_url = urllib.urlopen(MARKETPLACE_CONTRACT_ABI)
|
||||||
abi = json.load(abi_url)
|
abi_url = abi_url.read().decode(
|
||||||
|
abi_url.info().get_content_charset())
|
||||||
|
|
||||||
|
abi = json.loads(abi_url)
|
||||||
|
|
||||||
self.mkt_contract = self.web3.eth.contract(
|
self.mkt_contract = self.web3.eth.contract(
|
||||||
self.mkt_contract_address,
|
self.mkt_contract_address,
|
||||||
@@ -83,7 +86,10 @@ class Marketplace:
|
|||||||
contract_url.info().get_content_charset()).strip())
|
contract_url.info().get_content_charset()).strip())
|
||||||
|
|
||||||
abi_url = urllib.urlopen(ENIGMA_CONTRACT_ABI)
|
abi_url = urllib.urlopen(ENIGMA_CONTRACT_ABI)
|
||||||
abi = json.load(abi_url)
|
abi_url = abi_url.read().decode(
|
||||||
|
abi_url.info().get_content_charset())
|
||||||
|
|
||||||
|
abi = json.loads(abi_url)
|
||||||
|
|
||||||
self.eng_contract = self.web3.eth.contract(
|
self.eng_contract = self.web3.eth.contract(
|
||||||
self.eng_contract_address,
|
self.eng_contract_address,
|
||||||
@@ -126,9 +132,10 @@ class Marketplace:
|
|||||||
else:
|
else:
|
||||||
while True:
|
while True:
|
||||||
for i in range(0, len(self.addresses)):
|
for i in range(0, len(self.addresses)):
|
||||||
print('{}\t{}\t{}'.format(
|
print('{}\t{}\t{}\t{}'.format(
|
||||||
i,
|
i,
|
||||||
self.addresses[i]['pubAddr'],
|
self.addresses[i]['pubAddr'],
|
||||||
|
self.addresses[i]['wallet'].ljust(10),
|
||||||
self.addresses[i]['desc'])
|
self.addresses[i]['desc'])
|
||||||
)
|
)
|
||||||
address_i = int(input('Choose your address associated with '
|
address_i = int(input('Choose your address associated with '
|
||||||
@@ -145,7 +152,7 @@ class Marketplace:
|
|||||||
|
|
||||||
def sign_transaction(self, tx):
|
def sign_transaction(self, tx):
|
||||||
|
|
||||||
url = 'https://www.myetherwallet.com/#offline-transaction'
|
url = 'https://www.mycrypto.com/#offline-transaction'
|
||||||
print('\nVisit {url} and enter the following parameters:\n\n'
|
print('\nVisit {url} and enter the following parameters:\n\n'
|
||||||
'From Address:\t\t{_from}\n'
|
'From Address:\t\t{_from}\n'
|
||||||
'\n\tClick the "Generate Information" button\n\n'
|
'\n\tClick the "Generate Information" button\n\n'
|
||||||
@@ -177,10 +184,12 @@ class Marketplace:
|
|||||||
def check_transaction(self, tx_hash):
|
def check_transaction(self, tx_hash):
|
||||||
|
|
||||||
if 'ropsten' in ETH_REMOTE_NODE:
|
if 'ropsten' in ETH_REMOTE_NODE:
|
||||||
etherscan = 'https://ropsten.etherscan.io/tx/{}'.format(
|
etherscan = 'https://ropsten.etherscan.io/tx/'
|
||||||
tx_hash)
|
elif 'rinkeby' in ETH_REMOTE_NODE:
|
||||||
|
etherscan = 'https://rinkeby.etherscan.io/tx/'
|
||||||
else:
|
else:
|
||||||
etherscan = 'https://etherscan.io/tx/{}'.format(tx_hash)
|
etherscan = 'https://etherscan.io/tx/'
|
||||||
|
etherscan = '{}{}'.format(etherscan, tx_hash)
|
||||||
|
|
||||||
print('\nYou can check the outcome of your transaction here:\n'
|
print('\nYou can check the outcome of your transaction here:\n'
|
||||||
'{}\n\n'.format(etherscan))
|
'{}\n\n'.format(etherscan))
|
||||||
@@ -329,9 +338,6 @@ class Marketplace:
|
|||||||
'nonce': self.web3.eth.getTransactionCount(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)
|
signed_tx = self.sign_transaction(tx)
|
||||||
try:
|
try:
|
||||||
tx_hash = '0x{}'.format(
|
tx_hash = '0x{}'.format(
|
||||||
@@ -371,9 +377,6 @@ class Marketplace:
|
|||||||
'from': address,
|
'from': address,
|
||||||
'nonce': self.web3.eth.getTransactionCount(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)
|
signed_tx = self.sign_transaction(tx)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -434,10 +437,9 @@ class Marketplace:
|
|||||||
merge_bundles(zsource, ztarget)
|
merge_bundles(zsource, ztarget)
|
||||||
|
|
||||||
else:
|
else:
|
||||||
|
shutil.rmtree(bundle_folder, ignore_errors=True)
|
||||||
os.rename(tmp_bundle, bundle_folder)
|
os.rename(tmp_bundle, bundle_folder)
|
||||||
|
|
||||||
pass
|
|
||||||
|
|
||||||
def ingest(self, ds_name=None, start=None, end=None, force_download=False):
|
def ingest(self, ds_name=None, start=None, end=None, force_download=False):
|
||||||
|
|
||||||
if ds_name is None:
|
if ds_name is None:
|
||||||
@@ -502,20 +504,29 @@ class Marketplace:
|
|||||||
key = self.addresses[address_i]['key']
|
key = self.addresses[address_i]['key']
|
||||||
secret = self.addresses[address_i]['secret']
|
secret = self.addresses[address_i]['secret']
|
||||||
else:
|
else:
|
||||||
key, secret = get_key_secret(address)
|
key, secret = get_key_secret(address,
|
||||||
|
self.addresses[address_i]['wallet'])
|
||||||
|
|
||||||
headers = get_signed_headers(ds_name, key, secret)
|
headers = get_signed_headers(ds_name, key, secret)
|
||||||
log.debug('Starting download of dataset for ingestion...')
|
log.info('Starting download of dataset for ingestion...')
|
||||||
r = requests.post(
|
r = requests.post(
|
||||||
'{}/marketplace/ingest'.format(AUTH_SERVER),
|
'{}/marketplace/ingest'.format(AUTH_SERVER),
|
||||||
headers=headers,
|
headers=headers,
|
||||||
stream=True,
|
stream=True,
|
||||||
)
|
)
|
||||||
if r.status_code == 200:
|
if r.status_code == 200:
|
||||||
|
log.info('Dataset downloaded successfully. Processing dataset...')
|
||||||
target_path = get_temp_bundles_folder()
|
target_path = get_temp_bundles_folder()
|
||||||
try:
|
try:
|
||||||
decoder = MultipartDecoder.from_response(r)
|
decoder = MultipartDecoder.from_response(r)
|
||||||
|
# with maybe_show_progress(
|
||||||
|
# iter(decoder.parts),
|
||||||
|
# True,
|
||||||
|
# label='Processing files') as part:
|
||||||
|
counter = 1
|
||||||
for part in decoder.parts:
|
for part in decoder.parts:
|
||||||
|
log.info("Processing file {} of {}".format(
|
||||||
|
counter, len(decoder.parts)))
|
||||||
h = part.headers[b'Content-Disposition'].decode('utf-8')
|
h = part.headers[b'Content-Disposition'].decode('utf-8')
|
||||||
# Extracting the filename from the header
|
# Extracting the filename from the header
|
||||||
name = re.search(r'filename="(.*)"', h).group(1)
|
name = re.search(r'filename="(.*)"', h).group(1)
|
||||||
@@ -529,6 +540,7 @@ class Marketplace:
|
|||||||
f.write(part.content)
|
f.write(part.content)
|
||||||
|
|
||||||
self.process_temp_bundle(ds_name, filename)
|
self.process_temp_bundle(ds_name, filename)
|
||||||
|
counter += 1
|
||||||
|
|
||||||
except NonMultipartContentTypeException:
|
except NonMultipartContentTypeException:
|
||||||
response = r.json()
|
response = r.json()
|
||||||
@@ -596,7 +608,6 @@ class Marketplace:
|
|||||||
folder = get_bundle_folder(ds_name, data_frequency)
|
folder = get_bundle_folder(ds_name, data_frequency)
|
||||||
|
|
||||||
shutil.rmtree(folder)
|
shutil.rmtree(folder)
|
||||||
pass
|
|
||||||
|
|
||||||
def create_metadata(self, key, secret, ds_name, data_frequency, desc,
|
def create_metadata(self, key, secret, ds_name, data_frequency, desc,
|
||||||
has_history=True, has_live=True):
|
has_history=True, has_live=True):
|
||||||
@@ -632,7 +643,7 @@ class Marketplace:
|
|||||||
def register(self):
|
def register(self):
|
||||||
while True:
|
while True:
|
||||||
desc = input('Enter the name of the dataset to register: ')
|
desc = input('Enter the name of the dataset to register: ')
|
||||||
dataset = desc.lower()
|
dataset = desc.lower().strip()
|
||||||
provider_info = self.mkt_contract.functions.getDataProviderInfo(
|
provider_info = self.mkt_contract.functions.getDataProviderInfo(
|
||||||
Web3.toHex(dataset)
|
Web3.toHex(dataset)
|
||||||
).call()
|
).call()
|
||||||
@@ -688,7 +699,8 @@ class Marketplace:
|
|||||||
key = self.addresses[address_i]['key']
|
key = self.addresses[address_i]['key']
|
||||||
secret = self.addresses[address_i]['secret']
|
secret = self.addresses[address_i]['secret']
|
||||||
else:
|
else:
|
||||||
key, secret = get_key_secret(address)
|
key, secret = get_key_secret(address,
|
||||||
|
self.addresses[address_i]['wallet'])
|
||||||
|
|
||||||
grains = to_grains(price)
|
grains = to_grains(price)
|
||||||
|
|
||||||
@@ -701,9 +713,6 @@ class Marketplace:
|
|||||||
'nonce': self.web3.eth.getTransactionCount(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)
|
signed_tx = self.sign_transaction(tx)
|
||||||
|
|
||||||
try:
|
try:
|
||||||
@@ -772,18 +781,21 @@ class Marketplace:
|
|||||||
key = match['key']
|
key = match['key']
|
||||||
secret = match['secret']
|
secret = match['secret']
|
||||||
else:
|
else:
|
||||||
key, secret = get_key_secret(provider_info[0])
|
key, secret = get_key_secret(provider_info[0], match['wallet'])
|
||||||
|
|
||||||
headers = get_signed_headers(dataset, key, secret)
|
|
||||||
filenames = glob.glob(os.path.join(datadir, '*.csv'))
|
filenames = glob.glob(os.path.join(datadir, '*.csv'))
|
||||||
|
|
||||||
if not filenames:
|
if not filenames:
|
||||||
raise MarketplaceNoCSVFiles(datadir=datadir)
|
raise MarketplaceNoCSVFiles(datadir=datadir)
|
||||||
|
|
||||||
files = []
|
files = []
|
||||||
for file in filenames:
|
for idx, file in enumerate(filenames):
|
||||||
|
log.info('Uploading file {} of {}: {}'.format(
|
||||||
|
idx+1, len(filenames), file))
|
||||||
|
files = []
|
||||||
files.append(('file', open(file, 'rb')))
|
files.append(('file', open(file, 'rb')))
|
||||||
|
|
||||||
|
headers = get_signed_headers(dataset, key, secret)
|
||||||
r = requests.post('{}/marketplace/publish'.format(AUTH_SERVER),
|
r = requests.post('{}/marketplace/publish'.format(AUTH_SERVER),
|
||||||
files=files,
|
files=files,
|
||||||
headers=headers)
|
headers=headers)
|
||||||
@@ -796,4 +808,7 @@ class Marketplace:
|
|||||||
raise MarketplaceHTTPRequest(request='upload file',
|
raise MarketplaceHTTPRequest(request='upload file',
|
||||||
error=r.json()['error'])
|
error=r.json()['error'])
|
||||||
|
|
||||||
print('Dataset {} uploaded successfully.'.format(dataset))
|
log.info('File processed successfully.')
|
||||||
|
|
||||||
|
print('\nDataset {} uploaded and processed successfully.'.format(
|
||||||
|
dataset))
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
|
import webbrowser
|
||||||
|
|
||||||
import requests
|
import requests
|
||||||
import time
|
import time
|
||||||
@@ -9,10 +10,10 @@ from catalyst.marketplace.marketplace_errors import (
|
|||||||
MarketplaceEmptySignature)
|
MarketplaceEmptySignature)
|
||||||
from catalyst.marketplace.utils.path_utils import (
|
from catalyst.marketplace.utils.path_utils import (
|
||||||
get_user_pubaddr, save_user_pubaddr)
|
get_user_pubaddr, save_user_pubaddr)
|
||||||
from catalyst.constants import AUTH_SERVER
|
from catalyst.constants import AUTH_SERVER, SUPPORTED_WALLETS
|
||||||
|
|
||||||
|
|
||||||
def get_key_secret(pubAddr, wallet='mew'):
|
def get_key_secret(pubAddr, wallet):
|
||||||
"""
|
"""
|
||||||
Obtain a new key/secret pair from authentication server
|
Obtain a new key/secret pair from authentication server
|
||||||
|
|
||||||
@@ -42,14 +43,22 @@ def get_key_secret(pubAddr, wallet='mew'):
|
|||||||
auth_type, auth_info = header.split(None, 1)
|
auth_type, auth_info = header.split(None, 1)
|
||||||
d = requests.utils.parse_dict_header(auth_info)
|
d = requests.utils.parse_dict_header(auth_info)
|
||||||
|
|
||||||
nonce = '0x{}'.format(d['nonce'])
|
nonce = 'Catalyst nonce: 0x{}'.format(d['nonce'])
|
||||||
|
|
||||||
|
if wallet in SUPPORTED_WALLETS:
|
||||||
|
url = 'https://www.mycrypto.com/signmsg.html'
|
||||||
|
|
||||||
if wallet == 'mew':
|
|
||||||
print('\nObtaining a key/secret pair to streamline all future '
|
print('\nObtaining a key/secret pair to streamline all future '
|
||||||
'requests with the authentication server.\n'
|
'requests with the authentication server.\n'
|
||||||
'Visit https://www.myetherwallet.com/signmsg.html and sign the '
|
'Visit {url} and sign the '
|
||||||
'following message:\n{}'.format(nonce))
|
'following message (copy the entire line, without the '
|
||||||
signature = input('Copy and Paste the "sig" field from '
|
'line break at the end):\n\n{nonce}'.format(
|
||||||
|
url=url,
|
||||||
|
nonce=nonce))
|
||||||
|
|
||||||
|
webbrowser.open_new(url)
|
||||||
|
|
||||||
|
signature = input('\nCopy and Paste the "sig" field from '
|
||||||
'the signature here (without the double quotes, '
|
'the signature here (without the double quotes, '
|
||||||
'only the HEX value):\n')
|
'only the HEX value):\n')
|
||||||
else:
|
else:
|
||||||
@@ -83,7 +92,8 @@ def get_key_secret(pubAddr, wallet='mew'):
|
|||||||
addresses = get_user_pubaddr()
|
addresses = get_user_pubaddr()
|
||||||
|
|
||||||
match = next((l for l in addresses if
|
match = next((l for l in addresses if
|
||||||
l['pubAddr'] == pubAddr), None)
|
l['pubAddr'].lower() == pubAddr.lower()), None)
|
||||||
|
|
||||||
match['key'] = response.json()['key']
|
match['key'] = response.json()['key']
|
||||||
match['secret'] = response.json()['secret']
|
match['secret'] = response.json()['secret']
|
||||||
|
|
||||||
@@ -113,7 +123,7 @@ def get_signed_headers(ds_name, key, secret):
|
|||||||
-------
|
-------
|
||||||
|
|
||||||
"""
|
"""
|
||||||
nonce = str(int(time.time()))
|
nonce = str(int(time.time() * 1000))
|
||||||
|
|
||||||
signature = hmac.new(
|
signature = hmac.new(
|
||||||
secret.encode('utf-8'),
|
secret.encode('utf-8'),
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import os
|
|||||||
import json
|
import json
|
||||||
import tarfile
|
import tarfile
|
||||||
|
|
||||||
|
from catalyst.constants import SUPPORTED_WALLETS
|
||||||
from catalyst.utils.deprecate import deprecated
|
from catalyst.utils.deprecate import deprecated
|
||||||
from catalyst.utils.paths import data_root, ensure_directory
|
from catalyst.utils.paths import data_root, ensure_directory
|
||||||
from catalyst.marketplace.marketplace_errors import MarketplaceJSONError
|
from catalyst.marketplace.marketplace_errors import MarketplaceJSONError
|
||||||
@@ -131,17 +132,63 @@ def get_user_pubaddr(environ=None):
|
|||||||
try:
|
try:
|
||||||
d = data[0]['pubAddr']
|
d = data[0]['pubAddr']
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
return [data, ]
|
data = [data, ]
|
||||||
|
|
||||||
|
changed = False
|
||||||
|
|
||||||
|
for idx, d in enumerate(data):
|
||||||
|
try:
|
||||||
|
if d['wallet'] not in SUPPORTED_WALLETS:
|
||||||
|
data[idx]['wallet'] = _choose_wallet(
|
||||||
|
d['pubAddr'], False)
|
||||||
|
changed = True
|
||||||
|
except KeyError:
|
||||||
|
data[idx]['wallet'] = _choose_wallet(
|
||||||
|
d['pubAddr'], True)
|
||||||
|
changed = True
|
||||||
|
|
||||||
|
if changed:
|
||||||
|
save_user_pubaddr(data)
|
||||||
|
|
||||||
return data
|
return data
|
||||||
|
|
||||||
else:
|
else:
|
||||||
data = []
|
data = []
|
||||||
data.append(dict(pubAddr='', desc=''))
|
data.append(dict(pubAddr='', desc='', wallet=''))
|
||||||
with open(filename, 'w') as f:
|
with open(filename, 'w') as f:
|
||||||
json.dump(data, f, sort_keys=False, indent=2,
|
json.dump(data, f, sort_keys=False, indent=2,
|
||||||
separators=(',', ':'))
|
separators=(',', ':'))
|
||||||
return data
|
return data
|
||||||
|
|
||||||
|
|
||||||
|
def _choose_wallet(pubAddr, missing):
|
||||||
|
while True:
|
||||||
|
if missing:
|
||||||
|
print('\nYou need to specify a wallet for address '
|
||||||
|
'{}.'.format(pubAddr))
|
||||||
|
else:
|
||||||
|
print('\nThe wallet specified for address {} is not '
|
||||||
|
'supported.'.format(pubAddr))
|
||||||
|
|
||||||
|
print('Please choose among the following options:')
|
||||||
|
for idx, wallet in enumerate(SUPPORTED_WALLETS):
|
||||||
|
print('{}\t{}'.format(idx, wallet))
|
||||||
|
|
||||||
|
lw = len(SUPPORTED_WALLETS)-1
|
||||||
|
w = input('Choose a number between 0 and {}: '.format(
|
||||||
|
lw))
|
||||||
|
try:
|
||||||
|
w = int(w)
|
||||||
|
except ValueError:
|
||||||
|
print('Enter a number between 0 and {}'.format(lw))
|
||||||
|
else:
|
||||||
|
if w not in range(0, lw+1):
|
||||||
|
print('Enter a number between 0 and '
|
||||||
|
'{}'.format(lw))
|
||||||
|
else:
|
||||||
|
return SUPPORTED_WALLETS[w]
|
||||||
|
|
||||||
|
|
||||||
def save_user_pubaddr(data, environ=None):
|
def save_user_pubaddr(data, environ=None):
|
||||||
"""
|
"""
|
||||||
Saves the user's public addresses and their related metadata in
|
Saves the user's public addresses and their related metadata in
|
||||||
|
|||||||
@@ -0,0 +1,49 @@
|
|||||||
|
import pytz
|
||||||
|
from datetime import datetime
|
||||||
|
from catalyst.api import symbol
|
||||||
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
|
||||||
|
coin = 'btc'
|
||||||
|
base_currency = 'usd'
|
||||||
|
n_candles = 5
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
context.symbol = symbol('%s_%s' % (coin, base_currency))
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data_polo_partial_candles(context, data):
|
||||||
|
history = data.history(symbol('btc_usdt'), ['volume'],
|
||||||
|
bar_count=10,
|
||||||
|
frequency='4H')
|
||||||
|
print('\nnow: %s\n%s' % (data.current_dt, history))
|
||||||
|
if not hasattr(context, 'i'):
|
||||||
|
context.i = 0
|
||||||
|
context.i += 1
|
||||||
|
if context.i > 5:
|
||||||
|
raise Exception('stop')
|
||||||
|
|
||||||
|
|
||||||
|
live = False
|
||||||
|
|
||||||
|
if live:
|
||||||
|
run_algorithm(initialize=lambda ctx: True,
|
||||||
|
handle_data=handle_data_polo_partial_candles,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
base_currency='usdt',
|
||||||
|
algo_namespace='ns',
|
||||||
|
live=True,
|
||||||
|
data_frequency='minute',
|
||||||
|
capital_base=3000)
|
||||||
|
else:
|
||||||
|
run_algorithm(initialize=lambda ctx: True,
|
||||||
|
handle_data=handle_data_polo_partial_candles,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
base_currency='usdt',
|
||||||
|
algo_namespace='ns',
|
||||||
|
live=False,
|
||||||
|
data_frequency='minute',
|
||||||
|
capital_base=3000,
|
||||||
|
start=datetime(2018, 2, 2, 0, 0, 0, 0, pytz.utc),
|
||||||
|
end=datetime(2018, 2, 20, 0, 0, 0, 0, pytz.utc)
|
||||||
|
)
|
||||||
@@ -1,8 +1,7 @@
|
|||||||
from catalyst.api import symbol
|
from catalyst.api import symbol
|
||||||
from catalyst.utils.run_algo import run_algorithm
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
|
||||||
coins = ['dash', 'btc', 'dash', 'etc', 'eth', 'ltc', 'nxt', 'rep', 'str',
|
coins = ['dash', 'btc', 'dash', 'etc', 'eth', 'ltc', 'nxt', 'rep', 'str', 'xmr', 'xrp', 'zec']
|
||||||
'xmr', 'xrp', 'zec']
|
|
||||||
symbols = None
|
symbols = None
|
||||||
|
|
||||||
|
|
||||||
@@ -14,14 +13,13 @@ def _handle_data(context, data):
|
|||||||
global symbols
|
global symbols
|
||||||
if symbols is None: symbols = [symbol(c + '_usdt') for c in coins]
|
if symbols is None: symbols = [symbol(c + '_usdt') for c in coins]
|
||||||
|
|
||||||
print('getting history for: %s' % [s.symbol for s in symbols])
|
print'getting history for: %s' % [s.symbol for s in symbols]
|
||||||
history = data.history(symbols,
|
history = data.history(symbols,
|
||||||
['close', 'volume'],
|
['close', 'volume'],
|
||||||
bar_count=1, # EXCEPTION, Change to 2
|
bar_count=1, # EXCEPTION, Change to 2
|
||||||
frequency='5T')
|
frequency='5T')
|
||||||
#print 'history: %s' % history.shape
|
#print 'history: %s' % history.shape
|
||||||
|
|
||||||
|
|
||||||
run_algorithm(initialize=initialize,
|
run_algorithm(initialize=initialize,
|
||||||
handle_data=_handle_data,
|
handle_data=_handle_data,
|
||||||
analyze=lambda _, results: True,
|
analyze=lambda _, results: True,
|
||||||
|
|||||||
@@ -0,0 +1,35 @@
|
|||||||
|
import pytz
|
||||||
|
from datetime import datetime
|
||||||
|
from catalyst.api import symbol
|
||||||
|
from catalyst.utils.run_algo import run_algorithm
|
||||||
|
|
||||||
|
coin = 'btc'
|
||||||
|
base_currency = 'usd'
|
||||||
|
|
||||||
|
|
||||||
|
def initialize(context):
|
||||||
|
context.symbol = symbol('%s_%s' % (coin, base_currency))
|
||||||
|
|
||||||
|
|
||||||
|
def handle_data_polo_partial_candles(context, data):
|
||||||
|
history = data.history(symbol('btc_usdt'), ['volume'],
|
||||||
|
bar_count=10,
|
||||||
|
frequency='1D')
|
||||||
|
print('\nnow: %s\n%s' % (data.current_dt, history))
|
||||||
|
if not hasattr(context, 'i'):
|
||||||
|
context.i = 0
|
||||||
|
context.i += 1
|
||||||
|
if context.i > 5:
|
||||||
|
raise Exception('stop')
|
||||||
|
|
||||||
|
|
||||||
|
run_algorithm(initialize=lambda ctx: True,
|
||||||
|
handle_data=handle_data_polo_partial_candles,
|
||||||
|
exchange_name='poloniex',
|
||||||
|
base_currency='usdt',
|
||||||
|
algo_namespace='ns',
|
||||||
|
live=False,
|
||||||
|
data_frequency='minute',
|
||||||
|
capital_base=3000,
|
||||||
|
start=datetime(2018, 2, 2, 0, 0, 0, 0, pytz.utc),
|
||||||
|
end=datetime(2018, 2, 20, 0, 0, 0, 0, pytz.utc))
|
||||||
+15
-1
@@ -143,7 +143,7 @@ with the following steps:
|
|||||||
|
|
||||||
.. code-block:: bash
|
.. code-block:: bash
|
||||||
|
|
||||||
conda create --name catalyst python=2.7 scipy zlib
|
conda create --name catalyst python=3.6 scipy zlib
|
||||||
|
|
||||||
3. Activate the environment:
|
3. Activate the environment:
|
||||||
|
|
||||||
@@ -314,6 +314,16 @@ Troubleshooting ``pip`` Install
|
|||||||
|
|
||||||
$ sudo apt-get install python-dev
|
$ sudo apt-get install python-dev
|
||||||
|
|
||||||
|
----
|
||||||
|
|
||||||
|
**Issue**:
|
||||||
|
Missing TA_Lib
|
||||||
|
|
||||||
|
**Solution**:
|
||||||
|
Follow `these instructions
|
||||||
|
<https://mrjbq7.github.io/ta-lib/install.html>`_ to install the TA_Lib Python wrapper
|
||||||
|
(and if needed, its underlying C library as well).
|
||||||
|
|
||||||
.. _pipenv:
|
.. _pipenv:
|
||||||
|
|
||||||
Installing with ``pipenv``
|
Installing with ``pipenv``
|
||||||
@@ -552,6 +562,10 @@ If after following the instructions above, and going through the
|
|||||||
*Troubleshooting* sections, you still experience problems installing Catalyst,
|
*Troubleshooting* sections, you still experience problems installing Catalyst,
|
||||||
you can seek additional help through the following channels:
|
you can seek additional help through the following channels:
|
||||||
|
|
||||||
|
- Join our `Catalyst Forum <https://catalyst.enigma.co/>`_, and browse a variety
|
||||||
|
of topics and conversations around common issues that others face when using
|
||||||
|
Catalyst, and how to resolve them. And join the conversation!
|
||||||
|
|
||||||
- Join our `Discord community <https://discord.gg/SJK32GY>`_, and head over
|
- Join our `Discord community <https://discord.gg/SJK32GY>`_, and head over
|
||||||
the #catalyst_dev channel where many other users (as well as the project
|
the #catalyst_dev channel where many other users (as well as the project
|
||||||
developers) hang out, and can assist you with your particular issue. The
|
developers) hang out, and can assist you with your particular issue. The
|
||||||
|
|||||||
@@ -2,6 +2,93 @@
|
|||||||
Release Notes
|
Release Notes
|
||||||
=============
|
=============
|
||||||
|
|
||||||
|
Version 0.5.8
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
**Release Date**: 2018-03-29
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
~~~~~~~~~
|
||||||
|
- Fix Data Marketplace release on mainnet
|
||||||
|
|
||||||
|
Version 0.5.7
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
**Release Date**: 2018-03-29
|
||||||
|
|
||||||
|
Build
|
||||||
|
~~~~~
|
||||||
|
- Data Marketplace deployed on mainnet.
|
||||||
|
- Added progress indicators for publishing data, and made the data publishing
|
||||||
|
synchronous to provide feedback to the publisher.
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
~~~~~~~~~
|
||||||
|
- fixes in storing and loading the state :issue:`214`,
|
||||||
|
:issue:`287`
|
||||||
|
|
||||||
|
Version 0.5.6
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
**Release Date**: 2018-03-22
|
||||||
|
|
||||||
|
Build
|
||||||
|
~~~~~
|
||||||
|
- Data Marketplace: ensures compatibility across wallets, now fully supporting
|
||||||
|
``ledger``, ``trezor``, ``keystore``, ``private key``. Partial support for
|
||||||
|
``metamask`` (includes sign_msg, but not sign_tx). Current support for
|
||||||
|
``Digital Bitbox`` is unknown, but believed to be supported.
|
||||||
|
- Data Marketplace: Switched online provider from MyEtherWallet to MyCrypto.
|
||||||
|
- Data Marketplace: Added progress indicator for data ingestion.
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
~~~~~~~~~
|
||||||
|
- Changed benchmark to be constant, so it doesn't ingest data at all. Temporary
|
||||||
|
fix for :issue:`271`, :issue:`285`
|
||||||
|
|
||||||
|
Version 0.5.5
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
**Release Date**: 2018-03-19
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
~~~~~~~~~
|
||||||
|
- Fixed an issue with the data history in daily frequency :issue:`274`
|
||||||
|
- Fix hourly frequency issues :issue:`227` and :issue:`114`
|
||||||
|
|
||||||
|
Version 0.5.4
|
||||||
|
^^^^^^^^^^^^^
|
||||||
|
**Release Date**: 2018-03-14
|
||||||
|
|
||||||
|
Build
|
||||||
|
~~~~~
|
||||||
|
- Switched Data Marketplace from Ropstein testnet to Rinkeby testnet after
|
||||||
|
incorporating changes resulting from the marketplace contract audit
|
||||||
|
- Several usability improvements of the Data Marketplace that make the
|
||||||
|
`--dataset` parameter optional. If it is not included in the command line,
|
||||||
|
will list available datasets, and let you choose interactively.
|
||||||
|
|
||||||
|
Bug Fixes
|
||||||
|
~~~~~~~~~
|
||||||
|
- Fix Binance requirement of symbol to be included in the cancelled order
|
||||||
|
:issue:`204`
|
||||||
|
- Fix `notenoughcasherror` when an open order is filled minutes later
|
||||||
|
:issue:`237`
|
||||||
|
- Properly handle of empty candles received from exchanges :issue:`236`
|
||||||
|
- Added a function to reduce open orders amount from calculated target/amount
|
||||||
|
for target orders :issue:`243`
|
||||||
|
- Fix missing file in live trading mode on date change :issue:`252`,
|
||||||
|
:issue:`253`
|
||||||
|
- Upgraded Data Marketplace to Web3==4.0.0b11, which was breaking some
|
||||||
|
functionality from prior version 4.0.0b7 :issue:`257`
|
||||||
|
- Always request more data to avoid empty bars and always give the exact bar
|
||||||
|
number :issue:`260`
|
||||||
|
|
||||||
|
Documentation
|
||||||
|
~~~~~~~~~~~~~
|
||||||
|
- PyCharm documentation :issue:`195`
|
||||||
|
- Added TA-Lib troubleshooting instructions
|
||||||
|
- Added instructions on how to create a Conda environment for Python 3.6, and
|
||||||
|
updated Visual C++ instructions for Windows and Python 3
|
||||||
|
- Linking example algorithms in the documentation to their sources
|
||||||
|
|
||||||
|
|
||||||
Version 0.5.3
|
Version 0.5.3
|
||||||
^^^^^^^^^^^^^
|
^^^^^^^^^^^^^
|
||||||
**Release Date**: 2018-02-09
|
**Release Date**: 2018-02-09
|
||||||
|
|||||||
@@ -22,7 +22,7 @@ dependencies:
|
|||||||
- bcolz==0.12.1
|
- bcolz==0.12.1
|
||||||
- bottleneck==1.2.1
|
- bottleneck==1.2.1
|
||||||
- chardet==3.0.4
|
- chardet==3.0.4
|
||||||
- ccxt==1.11.22
|
- ccxt==1.10.1094
|
||||||
# The Enigma Data Marketplace requires Python3 because it depends on
|
# The Enigma Data Marketplace requires Python3 because it depends on
|
||||||
# web3, which requires Python3, as building its dependencies breaks in Python2
|
# web3, which requires Python3, as building its dependencies breaks in Python2
|
||||||
# - web3==4.0.0b7
|
# - web3==4.0.0b7
|
||||||
|
|||||||
@@ -31,7 +31,7 @@ dependencies:
|
|||||||
- botocore==1.8.41
|
- botocore==1.8.41
|
||||||
- bottleneck==1.2.1
|
- bottleneck==1.2.1
|
||||||
- cchardet==2.1.1
|
- cchardet==2.1.1
|
||||||
- ccxt==1.11.22
|
- ccxt==1.10.1102
|
||||||
- chardet==3.0.4
|
- chardet==3.0.4
|
||||||
- click==6.7
|
- click==6.7
|
||||||
- contextlib2==0.5.5
|
- contextlib2==0.5.5
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ empyrical==0.2.1
|
|||||||
tables==3.3.0
|
tables==3.3.0
|
||||||
|
|
||||||
#Catalyst dependencies
|
#Catalyst dependencies
|
||||||
ccxt==1.11.22
|
ccxt==1.10.1094
|
||||||
boto3==1.4.8
|
boto3==1.4.8
|
||||||
redo==1.6
|
redo==1.6
|
||||||
web3==4.0.0b11; python_version > '3.4'
|
web3==4.0.0b11; python_version > '3.4'
|
||||||
|
|||||||
@@ -14,8 +14,7 @@ from catalyst.exchange.utils.bundle_utils import get_bcolz_chunk, \
|
|||||||
from catalyst.exchange.utils.datetime_utils import get_start_dt
|
from catalyst.exchange.utils.datetime_utils import get_start_dt
|
||||||
from catalyst.exchange.utils.exchange_utils import get_exchange_folder
|
from catalyst.exchange.utils.exchange_utils import get_exchange_folder
|
||||||
from catalyst.exchange.utils.factory import get_exchange
|
from catalyst.exchange.utils.factory import get_exchange
|
||||||
from catalyst.exchange.utils.stats_utils import df_to_string, \
|
from catalyst.exchange.utils.stats_utils import df_to_string
|
||||||
set_print_settings
|
|
||||||
from catalyst.utils.paths import ensure_directory
|
from catalyst.utils.paths import ensure_directory
|
||||||
|
|
||||||
log = getLogger('test_exchange_bundle')
|
log = getLogger('test_exchange_bundle')
|
||||||
@@ -46,9 +45,9 @@ class TestExchangeBundle:
|
|||||||
exchange_name = 'binance'
|
exchange_name = 'binance'
|
||||||
|
|
||||||
exchange = get_exchange(exchange_name)
|
exchange = get_exchange(exchange_name)
|
||||||
exchange_bundle = ExchangeBundle(exchange_name)
|
exchange_bundle = ExchangeBundle(exchange)
|
||||||
assets = [
|
assets = [
|
||||||
exchange.get_asset('bch_eth')
|
exchange.get_asset('eth_btc')
|
||||||
]
|
]
|
||||||
|
|
||||||
start = pd.to_datetime('2018-03-01', utc=True)
|
start = pd.to_datetime('2018-03-01', utc=True)
|
||||||
@@ -62,8 +61,7 @@ class TestExchangeBundle:
|
|||||||
exclude_symbols=None,
|
exclude_symbols=None,
|
||||||
start=start,
|
start=start,
|
||||||
end=end,
|
end=end,
|
||||||
show_progress=False,
|
show_progress=True
|
||||||
show_breakdown=False
|
|
||||||
)
|
)
|
||||||
|
|
||||||
reader = exchange_bundle.get_reader(data_frequency)
|
reader = exchange_bundle.get_reader(data_frequency)
|
||||||
@@ -74,15 +72,9 @@ class TestExchangeBundle:
|
|||||||
start_dt=start,
|
start_dt=start,
|
||||||
end_dt=end
|
end_dt=end
|
||||||
)
|
)
|
||||||
periods = exchange_bundle.get_calendar_periods_range(
|
print('found {} rows for {} ingestion\n{}'.format(
|
||||||
start, end, data_frequency
|
len(arrays[0]), asset.symbol, arrays[0])
|
||||||
)
|
)
|
||||||
|
|
||||||
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
|
pass
|
||||||
|
|
||||||
def test_ingest_minute_all(self):
|
def test_ingest_minute_all(self):
|
||||||
@@ -230,14 +222,9 @@ class TestExchangeBundle:
|
|||||||
start_dt=start,
|
start_dt=start,
|
||||||
end_dt=end
|
end_dt=end
|
||||||
)
|
)
|
||||||
periods = exchange_bundle.get_calendar_periods_range(
|
print('found {} rows for {} ingestion\n{}'.format(
|
||||||
start, end, data_frequency
|
len(arrays[0]), asset.symbol, arrays[0])
|
||||||
)
|
)
|
||||||
|
|
||||||
dx = get_df_from_arrays(arrays, periods)
|
|
||||||
print('found {} rows for last ingestion'.format(
|
|
||||||
len(dx)
|
|
||||||
))
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_daily_data_to_minute_table(self):
|
def test_daily_data_to_minute_table(self):
|
||||||
@@ -303,21 +290,16 @@ class TestExchangeBundle:
|
|||||||
for asset in assets:
|
for asset in assets:
|
||||||
sid = asset.sid
|
sid = asset.sid
|
||||||
|
|
||||||
arrays = reader.load_raw_arrays(
|
daily_values = reader.load_raw_arrays(
|
||||||
fields=['open', 'high', 'low', 'close', 'volume'],
|
fields=['open', 'high', 'low', 'close', 'volume'],
|
||||||
start_dt=start,
|
start_dt=start,
|
||||||
end_dt=end,
|
end_dt=end,
|
||||||
sids=[sid],
|
sids=[sid],
|
||||||
)
|
)
|
||||||
|
|
||||||
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(
|
print('found {} rows for last ingestion'.format(
|
||||||
len(dx)
|
len(daily_values[0]))
|
||||||
))
|
)
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_minute_bundle(self):
|
def test_minute_bundle(self):
|
||||||
|
|||||||
@@ -5,8 +5,7 @@ from catalyst.exchange.utils.stats_utils import set_print_settings
|
|||||||
from .base import BaseExchangeTestCase
|
from .base import BaseExchangeTestCase
|
||||||
from catalyst.exchange.ccxt.ccxt_exchange import CCXT
|
from catalyst.exchange.ccxt.ccxt_exchange import CCXT
|
||||||
from catalyst.exchange.exchange_execution import ExchangeLimitOrder
|
from catalyst.exchange.exchange_execution import ExchangeLimitOrder
|
||||||
from catalyst.exchange.utils.exchange_utils import get_exchange_auth, \
|
from catalyst.exchange.utils.exchange_utils import get_exchange_auth
|
||||||
get_trades_df, candles_from_trades
|
|
||||||
from catalyst.finance.order import Order
|
from catalyst.finance.order import Order
|
||||||
|
|
||||||
log = Logger('test_ccxt')
|
log = Logger('test_ccxt')
|
||||||
@@ -15,13 +14,12 @@ log = Logger('test_ccxt')
|
|||||||
class TestCCXT(BaseExchangeTestCase):
|
class TestCCXT(BaseExchangeTestCase):
|
||||||
@classmethod
|
@classmethod
|
||||||
def setup(self):
|
def setup(self):
|
||||||
exchange_name = 'binance'
|
exchange_name = 'bittrex'
|
||||||
auth = get_exchange_auth(exchange_name)
|
auth = get_exchange_auth(exchange_name)
|
||||||
self.exchange = CCXT(
|
self.exchange = CCXT(
|
||||||
exchange_name=exchange_name,
|
exchange_name=exchange_name,
|
||||||
key=auth['key'],
|
key=auth['key'],
|
||||||
secret=auth['secret'],
|
secret=auth['secret'],
|
||||||
password=None,
|
|
||||||
base_currency='usdt',
|
base_currency='usdt',
|
||||||
)
|
)
|
||||||
self.exchange.init()
|
self.exchange.init()
|
||||||
@@ -60,9 +58,9 @@ class TestCCXT(BaseExchangeTestCase):
|
|||||||
log.info('retrieving candles')
|
log.info('retrieving candles')
|
||||||
candles = self.exchange.get_candles(
|
candles = self.exchange.get_candles(
|
||||||
freq='1T',
|
freq='1T',
|
||||||
assets=[self.exchange.get_asset('eng_eth')],
|
assets=[self.exchange.get_asset('eth_btc')],
|
||||||
bar_count=200,
|
bar_count=200,
|
||||||
start_dt=pd.to_datetime('2017-09-01', utc=True),
|
# start_dt=pd.to_datetime('2017-09-01', utc=True),
|
||||||
)
|
)
|
||||||
|
|
||||||
for asset in candles:
|
for asset in candles:
|
||||||
@@ -92,37 +90,6 @@ class TestCCXT(BaseExchangeTestCase):
|
|||||||
assert trades
|
assert trades
|
||||||
pass
|
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):
|
def test_get_executed_order(self):
|
||||||
log.info('retrieving executed order')
|
log.info('retrieving executed order')
|
||||||
asset = self.exchange.get_asset('eng_eth')
|
asset = self.exchange.get_asset('eng_eth')
|
||||||
|
|||||||
@@ -1,8 +0,0 @@
|
|||||||
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.exchange_utils import get_common_assets
|
||||||
from catalyst.exchange.utils.factory import get_exchanges
|
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')
|
log = Logger('test_bitfinex')
|
||||||
|
|
||||||
|
|||||||
@@ -197,7 +197,6 @@ class TestSuiteBundle:
|
|||||||
# population=exchange_population,
|
# population=exchange_population,
|
||||||
# features=[bundle],
|
# features=[bundle],
|
||||||
# ) # Type: list[Exchange]
|
# ) # Type: list[Exchange]
|
||||||
# TODO: currently focusing on Binance, try other exchanges
|
|
||||||
exchanges = [get_exchange('poloniex', skip_init=True)]
|
exchanges = [get_exchange('poloniex', skip_init=True)]
|
||||||
|
|
||||||
data_portal = TestSuiteBundle.get_data_portal(exchanges)
|
data_portal = TestSuiteBundle.get_data_portal(exchanges)
|
||||||
@@ -205,20 +204,17 @@ class TestSuiteBundle:
|
|||||||
exchange.init()
|
exchange.init()
|
||||||
|
|
||||||
frequencies = exchange.get_candle_frequencies(data_frequency)
|
frequencies = exchange.get_candle_frequencies(data_frequency)
|
||||||
# freq = random.sample(frequencies, 1)[0]
|
freq = random.sample(frequencies, 1)[0]
|
||||||
freq = '5T'
|
|
||||||
rnd = random.SystemRandom()
|
rnd = random.SystemRandom()
|
||||||
# field = rnd.choice(['open', 'high', 'low', 'close', 'volume'])
|
# field = rnd.choice(['open', 'high', 'low', 'close', 'volume'])
|
||||||
field = rnd.choice(['close'])
|
field = rnd.choice(['volume'])
|
||||||
|
|
||||||
# bar_count = random.randint(3, 6)
|
bar_count = random.randint(3, 6)
|
||||||
bar_count = 5
|
|
||||||
|
|
||||||
# assets = select_random_assets(
|
assets = select_random_assets(
|
||||||
# exchange.assets, asset_population
|
exchange.assets, asset_population
|
||||||
# )
|
)
|
||||||
assets = [exchange.get_asset('bch_eth')]
|
end_dt = None
|
||||||
end_dt = pd.to_datetime('2018-03-01', utc=True)
|
|
||||||
for asset in assets:
|
for asset in assets:
|
||||||
attribute = 'end_{}'.format(data_frequency)
|
attribute = 'end_{}'.format(data_frequency)
|
||||||
asset_end_dt = getattr(asset, attribute)
|
asset_end_dt = getattr(asset, attribute)
|
||||||
|
|||||||
@@ -5,28 +5,63 @@ from logging import Logger, WARNING
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
|
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
|
from catalyst.assets._assets import TradingPair
|
||||||
from logbook import TestHandler
|
from logbook import TestHandler
|
||||||
|
|
||||||
from catalyst.assets._assets import TradingPair
|
from catalyst.exchange.exchange_errors import ExchangeRequestError
|
||||||
from catalyst.exchange.exchange_execution import ExchangeLimitOrder
|
from catalyst.exchange.exchange_execution import ExchangeLimitOrder
|
||||||
from catalyst.exchange.utils.exchange_utils import get_exchange_folder
|
from catalyst.exchange.utils.exchange_utils import get_exchange_folder
|
||||||
from catalyst.exchange.utils.factory import get_exchanges, get_exchange
|
|
||||||
from catalyst.exchange.utils.test_utils import select_random_exchanges, \
|
from catalyst.exchange.utils.test_utils import select_random_exchanges, \
|
||||||
select_random_assets
|
handle_exchange_error, select_random_assets
|
||||||
from catalyst.testing import ZiplineTestCase
|
from catalyst.testing import ZiplineTestCase
|
||||||
from catalyst.testing.fixtures import WithLogger
|
from catalyst.testing.fixtures import WithLogger
|
||||||
|
from catalyst.exchange.utils.factory import get_exchanges, get_exchange
|
||||||
|
|
||||||
log = Logger('TestSuiteExchange')
|
log = Logger('TestSuiteExchange')
|
||||||
|
|
||||||
|
|
||||||
class TestSuiteExchange(WithLogger, ZiplineTestCase):
|
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):
|
def test_markets(self):
|
||||||
population = 3
|
population = 3
|
||||||
results = dict()
|
results = dict()
|
||||||
|
|
||||||
exchanges = select_random_exchanges(population) # Type: list[Exchange]
|
exchanges = select_random_exchanges(population) # Type: list[Exchange]
|
||||||
for exchange in exchanges:
|
for exchange in exchanges:
|
||||||
exchange.init()
|
|
||||||
assets = self._test_markets_exchange(exchange)
|
assets = self._test_markets_exchange(exchange)
|
||||||
|
|
||||||
if assets is not None:
|
if assets is not None:
|
||||||
|
|||||||
Reference in New Issue
Block a user