From 3914250caf4afb7550ceadd8ad250aa69ee3dfd6 Mon Sep 17 00:00:00 2001 From: Zichao Lin Date: Sat, 19 Jul 2025 00:39:11 +0800 Subject: [PATCH] It worked --- .gitignore | 3 + .pylintrc | 4 + main.py | 845 ++++++++++++++++++++++++++++++++++++++++++++++++ mexc_spot_v3.py | 535 ++++++++++++++++++++++++++++++ 4 files changed, 1387 insertions(+) create mode 100644 .gitignore create mode 100644 .pylintrc create mode 100644 main.py create mode 100644 mexc_spot_v3.py diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..16fee6c --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +output/ +config/ +__pycache__ \ No newline at end of file diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..0399df7 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,4 @@ +[MASTER] +disable= + broad-exception-caught, + line-too-long \ No newline at end of file diff --git a/main.py b/main.py new file mode 100644 index 0000000..ba19697 --- /dev/null +++ b/main.py @@ -0,0 +1,845 @@ +import time +import logging +from typing import Dict, List, Optional, Tuple +from dataclasses import dataclass + +import mexc_spot_v3 + +# 配置日志 +CUSTOM_DEBUG_LEVEL = 15 +logging.addLevelName(CUSTOM_DEBUG_LEVEL, "CUSTOM_DEBUG") + + +class CustomLogger(logging.getLoggerClass()): + def custom_debug(self, msg, *args, **kwargs): + if self.isEnabledFor(CUSTOM_DEBUG_LEVEL): + self._log(CUSTOM_DEBUG_LEVEL, msg, args, **kwargs) + + +logging.setLoggerClass(CustomLogger) +logging.basicConfig( + level=CUSTOM_DEBUG_LEVEL, + format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", + handlers=[ + logging.FileHandler("output/mexc_spot_grid_bot.log", encoding="utf-8"), + logging.StreamHandler(), + ], +) +logger = logging.getLogger(__name__) +logger.custom_debug = lambda msg, *args, **kwargs: logger._log( # pylint: disable=W0212 + CUSTOM_DEBUG_LEVEL, msg, args, **kwargs +) + + +@dataclass +class Order: + order_id: str + price: float + quantity: float + side: str # 'BUY' or 'SELL' + status: str # 'NEW', 'FILLED', 'CANCELED' + filled_time: Optional[float] = None # 订单成交时间戳 + + +class GridTradingBot: + def __init__(self, conf: Dict): + """ + 初始化网格交易机器人 + + 参数: + conf (Dict): 配置字典,包含: + - symbol: 交易对 (如 'BTCUSDC') + - grid_percentage: 每格百分比 (如 0.005 表示 0.5%) + - grid_count: 单边网格数量 (如 3) + - order_amount: 每单加密货币数量 (如 0.00001 BTC) + - min_order_value: 最小挂单价值 (如 1 USDC) + - reserve_base: 保留的基础货币数量 (如 1 BTC) + - reserve_quote: 保留的报价货币数量 (如 100 USDC) + """ + logger.custom_debug( + "[GridTradingBot.__init__] Initializing GridTradingBot with config: %r", + conf, + ) + self.config = conf + self.symbol = conf["symbol"] + self.grid_percentage = conf["grid_percentage"] + self.grid_count = conf["grid_count"] + self.order_amount = conf["order_amount"] + self.min_order_value = conf["min_order_value"] + self.reserve_base = conf["reserve_base"] + self.reserve_quote = conf["reserve_quote"] + + # 当前活跃的订单 + self.active_orders: Dict[str, Order] = {} # order_id -> Order + + # 当前网格范围 + self.current_buy_levels = 0 + self.current_sell_levels = 0 + + # 初始化API + logger.custom_debug("[GridTradingBot.__init__] Initializing MEXC API clients") + self.api_trade = mexc_spot_v3.mexc_trade() + self.api_market = mexc_spot_v3.mexc_market() + + # 运行标志 + self.running = False + logger.info("[GridTradingBot.__init__] GridTradingBot initialized successfully") + + def api_get_price(self) -> float: + """获取当前市场价格""" + logger.custom_debug( + "[GridTradingBot.api_get_price] Fetching current market price for symbol: %s", + self.symbol, + ) + try: + ticker = self.api_market.get_price({"symbol": self.symbol}) + price = float(ticker["price"]) + logger.custom_debug( + "[GridTradingBot.api_get_price] Current market price: %f", price + ) + return price + except Exception as e: + logger.error( + "[GridTradingBot.api_get_price] Failed to get price for %s: %s", + self.symbol, + str(e), + ) + raise + + def api_get_balances(self) -> Tuple[float, float]: + """获取基础货币和报价货币的可用余额(不包括冻结金额)""" + base_currency = self.symbol[:-4] # 假设报价货币是4个字母,如USDC + quote_currency = self.symbol[-4:] + base_available = 0.0 + quote_available = 0.0 + + logger.custom_debug( + "[GridTradingBot.api_get_balances] Fetching balances for %s and %s", + base_currency, + quote_currency, + ) + try: + balances = self.api_trade.get_account_info() + for balance in balances.get("balances", []): + if balance["asset"] == base_currency: + base_available = float(balance["free"]) + logger.custom_debug( + "[GridTradingBot.api_get_balances] Base currency (%s) available: %f", + base_currency, + base_available, + ) + elif balance["asset"] == quote_currency: + quote_available = float(balance["free"]) + logger.custom_debug( + "[GridTradingBot.api_get_balances] Quote currency (%s) available: %f", + quote_currency, + quote_available, + ) + + logger.custom_debug( + "[GridTradingBot.api_get_balances] Final balances - base: %f, quote: %f", + base_available, + quote_available, + ) + return base_available, quote_available + except Exception as e: + logger.error( + "[GridTradingBot.api_get_balances] Failed to get balances: %s", str(e) + ) + raise + + def calculate_order_value(self, price: float) -> float: + """计算订单价值 (价格 * 数量)""" + value = price * self.order_amount + logger.custom_debug( + "[GridTradingBot.calculate_order_value] Calculated order value: price=%f * amount=%f = %f", + price, + self.order_amount, + value, + ) + return value + + def is_order_value_valid(self, price: float) -> bool: + """检查订单价值是否满足最小挂单要求""" + value = self.calculate_order_value(price) + is_valid = value >= self.min_order_value + logger.custom_debug( + "[GridTradingBot.is_order_value_valid] Order value validation: %f >= %f? %s", + value, + self.min_order_value, + is_valid, + ) + return is_valid + + def calculate_grid_price(self, base_price: float, level: int, side: str) -> float: + """ + 计算网格价格 + + 参数: + base_price: 基础价格 + level: 网格级别 (正数) + side: 'BUY' 或 'SELL' + + 返回: + 计算后的价格 + """ + logger.custom_debug( + "[GridTradingBot.calculate_grid_price] Calculating grid price: base=%f, level=%d, side=%s", + base_price, + level, + side, + ) + + if side == "BUY": + price = base_price * (1 - self.grid_percentage) ** level + logger.custom_debug( + "[GridTradingBot.calculate_grid_price] Calculated BUY price: %f * (1 - %f)^%d = %f", + base_price, + self.grid_percentage, + level, + price, + ) + return price + elif side == "SELL": + price = base_price * (1 + self.grid_percentage) ** level + logger.custom_debug( + "[GridTradingBot.calculate_grid_price] Calculated SELL price: %f * (1 + %f)^%d = %f", + base_price, + self.grid_percentage, + level, + price, + ) + return price + else: + logger.error( + "[GridTradingBot.calculate_grid_price] Invalid side for grid price calculation: %s", + side, + ) + raise ValueError(f"Invalid side: {side}") + + def api_place_order(self, price: float, side: str) -> Optional[str]: + """ + 下订单 + + 参数: + price: 价格 + side: 'BUY' 或 'SELL' + + 返回: + 订单ID (如果下单成功) 或 None (如果失败) + """ + logger.custom_debug( + "[GridTradingBot.api_place_order] Attempting to place %s order at price: %f", + side, + price, + ) + + if not self.is_order_value_valid(price): + logger.error( + "[GridTradingBot.api_place_order] Order value too small: price=%f, amount=%f, value=%f < min=%f", + price, + self.order_amount, + price * self.order_amount, + self.min_order_value, + ) + return None + + try: + # logger.custom_debug( + # "[GridTradingBot.api_place_order] Sending order request to exchange" + # ) + order_detail = self.api_trade.post_order( + { + "symbol": self.symbol, + "side": side, + "type": "LIMIT", + "price": price, + "quantity": self.order_amount, + } + ) + + order_id = order_detail.get("orderId") + if order_id: + logger.info( + "[GridTradingBot.api_place_order] Successfully placed %s order - price: %f, amount: %f, order_id: %s", + side, + price, + self.order_amount, + order_id, + ) + return order_id + else: + logger.error( + "[GridTradingBot.api_place_order] Failed to place %s order at %f: %r", + side, + price, + order_detail, + ) + except Exception as e: + logger.error( + "[GridTradingBot.api_place_order] Exception occurred while placing %s order at %f: %s", + side, + price, + str(e), + ) + return None + + def api_cancel_order(self, order_id: str): + """取消订单""" + logger.custom_debug( + "[GridTradingBot.api_cancel_order] Attempting to cancel order: %s", order_id + ) + try: + self.api_trade.delete_order({"symbol": self.symbol, "orderId": order_id}) + logger.info( + "[GridTradingBot.api_cancel_order] Successfully canceled order: %s", + order_id, + ) + except Exception as e: + logger.error( + "[GridTradingBot.api_cancel_order] Failed to cancel order %s: %s", + order_id, + str(e), + ) + + def api_cancel_all_orders(self): + """取消所有活跃订单""" + logger.custom_debug( + "[GridTradingBot.api_cancel_all_orders] Attempting to cancel all open orders for symbol: %s", + self.symbol, + ) + try: + self.api_trade.delete_openorders({"symbol": self.symbol}) + logger.info( + "[GridTradingBot.api_cancel_all_orders] Successfully canceled all open orders for %s", + self.symbol, + ) + except Exception as e: + logger.error( + "[GridTradingBot.api_cancel_all_orders] Failed to cancel all open orders: %s", + str(e), + ) + + def api_update_order_statuses(self): + """更新所有活跃订单的状态""" + logger.custom_debug( + "[GridTradingBot.api_update_order_statuses] Updating status for %d active orders", + len(self.active_orders), + ) + + for order_id in list(self.active_orders.keys()): + try: + logger.custom_debug( + "[GridTradingBot.api_update_order_statuses] Checking status for order: %s", + order_id, + ) + order_info = self.api_trade.get_order( + {"symbol": self.symbol, "orderId": order_id} + ) + + # 更新订单状态 + old_status = self.active_orders[order_id].status + new_status = order_info["status"] + self.active_orders[order_id].status = new_status + + # 记录状态变化 + if old_status != new_status: + logger.info( + "[GridTradingBot.api_update_order_statuses] Order %s status changed from %s to %s", + order_id, + old_status, + new_status, + ) + + # 如果订单已完成,记录成交时间 + if new_status == "FILLED": + self.active_orders[order_id].filled_time = time.time() + logger.custom_debug( + "[GridTradingBot.api_update_order_statuses] Order %s filled at %f", + order_id, + self.active_orders[order_id].filled_time, + ) + + # 如果订单已完成或已取消,从活跃订单中移除 + # if new_status in ["FILLED", "CANCELED"]: + if new_status in ["CANCELED"]: + logger.custom_debug( + "[GridTradingBot.api_update_order_statuses] Removing order %s from active orders (status: %s)", + order_id, + new_status, + ) + self.active_orders.pop(order_id) + + except Exception as e: + logger.error( + "[GridTradingBot.api_update_order_statuses] Failed to get status for order %s: %s", + order_id, + str(e), + ) + + def get_active_orders_by_side(self, side: str) -> List[Order]: + """获取指定方向的所有活跃订单""" + orders = [order for order in self.active_orders.values() if order.side == side] + logger.custom_debug( + "[GridTradingBot.get_active_orders_by_side] Found %d active %s orders", + len(orders), + side, + ) + return orders + + def get_extreme_prices(self) -> Tuple[Optional[float], Optional[float]]: + """ + 获取当前最极端的买单和卖单价格 + + 返回: + (最低买单价格, 最高卖单价格) 如果没有订单则为 (None, None) + """ + buy_orders = self.get_active_orders_by_side("BUY") + sell_orders = self.get_active_orders_by_side("SELL") + + lowest_buy = min([order.price for order in buy_orders]) if buy_orders else None + highest_sell = ( + max([order.price for order in sell_orders]) if sell_orders else None + ) + + logger.custom_debug( + "[GridTradingBot.get_extreme_prices] Extreme prices - lowest buy: %s, highest sell: %s", + lowest_buy, + highest_sell, + ) + return lowest_buy, highest_sell + + def initialize_grid(self): + """初始化网格""" + logger.info( + "[GridTradingBot.initialize_grid] Initializing grid for symbol: %s", + self.symbol, + ) + + try: + market_price = self.api_get_price() + logger.info( + "[GridTradingBot.initialize_grid] Current market price: %f", + market_price, + ) + + # 初始下单:买1和卖1 + buy_price = self.calculate_grid_price(market_price, 1, "BUY") + sell_price = self.calculate_grid_price(market_price, 1, "SELL") + logger.custom_debug( + "[GridTradingBot.initialize_grid] Initial buy price: %f, sell price: %f", + buy_price, + sell_price, + ) + + # 检查余额并下单 + base_balance, quote_balance = self.api_get_balances() + logger.info( + "[GridTradingBot.initialize_grid] Current balances - base: %f, quote: %f", + base_balance, + quote_balance, + ) + + # 下单买1 + if quote_balance > self.reserve_quote: + logger.custom_debug( + "[GridTradingBot.initialize_grid] Quote balance sufficient (%f > %f), checking order value", + quote_balance, + self.reserve_quote, + ) + if self.is_order_value_valid(buy_price): + logger.custom_debug( + "[GridTradingBot.initialize_grid] Order value valid, placing buy order" + ) + buy_order_id = self.api_place_order(buy_price, "BUY") + if buy_order_id: + self.active_orders[buy_order_id] = Order( + order_id=buy_order_id, + price=buy_price, + quantity=self.order_amount, + side="BUY", + status="NEW", + ) + self.current_buy_levels = 1 + logger.info( + "[GridTradingBot.initialize_grid] Initial buy order placed at %f", + buy_price, + ) + else: + logger.error( + "[GridTradingBot.initialize_grid] Initial buy order value too small: %f < %f", + buy_price * self.order_amount, + self.min_order_value, + ) + else: + logger.error( + "[GridTradingBot.initialize_grid] Insufficient quote balance for initial buy: %f <= %f", + quote_balance, + self.reserve_quote, + ) + + # 下单卖1 + if base_balance > self.reserve_base: + logger.custom_debug( + "[GridTradingBot.initialize_grid] Base balance sufficient (%f > %f), checking order value", + base_balance, + self.reserve_base, + ) + if self.is_order_value_valid(sell_price): + logger.custom_debug( + "[GridTradingBot.initialize_grid] Order value valid, placing sell order" + ) + sell_order_id = self.api_place_order(sell_price, "SELL") + if sell_order_id: + self.active_orders[sell_order_id] = Order( + order_id=sell_order_id, + price=sell_price, + quantity=self.order_amount, + side="SELL", + status="NEW", + ) + self.current_sell_levels = 1 + logger.info( + "[GridTradingBot.initialize_grid] Initial sell order placed at %f", + sell_price, + ) + else: + logger.error( + "[GridTradingBot.initialize_grid] Initial sell order value too small: %f < %f", + sell_price * self.order_amount, + self.min_order_value, + ) + else: + logger.error( + "[GridTradingBot.initialize_grid] Insufficient base balance for initial sell: %f <= %f", + base_balance, + self.reserve_base, + ) + + for _ in range(1, self.grid_count): + self.extend_grid() + + except Exception as e: + logger.error( + "[GridTradingBot.initialize_grid] Failed to initialize grid: %s", str(e) + ) + raise + + def extend_grid(self): + """扩展网格,保证两侧都有指定个数的挂单""" + logger.custom_debug( + "[GridTradingBot.extend_grid] Extending grid - current buy levels: %d/%d, sell levels: %d/%d", + self.current_buy_levels, + self.grid_count, + self.current_sell_levels, + self.grid_count, + ) + + try: + lowest_buy, highest_sell = self.get_extreme_prices() + base_balance, quote_balance = self.api_get_balances() + + # 扩展买单网格 (向下) + if lowest_buy is not None and self.current_buy_levels < self.grid_count: + new_buy_price = self.calculate_grid_price(lowest_buy, 1, "BUY") + logger.custom_debug( + "[GridTradingBot.extend_grid] Extending buy grid - current lowest: %f, new price: %f", + lowest_buy, + new_buy_price, + ) + + if quote_balance > self.reserve_quote: + logger.custom_debug( + "[GridTradingBot.extend_grid] Quote balance sufficient (%f > %f), checking order value", + quote_balance, + self.reserve_quote, + ) + if self.is_order_value_valid(new_buy_price): + logger.custom_debug( + "[GridTradingBot.extend_grid] Order value valid, placing extended buy order" + ) + buy_order_id = self.api_place_order(new_buy_price, "BUY") + if buy_order_id: + self.active_orders[buy_order_id] = Order( + order_id=buy_order_id, + price=new_buy_price, + quantity=self.order_amount, + side="BUY", + status="NEW", + ) + self.current_buy_levels += 1 + logger.info( + "[GridTradingBot.extend_grid] Extended buy grid to level %d at price %f", + self.current_buy_levels, + new_buy_price, + ) + else: + logger.error( + "[GridTradingBot.extend_grid] Extended buy order value too small: %f < %f", + new_buy_price * self.order_amount, + self.min_order_value, + ) + else: + logger.error( + "[GridTradingBot.extend_grid] Insufficient quote balance for extended buy: %f <= %f", + quote_balance, + self.reserve_quote, + ) + + # 扩展卖单网格 (向上) + if highest_sell is not None and self.current_sell_levels < self.grid_count: + new_sell_price = self.calculate_grid_price(highest_sell, 1, "SELL") + logger.custom_debug( + "[GridTradingBot.extend_grid] Extending sell grid - current highest: %f, new price: %f", + highest_sell, + new_sell_price, + ) + + if base_balance > self.reserve_base: + logger.custom_debug( + "[GridTradingBot.extend_grid] Base balance sufficient (%f > %f), checking order value", + base_balance, + self.reserve_base, + ) + if self.is_order_value_valid(new_sell_price): + logger.custom_debug( + "[GridTradingBot.extend_grid] Order value valid, placing extended sell order" + ) + sell_order_id = self.api_place_order(new_sell_price, "SELL") + if sell_order_id: + self.active_orders[sell_order_id] = Order( + order_id=sell_order_id, + price=new_sell_price, + quantity=self.order_amount, + side="SELL", + status="NEW", + ) + self.current_sell_levels += 1 + logger.info( + "[GridTradingBot.extend_grid] Extended sell grid to level %d at price %f", + self.current_sell_levels, + new_sell_price, + ) + else: + logger.error( + "[GridTradingBot.extend_grid] Extended sell order value too small: %f < %f", + new_sell_price * self.order_amount, + self.min_order_value, + ) + else: + logger.error( + "[GridTradingBot.extend_grid] Insufficient base balance for extended sell: %f <= %f", + base_balance, + self.reserve_base, + ) + + except Exception as e: + logger.error( + "[GridTradingBot.extend_grid] Failed to extend grid: %s", str(e) + ) + raise + + def adjust_grid(self): + """调整网格 - 优化后的对称逻辑: + 买单成交时: + 1. 取消最高价卖单 + 2. 在卖单侧挂一个更低价的卖单(卖0) + 3. 在买单侧挂一个更低价的买单(买n) + 卖单成交时: + 1. 取消最低价买单 + 2. 在买单侧挂一个更高价的买单(买0) + 3. 在卖单侧挂一个更高价的卖单(卖n)""" + logger.custom_debug( + "[GridTradingBot.adjust_grid] Adjusting grid with symmetric logic" + ) + + try: + # 获取当前所有订单的快照 + current_order_ids = set(self.active_orders.keys()) + + # 找出新成交的订单(之前存在且现在状态为FILLED) + newly_filled_orders = [ + order + for order in self.active_orders.values() + if order.order_id in current_order_ids and order.status == "FILLED" + ] + + if not newly_filled_orders: + logger.custom_debug( + "[GridTradingBot.adjust_grid] No newly filled orders found" + ) + return + + # 处理每个新成交的订单 + for filled_order in newly_filled_orders: + filled_side = filled_order.side + filled_price = filled_order.price + filled_id = filled_order.order_id + self.active_orders.pop(filled_id, None) + logger.info( + "[GridTradingBot.adjust_grid] Processing newly filled %s order at price %f", + filled_side, + filled_price, + ) + + # 获取当前活跃订单 + active_buys = [ + o + for o in self.active_orders.values() + if o.side == "BUY" and o.status == "NEW" + ] + active_sells = [ + o + for o in self.active_orders.values() + if o.side == "SELL" and o.status == "NEW" + ] + + if filled_side == "BUY" and active_sells: + # 买单成交时的处理逻辑 + self.current_buy_levels -= 1 + # 1. 取消最高价卖单 + highest_sell = max(active_sells, key=lambda x: x.price) + self.api_cancel_order(highest_sell.order_id) + self.active_orders.pop(highest_sell.order_id, None) + logger.info( + "[GridTradingBot.adjust_grid] Cancelled highest SELL order at %f (order_id: %s)", + highest_sell.price, + highest_sell.order_id, + ) + # 2. 在卖单侧挂一个更低价的卖单(卖0) + # 使用最近成交的买单价格作为基准 + new_sell_price = self.calculate_grid_price(filled_price, 1, "SELL") + # 检查余额和订单价值 + base_balance, _ = self.api_get_balances() + if base_balance > self.reserve_base and self.is_order_value_valid( + new_sell_price + ): + sell_order_id = self.api_place_order(new_sell_price, "SELL") + if sell_order_id: + self.active_orders[sell_order_id] = Order( + order_id=sell_order_id, + price=new_sell_price, + quantity=self.order_amount, + side="SELL", + status="NEW", + ) + logger.info( + "[GridTradingBot.adjust_grid] Placed new lower SELL order at %f (order_id: %s)", + new_sell_price, + sell_order_id, + ) + + elif filled_side == "SELL" and active_buys: + # 卖单成交时的处理逻辑 + self.current_sell_levels -= 1 + # 1. 取消最低价买单 + lowest_buy = min(active_buys, key=lambda x: x.price) + self.api_cancel_order(lowest_buy.order_id) + self.active_orders.pop(lowest_buy.order_id, None) + + logger.info( + "[GridTradingBot.adjust_grid] Cancelled lowest BUY order at %f (order_id: %s)", + lowest_buy.price, + lowest_buy.order_id, + ) + # 2. 在买单侧挂一个更高价的买单(买0) + # 使用最近成交的卖单价格作为基准 + new_buy_price = self.calculate_grid_price(filled_price, 1, "BUY") + # 检查余额和订单价值 + _, quote_balance = self.api_get_balances() + if ( + quote_balance > self.reserve_quote + and self.is_order_value_valid(new_buy_price) + ): + buy_order_id = self.api_place_order(new_buy_price, "BUY") + if buy_order_id: + self.active_orders[buy_order_id] = Order( + order_id=buy_order_id, + price=new_buy_price, + quantity=self.order_amount, + side="BUY", + status="NEW", + ) + logger.info( + "[GridTradingBot.adjust_grid] Placed new higher BUY order at %f (order_id: %s)", + new_buy_price, + buy_order_id, + ) + + except Exception as e: + logger.error( + "[GridTradingBot.adjust_grid] Failed to adjust grid: %s", str(e) + ) + raise + + def run(self): + """运行网格交易机器人""" + self.running = True + logger.info( + "[GridTradingBot.run] Starting grid trading bot for symbol: %s", self.symbol + ) + + try: + # 初始化网格 + self.initialize_grid() + + # 主循环 + while self.running: + try: + logger.custom_debug( + "[GridTradingBot.run] Starting main loop iteration" + ) + + # 更新订单状态 + self.api_update_order_statuses() + + # 调整网格 + self.adjust_grid() + + # 尝试扩展网格 + self.extend_grid() + + # 等待一段时间再检查 + sleep_time = 0 + logger.custom_debug( + "[GridTradingBot.run] Sleeping for %d seconds", sleep_time + ) + time.sleep(sleep_time) + + except Exception as e: + logger.error("[GridTradingBot.run] Error in main loop: %s", str(e)) + sleep_time = 3 + logger.custom_debug( + "[GridTradingBot.run] Error occurred, sleeping for %d seconds", + sleep_time, + ) + time.sleep(sleep_time) # 出错后等待更长时间 + + except KeyboardInterrupt: + logger.info("[GridTradingBot.run] Received keyboard interrupt, stopping...") + finally: + self.stop() + + def stop(self): + """停止机器人""" + logger.info("[GridTradingBot.stop] Stopping grid trading bot...") + self.running = False + self.api_cancel_all_orders() + logger.info("[GridTradingBot.stop] Grid trading bot stopped successfully") + + +# 示例配置和使用 +if __name__ == "__main__": + config = { + "symbol": "BTCUSDC", # 交易对 + "grid_percentage": 0.001, # 等比网格的公比 + "grid_count": 3, # 单侧的挂单数,实时平衡 + "order_amount": 0.00001, # BTC数量 + "min_order_value": 1, # 交易所限制订单价值至少 1 USDC + "reserve_base": 0, # 保留 0 BTC 不参与网格 + "reserve_quote": 0, # 保留 0 USDC 不参与网格 + } + + logger.info("[__main__] Creating GridTradingBot instance with config: %r", config) + bot = GridTradingBot(config) + bot.run() diff --git a/mexc_spot_v3.py b/mexc_spot_v3.py new file mode 100644 index 0000000..e001941 --- /dev/null +++ b/mexc_spot_v3.py @@ -0,0 +1,535 @@ +import requests +import hmac +import hashlib +from urllib.parse import urlencode, quote +from config import config + +# ServerTime、Signature +class TOOL(object): + + def _get_server_time(self): + return requests.request('get', 'https://api.mexc.com/api/v3/time').json()['serverTime'] + + def _sign_v3(self, req_time, sign_params=None): + if sign_params: + sign_params = urlencode(sign_params, quote_via=quote) + to_sign = "{}×tamp={}".format(sign_params, req_time) + else: + to_sign = "timestamp={}".format(req_time) + sign = hmac.new(self.mexc_secret.encode('utf-8'), to_sign.encode('utf-8'), hashlib.sha256).hexdigest() + return sign + + def public_request(self, method, url, params=None): + url = '{}{}'.format(self.hosts, url) + return requests.request(method, url, params=params) + + def sign_request(self, method, url, params=None): + url = '{}{}'.format(self.hosts, url) + req_time = self._get_server_time() + if params: + params['signature'] = self._sign_v3(req_time=req_time, sign_params=params) + else: + params = {} + params['signature'] = self._sign_v3(req_time=req_time) + params['timestamp'] = req_time + headers = { + 'x-mexc-apikey': self.mexc_key, + 'Content-Type': 'application/json', + } + return requests.request(method, url, params=params, headers=headers) + + +# Market Data +class mexc_market(TOOL): + + def __init__(self): + self.api = '/api/v3' + self.hosts = config.mexc_host + self.method = 'GET' + + def get_ping(self): + """test connectivity""" + url = '{}{}'.format(self.api, '/ping') + response = self.public_request(self.method, url) + return response.json() + + def get_timestamp(self): + """get sever time""" + url = '{}{}'.format(self.api, '/time') + response = self.public_request(self.method, url) + return response.json() + + def get_defaultSymbols(self): + """get defaultSymbols""" + url = '{}{}'.format(self.api, '/defaultSymbols') + response = self.public_request(self.method, url) + return response.json() + + def get_exchangeInfo(self, params=None): + """get exchangeInfo""" + url = '{}{}'.format(self.api, '/exchangeInfo') + response = self.public_request(self.method, url, params=params) + return response.json() + + def get_depth(self, params): + """get symbol depth""" + url = '{}{}'.format(self.api, '/depth') + response = self.public_request(self.method, url, params=params) + return response.json() + + def get_deals(self, params): + """get current trade deals list""" + url = '{}{}'.format(self.api, '/trades') + response = self.public_request(self.method, url, params=params) + return response.json() + + def get_aggtrades(self, params): + """get aggregate trades list""" + url = '{}{}'.format(self.api, '/aggTrades') + response = self.public_request(self.method, url, params=params) + return response.json() + + def get_kline(self, params): + """get k-line data""" + url = '{}{}'.format(self.api, '/klines') + response = self.public_request(self.method, url, params=params) + return response.json() + + def get_avgprice(self, params): + """get current average prcie(default : 5m)""" + url = '{}{}'.format(self.api, '/avgPrice') + response = self.public_request(self.method, url, params=params) + return response.json() + + def get_24hr_ticker(self, params=None): + """get 24hr prcie ticker change statistics""" + url = '{}{}'.format(self.api, '/ticker/24hr') + response = self.public_request(self.method, url, params=params) + return response.json() + + def get_price(self, params=None): + """get symbol price ticker""" + url = '{}{}'.format(self.api, '/ticker/price') + response = self.public_request(self.method, url, params=params) + return response.json() + + def get_bookticker(self, params=None): + """get symbol order book ticker""" + url = '{}{}'.format(self.api, '/ticker/bookTicker') + response = self.public_request(self.method, url, params=params) + return response.json() + + def get_ETF_info(self, params=None): + """get ETF information""" + url = '{}{}'.format(self.api, '/etf/info') + response = self.public_request(self.method, url, params=params) + return response.json() + + +# Spot Trade +class mexc_trade(TOOL): + + def __init__(self): + self.api = '/api/v3' + self.hosts = config.mexc_host + self.mexc_key = config.api_key + self.mexc_secret = config.secret_key + + def get_selfSymbols(self): + """get currency information""" + method = 'GET' + url = '{}{}'.format(self.api, '/selfSymbols') + response = self.sign_request(method, url) + return response.json() + + def post_order_test(self, params): + """test new order""" + method = 'POST' + url = '{}{}'.format(self.api, '/order/test') + response = self.sign_request(method, url, params=params) + return response.json() + + def post_order(self, params): + """place order""" + method = 'POST' + url = '{}{}'.format(self.api, '/order') + response = self.sign_request(method, url, params=params) + return response.json() + + def post_batchorders(self, params): + """place batch orders(same symbol)""" + method = 'POST' + url = '{}{}'.format(self.api, '/batchOrders') + params = {"batchOrders": str(params)} + response = self.sign_request(method, url, params=params) + print(response.url) + return response.json() + + def delete_order(self, params): + """ + cancel order + 'origClientOrderId' or 'orderId' must be sent + """ + method = 'DELETE' + url = '{}{}'.format(self.api, '/order') + response = self.sign_request(method, url, params=params) + return response.json() + + def delete_openorders(self, params): + """ + cancel all order for a single symbol + """ + method = 'DELETE' + url = '{}{}'.format(self.api, '/openOrders') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_order(self, params): + """ + get order + 'origClientOrderId' or 'orderId' must be sent + """ + method = 'GET' + url = '{}{}'.format(self.api, '/order') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_openorders(self, params): + """get current pending order """ + method = 'GET' + url = '{}{}'.format(self.api, '/openOrders') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_allorders(self, params): + """ + get current all order + startTime and endTime need to use at the same time + """ + method = 'GET' + url = '{}{}'.format(self.api, '/allOrders') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_mytrades(self, params): + """ + get current all order + orderId need to use with symbol at the same time + """ + method = 'GET' + url = '{}{}'.format(self.api, '/myTrades') + response = self.sign_request(method, url, params=params) + return response.json() + + def post_mxDeDuct(self, params): + """Enable MX DeDuct""" + method = 'POST' + url = '{}{}'.format(self.api, '/mxDeduct/enable') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_mxDeDuct(self): + """MX DeDuct status""" + method = 'GET' + url = '{}{}'.format(self.api, '/mxDeduct/enable') + response = self.sign_request(method, url) + return response.json() + + def get_account_info(self): + """get account information""" + method = 'GET' + url = '{}{}'.format(self.api, '/account') + response = self.sign_request(method, url) + return response.json() + + +# Wallet +class mexc_wallet(TOOL): + + def __init__(self): + self.api = '/api/v3/capital' + self.hosts = config.mexc_host + self.mexc_key = config.api_key + self.mexc_secret = config.secret_key + + def get_coinlist(self): + """get currency information""" + method = 'GET' + url = '{}{}'.format(self.api, '/config/getall') + response = self.sign_request(method, url) + return response.json() + + def post_withdraw(self, params): + """withdraw""" + method = 'POST' + url = '{}{}'.format(self.api, '/withdraw/apply') + response = self.sign_request(method, url, params=params) + return response.json() + + def cancel_withdraw(self, params): + """withdraw""" + method = 'DELETE' + url = '{}{}'.format(self.api, '/withdraw') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_deposit_list(self, params): + """deposit history list""" + method = 'GET' + url = '{}{}'.format(self.api, '/deposit/hisrec') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_withdraw_list(self, params): + """withdraw history list""" + method = 'GET' + url = '{}{}'.format(self.api, '/withdraw/history') + response = self.sign_request(method, url, params=params) + return response.json() + + def post_deposit_address(self, params): + """generate deposit address""" + method = 'POST' + url = '{}{}'.format(self.api, '/deposit/address') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_deposit_address(self, params): + """get deposit address""" + method = 'GET' + url = '{}{}'.format(self.api, '/deposit/address') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_withdraw_address(self, params): + """get deposit address""" + method = 'GET' + url = '{}{}'.format(self.api, '/withdraw/address') + response = self.sign_request(method, url, params=params) + return response.json() + + def post_transfer(self, params): + """universal transfer""" + method = 'POST' + url = '{}{}'.format(self.api, '/transfer') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_transfer_list(self, params): + """universal transfer history""" + method = 'GET' + url = '{}{}'.format(self.api, '/transfer') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_transfer_list_byId(self, params): + """universal transfer history (by tranId)""" + method = 'GET' + url = '{}{}'.format(self.api, '/transfer/tranId') + response = self.sign_request(method, url, params=params) + return response.json() + + def post_transfer_internal(self, params): + """universal transfer""" + method = 'POST' + url = '{}{}'.format(self.api, '/transfer/internal') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_transfer_internal_list(self, params=None): + """universal transfer""" + method = 'GET' + url = '{}{}'.format(self.api, '/transfer/internal') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_smallAssets_list(self): + """small Assets convertible list""" + method = 'GET' + url = '{}{}'.format(self.api, '/convert/list') + response = self.sign_request(method, url) + return response.json() + + def post_smallAssets_convert(self, params): + """small Assets convert""" + method = 'POST' + url = '{}{}'.format(self.api, '/convert') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_smallAssets_history(self, params=None): + """small Assets convertible history""" + method = 'GET' + url = '{}{}'.format(self.api, '/convert') + response = self.sign_request(method, url, params=params) + return response.json() + + +# Sub-Account +class mexc_subaccount(TOOL): + + def __init__(self): + self.api = '/api/v3' + self.hosts = config.mexc_host + self.mexc_key = config.api_key + self.mexc_secret = config.secret_key + + def post_virtualSubAccount(self, params): + """create a sub-account""" + method = 'POST' + url = '{}{}'.format(self.api, '/sub-account/virtualSubAccount') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_SubAccountList(self, params=None): + """get sub-account list""" + method = 'GET' + url = '{}{}'.format(self.api, '/sub-account/list') + response = self.sign_request(method, url, params=params) + return response.json() + + def post_virtualApiKey(self, params): + """create sub-account's apikey""" + method = 'POST' + url = '{}{}'.format(self.api, '/sub-account/apiKey') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_virtualApiKey(self, params): + """get sub-account's apikey""" + method = 'GET' + url = '{}{}'.format(self.api, '/sub-account/apiKey') + response = self.sign_request(method, url, params=params) + return response.json() + + def delete_virtualApiKey(self, params): + """delete sub-account's apikey""" + method = 'DELETE' + url = '{}{}'.format(self.api, '/sub-account/apiKey') + response = self.sign_request(method, url, params=params) + return response.json() + + def post_universalTransfer(self, params): + """universal transfer between accounts""" + method = 'POST' + url = '{}{}'.format(self.api, '/capital/sub-account/universalTransfer') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_universalTransfer(self, params): + """universal transfer history between accounts""" + method = 'GET' + url = '{}{}'.format(self.api, '/capital/sub-account/universalTransfer') + response = self.sign_request(method, url, params=params) + return response.json() + + +# Rebate +class mexc_rebate(TOOL): + + def __init__(self): + self.api = '/api/v3/rebate' + self.hosts = config.mexc_host + self.mexc_key = config.api_key + self.mexc_secret = config.secret_key + + def get_taxQuery(self, params=None): + """get the rebate commission record""" + method = 'GET' + url = '{}{}'.format(self.api, '/taxQuery') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_rebate_detail(self, params=None): + """get rebate record details""" + method = 'GET' + url = '{}{}'.format(self.api, '/detail') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_kickback_detail(self, params=None): + """get self-return record details""" + method = 'GET' + url = '{}{}'.format(self.api, '/detail/kickback') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_inviter(self, params=None): + """get self-return record details""" + method = 'GET' + url = '{}{}'.format(self.api, '/referCode') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_affiliate_commission(self, params=None): + """get affiliate commission history""" + method = 'GET' + url = '{}{}'.format(self.api, '/affiliate/commission') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_affiliate_withdraw(self, params=None): + """get affiliate withdraw history""" + method = 'GET' + url = '{}{}'.format(self.api, '/affiliate/withdraw') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_affiliate_commission_detail(self, params=None): + """get affiliate commission details""" + method = 'GET' + url = '{}{}'.format(self.api, '/affiliate/commission/detail') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_affiliate_referral(self, params=None): + """get affiliate referral""" + method = 'GET' + url = '{}{}'.format(self.api, '/affiliate/referral') + response = self.sign_request(method, url, params=params) + return response.json() + + def get_affiliate_subaffiliates(self, params=None): + """get affiliate subaffiliates""" + method = 'GET' + url = '{}{}'.format(self.api, '/affiliate/subaffiliates') + response = self.sign_request(method, url, params=params) + return response.json() + + +# WebSocket ListenKey +class mexc_listenkey(TOOL): + + def __init__(self): + self.api = '/api/v3' + self.hosts = config.mexc_host + self.mexc_key = config.api_key + self.mexc_secret = config.secret_key + + def post_listenKey(self): + """ generate ListenKey """ + method = 'POST' + url = '{}{}'.format(self.api, '/userDataStream') + response = self.sign_request(method, url) + return response.json() + + def get_listenKey(self): + """ get valid ListenKey """ + method = 'GET' + url = '{}{}'.format(self.api, '/userDataStream') + response = self.sign_request(method, url) + return response.json() + + def put_listenKey(self, params): + """ extend ListenKey validity """ + method = 'PUT' + url = '{}{}'.format(self.api, '/userDataStream') + response = self.sign_request(method, url, params=params) + return response.json() + + def delete_listenKey(self, params): + """ delete ListenKey """ + method = 'DELETE' + url = '{}{}'.format(self.api, '/userDataStream') + response = self.sign_request(method, url, params=params) + return response.json()