mirror of
https://github.com/wassname/catalyst.git
synced 2026-06-29 23:23:34 +08:00
Working on multiple exchanges and a sample algo for arbitrage
This commit is contained in:
@@ -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
|
||||
)
|
||||
@@ -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):
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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,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)
|
||||
|
||||
Reference in New Issue
Block a user