154 lines
4.7 KiB
Python
154 lines
4.7 KiB
Python
from loguru import logger
|
|
|
|
class PositionManager:
|
|
"""
|
|
仓位管理 - Kelly公式动态仓位计算
|
|
"""
|
|
|
|
def __init__(self, config=None):
|
|
self.config = config or {}
|
|
self.max_position_ratio = self.config.get("max_position_ratio", 0.25) # 最大单笔25%
|
|
self.max_total_exposure = self.config.get("max_total_exposure", 0.80) # 最大总仓位80%
|
|
self.min_trade_size = self.config.get("min_trade_size", 10) # 最小交易额$10
|
|
logger.info("Initializing Position Manager...")
|
|
|
|
def kelly_criterion(self, win_prob: float, odds: float) -> float:
|
|
"""
|
|
Kelly公式计算最优投资比例
|
|
|
|
f = (bp - q) / b
|
|
f: 应投资的资金比例
|
|
b: 赔率 (盈利/亏损)
|
|
p: 胜率
|
|
q: 败率 (1-p)
|
|
|
|
Args:
|
|
win_prob: 预测胜率 (0-1)
|
|
odds: 赔率
|
|
|
|
Returns:
|
|
float: 建议投资比例 (0-1)
|
|
"""
|
|
if win_prob <= 0 or win_prob >= 1 or odds <= 0:
|
|
return 0.0
|
|
|
|
q = 1 - win_prob
|
|
f = (win_prob * odds - q) / odds
|
|
|
|
# 限制最大仓位
|
|
f = max(0, min(f, self.max_position_ratio))
|
|
|
|
logger.debug(f"Kelly ratio: {f:.4f} (win_prob={win_prob:.2f}, odds={odds:.2f})")
|
|
return f
|
|
|
|
def calculate_position_size(self,
|
|
total_capital: float,
|
|
win_prob: float,
|
|
market_price: float,
|
|
current_exposure: float = 0) -> dict:
|
|
"""
|
|
计算建议仓位大小
|
|
|
|
Args:
|
|
total_capital: 总资金
|
|
win_prob: 模型预测胜率
|
|
market_price: 当前市场价格 (0-1)
|
|
current_exposure: 当前已有仓位占比
|
|
|
|
Returns:
|
|
dict: 包含建议仓位大小和相关信息
|
|
"""
|
|
# 计算赔率
|
|
if market_price <= 0 or market_price >= 1:
|
|
return {"size": 0, "error": "Invalid market price"}
|
|
|
|
odds = (1 - market_price) / market_price
|
|
|
|
# Kelly计算
|
|
kelly_ratio = self.kelly_criterion(win_prob, odds)
|
|
|
|
# 检查总仓位限制
|
|
available_ratio = self.max_total_exposure - current_exposure
|
|
if available_ratio <= 0:
|
|
return {
|
|
"size": 0,
|
|
"kelly_ratio": kelly_ratio,
|
|
"reason": "Max exposure reached"
|
|
}
|
|
|
|
# 实际使用比例
|
|
actual_ratio = min(kelly_ratio, available_ratio)
|
|
|
|
# 计算金额
|
|
position_size = total_capital * actual_ratio
|
|
|
|
# 检查最小交易额
|
|
if position_size < self.min_trade_size:
|
|
return {
|
|
"size": 0,
|
|
"kelly_ratio": kelly_ratio,
|
|
"reason": f"Below minimum trade size (${self.min_trade_size})"
|
|
}
|
|
|
|
return {
|
|
"size": position_size,
|
|
"kelly_ratio": kelly_ratio,
|
|
"actual_ratio": actual_ratio,
|
|
"odds": odds,
|
|
"expected_return": (win_prob * odds - (1 - win_prob)) * position_size
|
|
}
|
|
|
|
def should_exit(self,
|
|
entry_price: float,
|
|
current_price: float,
|
|
current_prediction: float,
|
|
stop_loss: float = 0.15,
|
|
take_profit: float = 0.30) -> dict:
|
|
"""
|
|
判断是否应该平仓
|
|
|
|
Args:
|
|
entry_price: 入场价格
|
|
current_price: 当前价格
|
|
current_prediction: 当前模型预测
|
|
stop_loss: 止损比例
|
|
take_profit: 止盈比例
|
|
|
|
Returns:
|
|
dict: 退出建议
|
|
"""
|
|
if entry_price <= 0:
|
|
return {"should_exit": False}
|
|
|
|
pnl_ratio = (current_price - entry_price) / entry_price
|
|
|
|
# 止损
|
|
if pnl_ratio < -stop_loss:
|
|
return {
|
|
"should_exit": True,
|
|
"reason": "STOP_LOSS",
|
|
"pnl_ratio": pnl_ratio
|
|
}
|
|
|
|
# 止盈
|
|
if pnl_ratio > take_profit:
|
|
return {
|
|
"should_exit": True,
|
|
"reason": "TAKE_PROFIT",
|
|
"pnl_ratio": pnl_ratio
|
|
}
|
|
|
|
# 模型预测反转
|
|
if current_prediction < 0.4: # 预测胜率下降
|
|
return {
|
|
"should_exit": True,
|
|
"reason": "PREDICTION_REVERSAL",
|
|
"pnl_ratio": pnl_ratio,
|
|
"current_prediction": current_prediction
|
|
}
|
|
|
|
return {
|
|
"should_exit": False,
|
|
"pnl_ratio": pnl_ratio
|
|
}
|