Investment portfolio charts, search fix, and holding creation fixes

- Add four portfolio charts: allocation donut by holding, allocation
  donut by asset type, cost basis vs current value bar, return % bar
- Fix asset search to use yf.Search() full-text instead of ticker-only
  lookup — name searches like "vanguard ftse all world" now work
- Fix holding creation double-quantity bug: holdings now created with
  quantity=0 so buy transaction is sole source of quantity/cost basis
- Add per-share / total price toggle in Add Holding modal with live
  calculated equivalent shown as you type
- Add ErrorBoundary in AppShell so render errors show a message instead
  of a blank page
- Fix donut charts using || instead of ?? when falling back from
  current_value to cost_basis_total (0 was not falling through ??)
- Allow HoldingCreate.quantity >= 0 (was gt=0) to support zero-init
- Fix error display for Pydantic v2 array-of-objects validation errors

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
megaproxy 2026-04-22 23:06:41 +00:00
parent 26e2a055db
commit cdc1e67321
9 changed files with 424 additions and 70 deletions

View file

@ -36,7 +36,7 @@ class AssetPricePoint(BaseModel):
class HoldingCreate(BaseModel):
account_id: uuid.UUID
asset_id: uuid.UUID
quantity: Decimal = Field(..., gt=0)
quantity: Decimal = Field(..., ge=0)
avg_cost_basis: Decimal = Field(..., ge=0)
currency: str = Field(default="GBP", min_length=3, max_length=10)

View file

@ -98,19 +98,26 @@ async def fetch_history(symbol: str, days: int = 365) -> list[dict]:
def search_yahoo(query: str) -> list[dict]:
try:
import yfinance as yf
ticker = yf.Ticker(query)
info = ticker.fast_info
price = getattr(info, "last_price", None)
if price:
return [{
"symbol": query.upper(),
"name": getattr(info, "long_name", None) or query.upper(),
"type": "stock",
"currency": (getattr(info, "currency", None) or "USD").upper(),
"exchange": getattr(info, "exchange", None),
results = yf.Search(query, max_results=10).quotes
out = []
for r in results:
symbol = r.get("symbol")
if not symbol:
continue
quote_type = (r.get("quoteType") or r.get("typeDisp") or "stock").lower()
type_map = {"etf": "etf", "equity": "stock", "cryptocurrency": "crypto",
"mutualfund": "fund", "bond": "bond"}
asset_type = type_map.get(quote_type, "stock")
out.append({
"symbol": symbol,
"name": r.get("longname") or r.get("shortname") or symbol,
"type": asset_type,
"currency": "USD", # updated on first price sync
"exchange": r.get("exchDisp") or r.get("exchange"),
"data_source": "yahoo_finance",
"data_source_id": None,
}]
})
return out
except Exception:
pass
return []