Spaces:
Sleeping
Sleeping
Handle single asset portfolio analysis
Browse files
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
|
| 453 |
-
raise ValueError("At least
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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],
|