diff --git a/backend/app/crypto_agent/crypto_agent.py b/backend/app/crypto_agent/crypto_agent.py index c287d2a..bff5841 100644 --- a/backend/app/crypto_agent/crypto_agent.py +++ b/backend/app/crypto_agent/crypto_agent.py @@ -66,10 +66,12 @@ class CryptoAgent: 'UNI': 5, # 1 UNI/张 ≈ $50 }, 'max_margin_pct': 0.25, # 单笔最大25%(支持超激进配置) + 'min_position_value_balance_ratio': 1.0, # 合约单最低名义仓位 = 1x 账户权益 }, 'PaperTrading': { 'min_margin': {}, # 无最小限制 'max_margin_pct': 0.25, # 单笔最大25%(与实盘一致) + 'min_position_value_balance_ratio': 1.0, } } @@ -3639,6 +3641,7 @@ class CryptoAgent: symbol = signal.get('symbol', '').replace('USDT', '').upper() min_margin = min_margin_rules.get(symbol, 0) + min_position_value_balance_ratio = float(rules.get('min_position_value_balance_ratio', 0) or 0) current_leverage = account.get('current_total_leverage', 0) max_leverage = account.get('max_total_leverage', 10) order_leverage = account.get('order_leverage', 10) @@ -3679,6 +3682,7 @@ class CryptoAgent: setup_margin_pct_cap = setup_profile.get('max_margin_pct_cap') effective_max_margin_pct = min(max_margin_pct, setup_margin_pct_cap) if isinstance(setup_margin_pct_cap, (int, float)) else max_margin_pct + min_position_value = balance * min_position_value_balance_ratio if min_position_value_balance_ratio > 0 else 0.0 margin, _, budget_reason = calculate_margin_and_position_value( balance=balance, @@ -3689,6 +3693,7 @@ class CryptoAgent: target_margin_pct=target_margin_pct, max_margin_pct=effective_max_margin_pct, min_margin=min_margin, + min_position_value=min_position_value, min_effective_leverage=min_effective_leverage, ) diff --git a/backend/app/services/paper_trading_service.py b/backend/app/services/paper_trading_service.py index d310829..ccb2a80 100644 --- a/backend/app/services/paper_trading_service.py +++ b/backend/app/services/paper_trading_service.py @@ -440,6 +440,7 @@ class PaperTradingService: order_leverage=self.leverage, target_margin_pct=target_margin_pct, max_margin_pct=0.25, + min_position_value=balance, ) if margin <= 0: diff --git a/backend/app/services/position_sizing.py b/backend/app/services/position_sizing.py index c87ad74..eec9970 100644 --- a/backend/app/services/position_sizing.py +++ b/backend/app/services/position_sizing.py @@ -115,6 +115,7 @@ def calculate_margin_and_position_value( target_margin_pct: float, max_margin_pct: float, min_margin: float = 0.0, + min_position_value: float = 0.0, min_effective_leverage: float = 1.0, reserve_ratio: float = 0.05, ) -> Tuple[float, float, str]: @@ -152,16 +153,31 @@ def calculate_margin_and_position_value( target_margin = balance * target_margin_pct margin = min(target_margin, hard_cap) + adjustments = [] if min_margin > 0 and margin < min_margin: if min_margin <= hard_cap: margin = min_margin + adjustments.append(f"按最小保证金抬升至 ${min_margin:.2f}") else: return 0.0, 0.0, ( f"最小保证金 ${min_margin:.2f} 超过当前可用额度 " f"${hard_cap:.2f}" ) + if min_position_value > 0: + required_margin_for_min_position = min_position_value / order_leverage + if margin < required_margin_for_min_position: + if required_margin_for_min_position <= hard_cap: + margin = required_margin_for_min_position + adjustments.append( + f"按最低名义仓位 ${min_position_value:.2f} 抬升保证金至 ${margin:.2f}" + ) + else: + adjustments.append( + f"最低名义仓位 ${min_position_value:.2f} 受当前上限约束,实际最多 ${hard_cap * order_leverage:.2f}" + ) + position_value = margin * order_leverage if position_value < 50: return 0.0, 0.0, "可开仓位不足 $50" @@ -172,5 +188,7 @@ def calculate_margin_and_position_value( f"目标保证金 ${target_margin:.2f}, 实际保证金 ${margin:.2f}, " f"名义仓位 ${position_value:.2f}, 单笔杠杆 {order_leverage:.1f}x" ) + if adjustments: + detail = f"{detail}, 调整: {'; '.join(adjustments)}" logger.info(f"仓位预算计算: {detail}") return margin, position_value, detail diff --git a/backend/tests/test_position_sizing_regression.py b/backend/tests/test_position_sizing_regression.py index 14bd9b4..4e76546 100644 --- a/backend/tests/test_position_sizing_regression.py +++ b/backend/tests/test_position_sizing_regression.py @@ -165,3 +165,21 @@ def test_setup_profile_can_cap_margin_budget_more_conservatively(): assert margin == pytest.approx(1320.0) assert position_value == pytest.approx(13200.0) + + +def test_min_position_value_floor_can_raise_too_small_contract_position(): + module = load_position_sizing_module() + + margin, position_value, _ = module.calculate_margin_and_position_value( + balance=1400, + available_margin=1400, + current_total_leverage=0, + max_total_leverage=10, + order_leverage=10, + target_margin_pct=0.028, # 原本只会开到约 392U 名义仓位 + max_margin_pct=0.25, + min_position_value=1400, + ) + + assert margin == pytest.approx(140.0) + assert position_value == pytest.approx(1400.0)