walidukdz commited on
Commit
c79af28
·
verified ·
1 Parent(s): 79f8623

Create CryptoAnalyzer.py

Browse files
Files changed (1) hide show
  1. tools/CryptoAnalyzer.py +756 -0
tools/CryptoAnalyzer.py ADDED
@@ -0,0 +1,756 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import ccxt
2
+ import pandas as pd
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ import matplotlib.dates as mdates
6
+ from datetime import datetime, timedelta
7
+ import ta
8
+ from typing import List, Dict, Any, Optional, Tuple, Union, Callable
9
+ import os
10
+ import logging
11
+ import json
12
+ import time
13
+ from dataclasses import dataclass, asdict
14
+ import traceback
15
+
16
+ # Configure logging
17
+ logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
18
+ logger = logging.getLogger(__name__)
19
+
20
+ @dataclass
21
+ class AnalysisResult:
22
+ """Data class for structured analysis results"""
23
+ ticker: str
24
+ timeframe: str
25
+ summary: Dict[str, Any]
26
+ chart_path: str
27
+ indicators_used: List[str]
28
+ data_points: int
29
+ period: str
30
+ timestamp: str
31
+
32
+ def to_dict(self) -> Dict[str, Any]:
33
+ """Convert to dictionary"""
34
+ return asdict(self)
35
+
36
+ def to_json(self) -> str:
37
+ """Convert to JSON string"""
38
+ return json.dumps(self.to_dict(), indent=2)
39
+
40
+ def get_trading_signal(self) -> Tuple[str, float]:
41
+ """Extract trading signal from analysis"""
42
+ signal = "NEUTRAL"
43
+ confidence = 0.5
44
+
45
+ # Extract trend info
46
+ if 'trend' in self.summary:
47
+ if self.summary['trend'] == "Bullish":
48
+ signal = "BUY"
49
+ confidence = 0.7
50
+ elif self.summary['trend'] == "Bearish":
51
+ signal = "SELL"
52
+ confidence = 0.7
53
+
54
+ # Factor in RSI
55
+ if 'RSI' in self.summary:
56
+ rsi_value = self.summary['RSI']['value']
57
+ if rsi_value < 30 and signal != "BUY":
58
+ signal = "BUY"
59
+ confidence = max(confidence, 0.8)
60
+ elif rsi_value > 70 and signal != "SELL":
61
+ signal = "SELL"
62
+ confidence = max(confidence, 0.8)
63
+
64
+ # Consider MACD
65
+ if 'MACD' in self.summary:
66
+ if self.summary['MACD']['interpretation'] == "Bullish crossover" and signal != "SELL":
67
+ signal = "BUY"
68
+ confidence = max(confidence, 0.75)
69
+ elif self.summary['MACD']['interpretation'] == "Bearish crossover" and signal != "BUY":
70
+ signal = "SELL"
71
+ confidence = max(confidence, 0.75)
72
+
73
+ return signal, confidence
74
+
75
+ def get_summary_text(self) -> str:
76
+ """Get summary as formatted text"""
77
+ summary_lines = [f"**Analysis for {self.ticker} ({self.timeframe}):**\n"]
78
+
79
+ # Add trend information
80
+ if 'trend' in self.summary:
81
+ summary_lines.append(f"Overall Trend: {self.summary['trend']}")
82
+
83
+ # Add price information
84
+ if 'price' in self.summary:
85
+ price_info = self.summary['price']
86
+ change_text = f" (24h change: {price_info['change_24h']}%)" if price_info['change_24h'] is not None else ""
87
+ summary_lines.append(f"Current Price: {price_info['current']}{change_text}")
88
+
89
+ # Add RSI information
90
+ if 'RSI' in self.summary:
91
+ rsi_info = self.summary['RSI']
92
+ summary_lines.append(f"RSI: {rsi_info['value']} - {rsi_info['interpretation']}")
93
+
94
+ # Add MACD information
95
+ if 'MACD' in self.summary:
96
+ macd_info = self.summary['MACD']
97
+ summary_lines.append(f"MACD: {macd_info['value']}, Signal: {macd_info['signal']}")
98
+ summary_lines.append(f"MACD Interpretation: {macd_info['interpretation']}")
99
+
100
+ # Add Bollinger Bands information
101
+ if 'Bollinger_Bands' in self.summary:
102
+ bb_info = self.summary['Bollinger_Bands']
103
+ summary_lines.append(f"Bollinger Bands: Upper: {bb_info['upper']}, Middle: {bb_info['middle']}, Lower: {bb_info['lower']}")
104
+ summary_lines.append(f"Bandwidth: {bb_info['bandwidth']}%, Position: {bb_info['position']}, Squeeze: {bb_info['squeeze']}")
105
+
106
+ # Add support and resistance levels if available
107
+ if 'Support' in self.summary and 'Resistance' in self.summary:
108
+ summary_lines.append(f"Support Levels: {self.summary['Support']}")
109
+ summary_lines.append(f"Resistance Levels: {self.summary['Resistance']}")
110
+
111
+ # Add trading signal
112
+ signal, confidence = self.get_trading_signal()
113
+ summary_lines.append(f"\nTrading Signal: {signal} (Confidence: {confidence:.2f})")
114
+
115
+ # Add chart path
116
+ summary_lines.append(f"\nChart saved to: {self.chart_path}")
117
+
118
+ # Add analysis period
119
+ summary_lines.append(f"Analysis period: {self.period}")
120
+
121
+ return "\n".join(summary_lines)
122
+
123
+ class CryptoAnalyzer:
124
+ """A class to analyze cryptocurrency charts with technical indicators."""
125
+
126
+ def __init__(self,
127
+ exchange_name: str = "binance",
128
+ output_dir: str = "./charts",
129
+ rate_limit_pause: float = 1.0,
130
+ max_retries: int = 3):
131
+ """
132
+ Initialize the crypto analyzer with an exchange.
133
+
134
+ Args:
135
+ exchange_name (str): Name of CCXT-supported exchange (default: "binance")
136
+ output_dir (str): Directory to save chart images
137
+ rate_limit_pause (float): Pause between API calls to avoid rate limits
138
+ max_retries (int): Maximum number of API call retries
139
+ """
140
+ try:
141
+ self.exchange = getattr(ccxt, exchange_name)()
142
+ self.output_dir = output_dir
143
+ self.rate_limit_pause = rate_limit_pause
144
+ self.max_retries = max_retries
145
+ self.supports_advanced_patterns = True # Flag for pattern recognition capabilities
146
+
147
+ # Create output directory if it doesn't exist
148
+ if not os.path.exists(output_dir):
149
+ os.makedirs(output_dir)
150
+
151
+ # Cache for market data to reduce API calls
152
+ self._market_cache = {}
153
+ self._last_api_call = 0
154
+
155
+ except AttributeError:
156
+ supported = ", ".join(ccxt.exchanges)
157
+ raise ValueError(f"Exchange '{exchange_name}' not supported. Choose from: {supported}")
158
+ except Exception as e:
159
+ raise Exception(f"Failed to initialize analyzer: {str(e)}")
160
+
161
+ def get_supported_exchanges(self) -> List[str]:
162
+ """Return list of supported exchanges"""
163
+ return ccxt.exchanges
164
+
165
+ def get_supported_timeframes(self) -> List[str]:
166
+ """Return list of supported timeframes for current exchange"""
167
+ return list(self.exchange.timeframes.keys()) if hasattr(self.exchange, 'timeframes') else ["1m", "5m", "15m", "30m", "1h", "4h", "1d", "1w"]
168
+
169
+ def get_supported_indicators(self) -> Dict[str, str]:
170
+ """Return dictionary of supported indicators with descriptions"""
171
+ return {
172
+ "RSI": "Relative Strength Index - Momentum oscillator measuring speed and change of price movements",
173
+ "MACD": "Moving Average Convergence Divergence - Trend-following momentum indicator",
174
+ "SMA": "Simple Moving Average - Average price over specified period",
175
+ "EMA": "Exponential Moving Average - Weighted moving average giving more importance to recent prices",
176
+ "BB": "Bollinger Bands - Volatility indicator showing price channels around moving average",
177
+ "ATR": "Average True Range - Volatility indicator measuring market volatility",
178
+ "OBV": "On-Balance Volume - Volume indicator using volume flow to predict changes in price",
179
+ "VWAP": "Volume Weighted Average Price - Average price weighted by volume",
180
+ "Ichimoku": "Ichimoku Cloud - Trend indicator showing support/resistance levels and momentum",
181
+ "Stochastic": "Stochastic Oscillator - Momentum indicator comparing close price to price range",
182
+ "Patterns": "Candlestick pattern recognition for common bullish and bearish patterns"
183
+ }
184
+
185
+ def _respect_rate_limit(self):
186
+ """Implement rate limiting to avoid API restrictions"""
187
+ elapsed = time.time() - self._last_api_call
188
+ if elapsed < self.rate_limit_pause:
189
+ time.sleep(self.rate_limit_pause - elapsed)
190
+ self._last_api_call = time.time()
191
+
192
+ def get_available_pairs(self, quote_currency: Optional[str] = None) -> List[str]:
193
+ """
194
+ Get available trading pairs from exchange
195
+
196
+ Args:
197
+ quote_currency (Optional[str]): Filter by quote currency (e.g., "USD", "BTC")
198
+
199
+ Returns:
200
+ List[str]: List of available trading pairs
201
+ """
202
+ try:
203
+ if 'markets' not in self._market_cache:
204
+ self._respect_rate_limit()
205
+ self._market_cache['markets'] = self.exchange.load_markets()
206
+
207
+ pairs = list(self._market_cache['markets'].keys())
208
+
209
+ if quote_currency:
210
+ pairs = [p for p in pairs if p.endswith(f"/{quote_currency}")]
211
+
212
+ return pairs
213
+ except Exception as e:
214
+ logger.error(f"Error fetching available pairs: {str(e)}")
215
+ return []
216
+
217
+ def fetch_data(self,
218
+ ticker: str,
219
+ timeframe: str,
220
+ days: int = 30,
221
+ retry_on_error: bool = True) -> pd.DataFrame:
222
+ """
223
+ Fetch OHLCV data for a specified ticker and timeframe.
224
+
225
+ Args:
226
+ ticker (str): Trading pair (e.g., "BTC/USD")
227
+ timeframe (str): Timeframe (e.g., "1h", "4h", "1d")
228
+ days (int): Number of days of historical data to fetch
229
+ retry_on_error (bool): Whether to retry on network errors
230
+
231
+ Returns:
232
+ pd.DataFrame: DataFrame with OHLCV data
233
+ """
234
+ retries = 0
235
+ last_error = None
236
+
237
+ while retries <= self.max_retries:
238
+ try:
239
+ # Format symbol according to exchange requirements
240
+ symbol = ticker
241
+
242
+ # Calculate timestamp for the specified number of days ago
243
+ since = int((datetime.now() - timedelta(days=days)).timestamp() * 1000)
244
+
245
+ # Fetch OHLCV data with rate limiting
246
+ self._respect_rate_limit()
247
+ logger.info(f"Fetching {days} days of {timeframe} data for {ticker}")
248
+ ohlcv = self.exchange.fetch_ohlcv(symbol, timeframe=timeframe, since=since, limit=1000)
249
+
250
+ # Check if we got data
251
+ if not ohlcv or len(ohlcv) < 2:
252
+ raise ValueError(f"Insufficient data returned for {ticker} ({timeframe})")
253
+
254
+ # Convert to DataFrame
255
+ df = pd.DataFrame(ohlcv, columns=['timestamp', 'open', 'high', 'low', 'close', 'volume'])
256
+ df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
257
+ df.set_index('timestamp', inplace=True)
258
+
259
+ return df
260
+
261
+ except (ccxt.NetworkError, ccxt.ExchangeNotAvailable) as e:
262
+ retries += 1
263
+ last_error = e
264
+ wait_time = retries * 2 # Exponential backoff
265
+
266
+ if retry_on_error and retries <= self.max_retries:
267
+ logger.warning(f"Network error: {str(e)}. Retrying in {wait_time}s... (Attempt {retries}/{self.max_retries})")
268
+ time.sleep(wait_time)
269
+ else:
270
+ raise ConnectionError(f"Failed to fetch data after {retries} attempts: {str(e)}")
271
+
272
+ except ccxt.ExchangeError as e:
273
+ logger.error(f"Exchange error: {str(e)}")
274
+ raise ValueError(f"Failed to fetch data: Exchange error - {str(e)}")
275
+
276
+ except Exception as e:
277
+ logger.error(f"Unexpected error: {str(e)}")
278
+ raise Exception(f"Failed to fetch data: {str(e)}")
279
+
280
+ # If we got here, we've exhausted retries
281
+ raise ConnectionError(f"Failed to fetch data after {self.max_retries} attempts: {str(last_error)}")
282
+
283
+ def calculate_indicators(self, df: pd.DataFrame, indicators: List[str]) -> pd.DataFrame:
284
+ """
285
+ Calculate technical indicators based on price data.
286
+
287
+ Args:
288
+ df (pd.DataFrame): OHLCV DataFrame
289
+ indicators (List[str]): List of indicators to calculate
290
+
291
+ Returns:
292
+ pd.DataFrame: DataFrame with added indicator columns
293
+ """
294
+ analysis = pd.DataFrame()
295
+ analysis['close'] = df['close']
296
+ analysis['open'] = df['open']
297
+ analysis['high'] = df['high']
298
+ analysis['low'] = df['low']
299
+ analysis['volume'] = df['volume']
300
+
301
+ indicator_map = {
302
+ "RSI": lambda: self._add_rsi(analysis, window=14),
303
+ "MACD": lambda: self._add_macd(analysis),
304
+ "SMA": lambda: self._add_sma(analysis, window=20),
305
+ "EMA": lambda: self._add_ema(analysis, window=20),
306
+ "BB": lambda: self._add_bollinger_bands(analysis, window=20, std=2),
307
+ "ATR": lambda: self._add_atr(analysis, df, window=14),
308
+ "OBV": lambda: self._add_obv(analysis, df),
309
+ "VWAP": lambda: self._add_vwap(analysis, df),
310
+ "Ichimoku": lambda: self._add_ichimoku(analysis),
311
+ "Stochastic": lambda: self._add_stochastic(analysis),
312
+ "Patterns": lambda: self._add_candlestick_patterns(analysis)
313
+ }
314
+
315
+ # Calculate requested indicators
316
+ for indicator in indicators:
317
+ if indicator in indicator_map:
318
+ try:
319
+ indicator_map[indicator]()
320
+ except Exception as e:
321
+ logger.warning(f"Failed to calculate {indicator}: {str(e)}")
322
+ else:
323
+ logger.warning(f"Indicator '{indicator}' not supported.")
324
+
325
+ return analysis
326
+
327
+ def _add_rsi(self, df: pd.DataFrame, window: int = 14) -> None:
328
+ """Add Relative Strength Index to DataFrame."""
329
+ df['RSI'] = ta.momentum.RSIIndicator(df['close'], window=window).rsi()
330
+
331
+ def _add_macd(self, df: pd.DataFrame, fast: int = 12, slow: int = 26, signal: int = 9) -> None:
332
+ """Add MACD indicator to DataFrame."""
333
+ macd_indicator = ta.trend.MACD(
334
+ df['close'],
335
+ window_fast=fast,
336
+ window_slow=slow,
337
+ window_sign=signal
338
+ )
339
+ df['MACD'] = macd_indicator.macd()
340
+ df['MACD_signal'] = macd_indicator.macd_signal()
341
+ df['MACD_histogram'] = macd_indicator.macd_diff()
342
+
343
+ def _add_sma(self, df: pd.DataFrame, window: int = 20) -> None:
344
+ """Add Simple Moving Average to DataFrame."""
345
+ df[f'SMA_{window}'] = ta.trend.SMAIndicator(df['close'], window=window).sma_indicator()
346
+
347
+ def _add_ema(self, df: pd.DataFrame, window: int = 20) -> None:
348
+ """Add Exponential Moving Average to DataFrame."""
349
+ df[f'EMA_{window}'] = ta.trend.EMAIndicator(df['close'], window=window).ema_indicator()
350
+
351
+ def _add_bollinger_bands(self, df: pd.DataFrame, window: int = 20, std: float = 2) -> None:
352
+ """Add Bollinger Bands to DataFrame."""
353
+ bollinger = ta.volatility.BollingerBands(df['close'], window=window, window_dev=std)
354
+ df['BB_upper'] = bollinger.bollinger_hband()
355
+ df['BB_middle'] = bollinger.bollinger_mavg()
356
+ df['BB_lower'] = bollinger.bollinger_lband()
357
+
358
+ def _add_atr(self, df: pd.DataFrame, ohlc: pd.DataFrame, window: int = 14) -> None:
359
+ """Add Average True Range to DataFrame."""
360
+ df['ATR'] = ta.volatility.AverageTrueRange(
361
+ high=ohlc['high'],
362
+ low=ohlc['low'],
363
+ close=ohlc['close'],
364
+ window=window
365
+ ).average_true_range()
366
+
367
+ def _add_obv(self, df: pd.DataFrame, ohlc: pd.DataFrame) -> None:
368
+ """Add On-Balance Volume to DataFrame."""
369
+ df['OBV'] = ta.volume.OnBalanceVolumeIndicator(
370
+ close=ohlc['close'],
371
+ volume=ohlc['volume']
372
+ ).on_balance_volume()
373
+
374
+ def _add_vwap(self, df: pd.DataFrame, ohlc: pd.DataFrame) -> None:
375
+ """Add Volume Weighted Average Price to DataFrame."""
376
+ try:
377
+ # Reset index to access timestamp for VWAP calculation
378
+ temp_df = ohlc.reset_index()
379
+ # Group by date for daily VWAP
380
+ temp_df['date'] = temp_df['timestamp'].dt.date
381
+
382
+ typical_price = (temp_df['high'] + temp_df['low'] + temp_df['close']) / 3
383
+ temp_df['VWAP'] = (typical_price * temp_df['volume']).cumsum() / temp_df['volume'].cumsum()
384
+
385
+ # Set back to original index
386
+ df['VWAP'] = temp_df.set_index('timestamp')['VWAP']
387
+ except Exception as e:
388
+ logger.error(f"VWAP calculation error: {str(e)}")
389
+
390
+ def _add_ichimoku(self, df: pd.DataFrame) -> None:
391
+ """Add Ichimoku Cloud indicator to DataFrame."""
392
+ try:
393
+ # Tenkan-sen (Conversion Line): (9-period high + 9-period low)/2
394
+ period9_high = df['high'].rolling(window=9).max()
395
+ period9_low = df['low'].rolling(window=9).min()
396
+ df['tenkan_sen'] = (period9_high + period9_low) / 2
397
+
398
+ # Kijun-sen (Base Line): (26-period high + 26-period low)/2
399
+ period26_high = df['high'].rolling(window=26).max()
400
+ period26_low = df['low'].rolling(window=26).min()
401
+ df['kijun_sen'] = (period26_high + period26_low) / 2
402
+
403
+ # Senkou Span A (Leading Span A): (Conversion Line + Base Line)/2
404
+ df['senkou_span_a'] = ((df['tenkan_sen'] + df['kijun_sen']) / 2).shift(26)
405
+
406
+ # Senkou Span B (Leading Span B): (52-period high + 52-period low)/2
407
+ period52_high = df['high'].rolling(window=52).max()
408
+ period52_low = df['low'].rolling(window=52).min()
409
+ df['senkou_span_b'] = ((period52_high + period52_low) / 2).shift(26)
410
+
411
+ # Chikou Span (Lagging Span): Close price shifted back 26 periods
412
+ df['chikou_span'] = df['close'].shift(-26)
413
+ except Exception as e:
414
+ logger.error(f"Ichimoku calculation error: {str(e)}")
415
+
416
+ def _add_stochastic(self, df: pd.DataFrame, k_window: int = 14, d_window: int = 3) -> None:
417
+ """Add Stochastic Oscillator to DataFrame."""
418
+ try:
419
+ stoch = ta.momentum.StochasticOscillator(
420
+ high=df['high'],
421
+ low=df['low'],
422
+ close=df['close'],
423
+ window=k_window,
424
+ smooth_window=d_window
425
+ )
426
+ df['stoch_k'] = stoch.stoch()
427
+ df['stoch_d'] = stoch.stoch_signal()
428
+ except Exception as e:
429
+ logger.error(f"Stochastic calculation error: {str(e)}")
430
+
431
+ def _add_candlestick_patterns(self, df: pd.DataFrame) -> None:
432
+ """Add candlestick pattern recognition to DataFrame."""
433
+ try:
434
+ # Detect common candlestick patterns
435
+ # Bullish patterns
436
+ df['doji'] = ta.candlestick.doji(df['open'], df['high'], df['low'], df['close'])
437
+ df['hammer'] = ta.candlestick.hammer(df['open'], df['high'], df['low'], df['close'])
438
+ df['morning_star'] = ta.candlestick.morning_star(df['open'], df['high'], df['low'], df['close'])
439
+
440
+ # Bearish patterns
441
+ df['shooting_star'] = ta.candlestick.shooting_star(df['open'], df['high'], df['low'], df['close'])
442
+ df['evening_star'] = ta.candlestick.evening_star(df['open'], df['high'], df['low'], df['close'])
443
+ df['bearish_harami'] = ta.candlestick.bearish_harami(df['open'], df['high'], df['low'], df['close'])
444
+
445
+ # Consolidate patterns into single column for easy identification
446
+ df['bullish_pattern'] = (df['doji'] | df['hammer'] | df['morning_star'])
447
+ df['bearish_pattern'] = (df['shooting_star'] | df['evening_star'] | df['bearish_harami'])
448
+ except Exception as e:
449
+ logger.error(f"Pattern recognition error: {str(e)}")
450
+
451
+ def identify_support_resistance(self, df: pd.DataFrame, window: int = 20, threshold: float = 0.03) -> Tuple[List[float], List[float]]:
452
+ """
453
+ Identify support and resistance levels using pivot points
454
+
455
+ Args:
456
+ df (pd.DataFrame): OHLCV data
457
+ window (int): Lookback window for pivot identification
458
+ threshold (float): Minimum price change to consider a pivot
459
+
460
+ Returns:
461
+ Tuple[List[float], List[float]]: Support and resistance levels
462
+ """
463
+ try:
464
+ # Identify pivot highs (resistance)
465
+ pivot_highs = []
466
+ for i in range(window, len(df) - window):
467
+ if all(df['high'].iloc[i] > df['high'].iloc[i-j] for j in range(1, window+1)) and \
468
+ all(df['high'].iloc[i] > df['high'].iloc[i+j] for j in range(1, window+1)):
469
+ pivot_highs.append(df['high'].iloc[i])
470
+
471
+ # Identify pivot lows (support)
472
+ pivot_lows = []
473
+ for i in range(window, len(df) - window):
474
+ if all(df['low'].iloc[i] < df['low'].iloc[i-j] for j in range(1, window+1)) and \
475
+ all(df['low'].iloc[i] < df['low'].iloc[i+j] for j in range(1, window+1)):
476
+ pivot_lows.append(df['low'].iloc[i])
477
+
478
+ # Group close levels
479
+ def group_levels(levels, threshold):
480
+ if not levels:
481
+ return []
482
+
483
+ levels = sorted(levels)
484
+ grouped = []
485
+ current_group = [levels[0]]
486
+
487
+ for level in levels[1:]:
488
+ if (level - current_group[0]) / current_group[0] <= threshold:
489
+ current_group.append(level)
490
+ else:
491
+ grouped.append(sum(current_group) / len(current_group))
492
+ current_group = [level]
493
+
494
+ if current_group:
495
+ grouped.append(sum(current_group) / len(current_group))
496
+
497
+ return grouped
498
+
499
+ return group_levels(pivot_lows, threshold), group_levels(pivot_highs, threshold)
500
+
501
+ except Exception as e:
502
+ logger.error(f"Support/resistance calculation error: {str(e)}")
503
+ return [], []
504
+
505
+ def generate_analysis_summary(self, analysis: pd.DataFrame, indicators: List[str]) -> Dict[str, Any]:
506
+ """
507
+ Generate a summary of the technical analysis.
508
+
509
+ Args:
510
+ analysis (pd.DataFrame): DataFrame with indicator data
511
+ indicators (List[str]): List of indicators used
512
+
513
+ Returns:
514
+ Dict[str, Any]: Dictionary with analysis results
515
+ """
516
+ summary = {}
517
+
518
+ try:
519
+ latest = analysis.iloc[-1]
520
+
521
+ # Overall trend determination
522
+ trend = "Neutral"
523
+ trend_strength = 0
524
+
525
+ # Using moving averages for trend
526
+ if "SMA" in indicators and "EMA" in indicators:
527
+ sma_cols = [col for col in analysis.columns if 'SMA' in col]
528
+ ema_cols = [col for col in analysis.columns if 'EMA' in col]
529
+ if sma_cols and ema_cols:
530
+ sma_col = sma_cols[0]
531
+ ema_col = ema_cols[0]
532
+
533
+ if latest['close'] > latest[sma_col] and latest['close'] > latest[ema_col]:
534
+ trend = "Bullish"
535
+ trend_strength += 1
536
+ elif latest['close'] < latest[sma_col] and latest['close'] < latest[ema_col]:
537
+ trend = "Bearish"
538
+ trend_strength += 1
539
+
540
+ # Using RSI for trend confirmation
541
+ if "RSI" in indicators and not pd.isna(latest.get('RSI', np.nan)):
542
+ rsi_value = latest['RSI']
543
+ if rsi_value > 60:
544
+ if trend == "Bullish":
545
+ trend_strength += 1
546
+ else:
547
+ trend = "Bullish"
548
+ elif rsi_value < 40:
549
+ if trend == "Bearish":
550
+ trend_strength += 1
551
+ else:
552
+ trend = "Bearish"
553
+
554
+ # Using MACD for trend confirmation
555
+ if "MACD" in indicators and not pd.isna(latest.get('MACD', np.nan)):
556
+ if latest['MACD'] > latest['MACD_signal']:
557
+ if trend == "Bullish":
558
+ trend_strength += 1
559
+ else:
560
+ trend = "Bullish"
561
+ elif latest['MACD'] < latest['MACD_signal']:
562
+ if trend == "Bearish":
563
+ trend_strength += 1
564
+ else:
565
+ trend = "Bearish"
566
+
567
+ # Store trend information
568
+ summary['trend'] = trend
569
+ summary['trend_strength'] = f"{trend_strength}/3" if trend_strength > 0 else "Weak"
570
+
571
+ # RSI analysis
572
+ if "RSI" in indicators and not pd.isna(latest.get('RSI', np.nan)):
573
+ rsi_value = latest['RSI']
574
+
575
+ # Check RSI divergence
576
+ has_divergence = False
577
+ divergence_type = None
578
+
579
+ if len(analysis) > 20: # Need enough data for divergence
580
+ # Find recent price high/low
581
+ price_section = analysis['close'].iloc[-20:]
582
+ rsi_section = analysis['RSI'].iloc[-20:]
583
+
584
+ price_high_idx = price_section.idxmax()
585
+ price_low_idx = price_section.idxmin()
586
+ rsi_high_idx = rsi_section.idxmax()
587
+ rsi_low_idx = rsi_section.idxmin()
588
+
589
+ # Bearish divergence: price makes higher high, RSI makes lower high
590
+ if price_high_idx != rsi_high_idx and price_section.max() > price_section.iloc[0]:
591
+ has_divergence = True
592
+ divergence_type = "Bearish"
593
+
594
+ # Bullish divergence: price makes lower low, RSI makes higher low
595
+ if price_low_idx != rsi_low_idx and price_section.min() < price_section.iloc[0]:
596
+ has_divergence = True
597
+ divergence_type = "Bullish"
598
+
599
+ summary['RSI'] = {
600
+ 'value': round(rsi_value, 2),
601
+ 'interpretation': "Overbought" if rsi_value > 70 else "Oversold" if rsi_value < 30 else "Neutral",
602
+ 'has_divergence': has_divergence,
603
+ 'divergence_type': divergence_type
604
+ }
605
+
606
+ # MACD analysis
607
+ if "MACD" in indicators and not pd.isna(latest.get('MACD', np.nan)):
608
+ macd_value = latest['MACD']
609
+ signal_value = latest['MACD_signal']
610
+ cross_direction = None
611
+
612
+ # Check for recent crossover (past 5 periods)
613
+ for i in range(min(5, len(analysis) - 1)):
614
+ prev_idx = -2 - i
615
+ if (analysis['MACD'].iloc[prev_idx] <= analysis['MACD_signal'].iloc[prev_idx] and
616
+ macd_value > signal_value):
617
+ cross_direction = "Bullish crossover"
618
+ break
619
+ elif (analysis['MACD'].iloc[prev_idx] >= analysis['MACD_signal'].iloc[prev_idx] and
620
+ macd_value < signal_value):
621
+ cross_direction = "Bearish crossover"
622
+ break
623
+
624
+ # Check for histogram momentum
625
+ histogram_momentum = "Increasing" if latest['MACD_histogram'] > analysis['MACD_histogram'].iloc[-2] else "Decreasing"
626
+
627
+ summary['MACD'] = {
628
+ 'value': round(macd_value, 2),
629
+ 'signal': round(signal_value, 2),
630
+ 'histogram': round(latest['MACD_histogram'], 2),
631
+ 'histogram_momentum': histogram_momentum,
632
+ 'interpretation': cross_direction if cross_direction else "Neutral"
633
+ }
634
+
635
+ # Bollinger Bands analysis
636
+ if "BB" in indicators and "BB_upper" in analysis.columns:
637
+ # Calculate bandwidth
638
+ bandwidth = (latest['BB_upper'] - latest['BB_lower']) / latest['BB_middle'] * 100
639
+ if latest['close'] > latest['BB_upper']:
640
+ bb_position = "Above upper band (potentially overbought)"
641
+ elif latest['close'] < latest['BB_lower']:
642
+ bb_position = "Below lower band (potentially oversold)"
643
+ else:
644
+ # Calculate position within bands as percentage
645
+ band_width = latest['BB_upper'] - latest['BB_lower']
646
+ if band_width > 0:
647
+ position = (latest['close'] - latest['BB_lower']) / band_width * 100
648
+ bb_position = f"Within bands ({round(position, 1)}% from lower band)"
649
+ else:
650
+ bb_position = "Within bands"
651
+
652
+ # Check for BB squeeze (narrowing bands)
653
+ is_squeeze = False
654
+ if len(analysis) > 20:
655
+ prev_bandwidth = (analysis['BB_upper'].iloc[-20] - analysis['BB_lower'].iloc[-20]) / analysis['BB_middle'].iloc[-20] * 100
656
+ is_squeeze = bandwidth < prev_bandwidth * 0.8 # 20% narrower bands
657
+
658
+ summary['Bollinger_Bands'] = {
659
+ 'upper': round(latest['BB_upper'], 2),
660
+ 'middle': round(latest['BB_middle'], 2),
661
+ 'lower': round(latest['BB_lower'], 2),
662
+ 'bandwidth': round(bandwidth, 2),
663
+ 'position': bb_position,
664
+ 'squeeze': is_squeeze
665
+ }
666
+
667
+ # Support and Resistance levels
668
+ support, resistance = self.identify_support_resistance(analysis)
669
+ summary['Support'] = support
670
+ summary['Resistance'] = resistance
671
+
672
+ return summary
673
+
674
+ except Exception as e:
675
+ logger.error(f"Error generating summary: {str(e)}")
676
+ return summary
677
+
678
+ def plot_chart(self, df: pd.DataFrame, ticker: str, timeframe: str, analysis: pd.DataFrame, indicators: List[str]) -> str:
679
+ """
680
+ Plot candlestick chart with indicators and save chart image.
681
+
682
+ Returns:
683
+ str: File path of saved chart image.
684
+ """
685
+ try:
686
+ plt.figure(figsize=(12,8))
687
+ # Plot close price
688
+ plt.plot(df.index, df['close'], label='Close Price', color='blue')
689
+
690
+ # Plot SMA and EMA if available
691
+ for col in df.columns:
692
+ if 'SMA' in col or 'EMA' in col:
693
+ plt.plot(df.index, df[col], label=col)
694
+
695
+ # Format x-axis with date labels
696
+ plt.gca().xaxis.set_major_formatter(mdates.DateFormatter('%Y-%m-%d'))
697
+ plt.gca().xaxis.set_major_locator(mdates.AutoDateLocator())
698
+ plt.gcf().autofmt_xdate()
699
+
700
+ plt.title(f"{ticker} Price Chart ({timeframe})")
701
+ plt.xlabel("Date")
702
+ plt.ylabel("Price")
703
+ plt.legend()
704
+ plt.grid(True)
705
+
706
+ # Save chart
707
+ timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
708
+ chart_filename = f"{ticker.replace('/', '_')}_{timeframe}_{timestamp}.png"
709
+ chart_path = os.path.join(self.output_dir, chart_filename)
710
+ plt.savefig(chart_path)
711
+ plt.close()
712
+ return chart_path
713
+ except Exception as e:
714
+ logger.error(f"Chart plotting error: {str(e)}")
715
+ return ""
716
+
717
+ def analyze(self, ticker: str, timeframe: str, indicators: List[str], days: int = 30) -> AnalysisResult:
718
+ """
719
+ Perform full analysis: fetch data, calculate indicators, generate summary and chart.
720
+
721
+ Returns:
722
+ AnalysisResult: Structured analysis result.
723
+ """
724
+ try:
725
+ df = self.fetch_data(ticker, timeframe, days=days)
726
+ analysis_df = self.calculate_indicators(df, indicators)
727
+ summary = self.generate_analysis_summary(analysis_df, indicators)
728
+ chart_path = self.plot_chart(df, ticker, timeframe, analysis_df, indicators)
729
+ period = f"Last {days} days"
730
+ timestamp = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
731
+ result = AnalysisResult(
732
+ ticker=ticker,
733
+ timeframe=timeframe,
734
+ summary=summary,
735
+ chart_path=chart_path,
736
+ indicators_used=indicators,
737
+ data_points=len(df),
738
+ period=period,
739
+ timestamp=timestamp
740
+ )
741
+ return result
742
+ except Exception as e:
743
+ logger.error(f"Analysis failed: {traceback.format_exc()}")
744
+ raise e
745
+
746
+ if __name__ == "__main__":
747
+ analyzer = CryptoAnalyzer(exchange_name="binance", output_dir="./charts", rate_limit_pause=1.0, max_retries=3)
748
+ ticker = "BTC/USDT"
749
+ timeframe = "1d"
750
+ indicators = ["RSI", "MACD", "SMA", "EMA", "BB", "ATR", "OBV", "VWAP", "Ichimoku", "Stochastic", "Patterns"]
751
+ try:
752
+ result = analyzer.analyze(ticker, timeframe, indicators, days=90)
753
+ print(result.get_summary_text())
754
+ print(result.to_json())
755
+ except Exception as e:
756
+ print(f"Error during analysis: {e}")