@heylemon/lemonade 0.2.2 → 0.2.4
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.
- package/dist/build-info.json +3 -3
- package/dist/canvas-host/a2ui/.bundle.hash +1 -1
- package/package.json +1 -1
- package/skills/frontend-design/SKILL.md +39 -0
- package/skills/self-improving-agent/SKILL.md +128 -0
- package/skills/stock-analysis/SKILL.md +131 -0
- package/skills/stock-analysis/scripts/analyze_stock.py +2532 -0
- package/skills/stock-analysis/scripts/dividends.py +365 -0
- package/skills/stock-analysis/scripts/hot_scanner.py +565 -0
- package/skills/stock-analysis/scripts/portfolio.py +528 -0
- package/skills/stock-analysis/scripts/rumor_scanner.py +330 -0
- package/skills/stock-analysis/scripts/watchlist.py +318 -0
- package/skills/youtube-watcher/SKILL.md +51 -0
- package/skills/youtube-watcher/scripts/get_transcript.py +81 -0
|
@@ -0,0 +1,365 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "yfinance>=0.2.40",
|
|
6
|
+
# "pandas>=2.0.0",
|
|
7
|
+
# ]
|
|
8
|
+
# ///
|
|
9
|
+
"""
|
|
10
|
+
Dividend Analysis Module.
|
|
11
|
+
|
|
12
|
+
Analyzes dividend metrics for income investors:
|
|
13
|
+
- Dividend Yield
|
|
14
|
+
- Payout Ratio
|
|
15
|
+
- Dividend Growth Rate (5Y CAGR)
|
|
16
|
+
- Dividend Safety Score
|
|
17
|
+
- Ex-Dividend Date
|
|
18
|
+
|
|
19
|
+
Usage:
|
|
20
|
+
uv run dividends.py AAPL
|
|
21
|
+
uv run dividends.py JNJ PG KO --output json
|
|
22
|
+
"""
|
|
23
|
+
|
|
24
|
+
import argparse
|
|
25
|
+
import json
|
|
26
|
+
import sys
|
|
27
|
+
from dataclasses import dataclass, asdict
|
|
28
|
+
from datetime import datetime
|
|
29
|
+
|
|
30
|
+
import pandas as pd
|
|
31
|
+
import yfinance as yf
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
@dataclass
|
|
35
|
+
class DividendAnalysis:
|
|
36
|
+
ticker: str
|
|
37
|
+
company_name: str
|
|
38
|
+
|
|
39
|
+
# Basic metrics
|
|
40
|
+
dividend_yield: float | None # Annual yield %
|
|
41
|
+
annual_dividend: float | None # Annual dividend per share
|
|
42
|
+
current_price: float | None
|
|
43
|
+
|
|
44
|
+
# Payout analysis
|
|
45
|
+
payout_ratio: float | None # Dividend / EPS
|
|
46
|
+
payout_status: str # "safe", "moderate", "high", "unsustainable"
|
|
47
|
+
|
|
48
|
+
# Growth
|
|
49
|
+
dividend_growth_5y: float | None # 5-year CAGR %
|
|
50
|
+
consecutive_years: int | None # Years of consecutive increases
|
|
51
|
+
dividend_history: list[dict] | None # Last 5 years
|
|
52
|
+
|
|
53
|
+
# Timing
|
|
54
|
+
ex_dividend_date: str | None
|
|
55
|
+
payment_frequency: str | None # "quarterly", "monthly", "annual"
|
|
56
|
+
|
|
57
|
+
# Safety score (0-100)
|
|
58
|
+
safety_score: int
|
|
59
|
+
safety_factors: list[str]
|
|
60
|
+
|
|
61
|
+
# Verdict
|
|
62
|
+
income_rating: str # "excellent", "good", "moderate", "poor", "no_dividend"
|
|
63
|
+
summary: str
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def analyze_dividends(ticker: str, verbose: bool = False) -> DividendAnalysis | None:
|
|
67
|
+
"""Analyze dividend metrics for a stock."""
|
|
68
|
+
try:
|
|
69
|
+
stock = yf.Ticker(ticker)
|
|
70
|
+
info = stock.info
|
|
71
|
+
|
|
72
|
+
company_name = info.get("longName") or info.get("shortName") or ticker
|
|
73
|
+
current_price = info.get("regularMarketPrice") or info.get("currentPrice")
|
|
74
|
+
|
|
75
|
+
# Basic dividend info
|
|
76
|
+
dividend_yield = info.get("dividendYield")
|
|
77
|
+
if dividend_yield:
|
|
78
|
+
dividend_yield = dividend_yield * 100 # Convert to percentage
|
|
79
|
+
|
|
80
|
+
annual_dividend = info.get("dividendRate")
|
|
81
|
+
|
|
82
|
+
# No dividend
|
|
83
|
+
if not annual_dividend or annual_dividend == 0:
|
|
84
|
+
return DividendAnalysis(
|
|
85
|
+
ticker=ticker,
|
|
86
|
+
company_name=company_name,
|
|
87
|
+
dividend_yield=None,
|
|
88
|
+
annual_dividend=None,
|
|
89
|
+
current_price=current_price,
|
|
90
|
+
payout_ratio=None,
|
|
91
|
+
payout_status="no_dividend",
|
|
92
|
+
dividend_growth_5y=None,
|
|
93
|
+
consecutive_years=None,
|
|
94
|
+
dividend_history=None,
|
|
95
|
+
ex_dividend_date=None,
|
|
96
|
+
payment_frequency=None,
|
|
97
|
+
safety_score=0,
|
|
98
|
+
safety_factors=["No dividend paid"],
|
|
99
|
+
income_rating="no_dividend",
|
|
100
|
+
summary=f"{ticker} does not pay a dividend.",
|
|
101
|
+
)
|
|
102
|
+
|
|
103
|
+
# Payout ratio
|
|
104
|
+
trailing_eps = info.get("trailingEps")
|
|
105
|
+
payout_ratio = None
|
|
106
|
+
payout_status = "unknown"
|
|
107
|
+
|
|
108
|
+
if trailing_eps and trailing_eps > 0 and annual_dividend:
|
|
109
|
+
payout_ratio = (annual_dividend / trailing_eps) * 100
|
|
110
|
+
|
|
111
|
+
if payout_ratio < 40:
|
|
112
|
+
payout_status = "safe"
|
|
113
|
+
elif payout_ratio < 60:
|
|
114
|
+
payout_status = "moderate"
|
|
115
|
+
elif payout_ratio < 80:
|
|
116
|
+
payout_status = "high"
|
|
117
|
+
else:
|
|
118
|
+
payout_status = "unsustainable"
|
|
119
|
+
|
|
120
|
+
# Dividend history (for growth calculation)
|
|
121
|
+
dividends = stock.dividends
|
|
122
|
+
dividend_history = None
|
|
123
|
+
dividend_growth_5y = None
|
|
124
|
+
consecutive_years = None
|
|
125
|
+
|
|
126
|
+
if dividends is not None and len(dividends) > 0:
|
|
127
|
+
# Group by year
|
|
128
|
+
dividends_df = dividends.reset_index()
|
|
129
|
+
dividends_df["Year"] = pd.to_datetime(dividends_df["Date"]).dt.year
|
|
130
|
+
yearly = dividends_df.groupby("Year")["Dividends"].sum().sort_index(ascending=False)
|
|
131
|
+
|
|
132
|
+
# Last 5 years history
|
|
133
|
+
dividend_history = []
|
|
134
|
+
for year in yearly.head(5).index:
|
|
135
|
+
dividend_history.append({
|
|
136
|
+
"year": int(year),
|
|
137
|
+
"total": round(float(yearly[year]), 4),
|
|
138
|
+
})
|
|
139
|
+
|
|
140
|
+
# Calculate 5-year CAGR
|
|
141
|
+
if len(yearly) >= 5:
|
|
142
|
+
current_div = yearly.iloc[0]
|
|
143
|
+
div_5y_ago = yearly.iloc[4]
|
|
144
|
+
|
|
145
|
+
if div_5y_ago > 0 and current_div > 0:
|
|
146
|
+
dividend_growth_5y = ((current_div / div_5y_ago) ** (1/5) - 1) * 100
|
|
147
|
+
|
|
148
|
+
# Count consecutive years of increases
|
|
149
|
+
consecutive_years = 0
|
|
150
|
+
prev_div = None
|
|
151
|
+
for div in yearly.values:
|
|
152
|
+
if prev_div is not None:
|
|
153
|
+
if div >= prev_div:
|
|
154
|
+
consecutive_years += 1
|
|
155
|
+
else:
|
|
156
|
+
break
|
|
157
|
+
prev_div = div
|
|
158
|
+
|
|
159
|
+
# Ex-dividend date
|
|
160
|
+
ex_dividend_date = info.get("exDividendDate")
|
|
161
|
+
if ex_dividend_date:
|
|
162
|
+
ex_dividend_date = datetime.fromtimestamp(ex_dividend_date).strftime("%Y-%m-%d")
|
|
163
|
+
|
|
164
|
+
# Payment frequency
|
|
165
|
+
payment_frequency = None
|
|
166
|
+
if dividends is not None and len(dividends) >= 4:
|
|
167
|
+
# Count dividends in last year
|
|
168
|
+
one_year_ago = pd.Timestamp.now() - pd.DateOffset(years=1)
|
|
169
|
+
recent_divs = dividends[dividends.index > one_year_ago]
|
|
170
|
+
count = len(recent_divs)
|
|
171
|
+
|
|
172
|
+
if count >= 10:
|
|
173
|
+
payment_frequency = "monthly"
|
|
174
|
+
elif count >= 3:
|
|
175
|
+
payment_frequency = "quarterly"
|
|
176
|
+
elif count >= 1:
|
|
177
|
+
payment_frequency = "annual"
|
|
178
|
+
|
|
179
|
+
# Safety score calculation (0-100)
|
|
180
|
+
safety_score = 50 # Base score
|
|
181
|
+
safety_factors = []
|
|
182
|
+
|
|
183
|
+
# Payout ratio factor (+/- 20)
|
|
184
|
+
if payout_ratio:
|
|
185
|
+
if payout_ratio < 40:
|
|
186
|
+
safety_score += 20
|
|
187
|
+
safety_factors.append(f"Low payout ratio ({payout_ratio:.0f}%)")
|
|
188
|
+
elif payout_ratio < 60:
|
|
189
|
+
safety_score += 10
|
|
190
|
+
safety_factors.append(f"Moderate payout ratio ({payout_ratio:.0f}%)")
|
|
191
|
+
elif payout_ratio < 80:
|
|
192
|
+
safety_score -= 10
|
|
193
|
+
safety_factors.append(f"High payout ratio ({payout_ratio:.0f}%)")
|
|
194
|
+
else:
|
|
195
|
+
safety_score -= 20
|
|
196
|
+
safety_factors.append(f"Unsustainable payout ratio ({payout_ratio:.0f}%)")
|
|
197
|
+
|
|
198
|
+
# Growth factor (+/- 15)
|
|
199
|
+
if dividend_growth_5y:
|
|
200
|
+
if dividend_growth_5y > 10:
|
|
201
|
+
safety_score += 15
|
|
202
|
+
safety_factors.append(f"Strong dividend growth ({dividend_growth_5y:.1f}% CAGR)")
|
|
203
|
+
elif dividend_growth_5y > 5:
|
|
204
|
+
safety_score += 10
|
|
205
|
+
safety_factors.append(f"Good dividend growth ({dividend_growth_5y:.1f}% CAGR)")
|
|
206
|
+
elif dividend_growth_5y > 0:
|
|
207
|
+
safety_score += 5
|
|
208
|
+
safety_factors.append(f"Positive dividend growth ({dividend_growth_5y:.1f}% CAGR)")
|
|
209
|
+
else:
|
|
210
|
+
safety_score -= 15
|
|
211
|
+
safety_factors.append(f"Dividend declining ({dividend_growth_5y:.1f}% CAGR)")
|
|
212
|
+
|
|
213
|
+
# Consecutive years factor (+/- 15)
|
|
214
|
+
if consecutive_years:
|
|
215
|
+
if consecutive_years >= 25:
|
|
216
|
+
safety_score += 15
|
|
217
|
+
safety_factors.append(f"Dividend Aristocrat ({consecutive_years}+ years)")
|
|
218
|
+
elif consecutive_years >= 10:
|
|
219
|
+
safety_score += 10
|
|
220
|
+
safety_factors.append(f"Long dividend history ({consecutive_years} years)")
|
|
221
|
+
elif consecutive_years >= 5:
|
|
222
|
+
safety_score += 5
|
|
223
|
+
safety_factors.append(f"Consistent dividend ({consecutive_years} years)")
|
|
224
|
+
|
|
225
|
+
# Yield factor (high yield can be risky)
|
|
226
|
+
if dividend_yield:
|
|
227
|
+
if dividend_yield > 8:
|
|
228
|
+
safety_score -= 10
|
|
229
|
+
safety_factors.append(f"Very high yield ({dividend_yield:.1f}%) - verify sustainability")
|
|
230
|
+
elif dividend_yield < 1:
|
|
231
|
+
safety_factors.append(f"Low yield ({dividend_yield:.2f}%)")
|
|
232
|
+
|
|
233
|
+
# Clamp score
|
|
234
|
+
safety_score = max(0, min(100, safety_score))
|
|
235
|
+
|
|
236
|
+
# Income rating
|
|
237
|
+
if safety_score >= 80:
|
|
238
|
+
income_rating = "excellent"
|
|
239
|
+
elif safety_score >= 60:
|
|
240
|
+
income_rating = "good"
|
|
241
|
+
elif safety_score >= 40:
|
|
242
|
+
income_rating = "moderate"
|
|
243
|
+
else:
|
|
244
|
+
income_rating = "poor"
|
|
245
|
+
|
|
246
|
+
# Summary
|
|
247
|
+
summary_parts = []
|
|
248
|
+
if dividend_yield:
|
|
249
|
+
summary_parts.append(f"{dividend_yield:.2f}% yield")
|
|
250
|
+
if payout_ratio:
|
|
251
|
+
summary_parts.append(f"{payout_ratio:.0f}% payout")
|
|
252
|
+
if dividend_growth_5y:
|
|
253
|
+
summary_parts.append(f"{dividend_growth_5y:+.1f}% 5Y growth")
|
|
254
|
+
if consecutive_years and consecutive_years >= 5:
|
|
255
|
+
summary_parts.append(f"{consecutive_years}Y streak")
|
|
256
|
+
|
|
257
|
+
summary = f"{ticker}: {', '.join(summary_parts)}. Rating: {income_rating.upper()}"
|
|
258
|
+
|
|
259
|
+
return DividendAnalysis(
|
|
260
|
+
ticker=ticker,
|
|
261
|
+
company_name=company_name,
|
|
262
|
+
dividend_yield=round(dividend_yield, 2) if dividend_yield else None,
|
|
263
|
+
annual_dividend=round(annual_dividend, 4) if annual_dividend else None,
|
|
264
|
+
current_price=current_price,
|
|
265
|
+
payout_ratio=round(payout_ratio, 1) if payout_ratio else None,
|
|
266
|
+
payout_status=payout_status,
|
|
267
|
+
dividend_growth_5y=round(dividend_growth_5y, 2) if dividend_growth_5y else None,
|
|
268
|
+
consecutive_years=consecutive_years,
|
|
269
|
+
dividend_history=dividend_history,
|
|
270
|
+
ex_dividend_date=ex_dividend_date,
|
|
271
|
+
payment_frequency=payment_frequency,
|
|
272
|
+
safety_score=safety_score,
|
|
273
|
+
safety_factors=safety_factors,
|
|
274
|
+
income_rating=income_rating,
|
|
275
|
+
summary=summary,
|
|
276
|
+
)
|
|
277
|
+
|
|
278
|
+
except Exception as e:
|
|
279
|
+
if verbose:
|
|
280
|
+
print(f"Error analyzing {ticker}: {e}", file=sys.stderr)
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
|
|
284
|
+
def format_text(analysis: DividendAnalysis) -> str:
|
|
285
|
+
"""Format dividend analysis as text."""
|
|
286
|
+
lines = [
|
|
287
|
+
"=" * 60,
|
|
288
|
+
f"DIVIDEND ANALYSIS: {analysis.ticker} ({analysis.company_name})",
|
|
289
|
+
"=" * 60,
|
|
290
|
+
"",
|
|
291
|
+
]
|
|
292
|
+
|
|
293
|
+
if analysis.income_rating == "no_dividend":
|
|
294
|
+
lines.append("This stock does not pay a dividend.")
|
|
295
|
+
lines.append("=" * 60)
|
|
296
|
+
return "\n".join(lines)
|
|
297
|
+
|
|
298
|
+
# Yield & Price
|
|
299
|
+
lines.append(f"Current Price: ${analysis.current_price:.2f}")
|
|
300
|
+
lines.append(f"Annual Dividend: ${analysis.annual_dividend:.2f}")
|
|
301
|
+
lines.append(f"Dividend Yield: {analysis.dividend_yield:.2f}%")
|
|
302
|
+
lines.append(f"Payment Freq: {analysis.payment_frequency or 'Unknown'}")
|
|
303
|
+
if analysis.ex_dividend_date:
|
|
304
|
+
lines.append(f"Ex-Dividend: {analysis.ex_dividend_date}")
|
|
305
|
+
|
|
306
|
+
lines.append("")
|
|
307
|
+
|
|
308
|
+
# Payout & Safety
|
|
309
|
+
lines.append(f"Payout Ratio: {analysis.payout_ratio:.1f}% ({analysis.payout_status})")
|
|
310
|
+
lines.append(f"5Y Div Growth: {analysis.dividend_growth_5y:+.1f}%" if analysis.dividend_growth_5y else "5Y Div Growth: N/A")
|
|
311
|
+
if analysis.consecutive_years:
|
|
312
|
+
lines.append(f"Consecutive Yrs: {analysis.consecutive_years}")
|
|
313
|
+
|
|
314
|
+
lines.append("")
|
|
315
|
+
lines.append(f"SAFETY SCORE: {analysis.safety_score}/100")
|
|
316
|
+
lines.append(f"INCOME RATING: {analysis.income_rating.upper()}")
|
|
317
|
+
|
|
318
|
+
lines.append("")
|
|
319
|
+
lines.append("Safety Factors:")
|
|
320
|
+
for factor in analysis.safety_factors:
|
|
321
|
+
lines.append(f" • {factor}")
|
|
322
|
+
|
|
323
|
+
# History
|
|
324
|
+
if analysis.dividend_history:
|
|
325
|
+
lines.append("")
|
|
326
|
+
lines.append("Dividend History:")
|
|
327
|
+
for h in analysis.dividend_history[:5]:
|
|
328
|
+
lines.append(f" {h['year']}: ${h['total']:.2f}")
|
|
329
|
+
|
|
330
|
+
lines.append("")
|
|
331
|
+
lines.append("=" * 60)
|
|
332
|
+
|
|
333
|
+
return "\n".join(lines)
|
|
334
|
+
|
|
335
|
+
|
|
336
|
+
def main():
|
|
337
|
+
parser = argparse.ArgumentParser(description="Dividend Analysis")
|
|
338
|
+
parser.add_argument("tickers", nargs="+", help="Stock ticker(s)")
|
|
339
|
+
parser.add_argument("--output", choices=["text", "json"], default="text")
|
|
340
|
+
parser.add_argument("--verbose", "-v", action="store_true")
|
|
341
|
+
|
|
342
|
+
args = parser.parse_args()
|
|
343
|
+
|
|
344
|
+
results = []
|
|
345
|
+
for ticker in args.tickers:
|
|
346
|
+
analysis = analyze_dividends(ticker.upper(), verbose=args.verbose)
|
|
347
|
+
if analysis:
|
|
348
|
+
results.append(analysis)
|
|
349
|
+
else:
|
|
350
|
+
print(f"Error: Could not analyze {ticker}", file=sys.stderr)
|
|
351
|
+
|
|
352
|
+
if args.output == "json":
|
|
353
|
+
if len(results) == 1:
|
|
354
|
+
print(json.dumps(asdict(results[0]), indent=2))
|
|
355
|
+
else:
|
|
356
|
+
print(json.dumps([asdict(r) for r in results], indent=2))
|
|
357
|
+
else:
|
|
358
|
+
for i, analysis in enumerate(results):
|
|
359
|
+
if i > 0:
|
|
360
|
+
print("\n")
|
|
361
|
+
print(format_text(analysis))
|
|
362
|
+
|
|
363
|
+
|
|
364
|
+
if __name__ == "__main__":
|
|
365
|
+
main()
|