Compare commits

...
Author SHA1 Message Date
AvishaiW 8a3cc7e5fa BLD: added a cmd for running on the cloud (WIP) 2018-03-01 01:30:18 +02:00
24 changed files with 879 additions and 1851 deletions
+174
View File
@@ -14,6 +14,7 @@ from catalyst.exchange.exchange_bundle import ExchangeBundle
from catalyst.exchange.utils.exchange_utils import delete_algo_folder from catalyst.exchange.utils.exchange_utils import delete_algo_folder
from catalyst.utils.cli import Date, Timestamp from catalyst.utils.cli import Date, Timestamp
from catalyst.utils.run_algo import _run, load_extensions from catalyst.utils.run_algo import _run, load_extensions
from catalyst.utils.run_server import run_server
try: try:
__IPYTHON__ __IPYTHON__
@@ -505,6 +506,179 @@ def live(ctx,
return perf return perf
@main.command(name='serve-live')
@click.option(
'-f',
'--algofile',
default=None,
type=click.File('r'),
help='The file that contains the algorithm to run.',
)
@click.option(
'--capital-base',
type=float,
show_default=True,
help='The amount of capital (in base_currency) allocated to trading.',
)
@click.option(
'-t',
'--algotext',
help='The algorithm script to run.',
)
@click.option(
'-D',
'--define',
multiple=True,
help="Define a name to be bound in the namespace before executing"
" the algotext. For example '-Dname=value'. The value may be"
" any python expression. These are evaluated in order so they"
" may refer to previously defined names.",
)
@click.option(
'-o',
'--output',
default='-',
metavar='FILENAME',
show_default=True,
help="The location to write the perf data. If this is '-' the perf will"
" be written to stdout.",
)
@click.option(
'--print-algo/--no-print-algo',
is_flag=True,
default=False,
help='Print the algorithm to stdout.',
)
@ipython_only(click.option(
'--local-namespace/--no-local-namespace',
is_flag=True,
default=None,
help='Should the algorithm methods be resolved in the local namespace.'
))
@click.option(
'-x',
'--exchange-name',
help='The name of the targeted exchange.',
)
@click.option(
'-n',
'--algo-namespace',
help='A label assigned to the algorithm for data storage purposes.'
)
@click.option(
'-c',
'--base-currency',
help='The base currency used to calculate statistics '
'(e.g. usd, btc, eth).',
)
@click.option(
'-e',
'--end',
type=Date(tz='utc', as_timestamp=True),
help='An optional end date at which to stop the execution.',
)
@click.option(
'--live-graph/--no-live-graph',
is_flag=True,
default=False,
help='Display live graph.',
)
@click.option(
'--simulate-orders/--no-simulate-orders',
is_flag=True,
default=True,
help='Simulating orders enable the paper trading mode. No orders will be '
'sent to the exchange unless set to false.',
)
@click.option(
'--auth-aliases',
default=None,
help='Authentication file aliases for the specified exchanges. By default,'
'each exchange uses the "auth.json" file in the exchange folder. '
'Specifying an "auth2" alias would use "auth2.json". It should be '
'specified like this: "[exchange_name],[alias],..." For example, '
'"binance,auth2" or "binance,auth2,bittrex,auth2".',
)
@click.pass_context
def serve_live(ctx,
algofile,
capital_base,
algotext,
define,
output,
print_algo,
local_namespace,
exchange_name,
algo_namespace,
base_currency,
end,
live_graph,
auth_aliases,
simulate_orders):
"""Trade live with the given algorithm on the server.
"""
if (algotext is not None) == (algofile is not None):
ctx.fail(
"must specify exactly one of '-f' / '--algofile' or"
" '-t' / '--algotext'",
)
if exchange_name is None:
ctx.fail("must specify an exchange name '-x'")
if algo_namespace is None:
ctx.fail("must specify an algorithm name '-n' in live execution mode")
if base_currency is None:
ctx.fail("must specify a base currency '-c' in live execution mode")
if capital_base is None:
ctx.fail("must specify a capital base with '--capital-base'")
if simulate_orders:
click.echo('Running in paper trading mode.', sys.stdout)
else:
click.echo('Running in live trading mode.', sys.stdout)
perf = run_server(
initialize=None,
handle_data=None,
before_trading_start=None,
analyze=None,
algofile=algofile,
algotext=algotext,
defines=define,
data_frequency=None,
capital_base=capital_base,
data=None,
bundle=None,
bundle_timestamp=None,
start=None,
end=end,
output=output,
print_algo=print_algo,
local_namespace=local_namespace,
environ=os.environ,
live=True,
exchange=exchange_name,
algo_namespace=algo_namespace,
base_currency=base_currency,
live_graph=live_graph,
analyze_live=None,
simulate_orders=simulate_orders,
auth_aliases=auth_aliases,
stats_output=None,
)
if output == '-':
click.echo(str(perf), sys.stdout)
elif output != os.devnull: # make the catalyst magic not write any data
perf.to_pickle(output)
return perf
@main.command(name='ingest-exchange') @main.command(name='ingest-exchange')
@click.option( @click.option(
'-x', '-x',
+10 -59
View File
@@ -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
@@ -693,9 +646,7 @@ cdef class TradingPair(Asset):
self.lot, self.lot,
self.decimals, self.decimals,
self.taker, self.taker,
self.maker, self.maker))
self.trading_state,
self.data_source))
def make_asset_array(int size, Asset asset): def make_asset_array(int size, Asset asset):
cdef np.ndarray out = np.empty([size], dtype=object) cdef np.ndarray out = np.empty([size], dtype=object)
+1 -2
View File
@@ -11,8 +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/exchanges/' \
'{exchange}/config.json'
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'
+16 -28
View File
@@ -4,8 +4,7 @@ import pandas as pd
from logbook import Logger from logbook import Logger
from catalyst import run_algorithm from catalyst import run_algorithm
from catalyst.api import (record, symbol, order_target_percent, from catalyst.api import (record, symbol, order_target_percent,)
get_open_orders)
from catalyst.exchange.utils.stats_utils import extract_transactions from catalyst.exchange.utils.stats_utils import extract_transactions
NAMESPACE = 'dual_moving_average' NAMESPACE = 'dual_moving_average'
@@ -20,8 +19,8 @@ def initialize(context):
def handle_data(context, data): def handle_data(context, data):
# define the windows for the moving averages # define the windows for the moving averages
short_window = 2 short_window = 50
long_window = 3 long_window = 200
# Skip as many bars as long_window to properly compute the average # Skip as many bars as long_window to properly compute the average
context.i += 1 context.i += 1
@@ -63,7 +62,7 @@ def handle_data(context, data):
# Since we are using limit orders, some orders may not execute immediately # Since we are using limit orders, some orders may not execute immediately
# we wait until all orders are executed before considering more trades. # we wait until all orders are executed before considering more trades.
orders = get_open_orders(context.asset) orders = context.blotter.open_orders
if len(orders) > 0: if len(orders) > 0:
return return
@@ -150,27 +149,16 @@ def analyze(context, perf):
if __name__ == '__main__': if __name__ == '__main__':
run_algorithm( run_algorithm(
capital_base=1000, capital_base=1000,
data_frequency='minute', data_frequency='minute',
initialize=initialize, initialize=initialize,
handle_data=handle_data, handle_data=handle_data,
analyze=analyze, analyze=analyze,
exchange_name='bitfinex', exchange_name='bitfinex',
algo_namespace=NAMESPACE, algo_namespace=NAMESPACE,
base_currency='usd', base_currency='usd',
simulate_orders=True, start=pd.to_datetime('2017-9-22', utc=True),
live=True, end=pd.to_datetime('2017-9-23', utc=True),
) )
# run_algorithm(
# capital_base=1000,
# data_frequency='minute',
# initialize=initialize,
# handle_data=handle_data,
# analyze=analyze,
# exchange_name='bitfinex',
# algo_namespace=NAMESPACE,
# base_currency='usd',
# start=pd.to_datetime('2017-9-22', utc=True),
# end=pd.to_datetime('2017-9-23', utc=True),
# )
+7 -7
View File
@@ -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,
+269 -152
View File
@@ -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( if os.path.exists(filename):
self.name, self._config timestamp = os.path.getmtime(filename)
dt = pd.to_datetime(timestamp, unit='s', utc=True)
if dt >= pd.Timestamp.utcnow().floor('1D'):
with open(filename) as f:
self.markets = json.load(f)
log.debug('loaded markets for {}'.format(self.name))
if self.markets is None:
try:
markets_symbols = self.api.load_markets()
log.debug(
'fetching {} markets:\n{}'.format(
self.name, markets_symbols
)
) )
)
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)
self.load_assets() self.load_assets()
self._is_init = True self._is_init = True
def load_assets(self): @staticmethod
if self._config is None: def find_exchanges(features=None, is_authenticated=False):
raise ValueError('Exchange config not available.') ccxt_features = []
if features is not None:
for feature in features:
if not feature.endswith('Bundle'):
ccxt_features.append(feature)
self.assets = [] exchange_names = []
for asset_dict in self._config['assets']: for exchange_name in ccxt.exchanges:
asset = TradingPair(**asset_dict) if is_authenticated:
self.assets.append(asset) exchange_auth = get_exchange_auth(exchange_name)
def _fetch_markets(self): has_auth = (exchange_auth['key'] != ''
markets_symbols = self.api.load_markets() and exchange_auth['secret'] != '')
log.debug(
'fetching {} markets:\n{}'.format(
self.name, markets_symbols
)
)
try:
markets = self.api.fetch_markets()
except NetworkError as e: if not has_auth:
raise ExchangeRequestError(error=e) continue
if not markets: log.debug('loading exchange: {}'.format(exchange_name))
raise MarketsNotFoundError( exchange = getattr(ccxt, exchange_name)()
exchange=self.name,
)
for market in markets: if ccxt_features is None:
if 'id' not in market: has_feature = True
raise InvalidMarketError(
exchange=self.name,
market=market,
)
return markets
def create_exchange_config(self): else:
config = dict( try:
name=self.name, has_feature = all(
features=[feature for feature in self.has if self.has[feature]] [exchange.has[feature] for feature in ccxt_features]
) )
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'] = [] except Exception:
for market in markets: has_feature = False
asset = self.create_trading_pair(market=market)
config['assets'].append(asset)
return config if has_feature:
try:
log.info('initializing {}'.format(exchange_name))
exchange_names.append(exchange_name)
def create_trading_pair(self, market, start_dt=None, end_dt=None, except Exception as e:
leverage=1, end_daily=None, end_minute=None): log.warn(
""" 'unable to initialize exchange {}: {}'.format(
Creating a TradingPair from market and asset data. exchange_name, e
)
)
Parameters return exchange_names
----------
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
@@ -232,6 +204,42 @@ 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'):
if source == 'catalyst':
currency = currency.upper()
key = self.api.common_currency_code(currency)
self._common_symbols[key] = currency.lower()
return key
else:
if currency in self._common_symbols:
return self._common_symbols[currency]
else:
return currency.lower()
def get_symbol(self, asset_or_symbol, source='catalyst'): def get_symbol(self, asset_or_symbol, source='catalyst'):
""" """
The CCXT symbol. The CCXT symbol.
@@ -249,7 +257,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
@@ -260,7 +274,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):
@@ -460,53 +480,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:
@@ -644,14 +755,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
) )
) )
@@ -865,7 +980,8 @@ class CCXT(Exchange):
) )
raise ExchangeRequestError(error=e) raise ExchangeRequestError(error=e)
def cancel_order(self, order_param, asset_or_symbol=None): def cancel_order(self, order_param,
asset_or_symbol=None, params={}):
order_id = order_param.id \ order_id = order_param.id \
if isinstance(order_param, Order) else order_param if isinstance(order_param, Order) else order_param
@@ -877,7 +993,8 @@ class CCXT(Exchange):
try: try:
symbol = self.get_symbol(asset_or_symbol) \ symbol = self.get_symbol(asset_or_symbol) \
if asset_or_symbol is not None else None if asset_or_symbol is not None else None
self.api.cancel_order(id=order_id, symbol=symbol) self.api.cancel_order(id=order_id,
symbol=symbol, params= params)
except (ExchangeError, NetworkError) as e: except (ExchangeError, NetworkError) as e:
log.warn( log.warn(
+60 -36
View File
@@ -1,12 +1,11 @@
import abc import abc
import pytz
from abc import ABCMeta, abstractmethod, abstractproperty from abc import ABCMeta, abstractmethod, abstractproperty
from datetime import timedelta from datetime import timedelta
from time import sleep 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,8 +17,9 @@ from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \
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 \ from catalyst.exchange.utils.exchange_utils import get_exchange_symbols, \
resample_history_df, has_bundle resample_history_df, has_bundle
from logbook import Logger
log = Logger('Exchange', level=LOG_LEVEL) log = Logger('Exchange', level=LOG_LEVEL)
@@ -291,6 +291,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):
""" """
@@ -302,13 +312,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'):
""" """
@@ -494,32 +515,37 @@ class Exchange:
series = dict() series = dict()
for asset in candles: for asset in candles:
first_candle = candles[asset][0] if candles[asset]:
asset_series = self.get_series_from_candles( first_candle = candles[asset][0]
candles=candles[asset], asset_series = self.get_series_from_candles(
start_dt=first_candle['last_traded'], candles=candles[asset],
end_dt=end_dt, start_dt=first_candle['last_traded'],
data_frequency=frequency, end_dt=end_dt,
field=field, data_frequency=frequency,
) field=field,
delta_candle_size = candle_size * 60 if unit == 'H' else candle_size
# Checking to make sure that the dates match
delta = get_delta(delta_candle_size, data_frequency)
adj_end_dt = end_dt - delta
last_traded = asset_series.index[-1]
if last_traded < adj_end_dt:
raise LastCandleTooEarlyError(
last_traded=last_traded,
end_dt=adj_end_dt,
exchange=self.name,
) )
delta_candle_size = candle_size * 60 if unit == 'H' else candle_size
# Checking to make sure that the dates match
delta = get_delta(delta_candle_size, data_frequency)
adj_end_dt = end_dt - delta
last_traded = asset_series.index[-1]
if last_traded < adj_end_dt:
raise LastCandleTooEarlyError(
last_traded=last_traded,
end_dt=adj_end_dt,
exchange=self.name,
)
else: # empty candle received
# because other assets are tz-aware, we need its tz to be set as well
asset_series = pd.Series([], index=pd.DatetimeIndex([], tz=pytz.utc))
series[asset] = asset_series series[asset] = asset_series
df = pd.DataFrame(series) df = pd.DataFrame(series)
df.dropna(inplace=True) #df.dropna(inplace=True) # commented out due to issue 236
return df return df
@@ -636,20 +662,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
@@ -679,7 +701,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,
@@ -906,7 +928,8 @@ class Exchange:
""" """
@abstractmethod @abstractmethod
def cancel_order(self, order_param, symbol_or_asset=None): def cancel_order(self, order_param,
symbol_or_asset=None, params={}):
"""Cancel an open order. """Cancel an open order.
Parameters Parameters
@@ -915,6 +938,7 @@ class Exchange:
The order_id or order object to cancel. The order_id or order object to cancel.
symbol_or_asset: str|TradingPair symbol_or_asset: str|TradingPair
The catalyst symbol, some exchanges need this The catalyst symbol, some exchanges need this
params:
""" """
pass pass
+56 -25
View File
@@ -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)
@@ -376,19 +375,30 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
if error: if error:
log.warning(error) log.warning(error)
self.pnl_stats = get_algo_df(self.algo_namespace, 'pnl_stats') # in order to save paper & live files separately
self.mode_name = 'paper' if kwargs['simulate_orders'] else 'live'
self.custom_signals_stats = \ self.pnl_stats = get_algo_df(
get_algo_df(self.algo_namespace, 'custom_signals_stats') self.algo_namespace,
'pnl_stats_{}'.format(self.mode_name),
)
self.exposure_stats = \ self.custom_signals_stats = get_algo_df(
get_algo_df(self.algo_namespace, 'exposure_stats') self.algo_namespace,
'custom_signals_stats_{}'.format(self.mode_name)
)
self.exposure_stats = get_algo_df(
self.algo_namespace,
'exposure_stats_{}'.format(self.mode_name)
)
self.is_running = True self.is_running = True
self.stats_minutes = 1 self.stats_minutes = 1
self._last_orders = [] self._last_orders = []
self._last_open_orders = []
self.trading_client = None self.trading_client = None
super(ExchangeTradingAlgorithmLive, self).__init__(*args, **kwargs) super(ExchangeTradingAlgorithmLive, self).__init__(*args, **kwargs)
@@ -515,7 +525,7 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
""" """
self.state = get_algo_object( self.state = get_algo_object(
algo_name=self.algo_namespace, algo_name=self.algo_namespace,
key='context.state', key='context.state_{}'.format(self.mode_name),
) )
if self.state is None: if self.state is None:
self.state = {} self.state = {}
@@ -538,7 +548,7 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
# Unpacking the perf_tracker and positions if available # Unpacking the perf_tracker and positions if available
cum_perf = get_algo_object( cum_perf = get_algo_object(
algo_name=self.algo_namespace, algo_name=self.algo_namespace,
key='cumulative_performance', key='cumulative_performance_{}'.format(self.mode_name),
) )
if cum_perf is not None: if cum_perf is not None:
tracker.cumulative_performance = cum_perf tracker.cumulative_performance = cum_perf
@@ -549,7 +559,7 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
todays_perf = get_algo_object( todays_perf = get_algo_object(
algo_name=self.algo_namespace, algo_name=self.algo_namespace,
key=today.strftime('%Y-%m-%d'), key=today.strftime('%Y-%m-%d'),
rel_path='daily_performance', rel_path='daily_performance_{}'.format(self.mode_name),
) )
if todays_perf is not None: if todays_perf is not None:
# Ensure single common position tracker # Ensure single common position tracker
@@ -641,7 +651,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,
) )
@@ -687,7 +696,11 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
) )
self.pnl_stats = pd.concat([self.pnl_stats, df]) self.pnl_stats = pd.concat([self.pnl_stats, df])
save_algo_df(self.algo_namespace, 'pnl_stats', self.pnl_stats) save_algo_df(
self.algo_namespace,
'pnl_stats_{}'.format(self.mode_name),
self.pnl_stats,
)
def add_custom_signals_stats(self, period_stats): def add_custom_signals_stats(self, period_stats):
""" """
@@ -708,8 +721,11 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
) )
self.custom_signals_stats = pd.concat([self.custom_signals_stats, df]) self.custom_signals_stats = pd.concat([self.custom_signals_stats, df])
save_algo_df(self.algo_namespace, 'custom_signals_stats', save_algo_df(
self.custom_signals_stats) self.algo_namespace,
'custom_signals_stats_{}'.format(self.mode_name),
self.custom_signals_stats,
)
def add_exposure_stats(self, period_stats): def add_exposure_stats(self, period_stats):
""" """
@@ -736,7 +752,9 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
self.exposure_stats = pd.concat([self.exposure_stats, df]) self.exposure_stats = pd.concat([self.exposure_stats, df])
save_algo_df( save_algo_df(
self.algo_namespace, 'exposure_stats', self.exposure_stats self.algo_namespace,
'exposure_stats_{}'.format(self.mode_name),
self.exposure_stats
) )
def nullify_frame_stats(self, now): def nullify_frame_stats(self, now):
@@ -760,6 +778,7 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
obj=self.frame_stats, obj=self.frame_stats,
rel_path='frame_stats' rel_path='frame_stats'
) )
error = remove_old_files( error = remove_old_files(
algo_name=self.algo_namespace, algo_name=self.algo_namespace,
today=now, today=now,
@@ -792,12 +811,17 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
self.nullify_frame_stats(now=data.current_dt) self.nullify_frame_stats(now=data.current_dt)
self.performance_needs_update = False self.performance_needs_update = False
orders = list(self.perf_tracker.todays_performance.orders_by_id.keys()) last_orders_list = list(self.blotter.orders.keys())
if orders != self._last_orders: open_orders_list = list(self.blotter.open_orders.keys())
if last_orders_list != self._last_orders or \
open_orders_list != self._last_open_orders:
self.performance_needs_update = True self.performance_needs_update = True
# Saving current orders to detect changes in the next frame # Saving current order positions
self._last_orders = copy.deepcopy(orders) # to detect changes in the next frame
self._last_orders = copy.deepcopy(last_orders_list)
self._last_open_orders = copy.deepcopy(open_orders_list)
if self.performance_needs_update: if self.performance_needs_update:
self.perf_tracker.update_performance() self.perf_tracker.update_performance()
@@ -839,7 +863,7 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
log.debug('saving cumulative performance object') log.debug('saving cumulative performance object')
save_algo_object( save_algo_object(
algo_name=self.algo_namespace, algo_name=self.algo_namespace,
key='cumulative_performance', key='cumulative_performance_{}'.format(self.mode_name),
obj=self.perf_tracker.cumulative_performance, obj=self.perf_tracker.cumulative_performance,
) )
log.debug('saving todays performance object') log.debug('saving todays performance object')
@@ -847,12 +871,12 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
algo_name=self.algo_namespace, algo_name=self.algo_namespace,
key=today.strftime('%Y-%m-%d'), key=today.strftime('%Y-%m-%d'),
obj=self.perf_tracker.todays_performance, obj=self.perf_tracker.todays_performance,
rel_path='daily_performance' rel_path='daily_performance_{}'.format(self.mode_name)
) )
log.debug('saving context.state object') log.debug('saving context.state object')
save_algo_object( save_algo_object(
algo_name=self.algo_namespace, algo_name=self.algo_namespace,
key='context.state', key='context.state_{}'.format(self.mode_name),
obj=self.state) obj=self.state)
def _process_stats(self, data): def _process_stats(self, data):
@@ -908,6 +932,7 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
csv_bytes = stats_to_algo_folder( csv_bytes = stats_to_algo_folder(
stats=self.frame_stats, stats=self.frame_stats,
algo_namespace=self.algo_namespace, algo_namespace=self.algo_namespace,
folder_name='stats_{}'.format(self.mode_name),
recorded_cols=recorded_cols, recorded_cols=recorded_cols,
) )
except Exception as e: except Exception as e:
@@ -1012,13 +1037,19 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
args=(order_id,)) args=(order_id,))
@api_method @api_method
def cancel_order(self, order_param, exchange_name): def cancel_order(self, order_param, exchange_name,
symbol=None, params={}):
"""Cancel an open order. """Cancel an open order.
Parameters Parameters
---------- ----------
order_param : str or Order order_param : str or Order
The order_id or order object to cancel. The order_id or order object to cancel.
exchange_name: name of exchange from
which you want to cancel the order
symbol:
params:
""" """
exchange = self.exchanges[exchange_name] exchange = self.exchanges[exchange_name]
@@ -1032,4 +1063,4 @@ class ExchangeTradingAlgorithmLive(ExchangeTradingAlgorithmBase):
sleeptime=self.attempts['retry_sleeptime'], sleeptime=self.attempts['retry_sleeptime'],
retry_exceptions=(ExchangeRequestError,), retry_exceptions=(ExchangeRequestError,),
cleanup=lambda: log.warn('cancelling order again.'), cleanup=lambda: log.warn('cancelling order again.'),
args=(order_id,)) args=(order_id, symbol, params))
+47 -40
View File
@@ -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, \
@@ -27,11 +22,15 @@ from catalyst.exchange.exchange_errors import EmptyValuesInBundleError, \
PricingDataNotLoadedError, DataCorruptionError, PricingDataValueError PricingDataNotLoadedError, DataCorruptionError, PricingDataValueError
from catalyst.exchange.utils.bundle_utils import range_in_bundle, \ from catalyst.exchange.utils.bundle_utils import range_in_bundle, \
get_bcolz_chunk, get_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_delta, get_start_dt, \
get_period_label, get_month_start_end, get_year_start_end get_period_label, get_month_start_end, get_year_start_end
from catalyst.exchange.utils.exchange_utils import get_exchange_folder from catalyst.exchange.utils.exchange_utils import get_exchange_folder, \
save_exchange_symbols, mixin_market_params, get_catalyst_symbol
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)
@@ -626,13 +625,13 @@ class ExchangeBundle:
key=lambda chunk: pd.to_datetime(chunk['period']) key=lambda chunk: pd.to_datetime(chunk['period'])
) )
with maybe_show_progress( with maybe_show_progress(
all_chunks, all_chunks,
show_progress, show_progress,
label='Ingesting {frequency} price data on ' label='Ingesting {frequency} price data on '
'{exchange}'.format( '{exchange}'.format(
exchange=self.exchange_name, exchange=self.exchange_name,
frequency=data_frequency, frequency=data_frequency,
)) as it: )) as it:
for chunk in it: for chunk in it:
problems += self.ingest_ctable( problems += self.ingest_ctable(
asset=chunk['asset'], asset=chunk['asset'],
@@ -701,36 +700,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']
else: params['start_date'] = asset_def['start_date'] \
asset.end_date = asset.end_minute = asset.end_minute \ if asset_def['start_date'] < start_dt else start_dt
if asset.end_minute > end_dt else end_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.data_source = 'local' params['symbol'] = get_catalyst_symbol(market)
asset.start_date = start_dt
asset.end_dt = end_dt
if data_frequency == 'daily': params['end_daily'] = end_dt \
asset.end_daily = end_dt if data_frequency == 'daily' else 'N/A'
asset.end_minute = None params['end_minute'] = end_dt \
if data_frequency == 'minute' else 'N/A'
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
@@ -738,9 +743,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),
-14
View File
@@ -322,17 +322,3 @@ class BalanceTooLowError(ZiplineError):
'add positions to hold a free amount greater than {amount}, or clean ' 'add positions to hold a free amount greater than {amount}, or clean '
'the state of this algo and restart.' 'the state of this algo and restart.'
).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()
-307
View File
@@ -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='2H'):
"""
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']
+115 -49
View File
@@ -11,14 +11,11 @@ from six import string_types
from six.moves.urllib import request from six.moves.urllib import request
from catalyst.constants import DATE_FORMAT, SYMBOLS_URL from catalyst.constants import DATE_FORMAT, SYMBOLS_URL
from catalyst.exchange.exchange_errors import ExchangeSymbolsNotFound, \ from catalyst.exchange.exchange_errors import ExchangeSymbolsNotFound
InvalidHistoryFrequencyError, InvalidHistoryFrequencyAlias
from catalyst.exchange.utils.serialization_utils import ExchangeJSONEncoder, \ from catalyst.exchange.utils.serialization_utils import ExchangeJSONEncoder, \
ExchangeJSONDecoder, ConfigJSONEncoder ExchangeJSONDecoder
from catalyst.utils.paths import data_root, ensure_directory, \ from catalyst.utils.paths import data_root, ensure_directory, \
last_modified_time last_modified_time
from six import string_types
from six.moves.urllib import request
def get_sid(symbol): def get_sid(symbol):
@@ -72,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.
@@ -86,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.
@@ -105,13 +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
def get_exchange_config(exchange_name, filename=None, environ=None): def get_exchange_symbols(exchange_name, is_local=False, environ=None):
""" """
The de-serialized content of the exchange's config.json. The de-serialized content of the exchange's symbols.json.
Parameters Parameters
---------- ----------
@@ -124,47 +123,55 @@ 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() with open(filename) as data_file:
limit = pd.Timedelta('2H') try:
if pd.Timedelta(now - last_modified_time(filename)) > limit: data = json.load(data_file, cls=ExchangeJSONDecoder)
download_exchange_config(exchange_name, filename, environ) return data
except ValueError:
return dict()
else: else:
download_exchange_config(exchange_name, filename, environ) raise ExchangeSymbolsNotFound(
exchange=exchange_name,
filename=filename
)
with open(filename) as data_file:
try:
data = json.load(data_file, cls=ExchangeJSONDecoder)
return data
except ValueError: def save_exchange_symbols(exchange_name, assets, is_local=False, environ=None):
return dict()
def save_exchange_config(exchange_name, config, filename=None, 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):
@@ -413,7 +420,7 @@ def clear_frame_stats_directory(algo_name):
return error return error
def remove_old_files(algo_name, today, rel_path): def remove_old_files(algo_name, today, rel_path, environ=None):
""" """
remove old files from a directory remove old files from a directory
to avoid overloading the disk to avoid overloading the disk
@@ -423,27 +430,31 @@ def remove_old_files(algo_name, today, rel_path):
algo_name: str algo_name: str
today: Timestamp today: Timestamp
rel_path: str rel_path: str
environ:
Returns Returns
------- -------
error: str error: str
""" """
error = None error = None
algo_folder = get_algo_folder(algo_name) algo_folder = get_algo_folder(algo_name, environ)
folder = os.path.join(algo_folder, rel_path) folder = os.path.join(algo_folder, rel_path)
ensure_directory(folder)
# run on all files in the folder # run on all files in the folder
for f in os.listdir(folder): for f in os.listdir(folder):
creation_unix = os.path.getctime(f) try:
creation_time = pd.to_datetime(creation_unix, unit='s', ) file_path = os.path.join(folder, f)
creation_unix = os.path.getctime(file_path)
creation_time = pd.to_datetime(creation_unix, unit='s', utc=True)
# if the file is older than 30 days erase it # if the file is older than 30 days erase it
if today - pd.DateOffset(30) > creation_time: if today - pd.DateOffset(30) > creation_time:
try: os.unlink(file_path)
os.unlink(f) except OSError:
except OSError: error = 'unable to erase files in {}'.format(folder)
error = 'unable to erase files in {}'.format(folder)
return error return error
@@ -501,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
@@ -590,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):
@@ -662,12 +726,14 @@ def get_candles_df(candles, field, freq, bar_count, end_dt,
values = [candle[field] for candle in candles[asset]] values = [candle[field] for candle in candles[asset]]
series = pd.Series(values, index=dates) series = pd.Series(values, index=dates)
"""
series = series.reindex( series = series.reindex(
periods, periods,
method='ffill', method='ffill',
fill_value=previous_value, fill_value=previous_value,
) )
series.sort_index(inplace=True) series.sort_index(inplace=True)
"""
all_series[asset] = series all_series[asset] = series
df = pd.DataFrame(all_series) df = pd.DataFrame(all_series)
+23 -30
View File
@@ -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,7 +13,7 @@ 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:
return exchange_cache[key] return exchange_cache[key]
@@ -37,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
@@ -55,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.
@@ -74,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
+1 -28
View File
@@ -3,42 +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):
return obj.to_dict()
# 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)
+4 -2
View File
@@ -396,7 +396,8 @@ def email_error(algo_name, dt, e, environ=None):
)}) )})
def stats_to_algo_folder(stats, algo_namespace, recorded_cols=None): def stats_to_algo_folder(stats, algo_namespace,
folder_name, recorded_cols=None):
""" """
Saves the performance stats to the algo local folder. Saves the performance stats to the algo local folder.
@@ -404,6 +405,7 @@ def stats_to_algo_folder(stats, algo_namespace, recorded_cols=None):
---------- ----------
stats: list[Object] stats: list[Object]
algo_namespace: str algo_namespace: str
folder_name: str
recorded_cols: list[str] recorded_cols: list[str]
Returns Returns
@@ -416,7 +418,7 @@ def stats_to_algo_folder(stats, algo_namespace, recorded_cols=None):
timestr = time.strftime('%Y%m%d') timestr = time.strftime('%Y%m%d')
folder = get_algo_folder(algo_namespace) folder = get_algo_folder(algo_namespace)
stats_folder = os.path.join(folder, 'stats') stats_folder = os.path.join(folder, folder_name)
ensure_directory(stats_folder) ensure_directory(stats_folder)
filename = os.path.join(stats_folder, '{}.csv'.format(timestr)) filename = os.path.join(stats_folder, '{}.csv'.format(timestr))
+2 -156
View File
@@ -580,162 +580,8 @@ which you can skim through for now. A copy of this algorithm is available in
the ``examples`` directory: the ``examples`` directory:
`dual_moving_average.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/dual_moving_average.py>`_. `dual_moving_average.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/dual_moving_average.py>`_.
.. code-block:: python .. literalinclude:: ../../catalyst/examples/dual_moving_average.py
:language: python
import numpy as np
import pandas as pd
from logbook import Logger
import matplotlib.pyplot as plt
from catalyst import run_algorithm
from catalyst.api import (order, record, symbol, order_target_percent,
get_open_orders)
from catalyst.exchange.utils.stats_utils import extract_transactions
NAMESPACE = 'dual_moving_average'
log = Logger(NAMESPACE)
def initialize(context):
context.i = 0
context.asset = symbol('ltc_usd')
context.base_price = None
def handle_data(context, data):
# define the windows for the moving averages
short_window = 50
long_window = 200
# Skip as many bars as long_window to properly compute the average
context.i += 1
if context.i < long_window:
return
# Compute moving averages calling data.history() for each
# moving average with the appropriate parameters. We choose to use
# minute bars for this simulation -> freq="1m"
# Returns a pandas dataframe.
short_mavg = data.history(context.asset, 'price',
bar_count=short_window, frequency="1m").mean()
long_mavg = data.history(context.asset, 'price',
bar_count=long_window, frequency="1m").mean()
# Let's keep the price of our asset in a more handy variable
price = data.current(context.asset, 'price')
# If base_price is not set, we use the current value. This is the
# price at the first bar which we reference to calculate price_change.
if context.base_price is None:
context.base_price = price
price_change = (price - context.base_price) / context.base_price
# Save values for later inspection
record(price=price,
cash=context.portfolio.cash,
price_change=price_change,
short_mavg=short_mavg,
long_mavg=long_mavg)
# Since we are using limit orders, some orders may not execute immediately
# we wait until all orders are executed before considering more trades.
orders = get_open_orders(context.asset)
if len(orders) > 0:
return
# Exit if we cannot trade
if not data.can_trade(context.asset):
return
# We check what's our position on our portfolio and trade accordingly
pos_amount = context.portfolio.positions[context.asset].amount
# Trading logic
if short_mavg > long_mavg and pos_amount == 0:
# we buy 100% of our portfolio for this asset
order_target_percent(context.asset, 1)
elif short_mavg < long_mavg and pos_amount > 0:
# we sell all our positions for this asset
order_target_percent(context.asset, 0)
def analyze(context, perf):
# Get the base_currency that was passed as a parameter to the simulation
exchange = list(context.exchanges.values())[0]
base_currency = exchange.base_currency.upper()
# First chart: Plot portfolio value using base_currency
ax1 = plt.subplot(411)
perf.loc[:, ['portfolio_value']].plot(ax=ax1)
ax1.legend_.remove()
ax1.set_ylabel('Portfolio Value\n({})'.format(base_currency))
start, end = ax1.get_ylim()
ax1.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
# Second chart: Plot asset price, moving averages and buys/sells
ax2 = plt.subplot(412, sharex=ax1)
perf.loc[:, ['price','short_mavg','long_mavg']].plot(ax=ax2, label='Price')
ax2.legend_.remove()
ax2.set_ylabel('{asset}\n({base})'.format(
asset = context.asset.symbol,
base = base_currency
))
start, end = ax2.get_ylim()
ax2.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
transaction_df = extract_transactions(perf)
if not transaction_df.empty:
buy_df = transaction_df[transaction_df['amount'] > 0]
sell_df = transaction_df[transaction_df['amount'] < 0]
ax2.scatter(
buy_df.index.to_pydatetime(),
perf.loc[buy_df.index, 'price'],
marker='^',
s=100,
c='green',
label=''
)
ax2.scatter(
sell_df.index.to_pydatetime(),
perf.loc[sell_df.index, 'price'],
marker='v',
s=100,
c='red',
label=''
)
# Third chart: Compare percentage change between our portfolio
# and the price of the asset
ax3 = plt.subplot(413, sharex=ax1)
perf.loc[:, ['algorithm_period_return', 'price_change']].plot(ax=ax3)
ax3.legend_.remove()
ax3.set_ylabel('Percent Change')
start, end = ax3.get_ylim()
ax3.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
# Fourth chart: Plot our cash
ax4 = plt.subplot(414, sharex=ax1)
perf.cash.plot(ax=ax4)
ax4.set_ylabel('Cash\n({})'.format(base_currency))
start, end = ax4.get_ylim()
ax4.yaxis.set_ticks(np.arange(0, end, end/5))
plt.show()
if __name__ == '__main__':
run_algorithm(
capital_base=1000,
data_frequency='minute',
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bitfinex',
algo_namespace=NAMESPACE,
base_currency='usd',
start=pd.to_datetime('2017-9-22', utc=True),
end=pd.to_datetime('2017-9-23', utc=True),
)
In order to run the code above, you have to ingest the needed data first: In order to run the code above, you have to ingest the needed data first:
+17 -881
View File
@@ -52,35 +52,8 @@ Buy BTC Simple Algorithm
Source code: `examples/buy_btc_simple.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/buy_btc_simple.py>`_ Source code: `examples/buy_btc_simple.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/buy_btc_simple.py>`_
.. code-block:: python .. literalinclude:: ../../catalyst/examples/buy_btc_simple.py
:language: python
'''
Run this example, by executing the following from your terminal:
catalyst ingest-exchange -x bitfinex -f daily -i btc_usdt
catalyst run -f buy_btc_simple.py -x bitfinex --start 2016-1-1 --end 2017-9-30 -o buy_btc_simple_out.pickle
If you want to run this code using another exchange, make sure that
the asset is available on that exchange. For example, if you were to run
it for exchange Poloniex, you would need to edit the following line:
context.asset = symbol('btc_usdt') # note 'usdt' instead of 'usd'
and specify exchange poloniex as follows:
catalyst ingest-exchange -x poloniex -f daily -i btc_usdt
catalyst run -f buy_btc_simple.py -x poloniex --start 2016-1-1 --end 2017-9-30 -o buy_btc_simple_out.pickle
To see which assets are available on each exchange, visit:
https://www.enigma.co/catalyst/status
'''
from catalyst.api import order, record, symbol
def initialize(context):
context.asset = symbol('btc_usd')
def handle_data(context, data):
order(context.asset, 1)
record(btc = data.current(context.asset, 'price'))
This simple algorithm does not produce any output nor displays any chart. This simple algorithm does not produce any output nor displays any chart.
@@ -90,8 +63,6 @@ This simple algorithm does not produce any output nor displays any chart.
Buy and Hodl Algorithm Buy and Hodl Algorithm
~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~
Source code: `examples/buy_and_hodl.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/buy_and_hodl.py>`_
First ingest the historical pricing data needed to run this algorithm: First ingest the historical pricing data needed to run this algorithm:
.. code-block:: bash .. code-block:: bash
@@ -119,157 +90,10 @@ that 2015-3-1 is the earliest date that Catalyst supports (if you choose an
earlier date, you'll get an error), and the most recent date you can choose is earlier date, you'll get an error), and the most recent date you can choose is
one day prior to the current date. one day prior to the current date.
Source code: `examples/buy_and_hodl.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/buy_and_hodl.py>`_
.. code-block:: python .. literalinclude:: ../../catalyst/examples/buy_and_hodl.py
:language: python
#!/usr/bin/env python
#
# Copyright 2017 Enigma MPC, Inc.
# Copyright 2015 Quantopian, Inc.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
import pandas as pd
import matplotlib.pyplot as plt
from catalyst import run_algorithm
from catalyst.api import (order_target_value, symbol, record,
cancel_order, get_open_orders, )
def initialize(context):
context.ASSET_NAME = 'btc_usd'
context.TARGET_HODL_RATIO = 0.8
context.RESERVE_RATIO = 1.0 - context.TARGET_HODL_RATIO
context.is_buying = True
context.asset = symbol(context.ASSET_NAME)
context.i = 0
def handle_data(context, data):
context.i += 1
starting_cash = context.portfolio.starting_cash
target_hodl_value = context.TARGET_HODL_RATIO * starting_cash
reserve_value = context.RESERVE_RATIO * starting_cash
# Cancel any outstanding orders
orders = get_open_orders(context.asset) or []
for order in orders:
cancel_order(order)
# Stop buying after passing the reserve threshold
cash = context.portfolio.cash
if cash <= reserve_value:
context.is_buying = False
# Retrieve current asset price from pricing data
price = data.current(context.asset, 'price')
# Check if still buying and could (approximately) afford another purchase
if context.is_buying and cash > price:
print('buying')
# Place order to make position in asset equal to target_hodl_value
order_target_value(
context.asset,
target_hodl_value,
limit_price=price * 1.1,
)
record(
price=price,
volume=data.current(context.asset, 'volume'),
cash=cash,
starting_cash=context.portfolio.starting_cash,
leverage=context.account.leverage,
)
def analyze(context=None, results=None):
# Plot the portfolio and asset data.
ax1 = plt.subplot(611)
results[['portfolio_value']].plot(ax=ax1)
ax1.set_ylabel('Portfolio Value (USD)')
ax2 = plt.subplot(612, sharex=ax1)
ax2.set_ylabel('{asset} (USD)'.format(asset=context.ASSET_NAME))
results[['price']].plot(ax=ax2)
trans = results.ix[[t != [] for t in results.transactions]]
buys = trans.ix[
[t[0]['amount'] > 0 for t in trans.transactions]
]
ax2.scatter(
buys.index.to_pydatetime(),
results.price[buys.index],
marker='^',
s=100,
c='g',
label=''
)
ax3 = plt.subplot(613, sharex=ax1)
results[['leverage', 'alpha', 'beta']].plot(ax=ax3)
ax3.set_ylabel('Leverage ')
ax4 = plt.subplot(614, sharex=ax1)
results[['starting_cash', 'cash']].plot(ax=ax4)
ax4.set_ylabel('Cash (USD)')
results[[
'treasury',
'algorithm',
'benchmark',
]] = results[[
'treasury_period_return',
'algorithm_period_return',
'benchmark_period_return',
]]
ax5 = plt.subplot(615, sharex=ax1)
results[[
'treasury',
'algorithm',
'benchmark',
]].plot(ax=ax5)
ax5.set_ylabel('Percent Change')
ax6 = plt.subplot(616, sharex=ax1)
results[['volume']].plot(ax=ax6)
ax6.set_ylabel('Volume (mCoins/5min)')
plt.legend(loc=3)
# Show the plot.
plt.gcf().set_size_inches(18, 8)
plt.show()
if __name__ == '__main__':
run_algorithm(
capital_base=10000,
data_frequency='daily',
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bitfinex',
algo_namespace='buy_and_hodl',
base_currency='usd',
start=pd.to_datetime('2015-03-01', utc=True),
end=pd.to_datetime('2017-10-31', utc=True),
)
.. image:: https://s3.amazonaws.com/enigmaco-docs/github.io/example_buy_and_hodl.png .. image:: https://s3.amazonaws.com/enigmaco-docs/github.io/example_buy_and_hodl.png
@@ -278,166 +102,13 @@ one day prior to the current date.
Dual Moving Average Crossover Dual Moving Average Crossover
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Source Code: `examples/dual_moving_average.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/dual_moving_average.py>`_
This strategy is covered in detail in the last part of This strategy is covered in detail in the last part of
`this tutorial <beginner-tutorial.html#history>`_. `this tutorial <beginner-tutorial.html#history>`_.
.. code-block:: python Source Code: `examples/dual_moving_average.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/dual_moving_average.py>`_
import numpy as np .. literalinclude:: ../../catalyst/examples/dual_moving_average.py
import pandas as pd :language: python
from logbook import Logger
import matplotlib.pyplot as plt
from catalyst import run_algorithm
from catalyst.api import (order, record, symbol, order_target_percent,
get_open_orders)
from catalyst.exchange.stats_utils import extract_transactions
NAMESPACE = 'dual_moving_average'
log = Logger(NAMESPACE)
def initialize(context):
context.i = 0
context.asset = symbol('ltc_usd')
context.base_price = None
def handle_data(context, data):
# define the windows for the moving averages
short_window = 50
long_window = 200
# Skip as many bars as long_window to properly compute the average
context.i += 1
if context.i < long_window:
return
# Compute moving averages calling data.history() for each
# moving average with the appropriate parameters. We choose to use
# minute bars for this simulation -> freq="1m"
# Returns a pandas dataframe.
short_mavg = data.history(context.asset, 'price',
bar_count=short_window, frequency="1m").mean()
long_mavg = data.history(context.asset, 'price',
bar_count=long_window, frequency="1m").mean()
# Let's keep the price of our asset in a more handy variable
price = data.current(context.asset, 'price')
# If base_price is not set, we use the current value. This is the
# price at the first bar which we reference to calculate price_change.
if context.base_price is None:
context.base_price = price
price_change = (price - context.base_price) / context.base_price
# Save values for later inspection
record(price=price,
cash=context.portfolio.cash,
price_change=price_change,
short_mavg=short_mavg,
long_mavg=long_mavg)
# Since we are using limit orders, some orders may not execute immediately
# we wait until all orders are executed before considering more trades.
orders = get_open_orders(context.asset)
if len(orders) > 0:
return
# Exit if we cannot trade
if not data.can_trade(context.asset):
return
# We check what's our position on our portfolio and trade accordingly
pos_amount = context.portfolio.positions[context.asset].amount
# Trading logic
if short_mavg > long_mavg and pos_amount == 0:
# we buy 100% of our portfolio for this asset
order_target_percent(context.asset, 1)
elif short_mavg < long_mavg and pos_amount > 0:
# we sell all our positions for this asset
order_target_percent(context.asset, 0)
def analyze(context, perf):
# Get the base_currency that was passed as a parameter to the simulation
base_currency = context.exchanges.values()[0].base_currency.upper()
# First chart: Plot portfolio value using base_currency
ax1 = plt.subplot(411)
perf.loc[:, ['portfolio_value']].plot(ax=ax1)
ax1.legend_.remove()
ax1.set_ylabel('Portfolio Value\n({})'.format(base_currency))
start, end = ax1.get_ylim()
ax1.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
# Second chart: Plot asset price, moving averages and buys/sells
ax2 = plt.subplot(412, sharex=ax1)
perf.loc[:, ['price','short_mavg','long_mavg']].plot(ax=ax2, label='Price')
ax2.legend_.remove()
ax2.set_ylabel('{asset}\n({base})'.format(
asset = context.asset.symbol,
base = base_currency
))
start, end = ax2.get_ylim()
ax2.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
transaction_df = extract_transactions(perf)
if not transaction_df.empty:
buy_df = transaction_df[transaction_df['amount'] > 0]
sell_df = transaction_df[transaction_df['amount'] < 0]
ax2.scatter(
buy_df.index.to_pydatetime(),
perf.loc[buy_df.index, 'price'],
marker='^',
s=100,
c='green',
label=''
)
ax2.scatter(
sell_df.index.to_pydatetime(),
perf.loc[sell_df.index, 'price'],
marker='v',
s=100,
c='red',
label=''
)
# Third chart: Compare percentage change between our portfolio
# and the price of the asset
ax3 = plt.subplot(413, sharex=ax1)
perf.loc[:, ['algorithm_period_return', 'price_change']].plot(ax=ax3)
ax3.legend_.remove()
ax3.set_ylabel('Percent Change')
start, end = ax3.get_ylim()
ax3.yaxis.set_ticks(np.arange(start, end, (end-start)/5))
# Fourth chart: Plot our cash
ax4 = plt.subplot(414, sharex=ax1)
perf.cash.plot(ax=ax4)
ax4.set_ylabel('Cash\n({})'.format(base_currency))
start, end = ax4.get_ylim()
ax4.yaxis.set_ticks(np.arange(0, end, end/5))
plt.show()
if __name__ == '__main__':
run_algorithm(
capital_base=1000,
data_frequency='minute',
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bitfinex',
algo_namespace=NAMESPACE,
base_currency='usd',
start=pd.to_datetime('2017-9-22', utc=True),
end=pd.to_datetime('2017-9-23', utc=True),
)
.. image:: https://s3.amazonaws.com/enigmaco-docs/github.io/tutorial_dual_moving_average.png .. image:: https://s3.amazonaws.com/enigmaco-docs/github.io/tutorial_dual_moving_average.png
@@ -447,8 +118,6 @@ This strategy is covered in detail in the last part of
Mean Reversion Algorithm Mean Reversion Algorithm
~~~~~~~~~~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~~~~~~~~~~
Source code: `examples/mean_reversion_simple.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/mean_reversion_simple.py>`_
This algorithm is based on a simple momentum strategy. When the cryptoasset goes This algorithm is based on a simple momentum strategy. When the cryptoasset goes
up quickly, we're going to buy; when it goes down quickly, we're going to sell. up quickly, we're going to buy; when it goes down quickly, we're going to sell.
Hopefully, we'll ride the waves. Hopefully, we'll ride the waves.
@@ -469,284 +138,10 @@ lines 218-245, so in order to run the algorithm we just type:
python mean_reversion_simple.py python mean_reversion_simple.py
.. code-block:: python Source code: `examples/mean_reversion_simple.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/mean_reversion_simple.py>`_
import os .. literalinclude:: ../../catalyst/examples/mean_reversion_simple.py
import tempfile :language: python
import time
import numpy as np
import pandas as pd
import talib
from logbook import Logger
from catalyst import run_algorithm
from catalyst.api import symbol, record, order_target_percent, get_open_orders
from catalyst.exchange.stats_utils import extract_transactions
# We give a name to the algorithm which Catalyst will use to persist its state.
# In this example, Catalyst will create the `.catalyst/data/live_algos`
# directory. If we stop and start the algorithm, Catalyst will resume its
# state using the files included in the folder.
from catalyst.utils.paths import ensure_directory
NAMESPACE = 'mean_reversion_simple'
log = Logger(NAMESPACE)
# To run an algorithm in Catalyst, you need two functions: initialize and
# handle_data.
def initialize(context):
# This initialize function sets any data or variables that you'll use in
# your algorithm. For instance, you'll want to define the trading pair (or
# trading pairs) you want to backtest. You'll also want to define any
# parameters or values you're going to use.
# In our example, we're looking at Neo in USD.
context.neo_eth = symbol('neo_usd')
context.base_price = None
context.current_day = None
context.RSI_OVERSOLD = 30
context.RSI_OVERBOUGHT = 80
context.CANDLE_SIZE = '15T'
context.start_time = time.time()
def handle_data(context, data):
# This handle_data function is where the real work is done. Our data is
# minute-level tick data, and each minute is called a frame. This function
# runs on each frame of the data.
# We flag the first period of each day.
# Since cryptocurrencies trade 24/7 the `before_trading_starts` handle
# would only execute once. This method works with minute and daily
# frequencies.
today = data.current_dt.floor('1D')
if today != context.current_day:
context.traded_today = False
context.current_day = today
# We're computing the volume-weighted-average-price of the security
# defined above, in the context.neo_eth variable. For this example, we're
# using three bars on the 15 min bars.
# The frequency attribute determine the bar size. We use this convention
# for the frequency alias:
# http://pandas.pydata.org/pandas-docs/stable/timeseries.html#offset-aliases
prices = data.history(
context.neo_eth,
fields='close',
bar_count=50,
frequency=context.CANDLE_SIZE
)
# Ta-lib calculates various technical indicator based on price and
# volume arrays.
# In this example, we are comp
rsi = talib.RSI(prices.values, timeperiod=14)
# We need a variable for the current price of the security to compare to
# the average. Since we are requesting two fields, data.current()
# returns a DataFrame with
current = data.current(context.neo_eth, fields=['close', 'volume'])
price = current['close']
# If base_price is not set, we use the current value. This is the
# price at the first bar which we reference to calculate price_change.
if context.base_price is None:
context.base_price = price
price_change = (price - context.base_price) / context.base_price
cash = context.portfolio.cash
# Now that we've collected all current data for this frame, we use
# the record() method to save it. This data will be available as
# a parameter of the analyze() function for further analysis.
record(
price=price,
volume=current['volume'],
price_change=price_change,
rsi=rsi[-1],
cash=cash
)
# We are trying to avoid over-trading by limiting our trades to
# one per day.
if context.traded_today:
return
# Since we are using limit orders, some orders may not execute immediately
# we wait until all orders are executed before considering more trades.
orders = get_open_orders(context.neo_eth)
if len(orders) > 0:
return
# Exit if we cannot trade
if not data.can_trade(context.neo_eth):
return
# Another powerful built-in feature of the Catalyst backtester is the
# portfolio object. The portfolio object tracks your positions, cash,
# cost basis of specific holdings, and more. In this line, we calculate
# how long or short our position is at this minute.
pos_amount = context.portfolio.positions[context.neo_eth].amount
if rsi[-1] <= context.RSI_OVERSOLD and pos_amount == 0:
log.info(
'{}: buying - price: {}, rsi: {}'.format(
data.current_dt, price, rsi[-1]
)
)
# Set a style for limit orders,
limit_price = price * 1.005
order_target_percent(
context.neo_eth, 1, limit_price=limit_price
)
context.traded_today = True
elif rsi[-1] >= context.RSI_OVERBOUGHT and pos_amount > 0:
log.info(
'{}: selling - price: {}, rsi: {}'.format(
data.current_dt, price, rsi[-1]
)
)
limit_price = price * 0.995
order_target_percent(
context.neo_eth, 0, limit_price=limit_price
)
context.traded_today = True
def analyze(context=None, perf=None):
end = time.time()
log.info('elapsed time: {}'.format(end - context.start_time))
import matplotlib.pyplot as plt
# The base currency of the algo exchange
base_currency = context.exchanges.values()[0].base_currency.upper()
# Plot the portfolio value over time.
ax1 = plt.subplot(611)
perf.loc[:, 'portfolio_value'].plot(ax=ax1)
ax1.set_ylabel('Portfolio\nValue\n({})'.format(base_currency))
# Plot the price increase or decrease over time.
ax2 = plt.subplot(612, sharex=ax1)
perf.loc[:, 'price'].plot(ax=ax2, label='Price')
ax2.set_ylabel('{asset}\n({base})'.format(
asset=context.neo_eth.symbol, base=base_currency
))
transaction_df = extract_transactions(perf)
if not transaction_df.empty:
buy_df = transaction_df[transaction_df['amount'] > 0]
sell_df = transaction_df[transaction_df['amount'] < 0]
ax2.scatter(
buy_df.index.to_pydatetime(),
perf.loc[buy_df.index.floor('1 min'), 'price'],
marker='^',
s=100,
c='green',
label=''
)
ax2.scatter(
sell_df.index.to_pydatetime(),
perf.loc[sell_df.index.floor('1 min'), 'price'],
marker='v',
s=100,
c='red',
label=''
)
ax4 = plt.subplot(613, sharex=ax1)
perf.loc[:, 'cash'].plot(
ax=ax4, label='Base Currency ({})'.format(base_currency)
)
ax4.set_ylabel('Cash\n({})'.format(base_currency))
perf['algorithm'] = perf.loc[:, 'algorithm_period_return']
ax5 = plt.subplot(614, sharex=ax1)
perf.loc[:, ['algorithm', 'price_change']].plot(ax=ax5)
ax5.set_ylabel('Percent\nChange')
ax6 = plt.subplot(615, sharex=ax1)
perf.loc[:, 'rsi'].plot(ax=ax6, label='RSI')
ax6.set_ylabel('RSI')
ax6.axhline(context.RSI_OVERBOUGHT, color='darkgoldenrod')
ax6.axhline(context.RSI_OVERSOLD, color='darkgoldenrod')
if not transaction_df.empty:
ax6.scatter(
buy_df.index.to_pydatetime(),
perf.loc[buy_df.index.floor('1 min'), 'rsi'],
marker='^',
s=100,
c='green',
label=''
)
ax6.scatter(
sell_df.index.to_pydatetime(),
perf.loc[sell_df.index.floor('1 min'), 'rsi'],
marker='v',
s=100,
c='red',
label=''
)
plt.legend(loc=3)
start, end = ax6.get_ylim()
ax6.yaxis.set_ticks(np.arange(0, end, end/5))
# Show the plot.
plt.gcf().set_size_inches(18, 8)
plt.show()
pass
if __name__ == '__main__':
# The execution mode: backtest or live
MODE = 'backtest'
if MODE == 'backtest':
folder = os.path.join(
tempfile.gettempdir(), 'catalyst', NAMESPACE
)
ensure_directory(folder)
timestr = time.strftime('%Y%m%d-%H%M%S')
out = os.path.join(folder, '{}.p'.format(timestr))
# catalyst run -f catalyst/examples/mean_reversion_simple.py -x bitfinex -s 2017-10-1 -e 2017-11-10 -c usdt -n mean-reversion --data-frequency minute --capital-base 10000
run_algorithm(
capital_base=10000,
data_frequency='minute',
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bitfinex',
algo_namespace=NAMESPACE,
base_currency='usd',
start=pd.to_datetime('2017-10-01', utc=True),
end=pd.to_datetime('2017-11-10', utc=True),
output=out
)
log.info('saved perf stats: {}'.format(out))
elif MODE == 'live':
run_algorithm(
capital_base=0.5,
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bittrex',
live=True,
algo_namespace=NAMESPACE,
base_currency='usd',
live_graph=False
)
.. image:: https://s3.amazonaws.com/enigmaco-docs/github.io/example_mean_reversion_simple.png .. image:: https://s3.amazonaws.com/enigmaco-docs/github.io/example_mean_reversion_simple.png
@@ -763,8 +158,6 @@ strategy.
Simple Universe Simple Universe
~~~~~~~~~~~~~~~ ~~~~~~~~~~~~~~~
Source code: `examples/simple_universe.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/simple_universe.py>`_
This example aims to provide an easy way for users to learn how to This example aims to provide an easy way for users to learn how to
collect data from any given exchange and select a subset of the available collect data from any given exchange and select a subset of the available
currency pairs for trading. You simply need to specify the exchange and currency pairs for trading. You simply need to specify the exchange and
@@ -791,142 +184,10 @@ of the file:
catalyst ingest-exchange -x bitfinex -f minute catalyst ingest-exchange -x bitfinex -f minute
.. code-block:: bash Source code: `examples/simple_universe.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/simple_universe.py>`_
python simple_universe.py
Credits: This code was originally submitted by `Abner Ayala-Acevedo
<https://github.com/abnera>`_. Thank you!
.. code-block:: python
from datetime import timedelta
import numpy as np
import pandas as pd
from catalyst import run_algorithm
from catalyst.exchange.utils.exchange_utils import get_exchange_symbols
from catalyst.api import (symbols, )
def initialize(context):
context.i = -1 # minute counter
context.exchange = context.exchanges.values()[0].name.lower()
context.base_currency = context.exchanges.values()[0].base_currency.lower()
def handle_data(context, data):
context.i += 1
lookback_days = 7 # 7 days
# current date & time in each iteration formatted into a string
now = data.current_dt
date, time = now.strftime('%Y-%m-%d %H:%M:%S').split(' ')
lookback_date = now - timedelta(days=lookback_days)
# keep only the date as a string, discard the time
lookback_date = lookback_date.strftime('%Y-%m-%d %H:%M:%S').split(' ')[0]
one_day_in_minutes = 1440 # 60 * 24 assumes data_frequency='minute'
# update universe everyday at midnight
if not context.i % one_day_in_minutes:
context.universe = universe(context, lookback_date, date)
# get data every 30 minutes
minutes = 30
# get lookback_days of history data: that is 'lookback' number of bins
lookback = one_day_in_minutes / minutes * lookback_days
if not context.i % minutes and context.universe:
# we iterate for every pair in the current universe
for coin in context.coins:
pair = str(coin.symbol)
# Get 30 minute interval OHLCV data. This is the standard data
# required for candlestick or indicators/signals. Return Pandas
# DataFrames. 30T means 30-minute re-sampling of one minute data.
# Adjust it to your desired time interval as needed.
opened = fill(data.history(coin, 'open',
bar_count=lookback, frequency='30T')).values
high = fill(data.history(coin, 'high',
bar_count=lookback, frequency='30T')).values
low = fill(data.history(coin, 'low',
bar_count=lookback, frequency='30T')).values
close = fill(data.history(coin, 'price',
bar_count=lookback, frequency='30T')).values
volume = fill(data.history(coin, 'volume',
bar_count=lookback, frequency='30T')).values
# close[-1] is the last value in the set, which is the equivalent
# to current price (as in the most recent value)
# displays the minute price for each pair every 30 minutes
print('{now}: {pair} -\tO:{o},\tH:{h},\tL:{c},\tC{c},\tV:{v}'.format(
now=now,
pair=pair,
o=opened[-1],
h=high[-1],
l=low[-1],
c=close[-1],
v=volume[-1],
))
# -------------------------------------------------------------
# --------------- Insert Your Strategy Here -------------------
# -------------------------------------------------------------
def analyze(context=None, results=None):
pass
# Get the universe for a given exchange and a given base_currency market
# Example: Poloniex BTC Market
def universe(context, lookback_date, current_date):
# get all the pairs for the given exchange
json_symbols = get_exchange_symbols(context.exchange)
# convert into a DataFrame for easier processing
df = pd.DataFrame.from_dict(json_symbols).transpose().astype(str)
df['base_currency'] = df.apply(lambda row: row.symbol.split('_')[1],axis=1)
df['market_currency'] = df.apply(lambda row: row.symbol.split('_')[0],axis=1)
# Filter all the pairs to get only the ones for a given base_currency
df = df[df['base_currency'] == context.base_currency]
# Filter all the pairs to ensure that pair existed in the current date range
df = df[df.start_date < lookback_date]
df = df[df.end_daily >= current_date]
context.coins = symbols(*df.symbol) # convert all the pairs to symbols
return df.symbol.tolist()
# Replace all NA, NAN or infinite values with its nearest value
def fill(series):
if isinstance(series, pd.Series):
return series.replace([np.inf, -np.inf], np.nan).ffill().bfill()
elif isinstance(series, np.ndarray):
return pd.Series(series).replace(
[np.inf, -np.inf], np.nan
).ffill().bfill().values
else:
return series
if __name__ == '__main__':
start_date = pd.to_datetime('2017-11-10', utc=True)
end_date = pd.to_datetime('2017-11-13', utc=True)
performance = run_algorithm(start=start_date, end=end_date,
capital_base=100.0, # amount of base_currency
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bitfinex',
data_frequency='minute',
base_currency='btc',
live=False,
live_graph=False,
algo_namespace='simple_universe')
.. literalinclude:: ../../catalyst/examples/simple_universe.py
:language: python
.. _portfolio_optimization: .. _portfolio_optimization:
@@ -940,135 +201,10 @@ use 180 days of historical data and rebalance every 30 days. This code was used
in writting the following article: in writting the following article:
`Markowitz Portfolio Optimization for Cryptocurrencies <https://blog.enigma.co/markowitz-portfolio-optimization-for-cryptocurrencies-in-catalyst-b23c38652556>`_. `Markowitz Portfolio Optimization for Cryptocurrencies <https://blog.enigma.co/markowitz-portfolio-optimization-for-cryptocurrencies-in-catalyst-b23c38652556>`_.
.. code-block:: python Source code: `examples/simple_universe.py <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/portfolio_optimization.py>`_
''' .. literalinclude:: ../../catalyst/examples/portfolio_optimization.py
You can run this code using the Python interpreter: :language: python
$ python portfolio_optimization.py
'''
from __future__ import division
import os
import pytz
import numpy as np
import pandas as pd
from scipy.optimize import minimize
import matplotlib.pyplot as plt
from datetime import datetime
from catalyst.api import record, symbol, symbols, order_target_percent
from catalyst.utils.run_algo import run_algorithm
np.set_printoptions(threshold='nan', suppress=True)
def initialize(context):
# Portfolio assets list
context.assets = symbols('btc_usdt', 'eth_usdt', 'ltc_usdt', 'dash_usdt',
'xmr_usdt')
context.nassets = len(context.assets)
# Set the time window that will be used to compute expected return
# and asset correlations
context.window = 180
# Set the number of days between each portfolio rebalancing
context.rebalance_period = 30
context.i = 0
def handle_data(context, data):
# Only rebalance at the beggining of the algorithm execution and
# every multiple of the rebalance period
if context.i == 0 or context.i%context.rebalance_period == 0:
n = context.window
prices = data.history(context.assets, fields='price',
bar_count=n+1, frequency='1d')
pr = np.asmatrix(prices)
t_prices = prices.iloc[1:n+1]
t_val = t_prices.values
tminus_prices = prices.iloc[0:n]
tminus_val = tminus_prices.values
# Compute daily returns (r)
r = np.asmatrix(t_val/tminus_val-1)
# Compute the expected returns of each asset with the average
# daily return for the selected time window
m = np.asmatrix(np.mean(r, axis=0))
# ###
stds = np.std(r, axis=0)
# Compute excess returns matrix (xr)
xr = r - m
# Matrix algebra to get variance-covariance matrix
cov_m = np.dot(np.transpose(xr),xr)/n
# Compute asset correlation matrix (informative only)
corr_m = cov_m/np.dot(np.transpose(stds),stds)
# Define portfolio optimization parameters
n_portfolios = 50000
results_array = np.zeros((3+context.nassets,n_portfolios))
for p in xrange(n_portfolios):
weights = np.random.random(context.nassets)
weights /= np.sum(weights)
w = np.asmatrix(weights)
p_r = np.sum(np.dot(w,np.transpose(m)))*365
p_std = np.sqrt(np.dot(np.dot(w,cov_m),np.transpose(w)))*np.sqrt(365)
#store results in results array
results_array[0,p] = p_r
results_array[1,p] = p_std
#store Sharpe Ratio (return / volatility) - risk free rate element
#excluded for simplicity
results_array[2,p] = results_array[0,p] / results_array[1,p]
i = 0
for iw in weights:
results_array[3+i,p] = weights[i]
i += 1
#convert results array to Pandas DataFrame
results_frame = pd.DataFrame(np.transpose(results_array),
columns=['r','stdev','sharpe']+context.assets)
#locate position of portfolio with highest Sharpe Ratio
max_sharpe_port = results_frame.iloc[results_frame['sharpe'].idxmax()]
#locate positon of portfolio with minimum standard deviation
min_vol_port = results_frame.iloc[results_frame['stdev'].idxmin()]
#order optimal weights for each asset
for asset in context.assets:
if data.can_trade(asset):
order_target_percent(asset, max_sharpe_port[asset])
#create scatter plot coloured by Sharpe Ratio
plt.scatter(results_frame.stdev,results_frame.r,c=results_frame.sharpe,cmap='RdYlGn')
plt.xlabel('Volatility')
plt.ylabel('Returns')
plt.colorbar()
#plot red star to highlight position of portfolio with highest Sharpe Ratio
plt.scatter(max_sharpe_port[1],max_sharpe_port[0],marker='o',color='b',s=200)
#plot green star to highlight position of minimum variance portfolio
plt.show()
print(max_sharpe_port)
record(pr=pr,r=r, m=m, stds=stds ,max_sharpe_port=max_sharpe_port, corr_m=corr_m)
context.i += 1
def analyze(context=None, results=None):
# Form DataFrame with selected data
data = results[['pr','r','m','stds','max_sharpe_port','corr_m','portfolio_value']]
# Save results in CSV file
filename = os.path.splitext(os.path.basename(__file__))[0]
data.to_csv(filename + '.csv')
# Bitcoin data is available from 2015-3-2. Dates vary for other tokens.
start = datetime(2017, 1, 1, 0, 0, 0, 0, pytz.utc)
end = datetime(2017, 8, 16, 0, 0, 0, 0, pytz.utc)
results = run_algorithm(initialize=initialize,
handle_data=handle_data,
analyze=analyze,
start=start,
end=end,
exchange_name='poloniex',
capital_base=100000, )
.. image:: https://cdn-images-1.medium.com/max/1600/0*EjjiKZHlYF3sn7yQ. .. image:: https://cdn-images-1.medium.com/max/1600/0*EjjiKZHlYF3sn7yQ.
:align: center :align: center
+10 -1
View File
@@ -89,7 +89,7 @@ Once either Conda or MiniConda has been set up you can install Catalyst:
.. code-block:: bash .. code-block:: bash
conda env create -f python2.7-environment.yml conda env create -f python2.7-environment.yml
4. Activate the environment (which you need to do every time you start a new 4. Activate the environment (which you need to do every time you start a new
session to run Catalyst): session to run Catalyst):
@@ -132,10 +132,19 @@ with the following steps:
conda env remove --name catalyst conda env remove --name catalyst
2. Create the environment: 2. Create the environment:
for python 2.7:
.. code-block:: bash .. code-block:: bash
conda create --name catalyst python=2.7 scipy zlib conda create --name catalyst python=2.7 scipy zlib
or for python 3.6:
.. code-block:: bash
conda create --name catalyst python=3.6 scipy zlib
3. Activate the environment: 3. Activate the environment:
+9
View File
@@ -184,5 +184,14 @@ Here is the breakdown of the new arguments:
essentially sleep and when the predefined time comes, it would start executing. essentially sleep and when the predefined time comes, it would start executing.
The `catalyst live` command offers additional parameters.
You can learn more by running the following from the command line:
.. code-block:: bash
catalyst live --help
Here is a complete algorithm for reference: Here is a complete algorithm for reference:
`Buy Low and Sell High <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/buy_low_sell_high_live.py>`_ `Buy Low and Sell High <https://github.com/enigmampc/catalyst/blob/master/catalyst/examples/buy_low_sell_high_live.py>`_
+2
View File
@@ -1,9 +1,11 @@
name: catalyst name: catalyst
channels: channels:
- defaults - defaults
- conda-forge
dependencies: dependencies:
- certifi=2016.2.28=py27_0 - certifi=2016.2.28=py27_0
- mkl=2017.0.3 - mkl=2017.0.3
- matplotlib=2.1.2=py36_0
- numpy=1.13.1=py27_0 - numpy=1.13.1=py27_0
- openssl=1.0.2l - openssl=1.0.2l
- pip=9.0.1=py27_1 - pip=9.0.1=py27_1
+16 -21
View File
@@ -1,29 +1,24 @@
name: catalyst name: catalyst
channels: channels:
- defaults - defaults
- conda-forge
dependencies: dependencies:
- ca-certificates=2017.08.26=ha1e5d58_0 - ca-certificates=2017.08.26
- certifi=2018.1.18=py36_0 - certifi=2018.1.18
- intel-openmp=2018.0.0=h8158457_8 - intel-openmp=2018.0.0
- libcxx=4.0.1=h579ed51_0 - mkl=2018.0.1
- libcxxabi=4.0.1=hebd6815_0 - numpy=1.14.0
- libedit=3.1=hb4e282d_0 - openssl=1.0.2n
- libffi=3.2.1=h475c297_4 - matplotlib=2.1.2=py36_0
- libgfortran=3.0.1=h93005f0_2 - pip=9.0.1
- mkl=2018.0.1=hfbd8650_4 - python=3.6.4
- ncurses=6.0=hd04f020_2 - scipy=1.0.0
- numpy=1.14.0=py36h8a80b8c_1
- openssl=1.0.2n=hdbc3d79_0
- pip=9.0.1=py36h1555ced_4
- python=3.6.4=hc167b69_1
- readline=7.0=hc1231fa_4
- scipy=1.0.0=py36h1de22e9_0
- setuptools=38.4.0=py36_0 - setuptools=38.4.0=py36_0
- sqlite=3.22.0=h3efe00b_0 - sqlite=3.22.0
- tk=8.6.7=h35a86e2_3 - tk=8.6.7
- wheel=0.30.0=py36h5eb2c71_1 - wheel=0.30.0
- xz=5.2.3=h0278029_2 - xz=5.2.3
- zlib=1.2.11=hf3cbc9b_2 - zlib=1.2.11
- pip: - pip:
- aiodns==1.1.1 - aiodns==1.1.1
- aiohttp==3.0.1 - aiohttp==3.0.1
-8
View File
@@ -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
@@ -197,7 +197,7 @@ class TestSuiteBundle:
# population=exchange_population, # population=exchange_population,
# features=[bundle], # features=[bundle],
# ) # Type: list[Exchange] # ) # Type: list[Exchange]
exchanges = [get_exchange('binance', skip_init=True)] exchanges = [get_exchange('poloniex', skip_init=True)]
data_portal = TestSuiteBundle.get_data_portal(exchanges) data_portal = TestSuiteBundle.get_data_portal(exchanges)
for exchange in exchanges: for exchange in exchanges:
@@ -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: