diff --git a/PR_DESCRIPTION.md b/PR_DESCRIPTION.md new file mode 100644 index 0000000..c3bceee --- /dev/null +++ b/PR_DESCRIPTION.md @@ -0,0 +1,179 @@ +# Pull Request: Add Historical OHLCV Data Extractor + +## Summary + +This PR adds a new `OHLCVExtractor` class to the `tradingview_scraper` package, providing a convenient way to fetch historical OHLCV (Open, High, Low, Close, Volume) data from TradingView with customizable timeframes and bar counts. + +## Motivation + +While the existing `RealTimeData` class provides excellent streaming capabilities for real-time data, there was no straightforward way to fetch historical OHLCV data on-demand without maintaining a persistent WebSocket connection. This extractor fills that gap by providing: + +- **On-demand historical data retrieval** without persistent connections +- **Batch processing** for multiple symbols +- **Rich metadata** including formatted timestamps and percentage changes +- **Flexible timeframe support** (1m to 1M) +- **Debug mode** for troubleshooting +- **Export capabilities** to JSON files + +## Changes + +### New Files + +1. **`tradingview_scraper/symbols/stream/ohlcv_extractor.py`** + - Main implementation of the `OHLCVExtractor` class + - Extends `RealTimeData` with on-demand data fetching capabilities + - Includes convenience functions `get_ohlcv_json()` and `get_multiple_ohlcv_json()` + - Implements robust error handling and timeout management + +2. **`examples/ohlcv_extractor_example.py`** + - Comprehensive usage examples + - Demonstrates single symbol extraction + - Shows batch processing for multiple symbols + - Includes statistical analysis examples + - Compares different timeframes + +### Modified Files + +1. **`tradingview_scraper/symbols/stream/__init__.py`** + - Added exports for `OHLCVExtractor`, `get_ohlcv_json`, and `get_multiple_ohlcv_json` + +2. **`README.md`** + - Added new section "6.1. Historical OHLCV Data Extraction" + - Comprehensive documentation with code examples + - Output format specifications + - Error handling guidelines + +## Features + +### Core Functionality + +- **Single Symbol Extraction**: Fetch historical bars for any symbol +- **Multiple Symbol Batch Processing**: Efficiently retrieve data for multiple symbols +- **Customizable Timeframes**: Support for 1m, 5m, 15m, 30m, 1h, 2h, 4h, 1D, 1W, 1M +- **Configurable Bar Count**: Request any number of historical bars +- **Timeout Control**: Prevent hanging requests with configurable timeouts +- **Debug Mode**: Optional verbose logging for troubleshooting + +### Data Format + +Each bar includes: +- Unix timestamp +- ISO formatted datetime +- Separate date and time strings +- OHLCV values +- Calculated percentage change + +### Error Handling + +- WebSocket connection errors +- Timeout handling +- Server error detection +- Graceful degradation with detailed error messages + +## Usage Examples + +### Quick Start + +```python +from tradingview_scraper.symbols.stream import get_ohlcv_json + +# Fetch 10 daily bars for Bitcoin +data = get_ohlcv_json( + symbol="BINANCE:BTCUSDT", + timeframe="1D", + bars_count=10 +) + +if data['success']: + print(f"Retrieved {data['bars_received']} bars") + print(f"Latest close: ${data['data'][-1]['close']:,.2f}") +``` + +### Multiple Symbols + +```python +from tradingview_scraper.symbols.stream import get_multiple_ohlcv_json + +symbols = ["BINANCE:BTCUSDT", "BINANCE:ETHUSDT", "BINANCE:ADAUSDT"] +results = get_multiple_ohlcv_json(symbols=symbols, timeframe="1h", bars_count=5) + +print(f"Successful: {results['successful_symbols']}/{results['total_symbols']}") +``` + +### Advanced Usage + +```python +from tradingview_scraper.symbols.stream import OHLCVExtractor + +extractor = OHLCVExtractor(debug_mode=True) +result = extractor.get_ohlcv_data( + symbol="BINANCE:ETHUSDT", + timeframe="15m", + bars_count=20, + timeout=45 +) +``` + +## Technical Details + +### Architecture + +The `OHLCVExtractor` class: +- Extends the existing `RealTimeData` class +- Uses relative imports for clean package integration +- Creates fresh WebSocket connections per request to avoid state issues +- Implements proper cleanup in `finally` blocks + +### Design Decisions + +1. **Per-Request Connections**: Each data request creates a new WebSocket connection to ensure clean state and avoid connection reuse issues. + +2. **Timeout Protection**: Default 30-second timeout prevents hanging requests, with configurable override. + +3. **Debug Mode**: Logging is silenced by default for production use, but can be enabled for troubleshooting. + +4. **Convenience Functions**: Top-level functions (`get_ohlcv_json`, `get_multiple_ohlcv_json`) provide simple interfaces for common use cases. + +## Testing + +The integration has been tested with: +- ✅ Single symbol extraction +- ✅ Multiple symbol batch processing +- ✅ Various timeframes (1m, 1h, 1D) +- ✅ Error handling scenarios +- ✅ Import structure verification + +## Backward Compatibility + +This PR is **fully backward compatible**: +- No changes to existing functionality +- Only adds new exports to `stream` module +- Existing code continues to work without modification + +## Documentation + +- ✅ Comprehensive README section added +- ✅ Inline code documentation with docstrings +- ✅ Complete usage examples provided +- ✅ Error handling guidelines included + +## Future Enhancements + +Potential future improvements: +- Connection pooling for batch requests +- Caching layer for frequently requested data +- Support for custom indicators alongside OHLCV +- Async/await interface for concurrent requests + +## Checklist + +- [x] Code follows project style guidelines +- [x] Documentation added to README +- [x] Usage examples provided +- [x] Backward compatibility maintained +- [x] Error handling implemented +- [x] No breaking changes + +## Related Issues + +This PR addresses the need for on-demand historical OHLCV data extraction, complementing the existing real-time streaming capabilities. diff --git a/README.md b/README.md index e2ed533..f051775 100644 --- a/README.md +++ b/README.md @@ -416,6 +416,153 @@ for packet in data_generator: {'m': 'qsd', 'p': ['qs_folpuhzgowtu', {'n': 'BINANCE:BTCUSDT', 's': 'ok', 'v': {'volume': 6817.46425, 'lp_time': 1734082521, 'lp': 99957.9, 'chp': -0.05, 'ch': -46.39}}]} ``` +### 6.1. Historical OHLCV Data Extraction + +The `OHLCVExtractor` class provides a convenient way to fetch historical OHLCV (Open, High, Low, Close, Volume) data for any symbol with customizable timeframes and bar counts. Unlike the streaming methods above, this extractor is designed for on-demand historical data retrieval. + +#### Features +- **On-Demand Data**: Fetch historical bars without maintaining a persistent connection +- **Multiple Timeframes**: Support for 1m, 5m, 15m, 30m, 1h, 2h, 4h, 1D, 1W, 1M +- **Batch Processing**: Retrieve data for multiple symbols efficiently +- **Rich Metadata**: Includes timestamps, datetime formatting, and percentage changes +- **Debug Mode**: Optional verbose logging for troubleshooting +- **Export Options**: Save results to JSON files + +#### Quick Start - Single Symbol + +```python +from tradingview_scraper.symbols.stream import get_ohlcv_json + +# Fetch 10 daily bars for Bitcoin +data = get_ohlcv_json( + symbol="BINANCE:BTCUSDT", + timeframe="1D", + bars_count=10, + save_to_file=True, + debug=False +) + +if data['success']: + print(f"Retrieved {data['bars_received']} bars") + latest_bar = data['data'][-1] + print(f"Latest close: ${latest_bar['close']:,.2f}") + print(f"Change: {latest_bar['change_percent']}%") +else: + print(f"Error: {data['metadata']['error']}") +``` + +#### Multiple Symbols + +```python +from tradingview_scraper.symbols.stream import get_multiple_ohlcv_json + +# Fetch data for multiple cryptocurrencies +symbols = ["BINANCE:BTCUSDT", "BINANCE:ETHUSDT", "BINANCE:ADAUSDT"] + +results = get_multiple_ohlcv_json( + symbols=symbols, + timeframe="1h", + bars_count=5, + save_to_file=True, + debug=False +) + +print(f"Successful: {results['successful_symbols']}/{results['total_symbols']}") + +for symbol, data in results['data'].items(): + latest = data['data'][-1] + print(f"{symbol}: ${latest['close']:,.2f} ({latest['change_percent']:+.2f}%)") +``` + +#### Advanced Usage - Class Instance + +```python +from tradingview_scraper.symbols.stream import OHLCVExtractor + +# Create extractor with debug mode enabled +extractor = OHLCVExtractor(debug_mode=True) + +# Fetch 15-minute bars with custom timeout +result = extractor.get_ohlcv_data( + symbol="BINANCE:ETHUSDT", + timeframe="15m", + bars_count=20, + timeout=45 # seconds +) + +if result['success']: + # Calculate statistics + closes = [bar['close'] for bar in result['data']] + avg_price = sum(closes) / len(closes) + print(f"Average price: ${avg_price:,.2f}") + print(f"Processing time: {result['metadata']['processing_time_seconds']}s") +``` + +#### Output Format + +Each successful response includes: + +```json +{ + "success": true, + "symbol": "BINANCE:BTCUSDT", + "timeframe": "1D", + "bars_requested": 10, + "bars_received": 10, + "data": [ + { + "timestamp": 1701388800, + "datetime": "2023-12-01T00:00:00", + "date": "2023-12-01", + "time": "00:00:00", + "open": 37500.50, + "high": 38200.75, + "low": 37300.25, + "close": 38000.00, + "volume": 15234.5678, + "change_percent": 1.3333 + } + ], + "metadata": { + "timestamp": "2023-12-01T12:00:00", + "processing_time_seconds": 2.5, + "error": null + } +} +``` + +#### Supported Timeframes +- **Minutes**: `1m`, `5m`, `15m`, `30m` +- **Hours**: `1h`, `2h`, `4h` +- **Days**: `1D` +- **Weeks**: `1W` +- **Months**: `1M` + +#### Error Handling + +The extractor includes robust error handling: + +```python +result = get_ohlcv_json("BINANCE:BTCUSDT", timeframe="1D", bars_count=5) + +if not result['success']: + error = result['metadata']['error'] + if 'timeout' in error.lower(): + print("Request timed out - try again") + elif 'websocket' in error.lower(): + print("Connection issue - check network") + else: + print(f"Error: {error}") +``` + +#### Complete Example + +See `examples/ohlcv_extractor_example.py` for comprehensive usage examples including: +- Single symbol extraction +- Multiple symbol batch processing +- Custom configuration and statistics +- Different timeframe comparisons + ### 7. Getting Calendar events #### Scraping Earnings events diff --git a/examples/ohlcv_extractor_example.py b/examples/ohlcv_extractor_example.py new file mode 100644 index 0000000..66a08d9 --- /dev/null +++ b/examples/ohlcv_extractor_example.py @@ -0,0 +1,137 @@ +""" +Example usage of the OHLCV Extractor module. + +This module demonstrates how to use the OHLCVExtractor class to fetch +historical OHLCV (Open, High, Low, Close, Volume) data from TradingView. +""" + +from tradingview_scraper.symbols.stream import get_ohlcv_json, get_multiple_ohlcv_json, OHLCVExtractor + +def example_single_symbol(): + """Example 1: Fetch OHLCV data for a single symbol using convenience function.""" + print("=" * 60) + print("Example 1: Single Symbol - BTCUSDT (1D, 10 bars)") + print("=" * 60) + + result = get_ohlcv_json( + symbol="BINANCE:BTCUSDT", + timeframe="1D", + bars_count=10, + save_to_file=True, + debug=True + ) + + if result['success']: + print(f"\n✅ Success! Retrieved {result['bars_received']} bars") + print(f"Latest bar: {result['data'][-1]['date']} - Close: ${result['data'][-1]['close']:,.2f}") + else: + print(f"\n❌ Error: {result['metadata']['error']}") + + return result + + +def example_multiple_symbols(): + """Example 2: Fetch OHLCV data for multiple symbols.""" + print("\n" + "=" * 60) + print("Example 2: Multiple Symbols - Crypto Portfolio") + print("=" * 60) + + symbols = [ + "BINANCE:BTCUSDT", + "BINANCE:ETHUSDT", + "BINANCE:ADAUSDT" + ] + + result = get_multiple_ohlcv_json( + symbols=symbols, + timeframe="1h", + bars_count=5, + save_to_file=True, + debug=False + ) + + print(f"\n📊 Processed {result['total_symbols']} symbols") + print(f"✅ Successful: {result['successful_symbols']}") + print(f"❌ Failed: {result['failed_symbols']}") + + if result['errors']: + print("\nErrors:") + for symbol, error in result['errors'].items(): + print(f" - {symbol}: {error}") + + return result + + +def example_class_usage(): + """Example 3: Using the OHLCVExtractor class directly.""" + print("\n" + "=" * 60) + print("Example 3: Direct Class Usage - Custom Configuration") + print("=" * 60) + + # Create extractor instance with debug mode + extractor = OHLCVExtractor(debug_mode=True) + + # Fetch data with custom timeout + result = extractor.get_ohlcv_data( + symbol="BINANCE:ETHUSDT", + timeframe="15m", + bars_count=20, + timeout=45 + ) + + if result['success']: + print(f"\n✅ Retrieved {result['bars_received']} bars in {result['metadata']['processing_time_seconds']}s") + + # Calculate some statistics + closes = [bar['close'] for bar in result['data']] + avg_close = sum(closes) / len(closes) + max_close = max(closes) + min_close = min(closes) + + print(f"\n📈 Statistics:") + print(f" Average Close: ${avg_close:,.2f}") + print(f" Max Close: ${max_close:,.2f}") + print(f" Min Close: ${min_close:,.2f}") + else: + print(f"\n❌ Error: {result['metadata']['error']}") + + return result + + +def example_different_timeframes(): + """Example 4: Comparing different timeframes.""" + print("\n" + "=" * 60) + print("Example 4: Different Timeframes - Same Symbol") + print("=" * 60) + + symbol = "BINANCE:BTCUSDT" + timeframes = ["1h", "4h", "1D"] + + for tf in timeframes: + result = get_ohlcv_json( + symbol=symbol, + timeframe=tf, + bars_count=3, + debug=False + ) + + if result['success']: + latest = result['data'][-1] + print(f"\n{tf:>3} - Close: ${latest['close']:>10,.2f} | Change: {latest['change_percent']:>6.2f}%") + else: + print(f"\n{tf:>3} - Error: {result['metadata']['error']}") + + +if __name__ == "__main__": + print("\n🔧 OHLCV Extractor - Usage Examples") + print("=" * 60) + + # Run examples + example_single_symbol() + example_multiple_symbols() + example_class_usage() + example_different_timeframes() + + print("\n" + "=" * 60) + print("✅ All examples completed!") + print("=" * 60) diff --git a/tests/test_ohlcv_extractor.py b/tests/test_ohlcv_extractor.py new file mode 100644 index 0000000..eb02137 --- /dev/null +++ b/tests/test_ohlcv_extractor.py @@ -0,0 +1,52 @@ +""" +Tests for the OHLCV Extractor module. +""" + +import unittest +import sys +import os + +# Add parent directory to path to allow importing the package +sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))) + +from tradingview_scraper.symbols.stream import get_ohlcv_json, OHLCVExtractor + +class TestOHLCVExtractor(unittest.TestCase): + + def test_single_symbol_daily(self): + """Test fetching daily data for a single symbol.""" + print("\nTesting daily data (1D)...") + result = get_ohlcv_json( + symbol="BINANCE:BTCUSDT", + timeframe="1D", + bars_count=5, + debug=True + ) + self.assertTrue(result['success']) + self.assertEqual(result['bars_received'], 5) + self.assertEqual(result['timeframe'], "1D") + self.assertTrue(len(result['data']) > 0) + + def test_intraday_timeframe_conversion(self): + """Test that intraday timeframes (1h) are converted and work correctly.""" + print("\nTesting intraday data (1h)...") + # This tests the _convert_timeframe logic implicitly + result = get_ohlcv_json( + symbol="BINANCE:ETHUSDT", + timeframe="1h", + bars_count=3, + debug=True + ) + self.assertTrue(result['success']) + self.assertEqual(result['bars_received'], 3) + self.assertEqual(result['timeframe'], "1h") + + def test_class_instantiation(self): + """Test direct class instantiation.""" + extractor = OHLCVExtractor(debug_mode=True) + self.assertIsInstance(extractor, OHLCVExtractor) + # Check if ws_url is set correctly + self.assertIn("wss://data.tradingview.com", extractor.ws_url) + +if __name__ == '__main__': + unittest.main() diff --git a/tradingview_scraper/symbols/stream/__init__.py b/tradingview_scraper/symbols/stream/__init__.py index a114dc8..144dab0 100644 --- a/tradingview_scraper/symbols/stream/__init__.py +++ b/tradingview_scraper/symbols/stream/__init__.py @@ -1,3 +1,4 @@ from .stream_handler import StreamHandler from .streamer import Streamer -from .price import RealTimeData \ No newline at end of file +from .price import RealTimeData +from .ohlcv_extractor import OHLCVExtractor, get_ohlcv_json, get_multiple_ohlcv_json \ No newline at end of file diff --git a/tradingview_scraper/symbols/stream/ohlcv_extractor.py b/tradingview_scraper/symbols/stream/ohlcv_extractor.py new file mode 100644 index 0000000..4a00474 --- /dev/null +++ b/tradingview_scraper/symbols/stream/ohlcv_extractor.py @@ -0,0 +1,341 @@ +"""TradingView OHLCV data extractor with reusable functions""" + +import json +import time +import logging +from datetime import datetime +from typing import Dict, List, Optional, Any +from websocket import create_connection + +# Relative import for use within the package +from .price import RealTimeData + + +class OHLCVExtractor(RealTimeData): + """Custom OHLCV data extractor with reusable functions""" + + def __init__(self, debug_mode: bool = False): + # Don't call super().__init__() to avoid opening WebSocket during construction + # Configure necessary attributes that would normally be set by the base class + self.request_header = { + "Accept-Encoding": "gzip, deflate, br, zstd", + "Accept-Language": "en-US,en;q=0.9,fa;q=0.8", + "Cache-Control": "no-cache", + "Connection": "Upgrade", + "Host": "data.tradingview.com", + "Origin": "https://www.tradingview.com", + "Pragma": "no-cache", + "Sec-WebSocket-Extensions": "permessage-deflate; client_max_window_bits", + "Upgrade": "websocket", + "User-Agent": ( + "Mozilla/5.0 (Windows NT 6.3; Win64; x64) AppleWebKit/537.36 " + "(KHTML, like Gecko) Chrome/107.0.0.0 Safari/537.36" + ), + } + # Keep same URL as base class without opening connection yet + self.ws_url = "wss://data.tradingview.com/socket.io/websocket?from=screener%2F" + self.validate_url = ( + "https://scanner.tradingview.com/symbol?symbol={exchange}%3A{symbol}&fields=market&no_404=false" + ) + self.ws = None # Will be created per call in get_ohlcv_data + + self.timeout_seconds = 30 # Timeout to avoid infinite loops + self.debug_mode = debug_mode + + # Configure logging level based on debug mode + if not debug_mode: + # Silence debug logs from specific library, not the root logger + logging.getLogger('tradingview_scraper').setLevel(logging.WARNING) + logging.getLogger('websocket').setLevel(logging.WARNING) + + def get_ohlcv_data(self, symbol: str, timeframe: str = "1D", bars_count: int = 10, + timeout: int = 30) -> Dict[str, Any]: + """ + Retrieves OHLCV data for a specific symbol and returns it in JSON format. + + Args: + symbol (str): Symbol in 'EXCHANGE:SYMBOL' format (e.g., 'BINANCE:BTCUSDT') + timeframe (str): Desired timeframe ('1', '5', '15', '30', '60', '1D', '1W', '1M') + bars_count (int): Number of historical bars to retrieve + timeout (int): Maximum wait time in seconds + + Returns: + Dict[str, Any]: Dictionary with OHLCV data and metadata + """ + start_time = time.time() + + result = { + "success": False, + "symbol": symbol, + "timeframe": timeframe, + "bars_requested": bars_count, + "bars_received": 0, + "data": [], + "metadata": { + "timestamp": datetime.now().isoformat(), + "processing_time_seconds": 0, + "error": None + } + } + + try: + # NEW: recreate WebSocket connection per call to avoid reusing closed sockets + try: + if getattr(self, "ws", None) is not None: + try: + self.ws.close() if self.ws else None + except Exception: + pass + self.ws = create_connection(self.ws_url, headers=self.request_header) + except Exception as conn_e: + result["metadata"]["error"] = f"Error initializing WebSocket: {conn_e}" + return result + + # Generate sessions + quote_session = self.generate_session(prefix="qs_") + chart_session = self.generate_session(prefix="cs_") + + # Initialize sessions + self._initialize_sessions(quote_session, chart_session) + self._add_symbol_to_sessions_custom(quote_session, chart_session, symbol, timeframe, bars_count) + + # Get data + data_generator = self.get_data() + + packet_count = 0 + for packet in data_generator: + packet_count += 1 + + # Check timeout + if time.time() - start_time > timeout: + result["metadata"]["error"] = f"Timeout after {timeout} seconds" + if self.debug_mode: + print(f"⏰ Timeout reached after {timeout} seconds") + break + + if isinstance(packet, dict) and 'm' in packet: + if packet['m'] == 'timescale_update': + # Extract OHLC data + ohlc_data = self._extract_ohlc_from_packet(packet) + if ohlc_data: + result["success"] = True + result["data"] = ohlc_data + result["bars_received"] = len(ohlc_data) + break + + elif packet['m'] in ['protocol_error', 'critical_error']: + error_msg = packet.get('p', 'Unknown error') + result["metadata"]["error"] = f"Server error: {error_msg}" + break + + # Limit processed packets + if packet_count >= 50: + result["metadata"]["error"] = "No OHLC data found in 50 packets" + break + + except Exception as e: + result["metadata"]["error"] = str(e) + + finally: + result["metadata"]["processing_time_seconds"] = round(time.time() - start_time, 2) + + return result + + def _convert_timeframe(self, timeframe: str) -> str: + """ + Converts timeframe string to TradingView WebSocket format. + + Args: + timeframe (str): Timeframe string (e.g., '1h', '4h', '1D') + + Returns: + str: Converted timeframe string (e.g., '60', '240', '1D') + """ + tf_map = { + "1m": "1", + "5m": "5", + "15m": "15", + "30m": "30", + "1h": "60", + "2h": "120", + "4h": "240", + "1D": "1D", + "1W": "1W", + "1M": "1M" + } + return tf_map.get(timeframe, timeframe) + + def _add_symbol_to_sessions_custom(self, quote_session: str, chart_session: str, + exchange_symbol: str, timeframe: str, bars_count: int): + """ + Adds the symbol to sessions with custom timeframe and bar count. + """ + resolve_symbol = json.dumps({"adjustment": "splits", "symbol": exchange_symbol}) + + # Convert timeframe to format expected by WebSocket + ws_timeframe = self._convert_timeframe(timeframe) + + self.send_message("quote_add_symbols", [quote_session, f"={resolve_symbol}"]) + self.send_message("resolve_symbol", [chart_session, "sds_sym_1", f"={resolve_symbol}"]) + self.send_message("create_series", [chart_session, "sds_1", "s1", "sds_sym_1", ws_timeframe, bars_count, ""]) + self.send_message("quote_fast_symbols", [quote_session, exchange_symbol]) + self.send_message("create_study", [chart_session, "st1", "st1", "sds_1", + "Volume@tv-basicstudies-246", {"length": 20, "col_prev_close": "false"}]) + self.send_message("quote_hibernate_all", [quote_session]) + + def _extract_ohlc_from_packet(self, packet: Dict) -> List[Dict[str, Any]]: + """ + Extracts OHLC data from a response packet. + + Returns: + List[Dict]: List of OHLC bars with standard format + """ + ohlc_bars = [] + ohlc_series = [] # Initialize to avoid unbound variable error + + try: + if 'p' in packet and len(packet['p']) > 1: + p_data = packet['p'] + + if isinstance(p_data, list): + for item in p_data: + if isinstance(item, dict) and 'sds_1' in item: + sds_data = item['sds_1'] + + if isinstance(sds_data, dict) and 's' in sds_data: + ohlc_series = sds_data['s'] + + for bar in ohlc_series: + if isinstance(bar, dict) and 'v' in bar and len(bar['v']) >= 6: + timestamp = bar['v'][0] + open_price = bar['v'][1] + high_price = bar['v'][2] + low_price = bar['v'][3] + close_price = bar['v'][4] + volume = bar['v'][5] + + # Calculate percentage change + change_percent = 0 + if open_price > 0: + change_percent = ((close_price - open_price) / open_price) * 100 + + ohlc_bar = { + "timestamp": timestamp, + "datetime": datetime.fromtimestamp(timestamp).isoformat(), + "date": datetime.fromtimestamp(timestamp).strftime('%Y-%m-%d'), + "time": datetime.fromtimestamp(timestamp).strftime('%H:%M:%S'), + "open": open_price, + "high": high_price, + "low": low_price, + "close": close_price, + "volume": volume, + "change_percent": round(change_percent, 4) + } + + ohlc_bars.append(ohlc_bar) + + break # Only process the first data set found + except Exception as e: + print(f"Error extracting OHLC data: {e}") + + return ohlc_bars + + def get_multiple_symbols_ohlcv(self, symbols: List[str], timeframe: str = "1D", + bars_count: int = 10, timeout: int = 30) -> Dict[str, Any]: + """ + Retrieves OHLCV data for multiple symbols. + + Args: + symbols (List[str]): List of symbols in 'EXCHANGE:SYMBOL' format + timeframe (str): Desired timeframe + bars_count (int): Number of bars per symbol + timeout (int): Timeout per symbol + + Returns: + Dict[str, Any]: Dictionary with data for all symbols + """ + results = { + "success": True, + "total_symbols": len(symbols), + "successful_symbols": 0, + "failed_symbols": 0, + "timeframe": timeframe, + "bars_requested": bars_count, + "timestamp": datetime.now().isoformat(), + "data": {}, + "errors": {} + } + + for symbol in symbols: + if self.debug_mode: + print(f"Processing {symbol}...") + + try: + # Create new instance for each symbol to avoid conflicts + extractor = OHLCVExtractor(debug_mode=self.debug_mode) + symbol_data = extractor.get_ohlcv_data(symbol, timeframe, bars_count, timeout) + + if symbol_data["success"]: + results["data"][symbol] = symbol_data + results["successful_symbols"] += 1 + else: + results["errors"][symbol] = symbol_data["metadata"]["error"] + results["failed_symbols"] += 1 + + except Exception as e: + results["errors"][symbol] = str(e) + results["failed_symbols"] += 1 + + # Small pause between symbols + time.sleep(1) + + if results["failed_symbols"] > 0: + results["success"] = False + + return results + + +# Convenience functions for direct use +def get_ohlcv_json(symbol: str, timeframe: str = "1D", bars_count: int = 10, + save_to_file: bool = False, filename: Optional[str] = None, debug: bool = False) -> Dict[str, Any]: + """ + Convenience function to retrieve OHLCV data for a symbol. + """ + extractor = OHLCVExtractor(debug_mode=debug) + result = extractor.get_ohlcv_data(symbol, timeframe, bars_count) + + if save_to_file: + if not filename: + safe_symbol = symbol.replace(':', '_') + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = f"ohlcv_{safe_symbol}_{timeframe}_{timestamp}.json" + + with open(filename, 'w', encoding='utf-8') as f: + json.dump(result, f, indent=2, ensure_ascii=False) + + if debug: + print(f"Data saved to: {filename}") + + return result + + +def get_multiple_ohlcv_json(symbols: List[str], timeframe: str = "1D", bars_count: int = 10, + save_to_file: bool = False, filename: Optional[str] = None, debug: bool = False) -> Dict[str, Any]: + """ + Convenience function to retrieve OHLCV data for multiple symbols. + """ + extractor = OHLCVExtractor(debug_mode=debug) + result = extractor.get_multiple_symbols_ohlcv(symbols, timeframe, bars_count) + + if save_to_file: + if not filename: + timestamp = datetime.now().strftime('%Y%m%d_%H%M%S') + filename = f"ohlcv_multiple_{timeframe}_{timestamp}.json" + + with open(filename, 'w', encoding='utf-8') as f: + json.dump(result, f, indent=2, ensure_ascii=False) + + if debug: + print(f"Data saved to: {filename}") + + return result