async def req(self, client: aiohttp.ClientSession, method: str, uri: str, params: dict, headers: dict, signed: bool = True) -> (dict, dict): """ make a Binance API request, return a parsed JSON response """ headers['Accept'] = 'application/json' if signed: headers['X-MBX-APIKEY'] = self.key params['timestamp'] = tstamp() params['signature'] = self.sign(params) url = urllib.parse.urljoin(self.url, uri) resp = await client.request(method, url, params=params, headers=headers) # check for issues if resp.status == 418: # IP ban raise CException(f"Your IP is banned for {resp.headers['Retry-After']} seconds. Wait!") if resp.status == 429: # Rate limiting raise CException(f"Your IP is rate limited for ' + \ '{resp.headers['Retry-After']} seconds. Wait!") jresp = await resp.json() if not resp.ok: # other issues raise self.ApiError(f'Request for {url} failed\n{resp.status}: {jresp}', jresp) return resp.headers, jresp
async def quote_qty_from_usd(client: aiohttp.ClientSession, api: BinanceAPI, usd_symbol: dict) -> float: """ get quote quantity by its value in USD <usd_symbol>: a <quote>/BUSD or <quote>/USDT symbol """ if api.env.usd_value < 0: raise CException('USD quote value cannot be negative') if usd_symbol == {}: raise CException('No BUSD/USDT trade symbol found, aborting!') api.set_pair(usd_symbol) return await api.last_price(client)
def __init__(self, symbols: list, nklines: int, limits: KlineLimits): if nklines <= 0: raise CException(f'Bad kline count: {nklines}') self.nklines = nklines self.symbols = {} for symb in symbols: self.symbols[symb['symbol']] = KlineStorage(symb, limits)
def lock(self, bcoin: str): """ lock in a trade pair and notify main thread InvalidPair => bad pair/try again, Exception => abort on success notify a waiting thread to call `start` """ self.mut1.acquire() # reject if locked if self.locked: self.mut1.release() raise CException('Market operation is already running!') # retry if bad coin is turned in if bcoin not in self.pairs: self.mut1.release() raise InvalidPair( f'Trading pair {bcoin}/{self.env.qcoin} not found') self.locked = True self.mut1.release() # lock in a trading pair self.api.set_pair(self.pairs[bcoin]) self.use_oco = self.env.stop > -100 and self.api.pair['ocoAllowed'] if self.env.stop > -100 and not self.api.pair['ocoAllowed']: CColors.wprint( 'You set a stop price but this trading pair doesn\'t allow OCO trades!' ) # notify worker with self.cvar: self.ready = True self.cvar.notify()
async def sell_coins_limit(self, client: aiohttp.ClientSession) -> int: """ sell coins using a limit or OCO sell, return order ID """ # try OCO if suitable if self.use_oco: oid = await self.api.sell_coin_oco(client, self.bqty, self.tprice, self.sprice) if not oid: raise CException('OCO sell failed') CColors.cprint(f'[OCO SELL] target profit: {self.env.profit:.2f}%, ' + \ f'max loss: {-self.env.stop:.2f}%', CColors.OKGREEN) return oid # try limit sell oid = await self.api.sell_coin_limit(client, self.bqty, self.tprice) if not oid: raise CException('Limit sell failed') CColors.cprint(f'[LIMIT SELL] target profit: {self.env.profit:.2f}%', CColors.OKGREEN) return oid
async def cancel_limit(self, client: aiohttp.ClientSession, oid: int): """ attempts to cancel a limit order and subtract its fills """ if self.use_oco: succ, qty = await self.api.cancel_oco_order(client, oid) else: succ, qty = await self.api.cancel_order(client, oid) if not succ: raise CException('Unable to cancel limit/OCO order, ' + \ 'it is probably executed already!') self.bqty -= qty
async def check_sell_eligibility(self, client: aiohttp.ClientSession): """ check if you can sell with your strategy """ avg = await self.api.avg_price(client) if self.env.sell_type == SellType.MARKET: low, high = self.api.qty_bound(avg, True) else: # adjust profit/loss targets plow, phigh = self.api.price_bound(avg) self.tprice = min(self.tprice, phigh) low, high = self.api.qty_bound(self.tprice) if self.use_oco: self.sprice = max(self.sprice, plow) low, _ = self.api.qty_bound(self.sprice) if not low <= self.bqty <= high: raise CException( 'Sell quantity out of allowed bounds, cannot sell!') if not low * 1.1 <= self.bqty <= high * 0.9: CColors.wprint('Caution, you are nearing Binance\'s quantity limits, ' + \ 'high price fluctuations might prohibit your sell!')
def market_order_status(self, resp: dict) -> (float, float): """ print market order status and return executed qty and average fill price <resp>: response object from `req` """ bcoin, qcoin = self.pair['baseAsset'], self.pair['quoteAsset'] bqty, qqty = float(resp["executedQty"]), float(resp["cummulativeQuoteQty"]) CColors.iprint(f'Executed market order (status: {resp["status"]})') if resp['side'] == 'BUY': print(f'Bought {self.bfmt(bqty)} {bcoin} with {self.qfmt(qqty)} {qcoin}') else: print(f'Sold {self.bfmt(bqty)} {bcoin} for {self.qfmt(qqty)} {qcoin}') print('Fills:') avg_price = 0 for fill in resp['fills']: price, qty = float(fill['price']), float(fill['qty']) avg_price += price * qty / bqty print(f' {self.bfmt(qty)} {bcoin} at {self.qfmt(price)} {qcoin} ' + \ f'(fee: {fill["commission"]} {fill["commissionAsset"]})') print(f'Average fill price: {self.qfmt(avg_price)} {qcoin}') if not avg_price: raise CException('Average fill price seems to be zero') return bqty, avg_price
async def buy_from_source(client: aiohttp.ClientSession, api: BinanceAPI, src_symbols: dict, balances: dict, bqty: float): """ Buy <val> of quote coin """ for asset, (free, _) in balances.items(): if free == 0 or asset not in src_symbols: continue symbol = src_symbols[asset] api.set_pair(symbol) avg, last = await asyncio.gather(api.avg_price(client), api.last_price(client)) qqty = bqty * last if free < qqty: continue low, high = api.qty_bound(avg, True) bqty = max(low, bqty) qqty = bqty * last if free < qqty or bqty > high: continue succ, qty, _ = await api.buy_coin_market(client, qqty) if not succ: continue return qty raise CException('Failed to buy quote coin')
async def sub_multi(self, streams: list): """ subscribe to multiple streams """ if len(streams) > 1024: raise CException('Cannot subscribe to more than 1024 streams') async for val in self.read(f'stream?streams={"/".join(streams)}'): yield val
async def balances(self, client: aiohttp.ClientSession) -> float: """ return coin balances """ _, resp = await self.req(client, 'GET', 'account', {}, {}) if 'balances' not in resp: raise CException('Failed to query user data, bailing out') return resp['balances']
async def setup(api: BinanceAPI) -> (dict, float): """ main parameter setup return exchange info and quote amount to sell """ env = api.env def set_stdin(prompt: str, default): if not env.override: return default ret = input(prompt) return ret if ret else default prompt = f'Enter quote coin symbol (coin to trade for) [default: {env.qcoin}]: ' env.qcoin = set_stdin(prompt, env.qcoin).upper() async with aiohttp.ClientSession() as client: info, lbals = await asyncio.gather(api.exchange_info(client), api.balances(client)) symbols, src_symbols, usd_symbol = api.quote_symbols(info) bals = filter_balances(lbals, [env.qcoin] + env.src_coins) if env.qcoin not in bals: raise CException('Quote coin is invalid') qbal, qloc = bals[env.qcoin] del bals[env.qcoin] print( f'Your free balance for {env.qcoin} is {ffmt(qbal)} (locked: {ffmt(qloc)})' ) def_qty = env.buy_perc / 100 * qbal if env.usd_value: # fixed USD quote balance feature usd_price = await quote_qty_from_usd(client, api, usd_symbol) qqty = env.usd_value / usd_price while qqty > qbal: diff = 1.02 * (qqty - qbal) qbal += await buy_from_source(client, api, src_symbols, bals, diff) else: prompt = f'Enter {env.qcoin} amount to sell ' + \ f'[default: {ffmt(def_qty)} ({env.buy_perc:.2f}%)]: ' qqty = float(set_stdin(prompt, def_qty)) if qqty <= 0: raise CException( f'Cannot sell non-positive amount of {env.qcoin}') if qqty > qbal: raise CException('Insufficient quote balance') prompt = f'Enter sell type (LIMIT|MARKET) [default: {env.sell_type.name}]: ' env.sell_type = SellType(set_stdin(prompt, env.sell_type)) prompt = f'Enter desired profit in % [default: {env.profit:.2f}]: ' env.profit = float(set_stdin(prompt, env.profit)) if env.profit <= 0: CColors.wprint( 'You have set a non-positive profit. Proceeding may net you a loss!' ) prompt = f'Enter stop level in % to manage risk [default: {env.stop:.2f}]: ' env.stop = float(set_stdin(prompt, env.stop)) if not -100 <= env.stop < env.profit: raise CException('Stop percentage must be lower than profits!') print('---- SELECTED OPTIONS ----') print(f'Selected quote coin: {env.qcoin}') print(f'Selected quote amount to sell: {ffmt(qqty)} {env.qcoin} ' + \ f'(available: {ffmt(qbal)} {env.qcoin})') print(f'Selected sell strategy: {env.sell_type.name}') print(f'Selected target profit: {env.profit:.2f}%') print(f'Selected stop percentage: {env.stop:.2f}%') print('--------------------------') return symbols, qqty
async def sell_coins_market(self, client: aiohttp.ClientSession): """ sell coins using market sell """ succ, _, price = await self.api.sell_coin_market(client, self.bqty) if not succ: raise CException('Market sell failed') self.sell_market_report(price)