diff --git a/CHANGELOG.md b/CHANGELOG.md index 53e2a49..e4d2205 100755 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,12 @@ +## 3.0.19 - 2025-01-27 +### Fix +* `on_balance_update_ex`: calculating initial balance for opposite coin in Reverse cycle + +### Update +* `funds_rate_exporter.py`: optimized current price queries on `coinmarketcap` +* Functions for placing and canceling orders have been optimized +* Bump requirements + ## 3.0.18 - 2025-01-18 ### Update * Change dependency model for exchanges-wrapper diff --git a/martin_binance/__init__.py b/martin_binance/__init__.py index 1adf486..74b37ce 100755 --- a/martin_binance/__init__.py +++ b/martin_binance/__init__.py @@ -6,7 +6,7 @@ __author__ = "Jerry Fedorenko" __copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM" __license__ = "MIT" -__version__ = "3.0.18" +__version__ = "3.0.19" __maintainer__ = "Jerry Fedorenko" __contact__ = "https://github.com/DogsTailFarmer" diff --git a/martin_binance/executor.py b/martin_binance/executor.py index b0446c2..2a6df25 100755 --- a/martin_binance/executor.py +++ b/martin_binance/executor.py @@ -4,7 +4,7 @@ __author__ = "Jerry Fedorenko" __copyright__ = "Copyright © 2021-2025 Jerry Fedorenko aka VM" __license__ = "MIT" -__version__ = "3.0.17" +__version__ = "3.0.19" __maintainer__ = "Jerry Fedorenko" __contact__ = 'https://github.com/DogsTailFarmer' ################################################################## @@ -1343,6 +1343,10 @@ def calc_grid(self, over_price: Decimal, calc_avg_amount=True, **kwargs): } def event_grid_update(self): + if not self.orders_grid: + self.place_grid_part() + return + # do_it = False if ADAPTIVE_TRADE_CONDITION and self.stable_state() and not self.part_amount \ and (self.orders_grid or self.orders_hold): @@ -2177,20 +2181,6 @@ def place_limit_order_check( def on_new_ticker(self, ticker: Ticker) -> None: # print(f"on_new_ticker:{datetime.fromtimestamp(ticker.timestamp/1000)}: last_price: {ticker.last_price}") self.last_ticker_update = int(self.get_time()) - if not self.orders_grid and not self.orders_init and self.orders_hold: - _, _buy, _amount, _price = self.orders_hold.get_first() - tcm = self.get_trading_capability_manager() - if ((_buy and _price >= tcm.get_min_buy_price(ticker.last_price)) or - (not _buy and _price <= tcm.get_max_sell_price(ticker.last_price))): - waiting_order_id = self.place_limit_order_check( - _buy, - _amount, - _price, - check=True - ) - self.orders_init.append_order(waiting_order_id, _buy, _amount, _price) - del self.orders_hold.orders_list[0] - # if (self.shift_grid_threshold and self.last_shift_time and self.get_time() - self.last_shift_time > SHIFT_GRID_DELAY and ((self.cycle_buy and ticker.last_price >= self.shift_grid_threshold) @@ -2293,8 +2283,7 @@ def on_balance_update_ex(self, balance: Dict) -> None: self.update_sum_amount(-delta, -deposit_add) self.place_profit_order() - elif not self.reverse: - self.initial_first += delta + self.initial_first += delta else: if asset == self.f_currency: restart = True @@ -2342,8 +2331,7 @@ def on_balance_update_ex(self, balance: Dict) -> None: self.update_sum_amount(-deposit_add, -delta) self.place_profit_order() - elif not self.reverse: - self.initial_second += delta + self.initial_second += delta self.debug_output() @@ -2556,6 +2544,9 @@ def cancel_reverse_hold(self): self.initial_reverse_first = self.initial_reverse_second = O_DEC self.message_log("Cancel hold reverse cycle", color=Style.B_WHITE) + def order_init_exist(self, place_order_id: int): + return bool(self.orders_init.exist(place_order_id) or place_order_id == self.tp_wait_id) + def on_place_order_success(self, place_order_id: int, order: Order) -> None: # print(f"on_place_order_success.place_order_id: {place_order_id}") if self.orders_init.exist(place_order_id): @@ -2584,7 +2575,7 @@ def on_place_order_success(self, place_order_id: int, order: Order) -> None: else: self.place_grid_part() else: - self.message_log(f"Did not have waiting order id for {place_order_id}", logging.ERROR) + self.message_log(f"Did not have waiting order {place_order_id}", logging.ERROR) def on_place_order_error(self, place_order_id: int, error: str) -> None: # Check all orders on exchange if exists required diff --git a/martin_binance/service/funds_rate_exporter.py b/martin_binance/service/funds_rate_exporter.py index 79a9f7b..c2e363c 100755 --- a/martin_binance/service/funds_rate_exporter.py +++ b/martin_binance/service/funds_rate_exporter.py @@ -7,7 +7,7 @@ __author__ = "Jerry Fedorenko" __copyright__ = "Copyright © 2021 Jerry Fedorenko aka VM" __license__ = "MIT" -__version__ = "3.0.17" +__version__ = "3.0.19" __maintainer__ = "Jerry Fedorenko" __contact__ = 'https://github.com/DogsTailFarmer' @@ -145,34 +145,36 @@ def get_rate(_currency_rate) -> {}: replace = { 'UST': 'USDT', 'IOT': 'MIOTA', - 'LUNA': 'LUNC', - 'LUNA2': 'LUNA', 'TESTUSDT': 'USDT', 'TESTBTC': 'BTC' } headers = {'Accepts': 'application/json', 'X-CMC_PRO_API_KEY': API} session = Session() session.headers.update(headers) + buffer_rate = {} for currency in _currency_rate: _currency = replace.get(currency, currency) - price = -1 - parameters = {'amount': 1, 'symbol': 'USD', 'convert': _currency} - try: - response = session.get(URL, params=parameters) - except Exception as er: - print(er) - else: - if response.status_code == 429: - time.sleep(61) - request_delay *= 1.5 - try: - response = session.get(URL, params=parameters) - except Exception as er: - print(er) - if response.status_code == 200: - data = response.json() - price = data['data'][0]['quote'][_currency]['price'] or -1 + price = buffer_rate.get(_currency) + if price is None: + price = -1 + parameters = {'amount': 1, 'symbol': 'USD', 'convert': _currency} + try: + response = session.get(URL, params=parameters) + except Exception as er: + print(er) + else: + if response.status_code == 429: + time.sleep(61) + request_delay *= 1.5 + try: + response = session.get(URL, params=parameters) + except Exception as er: + print(er) + if response.status_code == 200: + data = response.json() + price = data['data'][0]['quote'][_currency]['price'] or -1 + buffer_rate[_currency] = price _currency_rate[currency] = price # time.sleep(request_delay) return _currency_rate diff --git a/martin_binance/strategy_base.py b/martin_binance/strategy_base.py index bb5ad48..760a8a4 100755 --- a/martin_binance/strategy_base.py +++ b/martin_binance/strategy_base.py @@ -4,7 +4,7 @@ __author__ = "Jerry Fedorenko" __copyright__ = "Copyright © 2021-2025 Jerry Fedorenko aka VM" __license__ = "MIT" -__version__ = "3.0.17" +__version__ = "3.0.19" __maintainer__ = "Jerry Fedorenko" __contact__ = "https://github.com/DogsTailFarmer" @@ -102,8 +102,6 @@ def __init__(self): self.funds = {} self.order_book = {} self.order_id = int(datetime.now().strftime("%f%S%M")) - self.wait_order_id = [] # List of placed orders for time-out detect - self.canceled_order_id = [] # List canceled orders for time-out detect self.trades = [] # List of trades associated with strategy (limit = TRADES_LIST_LIMIT) self.orders = {} # {int(id): Order(), } of orders associated with strategy self.tcm = None # TradingCapabilityManager @@ -164,8 +162,6 @@ def reset_vars(self): self.funds = {} self.order_book = {} self.order_id = int(datetime.now().strftime("%f%S%M")) - self.wait_order_id = [] # List of placed orders for time-out detect - self.canceled_order_id = [] # List canceled orders for time-out detect self.trades = [] # List of trades associated with strategy (limit = TRADES_LIST_LIMIT) self.orders = {} # Set of orders associated with strategy self.get_buffered_funds_last_time = self.get_time() @@ -258,14 +254,12 @@ def place_limit_order(self, buy: bool, amount: Decimal, price: Decimal) -> int: self.message_log(f"Send order id:{self.order_id} for {'BUY' if buy else 'SELL'}" f" {any2str(amount)} by {any2str(price)} = {any2str(amount * price)}", color=Style.B_YELLOW) - self.tasks_manage(self.place_limit_order_timeout(self.order_id)) self.tasks_manage(self.create_limit_order(self.order_id, buy, any2str(amount), any2str(price))) if self.exchange == 'huobi': time.sleep(0.02) return self.order_id def cancel_order(self, order_id: int, cancel_all=False) -> None: - self.tasks_manage(self.cancel_order_timeout(order_id)) self.tasks_manage(self.cancel_order_call(order_id, cancel_all)) def message_log(self, msg: str, log_level=logging.INFO, tlg=False, color=Style.WHITE, tlg_inline=False) -> None: @@ -781,8 +775,7 @@ async def fetch_order(self, _id: int, _client_order_id: str = None, _filled_upda log_level=logging.INFO, color=Style.GREEN) if result: return result - self.message_log(f"Can't get status for order {_id}({_client_order_id})", - log_level=logging.WARNING) + self.message_log(f"Can't get status for order {_id}({_client_order_id})", log_level=logging.WARNING) return {} async def on_funds_update(self): @@ -863,11 +856,7 @@ def open_orders_snapshot(self, ts=None): self.grid_buy.update({ts or int(time.time() * 1000): pd.Series(orders_buy)}) self.grid_sell.update({ts or int(time.time() * 1000): pd.Series(orders_sell)}) - async def cancel_order_call(self, _id: int, cancel_all=False, count=0): - if count == 0: - self.canceled_order_id.append(_id) - elif _id in self.canceled_order_id: - self.canceled_order_id.remove(_id) + async def cancel_order_call(self, _id: int, cancel_all=False): _fetch_order = False try: if prm.MODE in ('T', 'TC'): @@ -917,20 +906,11 @@ async def cancel_order_call(self, _id: int, cancel_all=False, count=0): res = await self.fetch_order(_id, _filled_update_call=True) if res.get('status') in ('CANCELED', 'EXPIRED_IN_MATCH'): await self.cancel_order_handler(_id, cancel_all) - elif res.get('status') == 'FILLED': - if _id in self.canceled_order_id: - self.canceled_order_id.remove(_id) - elif not res or res.get('status') in ('NEW', 'PARTIALLY_FILLED'): - await asyncio.sleep(HEARTBEAT * count) - if count <= TRY_LIMIT: - await self.cancel_order_call(_id, cancel_all=False, count=count + 1) - else: - self.on_cancel_order_error_string(_id, 'Cancel order try limit exceeded') + else: + self.on_cancel_order_error_string(_id, 'order not canceled') async def cancel_order_handler(self, _id, cancel_all): - if _id in self.canceled_order_id: - self.canceled_order_id.remove(_id) - self.message_log(f"Cancel order {_id} success", color=Style.GREEN) + self.message_log(f"Cancel order {_id} success", color=Style.GREEN) self.remove_from_orders_lists([_id]) self.on_cancel_order_success(_id, cancel_all=cancel_all) if prm.MODE == 'TC' and prm.SAVE_DS and self.start_collect: @@ -938,13 +918,6 @@ async def cancel_order_handler(self, _id, cancel_all): elif prm.MODE == 'S': await self.on_funds_update() - async def cancel_order_timeout(self, _id): - await asyncio.sleep(ORDER_TIMEOUT) - if _id in self.canceled_order_id: - self.canceled_order_id.remove(_id) - self.on_cancel_order_error_string(_id, 'Cancel order timeout') - # await asyncio.sleep(0) - async def transfer2master(self, symbol: str, amount: str): try: res = await self.send_request( @@ -1025,12 +998,6 @@ async def buffered_funds(self, print_info: bool = True): self.quote_asset: FundsEntry(self.funds[self.quote_asset])} self.on_new_funds(funds) - async def place_limit_order_timeout(self, _id): - await asyncio.sleep(ORDER_TIMEOUT) - if _id in self.wait_order_id: - self.wait_order_id.remove(_id) - self.on_place_order_error(_id, 'Place order timeout') - async def get_exchange_info(self, _request, _symbol): """ Refresh trading rules for pair every 10 mins @@ -1118,7 +1085,6 @@ async def on_klines_update(self, _klines: {str: Klines}): self.tasks_manage(self.aiter_candles(_klines, i), name='wss') async def create_limit_order(self, _id: int, buy: bool, amount: str, price: str) -> None: - self.wait_order_id.append(_id) _fetch_order = False msg = None try: @@ -1148,14 +1114,11 @@ async def create_limit_order(self, _id: int, buy: bool, amount: str, price: str) pass # Task cancellation should not be logged as an error except GRPCError as ex: status_code = ex.status - if status_code == Status.FAILED_PRECONDITION: - self.message_log(f"Order {_id}: {status_code.name}, {ex.message}") - if _id in self.wait_order_id: - # Supress call strategy handler - self.wait_order_id.remove(_id) + msg = f"Create order {_id}: {status_code.name}, {ex.message}" + if status_code in (Status.FAILED_PRECONDITION, Status.DEADLINE_EXCEEDED): + self.on_place_order_error(_id, msg) else: _fetch_order = True - msg = f"Create order {_id}: {status_code.name}, {ex.message}" except Exception as _ex: _fetch_order = True msg = f"Exception creating order {_id}: {_ex}" @@ -1176,14 +1139,14 @@ async def create_limit_order(self, _id: int, buy: bool, amount: str, price: str) async def create_order_handler(self, _id, result): # print(f"create_order_handler.result: {result}") - if _id in self.wait_order_id and not self.order_exist(result['orderId']): - self.wait_order_id.remove(_id) + if self.order_init_exist(_id) and not self.order_exist(result['orderId']): order = Order(result) + self.orders[order.id] = order + self.on_place_order_success(_id, order) self.message_log( f"Order placed {order.id}({result.get('clientOrderId') or _id}) for {result.get('side')}" f" {any2str(order.amount)} by {any2str(order.price)} = {any2str(order.amount * order.price)}", color=Style.GREEN) - self.orders[order.id] = order if prm.MODE == 'S': await self.on_funds_update() @@ -1199,8 +1162,6 @@ async def create_order_handler(self, _id, result): if prm.SAVE_DS: self.open_orders_snapshot() - self.on_place_order_success(_id, order) - async def on_balance_update(self): try: async for res in self.for_request(self.stub.on_balance_update, mr.MarketRequest, symbol=self.symbol): @@ -1598,6 +1559,9 @@ async def main(self, _symbol): # /NOSONAR print(f"Can't get active orders: {ex}") else: active_orders = list(map(json.loads, _active_orders.orders)) + for order in active_orders: + print(f"Order: {order['orderId']}, side: {order['side']}, amount: {order['origQty']}" + f" price:{order['price']}, status: {order['status']}") # Try load last strategy state from saved files last_state = load_last_state(prm.LAST_STATE_FILE) restore_state = bool(last_state) @@ -1615,7 +1579,7 @@ async def main(self, _symbol): # /NOSONAR cancel_orders = ast.literal_eval(json.loads(res.result)) print('Before start was canceled orders:') for i in cancel_orders: - print(f"Order:{i['orderId']}, side:{i['side']}," + print(f"Order: {i['orderId']}, side:{i['side']}," f" amount:{i['origQty']}, price:{i['price']}, status:{i['status']}") print(EQUAL_STR) except asyncio.CancelledError: @@ -1820,6 +1784,10 @@ def on_cancel_order_success(self, *args, **kwargs): def on_place_order_error(self, *args): raise NotImplementedError + @abstractmethod + def order_init_exist(self, *args): + raise NotImplementedError + @abstractmethod def on_place_order_success(self, *args): raise NotImplementedError diff --git a/pyproject.toml b/pyproject.toml index 4917004..0a006a4 100755 --- a/pyproject.toml +++ b/pyproject.toml @@ -17,7 +17,7 @@ dynamic = ["version", "description"] requires-python = ">=3.9" dependencies = [ - "exchanges-wrapper>=2.1.26", + "exchanges-wrapper>=2.1.27", "jsonpickle==3.3.0", "psutil==6.0.0", "requests==2.32.3", diff --git a/requirements.txt b/requirements.txt index 07e597d..43b088d 100755 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,4 @@ -exchanges-wrapper>=2.1.26 +exchanges-wrapper>=2.1.27 jsonpickle==3.3.0 psutil==6.0.0 requests==2.32.3