From f6837452fb41e1c5fb3ad0d647713ccce75ba68d Mon Sep 17 00:00:00 2001 From: Jacob Plaster Date: Wed, 11 Sep 2019 11:15:06 +0100 Subject: [PATCH] bfxapi: add rest order endpoints --- bfxapi/models/order.py | 19 ++++- bfxapi/rest/bfx_rest.py | 133 +++++++++++++++++++++-------- bfxapi/utils/auth.py | 13 +++ bfxapi/websockets/order_manager.py | 22 ++--- 4 files changed, 134 insertions(+), 53 deletions(-) diff --git a/bfxapi/models/order.py b/bfxapi/models/order.py index f209bb8..7afb347 100644 --- a/bfxapi/models/order.py +++ b/bfxapi/models/order.py @@ -1,5 +1,5 @@ """ -Module used to describe all of the different data types +Module used to describe all of the different order data types """ import time @@ -38,8 +38,7 @@ class OrderSide: class OrderClosedModel: """ - Enum used ad an index match to locate the different values in a - raw order array + Enum used index the different values in a raw order array """ ID = 0 GID = 1 @@ -151,7 +150,7 @@ class Order: @staticmethod def from_raw_order(raw_order): """ - Parse a raw order object into an Order oject + Parse a raw order object into an Order object @return Order """ @@ -178,6 +177,18 @@ class Order: amount_orig, o_type, type_prev, flags, status, price, price_avg, price_trailing, price_aux_limit, notfiy, place_id) + @staticmethod + def from_raw_order_snapshot(raw_order_snapshot): + """ + Parse a raw order snapshot array into an array of order objects + + @return Orders: array of order objects + """ + parsed_orders = [] + for raw_order in raw_order_snapshot: + parsed_orders += [Order.from_raw_order(raw_order)] + return parsed_orders + def set_confirmed(self): """ Set the state of the order to be confirmed diff --git a/bfxapi/rest/bfx_rest.py b/bfxapi/rest/bfx_rest.py index c60859b..3c37b54 100644 --- a/bfxapi/rest/bfx_rest.py +++ b/bfxapi/rest/bfx_rest.py @@ -8,9 +8,9 @@ import time import json from ..utils.custom_logger import CustomLogger -from ..utils.auth import generate_auth_headers +from ..utils.auth import generate_auth_headers, calculate_order_flags, gen_unique_cid from ..models import Wallet, Order, Position, Trade, FundingLoan, FundingOffer -from ..models import FundingCredit +from ..models import FundingCredit, Notification class BfxRest: @@ -61,7 +61,7 @@ class BfxRest: 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: + if resp.status < 200 or resp.status > 299: raise Exception('POST {} failed with status {} - {}' .format(url, resp.status, text)) parsed = json.loads(text, parse_float=self.parse_float) @@ -355,44 +355,109 @@ class BfxRest: # 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): + 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, gid=None): """ Submit a new order + @param gid: assign the order to a group identifier @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 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) + @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 + @param time_in_force: datetime for automatic order cancellation ie. 2020-01-01 10:45:23 """ - 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) + cid = gen_unique_cid() + payload = { + "cid": cid, + "type": str(market_type), + "symbol": symbol, + "amount": str(amount), + "price": str(price), + } + # calculate and add flags + flags = calculate_order_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'] = str(oco_stop_price) + if (time_in_force): + payload['tif'] = time_in_force + if (gid): + payload['gid'] = gid + endpoint = "auth/w/order/submit" + raw_notification = await self.post(endpoint, payload) + return Notification.from_raw_order(raw_notification) + + async def submit_cancel_order(self, orderId): + """ + Cancel an existing open order + + @param orderId: the id of the order that you want to update + """ + endpoint = "auth/w/order/cancel" + raw_notification = await self.post(endpoint, { 'id': orderId }) + return Notification.from_raw_order(raw_notification) + + async def submit_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): + """ + Update an existing 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 + """ + 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 = calculate_order_flags( + hidden, close, reduce_only, post_only, False) + payload['flags'] = flags + endpoint = "auth/w/order/update" + raw_notification = await self.post(endpoint, payload) + print (raw_notification) + return Notification.from_raw_order(raw_notification) + ################################################## # Derivatives # diff --git a/bfxapi/utils/auth.py b/bfxapi/utils/auth.py index 31006ed..097c88e 100644 --- a/bfxapi/utils/auth.py +++ b/bfxapi/utils/auth.py @@ -6,6 +6,7 @@ to handle the http authentication of the client import hashlib import hmac import time +from ..models import Order def generate_auth_payload(API_KEY, API_SECRET): """ @@ -48,3 +49,15 @@ def _gen_signature(API_KEY, API_SECRET, nonce): def _gen_nonce(): return int(round(time.time() * 1000000)) + +def gen_unique_cid(): + return int(round(time.time() * 1000)) + +def calculate_order_flags(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.REDUCE_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/order_manager.py b/bfxapi/websockets/order_manager.py index bb478ae..4154733 100644 --- a/bfxapi/websockets/order_manager.py +++ b/bfxapi/websockets/order_manager.py @@ -7,6 +7,7 @@ import asyncio from ..utils.custom_logger import CustomLogger from ..models import Order +from ..utils.auth import calculate_order_flags, gen_unique_cid class OrderManager: @@ -83,9 +84,6 @@ class OrderManager: self.logger.info("Order new: {}".format(order)) self.bfxapi._emit('order_new', order) - def _gen_unqiue_cid(self): - return int(round(time.time() * 1000)) - 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, @@ -94,7 +92,7 @@ class OrderManager: """ Submit a new order - @param gid: assign the order to a group identitfier + @param gid: assign the order to a group identifier @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, @@ -118,7 +116,7 @@ class OrderManager: @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() + cid = self._gen_unique_cid() # create base payload with required data payload = { "cid": cid, @@ -128,7 +126,7 @@ class OrderManager: "price": str(price), } # caclulate and add flags - flags = self._calculate_flags(hidden, close, reduce_only, post_only, oco) + flags = calculate_order_flags(hidden, close, reduce_only, post_only, oco) payload['flags'] = flags # add extra parameters if (price_trailing): @@ -187,7 +185,7 @@ class OrderManager: 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( + flags = calculate_order_flags( hidden, close, reduce_only, post_only, False) payload['flags'] = flags await self.bfxapi._send_auth_command('ou', payload) @@ -261,11 +259,5 @@ class OrderManager: del callback_storage[key] await asyncio.gather(*tasks) - 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 + def _gen_unique_cid(self): + return gen_unique_cid()