07.4.3.14 — Auto Trading UI. Realistic Pricing & Debug Live Tools

This commit is contained in:
2026-05-09 01:34:46 +03:00
parent ee78f9774a
commit df76490783
15 changed files with 2161 additions and 464 deletions

View File

@@ -4,6 +4,7 @@ from __future__ import annotations
from src.trading.strategies.base import BaseStrategy
from src.trading.strategies.hold import HoldStrategy
from src.trading.strategies.scalp import ScalpStrategy
from src.trading.strategies.trend import TrendStrategy
@@ -13,7 +14,7 @@ class StrategyRegistry:
"HOLD": HoldStrategy(),
"TREND": TrendStrategy(),
"GRID": HoldStrategy(),
"SCALP": HoldStrategy(),
"SCALP": ScalpStrategy(),
}
# получить стратегию по имени

View File

@@ -0,0 +1,156 @@
# app/src/trading/strategies/scalp.py
from __future__ import annotations
from src.integrations.exchange.service import ExchangeService
from src.trading.strategies.base import StrategyContext
from src.trading.strategies.signals import SignalResult, SignalType
class ScalpStrategy:
name = "SCALP"
_price_window: dict[str, list[float]] = {}
# короткое окно = быстрая реакция
_window_size = 4
# ниже порог = чувствительнее TREND
_threshold_percent = 0.02
# для scalp допускаем чуть больше шума
_min_direction_ratio = 0.55
def analyze(self, context: StrategyContext) -> SignalResult:
try:
ticker = ExchangeService().get_price(context.symbol)
except Exception as exc:
return SignalResult(
signal=SignalType.HOLD,
reason="Не удалось получить рыночную цену. Безопасный HOLD.",
confidence=0.0,
payload={
"strategy": self.name,
"symbol": context.symbol,
"error": str(exc),
},
)
symbol = ticker.symbol
current_price = float(ticker.price)
prices = self._price_window.setdefault(symbol, [])
prices.append(current_price)
if len(prices) > self._window_size:
prices.pop(0)
if len(prices) < self._window_size:
return SignalResult(
signal=SignalType.HOLD,
reason="Недостаточно данных для SCALP.",
confidence=0.0,
payload={
"strategy": self.name,
"symbol": symbol,
"price": current_price,
"window_size": len(prices),
"required_window_size": self._window_size,
},
)
first_price = prices[0]
last_price = prices[-1]
if first_price <= 0:
return SignalResult(
signal=SignalType.HOLD,
reason="Некорректная стартовая цена в окне SCALP.",
confidence=0.0,
payload={
"strategy": self.name,
"symbol": symbol,
"prices": prices,
},
)
change_percent = ((last_price - first_price) / first_price) * 100
direction_ratio = self._direction_ratio(prices, change_percent)
payload = {
"strategy": self.name,
"symbol": symbol,
"first_price": first_price,
"current_price": last_price,
"change_percent": round(change_percent, 5),
"direction_ratio": round(direction_ratio, 3),
"window_size": len(prices),
"threshold_percent": self._threshold_percent,
"min_direction_ratio": self._min_direction_ratio,
}
if (
change_percent >= self._threshold_percent
and direction_ratio >= self._min_direction_ratio
):
return SignalResult(
signal=SignalType.BUY,
reason="Быстрый краткосрочный импульс вверх.",
confidence=self._calculate_confidence(change_percent, direction_ratio),
payload=payload,
)
if (
change_percent <= -self._threshold_percent
and direction_ratio >= self._min_direction_ratio
):
return SignalResult(
signal=SignalType.SELL,
reason="Быстрый краткосрочный импульс вниз.",
confidence=self._calculate_confidence(change_percent, direction_ratio),
payload=payload,
)
return SignalResult(
signal=SignalType.HOLD,
reason="SCALP-импульс недостаточно сильный.",
confidence=0.0,
payload=payload,
)
def _direction_ratio(self, prices: list[float], change_percent: float) -> float:
if len(prices) < 2:
return 0.0
up_moves = 0
down_moves = 0
for previous_price, current_price in zip(prices, prices[1:]):
if current_price > previous_price:
up_moves += 1
elif current_price < previous_price:
down_moves += 1
total_moves = max(1, len(prices) - 1)
if change_percent >= 0:
return up_moves / total_moves
return down_moves / total_moves
def _calculate_confidence(
self,
change_percent: float,
direction_ratio: float,
) -> float:
strength = abs(change_percent) / self._threshold_percent
if strength < 1:
return 0.0
strength_score = min(1.0, strength / 2)
direction_score = min(1.0, direction_ratio)
confidence = 0.35 + (strength_score * 0.4) + (direction_score * 0.25)
return round(min(1.0, confidence), 2)

View File

@@ -10,28 +10,24 @@ from src.trading.strategies.signals import SignalResult, SignalType
class TrendStrategy:
name = "TREND"
_last_prices: dict[str, float] = {}
_threshold_percent = 0.02
_price_window: dict[str, list[float]] = {}
# рассчитать уверенность сигнала по силе движения цены
def _calculate_confidence(self, change_percent: float) -> float:
strength = abs(change_percent) / self._threshold_percent
# длиннее окно = меньше шума
_window_size = 8
if strength < 1:
return 0.0
# общий порог изменения за окно
_threshold_percent = 0.05
confidence = 0.35 + ((strength - 1) / 2) * 0.65
# сколько движений внутри окна должно быть в сторону сигнала
_min_direction_ratio = 0.6
return round(min(1.0, confidence), 2)
# анализ простого тренда по изменению цены
def analyze(self, context: StrategyContext) -> SignalResult:
try:
ticker = ExchangeService().get_price(context.symbol)
snapshot = ExchangeService().get_market_snapshot(context.symbol)
except Exception as exc:
return SignalResult(
signal=SignalType.HOLD,
reason="Не удалось получить рыночную цену. Безопасный HOLD.",
reason="Не удалось получить рыночный snapshot. Безопасный HOLD.",
confidence=0.0,
payload={
"strategy": self.name,
@@ -40,63 +36,159 @@ class TrendStrategy:
},
)
symbol = ticker.symbol
current_price = ticker.price
previous_price = self._last_prices.get(symbol)
symbol = str(snapshot.get("symbol") or context.symbol)
current_price = self._analysis_price(snapshot)
self._last_prices[symbol] = current_price
if previous_price is None or previous_price <= 0:
if current_price <= 0:
return SignalResult(
signal=SignalType.HOLD,
reason="Недостаточно данных для определения тренда.",
reason="Некорректная рыночная цена. Безопасный HOLD.",
confidence=0.0,
payload={
"strategy": self.name,
"symbol": symbol,
"snapshot": snapshot,
},
)
prices = self._price_window.setdefault(symbol, [])
prices.append(current_price)
if len(prices) > self._window_size:
prices.pop(0)
if len(prices) < self._window_size:
return SignalResult(
signal=SignalType.HOLD,
reason="Недостаточно данных для анализа тренда.",
confidence=0.0,
payload={
"strategy": self.name,
"symbol": symbol,
"price": current_price,
"window_size": len(prices),
"required_window_size": self._window_size,
},
)
change_percent = ((current_price - previous_price) / previous_price) * 100
first_price = prices[0]
last_price = prices[-1]
if change_percent >= self._threshold_percent:
if first_price <= 0:
return SignalResult(
signal=SignalType.HOLD,
reason="Некорректная стартовая цена в окне.",
confidence=0.0,
payload={
"strategy": self.name,
"symbol": symbol,
"prices": prices,
},
)
change_percent = ((last_price - first_price) / first_price) * 100
direction_ratio = self._direction_ratio(prices, change_percent)
payload = {
"strategy": self.name,
"symbol": symbol,
"analysis_price": last_price,
"first_price": first_price,
"current_price": last_price,
"last_price": snapshot.get("last_price"),
"bid_price": snapshot.get("bid_price"),
"ask_price": snapshot.get("ask_price"),
"change_percent": round(change_percent, 5),
"direction_ratio": round(direction_ratio, 3),
"window_size": len(prices),
"threshold_percent": self._threshold_percent,
"min_direction_ratio": self._min_direction_ratio,
}
if (
change_percent >= self._threshold_percent
and direction_ratio >= self._min_direction_ratio
):
return SignalResult(
signal=SignalType.BUY,
reason="Цена растёт выше порога тренда.",
confidence=self._calculate_confidence(change_percent),
payload={
"strategy": self.name,
"symbol": symbol,
"previous_price": previous_price,
"current_price": current_price,
"change_percent": round(change_percent, 5),
},
reason="Устойчивый рост цены в окне TREND.",
confidence=self._calculate_confidence(change_percent, direction_ratio),
payload=payload,
)
if change_percent <= -self._threshold_percent:
if (
change_percent <= -self._threshold_percent
and direction_ratio >= self._min_direction_ratio
):
return SignalResult(
signal=SignalType.SELL,
reason="Цена падает ниже порога тренда.",
confidence=self._calculate_confidence(change_percent),
payload={
"strategy": self.name,
"symbol": symbol,
"previous_price": previous_price,
"current_price": current_price,
"change_percent": round(change_percent, 5),
},
reason="Устойчивое снижение цены в окне TREND.",
confidence=self._calculate_confidence(change_percent, direction_ratio),
payload=payload,
)
return SignalResult(
signal=SignalType.HOLD,
reason="Изменение цены ниже порога тренда.",
reason="Тренд недостаточно устойчивый.",
confidence=0.0,
payload={
"strategy": self.name,
"symbol": symbol,
"previous_price": previous_price,
"current_price": current_price,
"change_percent": round(change_percent, 5),
},
)
payload=payload,
)
def _analysis_price(self, snapshot: dict[str, object]) -> float:
bid = self._safe_float(snapshot.get("bid_price"))
ask = self._safe_float(snapshot.get("ask_price"))
if bid is not None and ask is not None and bid > 0 and ask > 0:
return (bid + ask) / 2
last = self._safe_float(snapshot.get("last_price"))
if last is not None:
return last
return 0.0
def _safe_float(self, value: object) -> float | None:
if value is None:
return None
try:
return float(value)
except (TypeError, ValueError):
return None
def _direction_ratio(self, prices: list[float], change_percent: float) -> float:
if len(prices) < 2:
return 0.0
up_moves = 0
down_moves = 0
for previous_price, current_price in zip(prices, prices[1:]):
if current_price > previous_price:
up_moves += 1
elif current_price < previous_price:
down_moves += 1
total_moves = max(1, len(prices) - 1)
if change_percent >= 0:
return up_moves / total_moves
return down_moves / total_moves
def _calculate_confidence(
self,
change_percent: float,
direction_ratio: float,
) -> float:
strength = abs(change_percent) / self._threshold_percent
if strength < 1:
return 0.0
strength_score = min(1.0, strength / 3)
direction_score = min(1.0, direction_ratio)
confidence = 0.3 + (strength_score * 0.4) + (direction_score * 0.3)
return round(min(1.0, confidence), 2)