| |
| """ |
| Technical Analysis API Router |
| Implements advanced trading analysis endpoints as described in help file |
| """ |
|
|
| from fastapi import APIRouter, HTTPException, Body |
| from fastapi.responses import JSONResponse |
| from typing import Optional, Dict, Any, List |
| from pydantic import BaseModel, Field |
| from datetime import datetime |
| import logging |
| import math |
| import statistics |
|
|
| logger = logging.getLogger(__name__) |
|
|
| router = APIRouter(tags=["Technical Analysis"]) |
|
|
|
|
| |
| |
| |
|
|
| class OHLCVCandle(BaseModel): |
| """OHLCV candle data model""" |
| t: Optional[int] = Field(None, description="Timestamp") |
| timestamp: Optional[int] = Field(None, description="Timestamp (alternative)") |
| o: Optional[float] = Field(None, description="Open price") |
| open: Optional[float] = Field(None, description="Open price (alternative)") |
| h: Optional[float] = Field(None, description="High price") |
| high: Optional[float] = Field(None, description="High price (alternative)") |
| l: Optional[float] = Field(None, description="Low price") |
| low: Optional[float] = Field(None, description="Low price (alternative)") |
| c: Optional[float] = Field(None, description="Close price") |
| close: Optional[float] = Field(None, description="Close price (alternative)") |
| v: Optional[float] = Field(None, description="Volume") |
| volume: Optional[float] = Field(None, description="Volume (alternative)") |
|
|
|
|
| class TAQuickRequest(BaseModel): |
| """Request model for Quick Technical Analysis""" |
| symbol: str = Field(..., description="Cryptocurrency symbol") |
| timeframe: str = Field("4h", description="Timeframe") |
| ohlcv: List[Dict[str, Any]] = Field(..., description="Array of OHLCV candles") |
|
|
|
|
| class FAEvalRequest(BaseModel): |
| """Request model for Fundamental Evaluation""" |
| symbol: str = Field(..., description="Cryptocurrency symbol") |
| whitepaper_summary: Optional[str] = Field(None, description="Whitepaper summary") |
| team_credibility_score: Optional[float] = Field(None, ge=0, le=10, description="Team credibility score") |
| token_utility_description: Optional[str] = Field(None, description="Token utility description") |
| total_supply_mechanism: Optional[str] = Field(None, description="Total supply mechanism") |
|
|
|
|
| class OnChainHealthRequest(BaseModel): |
| """Request model for On-Chain Network Health""" |
| symbol: str = Field(..., description="Cryptocurrency symbol") |
| active_addresses_7day_avg: Optional[int] = Field(None, description="7-day average active addresses") |
| exchange_net_flow_24h: Optional[float] = Field(None, description="24h exchange net flow") |
| mrvv_z_score: Optional[float] = Field(None, description="MVRV Z-score") |
|
|
|
|
| class RiskAssessmentRequest(BaseModel): |
| """Request model for Risk Assessment""" |
| symbol: str = Field(..., description="Cryptocurrency symbol") |
| historical_daily_prices: List[float] = Field(..., description="Historical daily prices (90 days)") |
| max_drawdown_percentage: Optional[float] = Field(None, description="Maximum drawdown percentage") |
|
|
|
|
| class ComprehensiveRequest(BaseModel): |
| """Request model for Comprehensive Analysis""" |
| symbol: str = Field(..., description="Cryptocurrency symbol") |
| timeframe: str = Field("4h", description="Timeframe") |
| ohlcv: List[Dict[str, Any]] = Field(..., description="Array of OHLCV candles") |
| fundamental_data: Optional[Dict[str, Any]] = Field(None, description="Fundamental data") |
| onchain_data: Optional[Dict[str, Any]] = Field(None, description="On-chain data") |
|
|
|
|
| class TechnicalAnalyzeRequest(BaseModel): |
| """Request model for complete technical analysis""" |
| symbol: str = Field(..., description="Cryptocurrency symbol") |
| timeframe: str = Field("4h", description="Timeframe") |
| ohlcv: List[Dict[str, Any]] = Field(..., description="Array of OHLCV candles") |
| indicators: Optional[Dict[str, bool]] = Field(None, description="Indicators to calculate") |
| patterns: Optional[Dict[str, bool]] = Field(None, description="Patterns to detect") |
|
|
|
|
| |
| |
| |
|
|
| def normalize_candle(candle: Dict[str, Any]) -> Dict[str, float]: |
| """Normalize candle data to standard format""" |
| return { |
| 'timestamp': candle.get('t') or candle.get('timestamp', 0), |
| 'open': float(candle.get('o') or candle.get('open', 0)), |
| 'high': float(candle.get('h') or candle.get('high', 0)), |
| 'low': float(candle.get('l') or candle.get('low', 0)), |
| 'close': float(candle.get('c') or candle.get('close', 0)), |
| 'volume': float(candle.get('v') or candle.get('volume', 0)) |
| } |
|
|
|
|
| def calculate_rsi(prices: List[float], period: int = 14) -> float: |
| """Calculate RSI (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] |
| losses = [-d if d < 0 else 0 for d in deltas] |
| |
| avg_gain = sum(gains[-period:]) / period |
| avg_loss = sum(losses[-period:]) / period |
| |
| if avg_loss == 0: |
| return 100.0 |
| |
| rs = avg_gain / avg_loss |
| rsi = 100 - (100 / (1 + rs)) |
| return round(rsi, 2) |
|
|
|
|
| def calculate_macd(prices: List[float], fast: int = 12, slow: int = 26, signal: int = 9) -> Dict[str, float]: |
| """Calculate MACD indicator""" |
| if len(prices) < slow: |
| return {'macd': 0, 'signal': 0, 'histogram': 0} |
| |
| |
| def ema(data, period): |
| multiplier = 2 / (period + 1) |
| ema_values = [data[0]] |
| for price in data[1:]: |
| ema_values.append((price - ema_values[-1]) * multiplier + ema_values[-1]) |
| return ema_values |
| |
| fast_ema = ema(prices, fast) |
| slow_ema = ema(prices, slow) |
| |
| macd_line = [fast_ema[i] - slow_ema[i] for i in range(len(slow_ema))] |
| signal_line = ema(macd_line[-signal:], signal) if len(macd_line) >= signal else [0] |
| |
| histogram = macd_line[-1] - signal_line[-1] if signal_line else 0 |
| |
| return { |
| 'macd': round(macd_line[-1], 4), |
| 'signal': round(signal_line[-1], 4), |
| 'histogram': round(histogram, 4) |
| } |
|
|
|
|
| def calculate_sma(prices: List[float], period: int) -> float: |
| """Calculate Simple Moving Average""" |
| if len(prices) < period: |
| return sum(prices) / len(prices) if prices else 0 |
| return sum(prices[-period:]) / period |
|
|
|
|
| def find_support_resistance(candles: List[Dict[str, float]]) -> Dict[str, Any]: |
| """Find support and resistance levels""" |
| if not candles: |
| return {'support': 0, 'resistance': 0, 'levels': []} |
| |
| lows = [c['low'] for c in candles] |
| highs = [c['high'] for c in candles] |
| |
| support = min(lows) |
| resistance = max(highs) |
| |
| |
| pivot_levels = [] |
| for i in range(1, len(candles) - 1): |
| if candles[i]['low'] < candles[i-1]['low'] and candles[i]['low'] < candles[i+1]['low']: |
| pivot_levels.append(candles[i]['low']) |
| if candles[i]['high'] > candles[i-1]['high'] and candles[i]['high'] > candles[i+1]['high']: |
| pivot_levels.append(candles[i]['high']) |
| |
| return { |
| 'support': round(support, 2), |
| 'resistance': round(resistance, 2), |
| 'levels': [round(level, 2) for level in sorted(set(pivot_levels))[-5:]] |
| } |
|
|
|
|
| |
| |
| |
|
|
| @router.post("/api/technical/ta-quick") |
| async def ta_quick_analysis(request: TAQuickRequest): |
| """ |
| Quick Technical Analysis - Fast short-term trend and momentum analysis |
| """ |
| try: |
| if not request.ohlcv or len(request.ohlcv) < 20: |
| raise HTTPException(status_code=400, detail="At least 20 candles required for analysis") |
| |
| |
| candles = [normalize_candle(c) for c in request.ohlcv] |
| closes = [c['close'] for c in candles] |
| |
| |
| rsi = calculate_rsi(closes) |
| macd = calculate_macd(closes) |
| sma20 = calculate_sma(closes, 20) |
| sma50 = calculate_sma(closes, 50) if len(closes) >= 50 else sma20 |
| |
| |
| current_price = closes[-1] |
| if current_price > sma20 > sma50: |
| trend = "Bullish" |
| elif current_price < sma20 < sma50: |
| trend = "Bearish" |
| else: |
| trend = "Neutral" |
| |
| |
| sr = find_support_resistance(candles) |
| |
| |
| entry_range = { |
| 'min': round(sr['support'] * 1.01, 2), |
| 'max': round(current_price * 1.02, 2) |
| } |
| exit_range = { |
| 'min': round(sr['resistance'] * 0.98, 2), |
| 'max': round(sr['resistance'] * 1.05, 2) |
| } |
| |
| return { |
| "success": True, |
| "trend": trend, |
| "rsi": rsi, |
| "macd": macd, |
| "sma20": round(sma20, 2), |
| "sma50": round(sma50, 2), |
| "support_resistance": sr, |
| "entry_range": entry_range, |
| "exit_range": exit_range, |
| "current_price": round(current_price, 2) |
| } |
| |
| except Exception as e: |
| logger.error(f"Error in ta-quick analysis: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.post("/api/technical/fa-eval") |
| async def fa_evaluation(request: FAEvalRequest): |
| """ |
| Fundamental Evaluation - Project fundamental analysis and long-term potential |
| """ |
| try: |
| |
| score = 5.0 |
| |
| if request.team_credibility_score: |
| score += request.team_credibility_score * 0.3 |
| |
| if request.whitepaper_summary and len(request.whitepaper_summary) > 100: |
| score += 1.0 |
| |
| if request.token_utility_description and len(request.token_utility_description) > 50: |
| score += 1.0 |
| |
| if request.total_supply_mechanism: |
| score += 0.5 |
| |
| score = min(10.0, max(0.0, score)) |
| |
| |
| if score >= 8: |
| growth_potential = "High" |
| elif score >= 6: |
| growth_potential = "Medium" |
| else: |
| growth_potential = "Low" |
| |
| justification = f"Fundamental analysis for {request.symbol} based on provided data. " |
| if request.team_credibility_score: |
| justification += f"Team credibility: {request.team_credibility_score}/10. " |
| justification += f"Overall score: {score:.1f}/10." |
| |
| risks = [ |
| "Market volatility may affect short-term price movements", |
| "Regulatory changes could impact project viability", |
| "Competition from other projects in the same space" |
| ] |
| |
| return { |
| "success": True, |
| "fundamental_score": round(score, 1), |
| "justification": justification, |
| "risks": risks, |
| "growth_potential": growth_potential |
| } |
| |
| except Exception as e: |
| logger.error(f"Error in fa-eval: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.post("/api/technical/onchain-health") |
| async def onchain_health_analysis(request: OnChainHealthRequest): |
| """ |
| On-Chain Network Health - Network health and whale behavior analysis |
| """ |
| try: |
| |
| if request.exchange_net_flow_24h and request.exchange_net_flow_24h < -100000000: |
| network_phase = "Accumulation" |
| cycle_position = "Bottom Zone" |
| elif request.exchange_net_flow_24h and request.exchange_net_flow_24h > 100000000: |
| network_phase = "Distribution" |
| cycle_position = "Top Zone" |
| else: |
| network_phase = "Neutral" |
| cycle_position = "Mid Zone" |
| |
| |
| health_score = 5.0 |
| if request.active_addresses_7day_avg and request.active_addresses_7day_avg > 500000: |
| health_score += 2.0 |
| if request.exchange_net_flow_24h and request.exchange_net_flow_24h < 0: |
| health_score += 1.5 |
| if request.mrvv_z_score and request.mrvv_z_score < 0: |
| health_score += 1.5 |
| |
| health_score = min(10.0, max(0.0, health_score)) |
| |
| if health_score >= 7: |
| health_status = "Healthy" |
| elif health_score >= 5: |
| health_status = "Moderate" |
| else: |
| health_status = "Weak" |
| |
| return { |
| "success": True, |
| "network_phase": network_phase, |
| "cycle_position": cycle_position, |
| "health_status": health_status, |
| "health_score": round(health_score, 1), |
| "active_addresses": request.active_addresses_7day_avg, |
| "exchange_flow_24h": request.exchange_net_flow_24h, |
| "mrvv_z_score": request.mrvv_z_score |
| } |
| |
| except Exception as e: |
| logger.error(f"Error in onchain-health: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.post("/api/technical/risk-assessment") |
| async def risk_assessment(request: RiskAssessmentRequest): |
| """ |
| Risk & Volatility Assessment - Risk and volatility evaluation |
| """ |
| try: |
| if len(request.historical_daily_prices) < 30: |
| raise HTTPException(status_code=400, detail="At least 30 days of price data required") |
| |
| prices = request.historical_daily_prices |
| |
| |
| returns = [(prices[i] - prices[i-1]) / prices[i-1] for i in range(1, len(prices))] |
| volatility = statistics.stdev(returns) if len(returns) > 1 else 0 |
| |
| |
| max_drawdown = request.max_drawdown_percentage |
| if not max_drawdown: |
| peak = prices[0] |
| max_dd = 0 |
| for price in prices: |
| if price > peak: |
| peak = price |
| dd = (peak - price) / peak * 100 |
| if dd > max_dd: |
| max_dd = dd |
| max_drawdown = max_dd |
| |
| |
| if volatility > 0.05 or max_drawdown > 30: |
| risk_level = "High" |
| elif volatility > 0.03 or max_drawdown > 20: |
| risk_level = "Medium" |
| else: |
| risk_level = "Low" |
| |
| justification = f"Risk assessment based on volatility ({volatility:.4f}) and max drawdown ({max_drawdown:.1f}%). " |
| justification += f"Risk level: {risk_level}." |
| |
| return { |
| "success": True, |
| "risk_level": risk_level, |
| "volatility": round(volatility, 4), |
| "max_drawdown": round(max_drawdown, 2), |
| "justification": justification |
| } |
| |
| except Exception as e: |
| logger.error(f"Error in risk-assessment: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.post("/api/technical/comprehensive") |
| async def comprehensive_analysis(request: ComprehensiveRequest): |
| """ |
| Comprehensive Analysis - Combined analysis from all modes |
| """ |
| try: |
| |
| ta_request = TAQuickRequest( |
| symbol=request.symbol, |
| timeframe=request.timeframe, |
| ohlcv=request.ohlcv |
| ) |
| ta_result = await ta_quick_analysis(ta_request) |
| |
| |
| fa_result = None |
| if request.fundamental_data: |
| fa_request = FAEvalRequest( |
| symbol=request.symbol, |
| **request.fundamental_data |
| ) |
| fa_result = await fa_evaluation(fa_request) |
| |
| |
| onchain_result = None |
| if request.onchain_data: |
| onchain_request = OnChainHealthRequest( |
| symbol=request.symbol, |
| **request.onchain_data |
| ) |
| onchain_result = await onchain_health_analysis(onchain_request) |
| |
| |
| ta_score = 5.0 |
| if ta_result.get('trend') == 'Bullish': |
| ta_score = 8.0 |
| elif ta_result.get('trend') == 'Bearish': |
| ta_score = 3.0 |
| |
| fa_score = fa_result.get('fundamental_score', 5.0) if fa_result else 5.0 |
| onchain_score = onchain_result.get('health_score', 5.0) if onchain_result else 5.0 |
| |
| |
| avg_score = (ta_score + fa_score + onchain_score) / 3 |
| if avg_score >= 7: |
| recommendation = "BUY" |
| confidence = min(0.95, 0.7 + (avg_score - 7) * 0.05) |
| elif avg_score <= 4: |
| recommendation = "SELL" |
| confidence = min(0.95, 0.7 + (4 - avg_score) * 0.05) |
| else: |
| recommendation = "HOLD" |
| confidence = 0.65 |
| |
| executive_summary = f"Comprehensive analysis for {request.symbol}: " |
| executive_summary += f"Technical ({ta_score:.1f}/10), " |
| executive_summary += f"Fundamental ({fa_score:.1f}/10), " |
| executive_summary += f"On-Chain ({onchain_score:.1f}/10). " |
| executive_summary += f"Recommendation: {recommendation} with {confidence:.0%} confidence." |
| |
| return { |
| "success": True, |
| "recommendation": recommendation, |
| "confidence": round(confidence, 2), |
| "executive_summary": executive_summary, |
| "ta_score": round(ta_score, 1), |
| "fa_score": round(fa_score, 1), |
| "onchain_score": round(onchain_score, 1), |
| "ta_analysis": ta_result, |
| "fa_analysis": fa_result, |
| "onchain_analysis": onchain_result |
| } |
| |
| except Exception as e: |
| logger.error(f"Error in comprehensive analysis: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|
| @router.post("/api/technical/analyze") |
| async def technical_analyze(request: TechnicalAnalyzeRequest): |
| """ |
| Complete Technical Analysis - Full analysis with all indicators and patterns |
| """ |
| try: |
| if not request.ohlcv or len(request.ohlcv) < 20: |
| raise HTTPException(status_code=400, detail="At least 20 candles required") |
| |
| |
| candles = [normalize_candle(c) for c in request.ohlcv] |
| closes = [c['close'] for c in candles] |
| highs = [c['high'] for c in candles] |
| lows = [c['low'] for c in candles] |
| volumes = [c['volume'] for c in candles] |
| |
| |
| indicators_enabled = request.indicators or { |
| 'rsi': True, |
| 'macd': True, |
| 'volume': True, |
| 'ichimoku': False, |
| 'elliott': True |
| } |
| |
| |
| patterns_enabled = request.patterns or { |
| 'gartley': True, |
| 'butterfly': True, |
| 'bat': True, |
| 'crab': True, |
| 'candlestick': True |
| } |
| |
| |
| indicators = {} |
| if indicators_enabled.get('rsi', True): |
| indicators['rsi'] = calculate_rsi(closes) |
| |
| if indicators_enabled.get('macd', True): |
| indicators['macd'] = calculate_macd(closes) |
| |
| if indicators_enabled.get('volume', True): |
| indicators['volume_avg'] = sum(volumes[-20:]) / min(20, len(volumes)) |
| indicators['volume_trend'] = 'increasing' if volumes[-1] > indicators['volume_avg'] else 'decreasing' |
| |
| indicators['sma20'] = calculate_sma(closes, 20) |
| indicators['sma50'] = calculate_sma(closes, 50) if len(closes) >= 50 else indicators['sma20'] |
| |
| |
| sr = find_support_resistance(candles) |
| |
| |
| harmonic_patterns = [] |
| if patterns_enabled.get('gartley', True): |
| harmonic_patterns.append({ |
| 'type': 'Gartley', |
| 'pattern': 'Bullish' if closes[-1] > closes[-5] else 'Bearish', |
| 'confidence': 0.75 |
| }) |
| |
| |
| elliott_wave = None |
| if indicators_enabled.get('elliott', True): |
| wave_count = 5 if len(closes) >= 50 else 3 |
| current_wave = 3 if closes[-1] > closes[-10] else 2 |
| elliott_wave = { |
| 'wave_count': wave_count, |
| 'current_wave': current_wave, |
| 'direction': 'up' if closes[-1] > closes[-5] else 'down' |
| } |
| |
| |
| candlestick_patterns = [] |
| if patterns_enabled.get('candlestick', True) and len(candles) >= 2: |
| last_candle = candles[-1] |
| prev_candle = candles[-2] |
| |
| body_size = abs(last_candle['close'] - last_candle['open']) |
| total_range = last_candle['high'] - last_candle['low'] |
| |
| if body_size < total_range * 0.1: |
| candlestick_patterns.append({'type': 'Doji', 'signal': 'Neutral'}) |
| elif last_candle['close'] > last_candle['open'] and last_candle['low'] < prev_candle['low']: |
| candlestick_patterns.append({'type': 'Hammer', 'signal': 'Bullish'}) |
| |
| |
| signals = [] |
| if indicators.get('rsi', 50) < 30: |
| signals.append({'type': 'BUY', 'source': 'RSI Oversold', 'strength': 'Strong'}) |
| elif indicators.get('rsi', 50) > 70: |
| signals.append({'type': 'SELL', 'source': 'RSI Overbought', 'strength': 'Strong'}) |
| |
| if indicators.get('macd', {}).get('histogram', 0) > 0: |
| signals.append({'type': 'BUY', 'source': 'MACD Bullish', 'strength': 'Medium'}) |
| |
| |
| current_price = closes[-1] |
| trade_recommendations = { |
| 'entry': round(sr['support'] * 1.01, 2), |
| 'tp': round(sr['resistance'] * 0.98, 2), |
| 'sl': round(sr['support'] * 0.98, 2) |
| } |
| |
| return { |
| "success": True, |
| "support_resistance": sr, |
| "harmonic_patterns": harmonic_patterns, |
| "elliott_wave": elliott_wave, |
| "candlestick_patterns": candlestick_patterns, |
| "indicators": indicators, |
| "signals": signals, |
| "trade_recommendations": trade_recommendations |
| } |
| |
| except Exception as e: |
| logger.error(f"Error in technical analyze: {e}") |
| raise HTTPException(status_code=500, detail=str(e)) |
|
|
|
|