mirror of
https://github.com/wassname/catalyst.git
synced 2026-07-05 21:39:31 +08:00
Adding the first exchange (Bitfinex) and unit testing
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
import pandas as pd
|
||||
import pytz
|
||||
|
||||
assets = list(
|
||||
dict(
|
||||
symbol='eth-usd',
|
||||
exchange='bitfinex',
|
||||
first_traded=pd.datetime(2010, 1, 1, 0, 0, 0, 0, pytz.utc)
|
||||
)
|
||||
)
|
||||
@@ -0,0 +1,266 @@
|
||||
import six
|
||||
import base64
|
||||
import hashlib
|
||||
import hmac
|
||||
import json
|
||||
import time
|
||||
import requests
|
||||
import pandas as pd
|
||||
# from websocket import create_connection
|
||||
from catalyst.exchange.exchange import Exchange, RTVolumeBar, Position
|
||||
from logbook import Logger
|
||||
from catalyst.finance.order import Order, ORDER_STATUS
|
||||
from catalyst.finance.execution import (MarketOrder,
|
||||
LimitOrder,
|
||||
StopOrder,
|
||||
StopLimitOrder)
|
||||
|
||||
BITFINEX_URL = 'https://api.bitfinex.com'
|
||||
BITFINEX_KEY = 'hjZ7DZzwbBZsIZPWeSSQtrWCPNwyhxw96r3LnY7jtOH'
|
||||
BITFINEX_SECRET = b'LilCoxcqUnHKBcGtrttwCIv4qONTdjuFMSdz8Rxh6OM'
|
||||
ASSETS = '{ "btcusd": {"symbol":"btc_usd", "start_date": "2010-01-01"}, "ltcusd": {"symbol":"ltc-usd", "start_date": "2010-01-01"}, "ltcbtc": {"symbol":"ltc_btc", "start_date": "2010-01-01"}, "ethusd": {"symbol":"eth_usd", "start_date": "2010-01-01"}, "ethbtc": {"symbol":"eth_btc", "start_date": "2010-01-01"}, "etcbtc": {"symbol":"etc_btc", "start_date": "2010-01-01"}, "etcusd": {"symbol":"etc_usd", "start_date": "2010-01-01"}, "rrtusd": {"symbol":"rrt_usd", "start_date": "2010-01-01"}, "rrtbtc": {"symbol":"rrt_btc", "start_date": "2010-01-01"}, "zecusd": {"symbol":"zec_usd", "start_date": "2010-01-01"}, "zecbtc": {"symbol":"zec_btc", "start_date": "2010-01-01"}, "xmrusd": {"symbol":"xmr_usd", "start_date": "2010-01-01"}, "xmrbtc": {"symbol":"xmr_btc", "start_date": "2010-01-01"}, "dshusd": {"symbol":"dsh_usd", "start_date": "2010-01-01"}, "dshbtc": {"symbol":"dsh_btc", "start_date": "2010-01-01"}, "bccbtc": {"symbol":"bcc_btc", "start_date": "2010-01-01"}, "bcubtc": {"symbol":"bcu_btc", "start_date": "2010-01-01"}, "bccusd": {"symbol":"bcc_usd", "start_date": "2010-01-01"}, "bcuusd": {"symbol":"bcu_usd", "start_date": "2010-01-01"}, "xrpusd": {"symbol":"xrp_usd", "start_date": "2010-01-01"}, "xrpbtc": {"symbol":"xrp_btc", "start_date": "2010-01-01"}, "iotusd": {"symbol":"iot_usd", "start_date": "2010-01-01"}, "iotbtc": {"symbol":"iot_btc", "start_date": "2010-01-01"}, "ioteth": {"symbol":"iot_eth", "start_date": "2010-01-01"}, "eosusd": {"symbol":"eos_usd", "start_date": "2010-01-01"}, "eosbtc": {"symbol":"eos_btc", "start_date": "2010-01-01"}, "eoseth": {"symbol":"eos_eth", "start_date": "2010-01-01"} }'
|
||||
|
||||
log = Logger('Bitfinex')
|
||||
warning_logger = Logger('AlgoWarning')
|
||||
|
||||
|
||||
class Bitfinex(Exchange):
|
||||
def __init__(self):
|
||||
self.url = BITFINEX_URL
|
||||
self.key = BITFINEX_KEY
|
||||
self.secret = BITFINEX_SECRET
|
||||
self.id = 'b'
|
||||
self.name = 'bitfinex'
|
||||
self.orders = {}
|
||||
self.assets = {}
|
||||
self.load_assets(ASSETS)
|
||||
|
||||
def request(self, operation, data, version='v1'):
|
||||
payload_object = {
|
||||
'request': '/{}/{}'.format(version, operation),
|
||||
'nonce': '{0:f}'.format(time.time() * 100000), # convert to string
|
||||
'options': {}
|
||||
}
|
||||
|
||||
if data is None:
|
||||
payload_dict = payload_object
|
||||
else:
|
||||
payload_dict = payload_object.copy()
|
||||
payload_dict.update(data)
|
||||
|
||||
payload_json = json.dumps(payload_dict)
|
||||
if six.PY3:
|
||||
payload = base64.b64encode(bytes(payload_json, 'utf-8'))
|
||||
else:
|
||||
payload = base64.b64encode(payload_json)
|
||||
|
||||
m = hmac.new(self.secret, payload, hashlib.sha384)
|
||||
m = m.hexdigest()
|
||||
|
||||
# headers
|
||||
headers = {
|
||||
'X-BFX-APIKEY': self.key,
|
||||
'X-BFX-PAYLOAD': payload,
|
||||
'X-BFX-SIGNATURE': m
|
||||
}
|
||||
|
||||
if data is None:
|
||||
request = requests.get(
|
||||
self.url + '/{version}/{operation}'.format(
|
||||
version=version,
|
||||
operation=operation
|
||||
), data={},
|
||||
headers=headers)
|
||||
else:
|
||||
request = requests.post(
|
||||
self.url + '/{version}/{operation}'.format(
|
||||
version=version,
|
||||
operation=operation
|
||||
),
|
||||
headers=headers)
|
||||
|
||||
return request
|
||||
|
||||
def subscribe_to_market_data(self, symbol):
|
||||
pass
|
||||
|
||||
def positions(self):
|
||||
pass
|
||||
|
||||
def portfolio(self):
|
||||
pass
|
||||
|
||||
def account(self):
|
||||
pass
|
||||
|
||||
@property
|
||||
def time_skew(self):
|
||||
# TODO: research the time skew conditions
|
||||
return None
|
||||
|
||||
def get_open_orders(self, asset):
|
||||
# TODO: map to asset
|
||||
response = self.request('orders', None)
|
||||
orders = response.json()
|
||||
# TODO: what is the right format?
|
||||
return orders
|
||||
|
||||
def get_order(self, order_id):
|
||||
pass
|
||||
|
||||
def get_spot_value(self, assets, field, dt, data_frequency):
|
||||
raise NotImplementedError()
|
||||
|
||||
def balance(self, currencies):
|
||||
response = self.request('balances', None)
|
||||
positions = response.json()
|
||||
if 'message' in positions:
|
||||
raise ValueError(
|
||||
'unable to fetch balance %s' % positions['message']
|
||||
)
|
||||
|
||||
balance = dict()
|
||||
for position in positions:
|
||||
if position['currency'] in currencies:
|
||||
balance[position['currency']] = float(position['available'])
|
||||
return balance
|
||||
|
||||
# TODO: why repeating prices if already in style?
|
||||
def order(self, asset, amount, limit_price, stop_price, style):
|
||||
"""
|
||||
The type of the order: LIMIT, MARKET, STOP, TRAILING STOP,
|
||||
EXCHANGE MARKET, EXCHANGE LIMIT, EXCHANGE STOP,
|
||||
EXCHANGE TRAILING STOP, FOK, EXCHANGE FOK.
|
||||
"""
|
||||
|
||||
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):
|
||||
raise NotImplementedError('Stop/limit orders not available')
|
||||
|
||||
exchange_symbol = self.get_symbol(asset)
|
||||
req = dict(
|
||||
symbol=exchange_symbol,
|
||||
amount=str(float(amount)),
|
||||
price=str(float(price)),
|
||||
side='buy' if is_buy else 'sell',
|
||||
type='exchange ' + order_type, # TODO: support margin trades
|
||||
exchange='bitfinex',
|
||||
is_hidden=False,
|
||||
is_postonly=False,
|
||||
use_all_available=0,
|
||||
ocoorder=False,
|
||||
buy_price_oco=0,
|
||||
sell_price_oco=0
|
||||
)
|
||||
|
||||
response = self.request('order/new', req)
|
||||
exchange_order = response.json()
|
||||
if 'message' in exchange_order:
|
||||
raise ValueError(
|
||||
'unable to create Bitfinex order %s' % exchange_order[
|
||||
'message']
|
||||
)
|
||||
|
||||
order = Order(
|
||||
dt=pd.Timestamp.utcnow(),
|
||||
asset=asset,
|
||||
amount=amount,
|
||||
stop=style.get_stop_price(is_buy),
|
||||
limit=style.get_limit_price(is_buy),
|
||||
)
|
||||
|
||||
order_id = order.broker_order_id = exchange_order['id']
|
||||
self.orders[order_id] = order
|
||||
|
||||
return order_id
|
||||
|
||||
def cancel_order(self, order_id):
|
||||
response = self.request('order/cancel', {'order_id': order_id})
|
||||
status = response.json()
|
||||
return status
|
||||
|
||||
def order_status(self, order_id):
|
||||
response = self.request('order/status', {'order_id': int(order_id)})
|
||||
order_status = response.json()
|
||||
if 'message' in order_status:
|
||||
raise ValueError(
|
||||
'Unable to retrieve order status: %s' % order_status['message']
|
||||
)
|
||||
|
||||
result = dict(exchange='b')
|
||||
|
||||
if order_status['is_cancelled']:
|
||||
warning_logger.warn(
|
||||
'removing cancelled order from the open orders list %s',
|
||||
order_status)
|
||||
result['status'] = 'canceled'
|
||||
|
||||
elif not order_status['is_live']:
|
||||
log.info('found executed order %s', order_status)
|
||||
result['status'] = 'closed'
|
||||
result['executed_price'] = float(
|
||||
order_status['avg_execution_price'])
|
||||
result['executed_amount'] = float(order_status['executed_amount'])
|
||||
|
||||
else:
|
||||
result['status'] = 'open'
|
||||
|
||||
return result
|
||||
|
||||
def get_v2_symbols(self, assets):
|
||||
"""
|
||||
Workaround to support Bitfinex v2
|
||||
TODO: Might require a separate asset dictionary
|
||||
|
||||
:param assets:
|
||||
:return:
|
||||
"""
|
||||
|
||||
v2_symbols = []
|
||||
for asset in assets:
|
||||
pair = asset.symbol.split('_')
|
||||
symbol = 't' + pair[0].upper() + pair[1].upper()
|
||||
v2_symbols.append(symbol)
|
||||
return v2_symbols
|
||||
|
||||
def tickers(self, date, assets):
|
||||
symbols = self.get_v2_symbols(assets)
|
||||
log.debug('fetching tickers {}'.format(symbols))
|
||||
|
||||
request = requests.get(
|
||||
self.url + '/v2/tickers?symbols={}'.format(','.join(symbols))
|
||||
)
|
||||
tickers = request.json()
|
||||
if 'message' in tickers:
|
||||
raise ValueError(
|
||||
'Unable to retrieve tickers: %s' % tickers['message']
|
||||
)
|
||||
|
||||
formatted_tickers = []
|
||||
for index, ticker in enumerate(tickers):
|
||||
if not len(ticker) == 11:
|
||||
raise ValueError('Invalid ticker: %s' % ticker)
|
||||
|
||||
tick = dict(
|
||||
asset=assets[index],
|
||||
timestamp=date,
|
||||
bid=ticker[1],
|
||||
ask=ticker[3],
|
||||
last_price=ticker[7],
|
||||
low=ticker[10],
|
||||
high=ticker[9],
|
||||
volume=ticker[8],
|
||||
)
|
||||
formatted_tickers.append(tick)
|
||||
|
||||
log.debug('got tickers {}'.format(formatted_tickers))
|
||||
return formatted_tickers
|
||||
@@ -0,0 +1,131 @@
|
||||
import abc
|
||||
from collections import namedtuple
|
||||
from abc import ABCMeta, abstractmethod, abstractproperty
|
||||
import json
|
||||
import pandas as pd
|
||||
from catalyst.assets._assets import Asset
|
||||
|
||||
RTVolumeBar = namedtuple('RTVolumeBar', ['last_trade_price',
|
||||
'last_trade_size',
|
||||
'last_trade_time',
|
||||
'total_volume',
|
||||
'vwap',
|
||||
'single_trade_flag'])
|
||||
|
||||
Position = namedtuple('Position', ['contract', 'position', 'market_price',
|
||||
'market_value', 'average_cost',
|
||||
'unrealized_pnl', 'realized_pnl',
|
||||
'account_name'])
|
||||
|
||||
|
||||
class Exchange:
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
def __init__(self):
|
||||
self.name = None
|
||||
self.trading_pairs = None
|
||||
self.assets = {}
|
||||
|
||||
def get_trading_pairs(self, pairs):
|
||||
return [pair for pair in pairs if pair in self.trading_pairs]
|
||||
|
||||
def get_symbol(self, asset):
|
||||
symbol = None
|
||||
|
||||
for key in self.assets:
|
||||
if not symbol and self.assets[key].symbol == asset.symbol:
|
||||
symbol = key
|
||||
|
||||
if not symbol:
|
||||
raise ValueError('Currency %s not supported by exchange %s' %
|
||||
(asset['symbol'], self.name))
|
||||
|
||||
return symbol
|
||||
|
||||
def get_symbols(self, assets):
|
||||
symbols = []
|
||||
for asset in assets:
|
||||
symbols.append(self.get_symbol(asset))
|
||||
return symbols
|
||||
|
||||
@staticmethod
|
||||
def asset_parser(asset):
|
||||
for key in asset:
|
||||
if key == 'start_date':
|
||||
asset[key] = pd.to_datetime(asset[key], utc=True)
|
||||
return asset
|
||||
|
||||
def load_assets(self, assets_json):
|
||||
assets = json.loads(
|
||||
assets_json,
|
||||
object_hook=Exchange.asset_parser
|
||||
)
|
||||
|
||||
for exchange_symbol in assets:
|
||||
asset_obj = Asset(
|
||||
sid=0,
|
||||
exchange=self.name,
|
||||
**assets[exchange_symbol]
|
||||
)
|
||||
self.assets[exchange_symbol] = asset_obj
|
||||
|
||||
@abstractmethod
|
||||
def subscribe_to_market_data(self, symbol):
|
||||
pass
|
||||
|
||||
@abstractproperty
|
||||
def positions(self):
|
||||
pass
|
||||
|
||||
@abstractproperty
|
||||
def portfolio(self):
|
||||
pass
|
||||
|
||||
@abstractproperty
|
||||
def account(self):
|
||||
pass
|
||||
|
||||
@abstractproperty
|
||||
def time_skew(self):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def order(self, asset, amount, limit_price, stop_price, style):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_open_orders(self, asset):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_order(self, order_id):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def cancel_order(self, order_param):
|
||||
pass
|
||||
|
||||
@abstractmethod
|
||||
def get_spot_value(self, assets, field, dt, data_frequency):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def tickers(self, date, pairs):
|
||||
return
|
||||
|
||||
# @abc.abstractmethod
|
||||
# def new_order(self, symbol, side, order_type, price, amount, leverage):
|
||||
# return
|
||||
#
|
||||
# @abc.abstractmethod
|
||||
# def cancel_order(self, order_id):
|
||||
# return
|
||||
#
|
||||
# @abc.abstractmethod
|
||||
# def order_status(self, order_id):
|
||||
# return
|
||||
#
|
||||
# @abc.abstractmethod
|
||||
# def balance(self, currencies):
|
||||
# return
|
||||
#
|
||||
@@ -0,0 +1 @@
|
||||
{ "btcusd": {"symbol":"btc_usd", "start_date": "2010-01-01"}, "ltcusd": {"symbol":"ltc-usd", "start_date": "2010-01-01"}, "ltcbtc": {"symbol":"ltc_btc", "start_date": "2010-01-01"}, "ethusd": {"symbol":"eth_usd", "start_date": "2010-01-01"}, "ethbtc": {"symbol":"eth_btc", "start_date": "2010-01-01"}, "etcbtc": {"symbol":"etc_btc", "start_date": "2010-01-01"}, "etcusd": {"symbol":"etc_usd", "start_date": "2010-01-01"}, "rrtusd": {"symbol":"rrt_usd", "start_date": "2010-01-01"}, "rrtbtc": {"symbol":"rrt_btc", "start_date": "2010-01-01"}, "zecusd": {"symbol":"zec_usd", "start_date": "2010-01-01"}, "zecbtc": {"symbol":"zec_btc", "start_date": "2010-01-01"}, "xmrusd": {"symbol":"xmr_usd", "start_date": "2010-01-01"}, "xmrbtc": {"symbol":"xmr_btc", "start_date": "2010-01-01"}, "dshusd": {"symbol":"dsh_usd", "start_date": "2010-01-01"}, "dshbtc": {"symbol":"dsh_btc", "start_date": "2010-01-01"}, "bccbtc": {"symbol":"bcc_btc", "start_date": "2010-01-01"}, "bcubtc": {"symbol":"bcu_btc", "start_date": "2010-01-01"}, "bccusd": {"symbol":"bcc_usd", "start_date": "2010-01-01"}, "bcuusd": {"symbol":"bcu_usd", "start_date": "2010-01-01"}, "xrpusd": {"symbol":"xrp_usd", "start_date": "2010-01-01"}, "xrpbtc": {"symbol":"xrp_btc", "start_date": "2010-01-01"}, "iotusd": {"symbol":"iot_usd", "start_date": "2010-01-01"}, "iotbtc": {"symbol":"iot_btc", "start_date": "2010-01-01"}, "ioteth": {"symbol":"iot_eth", "start_date": "2010-01-01"}, "eosusd": {"symbol":"eos_usd", "start_date": "2010-01-01"}, "eosbtc": {"symbol":"eos_btc", "start_date": "2010-01-01"}, "eoseth": {"symbol":"eos_eth", "start_date": "2010-01-01"} }
|
||||
@@ -0,0 +1,27 @@
|
||||
import unittest
|
||||
import abc
|
||||
from abc import ABCMeta
|
||||
|
||||
|
||||
class BaseExchangeTestCase():
|
||||
__metaclass__ = ABCMeta
|
||||
|
||||
@abc.abstractmethod
|
||||
def test_order(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def test_cancel_order(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def test_order_status(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def test_balance(self):
|
||||
pass
|
||||
|
||||
@abc.abstractmethod
|
||||
def test_ticker(self):
|
||||
pass
|
||||
@@ -0,0 +1,56 @@
|
||||
from catalyst.exchange.bitfinex import Bitfinex
|
||||
from .base import BaseExchangeTestCase
|
||||
from logbook import Logger
|
||||
import pandas as pd
|
||||
from catalyst.finance.execution import (MarketOrder,
|
||||
LimitOrder,
|
||||
StopOrder,
|
||||
StopLimitOrder)
|
||||
from catalyst.assets._assets import Asset
|
||||
|
||||
log = Logger('BitfinexTestCase')
|
||||
|
||||
|
||||
class BitfinexTestCase(BaseExchangeTestCase):
|
||||
def test_ticker(self):
|
||||
log.info('fetching ticker from bitfinex')
|
||||
bitfinex = Bitfinex()
|
||||
current_date = pd.Timestamp.utcnow()
|
||||
assets = [
|
||||
Asset(sid=0, exchange=bitfinex.name, symbol='eth_usd'),
|
||||
Asset(sid=1, exchange=bitfinex.name, symbol='etc_usd'),
|
||||
Asset(sid=2, exchange=bitfinex.name, symbol='eos_usd')
|
||||
]
|
||||
tickers = bitfinex.tickers(date=current_date, assets=assets)
|
||||
log.info('got tickers {}'.format(tickers))
|
||||
|
||||
def test_order(self):
|
||||
log.info('ordering from bitfinex')
|
||||
bitfinex = Bitfinex()
|
||||
asset = Asset(sid=0, exchange=bitfinex.name, symbol='eth_usd')
|
||||
order_id = bitfinex.order(
|
||||
asset=asset,
|
||||
style=LimitOrder(limit_price=200),
|
||||
limit_price=200,
|
||||
amount=1,
|
||||
stop_price=None
|
||||
)
|
||||
log.info('order created {}'.format(order_id))
|
||||
|
||||
def test_cancel_order(self):
|
||||
log.info('canceling order from bitfinex')
|
||||
bitfinex = Bitfinex()
|
||||
response = bitfinex.cancel_order(order_id=2776936269)
|
||||
log.info('canceled order: {}'.format(response))
|
||||
|
||||
def test_order_status(self):
|
||||
log.info('querying orders from bitfinex')
|
||||
bitfinex = Bitfinex()
|
||||
response = bitfinex.order_status(order_id=2776972180)
|
||||
log.info('the orders: {}'.format(response))
|
||||
|
||||
def test_balance(self):
|
||||
log.info('querying positions from bitfinex')
|
||||
bitfinex = Bitfinex()
|
||||
balance = bitfinex.balance(currencies=['usd', 'etc', 'pez'])
|
||||
log.info('the balance: {}'.format(balance))
|
||||
Reference in New Issue
Block a user