sophhub 0.2.3 → 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.
Files changed (107) hide show
  1. package/package.json +1 -1
  2. package/skills/consensus/skill.json +20 -0
  3. package/skills/consensus/src/SKILL.md +93 -0
  4. package/skills/deepwiki/skill.json +20 -0
  5. package/skills/deepwiki/src/SKILL.md +45 -0
  6. package/skills/deepwiki/src/_meta.json +6 -0
  7. package/skills/deepwiki/src/scripts/deepwiki.js +135 -0
  8. package/skills/feishu-bitable/skill.json +20 -0
  9. package/skills/feishu-bitable/src/CHECKLIST.md +150 -0
  10. package/skills/feishu-bitable/src/README.md +178 -0
  11. package/skills/feishu-bitable/src/SKILL.md +113 -0
  12. package/skills/feishu-bitable/src/_meta.json +6 -0
  13. package/skills/feishu-bitable/src/api.js +381 -0
  14. package/skills/feishu-bitable/src/bin/cli.js +284 -0
  15. package/skills/feishu-bitable/src/description.md +143 -0
  16. package/skills/feishu-bitable/src/examples/create-records.json +52 -0
  17. package/skills/feishu-bitable/src/examples/create-table.json +64 -0
  18. package/skills/feishu-bitable/src/package-lock.json +324 -0
  19. package/skills/feishu-bitable/src/package.json +33 -0
  20. package/skills/feishu-bitable/src/publish-config.json +14 -0
  21. package/skills/feishu-bitable/src/test-simple.js +61 -0
  22. package/skills/feishu-bitable/src/utils.js +261 -0
  23. package/skills/google-maps/skill.json +20 -0
  24. package/skills/google-maps/src/SKILL.md +237 -0
  25. package/skills/google-maps/src/_meta.json +6 -0
  26. package/skills/google-maps/src/lib/map_helper.py +912 -0
  27. package/skills/large-task-router/skill.json +20 -0
  28. package/skills/large-task-router/src/SKILL.md +79 -0
  29. package/skills/large-task-router/src/templates/plan.md +74 -0
  30. package/skills/skillhub/skill.json +11 -4
  31. package/skills/skillhub/src/SKILL.md +11 -1
  32. package/skills/sophnet-dailynews/skill.json +20 -0
  33. package/skills/sophnet-dailynews/src/SKILL.md +179 -0
  34. package/skills/sophnet-dailynews/src/cache.json +151 -0
  35. package/skills/sophnet-dailynews/src/sources.json +230 -0
  36. package/skills/sophnet-schedule/skill.json +20 -0
  37. package/skills/sophnet-schedule/src/ARCHITECTURE.md +321 -0
  38. package/skills/sophnet-schedule/src/IMPROVEMENTS.md +145 -0
  39. package/skills/sophnet-schedule/src/SKILL.md +1050 -0
  40. package/skills/sophnet-schedule/src/_meta.json +6 -0
  41. package/skills/sophnet-schedule/src/api/__init__.py +0 -0
  42. package/skills/sophnet-schedule/src/api/models.py +245 -0
  43. package/skills/sophnet-schedule/src/apps/add_event.py +237 -0
  44. package/skills/sophnet-schedule/src/apps/check_reminders.py +112 -0
  45. package/skills/sophnet-schedule/src/apps/check_roc.py +246 -0
  46. package/skills/sophnet-schedule/src/apps/generate_daily_plan.py +342 -0
  47. package/skills/sophnet-schedule/src/apps/import_events.py +216 -0
  48. package/skills/sophnet-schedule/src/apps/monitor_calendar_changes.py +140 -0
  49. package/skills/sophnet-schedule/src/apps/register_tasks.py +169 -0
  50. package/skills/sophnet-schedule/src/apps/sync_roc_to_gcal.py +174 -0
  51. package/skills/sophnet-schedule/src/compat.py +66 -0
  52. package/skills/sophnet-schedule/src/config/__init__.py +0 -0
  53. package/skills/sophnet-schedule/src/config/reminder_rules.yaml +96 -0
  54. package/skills/sophnet-schedule/src/config/roc_events.yaml +44 -0
  55. package/skills/sophnet-schedule/src/config/settings.py +133 -0
  56. package/skills/sophnet-schedule/src/config/task_registry.yaml +92 -0
  57. package/skills/sophnet-schedule/src/docs/FRONTEND_INTEGRATION_GUIDE.md +437 -0
  58. package/skills/sophnet-schedule/src/gcal/__init__.py +0 -0
  59. package/skills/sophnet-schedule/src/gcal/client.py +374 -0
  60. package/skills/sophnet-schedule/src/gcal/models.py +91 -0
  61. package/skills/sophnet-schedule/src/requirements.txt +6 -0
  62. package/skills/sophnet-schedule/src/scripts/setup_gcal_token.py +85 -0
  63. package/skills/sophnet-schedule/src/server.py +669 -0
  64. package/skills/sophnet-schedule/src/services/__init__.py +0 -0
  65. package/skills/sophnet-schedule/src/services/calendar_backend.py +139 -0
  66. package/skills/sophnet-schedule/src/services/conflict_detector.py +96 -0
  67. package/skills/sophnet-schedule/src/services/datetime_utils.py +117 -0
  68. package/skills/sophnet-schedule/src/services/event_classifier.py +100 -0
  69. package/skills/sophnet-schedule/src/services/event_diff.py +160 -0
  70. package/skills/sophnet-schedule/src/services/google_integration.py +500 -0
  71. package/skills/sophnet-schedule/src/services/job_store.py +100 -0
  72. package/skills/sophnet-schedule/src/services/local_event_store.py +266 -0
  73. package/skills/sophnet-schedule/src/services/reminder_planner.py +116 -0
  74. package/skills/sophnet-schedule/src/services/runtime_utils.py +31 -0
  75. package/skills/sophnet-schedule/src/services/table_parser.py +286 -0
  76. package/skills/sophnet-schedule/src/services/task_builder.py +167 -0
  77. package/skills/sophnet-schedule/src/services/time_window.py +72 -0
  78. package/skills/sophnet-stock/skill.json +20 -0
  79. package/skills/sophnet-stock/src/App-Plan.md +442 -0
  80. package/skills/sophnet-stock/src/README.md +214 -0
  81. package/skills/sophnet-stock/src/SKILL.md +236 -0
  82. package/skills/sophnet-stock/src/TODO.md +394 -0
  83. package/skills/sophnet-stock/src/_meta.json +6 -0
  84. package/skills/sophnet-stock/src/docs/ARCHITECTURE.md +408 -0
  85. package/skills/sophnet-stock/src/docs/CONCEPT.md +233 -0
  86. package/skills/sophnet-stock/src/docs/HOT_SCANNER.md +288 -0
  87. package/skills/sophnet-stock/src/docs/README.md +95 -0
  88. package/skills/sophnet-stock/src/docs/USAGE.md +465 -0
  89. package/skills/sophnet-stock/src/scripts/analyze_stock.py +2565 -0
  90. package/skills/sophnet-stock/src/scripts/dividends.py +365 -0
  91. package/skills/sophnet-stock/src/scripts/hot_scanner.py +582 -0
  92. package/skills/sophnet-stock/src/scripts/portfolio.py +548 -0
  93. package/skills/sophnet-stock/src/scripts/rumor_scanner.py +342 -0
  94. package/skills/sophnet-stock/src/scripts/test_stock_analysis.py +409 -0
  95. package/skills/sophnet-stock/src/scripts/watchlist.py +336 -0
  96. package/skills/xiaohongshu/skill.json +20 -0
  97. package/skills/xiaohongshu/src/SKILL.md +91 -0
  98. package/skills/xiaohongshu/src/_meta.json +6 -0
  99. package/skills/xiaohongshu/src/assets/card.html +216 -0
  100. package/skills/xiaohongshu/src/assets/cover.html +82 -0
  101. package/skills/xiaohongshu/src/assets/example.md +84 -0
  102. package/skills/xiaohongshu/src/assets/styles.css +318 -0
  103. package/skills/xiaohongshu/src/scripts/render_xhs_v2.py +737 -0
  104. package/skills/xiaohongshu/src/scripts/sign_server.py +158 -0
  105. package/skills/xiaohongshu/src/scripts/stealth.min.js +7 -0
  106. package/skills/xiaohongshu/src/scripts/xhs_tool.py +186 -0
  107. 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*"])