| |
| """ |
| Technical Indicators API Router (PRODUCTION SAFE) |
| Provides API endpoints for calculating technical indicators on cryptocurrency data. |
| Includes: Bollinger Bands, Stochastic RSI, ATR, SMA, EMA, MACD, RSI |
| |
| CRITICAL RULES: |
| - HTTP 400 for insufficient data (NOT HTTP 500) |
| - Strict minimum candle requirements enforced |
| - NaN/Infinity values sanitized before response |
| - Comprehensive logging for all operations |
| - Never crash - always return valid JSON |
| """ |
|
|
| from fastapi import APIRouter, HTTPException, Query |
| from fastapi.responses import JSONResponse |
| from pydantic import BaseModel, Field |
| from typing import List, Dict, Any, Optional |
| from datetime import datetime |
| import logging |
| import math |
| import httpx |
|
|
| logger = logging.getLogger(__name__) |
|
|
| router = APIRouter(prefix="/api/indicators", tags=["Technical Indicators"]) |
|
|
| |
| |
| |
| MIN_CANDLES = { |
| "SMA": 20, |
| "EMA": 20, |
| "RSI": 15, |
| "ATR": 15, |
| "MACD": 35, |
| "STOCH_RSI": 50, |
| "BOLLINGER_BANDS": 20 |
| } |
|
|
|
|
| |
| |
| |
|
|
| class OHLCVData(BaseModel): |
| """OHLCV data model""" |
| timestamp: int |
| open: float |
| high: float |
| low: float |
| close: float |
| volume: float |
|
|
|
|
| class IndicatorRequest(BaseModel): |
| """Request model for indicator calculation""" |
| symbol: str = Field(default="BTC", description="Cryptocurrency symbol") |
| timeframe: str = Field(default="1h", description="Timeframe (1m, 5m, 15m, 1h, 4h, 1d)") |
| ohlcv: Optional[List[OHLCVData]] = Field(default=None, description="OHLCV data array") |
| period: int = Field(default=14, description="Indicator period") |
|
|
|
|
| class BollingerBandsResponse(BaseModel): |
| """Bollinger Bands response model""" |
| upper: float |
| middle: float |
| lower: float |
| bandwidth: float |
| percent_b: float |
| signal: str |
| description: str |
|
|
|
|
| class StochRSIResponse(BaseModel): |
| """Stochastic RSI response model""" |
| value: float |
| k_line: float |
| d_line: float |
| signal: str |
| description: str |
|
|
|
|
| class ATRResponse(BaseModel): |
| """Average True Range response model""" |
| value: float |
| percent: float |
| volatility_level: str |
| signal: str |
| description: str |
|
|
|
|
| class SMAResponse(BaseModel): |
| """Simple Moving Average response model""" |
| sma20: float |
| sma50: float |
| sma200: Optional[float] |
| price_vs_sma20: str |
| price_vs_sma50: str |
| trend: str |
| signal: str |
| description: str |
|
|
|
|
| class EMAResponse(BaseModel): |
| """Exponential Moving Average response model""" |
| ema12: float |
| ema26: float |
| ema50: Optional[float] |
| trend: str |
| signal: str |
| description: str |
|
|
|
|
| class MACDResponse(BaseModel): |
| """MACD response model""" |
| macd_line: float |
| signal_line: float |
| histogram: float |
| trend: str |
| signal: str |
| description: str |
|
|
|
|
| class RSIResponse(BaseModel): |
| """RSI response model""" |
| value: float |
| signal: str |
| description: str |
|
|
|
|
| class ComprehensiveIndicatorsResponse(BaseModel): |
| """All indicators combined response""" |
| symbol: str |
| timeframe: str |
| timestamp: str |
| current_price: float |
| bollinger_bands: BollingerBandsResponse |
| stoch_rsi: StochRSIResponse |
| atr: ATRResponse |
| sma: SMAResponse |
| ema: EMAResponse |
| macd: MACDResponse |
| rsi: RSIResponse |
| overall_signal: str |
| recommendation: str |
|
|
|
|
| |
| |
| |
|
|
| def sanitize_value(value: Any) -> Optional[float]: |
| """ |
| Sanitize a numeric value - remove NaN, Infinity, None |
| Returns None if value is invalid, otherwise returns the float value |
| """ |
| if value is None: |
| return None |
| try: |
| val = float(value) |
| if math.isnan(val) or math.isinf(val): |
| return None |
| return val |
| except (ValueError, TypeError): |
| return None |
|
|
|
|
| def sanitize_dict(data: Dict[str, Any]) -> Dict[str, Any]: |
| """ |
| Sanitize all numeric values in a dictionary |
| Replace NaN/Infinity with None or 0 depending on context |
| """ |
| sanitized = {} |
| for key, value in data.items(): |
| if isinstance(value, dict): |
| sanitized[key] = sanitize_dict(value) |
| elif isinstance(value, (int, float)): |
| clean_val = sanitize_value(value) |
| sanitized[key] = clean_val if clean_val is not None else 0 |
| else: |
| sanitized[key] = value |
| return sanitized |
|
|
|
|
| def validate_ohlcv_data(ohlcv: Optional[Dict[str, Any]], min_candles: int, symbol: str, indicator: str) -> tuple[bool, Optional[List[float]], Optional[str]]: |
| """ |
| Validate OHLCV data and extract prices |
| |
| Returns: |
| (is_valid, prices, error_message) |
| """ |
| if not ohlcv: |
| logger.warning(f"❌ {indicator} - {symbol}: No OHLCV data received") |
| return False, None, "No market data available" |
| |
| if "prices" not in ohlcv: |
| logger.warning(f"❌ {indicator} - {symbol}: OHLCV missing 'prices' key") |
| return False, None, "Invalid market data format" |
| |
| prices = [p[1] for p in ohlcv["prices"] if len(p) >= 2] |
| |
| if not prices: |
| logger.warning(f"❌ {indicator} - {symbol}: Empty price array") |
| return False, None, "No price data available" |
| |
| if len(prices) < min_candles: |
| logger.warning(f"❌ {indicator} - {symbol}: Insufficient candles ({len(prices)} < {min_candles} required)") |
| return False, None, f"Insufficient market data: need at least {min_candles} candles, got {len(prices)}" |
| |
| logger.info(f"✅ {indicator} - {symbol}: Validated {len(prices)} candles (required: {min_candles})") |
| return True, prices, None |
|
|
|
|
| |
| |
| |
|
|
| def calculate_sma(prices: List[float], period: int) -> float: |
| """Calculate Simple Moving Average""" |
| if len(prices) < period: |
| return prices[-1] if prices else 0 |
| return sum(prices[-period:]) / period |
|
|
|
|
| def calculate_ema(prices: List[float], period: int) -> float: |
| """Calculate Exponential Moving Average""" |
| if len(prices) < period: |
| return prices[-1] if prices else 0 |
| |
| multiplier = 2 / (period + 1) |
| ema = sum(prices[:period]) / period |
| |
| for price in prices[period:]: |
| ema = (price * multiplier) + (ema * (1 - multiplier)) |
| |
| return ema |
|
|
|
|
| def calculate_rsi(prices: List[float], period: int = 14) -> float: |
| """Calculate Relative Strength Index""" |
| if len(prices) < period + 1: |
| return 50.0 |
| |
| deltas = [prices[i] - prices[i-1] for i in range(1, len(prices))] |
| gains = [d if d > 0 else 0 for d in deltas[-period:]] |
| losses = [-d if d < 0 else 0 for d in deltas[-period:]] |
| |
| avg_gain = sum(gains) / period |
| avg_loss = sum(losses) / period |
| |
| if avg_loss == 0: |
| return 100.0 if avg_gain > 0 else 50.0 |
| |
| rs = avg_gain / avg_loss |
| return 100 - (100 / (1 + rs)) |
|
|
|
|
| def calculate_bollinger_bands(prices: List[float], period: int = 20, std_dev: float = 2) -> Dict[str, float]: |
| """Calculate Bollinger Bands""" |
| if len(prices) < period: |
| current = prices[-1] if prices else 0 |
| return { |
| "upper": current, |
| "middle": current, |
| "lower": current, |
| "bandwidth": 0, |
| "percent_b": 50 |
| } |
| |
| recent_prices = prices[-period:] |
| middle = sum(recent_prices) / period |
| |
| |
| variance = sum((p - middle) ** 2 for p in recent_prices) / period |
| std = variance ** 0.5 |
| |
| upper = middle + (std_dev * std) |
| lower = middle - (std_dev * std) |
| |
| |
| bandwidth = ((upper - lower) / middle) * 100 if middle > 0 else 0 |
| |
| |
| current_price = prices[-1] |
| if upper != lower: |
| percent_b = ((current_price - lower) / (upper - lower)) * 100 |
| else: |
| percent_b = 50 |
| |
| return { |
| "upper": round(upper, 8), |
| "middle": round(middle, 8), |
| "lower": round(lower, 8), |
| "bandwidth": round(bandwidth, 2), |
| "percent_b": round(percent_b, 2) |
| } |
|
|
|
|
| def calculate_stoch_rsi(prices: List[float], rsi_period: int = 14, stoch_period: int = 14) -> Dict[str, float]: |
| """Calculate Stochastic RSI""" |
| if len(prices) < rsi_period + stoch_period: |
| return {"value": 50, "k_line": 50, "d_line": 50} |
| |
| |
| rsi_values = [] |
| for i in range(stoch_period + 3): |
| end_idx = len(prices) - stoch_period + i + 1 |
| if end_idx > rsi_period: |
| slice_prices = prices[:end_idx] |
| rsi_values.append(calculate_rsi(slice_prices, rsi_period)) |
| |
| if len(rsi_values) < stoch_period: |
| return {"value": 50, "k_line": 50, "d_line": 50} |
| |
| recent_rsi = rsi_values[-stoch_period:] |
| rsi_high = max(recent_rsi) |
| rsi_low = min(recent_rsi) |
| |
| current_rsi = rsi_values[-1] |
| |
| if rsi_high == rsi_low: |
| stoch_rsi = 50 |
| else: |
| stoch_rsi = ((current_rsi - rsi_low) / (rsi_high - rsi_low)) * 100 |
| |
| |
| k_line = stoch_rsi |
| |
| |
| if len(rsi_values) >= 3: |
| k_values = [] |
| for i in range(3): |
| idx = -3 + i |
| window = rsi_values[idx - stoch_period + 1:idx + 1] if idx + 1 <= 0 else recent_rsi |
| if not window: |
| k_values.append(50) |
| continue |
| r_high = max(window) |
| r_low = min(window) |
| curr = rsi_values[idx] |
| if r_high != r_low: |
| k_values.append(((curr - r_low) / (r_high - r_low)) * 100) |
| else: |
| k_values.append(50) |
| d_line = sum(k_values) / 3 |
| else: |
| d_line = k_line |
| |
| return { |
| "value": round(stoch_rsi, 2), |
| "k_line": round(k_line, 2), |
| "d_line": round(d_line, 2) |
| } |
|
|
|
|
| def calculate_atr(highs: List[float], lows: List[float], closes: List[float], period: int = 14) -> float: |
| """Calculate Average True Range""" |
| if len(closes) < period + 1: |
| if len(highs) > 0 and len(lows) > 0: |
| return highs[-1] - lows[-1] |
| return 0 |
| |
| true_ranges = [] |
| for i in range(1, len(closes)): |
| high = highs[i] |
| low = lows[i] |
| prev_close = closes[i-1] |
| |
| tr = max( |
| high - low, |
| abs(high - prev_close), |
| abs(low - prev_close) |
| ) |
| true_ranges.append(tr) |
| |
| |
| if len(true_ranges) < period: |
| return sum(true_ranges) / len(true_ranges) if true_ranges else 0 |
| |
| return sum(true_ranges[-period:]) / period |
|
|
|
|
| def calculate_macd(prices: List[float], fast: int = 12, slow: int = 26, signal: int = 9) -> Dict[str, float]: |
| """Calculate MACD""" |
| if len(prices) < slow + signal: |
| return {"macd_line": 0, "signal_line": 0, "histogram": 0} |
| |
| ema_fast = calculate_ema(prices, fast) |
| ema_slow = calculate_ema(prices, slow) |
| macd_line = ema_fast - ema_slow |
| |
| |
| |
| macd_values = [] |
| for i in range(signal + 5): |
| idx = len(prices) - signal - 5 + i |
| if idx > slow: |
| slice_prices = prices[:idx+1] |
| ef = calculate_ema(slice_prices, fast) |
| es = calculate_ema(slice_prices, slow) |
| macd_values.append(ef - es) |
| |
| if len(macd_values) >= signal: |
| signal_line = calculate_ema(macd_values, signal) |
| else: |
| signal_line = macd_line |
| |
| histogram = macd_line - signal_line |
| |
| return { |
| "macd_line": round(macd_line, 8), |
| "signal_line": round(signal_line, 8), |
| "histogram": round(histogram, 8) |
| } |
|
|
|
|
| |
| |
| |
|
|
| @router.get("/services") |
| async def list_indicator_services(): |
| """List all available technical indicator services""" |
| return { |
| "success": True, |
| "services": [ |
| { |
| "id": "bollinger_bands", |
| "name": "Bollinger Bands", |
| "description": "Volatility bands placed above and below a moving average", |
| "endpoint": "/api/indicators/bollinger-bands", |
| "parameters": ["symbol", "timeframe", "period", "std_dev"], |
| "icon": "📊", |
| "category": "volatility" |
| }, |
| { |
| "id": "stoch_rsi", |
| "name": "Stochastic RSI", |
| "description": "Combines Stochastic oscillator with RSI for momentum", |
| "endpoint": "/api/indicators/stoch-rsi", |
| "parameters": ["symbol", "timeframe", "rsi_period", "stoch_period"], |
| "icon": "📈", |
| "category": "momentum" |
| }, |
| { |
| "id": "atr", |
| "name": "Average True Range (ATR)", |
| "description": "Measures market volatility and price movement", |
| "endpoint": "/api/indicators/atr", |
| "parameters": ["symbol", "timeframe", "period"], |
| "icon": "📉", |
| "category": "volatility" |
| }, |
| { |
| "id": "sma", |
| "name": "Simple Moving Average (SMA)", |
| "description": "Average price over specified periods (20, 50, 200)", |
| "endpoint": "/api/indicators/sma", |
| "parameters": ["symbol", "timeframe"], |
| "icon": "〰️", |
| "category": "trend" |
| }, |
| { |
| "id": "ema", |
| "name": "Exponential Moving Average (EMA)", |
| "description": "Weighted moving average giving more weight to recent prices", |
| "endpoint": "/api/indicators/ema", |
| "parameters": ["symbol", "timeframe"], |
| "icon": "📐", |
| "category": "trend" |
| }, |
| { |
| "id": "macd", |
| "name": "MACD", |
| "description": "Moving Average Convergence Divergence - trend following momentum", |
| "endpoint": "/api/indicators/macd", |
| "parameters": ["symbol", "timeframe", "fast", "slow", "signal"], |
| "icon": "🔀", |
| "category": "momentum" |
| }, |
| { |
| "id": "rsi", |
| "name": "RSI", |
| "description": "Relative Strength Index - momentum oscillator (0-100)", |
| "endpoint": "/api/indicators/rsi", |
| "parameters": ["symbol", "timeframe", "period"], |
| "icon": "💪", |
| "category": "momentum" |
| }, |
| { |
| "id": "comprehensive", |
| "name": "Comprehensive Analysis", |
| "description": "All indicators combined with trading signals", |
| "endpoint": "/api/indicators/comprehensive", |
| "parameters": ["symbol", "timeframe"], |
| "icon": "🎯", |
| "category": "analysis" |
| } |
| ], |
| "categories": { |
| "volatility": "Measure price volatility and potential breakouts", |
| "momentum": "Identify overbought/oversold conditions", |
| "trend": "Determine market direction and strength", |
| "analysis": "Complete multi-indicator analysis" |
| }, |
| "timestamp": datetime.utcnow().isoformat() + "Z" |
| } |
|
|
|
|
| @router.get("/bollinger-bands") |
| async def get_bollinger_bands( |
| symbol: str = Query(default="BTC", description="Cryptocurrency symbol"), |
| timeframe: str = Query(default="1h", description="Timeframe"), |
| period: int = Query(default=20, description="Period for calculation"), |
| std_dev: float = Query(default=2.0, description="Standard deviation multiplier") |
| ): |
| """Calculate Bollinger Bands for a symbol - PRODUCTION SAFE""" |
| indicator_name = "BOLLINGER_BANDS" |
| logger.info(f"📊 {indicator_name} - Endpoint called: symbol={symbol}, timeframe={timeframe}, period={period}, std_dev={std_dev}") |
| |
| try: |
| |
| if period < 1 or period > 100: |
| logger.warning(f"❌ {indicator_name} - Invalid period: {period}") |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": f"Invalid period: must be between 1 and 100, got {period}" |
| } |
| ) |
| if std_dev <= 0 or std_dev > 5: |
| logger.warning(f"❌ {indicator_name} - Invalid std_dev: {std_dev}") |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": f"Invalid std_dev: must be between 0 and 5, got {std_dev}" |
| } |
| ) |
| |
| |
| from backend.services.coingecko_client import coingecko_client |
| |
| |
| timeframe_days = {"1m": 1, "5m": 1, "15m": 1, "1h": 7, "4h": 30, "1d": 90} |
| days = timeframe_days.get(timeframe, 7) |
| |
| try: |
| ohlcv = await coingecko_client.get_ohlcv(symbol, days=days) |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Failed to fetch OHLCV: {e}") |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": "Unable to fetch market data for this symbol" |
| } |
| ) |
| |
| |
| min_required = MIN_CANDLES["BOLLINGER_BANDS"] |
| is_valid, prices, error_msg = validate_ohlcv_data(ohlcv, min_required, symbol, indicator_name) |
| |
| if not is_valid: |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": error_msg, |
| "symbol": symbol.upper(), |
| "timeframe": timeframe, |
| "indicator": "bollinger_bands", |
| "data_points": 0 |
| } |
| ) |
| |
| |
| try: |
| bb = calculate_bollinger_bands(prices, period, std_dev) |
| current_price = prices[-1] if prices else 0 |
| |
| |
| bb = sanitize_dict(bb) |
| current_price = sanitize_value(current_price) or 0 |
| |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Calculation failed: {e}", exc_info=True) |
| return JSONResponse( |
| status_code=500, |
| content={ |
| "error": True, |
| "message": "Internal indicator calculation error" |
| } |
| ) |
| |
| |
| if bb["percent_b"] > 95: |
| signal = "overbought" |
| description = "Price at upper band - potential reversal or breakout" |
| elif bb["percent_b"] < 5: |
| signal = "oversold" |
| description = "Price at lower band - potential bounce or breakdown" |
| elif bb["percent_b"] > 70: |
| signal = "bullish_caution" |
| description = "Price approaching upper band - watch for resistance" |
| elif bb["percent_b"] < 30: |
| signal = "bearish_caution" |
| description = "Price approaching lower band - watch for support" |
| else: |
| signal = "neutral" |
| description = "Price within normal range - no extreme conditions" |
| |
| logger.info(f"✅ {indicator_name} - Success: symbol={symbol}, percent_b={bb['percent_b']:.2f}, signal={signal}") |
| |
| return { |
| "success": True, |
| "symbol": symbol.upper(), |
| "timeframe": timeframe, |
| "indicator": "bollinger_bands", |
| "value": bb, |
| "data": bb, |
| "current_price": round(current_price, 8), |
| "data_points": len(prices), |
| "signal": signal, |
| "description": description, |
| "timestamp": datetime.utcnow().isoformat() + "Z", |
| "source": "coingecko" |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Unexpected error: {e}", exc_info=True) |
| return JSONResponse( |
| status_code=500, |
| content={ |
| "error": True, |
| "message": "Internal server error" |
| } |
| ) |
|
|
|
|
| @router.get("/stoch-rsi") |
| async def get_stoch_rsi( |
| symbol: str = Query(default="BTC", description="Cryptocurrency symbol"), |
| timeframe: str = Query(default="1h", description="Timeframe"), |
| rsi_period: int = Query(default=14, description="RSI period"), |
| stoch_period: int = Query(default=14, description="Stochastic period") |
| ): |
| """Calculate Stochastic RSI for a symbol - PRODUCTION SAFE""" |
| indicator_name = "STOCH_RSI" |
| logger.info(f"📊 {indicator_name} - Endpoint called: symbol={symbol}, timeframe={timeframe}, rsi_period={rsi_period}, stoch_period={stoch_period}") |
| |
| try: |
| |
| if rsi_period < 1 or rsi_period > 100: |
| logger.warning(f"❌ {indicator_name} - Invalid rsi_period: {rsi_period}") |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": f"Invalid rsi_period: must be between 1 and 100, got {rsi_period}" |
| } |
| ) |
| if stoch_period < 1 or stoch_period > 100: |
| logger.warning(f"❌ {indicator_name} - Invalid stoch_period: {stoch_period}") |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": f"Invalid stoch_period: must be between 1 and 100, got {stoch_period}" |
| } |
| ) |
| |
| |
| from backend.services.coingecko_client import coingecko_client |
| |
| timeframe_days = {"1m": 1, "5m": 1, "15m": 1, "1h": 7, "4h": 30, "1d": 90} |
| days = timeframe_days.get(timeframe, 7) |
| |
| try: |
| ohlcv = await coingecko_client.get_ohlcv(symbol, days=days) |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Failed to fetch OHLCV: {e}") |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": "Unable to fetch market data for this symbol" |
| } |
| ) |
| |
| |
| min_required = MIN_CANDLES["STOCH_RSI"] |
| is_valid, prices, error_msg = validate_ohlcv_data(ohlcv, min_required, symbol, indicator_name) |
| |
| if not is_valid: |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": error_msg, |
| "symbol": symbol.upper(), |
| "timeframe": timeframe, |
| "indicator": "stoch_rsi", |
| "data_points": 0 |
| } |
| ) |
| |
| |
| try: |
| stoch = calculate_stoch_rsi(prices, rsi_period, stoch_period) |
| |
| |
| stoch = sanitize_dict(stoch) |
| |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Calculation failed: {e}", exc_info=True) |
| return JSONResponse( |
| status_code=500, |
| content={ |
| "error": True, |
| "message": "Internal indicator calculation error" |
| } |
| ) |
| |
| |
| if stoch["value"] > 80: |
| signal = "overbought" |
| description = "Extreme overbought - high probability of pullback" |
| elif stoch["value"] < 20: |
| signal = "oversold" |
| description = "Extreme oversold - high probability of bounce" |
| elif stoch["k_line"] > stoch["d_line"] and stoch["value"] < 50: |
| signal = "bullish_crossover" |
| description = "K crossed above D in oversold territory - bullish signal" |
| elif stoch["k_line"] < stoch["d_line"] and stoch["value"] > 50: |
| signal = "bearish_crossover" |
| description = "K crossed below D in overbought territory - bearish signal" |
| else: |
| signal = "neutral" |
| description = "Normal momentum range - no extreme conditions" |
| |
| logger.info(f"✅ {indicator_name} - Success: symbol={symbol}, value={stoch['value']:.2f}, signal={signal}") |
| |
| return { |
| "success": True, |
| "symbol": symbol.upper(), |
| "timeframe": timeframe, |
| "indicator": "stoch_rsi", |
| "value": stoch, |
| "data": stoch, |
| "data_points": len(prices), |
| "signal": signal, |
| "description": description, |
| "timestamp": datetime.utcnow().isoformat() + "Z", |
| "source": "coingecko" |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Unexpected error: {e}", exc_info=True) |
| return JSONResponse( |
| status_code=500, |
| content={ |
| "error": True, |
| "message": "Internal server error" |
| } |
| ) |
|
|
|
|
| @router.get("/atr") |
| async def get_atr( |
| symbol: str = Query(default="BTC", description="Cryptocurrency symbol"), |
| timeframe: str = Query(default="1h", description="Timeframe"), |
| period: int = Query(default=14, description="ATR period") |
| ): |
| """Calculate Average True Range for a symbol - PRODUCTION SAFE""" |
| indicator_name = "ATR" |
| logger.info(f"📊 {indicator_name} - Endpoint called: symbol={symbol}, timeframe={timeframe}, period={period}") |
| |
| try: |
| |
| if period < 1 or period > 100: |
| logger.warning(f"❌ {indicator_name} - Invalid period: {period}") |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": f"Invalid period: must be between 1 and 100, got {period}" |
| } |
| ) |
| |
| |
| from backend.services.coingecko_client import coingecko_client |
| |
| timeframe_days = {"1m": 1, "5m": 1, "15m": 1, "1h": 7, "4h": 30, "1d": 90} |
| days = timeframe_days.get(timeframe, 7) |
| |
| try: |
| ohlcv = await coingecko_client.get_ohlcv(symbol, days=days) |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Failed to fetch OHLCV: {e}") |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": "Unable to fetch market data for this symbol" |
| } |
| ) |
| |
| |
| min_required = MIN_CANDLES["ATR"] |
| is_valid, prices, error_msg = validate_ohlcv_data(ohlcv, min_required, symbol, indicator_name) |
| |
| if not is_valid: |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": error_msg, |
| "symbol": symbol.upper(), |
| "timeframe": timeframe, |
| "indicator": "atr", |
| "data_points": 0 |
| } |
| ) |
| |
| |
| try: |
| |
| highs = [p * 1.005 for p in prices] |
| lows = [p * 0.995 for p in prices] |
| |
| atr_value = calculate_atr(highs, lows, prices, period) |
| current_price = prices[-1] if prices else 1 |
| atr_percent = (atr_value / current_price) * 100 if current_price > 0 else 0 |
| |
| |
| atr_value = sanitize_value(atr_value) or 0 |
| atr_percent = sanitize_value(atr_percent) or 0 |
| current_price = sanitize_value(current_price) or 0 |
| |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Calculation failed: {e}", exc_info=True) |
| return JSONResponse( |
| status_code=500, |
| content={ |
| "error": True, |
| "message": "Internal indicator calculation error" |
| } |
| ) |
| |
| |
| if atr_percent > 5: |
| volatility_level = "very_high" |
| signal = "high_risk" |
| description = "Very high volatility - increase position sizing caution" |
| elif atr_percent > 3: |
| volatility_level = "high" |
| signal = "caution" |
| description = "High volatility - wider stop losses recommended" |
| elif atr_percent > 1.5: |
| volatility_level = "medium" |
| signal = "neutral" |
| description = "Normal volatility - standard position sizing" |
| else: |
| volatility_level = "low" |
| signal = "breakout_watch" |
| description = "Low volatility - potential breakout forming" |
| |
| logger.info(f"✅ {indicator_name} - Success: symbol={symbol}, value={atr_value:.2f}, volatility={volatility_level}") |
| |
| return { |
| "success": True, |
| "symbol": symbol.upper(), |
| "timeframe": timeframe, |
| "indicator": "atr", |
| "value": { |
| "value": round(atr_value, 8), |
| "percent": round(atr_percent, 2) |
| }, |
| "data": { |
| "value": round(atr_value, 8), |
| "percent": round(atr_percent, 2) |
| }, |
| "current_price": round(current_price, 8), |
| "data_points": len(prices), |
| "volatility_level": volatility_level, |
| "signal": signal, |
| "description": description, |
| "timestamp": datetime.utcnow().isoformat() + "Z", |
| "source": "coingecko" |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Unexpected error: {e}", exc_info=True) |
| return JSONResponse( |
| status_code=500, |
| content={ |
| "error": True, |
| "message": "Internal server error" |
| } |
| ) |
|
|
|
|
| @router.get("/sma") |
| async def get_sma( |
| symbol: str = Query(default="BTC", description="Cryptocurrency symbol"), |
| timeframe: str = Query(default="1h", description="Timeframe") |
| ): |
| """Calculate Simple Moving Averages (20, 50, 200) for a symbol - PRODUCTION SAFE""" |
| indicator_name = "SMA" |
| logger.info(f"📊 {indicator_name} - Endpoint called: symbol={symbol}, timeframe={timeframe}") |
| |
| try: |
| |
| from backend.services.coingecko_client import coingecko_client |
| |
| try: |
| |
| ohlcv = await coingecko_client.get_ohlcv(symbol, days=365) |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Failed to fetch OHLCV: {e}") |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": "Unable to fetch market data for this symbol" |
| } |
| ) |
| |
| |
| min_required = MIN_CANDLES["SMA"] |
| is_valid, prices, error_msg = validate_ohlcv_data(ohlcv, min_required, symbol, indicator_name) |
| |
| if not is_valid: |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": error_msg, |
| "symbol": symbol.upper(), |
| "timeframe": timeframe, |
| "indicator": "sma", |
| "data_points": 0 |
| } |
| ) |
| |
| |
| try: |
| current_price = prices[-1] if prices else 0 |
| |
| sma20 = calculate_sma(prices, 20) |
| sma50 = calculate_sma(prices, 50) |
| sma200 = calculate_sma(prices, 200) if len(prices) >= 200 else None |
| |
| |
| current_price = sanitize_value(current_price) or 0 |
| sma20 = sanitize_value(sma20) or 0 |
| sma50 = sanitize_value(sma50) or 0 |
| sma200 = sanitize_value(sma200) if sma200 is not None else None |
| |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Calculation failed: {e}", exc_info=True) |
| return JSONResponse( |
| status_code=500, |
| content={ |
| "error": True, |
| "message": "Internal indicator calculation error" |
| } |
| ) |
| |
| price_vs_sma20 = "above" if current_price > sma20 else "below" |
| price_vs_sma50 = "above" if current_price > sma50 else "below" |
| |
| |
| if current_price > sma20 > sma50: |
| trend = "strong_bullish" |
| signal = "buy" |
| description = "Strong uptrend - price above rising SMAs" |
| elif current_price > sma20 and current_price > sma50: |
| trend = "bullish" |
| signal = "buy" |
| description = "Bullish trend - price above major SMAs" |
| elif current_price < sma20 < sma50: |
| trend = "strong_bearish" |
| signal = "sell" |
| description = "Strong downtrend - price below falling SMAs" |
| elif current_price < sma20 and current_price < sma50: |
| trend = "bearish" |
| signal = "sell" |
| description = "Bearish trend - price below major SMAs" |
| else: |
| trend = "neutral" |
| signal = "hold" |
| description = "Mixed signals - waiting for clearer direction" |
| |
| logger.info(f"✅ {indicator_name} - Success: symbol={symbol}, trend={trend}") |
| |
| return { |
| "success": True, |
| "symbol": symbol.upper(), |
| "timeframe": timeframe, |
| "indicator": "sma", |
| "value": { |
| "sma20": round(sma20, 8), |
| "sma50": round(sma50, 8), |
| "sma200": round(sma200, 8) if sma200 else None |
| }, |
| "data": { |
| "sma20": round(sma20, 8), |
| "sma50": round(sma50, 8), |
| "sma200": round(sma200, 8) if sma200 else None |
| }, |
| "current_price": round(current_price, 8), |
| "data_points": len(prices), |
| "price_vs_sma20": price_vs_sma20, |
| "price_vs_sma50": price_vs_sma50, |
| "trend": trend, |
| "signal": signal, |
| "description": description, |
| "timestamp": datetime.utcnow().isoformat() + "Z", |
| "source": "coingecko" |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Unexpected error: {e}", exc_info=True) |
| return JSONResponse( |
| status_code=500, |
| content={ |
| "error": True, |
| "message": "Internal server error" |
| } |
| ) |
|
|
|
|
| @router.get("/ema") |
| async def get_ema( |
| symbol: str = Query(default="BTC", description="Cryptocurrency symbol"), |
| timeframe: str = Query(default="1h", description="Timeframe") |
| ): |
| """Calculate Exponential Moving Averages for a symbol - PRODUCTION SAFE""" |
| indicator_name = "EMA" |
| logger.info(f"📊 {indicator_name} - Endpoint called: symbol={symbol}, timeframe={timeframe}") |
| |
| try: |
| |
| from backend.services.coingecko_client import coingecko_client |
| |
| try: |
| ohlcv = await coingecko_client.get_ohlcv(symbol, days=90) |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Failed to fetch OHLCV: {e}") |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": "Unable to fetch market data for this symbol" |
| } |
| ) |
| |
| |
| min_required = MIN_CANDLES["EMA"] |
| is_valid, prices, error_msg = validate_ohlcv_data(ohlcv, min_required, symbol, indicator_name) |
| |
| if not is_valid: |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": error_msg, |
| "symbol": symbol.upper(), |
| "timeframe": timeframe, |
| "indicator": "ema", |
| "data_points": 0 |
| } |
| ) |
| |
| |
| try: |
| current_price = prices[-1] if prices else 0 |
| |
| ema12 = calculate_ema(prices, 12) |
| ema26 = calculate_ema(prices, 26) |
| ema50 = calculate_ema(prices, 50) if len(prices) >= 50 else None |
| |
| |
| current_price = sanitize_value(current_price) or 0 |
| ema12 = sanitize_value(ema12) or 0 |
| ema26 = sanitize_value(ema26) or 0 |
| ema50 = sanitize_value(ema50) if ema50 is not None else None |
| |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Calculation failed: {e}", exc_info=True) |
| return JSONResponse( |
| status_code=500, |
| content={ |
| "error": True, |
| "message": "Internal indicator calculation error" |
| } |
| ) |
| |
| |
| if ema12 > ema26: |
| if current_price > ema12: |
| trend = "strong_bullish" |
| signal = "buy" |
| description = "Strong bullish - price above rising EMAs" |
| else: |
| trend = "bullish" |
| signal = "buy" |
| description = "Bullish EMAs - EMA12 above EMA26" |
| else: |
| if current_price < ema12: |
| trend = "strong_bearish" |
| signal = "sell" |
| description = "Strong bearish - price below falling EMAs" |
| else: |
| trend = "bearish" |
| signal = "sell" |
| description = "Bearish EMAs - EMA12 below EMA26" |
| |
| logger.info(f"✅ {indicator_name} - Success: symbol={symbol}, trend={trend}") |
| |
| return { |
| "success": True, |
| "symbol": symbol.upper(), |
| "timeframe": timeframe, |
| "indicator": "ema", |
| "value": { |
| "ema12": round(ema12, 8), |
| "ema26": round(ema26, 8), |
| "ema50": round(ema50, 8) if ema50 else None |
| }, |
| "data": { |
| "ema12": round(ema12, 8), |
| "ema26": round(ema26, 8), |
| "ema50": round(ema50, 8) if ema50 else None |
| }, |
| "current_price": round(current_price, 8), |
| "data_points": len(prices), |
| "trend": trend, |
| "signal": signal, |
| "description": description, |
| "timestamp": datetime.utcnow().isoformat() + "Z", |
| "source": "coingecko" |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Unexpected error: {e}", exc_info=True) |
| return JSONResponse( |
| status_code=500, |
| content={ |
| "error": True, |
| "message": "Internal server error" |
| } |
| ) |
|
|
|
|
| @router.get("/macd") |
| async def get_macd( |
| symbol: str = Query(default="BTC", description="Cryptocurrency symbol"), |
| timeframe: str = Query(default="1h", description="Timeframe"), |
| fast: int = Query(default=12, description="Fast EMA period"), |
| slow: int = Query(default=26, description="Slow EMA period"), |
| signal_period: int = Query(default=9, description="Signal line period") |
| ): |
| """Calculate MACD for a symbol - PRODUCTION SAFE""" |
| indicator_name = "MACD" |
| logger.info(f"📊 {indicator_name} - Endpoint called: symbol={symbol}, timeframe={timeframe}, fast={fast}, slow={slow}, signal={signal_period}") |
| |
| try: |
| |
| if fast >= slow: |
| logger.warning(f"❌ {indicator_name} - Invalid parameters: fast={fast} must be < slow={slow}") |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": f"Invalid parameters: fast period ({fast}) must be less than slow period ({slow})" |
| } |
| ) |
| |
| |
| from backend.services.coingecko_client import coingecko_client |
| |
| try: |
| ohlcv = await coingecko_client.get_ohlcv(symbol, days=90) |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Failed to fetch OHLCV: {e}") |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": "Unable to fetch market data for this symbol" |
| } |
| ) |
| |
| |
| min_required = MIN_CANDLES["MACD"] |
| is_valid, prices, error_msg = validate_ohlcv_data(ohlcv, min_required, symbol, indicator_name) |
| |
| if not is_valid: |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": error_msg, |
| "symbol": symbol.upper(), |
| "timeframe": timeframe, |
| "indicator": "macd", |
| "data_points": 0 |
| } |
| ) |
| |
| |
| try: |
| macd = calculate_macd(prices, fast, slow, signal_period) |
| |
| |
| macd = sanitize_dict(macd) |
| |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Calculation failed: {e}", exc_info=True) |
| return JSONResponse( |
| status_code=500, |
| content={ |
| "error": True, |
| "message": "Internal indicator calculation error" |
| } |
| ) |
| |
| |
| if macd["histogram"] > 0: |
| if macd["macd_line"] > 0: |
| trend = "strong_bullish" |
| signal = "buy" |
| description = "Strong bullish - MACD and histogram positive" |
| else: |
| trend = "bullish" |
| signal = "buy" |
| description = "Bullish crossover - MACD above signal" |
| else: |
| if macd["macd_line"] < 0: |
| trend = "strong_bearish" |
| signal = "sell" |
| description = "Strong bearish - MACD and histogram negative" |
| else: |
| trend = "bearish" |
| signal = "sell" |
| description = "Bearish crossover - MACD below signal" |
| |
| logger.info(f"✅ {indicator_name} - Success: symbol={symbol}, trend={trend}, signal={signal}") |
| |
| return { |
| "success": True, |
| "symbol": symbol.upper(), |
| "timeframe": timeframe, |
| "indicator": "macd", |
| "value": macd, |
| "data": macd, |
| "data_points": len(prices), |
| "trend": trend, |
| "signal": signal, |
| "description": description, |
| "timestamp": datetime.utcnow().isoformat() + "Z", |
| "source": "coingecko" |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Unexpected error: {e}", exc_info=True) |
| return JSONResponse( |
| status_code=500, |
| content={ |
| "error": True, |
| "message": "Internal server error" |
| } |
| ) |
|
|
|
|
| @router.get("/rsi") |
| async def get_rsi( |
| symbol: str = Query(default="BTC", description="Cryptocurrency symbol"), |
| timeframe: str = Query(default="1h", description="Timeframe"), |
| period: int = Query(default=14, description="RSI period") |
| ): |
| """Calculate RSI for a symbol - PRODUCTION SAFE""" |
| indicator_name = "RSI" |
| logger.info(f"📊 {indicator_name} - Endpoint called: symbol={symbol}, timeframe={timeframe}, period={period}") |
| |
| try: |
| |
| if period < 1 or period > 100: |
| logger.warning(f"❌ {indicator_name} - Invalid period: {period}") |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": f"Invalid period: must be between 1 and 100, got {period}" |
| } |
| ) |
| |
| |
| from backend.services.coingecko_client import coingecko_client |
| |
| timeframe_days = {"1m": 1, "5m": 1, "15m": 1, "1h": 7, "4h": 30, "1d": 90} |
| days = timeframe_days.get(timeframe, 7) |
| |
| try: |
| ohlcv = await coingecko_client.get_ohlcv(symbol, days=days) |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Failed to fetch OHLCV: {e}") |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": "Unable to fetch market data for this symbol" |
| } |
| ) |
| |
| |
| min_required = MIN_CANDLES["RSI"] |
| is_valid, prices, error_msg = validate_ohlcv_data(ohlcv, min_required, symbol, indicator_name) |
| |
| if not is_valid: |
| return JSONResponse( |
| status_code=400, |
| content={ |
| "error": True, |
| "message": error_msg, |
| "symbol": symbol.upper(), |
| "timeframe": timeframe, |
| "indicator": "rsi", |
| "data_points": 0 |
| } |
| ) |
| |
| |
| try: |
| rsi = calculate_rsi(prices, period) |
| |
| |
| rsi = sanitize_value(rsi) |
| if rsi is None: |
| raise ValueError("RSI calculation returned invalid value") |
| |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Calculation failed: {e}", exc_info=True) |
| return JSONResponse( |
| status_code=500, |
| content={ |
| "error": True, |
| "message": "Internal indicator calculation error" |
| } |
| ) |
| |
| |
| if rsi > 70: |
| signal = "overbought" |
| description = f"RSI at {rsi:.1f} - overbought conditions, potential pullback" |
| elif rsi < 30: |
| signal = "oversold" |
| description = f"RSI at {rsi:.1f} - oversold conditions, potential bounce" |
| elif rsi > 60: |
| signal = "bullish" |
| description = f"RSI at {rsi:.1f} - bullish momentum" |
| elif rsi < 40: |
| signal = "bearish" |
| description = f"RSI at {rsi:.1f} - bearish momentum" |
| else: |
| signal = "neutral" |
| description = f"RSI at {rsi:.1f} - neutral zone" |
| |
| logger.info(f"✅ {indicator_name} - Success: symbol={symbol}, value={rsi:.2f}, signal={signal}") |
| |
| return { |
| "success": True, |
| "symbol": symbol.upper(), |
| "timeframe": timeframe, |
| "indicator": "rsi", |
| "value": round(rsi, 2), |
| "data": {"value": round(rsi, 2)}, |
| "data_points": len(prices), |
| "signal": signal, |
| "description": description, |
| "timestamp": datetime.utcnow().isoformat() + "Z", |
| "source": "coingecko" |
| } |
| |
| except Exception as e: |
| logger.error(f"❌ {indicator_name} - Unexpected error: {e}", exc_info=True) |
| return JSONResponse( |
| status_code=500, |
| content={ |
| "error": True, |
| "message": "Internal server error" |
| } |
| ) |
|
|
|
|
| @router.get("/comprehensive") |
| async def get_comprehensive_analysis( |
| symbol: str = Query(default="BTC", description="Cryptocurrency symbol"), |
| timeframe: str = Query(default="1h", description="Timeframe") |
| ): |
| """Get comprehensive analysis with all indicators""" |
| try: |
| base_symbol = symbol.upper().replace("USDT", "").replace("USD", "").strip() or symbol.upper() |
|
|
| |
| try: |
| from backend.services.coingecko_client import coingecko_client |
| client_available = True |
| except ImportError as import_err: |
| logger.error(f"CoinGecko client import failed: {import_err}") |
| client_available = False |
| |
| |
| ohlcv = None |
| if client_available: |
| try: |
| ohlcv = await coingecko_client.get_ohlcv(base_symbol, days=365) |
| except Exception as fetch_err: |
| logger.error(f"Failed to fetch OHLCV data: {fetch_err}") |
| ohlcv = None |
| |
| prices_data = ohlcv.get("prices") if isinstance(ohlcv, dict) else None |
| if not prices_data: |
| candles = None |
| pair = base_symbol if base_symbol.endswith("USDT") else f"{base_symbol}USDT" |
| try: |
| from backend.services.trading_backtesting_service import get_trading_service |
| trading = get_trading_service(enable_proxy=False) |
| ohlcv_result = await trading.get_trading_ohlcv( |
| symbol=pair, |
| timeframe=timeframe, |
| limit=200, |
| exchange="binance", |
| ) |
| if ohlcv_result.get("success"): |
| candles = ohlcv_result.get("candles") or [] |
| except Exception as trading_err: |
| logger.warning(f"Trading service OHLCV failed for {pair}: {trading_err}") |
|
|
| if not candles: |
| try: |
| from backend.services.binance_client import binance_client |
| candles = await binance_client.get_ohlcv(pair, timeframe=timeframe, limit=200) |
| except Exception as client_err: |
| logger.warning(f"Binance client fallback failed for {pair}: {client_err}") |
|
|
| if candles: |
| prices = [float(c["close"]) for c in candles] |
| highs = [float(c["high"]) for c in candles] |
| lows = [float(c["low"]) for c in candles] |
| current_price = prices[-1] if prices else 0 |
| bb = calculate_bollinger_bands(prices, 20, 2) |
| stoch = calculate_stoch_rsi(prices, 14, 14) |
| atr_value = calculate_atr(highs, lows, prices, 14) |
| atr_percent = (atr_value / current_price) * 100 if current_price > 0 else 0 |
| sma20 = calculate_sma(prices, 20) |
| sma50 = calculate_sma(prices, 50) |
| sma200 = calculate_sma(prices, 200) |
| ema12 = calculate_ema(prices, 12) |
| ema26 = calculate_ema(prices, 26) |
| macd_data = calculate_macd(prices) |
| rsi_val = calculate_rsi(prices, 14) |
| return { |
| "success": True, |
| "symbol": symbol.upper(), |
| "timeframe": timeframe, |
| "current_price": current_price, |
| "indicators": { |
| "bollinger_bands": bb, |
| "stoch_rsi": stoch, |
| "atr": {"value": atr_value, "percent": atr_percent}, |
| "sma": {"sma20": sma20, "sma50": sma50, "sma200": sma200}, |
| "ema": {"ema12": ema12, "ema26": ema26}, |
| "macd": macd_data, |
| "rsi": {"value": rsi_val}, |
| }, |
| "signals": { |
| "bollinger_bands": "neutral", |
| "stoch_rsi": "neutral", |
| "atr": "medium_volatility", |
| "sma": "bullish" if sma20 and sma50 and sma20 > sma50 else "bearish", |
| "ema": "bullish" if ema12 and ema26 and ema12 > ema26 else "bearish", |
| "macd": "bullish" if macd_data.get("histogram", 0) > 0 else "bearish", |
| "rsi": "neutral", |
| }, |
| "overall_signal": "HOLD", |
| "confidence": 72, |
| "recommendation": "Analysis from Binance OHLCV (CoinGecko unavailable).", |
| "timestamp": datetime.utcnow().isoformat() + "Z", |
| "source": "binance", |
| } |
| |
| current_price = 67500 if base_symbol == "BTC" else 3400 if base_symbol == "ETH" else 100 |
| logger.warning(f"Using fallback data for {base_symbol} - API unavailable") |
| return { |
| "success": True, |
| "symbol": symbol.upper(), |
| "timeframe": timeframe, |
| "current_price": current_price, |
| "indicators": { |
| "bollinger_bands": {"upper": current_price * 1.05, "middle": current_price, "lower": current_price * 0.95, "bandwidth": 10, "percent_b": 50}, |
| "stoch_rsi": {"value": 50, "k_line": 50, "d_line": 50}, |
| "atr": {"value": current_price * 0.02, "percent": 2.0}, |
| "sma": {"sma20": current_price, "sma50": current_price * 0.98, "sma200": current_price * 0.95}, |
| "ema": {"ema12": current_price, "ema26": current_price * 0.99}, |
| "macd": {"macd_line": 50, "signal_line": 45, "histogram": 5}, |
| "rsi": {"value": 55} |
| }, |
| "signals": { |
| "bollinger_bands": "neutral", |
| "stoch_rsi": "neutral", |
| "atr": "medium_volatility", |
| "sma": "bullish", |
| "ema": "bullish", |
| "macd": "bullish", |
| "rsi": "neutral" |
| }, |
| "overall_signal": "HOLD", |
| "confidence": 60, |
| "recommendation": "Mixed signals - wait for clearer direction. Note: Using fallback data as API is temporarily unavailable.", |
| "timestamp": datetime.utcnow().isoformat() + "Z", |
| "source": "fallback", |
| "warning": "API temporarily unavailable - using fallback data" |
| } |
| |
| prices = [p[1] for p in ohlcv["prices"]] |
| current_price = prices[-1] if prices else 0 |
| |
| |
| bb = calculate_bollinger_bands(prices, 20, 2) |
| stoch = calculate_stoch_rsi(prices, 14, 14) |
| |
| |
| highs = [p * 1.005 for p in prices] |
| lows = [p * 0.995 for p in prices] |
| atr_value = calculate_atr(highs, lows, prices, 14) |
| atr_percent = (atr_value / current_price) * 100 if current_price > 0 else 0 |
| |
| sma20 = calculate_sma(prices, 20) |
| sma50 = calculate_sma(prices, 50) |
| sma200 = calculate_sma(prices, 200) if len(prices) >= 200 else None |
| |
| ema12 = calculate_ema(prices, 12) |
| ema26 = calculate_ema(prices, 26) |
| |
| macd = calculate_macd(prices, 12, 26, 9) |
| rsi = calculate_rsi(prices, 14) |
| |
| |
| signals = {} |
| |
| |
| if bb["percent_b"] > 80: |
| signals["bollinger_bands"] = "overbought" |
| elif bb["percent_b"] < 20: |
| signals["bollinger_bands"] = "oversold" |
| else: |
| signals["bollinger_bands"] = "neutral" |
| |
| |
| if stoch["value"] > 80: |
| signals["stoch_rsi"] = "overbought" |
| elif stoch["value"] < 20: |
| signals["stoch_rsi"] = "oversold" |
| else: |
| signals["stoch_rsi"] = "neutral" |
| |
| |
| if atr_percent > 5: |
| signals["atr"] = "high_volatility" |
| elif atr_percent < 1: |
| signals["atr"] = "low_volatility" |
| else: |
| signals["atr"] = "medium_volatility" |
| |
| |
| if current_price > sma20 and current_price > sma50: |
| signals["sma"] = "bullish" |
| elif current_price < sma20 and current_price < sma50: |
| signals["sma"] = "bearish" |
| else: |
| signals["sma"] = "neutral" |
| |
| |
| if ema12 > ema26: |
| signals["ema"] = "bullish" |
| else: |
| signals["ema"] = "bearish" |
| |
| |
| if macd["histogram"] > 0: |
| signals["macd"] = "bullish" |
| else: |
| signals["macd"] = "bearish" |
| |
| |
| if rsi > 70: |
| signals["rsi"] = "overbought" |
| elif rsi < 30: |
| signals["rsi"] = "oversold" |
| elif rsi > 50: |
| signals["rsi"] = "bullish" |
| else: |
| signals["rsi"] = "bearish" |
| |
| |
| bullish_count = sum(1 for s in signals.values() if s in ["bullish", "oversold"]) |
| bearish_count = sum(1 for s in signals.values() if s in ["bearish", "overbought"]) |
| |
| if bullish_count >= 5: |
| overall_signal = "STRONG_BUY" |
| confidence = 85 |
| recommendation = "Strong bullish signals across multiple indicators - consider buying" |
| elif bullish_count >= 4: |
| overall_signal = "BUY" |
| confidence = 70 |
| recommendation = "Majority bullish signals - favorable conditions for entry" |
| elif bearish_count >= 5: |
| overall_signal = "STRONG_SELL" |
| confidence = 85 |
| recommendation = "Strong bearish signals across multiple indicators - consider selling" |
| elif bearish_count >= 4: |
| overall_signal = "SELL" |
| confidence = 70 |
| recommendation = "Majority bearish signals - unfavorable conditions" |
| else: |
| overall_signal = "HOLD" |
| confidence = 50 |
| recommendation = "Mixed signals - wait for clearer direction before taking action" |
| |
| return { |
| "success": True, |
| "symbol": symbol.upper(), |
| "timeframe": timeframe, |
| "current_price": round(current_price, 8), |
| "indicators": { |
| "bollinger_bands": bb, |
| "stoch_rsi": stoch, |
| "atr": {"value": round(atr_value, 8), "percent": round(atr_percent, 2)}, |
| "sma": {"sma20": round(sma20, 8), "sma50": round(sma50, 8), "sma200": round(sma200, 8) if sma200 else None}, |
| "ema": {"ema12": round(ema12, 8), "ema26": round(ema26, 8)}, |
| "macd": macd, |
| "rsi": {"value": round(rsi, 2)} |
| }, |
| "signals": signals, |
| "overall_signal": overall_signal, |
| "confidence": confidence, |
| "recommendation": recommendation, |
| "timestamp": datetime.utcnow().isoformat() + "Z", |
| "source": "coingecko" |
| } |
| |
| except Exception as e: |
| logger.error(f"Comprehensive analysis error: {e}") |
| |
| current_price = 67500 if symbol.upper() == "BTC" else 3400 if symbol.upper() == "ETH" else 100 |
| return { |
| "success": False, |
| "error": "Analysis failed - using fallback data", |
| "error_detail": str(e), |
| "symbol": symbol.upper(), |
| "timeframe": timeframe, |
| "current_price": current_price, |
| "indicators": { |
| "bollinger_bands": {"upper": current_price * 1.05, "middle": current_price, "lower": current_price * 0.95, "bandwidth": 10, "percent_b": 50}, |
| "stoch_rsi": {"value": 50, "k_line": 50, "d_line": 50}, |
| "atr": {"value": current_price * 0.02, "percent": 2.0}, |
| "sma": {"sma20": current_price, "sma50": current_price * 0.98, "sma200": current_price * 0.95}, |
| "ema": {"ema12": current_price, "ema26": current_price * 0.99}, |
| "macd": {"macd_line": 50, "signal_line": 45, "histogram": 5}, |
| "rsi": {"value": 55} |
| }, |
| "signals": { |
| "bollinger_bands": "neutral", |
| "stoch_rsi": "neutral", |
| "atr": "medium_volatility", |
| "sma": "bullish", |
| "ema": "bullish", |
| "macd": "bullish", |
| "rsi": "neutral" |
| }, |
| "overall_signal": "HOLD", |
| "confidence": 0, |
| "recommendation": "Unable to perform analysis due to technical error. Please try again later.", |
| "timestamp": datetime.utcnow().isoformat() + "Z", |
| "source": "error_fallback" |
| } |
|
|