mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-22 12:40:30 +08:00
Compare commits
36
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ad2d0e9253 | ||
|
|
d64f5275ef | ||
|
|
4f4f6c050b | ||
|
|
fdd6b62963 | ||
|
|
bcb5fd2b14 | ||
|
|
c5e1945558 | ||
|
|
11144d83b8 | ||
|
|
85c2e9db4f | ||
|
|
817cb07bee | ||
|
|
8054d1d520 | ||
|
|
c1d7022846 | ||
|
|
8f3c440bac | ||
|
|
a785607d8f | ||
|
|
6e166383ed | ||
|
|
1696c39912 | ||
|
|
d79fdca561 | ||
|
|
8b6a48633d | ||
|
|
d03ce37f6e | ||
|
|
c47e88c26f | ||
|
|
f4db9f7b1e | ||
|
|
753881bade | ||
|
|
1be39f97a1 | ||
|
|
49bfd32341 | ||
|
|
01473e5146 | ||
|
|
16f9ab3ba5 | ||
|
|
b4755111a9 | ||
|
|
cf54806843 | ||
|
|
c01f2a39a4 | ||
|
|
1cfcb1d96e | ||
|
|
c40fd98022 | ||
|
|
b8d442cf89 | ||
|
|
3a44a3cc1f | ||
|
|
24fd0fa6f8 | ||
|
|
b2e5b5f73d | ||
|
|
0ef8b341ca | ||
|
|
753ca1db5a |
@@ -79,7 +79,6 @@ __all__ = [
|
|||||||
'gens',
|
'gens',
|
||||||
'run_algorithm',
|
'run_algorithm',
|
||||||
'utils',
|
'utils',
|
||||||
'exchange',
|
|
||||||
]
|
]
|
||||||
|
|
||||||
from ._version import get_versions
|
from ._version import get_versions
|
||||||
|
|||||||
+7
-13
@@ -181,12 +181,6 @@ def ipython_only(option):
|
|||||||
default=False,
|
default=False,
|
||||||
help='Print the algorithm to stdout.',
|
help='Print the algorithm to stdout.',
|
||||||
)
|
)
|
||||||
@click.option(
|
|
||||||
'-s',
|
|
||||||
'--start',
|
|
||||||
type=Date(tz='utc', as_timestamp=True),
|
|
||||||
help='The start date of the simulation.',
|
|
||||||
)
|
|
||||||
@ipython_only(click.option(
|
@ipython_only(click.option(
|
||||||
'--local-namespace/--no-local-namespace',
|
'--local-namespace/--no-local-namespace',
|
||||||
is_flag=True,
|
is_flag=True,
|
||||||
@@ -202,18 +196,18 @@ def ipython_only(option):
|
|||||||
@click.option(
|
@click.option(
|
||||||
'-x',
|
'-x',
|
||||||
'--exchange-name',
|
'--exchange-name',
|
||||||
type=click.Choice({'bitfinex'}),
|
type=click.Choice({'bitfinex', 'bittrex'}),
|
||||||
help='The name of the exchange (supported: bitfinex).',
|
help='The name of the targeted exchange (supported: bitfinex, bittrex).',
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-n',
|
'-n',
|
||||||
'--algo-name',
|
'--algo-namespace',
|
||||||
help='A label assigned to the algorithm for tracking purposes.',
|
help='A label assigned to the algorithm for data storage purposes.'
|
||||||
)
|
)
|
||||||
@click.option(
|
@click.option(
|
||||||
'-c',
|
'-c',
|
||||||
'--reference-currency',
|
'--base-currency',
|
||||||
help='The reference currency used to calculate statistics '
|
help='The base currency used to calculate statistics '
|
||||||
'(e.g. usd, btc, eth).',
|
'(e.g. usd, btc, eth).',
|
||||||
)
|
)
|
||||||
@click.pass_context
|
@click.pass_context
|
||||||
@@ -245,7 +239,7 @@ def run(ctx,
|
|||||||
ctx.fail("must specify an algorithm name '-n' in live execution "
|
ctx.fail("must specify an algorithm name '-n' in live execution "
|
||||||
"mode '--live'")
|
"mode '--live'")
|
||||||
if base_currency is None:
|
if base_currency is None:
|
||||||
ctx.fail("must specify a reference currency '-c' in live "
|
ctx.fail("must specify a base currency '-c' in live "
|
||||||
"execution mode '--live'")
|
"execution mode '--live'")
|
||||||
else:
|
else:
|
||||||
# check that the start and end dates are passed correctly
|
# check that the start and end dates are passed correctly
|
||||||
|
|||||||
@@ -1138,8 +1138,6 @@ class TradingAlgorithm(object):
|
|||||||
|
|
||||||
freq = self.sim_params.data_frequency
|
freq = self.sim_params.data_frequency
|
||||||
|
|
||||||
freq = self.sim_params.data_frequency
|
|
||||||
|
|
||||||
date_rule = date_rule or date_rules.every_day()
|
date_rule = date_rule or date_rules.every_day()
|
||||||
if freq is 'daily':
|
if freq is 'daily':
|
||||||
# ignore time rule in daily mode
|
# ignore time rule in daily mode
|
||||||
|
|||||||
+163
-13
@@ -20,31 +20,31 @@ Cythonized Asset object.
|
|||||||
cimport cython
|
cimport cython
|
||||||
from cpython.number cimport PyNumber_Index
|
from cpython.number cimport PyNumber_Index
|
||||||
from cpython.object cimport (
|
from cpython.object cimport (
|
||||||
Py_EQ,
|
Py_EQ,
|
||||||
Py_NE,
|
Py_NE,
|
||||||
Py_GE,
|
Py_GE,
|
||||||
Py_LE,
|
Py_LE,
|
||||||
Py_GT,
|
Py_GT,
|
||||||
Py_LT,
|
Py_LT,
|
||||||
)
|
)
|
||||||
from cpython cimport bool
|
from cpython cimport bool
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from datetime import timedelta
|
||||||
import numpy as np
|
import numpy as np
|
||||||
from numpy cimport int64_t
|
from numpy cimport int64_t
|
||||||
import warnings
|
import warnings
|
||||||
cimport numpy as np
|
cimport numpy as np
|
||||||
|
|
||||||
from catalyst.utils.calendars import get_calendar
|
from catalyst.utils.calendars import get_calendar
|
||||||
|
from catalyst.exchange.exchange_errors import InvalidSymbolError, SidHashError
|
||||||
|
|
||||||
# IMPORTANT NOTE: You must change this template if you change
|
# IMPORTANT NOTE: You must change this template if you change
|
||||||
# Asset.__reduce__, or else we'll attempt to unpickle an old version of this
|
# Asset.__reduce__, or else we'll attempt to unpickle an old version of this
|
||||||
# class
|
# class
|
||||||
CACHE_FILE_TEMPLATE = '/tmp/.%s-%s.v7.cache'
|
CACHE_FILE_TEMPLATE = '/tmp/.%s-%s.v7.cache'
|
||||||
|
|
||||||
|
|
||||||
cdef class Asset:
|
cdef class Asset:
|
||||||
|
|
||||||
cdef readonly int sid
|
cdef readonly int sid
|
||||||
# Cached hash of self.sid
|
# Cached hash of self.sid
|
||||||
cdef int sid_hash
|
cdef int sid_hash
|
||||||
@@ -230,9 +230,7 @@ cdef class Asset:
|
|||||||
calendar = get_calendar(self.exchange)
|
calendar = get_calendar(self.exchange)
|
||||||
return calendar.is_open_on_minute(dt_minute)
|
return calendar.is_open_on_minute(dt_minute)
|
||||||
|
|
||||||
|
|
||||||
cdef class Equity(Asset):
|
cdef class Equity(Asset):
|
||||||
|
|
||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
attrs = ('symbol', 'asset_name', 'exchange',
|
attrs = ('symbol', 'asset_name', 'exchange',
|
||||||
'start_date', 'end_date', 'first_traded', 'auto_close_date',
|
'start_date', 'end_date', 'first_traded', 'auto_close_date',
|
||||||
@@ -276,9 +274,7 @@ cdef class Equity(Asset):
|
|||||||
DeprecationWarning)
|
DeprecationWarning)
|
||||||
return self.asset_name
|
return self.asset_name
|
||||||
|
|
||||||
|
|
||||||
cdef class Future(Asset):
|
cdef class Future(Asset):
|
||||||
|
|
||||||
cdef readonly object root_symbol
|
cdef readonly object root_symbol
|
||||||
cdef readonly object notice_date
|
cdef readonly object notice_date
|
||||||
cdef readonly object expiration_date
|
cdef readonly object expiration_date
|
||||||
@@ -388,6 +384,160 @@ cdef class Future(Asset):
|
|||||||
super_dict['multiplier'] = self.multiplier
|
super_dict['multiplier'] = self.multiplier
|
||||||
return super_dict
|
return super_dict
|
||||||
|
|
||||||
|
cdef class TradingPair(Asset):
|
||||||
|
cdef readonly float leverage
|
||||||
|
cdef readonly object market_currency
|
||||||
|
cdef readonly object base_currency
|
||||||
|
|
||||||
|
_kwargnames = frozenset({
|
||||||
|
'sid',
|
||||||
|
'symbol',
|
||||||
|
'asset_name',
|
||||||
|
'start_date',
|
||||||
|
'end_date',
|
||||||
|
'first_traded',
|
||||||
|
'auto_close_date',
|
||||||
|
'exchange',
|
||||||
|
'exchange_full',
|
||||||
|
'leverage',
|
||||||
|
'market_currency',
|
||||||
|
'base_currency'
|
||||||
|
})
|
||||||
|
def __init__(self,
|
||||||
|
object symbol,
|
||||||
|
object exchange,
|
||||||
|
object start_date=None,
|
||||||
|
object asset_name=None,
|
||||||
|
int sid=0,
|
||||||
|
float leverage=1.0,
|
||||||
|
object end_date=None,
|
||||||
|
object first_traded=None,
|
||||||
|
object auto_close_date=None,
|
||||||
|
object exchange_full=None):
|
||||||
|
"""
|
||||||
|
Replicates the Asset constructor with some built-in conventions
|
||||||
|
and a new 'leverage' attribute.
|
||||||
|
|
||||||
|
Symbol
|
||||||
|
------
|
||||||
|
Catalyst defines its own set of "universal" symbols to reference
|
||||||
|
trading pairs across exchanges. This is required because exchanges
|
||||||
|
are not adhering to a universal symbolism. For example, Bitfinex
|
||||||
|
uses the BTC symbol for Bitcon while Kraken uses XBT. In addition,
|
||||||
|
pairs are sometimes presented differently. For example, Bitfinex
|
||||||
|
puts the market currency before the base currency without a
|
||||||
|
separator, Bittrex puts the base currency first and uses a dash
|
||||||
|
seperator.
|
||||||
|
|
||||||
|
Here is the Catalyst convention: [Market Currency]_[Base Currency]
|
||||||
|
For example: btc_usd, eth_btc, neo_eth, ltc_eur.
|
||||||
|
|
||||||
|
The symbol for each currency (e.g. btc, eth, ltc) is generally
|
||||||
|
aligned with the Bittrex exchange.
|
||||||
|
|
||||||
|
Sid
|
||||||
|
---
|
||||||
|
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.
|
||||||
|
|
||||||
|
Leverage
|
||||||
|
--------
|
||||||
|
In contrast with equities, crypto exchanges generally assign
|
||||||
|
leverage values to specific trading pairs. Pairs with the
|
||||||
|
highest volume and market cap generally benefit from high leverage.
|
||||||
|
New currencies from ICO generally cannot be leveraged.
|
||||||
|
|
||||||
|
The leverage value is either None or and integer.
|
||||||
|
|
||||||
|
Leverage allows you to open a larger position with a smaller amount
|
||||||
|
of funds. For example, if you open a $5,000 position in BTC/USD
|
||||||
|
with 5:1 leverage, only one-fifth of this amount, or $1000, will be
|
||||||
|
tied to the position from your balance. Your remaining balance will
|
||||||
|
be available for opening more positions. If you open this same
|
||||||
|
position with 2:1 leverage, $2,500 of your balance will be tied to
|
||||||
|
the position. If you open with 1:1 leverage, $5,000 of your balance
|
||||||
|
will be tied to the position.
|
||||||
|
|
||||||
|
:param symbol:
|
||||||
|
:param exchange:
|
||||||
|
:param start_date:
|
||||||
|
:param asset_name:
|
||||||
|
:param sid:
|
||||||
|
:param leverage:
|
||||||
|
:param end_date:
|
||||||
|
:param first_traded:
|
||||||
|
:param auto_close_date:
|
||||||
|
:param exchange_full:
|
||||||
|
"""
|
||||||
|
|
||||||
|
symbol = symbol.lower()
|
||||||
|
try:
|
||||||
|
self.market_currency, self.base_currency = symbol.split('_')
|
||||||
|
except Exception as e:
|
||||||
|
raise InvalidSymbolError(symbol=symbol, error=e)
|
||||||
|
|
||||||
|
if sid == 0 or sid is None:
|
||||||
|
try:
|
||||||
|
sid = abs(hash(symbol)) % (10 ** 4)
|
||||||
|
except Exception as e:
|
||||||
|
raise SidHashError(symbol=symbol)
|
||||||
|
|
||||||
|
if asset_name is None:
|
||||||
|
asset_name = ' / '.join(symbol.split('_')).upper()
|
||||||
|
|
||||||
|
if start_date is None:
|
||||||
|
start_date = pd.Timestamp.utcnow()
|
||||||
|
|
||||||
|
if end_date is None:
|
||||||
|
end_date = pd.Timestamp.utcnow() + timedelta(days=365)
|
||||||
|
|
||||||
|
super().__init__(
|
||||||
|
sid,
|
||||||
|
exchange,
|
||||||
|
symbol=symbol,
|
||||||
|
asset_name=asset_name,
|
||||||
|
start_date=start_date,
|
||||||
|
end_date=end_date,
|
||||||
|
first_traded=first_traded,
|
||||||
|
auto_close_date=auto_close_date,
|
||||||
|
exchange_full=exchange_full,
|
||||||
|
)
|
||||||
|
|
||||||
|
self.leverage = leverage
|
||||||
|
|
||||||
|
def __repr__(self):
|
||||||
|
return 'Trading Pair {symbol}({sid}) Exchange: {exchange}, ' \
|
||||||
|
'Introduced On: {start_date}, ' \
|
||||||
|
'Market Currency: {market_currency}, ' \
|
||||||
|
'Base Currency: {base_currency}, ' \
|
||||||
|
'Exchange Leverage: {leverage}'.format(
|
||||||
|
symbol=self.symbol,
|
||||||
|
sid=self.sid,
|
||||||
|
exchange=self.exchange,
|
||||||
|
start_date=self.start_date,
|
||||||
|
market_currency=self.market_currency,
|
||||||
|
base_currency=self.base_currency,
|
||||||
|
leverage=self.leverage
|
||||||
|
)
|
||||||
|
|
||||||
|
cpdef __reduce__(self):
|
||||||
|
"""
|
||||||
|
Function used by pickle to determine how to serialize/deserialize this
|
||||||
|
class. Should return a tuple whose first element is self.__class__,
|
||||||
|
and whose second element is a tuple of all the attributes that should
|
||||||
|
be serialized/deserialized during pickling.
|
||||||
|
"""
|
||||||
|
return (self.__class__, (self.symbol,
|
||||||
|
self.exchange,
|
||||||
|
self.start_date,
|
||||||
|
self.asset_name,
|
||||||
|
self.sid,
|
||||||
|
self.leverage,
|
||||||
|
self.end_date,
|
||||||
|
self.first_traded,
|
||||||
|
self.auto_close_date,
|
||||||
|
self.exchange_full))
|
||||||
|
|
||||||
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)
|
||||||
|
|||||||
@@ -42,8 +42,7 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
@lazyval
|
@lazyval
|
||||||
def tar_url(self):
|
def tar_url(self):
|
||||||
return (
|
return (
|
||||||
'https://www.dropbox.com/s/9naqffawnq8o4r2/'
|
'https://s3.amazonaws.com/enigmaco/catalyst-bundles/poloniex/poloniex-bundle.tar.gz'
|
||||||
'poloniex-bundle.tar?dl=1'
|
|
||||||
)
|
)
|
||||||
|
|
||||||
@lazyval
|
@lazyval
|
||||||
@@ -103,7 +102,10 @@ class PoloniexBundle(BaseCryptoPricingBundle):
|
|||||||
)
|
)
|
||||||
raw.set_index('date', inplace=True)
|
raw.set_index('date', inplace=True)
|
||||||
|
|
||||||
scale = 1
|
# BcolzDailyBarReader introduces a 1/1000 factor in the way pricing is stored
|
||||||
|
# on disk, which we compensate here to get the right pricing amounts
|
||||||
|
# ref: data/us_equity_pricing.py
|
||||||
|
scale = 1000
|
||||||
raw.loc[:, 'open'] /= scale
|
raw.loc[:, 'open'] /= scale
|
||||||
raw.loc[:, 'high'] /= scale
|
raw.loc[:, 'high'] /= scale
|
||||||
raw.loc[:, 'low'] /= scale
|
raw.loc[:, 'low'] /= scale
|
||||||
@@ -164,4 +166,4 @@ register_bundle(PoloniexBundle, ['USDT_BTC',])
|
|||||||
For a production environment make sure to use (to bundle all pairs):
|
For a production environment make sure to use (to bundle all pairs):
|
||||||
register_bundle(PoloniexBundle)
|
register_bundle(PoloniexBundle)
|
||||||
'''
|
'''
|
||||||
register_bundle(PoloniexBundle)
|
register_bundle(PoloniexBundle, create_writers=False)
|
||||||
|
|||||||
+48
-15
@@ -96,16 +96,15 @@ def has_data_for_dates(series_or_df, first_date, last_date):
|
|||||||
first, last = dts[[0, -1]].tz_localize(None)
|
first, last = dts[[0, -1]].tz_localize(None)
|
||||||
return (first <= first_date.tz_localize(None)) and (last >= last_date.tz_localize(None))
|
return (first <= first_date.tz_localize(None)) and (last >= last_date.tz_localize(None))
|
||||||
|
|
||||||
def load_crypto_market_data(trading_day=None,
|
def load_crypto_market_data(trading_day=None, trading_days=None, bm_symbol='USDT_BTC',
|
||||||
trading_days=None,
|
bundle=None, bundle_data=None, environ=None):
|
||||||
bm_symbol='USDT_BTC',
|
|
||||||
environ=None):
|
|
||||||
if trading_day is None:
|
if trading_day is None:
|
||||||
trading_day = get_calendar('OPEN').trading_day
|
trading_day = get_calendar('OPEN').trading_day
|
||||||
if trading_days is None:
|
if trading_days is None:
|
||||||
trading_days = get_calendar('OPEN').all_sessions
|
trading_days = get_calendar('OPEN').all_sessions
|
||||||
|
|
||||||
first_date = trading_days[0]
|
first_date = trading_days[1]
|
||||||
now = pd.Timestamp.utcnow()
|
now = pd.Timestamp.utcnow()
|
||||||
|
|
||||||
# We expect to have benchmark and treasury data that's current up until
|
# We expect to have benchmark and treasury data that's current up until
|
||||||
@@ -122,6 +121,13 @@ def load_crypto_market_data(trading_day=None,
|
|||||||
|
|
||||||
# We'll attempt to download new data if the latest entry in our cache is
|
# We'll attempt to download new data if the latest entry in our cache is
|
||||||
# before this date.
|
# before this date.
|
||||||
|
if(bundle_data):
|
||||||
|
# If we are using the bundle to retrieve the cryptobenchmark, find the last
|
||||||
|
# date for which there is trading data in the bundle
|
||||||
|
asset = bundle_data.asset_finder.lookup_symbol(symbol=bm_symbol,as_of_date=None)
|
||||||
|
ix = bundle_data.daily_bar_reader._last_rows[asset.sid]
|
||||||
|
last_date = pd.to_datetime(bundle_data.daily_bar_reader._spot_col('day')[ix],unit='s')
|
||||||
|
else:
|
||||||
last_date = trading_days[trading_days.get_loc(now, method='ffill') - 2]
|
last_date = trading_days[trading_days.get_loc(now, method='ffill') - 2]
|
||||||
|
|
||||||
br = ensure_crypto_benchmark_data(
|
br = ensure_crypto_benchmark_data(
|
||||||
@@ -132,11 +138,13 @@ def load_crypto_market_data(trading_day=None,
|
|||||||
# We need the trading_day to figure out the close prior to the first
|
# We need the trading_day to figure out the close prior to the first
|
||||||
# date so that we can compute returns for the first date.
|
# date so that we can compute returns for the first date.
|
||||||
trading_day,
|
trading_day,
|
||||||
|
bundle,
|
||||||
|
bundle_data,
|
||||||
environ,
|
environ,
|
||||||
)
|
)
|
||||||
# Override first_date for treasury data since we have it for many more years
|
# Override first_date for treasury data since we have it for many more years
|
||||||
# and is independent of crypto data
|
# and is independent of crypto data
|
||||||
first_date_treasury = pd.Timestamp('1990-01-01', tz='UTC')
|
first_date_treasury = pd.Timestamp('1990-01-02', tz='UTC')
|
||||||
tc = ensure_treasury_data(
|
tc = ensure_treasury_data(
|
||||||
bm_symbol,
|
bm_symbol,
|
||||||
first_date_treasury,
|
first_date_treasury,
|
||||||
@@ -240,6 +248,8 @@ def ensure_crypto_benchmark_data(symbol,
|
|||||||
last_date,
|
last_date,
|
||||||
now,
|
now,
|
||||||
trading_day,
|
trading_day,
|
||||||
|
bundle,
|
||||||
|
bundle_data,
|
||||||
environ=None):
|
environ=None):
|
||||||
|
|
||||||
filename = get_benchmark_filename(symbol)
|
filename = get_benchmark_filename(symbol)
|
||||||
@@ -248,7 +258,7 @@ def ensure_crypto_benchmark_data(symbol,
|
|||||||
('Loading benchmark data for {symbol!r} '
|
('Loading benchmark data for {symbol!r} '
|
||||||
'from {first_date} to {last_date}'),
|
'from {first_date} to {last_date}'),
|
||||||
symbol=symbol,
|
symbol=symbol,
|
||||||
first_date=first_date - trading_day,
|
first_date=first_date,
|
||||||
last_date=last_date
|
last_date=last_date
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -261,19 +271,42 @@ def ensure_crypto_benchmark_data(symbol,
|
|||||||
environ,
|
environ,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
if data is not None:
|
if data is not None:
|
||||||
return data
|
return data
|
||||||
|
|
||||||
# If no cached data was found or it was missing any dates then download the
|
# If no cached data was found or it was missing any dates then download the
|
||||||
# necessary data.
|
# necessary data.
|
||||||
|
|
||||||
|
if(bundle == 'poloniex'):
|
||||||
|
'''
|
||||||
|
If we're using the Poloniex bundle, we'll get the benchmark from the bundle
|
||||||
|
instead of downloading it from Poloniex every time we need it.
|
||||||
|
Poloniex has a captcha for API queries originating from outside the US that
|
||||||
|
prevents users abroad from getting Catalyst to work
|
||||||
|
'''
|
||||||
logger.info(
|
logger.info(
|
||||||
('Downloading benchmark data for {symbol!r} '
|
('Retrieving benchmark data from bundle for {symbol!r} from {first_date} to {last_date}'),
|
||||||
'from {first_date} to {last_date}'),
|
symbol=symbol, first_date=first_date, last_date=last_date)
|
||||||
symbol=symbol,
|
|
||||||
first_date=first_date - trading_day,
|
asset = bundle_data.asset_finder.lookup_symbol(symbol=symbol,as_of_date=None)
|
||||||
last_date=last_date
|
fields = ['day', 'close']
|
||||||
)
|
raw = bundle_data.daily_bar_reader.load_raw_arrays(
|
||||||
|
columns=fields,
|
||||||
|
start_date=first_date - trading_day,
|
||||||
|
end_date=last_date,
|
||||||
|
assets=[asset,])
|
||||||
|
bench_raw = pd.concat([pd.DataFrame(raw[0], columns=['date']),pd.DataFrame(raw[1], columns=['close'])], axis=1)
|
||||||
|
bench_raw['date'] = pd.to_datetime(bench_raw['date'],unit='s')
|
||||||
|
bench_raw.set_index('date', inplace=True)
|
||||||
|
bench_raw.sort_index(inplace=True)
|
||||||
|
bench_raw = bench_raw[pd.to_datetime(first_date - trading_day):pd.to_datetime(last_date)]
|
||||||
|
|
||||||
|
else:
|
||||||
|
# This is how it used to be: downloading the benchmark everytime.
|
||||||
|
# Leaving this code here to be repurposed in the future for other bundles.
|
||||||
|
logger.info(
|
||||||
|
('Downloading benchmark data for {symbol!r} from {first_date} to {last_date}'),
|
||||||
|
symbol=symbol, first_date=first_date, last_date=last_date)
|
||||||
|
|
||||||
# Load benchmark symbol from Poloniex API
|
# Load benchmark symbol from Poloniex API
|
||||||
try:
|
try:
|
||||||
@@ -518,7 +551,7 @@ def _load_cached_data(filename, first_date, last_date, now, resource_name,
|
|||||||
)
|
)
|
||||||
|
|
||||||
logger.info(
|
logger.info(
|
||||||
"Cache at {path} does not have data from {start} to {end}.\n",
|
"Cache at {path} does not have data from {start} to {end}.",
|
||||||
start=first_date,
|
start=first_date,
|
||||||
end=last_date,
|
end=last_date,
|
||||||
path=path,
|
path=path,
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ from catalyst.api import (
|
|||||||
get_open_orders,
|
get_open_orders,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
def initialize(context):
|
||||||
context.ASSET_NAME = 'USDT_BTC'
|
context.ASSET_NAME = 'USDT_BTC'
|
||||||
context.TARGET_HODL_RATIO = 0.8
|
context.TARGET_HODL_RATIO = 0.8
|
||||||
@@ -42,8 +41,6 @@ def initialize(context):
|
|||||||
def handle_data(context, data):
|
def handle_data(context, data):
|
||||||
context.i += 1
|
context.i += 1
|
||||||
|
|
||||||
print 'i:', context.i
|
|
||||||
|
|
||||||
starting_cash = context.portfolio.starting_cash
|
starting_cash = context.portfolio.starting_cash
|
||||||
target_hodl_value = context.TARGET_HODL_RATIO * starting_cash
|
target_hodl_value = context.TARGET_HODL_RATIO * starting_cash
|
||||||
reserve_value = context.RESERVE_RATIO * starting_cash
|
reserve_value = context.RESERVE_RATIO * starting_cash
|
||||||
@@ -73,6 +70,7 @@ def handle_data(context, data):
|
|||||||
|
|
||||||
record(
|
record(
|
||||||
price=price,
|
price=price,
|
||||||
|
volume=data[context.asset].volume,
|
||||||
cash=cash,
|
cash=cash,
|
||||||
starting_cash=context.portfolio.starting_cash,
|
starting_cash=context.portfolio.starting_cash,
|
||||||
leverage=context.account.leverage,
|
leverage=context.account.leverage,
|
||||||
@@ -80,12 +78,13 @@ def handle_data(context, data):
|
|||||||
|
|
||||||
def analyze(context=None, results=None):
|
def analyze(context=None, results=None):
|
||||||
import matplotlib.pyplot as plt
|
import matplotlib.pyplot as plt
|
||||||
|
|
||||||
# Plot the portfolio and asset data.
|
# Plot the portfolio and asset data.
|
||||||
ax1 = plt.subplot(511)
|
ax1 = plt.subplot(611)
|
||||||
results[['portfolio_value']].plot(ax=ax1)
|
results[['portfolio_value']].plot(ax=ax1)
|
||||||
ax1.set_ylabel('Portfolio Value (USD)')
|
ax1.set_ylabel('Portfolio Value (USD)')
|
||||||
|
|
||||||
ax2 = plt.subplot(512, sharex=ax1)
|
ax2 = plt.subplot(612, sharex=ax1)
|
||||||
ax2.set_ylabel('{asset} (USD)'.format(asset=context.ASSET_NAME))
|
ax2.set_ylabel('{asset} (USD)'.format(asset=context.ASSET_NAME))
|
||||||
(context.TICK_SIZE * results[['price']]).plot(ax=ax2)
|
(context.TICK_SIZE * results[['price']]).plot(ax=ax2)
|
||||||
|
|
||||||
@@ -101,11 +100,11 @@ def analyze(context=None, results=None):
|
|||||||
color='g',
|
color='g',
|
||||||
)
|
)
|
||||||
|
|
||||||
ax3 = plt.subplot(513, sharex=ax1)
|
ax3 = plt.subplot(613, sharex=ax1)
|
||||||
results[['leverage', 'alpha', 'beta']].plot(ax=ax3)
|
results[['leverage', 'alpha', 'beta']].plot(ax=ax3)
|
||||||
ax3.set_ylabel('Leverage ')
|
ax3.set_ylabel('Leverage ')
|
||||||
|
|
||||||
ax4 = plt.subplot(514, sharex=ax1)
|
ax4 = plt.subplot(614, sharex=ax1)
|
||||||
results[['starting_cash', 'cash']].plot(ax=ax4)
|
results[['starting_cash', 'cash']].plot(ax=ax4)
|
||||||
ax4.set_ylabel('Cash (USD)')
|
ax4.set_ylabel('Cash (USD)')
|
||||||
|
|
||||||
@@ -119,7 +118,7 @@ def analyze(context=None, results=None):
|
|||||||
'benchmark_period_return',
|
'benchmark_period_return',
|
||||||
]]
|
]]
|
||||||
|
|
||||||
ax5 = plt.subplot(515, sharex=ax1)
|
ax5 = plt.subplot(615, sharex=ax1)
|
||||||
results[[
|
results[[
|
||||||
'treasury',
|
'treasury',
|
||||||
'algorithm',
|
'algorithm',
|
||||||
@@ -127,6 +126,10 @@ def analyze(context=None, results=None):
|
|||||||
]].plot(ax=ax5)
|
]].plot(ax=ax5)
|
||||||
ax5.set_ylabel('Percent Change')
|
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)
|
plt.legend(loc=3)
|
||||||
|
|
||||||
# Show the plot.
|
# Show the plot.
|
||||||
|
|||||||
@@ -1,79 +0,0 @@
|
|||||||
from catalyst.utils.run_algo import run_algorithm
|
|
||||||
from datetime import datetime
|
|
||||||
import pytz
|
|
||||||
|
|
||||||
from catalyst.api import (
|
|
||||||
order_target_value,
|
|
||||||
symbol,
|
|
||||||
record,
|
|
||||||
cancel_order,
|
|
||||||
get_open_orders,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def initialize(context):
|
|
||||||
context.ASSET_NAME = 'USDT_BTC'
|
|
||||||
context.TARGET_HODL_RATIO = 0.8
|
|
||||||
context.RESERVE_RATIO = 1.0 - context.TARGET_HODL_RATIO
|
|
||||||
|
|
||||||
# For all trading pairs in the poloniex bundle, the default denomination
|
|
||||||
# currently supported by Catalyst is 1/1000th of a full coin. Use this
|
|
||||||
# constant to scale the price of up to that of a full coin if desired.
|
|
||||||
context.TICK_SIZE = 1000.0
|
|
||||||
|
|
||||||
context.is_buying = True
|
|
||||||
context.asset = symbol(context.ASSET_NAME)
|
|
||||||
|
|
||||||
context.i = 0
|
|
||||||
|
|
||||||
|
|
||||||
def handle_data(context, data):
|
|
||||||
context.i += 1
|
|
||||||
|
|
||||||
print 'i:', context.i
|
|
||||||
|
|
||||||
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[context.asset].price
|
|
||||||
|
|
||||||
# Check if still buying and could (approximately) afford another purchase
|
|
||||||
if context.is_buying and cash > price:
|
|
||||||
# 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,
|
|
||||||
stop_price=price * 0.9,
|
|
||||||
)
|
|
||||||
|
|
||||||
record(
|
|
||||||
price=price,
|
|
||||||
cash=cash,
|
|
||||||
starting_cash=context.portfolio.starting_cash,
|
|
||||||
leverage=context.account.leverage,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
start = datetime(2015, 3, 1, 0, 0, 0, 0, pytz.utc)
|
|
||||||
end = datetime(2017, 6, 28, 0, 0, 0, 0, pytz.utc)
|
|
||||||
run_algorithm(
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=handle_data,
|
|
||||||
start=start,
|
|
||||||
end=end,
|
|
||||||
capital_base=100000,
|
|
||||||
bundle='poloniex'
|
|
||||||
)
|
|
||||||
@@ -1,3 +1,14 @@
|
|||||||
|
'''
|
||||||
|
This algorithm requires an additional library (ta-lib) beyond those required by catalyst.
|
||||||
|
Install it first by running:
|
||||||
|
$ pip install TA-Lib
|
||||||
|
|
||||||
|
If you get build errors like "fatal error: ta-lib/ta_libc.h: No such file or directory"
|
||||||
|
it typically means that it can't find the underlying TA-Lib library and needs to be installed.
|
||||||
|
See https://mrjbq7.github.io/ta-lib/install.html for instructions on how to install
|
||||||
|
the required dependencies.
|
||||||
|
'''
|
||||||
|
|
||||||
import talib
|
import talib
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
@@ -8,9 +19,9 @@ from catalyst.api import (
|
|||||||
record,
|
record,
|
||||||
get_open_orders,
|
get_open_orders,
|
||||||
)
|
)
|
||||||
from catalyst.utils.run_algo import run_algorithm
|
from catalyst.exchange.stats_utils import get_pretty_stats
|
||||||
|
|
||||||
algo_namespace = 'buy_the_dip_live'
|
algo_namespace = 'buy_low_sell_high_xrp'
|
||||||
log = Logger(algo_namespace)
|
log = Logger(algo_namespace)
|
||||||
|
|
||||||
|
|
||||||
@@ -21,7 +32,7 @@ def initialize(context):
|
|||||||
|
|
||||||
context.TARGET_POSITIONS = 5000
|
context.TARGET_POSITIONS = 5000
|
||||||
context.PROFIT_TARGET = 0.1
|
context.PROFIT_TARGET = 0.1
|
||||||
context.SLIPPAGE_ALLOWED = 0.02
|
context.SLIPPAGE_ALLOWED = 0.05
|
||||||
|
|
||||||
context.retry_check_open_orders = 10
|
context.retry_check_open_orders = 10
|
||||||
context.retry_update_portfolio = 10
|
context.retry_update_portfolio = 10
|
||||||
@@ -140,16 +151,5 @@ def handle_data(context, data):
|
|||||||
|
|
||||||
|
|
||||||
def analyze(context, stats):
|
def analyze(context, stats):
|
||||||
log.info('the full stats:\n{}'.format(stats.head()))
|
log.info('the daily stats:\n{}'.format(get_pretty_stats(stats)))
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
run_algorithm(
|
|
||||||
initialize=initialize,
|
|
||||||
handle_data=handle_data,
|
|
||||||
analyze=analyze,
|
|
||||||
exchange_name='bitfinex',
|
|
||||||
live=True,
|
|
||||||
algo_namespace=algo_namespace,
|
|
||||||
base_currency='usd'
|
|
||||||
)
|
|
||||||
@@ -18,6 +18,7 @@ from datetime import timedelta
|
|||||||
from time import sleep
|
from time import sleep
|
||||||
from os import listdir
|
from os import listdir
|
||||||
from os.path import isfile, join
|
from os.path import isfile, join
|
||||||
|
from collections import deque
|
||||||
|
|
||||||
import logbook
|
import logbook
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -35,6 +36,7 @@ from catalyst.exchange.exchange_errors import (
|
|||||||
)
|
)
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_minute_writer_root, \
|
from catalyst.exchange.exchange_utils import get_exchange_minute_writer_root, \
|
||||||
save_algo_object, get_algo_object, get_algo_folder
|
save_algo_object, get_algo_object, get_algo_folder
|
||||||
|
from catalyst.exchange.stats_utils import get_pretty_stats
|
||||||
from catalyst.finance.performance.period import calc_period_stats
|
from catalyst.finance.performance.period import calc_period_stats
|
||||||
from catalyst.gens.tradesimulation import AlgorithmSimulator
|
from catalyst.gens.tradesimulation import AlgorithmSimulator
|
||||||
from catalyst.utils.api_support import (
|
from catalyst.utils.api_support import (
|
||||||
@@ -55,16 +57,19 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
|
|||||||
self.exchange = kwargs.pop('exchange', None)
|
self.exchange = kwargs.pop('exchange', None)
|
||||||
self.algo_namespace = kwargs.pop('algo_namespace', None)
|
self.algo_namespace = kwargs.pop('algo_namespace', None)
|
||||||
self.orders = {}
|
self.orders = {}
|
||||||
|
self.minute_stats = deque(maxlen=60)
|
||||||
self.is_running = True
|
self.is_running = True
|
||||||
|
|
||||||
self.retry_check_open_orders = 5
|
self.retry_check_open_orders = 5
|
||||||
self.retry_update_portfolio = 5
|
self.retry_synchronize_portfolio = 5
|
||||||
self.retry_get_open_orders = 5
|
self.retry_get_open_orders = 5
|
||||||
self.retry_order = 2
|
self.retry_order = 2
|
||||||
self.retry_delay = 5
|
self.retry_delay = 5
|
||||||
|
|
||||||
|
self.stats_minutes = 5
|
||||||
|
|
||||||
super(self.__class__, self).__init__(*args, **kwargs)
|
super(self.__class__, self).__init__(*args, **kwargs)
|
||||||
self._create_minute_writer()
|
# self._create_minute_writer()
|
||||||
|
|
||||||
signal.signal(signal.SIGINT, self.signal_handler)
|
signal.signal(signal.SIGINT, self.signal_handler)
|
||||||
|
|
||||||
@@ -93,10 +98,14 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
|
|||||||
def signal_handler(self, signal, frame):
|
def signal_handler(self, signal, frame):
|
||||||
self.is_running = False
|
self.is_running = False
|
||||||
|
|
||||||
log.info('You pressed Ctrl+C!')
|
if self._analyze is None:
|
||||||
|
log.info('Interruption signal detected {}, exiting the '
|
||||||
|
'algorithm'.format(signal))
|
||||||
|
|
||||||
|
else:
|
||||||
|
log.info('Interruption signal detected {}, calling `analyze()` '
|
||||||
|
'before exiting the algorithm'.format(signal))
|
||||||
|
|
||||||
stats = None
|
|
||||||
try:
|
|
||||||
algo_folder = get_algo_folder(self.algo_namespace)
|
algo_folder = get_algo_folder(self.algo_namespace)
|
||||||
folder = join(algo_folder, 'daily_perf')
|
folder = join(algo_folder, 'daily_perf')
|
||||||
files = [f for f in listdir(folder) if isfile(join(folder, f))]
|
files = [f for f in listdir(folder) if isfile(join(folder, f))]
|
||||||
@@ -108,12 +117,9 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
|
|||||||
daily_perf_list.append(pickle.load(handle))
|
daily_perf_list.append(pickle.load(handle))
|
||||||
|
|
||||||
stats = pd.DataFrame(daily_perf_list)
|
stats = pd.DataFrame(daily_perf_list)
|
||||||
stats.set_index('period_close', drop=True, inplace=True)
|
|
||||||
|
|
||||||
except Exception as e:
|
|
||||||
log.warn('Unable to compute daily stats: {}'.format(e))
|
|
||||||
|
|
||||||
self.analyze(stats)
|
self.analyze(stats)
|
||||||
|
|
||||||
sys.exit(0)
|
sys.exit(0)
|
||||||
|
|
||||||
def _create_clock(self):
|
def _create_clock(self):
|
||||||
@@ -169,9 +175,9 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
|
|||||||
def updated_account(self):
|
def updated_account(self):
|
||||||
return self.exchange.account
|
return self.exchange.account
|
||||||
|
|
||||||
def _update_portfolio(self, attempt_index=0):
|
def _synchronize_portfolio(self, attempt_index=0):
|
||||||
try:
|
try:
|
||||||
self.exchange.update_portfolio()
|
self.exchange.synchronize_portfolio()
|
||||||
|
|
||||||
# Applying the updated last_sales_price to the positions
|
# Applying the updated last_sales_price to the positions
|
||||||
# in the performance tracker. This seems a bit redundant
|
# in the performance tracker. This seems a bit redundant
|
||||||
@@ -189,9 +195,9 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
|
|||||||
log.warn(
|
log.warn(
|
||||||
'update portfolio attempt {}: {}'.format(attempt_index, e)
|
'update portfolio attempt {}: {}'.format(attempt_index, e)
|
||||||
)
|
)
|
||||||
if attempt_index < self.retry_update_portfolio:
|
if attempt_index < self.retry_synchronize_portfolio:
|
||||||
sleep(self.retry_delay)
|
sleep(self.retry_delay)
|
||||||
self._update_portfolio(attempt_index + 1)
|
self._synchronize_portfolio(attempt_index + 1)
|
||||||
else:
|
else:
|
||||||
raise ExchangePortfolioDataError(
|
raise ExchangePortfolioDataError(
|
||||||
data_type='update-portfolio',
|
data_type='update-portfolio',
|
||||||
@@ -287,7 +293,7 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
|
|||||||
if not self.is_running:
|
if not self.is_running:
|
||||||
return
|
return
|
||||||
|
|
||||||
self._update_portfolio()
|
self._synchronize_portfolio()
|
||||||
|
|
||||||
transactions = self._check_open_orders()
|
transactions = self._check_open_orders()
|
||||||
for transaction in transactions:
|
for transaction in transactions:
|
||||||
@@ -306,10 +312,17 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
|
|||||||
# Performance tracker and keep only minute and cumulative
|
# Performance tracker and keep only minute and cumulative
|
||||||
self.perf_tracker.update_performance()
|
self.perf_tracker.update_performance()
|
||||||
|
|
||||||
# TODO: save for future use?
|
|
||||||
minute_stats = self.prepare_period_stats(
|
minute_stats = self.prepare_period_stats(
|
||||||
data.current_dt, data.current_dt + timedelta(minutes=1))
|
data.current_dt, data.current_dt + timedelta(minutes=1))
|
||||||
log.debug('the minute performance:\n{}'.format(minute_stats))
|
# Saving the last hour in memory
|
||||||
|
self.minute_stats.append(minute_stats)
|
||||||
|
|
||||||
|
print_df = pd.DataFrame(list(self.minute_stats))
|
||||||
|
log.debug(
|
||||||
|
'statistics for the last {stats_minutes} minutes:\n{stats}'.format(
|
||||||
|
stats_minutes=self.stats_minutes,
|
||||||
|
stats=get_pretty_stats(print_df, self.stats_minutes)
|
||||||
|
))
|
||||||
|
|
||||||
today = pd.to_datetime('today', utc=True)
|
today = pd.to_datetime('today', utc=True)
|
||||||
daily_stats = self.prepare_period_stats(
|
daily_stats = self.prepare_period_stats(
|
||||||
@@ -384,9 +397,11 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
|
|||||||
style)
|
style)
|
||||||
|
|
||||||
order_id = self._order(asset, amount, limit_price, stop_price, style)
|
order_id = self._order(asset, amount, limit_price, stop_price, style)
|
||||||
order = self.portfolio.open_orders[order_id]
|
|
||||||
|
|
||||||
|
if order_id is not None:
|
||||||
|
order = self.portfolio.open_orders[order_id]
|
||||||
self.perf_tracker.process_order(order)
|
self.perf_tracker.process_order(order)
|
||||||
|
|
||||||
return order
|
return order
|
||||||
|
|
||||||
def round_order(self, amount):
|
def round_order(self, amount):
|
||||||
|
|||||||
@@ -81,7 +81,7 @@ class AssetFinderExchange(object):
|
|||||||
there are multiple candidates for the given ``symbol`` on the
|
there are multiple candidates for the given ``symbol`` on the
|
||||||
``as_of_date``.
|
``as_of_date``.
|
||||||
"""
|
"""
|
||||||
log.info('looking up symbol: {}'.format(symbol))
|
log.debug('looking up symbol: {}'.format(symbol))
|
||||||
|
|
||||||
if symbol in self._asset_cache:
|
if symbol in self._asset_cache:
|
||||||
return self._asset_cache[symbol]
|
return self._asset_cache[symbol]
|
||||||
|
|||||||
@@ -1,28 +1,26 @@
|
|||||||
import base64
|
import base64
|
||||||
import numpy as np
|
|
||||||
import hashlib
|
import hashlib
|
||||||
import hmac
|
import hmac
|
||||||
import json
|
import json
|
||||||
import re
|
import re
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
import pytz
|
import pytz
|
||||||
import requests
|
import requests
|
||||||
import six
|
import six
|
||||||
from catalyst.assets._assets import Asset
|
from catalyst.assets._assets import TradingPair
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
# from websocket import create_connection
|
# from websocket import create_connection
|
||||||
from catalyst.exchange.exchange import Exchange
|
from catalyst.exchange.exchange import Exchange
|
||||||
from catalyst.exchange.exchange_errors import (
|
from catalyst.exchange.exchange_errors import (
|
||||||
ExchangeRequestError,
|
ExchangeRequestError,
|
||||||
InvalidHistoryFrequencyError
|
InvalidHistoryFrequencyError,
|
||||||
)
|
InvalidOrderStyle, OrderCancelError)
|
||||||
from catalyst.finance.execution import (MarketOrder,
|
from catalyst.exchange.exchange_execution import ExchangeLimitOrder, \
|
||||||
LimitOrder,
|
ExchangeStopLimitOrder, ExchangeStopOrder
|
||||||
StopOrder,
|
|
||||||
StopLimitOrder)
|
|
||||||
from catalyst.finance.order import Order, ORDER_STATUS
|
from catalyst.finance.order import Order, ORDER_STATUS
|
||||||
from catalyst.protocol import Account
|
from catalyst.protocol import Account
|
||||||
|
|
||||||
@@ -40,8 +38,7 @@ class Bitfinex(Exchange):
|
|||||||
def __init__(self, key, secret, base_currency, portfolio=None):
|
def __init__(self, key, secret, base_currency, portfolio=None):
|
||||||
self.url = BITFINEX_URL
|
self.url = BITFINEX_URL
|
||||||
self.key = key
|
self.key = key
|
||||||
self.secret = secret
|
self.secret = secret.encode('UTF-8')
|
||||||
self.id = 'b'
|
|
||||||
self.name = 'bitfinex'
|
self.name = 'bitfinex'
|
||||||
self.assets = {}
|
self.assets = {}
|
||||||
self.load_assets()
|
self.load_assets()
|
||||||
@@ -135,7 +132,10 @@ class Bitfinex(Exchange):
|
|||||||
|
|
||||||
amount = float(order_status['original_amount'])
|
amount = float(order_status['original_amount'])
|
||||||
filled = float(order_status['executed_amount'])
|
filled = float(order_status['executed_amount'])
|
||||||
is_buy = (amount > 0)
|
|
||||||
|
if order_status['side'] == 'sell':
|
||||||
|
amount = -amount
|
||||||
|
filled = -filled
|
||||||
|
|
||||||
price = float(order_status['price'])
|
price = float(order_status['price'])
|
||||||
order_type = order_status['type']
|
order_type = order_status['type']
|
||||||
@@ -154,7 +154,6 @@ class Bitfinex(Exchange):
|
|||||||
# TODO: bitfinex does not specify comission. I could calculate it but not sure if it's worth it.
|
# TODO: bitfinex does not specify comission. I could calculate it but not sure if it's worth it.
|
||||||
commission = None
|
commission = None
|
||||||
|
|
||||||
# TODO: zipline likes rounded dates to match statistics, is this ok?
|
|
||||||
date = pd.Timestamp.utcfromtimestamp(float(order_status['timestamp']))
|
date = pd.Timestamp.utcfromtimestamp(float(order_status['timestamp']))
|
||||||
date = pytz.utc.localize(date)
|
date = pytz.utc.localize(date)
|
||||||
order = Order(
|
order = Order(
|
||||||
@@ -164,20 +163,15 @@ class Bitfinex(Exchange):
|
|||||||
stop=stop_price,
|
stop=stop_price,
|
||||||
limit=limit_price,
|
limit=limit_price,
|
||||||
filled=filled,
|
filled=filled,
|
||||||
id=order_status['id'],
|
id=str(order_status['id']),
|
||||||
commission=commission
|
commission=commission
|
||||||
)
|
)
|
||||||
order.status = status
|
order.status = status
|
||||||
|
|
||||||
return order, executed_price
|
return order, executed_price
|
||||||
|
|
||||||
def update_portfolio(self):
|
def get_balances(self):
|
||||||
"""
|
log.debug('retrieving wallets balances')
|
||||||
Update the portfolio cash and position balances based on the
|
|
||||||
latest ticker prices.
|
|
||||||
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
try:
|
try:
|
||||||
response = self._request('balances', None)
|
response = self._request('balances', None)
|
||||||
balances = response.json()
|
balances = response.json()
|
||||||
@@ -189,56 +183,12 @@ class Bitfinex(Exchange):
|
|||||||
error='unable to fetch balance {}'.format(balances['message'])
|
error='unable to fetch balance {}'.format(balances['message'])
|
||||||
)
|
)
|
||||||
|
|
||||||
base_position = None
|
std_balances = dict()
|
||||||
for position in balances:
|
for balance in balances:
|
||||||
if not base_position and position['type'] == 'exchange' \
|
currency = balance['currency'].lower()
|
||||||
and position['currency'] == self.base_currency:
|
std_balances[currency] = float(balance['available'])
|
||||||
base_position = position
|
|
||||||
|
|
||||||
if position is None:
|
return std_balances
|
||||||
raise ValueError(
|
|
||||||
error='Base currency %s not found in portfolio' % self.base_currency
|
|
||||||
)
|
|
||||||
|
|
||||||
portfolio = self._portfolio
|
|
||||||
portfolio.cash = float(base_position['available'])
|
|
||||||
if portfolio.starting_cash is None:
|
|
||||||
portfolio.starting_cash = portfolio.cash
|
|
||||||
|
|
||||||
if portfolio.positions:
|
|
||||||
assets = portfolio.positions.keys()
|
|
||||||
tickers = self.tickers(assets)
|
|
||||||
portfolio.positions_value = 0.0
|
|
||||||
for ticker in tickers:
|
|
||||||
# TODO: convert if the position is not in the base currency
|
|
||||||
position = portfolio.positions[ticker['asset']]
|
|
||||||
position.last_sale_price = ticker['last_price']
|
|
||||||
position.last_sale_date = ticker['timestamp']
|
|
||||||
|
|
||||||
portfolio.positions_value += \
|
|
||||||
position.amount * position.last_sale_price
|
|
||||||
portfolio.portfolio_value = \
|
|
||||||
portfolio.positions_value + portfolio.cash
|
|
||||||
|
|
||||||
@property
|
|
||||||
def portfolio(self):
|
|
||||||
"""
|
|
||||||
Return the Portfolio
|
|
||||||
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
# if self._portfolio is None:
|
|
||||||
# portfolio = ExchangePortfolio(
|
|
||||||
# start_date=pd.Timestamp.utcnow()
|
|
||||||
# )
|
|
||||||
# self.store.portfolio = portfolio
|
|
||||||
# self.update_portfolio()
|
|
||||||
#
|
|
||||||
# portfolio.starting_cash = portfolio.cash
|
|
||||||
# else:
|
|
||||||
# portfolio = self.store.portfolio
|
|
||||||
|
|
||||||
return self._portfolio
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def account(self):
|
def account(self):
|
||||||
@@ -264,17 +214,14 @@ class Bitfinex(Exchange):
|
|||||||
|
|
||||||
return account
|
return account
|
||||||
|
|
||||||
@property
|
|
||||||
def positions(self):
|
|
||||||
return self.portfolio.positions
|
|
||||||
|
|
||||||
@property
|
@property
|
||||||
def time_skew(self):
|
def time_skew(self):
|
||||||
# TODO: research the time skew conditions
|
# TODO: research the time skew conditions
|
||||||
return pd.Timedelta('0s')
|
return pd.Timedelta('0s')
|
||||||
|
|
||||||
def subscribe_to_market_data(self, symbol):
|
def get_account(self):
|
||||||
pass
|
# TODO: fetch account data and keep in cache
|
||||||
|
return None
|
||||||
|
|
||||||
def get_candles(self, data_frequency, assets, bar_count=None):
|
def get_candles(self, data_frequency, assets, bar_count=None):
|
||||||
"""
|
"""
|
||||||
@@ -320,8 +267,8 @@ class Bitfinex(Exchange):
|
|||||||
)
|
)
|
||||||
|
|
||||||
# Making sure that assets are iterable
|
# Making sure that assets are iterable
|
||||||
asset_list = [assets] if isinstance(assets, Asset) else assets
|
asset_list = [assets] if isinstance(assets, TradingPair) else assets
|
||||||
ohlc_list = dict()
|
ohlc_map = dict()
|
||||||
for asset in asset_list:
|
for asset in asset_list:
|
||||||
symbol = self._get_v2_symbol(asset)
|
symbol = self._get_v2_symbol(asset)
|
||||||
url = '{url}/v2/candles/trade:{frequency}:{symbol}'.format(
|
url = '{url}/v2/candles/trade:{frequency}:{symbol}'.format(
|
||||||
@@ -359,8 +306,7 @@ class Bitfinex(Exchange):
|
|||||||
volume=np.float64(candle[5]),
|
volume=np.float64(candle[5]),
|
||||||
price=np.float64(candle[2]),
|
price=np.float64(candle[2]),
|
||||||
last_traded=pd.Timestamp.utcfromtimestamp(
|
last_traded=pd.Timestamp.utcfromtimestamp(
|
||||||
candle[0] / 1000.0),
|
candle[0] / 1000.0)
|
||||||
minute_dt=pd.Timestamp.utcnow().floor('1 min')
|
|
||||||
)
|
)
|
||||||
return ohlc
|
return ohlc
|
||||||
|
|
||||||
@@ -371,102 +317,43 @@ class Bitfinex(Exchange):
|
|||||||
ohlc = ohlc_from_candle(candle)
|
ohlc = ohlc_from_candle(candle)
|
||||||
ohlc_bars.append(ohlc)
|
ohlc_bars.append(ohlc)
|
||||||
|
|
||||||
ohlc_list[asset] = ohlc_bars
|
ohlc_map[asset] = ohlc_bars
|
||||||
|
|
||||||
else:
|
else:
|
||||||
ohlc = ohlc_from_candle(candles)
|
ohlc = ohlc_from_candle(candles)
|
||||||
ohlc_list[asset] = ohlc
|
ohlc_map[asset] = ohlc
|
||||||
|
|
||||||
return ohlc_list[assets] \
|
return ohlc_map[assets] \
|
||||||
if isinstance(assets, Asset) else ohlc_list
|
if isinstance(assets, TradingPair) else ohlc_map
|
||||||
|
|
||||||
def order(self, asset, amount, limit_price, stop_price, style):
|
def create_order(self, asset, amount, is_buy, style):
|
||||||
"""Place an order.
|
|
||||||
|
|
||||||
Parameters
|
|
||||||
----------
|
|
||||||
asset : Asset
|
|
||||||
The asset that this order is for.
|
|
||||||
amount : int
|
|
||||||
The amount of shares to order. If ``amount`` is positive, this is
|
|
||||||
the number of shares to buy or cover. If ``amount`` is negative,
|
|
||||||
this is the number of shares to sell or short.
|
|
||||||
limit_price : float, optional
|
|
||||||
The limit price for the order.
|
|
||||||
stop_price : float, optional
|
|
||||||
The stop price for the order.
|
|
||||||
style : ExecutionStyle, optional
|
|
||||||
The execution style for the order.
|
|
||||||
|
|
||||||
Returns
|
|
||||||
-------
|
|
||||||
order_id : str or None
|
|
||||||
The unique identifier for this order, or None if no order was
|
|
||||||
placed.
|
|
||||||
|
|
||||||
Notes
|
|
||||||
-----
|
|
||||||
The ``limit_price`` and ``stop_price`` arguments provide shorthands for
|
|
||||||
passing common execution styles. Passing ``limit_price=N`` is
|
|
||||||
equivalent to ``style=LimitOrder(N)``. Similarly, passing
|
|
||||||
``stop_price=M`` is equivalent to ``style=StopOrder(M)``, and passing
|
|
||||||
``limit_price=N`` and ``stop_price=M`` is equivalent to
|
|
||||||
``style=StopLimitOrder(N, M)``. It is an error to pass both a ``style``
|
|
||||||
and ``limit_price`` or ``stop_price``.
|
|
||||||
|
|
||||||
Bitfinex Order Types
|
|
||||||
--------------------
|
|
||||||
LIMIT, MARKET, STOP, TRAILING STOP,
|
|
||||||
EXCHANGE MARKET, EXCHANGE LIMIT, EXCHANGE STOP,
|
|
||||||
EXCHANGE TRAILING STOP, FOK, EXCHANGE FOK.
|
|
||||||
|
|
||||||
See Also
|
|
||||||
--------
|
|
||||||
:class:`catalyst.finance.execution.ExecutionStyle`
|
|
||||||
:func:`catalyst.api.order_value`
|
|
||||||
:func:`catalyst.api.order_percent`
|
|
||||||
"""
|
"""
|
||||||
if amount == 0:
|
Creating order on the exchange.
|
||||||
log.warn('skipping order amount of 0')
|
|
||||||
return None
|
|
||||||
|
|
||||||
base_currency = asset.symbol.split('_')[1]
|
|
||||||
if base_currency.lower() != self.base_currency.lower():
|
|
||||||
raise NotImplementedError(
|
|
||||||
'Currency pairs must share their base with the exchange.'
|
|
||||||
)
|
|
||||||
|
|
||||||
is_buy = (amount > 0)
|
|
||||||
|
|
||||||
if isinstance(style, MarketOrder):
|
|
||||||
order_type = 'market'
|
|
||||||
elif isinstance(style, LimitOrder):
|
|
||||||
order_type = 'limit'
|
|
||||||
price = limit_price
|
|
||||||
elif isinstance(style, StopOrder):
|
|
||||||
order_type = 'stop'
|
|
||||||
price = stop_price
|
|
||||||
elif isinstance(style, StopLimitOrder):
|
|
||||||
log.warn('using limit order instead of stop/limit')
|
|
||||||
# TODO: Not sure how to do this with the api. Investigate.
|
|
||||||
order_type = 'limit'
|
|
||||||
price = limit_price
|
|
||||||
else:
|
|
||||||
raise NotImplementedError('%s orders not available' % style)
|
|
||||||
|
|
||||||
log.debug(
|
|
||||||
'ordering {amount} {symbol} for {price}'.format(
|
|
||||||
amount=amount,
|
|
||||||
symbol=asset.symbol,
|
|
||||||
price=price
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
|
:param asset:
|
||||||
|
:param amount:
|
||||||
|
:param is_buy:
|
||||||
|
:param style:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
exchange_symbol = self.get_symbol(asset)
|
exchange_symbol = self.get_symbol(asset)
|
||||||
|
if isinstance(style, ExchangeLimitOrder) \
|
||||||
|
or isinstance(style, ExchangeStopLimitOrder):
|
||||||
|
price = style.get_limit_price(is_buy)
|
||||||
|
order_type = 'limit'
|
||||||
|
|
||||||
|
elif isinstance(style, ExchangeStopOrder):
|
||||||
|
price = style.get_stop_price(is_buy)
|
||||||
|
order_type = 'stop'
|
||||||
|
|
||||||
|
else:
|
||||||
|
raise InvalidOrderStyle(exchange=self.name,
|
||||||
|
style=style.__class__.__name__)
|
||||||
|
|
||||||
req = dict(
|
req = dict(
|
||||||
symbol=exchange_symbol,
|
symbol=exchange_symbol,
|
||||||
amount=str(float(abs(amount))),
|
amount=str(float(abs(amount))),
|
||||||
price=str(float(price)),
|
price="{:.20f}".format(float(price)),
|
||||||
side='buy' if is_buy else 'sell',
|
side='buy' if is_buy else 'sell',
|
||||||
type='exchange ' + order_type, # TODO: support margin trades
|
type='exchange ' + order_type, # TODO: support margin trades
|
||||||
exchange=self.name,
|
exchange=self.name,
|
||||||
@@ -481,17 +368,17 @@ class Bitfinex(Exchange):
|
|||||||
date = pd.Timestamp.utcnow()
|
date = pd.Timestamp.utcnow()
|
||||||
try:
|
try:
|
||||||
response = self._request('order/new', req)
|
response = self._request('order/new', req)
|
||||||
exchange_order = response.json()
|
order_status = response.json()
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise ExchangeRequestError(error=e)
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
if 'message' in exchange_order:
|
if 'message' in order_status:
|
||||||
raise ExchangeRequestError(
|
raise ExchangeRequestError(
|
||||||
error='unable to create Bitfinex order {}'.format(
|
error='unable to create Bitfinex order {}'.format(
|
||||||
exchange_order['message'])
|
order_status['message'])
|
||||||
)
|
)
|
||||||
|
|
||||||
order_id = exchange_order['id']
|
order_id = str(order_status['id'])
|
||||||
order = Order(
|
order = Order(
|
||||||
dt=date,
|
dt=date,
|
||||||
asset=asset,
|
asset=asset,
|
||||||
@@ -500,12 +387,8 @@ class Bitfinex(Exchange):
|
|||||||
limit=style.get_limit_price(is_buy),
|
limit=style.get_limit_price(is_buy),
|
||||||
id=order_id
|
id=order_id
|
||||||
)
|
)
|
||||||
# TODO: is this required?
|
|
||||||
order.broker_order_id = order_id
|
|
||||||
|
|
||||||
self.portfolio.create_order(order)
|
return order
|
||||||
|
|
||||||
return order_id
|
|
||||||
|
|
||||||
def get_open_orders(self, asset=None):
|
def get_open_orders(self, asset=None):
|
||||||
"""Retrieve all of the current open orders.
|
"""Retrieve all of the current open orders.
|
||||||
@@ -538,7 +421,7 @@ class Bitfinex(Exchange):
|
|||||||
|
|
||||||
orders = list()
|
orders = list()
|
||||||
for order_status in order_statuses:
|
for order_status in order_statuses:
|
||||||
order, = self._create_order(order_status)
|
order, executed_price = self._create_order(order_status)
|
||||||
if asset is None or asset == order.sid:
|
if asset is None or asset == order.sid:
|
||||||
orders.append(order)
|
orders.append(order)
|
||||||
|
|
||||||
@@ -590,9 +473,10 @@ class Bitfinex(Exchange):
|
|||||||
raise ExchangeRequestError(error=e)
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
if 'message' in status:
|
if 'message' in status:
|
||||||
raise ExchangeRequestError(
|
raise OrderCancelError(
|
||||||
error='Unable to cancel order: {} {}'.format(
|
order_id=order_id,
|
||||||
order_id, status['message'])
|
exchange=self.name,
|
||||||
|
error=status['message']
|
||||||
)
|
)
|
||||||
|
|
||||||
def tickers(self, assets):
|
def tickers(self, assets):
|
||||||
@@ -624,15 +508,14 @@ class Bitfinex(Exchange):
|
|||||||
|
|
||||||
tickers = response.json()
|
tickers = response.json()
|
||||||
|
|
||||||
formatted_tickers = []
|
ticks = dict()
|
||||||
for index, ticker in enumerate(tickers):
|
for index, ticker in enumerate(tickers):
|
||||||
if not len(ticker) == 11:
|
if not len(ticker) == 11:
|
||||||
raise ExchangeRequestError(
|
raise ExchangeRequestError(
|
||||||
error='Invalid ticker in response: {}'.format(ticker)
|
error='Invalid ticker in response: {}'.format(ticker)
|
||||||
)
|
)
|
||||||
|
|
||||||
tick = dict(
|
ticks[assets[index]] = dict(
|
||||||
asset=assets[index],
|
|
||||||
timestamp=pd.Timestamp.utcnow(),
|
timestamp=pd.Timestamp.utcnow(),
|
||||||
bid=ticker[1],
|
bid=ticker[1],
|
||||||
ask=ticker[3],
|
ask=ticker[3],
|
||||||
@@ -641,7 +524,6 @@ class Bitfinex(Exchange):
|
|||||||
high=ticker[9],
|
high=ticker[9],
|
||||||
volume=ticker[8],
|
volume=ticker[8],
|
||||||
)
|
)
|
||||||
formatted_tickers.append(tick)
|
|
||||||
|
|
||||||
log.debug('got tickers {}'.format(formatted_tickers))
|
log.debug('got tickers {}'.format(ticks))
|
||||||
return formatted_tickers
|
return ticks
|
||||||
|
|||||||
@@ -3,6 +3,10 @@
|
|||||||
"symbol": "btc_usd",
|
"symbol": "btc_usd",
|
||||||
"start_date": "2010-01-01"
|
"start_date": "2010-01-01"
|
||||||
},
|
},
|
||||||
|
"bchusd": {
|
||||||
|
"symbol": "bch_usd",
|
||||||
|
"start_date": "2010-01-01"
|
||||||
|
},
|
||||||
"ltcusd": {
|
"ltcusd": {
|
||||||
"symbol": "ltc_usd",
|
"symbol": "ltc_usd",
|
||||||
"start_date": "2010-01-01"
|
"start_date": "2010-01-01"
|
||||||
@@ -0,0 +1,307 @@
|
|||||||
|
import json
|
||||||
|
|
||||||
|
import pandas as pd
|
||||||
|
from catalyst.assets._assets import TradingPair
|
||||||
|
from logbook import Logger
|
||||||
|
from six.moves import urllib
|
||||||
|
|
||||||
|
from catalyst.exchange.bittrex.bittrex_api import Bittrex_api
|
||||||
|
from catalyst.exchange.exchange import Exchange
|
||||||
|
from catalyst.exchange.exchange_errors import InvalidHistoryFrequencyError, \
|
||||||
|
ExchangeRequestError, InvalidOrderStyle, OrderNotFound, OrderCancelError, \
|
||||||
|
CreateOrderError
|
||||||
|
from catalyst.finance.execution import LimitOrder, StopLimitOrder
|
||||||
|
from catalyst.finance.order import Order, ORDER_STATUS
|
||||||
|
|
||||||
|
log = Logger('Bittrex')
|
||||||
|
|
||||||
|
URL2 = 'https://bittrex.com/Api/v2.0'
|
||||||
|
|
||||||
|
|
||||||
|
class Bittrex(Exchange):
|
||||||
|
def __init__(self, key, secret, base_currency, portfolio=None):
|
||||||
|
self.api = Bittrex_api(key=key, secret=secret.encode('UTF-8'))
|
||||||
|
self.name = 'bittrex'
|
||||||
|
self.base_currency = base_currency
|
||||||
|
self._portfolio = portfolio
|
||||||
|
|
||||||
|
self.minute_writer = None
|
||||||
|
self.minute_reader = None
|
||||||
|
|
||||||
|
self.assets = dict()
|
||||||
|
self.load_assets()
|
||||||
|
|
||||||
|
@property
|
||||||
|
def account(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@property
|
||||||
|
def time_skew(self):
|
||||||
|
# TODO: research the time skew conditions
|
||||||
|
return pd.Timedelta('0s')
|
||||||
|
|
||||||
|
def sanitize_curency_symbol(self, exchange_symbol):
|
||||||
|
"""
|
||||||
|
Helper method used to build the universal pair.
|
||||||
|
Include any symbol mapping here if appropriate.
|
||||||
|
|
||||||
|
:param exchange_symbol:
|
||||||
|
:return universal_symbol:
|
||||||
|
"""
|
||||||
|
return exchange_symbol.lower()
|
||||||
|
|
||||||
|
def fetch_symbol_map(self):
|
||||||
|
"""
|
||||||
|
Since Bittrex gives us a complete dictionary of symbols,
|
||||||
|
we can build the symbol map ad-hoc as opposed to maintaining
|
||||||
|
a static file. We must be careful with mapping any unconventional
|
||||||
|
symbol name as appropriate.
|
||||||
|
|
||||||
|
:return symbol_map:
|
||||||
|
"""
|
||||||
|
symbol_map = dict()
|
||||||
|
|
||||||
|
markets = self.api.getmarkets()
|
||||||
|
for market in markets:
|
||||||
|
exchange_symbol = market['MarketName']
|
||||||
|
symbol = '{market}_{base}'.format(
|
||||||
|
market=self.sanitize_curency_symbol(market['MarketCurrency']),
|
||||||
|
base=self.sanitize_curency_symbol(market['BaseCurrency'])
|
||||||
|
)
|
||||||
|
symbol_map[exchange_symbol] = dict(
|
||||||
|
symbol=symbol,
|
||||||
|
start_date=pd.to_datetime(market['Created'], utc=True)
|
||||||
|
)
|
||||||
|
|
||||||
|
return symbol_map
|
||||||
|
|
||||||
|
def get_balances(self):
|
||||||
|
try:
|
||||||
|
log.debug('retrieving wallet balances')
|
||||||
|
balances = self.api.getbalances()
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
std_balances = dict()
|
||||||
|
for balance in balances:
|
||||||
|
currency = balance['Currency'].lower()
|
||||||
|
std_balances[currency] = balance['Available']
|
||||||
|
return std_balances
|
||||||
|
|
||||||
|
def create_order(self, asset, amount, is_buy, style):
|
||||||
|
log.info('creating {} order'.format('buy' if is_buy else 'sell'))
|
||||||
|
exchange_symbol = self.get_symbol(asset)
|
||||||
|
|
||||||
|
if isinstance(style, LimitOrder) or isinstance(style, StopLimitOrder):
|
||||||
|
if isinstance(style, StopLimitOrder):
|
||||||
|
log.warn('{} will ignore the stop price'.format(self.name))
|
||||||
|
|
||||||
|
price = style.get_limit_price(is_buy)
|
||||||
|
try:
|
||||||
|
if is_buy:
|
||||||
|
order_status = self.api.buylimit(exchange_symbol, amount,
|
||||||
|
price)
|
||||||
|
else:
|
||||||
|
order_status = self.api.selllimit(exchange_symbol,
|
||||||
|
abs(amount), price)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if 'uuid' in order_status:
|
||||||
|
order_id = order_status['uuid']
|
||||||
|
order = Order(
|
||||||
|
dt=pd.Timestamp.utcnow(),
|
||||||
|
asset=asset,
|
||||||
|
amount=amount,
|
||||||
|
stop=style.get_stop_price(is_buy),
|
||||||
|
limit=style.get_limit_price(is_buy),
|
||||||
|
id=order_id
|
||||||
|
)
|
||||||
|
return order
|
||||||
|
else:
|
||||||
|
raise CreateOrderError(exchange=self.name, error=order_status)
|
||||||
|
else:
|
||||||
|
raise InvalidOrderStyle(exchange=self.name,
|
||||||
|
style=style.__class__.__name__)
|
||||||
|
|
||||||
|
def get_open_orders(self, asset):
|
||||||
|
symbol = self.get_symbol(asset)
|
||||||
|
try:
|
||||||
|
open_orders = self.api.getopenorders(symbol)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
orders = list()
|
||||||
|
for order_status in open_orders:
|
||||||
|
order = self._create_order(order_status)
|
||||||
|
orders.append(order)
|
||||||
|
|
||||||
|
return orders
|
||||||
|
|
||||||
|
def _create_order(self, order_status):
|
||||||
|
log.info(
|
||||||
|
'creating catalyst order from Bittrex {}'.format(order_status))
|
||||||
|
if order_status['CancelInitiated']:
|
||||||
|
status = ORDER_STATUS.CANCELLED
|
||||||
|
elif order_status['Closed'] is not None:
|
||||||
|
status = ORDER_STATUS.FILLED
|
||||||
|
else:
|
||||||
|
status = ORDER_STATUS.OPEN
|
||||||
|
|
||||||
|
date = pd.to_datetime(order_status['Opened'], utc=True)
|
||||||
|
amount = order_status['Quantity']
|
||||||
|
filled = amount - order_status['QuantityRemaining']
|
||||||
|
order = Order(
|
||||||
|
dt=date,
|
||||||
|
asset=self.assets[order_status['Exchange']],
|
||||||
|
amount=amount,
|
||||||
|
stop=None, # Not yet supported by Bittrex
|
||||||
|
limit=order_status['Limit'],
|
||||||
|
filled=filled,
|
||||||
|
id=order_status['OrderUuid'],
|
||||||
|
commission=order_status['CommissionPaid']
|
||||||
|
)
|
||||||
|
order.status = status
|
||||||
|
|
||||||
|
executed_price = order_status['PricePerUnit']
|
||||||
|
|
||||||
|
return order, executed_price
|
||||||
|
|
||||||
|
def get_order(self, order_id):
|
||||||
|
log.info('retrieving order {}'.format(order_id))
|
||||||
|
try:
|
||||||
|
order_status = self.api.getorder(order_id)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if order_status is None:
|
||||||
|
raise OrderNotFound(order_id=order_id, exchange=self.name)
|
||||||
|
|
||||||
|
return self._create_order(order_status)
|
||||||
|
|
||||||
|
def cancel_order(self, order_param):
|
||||||
|
order_id = order_param.id \
|
||||||
|
if isinstance(order_param, Order) else order_param
|
||||||
|
log.info('cancelling order {}'.format(order_id))
|
||||||
|
|
||||||
|
try:
|
||||||
|
status = self.api.cancel(order_id)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if 'message' in status:
|
||||||
|
raise OrderCancelError(
|
||||||
|
order_id=order_id,
|
||||||
|
exchange=self.name,
|
||||||
|
error=status['message']
|
||||||
|
)
|
||||||
|
|
||||||
|
def get_candles(self, data_frequency, assets, bar_count=None):
|
||||||
|
"""
|
||||||
|
Supported Intervals
|
||||||
|
-------------------
|
||||||
|
day, oneMin, fiveMin, thirtyMin, hour
|
||||||
|
|
||||||
|
:param data_frequency:
|
||||||
|
:param assets:
|
||||||
|
:param bar_count:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
log.info('retrieving candles')
|
||||||
|
|
||||||
|
if data_frequency == 'minute' or data_frequency == '1m':
|
||||||
|
frequency = 'oneMin'
|
||||||
|
elif data_frequency == '5m':
|
||||||
|
frequency = 'fiveMin'
|
||||||
|
elif data_frequency == '30m':
|
||||||
|
frequency = 'thirtyMin'
|
||||||
|
elif data_frequency == '1h':
|
||||||
|
frequency = 'hour'
|
||||||
|
elif data_frequency == 'daily' or data_frequency == '1D':
|
||||||
|
frequency = 'day'
|
||||||
|
else:
|
||||||
|
raise InvalidHistoryFrequencyError(
|
||||||
|
frequency=data_frequency
|
||||||
|
)
|
||||||
|
|
||||||
|
# Making sure that assets are iterable
|
||||||
|
asset_list = [assets] if isinstance(assets, TradingPair) else assets
|
||||||
|
ohlc_map = dict()
|
||||||
|
for asset in asset_list:
|
||||||
|
url = '{url}/pub/market/GetTicks?marketName={symbol}' \
|
||||||
|
'&tickInterval={frequency}&_=1499127220008'.format(
|
||||||
|
url=URL2,
|
||||||
|
symbol=self.get_symbol(asset),
|
||||||
|
frequency=frequency
|
||||||
|
)
|
||||||
|
|
||||||
|
try:
|
||||||
|
data = json.loads(urllib.request.urlopen(url).read().decode())
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
if data['message']:
|
||||||
|
raise ExchangeRequestError(
|
||||||
|
error='Unable to fetch candles {}'.format(data['message'])
|
||||||
|
)
|
||||||
|
|
||||||
|
candles = data['result']
|
||||||
|
|
||||||
|
def ohlc_from_candle(candle):
|
||||||
|
ohlc = dict(
|
||||||
|
open=candle['O'],
|
||||||
|
high=candle['H'],
|
||||||
|
low=candle['L'],
|
||||||
|
close=candle['C'],
|
||||||
|
volume=candle['V'],
|
||||||
|
price=candle['C'],
|
||||||
|
last_traded=pd.to_datetime(candle['T'], utc=True)
|
||||||
|
)
|
||||||
|
return ohlc
|
||||||
|
|
||||||
|
ordered_candles = list(reversed(candles))
|
||||||
|
if bar_count is None:
|
||||||
|
ohlc_map[asset] = ohlc_from_candle(ordered_candles[0])
|
||||||
|
else:
|
||||||
|
ohlc_bars = []
|
||||||
|
for candle in ordered_candles[:bar_count]:
|
||||||
|
ohlc = ohlc_from_candle(candle)
|
||||||
|
ohlc_bars.append(ohlc)
|
||||||
|
|
||||||
|
ohlc_map[asset] = ohlc_bars
|
||||||
|
|
||||||
|
return ohlc_map[assets] \
|
||||||
|
if isinstance(assets, TradingPair) else ohlc_map
|
||||||
|
|
||||||
|
def tickers(self, assets):
|
||||||
|
"""
|
||||||
|
As of v1.1, Bittrex only allows one ticker at the time.
|
||||||
|
So we have to make multiple calls to fetch multiple assets.
|
||||||
|
|
||||||
|
:param assets:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
log.info('retrieving tickers')
|
||||||
|
|
||||||
|
ticks = dict()
|
||||||
|
for asset in assets:
|
||||||
|
symbol = self.get_symbol(asset)
|
||||||
|
try:
|
||||||
|
ticker = self.api.getticker(symbol)
|
||||||
|
except Exception as e:
|
||||||
|
raise ExchangeRequestError(error=e)
|
||||||
|
|
||||||
|
# TODO: catch invalid ticker
|
||||||
|
ticks[asset] = dict(
|
||||||
|
timestamp=pd.Timestamp.utcnow(),
|
||||||
|
bid=ticker['Bid'],
|
||||||
|
ask=ticker['Ask'],
|
||||||
|
last_price=ticker['Last']
|
||||||
|
)
|
||||||
|
|
||||||
|
log.debug('got tickers {}'.format(ticks))
|
||||||
|
return ticks
|
||||||
|
|
||||||
|
def get_account(self):
|
||||||
|
log.info('retrieving account data')
|
||||||
|
pass
|
||||||
@@ -0,0 +1,127 @@
|
|||||||
|
#!/usr/bin/env python
|
||||||
|
import json
|
||||||
|
import time
|
||||||
|
import hmac
|
||||||
|
import hashlib
|
||||||
|
|
||||||
|
from six.moves import urllib
|
||||||
|
|
||||||
|
# Workaround for backwards compatibility
|
||||||
|
# https://stackoverflow.com/questions/3745771/urllib-request-in-python-2-7
|
||||||
|
urlopen = urllib.request.urlopen
|
||||||
|
|
||||||
|
|
||||||
|
class Bittrex_api(object):
|
||||||
|
def __init__(self, key, secret):
|
||||||
|
self.key = key
|
||||||
|
self.secret = secret
|
||||||
|
self.public = ['getmarkets', 'getcurrencies', 'getticker',
|
||||||
|
'getmarketsummaries', 'getmarketsummary',
|
||||||
|
'getorderbook', 'getmarkethistory']
|
||||||
|
self.market = ['buylimit', 'buymarket', 'selllimit', 'sellmarket',
|
||||||
|
'cancel', 'getopenorders']
|
||||||
|
self.account = ['getbalances', 'getbalance', 'getdepositaddress',
|
||||||
|
'withdraw', 'getorder', 'getorderhistory',
|
||||||
|
'getwithdrawalhistory', 'getdeposithistory']
|
||||||
|
|
||||||
|
def query(self, method, values={}):
|
||||||
|
if method in self.public:
|
||||||
|
url = 'https://bittrex.com/api/v1.1/public/'
|
||||||
|
elif method in self.market:
|
||||||
|
url = 'https://bittrex.com/api/v1.1/market/'
|
||||||
|
elif method in self.account:
|
||||||
|
url = 'https://bittrex.com/api/v1.1/account/'
|
||||||
|
else:
|
||||||
|
return 'Something went wrong, sorry.'
|
||||||
|
|
||||||
|
url += method + '?' + urllib.parse.urlencode(values)
|
||||||
|
|
||||||
|
if method not in self.public:
|
||||||
|
url += '&apikey=' + self.key
|
||||||
|
url += '&nonce=' + str(int(time.time()))
|
||||||
|
signature = hmac.new(self.secret, url, hashlib.sha512).hexdigest()
|
||||||
|
headers = {'apisign': signature}
|
||||||
|
else:
|
||||||
|
headers = {}
|
||||||
|
|
||||||
|
req = urllib.request.Request(url, headers=headers)
|
||||||
|
response = json.loads(urlopen(req).read())
|
||||||
|
|
||||||
|
if response["result"]:
|
||||||
|
return response["result"]
|
||||||
|
else:
|
||||||
|
return response["message"]
|
||||||
|
|
||||||
|
def getmarkets(self):
|
||||||
|
return self.query('getmarkets')
|
||||||
|
|
||||||
|
def getcurrencies(self):
|
||||||
|
return self.query('getcurrencies')
|
||||||
|
|
||||||
|
def getticker(self, market):
|
||||||
|
return self.query('getticker', {'market': market})
|
||||||
|
|
||||||
|
def getmarketsummaries(self):
|
||||||
|
return self.query('getmarketsummaries')
|
||||||
|
|
||||||
|
def getmarketsummary(self, market):
|
||||||
|
return self.query('getmarketsummary', {'market': market})
|
||||||
|
|
||||||
|
def getorderbook(self, market, type, depth=20):
|
||||||
|
return self.query('getorderbook',
|
||||||
|
{'market': market, 'type': type, 'depth': depth})
|
||||||
|
|
||||||
|
def getmarkethistory(self, market, count=20):
|
||||||
|
return self.query('getmarkethistory',
|
||||||
|
{'market': market, 'count': count})
|
||||||
|
|
||||||
|
def buylimit(self, market, quantity, rate):
|
||||||
|
return self.query('buylimit', {'market': market, 'quantity': quantity,
|
||||||
|
'rate': rate})
|
||||||
|
|
||||||
|
def buymarket(self, market, quantity):
|
||||||
|
return self.query('buymarket',
|
||||||
|
{'market': market, 'quantity': quantity})
|
||||||
|
|
||||||
|
def selllimit(self, market, quantity, rate):
|
||||||
|
return self.query('selllimit', {'market': market, 'quantity': quantity,
|
||||||
|
'rate': rate})
|
||||||
|
|
||||||
|
def sellmarket(self, market, quantity):
|
||||||
|
return self.query('sellmarket',
|
||||||
|
{'market': market, 'quantity': quantity})
|
||||||
|
|
||||||
|
def cancel(self, uuid):
|
||||||
|
return self.query('cancel', {'uuid': uuid})
|
||||||
|
|
||||||
|
def getopenorders(self, market):
|
||||||
|
return self.query('getopenorders', {'market': market})
|
||||||
|
|
||||||
|
def getbalances(self):
|
||||||
|
return self.query('getbalances')
|
||||||
|
|
||||||
|
def getbalance(self, currency):
|
||||||
|
return self.query('getbalance', {'currency': currency})
|
||||||
|
|
||||||
|
def getdepositaddress(self, currency):
|
||||||
|
return self.query('getdepositaddress', {'currency': currency})
|
||||||
|
|
||||||
|
def withdraw(self, currency, quantity, address):
|
||||||
|
return self.query('withdraw',
|
||||||
|
{'currency': currency, 'quantity': quantity,
|
||||||
|
'address': address})
|
||||||
|
|
||||||
|
def getorder(self, uuid):
|
||||||
|
return self.query('getorder', {'uuid': uuid})
|
||||||
|
|
||||||
|
def getorderhistory(self, market, count):
|
||||||
|
return self.query('getorderhistory',
|
||||||
|
{'market': market, 'count': count})
|
||||||
|
|
||||||
|
def getwithdrawalhistory(self, currency, count):
|
||||||
|
return self.query('getwithdrawalhistory',
|
||||||
|
{'currency': currency, 'count': count})
|
||||||
|
|
||||||
|
def getdeposithistory(self, currency, count):
|
||||||
|
return self.query('getdeposithistory',
|
||||||
|
{'currency': currency, 'count': count})
|
||||||
+170
-40
@@ -1,22 +1,26 @@
|
|||||||
import abc
|
import abc
|
||||||
import random
|
|
||||||
from time import sleep
|
|
||||||
import collections
|
import collections
|
||||||
|
import random
|
||||||
from abc import ABCMeta, abstractmethod, abstractproperty
|
from abc import ABCMeta, abstractmethod, abstractproperty
|
||||||
from datetime import timedelta
|
from time import sleep
|
||||||
|
|
||||||
import numpy as np
|
import numpy as np
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
from catalyst.assets._assets import Asset
|
from catalyst.assets._assets import TradingPair
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
|
|
||||||
from catalyst.data.data_portal import BASE_FIELDS
|
from catalyst.data.data_portal import BASE_FIELDS
|
||||||
from catalyst.errors import (
|
from catalyst.errors import (
|
||||||
SymbolNotFound,
|
SymbolNotFound,
|
||||||
)
|
)
|
||||||
|
from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \
|
||||||
|
InvalidOrderStyle, BaseCurrencyNotFoundError
|
||||||
|
from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \
|
||||||
|
ExchangeLimitOrder, ExchangeStopOrder
|
||||||
|
from catalyst.exchange.exchange_portfolio import ExchangePortfolio
|
||||||
|
from catalyst.exchange.exchange_utils import get_exchange_symbols
|
||||||
from catalyst.finance.order import ORDER_STATUS
|
from catalyst.finance.order import ORDER_STATUS
|
||||||
from catalyst.finance.transaction import Transaction
|
from catalyst.finance.transaction import Transaction
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_symbols
|
|
||||||
|
|
||||||
log = Logger('Exchange')
|
log = Logger('Exchange')
|
||||||
|
|
||||||
@@ -31,22 +35,26 @@ class Exchange:
|
|||||||
self._portfolio = None
|
self._portfolio = None
|
||||||
self.minute_writer = None
|
self.minute_writer = None
|
||||||
self.minute_reader = None
|
self.minute_reader = None
|
||||||
|
self.base_currency = None
|
||||||
|
|
||||||
@abstractmethod
|
@property
|
||||||
def subscribe_to_market_data(self, symbol):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractproperty
|
|
||||||
def positions(self):
|
def positions(self):
|
||||||
pass
|
return self.portfolio.positions
|
||||||
|
|
||||||
@abstractproperty
|
@property
|
||||||
def update_portfolio(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractproperty
|
|
||||||
def portfolio(self):
|
def portfolio(self):
|
||||||
pass
|
"""
|
||||||
|
Return the Portfolio
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
if self._portfolio is None:
|
||||||
|
self._portfolio = ExchangePortfolio(
|
||||||
|
start_date=pd.Timestamp.utcnow()
|
||||||
|
)
|
||||||
|
self.synchronize_portfolio()
|
||||||
|
|
||||||
|
return self._portfolio
|
||||||
|
|
||||||
@abstractproperty
|
@abstractproperty
|
||||||
def account(self):
|
def account(self):
|
||||||
@@ -102,10 +110,13 @@ class Exchange:
|
|||||||
asset = self.assets[key]
|
asset = self.assets[key]
|
||||||
|
|
||||||
if not asset:
|
if not asset:
|
||||||
raise SymbolNotFound('Asset not found: %s' % symbol)
|
raise SymbolNotFound(symbol=symbol)
|
||||||
|
|
||||||
return asset
|
return asset
|
||||||
|
|
||||||
|
def fetch_symbol_map(self):
|
||||||
|
return get_exchange_symbols(self.name)
|
||||||
|
|
||||||
def load_assets(self):
|
def load_assets(self):
|
||||||
"""
|
"""
|
||||||
Populate the 'assets' attribute with a dictionary of Assets.
|
Populate the 'assets' attribute with a dictionary of Assets.
|
||||||
@@ -124,22 +135,40 @@ class Exchange:
|
|||||||
via its api.
|
via its api.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
symbol_map = get_exchange_symbols(self.name)
|
symbol_map = self.fetch_symbol_map()
|
||||||
for exchange_symbol in symbol_map:
|
for exchange_symbol in symbol_map:
|
||||||
asset = symbol_map[exchange_symbol]
|
asset = symbol_map[exchange_symbol]
|
||||||
symbol = asset['symbol']
|
|
||||||
asset_name = ' / '.join(symbol.split('_')).upper()
|
|
||||||
|
|
||||||
asset_obj = Asset(
|
if 'start_date' in asset:
|
||||||
symbol=symbol,
|
start_date = pd.to_datetime(asset['start_date'], utc=True)
|
||||||
asset_name=asset_name,
|
else:
|
||||||
sid=abs(hash(symbol)) % (10 ** 4),
|
start_date = None
|
||||||
|
|
||||||
|
if 'end_date' in asset:
|
||||||
|
end_date = pd.to_datetime(asset['end_date'], utc=True)
|
||||||
|
else:
|
||||||
|
end_date = None
|
||||||
|
|
||||||
|
if 'leverage' in asset:
|
||||||
|
leverage = asset['leverage']
|
||||||
|
else:
|
||||||
|
leverage = 1.0
|
||||||
|
|
||||||
|
if 'asset_name' in asset:
|
||||||
|
asset_name = asset['asset_name']
|
||||||
|
else:
|
||||||
|
asset_name = None
|
||||||
|
|
||||||
|
trading_pair = TradingPair(
|
||||||
|
symbol=asset['symbol'],
|
||||||
exchange=self.name,
|
exchange=self.name,
|
||||||
start_date=pd.to_datetime(asset['start_date'], utc=True),
|
start_date=start_date,
|
||||||
end_date=pd.Timestamp.utcnow() + timedelta(minutes=300000),
|
end_date=end_date,
|
||||||
|
leverage=leverage,
|
||||||
|
asset_name=asset_name
|
||||||
)
|
)
|
||||||
|
|
||||||
self.assets[exchange_symbol] = asset_obj
|
self.assets[exchange_symbol] = trading_pair
|
||||||
|
|
||||||
def check_open_orders(self):
|
def check_open_orders(self):
|
||||||
"""
|
"""
|
||||||
@@ -355,24 +384,76 @@ class Exchange:
|
|||||||
bar_count=bar_count,
|
bar_count=bar_count,
|
||||||
)
|
)
|
||||||
|
|
||||||
frames = []
|
series = dict()
|
||||||
for asset in assets:
|
for asset in assets:
|
||||||
asset_candles = candles[asset]
|
asset_candles = candles[asset]
|
||||||
|
|
||||||
asset_data = dict()
|
values = map(lambda candle: candle[field], asset_candles)
|
||||||
asset_data[asset] = map(lambda candle: candle[field],
|
dates = map(lambda candle: candle['last_traded'], asset_candles)
|
||||||
asset_candles)
|
|
||||||
|
|
||||||
dates = map(lambda candle: candle['last_traded'],
|
value_series = pd.Series(values, index=dates)
|
||||||
asset_candles)
|
series[asset] = value_series
|
||||||
|
|
||||||
df = pd.DataFrame(asset_data, index=dates)
|
df = pd.concat(series)
|
||||||
frames.append(df)
|
return df
|
||||||
|
|
||||||
return pd.concat(frames)
|
def synchronize_portfolio(self):
|
||||||
|
"""
|
||||||
|
Update the portfolio cash and position balances based on the
|
||||||
|
latest ticker prices.
|
||||||
|
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
log.debug('synchronizing portfolio with exchange {}'.format(self.name))
|
||||||
|
balances = self.get_balances()
|
||||||
|
|
||||||
|
base_position_available = balances[self.base_currency] \
|
||||||
|
if self.base_currency in balances else None
|
||||||
|
|
||||||
|
if base_position_available is None:
|
||||||
|
raise BaseCurrencyNotFoundError(
|
||||||
|
base_currency=self.base_currency,
|
||||||
|
exchange=self.name
|
||||||
|
)
|
||||||
|
|
||||||
|
portfolio = self._portfolio
|
||||||
|
portfolio.cash = base_position_available
|
||||||
|
log.debug('found base currency balance: {}'.format(portfolio.cash))
|
||||||
|
|
||||||
|
if portfolio.starting_cash is None:
|
||||||
|
portfolio.starting_cash = portfolio.cash
|
||||||
|
|
||||||
|
if portfolio.positions:
|
||||||
|
assets = portfolio.positions.keys()
|
||||||
|
tickers = self.tickers(assets)
|
||||||
|
|
||||||
|
portfolio.positions_value = 0.0
|
||||||
|
for asset in tickers:
|
||||||
|
# TODO: convert if the position is not in the base currency
|
||||||
|
ticker = tickers[asset]
|
||||||
|
position = portfolio.positions[asset]
|
||||||
|
position.last_sale_price = ticker['last_price']
|
||||||
|
position.last_sale_date = ticker['timestamp']
|
||||||
|
|
||||||
|
portfolio.positions_value += \
|
||||||
|
position.amount * position.last_sale_price
|
||||||
|
portfolio.portfolio_value = \
|
||||||
|
portfolio.positions_value + portfolio.cash
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def order(self, asset, amount, limit_price, stop_price, style):
|
def get_balances(self):
|
||||||
|
"""
|
||||||
|
Retrieve wallet balances for the exchange
|
||||||
|
:return balances: A dict of currency => available balance
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def create_order(self, asset, amount, is_buy, style):
|
||||||
|
pass
|
||||||
|
|
||||||
|
def order(self, asset, amount, limit_price=None, stop_price=None,
|
||||||
|
style=None):
|
||||||
"""Place an order.
|
"""Place an order.
|
||||||
|
|
||||||
Parameters
|
Parameters
|
||||||
@@ -412,7 +493,48 @@ class Exchange:
|
|||||||
:func:`catalyst.api.order_value`
|
:func:`catalyst.api.order_value`
|
||||||
:func:`catalyst.api.order_percent`
|
:func:`catalyst.api.order_percent`
|
||||||
"""
|
"""
|
||||||
pass
|
if amount == 0:
|
||||||
|
log.warn('skipping order amount of 0')
|
||||||
|
return None
|
||||||
|
|
||||||
|
if asset.base_currency != self.base_currency.lower():
|
||||||
|
raise MismatchingBaseCurrencies(
|
||||||
|
base_currency=asset.base_currency,
|
||||||
|
algo_currency=self.base_currency
|
||||||
|
)
|
||||||
|
|
||||||
|
is_buy = (amount > 0)
|
||||||
|
|
||||||
|
if limit_price is not None and stop_price is not None:
|
||||||
|
style = ExchangeStopLimitOrder(limit_price, stop_price,
|
||||||
|
exchange=self.name)
|
||||||
|
elif limit_price is not None:
|
||||||
|
style = ExchangeLimitOrder(limit_price, exchange=self.name)
|
||||||
|
|
||||||
|
elif stop_price is not None:
|
||||||
|
style = ExchangeStopOrder(stop_price, exchange=self.name)
|
||||||
|
|
||||||
|
elif style is not None:
|
||||||
|
raise InvalidOrderStyle(exchange=self.name,
|
||||||
|
style=style.__class__.__name__)
|
||||||
|
else:
|
||||||
|
raise ValueError('Incomplete order data.')
|
||||||
|
|
||||||
|
display_price = limit_price if limit_price is not None else stop_price
|
||||||
|
log.debug(
|
||||||
|
'issuing {side} order of {amount} {symbol} for {type}: {price}'.format(
|
||||||
|
side='buy' if is_buy else 'sell',
|
||||||
|
amount=amount,
|
||||||
|
symbol=asset.symbol,
|
||||||
|
type=style.__class__.__name__,
|
||||||
|
price='{}{}'.format(display_price, asset.base_currency)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
order = self.create_order(asset, amount, is_buy, style)
|
||||||
|
|
||||||
|
self._portfolio.create_order(order)
|
||||||
|
|
||||||
|
return order.id
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def get_open_orders(self, asset):
|
def get_open_orders(self, asset):
|
||||||
@@ -486,4 +608,12 @@ class Exchange:
|
|||||||
:param assets:
|
:param assets:
|
||||||
:return:
|
:return:
|
||||||
"""
|
"""
|
||||||
return
|
pass
|
||||||
|
|
||||||
|
@abc.abstractmethod
|
||||||
|
def get_account(self):
|
||||||
|
"""
|
||||||
|
Retrieve the account parameters.
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
pass
|
||||||
|
|||||||
@@ -58,3 +58,56 @@ class InvalidHistoryFrequencyError(ZiplineError):
|
|||||||
msg = (
|
msg = (
|
||||||
'History frequency {frequency} not supported by the exchange.'
|
'History frequency {frequency} not supported by the exchange.'
|
||||||
).strip()
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidSymbolError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Invalid trading pair symbol: {symbol}. '
|
||||||
|
'Catalyst symbols must follow this convention: '
|
||||||
|
'[Market Currency]_[Base Currency]. For example: eth_usd, btc_usd, '
|
||||||
|
'neo_eth, ubq_btc. Error details: {error}'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class InvalidOrderStyle(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Order style {style} not supported by exchange {exchange}.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class CreateOrderError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Unable to create order on exchange {exchange} {error}.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class OrderNotFound(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Order {order_id} not found on exchange {exchange}.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class OrderCancelError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Unable to cancel order {order_id} on exchange {exchange} {error}.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class SidHashError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Unable to hash sid from symbol {symbol}.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class BaseCurrencyNotFoundError(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Algorithm base currency {base_currency} not found in exchange '
|
||||||
|
'{exchange}.'
|
||||||
|
).strip()
|
||||||
|
|
||||||
|
|
||||||
|
class MismatchingBaseCurrencies(ZiplineError):
|
||||||
|
msg = (
|
||||||
|
'Unable to trade with base currency {base_currency} when the '
|
||||||
|
'algorithm uses {algo_currency}.'
|
||||||
|
).strip()
|
||||||
|
|||||||
@@ -0,0 +1,39 @@
|
|||||||
|
from catalyst.finance.execution import LimitOrder, StopOrder, StopLimitOrder
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeLimitOrder(LimitOrder):
|
||||||
|
def get_limit_price(self, is_buy):
|
||||||
|
"""
|
||||||
|
We may be trading Satoshis with 8 decimals, we cannot round numbers
|
||||||
|
:param is_buy:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return self.limit_price
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeStopOrder(StopOrder):
|
||||||
|
def get_stop_price(self, is_buy):
|
||||||
|
"""
|
||||||
|
We may be trading Satoshis with 8 decimals, we cannot round numbers
|
||||||
|
:param is_buy:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return self.stop_price
|
||||||
|
|
||||||
|
|
||||||
|
class ExchangeStopLimitOrder(StopLimitOrder):
|
||||||
|
def get_limit_price(self, is_buy):
|
||||||
|
"""
|
||||||
|
We may be trading Satoshis with 8 decimals, we cannot round numbers
|
||||||
|
:param is_buy:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return self.limit_price
|
||||||
|
|
||||||
|
def get_stop_price(self, is_buy):
|
||||||
|
"""
|
||||||
|
We may be trading Satoshis with 8 decimals, we cannot round numbers
|
||||||
|
:param is_buy:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
return self.stop_price
|
||||||
@@ -8,8 +8,9 @@ from catalyst.exchange.exchange_errors import ExchangeAuthNotFound, \
|
|||||||
ExchangeSymbolsNotFound
|
ExchangeSymbolsNotFound
|
||||||
from catalyst.utils.paths import data_root, ensure_directory
|
from catalyst.utils.paths import data_root, ensure_directory
|
||||||
|
|
||||||
|
# TODO: move to aws
|
||||||
SYMBOLS_URL = 'https://raw.githubusercontent.com/enigmampc/catalyst/' \
|
SYMBOLS_URL = 'https://raw.githubusercontent.com/enigmampc/catalyst/' \
|
||||||
'live-trading/catalyst/exchange/symbols/{exchange}.json'
|
'master/catalyst/exchange/{exchange}/symbols.json'
|
||||||
|
|
||||||
|
|
||||||
def get_exchange_folder(exchange_name, environ=None):
|
def get_exchange_folder(exchange_name, environ=None):
|
||||||
|
|||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import pandas as pd
|
||||||
|
|
||||||
|
|
||||||
|
def get_pretty_stats(stats_df, num_rows=10):
|
||||||
|
"""
|
||||||
|
Format and print the last few rows of a statistics DataFrame.
|
||||||
|
See the pyfolio project for the data structure.
|
||||||
|
|
||||||
|
:param stats_df:
|
||||||
|
:param num_rows:
|
||||||
|
:return:
|
||||||
|
"""
|
||||||
|
stats_df.set_index('period_close', drop=True, inplace=True)
|
||||||
|
stats_df.dropna(axis=1, how='all', inplace=True)
|
||||||
|
|
||||||
|
pd.set_option('display.expand_frame_repr', False)
|
||||||
|
pd.set_option('precision', 3)
|
||||||
|
pd.set_option('display.width', 1000)
|
||||||
|
pd.set_option('display.max_colwidth', 1000)
|
||||||
|
|
||||||
|
columns = ['starting_cash', 'ending_cash', 'portfolio_value',
|
||||||
|
'pnl', 'long_exposure', 'short_exposure', 'orders',
|
||||||
|
'transactions', 'positions']
|
||||||
|
|
||||||
|
def format_positions(positions):
|
||||||
|
parts = []
|
||||||
|
for position in positions:
|
||||||
|
msg = '{amount:.2f}{market} cost basis {cost_basis:.4f}{base}'.format(
|
||||||
|
amount=position['amount'],
|
||||||
|
market=position['sid'].market_currency,
|
||||||
|
cost_basis=position['cost_basis'],
|
||||||
|
base=position['sid'].base_currency
|
||||||
|
)
|
||||||
|
parts.append(msg)
|
||||||
|
return ', '.join(parts)
|
||||||
|
|
||||||
|
formatters = {
|
||||||
|
'orders': lambda orders: len(orders),
|
||||||
|
'transactions': lambda transactions: len(transactions),
|
||||||
|
'returns': lambda returns: "{0:.4f}".format(returns),
|
||||||
|
'positions': format_positions
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats_df.tail(num_rows).to_string(
|
||||||
|
columns=columns,
|
||||||
|
formatters=formatters
|
||||||
|
)
|
||||||
+100
-80
@@ -10,6 +10,8 @@ import pandas as pd
|
|||||||
|
|
||||||
import click
|
import click
|
||||||
|
|
||||||
|
from catalyst.exchange.bittrex.bittrex import Bittrex
|
||||||
|
|
||||||
try:
|
try:
|
||||||
from pygments import highlight
|
from pygments import highlight
|
||||||
from pygments.lexers import PythonLexer
|
from pygments.lexers import PythonLexer
|
||||||
@@ -42,8 +44,8 @@ from catalyst.exchange.asset_finder_exchange import AssetFinderExchange
|
|||||||
from catalyst.exchange.exchange_portfolio import ExchangePortfolio
|
from catalyst.exchange.exchange_portfolio import ExchangePortfolio
|
||||||
from catalyst.exchange.exchange_errors import (
|
from catalyst.exchange.exchange_errors import (
|
||||||
ExchangeRequestError,
|
ExchangeRequestError,
|
||||||
ExchangeRequestErrorTooManyAttempts
|
ExchangeRequestErrorTooManyAttempts,
|
||||||
)
|
BaseCurrencyNotFoundError)
|
||||||
from catalyst.exchange.exchange_utils import get_exchange_auth, \
|
from catalyst.exchange.exchange_utils import get_exchange_auth, \
|
||||||
get_algo_object
|
get_algo_object
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
@@ -142,10 +144,43 @@ def _run(handle_data,
|
|||||||
else:
|
else:
|
||||||
click.echo(algotext)
|
click.echo(algotext)
|
||||||
|
|
||||||
if exchange is not None:
|
mode = 'live' if live else 'backtest'
|
||||||
|
log.info('running algo in {mode} mode'.format(mode=mode))
|
||||||
|
|
||||||
|
if live and exchange is not None:
|
||||||
|
exchange_name = exchange
|
||||||
start = pd.Timestamp.utcnow()
|
start = pd.Timestamp.utcnow()
|
||||||
end = start + timedelta(minutes=1439)
|
end = start + timedelta(minutes=1439)
|
||||||
|
|
||||||
|
portfolio = get_algo_object(
|
||||||
|
algo_name=algo_namespace,
|
||||||
|
key='portfolio_{}'.format(exchange_name),
|
||||||
|
environ=environ
|
||||||
|
)
|
||||||
|
if portfolio is None:
|
||||||
|
portfolio = ExchangePortfolio(
|
||||||
|
start_date=pd.Timestamp.utcnow()
|
||||||
|
)
|
||||||
|
|
||||||
|
exchange_auth = get_exchange_auth(exchange_name)
|
||||||
|
if exchange_name == 'bitfinex':
|
||||||
|
exchange = Bitfinex(
|
||||||
|
key=exchange_auth['key'],
|
||||||
|
secret=exchange_auth['secret'],
|
||||||
|
base_currency=base_currency,
|
||||||
|
portfolio=portfolio
|
||||||
|
)
|
||||||
|
elif exchange_name == 'bittrex':
|
||||||
|
exchange = Bittrex(
|
||||||
|
key=exchange_auth['key'],
|
||||||
|
secret=exchange_auth['secret'],
|
||||||
|
base_currency=base_currency,
|
||||||
|
portfolio=portfolio
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
raise NotImplementedError(
|
||||||
|
'exchange not supported: %s' % exchange_name)
|
||||||
|
|
||||||
open_calendar = get_calendar('OPEN')
|
open_calendar = get_calendar('OPEN')
|
||||||
sim_params = create_simulation_parameters(
|
sim_params = create_simulation_parameters(
|
||||||
start=start,
|
start=start,
|
||||||
@@ -154,7 +189,65 @@ def _run(handle_data,
|
|||||||
data_frequency=data_frequency,
|
data_frequency=data_frequency,
|
||||||
emission_rate=data_frequency,
|
emission_rate=data_frequency,
|
||||||
)
|
)
|
||||||
if bundle is not None:
|
|
||||||
|
if live and exchange is not None:
|
||||||
|
env = TradingEnvironment(
|
||||||
|
environ=environ,
|
||||||
|
exchange_tz='UTC',
|
||||||
|
asset_db_path=None
|
||||||
|
)
|
||||||
|
env.asset_finder = AssetFinderExchange(exchange)
|
||||||
|
|
||||||
|
data = DataPortalExchange(
|
||||||
|
exchange=exchange,
|
||||||
|
asset_finder=env.asset_finder,
|
||||||
|
trading_calendar=open_calendar,
|
||||||
|
first_trading_day=pd.to_datetime('today', utc=True)
|
||||||
|
)
|
||||||
|
choose_loader = None
|
||||||
|
|
||||||
|
def fetch_capital_base(attempt_index=0):
|
||||||
|
"""
|
||||||
|
Fetch the base currency amount required to bootstrap
|
||||||
|
the algorithm against the exchange.
|
||||||
|
|
||||||
|
The algorithm cannot continue without this value.
|
||||||
|
|
||||||
|
:param attempt_index:
|
||||||
|
:return capital_base: the amount of base currency available for
|
||||||
|
trading
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
log.debug('retrieving capital base in {} to bootstrap '
|
||||||
|
'exchange {}'.format(base_currency, exchange_name))
|
||||||
|
balances = exchange.get_balances()
|
||||||
|
except ExchangeRequestError as e:
|
||||||
|
if attempt_index < 20:
|
||||||
|
sleep(5)
|
||||||
|
return fetch_capital_base(attempt_index + 1)
|
||||||
|
else:
|
||||||
|
raise ExchangeRequestErrorTooManyAttempts(
|
||||||
|
attempts=attempt_index,
|
||||||
|
error=e
|
||||||
|
)
|
||||||
|
|
||||||
|
if base_currency in balances:
|
||||||
|
return balances[base_currency]
|
||||||
|
else:
|
||||||
|
raise BaseCurrencyNotFoundError(
|
||||||
|
base_currency=base_currency,
|
||||||
|
exchange=exchange_name
|
||||||
|
)
|
||||||
|
|
||||||
|
sim_params = create_simulation_parameters(
|
||||||
|
start=start,
|
||||||
|
end=end,
|
||||||
|
capital_base=fetch_capital_base(),
|
||||||
|
emission_rate='minute',
|
||||||
|
data_frequency='minute'
|
||||||
|
)
|
||||||
|
|
||||||
|
elif bundle is not None:
|
||||||
bundles = bundle.split(',')
|
bundles = bundle.split(',')
|
||||||
|
|
||||||
def get_trading_env_and_data(bundles):
|
def get_trading_env_and_data(bundles):
|
||||||
@@ -184,7 +277,7 @@ def _run(handle_data,
|
|||||||
)
|
)
|
||||||
|
|
||||||
env = TradingEnvironment(
|
env = TradingEnvironment(
|
||||||
load=partial(load_crypto_market_data, environ=environ),
|
load=partial(load_crypto_market_data, bundle=b, bundle_data=bundle_data, environ=environ),
|
||||||
bm_symbol='USDT_BTC',
|
bm_symbol='USDT_BTC',
|
||||||
trading_calendar=open_calendar,
|
trading_calendar=open_calendar,
|
||||||
asset_db_path=connstr,
|
asset_db_path=connstr,
|
||||||
@@ -239,52 +332,6 @@ def _run(handle_data,
|
|||||||
"No PipelineLoader registered for column %s." % column
|
"No PipelineLoader registered for column %s." % column
|
||||||
)
|
)
|
||||||
|
|
||||||
else:
|
|
||||||
if live and exchange is not None:
|
|
||||||
env = TradingEnvironment(
|
|
||||||
environ=environ,
|
|
||||||
exchange_tz="UTC",
|
|
||||||
asset_db_path=None
|
|
||||||
)
|
|
||||||
env.asset_finder = AssetFinderExchange(exchange)
|
|
||||||
|
|
||||||
data = DataPortalExchange(
|
|
||||||
exchange=exchange,
|
|
||||||
asset_finder=env.asset_finder,
|
|
||||||
trading_calendar=open_calendar,
|
|
||||||
first_trading_day=pd.to_datetime('today', utc=True)
|
|
||||||
)
|
|
||||||
choose_loader = None
|
|
||||||
|
|
||||||
def update_portfolio(attempt_index=0):
|
|
||||||
"""
|
|
||||||
Fetch the portfolio for the exchange
|
|
||||||
We can't continue on error because it is required to bootstrap
|
|
||||||
the algorithm.
|
|
||||||
:param attempt_index:
|
|
||||||
:return:
|
|
||||||
"""
|
|
||||||
try:
|
|
||||||
exchange.update_portfolio()
|
|
||||||
return exchange.portfolio
|
|
||||||
except ExchangeRequestError as e:
|
|
||||||
if attempt_index < 20:
|
|
||||||
sleep(5)
|
|
||||||
return update_portfolio(attempt_index + 1)
|
|
||||||
else:
|
|
||||||
raise ExchangeRequestErrorTooManyAttempts(
|
|
||||||
attempts=attempt_index,
|
|
||||||
error=e
|
|
||||||
)
|
|
||||||
|
|
||||||
portfolio = update_portfolio()
|
|
||||||
sim_params = create_simulation_parameters(
|
|
||||||
start=start,
|
|
||||||
end=end,
|
|
||||||
capital_base=portfolio.starting_cash,
|
|
||||||
emission_rate='minute',
|
|
||||||
data_frequency='minute'
|
|
||||||
)
|
|
||||||
else:
|
else:
|
||||||
env = TradingEnvironment(environ=environ)
|
env = TradingEnvironment(environ=environ)
|
||||||
choose_loader = None
|
choose_loader = None
|
||||||
@@ -462,12 +509,9 @@ def run_algorithm(initialize,
|
|||||||
--------
|
--------
|
||||||
catalyst.data.bundles.bundles : The available data bundles.
|
catalyst.data.bundles.bundles : The available data bundles.
|
||||||
"""
|
"""
|
||||||
mode = 'live' if live else 'backtest'
|
|
||||||
log.info('running algo in {mode} mode'.format(mode=mode))
|
|
||||||
load_extensions(default_extension, extensions, strict_extensions, environ)
|
load_extensions(default_extension, extensions, strict_extensions, environ)
|
||||||
|
|
||||||
exchange = None
|
if not live:
|
||||||
if mode == 'backtest':
|
|
||||||
non_none_data = valfilter(bool, {
|
non_none_data = valfilter(bool, {
|
||||||
'data': data is not None,
|
'data': data is not None,
|
||||||
'bundle': bundle is not None,
|
'bundle': bundle is not None,
|
||||||
@@ -486,30 +530,6 @@ def run_algorithm(initialize,
|
|||||||
raise ValueError(
|
raise ValueError(
|
||||||
'cannot specify `bundle_timestamp` without passing `bundle`',
|
'cannot specify `bundle_timestamp` without passing `bundle`',
|
||||||
)
|
)
|
||||||
else:
|
|
||||||
if exchange_name is not None:
|
|
||||||
portfolio = get_algo_object(
|
|
||||||
algo_name=algo_namespace,
|
|
||||||
key='portfolio_{}'.format(exchange_name),
|
|
||||||
environ=environ
|
|
||||||
)
|
|
||||||
if portfolio is None:
|
|
||||||
portfolio = ExchangePortfolio(
|
|
||||||
start_date=pd.Timestamp.utcnow()
|
|
||||||
)
|
|
||||||
|
|
||||||
exchange_auth = get_exchange_auth(exchange_name)
|
|
||||||
if exchange_name == 'bitfinex':
|
|
||||||
exchange = Bitfinex(
|
|
||||||
key=exchange_auth['key'],
|
|
||||||
secret=exchange_auth['secret'].encode('UTF-8'),
|
|
||||||
base_currency=base_currency,
|
|
||||||
portfolio=portfolio
|
|
||||||
)
|
|
||||||
else:
|
|
||||||
raise NotImplementedError(
|
|
||||||
'exchange not supported: %s' % exchange_name)
|
|
||||||
|
|
||||||
return _run(
|
return _run(
|
||||||
handle_data=handle_data,
|
handle_data=handle_data,
|
||||||
initialize=initialize,
|
initialize=initialize,
|
||||||
@@ -530,7 +550,7 @@ def run_algorithm(initialize,
|
|||||||
local_namespace=False,
|
local_namespace=False,
|
||||||
environ=environ,
|
environ=environ,
|
||||||
live=live,
|
live=live,
|
||||||
exchange=exchange,
|
exchange=exchange_name,
|
||||||
algo_namespace=algo_namespace,
|
algo_namespace=algo_namespace,
|
||||||
base_currency=base_currency
|
base_currency=base_currency
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -0,0 +1,105 @@
|
|||||||
|
<h1>Live Trading</h1>
|
||||||
|
This document explains how to get started with live trading.
|
||||||
|
|
||||||
|
<h2>Supported Exchanges</h2>
|
||||||
|
Catalyst can trade against these exchanges:
|
||||||
|
|
||||||
|
* Bitfinex, id=`bitfinex`
|
||||||
|
* Bittrex, id=`bittrex`
|
||||||
|
|
||||||
|
<h3>Authentication</h3>
|
||||||
|
Most exchanges require key/token combination for authentication. By
|
||||||
|
convention, Catalyst uses an "auth.json" file to hold this data.
|
||||||
|
|
||||||
|
This example illustrates the convention using the Bitfinex exchange.
|
||||||
|
Here is how to generate key and secret values for bitfinex:
|
||||||
|
https://docs.bitfinex.com/v1/docs/api-access. Most exchanges follow
|
||||||
|
a similar process.
|
||||||
|
|
||||||
|
The auth.json file:
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"name": "bitfinex",
|
||||||
|
"key": "my-key",
|
||||||
|
"secret": "my-secret"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
The file goes here:
|
||||||
|
```
|
||||||
|
~/.catalyst/data/exchanges/bitfinex/auth.json
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the 'bitfinex' directory corresponds to the id of the Bitfinex
|
||||||
|
exchange as defined in the "Supported Exchanges" section above.
|
||||||
|
Attempting to run an algorithm where the targeted exchange is missing
|
||||||
|
its "auth.json" file will create the directory structure but result
|
||||||
|
in an error.
|
||||||
|
|
||||||
|
<h3>Currency Symbols</h3>
|
||||||
|
Catalyst introduces a universal convention to reference
|
||||||
|
trading pairs and individual currencies. This
|
||||||
|
is required to ensure that the `symbol()` api predictably
|
||||||
|
returns the correct asset regardless of the targeted exchange.
|
||||||
|
|
||||||
|
Exchanges tend to use their own convention to represent currencies
|
||||||
|
(e.g. XBT and BTC both represent Bitcoin on different exchanges).
|
||||||
|
Trading pairs are also inconsistent. For example, Bitfinex
|
||||||
|
puts the market currency before the base currency without a
|
||||||
|
separator, Bittrex puts the base currency first and uses a dash
|
||||||
|
seperator.
|
||||||
|
|
||||||
|
Here is the Catalyst convention:
|
||||||
|
|
||||||
|
*[Market Currency]_[Base Currency]* all lowercase.
|
||||||
|
|
||||||
|
Currency symbols (e.g. btc, eth, ltc) follow the Bittrex convention.
|
||||||
|
|
||||||
|
Here are some examples:
|
||||||
|
```python
|
||||||
|
# With Bitfinex
|
||||||
|
bitcoin_usd_asset = symbol('btc_usd')
|
||||||
|
ethereum_bitcoin_asset = symbol('eth_btc')
|
||||||
|
|
||||||
|
# With Bittrex
|
||||||
|
ethereum_bitcoin_asset = symbol('eth_btc')
|
||||||
|
neo_ethereum_asset = symbol('neo_eth)
|
||||||
|
```
|
||||||
|
|
||||||
|
Note that the trading pairs are always referenced in the same manner.
|
||||||
|
However, not all trading pairs are available on all exchanges. An
|
||||||
|
error will occur if the specified trading pair is not trading
|
||||||
|
on the exchange.
|
||||||
|
|
||||||
|
<h2>Trading an Algorithm</h2>
|
||||||
|
There is no special convention to follow when writing an
|
||||||
|
algorithm for live trading. The same algorithm should work in
|
||||||
|
backtest and live execution mode without modification.
|
||||||
|
|
||||||
|
What differs are the arguments provided to the catalyst client or
|
||||||
|
`run_algorithm()` interface. Here is example:
|
||||||
|
|
||||||
|
```python
|
||||||
|
run_algorithm(
|
||||||
|
initialize=initialize,
|
||||||
|
handle_data=handle_data,
|
||||||
|
analyze=analyze,
|
||||||
|
exchange_name='bitfinex',
|
||||||
|
live=True,
|
||||||
|
algo_namespace='my_algo_trading_xrp',
|
||||||
|
base_currency='btc'
|
||||||
|
)
|
||||||
|
```
|
||||||
|
|
||||||
|
Here is the breakdown of the new arguments:
|
||||||
|
* live: Boolean flag which enables live trading.
|
||||||
|
* exchange_name: The name of the targeted exchange
|
||||||
|
(supported values: *bitfinex*, *bittrex*).
|
||||||
|
* algo_namespace: A arbitrary label assigned to your algorithm for
|
||||||
|
data storage purposes.
|
||||||
|
* base_currency: The base currency used to calculate the
|
||||||
|
statistics of your algorithm. Currently, the base currency of all
|
||||||
|
trading pairs of your algorithm must match this value.
|
||||||
|
|
||||||
|
Here is a complete algorithm for reference:
|
||||||
|
[Buy Low and Sell High](../catalyst/examples/buy_low_sell_high_live.py)
|
||||||
@@ -9,7 +9,9 @@ Logbook==0.12.5
|
|||||||
# Scientific Libraries
|
# Scientific Libraries
|
||||||
|
|
||||||
pytz==2016.4
|
pytz==2016.4
|
||||||
numpy==1.11.1
|
|
||||||
|
# FF: Upgraded numpy because of errors with version 1.11
|
||||||
|
numpy==1.13.1
|
||||||
|
|
||||||
# for pandas-datareader
|
# for pandas-datareader
|
||||||
requests-file==1.4.1
|
requests-file==1.4.1
|
||||||
@@ -77,3 +79,4 @@ lru-dict==1.1.4
|
|||||||
empyrical==0.2.1
|
empyrical==0.2.1
|
||||||
|
|
||||||
tables==3.3.0
|
tables==3.3.0
|
||||||
|
|
||||||
|
|||||||
@@ -38,6 +38,7 @@ class LazyBuildExtCommandClass(dict):
|
|||||||
Lazy command class that defers operations requiring Cython and numpy until
|
Lazy command class that defers operations requiring Cython and numpy until
|
||||||
they've actually been downloaded and installed by setup_requires.
|
they've actually been downloaded and installed by setup_requires.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def __contains__(self, key):
|
def __contains__(self, key):
|
||||||
return (
|
return (
|
||||||
key == 'build_ext'
|
key == 'build_ext'
|
||||||
@@ -62,6 +63,7 @@ class LazyBuildExtCommandClass(dict):
|
|||||||
Custom build_ext command that lazily adds numpy's include_dir to
|
Custom build_ext command that lazily adds numpy's include_dir to
|
||||||
extensions.
|
extensions.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
def build_extensions(self):
|
def build_extensions(self):
|
||||||
"""
|
"""
|
||||||
Lazily append numpy's include directory to Extension includes.
|
Lazily append numpy's include directory to Extension includes.
|
||||||
@@ -75,6 +77,7 @@ class LazyBuildExtCommandClass(dict):
|
|||||||
ext.include_dirs.append(numpy_incl)
|
ext.include_dirs.append(numpy_incl)
|
||||||
|
|
||||||
super(build_ext, self).build_extensions()
|
super(build_ext, self).build_extensions()
|
||||||
|
|
||||||
return build_ext
|
return build_ext
|
||||||
|
|
||||||
|
|
||||||
@@ -100,7 +103,8 @@ ext_modules = [
|
|||||||
window_specialization('label'),
|
window_specialization('label'),
|
||||||
Extension('catalyst.lib.rank', ['catalyst/lib/rank.pyx']),
|
Extension('catalyst.lib.rank', ['catalyst/lib/rank.pyx']),
|
||||||
Extension('catalyst.data._equities', ['catalyst/data/_equities.pyx']),
|
Extension('catalyst.data._equities', ['catalyst/data/_equities.pyx']),
|
||||||
Extension('catalyst.data._adjustments', ['catalyst/data/_adjustments.pyx']),
|
Extension('catalyst.data._adjustments',
|
||||||
|
['catalyst/data/_adjustments.pyx']),
|
||||||
Extension('catalyst._protocol', ['catalyst/_protocol.pyx']),
|
Extension('catalyst._protocol', ['catalyst/_protocol.pyx']),
|
||||||
Extension('catalyst.gens.sim_engine', ['catalyst/gens/sim_engine.pyx']),
|
Extension('catalyst.gens.sim_engine', ['catalyst/gens/sim_engine.pyx']),
|
||||||
Extension(
|
Extension(
|
||||||
@@ -117,7 +121,6 @@ ext_modules = [
|
|||||||
),
|
),
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|
||||||
STR_TO_CMP = {
|
STR_TO_CMP = {
|
||||||
'<': lt,
|
'<': lt,
|
||||||
'<=': le,
|
'<=': le,
|
||||||
@@ -212,7 +215,7 @@ def read_requirements(path,
|
|||||||
conda_format=False,
|
conda_format=False,
|
||||||
filter_names=None):
|
filter_names=None):
|
||||||
"""
|
"""
|
||||||
Read a requirements.txt file, expressed as a path relative to Zipline root.
|
Read a requirements.txt file, expressed as a path relative to Catalyst root.
|
||||||
|
|
||||||
Returns requirements with the pinned versions as lower bounds
|
Returns requirements with the pinned versions as lower bounds
|
||||||
if `strict_bounds` is falsey.
|
if `strict_bounds` is falsey.
|
||||||
@@ -264,6 +267,7 @@ def setup_requirements(requirements_path, module_names, strict_bounds,
|
|||||||
)
|
)
|
||||||
return module_lines
|
return module_lines
|
||||||
|
|
||||||
|
|
||||||
conda_build = os.path.basename(sys.argv[0]) in ('conda-build', # unix
|
conda_build = os.path.basename(sys.argv[0]) in ('conda-build', # unix
|
||||||
'conda-build-script.py') # win
|
'conda-build-script.py') # win
|
||||||
|
|
||||||
|
|||||||
+13
-21
@@ -5,30 +5,14 @@ from abc import ABCMeta, abstractmethod
|
|||||||
class BaseExchangeTestCase():
|
class BaseExchangeTestCase():
|
||||||
__metaclass__ = ABCMeta
|
__metaclass__ = ABCMeta
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def test_positions(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def test_portfolio(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def test_account(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def test_time_skew(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
|
||||||
def test_get_open_orders(self):
|
|
||||||
pass
|
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def test_order(self):
|
def test_order(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def test_open_orders(self):
|
||||||
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def test_get_order(self):
|
def test_get_order(self):
|
||||||
pass
|
pass
|
||||||
@@ -38,9 +22,17 @@ class BaseExchangeTestCase():
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def test_get_spot_value(self):
|
def test_get_candles(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
@abstractmethod
|
@abstractmethod
|
||||||
def test_tickers(self):
|
def test_tickers(self):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def test_get_balances(self):
|
||||||
|
pass
|
||||||
|
|
||||||
|
@abstractmethod
|
||||||
|
def test_get_account(self):
|
||||||
|
pass
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
from catalyst.exchange.bitfinex import Bitfinex
|
from catalyst.exchange.bitfinex.bitfinex import Bitfinex
|
||||||
from .base import BaseExchangeTestCase
|
from .base import BaseExchangeTestCase
|
||||||
from logbook import Logger
|
from logbook import Logger
|
||||||
import pandas as pd
|
import pandas as pd
|
||||||
@@ -6,42 +6,27 @@ from catalyst.finance.execution import (MarketOrder,
|
|||||||
LimitOrder,
|
LimitOrder,
|
||||||
StopOrder,
|
StopOrder,
|
||||||
StopLimitOrder)
|
StopLimitOrder)
|
||||||
|
from catalyst.exchange.exchange_utils import get_exchange_auth
|
||||||
|
|
||||||
log = Logger('BitfinexTestCase')
|
log = Logger('test_bitfinex')
|
||||||
|
|
||||||
|
|
||||||
class BitfinexTestCase(BaseExchangeTestCase):
|
class BitfinexTestCase(BaseExchangeTestCase):
|
||||||
def test_positions(self):
|
@classmethod
|
||||||
log.info('querying positions from bitfinex')
|
def setup(self):
|
||||||
bitfinex = Bitfinex()
|
print ('creating bitfinex object')
|
||||||
balance = bitfinex.positions()
|
auth = get_exchange_auth('bitfinex')
|
||||||
log.info('the balance: {}'.format(balance))
|
self.exchange = Bitfinex(
|
||||||
pass
|
key=auth['key'],
|
||||||
|
secret=auth['secret'],
|
||||||
def test_portfolio(self):
|
base_currency='usd'
|
||||||
log.info('fetching portfolio data')
|
)
|
||||||
pass
|
|
||||||
|
|
||||||
def test_account(self):
|
|
||||||
log.info('fetching account data')
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_time_skew(self):
|
|
||||||
log.info('time skew not implemented')
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_get_open_orders(self):
|
|
||||||
log.info('fetching open orders')
|
|
||||||
bitfinex = Bitfinex()
|
|
||||||
order_id = bitfinex.get_open_orders()
|
|
||||||
log.info('open orders: {}'.format(order_id))
|
|
||||||
pass
|
|
||||||
|
|
||||||
def test_order(self):
|
def test_order(self):
|
||||||
log.info('ordering from bitfinex')
|
log.info('creating order')
|
||||||
bitfinex = Bitfinex()
|
asset = self.exchange.get_asset('eth_usd')
|
||||||
order_id = bitfinex.order(
|
order_id = self.exchange.order(
|
||||||
asset=bitfinex.get_asset('eth_usd'),
|
asset=asset,
|
||||||
style=LimitOrder(limit_price=200),
|
style=LimitOrder(limit_price=200),
|
||||||
limit_price=200,
|
limit_price=200,
|
||||||
amount=0.5,
|
amount=0.5,
|
||||||
@@ -50,45 +35,36 @@ class BitfinexTestCase(BaseExchangeTestCase):
|
|||||||
log.info('order created {}'.format(order_id))
|
log.info('order created {}'.format(order_id))
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
def test_open_orders(self):
|
||||||
|
log.info('retrieving open orders')
|
||||||
|
orders = self.exchange.get_open_orders()
|
||||||
|
pass
|
||||||
|
|
||||||
def test_get_order(self):
|
def test_get_order(self):
|
||||||
log.info('querying orders from bitfinex')
|
log.info('retrieving order')
|
||||||
bitfinex = Bitfinex()
|
|
||||||
response = bitfinex.get_order(order_id=3361248395)
|
|
||||||
log.info('the order: {}'.format(response))
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_cancel_order(self):
|
def test_cancel_order(self):
|
||||||
log.info('canceling order from bitfinex')
|
log.info('cancel order')
|
||||||
bitfinex = Bitfinex()
|
|
||||||
response = bitfinex.cancel_order(order_id=3330847408)
|
|
||||||
log.info('canceled order: {}'.format(response))
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_get_spot_value(self):
|
def test_get_candles(self):
|
||||||
log.info('spot value not implemented')
|
log.info('retrieving candles')
|
||||||
bitfinex = Bitfinex()
|
|
||||||
assets = [
|
|
||||||
bitfinex.get_asset('eth_usd'),
|
|
||||||
bitfinex.get_asset('etc_usd'),
|
|
||||||
bitfinex.get_asset('eos_usd'),
|
|
||||||
]
|
|
||||||
# assets = bitfinex.get_asset('eth_usd')
|
|
||||||
value = bitfinex.get_spot_value(
|
|
||||||
assets=assets,
|
|
||||||
field='close',
|
|
||||||
data_frequency='minute'
|
|
||||||
)
|
|
||||||
pass
|
pass
|
||||||
|
|
||||||
def test_tickers(self):
|
def test_tickers(self):
|
||||||
log.info('fetching ticker from bitfinex')
|
log.info('retrieving tickers')
|
||||||
bitfinex = Bitfinex()
|
tickers = self.exchange.tickers([
|
||||||
current_date = pd.Timestamp.utcnow()
|
self.exchange.get_asset('eth_usd'),
|
||||||
assets = [
|
self.exchange.get_asset('btc_usd')
|
||||||
bitfinex.get_asset('eth_usd'),
|
])
|
||||||
bitfinex.get_asset('etc_usd'),
|
pass
|
||||||
bitfinex.get_asset('eos_usd'),
|
|
||||||
]
|
def test_get_account(self):
|
||||||
tickers = bitfinex.tickers(date=current_date, assets=assets)
|
log.info('retrieving account data')
|
||||||
log.info('got tickers {}'.format(tickers))
|
pass
|
||||||
|
|
||||||
|
def test_get_balances(self):
|
||||||
|
log.info('testing exchange balances')
|
||||||
|
balances = self.exchange.get_balances()
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -0,0 +1,83 @@
|
|||||||
|
from catalyst.exchange.bittrex.bittrex import Bittrex
|
||||||
|
from catalyst.finance.order import Order
|
||||||
|
from .base import BaseExchangeTestCase
|
||||||
|
from logbook import Logger
|
||||||
|
from catalyst.exchange.exchange_utils import get_exchange_auth
|
||||||
|
|
||||||
|
log = Logger('test_bittrex')
|
||||||
|
|
||||||
|
|
||||||
|
class BittrexTestCase(BaseExchangeTestCase):
|
||||||
|
@classmethod
|
||||||
|
def setup(self):
|
||||||
|
print ('creating bittrex object')
|
||||||
|
auth = get_exchange_auth('bittrex')
|
||||||
|
self.exchange = Bittrex(
|
||||||
|
key=auth['key'],
|
||||||
|
secret=auth['secret'],
|
||||||
|
base_currency='btc'
|
||||||
|
)
|
||||||
|
|
||||||
|
def test_order(self):
|
||||||
|
log.info('creating order')
|
||||||
|
asset = self.exchange.get_asset('neo_btc')
|
||||||
|
order_id = self.exchange.order(
|
||||||
|
asset=asset,
|
||||||
|
limit_price=0.0005,
|
||||||
|
amount=1,
|
||||||
|
)
|
||||||
|
log.info('order created {}'.format(order_id))
|
||||||
|
assert order_id is not None
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_open_orders(self):
|
||||||
|
log.info('retrieving open orders')
|
||||||
|
asset = self.exchange.get_asset('neo_btc')
|
||||||
|
orders = self.exchange.get_open_orders(asset)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_get_order(self):
|
||||||
|
log.info('retrieving order')
|
||||||
|
order = self.exchange.get_order(
|
||||||
|
u'2c584020-9caf-4af5-bde0-332c0bba17e2')
|
||||||
|
assert isinstance(order, Order)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_cancel_order(self, ):
|
||||||
|
log.info('cancel order')
|
||||||
|
self.exchange.cancel_order(u'dc7bcca2-5219-4145-8848-8a593d2a72f9')
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_get_candles(self):
|
||||||
|
log.info('retrieving candles')
|
||||||
|
ohlcv_neo = self.exchange.get_candles(
|
||||||
|
data_frequency='5m',
|
||||||
|
assets=self.exchange.get_asset('neo_btc')
|
||||||
|
)
|
||||||
|
ohlcv_neo_ubq = self.exchange.get_candles(
|
||||||
|
data_frequency='5m',
|
||||||
|
assets=[
|
||||||
|
self.exchange.get_asset('neo_btc'),
|
||||||
|
self.exchange.get_asset('ubq_btc')
|
||||||
|
],
|
||||||
|
bar_count=14
|
||||||
|
)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_tickers(self):
|
||||||
|
log.info('retrieving tickers')
|
||||||
|
tickers = self.exchange.tickers([
|
||||||
|
self.exchange.get_asset('ubq_btc'),
|
||||||
|
self.exchange.get_asset('neo_btc')
|
||||||
|
])
|
||||||
|
assert len(tickers) == 2
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_get_balances(self):
|
||||||
|
log.info('testing wallet balances')
|
||||||
|
balances = self.exchange.get_balances()
|
||||||
|
pass
|
||||||
|
|
||||||
|
def test_get_account(self):
|
||||||
|
log.info('testing account data')
|
||||||
|
pass
|
||||||
Reference in New Issue
Block a user