diff --git a/.travis.yml b/.travis.yml index 02684ef..49bbd71 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,12 +1,12 @@ -language: python -python: - - "3.4" - - "3.5" - - "3.6" - # PyPy versions - - "pypy3.5" -# command to install dependencies -install: - - pip install -r requirements.txt -# command to run tests -script: pylint bfxapi +language: python +python: + - "3.4" + - "3.5" + - "3.6" + # PyPy versions + - "pypy3.5" +# command to install dependencies +install: + - pip install -r requirements.txt +# command to run tests +script: pylint --rcfile=pylint.rc bfxapi diff --git a/bfxapi/Client.py b/bfxapi/Client.py index 8a9d285..c5be0e0 100644 --- a/bfxapi/Client.py +++ b/bfxapi/Client.py @@ -1,13 +1,29 @@ +""" +This module exposes the core bitfinex clients which includes both +a websocket client and a rest interface client +""" + +# pylint: disable-all + import asyncio from .websockets.BfxWebsocket import BfxWebsocket from .rest.BfxRest import BfxRest +REST_HOST = 'https://api.bitfinex.com/v2' +WS_HOST = 'wss://api.bitfinex.com/ws/2' + class Client: - def __init__(self, API_KEY=None, API_SECRET=None, rest_host='https://api.bitfinex.com/v2', - ws_host='wss://api.bitfinex.com/ws/2', loop=None, logLevel='INFO', dead_man_switch=False, *args, **kwargs): - self.loop = loop or asyncio.get_event_loop() - self.ws = BfxWebsocket(API_KEY=API_KEY, API_SECRET=API_SECRET, host=ws_host, - loop=self.loop, logLevel=logLevel, dead_man_switch=dead_man_switch, *args, **kwargs) - self.rest = BfxRest(API_KEY=API_KEY, API_SECRET=API_SECRET, host=rest_host, - loop=self.loop, logLevel=logLevel, *args, **kwargs) + """ + The bfx client exposes rest and websocket objects + """ + + def __init__(self, API_KEY=None, API_SECRET=None, rest_host=REST_HOST, + ws_host=WS_HOST, loop=None, logLevel='INFO', dead_man_switch=False, + *args, **kwargs): + self.loop = loop or asyncio.get_event_loop() + self.ws = BfxWebsocket(API_KEY=API_KEY, API_SECRET=API_SECRET, host=ws_host, + loop=self.loop, logLevel=logLevel, dead_man_switch=dead_man_switch, + *args, **kwargs) + self.rest = BfxRest(API_KEY=API_KEY, API_SECRET=API_SECRET, host=rest_host, + loop=self.loop, logLevel=logLevel, *args, **kwargs) diff --git a/bfxapi/__init__.py b/bfxapi/__init__.py index 9c22fe5..1f9755e 100644 --- a/bfxapi/__init__.py +++ b/bfxapi/__init__.py @@ -1,5 +1,10 @@ -name = 'bfxapi' +""" +This module is used to interact with the bitfinex api +""" -from bfxapi.Client import Client -from bfxapi.websockets.GenericWebsocket import GenericWebsocket -from bfxapi.models import * +from .client import Client +from .models import (Order, Trade, OrderBook, Subscription, Wallet, + Position, FundingLoan, FundingOffer, FundingCredit) +from .websockets.GenericWebsocket import GenericWebsocket + +NAME = 'bfxapi' diff --git a/bfxapi/client.py b/bfxapi/client.py new file mode 100644 index 0000000..c5be0e0 --- /dev/null +++ b/bfxapi/client.py @@ -0,0 +1,29 @@ +""" +This module exposes the core bitfinex clients which includes both +a websocket client and a rest interface client +""" + +# pylint: disable-all + +import asyncio + +from .websockets.BfxWebsocket import BfxWebsocket +from .rest.BfxRest import BfxRest + +REST_HOST = 'https://api.bitfinex.com/v2' +WS_HOST = 'wss://api.bitfinex.com/ws/2' + +class Client: + """ + The bfx client exposes rest and websocket objects + """ + + def __init__(self, API_KEY=None, API_SECRET=None, rest_host=REST_HOST, + ws_host=WS_HOST, loop=None, logLevel='INFO', dead_man_switch=False, + *args, **kwargs): + self.loop = loop or asyncio.get_event_loop() + self.ws = BfxWebsocket(API_KEY=API_KEY, API_SECRET=API_SECRET, host=ws_host, + loop=self.loop, logLevel=logLevel, dead_man_switch=dead_man_switch, + *args, **kwargs) + self.rest = BfxRest(API_KEY=API_KEY, API_SECRET=API_SECRET, host=rest_host, + loop=self.loop, logLevel=logLevel, *args, **kwargs) diff --git a/bfxapi/examples/ws/resubscribe_orderbook.py b/bfxapi/examples/ws/resubscribe_orderbook.py index a48c3db..5e3362a 100644 --- a/bfxapi/examples/ws/resubscribe_orderbook.py +++ b/bfxapi/examples/ws/resubscribe_orderbook.py @@ -27,7 +27,7 @@ async def on_subscribe(subscription): @bfx.ws.once('subscribed') async def on_once_subscribe(subscription): print ("Performig resubscribe") - await bfx.ws.resubscribe(subscription.chanId) + await bfx.ws.resubscribe(subscription.chan_id) async def start(): diff --git a/bfxapi/models/FundingCredit.py b/bfxapi/models/FundingCredit.py deleted file mode 100644 index 016d555..0000000 --- a/bfxapi/models/FundingCredit.py +++ /dev/null @@ -1,86 +0,0 @@ - -class FundingCreditModel: - ID = 0 - SYMBOL = 1 - SIDE = 2 - MTS_CREATE = 3 - MTS_UPDATE = 4 - AMOUNT = 5 - FLAGS = 6 - STATUS = 7 - RATE = 11 - PERIOD = 12 - MTS_OPENING = 13 - MTS_LAST_PAYOUT = 14 - NOTIFY = 15 - HIDDEN = 16 - RENEW = 18 - NO_CLOSE = 20 - POSITION_PAIR = 21 - -class FundingLoan: - """ - ID integer Offer ID - SYMBOL string The currency of the offer (fUSD, etc) - SIDE string "Lend" or "Loan" - MTS_CREATE int Millisecond Time Stamp when the offer was created - MSG_UPDATE int Millisecond Time Stamp when the offer was updated - AMOUNT float Amount the offer is for - FLAGS object future params object (stay tuned) - STATUS string Offer Status: ACTIVE, EXECUTED, PARTIALLY FILLED, CANCELED - RATE float Rate of the offer - PERIOD int Period of the offer - MTS_OPENING int Millisecond Time Stamp when funding opened - MTS_LAST_PAYOUT int Millisecond Time Stamp when last payout received - NOTIFY int 0 if false, 1 if true - HIDDEN int 0 if false, 1 if true - RENEW int 0 if false, 1 if true - NO_CLOSE int 0 if false, 1 if true Whether the funding will be closed when the position is closed - POSITION_PAIR string Pair of the position that the funding was used for - """ - - def __init__(self, id, symbol, side, mts_create, mts_update, amount, flags, status, rate, - period, mts_opening, mts_last_payout, notify, hidden, renew, no_close, position_pair): - self.id = id - self.symbol = symbol - self.side = side - self.mts_create = mts_create - self.mts_update = mts_update - self.amount = amount - self.flags = flags - self.status = status - self.rate = rate - self.period = period - self.mts_opening = mts_opening - self.mts_last_payout = mts_last_payout - self.notify = notify - self.hidden = hidden - self.renew = renew - self.no_close = no_close - self.position_pair = position_pair - - @staticmethod - def from_raw_credit(raw_credit): - id = raw_credit[FundingCreditModel.ID] - symbol = raw_credit[FundingCreditModel.SYMBOL] - side = raw_credit[FundingCreditModel.SIDE] - mts_create = raw_credit[FundingCreditModel.MTS_CREATE] - mts_update = raw_credit[FundingCreditModel.MTS_UPDATE] - amount = raw_credit[FundingCreditModel.AMOUNT] - flags = raw_credit[FundingCreditModel.FLAGS] - status = raw_credit[FundingCreditModel.STATUS] - rate = raw_credit[FundingCreditModel.RATE] - period = raw_credit[FundingCreditModel.PERIOD] - mts_opening = raw_credit[FundingCreditModel.MTS_OPENING] - mts_last_payout = raw_credit[FundingCreditModel.MTS_LAST_PAYOUT] - notify = raw_credit[FundingCreditModel.NOTIFY] - hidden = raw_credit[FundingCreditModel.HIDDEN] - renew = raw_credit[FundingCreditModel.RENEW] - no_close = raw_credit[FundingCreditModel.NO_CLOSE] - position_pair = raw_credit[FundingCreditModel.POSITION_PAIR] - return FundingLoan(id, symbol, side, mts_create, mts_update, amount, flags, status, rate, - period, mts_opening, mts_last_payout, notify, hidden, renew, no_close, position_pair) - - def __str__(self): - return "FundingCredit '{}' ".format( - self.symbol, self.id, self.rate, self.amount, self.period, self.status) diff --git a/bfxapi/models/FundingLoan.py b/bfxapi/models/FundingLoan.py deleted file mode 100644 index 211712a..0000000 --- a/bfxapi/models/FundingLoan.py +++ /dev/null @@ -1,82 +0,0 @@ - -class FundingLoanModel: - ID = 0 - SYMBOL = 1 - SIDE = 2 - MTS_CREATE = 3 - MTS_UPDATE = 4 - AMOUNT = 5 - FLAGS = 6 - STATUS = 7 - RATE = 11 - PERIOD = 12 - MTS_OPENING = 13 - MTS_LAST_PAYOUT = 14 - NOTIFY = 15 - HIDDEN = 16 - RENEW = 18 - NO_CLOSE = 20 - -class FundingLoan: - """ - ID integer Offer ID - SYMBOL string The currency of the offer (fUSD, etc) - SIDE string "Lend" or "Loan" - MTS_CREATE int Millisecond Time Stamp when the offer was created - MTS_UPDATE int Millisecond Time Stamp when the offer was created - AMOUNT float Amount the offer is for - FLAGS object future params object (stay tuned) - STATUS string Offer Status: ACTIVE, EXECUTED, PARTIALLY FILLED, CANCELED - RATE float Rate of the offer - PERIOD int Period of the offer - MTS_OPENING int Millisecond Time Stamp for when the loan was opened - MTS_LAST_PAYOUT int Millisecond Time Stamp for when the last payout was made - NOTIFY int 0 if false, 1 if true - HIDDEN int 0 if false, 1 if true - RENEW int 0 if false, 1 if true - NO_CLOSE int If funding will be returned when position is closed. 0 if false, 1 if true - """ - - def __init__(self, id, symbol, side, mts_create, mts_update, amount, flags, status, rate, - period, mts_opening, mts_last_payout, notify, hidden, renew, no_close): - self.id = id - self.symbol = symbol - self.side = side - self.mts_create = mts_create - self.mts_update = mts_update - self.amount = amount - self.flags = flags - self.status = status - self.rate = rate - self.period = period - self.mts_opening = mts_opening - self.mts_last_payout = mts_last_payout - self.notify = notify - self.hidden = hidden - self.renew = renew - self.no_close = no_close - - @staticmethod - def from_raw_loan(raw_loan): - id = raw_loan[FundingLoanModel.ID] - symbol = raw_loan[FundingLoanModel.SYMBOL] - side = raw_loan[FundingLoanModel.SIDE] - mts_create = raw_loan[FundingLoanModel.MTS_CREATE] - mts_update = raw_loan[FundingLoanModel.MTS_UPDATE] - amount = raw_loan[FundingLoanModel.AMOUNT] - flags = raw_loan[FundingLoanModel.FLAGS] - status = raw_loan[FundingLoanModel.STATUS] - rate = raw_loan[FundingLoanModel.RATE] - period = raw_loan[FundingLoanModel.PERIOD] - mts_opening = raw_loan[FundingLoanModel.MTS_OPENING] - mts_last_payout = raw_loan[FundingLoanModel.MTS_LAST_PAYOUT] - notify = raw_loan[FundingLoanModel.NOTIFY] - hidden = raw_loan[FundingLoanModel.HIDDEN] - renew = raw_loan[FundingLoanModel.RENEW] - no_close = raw_loan[FundingLoanModel.NO_CLOSE] - return FundingLoan(id, symbol, side, mts_create, mts_update, amount, flags, status, rate, - period, mts_opening, mts_last_payout, notify, hidden, renew, no_close) - - def __str__(self): - return "FundingLoan '{}' ".format( - self.symbol, self.id, self.rate, self.amount, self.period, self.status) diff --git a/bfxapi/models/FundingOffer.py b/bfxapi/models/FundingOffer.py deleted file mode 100644 index 39f9aa7..0000000 --- a/bfxapi/models/FundingOffer.py +++ /dev/null @@ -1,74 +0,0 @@ - -class FundingOfferModel: - ID = 0 - SYMBOL = 1 - MTS_CREATE = 2 - MTS_UPDATED = 3 - AMOUNT = 4 - AMOUNT_ORIG = 5 - TYPE = 6 - FLAGS = 9 - STATUS = 10 - RATE = 14 - PERIOD = 15 - NOTFIY = 16 - HIDDEN = 17 - RENEW = 19 - -class FundingOffer: - """ - ID integer Offer ID - SYMBOL string The currency of the offer (fUSD, etc) - MTS_CREATED int Millisecond Time Stamp when the offer was created - MSG_UPDATED int Millisecond Time Stamp when the offer was created - AMOUNT float Amount the offer is for - AMOUNT_ORIG float Amount the offer was entered with originally - TYPE string "lend" or "loan" - FLAGS object future params object (stay tuned) - STATUS string Offer Status: ACTIVE, EXECUTED, PARTIALLY FILLED, CANCELED - RATE float Rate of the offer - PERIOD int Period of the offer - NOTIFY int 0 if false, 1 if true - HIDDEN int 0 if false, 1 if true - RENEW int 0 if false, 1 if true - """ - - def __init__(self, id, symbol, mts_create, mts_updated, amount, amount_orig, f_type, - flags, status, rate, period, notify, hidden, renew): - self.id = id - self.symbol = symbol - self.mts_create = mts_create - self.mts_updated = mts_updated - self.amount = amount - self.amount_orig = amount_orig - self.f_type = f_type - self.flags = flags - self.status = status - self.rate = rate - self.period = period - self.notify = notify - self.hidden = hidden - self.renew = renew - - @staticmethod - def from_raw_offer(raw_offer): - id = raw_offer[FundingOfferModel.ID] - symbol = raw_offer[FundingOfferModel.SYMBOL] - mts_create = raw_offer[FundingOfferModel.MTS_CREATE] - mts_updated = raw_offer[FundingOfferModel.MTS_UPDATED] - amount = raw_offer[FundingOfferModel.AMOUNT] - amount_orig = raw_offer[FundingOfferModel.AMOUNT_ORIG] - f_type = raw_offer[FundingOfferModel.TYPE] - flags = raw_offer[FundingOfferModel.FLAGS] - status = raw_offer[FundingOfferModel.STATUS] - rate = raw_offer[FundingOfferModel.RATE] - period = raw_offer[FundingOfferModel.PERIOD] - notify = raw_offer[FundingOfferModel.NOTFIY] - hidden = raw_offer[FundingOfferModel.HIDDEN] - renew = raw_offer[FundingOfferModel.RENEW] - return FundingOffer(id, symbol, mts_create, mts_updated, amount, - amount_orig, f_type,flags, status, rate, period, notify, hidden, renew) - - def __str__(self): - return "FundingOffer '{}' ".format( - self.symbol, self.id, self.rate, self.period, self.status) diff --git a/bfxapi/models/Order.py b/bfxapi/models/Order.py index f046c40..00cb7e2 100644 --- a/bfxapi/models/Order.py +++ b/bfxapi/models/Order.py @@ -1,161 +1,221 @@ +""" +Module used to describe all of the different data types +""" + import time import datetime + class OrderType: - MARKET = 'MARKET' - LIMIT = 'LIMIT' - STOP = 'STOP' - STOP_LIMIT = 'STOP LIMIT' - TRAILING_STOP = 'TRAILING STOP' - FILL_OR_KILL = 'FOK' - EXCHANGE_MARKET = 'EXCHANGE MARKET' - EXCHANGE_LIMIT = 'EXCHANGE LIMIT' - EXCHANGE_STOP = 'EXCHANGE STOP' - EXCHANGE_STOP_LIMIT = 'EXCHANGE STOP LIMIT' - EXCHANGE_TRAILING_STOP = 'EXCHANGE TRAILING STOP' - EXCHANGE_FILL_OR_KILL = 'EXCHANGE FOK' + """ + Enum used to describe all of the different order types available for use + """ + MARKET = 'MARKET' + LIMIT = 'LIMIT' + STOP = 'STOP' + STOP_LIMIT = 'STOP LIMIT' + TRAILING_STOP = 'TRAILING STOP' + FILL_OR_KILL = 'FOK' + EXCHANGE_MARKET = 'EXCHANGE MARKET' + EXCHANGE_LIMIT = 'EXCHANGE LIMIT' + EXCHANGE_STOP = 'EXCHANGE STOP' + EXCHANGE_STOP_LIMIT = 'EXCHANGE STOP LIMIT' + EXCHANGE_TRAILING_STOP = 'EXCHANGE TRAILING STOP' + EXCHANGE_FILL_OR_KILL = 'EXCHANGE FOK' + LIMIT_ORDERS = [OrderType.LIMIT, OrderType.STOP_LIMIT, OrderType.EXCHANGE_LIMIT, - OrderType.EXCHANGE_STOP_LIMIT, OrderType.FILL_OR_KILL, OrderType.EXCHANGE_FILL_OR_KILL] + OrderType.EXCHANGE_STOP_LIMIT, OrderType.FILL_OR_KILL, + OrderType.EXCHANGE_FILL_OR_KILL] + class OrderSide: - BUY = 'buy' - SELL = 'sell' + """ + Enum used to describe the different directions of an order + """ + BUY = 'buy' + SELL = 'sell' + class OrderClosedModel: - ID = 0 - GID = 1 - CID = 2 - SYMBOL = 3 - MTS_CREATE = 4 - MTS_UPDATE = 5 - AMOUNT = 6 - AMOUNT_ORIG = 7 - TYPE = 8 - TYPE_PREV = 9 - FLAGS = 12 - STATUS = 13 - PRICE = 16 - PRIVE_AVG = 17 - PRICE_TRAILING = 18 - PRICE_AUX_LIMIT = 19 - NOTIFY = 23 - PLACE_ID = 25 + """ + Enum used ad an index match to locate the different values in a + raw order array + """ + ID = 0 + GID = 1 + CID = 2 + SYMBOL = 3 + MTS_CREATE = 4 + MTS_UPDATE = 5 + AMOUNT = 6 + AMOUNT_ORIG = 7 + TYPE = 8 + TYPE_PREV = 9 + FLAGS = 12 + STATUS = 13 + PRICE = 16 + PRIVE_AVG = 17 + PRICE_TRAILING = 18 + PRICE_AUX_LIMIT = 19 + NOTIFY = 23 + PLACE_ID = 25 + class OrderFlags: - HIDDEN = 64 - CLOSE = 12 - REDUCE_ONLY = 1024 - POST_ONLY = 4096 - OCO = 16384 + """ + Enum used to explain the different values that can be passed in + as flags + """ + HIDDEN = 64 + CLOSE = 12 + REDUCE_ONLY = 1024 + POST_ONLY = 4096 + OCO = 16384 + def now_in_mills(): - return int(round(time.time() * 1000)) + """ + Gets the current time in milliseconds + """ + return int(round(time.time() * 1000)) + class Order: - """ - ID int64 Order ID - GID int Group ID - CID int Client Order ID - SYMBOL string Pair (tBTCUSD, …) - MTS_CREATE int Millisecond timestamp of creation - MTS_UPDATE int Millisecond timestamp of update - AMOUNT float Positive means buy, negative means sell. - AMOUNT_ORIG float Original amount - TYPE string The type of the order: LIMIT, MARKET, STOP, TRAILING STOP, EXCHANGE MARKET, EXCHANGE LIMIT, EXCHANGE STOP, EXCHANGE TRAILING STOP, FOK, EXCHANGE FOK. - TYPE_PREV string Previous order type - FLAGS int Upcoming Params Object (stay tuned) - ORDER_STATUS string Order Status: ACTIVE, EXECUTED, PARTIALLY FILLED, CANCELED - PRICE float Price - PRICE_AVG float Average price - PRICE_TRAILING float The trailing price - PRICE_AUX_LIMIT float Auxiliary Limit price (for STOP LIMIT) - HIDDEN int 1 if Hidden, 0 if not hidden - PLACED_ID int If another order caused this order to be placed (OCO) this will be that other order's ID - """ + """ + ID int64 Order ID + GID int Group ID + CID int Client Order ID + SYMBOL string Pair (tBTCUSD, ...) + MTS_CREATE int Millisecond timestamp of creation + MTS_UPDATE int Millisecond timestamp of update + AMOUNT float Positive means buy, negative means sell. + AMOUNT_ORIG float Original amount + TYPE string The type of the order: LIMIT, MARKET, STOP, TRAILING STOP, + EXCHANGE MARKET, EXCHANGE LIMIT, EXCHANGE STOP, EXCHANGE TRAILING STOP, FOK, EXCHANGE FOK. + TYPE_PREV string Previous order type + FLAGS int Upcoming Params Object (stay tuned) + ORDER_STATUS string Order Status: ACTIVE, EXECUTED, PARTIALLY FILLED, CANCELED + PRICE float Price + PRICE_AVG float Average price + PRICE_TRAILING float The trailing price + PRICE_AUX_LIMIT float Auxiliary Limit price (for STOP LIMIT) + HIDDEN int 1 if Hidden, 0 if not hidden + PLACED_ID int If another order caused this order to be placed (OCO) this will be that other + order's ID + """ - Type = OrderType() - Side = OrderSide() - Flags = OrderFlags() + Type = OrderType() + Side = OrderSide() + Flags = OrderFlags() - def __init__(self, id, gId, cId, symbol, mtsCreate, mtsUpdate, amount, amountOrig, oType, - typePrev, flags, status, price, priceAvg, priceTrailing, priceAuxLimit, notfiy, placeId): - self.id = id - self.gId = gId - self.cId = cId - self.symbol = symbol - self.mtsCreate = mtsCreate - self.mtsUpdate = mtsUpdate - # self.amount = amount - self.amount = amount - self.amountOrig = amountOrig - self.type = oType - self.typePrev = typePrev - self.flags = flags - self.status = status - self.price = price - self.priceAvg = priceAvg - self.priceTrailing = priceTrailing - self.priceAuxLimit = priceAuxLimit - self.notfiy = notfiy - self.placeId = placeId - self.tag = "" - self.fee = 0 + def __init__(self, oid, gid, cid, symbol, mts_create, mts_update, amount, + amount_orig, o_type, typePrev, flags, status, price, price_avg, + price_trailing, price_aux_limit, notfiy, place_id): + # pylint: disable=invalid-name + self.id = oid + self.gid = gid + self.cid = cid + self.symbol = symbol + self.mts_create = mts_create + self.mts_update = mts_update + # self.amount = amount + self.amount = amount + self.amount_orig = amount_orig + self.type = o_type + self.type_prev = typePrev + self.flags = flags + self.status = status + self.price = price + self.price_avg = price_avg + self.price_trailing = price_trailing + self.price_aux_limit = price_aux_limit + self.notfiy = notfiy + self.place_id = place_id + self.tag = "" + self.fee = 0 - self.is_pending_bool = True - self.is_confirmed_bool = False - self.is_open_bool = False + self.is_pending_bool = True + self.is_confirmed_bool = False + self.is_open_bool = False - self.date = datetime.datetime.fromtimestamp(mtsCreate/1000.0) - ## if cancelled then priceAvg wont exist - if priceAvg: - ## check if order is taker or maker - if self.type in LIMIT_ORDERS: - self.fee = (priceAvg * abs(amount)) * 0.001 - else: - self.fee = (priceAvg * abs(amount)) * 0.002 + self.date = datetime.datetime.fromtimestamp(mts_create/1000.0) + # if cancelled then priceAvg wont exist + if price_avg: + # check if order is taker or maker + if self.type in LIMIT_ORDERS: + self.fee = (price_avg * abs(amount)) * 0.001 + else: + self.fee = (price_avg * abs(amount)) * 0.002 - @staticmethod - def from_raw_order(raw_order): - oid = raw_order[OrderClosedModel.ID] - gId = raw_order[OrderClosedModel.GID] - cId = raw_order[OrderClosedModel.CID] - symbol = raw_order[OrderClosedModel.SYMBOL] - mtsCreate = raw_order[OrderClosedModel.MTS_CREATE] - mtsUpdate = raw_order[OrderClosedModel.MTS_UPDATE] - amount = raw_order[OrderClosedModel.AMOUNT] - amountOrig = raw_order[OrderClosedModel.AMOUNT_ORIG] - oType = raw_order[OrderClosedModel.TYPE] - typePrev = raw_order[OrderClosedModel.TYPE_PREV] - flags = raw_order[OrderClosedModel.FLAGS] - status = raw_order[OrderClosedModel.STATUS] - price = raw_order[OrderClosedModel.PRICE] - priceAvg = raw_order[OrderClosedModel.PRIVE_AVG] - priceTrailing = raw_order[OrderClosedModel.PRICE_TRAILING] - priceAuxLimit = raw_order[OrderClosedModel.PRICE_AUX_LIMIT] - notfiy = raw_order[OrderClosedModel.NOTIFY] - placeId = raw_order[OrderClosedModel.PLACE_ID] + @staticmethod + def from_raw_order(raw_order): + """ + Parse a raw order object into an Order oject - return Order(oid, gId, cId, symbol, mtsCreate, mtsUpdate, amount, amountOrig, oType, - typePrev, flags, status, price, priceAvg, priceTrailing, priceAuxLimit, notfiy, placeId) + @return Order + """ + oid = raw_order[OrderClosedModel.ID] + gid = raw_order[OrderClosedModel.GID] + cid = raw_order[OrderClosedModel.CID] + symbol = raw_order[OrderClosedModel.SYMBOL] + mts_create = raw_order[OrderClosedModel.MTS_CREATE] + mts_update = raw_order[OrderClosedModel.MTS_UPDATE] + amount = raw_order[OrderClosedModel.AMOUNT] + amount_orig = raw_order[OrderClosedModel.AMOUNT_ORIG] + o_type = raw_order[OrderClosedModel.TYPE] + type_prev = raw_order[OrderClosedModel.TYPE_PREV] + flags = raw_order[OrderClosedModel.FLAGS] + status = raw_order[OrderClosedModel.STATUS] + price = raw_order[OrderClosedModel.PRICE] + price_avg = raw_order[OrderClosedModel.PRIVE_AVG] + price_trailing = raw_order[OrderClosedModel.PRICE_TRAILING] + price_aux_limit = raw_order[OrderClosedModel.PRICE_AUX_LIMIT] + notfiy = raw_order[OrderClosedModel.NOTIFY] + place_id = raw_order[OrderClosedModel.PLACE_ID] - def set_confirmed(self): - self.is_pending_bool = False - self.is_confirmed_bool = True + return Order(oid, gid, cid, symbol, mts_create, mts_update, amount, + amount_orig, o_type, type_prev, flags, status, price, price_avg, + price_trailing, price_aux_limit, notfiy, place_id) - def set_open_state(self, isOpen): - self.is_open_bool = isOpen + def set_confirmed(self): + """ + Set the state of the order to be confirmed + """ + self.is_pending_bool = False + self.is_confirmed_bool = True - def isOpen(self): - return self.is_open_bool + def set_open_state(self, is_open): + """ + Set the is_open state of the order + """ + self.is_open_bool = is_open - def isPending(self): - return self.is_pending_bool + def is_open(self): + """ + Check if the order is still open - def isConfirmed(self): - return self.is_confirmed_bool - - def __str__(self): - ''' Allow us to print the Order object in a pretty format ''' - return "Order <'{}' mtsCreate={} status='{}' id={}>".format(self.symbol, self.mtsCreate, - self.status, self.id) + @return bool: Ture if order open else False + """ + return self.is_open_bool + + def is_pending(self): + """ + Check if the state of the order is still pending + + @return bool: True if is pending else False + """ + return self.is_pending_bool + + def is_confirmed(self): + """ + Check if the order has been confirmed by the bitfinex api + + @return bool: True if has been confirmed else False + """ + return self.is_confirmed_bool + + def __str__(self): + ''' Allow us to print the Order object in a pretty format ''' + text = "Order <'{}' mts_create={} status='{}' id={}>" + return text.format(self.symbol, self.mts_create, self.status, self.id) diff --git a/bfxapi/models/OrderBook.py b/bfxapi/models/OrderBook.py deleted file mode 100644 index 9ca0381..0000000 --- a/bfxapi/models/OrderBook.py +++ /dev/null @@ -1,90 +0,0 @@ -import zlib - -def preparePrice(price): - # convert to 4 significant figures - prepPrice = '{0:.4f}'.format(price) - # remove decimal place if zero float - return '{0:g}'.format(float(prepPrice)) - -class OrderBook: - - def __init__(self): - self.asks = [] - self.bids = [] - - def get_bids(self): - return self.bids - - def get_asks(self): - return self.asks - - def updateFromSnapshot(self, data): - # [[4642.3, 1, 4.192], [4641.5, 1, 1]] - for order in data: - if len(order) is 4: - if order[3] < 0: - self.bids += [order] - else: - self.asks += [order] - else: - if order[2] < 0: - self.asks += [order] - else: - self.bids += [order] - - def updateWith(self, order): - if len(order) is 4: - amount = order[3] - count = order[2] - side = self.bids if amount < 0 else self.asks - else: - amount = order[2] - side = self.asks if amount < 0 else self.bids - count = order[1] - price = order[0] - - # if first item in ordebook - if len(side) is 0: - side += [order] - return - - # match price level - for index, sOrder in enumerate(side): - sPrice = sOrder[0] - if sPrice == price: - if count is 0: - del side[index] - return - else: - # remove but add as new below - del side[index] - - # if ob is initialised w/o all price levels - if count is 0: - return - - # add to book and sort lowest to highest - side += [order] - side.sort(key=lambda x: x[0], reverse=not amount < 0) - return - - def checksum(self): - data = [] - # take set of top 25 bids/asks - for index in range(0, 25): - if index < len(self.bids): - bid = self.bids[index] - price = bid[0] - amount = bid[3] if len(bid) is 4 else bid[2] - data += [preparePrice(price)] - data += [str(amount)] - if index < len(self.asks): - ask = self.asks[index] - price = ask[0] - amount = ask[3] if len(ask) is 4 else ask[2] - data += [preparePrice(price)] - data += [str(amount)] - checksumStr = ':'.join(data) - # calculate checksum and force signed integer - checksum = zlib.crc32(checksumStr.encode('utf8')) & 0xffffffff - return checksum diff --git a/bfxapi/models/Position.py b/bfxapi/models/Position.py index f012993..43fc1ef 100644 --- a/bfxapi/models/Position.py +++ b/bfxapi/models/Position.py @@ -1,36 +1,47 @@ +""" +Module used to describe all of the different data types +""" + class Position: - """ - SYMBOL string Pair (tBTCUSD, …). - STATUS string Status (ACTIVE, CLOSED). - ±AMOUNT float Size of the position. Positive values means a long position, negative values means a short position. - BASE_PRICE float The price at which you entered your position. - MARGIN_FUNDING float The amount of funding being used for this position. - MARGIN_FUNDING_TYPE int 0 for daily, 1 for term. - PL float Profit & Loss - PL_PERC float Profit & Loss Percentage - PRICE_LIQ float Liquidation price - LEVERAGE float Beta value - """ + """ + SYMBOL string Pair (tBTCUSD, ...). + STATUS string Status (ACTIVE, CLOSED). + AMOUNT float Size of the position. Positive values means a long position, + negative values means a short position. + BASE_PRICE float The price at which you entered your position. + MARGIN_FUNDING float The amount of funding being used for this position. + MARGIN_FUNDING_TYPE int 0 for daily, 1 for term. + PL float Profit & Loss + PL_PERC float Profit & Loss Percentage + PRICE_LIQ float Liquidation price + LEVERAGE float Beta value + """ - def __init__(self, symbol, status, amount, bPrice, mFunding, mFundingType, - profit_loss, profit_loss_perc, lPrice, lev): - self.symbol = symbol - self.status = status - self.amount = amount - self.base_price = bPrice - self.margin_funding = mFunding - self.margin_funding_type = mFundingType - self.profit_loss = profit_loss - self.profit_loss_percentage = profit_loss_perc - self.liquidation_price = lPrice - self.leverage = lev + def __init__(self, symbol, status, amount, b_price, m_funding, m_funding_type, + profit_loss, profit_loss_perc, l_price, lev): + self.symbol = symbol + self.status = status + self.amount = amount + self.base_price = b_price + self.margin_funding = m_funding + self.margin_funding_type = m_funding_type + self.profit_loss = profit_loss + self.profit_loss_percentage = profit_loss_perc + self.liquidation_price = l_price + self.leverage = lev - @staticmethod - def from_raw_rest_position(raw_position): - return Position(*raw_position) - - def __str__(self): - ''' Allow us to print the Trade object in a pretty format ''' - return "Position '{}' {} x {} ".format( - self.symbol, self.base_price, self.amount, self.status, self.profit_loss) + @staticmethod + def from_raw_rest_position(raw_position): + """ + Generate a Position object from a raw position array + + @return Position + """ + return Position(*raw_position) + + def __str__(self): + ''' Allow us to print the Trade object in a pretty format ''' + text = "Position '{}' {} x {} " + return text.format(self.symbol, self.base_price, self.amount, + self.status, self.profit_loss) diff --git a/bfxapi/models/Subscription.py b/bfxapi/models/Subscription.py index 572575a..331a046 100644 --- a/bfxapi/models/Subscription.py +++ b/bfxapi/models/Subscription.py @@ -1,45 +1,74 @@ +""" +Module used to describe all of the different data types +""" + import time import json + class Subscription: + """ + Object used to represent an individual subscription to the websocket. + This class also exposes certain functions which helps to manage the subscription + such as unsibscribe and subscribe. + """ - def __init__(self, ws, channel_name, symbol, timeframe=None, **kwargs): - self.ws = ws - self.channel_name = channel_name - self.symbol = symbol - self.timeframe = timeframe - self.is_subscribed_bool = False - self.key = None - if timeframe: - self.key = 'trade:{}:{}'.format(self.timeframe, self.symbol) - self.sub_id = int(round(time.time() * 1000)) - self.send_payload = self._generate_payload(**kwargs) + def __init__(self, ws, channel_name, symbol, timeframe=None, **kwargs): + self._ws = ws + self.channel_name = channel_name + self.symbol = symbol + self.timeframe = timeframe + self.is_subscribed_bool = False + self.key = None + self.chan_id = None + if timeframe: + self.key = 'trade:{}:{}'.format(self.timeframe, self.symbol) + self.sub_id = int(round(time.time() * 1000)) + self.send_payload = self._generate_payload(**kwargs) - async def subscribe(self): - await self.ws.send(json.dumps(self.get_send_payload())) + def confirm_subscription(self, chan_id): + """ + Update the subscription to confirmed state + """ + self.is_subscribed_bool = True + self.chan_id = chan_id - async def unsubscribe(self): - if not self.is_subscribed(): - raise Exception("Subscription is not subscribed to websocket") - payload = { 'event': 'unsubscribe', 'chanId': self.chanId } - await self.ws.send(json.dumps(payload)) + async def unsubscribe(self): + """ + Send an unsubscription request to the bitfinex socket + """ + if not self.is_subscribed(): + raise Exception("Subscription is not subscribed to websocket") + payload = {'event': 'unsubscribe', 'chan_id': self.chan_id} + await self._ws.send(json.dumps(payload)) - def confirm_subscription(self, chanId): - self.is_subscribed_bool = True - self.chanId = chanId + async def subscribe(self): + """ + Send a subscription request to the bitfinex socket + """ + await self._ws.send(json.dumps(self._get_send_payload())) - def confirm_unsubscribe(self): - self.is_subscribed_bool = False + def confirm_unsubscribe(self): + """ + Update the subscription to unsubscribed state + """ + self.is_subscribed_bool = False - def is_subscribed(self): - return self.is_subscribed_bool + def is_subscribed(self): + """ + Check if the subscription is currently subscribed - def _generate_payload(self, **kwargs): - payload = { 'event': 'subscribe', 'channel': self.channel_name, 'symbol': self.symbol } - if self.timeframe: - payload['key'] = self.key - payload.update(**kwargs) - return payload + @return bool: True if subscribed else False + """ + return self.is_subscribed_bool - def get_send_payload(self): - return self.send_payload + def _generate_payload(self, **kwargs): + payload = {'event': 'subscribe', + 'channel': self.channel_name, 'symbol': self.symbol} + if self.timeframe: + payload['key'] = self.key + payload.update(**kwargs) + return payload + + def _get_send_payload(self): + return self.send_payload diff --git a/bfxapi/models/Trade.py b/bfxapi/models/Trade.py index 3ce8d3d..a4325f3 100644 --- a/bfxapi/models/Trade.py +++ b/bfxapi/models/Trade.py @@ -1,44 +1,54 @@ +""" +Module used to describe all of the different data types +""" + import datetime + class Trade: - """ - ID integer Trade database id - PAIR string Pair (BTCUSD, …) - MTS_CREATE integer Execution timestamp - ORDER_ID integer Order id - EXEC_AMOUNT float Positive means buy, negative means sell - EXEC_PRICE float Execution price - ORDER_TYPE string Order type - ORDER_PRICE float Order price - MAKER int 1 if true, 0 if false - FEE float Fee - FEE_CURRENCY string Fee currency - """ + """ + ID integer Trade database id + PAIR string Pair (BTCUSD, ...) + MTS_CREATE integer Execution timestamp + ORDER_ID integer Order id + EXEC_AMOUNT float Positive means buy, negative means sell + EXEC_PRICE float Execution price + ORDER_TYPE string Order type + ORDER_PRICE float Order price + MAKER int 1 if true, 0 if false + FEE float Fee + FEE_CURRENCY string Fee currency + """ - SHORT = 'SHORT' - LONG = 'LONG' + SHORT = 'SHORT' + LONG = 'LONG' - def __init__(self, id, pair, mts_create, order_id, amount, price, order_type, - order_price, maker, fee, fee_currency): - self.id = id - self.pair = pair - self.mts_create = mts_create - self.date = datetime.datetime.fromtimestamp(mts_create/1000.0) - self.order_id = order_id - self.amount = amount - self.direction = Trade.SHORT if amount < 0 else Trade.LONG - self.price = price - self.order_type = order_type - self.order_price = order_price - self.maker = maker - self.fee = fee - self.fee_currency = fee_currency + def __init__(self, tid, pair, mts_create, order_id, amount, price, order_type, + order_price, maker, fee, fee_currency): + # pylint: disable=invalid-name + self.id = tid + self.pair = pair + self.mts_create = mts_create + self.date = datetime.datetime.fromtimestamp(mts_create/1000.0) + self.order_id = order_id + self.amount = amount + self.direction = Trade.SHORT if amount < 0 else Trade.LONG + self.price = price + self.order_type = order_type + self.order_price = order_price + self.maker = maker + self.fee = fee + self.fee_currency = fee_currency - @staticmethod - def from_raw_rest_trade(raw_trade): - # [24224048, 'tBTCUSD', 1542800024000, 1151353484, 0.09399997, 19963, None, None, -1, -0.000188, 'BTC'] - return Trade(*raw_trade) - - def __str__(self): - return "Trade '{}' x {} @ {} ".format( - self.pair, self.amount, self.price, self.direction, self.fee) + @staticmethod + def from_raw_rest_trade(raw_trade): + """ + Generate a Trade object from a raw trade array + """ + # [24224048, 'tBTCUSD', 1542800024000, 1151353484, 0.09399997, 19963, None, None, + # -1, -0.000188, 'BTC'] + return Trade(*raw_trade) + + def __str__(self): + return "Trade '{}' x {} @ {} ".format( + self.pair, self.amount, self.price, self.direction, self.fee) diff --git a/bfxapi/models/Wallet.py b/bfxapi/models/Wallet.py index abb5c07..028d5ac 100644 --- a/bfxapi/models/Wallet.py +++ b/bfxapi/models/Wallet.py @@ -1,19 +1,33 @@ +""" +Module used to describe all of the different data types +""" + class Wallet: + """ + Stores data relevant to a users wallet such as balance and + currency + """ - def __init__(self, wType, currency, balance, unsettled_interest): - self.type = wType - self.currency = currency - self.balance = balance - self.unsettled_interest = unsettled_interest - self.key = "{}_{}".format(wType, currency) + def __init__(self, wType, currency, balance, unsettled_interest): + self.type = wType + self.currency = currency + self.balance = balance + self.unsettled_interest = unsettled_interest + self.key = "{}_{}".format(wType, currency) - def set_balance(self, data): - self.balance = data + def set_balance(self, data): + """ + Set the balance of the wallet + """ + self.balance = data - def set_unsettled_interest(self, data): - self.unsettled_interest = data + def set_unsettled_interest(self, data): + """ + Set the unsettled interest of the wallet + """ + self.unsettled_interest = data - def __str__(self): - return "Wallet <'{}_{}' balance='{}' unsettled='{}'>".format( - self.type, self.currency, self.balance, self.unsettled_interest) + def __str__(self): + return "Wallet <'{}_{}' balance='{}' unsettled='{}'>".format( + self.type, self.currency, self.balance, self.unsettled_interest) diff --git a/bfxapi/models/__init__.py b/bfxapi/models/__init__.py index 40cc181..e1ec275 100644 --- a/bfxapi/models/__init__.py +++ b/bfxapi/models/__init__.py @@ -1,11 +1,16 @@ -name = 'models' +""" +This module contains a group of different models which +are used to define data types +""" -from .Order import * -from .Trade import * -from .OrderBook import * -from .Subscription import * -from .Wallet import * -from .Position import * -from .FundingLoan import * -from .FundingOffer import * -from .FundingCredit import * +from .order import Order +from .trade import Trade +from .order_book import OrderBook +from .subscription import Subscription +from .wallet import Wallet +from .position import Position +from .funding_loan import FundingLoan +from .funding_offer import FundingOffer +from .funding_credit import FundingCredit + +NAME = 'models' diff --git a/bfxapi/models/funding_credit.py b/bfxapi/models/funding_credit.py new file mode 100644 index 0000000..0f9c9a3 --- /dev/null +++ b/bfxapi/models/funding_credit.py @@ -0,0 +1,104 @@ +""" +Module used to describe all of the different data types +""" + + +class FundingCreditModel: + """ + Enum used to index the location of each value in a raw array + """ + ID = 0 + SYMBOL = 1 + SIDE = 2 + MTS_CREATE = 3 + MTS_UPDATE = 4 + AMOUNT = 5 + FLAGS = 6 + STATUS = 7 + RATE = 11 + PERIOD = 12 + MTS_OPENING = 13 + MTS_LAST_PAYOUT = 14 + NOTIFY = 15 + HIDDEN = 16 + RENEW = 18 + NO_CLOSE = 20 + POSITION_PAIR = 21 + + +class FundingCredit: + """ + ID integer Offer ID + SYMBOL string The currency of the offer (fUSD, etc) + SIDE string "Lend" or "Loan" + MTS_CREATE int Millisecond Time Stamp when the offer was created + MSG_UPDATE int Millisecond Time Stamp when the offer was updated + AMOUNT float Amount the offer is for + FLAGS object future params object (stay tuned) + STATUS string Offer Status: ACTIVE, EXECUTED, PARTIALLY FILLED, CANCELED + RATE float Rate of the offer + PERIOD int Period of the offer + MTS_OPENING int Millisecond Time Stamp when funding opened + MTS_LAST_PAYOUT int Millisecond Time Stamp when last payout received + NOTIFY int 0 if false, 1 if true + HIDDEN int 0 if false, 1 if true + RENEW int 0 if false, 1 if true + NO_CLOSE int 0 if false, 1 if true Whether the funding will be closed when the + position is closed + POSITION_PAIR string Pair of the position that the funding was used for + """ + + def __init__(self, fid, symbol, side, mts_create, mts_update, amount, flags, status, rate, + period, mts_opening, mts_last_payout, notify, hidden, renew, no_close, + position_pair): + # pylint: disable=invalid-name + self.id = fid + self.symbol = symbol + self.side = side + self.mts_create = mts_create + self.mts_update = mts_update + self.amount = amount + self.flags = flags + self.status = status + self.rate = rate + self.period = period + self.mts_opening = mts_opening + self.mts_last_payout = mts_last_payout + self.notify = notify + self.hidden = hidden + self.renew = renew + self.no_close = no_close + self.position_pair = position_pair + + @staticmethod + def from_raw_credit(raw_credit): + """ + Parse a raw credit object into a FundingCredit object + + @return FundingCredit + """ + fid = raw_credit[FundingCreditModel.ID] + symbol = raw_credit[FundingCreditModel.SYMBOL] + side = raw_credit[FundingCreditModel.SIDE] + mts_create = raw_credit[FundingCreditModel.MTS_CREATE] + mts_update = raw_credit[FundingCreditModel.MTS_UPDATE] + amount = raw_credit[FundingCreditModel.AMOUNT] + flags = raw_credit[FundingCreditModel.FLAGS] + status = raw_credit[FundingCreditModel.STATUS] + rate = raw_credit[FundingCreditModel.RATE] + period = raw_credit[FundingCreditModel.PERIOD] + mts_opening = raw_credit[FundingCreditModel.MTS_OPENING] + mts_last_payout = raw_credit[FundingCreditModel.MTS_LAST_PAYOUT] + notify = raw_credit[FundingCreditModel.NOTIFY] + hidden = raw_credit[FundingCreditModel.HIDDEN] + renew = raw_credit[FundingCreditModel.RENEW] + no_close = raw_credit[FundingCreditModel.NO_CLOSE] + position_pair = raw_credit[FundingCreditModel.POSITION_PAIR] + return FundingCredit(fid, symbol, side, mts_create, mts_update, amount, + flags, status, rate, period, mts_opening, mts_last_payout, + notify, hidden, renew, no_close, position_pair) + + def __str__(self): + string = "FundingCredit '{}' " + return string.format(self.symbol, self.id, self.rate, self.amount, + self.period, self.status) diff --git a/bfxapi/models/funding_loan.py b/bfxapi/models/funding_loan.py new file mode 100644 index 0000000..ebb7537 --- /dev/null +++ b/bfxapi/models/funding_loan.py @@ -0,0 +1,96 @@ +""" +Module used to describe all of the different data types +""" + + +class FundingLoanModel: + """ + Enum used to index the location of each value in a raw array + """ + ID = 0 + SYMBOL = 1 + SIDE = 2 + MTS_CREATE = 3 + MTS_UPDATE = 4 + AMOUNT = 5 + FLAGS = 6 + STATUS = 7 + RATE = 11 + PERIOD = 12 + MTS_OPENING = 13 + MTS_LAST_PAYOUT = 14 + NOTIFY = 15 + HIDDEN = 16 + RENEW = 18 + NO_CLOSE = 20 + + +class FundingLoan: + """ + ID integer Offer ID + SYMBOL string The currency of the offer (fUSD, etc) + SIDE string "Lend" or "Loan" + MTS_CREATE int Millisecond Time Stamp when the offer was created + MTS_UPDATE int Millisecond Time Stamp when the offer was created + AMOUNT float Amount the offer is for + FLAGS object future params object (stay tuned) + STATUS string Offer Status: ACTIVE, EXECUTED, PARTIALLY FILLED, CANCELED + RATE float Rate of the offer + PERIOD int Period of the offer + MTS_OPENING int Millisecond Time Stamp for when the loan was opened + MTS_LAST_PAYOUT int Millisecond Time Stamp for when the last payout was made + NOTIFY int 0 if false, 1 if true + HIDDEN int 0 if false, 1 if true + RENEW int 0 if false, 1 if true + NO_CLOSE int If funding will be returned when position is closed. 0 if false, 1 if true + """ + + def __init__(self, fid, symbol, side, mts_create, mts_update, amount, flags, status, rate, + period, mts_opening, mts_last_payout, notify, hidden, renew, no_close): + # pylint: disable=invalid-name + self.id = fid + self.symbol = symbol + self.side = side + self.mts_create = mts_create + self.mts_update = mts_update + self.amount = amount + self.flags = flags + self.status = status + self.rate = rate + self.period = period + self.mts_opening = mts_opening + self.mts_last_payout = mts_last_payout + self.notify = notify + self.hidden = hidden + self.renew = renew + self.no_close = no_close + + @staticmethod + def from_raw_loan(raw_loan): + """ + Parse a raw funding load into a FundingLoan object + + @return FundingLoan + """ + fid = raw_loan[FundingLoanModel.ID] + symbol = raw_loan[FundingLoanModel.SYMBOL] + side = raw_loan[FundingLoanModel.SIDE] + mts_create = raw_loan[FundingLoanModel.MTS_CREATE] + mts_update = raw_loan[FundingLoanModel.MTS_UPDATE] + amount = raw_loan[FundingLoanModel.AMOUNT] + flags = raw_loan[FundingLoanModel.FLAGS] + status = raw_loan[FundingLoanModel.STATUS] + rate = raw_loan[FundingLoanModel.RATE] + period = raw_loan[FundingLoanModel.PERIOD] + mts_opening = raw_loan[FundingLoanModel.MTS_OPENING] + mts_last_payout = raw_loan[FundingLoanModel.MTS_LAST_PAYOUT] + notify = raw_loan[FundingLoanModel.NOTIFY] + hidden = raw_loan[FundingLoanModel.HIDDEN] + renew = raw_loan[FundingLoanModel.RENEW] + no_close = raw_loan[FundingLoanModel.NO_CLOSE] + return FundingLoan(fid, symbol, side, mts_create, mts_update, amount, flags, status, rate, + period, mts_opening, mts_last_payout, notify, hidden, renew, no_close) + + def __str__(self): + return "FundingLoan '{}' ".format( + self.symbol, self.id, self.rate, self.amount, self.period, self.status) diff --git a/bfxapi/models/funding_offer.py b/bfxapi/models/funding_offer.py new file mode 100644 index 0000000..16743a7 --- /dev/null +++ b/bfxapi/models/funding_offer.py @@ -0,0 +1,88 @@ +""" +Module used to describe all of the different data types +""" + + +class FundingOfferModel: + """ + Enum used to index the location of each value in a raw array + """ + ID = 0 + SYMBOL = 1 + MTS_CREATE = 2 + MTS_UPDATED = 3 + AMOUNT = 4 + AMOUNT_ORIG = 5 + TYPE = 6 + FLAGS = 9 + STATUS = 10 + RATE = 14 + PERIOD = 15 + NOTFIY = 16 + HIDDEN = 17 + RENEW = 19 + + +class FundingOffer: + """ + ID integer Offer ID + SYMBOL string The currency of the offer (fUSD, etc) + MTS_CREATED int Millisecond Time Stamp when the offer was created + MSG_UPDATED int Millisecond Time Stamp when the offer was created + AMOUNT float Amount the offer is for + AMOUNT_ORIG float Amount the offer was entered with originally + TYPE string "lend" or "loan" + FLAGS object future params object (stay tuned) + STATUS string Offer Status: ACTIVE, EXECUTED, PARTIALLY FILLED, CANCELED + RATE float Rate of the offer + PERIOD int Period of the offer + NOTIFY int 0 if false, 1 if true + HIDDEN int 0 if false, 1 if true + RENEW int 0 if false, 1 if true + """ + + def __init__(self, fid, symbol, mts_create, mts_updated, amount, amount_orig, f_type, + flags, status, rate, period, notify, hidden, renew): + # pylint: disable=invalid-name + self.id = fid + self.symbol = symbol + self.mts_create = mts_create + self.mts_updated = mts_updated + self.amount = amount + self.amount_orig = amount_orig + self.f_type = f_type + self.flags = flags + self.status = status + self.rate = rate + self.period = period + self.notify = notify + self.hidden = hidden + self.renew = renew + + @staticmethod + def from_raw_offer(raw_offer): + """ + Parse a raw funding offer into a RawFunding object + + @return FundingOffer + """ + oid = raw_offer[FundingOfferModel.ID] + symbol = raw_offer[FundingOfferModel.SYMBOL] + mts_create = raw_offer[FundingOfferModel.MTS_CREATE] + mts_updated = raw_offer[FundingOfferModel.MTS_UPDATED] + amount = raw_offer[FundingOfferModel.AMOUNT] + amount_orig = raw_offer[FundingOfferModel.AMOUNT_ORIG] + f_type = raw_offer[FundingOfferModel.TYPE] + flags = raw_offer[FundingOfferModel.FLAGS] + status = raw_offer[FundingOfferModel.STATUS] + rate = raw_offer[FundingOfferModel.RATE] + period = raw_offer[FundingOfferModel.PERIOD] + notify = raw_offer[FundingOfferModel.NOTFIY] + hidden = raw_offer[FundingOfferModel.HIDDEN] + renew = raw_offer[FundingOfferModel.RENEW] + return FundingOffer(oid, symbol, mts_create, mts_updated, amount, + amount_orig, f_type, flags, status, rate, period, notify, hidden, renew) + + def __str__(self): + return "FundingOffer '{}' ".format( + self.symbol, self.id, self.rate, self.period, self.status) diff --git a/bfxapi/models/order.py b/bfxapi/models/order.py new file mode 100644 index 0000000..00cb7e2 --- /dev/null +++ b/bfxapi/models/order.py @@ -0,0 +1,221 @@ +""" +Module used to describe all of the different data types +""" + +import time +import datetime + + +class OrderType: + """ + Enum used to describe all of the different order types available for use + """ + MARKET = 'MARKET' + LIMIT = 'LIMIT' + STOP = 'STOP' + STOP_LIMIT = 'STOP LIMIT' + TRAILING_STOP = 'TRAILING STOP' + FILL_OR_KILL = 'FOK' + EXCHANGE_MARKET = 'EXCHANGE MARKET' + EXCHANGE_LIMIT = 'EXCHANGE LIMIT' + EXCHANGE_STOP = 'EXCHANGE STOP' + EXCHANGE_STOP_LIMIT = 'EXCHANGE STOP LIMIT' + EXCHANGE_TRAILING_STOP = 'EXCHANGE TRAILING STOP' + EXCHANGE_FILL_OR_KILL = 'EXCHANGE FOK' + + +LIMIT_ORDERS = [OrderType.LIMIT, OrderType.STOP_LIMIT, OrderType.EXCHANGE_LIMIT, + OrderType.EXCHANGE_STOP_LIMIT, OrderType.FILL_OR_KILL, + OrderType.EXCHANGE_FILL_OR_KILL] + + +class OrderSide: + """ + Enum used to describe the different directions of an order + """ + BUY = 'buy' + SELL = 'sell' + + +class OrderClosedModel: + """ + Enum used ad an index match to locate the different values in a + raw order array + """ + ID = 0 + GID = 1 + CID = 2 + SYMBOL = 3 + MTS_CREATE = 4 + MTS_UPDATE = 5 + AMOUNT = 6 + AMOUNT_ORIG = 7 + TYPE = 8 + TYPE_PREV = 9 + FLAGS = 12 + STATUS = 13 + PRICE = 16 + PRIVE_AVG = 17 + PRICE_TRAILING = 18 + PRICE_AUX_LIMIT = 19 + NOTIFY = 23 + PLACE_ID = 25 + + +class OrderFlags: + """ + Enum used to explain the different values that can be passed in + as flags + """ + HIDDEN = 64 + CLOSE = 12 + REDUCE_ONLY = 1024 + POST_ONLY = 4096 + OCO = 16384 + + +def now_in_mills(): + """ + Gets the current time in milliseconds + """ + return int(round(time.time() * 1000)) + + +class Order: + """ + ID int64 Order ID + GID int Group ID + CID int Client Order ID + SYMBOL string Pair (tBTCUSD, ...) + MTS_CREATE int Millisecond timestamp of creation + MTS_UPDATE int Millisecond timestamp of update + AMOUNT float Positive means buy, negative means sell. + AMOUNT_ORIG float Original amount + TYPE string The type of the order: LIMIT, MARKET, STOP, TRAILING STOP, + EXCHANGE MARKET, EXCHANGE LIMIT, EXCHANGE STOP, EXCHANGE TRAILING STOP, FOK, EXCHANGE FOK. + TYPE_PREV string Previous order type + FLAGS int Upcoming Params Object (stay tuned) + ORDER_STATUS string Order Status: ACTIVE, EXECUTED, PARTIALLY FILLED, CANCELED + PRICE float Price + PRICE_AVG float Average price + PRICE_TRAILING float The trailing price + PRICE_AUX_LIMIT float Auxiliary Limit price (for STOP LIMIT) + HIDDEN int 1 if Hidden, 0 if not hidden + PLACED_ID int If another order caused this order to be placed (OCO) this will be that other + order's ID + """ + + Type = OrderType() + Side = OrderSide() + Flags = OrderFlags() + + def __init__(self, oid, gid, cid, symbol, mts_create, mts_update, amount, + amount_orig, o_type, typePrev, flags, status, price, price_avg, + price_trailing, price_aux_limit, notfiy, place_id): + # pylint: disable=invalid-name + self.id = oid + self.gid = gid + self.cid = cid + self.symbol = symbol + self.mts_create = mts_create + self.mts_update = mts_update + # self.amount = amount + self.amount = amount + self.amount_orig = amount_orig + self.type = o_type + self.type_prev = typePrev + self.flags = flags + self.status = status + self.price = price + self.price_avg = price_avg + self.price_trailing = price_trailing + self.price_aux_limit = price_aux_limit + self.notfiy = notfiy + self.place_id = place_id + self.tag = "" + self.fee = 0 + + self.is_pending_bool = True + self.is_confirmed_bool = False + self.is_open_bool = False + + self.date = datetime.datetime.fromtimestamp(mts_create/1000.0) + # if cancelled then priceAvg wont exist + if price_avg: + # check if order is taker or maker + if self.type in LIMIT_ORDERS: + self.fee = (price_avg * abs(amount)) * 0.001 + else: + self.fee = (price_avg * abs(amount)) * 0.002 + + @staticmethod + def from_raw_order(raw_order): + """ + Parse a raw order object into an Order oject + + @return Order + """ + oid = raw_order[OrderClosedModel.ID] + gid = raw_order[OrderClosedModel.GID] + cid = raw_order[OrderClosedModel.CID] + symbol = raw_order[OrderClosedModel.SYMBOL] + mts_create = raw_order[OrderClosedModel.MTS_CREATE] + mts_update = raw_order[OrderClosedModel.MTS_UPDATE] + amount = raw_order[OrderClosedModel.AMOUNT] + amount_orig = raw_order[OrderClosedModel.AMOUNT_ORIG] + o_type = raw_order[OrderClosedModel.TYPE] + type_prev = raw_order[OrderClosedModel.TYPE_PREV] + flags = raw_order[OrderClosedModel.FLAGS] + status = raw_order[OrderClosedModel.STATUS] + price = raw_order[OrderClosedModel.PRICE] + price_avg = raw_order[OrderClosedModel.PRIVE_AVG] + price_trailing = raw_order[OrderClosedModel.PRICE_TRAILING] + price_aux_limit = raw_order[OrderClosedModel.PRICE_AUX_LIMIT] + notfiy = raw_order[OrderClosedModel.NOTIFY] + place_id = raw_order[OrderClosedModel.PLACE_ID] + + return Order(oid, gid, cid, symbol, mts_create, mts_update, amount, + amount_orig, o_type, type_prev, flags, status, price, price_avg, + price_trailing, price_aux_limit, notfiy, place_id) + + def set_confirmed(self): + """ + Set the state of the order to be confirmed + """ + self.is_pending_bool = False + self.is_confirmed_bool = True + + def set_open_state(self, is_open): + """ + Set the is_open state of the order + """ + self.is_open_bool = is_open + + def is_open(self): + """ + Check if the order is still open + + @return bool: Ture if order open else False + """ + return self.is_open_bool + + def is_pending(self): + """ + Check if the state of the order is still pending + + @return bool: True if is pending else False + """ + return self.is_pending_bool + + def is_confirmed(self): + """ + Check if the order has been confirmed by the bitfinex api + + @return bool: True if has been confirmed else False + """ + return self.is_confirmed_bool + + def __str__(self): + ''' Allow us to print the Order object in a pretty format ''' + text = "Order <'{}' mts_create={} status='{}' id={}>" + return text.format(self.symbol, self.mts_create, self.status, self.id) diff --git a/bfxapi/models/order_book.py b/bfxapi/models/order_book.py new file mode 100644 index 0000000..48a8a44 --- /dev/null +++ b/bfxapi/models/order_book.py @@ -0,0 +1,121 @@ +""" +Module used to describe all of the different data types +""" + +import zlib + + +def prepare_price(price): + """ + Convert the price to an acceptable format + """ + # convert to 4 significant figures + prep_price = '{0:.4f}'.format(price) + # remove decimal place if zero float + return '{0:g}'.format(float(prep_price)) + + +class OrderBook: + """ + Object used to store the state of the orderbook. This can then be used + in one of two ways. To get the checksum of the book or so get the bids/asks + of the book + """ + + def __init__(self): + self.asks = [] + self.bids = [] + + def get_bids(self): + """ + Get all of the bids from the orderbook + + @return bids Array + """ + return self.bids + + def get_asks(self): + """ + Get all of the asks from the orderbook + + @return asks Array + """ + return self.asks + + def update_from_snapshot(self, data): + """ + Update the orderbook with a raw orderbook snapshot + """ + for order in data: + if len(order) == 4: + if order[3] < 0: + self.bids += [order] + else: + self.asks += [order] + else: + if order[2] < 0: + self.asks += [order] + else: + self.bids += [order] + + def update_with(self, order): + """ + Update the orderbook with a single update + """ + if len(order) == 4: + amount = order[3] + count = order[2] + side = self.bids if amount < 0 else self.asks + else: + amount = order[2] + side = self.asks if amount < 0 else self.bids + count = order[1] + price = order[0] + + # if first item in ordebook + if len(side) == 0: + side += [order] + return + + # match price level + for index, s_order in enumerate(side): + s_price = s_order[0] + if s_price == price: + if count == 0: + del side[index] + return + # remove but add as new below + del side[index] + + # if ob is initialised w/o all price levels + if count == 0: + return + + # add to book and sort lowest to highest + side += [order] + side.sort(key=lambda x: x[0], reverse=not amount < 0) + return + + def checksum(self): + """ + Generate a CRC32 checksum of the orderbook + """ + data = [] + # take set of top 25 bids/asks + for index in range(0, 25): + if index < len(self.bids): + bid = self.bids[index] + price = bid[0] + amount = bid[3] if len(bid) == 4 else bid[2] + data += [prepare_price(price)] + data += [str(amount)] + if index < len(self.asks): + ask = self.asks[index] + price = ask[0] + amount = ask[3] if len(ask) == 4 else ask[2] + data += [prepare_price(price)] + data += [str(amount)] + checksum_str = ':'.join(data) + # calculate checksum and force signed integer + checksum = zlib.crc32(checksum_str.encode('utf8')) & 0xffffffff + return checksum diff --git a/bfxapi/models/position.py b/bfxapi/models/position.py new file mode 100644 index 0000000..43fc1ef --- /dev/null +++ b/bfxapi/models/position.py @@ -0,0 +1,47 @@ +""" +Module used to describe all of the different data types +""" + + +class Position: + """ + SYMBOL string Pair (tBTCUSD, ...). + STATUS string Status (ACTIVE, CLOSED). + AMOUNT float Size of the position. Positive values means a long position, + negative values means a short position. + BASE_PRICE float The price at which you entered your position. + MARGIN_FUNDING float The amount of funding being used for this position. + MARGIN_FUNDING_TYPE int 0 for daily, 1 for term. + PL float Profit & Loss + PL_PERC float Profit & Loss Percentage + PRICE_LIQ float Liquidation price + LEVERAGE float Beta value + """ + + def __init__(self, symbol, status, amount, b_price, m_funding, m_funding_type, + profit_loss, profit_loss_perc, l_price, lev): + self.symbol = symbol + self.status = status + self.amount = amount + self.base_price = b_price + self.margin_funding = m_funding + self.margin_funding_type = m_funding_type + self.profit_loss = profit_loss + self.profit_loss_percentage = profit_loss_perc + self.liquidation_price = l_price + self.leverage = lev + + @staticmethod + def from_raw_rest_position(raw_position): + """ + Generate a Position object from a raw position array + + @return Position + """ + return Position(*raw_position) + + def __str__(self): + ''' Allow us to print the Trade object in a pretty format ''' + text = "Position '{}' {} x {} " + return text.format(self.symbol, self.base_price, self.amount, + self.status, self.profit_loss) diff --git a/bfxapi/models/subscription.py b/bfxapi/models/subscription.py new file mode 100644 index 0000000..331a046 --- /dev/null +++ b/bfxapi/models/subscription.py @@ -0,0 +1,74 @@ +""" +Module used to describe all of the different data types +""" + +import time +import json + + +class Subscription: + """ + Object used to represent an individual subscription to the websocket. + This class also exposes certain functions which helps to manage the subscription + such as unsibscribe and subscribe. + """ + + def __init__(self, ws, channel_name, symbol, timeframe=None, **kwargs): + self._ws = ws + self.channel_name = channel_name + self.symbol = symbol + self.timeframe = timeframe + self.is_subscribed_bool = False + self.key = None + self.chan_id = None + if timeframe: + self.key = 'trade:{}:{}'.format(self.timeframe, self.symbol) + self.sub_id = int(round(time.time() * 1000)) + self.send_payload = self._generate_payload(**kwargs) + + def confirm_subscription(self, chan_id): + """ + Update the subscription to confirmed state + """ + self.is_subscribed_bool = True + self.chan_id = chan_id + + async def unsubscribe(self): + """ + Send an unsubscription request to the bitfinex socket + """ + if not self.is_subscribed(): + raise Exception("Subscription is not subscribed to websocket") + payload = {'event': 'unsubscribe', 'chan_id': self.chan_id} + await self._ws.send(json.dumps(payload)) + + async def subscribe(self): + """ + Send a subscription request to the bitfinex socket + """ + await self._ws.send(json.dumps(self._get_send_payload())) + + def confirm_unsubscribe(self): + """ + Update the subscription to unsubscribed state + """ + self.is_subscribed_bool = False + + def is_subscribed(self): + """ + Check if the subscription is currently subscribed + + @return bool: True if subscribed else False + """ + return self.is_subscribed_bool + + def _generate_payload(self, **kwargs): + payload = {'event': 'subscribe', + 'channel': self.channel_name, 'symbol': self.symbol} + if self.timeframe: + payload['key'] = self.key + payload.update(**kwargs) + return payload + + def _get_send_payload(self): + return self.send_payload diff --git a/bfxapi/models/trade.py b/bfxapi/models/trade.py new file mode 100644 index 0000000..a4325f3 --- /dev/null +++ b/bfxapi/models/trade.py @@ -0,0 +1,54 @@ +""" +Module used to describe all of the different data types +""" + +import datetime + + +class Trade: + """ + ID integer Trade database id + PAIR string Pair (BTCUSD, ...) + MTS_CREATE integer Execution timestamp + ORDER_ID integer Order id + EXEC_AMOUNT float Positive means buy, negative means sell + EXEC_PRICE float Execution price + ORDER_TYPE string Order type + ORDER_PRICE float Order price + MAKER int 1 if true, 0 if false + FEE float Fee + FEE_CURRENCY string Fee currency + """ + + SHORT = 'SHORT' + LONG = 'LONG' + + def __init__(self, tid, pair, mts_create, order_id, amount, price, order_type, + order_price, maker, fee, fee_currency): + # pylint: disable=invalid-name + self.id = tid + self.pair = pair + self.mts_create = mts_create + self.date = datetime.datetime.fromtimestamp(mts_create/1000.0) + self.order_id = order_id + self.amount = amount + self.direction = Trade.SHORT if amount < 0 else Trade.LONG + self.price = price + self.order_type = order_type + self.order_price = order_price + self.maker = maker + self.fee = fee + self.fee_currency = fee_currency + + @staticmethod + def from_raw_rest_trade(raw_trade): + """ + Generate a Trade object from a raw trade array + """ + # [24224048, 'tBTCUSD', 1542800024000, 1151353484, 0.09399997, 19963, None, None, + # -1, -0.000188, 'BTC'] + return Trade(*raw_trade) + + def __str__(self): + return "Trade '{}' x {} @ {} ".format( + self.pair, self.amount, self.price, self.direction, self.fee) diff --git a/bfxapi/models/wallet.py b/bfxapi/models/wallet.py new file mode 100644 index 0000000..028d5ac --- /dev/null +++ b/bfxapi/models/wallet.py @@ -0,0 +1,33 @@ +""" +Module used to describe all of the different data types +""" + + +class Wallet: + """ + Stores data relevant to a users wallet such as balance and + currency + """ + + def __init__(self, wType, currency, balance, unsettled_interest): + self.type = wType + self.currency = currency + self.balance = balance + self.unsettled_interest = unsettled_interest + self.key = "{}_{}".format(wType, currency) + + def set_balance(self, data): + """ + Set the balance of the wallet + """ + self.balance = data + + def set_unsettled_interest(self, data): + """ + Set the unsettled interest of the wallet + """ + self.unsettled_interest = data + + def __str__(self): + return "Wallet <'{}_{}' balance='{}' unsettled='{}'>".format( + self.type, self.currency, self.balance, self.unsettled_interest) diff --git a/bfxapi/rest/BfxRest.py b/bfxapi/rest/BfxRest.py index 499b7a0..e74ae22 100644 --- a/bfxapi/rest/BfxRest.py +++ b/bfxapi/rest/BfxRest.py @@ -1,3 +1,7 @@ +""" +This module contains the BFX rest client data types +""" + import asyncio import aiohttp import time @@ -8,331 +12,355 @@ from ..utils.auth import generate_auth_headers from ..models import Wallet, Order, Position, Trade, FundingLoan, FundingOffer from ..models import FundingCredit + class BfxRest: - - def __init__(self, API_KEY, API_SECRET, host='https://api.bitfinex.com/v2', loop=None, - logLevel='INFO', *args, **kwargs): - self.loop = loop or asyncio.get_event_loop() - self.API_KEY = API_KEY - self.API_SECRET = API_SECRET - self.host = host - self.logger = CustomLogger('BfxRest', logLevel=logLevel) - - async def fetch(self, endpoint, params=""): - url = '{}/{}{}'.format(self.host, endpoint, params) - async with aiohttp.ClientSession() as session: - async with session.get(url) as resp: - text = await resp.text() - if resp.status is not 200: - raise Exception('GET {} failed with status {} - {}' - .format(url, resp.status, text)) - return await resp.json() - - async def post(self, endpoint, data={}, params=""): - url = '{}/{}'.format(self.host, endpoint) - sData = json.dumps(data) - headers = generate_auth_headers( - self.API_KEY, self.API_SECRET, endpoint, sData) - headers["content-type"] = "application/json" - async with aiohttp.ClientSession() as session: - async with session.post(url + params, headers=headers, data=sData) as resp: - text = await resp.text() - if resp.status is not 200: - raise Exception('POST {} failed with status {} - {}' - .format(url, resp.status, text)) - return await resp.json() - - ################################################## - # Public Data # - ################################################## - - async def get_seed_candles(self, symbol): - endpoint = 'candles/trade:1m:{}/hist?limit=5000&_bfx=1'.format(symbol) - time_difference = (1000 * 60) * 5000 - # get now to the nearest min - now = int(round((time.time() // 60 * 60) * 1000)) - task_batch = [] - for x in range(0, 10): - start = x * time_difference - end = now - (x * time_difference) - time_difference - e2 = endpoint + '&start={}&end={}'.format(start, end) - task_batch += [asyncio.ensure_future(self.fetch(e2))] - self.logger.info("Downloading seed candles from Bitfinex...") - # call all fetch requests async - done, _ = await asyncio.wait(*[ task_batch ]) - candles = [] - for task in done: - candles += task.result() - candles.sort(key=lambda x: x[0], reverse=True) - self.logger.info("Downloaded {} candles.".format(len(candles))) - return candles - - async def get_public_candles(self, symbol, start, end, section='hist', - tf='1m', limit="100", sort=-1): """ - Get all of the public candles between the start and end period. - - @param symbol symbol string: pair symbol i.e tBTCUSD - @param secton string: available values: "last", "hist" - @param start int: millisecond start time - @param end int: millisecond end time - @param limit int: max number of items in response - @param tf int: timeframe inbetween candles i.e 1m (min), ..., 1D (day), ... 1M (month) - @param sort int: if = 1 it sorts results returned with old > new - @return Array [ MTS, OPEN, CLOSE, HIGH, LOW, VOLUME ] + BFX rest interface contains functions which are used to interact with both the public + and private Bitfinex http api's. + To use the private api you have to set the API_KEY and API_SECRET variables to your + api key. """ - endpoint = "candles/trade:{}:{}/{}".format(tf, symbol, section) - params = "?start={}&end={}&limit={}&sort={}".format(start, end, limit, sort) - candles = await self.fetch(endpoint, params=params) - return candles - async def get_public_trades(self, symbol, start, end, limit="100", sort=-1): - """ - Get all of the public trades between the start and end period. + def __init__(self, API_KEY, API_SECRET, host='https://api.bitfinex.com/v2', loop=None, + logLevel='INFO', *args, **kwargs): + self.loop = loop or asyncio.get_event_loop() + self.API_KEY = API_KEY + self.API_SECRET = API_SECRET + self.host = host + self.logger = CustomLogger('BfxRest', logLevel=logLevel) - @param symbol symbol string: pair symbol i.e tBTCUSD - @param start int: millisecond start time - @param end int: millisecond end time - @param limit int: max number of items in response - @return Array [ ID, MTS, AMOUNT, RATE, PERIOD? ] - """ - endpoint = "trades/{}/hist".format(symbol) - params = "?start={}&end={}&limit={}&sort={}".format(start, end, limit, sort) - trades = await self.fetch(endpoint, params=params) - return trades + async def fetch(self, endpoint, params=""): + """ + Fetch a GET request from the bitfinex host - async def get_public_books(self, symbol, precision="P0", length=25): - """ - Get the public orderbook for a given symbol. + @return reponse + """ + url = '{}/{}{}'.format(self.host, endpoint, params) + async with aiohttp.ClientSession() as session: + async with session.get(url) as resp: + text = await resp.text() + if resp.status is not 200: + raise Exception('GET {} failed with status {} - {}' + .format(url, resp.status, text)) + return await resp.json() - @param symbol symbol string: pair symbol i.e tBTCUSD - @param precision string: level of price aggregation (P0, P1, P2, P3, P4, R0) - @param length int: number of price points ("25", "100") - @return Array [ PRICE, COUNT, AMOUNT ] - """ - endpoint = "book/{}/{}".format(symbol, precision) - params = "?len={}".format(length) - books = await self.fetch(endpoint, params) - return books + async def post(self, endpoint, data={}, params=""): + """ + Request a POST to the bitfinex host - async def get_public_ticker(self, symbol): - """ - Get tickers for the given symbol. Tickers shows you the current best bid and ask, - as well as the last trade price. + @return response + """ + url = '{}/{}'.format(self.host, endpoint) + sData = json.dumps(data) + headers = generate_auth_headers( + self.API_KEY, self.API_SECRET, endpoint, sData) + headers["content-type"] = "application/json" + async with aiohttp.ClientSession() as session: + async with session.post(url + params, headers=headers, data=sData) as resp: + text = await resp.text() + if resp.status is not 200: + raise Exception('POST {} failed with status {} - {}' + .format(url, resp.status, text)) + return await resp.json() - @parms symbols symbol string: pair symbol i.e tBTCUSD - @return Array [ SYMBOL, BID, BID_SIZE, ASK, ASK_SIZE, DAILY_CHANGE, DAILY_CHANGE_PERC, - LAST_PRICE, VOLUME, HIGH, LOW ] - """ - endpoint = "ticker/{}".format(symbol) - ticker = await self.fetch(endpoint) - return ticker + ################################################## + # Public Data # + ################################################## - async def get_public_tickers(self, symbols): - """ - Get tickers for the given symbols. Tickers shows you the current best bid and ask, - as well as the last trade price. + async def get_seed_candles(self, symbol): + """ + Used by the honey framework, this function gets the last 4k candles. + """ + endpoint = 'candles/trade:1m:{}/hist?limit=5000&_bfx=1'.format(symbol) + time_difference = (1000 * 60) * 5000 + # get now to the nearest min + now = int(round((time.time() // 60 * 60) * 1000)) + task_batch = [] + for x in range(0, 10): + start = x * time_difference + end = now - (x * time_difference) - time_difference + e2 = endpoint + '&start={}&end={}'.format(start, end) + task_batch += [asyncio.ensure_future(self.fetch(e2))] + self.logger.info("Downloading seed candles from Bitfinex...") + # call all fetch requests async + done, _ = await asyncio.wait(*[task_batch]) + candles = [] + for task in done: + candles += task.result() + candles.sort(key=lambda x: x[0], reverse=True) + self.logger.info("Downloaded {} candles.".format(len(candles))) + return candles - @parms symbols Array: array of symbols i.e [tBTCUSD, tETHUSD] - @return Array [ SYMBOL, BID, BID_SIZE, ASK, ASK_SIZE, DAILY_CHANGE, DAILY_CHANGE_PERC, - LAST_PRICE, VOLUME, HIGH, LOW ] - """ - endpoint = "tickers/?symbols={}".format(','.join(symbols)) - ticker = await self.fetch(endpoint) - return ticker + async def get_public_candles(self, symbol, start, end, section='hist', + tf='1m', limit="100", sort=-1): + """ + Get all of the public candles between the start and end period. - ################################################## - # Authenticated Data # - ################################################## + @param symbol symbol string: pair symbol i.e tBTCUSD + @param secton string: available values: "last", "hist" + @param start int: millisecond start time + @param end int: millisecond end time + @param limit int: max number of items in response + @param tf int: timeframe inbetween candles i.e 1m (min), ..., 1D (day), + ... 1M (month) + @param sort int: if = 1 it sorts results returned with old > new + @return Array [ MTS, OPEN, CLOSE, HIGH, LOW, VOLUME ] + """ + endpoint = "candles/trade:{}:{}/{}".format(tf, symbol, section) + params = "?start={}&end={}&limit={}&sort={}".format( + start, end, limit, sort) + candles = await self.fetch(endpoint, params=params) + return candles - async def get_wallets(self): - """ - Get all wallets on account associated with API_KEY - Requires authentication. + async def get_public_trades(self, symbol, start, end, limit="100", sort=-1): + """ + Get all of the public trades between the start and end period. - @return Array - """ - endpoint = "auth/r/wallets" - raw_wallets = await self.post(endpoint) - return [ Wallet(*rw[:4]) for rw in raw_wallets ] + @param symbol symbol string: pair symbol i.e tBTCUSD + @param start int: millisecond start time + @param end int: millisecond end time + @param limit int: max number of items in response + @return Array [ ID, MTS, AMOUNT, RATE, PERIOD? ] + """ + endpoint = "trades/{}/hist".format(symbol) + params = "?start={}&end={}&limit={}&sort={}".format( + start, end, limit, sort) + trades = await self.fetch(endpoint, params=params) + return trades - async def get_active_orders(self, symbol): - """ - Get all of the active orders associated with API_KEY - Requires authentication. + async def get_public_books(self, symbol, precision="P0", length=25): + """ + Get the public orderbook for a given symbol. - @param symbol string: pair symbol i.e tBTCUSD - @return Array - """ - endpoint = "auth/r/orders/{}".format(symbol) - raw_orders = await self.post(endpoint) - return [ Order.from_raw_order(ro) for ro in raw_orders ] + @param symbol symbol string: pair symbol i.e tBTCUSD + @param precision string: level of price aggregation (P0, P1, P2, P3, P4, R0) + @param length int: number of price points ("25", "100") + @return Array [ PRICE, COUNT, AMOUNT ] + """ + endpoint = "book/{}/{}".format(symbol, precision) + params = "?len={}".format(length) + books = await self.fetch(endpoint, params) + return books - async def get_order_history(self, symbol, start, end, limit=25, sort=-1): - """ - Get all of the orders between the start and end period associated with API_KEY - - Requires authentication. + async def get_public_ticker(self, symbol): + """ + Get tickers for the given symbol. Tickers shows you the current best bid and ask, + as well as the last trade price. - @param symbol string: pair symbol i.e tBTCUSD - @param start int: millisecond start time - @param end int: millisecond end time - @param limit int: max number of items in response - @return Array - """ - endpoint = "auth/r/orders/{}/hist".format(symbol) - params = "?start={}&end={}&limit={}&sort={}".format(start, end, limit, sort) - raw_orders = await self.post(endpoint, params=params) - return [ Order.from_raw_order(ro) for ro in raw_orders ] + @parms symbols symbol string: pair symbol i.e tBTCUSD + @return Array [ SYMBOL, BID, BID_SIZE, ASK, ASK_SIZE, DAILY_CHANGE, + DAILY_CHANGE_PERC, LAST_PRICE, VOLUME, HIGH, LOW ] + """ + endpoint = "ticker/{}".format(symbol) + ticker = await self.fetch(endpoint) + return ticker - async def get_active_position(self): - """ - Get all of the active position associated with API_KEY - Requires authentication. + async def get_public_tickers(self, symbols): + """ + Get tickers for the given symbols. Tickers shows you the current best bid and ask, + as well as the last trade price. - @return Array - """ - endpoint = "auth/r/positions" - raw_positions = await self.post(endpoint) - return [ Position.from_raw_rest_position(rp) for rp in raw_positions ] + @parms symbols Array: array of symbols i.e [tBTCUSD, tETHUSD] + @return Array [ SYMBOL, BID, BID_SIZE, ASK, ASK_SIZE, DAILY_CHANGE, DAILY_CHANGE_PERC, + LAST_PRICE, VOLUME, HIGH, LOW ] + """ + endpoint = "tickers/?symbols={}".format(','.join(symbols)) + ticker = await self.fetch(endpoint) + return ticker - async def get_order_trades(self, symbol, order_id): - """ - Get all of the trades that have been generated by the given order associated with API_KEY - - Requires authentication. + ################################################## + # Authenticated Data # + ################################################## - @param symbol string: pair symbol i.e tBTCUSD - @param order_id string: id of the order - @return Array - """ - endpoint = "auth/r/order/{}:{}/trades".format(symbol, order_id) - raw_trades = await self.post(endpoint) - return [ Trade.from_raw_rest_trade(rt) for rt in raw_trades ] + async def get_wallets(self): + """ + Get all wallets on account associated with API_KEY - Requires authentication. - async def get_trades(self, symbol, start, end, limit=25): - """ - Get all of the trades between the start and end period associated with API_KEY - - Requires authentication. + @return Array + """ + endpoint = "auth/r/wallets" + raw_wallets = await self.post(endpoint) + return [Wallet(*rw[:4]) for rw in raw_wallets] - @param symbol string: pair symbol i.e tBTCUSD - @param start int: millisecond start time - @param end int: millisecond end time - @param limit int: max number of items in response - @return Array - """ - endpoint = "auth/r/trades/{}/hist".format(symbol) - params = "?start={}&end={}&limit={}".format(start, end, limit) - raw_trades = await self.post(endpoint, params=params) - return [ Trade.from_raw_rest_trade(rt) for rt in raw_trades ] + async def get_active_orders(self, symbol): + """ + Get all of the active orders associated with API_KEY - Requires authentication. - async def get_funding_offers(self, symbol): - """ - Get all of the funding offers associated with API_KEY - Requires authentication. + @param symbol string: pair symbol i.e tBTCUSD + @return Array + """ + endpoint = "auth/r/orders/{}".format(symbol) + raw_orders = await self.post(endpoint) + return [Order.from_raw_order(ro) for ro in raw_orders] - @return Array - """ - endpoint = "auth/r/funding/offers/{}".format(symbol) - offers = await self.post(endpoint) - return [ FundingOffer.from_raw_offer(o) for o in offers ] + async def get_order_history(self, symbol, start, end, limit=25, sort=-1): + """ + Get all of the orders between the start and end period associated with API_KEY + - Requires authentication. - async def get_funding_offer_history(self, symbol, start, end, limit=25): - """ - Get all of the funding offers between the start and end period associated with API_KEY - - Requires authentication. + @param symbol string: pair symbol i.e tBTCUSD + @param start int: millisecond start time + @param end int: millisecond end time + @param limit int: max number of items in response + @return Array + """ + endpoint = "auth/r/orders/{}/hist".format(symbol) + params = "?start={}&end={}&limit={}&sort={}".format( + start, end, limit, sort) + raw_orders = await self.post(endpoint, params=params) + return [Order.from_raw_order(ro) for ro in raw_orders] - @param symbol string: pair symbol i.e tBTCUSD - @param start int: millisecond start time - @param end int: millisecond end time - @param limit int: max number of items in response - @return Array - """ - endpoint = "auth/r/funding/offers/{}/hist".format(symbol) - params = "?start={}&end={}&limit={}".format(start, end, limit) - offers = await self.post(endpoint, params=params) - return [ FundingOffer.from_raw_offer(o) for o in offers ] + async def get_active_position(self): + """ + Get all of the active position associated with API_KEY - Requires authentication. - async def get_funding_loans(self, symbol): - """ - Get all of the funding loans associated with API_KEY - Requires authentication. + @return Array + """ + endpoint = "auth/r/positions" + raw_positions = await self.post(endpoint) + return [Position.from_raw_rest_position(rp) for rp in raw_positions] - @return Array - """ - endpoint = "auth/r/funding/loans/{}".format(symbol) - loans = await self.post(endpoint) - return [ FundingLoan.from_raw_loan(o) for o in loans ] + async def get_order_trades(self, symbol, order_id): + """ + Get all of the trades that have been generated by the given order associated with API_KEY + - Requires authentication. - async def get_funding_loan_history(self, symbol, start, end, limit=25): - """ - Get all of the funding loans between the start and end period associated with API_KEY - - Requires authentication. + @param symbol string: pair symbol i.e tBTCUSD + @param order_id string: id of the order + @return Array + """ + endpoint = "auth/r/order/{}:{}/trades".format(symbol, order_id) + raw_trades = await self.post(endpoint) + return [Trade.from_raw_rest_trade(rt) for rt in raw_trades] - @param symbol string: pair symbol i.e tBTCUSD - @param start int: millisecond start time - @param end int: millisecond end time - @param limit int: max number of items in response - @return Array - """ - endpoint = "auth/r/funding/loans/{}/hist".format(symbol) - params = "?start={}&end={}&limit={}".format(start, end, limit) - loans = await self.post(endpoint, params=params) - return [ FundingLoan.from_raw_loan(o) for o in loans ] + async def get_trades(self, symbol, start, end, limit=25): + """ + Get all of the trades between the start and end period associated with API_KEY + - Requires authentication. - async def get_funding_credits(self, symbol): - endpoint = "auth/r/funding/credits/{}".format(symbol) - credits = await self.post(endpoint) - return [ FundingCredit.from_raw_credit(c) for c in credits] + @param symbol string: pair symbol i.e tBTCUSD + @param start int: millisecond start time + @param end int: millisecond end time + @param limit int: max number of items in response + @return Array + """ + endpoint = "auth/r/trades/{}/hist".format(symbol) + params = "?start={}&end={}&limit={}".format(start, end, limit) + raw_trades = await self.post(endpoint, params=params) + return [Trade.from_raw_rest_trade(rt) for rt in raw_trades] - async def get_funding_credit_history(self, symbol, start, end, limit=25): - """ - Get all of the funding credits between the start and end period associated with API_KEY - - Requires authentication. + async def get_funding_offers(self, symbol): + """ + Get all of the funding offers associated with API_KEY - Requires authentication. - @param symbol string: pair symbol i.e tBTCUSD - @param start int: millisecond start time - @param end int: millisecond end time - @param limit int: max number of items in response - @return Array - """ - endpoint = "auth/r/funding/credits/{}/hist".format(symbol) - params = "?start={}&end={}&limit={}".format(start, end, limit) - credits = await self.post(endpoint, params=params) - return [ FundingCredit.from_raw_credit(c) for c in credits] + @return Array + """ + endpoint = "auth/r/funding/offers/{}".format(symbol) + offers = await self.post(endpoint) + return [FundingOffer.from_raw_offer(o) for o in offers] - ################################################## - # Orders # - ################################################## + async def get_funding_offer_history(self, symbol, start, end, limit=25): + """ + Get all of the funding offers between the start and end period associated with API_KEY + - Requires authentication. - async def __submit_order(self, symbol, amount, price, oType=Order.Type.LIMIT, - is_hidden=False, is_postonly=False, use_all_available=False, stop_order=False, - stop_buy_price=0, stop_sell_price=0): - """ - Submit a new order + @param symbol string: pair symbol i.e tBTCUSD + @param start int: millisecond start time + @param end int: millisecond end time + @param limit int: max number of items in response + @return Array + """ + endpoint = "auth/r/funding/offers/{}/hist".format(symbol) + params = "?start={}&end={}&limit={}".format(start, end, limit) + offers = await self.post(endpoint, params=params) + return [FundingOffer.from_raw_offer(o) for o in offers] - @param symbol: the name of the symbol i.e 'tBTCUSD - @param amount: order size: how much you want to buy/sell, - a negative amount indicates a sell order and positive a buy order - @param price: the price you want to buy/sell at (must be positive) - @param oType: order type, see Order.Type enum - @param is_hidden: True if order should be hidden from orderbooks - @param is_postonly: True if should be post only. Only relevant for limit - @param use_all_available: True if order should use entire balance - @param stop_order: True to set an additional STOP OCO order linked to the - current order - @param stop_buy_price: set the OCO stop buy price (requires stop_order True) - @param stop_sell_price: set the OCO stop sell price (requires stop_order True) - """ - raise NotImplementedError( - "V2 submit order has not yet been added to the bfx api. Please use bfxapi.ws") - side = Order.Side.SELL if amount < 0 else Order.Side.BUY - use_all_balance = 1 if use_all_available else 0 - payload = {} - payload['symbol'] = symbol - payload['amount'] = abs(amount) - payload['price'] = price - payload['side'] = side - payload['type'] = oType - payload['is_hidden'] = is_hidden - payload['is_postonly'] = is_postonly - payload['use_all_available'] = use_all_balance - payload['ocoorder'] = stop_order - if stop_order: - payload['buy_price_oco'] = stop_buy_price - payload['sell_price_oco'] = stop_sell_price - endpoint = 'order/new' - return await self.post(endpoint, data=payload) + async def get_funding_loans(self, symbol): + """ + Get all of the funding loans associated with API_KEY - Requires authentication. + + @return Array + """ + endpoint = "auth/r/funding/loans/{}".format(symbol) + loans = await self.post(endpoint) + return [FundingLoan.from_raw_loan(o) for o in loans] + + async def get_funding_loan_history(self, symbol, start, end, limit=25): + """ + Get all of the funding loans between the start and end period associated with API_KEY + - Requires authentication. + + @param symbol string: pair symbol i.e tBTCUSD + @param start int: millisecond start time + @param end int: millisecond end time + @param limit int: max number of items in response + @return Array + """ + endpoint = "auth/r/funding/loans/{}/hist".format(symbol) + params = "?start={}&end={}&limit={}".format(start, end, limit) + loans = await self.post(endpoint, params=params) + return [FundingLoan.from_raw_loan(o) for o in loans] + + async def get_funding_credits(self, symbol): + endpoint = "auth/r/funding/credits/{}".format(symbol) + credits = await self.post(endpoint) + return [FundingCredit.from_raw_credit(c) for c in credits] + + async def get_funding_credit_history(self, symbol, start, end, limit=25): + """ + Get all of the funding credits between the start and end period associated with API_KEY + - Requires authentication. + + @param symbol string: pair symbol i.e tBTCUSD + @param start int: millisecond start time + @param end int: millisecond end time + @param limit int: max number of items in response + @return Array + """ + endpoint = "auth/r/funding/credits/{}/hist".format(symbol) + params = "?start={}&end={}&limit={}".format(start, end, limit) + credits = await self.post(endpoint, params=params) + return [FundingCredit.from_raw_credit(c) for c in credits] + + ################################################## + # Orders # + ################################################## + + async def __submit_order(self, symbol, amount, price, oType=Order.Type.LIMIT, + is_hidden=False, is_postonly=False, use_all_available=False, + stop_order=False, stop_buy_price=0, stop_sell_price=0): + """ + Submit a new order + + @param symbol: the name of the symbol i.e 'tBTCUSD + @param amount: order size: how much you want to buy/sell, + a negative amount indicates a sell order and positive a buy order + @param price: the price you want to buy/sell at (must be positive) + @param oType: order type, see Order.Type enum + @param is_hidden: True if order should be hidden from orderbooks + @param is_postonly: True if should be post only. Only relevant for limit + @param use_all_available: True if order should use entire balance + @param stop_order: True to set an additional STOP OCO order linked to the + current order + @param stop_buy_price: set the OCO stop buy price (requires stop_order True) + @param stop_sell_price: set the OCO stop sell price (requires stop_order True) + """ + raise NotImplementedError( + "V2 submit order has not yet been added to the bfx api. Please use bfxapi.ws") + side = Order.Side.SELL if amount < 0 else Order.Side.BUY + use_all_balance = 1 if use_all_available else 0 + payload = {} + payload['symbol'] = symbol + payload['amount'] = abs(amount) + payload['price'] = price + payload['side'] = side + payload['type'] = oType + payload['is_hidden'] = is_hidden + payload['is_postonly'] = is_postonly + payload['use_all_available'] = use_all_balance + payload['ocoorder'] = stop_order + if stop_order: + payload['buy_price_oco'] = stop_buy_price + payload['sell_price_oco'] = stop_sell_price + endpoint = 'order/new' + return await self.post(endpoint, data=payload) diff --git a/bfxapi/utils/CustomLogger.py b/bfxapi/utils/CustomLogger.py index 7ee622d..c2b76a0 100644 --- a/bfxapi/utils/CustomLogger.py +++ b/bfxapi/utils/CustomLogger.py @@ -1,3 +1,7 @@ +""" +Module used to describe all of the different data types +""" + import logging RESET_SEQ = "\033[0m" @@ -22,13 +26,19 @@ KEYWORD_COLORS = { } def formatter_message(message, use_color = True): - if use_color: - message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ) - else: - message = message.replace("$RESET", "").replace("$BOLD", "") - return message + """ + Syntax highlight certain keywords + """ + if use_color: + message = message.replace("$RESET", RESET_SEQ).replace("$BOLD", BOLD_SEQ) + else: + message = message.replace("$RESET", "").replace("$BOLD", "") + return message def format_word(message, word, color_seq, bold=False, underline=False): + """ + Surround the fiven word with a sequence + """ replacer = color_seq + word + RESET_SEQ if underline: replacer = UNDERLINE_SEQ + replacer @@ -45,6 +55,9 @@ class Formatter(logging.Formatter): self.use_color = use_color def format(self, record): + """ + Format and highlight certain keywords + """ levelname = record.levelname if self.use_color and levelname in KEYWORD_COLORS: levelname_color = KEYWORD_COLORS[levelname] + levelname + RESET_SEQ @@ -71,6 +84,9 @@ class CustomLogger(logging.Logger): return def trade(self, message, *args, **kws): + """ + Print a syntax highlighted trade signal + """ if self.isEnabledFor(self.TRADE): message = format_word(message, 'CLOSED ', YELLOW, bold=True) message = format_word(message, 'OPENED ', LIGHT_BLUE, bold=True) diff --git a/bfxapi/utils/auth.py b/bfxapi/utils/auth.py index 6eebd65..31006ed 100644 --- a/bfxapi/utils/auth.py +++ b/bfxapi/utils/auth.py @@ -1,8 +1,18 @@ +""" +This module is used to house all of the functions which are used +to handle the http authentication of the client +""" + import hashlib import hmac import time def generate_auth_payload(API_KEY, API_SECRET): + """ + Generate a signed payload + + @return json Oject headers + """ nonce = _gen_nonce() authMsg, sig = _gen_signature(API_KEY, API_SECRET, nonce) @@ -15,6 +25,9 @@ def generate_auth_payload(API_KEY, API_SECRET): } def generate_auth_headers(API_KEY, API_SECRET, path, body): + """ + Generate headers for a signed payload + """ nonce = str(_gen_nonce()) signature = "/api/v2/{}{}{}".format(path, nonce, body) h = hmac.new(API_SECRET.encode('utf8'), signature.encode('utf8'), hashlib.sha384) diff --git a/bfxapi/websockets/BfxWebsocket.py b/bfxapi/websockets/BfxWebsocket.py index 8dbacbd..172d291 100644 --- a/bfxapi/websockets/BfxWebsocket.py +++ b/bfxapi/websockets/BfxWebsocket.py @@ -1,3 +1,7 @@ +""" +Module used to house the bitfine websocket client +""" + import asyncio import json import time @@ -10,397 +14,410 @@ from .OrderManager import OrderManager from ..utils.auth import generate_auth_payload from ..models import Order, Trade, OrderBook -class Flags: - DEC_S = 9 - TIME_S = 32 - TIMESTAMP = 32768 - SEQ_ALL = 65536 - CHECKSUM = 131072 - strings = { - 9: 'DEC_S', - 32: 'TIME_S', - 32768: 'TIMESTAMP', - 65536: 'SEQ_ALL', - 131072: 'CHECKSUM' - } +class Flags: + """ + Enum used to index the available flags used in the authentication + websocket packet + """ + DEC_S = 9 + TIME_S = 32 + TIMESTAMP = 32768 + SEQ_ALL = 65536 + CHECKSUM = 131072 + + strings = { + 9: 'DEC_S', + 32: 'TIME_S', + 32768: 'TIMESTAMP', + 65536: 'SEQ_ALL', + 131072: 'CHECKSUM' + } + def _parse_candle(cData, symbol, tf): - return { - 'mts': cData[0], - 'open': cData[1], - 'close': cData[2], - 'high': cData[3], - 'low': cData[4], - 'volume': cData[5], - 'symbol': symbol, - 'tf': tf - } + return { + 'mts': cData[0], + 'open': cData[1], + 'close': cData[2], + 'high': cData[3], + 'low': cData[4], + 'volume': cData[5], + 'symbol': symbol, + 'tf': tf + } + def _parse_trade_snapshot_item(tData, symbol): - return { - 'mts': tData[3], - 'price': tData[4], - 'amount': tData[5], - 'symbol': symbol - } + return { + 'mts': tData[3], + 'price': tData[4], + 'amount': tData[5], + 'symbol': symbol + } def _parse_trade(tData, symbol): - return { - 'mts': tData[1], - 'price': tData[3], - 'amount': tData[2], - 'symbol': symbol - } + return { + 'mts': tData[1], + 'price': tData[3], + 'amount': tData[2], + 'symbol': symbol + } + class BfxWebsocket(GenericWebsocket): - """ - More complex websocket that heavily relies on the btfxwss module. This websocket requires - authentication and is capable of handling orders. - https://github.com/Crypto-toolbox/btfxwss - """ + """ + More complex websocket that heavily relies on the btfxwss module. + This websocket requires authentication and is capable of handling orders. + https://github.com/Crypto-toolbox/btfxwss + """ - ERRORS = { - 10000: 'Unknown event', - 10001: 'Generic error', - 10008: 'Concurrency error', - 10020: 'Request parameters error', - 10050: 'Configuration setup failed', - 10100: 'Failed authentication', - 10111: 'Error in authentication request payload', - 10112: 'Error in authentication request signature', - 10113: 'Error in authentication request encryption', - 10114: 'Error in authentication request nonce', - 10200: 'Error in un-authentication request', - 10300: 'Subscription Failed (generic)', - 10301: 'Already Subscribed', - 10302: 'Unknown channel', - 10400: 'Subscription Failed (generic)', - 10401: 'Not subscribed', - 11000: 'Not ready, try again later', - 20000: 'User is invalid!', - 20051: 'Websocket server stopping', - 20060: 'Websocket server resyncing', - 20061: 'Websocket server resync complete' - } - - def __init__(self, API_KEY=None, API_SECRET=None, host='wss://api.bitfinex.com/ws/2', manageOrderBooks=False, - dead_man_switch=False, logLevel='INFO', *args, **kwargs): - self.API_KEY=API_KEY - self.API_SECRET=API_SECRET - self.manageOrderBooks = manageOrderBooks - self.dead_man_switch = dead_man_switch - self.pendingOrders = {} - self.orderBooks = {} - - super(BfxWebsocket, self).__init__(host, logLevel=logLevel, *args, **kwargs) - self.subscriptionManager = SubscriptionManager(self, logLevel=logLevel) - self.orderManager = OrderManager(self, logLevel=logLevel) - self.wallets = WalletManager() - - self._WS_DATA_HANDLERS = { - 'tu': self._trade_update_handler, - 'wu': self._wallet_update_handler, - 'hb': self._heart_beat_handler, - 'te': self._trade_executed_handler, - 'oc': self._order_closed_handler, - 'ou': self._order_update_handler, - 'on': self._order_new_handler, - 'os': self._order_snapshot_handler, - 'ws': self._wallet_snapshot_handler, - 'ps': self._position_snapshot_handler, - 'fos': self._funding_offer_snapshot_handler, - 'fcs': self._funding_credit_snapshot_handler, - 'fls': self._funding_load_snapshot_handler, - 'bu': self._balance_update_handler, - 'n': self._notification_handler, - 'miu': self._margin_info_update_handler, - 'fiu': self._funding_info_update_handler + ERRORS = { + 10000: 'Unknown event', + 10001: 'Generic error', + 10008: 'Concurrency error', + 10020: 'Request parameters error', + 10050: 'Configuration setup failed', + 10100: 'Failed authentication', + 10111: 'Error in authentication request payload', + 10112: 'Error in authentication request signature', + 10113: 'Error in authentication request encryption', + 10114: 'Error in authentication request nonce', + 10200: 'Error in un-authentication request', + 10300: 'Subscription Failed (generic)', + 10301: 'Already Subscribed', + 10302: 'Unknown channel', + 10400: 'Subscription Failed (generic)', + 10401: 'Not subscribed', + 11000: 'Not ready, try again later', + 20000: 'User is invalid!', + 20051: 'Websocket server stopping', + 20060: 'Websocket server resyncing', + 20061: 'Websocket server resync complete' } - self._WS_SYSTEM_HANDLERS = { - 'info': self._system_info_handler, - 'subscribed': self._system_subscribed_handler, - 'unsubscribed': self._system_unsubscribe_handler, - 'error': self._system_error_handler, - 'auth': self._system_auth_handler, - 'conf': self._system_conf_handler - } + def __init__(self, API_KEY=None, API_SECRET=None, host='wss://api.bitfinex.com/ws/2', + manageOrderBooks=False, dead_man_switch=False, logLevel='INFO', *args, **kwargs): + self.API_KEY = API_KEY + self.API_SECRET = API_SECRET + self.manageOrderBooks = manageOrderBooks + self.dead_man_switch = dead_man_switch + self.pendingOrders = {} + self.orderBooks = {} - async def _ws_system_handler(self, msg): - eType = msg.get('event') - if eType in self._WS_SYSTEM_HANDLERS: - await self._WS_SYSTEM_HANDLERS[eType](msg) - else: - self.logger.warn("Unknown websocket event: '{}' {}".format(eType, msg)) + super(BfxWebsocket, self).__init__( + host, logLevel=logLevel, *args, **kwargs) + self.subscriptionManager = SubscriptionManager(self, logLevel=logLevel) + self.orderManager = OrderManager(self, logLevel=logLevel) + self.wallets = WalletManager() - async def _ws_data_handler(self, data): - dataEvent = data[1] - chanId = data[0] - - if type(dataEvent) is str and dataEvent in self._WS_DATA_HANDLERS: - return await self._WS_DATA_HANDLERS[dataEvent](data) - elif self.subscriptionManager.is_subscribed(chanId): - subscription = self.subscriptionManager.get(chanId) - # candles do not have an event - if subscription.channel_name == 'candles': - await self._candle_handler(data) - if subscription.channel_name == 'book': - await self._order_book_handler(data) - if subscription.channel_name == 'trades': - await self._trade_handler(data) - else: - self.logger.warn("Unknown data event: '{}' {}".format(dataEvent, data)) - - async def _system_info_handler(self, data): - self.logger.info(data) - if data.get('serverId', None): - ## connection has been established - await self.on_open() - - async def _system_conf_handler(self, data): - flag = data.get('flags') - status = data.get('status') - if flag not in Flags.strings: - self.logger.warn("Unknown config value set {}".format(flag)) - return - flagString = Flags.strings[flag] - if status == "OK": - self.logger.info("Enabled config flag {}".format(flagString)) - else: - self.logger.error("Unable to enable config flag {}".format(flagString)) - - async def _system_subscribed_handler(self, data): - await self.subscriptionManager.confirm_subscription(data) - - async def _system_unsubscribe_handler(self, data): - await self.subscriptionManager.confirm_unsubscribe(data) - - async def _system_error_handler(self, data): - self._emit('error', data) - - async def _system_auth_handler(self, data): - if data.get('status') == 'FAILED': - raise AuthError(self.ERRORS[data.get('code')]) - else: - self._emit('authenticated', data) - self.logger.info("Authentication successful.") - - async def _trade_update_handler(self, data): - tData = data[2] - # [209, 'tu', [312372989, 1542303108930, 0.35, 5688.61834032]] - if self.subscriptionManager.is_subscribed(data[0]): - symbol = self.subscriptionManager.get(data[0]).symbol - tradeObj = _parse_trade(tData, symbol) - self._emit('new_trade', tradeObj) - - async def _trade_executed_handler(self, data): - tData = data[2] - # [209, 'te', [312372989, 1542303108930, 0.35, 5688.61834032]] - if self.subscriptionManager.is_subscribed(data[0]): - symbol = self.subscriptionManager.get(data[0]).symbol - tradeObj = _parse_trade(tData, symbol) - self._emit('new_trade', tradeObj) - - async def _wallet_update_handler(self, data): - # [0,"wu",["exchange","USD",89134.66933283,0]] - uw = self.wallets._update_from_event(data) - self._emit('wallet_update', uw) - self.logger.info("Wallet update: {}".format(uw)) - - async def _heart_beat_handler(self, data): - self.logger.debug("Heartbeat - {}".format(self.host)) - - async def _margin_info_update_handler(self, data): - self._emit('margin_info_update', data) - self.logger.info("Margin info update: {}".format(data)) - - async def _funding_info_update_handler(self, data): - self._emit('funding_info_update', data) - self.logger.info("Funding info update: {}".format(data)) - - async def _notification_handler(self, data): - # [0, 'n', [1542289340429, 'on-req', None, None, - # [1151350600, None, 1542289341196, 'tBTCUSD', None, None, 0.01, None, 'EXCHANGE MARKET', - # None, None, None, None, None, None, None, 18970, None, 0, 0, None, None, None, 0, None, - # None, None, None, None, None, None, None], None, 'SUCCESS', 'Submitting exchange market buy order for 0.01 BTC.']] - nInfo = data[2] - self._emit('notification', nInfo) - notificationType = nInfo[6] - notificationText = nInfo[7] - if notificationType == 'ERROR': - # self._emit('error', notificationText) - self.logger.error("Notification ERROR: {}".format(notificationText)) - else: - self.logger.info("Notification SUCCESS: {}".format(notificationText)) - - async def _balance_update_handler(self, data): - self.logger.info('Balance update: {}'.format(data[2])) - self._emit('balance_update', data[2]) - - async def _order_closed_handler(self, data): - await self.orderManager.confirm_order_closed(data) - - async def _order_update_handler(self, data): - await self.orderManager.confirm_order_update(data) - - async def _order_new_handler(self, data): - await self.orderManager.confirm_order_new(data) - - async def _order_snapshot_handler(self, data): - await self.orderManager.build_from_order_snapshot(data) - - async def _wallet_snapshot_handler(self, data): - wallets = self.wallets._update_from_snapshot(data) - self._emit('wallet_snapshot', wallets) - - async def _position_snapshot_handler(self, data): - self._emit('position_snapshot', data) - self.logger.info("Position snapshot: {}".format(data)) - - async def _funding_offer_snapshot_handler(self, data): - self._emit('funding_offer_snapshot', data) - self.logger.info("Funding offer snapshot: {}".format(data)) - - async def _funding_load_snapshot_handler(self, data): - self._emit('funding_loan_snapshot', data[2]) - self.logger.info("Funding loan snapshot: {}".format(data)) - - async def _funding_credit_snapshot_handler(self, data): - self._emit('funding_credit_snapshot', data[2]) - self.logger.info("Funding credit snapshot: {}".format(data)) - - async def _trade_handler(self, data): - symbol = self.subscriptionManager.get(data[0]).symbol - if type(data[1]) is list: - data = data[1] - # Process the batch of seed trades on - # connection - data.reverse() - for t in data: - trade = { - 'mts': t[1], - 'amount': t[2], - 'price': t[3], - 'symbol': symbol + self._WS_DATA_HANDLERS = { + 'tu': self._trade_update_handler, + 'wu': self._wallet_update_handler, + 'hb': self._heart_beat_handler, + 'te': self._trade_executed_handler, + 'oc': self._order_closed_handler, + 'ou': self._order_update_handler, + 'on': self._order_new_handler, + 'os': self._order_snapshot_handler, + 'ws': self._wallet_snapshot_handler, + 'ps': self._position_snapshot_handler, + 'fos': self._funding_offer_snapshot_handler, + 'fcs': self._funding_credit_snapshot_handler, + 'fls': self._funding_load_snapshot_handler, + 'bu': self._balance_update_handler, + 'n': self._notification_handler, + 'miu': self._margin_info_update_handler, + 'fiu': self._funding_info_update_handler } - self._emit('seed_trade', trade) - async def _candle_handler(self, data): - subscription = self.subscriptionManager.get(data[0]) - if type(data[1][0]) is list: - # Process the batch of seed candles on - # websocket subscription - candlesSnapshot = data[1] - candlesSnapshot.reverse() - for c in candlesSnapshot: - candle = _parse_candle(c, subscription.symbol, subscription.timeframe) - self._emit('seed_candle', candle) - else: - candle = _parse_candle(data[1], subscription.symbol, subscription.timeframe) - self._emit('new_candle', candle) - - async def _order_book_handler(self, data): - obInfo = data[1] - chanId = data[0] - subscription = self.subscriptionManager.get(data[0]) - symbol = subscription.symbol - if data[1] == "cs": - dChecksum = data[2] & 0xffffffff # force to signed int - checksum = self.orderBooks[symbol].checksum() - # force checksums to signed integers - isValid = (dChecksum) == (checksum) - if isValid: - self.logger.debug("Checksum orderbook validation for '{}' successful." - .format(symbol)) - else: - self.logger.warn("Checksum orderbook invalid for '{}'. Resetting subscription." - .format(symbol)) - # re-build orderbook with snapshot - await self.subscriptionManager.resubscribe(chanId) - return - if obInfo == []: - self.orderBooks[symbol] = OrderBook() - return - isSnapshot = type(obInfo[0]) is list - if isSnapshot: - self.orderBooks[symbol] = OrderBook() - self.orderBooks[symbol].updateFromSnapshot(obInfo) - self._emit('order_book_snapshot', { 'symbol': symbol, 'data': obInfo }) - else: - self.orderBooks[symbol].updateWith(obInfo) - self._emit('order_book_update', { 'symbol': symbol, 'data': obInfo }) + self._WS_SYSTEM_HANDLERS = { + 'info': self._system_info_handler, + 'subscribed': self._system_subscribed_handler, + 'unsubscribed': self._system_unsubscribe_handler, + 'error': self._system_error_handler, + 'auth': self._system_auth_handler, + 'conf': self._system_conf_handler + } - async def on_message(self, message): - self.logger.debug(message) - msg = json.loads(message) - self._emit('all', msg) - if type(msg) is dict: - # System messages are received as json - await self._ws_system_handler(msg) - elif type(msg) is list: - # All data messages are received as a list - await self._ws_data_handler(msg) - else: - self.logger.warn('Unknown websocket response: {}'.format(msg)) + async def _ws_system_handler(self, msg): + eType = msg.get('event') + if eType in self._WS_SYSTEM_HANDLERS: + await self._WS_SYSTEM_HANDLERS[eType](msg) + else: + self.logger.warn( + "Unknown websocket event: '{}' {}".format(eType, msg)) - async def _ws_authenticate_socket(self): - jdata = generate_auth_payload(self.API_KEY, self.API_SECRET) - if self.dead_man_switch: - jdata['dms'] = 4 - await self.ws.send(json.dumps(jdata)) + async def _ws_data_handler(self, data): + dataEvent = data[1] + chan_id = data[0] - async def on_open(self): - self.logger.info("Websocket opened.") - self._emit('connected') - # Orders are simulated in backtest mode - if self.API_KEY and self.API_SECRET: - await self._ws_authenticate_socket() - # enable order book checksums - if self.manageOrderBooks: - await self.enable_flag(Flags.CHECKSUM) + if type(dataEvent) is str and dataEvent in self._WS_DATA_HANDLERS: + return await self._WS_DATA_HANDLERS[dataEvent](data) + elif self.subscriptionManager.is_subscribed(chan_id): + subscription = self.subscriptionManager.get(chan_id) + # candles do not have an event + if subscription.channel_name == 'candles': + await self._candle_handler(data) + if subscription.channel_name == 'book': + await self._order_book_handler(data) + if subscription.channel_name == 'trades': + await self._trade_handler(data) + else: + self.logger.warn( + "Unknown data event: '{}' {}".format(dataEvent, data)) - async def _send_auth_command(self, channel_name, data): - payload = [0, channel_name, None, data] - await self.ws.send(json.dumps(payload)) + async def _system_info_handler(self, data): + self.logger.info(data) + if data.get('serverId', None): + # connection has been established + await self.on_open() - async def enable_flag(self, flag): - payload = { - "event": 'conf', - "flags": flag - } - await self.ws.send(json.dumps(payload)) + async def _system_conf_handler(self, data): + flag = data.get('flags') + status = data.get('status') + if flag not in Flags.strings: + self.logger.warn("Unknown config value set {}".format(flag)) + return + flagString = Flags.strings[flag] + if status == "OK": + self.logger.info("Enabled config flag {}".format(flagString)) + else: + self.logger.error( + "Unable to enable config flag {}".format(flagString)) - def get_orderbook(self, symbol): - return self.orderBooks.get(symbol, None) + async def _system_subscribed_handler(self, data): + await self.subscriptionManager.confirm_subscription(data) - async def subscribe(self, *args, **kwargs): - return await self.subscriptionManager.subscribe(*args, **kwargs) + async def _system_unsubscribe_handler(self, data): + await self.subscriptionManager.confirm_unsubscribe(data) - async def unsubscribe(self, *args, **kwargs): - return await self.subscriptionManager.unsubscribe(*args, **kwargs) + async def _system_error_handler(self, data): + self._emit('error', data) - async def resubscribe(self, *args, **kwargs): - return await self.subscriptionManager.resubscribe(*args, **kwargs) + async def _system_auth_handler(self, data): + if data.get('status') == 'FAILED': + raise AuthError(self.ERRORS[data.get('code')]) + else: + self._emit('authenticated', data) + self.logger.info("Authentication successful.") - async def unsubscribe_all(self, *args, **kwargs): - return await self.subscriptionManager.unsubscribe_all(*args, **kwargs) + async def _trade_update_handler(self, data): + tData = data[2] + # [209, 'tu', [312372989, 1542303108930, 0.35, 5688.61834032]] + if self.subscriptionManager.is_subscribed(data[0]): + symbol = self.subscriptionManager.get(data[0]).symbol + tradeObj = _parse_trade(tData, symbol) + self._emit('new_trade', tradeObj) - async def resubscribe_all(self, *args, **kwargs): - return await self.subscriptionManager.resubscribe_all(*args, **kwargs) + async def _trade_executed_handler(self, data): + tData = data[2] + # [209, 'te', [312372989, 1542303108930, 0.35, 5688.61834032]] + if self.subscriptionManager.is_subscribed(data[0]): + symbol = self.subscriptionManager.get(data[0]).symbol + tradeObj = _parse_trade(tData, symbol) + self._emit('new_trade', tradeObj) - async def submit_order(self, *args, **kwargs): - return await self.orderManager.submit_order(*args, **kwargs) + async def _wallet_update_handler(self, data): + # [0,"wu",["exchange","USD",89134.66933283,0]] + uw = self.wallets._update_from_event(data) + self._emit('wallet_update', uw) + self.logger.info("Wallet update: {}".format(uw)) - async def update_order(self, *args, **kwargs): - return await self.orderManager.update_order(*args, **kwargs) + async def _heart_beat_handler(self, data): + self.logger.debug("Heartbeat - {}".format(self.host)) - async def cancel_order(self, *args, **kwargs): - return await self.orderManager.cancel_order(*args, **kwargs) + async def _margin_info_update_handler(self, data): + self._emit('margin_info_update', data) + self.logger.info("Margin info update: {}".format(data)) - async def cancel_all_orders(self, *args, **kwargs): - return await self.orderManager.cancel_all_orders(*args, **kwargs) - - async def cancel_order_multi(self, *args, **kwargs): - return await self.cancel_order_multi(*args, **kwargs) + async def _funding_info_update_handler(self, data): + self._emit('funding_info_update', data) + self.logger.info("Funding info update: {}".format(data)) + + async def _notification_handler(self, data): + nInfo = data[2] + self._emit('notification', nInfo) + notificationType = nInfo[6] + notificationText = nInfo[7] + if notificationType == 'ERROR': + # self._emit('error', notificationText) + self.logger.error( + "Notification ERROR: {}".format(notificationText)) + else: + self.logger.info( + "Notification SUCCESS: {}".format(notificationText)) + + async def _balance_update_handler(self, data): + self.logger.info('Balance update: {}'.format(data[2])) + self._emit('balance_update', data[2]) + + async def _order_closed_handler(self, data): + await self.orderManager.confirm_order_closed(data) + + async def _order_update_handler(self, data): + await self.orderManager.confirm_order_update(data) + + async def _order_new_handler(self, data): + await self.orderManager.confirm_order_new(data) + + async def _order_snapshot_handler(self, data): + await self.orderManager.build_from_order_snapshot(data) + + async def _wallet_snapshot_handler(self, data): + wallets = self.wallets._update_from_snapshot(data) + self._emit('wallet_snapshot', wallets) + + async def _position_snapshot_handler(self, data): + self._emit('position_snapshot', data) + self.logger.info("Position snapshot: {}".format(data)) + + async def _funding_offer_snapshot_handler(self, data): + self._emit('funding_offer_snapshot', data) + self.logger.info("Funding offer snapshot: {}".format(data)) + + async def _funding_load_snapshot_handler(self, data): + self._emit('funding_loan_snapshot', data[2]) + self.logger.info("Funding loan snapshot: {}".format(data)) + + async def _funding_credit_snapshot_handler(self, data): + self._emit('funding_credit_snapshot', data[2]) + self.logger.info("Funding credit snapshot: {}".format(data)) + + async def _trade_handler(self, data): + symbol = self.subscriptionManager.get(data[0]).symbol + if type(data[1]) is list: + data = data[1] + # Process the batch of seed trades on + # connection + data.reverse() + for t in data: + trade = { + 'mts': t[1], + 'amount': t[2], + 'price': t[3], + 'symbol': symbol + } + self._emit('seed_trade', trade) + + async def _candle_handler(self, data): + subscription = self.subscriptionManager.get(data[0]) + if type(data[1][0]) is list: + # Process the batch of seed candles on + # websocket subscription + candlesSnapshot = data[1] + candlesSnapshot.reverse() + for c in candlesSnapshot: + candle = _parse_candle( + c, subscription.symbol, subscription.timeframe) + self._emit('seed_candle', candle) + else: + candle = _parse_candle( + data[1], subscription.symbol, subscription.timeframe) + self._emit('new_candle', candle) + + async def _order_book_handler(self, data): + obInfo = data[1] + chan_id = data[0] + subscription = self.subscriptionManager.get(data[0]) + symbol = subscription.symbol + if data[1] == "cs": + dChecksum = data[2] & 0xffffffff # force to signed int + checksum = self.orderBooks[symbol].checksum() + # force checksums to signed integers + isValid = (dChecksum) == (checksum) + if isValid: + msg = "Checksum orderbook validation for '{}' successful." + self.logger.debug(msg.format(symbol)) + else: + msg = "Checksum orderbook invalid for '{}'. Resetting subscription." + self.logger.warn(msg.format(symbol)) + # re-build orderbook with snapshot + await self.subscriptionManager.resubscribe(chan_id) + return + if obInfo == []: + self.orderBooks[symbol] = OrderBook() + return + isSnapshot = type(obInfo[0]) is list + if isSnapshot: + self.orderBooks[symbol] = OrderBook() + self.orderBooks[symbol].update_from_snapshot(obInfo) + self._emit('order_book_snapshot', { + 'symbol': symbol, 'data': obInfo}) + else: + self.orderBooks[symbol].update_with(obInfo) + self._emit('order_book_update', {'symbol': symbol, 'data': obInfo}) + + async def on_message(self, message): + self.logger.debug(message) + msg = json.loads(message) + self._emit('all', msg) + if type(msg) is dict: + # System messages are received as json + await self._ws_system_handler(msg) + elif type(msg) is list: + # All data messages are received as a list + await self._ws_data_handler(msg) + else: + self.logger.warn('Unknown websocket response: {}'.format(msg)) + + async def _ws_authenticate_socket(self): + jdata = generate_auth_payload(self.API_KEY, self.API_SECRET) + if self.dead_man_switch: + jdata['dms'] = 4 + await self.ws.send(json.dumps(jdata)) + + async def on_open(self): + self.logger.info("Websocket opened.") + self._emit('connected') + # Orders are simulated in backtest mode + if self.API_KEY and self.API_SECRET: + await self._ws_authenticate_socket() + # enable order book checksums + if self.manageOrderBooks: + await self.enable_flag(Flags.CHECKSUM) + + async def _send_auth_command(self, channel_name, data): + payload = [0, channel_name, None, data] + await self.ws.send(json.dumps(payload)) + + async def enable_flag(self, flag): + payload = { + "event": 'conf', + "flags": flag + } + await self.ws.send(json.dumps(payload)) + + def get_orderbook(self, symbol): + return self.orderBooks.get(symbol, None) + + async def subscribe(self, *args, **kwargs): + return await self.subscriptionManager.subscribe(*args, **kwargs) + + async def unsubscribe(self, *args, **kwargs): + return await self.subscriptionManager.unsubscribe(*args, **kwargs) + + async def resubscribe(self, *args, **kwargs): + return await self.subscriptionManager.resubscribe(*args, **kwargs) + + async def unsubscribe_all(self, *args, **kwargs): + return await self.subscriptionManager.unsubscribe_all(*args, **kwargs) + + async def resubscribe_all(self, *args, **kwargs): + return await self.subscriptionManager.resubscribe_all(*args, **kwargs) + + async def submit_order(self, *args, **kwargs): + return await self.orderManager.submit_order(*args, **kwargs) + + async def update_order(self, *args, **kwargs): + return await self.orderManager.update_order(*args, **kwargs) + + async def cancel_order(self, *args, **kwargs): + return await self.orderManager.cancel_order(*args, **kwargs) + + async def cancel_all_orders(self, *args, **kwargs): + return await self.orderManager.cancel_all_orders(*args, **kwargs) + + async def cancel_order_multi(self, *args, **kwargs): + return await self.cancel_order_multi(*args, **kwargs) diff --git a/bfxapi/websockets/GenericWebsocket.py b/bfxapi/websockets/GenericWebsocket.py index bf3d5e0..0801fbf 100644 --- a/bfxapi/websockets/GenericWebsocket.py +++ b/bfxapi/websockets/GenericWebsocket.py @@ -1,3 +1,7 @@ +""" +Module used as a interfeace to describe a generick websocket client +""" + import asyncio import websockets import json @@ -5,66 +9,106 @@ import json from pyee import EventEmitter from ..utils.CustomLogger import CustomLogger -class AuthError(Exception): pass + +class AuthError(Exception): + """ + Thrown whenever there is a problem with the authentication packet + """ + pass + def is_json(myjson): - try: - json_object = json.loads(myjson) - except ValueError as e: - return False - return True + try: + json_object = json.loads(myjson) + except ValueError as e: + return False + return True -class GenericWebsocket(object): - def __init__(self, host, logLevel='INFO', loop=None): - self.host = host - self.logger = CustomLogger('BfxWebsocket', logLevel=logLevel) - self.loop = loop or asyncio.get_event_loop() - self.events = EventEmitter(scheduler=asyncio.ensure_future, loop=self.loop) - self.ws = None +class GenericWebsocket: + """ + Websocket object used to contain the base functionality of a websocket. + Inlcudes an event emitter and a standard websocket client. + """ - def run(self): - self.loop.run_until_complete(self._main(self.host)) + def __init__(self, host, logLevel='INFO', loop=None): + self.host = host + self.logger = CustomLogger('BfxWebsocket', logLevel=logLevel) + self.loop = loop or asyncio.get_event_loop() + self.events = EventEmitter( + scheduler=asyncio.ensure_future, loop=self.loop) + self.ws = None - def get_task_executable(self): - return self._main(self.host) + def run(self): + """ + Run the websocket connection indefinitely + """ + self.loop.run_until_complete(self._main(self.host)) - async def _main(self, host): - async with websockets.connect(host) as websocket: - self.ws = websocket - self.logger.info("Wesocket connectedt to {}".format(self.host)) - while True: - await asyncio.sleep(0) - message = await websocket.recv() - await self.on_message(message) + def get_task_executable(self): + """ + Get the run indefinitely asyncio task + """ + return self._main(self.host) - def remove_all_listeners(self, event): - self.events.remove_all_listeners(event) + async def _main(self, host): + async with websockets.connect(host) as websocket: + self.ws = websocket + self.logger.info("Wesocket connectedt to {}".format(self.host)) + while True: + await asyncio.sleep(0) + message = await websocket.recv() + await self.on_message(message) - def on(self, event, func=None): - if not func: - return self.events.on(event) - self.events.on(event, func) + def remove_all_listeners(self, event): + """ + Remove all listeners from event emitter + """ + self.events.remove_all_listeners(event) - def once(self, event, func=None): - if not func: - return self.events.once(event) - self.events.once(event, func) + def on(self, event, func=None): + """ + Add a new event to the event emitter + """ + if not func: + return self.events.on(event) + self.events.on(event, func) - def _emit(self, event, *args, **kwargs): - self.events.emit(event, *args, **kwargs) + def once(self, event, func=None): + """ + Add a new event to only fire once to the event + emitter + """ + if not func: + return self.events.once(event) + self.events.once(event, func) - async def on_error(self, error): - self.logger.error(error) - self.events.emit('error', error) + def _emit(self, event, *args, **kwargs): + self.events.emit(event, *args, **kwargs) - async def on_close(self): - self.logger.info("Websocket closed.") - await self.ws.close() - self._emit('done') + async def on_error(self, error): + """ + On websocket error print and fire event + """ + self.logger.error(error) + self.events.emit('error', error) - async def on_open(self): - pass + async def on_close(self): + """ + On websocket close print and fire event + """ + self.logger.info("Websocket closed.") + await self.ws.close() + self._emit('done') - async def on_message(self, message): - pass + async def on_open(self): + """ + On websocket open + """ + pass + + async def on_message(self, message): + """ + On websocket message + """ + pass diff --git a/bfxapi/websockets/OrderManager.py b/bfxapi/websockets/OrderManager.py index bb71c3c..5490b3d 100644 --- a/bfxapi/websockets/OrderManager.py +++ b/bfxapi/websockets/OrderManager.py @@ -1,268 +1,264 @@ +""" +Module used to house all of the functions/classes used to handle orders +""" + import time import asyncio from ..utils.CustomLogger import CustomLogger from ..models import Order + class OrderManager: - - def __init__(self, bfxapi, logLevel='INFO'): - self.bfxapi = bfxapi - self.pending_orders = {} - self.pending_callbacks = {} - self.closed_orders = {} - self.open_orders = {} - self.logger = CustomLogger('BfxOrderManager', logLevel=logLevel) - - def get_open_orders(self): - return list(self.open_orders.values()) - - def get_closed_orders(self): - return list(self.closed_orders.values()) - - def get_pending_orders(self): - return list(self.pending_orders.values()) - - async def _confirm_order(self, order, isClosed=False): """ - Called every time an order signal has been received. This function - manages the local list of open orders. + Handles all of the functionality for opening, updating and closing order. + Also contains state such as all of your open orders and orders that have + closed. """ - if order.cId in self.pending_orders: - await self._execute_confirm_callback(order.cId, order) - if isClosed: - await self._execute_close_callback(order.cId, order) - order.set_confirmed() - # remove from pending orders list - del self.pending_orders[order.cId] - self.bfxapi._emit('order_confirmed', order) - else: - await self._execute_confirm_callback(order.id, order) - if isClosed: - await self._execute_close_callback(order.id, order) - async def confirm_order_closed(self, raw_ws_data): - # order created and executed - # [0,"oc",[1151349678,null,1542203391995,"tBTCUSD",1542203389940,1542203389966,0,0.1, - # "EXCHANGE MARKET",null,null,null,0,"EXECUTED @ 18922.0(0.03299997): was PARTIALLY FILLED - # @ 18909.0(0.06700003)",null,null,18909,18913.2899961,0,0,null,null,null,0,0,null,null,null, - # "API>BFX",null,null,null]] - order = Order.from_raw_order(raw_ws_data[2]) - order.set_open_state(False) - if order.id in self.open_orders: - del self.open_orders[order.id] - await self._confirm_order(order, isClosed=True) - self.logger.info("Order closed: {} {}".format(order.symbol, order.status)) - self.bfxapi._emit('order_closed', order) + def __init__(self, bfxapi, logLevel='INFO'): + self.bfxapi = bfxapi + self.pending_orders = {} + self.pending_callbacks = {} + self.closed_orders = {} + self.open_orders = {} + self.logger = CustomLogger('BfxOrderManager', logLevel=logLevel) - async def build_from_order_snapshot(self, raw_ws_data): - print (raw_ws_data) - #[0, 'os', [[1151363978, None, 1544460962979, 'tBTCUSD', 1544460959604, 1544460959626, - # -0.12620639, -0.12620639, 'EXCHANGE LIMIT', None, None,None, 0, 'ACTIVE', None, None, 18793, - # 0, 0, 0, None, None, None, 0, 0, None, None, None, 'API>BFX', None, None, None]]] - ''' - Rebuild the user orderbook based on an incoming snapshot - ''' - osData = raw_ws_data[2] - self.open_orders = {} - for raw_order in osData: - order = Order.from_raw_order(raw_order) - order.set_open_state(True) - self.open_orders[order.id] = order - self.bfxapi._emit('order_snapshot', self.get_open_orders()) + def get_open_orders(self): + return list(self.open_orders.values()) - async def confirm_order_update(self, raw_ws_data): - # order created but partially filled - # [0, 'ou', [1151351581, None, 1542629457873, 'tBTCUSD', 1542629458071, - # 1542629458189, 0.01, 0.01, 'EXCHANGE LIMIT', None, None, None, 0, 'ACTIVE', - # None, None, 100, 0, 0, 0, None, None, None, 0, 0, None, None, None, 'API>BFX', - # None, None, None]] - order = Order.from_raw_order(raw_ws_data[2]) - order.set_open_state(True) - self.open_orders[order.id] = order - await self._confirm_order(order) - self.logger.info("Order update: {}".format(order)) - self.bfxapi._emit('order_update', order) + def get_closed_orders(self): + return list(self.closed_orders.values()) - async def confirm_order_new(self, raw_ws_data): - # order created but not executed / created but partially filled - # [0, 'on', [1151351563, None, 1542624024383, 'tBTCUSD', 1542624024596, - # 1542624024617, 0.01, 0.01, 'EXCHANGE LIMIT', None, None, None, 0, 'ACTIVE', - # None, None, 100, 0, 0, 0, None, None, None, 0, 0, None, None, None, 'API>BFX', - # None, None, None]] - order = Order.from_raw_order(raw_ws_data[2]) - order.set_open_state(True) - self.open_orders[order.id] = order - await self._confirm_order(order) - self.logger.info("Order new: {}".format(order)) - self.bfxapi._emit('order_new', order) + def get_pending_orders(self): + return list(self.pending_orders.values()) - def _gen_unqiue_cid(self): - return int(round(time.time() * 1000)) + async def _confirm_order(self, order, isClosed=False): + """ + Called every time an order signal has been received. This function + manages the local list of open orders. + """ + if order.cId in self.pending_orders: + await self._execute_confirm_callback(order.cId, order) + if isClosed: + await self._execute_close_callback(order.cId, order) + order.set_confirmed() + # remove from pending orders list + del self.pending_orders[order.cId] + self.bfxapi._emit('order_confirmed', order) + else: + await self._execute_confirm_callback(order.id, order) + if isClosed: + await self._execute_close_callback(order.id, order) - async def submit_order(self, symbol, price, amount, market_type=Order.Type.LIMIT, - hidden=False, price_trailing=None, price_aux_limit=None, oco_stop_price=None, - close=False, reduce_only=False, post_only=False, oco=False, time_in_force=None, - onConfirm=None, onClose=None, *args, **kwargs): - """ - Submit a new order + async def confirm_order_closed(self, raw_ws_data): + order = Order.from_raw_order(raw_ws_data[2]) + order.set_open_state(False) + if order.id in self.open_orders: + del self.open_orders[order.id] + await self._confirm_order(order, isClosed=True) + self.logger.info("Order closed: {} {}".format( + order.symbol, order.status)) + self.bfxapi._emit('order_closed', order) - @param symbol: the name of the symbol i.e 'tBTCUSD - @param price: the price you want to buy/sell at (must be positive) - @param amount: order size: how much you want to buy/sell, - a negative amount indicates a sell order and positive a buy order - @param market_type Order.Type: please see Order.Type enum - amount decimal string Positive for buy, Negative for sell - @param hidden: if True, order should be hidden from orderbooks - @param price_trailing: decimal trailing price - @param price_aux_limit: decimal auxiliary Limit price (only for STOP LIMIT) - @param oco_stop_price: set the oco stop price (requires oco = True) - @param close: if True, close position if position present - @param reduce_only: if True, ensures that the executed order does not flip the opened position - @param post_only: if True, ensures the limit order will be added to the order book and not - match with a pre-existing order - @param oco: cancels other order option allows you to place a pair of orders stipulating - that if one order is executed fully or partially, then the other is automatically canceled + async def build_from_order_snapshot(self, raw_ws_data): + ''' + Rebuild the user orderbook based on an incoming snapshot + ''' + osData = raw_ws_data[2] + self.open_orders = {} + for raw_order in osData: + order = Order.from_raw_order(raw_order) + order.set_open_state(True) + self.open_orders[order.id] = order + self.bfxapi._emit('order_snapshot', self.get_open_orders()) - @param time_in_force: datetime for automatic order cancellation ie. 2020-01-01 10:45:23 - @param onConfirm: function called when the bitfinex websocket receives signal that the order - was confirmed - @param onClose: function called when the bitfinex websocket receives signal that the order - was closed due to being filled or cancelled - """ - cId = self._gen_unqiue_cid() - # create base payload with required data - payload = { - "cid": cId, - "type": str(market_type), - "symbol": symbol, - "amount": str(amount), - "price": str(price), - } - # caclulate and add flags - flags = self._calculate_flags(hidden, close, reduce_only, post_only, oco) - payload['flags'] = flags - # add extra parameters - if (price_trailing): - payload['price_trailing'] = price_trailing - if (price_aux_limit): - payload['price_aux_limit'] = price_aux_limit - if (oco_stop_price): - payload['price_oco_stop'] = oco_stop_price - if (time_in_force): - payload['tif'] = time_in_force - # submit the order - self.pending_orders[cId] = payload - self._create_callback(cId, onConfirm=onConfirm, onClose=onClose) - await self.bfxapi._send_auth_command('on', payload) - self.logger.info("Order cid={} ({} {} @ {}) dispatched".format( - cId, symbol, amount, price)) + async def confirm_order_update(self, raw_ws_data): + order = Order.from_raw_order(raw_ws_data[2]) + order.set_open_state(True) + self.open_orders[order.id] = order + await self._confirm_order(order) + self.logger.info("Order update: {}".format(order)) + self.bfxapi._emit('order_update', order) - async def update_order(self, orderId, price=None, amount=None, delta=None, price_aux_limit=None, - price_trailing=None, hidden=False, close=False, reduce_only=False, post_only=False, - time_in_force=None, onConfirm=None, onClose=None): - """ - Update an existing order + async def confirm_order_new(self, raw_ws_data): + order = Order.from_raw_order(raw_ws_data[2]) + order.set_open_state(True) + self.open_orders[order.id] = order + await self._confirm_order(order) + self.logger.info("Order new: {}".format(order)) + self.bfxapi._emit('order_new', order) - @param orderId: the id of the order that you want to update - @param price: the price you want to buy/sell at (must be positive) - @param amount: order size: how much you want to buy/sell, - a negative amount indicates a sell order and positive a buy order - @param delta: change of amount - @param price_trailing: decimal trailing price - @param price_aux_limit: decimal auxiliary Limit price (only for STOP LIMIT) - @param hidden: if True, order should be hidden from orderbooks - @param close: if True, close position if position present - @param reduce_only: if True, ensures that the executed order does not flip the opened position - @param post_only: if True, ensures the limit order will be added to the order book and not - match with a pre-existing order - @param time_in_force: datetime for automatic order cancellation ie. 2020-01-01 10:45:23 - @param onConfirm: function called when the bitfinex websocket receives signal that the order - was confirmed - @param onClose: function called when the bitfinex websocket receives signal that the order - was closed due to being filled or cancelled - """ - order = self.open_orders[orderId] - self._create_callback(order.cId, onConfirm=onConfirm, onClose=onClose) - payload = { "id": orderId } - if price is not None: - payload['price'] = str(price) - if amount is not None: - payload['amount'] = str(amount) - if delta is not None: - payload['delta'] = str(delta) - if price_aux_limit is not None: - payload['price_aux_limit'] = str(price_aux_limit) - if price_trailing is not None: - payload['price_trailing'] = str(price_trailing) - if time_in_force is not None: - payload['time_in_force'] = str(time_in_force) - flags = self._calculate_flags(hidden, close, reduce_only, post_only, False) - payload['flags'] = flags - await self.bfxapi._send_auth_command('ou', payload) - self.logger.info("Update Order order_id={} dispatched".format(orderId)) + def _gen_unqiue_cid(self): + return int(round(time.time() * 1000)) - async def cancel_order(self, orderId, onConfirm=None, onClose=None): - """ - Cancel an existing open order + async def submit_order(self, symbol, price, amount, market_type=Order.Type.LIMIT, + hidden=False, price_trailing=None, price_aux_limit=None, + oco_stop_price=None, close=False, reduce_only=False, + post_only=False, oco=False, time_in_force=None, + onConfirm=None, onClose=None, *args, **kwargs): + """ + Submit a new order - @param orderId: the id of the order that you want to update - @param onConfirm: function called when the bitfinex websocket receives signal that the order - was confirmed - @param onClose: function called when the bitfinex websocket receives signal that the order - was closed due to being filled or cancelled - """ - # order = self.open_orders[orderId] - self._create_callback(orderId, onConfirm=onConfirm, onClose=onClose) - await self.bfxapi._send_auth_command('oc', { 'id': orderId }) - self.logger.info("Order cancel order_id={} dispatched".format(orderId)) + @param symbol: the name of the symbol i.e 'tBTCUSD + @param price: the price you want to buy/sell at (must be positive) + @param amount: order size: how much you want to buy/sell, + a negative amount indicates a sell order and positive a buy order + @param market_type Order.Type: please see Order.Type enum + amount decimal string Positive for buy, Negative for sell + @param hidden: if True, order should be hidden from orderbooks + @param price_trailing: decimal trailing price + @param price_aux_limit: decimal auxiliary Limit price (only for STOP LIMIT) + @param oco_stop_price: set the oco stop price (requires oco = True) + @param close: if True, close position if position present + @param reduce_only: if True, ensures that the executed order does not flip the opened position + @param post_only: if True, ensures the limit order will be added to the order book and not + match with a pre-existing order + @param oco: cancels other order option allows you to place a pair of orders stipulating + that if one order is executed fully or partially, then the other is automatically canceled - async def cancel_all_orders(self): - """ - Cancel all existing open orders + @param time_in_force: datetime for automatic order cancellation ie. 2020-01-01 10:45:23 + @param onConfirm: function called when the bitfinex websocket receives signal that the order + was confirmed + @param onClose: function called when the bitfinex websocket receives signal that the order + was closed due to being filled or cancelled + """ + cId = self._gen_unqiue_cid() + # create base payload with required data + payload = { + "cid": cId, + "type": str(market_type), + "symbol": symbol, + "amount": str(amount), + "price": str(price), + } + # caclulate and add flags + flags = self._calculate_flags( + hidden, close, reduce_only, post_only, oco) + payload['flags'] = flags + # add extra parameters + if (price_trailing): + payload['price_trailing'] = price_trailing + if (price_aux_limit): + payload['price_aux_limit'] = price_aux_limit + if (oco_stop_price): + payload['price_oco_stop'] = oco_stop_price + if (time_in_force): + payload['tif'] = time_in_force + # submit the order + self.pending_orders[cId] = payload + self._create_callback(cId, onConfirm=onConfirm, onClose=onClose) + await self.bfxapi._send_auth_command('on', payload) + self.logger.info("Order cid={} ({} {} @ {}) dispatched".format( + cId, symbol, amount, price)) - This function closes orders that have been tracked locally by the OrderManager. - """ - ids = [self.open_orders[x].id for x in self.open_orders] - await self.cancel_order_multi(ids) + async def update_order(self, orderId, price=None, amount=None, delta=None, price_aux_limit=None, + price_trailing=None, hidden=False, close=False, reduce_only=False, + post_only=False, time_in_force=None, onConfirm=None, onClose=None): + """ + Update an existing order - async def cancel_order_multi(self, orderIds): - """ - Cancel existing open orders as a batch + @param orderId: the id of the order that you want to update + @param price: the price you want to buy/sell at (must be positive) + @param amount: order size: how much you want to buy/sell, + a negative amount indicates a sell order and positive a buy order + @param delta: change of amount + @param price_trailing: decimal trailing price + @param price_aux_limit: decimal auxiliary Limit price (only for STOP LIMIT) + @param hidden: if True, order should be hidden from orderbooks + @param close: if True, close position if position present + @param reduce_only: if True, ensures that the executed order does not flip the opened position + @param post_only: if True, ensures the limit order will be added to the order book and not + match with a pre-existing order + @param time_in_force: datetime for automatic order cancellation ie. 2020-01-01 10:45:23 + @param onConfirm: function called when the bitfinex websocket receives signal that the order + was confirmed + @param onClose: function called when the bitfinex websocket receives signal that the order + was closed due to being filled or cancelled + """ + order = self.open_orders[orderId] + self._create_callback(order.cId, onConfirm=onConfirm, onClose=onClose) + payload = {"id": orderId} + if price is not None: + payload['price'] = str(price) + if amount is not None: + payload['amount'] = str(amount) + if delta is not None: + payload['delta'] = str(delta) + if price_aux_limit is not None: + payload['price_aux_limit'] = str(price_aux_limit) + if price_trailing is not None: + payload['price_trailing'] = str(price_trailing) + if time_in_force is not None: + payload['time_in_force'] = str(time_in_force) + flags = self._calculate_flags( + hidden, close, reduce_only, post_only, False) + payload['flags'] = flags + await self.bfxapi._send_auth_command('ou', payload) + self.logger.info("Update Order order_id={} dispatched".format(orderId)) - @param orderIds: an array of order ids - """ - task_batch = [] - for oid in orderIds: - task_batch += [ - asyncio.ensure_future(self.open_orders[oid].close()) - ] - await asyncio.wait(*[ task_batch ]) + async def cancel_order(self, orderId, onConfirm=None, onClose=None): + """ + Cancel an existing open order - def _create_callback(self, order_identifier, onConfirm=None, onClose=None): - if order_identifier in self.pending_callbacks: - self.pending_callbacks[order_identifier] += [(onClose, onConfirm)] - else: - self.pending_callbacks[order_identifier] = [(onClose, onConfirm)] + @param orderId: the id of the order that you want to update + @param onConfirm: function called when the bitfinex websocket receives signal that the + order + was confirmed + @param onClose: function called when the bitfinex websocket receives signal that the order + was closed due to being filled or cancelled + """ + # order = self.open_orders[orderId] + self._create_callback(orderId, onConfirm=onConfirm, onClose=onClose) + await self.bfxapi._send_auth_command('oc', {'id': orderId}) + self.logger.info("Order cancel order_id={} dispatched".format(orderId)) - async def _execute_close_callback(self, order_identifier, *args, **kwargs): - if order_identifier in self.pending_callbacks: - for c in self.pending_callbacks[order_identifier]: - if c[0]: - await c[0](*args, **kwargs) - del self.pending_callbacks[order_identifier] + async def cancel_all_orders(self): + """ + Cancel all existing open orders - async def _execute_confirm_callback(self, order_identifier, *args, **kwargs): - if order_identifier in self.pending_callbacks: - for c in self.pending_callbacks[order_identifier]: - if c[1]: - await c[1](*args, **kwargs) + This function closes orders that have been tracked locally by the OrderManager. + """ + ids = [self.open_orders[x].id for x in self.open_orders] + await self.cancel_order_multi(ids) - def _calculate_flags(self, hidden, close, reduce_only, post_only, oco): - flags = 0 - flags = flags + Order.Flags.HIDDEN if hidden else flags - flags = flags + Order.Flags.CLOSE if close else flags - flags = flags + Order.Flags.REDUUCE_ONLY if reduce_only else flags - flags = flags + Order.Flags.POST_ONLY if post_only else flags - flags = flags + Order.Flags.OCO if oco else flags - return flags + async def cancel_order_multi(self, orderIds): + """ + Cancel existing open orders as a batch + + @param orderIds: an array of order ids + """ + task_batch = [] + for oid in orderIds: + task_batch += [ + asyncio.ensure_future(self.open_orders[oid].close()) + ] + await asyncio.wait(*[task_batch]) + + def _create_callback(self, order_identifier, onConfirm=None, onClose=None): + if order_identifier in self.pending_callbacks: + self.pending_callbacks[order_identifier] += [(onClose, onConfirm)] + else: + self.pending_callbacks[order_identifier] = [(onClose, onConfirm)] + + async def _execute_close_callback(self, order_identifier, *args, **kwargs): + if order_identifier in self.pending_callbacks: + for c in self.pending_callbacks[order_identifier]: + if c[0]: + await c[0](*args, **kwargs) + del self.pending_callbacks[order_identifier] + + async def _execute_confirm_callback(self, order_identifier, *args, **kwargs): + if order_identifier in self.pending_callbacks: + for c in self.pending_callbacks[order_identifier]: + if c[1]: + await c[1](*args, **kwargs) + + def _calculate_flags(self, hidden, close, reduce_only, post_only, oco): + flags = 0 + flags = flags + Order.Flags.HIDDEN if hidden else flags + flags = flags + Order.Flags.CLOSE if close else flags + flags = flags + Order.Flags.REDUUCE_ONLY if reduce_only else flags + flags = flags + Order.Flags.POST_ONLY if post_only else flags + flags = flags + Order.Flags.OCO if oco else flags + return flags diff --git a/bfxapi/websockets/SubscriptionManager.py b/bfxapi/websockets/SubscriptionManager.py index 92b1c8b..b6be132 100644 --- a/bfxapi/websockets/SubscriptionManager.py +++ b/bfxapi/websockets/SubscriptionManager.py @@ -1,3 +1,8 @@ +""" +Module used to house all of the functions/classes used to handle +subscriptions +""" + import json import asyncio import time @@ -5,126 +10,126 @@ import time from ..utils.CustomLogger import CustomLogger from ..models import Subscription + class SubscriptionManager: - def __init__(self, bfxapi, logLevel='INFO'): - self.pending_subscriptions = {} - self.subscriptions_chanid = {} - self.subscriptions_subid = {} - self.unsubscribe_callbacks = {} - self.bfxapi = bfxapi - self.logger = CustomLogger('BfxSubscriptionManager', logLevel=logLevel) + def __init__(self, bfxapi, logLevel='INFO'): + self.pending_subscriptions = {} + self.subscriptions_chanid = {} + self.subscriptions_subid = {} + self.unsubscribe_callbacks = {} + self.bfxapi = bfxapi + self.logger = CustomLogger('BfxSubscriptionManager', logLevel=logLevel) - async def subscribe(self, channel_name, symbol, timeframe=None, **kwargs): - """ - Subscribe to a new channel + async def subscribe(self, channel_name, symbol, timeframe=None, **kwargs): + """ + Subscribe to a new channel - @param channel_name: the name of the channel i.e 'books', 'candles' - @param symbol: the trading symbol i.e 'tBTCUSD' - @param timeframe: sepecifies the data timeframe between each candle (only required - for the candles channel) - """ - # create a new subscription - subscription = Subscription(self.bfxapi.ws, channel_name, symbol, timeframe, **kwargs) - self.logger.info("Subscribing to channel {}".format(channel_name)) - key = "{}_{}".format(channel_name, subscription.key or symbol) - self.pending_subscriptions[key] = subscription - await subscription.subscribe() + @param channel_name: the name of the channel i.e 'books', 'candles' + @param symbol: the trading symbol i.e 'tBTCUSD' + @param timeframe: sepecifies the data timeframe between each candle (only required + for the candles channel) + """ + # create a new subscription + subscription = Subscription( + self.bfxapi.ws, channel_name, symbol, timeframe, **kwargs) + self.logger.info("Subscribing to channel {}".format(channel_name)) + key = "{}_{}".format(channel_name, subscription.key or symbol) + self.pending_subscriptions[key] = subscription + await subscription.subscribe() - async def confirm_subscription(self, raw_ws_data): - # {"event":"subscribed","channel":"trades","chanId":1,"symbol":"tBTCUSD","pair":"BTCUSD"} - # {"event":"subscribed","channel":"candles","chanId":351,"key":"trade:1m:tBTCUSD"} - # {"event":"subscribed","channel":"book","chanId":4,"symbol":"tBTCUSD","prec":"P0","freq":"F0","len":"25","pair":"BTCUSD"} - symbol = raw_ws_data.get("symbol", None) - channel = raw_ws_data.get("channel") - chanId = raw_ws_data.get("chanId") - key = raw_ws_data.get("key", None) - get_key = "{}_{}".format(channel, key or symbol) + async def confirm_subscription(self, raw_ws_data): + symbol = raw_ws_data.get("symbol", None) + channel = raw_ws_data.get("channel") + chan_id = raw_ws_data.get("chanId") + key = raw_ws_data.get("key", None) + get_key = "{}_{}".format(channel, key or symbol) - if chanId in self.subscriptions_chanid: - # subscription has already existed in the past - p_sub = self.subscriptions_chanid[chanId] - else: - # has just been created and is pending - p_sub = self.pending_subscriptions[get_key] - # remove from pending list - del self.pending_subscriptions[get_key] - p_sub.confirm_subscription(chanId) - # add to confirmed list - self.subscriptions_chanid[chanId] = p_sub - self.subscriptions_subid[p_sub.sub_id] = p_sub - self.bfxapi._emit('subscribed', p_sub) + if chan_id in self.subscriptions_chanid: + # subscription has already existed in the past + p_sub = self.subscriptions_chanid[chan_id] + else: + # has just been created and is pending + p_sub = self.pending_subscriptions[get_key] + # remove from pending list + del self.pending_subscriptions[get_key] + p_sub.confirm_subscription(chan_id) + # add to confirmed list + self.subscriptions_chanid[chan_id] = p_sub + self.subscriptions_subid[p_sub.sub_id] = p_sub + self.bfxapi._emit('subscribed', p_sub) - async def confirm_unsubscribe(self, raw_ws_data): - chanId = raw_ws_data.get("chanId") - sub = self.subscriptions_chanid[chanId] - sub.confirm_unsubscribe() - self.bfxapi._emit('unsubscribed', sub) - # call onComplete callback if exists - if sub.sub_id in self.unsubscribe_callbacks: - await self.unsubscribe_callbacks[sub.sub_id]() - del self.unsubscribe_callbacks[sub.sub_id] + async def confirm_unsubscribe(self, raw_ws_data): + chan_id = raw_ws_data.get("chanId") + sub = self.subscriptions_chanid[chan_id] + sub.confirm_unsubscribe() + self.bfxapi._emit('unsubscribed', sub) + # call onComplete callback if exists + if sub.sub_id in self.unsubscribe_callbacks: + await self.unsubscribe_callbacks[sub.sub_id]() + del self.unsubscribe_callbacks[sub.sub_id] - def get(self, chanId): - return self.subscriptions_chanid[chanId] + def get(self, chan_id): + return self.subscriptions_chanid[chan_id] - async def unsubscribe(self, chanId, onComplete=None): - """ - Unsubscribe from the channel with the given chanId + async def unsubscribe(self, chan_id, onComplete=None): + """ + Unsubscribe from the channel with the given chanId - @param onComplete: function called when the bitfinex websocket resoponds with - a signal that confirms the subscription has been unsubscribed to - """ - sub = self.subscriptions_chanid[chanId] - if onComplete: - self.unsubscribe_callbacks[sub.sub_id] = onComplete - if sub.is_subscribed(): - await self.subscriptions_chanid[chanId].unsubscribe() + @param onComplete: function called when the bitfinex websocket resoponds with + a signal that confirms the subscription has been unsubscribed to + """ + sub = self.subscriptions_chanid[chan_id] + if onComplete: + self.unsubscribe_callbacks[sub.sub_id] = onComplete + if sub.is_subscribed(): + await self.subscriptions_chanid[chan_id].unsubscribe() - async def resubscribe(self, chanId): - """ - Unsubscribes and then subscribes to the channel with the given Id + async def resubscribe(self, chan_id): + """ + Unsubscribes and then subscribes to the channel with the given Id - This function is mostly used to force the channel to produce a fresh snapshot. - """ - sub = self.subscriptions_chanid[chanId] - async def re_sub(): - await sub.subscribe() - if sub.is_subscribed(): - # unsubscribe first and call callback to subscribe - await self.unsubscribe(chanId, re_sub) - else: - # already unsibscribed, so just subscribe - await sub.subscribe() + This function is mostly used to force the channel to produce a fresh snapshot. + """ + sub = self.subscriptions_chanid[chan_d] - def is_subscribed(self, chanId): - """ - Returns True if the channel with the given chanId is currenly subscribed to - """ - if chanId not in self.subscriptions_chanid: - return False - return self.subscriptions_chanid[chanId].is_subscribed() + async def re_sub(): + await sub.subscribe() + if sub.is_subscribed(): + # unsubscribe first and call callback to subscribe + await self.unsubscribe(chan_id, re_sub) + else: + # already unsibscribed, so just subscribe + await sub.subscribe() - async def unsubscribe_all(self): - """ - Unsubscribe from all channels. - """ - task_batch = [] - for chanId in self.subscriptions_chanid: - sub = self.get(chanId) - if sub.is_subscribed(): - task_batch += [ - asyncio.ensure_future(self.unsubscribe(chanId)) - ] - await asyncio.wait(*[ task_batch ]) + def is_subscribed(self, chan_id): + """ + Returns True if the channel with the given chanId is currenly subscribed to + """ + if chan_id not in self.subscriptions_chanid: + return False + return self.subscriptions_chanid[chan_id].is_subscribed() - async def resubscribe_all(self): - """ - Unsubscribe and then subscribe to all channels - """ - task_batch = [] - for chanId in self.subscriptions_chanid: - task_batch += [ - asyncio.ensure_future(self.resubscribe(chanId)) - ] - await asyncio.wait(*[ task_batch ]) + async def unsubscribe_all(self): + """ + Unsubscribe from all channels. + """ + task_batch = [] + for chan_id in self.subscriptions_chanid: + sub = self.get(chan_id) + if sub.is_subscribed(): + task_batch += [ + asyncio.ensure_future(self.unsubscribe(chan_id)) + ] + await asyncio.wait(*[task_batch]) + + async def resubscribe_all(self): + """ + Unsubscribe and then subscribe to all channels + """ + task_batch = [] + for chan_id in self.subscriptions_chanid: + task_batch += [ + asyncio.ensure_future(self.resubscribe(chan_id)) + ] + await asyncio.wait(*[task_batch]) diff --git a/bfxapi/websockets/WalletManager.py b/bfxapi/websockets/WalletManager.py index e194263..2c22c32 100644 --- a/bfxapi/websockets/WalletManager.py +++ b/bfxapi/websockets/WalletManager.py @@ -1,27 +1,31 @@ +""" +Module used to handle wallet updates and data types +""" from ..models import Wallet + class WalletManager: + """ + This class is used to interact with all of the different wallets + """ - def __init__(self): - self.wallets = {} + def __init__(self): + self.wallets = {} - def _update_from_snapshot(self, raw_ws_data): - # [0, 'ws', [['exchange', 'BTC', 41.25809589, 0, None], ['exchange', 'USD', 62761.86070104, 0, None]]] - wData = raw_ws_data[2] - self.wallets = {} - for wallet in wData: - new_wallet = Wallet(wallet[0], wallet[1], wallet[2], wallet[3]) - self.wallets[new_wallet.key] = new_wallet - return self.get_wallets() + def _update_from_snapshot(self, raw_ws_data): + wData = raw_ws_data[2] + self.wallets = {} + for wallet in wData: + new_wallet = Wallet(wallet[0], wallet[1], wallet[2], wallet[3]) + self.wallets[new_wallet.key] = new_wallet + return self.get_wallets() - def _update_from_event(self, raw_ws_data): - # [0,"wu",["exchange","USD",62761.86070104,0,61618.66070104]] - wallet = raw_ws_data[2] - new_wallet = Wallet(wallet[0], wallet[1], wallet[2], wallet[3]) - self.wallets[new_wallet.key] = new_wallet - return new_wallet - - def get_wallets(self): - return list(self.wallets.values()) + def _update_from_event(self, raw_ws_data): + wallet = raw_ws_data[2] + new_wallet = Wallet(wallet[0], wallet[1], wallet[2], wallet[3]) + self.wallets[new_wallet.key] = new_wallet + return new_wallet + def get_wallets(self): + return list(self.wallets.values()) diff --git a/pylint.rc b/pylint.rc new file mode 100644 index 0000000..59978ac --- /dev/null +++ b/pylint.rc @@ -0,0 +1,10 @@ +[MESSAGES CONTROL] + +disable=too-few-public-methods, + import-error, + too-many-arguments, + duplicate-code, + too-many-locals, + no-init, + len-as-condition, + too-many-instance-attributes