Skip to content

Commit

Permalink
fixes #37, fixes #30
Browse files Browse the repository at this point in the history
  • Loading branch information
timkpaine committed Jul 2, 2019
1 parent a626ab8 commit 9e94850
Show file tree
Hide file tree
Showing 10 changed files with 121 additions and 227 deletions.
67 changes: 27 additions & 40 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -363,23 +363,43 @@ Apart from writing new strategies, this library can be extended by adding new ex
Here is the coinbase exchange. Most of the code is to manage different websocket subscription options, and to convert between `aat`, `ccxt` and exchange-specific formatting of things like symbols, order types, etc.

```python3
class CoinbaseExchange(CoinbaseMixins, Exchange):
class CoinbaseExchange(Exchange):
@lru_cache(None)
def subscription(self):
return [json.dumps({"type": "subscribe", "product_id": self.currencyPairToString(x)}) for x in self.options().currency_pairs]
return [json.dumps({"type": "subscribe", "product_id": x.value[0].value + '-' + x.value[1].value}) for x in self.options().currency_pairs]

@lru_cache(None)
def heartbeat(self):
return json.dumps({"type": "heartbeat", "on": True})

class CoinbaseMixins(object):
def tickToData(self, jsn: dict) -> MarketData:
'''convert a jsn tick off the websocket to a MarketData struct'''
if jsn.get('type') == 'received':
return
typ = self.strToTradeType(jsn.get('type'), jsn.get('reason', ''))

s = jsn.get('type').upper()
reason = jsn.get('reason', '').upper()
if s == 'MATCH' or (s == 'DONE' and reason == 'FILLED'):
typ = TickType.TRADE
elif s in ('OPEN', 'DONE', 'CHANGE', 'HEARTBEAT'):
if reason == 'CANCELED':
typ = TickType.CANCEL
elif s == 'DONE':
typ = TickType.FILL
else:
typ = TickType_from_string(s.upper())
else:
typ = TickType.ERROR

order_id = jsn.get('order_id', jsn.get('maker_order_id', ''))
time = parse_date(jsn.get('time')) if jsn.get('time') else datetime.now()

if typ in (TickType.CANCEL, TickType.OPEN):
volume = float(jsn.get('remaining_size', 'nan'))
else:
volume = float(jsn.get('size', 'nan'))
price = float(jsn.get('price', 'nan'))
volume = float(jsn.get('size', 'nan'))

currency_pair = str_to_currency_pair_type(jsn.get('product_id')) if typ != TickType.ERROR else PairType.NONE

instrument = Instrument(underlying=currency_pair)
Expand All @@ -389,7 +409,8 @@ class CoinbaseMixins(object):
remaining_volume = float(jsn.get('remaining_size', 0.0))

sequence = int(jsn.get('sequence', -1))
ret = MarketData(time=time,
ret = MarketData(order_id=order_id,
time=time,
volume=volume,
price=price,
type=typ,
Expand All @@ -400,38 +421,4 @@ class CoinbaseMixins(object):
order_type=order_type,
sequence=sequence)
return ret

def strToTradeType(self, s: str, reason: str = '') -> TickType:
if s == 'match':
return TickType.TRADE
elif s in ('open', 'done', 'change', 'heartbeat'):
if reason == 'canceled':
return TickType.CANCEL
elif reason == 'filled':
return TickType.FILL
return TickType(s.upper())
else:
return TickType.ERROR

def tradeReqToParams(self, req) -> dict:
p = {}
p['price'] = str(req.price)
p['size'] = str(req.volume)
p['product_id'] = self.currencyPairToString(req.instrument.currency_pair)
p['type'] = self.orderTypeToString(req.order_type)

if req.order_sub_type == OrderSubType.FILL_OR_KILL:
p['time_in_force'] = 'FOK'
elif req.order_sub_type == OrderSubType.POST_ONLY:
p['post_only'] = '1'
return p

def currencyPairToString(self, cur: PairType) -> str:
return cur.value[0].value + '-' + cur.value[1].value

def orderTypeToString(self, typ: OrderType) -> str:
return typ.value.lower()

def reasonToTradeType(self, s: str) -> TickType:
pass
```
40 changes: 18 additions & 22 deletions aat/exchanges/coinbase.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
import json
from functools import lru_cache
from datetime import datetime
from ..enums import OrderSubType, PairType, TickType, TickType_from_string
from ..enums import PairType, TickType, TickType_from_string
from ..exchange import Exchange
from ..structs import MarketData, Instrument
from ..utils import parse_date, str_to_currency_pair_type, str_to_side, str_to_order_type
Expand All @@ -10,31 +10,40 @@
class CoinbaseExchange(Exchange):
@lru_cache(None)
def subscription(self):
return [json.dumps({"type": "subscribe", "product_id": self.currencyPairToString(x)}) for x in self.options().currency_pairs]
return [json.dumps({"type": "subscribe", "product_id": x.value[0].value + '-' + x.value[1].value}) for x in self.options().currency_pairs]

@lru_cache(None)
def heartbeat(self):
return json.dumps({"type": "heartbeat", "on": True})

def tickToData(self, jsn: dict) -> MarketData:
'''convert a jsn tick off the websocket to a MarketData struct'''
if jsn.get('type') == 'received':
return

s = jsn.get('type')
reason = jsn.get('reason')
if s == 'match' or (s == 'done' and reason == 'filled'):
s = jsn.get('type').upper()
reason = jsn.get('reason', '').upper()
if s == 'MATCH' or (s == 'DONE' and reason == 'FILLED'):
typ = TickType.TRADE
elif s in ('open', 'done', 'change', 'heartbeat'):
if reason == 'canceled':
elif s in ('OPEN', 'DONE', 'CHANGE', 'HEARTBEAT'):
if reason == 'CANCELED':
typ = TickType.CANCEL
typ = TickType_from_string(s.upper())
elif s == 'DONE':
typ = TickType.FILL
else:
typ = TickType_from_string(s.upper())
else:
typ = TickType.ERROR

order_id = jsn.get('order_id', jsn.get('maker_order_id', ''))
time = parse_date(jsn.get('time')) if jsn.get('time') else datetime.now()

if typ in (TickType.CANCEL, TickType.OPEN):
volume = float(jsn.get('remaining_size', 'nan'))
else:
volume = float(jsn.get('size', 'nan'))
price = float(jsn.get('price', 'nan'))
volume = float(jsn.get('size', 'nan'))

currency_pair = str_to_currency_pair_type(jsn.get('product_id')) if typ != TickType.ERROR else PairType.NONE

instrument = Instrument(underlying=currency_pair)
Expand All @@ -56,16 +65,3 @@ def tickToData(self, jsn: dict) -> MarketData:
order_type=order_type,
sequence=sequence)
return ret

def tradeReqToParams(self, req) -> dict:
p = {}
p['price'] = str(req.price)
p['size'] = str(req.volume)
p['product_id'] = req.instrument.currency_pair.value[0].value + '-' + req.instrument.currency_pair.value[1].value
p['type'] = req.order_type.value.lower()

if req.order_sub_type == OrderSubType.FILL_OR_KILL:
p['time_in_force'] = 'FOK'
elif req.order_sub_type == OrderSubType.POST_ONLY:
p['post_only'] = '1'
return p
44 changes: 16 additions & 28 deletions aat/exchanges/gemini.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,18 +5,20 @@
import hmac
import time
from aiostream import stream
from datetime import datetime
from functools import lru_cache
from ..define import EXCHANGE_MARKET_DATA_ENDPOINT
from ..enums import TickType, OrderType, OrderSubType
from ..structs import MarketData
from ..enums import TickType, TickType_from_string, OrderType, OrderSubType
from ..exchange import Exchange
from ..logging import log
from ..structs import MarketData, Instrument
from ..utils import str_to_side, str_to_currency_pair_type


class GeminiExchange(Exchange):
@lru_cache(None)
def subscription(self):
return [json.dumps({"type": "subscribe", "product_id": self.currencyPairToString(x)}) for x in self.options().currency_pairs]
return [json.dumps({"type": "subscribe", "product_id": x.value[0].value + x.value[1].value}) for x in self.options().currency_pairs]

@lru_cache(None)
def heartbeat(self):
Expand Down Expand Up @@ -88,14 +90,19 @@ async def get_data_sub_pair(ws, sub=None):

for item in events:
if item.get('type', 'subscription_ack') in ('subscription_ack', 'heartbeat'):
# can skip these
continue
if item.get('type') == 'accepted':
# can ignore these as well
# can ignore these as well, will have a fill and/or booked
# https://docs.gemini.com/websocket-api/#workflow
continue
if item.get('type') == 'closed':
# can ignore these as well, will have a fill or cancelled
# https://docs.gemini.com/websocket-api/#workflow
continue

if pair is None:
# private events
import ipdb; ipdb.set_trace()
pair = item['symbol']

item['symbol'] = pair
Expand All @@ -113,10 +120,10 @@ def tickToData(self, jsn: dict) -> MarketData:
price = float(jsn.get('price', 'nan'))
volume = float(jsn.get('amount', 0.0))

s = jsn.get('type')
if s in ('BLOCK_TRADE', ):
s = jsn.get('type').upper()
if s in ('BLOCK_TRADE', 'FILL'): # Market data can't trigger fill event
typ = TickType.TRADE
elif s in ('AUCTION_INDICATIVE', 'AUCTION_OPEN'):
elif s in ('AUCTION_INDICATIVE', 'AUCTION_OPEN', 'BOOKED', 'INITIAL'):
typ = TickType.OPEN
else:
typ = TickType_from_string(s)
Expand All @@ -128,7 +135,7 @@ def tickToData(self, jsn: dict) -> MarketData:
# typ = self.reasonToTradeType(reason)

side = str_to_side(jsn.get('side', ''))
remaining_volume = float(jsn.get('remaining', 'nan'))
remaining_volume = float(jsn.get('remaining', jsn.get('remaining_amount', 'nan')))
sequence = -1

if 'symbol' not in jsn:
Expand All @@ -148,22 +155,3 @@ def tickToData(self, jsn: dict) -> MarketData:
exchange=self.exchange(),
sequence=sequence)
return ret

def tradeReqToParams(self, req) -> dict:
p = {}
p['price'] = str(req.price)
p['size'] = str(req.volume)
p['product_id'] = req.instrument.currency_pair.value[0].value + req.instrument.currency_pair.value[1].value
p['type'] = req.order_type.value.lower()

if p['type'] == OrderType.MARKET:
if req.side == Side.BUY:
p['price'] = 100000000.0
else:
p['price'] = .00000001

if req.order_sub_type == OrderSubType.FILL_OR_KILL:
p['time_in_force'] = 'FOK'
elif req.order_sub_type == OrderSubType.POST_ONLY:
p['post_only'] = '1'
return p
3 changes: 0 additions & 3 deletions aat/exchanges/kraken.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,3 @@ def heartbeat(self):

def tickToData(self, jsn: dict) -> MarketData:
raise NotImplementedError()

def tradeReqToParams(self, req) -> dict:
raise NotImplementedError()
3 changes: 0 additions & 3 deletions aat/exchanges/poloniex.py
Original file line number Diff line number Diff line change
Expand Up @@ -24,9 +24,6 @@ def heartbeat(self):
def tickToData(self, jsn: dict) -> MarketData:
raise NotImplementedError()

def tradeReqToParams(self, req) -> dict:
raise NotImplementedError()

POLONIEX_CURRENCY_ID = {
'1CR': '1',
'ABY': '2',
Expand Down
2 changes: 1 addition & 1 deletion aat/order_entry.py
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,7 @@ def orderBook(self, level=1):

def _extract_fields(self, order, exchange):
side = order.get('side', order.get('info', {}).get('side'))
filled = float(order.get('filled') or order.get('info', {}).get('executed_amount') or order.get('info', {}).get('executed_amount'))
filled = float(order.get('filled') or order.get('info', {}).get('filled_size') or order.get('info', {}).get('executed_amount'))
price = order.get('price') or order.get('info', {}).get('price')
datetime = order.get('datetime') or order.get('info', {}).get('timestamp')
status = order.get('status')
Expand Down
29 changes: 0 additions & 29 deletions aat/tests/exchanges/test_coinbase.py
Original file line number Diff line number Diff line change
Expand Up @@ -74,32 +74,3 @@ def test_receive(self):
# assert e._missingseqnum
# e.receive()
# assert e._missingseqnum == set()

def test_trade_req_to_params_coinbase(self):
from ...config import ExchangeConfig
from ...exchanges.coinbase import CoinbaseExchange
from ...enums import PairType, OrderType, OrderSubType, ExchangeType
from ...structs import Instrument

ec = ExchangeConfig()
ec.exchange_type = ExchangeType.COINBASE
e = CoinbaseExchange(ExchangeType.COINBASE, ec)

class tmp:
def __init__(self, a=True):
self.price = 'test'
self.volume = 'test'
self.instrument = Instrument(underlying=PairType.BTCUSD)
self.order_type = OrderType.LIMIT
self.order_sub_type = OrderSubType.POST_ONLY if a \
else OrderSubType.FILL_OR_KILL

res1 = e.tradeReqToParams(tmp())
res2 = e.tradeReqToParams(tmp(False))

assert(res1['price'] == 'test')
assert(res1['size'] == 'test')
assert(res1['product_id'] == 'BTC-USD')
assert(res1['type'] == 'limit')
assert(res1['post_only'] == '1')
assert(res2['time_in_force'] == 'FOK')
29 changes: 0 additions & 29 deletions aat/tests/exchanges/test_gemini.py
Original file line number Diff line number Diff line change
Expand Up @@ -54,32 +54,3 @@ def test_receive(self):
"price": "1059.54"
}]}
e.receive()

def test_trade_req_to_params_gemini(self):
from ...exchanges.gemini import GeminiExchange
from ...enums import PairType, OrderType, OrderSubType
from ...structs import Instrument
from ...config import ExchangeConfig
from ...enums import ExchangeType
ec = ExchangeConfig()
ec.exchange_type = ExchangeType.GEMINI
e = GeminiExchange(ExchangeType.GEMINI, ec)

class tmp:
def __init__(self, a=True):
self.price = 'test'
self.volume = 'test'
self.instrument = Instrument(underlying=PairType.BTCUSD)
self.order_type = OrderType.LIMIT
self.order_sub_type = OrderSubType.POST_ONLY if a \
else OrderSubType.FILL_OR_KILL

res1 = e.tradeReqToParams(tmp())
res2 = e.tradeReqToParams(tmp(False))

assert(res1['price'] == 'test')
assert(res1['size'] == 'test')
assert(res1['product_id'] == 'BTCUSD')
assert(res1['type'] == 'limit')
assert(res1['post_only'] == '1')
assert(res2['time_in_force'] == 'FOK')
2 changes: 1 addition & 1 deletion cpp/include/aat/common.h
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
#pragma once

#define ENUM_TO_STRING(type) std::string type##_to_string(type typ) { return type##_names[static_cast<int>(typ)]; }
#define ENUM_FROM_STRING(type) type type##_from_string(char *s) { if(_##type##_mapping.find(s) == _##type##_mapping.end()){ throw py::value_error(); } return _##type##_mapping[s]; }
#define ENUM_FROM_STRING(type) type type##_from_string(char *s) { if(_##type##_mapping.find(s) == _##type##_mapping.end()){ throw py::value_error(s); } return _##type##_mapping[s]; }

0 comments on commit 9e94850

Please sign in to comment.