Compare commits

..
36 Commits
Author SHA1 Message Date
fredfortierandVictor Grau Serrat ad2d0e9253 Fixed path issue with obsolete branch 2017-09-07 13:26:09 -06:00
Victor Grau Serrat d64f5275ef Merge branch 'develop' 2017-09-03 11:46:57 -06:00
Victor Grau Serrat 4f4f6c050b Merge branch 'exchange-trading' into develop 2017-09-03 11:44:15 -06:00
fredfortier fdd6b62963 Removing run_algorithm() from examples 2017-09-03 13:25:47 -04:00
fredfortier bcb5fd2b14 Optimizing algorithm initialization 2017-09-03 13:05:18 -04:00
Victor Grau Serrat c5e1945558 fix command line run for exchange-trading 2017-09-01 22:35:24 -06:00
fredfortier 11144d83b8 Fixed issue with defining the exchange in run_algo 2017-09-01 11:34:38 -04:00
Victor Grau Serrat 85c2e9db4f Merge branch 'exchange-trading' of github.com:enigmampc/catalyst into exchange-trading 2017-09-01 09:28:48 -06:00
Victor Grau Serrat 817cb07bee minor fix reference-currency -> base-currency 2017-09-01 09:24:34 -06:00
fredfortier 8054d1d520 Fixed issue with spot price on Bittrex 2017-09-01 11:17:43 -04:00
fredfortier c1d7022846 Fixed issue with Bittrex order 2017-09-01 10:39:49 -04:00
fredfortier 8f3c440bac Created wiki documentation 2017-08-31 14:14:54 -04:00
fredfortier a785607d8f Fixed a bug with sell orders and added documentation 2017-08-31 13:15:43 -04:00
fredfortier 6e166383ed Fixed bug with order execution 2017-08-31 12:47:09 -04:00
fredfortier 1696c39912 Bug fix in symbol loader 2017-08-31 12:34:08 -04:00
fredfortier d79fdca561 Bug fix in symbol loader 2017-08-31 12:22:04 -04:00
fredfortier 8b6a48633d Poloshing unit tests and finalizing Bittrex implementation 2017-08-30 17:09:13 -04:00
fredfortier d03ce37f6e Poloshing unit tests and finalizing Bittrex implementation 2017-08-30 08:51:31 -04:00
fredfortier c47e88c26f More unit testing and refactoring related to the Bittrex addition 2017-08-29 16:05:38 -04:00
fredfortier f4db9f7b1e Working on Bittrex implementation and unit tests 2017-08-28 22:50:46 -04:00
fredfortier 753881bade Bug fixes and polishing stats 2017-08-28 22:00:31 -04:00
fredfortier 1be39f97a1 Refactoring to optimize multiple exchanges 2017-08-28 16:19:03 -04:00
Victor Grau Serrat 49bfd32341 Merge branch 'develop' 2017-08-28 13:20:20 -06:00
Victor Grau Serrat 01473e5146 Fixes 1000 scaling price issue 2017-08-28 13:15:31 -06:00
Victor Grau Serrat 16f9ab3ba5 Retrieve cryptobenchmark from bundle, instead of Polo 2017-08-28 12:20:54 -06:00
fredfortier b4755111a9 Extended Asset to create TradingPair allowing us to store leverage value 2017-08-28 11:25:01 -04:00
fredfortier cf54806843 Extended Asset to create TradingPair allowing us to store leverage value 2017-08-28 10:25:46 -04:00
fredfortier c01f2a39a4 Initial work on bittrex implementation 2017-08-28 00:27:10 -04:00
fredfortier 1cfcb1d96e Initial work on bittrex implementation 2017-08-27 23:26:48 -04:00
fredfortier c40fd98022 Adjusted common files 2017-08-27 18:06:48 -04:00
fredfortier b8d442cf89 Creating a clean branch for live trading 2017-08-27 15:19:13 -04:00
Victor Grau Serrat 3a44a3cc1f Date fix for treasury_data, starts 1990-01-02 2017-08-25 12:44:43 -06:00
Victor Grau Serrat 24fd0fa6f8 Fix issue #28: buy_and_hodl.py example 2017-08-25 09:29:25 -06:00
Victor Grau Serrat b2e5b5f73d Dropbox -> AWS switch for Poloniex bundle 2017-08-24 12:55:33 -06:00
Victor Grau Serrat 0ef8b341ca Merge branch 'develop' 2017-08-17 23:13:04 -06:00
Victor Grau Serrat 753ca1db5a fix issue #27: create_writers=False except when bundling 2017-08-17 23:06:32 -06:00
28 changed files with 1483 additions and 595 deletions
-1
View File
@@ -79,7 +79,6 @@ __all__ = [
'gens',
'run_algorithm',
'utils',
'exchange',
]
from ._version import get_versions
+10 -16
View File
@@ -181,12 +181,6 @@ def ipython_only(option):
default=False,
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(
'--local-namespace/--no-local-namespace',
is_flag=True,
@@ -202,18 +196,18 @@ def ipython_only(option):
@click.option(
'-x',
'--exchange-name',
type=click.Choice({'bitfinex'}),
help='The name of the exchange (supported: bitfinex).',
type=click.Choice({'bitfinex', 'bittrex'}),
help='The name of the targeted exchange (supported: bitfinex, bittrex).',
)
@click.option(
'-n',
'--algo-name',
help='A label assigned to the algorithm for tracking purposes.',
'--algo-namespace',
help='A label assigned to the algorithm for data storage purposes.'
)
@click.option(
'-c',
'--reference-currency',
help='The reference currency used to calculate statistics '
'--base-currency',
help='The base currency used to calculate statistics '
'(e.g. usd, btc, eth).',
)
@click.pass_context
@@ -245,7 +239,7 @@ def run(ctx,
ctx.fail("must specify an algorithm name '-n' in live execution "
"mode '--live'")
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'")
else:
# check that the start and end dates are passed correctly
@@ -387,14 +381,14 @@ def ingest(bundle, compile_locally, assets_version, show_progress):
'--before',
type=Timestamp(),
help='Clear all data before TIMESTAMP.'
' This may not be passed with -k / --keep-last',
' This may not be passed with -k / --keep-last',
)
@click.option(
'-a',
'--after',
type=Timestamp(),
help='Clear all data after TIMESTAMP'
' This may not be passed with -k / --keep-last',
' This may not be passed with -k / --keep-last',
)
@click.option(
'-k',
@@ -402,7 +396,7 @@ def ingest(bundle, compile_locally, assets_version, show_progress):
type=int,
metavar='N',
help='Clear all but the last N downloads.'
' This may not be passed with -e / --before or -a / --after',
' This may not be passed with -e / --before or -a / --after',
)
def clean(bundle, before, after, keep_last):
"""Clean up data downloaded with the ingest command.
-2
View File
@@ -1138,8 +1138,6 @@ class TradingAlgorithm(object):
freq = self.sim_params.data_frequency
freq = self.sim_params.data_frequency
date_rule = date_rule or date_rules.every_day()
if freq is 'daily':
# ignore time rule in daily mode
+173 -23
View File
@@ -20,31 +20,31 @@ Cythonized Asset object.
cimport cython
from cpython.number cimport PyNumber_Index
from cpython.object cimport (
Py_EQ,
Py_NE,
Py_GE,
Py_LE,
Py_GT,
Py_LT,
Py_EQ,
Py_NE,
Py_GE,
Py_LE,
Py_GT,
Py_LT,
)
from cpython cimport bool
import pandas as pd
from datetime import timedelta
import numpy as np
from numpy cimport int64_t
import warnings
cimport numpy as np
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
# Asset.__reduce__, or else we'll attempt to unpickle an old version of this
# class
CACHE_FILE_TEMPLATE = '/tmp/.%s-%s.v7.cache'
cdef class Asset:
cdef readonly int sid
# Cached hash of self.sid
cdef int sid_hash
@@ -73,8 +73,8 @@ cdef class Asset:
})
def __init__(self,
int sid, # sid is required
object exchange, # exchange is required
int sid, # sid is required
object exchange, # exchange is required
object symbol="",
object asset_name="",
object start_date=None,
@@ -230,9 +230,7 @@ cdef class Asset:
calendar = get_calendar(self.exchange)
return calendar.is_open_on_minute(dt_minute)
cdef class Equity(Asset):
def __repr__(self):
attrs = ('symbol', 'asset_name', 'exchange',
'start_date', 'end_date', 'first_traded', 'auto_close_date',
@@ -250,8 +248,8 @@ cdef class Equity(Asset):
"""
def __get__(self):
warnings.warn("The security_start_date property will soon be "
"retired. Please use the start_date property instead.",
DeprecationWarning)
"retired. Please use the start_date property instead.",
DeprecationWarning)
return self.start_date
property security_end_date:
@@ -261,8 +259,8 @@ cdef class Equity(Asset):
"""
def __get__(self):
warnings.warn("The security_end_date property will soon be "
"retired. Please use the end_date property instead.",
DeprecationWarning)
"retired. Please use the end_date property instead.",
DeprecationWarning)
return self.end_date
property security_name:
@@ -272,13 +270,11 @@ cdef class Equity(Asset):
"""
def __get__(self):
warnings.warn("The security_name property will soon be "
"retired. Please use the asset_name property instead.",
DeprecationWarning)
"retired. Please use the asset_name property instead.",
DeprecationWarning)
return self.asset_name
cdef class Future(Asset):
cdef readonly object root_symbol
cdef readonly object notice_date
cdef readonly object expiration_date
@@ -303,8 +299,8 @@ cdef class Future(Asset):
})
def __init__(self,
int sid, # sid is required
object exchange, # exchange is required
int sid, # sid is required
object exchange, # exchange is required
object symbol="",
object root_symbol="",
object asset_name="",
@@ -388,6 +384,160 @@ cdef class Future(Asset):
super_dict['multiplier'] = self.multiplier
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):
cdef np.ndarray out = np.empty([size], dtype=object)
+6 -4
View File
@@ -42,8 +42,7 @@ class PoloniexBundle(BaseCryptoPricingBundle):
@lazyval
def tar_url(self):
return (
'https://www.dropbox.com/s/9naqffawnq8o4r2/'
'poloniex-bundle.tar?dl=1'
'https://s3.amazonaws.com/enigmaco/catalyst-bundles/poloniex/poloniex-bundle.tar.gz'
)
@lazyval
@@ -103,7 +102,10 @@ class PoloniexBundle(BaseCryptoPricingBundle):
)
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[:, 'high'] /= 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):
register_bundle(PoloniexBundle)
'''
register_bundle(PoloniexBundle)
register_bundle(PoloniexBundle, create_writers=False)
+65 -32
View File
@@ -96,16 +96,15 @@ def has_data_for_dates(series_or_df, first_date, last_date):
first, last = dts[[0, -1]].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,
trading_days=None,
bm_symbol='USDT_BTC',
environ=None):
def load_crypto_market_data(trading_day=None, trading_days=None, bm_symbol='USDT_BTC',
bundle=None, bundle_data=None, environ=None):
if trading_day is None:
trading_day = get_calendar('OPEN').trading_day
if trading_days is None:
trading_days = get_calendar('OPEN').all_sessions
first_date = trading_days[0]
first_date = trading_days[1]
now = pd.Timestamp.utcnow()
# We expect to have benchmark and treasury data that's current up until
@@ -122,8 +121,15 @@ def load_crypto_market_data(trading_day=None,
# We'll attempt to download new data if the latest entry in our cache is
# before this date.
last_date = trading_days[trading_days.get_loc(now, method='ffill') - 2]
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]
br = ensure_crypto_benchmark_data(
bm_symbol,
first_date,
@@ -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
# date so that we can compute returns for the first date.
trading_day,
bundle,
bundle_data,
environ,
)
# Override first_date for treasury data since we have it for many more years
# 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(
bm_symbol,
first_date_treasury,
@@ -240,6 +248,8 @@ def ensure_crypto_benchmark_data(symbol,
last_date,
now,
trading_day,
bundle,
bundle_data,
environ=None):
filename = get_benchmark_filename(symbol)
@@ -248,7 +258,7 @@ def ensure_crypto_benchmark_data(symbol,
('Loading benchmark data for {symbol!r} '
'from {first_date} to {last_date}'),
symbol=symbol,
first_date=first_date - trading_day,
first_date=first_date,
last_date=last_date
)
@@ -261,34 +271,57 @@ def ensure_crypto_benchmark_data(symbol,
environ,
)
if data is not None:
return data
# If no cached data was found or it was missing any dates then download the
# necessary data.
logger.info(
('Downloading benchmark data for {symbol!r} '
'from {first_date} to {last_date}'),
symbol=symbol,
first_date=first_date - trading_day,
last_date=last_date
)
# Load benchmark symbol from Poloniex API
try:
bundle = PoloniexBundle()
bench_raw = bundle._fetch_symbol_frame(
None,
symbol,
get_calendar(bundle.calendar_name),
first_date - trading_day,
last_date,
'daily',
)
except (OSError, IOError, HTTPError):
logger.exception('Failed to fetch new crypto benchmark returns')
raise
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(
('Retrieving benchmark data from bundle for {symbol!r} from {first_date} to {last_date}'),
symbol=symbol, first_date=first_date, last_date=last_date)
asset = bundle_data.asset_finder.lookup_symbol(symbol=symbol,as_of_date=None)
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
try:
bundle = PoloniexBundle()
bench_raw = bundle._fetch_symbol_frame(
None,
symbol,
get_calendar(bundle.calendar_name),
first_date - trading_day,
last_date,
'daily',
)
except (OSError, IOError, HTTPError):
logger.exception('Failed to fetch new crypto benchmark returns')
raise
# select close column and compute percent change between days
daily_close = bench_raw[['close']]
@@ -518,7 +551,7 @@ def _load_cached_data(filename, first_date, last_date, now, resource_name,
)
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,
end=last_date,
path=path,
+12 -9
View File
@@ -23,7 +23,6 @@ from catalyst.api import (
get_open_orders,
)
def initialize(context):
context.ASSET_NAME = 'USDT_BTC'
context.TARGET_HODL_RATIO = 0.8
@@ -42,8 +41,6 @@ def initialize(context):
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
@@ -73,6 +70,7 @@ def handle_data(context, data):
record(
price=price,
volume=data[context.asset].volume,
cash=cash,
starting_cash=context.portfolio.starting_cash,
leverage=context.account.leverage,
@@ -80,12 +78,13 @@ def handle_data(context, data):
def analyze(context=None, results=None):
import matplotlib.pyplot as plt
# Plot the portfolio and asset data.
ax1 = plt.subplot(511)
ax1 = plt.subplot(611)
results[['portfolio_value']].plot(ax=ax1)
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))
(context.TICK_SIZE * results[['price']]).plot(ax=ax2)
@@ -101,11 +100,11 @@ def analyze(context=None, results=None):
color='g',
)
ax3 = plt.subplot(513, sharex=ax1)
ax3 = plt.subplot(613, sharex=ax1)
results[['leverage', 'alpha', 'beta']].plot(ax=ax3)
ax3.set_ylabel('Leverage ')
ax4 = plt.subplot(514, sharex=ax1)
ax4 = plt.subplot(614, sharex=ax1)
results[['starting_cash', 'cash']].plot(ax=ax4)
ax4.set_ylabel('Cash (USD)')
@@ -119,7 +118,7 @@ def analyze(context=None, results=None):
'benchmark_period_return',
]]
ax5 = plt.subplot(515, sharex=ax1)
ax5 = plt.subplot(615, sharex=ax1)
results[[
'treasury',
'algorithm',
@@ -127,8 +126,12 @@ def analyze(context=None, results=None):
]].plot(ax=ax5)
ax5.set_ylabel('Percent Change')
ax6 = plt.subplot(616, sharex=ax1)
results[['volume']].plot(ax=ax6)
ax6.set_ylabel('Volume (mCoins/5min)')
plt.legend(loc=3)
# Show the plot.
plt.gcf().set_size_inches(18, 8)
plt.show()
plt.show()
-79
View File
@@ -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
from logbook import Logger
@@ -8,9 +19,9 @@ from catalyst.api import (
record,
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)
@@ -21,7 +32,7 @@ def initialize(context):
context.TARGET_POSITIONS = 5000
context.PROFIT_TARGET = 0.1
context.SLIPPAGE_ALLOWED = 0.02
context.SLIPPAGE_ALLOWED = 0.05
context.retry_check_open_orders = 10
context.retry_update_portfolio = 10
@@ -140,16 +151,5 @@ def handle_data(context, data):
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
run_algorithm(
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bitfinex',
live=True,
algo_namespace=algo_namespace,
base_currency='usd'
)
+33 -18
View File
@@ -18,6 +18,7 @@ from datetime import timedelta
from time import sleep
from os import listdir
from os.path import isfile, join
from collections import deque
import logbook
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, \
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.gens.tradesimulation import AlgorithmSimulator
from catalyst.utils.api_support import (
@@ -55,16 +57,19 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
self.exchange = kwargs.pop('exchange', None)
self.algo_namespace = kwargs.pop('algo_namespace', None)
self.orders = {}
self.minute_stats = deque(maxlen=60)
self.is_running = True
self.retry_check_open_orders = 5
self.retry_update_portfolio = 5
self.retry_synchronize_portfolio = 5
self.retry_get_open_orders = 5
self.retry_order = 2
self.retry_delay = 5
self.stats_minutes = 5
super(self.__class__, self).__init__(*args, **kwargs)
self._create_minute_writer()
# self._create_minute_writer()
signal.signal(signal.SIGINT, self.signal_handler)
@@ -93,10 +98,14 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
def signal_handler(self, signal, frame):
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)
folder = join(algo_folder, 'daily_perf')
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))
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)
def _create_clock(self):
@@ -169,9 +175,9 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
def updated_account(self):
return self.exchange.account
def _update_portfolio(self, attempt_index=0):
def _synchronize_portfolio(self, attempt_index=0):
try:
self.exchange.update_portfolio()
self.exchange.synchronize_portfolio()
# Applying the updated last_sales_price to the positions
# in the performance tracker. This seems a bit redundant
@@ -189,9 +195,9 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
log.warn(
'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)
self._update_portfolio(attempt_index + 1)
self._synchronize_portfolio(attempt_index + 1)
else:
raise ExchangePortfolioDataError(
data_type='update-portfolio',
@@ -287,7 +293,7 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
if not self.is_running:
return
self._update_portfolio()
self._synchronize_portfolio()
transactions = self._check_open_orders()
for transaction in transactions:
@@ -306,10 +312,17 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
# Performance tracker and keep only minute and cumulative
self.perf_tracker.update_performance()
# TODO: save for future use?
minute_stats = self.prepare_period_stats(
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)
daily_stats = self.prepare_period_stats(
@@ -384,9 +397,11 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
style)
order_id = self._order(asset, amount, limit_price, stop_price, style)
order = self.portfolio.open_orders[order_id]
self.perf_tracker.process_order(order)
if order_id is not None:
order = self.portfolio.open_orders[order_id]
self.perf_tracker.process_order(order)
return order
def round_order(self, amount):
+1 -1
View File
@@ -81,7 +81,7 @@ class AssetFinderExchange(object):
there are multiple candidates for the given ``symbol`` on the
``as_of_date``.
"""
log.info('looking up symbol: {}'.format(symbol))
log.debug('looking up symbol: {}'.format(symbol))
if symbol in self._asset_cache:
return self._asset_cache[symbol]
+65 -183
View File
@@ -1,28 +1,26 @@
import base64
import numpy as np
import hashlib
import hmac
import json
import re
import time
import numpy as np
import pandas as pd
import pytz
import requests
import six
from catalyst.assets._assets import Asset
from catalyst.assets._assets import TradingPair
from logbook import Logger
# from websocket import create_connection
from catalyst.exchange.exchange import Exchange
from catalyst.exchange.exchange_errors import (
ExchangeRequestError,
InvalidHistoryFrequencyError
)
from catalyst.finance.execution import (MarketOrder,
LimitOrder,
StopOrder,
StopLimitOrder)
InvalidHistoryFrequencyError,
InvalidOrderStyle, OrderCancelError)
from catalyst.exchange.exchange_execution import ExchangeLimitOrder, \
ExchangeStopLimitOrder, ExchangeStopOrder
from catalyst.finance.order import Order, ORDER_STATUS
from catalyst.protocol import Account
@@ -40,8 +38,7 @@ class Bitfinex(Exchange):
def __init__(self, key, secret, base_currency, portfolio=None):
self.url = BITFINEX_URL
self.key = key
self.secret = secret
self.id = 'b'
self.secret = secret.encode('UTF-8')
self.name = 'bitfinex'
self.assets = {}
self.load_assets()
@@ -135,7 +132,10 @@ class Bitfinex(Exchange):
amount = float(order_status['original_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'])
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.
commission = None
# TODO: zipline likes rounded dates to match statistics, is this ok?
date = pd.Timestamp.utcfromtimestamp(float(order_status['timestamp']))
date = pytz.utc.localize(date)
order = Order(
@@ -164,20 +163,15 @@ class Bitfinex(Exchange):
stop=stop_price,
limit=limit_price,
filled=filled,
id=order_status['id'],
id=str(order_status['id']),
commission=commission
)
order.status = status
return order, executed_price
def update_portfolio(self):
"""
Update the portfolio cash and position balances based on the
latest ticker prices.
:return:
"""
def get_balances(self):
log.debug('retrieving wallets balances')
try:
response = self._request('balances', None)
balances = response.json()
@@ -189,56 +183,12 @@ class Bitfinex(Exchange):
error='unable to fetch balance {}'.format(balances['message'])
)
base_position = None
for position in balances:
if not base_position and position['type'] == 'exchange' \
and position['currency'] == self.base_currency:
base_position = position
std_balances = dict()
for balance in balances:
currency = balance['currency'].lower()
std_balances[currency] = float(balance['available'])
if position is None:
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
return std_balances
@property
def account(self):
@@ -264,17 +214,14 @@ class Bitfinex(Exchange):
return account
@property
def positions(self):
return self.portfolio.positions
@property
def time_skew(self):
# TODO: research the time skew conditions
return pd.Timedelta('0s')
def subscribe_to_market_data(self, symbol):
pass
def get_account(self):
# TODO: fetch account data and keep in cache
return None
def get_candles(self, data_frequency, assets, bar_count=None):
"""
@@ -320,8 +267,8 @@ class Bitfinex(Exchange):
)
# Making sure that assets are iterable
asset_list = [assets] if isinstance(assets, Asset) else assets
ohlc_list = dict()
asset_list = [assets] if isinstance(assets, TradingPair) else assets
ohlc_map = dict()
for asset in asset_list:
symbol = self._get_v2_symbol(asset)
url = '{url}/v2/candles/trade:{frequency}:{symbol}'.format(
@@ -359,8 +306,7 @@ class Bitfinex(Exchange):
volume=np.float64(candle[5]),
price=np.float64(candle[2]),
last_traded=pd.Timestamp.utcfromtimestamp(
candle[0] / 1000.0),
minute_dt=pd.Timestamp.utcnow().floor('1 min')
candle[0] / 1000.0)
)
return ohlc
@@ -371,102 +317,43 @@ class Bitfinex(Exchange):
ohlc = ohlc_from_candle(candle)
ohlc_bars.append(ohlc)
ohlc_list[asset] = ohlc_bars
ohlc_map[asset] = ohlc_bars
else:
ohlc = ohlc_from_candle(candles)
ohlc_list[asset] = ohlc
ohlc_map[asset] = ohlc
return ohlc_list[assets] \
if isinstance(assets, Asset) else ohlc_list
return ohlc_map[assets] \
if isinstance(assets, TradingPair) else ohlc_map
def order(self, asset, amount, limit_price, stop_price, 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`
def create_order(self, asset, amount, is_buy, style):
"""
if amount == 0:
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
)
)
Creating order on the exchange.
:param asset:
:param amount:
:param is_buy:
:param style:
:return:
"""
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(
symbol=exchange_symbol,
amount=str(float(abs(amount))),
price=str(float(price)),
price="{:.20f}".format(float(price)),
side='buy' if is_buy else 'sell',
type='exchange ' + order_type, # TODO: support margin trades
exchange=self.name,
@@ -481,17 +368,17 @@ class Bitfinex(Exchange):
date = pd.Timestamp.utcnow()
try:
response = self._request('order/new', req)
exchange_order = response.json()
order_status = response.json()
except Exception as e:
raise ExchangeRequestError(error=e)
if 'message' in exchange_order:
if 'message' in order_status:
raise ExchangeRequestError(
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(
dt=date,
asset=asset,
@@ -500,12 +387,8 @@ class Bitfinex(Exchange):
limit=style.get_limit_price(is_buy),
id=order_id
)
# TODO: is this required?
order.broker_order_id = order_id
self.portfolio.create_order(order)
return order_id
return order
def get_open_orders(self, asset=None):
"""Retrieve all of the current open orders.
@@ -538,7 +421,7 @@ class Bitfinex(Exchange):
orders = list()
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:
orders.append(order)
@@ -590,9 +473,10 @@ class Bitfinex(Exchange):
raise ExchangeRequestError(error=e)
if 'message' in status:
raise ExchangeRequestError(
error='Unable to cancel order: {} {}'.format(
order_id, status['message'])
raise OrderCancelError(
order_id=order_id,
exchange=self.name,
error=status['message']
)
def tickers(self, assets):
@@ -624,15 +508,14 @@ class Bitfinex(Exchange):
tickers = response.json()
formatted_tickers = []
ticks = dict()
for index, ticker in enumerate(tickers):
if not len(ticker) == 11:
raise ExchangeRequestError(
error='Invalid ticker in response: {}'.format(ticker)
)
tick = dict(
asset=assets[index],
ticks[assets[index]] = dict(
timestamp=pd.Timestamp.utcnow(),
bid=ticker[1],
ask=ticker[3],
@@ -641,7 +524,6 @@ class Bitfinex(Exchange):
high=ticker[9],
volume=ticker[8],
)
formatted_tickers.append(tick)
log.debug('got tickers {}'.format(formatted_tickers))
return formatted_tickers
log.debug('got tickers {}'.format(ticks))
return ticks
@@ -3,6 +3,10 @@
"symbol": "btc_usd",
"start_date": "2010-01-01"
},
"bchusd": {
"symbol": "bch_usd",
"start_date": "2010-01-01"
},
"ltcusd": {
"symbol": "ltc_usd",
"start_date": "2010-01-01"
+307
View File
@@ -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
+127
View File
@@ -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
View File
@@ -1,22 +1,26 @@
import abc
import random
from time import sleep
import collections
import random
from abc import ABCMeta, abstractmethod, abstractproperty
from datetime import timedelta
from time import sleep
import numpy as np
import pandas as pd
from catalyst.assets._assets import Asset
from catalyst.assets._assets import TradingPair
from logbook import Logger
from catalyst.data.data_portal import BASE_FIELDS
from catalyst.errors import (
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.transaction import Transaction
from catalyst.exchange.exchange_utils import get_exchange_symbols
log = Logger('Exchange')
@@ -31,22 +35,26 @@ class Exchange:
self._portfolio = None
self.minute_writer = None
self.minute_reader = None
self.base_currency = None
@abstractmethod
def subscribe_to_market_data(self, symbol):
pass
@abstractproperty
@property
def positions(self):
pass
return self.portfolio.positions
@abstractproperty
def update_portfolio(self):
pass
@abstractproperty
@property
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
def account(self):
@@ -102,10 +110,13 @@ class Exchange:
asset = self.assets[key]
if not asset:
raise SymbolNotFound('Asset not found: %s' % symbol)
raise SymbolNotFound(symbol=symbol)
return asset
def fetch_symbol_map(self):
return get_exchange_symbols(self.name)
def load_assets(self):
"""
Populate the 'assets' attribute with a dictionary of Assets.
@@ -124,22 +135,40 @@ class Exchange:
via its api.
"""
symbol_map = get_exchange_symbols(self.name)
symbol_map = self.fetch_symbol_map()
for exchange_symbol in symbol_map:
asset = symbol_map[exchange_symbol]
symbol = asset['symbol']
asset_name = ' / '.join(symbol.split('_')).upper()
asset_obj = Asset(
symbol=symbol,
asset_name=asset_name,
sid=abs(hash(symbol)) % (10 ** 4),
if 'start_date' in asset:
start_date = pd.to_datetime(asset['start_date'], utc=True)
else:
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,
start_date=pd.to_datetime(asset['start_date'], utc=True),
end_date=pd.Timestamp.utcnow() + timedelta(minutes=300000),
start_date=start_date,
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):
"""
@@ -355,24 +384,76 @@ class Exchange:
bar_count=bar_count,
)
frames = []
series = dict()
for asset in assets:
asset_candles = candles[asset]
asset_data = dict()
asset_data[asset] = map(lambda candle: candle[field],
asset_candles)
values = map(lambda candle: candle[field], asset_candles)
dates = map(lambda candle: candle['last_traded'], asset_candles)
dates = map(lambda candle: candle['last_traded'],
asset_candles)
value_series = pd.Series(values, index=dates)
series[asset] = value_series
df = pd.DataFrame(asset_data, index=dates)
frames.append(df)
df = pd.concat(series)
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
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.
Parameters
@@ -412,7 +493,48 @@ class Exchange:
:func:`catalyst.api.order_value`
: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
def get_open_orders(self, asset):
@@ -486,4 +608,12 @@ class Exchange:
:param assets:
:return:
"""
return
pass
@abc.abstractmethod
def get_account(self):
"""
Retrieve the account parameters.
:return:
"""
pass
+53
View File
@@ -58,3 +58,56 @@ class InvalidHistoryFrequencyError(ZiplineError):
msg = (
'History frequency {frequency} not supported by the exchange.'
).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()
+39
View File
@@ -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
+2 -1
View File
@@ -8,8 +8,9 @@ from catalyst.exchange.exchange_errors import ExchangeAuthNotFound, \
ExchangeSymbolsNotFound
from catalyst.utils.paths import data_root, ensure_directory
# TODO: move to aws
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):
+47
View File
@@ -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
)
+102 -82
View File
@@ -10,6 +10,8 @@ import pandas as pd
import click
from catalyst.exchange.bittrex.bittrex import Bittrex
try:
from pygments import highlight
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_errors import (
ExchangeRequestError,
ExchangeRequestErrorTooManyAttempts
)
ExchangeRequestErrorTooManyAttempts,
BaseCurrencyNotFoundError)
from catalyst.exchange.exchange_utils import get_exchange_auth, \
get_algo_object
from logbook import Logger
@@ -142,10 +144,43 @@ def _run(handle_data,
else:
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()
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')
sim_params = create_simulation_parameters(
start=start,
@@ -154,7 +189,65 @@ def _run(handle_data,
data_frequency=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(',')
def get_trading_env_and_data(bundles):
@@ -184,7 +277,7 @@ def _run(handle_data,
)
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',
trading_calendar=open_calendar,
asset_db_path=connstr,
@@ -240,54 +333,8 @@ def _run(handle_data,
)
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:
env = TradingEnvironment(environ=environ)
choose_loader = None
env = TradingEnvironment(environ=environ)
choose_loader = None
TradingAlgorithmClass = (
partial(ExchangeTradingAlgorithm, exchange=exchange,
@@ -462,12 +509,9 @@ def run_algorithm(initialize,
--------
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)
exchange = None
if mode == 'backtest':
if not live:
non_none_data = valfilter(bool, {
'data': data is not None,
'bundle': bundle is not None,
@@ -486,30 +530,6 @@ def run_algorithm(initialize,
raise ValueError(
'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(
handle_data=handle_data,
initialize=initialize,
@@ -530,7 +550,7 @@ def run_algorithm(initialize,
local_namespace=False,
environ=environ,
live=live,
exchange=exchange,
exchange=exchange_name,
algo_namespace=algo_namespace,
base_currency=base_currency
)
+105
View File
@@ -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)
+4 -1
View File
@@ -9,7 +9,9 @@ Logbook==0.12.5
# Scientific Libraries
pytz==2016.4
numpy==1.11.1
# FF: Upgraded numpy because of errors with version 1.11
numpy==1.13.1
# for pandas-datareader
requests-file==1.4.1
@@ -77,3 +79,4 @@ lru-dict==1.1.4
empyrical==0.2.1
tables==3.3.0
+8 -4
View File
@@ -38,6 +38,7 @@ class LazyBuildExtCommandClass(dict):
Lazy command class that defers operations requiring Cython and numpy until
they've actually been downloaded and installed by setup_requires.
"""
def __contains__(self, key):
return (
key == 'build_ext'
@@ -62,6 +63,7 @@ class LazyBuildExtCommandClass(dict):
Custom build_ext command that lazily adds numpy's include_dir to
extensions.
"""
def build_extensions(self):
"""
Lazily append numpy's include directory to Extension includes.
@@ -75,6 +77,7 @@ class LazyBuildExtCommandClass(dict):
ext.include_dirs.append(numpy_incl)
super(build_ext, self).build_extensions()
return build_ext
@@ -100,7 +103,8 @@ ext_modules = [
window_specialization('label'),
Extension('catalyst.lib.rank', ['catalyst/lib/rank.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.gens.sim_engine', ['catalyst/gens/sim_engine.pyx']),
Extension(
@@ -117,7 +121,6 @@ ext_modules = [
),
]
STR_TO_CMP = {
'<': lt,
'<=': le,
@@ -212,7 +215,7 @@ def read_requirements(path,
conda_format=False,
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
if `strict_bounds` is falsey.
@@ -264,6 +267,7 @@ def setup_requirements(requirements_path, module_names, strict_bounds,
)
return module_lines
conda_build = os.path.basename(sys.argv[0]) in ('conda-build', # unix
'conda-build-script.py') # win
@@ -295,7 +299,7 @@ setup(
ext_modules=ext_modules,
include_package_data=True,
package_data={root.replace(os.sep, '.'):
['*.pyi', '*.pyx', '*.pxi', '*.pxd']
['*.pyi', '*.pyx', '*.pxi', '*.pxd']
for root, dirnames, filenames in os.walk('catalyst')
if '__pycache__' not in root},
license='Apache 2.0',
+13 -21
View File
@@ -5,30 +5,14 @@ from abc import ABCMeta, abstractmethod
class BaseExchangeTestCase():
__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
def test_order(self):
pass
@abstractmethod
def test_open_orders(self):
pass
@abstractmethod
def test_get_order(self):
pass
@@ -38,9 +22,17 @@ class BaseExchangeTestCase():
pass
@abstractmethod
def test_get_spot_value(self):
def test_get_candles(self):
pass
@abstractmethod
def test_tickers(self):
pass
@abstractmethod
def test_get_balances(self):
pass
@abstractmethod
def test_get_account(self):
pass
+39 -63
View File
@@ -1,4 +1,4 @@
from catalyst.exchange.bitfinex import Bitfinex
from catalyst.exchange.bitfinex.bitfinex import Bitfinex
from .base import BaseExchangeTestCase
from logbook import Logger
import pandas as pd
@@ -6,42 +6,27 @@ from catalyst.finance.execution import (MarketOrder,
LimitOrder,
StopOrder,
StopLimitOrder)
from catalyst.exchange.exchange_utils import get_exchange_auth
log = Logger('BitfinexTestCase')
log = Logger('test_bitfinex')
class BitfinexTestCase(BaseExchangeTestCase):
def test_positions(self):
log.info('querying positions from bitfinex')
bitfinex = Bitfinex()
balance = bitfinex.positions()
log.info('the balance: {}'.format(balance))
pass
def test_portfolio(self):
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
@classmethod
def setup(self):
print ('creating bitfinex object')
auth = get_exchange_auth('bitfinex')
self.exchange = Bitfinex(
key=auth['key'],
secret=auth['secret'],
base_currency='usd'
)
def test_order(self):
log.info('ordering from bitfinex')
bitfinex = Bitfinex()
order_id = bitfinex.order(
asset=bitfinex.get_asset('eth_usd'),
log.info('creating order')
asset = self.exchange.get_asset('eth_usd')
order_id = self.exchange.order(
asset=asset,
style=LimitOrder(limit_price=200),
limit_price=200,
amount=0.5,
@@ -50,45 +35,36 @@ class BitfinexTestCase(BaseExchangeTestCase):
log.info('order created {}'.format(order_id))
pass
def test_open_orders(self):
log.info('retrieving open orders')
orders = self.exchange.get_open_orders()
pass
def test_get_order(self):
log.info('querying orders from bitfinex')
bitfinex = Bitfinex()
response = bitfinex.get_order(order_id=3361248395)
log.info('the order: {}'.format(response))
log.info('retrieving order')
pass
def test_cancel_order(self):
log.info('canceling order from bitfinex')
bitfinex = Bitfinex()
response = bitfinex.cancel_order(order_id=3330847408)
log.info('canceled order: {}'.format(response))
log.info('cancel order')
pass
def test_get_spot_value(self):
log.info('spot value not implemented')
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'
)
def test_get_candles(self):
log.info('retrieving candles')
pass
def test_tickers(self):
log.info('fetching ticker from bitfinex')
bitfinex = Bitfinex()
current_date = pd.Timestamp.utcnow()
assets = [
bitfinex.get_asset('eth_usd'),
bitfinex.get_asset('etc_usd'),
bitfinex.get_asset('eos_usd'),
]
tickers = bitfinex.tickers(date=current_date, assets=assets)
log.info('got tickers {}'.format(tickers))
log.info('retrieving tickers')
tickers = self.exchange.tickers([
self.exchange.get_asset('eth_usd'),
self.exchange.get_asset('btc_usd')
])
pass
def test_get_account(self):
log.info('retrieving account data')
pass
def test_get_balances(self):
log.info('testing exchange balances')
balances = self.exchange.get_balances()
pass
+83
View File
@@ -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