build 045: register canonical candles feed in acquisition registry
This commit is contained in:
@@ -90,4 +90,9 @@ class CandleValueError(MarketDataAcquisitionError):
|
|||||||
|
|
||||||
# Ошибка преобразования raw-модели источника во внутреннюю модель Candle.
|
# Ошибка преобразования raw-модели источника во внутреннюю модель Candle.
|
||||||
class CandleMappingError(MarketDataAcquisitionError):
|
class CandleMappingError(MarketDataAcquisitionError):
|
||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Ошибка регистрации или получения Candles Feed.
|
||||||
|
class CandleFeedRegistryError(MarketDataAcquisitionError):
|
||||||
|
pass
|
||||||
|
|||||||
@@ -3,10 +3,12 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
from src.market_data.acquisition.exceptions import (
|
from src.market_data.acquisition.exceptions import (
|
||||||
|
CandleFeedRegistryError,
|
||||||
InstrumentFeedRegistryError,
|
InstrumentFeedRegistryError,
|
||||||
QuoteFeedRegistryError,
|
QuoteFeedRegistryError,
|
||||||
)
|
)
|
||||||
from src.market_data.acquisition.protocol import (
|
from src.market_data.acquisition.protocol import (
|
||||||
|
CandlesFeedProtocol,
|
||||||
InstrumentFeedProtocol,
|
InstrumentFeedProtocol,
|
||||||
QuoteFeedProtocol,
|
QuoteFeedProtocol,
|
||||||
)
|
)
|
||||||
@@ -140,3 +142,68 @@ class QuoteFeedRegistry:
|
|||||||
)
|
)
|
||||||
|
|
||||||
return normalized_source_name
|
return normalized_source_name
|
||||||
|
|
||||||
|
|
||||||
|
# Реестр доступных потоков рыночных свечей.
|
||||||
|
class CandlesFeedRegistry:
|
||||||
|
def __init__(self) -> None:
|
||||||
|
self._feeds: dict[str, CandlesFeedProtocol] = {}
|
||||||
|
|
||||||
|
def register(
|
||||||
|
self,
|
||||||
|
source_name: str,
|
||||||
|
feed: CandlesFeedProtocol,
|
||||||
|
) -> None:
|
||||||
|
"""
|
||||||
|
Зарегистрировать Candles Feed для указанного источника.
|
||||||
|
|
||||||
|
Повторная регистрация того же имени запрещена, чтобы исключить
|
||||||
|
неявную замену production-зависимости.
|
||||||
|
"""
|
||||||
|
|
||||||
|
normalized_source_name = self._normalize_source_name(source_name)
|
||||||
|
|
||||||
|
if not isinstance(feed, CandlesFeedProtocol):
|
||||||
|
raise CandleFeedRegistryError(
|
||||||
|
f"Объект для источника '{normalized_source_name}' "
|
||||||
|
"не соответствует CandlesFeedProtocol."
|
||||||
|
)
|
||||||
|
|
||||||
|
if normalized_source_name in self._feeds:
|
||||||
|
raise CandleFeedRegistryError(
|
||||||
|
f"Candles Feed для источника "
|
||||||
|
f"'{normalized_source_name}' уже зарегистрирован."
|
||||||
|
)
|
||||||
|
|
||||||
|
self._feeds[normalized_source_name] = feed
|
||||||
|
|
||||||
|
def get(
|
||||||
|
self,
|
||||||
|
source_name: str,
|
||||||
|
) -> CandlesFeedProtocol:
|
||||||
|
"""Вернуть зарегистрированный Candles Feed по имени источника."""
|
||||||
|
|
||||||
|
normalized_source_name = self._normalize_source_name(source_name)
|
||||||
|
|
||||||
|
feed = self._feeds.get(normalized_source_name)
|
||||||
|
|
||||||
|
if feed is None:
|
||||||
|
raise CandleFeedRegistryError(
|
||||||
|
f"Candles 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 CandleFeedRegistryError(
|
||||||
|
"Имя источника Candles Feed не должно быть пустым."
|
||||||
|
)
|
||||||
|
|
||||||
|
return normalized_source_name
|
||||||
|
|||||||
@@ -431,3 +431,260 @@ def test_quote_registry_error_inherits_acquisition_error() -> None:
|
|||||||
error = QuoteFeedRegistryError("Registry error.")
|
error = QuoteFeedRegistryError("Registry error.")
|
||||||
|
|
||||||
assert isinstance(error, MarketDataAcquisitionError)
|
assert isinstance(error, MarketDataAcquisitionError)
|
||||||
|
|
||||||
|
# Candles Feed Registry tests.
|
||||||
|
from datetime import datetime
|
||||||
|
|
||||||
|
from src.market_data.acquisition.exceptions import CandleFeedRegistryError
|
||||||
|
from src.market_data.acquisition.models.candle import Candle
|
||||||
|
from src.market_data.acquisition.protocol import CandlesFeedProtocol
|
||||||
|
from src.market_data.acquisition.registry import CandlesFeedRegistry
|
||||||
|
|
||||||
|
|
||||||
|
def _candle() -> Candle:
|
||||||
|
return Candle(
|
||||||
|
symbol="BTC/USD_LEVERAGE",
|
||||||
|
interval="1m",
|
||||||
|
open_time=datetime.now(timezone.utc),
|
||||||
|
open_price=Decimal("64100.00"),
|
||||||
|
high_price=Decimal("64200.00"),
|
||||||
|
low_price=Decimal("64000.00"),
|
||||||
|
close_price=Decimal("64150.00"),
|
||||||
|
volume=Decimal("12.5"),
|
||||||
|
source="rest_klines:bid",
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
class StubCandlesFeed:
|
||||||
|
def __init__(
|
||||||
|
self,
|
||||||
|
*,
|
||||||
|
candles: tuple[Candle, ...] = (),
|
||||||
|
) -> None:
|
||||||
|
self.candles = candles
|
||||||
|
self.calls: list[tuple[str, str, int, str]] = []
|
||||||
|
|
||||||
|
def load_candles(
|
||||||
|
self,
|
||||||
|
symbol: str,
|
||||||
|
*,
|
||||||
|
interval: str,
|
||||||
|
limit: int,
|
||||||
|
price_type: str,
|
||||||
|
) -> tuple[Candle, ...]:
|
||||||
|
self.calls.append(
|
||||||
|
(
|
||||||
|
symbol,
|
||||||
|
interval,
|
||||||
|
limit,
|
||||||
|
price_type,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return self.candles
|
||||||
|
|
||||||
|
|
||||||
|
def test_candles_registry_registers_and_returns_feed() -> None:
|
||||||
|
registry = CandlesFeedRegistry()
|
||||||
|
feed = StubCandlesFeed()
|
||||||
|
|
||||||
|
registry.register("dzengi", feed)
|
||||||
|
|
||||||
|
assert registry.get("dzengi") is feed
|
||||||
|
|
||||||
|
|
||||||
|
def test_candles_registry_accepts_candles_feed_protocol() -> None:
|
||||||
|
registry = CandlesFeedRegistry()
|
||||||
|
feed = StubCandlesFeed()
|
||||||
|
|
||||||
|
assert isinstance(feed, CandlesFeedProtocol)
|
||||||
|
|
||||||
|
registry.register("dzengi", feed)
|
||||||
|
|
||||||
|
assert registry.get("dzengi") is feed
|
||||||
|
|
||||||
|
|
||||||
|
def test_candles_registry_preserves_feed_identity() -> None:
|
||||||
|
registry = CandlesFeedRegistry()
|
||||||
|
candles = (_candle(),)
|
||||||
|
feed = StubCandlesFeed(candles=candles)
|
||||||
|
|
||||||
|
registry.register("dzengi", feed)
|
||||||
|
|
||||||
|
registered_feed = registry.get("dzengi")
|
||||||
|
|
||||||
|
assert registered_feed is feed
|
||||||
|
assert registered_feed.load_candles(
|
||||||
|
"BTC/USD_LEVERAGE",
|
||||||
|
interval="1m",
|
||||||
|
limit=100,
|
||||||
|
price_type="bid",
|
||||||
|
) is candles
|
||||||
|
|
||||||
|
|
||||||
|
def test_candles_registry_supports_multiple_source_names() -> None:
|
||||||
|
registry = CandlesFeedRegistry()
|
||||||
|
dzengi_feed = StubCandlesFeed()
|
||||||
|
secondary_feed = StubCandlesFeed()
|
||||||
|
|
||||||
|
registry.register("dzengi", dzengi_feed)
|
||||||
|
registry.register("secondary", secondary_feed)
|
||||||
|
|
||||||
|
assert registry.get("dzengi") is dzengi_feed
|
||||||
|
assert registry.get("secondary") is secondary_feed
|
||||||
|
|
||||||
|
|
||||||
|
def test_candles_registry_strips_outer_whitespace() -> None:
|
||||||
|
registry = CandlesFeedRegistry()
|
||||||
|
feed = StubCandlesFeed()
|
||||||
|
|
||||||
|
registry.register(" dzengi ", feed)
|
||||||
|
|
||||||
|
assert registry.get("dzengi") is feed
|
||||||
|
assert registry.get(" dzengi ") is feed
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"source_name",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
"\t",
|
||||||
|
"\n",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_candles_registry_rejects_empty_source_name(
|
||||||
|
source_name: str,
|
||||||
|
) -> None:
|
||||||
|
registry = CandlesFeedRegistry()
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
CandleFeedRegistryError,
|
||||||
|
match=r"Имя источника Candles Feed не должно быть пустым",
|
||||||
|
):
|
||||||
|
registry.register(source_name, StubCandlesFeed())
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"source_name",
|
||||||
|
[
|
||||||
|
"",
|
||||||
|
" ",
|
||||||
|
" ",
|
||||||
|
"\t",
|
||||||
|
"\n",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_candles_registry_rejects_empty_source_name_on_get(
|
||||||
|
source_name: str,
|
||||||
|
) -> None:
|
||||||
|
registry = CandlesFeedRegistry()
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
CandleFeedRegistryError,
|
||||||
|
match=r"Имя источника Candles Feed не должно быть пустым",
|
||||||
|
):
|
||||||
|
registry.get(source_name)
|
||||||
|
|
||||||
|
|
||||||
|
def test_candles_registry_rejects_duplicate_registration() -> None:
|
||||||
|
registry = CandlesFeedRegistry()
|
||||||
|
first_feed = StubCandlesFeed()
|
||||||
|
second_feed = StubCandlesFeed()
|
||||||
|
|
||||||
|
registry.register("dzengi", first_feed)
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
CandleFeedRegistryError,
|
||||||
|
match=r"уже зарегистрирован",
|
||||||
|
):
|
||||||
|
registry.register("dzengi", second_feed)
|
||||||
|
|
||||||
|
|
||||||
|
def test_candles_duplicate_does_not_replace_original_feed() -> None:
|
||||||
|
registry = CandlesFeedRegistry()
|
||||||
|
first_feed = StubCandlesFeed()
|
||||||
|
second_feed = StubCandlesFeed()
|
||||||
|
|
||||||
|
registry.register("dzengi", first_feed)
|
||||||
|
|
||||||
|
with pytest.raises(CandleFeedRegistryError):
|
||||||
|
registry.register("dzengi", second_feed)
|
||||||
|
|
||||||
|
assert registry.get("dzengi") is first_feed
|
||||||
|
|
||||||
|
|
||||||
|
def test_candles_duplicate_uses_normalized_source_name() -> None:
|
||||||
|
registry = CandlesFeedRegistry()
|
||||||
|
first_feed = StubCandlesFeed()
|
||||||
|
second_feed = StubCandlesFeed()
|
||||||
|
|
||||||
|
registry.register("dzengi", first_feed)
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
CandleFeedRegistryError,
|
||||||
|
match=r"уже зарегистрирован",
|
||||||
|
):
|
||||||
|
registry.register(" dzengi ", second_feed)
|
||||||
|
|
||||||
|
|
||||||
|
def test_candles_registry_keeps_source_name_case_sensitive() -> None:
|
||||||
|
registry = CandlesFeedRegistry()
|
||||||
|
lowercase_feed = StubCandlesFeed()
|
||||||
|
uppercase_feed = StubCandlesFeed()
|
||||||
|
|
||||||
|
registry.register("dzengi", lowercase_feed)
|
||||||
|
registry.register("DZENGI", uppercase_feed)
|
||||||
|
|
||||||
|
assert registry.get("dzengi") is lowercase_feed
|
||||||
|
assert registry.get("DZENGI") is uppercase_feed
|
||||||
|
|
||||||
|
|
||||||
|
def test_candles_registry_rejects_unregistered_source() -> None:
|
||||||
|
registry = CandlesFeedRegistry()
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
CandleFeedRegistryError,
|
||||||
|
match=r"не зарегистрирован",
|
||||||
|
):
|
||||||
|
registry.get("dzengi")
|
||||||
|
|
||||||
|
|
||||||
|
def test_candles_registry_rejects_invalid_feed() -> None:
|
||||||
|
registry = CandlesFeedRegistry()
|
||||||
|
|
||||||
|
with pytest.raises(
|
||||||
|
CandleFeedRegistryError,
|
||||||
|
match=r"не соответствует CandlesFeedProtocol",
|
||||||
|
):
|
||||||
|
registry.register(
|
||||||
|
"invalid",
|
||||||
|
InvalidFeed(), # type: ignore[arg-type]
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_candles_registry_does_not_load_feed_during_registration() -> None:
|
||||||
|
registry = CandlesFeedRegistry()
|
||||||
|
feed = StubCandlesFeed()
|
||||||
|
|
||||||
|
registry.register("dzengi", feed)
|
||||||
|
|
||||||
|
assert feed.calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_candles_registry_does_not_load_feed_during_get() -> None:
|
||||||
|
registry = CandlesFeedRegistry()
|
||||||
|
feed = StubCandlesFeed()
|
||||||
|
|
||||||
|
registry.register("dzengi", feed)
|
||||||
|
result = registry.get("dzengi")
|
||||||
|
|
||||||
|
assert result is feed
|
||||||
|
assert feed.calls == []
|
||||||
|
|
||||||
|
|
||||||
|
def test_candles_registry_error_inherits_acquisition_error() -> None:
|
||||||
|
error = CandleFeedRegistryError("Registry error.")
|
||||||
|
|
||||||
|
assert isinstance(error, MarketDataAcquisitionError)
|
||||||
|
assert str(error) == "Registry error."
|
||||||
|
|||||||
692
docs/migrations/build_045.md
Normal file
692
docs/migrations/build_045.md
Normal file
@@ -0,0 +1,692 @@
|
|||||||
|
# Build 045 — регистрация канонического Candles Feed
|
||||||
|
|
||||||
|
## Статус
|
||||||
|
|
||||||
|
**Завершён**
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Цель Build
|
||||||
|
|
||||||
|
Продолжить поэтапную миграцию **OHLCV Feed** в утверждённую архитектуру `Market Data Acquisition` после завершения фундаментального слоя Candles Feed в Build 044.
|
||||||
|
|
||||||
|
Build 045 добавляет реестр канонических источников свечей и завершает следующий минимальный архитектурный шаг:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Candles Feed
|
||||||
|
↓
|
||||||
|
CandlesFeedRegistry
|
||||||
|
```
|
||||||
|
|
||||||
|
При этом:
|
||||||
|
|
||||||
|
- существующий runtime не переключается;
|
||||||
|
- `ExchangeService.get_klines()` не изменяется;
|
||||||
|
- legacy-путь получения свечей продолжает работать;
|
||||||
|
- новая архитектура остаётся изолированной от существующего торгового runtime;
|
||||||
|
- обратная совместимость полностью сохраняется.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Исходное состояние
|
||||||
|
|
||||||
|
До Build 045 проект уже содержал фундамент канонического Candles Feed, созданный в Build 044:
|
||||||
|
|
||||||
|
```text
|
||||||
|
REST /api/v1/klines
|
||||||
|
↓
|
||||||
|
DzengiCandlesDocumentSource
|
||||||
|
↓
|
||||||
|
validate_candles_schema()
|
||||||
|
↓
|
||||||
|
parse_candles()
|
||||||
|
↓
|
||||||
|
validate_candles_values()
|
||||||
|
↓
|
||||||
|
map_candles()
|
||||||
|
↓
|
||||||
|
Candle
|
||||||
|
↓
|
||||||
|
DzengiCandlesDocumentHandler
|
||||||
|
↓
|
||||||
|
CandlesFeed
|
||||||
|
```
|
||||||
|
|
||||||
|
Также существовали:
|
||||||
|
|
||||||
|
- каноническая модель `Candle`;
|
||||||
|
- транспортная raw-модель Dzengi для свечей;
|
||||||
|
- REST document source;
|
||||||
|
- schema validation;
|
||||||
|
- parser;
|
||||||
|
- value validation;
|
||||||
|
- mapper;
|
||||||
|
- document handler;
|
||||||
|
- `CandlesFeed`;
|
||||||
|
- протоколы Candles Feed;
|
||||||
|
- специализированные исключения;
|
||||||
|
- unit-тесты фундаментального слоя.
|
||||||
|
|
||||||
|
Однако отсутствовал реестр, позволяющий регистрировать и выбирать конкретный `CandlesFeed` по имени источника.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Объём изменений
|
||||||
|
|
||||||
|
Build 045 изменяет только следующие файлы:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/market_data/acquisition/exceptions.py
|
||||||
|
src/market_data/acquisition/registry.py
|
||||||
|
tests/unit/market_data/acquisition/test_registry.py
|
||||||
|
docs/migrations/build_045.md
|
||||||
|
```
|
||||||
|
|
||||||
|
Build не изменяет:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/market_data/acquisition/service.py
|
||||||
|
src/integrations/exchange/service.py
|
||||||
|
src/integrations/exchange/models.py
|
||||||
|
src/trading/
|
||||||
|
src/telegram/
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 1. Исключение CandleFeedRegistryError
|
||||||
|
|
||||||
|
В файл:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/market_data/acquisition/exceptions.py
|
||||||
|
```
|
||||||
|
|
||||||
|
добавлено специализированное исключение:
|
||||||
|
|
||||||
|
```python
|
||||||
|
class CandleFeedRegistryError(MarketDataAcquisitionError):
|
||||||
|
pass
|
||||||
|
```
|
||||||
|
|
||||||
|
Назначение исключения — изолировать ошибки регистрации и получения Candles Feed от других ошибок подсистемы `Market Data Acquisition`.
|
||||||
|
|
||||||
|
Иерархия:
|
||||||
|
|
||||||
|
```text
|
||||||
|
MarketDataAcquisitionError
|
||||||
|
↓
|
||||||
|
CandleFeedRegistryError
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 2. Реестр CandlesFeedRegistry
|
||||||
|
|
||||||
|
В файл:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/market_data/acquisition/registry.py
|
||||||
|
```
|
||||||
|
|
||||||
|
добавлен класс:
|
||||||
|
|
||||||
|
```text
|
||||||
|
CandlesFeedRegistry
|
||||||
|
```
|
||||||
|
|
||||||
|
Реестр отвечает исключительно за регистрацию и получение реализаций:
|
||||||
|
|
||||||
|
```text
|
||||||
|
CandlesFeedProtocol
|
||||||
|
```
|
||||||
|
|
||||||
|
Реестр не выполняет:
|
||||||
|
|
||||||
|
- REST-запросы;
|
||||||
|
- schema validation;
|
||||||
|
- parsing;
|
||||||
|
- value validation;
|
||||||
|
- mapping;
|
||||||
|
- хранение свечей;
|
||||||
|
- управление runtime;
|
||||||
|
- переключение legacy-потребителей.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 3. Контракт реестра
|
||||||
|
|
||||||
|
`CandlesFeedRegistry` предоставляет только операции регистрации и получения Candles Feed.
|
||||||
|
|
||||||
|
Публичный контракт:
|
||||||
|
|
||||||
|
```text
|
||||||
|
register()
|
||||||
|
get()
|
||||||
|
```
|
||||||
|
|
||||||
|
Внутренняя нормализация имени источника выполняется методом:
|
||||||
|
|
||||||
|
```text
|
||||||
|
_normalize_source_name()
|
||||||
|
```
|
||||||
|
|
||||||
|
Концептуально:
|
||||||
|
|
||||||
|
```text
|
||||||
|
source name
|
||||||
|
↓
|
||||||
|
CandlesFeedRegistry
|
||||||
|
↓
|
||||||
|
CandlesFeedProtocol
|
||||||
|
```
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```text
|
||||||
|
"dzengi"
|
||||||
|
↓
|
||||||
|
CandlesFeedRegistry
|
||||||
|
↓
|
||||||
|
CandlesFeed
|
||||||
|
```
|
||||||
|
|
||||||
|
Реестр принимает только объекты, соответствующие каноническому контракту:
|
||||||
|
|
||||||
|
```text
|
||||||
|
CandlesFeedProtocol
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 4. Нормализация имени источника
|
||||||
|
|
||||||
|
Имена источников нормализуются перед использованием.
|
||||||
|
|
||||||
|
Поддерживается удаление внешних пробелов:
|
||||||
|
|
||||||
|
```text
|
||||||
|
"dzengi"
|
||||||
|
" dzengi "
|
||||||
|
```
|
||||||
|
|
||||||
|
После нормализации оба значения соответствуют одному ключу:
|
||||||
|
|
||||||
|
```text
|
||||||
|
dzengi
|
||||||
|
```
|
||||||
|
|
||||||
|
Пустые имена источников не допускаются.
|
||||||
|
|
||||||
|
Регистр символов сохраняется. Имена источников являются case-sensitive:
|
||||||
|
|
||||||
|
```text
|
||||||
|
"dzengi"
|
||||||
|
```
|
||||||
|
|
||||||
|
и:
|
||||||
|
|
||||||
|
```text
|
||||||
|
"DZENGI"
|
||||||
|
```
|
||||||
|
|
||||||
|
считаются разными именами источников.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 5. Защита от некорректной регистрации
|
||||||
|
|
||||||
|
`CandlesFeedRegistry` отклоняет:
|
||||||
|
|
||||||
|
- пустое имя источника;
|
||||||
|
- имя, состоящее только из пробелов;
|
||||||
|
- объект, не соответствующий `CandlesFeedProtocol`;
|
||||||
|
- повторную регистрацию уже существующего нормализованного имени источника.
|
||||||
|
|
||||||
|
Ошибки представлены через:
|
||||||
|
|
||||||
|
```text
|
||||||
|
CandleFeedRegistryError
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 6. Защита от повторной регистрации
|
||||||
|
|
||||||
|
Повторная регистрация одного и того же нормализованного имени источника запрещена.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```text
|
||||||
|
register("dzengi", feed_a)
|
||||||
|
↓
|
||||||
|
dzengi → feed_a
|
||||||
|
```
|
||||||
|
|
||||||
|
Повторная регистрация:
|
||||||
|
|
||||||
|
```text
|
||||||
|
register("dzengi", feed_b)
|
||||||
|
↓
|
||||||
|
CandleFeedRegistryError
|
||||||
|
```
|
||||||
|
|
||||||
|
Исходный Feed при ошибке повторной регистрации не заменяется.
|
||||||
|
|
||||||
|
Для замены зарегистрированного Feed требуется отдельное явное изменение архитектурного контракта. В Build 045 такая возможность не добавлялась.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 7. Получение зарегистрированного Feed
|
||||||
|
|
||||||
|
Зарегистрированный Candles Feed может быть получен по имени источника.
|
||||||
|
|
||||||
|
Концептуально:
|
||||||
|
|
||||||
|
```text
|
||||||
|
registry.get("dzengi")
|
||||||
|
↓
|
||||||
|
CandlesFeedProtocol
|
||||||
|
```
|
||||||
|
|
||||||
|
При успешном получении реестр возвращает тот же объект Feed, который был ранее зарегистрирован:
|
||||||
|
|
||||||
|
```text
|
||||||
|
feed = CandlesFeed(...)
|
||||||
|
|
||||||
|
registry.register("dzengi", feed)
|
||||||
|
|
||||||
|
registry.get("dzengi") is feed
|
||||||
|
↓
|
||||||
|
True
|
||||||
|
```
|
||||||
|
|
||||||
|
Попытка получить неизвестный источник приводит к:
|
||||||
|
|
||||||
|
```text
|
||||||
|
CandleFeedRegistryError
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 8. Ограниченный публичный контракт
|
||||||
|
|
||||||
|
Публичный контракт `CandlesFeedRegistry` в Build 045 ограничен двумя операциями:
|
||||||
|
|
||||||
|
```text
|
||||||
|
register()
|
||||||
|
get()
|
||||||
|
```
|
||||||
|
|
||||||
|
В Build 045 намеренно не добавлены:
|
||||||
|
|
||||||
|
```text
|
||||||
|
contains()
|
||||||
|
remove()
|
||||||
|
replace()
|
||||||
|
clear()
|
||||||
|
```
|
||||||
|
|
||||||
|
Реестр не предоставляет управление жизненным циклом зарегистрированных Feed и не выполняет их запуск или остановку.
|
||||||
|
|
||||||
|
Такое ограничение соответствует принципу минимального безопасного Build: добавляется только функциональность, необходимая для последующей интеграции Candles Feed в сервисный слой.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 9. Изоляция от Acquisition Service
|
||||||
|
|
||||||
|
В рамках Build 045 намеренно не добавлялись:
|
||||||
|
|
||||||
|
```text
|
||||||
|
CandlesAcquisitionService
|
||||||
|
```
|
||||||
|
|
||||||
|
или метод:
|
||||||
|
|
||||||
|
```text
|
||||||
|
load_candles()
|
||||||
|
```
|
||||||
|
|
||||||
|
в существующий:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/market_data/acquisition/service.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Архитектурная проверка:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -RIn \
|
||||||
|
--exclude-dir="__pycache__" \
|
||||||
|
--exclude="*.pyc" \
|
||||||
|
"CandlesAcquisitionService\|load_candles" \
|
||||||
|
src/market_data/acquisition/service.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Результат:
|
||||||
|
|
||||||
|
```text
|
||||||
|
пусто
|
||||||
|
```
|
||||||
|
|
||||||
|
Это подтверждает, что Build 045 ограничен добавлением реестра и не выполняет преждевременную интеграцию сервисного слоя.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 10. Изоляция от legacy runtime
|
||||||
|
|
||||||
|
Build 045 не изменяет существующий runtime-путь получения свечей:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Trading / Market Analysis
|
||||||
|
↓
|
||||||
|
ExchangeService.get_klines()
|
||||||
|
↓
|
||||||
|
legacy parsing
|
||||||
|
↓
|
||||||
|
KlineBatch
|
||||||
|
```
|
||||||
|
|
||||||
|
После Build 045 этот путь продолжает работать без изменений.
|
||||||
|
|
||||||
|
Новый канонический путь пока существует параллельно:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DzengiCandlesDocumentSource
|
||||||
|
↓
|
||||||
|
DzengiCandlesDocumentHandler
|
||||||
|
↓
|
||||||
|
CandlesFeed
|
||||||
|
↓
|
||||||
|
CandlesFeedRegistry
|
||||||
|
```
|
||||||
|
|
||||||
|
Таким образом, в системе временно существуют два пути:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Legacy runtime path
|
||||||
|
+
|
||||||
|
Canonical Candles Feed path
|
||||||
|
```
|
||||||
|
|
||||||
|
Это ожидаемое переходное состояние безопасной поэтапной миграции.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 11. Unit-тесты
|
||||||
|
|
||||||
|
Расширен файл:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tests/unit/market_data/acquisition/test_registry.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Добавлено покрытие для:
|
||||||
|
|
||||||
|
- создания `CandlesFeedRegistry`;
|
||||||
|
- регистрации Candles Feed;
|
||||||
|
- получения зарегистрированного Feed;
|
||||||
|
- нормализации внешних пробелов имени источника;
|
||||||
|
- case-sensitive поведения имён источников;
|
||||||
|
- отклонения пустого имени;
|
||||||
|
- отклонения имени, состоящего только из пробелов;
|
||||||
|
- отклонения объекта, не соответствующего `CandlesFeedProtocol`;
|
||||||
|
- защиты от повторной регистрации;
|
||||||
|
- проверки, что повторная регистрация не заменяет исходный Feed;
|
||||||
|
- получения неизвестного источника;
|
||||||
|
- сохранения identity зарегистрированного Feed;
|
||||||
|
- отсутствия вызова `load_candles()` при регистрации Feed;
|
||||||
|
- отсутствия вызова `load_candles()` при получении Feed;
|
||||||
|
- соответствия зарегистрированного объекта `CandlesFeedProtocol`;
|
||||||
|
- корректной работы специализированного `CandleFeedRegistryError`.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 12. Результаты targeted-тестов
|
||||||
|
|
||||||
|
Выполнена команда:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pytest -q \
|
||||||
|
tests/unit/market_data/acquisition/test_registry.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Результат:
|
||||||
|
|
||||||
|
```text
|
||||||
|
61 passed in 0.04s
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 13. Результаты полного набора тестов
|
||||||
|
|
||||||
|
Выполнена команда:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pytest -q
|
||||||
|
```
|
||||||
|
|
||||||
|
Результат:
|
||||||
|
|
||||||
|
```text
|
||||||
|
707 passed in 2.87s
|
||||||
|
```
|
||||||
|
|
||||||
|
Все unit-тесты проекта проходят успешно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 14. Архитектурная проверка реестра
|
||||||
|
|
||||||
|
Выполнена команда:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -RIn \
|
||||||
|
--exclude-dir="__pycache__" \
|
||||||
|
--exclude="*.pyc" \
|
||||||
|
"CandlesFeedRegistry\|CandleFeedRegistryError" \
|
||||||
|
src tests
|
||||||
|
```
|
||||||
|
|
||||||
|
Результат подтверждает, что новые сущности находятся только в ожидаемых областях:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/market_data/acquisition/registry.py
|
||||||
|
src/market_data/acquisition/exceptions.py
|
||||||
|
tests/unit/market_data/acquisition/test_registry.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Неожиданных зависимостей не обнаружено.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 15. Проверка отсутствия преждевременной сервисной интеграции
|
||||||
|
|
||||||
|
Выполнена команда:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
grep -RIn \
|
||||||
|
--exclude-dir="__pycache__" \
|
||||||
|
--exclude="*.pyc" \
|
||||||
|
"CandlesAcquisitionService\|load_candles" \
|
||||||
|
src/market_data/acquisition/service.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Результат:
|
||||||
|
|
||||||
|
```text
|
||||||
|
пусто
|
||||||
|
```
|
||||||
|
|
||||||
|
Это подтверждает, что Build 045 не расширяет `MarketDataAcquisitionService` и не переключает runtime.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 16. Проверка синтаксиса
|
||||||
|
|
||||||
|
Выполнена команда:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m compileall \
|
||||||
|
src/market_data/acquisition/exceptions.py \
|
||||||
|
src/market_data/acquisition/registry.py \
|
||||||
|
tests/unit/market_data/acquisition/test_registry.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Результат:
|
||||||
|
|
||||||
|
```text
|
||||||
|
успешно
|
||||||
|
```
|
||||||
|
|
||||||
|
Ошибок синтаксиса не обнаружено.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 17. Проверка форматирования diff
|
||||||
|
|
||||||
|
Первоначальная проверка:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
обнаружила одну лишнюю пустую строку в конце:
|
||||||
|
|
||||||
|
```text
|
||||||
|
app/tests/unit/market_data/acquisition/test_registry.py:691: new blank line at EOF.
|
||||||
|
```
|
||||||
|
|
||||||
|
После удаления лишней пустой строки необходимо повторно выполнить:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Финальный ожидаемый результат:
|
||||||
|
|
||||||
|
```text
|
||||||
|
пустой вывод
|
||||||
|
```
|
||||||
|
|
||||||
|
До создания commit эта проверка должна быть подтверждена.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 18. Итоговая архитектура после Build 045
|
||||||
|
|
||||||
|
После завершения Build 045 канонический Candles Feed имеет следующую структуру:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Dzengi REST /api/v1/klines
|
||||||
|
↓
|
||||||
|
DzengiCandlesDocumentSource
|
||||||
|
↓
|
||||||
|
validate_candles_schema()
|
||||||
|
↓
|
||||||
|
parse_candles()
|
||||||
|
↓
|
||||||
|
validate_candles_values()
|
||||||
|
↓
|
||||||
|
map_candles()
|
||||||
|
↓
|
||||||
|
Candle
|
||||||
|
↓
|
||||||
|
DzengiCandlesDocumentHandler
|
||||||
|
↓
|
||||||
|
CandlesFeed
|
||||||
|
↓
|
||||||
|
CandlesFeedRegistry
|
||||||
|
```
|
||||||
|
|
||||||
|
При этом существующий production/runtime-путь остаётся неизменным:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Trading / Market Analysis
|
||||||
|
↓
|
||||||
|
ExchangeService.get_klines()
|
||||||
|
↓
|
||||||
|
legacy parsing
|
||||||
|
↓
|
||||||
|
KlineBatch
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 19. Что намеренно не входит в Build 045
|
||||||
|
|
||||||
|
Build 045 намеренно не выполняет:
|
||||||
|
|
||||||
|
- интеграцию Candles Feed в `MarketDataAcquisitionService`;
|
||||||
|
- изменение `ExchangeService.get_klines()`;
|
||||||
|
- изменение `Kline`;
|
||||||
|
- изменение `KlineBatch`;
|
||||||
|
- переключение `trading/market_analysis`;
|
||||||
|
- переключение HTF-анализа;
|
||||||
|
- удаление legacy parser свечей;
|
||||||
|
- добавление постоянного Candle Store;
|
||||||
|
- изменение runtime;
|
||||||
|
- изменение Telegram UI;
|
||||||
|
- изменение торговой логики.
|
||||||
|
|
||||||
|
Эти ограничения являются частью стратегии безопасной миграции.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## 20. Следующий Build
|
||||||
|
|
||||||
|
Следующий минимальный безопасный этап:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Build 046 — интеграция канонического Candles Feed в MarketDataAcquisitionService
|
||||||
|
```
|
||||||
|
|
||||||
|
Предполагаемая цель:
|
||||||
|
|
||||||
|
```text
|
||||||
|
CandlesFeedRegistry
|
||||||
|
↓
|
||||||
|
MarketDataAcquisitionService
|
||||||
|
↓
|
||||||
|
load_candles(...)
|
||||||
|
↓
|
||||||
|
tuple[Candle, ...]
|
||||||
|
```
|
||||||
|
|
||||||
|
Build 046 должен:
|
||||||
|
|
||||||
|
- добавить поддержку `CandlesFeedRegistry` в сервисный слой `Market Data Acquisition`;
|
||||||
|
- добавить публичный метод загрузки свечей через канонический сервис;
|
||||||
|
- сохранить существующие Instrument Reference Data и Quotes Feed без изменений;
|
||||||
|
- не переключать `ExchangeService.get_klines()`;
|
||||||
|
- не удалять legacy `Kline` и `KlineBatch`;
|
||||||
|
- не изменять торговую логику;
|
||||||
|
- сохранить полную обратную совместимость.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Итог
|
||||||
|
|
||||||
|
Build 045 завершает регистрацию канонического Candles Feed.
|
||||||
|
|
||||||
|
Достигнуто состояние:
|
||||||
|
|
||||||
|
```text
|
||||||
|
OHLCV Feed foundation
|
||||||
|
✓ Candle model
|
||||||
|
✓ REST document source
|
||||||
|
✓ schema validation
|
||||||
|
✓ parser
|
||||||
|
✓ value validation
|
||||||
|
✓ mapper
|
||||||
|
✓ document handler
|
||||||
|
✓ Candles Feed
|
||||||
|
✓ CandlesFeedRegistry
|
||||||
|
☐ Acquisition Service integration
|
||||||
|
☐ ExchangeService compatibility bridge
|
||||||
|
☐ Consumer migration
|
||||||
|
☐ Legacy Kline removal
|
||||||
|
```
|
||||||
|
|
||||||
|
Build 045 является небольшим изолированным архитектурным шагом и не изменяет поведение существующего рабочего торгового бота.
|
||||||
Reference in New Issue
Block a user