Merge pull request #2 from JacobPlaster/add-rest-services

Add rest interface to bfxapi
This commit is contained in:
Paolo Ardoino
2018-12-06 18:03:00 +01:00
committed by GitHub
25 changed files with 1400 additions and 424 deletions

153
README.md
View File

@@ -1,5 +1,5 @@
```
```python
from bfxapi import Client
bfx = Client(
@@ -20,15 +20,15 @@ bfx.ws.run()
```
# Official Python `bfxapi`
# Bitfinex Trading API for Python. Bitcoin, Ether and Litecoin trading
This is an official python library that is used to connect interact with the Bitfinex api. Currently it only has support for websockets but will soon have Rest functionality as well.
Install dependencies
```
```sh
pip3 install -r requirements.txt
```
Run the trades/candles example:
```
```sh
cd bfxapi/examples
python3 subsribe_trades_candles.py
```
@@ -46,7 +46,7 @@ python3 subsribe_trades_candles.py
### Authenticate
```
```python
bfx = Client(
API_KEY='<YOUR_API_KEY>'
API_SECRET='<YOUR_API_SECRET>'
@@ -61,26 +61,38 @@ bfx.ws.run()
### Submit limit order
```
await bfx.ws.submit_order('tBTCUSD', 19000, 0.01, 'EXCHANGE LIMIT')
```python
from bfxapi import Order
await bfx.ws.submit_order('tBTCUSD', 100, 0.01, Order.Type.EXCHANGE_MARKET)
```
### Listen for completion
```
```python
@bfx.ws.on('order_confirmed')
async def order_completed(order, trade):
print ("Order has been confrmed")
```
### Get wallets
```
wallets = bfxapi.wallets.get_wallets()
```python
wallets = bfxapi.ws.wallets.get_wallets()
# [ Wallet <'exchange_BTC' balance='41.25809589' unsettled='0'>,
# Wallet <'exchange_USD' balance='62761.86070104' unsettled='0'> ]
```
### Close all orders
### Order manipulation
All order function support onConfirm and onClose async callbacks. onConfirm is fired when we receive a signal from the websocket that the order has been confirmed. onClose is fired when we receive a signal that the order has either been filled or canceled.
```python
async def on_confirm(order):
await bfx.ws.update_order(order.id, price=1000)
await bfx.ws.submit_order('tBTCUSD', 800, 0.1, onConfirm=on_confirm)
```
### Close all orders
```python
await bfx.ws.close_all_orders()
```
@@ -118,7 +130,7 @@ The websocket exposes a collection of events that are triggered when certain dat
# `bfxapi.ws` interface
# bfxapi ws interface
#### `on(event, function)`
@@ -148,15 +160,15 @@ The websocket exposes a collection of events that are triggered when certain dat
Unsubscribe and subscribe to all data feeds
#### `submit_order(symbol, price, amount, market_type, hidden=False, onComplete=None, onError=None, *args, **kwargs)`
#### `submit_order(symbol, price, amount, market_type, hidden=False, onConfirm=None, onClose=None, *args, **kwargs)`
Submits an order to the Bitfinex api. If the order is successful then the order_closed event will be triggered and the onComplete function will also be called if provided in the parameters.
Submits an order to the Bitfinex api. When it has been verified that bitfine xhas received the order then the `onConfirm` callback will be called followed by the `order_confirmed` event. Once it has been verified that the order has completely closed due to either being filled or canceled then the `onClose` function will be called, followed by the `order_closed` event.
#### `update_order(orderId, price=None, amount=None, delta=None, price_aux_limit=None, price_trailing=None, flags=None, time_in_force=None, onComplete=None, onError=None)`
#### `update_order(orderId, price=None, amount=None, delta=None, price_aux_limit=None, price_trailing=None, flags=None, time_in_force=None, onConfirm=None, onClose=None)`
Attempts to update an order with the given values. If the order is no longer open then the update will be ignored.
#### `close_order(self, orderId, onComplete=None, onError=None):`
#### `close_order(self, orderId, onConfirm=None, onClose=None):`
Close the order with the given orderId if it still open.
@@ -168,35 +180,13 @@ The websocket exposes a collection of events that are triggered when certain dat
Takes an array of orderIds and closes them all.
# `bfxapi.wallets`
## bfxapi.ws.wallets
### `get_wallets()`
Returns an array of wallets that represent the users balance in the different currencies
# `Order obj`
### `close()`
Signals Bitfinex to close the order
### `update(self, price=None, amount=None, delta=None, price_aux_limit=None, price_trailing=None, flags=None, time_in_force=None)`
Signals Bitfinex to update the order with the given values
### `isOpen()`
Returns true if the order has not been closed
### `isPending()`
Returns true if Bitfinex has not responded to confirm the order has been received
### `isConfirmed()`
Returns true if Bitfinex has responded to confirm the order
# `Subscription obj`
## `bfx.ws.Subscription obj`
### `subscribe()`
@@ -210,7 +200,88 @@ The websocket exposes a collection of events that are triggered when certain dat
Returns true if the subscription is open and receiving data
# bfxapi rest interface
### `get_public_candles(symbol, start, end, section='hist', tf='1m', limit="100", sort=-1 `
Get All of the public candles between the given start and end period
### `get_public_trades(symbol, start, end, limit="100", sort=-1)`
Get all of the public trades between the start and end period
### `get_public_books(symbol, precision="P0", length=25)`
Get the public orderbook of a given symbol
### `get_public_ticker(symbol)`
Get tickers for the given symbol. Tickers shows you the current best bid and ask,
as well as the last trade price.
### `get_public_tickers(symbols)`
Get tickers for the given symbols. Tickers shows you the current best bid and ask,
as well as the last trade price.
### `get_wallets()`
Get all wallets on account associated with API_KEY - Requires authentication.
### `get_active_orders(symbol)`
Get all of the active orders associated with API_KEY - Requires authentication.
### `get_order_history(symbol, start, end, limit=25, sort=-1)`
Get all of the orders between the start and end period associated with API_KEY
- Requires authentication.
### `get_active_positions()`
Get all of the active position associated with API_KEY - Requires authentication.
### `get_order_trades(symbol, order_id)`
Get all of the trades that have been generated by the given order associated with API_KEY - Requires authentication.
### `get_trades(symbol, start, end, limit=25)`
Get all of the trades between the start and end period associated with API_KEY
- Requires authentication.
### `get_funding_offers(symbol)
Get all of the funding offers associated with API_KEY - Requires authentication.
### `get_funding_offer_history(symbol, start, end, limit=25)`
Get all of the funding offers between the start and end period associated with API_KEY - Requires authentication.
### `get_funding_loans(symbol)`
Get all of the funding loans associated with API_KEY - Requires authentication.
### `get_funding_loan_history(symbol, start, end, limit=25)`
Get all of the funding loans between the start and end period associated with API_KEY - Requires authentication.
### `get_funding_credits(symbol)`
Get all of the funding credits associated with API_KEY - Requires authentication.
### `get_funding_credit_history(symbol, start, end, limit=25)`
Get all of the funding credits between the start and end period associated with API_KEY - Requires authentication.
# Examples
For more info on how to use this library please see the example scripts in the `bfxapi/examples` directory.
For more info on how to use this library please see the example scripts in the `bfxapi/examples` directory. Here you will find usage of all interface exposed functions for both the rest and websocket.
## Contributing
1. Fork it ( https://github.com/[my-github-username]/bitfinex/fork )
2. Create your feature branch (`git checkout -b my-new-feature`)
3. Commit your changes (`git commit -am 'Add some feature'`)
4. Push to the branch (`git push origin my-new-feature`)
5. Create a new Pull Request

View File

@@ -2,3 +2,4 @@ name = 'bfxapi'
from bfxapi.Client import Client
from bfxapi.websockets.GenericWebsocket import GenericWebsocket
from bfxapi.models import *

View File

@@ -0,0 +1,97 @@
import os
import sys
import asyncio
import time
sys.path.append('../')
from bfxapi import Client
API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET")
bfx = Client(
API_KEY=API_KEY,
API_SECRET=API_SECRET,
logLevel='DEBUG',
rest_host='https://test.bitfinex.com/v2'
)
now = int(round(time.time() * 1000))
then = now - (1000 * 60 * 60 * 24 * 10) # 10 days ago
async def log_wallets():
wallets = await bfx.rest.get_wallets()
print ("Wallets:")
[ print (w) for w in wallets ]
async def log_active_orders():
orders = await bfx.rest.get_active_orders('tBTCUSD')
print ("Orders:")
[ print (o) for o in orders ]
async def log_orders_history():
orders = await bfx.rest.get_order_history('tBTCUSD', 0, then)
print ("Orders:")
[ print (o) for o in orders ]
async def log_active_positions():
positions = await bfx.rest.get_active_position()
print ("Positions:")
[ print (p) for p in positions ]
async def log_trades():
trades = await bfx.rest.get_trades('tBTCUSD', 0, then)
print ("Trades:")
[ print (t) for t in trades]
async def log_order_trades():
order_id = 1151353463
trades = await bfx.rest.get_order_trades('tBTCUSD', order_id)
print ("Trade orders:")
[ print (t) for t in trades]
async def log_funding_offers():
offers = await bfx.rest.get_funding_offers('tBTCUSD')
print ("Offers:")
[ print (o) for o in offers]
async def log_funding_offer_history():
offers = await bfx.rest.get_funding_offer_history('tBTCUSD', 0, then)
print ("Offers history:")
[ print (o) for o in offers]
async def log_funding_loans():
loans = await bfx.rest.get_funding_loans('tBTCUSD')
print ("Funding loans:")
[ print (l) for l in loans ]
async def log_funding_loans_history():
loans = await bfx.rest.get_funding_loan_history('tBTCUSD', 0, then)
print ("Funding loan history:")
[ print (l) for l in loans ]
async def log_funding_credits():
credits = await bfx.rest.get_funding_credits('tBTCUSD')
print ("Funding credits:")
[ print (c) for c in credits ]
async def log_funding_credits_history():
credit = await bfx.rest.get_funding_credit_history('tBTCUSD', 0, then)
print ("Funding credit history:")
[ print (c) for c in credit ]
async def run():
await log_wallets()
await log_active_orders()
await log_orders_history()
await log_active_positions()
await log_trades()
await log_order_trades()
await log_funding_offers()
await log_funding_offer_history()
await log_funding_credits()
await log_funding_credits_history()
t = asyncio.ensure_future(run())
asyncio.get_event_loop().run_until_complete(t)

View File

@@ -0,0 +1,49 @@
import os
import sys
import asyncio
import time
sys.path.append('../')
from bfxapi import Client
bfx = Client(
logLevel='DEBUG',
)
now = int(round(time.time() * 1000))
then = now - (1000 * 60 * 60 * 24 * 10) # 10 days ago
async def log_historical_candles():
candles = await bfx.rest.get_public_candles('tBTCUSD', 0, then)
print ("Candles:")
[ print (c) for c in candles ]
async def log_historical_trades():
trades = await bfx.rest.get_public_trades('tBTCUSD', 0, then)
print ("Trades:")
[ print (t) for t in trades ]
async def log_books():
orders = await bfx.rest.get_public_books('tBTCUSD')
print ("Order book:")
[ print (o) for o in orders ]
async def log_ticker():
ticker = await bfx.rest.get_public_ticker('tBTCUSD')
print ("Ticker:")
print (ticker)
async def log_mul_tickers():
tickers = await bfx.rest.get_public_tickers(['tBTCUSD', 'tETHBTC'])
print ("Tickers:")
print (tickers)
async def run():
await log_historical_candles()
await log_historical_trades()
await log_books()
await log_ticker()
await log_mul_tickers()
t = asyncio.ensure_future(run())
asyncio.get_event_loop().run_until_complete(t)

View File

@@ -2,7 +2,7 @@ import os
import sys
sys.path.append('../')
from bfxapi import Client
from bfxapi import Client, Order
API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET")
@@ -33,6 +33,6 @@ def log_error(msg):
@bfx.ws.once('authenticated')
async def submit_order(auth_message):
# create an inital order a really low price so it stays open
await bfx.ws.submit_order('tBTCUSD', 10, 1, 'EXCHANGE LIMIT')
await bfx.ws.submit_order('tBTCUSD', 10, 1, Order.Type.EXCHANGE_LIMIT)
bfx.ws.run()

View File

@@ -2,7 +2,7 @@ import os
import sys
sys.path.append('../')
from bfxapi import Client
from bfxapi import Client, Order
API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET")
@@ -14,20 +14,19 @@ bfx = Client(
)
@bfx.ws.on('order_snapshot')
async def close_all(data):
await bfx.ws.close_all_orders()
async def cancel_all(data):
await bfx.ws.cancel_all_orders()
@bfx.ws.on('order_confirmed')
async def trade_completed(order, trade):
async def trade_completed(order):
print ("Order confirmed.")
print (order)
print (trade)
## close the order
# await order.close()
# or
# await bfx.ws.close_order(order.id)
# await bfx.ws.cancel_order(order.id)
# or
# await bfx.ws.close_all_orders()
# await bfx.ws.cancel_all_orders()
@bfx.ws.on('error')
def log_error(msg):
@@ -35,7 +34,7 @@ def log_error(msg):
@bfx.ws.on('authenticated')
async def submit_order(auth_message):
await bfx.ws.submit_order('tBTCUSD', 19000, 0.01, 'EXCHANGE LIMIT')
await bfx.ws.submit_order('tBTCUSD', 19000, 0.01, Order.Type.EXCHANGE_MARKET)
# If you dont want to use a decorator
# ws.on('authenticated', submit_order)
@@ -43,6 +42,6 @@ async def submit_order(auth_message):
# You can also provide a callback
# await ws.submit_order('tBTCUSD', 0, 0.01,
# 'EXCHANGE MARKET', onComplete=trade_complete)
# 'EXCHANGE MARKET', onClose=trade_complete)
bfx.ws.run()

View File

@@ -2,7 +2,7 @@ import os
import sys
sys.path.append('../')
from bfxapi import Client
from bfxapi import Client, Order
API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET")
@@ -14,21 +14,19 @@ bfx = Client(
)
@bfx.ws.on('order_update')
def order_updated(order, trade):
def order_updated(order):
print ("Order updated.")
print (order)
print (trade)
@bfx.ws.once('order_update')
async def order_once_updated(order, trade):
async def order_once_updated(order):
# update a second time using the object function
await order.update(price=80, amount=0.02, flags="2nd update")
@bfx.ws.once('order_confirmed')
async def trade_completed(order, trade):
async def trade_completed(order):
print ("Order confirmed.")
print (order)
print (trade)
await bfx.ws.update_order(order.id, price=100, amount=0.01)
@bfx.ws.on('error')
@@ -38,6 +36,6 @@ def log_error(msg):
@bfx.ws.once('authenticated')
async def submit_order(auth_message):
# create an inital order a really low price so it stays open
await bfx.ws.submit_order('tBTCUSD', 10, 1, 'EXCHANGE LIMIT')
await bfx.ws.submit_order('tBTCUSD', 10, 1, Order.Type.EXCHANGE_LIMIT)
bfx.ws.run()

View File

@@ -0,0 +1,86 @@
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 '{}' <id={} rate={} amount={} period={} status='{}'>".format(
self.symbol, self.id, self.rate, self.amount, self.period, self.status)

View File

@@ -0,0 +1,82 @@
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 '{}' <id={} rate={} amount={} period={} status='{}'>".format(
self.symbol, self.id, self.rate, self.amount, self.period, self.status)

View File

@@ -0,0 +1,74 @@
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 '{}' <id={} rate={} period={} status='{}'>".format(
self.symbol, self.id, self.rate, self.period, self.status)

View File

@@ -1,4 +1,21 @@
import time
import datetime
class OrderType:
MARKET = 'MARKET'
LIMIT = 'LIMIT'
STOP = 'STOP'
TRAILING_STOP = 'TRAILING STOP'
FILL_OR_KILL = 'FOK'
EXCHANGE_MARKET = 'EXCHANGE MARKET'
EXCHANGE_LIMIT = 'EXCHANGE LIMIT'
EXCHANGE_STOP = 'EXCHANGE STOP'
EXCHANGE_TRAILING_STOP = 'EXCHANGE TRAILING STOP'
EXCHANGE_FILL_OR_KILL = 'EXCHANGE FOK'
class OrderSide:
BUY = 'buy'
SELL = 'sell'
class OrderClosedModel:
ID = 0
@@ -20,55 +37,95 @@ class OrderClosedModel:
NOTIFY = 23
PLACE_ID = 25
class OrderFlags:
HIDDEN = 64
CLOSE = 12
REDUCE_ONLY = 1024
POST_ONLY = 4096
OCO = 16384
def now_in_mills():
return int(round(time.time() * 1000))
class Order:
def __init__(self, bfxapi, closingOrderArray):
self.bfxapi = bfxapi
self.id = closingOrderArray[OrderClosedModel.ID]
self.gId = closingOrderArray[OrderClosedModel.GID]
self.cId = closingOrderArray[OrderClosedModel.CID]
self.symbol = closingOrderArray[OrderClosedModel.SYMBOL]
self.mtsCreate = closingOrderArray[OrderClosedModel.MTS_CREATE]
self.mtsUpdate = closingOrderArray[OrderClosedModel.MTS_UPDATE]
self.amount = closingOrderArray[OrderClosedModel.AMOUNT]
self.amountOrig = closingOrderArray[OrderClosedModel.AMOUNT_ORIG]
self.type = closingOrderArray[OrderClosedModel.TYPE]
self.typePrev = closingOrderArray[OrderClosedModel.TYPE_PREV]
self.flags = closingOrderArray[OrderClosedModel.FLAGS]
self.status = closingOrderArray[OrderClosedModel.STATUS]
self.price = closingOrderArray[OrderClosedModel.PRICE]
self.priceAvg = closingOrderArray[OrderClosedModel.PRIVE_AVG]
self.priceTrailing = closingOrderArray[OrderClosedModel.PRICE_TRAILING]
self.priceAuxLimit = closingOrderArray[OrderClosedModel.PRICE_AUX_LIMIT]
self.notfiy = closingOrderArray[OrderClosedModel.NOTIFY]
self.placeId = closingOrderArray[OrderClosedModel.PLACE_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()
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.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.is_pending_bool = True
self.is_confirmed_bool = False
self.is_open_bool = False
async def update(self, price=None, amount=None, delta=None, price_aux_limit=None,
price_trailing=None, flags=None, time_in_force=None):
payload = { "id": self.id }
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 flags is not None:
payload['flags'] = str(flags)
if time_in_force is not None:
payload['time_in_force'] = str(time_in_force)
await self.bfxapi._send_auth_command('ou', payload)
self.date = datetime.datetime.fromtimestamp(mtsCreate/1000.0)
if priceAvg:
## if cancelled then priceAvg wont exist
self.fee = (priceAvg * abs(amount)) * 0.002
async def close(self):
await self.bfxapi._send_auth_command('oc', { 'id': self.id })
@staticmethod
def from_raw_order(raw_order):
id = 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]
return Order(id, gId, cId, symbol, mtsCreate, mtsUpdate, amount, amountOrig, oType,
typePrev, flags, status, price, priceAvg, priceTrailing, priceAuxLimit, notfiy, placeId)
def set_confirmed(self):
self.is_pending_bool = False
@@ -88,5 +145,5 @@ class Order:
def __str__(self):
''' Allow us to print the Order object in a pretty format '''
return "Order <'{0}' mtsCreate={1} {2}>".format(self.symbol, self.mtsCreate,
self.status)
return "Order <'{}' mtsCreate={} status='{}' id={}>".format(self.symbol, self.mtsCreate,
self.status, self.id)

36
bfxapi/models/Position.py Normal file
View File

@@ -0,0 +1,36 @@
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, 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
@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 {} <Sstatus='{}' p&l={}>".format(
self.symbol, self.base_price, self.amount, self.status, self.profit_loss)

View File

@@ -1,20 +1,44 @@
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, order, tag=''):
self.order = order
self.amount = order.amount
self.price = order.priceAvg
self.fee = (order.priceAvg * abs(order.amount)) * 0.002
self.mts = order.mtsCreate
self.date = datetime.datetime.fromtimestamp(order.mtsCreate/1000.0)
self.direction = self.SHORT if order.amount < 0 else self.LONG
self.tag = tag
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
@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):
''' Allow us to print the Trade object in a pretty format '''
return "Trade {} @ {} fee={} <order='{}'>".format(
self.amount, self.price, self.fee, self.order)
return "Trade '{}' x {} @ {} <direction='{}' fee={}>".format(
self.pair, self.amount, self.price, self.direction, self.fee)

View File

@@ -5,3 +5,7 @@ from .Trade import *
from .OrderBook import *
from .Subscription import *
from .Wallet import *
from .Position import *
from .FundingLoan import *
from .FundingOffer import *
from .FundingCredit import *

View File

@@ -4,24 +4,47 @@ import time
import json
from ..utils.CustomLogger import CustomLogger
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):
url = '{}/{}'.format(self.host, endpoint)
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('Unable to seed trades. Received status {} - {}'
.format(resp.status, text))
return json.loads(text)
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)
@@ -43,3 +66,273 @@ class BfxRest:
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 ]
"""
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.
@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_public_books(self, symbol, precision="P0", length=25):
"""
Get the public orderbook for a given symbol.
@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_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.
@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_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.
@parms symbols Array<string>: 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
##################################################
# Authenticated Data #
##################################################
async def get_wallets(self):
"""
Get all wallets on account associated with API_KEY - Requires authentication.
@return Array <models.Wallet>
"""
endpoint = "auth/r/wallets"
raw_wallets = await self.post(endpoint)
return [ Wallet(*rw[:4]) for rw in raw_wallets ]
async def get_active_orders(self, symbol):
"""
Get all of the active orders associated with API_KEY - Requires authentication.
@param symbol string: pair symbol i.e tBTCUSD
@return Array <models.Order>
"""
endpoint = "auth/r/orders/{}".format(symbol)
raw_orders = await self.post(endpoint)
return [ Order.from_raw_order(ro) for ro in raw_orders ]
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.
@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 <models.Order>
"""
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 ]
async def get_active_position(self):
"""
Get all of the active position associated with API_KEY - Requires authentication.
@return Array <models.Position>
"""
endpoint = "auth/r/positions"
raw_positions = await self.post(endpoint)
return [ Position.from_raw_rest_position(rp) for rp in raw_positions ]
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.
@param symbol string: pair symbol i.e tBTCUSD
@param order_id string: id of the order
@return Array <models.Trade>
"""
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_trades(self, symbol, start, end, limit=25):
"""
Get all of the trades 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 <models.Trade>
"""
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_offers(self, symbol):
"""
Get all of the funding offers associated with API_KEY - Requires authentication.
@return Array <models.FundingOffer>
"""
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_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 <models.FundingOffer>
"""
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_funding_loans(self, symbol):
"""
Get all of the funding loans associated with API_KEY - Requires authentication.
@return Array <models.FundingLoan>
"""
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 <models.FundingLoan>
"""
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 <models.FundingCredit>
"""
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)

37
bfxapi/utils/auth.py Normal file
View File

@@ -0,0 +1,37 @@
import hashlib
import hmac
import time
def generate_auth_payload(API_KEY, API_SECRET):
nonce = _gen_nonce()
authMsg, sig = _gen_signature(API_KEY, API_SECRET, nonce)
return {
'apiKey': API_KEY,
'authSig': sig,
'authNonce': nonce,
'authPayload': authMsg,
'event': 'auth'
}
def generate_auth_headers(API_KEY, API_SECRET, path, body):
nonce = str(_gen_nonce())
signature = "/api/v2/{}{}{}".format(path, nonce, body)
h = hmac.new(API_SECRET.encode('utf8'), signature.encode('utf8'), hashlib.sha384)
signature = h.hexdigest()
return {
"bfx-nonce": nonce,
"bfx-apikey": API_KEY,
"bfx-signature": signature
}
def _gen_signature(API_KEY, API_SECRET, nonce):
authMsg = 'AUTH{}'.format(nonce)
secret = API_SECRET.encode('utf8')
sig = hmac.new(secret, authMsg.encode('utf8'), hashlib.sha384).hexdigest()
return authMsg, sig
def _gen_nonce():
return int(round(time.time() * 1000000))

View File

@@ -1,14 +1,13 @@
import asyncio
import json
import time
import hashlib
import hmac
import random
from .GenericWebsocket import GenericWebsocket, AuthError
from .SubscriptionManager import SubscriptionManager
from .WalletManager import WalletManager
from .OrderManager import OrderManager
from ..utils.auth import generate_auth_payload
from ..models import Order, Trade, OrderBook
class Flags:
@@ -56,59 +55,11 @@ def _parse_trade(tData, 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
Translation names:
translation table for channel names:
Data Channels
os - Orders
hos - Historical Orders
ps - Positions
hts - Trades (snapshot)
te - Trade Executed
tu - Trade Execution update
ws - Wallets
bu - Balance Info
miu - Margin Info
fiu - Funding Info
fos - Offers
hfos - Historical Offers
fcs - Credits
hfcs - Historical Credits
fls - Loans
hfls - Historical Loans
htfs - Funding Trades
n - Notifications (WIP)
Events:
- all: listen for all messages coming through
- connected: called when a connection is made
- authenticated: called when the websocket passes authentication
- notification (array): incoming account notification
- error (string): error from the websocket
- order_closed (Order, Trade): when an order has been closed
- order_new (Order, Trade): when an order has been created but not closed. Note: will
not be called if order is executed and filled instantly
- order_confirmed (Order, Trade): when an order has been submitted and received
- wallet_snapshot (string): Initial wallet balances (Fired once)
- order_snapshot (string): Initial open orders (Fired once)
- positions_snapshot (string): Initial open positions (Fired once)
- wallet_update (string): changes to the balance of wallets
- seed_candle (candleArray): initial past candle to prime strategy
- seed_trade (tradeArray): initial past trade to prime strategy
- funding_offer_snapshot:
- funding_loan_snapshot:
- funding_credit_snapshot:
- balance_update when the state of a balance is changed
- new_trade: a new trade on the market has been executed
- new_candle: a new candle has been produced
- margin_info_update: new margin information has been broadcasted
- funding_info_update: new funding information has been broadcasted
'''
"""
ERRORS = {
10000: 'Unknown event',
@@ -142,7 +93,7 @@ class BfxWebsocket(GenericWebsocket):
self.pendingOrders = {}
self.orderBooks = {}
super(BfxWebsocket, self).__init__(host, *args, **kwargs)
super(BfxWebsocket, self).__init__(host, logLevel=logLevel, *args, **kwargs)
self.subscriptionManager = SubscriptionManager(self, logLevel=logLevel)
self.orderManager = OrderManager(self, logLevel=logLevel)
self.wallets = WalletManager()
@@ -392,18 +343,7 @@ class BfxWebsocket(GenericWebsocket):
self.logger.warn('Unknown websocket response: {}'.format(msg))
async def _ws_authenticate_socket(self):
nonce = int(round(time.time() * 1000000))
authMsg = 'AUTH{}'.format(nonce)
secret = self.API_SECRET.encode()
sig = hmac.new(secret, authMsg.encode(), hashlib.sha384).hexdigest()
hmac.new(secret, self.API_SECRET.encode('utf'), hashlib.sha384).hexdigest()
jdata = {
'apiKey': self.API_KEY,
'authSig': sig,
'authNonce': nonce,
'authPayload': authMsg,
'event': 'auth'
}
jdata = generate_auth_payload(self.API_KEY, self.API_SECRET)
await self.ws.send(json.dumps(jdata))
async def on_open(self):
@@ -448,11 +388,11 @@ class BfxWebsocket(GenericWebsocket):
async def update_order(self, *args, **kwargs):
return await self.orderManager.update_order(*args, **kwargs)
async def close_order(self, *args, **kwargs):
return await self.orderManager.close_order(*args, **kwargs)
async def cancel_order(self, *args, **kwargs):
return await self.orderManager.cancel_order(*args, **kwargs)
async def close_all_orders(self, *args, **kwargs):
return await self.orderManager.close_all_orders(*args, **kwargs)
async def cancel_all_orders(self, *args, **kwargs):
return await self.orderManager.cancel_all_orders(*args, **kwargs)
async def close_order_multi(self, *args, **kwargs):
return await self.close_order_multi(*args, **kwargs)
async def cancel_order_multi(self, *args, **kwargs):
return await self.cancel_order_multi(*args, **kwargs)

View File

@@ -2,7 +2,7 @@ import time
import asyncio
from ..utils.CustomLogger import CustomLogger
from ..models import Order, Trade
from ..models import Order
class OrderManager:
@@ -23,20 +23,22 @@ class OrderManager:
def get_pending_orders(self):
return list(self.pending_orders.values())
async def _confirm_order(self, order, trade):
'''
Called once when we first recieve infomation back from the bitfinex api
that the order has been accepted.
'''
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:
if self.pending_callbacks[order.cId][0]:
# call onComplete callback
await self.pending_callbacks[order.cId][0](order, trade)
# call onConfirm callback
await self.pending_callbacks[order.cId][0](order)
if isClosed:
await self.pending_callbacks[order.cId][1](order)
del self.pending_callbacks[order.cId]
order.set_confirmed()
# remove from pending orders list
del self.pending_orders[order.cId]
del self.pending_callbacks[order.cId]
self.bfxapi._emit('order_confirmed', order, trade)
self.bfxapi._emit('order_confirmed', order)
async def confirm_order_closed(self, raw_ws_data):
# order created and executed
@@ -44,14 +46,13 @@ class OrderManager:
# "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(self.bfxapi, raw_ws_data[2])
trade = Trade(order)
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, trade)
await self._confirm_order(order, isClosed=True)
self.logger.info("Order closed: {} {}".format(order.symbol, order.status))
self.bfxapi._emit('order_closed', order, trade)
self.bfxapi._emit('order_closed', order)
async def build_from_order_snapshot(self, raw_ws_data):
'''
@@ -60,11 +61,9 @@ class OrderManager:
osData = raw_ws_data[2]
self.open_orders = {}
for raw_order in osData:
order = Order(self.bfxapi, raw_order)
trade = Trade(order)
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, trade)
self.bfxapi._emit('order_snapshot', self.get_open_orders())
async def confirm_order_update(self, raw_ws_data):
@@ -73,13 +72,12 @@ class OrderManager:
# 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(self.bfxapi, raw_ws_data[2])
order = Order.from_raw_order(raw_ws_data[2])
order.set_open_state(True)
trade = Trade(order)
self.open_orders[order.id] = order
await self._confirm_order(order, trade)
self.logger.info("Order update: {} {}".format(order, trade))
self.bfxapi._emit('order_update', order, trade)
await self._confirm_order(order)
self.logger.info("Order update: {}".format(order))
self.bfxapi._emit('order_update', order)
async def confirm_order_new(self, raw_ws_data):
# order created but not executed / created but partially filled
@@ -87,58 +85,160 @@ class OrderManager:
# 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(self.bfxapi, raw_ws_data[2])
order = Order.from_raw_order(raw_ws_data[2])
order.set_open_state(True)
trade = Trade(order)
self.open_orders[order.id] = order
await self._confirm_order(order, trade)
self.logger.info("Order new: {} {}".format(order, trade))
self.bfxapi._emit('order_new', order, trade)
await self._confirm_order(order)
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,
hidden=False, onComplete=None, onError=None, *args, **kwargs):
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 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
@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()
# send order over websocket
# create base payload with required data
payload = {
"cid": cId,
"type": str(market_type),
"symbol": symbol,
"amount": str(amount),
"price": str(price)
"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.pending_callbacks[cId] = (onComplete, onError)
self.pending_callbacks[cId] = (onConfirm, onClose)
await self.bfxapi._send_auth_command('on', payload)
self.logger.info("Order cid={} ({} {} @ {}) dispatched".format(
cId, symbol, amount, price))
async def update_order(self, orderId, *args, onComplete=None, onError=None, **kwargs):
if orderId not in self.open_orders:
raise Exception("Order id={} is not open".format(orderId))
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
@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.pending_callbacks[order.cId] = (onComplete, onError)
await order.update(*args, **kwargs)
self.pending_callbacks[order.cId] = (onConfirm, 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))
async def close_order(self, orderId, onComplete=None, onError=None):
if orderId not in self.open_orders:
raise Exception("Order id={} is not open".format(orderId))
async def cancel_order(self, orderId, onConfirm=None, onClose=None):
"""
Cancel an existing open 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.pending_callbacks[order.cId] = (onComplete, onError)
await order.cancel()
self.pending_callbacks[order.cId] = (onConfirm, onClose)
await self.bfxapi._send_auth_command('oc', { 'id': orderId })
self.logger.info("Order cancel order_id={} dispatched".format(orderId))
async def close_all_orders(self):
ids = [self.open_orders[x].id for x in self.open_orders]
await self.close_order_multi(ids)
async def cancel_all_orders(self):
"""
Cancel all existing open orders
async def close_order_multi(self, orderIds):
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 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 _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

View File

@@ -16,6 +16,14 @@ class SubscriptionManager:
self.logger = CustomLogger('BfxSubscriptionManager', logLevel=logLevel)
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))
@@ -61,6 +69,12 @@ class SubscriptionManager:
return self.subscriptions_chanid[chanId]
async def unsubscribe(self, chanId, 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
@@ -68,6 +82,11 @@ class SubscriptionManager:
await self.subscriptions_chanid[chanId].unsubscribe()
async def resubscribe(self, chanId):
"""
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()
@@ -79,11 +98,17 @@ class SubscriptionManager:
await sub.subscribe()
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 unsubscribe_all(self):
"""
Unsubscribe from all channels.
"""
task_batch = []
for chanId in self.subscriptions_chanid:
sub = self.get(chanId)
@@ -94,6 +119,9 @@ class SubscriptionManager:
await asyncio.wait(*[ task_batch ])
async def resubscribe_all(self):
"""
Unsubscribe and then subscribe to all channels
"""
task_batch = []
for chanId in self.subscriptions_chanid:
task_batch += [