mirror of
https://github.com/wassname/catalyst.git
synced 2026-06-30 02:37:32 +08:00
276 lines
8.3 KiB
Python
276 lines
8.3 KiB
Python
from logbook import Logger
|
|
|
|
from catalyst.api import (
|
|
record,
|
|
order,
|
|
symbol,
|
|
get_open_orders
|
|
)
|
|
from catalyst.exchange.stats_utils import get_pretty_stats
|
|
from catalyst.utils.run_algo import run_algorithm
|
|
|
|
algo_namespace = 'arbitrage_eth_btc'
|
|
log = Logger(algo_namespace)
|
|
|
|
|
|
def initialize(context):
|
|
log.info('initializing arbitrage algorithm')
|
|
|
|
# The context contains a new "exchanges" attribute which is a dictionary
|
|
# of exchange objects by exchange name. This allow easy access to the
|
|
# exchanges.
|
|
context.buying_exchange = context.exchanges['poloniex']
|
|
context.selling_exchange = context.exchanges['bitfinex']
|
|
|
|
context.trading_pair_symbol = 'eth_btc'
|
|
context.trading_pairs = dict()
|
|
|
|
# Note the second parameter of the symbol() method
|
|
# Passing the exchange name here returns a TradingPair object including
|
|
# the exchange information. This allow all other operations using
|
|
# the TradingPair to target the correct exchange.
|
|
context.trading_pairs[context.buying_exchange] = \
|
|
symbol('eth_btc', context.buying_exchange.name)
|
|
|
|
context.trading_pairs[context.selling_exchange] = \
|
|
symbol(context.trading_pair_symbol, context.selling_exchange.name)
|
|
|
|
context.entry_points = [
|
|
dict(gap=0.03, amount=0.05),
|
|
dict(gap=0.04, amount=0.1),
|
|
dict(gap=0.05, amount=0.5),
|
|
]
|
|
context.exit_points = [
|
|
dict(gap=-0.02, amount=0.5),
|
|
]
|
|
|
|
context.SLIPPAGE_ALLOWED = 0.02
|
|
pass
|
|
|
|
|
|
def place_orders(context, amount, buying_price, selling_price, action):
|
|
"""
|
|
This method will always place two orders of the same amount to keep
|
|
the currency position the same as it moves between the two exchanges.
|
|
|
|
:param context: TradingAlgorithm
|
|
:param amount: float
|
|
The trading pair amount to trade on both exchanges.
|
|
:param buying_price: float
|
|
The current trading pair price on the buying exchange.
|
|
:param selling_price: float
|
|
The current trading pair price on the selling exchange.
|
|
:param action: string
|
|
"enter": buys on the buying exchange and sells on the selling exchange
|
|
"exit": buys on the selling exchange and sells on the buying exchange
|
|
|
|
:return:
|
|
"""
|
|
if action == 'enter':
|
|
enter_exchange = context.buying_exchange
|
|
entry_price = buying_price
|
|
|
|
exit_exchange = context.selling_exchange
|
|
exit_price = selling_price
|
|
|
|
elif action == 'exit':
|
|
enter_exchange = context.selling_exchange
|
|
entry_price = selling_price
|
|
|
|
exit_exchange = context.buying_exchange
|
|
exit_price = buying_price
|
|
|
|
else:
|
|
raise ValueError('invalid order action')
|
|
|
|
base_currency = enter_exchange.base_currency
|
|
base_currency_amount = enter_exchange.portfolio.cash
|
|
|
|
exit_balances = exit_exchange.get_balances()
|
|
exit_currency = context.trading_pairs[
|
|
context.selling_exchange].market_currency
|
|
|
|
if exit_currency in exit_balances:
|
|
market_currency_amount = exit_balances[exit_currency]
|
|
else:
|
|
log.warn(
|
|
'the selling exchange {exchange_name} does not hold '
|
|
'currency {currency}'.format(
|
|
exchange_name=exit_exchange.name,
|
|
currency=exit_currency
|
|
)
|
|
)
|
|
return
|
|
|
|
if base_currency_amount < (amount * entry_price):
|
|
adj_amount = base_currency_amount / entry_price
|
|
log.warn(
|
|
'not enough {base_currency} ({base_currency_amount}) to buy '
|
|
'{amount}, adjusting the amount to {adj_amount}'.format(
|
|
base_currency=base_currency,
|
|
base_currency_amount=base_currency_amount,
|
|
amount=amount,
|
|
adj_amount=adj_amount
|
|
)
|
|
)
|
|
amount = adj_amount
|
|
|
|
elif market_currency_amount < amount:
|
|
log.warn(
|
|
'not enough {currency} ({currency_amount}) to sell '
|
|
'{amount}, aborting'.format(
|
|
currency=exit_currency,
|
|
currency_amount=market_currency_amount,
|
|
amount=amount
|
|
)
|
|
)
|
|
return
|
|
|
|
adj_buy_price = entry_price * (1 + context.SLIPPAGE_ALLOWED)
|
|
log.info(
|
|
'buying {amount} {trading_pair} on {exchange_name} with price '
|
|
'limit {limit_price}'.format(
|
|
amount=amount,
|
|
trading_pair=context.trading_pair_symbol,
|
|
exchange_name=enter_exchange.name,
|
|
limit_price=adj_buy_price
|
|
)
|
|
)
|
|
order(
|
|
asset=context.trading_pairs[enter_exchange],
|
|
amount=amount,
|
|
limit_price=adj_buy_price
|
|
)
|
|
|
|
adj_sell_price = exit_price * (1 - context.SLIPPAGE_ALLOWED)
|
|
log.info(
|
|
'selling {amount} {trading_pair} on {exchange_name} with price '
|
|
'limit {limit_price}'.format(
|
|
amount=-amount,
|
|
trading_pair=context.trading_pair_symbol,
|
|
exchange_name=exit_exchange.name,
|
|
limit_price=adj_sell_price
|
|
)
|
|
)
|
|
order(
|
|
asset=context.trading_pairs[exit_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.name.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.name.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: {gap} ({gap_percent}%)'.format(
|
|
gap=gap,
|
|
gap_percent=gap * 100
|
|
)
|
|
)
|
|
record(buying_price=buying_price, selling_price=selling_price, gap=gap)
|
|
|
|
# Waiting for orders to close before initiating new ones
|
|
for exchange in context.trading_pairs:
|
|
asset = context.trading_pairs[exchange]
|
|
|
|
orders = get_open_orders(asset)
|
|
if orders:
|
|
log.info(
|
|
'found {order_count} open orders on {exchange_name} '
|
|
'skipping bar until all open orders execute'.format(
|
|
order_count=len(orders),
|
|
exchange_name=exchange.name
|
|
)
|
|
)
|
|
return
|
|
|
|
# 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_orders(
|
|
context=context,
|
|
amount=buy_amount,
|
|
buying_price=buying_price,
|
|
selling_price=selling_price,
|
|
action='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_orders(
|
|
context=context,
|
|
amount=sell_amount,
|
|
buying_price=buying_price,
|
|
selling_price=selling_price,
|
|
action='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='poloniex,bitfinex',
|
|
live=True,
|
|
algo_namespace=algo_namespace,
|
|
base_currency='btc',
|
|
live_graph=False
|
|
)
|