@@ -82,6 +82,11 @@ def _cmd_download(args: argparse.Namespace) -> None:
8282 level = level ,
8383 )
8484
85+ if getattr (args , "stocks" , False ):
86+ for symbol in args .symbols :
87+ _download_stocks_with_rich (symbol .upper ())
88+ return
89+
8590 from optopsy .ui .providers import get_provider_for_tool
8691 from optopsy .ui .providers .eodhd import EODHDProvider
8792
@@ -168,6 +173,97 @@ def _on_status(msg: str) -> None:
168173 console .print (f"\n { summary } " )
169174
170175
176+ def _download_stocks_with_rich (symbol : str ) -> None :
177+ """Download stock/index OHLCV data via yfinance with Rich progress display."""
178+ from datetime import date
179+
180+ import pandas as pd
181+ from rich .console import Console
182+
183+ from optopsy .ui .providers .cache import ParquetCache
184+ from optopsy .ui .tools ._helpers import _YF_CACHE_CATEGORY , _yf_fetch_and_cache
185+
186+ console = Console ()
187+ console .rule (f"Downloading stock data for { symbol } " )
188+
189+ cache = ParquetCache ()
190+ cached = cache .read (_YF_CACHE_CATEGORY , symbol )
191+
192+ with console .status (f"[bold green]Fetching { symbol } from yfinance…" ):
193+ try :
194+ result = _yf_fetch_and_cache (symbol , cached , date .today ())
195+ except (OSError , ValueError ) as exc :
196+ console .print (f" [red]Error fetching { symbol } : { exc } [/red]" )
197+ return
198+
199+ if result is None or result .empty :
200+ console .print (f" [yellow]No data returned for { symbol } .[/yellow]" )
201+ return
202+
203+ date_min = pd .to_datetime (result ["date" ]).dt .date .min ()
204+ date_max = pd .to_datetime (result ["date" ]).dt .date .max ()
205+ row_count = len (result )
206+
207+ size_bytes = cache .size ().get (f"{ _YF_CACHE_CATEGORY } /{ symbol } " , 0 )
208+ size_str = _format_bytes (size_bytes )
209+
210+ console .print (f" [bold]{ symbol } [/bold] { date_min } → { date_max } " )
211+ console .print (
212+ f" [cyan]{ row_count :,} rows[/cyan] [dim]({ size_str } on disk)[/dim]\n "
213+ )
214+
215+
216+ def _cmd_symbols (args : argparse .Namespace ) -> None :
217+ """List symbols that have options data available from configured providers."""
218+ from rich .columns import Columns
219+ from rich .console import Console
220+
221+ _load_env ()
222+
223+ from optopsy .ui .providers import get_available_providers
224+
225+ console = Console ()
226+ search = getattr (args , "search" , None )
227+ use_pager = not search and console .is_terminal
228+ found = False
229+
230+ def _render () -> None :
231+ nonlocal found
232+ for provider in get_available_providers ():
233+ if not provider .is_available ():
234+ continue
235+ symbols = provider .list_available_symbols ()
236+ if symbols is None :
237+ continue
238+ found = True
239+
240+ if search :
241+ term = search .upper ()
242+ symbols = [s for s in symbols if term in s ]
243+
244+ if not symbols :
245+ console .print (
246+ f"[yellow]No symbols matching '{ search } ' from { provider .name } .[/yellow]"
247+ )
248+ continue
249+
250+ console .rule (f"{ provider .name } — { len (symbols ):,} symbols" )
251+ console .print (Columns (symbols , padding = (0 , 2 ), column_first = True ))
252+ console .print ()
253+
254+ if use_pager :
255+ with console .pager (styles = True ):
256+ _render ()
257+ else :
258+ _render ()
259+
260+ if not found :
261+ console .print (
262+ "[yellow]No data provider supports listing symbols.\n "
263+ "Set EODHD_API_KEY in your environment or .env file.[/yellow]"
264+ )
265+
266+
171267def _cmd_run (args : argparse .Namespace ) -> None :
172268 """Configure environment and launch the Chainlit server."""
173269 if args .host :
@@ -250,18 +346,44 @@ def main(argv: list[str] | None = None) -> None:
250346 # --- download ---
251347 dl_parser = subparsers .add_parser (
252348 "download" ,
253- help = "Download historical options data for one or more symbols" ,
349+ help = "Download historical market data for one or more symbols" ,
254350 )
255351 dl_parser .add_argument (
256352 "symbols" ,
257353 nargs = "+" ,
258354 help = "One or more US stock ticker symbols (e.g. SPY AAPL TSLA)" ,
259355 )
356+ dl_group = dl_parser .add_mutually_exclusive_group ()
357+ dl_group .add_argument (
358+ "-o" ,
359+ "--options" ,
360+ action = "store_true" ,
361+ default = True ,
362+ help = "Download options chain data via EODHD (default)" ,
363+ )
364+ dl_group .add_argument (
365+ "-s" ,
366+ "--stocks" ,
367+ action = "store_true" ,
368+ help = "Download stock/index OHLCV data via yfinance (no API key needed)" ,
369+ )
260370 dl_parser .add_argument (
261371 "-v" , "--verbose" , action = "store_true" , help = "Enable debug logging"
262372 )
263373 dl_parser .set_defaults (func = _cmd_download )
264374
375+ # --- symbols ---
376+ sym_parser = subparsers .add_parser (
377+ "symbols" , help = "List symbols with options data available for download"
378+ )
379+ sym_parser .add_argument (
380+ "-q" ,
381+ "--search" ,
382+ default = None ,
383+ help = "Filter symbols containing TERM (case-insensitive)" ,
384+ )
385+ sym_parser .set_defaults (func = _cmd_symbols )
386+
265387 # --- cache ---
266388 cache_parser = subparsers .add_parser ("cache" , help = "Manage the data cache" )
267389 cache_sub = cache_parser .add_subparsers (dest = "cache_command" )
0 commit comments