""" Investment Service — price fetching via yfinance, portfolio calculations. """ import logging from datetime import datetime from app.extensions import db from app.models.investment import Investment log = logging.getLogger(__name__) def fetch_price(ticker): """ Fetch latest price for a ticker via yfinance. Returns float or None on failure. """ if not ticker: return None try: import yfinance as yf t = yf.Ticker(ticker.upper()) hist = t.history(period='2d') if hist.empty: return None return float(hist['Close'].iloc[-1]) except Exception as e: log.warning(f'[investment] price fetch failed for {ticker}: {e}') return None def update_prices(investment_ids=None): """ Update current_price for all (or specified) investments with a ticker. Returns dict: {ticker: new_price} """ query = Investment.query.filter( Investment.ticker != None, Investment.ticker != '', Investment.is_active == True, ) if investment_ids: query = query.filter(Investment.id.in_(investment_ids)) investments = query.all() updated = {} for inv in investments: price = fetch_price(inv.ticker) if price is not None: inv.current_price = price inv.last_price_update = datetime.utcnow() updated[inv.ticker] = price if updated: try: db.session.commit() except Exception as e: db.session.rollback() log.error(f'[investment] DB commit failed: {e}') return updated def get_portfolio_summary(): """ Return portfolio-level aggregates across all active investments. """ investments = Investment.query.filter_by(is_active=True).all() total_cost = sum(i.total_cost for i in investments) total_value = sum(i.current_value for i in investments) total_gain = total_value - total_cost total_gain_pct = round((total_gain / total_cost) * 100, 2) if total_cost > 0 else 0 # Group by asset type for allocation chart type_totals = {} for inv in investments: t = inv.asset_type type_totals[t] = type_totals.get(t, 0) + inv.current_value allocation = [] for asset_type, value in sorted(type_totals.items(), key=lambda x: -x[1]): pct = round((value / total_value * 100), 1) if total_value > 0 else 0 allocation.append({ 'type': asset_type, 'value': value, 'pct': pct, 'color': ASSET_COLORS.get(asset_type, '#94a3b8'), }) return { 'investments': investments, 'total_cost': total_cost, 'total_value': total_value, 'total_gain': total_gain, 'total_gain_pct': total_gain_pct, 'allocation': allocation, 'count': len(investments), } # Consistent colors per asset type ASSET_COLORS = { 'stock': '#3b82f6', 'etf': '#06b6d4', 'crypto': '#f59e0b', 'real_estate': '#10b981', 'bond': '#8b5cf6', 'cash': '#64748b', 'other': '#ec4899', } ASSET_TYPE_LABELS = { 'stock': 'Stock', 'etf': 'ETF', 'crypto': 'Crypto', 'real_estate': 'Real Estate', 'bond': 'Bond', 'cash': 'Cash', 'other': 'Other', }