diff --git a/backend/api/routes/account.py b/backend/api/routes/account.py index 636a866..65979eb 100644 --- a/backend/api/routes/account.py +++ b/backend/api/routes/account.py @@ -740,6 +740,7 @@ async def fetch_realtime_positions(account_id: int): pnl_percent = (unrealized_pnl / margin_usdt_live * 100) if margin_usdt_live > 0 else 0 entry_time = None + created_at = None stop_loss_price = None take_profit_price = None take_profit_1 = None @@ -765,6 +766,7 @@ async def fetch_realtime_positions(account_id: int): if matched is None: matched = db_trades[0] entry_time = matched.get('entry_time') + created_at = matched.get('created_at') # 创建时间,无 entry_time 时用于展示开仓时间 stop_loss_price = matched.get('stop_loss_price') take_profit_price = matched.get('take_profit_price') take_profit_1 = matched.get('take_profit_1') @@ -833,6 +835,7 @@ async def fetch_realtime_positions(account_id: int): "pnl_percent": pnl_percent, "leverage": int(pos.get('leverage', 1)), "entry_time": entry_time, + "created_at": created_at, "stop_loss_price": stop_loss_price, "take_profit_price": take_profit_price, "take_profit_1": take_profit_1, diff --git a/backend/database/add_created_at_to_trades.sql b/backend/database/add_created_at_to_trades.sql new file mode 100644 index 0000000..22d5248 --- /dev/null +++ b/backend/database/add_created_at_to_trades.sql @@ -0,0 +1,20 @@ +-- 为 trades 表增加 created_at(创建时间)字段(仅当不存在时) +-- 用于持仓/订单展示「开仓时间」时至少有创建时间可显示;与 init.sql 中定义一致。 + +-- MySQL 5.7+:通过 procedure 判断后添加,避免重复执行报错 +DELIMITER // +DROP PROCEDURE IF EXISTS add_created_at_to_trades_if_missing// +CREATE PROCEDURE add_created_at_to_trades_if_missing() +BEGIN + IF (SELECT COUNT(*) FROM information_schema.COLUMNS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'trades' AND COLUMN_NAME = 'created_at') = 0 THEN + ALTER TABLE trades + ADD COLUMN created_at INT UNSIGNED NULL COMMENT '创建时间(Unix时间戳秒数)' AFTER status; + UPDATE trades SET created_at = COALESCE(entry_time, UNIX_TIMESTAMP()) WHERE created_at IS NULL; + ALTER TABLE trades + MODIFY COLUMN created_at INT UNSIGNED NOT NULL DEFAULT (UNIX_TIMESTAMP()) COMMENT '创建时间(Unix时间戳秒数)'; + END IF; +END// +DELIMITER ; +CALL add_created_at_to_trades_if_missing(); +DROP PROCEDURE IF EXISTS add_created_at_to_trades_if_missing; diff --git a/frontend/src/components/StatsDashboard.jsx b/frontend/src/components/StatsDashboard.jsx index 7443114..af8c56c 100644 --- a/frontend/src/components/StatsDashboard.jsx +++ b/frontend/src/components/StatsDashboard.jsx @@ -739,7 +739,7 @@ const StatsDashboard = () => {
- 开仓时间: {trade.entry_time ? formatEntryTime(trade.entry_time) : '—'} + 开仓时间: {(trade.entry_time || trade.created_at) ? formatEntryTime(trade.entry_time || trade.created_at) : '—'}
diff --git a/trading_system/binance_client.py b/trading_system/binance_client.py index 658ed69..a3589b2 100644 --- a/trading_system/binance_client.py +++ b/trading_system/binance_client.py @@ -12,6 +12,14 @@ from typing import Dict, List, Optional, Any from binance import AsyncClient, BinanceSocketManager from binance.exceptions import BinanceAPIException + +class AlgoOrderPositionUnavailableError(Exception): + """条件单被拒:持仓未就绪或已平(如 GTE can only be used with open positions),调用方可仅打 WARNING 不刷 ERROR。""" + def __init__(self, symbol: str, message: str = ""): + self.symbol = symbol + self.message = message or "Position not available for algo order" + super().__init__(f"{symbol}: {self.message}") + try: from . import config from .redis_cache import RedisCache @@ -2230,12 +2238,12 @@ class BinanceClient: err_msg = str(e).strip() if code in (-4509, -4061): raise BinanceAPIException(None, 400, json.dumps({"code": code, "msg": err_msg})) - # "Time in Force (TIF) GTE can only be used with open positions":持仓尚未可用或已平,不刷屏 + # "Time in Force (TIF) GTE can only be used with open positions":持仓尚未可用或已平 if "GTE" in err_msg and "open positions" in err_msg: logger.warning( f"{symbol} 条件单被拒(持仓未就绪或已平): {err_msg[:80]}…,将依赖 WebSocket 监控" ) - return None + raise AlgoOrderPositionUnavailableError(symbol, err_msg[:200]) logger.debug(f"{symbol} WS 条件单异常: {e},回退到 REST") # 回退到 REST(原有逻辑) @@ -2263,7 +2271,7 @@ class BinanceClient: logger.warning( f"{symbol} 条件单被拒(持仓未就绪或已平): {error_msg[:80]}…,将依赖 WebSocket 监控" ) - return None + raise AlgoOrderPositionUnavailableError(symbol, error_msg[:200]) trigger_type = params.get('type', 'UNKNOWN') logger.error(f"{symbol} ❌ 创建 Algo 条件单失败({trigger_type}): {error_msg}") logger.error(f" 错误代码: {error_code}") diff --git a/trading_system/position_manager.py b/trading_system/position_manager.py index 52e24ae..16c1eef 100644 --- a/trading_system/position_manager.py +++ b/trading_system/position_manager.py @@ -11,11 +11,11 @@ from pathlib import Path from typing import Dict, List, Optional from datetime import datetime try: - from .binance_client import BinanceClient + from .binance_client import BinanceClient, AlgoOrderPositionUnavailableError from .risk_manager import RiskManager from . import config except ImportError: - from binance_client import BinanceClient + from binance_client import BinanceClient, AlgoOrderPositionUnavailableError from risk_manager import RiskManager import config @@ -1532,6 +1532,7 @@ class PositionManager: logger.warning(f"{symbol} ⚠️ 无法获取当前价格(已尝试 WS bookTicker/ticker24h、持仓、depth、REST ticker),止损单可能无法正确验证触发条件") # 在挂止损单前,检查当前价格是否已经触发止损(避免 -2021 错误) + sl_failed_due_to_gte = False # GTE/持仓未就绪时仅打 WARNING,不刷 ERROR if current_price and stop_loss: try: current_price_val = float(current_price) @@ -1588,6 +1589,9 @@ class PositionManager: current_price=current_price, working_type="MARK_PRICE", ) + except AlgoOrderPositionUnavailableError: + sl_failed_due_to_gte = True + sl_order = None except Exception as e: # 检查是否是 -2021 (立即触发) error_msg = str(e) @@ -1606,6 +1610,9 @@ class PositionManager: current_price=current_price, working_type="MARK_PRICE", ) + except AlgoOrderPositionUnavailableError: + sl_failed_due_to_gte = True + sl_order = None except Exception as retry_e: retry_msg = str(retry_e) if "-2021" in retry_msg or "immediately trigger" in retry_msg: @@ -1624,6 +1631,9 @@ class PositionManager: current_price=current_price, working_type="MARK_PRICE", ) + except AlgoOrderPositionUnavailableError: + sl_failed_due_to_gte = True + sl_order = None except Exception as e: error_msg = str(e) if "-2021" in error_msg or "immediately trigger" in error_msg: @@ -1636,12 +1646,15 @@ class PositionManager: if sl_order: logger.info(f"{symbol} ✓ 止损单已成功挂到交易所: {sl_order.get('algoId', 'N/A')}") else: - logger.error(f"{symbol} ❌ 止损单挂单失败!将依赖WebSocket监控,但可能无法及时止损") - logger.error(f" 止损价格: {stop_loss:.8f}") - logger.error(f" 当前价格: {current_price if current_price else '无(已尝试 WS bookTicker/ticker24h、持仓、REST)'}") - logger.error(f" 持仓方向: {side}") - logger.error(f" ⚠️ 警告: 没有交易所级别的止损保护,如果系统崩溃或网络中断,可能无法及时止损!") - logger.error(f" 💡 建议: 检查止损价格计算是否正确,或手动在币安界面设置止损") + if sl_failed_due_to_gte: + logger.warning(f"{symbol} 条件单被拒(持仓未就绪或已平),跳过交易所止损单,将依赖 WebSocket 监控") + else: + logger.error(f"{symbol} ❌ 止损单挂单失败!将依赖WebSocket监控,但可能无法及时止损") + logger.error(f" 止损价格: {stop_loss:.8f}") + logger.error(f" 当前价格: {current_price if current_price else '无(已尝试 WS bookTicker/ticker24h、持仓、REST)'}") + logger.error(f" 持仓方向: {side}") + logger.error(f" ⚠️ 警告: 没有交易所级别的止损保护,如果系统崩溃或网络中断,可能无法及时止损!") + logger.error(f" 💡 建议: 检查止损价格计算是否正确,或手动在币安界面设置止损") # ⚠️ 关键修复:止损单挂单失败后,立即检查当前价格是否已触发止损 # 如果已触发,立即执行市价平仓,避免亏损扩大