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
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
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()
|
||||
Reference in New Issue
Block a user