diff --git a/stake/asx/product.py b/stake/asx/product.py index 80806ea..218a9de 100644 --- a/stake/asx/product.py +++ b/stake/asx/product.py @@ -44,6 +44,60 @@ class Product(BaseModel): 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) + + +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[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) + + +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[CourseOfSale]] = None + model_config = ConfigDict(alias_generator=camelcase) + + class ProductsClient(BaseClient): async def get(self, symbol: str) -> Optional[Product]: """Given a symbol it will return the matching product. @@ -57,6 +111,18 @@ async def get(self, symbol: str) -> Optional[Product]: return Product(**data) + async def depth(self, symbol: str) -> ProductAggregatedDepth: + data = await self._client.get( + self._client.exchange.aggregated_depth.format(symbol=symbol) + ) + return ProductAggregatedDepth(**data) + + async def course_of_sales(self, symbol: str) -> ProductCourseOfSales: + data = await self._client.get( + self._client.exchange.course_of_sales.format(symbol=symbol) + ) + return ProductCourseOfSales(**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..88701fd --- /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?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}]}]}' + headers: {} + status: + code: 200 + message: OK +version: 1 diff --git a/tests/test_product.py b/tests/test_product.py index bff3702..50f4419 100644 --- a/tests/test_product.py +++ b/tests/test_product.py @@ -75,3 +75,50 @@ 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 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 +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 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)