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
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user