Skip to content

feat(holdings): add holdings management and investments account support - #22

Merged
eshaffer321 merged 3 commits into
eshaffer321:mainfrom
brewgator:pr/holdings-support
May 21, 2026
Merged

eshaffer321 merged 3 commits into
eshaffer321:mainfrom
brewgator:pr/holdings-support

Conversation

@brewgator

Copy link
Copy Markdown
Contributor

Summary

  • Add SearchSecurities(ctx, query, limit) — searches for securities by ticker or name, with configurable result limit (defaults to 10 if <= 0)
  • Add CreateHolding / DeleteHolding / UpdateHoldingQuantity for manual investment holdings
  • Add CreateHoldingByTicker convenience method (looks up security then creates holding)
  • Add CreateInvestmentsAccount for creating manual investment accounts with initial holdings
  • Simplify GetHoldings query to use the account-based holdings endpoint (simpler, more reliable)
  • Add holdings_test.go with tests for search, create, delete, and update operations

No auth or transaction changes are included — this PR is self-contained.

Split from #19 per review feedback.

Test plan

  • Existing tests pass (go test ./...)
  • SearchSecurities("BTC", 5) returns bitcoin-related securities
  • CreateHoldingByTicker(accountID, "BTC", 1.5) creates a BTC holding
  • DeleteHolding(holdingID) removes a holding
  • UpdateHoldingQuantity(accountID, holdingID, 2.0) updates quantity
  • CreateInvestmentsAccount creates account with initial holdings

brewgator added 2 commits May 20, 2026 10:33
- Add SearchSecurities with configurable limit parameter
- Add CreateHolding, DeleteHolding, UpdateHoldingQuantity
- Add CreateHoldingByTicker convenience method
- Add CreateInvestmentsAccount for manual investment accounts
- Simplify GetHoldings query to use account-based holdings endpoint
- Add holdings_test.go with tests for search, create, delete, update
- Add CreateInvestmentsAccountParams and InitialHolding types
Cover UpdateHoldingQuantity (success, not found, get error, delete error),
SearchSecurities (default limit, GraphQL error), CreateHolding (GraphQL error),
DeleteHolding (GraphQL error, with errors), and CreateHoldingByTicker (search error).
Comment thread pkg/monarch/holdings.go

// UpdateHoldingQuantity updates a holding's quantity by deleting and recreating it.
// The Monarch API does not support direct quantity updates on holdings.
func (s *accountService) UpdateHoldingQuantity(ctx context.Context, accountID, holdingID string, newQuantity float64) (*Holding, error) {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This update path is more destructive than it needs to be. Monarch exposes a direct updateHolding(input:) mutation, and UpdateHoldingInput includes quantity, so delete+recreate can lose the holding if recreate fails, changes the holding ID, and can drop metadata like cost basis/tax lots/history. I live-smoked this on a temporary manual account and the method did return a new holding ID. I think this should call updateHolding directly instead.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Good catch — I'll replace the delete+recreate with a direct updateHolding(input:) mutation call. This preserves the holding ID, cost basis, and history. I'll add the GraphQL mutation file and update the method.

Comment thread pkg/monarch/accounts.go
Name: name,
Quantity: edge.Node.Quantity,
Price: edge.Node.Price,
Price: price,

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

This only uses security.currentPrice, but the query also fetches holdings[].closingPrice and that value can be the only populated price. In the live smoke against my account, 4 returned holdings had Value > 0 but Price == 0 through this mapper. Please fall back to the sub-holding closing price, or derive from totalValue / quantity when quantity is non-zero, so Holding.Price stays useful.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

You're right. I'll add a fallback chain: use security.currentPrice first, then holdings[0].closingPrice, then derive from totalValue / quantity when quantity > 0.

Comment thread pkg/monarch/holdings.go
break
}
}
if securityID == "" {

Copy link
Copy Markdown
Owner

Choose a reason for hiding this comment

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

For a mutation helper, I think this should fail closed instead of silently using the first result when there is no exact ticker match. Search results are popularity-ranked and can include adjacent/ambiguous securities; creating the wrong holding would be hard for callers to notice. Could we require an exact ticker match, probably case-normalized, and return an error otherwise?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

Agreed — fail closed is safer here. I'll require a case-insensitive exact ticker match and return an error if none is found.

- UpdateHoldingQuantity: use direct updateHolding mutation instead of
  destructive delete+recreate. Preserves holding ID, cost basis, and history.
- GetHoldings price: add fallback chain — security.currentPrice, then
  holdings[0].closingPrice, then totalValue/quantity.
- CreateHoldingByTicker: require case-insensitive exact ticker match,
  return error instead of silently using first result.
- Update tests to match new behavior.
@codecov

codecov Bot commented May 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 58.16327% with 82 lines in your changes missing coverage. Please review.
✅ Project coverage is 48.35%. Comparing base (56cb2fd) to head (b6a4e73).
⚠️ Report is 1 commits behind head on main.

Files with missing lines Patch % Lines
pkg/monarch/accounts.go 0.00% 80 Missing ⚠️
pkg/monarch/holdings.go 98.27% 1 Missing and 1 partial ⚠️

❌ Your patch check has failed because the patch coverage (58.16%) is below the target coverage (80.00%). You can increase the patch coverage or adjust the target coverage.
❌ Your project check has failed because the head coverage (48.35%) is below the target coverage (70.00%). You can increase the head coverage or adjust the target coverage.

Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main      #22      +/-   ##
==========================================
+ Coverage   42.77%   48.35%   +5.57%     
==========================================
  Files          16       17       +1     
  Lines        1833     2031     +198     
==========================================
+ Hits          784      982     +198     
+ Misses        974      959      -15     
- Partials       75       90      +15     
Flag Coverage Δ
unittests 48.35% <58.16%> (+5.57%) ⬆️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
pkg/monarch/holdings.go 98.27% <98.27%> (ø)
pkg/monarch/accounts.go 24.76% <0.00%> (-3.77%) ⬇️

... and 3 files with indirect coverage changes


Continue to review full report in Codecov by Sentry.

Legend - Click here to learn more
Δ = absolute <relative> (impact), ø = not affected, ? = missing data
Powered by Codecov. Last update 56cb2fd...b6a4e73. Read the comment docs.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@eshaffer321
eshaffer321 merged commit ba1d9e7 into eshaffer321:main May 21, 2026
11 of 13 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.

2 participants