diff --git a/README.md b/README.md index 9c23dcd..79704bb 100644 --- a/README.md +++ b/README.md @@ -8,11 +8,12 @@ MEXC 定投交易机器人是一个自动化交易工具,用于在 MEXC 交易 1. **配置文件管理**:从 JSON 文件读取交易指令 2. **灵活调度**:支持按日期执行交易(`*`表示每天执行) -3. **多种订单类型**:支持 LIMIT、MARKET、LIMIT_MAKER、IMMEDIATE_OR_CANCEL、FILL_OR_KILL 等订单类型 +3. **多种订单类型**:支持 limit(限价)、market(市价)、convert(闪兑)等订单类型 4. **智能价格获取**:当无 price 参数时自动获取实时价格作为限价 -5. **自动计算数量**:LIMIT 订单支持只提供 quoteOrderQty 自动计算 quantity -6. **证券代码映射**:支持不同代码格式间的转换 -7. **完整交易记录**:记录交易到 CSV 文件和日志 +5. **自动计算数量**:limit 订单支持只提供 quoteOrderQty 自动计算 quantity +6. **闪兑交易**:convert 订单直接币币兑换,不走订单簿(buy 用 quoteOrderQty,sell 用 quantity) +7. **证券代码映射**:支持不同代码格式间的转换 +8. **完整交易记录**:记录交易到 CSV 文件和日志 ## 安装与使用 @@ -81,19 +82,17 @@ MEXC 定投交易机器人是一个自动化交易工具,用于在 MEXC 交易 - 类型:字符串 - 必填:是 - 可选值: - - `"LIMIT"`:限价单 - - `"MARKET"`:市价单 - - `"LIMIT_MAKER"`:限价做市单 - - `"IMMEDIATE_OR_CANCEL"`:立即成交或取消 - - `"FILL_OR_KILL"`:全部成交或取消 + - `"limit"`:限价单 + - `"market"`:市价单 + - `"convert"`:闪兑(币币兑换,不走订单簿,见下方说明) ##### 3. 交易方向 (`side`) - 类型:字符串 - 必填:是 - 可选值: - - `"BUY"`:买入 - - `"SELL"`:卖出 + - `"buy"`:买入 + - `"sell"`:卖出 ##### 4. 执行日期 (`execute_dates`) @@ -118,6 +117,16 @@ MEXC 定投交易机器人是一个自动化交易工具,用于在 MEXC 交易 - `quoteOrderQty`:交易金额(字符串格式的数字) - `price`:限价价格(字符串格式的数字),如果为空则自动获取最新价 +> convert 闪兑订单的参数按方向区分,`quantity` / `quoteOrderQty` 至少提供一个: +> - `buy`:用计价币兑换基础币 +> - `quoteOrderQty`:指定花费金额(如 `"100"` 表示用 100 USDT 兑换) +> - `quantity`:指定目标数量(如 `"0.01"` 表示兑换约 0.01 BTC,自动按实时价估算所需金额) +> - `sell`:用基础币兑换计价币 +> - `quantity`:指定卖出数量(如 `"0.5"` 表示卖出 0.5 ETH) +> - `quoteOrderQty`:指定目标金额(如 `"2000"` 表示兑换到约 2000 USDT,自动按实时价估算卖出数量) +> +> convert 订单`symbol`需使用 `BASE/QUOTE` 格式,如 `"BTC/USDT"`。 + ### 配置示例 #### 示例 1:每日限价买入 @@ -157,7 +166,87 @@ MEXC 定投交易机器人是一个自动化交易工具,用于在 MEXC 交易 } ``` -#### 示例 3:多种订单组合 +#### 示例 3:每日闪兑买入 + +```json +{ + "trades": [ + { + "symbol": "BTC/USDT", + "order_type": "convert", + "side": "buy", + "execute_dates": ["*"], + "params": { + "quoteOrderQty": "100" + } + } + ] +} +``` + +说明:每天用 100 USDT 通过闪兑兑换成 BTC(不走订单簿)。 + +#### 示例 4:闪兑卖出(按数量) + +```json +{ + "trades": [ + { + "symbol": "ETH/USDT", + "order_type": "convert", + "side": "sell", + "execute_dates": ["2023-12-25"], + "params": { + "quantity": "0.5" + } + } + ] +} +``` + +说明:在指定日期将 0.5 ETH 通过闪兑兑换成 USDT。 + +#### 示例 4b:闪兑买入(按目标数量自动估算金额) + +```json +{ + "trades": [ + { + "symbol": "BTC/USDT", + "order_type": "convert", + "side": "buy", + "execute_dates": ["*"], + "params": { + "quantity": "0.01" + } + } + ] +} +``` + +说明:每天按实时价估算约 0.01 BTC 所需 USDT,然后通过闪兑买入。 + +#### 示例 4c:闪兑卖出(按目标金额自动估算数量) + +```json +{ + "trades": [ + { + "symbol": "ETH/USDT", + "order_type": "convert", + "side": "sell", + "execute_dates": ["2023-12-25"], + "params": { + "quoteOrderQty": "2000" + } + } + ] +} +``` + +说明:在指定日期按实时价估算卖出 ETH,兑换到约 2000 USDT。 + +#### 示例 5:多种订单组合 ```json { @@ -188,10 +277,12 @@ MEXC 定投交易机器人是一个自动化交易工具,用于在 MEXC 交易 ### 注意事项 1. 价格和数量都使用字符串格式而非数字,以避免浮点数精度问题 -2. 对于 LIMIT 订单,可以只提供`quoteOrderQty`,系统会自动计算`quantity` +2. 对于 `limit` 订单,可以只提供`quoteOrderQty`,系统会自动计算`quantity` 3. 当`execute_dates`包含`"*"`时,会忽略其他日期设置 4. 系统会自动创建`output`目录存放日志和交易记录 5. 所有时间均以系统时区为准 +6. `order_type`、`side` 均为小写;`convert` 订单的`symbol`使用 `BASE/QUOTE` 格式 +7. `convert` 闪兑产出的 CSV 备注为 `DCA Order ID (Convert): {id}`,`tool_csv_merge.py` 已兼容该格式 ### 错误处理 diff --git a/main.py b/main.py index a9d1580..b954339 100644 --- a/main.py +++ b/main.py @@ -7,7 +7,7 @@ 功能: 1. 从JSON配置文件读取交易指令 2. 支持按日期执行多个交易(*表示每天执行) -3. 支持多种订单类型 (limit, market) +3. 支持多种订单类型 (limit, market, convert) 4. 当无price时自动获取实时价格作为限价 5. limit订单支持只提供quoteOrderQty自动计算quantity 6. 证券代码映射功能 @@ -100,7 +100,8 @@ class BotSpotTrade: ORDER_TYPE_REQUIREMENTS = { "limit": ["quantity", "price", "quoteOrderQty"], - "market": ["quantity", "quoteOrderQty"] + "market": ["quantity", "quoteOrderQty"], + "convert": ["quoteOrderQty", "quantity"] } def __init__(self, exchange: ccxt.Exchange, symbol_mapping: Dict[str, str], config_file_name: str): @@ -138,7 +139,18 @@ class BotSpotTrade: def _tool_map_symbol(self, symbol: str) -> str: return self.symbol_mapping.get(symbol, symbol) - def _tool_validate_order_params(self, order_type: str, params: Dict[str, Any]) -> Tuple[bool, str]: + def _tool_validate_order_params( + self, order_type: str, params: Dict[str, Any], side: Optional[str] = None + ) -> Tuple[bool, str]: + if order_type == "convert": + if side == "buy": + if "quoteOrderQty" not in params and "quantity" not in params: + return False, "convert订单(买入)需要quoteOrderQty或quantity参数" + elif side == "sell": + if "quantity" not in params and "quoteOrderQty" not in params: + return False, "convert订单(卖出)需要quantity或quoteOrderQty参数" + return True, "" + required_params = self.ORDER_TYPE_REQUIREMENTS.get(order_type, []) if not required_params: return False, f"未知的订单类型: {order_type}" @@ -167,7 +179,7 @@ class BotSpotTrade: result = result.rstrip('0').rstrip('.') return result - def _tool_record_transaction(self, order_data: Dict[str, Any]) -> bool: + def _tool_record_transaction(self, order_data: Dict[str, Any], is_convert: bool = False) -> bool: try: original_symbol = order_data["symbol"] mapped_symbol = self._tool_map_symbol(original_symbol) @@ -180,6 +192,11 @@ class BotSpotTrade: trade_type = "买入" if side == "buy" else "卖出" timestamp = datetime.fromtimestamp(order_data["timestamp"] / 1000).strftime("%Y-%m-%dT%H:%M") + if is_convert: + note = f"DCA Order ID (Convert): {order_id}" + else: + note = f"DCA Order ID: {order_id}" + row = [ timestamp, trade_type, @@ -188,7 +205,7 @@ class BotSpotTrade: cummulative_quote_qty, "资金账户", "CEX", - f"DCA Order ID: {order_id}", + note, balances, ] @@ -214,7 +231,117 @@ class BotSpotTrade: self.logger.error("记录交易失败: %s", str(e)) return False + def _trade_convert(self, symbol: str, side: str, **kwargs) -> Optional[Dict[str, Any]]: + """闪兑交易:直接币币兑换,不走订单簿""" + if side not in ["buy", "sell"]: + self.logger.error("无效的交易方向: %s", side) + return None + + quote_order_qty = kwargs.get("quoteOrderQty") + quantity = kwargs.get("quantity") + + try: + base_currency, quote_currency = symbol.split("/") + except ValueError: + self.logger.error("无效的交易对格式: %s(应为 BASE/QUOTE 格式)", symbol) + return None + + if side == "buy": + from_currency = quote_currency + to_currency = base_currency + if quote_order_qty is not None: + amount_str = quote_order_qty + else: + price = self.market.get_price(symbol) + if price is None: + self.logger.error("无法获取实时价格来估算花费金额") + return None + amount_str = str(float(quantity) * price) + else: + from_currency = base_currency + to_currency = quote_currency + if quantity is not None: + amount_str = quantity + else: + price = self.market.get_price(symbol) + if price is None: + self.logger.error("无法获取实时价格来估算卖出数量") + return None + amount_str = str(float(quote_order_qty) / price) + + try: + amount = float(amount_str) + except (ValueError, TypeError): + self.logger.error("无效的金额: %s", amount_str) + return None + + self.logger.info("开始闪兑: %s %s → %s", amount, from_currency, to_currency) + + try: + quote = self.exchange.fetchConvertQuote(from_currency, to_currency, amount) + self.logger.info( + "获取闪兑报价成功: %s %s → %s %s, 报价ID: %s", + from_currency, amount, to_currency, + quote.get("toAmount"), quote.get("id"), + ) + except Exception as e: + self.logger.error("获取闪兑报价失败: %s", str(e)) + return None + + if not quote or not quote.get("id"): + self.logger.error("闪兑报价无效: %s", quote) + return None + + quote_id = quote["id"] + + try: + convert_trade = self.exchange.createConvertTrade( + quote_id, from_currency, to_currency, amount + ) + self.logger.info("创建闪兑交易成功: %s", convert_trade) + except Exception as e: + self.logger.error("创建闪兑交易失败: %s", str(e)) + return None + + if not convert_trade or not convert_trade.get("id"): + self.logger.error("闪兑交易未返回有效的ID: %s", convert_trade) + return None + + trade_id = convert_trade["id"] + + trade_detail = None + try: + trade_detail = self.exchange.fetchConvertTrade(trade_id, from_currency) + self.logger.info("查询闪兑结果成功: %s", trade_detail) + except Exception as e: + self.logger.warning("查询闪兑结果失败: %s(使用创建返回的数据)", str(e)) + + final_data = trade_detail or convert_trade + + record_data = { + "symbol": symbol, + "id": trade_id, + "filled": final_data.get("toAmount", 0.0), + "cost": final_data.get("fromAmount", 0.0), + "side": side, + "timestamp": final_data.get("timestamp", int(time.time() * 1000)), + } + + if not self._tool_record_transaction(record_data, is_convert=True): + self.logger.error("闪兑交易记录失败") + + self.logger.info("闪兑交易完成: %s %s → %s, 交易ID: %s", side, from_currency, to_currency, trade_id) + return final_data + def trade(self, symbol: str, order_type: str, side: str, **kwargs) -> Optional[Dict[str, Any]]: + is_valid, error_msg = self._tool_validate_order_params(order_type, kwargs, side) + if not is_valid: + self.logger.error("订单参数验证失败: %s", error_msg) + return None + + if order_type == "convert": + return self._trade_convert(symbol, side, **kwargs) + if side not in ["buy", "sell"]: self.logger.error("无效的交易方向: %s", side) return None @@ -273,7 +400,7 @@ class BotSpotTrade: clean_price = current_price amount = float(processed_kwargs["quoteOrderQty"]) / clean_price - is_valid, error_msg = self._tool_validate_order_params(order_type, processed_kwargs) + is_valid, error_msg = self._tool_validate_order_params(order_type, processed_kwargs, side) if not is_valid: self.logger.error("订单参数验证失败: %s", error_msg) return None diff --git a/tool_csv_merge.py b/tool_csv_merge.py index 5f85c02..581c7fc 100644 --- a/tool_csv_merge.py +++ b/tool_csv_merge.py @@ -5,6 +5,7 @@ import csv import os +import re from collections import defaultdict @@ -33,7 +34,8 @@ def process_csv(input_file, output_file): for row in reader: datetime_str = row["日期"] date_part, time_part = datetime_str.split("T") - order_id = row["备注"].split("Order ID: ")[-1].strip() + match = re.search(r"Order ID[^:]*:\s*([^\s,]+)", row["备注"]) + order_id = match.group(1) if match else row["备注"] merge_key = (date_part, row["证券代码"]) record = merged_records[merge_key]