This commit is contained in:
matthewli1409
2022-10-18 16:08:48 +07:00
45 changed files with 1232 additions and 64 deletions

36
.github/workflows/python-app.yml vendored Normal file
View File

@@ -0,0 +1,36 @@
# This workflow will install Python dependencies, run tests and lint with a single version of Python
# For more information see: https://help.github.com/actions/language-and-framework-guides/using-python-with-github-actions
name: Python application
on:
push:
branches: [ master ]
pull_request:
branches: [ master ]
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v2
- name: Set up Python 3.9
uses: actions/setup-python@v2
with:
python-version: 3.9
- name: Install dependencies
run: |
python -m pip install --upgrade pip
pip install flake8 pytest
if [ -f requirements.txt ]; then pip install -r requirements.txt; fi
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
pytest

View File

@@ -1,3 +1,71 @@
2.0.3
-) Implemented Liquidations endpoint (REST)
2.0.2
-) Use private host for auth-based requests
2.0.1
-) Added User Settings Write/Read/Delete endpoints (REST)
-) Added Balance Available for Orders/Offers endpoint (REST)
-) Added Alerts endpoints (REST)
-) Fixed trades handling error
2.0.0
-) Implemented Movement endpoints (REST)
-) Fixed unawaited stop
-) Changed account's trade execution (te) and trade update (tu) handling
1.3.4
-) Fixed undefined p_sub issue in subscription_manager.py
-) Added submit cancel all funding orders endpoint (REST)
-) Added get all exchange pairs endpoint (REST)
1.3.3
-) Fixed socket.send() issue (IndexError: deque index out of range)
1.3.2
-) Implemented Merchants endpoints (REST)
1.3.1
-) Handle exception of asyncio.get_event_loop() | Related to v1.2.8
1.3.0
-) Adjusted get_trades() to allow symbol to be None and get trades for all symbols
1.2.8
-) Bugfix - It is possible to call bfx.ws.run() from an already running event loop
1.2.7
-) Added ws support for Python 3.9 and 3.10
1.2.6
-) Updated websockets to 9.1
1.2.5
-) Adjusted get_order_history() rest endpoint
1.2.4
-) Added example of MARKET order with price=None
1.2.3
-) Tests adjusted
1.2.2
-) WS bugfix (exception InvalidStatusCode not handled)
1.2.1
-) Added orderbook implementation example (ws)
1.2.0
-) Implemented Margin Info (rest)
-) Implemented claim position (rest)
-) When max_retries == 0 continue forever to retry (websocket)
1.1.15
-) Added 'ids' parameter to get_order_history()
-) Added an example to show how it is possible to spawn multiple bfx ws instances to comply with the open subscriptions number constraint (max. 25)
-) Implemented Funding Trades (rest)
1.1.14 1.1.14
-) bfx_websockets.py ERRORS dictionary now contains a message for error number 10305 -) bfx_websockets.py ERRORS dictionary now contains a message for error number 10305

View File

@@ -3,9 +3,10 @@ This module is used to interact with the bitfinex api
""" """
from .version import __version__ from .version import __version__
from .client import Client from .client import Client, PUB_REST_HOST, PUB_WS_HOST, REST_HOST, WS_HOST
from .models import (Order, Trade, OrderBook, Subscription, Wallet, from .models import (Order, Trade, OrderBook, Subscription, Wallet,
Position, FundingLoan, FundingOffer, FundingCredit) Position, FundingLoan, FundingOffer, FundingCredit,
Movement)
from .websockets.generic_websocket import GenericWebsocket, Socket from .websockets.generic_websocket import GenericWebsocket, Socket
from .websockets.bfx_websocket import BfxWebsocket from .websockets.bfx_websocket import BfxWebsocket
from .utils.decimal import Decimal from .utils.decimal import Decimal

View File

@@ -5,13 +5,9 @@ a websocket client and a rest interface client
# pylint: disable-all # pylint: disable-all
import asyncio
from .websockets.bfx_websocket import BfxWebsocket from .websockets.bfx_websocket import BfxWebsocket
from .rest.bfx_rest import BfxRest from .rest.bfx_rest import BfxRest
from .constants import *
REST_HOST = 'https://api-pub.bitfinex.com/v2'
WS_HOST = 'wss://api-pub.bitfinex.com/ws/2'
class Client: class Client:
""" """

4
bfxapi/constants.py Normal file
View File

@@ -0,0 +1,4 @@
REST_HOST = 'https://api.bitfinex.com/v2'
WS_HOST = 'wss://api.bitfinex.com/ws/2'
PUB_REST_HOST = 'https://api-pub.bitfinex.com/v2'
PUB_WS_HOST = 'wss://api-pub.bitfinex.com/ws/2'

View File

@@ -1,18 +1,21 @@
import os import os
import sys import sys
import asyncio import asyncio
import time
sys.path.append('../../../') sys.path.append('../../../')
from bfxapi import Client from bfxapi import Client
from bfxapi.constants import WS_HOST, REST_HOST
API_KEY=os.getenv("BFX_KEY") API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET") API_SECRET=os.getenv("BFX_SECRET")
# Create funding requires private hosts
bfx = Client( bfx = Client(
API_KEY=API_KEY, API_KEY=API_KEY,
API_SECRET=API_SECRET, API_SECRET=API_SECRET,
logLevel='DEBUG' logLevel='DEBUG',
ws_host=WS_HOST,
rest_host=REST_HOST
) )
async def create_funding(): async def create_funding():

View File

@@ -4,18 +4,23 @@ import asyncio
import time import time
sys.path.append('../../../') sys.path.append('../../../')
from bfxapi import Client from bfxapi import Client
from bfxapi.constants import WS_HOST, REST_HOST
from bfxapi.models import OrderType
API_KEY=os.getenv("BFX_KEY") API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET") API_SECRET=os.getenv("BFX_SECRET")
# Create order requires private hosts
bfx = Client( bfx = Client(
API_KEY=API_KEY, API_KEY=API_KEY,
API_SECRET=API_SECRET, API_SECRET=API_SECRET,
logLevel='DEBUG' logLevel='DEBUG',
ws_host=WS_HOST,
rest_host=REST_HOST
) )
async def create_order(): async def create_order():
response = await bfx.rest.submit_order("tBTCUSD", 10, 0.1) response = await bfx.rest.submit_order(symbol="tBTCUSD", amount=10, price=None, market_type=OrderType.MARKET)
# response is in the form of a Notification object # response is in the form of a Notification object
for o in response.notify_info: for o in response.notify_info:
# each item is in the form of an Order object # each item is in the form of an Order object

View File

@@ -5,14 +5,18 @@ import time
sys.path.append('../../../') sys.path.append('../../../')
from bfxapi import Client from bfxapi import Client
from bfxapi.constants import WS_HOST, REST_HOST
API_KEY=os.getenv("BFX_KEY") API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET") API_SECRET=os.getenv("BFX_SECRET")
# Retrieving authenticated data requires private hosts
bfx = Client( bfx = Client(
API_KEY=API_KEY, API_KEY=API_KEY,
API_SECRET=API_SECRET, API_SECRET=API_SECRET,
logLevel='DEBUG' logLevel='DEBUG',
ws_host=WS_HOST,
rest_host=REST_HOST
) )
now = int(round(time.time() * 1000)) now = int(round(time.time() * 1000))
@@ -39,7 +43,7 @@ async def log_active_positions():
[ print (p) for p in positions ] [ print (p) for p in positions ]
async def log_trades(): async def log_trades():
trades = await bfx.rest.get_trades('tBTCUSD', 0, then) trades = await bfx.rest.get_trades(symbol='tBTCUSD', start=0, end=then)
print ("Trades:") print ("Trades:")
[ print (t) for t in trades] [ print (t) for t in trades]
@@ -79,6 +83,15 @@ async def log_funding_credits_history():
print ("Funding credit history:") print ("Funding credit history:")
[ print (c) for c in credit ] [ print (c) for c in credit ]
async def log_margin_info():
margin_info = await bfx.rest.get_margin_info('tBTCUSD')
print(margin_info)
sym_all = await bfx.rest.get_margin_info('sym_all') # list of Margin Info
for margin_info in sym_all:
print(margin_info)
base = await bfx.rest.get_margin_info('base')
print(base)
async def run(): async def run():
await log_wallets() await log_wallets()
await log_active_orders() await log_active_orders()
@@ -90,6 +103,7 @@ async def run():
await log_funding_offer_history() await log_funding_offer_history()
await log_funding_credits() await log_funding_credits()
await log_funding_credits_history() await log_funding_credits_history()
await log_margin_info()
t = asyncio.ensure_future(run()) t = asyncio.ensure_future(run())

View File

@@ -0,0 +1,21 @@
import os
import sys
import asyncio
import time
sys.path.append('../../../')
from bfxapi import Client, PUB_REST_HOST
bfx = Client(
logLevel='INFO',
rest_host=PUB_REST_HOST
)
now = int(round(time.time() * 1000))
then = now - (1000 * 60 * 60 * 24 * 10) # 10 days ago
async def get_liquidations():
liquidations = await bfx.rest.get_liquidations(start=then, end=now)
print(liquidations)
asyncio.get_event_loop().run_until_complete(get_liquidations())

View File

@@ -5,9 +5,13 @@ import time
sys.path.append('../../../') sys.path.append('../../../')
from bfxapi import Client from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
# Retrieving public data requires public hosts
bfx = Client( bfx = Client(
logLevel='DEBUG', logLevel='DEBUG',
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST
) )
now = int(round(time.time() * 1000)) now = int(round(time.time() * 1000))

View File

@@ -4,9 +4,13 @@ import asyncio
sys.path.append('../../../') sys.path.append('../../../')
from bfxapi import Client from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
# Retrieving seed trades requires public hosts
bfx = Client( bfx = Client(
logLevel='INFO' logLevel='INFO',
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST
) )
async def get_seeds(): async def get_seeds():

View File

@@ -0,0 +1,37 @@
import os
import sys
import asyncio
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import WS_HOST, REST_HOST
API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET")
# Submitting invoices requires private hosts
bfx = Client(
API_KEY=API_KEY,
API_SECRET=API_SECRET,
logLevel='DEBUG',
ws_host=WS_HOST,
rest_host=REST_HOST
)
async def run():
await bfx.rest.submit_invoice(amount='2.0', currency='USD', pay_currencies=['BTC', 'ETH'], order_id='order123', webhook='https://example.com/api/v3/order/order123',
redirect_url='https://example.com/api/v3/order/order123', customer_info_nationality='DE',
customer_info_resid_country='GB', customer_info_resid_city='London', customer_info_resid_zip_code='WC2H 7NA',
customer_info_resid_street='5-6 Leicester Square', customer_info_resid_building_no='23 A',
customer_info_full_name='John Doe', customer_info_email='john@example.com', duration=86339)
invoices = await bfx.rest.get_invoices()
print(invoices)
# await bfx.rest.complete_invoice(id=invoices[0]['id'], pay_ccy='BTC', deposit_id=1357996)
unlinked_deposits = await bfx.rest.get_unlinked_deposits(ccy='BTC')
print(unlinked_deposits)
t = asyncio.ensure_future(run())
asyncio.get_event_loop().run_until_complete(t)

View File

@@ -1,18 +1,21 @@
import os import os
import sys import sys
import asyncio import asyncio
import time
sys.path.append('../../../') sys.path.append('../../../')
from bfxapi import Client from bfxapi import Client
from bfxapi.constants import WS_HOST, REST_HOST
API_KEY=os.getenv("BFX_KEY") API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET") API_SECRET=os.getenv("BFX_SECRET")
# Transfer wallet requires private hosts
bfx = Client( bfx = Client(
API_KEY=API_KEY, API_KEY=API_KEY,
API_SECRET=API_SECRET, API_SECRET=API_SECRET,
logLevel='DEBUG' logLevel='DEBUG',
ws_host=WS_HOST,
rest_host=REST_HOST
) )
async def transfer_wallet(): async def transfer_wallet():

View File

@@ -3,14 +3,18 @@ import sys
sys.path.append('../../../') sys.path.append('../../../')
from bfxapi import Client, Order from bfxapi import Client, Order
from bfxapi.constants import WS_HOST, REST_HOST
API_KEY=os.getenv("BFX_KEY") API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET") API_SECRET=os.getenv("BFX_SECRET")
# Canceling orders requires private hosts
bfx = Client( bfx = Client(
API_KEY=API_KEY, API_KEY=API_KEY,
API_SECRET=API_SECRET, API_SECRET=API_SECRET,
logLevel='DEBUG' logLevel='DEBUG',
ws_host=WS_HOST,
rest_host=REST_HOST
) )
@bfx.ws.on('order_closed') @bfx.ws.on('order_closed')

View File

@@ -3,9 +3,12 @@ import sys
sys.path.append('../../../') sys.path.append('../../../')
from bfxapi import Client from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
bfx = Client( bfx = Client(
logLevel='DEBUG' logLevel='DEBUG',
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST
) )
@bfx.ws.on('error') @bfx.ws.on('error')

View File

@@ -2,7 +2,8 @@ import os
import sys import sys
sys.path.append('../../../') sys.path.append('../../../')
from bfxapi import Client, Order from bfxapi import Client
from bfxapi.constants import WS_HOST, REST_HOST
API_KEY=os.getenv("BFX_KEY") API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET") API_SECRET=os.getenv("BFX_SECRET")
@@ -11,6 +12,8 @@ bfx = Client(
API_KEY=API_KEY, API_KEY=API_KEY,
API_SECRET=API_SECRET, API_SECRET=API_SECRET,
logLevel='DEBUG', logLevel='DEBUG',
ws_host=WS_HOST,
rest_host=REST_HOST,
dead_man_switch=True, # <-- kill all orders if this connection drops dead_man_switch=True, # <-- kill all orders if this connection drops
channel_filter=['wallet'] # <-- only receive wallet updates channel_filter=['wallet'] # <-- only receive wallet updates
) )

View File

@@ -0,0 +1,83 @@
import sys
import time
from collections import OrderedDict
sys.path.append('../../../')
from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
# Retrieving orderbook requires public hosts
bfx = Client(
manageOrderBooks=True,
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST
)
class OrderBook:
def __init__(self, snapshot):
self.bids = OrderedDict()
self.asks = OrderedDict()
self.load(snapshot)
def load(self, snapshot):
for record in snapshot:
if record[2] >= 0:
self.bids[record[0]] = {
'count': record[1],
'amount': record[2]
}
else:
self.asks[record[0]] = {
'count': record[1],
'amount': record[2]
}
def update(self, record):
# count is 0
if record[1] == 0:
if record[2] == 1:
# remove from bids
del self.bids[record[0]]
elif record[2] == -1:
# remove from asks
del self.asks[record[0]]
elif record[1] > 0:
if record[2] > 0:
# update bids
if record[0] not in self.bids:
self.bids[record[0]] = {}
self.bids[record[0]]['count'] = record[1]
self.bids[record[0]]['amount'] = record[2]
elif record[2] < 0:
# update asks
if record[0] not in self.asks:
self.asks[record[0]] = {}
self.asks[record[0]]['count'] = record[1]
self.asks[record[0]]['amount'] = record[2]
obs = {}
@bfx.ws.on('error')
def log_error(err):
print ("Error: {}".format(err))
@bfx.ws.on('order_book_update')
def log_update(data):
obs[data['symbol']].update(data['data'])
@bfx.ws.on('order_book_snapshot')
def log_snapshot(data):
obs[data['symbol']] = OrderBook(data['data'])
async def start():
await bfx.ws.subscribe('book', 'tBTCUSD')
bfx.ws.on('connected', start)
bfx.ws.run()
for n in range(0, 10):
time.sleep(2)
for key in obs:
print(f"Printing {key} orderbook...")
print(f"{obs[key].bids}\n")
print(f"{obs[key].asks}\n")

View File

@@ -0,0 +1,180 @@
"""
This is an example of how it is possible to spawn multiple
bfx ws instances to comply with the open subscriptions number constraint (max. 25)
(https://docs.bitfinex.com/docs/requirements-and-limitations)
"""
import sys
sys.path.append('../../../')
import asyncio
from functools import partial
import websockets as ws
from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
import math
import random
MAX_CHANNELS = 25
def get_random_list_of_tickers():
tickers = ["FILUST", "FTTUSD", "FTTUST", "FUNUSD", "GNOUSD", "GNTUSD", "GOTEUR", "GOTUSD", "GTXUSD", "ZRXUSD"]
return random.sample(tickers, 1)
class Instance:
def __init__(self, _id):
self.id = _id
self.bfx = Client(logLevel='INFO', ws_host=PUB_WS_HOST, rest_host=PUB_REST_HOST)
self.subscriptions = {'trades': {}, 'ticker': {}}
self.is_ready = False
def run(self):
self.bfx.ws.run()
self.bfx.ws.on('error', log_error)
self.bfx.ws.on('new_trade', log_trade)
self.bfx.ws.on('new_ticker', log_ticker)
self.bfx.ws.on('subscribed', partial(on_subscribe, self))
self.bfx.ws.on('unsubscribed', partial(on_unsubscribed, self))
self.bfx.ws.on('connected', partial(on_connected, self))
self.bfx.ws.on('stopped', partial(on_stopped, self))
async def subscribe(self, symbols):
for symbol in symbols:
print(f'Subscribing to {symbol} channel')
await self.bfx.ws.subscribe_ticker(symbol)
await self.bfx.ws.subscribe_trades(symbol)
self.subscriptions['trades'][symbol] = None
self.subscriptions['ticker'][symbol] = None
async def unsubscribe(self, symbols):
for symbol in symbols:
if symbol in self.subscriptions['trades']:
print(f'Unsubscribing to {symbol} channel')
trades_ch_id = self.subscriptions['trades'][symbol]
ticker_ch_id = self.subscriptions['ticker'][symbol]
if trades_ch_id:
await self.bfx.ws.unsubscribe(trades_ch_id)
else:
del self.subscriptions['trades'][symbol]
if ticker_ch_id:
await self.bfx.ws.unsubscribe(ticker_ch_id)
else:
del self.subscriptions['ticker'][symbol]
class Routine:
is_stopped = False
def __new__(cls, _loop, _ws, interval=1, start_delay=10):
instance = super().__new__(cls)
instance.interval = interval
instance.start_delay = start_delay
instance.ws = _ws
instance.task = _loop.create_task(instance.run())
return instance.task
async def run(self):
await asyncio.sleep(self.start_delay)
await self.do()
while True:
await asyncio.sleep(self.interval)
await self.do()
async def do(self):
subbed_tickers = get_all_subscriptions_tickers()
print(f'Subscribed tickers: {subbed_tickers}')
# if ticker is not in subbed tickers, then we subscribe to the channel
to_sub = [f"t{ticker}" for ticker in get_random_list_of_tickers() if f"t{ticker}" not in subbed_tickers]
for ticker in to_sub:
print(f'To subscribe: {ticker}')
instance = get_available_instance()
if instance and instance.is_ready:
print(f'Subscribing on instance {instance.id}')
await instance.subscribe([ticker])
else:
instances_to_create = math.ceil(len(to_sub) / MAX_CHANNELS)
create_instances(instances_to_create)
break
to_unsub = [f"t{ticker}" for ticker in subbed_tickers if f"t{ticker}" in get_random_list_of_tickers()]
if len(to_unsub) > 0:
print(f'To unsubscribe: {to_unsub}')
for instance in instances:
await instance.unsubscribe(to_unsub)
def stop(self):
self.task.cancel()
self.is_stopped = True
instances = []
def get_all_subscriptions_tickers():
tickers = []
for instance in instances:
for ticker in instance.subscriptions['trades']:
tickers.append(ticker)
return tickers
def count_open_channels(instance):
return len(instance.subscriptions['trades']) + len(instance.subscriptions['ticker'])
def create_instances(instances_to_create):
for _ in range(0, instances_to_create):
instance = Instance(len(instances))
instance.run()
instances.append(instance)
def get_available_instance():
for instance in instances:
if count_open_channels(instance) + 1 <= MAX_CHANNELS:
return instance
return None
def log_error(err):
print("Error: {}".format(err))
def log_trade(trade):
print(trade)
def log_ticker(ticker):
print(ticker)
async def on_subscribe(instance, subscription):
print(f'Subscribed to {subscription.symbol} channel {subscription.channel_name}')
instance.subscriptions[subscription.channel_name][subscription.symbol] = subscription.chan_id
async def on_unsubscribed(instance, subscription):
print(f'Unsubscribed to {subscription.symbol} channel {subscription.channel_name}')
instance.subscriptions[subscription.channel_name][subscription.symbol] = subscription.chan_id
del instance.subscriptions[subscription.channel_name][subscription.symbol]
async def on_connected(instance):
print(f"Instance {instance.id} is connected")
instance.is_ready = True
async def on_stopped(instance):
print(f"Instance {instance.id} is dead, removing it from instances list")
instances.pop(instance.id)
def run():
loop = asyncio.get_event_loop()
task = Routine(loop, ws, interval=5)
loop.run_until_complete(task)
run()

View File

@@ -1,11 +1,14 @@
import os
import sys import sys
sys.path.append('../../../') sys.path.append('../../../')
from bfxapi import Client from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
# Retrieving orderbook requires public hosts
bfx = Client( bfx = Client(
logLevel='INFO' manageOrderBooks=True,
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST
) )
@bfx.ws.on('error') @bfx.ws.on('error')

View File

@@ -3,14 +3,18 @@ import sys
sys.path.append('../../../') sys.path.append('../../../')
from bfxapi import Client, Order from bfxapi import Client, Order
from bfxapi.constants import WS_HOST, REST_HOST
API_KEY=os.getenv("BFX_KEY") API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET") API_SECRET=os.getenv("BFX_SECRET")
# Sending order requires private hosts
bfx = Client( bfx = Client(
API_KEY=API_KEY, API_KEY=API_KEY,
API_SECRET=API_SECRET, API_SECRET=API_SECRET,
logLevel='DEBUG' logLevel='DEBUG',
ws_host=WS_HOST,
rest_host=REST_HOST
) )
@bfx.ws.on('order_snapshot') @bfx.ws.on('order_snapshot')
@@ -34,7 +38,7 @@ def log_error(msg):
@bfx.ws.on('authenticated') @bfx.ws.on('authenticated')
async def submit_order(auth_message): async def submit_order(auth_message):
await bfx.ws.submit_order('tBTCUSD', 19000, 0.01, Order.Type.EXCHANGE_MARKET) await bfx.ws.submit_order(symbol='tBTCUSD', price=None, amount=0.01, market_type=Order.Type.EXCHANGE_MARKET)
# If you dont want to use a decorator # If you dont want to use a decorator
# ws.on('authenticated', submit_order) # ws.on('authenticated', submit_order)

View File

@@ -1,11 +1,13 @@
import os
import sys import sys
sys.path.append('../../../') sys.path.append('../../../')
from bfxapi import Client from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
bfx = Client( bfx = Client(
logLevel='DEBUG', logLevel='DEBUG',
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST
) )
@bfx.ws.on('order_book_snapshot') @bfx.ws.on('order_book_snapshot')

View File

@@ -1,11 +1,14 @@
import os
import sys import sys
sys.path.append('../../../') sys.path.append('../../../')
from bfxapi import Client from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
# Retrieving derivative status requires public hosts
bfx = Client( bfx = Client(
logLevel='INFO' logLevel='DEBUG',
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST
) )
@bfx.ws.on('error') @bfx.ws.on('error')

View File

@@ -3,9 +3,13 @@ import sys
sys.path.append('../../../') sys.path.append('../../../')
from bfxapi import Client from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
# Retrieving trades/candles requires public hosts
bfx = Client( bfx = Client(
logLevel='DEBUG', logLevel='DEBUG',
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST,
# Verifies that the local orderbook is up to date # Verifies that the local orderbook is up to date
# with the bitfinex servers # with the bitfinex servers
manageOrderBooks=True manageOrderBooks=True

View File

@@ -1,11 +1,14 @@
import os
import sys import sys
sys.path.append('../../../') sys.path.append('../../../')
from bfxapi import Client from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
# Retrieving tickers requires public hosts
bfx = Client( bfx = Client(
logLevel='DEBUG' logLevel='DEBUG',
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST
) )
@bfx.ws.on('error') @bfx.ws.on('error')

View File

@@ -1,11 +1,14 @@
import os
import sys import sys
sys.path.append('../../../') sys.path.append('../../../')
from bfxapi import Client from bfxapi import Client
from bfxapi.constants import PUB_WS_HOST, PUB_REST_HOST
# Retrieving trades/candles requires public hosts
bfx = Client( bfx = Client(
logLevel='DEBUG' logLevel='DEBUG',
ws_host=PUB_WS_HOST,
rest_host=PUB_REST_HOST
) )
@bfx.ws.on('error') @bfx.ws.on('error')
@@ -20,6 +23,10 @@ def log_candle(candle):
def log_trade(trade): def log_trade(trade):
print ("New trade: {}".format(trade)) print ("New trade: {}".format(trade))
@bfx.ws.on('new_user_trade')
def log_user_trade(trade):
print ("New user trade: {}".format(trade))
async def start(): async def start():
await bfx.ws.subscribe('candles', 'tBTCUSD', timeframe='1m') await bfx.ws.subscribe('candles', 'tBTCUSD', timeframe='1m')
await bfx.ws.subscribe('trades', 'tBTCUSD') await bfx.ws.subscribe('trades', 'tBTCUSD')

View File

@@ -3,14 +3,18 @@ import sys
sys.path.append('../../../') sys.path.append('../../../')
from bfxapi import Client, Order from bfxapi import Client, Order
from bfxapi.constants import WS_HOST, REST_HOST
API_KEY=os.getenv("BFX_KEY") API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET") API_SECRET=os.getenv("BFX_SECRET")
# Update order requires private hosts
bfx = Client( bfx = Client(
API_KEY=API_KEY, API_KEY=API_KEY,
API_SECRET=API_SECRET, API_SECRET=API_SECRET,
logLevel='DEBUG' logLevel='INFO',
ws_host=WS_HOST,
rest_host=REST_HOST
) )
@bfx.ws.on('order_update') @bfx.ws.on('order_update')

View File

@@ -2,14 +2,18 @@ import os
import sys import sys
sys.path.append('../../../') sys.path.append('../../../')
from bfxapi import Client from bfxapi import Client
from bfxapi.constants import WS_HOST, REST_HOST
API_KEY=os.getenv("BFX_KEY") API_KEY=os.getenv("BFX_KEY")
API_SECRET=os.getenv("BFX_SECRET") API_SECRET=os.getenv("BFX_SECRET")
# Checking wallet balances requires private hosts
bfx = Client( bfx = Client(
API_KEY=API_KEY, API_KEY=API_KEY,
API_SECRET=API_SECRET, API_SECRET=API_SECRET,
logLevel='INFO' logLevel='INFO',
ws_host=WS_HOST,
rest_host=REST_HOST
) )
@bfx.ws.on('wallet_snapshot') @bfx.ws.on('wallet_snapshot')

View File

@@ -19,5 +19,9 @@ from .withdraw import Withdraw
from .ticker import Ticker from .ticker import Ticker
from .funding_ticker import FundingTicker from .funding_ticker import FundingTicker
from .ledger import Ledger from .ledger import Ledger
from .funding_trade import FundingTrade
from .margin_info import MarginInfo
from .margin_info_base import MarginInfoBase
from .movement import Movement
NAME = 'models' NAME = "models"

View File

@@ -0,0 +1,55 @@
"""
Module used to describe all of the different data types
"""
class FundingTradeModel:
"""
Enum used to index the different values in a raw funding trade array
"""
ID = 0
SYMBOL = 1
MTS_CREATE = 2
OFFER_ID = 3
AMOUNT = 4
RATE = 5
PERIOD = 6
class FundingTrade:
"""
ID integer Offer ID
SYMBOL string The currency of the offer (fUSD, etc)
MTS_CREATE int Millisecond Time Stamp when the offer was created
OFFER_ID int The ID of the offer
AMOUNT float Amount the offer is for
RATE float Rate of the offer
PERIOD int Period of the offer
"""
def __init__(self, tid, symbol, mts_create, offer_id, amount, rate, period):
self.tid = tid
self.symbol = symbol
self.mts_create = mts_create
self.offer_id = offer_id
self.amount = amount
self.rate = rate
self.period = period
@staticmethod
def from_raw_rest_trade(raw_trade):
"""
Generate a Ticker object from a raw ticker array
"""
# [[636040,"fUST",1574077528000,41237922,-100,0.0024,2,null]]
return FundingTrade(
raw_trade[FundingTradeModel.ID],
raw_trade[FundingTradeModel.SYMBOL],
raw_trade[FundingTradeModel.MTS_CREATE],
raw_trade[FundingTradeModel.OFFER_ID],
raw_trade[FundingTradeModel.AMOUNT],
raw_trade[FundingTradeModel.RATE],
raw_trade[FundingTradeModel.PERIOD]
)
def __str__(self):
return "FundingTrade '{}' x {} @ {} for {} days".format(
self.symbol, self.amount, self.rate, self.period)

View File

@@ -0,0 +1,47 @@
"""
Module used to describe all of the different data types
"""
import datetime
class MarginInfoModel:
"""
Enum used to index the different values in a raw margin info array
"""
TRADABLE_BALANCE = 0
GROSS_BALANCE = 1
BUY = 2
SELL = 3
class MarginInfo:
"""
SYMBOL string
TRADABLE BALANCE float
GROSS_BALANCE float
BUY
SELL
"""
def __init__(self, symbol, tradable_balance, gross_balance, buy, sell):
# pylint: disable=invalid-name
self.symbol = symbol
self.tradable_balance = tradable_balance
self.gross_balance = gross_balance
self.buy = buy
self.sell = sell
@staticmethod
def from_raw_margin_info(raw_margin_info):
"""
Generate a MarginInfo object from a raw margin info array
"""
symbol = raw_margin_info[1]
tradable_balance = raw_margin_info[2][MarginInfoModel.TRADABLE_BALANCE]
gross_balance = raw_margin_info[2][MarginInfoModel.GROSS_BALANCE]
buy = raw_margin_info[2][MarginInfoModel.BUY]
sell = raw_margin_info[2][MarginInfoModel.SELL]
return MarginInfo(symbol, tradable_balance, gross_balance, buy, sell)
def __str__(self):
return "Margin Info {} buy={} sell={} tradable_balance={} gross_balance={}" \
"".format(self.symbol, self.buy, self.sell, self. tradable_balance, self. gross_balance)

View File

@@ -0,0 +1,48 @@
"""
Module used to describe all of the different data types
"""
import datetime
class MarginInfoBaseModel:
"""
Enum used to index the different values in a raw margin info array
"""
USER_PL = 0
USER_SWAPS = 1
MARGIN_BALANCE = 2
MARGIN_NET = 3
MARGIN_MIN = 4
class MarginInfoBase:
"""
USER_PL float
USER_SWAPS float
MARGIN_BALANCE float
MARGIN_NET float
MARGIN_MIN float
"""
def __init__(self, user_pl, user_swaps, margin_balance, margin_net, margin_min):
# pylint: disable=invalid-name
self.user_pl = user_pl
self.user_swaps = user_swaps
self.margin_balance = margin_balance
self.margin_net = margin_net
self.margin_min = margin_min
@staticmethod
def from_raw_margin_info(raw_margin_info):
"""
Generate a MarginInfoBase object from a raw margin info array
"""
user_pl = raw_margin_info[1][MarginInfoBaseModel.USER_PL]
user_swaps = raw_margin_info[1][MarginInfoBaseModel.USER_SWAPS]
margin_balance = raw_margin_info[1][MarginInfoBaseModel.MARGIN_BALANCE]
margin_net = raw_margin_info[1][MarginInfoBaseModel.MARGIN_NET]
margin_min = raw_margin_info[1][MarginInfoBaseModel.MARGIN_MIN]
return MarginInfoBase(user_pl, user_swaps, margin_balance, margin_net, margin_min)
def __str__(self):
return "Margin Info Base user_pl={} user_swaps={} margin_balance={} margin_net={} margin_min={}" \
"".format(self.user_pl, self.user_swaps, self.margin_balance, self.margin_net, self.margin_min)

76
bfxapi/models/movement.py Normal file
View File

@@ -0,0 +1,76 @@
"""
Module used to describe movement data types
"""
import time
import datetime
class MovementModel:
"""
Enum used index the different values in a raw movement array
"""
ID = 0
CURRENCY = 1
CURRENCY_NAME = 2
MTS_STARTED = 5
MTS_UPDATED = 6
STATUS = 9
AMOUNT = 12
FEES = 13
DESTINATION_ADDRESS = 16
TRANSACTION_ID = 20
class Movement:
"""
ID String Movement identifier
CURRENCY String The symbol of the currency (ex. "BTC")
CURRENCY_NAME String The extended name of the currency (ex. "BITCOIN")
MTS_STARTED Date Movement started at
MTS_UPDATED Date Movement last updated at
STATUS String Current status
AMOUNT String Amount of funds moved
FEES String Tx Fees applied
DESTINATION_ADDRESS String Destination address
TRANSACTION_ID String Transaction identifier
"""
def __init__(self, mid, currency, mts_started, mts_updated, status, amount, fees, dst_address, tx_id):
self.id = mid
self.currency = currency
self.mts_started = mts_started
self.mts_updated = mts_updated
self.status = status
self.amount = amount
self.fees = fees
self.dst_address = dst_address
self.tx_id = tx_id
self.date = datetime.datetime.fromtimestamp(mts_started/1000.0)
@staticmethod
def from_raw_movement(raw_movement):
"""
Parse a raw movement object into a Movement object
@return Movement
"""
mid = raw_movement[MovementModel.ID]
currency = raw_movement[MovementModel.CURRENCY]
mts_started = raw_movement[MovementModel.MTS_STARTED]
mts_updated = raw_movement[MovementModel.MTS_UPDATED]
status = raw_movement[MovementModel.STATUS]
amount = raw_movement[MovementModel.AMOUNT]
fees = raw_movement[MovementModel.FEES]
dst_address = raw_movement[MovementModel.DESTINATION_ADDRESS]
tx_id = raw_movement[MovementModel.TRANSACTION_ID]
return Movement(mid, currency, mts_started, mts_updated, status, amount, fees, dst_address, tx_id)
def __str__(self):
''' Allow us to print the Movement object in a pretty format '''
text = "Movement <'{}' amount={} fees={} mts_created={} mts_updated={} status='{}' destination_address={} transaction_id={}>"
return text.format(self.currency, self.amount, self.fees,
self.mts_started, self.mts_updated, self.status, self.dst_address, self.tx_id)

View File

@@ -67,7 +67,7 @@ class OrderFlags:
as flags as flags
""" """
HIDDEN = 64 HIDDEN = 64
CLOSE = 12 CLOSE = 512
REDUCE_ONLY = 1024 REDUCE_ONLY = 1024
POST_ONLY = 4096 POST_ONLY = 4096
OCO = 16384 OCO = 16384

View File

@@ -10,8 +10,8 @@ import datetime
from ..utils.custom_logger import CustomLogger from ..utils.custom_logger import CustomLogger
from ..utils.auth import generate_auth_headers, calculate_order_flags, gen_unique_cid 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 Wallet, Order, Position, Trade, FundingLoan, FundingOffer, FundingTrade, MarginInfoBase, MarginInfo
from ..models import FundingCredit, Notification, Ledger from ..models import FundingCredit, Notification, Ledger, Movement
class BfxRest: class BfxRest:
@@ -219,6 +219,24 @@ class BfxRest:
status = await self.fetch(endpoint) status = await self.fetch(endpoint)
return status return status
async def get_liquidations(self, start, end, limit=100, sort=-1):
"""
Endpoint to retrieve liquidations. By default it will retrieve the most recent liquidations,
but time-specific data can be retrieved using timestamps.
# Attributes
@param start int: millisecond start time
@param end int: millisecond end time
@param limit int: max number of items in response (max. 500)
@param sort int: if = 1 it sorts results returned with old > new
@return Array [ POS_ID, MTS, SYMBOL, AMOUNT, BASE_PRICE, IS_MATCH, IS_MARKET_SOLD, PRICE_ACQUIRED ]
"""
endpoint = "liquidations/hist"
params = "?start={}&end={}&limit={}&sort={}".format(
start, end, limit, sort)
liquidations = await self.fetch(endpoint, params=params)
return liquidations
async def get_public_pulse_hist(self, end=None, limit=25): async def get_public_pulse_hist(self, end=None, limit=25):
""" """
View the latest pulse messages. You can specify an end timestamp to view older messages. View the latest pulse messages. You can specify an end timestamp to view older messages.
@@ -370,6 +388,15 @@ class BfxRest:
stats = await self.fetch(endpoint) stats = await self.fetch(endpoint)
return stats return stats
async def get_conf_list_pair_exchange(self):
"""
Get list of available exchange pairs
# Attributes
@return Array [ SYMBOL ]
"""
endpoint = "conf/pub:list:pair:exchange"
pairs = await self.fetch(endpoint)
return pairs
################################################## ##################################################
# Authenticated Data # # Authenticated Data #
@@ -385,6 +412,22 @@ class BfxRest:
raw_wallets = await self.post(endpoint) raw_wallets = await self.post(endpoint)
return [Wallet(*rw[:5]) for rw in raw_wallets] return [Wallet(*rw[:5]) for rw in raw_wallets]
async def get_margin_info(self, symbol='base'):
"""
Get account margin information (like P/L, Swaps, Margin Balance, Tradable Balance and others).
Use different keys (base, SYMBOL, sym_all) to retrieve different kinds of data.
@return Array
"""
endpoint = f"auth/r/info/margin/{symbol}"
raw_margin_info = await self.post(endpoint)
if symbol == 'base':
return MarginInfoBase.from_raw_margin_info(raw_margin_info)
elif symbol == 'sym_all':
return [MarginInfo.from_raw_margin_info(record) for record in raw_margin_info]
else:
return MarginInfo.from_raw_margin_info(raw_margin_info)
async def get_active_orders(self, symbol): async def get_active_orders(self, symbol):
""" """
Get all of the active orders associated with API_KEY - Requires authentication. Get all of the active orders associated with API_KEY - Requires authentication.
@@ -397,7 +440,7 @@ class BfxRest:
raw_orders = await self.post(endpoint) raw_orders = await self.post(endpoint)
return [Order.from_raw_order(ro) for ro in raw_orders] return [Order.from_raw_order(ro) for ro in raw_orders]
async def get_order_history(self, symbol, start, end, limit=25, sort=-1): async def get_order_history(self, symbol, start, end, limit=25, sort=-1, ids=None):
""" """
Get all of the orders between the start and end period associated with API_KEY Get all of the orders between the start and end period associated with API_KEY
- Requires authentication. - Requires authentication.
@@ -407,12 +450,22 @@ class BfxRest:
@param start int: millisecond start time @param start int: millisecond start time
@param end int: millisecond end time @param end int: millisecond end time
@param limit int: max number of items in response @param limit int: max number of items in response
@param ids list of int: allows you to retrieve specific orders by order ID (ids: [ID1, ID2, ID3])
@return Array <models.Order> @return Array <models.Order>
""" """
endpoint = "auth/r/orders/{}/hist".format(symbol) endpoint = "auth/r/orders/{}/hist".format(symbol)
params = "?start={}&end={}&limit={}&sort={}".format( payload = {}
start, end, limit, sort) if start:
raw_orders = await self.post(endpoint, params=params) payload['start'] = start
if end:
payload['end'] = end
if limit:
payload['limit'] = limit
if sort:
payload['sort'] = sort
if ids:
payload['id'] = ids
raw_orders = await self.post(endpoint, payload)
return [Order.from_raw_order(ro) for ro in raw_orders] return [Order.from_raw_order(ro) for ro in raw_orders]
async def get_active_position(self): async def get_active_position(self):
@@ -439,7 +492,7 @@ class BfxRest:
raw_trades = await self.post(endpoint) raw_trades = await self.post(endpoint)
return [Trade.from_raw_rest_trade(rt) for rt in raw_trades] return [Trade.from_raw_rest_trade(rt) for rt in raw_trades]
async def get_trades(self, symbol, start, end, limit=25): async def get_trades(self, start, end, symbol=None, limit=25):
""" """
Get all of the trades between the start and end period associated with API_KEY Get all of the trades between the start and end period associated with API_KEY
- Requires authentication. - Requires authentication.
@@ -451,11 +504,27 @@ class BfxRest:
@param limit int: max number of items in response @param limit int: max number of items in response
@return Array <models.Trade> @return Array <models.Trade>
""" """
endpoint = "auth/r/trades/{}/hist".format(symbol) endpoint = "auth/r/trades/{}/hist".format(symbol) if symbol else "auth/r/trades/hist"
params = "?start={}&end={}&limit={}".format(start, end, limit) params = "?start={}&end={}&limit={}".format(start, end, limit)
raw_trades = await self.post(endpoint, params=params) raw_trades = await self.post(endpoint, params=params)
return [Trade.from_raw_rest_trade(rt) for rt in raw_trades] return [Trade.from_raw_rest_trade(rt) for rt in raw_trades]
async def get_funding_trades(self, symbol, start, end, limit=25):
"""
Get all of the funding trades between the start and end period associated with API_KEY
- Requires authentication.
# Attributes
@param symbol string: pair symbol i.e fUSD
@param start int: millisecond start time
@param end int: millisecond end time
@param limit int: max number of items in response
@return Array <models.FundingTrade>
"""
endpoint = "auth/r/funding/trades/{}/hist".format(symbol)
raw_trades = await self.post(endpoint)
return [FundingTrade.from_raw_rest_trade(rt) for rt in raw_trades]
async def get_funding_offers(self, symbol): async def get_funding_offers(self, symbol):
""" """
Get all of the funding offers associated with API_KEY - Requires authentication. Get all of the funding offers associated with API_KEY - Requires authentication.
@@ -558,6 +627,22 @@ class BfxRest:
raw_ledgers = await self.post(endpoint, params=params) raw_ledgers = await self.post(endpoint, params=params)
return [Ledger.from_raw_ledger(rl) for rl in raw_ledgers] return [Ledger.from_raw_ledger(rl) for rl in raw_ledgers]
async def get_movement_history(self, currency, start="", end="", limit=25):
"""
Get all of the deposits and withdraws between the start and end period associated with API_KEY
- Requires authentication.
# Attributes
@param currency string: pair symbol i.e BTC
@param start int: millisecond start time
@param end int: millisecond end time
@param limit int: max number of items in response
@return Array <models.Movement>
"""
endpoint = "auth/r/movements/{}/hist".format(currency)
params = "?start={}&end={}&limit={}".format(start, end, limit)
raw_movements = await self.post(endpoint, params=params)
return [Movement.from_raw_movement(rm) for rm in raw_movements]
async def submit_funding_offer(self, symbol, amount, rate, period, async def submit_funding_offer(self, symbol, amount, rate, period,
funding_type=FundingOffer.Type.LIMIT, hidden=False): funding_type=FundingOffer.Type.LIMIT, hidden=False):
""" """
@@ -594,6 +679,17 @@ class BfxRest:
raw_notification = await self.post(endpoint, {'id': fundingId}) raw_notification = await self.post(endpoint, {'id': fundingId})
return Notification.from_raw_notification(raw_notification) return Notification.from_raw_notification(raw_notification)
async def submit_cancel_all_funding_offer(self, currency):
"""
Cancel all funding offers at once
# Attributes
@param currency str: currency for which to cancel all offers (USD, BTC, UST ...)
"""
endpoint = "auth/w/funding/offer/cancel/all"
raw_notification = await self.post(endpoint, {'currency': currency})
return Notification.from_raw_notification(raw_notification)
async def keep_funding(self, type, id): async def keep_funding(self, type, id):
""" """
Toggle to keep funding taken. Specify loan for unused funding and credit for used funding. Toggle to keep funding taken. Specify loan for unused funding and credit for used funding.
@@ -834,7 +930,7 @@ class BfxRest:
if price_trailing != None: if price_trailing != None:
payload['price_trailing'] = str(price_trailing) payload['price_trailing'] = str(price_trailing)
if time_in_force != None: if time_in_force != None:
payload['time_in_force'] = str(time_in_force) payload['tif'] = str(time_in_force)
if leverage != None: if leverage != None:
payload["lev"] = str(leverage) payload["lev"] = str(leverage)
flags = calculate_order_flags( flags = calculate_order_flags(
@@ -905,6 +1001,145 @@ class BfxRest:
raw_notification = await self.post(endpoint, payload) raw_notification = await self.post(endpoint, payload)
return Notification.from_raw_notification(raw_notification) return Notification.from_raw_notification(raw_notification)
async def claim_position(self, position_id, amount):
"""
The claim feature allows the use of funds you have in your Margin Wallet
to settle a leveraged position as an exchange buy or sale
# Attributes
@param position_id: id of the position
@param amount: amount to claim
@return Array [ MTS, TYPE, MESSAGE_ID, null, [SYMBOL, POSITION_STATUS,
AMOUNT, BASE_PRICE, MARGIN_FUNDING, MARGIN_FUNDING_TYPE, PLACEHOLDER,
PLACEHOLDER, PLACEHOLDER, PLACEHOLDER, PLACEHOLDER, POSITION_ID, MTS_CREATE,
MTS_UPDATE, PLACEHOLDER, POS_TYPE, PLACEHOLDER, COLLATERAL, MIN_COLLATERAL,
META], CODE, STATUS, TEXT]
"""
payload = {
"id": position_id,
"amount": f"{amount * -1}"
}
endpoint = "auth/w/position/claim"
message = await self.post(endpoint, payload)
return message
async def get_alerts(self):
"""
Retrieve a list of active price alerts
"""
endpoint = f"auth/r/alerts"
message = await self.post(endpoint, {})
return message
async def set_alert(self, type, symbol, price):
"""
Sets up a price alert at the given value
# Attributes
@param type string
@param symbol string
@param price float
"""
endpoint = f"auth/w/alert/set"
payload = {
"type": type,
"symbol": symbol,
"price": price
}
message = await self.post(endpoint, payload)
return message
async def delete_alert(self, symbol, price):
"""
Delete an active alert
# Attributes
@param symbol string
@param price float
"""
endpoint = f"auth/w/alert/price:{symbol}:{price}/del"
payload = {
"symbol": symbol,
"price": price
}
message = await self.post(endpoint, payload)
return message
async def calc_order_avail(self, symbol, type, lev, dir=None, rate=None):
"""
Calculate the balance available for orders/offers
# Attributes
@param symbol str: Symbol (tBTCUSD, tBTCUST, fUSD, .... )
@param dir int: Direction of the order (1 for by, -1 for sell) (Mandator for EXCHANGE and MARGIN type, not used for FUNDING)
@param rate str: Order price (Mandator for EXCHANGE and MARGIN type, not used for FUNDING)
@param type str: Type of the order/offer EXCHANGE, MARGIN, DERIV, or FUNDING
@param lev str: Leverage that you want to use in calculating the max order amount (DERIV only)
"""
endpoint = f"auth/calc/order/avail"
payload = {
"symbol": symbol,
"type": type,
"lev": lev
}
if dir:
payload["dir"] = dir
if rate:
payload["rate"] = rate
message = await self.post(endpoint, payload)
return message
async def write_user_settings(self, settings):
"""
Allows you to create custom settings by creating key: value pairs
# Attributes
@param Settings object: object of keys and values to be set. Must follow regex pattern /^api:[A-Za-z0-9_-]*$/
"""
endpoint = f"auth/w/settings/set"
payload = {
"Settings": settings
}
message = await self.post(endpoint, payload)
return message
async def read_user_settings(self, keys):
"""
Allows you to read custom settings by providing a key
# Attributes
@param Keys array: the keys for which you wish to retrieve the values
"""
endpoint = f"auth/w/settings"
payload = {
"Keys": keys
}
message = await self.post(endpoint, payload)
return message
async def delete_user_settings(self, settings):
"""
Allows you to delete custom settings
# Attributes
@param settings object: object of keys to be deleted followed by value 1. Must follow regex pattern /^api:[A-Za-z0-9_-]*$/
"""
endpoint = f"auth/w/settings/del"
payload = {
"Settings": settings
}
message = await self.post(endpoint, payload)
return message
async def get_auth_pulse_hist(self, is_public=None): async def get_auth_pulse_hist(self, is_public=None):
""" """
Allows you to retrieve your private pulse history or the public pulse history with an additional UID_LIKED field. Allows you to retrieve your private pulse history or the public pulse history with an additional UID_LIKED field.
@@ -1045,3 +1280,133 @@ class BfxRest:
payload['symbol'] = symbol payload['symbol'] = symbol
payload['collateral'] = collateral payload['collateral'] = collateral
return await self.post(endpoint, data=payload) return await self.post(endpoint, data=payload)
##################################################
# Merchants #
##################################################
async def submit_invoice(self, amount, currency, pay_currencies, order_id, webhook, redirect_url, customer_info_nationality,
customer_info_resid_country, customer_info_resid_city, customer_info_resid_zip_code,
customer_info_resid_street, customer_info_full_name, customer_info_email,
customer_info_resid_state=None, customer_info_resid_building_no=None, duration=None):
"""
Submit an invoice for payment
# Attributes
@param amount str: Invoice amount in currency (From 0.1 USD to 1000 USD)
@param currency str: Invoice currency, currently supported: USD
@param pay_currencies list of str: Currencies in which invoice accepts the payments, supported values are BTC, ETH, UST-ETH, UST-TRX, UST-LBT, LNX, LBT
@param order_id str: Reference order identifier in merchant's platform
@param webhook str: The endpoint that will be called once the payment is completed/expired
@param redirect_url str: Merchant redirect URL, this one is used in UI to redirect customer to merchant's site once the payment is completed/expired
@param customer_info_nationality str: Customer's nationality, alpha2 code or full country name (alpha2 preffered)
@param customer_info_resid_country str: Customer's residential country, alpha2 code or full country name (alpha2 preffered)
@param customer_info_resid_city str: Customer's residential city/town
@param customer_info_resid_zip_code str: Customer's residential zip code/postal code
@param customer_info_resid_street str: Customer's residential street address
@param customer_info_full_name str: Customer's full name
@param customer_info_email str: Customer's email address
@param customer_info_resid_state str: Optional, customer's residential state/province
@param customer_info_resid_building_no str: Optional, customer's residential building number/name
@param duration int: Optional, invoice expire time in seconds, minimal duration is 5 mins (300) and maximal duration is 24 hours (86400). Default value is 15 minutes
"""
endpoint = 'auth/w/ext/pay/invoice/create'
payload = {
'amount': amount,
'currency': currency,
'payCurrencies': pay_currencies,
'orderId': order_id,
'webhook': webhook,
'redirectUrl': redirect_url,
'customerInfo': {
'nationality': customer_info_nationality,
'residCountry': customer_info_resid_country,
'residCity': customer_info_resid_city,
'residZipCode': customer_info_resid_zip_code,
'residStreet': customer_info_resid_street,
'fullName': customer_info_full_name,
'email': customer_info_email
},
'duration': duration
}
if customer_info_resid_state:
payload['customerInfo']['residState'] = customer_info_resid_state
if customer_info_resid_building_no:
payload['customerInfo']['residBuildingNo'] = customer_info_resid_building_no
return await self.post(endpoint, data=payload)
async def get_invoices(self, id=None, start=None, end=None, limit=10):
"""
List submitted invoices
# Attributes
@param id str: Unique invoice identifier
@param start int: Millisecond start time
@param end int: Millisecond end time
@param limit int: Millisecond start time
"""
endpoint = 'auth/r/ext/pay/invoices'
payload = {}
if id:
payload['id'] = id
if start:
payload['start'] = start
if end:
payload['end'] = end
if limit:
payload['limit'] = limit
return await self.post(endpoint, data=payload)
async def complete_invoice(self, id, pay_ccy, deposit_id=None, ledger_id=None):
"""
Manually complete an invoice
# Attributes
@param id str: Unique invoice identifier
@param pay_ccy str: Paid invoice currency, should be one of values under payCurrencies field on invoice
@param deposit_id int: Movement/Deposit Id linked to invoice as payment
@param ledger_id int: Ledger entry Id linked to invoice as payment, use either depositId or ledgerId
"""
endpoint = 'auth/w/ext/pay/invoice/complete'
payload = {
'id': id,
'payCcy': pay_ccy
}
if deposit_id:
payload['depositId'] = deposit_id
if ledger_id:
payload['ledgerId'] = ledger_id
return await self.post(endpoint, data=payload)
async def get_unlinked_deposits(self, ccy, start=None, end=None):
"""
Retrieve deposits that possibly could be linked to bitfinex pay invoices
# Attributes
@param ccy str: Pay currency to search deposits for, supported values are: BTC, ETH, UST-ETH, UST-TRX, UST-LBT, LNX, LBT
@param start int: Millisecond start time
@param end int: Millisecond end time
"""
endpoint = 'auth/r/ext/pay/deposits/unlinked'
payload = {
'ccy': ccy
}
if start:
payload['start'] = start
if end:
payload['end'] = end
return await self.post(endpoint, data=payload)

View File

@@ -16,7 +16,7 @@ async def run():
print(trades) print(trades)
for trade in trades: for trade in trades:
orders_ids.append(trade[0]) orders_ids.append(trade[0])
assert orders_ids == [657815316, 657815314, 657815312, 657815311, 657815309] assert orders_ids == [657815316, 657815314, 657815312, 657815308, 657815304]
# check that strictly decreasing order id condition is always respected # check that strictly decreasing order id condition is always respected
# check that not increasing timestamp condition is always respected # check that not increasing timestamp condition is always respected

View File

@@ -1,6 +1,5 @@
import pytest import pytest
import json import json
import asyncio
from .helpers import (create_stubbed_client, ws_publish_connection_init, EventWatcher) from .helpers import (create_stubbed_client, ws_publish_connection_init, EventWatcher)
@pytest.mark.asyncio @pytest.mark.asyncio

View File

@@ -0,0 +1,12 @@
from ..utils.custom_logger import CustomLogger
def handle_failure(func):
async def inner_function(*args, **kwargs):
logger = CustomLogger('BfxWebsocket', logLevel="DEBUG")
try:
await func(*args, **kwargs)
except Exception as exception_message:
logger.error(exception_message)
return inner_function

View File

@@ -2,4 +2,4 @@
This module contains the current version of the bfxapi lib This module contains the current version of the bfxapi lib
""" """
__version__ = '1.1.14' __version__ = '2.0.3'

View File

@@ -12,7 +12,9 @@ from .subscription_manager import SubscriptionManager
from .wallet_manager import WalletManager from .wallet_manager import WalletManager
from .order_manager import OrderManager from .order_manager import OrderManager
from ..utils.auth import generate_auth_payload from ..utils.auth import generate_auth_payload
from ..utils.decorators import handle_failure
from ..models import Order, Trade, OrderBook, Ticker, FundingTicker from ..models import Order, Trade, OrderBook, Ticker, FundingTicker
from ..constants import PUB_WS_HOST
class Flags: class Flags:
@@ -65,6 +67,39 @@ def _parse_trade(tData, symbol):
'symbol': symbol 'symbol': symbol
} }
def _parse_user_trade(tData):
return {
'id': tData[0],
'symbol': tData[1],
'mts_create': tData[2],
'order_id': tData[3],
'exec_amount': tData[4],
'exec_price': tData[5],
'order_type': tData[6],
'order_price': tData[7],
'maker': tData[8],
'cid': tData[11],
}
def _parse_user_trade_update(tData):
return {
'id': tData[0],
'symbol': tData[1],
'mts_create': tData[2],
'order_id': tData[3],
'exec_amount': tData[4],
'exec_price': tData[5],
'order_type': tData[6],
'order_price': tData[7],
'maker': tData[8],
'fee': tData[9],
'fee_currency': tData[10],
'cid': tData[11],
}
def _parse_deriv_status_update(sData, symbol): def _parse_deriv_status_update(sData, symbol):
return { return {
'symbol': symbol, 'symbol': symbol,
@@ -139,6 +174,7 @@ class BfxWebsocket(GenericWebsocket):
- `funding_credit_snapshot` (array): Opening funding credit balances - `funding_credit_snapshot` (array): Opening funding credit balances
- `balance_update` (array): When the state of a balance is changed - `balance_update` (array): When the state of a balance is changed
- `new_trade` (array): A new trade on the market has been executed - `new_trade` (array): A new trade on the market has been executed
- `new_user_trade` (array): A new - your - trade has been executed
- `new_ticker` (Ticker|FundingTicker): A new ticker update has been published - `new_ticker` (Ticker|FundingTicker): A new ticker update has been published
- `new_funding_ticker` (FundingTicker): A new funding ticker update has been published - `new_funding_ticker` (FundingTicker): A new funding ticker update has been published
- `new_trading_ticker` (Ticker): A new trading ticker update has been published - `new_trading_ticker` (Ticker): A new trading ticker update has been published
@@ -152,7 +188,7 @@ class BfxWebsocket(GenericWebsocket):
- `unsubscribed` (Subscription): A channel has been un-subscribed - `unsubscribed` (Subscription): A channel has been un-subscribed
""" """
def __init__(self, API_KEY=None, API_SECRET=None, host='wss://api-pub.bitfinex.com/ws/2', def __init__(self, API_KEY=None, API_SECRET=None, host=PUB_WS_HOST,
manageOrderBooks=False, dead_man_switch=False, ws_capacity=25, logLevel='INFO', parse_float=float, manageOrderBooks=False, dead_man_switch=False, ws_capacity=25, logLevel='INFO', parse_float=float,
channel_filter=[], *args, **kwargs): channel_filter=[], *args, **kwargs):
self.API_KEY = API_KEY self.API_KEY = API_KEY
@@ -266,7 +302,7 @@ class BfxWebsocket(GenericWebsocket):
socketId, socketId,
ERRORS[data.get('code', 10000)], ERRORS[data.get('code', 10000)],
data.get("msg", "")) data.get("msg", ""))
self._emit('error', err_string) self._emit(Exception(err_string))
async def _system_auth_handler(self, socketId, data): async def _system_auth_handler(self, socketId, data):
if data.get('status') == 'FAILED': if data.get('status') == 'FAILED':
@@ -282,6 +318,11 @@ class BfxWebsocket(GenericWebsocket):
symbol = self.subscriptionManager.get(data[0]).symbol symbol = self.subscriptionManager.get(data[0]).symbol
tradeObj = _parse_trade(tData, symbol) tradeObj = _parse_trade(tData, symbol)
self._emit('trade_update', tradeObj) self._emit('trade_update', tradeObj)
else:
# user trade
# [0,"tu",[738045455,"tTESTBTC:TESTUSD",1622169615771,66635385225,0.001,38175,"EXCHANGE LIMIT",39000,-1,-0.000002,"TESTBTC",1622169615685]]
tradeObj = _parse_user_trade_update(tData)
self._emit('user_trade_update', tradeObj)
async def _trade_executed_handler(self, data): async def _trade_executed_handler(self, data):
tData = data[2] tData = data[2]
@@ -290,6 +331,11 @@ class BfxWebsocket(GenericWebsocket):
symbol = self.subscriptionManager.get(data[0]).symbol symbol = self.subscriptionManager.get(data[0]).symbol
tradeObj = _parse_trade(tData, symbol) tradeObj = _parse_trade(tData, symbol)
self._emit('new_trade', tradeObj) self._emit('new_trade', tradeObj)
else:
# user trade
# [0, 'te', [37558151, 'tBTCUSD', 1643542688513, 1512164914, 0.0001, 30363, 'EXCHANGE MARKET', 100000, -1, None, None, 1643542688390]]
tradeObj = _parse_user_trade(tData)
self._emit('new_user_trade', tradeObj)
async def _wallet_update_handler(self, data): async def _wallet_update_handler(self, data):
# [0,"wu",["exchange","USD",89134.66933283,0]] # [0,"wu",["exchange","USD",89134.66933283,0]]
@@ -481,6 +527,7 @@ class BfxWebsocket(GenericWebsocket):
else: else:
self.logger.warn('Unknown (socketId={}) websocket response: {}'.format(socketId, msg)) self.logger.warn('Unknown (socketId={}) websocket response: {}'.format(socketId, msg))
@handle_failure
async def _ws_authenticate_socket(self, socketId): async def _ws_authenticate_socket(self, socketId):
socket = self.sockets[socketId] socket = self.sockets[socketId]
socket.set_authenticated() socket.set_authenticated()
@@ -507,6 +554,7 @@ class BfxWebsocket(GenericWebsocket):
# re-subscribe to existing channels # re-subscribe to existing channels
await self.subscriptionManager.resubscribe_by_socket(socket_id) await self.subscriptionManager.resubscribe_by_socket(socket_id)
@handle_failure
async def _send_auth_command(self, channel_name, data): async def _send_auth_command(self, channel_name, data):
payload = [0, channel_name, None, data] payload = [0, channel_name, None, data]
socket = self.get_authenticated_socket() socket = self.get_authenticated_socket()
@@ -538,6 +586,7 @@ class BfxWebsocket(GenericWebsocket):
total += self.get_socket_capacity(socketId) total += self.get_socket_capacity(socketId)
return total return total
@handle_failure
async def enable_flag(self, flag): async def enable_flag(self, flag):
""" """
Enable flag on websocket connection Enable flag on websocket connection

View File

@@ -13,7 +13,7 @@ from pyee import AsyncIOEventEmitter
from ..utils.custom_logger import CustomLogger from ..utils.custom_logger import CustomLogger
# websocket exceptions # websocket exceptions
from websockets.exceptions import ConnectionClosed from websockets.exceptions import ConnectionClosed, InvalidStatusCode
class AuthError(Exception): class AuthError(Exception):
""" """
@@ -44,7 +44,7 @@ class Socket():
def set_authenticated(self): def set_authenticated(self):
self.isAuthenticated = True self.isAuthenticated = True
def set_unauthenticated(self): def set_unauthenticated(self):
self.isAuthenticated = False self.isAuthenticated = False
@@ -84,6 +84,10 @@ class GenericWebsocket:
thread and connection. thread and connection.
""" """
self._start_new_socket() self._start_new_socket()
event_loop = asyncio.get_event_loop()
if not event_loop or not event_loop.is_running():
while True:
time.sleep(1)
def get_task_executable(self): def get_task_executable(self):
""" """
@@ -91,14 +95,14 @@ class GenericWebsocket:
""" """
return self._run_socket() return self._run_socket()
def _start_new_async_socket(self):
loop = asyncio.new_event_loop()
loop.run_until_complete(self._run_socket())
def _start_new_socket(self, socketId=None): def _start_new_socket(self, socketId=None):
if not socketId: if not socketId:
socketId = len(self.sockets) socketId = len(self.sockets)
def start_loop(loop): worker = Thread(target=self._start_new_async_socket)
asyncio.set_event_loop(loop)
loop.run_until_complete(self._run_socket())
worker_loop = asyncio.new_event_loop()
worker = Thread(target=start_loop, args=(worker_loop,))
worker.start() worker.start()
return socketId return socketId
@@ -128,7 +132,7 @@ class GenericWebsocket:
s = Socket(sId) s = Socket(sId)
self.sockets[sId] = s self.sockets[sId] = s
loop = asyncio.get_event_loop() loop = asyncio.get_event_loop()
while retries < self.max_retries and self.attempt_retry: while self.max_retries == 0 or (retries < self.max_retries and self.attempt_retry):
try: try:
async with websockets.connect(self.host) as websocket: async with websockets.connect(self.host) as websocket:
self.sockets[sId].set_websocket(websocket) self.sockets[sId].set_websocket(websocket)
@@ -141,7 +145,7 @@ class GenericWebsocket:
await asyncio.sleep(0) await asyncio.sleep(0)
message = await websocket.recv() message = await websocket.recv()
await self.on_message(sId, message) await self.on_message(sId, message)
except (ConnectionClosed, socket.error) as e: except (ConnectionClosed, socket.error, InvalidStatusCode) as e:
self.sockets[sId].set_disconnected() self.sockets[sId].set_disconnected()
if self.sockets[sId].isAuthenticated: if self.sockets[sId].isAuthenticated:
self.sockets[sId].set_unauthenticated() self.sockets[sId].set_unauthenticated()
@@ -190,6 +194,8 @@ class GenericWebsocket:
self.events.once(event, func) self.events.once(event, func)
def _emit(self, event, *args, **kwargs): def _emit(self, event, *args, **kwargs):
if type(event) == Exception:
self.logger.error(event)
self.events.emit(event, *args, **kwargs) self.events.emit(event, *args, **kwargs)
async def on_error(self, error): async def on_error(self, error):
@@ -202,7 +208,7 @@ class GenericWebsocket:
""" """
This is used by the HF data server. This is used by the HF data server.
""" """
self.stop() await self.stop()
async def on_open(self): async def on_open(self):
""" """

View File

@@ -202,7 +202,7 @@ class OrderManager:
if price_trailing != None: if price_trailing != None:
payload['price_trailing'] = str(price_trailing) payload['price_trailing'] = str(price_trailing)
if time_in_force != None: if time_in_force != None:
payload['time_in_force'] = str(time_in_force) payload['tif'] = str(time_in_force)
if leverage != None: if leverage != None:
payload['lev'] = str(leverage) payload['lev'] = str(leverage)
flags = calculate_order_flags( flags = calculate_order_flags(

View File

@@ -62,6 +62,7 @@ class SubscriptionManager:
channel = raw_ws_data.get("channel") channel = raw_ws_data.get("channel")
chan_id = raw_ws_data.get("chanId") chan_id = raw_ws_data.get("chanId")
key = raw_ws_data.get("key", None) key = raw_ws_data.get("key", None)
p_sub = None
get_key = "{}_{}".format(channel, key or symbol) get_key = "{}_{}".format(channel, key or symbol)
if chan_id in self.subscriptions_chanid: if chan_id in self.subscriptions_chanid:
# subscription has already existed in the past # subscription has already existed in the past

View File

@@ -58,6 +58,7 @@ https://github.com/Crypto-toolbox/btfxwss
- `notification` (Notification): incoming account notification - `notification` (Notification): incoming account notification
- `error` (array): error from the websocket - `error` (array): error from the websocket
- `order_closed` (Order, Trade): when an order has been closed - `order_closed` (Order, Trade): when an order has been closed
- `order_update` (Order, Trade): when an order has been updated
- `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_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 - `order_confirmed` (Order, Trade): When an order has been submitted and received
- `wallet_snapshot` (array[Wallet]): Initial wallet balances (Fired once) - `wallet_snapshot` (array[Wallet]): Initial wallet balances (Fired once)
@@ -75,10 +76,12 @@ https://github.com/Crypto-toolbox/btfxwss
- `funding_credit_snapshot` (array): Opening funding credit balances - `funding_credit_snapshot` (array): Opening funding credit balances
- `balance_update` (array): When the state of a balance is changed - `balance_update` (array): When the state of a balance is changed
- `new_trade` (array): A new trade on the market has been executed - `new_trade` (array): A new trade on the market has been executed
- `new_user_trade` (array): A new - your - trade has been executed
- `new_ticker` (Ticker|FundingTicker): A new ticker update has been published - `new_ticker` (Ticker|FundingTicker): A new ticker update has been published
- `new_funding_ticker` (FundingTicker): A new funding ticker update has been published - `new_funding_ticker` (FundingTicker): A new funding ticker update has been published
- `new_trading_ticker` (Ticker): A new trading ticker update has been published - `new_trading_ticker` (Ticker): A new trading ticker update has been published
- `trade_update` (array): A trade on the market has been updated - `trade_update` (array): A trade on the market has been updated
- `user_trade_update` (array): A - your - trade has been updated
- `new_candle` (array): A new candle has been produced - `new_candle` (array): A new candle has been produced
- `margin_info_updates` (array): New margin information has been broadcasted - `margin_info_updates` (array): New margin information has been broadcasted
- `funding_info_updates` (array): New funding information has been broadcasted - `funding_info_updates` (array): New funding information has been broadcasted

View File

@@ -1,8 +1,8 @@
asyncio==3.4.3 asyncio==3.4.3
websockets==8.1 websockets==9.1
pylint==2.3.0 pylint==2.3.0
pytest-asyncio==0.10.0 pytest-asyncio==0.15.1
six==1.12.0 six==1.12.0
pyee==8.0.1 pyee==8.0.1
aiohttp==3.4.4 aiohttp==3.4.4
isort==4.3.21 isort==4.3.21

View File

@@ -11,7 +11,7 @@ from os import path
here = path.abspath(path.dirname(__file__)) here = path.abspath(path.dirname(__file__))
setup( setup(
name='bitfinex-api-py', name='bitfinex-api-py',
version='1.1.14', version='2.0.3',
description='Official Bitfinex Python API', description='Official Bitfinex Python API',
long_description='A Python reference implementation of the Bitfinex API for both REST and websocket interaction', long_description='A Python reference implementation of the Bitfinex API for both REST and websocket interaction',
long_description_content_type='text/markdown', long_description_content_type='text/markdown',
@@ -49,7 +49,7 @@ setup(
# deps installed by pip # deps installed by pip
install_requires=[ install_requires=[
'asyncio~=3.0', 'asyncio~=3.0',
'websockets~=8.0', 'websockets>=8,<10',
'aiohttp~=3.0', 'aiohttp~=3.0',
'pyee~=8.0' 'pyee~=8.0'
], ],