Mbonea commited on
Commit
ef59ff7
·
1 Parent(s): 6551f5e

Handle single asset portfolio analysis

Browse files
Files changed (1) hide show
  1. App/analysis/portfolio_optimizer.py +64 -2
App/analysis/portfolio_optimizer.py CHANGED
@@ -449,8 +449,70 @@ async def analyze_portfolio_growth_allocation(
449
  simulations: int = DEFAULT_SIMULATIONS,
450
  ) -> dict[str, Any]:
451
  assets = await _build_assets(portfolio_id)
452
- if len(assets) < 2:
453
- raise ValueError("At least two priced assets are required for allocation analysis")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
454
  return await asyncio.to_thread(
455
  _optimize_sync,
456
  [asset.__dict__ for asset in assets],
 
449
  simulations: int = DEFAULT_SIMULATIONS,
450
  ) -> dict[str, Any]:
451
  assets = await _build_assets(portfolio_id)
452
+ if not assets:
453
+ raise ValueError("At least one priced asset is required for allocation analysis")
454
+ if len(assets) == 1:
455
+ asset = assets[0]
456
+ current_stats = {
457
+ "expected_return": asset.expected_return,
458
+ "volatility": asset.volatility,
459
+ "sharpe": (
460
+ (asset.expected_return - DEFAULT_RISK_FREE_RATE) / asset.volatility
461
+ if asset.volatility > 0
462
+ else 0.0
463
+ ),
464
+ }
465
+ allocation = {
466
+ "asset_id": asset.asset_id,
467
+ "asset_type": asset.asset_type,
468
+ "symbol": asset.symbol,
469
+ "name": asset.name,
470
+ "current_weight": 1.0,
471
+ "suggested_weight": 1.0,
472
+ "difference": 0.0,
473
+ "current_value": round(asset.current_value, 2),
474
+ "suggested_value": round(asset.current_value, 2),
475
+ "rebalance_amount": 0.0,
476
+ "expected_return": round(asset.expected_return, 4),
477
+ "volatility": round(asset.volatility, 4),
478
+ "fee_rate": round(asset.fee_rate, 4),
479
+ "reason": "Only priced asset in portfolio; add other assets before optimization can rebalance.",
480
+ }
481
+ return {
482
+ "objective": "growth",
483
+ "total_value": round(asset.current_value, 2),
484
+ "risk_free_rate": DEFAULT_RISK_FREE_RATE,
485
+ "constraints": {
486
+ "max_asset_weight": 1.0,
487
+ "class_limits": GROWTH_CLASS_LIMITS,
488
+ "class_limits_apply_only_when_multiple_asset_classes_exist": True,
489
+ },
490
+ "current": {k: round(v, 4) for k, v in current_stats.items()},
491
+ "suggested": {k: round(v, 4) for k, v in current_stats.items()},
492
+ "estimated_rebalance_fees": 0.0,
493
+ "advisor_comment": {
494
+ "title": "Concentration warning",
495
+ "summary": (
496
+ f"Your portfolio is currently 100% allocated to {asset.symbol}. "
497
+ "That may have worked well, but the growth engine is concentrated in one security, "
498
+ "so the next improvement is diversification rather than reweighting."
499
+ ),
500
+ "key_actions": [
501
+ "Add at least one more priced asset before running a full optimizer.",
502
+ "For a growth portfolio, consider blending stocks with a liquid fund, ETF, or bond exposure to reduce single-name risk.",
503
+ "Keep fees in mind: adding a new DSE stock position has transaction costs, so diversify in meaningful ticket sizes.",
504
+ ],
505
+ "caution": (
506
+ "The model cannot calculate an optimized allocation from one asset alone. "
507
+ "It can only flag concentration risk and explain what data is missing."
508
+ ),
509
+ },
510
+ "allocations": [allocation],
511
+ "notes": [
512
+ "Single-asset portfolios receive a concentration analysis instead of a failed optimization.",
513
+ "Add at least two priced assets to enable portfolio rebalancing advice.",
514
+ ],
515
+ }
516
  return await asyncio.to_thread(
517
  _optimize_sync,
518
  [asset.__dict__ for asset in assets],