07.4.3.16 — Production Execution Pricing Layer

This commit is contained in:
2026-05-09 13:08:29 +03:00
parent 71cf206e32
commit e97dcd372b
15 changed files with 1179 additions and 188 deletions

View File

@@ -296,7 +296,7 @@ def _market_snapshot(symbol: str | None) -> dict[str, object] | None:
return None
try:
return ExchangeService().get_market_snapshot(symbol)
return ExchangeService().get_market_snapshot(symbol, runtime_key="auto")
except Exception:
return None

View File

@@ -2,6 +2,8 @@
from __future__ import annotations
import time
from aiogram import F, Router
from aiogram.exceptions import TelegramBadRequest
from aiogram.fsm.context import FSMContext
@@ -18,6 +20,11 @@ from src.trading.debug.service import DebugTradeService
router = Router(name="debug_auto")
def _ensure_signal_started_at(state) -> None:
if state.signal_started_at is None:
state.signal_started_at = time.monotonic()
async def render_debug_auto_screen(
target_message: Message,
*,
@@ -77,6 +84,7 @@ async def debug_auto_start(callback: CallbackQuery) -> None:
service = DebugTradeService()
state = service.get_state()
state.status = "RUNNING"
_ensure_signal_started_at(state)
DebugTradeRunner.set_current_screen("debug_auto")
DebugTradeRunner.start()
@@ -89,6 +97,7 @@ async def debug_auto_start(callback: CallbackQuery) -> None:
@router.callback_query(F.data == "debug_auto:stop")
async def debug_auto_stop(callback: CallbackQuery) -> None:
DebugTradeRunner.set_current_screen("debug_auto")
DebugTradeRunner.stop()
DebugTradeService().stop()
@@ -145,6 +154,8 @@ async def debug_auto_close(callback: CallbackQuery) -> None:
service = DebugTradeService()
_, result = service.close(reason="DEBUG_SCREEN_CLOSE")
DebugTradeRunner.set_current_screen("debug_auto")
if callback.message is not None:
await render_debug_auto_screen(callback.message, edit_mode=True)
@@ -153,7 +164,9 @@ async def debug_auto_close(callback: CallbackQuery) -> None:
@router.callback_query(F.data == "debug_auto:reset")
async def debug_auto_reset(callback: CallbackQuery) -> None:
DebugTradeService().reset()
DebugTradeRunner.set_current_screen("debug_auto")
state = DebugTradeService().reset()
_ensure_signal_started_at(state)
if callback.message is not None:
await render_debug_auto_screen(callback.message, edit_mode=True)

View File

@@ -7,6 +7,7 @@ import time
from aiogram.types import InlineKeyboardMarkup
from aiogram.utils.keyboard import InlineKeyboardBuilder
from src.integrations.exchange.service import ExchangeService
from src.trading.debug.service import DebugTradeService
@@ -38,6 +39,8 @@ def build_debug_auto_text() -> str:
f"<b>Баланс</b> · $ {_format_money_compact(state.allocated_balance_usd)}",
f"<b>Realized PnL</b> · {_format_signed_usd(state.realized_pnl_usd)}",
"",
*_market_snapshot_lines(state.symbol),
"",
_signal_line(state),
]
@@ -106,6 +109,90 @@ def build_debug_auto_text() -> str:
return "\n".join(parts)
def _format_updated_at(value: object) -> str:
if not value:
return ""
text = str(value)
if " " in text:
return text.rsplit(" ", 1)[-1]
return text
def _market_snapshot_lines(symbol: str | None) -> list[str]:
if not symbol:
return [
"<b>Market</b>",
"Last · —",
"Bid · —",
"Ask · —",
"Source · —",
"Age · —",
"",
"<b>Execution</b>",
"Source · —",
"Age · —",
]
market = None
execution = None
error = None
try:
market = ExchangeService().get_market_snapshot(
symbol,
runtime_key="debug_auto",
)
except Exception as exc:
error = str(exc)
try:
execution = ExchangeService().get_execution_snapshot(
symbol,
runtime_key="debug_auto",
)
except Exception as exc:
if error is None:
error = str(exc)
if market is None and execution is None:
return [
"<b>Market</b>",
"Last · —",
"Bid · —",
"Ask · —",
"Source · error",
f"Error · {error or 'unknown'}",
]
last_price = market.get("last_price") if market else getattr(execution, "last_price", None)
bid_price = market.get("bid_price") if market else getattr(execution, "bid_price", None)
ask_price = market.get("ask_price") if market else getattr(execution, "ask_price", None)
market_source = market.get("source") if market else ""
market_age = market.get("age_seconds") if market else None
execution_source = getattr(execution, "source", "") if execution else ""
execution_age = getattr(execution, "age_seconds", None) if execution else None
execution_fresh = getattr(execution, "is_fresh", None) if execution else None
return [
"<b>Market</b>",
f"Last · {_format_usd_or_dash(last_price)}",
f"Bid · {_format_usd_or_dash(bid_price)}",
f"Ask · {_format_usd_or_dash(ask_price)}",
f"Source · {market_source or ''}",
f"Quote age · {_format_age(market_age)}",
f"Exchange time · {_format_updated_at(market.get('updated_at') if market else None)}",
"",
"<b>Execution</b>",
f"Source · {execution_source or ''}",
f"Quote age · {_format_age(execution_age)}",
f"Fresh · {_format_bool(execution_fresh)}",
]
def _signal_line(state) -> str:
signal = state.last_signal or "HOLD"
@@ -225,4 +312,44 @@ def _format_signed_usd(value: float | int | None) -> str:
if amount < 0:
return f"🔴 $ {_format_money_compact(abs(amount))}"
return "$ 0"
return "$ 0"
def _format_age(value: object) -> str:
if value is None:
return ""
try:
age = max(0.0, float(value))
except (TypeError, ValueError):
return ""
if age < 1:
return f"{age:.2f}с"
if age < 10:
return f"{age:.1f}с"
total_seconds = int(age)
hours = total_seconds // 3600
minutes = (total_seconds % 3600) // 60
seconds = total_seconds % 60
if hours > 0:
return f"{hours}ч {minutes:02d}м"
if minutes > 0:
return f"{minutes}м {seconds:02d}с"
return f"{seconds}с"
def _format_bool(value: object) -> str:
if value is True:
return "yes"
if value is False:
return "no"
return ""