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.
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/.env.example +1 -1
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/PKG-INFO +14 -3
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/README.md +12 -1
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/mcpb/build_manifest.py +1 -1
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/pyproject.toml +2 -2
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/server.json +2 -2
- webull_openapi_mcp-1.2.6/tests/test_community_tools.py +126 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_fundamental_screener_registration.py +150 -3
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_fundamental_tools.py +85 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_guards.py +114 -1
- webull_openapi_mcp-1.2.6/tests/test_news_tools.py +52 -0
- webull_openapi_mcp-1.2.6/tests/test_order_history_tool.py +96 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_property_server.py +185 -2
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_screener_tools.py +34 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_sdk_client.py +2 -2
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/__init__.py +1 -1
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/cli.py +1 -1
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/errors.py +51 -14
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/formatters.py +302 -32
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/guards.py +193 -27
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/region_config.py +49 -11
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/sdk_client.py +7 -1
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/server.py +17 -2
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/__init__.py +4 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/__init__.py +2 -0
- webull_openapi_mcp-1.2.6/webull_openapi_mcp/tools/market_data/community.py +197 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/fundamental.py +211 -1
- webull_openapi_mcp-1.2.6/webull_openapi_mcp/tools/market_data/news.py +122 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/option.py +30 -5
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/screener.py +71 -1
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/account.py +7 -4
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/assets.py +12 -12
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/instrument.py +106 -31
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/option_order.py +64 -4
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/order.py +46 -31
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/.github/workflows/pypi-ci.yml +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/.gitignore +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/DISCLAIMER.md +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/LICENSE +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/mcpb/.mcpbignore +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/mcpb/build.sh +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/mcpb/src/server.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/__init__.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_account_tools.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_audit.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_cli.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_config.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_errors.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_formatters.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_instrument_tools.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_market_data_tools.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_property_audit.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_property_config.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_property_formatters.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_property_guards.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_property_sdk.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/tests/test_stock_order_tools.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/__main__.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/audit.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/config.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/constants.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/crypto.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/event.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/financial.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/futures.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/stock.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/market_data/watchlist.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/__init__.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/crypto_order.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/event_order.py +0 -0
- {webull_openapi_mcp-1.2.4 → webull_openapi_mcp-1.2.6}/webull_openapi_mcp/tools/trading/futures_order.py +0 -0
- {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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|