Shift adaptive learning to growth sizing

This commit is contained in:
Codex
2026-06-21 08:16:03 +03:00
parent 7bbb721da1
commit 25651d7fa7
9 changed files with 144 additions and 42 deletions
+54 -27
View File
@@ -127,17 +127,27 @@ class TradeLearner:
rules = dict(self.state.adaptive_rules or {})
symbol_adjustments = rules.get("symbol_threshold_adjustments") or {}
pattern_adjustments = rules.get("pattern_threshold_adjustments") or {}
symbol_multipliers = rules.get("symbol_position_multipliers") or {}
pattern_multipliers = rules.get("pattern_position_multipliers") or {}
symbol_adjustment = float(symbol_adjustments.get(symbol, 0.0) or 0.0)
pattern_adjustment = float(pattern_adjustments.get(pattern, 0.0) or 0.0)
base_adjustment = float(rules.get("entry_threshold_adjustment", 0.0) or 0.0)
base_multiplier = float(rules.get("position_size_multiplier", 1.0) or 1.0)
symbol_multiplier = float(symbol_multipliers.get(symbol, 1.0) or 1.0)
pattern_multiplier = float(pattern_multipliers.get(pattern, 1.0) or 1.0)
max_multiplier = max(1.0, self.settings.learning_max_position_multiplier)
effective = _clamp(
base_adjustment + symbol_adjustment + pattern_adjustment,
-self.settings.learning_max_adjustment,
self.settings.learning_max_adjustment,
)
effective_multiplier = _clamp(base_multiplier * symbol_multiplier * pattern_multiplier, 0.25, max_multiplier)
rules["symbol_entry_threshold_adjustment"] = round(symbol_adjustment, 4)
rules["pattern_entry_threshold_adjustment"] = round(pattern_adjustment, 4)
rules["effective_entry_threshold_adjustment"] = round(effective, 4)
rules["symbol_position_size_multiplier"] = round(symbol_multiplier, 4)
rules["pattern_position_size_multiplier"] = round(pattern_multiplier, 4)
rules["effective_position_size_multiplier"] = round(effective_multiplier, 4)
rules["symbol_blocked"] = symbol in set(rules.get("blocked_symbols") or [])
rules["pattern_blocked"] = pattern in set(rules.get("blocked_patterns") or [])
return rules
@@ -210,42 +220,26 @@ def _build_adaptive_rules(
win_rate = wins / sample_size if sample_size else 0.0
if total_net < 0:
threshold = _clamp(
(0.5 - win_rate) * 0.12 + min(abs(total_net) / max(sample_size, 1), 0.05),
0.0,
settings.learning_max_adjustment,
rules["risk_mode"] = "selective"
rules["trade_permission"] = "selective_growth"
rules["reasons"].append(
"общая статистика обучения убыточна: глобальная защита капитала выключена, блокируются только плохие пары/паттерны"
)
rules["entry_threshold_adjustment"] = round(threshold, 4)
rules["risk_mode"] = "defensive"
rules["trade_permission"] = "capital_protection"
rules["reduce_exposure"] = True
rules["bad_market_entry_block"] = True
rules["target_total_exposure_usdt"] = round(
min(settings.max_total_exposure_usdt, max(settings.min_position_usdt, settings.starting_balance_usdt * 0.35)),
2,
)
rules["target_symbol_exposure_usdt"] = round(
min(settings.max_symbol_exposure_usdt, max(settings.min_position_usdt, settings.max_position_usdt * 0.5)),
2,
)
rules["min_hold_seconds"] = int(min(max(settings.min_hold_seconds * 2, 300), 900))
if win_rate <= 0.25:
rules["stop_loss_percent"] = round(max(0.008, settings.stop_loss_percent * 0.85), 4)
rules["take_profit_percent"] = round(
max(settings.take_profit_percent, (_round_trip_cost_percent(settings) + 0.6) / 100),
4,
)
rules["reasons"].append("общая статистика обучения убыточна: вход ужесточен, риск снижен")
elif win_rate >= 0.55:
threshold = -min(settings.learning_max_adjustment / 2, (win_rate - 0.5) * 0.08)
rules["entry_threshold_adjustment"] = round(threshold, 4)
rules["risk_mode"] = "expansion"
rules["reasons"].append("общая статистика обучения положительная: вход можно немного расширить")
rules["risk_mode"] = "growth"
rules["trade_permission"] = "positive_expectancy"
rules["position_size_multiplier"] = _global_position_multiplier(total_net, win_rate, sample_size, settings)
rules["reasons"].append("общая статистика обучения положительная: хорошие сетапы можно масштабировать по Kelly")
for name, stat in symbol_stats.items():
adjustment = _threshold_adjustment_from_stat(stat, settings)
if adjustment:
rules["symbol_threshold_adjustments"][name] = adjustment
multiplier = _position_multiplier_from_stat(stat, settings)
if multiplier > 1.0:
rules["symbol_position_multipliers"][name] = multiplier
if _should_block_stat(stat, settings):
rules["blocked_symbols"].append(name)
@@ -253,6 +247,9 @@ def _build_adaptive_rules(
adjustment = _threshold_adjustment_from_stat(stat, settings)
if adjustment:
rules["pattern_threshold_adjustments"][name] = adjustment
multiplier = _position_multiplier_from_stat(stat, settings)
if multiplier > 1.0:
rules["pattern_position_multipliers"][name] = multiplier
if _should_block_stat(stat, settings):
rules["blocked_patterns"].append(name)
@@ -286,6 +283,7 @@ def _neutral_rules(settings: Settings, reason: str | None = None) -> dict[str, A
"win_rate": 0.0,
"round_trip_cost_percent": round(_round_trip_cost_percent(settings), 4),
"entry_threshold_adjustment": 0.0,
"position_size_multiplier": 1.0,
"risk_mode": "neutral",
"trade_permission": "normal",
"allow_new_entries": True,
@@ -305,6 +303,8 @@ def _neutral_rules(settings: Settings, reason: str | None = None) -> dict[str, A
"trailing_stop_percent": settings.trailing_stop_percent,
"symbol_threshold_adjustments": {},
"pattern_threshold_adjustments": {},
"symbol_position_multipliers": {},
"pattern_position_multipliers": {},
"blocked_symbols": [],
"blocked_patterns": [],
"exit_reason_stats": {},
@@ -389,6 +389,33 @@ def _threshold_adjustment_from_stat(stat: dict[str, Any], settings: Settings) ->
return 0.0
def _position_multiplier_from_stat(stat: dict[str, Any], settings: Settings) -> float:
sample_size = int(stat.get("sample_size", 0) or 0)
if sample_size < settings.learning_min_samples:
return 1.0
net_pnl = float(stat.get("net_pnl", 0.0) or 0.0)
avg_pnl = float(stat.get("average_net_pnl", 0.0) or 0.0)
win_rate = float(stat.get("win_rate", 0.0) or 0.0)
if net_pnl <= 0 or avg_pnl <= 0 or win_rate < 0.50:
return 1.0
cost_usdt = max(settings.min_position_usdt * _round_trip_cost_percent(settings) / 100, 0.01)
edge_score = _clamp(avg_pnl / cost_usdt, 0.0, 1.0)
win_score = _clamp((win_rate - 0.50) / 0.35, 0.0, 1.0)
raw = 1.0 + edge_score * 0.25 + win_score * 0.35
return round(_clamp(raw, 1.0, max(1.0, settings.learning_max_position_multiplier)), 4)
def _global_position_multiplier(total_net: float, win_rate: float, sample_size: int, settings: Settings) -> float:
if sample_size < settings.learning_min_samples or total_net <= 0 or win_rate < 0.55:
return 1.0
avg_pnl = total_net / max(sample_size, 1)
cost_usdt = max(settings.min_position_usdt * _round_trip_cost_percent(settings) / 100, 0.01)
edge_score = _clamp(avg_pnl / cost_usdt, 0.0, 1.0)
win_score = _clamp((win_rate - 0.55) / 0.30, 0.0, 1.0)
raw = 1.0 + edge_score * 0.15 + win_score * 0.20
return round(_clamp(raw, 1.0, max(1.0, settings.learning_max_position_multiplier)), 4)
def _is_bad_stat(stat: dict[str, Any], settings: Settings) -> bool:
sample_size = int(stat.get("sample_size", 0) or 0)
net_pnl = float(stat.get("net_pnl", 0.0) or 0.0)