fix(config_manager, account, trades, position_manager, risk_manager): 清理多余空行并优化代码风格

在多个模块中,移除多余的空行以提升代码可读性,并确保遵循一致的代码风格。此外,优化了部分逻辑的缩进和结构,增强了代码的整洁性和可维护性。这一改动旨在提升代码质量,确保团队协作时的代码一致性。
This commit is contained in:
薇薇安 2026-02-25 22:22:47 +08:00
parent 41b2a21c3d
commit f3ce4d5d11
6 changed files with 347 additions and 347 deletions

View File

@ -361,7 +361,7 @@ async def get_realtime_account_data(account_id: int = 1):
logger.info(f" - API密钥长度: {len(api_key)} 字符")
else:
logger.warning(" - API密钥为空!")
logger.info(f" - API密钥Secret存在: {bool(api_secret)}")
if api_secret:
logger.info(f" - API密钥Secret长度: {len(api_secret)} 字符")
@ -667,7 +667,7 @@ async def fetch_realtime_positions(account_id: int):
trading_system_path = project_root / 'trading_system'
sys.path.insert(0, str(trading_system_path))
from binance_client import BinanceClient
client = BinanceClient(api_key=api_key, api_secret=api_secret, testnet=use_testnet)
await client.connect()
positions = await client.get_open_positions()
@ -758,9 +758,9 @@ async def fetch_realtime_positions(account_id: int):
matched = None
for db_trade in db_trades:
try:
if abs(float(db_trade.get('entry_price', 0)) - entry_price) < 0.01:
if abs(float(db_trade.get('entry_price', 0)) - entry_price) < 0.01:
matched = db_trade
break
break
except Exception:
continue
if matched is None:
@ -779,7 +779,7 @@ async def fetch_realtime_positions(account_id: int):
id = matched.get('id')
except Exception as e:
logger.debug(f"获取数据库信息失败: {e}")
if entry_order_id:
try:
info = await client.client.futures_get_order(symbol=pos.get('symbol'), orderId=int(entry_order_id))
@ -819,7 +819,7 @@ async def fetch_realtime_positions(account_id: int):
take_profit_2 = tp_prices[1]
if take_profit_price is None:
take_profit_price = take_profit_1
formatted_positions.append({
"id": id,
"symbol": pos.get('symbol'),
@ -938,8 +938,8 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
# 兼容旧逻辑:如果原始接口异常,回退到封装方法
if not nonzero_positions:
try:
positions = await client.get_open_positions()
position = next((p for p in positions if p['symbol'] == symbol and float(p['positionAmt']) != 0), None)
positions = await client.get_open_positions()
position = next((p for p in positions if p['symbol'] == symbol and float(p['positionAmt']) != 0), None)
if position:
nonzero_positions = [(float(position["positionAmt"]), {"positionAmt": position["positionAmt"]})]
except Exception:
@ -981,7 +981,7 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
"symbol": symbol,
"status": "closed"
}
# 获取交易对精度信息,调整数量精度(平仓不要向上补 minQty避免超过持仓数量
symbol_info = None
try:
@ -995,7 +995,7 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
q = float(qty)
if not symbol_info:
return q
quantity_precision = symbol_info.get('quantityPrecision', 8)
quantity_precision = symbol_info.get('quantityPrecision', 8)
step_size = float(symbol_info.get('stepSize', 0) or 0)
if step_size and step_size > 0:
# 向下取整,避免超过持仓
@ -1013,7 +1013,7 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
if dual_side is None:
if any(isinstance(p, dict) and (p.get("positionSide") in ("LONG", "SHORT")) for _, p in nonzero_positions):
dual_side = True
else:
else:
dual_side = False
logger.info(f"{symbol} 持仓模式: {'HEDGE(对冲)' if dual_side else 'ONE-WAY(单向)'}")
@ -1067,14 +1067,14 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
oid = order.get("orderId")
if oid:
order_ids.append(oid)
except Exception as order_error:
error_msg = f"{symbol} 平仓失败:下单异常 - {str(order_error)}"
logger.error(error_msg)
logger.error(f" 错误类型: {type(order_error).__name__}")
import traceback
logger.error(f" 完整错误堆栈:\n{traceback.format_exc()}")
raise HTTPException(status_code=500, detail=error_msg)
except Exception as order_error:
error_msg = f"{symbol} 平仓失败:下单异常 - {str(order_error)}"
logger.error(error_msg)
logger.error(f" 错误类型: {type(order_error).__name__}")
import traceback
logger.error(f" 完整错误堆栈:\n{traceback.format_exc()}")
raise HTTPException(status_code=500, detail=error_msg)
if not orders:
raise HTTPException(status_code=400, detail=f"{symbol} 无可平仓的有效仓位数量调整后为0或无持仓")
@ -1103,17 +1103,17 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
try:
# 1. 获取价格
order_info = await client.client.futures_get_order(symbol=symbol, orderId=oid)
if order_info:
if order_info:
p = float(order_info.get('avgPrice', 0)) or float(order_info.get('price', 0))
if p <= 0 and order_info.get('fills'):
total_qty = 0
total_value = 0
for fill in order_info.get('fills', []):
qty = float(fill.get('qty', 0))
price = float(fill.get('price', 0))
total_qty += qty
total_value += qty * price
if total_qty > 0:
total_qty = 0
total_value = 0
for fill in order_info.get('fills', []):
qty = float(fill.get('qty', 0))
price = float(fill.get('price', 0))
total_qty += qty
total_value += qty * price
if total_qty > 0:
p = total_value / total_qty
if p > 0:
exit_prices[oid] = p
@ -1132,9 +1132,9 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
exit_realized_pnls[oid] = total_realized_pnl
exit_commissions[oid] = total_commission
exit_commission_assets[oid] = "/".join(commission_assets) if commission_assets else None
except Exception as e:
except Exception as e:
logger.warning(f"获取订单详情失败 (orderId={oid}): {e}")
# 兜底:如果无法获取订单价格,使用当前价格
fallback_exit_price = None
try:
@ -1150,8 +1150,8 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
used_order_ids = set()
for trade in open_trades:
try:
entry_price = float(trade['entry_price'])
trade_quantity = float(trade['quantity'])
entry_price = float(trade['entry_price'])
trade_quantity = float(trade['quantity'])
except Exception:
continue
@ -1171,24 +1171,24 @@ async def close_position(symbol: str, account_id: int = Depends(get_account_id))
exit_price = fallback_exit_price or entry_price
# 计算盈亏(数据库侧依旧按名义盈亏;收益率展示用保证金口径在前端/统计里另算)
if trade['side'] == 'BUY':
pnl = (exit_price - entry_price) * trade_quantity
pnl_percent = ((exit_price - entry_price) / entry_price) * 100
else:
pnl = (entry_price - exit_price) * trade_quantity
pnl_percent = ((entry_price - exit_price) / entry_price) * 100
Trade.update_exit(
trade_id=trade['id'],
exit_price=exit_price,
exit_reason='manual',
pnl=pnl,
pnl_percent=pnl_percent,
if trade['side'] == 'BUY':
pnl = (exit_price - entry_price) * trade_quantity
pnl_percent = ((exit_price - entry_price) / entry_price) * 100
else:
pnl = (entry_price - exit_price) * trade_quantity
pnl_percent = ((entry_price - exit_price) / entry_price) * 100
Trade.update_exit(
trade_id=trade['id'],
exit_price=exit_price,
exit_reason='manual',
pnl=pnl,
pnl_percent=pnl_percent,
exit_order_id=chosen_oid,
realized_pnl=exit_realized_pnls.get(chosen_oid),
commission=exit_commissions.get(chosen_oid),
commission_asset=exit_commission_assets.get(chosen_oid)
)
)
logger.info(f"✓ 已更新数据库记录 trade_id={trade['id']} order_id={chosen_oid} (盈亏: {pnl:.2f} USDT, {pnl_percent:.2f}%)")
logger.info(f"{symbol} 平仓成功")
@ -1839,8 +1839,8 @@ async def sync_positions(
pass
if not exit_price or exit_price <= 0:
ticker = await client.get_ticker_24h(symbol)
exit_price = float(ticker['price']) if ticker else entry_price
ticker = await client.get_ticker_24h(symbol)
exit_price = float(ticker['price']) if ticker else entry_price
# 计算盈亏
if trade['side'] == 'BUY':
@ -1869,7 +1869,7 @@ async def sync_positions(
sync_realized_pnl = sum(float(t.get('realizedPnl', 0)) for t in related)
except Exception as fee_err:
logger.debug(f"同步 {symbol} 平仓手续费失败: {fee_err}")
# 更新数据库记录
duration_minutes = None
try:

View File

@ -412,7 +412,7 @@ async def sync_trades_from_binance(
testnet=use_testnet,
)
await client.connect()
# 获取需要同步的 symbol 列表
symbol_list = []
auto_full_sync = False # 是否因 DB 无记录而自动全量
@ -537,7 +537,7 @@ async def sync_trades_from_binance(
# 按时间排序,从旧到新
all_orders.sort(key=lambda x: x.get('time', 0))
# 全量或自动全量时,对无法匹配的开仓订单也创建新记录
effective_sync_all = sync_all_symbols or auto_full_sync
@ -571,8 +571,8 @@ async def sync_trades_from_binance(
try:
# 这是平仓订单close_orders 已经筛选出 reduceOnly=True 的订单)
# 首先检查是否已经通过订单号同步过(避免重复)
existing_trade = Trade.get_by_exit_order_id(order_id)
# 首先检查是否已经通过订单号同步过(避免重复)
existing_trade = Trade.get_by_exit_order_id(order_id)
# 如果已有 exit_order_id 且 exit_reason 不是 sync说明已完整同步跳过
if existing_trade and existing_trade.get("exit_order_id") and existing_trade.get("exit_reason") not in (None, "", "sync"):
skipped_existing += 1
@ -608,21 +608,21 @@ async def sync_trades_from_binance(
# 如果之前没有 exit_order_id记录为补全
if not trade.get("exit_order_id") or str(trade.get("exit_order_id")).strip() in ("", "0"):
exit_order_id_filled += 1
# 计算盈亏
entry_price = float(trade['entry_price'])
entry_quantity = float(trade['quantity'])
# 使用实际成交数量(可能部分平仓)
actual_quantity = min(quantity, entry_quantity)
if trade['side'] == 'BUY':
pnl = (avg_price - entry_price) * actual_quantity
pnl_percent = ((avg_price - entry_price) / entry_price) * 100
else: # SELL
pnl = (entry_price - avg_price) * actual_quantity
pnl_percent = ((entry_price - avg_price) / entry_price) * 100
# 计算盈亏
entry_price = float(trade['entry_price'])
entry_quantity = float(trade['quantity'])
# 使用实际成交数量(可能部分平仓)
actual_quantity = min(quantity, entry_quantity)
if trade['side'] == 'BUY':
pnl = (avg_price - entry_price) * actual_quantity
pnl_percent = ((avg_price - entry_price) / entry_price) * 100
else: # SELL
pnl = (entry_price - avg_price) * actual_quantity
pnl_percent = ((entry_price - avg_price) / entry_price) * 100
# 细分 exit_reason优先使用币安订单类型其次用价格接近止损/止盈做兜底
exit_reason = "sync"
# 检查订单的 reduceOnly 字段:如果是 true说明是自动平仓不应该标记为 manual
@ -698,20 +698,20 @@ async def sync_trades_from_binance(
duration_minutes = None
# 更新数据库(包含订单号、手续费与实际盈亏)
Trade.update_exit(
trade_id=trade_id,
exit_price=avg_price,
Trade.update_exit(
trade_id=trade_id,
exit_price=avg_price,
exit_reason=exit_reason,
pnl=pnl,
pnl_percent=pnl_percent,
pnl=pnl,
pnl_percent=pnl_percent,
exit_order_id=order_id, # 保存订单号,确保唯一性
duration_minutes=duration_minutes,
exit_time_ts=exit_time_ts,
commission=sync_commission,
commission_asset=sync_commission_asset,
realized_pnl=sync_realized_pnl,
)
updated_count += 1
)
updated_count += 1
logger.info(
f"✓ 更新平仓记录: {symbol} (ID: {trade_id}, 订单号: {order_id}, "
f"类型: {otype or '-'}, 原因: {exit_reason}, 成交价: {avg_price:.4f})"
@ -754,7 +754,7 @@ async def sync_trades_from_binance(
existing_trade = Trade.get_by_entry_order_id(order_id)
if existing_trade:
# 如果已存在,跳过(开仓订单信息通常已完整)
logger.debug(f"开仓订单 {order_id} 已存在,跳过")
logger.debug(f"开仓订单 {order_id} 已存在,跳过")
else:
# 如果不存在,尝试查找没有 entry_order_id 的记录并补全,或创建新记录
try:

View File

@ -369,7 +369,7 @@ class ConfigManager:
# 从环境变量获取SSL配置如果未设置使用默认值
ssl_cert_reqs = os.getenv('REDIS_SSL_CERT_REQS', 'required')
ssl_ca_certs = os.getenv('REDIS_SSL_CA_CERTS', None)
connection_kwargs['select'] = os.getenv('REDIS_SELECT', 0)
if connection_kwargs['select'] is not None:
connection_kwargs['select'] = int(connection_kwargs['select'])
@ -652,7 +652,7 @@ class ConfigManager:
# 4. 返回默认值
return default
def set(self, key, value, config_type='string', category='general', description=None):
"""设置配置同时更新数据库、Redis缓存和本地缓存"""
# 账号私有API Key/Secret/Testnet 写入 accounts 表

View File

@ -267,7 +267,7 @@ class BinanceClient:
logger.warning("API密钥Secret已更新但客户端已连接需要重新连接才能使用新密钥")
except Exception as e:
logger.debug(f"从配置管理器刷新API密钥失败: {e},使用现有值")
# 注意redis_cache 已在 __init__ 中初始化,这里不需要再次初始化
async def connect(self, timeout: int = None, retries: int = None, requests_params: Dict = None):
@ -282,7 +282,7 @@ class BinanceClient:
# 连接前刷新API密钥确保使用最新值支持热更新
# 但如果 API 密钥为空(只用于获取公开行情),则跳过
if self.api_key and self.api_secret:
self._refresh_api_credentials()
self._refresh_api_credentials()
else:
logger.info("BinanceClient: 使用公开 API无需认证只能获取行情数据")
@ -322,7 +322,7 @@ class BinanceClient:
# 验证API密钥权限仅当提供了有效的 API key 时)
if self.api_key and self.api_secret:
await self._verify_api_permissions()
await self._verify_api_permissions()
else:
logger.info("✓ 使用公开 API跳过权限验证只能获取行情数据")
@ -702,7 +702,7 @@ class BinanceClient:
if self.client:
await self.client.close_connection()
logger.info("币安客户端已断开连接")
def _resolve_api_symbol(self, symbol: str) -> str:
"""
将显示名含中文等非 ASCII解析为交易所 API 使用的英文 symbol
@ -755,13 +755,13 @@ class BinanceClient:
self._display_to_api_symbol.update(display_to_api)
if display_to_api:
logger.info(f"已映射 {len(display_to_api)} 个中文/非ASCII交易对到英文 symbol均可正常下单")
logger.info(f"获取到 {len(usdt_pairs)} 个USDT永续合约交易对")
logger.info(f"获取到 {len(usdt_pairs)} 个USDT永续合约交易对")
# 回写 DB 供下次使用
try:
await loop.run_in_executor(None, lambda: _save_exchange_info_to_db(exchange_info))
except Exception as e:
logger.debug("exchange_info 写入 DB 失败: %s", e)
return usdt_pairs
return usdt_pairs
except asyncio.TimeoutError:
if attempt < max_retries:
@ -772,9 +772,9 @@ class BinanceClient:
logger.error(f"获取交易对失败:{max_retries}次重试后仍然超时")
return []
except BinanceAPIException as e:
except BinanceAPIException as e:
logger.error(f"获取交易对失败API错误: {e}")
return []
return []
except Exception as e:
if attempt < max_retries:
@ -870,7 +870,7 @@ class BinanceClient:
from .market_ws_leader import KEY_KLINE_PREFIX
shared_key = f"{KEY_KLINE_PREFIX}{symbol.upper()}:{interval.lower()}"
# 使用较长的 TTL因为这是共享缓存多个账号都会使用
ttl_map = {
ttl_map = {
'1m': 60, '3m': 120, '5m': 180, '15m': 300, '30m': 600,
'1h': 900, '2h': 1800, '4h': 3600, '6h': 5400, '8h': 7200, '12h': 10800, '1d': 21600
}
@ -902,10 +902,10 @@ class BinanceClient:
"""
获取24小时行情数据合约市场
优先从WebSocket缓存读取其次从Redis缓存读取最后使用REST API
Args:
symbol: 交易对支持中文名会解析为 API symbol
Returns:
24小时行情数据
"""
@ -1152,7 +1152,7 @@ class BinanceClient:
params["endTime"] = end_time
out = await self._futures_data_get("takerlongshortRatio", params)
return out if isinstance(out, list) else []
async def get_account_balance(self) -> Dict[str, float]:
"""
获取U本位合约账户余额
@ -1199,7 +1199,7 @@ class BinanceClient:
except BinanceAPIException as e:
error_code = e.code if hasattr(e, 'code') else None
error_msg = str(e)
# 合并成“单条多行日志”,避免日志/Redis 里刷屏
lines = [
"=" * 60,
@ -1280,19 +1280,19 @@ class BinanceClient:
skipped_low.append((pos['symbol'], round(notional, 4)))
continue
open_positions.append({
'symbol': pos['symbol'],
'symbol': pos['symbol'],
'positionAmt': amt,
'entryPrice': entry_price,
'markPrice': float(pos.get('markPrice', 0)),
'unRealizedProfit': float(pos['unRealizedProfit']),
'leverage': int(pos['leverage'])
'markPrice': float(pos.get('markPrice', 0)),
'unRealizedProfit': float(pos['unRealizedProfit']),
'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:
last_error = e
# 如果是API异常检查是否是网络相关或服务器错误
@ -1366,16 +1366,16 @@ class BinanceClient:
return []
if last_error:
logger.warning(f"获取成交记录失败 {symbol} (已重试 {attempts_made} 次): {_format_exception(last_error)}")
return []
return []
async def get_symbol_info(self, symbol: str) -> Optional[Dict]:
"""
获取交易对的精度和限制信息
优先从 Redis 缓存读取如果缓存不可用或过期则使用 REST API
Args:
symbol: 交易对支持中文名会解析为 API symbol
Returns:
交易对信息字典包含 quantityPrecision, minQty, stepSize
"""
@ -1389,7 +1389,7 @@ class BinanceClient:
if isinstance(cached, dict) and ("tickSize" not in cached or "pricePrecision" not in cached):
logger.info(f"{symbol} symbol_info 缓存缺少 tickSize/pricePrecision自动刷新一次")
else:
return cached
return cached
# 2. 降级到进程内存(仅当 Redis 不可用时会有数据)
if symbol in self._symbol_info_cache:
cached_mem = self._symbol_info_cache[symbol]
@ -1498,7 +1498,7 @@ class BinanceClient:
_oldest = next(iter(self._symbol_info_cache), None)
if _oldest is not None:
self._symbol_info_cache.pop(_oldest, None)
self._symbol_info_cache[symbol] = info
self._symbol_info_cache[symbol] = info
logger.debug(f"获取 {symbol} 精度信息: {info}")
return info
@ -1821,11 +1821,11 @@ class BinanceClient:
current_price = ticker['price']
except Exception as e:
logger.debug(f"使用 bookTicker 估算价格失败,回退到 ticker: {e}")
ticker = await self.get_ticker_24h(symbol)
if not ticker:
logger.error(f"无法获取 {symbol} 的价格信息")
return None
current_price = ticker['price']
ticker = await self.get_ticker_24h(symbol)
if not ticker:
logger.error(f"无法获取 {symbol} 的价格信息")
return None
current_price = ticker['price']
else:
current_price = price
@ -1873,8 +1873,8 @@ class BinanceClient:
position = positions[0]
# 优先使用 API 返回的 leverage不再限制必须有持仓
leverage_bracket = position.get('leverage')
if leverage_bracket:
current_leverage = int(leverage_bracket)
if leverage_bracket:
current_leverage = int(leverage_bracket)
except Exception as e:
logger.debug(f"无法获取 {symbol} 的杠杆信息,使用默认值: {current_leverage}x ({e})")
@ -1997,7 +1997,7 @@ class BinanceClient:
prefix = (config.TRADING_CONFIG.get('SYSTEM_ORDER_ID_PREFIX') or '').strip()
if prefix:
order_params['newClientOrderId'] = f"{prefix}_{int(time.time() * 1000)}_{random.randint(0, 0xFFFF):04x}"[:36]
# 如果是平仓订单,添加 reduceOnly 参数
# 根据币安API文档reduceOnly 应该是字符串 "true" 或 "false"
if reduce_only:
@ -2099,7 +2099,7 @@ class BinanceClient:
if reduce_only:
logger.warning(f"下单被拒绝 {symbol} {side}: ReduceOnly(-2022)可能仓位已为0/方向腿不匹配),将由上层做幂等处理")
else:
logger.error(f"下单失败 {symbol} {side}: ReduceOnly 订单被拒绝 - {e}")
logger.error(f"下单失败 {symbol} {side}: ReduceOnly 订单被拒绝 - {e}")
elif "reduceOnly" in error_msg.lower() or "reduce only" in error_msg.lower():
logger.error(f"下单失败 {symbol} {side}: ReduceOnly 相关错误 - {e}")
logger.error(f" 错误码: {error_code}")
@ -2123,11 +2123,11 @@ class BinanceClient:
async def cancel_order(self, symbol: str, order_id: int) -> bool:
"""
取消订单
Args:
symbol: 交易对
order_id: 订单ID
Returns:
是否成功
"""
@ -2172,7 +2172,7 @@ class BinanceClient:
return False
logger.error(f"取消订单失败: {e}")
return False
# =========================
# Algo Orders条件单/止盈止损/计划委托)
# 说明:币安在 2025-12 后将 USDT-M 合约的 STOP/TP/Trailing 等条件单迁移到 Algo 接口:
@ -2645,11 +2645,11 @@ class BinanceClient:
"""
设置杠杆倍数
如果设置失败比如超过交易对支持的最大杠杆会自动降低杠杆重试
Args:
symbol: 交易对
leverage: 杠杆倍数可为 int float内部会转为 int
Returns:
成功设置的杠杆倍数如果失败返回 0
"""
@ -2677,8 +2677,8 @@ class BinanceClient:
else:
logger.error(f"设置杠杆请求超时 ({symbol} {target_leverage}x),已重试 2 次仍失败")
return 0
except BinanceAPIException as e:
error_msg = str(e).lower()
except BinanceAPIException as e:
error_msg = str(e).lower()
logger.warning(f"设置杠杆 {target_leverage}x 失败: {e},尝试降低杠杆...")
# 如果是 leverage 相关错误,尝试降级
if 'leverage' in error_msg or 'invalid' in error_msg or 'max' in error_msg:
@ -2687,12 +2687,12 @@ class BinanceClient:
continue
try:
await self.client.futures_change_leverage(symbol=symbol, leverage=fallback)
logger.warning(
logger.warning(
f"{symbol} 杠杆降级成功: {target_leverage}x -> {fallback}x"
)
return fallback
except (TimeoutError, asyncio.TimeoutError, BinanceAPIException):
continue
continue
logger.error(f"设置杠杆最终失败: {symbol} (目标: {target_leverage}x)")
return 0

View File

@ -287,7 +287,7 @@ class PositionManager:
# 判断是否应该交易
if not await self.risk_manager.should_trade(symbol, change_percent):
return None
# 设置杠杆(确保为 int避免动态杠杆传入 float 导致 API/range 报错)
actual_leverage = await self.client.set_leverage(symbol, int(leverage))
@ -299,7 +299,7 @@ class PositionManager:
if actual_leverage != int(leverage):
logger.warning(f"{symbol} 杠杆被调整: {int(leverage)}x -> {actual_leverage}x")
leverage = actual_leverage
# 计算仓位大小(传入实际使用的杠杆)
# ⚠️ 优化:先估算止损价格,用于固定风险百分比计算
logger.info(f"开始为 {symbol} 计算仓位大小...")
@ -498,9 +498,9 @@ class PositionManager:
if DB_AVAILABLE and Trade:
try:
pending_trade_id = Trade.create(
symbol=symbol,
side=side,
quantity=quantity,
symbol=symbol,
side=side,
quantity=quantity,
entry_price=entry_price,
leverage=leverage,
entry_reason=entry_reason,
@ -603,7 +603,7 @@ class PositionManager:
entry_order_id = order.get("orderId") if isinstance(order, dict) else None
except Exception:
entry_order_id = None
break
break
else:
logger.info(f"{symbol} [智能入场] 限价超时,但偏离{drift_ratio*100:.2f}%>{max_drift_ratio*100:.2f}%,取消并放弃本次交易")
try:
@ -611,7 +611,7 @@ class PositionManager:
except Exception:
pass
self._pending_entry_orders.pop(symbol, None)
return None
return None
# 震荡/不允许市价兜底:尝试追价(减小 offset -> 更靠近当前价),但不突破追价上限
try:
@ -626,7 +626,7 @@ class PositionManager:
if side == "BUY":
cap = initial_limit * (1 + max_drift_ratio)
desired = min(desired, cap)
else:
else:
cap = initial_limit * (1 - max_drift_ratio)
desired = max(desired, cap)
@ -685,13 +685,13 @@ class PositionManager:
logger.info(f"{symbol} [开仓] 撤单后发现已成交,继续完善记录 (成交价={actual_entry_price:.4f} 数量={filled_quantity:.4f})")
else:
self._pending_entry_orders.pop(symbol, None)
return None
return None
if not actual_entry_price or actual_entry_price <= 0:
logger.error(f"{symbol} [开仓] ❌ 无法获取实际成交价格,不保存到数据库")
self._pending_entry_orders.pop(symbol, None)
return None
if filled_quantity <= 0:
logger.error(f"{symbol} [开仓] ❌ 成交数量为0不保存到数据库")
self._pending_entry_orders.pop(symbol, None)
@ -851,30 +851,30 @@ class PositionManager:
logger.info(f"{symbol} 已完善 pending 记录 (ID: {trade_id}, 订单号: {entry_order_id}, 成交价: {entry_price:.4f}, 成交数量: {quantity:.4f})")
if trade_id is None:
# 无 pending 或未匹配到:走新建(兜底)
logger.info(f"正在保存 {symbol} 交易记录到数据库...")
logger.info(f"正在保存 {symbol} 交易记录到数据库...")
fallback_client_order_id = (order.get("clientOrderId") if order else None) or client_order_id
# 如果 REST 已获取到 entry_order_id直接写入否则留空等待 WS 推送或后续同步补全
trade_id = Trade.create(
symbol=symbol,
side=side,
trade_id = Trade.create(
symbol=symbol,
side=side,
quantity=quantity,
entry_price=entry_price,
leverage=leverage,
entry_reason=entry_reason,
leverage=leverage,
entry_reason=entry_reason,
entry_order_id=entry_order_id, # REST 已获取则直接写入
client_order_id=fallback_client_order_id,
stop_loss_price=stop_loss_price,
take_profit_price=take_profit_price,
take_profit_1=take_profit_1,
take_profit_2=take_profit_2,
atr=atr,
notional_usdt=notional_usdt,
margin_usdt=margin_usdt,
stop_loss_price=stop_loss_price,
take_profit_price=take_profit_price,
take_profit_1=take_profit_1,
take_profit_2=take_profit_2,
atr=atr,
notional_usdt=notional_usdt,
margin_usdt=margin_usdt,
entry_context=entry_context,
account_id=self.account_id,
)
)
if entry_order_id:
logger.info(f"{symbol} 交易记录已保存到数据库 (ID: {trade_id}, 订单号: {entry_order_id}, 成交价: {entry_price:.4f}, 成交数量: {quantity:.4f})")
logger.info(f"{symbol} 交易记录已保存到数据库 (ID: {trade_id}, 订单号: {entry_order_id}, 成交价: {entry_price:.4f}, 成交数量: {quantity:.4f})")
else:
logger.warning(f"{symbol} 交易记录已保存但 entry_order_id 为空 (ID: {trade_id}),等待 WS 推送或后续同步补全")
# 如果有 client_order_id尝试通过 REST 查询订单号补全
@ -1065,48 +1065,48 @@ class PositionManager:
position_info = self.active_positions[symbol]
trade_id = position_info.get('tradeId')
if trade_id:
logger.info(f"{symbol} [平仓] 更新数据库状态为已平仓 (ID: {trade_id})...")
ticker = await self.client.get_ticker_24h(symbol)
exit_price = float(ticker['price']) if ticker else float(position_info['entryPrice'])
entry_price = float(position_info['entryPrice'])
quantity = float(position_info['quantity'])
if position_info['side'] == 'BUY':
pnl = (exit_price - entry_price) * quantity
pnl_percent = ((exit_price - entry_price) / entry_price) * 100
else:
pnl = (entry_price - exit_price) * quantity
pnl_percent = ((entry_price - exit_price) / entry_price) * 100
entry_time = position_info.get('entryTime')
duration_minutes = None
if entry_time:
try:
if isinstance(entry_time, str):
entry_dt = datetime.strptime(entry_time, '%Y-%m-%d %H:%M:%S')
else:
entry_dt = entry_time
logger.info(f"{symbol} [平仓] 更新数据库状态为已平仓 (ID: {trade_id})...")
ticker = await self.client.get_ticker_24h(symbol)
exit_price = float(ticker['price']) if ticker else float(position_info['entryPrice'])
entry_price = float(position_info['entryPrice'])
quantity = float(position_info['quantity'])
if position_info['side'] == 'BUY':
pnl = (exit_price - entry_price) * quantity
pnl_percent = ((exit_price - entry_price) / entry_price) * 100
else:
pnl = (entry_price - exit_price) * quantity
pnl_percent = ((entry_price - exit_price) / entry_price) * 100
entry_time = position_info.get('entryTime')
duration_minutes = None
if entry_time:
try:
if isinstance(entry_time, str):
entry_dt = datetime.strptime(entry_time, '%Y-%m-%d %H:%M:%S')
else:
entry_dt = entry_time
exit_dt = get_beijing_time()
duration = exit_dt - entry_dt
duration_minutes = int(duration.total_seconds() / 60)
except Exception as e:
logger.debug(f"计算持仓持续时间失败: {e}")
strategy_type = position_info.get('strategyType', 'trend_following')
duration = exit_dt - entry_dt
duration_minutes = int(duration.total_seconds() / 60)
except Exception as e:
logger.debug(f"计算持仓持续时间失败: {e}")
strategy_type = position_info.get('strategyType', 'trend_following')
db_update_retries = 3
for db_attempt in range(db_update_retries):
try:
Trade.update_exit(
trade_id=trade_id,
exit_price=exit_price,
exit_reason=reason,
pnl=pnl,
pnl_percent=pnl_percent,
Trade.update_exit(
trade_id=trade_id,
exit_price=exit_price,
exit_reason=reason,
pnl=pnl,
pnl_percent=pnl_percent,
exit_order_id=None,
strategy_type=strategy_type,
duration_minutes=duration_minutes
)
logger.info(f"{symbol} [平仓] ✓ 数据库状态已更新")
updated = True
strategy_type=strategy_type,
duration_minutes=duration_minutes
)
logger.info(f"{symbol} [平仓] ✓ 数据库状态已更新")
updated = True
break
except Exception as e:
except Exception as e:
err_msg = str(e).strip() or f"{type(e).__name__}"
if db_attempt < db_update_retries - 1:
wait_sec = 2
@ -1192,7 +1192,7 @@ class PositionManager:
del self.active_positions[symbol]
logger.info(f"{symbol} [平仓] ✓ 平仓完成: {side} {quantity:.4f} (原因: {reason})")
return True
else:
else:
# place_order 返回 None可能是 -2022ReduceOnly rejected等竞态场景
# 兜底再查一次实时持仓如果已经为0则当作“已平仓”处理避免刷屏与误判失败
try:
@ -1201,10 +1201,10 @@ class PositionManager:
live2 = None
if live2 is None or abs(live2) <= 0:
logger.warning(f"{symbol} [平仓] 下单返回None但实时持仓已为0按已平仓处理可能竞态/手动平仓)")
await self._stop_position_monitoring(symbol)
if symbol in self.active_positions:
del self.active_positions[symbol]
return True
await self._stop_position_monitoring(symbol)
if symbol in self.active_positions:
del self.active_positions[symbol]
return True
logger.error(f"{symbol} [平仓] ❌ 下单返回 None实时持仓仍存在: {live2}),可能的原因:")
logger.error(f" 1. ReduceOnly 被拒绝(-2022但持仓未同步")
@ -1244,13 +1244,13 @@ class PositionManager:
except Exception:
amt0 = None
if amt0 is not None and abs(amt0) <= 0:
try:
await self._stop_position_monitoring(symbol)
try:
await self._stop_position_monitoring(symbol)
except Exception:
pass
try:
if symbol in self.active_positions:
del self.active_positions[symbol]
if symbol in self.active_positions:
del self.active_positions[symbol]
except Exception:
pass
logger.warning(f"{symbol} [平仓] 异常后检查币安持仓已为0已清理本地记录")
@ -2032,7 +2032,7 @@ class PositionManager:
logger.warning(
f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}",
exc_info=False,
)
)
else:
# 盈利超过阈值后,止损移至保护利润位(基于保证金)
# 如果已经部分止盈,使用剩余仓位计算
@ -2062,7 +2062,7 @@ class PositionManager:
logger.warning(
f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}",
exc_info=False,
)
)
else:
new_stop_loss = entry_price + (remaining_pnl - protect_amount) / remaining_quantity
new_stop_loss = min(new_stop_loss, self._breakeven_stop_price(entry_price, 'SELL'))
@ -2081,7 +2081,7 @@ class PositionManager:
logger.warning(
f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}",
exc_info=False,
)
)
else:
# 未部分止盈,使用原始仓位计算;保护金额至少覆盖手续费
protect_amount = max(margin * trailing_protect, self._min_protect_amount_for_fees(margin, leverage))
@ -2102,7 +2102,7 @@ class PositionManager:
logger.warning(
f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}",
exc_info=False,
)
)
else:
# 做空:止损价 = 开仓价 + (当前盈亏 - 保护金额) / 数量
# 注意:对于做空,止损价应该高于开仓价,所以用加法
@ -2124,8 +2124,8 @@ class PositionManager:
logger.warning(
f"{symbol} [定时检查] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}",
exc_info=False,
)
)
# 检查止损(使用更新后的止损价,基于保证金收益比)
# ⚠️ 重要:止损检查应该在时间锁之前,止损必须立即执行
stop_loss_raw = position_info.get('stopLoss')
@ -2519,7 +2519,7 @@ class PositionManager:
return await reconcile_pending_with_binance(self.client, self.account_id)
except ImportError:
return 0
async def sync_positions_with_binance(self):
"""
同步币安实际持仓状态与数据库状态
@ -2797,7 +2797,7 @@ class PositionManager:
# 限制为最近1小时避免匹配到几天的订单导致"时间倒流"
start_time = end_time - (60 * 60 * 1000)
logger.warning(f"{symbol} [状态同步] ⚠️ 交易记录缺少入场时间将搜索范围限制为最近1小时")
logger.debug(
f"{symbol} [状态同步] 获取历史订单: "
f"symbol={symbol}, startTime={start_time}, endTime={end_time}"
@ -2895,14 +2895,14 @@ class PositionManager:
f"{symbol} [状态同步] ❌ 获取当前价格时KeyError: {key_error}, "
f"ticker数据: {ticker if 'ticker' in locals() else 'N/A'}"
)
continue
continue
except Exception as ticker_error:
logger.warning(
f"{symbol} [状态同步] 获取当前价格失败: "
f"错误类型={type(ticker_error).__name__}, 错误消息={str(ticker_error)}"
f"将保留open状态等待下次同步"
)
continue
continue
# 计算盈亏确保所有值都是float类型避免Decimal类型问题
try:
@ -2928,12 +2928,12 @@ class PositionManager:
pnl_percent = ((entry_price - exit_price) / entry_price) * 100
else:
# 降级方案:使用价格差计算
if trade.get('side') == 'BUY':
pnl = (exit_price - entry_price) * quantity
pnl_percent = ((exit_price - entry_price) / entry_price) * 100
else: # SELL
pnl = (entry_price - exit_price) * quantity
pnl_percent = ((entry_price - exit_price) / entry_price) * 100
if trade.get('side') == 'BUY':
pnl = (exit_price - entry_price) * quantity
pnl_percent = ((exit_price - entry_price) / entry_price) * 100
else: # SELL
pnl = (entry_price - exit_price) * quantity
pnl_percent = ((entry_price - exit_price) / entry_price) * 100
logger.debug(
f"{symbol} [状态同步] 盈亏计算: "
@ -3002,7 +3002,7 @@ class PositionManager:
# 计算持仓时间和亏损比例(用于特征判断)
entry_time = trade.get("entry_time")
duration_minutes = None
duration_minutes = None
if entry_time and exit_time_ts:
try:
duration_minutes = (exit_time_ts - int(entry_time)) / 60.0
@ -3038,7 +3038,7 @@ class PositionManager:
# 检查是否有移动止损标记
if is_reduce_only:
exit_reason = "trailing_stop" # 可能是移动止损
else:
else:
exit_reason = "manual" # 可能是手动平仓
else:
# 亏损单:检查止损价格匹配
@ -3140,9 +3140,9 @@ class PositionManager:
xt = int(exit_time_ts) if exit_time_ts is not None else int(get_beijing_time().timestamp())
if et is not None and xt >= et:
duration_minutes = int((xt - et) / 60)
except Exception as e:
logger.debug(f"计算持仓持续时间失败: {e}")
except Exception as e:
logger.debug(f"计算持仓持续时间失败: {e}")
strategy_type = 'trend_following' # 默认策略类型
# ⚠️ 关键修复:使用基于保证金的盈亏百分比更新数据库(与实时监控逻辑一致)
@ -3437,29 +3437,29 @@ class PositionManager:
)
elif sync_create_manual:
# 为手动开仓的持仓创建数据库记录并启动监控(仅当显式开启且未走上面的「补建系统单」时)
for symbol in missing_in_db:
try:
# 获取币安持仓详情
binance_position = next(
(p for p in binance_positions if p['symbol'] == symbol),
None
)
if not binance_position:
continue
position_amt = binance_position['positionAmt']
entry_price = binance_position['entryPrice']
quantity = abs(position_amt)
side = 'BUY' if position_amt > 0 else 'SELL'
for symbol in missing_in_db:
try:
# 获取币安持仓详情
binance_position = next(
(p for p in binance_positions if p['symbol'] == symbol),
None
)
if not binance_position:
continue
position_amt = binance_position['positionAmt']
entry_price = binance_position['entryPrice']
quantity = abs(position_amt)
side = 'BUY' if position_amt > 0 else 'SELL'
notional = (float(entry_price) * float(quantity)) if entry_price and quantity else 0
if notional < 1.0:
logger.debug(f"{symbol} [状态同步] 跳过灰尘持仓 (名义 {notional:.4f} USDT < 1),不创建记录")
continue
logger.info(
f"{symbol} [状态同步] 检测到手动开仓,创建数据库记录... "
f"({side} {quantity:.4f} @ {entry_price:.4f})"
)
logger.info(
f"{symbol} [状态同步] 检测到手动开仓,创建数据库记录... "
f"({side} {quantity:.4f} @ {entry_price:.4f})"
)
# 尽量从币安成交取 entry_order_id 与真实开仓时间limit=100、空时重试一次
entry_order_id = None
entry_time_ts = None
@ -3486,82 +3486,82 @@ class PositionManager:
except Exception:
pass
# 创建数据库记录(显式传入 account_id、真实开仓时间
trade_id = Trade.create(
symbol=symbol,
side=side,
quantity=quantity,
entry_price=entry_price,
leverage=binance_position.get('leverage', 10),
entry_reason='manual_entry', # 标记为手动开仓
trade_id = Trade.create(
symbol=symbol,
side=side,
quantity=quantity,
entry_price=entry_price,
leverage=binance_position.get('leverage', 10),
entry_reason='manual_entry', # 标记为手动开仓
entry_order_id=entry_order_id,
notional_usdt=notional,
margin_usdt=(notional / float(binance_position.get('leverage', 10) or 10)) if float(binance_position.get('leverage', 10) or 0) > 0 else None,
account_id=self.account_id,
entry_time=entry_time_ts,
)
logger.info(f"{symbol} [状态同步] ✓ 数据库记录已创建 (ID: {trade_id})")
# 创建本地持仓记录(用于监控)
ticker = await self.client.get_ticker_24h(symbol)
current_price = ticker['price'] if ticker else entry_price
# 计算止损止盈(基于保证金)
leverage = binance_position.get('leverage', 10)
stop_loss_pct_margin = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.08)
)
logger.info(f"{symbol} [状态同步] ✓ 数据库记录已创建 (ID: {trade_id})")
# 创建本地持仓记录(用于监控)
ticker = await self.client.get_ticker_24h(symbol)
current_price = ticker['price'] if ticker else entry_price
# 计算止损止盈(基于保证金)
leverage = binance_position.get('leverage', 10)
stop_loss_pct_margin = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.08)
# ⚠️ 关键修复:配置值格式转换(兼容百分比形式和比例形式)
if stop_loss_pct_margin is not None and stop_loss_pct_margin > 1:
stop_loss_pct_margin = stop_loss_pct_margin / 100.0
take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.15)
take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.15)
# ⚠️ 关键修复:配置值格式转换(兼容百分比形式和比例形式)
if take_profit_pct_margin is not None and take_profit_pct_margin > 1:
take_profit_pct_margin = take_profit_pct_margin / 100.0
# 如果配置中没有设置止盈则使用止损的2倍作为默认
if take_profit_pct_margin is None or take_profit_pct_margin == 0:
take_profit_pct_margin = stop_loss_pct_margin * 2.0
stop_loss_price = self.risk_manager.get_stop_loss_price(
entry_price, side, quantity, leverage,
stop_loss_pct=stop_loss_pct_margin
)
take_profit_price = self.risk_manager.get_take_profit_price(
entry_price, side, quantity, leverage,
take_profit_pct=take_profit_pct_margin
)
position_info = {
'symbol': symbol,
'side': side,
'quantity': quantity,
'entryPrice': entry_price,
'changePercent': 0, # 手动开仓,无法计算涨跌幅
# 如果配置中没有设置止盈则使用止损的2倍作为默认
if take_profit_pct_margin is None or take_profit_pct_margin == 0:
take_profit_pct_margin = stop_loss_pct_margin * 2.0
stop_loss_price = self.risk_manager.get_stop_loss_price(
entry_price, side, quantity, leverage,
stop_loss_pct=stop_loss_pct_margin
)
take_profit_price = self.risk_manager.get_take_profit_price(
entry_price, side, quantity, leverage,
take_profit_pct=take_profit_pct_margin
)
position_info = {
'symbol': symbol,
'side': side,
'quantity': quantity,
'entryPrice': entry_price,
'changePercent': 0, # 手动开仓,无法计算涨跌幅
'orderId': entry_order_id,
'tradeId': trade_id,
'stopLoss': stop_loss_price,
'takeProfit': take_profit_price,
'initialStopLoss': stop_loss_price,
'leverage': leverage,
'entryReason': 'manual_entry',
'tradeId': trade_id,
'stopLoss': stop_loss_price,
'takeProfit': take_profit_price,
'initialStopLoss': stop_loss_price,
'leverage': leverage,
'entryReason': 'manual_entry',
'entryTime': entry_time_ts if entry_time_ts is not None else get_beijing_time(), # 真实开仓时间(来自币安成交/订单)
'atr': None,
'maxProfit': 0.0,
'atr': None,
'maxProfit': 0.0,
'trailingStopActivated': False,
'breakevenStopSet': False
}
self.active_positions[symbol] = position_info
# 启动WebSocket监控
if self._monitoring_enabled:
await self._start_position_monitoring(symbol)
logger.info(f"[账号{self.account_id}] {symbol} [状态同步] ✓ 已启动实时监控")
logger.info(f"{symbol} [状态同步] ✓ 手动开仓同步完成")
}
except Exception as e:
logger.error(f"{symbol} [状态同步] ❌ 处理手动开仓失败: {e}")
import traceback
logger.error(f" 错误详情:\n{traceback.format_exc()}")
self.active_positions[symbol] = position_info
# 启动WebSocket监控
if self._monitoring_enabled:
await self._start_position_monitoring(symbol)
logger.info(f"[账号{self.account_id}] {symbol} [状态同步] ✓ 已启动实时监控")
logger.info(f"{symbol} [状态同步] ✓ 手动开仓同步完成")
except Exception as e:
logger.error(f"{symbol} [状态同步] ❌ 处理手动开仓失败: {e}")
import traceback
logger.error(f" 错误详情:\n{traceback.format_exc()}")
# 6. 同步挂单信息 (STOP_MARKET / TAKE_PROFIT_MARKET)
if self.active_positions:
@ -3600,7 +3600,7 @@ class PositionManager:
logger.warning(f"{symbol} 获取挂单失败: {orders}")
except Exception as e:
logger.error(f"同步挂单信息失败: {e}")
logger.info("持仓状态同步完成")
except Exception as e:
@ -3685,24 +3685,24 @@ class PositionManager:
stop_loss_price = None
take_profit_price = None
if stop_loss_price is None or take_profit_price is None:
stop_loss_pct_margin = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.08)
stop_loss_pct_margin = config.TRADING_CONFIG.get('STOP_LOSS_PERCENT', 0.08)
if stop_loss_pct_margin is not None and stop_loss_pct_margin > 1:
stop_loss_pct_margin = stop_loss_pct_margin / 100.0
take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.15)
take_profit_pct_margin = config.TRADING_CONFIG.get('TAKE_PROFIT_PERCENT', 0.15)
if take_profit_pct_margin is not None and take_profit_pct_margin > 1:
take_profit_pct_margin = take_profit_pct_margin / 100.0
if take_profit_pct_margin is None or take_profit_pct_margin == 0:
if take_profit_pct_margin is None or take_profit_pct_margin == 0:
take_profit_pct_margin = (stop_loss_pct_margin or 0.05) * 2.0
if stop_loss_price is None:
stop_loss_price = self.risk_manager.get_stop_loss_price(
entry_price, side, quantity, leverage,
stop_loss_price = self.risk_manager.get_stop_loss_price(
entry_price, side, quantity, leverage,
stop_loss_pct=stop_loss_pct_margin or 0.05
)
)
if take_profit_price is None:
take_profit_price = self.risk_manager.get_take_profit_price(
entry_price, side, quantity, leverage,
take_profit_pct=take_profit_pct_margin
)
take_profit_price = self.risk_manager.get_take_profit_price(
entry_price, side, quantity, leverage,
take_profit_pct=take_profit_pct_margin
)
entry_reason = 'manual_entry_temp' if sync_create_manual else 'sync_recovered'
position_info = {
@ -3754,7 +3754,7 @@ class PositionManager:
except Exception:
mp = None
await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=mp or current_price)
except Exception as e:
except Exception as e:
logger.warning(f"{symbol} 补挂币安止盈止损失败(不影响监控): {e}")
# 重启后立即按当前价做一次保本/移动止损检查并同步,不依赖首条 WS 推送
try:
@ -3829,7 +3829,7 @@ class PositionManager:
task = self._monitor_tasks.pop(symbol, None)
if task is None:
return
if not task.done():
task.cancel()
try:
@ -4084,7 +4084,7 @@ class PositionManager:
logger.warning(
f"[账号{self.account_id}] {symbol} [实时监控] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}",
exc_info=False,
)
)
else:
# ⚠️ 优化:如果分步止盈第一目标已触发,移动止损不再更新剩余仓位的止损价
# 原因分步止盈第一目标触发后剩余50%仓位止损已移至成本价(保本),等待第二目标
@ -4095,14 +4095,14 @@ class PositionManager:
else:
# 盈利超过阈值后,止损移至保护利润位(基于保证金);保护金额至少覆盖手续费
protect_amount = max(margin * trailing_protect, self._min_protect_amount_for_fees(margin, leverage))
if position_info['side'] == 'BUY':
new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity
if position_info['side'] == 'BUY':
new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity
new_stop_loss = max(new_stop_loss, self._breakeven_stop_price(entry_price, 'BUY'))
if new_stop_loss > position_info['stopLoss']:
position_info['stopLoss'] = new_stop_loss
logger.info(
if new_stop_loss > position_info['stopLoss']:
position_info['stopLoss'] = new_stop_loss
logger.info(
f"[账号{self.account_id}] {symbol} [实时监控] 移动止损更新: {new_stop_loss:.4f} "
f"(保护{trailing_protect*100:.1f}% of margin = {protect_amount:.4f} USDT)"
f"(保护{trailing_protect*100:.1f}% of margin = {protect_amount:.4f} USDT)"
)
try:
await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float)
@ -4111,15 +4111,15 @@ class PositionManager:
logger.warning(
f"[账号{self.account_id}] {symbol} [实时监控] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}",
exc_info=False,
)
else: # SELL
)
else: # SELL
new_stop_loss = entry_price + (pnl_amount - protect_amount) / quantity
new_stop_loss = min(new_stop_loss, self._breakeven_stop_price(entry_price, 'SELL'))
if new_stop_loss > position_info['stopLoss'] and pnl_amount > 0:
position_info['stopLoss'] = new_stop_loss
logger.info(
position_info['stopLoss'] = new_stop_loss
logger.info(
f"[账号{self.account_id}] {symbol} [实时监控] 移动止损更新: {new_stop_loss:.4f} "
f"(保护{trailing_protect*100:.1f}% of margin = {protect_amount:.4f} USDT)"
f"(保护{trailing_protect*100:.1f}% of margin = {protect_amount:.4f} USDT)"
)
try:
await self._ensure_exchange_sltp_orders(symbol, position_info, current_price=current_price_float)
@ -4128,7 +4128,7 @@ class PositionManager:
logger.warning(
f"[账号{self.account_id}] {symbol} [实时监控] 同步移动止损至交易所失败: {type(sync_e).__name__}: {sync_e}",
exc_info=False,
)
)
# 检查止损(基于保证金收益比)
# ⚠️ 重要:止损检查应该在时间锁之前,止损必须立即执行

View File

@ -79,7 +79,7 @@ class RiskManager:
# 获取账户余额(优先 WS 缓存Redis
balance = await _get_balance_from_cache(self.client) if _get_stream_instance() else None
if balance is None:
balance = await self.client.get_account_balance()
balance = await self.client.get_account_balance()
available_balance = balance.get('available', 0)
if available_balance <= 0:
@ -170,7 +170,7 @@ class RiskManager:
# 获取当前持仓(优先 WS 缓存Redis
positions = await _get_positions_from_cache(self.client) if _get_stream_instance() else None
if positions is None:
positions = await self.client.get_open_positions()
positions = await self.client.get_open_positions()
# 计算当前总保证金占用
current_position_values = []
@ -202,7 +202,7 @@ class RiskManager:
# 获取账户余额(优先 WS 缓存Redis
balance = await _get_balance_from_cache(self.client) if _get_stream_instance() else None
if balance is None:
balance = await self.client.get_account_balance()
balance = await self.client.get_account_balance()
total_balance = balance.get('total', 0)
available_balance = balance.get('available', 0)
@ -418,7 +418,7 @@ class RiskManager:
logger.info(f" ⚖️ 最终动态杠杆: {final_leverage}x (信号:{int(signal_leverage)}x, ATR限制:{int(atr_limit_leverage)}x, 风险安全:{int(risk_safe_leverage)}x)")
return final_leverage
async def calculate_position_size(
self,
symbol: str,
@ -453,7 +453,7 @@ class RiskManager:
# 获取账户余额(优先 WS 缓存Redis
balance = await _get_balance_from_cache(self.client) if _get_stream_instance() else None
if balance is None:
balance = await self.client.get_account_balance()
balance = await self.client.get_account_balance()
available_balance = balance.get('available', 0)
total_balance = balance.get('total', 0)
@ -664,19 +664,19 @@ class RiskManager:
effective_max = config.get_effective_config('MAX_POSITION_PERCENT', 0.20)
base_position_percent = effective_max * signal_multiplier
max_position_percent = effective_max * signal_multiplier
min_position_percent = config.TRADING_CONFIG['MIN_POSITION_PERCENT']
min_position_percent = config.TRADING_CONFIG['MIN_POSITION_PERCENT']
# 涨跌幅超过5%时,可以适当增加保证金占比,但必须遵守 MAX_POSITION_PERCENT 上限
if abs(change_percent) > 5:
position_percent = min(
base_position_percent * 1.5,
if abs(change_percent) > 5:
position_percent = min(
base_position_percent * 1.5,
max_position_percent
)
logger.info(f" 涨跌幅 {change_percent:.2f}% > 5%,使用增强仓位比例: {position_percent*100:.1f}%")
else:
position_percent = base_position_percent
logger.info(f" 涨跌幅 {change_percent:.2f}%,使用标准仓位比例: {position_percent*100:.1f}%")
)
logger.info(f" 涨跌幅 {change_percent:.2f}% > 5%,使用增强仓位比例: {position_percent*100:.1f}%")
else:
position_percent = base_position_percent
logger.info(f" 涨跌幅 {change_percent:.2f}%,使用标准仓位比例: {position_percent*100:.1f}%")
# 计算保证金与名义价值
margin_value = available_balance * position_percent
notional_value = margin_value * actual_leverage
@ -790,7 +790,7 @@ class RiskManager:
quantity = final_notional_value / current_price if current_price and current_price > 0 else quantity
logger.info(f" 仓位放大后超过单笔上限,已截断至 MAX_POSITION_PERCENT 对应保证金")
logger.info(f" 仓位放大系数: {position_scale:.2f} -> 最终数量: {quantity:.4f}")
# 添加最小名义价值检查0.2 USDT避免下无意义的小单子
MIN_NOTIONAL_VALUE = 0.2 # 最小名义价值0.2 USDT
if final_notional_value < MIN_NOTIONAL_VALUE:
@ -840,7 +840,7 @@ class RiskManager:
# 检查是否已有持仓 / 总持仓数量限制(优先 WS 缓存)
positions = await _get_positions_from_cache(self.client) if _get_stream_instance() else None
if positions is None:
positions = await self.client.get_open_positions()
positions = await self.client.get_open_positions()
try:
max_open = int(config.TRADING_CONFIG.get("MAX_OPEN_POSITIONS", 0) or 0)
except Exception:
@ -1090,10 +1090,10 @@ class RiskManager:
logger.debug(f"优先使用ATR止损: {stop_loss_price:.4f}, 忽略保证金止损: {stop_loss_price_margin:.4f}")
else:
# ATR不可用使用保证金止损和价格百分比止损中"更紧"的一个(保护资金)
if side == 'BUY':
if side == 'BUY':
# 做多:取最大值(更高的止损价,更接近入场价)
stop_loss_price = max(p[1] for p in candidate_prices if p[0] != 'ATR')
else:
else:
# 做空:取最小值(更低的止损价,更接近入场价)
stop_loss_price = min(p[1] for p in candidate_prices if p[0] != 'ATR')
@ -1159,15 +1159,15 @@ class RiskManager:
final_stop_loss = atr_candidate[1]
selected_method = 'ATR'
else:
if side == 'BUY':
if side == 'BUY':
# 做多:选择更高的止损价(更紧)
final_stop_loss = max(p[1] for p in candidate_prices)
selected_method = [p[0] for p in candidate_prices if p[1] == final_stop_loss][0]
else:
final_stop_loss = max(p[1] for p in candidate_prices)
selected_method = [p[0] for p in candidate_prices if p[1] == final_stop_loss][0]
else:
# ⚠️ 关键修复:做空必须选择更低的止损价(更接近入场价,更紧)
# 注意对于SELL单止损价高于入场价所以"更低的止损价"意味着更接近入场价
final_stop_loss = min(p[1] for p in candidate_prices)
selected_method = [p[0] for p in candidate_prices if p[1] == final_stop_loss][0]
final_stop_loss = min(p[1] for p in candidate_prices)
selected_method = [p[0] for p in candidate_prices if p[1] == final_stop_loss][0]
# ⚠️ 关键修复:验证最终止损价对应的保证金百分比不超过配置值
if side == 'BUY':
@ -1272,7 +1272,7 @@ class RiskManager:
f"强制调整为安全止损价: {safe_sl:.4f} (+0.5%)"
)
final_stop_loss = safe_sl
logger.info(
f"最终止损 ({side}): {final_stop_loss:.4f} (使用{selected_method}), "
+ (f"ATR={stop_loss_price_atr:.4f}, " if stop_loss_price_atr else "")
@ -1392,7 +1392,7 @@ class RiskManager:
selected_method = 'ATR盈亏比'
if use_margin_cap and take_profit_price_margin is not None:
# 取「更近」的止盈,避免盈亏比止盈过远难以触及
if side == 'BUY':
if side == 'BUY':
if take_profit_price_margin < take_profit_price:
take_profit_price = take_profit_price_margin
selected_method = '保证金(止盈上限)'
@ -1406,10 +1406,10 @@ class RiskManager:
# 如果没有基于盈亏比的止盈,选择最远的止盈(给利润更多空间,提高盈亏比)
if side == 'BUY':
# 做多:选择更高的止盈价(更远,给利润更多空间)
take_profit_price = max(p[1] for p in candidate_prices)
else:
take_profit_price = max(p[1] for p in candidate_prices)
else:
# 做空:选择更低的止盈价(更远,给利润更多空间)
take_profit_price = min(p[1] for p in candidate_prices)
take_profit_price = min(p[1] for p in candidate_prices)
selected_method = [p[0] for p in candidate_prices if p[1] == take_profit_price][0]
logger.info(