Working on multiple exchanges and a sample algo for arbitrage

This commit is contained in:
fredfortier
2017-09-10 20:20:34 -04:00
parent 36881b03e2
commit 7e280aeb5c
8 changed files with 299 additions and 25 deletions
@@ -0,0 +1,190 @@
import talib
from logbook import Logger
from catalyst.api import (
order,
order_target_percent,
symbol,
record,
get_open_orders,
)
from catalyst.exchange.stats_utils import get_pretty_stats
from catalyst.utils.run_algo import run_algorithm
algo_namespace = 'arbitrage_neo_eth'
log = Logger(algo_namespace)
def initialize(context):
log.info('initializing arbitrage algorithm')
context.buying_exchange = 'bittrex'
context.selling_exchange = 'bitfinex'
context.trading_pair_symbol = 'neo_eth'
context.trading_pairs = dict()
context.trading_pairs[context.buying_exchange] = \
symbol(context.trading_pair_symbol, context.buying_exchange)
context.trading_pairs[context.selling_exchange] = \
symbol(context.trading_pair_symbol, context.selling_exchange)
context.entry_points = [
dict(gap=0.001, amount=0.05),
dict(gap=0.002, amount=0.1),
]
context.exit_points = [
dict(gap=0, amount=0.05),
dict(gap=-0.001, amount=0.01),
]
context.MAX_POSITIONS = 50
context.SLIPPAGE_ALLOWED = 0.02
pass
def place_order(context, amount, buying_price, selling_price,
action):
if action == 'enter':
buying_exchange = context.exchanges[context.buying_exchange]
buy_price = buying_price
selling_exchange = context.exchanges[context.selling_exchange]
sell_price = selling_price
elif action == 'exit':
buying_exchange = context.exchanges[context.selling_exchange]
buy_price = selling_price
selling_exchange = context.exchanges[context.buying_exchange]
sell_price = buying_price
else:
raise ValueError('invalid order action')
base_currency = buying_exchange.base_currency
base_currency_amount = buying_exchange.portfolio.cash
sell_balances = selling_exchange.get_balances()
sell_currency = context.trading_pairs[
context.selling_exchange].market_currency
if sell_currency in sell_balances:
market_currency_amount = sell_balances[sell_currency]
else:
log.warn('the selling exchange {} does not hold currency {}'.format(
selling_exchange.name, sell_currency
))
return
if base_currency_amount < amount:
log.warn('not enough {} ({}) to buy {}, adjusting the amount'.format(
base_currency, base_currency_amount, amount))
amount = base_currency_amount
elif market_currency_amount < amount:
log.warn('not enough {} ({}) to sell {}, aborting'.format(
sell_currency, market_currency_amount, amount))
return
adj_buy_price = buy_price * (1 + context.SLIPPAGE_ALLOWED)
log.info('buying {} limit at {}{} on {}'.format(
amount, buying_price, context.trading_pair_symbol,
buying_exchange.name))
order(
asset=context.trading_pairs[buying_exchange],
amount=amount,
limit_price=adj_buy_price
)
adj_sell_price = sell_price * (1 - context.SLIPPAGE_ALLOWED)
log.info('selling {} limit at {}{} on {}'.format(
amount, adj_sell_price, context.trading_pair_symbol,
selling_exchange.name))
order(
asset=context.trading_pairs[selling_exchange],
amount=amount,
limit_price=adj_sell_price
)
pass
def handle_data(context, data):
log.info('handling bar {}'.format(data.current_dt))
buying_price = data.current(
context.trading_pairs[context.buying_exchange], 'price')
log.info('price on buying exchange {exchange}: {price}'.format(
exchange=context.buying_exchange.upper(),
price=buying_price,
))
selling_price = data.current(
context.trading_pairs[context.selling_exchange], 'price')
log.info('price on selling exchange {exchange}: {price}'.format(
exchange=context.selling_exchange.upper(),
price=selling_price,
))
# If for example,
# selling price = 50
# buying price = 25
# expected gap = 1
# If follows that,
# selling price - buying price / buying price
# 50 - 25 / 25 = 1
gap = (selling_price - buying_price) / buying_price
log.info('the price gap: {} ({}%)'.format(gap, gap * 100))
# Consider the least ambitious entry point first
# Override of wider gap is found
entry_points = sorted(
context.entry_points,
key=lambda point: point['gap'],
)
buy_amount = None
for entry_point in entry_points:
if gap > entry_point['gap']:
buy_amount = entry_point['amount']
if buy_amount:
log.info('found buy trigger for amount: {}'.format(buy_amount))
place_order(context, buy_amount, buying_price, selling_price, 'enter')
else:
# Consider the narrowest exit gap first
# Override of wider gap is found
exit_points = sorted(
context.exit_points,
key=lambda point: point['gap'],
reverse=True
)
sell_amount = None
for exit_point in exit_points:
if gap < exit_point['gap']:
sell_amount = exit_point['amount']
if sell_amount:
log.info('found sell trigger for amount: {}'.format(sell_amount))
place_order(context, sell_amount, buying_price, selling_price,
'exit')
def analyze(context, stats):
log.info('the daily stats:\n{}'.format(get_pretty_stats(stats)))
pass
run_algorithm(
initialize=initialize,
handle_data=handle_data,
analyze=analyze,
exchange_name='bittrex,bitfinex',
live=True,
algo_namespace=algo_namespace,
base_currency='eth',
live_graph=False
)
+44 -14
View File
@@ -11,42 +11,43 @@
# See the License for the specific language governing permissions and
# limitations under the License.
import os
import pickle
import signal
import sys
import pickle
from collections import deque
from datetime import timedelta
from time import sleep
from os import listdir
from os.path import isfile, join
from collections import deque
import numpy as np
from time import sleep
import logbook
import pandas as pd
from catalyst.utils.preprocess import preprocess
from catalyst.assets._assets import TradingPair
import catalyst.protocol as zp
from catalyst.algorithm import TradingAlgorithm
from catalyst.data.minute_bars import BcolzMinuteBarWriter, \
BcolzMinuteBarReader
from catalyst.errors import OrderInBeforeTradingStart
from catalyst.exchange.simple_clock import SimpleClock
from catalyst.exchange.live_graph_clock import LiveGraphClock
from catalyst.exchange.exchange_errors import (
ExchangeRequestError,
ExchangePortfolioDataError,
ExchangeTransactionError
)
ExchangeTransactionError,
OrphanOrderError)
from catalyst.exchange.exchange_utils import get_exchange_minute_writer_root, \
save_algo_object, get_algo_object, get_algo_folder, get_algo_df, \
save_algo_df
from catalyst.exchange.live_graph_clock import LiveGraphClock
from catalyst.exchange.simple_clock import SimpleClock
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 (
api_method,
disallowed_in_before_trading_start)
from catalyst.utils.input_validation import error_keywords, ensure_upper_case
from catalyst.utils.input_validation import error_keywords, ensure_upper_case, \
expect_types
from catalyst.utils.preprocess import preprocess
log = logbook.Logger("ExchangeTradingAlgorithm")
@@ -406,7 +407,9 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
self.minute_stats.append(minute_stats)
self.add_pnl_stats(minute_stats)
self.add_custom_signals_stats(minute_stats)
if self.recorded_vars:
self.add_custom_signals_stats(minute_stats)
self.add_exposure_stats(minute_stats)
print_df = pd.DataFrame(list(self.minute_stats))
@@ -481,23 +484,50 @@ class ExchangeTradingAlgorithm(TradingAlgorithm):
@api_method
@disallowed_in_before_trading_start(OrderInBeforeTradingStart())
@expect_types(asset=TradingPair)
def order(self,
asset,
amount,
limit_price=None,
stop_price=None,
style=None):
"""
We use the exchange specific portfolio to place orders.
The cumulative portfolio does not contain open orders but exchange
portfolios do.
:param asset: TradingPair
:param amount: float
:param limit_price: float
:param stop_price: float
:param style: Style
:return order: Order
The catalyst order object or None
"""
amount, style = self._calculate_order(asset, amount,
limit_price, stop_price,
style)
order_id = self._order(asset, amount, limit_price, stop_price, style)
exchange = self.exchanges[asset.exchange]
exchange_portfolio = exchange.portfolio
if order_id is not None:
order = self.portfolio.open_orders[order_id]
self.perf_tracker.process_order(order)
return order
if order_id in exchange_portfolio.open_orders:
order = exchange_portfolio.open_orders[order_id]
self.perf_tracker.process_order(order)
return order
else:
raise OrphanOrderError(
order_id=order_id,
exchange=exchange.name
)
else:
log.warn('unable to order {} {} on exchange {}'.format(
amount, asset.symbol, asset.exchange))
return None
def round_order(self, amount):
+1
View File
@@ -40,6 +40,7 @@ class Bitfinex(Exchange):
self.key = key
self.secret = secret.encode('UTF-8')
self.name = 'bitfinex'
self.color = 'green'
self.assets = {}
self.load_assets()
self.base_currency = base_currency
+12
View File
@@ -1,4 +1,16 @@
{
"neobtc": {
"symbol": "neo_btc",
"start_date": "2017-09-07"
},
"neousd": {
"symbol": "neo_usd",
"start_date": "2017-09-07"
},
"neoeth": {
"symbol": "neo_eth",
"start_date": "2017-09-07"
},
"btcusd": {
"symbol": "btc_usd",
"start_date": "2010-01-01"
+1
View File
@@ -22,6 +22,7 @@ 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.color = 'blue'
self.base_currency = base_currency
self._portfolio = portfolio
+7 -3
View File
@@ -14,7 +14,7 @@ from catalyst.errors import (
SymbolNotFound,
)
from catalyst.exchange.exchange_errors import MismatchingBaseCurrencies, \
InvalidOrderStyle, BaseCurrencyNotFoundError
InvalidOrderStyle, BaseCurrencyNotFoundError, SymbolNotFoundOnExchange
from catalyst.exchange.exchange_execution import ExchangeStopLimitOrder, \
ExchangeLimitOrder, ExchangeStopOrder
from catalyst.exchange.exchange_portfolio import ExchangePortfolio
@@ -30,7 +30,6 @@ class Exchange:
def __init__(self):
self.name = None
self.trading_pairs = None
self.assets = {}
self._portfolio = None
self.minute_writer = None
@@ -110,7 +109,12 @@ class Exchange:
asset = self.assets[key]
if not asset:
raise SymbolNotFound(symbol=symbol)
supported_symbols = [pair.symbol for pair in self.assets.values()]
raise SymbolNotFoundOnExchange(
symbol=symbol,
exchange=self.name,
supported_symbols=supported_symbols
)
return asset
+22
View File
@@ -94,6 +94,13 @@ class OrderNotFound(ZiplineError):
).strip()
class OrphanOrderError(ZiplineError):
msg = (
'Order {order_id} found in exchange {exchange} but not tracked by '
'the algorithm.'
).strip()
class OrderCancelError(ZiplineError):
msg = (
'Unable to cancel order {order_id} on exchange {exchange} {error}.'
@@ -118,3 +125,18 @@ class MismatchingBaseCurrencies(ZiplineError):
'Unable to trade with base currency {base_currency} when the '
'algorithm uses {algo_currency}.'
).strip()
class MismatchingBaseCurrenciesExchanges(ZiplineError):
msg = (
'Unable to trade with base currency {base_currency} when the '
'exchange {exchange_name} users {exchange_currency}.'
).strip()
class SymbolNotFoundOnExchange(ZiplineError):
"""
Raised when a symbol() call contains a non-existant symbol.
"""
msg = ('Symbol {symbol} not found on exchange {exchange}. '
'Choose from: {supported_symbols}').strip()
+22 -8
View File
@@ -22,6 +22,9 @@ from logbook import Logger
from matplotlib import pyplot as plt
from matplotlib import style
from catalyst.exchange.exchange_errors import \
MismatchingBaseCurrenciesExchanges
log = Logger('LiveGraphClock')
style.use('dark_background')
@@ -154,17 +157,31 @@ class LiveGraphClock(object):
context = self.context
df = context.exposure_stats
# TODO: list exchanges in graph
base_currency = None
positions = []
for exchange_name in context.exchanges:
exchange = context.exchanges[exchange_name]
if not base_currency:
base_currency = exchange.base_currency
elif base_currency != exchange.base_currency:
raise MismatchingBaseCurrenciesExchanges(
base_currency=base_currency,
exchange_name=exchange.name,
exchange_currency=exchange.base_currency
)
positions += exchange.portfolio.positions
ax.clear()
ax.set_title('Exposure')
ax.plot(df.index, df['base_currency'], '-',
color='green',
linewidth=1.0,
label='Base Currency: {}'.format(
context.exchange.base_currency.upper()
)
label='Base Currency: {}'.format(base_currency.upper())
)
positions = context.exchange.portfolio.positions
symbols = []
for position in positions:
symbols.append(position.symbol)
@@ -172,10 +189,7 @@ class LiveGraphClock(object):
ax.plot(df.index, df['long_exposure'], '-',
color='blue',
linewidth=1.0,
label='Long Exposure: {}'.format(
', '.join(symbols).upper()
)
)
label='Long Exposure: {}'.format(', '.join(symbols).upper()))
self.set_legend(ax)
self.format_ax(ax)