You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

312 lines
11 KiB

import asyncio
from typing import List, Dict
from .api import ApiProvider, ApiError
from .market import Exchange
from .structs import (
Deposit, DepositStatus, Pair, OrderSide, OrderStatus, OrderType, Order,
Coin, Balance, OrderExecType, OrderForceType, PrivateTrade, Interest,
Withdrawal, WithdrawalStatus
)
class Account:
"""Provides access to account actions and data. Balance, trades, orders."""
def __init__(
self, *, api_key: str = '', api_secret: str = '',
from_env: bool = False, exchange: Exchange = None,
api: ApiProvider = None):
if not api and not (api_key and api_secret) and not from_env:
raise ValueError(
'Pass ApiProvider or api_key with api_secret or from_env')
self.api = api or ApiProvider(
api_key=api_key, api_secret=api_secret, from_env=from_env)
self.exchange = exchange or Exchange(api)
self.pairs = self.exchange.pairs
async def sync_pairs(self):
await self.exchange.sync_pairs()
self.pairs = self.exchange.pairs
async def get_balance(self) -> Dict[Coin, Balance]:
"""Return balance."""
data = await self.api.post('private/get-account-summary')
return {
Coin(bal['currency']): Balance.from_api(bal)
for bal in data['accounts']
}
async def get_deposit_history(
self, coin: Coin, start_ts: int = None, end_ts: int = None,
status: DepositStatus = None, page: int = 0, page_size: int = 20
) -> List[Deposit]:
"""Return all history withdrawals."""
params = {'page_size': page_size, 'page': page}
if coin:
params['currency'] = coin.name
if start_ts:
params['start_ts'] = int(start_ts) * 1000
if end_ts:
params['end_ts'] = int(end_ts) * 1000
if status:
params['status'] = status
data = await self.api.post(
'private/get-deposit-history', {'params': params}) or {}
return [
Deposit.create_from_api(trx)
for trx in data.get('deposit_list') or []
]
async def get_withdrawal_history(
self, coin: Coin, start_ts: int = None, end_ts: int = None,
status: WithdrawalStatus = None, page: int = 0, page_size: int = 20
) -> List[Withdrawal]:
"""Return all history for withdrawal transactions."""
params = {'page_size': page_size, 'page': page}
if coin:
params['currency'] = coin.name
if start_ts:
params['start_ts'] = int(start_ts) * 1000
if end_ts:
params['end_ts'] = int(end_ts) * 1000
if status:
params['status'] = status
data = await self.api.post(
'private/get-withdrawal-history', {'params': params}) or {}
return [
Withdrawal.create_from_api(trx)
for trx in data.get('withdrawal_list') or []
]
async def get_interest_history(
self, coin: Coin, start_ts: int = None, end_ts: int = None,
page: int = 0, page_size: int = 20) -> List[Interest]:
"""Return all history interest."""
params = {'page_size': page_size, 'page': page}
if coin:
params['currency'] = coin.name
if start_ts:
params['start_ts'] = int(start_ts) * 1000
if end_ts:
params['end_ts'] = int(end_ts) * 1000
data = await self.api.post(
'private/margin/get-order-history', {'params': params}) or {}
return [
Interest.create_from_api(interest)
for interest in data.get('list') or []
]
async def get_orders_history(
self, pair: Pair = None, start_ts: int = None, end_ts: int = None,
page: int = 0, page_size: int = 200) -> List[Order]:
"""Return all history orders."""
params = {'page_size': page_size, 'page': page}
if pair:
params['instrument_name'] = pair.name
if start_ts:
params['start_ts'] = int(start_ts) * 1000
if end_ts:
params['end_ts'] = int(end_ts) * 1000
data = await self.api.post(
'private/get-order-history', {'params': params}) or {}
return [
Order.create_from_api(self.pairs[order['instrument_name']], order)
for order in data.get('order_list') or []
]
async def get_open_orders(
self, pair: Pair = None, page: int = 0,
page_size: int = 200) -> List[Order]:
"""Return open orders."""
params = {'page_size': page_size, 'page': page}
if pair:
params['instrument_name'] = pair.name
data = await self.api.post(
'private/get-open-orders', {'params': params})
return [
Order.create_from_api(self.pairs[order['instrument_name']], order)
for order in data.get('order_list') or []
]
async def get_trades(
self, pair: Pair = None, page: int = 0,
page_size: int = 200) -> List[PrivateTrade]:
"""Return trades."""
params = {'page_size': page_size, 'page': page}
if pair:
params['instrument_name'] = pair.name
data = await self.api.post('private/get-trades', {'params': params})
return [
PrivateTrade.create_from_api(
self.pairs[trade['instrument_name']], trade
)
for trade in data.get('trade_list') or []
]
async def create_order(
self, pair: Pair, side: OrderSide, type_: OrderType,
quantity: float, price: float = 0,
force_type: OrderForceType = OrderForceType.GOOD_TILL_CANCEL,
exec_type: OrderExecType = OrderExecType.MARKET,
client_id: int = None) -> int:
"""Create raw order with buy or sell side."""
data = {
'instrument_name': pair.name, 'side': side.value,
'type': type_.value
}
data['time_in_force'] = force_type.value
data['exec_inst'] = exec_type.value
quantity = "{:.{}f}".format(quantity, pair.quantity_precision)
if type_ == OrderType.MARKET and side == OrderSide.BUY:
data['notional'] = quantity
else:
data['quantity'] = quantity
if client_id:
data['client_oid'] = str(client_id)
if price:
if type_ == OrderType.MARKET:
raise ValueError(
"Error, MARKET execution do not support price value")
data['price'] = "{:.{}f}".format(price, pair.price_precision)
resp = await self.api.post('private/create-order', {'params': data})
return int(resp['order_id'])
async def buy_limit(
self, pair: Pair, quantity: float, price: float,
force_type: OrderForceType = OrderForceType.GOOD_TILL_CANCEL,
exec_type: OrderExecType = OrderExecType.MARKET,
client_id: int = None) -> int:
"""Buy limit order."""
return await self.create_order(
pair, OrderSide.BUY, OrderType.LIMIT, quantity, price,
force_type, exec_type
)
async def sell_limit(
self, pair: Pair, quantity: float, price: float,
force_type: OrderForceType = OrderForceType.GOOD_TILL_CANCEL,
exec_type: OrderExecType = OrderExecType.MARKET,
client_id: int = None) -> int:
"""Sell limit order."""
return await self.create_order(
pair, OrderSide.SELL, OrderType.LIMIT, quantity, price,
force_type, exec_type
)
async def wait_for_status(
self, order_id: int, statuses, delay: int = 0.1) -> None:
"""Wait for order status."""
order = await self.get_order(order_id)
for _ in range(self.api.retries):
if order.status in statuses:
break
await asyncio.sleep(delay)
order = await self.get_order(order_id)
if order.status not in statuses:
raise ApiError(
f"Status not changed for: {order}, must be in: {statuses}")
async def buy_market(
self, pair: Pair, spend: float, wait_for_fill=False) -> int:
"""Buy market order."""
order_id = await self.create_order(
pair, OrderSide.BUY, OrderType.MARKET, spend
)
if wait_for_fill:
await self.wait_for_status(order_id, (
OrderStatus.FILLED, OrderStatus.CANCELED, OrderStatus.EXPIRED,
OrderStatus.REJECTED
))
return order_id
async def sell_market(
self, pair: Pair, quantity: float, wait_for_fill=False) -> int:
"""Sell market order."""
order_id = await self.create_order(
pair, OrderSide.SELL, OrderType.MARKET, quantity
)
if wait_for_fill:
await self.wait_for_status(order_id, (
OrderStatus.FILLED, OrderStatus.CANCELED, OrderStatus.EXPIRED,
OrderStatus.REJECTED
))
return order_id
async def get_order(self, order_id: int) -> Order:
"""Get order info."""
order_info = {}
retries = 0
while True:
data = await self.api.post('private/get-order-detail', {
'params': {'order_id': str(order_id)}
})
order_info = data.get('order_info', {})
if not order_info:
if retries < 10:
await asyncio.sleep(0.5)
retries += 1
else:
raise ApiError(f'No order info found for id: {order_id}')
else:
break
return Order.create_from_api(
self.pairs[order_info['instrument_name']],
order_info, data['trade_list']
)
async def cancel_order(
self, order_id: int, pair: Pair, check_status=False) -> None:
"""Cancel order."""
await self.api.post('private/cancel-order', {
'params': {'order_id': order_id, 'instrument_name': pair.name}
})
if not check_status:
return
await self.wait_for_status(order_id, (
OrderStatus.CANCELED, OrderStatus.EXPIRED, OrderStatus.REJECTED
))
async def cancel_open_orders(self, pair: Pair) -> None:
"""Cancel all open orders."""
await self.api.post('private/cancel-all-orders', {
'params': {'instrument_name': pair.name}
})
async def listen_balance(self) -> Balance:
async for data in self.api.listen(
'user', 'user.balance', sign=True):
for bal in data.get('data', []):
yield Balance(
total=bal['balance'],
available=bal['available'],
in_orders=bal['order'],
in_stake=bal['stake'],
coin=Coin(bal['currency'])
)
async def listen_orders(self, pair: Pair) -> Order:
async for data in self.api.listen(
'user', f'user.order.{pair.name}', sign=True):
for order in data.get('data', []):
yield Order.create_from_api(
self.pairs[data['instrument_name']], order
)