07.4.4.1.11 — Advanced Trend Quality & EMA Distance Layer
This commit is contained in:
@@ -3,11 +3,14 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import math
|
||||
import time
|
||||
|
||||
from aiogram import F, Router
|
||||
from aiogram.types import Message
|
||||
|
||||
from src.core.config import load_settings
|
||||
from src.core.numbers import safe_float
|
||||
from src.core.types import JsonList, NumericLike
|
||||
from src.trading.debug.execution import DebugExecutionEngine
|
||||
from src.trading.debug.service import DebugTradeService
|
||||
from src.trading.debug.state import DebugTradeState
|
||||
@@ -18,7 +21,7 @@ router = Router(name="debug")
|
||||
|
||||
|
||||
def _debug_enabled() -> bool:
|
||||
return load_settings().debug_enabled
|
||||
return bool(load_settings().debug_enabled)
|
||||
|
||||
|
||||
def _debug_help_text() -> str:
|
||||
@@ -80,6 +83,7 @@ async def debug_auto(message: Message) -> None:
|
||||
|
||||
if command == "reset":
|
||||
state = service.reset()
|
||||
|
||||
await message.answer(
|
||||
"✅ [DEBUG] Runtime reset\n\n"
|
||||
f"{_debug_state_text(state)}"
|
||||
@@ -88,6 +92,7 @@ async def debug_auto(message: Message) -> None:
|
||||
|
||||
if command == "off":
|
||||
state = service.stop()
|
||||
|
||||
await message.answer(
|
||||
"✅ [DEBUG] Runtime stopped\n\n"
|
||||
f"{_debug_state_text(state)}"
|
||||
@@ -97,17 +102,20 @@ async def debug_auto(message: Message) -> None:
|
||||
if command == "state":
|
||||
state = service.get_state()
|
||||
service.update_market()
|
||||
|
||||
await message.answer(_debug_state_text(state))
|
||||
return
|
||||
|
||||
if command == "hold":
|
||||
seconds = _parse_int(parts, index=2, default=335)
|
||||
|
||||
state = service.set_signal_duration(
|
||||
signal="HOLD",
|
||||
seconds=seconds,
|
||||
confidence=0.0,
|
||||
force_ready=False,
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
f"✅ [DEBUG] HOLD {seconds}s\n\n"
|
||||
f"{_debug_state_text(state)}"
|
||||
@@ -182,15 +190,31 @@ async def debug_auto(message: Message) -> None:
|
||||
|
||||
if command == "long":
|
||||
state, result = service.open_long()
|
||||
await message.answer(_execution_result_text("OPEN LONG", state, result))
|
||||
|
||||
await message.answer(
|
||||
_execution_result_text(
|
||||
"OPEN LONG",
|
||||
state,
|
||||
result,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if command == "short":
|
||||
state, result = service.open_short()
|
||||
await message.answer(_execution_result_text("OPEN SHORT", state, result))
|
||||
|
||||
await message.answer(
|
||||
_execution_result_text(
|
||||
"OPEN SHORT",
|
||||
state,
|
||||
result,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
await message.answer(f"⛔️ Неизвестная команда: {command}\n\n{_debug_help_text()}")
|
||||
await message.answer(
|
||||
f"⛔️ Неизвестная команда: {command}\n\n{_debug_help_text()}"
|
||||
)
|
||||
|
||||
|
||||
@router.message(F.text.startswith("/debug_exec"))
|
||||
@@ -211,43 +235,82 @@ async def debug_exec(message: Message) -> None:
|
||||
if command == "state":
|
||||
state = service.get_state()
|
||||
service.update_market()
|
||||
|
||||
await message.answer(_debug_state_text(state))
|
||||
return
|
||||
|
||||
if command == "buy":
|
||||
state, result = service.open_long()
|
||||
await message.answer(_execution_result_text("EXEC BUY / LONG", state, result))
|
||||
|
||||
await message.answer(
|
||||
_execution_result_text(
|
||||
"EXEC BUY / LONG",
|
||||
state,
|
||||
result,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if command == "sell":
|
||||
state, result = service.open_short()
|
||||
await message.answer(_execution_result_text("EXEC SELL / SHORT", state, result))
|
||||
|
||||
await message.answer(
|
||||
_execution_result_text(
|
||||
"EXEC SELL / SHORT",
|
||||
state,
|
||||
result,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if command == "flip":
|
||||
state, result = service.flip()
|
||||
await message.answer(_execution_result_text("EXEC AUTO FLIP", state, result))
|
||||
|
||||
await message.answer(
|
||||
_execution_result_text(
|
||||
"EXEC AUTO FLIP",
|
||||
state,
|
||||
result,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if command == "close":
|
||||
state, result = service.close(reason="DEBUG_CLOSE")
|
||||
await message.answer(_execution_result_text("EXEC CLOSE", state, result))
|
||||
|
||||
await message.answer(
|
||||
_execution_result_text(
|
||||
"EXEC CLOSE",
|
||||
state,
|
||||
result,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if command == "process":
|
||||
state, result = service.process()
|
||||
await message.answer(_execution_result_text("EXEC PROCESS", state, result))
|
||||
|
||||
await message.answer(
|
||||
_execution_result_text(
|
||||
"EXEC PROCESS",
|
||||
state,
|
||||
result,
|
||||
)
|
||||
)
|
||||
return
|
||||
|
||||
if command == "update":
|
||||
state = service.update_market()
|
||||
|
||||
await message.answer(
|
||||
"✅ [DEBUG] Market update\n\n"
|
||||
f"{_debug_state_text(state)}"
|
||||
)
|
||||
return
|
||||
|
||||
await message.answer(f"⛔️ Неизвестная команда: {command}\n\n{_debug_help_text()}")
|
||||
await message.answer(
|
||||
f"⛔️ Неизвестная команда: {command}\n\n{_debug_help_text()}"
|
||||
)
|
||||
|
||||
|
||||
@router.message(F.text.startswith("/debug_live"))
|
||||
@@ -264,8 +327,9 @@ async def debug_live(message: Message) -> None:
|
||||
"/debug_exec sell\n"
|
||||
"/debug_exec flip\n"
|
||||
"/debug_exec close\n\n"
|
||||
"Live-мониторинг для изолированного debug будет добавлен в следующем пакете "
|
||||
"через отдельный DebugTradeRunner и отдельный Debug Auto экран."
|
||||
"Live-мониторинг для изолированного debug будет добавлен "
|
||||
"в следующем пакете через отдельный DebugTradeRunner "
|
||||
"и отдельный Debug Auto экран."
|
||||
)
|
||||
|
||||
|
||||
@@ -275,19 +339,30 @@ async def debug_signal(message: Message) -> None:
|
||||
await message.answer("Debug mode выключен.")
|
||||
return
|
||||
|
||||
signal, confidence, repeat_count, error = _parse_debug_signal_args(message.text)
|
||||
signal, confidence, repeat_count, error = _parse_debug_signal_args(
|
||||
message.text,
|
||||
)
|
||||
|
||||
if error is not None:
|
||||
await message.answer(f"⛔️ {error}\n\n{_debug_help_text()}")
|
||||
await message.answer(
|
||||
f"⛔️ {error}\n\n{_debug_help_text()}"
|
||||
)
|
||||
return
|
||||
|
||||
service = DebugTradeService()
|
||||
|
||||
state = service.set_signal(
|
||||
signal=signal,
|
||||
confidence=confidence,
|
||||
repeat_count=repeat_count,
|
||||
reason=f"[DEBUG] LEGACY FORCE {signal} {confidence:.2f} ×{repeat_count}",
|
||||
force_ready=signal in {"BUY", "SELL"} and repeat_count >= 2,
|
||||
reason=(
|
||||
f"[DEBUG] LEGACY FORCE "
|
||||
f"{signal} {confidence:.2f} ×{repeat_count}"
|
||||
),
|
||||
force_ready=(
|
||||
signal in {"BUY", "SELL"}
|
||||
and repeat_count >= 2
|
||||
),
|
||||
)
|
||||
|
||||
await message.answer(
|
||||
@@ -303,6 +378,7 @@ async def debug_ready(message: Message) -> None:
|
||||
return
|
||||
|
||||
service = DebugTradeService()
|
||||
|
||||
state = service.set_signal_duration(
|
||||
signal="BUY",
|
||||
seconds=15,
|
||||
@@ -323,13 +399,16 @@ async def debug_state(message: Message) -> None:
|
||||
return
|
||||
|
||||
service = DebugTradeService()
|
||||
|
||||
state = service.get_state()
|
||||
service.update_market()
|
||||
|
||||
await message.answer(_debug_state_text(state))
|
||||
|
||||
|
||||
def _debug_state_text(state: DebugTradeState) -> str:
|
||||
def _debug_state_text(
|
||||
state: DebugTradeState,
|
||||
) -> str:
|
||||
position = state.position
|
||||
|
||||
duration = _signal_duration_text(state)
|
||||
@@ -348,7 +427,7 @@ def _debug_state_text(state: DebugTradeState) -> str:
|
||||
f"Signal: {_signal_icon(state.last_signal)} {state.last_signal}\n"
|
||||
f"Duration: {duration}\n"
|
||||
f"Repeats: {state.last_signal_repeat_count}\n"
|
||||
f"Confidence: {state.last_signal_confidence:.2f}\n"
|
||||
f"Confidence: {safe_float(state.last_signal_confidence) or 0.0:.2f}\n"
|
||||
f"Decision: {state.decision_status}\n"
|
||||
f"Ready: {state.is_signal_ready}\n"
|
||||
f"Reason: {state.last_signal_reason or '—'}\n\n"
|
||||
@@ -385,53 +464,95 @@ def _execution_result_text(
|
||||
)
|
||||
|
||||
|
||||
def _parse_debug_signal_args(raw_text: str | None) -> tuple[str, float, int, str | None]:
|
||||
def _parse_debug_signal_args(
|
||||
raw_text: str | None,
|
||||
) -> tuple[str, float, int, str | None]:
|
||||
parts = (raw_text or "").split()
|
||||
|
||||
signal = parts[1].upper() if len(parts) > 1 else "BUY"
|
||||
if signal not in {"BUY", "SELL", "HOLD"}:
|
||||
return "BUY", 0.9, 2, "SIGNAL должен быть BUY, SELL или HOLD."
|
||||
|
||||
try:
|
||||
confidence = float(parts[2]) if len(parts) > 2 else 0.9
|
||||
except ValueError:
|
||||
return "BUY", 0.9, 2, "CONFIDENCE должен быть числом от 0.00 до 1.00."
|
||||
if signal not in {"BUY", "SELL", "HOLD"}:
|
||||
return (
|
||||
"BUY",
|
||||
0.9,
|
||||
2,
|
||||
"SIGNAL должен быть BUY, SELL или HOLD.",
|
||||
)
|
||||
|
||||
confidence = _parse_float(parts, index=2, default=0.9)
|
||||
|
||||
if confidence < 0 or confidence > 1:
|
||||
return "BUY", 0.9, 2, "CONFIDENCE должен быть от 0.00 до 1.00."
|
||||
return (
|
||||
"BUY",
|
||||
0.9,
|
||||
2,
|
||||
"CONFIDENCE должен быть от 0.00 до 1.00.",
|
||||
)
|
||||
|
||||
try:
|
||||
repeat_count = int(parts[3]) if len(parts) > 3 else 2
|
||||
except ValueError:
|
||||
return "BUY", 0.9, 2, "REPEATS должен быть целым числом."
|
||||
repeat_count = _parse_int(parts, index=3, default=2)
|
||||
|
||||
if repeat_count < 1:
|
||||
return "BUY", 0.9, 2, "REPEATS должен быть больше или равен 1."
|
||||
return (
|
||||
"BUY",
|
||||
0.9,
|
||||
2,
|
||||
"REPEATS должен быть больше или равен 1.",
|
||||
)
|
||||
|
||||
return signal, confidence, repeat_count, None
|
||||
|
||||
|
||||
def _parse_int(parts: list[str], *, index: int, default: int) -> int:
|
||||
def _parse_int(
|
||||
parts: JsonList,
|
||||
*,
|
||||
index: int,
|
||||
default: int,
|
||||
) -> int:
|
||||
try:
|
||||
return int(parts[index])
|
||||
except (IndexError, TypeError, ValueError):
|
||||
value = parts[index]
|
||||
except (IndexError, TypeError):
|
||||
return default
|
||||
|
||||
number = safe_float(value)
|
||||
|
||||
def _parse_float(parts: list[str], *, index: int, default: float) -> float:
|
||||
try:
|
||||
return float(parts[index])
|
||||
except (IndexError, TypeError, ValueError):
|
||||
if number is None:
|
||||
return default
|
||||
|
||||
return int(number)
|
||||
|
||||
def _signal_duration_text(state: DebugTradeState) -> str:
|
||||
started_at = state.signal_started_at
|
||||
|
||||
def _parse_float(
|
||||
parts: JsonList,
|
||||
*,
|
||||
index: int,
|
||||
default: float,
|
||||
) -> float:
|
||||
try:
|
||||
value = parts[index]
|
||||
except (IndexError, TypeError):
|
||||
return default
|
||||
|
||||
number = safe_float(value)
|
||||
|
||||
if number is None:
|
||||
return default
|
||||
|
||||
return number
|
||||
|
||||
|
||||
def _signal_duration_text(
|
||||
state: DebugTradeState,
|
||||
) -> str:
|
||||
started_at = safe_float(state.signal_started_at)
|
||||
|
||||
if started_at is not None:
|
||||
total_seconds = max(0, int(__import__("time").monotonic() - float(started_at)))
|
||||
total_seconds = max(
|
||||
0,
|
||||
int(time.monotonic() - started_at),
|
||||
)
|
||||
else:
|
||||
total_seconds = max(0, (state.last_signal_repeat_count or 0) * 5)
|
||||
repeats = state.last_signal_repeat_count or 0
|
||||
total_seconds = max(0, int(repeats) * 5)
|
||||
|
||||
hours = total_seconds // 3600
|
||||
minutes = (total_seconds % 3600) // 60
|
||||
@@ -446,73 +567,98 @@ def _signal_duration_text(state: DebugTradeState) -> str:
|
||||
return f"{seconds}с"
|
||||
|
||||
|
||||
def _signal_icon(signal: str | None) -> str:
|
||||
def _signal_icon(
|
||||
signal: str | None,
|
||||
) -> str:
|
||||
mapping = {
|
||||
"BUY": "🟢",
|
||||
"SELL": "🔴",
|
||||
"HOLD": "🟡",
|
||||
}
|
||||
|
||||
return mapping.get(signal or "", "⚪")
|
||||
|
||||
|
||||
def _format_leverage(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
def _format_leverage(
|
||||
value: NumericLike | None,
|
||||
) -> str:
|
||||
number = safe_float(value)
|
||||
|
||||
if number is None:
|
||||
return "x—"
|
||||
|
||||
return f"x{float(value):g}"
|
||||
return f"x{number:g}"
|
||||
|
||||
|
||||
def _format_crypto_size(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
def _format_crypto_size(
|
||||
value: NumericLike | None,
|
||||
) -> str:
|
||||
number = safe_float(value)
|
||||
|
||||
if number is None:
|
||||
return "—"
|
||||
|
||||
number = float(value)
|
||||
return f"{number:.5f}".rstrip("0").rstrip(".")
|
||||
|
||||
|
||||
def _format_percent(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
def _format_percent(
|
||||
value: NumericLike | None,
|
||||
) -> str:
|
||||
number = safe_float(value)
|
||||
|
||||
if number is None:
|
||||
return "off"
|
||||
|
||||
number = float(value)
|
||||
|
||||
if abs(number - round(number)) < 1e-9:
|
||||
if math.isclose(number, round(number), abs_tol=1e-9):
|
||||
return f"{int(round(number))}%"
|
||||
|
||||
return f"{number:.2f}".rstrip("0").rstrip(".") + "%"
|
||||
|
||||
|
||||
def _format_money_compact(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
def _format_money_compact(
|
||||
value: NumericLike | None,
|
||||
) -> str:
|
||||
number = safe_float(value)
|
||||
|
||||
if number is None:
|
||||
return "—"
|
||||
|
||||
number = float(value)
|
||||
|
||||
if abs(number - round(number)) < 1e-9:
|
||||
if math.isclose(number, round(number), abs_tol=1e-9):
|
||||
return f"{number:,.0f}".replace(",", " ")
|
||||
|
||||
return f"{number:,.2f}".replace(",", " ").rstrip("0").rstrip(".")
|
||||
return (
|
||||
f"{number:,.2f}"
|
||||
.replace(",", " ")
|
||||
.rstrip("0")
|
||||
.rstrip(".")
|
||||
)
|
||||
|
||||
|
||||
def _format_usd_or_dash(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
def _format_usd_or_dash(
|
||||
value: NumericLike | None,
|
||||
) -> str:
|
||||
if safe_float(value) is None:
|
||||
return "—"
|
||||
|
||||
return f"$ {_format_money_compact(value)}"
|
||||
|
||||
|
||||
def _format_usd_or_off(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
def _format_usd_or_off(
|
||||
value: NumericLike | None,
|
||||
) -> str:
|
||||
if safe_float(value) is None:
|
||||
return "off"
|
||||
|
||||
return f"$ {_format_money_compact(value)}"
|
||||
|
||||
|
||||
def _format_signed_usd(value: float | int | None) -> str:
|
||||
if value is None:
|
||||
return "—"
|
||||
def _format_signed_usd(
|
||||
value: NumericLike | None,
|
||||
) -> str:
|
||||
amount = safe_float(value)
|
||||
|
||||
amount = float(value)
|
||||
if amount is None:
|
||||
return "—"
|
||||
|
||||
if amount > 0:
|
||||
return f"🟢 +$ {_format_money_compact(amount)}"
|
||||
|
||||
Reference in New Issue
Block a user