|
|
|
|
|
"""
|
|
|
Technical Analysis API Router
|
|
|
Implements advanced trading analysis endpoints as described in help file
|
|
|
"""
|
|
|
|
|
|
from fastapi import APIRouter, HTTPException, Body, Query
|
|
|
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 calculate_ema(prices: List[float], period: int) -> float:
|
|
|
"""Calculate Exponential Moving Average"""
|
|
|
if len(prices) < period:
|
|
|
return sum(prices) / len(prices) if prices else 0
|
|
|
|
|
|
multiplier = 2 / (period + 1)
|
|
|
ema_value = sum(prices[:period]) / period
|
|
|
|
|
|
for price in prices[period:]:
|
|
|
ema_value = (price - ema_value) * multiplier + ema_value
|
|
|
|
|
|
return ema_value
|
|
|
|
|
|
|
|
|
def calculate_bollinger_bands(prices: List[float], period: int = 20, std_dev: float = 2.0) -> Dict[str, float]:
|
|
|
"""Calculate Bollinger Bands"""
|
|
|
if len(prices) < period:
|
|
|
sma = sum(prices) / len(prices) if prices else 0
|
|
|
return {'upper': sma, 'middle': sma, 'lower': sma}
|
|
|
|
|
|
sma = calculate_sma(prices, period)
|
|
|
recent_prices = prices[-period:]
|
|
|
|
|
|
|
|
|
variance = sum((p - sma) ** 2 for p in recent_prices) / period
|
|
|
std = math.sqrt(variance)
|
|
|
|
|
|
return {
|
|
|
'upper': round(sma + (std_dev * std), 2),
|
|
|
'middle': round(sma, 2),
|
|
|
'lower': round(sma - (std_dev * std), 2),
|
|
|
'width': round(std_dev * std * 2, 2)
|
|
|
}
|
|
|
|
|
|
|
|
|
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))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
async def _fetch_ohlcv_data(symbol: str, timeframe: str, limit: int = 200) -> List[Dict[str, Any]]:
|
|
|
"""Fetch OHLCV data from backend"""
|
|
|
try:
|
|
|
from backend.services.binance_client import BinanceClient
|
|
|
binance_client = BinanceClient()
|
|
|
symbol_upper = symbol.upper()
|
|
|
ohlcv_data = await binance_client.get_ohlcv(symbol_upper, timeframe, limit=limit)
|
|
|
return ohlcv_data or []
|
|
|
except Exception as e:
|
|
|
logger.error(f"Failed to fetch OHLCV for {symbol}: {e}")
|
|
|
|
|
|
try:
|
|
|
from backend.services.coingecko_client import coingecko_client
|
|
|
market_data = await coingecko_client.get_market_prices(symbols=[symbol_upper], limit=1)
|
|
|
if market_data:
|
|
|
|
|
|
return [{
|
|
|
'open': market_data[0].get('price', 0),
|
|
|
'high': market_data[0].get('price', 0),
|
|
|
'low': market_data[0].get('price', 0),
|
|
|
'close': market_data[0].get('price', 0),
|
|
|
'volume': 0,
|
|
|
'timestamp': int(datetime.utcnow().timestamp() * 1000)
|
|
|
}]
|
|
|
except:
|
|
|
pass
|
|
|
return []
|
|
|
|
|
|
|
|
|
@router.get("/api/technical/rsi")
|
|
|
async def get_rsi(
|
|
|
symbol: str = Query(..., description="Cryptocurrency symbol (e.g., BTC, ETH)"),
|
|
|
timeframe: str = Query("1h", description="Timeframe (1h, 4h, 1d)"),
|
|
|
period: int = Query(14, ge=1, le=50, description="RSI period"),
|
|
|
limit: int = Query(200, ge=20, le=500, description="Number of candles")
|
|
|
):
|
|
|
"""Get RSI (Relative Strength Index) indicator"""
|
|
|
try:
|
|
|
ohlcv_data = await _fetch_ohlcv_data(symbol, timeframe, limit)
|
|
|
if len(ohlcv_data) < period + 1:
|
|
|
raise HTTPException(status_code=400, detail=f"Not enough data. Need at least {period + 1} candles, got {len(ohlcv_data)}")
|
|
|
|
|
|
candles = [normalize_candle(c) for c in ohlcv_data]
|
|
|
closes = [c['close'] for c in candles]
|
|
|
rsi_value = calculate_rsi(closes, period)
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
"symbol": symbol.upper(),
|
|
|
"timeframe": timeframe,
|
|
|
"indicator": "RSI",
|
|
|
"period": period,
|
|
|
"value": rsi_value,
|
|
|
"signal": "overbought" if rsi_value > 70 else "oversold" if rsi_value < 30 else "neutral",
|
|
|
"timestamp": datetime.utcnow().isoformat() + "Z"
|
|
|
}
|
|
|
except HTTPException:
|
|
|
raise
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error calculating RSI: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.get("/api/technical/macd")
|
|
|
async def get_macd(
|
|
|
symbol: str = Query(..., description="Cryptocurrency symbol"),
|
|
|
timeframe: str = Query("1h", description="Timeframe"),
|
|
|
fast: int = Query(12, ge=1, le=50, description="Fast EMA period"),
|
|
|
slow: int = Query(26, ge=1, le=100, description="Slow EMA period"),
|
|
|
signal: int = Query(9, ge=1, le=50, description="Signal line period"),
|
|
|
limit: int = Query(200, ge=50, le=500, description="Number of candles")
|
|
|
):
|
|
|
"""Get MACD (Moving Average Convergence Divergence) indicator"""
|
|
|
try:
|
|
|
ohlcv_data = await _fetch_ohlcv_data(symbol, timeframe, limit)
|
|
|
if len(ohlcv_data) < slow:
|
|
|
raise HTTPException(status_code=400, detail=f"Not enough data. Need at least {slow} candles")
|
|
|
|
|
|
candles = [normalize_candle(c) for c in ohlcv_data]
|
|
|
closes = [c['close'] for c in candles]
|
|
|
macd_data = calculate_macd(closes, fast, slow, signal)
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
"symbol": symbol.upper(),
|
|
|
"timeframe": timeframe,
|
|
|
"indicator": "MACD",
|
|
|
"macd": macd_data['macd'],
|
|
|
"signal": macd_data['signal'],
|
|
|
"histogram": macd_data['histogram'],
|
|
|
"trend": "bullish" if macd_data['histogram'] > 0 else "bearish",
|
|
|
"timestamp": datetime.utcnow().isoformat() + "Z"
|
|
|
}
|
|
|
except HTTPException:
|
|
|
raise
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error calculating MACD: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.get("/api/technical/bollinger")
|
|
|
async def get_bollinger_bands(
|
|
|
symbol: str = Query(..., description="Cryptocurrency symbol"),
|
|
|
timeframe: str = Query("1h", description="Timeframe"),
|
|
|
period: int = Query(20, ge=5, le=50, description="SMA period"),
|
|
|
std_dev: float = Query(2.0, ge=1.0, le=3.0, description="Standard deviation multiplier"),
|
|
|
limit: int = Query(200, ge=20, le=500, description="Number of candles")
|
|
|
):
|
|
|
"""Get Bollinger Bands indicator"""
|
|
|
try:
|
|
|
ohlcv_data = await _fetch_ohlcv_data(symbol, timeframe, limit)
|
|
|
if len(ohlcv_data) < period:
|
|
|
raise HTTPException(status_code=400, detail=f"Not enough data. Need at least {period} candles")
|
|
|
|
|
|
candles = [normalize_candle(c) for c in ohlcv_data]
|
|
|
closes = [c['close'] for c in candles]
|
|
|
bb_data = calculate_bollinger_bands(closes, period, std_dev)
|
|
|
current_price = closes[-1]
|
|
|
|
|
|
|
|
|
if current_price > bb_data['upper']:
|
|
|
position = "above_upper"
|
|
|
signal = "overbought"
|
|
|
elif current_price < bb_data['lower']:
|
|
|
position = "below_lower"
|
|
|
signal = "oversold"
|
|
|
else:
|
|
|
position = "middle"
|
|
|
signal = "neutral"
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
"symbol": symbol.upper(),
|
|
|
"timeframe": timeframe,
|
|
|
"indicator": "Bollinger Bands",
|
|
|
"period": period,
|
|
|
"std_dev": std_dev,
|
|
|
"upper": bb_data['upper'],
|
|
|
"middle": bb_data['middle'],
|
|
|
"lower": bb_data['lower'],
|
|
|
"width": bb_data['width'],
|
|
|
"current_price": round(current_price, 2),
|
|
|
"position": position,
|
|
|
"signal": signal,
|
|
|
"timestamp": datetime.utcnow().isoformat() + "Z"
|
|
|
}
|
|
|
except HTTPException:
|
|
|
raise
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error calculating Bollinger Bands: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|
|
|
@router.get("/api/technical/indicators")
|
|
|
async def get_all_indicators(
|
|
|
symbol: str = Query(..., description="Cryptocurrency symbol"),
|
|
|
timeframe: str = Query("1h", description="Timeframe"),
|
|
|
rsi_period: int = Query(14, ge=1, le=50, description="RSI period"),
|
|
|
macd_fast: int = Query(12, ge=1, le=50, description="MACD fast period"),
|
|
|
macd_slow: int = Query(26, ge=1, le=100, description="MACD slow period"),
|
|
|
bb_period: int = Query(20, ge=5, le=50, description="Bollinger Bands period"),
|
|
|
limit: int = Query(200, ge=50, le=500, description="Number of candles")
|
|
|
):
|
|
|
"""Get all technical indicators at once (RSI, MACD, Bollinger Bands, SMA, EMA)"""
|
|
|
try:
|
|
|
ohlcv_data = await _fetch_ohlcv_data(symbol, timeframe, limit)
|
|
|
if len(ohlcv_data) < max(rsi_period + 1, macd_slow, bb_period):
|
|
|
raise HTTPException(status_code=400, detail="Not enough data for all indicators")
|
|
|
|
|
|
candles = [normalize_candle(c) for c in ohlcv_data]
|
|
|
closes = [c['close'] for c in candles]
|
|
|
current_price = closes[-1]
|
|
|
|
|
|
|
|
|
rsi = calculate_rsi(closes, rsi_period)
|
|
|
macd = calculate_macd(closes, macd_fast, macd_slow)
|
|
|
bb = calculate_bollinger_bands(closes, bb_period)
|
|
|
sma20 = calculate_sma(closes, 20)
|
|
|
sma50 = calculate_sma(closes, 50) if len(closes) >= 50 else sma20
|
|
|
ema20 = calculate_ema(closes, 20)
|
|
|
|
|
|
|
|
|
sr = find_support_resistance(candles)
|
|
|
|
|
|
return {
|
|
|
"success": True,
|
|
|
"symbol": symbol.upper(),
|
|
|
"timeframe": timeframe,
|
|
|
"current_price": round(current_price, 2),
|
|
|
"indicators": {
|
|
|
"rsi": {
|
|
|
"value": rsi,
|
|
|
"period": rsi_period,
|
|
|
"signal": "overbought" if rsi > 70 else "oversold" if rsi < 30 else "neutral"
|
|
|
},
|
|
|
"macd": {
|
|
|
"macd": macd['macd'],
|
|
|
"signal": macd['signal'],
|
|
|
"histogram": macd['histogram'],
|
|
|
"trend": "bullish" if macd['histogram'] > 0 else "bearish"
|
|
|
},
|
|
|
"bollinger_bands": {
|
|
|
"upper": bb['upper'],
|
|
|
"middle": bb['middle'],
|
|
|
"lower": bb['lower'],
|
|
|
"width": bb['width'],
|
|
|
"position": "above_upper" if current_price > bb['upper'] else "below_lower" if current_price < bb['lower'] else "middle"
|
|
|
},
|
|
|
"sma": {
|
|
|
"sma20": round(sma20, 2),
|
|
|
"sma50": round(sma50, 2),
|
|
|
"trend": "bullish" if current_price > sma20 > sma50 else "bearish" if current_price < sma20 < sma50 else "neutral"
|
|
|
},
|
|
|
"ema": {
|
|
|
"ema20": round(ema20, 2)
|
|
|
}
|
|
|
},
|
|
|
"support_resistance": sr,
|
|
|
"timestamp": datetime.utcnow().isoformat() + "Z"
|
|
|
}
|
|
|
except HTTPException:
|
|
|
raise
|
|
|
except Exception as e:
|
|
|
logger.error(f"Error calculating indicators: {e}")
|
|
|
raise HTTPException(status_code=500, detail=str(e))
|
|
|
|
|
|
|