Code documentation and cleanup

This commit is contained in:
Frederic Fortier
2017-08-14 09:01:41 -04:00
parent 0b16cce71e
commit 383370f9d8
4 changed files with 350 additions and 117 deletions
+187 -64
View File
@@ -6,6 +6,7 @@ import json
import time
import requests
import pandas as pd
from catalyst.protocol import Portfolio, Account
# from websocket import create_connection
from catalyst.exchange.exchange import Exchange, RTVolumeBar, Position
from logbook import Logger
@@ -18,7 +19,7 @@ from catalyst.finance.execution import (MarketOrder,
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"} }'
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')
@@ -35,7 +36,7 @@ class Bitfinex(Exchange):
self.assets = {}
self.load_assets(ASSETS)
def request(self, operation, data, version='v1'):
def _request(self, operation, data, version='v1'):
payload_object = {
'request': '/{}/{}'.format(version, operation),
'nonce': '{0:f}'.format(time.time() * 100000), # convert to string
@@ -66,14 +67,16 @@ class Bitfinex(Exchange):
if data is None:
request = requests.get(
self.url + '/{version}/{operation}'.format(
'{url}/{version}/{operation}'.format(
url=self.url,
version=version,
operation=operation
), data={},
headers=headers)
else:
request = requests.post(
self.url + '/{version}/{operation}'.format(
'{url}/{version}/{operation}'.format(
url=self.url,
version=version,
operation=operation
),
@@ -81,56 +84,136 @@ class Bitfinex(Exchange):
return request
def subscribe_to_market_data(self, symbol):
pass
def _get_v2_symbols(self, assets):
"""
Workaround to support Bitfinex v2
TODO: Might require a separate asset dictionary
def positions(self):
pass
:param assets:
:return:
"""
def portfolio(self):
pass
def account(self):
pass
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
@property
def time_skew(self):
# TODO: research the time skew conditions
return None
def portfolio(self):
"""
TODO: I'm not sure how that's used yet
:return:
"""
portfolio = Portfolio()
portfolio.capital_used = None
portfolio.starting_cash = 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
portfolio.portfolio_value = None
portfolio.pnl = None
portfolio.cash = None
def get_order(self, order_id):
pass
portfolio.returns = None
portfolio.start_date = None
portfolio.positions = self.positions
portfolio.positions_value = None
portfolio.positions_exposure = None
def get_spot_value(self, assets, field, dt, data_frequency):
raise NotImplementedError()
return portfolio
def balance(self, currencies):
response = self.request('balances', None)
@property
def account(self):
account = Account()
account.settled_cash = None
account.accrued_interest = None
account.buying_power = None
account.equity_with_loan = None
account.total_positions_value = None
account.total_positions_exposure = None
account.regt_equity = None
account.regt_margin = None
account.initial_margin_requirement = None
account.maintenance_margin_requirement = None
account.available_funds = None
account.excess_liquidity = None
account.cushion = None
account.day_trades_remaining = None
account.leverage = None
account.net_leverage = None
account.net_liquidation = None
return account
@property
def positions(self):
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
return positions
@property
def time_skew(self):
# TODO: research the time skew conditions
return None
def subscribe_to_market_data(self, symbol):
pass
def get_spot_value(self, assets, field, dt, data_frequency):
raise NotImplementedError()
# 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.
"""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`
"""
is_buy = (amount > 0)
@@ -153,7 +236,7 @@ class Bitfinex(Exchange):
price=str(float(price)),
side='buy' if is_buy else 'sell',
type='exchange ' + order_type, # TODO: support margin trades
exchange='bitfinex',
exchange=self.name,
is_hidden=False,
is_postonly=False,
use_all_available=0,
@@ -162,7 +245,7 @@ class Bitfinex(Exchange):
sell_price_oco=0
)
response = self.request('order/new', req)
response = self._request('order/new', req)
exchange_order = response.json()
if 'message' in exchange_order:
raise ValueError(
@@ -183,20 +266,51 @@ class Bitfinex(Exchange):
return order_id
def cancel_order(self, order_id):
response = self.request('order/cancel', {'order_id': order_id})
status = response.json()
return status
def get_open_orders(self, asset):
"""Retrieve all of the current open orders.
def order_status(self, order_id):
response = self.request('order/status', {'order_id': int(order_id)})
Parameters
----------
asset : Asset
If passed and not None, return only the open orders for the given
asset instead of all open orders.
Returns
-------
open_orders : dict[list[Order]] or list[Order]
If no asset is passed this will return a dict mapping Assets
to a list containing all the open orders for the asset.
If an asset is passed then this will return a list of the open
orders for this 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):
"""Lookup an order based on the order id returned from one of the
order functions.
Parameters
----------
order_id : str
The unique identifier for the order.
Returns
-------
order : Order
The order object.
"""
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')
result = dict(exchange=self.name)
if order_status['is_cancelled']:
warning_logger.warn(
@@ -207,39 +321,48 @@ class Bitfinex(Exchange):
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'])
result['executed_price'] = \
float(order_status['avg_execution_price'])
result['executed_amount'] = \
float(order_status['executed_amount'])
else:
result['status'] = 'open'
# TODO: what's the right format?
return result
def get_v2_symbols(self, assets):
"""
Workaround to support Bitfinex v2
TODO: Might require a separate asset dictionary
def cancel_order(self, order_id):
"""Cancel an open order.
Parameters
----------
order_param : str or Order
The order_id or order object to cancel.
"""
response = self._request('order/cancel', {'order_id': order_id})
status = response.json()
return status
def tickers(self, date, assets):
"""
Fetch ticket data for assets
https://docs.bitfinex.com/v2/reference#rest-public-tickers
:param date:
: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)
symbols = self._get_v2_symbols(assets)
log.debug('fetching tickers {}'.format(symbols))
request = requests.get(
self.url + '/v2/tickers?symbols={}'.format(','.join(symbols))
'{url}/v2/tickers?symbols={symbols}'.format(
url=self.url,
symbols=','.join(symbols),
)
)
tickers = request.json()
if 'message' in tickers:
raise ValueError(
'Unable to retrieve tickers: %s' % tickers['message']
+85 -18
View File
@@ -42,6 +42,15 @@ class Exchange:
return symbol
def get_asset(self, symbol):
asset = None
for key in self.assets:
if not asset and self.assets[key].symbol == symbol:
asset = self.assets[key]
return asset
def get_symbols(self, assets):
symbols = []
for asset in assets:
@@ -63,7 +72,7 @@ class Exchange:
for exchange_symbol in assets:
asset_obj = Asset(
sid=0,
sid=abs(hash(assets[exchange_symbol]['symbol'])) % (10 ** 4),
exchange=self.name,
**assets[exchange_symbol]
)
@@ -91,18 +100,93 @@ class Exchange:
@abstractmethod
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``.
See Also
--------
:class:`catalyst.finance.execution.ExecutionStyle`
:func:`catalyst.api.order_value`
:func:`catalyst.api.order_percent`
"""
pass
@abstractmethod
def get_open_orders(self, asset):
"""Retrieve all of the current open orders.
Parameters
----------
asset : Asset
If passed and not None, return only the open orders for the given
asset instead of all open orders.
Returns
-------
open_orders : dict[list[Order]] or list[Order]
If no asset is passed this will return a dict mapping Assets
to a list containing all the open orders for the asset.
If an asset is passed then this will return a list of the open
orders for this asset.
"""
pass
@abstractmethod
def get_order(self, order_id):
"""Lookup an order based on the order id returned from one of the
order functions.
Parameters
----------
order_id : str
The unique identifier for the order.
Returns
-------
order : Order
The order object.
"""
pass
@abstractmethod
def cancel_order(self, order_param):
"""Cancel an open order.
Parameters
----------
order_param : str or Order
The order_id or order object to cancel.
"""
pass
@abstractmethod
@@ -112,20 +196,3 @@ class Exchange:
@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
#
+31 -12
View File
@@ -1,27 +1,46 @@
import unittest
import abc
from abc import ABCMeta
from abc import ABCMeta, abstractmethod
class BaseExchangeTestCase():
__metaclass__ = ABCMeta
@abc.abstractmethod
@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
@abc.abstractmethod
@abstractmethod
def test_get_order(self):
pass
@abstractmethod
def test_cancel_order(self):
pass
@abc.abstractmethod
def test_order_status(self):
@abstractmethod
def test_spot_value(self):
pass
@abc.abstractmethod
def test_balance(self):
pass
@abc.abstractmethod
def test_ticker(self):
@abstractmethod
def test_tickers(self):
pass
+47 -23
View File
@@ -6,51 +6,75 @@ from catalyst.finance.execution import (MarketOrder,
LimitOrder,
StopOrder,
StopLimitOrder)
from catalyst.assets._assets import Asset
import catalyst.protocol as protocol
log = Logger('BitfinexTestCase')
class BitfinexTestCase(BaseExchangeTestCase):
def test_ticker(self):
log.info('fetching ticker from bitfinex')
def test_positions(self):
log.info('querying positions 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))
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')
pass
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,
asset=bitfinex.get_asset('eth_usd'),
style=LimitOrder(limit_price=200),
limit_price=200,
amount=1,
stop_price=None
)
log.info('order created {}'.format(order_id))
pass
def test_get_order(self):
log.info('querying orders from bitfinex')
bitfinex = Bitfinex()
response = bitfinex.order_status(order_id=3330866978)
log.info('the orders: {}'.format(response))
pass
def test_cancel_order(self):
log.info('canceling order from bitfinex')
bitfinex = Bitfinex()
response = bitfinex.cancel_order(order_id=2776936269)
response = bitfinex.cancel_order(order_id=3330847408)
log.info('canceled order: {}'.format(response))
pass
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_spot_value(self):
log.info('spot valud not implemented')
pass
def test_balance(self):
log.info('querying positions from bitfinex')
def test_tickers(self):
log.info('fetching ticker from bitfinex')
bitfinex = Bitfinex()
balance = bitfinex.balance(currencies=['usd', 'etc', 'pez'])
log.info('the balance: {}'.format(balance))
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))
pass