grynn-fplot 0.3.2__tar.gz → 0.3.3__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (40) hide show
  1. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/PKG-INFO +28 -6
  2. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/README.md +26 -5
  3. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3/docs}/FILTER_DESIGN.md +2 -2
  4. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/grynn_fplot/cli.py +160 -10
  5. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/grynn_fplot/core.py +313 -50
  6. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/grynn_fplot/filter_parser.py +14 -0
  7. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/pyproject.toml +2 -1
  8. grynn_fplot-0.3.3/tests/test_candlestick.py +143 -0
  9. grynn_fplot-0.3.3/tests/test_cli_integration.py +123 -0
  10. grynn_fplot-0.3.3/tests/test_input_parsing.py +151 -0
  11. grynn_fplot-0.3.3/tests/test_leverage.py +167 -0
  12. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/uv.lock +16 -1
  13. grynn_fplot-0.3.2/SUMMARY.md +0 -257
  14. grynn_fplot-0.3.2/todo.md +0 -1
  15. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/.devcontainer/devcontainer.json +0 -0
  16. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/.github/copilot-instructions.md +0 -0
  17. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/.github/dependabot.yml +0 -0
  18. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/.github/workflows/publish.yml +0 -0
  19. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/.gitignore +0 -0
  20. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/.pre-commit-config.yaml +0 -0
  21. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/.secrets.baseline +0 -0
  22. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/.vscode/settings.json +0 -0
  23. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/Makefile +0 -0
  24. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3/docs}/FILTER_EXAMPLES.sh +0 -0
  25. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3/docs}/PERFORMANCE_OPTIMIZATION.md +0 -0
  26. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3/docs}/WEB_REFACTOR.md +0 -0
  27. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/example_usage.sh +0 -0
  28. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/grynn_fplot/__init__.py +0 -0
  29. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/grynn_fplot/drawdowns.py +0 -0
  30. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/grynn_fplot/index.html +0 -0
  31. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/grynn_fplot/plot_option_interactive.py +0 -0
  32. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/grynn_fplot/serve.py +0 -0
  33. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/grynn_fplot/web_api.py +0 -0
  34. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/tests/test_cli_options.py +0 -0
  35. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/tests/test_filter_evaluation.py +0 -0
  36. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/tests/test_filter_parser.py +0 -0
  37. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/tests/test_fplot.py +0 -0
  38. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/tests/test_options.py +0 -0
  39. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/tests/test_web_api.py +0 -0
  40. {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/tests/test_web_interface.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: grynn-fplot
3
- Version: 0.3.2
3
+ Version: 0.3.3
4
4
  Summary: CLI tool for plotting financial data and analyzing options
5
5
  Project-URL: Homepage, https://github.com/Grynn/grynn_cli_fplot
6
6
  Project-URL: Issues, https://github.com/Grynn/grynn_cli_fplot/issues
@@ -23,6 +23,7 @@ Requires-Dist: grynn-pylib
23
23
  Requires-Dist: ipywidgets>=8.1.5
24
24
  Requires-Dist: matplotlib>=3.9.2
25
25
  Requires-Dist: mplcursors~=0.5.3
26
+ Requires-Dist: mplfinance>=0.12.9
26
27
  Requires-Dist: pandas>=2.2.3
27
28
  Requires-Dist: scikit-learn>=1.6.1
28
29
  Requires-Dist: tabulate>=0.9.0
@@ -106,16 +107,25 @@ Examples:
106
107
  - `fplot AAPL --call --filter "dte>10, dte<50"` # 10-50 days to expiry
107
108
  - `fplot AAPL --call --filter "dte>1y"` # Options with 1+ year to expiry
108
109
 
109
- The options output includes pricing and return metrics:
110
+ The options output includes pricing, return metrics, implied leverage, and efficiency:
110
111
  ```
111
- AAPL 225C 35DTE ($5.25, 18.5%)
112
- AAPL 230C 35DTE ($3.10, 25.2%)
113
- AAPL 235C 35DTE ($1.85, 35.1%)
112
+ AAPL 225C 35DTE ($5.25, 18.5%, 19.0x, eff:45)
113
+ AAPL 230C 35DTE ($3.10, 25.2%, 32.3x, eff:78)
114
+ AAPL 235C 35DTE ($1.85, 35.1%, 54.1x, eff:92)
114
115
  ```
115
116
 
116
- Format: `TICKER STRIKE[C|P] DAYS_TO_EXPIRY (price, return_metric)`
117
+ Format: `TICKER STRIKE[C|P] DAYS_TO_EXPIRY (price, return_metric, leverage, eff:percentile)`
117
118
  - For calls: return_metric is CAGR to breakeven
118
119
  - For puts: return_metric is annualized return
120
+ - Leverage: Implied leverage (Ω = Δ × S/O) where Δ is Black-Scholes delta, S is spot price, O is option price
121
+ - Delta calculated using actual implied volatility from Yahoo Finance
122
+ - Shows "N/A" if implied volatility is not available
123
+ - Shows the percentage change in option value for a 1% change in stock price
124
+ - Example: 10x leverage means a 1% stock move results in ~10% option move
125
+ - Efficiency: Percentile rank (0-100) of leverage/CAGR ratio
126
+ - Higher = better (high leverage with low required stock movement)
127
+ - 80+ = top 20% most efficient options
128
+ - Shows "N/A" if leverage or return unavailable
119
129
 
120
130
  **Expiry Filtering Options:**
121
131
  - `--max <time>`: Filter to show only options expiring within the specified time
@@ -144,6 +154,8 @@ The `--filter` option supports complex filter expressions with logical operators
144
154
  - `return`, `ret`, `ar`: Return metric (CAGR for calls, annualized return for puts) - all aliases work
145
155
  - `strike_pct`, `sp`: Strike percentage above/below spot (positive = above spot, negative = below spot)
146
156
  - `lt_days`: Days since last trade (useful for filtering stale options)
157
+ - `leverage`, `lev`: Implied leverage (Ω = Δ × S/O, using Black-Scholes delta)
158
+ - `efficiency`, `eff`: Efficiency percentile (leverage/CAGR ratio, 0-100 scale)
147
159
 
148
160
  - **Examples:**
149
161
  - `--filter "dte>300"` - Options with more than 300 days to expiry
@@ -154,6 +166,10 @@ The `--filter` option supports complex filter expressions with logical operators
154
166
  - `--filter "volume>=100"` - High volume options
155
167
  - `--filter "lt_days<=7"` - Options traded within last 7 days
156
168
  - `--filter "ar>50"` - Annualized return > 50%
169
+ - `--filter "leverage>10"` - High leverage options (10x or more)
170
+ - `--filter "lev>5, lev<20"` - Moderate leverage options (5x-20x)
171
+ - `--filter "efficiency>80"` - Top 20% most efficient options
172
+ - `--filter "eff>60, dte<60"` - Above-average efficiency with <60 days to expiry
157
173
 
158
174
  - **Time Values:**
159
175
  - DTE-style expressions: `1y` (365 days), `6m` (180 days), `2w` (14 days)
@@ -164,3 +180,9 @@ The `--filter` option supports complex filter expressions with logical operators
164
180
  - `--filter "lt_days<=7"` - Options traded in the last week
165
181
 
166
182
  Options data is cached for 1 hour to improve performance and reduce API calls.
183
+
184
+ ## TODO
185
+
186
+ - fplot --call | fzf lets you select a call; once a call is selected it should be possible to plot a chart for it (yfinance provides price history for option identifiers)
187
+ - need more examples of usage with fzf to pick a call
188
+ - use grynn_pylib to download options (does it have identifiers that can be used to fetch price history?)
@@ -68,16 +68,25 @@ Examples:
68
68
  - `fplot AAPL --call --filter "dte>10, dte<50"` # 10-50 days to expiry
69
69
  - `fplot AAPL --call --filter "dte>1y"` # Options with 1+ year to expiry
70
70
 
71
- The options output includes pricing and return metrics:
71
+ The options output includes pricing, return metrics, implied leverage, and efficiency:
72
72
  ```
73
- AAPL 225C 35DTE ($5.25, 18.5%)
74
- AAPL 230C 35DTE ($3.10, 25.2%)
75
- AAPL 235C 35DTE ($1.85, 35.1%)
73
+ AAPL 225C 35DTE ($5.25, 18.5%, 19.0x, eff:45)
74
+ AAPL 230C 35DTE ($3.10, 25.2%, 32.3x, eff:78)
75
+ AAPL 235C 35DTE ($1.85, 35.1%, 54.1x, eff:92)
76
76
  ```
77
77
 
78
- Format: `TICKER STRIKE[C|P] DAYS_TO_EXPIRY (price, return_metric)`
78
+ Format: `TICKER STRIKE[C|P] DAYS_TO_EXPIRY (price, return_metric, leverage, eff:percentile)`
79
79
  - For calls: return_metric is CAGR to breakeven
80
80
  - For puts: return_metric is annualized return
81
+ - Leverage: Implied leverage (Ω = Δ × S/O) where Δ is Black-Scholes delta, S is spot price, O is option price
82
+ - Delta calculated using actual implied volatility from Yahoo Finance
83
+ - Shows "N/A" if implied volatility is not available
84
+ - Shows the percentage change in option value for a 1% change in stock price
85
+ - Example: 10x leverage means a 1% stock move results in ~10% option move
86
+ - Efficiency: Percentile rank (0-100) of leverage/CAGR ratio
87
+ - Higher = better (high leverage with low required stock movement)
88
+ - 80+ = top 20% most efficient options
89
+ - Shows "N/A" if leverage or return unavailable
81
90
 
82
91
  **Expiry Filtering Options:**
83
92
  - `--max <time>`: Filter to show only options expiring within the specified time
@@ -106,6 +115,8 @@ The `--filter` option supports complex filter expressions with logical operators
106
115
  - `return`, `ret`, `ar`: Return metric (CAGR for calls, annualized return for puts) - all aliases work
107
116
  - `strike_pct`, `sp`: Strike percentage above/below spot (positive = above spot, negative = below spot)
108
117
  - `lt_days`: Days since last trade (useful for filtering stale options)
118
+ - `leverage`, `lev`: Implied leverage (Ω = Δ × S/O, using Black-Scholes delta)
119
+ - `efficiency`, `eff`: Efficiency percentile (leverage/CAGR ratio, 0-100 scale)
109
120
 
110
121
  - **Examples:**
111
122
  - `--filter "dte>300"` - Options with more than 300 days to expiry
@@ -116,6 +127,10 @@ The `--filter` option supports complex filter expressions with logical operators
116
127
  - `--filter "volume>=100"` - High volume options
117
128
  - `--filter "lt_days<=7"` - Options traded within last 7 days
118
129
  - `--filter "ar>50"` - Annualized return > 50%
130
+ - `--filter "leverage>10"` - High leverage options (10x or more)
131
+ - `--filter "lev>5, lev<20"` - Moderate leverage options (5x-20x)
132
+ - `--filter "efficiency>80"` - Top 20% most efficient options
133
+ - `--filter "eff>60, dte<60"` - Above-average efficiency with <60 days to expiry
119
134
 
120
135
  - **Time Values:**
121
136
  - DTE-style expressions: `1y` (365 days), `6m` (180 days), `2w` (14 days)
@@ -126,3 +141,9 @@ The `--filter` option supports complex filter expressions with logical operators
126
141
  - `--filter "lt_days<=7"` - Options traded in the last week
127
142
 
128
143
  Options data is cached for 1 hour to improve performance and reduce API calls.
144
+
145
+ ## TODO
146
+
147
+ - fplot --call | fzf lets you select a call; once a call is selected it should be possible to plot a chart for it (yfinance provides price history for option identifiers)
148
+ - need more examples of usage with fzf to pick a call
149
+ - use grynn_pylib to download options (does it have identifiers that can be used to fetch price history?)
@@ -138,11 +138,11 @@ The `evaluate_filter()` function in `grynn_fplot/core.py` evaluates a filter AST
138
138
  ```python
139
139
  def evaluate_filter(filter_ast: dict, data: dict) -> bool:
140
140
  """Evaluate a filter AST against data.
141
-
141
+
142
142
  Args:
143
143
  filter_ast: Parsed filter AST
144
144
  data: Dictionary with option data
145
-
145
+
146
146
  Returns:
147
147
  True if data passes filter, False otherwise
148
148
  """
@@ -7,6 +7,7 @@ import click
7
7
  import matplotlib.pyplot as plt
8
8
  import mplcursors
9
9
  import numpy as np
10
+ import mplfinance as mpf
10
11
  from grynn_pylib.finance.timeseries import rolling_cagr
11
12
  from loguru import logger
12
13
  from tabulate import tabulate
@@ -16,6 +17,7 @@ from grynn_fplot.core import (
16
17
  calculate_cagr,
17
18
  calculate_drawdowns,
18
19
  download_ticker_data,
20
+ download_ohlcv_data,
19
21
  normalize_prices,
20
22
  parse_start_date,
21
23
  format_options_for_display,
@@ -31,7 +33,7 @@ except importlib.metadata.PackageNotFoundError:
31
33
  @click.command()
32
34
  @click.option("--since", type=str, default=None, help="Start date for data (e.g., '1y', '6m', '2023-01-01')")
33
35
  @click.option("--interval", type=str, default="1d", help="Data interval (1d, 1wk, 1mo)")
34
- @click.argument("ticker", type=str, nargs=1, required=False)
36
+ @click.argument("ticker", type=str, nargs=-1, required=False)
35
37
  @click.option("--version", "-v", is_flag=True, help="Show version and exit")
36
38
  @click.option("--debug", is_flag=True, help="Enable debug mode")
37
39
  @click.option("--call", is_flag=True, help="List all available call options for the ticker")
@@ -86,6 +88,24 @@ def display_plot(
86
88
 
87
89
  Examples:
88
90
  \b
91
+ # Single ticker
92
+ fplot AAPL
93
+
94
+ # Multiple tickers (space-separated)
95
+ fplot AAPL TSLA MSFT
96
+
97
+ # Comma-separated tickers
98
+ fplot AAPL,TSLA
99
+
100
+ # Division operations
101
+ fplot AAPL/XLK
102
+
103
+ # Mixed inputs
104
+ fplot AAPL AAPL/XLK TW.L
105
+
106
+ # Quoted strings with commas
107
+ fplot "AAPL, TSLA"
108
+
89
109
  # List all AAPL call options (default: 6 months max)
90
110
  fplot AAPL --call
91
111
 
@@ -109,23 +129,33 @@ def display_plot(
109
129
  print(f"fplot {__version__}")
110
130
  return
111
131
 
112
- # Show filter help if requested
132
+ # Show filter help if requested
113
133
  if filter_help:
114
134
  from grynn_fplot.filter_parser import get_filter_help
115
135
 
116
136
  print(get_filter_help())
117
137
  return
118
138
 
139
+ # Convert ticker tuple to list (Click's variadic arguments return a tuple)
140
+ ticker_list = list(ticker) if ticker else []
119
141
  # Launch web interface if --web flag is used
120
142
  if web:
121
- launch_web_interface(ticker, since, interval, port, host, no_browser, debug)
143
+ # For web interface, join tickers back into a string
144
+ ticker_str = ','.join(ticker_list) if ticker_list else None
145
+ launch_web_interface(ticker_str, since, interval, port, host, no_browser, debug)
122
146
  return
123
147
 
124
148
  # CLI mode - require ticker
125
- if not ticker:
149
+ if not ticker_list:
126
150
  click.echo(
127
- "Error: Missing argument 'TICKER'. Please provide a ticker symbol or symbols as a comma separated list."
151
+ "Error: Missing argument 'TICKER'. Please provide ticker symbol(s)."
128
152
  )
153
+ click.echo("Examples:")
154
+ click.echo(" fplot AAPL")
155
+ click.echo(" fplot AAPL TSLA")
156
+ click.echo(" fplot AAPL,TSLA")
157
+ click.echo(" fplot AAPL/XLK")
158
+ click.echo(' fplot "AAPL, TSLA"')
129
159
  click.echo("Hint: Use --web or -w to launch the interactive web interface.")
130
160
  return
131
161
 
@@ -176,8 +206,10 @@ def display_plot(
176
206
 
177
207
  # Handle options listing
178
208
  if call:
209
+ # For options, use the first ticker only (options only work with single ticker)
210
+ ticker_for_options = ticker_list[0] if ticker_list else ""
179
211
  options_list = format_options_for_display(
180
- ticker,
212
+ ticker_for_options,
181
213
  "calls",
182
214
  max_expiry=use_max_expiry,
183
215
  min_dte=parsed_min_dte,
@@ -185,7 +217,7 @@ def display_plot(
185
217
  filter_ast=parsed_filter,
186
218
  )
187
219
  if not options_list:
188
- click.echo(f"No call options found for {ticker.upper()}")
220
+ click.echo(f"No call options found for {ticker_for_options.upper()}")
189
221
  return
190
222
 
191
223
  for option in options_list:
@@ -193,8 +225,10 @@ def display_plot(
193
225
  return
194
226
 
195
227
  if put:
228
+ # For options, use the first ticker only (options only work with single ticker)
229
+ ticker_for_options = ticker_list[0] if ticker_list else ""
196
230
  options_list = format_options_for_display(
197
- ticker,
231
+ ticker_for_options,
198
232
  "puts",
199
233
  max_expiry=use_max_expiry,
200
234
  min_dte=parsed_min_dte,
@@ -202,7 +236,7 @@ def display_plot(
202
236
  filter_ast=parsed_filter,
203
237
  )
204
238
  if not options_list:
205
- click.echo(f"No put options found for {ticker.upper()}")
239
+ click.echo(f"No put options found for {ticker_for_options.upper()}")
206
240
  return
207
241
 
208
242
  for option in options_list:
@@ -210,7 +244,7 @@ def display_plot(
210
244
  return
211
245
 
212
246
  # Continue with original CLI plotting logic
213
- display_cli_plot(ticker, since, interval, debug)
247
+ display_cli_plot(ticker_list, since, interval, debug)
214
248
 
215
249
 
216
250
  def launch_web_interface(ticker, since, interval, port, host, no_browser, debug):
@@ -314,8 +348,124 @@ def launch_web_interface(ticker, since, interval, port, host, no_browser, debug)
314
348
  return
315
349
 
316
350
 
351
+ def display_candlestick_plot(ticker, since, interval, debug):
352
+ """Display candlestick plot with volume and SMAs for a single ticker"""
353
+ from dateutil.relativedelta import relativedelta
354
+
355
+ requested_since = parse_start_date(since)
356
+
357
+ # Always fetch 3 years of data to ensure we can compute 200-day SMA
358
+ # even if the user requested a shorter timeframe
359
+ fetch_since = datetime.now() - relativedelta(years=3)
360
+
361
+ # Download OHLCV data (fetch 3 years)
362
+ df_full = download_ohlcv_data(ticker, fetch_since, interval)
363
+ if df_full.empty:
364
+ print(f"No data found for {ticker}.")
365
+ return
366
+
367
+ # Calculate SMAs on the full dataset (3 years)
368
+ sma_50 = df_full['Close'].rolling(window=50).mean()
369
+ sma_200 = df_full['Close'].rolling(window=200).mean()
370
+
371
+ # Filter to the requested timeframe for display
372
+ if requested_since is not None:
373
+ df = df_full[df_full.index >= requested_since]
374
+ sma_50 = sma_50[sma_50.index >= requested_since]
375
+ sma_200 = sma_200[sma_200.index >= requested_since]
376
+ else:
377
+ df = df_full
378
+
379
+ if df.empty:
380
+ print(f"No data found for {ticker} in the requested timeframe.")
381
+ return
382
+
383
+ print(f"Generating candlestick plot for {ticker} since {requested_since.date() if requested_since else 'max'}. Interval: {interval}")
384
+
385
+ if debug:
386
+ print(f"Data for {ticker}:")
387
+ print(f"Full dataset: {len(df_full)} rows, Display dataset: {len(df)} rows")
388
+ print(df.head())
389
+ temp_file = tempfile.NamedTemporaryFile(suffix=".csv", delete=False)
390
+ df.to_csv(temp_file.name)
391
+ print(f"Data saved to temporary file: {temp_file.name}")
392
+
393
+ # Create additional plots list for SMAs
394
+ add_plots = []
395
+
396
+ # Add 50-day SMA if we have enough data
397
+ if not sma_50.isna().all():
398
+ add_plots.append(mpf.make_addplot(sma_50, color='orange', width=1.5, label='50-day SMA'))
399
+
400
+ # Add 200-day SMA if we have enough data
401
+ if not sma_200.isna().all():
402
+ add_plots.append(mpf.make_addplot(sma_200, color='red', width=1.5, label='200-day SMA'))
403
+
404
+ # Configure mplfinance style
405
+ mc = mpf.make_marketcolors(
406
+ up='green', down='red',
407
+ wick={'up': 'green', 'down': 'red'},
408
+ edge={'up': 'green', 'down': 'red'},
409
+ volume={'up': 'green', 'down': 'red'}
410
+ )
411
+ s = mpf.make_mpf_style(marketcolors=mc, gridstyle=':', y_on_right=False)
412
+
413
+ # Create the plot
414
+ fig, axes = mpf.plot(
415
+ df,
416
+ type='candle',
417
+ style=s,
418
+ volume=True,
419
+ addplot=add_plots if add_plots else None,
420
+ title=f"{ticker} - Candlestick Chart",
421
+ ylabel='Price',
422
+ ylabel_lower='Volume',
423
+ figsize=(16, 10),
424
+ datetime_format='%Y-%m-%d',
425
+ xrotation=15,
426
+ returnfig=True,
427
+ warn_too_much_data=1000 # Suppress warning for large datasets
428
+ )
429
+
430
+ # Add legend for SMAs if they exist
431
+ if add_plots:
432
+ ax = axes[0] # Main price axis
433
+ # Create custom legend entries
434
+ from matplotlib.lines import Line2D
435
+ legend_elements = [
436
+ Line2D([0], [0], color='green', lw=2, label='Up Day'),
437
+ Line2D([0], [0], color='red', lw=2, label='Down Day'),
438
+ ]
439
+ if not sma_50.isna().all():
440
+ legend_elements.append(Line2D([0], [0], color='orange', lw=1.5, label='50-day SMA'))
441
+ if not sma_200.isna().all():
442
+ legend_elements.append(Line2D([0], [0], color='red', lw=1.5, label='200-day SMA'))
443
+
444
+ ax.legend(handles=legend_elements, loc='best')
445
+
446
+ plt.show()
447
+
448
+
317
449
  def display_cli_plot(ticker, since, interval, debug):
318
450
  """Display plot using matplotlib (original CLI functionality)"""
451
+ from grynn_fplot.core import parse_ticker_input
452
+
453
+ # Parse ticker input to understand what we're dealing with
454
+ parsed_tickers = parse_ticker_input(ticker)
455
+
456
+ # Check if this is a single ticker scenario (should use candlestick)
457
+ # Single ticker means:
458
+ # 1. No division operators in any ticker
459
+ # 2. Exactly one ticker provided (not counting SPY which is auto-added)
460
+ has_division = any('/' in t for t in parsed_tickers)
461
+ ticker_count = len(parsed_tickers)
462
+
463
+ # Route to candlestick chart for single ticker without division
464
+ if not has_division and ticker_count == 1:
465
+ display_candlestick_plot(parsed_tickers[0], since, interval, debug)
466
+ return
467
+
468
+ # Otherwise, continue with existing line chart logic for multi-ticker or division
319
469
  since = parse_start_date(since)
320
470
 
321
471
  # Download and prepare data