1
This commit is contained in:
parent
19371a8e60
commit
16cf4f2157
|
|
@ -648,27 +648,19 @@ async def fetch_live_positions_pnl(account_id: int):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
@router.get("/positions")
|
async def fetch_realtime_positions(account_id: int):
|
||||||
async def get_realtime_positions(account_id: int = Depends(get_account_id)):
|
"""
|
||||||
"""获取实时持仓数据"""
|
获取指定账号的「币安实时持仓」列表(与仪表板/GET /positions 一致)。
|
||||||
|
每条持仓会尝试关联本账号下的 DB 记录(开仓时间、止损止盈、entry_order_id 等)。
|
||||||
|
失败时返回 [],不抛异常,便于仪表板回退到 DB 列表。
|
||||||
|
"""
|
||||||
client = None
|
client = None
|
||||||
try:
|
try:
|
||||||
logger.info(f"get_realtime_positions: 请求的 account_id={account_id}")
|
|
||||||
api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
|
api_key, api_secret, use_testnet, status = Account.get_credentials(account_id)
|
||||||
logger.info(f"get_realtime_positions: 获取到的 account_id={account_id}, status={status}, api_key exists={bool(api_key)}")
|
|
||||||
logger.info(f"get_realtime_positions: API Key 前4位={api_key[:4] if api_key and len(api_key) >= 4 else 'N/A'}, 后4位=...{api_key[-4:] if api_key and len(api_key) >= 4 else 'N/A'}")
|
|
||||||
|
|
||||||
logger.info(f"尝试获取实时持仓数据 (testnet={use_testnet}, account_id={account_id})")
|
|
||||||
|
|
||||||
if not api_key or not api_secret:
|
if not api_key or not api_secret:
|
||||||
error_msg = f"API密钥未配置(account_id={account_id})"
|
logger.debug(f"fetch_realtime_positions(account_id={account_id}): 无 API 密钥,返回空列表")
|
||||||
logger.warning(f"[account_id={account_id}] {error_msg}")
|
return []
|
||||||
raise HTTPException(
|
|
||||||
status_code=400,
|
|
||||||
detail=error_msg
|
|
||||||
)
|
|
||||||
|
|
||||||
# 导入BinanceClient
|
|
||||||
try:
|
try:
|
||||||
from binance_client import BinanceClient
|
from binance_client import BinanceClient
|
||||||
except ImportError:
|
except ImportError:
|
||||||
|
|
@ -676,70 +668,44 @@ async def get_realtime_positions(account_id: int = Depends(get_account_id)):
|
||||||
sys.path.insert(0, str(trading_system_path))
|
sys.path.insert(0, str(trading_system_path))
|
||||||
from binance_client import BinanceClient
|
from binance_client import BinanceClient
|
||||||
|
|
||||||
# 确保传递了正确的 api_key 和 api_secret,避免 BinanceClient 从 config 读取
|
client = BinanceClient(api_key=api_key, api_secret=api_secret, testnet=use_testnet)
|
||||||
client = BinanceClient(
|
|
||||||
api_key=api_key, # 明确传递,避免从 config 读取
|
|
||||||
api_secret=api_secret, # 明确传递,避免从 config 读取
|
|
||||||
testnet=use_testnet
|
|
||||||
)
|
|
||||||
logger.info(f"BinanceClient 创建成功 (account_id={account_id})")
|
|
||||||
|
|
||||||
logger.info("连接币安API获取持仓...")
|
|
||||||
await client.connect()
|
await client.connect()
|
||||||
positions = await client.get_open_positions()
|
positions = await client.get_open_positions()
|
||||||
|
|
||||||
logger.info(f"获取到 {len(positions)} 个持仓")
|
|
||||||
|
|
||||||
# 并发获取所有持仓的挂单信息(包括普通挂单和Algo挂单)
|
|
||||||
open_orders_map = {}
|
open_orders_map = {}
|
||||||
try:
|
try:
|
||||||
position_symbols = [p.get('symbol') for p in positions if float(p.get('positionAmt', 0)) != 0]
|
position_symbols = [p.get('symbol') for p in positions if float(p.get('positionAmt', 0)) != 0]
|
||||||
if position_symbols:
|
if position_symbols:
|
||||||
# 定义获取函数:同时获取普通挂单和Algo挂单
|
|
||||||
async def fetch_both_orders(symbol):
|
async def fetch_both_orders(symbol):
|
||||||
try:
|
try:
|
||||||
# 并发调用两个接口
|
|
||||||
t1 = client.get_open_orders(symbol)
|
t1 = client.get_open_orders(symbol)
|
||||||
t2 = client.futures_get_open_algo_orders(symbol, algo_type="CONDITIONAL")
|
t2 = client.futures_get_open_algo_orders(symbol, algo_type="CONDITIONAL")
|
||||||
res = await asyncio.gather(t1, t2, return_exceptions=True)
|
res = await asyncio.gather(t1, t2, return_exceptions=True)
|
||||||
|
|
||||||
orders = []
|
orders = []
|
||||||
# 1. 普通订单
|
|
||||||
if isinstance(res[0], list):
|
if isinstance(res[0], list):
|
||||||
orders.extend(res[0])
|
orders.extend(res[0])
|
||||||
else:
|
|
||||||
logger.warning(f"获取 {symbol} 普通挂单失败: {res[0]}")
|
|
||||||
|
|
||||||
# 2. Algo订单 (需要标准化为普通订单格式)
|
|
||||||
if isinstance(res[1], list):
|
if isinstance(res[1], list):
|
||||||
for algo in res[1]:
|
for algo in res[1]:
|
||||||
# 标准化 Algo Order 结构
|
|
||||||
orders.append({
|
orders.append({
|
||||||
'orderId': algo.get('algoId'),
|
'orderId': algo.get('algoId'),
|
||||||
'type': algo.get('orderType'), # Algo订单使用 orderType
|
'type': algo.get('orderType'),
|
||||||
'side': algo.get('side'),
|
'side': algo.get('side'),
|
||||||
'stopPrice': algo.get('triggerPrice'), # Algo订单使用 triggerPrice
|
'stopPrice': algo.get('triggerPrice'),
|
||||||
'price': 0, # 通常为0
|
'price': 0,
|
||||||
'origType': algo.get('algoType'),
|
'origType': algo.get('algoType'),
|
||||||
'reduceOnly': algo.get('reduceOnly'),
|
'reduceOnly': algo.get('reduceOnly'),
|
||||||
'status': 'NEW', # 列表中都是生效中的
|
'status': 'NEW',
|
||||||
'_is_algo': True
|
'_is_algo': True
|
||||||
})
|
})
|
||||||
else:
|
|
||||||
logger.warning(f"获取 {symbol} Algo挂单失败: {res[1]}")
|
|
||||||
|
|
||||||
return orders
|
return orders
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"获取 {symbol} 订单组合失败: {e}")
|
logger.debug(f"获取 {symbol} 订单失败: {e}")
|
||||||
return []
|
return []
|
||||||
|
|
||||||
logger.info(f"正在获取挂单信息(包含Algo): {position_symbols}")
|
|
||||||
tasks = [fetch_both_orders(sym) for sym in position_symbols]
|
tasks = [fetch_both_orders(sym) for sym in position_symbols]
|
||||||
results = await asyncio.gather(*tasks, return_exceptions=True)
|
results = await asyncio.gather(*tasks, return_exceptions=True)
|
||||||
|
|
||||||
for sym, orders in zip(position_symbols, results):
|
for sym, orders in zip(position_symbols, results):
|
||||||
if isinstance(orders, list):
|
if isinstance(orders, list):
|
||||||
# 过滤出止盈止损单
|
|
||||||
conditional_orders = []
|
conditional_orders = []
|
||||||
for o in orders:
|
for o in orders:
|
||||||
o_type = o.get('type')
|
o_type = o.get('type')
|
||||||
|
|
@ -756,50 +722,23 @@ async def get_realtime_positions(account_id: int = Depends(get_account_id)):
|
||||||
})
|
})
|
||||||
if conditional_orders:
|
if conditional_orders:
|
||||||
open_orders_map[sym] = conditional_orders
|
open_orders_map[sym] = conditional_orders
|
||||||
else:
|
|
||||||
logger.warning(f"获取 {sym} 挂单失败: {orders}")
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"批量获取挂单失败: {e}")
|
logger.debug(f"批量获取挂单失败: {e}")
|
||||||
|
|
||||||
# 格式化持仓数据
|
|
||||||
formatted_positions = []
|
formatted_positions = []
|
||||||
for pos in positions:
|
for pos in positions:
|
||||||
position_amt = float(pos.get('positionAmt', 0))
|
position_amt = float(pos.get('positionAmt', 0))
|
||||||
if position_amt == 0:
|
if position_amt == 0:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
entry_price = float(pos.get('entryPrice', 0))
|
entry_price = float(pos.get('entryPrice', 0))
|
||||||
mark_price = float(pos.get('markPrice', 0))
|
mark_price = float(pos.get('markPrice', 0)) or entry_price
|
||||||
unrealized_pnl = float(pos.get('unRealizedProfit', 0))
|
unrealized_pnl = float(pos.get('unRealizedProfit', 0))
|
||||||
|
|
||||||
if mark_price == 0:
|
|
||||||
mark_price = entry_price
|
|
||||||
|
|
||||||
# === 名义/保证金口径说明(与币安展示更接近)===
|
|
||||||
# - 币安的名义价值/仓位价值通常随标记价(markPrice)变动
|
|
||||||
# - DB 中的 notional_usdt/margin_usdt 通常是“开仓时”写入,用于复盘/统计
|
|
||||||
# - 若发生部分止盈/减仓:币安 positionAmt 会变小,但 DB 里的 notional/margin 可能仍是“原始开仓量”
|
|
||||||
# → 会出现:数量=6.8,但名义/保证金像是 13.6 的两倍(与你反馈一致)
|
|
||||||
#
|
|
||||||
# 因此:实时持仓展示统一使用“当前数量×标记价”的实时名义/保证金,
|
|
||||||
# 并额外返回 original_* 字段保留 DB 开仓口径,避免混用导致误解。
|
|
||||||
|
|
||||||
# 兼容旧字段:entry_value_usdt 仍保留(但它是按入场价计算的名义)
|
|
||||||
entry_value_usdt = abs(position_amt) * entry_price
|
entry_value_usdt = abs(position_amt) * entry_price
|
||||||
|
leverage = max(1.0, float(pos.get('leverage', 1)))
|
||||||
leverage = float(pos.get('leverage', 1))
|
|
||||||
if leverage <= 0:
|
|
||||||
leverage = 1.0
|
|
||||||
|
|
||||||
# 当前持仓名义价值(USDT):按标记价
|
|
||||||
notional_usdt_live = abs(position_amt) * mark_price
|
notional_usdt_live = abs(position_amt) * mark_price
|
||||||
# 当前持仓保证金(USDT):名义/杠杆
|
|
||||||
margin_usdt_live = notional_usdt_live / leverage
|
margin_usdt_live = notional_usdt_live / leverage
|
||||||
pnl_percent = 0
|
pnl_percent = (unrealized_pnl / margin_usdt_live * 100) if margin_usdt_live > 0 else 0
|
||||||
if margin_usdt_live > 0:
|
|
||||||
pnl_percent = (unrealized_pnl / margin_usdt_live) * 100
|
|
||||||
|
|
||||||
# 尝试从数据库获取开仓时间、止损止盈价格(以及交易规模字段)
|
|
||||||
entry_time = None
|
entry_time = None
|
||||||
stop_loss_price = None
|
stop_loss_price = None
|
||||||
take_profit_price = None
|
take_profit_price = None
|
||||||
|
|
@ -810,11 +749,11 @@ async def get_realtime_positions(account_id: int = Depends(get_account_id)):
|
||||||
db_notional_usdt = None
|
db_notional_usdt = None
|
||||||
entry_order_id = None
|
entry_order_id = None
|
||||||
entry_order_type = None
|
entry_order_type = None
|
||||||
|
id = None
|
||||||
try:
|
try:
|
||||||
from database.models import Trade
|
from database.models import Trade
|
||||||
db_trades = Trade.get_by_symbol(pos.get('symbol'), status='open')
|
db_trades = Trade.get_by_symbol(pos.get('symbol'), status='open', account_id=account_id)
|
||||||
if db_trades:
|
if db_trades:
|
||||||
# 找到匹配的交易记录(优先通过 entry_price 近似匹配;否则取最新一条 open 记录兜底)
|
|
||||||
matched = None
|
matched = None
|
||||||
for db_trade in db_trades:
|
for db_trade in db_trades:
|
||||||
try:
|
try:
|
||||||
|
|
@ -825,7 +764,6 @@ async def get_realtime_positions(account_id: int = Depends(get_account_id)):
|
||||||
continue
|
continue
|
||||||
if matched is None:
|
if matched is None:
|
||||||
matched = db_trades[0]
|
matched = db_trades[0]
|
||||||
|
|
||||||
entry_time = matched.get('entry_time')
|
entry_time = matched.get('entry_time')
|
||||||
stop_loss_price = matched.get('stop_loss_price')
|
stop_loss_price = matched.get('stop_loss_price')
|
||||||
take_profit_price = matched.get('take_profit_price')
|
take_profit_price = matched.get('take_profit_price')
|
||||||
|
|
@ -835,10 +773,10 @@ async def get_realtime_positions(account_id: int = Depends(get_account_id)):
|
||||||
db_margin_usdt = matched.get('margin_usdt')
|
db_margin_usdt = matched.get('margin_usdt')
|
||||||
db_notional_usdt = matched.get('notional_usdt')
|
db_notional_usdt = matched.get('notional_usdt')
|
||||||
entry_order_id = matched.get('entry_order_id')
|
entry_order_id = matched.get('entry_order_id')
|
||||||
|
id = matched.get('id')
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"获取数据库信息失败: {e}")
|
logger.debug(f"获取数据库信息失败: {e}")
|
||||||
|
|
||||||
# 如果数据库中有 entry_order_id,尝试从币安查询订单类型(LIMIT/MARKET)
|
|
||||||
if entry_order_id:
|
if entry_order_id:
|
||||||
try:
|
try:
|
||||||
info = await client.client.futures_get_order(symbol=pos.get('symbol'), orderId=int(entry_order_id))
|
info = await client.client.futures_get_order(symbol=pos.get('symbol'), orderId=int(entry_order_id))
|
||||||
|
|
@ -847,47 +785,36 @@ async def get_realtime_positions(account_id: int = Depends(get_account_id)):
|
||||||
except Exception:
|
except Exception:
|
||||||
entry_order_type = None
|
entry_order_type = None
|
||||||
|
|
||||||
# 如果没有从数据库获取到止损止盈价格,前端会自己计算
|
|
||||||
# 注意:数据库可能没有存储止损止盈价格,这是正常的
|
|
||||||
|
|
||||||
formatted_positions.append({
|
formatted_positions.append({
|
||||||
|
"id": id,
|
||||||
"symbol": pos.get('symbol'),
|
"symbol": pos.get('symbol'),
|
||||||
"side": "BUY" if position_amt > 0 else "SELL",
|
"side": "BUY" if position_amt > 0 else "SELL",
|
||||||
"quantity": abs(position_amt),
|
"quantity": abs(position_amt),
|
||||||
"entry_price": entry_price,
|
"entry_price": entry_price,
|
||||||
# 兼容旧字段:entry_value_usdt 仍保留(前端已有使用)
|
|
||||||
"entry_value_usdt": entry_value_usdt,
|
"entry_value_usdt": entry_value_usdt,
|
||||||
# 实时展示字段:与币安更一致(按当前数量×标记价)
|
|
||||||
"notional_usdt": notional_usdt_live,
|
"notional_usdt": notional_usdt_live,
|
||||||
"margin_usdt": margin_usdt_live,
|
"margin_usdt": margin_usdt_live,
|
||||||
# 额外返回“开仓记录口径”(用于排查部分止盈/减仓导致的不一致)
|
|
||||||
"original_notional_usdt": db_notional_usdt,
|
"original_notional_usdt": db_notional_usdt,
|
||||||
"original_margin_usdt": db_margin_usdt,
|
"original_margin_usdt": db_margin_usdt,
|
||||||
"mark_price": mark_price,
|
"mark_price": mark_price,
|
||||||
"pnl": unrealized_pnl,
|
"pnl": unrealized_pnl,
|
||||||
"pnl_percent": pnl_percent, # 基于保证金的盈亏百分比
|
"pnl_percent": pnl_percent,
|
||||||
"leverage": int(pos.get('leverage', 1)),
|
"leverage": int(pos.get('leverage', 1)),
|
||||||
"entry_time": entry_time, # 开仓时间
|
"entry_time": entry_time,
|
||||||
"stop_loss_price": stop_loss_price, # 止损价格(如果可用)
|
"stop_loss_price": stop_loss_price,
|
||||||
"take_profit_price": take_profit_price, # 止盈价格(如果可用)
|
"take_profit_price": take_profit_price,
|
||||||
"take_profit_1": take_profit_1,
|
"take_profit_1": take_profit_1,
|
||||||
"take_profit_2": take_profit_2,
|
"take_profit_2": take_profit_2,
|
||||||
"atr": atr_value,
|
"atr": atr_value,
|
||||||
"entry_order_id": entry_order_id,
|
"entry_order_id": entry_order_id,
|
||||||
"entry_order_type": entry_order_type, # LIMIT / MARKET(用于仪表板展示“限价/市价”)
|
"entry_order_type": entry_order_type,
|
||||||
"open_orders": open_orders_map.get(pos.get('symbol'), []), # 实时挂单信息
|
"open_orders": open_orders_map.get(pos.get('symbol'), []),
|
||||||
})
|
})
|
||||||
|
|
||||||
logger.info(f"格式化后 {len(formatted_positions)} 个有效持仓")
|
|
||||||
return formatted_positions
|
return formatted_positions
|
||||||
except HTTPException:
|
|
||||||
raise
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
error_msg = f"获取持仓数据失败: {str(e)}"
|
logger.warning(f"fetch_realtime_positions(account_id={account_id}) 失败: {e}", exc_info=True)
|
||||||
logger.error(error_msg, exc_info=True)
|
return []
|
||||||
raise HTTPException(status_code=500, detail=error_msg)
|
|
||||||
finally:
|
finally:
|
||||||
# 确保断开连接(避免连接泄漏)
|
|
||||||
try:
|
try:
|
||||||
if client is not None:
|
if client is not None:
|
||||||
await client.disconnect()
|
await client.disconnect()
|
||||||
|
|
@ -895,6 +822,18 @@ async def get_realtime_positions(account_id: int = Depends(get_account_id)):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/positions")
|
||||||
|
async def get_realtime_positions(account_id: int = Depends(get_account_id)):
|
||||||
|
"""获取实时持仓数据(币安实际持仓,并关联本账号 DB 记录)"""
|
||||||
|
api_key, api_secret, _, _ = Account.get_credentials(account_id)
|
||||||
|
if not api_key or not api_secret:
|
||||||
|
raise HTTPException(status_code=400, detail=f"API密钥未配置(account_id={account_id})")
|
||||||
|
result = await fetch_realtime_positions(account_id)
|
||||||
|
if result is None:
|
||||||
|
raise HTTPException(status_code=500, detail="获取持仓数据失败")
|
||||||
|
return result
|
||||||
|
|
||||||
|
|
||||||
@router.post("/positions/{symbol}/close")
|
@router.post("/positions/{symbol}/close")
|
||||||
async def close_position(symbol: str, account_id: int = Depends(get_account_id)):
|
async def close_position(symbol: str, account_id: int = Depends(get_account_id)):
|
||||||
"""手动平仓指定交易对的持仓"""
|
"""手动平仓指定交易对的持仓"""
|
||||||
|
|
|
||||||
|
|
@ -151,32 +151,31 @@ async def get_dashboard_data(account_id: int = Depends(get_account_id)):
|
||||||
"open_positions": 0
|
"open_positions": 0
|
||||||
}
|
}
|
||||||
|
|
||||||
# 获取持仓数据(强制使用数据库记录)
|
# 获取持仓数据:优先「币安实时持仓」(含本系统下的挂单),失败时回退到数据库列表
|
||||||
open_trades = []
|
open_trades = []
|
||||||
positions_error = None
|
positions_error = None
|
||||||
try:
|
try:
|
||||||
db_trades = Trade.get_all(status='open', account_id=account_id)
|
try:
|
||||||
# 格式化数据库记录,添加 entry_value_usdt 字段
|
from api.routes.account import fetch_realtime_positions
|
||||||
|
open_trades = await fetch_realtime_positions(account_id)
|
||||||
|
except Exception as fetch_err:
|
||||||
|
logger.warning(f"获取币安实时持仓失败,回退到数据库列表: {fetch_err}")
|
||||||
open_trades = []
|
open_trades = []
|
||||||
|
if not open_trades:
|
||||||
|
db_trades = Trade.get_all(status='open', account_id=account_id)
|
||||||
for trade in db_trades:
|
for trade in db_trades:
|
||||||
entry_value_usdt = float(trade.get('quantity', 0)) * float(trade.get('entry_price', 0))
|
entry_value_usdt = float(trade.get('quantity', 0)) * float(trade.get('entry_price', 0))
|
||||||
leverage = float(trade.get('leverage', 1))
|
leverage = float(trade.get('leverage', 1))
|
||||||
pnl = float(trade.get('pnl', 0))
|
pnl = float(trade.get('pnl', 0))
|
||||||
|
|
||||||
# 数据库中的pnl_percent是价格涨跌幅,需要转换为收益率
|
|
||||||
# 收益率 = 盈亏 / 保证金
|
|
||||||
margin = entry_value_usdt / leverage if leverage > 0 else entry_value_usdt
|
margin = entry_value_usdt / leverage if leverage > 0 else entry_value_usdt
|
||||||
pnl_percent = (pnl / margin * 100) if margin > 0 else 0
|
pnl_percent = (pnl / margin * 100) if margin > 0 else 0
|
||||||
|
open_trades.append({
|
||||||
formatted_trade = {
|
|
||||||
**trade,
|
**trade,
|
||||||
'entry_value_usdt': entry_value_usdt,
|
'entry_value_usdt': entry_value_usdt,
|
||||||
'mark_price': trade.get('entry_price', 0), # 默认入场价,下面用实时数据覆盖
|
'mark_price': trade.get('entry_price', 0),
|
||||||
'pnl': pnl,
|
'pnl': pnl,
|
||||||
'pnl_percent': pnl_percent
|
'pnl_percent': pnl_percent
|
||||||
}
|
})
|
||||||
open_trades.append(formatted_trade)
|
|
||||||
# 合并实时持仓盈亏(mark_price / pnl / pnl_percent),仪表板可显示浮盈浮亏
|
|
||||||
try:
|
try:
|
||||||
from api.routes.account import fetch_live_positions_pnl
|
from api.routes.account import fetch_live_positions_pnl
|
||||||
live_list = await fetch_live_positions_pnl(account_id)
|
live_list = await fetch_live_positions_pnl(account_id)
|
||||||
|
|
@ -188,11 +187,11 @@ async def get_dashboard_data(account_id: int = Depends(get_account_id)):
|
||||||
t["mark_price"] = lp.get("mark_price", t.get("entry_price"))
|
t["mark_price"] = lp.get("mark_price", t.get("entry_price"))
|
||||||
t["pnl"] = lp.get("pnl", 0)
|
t["pnl"] = lp.get("pnl", 0)
|
||||||
t["pnl_percent"] = lp.get("pnl_percent", 0)
|
t["pnl_percent"] = lp.get("pnl_percent", 0)
|
||||||
if by_symbol:
|
|
||||||
logger.info(f"已合并 {len(by_symbol)} 个实时持仓盈亏到仪表板")
|
|
||||||
except Exception as merge_err:
|
except Exception as merge_err:
|
||||||
logger.debug(f"合并实时持仓盈亏失败(仪表板仍显示持仓,盈亏为 0): {merge_err}")
|
logger.debug(f"合并实时持仓盈亏失败: {merge_err}")
|
||||||
logger.info(f"使用数据库记录作为持仓数据: {len(open_trades)} 个持仓")
|
logger.info(f"使用数据库记录作为持仓数据: {len(open_trades)} 个持仓")
|
||||||
|
else:
|
||||||
|
logger.info(f"使用币安实时持仓作为列表: {len(open_trades)} 个持仓")
|
||||||
except Exception as db_error:
|
except Exception as db_error:
|
||||||
logger.error(f"从数据库获取持仓记录失败: {db_error}")
|
logger.error(f"从数据库获取持仓记录失败: {db_error}")
|
||||||
|
|
||||||
|
|
@ -286,9 +285,12 @@ async def get_dashboard_data(account_id: int = Depends(get_account_id)):
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.debug(f"获取交易配置失败: {e}")
|
logger.debug(f"获取交易配置失败: {e}")
|
||||||
|
|
||||||
|
# 本系统持仓数 = 数据库 status=open 条数,与下方「当前持仓」列表一致;币安持仓数 = 接口/快照中的 open_positions,可能与币安页面一致
|
||||||
|
open_trades_count = len(open_trades)
|
||||||
result = {
|
result = {
|
||||||
"account": account_data,
|
"account": account_data,
|
||||||
"open_trades": open_trades,
|
"open_trades": open_trades,
|
||||||
|
"open_trades_count": open_trades_count, # 本系统持仓数,与列表条数一致
|
||||||
"recent_scans": recent_scans,
|
"recent_scans": recent_scans,
|
||||||
"recent_signals": recent_signals,
|
"recent_signals": recent_signals,
|
||||||
"position_stats": position_stats,
|
"position_stats": position_stats,
|
||||||
|
|
@ -296,7 +298,7 @@ async def get_dashboard_data(account_id: int = Depends(get_account_id)):
|
||||||
"_debug": { # 添加调试信息
|
"_debug": { # 添加调试信息
|
||||||
"account_id": account_id,
|
"account_id": account_id,
|
||||||
"account_data_total_balance": account_data.get('total_balance', 'N/A') if account_data else 'N/A',
|
"account_data_total_balance": account_data.get('total_balance', 'N/A') if account_data else 'N/A',
|
||||||
"open_trades_count": len(open_trades),
|
"open_trades_count": open_trades_count,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -467,6 +467,12 @@
|
||||||
margin: 0;
|
margin: 0;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.positions-subtitle {
|
||||||
|
margin: 0 0 10px 0;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6c757d;
|
||||||
|
}
|
||||||
|
|
||||||
.sltp-all-btn {
|
.sltp-all-btn {
|
||||||
padding: 0.5rem 0.9rem;
|
padding: 0.5rem 0.9rem;
|
||||||
background: #1f7aec;
|
background: #1f7aec;
|
||||||
|
|
|
||||||
|
|
@ -408,7 +408,12 @@ const StatsDashboard = () => {
|
||||||
</div>
|
</div>
|
||||||
<div className="info-item">
|
<div className="info-item">
|
||||||
<span className="label">持仓数量:</span>
|
<span className="label">持仓数量:</span>
|
||||||
<span className="value">{account.open_positions}</span>
|
<span className="value">
|
||||||
|
本系统 <strong>{openTrades.length}</strong>
|
||||||
|
{account.open_positions != null && Number(account.open_positions) !== openTrades.length && (
|
||||||
|
<> · 币安 <strong>{account.open_positions}</strong></>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div className="info-item">
|
<div className="info-item">
|
||||||
<span className="label">持仓模式:</span>
|
<span className="label">持仓模式:</span>
|
||||||
|
|
@ -524,6 +529,11 @@ const StatsDashboard = () => {
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
{account?.open_positions != null && openTrades.length !== Number(account.open_positions) && (
|
||||||
|
<p className="positions-subtitle" title="仅展示本系统开仓并记录的持仓;币安上其它持仓(如手动开仓)不会出现在此列表">
|
||||||
|
仅本系统记录 <strong>{openTrades.length}</strong> 条,币安实际 <strong>{account.open_positions}</strong> 个
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
<div className="entry-type-summary">
|
<div className="entry-type-summary">
|
||||||
<span className="entry-type-badge limit">限价入场: {entryTypeCounts.limit}</span>
|
<span className="entry-type-badge limit">限价入场: {entryTypeCounts.limit}</span>
|
||||||
<span className="entry-type-badge market">市价入场: {entryTypeCounts.market}</span>
|
<span className="entry-type-badge market">市价入场: {entryTypeCounts.market}</span>
|
||||||
|
|
|
||||||
|
|
@ -776,16 +776,18 @@ class BinanceClient:
|
||||||
self.client.futures_position_information(recvWindow=20000),
|
self.client.futures_position_information(recvWindow=20000),
|
||||||
timeout=read_timeout
|
timeout=read_timeout
|
||||||
)
|
)
|
||||||
# 只保留真实持仓:非零且名义价值 >= 1 USDT,避免灰尘持仓被当成“有仓”导致同步时批量创建假 manual_entry
|
# 只保留非零持仓,且名义价值 >= 配置阈值,避免灰尘持仓被当成“有仓”;与仪表板不一致时可调低 POSITION_MIN_NOTIONAL_USDT 或设为 0
|
||||||
min_notional = 1.0
|
min_notional = getattr(config, 'POSITION_MIN_NOTIONAL_USDT', 1.0)
|
||||||
open_positions = []
|
open_positions = []
|
||||||
|
skipped_low = []
|
||||||
for pos in positions:
|
for pos in positions:
|
||||||
amt = float(pos['positionAmt'])
|
amt = float(pos['positionAmt'])
|
||||||
if amt == 0:
|
if amt == 0:
|
||||||
continue
|
continue
|
||||||
entry_price = float(pos['entryPrice'])
|
entry_price = float(pos['entryPrice'])
|
||||||
notional = abs(amt) * entry_price
|
notional = abs(amt) * entry_price
|
||||||
if notional < min_notional:
|
if min_notional > 0 and notional < min_notional:
|
||||||
|
skipped_low.append((pos['symbol'], round(notional, 4)))
|
||||||
continue
|
continue
|
||||||
open_positions.append({
|
open_positions.append({
|
||||||
'symbol': pos['symbol'],
|
'symbol': pos['symbol'],
|
||||||
|
|
@ -795,6 +797,11 @@ class BinanceClient:
|
||||||
'unRealizedProfit': float(pos['unRealizedProfit']),
|
'unRealizedProfit': float(pos['unRealizedProfit']),
|
||||||
'leverage': int(pos['leverage'])
|
'leverage': int(pos['leverage'])
|
||||||
})
|
})
|
||||||
|
if skipped_low and logger.isEnabledFor(logging.DEBUG):
|
||||||
|
logger.debug(
|
||||||
|
f"获取持仓: 过滤掉 {len(skipped_low)} 个名义价值 < {min_notional} USDT 的仓位 {skipped_low},"
|
||||||
|
"与仪表板不一致时可设 POSITION_MIN_NOTIONAL_USDT=0 或更小"
|
||||||
|
)
|
||||||
return open_positions
|
return open_positions
|
||||||
except (asyncio.TimeoutError, BinanceAPIException) as e:
|
except (asyncio.TimeoutError, BinanceAPIException) as e:
|
||||||
last_error = e
|
last_error = e
|
||||||
|
|
|
||||||
|
|
@ -391,6 +391,8 @@ CONNECTION_TIMEOUT = int(os.getenv('CONNECTION_TIMEOUT', '30')) # 连接超时
|
||||||
CONNECTION_RETRIES = int(os.getenv('CONNECTION_RETRIES', '3')) # 连接重试次数
|
CONNECTION_RETRIES = int(os.getenv('CONNECTION_RETRIES', '3')) # 连接重试次数
|
||||||
# 仅用于 get_open_positions / get_recent_trades 等只读接口的单次等待时间,不影响下单/止损止盈的快速失败
|
# 仅用于 get_open_positions / get_recent_trades 等只读接口的单次等待时间,不影响下单/止损止盈的快速失败
|
||||||
READ_ONLY_REQUEST_TIMEOUT = int(os.getenv('READ_ONLY_REQUEST_TIMEOUT', '60'))
|
READ_ONLY_REQUEST_TIMEOUT = int(os.getenv('READ_ONLY_REQUEST_TIMEOUT', '60'))
|
||||||
|
# 获取持仓时过滤掉名义价值低于此值的仓位(USDT),与币安仪表板不一致时可调低或设为 0
|
||||||
|
POSITION_MIN_NOTIONAL_USDT = float(os.getenv('POSITION_MIN_NOTIONAL_USDT', '1.0'))
|
||||||
|
|
||||||
# Redis 缓存配置(优先从数据库,回退到环境变量和默认值)
|
# Redis 缓存配置(优先从数据库,回退到环境变量和默认值)
|
||||||
REDIS_URL = _get_config_value('REDIS_URL', os.getenv('REDIS_URL', 'redis://localhost:6379'))
|
REDIS_URL = _get_config_value('REDIS_URL', os.getenv('REDIS_URL', 'redis://localhost:6379'))
|
||||||
|
|
|
||||||
|
|
@ -2951,22 +2951,28 @@ class PositionManager:
|
||||||
logger.warning("客户端未初始化,无法启动实时监控")
|
logger.warning("客户端未初始化,无法启动实时监控")
|
||||||
return
|
return
|
||||||
|
|
||||||
# 获取当前所有持仓
|
# 获取当前所有持仓(与 sync 一致:仅本系统关心的持仓会进 active_positions)
|
||||||
positions = await self.client.get_open_positions()
|
positions = await self.client.get_open_positions()
|
||||||
binance_symbols = {p['symbol'] for p in positions}
|
binance_symbols = {p['symbol'] for p in positions}
|
||||||
active_symbols = set(self.active_positions.keys())
|
active_symbols = set(self.active_positions.keys())
|
||||||
|
sync_create_manual = config.TRADING_CONFIG.get("SYNC_CREATE_MANUAL_ENTRY_RECORD", False)
|
||||||
|
|
||||||
logger.info(f"币安持仓: {len(binance_symbols)} 个 ({', '.join(binance_symbols) if binance_symbols else '无'})")
|
logger.info(f"币安持仓: {len(binance_symbols)} 个 ({', '.join(binance_symbols) if binance_symbols else '无'})")
|
||||||
logger.info(f"本地持仓记录: {len(active_symbols)} 个 ({', '.join(active_symbols) if active_symbols else '无'})")
|
logger.info(f"本地持仓记录: {len(active_symbols)} 个 ({', '.join(active_symbols) if active_symbols else '无'})")
|
||||||
|
|
||||||
# 为所有币安持仓启动监控(即使不在active_positions中,可能是手动开仓的)
|
# 仅为本系统已有记录的持仓启动监控;若未开启「同步创建手动开仓记录」,则不为「仅币安有仓」创建临时记录或监控
|
||||||
|
only_binance = binance_symbols - active_symbols
|
||||||
|
if only_binance and not sync_create_manual:
|
||||||
|
logger.info(f"跳过 {len(only_binance)} 个仅币安持仓的监控(SYNC_CREATE_MANUAL_ENTRY_RECORD=False): {', '.join(only_binance)}")
|
||||||
|
|
||||||
for position in positions:
|
for position in positions:
|
||||||
symbol = position['symbol']
|
symbol = position['symbol']
|
||||||
if symbol not in self._monitor_tasks:
|
if symbol not in self._monitor_tasks:
|
||||||
# 如果不在active_positions中,先创建记录
|
# 若不在 active_positions 且未开启「同步创建手动开仓记录」,不创建临时记录、不为其启动监控
|
||||||
if symbol not in self.active_positions:
|
if symbol not in self.active_positions:
|
||||||
|
if not sync_create_manual:
|
||||||
|
continue
|
||||||
logger.warning(f"{symbol} 在币安有持仓但不在本地记录中,可能是手动开仓,尝试创建记录...")
|
logger.warning(f"{symbol} 在币安有持仓但不在本地记录中,可能是手动开仓,尝试创建记录...")
|
||||||
# 这里会通过sync_positions_with_binance来处理,但先启动监控
|
|
||||||
try:
|
try:
|
||||||
entry_price = position.get('entryPrice', 0)
|
entry_price = position.get('entryPrice', 0)
|
||||||
position_amt = position['positionAmt']
|
position_amt = position['positionAmt']
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue
Block a user