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" 💡 建议: 检查止损价格计算是否正确,或手动在币安界面设置止损")
# ⚠️ 关键修复:止损单挂单失败后,立即检查当前价格是否已触发止损
# 如果已触发,立即执行市价平仓,避免亏损扩大