File size: 12,471 Bytes
fc115d5
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
"""
Multi-Timeframe Confluence Analyzer

Requires alignment across multiple timeframes before trading:
- 4H: Overall trend direction (higher timeframe bias)
- 1H: Primary trading timeframe (model operates here)
- 15m: Entry timing (fine-tune entries)

Only trades when all timeframes agree on direction.
"""

import numpy as np
import pandas as pd
from typing import Dict, Tuple, Optional
from dataclasses import dataclass
from enum import Enum
import logging
import requests
from datetime import datetime, timedelta

logger = logging.getLogger(__name__)


class TrendDirection(Enum):
    BULLISH = "bullish"
    BEARISH = "bearish"
    NEUTRAL = "neutral"


@dataclass
class TimeframeSignal:
    """Signal from a single timeframe."""
    timeframe: str
    direction: TrendDirection
    strength: float  # 0-1
    price: float
    ema_fast: float
    ema_slow: float
    rsi: float


@dataclass
class ConfluenceResult:
    """Result of multi-timeframe analysis."""
    aligned: bool
    direction: TrendDirection
    strength: float  # Average strength across timeframes
    signals: Dict[str, TimeframeSignal]
    recommendation: str


class MultiTimeframeAnalyzer:
    """
    Analyze multiple timeframes for trade confluence.
    
    Strategy:
    - 4H determines overall bias (trend direction)
    - 1H is the primary trading timeframe
    - 15m provides entry timing confirmation
    
    Only enter trades when all timeframes align.
    """
    
    def __init__(
        self,
        symbol: str = "BTCUSDT",
        ema_fast: int = 9,
        ema_slow: int = 21,
        rsi_period: int = 14,
        rsi_overbought: float = 70,
        rsi_oversold: float = 30
    ):
        """
        Initialize multi-timeframe analyzer.
        
        Args:
            symbol: Trading symbol
            ema_fast: Fast EMA period
            ema_slow: Slow EMA period
            rsi_period: RSI period
            rsi_overbought: RSI overbought level
            rsi_oversold: RSI oversold level
        """
        self.symbol = symbol
        self.ema_fast = ema_fast
        self.ema_slow = ema_slow
        self.rsi_period = rsi_period
        self.rsi_overbought = rsi_overbought
        self.rsi_oversold = rsi_oversold
        
        # Timeframes to analyze
        self.timeframes = ["4h", "1h", "15m"]
        
        # Cache for fetched data
        self._data_cache: Dict[str, pd.DataFrame] = {}
        self._cache_time: Dict[str, datetime] = {}
        
        logger.info(f"πŸ“Š MTF Analyzer initialized for {symbol} ({', '.join(self.timeframes)})")
    
    def _fetch_klines(self, timeframe: str, limit: int = 100) -> Optional[pd.DataFrame]:
        """Fetch klines from Binance for a specific timeframe."""
        # Check cache (valid for 1 minute)
        cache_key = f"{self.symbol}_{timeframe}"
        if cache_key in self._cache_time:
            if datetime.now() - self._cache_time[cache_key] < timedelta(minutes=1):
                return self._data_cache.get(cache_key)
        
        try:
            import os
            url = os.environ.get("BINANCE_FUTURES_URL", "https://data-api.binance.vision") + "/api/v3/klines"
            params = {
                "symbol": self.symbol,
                "interval": timeframe,
                "limit": limit
            }
            response = requests.get(url, params=params, timeout=10)
            response.raise_for_status()
            data = response.json()
            
            df = pd.DataFrame(data, columns=[
                'timestamp', 'open', 'high', 'low', 'close', 'volume',
                'close_time', 'quote_volume', 'trades', 'taker_buy_base',
                'taker_buy_quote', 'ignore'
            ])
            
            df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
            for col in ['open', 'high', 'low', 'close', 'volume']:
                df[col] = df[col].astype(float)
            
            # Cache the result
            self._data_cache[cache_key] = df
            self._cache_time[cache_key] = datetime.now()
            
            return df
            
        except Exception as e:
            logger.error(f"Failed to fetch {timeframe} klines: {e}")
            return None
    
    def _calculate_ema(self, series: pd.Series, period: int) -> pd.Series:
        """Calculate Exponential Moving Average."""
        return series.ewm(span=period, adjust=False).mean()
    
    def _calculate_rsi(self, series: pd.Series, period: int = 14) -> pd.Series:
        """Calculate Relative Strength Index."""
        delta = series.diff()
        gain = (delta.where(delta > 0, 0)).rolling(window=period).mean()
        loss = (-delta.where(delta < 0, 0)).rolling(window=period).mean()
        rs = gain / (loss + 1e-10)
        return 100 - (100 / (1 + rs))
    
    def analyze_timeframe(self, timeframe: str, df: pd.DataFrame = None) -> Optional[TimeframeSignal]:
        """
        Analyze a single timeframe.
        
        Args:
            timeframe: Timeframe to analyze (e.g., "4h", "1h", "15m")
            df: Optional pre-fetched dataframe
            
        Returns:
            TimeframeSignal with direction and strength
        """
        if df is None:
            df = self._fetch_klines(timeframe)
        
        if df is None or len(df) < self.ema_slow + 5:
            return None
        
        close = df['close']
        current_price = close.iloc[-1]
        
        # Calculate indicators
        ema_fast = self._calculate_ema(close, self.ema_fast)
        ema_slow = self._calculate_ema(close, self.ema_slow)
        rsi = self._calculate_rsi(close, self.rsi_period)
        
        current_ema_fast = ema_fast.iloc[-1]
        current_ema_slow = ema_slow.iloc[-1]
        current_rsi = rsi.iloc[-1]
        
        # Determine direction
        direction = TrendDirection.NEUTRAL
        strength = 0.0
        
        # EMA crossover direction
        ema_diff_pct = (current_ema_fast - current_ema_slow) / current_ema_slow * 100
        
        if ema_diff_pct > 0.1:  # Fast EMA above slow = bullish
            direction = TrendDirection.BULLISH
            strength = min(abs(ema_diff_pct) / 2, 1.0)  # Cap at 1.0
        elif ema_diff_pct < -0.1:  # Fast EMA below slow = bearish
            direction = TrendDirection.BEARISH
            strength = min(abs(ema_diff_pct) / 2, 1.0)
        
        # RSI confirmation
        if current_rsi > self.rsi_overbought:
            if direction == TrendDirection.BEARISH:
                strength = min(strength * 1.3, 1.0)  # Boost bearish
            elif direction == TrendDirection.BULLISH:
                strength *= 0.7  # Reduce bullish (overbought warning)
        elif current_rsi < self.rsi_oversold:
            if direction == TrendDirection.BULLISH:
                strength = min(strength * 1.3, 1.0)  # Boost bullish
            elif direction == TrendDirection.BEARISH:
                strength *= 0.7  # Reduce bearish (oversold warning)
        
        return TimeframeSignal(
            timeframe=timeframe,
            direction=direction,
            strength=strength,
            price=current_price,
            ema_fast=current_ema_fast,
            ema_slow=current_ema_slow,
            rsi=current_rsi
        )
    
    def get_confluence(self, primary_df: pd.DataFrame = None) -> ConfluenceResult:
        """
        Analyze all timeframes and check for confluence.
        
        Args:
            primary_df: Optional 1H dataframe if already fetched
            
        Returns:
            ConfluenceResult with alignment status and recommendation
        """
        signals: Dict[str, TimeframeSignal] = {}
        
        # Analyze each timeframe
        for tf in self.timeframes:
            df = primary_df if tf == "1h" else None
            signal = self.analyze_timeframe(tf, df)
            if signal:
                signals[tf] = signal
        
        # Check if we have all signals
        if len(signals) < len(self.timeframes):
            missing = set(self.timeframes) - set(signals.keys())
            return ConfluenceResult(
                aligned=False,
                direction=TrendDirection.NEUTRAL,
                strength=0,
                signals=signals,
                recommendation=f"Missing data for: {', '.join(missing)}"
            )
        
        # Count directions
        bullish_count = sum(1 for s in signals.values() if s.direction == TrendDirection.BULLISH)
        bearish_count = sum(1 for s in signals.values() if s.direction == TrendDirection.BEARISH)
        
        # All aligned?
        all_bullish = bullish_count == len(self.timeframes)
        all_bearish = bearish_count == len(self.timeframes)
        aligned = all_bullish or all_bearish
        
        # Determine overall direction
        if all_bullish:
            direction = TrendDirection.BULLISH
        elif all_bearish:
            direction = TrendDirection.BEARISH
        elif bullish_count > bearish_count:
            direction = TrendDirection.BULLISH
        elif bearish_count > bullish_count:
            direction = TrendDirection.BEARISH
        else:
            direction = TrendDirection.NEUTRAL
        
        # Calculate average strength
        avg_strength = np.mean([s.strength for s in signals.values()])
        
        # Generate recommendation
        if aligned:
            if direction == TrendDirection.BULLISH:
                recommendation = f"βœ… ALL BULLISH - Strong LONG setup (strength: {avg_strength:.0%})"
            else:
                recommendation = f"βœ… ALL BEARISH - Strong SHORT setup (strength: {avg_strength:.0%})"
        else:
            # Describe the conflict
            tf_dirs = [f"{tf}={s.direction.value}" for tf, s in signals.items()]
            recommendation = f"⚠️ CONFLICTING: {', '.join(tf_dirs)}"
        
        result = ConfluenceResult(
            aligned=aligned,
            direction=direction,
            strength=avg_strength,
            signals=signals,
            recommendation=recommendation
        )
        
        # Log the analysis
        emoji = "βœ…" if aligned else "⚠️"
        logger.info(
            f"πŸ“Š MTF Analysis: {emoji} {direction.value.upper()} | "
            f"4H={signals['4h'].direction.value if '4h' in signals else '?'} "
            f"1H={signals['1h'].direction.value if '1h' in signals else '?'} "
            f"15m={signals['15m'].direction.value if '15m' in signals else '?'}"
        )
        
        return result
    
    def should_trade(self, trade_type: str, primary_df: pd.DataFrame = None) -> Tuple[bool, str]:
        """
        Check if trade is allowed based on MTF confluence.
        
        Args:
            trade_type: "long" or "short"
            primary_df: Optional 1H dataframe
            
        Returns:
            Tuple of (should_trade, reason)
        """
        result = self.get_confluence(primary_df)
        
        # If not aligned, don't trade
        if not result.aligned:
            return False, f"MTF not aligned: {result.recommendation}"
        
        # Check direction matches trade type
        if trade_type == "long" and result.direction == TrendDirection.BULLISH:
            return True, f"MTF confirms LONG: {result.recommendation}"
        elif trade_type == "short" and result.direction == TrendDirection.BEARISH:
            return True, f"MTF confirms SHORT: {result.recommendation}"
        else:
            return False, f"MTF opposes {trade_type.upper()}: Market is {result.direction.value}"
    
    def get_summary(self, primary_df: pd.DataFrame = None) -> Dict:
        """Get a summary dict for UI/logging."""
        result = self.get_confluence(primary_df)
        
        signal_summary = {}
        for tf, signal in result.signals.items():
            signal_summary[tf] = {
                'direction': signal.direction.value,
                'strength': signal.strength,
                'rsi': signal.rsi,
                'emoji': 'πŸ“ˆ' if signal.direction == TrendDirection.BULLISH else 
                         'πŸ“‰' if signal.direction == TrendDirection.BEARISH else '➑️'
            }
        
        return {
            'aligned': result.aligned,
            'direction': result.direction.value,
            'strength': result.strength,
            'signals': signal_summary,
            'recommendation': result.recommendation
        }