From b120d5787e5802a42c6da50c585bac01c2cd5440 Mon Sep 17 00:00:00 2001 From: ZackFan Date: Mon, 22 Jun 2026 17:30:45 +0800 Subject: [PATCH 1/4] feat: add stock ownership etf_holdings REST endpoint Add client method for GET /v1.0/stock/ownership/etf-holdings/{symbol}, exposing daily ETF component holdings via stock.ownership.etf_holdings() with optional from/to/sort/code query params. Since `from` is a reserved Python keyword, callers pass it as `from_` (mapped to `from` in the query string). Includes the ownership module, client property, tests, and README usage example. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 4 +++ fugle_marketdata/rest/stock/client.py | 5 ++++ fugle_marketdata/rest/stock/ownership.py | 10 +++++++ tests/test_http_client.py | 36 ++++++++++++++++++++++++ 4 files changed, 55 insertions(+) create mode 100644 fugle_marketdata/rest/stock/ownership.py diff --git a/README.md b/README.md index 44fb6ee..cfc38f8 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,10 @@ The library is an isomorphic Python client that supports REST API and WebSocket. client = RestClient(api_key = 'YOUR_API_KEY') stock = client.stock # Stock REST API client print(stock.intraday.quote(symbol="2330")) + +# Get the daily component holdings of an ETF. +# Note: `from` is a reserved Python keyword, so pass it as `from_`. +print(stock.ownership.etf_holdings(symbol="0050", from_="2026-05-01", to="2026-05-21", sort="desc")) ``` ### WebSocket API diff --git a/fugle_marketdata/rest/stock/client.py b/fugle_marketdata/rest/stock/client.py index 59a5eab..dcff310 100644 --- a/fugle_marketdata/rest/stock/client.py +++ b/fugle_marketdata/rest/stock/client.py @@ -3,6 +3,7 @@ from .snapshot import Snapshot from .technical import Technical from .corporate_actions import CorporateActions +from .ownership import Ownership class RestStockClient: @@ -29,3 +30,7 @@ def technical(self): @property def corporate_actions(self): return CorporateActions(**self.config) + + @property + def ownership(self): + return Ownership(**self.config) diff --git a/fugle_marketdata/rest/stock/ownership.py b/fugle_marketdata/rest/stock/ownership.py new file mode 100644 index 0000000..42914f6 --- /dev/null +++ b/fugle_marketdata/rest/stock/ownership.py @@ -0,0 +1,10 @@ +from ..base_rest import BaseRest + + +class Ownership(BaseRest): + def etf_holdings(self, **params): + symbol = params.pop('symbol') + # `from` is a reserved keyword in Python, so callers pass `from_`. + # Rebuild the dict to keep the original argument order in the query string. + query = {('from' if key == 'from_' else key): value for key, value in params.items()} + return self.request(f"ownership/etf-holdings/{symbol}", **query) diff --git a/tests/test_http_client.py b/tests/test_http_client.py index ded5d41..f6534f9 100644 --- a/tests/test_http_client.py +++ b/tests/test_http_client.py @@ -218,6 +218,42 @@ def test_historical_stats_bearer_token(self, bearer_client, mocker): headers={'Authorization': 'Bearer bearer-token'} ) +class TestStockRestOwnershipClient: + def test_stock_ownership(self, api_key_client): + stock = api_key_client.stock + assert hasattr(stock.ownership, 'etf_holdings') + + def test_ownership_etf_holdings_api_key(self, mocker, api_key_client): + stock = api_key_client.stock + mock_get = mocker.patch('requests.get') + mock_get.return_value.status_code = 200 + stock.ownership.etf_holdings(symbol='0050') + mock_get.assert_called_once_with( + 'https://api.fugle.tw/marketdata/v1.0/stock/ownership/etf-holdings/0050', + headers={'X-API-KEY': 'api-key'} + ) + + def test_ownership_etf_holdings_bearer_token(self, bearer_client, mocker): + stock = bearer_client.stock + mock_get = mocker.patch('requests.get') + mock_get.return_value.status_code = 200 + stock.ownership.etf_holdings(symbol='0050') + mock_get.assert_called_once_with( + 'https://api.fugle.tw/marketdata/v1.0/stock/ownership/etf-holdings/0050', + headers={'Authorization': 'Bearer bearer-token'} + ) + + def test_ownership_etf_holdings_query_params(self, mocker, api_key_client): + stock = api_key_client.stock + mock_get = mocker.patch('requests.get') + mock_get.return_value.status_code = 200 + stock.ownership.etf_holdings(symbol='0050', from_='2026-05-01', to='2026-05-21', sort='desc', code='2330') + mock_get.assert_called_once_with( + 'https://api.fugle.tw/marketdata/v1.0/stock/ownership/etf-holdings/0050?from=2026-05-01&to=2026-05-21&sort=desc&code=2330', + headers={'X-API-KEY': 'api-key'} + ) + + class TestStockRestSnapshotClient: def test_stock_historical(self, api_key_client): stock = api_key_client.stock From eb9c51d4c73503637f98962f04ced27e53190df5 Mon Sep 17 00:00:00 2001 From: ZackFan Date: Tue, 23 Jun 2026 10:58:10 +0800 Subject: [PATCH 2/4] refactor: align etf_holdings with existing **params convention Drop the bespoke `from_` -> `from` remapping; callers pass `from` via dict unpacking (`**{'from': ...}`) like every other endpoint in the SDK, keeping the API consistent. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 4 ++-- fugle_marketdata/rest/stock/ownership.py | 5 +---- tests/test_http_client.py | 2 +- 3 files changed, 4 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index cfc38f8..1ae69f1 100644 --- a/README.md +++ b/README.md @@ -28,8 +28,8 @@ stock = client.stock # Stock REST API client print(stock.intraday.quote(symbol="2330")) # Get the daily component holdings of an ETF. -# Note: `from` is a reserved Python keyword, so pass it as `from_`. -print(stock.ownership.etf_holdings(symbol="0050", from_="2026-05-01", to="2026-05-21", sort="desc")) +# Note: `from` is a reserved Python keyword, so pass it via dict unpacking. +print(stock.ownership.etf_holdings(symbol="0050", **{"from": "2026-05-01"}, to="2026-05-21", sort="desc")) ``` ### WebSocket API diff --git a/fugle_marketdata/rest/stock/ownership.py b/fugle_marketdata/rest/stock/ownership.py index 42914f6..44954b9 100644 --- a/fugle_marketdata/rest/stock/ownership.py +++ b/fugle_marketdata/rest/stock/ownership.py @@ -4,7 +4,4 @@ class Ownership(BaseRest): def etf_holdings(self, **params): symbol = params.pop('symbol') - # `from` is a reserved keyword in Python, so callers pass `from_`. - # Rebuild the dict to keep the original argument order in the query string. - query = {('from' if key == 'from_' else key): value for key, value in params.items()} - return self.request(f"ownership/etf-holdings/{symbol}", **query) + return self.request(f"ownership/etf-holdings/{symbol}", **params) diff --git a/tests/test_http_client.py b/tests/test_http_client.py index f6534f9..27640b0 100644 --- a/tests/test_http_client.py +++ b/tests/test_http_client.py @@ -247,7 +247,7 @@ def test_ownership_etf_holdings_query_params(self, mocker, api_key_client): stock = api_key_client.stock mock_get = mocker.patch('requests.get') mock_get.return_value.status_code = 200 - stock.ownership.etf_holdings(symbol='0050', from_='2026-05-01', to='2026-05-21', sort='desc', code='2330') + stock.ownership.etf_holdings(symbol='0050', **{'from': '2026-05-01'}, to='2026-05-21', sort='desc', code='2330') mock_get.assert_called_once_with( 'https://api.fugle.tw/marketdata/v1.0/stock/ownership/etf-holdings/0050?from=2026-05-01&to=2026-05-21&sort=desc&code=2330', headers={'X-API-KEY': 'api-key'} From 260b1aedf4eddb62ec55078e1889820587fd3543 Mon Sep 17 00:00:00 2001 From: ZackFan Date: Tue, 23 Jun 2026 11:02:05 +0800 Subject: [PATCH 3/4] docs: drop ETF example comments to match other endpoints Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 3 --- 1 file changed, 3 deletions(-) diff --git a/README.md b/README.md index 1ae69f1..190cf17 100644 --- a/README.md +++ b/README.md @@ -26,9 +26,6 @@ The library is an isomorphic Python client that supports REST API and WebSocket. client = RestClient(api_key = 'YOUR_API_KEY') stock = client.stock # Stock REST API client print(stock.intraday.quote(symbol="2330")) - -# Get the daily component holdings of an ETF. -# Note: `from` is a reserved Python keyword, so pass it via dict unpacking. print(stock.ownership.etf_holdings(symbol="0050", **{"from": "2026-05-01"}, to="2026-05-21", sort="desc")) ``` From dd031295d845fa1e0c1b43daa08fc966ced100b1 Mon Sep 17 00:00:00 2001 From: ZackFan Date: Tue, 23 Jun 2026 11:03:29 +0800 Subject: [PATCH 4/4] docs: remove ETF example from README Other endpoints aren't individually listed in the README; keep it consistent. Co-Authored-By: Claude Opus 4.8 (1M context) --- README.md | 1 - 1 file changed, 1 deletion(-) diff --git a/README.md b/README.md index 190cf17..44fb6ee 100644 --- a/README.md +++ b/README.md @@ -26,7 +26,6 @@ The library is an isomorphic Python client that supports REST API and WebSocket. client = RestClient(api_key = 'YOUR_API_KEY') stock = client.stock # Stock REST API client print(stock.intraday.quote(symbol="2330")) -print(stock.ownership.etf_holdings(symbol="0050", **{"from": "2026-05-01"}, to="2026-05-21", sort="desc")) ``` ### WebSocket API