07.4.4.1.11 — Advanced Trend Quality & EMA Distance Layer
This commit is contained in:
@@ -13,6 +13,8 @@ from src.trading.auto.state import AutoTradeState
|
||||
from src.trading.execution.models import ExecutionDecision
|
||||
from src.trading.journal.service import JournalService
|
||||
from src.trading.position.state import PositionState
|
||||
from src.core.numbers import safe_float
|
||||
from src.core.types import JsonDict, NumericLike
|
||||
|
||||
|
||||
@dataclass(slots=True)
|
||||
@@ -108,7 +110,7 @@ class ExecutionEngine:
|
||||
final_size=size,
|
||||
)
|
||||
|
||||
size = self._round_order_size(size)
|
||||
size = self._round_size(size)
|
||||
|
||||
if size <= 0:
|
||||
return ExecutionDecision(
|
||||
@@ -134,7 +136,7 @@ class ExecutionEngine:
|
||||
state.last_execution_action = action
|
||||
state.last_execution_reason = f"Позиция {side} открыта."
|
||||
|
||||
payload = {
|
||||
payload: JsonDict = {
|
||||
"execution_type": "ENTRY",
|
||||
"action": action,
|
||||
"symbol": state.symbol,
|
||||
@@ -218,7 +220,7 @@ class ExecutionEngine:
|
||||
final_size=new_size,
|
||||
)
|
||||
|
||||
new_size = self._round_order_size(new_size)
|
||||
new_size = self._round_size(new_size)
|
||||
|
||||
if new_size <= 0:
|
||||
return ExecutionDecision(
|
||||
@@ -230,11 +232,10 @@ class ExecutionEngine:
|
||||
state.realized_pnl_usd += pnl
|
||||
state.cycle_realized_pnl_usd += pnl
|
||||
|
||||
state.last_flip_old_side = old_side
|
||||
state.last_flip_new_side = new_side
|
||||
state.last_flip_pnl_usd = pnl
|
||||
state.last_flip_reason = state.last_signal_reason
|
||||
state.last_flip_monotonic_at = time.monotonic()
|
||||
state.cycle_closed_trades += 1
|
||||
|
||||
if pnl > 0:
|
||||
state.cycle_winning_trades += 1
|
||||
|
||||
old_side = position.side
|
||||
old_entry_price = position.entry_price
|
||||
@@ -242,6 +243,12 @@ class ExecutionEngine:
|
||||
old_leverage = position.leverage
|
||||
old_opened_at = position.opened_at
|
||||
|
||||
state.last_flip_old_side = old_side
|
||||
state.last_flip_new_side = new_side
|
||||
state.last_flip_pnl_usd = pnl
|
||||
state.last_flip_reason = state.last_signal_reason
|
||||
state.last_flip_monotonic_at = time.monotonic()
|
||||
|
||||
type(self)._position = PositionState(
|
||||
side=new_side,
|
||||
symbol=state.symbol,
|
||||
@@ -261,7 +268,7 @@ class ExecutionEngine:
|
||||
state.last_flip_at = now
|
||||
type(self)._last_flip_block_key = None
|
||||
|
||||
payload = {
|
||||
payload: JsonDict = {
|
||||
"execution_type": "FLIP",
|
||||
"action": f"FLIP_{old_side}_TO_{new_side}",
|
||||
"symbol": state.symbol,
|
||||
@@ -326,8 +333,8 @@ class ExecutionEngine:
|
||||
state: AutoTradeState,
|
||||
*,
|
||||
forced_reason: str | None = None,
|
||||
forced_exit_price: float | None = None,
|
||||
forced_pnl: float | None = None,
|
||||
forced_exit_price: NumericLike | None = None,
|
||||
forced_pnl: NumericLike | None = None,
|
||||
forced_price_meta: _ExecutionPrice | None = None,
|
||||
) -> ExecutionDecision:
|
||||
position = type(self)._position
|
||||
@@ -337,7 +344,7 @@ class ExecutionEngine:
|
||||
return ExecutionDecision("NONE", False, "Нет открытой позиции для закрытия.")
|
||||
|
||||
if forced_exit_price is not None:
|
||||
exit_price = forced_exit_price
|
||||
exit_price = safe_float(forced_exit_price) or 0.0
|
||||
exit_execution = forced_price_meta
|
||||
else:
|
||||
try:
|
||||
@@ -346,14 +353,26 @@ class ExecutionEngine:
|
||||
except Exception as exc:
|
||||
return ExecutionDecision("NONE", False, f"Ошибка получения цены для закрытия: {exc}")
|
||||
|
||||
pnl = forced_pnl if forced_pnl is not None else self._calculate_pnl(exit_price)
|
||||
pnl = (
|
||||
safe_float(forced_pnl)
|
||||
if forced_pnl is not None
|
||||
else self._calculate_pnl(exit_price)
|
||||
)
|
||||
|
||||
if pnl is None:
|
||||
pnl = 0.0
|
||||
|
||||
state.realized_pnl_usd += pnl
|
||||
state.cycle_realized_pnl_usd += pnl
|
||||
|
||||
state.cycle_closed_trades += 1
|
||||
|
||||
if pnl > 0:
|
||||
state.cycle_winning_trades += 1
|
||||
|
||||
now = self._now_time()
|
||||
|
||||
payload = {
|
||||
payload: JsonDict = {
|
||||
"execution_type": "EXIT",
|
||||
"action": "CLOSE",
|
||||
"symbol": state.symbol,
|
||||
@@ -413,7 +432,7 @@ class ExecutionEngine:
|
||||
f"Позиция закрыта по правилу защиты: {forced_reason}.",
|
||||
)
|
||||
|
||||
|
||||
|
||||
return ExecutionDecision("CLOSE", True, "Позиция закрыта.")
|
||||
|
||||
def _risk_close_decision(self, state: AutoTradeState) -> ExecutionDecision | None:
|
||||
@@ -475,18 +494,24 @@ class ExecutionEngine:
|
||||
return False
|
||||
return unrealized_pnl <= -abs(state.max_loss_usd)
|
||||
|
||||
def _calculate_price_move_percent(self, current_price: float) -> float:
|
||||
def _calculate_price_move_percent(
|
||||
self,
|
||||
current_price: NumericLike | None,
|
||||
) -> float:
|
||||
position = type(self)._position
|
||||
|
||||
entry = position.entry_price or 0.0
|
||||
price = safe_float(current_price) or 0.0
|
||||
|
||||
entry = safe_float(position.entry_price) or 0.0
|
||||
|
||||
if entry <= 0:
|
||||
return 0.0
|
||||
|
||||
if position.side == "LONG":
|
||||
return round(((current_price - entry) / entry) * 100, 4)
|
||||
return round(((price - entry) / entry) * 100, 4)
|
||||
|
||||
if position.side == "SHORT":
|
||||
return round(((entry - current_price) / entry) * 100, 4)
|
||||
return round(((entry - price) / entry) * 100, 4)
|
||||
|
||||
return 0.0
|
||||
|
||||
@@ -507,9 +532,9 @@ class ExecutionEngine:
|
||||
def _flip_block_reason(self, state: AutoTradeState) -> str | None:
|
||||
position = type(self)._position
|
||||
|
||||
confidence = float(state.last_signal_confidence or 0.0)
|
||||
repeat_count = int(state.last_signal_repeat_count or 0)
|
||||
unrealized_pnl = float(state.unrealized_pnl_usd or 0.0)
|
||||
confidence = safe_float(state.last_signal_confidence) or 0.0
|
||||
repeat_count = int(safe_float(state.last_signal_repeat_count) or 0)
|
||||
unrealized_pnl = safe_float(state.unrealized_pnl_usd) or 0.0
|
||||
hold_seconds = self._position_hold_seconds(position)
|
||||
momentum_direction = getattr(state, "momentum_direction", None)
|
||||
momentum_state = getattr(state, "momentum_state", None)
|
||||
@@ -560,7 +585,7 @@ class ExecutionEngine:
|
||||
reason: str,
|
||||
) -> ExecutionDecision:
|
||||
position = type(self)._position
|
||||
confidence = float(state.last_signal_confidence or 0.0)
|
||||
confidence = safe_float(state.last_signal_confidence) or 0.0
|
||||
|
||||
state.execution_block_reason = reason
|
||||
state.last_flip_block_reason = reason
|
||||
@@ -578,7 +603,7 @@ class ExecutionEngine:
|
||||
if block_key != type(self)._last_flip_block_key:
|
||||
type(self)._last_flip_block_key = block_key
|
||||
|
||||
payload = {
|
||||
payload: JsonDict = {
|
||||
"execution_type": "FLIP_BLOCKED",
|
||||
"symbol": state.symbol,
|
||||
"position_side": position.side,
|
||||
@@ -700,8 +725,10 @@ class ExecutionEngine:
|
||||
multiplier = 1.0
|
||||
|
||||
execution_confidence_score = getattr(state, "execution_confidence_score", None)
|
||||
if execution_confidence_score is not None:
|
||||
score = max(0.0, min(1.0, float(execution_confidence_score)))
|
||||
score_raw = safe_float(execution_confidence_score)
|
||||
|
||||
if score_raw is not None:
|
||||
score = max(0.0, min(1.0, score_raw))
|
||||
|
||||
if score < 0.55:
|
||||
multiplier *= 0.0
|
||||
@@ -750,17 +777,14 @@ class ExecutionEngine:
|
||||
multiplier *= 1.05
|
||||
|
||||
if momentum_strength is not None:
|
||||
try:
|
||||
strength = float(momentum_strength)
|
||||
strength = safe_float(momentum_strength)
|
||||
|
||||
if strength is not None:
|
||||
if strength >= 1.5:
|
||||
multiplier *= 1.1
|
||||
elif strength <= 0.7:
|
||||
multiplier *= 0.8
|
||||
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if signal == "BUY":
|
||||
if momentum_direction == "DOWN":
|
||||
multiplier *= 0.75
|
||||
@@ -800,7 +824,10 @@ class ExecutionEngine:
|
||||
state.adaptive_size_final = self._round_size(final_size)
|
||||
state.adaptive_size_multiplier = multiplier
|
||||
|
||||
base_risk_percent = float(state.risk_percent or 0.0)
|
||||
if multiplier != 1:
|
||||
state.adaptive_size_changed_at = time.monotonic()
|
||||
|
||||
base_risk_percent = safe_float(state.risk_percent) or 0.0
|
||||
|
||||
state.effective_risk_percent = round(
|
||||
base_risk_percent * multiplier,
|
||||
@@ -847,7 +874,7 @@ class ExecutionEngine:
|
||||
base_size: float,
|
||||
final_size: float,
|
||||
) -> None:
|
||||
adaptive_final = float(state.adaptive_size_final or 0.0)
|
||||
adaptive_final = safe_float(state.adaptive_size_final) or 0.0
|
||||
|
||||
if adaptive_final <= 0:
|
||||
state.effective_risk_percent = 0.0
|
||||
@@ -859,7 +886,7 @@ class ExecutionEngine:
|
||||
min(1.0, final_size / adaptive_final),
|
||||
)
|
||||
|
||||
current_effective_risk = float(state.effective_risk_percent or 0.0)
|
||||
current_effective_risk = safe_float(state.effective_risk_percent) or 0.0
|
||||
|
||||
state.effective_risk_percent = round(
|
||||
current_effective_risk * margin_ratio,
|
||||
@@ -917,7 +944,7 @@ class ExecutionEngine:
|
||||
|
||||
limited_size = self._round_size(max_size)
|
||||
|
||||
adaptive_final = float(state.adaptive_size_final or 0.0)
|
||||
adaptive_final = safe_float(state.adaptive_size_final) or 0.0
|
||||
|
||||
if adaptive_final > 0:
|
||||
effective_multiplier = limited_size / adaptive_final
|
||||
@@ -1011,32 +1038,55 @@ class ExecutionEngine:
|
||||
pricing_role="MARKET_LAST",
|
||||
)
|
||||
|
||||
def _snapshot_price(self, raw_price: object, name: str) -> float:
|
||||
def _snapshot_price(
|
||||
self,
|
||||
raw_price: NumericLike | None,
|
||||
name: str,
|
||||
) -> float:
|
||||
if raw_price is None:
|
||||
raise ValueError(f"Execution snapshot price '{name}' is missing.")
|
||||
raise ValueError(
|
||||
f"Execution snapshot price '{name}' is missing."
|
||||
)
|
||||
|
||||
price = float(raw_price)
|
||||
price = safe_float(raw_price)
|
||||
|
||||
if price is None:
|
||||
raise ValueError(
|
||||
f"Execution snapshot price '{name}' is invalid."
|
||||
)
|
||||
|
||||
if price <= 0:
|
||||
raise ValueError(f"Execution snapshot price '{name}' is invalid: {price}")
|
||||
raise ValueError(
|
||||
f"Execution snapshot price '{name}' is invalid: {price}"
|
||||
)
|
||||
|
||||
return price
|
||||
|
||||
def _round_size(self, size: float) -> float:
|
||||
factor = 10 ** self._size_precision
|
||||
return math.floor(float(size) * factor) / factor
|
||||
def _round_size(self, size: NumericLike | None) -> float:
|
||||
value = safe_float(size)
|
||||
|
||||
def _calculate_pnl(self, current_price: float) -> float:
|
||||
if value is None:
|
||||
return 0.0
|
||||
|
||||
factor = 10 ** self._size_precision
|
||||
return math.floor(value * factor) / factor
|
||||
|
||||
def _calculate_pnl(
|
||||
self,
|
||||
current_price: NumericLike | None,
|
||||
) -> float:
|
||||
position = type(self)._position
|
||||
|
||||
entry = position.entry_price or 0.0
|
||||
size = position.size or 0.0
|
||||
price = safe_float(current_price) or 0.0
|
||||
|
||||
entry = safe_float(position.entry_price) or 0.0
|
||||
size = safe_float(position.size) or 0.0
|
||||
|
||||
if position.side == "LONG":
|
||||
return round((current_price - entry) * size, 4)
|
||||
return round((price - entry) * size, 4)
|
||||
|
||||
if position.side == "SHORT":
|
||||
return round((entry - current_price) * size, 4)
|
||||
return round((entry - price) * size, 4)
|
||||
|
||||
return 0.0
|
||||
|
||||
@@ -1048,9 +1098,5 @@ class ExecutionEngine:
|
||||
state.position_size = position.size
|
||||
state.unrealized_pnl_usd = position.unrealized_pnl_usd
|
||||
|
||||
def _round_order_size(self, value: float) -> float:
|
||||
factor = 10 ** self._size_precision
|
||||
return math.floor(float(value) * factor) / factor
|
||||
|
||||
def _now_time(self) -> str:
|
||||
return datetime.now().strftime("%H:%M:%S")
|
||||
Reference in New Issue
Block a user