Skip to content

Add endpoints for ASX market depth and ASX course of sales#175

Open
apmarkham wants to merge 1 commit intostabacco:masterfrom
apmarkham:add-stake-black-asx-data
Open

Add endpoints for ASX market depth and ASX course of sales#175
apmarkham wants to merge 1 commit intostabacco:masterfrom
apmarkham:add-stake-black-asx-data

Conversation

@apmarkham
Copy link

@apmarkham apmarkham commented Feb 24, 2026

Hi @stabacco,

Thanks for the great package, it's been very useful. Stake Black (their paid monthly subscription) gives access to a few extra endpoints which the package doesn't currently support, see the image below for some of the data it gives access to:

image

This PR implements just the two ASX endpoints for now, so market depth and course of sales. The data looks a bit like this in the UI:

image

To take a look at the data the API returns just authenticate and call the methods:

    async with StakeClient(login_request, exchange=ASX) as client:
        depth = await client.products.depth(symbol)
        sales = await client.products.course_of_sales(symbol)

I implemented this in the Product class which seems the best approach, given the data is unique to each ticker.

Please review when you have some time and let me know if you have any feedback on style and how I've implemented this. If you're happy with these changes I will make time to implement some of the other US-exchange features (financials, analyst ratings, price targets).

Summary by Sourcery

Add support for ASX market depth and course-of-sales data on products.

New Features:

  • Expose ASX aggregated depth data for a given symbol via the products client.
  • Expose ASX course-of-sales data for a given symbol via the products client.

Enhancements:

  • Extend the ASX product model to include aggregated depth and course-of-sales fields.
  • Add ASX API URL constants for aggregated depth and course-of-sales endpoints.

Tests:

  • Add VCR-backed tests covering ASX product depth and course-of-sales endpoints.

@sourcery-ai
Copy link

sourcery-ai bot commented Feb 24, 2026

Reviewer's Guide

Adds ASX-specific market depth and course-of-sales support by extending the ASX Product model with new depth and sales fields, wiring new ASX URLs, and exposing async client methods with VCR-backed tests.

Sequence diagram for new ASX market depth and course of sales calls

sequenceDiagram
    actor User
    participant StakeClient
    participant ProductClient
    participant Exchange
    participant ASXUrl
    participant HttpClient

    User->>StakeClient: create_with_login(login_request, exchange ASX)
    activate StakeClient
    StakeClient->>ProductClient: get_products_client()
    activate ProductClient

    User->>ProductClient: depth(symbol)
    activate ProductClient
    ProductClient->>Exchange: get_exchange()
    activate Exchange
    Exchange->>ASXUrl: access_aggregated_depth_template()
    activate ASXUrl
    ASXUrl-->>Exchange: aggregated_depth_url_template
    deactivate ASXUrl
    Exchange-->>ProductClient: aggregated_depth_url_template
    deactivate Exchange
    ProductClient->>HttpClient: get(formatted_aggregated_depth_url)
    activate HttpClient
    HttpClient-->>ProductClient: depth_json
    deactivate HttpClient
    ProductClient-->>User: Product(depth_fields)
    deactivate ProductClient

    User->>ProductClient: course_of_sales(symbol)
    activate ProductClient
    ProductClient->>Exchange: get_exchange()
    activate Exchange
    Exchange->>ASXUrl: access_course_of_sales_template()
    activate ASXUrl
    ASXUrl-->>Exchange: course_of_sales_url_template
    deactivate ASXUrl
    Exchange-->>ProductClient: course_of_sales_url_template
    deactivate Exchange
    ProductClient->>HttpClient: get(formatted_course_of_sales_url)
    activate HttpClient
    HttpClient-->>ProductClient: course_of_sales_json
    deactivate HttpClient
    ProductClient-->>User: Product(course_of_sales_fields)
    deactivate ProductClient

    deactivate StakeClient
Loading

Class diagram for updated ASX Product and ASXUrl models

classDiagram
    class Product {
        +str~Optional~ id
        +str~Optional~ ticker
        +int~Optional~ total_buy_count
        +int~Optional~ total_sell_count
        +int~Optional~ total_buy_volume
        +int~Optional~ total_sell_volume
        +List~dict~buy_orders
        +List~dict~sell_orders
        +int~Optional~ total_volume
        +int~Optional~ total_trades
        +float~Optional~ total_value
        +List~dict~course_of_sales
    }

    class ASXUrl {
        +str market_status
        +str aggregated_depth
        +str course_of_sales
        +str orders
    }

    class ProductClient {
        +Product get(symbol str)
        +Product depth(symbol str)
        +Product course_of_sales(symbol str)
        +List~Instrument~ search(request ProductSearchByName)
    }

    class HttpClient {
        +Any get(url str)
    }

    class Exchange {
        +ASXUrl exchange
    }

    ProductClient --> Product : returns
    ProductClient --> HttpClient : uses
    Exchange --> ASXUrl : has
    ProductClient --> Exchange : uses
Loading

File-Level Changes

Change Details Files
Extend ASX Product model to represent aggregated market depth and course-of-sales responses.
  • Add optional identifiers, ticker, aggregated buy/sell count and volume fields to the Product model
  • Add buy_orders and sell_orders collections to capture order book levels
  • Add total_volume, total_trades, total_value and course_of_sales collections for trade history data
  • Keep existing quote-related fields and camelCase alias generation unchanged
stake/asx/product.py
Expose new async client methods for ASX market depth and course-of-sales using exchange-specific URLs.
  • Implement depth(symbol) to GET the aggregated_depth endpoint and deserialize into Product
  • Implement course_of_sales(symbol) to GET the course_of_sales endpoint and deserialize into Product
stake/asx/product.py
Define ASX URLs for aggregated depth and course-of-sales endpoints and cover them with VCR tests.
  • Add aggregated_depth and course_of_sales URL templates to ASXUrl including symbol placeholder and query parameters
  • Record VCR cassettes for sample depth and course-of-sales responses for ORG
  • Add async pytest tests that switch the client to ASX, invoke the new methods, and assert key fields are populated
stake/constant.py
tests/test_product.py
tests/cassettes/test_product/test_asx_product_course_of_sales.yaml
tests/cassettes/test_product/test_asx_product_depth.yaml

Tips and commands

Interacting with Sourcery

  • Trigger a new review: Comment @sourcery-ai review on the pull request.
  • Continue discussions: Reply directly to Sourcery's review comments.
  • Generate a GitHub issue from a review comment: Ask Sourcery to create an
    issue from a review comment by replying to it. You can also reply to a
    review comment with @sourcery-ai issue to create an issue from it.
  • Generate a pull request title: Write @sourcery-ai anywhere in the pull
    request title to generate a title at any time. You can also comment
    @sourcery-ai title on the pull request to (re-)generate the title at any time.
  • Generate a pull request summary: Write @sourcery-ai summary anywhere in
    the pull request body to generate a PR summary at any time exactly where you
    want it. You can also comment @sourcery-ai summary on the pull request to
    (re-)generate the summary at any time.
  • Generate reviewer's guide: Comment @sourcery-ai guide on the pull
    request to (re-)generate the reviewer's guide at any time.
  • Resolve all Sourcery comments: Comment @sourcery-ai resolve on the
    pull request to resolve all Sourcery comments. Useful if you've already
    addressed all the comments and don't want to see them anymore.
  • Dismiss all Sourcery reviews: Comment @sourcery-ai dismiss on the pull
    request to dismiss all existing Sourcery reviews. Especially useful if you
    want to start fresh with a new review - don't forget to comment
    @sourcery-ai review to trigger a new review!

Customizing Your Experience

Access your dashboard to:

  • Enable or disable review features such as the Sourcery-generated pull request
    summary, the reviewer's guide, and others.
  • Change the review language.
  • Add, remove or edit custom review instructions.
  • Adjust other review settings.

Getting Help

Copy link

@sourcery-ai sourcery-ai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hey - I've found 2 issues, and left some high level feedback:

  • The Product model is starting to accumulate unrelated concerns (quote data, depth, and course of sales); consider introducing separate Pydantic models (e.g., Depth, CourseOfSales) instead of adding all fields to Product to keep the domain model cohesive.
  • The fields buy_orders, sell_orders, and course_of_sales are typed as List[dict[str, Any]]; defining dedicated typed models for these structures would improve type safety and make the API easier to understand and use.
  • Both depth and course_of_sales currently return Product, even though the underlying payloads are structurally different; returning more specific response types would make the methods’ contracts clearer and avoid partial/unused fields on Product.
Prompt for AI Agents
Please address the comments from this code review:

## Overall Comments
- The `Product` model is starting to accumulate unrelated concerns (quote data, depth, and course of sales); consider introducing separate Pydantic models (e.g., `Depth`, `CourseOfSales`) instead of adding all fields to `Product` to keep the domain model cohesive.
- The fields `buy_orders`, `sell_orders`, and `course_of_sales` are typed as `List[dict[str, Any]]`; defining dedicated typed models for these structures would improve type safety and make the API easier to understand and use.
- Both `depth` and `course_of_sales` currently return `Product`, even though the underlying payloads are structurally different; returning more specific response types would make the methods’ contracts clearer and avoid partial/unused fields on `Product`.

## Individual Comments

### Comment 1
<location path="tests/test_product.py" line_range="82-89" />
<code_context>
+
+@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
+
+
</code_context>
<issue_to_address>
**suggestion (testing):** Consider asserting more of the depth response shape and numeric fields, not just presence of orders.

This test only checks that `ticker` is set and that `buy_orders`/`sell_orders` are truthy, but not the additional depth fields you introduced (`total_buy_count`, `total_sell_count`, `total_buy_volume`, `total_sell_volume`, etc.) or the shape of individual order entries. Please extend the assertions to cover some of these numeric fields (e.g., non-`None`, correct type) and verify the structure of an order item (expected keys like price/volume) so the test better guards against regressions in the response mapping.
</issue_to_address>

### Comment 2
<location path="tests/test_product.py" line_range="94-101" />
<code_context>
+
+@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
</code_context>
<issue_to_address>
**suggestion (testing):** Expand the course-of-sales test to validate key numeric fields and the shape of individual sales entries.

Right now this only asserts `ticker`, `total_trades`, and that `course_of_sales` is truthy. Since the `Product` model now exposes `total_volume`, `total_trades`, and `total_value`, please assert these are populated and of the expected type for this cassette. Also add an assertion that at least one `course_of_sales` entry has the expected keys/structure to catch future API or deserialization changes.
</issue_to_address>

Sourcery is free for open source - if you like our reviews please consider sharing them ✨
Help me be more useful! Please click 👍 or 👎 on each comment and I'll use the feedback to improve your reviews.

Comment on lines +82 to +89
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Consider asserting more of the depth response shape and numeric fields, not just presence of orders.

This test only checks that ticker is set and that buy_orders/sell_orders are truthy, but not the additional depth fields you introduced (total_buy_count, total_sell_count, total_buy_volume, total_sell_volume, etc.) or the shape of individual order entries. Please extend the assertions to cover some of these numeric fields (e.g., non-None, correct type) and verify the structure of an order item (expected keys like price/volume) so the test better guards against regressions in the response mapping.

Comment on lines +94 to +101
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
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion (testing): Expand the course-of-sales test to validate key numeric fields and the shape of individual sales entries.

Right now this only asserts ticker, total_trades, and that course_of_sales is truthy. Since the Product model now exposes total_volume, total_trades, and total_value, please assert these are populated and of the expected type for this cassette. Also add an assertion that at least one course_of_sales entry has the expected keys/structure to catch future API or deserialization changes.

@stabacco
Copy link
Owner

@apmarkham are you able to address the comments that sourcery has added?

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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

please let's avoid using Any. we need to define an exact object.

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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's make this a separate object, maybe ProductAggregatedDepth ?

Comment on lines +55 to +59
# 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
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

let's make this a separated object , ProductCourseOfSales .

course_of_sales should be Optional[List[CourseOfSale]]

@stabacco
Copy link
Owner

Thanks @apmarkham for the contribution, it will be very helpful

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants