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()
def sell_market_report(self, sprice: float): """ report on a successful market sell <sprice>: average sell price """ profit = 100 * (sprice / self.bprice - 1) if profit >= 0: CColors.cprint(f'[MARKET SELL PROFIT] +{profit:.2f}%', CColors.OKGREEN) else: CColors.cprint(f'[MARKET SELL LOSS] {profit:.2f}%', CColors.FAIL)
async def read(self, uri: str): """ subscribe to a single stream and yield data on reception """ url = urllib.parse.urljoin(self.url, uri) while True: try: async with websockets.connect(url, ssl=True) as wsock: while True: yield json.loads(await wsock.recv()) except websockets.exceptions.WebSocketException as exc: CColors.wprint(f'WebSockets error, attempting to reconnect: {exc}')
async def cancel_order(self, client: aiohttp.ClientSession, order_id: int) -> (bool, float): """ Cancel a regular order, return success and executed quantity """ params = { 'symbol': self.pair['symbol'], 'orderId': order_id } try: _, resp = await self.req(client, 'DELETE', 'order', params, {}) CColors.iprint(f'Canceled limit order #{order_id} (status: {resp["status"]})') return True, float(resp['executedQty']) except BinanceAPI.ApiError as exc: CColors.eprint(f'Order cancel failed with {exc.data}') return False, 0
async def bailout(self): """ bailout and immediately cancel current order and sell coins """ if not self.allow_bailout: return async with aiohttp.ClientSession() as client: # Cancel limit orders if any if self.oid: await self.cancel_limit(client, self.oid) # Sell everything on market immediately CColors.wprint('Selling on market immediately!') succ, _, price = await self.api.sell_coin_market(client, self.bqty) if succ: self.sell_market_report(price)
async def cancel_oco_order(self, client: aiohttp.ClientSession, order_list_id: int) -> (bool, float): """ Cancel an OCO order, return success and executed quantity """ params = { 'symbol': self.pair['symbol'], 'orderListId': order_list_id } try: _, resp = await self.req(client, 'DELETE', 'orderList', params, {}) CColors.iprint(f'Canceled OCO #{order_list_id} (status: {resp["listOrderStatus"]})') qty = sum((float(rep['executedQty']) for rep in resp['orderReports'])) return True, qty except BinanceAPI.ApiError as exc: CColors.eprint(f'OCO order cancel failed with {exc.data}') return False, 0
async def start(self): """ start trading, only call this method after locking in a pair """ CColors.iprint( f'Market manager started with pair {self.api.pair["symbol"]}') async with aiohttp.ClientSession() as client: # buy <bcoin> immediately at market price, get qty. and avg. price succ, self.bqty, self.bprice = await self.api.buy_coin_market( client, self.qqty) if not succ: return self.tprice = (1 + self.env.profit / 100) * self.bprice self.sprice = (1 + self.env.stop / 100) * self.bprice # check sell amount eligibility at this point await self.check_sell_eligibility(client) # sell bought <bcoin> with <profit>% profit await self.sell_coins(client)
def analyze_mins(self, mins: int, color: CColors): """ analyze symbol per minutes """ klast = self.klines[-1] kprev = self.klines[-1 - mins] dcl = (klast.price_cl / kprev.price_cl - 1) * 100 rvol = self.vol_ratio(mins) if mins not in self.nalarm: self.nalarm[mins] = 0 if rvol and dcl > self.limits.price_chg_min and \ (dcl > self.limits.price_thr or rvol > self.limits.vol_thr and dcl) > 0: mult = self.nalarm[mins] + 1 CColors.cprint(f'{self.prefix(mult)} up {dcl:.2f}% ' + \ f'in {mins}min, vol. chg. {rvol:.2f}%', color) else: mult = 0 self.nalarm[mins] = mult
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 sell_coin_market(self, client: aiohttp.ClientSession, bqty: float) -> (bool, float, float): """ sell <bqty> of <bcoin> for <qcoin> at market price return success, amount of <qcoin> purchased and average trade price """ bcoin = self.pair['baseAsset'] CColors.cprint(f'[MARKET SELL] Selling {self.bfmt_mkt(bqty)} {bcoin}', CColors.WARNING) params = { 'symbol': self.pair['symbol'], 'side': 'SELL', 'type': 'MARKET', 'quantity': self.bfmt_mkt(bqty), # sell <bqty> of <bcoin> } try: _, resp = await self.req(client, 'POST', 'order', params, {}) return (True, *self.market_order_status(resp)) except BinanceAPI.ApiError as exc: CColors.eprint(f'Market sell failed with {exc.data}') return False, 0, 0
async def buy_coin_market(self, client: aiohttp.ClientSession, qqty: float) -> (bool, float, float): """ buy <bcoin> with <qqty> of <qcoin> at market price return amount of <bcoin> purchased and average trade price """ bcoin, qcoin = self.pair['baseAsset'], self.pair['quoteAsset'] msg = f'[MARKET BUY] Buying {bcoin} with {self.qfmt(qqty)} {qcoin}' CColors.cprint(msg, CColors.WARNING) params = { 'symbol': self.pair['symbol'], 'side': 'BUY', 'type': 'MARKET', 'quoteOrderQty': self.qfmt(qqty), # buy with <qqty> of <qcoin> } try: _, resp = await self.req(client, 'POST', 'order', params, {}) return (True, *self.market_order_status(resp)) except BinanceAPI.ApiError as exc: CColors.eprint(f'Market buy failed with {exc.data}') return False, 0, 0
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 main(): """ Entrypoint """ klen = 241 # 4h remanence thresh = 5, 900 # 5% positive price fluctuation, 900% positive volume fluctuation min_vol = 0.1 # do not alert under 0.1 executed quote volume (tuned for BTC) min_chg = 0.1 # minimum acceptable price change, regardless of volume limits = KlineLimits(*thresh, min_vol, min_chg) env = Environment('.env') api = BinanceAPI(env) wapi = BinanceWSAPI(env) # fetch symbols to track qsymbols = await quote_symbols(api) qvalues = qsymbols.values() symb_names = [symb['symbol'] for symb in qvalues] qlen = len(qvalues) CColors.iprint( f'DawnSpotter online.\nTracking {qlen} pairs: {", ".join(symb_names)}') # prepare the kline data structure manager = KlineManager(qvalues, klen, limits) # Pull historical data from the API maxrun = 1200 // (qlen + 41) print( f'Pulling historical data from REST API, do not rerun this more than {maxrun}x/min!' ) async with aiohttp.ClientSession() as client: coros = (api.last_klines(client, '1m', klen, symbol) for symbol in qvalues) preconf = await asyncio.gather(*coros) manager.fill(zip(symb_names, preconf)) # read trade data from WS print('Updating data from WebSockets...') async for tdata in wapi.klines_bulk(symb_names, '1m'): manager.update(tdata['data']['k'])
async def sell_coins(self, client: aiohttp.ClientSession): """ coin selling logic """ bcoin = self.api.pair['baseAsset'] lprice = 0 # last traded price if self.env.sell_type == SellType.LIMIT: # put a limit order on the book immediately self.oid = await self.sell_coins_limit(client) if not self.env.bailout: return self.allow_bailout = self.env.bailout async for tdata in self.wapi.agg_trades_single( self.api.pair['symbol']): # last traded price is the current market price _lprice = float(tdata['p']) if _lprice == lprice: continue lprice = _lprice # calculate estimated profit eprofit = 100 * (lprice / self.bprice - 1) if eprofit >= 0: CColors.cprint(f'[+{eprofit:.2f}%] 1 {bcoin} = ' + \ f'{self.api.qfmt(lprice)} {self.env.qcoin}', CColors.OKGREEN) else: CColors.cprint(f'[{eprofit:.2f}%] 1 {bcoin} = ' + \ f'{self.api.qfmt(lprice)} {self.env.qcoin}', CColors.FAIL) if self.env.sell_type == SellType.LIMIT: # limit orders are here just to be able to bailout continue # sell if forced or profit/loss limits are triggered if lprice > self.tprice or lprice < self.sprice: self.allow_bailout = False await self.sell_coins_market(client) return
def coin_from_stdin(manager: MarketManager): """ fetch a coin name from stdin """ prompt = CColors.cstr('Enter base coin symbol (coin to buy and sell): ', CColors.WARNING) while True: try: bcoin = input(prompt).upper() except Exception: break if not bcoin: continue try: manager.lock(bcoin) return except InvalidPair as exc: print(str(exc)) except Exception as exc: print(str(exc)) break with manager.cvar: manager.cvar.notify()
async def sell_coin_limit(self, client: aiohttp.ClientSession, bqty: float, price: float) -> int: """ sell <bqty> of <bcoin>, return order ID (0 = fail) """ bcoin = self.pair['baseAsset'] CColors.cprint(f'[LIMIT SELL] Selling {self.bfmt(bqty)} {bcoin} at {self.qfmt(price)}', CColors.WARNING) params = { 'symbol': self.pair['symbol'], 'side': 'SELL', 'type': 'LIMIT', 'timeInForce': 'GTC', # good till cancelled 'quantity': self.bfmt(bqty), 'price': self.qfmt(price), } try: _, resp = await self.req(client, 'POST', 'order', params, {}) CColors.iprint(f'Executed limit sell order (status: {resp["status"]})') except BinanceAPI.ApiError as exc: CColors.eprint(f'Limit sell failed with {exc.data}') return 0 return resp['orderId']
async def sell_coin_oco(self, client: aiohttp.ClientSession, bqty: float, price: float, sprice: float) -> int: """ sell <bqty> of <bcoin> as OCO, return order ID (0 = fail) """ bcoin = self.pair['baseAsset'] msg = f'[OCO SELL] Selling {self.bfmt(bqty)} {bcoin} at {self.qfmt(price)}' + \ f', stop: {self.qfmt(sprice)}' CColors.cprint(msg, CColors.WARNING) params = { 'symbol': self.pair['symbol'], 'side': 'SELL', 'quantity': self.bfmt(bqty), 'price': self.qfmt(price), 'stopPrice': self.qfmt(sprice), 'stopLimitPrice': self.qfmt(sprice * 0.98), 'stopLimitTimeInForce': 'GTC' # good till cancelled } try: _, resp = await self.req(client, 'POST', 'order/oco', params, {}) CColors.iprint(f'Executed OCO order (status: {resp["listOrderStatus"]})') except BinanceAPI.ApiError as exc: CColors.eprint(f'OCO sell failed with {exc.data}') return 0 return resp['orderListId']
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