Skip to content

feat: Implement HDFC Securities adapter#35

Merged
coderkrp merged 17 commits into
masterfrom
feat/issue-10-hdfc-securities-adapter
Oct 5, 2025
Merged

feat: Implement HDFC Securities adapter#35
coderkrp merged 17 commits into
masterfrom
feat/issue-10-hdfc-securities-adapter

Conversation

@coderkrp

@coderkrp coderkrp commented Oct 4, 2025

Copy link
Copy Markdown
Owner

Summary

  • What: Implemented the HDFC Securities adapter, including authentication, portfolio fetching, and order placement.
  • Why: To enable the service to connect to HDFC Securities as a broker.

Related Issue

Closes: #10

Type of change

  • Feature

How to test / QA steps

  1. Checkout this branch: git checkout feat/issue-10-hdfc-securities-adapter
  2. Install / run steps:
    • uv sync
    • uv run pytest
  3. Steps to reproduce / verify the change:
    • The tests in tests/adapters/test_hdfc.py cover the functionality of the adapter.

Checklist — Definition of Done

  • Code builds locally and on CI
  • All tests pass (unit/integration as applicable)
  • Linting passed
  • New logic covered by tests or rationale provided
  • README / docs updated if behavior changed
  • Issue linked and will be closed on merge (see "Closes #")
  • Branch named according to convention (see README)
  • No sensitive data or secrets included

Summary by CodeRabbit

  • New Features
    • Added full HDFC Securities broker support: login (including 2FA), session management, portfolio/holdings, place/modify/cancel orders, order/trade books, positions, and profile; broker interface extended and other adapters updated with corresponding placeholders and typed order/position/profile models.
  • Documentation
    • Story moved to Done; QA gate added and marked PASS with non‑functional validations.
  • Tests
    • Extensive HDFC adapter tests added; adapter/mock tests updated to use typed portfolio models and attribute access.
  • Chores
    • Added HDFC config options, pinned dependency list updated and email-validator added; minor gitignore comment adjusted.

@coderabbitai

coderabbitai Bot commented Oct 4, 2025

Copy link
Copy Markdown
Contributor

Walkthrough

Adds a new HDFC broker adapter with multi-step authentication (2FA/consent), session/access-token management, order/portfolio/profile/positions endpoints, new order/trade/position/profile models, extends broker interface, updates config to enable HDFC, adapts Mock/Fyers to the new interface, adds tests, docs, and pinned dependencies. (33 words)

Changes

Cohort / File(s) Summary
Git metadata
\.gitignore
Comment header changed from "BMad and Gemini specific directories" to "Gemini specific directories"; .gemini/ entry unchanged.
QA / Story docs
docs/qa/gates/2.1-hdfc-securities-adapter.yml, docs/stories/story-2.1.md
Adds QA gate YAML for HDFC adapter (PASS) and marks story-2.1 Done with changelog, QA notes, and gating state.
HDFC adapter & models
src/ordo/adapters/hdfc.py
New HDFCAdapter implementing IBrokerAdapter plus many Pydantic request/response models; implements initiate/complete login (2FA and consent flows), session/access-token handling, place/modify/cancel orders, fetch portfolio/holdings/positions/profile/order & trade books, and maps API payloads to internal DTOs.
Config wiring
src/ordo/config.py
Adds HDFC credential fields to Settings and lazy-loads HDFCAdapter in get_adapter("hdfc").
Broker interface
src/ordo/adapters/base.py
Changes get_portfolio return type to Portfolio and adds abstract methods: modify_order, cancel_order, get_order_book, get_trade_book, get_profile, get_holdings, get_positions.
Adapters updated with stubs & typed portfolio
src/ordo/adapters/fyers.py, src/ordo/adapters/mock.py
Adds seven async stub methods (modify_order, cancel_order, get_order_book, get_trade_book, get_profile, get_holdings, get_positions). MockAdapter.get_portfolio now returns a typed Portfolio constructed from Holdings/Funds models.
New API models
src/ordo/models/api/order.py, src/ordo/models/api/user.py
Adds enums and Pydantic models for orders/trades/positions and OrderResponse; adds Profile model for user profile data.
Tests — HDFC
tests/adapters/test_hdfc.py
New extensive unit tests for HDFCAdapter covering login (2FA/non-2FA), complete_login, portfolio/holdings, order flows, error cases, and session utilities using pytest/respx/httpx mocks.
Tests — Mock / Fyers updates
tests/adapters/test_mock.py, tests/adapters/test_fyers.py
Tests updated to expect typed Portfolio/Holding/Funds objects and to use attribute access instead of dict indexing.
Dependencies / packaging
pyproject.toml, requirements.txt
Adds email-validator dependency and updates/pins a comprehensive set of requirements in requirements.txt.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  actor User
  participant App as App/Ordo
  participant HDFC as HDFCAdapter
  participant API as HDFC HTTP API

  rect rgba(200,235,255,0.12)
  Note over User,API: Initiate login
  User->>App: initiate_login(credentials)
  App->>HDFC: initiate_login(credentials)
  HDFC->>API: POST /login/init
  API-->>HDFC: { tokenId, twoFAEnabled, loginId }
  HDFC-->>App: session_data
  App-->>User: session_data
  end

  rect rgba(220,255,220,0.12)
  Note over User,API: Complete login (validate → authorize → access_token)
  User->>App: complete_login(session_data, otp?, consent)
  App->>HDFC: complete_login(...)
  alt twoFA enabled
    HDFC->>API: POST /login/2fa-validate (otp)
    API-->>HDFC: { request_token }
  else no twoFA
    HDFC->>API: POST /login/validate
    API-->>HDFC: { request_token }
  end
  HDFC->>API: POST /authorize (consent)
  API-->>HDFC: { auth_code }
  HDFC->>API: POST /access-token (auth_code)
  API-->>HDFC: { access_token }
  HDFC-->>App: session_data + access_token
  App-->>User: login complete
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant App as App/Ordo
  participant HDFC as HDFCAdapter
  participant API as HDFC HTTP API

  rect rgba(255,245,200,0.12)
  Note over User,API: Place order
  User->>App: place_order(session_data, order_details)
  App->>HDFC: place_order(...)
  HDFC->>HDFC: Validate order (Pydantic)
  HDFC->>API: POST /orders (Bearer access_token)
  API-->>HDFC: { orderId, status }
  HDFC-->>App: OrderResponse
  App-->>User: order result
  end
Loading
sequenceDiagram
  autonumber
  actor User
  participant App as App/Ordo
  participant HDFC as HDFCAdapter
  participant API as HDFC HTTP API

  rect rgba(235,225,255,0.12)
  Note over User,API: Fetch portfolio
  User->>App: get_portfolio(session_data)
  App->>HDFC: get_portfolio(...)
  HDFC->>API: GET /holdings (Bearer access_token)
  API-->>HDFC: holdings payload
  HDFC->>HDFC: Map payload → Portfolio DTO
  HDFC-->>App: Portfolio
  App-->>User: Portfolio DTO
  end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Possibly related PRs

Poem

I nibble bytes beneath the moonlight,
Tokens hop and flows take flight.
Models stitched with careful paws,
Tests green now — I drum my claws. 🥕

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Out of Scope Changes Check ⚠️ Warning Beyond the HDFCAdapter feature, the PR also adds stub methods to FyersAdapter and MockAdapter, extends the base adapter interface, introduces broad new API order and profile models, overhauls requirements.txt, and updates story and QA docs that are not directly required by issue #10. Please separate unrelated infrastructure, dependency, and documentation changes into their own pull requests and limit this PR to the HDFCAdapter implementation, its tests, and only the supporting models and config necessary for that feature.
Docstring Coverage ⚠️ Warning Docstring coverage is 47.37% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (3 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title Check ✅ Passed The title “feat: Implement HDFC Securities adapter” succinctly captures the primary change in this pull request by clearly stating that a new HDFC Securities adapter feature is being added, and it remains concise and specific.
Linked Issues Check ✅ Passed All acceptance criteria from issue #10 are satisfied: the HDFCAdapter class implements the full IBrokerAdapter interface with authentication, portfolio, and order methods; it handles HDFC-specific API workflows and maps data to the Portfolio DTO; it supports order placement and error handling; and the accompanying test suite covers successful and failed scenarios for each function.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/issue-10-hdfc-securities-adapter

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 4

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between e4e709a and 914bfe7.

📒 Files selected for processing (7)
  • .gitignore (1 hunks)
  • docs/qa/gates/2.1-hdfc-securities-adapter.yml (1 hunks)
  • docs/stories/story-2.1.md (3 hunks)
  • src/ordo/adapters/hdfc.py (1 hunks)
  • src/ordo/config.py (2 hunks)
  • tests/adapters/test_fyers.py (1 hunks)
  • tests/adapters/test_hdfc.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/ordo/config.py (1)
src/ordo/adapters/hdfc.py (1)
  • HDFCAdapter (84-365)
tests/adapters/test_hdfc.py (4)
src/ordo/adapters/base.py (1)
  • IBrokerAdapter (5-29)
src/ordo/adapters/hdfc.py (5)
  • HDFCAdapter (84-365)
  • initiate_login (112-146)
  • complete_login (184-225)
  • get_portfolio (281-365)
  • place_order (227-279)
src/ordo/models/api/errors.py (1)
  • ApiException (22-25)
src/ordo/security/session.py (2)
  • set_session (19-25)
  • get_session (27-36)
src/ordo/adapters/hdfc.py (4)
src/ordo/adapters/base.py (1)
  • IBrokerAdapter (5-29)
src/ordo/models/api/errors.py (2)
  • ApiError (7-19)
  • ApiException (22-25)
src/ordo/models/api/portfolio.py (3)
  • Portfolio (29-42)
  • Holding (5-16)
  • Funds (19-26)
src/ordo/security/session.py (3)
  • SessionManager (6-36)
  • set_session (19-25)
  • get_session (27-36)
🪛 GitHub Actions: Python CI
src/ordo/adapters/hdfc.py

[error] 389-389: IndentationError in hdfc.py: unindent does not match any outer indentation level while defining 'cancel_order'. This caused pytest collection to fail.

🪛 markdownlint-cli2 (0.18.1)
docs/stories/story-2.1.md

33-33: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


36-36: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


42-42: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


45-45: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


49-49: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


112-112: Multiple headings with the same content

(MD024, no-duplicate-heading)


114-114: Multiple headings with the same content

(MD024, no-duplicate-heading)


118-118: Multiple headings with the same content

(MD024, no-duplicate-heading)


125-125: Multiple headings with the same content

(MD024, no-duplicate-heading)


131-131: Multiple headings with the same content

(MD024, no-duplicate-heading)


135-135: Multiple headings with the same content

(MD024, no-duplicate-heading)


139-139: Multiple headings with the same content

(MD024, no-duplicate-heading)


143-143: Multiple headings with the same content

(MD024, no-duplicate-heading)


147-147: Multiple headings with the same content

(MD024, no-duplicate-heading)

🪛 YAMLlint (1.37.1)
docs/qa/gates/2.1-hdfc-securities-adapter.yml

[error] 10-10: too many spaces inside braces

(braces)


[error] 10-10: too many spaces inside braces

(braces)

🔇 Additional comments (13)
.gitignore (1)

30-30: LGTM!

The comment update accurately reflects the current state of the ignored directories.

src/ordo/config.py (2)

20-23: LGTM!

The HDFC configuration fields follow the established pattern for broker credentials and integrate cleanly with the existing settings structure.


39-44: LGTM!

The lazy import pattern for the HDFC adapter matches the existing approach for the Fyers adapter, maintaining consistency and avoiding circular dependencies.

tests/adapters/test_hdfc.py (1)

1-457: LGTM!

The test suite comprehensively covers all acceptance criteria:

  • Interface conformance (line 32-36)
  • Login flows with and without 2FA (lines 42-258)
  • Portfolio retrieval success and error cases (lines 263-346)
  • Order placement with validation (lines 352-457)

Test structure follows established patterns and uses appropriate mocking with respx and pytest fixtures.

src/ordo/adapters/hdfc.py (9)

96-101: LGTM!

The _get_login_token helper method is well-structured with proper error handling via raise_for_status() and Pydantic validation of the response.


103-110: LGTM!

The _validate_user helper method properly validates credentials and uses Pydantic for response parsing.


112-146: LGTM!

The initiate_login implementation correctly:

  • Validates credentials with Pydantic
  • Delegates to helper methods for clarity
  • Returns session_data for state management
  • Handles both HTTPStatusError and generic exceptions appropriately

148-158: LGTM!

The _validate_2fa helper correctly validates OTP and checks for the requestToken in the response.


160-170: LGTM!

The _authorize_session helper properly handles consent and validates the authorization response.


172-182: LGTM!

The _get_access_token helper correctly retrieves and validates the final access token.


184-225: LGTM!

The complete_login implementation correctly:

  • Validates required OTP for 2FA scenarios
  • Orchestrates the multi-step flow via helper methods
  • Stores the access token in session management
  • Provides comprehensive error handling

227-279: LGTM!

The place_order implementation correctly:

  • Retrieves and validates the access token
  • Validates order details with Pydantic
  • Uses proper authorization headers
  • Handles errors comprehensively

281-365: LGTM!

The get_portfolio implementation correctly:

  • Validates access token and login_id
  • Retrieves holdings and summary in parallel-capable structure
  • Transforms HDFC data to standard Portfolio DTO
  • Documents day_pnl limitation appropriately (lines 340-342, 359-361)

Comment thread docs/qa/gates/2.1-hdfc-securities-adapter.yml Outdated
Comment thread docs/stories/story-2.1.md
Comment on lines +33 to +149
## QA Notes
- Risk profile: docs/qa/assessments/2.1-risk-20251003.md

## File List
- src/ordo/adapters/hdfc.py
- tests/adapters/test_hdfc.py

## Dev Agent Record

### Agent Model Used
Gemini

### Debug Log References
- `uv run ruff check . --fix`: Fixed unused imports.
- `uv run pytest`: All tests passed.

### Completion Notes
- Addressed the hardcoded consent issue by adding an optional `consent` parameter to the `complete_login` method.
- Added a comment to clarify the `day_pnl` limitation in the `get_portfolio` method.

### Change Log

- **2025-10-04**: Applied fixes based on QA feedback.
- Modified `src/ordo/adapters/hdfc.py` to handle user consent dynamically.
- Updated comments in `src/ordo/adapters/hdfc.py` regarding `day_pnl`.
- Removed unused imports from `src/ordo/adapters/hdfc.py` and `tests/adapters/test_hdfc.py`.

## QA Results

### Review Date: 2025-10-04

### Reviewed By: Quinn (Test Architect)

### Code Quality Assessment

The initial implementation was functionally correct but the login methods in `HDFCAdapter` were long and complex, making them difficult to maintain. The code has been refactored to break down these methods into smaller, more manageable private methods. This improves readability and aligns with best practices.

### Refactoring Performed

- **File**: `src/ordo/adapters/hdfc.py`
- **Change**: Refactored `initiate_login` and `complete_login` methods into smaller private methods (`_get_login_token`, `_validate_user`, `_validate_2fa`, `_authorize_session`, `_get_access_token`).
- **Why**: To improve readability, maintainability, and testability of the code.
- **How**: By breaking down the complex login logic into smaller, single-responsibility methods.

### Compliance Check

- Coding Standards: ✓
- Project Structure: ✓
- Testing Strategy: ✓
- All ACs Met: ✓

### Improvements Checklist

- [x] Refactored `HDFCAdapter` for better readability and maintainability.
- [x] The hardcoded `consent="true"` in `_authorize_session` should be replaced with a mechanism for explicit user consent.
- [x] The hardcoded `day_pnl=0.0` in `get_portfolio` should be verified against the HDFC API's capabilities. If the API provides this data, it should be used.

### Security Review

- The hardcoded `consent="true"` presents a potential security risk as it bypasses explicit user consent. This has been flagged as a concern.

### Performance Considerations

- No performance issues were identified.

### Files Modified During Review

- `src/ordo/adapters/hdfc.py`

### Gate Status

Gate: CONCERNS → qa/gates/2.1-hdfc-securities-adapter.yml

### Recommended Status

✗ Changes Required - See unchecked items above

### Review Date: 2025-10-04 (Follow-up)

### Reviewed By: Quinn (Test Architect)

### Code Quality Assessment

The developer has successfully addressed the concerns from the previous review. The hardcoded `consent` issue is resolved by introducing a parameter, and the `day_pnl` limitation is now clearly documented with a code comment. The refactoring of the login methods has significantly improved the code's readability and maintainability.

### Compliance Check

- Coding Standards: ✓
- Project Structure: ✓
- Testing Strategy: ✓
- All ACs Met: ✓

### Improvements Checklist

- [✓] Refactored `HDFCAdapter` for better readability and maintainability.
- [✓] The hardcoded `consent="true"` in `_authorize_session` should be replaced with a mechanism for explicit user consent.
- [✓] The hardcoded `day_pnl=0.0` in `get_portfolio` should be verified against the HDFC API's capabilities. If the API provides this data, it should be used.

### Security Review

- The consent issue has been addressed. No further security concerns were identified.

### Performance Considerations

- No performance issues were identified.

### Files Modified During Review

- None

### Gate Status

Gate: PASS → qa/gates/2.1-hdfc-securities-adapter.yml

### Recommended Status

✓ Ready for Done

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Consider fixing markdown formatting issues.

Markdownlint flags missing blank lines around several headings. While this doesn't affect functionality, fixing these would improve document consistency.

Add blank lines before headings at lines 33, 36, 42, 45, and 49 to comply with MD022 (blanks-around-headings).

🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

33-33: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


36-36: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


42-42: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


45-45: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


49-49: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)


112-112: Multiple headings with the same content

(MD024, no-duplicate-heading)


114-114: Multiple headings with the same content

(MD024, no-duplicate-heading)


118-118: Multiple headings with the same content

(MD024, no-duplicate-heading)


125-125: Multiple headings with the same content

(MD024, no-duplicate-heading)


131-131: Multiple headings with the same content

(MD024, no-duplicate-heading)


135-135: Multiple headings with the same content

(MD024, no-duplicate-heading)


139-139: Multiple headings with the same content

(MD024, no-duplicate-heading)


143-143: Multiple headings with the same content

(MD024, no-duplicate-heading)


147-147: Multiple headings with the same content

(MD024, no-duplicate-heading)

🤖 Prompt for AI Agents
docs/stories/story-2.1.md lines 33-149: Several headings lack the required blank
line above them (MD022) at lines 33, 36, 42, 45, and 49; insert a single blank
line immediately before each of those heading lines so every heading is
separated from the preceding content by an empty line, then re-run markdownlint
to confirm the MD022 warnings are resolved.

Comment thread src/ordo/adapters/hdfc.py Outdated
Comment thread src/ordo/adapters/hdfc.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
src/ordo/adapters/hdfc.py (1)

185-192: Resource leak: httpx.AsyncClient not properly closed.

The http_client is created in __init__ but never closed, leading to a resource leak. This is a duplicate of a previous review comment but remains unresolved.

Apply this diff to create the client per-request:

     def __init__(self):
         self.base_url = "https://developer.hdfcsec.com/oapi/v1"
         self.session_manager = SessionManager(settings.SECRET_KEY)
-        self.http_client = httpx.AsyncClient(
-            headers={
-                "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
-            }
-        )
+        self._headers = {
+            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
+        }
+
+    def _create_client(self) -> httpx.AsyncClient:
+        return httpx.AsyncClient(headers=self._headers)

Then update methods like modify_order, cancel_order, get_order_book, get_trade_book, get_profile, get_holdings, and get_positions to use:

async with self._create_client() as client:
    # ... existing code

Note: Methods _get_login_token and _validate_user also use self.http_client and should be updated.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 914bfe7 and 2cc3ec0.

📒 Files selected for processing (7)
  • src/ordo/adapters/base.py (2 hunks)
  • src/ordo/adapters/fyers.py (1 hunks)
  • src/ordo/adapters/hdfc.py (1 hunks)
  • src/ordo/adapters/mock.py (1 hunks)
  • src/ordo/models/api/order.py (1 hunks)
  • src/ordo/models/api/user.py (1 hunks)
  • tests/adapters/test_hdfc.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (5)
src/ordo/adapters/base.py (5)
src/ordo/models/api/order.py (4)
  • Order (24-33)
  • Trade (36-43)
  • Position (46-52)
  • OrderResponse (55-57)
src/ordo/models/api/user.py (1)
  • Profile (4-7)
src/ordo/models/api/portfolio.py (2)
  • Holding (5-16)
  • Portfolio (29-42)
src/ordo/adapters/mock.py (8)
  • get_portfolio (19-68)
  • modify_order (70-71)
  • cancel_order (73-74)
  • get_order_book (76-77)
  • get_trade_book (79-80)
  • get_profile (82-83)
  • get_holdings (85-86)
  • get_positions (88-89)
src/ordo/adapters/hdfc.py (8)
  • get_portfolio (429-522)
  • modify_order (524-570)
  • cancel_order (572-607)
  • get_order_book (609-657)
  • get_trade_book (659-705)
  • get_profile (707-740)
  • get_holdings (742-797)
  • get_positions (799-844)
src/ordo/adapters/mock.py (1)
src/ordo/adapters/base.py (7)
  • modify_order (36-42)
  • cancel_order (45-51)
  • get_order_book (54-58)
  • get_trade_book (61-65)
  • get_profile (68-72)
  • get_holdings (75-79)
  • get_positions (82-86)
src/ordo/adapters/fyers.py (1)
src/ordo/adapters/mock.py (7)
  • modify_order (70-71)
  • cancel_order (73-74)
  • get_order_book (76-77)
  • get_trade_book (79-80)
  • get_profile (82-83)
  • get_holdings (85-86)
  • get_positions (88-89)
tests/adapters/test_hdfc.py (4)
src/ordo/adapters/base.py (11)
  • IBrokerAdapter (9-86)
  • initiate_login (15-19)
  • complete_login (22-26)
  • get_portfolio (29-33)
  • modify_order (36-42)
  • cancel_order (45-51)
  • get_order_book (54-58)
  • get_trade_book (61-65)
  • get_profile (68-72)
  • get_holdings (75-79)
  • get_positions (82-86)
src/ordo/adapters/hdfc.py (12)
  • HDFCAdapter (180-844)
  • initiate_login (214-255)
  • complete_login (310-364)
  • get_portfolio (429-522)
  • place_order (366-427)
  • modify_order (524-570)
  • cancel_order (572-607)
  • get_order_book (609-657)
  • get_trade_book (659-705)
  • get_profile (707-740)
  • get_holdings (742-797)
  • get_positions (799-844)
src/ordo/models/api/errors.py (1)
  • ApiException (22-25)
src/ordo/security/session.py (2)
  • set_session (19-25)
  • get_session (27-36)
src/ordo/adapters/hdfc.py (6)
src/ordo/adapters/base.py (11)
  • IBrokerAdapter (9-86)
  • initiate_login (15-19)
  • complete_login (22-26)
  • get_portfolio (29-33)
  • modify_order (36-42)
  • cancel_order (45-51)
  • get_order_book (54-58)
  • get_trade_book (61-65)
  • get_profile (68-72)
  • get_holdings (75-79)
  • get_positions (82-86)
src/ordo/models/api/errors.py (2)
  • ApiError (7-19)
  • ApiException (22-25)
src/ordo/models/api/portfolio.py (3)
  • Portfolio (29-42)
  • Holding (5-16)
  • Funds (19-26)
src/ordo/models/api/order.py (7)
  • Order (24-33)
  • Trade (36-43)
  • Position (46-52)
  • OrderResponse (55-57)
  • TransactionType (5-7)
  • OrderType (10-14)
  • ProductType (17-21)
src/ordo/models/api/user.py (1)
  • Profile (4-7)
src/ordo/security/session.py (3)
  • SessionManager (6-36)
  • set_session (19-25)
  • get_session (27-36)
🪛 GitHub Actions: Python CI
src/ordo/adapters/base.py

[error] 1-1: Black formatting check failed. 3 files would be reformatted by Black: /home/runner/work/Ordo/Ordo/src/ordo/adapters/mock.py, /home/runner/work/Ordo/Ordo/src/ordo/adapters/fyers.py, /home/runner/work/Ordo/Ordo/src/ordo/adapters/hdfc.py.

src/ordo/adapters/mock.py

[error] 1-1: Black formatting check failed. 3 files would be reformatted (including mock.py). Run 'black .' to fix formatting.

src/ordo/adapters/fyers.py

[error] 1-1: Black formatting check failed. 3 files would be reformatted (including fyers.py). Run 'black .' to fix formatting.

src/ordo/models/api/order.py

[error] 1-1: Black formatting check failed. 3 files would be reformatted by Black: /home/runner/work/Ordo/Ordo/src/ordo/adapters/mock.py, /home/runner/work/Ordo/Ordo/src/ordo/adapters/fyers.py, /home/runner/work/Ordo/Ordo/src/ordo/adapters/hdfc.py.

src/ordo/models/api/user.py

[error] 1-1: Black formatting check failed. 3 files would be reformatted by Black: /home/runner/work/Ordo/Ordo/src/ordo/adapters/mock.py, /home/runner/work/Ordo/Ordo/src/ordo/adapters/fyers.py, /home/runner/work/Ordo/Ordo/src/ordo/adapters/hdfc.py.

src/ordo/adapters/hdfc.py

[error] 1-1: Black formatting check failed. 3 files would be reformatted (including hdfc.py). Run 'black .' to fix formatting.

🔇 Additional comments (28)
src/ordo/adapters/base.py (2)

2-6: LGTM! Proper domain model imports.

The new imports introduce typed domain models (Portfolio, Holding, Order, Trade, Position, OrderResponse, Profile) which improve type safety across the adapter interface.


35-86: LGTM! Well-designed interface expansion.

The new abstract methods provide comprehensive broker functionality covering order management, user profile, holdings, and positions. Each method:

  • Has clear documentation
  • Uses appropriate domain models for return types
  • Follows consistent parameter patterns (session_data)
  • Properly raises NotImplementedError
src/ordo/adapters/fyers.py (1)

247-267: LGTM! Proper placeholder stubs for future implementation.

The new methods correctly implement the IBrokerAdapter interface with NotImplementedError stubs, maintaining API compatibility while deferring implementation. This is the appropriate pattern for incremental adapter development.

Note: The Black formatting check failed. Ensure you run black . before pushing.

src/ordo/models/api/order.py (2)

5-21: LGTM! Well-defined enums for order domain.

The enums (TransactionType, OrderType, ProductType) use the str, Enum pattern which provides both string serialization and type safety. The values are lowercase, which is consistent with common broker API conventions.


24-57: LGTM! Comprehensive order domain models.

The models (Order, Trade, Position, OrderResponse) are well-structured with appropriate fields:

  • Order includes all essential attributes (id, symbol, status, type info, quantity, price, timestamp)
  • Trade links to orders and captures execution details
  • Position tracks holdings with P&L
  • OrderResponse provides a simple response structure

All models use proper typing.

src/ordo/adapters/mock.py (1)

70-90: LGTM! Consistent stub methods for mock adapter.

The new methods properly implement the IBrokerAdapter interface with NotImplementedError stubs, maintaining consistency with the FyersAdapter pattern. This allows the mock adapter to remain compliant with the interface while keeping focus on the implemented methods.

Note: The Black formatting check failed. Run black . to fix formatting.

tests/adapters/test_hdfc.py (7)

11-28: LGTM! Well-designed test fixtures.

The fixtures provide:

  • mock_session_manager: Patches SessionManager for isolated testing
  • hdfc_credentials: Reusable test credentials

Both follow good testing practices with proper mocking and fixture scope.


31-36: LGTM! Interface compliance test.

This test verifies that HDFCAdapter correctly implements IBrokerAdapter, ensuring type compatibility across the adapter layer.


39-133: LGTM! Comprehensive login initiation tests.

The test suite covers:

  1. Success case without 2FA (lines 42-75)
  2. Success case with 2FA (lines 80-113)
  3. API error handling (lines 118-133)

All tests properly mock HTTP responses and verify expected behavior.


136-283: LGTM! Thorough complete_login tests.

The test suite covers:

  1. Success without 2FA (lines 139-175)
  2. Success with 2FA using OTP (lines 180-231)
  3. API error during 2FA validation (lines 236-259)
  4. Missing OTP when 2FA enabled (lines 263-283)

All critical paths are tested with proper assertions on session management and error handling.


285-377: LGTM! Complete portfolio retrieval tests.

The tests cover:

  1. Success path with holdings and portfolio summary (lines 288-348)
  2. API error handling (lines 353-377)

The success test properly verifies all portfolio fields including holdings details, funds, and P&L calculations.


380-489: LGTM! Comprehensive order placement tests.

The tests cover:

  1. Success case (lines 383-417)
  2. API error handling (lines 422-457)
  3. Invalid order details/Pydantic validation (lines 461-489)

All critical paths are tested with proper error type assertions.


491-752: LGTM! Complete test coverage for all broker operations.

The test suite covers:

  1. modify_order success (lines 494-519)
  2. cancel_order success (lines 524-545)
  3. get_order_book success (lines 550-589)
  4. get_trade_book success (lines 594-632)
  5. get_profile success (lines 637-665)
  6. get_holdings success (lines 670-711)
  7. get_positions success (lines 716-752)

All tests properly mock HTTP responses, verify data transformations, and assert expected behavior. Excellent coverage of the new adapter methods.

src/ordo/adapters/hdfc.py (14)

23-177: LGTM! Comprehensive Pydantic models for HDFC API.

The models cover all necessary API request/response structures:

  • Order operations (modify, action, place)
  • Login flow (init, validate, 2FA, authorize, access token)
  • Data retrieval (holdings, portfolio, order book, trade book, profile, positions)

All models use appropriate field types and follow Pydantic best practices.


194-212: LGTM! Clean login token retrieval.

The _get_login_token helper properly:

  • Makes async HTTP call
  • Raises on HTTP errors
  • Validates response with Pydantic
  • Returns the tokenId

Note: This method uses self.http_client which has the resource leak issue mentioned in a separate comment.


214-255: LGTM! Robust initiate_login implementation.

The method properly:

  1. Validates credentials with Pydantic (lines 218-221)
  2. Orchestrates login token and user validation (lines 224-225)
  3. Returns comprehensive session data including 2FA status (lines 227-237)
  4. Handles HTTP errors with ApiException (lines 238-248)
  5. Catches general exceptions (lines 249-255)

Error handling provides detailed context with status codes and response data.


257-292: LGTM! Well-structured multi-step login helpers.

The helper methods _validate_2fa (lines 257-271) and _authorize_session (lines 273-292) properly:

  • Accept httpx.AsyncClient to use context manager
  • Validate responses with Pydantic models
  • Check for required tokens and raise clear errors
  • Return the request tokens for chaining

Good separation of concerns with each step isolated.


310-364: LGTM! Comprehensive complete_login implementation.

The method properly:

  1. Validates 2FA requirements upfront (lines 320-326)
  2. Creates a context-managed httpx.AsyncClient (line 328)
  3. Conditionally validates 2FA OTP (lines 330-333)
  4. Authorizes session (lines 335-337)
  5. Retrieves access token (lines 338-340)
  6. Stores token in session manager (lines 342-344)
  7. Handles HTTP errors with detailed context (lines 347-357)
  8. Catches general exceptions (lines 358-364)

Excellent error handling and proper async client lifecycle management.


366-427: LGTM! Well-implemented place_order method.

The method properly:

  1. Validates credentials and retrieves access token (lines 372-376)
  2. Validates order details with Pydantic (lines 378-381)
  3. Constructs proper headers (lines 383-386)
  4. Uses context-managed httpx.AsyncClient (line 388)
  5. Sends request with proper serialization (lines 390-396)
  6. Validates response fields (lines 398-406)
  7. Provides comprehensive error handling (lines 410-427)

Good separation of validation, execution, and error handling.


429-522: LGTM! Comprehensive get_portfolio implementation.

The method properly:

  1. Validates session data and tokens (lines 433-440)
  2. Retrieves holdings and portfolio summary in parallel context (lines 444-468)
  3. Handles HTTP errors with detailed context (lines 470-487)
  4. Transforms HDFC data to domain models (lines 490-520)
  5. Documents API limitations (day_pnl hardcoded to 0.0)

The data transformation correctly maps HDFC's response structure to the internal Portfolio model.


572-607: LGTM! Clean cancel_order implementation.

The method properly:

  1. Validates access token (lines 575-579)
  2. Constructs DELETE request URL (line 581)
  3. Sends request with proper auth headers (lines 582-588)
  4. Parses response and returns OrderResponse (line 589)
  5. Handles errors with detailed context (lines 590-607)

Clean and straightforward implementation.


609-657: LGTM! Comprehensive get_order_book implementation.

The method properly:

  1. Validates access token (lines 610-614)
  2. Retrieves order book (lines 616-623)
  3. Transforms HDFC orders to domain models (lines 624-639)
  4. Handles API limitations (hardcodes OrderType.MARKET with comment on line 632)
  5. Provides comprehensive error handling (lines 640-657)

Good data transformation with proper enum mapping.


659-705: LGTM! Well-implemented get_trade_book method.

The method properly:

  1. Validates access token (lines 660-664)
  2. Retrieves trade book (lines 666-673)
  3. Transforms HDFC trades to domain models (lines 674-687)
  4. Handles errors comprehensively (lines 688-705)

Clean data transformation with proper field mapping.


707-740: LGTM! Simple and correct get_profile implementation.

The method properly:

  1. Validates access token (lines 708-712)
  2. Retrieves profile data (lines 714-721)
  3. Transforms to Profile model (line 722)
  4. Handles errors with detailed context (lines 723-740)

Straightforward implementation with proper error handling.


742-797: LGTM! Comprehensive get_holdings implementation.

The method properly:

  1. Validates access token and loginId (lines 743-752)
  2. Retrieves holdings with clientId parameter (lines 756-766)
  3. Transforms HDFC holdings to domain models (lines 768-779)
  4. Documents API limitation (day_pnl hardcoded to 0.0 on line 775)
  5. Handles errors comprehensively (lines 780-797)

Good data transformation with proper field mapping.


799-844: LGTM! Well-implemented get_positions method.

The method properly:

  1. Validates access token (lines 800-804)
  2. Retrieves positions (lines 806-813)
  3. Transforms HDFC positions to domain models (lines 814-826)
  4. Handles errors comprehensively (lines 827-844)

Clean implementation with proper enum conversion for product_type.


524-570: Please provide the official HDFC Securities API documentation (link or excerpt) for the modify order endpoint showing default values for optional parameters so I can verify they align with your implementation.

src/ordo/models/api/user.py (1)

1-7: LGTM! Simple and clean Profile model. The Profile model is well-defined with appropriate fields for user identification and contact information.

CI’s Black check failed—please run:

black src/ordo/models/api/user.py src/ordo/adapters/mock.py src/ordo/adapters/fyers.py src/ordo/adapters/hdfc.py

locally and commit the formatted changes.

Comment thread src/ordo/adapters/base.py

@abstractmethod
async def get_portfolio(self, session_data: Dict[str, Any]) -> Dict[str, Any]:
async def get_portfolio(self, session_data: Dict[str, Any]) -> Portfolio:

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

Verify that existing adapters handle the Portfolio return type correctly.

The return type changed from Dict[str, Any] to Portfolio. Ensure that:

  1. FyersAdapter's get_portfolio method returns a Portfolio instance (not a dict)
  2. MockAdapter's get_portfolio method returns a Portfolio instance (not a dict)

🏁 Script executed:

#!/bin/bash
# Check FyersAdapter.get_portfolio implementation
rg -A 20 "async def get_portfolio" src/ordo/adapters/fyers.py

# Check MockAdapter.get_portfolio implementation
rg -A 20 "async def get_portfolio" src/ordo/adapters/mock.py

Length of output: 1761


Convert adapters’ get_portfolio to return Portfolio instances

  • FyersAdapter (src/ordo/adapters/fyers.py): update async def get_portfolio(...) -> Portfolio and construct/return a Portfolio object instead of a dict.
  • MockAdapter (src/ordo/adapters/mock.py): update async def get_portfolio(...) -> Portfolio and return a Portfolio instance instead of the hardcoded dict.
🤖 Prompt for AI Agents
In src/ordo/adapters/base.py around line 29, update the adapters’ get_portfolio
contracts to return Portfolio instances; then in src/ordo/adapters/fyers.py and
src/ordo/adapters/mock.py change the method signatures to async def
get_portfolio(...)->Portfolio, import the Portfolio model/class, and replace the
dict return values with constructed Portfolio objects (mapping fields like
holdings, cash, updated_at, etc. into the Portfolio constructor or factory) so
each implementation returns a Portfolio instance instead of a plain dict; keep
the async behavior and ensure types and imports are updated accordingly.

Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (4)
src/ordo/adapters/hdfc.py (3)

185-192: Resource leak: httpx.AsyncClient not properly closed.

The http_client is created in __init__ but never closed, leading to a resource leak. Additionally, the codebase shows inconsistent patterns: some methods (complete_login, place_order, get_portfolio) create new clients with context managers (lines 328, 388, 444), while others (modify_order, cancel_order, get_order_book, get_trade_book, get_profile, get_holdings, get_positions) use self.http_client.

Consider one of these solutions:

Solution 1 (preferred): Use context manager per request consistently:

-    def __init__(self):
-        self.base_url = "https://developer.hdfcsec.com/oapi/v1"
-        self.session_manager = SessionManager(settings.SECRET_KEY)
-        self.http_client = httpx.AsyncClient(
-            headers={
-                "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
-            }
-        )
+    def __init__(self):
+        self.base_url = "https://developer.hdfcsec.com/oapi/v1"
+        self.session_manager = SessionManager(settings.SECRET_KEY)
+        self._headers = {
+            "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/123.0.0.0 Safari/537.36"
+        }
+
+    def _create_client(self) -> httpx.AsyncClient:
+        return httpx.AsyncClient(headers=self._headers)

Then update all methods using self.http_client to use async with self._create_client() as client: instead.

Solution 2: Add async lifecycle methods:

+    async def __aenter__(self):
+        return self
+
+    async def __aexit__(self, exc_type, exc_val, exc_tb):
+        await self.http_client.aclose()

And document that callers must use async with HDFCAdapter() as adapter:.


524-572: Multiple issues in modify_order method.

  1. Resource leak: Uses self.http_client at line 549 instead of creating a client with context manager.
  2. Missing error_code: ApiError at line 531 lacks the required error_code field.
  3. Missing parameter validation: No validation for required new_quantity parameter (previously flagged).

Apply this diff to address all three issues:

     async def modify_order(
         self, session_data: Dict[str, Any], order_id: str, **kwargs
     ) -> OrderResponse:
         config = HDFCConfig(**session_data["credentials"])
         access_token = self.session_manager.get_session(config.api_key, "access_token")

         if not access_token:
-            raise ApiException(ApiError(message="No access token found in session."))
+            raise ApiException(
+                ApiError(
+                    error_code="UNAUTHORIZED",
+                    message="No access token found in session."
+                )
+            )
+
+        # Validate required parameters
+        if "new_quantity" not in kwargs:
+            raise ApiException(
+                ApiError(
+                    error_code="INVALID_PARAMETERS",
+                    message="new_quantity is required for order modification"
+                )
+            )

         url = f"{self.base_url}/orders/regular/{order_id}?api_key={config.api_key}"
         payload = HDFCModifyOrderRequest(
             quantity=kwargs.get("new_quantity"),
             order_type=kwargs.get("order_type", "MARKET").upper(),
             validity=kwargs.get("validity", "DAY").upper(),
             disclosed_quantity=kwargs.get("disclosed_quantity", 0),
             product=kwargs.get("product", "DELIVERY").upper(),
             price=kwargs.get("new_price", 0.0),
             trigger_price=kwargs.get("trigger_price", 0.0),
             amo=kwargs.get("amo", False),
         )
         headers = {
             "Authorization": f"Bearer {access_token}",
             "Content-Type": "application/json",
         }
-        try:
-            response = await self.http_client.put(
-                url, json=payload.model_dump(), headers=headers
-            )
-            response.raise_for_status()
-            data = HDFCOrderActionResponse(**response.json())
-            return OrderResponse(order_id=data.data.order_id, status="success")
-        except httpx.HTTPStatusError as e:
-            raise ApiException(
-                ApiError(
-                    error_code="BROKER_API_ERROR",
-                    message=f"HDFC API error during modify_order: {e.response.text}",
-                    details={
-                        "status_code": e.response.status_code,
-                        "response": e.response.json(),
-                    },
-                )
-            )
-        except Exception as e:
-            raise ApiException(
-                ApiError(
-                    error_code="BROKER_REQUEST_FAILED",
-                    message=f"Failed to modify order with HDFC: {e}",
-                )
-            )
+        async with httpx.AsyncClient() as client:
+            try:
+                response = await client.put(
+                    url, json=payload.model_dump(), headers=headers
+                )
+                response.raise_for_status()
+                data = HDFCOrderActionResponse(**response.json())
+                return OrderResponse(order_id=data.data.order_id, status="success")
+            except httpx.HTTPStatusError as e:
+                raise ApiException(
+                    ApiError(
+                        error_code="BROKER_API_ERROR",
+                        message=f"HDFC API error during modify_order: {e.response.text}",
+                        details={
+                            "status_code": e.response.status_code,
+                            "response": e.response.json(),
+                        },
+                    )
+                )
+            except Exception as e:
+                raise ApiException(
+                    ApiError(
+                        error_code="BROKER_REQUEST_FAILED",
+                        message=f"Failed to modify order with HDFC: {e}",
+                    )
+                )

214-255: Create client with context manager in initiate_login.

The initiate_login method calls helpers that use the leaked self.http_client. After fixing the helper methods to accept a client parameter, wrap the API calls in a context manager.

Apply this diff:

     async def initiate_login(self, credentials: Dict[str, Any]) -> Dict[str, Any]:
         """
         Initiates the login process for HDFC Securities (Steps 1 & 2).
         """
         try:
             config = HDFCConfig(**credentials)
         except ValidationError as e:
             raise ValueError(f"Missing or invalid HDFC credentials: {e}")

-        try:
-            token_id = await self._get_login_token(config)
-            validate_data = await self._validate_user(config, token_id)
-
-            return {
-                "tokenId": token_id,
-                "loginId": validate_data.loginId,
-                "twofa": validate_data.twofa,
-                "twoFAEnabled": validate_data.twoFAEnabled,
-                "session_data": {
-                    "credentials": credentials,
-                    "tokenId": token_id,
-                    "loginId": validate_data.loginId,
-                },
-            }
-        except httpx.HTTPStatusError as e:
+        async with httpx.AsyncClient() as client:
+            try:
+                token_id = await self._get_login_token(client, config)
+                validate_data = await self._validate_user(client, config, token_id)
+
+                return {
+                    "tokenId": token_id,
+                    "loginId": validate_data.loginId,
+                    "twofa": validate_data.twofa,
+                    "twoFAEnabled": validate_data.twoFAEnabled,
+                    "session_data": {
+                        "credentials": credentials,
+                        "tokenId": token_id,
+                        "loginId": validate_data.loginId,
+                    },
+                }
+            except httpx.HTTPStatusError as e:
+                raise ApiException(
+                    ApiError(
+                        error_code="BROKER_API_ERROR",
+                        message=f"HDFC API error during initiate_login: {e.response.text}",
+                        details={
+                            "status_code": e.response.status_code,
+                            "response": e.response.json(),
+                        },
+                    )
+                )
+            except Exception as e:
+                raise ApiException(
+                    ApiError(
+                        error_code="BROKER_REQUEST_FAILED",
+                        message=f"Failed to initiate login with HDFC: {e}",
+                    )
+                )
-            raise ApiException(
-                ApiError(
-                    error_code="BROKER_API_ERROR",
-                    message=f"HDFC API error during initiate_login: {e.response.text}",
-                    details={
-                        "status_code": e.response.status_code,
-                        "response": e.response.json(),
-                    },
-                )
-            )
-        except Exception as e:
-            raise ApiException(
-                ApiError(
-                    error_code="BROKER_REQUEST_FAILED",
-                    message=f"Failed to initiate login with HDFC: {e}",
-                )
-            )
docs/qa/gates/2.1-hdfc-securities-adapter.yml (1)

10-10: Fix YAML formatting.

YAMLlint flags excess spacing inside braces at line 10.

Apply this diff:

-waiver: {active: false}
+waiver: {active: false}

Or use block style:

-waiver: {active: false}
+waiver:
+  active: false
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2cc3ec0 and c9562f1.

📒 Files selected for processing (4)
  • docs/qa/gates/2.1-hdfc-securities-adapter.yml (1 hunks)
  • src/ordo/adapters/fyers.py (1 hunks)
  • src/ordo/adapters/hdfc.py (1 hunks)
  • src/ordo/adapters/mock.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (3)
src/ordo/adapters/hdfc.py (6)
src/ordo/adapters/base.py (11)
  • IBrokerAdapter (9-86)
  • initiate_login (15-19)
  • complete_login (22-26)
  • get_portfolio (29-33)
  • modify_order (36-42)
  • cancel_order (45-51)
  • get_order_book (54-58)
  • get_trade_book (61-65)
  • get_profile (68-72)
  • get_holdings (75-79)
  • get_positions (82-86)
src/ordo/models/api/errors.py (2)
  • ApiError (7-19)
  • ApiException (22-25)
src/ordo/models/api/portfolio.py (3)
  • Portfolio (29-42)
  • Holding (5-16)
  • Funds (19-26)
src/ordo/models/api/order.py (7)
  • Order (24-33)
  • Trade (36-43)
  • Position (46-52)
  • OrderResponse (55-57)
  • TransactionType (5-7)
  • OrderType (10-14)
  • ProductType (17-21)
src/ordo/models/api/user.py (1)
  • Profile (4-7)
src/ordo/security/session.py (3)
  • SessionManager (6-36)
  • set_session (19-25)
  • get_session (27-36)
src/ordo/adapters/fyers.py (2)
src/ordo/adapters/hdfc.py (7)
  • modify_order (524-572)
  • cancel_order (574-609)
  • get_order_book (611-659)
  • get_trade_book (661-707)
  • get_profile (709-742)
  • get_holdings (744-797)
  • get_positions (799-844)
src/ordo/adapters/mock.py (7)
  • modify_order (70-73)
  • cancel_order (75-76)
  • get_order_book (78-79)
  • get_trade_book (81-82)
  • get_profile (84-85)
  • get_holdings (87-88)
  • get_positions (90-91)
src/ordo/adapters/mock.py (1)
src/ordo/adapters/base.py (7)
  • modify_order (36-42)
  • cancel_order (45-51)
  • get_order_book (54-58)
  • get_trade_book (61-65)
  • get_profile (68-72)
  • get_holdings (75-79)
  • get_positions (82-86)
🔇 Additional comments (3)
src/ordo/adapters/hdfc.py (1)

23-178: LGTM!

The Pydantic models comprehensively cover HDFC API request/response structures with appropriate field types and naming conventions that match the HDFC API schema.

src/ordo/adapters/fyers.py (1)

247-268: LGTM!

The placeholder methods correctly implement the extended IBrokerAdapter interface. Raising NotImplementedError is appropriate for functionality that hasn't been implemented yet in the Fyers adapter.

src/ordo/adapters/mock.py (1)

70-91: LGTM!

The placeholder methods correctly implement the extended IBrokerAdapter interface. Raising NotImplementedError is appropriate for mock functionality that hasn't been implemented yet.

Comment thread src/ordo/adapters/hdfc.py Outdated
Comment thread src/ordo/adapters/hdfc.py
Comment thread src/ordo/adapters/hdfc.py
Comment thread src/ordo/adapters/hdfc.py
Comment on lines +574 to +844
async def cancel_order(
self, session_data: Dict[str, Any], order_id: str
) -> OrderResponse:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")

if not access_token:
raise ApiException(ApiError(message="No access token found in session."))

url = f"{self.base_url}/orders/regular/{order_id}?api_key={config.api_key}"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
response = await self.http_client.delete(url, headers=headers)
response.raise_for_status()
data = HDFCOrderActionResponse(**response.json())
return OrderResponse(order_id=data.data.order_id, status="cancelled")
except httpx.HTTPStatusError as e:
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during cancel_order: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": e.response.json(),
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to cancel order with HDFC: {e}",
)
)

async def get_order_book(self, session_data: Dict[str, Any]) -> List[Order]:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")

if not access_token:
raise ApiException(ApiError(message="No access token found in session."))

url = f"{self.base_url}/orders?api_key={config.api_key}"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
response = await self.http_client.get(url, headers=headers)
response.raise_for_status()
data = HDFCOrderBookResponse(**response.json())
orders = []
for item in data.data:
orders.append(
Order(
order_id=item.order_id,
symbol=item.tradingsymbol,
status=item.status,
transaction_type=TransactionType(item.transaction_type.lower()),
order_type=OrderType.MARKET, # HDFC does not provide order type in order book
product_type=ProductType(item.product.lower()),
quantity=item.quantity,
price=item.price,
timestamp=item.order_timestamp,
)
)
return orders
except httpx.HTTPStatusError as e:
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_order_book: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": e.response.json(),
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get order book from HDFC: {e}",
)
)

async def get_trade_book(self, session_data: Dict[str, Any]) -> List[Trade]:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")

if not access_token:
raise ApiException(ApiError(message="No access token found in session."))

url = f"{self.base_url}/trades?api_key={config.api_key}"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
response = await self.http_client.get(url, headers=headers)
response.raise_for_status()
data = HDFCTradeBookResponse(**response.json())
trades = []
for item in data.data:
trades.append(
Trade(
trade_id=item.trade_id,
order_id=item.order_id,
symbol=item.security_id,
transaction_type=TransactionType(item.transaction_type.lower()),
quantity=item.filled_quantity,
price=item.average_price,
timestamp=item.fill_timestamp,
)
)
return trades
except httpx.HTTPStatusError as e:
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_trade_book: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": e.response.json(),
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get trade book from HDFC: {e}",
)
)

async def get_profile(self, session_data: Dict[str, Any]) -> Profile:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")

if not access_token:
raise ApiException(ApiError(message="No access token found in session."))

url = f"{self.base_url}/profile"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
response = await self.http_client.get(url, headers=headers)
response.raise_for_status()
data = HDFCProfileResponse(**response.json())
return Profile(client_id=data.client_id, name=data.name, email=data.email)
except httpx.HTTPStatusError as e:
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_profile: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": e.response.json(),
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get profile from HDFC: {e}",
)
)

async def get_holdings(self, session_data: Dict[str, Any]) -> List[Holding]:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")

if not access_token:
raise ApiException(ApiError(message="No access token found in session."))

login_id = session_data.get("loginId")

if not login_id:
raise ApiException(ApiError(message="No login ID found in session."))

headers = {"Authorization": f"Bearer {access_token}"}

try:
# Retrieve Holdings
holdings_response = await self.http_client.get(
f"{self.base_url}/holdings",
headers=headers,
params={"clientId": login_id}, # Assuming clientId is a query parameter
)
holdings_response.raise_for_status()
holdings_data = HDFCHoldingsResponse(**holdings_response.json())

return [
Holding(
symbol=h.symbol,
quantity=h.quantity,
ltp=h.currentPrice,
avg_price=h.averagePrice,
pnl=h.profitLoss,
day_pnl=0.0, # HDFC API does not provide day P&L in the holdings endpoint.
value=h.totalValue,
)
for h in holdings_data.holdings
]
except httpx.HTTPStatusError as e:
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_holdings: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": e.response.json(),
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get holdings from HDFC: {e}",
)
)

async def get_positions(self, session_data: Dict[str, Any]) -> List[Position]:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")

if not access_token:
raise ApiException(ApiError(message="No access token found in session."))

url = f"{self.base_url}/portfolio/overall_positions?api_key={config.api_key}"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
response = await self.http_client.get(url, headers=headers)
response.raise_for_status()
response_data = HDFCPositionsResponse(**response.json())
positions = []
for item in response_data.data.net:
positions.append(
Position(
symbol=item.security_id,
quantity=item.net_qty,
product_type=ProductType(item.product.lower()),
exchange=item.exchange,
instrument_type=item.instrument_segment,
realised_pnl=item.realised_pl_overall_position,
)
)
return positions
except httpx.HTTPStatusError as e:
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_positions: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": e.response.json(),
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get positions from HDFC: {e}",
)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Consistent issues across remaining methods: resource leak and missing error_code.

All remaining methods (cancel_order, get_order_book, get_trade_book, get_profile, get_holdings, get_positions) have two issues:

  1. Resource leak: Use self.http_client instead of creating clients with context managers.
  2. Missing error_code: ApiError instances for session validation lack the required error_code field (lines 581, 616, 666, 714, 749, 754, 804).

For each method, apply a similar fix pattern. Example for cancel_order:

     async def cancel_order(
         self, session_data: Dict[str, Any], order_id: str
     ) -> OrderResponse:
         config = HDFCConfig(**session_data["credentials"])
         access_token = self.session_manager.get_session(config.api_key, "access_token")

         if not access_token:
-            raise ApiException(ApiError(message="No access token found in session."))
+            raise ApiException(
+                ApiError(
+                    error_code="UNAUTHORIZED",
+                    message="No access token found in session."
+                )
+            )

         url = f"{self.base_url}/orders/regular/{order_id}?api_key={config.api_key}"
         headers = {
             "Authorization": f"Bearer {access_token}",
         }
-        try:
-            response = await self.http_client.delete(url, headers=headers)
-            response.raise_for_status()
-            data = HDFCOrderActionResponse(**response.json())
-            return OrderResponse(order_id=data.data.order_id, status="cancelled")
-        except httpx.HTTPStatusError as e:
-            raise ApiException(
-                ApiError(
-                    error_code="BROKER_API_ERROR",
-                    message=f"HDFC API error during cancel_order: {e.response.text}",
-                    details={
-                        "status_code": e.response.status_code,
-                        "response": e.response.json(),
-                    },
-                )
-            )
-        except Exception as e:
-            raise ApiException(
-                ApiError(
-                    error_code="BROKER_REQUEST_FAILED",
-                    message=f"Failed to cancel order with HDFC: {e}",
-                )
-            )
+        async with httpx.AsyncClient() as client:
+            try:
+                response = await client.delete(url, headers=headers)
+                response.raise_for_status()
+                data = HDFCOrderActionResponse(**response.json())
+                return OrderResponse(order_id=data.data.order_id, status="cancelled")
+            except httpx.HTTPStatusError as e:
+                raise ApiException(
+                    ApiError(
+                        error_code="BROKER_API_ERROR",
+                        message=f"HDFC API error during cancel_order: {e.response.text}",
+                        details={
+                            "status_code": e.response.status_code,
+                            "response": e.response.json(),
+                        },
+                    )
+                )
+            except Exception as e:
+                raise ApiException(
+                    ApiError(
+                        error_code="BROKER_REQUEST_FAILED",
+                        message=f"Failed to cancel order with HDFC: {e}",
+                    )
+                )

Apply the same pattern to the other five methods, adjusting error messages as appropriate.

📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def cancel_order(
self, session_data: Dict[str, Any], order_id: str
) -> OrderResponse:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")
if not access_token:
raise ApiException(ApiError(message="No access token found in session."))
url = f"{self.base_url}/orders/regular/{order_id}?api_key={config.api_key}"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
response = await self.http_client.delete(url, headers=headers)
response.raise_for_status()
data = HDFCOrderActionResponse(**response.json())
return OrderResponse(order_id=data.data.order_id, status="cancelled")
except httpx.HTTPStatusError as e:
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during cancel_order: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": e.response.json(),
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to cancel order with HDFC: {e}",
)
)
async def get_order_book(self, session_data: Dict[str, Any]) -> List[Order]:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")
if not access_token:
raise ApiException(ApiError(message="No access token found in session."))
url = f"{self.base_url}/orders?api_key={config.api_key}"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
response = await self.http_client.get(url, headers=headers)
response.raise_for_status()
data = HDFCOrderBookResponse(**response.json())
orders = []
for item in data.data:
orders.append(
Order(
order_id=item.order_id,
symbol=item.tradingsymbol,
status=item.status,
transaction_type=TransactionType(item.transaction_type.lower()),
order_type=OrderType.MARKET, # HDFC does not provide order type in order book
product_type=ProductType(item.product.lower()),
quantity=item.quantity,
price=item.price,
timestamp=item.order_timestamp,
)
)
return orders
except httpx.HTTPStatusError as e:
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_order_book: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": e.response.json(),
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get order book from HDFC: {e}",
)
)
async def get_trade_book(self, session_data: Dict[str, Any]) -> List[Trade]:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")
if not access_token:
raise ApiException(ApiError(message="No access token found in session."))
url = f"{self.base_url}/trades?api_key={config.api_key}"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
response = await self.http_client.get(url, headers=headers)
response.raise_for_status()
data = HDFCTradeBookResponse(**response.json())
trades = []
for item in data.data:
trades.append(
Trade(
trade_id=item.trade_id,
order_id=item.order_id,
symbol=item.security_id,
transaction_type=TransactionType(item.transaction_type.lower()),
quantity=item.filled_quantity,
price=item.average_price,
timestamp=item.fill_timestamp,
)
)
return trades
except httpx.HTTPStatusError as e:
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_trade_book: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": e.response.json(),
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get trade book from HDFC: {e}",
)
)
async def get_profile(self, session_data: Dict[str, Any]) -> Profile:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")
if not access_token:
raise ApiException(ApiError(message="No access token found in session."))
url = f"{self.base_url}/profile"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
response = await self.http_client.get(url, headers=headers)
response.raise_for_status()
data = HDFCProfileResponse(**response.json())
return Profile(client_id=data.client_id, name=data.name, email=data.email)
except httpx.HTTPStatusError as e:
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_profile: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": e.response.json(),
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get profile from HDFC: {e}",
)
)
async def get_holdings(self, session_data: Dict[str, Any]) -> List[Holding]:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")
if not access_token:
raise ApiException(ApiError(message="No access token found in session."))
login_id = session_data.get("loginId")
if not login_id:
raise ApiException(ApiError(message="No login ID found in session."))
headers = {"Authorization": f"Bearer {access_token}"}
try:
# Retrieve Holdings
holdings_response = await self.http_client.get(
f"{self.base_url}/holdings",
headers=headers,
params={"clientId": login_id}, # Assuming clientId is a query parameter
)
holdings_response.raise_for_status()
holdings_data = HDFCHoldingsResponse(**holdings_response.json())
return [
Holding(
symbol=h.symbol,
quantity=h.quantity,
ltp=h.currentPrice,
avg_price=h.averagePrice,
pnl=h.profitLoss,
day_pnl=0.0, # HDFC API does not provide day P&L in the holdings endpoint.
value=h.totalValue,
)
for h in holdings_data.holdings
]
except httpx.HTTPStatusError as e:
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_holdings: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": e.response.json(),
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get holdings from HDFC: {e}",
)
)
async def get_positions(self, session_data: Dict[str, Any]) -> List[Position]:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")
if not access_token:
raise ApiException(ApiError(message="No access token found in session."))
url = f"{self.base_url}/portfolio/overall_positions?api_key={config.api_key}"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
response = await self.http_client.get(url, headers=headers)
response.raise_for_status()
response_data = HDFCPositionsResponse(**response.json())
positions = []
for item in response_data.data.net:
positions.append(
Position(
symbol=item.security_id,
quantity=item.net_qty,
product_type=ProductType(item.product.lower()),
exchange=item.exchange,
instrument_type=item.instrument_segment,
realised_pnl=item.realised_pl_overall_position,
)
)
return positions
except httpx.HTTPStatusError as e:
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_positions: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": e.response.json(),
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get positions from HDFC: {e}",
)
)
async def cancel_order(
self, session_data: Dict[str, Any], order_id: str
) -> OrderResponse:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")
if not access_token:
raise ApiException(
ApiError(
error_code="UNAUTHORIZED",
message="No access token found in session."
)
)
url = f"{self.base_url}/orders/regular/{order_id}?api_key={config.api_key}"
headers = {
"Authorization": f"Bearer {access_token}",
}
async with httpx.AsyncClient() as client:
try:
response = await client.delete(url, headers=headers)
response.raise_for_status()
data = HDFCOrderActionResponse(**response.json())
return OrderResponse(order_id=data.data.order_id, status="cancelled")
except httpx.HTTPStatusError as e:
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during cancel_order: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": e.response.json(),
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to cancel order with HDFC: {e}",
)
)
🤖 Prompt for AI Agents
In src/ordo/adapters/hdfc.py around lines 574-844, update all session-validation
ApiError raises to include an error_code (add e.g.
error_code="MISSING_ACCESS_TOKEN" for the access_token checks at lines ~581,
~616, ~666, ~714, ~804 and error_code="MISSING_LOGIN_ID" for the loginId check
at ~749/754) and ensure no new httpx.AsyncClient instances are created (use the
injected self.http_client for all requests to avoid resource leaks); apply the
same pattern and adjust the error message text appropriately for cancel_order,
get_order_book, get_trade_book, get_profile, get_holdings, and get_positions.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 5

♻️ Duplicate comments (7)
src/ordo/adapters/hdfc.py (7)

276-295: Add missing error_code to ApiError.

The ApiError at line 293 is missing the required error_code field.

Apply this diff:

         if not authorise_data.requestToken:
             raise ApiException(
-                ApiError(message="Failed to get requestToken after authorization")
+                ApiError(
+                    error_code="AUTHORIZATION_FAILED",
+                    message="Failed to get requestToken after authorization"
+                )
             )

297-311: Add missing error_code to ApiError.

The ApiError at line 309 is missing the required error_code field.

Apply this diff:

         if not access_token_data.accessToken:
             raise ApiException(
-                ApiError(message="Failed to get accessToken from HDFC API")
+                ApiError(
+                    error_code="ACCESS_TOKEN_FAILED",
+                    message="Failed to get accessToken from HDFC API"
+                )
             )

260-274: Add missing error_code to ApiError.

The ApiError at line 272 is missing the required error_code field. Per the ApiError model in src/ordo/models/api/errors.py, error_code is a required field.

Apply this diff:

         if not twofa_data.requestToken:
             raise ApiException(
-                ApiError(message="Failed to get requestToken after 2FA validation")
+                ApiError(
+                    error_code="2FA_VALIDATION_FAILED",
+                    message="Failed to get requestToken after 2FA validation"
+                )
             )

432-525: Add missing error_code to ApiError instances.

The ApiError instances at lines 441 and 443 are missing the required error_code field.

Apply this diff:

         if not access_token:
-            raise ApiException(ApiError(message="No access token found in session."))
+            raise ApiException(
+                ApiError(
+                    error_code="UNAUTHORIZED",
+                    message="No access token found in session."
+                )
+            )
         if not login_id:
-            raise ApiException(ApiError(message="No login ID found in session."))
+            raise ApiException(
+                ApiError(
+                    error_code="INVALID_SESSION",
+                    message="No login ID found in session."
+                )
+            )

754-810: Add missing error_code to ApiError instances.

The ApiError instances at lines 759 and 764 are missing the required error_code field.

Apply this diff:

         if not access_token:
-            raise ApiException(ApiError(message="No access token found in session."))
+            raise ApiException(
+                ApiError(
+                    error_code="UNAUTHORIZED",
+                    message="No access token found in session."
+                )
+            )

         login_id = session_data.get("loginId")

         if not login_id:
-            raise ApiException(ApiError(message="No login ID found in session."))
+            raise ApiException(
+                ApiError(
+                    error_code="INVALID_SESSION",
+                    message="No login ID found in session."
+                )
+            )

578-614: Add missing error_code to ApiError.

The ApiError at line 585 is missing the required error_code field.

Apply this diff:

         if not access_token:
-            raise ApiException(ApiError(message="No access token found in session."))
+            raise ApiException(
+                ApiError(
+                    error_code="UNAUTHORIZED",
+                    message="No access token found in session."
+                )
+            )

716-752: Add missing error_code to ApiError.

The ApiError at line 721 is missing the required error_code field.

Apply this diff:

         if not access_token:
-            raise ApiException(ApiError(message="No access token found in session."))
+            raise ApiException(
+                ApiError(
+                    error_code="UNAUTHORIZED",
+                    message="No access token found in session."
+                )
+            )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c9562f1 and 9c7063f.

📒 Files selected for processing (1)
  • src/ordo/adapters/hdfc.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/ordo/adapters/hdfc.py (6)
src/ordo/adapters/base.py (11)
  • IBrokerAdapter (9-86)
  • initiate_login (15-19)
  • complete_login (22-26)
  • get_portfolio (29-33)
  • modify_order (36-42)
  • cancel_order (45-51)
  • get_order_book (54-58)
  • get_trade_book (61-65)
  • get_profile (68-72)
  • get_holdings (75-79)
  • get_positions (82-86)
src/ordo/models/api/errors.py (2)
  • ApiError (7-19)
  • ApiException (22-25)
src/ordo/models/api/portfolio.py (3)
  • Portfolio (29-42)
  • Holding (5-16)
  • Funds (19-26)
src/ordo/models/api/order.py (7)
  • Order (24-33)
  • Trade (36-43)
  • Position (46-52)
  • OrderResponse (55-57)
  • TransactionType (5-7)
  • OrderType (10-14)
  • ProductType (17-21)
src/ordo/models/api/user.py (1)
  • Profile (4-7)
src/ordo/security/session.py (3)
  • SessionManager (6-36)
  • set_session (19-25)
  • get_session (27-36)

Comment thread src/ordo/adapters/hdfc.py
Comment thread src/ordo/adapters/hdfc.py
Comment on lines +527 to +576
async def modify_order(
self, session_data: Dict[str, Any], order_id: str, **kwargs
) -> OrderResponse:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")

if not access_token:
raise ApiException(ApiError(message="No access token found in session."))

url = f"{self.base_url}/orders/regular/{order_id}?api_key={config.api_key}"
payload = HDFCModifyOrderRequest(
quantity=kwargs.get("new_quantity"),
order_type=kwargs.get("order_type", "MARKET").upper(),
validity=kwargs.get("validity", "DAY").upper(),
disclosed_quantity=kwargs.get("disclosed_quantity", 0),
product=kwargs.get("product", "DELIVERY").upper(),
price=kwargs.get("new_price", 0.0),
trigger_price=kwargs.get("trigger_price", 0.0),
amo=kwargs.get("amo", False),
)
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
try:
async with self._create_client() as client:
response = await client.put(
url, json=payload.model_dump(), headers=headers
)
response.raise_for_status()
data = HDFCOrderActionResponse(**response.json())
return OrderResponse(order_id=data.data.order_id, status="success")
except httpx.HTTPStatusError as e:
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during modify_order: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": e.response.json(),
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to modify order with HDFC: {e}",
)
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

Add missing error_code and consider parameter validation.

The ApiError at line 534 is missing the required error_code field. Additionally, consider validating that required parameters like new_quantity are provided before constructing the Pydantic model.

Apply this diff:

         if not access_token:
-            raise ApiException(ApiError(message="No access token found in session."))
+            raise ApiException(
+                ApiError(
+                    error_code="UNAUTHORIZED",
+                    message="No access token found in session."
+                )
+            )
+
+        # Validate required parameters
+        if "new_quantity" not in kwargs or kwargs["new_quantity"] is None:
+            raise ApiException(
+                ApiError(
+                    error_code="INVALID_PARAMETERS",
+                    message="new_quantity is required for order modification"
+                )
+            )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def modify_order(
self, session_data: Dict[str, Any], order_id: str, **kwargs
) -> OrderResponse:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")
if not access_token:
raise ApiException(ApiError(message="No access token found in session."))
url = f"{self.base_url}/orders/regular/{order_id}?api_key={config.api_key}"
payload = HDFCModifyOrderRequest(
quantity=kwargs.get("new_quantity"),
order_type=kwargs.get("order_type", "MARKET").upper(),
validity=kwargs.get("validity", "DAY").upper(),
disclosed_quantity=kwargs.get("disclosed_quantity", 0),
product=kwargs.get("product", "DELIVERY").upper(),
price=kwargs.get("new_price", 0.0),
trigger_price=kwargs.get("trigger_price", 0.0),
amo=kwargs.get("amo", False),
)
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
try:
async with self._create_client() as client:
response = await client.put(
url, json=payload.model_dump(), headers=headers
)
response.raise_for_status()
data = HDFCOrderActionResponse(**response.json())
return OrderResponse(order_id=data.data.order_id, status="success")
except httpx.HTTPStatusError as e:
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during modify_order: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": e.response.json(),
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to modify order with HDFC: {e}",
)
)
async def modify_order(
self, session_data: Dict[str, Any], order_id: str, **kwargs
) -> OrderResponse:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")
if not access_token:
raise ApiException(
ApiError(
error_code="UNAUTHORIZED",
message="No access token found in session."
)
)
# Validate required parameters
if "new_quantity" not in kwargs or kwargs["new_quantity"] is None:
raise ApiException(
ApiError(
error_code="INVALID_PARAMETERS",
message="new_quantity is required for order modification"
)
)
url = f"{self.base_url}/orders/regular/{order_id}?api_key={config.api_key}"
payload = HDFCModifyOrderRequest(
quantity=kwargs.get("new_quantity"),
order_type=kwargs.get("order_type", "MARKET").upper(),
validity=kwargs.get("validity", "DAY").upper(),
disclosed_quantity=kwargs.get("disclosed_quantity", 0),
product=kwargs.get("product", "DELIVERY").upper(),
price=kwargs.get("new_price", 0.0),
trigger_price=kwargs.get("trigger_price", 0.0),
amo=kwargs.get("amo", False),
)
headers = {
"Authorization": f"Bearer {access_token}",
"Content-Type": "application/json",
}
try:
async with self._create_client() as client:
response = await client.put(
url, json=payload.model_dump(), headers=headers
)
response.raise_for_status()
data = HDFCOrderActionResponse(**response.json())
return OrderResponse(order_id=data.data.order_id, status="success")
except httpx.HTTPStatusError as e:
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during modify_order: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": e.response.json(),
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to modify order with HDFC: {e}",
)
)

coderkrp and others added 2 commits October 5, 2025 08:28
Co-authored-by: coderabbitai[bot] <136622811+coderabbitai[bot]@users.noreply.github.com>

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (2)
src/ordo/adapters/fyers.py (2)

168-168: Add explicit timeouts to prevent hanging requests.

The httpx.AsyncClient() is created without a timeout configuration. If the Fyers API becomes unresponsive, requests could hang indefinitely, blocking the async event loop and degrading user experience.

Apply this diff to add a reasonable timeout:

-        async with httpx.AsyncClient() as client:
+        async with httpx.AsyncClient(timeout=30.0) as client:

Consider using more granular timeouts if needed:

timeout = httpx.Timeout(connect=5.0, read=30.0, write=5.0, pool=5.0)
async with httpx.AsyncClient(timeout=timeout) as client:

Based on learnings about httpx best practices.


227-234: Consider validating the funds response structure.

The funds transformation creates a title-based mapping and uses .get() with defaults. While this won't crash if expected keys are missing, it silently defaults to 0, which could mask API contract changes or unexpected responses.

Consider adding validation or logging if the expected structure is not present:

         # Transform funds
         funds_map = {item["title"]: item for item in funds_data.get("fund_limit", [])}
+        expected_titles = ["Available Balance", "Utilized Amount", "Total Balance"]
+        missing_titles = [t for t in expected_titles if t not in funds_map]
+        if missing_titles:
+            # Log warning about missing fund data
+            pass
         funds = Funds(

This helps detect API changes during development and debugging.

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 6cc460d and ea9e4fc.

📒 Files selected for processing (2)
  • src/ordo/adapters/fyers.py (2 hunks)
  • src/ordo/adapters/mock.py (2 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
src/ordo/adapters/fyers.py (2)
src/ordo/adapters/mock.py (8)
  • get_portfolio (21-99)
  • modify_order (101-104)
  • cancel_order (106-107)
  • get_order_book (109-110)
  • get_trade_book (112-113)
  • get_profile (115-116)
  • get_holdings (118-119)
  • get_positions (121-122)
src/ordo/models/api/portfolio.py (1)
  • Portfolio (29-42)
src/ordo/adapters/mock.py (3)
src/ordo/models/api/portfolio.py (3)
  • Portfolio (29-42)
  • Holding (5-16)
  • Funds (19-26)
src/ordo/adapters/base.py (1)
  • get_portfolio (29-33)
tests/adapters/test_mock.py (1)
  • get_portfolio (46-61)
🔇 Additional comments (3)
src/ordo/adapters/fyers.py (1)

247-268: LGTM! Stub methods correctly implement the extended interface.

These NotImplementedError stubs correctly implement the newly added methods in the IBrokerAdapter interface. This approach is consistent with the pattern used in mock.py and allows the adapter to conform to the interface while HDFC-specific implementations are developed.

src/ordo/adapters/mock.py (2)

3-3: LGTM!

The new imports correctly bring in the Portfolio, Holding, and Funds models needed for the updated get_portfolio method.


101-122: LGTM: Interface stubs added.

The seven new async methods correctly implement the extended IBrokerAdapter interface by providing stub implementations that raise NotImplementedError. This is acceptable for a mock adapter where not all functionality is needed for testing.

Note that get_portfolio is fully implemented while related methods like get_holdings and get_positions remain as stubs. This selective implementation is fine if those specific methods aren't required for current tests.

Comment thread src/ordo/adapters/mock.py
Comment thread src/ordo/adapters/mock.py
Comment on lines +23 to +68
holdings_data = [
{
"symbol": "RELIANCE-EQ",
"exchange": "NSE",
"quantity": 50,
"average_price": 2500.00,
"last_price": 2850.50,
"pnl": 17525.00,
"day_pnl": 250.00,
"value": 142525.00,
"instrument_type": "EQ",
},
{
"symbol": "TCS-EQ",
"exchange": "NSE",
"quantity": 100,
"average_price": 3500.00,
"last_price": 3800.00,
"pnl": 30000.00,
"day_pnl": -1500.00,
"value": 380000.00,
"instrument_type": "EQ",
},
{
"symbol": "HDFCBANK-EQ",
"exchange": "NSE",
"quantity": 200,
"average_price": 1500.00,
"last_price": 1450.00,
"pnl": -10000.00,
"day_pnl": -2000.00,
"value": 290000.00,
"instrument_type": "EQ",
},
{
"symbol": "NIFTYBEES",
"exchange": "NSE",
"quantity": 500,
"average_price": 200.00,
"last_price": 225.00,
"pnl": 12500.00,
"day_pnl": 500.00,
"value": 112500.00,
"instrument_type": "ETF",
},
}
]

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Unused fields in holdings_data.

The holdings_data dictionary includes exchange and instrument_type fields that are not used when constructing Holding instances (lines 71-81). These fields are omitted because the Holding model doesn't include them.

While not harmful for mock data, consider either:

  • Removing these unused fields for clarity
  • Adding a comment explaining they're kept for potential future use

If these fields are not needed, apply this diff to remove them:

         holdings_data = [
             {
                 "symbol": "RELIANCE-EQ",
-                "exchange": "NSE",
                 "quantity": 50,
                 "average_price": 2500.00,
                 "last_price": 2850.50,
                 "pnl": 17525.00,
                 "day_pnl": 250.00,
                 "value": 142525.00,
-                "instrument_type": "EQ",
             },
             # ... apply same pattern to other holdings
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
holdings_data = [
{
"symbol": "RELIANCE-EQ",
"exchange": "NSE",
"quantity": 50,
"average_price": 2500.00,
"last_price": 2850.50,
"pnl": 17525.00,
"day_pnl": 250.00,
"value": 142525.00,
"instrument_type": "EQ",
},
{
"symbol": "TCS-EQ",
"exchange": "NSE",
"quantity": 100,
"average_price": 3500.00,
"last_price": 3800.00,
"pnl": 30000.00,
"day_pnl": -1500.00,
"value": 380000.00,
"instrument_type": "EQ",
},
{
"symbol": "HDFCBANK-EQ",
"exchange": "NSE",
"quantity": 200,
"average_price": 1500.00,
"last_price": 1450.00,
"pnl": -10000.00,
"day_pnl": -2000.00,
"value": 290000.00,
"instrument_type": "EQ",
},
{
"symbol": "NIFTYBEES",
"exchange": "NSE",
"quantity": 500,
"average_price": 200.00,
"last_price": 225.00,
"pnl": 12500.00,
"day_pnl": 500.00,
"value": 112500.00,
"instrument_type": "ETF",
},
}
]
holdings_data = [
{
"symbol": "RELIANCE-EQ",
"quantity": 50,
"average_price": 2500.00,
"last_price": 2850.50,
"pnl": 17525.00,
"day_pnl": 250.00,
"value": 142525.00,
},
{
"symbol": "TCS-EQ",
"quantity": 100,
"average_price": 3500.00,
"last_price": 3800.00,
"pnl": 30000.00,
"day_pnl": -1500.00,
"value": 380000.00,
},
{
"symbol": "HDFCBANK-EQ",
"quantity": 200,
"average_price": 1500.00,
"last_price": 1450.00,
"pnl": -10000.00,
"day_pnl": -2000.00,
"value": 290000.00,
},
{
"symbol": "NIFTYBEES",
"quantity": 500,
"average_price": 200.00,
"last_price": 225.00,
"pnl": 12500.00,
"day_pnl": 500.00,
"value": 112500.00,
},
]
🤖 Prompt for AI Agents
In src/ordo/adapters/mock.py around lines 23 to 68, the mock holdings_data
entries include unused keys "exchange" and "instrument_type" which are not
consumed when creating Holding instances (see lines ~71-81); remove these two
keys from each holdings dict to keep mock data minimal and consistent with the
Holding model, or alternatively add a single-line comment above holdings_data
explaining these fields are intentionally retained for future use—choose one
approach and apply it consistently to all entries.

This commit updates the HDFC Securities adapter to conform to the provided "Place Order" API documentation.

- Updates the `place_order` endpoint URL and request/response models in `HDFCAdapter`.
- Corrects the data types and enumerations for order parameters (`TransactionType`, `OrderType`, `ProductType`, etc.) to match the API specification.
- Removes the hardcoded local path from `requirements.txt` to ensure portability.
- Adds `email-validator` dependency.
- Fixes linting and formatting issues identified by `ruff` and `black`.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (10)
src/ordo/adapters/hdfc.py (10)

509-512: Add missing error_code fields.

The ApiError instances at lines 510 and 512 are missing the required error_code field.

Apply this diff:

         if not access_token:
-            raise ApiException(ApiError(message="No access token found in session."))
+            raise ApiException(
+                ApiError(
+                    error_code="UNAUTHORIZED",
+                    message="No access token found in session."
+                )
+            )
         if not login_id:
-            raise ApiException(ApiError(message="No login ID found in session."))
+            raise ApiException(
+                ApiError(
+                    error_code="INVALID_SESSION",
+                    message="No login ID found in session."
+                )
+            )

655-656: Add missing error_code field.

The ApiError at line 656 is missing the required error_code field.

Apply this diff:

         if not access_token:
-            raise ApiException(ApiError(message="No access token found in session."))
+            raise ApiException(
+                ApiError(
+                    error_code="UNAUTHORIZED",
+                    message="No access token found in session."
+                )
+            )

358-362: Add missing error_code field.

The ApiError at line 360 is missing the required error_code field.

Apply this diff:

         if not authorise_data.requestToken:
             raise ApiException(
-                ApiError(message="Failed to get requestToken after authorization")
+                ApiError(
+                    error_code="AUTHORIZATION_FAILED",
+                    message="Failed to get requestToken after authorization"
+                )
             )

472-477: Add missing error_code field.

The ApiError at line 475 is missing the required error_code field.

Apply this diff:

                 if not order_id or not status:
                     raise ApiException(
                         ApiError(
+                            error_code="BROKER_RESPONSE_INCOMPLETE",
                             message="Failed to place order: Missing order_id or status in response."
                         )
                     )

603-604: Add missing error_code field and validate required parameters.

The ApiError at line 604 is missing the required error_code field. Additionally, new_quantity from kwargs may be None, causing a validation error when constructing HDFCModifyOrderRequest.

Apply this diff:

         if not access_token:
-            raise ApiException(ApiError(message="No access token found in session."))
+            raise ApiException(
+                ApiError(
+                    error_code="UNAUTHORIZED",
+                    message="No access token found in session."
+                )
+            )
+
+        # Validate required parameters
+        if "new_quantity" not in kwargs or kwargs["new_quantity"] is None:
+            raise ApiException(
+                ApiError(
+                    error_code="INVALID_PARAMETERS",
+                    message="new_quantity is required for order modification"
+                )
+            )

374-378: Add missing error_code field.

The ApiError at line 376 is missing the required error_code field.

Apply this diff:

         if not access_token_data.accessToken:
             raise ApiException(
-                ApiError(message="Failed to get accessToken from HDFC API")
+                ApiError(
+                    error_code="ACCESS_TOKEN_FAILED",
+                    message="Failed to get accessToken from HDFC API"
+                )
             )

839-845: Add missing error_code fields.

The ApiError instances at lines 840 and 845 are missing the required error_code field.

Apply this diff:

         if not access_token:
-            raise ApiException(ApiError(message="No access token found in session."))
+            raise ApiException(
+                ApiError(
+                    error_code="UNAUTHORIZED",
+                    message="No access token found in session."
+                )
+            )

         login_id = session_data.get("loginId")

         if not login_id:
-            raise ApiException(ApiError(message="No login ID found in session."))
+            raise ApiException(
+                ApiError(
+                    error_code="INVALID_SESSION",
+                    message="No login ID found in session."
+                )
+            )

446-447: Add missing error_code field.

The ApiError at line 447 is missing the required error_code field.

Apply this diff:

         if not access_token:
-            raise ApiException(ApiError(message="No access token found in session."))
+            raise ApiException(
+                ApiError(
+                    error_code="UNAUTHORIZED",
+                    message="No access token found in session."
+                )
+            )

337-341: Add missing error_code field.

The ApiError at line 339 is missing the required error_code field. Per src/ordo/models/api/errors.py, error_code is mandatory.

Apply this diff:

         if not twofa_data.requestToken:
             raise ApiException(
-                ApiError(message="Failed to get requestToken after 2FA validation")
+                ApiError(
+                    error_code="2FA_VALIDATION_FAILED",
+                    message="Failed to get requestToken after 2FA validation"
+                )
             )

740-794: Critical: Fix indentation and add missing error_code.

Two critical issues:

  1. The ApiError at line 745 is missing the required error_code field.
  2. The for loop at lines 757-774 is incorrectly indented outside the try block, causing it to execute after the async client context closes.
  3. Pipeline failures indicate validation errors for missing required fields in HDFCTradeBookResponse.

Apply this diff:

         if not access_token:
-            raise ApiException(ApiError(message="No access token found in session."))
+            raise ApiException(
+                ApiError(
+                    error_code="UNAUTHORIZED",
+                    message="No access token found in session."
+                )
+            )

         url = f"{self.base_url}/trades?api_key={config.api_key}"
         headers = {
             "Authorization": f"Bearer {access_token}",
         }
         try:
             async with self._create_client() as client:
                 response = await client.get(url, headers=headers)
                 response.raise_for_status()
                 data = HDFCTradeBookResponse(**response.json())
                 trades = []
-            for item in data.data:
-                trades.append(
-                    Trade(
-                        trade_id=item.trade_id,
-                        order_id=item.order_id,
-                        exchange=item.exchange,
-                        product=ProductType(item.product.lower()),
-                        average_price=item.average_price,
-                        filled_quantity=item.filled_quantity,
-                        exchange_order_id=item.exchange_order_id,
-                        transaction_type=TransactionType(item.transaction_type.lower()),
-                        fill_timestamp=datetime.strptime(
-                            item.fill_timestamp, "%d/%m/%Y %H:%M:%S"
-                        ),
-                        security_id=item.security_id,
-                        company_name=item.company_name,
+                for item in data.data:
+                    trades.append(
+                        Trade(
+                            trade_id=item.trade_id,
+                            order_id=item.order_id,
+                            exchange=item.exchange,
+                            product=ProductType(item.product.upper()),
+                            average_price=item.average_price,
+                            filled_quantity=item.filled_quantity,
+                            exchange_order_id=item.exchange_order_id,
+                            transaction_type=TransactionType(item.transaction_type.upper()),
+                            fill_timestamp=datetime.strptime(
+                                item.fill_timestamp, "%d/%m/%Y %H:%M:%S"
+                            ),
+                            security_id=item.security_id,
+                            company_name=item.company_name,
+                        )
                     )
-                )
-            return trades
+                return trades
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 108f2cf and 8c64f7b.

⛔ Files ignored due to path filters (2)
  • poetry.lock is excluded by !**/*.lock
  • uv.lock is excluded by !**/*.lock
📒 Files selected for processing (6)
  • docs/stories/story-2.1.md (1 hunks)
  • pyproject.toml (1 hunks)
  • requirements.txt (1 hunks)
  • src/ordo/adapters/hdfc.py (1 hunks)
  • src/ordo/models/api/order.py (1 hunks)
  • src/ordo/models/api/user.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/ordo/adapters/hdfc.py (6)
src/ordo/adapters/base.py (11)
  • IBrokerAdapter (9-86)
  • initiate_login (15-19)
  • complete_login (22-26)
  • get_portfolio (29-33)
  • modify_order (36-42)
  • cancel_order (45-51)
  • get_order_book (54-58)
  • get_trade_book (61-65)
  • get_profile (68-72)
  • get_holdings (75-79)
  • get_positions (82-86)
src/ordo/models/api/errors.py (2)
  • ApiError (7-19)
  • ApiException (22-25)
src/ordo/models/api/portfolio.py (3)
  • Portfolio (29-42)
  • Holding (5-16)
  • Funds (19-26)
src/ordo/models/api/order.py (12)
  • Order (62-81)
  • Trade (84-103)
  • Position (106-112)
  • OrderResponse (115-117)
  • TransactionType (21-23)
  • OrderType (26-30)
  • ProductType (33-40)
  • OrderStatus (54-59)
  • ExchangeType (6-8)
  • InstrumentSegmentType (11-18)
  • ValidityType (43-46)
  • OptionType (49-51)
src/ordo/models/api/user.py (1)
  • Profile (4-9)
src/ordo/security/session.py (3)
  • SessionManager (6-36)
  • set_session (19-25)
  • get_session (27-36)
🪛 GitHub Actions: Python CI
src/ordo/adapters/hdfc.py

[error] 450-450: ValidationError during place_order: required fields security_id, instrument_segment, transaction_type, product, order_type, validity are missing in order_details when constructing HDFCPlaceOrderRequest.


[error] 452-452: Invalid order details: 6 validation errors for HDFCPlaceOrderRequest (security_id, instrument_segment, transaction_type, product, order_type, validity).


[error] 711-711: get_order_book failed: ValueError -> 'buy' is not a valid TransactionType while parsing order data (lowercase in API response).


[error] 733-733: ApiException: Failed to get order book from HDFC: 'buy' is not a valid TransactionType.


[error] 755-755: get_trade_book failed: ValidationError -> multiple missing required fields in HDFCTradeBookResponse data (e.g., client_id, exchange, product, etc.).


[error] 789-789: ApiException: Failed to get trade book from HDFC: 17 validation errors for HDFCTradeBookResponse (missing many required fields in data[].)


[error] 921-921: get_positions failed: ProductType('delivery') is not a valid ProductType value (case mismatch or unsupported value).


[warning] 1-1: Pydantic deprecation: Using extra keyword arguments on Field is deprecated in the used version; consider json_schema_extra instead.

🪛 markdownlint-cli2 (0.18.1)
docs/stories/story-2.1.md

35-35: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🔇 Additional comments (6)
pyproject.toml (1)

18-19: LGTM!

The additions of anyio and email-validator dependencies are necessary and properly versioned. The email-validator package enables the EmailStr field validation in the new Profile model, and anyio provides async utility support.

requirements.txt (1)

1-74: LGTM!

The dependencies are properly pinned for reproducible builds. Key versions align with the project requirements and learnings:

  • httpx==0.28.1 (latest stable with SSL fixes)
  • pydantic==2.11.9 (latest with performance improvements and backported fixes)
  • email-validator==2.3.0 (for EmailStr validation)
src/ordo/models/api/order.py (1)

1-117: LGTM!

The domain models are well-structured with:

  • Proper enum definitions for trading entities
  • Strong typing with Pydantic BaseModel
  • Descriptive Field annotations
  • ISO8601 timestamp semantics

These models provide a solid foundation for the broker adapter API surface.

src/ordo/models/api/user.py (1)

1-9: LGTM!

The Profile model is clean and well-structured:

  • Uses EmailStr for email validation (enabled by the email-validator dependency added in pyproject.toml)
  • Proper Field descriptions
  • Follows Pydantic best practices
docs/stories/story-2.1.md (1)

1-159: Documentation update looks good.

The story documentation comprehensively tracks the HDFC adapter implementation progress, including:

  • Completed acceptance criteria
  • QA review results with security and compliance checks
  • Refactoring notes for improved maintainability
  • Clear changelog of fixes
src/ordo/adapters/hdfc.py (1)

165-173: Field definitions consistently use json_schema_extra; no deprecated extra= found.

Comment thread docs/stories/story-2.1.md
- **External API:** HDFC Securities API documentation is at `https://developer.hdfcsec.com/`.
- **Complexity:** This adapter adds a second real broker, which will validate the robustness of the `IBrokerAdapter` interface.

### Testing

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

🧹 Nitpick | 🔵 Trivial

Fix markdown formatting.

Markdownlint flags a missing blank line before the heading at line 35.

Add a blank line before the heading:

 - **Complexity:** This adapter adds a second real broker, which will validate the robustness of the `IBrokerAdapter` interface.
+
 ### Testing
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
### Testing
**Complexity:** This adapter adds a second real broker, which will validate the robustness of the `IBrokerAdapter` interface.
### Testing
🧰 Tools
🪛 markdownlint-cli2 (0.18.1)

35-35: Headings should be surrounded by blank lines
Expected: 1; Actual: 0; Below

(MD022, blanks-around-headings)

🤖 Prompt for AI Agents
In docs/stories/story-2.1.md at line 35, there is a missing blank line before
the "### Testing" heading; add a single blank line above that heading so the
heading is separated from the preceding paragraph or content to satisfy
markdownlint and ensure proper Markdown rendering.

Comment thread src/ordo/adapters/hdfc.py Outdated

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 2

📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 8c64f7b and d8f39a2.

📒 Files selected for processing (2)
  • src/ordo/adapters/hdfc.py (1 hunks)
  • tests/adapters/test_hdfc.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (2)
tests/adapters/test_hdfc.py (4)
src/ordo/adapters/base.py (11)
  • IBrokerAdapter (9-86)
  • initiate_login (15-19)
  • complete_login (22-26)
  • get_portfolio (29-33)
  • modify_order (36-42)
  • cancel_order (45-51)
  • get_order_book (54-58)
  • get_trade_book (61-65)
  • get_profile (68-72)
  • get_holdings (75-79)
  • get_positions (82-86)
src/ordo/adapters/hdfc.py (12)
  • HDFCAdapter (239-946)
  • initiate_login (284-325)
  • complete_login (380-435)
  • get_portfolio (501-595)
  • place_order (437-499)
  • modify_order (597-647)
  • cancel_order (649-686)
  • get_order_book (688-738)
  • get_trade_book (740-794)
  • get_profile (796-833)
  • get_holdings (835-892)
  • get_positions (894-946)
src/ordo/models/api/errors.py (1)
  • ApiException (22-25)
src/ordo/security/session.py (2)
  • set_session (19-25)
  • get_session (27-36)
src/ordo/adapters/hdfc.py (6)
src/ordo/adapters/base.py (11)
  • IBrokerAdapter (9-86)
  • initiate_login (15-19)
  • complete_login (22-26)
  • get_portfolio (29-33)
  • modify_order (36-42)
  • cancel_order (45-51)
  • get_order_book (54-58)
  • get_trade_book (61-65)
  • get_profile (68-72)
  • get_holdings (75-79)
  • get_positions (82-86)
src/ordo/models/api/errors.py (2)
  • ApiError (7-19)
  • ApiException (22-25)
src/ordo/models/api/portfolio.py (3)
  • Portfolio (29-42)
  • Holding (5-16)
  • Funds (19-26)
src/ordo/models/api/order.py (12)
  • Order (62-81)
  • Trade (84-103)
  • Position (106-112)
  • OrderResponse (115-117)
  • TransactionType (21-23)
  • OrderType (26-30)
  • ProductType (33-40)
  • OrderStatus (54-59)
  • ExchangeType (6-8)
  • InstrumentSegmentType (11-18)
  • ValidityType (43-46)
  • OptionType (49-51)
src/ordo/models/api/user.py (1)
  • Profile (4-9)
src/ordo/security/session.py (3)
  • SessionManager (6-36)
  • set_session (19-25)
  • get_session (27-36)

Comment thread src/ordo/adapters/hdfc.py
Comment thread src/ordo/adapters/hdfc.py
This commit addresses several issues in the HDFC adapter:

- Adds the `error_code` parameter to all `ApiError` exceptions to provide more specific error information.
- Fixes a timestamp parsing bug in the `get_order_book` method by normalizing ISO strings ending in Z to the +00:00 UTC offset.
- Resolves a Pydantic V2 deprecation warning by replacing the `example` keyword argument with `json_schema_extra` in the `Profile` model.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Actionable comments posted: 3

♻️ Duplicate comments (1)
src/ordo/adapters/hdfc.py (1)

621-676: Consider adding explicit validation for required parameters.

The modify_order method constructs the Pydantic model directly with kwargs.get("new_quantity") (line 637), which will be None if not provided. While Pydantic validation will catch this, the error message may not be user-friendly. Consider explicit validation:

     if not access_token:
         raise ApiException(
             ApiError(
                 error_code="UNAUTHORIZED",
                 message="No access token found in session.",
             )
         )
+
+    if "new_quantity" not in kwargs or kwargs["new_quantity"] is None:
+        raise ApiException(
+            ApiError(
+                error_code="INVALID_PARAMETERS",
+                message="new_quantity is required for order modification"
+            )
+        )
📜 Review details

Configuration used: CodeRabbit UI

Review profile: ASSERTIVE

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d8f39a2 and 9259203.

📒 Files selected for processing (2)
  • src/ordo/adapters/hdfc.py (1 hunks)
  • src/ordo/models/api/user.py (1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
src/ordo/adapters/hdfc.py (7)
src/ordo/adapters/base.py (11)
  • IBrokerAdapter (9-86)
  • initiate_login (15-19)
  • complete_login (22-26)
  • get_portfolio (29-33)
  • modify_order (36-42)
  • cancel_order (45-51)
  • get_order_book (54-58)
  • get_trade_book (61-65)
  • get_profile (68-72)
  • get_holdings (75-79)
  • get_positions (82-86)
src/ordo/models/api/errors.py (2)
  • ApiError (7-19)
  • ApiException (22-25)
src/ordo/models/api/portfolio.py (3)
  • Portfolio (29-42)
  • Holding (5-16)
  • Funds (19-26)
src/ordo/models/api/order.py (12)
  • Order (62-81)
  • Trade (84-103)
  • Position (106-112)
  • OrderResponse (115-117)
  • TransactionType (21-23)
  • OrderType (26-30)
  • ProductType (33-40)
  • OrderStatus (54-59)
  • ExchangeType (6-8)
  • InstrumentSegmentType (11-18)
  • ValidityType (43-46)
  • OptionType (49-51)
src/ordo/models/api/user.py (1)
  • Profile (4-11)
src/ordo/security/session.py (3)
  • SessionManager (6-36)
  • set_session (19-25)
  • get_session (27-36)
src/ordo/adapters/fyers.py (10)
  • initiate_login (34-57)
  • complete_login (59-93)
  • get_portfolio (154-245)
  • modify_order (247-250)
  • cancel_order (252-253)
  • get_order_book (255-256)
  • get_trade_book (258-259)
  • get_profile (261-262)
  • get_holdings (264-265)
  • get_positions (267-268)
🔇 Additional comments (9)
src/ordo/adapters/hdfc.py (8)

1-237: LGTM! Comprehensive Pydantic models with proper validation.

The imports are well-organized and the Pydantic models comprehensively cover the HDFC API surface. Field descriptions are clear and the models follow consistent patterns.


239-282: LGTM! Proper httpx.AsyncClient lifecycle management.

The adapter correctly uses the _create_client() factory pattern with async context managers throughout, avoiding resource leaks. The session manager integration is clean.


284-444: LGTM! Well-structured multi-step authentication flow.

The login implementation properly handles HDFC's multi-step authentication (token → validate → 2FA → authorize → access token) with clear helper methods and comprehensive error handling. Session management is correctly implemented.


446-514: LGTM! Robust order placement with proper validation.

Order placement correctly validates credentials, uses Pydantic validation for request data, and includes comprehensive error handling. The response validation ensures required fields are present.


516-619: LGTM! Portfolio retrieval with documented limitations.

The implementation correctly fetches holdings and portfolio summary, transforms the data to standard models, and documents the API limitation (no day P&L data) inline. Error handling is comprehensive.


678-720: LGTM! Clean order cancellation implementation.

The cancel_order method correctly validates credentials, uses proper resource management, and handles errors comprehensively.


848-890: LGTM! Clean profile retrieval.

The get_profile method correctly fetches and transforms profile data with proper error handling.


892-958: LGTM! Holdings retrieval with documented limitation.

The method correctly validates session data, fetches holdings, and documents the API limitation (no day P&L) inline. Error handling is comprehensive.

src/ordo/models/api/user.py (1)

1-11: LGTM! Clean and well-documented Profile model.

The Profile model is straightforward with proper type validation (EmailStr), clear field descriptions, and a helpful example. Follows Pydantic best practices.

Comment thread src/ordo/adapters/hdfc.py
Comment on lines +722 to +785
async def get_order_book(self, session_data: Dict[str, Any]) -> List[Order]:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")

if not access_token:
raise ApiException(
ApiError(
error_code="UNAUTHORIZED",
message="No access token found in session.",
)
)

url = f"{self.base_url}/orders?api_key={config.api_key}"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
async with self._create_client() as client:
response = await client.get(url, headers=headers)
response.raise_for_status()
data = HDFCOrderBookResponse(**response.json())
orders = []
for item in data.data:
timestamp_str = item.order_timestamp
if timestamp_str and timestamp_str.endswith("Z"):
timestamp_str = timestamp_str.replace("Z", "+00:00")

orders.append(
Order(
order_id=item.order_id,
symbol=item.tradingsymbol,
status=OrderStatus(item.status),
transaction_type=TransactionType(item.transaction_type),
order_type=OrderType.MARKET, # HDFC does not provide order type in order book
product_type=ProductType(item.product),
quantity=item.quantity,
price=item.price,
timestamp=(
datetime.fromisoformat(timestamp_str)
if timestamp_str
else None
),
)
)
return orders
except httpx.HTTPStatusError as e:
response_content = self._get_response_json_or_text(e.response)
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_order_book: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": response_content,
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get order book from HDFC: {e}",
)
)

@coderabbitai coderabbitai Bot Oct 5, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Add case normalization for enum conversions.

The enum constructors at lines 753-756 may fail if the HDFC API returns values in unexpected case:

  • TransactionType expects uppercase ("BUY", "SELL") but API might return lowercase
  • ProductType expects uppercase values
  • OrderStatus expects lowercase values

Past pipeline failures indicated 'buy' is not a valid TransactionType. Add case normalization:

                 orders.append(
                     Order(
                         order_id=item.order_id,
                         symbol=item.tradingsymbol,
-                        status=OrderStatus(item.status),
-                        transaction_type=TransactionType(item.transaction_type),
+                        status=OrderStatus(item.status.lower()),
+                        transaction_type=TransactionType(item.transaction_type.upper()),
                         order_type=OrderType.MARKET,
-                        product_type=ProductType(item.product),
+                        product_type=ProductType(item.product.upper()),
                         quantity=item.quantity,
                         price=item.price,
                         timestamp=(
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def get_order_book(self, session_data: Dict[str, Any]) -> List[Order]:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")
if not access_token:
raise ApiException(
ApiError(
error_code="UNAUTHORIZED",
message="No access token found in session.",
)
)
url = f"{self.base_url}/orders?api_key={config.api_key}"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
async with self._create_client() as client:
response = await client.get(url, headers=headers)
response.raise_for_status()
data = HDFCOrderBookResponse(**response.json())
orders = []
for item in data.data:
timestamp_str = item.order_timestamp
if timestamp_str and timestamp_str.endswith("Z"):
timestamp_str = timestamp_str.replace("Z", "+00:00")
orders.append(
Order(
order_id=item.order_id,
symbol=item.tradingsymbol,
status=OrderStatus(item.status),
transaction_type=TransactionType(item.transaction_type),
order_type=OrderType.MARKET, # HDFC does not provide order type in order book
product_type=ProductType(item.product),
quantity=item.quantity,
price=item.price,
timestamp=(
datetime.fromisoformat(timestamp_str)
if timestamp_str
else None
),
)
)
return orders
except httpx.HTTPStatusError as e:
response_content = self._get_response_json_or_text(e.response)
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_order_book: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": response_content,
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get order book from HDFC: {e}",
)
)
async def get_order_book(self, session_data: Dict[str, Any]) -> List[Order]:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")
if not access_token:
raise ApiException(
ApiError(
error_code="UNAUTHORIZED",
message="No access token found in session.",
)
)
url = f"{self.base_url}/orders?api_key={config.api_key}"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
async with self._create_client() as client:
response = await client.get(url, headers=headers)
response.raise_for_status()
data = HDFCOrderBookResponse(**response.json())
orders = []
for item in data.data:
timestamp_str = item.order_timestamp
if timestamp_str and timestamp_str.endswith("Z"):
timestamp_str = timestamp_str.replace("Z", "+00:00")
orders.append(
Order(
order_id=item.order_id,
symbol=item.tradingsymbol,
status=OrderStatus(item.status.lower()),
transaction_type=TransactionType(item.transaction_type.upper()),
order_type=OrderType.MARKET, # HDFC does not provide order type in order book
product_type=ProductType(item.product.upper()),
quantity=item.quantity,
price=item.price,
timestamp=(
datetime.fromisoformat(timestamp_str)
if timestamp_str
else None
),
)
)
return orders
except httpx.HTTPStatusError as e:
response_content = self._get_response_json_or_text(e.response)
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_order_book: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": response_content,
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get order book from HDFC: {e}",
)
)
🤖 Prompt for AI Agents
In src/ordo/adapters/hdfc.py around lines 722 to 785, the enum conversions for
OrderStatus, TransactionType, and ProductType can fail due to casing differences
from the HDFC API; normalize the API strings before constructing enums by
trimming whitespace and converting TransactionType and ProductType inputs to
upper() and OrderStatus to lower(), handling None/empty values (e.g., use a safe
value or conditional) so enum constructors receive correctly-cased strings and
avoid raising validation errors.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Ignore this. The enums have been set as per HDFC Securities API documentation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@coderkrp, understood! I've noted that the enum values are correctly aligned with the HDFC Securities API documentation. I'll mark this as resolved.

Comment thread src/ordo/adapters/hdfc.py
Comment on lines +787 to +846
async def get_trade_book(self, session_data: Dict[str, Any]) -> List[Trade]:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")

if not access_token:
raise ApiException(
ApiError(
error_code="UNAUTHORIZED",
message="No access token found in session.",
)
)

url = f"{self.base_url}/trades?api_key={config.api_key}"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
async with self._create_client() as client:
response = await client.get(url, headers=headers)
response.raise_for_status()
data = HDFCTradeBookResponse(**response.json())
trades = []
for item in data.data:
trades.append(
Trade(
trade_id=item.trade_id,
order_id=item.order_id,
exchange=item.exchange,
product=ProductType(item.product),
average_price=item.average_price,
filled_quantity=item.filled_quantity,
exchange_order_id=item.exchange_order_id,
transaction_type=TransactionType(item.transaction_type),
fill_timestamp=datetime.strptime(
item.fill_timestamp, "%d/%m/%Y %H:%M:%S"
),
security_id=item.security_id,
company_name=item.company_name,
)
)
return trades
except httpx.HTTPStatusError as e:
response_content = self._get_response_json_or_text(e.response)
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_trade_book: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": response_content,
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get trade book from HDFC: {e}",
)
)

@coderabbitai coderabbitai Bot Oct 5, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Add case normalization for enum conversions.

Similar to get_order_book, the enum constructors need case normalization to handle API responses:

             for item in data.data:
                 trades.append(
                     Trade(
                         trade_id=item.trade_id,
                         order_id=item.order_id,
                         exchange=item.exchange,
-                        product=ProductType(item.product),
+                        product=ProductType(item.product.upper()),
                         average_price=item.average_price,
                         filled_quantity=item.filled_quantity,
                         exchange_order_id=item.exchange_order_id,
-                        transaction_type=TransactionType(item.transaction_type),
+                        transaction_type=TransactionType(item.transaction_type.upper()),
                         fill_timestamp=datetime.strptime(
                             item.fill_timestamp, "%d/%m/%Y %H:%M:%S"
                         ),
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def get_trade_book(self, session_data: Dict[str, Any]) -> List[Trade]:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")
if not access_token:
raise ApiException(
ApiError(
error_code="UNAUTHORIZED",
message="No access token found in session.",
)
)
url = f"{self.base_url}/trades?api_key={config.api_key}"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
async with self._create_client() as client:
response = await client.get(url, headers=headers)
response.raise_for_status()
data = HDFCTradeBookResponse(**response.json())
trades = []
for item in data.data:
trades.append(
Trade(
trade_id=item.trade_id,
order_id=item.order_id,
exchange=item.exchange,
product=ProductType(item.product),
average_price=item.average_price,
filled_quantity=item.filled_quantity,
exchange_order_id=item.exchange_order_id,
transaction_type=TransactionType(item.transaction_type),
fill_timestamp=datetime.strptime(
item.fill_timestamp, "%d/%m/%Y %H:%M:%S"
),
security_id=item.security_id,
company_name=item.company_name,
)
)
return trades
except httpx.HTTPStatusError as e:
response_content = self._get_response_json_or_text(e.response)
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_trade_book: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": response_content,
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get trade book from HDFC: {e}",
)
)
async def get_trade_book(self, session_data: Dict[str, Any]) -> List[Trade]:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")
if not access_token:
raise ApiException(
ApiError(
error_code="UNAUTHORIZED",
message="No access token found in session.",
)
)
url = f"{self.base_url}/trades?api_key={config.api_key}"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
async with self._create_client() as client:
response = await client.get(url, headers=headers)
response.raise_for_status()
data = HDFCTradeBookResponse(**response.json())
trades = []
for item in data.data:
trades.append(
Trade(
trade_id=item.trade_id,
order_id=item.order_id,
exchange=item.exchange,
product=ProductType(item.product.upper()),
average_price=item.average_price,
filled_quantity=item.filled_quantity,
exchange_order_id=item.exchange_order_id,
transaction_type=TransactionType(item.transaction_type.upper()),
fill_timestamp=datetime.strptime(
item.fill_timestamp, "%d/%m/%Y %H:%M:%S"
),
security_id=item.security_id,
company_name=item.company_name,
)
)
return trades
except httpx.HTTPStatusError as e:
response_content = self._get_response_json_or_text(e.response)
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_trade_book: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": response_content,
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get trade book from HDFC: {e}",
)
)
🤖 Prompt for AI Agents
In src/ordo/adapters/hdfc.py around lines 787 to 846, the enum constructions for
ProductType and TransactionType need case normalization like in get_order_book;
update the conversions to normalize the API strings (e.g., call .upper() or
.lower() consistently to match enum member names and guard against None) before
passing to ProductType(...) and TransactionType(...), so enum lookups won't fail
for differently-cased API responses and add a simple fallback or explicit
mapping if needed.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Ignore this. The enums have been set as per HDFC Securities API documentation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@coderkrp, understood! I've noted your decision. Since the enums are designed to match the HDFC Securities API documentation, the current implementation is correct as-is.

Comment thread src/ordo/adapters/hdfc.py
Comment on lines +960 to +1012
async def get_positions(self, session_data: Dict[str, Any]) -> List[Position]:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")

if not access_token:
raise ApiException(
ApiError(
error_code="UNAUTHORIZED",
message="No access token found in session.",
)
)

url = f"{self.base_url}/portfolio/overall_positions?api_key={config.api_key}"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
async with self._create_client() as client:
response = await client.get(url, headers=headers)
response.raise_for_status()
response_data = HDFCPositionsResponse(**response.json())
positions = []
for item in response_data.data.net:
positions.append(
Position(
symbol=item.security_id,
quantity=item.net_qty,
product_type=ProductType(item.product),
exchange=item.exchange,
instrument_type=item.instrument_segment,
realised_pnl=item.realised_pl_overall_position,
)
)
return positions
except httpx.HTTPStatusError as e:
response_content = self._get_response_json_or_text(e.response)
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_positions: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": response_content,
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get positions from HDFC: {e}",
)
)

@coderabbitai coderabbitai Bot Oct 5, 2025

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🔴 Critical

Critical: Add case normalization for ProductType.

The ProductType enum constructor needs case normalization:

                 for item in response_data.data.net:
                     positions.append(
                         Position(
                             symbol=item.security_id,
                             quantity=item.net_qty,
-                            product_type=ProductType(item.product),
+                            product_type=ProductType(item.product.upper()),
                             exchange=item.exchange,
                             instrument_type=item.instrument_segment,
                             realised_pnl=item.realised_pl_overall_position,
                         )
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
async def get_positions(self, session_data: Dict[str, Any]) -> List[Position]:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")
if not access_token:
raise ApiException(
ApiError(
error_code="UNAUTHORIZED",
message="No access token found in session.",
)
)
url = f"{self.base_url}/portfolio/overall_positions?api_key={config.api_key}"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
async with self._create_client() as client:
response = await client.get(url, headers=headers)
response.raise_for_status()
response_data = HDFCPositionsResponse(**response.json())
positions = []
for item in response_data.data.net:
positions.append(
Position(
symbol=item.security_id,
quantity=item.net_qty,
product_type=ProductType(item.product),
exchange=item.exchange,
instrument_type=item.instrument_segment,
realised_pnl=item.realised_pl_overall_position,
)
)
return positions
except httpx.HTTPStatusError as e:
response_content = self._get_response_json_or_text(e.response)
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_positions: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": response_content,
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get positions from HDFC: {e}",
)
)
async def get_positions(self, session_data: Dict[str, Any]) -> List[Position]:
config = HDFCConfig(**session_data["credentials"])
access_token = self.session_manager.get_session(config.api_key, "access_token")
if not access_token:
raise ApiException(
ApiError(
error_code="UNAUTHORIZED",
message="No access token found in session.",
)
)
url = f"{self.base_url}/portfolio/overall_positions?api_key={config.api_key}"
headers = {
"Authorization": f"Bearer {access_token}",
}
try:
async with self._create_client() as client:
response = await client.get(url, headers=headers)
response.raise_for_status()
response_data = HDFCPositionsResponse(**response.json())
positions = []
for item in response_data.data.net:
positions.append(
Position(
symbol=item.security_id,
quantity=item.net_qty,
product_type=ProductType(item.product.upper()),
exchange=item.exchange,
instrument_type=item.instrument_segment,
realised_pnl=item.realised_pl_overall_position,
)
)
return positions
except httpx.HTTPStatusError as e:
response_content = self._get_response_json_or_text(e.response)
raise ApiException(
ApiError(
error_code="BROKER_API_ERROR",
message=f"HDFC API error during get_positions: {e.response.text}",
details={
"status_code": e.response.status_code,
"response": response_content,
},
)
)
except Exception as e:
raise ApiException(
ApiError(
error_code="BROKER_REQUEST_FAILED",
message=f"Failed to get positions from HDFC: {e}",
)
)
🤖 Prompt for AI Agents
In src/ordo/adapters/hdfc.py around lines 960 to 1012, the code constructs
ProductType directly from item.product without normalizing case; update it to
normalize (trim whitespace and convert to the enum's expected case, e.g.,
upper()) before converting, and wrap the conversion in a safe path that catches
invalid values and raises an ApiException with a clear error_code/message (or
maps unknown values to a default/UNKNOWN enum entry if available). Ensure
trimming and case normalization are applied consistently where ProductType is
constructed and that any ValueError is translated to a Broker API error with
status/context.

Copy link
Copy Markdown
Owner Author

Choose a reason for hiding this comment

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

Ignore this. The enums have been set as per HDFC Securities API documentation.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@coderkrp, understood! I'll skip this suggestion since the enum values align with the HDFC Securities API documentation.

@coderkrp coderkrp merged commit 4209c22 into master Oct 5, 2025
2 checks passed
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.

Feature — HDFC Securities Adapter

1 participant