Files
robinhood/backend/app/services/price_anchor.py
T
Krikorios 48e60d015f feat: Add Phase 4 advanced metrics and components
- Add advanced metrics dashboard with trade analytics
- Add new trading components (EntryTypeAnalysis, MultiDayPositionTracker, NewsEventTracker, etc.)
- Add strategy mode selector and trend confirmation
- Add risk automation panel and slippage correlation analysis
- Add daily trading plan enhancements with modal components
- Add custom hooks (useApi, useLocalStorage, useAdvancedTradeMetrics)
- Add broker service integration and trading API
- Add test setup and vitest configuration
- Include parquet data files for live market data
- Add comprehensive documentation in docs/ folder
2025-11-27 10:23:58 +02:00

118 lines
4.8 KiB
Python

from __future__ import annotations
import logging
import time
from typing import Callable, Awaitable, Optional, Sequence
from app.schemas.schemas import PositionMetrics, PatternSignal
from app.services.metals.bullionvault_service import get_bullionvault_gold_price
from app.services.metals.gold_price_fetcher import gold_price_fetcher
logger = logging.getLogger(__name__)
class PriceAnchorService:
"""Rescales simulated metric snapshots to the live gold price feed."""
def __init__(self, ttl_seconds: int = 30) -> None:
self._ttl = ttl_seconds
self._cache_price: Optional[float] = None
self._cache_ts: float = 0.0
async def get_anchor_price(self, symbol: str = "XAUUSD") -> Optional[float]:
now = time.time()
if self._cache_price and (now - self._cache_ts) < self._ttl:
return self._cache_price
fetchers: Sequence[Callable[[], Awaitable[Optional[float]]]] = (
self._get_bullionvault_price,
self._get_fallback_price,
)
for fetch in fetchers:
try:
price = await fetch()
except Exception as exc: # pragma: no cover - best effort logging only
logger.warning("Price anchor fetch failed: %s", exc)
continue
if price and price > 0:
self._cache_price = float(price)
self._cache_ts = now
return self._cache_price
return self._cache_price
def get_anchor_price_sync(self, symbol: str = "XAUUSD") -> Optional[float]:
"""Synchronous version that returns cached price only"""
now = time.time()
if self._cache_price and (now - self._cache_ts) < self._ttl:
return self._cache_price
return self._cache_price
async def _get_bullionvault_price(self) -> Optional[float]:
data = await get_bullionvault_gold_price("USD")
return float(data["price"]) if data and data.get("price") else None
async def _get_fallback_price(self) -> Optional[float]:
data = await gold_price_fetcher.get_current_gold_price()
return float(data["price"]) if data and data.get("price") else None
def apply_anchor(self, metrics: PositionMetrics, anchor_price: Optional[float]) -> PositionMetrics:
if not anchor_price or metrics.current_price <= 0:
return metrics
scale = anchor_price / metrics.current_price
if abs(scale - 1.0) < 0.005:
# Already close enough to the anchor, skip unnecessary work
return metrics
if not 0.2 <= scale <= 5:
logger.warning("Skipping unrealistic price anchor scaling (scale=%.4f)", scale)
return metrics
scaled = metrics.model_copy(deep=True)
def scale_value(value: Optional[float], decimals: int = 4) -> Optional[float]:
if value is None:
return None
return round(value * scale, decimals)
def scale_list(values: list[float]) -> list[float]:
return [round(v * scale, 2) for v in values]
scaled.current_price = round(anchor_price, 2)
scaled.previous_close = scale_value(scaled.previous_close, 2)
scaled.high = scale_value(scaled.high, 2)
scaled.low = scale_value(scaled.low, 2)
scaled.atr14 = scale_value(scaled.atr14)
scaled.ema21 = scale_value(scaled.ema21)
scaled.sma55 = scale_value(scaled.sma55)
scaled.sma100 = scale_value(scaled.sma100)
scaled.sma200 = scale_value(scaled.sma200)
scaled.bb_basis = scale_value(scaled.bb_basis)
scaled.bb_upper = scale_value(scaled.bb_upper)
scaled.bb_lower = scale_value(scaled.bb_lower)
scaled.zlsma = scale_value(scaled.zlsma)
scaled.chandelier_long_stop = scale_value(scaled.chandelier_long_stop, 2)
scaled.chandelier_short_stop = scale_value(scaled.chandelier_short_stop, 2)
scaled.momentum12 = scale_value(scaled.momentum12)
scaled.support_levels = scale_list(scaled.support_levels)
scaled.resistance_levels = scale_list(scaled.resistance_levels)
scaled.pattern_signals = [
signal.model_copy(update={"price": scale_value(signal.price, 2)})
for signal in scaled.pattern_signals
]
if scaled.previous_close is not None:
scaled.change = round(scaled.current_price - scaled.previous_close, 4)
if scaled.previous_close:
scaled.change_percent = round((scaled.change / scaled.previous_close) * 100, 4)
else:
scaled.change = scale_value(scaled.change)
if scaled.previous_close:
scaled.change_percent = round((scaled.change or 0.0) / scaled.previous_close * 100, 4)
return scaled
price_anchor_service = PriceAnchorService()