Initial commit: Gold Trading Simulator with AI-powered analysis
This commit is contained in:
@@ -0,0 +1,270 @@
|
||||
from typing import List, Dict, Optional
|
||||
from datetime import datetime, timedelta
|
||||
import uuid
|
||||
from app.schemas.schemas import (
|
||||
Alert,
|
||||
AlertType,
|
||||
AlertSeverity,
|
||||
AlertsResponse,
|
||||
PriceData,
|
||||
NewsPriceCorrelation,
|
||||
CorrelationAnalysisResponse,
|
||||
)
|
||||
from app.config import settings
|
||||
|
||||
|
||||
class AlertService:
|
||||
def __init__(self):
|
||||
self.alerts: List[Alert] = []
|
||||
self.price_history: List[PriceData] = []
|
||||
self.last_price: Optional[float] = None
|
||||
self.support_levels: List[float] = []
|
||||
self.resistance_levels: List[float] = []
|
||||
|
||||
def set_support_resistance(self, support: List[float], resistance: List[float]):
|
||||
"""Set support and resistance levels for breach detection"""
|
||||
self.support_levels = support
|
||||
self.resistance_levels = resistance
|
||||
|
||||
def add_price_data(self, price_data: PriceData):
|
||||
"""Add new price data and check for alerts"""
|
||||
self.price_history.append(price_data)
|
||||
|
||||
# Keep only last 1000 data points
|
||||
if len(self.price_history) > 1000:
|
||||
self.price_history = self.price_history[-1000:]
|
||||
|
||||
current_price = price_data.close
|
||||
|
||||
if self.last_price:
|
||||
self._check_price_alerts(current_price, self.last_price)
|
||||
self._check_volatility_alerts(price_data)
|
||||
self._check_support_resistance_breach(current_price)
|
||||
|
||||
self.last_price = current_price
|
||||
|
||||
def _check_price_alerts(self, current_price: float, last_price: float):
|
||||
"""Check for significant price movements"""
|
||||
change_percent = ((current_price - last_price) / last_price) * 100
|
||||
|
||||
threshold = settings.PRICE_ALERT_THRESHOLD
|
||||
|
||||
if abs(change_percent) >= threshold:
|
||||
if change_percent > 0:
|
||||
alert_type = AlertType.PRICE_SPIKE
|
||||
title = f"Gold Price Spike: +{change_percent:.2f}%"
|
||||
severity = AlertSeverity.HIGH if change_percent > 2.0 else AlertSeverity.MEDIUM
|
||||
else:
|
||||
alert_type = AlertType.PRICE_DROP
|
||||
title = f"Gold Price Drop: {change_percent:.2f}%"
|
||||
severity = AlertSeverity.HIGH if change_percent < -2.0 else AlertSeverity.MEDIUM
|
||||
|
||||
alert = Alert(
|
||||
id=str(uuid.uuid4()),
|
||||
type=alert_type,
|
||||
severity=severity,
|
||||
title=title,
|
||||
message=f"Gold price moved from ${last_price:.2f} to ${current_price:.2f} ({change_percent:+.2f}%)",
|
||||
price=current_price,
|
||||
change_percent=change_percent,
|
||||
timestamp=datetime.now(),
|
||||
action_required=severity == AlertSeverity.HIGH,
|
||||
)
|
||||
|
||||
self.alerts.append(alert)
|
||||
|
||||
def _check_volatility_alerts(self, price_data: PriceData):
|
||||
"""Check for high volatility conditions"""
|
||||
if len(self.price_history) < 20:
|
||||
return
|
||||
|
||||
# Calculate ATR-like volatility
|
||||
recent_data = self.price_history[-20:]
|
||||
ranges = [d.high - d.low for d in recent_data]
|
||||
avg_range = sum(ranges) / len(ranges)
|
||||
current_range = price_data.high - price_data.low
|
||||
|
||||
# Alert if current range is 2x average
|
||||
if current_range > avg_range * 2:
|
||||
alert = Alert(
|
||||
id=str(uuid.uuid4()),
|
||||
type=AlertType.HIGH_VOLATILITY,
|
||||
severity=AlertSeverity.MEDIUM,
|
||||
title="High Volatility Detected",
|
||||
message=f"Current price range ${current_range:.2f} is significantly higher than average ${avg_range:.2f}",
|
||||
price=price_data.close,
|
||||
timestamp=datetime.now(),
|
||||
)
|
||||
|
||||
self.alerts.append(alert)
|
||||
|
||||
def _check_support_resistance_breach(self, current_price: float):
|
||||
"""Check if price breached support or resistance levels"""
|
||||
if not self.last_price:
|
||||
return
|
||||
|
||||
# Check resistance breach (upward)
|
||||
for resistance in self.resistance_levels:
|
||||
if self.last_price < resistance <= current_price:
|
||||
alert = Alert(
|
||||
id=str(uuid.uuid4()),
|
||||
type=AlertType.RESISTANCE_BREACH,
|
||||
severity=AlertSeverity.HIGH,
|
||||
title=f"Resistance Breached: ${resistance:.2f}",
|
||||
message=f"Gold price broke above resistance level of ${resistance:.2f}",
|
||||
price=current_price,
|
||||
timestamp=datetime.now(),
|
||||
action_required=True,
|
||||
)
|
||||
self.alerts.append(alert)
|
||||
|
||||
# Check support breach (downward)
|
||||
for support in self.support_levels:
|
||||
if self.last_price > support >= current_price:
|
||||
alert = Alert(
|
||||
id=str(uuid.uuid4()),
|
||||
type=AlertType.SUPPORT_BREACH,
|
||||
severity=AlertSeverity.HIGH,
|
||||
title=f"Support Breached: ${support:.2f}",
|
||||
message=f"Gold price broke below support level of ${support:.2f}",
|
||||
price=current_price,
|
||||
timestamp=datetime.now(),
|
||||
action_required=True,
|
||||
)
|
||||
self.alerts.append(alert)
|
||||
|
||||
def add_news_alert(self, news_title: str, impact: str, sentiment: str):
|
||||
"""Add alert for breaking news"""
|
||||
severity_map = {
|
||||
"HIGH": AlertSeverity.CRITICAL,
|
||||
"MEDIUM": AlertSeverity.HIGH,
|
||||
"LOW": AlertSeverity.MEDIUM,
|
||||
}
|
||||
|
||||
alert = Alert(
|
||||
id=str(uuid.uuid4()),
|
||||
type=AlertType.NEWS_BREAKING,
|
||||
severity=severity_map.get(impact, AlertSeverity.MEDIUM),
|
||||
title=f"Breaking: {news_title[:50]}...",
|
||||
message=f"High-impact news detected: {news_title}",
|
||||
timestamp=datetime.now(),
|
||||
action_required=impact == "HIGH",
|
||||
)
|
||||
|
||||
self.alerts.append(alert)
|
||||
|
||||
def add_economic_event_alert(self, event_title: str, importance: str):
|
||||
"""Add alert for upcoming economic event"""
|
||||
severity_map = {
|
||||
"HIGH": AlertSeverity.HIGH,
|
||||
"MEDIUM": AlertSeverity.MEDIUM,
|
||||
"LOW": AlertSeverity.LOW,
|
||||
}
|
||||
|
||||
alert = Alert(
|
||||
id=str(uuid.uuid4()),
|
||||
type=AlertType.ECONOMIC_EVENT,
|
||||
severity=severity_map.get(importance, AlertSeverity.MEDIUM),
|
||||
title=f"Upcoming: {event_title}",
|
||||
message=f"Important economic event scheduled: {event_title}",
|
||||
timestamp=datetime.now(),
|
||||
action_required=importance == "HIGH",
|
||||
)
|
||||
|
||||
self.alerts.append(alert)
|
||||
|
||||
def get_alerts(self, limit: int = 50) -> AlertsResponse:
|
||||
"""Get recent alerts"""
|
||||
# Sort by timestamp (newest first)
|
||||
sorted_alerts = sorted(self.alerts, key=lambda x: x.timestamp, reverse=True)
|
||||
|
||||
# Limit results
|
||||
recent_alerts = sorted_alerts[:limit]
|
||||
|
||||
# Count critical alerts
|
||||
critical_count = sum(1 for a in recent_alerts if a.severity == AlertSeverity.CRITICAL)
|
||||
|
||||
# For MVP, all alerts are unread
|
||||
unread_count = len(recent_alerts)
|
||||
|
||||
return AlertsResponse(
|
||||
alerts=recent_alerts,
|
||||
critical_count=critical_count,
|
||||
unread_count=unread_count,
|
||||
)
|
||||
|
||||
def clear_old_alerts(self, hours: int = 24):
|
||||
"""Remove alerts older than specified hours"""
|
||||
cutoff = datetime.now() - timedelta(hours=hours)
|
||||
self.alerts = [a for a in self.alerts if a.timestamp > cutoff]
|
||||
|
||||
def analyze_news_price_correlation(
|
||||
self,
|
||||
news_articles: List,
|
||||
price_data: List[PriceData],
|
||||
) -> CorrelationAnalysisResponse:
|
||||
"""Analyze correlation between news and price movements"""
|
||||
correlations = []
|
||||
|
||||
for article in news_articles:
|
||||
news_time = article.published_at
|
||||
|
||||
# Find price before and after news
|
||||
price_before = None
|
||||
price_after = None
|
||||
|
||||
for i, data in enumerate(price_data):
|
||||
data_time = datetime.fromtimestamp(data.time)
|
||||
|
||||
# Price before news (within 1 hour before)
|
||||
if data_time < news_time and (news_time - data_time).total_seconds() < 3600:
|
||||
price_before = data.close
|
||||
|
||||
# Price after news (within 1 hour after)
|
||||
if data_time > news_time and (data_time - news_time).total_seconds() < 3600:
|
||||
if not price_after: # Take first price after
|
||||
price_after = data.close
|
||||
|
||||
if price_before and price_after:
|
||||
price_change = price_after - price_before
|
||||
price_change_percent = (price_change / price_before) * 100
|
||||
time_delta = 60 # Approximate minutes
|
||||
|
||||
# Determine correlation strength
|
||||
if abs(price_change_percent) > 1.0:
|
||||
strength = "STRONG"
|
||||
elif abs(price_change_percent) > 0.5:
|
||||
strength = "MODERATE"
|
||||
else:
|
||||
strength = "WEAK"
|
||||
|
||||
correlation = NewsPriceCorrelation(
|
||||
news_id=article.id,
|
||||
news_title=article.title,
|
||||
news_time=news_time,
|
||||
price_before=price_before,
|
||||
price_after=price_after,
|
||||
price_change=price_change,
|
||||
price_change_percent=price_change_percent,
|
||||
time_delta_minutes=time_delta,
|
||||
correlation_strength=strength,
|
||||
)
|
||||
|
||||
correlations.append(correlation)
|
||||
|
||||
# Calculate statistics
|
||||
significant_events = sum(1 for c in correlations if c.correlation_strength in ["STRONG", "MODERATE"])
|
||||
avg_impact = (
|
||||
sum(abs(c.price_change_percent) for c in correlations) / len(correlations)
|
||||
if correlations else 0.0
|
||||
)
|
||||
|
||||
return CorrelationAnalysisResponse(
|
||||
correlations=correlations[:20], # Limit to 20 most recent
|
||||
significant_events=significant_events,
|
||||
avg_price_impact=avg_impact,
|
||||
)
|
||||
|
||||
|
||||
# Global instance
|
||||
alert_service = AlertService()
|
||||
Reference in New Issue
Block a user