-
Notifications
You must be signed in to change notification settings - Fork 0
feat(portfolio): Implement Fyers portfolio fetching and complete QA #34
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
a4c101f
6612e8a
cda1713
719e1e4
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,38 @@ | ||
| schema: 1 | ||
| story: '1.5' | ||
| story_title: 'Fyers Adapter - Portfolio' | ||
| gate: PASS | ||
| status_reason: 'The implementation is of high quality, well-tested, and meets all acceptance criteria. No major issues were found.' | ||
| reviewer: 'Quinn (Test Architect)' | ||
| updated: '2025-10-03T14:30:00Z' | ||
| quality_score: 100 | ||
| expires: '2025-10-17T14:30:00Z' | ||
|
|
||
| evidence: | ||
| tests_reviewed: 11 | ||
| risks_identified: 0 | ||
| trace: | ||
| ac_covered: [1, 2, 3, 4] | ||
| ac_gaps: [] | ||
|
|
||
| nfr_validation: | ||
| security: | ||
| status: PASS | ||
| notes: 'CSRF protection and hashing are properly implemented.' | ||
| performance: | ||
| status: PASS | ||
| notes: 'Asynchronous HTTP client is used for non-blocking I/O.' | ||
| reliability: | ||
| status: PASS | ||
| notes: 'API errors are handled and raised as standardized exceptions.' | ||
| maintainability: | ||
| status: PASS | ||
| notes: 'The code is clean, well-structured, and follows project standards.' | ||
|
|
||
| recommendations: | ||
| immediate: [] | ||
| future: | ||
| - action: 'Consider adding tests for edge cases like empty holdings or funds in the API response.' | ||
| refs: ['tests/adapters/test_fyers.py'] | ||
| - action: 'Confirm from Fyers API documentation if day_pnl is available.' | ||
| refs: ['src/ordo/adapters/fyers.py'] |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,4 @@ | ||
| class OrdoError(Exception): | ||
| """Base exception for all Ordo errors.""" | ||
|
|
||
| pass |
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,42 @@ | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from pydantic import BaseModel, Field | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| from typing import List | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class Holding(BaseModel): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| symbol: str = Field(..., description="Trading symbol of the instrument.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| quantity: int = Field(..., description="The quantity of the instrument held.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ltp: float = Field(..., description="Last Traded Price of the instrument.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| avg_price: float = Field( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ..., description="Average acquisition price of the instrument." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| pnl: float = Field(..., description="Profit and Loss for the holding.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| day_pnl: float = Field(..., description="Profit and Loss for the current day.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| value: float = Field( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ..., description="Current market value of the holding (quantity * ltp)." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+5
to
+17
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider validating or computing derived fields. The Consider one of these approaches:
Option 1: Make value and pnl computed fields (if broker doesn't return them): +from pydantic import computed_field
class Holding(BaseModel):
symbol: str = Field(..., description="Trading symbol of the instrument.")
quantity: int = Field(..., description="The quantity of the instrument held.")
ltp: float = Field(..., description="Last Traded Price of the instrument.")
avg_price: float = Field(
..., description="Average acquisition price of the instrument."
)
- pnl: float = Field(..., description="Profit and Loss for the holding.")
day_pnl: float = Field(..., description="Profit and Loss for the current day.")
- value: float = Field(
- ..., description="Current market value of the holding (quantity * ltp)."
- )
+
+ @computed_field
+ @property
+ def value(self) -> float:
+ """Current market value of the holding (quantity * ltp)."""
+ return self.quantity * self.ltp
+
+ @computed_field
+ @property
+ def pnl(self) -> float:
+ """Profit and Loss for the holding."""
+ return (self.ltp - self.avg_price) * self.quantityOption 2: Add validation (if broker returns these values and you want to verify): +from pydantic import model_validator
class Holding(BaseModel):
symbol: str = Field(..., description="Trading symbol of the instrument.")
quantity: int = Field(..., description="The quantity of the instrument held.")
ltp: float = Field(..., description="Last Traded Price of the instrument.")
avg_price: float = Field(
..., description="Average acquisition price of the instrument."
)
pnl: float = Field(..., description="Profit and Loss for the holding.")
day_pnl: float = Field(..., description="Profit and Loss for the current day.")
value: float = Field(
..., description="Current market value of the holding (quantity * ltp)."
)
+
+ @model_validator(mode='after')
+ def validate_computed_fields(self) -> 'Holding':
+ expected_value = self.quantity * self.ltp
+ expected_pnl = (self.ltp - self.avg_price) * self.quantity
+ if abs(self.value - expected_value) > 0.01:
+ raise ValueError(f"value {self.value} does not match quantity * ltp = {expected_value}")
+ if abs(self.pnl - expected_pnl) > 0.01:
+ raise ValueError(f"pnl {self.pnl} does not match (ltp - avg_price) * quantity = {expected_pnl}")
+ return self📝 Committable suggestion
Suggested change
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class Funds(BaseModel): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| available_balance: float = Field( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ..., description="The total available balance in the account." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| margin_used: float = Field(..., description="The total margin utilized for trades.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total_balance: float = Field( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ..., description="The total balance in the account (available + margin)." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+19
to
+27
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider computing or validating total_balance. The Consider:
Option 1: Make total_balance a computed field: +from pydantic import computed_field
class Funds(BaseModel):
available_balance: float = Field(
..., description="The total available balance in the account."
)
margin_used: float = Field(..., description="The total margin utilized for trades.")
- total_balance: float = Field(
- ..., description="The total balance in the account (available + margin)."
- )
+
+ @computed_field
+ @property
+ def total_balance(self) -> float:
+ """The total balance in the account (available + margin)."""
+ return self.available_balance + self.margin_usedOption 2: Add validation: +from pydantic import model_validator
class Funds(BaseModel):
available_balance: float = Field(
..., description="The total available balance in the account."
)
margin_used: float = Field(..., description="The total margin utilized for trades.")
total_balance: float = Field(
..., description="The total balance in the account (available + margin)."
)
+
+ @model_validator(mode='after')
+ def validate_total_balance(self) -> 'Funds':
+ expected_total = self.available_balance + self.margin_used
+ if abs(self.total_balance - expected_total) > 0.01:
+ raise ValueError(f"total_balance {self.total_balance} does not match available_balance + margin_used = {expected_total}")
+ return self🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| class Portfolio(BaseModel): | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| holdings: List[Holding] = Field( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ..., description="List of all holdings in the portfolio." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| funds: Funds = Field(..., description="Details of the funds in the account.") | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total_pnl: float = Field( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ..., description="Total Profit and Loss for the portfolio." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total_day_pnl: float = Field( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ..., description="Total Profit and Loss for the current day for the portfolio." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| total_value: float = Field( | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ..., description="Total current market value of the portfolio." | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
| ) | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
Comment on lines
+29
to
+42
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 🧹 Nitpick | 🔵 Trivial Consider computing aggregate fields from holdings. The Consider:
Option 1: Make aggregates computed fields: +from pydantic import computed_field
class Portfolio(BaseModel):
holdings: List[Holding] = Field(
..., description="List of all holdings in the portfolio."
)
funds: Funds = Field(..., description="Details of the funds in the account.")
- total_pnl: float = Field(
- ..., description="Total Profit and Loss for the portfolio."
- )
- total_day_pnl: float = Field(
- ..., description="Total Profit and Loss for the current day for the portfolio."
- )
- total_value: float = Field(
- ..., description="Total current market value of the portfolio."
- )
+
+ @computed_field
+ @property
+ def total_pnl(self) -> float:
+ """Total Profit and Loss for the portfolio."""
+ return sum(h.pnl for h in self.holdings)
+
+ @computed_field
+ @property
+ def total_day_pnl(self) -> float:
+ """Total Profit and Loss for the current day for the portfolio."""
+ return sum(h.day_pnl for h in self.holdings)
+
+ @computed_field
+ @property
+ def total_value(self) -> float:
+ """Total current market value of the portfolio."""
+ return sum(h.value for h in self.holdings)Option 2: Add validation: +from pydantic import model_validator
class Portfolio(BaseModel):
holdings: List[Holding] = Field(
..., description="List of all holdings in the portfolio."
)
funds: Funds = Field(..., description="Details of the funds in the account.")
total_pnl: float = Field(
..., description="Total Profit and Loss for the portfolio."
)
total_day_pnl: float = Field(
..., description="Total Profit and Loss for the current day for the portfolio."
)
total_value: float = Field(
..., description="Total current market value of the portfolio."
)
+
+ @model_validator(mode='after')
+ def validate_totals(self) -> 'Portfolio':
+ expected_pnl = sum(h.pnl for h in self.holdings)
+ expected_day_pnl = sum(h.day_pnl for h in self.holdings)
+ expected_value = sum(h.value for h in self.holdings)
+
+ if abs(self.total_pnl - expected_pnl) > 0.01:
+ raise ValueError(f"total_pnl {self.total_pnl} does not match sum of holdings pnl = {expected_pnl}")
+ if abs(self.total_day_pnl - expected_day_pnl) > 0.01:
+ raise ValueError(f"total_day_pnl {self.total_day_pnl} does not match sum of holdings day_pnl = {expected_day_pnl}")
+ if abs(self.total_value - expected_value) > 0.01:
+ raise ValueError(f"total_value {self.total_value} does not match sum of holdings value = {expected_value}")
+ return self📝 Committable suggestion
Suggested change
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Uh oh!
There was an error while loading. Please reload this page.