fix(config_manager, account, trades, position_manager, risk_manager): 清理多余空行并优化代码风格
在多个模块中,移除多余的空行以提升代码可读性,并确保遵循一致的代码风格。此外,优化了部分逻辑的缩进和结构,增强了代码的整洁性和可维护性。这一改动旨在提升代码质量,确保团队协作时的代码一致性。
This commit is contained in:
parent
41b2a21c3d
commit
f3ce4d5d11
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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:
|
||||
|
|
|
|||
|
|
@ -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 表
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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:可能是 -2022(ReduceOnly 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,
|
||||
)
|
||||
)
|
||||
|
||||
# 检查止损(基于保证金收益比)
|
||||
# ⚠️ 重要:止损检查应该在时间锁之前,止损必须立即执行
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user