Build 060.17 — Trades Feed Core

This commit is contained in:
2026-07-20 01:55:52 +03:00
parent be6beac560
commit fe308b9e60
11 changed files with 2314 additions and 2 deletions

View File

@@ -1 +1,9 @@
# app/src/market_data/acquisition/adapters/dzengi/__init__.py
# app/src/market_data/acquisition/adapters/dzengi/__init__.py
from src.market_data.acquisition.adapters.dzengi.websocket_trade_adapter import (
adapt_websocket_trade_document,
)
__all__ = [
"adapt_websocket_trade_document",
]

View File

@@ -145,6 +145,11 @@ class TradeMappingError(MarketDataAcquisitionError):
pass
# Ошибка регистрации или получения Trades Feed.
class TradeFeedRegistryError(MarketDataAcquisitionError):
pass
# Ошибка определения типа входящего WebSocket-сообщения
# и выбора специализированного адаптера.
class WebSocketMessageRoutingError(MarketDataAcquisitionError):

View File

@@ -0,0 +1,35 @@
# app/src/market_data/acquisition/feeds/trades_feed.py
from __future__ import annotations
from src.market_data.acquisition.models.trade import Trade
from src.market_data.acquisition.protocol import (
TradeDocumentHandler,
TradeDocumentSource,
)
# Источник готовой внутренней модели Trade.
class TradesFeed:
def __init__(
self,
source: TradeDocumentSource,
handler: TradeDocumentHandler,
) -> None:
self._source = source
self._handler = handler
def load_trade(
self,
symbol: str,
) -> Trade:
"""
Получить последнюю сделку инструмента.
Feed получает сырой документ у источника и делегирует
его преобразование специализированному обработчику.
"""
document = self._source.fetch_trade_document(symbol)
return self._handler.handle_trade_document(document)

View File

@@ -0,0 +1,24 @@
# app/src/market_data/acquisition/handlers/trades_handler.py
# app/src/market_data/acquisition/handlers/trades_handler.py
from __future__ import annotations
from src.market_data.acquisition.adapters.dzengi import (
adapt_websocket_trade_document,
)
from src.market_data.acquisition.models.trade import Trade
from src.market_data.acquisition.validation.schema import (
validate_dzengi_websocket_trade_schema,
)
# Обработчик документа Trades Feed формата Dzengi WebSocket internal.trade.
class DzengiTradeDocumentHandler:
def handle_trade_document(
self,
document: object,
) -> Trade:
validated_document = validate_dzengi_websocket_trade_schema(document)
return adapt_websocket_trade_document(validated_document)

View File

@@ -7,6 +7,7 @@ from typing import Protocol, runtime_checkable
from src.market_data.acquisition.models.candle import Candle
from src.market_data.acquisition.models.instrument import Instrument
from src.market_data.acquisition.models.quote import Quote
from src.market_data.acquisition.models.trade import Trade
# Источник сырого документа Instrument Reference Data.
@@ -87,6 +88,49 @@ class QuoteFeedProtocol(Protocol):
...
# Источник сырого документа Trades Feed.
@runtime_checkable
class TradeDocumentSource(Protocol):
def fetch_trade_document(
self,
symbol: str,
) -> object:
"""
Получить декодированный транспортный документ сделок.
Источник не выполняет schema validation, parsing,
value validation или mapping во внутреннюю модель Trade.
"""
...
# Обработчик сырого документа Trades Feed.
@runtime_checkable
class TradeDocumentHandler(Protocol):
def handle_trade_document(
self,
document: object,
) -> Trade:
"""
Преобразовать сырой документ
в проверенную внутреннюю модель Trade.
"""
...
# Источник готовой модели Trade.
@runtime_checkable
class TradesFeedProtocol(Protocol):
def load_trade(
self,
symbol: str,
) -> Trade:
"""
Получить внутреннюю модель последней сделки инструмента.
"""
...
# Источник сырого документа Candles Feed.
@runtime_checkable
class CandlesDocumentSource(Protocol):

View File

@@ -6,11 +6,13 @@ from src.market_data.acquisition.exceptions import (
CandleFeedRegistryError,
InstrumentFeedRegistryError,
QuoteFeedRegistryError,
TradeFeedRegistryError,
)
from src.market_data.acquisition.protocol import (
CandlesFeedProtocol,
InstrumentFeedProtocol,
QuoteFeedProtocol,
TradesFeedProtocol,
)
@@ -144,6 +146,71 @@ class QuoteFeedRegistry:
return normalized_source_name
# Реестр доступных потоков рыночных сделок.
class TradesFeedRegistry:
def __init__(self) -> None:
self._feeds: dict[str, TradesFeedProtocol] = {}
def register(
self,
source_name: str,
feed: TradesFeedProtocol,
) -> None:
"""
Зарегистрировать Trades Feed для указанного источника.
Повторная регистрация того же имени запрещена, чтобы исключить
неявную замену production-зависимости.
"""
normalized_source_name = self._normalize_source_name(source_name)
if not isinstance(feed, TradesFeedProtocol):
raise TradeFeedRegistryError(
f"Объект для источника '{normalized_source_name}' "
"не соответствует TradesFeedProtocol."
)
if normalized_source_name in self._feeds:
raise TradeFeedRegistryError(
f"Trades Feed для источника "
f"'{normalized_source_name}' уже зарегистрирован."
)
self._feeds[normalized_source_name] = feed
def get(
self,
source_name: str,
) -> TradesFeedProtocol:
"""Вернуть зарегистрированный Trades Feed по имени источника."""
normalized_source_name = self._normalize_source_name(source_name)
feed = self._feeds.get(normalized_source_name)
if feed is None:
raise TradeFeedRegistryError(
f"Trades Feed для источника "
f"'{normalized_source_name}' не зарегистрирован."
)
return feed
def _normalize_source_name(
self,
source_name: str,
) -> str:
normalized_source_name = source_name.strip()
if not normalized_source_name:
raise TradeFeedRegistryError(
"Имя источника Trades Feed не должно быть пустым."
)
return normalized_source_name
# Реестр доступных потоков рыночных свечей.
class CandlesFeedRegistry:
def __init__(self) -> None: