feat(account, stats_dashboard, binance_client, position_manager): 增强开仓时间记录与条件单错误处理
在 `account.py` 中新增 `created_at` 字段以记录开仓时间,并在 `StatsDashboard.jsx` 中更新展示逻辑,优先显示开仓时间或创建时间。`binance_client.py` 中引入 `AlgoOrderPositionUnavailableError` 异常处理,确保在条件单被拒时记录警告信息。`position_manager.py` 中优化了止损单挂单失败的处理逻辑,提升了系统的稳定性与风险控制能力。
This commit is contained in:
parent
7569c88a67
commit
3ce8493af2
|
|
@ -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,
|
||||
|
|
|
|||
20
backend/database/add_created_at_to_trades.sql
Normal file
20
backend/database/add_created_at_to_trades.sql
Normal file
|
|
@ -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;
|
||||
|
|
@ -739,7 +739,7 @@ const StatsDashboard = () => {
|
|||
|
||||
|
||||
<div className="entry-time">
|
||||
开仓时间: {trade.entry_time ? formatEntryTime(trade.entry_time) : '—'}
|
||||
开仓时间: {(trade.entry_time || trade.created_at) ? formatEntryTime(trade.entry_time || trade.created_at) : '—'}
|
||||
</div>
|
||||
</div>
|
||||
<div className="trade-protection-col">
|
||||
|
|
|
|||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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" 💡 建议: 检查止损价格计算是否正确,或手动在币安界面设置止损")
|
||||
|
||||
# ⚠️ 关键修复:止损单挂单失败后,立即检查当前价格是否已触发止损
|
||||
# 如果已触发,立即执行市价平仓,避免亏损扩大
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user