build 048: switch market analysis consumers to canonical Candle model
This commit is contained in:
@@ -3,59 +3,90 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from math import isfinite
|
||||
|
||||
from src.core.numbers import safe_float
|
||||
from src.core.types import NumericLike
|
||||
from src.integrations.exchange.models import Kline
|
||||
from src.market_data.acquisition.models.candle import Candle
|
||||
from src.trading.market_analysis.models import VolatilityState
|
||||
|
||||
|
||||
def atr(candles: list[Kline], period: int = 14) -> float | None:
|
||||
def atr(
|
||||
candles: Sequence[Candle],
|
||||
period: int = 14,
|
||||
) -> float | None:
|
||||
if period <= 0 or len(candles) < period + 1:
|
||||
return None
|
||||
|
||||
true_ranges: list[float] = []
|
||||
|
||||
for previous, current in zip(candles, candles[1:]):
|
||||
high_low = current.high_price - current.low_price
|
||||
high_close = abs(current.high_price - previous.close_price)
|
||||
low_close = abs(current.low_price - previous.close_price)
|
||||
previous_close = safe_float(previous.close_price)
|
||||
current_high = safe_float(current.high_price)
|
||||
current_low = safe_float(current.low_price)
|
||||
|
||||
true_ranges.append(max(high_low, high_close, low_close))
|
||||
if (
|
||||
previous_close is None
|
||||
or current_high is None
|
||||
or current_low is None
|
||||
or not isfinite(previous_close)
|
||||
or not isfinite(current_high)
|
||||
or not isfinite(current_low)
|
||||
):
|
||||
continue
|
||||
|
||||
high_low = current_high - current_low
|
||||
high_close = abs(current_high - previous_close)
|
||||
low_close = abs(current_low - previous_close)
|
||||
|
||||
true_ranges.append(
|
||||
max(
|
||||
high_low,
|
||||
high_close,
|
||||
low_close,
|
||||
)
|
||||
)
|
||||
|
||||
if len(true_ranges) < period:
|
||||
return None
|
||||
|
||||
recent = true_ranges[-period:]
|
||||
|
||||
return sum(recent) / period
|
||||
|
||||
|
||||
def atr_percent_baseline(
|
||||
*,
|
||||
candles: Sequence[Kline],
|
||||
candles: Sequence[Candle],
|
||||
close_price: float,
|
||||
atr_period: int,
|
||||
atr_baseline_window: int,
|
||||
) -> float | None:
|
||||
if close_price <= 0:
|
||||
if not isfinite(close_price) or close_price <= 0:
|
||||
return None
|
||||
|
||||
values: list[float] = []
|
||||
window: list[Kline] = list(candles[-atr_baseline_window:])
|
||||
window = list(candles[-atr_baseline_window:])
|
||||
|
||||
for index in range(atr_period, len(window) + 1):
|
||||
part: list[Kline] = window[:index]
|
||||
atr_value = atr(list(part), atr_period)
|
||||
part = window[:index]
|
||||
atr_value = atr(part, atr_period)
|
||||
|
||||
if atr_value is None:
|
||||
if atr_value is None or not isfinite(atr_value):
|
||||
continue
|
||||
|
||||
close = getattr(part[-1], "close_price", None)
|
||||
close = safe_float(part[-1].close_price)
|
||||
|
||||
if close is None or close <= 0:
|
||||
if (
|
||||
close is None
|
||||
or not isfinite(close)
|
||||
or close <= 0
|
||||
):
|
||||
continue
|
||||
|
||||
values.append((atr_value / close) * 100)
|
||||
values.append(
|
||||
(atr_value / close) * 100
|
||||
)
|
||||
|
||||
if not values:
|
||||
return None
|
||||
@@ -66,7 +97,10 @@ def atr_percent_baseline(
|
||||
if len(values) % 2 == 1:
|
||||
return values[middle]
|
||||
|
||||
return (values[middle - 1] + values[middle]) / 2
|
||||
return (
|
||||
values[middle - 1]
|
||||
+ values[middle]
|
||||
) / 2
|
||||
|
||||
|
||||
def adaptive_threshold(
|
||||
@@ -79,10 +113,20 @@ def adaptive_threshold(
|
||||
multiplier_value = safe_float(multiplier)
|
||||
minimum_value = safe_float(minimum) or 0.0
|
||||
|
||||
if atr_value is None or atr_value <= 0 or multiplier_value is None:
|
||||
return minimum_value
|
||||
if (
|
||||
atr_value is None
|
||||
or multiplier_value is None
|
||||
or not isfinite(atr_value)
|
||||
or not isfinite(multiplier_value)
|
||||
or not isfinite(minimum_value)
|
||||
or atr_value <= 0
|
||||
):
|
||||
return minimum_value if isfinite(minimum_value) else 0.0
|
||||
|
||||
return max(minimum_value, atr_value * multiplier_value)
|
||||
return max(
|
||||
minimum_value,
|
||||
atr_value * multiplier_value,
|
||||
)
|
||||
|
||||
|
||||
def classify_volatility(
|
||||
@@ -95,22 +139,50 @@ def classify_volatility(
|
||||
) -> VolatilityState:
|
||||
atr_value = safe_float(atr_percent)
|
||||
|
||||
if atr_value is None or atr_value <= 0:
|
||||
if (
|
||||
atr_value is None
|
||||
or not isfinite(atr_value)
|
||||
or atr_value <= 0
|
||||
):
|
||||
return VolatilityState.UNKNOWN
|
||||
|
||||
local_ratio = safe_float(volatility_ratio)
|
||||
htf_ratio = safe_float(htf_volatility_ratio)
|
||||
|
||||
if local_ratio is not None and not isfinite(local_ratio):
|
||||
local_ratio = None
|
||||
|
||||
if htf_ratio is not None and not isfinite(htf_ratio):
|
||||
htf_ratio = None
|
||||
|
||||
if htf_ratio is not None:
|
||||
if htf_ratio > 1.8 and (local_ratio is None or local_ratio > 1.1):
|
||||
if (
|
||||
htf_ratio > 1.8
|
||||
and (
|
||||
local_ratio is None
|
||||
or local_ratio > 1.1
|
||||
)
|
||||
):
|
||||
return VolatilityState.HIGH
|
||||
|
||||
if htf_ratio < 0.55 and (local_ratio is None or local_ratio < 0.85):
|
||||
if (
|
||||
htf_ratio < 0.55
|
||||
and (
|
||||
local_ratio is None
|
||||
or local_ratio < 0.85
|
||||
)
|
||||
):
|
||||
return VolatilityState.LOW
|
||||
|
||||
if local_ratio is None:
|
||||
low_value = safe_float(low_volatility_atr_percent) or 0.05
|
||||
high_value = safe_float(high_volatility_atr_percent) or 1.8
|
||||
low_value = safe_float(low_volatility_atr_percent)
|
||||
high_value = safe_float(high_volatility_atr_percent)
|
||||
|
||||
if low_value is None or not isfinite(low_value):
|
||||
low_value = 0.05
|
||||
|
||||
if high_value is None or not isfinite(high_value):
|
||||
high_value = 1.8
|
||||
|
||||
if atr_value < low_value:
|
||||
return VolatilityState.LOW
|
||||
|
||||
@@ -3,13 +3,15 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Sequence
|
||||
from math import isfinite
|
||||
|
||||
from src.integrations.exchange.models import Kline
|
||||
from src.core.numbers import safe_float
|
||||
from src.market_data.acquisition.models.candle import Candle
|
||||
from src.trading.market_analysis.models import TrendDirection
|
||||
|
||||
|
||||
def candle_noise_score(
|
||||
candles: Sequence[Kline],
|
||||
candles: Sequence[Candle],
|
||||
*,
|
||||
candle_noise_window: int,
|
||||
min_clean_body_ratio: float,
|
||||
@@ -23,16 +25,20 @@ def candle_noise_score(
|
||||
total_count = 0
|
||||
|
||||
for candle in window:
|
||||
high = getattr(candle, "high_price", None)
|
||||
low = getattr(candle, "low_price", None)
|
||||
open_price = getattr(candle, "open_price", None)
|
||||
close_price = getattr(candle, "close_price", None)
|
||||
high = safe_float(candle.high_price)
|
||||
low = safe_float(candle.low_price)
|
||||
open_price = safe_float(candle.open_price)
|
||||
close_price = safe_float(candle.close_price)
|
||||
|
||||
if (
|
||||
high is None
|
||||
or low is None
|
||||
or open_price is None
|
||||
or close_price is None
|
||||
or not isfinite(high)
|
||||
or not isfinite(low)
|
||||
or not isfinite(open_price)
|
||||
or not isfinite(close_price)
|
||||
or high <= low
|
||||
):
|
||||
continue
|
||||
|
||||
@@ -6,7 +6,7 @@ from collections.abc import Sequence
|
||||
|
||||
from src.core.numbers import safe_float
|
||||
from src.core.types import NumericLike
|
||||
from src.integrations.exchange.models import Kline
|
||||
from src.market_data.acquisition.models.candle import Candle
|
||||
from src.trading.market_analysis.models import MarketStructure
|
||||
|
||||
|
||||
@@ -47,12 +47,12 @@ def structure_params(
|
||||
return window, left, right
|
||||
|
||||
|
||||
# определить структуру рынка по swing high / swing low:
|
||||
# Определить структуру рынка по swing high / swing low:
|
||||
# HH/HL = восходящая структура
|
||||
# LH/LL = нисходящая структура
|
||||
# MIXED = противоречивая структура
|
||||
def market_structure(
|
||||
candles: Sequence[Kline],
|
||||
candles: Sequence[Candle],
|
||||
*,
|
||||
atr_percent: NumericLike | None = None,
|
||||
candle_noise_score: NumericLike | None = None,
|
||||
|
||||
@@ -0,0 +1,136 @@
|
||||
# app/tests/unit/trading/market_analysis/indicators/test_volatility_candle.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
from src.market_data.acquisition.models.candle import Candle
|
||||
|
||||
|
||||
def _candle(
|
||||
*,
|
||||
index: int = 0,
|
||||
open_price: str = "100",
|
||||
high_price: str = "110",
|
||||
low_price: str = "90",
|
||||
close_price: str = "105",
|
||||
volume: str = "10",
|
||||
) -> Candle:
|
||||
return Candle(
|
||||
symbol="BTC/USD_LEVERAGE",
|
||||
interval="1m",
|
||||
open_time=datetime.fromtimestamp(
|
||||
1_750_000_000 + index * 60,
|
||||
tz=timezone.utc,
|
||||
),
|
||||
open_price=Decimal(open_price),
|
||||
high_price=Decimal(high_price),
|
||||
low_price=Decimal(low_price),
|
||||
close_price=Decimal(close_price),
|
||||
volume=Decimal(volume),
|
||||
source="test",
|
||||
)
|
||||
|
||||
|
||||
from src.trading.market_analysis.indicators.volatility import (
|
||||
atr,
|
||||
atr_percent_baseline,
|
||||
)
|
||||
|
||||
|
||||
def test_atr_accepts_canonical_candles_with_decimal_values() -> None:
|
||||
candles = [
|
||||
_candle(
|
||||
index=0,
|
||||
high_price="105",
|
||||
low_price="95",
|
||||
close_price="100",
|
||||
),
|
||||
_candle(
|
||||
index=1,
|
||||
high_price="110",
|
||||
low_price="95",
|
||||
close_price="105",
|
||||
),
|
||||
_candle(
|
||||
index=2,
|
||||
high_price="112",
|
||||
low_price="100",
|
||||
close_price="108",
|
||||
),
|
||||
]
|
||||
|
||||
result = atr(candles, period=2)
|
||||
|
||||
assert result == 13.5
|
||||
assert isinstance(result, float)
|
||||
|
||||
|
||||
def test_atr_returns_none_when_candles_are_insufficient() -> None:
|
||||
candles = [
|
||||
_candle(index=0),
|
||||
_candle(index=1),
|
||||
]
|
||||
|
||||
assert atr(candles, period=2) is None
|
||||
|
||||
|
||||
def test_atr_returns_none_for_non_positive_period() -> None:
|
||||
candles = [
|
||||
_candle(index=0),
|
||||
_candle(index=1),
|
||||
]
|
||||
|
||||
assert atr(candles, period=0) is None
|
||||
|
||||
|
||||
def test_atr_skips_pair_with_non_finite_decimal_value() -> None:
|
||||
candles = [
|
||||
_candle(index=0, close_price="100"),
|
||||
_candle(index=1, high_price="NaN", low_price="95"),
|
||||
_candle(index=2, high_price="112", low_price="100"),
|
||||
]
|
||||
|
||||
assert atr(candles, period=2) is None
|
||||
|
||||
|
||||
def test_atr_percent_baseline_accepts_decimal_candles() -> None:
|
||||
candles = [
|
||||
_candle(
|
||||
index=index,
|
||||
open_price=str(100 + index),
|
||||
high_price=str(105 + index),
|
||||
low_price=str(95 + index),
|
||||
close_price=str(101 + index),
|
||||
)
|
||||
for index in range(8)
|
||||
]
|
||||
|
||||
result = atr_percent_baseline(
|
||||
candles=candles,
|
||||
close_price=108.0,
|
||||
atr_period=2,
|
||||
atr_baseline_window=8,
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert isinstance(result, float)
|
||||
assert result > 0
|
||||
|
||||
|
||||
def test_atr_percent_baseline_returns_none_for_invalid_close_price() -> None:
|
||||
candles = [
|
||||
_candle(index=index)
|
||||
for index in range(5)
|
||||
]
|
||||
|
||||
assert (
|
||||
atr_percent_baseline(
|
||||
candles=candles,
|
||||
close_price=0.0,
|
||||
atr_period=2,
|
||||
atr_baseline_window=5,
|
||||
)
|
||||
is None
|
||||
)
|
||||
154
app/tests/unit/trading/market_analysis/test_quality_candle.py
Normal file
154
app/tests/unit/trading/market_analysis/test_quality_candle.py
Normal file
@@ -0,0 +1,154 @@
|
||||
# app/tests/unit/trading/market_analysis/test_quality_candle.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
from src.market_data.acquisition.models.candle import Candle
|
||||
|
||||
|
||||
def _candle(
|
||||
*,
|
||||
index: int = 0,
|
||||
open_price: str = "100",
|
||||
high_price: str = "110",
|
||||
low_price: str = "90",
|
||||
close_price: str = "105",
|
||||
volume: str = "10",
|
||||
) -> Candle:
|
||||
return Candle(
|
||||
symbol="BTC/USD_LEVERAGE",
|
||||
interval="1m",
|
||||
open_time=datetime.fromtimestamp(
|
||||
1_750_000_000 + index * 60,
|
||||
tz=timezone.utc,
|
||||
),
|
||||
open_price=Decimal(open_price),
|
||||
high_price=Decimal(high_price),
|
||||
low_price=Decimal(low_price),
|
||||
close_price=Decimal(close_price),
|
||||
volume=Decimal(volume),
|
||||
source="test",
|
||||
)
|
||||
|
||||
|
||||
from src.trading.market_analysis.quality import candle_noise_score
|
||||
|
||||
|
||||
def test_candle_noise_score_accepts_decimal_candles() -> None:
|
||||
candles = [
|
||||
_candle(
|
||||
index=0,
|
||||
open_price="91",
|
||||
high_price="100",
|
||||
low_price="90",
|
||||
close_price="99",
|
||||
),
|
||||
_candle(
|
||||
index=1,
|
||||
open_price="94",
|
||||
high_price="100",
|
||||
low_price="90",
|
||||
close_price="96",
|
||||
),
|
||||
]
|
||||
|
||||
result = candle_noise_score(
|
||||
candles,
|
||||
candle_noise_window=2,
|
||||
min_clean_body_ratio=0.5,
|
||||
)
|
||||
|
||||
assert result == 0.5
|
||||
assert isinstance(result, float)
|
||||
|
||||
|
||||
def test_candle_noise_score_returns_none_for_empty_sequence() -> None:
|
||||
assert (
|
||||
candle_noise_score(
|
||||
[],
|
||||
candle_noise_window=10,
|
||||
min_clean_body_ratio=0.5,
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_candle_noise_score_skips_zero_range_candle() -> None:
|
||||
candles = [
|
||||
_candle(
|
||||
index=0,
|
||||
open_price="100",
|
||||
high_price="100",
|
||||
low_price="100",
|
||||
close_price="100",
|
||||
),
|
||||
_candle(
|
||||
index=1,
|
||||
open_price="91",
|
||||
high_price="100",
|
||||
low_price="90",
|
||||
close_price="99",
|
||||
),
|
||||
]
|
||||
|
||||
result = candle_noise_score(
|
||||
candles,
|
||||
candle_noise_window=2,
|
||||
min_clean_body_ratio=0.5,
|
||||
)
|
||||
|
||||
assert result == 1.0
|
||||
|
||||
|
||||
def test_candle_noise_score_skips_non_finite_decimal_value() -> None:
|
||||
candles = [
|
||||
_candle(
|
||||
index=0,
|
||||
high_price="NaN",
|
||||
),
|
||||
]
|
||||
|
||||
assert (
|
||||
candle_noise_score(
|
||||
candles,
|
||||
candle_noise_window=1,
|
||||
min_clean_body_ratio=0.5,
|
||||
)
|
||||
is None
|
||||
)
|
||||
|
||||
|
||||
def test_candle_noise_score_uses_requested_tail_window() -> None:
|
||||
candles = [
|
||||
_candle(
|
||||
index=0,
|
||||
open_price="91",
|
||||
high_price="100",
|
||||
low_price="90",
|
||||
close_price="99",
|
||||
),
|
||||
_candle(
|
||||
index=1,
|
||||
open_price="94",
|
||||
high_price="100",
|
||||
low_price="90",
|
||||
close_price="96",
|
||||
),
|
||||
_candle(
|
||||
index=2,
|
||||
open_price="92",
|
||||
high_price="100",
|
||||
low_price="90",
|
||||
close_price="98",
|
||||
),
|
||||
]
|
||||
|
||||
result = candle_noise_score(
|
||||
candles,
|
||||
candle_noise_window=2,
|
||||
min_clean_body_ratio=0.5,
|
||||
)
|
||||
|
||||
assert result == 0.5
|
||||
170
app/tests/unit/trading/market_analysis/test_structure_candle.py
Normal file
170
app/tests/unit/trading/market_analysis/test_structure_candle.py
Normal file
@@ -0,0 +1,170 @@
|
||||
# app/tests/unit/trading/market_analysis/test_structure_candle.py
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timezone
|
||||
from decimal import Decimal
|
||||
|
||||
from src.market_data.acquisition.models.candle import Candle
|
||||
|
||||
|
||||
def _candle(
|
||||
*,
|
||||
index: int = 0,
|
||||
open_price: str = "100",
|
||||
high_price: str = "110",
|
||||
low_price: str = "90",
|
||||
close_price: str = "105",
|
||||
volume: str = "10",
|
||||
) -> Candle:
|
||||
return Candle(
|
||||
symbol="BTC/USD_LEVERAGE",
|
||||
interval="1m",
|
||||
open_time=datetime.fromtimestamp(
|
||||
1_750_000_000 + index * 60,
|
||||
tz=timezone.utc,
|
||||
),
|
||||
open_price=Decimal(open_price),
|
||||
high_price=Decimal(high_price),
|
||||
low_price=Decimal(low_price),
|
||||
close_price=Decimal(close_price),
|
||||
volume=Decimal(volume),
|
||||
source="test",
|
||||
)
|
||||
|
||||
|
||||
from src.trading.market_analysis.models import MarketStructure
|
||||
from src.trading.market_analysis.structure import market_structure
|
||||
|
||||
|
||||
def _structure_candles(
|
||||
*,
|
||||
highs: list[float],
|
||||
lows: list[float],
|
||||
) -> list[Candle]:
|
||||
candles: list[Candle] = []
|
||||
|
||||
for index, (high, low) in enumerate(zip(highs, lows)):
|
||||
midpoint = (high + low) / 2
|
||||
|
||||
candles.append(
|
||||
_candle(
|
||||
index=index,
|
||||
open_price=str(midpoint),
|
||||
high_price=str(high),
|
||||
low_price=str(low),
|
||||
close_price=str(midpoint),
|
||||
)
|
||||
)
|
||||
|
||||
return candles
|
||||
|
||||
|
||||
def test_market_structure_detects_higher_highs_and_higher_lows() -> None:
|
||||
candles = _structure_candles(
|
||||
highs=[2, 4, 3, 5, 4, 6, 5, 7, 6, 5],
|
||||
lows=[1, 0, 1, 0.5, 1.5, 1, 2, 1.5, 2.5, 2],
|
||||
)
|
||||
|
||||
result, reason = market_structure(
|
||||
candles,
|
||||
atr_percent=0.30,
|
||||
structure_window=10,
|
||||
structure_swing_left=1,
|
||||
structure_swing_right=1,
|
||||
)
|
||||
|
||||
assert result == MarketStructure.HH_HL
|
||||
assert reason.startswith("HIGHER_HIGH_HIGHER_LOW:")
|
||||
|
||||
|
||||
def test_market_structure_detects_lower_highs_and_lower_lows() -> None:
|
||||
candles = _structure_candles(
|
||||
highs=[
|
||||
8.0,
|
||||
10.0,
|
||||
9.0,
|
||||
9.5,
|
||||
8.5,
|
||||
9.0,
|
||||
8.0,
|
||||
8.5,
|
||||
7.5,
|
||||
7.0,
|
||||
],
|
||||
lows=[
|
||||
7.0,
|
||||
6.0,
|
||||
7.0,
|
||||
5.5,
|
||||
6.5,
|
||||
5.0,
|
||||
6.0,
|
||||
4.5,
|
||||
5.5,
|
||||
5.0,
|
||||
],
|
||||
)
|
||||
|
||||
result, reason = market_structure(
|
||||
candles,
|
||||
atr_percent=0.30,
|
||||
structure_window=10,
|
||||
structure_swing_left=1,
|
||||
structure_swing_right=1,
|
||||
)
|
||||
|
||||
assert result == MarketStructure.LH_LL
|
||||
assert reason.startswith("LOWER_HIGH_LOWER_LOW:")
|
||||
|
||||
|
||||
def test_market_structure_detects_mixed_structure() -> None:
|
||||
candles = _structure_candles(
|
||||
highs=[2, 4, 3, 5, 4, 6, 5, 7, 6, 5],
|
||||
lows=[5, 4, 5, 3, 4, 2, 3, 1, 2, 1.5],
|
||||
)
|
||||
|
||||
result, reason = market_structure(
|
||||
candles,
|
||||
atr_percent=0.30,
|
||||
structure_window=10,
|
||||
structure_swing_left=1,
|
||||
structure_swing_right=1,
|
||||
)
|
||||
|
||||
assert result == MarketStructure.MIXED
|
||||
assert reason.startswith("MIXED_MARKET_STRUCTURE:")
|
||||
|
||||
|
||||
def test_market_structure_returns_unknown_when_candles_are_insufficient() -> None:
|
||||
candles = [
|
||||
_candle(index=index)
|
||||
for index in range(5)
|
||||
]
|
||||
|
||||
result, reason = market_structure(
|
||||
candles,
|
||||
structure_window=10,
|
||||
structure_swing_left=1,
|
||||
structure_swing_right=1,
|
||||
)
|
||||
|
||||
assert result == MarketStructure.UNKNOWN
|
||||
assert reason == "STRUCTURE_NOT_ENOUGH_CANDLES"
|
||||
|
||||
|
||||
def test_market_structure_accepts_decimal_candle_values() -> None:
|
||||
candles = _structure_candles(
|
||||
highs=[2, 4, 3, 5, 4, 6, 5, 7, 6, 5],
|
||||
lows=[1, 0, 1, 0.5, 1.5, 1, 2, 1.5, 2.5, 2],
|
||||
)
|
||||
|
||||
result, _ = market_structure(
|
||||
candles,
|
||||
atr_percent=0.30,
|
||||
structure_window=10,
|
||||
structure_swing_left=1,
|
||||
structure_swing_right=1,
|
||||
)
|
||||
|
||||
assert result == MarketStructure.HH_HL
|
||||
907
docs/migrations/build_048.md
Normal file
907
docs/migrations/build_048.md
Normal file
@@ -0,0 +1,907 @@
|
||||
# Build 048 — Переключение вычислительных потребителей Market Analysis на каноническую модель Candle
|
||||
|
||||
**Статус:** Completed
|
||||
|
||||
**Дата:** 2026-07-15
|
||||
|
||||
**Подсистема:** Market Data Acquisition / OHLCV Feed / Market Analysis
|
||||
|
||||
---
|
||||
|
||||
## 1. Назначение Build
|
||||
|
||||
Build 048 переводит активные вычислительные компоненты подсистемы `Market Analysis`, непосредственно работающие с последовательностями рыночных свечей, с legacy-модели:
|
||||
|
||||
```text
|
||||
src.integrations.exchange.models.Kline
|
||||
```
|
||||
|
||||
на каноническую модель рыночной свечи:
|
||||
|
||||
```text
|
||||
src.market_data.acquisition.models.candle.Candle
|
||||
```
|
||||
|
||||
Build является очередным этапом поэтапной миграции получения и использования OHLCV-данных в канонический слой:
|
||||
|
||||
```text
|
||||
Market Data Acquisition
|
||||
```
|
||||
|
||||
Основная цель Build — устранить прямую зависимость активных вычислительных функций анализа рынка от legacy-модели `Kline`, сохранив существующую торговую семантику и поведение системы.
|
||||
|
||||
---
|
||||
|
||||
## 2. Контекст миграции
|
||||
|
||||
До Build 048 канонический OHLCV pipeline уже был создан и интегрирован поэтапно.
|
||||
|
||||
### Build 044
|
||||
|
||||
Создан фундамент канонического Candles Feed:
|
||||
|
||||
```text
|
||||
REST transport
|
||||
↓
|
||||
schema validation
|
||||
↓
|
||||
parser
|
||||
↓
|
||||
value validation
|
||||
↓
|
||||
mapper
|
||||
↓
|
||||
Candle
|
||||
↓
|
||||
handler
|
||||
↓
|
||||
CandlesFeed
|
||||
```
|
||||
|
||||
Каноническая модель:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/models/candle.py
|
||||
```
|
||||
|
||||
### Build 045
|
||||
|
||||
Создан и протестирован:
|
||||
|
||||
```text
|
||||
CandlesFeedRegistry
|
||||
```
|
||||
|
||||
### Build 046
|
||||
|
||||
Создан и интегрирован:
|
||||
|
||||
```text
|
||||
CandlesAcquisitionService
|
||||
```
|
||||
|
||||
### Build 047
|
||||
|
||||
Legacy-метод:
|
||||
|
||||
```text
|
||||
ExchangeService.get_klines()
|
||||
```
|
||||
|
||||
переключён на канонический Candles Feed.
|
||||
|
||||
После Build 047 транспортный запрос:
|
||||
|
||||
```text
|
||||
/api/v1/klines
|
||||
```
|
||||
|
||||
выполняется только внутри:
|
||||
|
||||
```text
|
||||
src/market_data/acquisition/adapters/dzengi/rest.py
|
||||
```
|
||||
|
||||
При этом `ExchangeService.get_klines()` временно сохраняет обратную совместимость и преобразует канонические `Candle` обратно в legacy-модели `Kline`.
|
||||
|
||||
Build 048 начинает устранять эту обратную legacy-зависимость непосредственно внутри вычислительных потребителей `Market Analysis`.
|
||||
|
||||
---
|
||||
|
||||
## 3. Границы Build
|
||||
|
||||
В Build 048 изменены только активные вычислительные компоненты `Market Analysis`, которые непосредственно принимают последовательности свечей и используют поля OHLCV.
|
||||
|
||||
Изменены:
|
||||
|
||||
```text
|
||||
src/trading/market_analysis/indicators/volatility.py
|
||||
src/trading/market_analysis/quality.py
|
||||
src/trading/market_analysis/structure.py
|
||||
```
|
||||
|
||||
Добавлены специализированные unit-тесты:
|
||||
|
||||
```text
|
||||
tests/unit/trading/market_analysis/indicators/test_volatility_candle.py
|
||||
tests/unit/trading/market_analysis/test_quality_candle.py
|
||||
tests/unit/trading/market_analysis/test_structure_candle.py
|
||||
```
|
||||
|
||||
Build 048 не изменяет:
|
||||
|
||||
```text
|
||||
src/trading/market_analysis/service.py
|
||||
src/trading/market_analysis/htf.py
|
||||
src/trading/market_analysis/indicators_legacy.py
|
||||
src/integrations/exchange/service.py
|
||||
src/integrations/exchange/models.py
|
||||
```
|
||||
|
||||
Build также не удаляет:
|
||||
|
||||
```text
|
||||
Kline
|
||||
KlineBatch
|
||||
ExchangeService.get_klines()
|
||||
```
|
||||
|
||||
Их дальнейшая судьба определяется отдельными последующими этапами миграции.
|
||||
|
||||
---
|
||||
|
||||
## 4. Основное архитектурное изменение
|
||||
|
||||
До Build 048 активные вычислительные функции использовали legacy-модель:
|
||||
|
||||
```python
|
||||
from src.integrations.exchange.models import Kline
|
||||
```
|
||||
|
||||
После Build 048 они используют каноническую модель:
|
||||
|
||||
```python
|
||||
from src.market_data.acquisition.models.candle import Candle
|
||||
```
|
||||
|
||||
Изменение выполнено в:
|
||||
|
||||
```text
|
||||
src/trading/market_analysis/indicators/volatility.py
|
||||
src/trading/market_analysis/quality.py
|
||||
src/trading/market_analysis/structure.py
|
||||
```
|
||||
|
||||
Таким образом, активные вычислительные функции:
|
||||
|
||||
```text
|
||||
atr()
|
||||
atr_percent_baseline()
|
||||
candle_noise_score()
|
||||
market_structure()
|
||||
```
|
||||
|
||||
больше не зависят от:
|
||||
|
||||
```text
|
||||
src.integrations.exchange.models.Kline
|
||||
```
|
||||
|
||||
и работают непосредственно с:
|
||||
|
||||
```text
|
||||
src.market_data.acquisition.models.candle.Candle
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. Адаптация к канонической модели Candle
|
||||
|
||||
Каноническая модель `Candle` использует точные числовые значения:
|
||||
|
||||
```text
|
||||
Decimal
|
||||
```
|
||||
|
||||
для полей:
|
||||
|
||||
```text
|
||||
open_price
|
||||
high_price
|
||||
low_price
|
||||
close_price
|
||||
volume
|
||||
```
|
||||
|
||||
Legacy-модель `Kline` использовала:
|
||||
|
||||
```text
|
||||
float
|
||||
```
|
||||
|
||||
Поэтому Build 048 выполняет явную адаптацию вычислительного слоя к `Decimal`.
|
||||
|
||||
Для безопасного преобразования используется существующий helper:
|
||||
|
||||
```text
|
||||
src.core.numbers.safe_float
|
||||
```
|
||||
|
||||
Числовые значения преобразуются непосредственно на границе вычислений.
|
||||
|
||||
Принцип:
|
||||
|
||||
```text
|
||||
Candle
|
||||
↓
|
||||
Decimal OHLCV
|
||||
↓
|
||||
safe_float(...)
|
||||
↓
|
||||
float
|
||||
↓
|
||||
существующая вычислительная логика Market Analysis
|
||||
```
|
||||
|
||||
Это позволяет:
|
||||
|
||||
- сохранить точность канонической модели данных;
|
||||
- не переносить `float` в слой Market Data Acquisition;
|
||||
- сохранить существующие вычислительные алгоритмы;
|
||||
- минимизировать область изменений;
|
||||
- не изменять торговую семантику в рамках миграционного Build.
|
||||
|
||||
---
|
||||
|
||||
## 6. Изменения volatility.py
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
src/trading/market_analysis/indicators/volatility.py
|
||||
```
|
||||
|
||||
переведён с:
|
||||
|
||||
```text
|
||||
Sequence[Kline]
|
||||
```
|
||||
|
||||
на:
|
||||
|
||||
```text
|
||||
Sequence[Candle]
|
||||
```
|
||||
|
||||
Адаптированы функции:
|
||||
|
||||
```text
|
||||
atr()
|
||||
atr_percent_baseline()
|
||||
```
|
||||
|
||||
Поля канонической модели:
|
||||
|
||||
```text
|
||||
previous.close_price
|
||||
current.high_price
|
||||
current.low_price
|
||||
```
|
||||
|
||||
преобразуются через:
|
||||
|
||||
```text
|
||||
safe_float(...)
|
||||
```
|
||||
|
||||
Дополнительно выполняется проверка:
|
||||
|
||||
```text
|
||||
math.isfinite(...)
|
||||
```
|
||||
|
||||
для исключения нечисловых конечных значений:
|
||||
|
||||
```text
|
||||
NaN
|
||||
Infinity
|
||||
-Infinity
|
||||
```
|
||||
|
||||
Это необходимо, поскольку преобразование:
|
||||
|
||||
```text
|
||||
Decimal("NaN")
|
||||
```
|
||||
|
||||
в `float` само по себе не возвращает `None`, а создаёт:
|
||||
|
||||
```text
|
||||
float("nan")
|
||||
```
|
||||
|
||||
Без проверки `isfinite()` такое значение могло проникнуть в вычисление ATR.
|
||||
|
||||
---
|
||||
|
||||
## 7. Изменения quality.py
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
src/trading/market_analysis/quality.py
|
||||
```
|
||||
|
||||
переведён с:
|
||||
|
||||
```text
|
||||
Sequence[Kline]
|
||||
```
|
||||
|
||||
на:
|
||||
|
||||
```text
|
||||
Sequence[Candle]
|
||||
```
|
||||
|
||||
Адаптирована функция:
|
||||
|
||||
```text
|
||||
candle_noise_score()
|
||||
```
|
||||
|
||||
Поля:
|
||||
|
||||
```text
|
||||
high_price
|
||||
low_price
|
||||
open_price
|
||||
close_price
|
||||
```
|
||||
|
||||
преобразуются через:
|
||||
|
||||
```text
|
||||
safe_float(...)
|
||||
```
|
||||
|
||||
и проверяются через:
|
||||
|
||||
```text
|
||||
isfinite(...)
|
||||
```
|
||||
|
||||
Невалидные свечи не участвуют в расчёте.
|
||||
|
||||
Если после фильтрации отсутствуют допустимые свечи, функция возвращает:
|
||||
|
||||
```text
|
||||
None
|
||||
```
|
||||
|
||||
Существующая функция:
|
||||
|
||||
```text
|
||||
price_position_score()
|
||||
```
|
||||
|
||||
сохранена.
|
||||
|
||||
Build 048 не изменяет её назначение и вычислительную семантику.
|
||||
|
||||
---
|
||||
|
||||
## 8. Изменения structure.py
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
src/trading/market_analysis/structure.py
|
||||
```
|
||||
|
||||
переведён с:
|
||||
|
||||
```text
|
||||
Sequence[Kline]
|
||||
```
|
||||
|
||||
на:
|
||||
|
||||
```text
|
||||
Sequence[Candle]
|
||||
```
|
||||
|
||||
Адаптирована функция:
|
||||
|
||||
```text
|
||||
market_structure()
|
||||
```
|
||||
|
||||
Для построения последовательностей максимумов и минимумов используются значения:
|
||||
|
||||
```text
|
||||
candle.high_price
|
||||
candle.low_price
|
||||
```
|
||||
|
||||
с безопасным преобразованием к вычислительному типу.
|
||||
|
||||
Существующая логика определения структуры рынка сохранена:
|
||||
|
||||
```text
|
||||
HH_HL
|
||||
LH_LL
|
||||
MIXED
|
||||
UNKNOWN
|
||||
```
|
||||
|
||||
Build 048 не изменяет:
|
||||
|
||||
- правила определения swing high;
|
||||
- правила определения swing low;
|
||||
- правила сравнения последних swing-точек;
|
||||
- adaptive structure parameters;
|
||||
- существующие причины результата;
|
||||
- существующую классификацию `MarketStructure`.
|
||||
|
||||
---
|
||||
|
||||
## 9. Защита от невалидных числовых значений
|
||||
|
||||
В ходе специализированного тестирования была выявлена необходимость явной проверки конечности числовых значений.
|
||||
|
||||
Для вычислительных функций используется:
|
||||
|
||||
```python
|
||||
from math import isfinite
|
||||
```
|
||||
|
||||
После:
|
||||
|
||||
```text
|
||||
safe_float(...)
|
||||
```
|
||||
|
||||
значения дополнительно проверяются на:
|
||||
|
||||
```text
|
||||
NaN
|
||||
Infinity
|
||||
-Infinity
|
||||
```
|
||||
|
||||
Это предотвращает распространение невалидных числовых значений внутри:
|
||||
|
||||
```text
|
||||
ATR
|
||||
ATR baseline
|
||||
candle noise score
|
||||
```
|
||||
|
||||
В частности, предотвращается ситуация:
|
||||
|
||||
```text
|
||||
Decimal("NaN")
|
||||
↓
|
||||
float("nan")
|
||||
↓
|
||||
результат вычисления nan
|
||||
```
|
||||
|
||||
Вместо этого невалидное значение исключается из вычисления согласно локальной логике соответствующей функции.
|
||||
|
||||
---
|
||||
|
||||
## 10. Специализированные unit-тесты
|
||||
|
||||
Добавлены три специализированных тестовых файла.
|
||||
|
||||
### 10.1. Volatility
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
tests/unit/trading/market_analysis/indicators/test_volatility_candle.py
|
||||
```
|
||||
|
||||
Проверяет:
|
||||
|
||||
- работу `atr()` с каноническими `Candle`;
|
||||
- поддержку `Decimal`;
|
||||
- недостаточное количество свечей;
|
||||
- невалидный период;
|
||||
- обработку не конечных числовых значений;
|
||||
- работу `atr_percent_baseline()`;
|
||||
- невалидную цену закрытия.
|
||||
|
||||
### 10.2. Quality
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
tests/unit/trading/market_analysis/test_quality_candle.py
|
||||
```
|
||||
|
||||
Проверяет:
|
||||
|
||||
- работу `candle_noise_score()` с `Candle`;
|
||||
- поддержку `Decimal`;
|
||||
- пустую последовательность;
|
||||
- свечу с нулевым диапазоном;
|
||||
- не конечные числовые значения;
|
||||
- применение указанного tail window.
|
||||
|
||||
### 10.3. Structure
|
||||
|
||||
Файл:
|
||||
|
||||
```text
|
||||
tests/unit/trading/market_analysis/test_structure_candle.py
|
||||
```
|
||||
|
||||
Проверяет:
|
||||
|
||||
- структуру `HH_HL`;
|
||||
- структуру `LH_LL`;
|
||||
- структуру `MIXED`;
|
||||
- недостаточное количество свечей;
|
||||
- непосредственную работу с `Decimal`-значениями `Candle`.
|
||||
|
||||
---
|
||||
|
||||
## 11. Результаты специализированных тестов
|
||||
|
||||
Команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q \
|
||||
tests/unit/trading/market_analysis/indicators/test_volatility_candle.py \
|
||||
tests/unit/trading/market_analysis/test_quality_candle.py \
|
||||
tests/unit/trading/market_analysis/test_structure_candle.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
16 passed in 0.02s
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 12. Regression-проверка стратегий
|
||||
|
||||
Дополнительно выполнена проверка стратегий:
|
||||
|
||||
```bash
|
||||
python -m pytest -q \
|
||||
tests/unit/trading/strategies/test_scalp_quote.py \
|
||||
tests/unit/trading/strategies/test_trend_quote.py
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
6 passed in 0.14s
|
||||
```
|
||||
|
||||
Эта проверка подтверждает сохранение импортов и существующей интеграции `Market Analysis` с торговыми стратегиями.
|
||||
|
||||
---
|
||||
|
||||
## 13. Полный regression suite
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
python -m pytest -q
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
766 passed in 2.53s
|
||||
```
|
||||
|
||||
Полный unit test suite проекта проходит успешно.
|
||||
|
||||
---
|
||||
|
||||
## 14. Контроль legacy-зависимости Kline
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
"integrations.exchange.models import Kline" \
|
||||
src/trading/market_analysis
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
src/trading/market_analysis/indicators_legacy.py:5:from src.integrations.exchange.models import Kline
|
||||
```
|
||||
|
||||
Следовательно, после Build 048 активные вычислительные компоненты:
|
||||
|
||||
```text
|
||||
volatility.py
|
||||
quality.py
|
||||
structure.py
|
||||
```
|
||||
|
||||
больше не импортируют legacy-модель `Kline`.
|
||||
|
||||
Оставшийся импорт находится только в:
|
||||
|
||||
```text
|
||||
indicators_legacy.py
|
||||
```
|
||||
|
||||
который не изменяется в рамках Build 048.
|
||||
|
||||
---
|
||||
|
||||
## 15. Контроль использования канонической модели Candle
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
"models.candle import Candle" \
|
||||
src/trading/market_analysis
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
src/trading/market_analysis/indicators/volatility.py:10:from src.market_data.acquisition.models.candle import Candle
|
||||
src/trading/market_analysis/structure.py:9:from src.market_data.acquisition.models.candle import Candle
|
||||
src/trading/market_analysis/quality.py:9:from src.market_data.acquisition.models.candle import Candle
|
||||
```
|
||||
|
||||
Это подтверждает прямую зависимость трёх активных вычислительных потребителей от канонической модели `Candle`.
|
||||
|
||||
---
|
||||
|
||||
## 16. Контроль оставшихся вызовов ExchangeService.get_klines()
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
grep -RIn \
|
||||
--exclude-dir="__pycache__" \
|
||||
--exclude="*.pyc" \
|
||||
"\.get_klines(" \
|
||||
src/trading/market_analysis
|
||||
```
|
||||
|
||||
Результат:
|
||||
|
||||
```text
|
||||
src/trading/market_analysis/service.py:243: batch = ExchangeService().get_klines(
|
||||
src/trading/market_analysis/htf.py:55: batch = ExchangeService().get_klines(
|
||||
src/trading/market_analysis/htf.py:153: batch = ExchangeService().get_klines(
|
||||
```
|
||||
|
||||
Эти три вызова являются ожидаемыми и не удаляются в Build 048.
|
||||
|
||||
Они показывают следующую границу дальнейшей миграции:
|
||||
|
||||
```text
|
||||
Market Analysis orchestration
|
||||
↓
|
||||
прямое получение Candle
|
||||
↓
|
||||
устранение зависимости от ExchangeService.get_klines()
|
||||
```
|
||||
|
||||
Такое переключение должно выполняться отдельным Build после анализа контрактов:
|
||||
|
||||
```text
|
||||
service.py
|
||||
htf.py
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 17. Проверка форматирования diff
|
||||
|
||||
Выполнена команда:
|
||||
|
||||
```bash
|
||||
git diff --check
|
||||
```
|
||||
|
||||
Вывод отсутствует.
|
||||
|
||||
Следовательно:
|
||||
|
||||
```text
|
||||
trailing whitespace отсутствует
|
||||
ошибки whitespace отсутствуют
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 18. Архитектурный результат
|
||||
|
||||
После Build 048 активный вычислительный путь выглядит следующим образом:
|
||||
|
||||
```text
|
||||
Dzengi REST API
|
||||
↓
|
||||
DzengiCandlesDocumentSource
|
||||
↓
|
||||
validate_candles_schema()
|
||||
↓
|
||||
parse_candles()
|
||||
↓
|
||||
validate_candles_values()
|
||||
↓
|
||||
map_candles()
|
||||
↓
|
||||
Candle
|
||||
↓
|
||||
CandlesDocumentHandler
|
||||
↓
|
||||
CandlesFeed
|
||||
↓
|
||||
CandlesFeedRegistry
|
||||
↓
|
||||
CandlesAcquisitionService
|
||||
↓
|
||||
ExchangeService.get_klines()
|
||||
↓
|
||||
legacy compatibility conversion
|
||||
↓
|
||||
Market Analysis orchestration
|
||||
↓
|
||||
active computational consumers typed for Candle
|
||||
```
|
||||
|
||||
При этом три активных вычислительных компонента уже используют канонический контракт:
|
||||
|
||||
```text
|
||||
volatility.py → Candle
|
||||
quality.py → Candle
|
||||
structure.py → Candle
|
||||
```
|
||||
|
||||
Однако orchestration-слой `Market Analysis` пока продолжает получать данные через:
|
||||
|
||||
```text
|
||||
ExchangeService.get_klines()
|
||||
```
|
||||
|
||||
Поэтому полное устранение legacy-преобразования `Candle → Kline` ещё не завершено.
|
||||
|
||||
---
|
||||
|
||||
## 19. Что сознательно не сделано
|
||||
|
||||
В Build 048 сознательно не выполнялось:
|
||||
|
||||
- удаление `Kline`;
|
||||
- удаление `KlineBatch`;
|
||||
- удаление `ExchangeService.get_klines()`;
|
||||
- изменение `MarketAnalysisService`;
|
||||
- изменение HTF orchestration;
|
||||
- изменение `indicators_legacy.py`;
|
||||
- изменение торговых стратегий;
|
||||
- изменение scoring;
|
||||
- изменение signal logic;
|
||||
- изменение торговых порогов;
|
||||
- изменение определения market structure;
|
||||
- изменение правил ATR;
|
||||
- изменение существующей архитектуры каталогов;
|
||||
- удаление рабочего legacy-кода.
|
||||
|
||||
Это соответствует принципу поэтапной миграции:
|
||||
|
||||
```text
|
||||
сначала новый канонический путь
|
||||
↓
|
||||
затем переключение потребителей
|
||||
↓
|
||||
затем подтверждение отсутствия legacy-потребителей
|
||||
↓
|
||||
только после этого отдельное удаление legacy-кода
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 20. Соблюдение правил миграции
|
||||
|
||||
Build 048 соответствует утверждённым правилам проекта Dzentra:
|
||||
|
||||
1. Рабочий код не удаляется без отдельного согласования.
|
||||
2. Изменения ограничены согласованными границами Build.
|
||||
3. Канонический слой Market Data не зависит от `src.trading`.
|
||||
4. Миграция выполняется поэтапно.
|
||||
5. Обратная совместимость сохраняется там, где она ещё необходима.
|
||||
6. Legacy-код не удаляется до подтверждённого переключения всех его потребителей.
|
||||
7. Существующая торговая семантика не изменяется в рамках инфраструктурной миграции.
|
||||
8. Каждый Build подтверждается специализированными тестами, полным regression suite и контрольными grep.
|
||||
|
||||
---
|
||||
|
||||
## 21. Итог Build 048
|
||||
|
||||
Build 048 завершён успешно.
|
||||
|
||||
В результате:
|
||||
|
||||
```text
|
||||
volatility.py
|
||||
quality.py
|
||||
structure.py
|
||||
```
|
||||
|
||||
переведены с:
|
||||
|
||||
```text
|
||||
Kline
|
||||
```
|
||||
|
||||
на:
|
||||
|
||||
```text
|
||||
Candle
|
||||
```
|
||||
|
||||
Канонические `Decimal`-значения адаптированы к существующему вычислительному слою через:
|
||||
|
||||
```text
|
||||
safe_float(...)
|
||||
```
|
||||
|
||||
с дополнительной защитой от:
|
||||
|
||||
```text
|
||||
NaN
|
||||
Infinity
|
||||
-Infinity
|
||||
```
|
||||
|
||||
Подтверждено:
|
||||
|
||||
```text
|
||||
16 специализированных тестов passed
|
||||
6 regression-тестов стратегий passed
|
||||
766 тестов полного suite passed
|
||||
git diff --check — чисто
|
||||
```
|
||||
|
||||
Build 048 завершает переключение выбранных активных вычислительных потребителей `Market Analysis` на каноническую модель `Candle`.
|
||||
|
||||
Следующая архитектурная граница миграции — прямое переключение orchestration-компонентов:
|
||||
|
||||
```text
|
||||
src/trading/market_analysis/service.py
|
||||
src/trading/market_analysis/htf.py
|
||||
```
|
||||
|
||||
с:
|
||||
|
||||
```text
|
||||
ExchangeService.get_klines()
|
||||
```
|
||||
|
||||
на получение канонических:
|
||||
|
||||
```text
|
||||
Candle
|
||||
```
|
||||
|
||||
через слой:
|
||||
|
||||
```text
|
||||
Market Data Acquisition
|
||||
```
|
||||
|
||||
Такое переключение должно выполняться отдельным Build с сохранением существующего поведения системы.
|
||||
Reference in New Issue
Block a user