webull-openapi-mcp 1.2.4__tar.gz → 1.2.6__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.
Files changed (72) hide show
  1. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/.env.example +1 -1
  2. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/PKG-INFO +14 -3
  3. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/README.md +12 -1
  4. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/mcpb/build_manifest.py +1 -1
  5. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/pyproject.toml +2 -2
  6. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/server.json +2 -2
  7. webull_openapi_mcp-1.2.6/tests/test_community_tools.py +126 -0
  8. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_fundamental_screener_registration.py +150 -3
  9. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_fundamental_tools.py +85 -0
  10. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_guards.py +114 -1
  11. webull_openapi_mcp-1.2.6/tests/test_news_tools.py +52 -0
  12. webull_openapi_mcp-1.2.6/tests/test_order_history_tool.py +96 -0
  13. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_property_server.py +185 -2
  14. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_screener_tools.py +34 -0
  15. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_sdk_client.py +2 -2
  16. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/__init__.py +1 -1
  17. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/cli.py +1 -1
  18. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/errors.py +51 -14
  19. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/formatters.py +302 -32
  20. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/guards.py +193 -27
  21. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/region_config.py +49 -11
  22. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/sdk_client.py +7 -1
  23. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/server.py +17 -2
  24. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/__init__.py +4 -0
  25. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/__init__.py +2 -0
  26. webull_openapi_mcp-1.2.6/webull_openapi_mcp/tools/market_data/community.py +197 -0
  27. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/fundamental.py +211 -1
  28. webull_openapi_mcp-1.2.6/webull_openapi_mcp/tools/market_data/news.py +122 -0
  29. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/option.py +30 -5
  30. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/screener.py +71 -1
  31. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/account.py +7 -4
  32. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/assets.py +12 -12
  33. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/instrument.py +106 -31
  34. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/option_order.py +64 -4
  35. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/order.py +46 -31
  36. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/.github/workflows/pypi-ci.yml +0 -0
  37. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/.gitignore +0 -0
  38. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/DISCLAIMER.md +0 -0
  39. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/LICENSE +0 -0
  40. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/mcpb/.mcpbignore +0 -0
  41. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/mcpb/build.sh +0 -0
  42. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/mcpb/src/server.py +0 -0
  43. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/__init__.py +0 -0
  44. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_account_tools.py +0 -0
  45. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_audit.py +0 -0
  46. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_cli.py +0 -0
  47. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_config.py +0 -0
  48. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_errors.py +0 -0
  49. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_formatters.py +0 -0
  50. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_instrument_tools.py +0 -0
  51. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_market_data_tools.py +0 -0
  52. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_property_audit.py +0 -0
  53. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_property_config.py +0 -0
  54. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_property_formatters.py +0 -0
  55. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_property_guards.py +0 -0
  56. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_property_sdk.py +0 -0
  57. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_stock_order_tools.py +0 -0
  58. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/__main__.py +0 -0
  59. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/audit.py +0 -0
  60. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/config.py +0 -0
  61. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/constants.py +0 -0
  62. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/crypto.py +0 -0
  63. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/event.py +0 -0
  64. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/financial.py +0 -0
  65. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/futures.py +0 -0
  66. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/stock.py +0 -0
  67. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/watchlist.py +0 -0
  68. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/__init__.py +0 -0
  69. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/crypto_order.py +0 -0
  70. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/event_order.py +0 -0
  71. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/futures_order.py +0 -0
  72. {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/stock_order.py +0 -0
@@ -20,7 +20,7 @@ WEBULL_APP_SECRET=
20
20
  # Set to 'prod' for live trading (use with caution)
21
21
  # WEBULL_ENVIRONMENT=uat
22
22
 
23
- # Region ID: us, hk, jp, sg, my, uk, mx, br, or za
23
+ # Region ID: us, hk, jp, sg, th, my, uk, mx, br, or za
24
24
  # Default: us
25
25
  # WEBULL_REGION_ID=us
26
26
 
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.5
2
2
  Name: webull-openapi-mcp
3
- Version: 1.2.4
3
+ Version: 1.2.6
4
4
  Summary: MCP Server for Webull OpenAPI - enables AI assistants to securely access Webull trading and market data
5
5
  Project-URL: Homepage, https://github.com/webull-inc/webull-openapi-mcp
6
6
  Project-URL: Repository, https://github.com/webull-inc/webull-openapi-mcp
@@ -20,7 +20,7 @@ Requires-Python: <3.13,>=3.10
20
20
  Requires-Dist: click
21
21
  Requires-Dist: fastmcp==3.0.2
22
22
  Requires-Dist: python-dotenv
23
- Requires-Dist: webull-openapi-python-sdk==3.0.0
23
+ Requires-Dist: webull-openapi-python-sdk==3.0.4
24
24
  Provides-Extra: dev
25
25
  Requires-Dist: hypothesis; extra == 'dev'
26
26
  Requires-Dist: pytest; extra == 'dev'
@@ -54,6 +54,7 @@ See [DISCLAIMER.md](DISCLAIMER.md) for the full disclaimer.
54
54
  - **NOII Data** — Net Order Imbalance Indicator bars and snapshots for US stock opening/closing auctions
55
55
  - **Screener** — Top gainers/losers, most active, market sectors, high dividend, and 52-week high/low rankings
56
56
  - **Watchlist** — Create, manage, and query user watchlists and instruments
57
+ - **Community** (US only) — Create posts and comments, list hot stocks, browse posts by symbol, view post details, and list your own posts
57
58
  - **Fundamental Data** — Company profiles, analyst ratings, and target prices
58
59
  - **Stock & Fund Fundamentals** — Capital flow, SEC filings, earnings/dividend calendar, forecast EPS, industry comparison, and full fund data (rating, performance, allocation, holdings, brief, dividends, splits, net value, files)
59
60
  - **Financial Statements** — Financial alert, indicators, income statement, balance sheet, and cash flow
@@ -93,6 +94,12 @@ Here are some prompts you can use with your AI assistant:
93
94
  - Add AAPL and MSFT to my watchlist
94
95
  - What stocks are in my watchlist?
95
96
 
97
+ **Community**
98
+ - What are the community hot stocks right now?
99
+ - Show me recent community posts about NVDA
100
+ - Post "Great earnings report!" tagged with AAPL
101
+ - List my community posts
102
+
96
103
  **Fundamental & Analyst**
97
104
  - Tell me about NVDA's company profile
98
105
  - What do analysts rate AAPL?
@@ -333,8 +340,11 @@ See [.env.example](.env.example) for full configuration template.
333
340
  | **Crypto** | `get_crypto_snapshot`, `get_crypto_bars` | US |
334
341
  | **Option** | `get_option_tick`, `get_option_snapshot`, `get_option_bars` | US, HK, JP |
335
342
  | **Event** | `get_event_tick`, `get_event_snapshot`, `get_event_depth`, `get_event_bars` | US |
343
+ | **Market Insights** | `get_market_sentiment`, `get_stock_unusual_activities`, `get_institution_identifier`, `get_institution_holdings`, `get_institution_holding_changes`, `get_insider_transactions`, `get_insider_holdings`, `get_macroeconomic_calendar` | US |
344
+ | **News** | `search_news`, `get_news_detail`, `get_stock_news` | US |
336
345
  | **Screener** | `get_gainers_losers`, `get_most_active`, `get_market_sectors`, `get_market_sectors_detail`, `get_high_dividend`, `get_52_week_high_low` | All |
337
346
  | **Watchlist** | `get_watchlists`, `create_watchlist`, `update_watchlist`, `delete_watchlist`, `get_watchlist_instruments`, `add_watchlist_instruments`, `remove_watchlist_instruments`, `update_watchlist_instruments` | All |
347
+ | **Community** | `create_community_post`, `create_community_comment`, `get_community_hot_stocks`, `get_community_posts`, `get_community_post_detail`, `get_my_community_posts` | US |
338
348
 
339
349
  ### Fundamental & Instrument
340
350
 
@@ -432,7 +442,7 @@ All commands accept `--env-file PATH` to specify a custom `.env` file location (
432
442
  - **Review before trading** — Always review order details proposed by the AI before confirming. Use `preview_stock_order` / `preview_option_order` before placing orders.
433
443
  - **Use toolset filtering** — Set `WEBULL_TOOLSETS=account,market-data` to disable trading tools entirely if you only need read-only access. Valid toolsets: `account`, `market-data`, `trading`, `instrument`.
434
444
  - **Default sandbox** — The server defaults to UAT (sandbox) environment. You must explicitly set `WEBULL_ENVIRONMENT=prod` for live trading.
435
- - **Dependency security** — `fastmcp` is pinned to version `3.0.2` and `webull-openapi-python-sdk` is pinned to `3.0.0`. Users are responsible for monitoring and updating third-party dependencies for security patches. Review release notes before upgrading.
445
+ - **Dependency security** — `fastmcp` is pinned to version `3.0.2` and `webull-openapi-python-sdk` is pinned to `3.0.4`. Users are responsible for monitoring and updating third-party dependencies for security patches. Review release notes before upgrading.
436
446
 
437
447
  ---
438
448
 
@@ -528,6 +538,7 @@ webull-openapi-mcp/
528
538
  │ │ ├── event.py # Event contract market data
529
539
  │ │ ├── screener.py # Gainers/losers, most active, market sectors, high dividend, 52-week high/low
530
540
  │ │ ├── watchlist.py# Watchlist CRUD and instrument management
541
+ │ │ ├── community.py # Community posts, comments, hot stocks
531
542
  │ │ ├── fundamental.py # Company profile, analyst data; stock & fund fundamentals (US/HK/JP)
532
543
  │ │ └── financial.py # Financial statements: alert, indicators, income, balance sheet, cash flow (US/HK/JP)
533
544
  │ └── trading/
@@ -24,6 +24,7 @@ See [DISCLAIMER.md](DISCLAIMER.md) for the full disclaimer.
24
24
  - **NOII Data** — Net Order Imbalance Indicator bars and snapshots for US stock opening/closing auctions
25
25
  - **Screener** — Top gainers/losers, most active, market sectors, high dividend, and 52-week high/low rankings
26
26
  - **Watchlist** — Create, manage, and query user watchlists and instruments
27
+ - **Community** (US only) — Create posts and comments, list hot stocks, browse posts by symbol, view post details, and list your own posts
27
28
  - **Fundamental Data** — Company profiles, analyst ratings, and target prices
28
29
  - **Stock & Fund Fundamentals** — Capital flow, SEC filings, earnings/dividend calendar, forecast EPS, industry comparison, and full fund data (rating, performance, allocation, holdings, brief, dividends, splits, net value, files)
29
30
  - **Financial Statements** — Financial alert, indicators, income statement, balance sheet, and cash flow
@@ -63,6 +64,12 @@ Here are some prompts you can use with your AI assistant:
63
64
  - Add AAPL and MSFT to my watchlist
64
65
  - What stocks are in my watchlist?
65
66
 
67
+ **Community**
68
+ - What are the community hot stocks right now?
69
+ - Show me recent community posts about NVDA
70
+ - Post "Great earnings report!" tagged with AAPL
71
+ - List my community posts
72
+
66
73
  **Fundamental & Analyst**
67
74
  - Tell me about NVDA's company profile
68
75
  - What do analysts rate AAPL?
@@ -303,8 +310,11 @@ See [.env.example](.env.example) for full configuration template.
303
310
  | **Crypto** | `get_crypto_snapshot`, `get_crypto_bars` | US |
304
311
  | **Option** | `get_option_tick`, `get_option_snapshot`, `get_option_bars` | US, HK, JP |
305
312
  | **Event** | `get_event_tick`, `get_event_snapshot`, `get_event_depth`, `get_event_bars` | US |
313
+ | **Market Insights** | `get_market_sentiment`, `get_stock_unusual_activities`, `get_institution_identifier`, `get_institution_holdings`, `get_institution_holding_changes`, `get_insider_transactions`, `get_insider_holdings`, `get_macroeconomic_calendar` | US |
314
+ | **News** | `search_news`, `get_news_detail`, `get_stock_news` | US |
306
315
  | **Screener** | `get_gainers_losers`, `get_most_active`, `get_market_sectors`, `get_market_sectors_detail`, `get_high_dividend`, `get_52_week_high_low` | All |
307
316
  | **Watchlist** | `get_watchlists`, `create_watchlist`, `update_watchlist`, `delete_watchlist`, `get_watchlist_instruments`, `add_watchlist_instruments`, `remove_watchlist_instruments`, `update_watchlist_instruments` | All |
317
+ | **Community** | `create_community_post`, `create_community_comment`, `get_community_hot_stocks`, `get_community_posts`, `get_community_post_detail`, `get_my_community_posts` | US |
308
318
 
309
319
  ### Fundamental & Instrument
310
320
 
@@ -402,7 +412,7 @@ All commands accept `--env-file PATH` to specify a custom `.env` file location (
402
412
  - **Review before trading** — Always review order details proposed by the AI before confirming. Use `preview_stock_order` / `preview_option_order` before placing orders.
403
413
  - **Use toolset filtering** — Set `WEBULL_TOOLSETS=account,market-data` to disable trading tools entirely if you only need read-only access. Valid toolsets: `account`, `market-data`, `trading`, `instrument`.
404
414
  - **Default sandbox** — The server defaults to UAT (sandbox) environment. You must explicitly set `WEBULL_ENVIRONMENT=prod` for live trading.
405
- - **Dependency security** — `fastmcp` is pinned to version `3.0.2` and `webull-openapi-python-sdk` is pinned to `3.0.0`. Users are responsible for monitoring and updating third-party dependencies for security patches. Review release notes before upgrading.
415
+ - **Dependency security** — `fastmcp` is pinned to version `3.0.2` and `webull-openapi-python-sdk` is pinned to `3.0.4`. Users are responsible for monitoring and updating third-party dependencies for security patches. Review release notes before upgrading.
406
416
 
407
417
  ---
408
418
 
@@ -498,6 +508,7 @@ webull-openapi-mcp/
498
508
  │ │ ├── event.py # Event contract market data
499
509
  │ │ ├── screener.py # Gainers/losers, most active, market sectors, high dividend, 52-week high/low
500
510
  │ │ ├── watchlist.py# Watchlist CRUD and instrument management
511
+ │ │ ├── community.py # Community posts, comments, hot stocks
501
512
  │ │ ├── fundamental.py # Company profile, analyst data; stock & fund fundamentals (US/HK/JP)
502
513
  │ │ └── financial.py # Financial statements: alert, indicators, income, balance sheet, cash flow (US/HK/JP)
503
514
  │ └── trading/
@@ -194,7 +194,7 @@ def build_manifest(
194
194
  "region_id": {
195
195
  "type": "string",
196
196
  "title": "Region",
197
- "description": "Webull region: us, hk, jp, sg, my, uk, mx, br, or za.",
197
+ "description": "Webull region: us, hk, jp, sg, th, my, uk, mx, br, or za.",
198
198
  "default": "us",
199
199
  "required": True,
200
200
  },
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "webull-openapi-mcp"
7
- version = "1.2.4"
7
+ version = "1.2.6"
8
8
  description = "MCP Server for Webull OpenAPI - enables AI assistants to securely access Webull trading and market data"
9
9
  readme = "README.md"
10
10
  license = "Apache-2.0"
@@ -24,7 +24,7 @@ dependencies = [
24
24
  "fastmcp==3.0.2",
25
25
  "click",
26
26
  "python-dotenv",
27
- "webull-openapi-python-sdk==3.0.0",
27
+ "webull-openapi-python-sdk==3.0.4",
28
28
  ]
29
29
 
30
30
  [project.urls]
@@ -3,7 +3,7 @@
3
3
  "name": "io.github.webull-inc/webull-openapi-mcp",
4
4
  "title": "Webull OpenAPI",
5
5
  "description": "MCP server for Webull OpenAPI: trading and market data access for AI assistants",
6
- "version": "1.2.4",
6
+ "version": "1.2.6",
7
7
  "repository": {
8
8
  "url": "https://github.com/webull-inc/webull-openapi-mcp",
9
9
  "source": "github"
@@ -13,7 +13,7 @@
13
13
  "registryType": "pypi",
14
14
  "registryBaseUrl": "https://pypi.org",
15
15
  "identifier": "webull-openapi-mcp",
16
- "version": "1.2.4",
16
+ "version": "1.2.6",
17
17
  "runtimeHint": "uvx",
18
18
  "transport": {
19
19
  "type": "stdio"
@@ -0,0 +1,126 @@
1
+ """Unit tests for community formatters (posts, comments, hot stocks)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import pytest
6
+
7
+ from webull_openapi_mcp.formatters import (
8
+ format_community_create_result,
9
+ format_community_hot_stocks,
10
+ format_community_post_detail,
11
+ format_community_post_list,
12
+ )
13
+
14
+
15
+ _NO_DATA = "No data available."
16
+
17
+
18
+ # ---------------------------------------------------------------------------
19
+ # Empty / None data handling
20
+ # ---------------------------------------------------------------------------
21
+
22
+ @pytest.mark.parametrize("fn", [
23
+ format_community_hot_stocks,
24
+ format_community_post_list,
25
+ format_community_post_detail,
26
+ ])
27
+ def test_returns_no_data_for_none(fn):
28
+ assert fn(None) == _NO_DATA
29
+
30
+
31
+ @pytest.mark.parametrize("fn", [
32
+ format_community_hot_stocks,
33
+ format_community_post_list,
34
+ ])
35
+ def test_returns_no_data_for_empty_list(fn):
36
+ assert fn([]) == _NO_DATA
37
+
38
+
39
+ # ---------------------------------------------------------------------------
40
+ # Create result (post / comment)
41
+ # ---------------------------------------------------------------------------
42
+
43
+ class TestFormatCreateResult:
44
+ def test_formats_post_id(self):
45
+ out = format_community_create_result({"post_id": "995005758239301632"})
46
+ assert "Post ID: 995005758239301632" in out
47
+
48
+ def test_formats_comment_id(self):
49
+ out = format_community_create_result({"comment_id": "123456"})
50
+ assert "Comment ID: 123456" in out
51
+
52
+ def test_handles_unexpected_shape(self):
53
+ # Non-dict / empty still produces a safe message rather than crashing.
54
+ assert format_community_create_result(None) == "Operation completed."
55
+
56
+
57
+ # ---------------------------------------------------------------------------
58
+ # Hot stocks
59
+ # ---------------------------------------------------------------------------
60
+
61
+ class TestFormatHotStocks:
62
+ def test_formats_ranking(self):
63
+ data = {"data": [
64
+ {"rank": 1, "symbol": "NVDA", "category": "US_STOCK"},
65
+ {"rank": 2, "symbol": "AAPL", "category": "US_STOCK"},
66
+ ]}
67
+ out = format_community_hot_stocks(data)
68
+ assert "Community Hot Stocks" in out
69
+ assert "#1 NVDA (US_STOCK)" in out
70
+ assert "#2 AAPL (US_STOCK)" in out
71
+
72
+ def test_accepts_bare_list(self):
73
+ out = format_community_hot_stocks([{"rank": 1, "symbol": "TSLA", "category": "US_STOCK"}])
74
+ assert "TSLA" in out
75
+
76
+
77
+ # ---------------------------------------------------------------------------
78
+ # Post list (by symbol / my posts)
79
+ # ---------------------------------------------------------------------------
80
+
81
+ class TestFormatPostList:
82
+ def test_formats_posts_and_pagination(self):
83
+ data = {
84
+ "data": [
85
+ {"post_id": "1", "text": "hello", "publish_time": "2026-10-20",
86
+ "thumbs": 3, "comments": 1},
87
+ ],
88
+ "pagination_key": "cursor-abc",
89
+ }
90
+ out = format_community_post_list(data, title="Posts by Symbol")
91
+ assert "Community Posts by Symbol" in out
92
+ assert "Post ID: 1" in out
93
+ assert "pagination_key=cursor-abc" in out
94
+
95
+ def test_no_pagination_hint_when_absent(self):
96
+ data = {"data": [{"post_id": "1", "text": "hi", "publish_time": "t",
97
+ "thumbs": 0, "comments": 0}]}
98
+ out = format_community_post_list(data, title="My Posts")
99
+ assert "pagination_key" not in out
100
+
101
+
102
+ # ---------------------------------------------------------------------------
103
+ # Post detail
104
+ # ---------------------------------------------------------------------------
105
+
106
+ class TestFormatPostDetail:
107
+ def test_formats_detail_with_tickers(self):
108
+ data = {
109
+ "post_id": "995",
110
+ "text": "great earnings",
111
+ "publish_time": "2026-10-20",
112
+ "thumbs": 10,
113
+ "comments": 4,
114
+ "tickers": [{"symbol": "NVDA", "category": "US_STOCK"}],
115
+ }
116
+ out = format_community_post_detail(data)
117
+ assert "Community Post Detail" in out
118
+ assert "Post ID: 995" in out
119
+ assert "NVDA (US_STOCK)" in out
120
+
121
+ def test_formats_detail_without_tickers(self):
122
+ data = {"post_id": "1", "text": "x", "publish_time": "t",
123
+ "thumbs": 0, "comments": 0}
124
+ out = format_community_post_detail(data)
125
+ assert "Post ID: 1" in out
126
+ assert "Tickers:" not in out
@@ -1,4 +1,4 @@
1
- """Tool registration & SDK-wiring tests for the 24 new fundamental/financial/screener tools."""
1
+ """Tool registration & SDK-wiring tests for the 35 new fundamental/financial/news/screener tools."""
2
2
 
3
3
  from __future__ import annotations
4
4
 
@@ -13,6 +13,7 @@ from webull_openapi_mcp.tools.market_data.fundamental import (
13
13
  register_extended_fundamental_tools,
14
14
  )
15
15
  from webull_openapi_mcp.tools.market_data.financial import register_financial_tools
16
+ from webull_openapi_mcp.tools.market_data.news import register_news_tools
16
17
  from webull_openapi_mcp.tools.market_data.screener import (
17
18
  register_screener_tools,
18
19
  register_extended_screener_tools,
@@ -31,7 +32,10 @@ def _build_mcp() -> tuple[FastMCP, MagicMock]:
31
32
  audit = MagicMock()
32
33
  config = _config()
33
34
  register_fundamental_tools(mcp, sdk, audit, config)
35
+ # register_extended_* also registers the US-only insight tools because
36
+ # config.region_id is "us".
34
37
  register_extended_fundamental_tools(mcp, sdk, audit, config)
38
+ register_news_tools(mcp, sdk, audit, config)
35
39
  register_financial_tools(mcp, sdk, audit, config)
36
40
  register_screener_tools(mcp, sdk, audit, config)
37
41
  register_extended_screener_tools(mcp, sdk, audit, config)
@@ -199,15 +203,158 @@ def test_new_tools_registered_for_supported_regions(region):
199
203
  assert not missing, f"{region} missing: {missing}"
200
204
 
201
205
 
202
- @pytest.mark.parametrize("region", ["sg", "my", "uk", "mx", "br"])
206
+ @pytest.mark.parametrize("region", ["sg", "th", "my", "uk", "mx", "br"])
203
207
  def test_new_tools_absent_for_unsupported_regions(region):
204
208
  names = _build_server_tools(region)
205
209
  present = [t for t in EXPECTED_TOOLS if t in names]
206
210
  assert present, f"{region} should not expose: {present}"
207
211
 
208
212
 
209
- @pytest.mark.parametrize("region", ["us", "hk", "jp", "sg", "my", "uk", "mx", "br"])
213
+ @pytest.mark.parametrize("region", ["us", "hk", "jp", "sg", "th", "my", "uk", "mx", "br"])
210
214
  def test_legacy_tools_available_in_all_regions(region):
211
215
  names = _build_server_tools(region)
212
216
  missing = [t for t in LEGACY_TOOLS if t not in names]
213
217
  assert not missing, f"{region} missing legacy: {missing}"
218
+
219
+
220
+ def test_market_sentiment_invokes_sdk_with_us_category():
221
+ mcp, sdk = _build_mcp()
222
+ sdk.data.screener.get_fear_and_greed_index.return_value = {
223
+ "time_zone": "America/New_York",
224
+ "current": {"date": "2026-10-08", "fear_greed_index": 43, "grade": "Neutral"},
225
+ }
226
+ text = _call(mcp, "get_market_sentiment", {})
227
+ sdk.data.screener.get_fear_and_greed_index.assert_called_once_with(category="US_STOCK")
228
+ assert "Market Sentiment Index" in text
229
+ assert "43" in text
230
+
231
+
232
+ def test_unusual_activities_invokes_screener_with_filters():
233
+ mcp, sdk = _build_mcp()
234
+ sdk.data.screener.get_stock_unusual_activities.return_value = {
235
+ "data": [{"symbol": "NVDA", "alert_type": "VOLUME_SURGE"}]
236
+ }
237
+ text = _call(mcp, "get_stock_unusual_activities",
238
+ {"symbol": "NVDA", "count": 5, "extended_hours_required": True})
239
+ _, kwargs = sdk.data.screener.get_stock_unusual_activities.call_args
240
+ assert kwargs["symbol"] == "NVDA"
241
+ assert kwargs["category"] == "US_STOCK"
242
+ assert kwargs["count"] == 5
243
+ assert kwargs["extended_hours_required"] is True
244
+ assert "Stock Unusual Activities" in text
245
+
246
+
247
+ def test_search_institutions_invokes_sdk():
248
+ mcp, sdk = _build_mcp()
249
+ sdk.data.fundamentals.get_institution_search.return_value = {
250
+ "data": [{"cik": "0001067983", "name": "Berkshire Hathaway"}]
251
+ }
252
+ text = _call(mcp, "get_institution_identifier", {"keyword": "Berkshire", "pagination_key": "k"})
253
+ sdk.data.fundamentals.get_institution_search.assert_called_once_with(
254
+ "Berkshire", "k"
255
+ )
256
+ assert "Institution Search" in text
257
+ assert "Berkshire Hathaway" in text
258
+
259
+
260
+ def test_institution_holdings_invokes_sdk():
261
+ mcp, sdk = _build_mcp()
262
+ sdk.data.fundamentals.get_institution_holdings.return_value = {
263
+ "data": [{"stock": "AAPL", "quantity": "49665910"}],
264
+ }
265
+ text = _call(mcp, "get_institution_holdings", {"cik": "1067983"})
266
+ sdk.data.fundamentals.get_institution_holdings.assert_called_once_with("1067983", None)
267
+ assert "Institution 13F Holdings" in text
268
+ assert "AAPL" in text
269
+
270
+
271
+ def test_institution_holding_changes_invokes_sdk():
272
+ mcp, sdk = _build_mcp()
273
+ sdk.data.fundamentals.get_institution_holding_changes.return_value = {
274
+ "data": [{"target_symbol": "AAPL"}],
275
+ }
276
+ text = _call(mcp, "get_institution_holding_changes", {"cik": "1067983", "position": "open"})
277
+ sdk.data.fundamentals.get_institution_holding_changes.assert_called_once_with(
278
+ "1067983", "OPEN", pagination_key=None
279
+ )
280
+ assert "Institution Holding Changes" in text
281
+
282
+
283
+ def test_insider_transactions_invokes_sdk():
284
+ mcp, sdk = _build_mcp()
285
+ sdk.data.fundamentals.get_insider_transactions.return_value = {
286
+ "data": [{"side": "BUY", "insider_name": "Gawel Scott"}],
287
+ }
288
+ text = _call(mcp, "get_insider_transactions", {"symbol": "NVDA", "side": "sell"})
289
+ _, kwargs = sdk.data.fundamentals.get_insider_transactions.call_args
290
+ assert kwargs["side"] == "SELL"
291
+ assert kwargs["category"] == "US_STOCK"
292
+ assert "Insider Transactions" in text
293
+
294
+
295
+ def test_insider_holdings_overview_invokes_sdk():
296
+ mcp, sdk = _build_mcp()
297
+ sdk.data.fundamentals.get_insider_holdings.return_value = {
298
+ "symbol": "NVDA", "insiders_count": 20, "ownership_pct": "3.9665",
299
+ "changes": [{"period": "1M", "shares": "1000"}],
300
+ }
301
+ text = _call(mcp, "get_insider_holdings", {"symbol": "NVDA", "compare_months": 3})
302
+ _, kwargs = sdk.data.fundamentals.get_insider_holdings.call_args
303
+ assert kwargs["category"] == "US_STOCK"
304
+ assert kwargs["compare_months"] == 3
305
+ assert "Insider Holdings Overview" in text
306
+ # The 'changes' list must not be silently dropped (see format fix).
307
+ assert "changes" in text
308
+
309
+
310
+ def test_economic_calendar_invokes_sdk_with_us_region():
311
+ mcp, sdk = _build_mcp()
312
+ sdk.data.fundamentals.get_economic_calendars.return_value = {
313
+ "data": [{"event_name": "Unemployment Claimant", "publish_date": "2026-10-16"}],
314
+ }
315
+ text = _call(mcp, "get_macroeconomic_calendar", {"start_date": "2026-10-09"})
316
+ _, kwargs = sdk.data.fundamentals.get_economic_calendars.call_args
317
+ assert kwargs["region"] == "US"
318
+ assert kwargs["start_date"] == "2026-10-09"
319
+ assert "US Economic Calendar" in text
320
+
321
+
322
+ def test_search_news_invokes_sdk():
323
+ mcp, sdk = _build_mcp()
324
+ sdk.data.news.get_news_search.return_value = {
325
+ "keyword": "Starlink",
326
+ "pagination_key": "1791348877000",
327
+ "data": [{"news_id": "15716519902127104", "title": "Musk calls spectrum deal"}],
328
+ }
329
+ text = _call(mcp, "search_news", {"keyword": "Starlink"})
330
+ sdk.data.news.get_news_search.assert_called_once_with("Starlink", None, None, None)
331
+ assert "News Keyword Search" in text
332
+ assert "Musk calls spectrum deal" in text
333
+
334
+
335
+ def test_news_detail_invokes_sdk():
336
+ mcp, sdk = _build_mcp()
337
+ sdk.data.news.get_news_detail.return_value = {
338
+ "news_id": "15716519902127104",
339
+ "title": "Musk calls spectrum deal",
340
+ "content": "<p>full article body</p>",
341
+ }
342
+ text = _call(mcp, "get_news_detail", {"news_id": "15716519902127104"})
343
+ sdk.data.news.get_news_detail.assert_called_once_with("15716519902127104")
344
+ assert "News Detail" in text
345
+ assert "full article body" in text
346
+
347
+
348
+ def test_stock_news_invokes_sdk_with_us_category():
349
+ mcp, sdk = _build_mcp()
350
+ sdk.data.news.get_stock_news.return_value = {
351
+ "symbol": "TSLA",
352
+ "pagination_key": "1791348877000",
353
+ "data": [{"news_id": "15716519902127104", "title": "some title"}],
354
+ }
355
+ text = _call(mcp, "get_stock_news", {"symbol": "TSLA", "sentiment": "positive"})
356
+ args, kwargs = sdk.data.news.get_stock_news.call_args
357
+ assert args == ("TSLA",)
358
+ assert kwargs["category"] == "US_STOCK"
359
+ assert kwargs["sentiment"] == "POSITIVE"
360
+ assert "Stock News List" in text
@@ -9,6 +9,7 @@ from webull_openapi_mcp.formatters import (
9
9
  format_capital_flow,
10
10
  format_cash_flow,
11
11
  format_dividend_calendar,
12
+ format_economic_calendar,
12
13
  format_earnings_calendar,
13
14
  format_financial_alert,
14
15
  format_financial_indicators,
@@ -24,6 +25,11 @@ from webull_openapi_mcp.formatters import (
24
25
  format_fund_splits,
25
26
  format_income_statement,
26
27
  format_industry_comparison,
28
+ format_institution_holding_changes,
29
+ format_institution_holdings,
30
+ format_institution_search,
31
+ format_insider_holdings_overview,
32
+ format_insider_transactions,
27
33
  format_sec_filings,
28
34
  )
29
35
 
@@ -56,6 +62,12 @@ _NO_DATA = "No data available."
56
62
  format_income_statement,
57
63
  format_balance_sheet,
58
64
  format_cash_flow,
65
+ format_institution_search,
66
+ format_institution_holdings,
67
+ format_institution_holding_changes,
68
+ format_insider_transactions,
69
+ format_insider_holdings_overview,
70
+ format_economic_calendar,
59
71
  ])
60
72
  def test_returns_no_data_for_none(fn):
61
73
  assert fn(None) == _NO_DATA
@@ -532,3 +544,76 @@ class TestFormatCashFlow:
532
544
  assert "800" in result
533
545
  assert "Capital Expenditure" in result
534
546
  assert "-250" in result
547
+
548
+
549
+ class TestFormatInstitutionSearch:
550
+ def test_formats_list_with_pagination(self):
551
+ data = {
552
+ "keyword": "berkshire",
553
+ "pagination_key": "Ne8Ld4XUwpFFRMWyI2Z1",
554
+ "data": [
555
+ {"cik": "0001067983", "name": "Berkshire Hathaway Inc",
556
+ "notable_people": "Warren Buffett"},
557
+ ],
558
+ }
559
+ result = format_institution_search(data)
560
+ assert "=== Institution Search ===" in result
561
+ assert "[1]" in result
562
+ assert "Berkshire Hathaway Inc" in result
563
+ assert "Warren Buffett" in result
564
+ assert "Next pagination_key: Ne8Ld4XUwpFFRMWyI2Z1" in result
565
+
566
+
567
+ class TestFormatInstitutionHoldings:
568
+ def test_formats_list_with_pagination(self):
569
+ data = {"data": [{"stock": "AAPL", "quantity": "49665910"}]}
570
+ result = format_institution_holdings(data)
571
+ assert "=== Institution 13F Holdings ===" in result
572
+ assert "AAPL" in result
573
+
574
+
575
+ class TestFormatInstitutionHoldingChanges:
576
+ def test_formats_list_with_pagination(self):
577
+ data = {
578
+ "next_pagination_key": "b25664000b8a04fb4fee",
579
+ "data": [{"stock": "AAPL", "position": "OPEN"}],
580
+ }
581
+ result = format_institution_holding_changes(data)
582
+ assert "=== Institution Holding Changes ===" in result
583
+ assert "Next pagination_key: b25664000b8a04fb4fee" in result
584
+
585
+
586
+ class TestFormatInsiderTransactions:
587
+ def test_formats_list_with_pagination(self):
588
+ data = {
589
+ "data": [
590
+ {"side": "BUY", "shares": "13866", "price": "0",
591
+ "insider_name": "Gawel Scott", "transaction_date": "2026-06-08"},
592
+ ],
593
+ }
594
+ result = format_insider_transactions(data)
595
+ assert "=== Insider Transactions ===" in result
596
+ assert "Gawel Scott" in result
597
+ assert "2026-06-08" in result
598
+
599
+
600
+ class TestFormatInsiderHoldingsOverview:
601
+ def test_formats_overview(self):
602
+ data = {"symbol": "NVDA", "insiders_count": 20, "ownership_pct": "3.9665"}
603
+ result = format_insider_holdings_overview(data)
604
+ assert "=== Insider Holdings Overview ===" in result
605
+ assert "3.9665" in result
606
+
607
+
608
+ class TestFormatEconomicCalendar:
609
+ def test_formats_list_with_pagination(self):
610
+ data = {
611
+ "pkey": "iPxeAijSJUKDUMAAAiAAAA",
612
+ "data": [
613
+ {"event_name": "Net L-T Flows,Incl.Swaps", "publish_date": "2026-10-16",
614
+ "is_fed_related": False},
615
+ ],
616
+ }
617
+ result = format_economic_calendar(data)
618
+ assert "=== US Economic Calendar ===" in result
619
+ assert "2026-10-16" in result