07.4.4.1.2 — Market State Journal Events

This commit is contained in:
2026-05-11 00:28:26 +03:00
parent ef7cec68cc
commit c07a1a4dff
9 changed files with 532 additions and 73 deletions

View File

@@ -107,6 +107,7 @@ class MarketDataRunner:
ws_symbol = cls._ws_symbol(symbol)
if symbol != last_symbol:
previous_symbol = last_symbol
last_symbol = symbol
if not cls._is_cache_symbol_used_by_other_runtime(
@@ -115,16 +116,18 @@ class MarketDataRunner:
):
MarketPriceCache.clear(cache_symbol)
cls._log_info(
context,
"market_symbol_changed",
f"Инструмент автоторговли изменён на {cache_symbol}.",
{
"symbol": symbol,
"cache_symbol": cache_symbol,
"ws_symbol": ws_symbol,
},
)
if previous_symbol is not None:
cls._log_info(
context,
"market_symbol_changed",
f"Инструмент автоторговли изменён на {cache_symbol}.",
{
"previous_symbol": previous_symbol,
"symbol": symbol,
"cache_symbol": cache_symbol,
"ws_symbol": ws_symbol,
},
)
try:
await cls._run_websocket(context, symbol)

View File

@@ -243,7 +243,7 @@ def _market_state_line(state) -> str:
labels = {
"TREND_UP": "📈 Рынок · Рост",
"TREND_DOWN": "📉 Рынок · Падение",
"RANGE": "🟰 Рынок · Флэт",
"RANGE": "🟰 Рынок · Без направления",
"HIGH_VOLATILITY": "⚠️ Рынок · Волатильность",
"LOW_VOLATILITY": "🟰 Рынок · Спокойный",
"UNKNOWN": "⏳ Рынок · Анализ",

View File

@@ -344,10 +344,44 @@ async def open_auto_strategy_settings(callback: CallbackQuery) -> None:
await callback.answer()
def _log_auto_setting_updated(
*,
event_type: str = "auto_settings_updated",
message: str,
action: str,
payload: dict,
) -> None:
try:
JournalService().log_ui_info(
event_type=event_type,
message=message,
screen="settings_auto",
action=action,
payload=payload,
)
except Exception:
pass
@router.callback_query(F.data.startswith("settings:auto_strategy:"))
async def set_auto_strategy(callback: CallbackQuery) -> None:
strategy = callback.data.split(":", 2)[2]
AutoTradeService().set_strategy(strategy.upper())
strategy = callback.data.split(":", 2)[2].upper()
service = AutoTradeService()
state = service.get_state()
previous_strategy = state.strategy
service.set_strategy(strategy)
if previous_strategy != strategy:
_log_auto_setting_updated(
message=f"Стратегия автоторговли изменена на {strategy}.",
action="set_strategy",
payload={
"previous_strategy": previous_strategy,
"strategy": strategy,
},
)
await open_auto_settings(callback)
await callback.answer("Стратегия обновлена")
@@ -380,7 +414,22 @@ async def open_auto_symbol_settings(callback: CallbackQuery) -> None:
@router.callback_query(F.data.startswith("settings:auto_symbol:"))
async def set_auto_symbol(callback: CallbackQuery) -> None:
symbol = callback.data.split(":", 2)[2]
AutoTradeService().set_symbol(symbol)
service = AutoTradeService()
state = service.get_state()
previous_symbol = state.symbol
service.set_symbol(symbol)
if previous_symbol != symbol:
_log_auto_setting_updated(
message=f"Актив автоторговли изменён на {symbol}.",
action="set_symbol",
payload={
"previous_symbol": previous_symbol,
"symbol": symbol,
},
)
await open_auto_settings(callback)
await callback.answer("Актив обновлён")
@@ -412,7 +461,22 @@ async def open_auto_risk_settings(callback: CallbackQuery) -> None:
@router.callback_query(F.data.startswith("settings:auto_risk:"))
async def set_auto_risk(callback: CallbackQuery) -> None:
risk = float(callback.data.split(":", 2)[2])
AutoTradeService().set_risk_percent(risk)
service = AutoTradeService()
state = service.get_state()
previous_risk = state.risk_percent
service.set_risk_percent(risk)
if previous_risk != risk:
_log_auto_setting_updated(
message=f"Риск на сделку изменён на {risk:g}%.",
action="set_risk_percent",
payload={
"previous_risk_percent": previous_risk,
"risk_percent": risk,
},
)
await open_auto_settings(callback)
await callback.answer("Риск обновлён")
@@ -447,12 +511,79 @@ async def open_auto_leverage_settings(callback: CallbackQuery) -> None:
@router.callback_query(F.data.startswith("settings:auto_leverage:"))
async def set_auto_leverage(callback: CallbackQuery) -> None:
leverage = float(callback.data.split(":", 2)[2])
AutoTradeService().set_leverage(leverage)
service = AutoTradeService()
state = service.get_state()
previous_leverage = state.leverage
service.set_leverage(leverage)
if previous_leverage != leverage:
_log_auto_setting_updated(
message=f"Плечо автоторговли изменено на x{leverage:g}.",
action="set_leverage",
payload={
"previous_leverage": previous_leverage,
"leverage": leverage,
},
)
await open_auto_settings(callback)
await callback.answer("Плечо обновлено")
@router.callback_query(F.data == "settings:auto_max_reserved")
async def open_auto_max_reserved_settings(callback: CallbackQuery) -> None:
if not await _prepare_system_from_callback(callback, screen="settings_auto"):
return
text = (
"<b>🏦 Лимит на сделку</b>\n\n"
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
"Максимальная доля баланса, которую можно зарезервировать под позицию:"
)
builder = InlineKeyboardBuilder()
builder.button(text="25%", callback_data="settings:auto_max_reserved:25")
builder.button(text="50%", callback_data="settings:auto_max_reserved:50")
builder.button(text="75%", callback_data="settings:auto_max_reserved:75")
builder.button(text="100%", callback_data="settings:auto_max_reserved:100")
builder.button(text="off", callback_data="settings:auto_max_reserved:off")
builder.button(text="⬅️ Назад", callback_data="settings:auto")
builder.adjust(2, 2, 1, 1)
await callback.message.edit_text(text, reply_markup=builder.as_markup())
_register_system_screen(callback.message, screen="settings_auto")
await callback.answer()
@router.callback_query(F.data.startswith("settings:auto_max_reserved:"))
async def set_auto_max_reserved(callback: CallbackQuery) -> None:
raw_value = callback.data.split(":", 2)[2]
value = None if raw_value == "off" else float(raw_value)
service = AutoTradeService()
state = service.get_state()
previous_value = state.max_reserved_balance_percent
service.set_max_reserved_balance_percent(value)
if previous_value != value:
value_text = "off" if value is None else f"{value:g}%"
_log_auto_setting_updated(
message=f"Лимит на сделку изменён на {value_text}.",
action="set_max_reserved_balance_percent",
payload={
"previous_max_reserved_balance_percent": previous_value,
"max_reserved_balance_percent": value,
},
)
await open_auto_settings(callback)
await callback.answer("Лимит обновлён")
@router.callback_query(F.data == "settings:trade")
async def open_trade_settings(callback: CallbackQuery) -> None:
if not await _prepare_system_from_callback(callback, screen="settings_trade"):
@@ -665,40 +796,4 @@ async def open_system_about(callback: CallbackQuery) -> None:
await callback.message.edit_text(text, reply_markup=builder.as_markup())
_register_system_screen(callback.message, screen="system_about")
await callback.answer()
@router.callback_query(F.data == "settings:auto_max_reserved")
async def open_auto_max_reserved_settings(callback: CallbackQuery) -> None:
if not await _prepare_system_from_callback(callback, screen="settings_auto"):
return
text = (
"<b>🏦 Лимит на сделку</b>\n\n"
"<b>СИСТЕМА</b> · Настройки · Автоторговля\n\n"
"Максимальная доля баланса, которую можно зарезервировать под позицию:"
)
builder = InlineKeyboardBuilder()
builder.button(text="25%", callback_data="settings:auto_max_reserved:25")
builder.button(text="50%", callback_data="settings:auto_max_reserved:50")
builder.button(text="75%", callback_data="settings:auto_max_reserved:75")
builder.button(text="100%", callback_data="settings:auto_max_reserved:100")
builder.button(text="off", callback_data="settings:auto_max_reserved:off")
builder.button(text="⬅️ Назад", callback_data="settings:auto")
builder.adjust(2, 2, 1, 1)
await callback.message.edit_text(text, reply_markup=builder.as_markup())
_register_system_screen(callback.message, screen="settings_auto")
await callback.answer()
@router.callback_query(F.data.startswith("settings:auto_max_reserved:"))
async def set_auto_max_reserved(callback: CallbackQuery) -> None:
raw_value = callback.data.split(":", 2)[2]
value = None if raw_value == "off" else float(raw_value)
AutoTradeService().set_max_reserved_balance_percent(value)
await open_auto_settings(callback)
await callback.answer("Max Reserved обновлён")
await callback.answer()

View File

@@ -32,6 +32,9 @@ class AutoTradeService:
_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
@@ -649,12 +652,115 @@ class AutoTradeService:
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()

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
import csv
import json
import re
from datetime import datetime
from io import BytesIO, StringIO
from zoneinfo import ZoneInfo
@@ -38,6 +39,22 @@ EVENT_TITLES = {
}
_EMOJI_RE = re.compile(
"["
"\U0001F300-\U0001FAFF"
"\U00002700-\U000027BF"
"\U00002600-\U000026FF"
"\U0001F1E6-\U0001F1FF"
"]+",
flags=re.UNICODE,
)
def _strip_emoji(value: object) -> str:
text = str(value or "")
return _EMOJI_RE.sub("", text).strip()
def _now_local() -> datetime:
settings = load_settings()
try:
@@ -76,7 +93,9 @@ def _payload(row: dict) -> dict:
def _payload_json(payload: dict) -> str:
if not payload:
return ""
return json.dumps(payload, ensure_ascii=False, sort_keys=True)
text = json.dumps(payload, ensure_ascii=False, sort_keys=True)
return _strip_emoji(text)
def _export_row(row: dict) -> list[str]:
@@ -84,15 +103,15 @@ def _export_row(row: dict) -> list[str]:
return [
_format_datetime(row.get("created_at")),
str(row.get("level") or ""),
str(row.get("event_type") or ""),
_event_title(row.get("event_type")),
str(row.get("message") or ""),
str(payload.get("account_mode") or "").upper(),
str(payload.get("screen") or ""),
str(payload.get("action") or ""),
str(payload.get("error_type") or ""),
str(payload.get("raw_error") or ""),
_strip_emoji(row.get("level")),
_strip_emoji(row.get("event_type")),
_strip_emoji(_event_title(row.get("event_type"))),
_strip_emoji(row.get("message")),
_strip_emoji(str(payload.get("account_mode") or "").upper()),
_strip_emoji(payload.get("screen")),
_strip_emoji(payload.get("action")),
_strip_emoji(payload.get("error_type")),
_strip_emoji(payload.get("raw_error")),
_payload_json(payload),
]

View File

@@ -204,19 +204,19 @@ class MarketAnalysisService:
rsi_text = f", RSI={rsi_value:.2f}" if rsi_value is not None else ""
if state == MarketState.TREND_UP:
return f"Рынок в восходящем тренде. ATR={atr_percent:.2f}%{rsi_text}."
return f"Рынок перешёл в рост. ATR={atr_percent:.2f}%{rsi_text}."
if state == MarketState.TREND_DOWN:
return f"Рынок в нисходящем тренде. ATR={atr_percent:.2f}%{rsi_text}."
return f"Рынок перешёл в снижение. ATR={atr_percent:.2f}%{rsi_text}."
if state == MarketState.RANGE:
return f"Рынок в боковике. Тренд не подтверждён. ATR={atr_percent:.2f}%{rsi_text}."
return f"На рынке нет выраженного направления. ATR={atr_percent:.2f}%{rsi_text}."
if state == MarketState.HIGH_VOLATILITY:
return f"Рынок слишком волатильный. ATR={atr_percent:.2f}%{rsi_text}."
return f"Рынок стал слишком волатильным. ATR={atr_percent:.2f}%{rsi_text}."
if state == MarketState.LOW_VOLATILITY:
return f"Рынок слишком спокойный. ATR={atr_percent:.2f}%{rsi_text}."
return f"Рынок почти не движется. ATR={atr_percent:.2f}%{rsi_text}."
return f"Состояние рынка не определено. Trend={trend}, volatility={volatility}."