sophhub 0.2.3 → 0.3.0
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/package.json +1 -1
- package/skills/consensus/skill.json +20 -0
- package/skills/consensus/src/SKILL.md +93 -0
- package/skills/deepwiki/skill.json +20 -0
- package/skills/deepwiki/src/SKILL.md +45 -0
- package/skills/deepwiki/src/_meta.json +6 -0
- package/skills/deepwiki/src/scripts/deepwiki.js +135 -0
- package/skills/feishu-bitable/skill.json +20 -0
- package/skills/feishu-bitable/src/CHECKLIST.md +150 -0
- package/skills/feishu-bitable/src/README.md +178 -0
- package/skills/feishu-bitable/src/SKILL.md +113 -0
- package/skills/feishu-bitable/src/_meta.json +6 -0
- package/skills/feishu-bitable/src/api.js +381 -0
- package/skills/feishu-bitable/src/bin/cli.js +284 -0
- package/skills/feishu-bitable/src/description.md +143 -0
- package/skills/feishu-bitable/src/examples/create-records.json +52 -0
- package/skills/feishu-bitable/src/examples/create-table.json +64 -0
- package/skills/feishu-bitable/src/package-lock.json +324 -0
- package/skills/feishu-bitable/src/package.json +33 -0
- package/skills/feishu-bitable/src/publish-config.json +14 -0
- package/skills/feishu-bitable/src/test-simple.js +61 -0
- package/skills/feishu-bitable/src/utils.js +261 -0
- package/skills/google-maps/skill.json +20 -0
- package/skills/google-maps/src/SKILL.md +237 -0
- package/skills/google-maps/src/_meta.json +6 -0
- package/skills/google-maps/src/lib/map_helper.py +912 -0
- package/skills/large-task-router/skill.json +20 -0
- package/skills/large-task-router/src/SKILL.md +79 -0
- package/skills/large-task-router/src/templates/plan.md +74 -0
- package/skills/notes-hub-assistant/skill.json +20 -0
- package/skills/notes-hub-assistant/src/SKILL.md +233 -0
- package/skills/notes-hub-assistant/src/scripts/_resolve_lark_cli.py +48 -0
- package/skills/notes-hub-assistant/src/scripts/openclaw_meeting_minutes.py +473 -0
- package/skills/notes-hub-assistant/src/scripts/openclaw_notes_crud.py +596 -0
- package/skills/notes-hub-assistant/src/scripts/openclaw_wolai_notes_crud.py +364 -0
- package/skills/notes-hub-assistant/src/scripts/run_meeting_minutes.py +79 -0
- package/skills/notes-hub-assistant/src/scripts/run_note_crud.py +37 -0
- package/skills/notes-hub-assistant/src/scripts/run_notionbot.py +36 -0
- package/skills/notes-hub-assistant/src/scripts/run_wolai_note_crud.py +27 -0
- package/skills/skillhub/skill.json +11 -4
- package/skills/skillhub/src/SKILL.md +11 -1
- package/skills/sophnet-dailynews/skill.json +20 -0
- package/skills/sophnet-dailynews/src/SKILL.md +179 -0
- package/skills/sophnet-dailynews/src/cache.json +151 -0
- package/skills/sophnet-dailynews/src/sources.json +230 -0
- package/skills/sophnet-schedule/skill.json +20 -0
- package/skills/sophnet-schedule/src/ARCHITECTURE.md +321 -0
- package/skills/sophnet-schedule/src/IMPROVEMENTS.md +145 -0
- package/skills/sophnet-schedule/src/SKILL.md +1050 -0
- package/skills/sophnet-schedule/src/_meta.json +6 -0
- package/skills/sophnet-schedule/src/api/__init__.py +0 -0
- package/skills/sophnet-schedule/src/api/models.py +245 -0
- package/skills/sophnet-schedule/src/apps/add_event.py +237 -0
- package/skills/sophnet-schedule/src/apps/check_reminders.py +112 -0
- package/skills/sophnet-schedule/src/apps/check_roc.py +246 -0
- package/skills/sophnet-schedule/src/apps/generate_daily_plan.py +342 -0
- package/skills/sophnet-schedule/src/apps/import_events.py +216 -0
- package/skills/sophnet-schedule/src/apps/monitor_calendar_changes.py +140 -0
- package/skills/sophnet-schedule/src/apps/register_tasks.py +169 -0
- package/skills/sophnet-schedule/src/apps/sync_roc_to_gcal.py +174 -0
- package/skills/sophnet-schedule/src/compat.py +66 -0
- package/skills/sophnet-schedule/src/config/__init__.py +0 -0
- package/skills/sophnet-schedule/src/config/reminder_rules.yaml +96 -0
- package/skills/sophnet-schedule/src/config/roc_events.yaml +44 -0
- package/skills/sophnet-schedule/src/config/settings.py +133 -0
- package/skills/sophnet-schedule/src/config/task_registry.yaml +92 -0
- package/skills/sophnet-schedule/src/docs/FRONTEND_INTEGRATION_GUIDE.md +437 -0
- package/skills/sophnet-schedule/src/gcal/__init__.py +0 -0
- package/skills/sophnet-schedule/src/gcal/client.py +374 -0
- package/skills/sophnet-schedule/src/gcal/models.py +91 -0
- package/skills/sophnet-schedule/src/requirements.txt +6 -0
- package/skills/sophnet-schedule/src/scripts/setup_gcal_token.py +85 -0
- package/skills/sophnet-schedule/src/server.py +669 -0
- package/skills/sophnet-schedule/src/services/__init__.py +0 -0
- package/skills/sophnet-schedule/src/services/calendar_backend.py +139 -0
- package/skills/sophnet-schedule/src/services/conflict_detector.py +96 -0
- package/skills/sophnet-schedule/src/services/datetime_utils.py +117 -0
- package/skills/sophnet-schedule/src/services/event_classifier.py +100 -0
- package/skills/sophnet-schedule/src/services/event_diff.py +160 -0
- package/skills/sophnet-schedule/src/services/google_integration.py +500 -0
- package/skills/sophnet-schedule/src/services/job_store.py +100 -0
- package/skills/sophnet-schedule/src/services/local_event_store.py +266 -0
- package/skills/sophnet-schedule/src/services/reminder_planner.py +116 -0
- package/skills/sophnet-schedule/src/services/runtime_utils.py +31 -0
- package/skills/sophnet-schedule/src/services/table_parser.py +286 -0
- package/skills/sophnet-schedule/src/services/task_builder.py +167 -0
- package/skills/sophnet-schedule/src/services/time_window.py +72 -0
- package/skills/sophnet-stock/skill.json +20 -0
- package/skills/sophnet-stock/src/App-Plan.md +442 -0
- package/skills/sophnet-stock/src/README.md +214 -0
- package/skills/sophnet-stock/src/SKILL.md +236 -0
- package/skills/sophnet-stock/src/TODO.md +394 -0
- package/skills/sophnet-stock/src/_meta.json +6 -0
- package/skills/sophnet-stock/src/docs/ARCHITECTURE.md +408 -0
- package/skills/sophnet-stock/src/docs/CONCEPT.md +233 -0
- package/skills/sophnet-stock/src/docs/HOT_SCANNER.md +288 -0
- package/skills/sophnet-stock/src/docs/README.md +95 -0
- package/skills/sophnet-stock/src/docs/USAGE.md +465 -0
- package/skills/sophnet-stock/src/scripts/analyze_stock.py +2565 -0
- package/skills/sophnet-stock/src/scripts/dividends.py +365 -0
- package/skills/sophnet-stock/src/scripts/hot_scanner.py +582 -0
- package/skills/sophnet-stock/src/scripts/portfolio.py +548 -0
- package/skills/sophnet-stock/src/scripts/rumor_scanner.py +342 -0
- package/skills/sophnet-stock/src/scripts/test_stock_analysis.py +409 -0
- package/skills/sophnet-stock/src/scripts/watchlist.py +336 -0
- package/skills/xiaohongshu/skill.json +20 -0
- package/skills/xiaohongshu/src/SKILL.md +91 -0
- package/skills/xiaohongshu/src/_meta.json +6 -0
- package/skills/xiaohongshu/src/assets/card.html +216 -0
- package/skills/xiaohongshu/src/assets/cover.html +82 -0
- package/skills/xiaohongshu/src/assets/example.md +84 -0
- package/skills/xiaohongshu/src/assets/styles.css +318 -0
- package/skills/xiaohongshu/src/scripts/render_xhs_v2.py +737 -0
- package/skills/xiaohongshu/src/scripts/sign_server.py +158 -0
- package/skills/xiaohongshu/src/scripts/stealth.min.js +7 -0
- package/skills/xiaohongshu/src/scripts/xhs_tool.py +186 -0
- package/skills/xiaohongshu/src/workflow.py +185 -0
|
@@ -0,0 +1,409 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
# /// script
|
|
3
|
+
# requires-python = ">=3.10"
|
|
4
|
+
# dependencies = [
|
|
5
|
+
# "pytest>=8.0.0",
|
|
6
|
+
# "yfinance>=0.2.40",
|
|
7
|
+
# "pandas>=2.0.0",
|
|
8
|
+
# ]
|
|
9
|
+
# ///
|
|
10
|
+
"""
|
|
11
|
+
Tests for Stock Analysis Skill v6.0
|
|
12
|
+
|
|
13
|
+
Run with: uv run pytest test_stock_analysis.py -v
|
|
14
|
+
"""
|
|
15
|
+
|
|
16
|
+
import json
|
|
17
|
+
import pytest
|
|
18
|
+
from unittest.mock import Mock, patch, MagicMock
|
|
19
|
+
from datetime import datetime, timezone
|
|
20
|
+
import pandas as pd
|
|
21
|
+
|
|
22
|
+
# Import modules to test
|
|
23
|
+
from analyze_stock import (
|
|
24
|
+
detect_asset_type,
|
|
25
|
+
normalize_ticker,
|
|
26
|
+
format_error_output,
|
|
27
|
+
calculate_rsi,
|
|
28
|
+
fetch_stock_data,
|
|
29
|
+
analyze_earnings_surprise,
|
|
30
|
+
analyze_fundamentals,
|
|
31
|
+
analyze_momentum,
|
|
32
|
+
synthesize_signal,
|
|
33
|
+
EarningsSurprise,
|
|
34
|
+
Fundamentals,
|
|
35
|
+
MomentumAnalysis,
|
|
36
|
+
MarketContext,
|
|
37
|
+
StockData,
|
|
38
|
+
)
|
|
39
|
+
from dividends import analyze_dividends
|
|
40
|
+
from watchlist import (
|
|
41
|
+
add_to_watchlist,
|
|
42
|
+
remove_from_watchlist,
|
|
43
|
+
list_watchlist,
|
|
44
|
+
WatchlistItem,
|
|
45
|
+
)
|
|
46
|
+
from portfolio import PortfolioStore
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class TestAssetTypeDetection:
|
|
50
|
+
"""Test asset type detection."""
|
|
51
|
+
|
|
52
|
+
def test_stock_detection(self):
|
|
53
|
+
assert detect_asset_type("AAPL") == "stock"
|
|
54
|
+
assert detect_asset_type("MSFT") == "stock"
|
|
55
|
+
assert detect_asset_type("googl") == "stock"
|
|
56
|
+
|
|
57
|
+
def test_crypto_detection(self):
|
|
58
|
+
assert detect_asset_type("BTC-USD") == "crypto"
|
|
59
|
+
assert detect_asset_type("ETH-USD") == "crypto"
|
|
60
|
+
assert detect_asset_type("sol-usd") == "crypto"
|
|
61
|
+
|
|
62
|
+
def test_edge_cases(self):
|
|
63
|
+
# Ticker ending in USD but not crypto format
|
|
64
|
+
assert detect_asset_type("MUSD") == "stock"
|
|
65
|
+
# Numbers in ticker
|
|
66
|
+
assert detect_asset_type("BRK.B") == "stock"
|
|
67
|
+
|
|
68
|
+
|
|
69
|
+
class TestTickerNormalization:
|
|
70
|
+
"""Test ticker normalization for index aliases."""
|
|
71
|
+
|
|
72
|
+
def test_index_aliases(self):
|
|
73
|
+
assert normalize_ticker("IXIC") == "^IXIC"
|
|
74
|
+
assert normalize_ticker("GSPC") == "^GSPC"
|
|
75
|
+
assert normalize_ticker("DJI") == "^DJI"
|
|
76
|
+
|
|
77
|
+
def test_non_index_tickers_unchanged_except_case(self):
|
|
78
|
+
assert normalize_ticker("aapl") == "AAPL"
|
|
79
|
+
assert normalize_ticker("^ixic") == "^IXIC"
|
|
80
|
+
assert normalize_ticker("btc-usd") == "BTC-USD"
|
|
81
|
+
|
|
82
|
+
|
|
83
|
+
class TestErrorFormatting:
|
|
84
|
+
"""Test JSON-safe error formatting for tool integrations."""
|
|
85
|
+
|
|
86
|
+
def test_json_error_output(self):
|
|
87
|
+
payload = json.loads(format_error_output("Invalid ticker", output="json"))
|
|
88
|
+
assert payload["success"] is False
|
|
89
|
+
assert payload["error"] == "Invalid ticker"
|
|
90
|
+
|
|
91
|
+
def test_text_error_output(self):
|
|
92
|
+
assert format_error_output("Invalid ticker", output="text") == "Error: Invalid ticker"
|
|
93
|
+
|
|
94
|
+
|
|
95
|
+
class TestRSICalculation:
|
|
96
|
+
"""Test RSI calculation."""
|
|
97
|
+
|
|
98
|
+
def test_rsi_overbought(self):
|
|
99
|
+
"""Test RSI > 70 (overbought)."""
|
|
100
|
+
# Create rising prices
|
|
101
|
+
prices = pd.Series([100 + i * 2 for i in range(20)])
|
|
102
|
+
rsi = calculate_rsi(prices, period=14)
|
|
103
|
+
assert rsi is not None
|
|
104
|
+
assert rsi > 70
|
|
105
|
+
|
|
106
|
+
def test_rsi_oversold(self):
|
|
107
|
+
"""Test RSI < 30 (oversold)."""
|
|
108
|
+
# Create falling prices
|
|
109
|
+
prices = pd.Series([100 - i * 2 for i in range(20)])
|
|
110
|
+
rsi = calculate_rsi(prices, period=14)
|
|
111
|
+
assert rsi is not None
|
|
112
|
+
assert rsi < 30
|
|
113
|
+
|
|
114
|
+
def test_rsi_insufficient_data(self):
|
|
115
|
+
"""Test RSI with insufficient data."""
|
|
116
|
+
prices = pd.Series([100, 101, 102]) # Too few points
|
|
117
|
+
rsi = calculate_rsi(prices, period=14)
|
|
118
|
+
assert rsi is None
|
|
119
|
+
|
|
120
|
+
|
|
121
|
+
class TestEarningsSurprise:
|
|
122
|
+
"""Test earnings surprise analysis."""
|
|
123
|
+
|
|
124
|
+
def test_earnings_beat(self):
|
|
125
|
+
"""Test positive earnings surprise."""
|
|
126
|
+
# Mock StockData with earnings beat
|
|
127
|
+
mock_earnings = pd.DataFrame({
|
|
128
|
+
"Reported EPS": [1.50],
|
|
129
|
+
"EPS Estimate": [1.20],
|
|
130
|
+
}, index=[pd.Timestamp("2024-01-15")])
|
|
131
|
+
|
|
132
|
+
mock_data = Mock(spec=StockData)
|
|
133
|
+
mock_data.earnings_history = mock_earnings
|
|
134
|
+
|
|
135
|
+
result = analyze_earnings_surprise(mock_data)
|
|
136
|
+
|
|
137
|
+
assert result is not None
|
|
138
|
+
assert result.score > 0
|
|
139
|
+
assert result.surprise_pct > 0
|
|
140
|
+
assert "Beat" in result.explanation
|
|
141
|
+
|
|
142
|
+
def test_earnings_miss(self):
|
|
143
|
+
"""Test negative earnings surprise."""
|
|
144
|
+
mock_earnings = pd.DataFrame({
|
|
145
|
+
"Reported EPS": [0.80],
|
|
146
|
+
"EPS Estimate": [1.00],
|
|
147
|
+
}, index=[pd.Timestamp("2024-01-15")])
|
|
148
|
+
|
|
149
|
+
mock_data = Mock(spec=StockData)
|
|
150
|
+
mock_data.earnings_history = mock_earnings
|
|
151
|
+
|
|
152
|
+
result = analyze_earnings_surprise(mock_data)
|
|
153
|
+
|
|
154
|
+
assert result is not None
|
|
155
|
+
assert result.score < 0
|
|
156
|
+
assert result.surprise_pct < 0
|
|
157
|
+
assert "Missed" in result.explanation
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class TestFundamentals:
|
|
161
|
+
"""Test fundamentals analysis."""
|
|
162
|
+
|
|
163
|
+
def test_strong_fundamentals(self):
|
|
164
|
+
"""Test stock with strong fundamentals."""
|
|
165
|
+
mock_data = Mock(spec=StockData)
|
|
166
|
+
mock_data.info = {
|
|
167
|
+
"trailingPE": 15,
|
|
168
|
+
"operatingMargins": 0.25,
|
|
169
|
+
"revenueGrowth": 0.30,
|
|
170
|
+
"debtToEquity": 30,
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
result = analyze_fundamentals(mock_data)
|
|
174
|
+
|
|
175
|
+
assert result is not None
|
|
176
|
+
assert result.score > 0
|
|
177
|
+
assert "pe_ratio" in result.key_metrics
|
|
178
|
+
|
|
179
|
+
def test_weak_fundamentals(self):
|
|
180
|
+
"""Test stock with weak fundamentals."""
|
|
181
|
+
mock_data = Mock(spec=StockData)
|
|
182
|
+
mock_data.info = {
|
|
183
|
+
"trailingPE": 50,
|
|
184
|
+
"operatingMargins": 0.02,
|
|
185
|
+
"revenueGrowth": -0.10,
|
|
186
|
+
"debtToEquity": 300,
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
result = analyze_fundamentals(mock_data)
|
|
190
|
+
|
|
191
|
+
assert result is not None
|
|
192
|
+
assert result.score < 0
|
|
193
|
+
|
|
194
|
+
|
|
195
|
+
class TestMomentum:
|
|
196
|
+
"""Test momentum analysis."""
|
|
197
|
+
|
|
198
|
+
def test_overbought_momentum(self):
|
|
199
|
+
"""Test overbought conditions."""
|
|
200
|
+
# Create mock price history with rising prices near 52w high
|
|
201
|
+
dates = pd.date_range(end=datetime.now(), periods=100)
|
|
202
|
+
prices = pd.DataFrame({
|
|
203
|
+
"Close": [100 + i * 0.5 for i in range(100)],
|
|
204
|
+
"Volume": [1000000] * 100,
|
|
205
|
+
}, index=dates)
|
|
206
|
+
|
|
207
|
+
mock_data = Mock(spec=StockData)
|
|
208
|
+
mock_data.price_history = prices
|
|
209
|
+
mock_data.info = {
|
|
210
|
+
"fiftyTwoWeekHigh": 150,
|
|
211
|
+
"fiftyTwoWeekLow": 80,
|
|
212
|
+
"regularMarketPrice": 148,
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
result = analyze_momentum(mock_data)
|
|
216
|
+
|
|
217
|
+
assert result is not None
|
|
218
|
+
assert result.rsi_status == "overbought"
|
|
219
|
+
assert result.near_52w_high == True
|
|
220
|
+
assert result.score < 0 # Overbought = negative score
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
class TestSignalSynthesis:
|
|
224
|
+
"""Test signal synthesis."""
|
|
225
|
+
|
|
226
|
+
def test_buy_signal(self):
|
|
227
|
+
"""Test BUY recommendation synthesis."""
|
|
228
|
+
earnings = EarningsSurprise(score=0.8, explanation="Beat by 20%", actual_eps=1.2, expected_eps=1.0, surprise_pct=20)
|
|
229
|
+
fundamentals = Fundamentals(score=0.6, key_metrics={"pe_ratio": 15}, explanation="Strong margins")
|
|
230
|
+
|
|
231
|
+
signal = synthesize_signal(
|
|
232
|
+
ticker="TEST",
|
|
233
|
+
company_name="Test Corp",
|
|
234
|
+
earnings=earnings,
|
|
235
|
+
fundamentals=fundamentals,
|
|
236
|
+
analysts=None,
|
|
237
|
+
historical=None,
|
|
238
|
+
market_context=None,
|
|
239
|
+
sector=None,
|
|
240
|
+
earnings_timing=None,
|
|
241
|
+
momentum=None,
|
|
242
|
+
sentiment=None,
|
|
243
|
+
)
|
|
244
|
+
|
|
245
|
+
assert signal.recommendation == "BUY"
|
|
246
|
+
assert signal.confidence > 0.5
|
|
247
|
+
|
|
248
|
+
def test_sell_signal(self):
|
|
249
|
+
"""Test SELL recommendation synthesis."""
|
|
250
|
+
earnings = EarningsSurprise(score=-0.8, explanation="Missed by 20%", actual_eps=0.8, expected_eps=1.0, surprise_pct=-20)
|
|
251
|
+
fundamentals = Fundamentals(score=-0.6, key_metrics={"pe_ratio": 50}, explanation="Weak margins")
|
|
252
|
+
|
|
253
|
+
signal = synthesize_signal(
|
|
254
|
+
ticker="TEST",
|
|
255
|
+
company_name="Test Corp",
|
|
256
|
+
earnings=earnings,
|
|
257
|
+
fundamentals=fundamentals,
|
|
258
|
+
analysts=None,
|
|
259
|
+
historical=None,
|
|
260
|
+
market_context=None,
|
|
261
|
+
sector=None,
|
|
262
|
+
earnings_timing=None,
|
|
263
|
+
momentum=None,
|
|
264
|
+
sentiment=None,
|
|
265
|
+
)
|
|
266
|
+
|
|
267
|
+
assert signal.recommendation == "SELL"
|
|
268
|
+
|
|
269
|
+
def test_risk_off_penalty(self):
|
|
270
|
+
"""Test risk-off mode reduces BUY confidence."""
|
|
271
|
+
earnings = EarningsSurprise(score=0.8, explanation="Beat", actual_eps=1.2, expected_eps=1.0, surprise_pct=20)
|
|
272
|
+
fundamentals = Fundamentals(score=0.6, key_metrics={}, explanation="Strong")
|
|
273
|
+
market = MarketContext(
|
|
274
|
+
vix_level=25,
|
|
275
|
+
vix_status="elevated",
|
|
276
|
+
spy_trend_10d=2.0,
|
|
277
|
+
qqq_trend_10d=1.5,
|
|
278
|
+
market_regime="choppy",
|
|
279
|
+
score=-0.2,
|
|
280
|
+
explanation="Risk-off",
|
|
281
|
+
gld_change_5d=3.0,
|
|
282
|
+
tlt_change_5d=2.0,
|
|
283
|
+
uup_change_5d=1.5,
|
|
284
|
+
risk_off_detected=True,
|
|
285
|
+
)
|
|
286
|
+
|
|
287
|
+
signal = synthesize_signal(
|
|
288
|
+
ticker="TEST",
|
|
289
|
+
company_name="Test Corp",
|
|
290
|
+
earnings=earnings,
|
|
291
|
+
fundamentals=fundamentals,
|
|
292
|
+
analysts=None,
|
|
293
|
+
historical=None,
|
|
294
|
+
market_context=market,
|
|
295
|
+
sector=None,
|
|
296
|
+
earnings_timing=None,
|
|
297
|
+
momentum=None,
|
|
298
|
+
sentiment=None,
|
|
299
|
+
)
|
|
300
|
+
|
|
301
|
+
# Should still be BUY but with reduced confidence
|
|
302
|
+
assert signal.recommendation in ["BUY", "HOLD"]
|
|
303
|
+
assert any("RISK-OFF" in c for c in signal.caveats)
|
|
304
|
+
|
|
305
|
+
|
|
306
|
+
class TestWatchlist:
|
|
307
|
+
"""Test watchlist functionality."""
|
|
308
|
+
|
|
309
|
+
@patch('watchlist.get_current_price')
|
|
310
|
+
@patch('watchlist.save_watchlist')
|
|
311
|
+
@patch('watchlist.load_watchlist')
|
|
312
|
+
def test_add_to_watchlist(self, mock_load, mock_save, mock_price):
|
|
313
|
+
"""Test adding ticker to watchlist."""
|
|
314
|
+
mock_load.return_value = []
|
|
315
|
+
mock_price.return_value = 150.0
|
|
316
|
+
mock_save.return_value = None
|
|
317
|
+
|
|
318
|
+
result = add_to_watchlist("AAPL", target_price=200.0)
|
|
319
|
+
|
|
320
|
+
assert result["success"] == True
|
|
321
|
+
assert result["action"] == "added"
|
|
322
|
+
assert result["ticker"] == "AAPL"
|
|
323
|
+
assert result["target_price"] == 200.0
|
|
324
|
+
|
|
325
|
+
@patch('watchlist.save_watchlist')
|
|
326
|
+
@patch('watchlist.load_watchlist')
|
|
327
|
+
def test_remove_from_watchlist(self, mock_load, mock_save):
|
|
328
|
+
"""Test removing ticker from watchlist."""
|
|
329
|
+
mock_load.return_value = [
|
|
330
|
+
WatchlistItem(ticker="AAPL", added_at="2024-01-01T00:00:00+00:00")
|
|
331
|
+
]
|
|
332
|
+
mock_save.return_value = None
|
|
333
|
+
|
|
334
|
+
result = remove_from_watchlist("AAPL")
|
|
335
|
+
|
|
336
|
+
assert result["success"] == True
|
|
337
|
+
assert result["removed"] == "AAPL"
|
|
338
|
+
|
|
339
|
+
|
|
340
|
+
class TestDividendAnalysis:
|
|
341
|
+
"""Test dividend analysis."""
|
|
342
|
+
|
|
343
|
+
@patch('yfinance.Ticker')
|
|
344
|
+
def test_dividend_stock(self, mock_ticker):
|
|
345
|
+
"""Test analysis of dividend-paying stock."""
|
|
346
|
+
mock_stock = Mock()
|
|
347
|
+
mock_stock.info = {
|
|
348
|
+
"longName": "Johnson & Johnson",
|
|
349
|
+
"regularMarketPrice": 160.0,
|
|
350
|
+
"dividendYield": 0.03,
|
|
351
|
+
"dividendRate": 4.80,
|
|
352
|
+
"trailingEps": 6.00,
|
|
353
|
+
}
|
|
354
|
+
mock_stock.dividends = pd.Series(
|
|
355
|
+
[1.2, 1.2, 1.2, 1.2] * 5, # 5 years of quarterly dividends
|
|
356
|
+
index=pd.date_range(start="2019-01-01", periods=20, freq="Q")
|
|
357
|
+
)
|
|
358
|
+
mock_ticker.return_value = mock_stock
|
|
359
|
+
|
|
360
|
+
result = analyze_dividends("JNJ")
|
|
361
|
+
|
|
362
|
+
assert result is not None
|
|
363
|
+
assert result.dividend_yield == 3.0
|
|
364
|
+
assert result.payout_ratio == 80.0
|
|
365
|
+
assert result.income_rating != "no_dividend"
|
|
366
|
+
|
|
367
|
+
@patch('yfinance.Ticker')
|
|
368
|
+
def test_no_dividend_stock(self, mock_ticker):
|
|
369
|
+
"""Test analysis of non-dividend stock."""
|
|
370
|
+
mock_stock = Mock()
|
|
371
|
+
mock_stock.info = {
|
|
372
|
+
"longName": "Amazon",
|
|
373
|
+
"regularMarketPrice": 180.0,
|
|
374
|
+
"dividendYield": None,
|
|
375
|
+
"dividendRate": None,
|
|
376
|
+
}
|
|
377
|
+
mock_ticker.return_value = mock_stock
|
|
378
|
+
|
|
379
|
+
result = analyze_dividends("AMZN")
|
|
380
|
+
|
|
381
|
+
assert result is not None
|
|
382
|
+
assert result.income_rating == "no_dividend"
|
|
383
|
+
|
|
384
|
+
|
|
385
|
+
class TestIntegration:
|
|
386
|
+
"""Integration tests (require network)."""
|
|
387
|
+
|
|
388
|
+
@pytest.mark.integration
|
|
389
|
+
def test_real_stock_analysis(self):
|
|
390
|
+
"""Test real stock analysis (AAPL)."""
|
|
391
|
+
data = fetch_stock_data("AAPL", verbose=False)
|
|
392
|
+
|
|
393
|
+
assert data is not None
|
|
394
|
+
assert data.ticker == "AAPL"
|
|
395
|
+
assert data.info is not None
|
|
396
|
+
assert "regularMarketPrice" in data.info
|
|
397
|
+
|
|
398
|
+
@pytest.mark.integration
|
|
399
|
+
def test_real_crypto_analysis(self):
|
|
400
|
+
"""Test real crypto analysis (BTC-USD)."""
|
|
401
|
+
data = fetch_stock_data("BTC-USD", verbose=False)
|
|
402
|
+
|
|
403
|
+
assert data is not None
|
|
404
|
+
assert data.asset_type == "crypto"
|
|
405
|
+
|
|
406
|
+
|
|
407
|
+
# Run tests
|
|
408
|
+
if __name__ == "__main__":
|
|
409
|
+
pytest.main([__file__, "-v", "--ignore-glob=*integration*"])
|