Skip to content

Commit 2977330

Browse files
author
方佳
committed
Merge branch 'main_260515_merge' into 'main'
feat: Added interfaces for company profiles, analyst ratings/target prices,... See merge request webull/webull-openapi-mcp!3
2 parents b4c678b + 474a234 commit 2977330

14 files changed

Lines changed: 1409 additions & 40 deletions

‎README.md‎

Lines changed: 43 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,10 @@ See [DISCLAIMER.md](DISCLAIMER.md) for the full disclaimer.
1919

2020
- **Multi-Region Support** — US, HK, and JP regions with region-specific order types, trading sessions, and validation
2121
- **Market Data** — Real-time snapshots, tick data, quotes (depth), footprint, and OHLCV bars for stocks, futures, crypto, and event contracts
22+
- **NOII Data** — Net Order Imbalance Indicator bars and snapshots for US stock opening/closing auctions
23+
- **Screener** — Top gainers/losers and most active stocks ranking
24+
- **Watchlist** — Create, manage, and query user watchlists and instruments (US, HK)
25+
- **Fundamental Data** — Company profiles, analyst ratings, and target prices
2226
- **Trading** — Place, modify, cancel orders for stocks, options, futures, crypto, and event contracts
2327
- **Combo Orders** — OTO, OCO, OTOCO combo orders (US only)
2428
- **Option Strategies** — Multi-leg option strategies: vertical, straddle, strangle, butterfly, condor, etc. (US only)
@@ -39,6 +43,23 @@ Here are some prompts you can use with your AI assistant:
3943
- Get a real-time snapshot for AAPL, MSFT, and GOOGL
4044
- What's the current bid/ask for TSLA?
4145
- Show me 1-minute tick data for NVDA
46+
- Show me the NOII data for AAPL before market open
47+
48+
**Screener**
49+
- What are today's top gainers?
50+
- Show me the biggest losers in pre-market
51+
- What are the most actively traded stocks right now?
52+
53+
**Watchlist**
54+
- Show me all my watchlists
55+
- Create a new watchlist called "Tech Stocks"
56+
- Add AAPL and MSFT to my watchlist
57+
- What stocks are in my watchlist?
58+
59+
**Fundamental & Analyst**
60+
- Tell me about NVDA's company profile
61+
- What do analysts rate AAPL?
62+
- What's the analyst target price for TSLA?
4263

4364
**Account & Portfolio**
4465
- What's my account balance and buying power?
@@ -229,18 +250,26 @@ See [.env.example](.env.example) for full configuration template.
229250

230251
| Category | Tools | Region |
231252
|----------|-------|--------|
232-
| **Stock** | `get_stock_tick`, `get_stock_snapshot`, `get_stock_quotes`, `get_stock_footprint`, `get_stock_bars`, `get_stock_bars_single` | US, HK, JP |
253+
| **Stock** | `get_stock_tick`, `get_stock_snapshot`, `get_stock_quotes`, `get_stock_footprint`, `get_stock_bars`, `get_stock_bars_single`, `get_stock_noii_bars`, `get_stock_noii_snapshot` | US, HK, JP |
233254
| **Futures** | `get_futures_tick`, `get_futures_snapshot`, `get_futures_depth`, `get_futures_bars`, `get_futures_footprint` | US |
234255
| **Crypto** | `get_crypto_snapshot`, `get_crypto_bars` | US |
235256
| **Event** | `get_event_tick`, `get_event_snapshot`, `get_event_depth`, `get_event_bars` | US |
257+
| **Screener** | `get_gainers_losers`, `get_most_active` | US, HK, JP |
258+
| **Watchlist** | `get_watchlists`, `create_watchlist`, `update_watchlist`, `delete_watchlist`, `get_watchlist_instruments`, `add_watchlist_instruments`, `remove_watchlist_instruments`, `update_watchlist_instruments` | US, HK, JP |
259+
260+
### Fundamental & Instrument
261+
262+
| Category | Tools | Region |
263+
|----------|-------|--------|
264+
| **Instrument** | `get_instruments`, `get_futures_instruments`, `get_futures_products`, `get_crypto_instruments`, `get_event_series`, `get_event_instruments`, `get_event_categories`, `get_event_events` | varies |
265+
| **Fundamental** | `get_company_profile`, `get_analyst_rating`, `get_analyst_target_price` | US, HK, JP |
236266

237267
### Trading
238268

239269
| Category | Tools | Region |
240270
|----------|-------|--------|
241271
| **Account** | `get_account_list` | US, HK, JP |
242272
| **Assets** | `get_account_balance`, `get_account_positions`, `get_account_position_details` (JP only) | US, HK, JP |
243-
| **Instrument** | `get_instruments`, `get_futures_instruments`, `get_futures_products`, `get_crypto_instruments`, `get_event_series`, `get_event_instruments`, `get_event_categories`, `get_event_events` | varies |
244273
| **Stock Order** | `place_stock_order`, `preview_stock_order`, `replace_stock_order` | US, HK, JP |
245274
| **Combo Order** | `place_stock_combo_order` (OTO/OCO/OTOCO) | US |
246275
| **Option Order** | `place_option_single_order`, `preview_option_order`, `replace_option_order` | US, HK |
@@ -263,12 +292,18 @@ See [.env.example](.env.example) for full configuration template.
263292
| Combo Orders | Yes | No | No |
264293
| Option Strategies | Yes | No | No |
265294
| Algo Orders | Yes | No | No |
295+
| Screener (Gainers/Losers/Active) | Yes | Yes | Yes |
296+
| Watchlist | Yes | Yes | Yes |
297+
| Fundamental (Company/Analyst) | Yes | Yes | Yes |
298+
| NOII (Auction Imbalance) | Yes | Yes | Yes |
266299
| Markets | US | US, HK, CN | US, JP |
267300
| Instrument Categories | US_STOCK, US_ETF | US_STOCK, US_ETF, HK_STOCK, CN_STOCK | US_STOCK, US_ETF |
268301
| Order Types | LIMIT, MARKET, STOP_LOSS, STOP_LOSS_LIMIT, TRAILING_STOP_LOSS, etc. | LIMIT, MARKET, ENHANCED_LIMIT, AT_AUCTION, AT_AUCTION_LIMIT, etc. | JP market: LIMIT, MARKET — US market: LIMIT, MARKET, STOP_LOSS, STOP_LOSS_LIMIT |
269302
| Time-in-Force | DAY, GTC | US market: DAY, GTC, GTD — HK market: DAY, GTC — CN market: DAY | JP market: DAY — US market: DAY, GTC, GTD |
270303
| JP Order Fields | — | — | `account_tax_type` required (GENERAL or SPECIFIC); `margin_type` (ONE_DAY or INDEFINITE) and `position_intent` optional margin-account-only fields; `close_contracts` optional |
271304

305+
> **Note:** Screener, Fundamental, and NOII currently only support querying US stock data (`US_STOCK` category). Watchlist supports US stocks and HK stocks.
306+
272307
---
273308

274309
## CLI Commands
@@ -312,7 +347,7 @@ All commands accept `--env-file PATH` to specify a custom `.env` file location (
312347
- **Review before trading** — Always review order details proposed by the AI before confirming. Use `preview_stock_order` / `preview_option_order` before placing orders.
313348
- **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`.
314349
- **Default sandbox** — The server defaults to UAT (sandbox) environment. You must explicitly set `WEBULL_ENVIRONMENT=prod` for live trading.
315-
- **Dependency security** — `fastmcp` is pinned to version `3.0.2` and `webull-openapi-python-sdk` is pinned to `2.0.5`. Users are responsible for monitoring and updating third-party dependencies for security patches. Review release notes before upgrading.
350+
- **Dependency security** — `fastmcp` is pinned to version `3.0.2` and `webull-openapi-python-sdk` is pinned to `2.0.7`. Users are responsible for monitoring and updating third-party dependencies for security patches. Review release notes before upgrading.
316351

317352
---
318353

@@ -395,10 +430,13 @@ webull-openapi-mcp/
395430
│ └── tools/
396431
│ ├── __init__.py # Tool registration exports
397432
│ ├── market_data/
398-
│ │ ├── stock.py # Stock market data (snapshot, quotes, bars, tick, footprint)
433+
│ │ ├── stock.py # Stock market data (snapshot, quotes, bars, tick, footprint, NOII)
399434
│ │ ├── futures.py # Futures market data
400435
│ │ ├── crypto.py # Crypto market data
401-
│ │ └── event.py # Event contract market data
436+
│ │ ├── event.py # Event contract market data
437+
│ │ ├── screener.py # Gainers/losers, most active rankings
438+
│ │ ├── watchlist.py# Watchlist CRUD and instrument management
439+
│ │ └── fundamental.py # Company profile, analyst ratings/target price
402440
│ └── trading/
403441
│ ├── account.py # Account list
404442
│ ├── assets.py # Balance, positions

‎pyproject.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
44

55
[project]
66
name = "webull-openapi-mcp"
7-
version = "1.0.0"
7+
version = "1.1.0"
88
description = "MCP Server for Webull OpenAPI - enables AI assistants to securely access Webull trading and market data"
99
readme = "README.md"
1010
license = "Apache-2.0"
@@ -24,7 +24,7 @@ dependencies = [
2424
"fastmcp==3.0.2",
2525
"click",
2626
"python-dotenv",
27-
"webull-openapi-python-sdk==2.0.5",
27+
"webull-openapi-python-sdk==2.0.7",
2828
]
2929

3030
[project.urls]

‎tests/test_instrument_tools.py‎

Lines changed: 154 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,154 @@
1+
"""Unit tests for instrument tools (company profile, analyst rating, analyst target price)."""
2+
3+
from __future__ import annotations
4+
5+
import pytest
6+
7+
from webull_openapi_mcp.formatters import (
8+
format_analyst_rating,
9+
format_analyst_target_price,
10+
format_company_profile,
11+
)
12+
13+
14+
_NO_DATA = "No data available."
15+
16+
17+
# ---------------------------------------------------------------------------
18+
# Empty / None data handling
19+
# ---------------------------------------------------------------------------
20+
21+
@pytest.mark.parametrize("fn", [
22+
format_company_profile,
23+
format_analyst_rating,
24+
format_analyst_target_price,
25+
])
26+
def test_returns_no_data_for_none(fn):
27+
assert fn(None) == _NO_DATA
28+
29+
30+
@pytest.mark.parametrize("fn", [
31+
format_company_profile,
32+
format_analyst_rating,
33+
format_analyst_target_price,
34+
])
35+
def test_returns_no_data_for_empty_dict(fn):
36+
assert fn({}) == _NO_DATA
37+
38+
39+
# ---------------------------------------------------------------------------
40+
# Company Profile
41+
# ---------------------------------------------------------------------------
42+
43+
class TestFormatCompanyProfile:
44+
def test_formats_basic_fields(self):
45+
data = {
46+
"symbol": "AAPL",
47+
"company_name": "Apple Inc",
48+
"exhibition_code": "NASDAQ",
49+
"ceo": "Tim Cook",
50+
"employees": "164000",
51+
"establish_date": "1977-04-01",
52+
"address": "One Apple Park Way, Cupertino, CA",
53+
"industries": ["Consumer Electronics", "Technology"],
54+
"profile": "Apple designs and manufactures consumer electronics.",
55+
}
56+
result = format_company_profile(data)
57+
assert "AAPL" in result
58+
assert "Apple Inc" in result
59+
assert "NASDAQ" in result
60+
assert "Tim Cook" in result
61+
assert "164000" in result
62+
assert "Consumer Electronics" in result
63+
assert "Apple designs" in result
64+
65+
def test_handles_missing_optional_fields(self):
66+
data = {"symbol": "TSLA"}
67+
result = format_company_profile(data)
68+
assert "TSLA" in result
69+
assert "N/A" in result
70+
71+
def test_industries_as_string(self):
72+
data = {"symbol": "X", "industries": "Mining"}
73+
result = format_company_profile(data)
74+
assert "Mining" in result
75+
76+
def test_no_profile_description(self):
77+
data = {"symbol": "GOOG", "company_name": "Alphabet"}
78+
result = format_company_profile(data)
79+
assert "Description" not in result
80+
81+
def test_non_dict_returns_no_data(self):
82+
assert format_company_profile("invalid") == _NO_DATA
83+
assert format_company_profile([]) == _NO_DATA
84+
85+
86+
# ---------------------------------------------------------------------------
87+
# Analyst Rating
88+
# ---------------------------------------------------------------------------
89+
90+
class TestFormatAnalystRating:
91+
def test_formats_all_fields(self):
92+
data = {
93+
"symbol": "NVDA",
94+
"number": "58",
95+
"strong_buy": "43",
96+
"buy": "11",
97+
"hold": "4",
98+
"sell": "0",
99+
"under_perform": "0",
100+
"effective_start_date": "2021-12-29",
101+
}
102+
result = format_analyst_rating(data)
103+
assert "NVDA" in result
104+
assert "58" in result
105+
assert "43" in result
106+
assert "11" in result
107+
assert "4" in result
108+
assert "2021-12-29" in result
109+
110+
def test_handles_missing_fields(self):
111+
data = {"symbol": "AAPL", "number": "47"}
112+
result = format_analyst_rating(data)
113+
assert "AAPL" in result
114+
assert "47" in result
115+
assert "N/A" in result
116+
117+
def test_non_dict_returns_no_data(self):
118+
assert format_analyst_rating("invalid") == _NO_DATA
119+
assert format_analyst_rating([]) == _NO_DATA
120+
121+
122+
# ---------------------------------------------------------------------------
123+
# Analyst Target Price
124+
# ---------------------------------------------------------------------------
125+
126+
class TestFormatAnalystTargetPrice:
127+
def test_formats_all_fields(self):
128+
data = {
129+
"symbol": "AAPL",
130+
"mean": "226.18",
131+
"low": "170",
132+
"high": "300",
133+
"median": "220",
134+
"currency": "USD",
135+
"effective_start_date": "2024-01-01",
136+
}
137+
result = format_analyst_target_price(data)
138+
assert "AAPL" in result
139+
assert "226.18" in result
140+
assert "170" in result
141+
assert "300" in result
142+
assert "220" in result
143+
assert "USD" in result
144+
assert "2024-01-01" in result
145+
146+
def test_handles_missing_fields(self):
147+
data = {"symbol": "TSLA"}
148+
result = format_analyst_target_price(data)
149+
assert "TSLA" in result
150+
assert "N/A" in result
151+
152+
def test_non_dict_returns_no_data(self):
153+
assert format_analyst_target_price("invalid") == _NO_DATA
154+
assert format_analyst_target_price([]) == _NO_DATA

0 commit comments

Comments
 (0)