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.
235 lines
7.8 KiB
235 lines
7.8 KiB
import asyncio |
|
|
|
from dataclasses import dataclass |
|
|
|
from .api import ApiProvider, ApiError |
|
from .enums import ( |
|
Symbol, Period, Depth, PeriodWebSocket, OrderSide, OrderStatus, OrderType |
|
) |
|
|
|
|
|
@dataclass |
|
class Candle: |
|
time: int |
|
open: float |
|
high: float |
|
low: float |
|
close: float |
|
volume: float |
|
|
|
|
|
class Exchange: |
|
def __init__(self, api: ApiProvider = None): |
|
self.api = api or ApiProvider(auth_required=False) |
|
|
|
async def get_symbols(self): |
|
"""List all available market symbols.""" |
|
return await self.api.get('symbols') |
|
|
|
async def get_tickers(self): |
|
"""Get tickers in all available markets.""" |
|
response = await self.api.ws_request( |
|
{'event': 'req', 'params': {'channel': 'review'}}) |
|
return response['data'] |
|
|
|
async def get_ticker(self, symbol: Symbol): |
|
return (await self.get_tickers()).get(symbol.value) |
|
|
|
async def get_candles(self, symbol: Symbol, period: Period = Period.D1): |
|
"""Get k-line data over a specified period.""" |
|
data = await self.api.get( |
|
'klines', {'symbol': symbol.value, 'period': period.value}) |
|
for candle in reversed(data): |
|
yield Candle(*candle) |
|
|
|
async def get_trades(self, symbol: Symbol): |
|
"""Get last 200 trades in a specified market.""" |
|
return await self.api.get('trades', {'symbol': symbol.value}) |
|
|
|
async def get_prices(self): |
|
"""Get latest execution price for all markets.""" |
|
return await self.api.get('ticker/price') |
|
|
|
async def get_price(self, symbol: Symbol): |
|
return float((await self.get_prices())[symbol.value]) |
|
|
|
async def get_orderbook(self, symbol: Symbol, depth: Depth = Depth.LOW): |
|
"""Get the order book for a particular market.""" |
|
data = await self.api.get( |
|
'depth', {'symbol': symbol.value, 'type': depth.value}) |
|
return data['tick'] |
|
|
|
async def listen_candles(self, symbol: Symbol, period: Period): |
|
period = PeriodWebSocket[period.name].value |
|
channel = { |
|
'event': 'sub', |
|
'params': {'channel': f'market_{symbol.value}_kline_{period}'} |
|
} |
|
prev_id = None |
|
async for data in self.api.ws_listen(channel): |
|
if 'ping' in data: |
|
continue |
|
candle = data['tick'] |
|
if candle['id'] == prev_id: |
|
continue |
|
prev_id = candle['id'] |
|
yield Candle( |
|
candle['id'], candle['open'], candle['high'], |
|
candle['low'], candle['close'], candle['vol'] |
|
) |
|
|
|
async def listen_trades(self, symbol: Symbol): |
|
channel = { |
|
'event': 'sub', |
|
'params': {'channel': f'market_{symbol.value}_trade_ticker'} |
|
} |
|
async for data in self.api.ws_listen(channel): |
|
if 'ping' in data: |
|
continue |
|
yield data['tick']['data'] |
|
|
|
async def listen_order_book( |
|
self, symbol: Symbol, depth: Depth = Depth.LOW): |
|
channel = { |
|
'event': 'sub', |
|
'params': { |
|
'channel': f'market_{symbol.value}_depth_{depth.value}', |
|
'asks': 150, |
|
'buys': 150 |
|
} |
|
} |
|
async for data in self.api.ws_listen(channel): |
|
if 'ping' in data: |
|
continue |
|
data['tick']['bids'] = data['tick'].pop('buys') |
|
yield data['tick'] |
|
|
|
|
|
class Account: |
|
def __init__( |
|
self, *, api_key: str = '', api_secret: str = '', |
|
from_env: bool = False, 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) |
|
|
|
async def get_balance(self): |
|
return await self.api.post('account') |
|
|
|
async def get_orders( |
|
self, symbol: Symbol, page: int = 1, page_size: int = 20): |
|
data = await self.api.post('allOrders', { |
|
'symbol': symbol.value, |
|
'pageSize': page_size, |
|
'page': page |
|
}) |
|
return data.get('orderList') or [] |
|
|
|
async def get_open_orders( |
|
self, symbol: Symbol, page: int = 1, page_size: int = 20): |
|
data = await self.api.post('openOrders', { |
|
'symbol': symbol.value, |
|
'pageSize': page_size, |
|
'page': page |
|
}) |
|
return data.get('resultList') or [] |
|
|
|
async def get_trades( |
|
self, symbol: Symbol, page: int = 1, page_size: int = 20): |
|
data = await self.api.post('myTrades', { |
|
'symbol': symbol.value, |
|
'pageSize': page_size, |
|
'page': page |
|
}) |
|
return data.get('resultList') or [] |
|
|
|
async def create_order( |
|
self, symbol: Symbol, side: OrderSide, type_: OrderType, |
|
volume: float, price: float = 0) -> int: |
|
data = { |
|
'symbol': symbol.value, 'side': side.value, |
|
'type': type_.value, 'volume': volume, |
|
} |
|
|
|
if price: |
|
if type_ == OrderType.MARKET: |
|
raise ValueError( |
|
"Error, MARKET execution do not support price value") |
|
data['price'] = price |
|
|
|
resp = await self.api.post('order', data) |
|
return int(resp['order_id']) |
|
|
|
async def buy_limit(self, symbol: Symbol, volume: float, price: float): |
|
return await self.create_order( |
|
symbol, OrderSide.BUY, OrderType.LIMIT, volume, price |
|
) |
|
|
|
async def sell_limit(self, symbol: Symbol, volume: float, price: float): |
|
return await self.create_order( |
|
symbol, OrderSide.SELL, OrderType.LIMIT, volume, price |
|
) |
|
|
|
async def wait_for_status( |
|
self, order_id: int, symbol: Symbol, statuses, delay: int = 0.2): |
|
order = await self.get_order(order_id, symbol) |
|
statuses = ( |
|
OrderStatus.FILLED, OrderStatus.CANCELED, OrderStatus.EXPIRED |
|
) |
|
|
|
for _ in range(self.api.retries): |
|
await asyncio.sleep(delay) |
|
order = await self.get_order(order_id, symbol) |
|
if order['status'] in statuses: |
|
break |
|
|
|
if order['status'] not in statuses: |
|
raise ApiError( |
|
f"Status not changed for: {order}, must be in: {statuses}") |
|
|
|
async def buy_market( |
|
self, symbol: Symbol, volume: float, wait_for_fill=True): |
|
order_id = await self.create_order( |
|
symbol, OrderSide.BUY, OrderType.MARKET, volume |
|
) |
|
if wait_for_fill: |
|
await self.wait_for_status(order_id, symbol, ( |
|
OrderStatus.FILLED, OrderStatus.CANCELED, OrderStatus.EXPIRED |
|
)) |
|
|
|
return order_id |
|
|
|
async def sell_market( |
|
self, symbol: Symbol, volume: float, wait_for_fill=True): |
|
order_id = await self.create_order( |
|
symbol, OrderSide.SELL, OrderType.MARKET, volume |
|
) |
|
|
|
if wait_for_fill: |
|
await self.wait_for_status(order_id, symbol, ( |
|
OrderStatus.FILLED, OrderStatus.CANCELED, OrderStatus.EXPIRED |
|
)) |
|
|
|
return order_id |
|
|
|
async def get_order(self, order_id: int, symbol: Symbol): |
|
data = await self.api.post( |
|
'showOrder', {'order_id': order_id, 'symbol': symbol.value}) |
|
return data['order_info'] |
|
|
|
async def cancel_order( |
|
self, order_id: int, symbol: Symbol, wait_for_cancel=True): |
|
await self.api.post( |
|
'orders/cancel', {'order_id': order_id, 'symbol': symbol.value}) |
|
|
|
if not wait_for_cancel: |
|
return |
|
|
|
await self.wait_for_status(order_id, symbol, ( |
|
OrderStatus.CANCELED, OrderStatus.EXPIRED |
|
)) |
|
|
|
async def cancel_open_orders(self, symbol: Symbol): |
|
return await self.api.post('cancelAllOrders', {'symbol': symbol.value})
|
|
|