Add fractional Kelly position sizing
This commit is contained in:
+92
-17
@@ -21,6 +21,7 @@ class SpotStrategy:
|
||||
learning: dict | None = None,
|
||||
llm: dict | None = None,
|
||||
forecast: dict | None = None,
|
||||
account: dict | None = None,
|
||||
) -> Signal:
|
||||
if ticker is None:
|
||||
return Signal(symbol, "HOLD", 0.0, "нет ticker-данных")
|
||||
@@ -134,15 +135,16 @@ class SpotStrategy:
|
||||
pattern_blocks_entry = negative_pattern and not (
|
||||
rebound["active"] and rebound_entry_score >= entry_threshold
|
||||
)
|
||||
position_notional = _position_notional(
|
||||
position_sizing = _position_sizing(
|
||||
settings=self.settings,
|
||||
final_score=final_score,
|
||||
grid_active=grid["active"],
|
||||
rebound_active=rebound["active"],
|
||||
llm=llm,
|
||||
forecast=forecast,
|
||||
adaptive=adaptive,
|
||||
account=account,
|
||||
)
|
||||
position_notional = float(position_sizing["notional_usdt"])
|
||||
trade_mode = "GRID" if grid["active"] else "REBOUND" if rebound["active"] else "NORMAL"
|
||||
|
||||
diagnostics = {
|
||||
@@ -163,6 +165,7 @@ class SpotStrategy:
|
||||
"falling_market": falling_market,
|
||||
"open_positions_for_symbol": open_positions_for_symbol,
|
||||
"position_notional_usdt": position_notional,
|
||||
"position_sizing": position_sizing,
|
||||
"trade_mode": trade_mode,
|
||||
"base_entry_threshold": round(base_entry_threshold, 4),
|
||||
"adaptive_entry_threshold_adjustment": round(adaptive_entry_adjustment, 4),
|
||||
@@ -473,16 +476,16 @@ def _clamp(value: float, low: float, high: float) -> float:
|
||||
return max(low, min(high, value))
|
||||
|
||||
|
||||
def _position_notional(
|
||||
def _position_sizing(
|
||||
*,
|
||||
settings: Settings,
|
||||
final_score: float,
|
||||
grid_active: bool,
|
||||
rebound_active: bool,
|
||||
llm: dict,
|
||||
forecast: dict | None = None,
|
||||
adaptive: dict | None = None,
|
||||
) -> float:
|
||||
account: dict | None = None,
|
||||
) -> dict[str, float | bool | str]:
|
||||
low = max(0.0, settings.min_position_usdt)
|
||||
high = max(low, settings.max_position_usdt)
|
||||
if grid_active:
|
||||
@@ -491,28 +494,100 @@ def _position_notional(
|
||||
high = max(low, min(high, settings.rebound_max_position_usdt))
|
||||
denominator = max(0.0001, 1.0 - settings.min_signal_confidence)
|
||||
confidence_ratio = _clamp((final_score - settings.min_signal_confidence) / denominator, 0.0, 1.0)
|
||||
raw = low + (high - low) * confidence_ratio
|
||||
risk = str(llm.get("risk_level", "medium")).lower()
|
||||
if risk == "high":
|
||||
raw *= 0.55
|
||||
elif risk == "low":
|
||||
raw *= 1.10
|
||||
confidence_notional = low + (high - low) * confidence_ratio
|
||||
risk_multiplier = _position_risk_multiplier(forecast, adaptive)
|
||||
method = "confidence"
|
||||
raw = confidence_notional
|
||||
kelly = _kelly_position(
|
||||
settings=settings,
|
||||
final_score=final_score,
|
||||
forecast=forecast or {},
|
||||
adaptive=adaptive or {},
|
||||
account=account,
|
||||
)
|
||||
if settings.kelly_sizing_enabled:
|
||||
method = "fractional_kelly"
|
||||
raw = float(kelly["kelly_notional_usdt"])
|
||||
raw *= risk_multiplier
|
||||
notional = round(_clamp(raw, low, high), 2)
|
||||
return {
|
||||
"method": method,
|
||||
"enabled": bool(settings.kelly_sizing_enabled),
|
||||
"notional_usdt": notional,
|
||||
"confidence_notional_usdt": round(confidence_notional, 2),
|
||||
"risk_multiplier": round(risk_multiplier, 4),
|
||||
"low_cap_usdt": round(low, 2),
|
||||
"high_cap_usdt": round(high, 2),
|
||||
**kelly,
|
||||
}
|
||||
|
||||
|
||||
def _position_risk_multiplier(forecast: dict | None, adaptive: dict | None) -> float:
|
||||
multiplier = 1.0
|
||||
forecast = forecast or {}
|
||||
if forecast.get("usable"):
|
||||
probability_up = _safe_float(forecast.get("probability_up"), 0.5)
|
||||
volatility_percent = _safe_float(forecast.get("volatility_percent"), 0.0)
|
||||
if probability_up < 0.52:
|
||||
raw *= 0.75
|
||||
multiplier *= 0.75
|
||||
elif probability_up >= 0.60:
|
||||
raw *= 1.08
|
||||
multiplier *= 1.08
|
||||
if volatility_percent >= 0.8:
|
||||
raw *= 0.70
|
||||
multiplier *= 0.70
|
||||
risk_mode = str((adaptive or {}).get("risk_mode", "neutral")).lower()
|
||||
if risk_mode == "defensive":
|
||||
raw *= 0.65
|
||||
multiplier *= 0.65
|
||||
elif risk_mode == "expansion":
|
||||
raw *= 1.10
|
||||
return round(_clamp(raw, low, high), 2)
|
||||
multiplier *= 1.10
|
||||
return multiplier
|
||||
|
||||
|
||||
def _kelly_position(
|
||||
*,
|
||||
settings: Settings,
|
||||
final_score: float,
|
||||
forecast: dict,
|
||||
adaptive: dict,
|
||||
account: dict | None,
|
||||
) -> dict[str, float | bool | str]:
|
||||
confidence_probability = _confidence_probability(final_score, settings.min_signal_confidence)
|
||||
probability_source = "confidence"
|
||||
probability = confidence_probability
|
||||
if forecast.get("usable"):
|
||||
probability = _safe_float(forecast.get("probability_up"), confidence_probability)
|
||||
probability_source = "forecast"
|
||||
probability = _clamp(probability, 0.0, 1.0)
|
||||
|
||||
stop_loss = _adaptive_percent(adaptive, "stop_loss_percent", settings.stop_loss_percent, 0.003, 0.08)
|
||||
take_profit = _adaptive_percent(adaptive, "take_profit_percent", settings.take_profit_percent, 0.003, 0.20)
|
||||
round_trip_cost = max(0.0, 2.0 * (settings.taker_fee_rate + settings.slippage_rate))
|
||||
win_return = max(0.0, take_profit - round_trip_cost)
|
||||
loss_return = max(0.0001, stop_loss + round_trip_cost)
|
||||
reward_loss_ratio = win_return / loss_return if loss_return > 0 else 0.0
|
||||
full_kelly = probability - ((1.0 - probability) / reward_loss_ratio) if reward_loss_ratio > 0 else 0.0
|
||||
full_kelly = max(0.0, full_kelly)
|
||||
fractional_kelly = full_kelly * _clamp(settings.kelly_fraction, 0.0, 1.0)
|
||||
effective_fraction = _clamp(fractional_kelly, 0.0, _clamp(settings.kelly_max_fraction, 0.0, 1.0))
|
||||
bankroll = _safe_float((account or {}).get("equity"), settings.starting_balance_usdt)
|
||||
if bankroll <= 0:
|
||||
bankroll = settings.starting_balance_usdt
|
||||
kelly_notional = max(0.0, bankroll * effective_fraction)
|
||||
return {
|
||||
"kelly_probability": round(probability, 4),
|
||||
"kelly_probability_source": probability_source,
|
||||
"kelly_reward_loss_ratio": round(reward_loss_ratio, 4),
|
||||
"kelly_full_fraction": round(full_kelly, 4),
|
||||
"kelly_fractional_fraction": round(fractional_kelly, 4),
|
||||
"kelly_effective_fraction": round(effective_fraction, 4),
|
||||
"kelly_bankroll_usdt": round(bankroll, 2),
|
||||
"kelly_notional_usdt": round(kelly_notional, 2),
|
||||
}
|
||||
|
||||
|
||||
def _confidence_probability(final_score: float, min_signal_confidence: float) -> float:
|
||||
denominator = max(0.0001, 1.0 - min_signal_confidence)
|
||||
ratio = _clamp((final_score - min_signal_confidence) / denominator, 0.0, 1.0)
|
||||
return 0.50 + ratio * 0.18
|
||||
|
||||
|
||||
def _grid_state(
|
||||
|
||||
Reference in New Issue
Block a user