|
|
|
@@ -632,7 +632,7 @@ def _torch_forecast_entry_signal(
|
|
|
|
|
return Signal(symbol, "HOLD", 0.0, "torch_forecast: symbol position limit reached")
|
|
|
|
|
|
|
|
|
|
stop_loss_percent = _clamp(settings.stop_loss_percent, 0.003, 0.08)
|
|
|
|
|
sizing = _torch_forecast_position_sizing(settings, account, stop_loss_percent, forecast)
|
|
|
|
|
sizing = _torch_forecast_position_sizing(settings, account, stop_loss_percent, forecast, symbol)
|
|
|
|
|
position_notional = float(sizing["notional_usdt"])
|
|
|
|
|
expected_return = _safe_float(forecast.get("expected_return_percent"), 0.0)
|
|
|
|
|
probability_up = _safe_float(forecast.get("probability_up"), 0.5)
|
|
|
|
@@ -721,6 +721,8 @@ def _torch_forecast_entry_signal(
|
|
|
|
|
"rebound_probability": rebound.get("probability", 0.0),
|
|
|
|
|
}
|
|
|
|
|
edge_mode = "rebound_fallback" if fallback_rebound_entry_ok else "rebound"
|
|
|
|
|
risk_size_ok = position_notional >= settings.min_position_usdt
|
|
|
|
|
rebound_entry_sized_ok = rebound_entry_ok and risk_size_ok
|
|
|
|
|
checks = {
|
|
|
|
|
"torch_model_ok": model_ok,
|
|
|
|
|
"quality_gate_ok": quality_gate_ok,
|
|
|
|
@@ -732,7 +734,7 @@ def _torch_forecast_entry_signal(
|
|
|
|
|
"confidence_ok": confidence >= settings.time_series_min_confidence,
|
|
|
|
|
"spread_ok": spread_ok,
|
|
|
|
|
"liquidity_ok": liquidity_ok,
|
|
|
|
|
"risk_size_ok": position_notional >= settings.min_position_usdt,
|
|
|
|
|
"risk_size_ok": risk_size_ok,
|
|
|
|
|
}
|
|
|
|
|
diagnostics = {
|
|
|
|
|
"strategy_mode": "torch_forecast",
|
|
|
|
@@ -756,6 +758,7 @@ def _torch_forecast_entry_signal(
|
|
|
|
|
"model_rebound_entry_ok": model_rebound_entry_ok,
|
|
|
|
|
"fallback_rebound_entry_ok": fallback_rebound_entry_ok,
|
|
|
|
|
"rebound_entry_ok": rebound_entry_ok,
|
|
|
|
|
"rebound_entry_sized_ok": rebound_entry_sized_ok,
|
|
|
|
|
"min_confidence": settings.time_series_min_confidence,
|
|
|
|
|
"skill": skill,
|
|
|
|
|
"quality_gate": forecast.get("quality_gate", {}),
|
|
|
|
@@ -769,8 +772,8 @@ def _torch_forecast_entry_signal(
|
|
|
|
|
"llm": {},
|
|
|
|
|
}
|
|
|
|
|
base_entry_ok = all(checks.values())
|
|
|
|
|
if base_entry_ok or rebound_entry_ok:
|
|
|
|
|
buy_confidence = max(confidence, float(rebound.get("probability", 0.0) or 0.0)) if rebound_entry_ok else confidence
|
|
|
|
|
if base_entry_ok or rebound_entry_sized_ok:
|
|
|
|
|
buy_confidence = max(confidence, float(rebound.get("probability", 0.0) or 0.0)) if rebound_entry_sized_ok else confidence
|
|
|
|
|
entry_path = edge_mode if rebound_entry_ok and not base_entry_ok else edge_mode
|
|
|
|
|
diagnostics["entry_path"] = entry_path
|
|
|
|
|
if fallback_rebound_entry_ok and not base_entry_ok:
|
|
|
|
@@ -961,8 +964,12 @@ def _torch_min_probability(settings: Settings) -> float:
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _dynamic_symbol_position_limit(settings: Settings) -> int:
|
|
|
|
|
exposure_based_limit = int(settings.max_symbol_exposure_usdt // max(settings.min_position_usdt, 0.01))
|
|
|
|
|
return max(1, settings.max_positions_per_symbol, exposure_based_limit)
|
|
|
|
|
configured_limit = max(1, settings.max_positions_per_symbol)
|
|
|
|
|
exposure_based_limit = max(
|
|
|
|
|
1,
|
|
|
|
|
int(settings.max_symbol_exposure_usdt // max(settings.min_position_usdt, 0.01)),
|
|
|
|
|
)
|
|
|
|
|
return min(configured_limit, exposure_based_limit)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _torch_forecast_confidence(settings: Settings, forecast: dict) -> float:
|
|
|
|
@@ -982,30 +989,43 @@ def _torch_forecast_position_sizing(
|
|
|
|
|
account: dict | None,
|
|
|
|
|
stop_loss_percent: float,
|
|
|
|
|
forecast: dict,
|
|
|
|
|
symbol: str | None = None,
|
|
|
|
|
) -> dict[str, float | str]:
|
|
|
|
|
base = _trend_position_sizing(settings, account, stop_loss_percent)
|
|
|
|
|
base_notional = float(base["notional_usdt"])
|
|
|
|
|
if base_notional <= 0:
|
|
|
|
|
kelly = _kelly_position(
|
|
|
|
|
settings=settings,
|
|
|
|
|
final_score=_torch_forecast_confidence(settings, forecast),
|
|
|
|
|
forecast=forecast,
|
|
|
|
|
adaptive={},
|
|
|
|
|
account=account,
|
|
|
|
|
symbol=symbol,
|
|
|
|
|
)
|
|
|
|
|
expected_return = max(0.0, _safe_float(forecast.get("expected_return_percent"), 0.0))
|
|
|
|
|
probability_up = _safe_float(forecast.get("probability_up"), 0.5)
|
|
|
|
|
skill = max(0.0, _safe_float(forecast.get("skill"), 0.0))
|
|
|
|
|
min_edge = max(0.01, settings.time_series_min_edge_percent)
|
|
|
|
|
edge_multiplier = _clamp(expected_return / max(min_edge * 3.0, 0.01), 0.25, 1.15)
|
|
|
|
|
probability_multiplier = _clamp(0.75 + (probability_up - 0.55) * 3.0, 0.50, 1.20)
|
|
|
|
|
skill_multiplier = _clamp(0.85 + skill * 0.60, 0.60, 1.15)
|
|
|
|
|
if settings.kelly_sizing_enabled:
|
|
|
|
|
raw = float(kelly["kelly_remaining_notional_usdt"]) * _risk_guard_multiplier(account)
|
|
|
|
|
notional = 0.0 if raw < settings.min_position_usdt else min(raw, settings.max_position_usdt)
|
|
|
|
|
elif base_notional <= 0:
|
|
|
|
|
notional = 0.0
|
|
|
|
|
edge_multiplier = probability_multiplier = skill_multiplier = 0.0
|
|
|
|
|
else:
|
|
|
|
|
expected_return = max(0.0, _safe_float(forecast.get("expected_return_percent"), 0.0))
|
|
|
|
|
probability_up = _safe_float(forecast.get("probability_up"), 0.5)
|
|
|
|
|
skill = max(0.0, _safe_float(forecast.get("skill"), 0.0))
|
|
|
|
|
min_edge = max(0.01, settings.time_series_min_edge_percent)
|
|
|
|
|
edge_multiplier = _clamp(expected_return / max(min_edge * 3.0, 0.01), 0.25, 1.15)
|
|
|
|
|
probability_multiplier = _clamp(0.75 + (probability_up - 0.55) * 3.0, 0.50, 1.20)
|
|
|
|
|
skill_multiplier = _clamp(0.85 + skill * 0.60, 0.60, 1.15)
|
|
|
|
|
raw = base_notional * edge_multiplier * probability_multiplier * skill_multiplier
|
|
|
|
|
notional = 0.0 if raw < settings.min_position_usdt else min(raw, settings.max_position_usdt)
|
|
|
|
|
return {
|
|
|
|
|
**base,
|
|
|
|
|
"method": "torch_forecast_risk",
|
|
|
|
|
"method": "torch_forecast_fractional_kelly" if settings.kelly_sizing_enabled else "torch_forecast_risk",
|
|
|
|
|
"enabled": bool(settings.kelly_sizing_enabled),
|
|
|
|
|
"notional_usdt": round(notional, 2),
|
|
|
|
|
"base_notional_usdt": base["notional_usdt"],
|
|
|
|
|
"torch_edge_multiplier": round(edge_multiplier, 4),
|
|
|
|
|
"torch_probability_multiplier": round(probability_multiplier, 4),
|
|
|
|
|
"torch_skill_multiplier": round(skill_multiplier, 4),
|
|
|
|
|
**kelly,
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@@ -1168,6 +1188,7 @@ def _kelly_position(
|
|
|
|
|
forecast: dict,
|
|
|
|
|
adaptive: dict,
|
|
|
|
|
account: dict | None,
|
|
|
|
|
symbol: str | None = None,
|
|
|
|
|
) -> dict[str, float | bool | str]:
|
|
|
|
|
confidence_probability = _confidence_probability(final_score, settings.min_signal_confidence)
|
|
|
|
|
probability_source = "confidence"
|
|
|
|
@@ -1180,8 +1201,13 @@ def _kelly_position(
|
|
|
|
|
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)
|
|
|
|
|
base_win_return = max(0.0, take_profit - round_trip_cost)
|
|
|
|
|
loss_return = max(0.0001, stop_loss + round_trip_cost)
|
|
|
|
|
expected_net_return = max(0.0, _safe_float(forecast.get("expected_return_percent"), 0.0) / 100.0)
|
|
|
|
|
implied_win_return = 0.0
|
|
|
|
|
if probability > 0:
|
|
|
|
|
implied_win_return = max(0.0, (expected_net_return + (1.0 - probability) * loss_return) / probability)
|
|
|
|
|
win_return = max(base_win_return, implied_win_return)
|
|
|
|
|
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)
|
|
|
|
@@ -1190,19 +1216,41 @@ def _kelly_position(
|
|
|
|
|
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)
|
|
|
|
|
target_notional = max(0.0, bankroll * effective_fraction)
|
|
|
|
|
open_symbol_exposure = _account_symbol_exposure(account, symbol)
|
|
|
|
|
remaining_notional = max(0.0, target_notional - open_symbol_exposure)
|
|
|
|
|
return {
|
|
|
|
|
"kelly_probability": round(probability, 4),
|
|
|
|
|
"kelly_probability_source": probability_source,
|
|
|
|
|
"kelly_reward_loss_ratio": round(reward_loss_ratio, 4),
|
|
|
|
|
"kelly_win_return_percent": round(win_return * 100.0, 4),
|
|
|
|
|
"kelly_loss_return_percent": round(loss_return * 100.0, 4),
|
|
|
|
|
"kelly_expected_net_percent": round(expected_net_return * 100.0, 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),
|
|
|
|
|
"kelly_target_notional_usdt": round(target_notional, 2),
|
|
|
|
|
"kelly_open_symbol_exposure_usdt": round(open_symbol_exposure, 2),
|
|
|
|
|
"kelly_remaining_notional_usdt": round(remaining_notional, 2),
|
|
|
|
|
"kelly_notional_usdt": round(remaining_notional, 2),
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def _account_symbol_exposure(account: dict | None, symbol: str | None = None) -> float:
|
|
|
|
|
if not isinstance(account, dict):
|
|
|
|
|
return 0.0
|
|
|
|
|
direct = _safe_float(account.get("symbol_exposure_usdt"), -1.0)
|
|
|
|
|
if direct >= 0:
|
|
|
|
|
return max(0.0, direct)
|
|
|
|
|
if not symbol:
|
|
|
|
|
symbol = str(account.get("symbol", "") or "")
|
|
|
|
|
exposures = account.get("symbol_exposures")
|
|
|
|
|
if isinstance(exposures, dict) and symbol:
|
|
|
|
|
return max(0.0, _safe_float(exposures.get(symbol), 0.0))
|
|
|
|
|
return 0.0
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
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)
|
|
|
|
|