stock-ai-agent/backend/app/services/position_sizing.py
2026-04-28 15:48:42 +08:00

195 lines
6.9 KiB
Python

"""
统一仓位 sizing 规则。
目标:
- 单笔仓位按账户权益百分比控制保证金
- 总杠杆限制按“名义仓位空间”换算成“可加保证金”
- 给模拟盘和执行决策层复用,避免多套逻辑漂移
"""
from typing import Dict, Optional, Tuple
from app.utils.logger import logger
DEFAULT_POSITION_SIZE_MARGIN_PCTS: Dict[str, float] = {
"micro": 0.01,
"light": 0.08,
"medium": 0.12,
"heavy": 0.18,
}
DEFAULT_SIGNAL_POSITION_SIZE_BY_TIMEFRAME: Dict[str, str] = {
"short_term": "light",
"medium_term": "light",
"long_term": "medium",
}
DEFAULT_TIMEFRAME_MARGIN_MULTIPLIERS: Dict[str, float] = {
"short_term": 0.90,
"medium_term": 1.00,
"long_term": 1.10,
}
DEFAULT_GRADE_MARGIN_MULTIPLIERS: Dict[str, float] = {
"A": 1.10,
"B": 1.00,
"C": 0.80,
"D": 0.00,
}
def normalize_signal_type(signal_type: Optional[str]) -> str:
normalized = (signal_type or "medium_term").strip().lower()
if normalized not in {"short_term", "medium_term", "long_term"}:
return "medium_term"
return normalized
def infer_signal_grade(confidence: Optional[float], explicit_grade: Optional[str] = None) -> str:
if explicit_grade:
grade = str(explicit_grade).strip().upper()
if grade in {"A", "B", "C", "D"}:
return grade
confidence_value = float(confidence or 0)
if confidence_value >= 80:
return "A"
if confidence_value >= 60:
return "B"
if confidence_value >= 40:
return "C"
return "D"
def normalize_position_size(
position_size: Optional[str],
signal_type: Optional[str],
defaults: Optional[Dict[str, str]] = None,
) -> str:
normalized_type = normalize_signal_type(signal_type)
normalized_size = (position_size or "").strip().lower()
if normalized_size in DEFAULT_POSITION_SIZE_MARGIN_PCTS:
return normalized_size
fallback_defaults = defaults or DEFAULT_SIGNAL_POSITION_SIZE_BY_TIMEFRAME
return fallback_defaults.get(normalized_type, "light")
def resolve_target_margin_pct(
position_size: Optional[str],
signal_type: Optional[str],
confidence: Optional[float] = None,
grade: Optional[str] = None,
size_margin_pcts: Optional[Dict[str, float]] = None,
timeframe_multipliers: Optional[Dict[str, float]] = None,
grade_multipliers: Optional[Dict[str, float]] = None,
default_positions: Optional[Dict[str, str]] = None,
) -> Tuple[float, str, str, str]:
normalized_type = normalize_signal_type(signal_type)
normalized_size = normalize_position_size(position_size, normalized_type, default_positions)
normalized_grade = infer_signal_grade(confidence, grade)
base_margin_pcts = size_margin_pcts or DEFAULT_POSITION_SIZE_MARGIN_PCTS
type_multipliers = timeframe_multipliers or DEFAULT_TIMEFRAME_MARGIN_MULTIPLIERS
effective_grade_multipliers = grade_multipliers or DEFAULT_GRADE_MARGIN_MULTIPLIERS
base_pct = base_margin_pcts.get(normalized_size, base_margin_pcts["light"])
timeframe_multiplier = type_multipliers.get(normalized_type, 1.0)
grade_multiplier = effective_grade_multipliers.get(normalized_grade, 1.0)
target_pct = base_pct * timeframe_multiplier * grade_multiplier
reason = (
f"{normalized_type} {normalized_grade}{normalized_size}仓位"
f" -> {target_pct * 100:.1f}%权益保证金"
)
return target_pct, reason, normalized_size, normalized_grade
def calculate_margin_and_position_value(
*,
balance: float,
available_margin: float,
current_total_leverage: float,
max_total_leverage: float,
order_leverage: float,
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]:
if balance <= 0:
return 0.0, 0.0, "账户余额无效"
if available_margin <= 0:
return 0.0, 0.0, "可用保证金不足"
if order_leverage <= 0:
return 0.0, 0.0, "订单杠杆无效"
if min_effective_leverage <= 0:
min_effective_leverage = 1.0
if order_leverage < min_effective_leverage:
return 0.0, 0.0, (
f"订单杠杆 {order_leverage:.1f}x 低于最小有效杠杆 "
f"{min_effective_leverage:.1f}x"
)
if target_margin_pct <= 0:
return 0.0, 0.0, "目标保证金比例无效"
reserve_ratio = min(max(reserve_ratio, 0.0), 0.5)
buffer_available_margin = max(0.0, available_margin * (1 - reserve_ratio))
if buffer_available_margin <= 0:
return 0.0, 0.0, "可用保证金不足"
max_margin_by_platform = balance * max_margin_pct if max_margin_pct > 0 else buffer_available_margin
leverage_headroom = max(0.0, max_total_leverage - current_total_leverage)
if leverage_headroom <= 0:
return 0.0, 0.0, f"已达最大总杠杆 {current_total_leverage:.2f}x/{max_total_leverage:.2f}x"
# 总杠杆限制是“还能开的名义仓位”,这里换算成“还能加的保证金”。
max_margin_by_total_leverage = (balance * leverage_headroom) / order_leverage
hard_cap = min(buffer_available_margin, max_margin_by_platform, max_margin_by_total_leverage)
if hard_cap <= 0:
return 0.0, 0.0, "可开保证金额度不足"
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"
margin = round(margin, 2)
position_value = round(position_value, 2)
detail = (
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