build 048: switch market analysis consumers to canonical Candle model

This commit is contained in:
2026-07-15 18:55:17 +03:00
parent 77e87c0504
commit 3a253d89a9
7 changed files with 1478 additions and 33 deletions

View File

@@ -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

View File

@@ -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

View File

@@ -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,