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.
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/PKG-INFO +28 -6
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/README.md +26 -5
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3/docs}/FILTER_DESIGN.md +2 -2
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/grynn_fplot/cli.py +160 -10
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/grynn_fplot/core.py +313 -50
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/grynn_fplot/filter_parser.py +14 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/pyproject.toml +2 -1
- grynn_fplot-0.3.3/tests/test_candlestick.py +143 -0
- grynn_fplot-0.3.3/tests/test_cli_integration.py +123 -0
- grynn_fplot-0.3.3/tests/test_input_parsing.py +151 -0
- grynn_fplot-0.3.3/tests/test_leverage.py +167 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/uv.lock +16 -1
- grynn_fplot-0.3.2/SUMMARY.md +0 -257
- grynn_fplot-0.3.2/todo.md +0 -1
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/.devcontainer/devcontainer.json +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/.github/copilot-instructions.md +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/.github/dependabot.yml +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/.github/workflows/publish.yml +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/.gitignore +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/.pre-commit-config.yaml +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/.secrets.baseline +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/.vscode/settings.json +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/Makefile +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3/docs}/FILTER_EXAMPLES.sh +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3/docs}/PERFORMANCE_OPTIMIZATION.md +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3/docs}/WEB_REFACTOR.md +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/example_usage.sh +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/grynn_fplot/__init__.py +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/grynn_fplot/drawdowns.py +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/grynn_fplot/index.html +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/grynn_fplot/plot_option_interactive.py +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/grynn_fplot/serve.py +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/grynn_fplot/web_api.py +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/tests/test_cli_options.py +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/tests/test_filter_evaluation.py +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/tests/test_filter_parser.py +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/tests/test_fplot.py +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/tests/test_options.py +0 -0
- {grynn_fplot-0.3.2 → grynn_fplot-0.3.3}/tests/test_web_api.py +0 -0
- {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.
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
149
|
+
if not ticker_list:
|
|
126
150
|
click.echo(
|
|
127
|
-
"Error: Missing argument 'TICKER'. Please provide
|
|
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
|
-
|
|
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 {
|
|
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
|
-
|
|
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 {
|
|
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(
|
|
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
|