Stage 07.4.3 — trend strategy, signal aggregation and journal UX improvements

This commit is contained in:
2026-05-01 11:43:26 +03:00
parent 80f29443d4
commit ec8e53c416
8 changed files with 510 additions and 20 deletions

View File

@@ -7,6 +7,7 @@ from datetime import datetime
from src.core.config import load_settings
from src.trading.auto.state import AutoTradeState
from src.trading.journal.service import JournalService
from src.trading.strategies.base import BaseStrategy, StrategyContext
from src.trading.strategies.registry import StrategyRegistry
@@ -15,6 +16,9 @@ class AutoTradeService:
_state = AutoTradeState()
_loop_task: asyncio.Task | None = None
_loop_interval_seconds = 5
_last_signal_key: str | None = None
_last_signal_value: str | None = None
_same_signal_count = 0
# получить текущее состояние автоторговли
def get_state(self) -> AutoTradeState:
@@ -108,6 +112,9 @@ class AutoTradeService:
def set_strategy(self, strategy: str) -> AutoTradeState:
state = self.get_state()
state.strategy = strategy.strip().upper()
self._last_signal_key = None
self._last_signal_value = None
self._same_signal_count = 0
return state
# установить риск
@@ -131,6 +138,135 @@ class AutoTradeService:
state = self.get_state()
return StrategyRegistry.get(state.strategy)
# записать новый сигнал и итог предыдущей серии при смене сигнала
def _log_signal_if_changed(
self,
*,
strategy_name: str,
state: AutoTradeState,
signal: str,
reason: str,
confidence: float,
payload: dict | None,
) -> None:
signal_key = f"{state.status}:{state.symbol}:{strategy_name}:{signal}:{reason}"
previous_signal = self._last_signal_value
previous_count = self._same_signal_count
is_same_signal = signal_key == self._last_signal_key
if is_same_signal:
self._same_signal_count += 1
return
if previous_signal is not None:
if previous_count > 1:
self._log_signal_summary(
strategy_name=strategy_name,
state=state,
previous_signal=previous_signal,
previous_count=previous_count,
next_signal=signal,
)
else:
self._log_signal_event(
strategy_name=strategy_name,
state=state,
signal=previous_signal,
reason=f"{previous_signal} завершился без серии.",
confidence=0.0,
payload={
"previous_signal": previous_signal,
"next_signal": signal,
},
)
self._last_signal_key = signal_key
self._last_signal_value = signal
self._same_signal_count = 1
# Новый сигнал не пишем сразу.
# Он попадёт в журнал при следующей смене сигнала:
# либо как одиночный сигнал, либо как серия.
# записать сам сигнал в журнал
def _log_signal_event(
self,
*,
strategy_name: str,
state: AutoTradeState,
signal: str,
reason: str,
confidence: float,
payload: dict | None,
) -> None:
emoji_map = {
"BUY": "🟢",
"SELL": "🔴",
"HOLD": "🟡",
}
emoji = emoji_map.get(signal, "")
try:
JournalService().log_ui_info(
event_type="auto_signal_generated",
message=f"{emoji} Сигнал автоторговли {signal}: {reason}",
screen="auto",
action="run_cycle",
payload={
"strategy": strategy_name,
"status": state.status,
"symbol": state.symbol,
"signal": signal,
"confidence": confidence,
"reason": reason,
"repeat_count": 1,
"is_strong_signal": confidence > 0.7,
"is_aggregated": False,
"payload": payload or {},
},
)
except Exception:
pass
# записать итог серии одинаковых сигналов при смене сигнала
def _log_signal_summary(
self,
*,
strategy_name: str,
state: AutoTradeState,
previous_signal: str,
previous_count: int,
next_signal: str,
) -> None:
emoji_map = {
"BUY": "🟢",
"SELL": "🔴",
"HOLD": "🟡",
}
emoji = emoji_map.get(previous_signal, "")
try:
JournalService().log_ui_info(
event_type="auto_signal_summary",
message=(
f"{emoji} {previous_count} {previous_signal} подряд "
f"до смены на {next_signal}"
),
screen="auto",
action="signal_summary",
payload={
"strategy": strategy_name,
"status": state.status,
"symbol": state.symbol,
"signal": previous_signal,
"next_signal": next_signal,
"repeat_count": previous_count,
"is_aggregated": True,
},
)
except Exception:
pass
# выполнить один цикл анализа рынка
def run_cycle(self) -> AutoTradeState:
state = self.get_state()
@@ -145,4 +281,17 @@ class AutoTradeService:
state.last_check_at = datetime.now().strftime("%H:%M:%S")
state.last_signal = result.signal.value
self._log_signal_if_changed(
strategy_name=strategy.name,
state=state,
signal=result.signal.value,
reason=result.reason,
confidence=result.confidence,
payload=result.payload,
)
state.last_signal_repeat_count = self._same_signal_count
state.last_signal_confidence = result.confidence
state.last_signal_reason = result.reason
return state