Allow exchange-minimum Kelly layers
This commit is contained in:
@@ -140,7 +140,7 @@ class CryptoSpotBot:
|
||||
symbol,
|
||||
"HOLD",
|
||||
0.0,
|
||||
"пауза после закрытия позиции",
|
||||
"пауза между входами по паре",
|
||||
{"cooldown_remaining_seconds": cooldown_seconds - age},
|
||||
)
|
||||
)
|
||||
@@ -150,6 +150,7 @@ class CryptoSpotBot:
|
||||
candles = self.market.candles.get(symbol, [])
|
||||
trend_candles = self.market.trend_candles.get(symbol, [])
|
||||
open_count = len(self.broker.positions_for_symbol(symbol))
|
||||
instrument = self.market.instruments.get(symbol)
|
||||
pattern = self.market.patterns.get(symbol, {})
|
||||
forecast = self.market.forecasts.get(symbol, {})
|
||||
learning = self.learner.adjustment_for(symbol, str(pattern.get("label", ""))).as_dict()
|
||||
@@ -159,6 +160,7 @@ class CryptoSpotBot:
|
||||
account["symbol"] = symbol
|
||||
account["symbol_exposure_usdt"] = self.broker.symbol_exposure(symbol)
|
||||
account["open_positions_for_symbol"] = open_count
|
||||
account["exchange_min_entry_usdt"] = self.broker.minimum_entry_budget(instrument, ticker)
|
||||
if risk_guard.get("block_new_entries"):
|
||||
self.storage.insert_signal(
|
||||
Signal(
|
||||
@@ -224,12 +226,14 @@ class CryptoSpotBot:
|
||||
)
|
||||
self.storage.insert_signal(signal)
|
||||
if signal.action == "BUY" and ticker is not None:
|
||||
self.broker.buy(
|
||||
position = self.broker.buy(
|
||||
signal,
|
||||
ticker,
|
||||
self.market.instruments.get(symbol),
|
||||
instrument,
|
||||
prices,
|
||||
)
|
||||
if position is not None:
|
||||
self._entry_cooldown_until[symbol] = utc_now()
|
||||
|
||||
@staticmethod
|
||||
def _risk_guard_for_symbol(risk_guard: dict, symbol: str) -> dict:
|
||||
|
||||
@@ -278,6 +278,10 @@ class PaperBroker:
|
||||
value = default
|
||||
return max(low, min(high, value))
|
||||
|
||||
def minimum_entry_budget(self, instrument: Instrument | None, ticker: Ticker | None = None) -> float:
|
||||
fill_price = self._buy_price(ticker) if ticker is not None else None
|
||||
return self._minimum_entry_budget(instrument, fill_price)
|
||||
|
||||
def _minimum_entry_budget(self, instrument: Instrument | None, fill_price: float | None = None) -> float:
|
||||
minimum = max(0.0, self.settings.min_position_usdt)
|
||||
if instrument:
|
||||
|
||||
@@ -631,8 +631,11 @@ def _torch_forecast_entry_signal(
|
||||
if open_positions_for_symbol >= _dynamic_symbol_position_limit(settings):
|
||||
return Signal(symbol, "HOLD", 0.0, "torch_forecast: symbol position limit reached")
|
||||
|
||||
account_context = dict(account or {})
|
||||
account_context.setdefault("symbol", symbol)
|
||||
account_context.setdefault("open_positions_for_symbol", open_positions_for_symbol)
|
||||
stop_loss_percent = _clamp(settings.stop_loss_percent, 0.003, 0.08)
|
||||
sizing = _torch_forecast_position_sizing(settings, account, stop_loss_percent, forecast, symbol)
|
||||
sizing = _torch_forecast_position_sizing(settings, account_context, 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)
|
||||
@@ -1218,7 +1221,28 @@ def _kelly_position(
|
||||
bankroll = settings.starting_balance_usdt
|
||||
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)
|
||||
raw_remaining_notional = max(0.0, target_notional - open_symbol_exposure)
|
||||
exchange_min_entry = _account_exchange_min_entry(account, settings)
|
||||
remaining_notional = raw_remaining_notional
|
||||
effective_target_notional = target_notional
|
||||
layer_mode = False
|
||||
if (
|
||||
symbol
|
||||
and target_notional > 0
|
||||
and raw_remaining_notional < exchange_min_entry
|
||||
and exchange_min_entry > settings.min_position_usdt + 1e-9
|
||||
and _account_open_positions_for_symbol(account) > 0
|
||||
):
|
||||
room = min(
|
||||
max(0.0, settings.max_position_usdt),
|
||||
max(0.0, settings.max_symbol_exposure_usdt - open_symbol_exposure),
|
||||
max(0.0, settings.max_total_exposure_usdt - _account_total_exposure(account)),
|
||||
max(0.0, _safe_float((account or {}).get("cash"), settings.starting_balance_usdt) - settings.min_cash_reserve_usdt),
|
||||
)
|
||||
if room >= exchange_min_entry:
|
||||
remaining_notional = exchange_min_entry
|
||||
effective_target_notional = open_symbol_exposure + exchange_min_entry
|
||||
layer_mode = True
|
||||
return {
|
||||
"kelly_probability": round(probability, 4),
|
||||
"kelly_probability_source": probability_source,
|
||||
@@ -1231,9 +1255,13 @@ def _kelly_position(
|
||||
"kelly_effective_fraction": round(effective_fraction, 4),
|
||||
"kelly_bankroll_usdt": round(bankroll, 2),
|
||||
"kelly_target_notional_usdt": round(target_notional, 2),
|
||||
"kelly_effective_target_notional_usdt": round(effective_target_notional, 2),
|
||||
"kelly_open_symbol_exposure_usdt": round(open_symbol_exposure, 2),
|
||||
"kelly_raw_remaining_notional_usdt": round(raw_remaining_notional, 2),
|
||||
"kelly_remaining_notional_usdt": round(remaining_notional, 2),
|
||||
"kelly_notional_usdt": round(remaining_notional, 2),
|
||||
"kelly_exchange_min_entry_usdt": round(exchange_min_entry, 2),
|
||||
"kelly_layer_mode": layer_mode,
|
||||
}
|
||||
|
||||
|
||||
@@ -1251,6 +1279,28 @@ def _account_symbol_exposure(account: dict | None, symbol: str | None = None) ->
|
||||
return 0.0
|
||||
|
||||
|
||||
def _account_total_exposure(account: dict | None) -> float:
|
||||
if not isinstance(account, dict):
|
||||
return 0.0
|
||||
return max(0.0, _safe_float(account.get("exposure"), 0.0))
|
||||
|
||||
|
||||
def _account_open_positions_for_symbol(account: dict | None) -> int:
|
||||
if not isinstance(account, dict):
|
||||
return 0
|
||||
try:
|
||||
return max(0, int(account.get("open_positions_for_symbol", 0)))
|
||||
except (TypeError, ValueError):
|
||||
return 0
|
||||
|
||||
|
||||
def _account_exchange_min_entry(account: dict | None, settings: Settings) -> float:
|
||||
minimum = max(0.0, settings.min_position_usdt)
|
||||
if not isinstance(account, dict):
|
||||
return minimum
|
||||
return max(minimum, _safe_float(account.get("exchange_min_entry_usdt"), minimum))
|
||||
|
||||
|
||||
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)
|
||||
|
||||
Reference in New Issue
Block a user