Team Ai
Apppublic

diegobeyl/backtesting

sourceHugging Faceupdated 9mo agoView on Hugging Face
2likes
mt5_data_provider.py242 linesDownload Raw Back to core
1"""
2MT5 Data Provider
3Fetches historical OHLCV data from MetaTrader 5
4"""
5
6import pandas as pd
7from datetime import datetime, timedelta
8from typing import Optional, List, Dict
9import logging
10
11logger = logging.getLogger(__name__)
12
13# Try to import MetaTrader5 (only available on Windows)
14try:
15    import MetaTrader5 as mt5
16    MT5_AVAILABLE = True
17except ImportError:
18    MT5_AVAILABLE = False
19    logger.warning("MetaTrader5 not available (only works on Windows)")
20    # Create mock constants for other platforms
21    class MT5Mock:
22        TIMEFRAME_M1 = 1
23        TIMEFRAME_M5 = 5
24        TIMEFRAME_M15 = 15
25        TIMEFRAME_M30 = 30
26        TIMEFRAME_H1 = 60
27        TIMEFRAME_H4 = 240
28        TIMEFRAME_D1 = 1440
29        TIMEFRAME_W1 = 10080
30        TIMEFRAME_MN1 = 43200
31    mt5 = MT5Mock()
32
33# Timeframe mapping
34TIMEFRAME_MAP = {
35    "M1": mt5.TIMEFRAME_M1,
36    "M5": mt5.TIMEFRAME_M5,
37    "M15": mt5.TIMEFRAME_M15,
38    "M30": mt5.TIMEFRAME_M30,
39    "H1": mt5.TIMEFRAME_H1,
40    "H4": mt5.TIMEFRAME_H4,
41    "D1": mt5.TIMEFRAME_D1,
42    "W1": mt5.TIMEFRAME_W1,
43    "MN": mt5.TIMEFRAME_MN1,
44    "MN1": mt5.TIMEFRAME_MN1,
45}
46
47# Timeframe to seconds mapping
48TIMEFRAME_SECONDS = {
49    "M1": 60,
50    "M5": 300,
51    "M15": 900,
52    "M30": 1800,
53    "H1": 3600,
54    "H4": 14400,
55    "D1": 86400,
56    "W1": 604800,
57    "MN": 2592000,
58    "MN1": 2592000,
59}
60
61
62class MT5DataProvider:
63    """Provides historical data from MetaTrader 5"""
64    
65    def __init__(self):
66        self._connected = False
67        self._cache: Dict[str, pd.DataFrame] = {}
68        
69    def connect(self) -> bool:
70        """Initialize connection to MT5"""
71        if not MT5_AVAILABLE:
72            logger.warning("MT5 not available on this platform")
73            return False
74            
75        if self._connected:
76            return True
77            
78        try:
79            if not mt5.initialize():
80                error = mt5.last_error()
81                logger.error(f"MT5 initialization failed: {error}")
82                return False
83            
84            account_info = mt5.account_info()
85            if account_info is None:
86                logger.error("Failed to get account info")
87                return False
88                
89            logger.info(f"Connected to MT5 - Account: {account_info.login}")
90            self._connected = True
91            return True
92            
93        except Exception as e:
94            logger.error(f"Error connecting to MT5: {e}")
95            return False
96    
97    def disconnect(self):
98        """Close MT5 connection"""
99        if self._connected:
100            mt5.shutdown()
101            self._connected = False
102            logger.info("Disconnected from MT5")
103    
104    def get_symbols(self) -> List[str]:
105        """Get list of available symbols"""
106        if not self.connect():
107            return []
108            
109        try:
110            symbols = mt5.symbols_get()
111            if symbols is None:
112                return []
113            return [s.name for s in symbols]
114        except Exception as e:
115            logger.error(f"Error getting symbols: {e}")
116            return []
117    
118    def get_symbol_info(self, symbol: str) -> Optional[dict]:
119        """Get symbol information"""
120        if not self.connect():
121            return None
122            
123        try:
124            info = mt5.symbol_info(symbol)
125            if info is None:
126                return None
127            return {
128                "name": info.name,
129                "description": info.description,
130                "digits": info.digits,
131                "point": info.point,
132                "spread": info.spread,
133                "volume_min": info.volume_min,
134                "volume_max": info.volume_max,
135                "volume_step": info.volume_step,
136            }
137        except Exception as e:
138            logger.error(f"Error getting symbol info: {e}")
139            return None
140    
141    def get_data(
142        self,
143        symbol: str,
144        timeframe: str,
145        bars: int = 500,
146        start_date: Optional[datetime] = None,
147        end_date: Optional[datetime] = None,
148        use_cache: bool = True
149    ) -> Optional[pd.DataFrame]:
150        """
151        Fetch OHLCV data from MT5
152        
153        Args:
154            symbol: Trading symbol (e.g., BTCUSD, EURUSD)
155            timeframe: Timeframe string (D1, W1, MN, etc.)
156            bars: Number of bars to fetch (if no date range)
157            start_date: Start date for data
158            end_date: End date for data
159            use_cache: Whether to use cached data
160            
161        Returns:
162            DataFrame with columns: Open, High, Low, Close, Volume
163        """
164        if not self.connect():
165            return None
166        
167        # Check cache
168        cache_key = f"{symbol}_{timeframe}_{bars}"
169        if use_cache and cache_key in self._cache:
170            logger.debug(f"Using cached data for {cache_key}")
171            return self._cache[cache_key].copy()
172        
173        try:
174            # Select symbol
175            if not mt5.symbol_select(symbol, True):
176                logger.error(f"Failed to select symbol {symbol}")
177                return None
178            
179            # Get MT5 timeframe constant
180            mt5_tf = TIMEFRAME_MAP.get(timeframe.upper())
181            if mt5_tf is None:
182                logger.error(f"Unknown timeframe: {timeframe}")
183                return None
184            
185            # Fetch data
186            if start_date and end_date:
187                rates = mt5.copy_rates_range(symbol, mt5_tf, start_date, end_date)
188            else:
189                rates = mt5.copy_rates_from_pos(symbol, mt5_tf, 0, bars)
190            
191            if rates is None or len(rates) == 0:
192                error = mt5.last_error()
193                logger.error(f"Failed to get data for {symbol}: {error}")
194                return None
195            
196            # Convert to DataFrame
197            df = pd.DataFrame(rates)
198            df['time'] = pd.to_datetime(df['time'], unit='s')
199            df.set_index('time', inplace=True)
200            
201            # Rename columns for backtesting.py compatibility
202            df.rename(columns={
203                'open': 'Open',
204                'high': 'High',
205                'low': 'Low',
206                'close': 'Close',
207                'tick_volume': 'Volume'
208            }, inplace=True)
209            
210            # Keep only required columns
211            df = df[['Open', 'High', 'Low', 'Close', 'Volume']]
212            
213            # Cache the data
214            self._cache[cache_key] = df.copy()
215            
216            logger.info(f"Fetched {len(df)} bars for {symbol} {timeframe}")
217            return df
218            
219        except Exception as e:
220            logger.error(f"Error fetching data: {e}")
221            return None
222    
223    def clear_cache(self):
224        """Clear data cache"""
225        self._cache.clear()
226        logger.info("Cache cleared")
227    
228    def get_timeframe_seconds(self, timeframe: str) -> int:
229        """Get timeframe duration in seconds"""
230        return TIMEFRAME_SECONDS.get(timeframe.upper(), 86400)
231
232
233# Singleton instance
234_provider = None
235
236def get_data_provider() -> MT5DataProvider:
237    """Get singleton data provider instance"""
238    global _provider
239    if _provider is None:
240        _provider = MT5DataProvider()
241    return _provider
242