build 059.3: parse websocket OHLC events
This commit is contained in:
@@ -19,10 +19,12 @@ from src.market_data.acquisition.adapters.dzengi.models import (
|
|||||||
DzengiRawNumeric,
|
DzengiRawNumeric,
|
||||||
DzengiUnknownFilter,
|
DzengiUnknownFilter,
|
||||||
DzengiTicker24hrResponse,
|
DzengiTicker24hrResponse,
|
||||||
|
DzengiWebSocketOhlcEvent,
|
||||||
DzengiWebSocketQuoteResponse,
|
DzengiWebSocketQuoteResponse,
|
||||||
)
|
)
|
||||||
from src.market_data.acquisition.exceptions import (
|
from src.market_data.acquisition.exceptions import (
|
||||||
CandleParseError,
|
CandleParseError,
|
||||||
|
CandleWebSocketParseError,
|
||||||
InstrumentReferenceParseError,
|
InstrumentReferenceParseError,
|
||||||
QuoteParseError,
|
QuoteParseError,
|
||||||
)
|
)
|
||||||
@@ -30,6 +32,7 @@ from src.market_data.acquisition.validation.schema import (
|
|||||||
ValidatedCandlesDocument,
|
ValidatedCandlesDocument,
|
||||||
ValidatedExchangeInfoDocument,
|
ValidatedExchangeInfoDocument,
|
||||||
ValidatedQuoteDocument,
|
ValidatedQuoteDocument,
|
||||||
|
ValidatedWebSocketOhlcDocument,
|
||||||
ValidatedWebSocketQuoteDocument,
|
ValidatedWebSocketQuoteDocument,
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -719,6 +722,101 @@ def _websocket_optional_timestamp(
|
|||||||
|
|
||||||
return _quote_required_int(value, path=path)
|
return _quote_required_int(value, path=path)
|
||||||
|
|
||||||
|
|
||||||
|
# Преобразовать структурно проверенное событие WebSocket OHLC
|
||||||
|
# в транспортную модель адаптера Dzengi.
|
||||||
|
def parse_dzengi_websocket_ohlc(
|
||||||
|
document: ValidatedWebSocketOhlcDocument,
|
||||||
|
) -> DzengiWebSocketOhlcEvent:
|
||||||
|
"""
|
||||||
|
Преобразовать проверенный payload события ``ohlc.event``
|
||||||
|
в transport-модель Dzengi.
|
||||||
|
|
||||||
|
Функция не выполняет предметную проверку значений, не проверяет
|
||||||
|
допустимость интервала или типа свечи, не преобразует timestamp
|
||||||
|
в datetime и не выполняет mapping во внутреннюю модель Dzentra.
|
||||||
|
"""
|
||||||
|
|
||||||
|
payload = document.payload
|
||||||
|
|
||||||
|
return DzengiWebSocketOhlcEvent(
|
||||||
|
symbol=_websocket_ohlc_required_string(
|
||||||
|
payload.get("symbol"),
|
||||||
|
path="$.payload.symbol",
|
||||||
|
),
|
||||||
|
interval=_websocket_ohlc_required_string(
|
||||||
|
payload.get("interval"),
|
||||||
|
path="$.payload.interval",
|
||||||
|
),
|
||||||
|
candle_type=_websocket_ohlc_required_string(
|
||||||
|
payload.get("type"),
|
||||||
|
path="$.payload.type",
|
||||||
|
),
|
||||||
|
open_time=_websocket_ohlc_required_int(
|
||||||
|
payload.get("t"),
|
||||||
|
path="$.payload.t",
|
||||||
|
),
|
||||||
|
open_price=_websocket_ohlc_required_raw_numeric(
|
||||||
|
payload.get("o"),
|
||||||
|
path="$.payload.o",
|
||||||
|
),
|
||||||
|
high_price=_websocket_ohlc_required_raw_numeric(
|
||||||
|
payload.get("h"),
|
||||||
|
path="$.payload.h",
|
||||||
|
),
|
||||||
|
low_price=_websocket_ohlc_required_raw_numeric(
|
||||||
|
payload.get("l"),
|
||||||
|
path="$.payload.l",
|
||||||
|
),
|
||||||
|
close_price=_websocket_ohlc_required_raw_numeric(
|
||||||
|
payload.get("c"),
|
||||||
|
path="$.payload.c",
|
||||||
|
),
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _websocket_ohlc_required_string(
|
||||||
|
value: object,
|
||||||
|
*,
|
||||||
|
path: str,
|
||||||
|
) -> str:
|
||||||
|
if not isinstance(value, str):
|
||||||
|
raise CandleWebSocketParseError(
|
||||||
|
f"{path} должен быть строкой, "
|
||||||
|
f"получен {type(value).__name__}."
|
||||||
|
)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _websocket_ohlc_required_int(
|
||||||
|
value: object,
|
||||||
|
*,
|
||||||
|
path: str,
|
||||||
|
) -> int:
|
||||||
|
if isinstance(value, bool) or not isinstance(value, int):
|
||||||
|
raise CandleWebSocketParseError(
|
||||||
|
f"{path} должен быть целым числом, "
|
||||||
|
f"получен {type(value).__name__}."
|
||||||
|
)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
|
def _websocket_ohlc_required_raw_numeric(
|
||||||
|
value: object,
|
||||||
|
*,
|
||||||
|
path: str,
|
||||||
|
) -> DzengiRawNumeric:
|
||||||
|
if isinstance(value, bool) or not isinstance(value, (str, int, float)):
|
||||||
|
raise CandleWebSocketParseError(
|
||||||
|
f"{path} должен быть строкой или числом, "
|
||||||
|
f"получен {type(value).__name__}."
|
||||||
|
)
|
||||||
|
|
||||||
|
return value
|
||||||
|
|
||||||
|
|
||||||
# Преобразовать структурно проверенный ответ klines в raw-модели Dzengi.
|
# Преобразовать структурно проверенный ответ klines в raw-модели Dzengi.
|
||||||
def parse_candles(
|
def parse_candles(
|
||||||
document: ValidatedCandlesDocument,
|
document: ValidatedCandlesDocument,
|
||||||
|
|||||||
@@ -83,6 +83,11 @@ class CandleWebSocketSchemaError(MarketDataAcquisitionError):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# Ошибка преобразования проверенного WebSocket OHLC в raw-модель адаптера.
|
||||||
|
class CandleWebSocketParseError(MarketDataAcquisitionError):
|
||||||
|
pass
|
||||||
|
|
||||||
|
|
||||||
# Ошибка преобразования проверенного документа в raw-модели свечей.
|
# Ошибка преобразования проверенного документа в raw-модели свечей.
|
||||||
class CandleParseError(MarketDataAcquisitionError):
|
class CandleParseError(MarketDataAcquisitionError):
|
||||||
pass
|
pass
|
||||||
|
|||||||
@@ -0,0 +1,200 @@
|
|||||||
|
# app/tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_parser.py
|
||||||
|
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
from types import MappingProxyType
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
from src.market_data.acquisition.adapters.dzengi.models import (
|
||||||
|
DzengiWebSocketOhlcEvent,
|
||||||
|
)
|
||||||
|
from src.market_data.acquisition.adapters.dzengi.parser import (
|
||||||
|
parse_dzengi_websocket_ohlc,
|
||||||
|
)
|
||||||
|
from src.market_data.acquisition.exceptions import (
|
||||||
|
CandleWebSocketParseError,
|
||||||
|
)
|
||||||
|
from src.market_data.acquisition.validation.schema import (
|
||||||
|
ValidatedWebSocketOhlcDocument,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _validated_document(
|
||||||
|
**payload_overrides: object,
|
||||||
|
) -> ValidatedWebSocketOhlcDocument:
|
||||||
|
payload: dict[str, object] = {
|
||||||
|
"symbol": "BTC/USD_LEVERAGE",
|
||||||
|
"interval": "1m",
|
||||||
|
"type": "classic",
|
||||||
|
"t": 1784224740000,
|
||||||
|
"o": 63992.0,
|
||||||
|
"h": 64032.55,
|
||||||
|
"l": 63984.0,
|
||||||
|
"c": 64032.55,
|
||||||
|
}
|
||||||
|
payload.update(payload_overrides)
|
||||||
|
|
||||||
|
return ValidatedWebSocketOhlcDocument(
|
||||||
|
payload=MappingProxyType(payload),
|
||||||
|
status="OK",
|
||||||
|
destination="ohlc.event",
|
||||||
|
correlation_id=None,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_websocket_ohlc_returns_transport_model() -> None:
|
||||||
|
result = parse_dzengi_websocket_ohlc(
|
||||||
|
_validated_document()
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result == DzengiWebSocketOhlcEvent(
|
||||||
|
symbol="BTC/USD_LEVERAGE",
|
||||||
|
interval="1m",
|
||||||
|
candle_type="classic",
|
||||||
|
open_time=1784224740000,
|
||||||
|
open_price=63992.0,
|
||||||
|
high_price=64032.55,
|
||||||
|
low_price=63984.0,
|
||||||
|
close_price=64032.55,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_websocket_ohlc_preserves_heikin_ashi_type() -> None:
|
||||||
|
result = parse_dzengi_websocket_ohlc(
|
||||||
|
_validated_document(type="heikin-ashi")
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.candle_type == "heikin-ashi"
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_websocket_ohlc_preserves_raw_numeric_types() -> None:
|
||||||
|
result = parse_dzengi_websocket_ohlc(
|
||||||
|
_validated_document(
|
||||||
|
o="63992.00",
|
||||||
|
h=64033,
|
||||||
|
l=63984.0,
|
||||||
|
c="64032.55",
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.open_price == "63992.00"
|
||||||
|
assert result.high_price == 64033
|
||||||
|
assert result.low_price == 63984.0
|
||||||
|
assert result.close_price == "64032.55"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
("field_name", "value"),
|
||||||
|
[
|
||||||
|
("symbol", None),
|
||||||
|
("symbol", 123),
|
||||||
|
("interval", None),
|
||||||
|
("interval", []),
|
||||||
|
("type", None),
|
||||||
|
("type", True),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_parse_websocket_ohlc_rejects_non_string_fields(
|
||||||
|
field_name: str,
|
||||||
|
value: object,
|
||||||
|
) -> None:
|
||||||
|
with pytest.raises(
|
||||||
|
CandleWebSocketParseError,
|
||||||
|
match=rf"\$\.payload\.{field_name} должен быть строкой",
|
||||||
|
):
|
||||||
|
parse_dzengi_websocket_ohlc(
|
||||||
|
_validated_document(**{field_name: value})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"value",
|
||||||
|
[
|
||||||
|
None,
|
||||||
|
True,
|
||||||
|
1784224740000.0,
|
||||||
|
"1784224740000",
|
||||||
|
[],
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_parse_websocket_ohlc_rejects_invalid_open_time(
|
||||||
|
value: object,
|
||||||
|
) -> None:
|
||||||
|
with pytest.raises(
|
||||||
|
CandleWebSocketParseError,
|
||||||
|
match=r"\$\.payload\.t должен быть целым числом",
|
||||||
|
):
|
||||||
|
parse_dzengi_websocket_ohlc(
|
||||||
|
_validated_document(t=value)
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"field_name",
|
||||||
|
[
|
||||||
|
"o",
|
||||||
|
"h",
|
||||||
|
"l",
|
||||||
|
"c",
|
||||||
|
],
|
||||||
|
)
|
||||||
|
@pytest.mark.parametrize(
|
||||||
|
"value",
|
||||||
|
[
|
||||||
|
None,
|
||||||
|
True,
|
||||||
|
[],
|
||||||
|
{},
|
||||||
|
object(),
|
||||||
|
],
|
||||||
|
)
|
||||||
|
def test_parse_websocket_ohlc_rejects_invalid_raw_numeric(
|
||||||
|
field_name: str,
|
||||||
|
value: object,
|
||||||
|
) -> None:
|
||||||
|
with pytest.raises(
|
||||||
|
CandleWebSocketParseError,
|
||||||
|
match=rf"\$\.payload\.{field_name} должен быть строкой или числом",
|
||||||
|
):
|
||||||
|
parse_dzengi_websocket_ohlc(
|
||||||
|
_validated_document(**{field_name: value})
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_websocket_ohlc_does_not_validate_subject_values() -> None:
|
||||||
|
result = parse_dzengi_websocket_ohlc(
|
||||||
|
_validated_document(
|
||||||
|
symbol="",
|
||||||
|
interval="unsupported",
|
||||||
|
type="unknown",
|
||||||
|
t=-1,
|
||||||
|
o="NaN",
|
||||||
|
h=-10,
|
||||||
|
l=100,
|
||||||
|
c=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
|
||||||
|
assert result.symbol == ""
|
||||||
|
assert result.interval == "unsupported"
|
||||||
|
assert result.candle_type == "unknown"
|
||||||
|
assert result.open_time == -1
|
||||||
|
assert result.open_price == "NaN"
|
||||||
|
assert result.high_price == -10
|
||||||
|
assert result.low_price == 100
|
||||||
|
assert result.close_price == 0
|
||||||
|
|
||||||
|
|
||||||
|
def test_parse_websocket_ohlc_ignores_envelope_metadata() -> None:
|
||||||
|
document = ValidatedWebSocketOhlcDocument(
|
||||||
|
payload=_validated_document().payload,
|
||||||
|
status=123,
|
||||||
|
destination=None,
|
||||||
|
correlation_id={"unexpected": "value"},
|
||||||
|
)
|
||||||
|
|
||||||
|
result = parse_dzengi_websocket_ohlc(document)
|
||||||
|
|
||||||
|
assert result.symbol == "BTC/USD_LEVERAGE"
|
||||||
|
assert result.interval == "1m"
|
||||||
593
docs/migrations/build_059_3.md
Normal file
593
docs/migrations/build_059_3.md
Normal file
@@ -0,0 +1,593 @@
|
|||||||
|
# Build 059.3 — WebSocket OHLC Parser
|
||||||
|
|
||||||
|
**Проект:** Dzentra
|
||||||
|
**Подсистема:** Market Data Acquisition
|
||||||
|
**Этап:** 059.3
|
||||||
|
**Статус:** Completed
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Цель
|
||||||
|
|
||||||
|
Добавить parser для структурно проверенного события Dzengi WebSocket OHLC.
|
||||||
|
|
||||||
|
Parser должен преобразовывать:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ValidatedWebSocketOhlcDocument
|
||||||
|
```
|
||||||
|
|
||||||
|
в:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DzengiWebSocketOhlcEvent
|
||||||
|
```
|
||||||
|
|
||||||
|
При этом parser не должен выполнять:
|
||||||
|
|
||||||
|
- предметную проверку значений;
|
||||||
|
- проверку поддерживаемых интервалов;
|
||||||
|
- проверку допустимого типа свечи;
|
||||||
|
- преобразование timestamp в `datetime`;
|
||||||
|
- преобразование цен в `Decimal`;
|
||||||
|
- проверку OHLC-инвариантов;
|
||||||
|
- mapping во внутреннюю модель Dzentra.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Причина изменения
|
||||||
|
|
||||||
|
После Build 059.2 проект умеет проверять структуру сообщения Dzengi WebSocket:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"status": "OK",
|
||||||
|
"destination": "ohlc.event",
|
||||||
|
"payload": {
|
||||||
|
"symbol": "BTC/USD_LEVERAGE",
|
||||||
|
"interval": "1m",
|
||||||
|
"type": "classic",
|
||||||
|
"t": 1784224740000,
|
||||||
|
"o": 63992.0,
|
||||||
|
"h": 64032.55,
|
||||||
|
"l": 63984.0,
|
||||||
|
"c": 64032.55
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Однако структурно проверенный документ ещё не преобразовывался в transport-модель адаптера Dzengi.
|
||||||
|
|
||||||
|
Build 059.3 добавляет отдельный parsing-слой между schema validation и value validation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Реализованные изменения
|
||||||
|
|
||||||
|
## Parser
|
||||||
|
|
||||||
|
В файл:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/market_data/acquisition/adapters/dzengi/parser.py
|
||||||
|
```
|
||||||
|
|
||||||
|
добавлена функция:
|
||||||
|
|
||||||
|
```text
|
||||||
|
parse_dzengi_websocket_ohlc()
|
||||||
|
```
|
||||||
|
|
||||||
|
Она принимает:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ValidatedWebSocketOhlcDocument
|
||||||
|
```
|
||||||
|
|
||||||
|
и возвращает:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DzengiWebSocketOhlcEvent
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Исключение parser-слоя
|
||||||
|
|
||||||
|
В файл:
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/market_data/acquisition/exceptions.py
|
||||||
|
```
|
||||||
|
|
||||||
|
добавлено исключение:
|
||||||
|
|
||||||
|
```text
|
||||||
|
CandleWebSocketParseError
|
||||||
|
```
|
||||||
|
|
||||||
|
Оно используется только при невозможности преобразовать структурно проверенный WebSocket OHLC-документ в transport-модель адаптера.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Unit-тесты
|
||||||
|
|
||||||
|
Создан файл:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_parser.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Он покрывает успешное преобразование и ошибки транспортных типов.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Соответствие полей
|
||||||
|
|
||||||
|
Parser выполняет следующее преобразование:
|
||||||
|
|
||||||
|
| WebSocket payload | Transport model |
|
||||||
|
|---|---|
|
||||||
|
| `symbol` | `symbol` |
|
||||||
|
| `interval` | `interval` |
|
||||||
|
| `type` | `candle_type` |
|
||||||
|
| `t` | `open_time` |
|
||||||
|
| `o` | `open_price` |
|
||||||
|
| `h` | `high_price` |
|
||||||
|
| `l` | `low_price` |
|
||||||
|
| `c` | `close_price` |
|
||||||
|
|
||||||
|
Parser не переименовывает и не интерпретирует значения предметно. Он только переносит их в явную transport-модель адаптера.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Проверка базовых транспортных типов
|
||||||
|
|
||||||
|
## Строковые поля
|
||||||
|
|
||||||
|
Следующие поля обязаны быть строками:
|
||||||
|
|
||||||
|
```text
|
||||||
|
symbol
|
||||||
|
interval
|
||||||
|
type
|
||||||
|
```
|
||||||
|
|
||||||
|
Если одно из них не является строкой, выбрасывается:
|
||||||
|
|
||||||
|
```text
|
||||||
|
CandleWebSocketParseError
|
||||||
|
```
|
||||||
|
|
||||||
|
Пример ошибки:
|
||||||
|
|
||||||
|
```text
|
||||||
|
$.payload.symbol должен быть строкой
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Timestamp
|
||||||
|
|
||||||
|
Поле:
|
||||||
|
|
||||||
|
```text
|
||||||
|
t
|
||||||
|
```
|
||||||
|
|
||||||
|
должно быть целым числом Python:
|
||||||
|
|
||||||
|
```text
|
||||||
|
int
|
||||||
|
```
|
||||||
|
|
||||||
|
Не принимаются:
|
||||||
|
|
||||||
|
```text
|
||||||
|
None
|
||||||
|
bool
|
||||||
|
float
|
||||||
|
str
|
||||||
|
list
|
||||||
|
```
|
||||||
|
|
||||||
|
`bool` отклоняется отдельно, несмотря на то что в Python он является подклассом `int`.
|
||||||
|
|
||||||
|
Parser не проверяет, является ли timestamp положительным или реалистичным. Это относится к Value Validation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Поля OHLC
|
||||||
|
|
||||||
|
Поля:
|
||||||
|
|
||||||
|
```text
|
||||||
|
o
|
||||||
|
h
|
||||||
|
l
|
||||||
|
c
|
||||||
|
```
|
||||||
|
|
||||||
|
могут быть представлены как:
|
||||||
|
|
||||||
|
```text
|
||||||
|
str
|
||||||
|
int
|
||||||
|
float
|
||||||
|
```
|
||||||
|
|
||||||
|
Это соответствует существующему транспортному типу:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DzengiRawNumeric
|
||||||
|
```
|
||||||
|
|
||||||
|
Такой контракт необходим, поскольку внешнее API может передавать числовые значения как JSON-числа или строки.
|
||||||
|
|
||||||
|
Не принимаются:
|
||||||
|
|
||||||
|
```text
|
||||||
|
None
|
||||||
|
bool
|
||||||
|
list
|
||||||
|
dict
|
||||||
|
object
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Сохранение исходных числовых типов
|
||||||
|
|
||||||
|
Parser не преобразует цены.
|
||||||
|
|
||||||
|
Например, документ:
|
||||||
|
|
||||||
|
```json
|
||||||
|
{
|
||||||
|
"o": "63992.00",
|
||||||
|
"h": 64033,
|
||||||
|
"l": 63984.0,
|
||||||
|
"c": "64032.55"
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
преобразуется в transport-модель с теми же исходными типами:
|
||||||
|
|
||||||
|
```text
|
||||||
|
open_price = str
|
||||||
|
high_price = int
|
||||||
|
low_price = float
|
||||||
|
close_price = str
|
||||||
|
```
|
||||||
|
|
||||||
|
Преобразование в `Decimal` будет выполняться только на mapping-этапе.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Поддержка типов свечей
|
||||||
|
|
||||||
|
Parser сохраняет значение поля:
|
||||||
|
|
||||||
|
```text
|
||||||
|
type
|
||||||
|
```
|
||||||
|
|
||||||
|
в поле transport-модели:
|
||||||
|
|
||||||
|
```text
|
||||||
|
candle_type
|
||||||
|
```
|
||||||
|
|
||||||
|
Поддерживаются на уровне базового транспортного типа любые строки.
|
||||||
|
|
||||||
|
Например:
|
||||||
|
|
||||||
|
```text
|
||||||
|
classic
|
||||||
|
heikin-ashi
|
||||||
|
unknown
|
||||||
|
```
|
||||||
|
|
||||||
|
Parser не определяет допустимость значения.
|
||||||
|
|
||||||
|
Проверка, что значение входит в подтверждённый runtime-набор:
|
||||||
|
|
||||||
|
```text
|
||||||
|
classic
|
||||||
|
heikin-ashi
|
||||||
|
```
|
||||||
|
|
||||||
|
будет реализована в Build 059.4 — Value Validation.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Отсутствие предметной валидации
|
||||||
|
|
||||||
|
Parser намеренно пропускает значения, которые имеют допустимый транспортный тип, но являются предметно некорректными.
|
||||||
|
|
||||||
|
Пример:
|
||||||
|
|
||||||
|
```text
|
||||||
|
symbol = ""
|
||||||
|
interval = "unsupported"
|
||||||
|
candle_type = "unknown"
|
||||||
|
open_time = -1
|
||||||
|
open_price = "NaN"
|
||||||
|
high_price = -10
|
||||||
|
low_price = 100
|
||||||
|
close_price = 0
|
||||||
|
```
|
||||||
|
|
||||||
|
Такой документ успешно преобразуется в `DzengiWebSocketOhlcEvent`.
|
||||||
|
|
||||||
|
Это ожидаемое поведение.
|
||||||
|
|
||||||
|
Следующий слой должен проверить:
|
||||||
|
|
||||||
|
- непустой символ;
|
||||||
|
- допустимый интервал;
|
||||||
|
- допустимый тип свечи;
|
||||||
|
- положительный timestamp;
|
||||||
|
- конечность чисел;
|
||||||
|
- положительность цен;
|
||||||
|
- OHLC-инварианты.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Игнорирование metadata envelope
|
||||||
|
|
||||||
|
Parser использует только:
|
||||||
|
|
||||||
|
```text
|
||||||
|
document.payload
|
||||||
|
```
|
||||||
|
|
||||||
|
Поля внешнего envelope:
|
||||||
|
|
||||||
|
```text
|
||||||
|
status
|
||||||
|
destination
|
||||||
|
correlation_id
|
||||||
|
```
|
||||||
|
|
||||||
|
не участвуют в создании `DzengiWebSocketOhlcEvent`.
|
||||||
|
|
||||||
|
Это обеспечивает разделение ответственности:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Schema Validation
|
||||||
|
↓
|
||||||
|
проверяет структуру envelope и payload
|
||||||
|
|
||||||
|
Parser
|
||||||
|
↓
|
||||||
|
преобразует payload в transport-модель
|
||||||
|
|
||||||
|
Value Validation
|
||||||
|
↓
|
||||||
|
проверяет предметную допустимость значений
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Новый конвейер
|
||||||
|
|
||||||
|
После Build 059.3 подготовленная часть конвейера выглядит так:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Raw WebSocket message
|
||||||
|
↓
|
||||||
|
validate_dzengi_websocket_ohlc_schema()
|
||||||
|
↓
|
||||||
|
ValidatedWebSocketOhlcDocument
|
||||||
|
↓
|
||||||
|
parse_dzengi_websocket_ohlc()
|
||||||
|
↓
|
||||||
|
DzengiWebSocketOhlcEvent
|
||||||
|
↓
|
||||||
|
Value Validation
|
||||||
|
↓
|
||||||
|
Internal Close Event
|
||||||
|
↓
|
||||||
|
REST reconciliation
|
||||||
|
↓
|
||||||
|
Canonical Candle
|
||||||
|
```
|
||||||
|
|
||||||
|
На текущем этапе реализована часть до `DzengiWebSocketOhlcEvent` включительно.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Unit Tests
|
||||||
|
|
||||||
|
Добавлен файл:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_parser.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Проверяются следующие сценарии:
|
||||||
|
|
||||||
|
- успешное создание transport-модели;
|
||||||
|
- корректное соответствие всех полей;
|
||||||
|
- сохранение типа `classic`;
|
||||||
|
- сохранение типа `heikin-ashi`;
|
||||||
|
- сохранение исходных числовых типов;
|
||||||
|
- отклонение нестрокового `symbol`;
|
||||||
|
- отклонение нестрокового `interval`;
|
||||||
|
- отклонение нестрокового `type`;
|
||||||
|
- отклонение `None` в timestamp;
|
||||||
|
- отклонение `bool` в timestamp;
|
||||||
|
- отклонение `float` в timestamp;
|
||||||
|
- отклонение строкового timestamp;
|
||||||
|
- отклонение коллекции вместо timestamp;
|
||||||
|
- отклонение некорректных типов для `o`;
|
||||||
|
- отклонение некорректных типов для `h`;
|
||||||
|
- отклонение некорректных типов для `l`;
|
||||||
|
- отклонение некорректных типов для `c`;
|
||||||
|
- отсутствие предметной валидации;
|
||||||
|
- игнорирование metadata внешнего envelope.
|
||||||
|
|
||||||
|
Всего выполнено:
|
||||||
|
|
||||||
|
```text
|
||||||
|
36 tests
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Проверка синтаксиса
|
||||||
|
|
||||||
|
Выполнена команда:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m compileall \
|
||||||
|
src/market_data/acquisition/exceptions.py \
|
||||||
|
src/market_data/acquisition/adapters/dzengi/parser.py \
|
||||||
|
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_parser.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Результат:
|
||||||
|
|
||||||
|
```text
|
||||||
|
успешно
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Целевые тесты
|
||||||
|
|
||||||
|
Выполнена команда:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pytest -q \
|
||||||
|
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_parser.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Результат:
|
||||||
|
|
||||||
|
```text
|
||||||
|
36 passed in 0.03s
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Регрессия parser-слоя
|
||||||
|
|
||||||
|
Выполнена команда:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
python -m pytest -q \
|
||||||
|
tests/unit/market_data/acquisition/adapters/dzengi/test_parser.py \
|
||||||
|
tests/unit/market_data/acquisition/adapters/dzengi/test_quote_parser.py \
|
||||||
|
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_quote_parser.py \
|
||||||
|
tests/unit/market_data/acquisition/adapters/dzengi/test_candle_parser.py \
|
||||||
|
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_parser.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Результат:
|
||||||
|
|
||||||
|
```text
|
||||||
|
80 passed in 0.05s
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Проверка форматирования
|
||||||
|
|
||||||
|
Выполнена команда:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git diff --check
|
||||||
|
```
|
||||||
|
|
||||||
|
Ошибок форматирования не обнаружено.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Изменённые файлы
|
||||||
|
|
||||||
|
```text
|
||||||
|
src/market_data/acquisition/adapters/dzengi/parser.py
|
||||||
|
src/market_data/acquisition/exceptions.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Создан файл:
|
||||||
|
|
||||||
|
```text
|
||||||
|
tests/unit/market_data/acquisition/adapters/dzengi/test_websocket_ohlc_parser.py
|
||||||
|
```
|
||||||
|
|
||||||
|
Документация этапа:
|
||||||
|
|
||||||
|
```text
|
||||||
|
docs/migrations/build_059_3.md
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Совместимость
|
||||||
|
|
||||||
|
Изменение полностью обратно совместимо.
|
||||||
|
|
||||||
|
Не изменены:
|
||||||
|
|
||||||
|
- REST Candles Feed;
|
||||||
|
- REST parser свечей;
|
||||||
|
- Quotes Feed;
|
||||||
|
- WebSocket Quote parser;
|
||||||
|
- WebSocket Quote Adapter;
|
||||||
|
- ExchangeService;
|
||||||
|
- Market Analysis;
|
||||||
|
- Trading;
|
||||||
|
- runtime.
|
||||||
|
|
||||||
|
Новый parser пока не подключён к рабочему WebSocket runtime и не влияет на действующий бот.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Ограничения этапа
|
||||||
|
|
||||||
|
Build 059.3 не выполняет:
|
||||||
|
|
||||||
|
- проверку значения `status`;
|
||||||
|
- проверку значения `destination`;
|
||||||
|
- проверку допустимых интервалов;
|
||||||
|
- проверку допустимого типа свечи;
|
||||||
|
- проверку timestamp;
|
||||||
|
- проверку конечности OHLC;
|
||||||
|
- проверку положительности OHLC;
|
||||||
|
- проверку OHLC-инвариантов;
|
||||||
|
- преобразование в `Decimal`;
|
||||||
|
- преобразование времени в UTC `datetime`;
|
||||||
|
- создание внутреннего close event;
|
||||||
|
- REST reconciliation;
|
||||||
|
- выпуск канонической `Candle`.
|
||||||
|
|
||||||
|
Эти обязанности остаются за последующими подпунктами Build 059.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
# Итог
|
||||||
|
|
||||||
|
Build 059.3 завершает parsing-слой Dzengi WebSocket OHLC.
|
||||||
|
|
||||||
|
Проект теперь умеет преобразовывать структурно проверенный документ:
|
||||||
|
|
||||||
|
```text
|
||||||
|
ValidatedWebSocketOhlcDocument
|
||||||
|
```
|
||||||
|
|
||||||
|
в transport-модель:
|
||||||
|
|
||||||
|
```text
|
||||||
|
DzengiWebSocketOhlcEvent
|
||||||
|
```
|
||||||
|
|
||||||
|
без смешивания parsing, value validation и domain mapping.
|
||||||
|
|
||||||
|
Следующий этап:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Build 059.4 — Value Validation
|
||||||
|
```
|
||||||
Reference in New Issue
Block a user