yfin-cli 0.1.1__py3-none-any.whl
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.
- src/__init__.py +1 -0
- src/cli.py +133 -0
- src/commands/__init__.py +0 -0
- src/commands/analysis.py +153 -0
- src/commands/calendar.py +84 -0
- src/commands/financials.py +83 -0
- src/commands/industry.py +44 -0
- src/commands/market.py +11 -0
- src/commands/screen.py +260 -0
- src/commands/sector.py +66 -0
- src/commands/stock.py +108 -0
- src/decorators.py +33 -0
- src/typer.py +223 -0
- src/utils.py +91 -0
- src/writer.py +43 -0
- yfin_cli-0.1.1.dist-info/METADATA +124 -0
- yfin_cli-0.1.1.dist-info/RECORD +20 -0
- yfin_cli-0.1.1.dist-info/WHEEL +4 -0
- yfin_cli-0.1.1.dist-info/entry_points.txt +2 -0
- yfin_cli-0.1.1.dist-info/licenses/LICENSE +21 -0
src/commands/screen.py
ADDED
|
@@ -0,0 +1,260 @@
|
|
|
1
|
+
import yfinance as yf
|
|
2
|
+
import typer
|
|
3
|
+
import json
|
|
4
|
+
from ..decorators import command
|
|
5
|
+
from ..typer import (
|
|
6
|
+
ScreenFilterTypeOptional,
|
|
7
|
+
ScreenPredefinedQueryTypeOptional,
|
|
8
|
+
ScreenJsonQueryTypeOptional,
|
|
9
|
+
OffsetType,
|
|
10
|
+
default_offset,
|
|
11
|
+
LimitType,
|
|
12
|
+
default_limit,
|
|
13
|
+
ScreenQueryFieldType,
|
|
14
|
+
ScreenSortOrderType,
|
|
15
|
+
default_sort_order,
|
|
16
|
+
)
|
|
17
|
+
from ..utils import is_number, count_specified
|
|
18
|
+
|
|
19
|
+
VALID_FIELDS_DICT = yf.const.EQUITY_SCREENER_FIELDS
|
|
20
|
+
VALID_VALUES_DICT = yf.const.EQUITY_SCREENER_EQ_MAP
|
|
21
|
+
VALID_FIELDS = set().union(*VALID_FIELDS_DICT.values())
|
|
22
|
+
VALID_REGIONS = set(VALID_VALUES_DICT.get("region"))
|
|
23
|
+
VALID_EXCHANGES = set().union(*VALID_VALUES_DICT.get("exchange").values())
|
|
24
|
+
VALID_SECTORS = set(VALID_VALUES_DICT.get("sector"))
|
|
25
|
+
VALID_INDUSTRIES = set().union(*VALID_VALUES_DICT.get("industry").values())
|
|
26
|
+
VALID_PEER_GROUPS = set(VALID_VALUES_DICT.get("peer_group"))
|
|
27
|
+
PREDEFINED_QUERIES = set(yf.PREDEFINED_SCREENER_QUERIES.keys())
|
|
28
|
+
VALID_OPERATORS = {
|
|
29
|
+
"eq",
|
|
30
|
+
"gt",
|
|
31
|
+
"gte",
|
|
32
|
+
"lt",
|
|
33
|
+
"lte",
|
|
34
|
+
"btwn",
|
|
35
|
+
"is-in",
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@command
|
|
40
|
+
def screen_query_fields():
|
|
41
|
+
"""Get a list of all valid fields for screening."""
|
|
42
|
+
return [{"field": f} for f in sorted(list(VALID_FIELDS))]
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
@command
|
|
46
|
+
def screen_query_values(field: ScreenQueryFieldType):
|
|
47
|
+
"""Get a list of all valid values for a field."""
|
|
48
|
+
if field == "region":
|
|
49
|
+
values = list(VALID_REGIONS)
|
|
50
|
+
elif field == "exchange":
|
|
51
|
+
values = list(VALID_EXCHANGES)
|
|
52
|
+
elif field == "sector":
|
|
53
|
+
values = list(VALID_SECTORS)
|
|
54
|
+
elif field == "industry":
|
|
55
|
+
values = list(VALID_INDUSTRIES)
|
|
56
|
+
elif field == "peer_group":
|
|
57
|
+
values = list(VALID_PEER_GROUPS)
|
|
58
|
+
else:
|
|
59
|
+
validate_field(field)
|
|
60
|
+
values = None
|
|
61
|
+
|
|
62
|
+
if values is None:
|
|
63
|
+
return None
|
|
64
|
+
|
|
65
|
+
return [{"value": v} for v in sorted(values)]
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@command
|
|
69
|
+
def screen_predefined_queries():
|
|
70
|
+
"""Get a list of all valid predefined queries."""
|
|
71
|
+
return [{"query": q} for q in sorted(list(PREDEFINED_QUERIES))]
|
|
72
|
+
|
|
73
|
+
|
|
74
|
+
def validate_field(field: str):
|
|
75
|
+
"""
|
|
76
|
+
Validate the field.
|
|
77
|
+
"""
|
|
78
|
+
if field not in VALID_FIELDS:
|
|
79
|
+
raise typer.BadParameter(
|
|
80
|
+
f"Invalid field: '{field}'. Valid fields can be found using `yfin screen-query-fields`."
|
|
81
|
+
)
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
def parse_value(field: str, value: str) -> str | float:
|
|
85
|
+
"""
|
|
86
|
+
Validate the value against the field, then return it into the correct type.
|
|
87
|
+
"""
|
|
88
|
+
if field in VALID_FIELDS_DICT.get("eq_fields"):
|
|
89
|
+
# fields in `eq_fields` should operate with fixed values
|
|
90
|
+
if field not in ["region", "exchange", "sector", "industry", "peer_group"]:
|
|
91
|
+
# This should never happen
|
|
92
|
+
raise ValueError(f"Invalid field '{field}' for eq_fields")
|
|
93
|
+
|
|
94
|
+
if field == "region" and value not in VALID_REGIONS:
|
|
95
|
+
raise typer.BadParameter(
|
|
96
|
+
f"Invalid value: '{value}' for 'region' field. Valid values can be found using `yfin screen-query-values --field region`."
|
|
97
|
+
)
|
|
98
|
+
elif field == "exchange" and value not in VALID_EXCHANGES:
|
|
99
|
+
raise typer.BadParameter(
|
|
100
|
+
f"Invalid value: '{value}' for 'exchange' field. Valid values can be found using `yfin screen-query-values --field exchange`."
|
|
101
|
+
)
|
|
102
|
+
elif field == "sector" and value not in VALID_SECTORS:
|
|
103
|
+
raise typer.BadParameter(
|
|
104
|
+
f"Invalid value: '{value}' for 'sector' field. Valid values can be found using `yfin screen-query-values --field sector`."
|
|
105
|
+
)
|
|
106
|
+
elif field == "industry" and value not in VALID_INDUSTRIES:
|
|
107
|
+
raise typer.BadParameter(
|
|
108
|
+
f"Invalid value: '{value}' for 'industry' field. Valid values can be found using `yfin screen-query-values --field industry`."
|
|
109
|
+
)
|
|
110
|
+
elif field == "peer_group" and value not in VALID_PEER_GROUPS:
|
|
111
|
+
raise typer.BadParameter(
|
|
112
|
+
f"Invalid value: '{value}' for 'peer_group' field. Valid values can be found using `yfin screen-query-values --field peer_group`."
|
|
113
|
+
)
|
|
114
|
+
return value
|
|
115
|
+
|
|
116
|
+
# Remaining fields are numeric
|
|
117
|
+
if not is_number(value):
|
|
118
|
+
raise typer.BadParameter(
|
|
119
|
+
f"Invalid value: '{value}' for '{field}' field. Expected a number."
|
|
120
|
+
)
|
|
121
|
+
return float(value)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def parse_filter(filter_str: str) -> yf.EquityQuery:
|
|
125
|
+
"""
|
|
126
|
+
Parse a filter string like '<field> <operator> <value>' into an EquityQuery.
|
|
127
|
+
Example: 'sector eq Technology' -> EquityQuery('eq', ['sector', 'Technology'])
|
|
128
|
+
"""
|
|
129
|
+
parts = filter_str.strip().split(maxsplit=2)
|
|
130
|
+
if len(parts) != 3:
|
|
131
|
+
raise typer.BadParameter(
|
|
132
|
+
f"Invalid filter format: '{filter_str}'. Expected '<field> <operator> <value>'."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
field, operator, value = (parts[0].lower(), parts[1].lower(), parts[2])
|
|
136
|
+
|
|
137
|
+
validate_field(field)
|
|
138
|
+
|
|
139
|
+
if operator not in VALID_OPERATORS:
|
|
140
|
+
raise typer.BadParameter(
|
|
141
|
+
f"Invalid operator: '{operator}'. Use one of {', '.join(sorted(list(VALID_OPERATORS)))}"
|
|
142
|
+
)
|
|
143
|
+
|
|
144
|
+
if operator in ["is-in", "btwn"]:
|
|
145
|
+
# comma-separated values
|
|
146
|
+
values = [parse_value(field, v.strip()) for v in value.split(",")]
|
|
147
|
+
if operator == "btwn" and len(values) != 2:
|
|
148
|
+
raise typer.BadParameter(
|
|
149
|
+
f"Invalid value: '{value}' for '{field}' field. Expected exactly two values."
|
|
150
|
+
)
|
|
151
|
+
else:
|
|
152
|
+
values = [parse_value(field, value)]
|
|
153
|
+
|
|
154
|
+
return yf.EquityQuery(operator, [field] + values)
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
def parse_json_query(json_query: str) -> yf.EquityQuery:
|
|
158
|
+
"""
|
|
159
|
+
Parse a JSON query into an EquityQuery.
|
|
160
|
+
"""
|
|
161
|
+
try:
|
|
162
|
+
query_node = json.loads(json_query)
|
|
163
|
+
except json.JSONDecodeError:
|
|
164
|
+
raise typer.BadParameter(f"Invalid JSON query: '{json_query}'.")
|
|
165
|
+
|
|
166
|
+
def _build_query(node):
|
|
167
|
+
if isinstance(node, str):
|
|
168
|
+
return parse_filter(node)
|
|
169
|
+
|
|
170
|
+
if isinstance(node, dict):
|
|
171
|
+
operator = node.get("operator")
|
|
172
|
+
queries = node.get("queries")
|
|
173
|
+
if operator is None:
|
|
174
|
+
raise typer.BadParameter("Invalid query, 'operator' is required.")
|
|
175
|
+
|
|
176
|
+
if queries is None:
|
|
177
|
+
raise typer.BadParameter("Invalid query, 'queries' is required.")
|
|
178
|
+
|
|
179
|
+
if operator not in ["and", "or"]:
|
|
180
|
+
raise typer.BadParameter(
|
|
181
|
+
"Invalid query, 'operator' must be either 'and' or 'or'."
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
if not isinstance(queries, list) or len(queries) == 0:
|
|
185
|
+
raise typer.BadParameter(
|
|
186
|
+
"Invalid query, 'queries' must be a non-empty list."
|
|
187
|
+
)
|
|
188
|
+
operands = [_build_query(q) for q in queries]
|
|
189
|
+
|
|
190
|
+
# yfinance does not support single operand queries
|
|
191
|
+
if len(operands) == 1:
|
|
192
|
+
return operands[0]
|
|
193
|
+
|
|
194
|
+
return yf.EquityQuery(operator, operands)
|
|
195
|
+
|
|
196
|
+
raise typer.BadParameter(
|
|
197
|
+
f"Invalid query, unexpected type: {type(node).__name__}"
|
|
198
|
+
)
|
|
199
|
+
|
|
200
|
+
return _build_query(query_node)
|
|
201
|
+
|
|
202
|
+
|
|
203
|
+
@command
|
|
204
|
+
def screen(
|
|
205
|
+
filters: ScreenFilterTypeOptional = None,
|
|
206
|
+
predefined: ScreenPredefinedQueryTypeOptional = None,
|
|
207
|
+
json_query: ScreenJsonQueryTypeOptional = None,
|
|
208
|
+
offset: OffsetType = default_offset,
|
|
209
|
+
limit: LimitType = default_limit,
|
|
210
|
+
sort_field: ScreenQueryFieldType = None,
|
|
211
|
+
sort_order: ScreenSortOrderType = default_sort_order,
|
|
212
|
+
):
|
|
213
|
+
"""
|
|
214
|
+
Run a stock screener.
|
|
215
|
+
|
|
216
|
+
Supports predefined queries, simple filters (implicitly ANDed), or complex JSON queries.
|
|
217
|
+
Options --predefined, --filter, and --json-query are mutually exclusive.
|
|
218
|
+
"""
|
|
219
|
+
specified_count = count_specified(filters, predefined, json_query)
|
|
220
|
+
if specified_count != 1:
|
|
221
|
+
raise typer.BadParameter(
|
|
222
|
+
"Exactly one of --filter, --predefined, or --json-query must be specified."
|
|
223
|
+
)
|
|
224
|
+
|
|
225
|
+
if predefined:
|
|
226
|
+
if predefined not in PREDEFINED_QUERIES:
|
|
227
|
+
raise typer.BadParameter(
|
|
228
|
+
f"Invalid predefined query: '{predefined}'. Valid predefined queries can be found using `yfin screen-predefined-queries`."
|
|
229
|
+
)
|
|
230
|
+
final_query = predefined
|
|
231
|
+
elif filters:
|
|
232
|
+
queries = [parse_filter(f) for f in filters]
|
|
233
|
+
final_query = (
|
|
234
|
+
queries[0] if len(queries) == 1 else yf.EquityQuery("and", queries)
|
|
235
|
+
)
|
|
236
|
+
else:
|
|
237
|
+
final_query = parse_json_query(json_query)
|
|
238
|
+
|
|
239
|
+
if sort_field:
|
|
240
|
+
validate_field(sort_field)
|
|
241
|
+
|
|
242
|
+
# yfinance docs say that size is only for custom queries, count is only for predefined queries
|
|
243
|
+
if predefined:
|
|
244
|
+
response = yf.screen(
|
|
245
|
+
final_query,
|
|
246
|
+
offset=offset,
|
|
247
|
+
count=limit,
|
|
248
|
+
sortField=sort_field,
|
|
249
|
+
sortAsc=sort_order == "asc",
|
|
250
|
+
)
|
|
251
|
+
else:
|
|
252
|
+
response = yf.screen(
|
|
253
|
+
final_query,
|
|
254
|
+
offset=offset,
|
|
255
|
+
size=limit,
|
|
256
|
+
sortField=sort_field,
|
|
257
|
+
sortAsc=sort_order == "asc",
|
|
258
|
+
)
|
|
259
|
+
|
|
260
|
+
return response["quotes"]
|
src/commands/sector.py
ADDED
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import yfinance as yf
|
|
2
|
+
from ..typer import SectorKeyType
|
|
3
|
+
from ..decorators import command
|
|
4
|
+
from ..utils import data_frame_to_list
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
@command
|
|
8
|
+
def sector_keys():
|
|
9
|
+
"""
|
|
10
|
+
Get all available sector keys.
|
|
11
|
+
"""
|
|
12
|
+
return [{"key": k} for k in yf.const.SECTOR_INDUSTY_MAPPING_LC.keys()]
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
@command
|
|
16
|
+
def sector_industries(key: SectorKeyType):
|
|
17
|
+
"""
|
|
18
|
+
Get the industries within a sector.
|
|
19
|
+
"""
|
|
20
|
+
return data_frame_to_list(yf.Sector(key).industries)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@command
|
|
24
|
+
def sector_overview(key: SectorKeyType):
|
|
25
|
+
"""
|
|
26
|
+
Get the overview information of the domain entity.
|
|
27
|
+
"""
|
|
28
|
+
return yf.Sector(key).overview
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
@command
|
|
32
|
+
def sector_research_reports(key: SectorKeyType):
|
|
33
|
+
"""
|
|
34
|
+
Get research reports related to the domain entity.
|
|
35
|
+
"""
|
|
36
|
+
return yf.Sector(key).research_reports
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
@command
|
|
40
|
+
def sector_top_companies(key: SectorKeyType):
|
|
41
|
+
"""
|
|
42
|
+
Get the top companies within the domain entity.
|
|
43
|
+
"""
|
|
44
|
+
return data_frame_to_list(yf.Sector(key).top_companies)
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
@command
|
|
48
|
+
def sector_top_etfs(key: SectorKeyType):
|
|
49
|
+
"""
|
|
50
|
+
Get the top ETFs for the sector.
|
|
51
|
+
"""
|
|
52
|
+
return [
|
|
53
|
+
{"symbol": symbol, "name": name}
|
|
54
|
+
for symbol, name in yf.Sector(key).top_etfs.items()
|
|
55
|
+
]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@command
|
|
59
|
+
def sector_top_mutual_funds(key: SectorKeyType):
|
|
60
|
+
"""
|
|
61
|
+
Get the top mutual funds for the sector.
|
|
62
|
+
"""
|
|
63
|
+
return [
|
|
64
|
+
{"symbol": symbol, "name": name}
|
|
65
|
+
for symbol, name in yf.Sector(key).top_mutual_funds.items()
|
|
66
|
+
]
|
src/commands/stock.py
ADDED
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import yfinance as yf
|
|
2
|
+
import typer
|
|
3
|
+
from ..typer import (
|
|
4
|
+
TickerType,
|
|
5
|
+
IntervalType,
|
|
6
|
+
default_interval,
|
|
7
|
+
StartDateTypeOptional,
|
|
8
|
+
EndDateTypeOptional,
|
|
9
|
+
PeriodTypeOptional,
|
|
10
|
+
PeriodType,
|
|
11
|
+
default_period,
|
|
12
|
+
NewsTabType,
|
|
13
|
+
default_news_tab,
|
|
14
|
+
CountType,
|
|
15
|
+
default_count,
|
|
16
|
+
)
|
|
17
|
+
from ..decorators import command
|
|
18
|
+
from ..utils import count_specified, compact, data_frame_to_list, series_to_list
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
@command
|
|
22
|
+
def history(
|
|
23
|
+
ticker: TickerType,
|
|
24
|
+
interval: IntervalType = default_interval,
|
|
25
|
+
period: PeriodTypeOptional = None,
|
|
26
|
+
start: StartDateTypeOptional = None,
|
|
27
|
+
end: EndDateTypeOptional = None,
|
|
28
|
+
):
|
|
29
|
+
"""
|
|
30
|
+
Get historical market data for a stock ticker.
|
|
31
|
+
|
|
32
|
+
Note: period, start, and end - at most 2 of these can be specified together.
|
|
33
|
+
"""
|
|
34
|
+
specified_count = count_specified(period, start, end)
|
|
35
|
+
if specified_count > 2:
|
|
36
|
+
raise typer.BadParameter(
|
|
37
|
+
"At most 2 of --period, --start, --end can be specified together."
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
if specified_count == 0:
|
|
41
|
+
period = "1mo"
|
|
42
|
+
|
|
43
|
+
kwargs = compact(
|
|
44
|
+
interval=interval,
|
|
45
|
+
period=period,
|
|
46
|
+
start=start,
|
|
47
|
+
end=end,
|
|
48
|
+
)
|
|
49
|
+
|
|
50
|
+
stock = yf.Ticker(ticker)
|
|
51
|
+
data_frame = stock.history(**kwargs)
|
|
52
|
+
return data_frame_to_list(data_frame)
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
@command
|
|
56
|
+
def dividends(
|
|
57
|
+
ticker: TickerType,
|
|
58
|
+
period: PeriodType = default_period,
|
|
59
|
+
):
|
|
60
|
+
"""
|
|
61
|
+
Get dividends for a stock ticker.
|
|
62
|
+
"""
|
|
63
|
+
stock = yf.Ticker(ticker)
|
|
64
|
+
series = stock.get_dividends(period=period)
|
|
65
|
+
return series_to_list(series)
|
|
66
|
+
|
|
67
|
+
|
|
68
|
+
@command
|
|
69
|
+
def fast_info(
|
|
70
|
+
ticker: TickerType,
|
|
71
|
+
):
|
|
72
|
+
"""
|
|
73
|
+
Get fast info (15 min delayed) summary for a stock ticker.
|
|
74
|
+
|
|
75
|
+
Returns key metrics like price, market cap, volume, and 52-week range.
|
|
76
|
+
"""
|
|
77
|
+
stock = yf.Ticker(ticker)
|
|
78
|
+
fast_info = stock.get_fast_info()
|
|
79
|
+
if fast_info is None:
|
|
80
|
+
return None
|
|
81
|
+
fast_info_dict = dict(fast_info) # fast_info is a dict-like object
|
|
82
|
+
return fast_info_dict
|
|
83
|
+
|
|
84
|
+
|
|
85
|
+
@command
|
|
86
|
+
def news(
|
|
87
|
+
ticker: TickerType,
|
|
88
|
+
count: CountType = default_count,
|
|
89
|
+
tab: NewsTabType = default_news_tab,
|
|
90
|
+
):
|
|
91
|
+
"""
|
|
92
|
+
Get news for a stock ticker.
|
|
93
|
+
"""
|
|
94
|
+
stock = yf.Ticker(ticker)
|
|
95
|
+
news_list = stock.get_news(count, tab)
|
|
96
|
+
processed_news_list = []
|
|
97
|
+
for article in news_list:
|
|
98
|
+
content = article.get("content") or {}
|
|
99
|
+
processed_news_list.append(
|
|
100
|
+
{
|
|
101
|
+
"Date": content.get("pubDate"),
|
|
102
|
+
"Title": content.get("title"),
|
|
103
|
+
"Summary": content.get("summary"),
|
|
104
|
+
"URL": (content.get("canonicalUrl") or {}).get("url"),
|
|
105
|
+
"Source": (content.get("provider") or {}).get("displayName"),
|
|
106
|
+
}
|
|
107
|
+
)
|
|
108
|
+
return processed_news_list
|
src/decorators.py
ADDED
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
import click
|
|
3
|
+
from functools import wraps
|
|
4
|
+
from .writer import WriterFactory
|
|
5
|
+
from .utils import console_print_error, console_print_warning
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def command(func):
|
|
9
|
+
"""Decorator to handle standard errors and output writing in CLI commands."""
|
|
10
|
+
|
|
11
|
+
@wraps(func)
|
|
12
|
+
def wrapper(*args, **kwargs):
|
|
13
|
+
try:
|
|
14
|
+
data = func(*args, **kwargs)
|
|
15
|
+
|
|
16
|
+
if data is None:
|
|
17
|
+
console_print_warning("No data found")
|
|
18
|
+
raise typer.Exit(code=1)
|
|
19
|
+
|
|
20
|
+
if not isinstance(data, (dict, list)):
|
|
21
|
+
raise ValueError(f"Unsupported data type: {type(data).__name__}")
|
|
22
|
+
|
|
23
|
+
ctx = click.get_current_context()
|
|
24
|
+
output_type = ctx.obj.get("output")
|
|
25
|
+
writer = WriterFactory.get_writer(output_type)
|
|
26
|
+
writer.write(data)
|
|
27
|
+
except (typer.Exit, typer.Abort, typer.BadParameter):
|
|
28
|
+
raise
|
|
29
|
+
except Exception as e:
|
|
30
|
+
console_print_error(f"Unexpected error: {e}")
|
|
31
|
+
raise typer.Exit(code=1)
|
|
32
|
+
|
|
33
|
+
return wrapper
|
src/typer.py
ADDED
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
import typer
|
|
2
|
+
from typing import Annotated
|
|
3
|
+
from .utils import (
|
|
4
|
+
get_today_date_string,
|
|
5
|
+
validate_date_string,
|
|
6
|
+
validate_value_in_list,
|
|
7
|
+
)
|
|
8
|
+
|
|
9
|
+
TickerType = Annotated[
|
|
10
|
+
str,
|
|
11
|
+
typer.Argument(
|
|
12
|
+
help="Stock ticker symbol (e.g., TSLA, AAPL)", callback=lambda x: x.upper()
|
|
13
|
+
),
|
|
14
|
+
]
|
|
15
|
+
|
|
16
|
+
VALID_OUTPUT_TYPES = ["json", "table"]
|
|
17
|
+
|
|
18
|
+
default_output = VALID_OUTPUT_TYPES[0]
|
|
19
|
+
|
|
20
|
+
OutputType = Annotated[
|
|
21
|
+
str,
|
|
22
|
+
typer.Option(
|
|
23
|
+
callback=validate_value_in_list(VALID_OUTPUT_TYPES),
|
|
24
|
+
help=f"Output format ({', '.join(VALID_OUTPUT_TYPES)})",
|
|
25
|
+
),
|
|
26
|
+
]
|
|
27
|
+
|
|
28
|
+
default_frequency = "yearly"
|
|
29
|
+
|
|
30
|
+
FrequencyType = Annotated[
|
|
31
|
+
str,
|
|
32
|
+
typer.Option(
|
|
33
|
+
callback=validate_value_in_list(["yearly", "quarterly"]),
|
|
34
|
+
help="Frequency of the data (yearly, quarterly)",
|
|
35
|
+
),
|
|
36
|
+
]
|
|
37
|
+
|
|
38
|
+
ExtendedFrequencyType = Annotated[
|
|
39
|
+
str,
|
|
40
|
+
typer.Option(
|
|
41
|
+
callback=validate_value_in_list(["yearly", "quarterly", "trailing"]),
|
|
42
|
+
help="Frequency of the data (yearly, quarterly, trailing)",
|
|
43
|
+
),
|
|
44
|
+
]
|
|
45
|
+
|
|
46
|
+
StartDateType = Annotated[
|
|
47
|
+
str,
|
|
48
|
+
typer.Option(
|
|
49
|
+
default_factory=get_today_date_string,
|
|
50
|
+
callback=validate_date_string,
|
|
51
|
+
help="Start date (YYYY-MM-DD), default today",
|
|
52
|
+
),
|
|
53
|
+
]
|
|
54
|
+
|
|
55
|
+
StartDateTypeOptional = Annotated[
|
|
56
|
+
str | None,
|
|
57
|
+
typer.Option(
|
|
58
|
+
callback=lambda x: validate_date_string(x) if x else None,
|
|
59
|
+
help="Start date (YYYY-MM-DD)",
|
|
60
|
+
),
|
|
61
|
+
]
|
|
62
|
+
|
|
63
|
+
EndDateType = Annotated[
|
|
64
|
+
str | None,
|
|
65
|
+
typer.Option(
|
|
66
|
+
callback=lambda x: validate_date_string(x) if x else None,
|
|
67
|
+
help="End date (YYYY-MM-DD), default 7 days from start",
|
|
68
|
+
),
|
|
69
|
+
]
|
|
70
|
+
|
|
71
|
+
EndDateTypeOptional = Annotated[
|
|
72
|
+
str | None,
|
|
73
|
+
typer.Option(
|
|
74
|
+
callback=lambda x: validate_date_string(x) if x else None,
|
|
75
|
+
help="End date (YYYY-MM-DD)",
|
|
76
|
+
),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
VALID_PERIODS = ["1d", "5d", "1mo", "3mo", "6mo", "1y", "2y", "5y", "10y", "ytd", "max"]
|
|
80
|
+
|
|
81
|
+
default_period = VALID_PERIODS[-1] # max
|
|
82
|
+
|
|
83
|
+
|
|
84
|
+
PeriodType = Annotated[
|
|
85
|
+
str,
|
|
86
|
+
typer.Option(
|
|
87
|
+
callback=validate_value_in_list(VALID_PERIODS),
|
|
88
|
+
help=f"Data period {', '.join(VALID_PERIODS)}",
|
|
89
|
+
),
|
|
90
|
+
]
|
|
91
|
+
|
|
92
|
+
PeriodTypeOptional = Annotated[
|
|
93
|
+
str | None,
|
|
94
|
+
typer.Option(
|
|
95
|
+
callback=lambda x: validate_value_in_list(VALID_PERIODS)(x) if x else None,
|
|
96
|
+
help=f"Data period {', '.join(VALID_PERIODS)}",
|
|
97
|
+
),
|
|
98
|
+
]
|
|
99
|
+
|
|
100
|
+
default_limit = 12
|
|
101
|
+
|
|
102
|
+
LimitType = Annotated[
|
|
103
|
+
int,
|
|
104
|
+
typer.Option(
|
|
105
|
+
help="Maximum number of results to show",
|
|
106
|
+
),
|
|
107
|
+
]
|
|
108
|
+
|
|
109
|
+
default_offset = 0
|
|
110
|
+
|
|
111
|
+
OffsetType = Annotated[
|
|
112
|
+
int,
|
|
113
|
+
typer.Option(
|
|
114
|
+
help="Offset of the results",
|
|
115
|
+
),
|
|
116
|
+
]
|
|
117
|
+
|
|
118
|
+
default_market_cap = 0
|
|
119
|
+
|
|
120
|
+
MarketCapType = Annotated[
|
|
121
|
+
int,
|
|
122
|
+
typer.Option(
|
|
123
|
+
help="Market cap of the company",
|
|
124
|
+
),
|
|
125
|
+
]
|
|
126
|
+
|
|
127
|
+
VALID_INTERVALS = [
|
|
128
|
+
"1m",
|
|
129
|
+
"2m",
|
|
130
|
+
"5m",
|
|
131
|
+
"15m",
|
|
132
|
+
"30m",
|
|
133
|
+
"60m",
|
|
134
|
+
"90m",
|
|
135
|
+
"1h",
|
|
136
|
+
"1d",
|
|
137
|
+
"5d",
|
|
138
|
+
"1wk",
|
|
139
|
+
"1mo",
|
|
140
|
+
"3mo",
|
|
141
|
+
]
|
|
142
|
+
|
|
143
|
+
default_interval = VALID_INTERVALS[8] # "1d"
|
|
144
|
+
|
|
145
|
+
IntervalType = Annotated[
|
|
146
|
+
str,
|
|
147
|
+
typer.Option(
|
|
148
|
+
callback=validate_value_in_list(VALID_INTERVALS),
|
|
149
|
+
help=f"Data interval ({', '.join(VALID_INTERVALS)})",
|
|
150
|
+
),
|
|
151
|
+
]
|
|
152
|
+
|
|
153
|
+
default_count = 10
|
|
154
|
+
|
|
155
|
+
CountType = Annotated[
|
|
156
|
+
int,
|
|
157
|
+
typer.Option(
|
|
158
|
+
help="Number of results to show",
|
|
159
|
+
),
|
|
160
|
+
]
|
|
161
|
+
|
|
162
|
+
default_news_tab = "all"
|
|
163
|
+
|
|
164
|
+
NewsTabType = Annotated[
|
|
165
|
+
str,
|
|
166
|
+
typer.Option(
|
|
167
|
+
callback=validate_value_in_list(["all", "news", "press releases"]),
|
|
168
|
+
help="Tab of the news (all, news, press releases)",
|
|
169
|
+
),
|
|
170
|
+
]
|
|
171
|
+
|
|
172
|
+
SectorKeyType = Annotated[
|
|
173
|
+
str,
|
|
174
|
+
typer.Argument(
|
|
175
|
+
help="A valid sector key, can be found using `yfin sector-keys`",
|
|
176
|
+
),
|
|
177
|
+
]
|
|
178
|
+
|
|
179
|
+
IndustryKeyType = Annotated[
|
|
180
|
+
str,
|
|
181
|
+
typer.Argument(
|
|
182
|
+
help="A valid industry key, can be found using `yfin sector-industries <sector-key>`",
|
|
183
|
+
),
|
|
184
|
+
]
|
|
185
|
+
|
|
186
|
+
ScreenFilterTypeOptional = Annotated[
|
|
187
|
+
list[str] | None,
|
|
188
|
+
typer.Option(
|
|
189
|
+
"--filter", # parameter name is plural
|
|
190
|
+
help="Filter in '<field> <operator> <value>' format (e.g., 'sector eq Technology')",
|
|
191
|
+
),
|
|
192
|
+
]
|
|
193
|
+
|
|
194
|
+
ScreenPredefinedQueryTypeOptional = Annotated[
|
|
195
|
+
str | None,
|
|
196
|
+
typer.Option(
|
|
197
|
+
help="Predefined query, can be found using `yfin screen-predefined-queries`",
|
|
198
|
+
),
|
|
199
|
+
]
|
|
200
|
+
|
|
201
|
+
ScreenJsonQueryTypeOptional = Annotated[
|
|
202
|
+
str | None,
|
|
203
|
+
typer.Option(
|
|
204
|
+
help="Complex query in JSON format: {'operator': 'and|or', 'queries': [<filter1>, <filter2>, <json_query>, ...]}.",
|
|
205
|
+
),
|
|
206
|
+
]
|
|
207
|
+
|
|
208
|
+
ScreenQueryFieldType = Annotated[
|
|
209
|
+
str | None,
|
|
210
|
+
typer.Option(
|
|
211
|
+
help="Query field, can be found using `yfin screen-query-fields`",
|
|
212
|
+
),
|
|
213
|
+
]
|
|
214
|
+
|
|
215
|
+
default_sort_order = "desc"
|
|
216
|
+
|
|
217
|
+
ScreenSortOrderType = Annotated[
|
|
218
|
+
str,
|
|
219
|
+
typer.Option(
|
|
220
|
+
callback=validate_value_in_list(["asc", "desc"]),
|
|
221
|
+
help="Sort order, can be 'asc' or 'desc'",
|
|
222
|
+
),
|
|
223
|
+
]
|