07.4.3.14 — Auto Trading UI. Realistic Pricing & Debug Live Tools
This commit is contained in:
@@ -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(),
|
||||
}
|
||||
|
||||
# получить стратегию по имени
|
||||
|
||||
156
app/src/trading/strategies/scalp.py
Normal file
156
app/src/trading/strategies/scalp.py
Normal 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)
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user