feat: add convert order type
- Add `convert` orders for direct base/quote token exchange without order book - Distinguish buy/sell validation: buy uses quoteOrderQty, sell uses quantity - Support automatic amount estimation when only one of quantity/quoteOrderQty is provided - Update README: replace old order type list, document convert usage with BASE/QUOTE symbols and new examples - Lowercase order_type and side values throughout documentation
This commit is contained in:
3 files changed
+240
-20
No files matched your search
@@ -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` 已兼容该格式
|
||||
|
||||
### 错误处理
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
+3
-1
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user