792 lines
27 KiB
Python
792 lines
27 KiB
Python
# app/src/trading/auto/service.py
|
||
|
||
from __future__ import annotations
|
||
|
||
import asyncio
|
||
import time
|
||
from datetime import datetime
|
||
|
||
from src.core.config import load_settings
|
||
from src.core.event_bus import EventBus
|
||
from src.trading.auto.state import AutoTradeState
|
||
from src.trading.execution.engine import ExecutionEngine
|
||
from src.trading.journal.service import JournalService
|
||
from src.trading.strategies.base import BaseStrategy, StrategyContext
|
||
from src.trading.strategies.registry import StrategyRegistry
|
||
|
||
|
||
class AutoTradeService:
|
||
_state = AutoTradeState()
|
||
_loop_task: asyncio.Task | None = None
|
||
_loop_interval_seconds = 5
|
||
|
||
# минимальное количество повторов BUY / SELL для подтверждения сигнала
|
||
_confirm_repeats = 2
|
||
|
||
# минимальная уверенность для готовности к будущему execution
|
||
_ready_confidence = 0.3
|
||
|
||
_last_signal_key: str | None = None
|
||
_last_signal_value: str | None = None
|
||
_last_signal_reason: str = ""
|
||
_last_signal_confidence: float = 0.0
|
||
_last_signal_payload: dict | None = None
|
||
_last_signal_started_at: float | None = None
|
||
_last_logged_market_state: str | None = None
|
||
_last_logged_market_trend: str | None = None
|
||
_last_logged_market_volatility: str | None = None
|
||
_same_signal_count = 0
|
||
|
||
# debug: принудительно выставить сигнал и decision
|
||
def debug_force_signal(
|
||
self,
|
||
*,
|
||
signal: str,
|
||
confidence: float = 0.9,
|
||
repeat_count: int = 2,
|
||
reason: str = "DEBUG SIGNAL",
|
||
) -> AutoTradeState:
|
||
state = self.get_state()
|
||
|
||
normalized_signal = signal.strip().upper()
|
||
if normalized_signal not in {"BUY", "SELL", "HOLD"}:
|
||
normalized_signal = "HOLD"
|
||
|
||
previous_signal = state.last_signal
|
||
previous_decision_status = state.decision_status
|
||
|
||
if previous_signal != normalized_signal or state.signal_started_at is None:
|
||
state.signal_started_at = time.monotonic()
|
||
|
||
state.last_signal = normalized_signal
|
||
state.last_signal_repeat_count = repeat_count
|
||
state.last_signal_confidence = confidence
|
||
state.last_signal_reason = reason
|
||
|
||
if normalized_signal == "HOLD":
|
||
state.decision_status = "WAITING"
|
||
state.decision_reason = "Debug HOLD."
|
||
state.is_signal_confirmed = False
|
||
state.is_signal_ready = False
|
||
else:
|
||
state.decision_status = "READY"
|
||
state.decision_reason = "Debug READY signal."
|
||
state.is_signal_confirmed = True
|
||
state.is_signal_ready = True
|
||
|
||
signal_intent = self._signal_intent(
|
||
state=state,
|
||
signal=state.last_signal,
|
||
)
|
||
|
||
EventBus.emit(
|
||
"auto_decision_changed",
|
||
{
|
||
"previous_signal": previous_signal,
|
||
"previous_decision_status": previous_decision_status,
|
||
"decision_status": state.decision_status,
|
||
"signal": state.last_signal,
|
||
"signal_intent": signal_intent,
|
||
"repeat_count": state.last_signal_repeat_count,
|
||
"confidence": state.last_signal_confidence,
|
||
"symbol": state.symbol,
|
||
"strategy": state.strategy,
|
||
"leverage": state.leverage,
|
||
"reason": state.last_signal_reason,
|
||
"debug": True,
|
||
},
|
||
)
|
||
|
||
return state
|
||
|
||
# установить капитал, выделенный под автоторговлю
|
||
def set_allocated_balance_usd(self, value: float) -> AutoTradeState:
|
||
state = self.get_state()
|
||
|
||
if value <= 0:
|
||
value = 1000.0
|
||
|
||
state.allocated_balance_usd = value
|
||
state.execution_block_reason = None
|
||
state.execution_size_adjustment_reason = None
|
||
return state
|
||
|
||
# получить текущее состояние автоторговли
|
||
def get_state(self) -> AutoTradeState:
|
||
if not self._state.symbol:
|
||
self._state.symbol = load_settings().default_symbol
|
||
return self._state
|
||
|
||
# проверить, запущен ли background loop
|
||
def is_loop_running(self) -> bool:
|
||
return self._loop_task is not None and not self._loop_task.done()
|
||
|
||
# запустить background loop, если он ещё не запущен
|
||
def start_loop(self) -> None:
|
||
if self.is_loop_running():
|
||
return
|
||
|
||
self._loop_task = asyncio.create_task(self._loop_worker())
|
||
|
||
# остановить background loop
|
||
def stop_loop(self) -> None:
|
||
if self._loop_task is None:
|
||
return
|
||
|
||
self._loop_task.cancel()
|
||
self._loop_task = None
|
||
|
||
# рабочий цикл автоторговли
|
||
async def _loop_worker(self) -> None:
|
||
while True:
|
||
state = self.get_state()
|
||
|
||
if state.status == "OFF":
|
||
break
|
||
|
||
self.run_cycle()
|
||
await asyncio.sleep(self._loop_interval_seconds)
|
||
|
||
# запустить активную торговлю
|
||
def start(self) -> tuple[AutoTradeState, str]:
|
||
state = self.get_state()
|
||
previous_status = state.status
|
||
|
||
if state.status == "RUNNING":
|
||
return state, "Автоторговля уже активна."
|
||
|
||
if state.status == "OBSERVING":
|
||
state.status = "RUNNING"
|
||
EventBus.emit(
|
||
"auto_status_changed",
|
||
{
|
||
"previous_status": previous_status,
|
||
"status": state.status,
|
||
},
|
||
)
|
||
return state, "Автоторговля активирована."
|
||
|
||
state.status = "RUNNING"
|
||
self._reset_signal_tracking()
|
||
state.last_signal = "HOLD"
|
||
state.signal_started_at = time.monotonic()
|
||
|
||
EventBus.emit(
|
||
"auto_status_changed",
|
||
{
|
||
"previous_status": previous_status,
|
||
"status": state.status,
|
||
},
|
||
)
|
||
return state, "Автоторговля запущена."
|
||
|
||
# включить режим наблюдения
|
||
def observe(self) -> tuple[AutoTradeState, str]:
|
||
state = self.get_state()
|
||
previous_status = state.status
|
||
|
||
if previous_status == "OBSERVING":
|
||
return state, "Режим наблюдения уже включён."
|
||
|
||
state.status = "OBSERVING"
|
||
|
||
EventBus.emit(
|
||
"auto_status_changed",
|
||
{
|
||
"previous_status": previous_status,
|
||
"status": state.status,
|
||
},
|
||
)
|
||
|
||
if previous_status == "OFF":
|
||
return state, "Включён режим наблюдения."
|
||
|
||
return state, "Автоторговля переведена в режим наблюдения."
|
||
|
||
# полностью выключить автоторговлю
|
||
def stop(self) -> tuple[AutoTradeState, str]:
|
||
state = self.get_state()
|
||
previous_status = state.status
|
||
|
||
if state.status == "OFF":
|
||
self.stop_loop()
|
||
return state, "Автоторговля уже выключена."
|
||
|
||
state.status = "OFF"
|
||
self.stop_loop()
|
||
|
||
EventBus.emit(
|
||
"auto_status_changed",
|
||
{
|
||
"previous_status": previous_status,
|
||
"status": state.status,
|
||
},
|
||
)
|
||
|
||
return state, "Автоторговля выключена."
|
||
|
||
# установить инструмент
|
||
def set_symbol(self, symbol: str) -> AutoTradeState:
|
||
state = self.get_state()
|
||
state.symbol = symbol
|
||
self._reset_signal_tracking()
|
||
return state
|
||
|
||
# установить стратегию
|
||
def set_strategy(self, strategy: str) -> AutoTradeState:
|
||
state = self.get_state()
|
||
state.strategy = strategy.strip().upper()
|
||
self._reset_signal_tracking()
|
||
return state
|
||
|
||
# установить риск
|
||
def set_risk_percent(self, risk_percent: float) -> AutoTradeState:
|
||
state = self.get_state()
|
||
state.risk_percent = risk_percent
|
||
return state
|
||
|
||
# установить плечо
|
||
def set_leverage(self, leverage: float) -> AutoTradeState:
|
||
state = self.get_state()
|
||
state.leverage = leverage
|
||
return state
|
||
|
||
# установить stop loss в %
|
||
def set_stop_loss_percent(self, value: float | None) -> AutoTradeState:
|
||
state = self.get_state()
|
||
state.stop_loss_percent = value
|
||
return state
|
||
|
||
# установить take profit в %
|
||
def set_take_profit_percent(self, value: float | None) -> AutoTradeState:
|
||
state = self.get_state()
|
||
state.take_profit_percent = value
|
||
return state
|
||
|
||
# установить max loss в USD
|
||
def set_max_loss_usd(self, value: float | None) -> AutoTradeState:
|
||
state = self.get_state()
|
||
state.max_loss_usd = value
|
||
return state
|
||
|
||
# установить максимальное использование баланса под маржу
|
||
def set_max_reserved_balance_percent(self, value: float | None) -> AutoTradeState:
|
||
state = self.get_state()
|
||
state.max_reserved_balance_percent = value
|
||
state.execution_block_reason = None
|
||
return state
|
||
|
||
# сбросить внутренний трекинг сигналов
|
||
def _reset_signal_tracking(self) -> None:
|
||
self._last_signal_key = None
|
||
self._last_signal_value = None
|
||
self._last_signal_reason = ""
|
||
self._last_signal_confidence = 0.0
|
||
self._last_signal_payload = None
|
||
self._last_signal_started_at = None
|
||
self._same_signal_count = 0
|
||
|
||
state = self.get_state()
|
||
state.last_signal_repeat_count = 0
|
||
state.last_signal_confidence = 0.0
|
||
state.last_signal_reason = None
|
||
state.decision_status = "WAITING"
|
||
state.decision_reason = None
|
||
state.is_signal_confirmed = False
|
||
state.is_signal_ready = False
|
||
state.execution_block_reason = None
|
||
state.signal_started_at = None
|
||
|
||
# собрать контекст для стратегии
|
||
def _build_strategy_context(self) -> StrategyContext:
|
||
state = self.get_state()
|
||
|
||
return StrategyContext(
|
||
symbol=state.symbol,
|
||
status=state.status,
|
||
risk_percent=state.risk_percent,
|
||
)
|
||
|
||
# получить стратегию для текущего цикла
|
||
def _get_strategy(self) -> BaseStrategy:
|
||
state = self.get_state()
|
||
return StrategyRegistry.get(state.strategy)
|
||
|
||
# определить смысл сигнала с учетом открытой позиции
|
||
def _signal_intent(self, *, state: AutoTradeState, signal: str | None) -> str:
|
||
normalized_signal = (signal or "HOLD").upper()
|
||
position_side = str(getattr(state, "position_side", "NONE") or "NONE").upper()
|
||
|
||
if normalized_signal == "HOLD":
|
||
return "HOLD_MARKET"
|
||
|
||
if normalized_signal not in {"BUY", "SELL"}:
|
||
return "NOISE"
|
||
|
||
if position_side == "NONE":
|
||
return "ENTRY_CANDIDATE"
|
||
|
||
if position_side == "LONG" and normalized_signal == "BUY":
|
||
return "REINFORCE_POSITION"
|
||
|
||
if position_side == "SHORT" and normalized_signal == "SELL":
|
||
return "REINFORCE_POSITION"
|
||
|
||
if position_side == "LONG" and normalized_signal == "SELL":
|
||
return "REVERSAL_CANDIDATE"
|
||
|
||
if position_side == "SHORT" and normalized_signal == "BUY":
|
||
return "REVERSAL_CANDIDATE"
|
||
|
||
return "NOISE"
|
||
|
||
# обновить статус решения по текущему сигналу
|
||
def _update_decision_state(
|
||
self,
|
||
*,
|
||
state: AutoTradeState,
|
||
signal: str,
|
||
confidence: float,
|
||
) -> None:
|
||
state.is_signal_confirmed = False
|
||
state.is_signal_ready = False
|
||
|
||
if signal == "HOLD":
|
||
state.decision_status = "WAITING"
|
||
state.decision_reason = "Нет торгового направления."
|
||
return
|
||
|
||
if self._same_signal_count < self._confirm_repeats:
|
||
state.decision_status = "CONFIRMING"
|
||
state.decision_reason = (
|
||
f"Сигнал {signal} подтверждается: "
|
||
f"{self._same_signal_count}/{self._confirm_repeats} повторов."
|
||
)
|
||
return
|
||
|
||
state.is_signal_confirmed = True
|
||
|
||
if confidence < self._ready_confidence:
|
||
state.decision_status = "BLOCKED"
|
||
state.decision_reason = (
|
||
f"Сигнал {signal} подтверждён, но уверенность низкая: "
|
||
f"{confidence:.2f} < {self._ready_confidence:.2f}."
|
||
)
|
||
return
|
||
|
||
state.is_signal_ready = True
|
||
state.decision_status = "READY"
|
||
state.decision_reason = (
|
||
f"Сигнал {signal} подтверждён и готов к будущему execution."
|
||
)
|
||
|
||
# записать новый сигнал и итог предыдущей серии при смене сигнала
|
||
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}"
|
||
previous_signal = self._last_signal_value
|
||
previous_count = self._same_signal_count
|
||
is_same_signal = signal_key == self._last_signal_key
|
||
now = time.monotonic()
|
||
|
||
if is_same_signal:
|
||
self._same_signal_count += 1
|
||
self._last_signal_reason = reason
|
||
self._last_signal_confidence = confidence
|
||
self._last_signal_payload = payload
|
||
|
||
self._update_signal_state_fields(
|
||
state=state,
|
||
signal=signal,
|
||
reason=reason,
|
||
confidence=confidence,
|
||
)
|
||
return
|
||
|
||
if previous_signal is not None and previous_signal != signal:
|
||
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,
|
||
reason=self._last_signal_reason,
|
||
confidence=self._last_signal_confidence,
|
||
payload=self._last_signal_payload,
|
||
duration_seconds=self._signal_duration_seconds(now=now),
|
||
)
|
||
else:
|
||
self._log_signal_event(
|
||
strategy_name=strategy_name,
|
||
state=state,
|
||
signal=previous_signal,
|
||
reason=f"{previous_signal} завершился без серии.",
|
||
confidence=self._last_signal_confidence,
|
||
payload={
|
||
"previous_signal": previous_signal,
|
||
"next_signal": signal,
|
||
},
|
||
)
|
||
|
||
self._last_signal_key = signal_key
|
||
self._last_signal_value = signal
|
||
self._last_signal_reason = reason
|
||
self._last_signal_confidence = confidence
|
||
self._last_signal_payload = payload
|
||
self._last_signal_started_at = now
|
||
self._same_signal_count = 1
|
||
|
||
self._update_signal_state_fields(
|
||
state=state,
|
||
signal=signal,
|
||
reason=reason,
|
||
confidence=confidence,
|
||
)
|
||
|
||
def _signal_duration_seconds(self, *, now: float) -> int:
|
||
if self._last_signal_started_at is None:
|
||
return max(0, int(self._same_signal_count * self._loop_interval_seconds))
|
||
|
||
return max(0, int(now - self._last_signal_started_at))
|
||
|
||
def _format_duration(self, total_seconds: int) -> str:
|
||
total_seconds = max(0, int(total_seconds))
|
||
|
||
hours = total_seconds // 3600
|
||
minutes = (total_seconds % 3600) // 60
|
||
seconds = total_seconds % 60
|
||
|
||
if hours > 0:
|
||
return f"{hours}ч {minutes:02d}м {seconds:02d}с"
|
||
|
||
if minutes > 0:
|
||
return f"{minutes}м {seconds:02d}с"
|
||
|
||
return f"{seconds}с"
|
||
|
||
# обновить поля state для экрана автоторговли
|
||
def _update_signal_state_fields(
|
||
self,
|
||
*,
|
||
state: AutoTradeState,
|
||
signal: str,
|
||
reason: str,
|
||
confidence: float,
|
||
) -> None:
|
||
previous_signal = state.last_signal
|
||
previous_decision_status = state.decision_status
|
||
|
||
if previous_signal != signal or state.signal_started_at is None:
|
||
state.signal_started_at = time.monotonic()
|
||
|
||
state.last_signal = signal
|
||
state.last_signal_repeat_count = self._same_signal_count
|
||
state.last_signal_confidence = confidence
|
||
state.last_signal_reason = reason
|
||
|
||
self._update_decision_state(
|
||
state=state,
|
||
signal=signal,
|
||
confidence=confidence,
|
||
)
|
||
|
||
signal_intent = self._signal_intent(
|
||
state=state,
|
||
signal=state.last_signal,
|
||
)
|
||
|
||
if (
|
||
previous_decision_status != state.decision_status
|
||
and state.decision_status == "READY"
|
||
):
|
||
self._log_ready_signal(
|
||
state=state,
|
||
signal=state.last_signal,
|
||
reason=state.last_signal_reason or reason,
|
||
confidence=state.last_signal_confidence,
|
||
signal_intent=signal_intent,
|
||
)
|
||
|
||
if previous_signal != state.last_signal:
|
||
EventBus.emit(
|
||
"auto_signal_changed",
|
||
{
|
||
"previous_signal": previous_signal,
|
||
"signal": state.last_signal,
|
||
"signal_intent": signal_intent,
|
||
"repeat_count": state.last_signal_repeat_count,
|
||
"confidence": state.last_signal_confidence,
|
||
},
|
||
)
|
||
|
||
if previous_decision_status != state.decision_status:
|
||
EventBus.emit(
|
||
"auto_decision_changed",
|
||
{
|
||
"previous_decision_status": previous_decision_status,
|
||
"decision_status": state.decision_status,
|
||
"signal": state.last_signal,
|
||
"signal_intent": signal_intent,
|
||
"repeat_count": state.last_signal_repeat_count,
|
||
"confidence": state.last_signal_confidence,
|
||
"symbol": state.symbol,
|
||
"strategy": state.strategy,
|
||
"leverage": state.leverage,
|
||
"reason": state.last_signal_reason,
|
||
},
|
||
)
|
||
|
||
# одиночные BUY / SELL больше не пишем в журнал как полезные события
|
||
def _log_signal_event(
|
||
self,
|
||
*,
|
||
strategy_name: str,
|
||
state: AutoTradeState,
|
||
signal: str,
|
||
reason: str,
|
||
confidence: float,
|
||
payload: dict | None,
|
||
) -> None:
|
||
return
|
||
|
||
# записать итог серии одинаковых сигналов при смене сигнала
|
||
def _log_signal_summary(
|
||
self,
|
||
*,
|
||
strategy_name: str,
|
||
state: AutoTradeState,
|
||
previous_signal: str,
|
||
previous_count: int,
|
||
next_signal: str,
|
||
reason: str,
|
||
confidence: float,
|
||
payload: dict | None,
|
||
duration_seconds: int,
|
||
) -> None:
|
||
if previous_signal != "HOLD":
|
||
return
|
||
|
||
duration_text = self._format_duration(duration_seconds)
|
||
signal_intent = "HOLD_MARKET"
|
||
|
||
try:
|
||
JournalService().log_ui_info(
|
||
event_type="signal_summary",
|
||
message=(
|
||
f"🟡 HOLD {duration_text} завершён сигналом {next_signal}."
|
||
),
|
||
screen="auto",
|
||
action="signal_summary",
|
||
payload={
|
||
"strategy": strategy_name,
|
||
"status": state.status,
|
||
"symbol": state.symbol,
|
||
"signal": previous_signal,
|
||
"next_signal": next_signal,
|
||
"signal_intent": signal_intent,
|
||
"repeat_count": previous_count,
|
||
"duration_seconds": duration_seconds,
|
||
"duration_text": duration_text,
|
||
"confidence": confidence,
|
||
"reason": reason,
|
||
"is_strong_signal": False,
|
||
"is_aggregated": True,
|
||
"payload": payload or {},
|
||
},
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
def _log_ready_signal(
|
||
self,
|
||
*,
|
||
state: AutoTradeState,
|
||
signal: str | None,
|
||
reason: str,
|
||
confidence: float,
|
||
signal_intent: str,
|
||
) -> None:
|
||
normalized_signal = (signal or "HOLD").upper()
|
||
if normalized_signal not in {"BUY", "SELL"}:
|
||
return
|
||
|
||
try:
|
||
JournalService().log_ui_info(
|
||
event_type="signal_ready",
|
||
message=f"Сигнал {normalized_signal} готов к исполнению.",
|
||
screen="auto",
|
||
action="signal_ready",
|
||
payload={
|
||
"strategy": state.strategy,
|
||
"status": state.status,
|
||
"symbol": state.symbol,
|
||
"signal": normalized_signal,
|
||
"signal_intent": signal_intent,
|
||
"confidence": confidence,
|
||
"reason": reason,
|
||
"repeat_count": state.last_signal_repeat_count,
|
||
"position_side": state.position_side,
|
||
"decision_status": state.decision_status,
|
||
"is_strong_signal": confidence > self._ready_confidence,
|
||
"is_aggregated": False,
|
||
},
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
def _sync_market_analysis_state(
|
||
self,
|
||
*,
|
||
state: AutoTradeState,
|
||
payload: dict | None,
|
||
) -> None:
|
||
if not isinstance(payload, dict):
|
||
return
|
||
|
||
previous_market_state = state.market_state
|
||
previous_market_trend = state.market_trend
|
||
previous_market_volatility = state.market_volatility
|
||
|
||
state.market_state = payload.get("market_state")
|
||
state.market_trend = payload.get("market_trend")
|
||
state.market_volatility = payload.get("market_volatility")
|
||
state.market_analysis_interval = payload.get("market_analysis_interval")
|
||
state.market_analysis_reason = payload.get("market_analysis_reason")
|
||
|
||
self._log_market_state_if_changed(
|
||
state=state,
|
||
payload=payload,
|
||
previous_market_state=previous_market_state,
|
||
previous_market_trend=previous_market_trend,
|
||
previous_market_volatility=previous_market_volatility,
|
||
)
|
||
|
||
def _log_market_state_if_changed(
|
||
self,
|
||
*,
|
||
state: AutoTradeState,
|
||
payload: dict,
|
||
previous_market_state: str | None,
|
||
previous_market_trend: str | None,
|
||
previous_market_volatility: str | None,
|
||
) -> None:
|
||
market_state = state.market_state
|
||
market_trend = state.market_trend
|
||
market_volatility = state.market_volatility
|
||
|
||
if not market_state or market_state == "UNKNOWN":
|
||
return
|
||
|
||
state_changed = (
|
||
market_state != previous_market_state
|
||
and market_state != type(self)._last_logged_market_state
|
||
)
|
||
|
||
trend_changed = (
|
||
market_trend is not None
|
||
and market_trend != previous_market_trend
|
||
and market_trend != type(self)._last_logged_market_trend
|
||
)
|
||
|
||
volatility_changed = (
|
||
market_volatility is not None
|
||
and market_volatility != previous_market_volatility
|
||
and market_volatility != type(self)._last_logged_market_volatility
|
||
)
|
||
|
||
if not state_changed and not trend_changed and not volatility_changed:
|
||
return
|
||
|
||
type(self)._last_logged_market_state = market_state
|
||
type(self)._last_logged_market_trend = market_trend
|
||
type(self)._last_logged_market_volatility = market_volatility
|
||
|
||
level = self._market_journal_level(market_state)
|
||
message = self._market_state_message(market_state)
|
||
|
||
journal_payload = {
|
||
**payload,
|
||
"previous_market_state": previous_market_state,
|
||
"previous_market_trend": previous_market_trend,
|
||
"previous_market_volatility": previous_market_volatility,
|
||
"current_market_state": market_state,
|
||
"current_market_trend": market_trend,
|
||
"current_market_volatility": market_volatility,
|
||
}
|
||
|
||
try:
|
||
if level == "WARNING":
|
||
JournalService().log_ui_warning(
|
||
event_type="market_state_changed",
|
||
message=message,
|
||
screen="auto",
|
||
action="market_analysis",
|
||
payload=journal_payload,
|
||
)
|
||
return
|
||
|
||
JournalService().log_ui_info(
|
||
event_type="market_state_changed",
|
||
message=message,
|
||
screen="auto",
|
||
action="market_analysis",
|
||
payload=journal_payload,
|
||
)
|
||
except Exception:
|
||
pass
|
||
|
||
def _market_journal_level(self, market_state: str) -> str:
|
||
if market_state == "HIGH_VOLATILITY":
|
||
return "WARNING"
|
||
|
||
return "INFO"
|
||
|
||
def _market_state_message(self, market_state: str) -> str:
|
||
messages = {
|
||
"TREND_UP": "📈 Рынок перешёл в рост.",
|
||
"TREND_DOWN": "📉 Рынок перешёл в снижение.",
|
||
"RANGE": "🟰 На рынке нет выраженного направления.",
|
||
"HIGH_VOLATILITY": "⚠️ Рынок стал слишком волатильным.",
|
||
"LOW_VOLATILITY": "💤 Рынок почти не движется.",
|
||
}
|
||
|
||
return messages.get(market_state, "⏳ Состояние рынка анализируется.")
|
||
|
||
def run_cycle(self) -> AutoTradeState:
|
||
state = self.get_state()
|
||
|
||
if state.status == "OFF":
|
||
return state
|
||
|
||
strategy = self._get_strategy()
|
||
context = self._build_strategy_context()
|
||
result = strategy.analyze(context)
|
||
|
||
self._sync_market_analysis_state(
|
||
state=state,
|
||
payload=result.payload,
|
||
)
|
||
|
||
state.last_check_at = datetime.now().strftime("%H:%M:%S")
|
||
|
||
self._log_signal_if_changed(
|
||
strategy_name=strategy.name,
|
||
state=state,
|
||
signal=result.signal.value,
|
||
reason=result.reason,
|
||
confidence=result.confidence,
|
||
payload=result.payload,
|
||
)
|
||
|
||
ExecutionEngine().process(state)
|
||
|
||
return state |