Add WebSocket support
This commit is contained in:
@@ -40,6 +40,7 @@
|
||||
- `Tickers`: multiple tickers' data
|
||||
- `download`: download market data for multiple tickers
|
||||
- `Market`: get information about a market
|
||||
- `WebSocket` and `AsyncWebSocket`: live streaming data
|
||||
- `Search`: quotes and news from search
|
||||
- `Sector` and `Industry`: sector and industry information
|
||||
- `EquityQuery` and `Screener`: build query to screen market
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
import asyncio
|
||||
import yfinance as yf
|
||||
|
||||
# define your message callback
|
||||
def message_handler(message):
|
||||
print("Received message:", message)
|
||||
|
||||
async def main():
|
||||
# =======================
|
||||
# With Context Manager
|
||||
# =======================
|
||||
async with yf.AsyncWebSocket() as ws:
|
||||
await ws.subscribe(["AAPL", "BTC-USD"])
|
||||
await ws.listen()
|
||||
|
||||
# =======================
|
||||
# Without Context Manager
|
||||
# =======================
|
||||
ws = yf.AsyncWebSocket()
|
||||
await ws.subscribe(["AAPL", "BTC-USD"])
|
||||
await ws.listen()
|
||||
|
||||
asyncio.run(main())
|
||||
@@ -0,0 +1,19 @@
|
||||
import yfinance as yf
|
||||
|
||||
# define your message callback
|
||||
def message_handler(message):
|
||||
print("Received message:", message)
|
||||
|
||||
# =======================
|
||||
# With Context Manager
|
||||
# =======================
|
||||
with yf.WebSocket() as ws:
|
||||
ws.subscribe(["AAPL", "BTC-USD"])
|
||||
ws.listen(message_handler)
|
||||
|
||||
# =======================
|
||||
# Without Context Manager
|
||||
# =======================
|
||||
ws = yf.WebSocket()
|
||||
ws.subscribe(["AAPL", "BTC-USD"])
|
||||
ws.listen(message_handler)
|
||||
@@ -20,3 +20,6 @@ dat.info
|
||||
|
||||
# analysis
|
||||
dat.analyst_price_targets
|
||||
|
||||
# websocket
|
||||
dat.live()
|
||||
|
||||
@@ -5,4 +5,7 @@ tickers = yf.Tickers('msft aapl goog')
|
||||
# access each ticker using (example)
|
||||
tickers.tickers['MSFT'].info
|
||||
tickers.tickers['AAPL'].history(period="1mo")
|
||||
tickers.tickers['GOOG'].actions
|
||||
tickers.tickers['GOOG'].actions
|
||||
|
||||
# websocket
|
||||
tickers.live()
|
||||
|
||||
@@ -19,6 +19,8 @@ The following are the publicly available classes, and functions exposed by the `
|
||||
- :attr:`download <yfinance.download>`: Function to download market data for multiple tickers.
|
||||
- :attr:`Search <yfinance.Search>`: Class for accessing search results.
|
||||
- :attr:`Lookup <yfinance.Lookup>`: Class for looking up tickers.
|
||||
- :class:`WebSocket <yfinance.WebSocket>`: Class for synchronously streaming live market data.
|
||||
- :class:`AsyncWebSocket <yfinance.AsyncWebSocket>`: Class for asynchronously streaming live market data.
|
||||
- :attr:`Sector <yfinance.Sector>`: Domain class for accessing sector information.
|
||||
- :attr:`Industry <yfinance.Industry>`: Domain class for accessing industry information.
|
||||
- :attr:`Market <yfinance.Market>`: Class for accessing market status & summary.
|
||||
@@ -41,6 +43,7 @@ The following are the publicly available classes, and functions exposed by the `
|
||||
yfinance.market
|
||||
yfinance.search
|
||||
yfinance.lookup
|
||||
yfinance.websocket
|
||||
yfinance.sector_industry
|
||||
yfinance.screener
|
||||
yfinance.functions
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
=====================
|
||||
WebSocket
|
||||
=====================
|
||||
|
||||
.. currentmodule:: yfinance
|
||||
|
||||
The `WebSocket` module allows you to stream live price data from Yahoo Finance using both synchronous and asynchronous clients.
|
||||
|
||||
Classes
|
||||
------------
|
||||
|
||||
.. autosummary::
|
||||
:toctree: api/
|
||||
|
||||
WebSocket
|
||||
AsyncWebSocket
|
||||
|
||||
Synchronous WebSocket
|
||||
----------------------
|
||||
|
||||
The `WebSocket` class provides a synchronous interface for subscribing to price updates.
|
||||
|
||||
Sample Code:
|
||||
|
||||
.. literalinclude:: examples/live_sync.py
|
||||
:language: python
|
||||
|
||||
Asynchronous WebSocket
|
||||
-----------------------
|
||||
|
||||
The `AsyncWebSocket` class provides an asynchronous interface for subscribing to price updates.
|
||||
|
||||
Sample Code:
|
||||
|
||||
.. literalinclude:: examples/live_async.py
|
||||
:language: python
|
||||
|
||||
.. note::
|
||||
If you're running asynchronous code in a Jupyter notebook, you may encounter issues with event loops. To resolve this, you need to import and apply `nest_asyncio` to allow nested event loops.
|
||||
|
||||
Add the following code before running asynchronous operations:
|
||||
|
||||
.. code-block:: python
|
||||
|
||||
import nest_asyncio
|
||||
nest_asyncio.apply()
|
||||
+3
-1
@@ -9,4 +9,6 @@ beautifulsoup4>=4.11.1
|
||||
peewee>=3.16.2
|
||||
requests_cache>=1.0
|
||||
requests_ratelimiter>=0.3.1
|
||||
scipy>=1.6.3
|
||||
scipy>=1.6.3
|
||||
protobuf>=5.29.0,<6
|
||||
websockets>=11.0
|
||||
@@ -63,11 +63,17 @@ setup(
|
||||
'requests>=2.31', 'multitasking>=0.0.7',
|
||||
'platformdirs>=2.0.0', 'pytz>=2022.5',
|
||||
'frozendict>=2.3.4', 'peewee>=3.16.2',
|
||||
'beautifulsoup4>=4.11.1'],
|
||||
'beautifulsoup4>=4.11.1',
|
||||
'protobuf>=5.29.0,<6', 'websockets>=11.0'],
|
||||
extras_require={
|
||||
'nospam': ['requests_cache>=1.0', 'requests_ratelimiter>=0.3.1'],
|
||||
'repair': ['scipy>=1.6.3'],
|
||||
},
|
||||
# Include protobuf files for websocket support
|
||||
package_data={
|
||||
'yfinance': ['pricing.proto', 'pricing_pb2.py'],
|
||||
},
|
||||
include_package_data=True,
|
||||
# Note: Pandas.read_html() needs html5lib & beautifulsoup4
|
||||
entry_points={
|
||||
'console_scripts': [
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
import unittest
|
||||
from unittest.mock import Mock
|
||||
|
||||
from yfinance.live import BaseWebSocket
|
||||
|
||||
|
||||
class TestWebSocket(unittest.TestCase):
|
||||
def test_decode_message_valid(self):
|
||||
message = ("CgdCVEMtVVNEFYoMuUcYwLCVgIplIgNVU0QqA0NDQzApOAFFPWrEP0iAgOrxvANVx/25R12csrRHZYD8skR9/"
|
||||
"7i0R7ABgIDq8bwD2AEE4AGAgOrxvAPoAYCA6vG8A/IBA0JUQ4ECAAAAwPrjckGJAgAA2P5ZT3tC")
|
||||
|
||||
ws = BaseWebSocket(Mock())
|
||||
decoded = ws._decode_message(message)
|
||||
|
||||
expected = {'id': 'BTC-USD', 'price': 94745.08, 'time': '1736509140000', 'currency': 'USD', 'exchange': 'CCC',
|
||||
'quote_type': 41, 'market_hours': 1, 'change_percent': 1.5344921, 'day_volume': '59712028672',
|
||||
'day_high': 95227.555, 'day_low': 92517.22, 'change': 1431.8906, 'open_price': 92529.99,
|
||||
'last_size': '59712028672', 'price_hint': '2', 'vol_24hr': '59712028672',
|
||||
'vol_all_currencies': '59712028672', 'from_currency': 'BTC', 'circulating_supply': 19808172.0,
|
||||
'market_cap': 1876726640000.0}
|
||||
|
||||
self.assertEqual(expected, decoded)
|
||||
|
||||
def test_decode_message_invalid(self):
|
||||
websocket = BaseWebSocket(Mock())
|
||||
base64_message = "invalid_base64_string"
|
||||
decoded = websocket._decode_message(base64_message)
|
||||
assert "error" in decoded
|
||||
assert "raw_base64" in decoded
|
||||
self.assertEqual(base64_message, decoded["raw_base64"])
|
||||
@@ -25,6 +25,7 @@ from .lookup import Lookup
|
||||
from .ticker import Ticker
|
||||
from .tickers import Tickers
|
||||
from .multi import download
|
||||
from .live import WebSocket, AsyncWebSocket
|
||||
from .utils import enable_debug_mode
|
||||
from .cache import set_tz_cache_location
|
||||
from .domain.sector import Sector
|
||||
@@ -41,7 +42,7 @@ __author__ = "Ran Aroussi"
|
||||
import warnings
|
||||
warnings.filterwarnings('default', category=DeprecationWarning, module='^yfinance')
|
||||
|
||||
__all__ = ['download', 'Market', 'Search', 'Lookup', 'Ticker', 'Tickers', 'enable_debug_mode', 'set_tz_cache_location', 'Sector', 'Industry']
|
||||
__all__ = ['download', 'Market', 'Search', 'Lookup', 'Ticker', 'Tickers', 'enable_debug_mode', 'set_tz_cache_location', 'Sector', 'Industry', 'WebSocket', 'AsyncWebSocket']
|
||||
# screener stuff:
|
||||
__all__ += ['EquityQuery', 'FundQuery', 'screen', 'PREDEFINED_SCREENER_QUERIES']
|
||||
|
||||
|
||||
@@ -33,6 +33,7 @@ import requests
|
||||
from . import utils, cache
|
||||
from .data import YfData
|
||||
from .exceptions import YFEarningsDateMissing, YFRateLimitError
|
||||
from .live import WebSocket
|
||||
from .scrapers.analysis import Analysis
|
||||
from .scrapers.fundamentals import Fundamentals
|
||||
from .scrapers.holders import Holders
|
||||
@@ -86,6 +87,9 @@ class TickerBase:
|
||||
|
||||
self._fast_info = None
|
||||
|
||||
self._message_handler = None
|
||||
self.ws = None
|
||||
|
||||
@utils.log_indent_decorator
|
||||
def history(self, *args, **kwargs) -> pd.DataFrame:
|
||||
return self._lazy_load_price_history().history(*args, **kwargs)
|
||||
@@ -792,3 +796,10 @@ class TickerBase:
|
||||
self._funds_data = FundsData(self._data, self.ticker)
|
||||
|
||||
return self._funds_data
|
||||
|
||||
def live(self, message_handler=None, verbose=True):
|
||||
self._message_handler = message_handler
|
||||
|
||||
self.ws = WebSocket(verbose=verbose)
|
||||
self.ws.subscribe(self.ticker)
|
||||
self.ws.listen(self._message_handler)
|
||||
|
||||
@@ -0,0 +1,335 @@
|
||||
import asyncio
|
||||
import base64
|
||||
import json
|
||||
from typing import List, Optional, Callable
|
||||
|
||||
from websockets.sync.client import connect as sync_connect
|
||||
from websockets.asyncio.client import connect as async_connect
|
||||
|
||||
from yfinance import utils
|
||||
from yfinance.pricing_pb2 import PricingData
|
||||
from google.protobuf.json_format import MessageToDict
|
||||
|
||||
|
||||
class BaseWebSocket:
|
||||
def __init__(self, url: str = "wss://streamer.finance.yahoo.com/?version=2", verbose=True):
|
||||
self.url = url
|
||||
self.verbose = verbose
|
||||
self.logger = utils.get_yf_logger()
|
||||
self._ws = None
|
||||
self._subscriptions = set()
|
||||
self._subscription_interval = 15 # seconds
|
||||
|
||||
def _decode_message(self, base64_message: str) -> dict:
|
||||
try:
|
||||
decoded_bytes = base64.b64decode(base64_message)
|
||||
pricing_data = PricingData()
|
||||
pricing_data.ParseFromString(decoded_bytes)
|
||||
return MessageToDict(pricing_data, preserving_proto_field_name=True)
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to decode message: %s", e, exc_info=True)
|
||||
if self.verbose:
|
||||
print("Failed to decode message: %s", e)
|
||||
return {
|
||||
'error': str(e),
|
||||
'raw_base64': base64_message
|
||||
}
|
||||
|
||||
|
||||
class AsyncWebSocket(BaseWebSocket):
|
||||
"""
|
||||
Asynchronous WebSocket client for streaming real time pricing data.
|
||||
"""
|
||||
|
||||
def __init__(self, url: str = "wss://streamer.finance.yahoo.com/?version=2", verbose=True):
|
||||
"""
|
||||
Initialize the AsyncWebSocket client.
|
||||
|
||||
Args:
|
||||
url (str): The WebSocket server URL. Defaults to Yahoo Finance's WebSocket URL.
|
||||
verbose (bool): Flag to enable or disable print statements. Defaults to True.
|
||||
"""
|
||||
super().__init__(url, verbose)
|
||||
self._message_handler = None # Callable to handle messages
|
||||
self._heartbeat_task = None # Task to send heartbeat subscribe
|
||||
|
||||
async def _connect(self):
|
||||
try:
|
||||
if self._ws is None:
|
||||
self._ws = await async_connect(self.url)
|
||||
self.logger.info("Connected to WebSocket.")
|
||||
if self.verbose:
|
||||
print("Connected to WebSocket.")
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to connect to WebSocket: %s", e, exc_info=True)
|
||||
if self.verbose:
|
||||
print(f"Failed to connect to WebSocket: {e}")
|
||||
self._ws = None
|
||||
raise
|
||||
|
||||
async def _periodic_subscribe(self):
|
||||
while True:
|
||||
try:
|
||||
await asyncio.sleep(self._subscription_interval)
|
||||
|
||||
if self._subscriptions:
|
||||
message = {"subscribe": list(self._subscriptions)}
|
||||
await self._ws.send(json.dumps(message))
|
||||
|
||||
if self.verbose:
|
||||
print(f"Heartbeat subscription sent for symbols: {self._subscriptions}")
|
||||
except Exception as e:
|
||||
self.logger.error("Error in heartbeat subscription: %s", e, exc_info=True)
|
||||
if self.verbose:
|
||||
print(f"Error in heartbeat subscription: {e}")
|
||||
break
|
||||
|
||||
async def subscribe(self, symbols: str | List[str]):
|
||||
"""
|
||||
Subscribe to a stock symbol or a list of stock symbols.
|
||||
|
||||
Args:
|
||||
symbols (str | List[str]): Stock symbol(s) to subscribe to.
|
||||
"""
|
||||
await self._connect()
|
||||
|
||||
if isinstance(symbols, str):
|
||||
symbols = [symbols]
|
||||
|
||||
self._subscriptions.update(symbols)
|
||||
|
||||
message = {"subscribe": list(self._subscriptions)}
|
||||
await self._ws.send(json.dumps(message))
|
||||
|
||||
# Start heartbeat subscription task
|
||||
if self._heartbeat_task is None:
|
||||
self._heartbeat_task = asyncio.create_task(self._periodic_subscribe())
|
||||
|
||||
self.logger.info(f"Subscribed to symbols: {symbols}")
|
||||
if self.verbose:
|
||||
print(f"Subscribed to symbols: {symbols}")
|
||||
|
||||
async def unsubscribe(self, symbols: str | List[str]):
|
||||
"""
|
||||
Unsubscribe from a stock symbol or a list of stock symbols.
|
||||
|
||||
Args:
|
||||
symbols (str | List[str]): Stock symbol(s) to unsubscribe from.
|
||||
"""
|
||||
await self._connect()
|
||||
|
||||
if isinstance(symbols, str):
|
||||
symbols = [symbols]
|
||||
|
||||
self._subscriptions.difference_update(symbols)
|
||||
|
||||
message = {"unsubscribe": symbols}
|
||||
await self._ws.send(json.dumps(message))
|
||||
|
||||
self.logger.info(f"Unsubscribed from symbols: {symbols}")
|
||||
if self.verbose:
|
||||
print(f"Unsubscribed from symbols: {symbols}")
|
||||
|
||||
async def listen(self, message_handler=None):
|
||||
"""
|
||||
Start listening to messages from the WebSocket server.
|
||||
|
||||
Args:
|
||||
message_handler (Optional[Callable[[dict], None]]): Optional function to handle received messages.
|
||||
"""
|
||||
await self._connect()
|
||||
self._message_handler = message_handler
|
||||
|
||||
self.logger.info("Listening for messages...")
|
||||
if self.verbose:
|
||||
print("Listening for messages...")
|
||||
|
||||
# Start heartbeat subscription task
|
||||
if self._heartbeat_task is None:
|
||||
self._heartbeat_task = asyncio.create_task(self._periodic_subscribe())
|
||||
|
||||
while True:
|
||||
try:
|
||||
async for message in self._ws:
|
||||
message_json = json.loads(message)
|
||||
encoded_data = message_json.get("message", "")
|
||||
decoded_message = self._decode_message(encoded_data)
|
||||
|
||||
if self._message_handler:
|
||||
try:
|
||||
if asyncio.iscoroutinefunction(self._message_handler):
|
||||
await self._message_handler(decoded_message)
|
||||
else:
|
||||
self._message_handler(decoded_message)
|
||||
except Exception as handler_exception:
|
||||
self.logger.error("Error in message handler: %s", handler_exception, exc_info=True)
|
||||
if self.verbose:
|
||||
print("Error in message handler:", handler_exception)
|
||||
else:
|
||||
print(decoded_message)
|
||||
|
||||
except (KeyboardInterrupt, asyncio.CancelledError):
|
||||
self.logger.info("WebSocket listening interrupted. Closing connection...")
|
||||
if self.verbose:
|
||||
print("WebSocket listening interrupted. Closing connection...")
|
||||
await self.close()
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error("Error while listening to messages: %s", e, exc_info=True)
|
||||
if self.verbose:
|
||||
print("Error while listening to messages: %s", e)
|
||||
|
||||
# Attempt to reconnect if connection drops
|
||||
self.logger.info("Attempting to reconnect...")
|
||||
if self.verbose:
|
||||
print("Attempting to reconnect...")
|
||||
await asyncio.sleep(3) # backoff
|
||||
await self._connect()
|
||||
|
||||
async def close(self):
|
||||
"""Close the WebSocket connection."""
|
||||
if self._heartbeat_task:
|
||||
self._heartbeat_task.cancel()
|
||||
|
||||
if self._ws is not None: # and not self._ws.closed:
|
||||
await self._ws.close()
|
||||
self.logger.info("WebSocket connection closed.")
|
||||
if self.verbose:
|
||||
print("WebSocket connection closed.")
|
||||
|
||||
async def __aenter__(self):
|
||||
await self._connect()
|
||||
return self
|
||||
|
||||
async def __aexit__(self, exc_type, exc_value, traceback):
|
||||
await self.close()
|
||||
|
||||
|
||||
class WebSocket(BaseWebSocket):
|
||||
"""
|
||||
Synchronous WebSocket client for streaming real time pricing data.
|
||||
"""
|
||||
|
||||
def __init__(self, url: str = "wss://streamer.finance.yahoo.com/?version=2", verbose=True):
|
||||
"""
|
||||
Initialize the WebSocket client.
|
||||
|
||||
Args:
|
||||
url (str): The WebSocket server URL. Defaults to Yahoo Finance's WebSocket URL.
|
||||
verbose (bool): Flag to enable or disable print statements. Defaults to True.
|
||||
"""
|
||||
super().__init__(url, verbose)
|
||||
|
||||
def _connect(self):
|
||||
try:
|
||||
if self._ws is None:
|
||||
self._ws = sync_connect(self.url)
|
||||
self.logger.info("Connected to WebSocket.")
|
||||
if self.verbose:
|
||||
print("Connected to WebSocket.")
|
||||
except Exception as e:
|
||||
self.logger.error("Failed to connect to WebSocket: %s", e, exc_info=True)
|
||||
if self.verbose:
|
||||
print(f"Failed to connect to WebSocket: {e}")
|
||||
self._ws = None
|
||||
raise
|
||||
|
||||
def subscribe(self, symbols: str | List[str]):
|
||||
"""
|
||||
Subscribe to a stock symbol or a list of stock symbols.
|
||||
|
||||
Args:
|
||||
symbols (str | List[str]): Stock symbol(s) to subscribe to.
|
||||
"""
|
||||
self._connect()
|
||||
|
||||
if isinstance(symbols, str):
|
||||
symbols = [symbols]
|
||||
|
||||
self._subscriptions.update(symbols)
|
||||
|
||||
message = {"subscribe": list(self._subscriptions)}
|
||||
self._ws.send(json.dumps(message))
|
||||
|
||||
self.logger.info(f"Subscribed to symbols: {symbols}")
|
||||
if self.verbose:
|
||||
print(f"Subscribed to symbols: {symbols}")
|
||||
|
||||
def unsubscribe(self, symbols: str | List[str]):
|
||||
"""
|
||||
Unsubscribe from a stock symbol or a list of stock symbols.
|
||||
|
||||
Args:
|
||||
symbols (str | List[str]): Stock symbol(s) to unsubscribe from.
|
||||
"""
|
||||
self._connect()
|
||||
|
||||
if isinstance(symbols, str):
|
||||
symbols = [symbols]
|
||||
|
||||
self._subscriptions.difference_update(symbols)
|
||||
|
||||
message = {"unsubscribe": symbols}
|
||||
self._ws.send(json.dumps(message))
|
||||
|
||||
self.logger.info(f"Unsubscribed from symbols: {symbols}")
|
||||
if self.verbose:
|
||||
print(f"Unsubscribed from symbols: {symbols}")
|
||||
|
||||
def listen(self, message_handler: Optional[Callable[[dict], None]] = None):
|
||||
"""
|
||||
Start listening to messages from the WebSocket server.
|
||||
|
||||
Args:
|
||||
message_handler (Optional[Callable[[dict], None]]): Optional function to handle received messages.
|
||||
"""
|
||||
self._connect()
|
||||
|
||||
self.logger.info("Listening for messages...")
|
||||
if self.verbose:
|
||||
print("Listening for messages...")
|
||||
|
||||
while True:
|
||||
try:
|
||||
message = self._ws.recv()
|
||||
message_json = json.loads(message)
|
||||
encoded_data = message_json.get("message", "")
|
||||
decoded_message = self._decode_message(encoded_data)
|
||||
|
||||
if message_handler:
|
||||
try:
|
||||
message_handler(decoded_message)
|
||||
except Exception as handler_exception:
|
||||
self.logger.error("Error in message handler: %s", handler_exception, exc_info=True)
|
||||
if self.verbose:
|
||||
print("Error in message handler:", handler_exception)
|
||||
else:
|
||||
print(decoded_message)
|
||||
|
||||
except KeyboardInterrupt:
|
||||
if self.verbose:
|
||||
print("Received keyboard interrupt.")
|
||||
self.close()
|
||||
break
|
||||
|
||||
except Exception as e:
|
||||
self.logger.error("Error while listening to messages: %s", e, exc_info=True)
|
||||
if self.verbose:
|
||||
print("Error while listening to messages: %s", e)
|
||||
break
|
||||
|
||||
def close(self):
|
||||
"""Close the WebSocket connection."""
|
||||
if self._ws is not None:
|
||||
self._ws.close()
|
||||
self.logger.info("WebSocket connection closed.")
|
||||
if self.verbose:
|
||||
print("WebSocket connection closed.")
|
||||
|
||||
def __enter__(self):
|
||||
self._connect()
|
||||
return self
|
||||
|
||||
def __exit__(self, exc_type, exc_value, traceback):
|
||||
self.close()
|
||||
+12
-1
@@ -22,6 +22,7 @@
|
||||
from __future__ import print_function
|
||||
|
||||
from . import Ticker, multi
|
||||
from .live import WebSocket
|
||||
from .utils import print_once
|
||||
from .data import YfData
|
||||
from .const import _SENTINEL_
|
||||
@@ -40,6 +41,9 @@ class Tickers:
|
||||
|
||||
self._data = YfData(session=session)
|
||||
|
||||
self._message_handler = None
|
||||
self.ws = None
|
||||
|
||||
# self.tickers = _namedtuple(
|
||||
# "Tickers", ticker_objects.keys(), rename=True
|
||||
# )(*ticker_objects.values())
|
||||
@@ -58,7 +62,7 @@ class Tickers:
|
||||
return self.download(
|
||||
period, interval,
|
||||
start, end, prepost,
|
||||
actions, auto_adjust, repair,
|
||||
actions, auto_adjust, repair,
|
||||
threads, group_by, progress,
|
||||
timeout, **kwargs)
|
||||
|
||||
@@ -98,3 +102,10 @@ class Tickers:
|
||||
|
||||
def news(self):
|
||||
return {ticker: [item for item in Ticker(ticker).news] for ticker in self.symbols}
|
||||
|
||||
def live(self, message_handler=None, verbose=True):
|
||||
self._message_handler = message_handler
|
||||
|
||||
self.ws = WebSocket(verbose=verbose)
|
||||
self.ws.subscribe(self.symbols)
|
||||
self.ws.listen(self._message_handler)
|
||||
|
||||
Reference in New Issue
Block a user