From d5109c2fccb0d45daa46fd8a9bd9112482b1a9d6 Mon Sep 17 00:00:00 2001 From: Anthony Date: Tue, 24 Feb 2026 21:44:50 +1100 Subject: [PATCH 1/2] Add endpoints for ASX market depth and ASX course of sales --- stake/asx/product.py | 31 ++++++++++++++++- stake/constant.py | 10 ++++++ .../test_asx_product_course_of_sales.yaml | 34 +++++++++++++++++++ .../test_product/test_asx_product_depth.yaml | 34 +++++++++++++++++++ tests/test_product.py | 24 +++++++++++++ 5 files changed, 132 insertions(+), 1 deletion(-) create mode 100644 tests/cassettes/test_product/test_asx_product_course_of_sales.yaml create mode 100644 tests/cassettes/test_product/test_asx_product_depth.yaml diff --git a/stake/asx/product.py b/stake/asx/product.py index 80806ea..e8456c4 100644 --- a/stake/asx/product.py +++ b/stake/asx/product.py @@ -1,4 +1,4 @@ -from typing import List, Optional +from typing import Any, List, Optional from pydantic import BaseModel, ConfigDict @@ -41,6 +41,23 @@ class Product(BaseModel): points_change: Optional[float] = None percentage_change: Optional[float] = None out_of_market_price: Optional[float] = None + + # Aggregated depth fields + id: Optional[str] = None + ticker: Optional[str] = None + total_buy_count: Optional[int] = None + total_sell_count: Optional[int] = None + total_buy_volume: Optional[int] = None + total_sell_volume: Optional[int] = None + buy_orders: Optional[List[dict[str, Any]]] = None + sell_orders: Optional[List[dict[str, Any]]] = None + + # Course of sales fields + total_volume: Optional[int] = None + total_trades: Optional[int] = None + total_value: Optional[float] = None + course_of_sales: Optional[List[dict[str, Any]]] = None + model_config = ConfigDict(alias_generator=camelcase) @@ -57,6 +74,18 @@ async def get(self, symbol: str) -> Optional[Product]: return Product(**data) + async def depth(self, symbol: str) -> Product: + data = await self._client.get( + self._client.exchange.aggregated_depth.format(symbol=symbol) + ) + return Product(**data) + + async def course_of_sales(self, symbol: str) -> Product: + data = await self._client.get( + self._client.exchange.course_of_sales.format(symbol=symbol) + ) + return Product(**data) + async def search(self, request: ProductSearchByName) -> List[Instrument]: products = await self._client.get( self._client.exchange.products_suggestions.format(keyword=request.keyword) diff --git a/stake/constant.py b/stake/constant.py index 6ab0f13..540f8f2 100644 --- a/stake/constant.py +++ b/stake/constant.py @@ -119,6 +119,16 @@ class ASXUrl(BaseModel): market_status: str = urljoin( ASX_STAKE_URL, "api/asx/instrument/quoteTwo/ASX", allow_fragments=True ) + aggregated_depth: str = urljoin( + ASX_STAKE_URL, + "api/asx/instrument/aggregatedDepth/{symbol}?type=EQUITY", + allow_fragments=True, + ) + course_of_sales: str = urljoin( + ASX_STAKE_URL, + "api/asx/instrument/courseOfSales/{symbol}", + allow_fragments=True, + ) orders: str = urljoin(ASX_STAKE_URL, "api/asx/orders", allow_fragments=True) diff --git a/tests/cassettes/test_product/test_asx_product_course_of_sales.yaml b/tests/cassettes/test_product/test_asx_product_course_of_sales.yaml new file mode 100644 index 0000000..9633aff --- /dev/null +++ b/tests/cassettes/test_product/test_asx_product_course_of_sales.yaml @@ -0,0 +1,34 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + Content-Type: + - application/json + method: GET + uri: https://api2.prd.hellostake.com/api/user + response: + body: + string: '{"canTradeOnUnsettledFunds": false, "cpfValue": null, "emailVerified": true, "hasFunded": true, "hasTraded": true, "userId": "7c9bbfae-0000-47b7-0000-0e66d868c2cf", "username": "michael29", "emailAddress": "reevesmegan@gilmore-wright.biz", "dw_AccountId": "1cf93550-8eb4-4c32-a229-826cf8c1be59", "dw_AccountNumber": "z0-0593879b", "macAccountNumber": "d9-0481457G", "status": null, "macStatus": "BASIC_USER", "dwStatus": null, "truliooStatus": "APPROVED", "truliooStatusWithWatchlist": null, "firstName": "Rita", "middleName": null, "lastName": "Jones", "phoneNumber": "(640)242-4270x965", "signUpPhase": 0, "ackSignedWhen": "2023-10-01", "createdDate": 1574303699770, "stakeApprovedDate": null, "accountType": "INDIVIDUAL", "masterAccountId": null, "referralCode": "W2-6612029X", "referredByCode": null, "regionIdentifier": "AUS", "assetSummary": null, "fundingStatistics": null, "tradingStatistics": null, "w8File": [], "rewardJourneyTimestamp": null, "rewardJourneyStatus": null, "userProfile": {"residentialAddress": null, "postalAddress": null}, "ledgerBalance": 0.0, "fxSpeed": "Regular", "dateOfBirth": null, "upToDateDetails2021": "NO_REQUIREMENTS", "stakeKycStatus": "KYC_APPROVED", "awxMigrationDocsRequired": null, "documentsStatus": "NO_ACTION", "accountStatus": "OPEN", "mfaenabled": false}' + headers: {} + status: + code: 200 + message: OK + - request: + body: null + headers: + Accept: + - application/json + Content-Type: + - application/json + method: GET + uri: https://api2.prd.hellostake.com/api/asx/instrument/courseOfSales/ORG + response: + body: + string: '{"ticker":"ORG","totalVolume":4340689,"totalTrades":15192,"totalValue":52386677.84,"courseOfSales":[{"id":"1430712669","instrumentCodeId":"ORG.XAU","exchangeMarket":"ASX","price":12.07,"volume":126,"value":1520.82,"tradeTimeMillis":1771824891233,"cancelledTimeMillis":null,"buyOrderNumber":"8116826296338466744","sellOrderNumber":"8116826296338466744"},{"id":"156728782706","instrumentCodeId":"ORG.XAU","exchangeMarket":"CXA","price":12.07,"volume":4116,"value":49680.12,"tradeTimeMillis":1771824535706,"cancelledTimeMillis":null,"buyOrderNumber":null,"sellOrderNumber":null}]}' + headers: {} + status: + code: 200 + message: OK +version: 1 diff --git a/tests/cassettes/test_product/test_asx_product_depth.yaml b/tests/cassettes/test_product/test_asx_product_depth.yaml new file mode 100644 index 0000000..af00520 --- /dev/null +++ b/tests/cassettes/test_product/test_asx_product_depth.yaml @@ -0,0 +1,34 @@ +interactions: + - request: + body: null + headers: + Accept: + - application/json + Content-Type: + - application/json + method: GET + uri: https://api2.prd.hellostake.com/api/user + response: + body: + string: '{"canTradeOnUnsettledFunds": false, "cpfValue": null, "emailVerified": true, "hasFunded": true, "hasTraded": true, "userId": "7c9bbfae-0000-47b7-0000-0e66d868c2cf", "username": "michael29", "emailAddress": "reevesmegan@gilmore-wright.biz", "dw_AccountId": "1cf93550-8eb4-4c32-a229-826cf8c1be59", "dw_AccountNumber": "z0-0593879b", "macAccountNumber": "d9-0481457G", "status": null, "macStatus": "BASIC_USER", "dwStatus": null, "truliooStatus": "APPROVED", "truliooStatusWithWatchlist": null, "firstName": "Rita", "middleName": null, "lastName": "Jones", "phoneNumber": "(640)242-4270x965", "signUpPhase": 0, "ackSignedWhen": "2023-10-01", "createdDate": 1574303699770, "stakeApprovedDate": null, "accountType": "INDIVIDUAL", "masterAccountId": null, "referralCode": "W2-6612029X", "referredByCode": null, "regionIdentifier": "AUS", "assetSummary": null, "fundingStatistics": null, "tradingStatistics": null, "w8File": [], "rewardJourneyTimestamp": null, "rewardJourneyStatus": null, "userProfile": {"residentialAddress": null, "postalAddress": null}, "ledgerBalance": 0.0, "fxSpeed": "Regular", "dateOfBirth": null, "upToDateDetails2021": "NO_REQUIREMENTS", "stakeKycStatus": "KYC_APPROVED", "awxMigrationDocsRequired": null, "documentsStatus": "NO_ACTION", "accountStatus": "OPEN", "mfaenabled": false}' + headers: {} + status: + code: 200 + message: OK + - request: + body: null + headers: + Accept: + - application/json + Content-Type: + - application/json + method: GET + uri: https://api2.prd.hellostake.com/api/asx/instrument/aggregatedDepth/ORG + response: + body: + string: '{"id":"ORG#depth","ticker":"ORG","totalBuyCount":2,"totalSellCount":2,"totalBuyVolume":17227,"totalSellVolume":34912,"buyOrders":[{"id":"buy-12.06","price":12.06,"volume":15461,"numberOfOrders":1,"value":186459.66,"orders":[{"id":"buy-1","exchange":"ASX","volume":15461,"value":186459.66,"undisclosed":false}]},{"id":"buy-12.05","price":12.05,"volume":1766,"numberOfOrders":1,"value":21280.3,"orders":[{"id":"buy-2","exchange":"ASX","volume":1766,"value":21280.3,"undisclosed":false}]}],"sellOrders":[{"id":"sell-12.08","price":12.08,"volume":514,"numberOfOrders":1,"value":6209.12,"orders":[{"id":"sell-1","exchange":"ASX","volume":514,"value":6209.12,"undisclosed":false}]},{"id":"sell-12.10","price":12.1,"volume":34398,"numberOfOrders":3,"value":416215.8,"orders":[{"id":"sell-2","exchange":"ASX","volume":18500,"value":223850.0,"undisclosed":false}]}]}' + headers: {} + status: + code: 200 + message: OK +version: 1 diff --git a/tests/test_product.py b/tests/test_product.py index bff3702..ae3419b 100644 --- a/tests/test_product.py +++ b/tests/test_product.py @@ -75,3 +75,27 @@ async def test_search_products( product = await tracing_client.products.product_from_instrument(search_results[0]) assert product + + +@pytest.mark.vcr() +@pytest.mark.asyncio +async def test_asx_product_depth(tracing_client: StakeClient): + tracing_client.set_exchange(constant.ASX) + + depth = await tracing_client.products.depth("ORG") + + assert depth.ticker == "ORG" + assert depth.buy_orders + assert depth.sell_orders + + +@pytest.mark.vcr() +@pytest.mark.asyncio +async def test_asx_product_course_of_sales(tracing_client: StakeClient): + tracing_client.set_exchange(constant.ASX) + + sales = await tracing_client.products.course_of_sales("ORG") + + assert sales.ticker == "ORG" + assert sales.total_trades + assert sales.course_of_sales \ No newline at end of file From fedb029a8fe056859ed61590c934dab29f3ebebb Mon Sep 17 00:00:00 2001 From: Anthony Date: Sun, 8 Mar 2026 19:35:44 +1100 Subject: [PATCH 2/2] Change model structure, improve tests, fix cassette endpoint --- stake/asx/product.py | 59 +++++++++++++++---- .../test_product/test_asx_product_depth.yaml | 2 +- tests/test_product.py | 27 ++++++++- 3 files changed, 74 insertions(+), 14 deletions(-) diff --git a/stake/asx/product.py b/stake/asx/product.py index e8456c4..218a9de 100644 --- a/stake/asx/product.py +++ b/stake/asx/product.py @@ -1,4 +1,4 @@ -from typing import Any, List, Optional +from typing import List, Optional from pydantic import BaseModel, ConfigDict @@ -41,23 +41,60 @@ class Product(BaseModel): points_change: Optional[float] = None percentage_change: Optional[float] = None out_of_market_price: Optional[float] = None + model_config = ConfigDict(alias_generator=camelcase) + + +class DepthOrder(BaseModel): + id: Optional[str] = None + exchange: Optional[str] = None + volume: Optional[int] = None + value: Optional[float] = None + undisclosed: Optional[bool] = None + model_config = ConfigDict(alias_generator=camelcase) + + +class DepthLevel(BaseModel): + id: Optional[str] = None + price: Optional[float] = None + volume: Optional[int] = None + number_of_orders: Optional[int] = None + value: Optional[float] = None + orders: Optional[List[DepthOrder]] = None + model_config = ConfigDict(alias_generator=camelcase) + - # Aggregated depth fields +class ProductAggregatedDepth(BaseModel): id: Optional[str] = None ticker: Optional[str] = None total_buy_count: Optional[int] = None total_sell_count: Optional[int] = None total_buy_volume: Optional[int] = None total_sell_volume: Optional[int] = None - buy_orders: Optional[List[dict[str, Any]]] = None - sell_orders: Optional[List[dict[str, Any]]] = None + buy_orders: Optional[List[DepthLevel]] = None + sell_orders: Optional[List[DepthLevel]] = None + model_config = ConfigDict(alias_generator=camelcase) + + +class CourseOfSale(BaseModel): + id: Optional[str] = None + instrument_code_id: Optional[str] = None + exchange_market: Optional[str] = None + price: Optional[float] = None + volume: Optional[int] = None + value: Optional[float] = None + trade_time_millis: Optional[int] = None + cancelled_time_millis: Optional[int] = None + buy_order_number: Optional[str] = None + sell_order_number: Optional[str] = None + model_config = ConfigDict(alias_generator=camelcase) - # Course of sales fields + +class ProductCourseOfSales(BaseModel): + ticker: Optional[str] = None total_volume: Optional[int] = None total_trades: Optional[int] = None total_value: Optional[float] = None - course_of_sales: Optional[List[dict[str, Any]]] = None - + course_of_sales: Optional[List[CourseOfSale]] = None model_config = ConfigDict(alias_generator=camelcase) @@ -74,17 +111,17 @@ async def get(self, symbol: str) -> Optional[Product]: return Product(**data) - async def depth(self, symbol: str) -> Product: + async def depth(self, symbol: str) -> ProductAggregatedDepth: data = await self._client.get( self._client.exchange.aggregated_depth.format(symbol=symbol) ) - return Product(**data) + return ProductAggregatedDepth(**data) - async def course_of_sales(self, symbol: str) -> Product: + async def course_of_sales(self, symbol: str) -> ProductCourseOfSales: data = await self._client.get( self._client.exchange.course_of_sales.format(symbol=symbol) ) - return Product(**data) + return ProductCourseOfSales(**data) async def search(self, request: ProductSearchByName) -> List[Instrument]: products = await self._client.get( diff --git a/tests/cassettes/test_product/test_asx_product_depth.yaml b/tests/cassettes/test_product/test_asx_product_depth.yaml index af00520..88701fd 100644 --- a/tests/cassettes/test_product/test_asx_product_depth.yaml +++ b/tests/cassettes/test_product/test_asx_product_depth.yaml @@ -23,7 +23,7 @@ interactions: Content-Type: - application/json method: GET - uri: https://api2.prd.hellostake.com/api/asx/instrument/aggregatedDepth/ORG + uri: https://api2.prd.hellostake.com/api/asx/instrument/aggregatedDepth/ORG?type=EQUITY response: body: string: '{"id":"ORG#depth","ticker":"ORG","totalBuyCount":2,"totalSellCount":2,"totalBuyVolume":17227,"totalSellVolume":34912,"buyOrders":[{"id":"buy-12.06","price":12.06,"volume":15461,"numberOfOrders":1,"value":186459.66,"orders":[{"id":"buy-1","exchange":"ASX","volume":15461,"value":186459.66,"undisclosed":false}]},{"id":"buy-12.05","price":12.05,"volume":1766,"numberOfOrders":1,"value":21280.3,"orders":[{"id":"buy-2","exchange":"ASX","volume":1766,"value":21280.3,"undisclosed":false}]}],"sellOrders":[{"id":"sell-12.08","price":12.08,"volume":514,"numberOfOrders":1,"value":6209.12,"orders":[{"id":"sell-1","exchange":"ASX","volume":514,"value":6209.12,"undisclosed":false}]},{"id":"sell-12.10","price":12.1,"volume":34398,"numberOfOrders":3,"value":416215.8,"orders":[{"id":"sell-2","exchange":"ASX","volume":18500,"value":223850.0,"undisclosed":false}]}]}' diff --git a/tests/test_product.py b/tests/test_product.py index ae3419b..50f4419 100644 --- a/tests/test_product.py +++ b/tests/test_product.py @@ -85,9 +85,21 @@ async def test_asx_product_depth(tracing_client: StakeClient): depth = await tracing_client.products.depth("ORG") assert depth.ticker == "ORG" + assert isinstance(depth.total_buy_count, int) + assert isinstance(depth.total_sell_count, int) + assert isinstance(depth.total_buy_volume, int) + assert isinstance(depth.total_sell_volume, int) assert depth.buy_orders assert depth.sell_orders + buy_order = depth.buy_orders[0] + assert isinstance(buy_order.price, float) + assert isinstance(buy_order.volume, int) + assert isinstance(buy_order.number_of_orders, int) + assert buy_order.orders + assert isinstance(buy_order.orders[0].exchange, str) + assert isinstance(buy_order.orders[0].undisclosed, bool) + @pytest.mark.vcr() @pytest.mark.asyncio @@ -97,5 +109,16 @@ async def test_asx_product_course_of_sales(tracing_client: StakeClient): sales = await tracing_client.products.course_of_sales("ORG") assert sales.ticker == "ORG" - assert sales.total_trades - assert sales.course_of_sales \ No newline at end of file + assert isinstance(sales.total_volume, int) + assert isinstance(sales.total_trades, int) + assert isinstance(sales.total_value, float) + assert sales.course_of_sales + + sale = sales.course_of_sales[0] + assert isinstance(sale.id, str) + assert isinstance(sale.instrument_code_id, str) + assert isinstance(sale.exchange_market, str) + assert isinstance(sale.price, float) + assert isinstance(sale.volume, int) + assert isinstance(sale.value, float) + assert isinstance(sale.trade_time_millis, int)