Spaces:
Sleeping
Sleeping
Upload 21 files
Browse files- app/__init__.py +0 -0
- app/api/__init__.py +0 -0
- app/api/endpoints/__init__.py +0 -0
- app/api/endpoints/ai.py +99 -0
- app/api/endpoints/funds.py +184 -0
- app/api/endpoints/goals.py +81 -0
- app/api/endpoints/market.py +17 -0
- app/api/endpoints/portfolio.py +72 -0
- app/config.py +25 -0
- app/models/__init__.py +0 -0
- app/models/ai_models.py +38 -0
- app/models/fund_models.py +62 -0
- app/models/goal_models.py +74 -0
- app/models/portfolio_models.py +62 -0
- app/services/__init__.py +0 -0
- app/services/ai_swarm.py +338 -0
- app/services/data_fetcher.py +101 -0
- app/services/portfolio_analyzer.py +296 -0
- app/services/sip_calculator.py +115 -0
- app/utils/__init__.py +0 -0
- app/utils/helpers.py +0 -0
app/__init__.py
ADDED
File without changes
|
app/api/__init__.py
ADDED
File without changes
|
app/api/endpoints/__init__.py
ADDED
File without changes
|
app/api/endpoints/ai.py
ADDED
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Depends, HTTPException
|
2 |
+
from app.services.ai_swarm import AISwarmService
|
3 |
+
from app.models.ai_models import (
|
4 |
+
AIAnalysisRequest, AIAnalysisResponse,
|
5 |
+
AnalysisFocus, MarketView, InvestmentHorizon
|
6 |
+
)
|
7 |
+
|
8 |
+
router = APIRouter()
|
9 |
+
|
10 |
+
@router.post("/analyze", response_model=AIAnalysisResponse)
|
11 |
+
async def get_ai_recommendations(request: AIAnalysisRequest):
|
12 |
+
"""
|
13 |
+
Get AI-powered investment recommendations based on client profile, portfolio, and goals
|
14 |
+
"""
|
15 |
+
try:
|
16 |
+
ai_service = AISwarmService()
|
17 |
+
result = ai_service.get_enhanced_analysis(request)
|
18 |
+
return result
|
19 |
+
except Exception as e:
|
20 |
+
raise HTTPException(status_code=500, detail=f"Error generating AI recommendations: {str(e)}")
|
21 |
+
|
22 |
+
@router.post("/fund-selection")
|
23 |
+
async def get_fund_selection_recommendations(
|
24 |
+
client_profile: dict,
|
25 |
+
goals_data: dict,
|
26 |
+
market_conditions: MarketView = MarketView.NEUTRAL,
|
27 |
+
investment_horizon: InvestmentHorizon = InvestmentHorizon.MEDIUM_TERM
|
28 |
+
):
|
29 |
+
"""
|
30 |
+
Get AI-powered fund selection recommendations
|
31 |
+
"""
|
32 |
+
try:
|
33 |
+
ai_service = AISwarmService()
|
34 |
+
|
35 |
+
request = AIAnalysisRequest(
|
36 |
+
client_profile=client_profile,
|
37 |
+
portfolio_data={},
|
38 |
+
goals_data=goals_data,
|
39 |
+
analysis_focus=[AnalysisFocus.FUND_SELECTION],
|
40 |
+
market_conditions=market_conditions,
|
41 |
+
investment_horizon=investment_horizon
|
42 |
+
)
|
43 |
+
|
44 |
+
result = ai_service.get_enhanced_analysis(request)
|
45 |
+
return result
|
46 |
+
except Exception as e:
|
47 |
+
raise HTTPException(status_code=500, detail=f"Error generating fund selection recommendations: {str(e)}")
|
48 |
+
|
49 |
+
@router.post("/risk-assessment")
|
50 |
+
async def get_risk_assessment(
|
51 |
+
client_profile: dict,
|
52 |
+
portfolio_data: dict,
|
53 |
+
market_conditions: MarketView = MarketView.NEUTRAL
|
54 |
+
):
|
55 |
+
"""
|
56 |
+
Get AI-powered risk assessment for a portfolio
|
57 |
+
"""
|
58 |
+
try:
|
59 |
+
ai_service = AISwarmService()
|
60 |
+
|
61 |
+
request = AIAnalysisRequest(
|
62 |
+
client_profile=client_profile,
|
63 |
+
portfolio_data=portfolio_data,
|
64 |
+
goals_data={},
|
65 |
+
analysis_focus=[AnalysisFocus.RISK_ASSESSMENT],
|
66 |
+
market_conditions=market_conditions,
|
67 |
+
investment_horizon=InvestmentHorizon.MEDIUM_TERM
|
68 |
+
)
|
69 |
+
|
70 |
+
result = ai_service.get_enhanced_analysis(request)
|
71 |
+
return result
|
72 |
+
except Exception as e:
|
73 |
+
raise HTTPException(status_code=500, detail=f"Error generating risk assessment: {str(e)}")
|
74 |
+
|
75 |
+
@router.post("/goal-planning")
|
76 |
+
async def get_goal_planning_recommendations(
|
77 |
+
client_profile: dict,
|
78 |
+
goals_data: dict,
|
79 |
+
investment_horizon: InvestmentHorizon = InvestmentHorizon.MEDIUM_TERM
|
80 |
+
):
|
81 |
+
"""
|
82 |
+
Get AI-powered goal planning recommendations
|
83 |
+
"""
|
84 |
+
try:
|
85 |
+
ai_service = AISwarmService()
|
86 |
+
|
87 |
+
request = AIAnalysisRequest(
|
88 |
+
client_profile=client_profile,
|
89 |
+
portfolio_data={},
|
90 |
+
goals_data=goals_data,
|
91 |
+
analysis_focus=[AnalysisFocus.GOAL_ALIGNMENT],
|
92 |
+
market_conditions=MarketView.NEUTRAL,
|
93 |
+
investment_horizon=investment_horizon
|
94 |
+
)
|
95 |
+
|
96 |
+
result = ai_service.get_enhanced_analysis(request)
|
97 |
+
return result
|
98 |
+
except Exception as e:
|
99 |
+
raise HTTPException(status_code=500, detail=f"Error generating goal planning recommendations: {str(e)}")
|
app/api/endpoints/funds.py
ADDED
@@ -0,0 +1,184 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
2 |
+
from typing import List, Dict, Any, Optional
|
3 |
+
from app.services.data_fetcher import MutualFundDataFetcher
|
4 |
+
from app.services.sip_calculator import SIPCalculator
|
5 |
+
from app.models.fund_models import (
|
6 |
+
FundNAVResponse, FundAnalysisRequest, FundAnalysisResponse
|
7 |
+
)
|
8 |
+
from app.models.goal_models import (
|
9 |
+
SIPCalculationRequest, SIPCalculationResponse,
|
10 |
+
RequiredSIPRequest, RequiredSIPResponse
|
11 |
+
)
|
12 |
+
|
13 |
+
# Popular mutual fund categories with scheme codes
|
14 |
+
POPULAR_FUNDS = {
|
15 |
+
'Large Cap Equity': {
|
16 |
+
'HDFC Top 100 Fund': '120503',
|
17 |
+
'ICICI Pru Bluechip Fund': '120505',
|
18 |
+
'SBI Bluechip Fund': '125497',
|
19 |
+
'Axis Bluechip Fund': '120503',
|
20 |
+
'Kotak Bluechip Fund': '118989'
|
21 |
+
},
|
22 |
+
'Mid Cap Equity': {
|
23 |
+
'HDFC Mid-Cap Opportunities Fund': '118551',
|
24 |
+
'ICICI Pru Mid Cap Fund': '120544',
|
25 |
+
'Kotak Emerging Equity Fund': '118999',
|
26 |
+
'SBI Magnum Mid Cap Fund': '100281',
|
27 |
+
'DSP Mid Cap Fund': '112618'
|
28 |
+
},
|
29 |
+
'Small Cap Equity': {
|
30 |
+
'SBI Small Cap Fund': '122639',
|
31 |
+
'DSP Small Cap Fund': '112618',
|
32 |
+
'HDFC Small Cap Fund': '118551',
|
33 |
+
'Axis Small Cap Fund': '125487',
|
34 |
+
'Kotak Small Cap Fund': '119028'
|
35 |
+
},
|
36 |
+
'ELSS (Tax Saving)': {
|
37 |
+
'Axis Long Term Equity Fund': '125494',
|
38 |
+
'HDFC Tax Saver': '100277',
|
39 |
+
'SBI Tax Saver': '125497',
|
40 |
+
'ICICI Pru ELSS Tax Saver': '120503',
|
41 |
+
'Kotak Tax Saver': '118989'
|
42 |
+
},
|
43 |
+
'Debt Funds': {
|
44 |
+
'HDFC Corporate Bond Fund': '101762',
|
45 |
+
'ICICI Pru Corporate Bond Fund': '120503',
|
46 |
+
'SBI Corporate Bond Fund': '125497',
|
47 |
+
'Kotak Corporate Bond Fund': '118989',
|
48 |
+
'Axis Corporate Debt Fund': '125494'
|
49 |
+
},
|
50 |
+
'Hybrid Funds': {
|
51 |
+
'HDFC Hybrid Equity Fund': '118551',
|
52 |
+
'ICICI Pru Balanced Advantage Fund': '120505',
|
53 |
+
'SBI Equity Hybrid Fund': '125497',
|
54 |
+
'Kotak Equity Hybrid Fund': '118999',
|
55 |
+
'Axis Hybrid Fund': '125494'
|
56 |
+
}
|
57 |
+
}
|
58 |
+
|
59 |
+
router = APIRouter()
|
60 |
+
|
61 |
+
@router.get("/schemes")
|
62 |
+
async def get_all_schemes():
|
63 |
+
"""
|
64 |
+
Get all available mutual fund schemes
|
65 |
+
"""
|
66 |
+
try:
|
67 |
+
fetcher = MutualFundDataFetcher()
|
68 |
+
schemes = fetcher.get_all_schemes()
|
69 |
+
return {"schemes": schemes}
|
70 |
+
except Exception as e:
|
71 |
+
raise HTTPException(status_code=500, detail=f"Error fetching schemes: {str(e)}")
|
72 |
+
|
73 |
+
@router.get("/nav/{scheme_code}", response_model=FundNAVResponse)
|
74 |
+
async def get_fund_nav_history(scheme_code: str):
|
75 |
+
"""
|
76 |
+
Get NAV history for a specific mutual fund scheme
|
77 |
+
"""
|
78 |
+
try:
|
79 |
+
fetcher = MutualFundDataFetcher()
|
80 |
+
nav_data, fund_meta = fetcher.get_fund_nav_history(scheme_code)
|
81 |
+
|
82 |
+
return FundNAVResponse(
|
83 |
+
meta=fund_meta,
|
84 |
+
data=nav_data.to_dict('records')
|
85 |
+
)
|
86 |
+
except Exception as e:
|
87 |
+
raise HTTPException(status_code=500, detail=f"Error fetching NAV data: {str(e)}")
|
88 |
+
|
89 |
+
@router.get("/popular")
|
90 |
+
async def get_popular_funds():
|
91 |
+
"""
|
92 |
+
Get list of popular mutual funds by category
|
93 |
+
"""
|
94 |
+
return POPULAR_FUNDS
|
95 |
+
|
96 |
+
@router.post("/sip-calculate", response_model=SIPCalculationResponse)
|
97 |
+
async def calculate_sip(request: SIPCalculationRequest,
|
98 |
+
include_yearly_breakdown: bool = Query(False, description="Include yearly breakdown in response")):
|
99 |
+
"""
|
100 |
+
Calculate SIP maturity amount based on monthly investment, expected return, and time period
|
101 |
+
"""
|
102 |
+
try:
|
103 |
+
calculator = SIPCalculator()
|
104 |
+
result = calculator.get_sip_calculation(request, include_yearly_breakdown)
|
105 |
+
return result
|
106 |
+
except Exception as e:
|
107 |
+
raise HTTPException(status_code=500, detail=f"Error calculating SIP: {str(e)}")
|
108 |
+
|
109 |
+
@router.post("/required-sip", response_model=RequiredSIPResponse)
|
110 |
+
async def calculate_required_sip(request: RequiredSIPRequest):
|
111 |
+
"""
|
112 |
+
Calculate required SIP amount to reach a target amount
|
113 |
+
"""
|
114 |
+
try:
|
115 |
+
calculator = SIPCalculator()
|
116 |
+
result = calculator.get_required_sip(request)
|
117 |
+
return result
|
118 |
+
except Exception as e:
|
119 |
+
raise HTTPException(status_code=500, detail=f"Error calculating required SIP: {str(e)}")
|
120 |
+
|
121 |
+
@router.post("/analyze", response_model=FundAnalysisResponse)
|
122 |
+
async def analyze_funds(request: FundAnalysisRequest):
|
123 |
+
"""
|
124 |
+
Analyze performance of selected mutual funds over a specified period
|
125 |
+
"""
|
126 |
+
try:
|
127 |
+
fetcher = MutualFundDataFetcher()
|
128 |
+
calculator = SIPCalculator()
|
129 |
+
|
130 |
+
analysis_results = []
|
131 |
+
|
132 |
+
for fund_name in request.fund_names:
|
133 |
+
# Find scheme code for the fund
|
134 |
+
scheme_code = None
|
135 |
+
fund_category = None
|
136 |
+
|
137 |
+
for category, funds in POPULAR_FUNDS.items():
|
138 |
+
if fund_name in funds:
|
139 |
+
scheme_code = funds[fund_name]
|
140 |
+
fund_category = category
|
141 |
+
break
|
142 |
+
|
143 |
+
if not scheme_code:
|
144 |
+
continue
|
145 |
+
|
146 |
+
# Fetch NAV data
|
147 |
+
nav_data, fund_meta = fetcher.get_fund_nav_history(scheme_code)
|
148 |
+
|
149 |
+
if not nav_data.empty:
|
150 |
+
returns_data = calculator.calculate_fund_returns(
|
151 |
+
nav_data, request.investment_amount, request.start_date, request.end_date
|
152 |
+
)
|
153 |
+
|
154 |
+
if returns_data:
|
155 |
+
analysis_results.append({
|
156 |
+
'fund_name': fund_name,
|
157 |
+
'category': fund_category,
|
158 |
+
'scheme_code': scheme_code,
|
159 |
+
'fund_house': fund_meta.fund_house,
|
160 |
+
'returns_data': returns_data,
|
161 |
+
'nav_data': nav_data.to_dict('records'),
|
162 |
+
'fund_meta': fund_meta.dict()
|
163 |
+
})
|
164 |
+
|
165 |
+
# Prepare comparison data
|
166 |
+
comparison_data = []
|
167 |
+
for result in analysis_results:
|
168 |
+
returns = result['returns_data']
|
169 |
+
comparison_data.append({
|
170 |
+
'Fund Name': result['fund_name'],
|
171 |
+
'Category': result['category'],
|
172 |
+
'Fund House': result['fund_house'],
|
173 |
+
'Investment': f"₹{returns['investment_amount']:,.0f}",
|
174 |
+
'Current Value': f"₹{returns['final_value']:,.0f}",
|
175 |
+
'Total Return': f"{returns['total_return']:.2f}%",
|
176 |
+
'Absolute Gain': f"₹{returns['final_value'] - returns['investment_amount']:,.0f}"
|
177 |
+
})
|
178 |
+
|
179 |
+
return FundAnalysisResponse(
|
180 |
+
results=analysis_results,
|
181 |
+
comparison_data=comparison_data
|
182 |
+
)
|
183 |
+
except Exception as e:
|
184 |
+
raise HTTPException(status_code=500, detail=f"Error analyzing funds: {str(e)}")
|
app/api/endpoints/goals.py
ADDED
@@ -0,0 +1,81 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Depends, HTTPException
|
2 |
+
from typing import List, Dict, Any
|
3 |
+
from app.models.goal_models import (
|
4 |
+
ClientProfile, InvestmentGoal, GoalsDashboard,
|
5 |
+
SIPCalculationRequest, SIPCalculationResponse,
|
6 |
+
RequiredSIPRequest, RequiredSIPResponse, GoalsDashboardRequest
|
7 |
+
)
|
8 |
+
from app.services.sip_calculator import SIPCalculator
|
9 |
+
|
10 |
+
router = APIRouter()
|
11 |
+
|
12 |
+
@router.post("/dashboard", response_model=GoalsDashboard)
|
13 |
+
async def get_goals_dashboard(request: GoalsDashboardRequest):
|
14 |
+
"""
|
15 |
+
Calculate goals dashboard metrics including total required SIP, shortfall/surplus
|
16 |
+
"""
|
17 |
+
try:
|
18 |
+
total_required_sip = sum(goal.required_sip for goal in request.goals)
|
19 |
+
shortfall = max(0, total_required_sip - request.monthly_savings)
|
20 |
+
surplus = max(0, request.monthly_savings - total_required_sip)
|
21 |
+
|
22 |
+
return GoalsDashboard(
|
23 |
+
goals=request.goals,
|
24 |
+
total_required_sip=total_required_sip,
|
25 |
+
monthly_savings=request.monthly_savings,
|
26 |
+
shortfall=shortfall,
|
27 |
+
surplus=surplus
|
28 |
+
)
|
29 |
+
except Exception as e:
|
30 |
+
raise HTTPException(status_code=500, detail=f"Error calculating goals dashboard: {str(e)}")
|
31 |
+
|
32 |
+
@router.post("/calculate-sip", response_model=SIPCalculationResponse)
|
33 |
+
async def calculate_sip(
|
34 |
+
request: SIPCalculationRequest,
|
35 |
+
include_yearly_breakdown: bool = False
|
36 |
+
):
|
37 |
+
"""
|
38 |
+
Calculate SIP maturity amount based on monthly investment, expected return, and time period
|
39 |
+
"""
|
40 |
+
try:
|
41 |
+
calculator = SIPCalculator()
|
42 |
+
result = calculator.get_sip_calculation(request, include_yearly_breakdown)
|
43 |
+
return result
|
44 |
+
except Exception as e:
|
45 |
+
raise HTTPException(status_code=500, detail=f"Error calculating SIP: {str(e)}")
|
46 |
+
|
47 |
+
@router.post("/required-sip", response_model=RequiredSIPResponse)
|
48 |
+
async def calculate_required_sip(request: RequiredSIPRequest):
|
49 |
+
"""
|
50 |
+
Calculate required SIP amount to reach a target amount
|
51 |
+
"""
|
52 |
+
try:
|
53 |
+
calculator = SIPCalculator()
|
54 |
+
result = calculator.get_required_sip(request)
|
55 |
+
return result
|
56 |
+
except Exception as e:
|
57 |
+
raise HTTPException(status_code=500, detail=f"Error calculating required SIP: {str(e)}")
|
58 |
+
|
59 |
+
@router.post("/inflation-adjusted")
|
60 |
+
async def calculate_inflation_adjusted_amount(
|
61 |
+
target_amount: float,
|
62 |
+
years: int,
|
63 |
+
expected_inflation: float
|
64 |
+
):
|
65 |
+
"""
|
66 |
+
Calculate inflation-adjusted target amount for a goal
|
67 |
+
"""
|
68 |
+
try:
|
69 |
+
inflation_adjusted_amount = target_amount * ((1 + expected_inflation/100) ** years)
|
70 |
+
calculator = SIPCalculator()
|
71 |
+
required_sip = calculator.calculate_required_sip(inflation_adjusted_amount, years, 12)
|
72 |
+
|
73 |
+
return {
|
74 |
+
"original_amount": target_amount,
|
75 |
+
"inflation_adjusted_amount": inflation_adjusted_amount,
|
76 |
+
"required_sip": required_sip,
|
77 |
+
"years": years,
|
78 |
+
"expected_inflation": expected_inflation
|
79 |
+
}
|
80 |
+
except Exception as e:
|
81 |
+
raise HTTPException(status_code=500, detail=f"Error calculating inflation-adjusted amount: {str(e)}")
|
app/api/endpoints/market.py
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Depends, HTTPException
|
2 |
+
from app.services.data_fetcher import MutualFundDataFetcher
|
3 |
+
from app.models.fund_models import MarketIndicesResponse
|
4 |
+
|
5 |
+
router = APIRouter()
|
6 |
+
|
7 |
+
@router.get("/indices", response_model=MarketIndicesResponse)
|
8 |
+
async def get_market_indices():
|
9 |
+
"""
|
10 |
+
Get current market indices data including Nifty 50, Sensex, etc.
|
11 |
+
"""
|
12 |
+
try:
|
13 |
+
fetcher = MutualFundDataFetcher()
|
14 |
+
indices_data = fetcher.get_market_indices()
|
15 |
+
return indices_data
|
16 |
+
except Exception as e:
|
17 |
+
raise HTTPException(status_code=500, detail=f"Error fetching market indices: {str(e)}")
|
app/api/endpoints/portfolio.py
ADDED
@@ -0,0 +1,72 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from fastapi import APIRouter, Depends, HTTPException, Query
|
2 |
+
from typing import Dict, Any, List
|
3 |
+
from app.services.portfolio_analyzer import PortfolioAnalyzer
|
4 |
+
from app.models.portfolio_models import (
|
5 |
+
Portfolio, PortfolioMetrics, PortfolioTemplate,
|
6 |
+
RebalanceAnalysis, PerformanceReport
|
7 |
+
)
|
8 |
+
|
9 |
+
router = APIRouter()
|
10 |
+
|
11 |
+
@router.post("/metrics", response_model=PortfolioMetrics)
|
12 |
+
async def calculate_portfolio_metrics(portfolio: Portfolio):
|
13 |
+
"""
|
14 |
+
Calculate portfolio-level metrics including total value, gains, and category allocation
|
15 |
+
"""
|
16 |
+
try:
|
17 |
+
analyzer = PortfolioAnalyzer()
|
18 |
+
metrics = analyzer.calculate_portfolio_metrics(portfolio.holdings)
|
19 |
+
return metrics
|
20 |
+
except Exception as e:
|
21 |
+
raise HTTPException(status_code=500, detail=f"Error calculating portfolio metrics: {str(e)}")
|
22 |
+
|
23 |
+
@router.post("/rebalance", response_model=RebalanceAnalysis)
|
24 |
+
async def generate_rebalance_analysis(portfolio: Portfolio):
|
25 |
+
"""
|
26 |
+
Generate detailed rebalancing analysis and recommendations for a portfolio
|
27 |
+
"""
|
28 |
+
try:
|
29 |
+
analyzer = PortfolioAnalyzer()
|
30 |
+
rebalance_analysis = analyzer.generate_rebalance_analysis(portfolio.holdings)
|
31 |
+
return rebalance_analysis
|
32 |
+
except Exception as e:
|
33 |
+
raise HTTPException(status_code=500, detail=f"Error generating rebalance analysis: {str(e)}")
|
34 |
+
|
35 |
+
@router.post("/performance", response_model=PerformanceReport)
|
36 |
+
async def generate_performance_report(portfolio: Portfolio):
|
37 |
+
"""
|
38 |
+
Generate comprehensive performance report for a portfolio
|
39 |
+
"""
|
40 |
+
try:
|
41 |
+
analyzer = PortfolioAnalyzer()
|
42 |
+
performance_report = analyzer.generate_performance_report(portfolio.holdings)
|
43 |
+
return performance_report
|
44 |
+
except Exception as e:
|
45 |
+
raise HTTPException(status_code=500, detail=f"Error generating performance report: {str(e)}")
|
46 |
+
|
47 |
+
@router.get("/template/{template}")
|
48 |
+
async def get_portfolio_template(template: PortfolioTemplate):
|
49 |
+
"""
|
50 |
+
Get a predefined portfolio template (Conservative, Balanced, Aggressive, Custom Sample)
|
51 |
+
"""
|
52 |
+
try:
|
53 |
+
analyzer = PortfolioAnalyzer()
|
54 |
+
template_portfolio = analyzer.get_template_portfolio(template)
|
55 |
+
return {"template": template_portfolio}
|
56 |
+
except Exception as e:
|
57 |
+
raise HTTPException(status_code=500, detail=f"Error getting portfolio template: {str(e)}")
|
58 |
+
|
59 |
+
@router.get("/templates")
|
60 |
+
async def get_all_portfolio_templates():
|
61 |
+
"""
|
62 |
+
Get all available portfolio templates
|
63 |
+
"""
|
64 |
+
try:
|
65 |
+
templates = {}
|
66 |
+
for template in PortfolioTemplate:
|
67 |
+
analyzer = PortfolioAnalyzer()
|
68 |
+
template_portfolio = analyzer.get_template_portfolio(template)
|
69 |
+
templates[template.value] = template_portfolio
|
70 |
+
return templates
|
71 |
+
except Exception as e:
|
72 |
+
raise HTTPException(status_code=500, detail=f"Error getting portfolio templates: {str(e)}")
|
app/config.py
ADDED
@@ -0,0 +1,25 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import os
|
2 |
+
from pydantic_settings import BaseSettings
|
3 |
+
|
4 |
+
class Settings(BaseSettings):
|
5 |
+
# API Configuration
|
6 |
+
API_V1_STR: str = "/api/v1"
|
7 |
+
PROJECT_NAME: str = "Mutual Fund Investment API"
|
8 |
+
|
9 |
+
# External API Keys
|
10 |
+
SWARMS_API_KEY: str = os.getenv("SWARMS_API_KEY", "")
|
11 |
+
SWARMS_BASE_URL: str = "https://api.swarms.world"
|
12 |
+
|
13 |
+
# Mutual Fund API
|
14 |
+
MFAPI_BASE_URL: str = "https://api.mfapi.in/mf"
|
15 |
+
|
16 |
+
# CORS Configuration
|
17 |
+
BACKEND_CORS_ORIGINS: list[str] = ["http://localhost:8501", "http://localhost:3000"]
|
18 |
+
|
19 |
+
# Cache TTL (in seconds)
|
20 |
+
CACHE_TTL: int = 3600
|
21 |
+
|
22 |
+
class Config:
|
23 |
+
env_file = ".env"
|
24 |
+
|
25 |
+
settings = Settings()
|
app/models/__init__.py
ADDED
File without changes
|
app/models/ai_models.py
ADDED
@@ -0,0 +1,38 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pydantic import BaseModel, Field
|
2 |
+
from typing import List, Dict, Optional, Any
|
3 |
+
from enum import Enum
|
4 |
+
|
5 |
+
class AnalysisFocus(str, Enum):
|
6 |
+
FUND_SELECTION = "Fund Selection"
|
7 |
+
RISK_ASSESSMENT = "Risk Assessment"
|
8 |
+
GOAL_ALIGNMENT = "Goal Alignment"
|
9 |
+
TAX_OPTIMIZATION = "Tax Optimization"
|
10 |
+
REBALANCING = "Rebalancing"
|
11 |
+
|
12 |
+
class MarketView(str, Enum):
|
13 |
+
BULLISH = "Bullish"
|
14 |
+
NEUTRAL = "Neutral"
|
15 |
+
BEARISH = "Bearish"
|
16 |
+
VOLATILE = "Volatile"
|
17 |
+
|
18 |
+
class InvestmentHorizon(str, Enum):
|
19 |
+
SHORT_TERM = "Short Term (1-3 years)"
|
20 |
+
MEDIUM_TERM = "Medium Term (3-7 years)"
|
21 |
+
LONG_TERM = "Long Term (7+ years)"
|
22 |
+
|
23 |
+
class AIAnalysisRequest(BaseModel):
|
24 |
+
client_profile: Dict[str, Any]
|
25 |
+
portfolio_data: Dict[str, Any]
|
26 |
+
goals_data: Dict[str, Any]
|
27 |
+
analysis_focus: List[AnalysisFocus]
|
28 |
+
market_conditions: MarketView
|
29 |
+
investment_horizon: InvestmentHorizon
|
30 |
+
|
31 |
+
class AISwarmAgent(BaseModel):
|
32 |
+
agent_name: str
|
33 |
+
content: Optional[str] = None
|
34 |
+
|
35 |
+
class AIAnalysisResponse(BaseModel):
|
36 |
+
success: bool
|
37 |
+
error: Optional[str] = None
|
38 |
+
output: Optional[List[AISwarmAgent]] = None
|
app/models/fund_models.py
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pydantic import BaseModel, Field, field_validator
|
2 |
+
from typing import List, Dict, Optional, Any
|
3 |
+
from datetime import datetime
|
4 |
+
|
5 |
+
class FundMeta(BaseModel):
|
6 |
+
fund_house: Optional[str] = None
|
7 |
+
scheme_type: Optional[str] = None
|
8 |
+
scheme_category: Optional[str] = None
|
9 |
+
scheme_code: Optional[str] = None
|
10 |
+
|
11 |
+
@field_validator('scheme_code', mode='before')
|
12 |
+
@classmethod
|
13 |
+
def convert_scheme_code_to_str(cls, v):
|
14 |
+
if v is not None:
|
15 |
+
return str(v)
|
16 |
+
return v
|
17 |
+
|
18 |
+
class NAVData(BaseModel):
|
19 |
+
date: datetime
|
20 |
+
nav: float
|
21 |
+
|
22 |
+
class FundNAVResponse(BaseModel):
|
23 |
+
meta: FundMeta
|
24 |
+
data: List[NAVData]
|
25 |
+
|
26 |
+
class MarketIndex(BaseModel):
|
27 |
+
name: str
|
28 |
+
symbol: str
|
29 |
+
current_price: float
|
30 |
+
change: float
|
31 |
+
change_pct: float
|
32 |
+
|
33 |
+
class MarketIndicesResponse(BaseModel):
|
34 |
+
indices: List[MarketIndex]
|
35 |
+
last_updated: datetime
|
36 |
+
|
37 |
+
class FundAnalysisRequest(BaseModel):
|
38 |
+
fund_names: List[str]
|
39 |
+
investment_amount: float = Field(gt=0)
|
40 |
+
start_date: datetime
|
41 |
+
end_date: datetime
|
42 |
+
|
43 |
+
class FundReturn(BaseModel):
|
44 |
+
start_nav: float
|
45 |
+
end_nav: float
|
46 |
+
units: float
|
47 |
+
final_value: float
|
48 |
+
total_return: float
|
49 |
+
investment_amount: float
|
50 |
+
|
51 |
+
class FundAnalysisResult(BaseModel):
|
52 |
+
fund_name: str
|
53 |
+
category: str
|
54 |
+
scheme_code: str
|
55 |
+
fund_house: str
|
56 |
+
returns_data: FundReturn
|
57 |
+
nav_data: List[NAVData]
|
58 |
+
fund_meta: FundMeta
|
59 |
+
|
60 |
+
class FundAnalysisResponse(BaseModel):
|
61 |
+
results: List[FundAnalysisResult]
|
62 |
+
comparison_data: List[Dict[str, Any]]
|
app/models/goal_models.py
ADDED
@@ -0,0 +1,74 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pydantic import BaseModel, Field
|
2 |
+
from typing import List, Dict, Optional, Any
|
3 |
+
from datetime import datetime
|
4 |
+
from enum import Enum
|
5 |
+
|
6 |
+
class RiskTolerance(str, Enum):
|
7 |
+
CONSERVATIVE = "Conservative"
|
8 |
+
MODERATE = "Moderate"
|
9 |
+
AGGRESSIVE = "Aggressive"
|
10 |
+
|
11 |
+
class InvestmentExperience(str, Enum):
|
12 |
+
BEGINNER = "Beginner"
|
13 |
+
INTERMEDIATE = "Intermediate"
|
14 |
+
ADVANCED = "Advanced"
|
15 |
+
|
16 |
+
class TaxBracket(str, Enum):
|
17 |
+
TEN_PERCENT = "10%"
|
18 |
+
TWENTY_PERCENT = "20%"
|
19 |
+
THIRTY_PERCENT = "30%"
|
20 |
+
|
21 |
+
class PriorityLevel(str, Enum):
|
22 |
+
LOW = "Low"
|
23 |
+
MEDIUM = "Medium"
|
24 |
+
HIGH = "High"
|
25 |
+
CRITICAL = "Critical"
|
26 |
+
|
27 |
+
class ClientProfile(BaseModel):
|
28 |
+
age: int = Field(ge=18, le=100)
|
29 |
+
monthly_income: float = Field(gt=0)
|
30 |
+
risk_tolerance: RiskTolerance
|
31 |
+
investment_experience: InvestmentExperience
|
32 |
+
tax_bracket: TaxBracket
|
33 |
+
monthly_savings: float = Field(gt=0)
|
34 |
+
|
35 |
+
class InvestmentGoal(BaseModel):
|
36 |
+
name: str
|
37 |
+
amount: float = Field(gt=0)
|
38 |
+
inflation_adjusted_amount: float = Field(gt=0)
|
39 |
+
years: int = Field(ge=1, le=40)
|
40 |
+
priority: PriorityLevel
|
41 |
+
required_sip: float = Field(ge=0)
|
42 |
+
expected_inflation: float = Field(ge=0, le=20)
|
43 |
+
id: int
|
44 |
+
|
45 |
+
class GoalsDashboard(BaseModel):
|
46 |
+
goals: List[InvestmentGoal]
|
47 |
+
total_required_sip: float
|
48 |
+
monthly_savings: float
|
49 |
+
shortfall: float
|
50 |
+
surplus: float
|
51 |
+
|
52 |
+
class SIPCalculationRequest(BaseModel):
|
53 |
+
monthly_amount: float = Field(gt=0)
|
54 |
+
annual_return: float = Field(ge=0)
|
55 |
+
years: int = Field(ge=1)
|
56 |
+
|
57 |
+
class SIPCalculationResponse(BaseModel):
|
58 |
+
maturity_amount: float
|
59 |
+
total_invested: float
|
60 |
+
gains: float
|
61 |
+
return_multiple: float
|
62 |
+
yearly_breakdown: Optional[List[Dict[str, Any]]] = None
|
63 |
+
|
64 |
+
class RequiredSIPRequest(BaseModel):
|
65 |
+
target_amount: float = Field(gt=0)
|
66 |
+
years: int = Field(ge=1)
|
67 |
+
expected_return: float = Field(ge=0)
|
68 |
+
|
69 |
+
class RequiredSIPResponse(BaseModel):
|
70 |
+
required_sip: float
|
71 |
+
|
72 |
+
class GoalsDashboardRequest(BaseModel):
|
73 |
+
goals: List[InvestmentGoal]
|
74 |
+
monthly_savings: float
|
app/models/portfolio_models.py
ADDED
@@ -0,0 +1,62 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
from pydantic import BaseModel, Field
|
2 |
+
from typing import List, Dict, Optional, Any
|
3 |
+
from datetime import datetime
|
4 |
+
from enum import Enum
|
5 |
+
|
6 |
+
class InvestmentType(str, Enum):
|
7 |
+
LUMP_SUM = "Lump Sum"
|
8 |
+
SIP_MONTHLY = "SIP (Monthly)"
|
9 |
+
STP = "STP"
|
10 |
+
|
11 |
+
class PortfolioHolding(BaseModel):
|
12 |
+
scheme_code: str
|
13 |
+
category: str
|
14 |
+
fund_house: str
|
15 |
+
invested_amount: float = Field(gt=0)
|
16 |
+
current_value: float = Field(gt=0)
|
17 |
+
units: float = Field(gt=0)
|
18 |
+
current_nav: float = Field(gt=0)
|
19 |
+
investment_type: InvestmentType
|
20 |
+
nav_data: List[Any] = []
|
21 |
+
|
22 |
+
class Portfolio(BaseModel):
|
23 |
+
holdings: Dict[str, PortfolioHolding] = {}
|
24 |
+
|
25 |
+
class PortfolioMetrics(BaseModel):
|
26 |
+
total_value: float
|
27 |
+
total_invested: float
|
28 |
+
total_gains: float
|
29 |
+
category_allocation: Dict[str, float]
|
30 |
+
|
31 |
+
class PortfolioTemplate(str, Enum):
|
32 |
+
CONSERVATIVE = "Conservative"
|
33 |
+
BALANCED = "Balanced"
|
34 |
+
AGGRESSIVE = "Aggressive"
|
35 |
+
CUSTOM_SAMPLE = "Custom Sample"
|
36 |
+
|
37 |
+
class RebalanceAction(BaseModel):
|
38 |
+
category: str
|
39 |
+
current_pct: float
|
40 |
+
target_pct: float
|
41 |
+
difference: float
|
42 |
+
amount_change: float
|
43 |
+
action: str # "INCREASE" or "DECREASE"
|
44 |
+
|
45 |
+
class RebalanceAnalysis(BaseModel):
|
46 |
+
actions: List[RebalanceAction]
|
47 |
+
recommended_strategy: str
|
48 |
+
target_allocation: Dict[str, float]
|
49 |
+
|
50 |
+
class PerformanceReport(BaseModel):
|
51 |
+
total_invested: float
|
52 |
+
total_value: float
|
53 |
+
total_gains: float
|
54 |
+
overall_return_pct: float
|
55 |
+
fund_performance: List[Dict[str, Any]]
|
56 |
+
best_performer: Optional[str] = None
|
57 |
+
worst_performer: Optional[str] = None
|
58 |
+
max_return: float
|
59 |
+
min_return: float
|
60 |
+
volatility: float
|
61 |
+
sharpe_ratio: float
|
62 |
+
portfolio_metrics: PortfolioMetrics
|
app/services/__init__.py
ADDED
File without changes
|
app/services/ai_swarm.py
ADDED
@@ -0,0 +1,338 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
import json
|
3 |
+
from typing import Dict, Any, List
|
4 |
+
from app.config import settings
|
5 |
+
from app.models.ai_models import AIAnalysisRequest, AIAnalysisResponse, AISwarmAgent
|
6 |
+
import pandas as pd
|
7 |
+
|
8 |
+
# Popular mutual fund categories with scheme codes
|
9 |
+
POPULAR_FUNDS = {
|
10 |
+
'Large Cap Equity': {
|
11 |
+
'HDFC Top 100 Fund': '120503',
|
12 |
+
'ICICI Pru Bluechip Fund': '120505',
|
13 |
+
'SBI Bluechip Fund': '125497',
|
14 |
+
'Axis Bluechip Fund': '120503',
|
15 |
+
'Kotak Bluechip Fund': '118989'
|
16 |
+
},
|
17 |
+
'Mid Cap Equity': {
|
18 |
+
'HDFC Mid-Cap Opportunities Fund': '118551',
|
19 |
+
'ICICI Pru Mid Cap Fund': '120544',
|
20 |
+
'Kotak Emerging Equity Fund': '118999',
|
21 |
+
'SBI Magnum Mid Cap Fund': '100281',
|
22 |
+
'DSP Mid Cap Fund': '112618'
|
23 |
+
},
|
24 |
+
'Small Cap Equity': {
|
25 |
+
'SBI Small Cap Fund': '122639',
|
26 |
+
'DSP Small Cap Fund': '112618',
|
27 |
+
'HDFC Small Cap Fund': '118551',
|
28 |
+
'Axis Small Cap Fund': '125487',
|
29 |
+
'Kotak Small Cap Fund': '119028'
|
30 |
+
},
|
31 |
+
'ELSS (Tax Saving)': {
|
32 |
+
'Axis Long Term Equity Fund': '125494',
|
33 |
+
'HDFC Tax Saver': '100277',
|
34 |
+
'SBI Tax Saver': '125497',
|
35 |
+
'ICICI Pru ELSS Tax Saver': '120503',
|
36 |
+
'Kotak Tax Saver': '118989'
|
37 |
+
},
|
38 |
+
'Debt Funds': {
|
39 |
+
'HDFC Corporate Bond Fund': '101762',
|
40 |
+
'ICICI Pru Corporate Bond Fund': '120503',
|
41 |
+
'SBI Corporate Bond Fund': '125497',
|
42 |
+
'Kotak Corporate Bond Fund': '118989',
|
43 |
+
'Axis Corporate Debt Fund': '125494'
|
44 |
+
},
|
45 |
+
'Hybrid Funds': {
|
46 |
+
'HDFC Hybrid Equity Fund': '118551',
|
47 |
+
'ICICI Pru Balanced Advantage Fund': '120505',
|
48 |
+
'SBI Equity Hybrid Fund': '125497',
|
49 |
+
'Kotak Equity Hybrid Fund': '118999',
|
50 |
+
'Axis Hybrid Fund': '125494'
|
51 |
+
}
|
52 |
+
}
|
53 |
+
|
54 |
+
# AI Agent Prompts for Mutual Funds
|
55 |
+
FUND_SELECTION_PROMPT = """
|
56 |
+
You are an expert Indian mutual fund selection specialist with deep knowledge of fund analysis,
|
57 |
+
performance evaluation, and fund house comparisons for Indian mutual fund markets.
|
58 |
+
Your responsibilities:
|
59 |
+
1. Analyze fund performance across different time periods (1Y, 3Y, 5Y)
|
60 |
+
2. Evaluate expense ratios and their impact on long-term returns
|
61 |
+
3. Assess fund manager track record and consistency
|
62 |
+
4. Compare funds within categories using quantitative metrics
|
63 |
+
5. Identify funds with consistent alpha generation
|
64 |
+
6. Evaluate fund house stability and investor service quality
|
65 |
+
Focus areas for Indian mutual fund analysis:
|
66 |
+
- Performance consistency across market cycles
|
67 |
+
- Expense ratio optimization (direct vs regular plans)
|
68 |
+
- Fund manager tenure and investment philosophy
|
69 |
+
- AUM growth and scalability
|
70 |
+
- Category benchmarking and peer comparison
|
71 |
+
- Tax efficiency and dividend distribution policy
|
72 |
+
- Goal-based fund recommendations
|
73 |
+
Provide specific recommendations with rationale for Indian investors.
|
74 |
+
"""
|
75 |
+
|
76 |
+
GOAL_PLANNING_PROMPT = """
|
77 |
+
You are an expert in goal-based financial planning specialized in mapping investment
|
78 |
+
goals to appropriate mutual fund strategies for Indian investors.
|
79 |
+
Your responsibilities:
|
80 |
+
1. Analyze client goals for timeline, amount, and priority
|
81 |
+
2. Map goals to appropriate fund categories and asset allocation
|
82 |
+
3. Design SIP strategies aligned with goal timelines
|
83 |
+
4. Recommend optimal fund combinations for multiple goals
|
84 |
+
5. Plan step-up SIP strategies for inflation adjustment
|
85 |
+
6. Create tax-efficient investment strategies including ELSS
|
86 |
+
Goal-based fund mapping expertise:
|
87 |
+
- Short-term goals (1-3 years): Debt funds, liquid funds
|
88 |
+
- Medium-term goals (3-7 years): Hybrid funds, large cap equity
|
89 |
+
- Long-term goals (7+ years): Mid cap, small cap equity
|
90 |
+
- Tax-saving goals: ELSS funds with 3-year lock-in
|
91 |
+
- Retirement planning: Systematic equity exposure with debt balancing
|
92 |
+
- Emergency funds: Liquid funds with instant redemption capability
|
93 |
+
Provide actionable SIP recommendations with specific amounts and fund selections.
|
94 |
+
"""
|
95 |
+
|
96 |
+
RISK_ASSESSMENT_PROMPT = """
|
97 |
+
You are an expert in mutual fund risk analysis with extensive knowledge of
|
98 |
+
fund-specific risks, category risks, and portfolio risk management for Indian mutual fund investments.
|
99 |
+
Your responsibilities:
|
100 |
+
1. Assess fund-specific risks including manager risk, style drift
|
101 |
+
2. Analyze category concentration and diversification needs
|
102 |
+
3. Evaluate expense ratio impact on long-term wealth creation
|
103 |
+
4. Assess liquidity risks across different fund categories
|
104 |
+
5. Identify regulatory and tax-related risks
|
105 |
+
6. Recommend risk mitigation strategies
|
106 |
+
Focus on Indian mutual fund risk factors:
|
107 |
+
- Fund manager tenure and philosophy changes
|
108 |
+
- Category concentration and overlap analysis
|
109 |
+
- Expense ratio drag on returns over long periods
|
110 |
+
- Exit load structures and liquidity constraints
|
111 |
+
- Tax implications of different fund types
|
112 |
+
- Market timing risks in equity fund investments
|
113 |
+
- Credit risks in debt fund categories
|
114 |
+
Provide practical risk management recommendations for Indian mutual fund investors.
|
115 |
+
"""
|
116 |
+
|
117 |
+
FUND_ALLOCATION_PROMPT = """
|
118 |
+
You are an expert in mutual fund portfolio allocation and performance analysis, with deep knowledge of fund analysis, performance evaluation, and fund house comparisons for the Indian mutual fund market. Your role is to evaluate fund performance, provide insights on key metrics, and generate tailored recommendations based on individual investor needs and goals.
|
119 |
+
Your Responsibilities:
|
120 |
+
1. Fund Performance Analysis:
|
121 |
+
Analyze fund performance over different time periods (1Y, 3Y, 5Y) to assess consistency and growth potential.
|
122 |
+
2. Expense Ratio Evaluation:
|
123 |
+
Evaluate expense ratios (direct vs regular plans) and their long-term impact on net returns for different types of investors.
|
124 |
+
3. Fund Manager Analysis:
|
125 |
+
Assess the track record and investment philosophy of the fund manager. Determine consistency in management and its impact on performance.
|
126 |
+
4. Category Comparison:
|
127 |
+
Compare funds within categories (e.g., equity, debt, hybrid) using quantitative metrics such as:
|
128 |
+
Risk-adjusted returns
|
129 |
+
Sharpe ratio
|
130 |
+
Alpha generation
|
131 |
+
Drawdowns and volatility
|
132 |
+
5. Alpha Generation Identification:
|
133 |
+
Identify funds that consistently generate alpha (outperform the benchmark) and assess the strategies that lead to such performance.
|
134 |
+
6. Fund House Stability:
|
135 |
+
Evaluate the fund house's financial stability, reputation, and investor service quality.
|
136 |
+
Analyze AUM growth and scalability of the fund house to ensure long-term reliability.
|
137 |
+
7. Tax Efficiency & Dividend Distribution Policy:
|
138 |
+
Evaluate the tax efficiency of each fund considering capital gains tax, dividend distribution, and holding period taxation.
|
139 |
+
Provide insights into how these factors impact long-term returns for Indian investors.
|
140 |
+
8. Goal-Based Fund Recommendations:
|
141 |
+
Provide goal-based fund recommendations (e.g., retirement, education, wealth creation) with tailored suggestions based on:
|
142 |
+
Risk tolerance
|
143 |
+
Investment horizon
|
144 |
+
Tax considerations
|
145 |
+
|
146 |
+
Key Areas of Focus for Indian Mutual Fund Analysis:
|
147 |
+
Performance Consistency Across Market Cycles:
|
148 |
+
Assess how well the fund performs in different market environments (bullish, bearish, sideways).
|
149 |
+
Expense Ratio Optimization (Direct vs Regular Plans):
|
150 |
+
Evaluate the trade-off between cost efficiency and accessibility, recommending the most suitable fund plans for different investor types.
|
151 |
+
Fund Manager Tenure & Investment Philosophy:
|
152 |
+
Assess the impact of a fund manager's tenure and their investment philosophy on the fund's consistency and long-term performance.
|
153 |
+
AUM Growth & Scalability:
|
154 |
+
Determine how AUM growth impacts a fund's ability to scale and maintain performance. Large AUM may affect liquidity and flexibility, but also signal trust.
|
155 |
+
Category Benchmarking & Peer Comparison:
|
156 |
+
Compare funds against category benchmarks (e.g., Nifty 50 for equity, Crisil for debt) and identify top performers within their category.
|
157 |
+
Tax Efficiency & Dividend Policies:
|
158 |
+
Evaluate tax efficiency considering capital gains, dividends, and fund turnover. Provide strategies to minimize tax liabilities over the investment horizon.
|
159 |
+
Goal-Based Recommendations:
|
160 |
+
Provide tailored investment solutions based on individual investor goals, such as:
|
161 |
+
Retirement planning
|
162 |
+
Wealth creation
|
163 |
+
Education funding
|
164 |
+
Short-term goals
|
165 |
+
|
166 |
+
Recommendations for Indian Investors:
|
167 |
+
1. Equity Funds (Growth-Oriented):
|
168 |
+
Focus on funds with strong long-term performance, consistently high alpha, and low expense ratios. These funds are suitable for growth-focused investors seeking capital appreciation.
|
169 |
+
2. Debt Funds (Risk-Averse):
|
170 |
+
For investors with a low risk tolerance, recommend low-volatility funds with stable returns and a track record of consistency. Ensure tax-efficient funds for better after-tax returns.
|
171 |
+
3. Hybrid Funds (Balanced Approach):
|
172 |
+
Combine equity and debt for a diversified approach. These funds are suitable for investors seeking a balance between risk and reward, particularly those with a medium-term horizon.
|
173 |
+
4. Tax Efficiency:
|
174 |
+
Recommend funds that are tax-efficient, such as those with lower turnover and capital gains distributions. Focus on LTCG tax advantages for long-term investors.
|
175 |
+
5. Goal-Based Recommendations:
|
176 |
+
For retirement planning, suggest equity funds for long-term growth. For short-term goals, recommend debt funds or hybrid funds based on the investor's risk appetite.
|
177 |
+
Provide specific recommendations with rationale for Indian investors.
|
178 |
+
Make use of the following advanced portfolio techniques where applicable:
|
179 |
+
1. Modern Portfolio Theory (MPT) – Efficient Frontier
|
180 |
+
2. Risk Parity Allocation
|
181 |
+
3. Black-Litterman Model
|
182 |
+
4. Monte Carlo Simulation
|
183 |
+
"""
|
184 |
+
|
185 |
+
class AISwarmService:
|
186 |
+
"""Service for creating AI swarms for mutual fund analysis"""
|
187 |
+
|
188 |
+
def __init__(self):
|
189 |
+
self.api_key = settings.SWARMS_API_KEY
|
190 |
+
self.base_url = settings.SWARMS_BASE_URL
|
191 |
+
self.headers = {
|
192 |
+
"x-api-key": self.api_key,
|
193 |
+
"Content-Type": "application/json"
|
194 |
+
}
|
195 |
+
|
196 |
+
def create_mutual_fund_swarm(self, portfolio_data: Dict[str, Any],
|
197 |
+
client_profile: Dict[str, Any],
|
198 |
+
goals_data: Dict[str, Any]) -> AIAnalysisResponse:
|
199 |
+
"""Create AI swarm for mutual fund analysis"""
|
200 |
+
|
201 |
+
swarm_config = {
|
202 |
+
"name": "Mutual Fund Investment Analysis Swarm",
|
203 |
+
"description": "AI swarm for Indian mutual fund investment analysis and recommendations",
|
204 |
+
"agents": [
|
205 |
+
{
|
206 |
+
"agent_name": "Fund Selection Specialist",
|
207 |
+
"system_prompt": FUND_SELECTION_PROMPT,
|
208 |
+
"model_name": "gpt-4o",
|
209 |
+
"role": "worker",
|
210 |
+
"max_loops": 1,
|
211 |
+
"max_tokens": 4096,
|
212 |
+
"temperature": 0.3,
|
213 |
+
},
|
214 |
+
{
|
215 |
+
"agent_name": "Goal Planning Specialist",
|
216 |
+
"system_prompt": GOAL_PLANNING_PROMPT,
|
217 |
+
"model_name": "gpt-4o",
|
218 |
+
"role": "worker",
|
219 |
+
"max_loops": 1,
|
220 |
+
"max_tokens": 4096,
|
221 |
+
"temperature": 0.3,
|
222 |
+
},
|
223 |
+
{
|
224 |
+
"agent_name": "Risk Assessment Specialist",
|
225 |
+
"system_prompt": RISK_ASSESSMENT_PROMPT,
|
226 |
+
"model_name": "gpt-4o",
|
227 |
+
"role": "worker",
|
228 |
+
"max_loops": 1,
|
229 |
+
"max_tokens": 4096,
|
230 |
+
"temperature": 0.3,
|
231 |
+
},
|
232 |
+
{
|
233 |
+
"agent_name": "Fund Allocation Specialist",
|
234 |
+
"system_prompt": FUND_ALLOCATION_PROMPT,
|
235 |
+
"model_name": "gpt-4o",
|
236 |
+
"role": "worker",
|
237 |
+
"max_loops": 1,
|
238 |
+
"max_tokens": 4096,
|
239 |
+
"temperature": 0.3,
|
240 |
+
}
|
241 |
+
],
|
242 |
+
"max_loops": 2,
|
243 |
+
"swarm_type": "ConcurrentWorkflow",
|
244 |
+
"task": f"""
|
245 |
+
Analyze the mutual fund investment requirements:
|
246 |
+
|
247 |
+
Client Profile: {client_profile}
|
248 |
+
Current Portfolio: {portfolio_data}
|
249 |
+
Investment Goals: {goals_data}
|
250 |
+
|
251 |
+
Provide comprehensive recommendations for:
|
252 |
+
1. Fund selection and optimization
|
253 |
+
2. Goal-based SIP planning
|
254 |
+
3. Risk assessment and mitigation
|
255 |
+
4. Tax-efficient strategies
|
256 |
+
5. Implementation roadmap
|
257 |
+
"""
|
258 |
+
}
|
259 |
+
|
260 |
+
try:
|
261 |
+
if self.api_key:
|
262 |
+
response = requests.post(
|
263 |
+
f"{self.base_url}/v1/swarm/completions",
|
264 |
+
headers=self.headers,
|
265 |
+
json=swarm_config,
|
266 |
+
timeout=120
|
267 |
+
)
|
268 |
+
|
269 |
+
if response.status_code == 200:
|
270 |
+
result = response.json()
|
271 |
+
|
272 |
+
# Parse the output to extract agent responses
|
273 |
+
output_agents = []
|
274 |
+
if "output" in result and isinstance(result["output"], list):
|
275 |
+
for agent_output in result["output"]:
|
276 |
+
if isinstance(agent_output, dict):
|
277 |
+
output_agents.append(AISwarmAgent(
|
278 |
+
agent_name=agent_output.get("agent_name", ""),
|
279 |
+
content=agent_output.get("content", "")
|
280 |
+
))
|
281 |
+
|
282 |
+
return AIAnalysisResponse(
|
283 |
+
success=True,
|
284 |
+
output=output_agents
|
285 |
+
)
|
286 |
+
else:
|
287 |
+
return AIAnalysisResponse(
|
288 |
+
success=False,
|
289 |
+
error=f"API request failed with status code {response.status_code}"
|
290 |
+
)
|
291 |
+
else:
|
292 |
+
return AIAnalysisResponse(
|
293 |
+
success=False,
|
294 |
+
error="Swarms API key not configured"
|
295 |
+
)
|
296 |
+
except Exception as e:
|
297 |
+
return AIAnalysisResponse(
|
298 |
+
success=False,
|
299 |
+
error=f"Swarm analysis failed: {str(e)}"
|
300 |
+
)
|
301 |
+
|
302 |
+
def get_enhanced_analysis(self, request: AIAnalysisRequest) -> AIAnalysisResponse:
|
303 |
+
"""Get enhanced AI analysis with additional context"""
|
304 |
+
|
305 |
+
# Prepare comprehensive data for AI analysis
|
306 |
+
client_profile = request.client_profile
|
307 |
+
|
308 |
+
portfolio_data = {
|
309 |
+
'holdings': request.portfolio_data.get('holdings', []),
|
310 |
+
'categories': request.portfolio_data.get('categories', []),
|
311 |
+
'total_value': request.portfolio_data.get('total_value', 0),
|
312 |
+
'total_invested': request.portfolio_data.get('total_invested', 0),
|
313 |
+
'total_gains': request.portfolio_data.get('total_gains', 0),
|
314 |
+
'category_allocation': request.portfolio_data.get('category_allocation', {})
|
315 |
+
}
|
316 |
+
|
317 |
+
goals_data = {
|
318 |
+
'goals': request.goals_data.get('goals', []),
|
319 |
+
'goal_details': request.goals_data.get('goal_details', []),
|
320 |
+
'total_required_sip': request.goals_data.get('total_required_sip', 0),
|
321 |
+
'timeline_range': request.goals_data.get('timeline_range', ""),
|
322 |
+
'priority_goals': request.goals_data.get('priority_goals', [])
|
323 |
+
}
|
324 |
+
|
325 |
+
# Enhanced context
|
326 |
+
analysis_context = {
|
327 |
+
'market_conditions': request.market_conditions.value,
|
328 |
+
'investment_horizon': request.investment_horizon.value,
|
329 |
+
'analysis_focus': [focus.value for focus in request.analysis_focus],
|
330 |
+
'current_date': str(pd.Timestamp.now())
|
331 |
+
}
|
332 |
+
|
333 |
+
# Run AI analysis
|
334 |
+
return self.create_mutual_fund_swarm(
|
335 |
+
{**portfolio_data, **analysis_context},
|
336 |
+
client_profile,
|
337 |
+
goals_data
|
338 |
+
)
|
app/services/data_fetcher.py
ADDED
@@ -0,0 +1,101 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
import yfinance as yf
|
3 |
+
import pandas as pd
|
4 |
+
from datetime import datetime, timedelta
|
5 |
+
from typing import List, Dict, Any, Optional, Tuple
|
6 |
+
from app.config import settings
|
7 |
+
from app.models.fund_models import FundMeta, NAVData, FundNAVResponse, MarketIndex, MarketIndicesResponse
|
8 |
+
|
9 |
+
class MutualFundDataFetcher:
|
10 |
+
"""Fetch mutual fund data from MFAPI.in and Yahoo Finance for indices"""
|
11 |
+
|
12 |
+
def __init__(self):
|
13 |
+
self.mfapi_base = settings.MFAPI_BASE_URL
|
14 |
+
|
15 |
+
def get_all_schemes(self) -> List[Dict[str, Any]]:
|
16 |
+
"""Fetch all mutual fund schemes"""
|
17 |
+
try:
|
18 |
+
response = requests.get(self.mfapi_base)
|
19 |
+
if response.status_code == 200:
|
20 |
+
return response.json()
|
21 |
+
else:
|
22 |
+
return []
|
23 |
+
except Exception as e:
|
24 |
+
print(f"Error fetching schemes: {e}")
|
25 |
+
return []
|
26 |
+
|
27 |
+
def get_fund_nav_history(self, scheme_code: str) -> Tuple[pd.DataFrame, FundMeta]:
|
28 |
+
"""Fetch NAV history for a specific scheme"""
|
29 |
+
try:
|
30 |
+
url = f"{self.mfapi_base}/{scheme_code}"
|
31 |
+
response = requests.get(url)
|
32 |
+
if response.status_code == 200:
|
33 |
+
data = response.json()
|
34 |
+
nav_data = data.get('data', [])
|
35 |
+
|
36 |
+
# Convert to DataFrame
|
37 |
+
df = pd.DataFrame(nav_data)
|
38 |
+
if not df.empty:
|
39 |
+
df['date'] = pd.to_datetime(df['date'], format='%d-%m-%Y')
|
40 |
+
df['nav'] = pd.to_numeric(df['nav'])
|
41 |
+
df = df.sort_values('date')
|
42 |
+
|
43 |
+
# Create FundMeta object
|
44 |
+
meta_data = data.get('meta', {})
|
45 |
+
fund_meta = FundMeta(
|
46 |
+
fund_house=meta_data.get('fund_house'),
|
47 |
+
scheme_type=meta_data.get('scheme_type'),
|
48 |
+
scheme_category=meta_data.get('scheme_category'),
|
49 |
+
scheme_code=str(meta_data.get('scheme_code', scheme_code))
|
50 |
+
)
|
51 |
+
|
52 |
+
return df, fund_meta
|
53 |
+
else:
|
54 |
+
return pd.DataFrame(), FundMeta()
|
55 |
+
except Exception as e:
|
56 |
+
print(f"Error fetching NAV data: {e}")
|
57 |
+
return pd.DataFrame(), FundMeta()
|
58 |
+
|
59 |
+
def get_market_indices(self) -> MarketIndicesResponse:
|
60 |
+
"""Fetch Indian market indices using Yahoo Finance"""
|
61 |
+
indices = {
|
62 |
+
'^NSEI': 'Nifty 50',
|
63 |
+
'^BSESN': 'BSE Sensex',
|
64 |
+
'^NSEBANK': 'Nifty Bank',
|
65 |
+
'^CNXIT': 'Nifty IT',
|
66 |
+
'^NSEMDCP50': 'Nifty Midcap 50',
|
67 |
+
'NIFTYSMLCAP50.NS': 'Nifty Smallcap 50',
|
68 |
+
'^CNXPHARMA': 'Nifty Pharma',
|
69 |
+
'^CNXAUTO': 'Nifty Auto',
|
70 |
+
'^CNXFMCG': 'Nifty FMCG',
|
71 |
+
'^CNXENERGY': 'Nifty Energy',
|
72 |
+
'^CNXREALTY': 'Nifty Realty',
|
73 |
+
'^NSMIDCP': 'Nifty Next 50',
|
74 |
+
}
|
75 |
+
|
76 |
+
indices_data = []
|
77 |
+
for symbol, name in indices.items():
|
78 |
+
try:
|
79 |
+
ticker = yf.Ticker(symbol)
|
80 |
+
hist = ticker.history(period="5d")
|
81 |
+
|
82 |
+
if not hist.empty:
|
83 |
+
current_price = hist['Close'].iloc[-1]
|
84 |
+
previous_close = hist['Close'].iloc[-2] if len(hist) > 1 else current_price
|
85 |
+
change = current_price - previous_close
|
86 |
+
change_pct = (change / previous_close) * 100
|
87 |
+
|
88 |
+
indices_data.append(MarketIndex(
|
89 |
+
name=name,
|
90 |
+
symbol=symbol,
|
91 |
+
current_price=current_price,
|
92 |
+
change=change,
|
93 |
+
change_pct=change_pct
|
94 |
+
))
|
95 |
+
except Exception as e:
|
96 |
+
print(f"Could not fetch {name}: {e}")
|
97 |
+
|
98 |
+
return MarketIndicesResponse(
|
99 |
+
indices=indices_data,
|
100 |
+
last_updated=datetime.now()
|
101 |
+
)
|
app/services/portfolio_analyzer.py
ADDED
@@ -0,0 +1,296 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import numpy as np
|
2 |
+
import pandas as pd
|
3 |
+
from typing import Dict, Any, List, Tuple, Optional
|
4 |
+
from app.models.portfolio_models import (
|
5 |
+
Portfolio, PortfolioMetrics, PortfolioTemplate,
|
6 |
+
RebalanceAction, RebalanceAnalysis, PerformanceReport
|
7 |
+
)
|
8 |
+
|
9 |
+
class PortfolioAnalyzer:
|
10 |
+
"""Analyze mutual fund portfolio performance and allocation"""
|
11 |
+
|
12 |
+
@staticmethod
|
13 |
+
def calculate_portfolio_metrics(portfolio_holdings: Dict[str, Any]) -> PortfolioMetrics:
|
14 |
+
"""Calculate portfolio-level metrics"""
|
15 |
+
if not portfolio_holdings:
|
16 |
+
return PortfolioMetrics(
|
17 |
+
total_value=0,
|
18 |
+
total_invested=0,
|
19 |
+
total_gains=0,
|
20 |
+
category_allocation={}
|
21 |
+
)
|
22 |
+
|
23 |
+
total_value = sum(holding.current_value for holding in portfolio_holdings.values())
|
24 |
+
|
25 |
+
metrics = {
|
26 |
+
'total_value': total_value,
|
27 |
+
'total_invested': sum(holding.invested_amount for holding in portfolio_holdings.values()),
|
28 |
+
'total_gains': total_value - sum(holding.invested_amount for holding in portfolio_holdings.values()),
|
29 |
+
'category_allocation': {}
|
30 |
+
}
|
31 |
+
|
32 |
+
# Calculate category allocation
|
33 |
+
for fund_name, holding in portfolio_holdings.items():
|
34 |
+
category = holding.category if hasattr(holding, 'category') else 'Other'
|
35 |
+
if category not in metrics['category_allocation']:
|
36 |
+
metrics['category_allocation'][category] = 0
|
37 |
+
metrics['category_allocation'][category] += holding.current_value
|
38 |
+
|
39 |
+
# Convert to percentages
|
40 |
+
for category in metrics['category_allocation']:
|
41 |
+
metrics['category_allocation'][category] = (
|
42 |
+
metrics['category_allocation'][category] / total_value
|
43 |
+
) * 100 if total_value > 0 else 0
|
44 |
+
|
45 |
+
return PortfolioMetrics(**metrics)
|
46 |
+
|
47 |
+
@staticmethod
|
48 |
+
def generate_rebalance_analysis(portfolio: Dict[str, Any]) -> RebalanceAnalysis:
|
49 |
+
"""Generate detailed rebalancing analysis and recommendations"""
|
50 |
+
if not portfolio:
|
51 |
+
return RebalanceAnalysis(
|
52 |
+
actions=[],
|
53 |
+
recommended_strategy="No portfolio data available",
|
54 |
+
target_allocation={}
|
55 |
+
)
|
56 |
+
|
57 |
+
portfolio_metrics = PortfolioAnalyzer.calculate_portfolio_metrics(portfolio)
|
58 |
+
current_allocation = portfolio_metrics.category_allocation
|
59 |
+
|
60 |
+
# Define target allocations based on common strategies
|
61 |
+
target_allocations = {
|
62 |
+
'Conservative': {
|
63 |
+
'Large Cap Equity': 30, 'Debt Funds': 40, 'Hybrid Funds': 25,
|
64 |
+
'ELSS (Tax Saving)': 5, 'Mid Cap Equity': 0, 'Small Cap Equity': 0
|
65 |
+
},
|
66 |
+
'Balanced': {
|
67 |
+
'Large Cap Equity': 40, 'Mid Cap Equity': 25, 'ELSS (Tax Saving)': 15,
|
68 |
+
'Debt Funds': 15, 'Hybrid Funds': 5, 'Small Cap Equity': 0
|
69 |
+
},
|
70 |
+
'Aggressive': {
|
71 |
+
'Large Cap Equity': 35, 'Mid Cap Equity': 30, 'Small Cap Equity': 25,
|
72 |
+
'ELSS (Tax Saving)': 10, 'Debt Funds': 0, 'Hybrid Funds': 0
|
73 |
+
}
|
74 |
+
}
|
75 |
+
|
76 |
+
# Determine closest target allocation
|
77 |
+
total_equity = (current_allocation.get('Large Cap Equity', 0) +
|
78 |
+
current_allocation.get('Mid Cap Equity', 0) +
|
79 |
+
current_allocation.get('Small Cap Equity', 0))
|
80 |
+
|
81 |
+
if total_equity >= 70:
|
82 |
+
recommended_strategy = 'Aggressive'
|
83 |
+
elif total_equity >= 45:
|
84 |
+
recommended_strategy = 'Balanced'
|
85 |
+
else:
|
86 |
+
recommended_strategy = 'Conservative'
|
87 |
+
|
88 |
+
target = target_allocations[recommended_strategy]
|
89 |
+
total_portfolio_value = portfolio_metrics.total_value
|
90 |
+
|
91 |
+
# Calculate rebalancing requirements
|
92 |
+
rebalance_actions = []
|
93 |
+
for category in target:
|
94 |
+
current_pct = current_allocation.get(category, 0)
|
95 |
+
target_pct = target[category]
|
96 |
+
difference = target_pct - current_pct
|
97 |
+
|
98 |
+
if abs(difference) > 5: # Only suggest rebalancing if difference > 5%
|
99 |
+
current_value = (current_pct / 100) * total_portfolio_value
|
100 |
+
target_value = (target_pct / 100) * total_portfolio_value
|
101 |
+
amount_change = target_value - current_value
|
102 |
+
|
103 |
+
action = "INCREASE" if difference > 0 else "DECREASE"
|
104 |
+
rebalance_actions.append(RebalanceAction(
|
105 |
+
category=category,
|
106 |
+
current_pct=current_pct,
|
107 |
+
target_pct=target_pct,
|
108 |
+
difference=difference,
|
109 |
+
amount_change=amount_change,
|
110 |
+
action=action
|
111 |
+
))
|
112 |
+
|
113 |
+
return RebalanceAnalysis(
|
114 |
+
actions=rebalance_actions,
|
115 |
+
recommended_strategy=recommended_strategy,
|
116 |
+
target_allocation=target
|
117 |
+
)
|
118 |
+
|
119 |
+
@staticmethod
|
120 |
+
def generate_performance_report(portfolio: Dict[str, Any]) -> PerformanceReport:
|
121 |
+
"""Generate comprehensive performance report"""
|
122 |
+
if not portfolio:
|
123 |
+
return PerformanceReport(
|
124 |
+
total_invested=0,
|
125 |
+
total_value=0,
|
126 |
+
total_gains=0,
|
127 |
+
overall_return_pct=0,
|
128 |
+
fund_performance=[],
|
129 |
+
max_return=0,
|
130 |
+
min_return=0,
|
131 |
+
volatility=0,
|
132 |
+
sharpe_ratio=0,
|
133 |
+
portfolio_metrics=PortfolioMetrics(
|
134 |
+
total_value=0,
|
135 |
+
total_invested=0,
|
136 |
+
total_gains=0,
|
137 |
+
category_allocation={}
|
138 |
+
)
|
139 |
+
)
|
140 |
+
|
141 |
+
portfolio_metrics = PortfolioAnalyzer.calculate_portfolio_metrics(portfolio)
|
142 |
+
|
143 |
+
# Calculate performance metrics
|
144 |
+
total_invested = portfolio_metrics.total_invested
|
145 |
+
total_value = portfolio_metrics.total_value
|
146 |
+
total_gains = portfolio_metrics.total_gains
|
147 |
+
|
148 |
+
if total_invested > 0:
|
149 |
+
overall_return_pct = (total_gains / total_invested) * 100
|
150 |
+
else:
|
151 |
+
overall_return_pct = 0
|
152 |
+
|
153 |
+
# Fund-wise performance
|
154 |
+
fund_performance = []
|
155 |
+
best_performer = None
|
156 |
+
worst_performer = None
|
157 |
+
max_return = float('-inf')
|
158 |
+
min_return = float('inf')
|
159 |
+
|
160 |
+
for fund_name, holding in portfolio.items():
|
161 |
+
invested = holding.invested_amount
|
162 |
+
current = holding.current_value
|
163 |
+
gain_loss = current - invested
|
164 |
+
return_pct = (gain_loss / invested * 100) if invested > 0 else 0
|
165 |
+
|
166 |
+
fund_performance.append({
|
167 |
+
'fund': fund_name,
|
168 |
+
'category': holding.category if hasattr(holding, 'category') else 'Other',
|
169 |
+
'invested': invested,
|
170 |
+
'current_value': current,
|
171 |
+
'gain_loss': gain_loss,
|
172 |
+
'return_pct': return_pct
|
173 |
+
})
|
174 |
+
|
175 |
+
if return_pct > max_return:
|
176 |
+
max_return = return_pct
|
177 |
+
best_performer = fund_name
|
178 |
+
|
179 |
+
if return_pct < min_return:
|
180 |
+
min_return = return_pct
|
181 |
+
worst_performer = fund_name
|
182 |
+
|
183 |
+
# Risk metrics (simplified)
|
184 |
+
returns = [perf['return_pct'] for perf in fund_performance]
|
185 |
+
if len(returns) > 1:
|
186 |
+
volatility = np.std(returns)
|
187 |
+
sharpe_ratio = (np.mean(returns) - 6) / volatility if volatility > 0 else 0 # Assuming 6% risk-free rate
|
188 |
+
else:
|
189 |
+
volatility = 0
|
190 |
+
sharpe_ratio = 0
|
191 |
+
|
192 |
+
return PerformanceReport(
|
193 |
+
total_invested=total_invested,
|
194 |
+
total_value=total_value,
|
195 |
+
total_gains=total_gains,
|
196 |
+
overall_return_pct=overall_return_pct,
|
197 |
+
fund_performance=fund_performance,
|
198 |
+
best_performer=best_performer,
|
199 |
+
worst_performer=worst_performer,
|
200 |
+
max_return=max_return,
|
201 |
+
min_return=min_return,
|
202 |
+
volatility=volatility,
|
203 |
+
sharpe_ratio=sharpe_ratio,
|
204 |
+
portfolio_metrics=portfolio_metrics
|
205 |
+
)
|
206 |
+
|
207 |
+
@staticmethod
|
208 |
+
def get_template_portfolio(template: PortfolioTemplate) -> Dict[str, Any]:
|
209 |
+
"""Get a predefined portfolio template"""
|
210 |
+
templates = {
|
211 |
+
PortfolioTemplate.CONSERVATIVE: {
|
212 |
+
'HDFC Corporate Bond Fund': {
|
213 |
+
'scheme_code': '101762', 'category': 'Debt Funds',
|
214 |
+
'invested_amount': 40000, 'current_value': 42000, 'units': 800,
|
215 |
+
'investment_type': 'SIP (Monthly)'
|
216 |
+
},
|
217 |
+
'HDFC Top 100 Fund': {
|
218 |
+
'scheme_code': '120503', 'category': 'Large Cap Equity',
|
219 |
+
'invested_amount': 30000, 'current_value': 33000, 'units': 600,
|
220 |
+
'investment_type': 'SIP (Monthly)'
|
221 |
+
},
|
222 |
+
'HDFC Hybrid Equity Fund': {
|
223 |
+
'scheme_code': '118551', 'category': 'Hybrid Funds',
|
224 |
+
'invested_amount': 30000, 'current_value': 32000, 'units': 600,
|
225 |
+
'investment_type': 'SIP (Monthly)'
|
226 |
+
}
|
227 |
+
},
|
228 |
+
PortfolioTemplate.BALANCED: {
|
229 |
+
'HDFC Top 100 Fund': {
|
230 |
+
'scheme_code': '120503', 'category': 'Large Cap Equity',
|
231 |
+
'invested_amount': 40000, 'current_value': 45000, 'units': 800,
|
232 |
+
'investment_type': 'SIP (Monthly)'
|
233 |
+
},
|
234 |
+
'ICICI Pru Mid Cap Fund': {
|
235 |
+
'scheme_code': '120544', 'category': 'Mid Cap Equity',
|
236 |
+
'invested_amount': 30000, 'current_value': 36000, 'units': 400,
|
237 |
+
'investment_type': 'SIP (Monthly)'
|
238 |
+
},
|
239 |
+
'HDFC Tax Saver': {
|
240 |
+
'scheme_code': '100277', 'category': 'ELSS (Tax Saving)',
|
241 |
+
'invested_amount': 20000, 'current_value': 23000, 'units': 400,
|
242 |
+
'investment_type': 'SIP (Monthly)'
|
243 |
+
},
|
244 |
+
'HDFC Corporate Bond Fund': {
|
245 |
+
'scheme_code': '101762', 'category': 'Debt Funds',
|
246 |
+
'invested_amount': 10000, 'current_value': 10500, 'units': 200,
|
247 |
+
'investment_type': 'Lump Sum'
|
248 |
+
}
|
249 |
+
},
|
250 |
+
PortfolioTemplate.AGGRESSIVE: {
|
251 |
+
'SBI Small Cap Fund': {
|
252 |
+
'scheme_code': '122639', 'category': 'Small Cap Equity',
|
253 |
+
'invested_amount': 30000, 'current_value': 38000, 'units': 500,
|
254 |
+
'investment_type': 'SIP (Monthly)'
|
255 |
+
},
|
256 |
+
'ICICI Pru Mid Cap Fund': {
|
257 |
+
'scheme_code': '120544', 'category': 'Mid Cap Equity',
|
258 |
+
'invested_amount': 30000, 'current_value': 36000, 'units': 400,
|
259 |
+
'investment_type': 'SIP (Monthly)'
|
260 |
+
},
|
261 |
+
'HDFC Top 100 Fund': {
|
262 |
+
'scheme_code': '120503', 'category': 'Large Cap Equity',
|
263 |
+
'invested_amount': 25000, 'current_value': 28000, 'units': 500,
|
264 |
+
'investment_type': 'SIP (Monthly)'
|
265 |
+
},
|
266 |
+
'Kotak Emerging Equity Fund': {
|
267 |
+
'scheme_code': '118999', 'category': 'Mid Cap Equity',
|
268 |
+
'invested_amount': 15000, 'current_value': 18000, 'units': 200,
|
269 |
+
'investment_type': 'SIP (Monthly)'
|
270 |
+
}
|
271 |
+
},
|
272 |
+
PortfolioTemplate.CUSTOM_SAMPLE: {
|
273 |
+
'HDFC Balanced Advantage Fund': {
|
274 |
+
'scheme_code': '104248', 'category': 'Hybrid Funds',
|
275 |
+
'invested_amount': 30000, 'current_value': 34000, 'units': 600,
|
276 |
+
'investment_type': 'SIP (Monthly)'
|
277 |
+
},
|
278 |
+
'HDFC Top 100 Fund': {
|
279 |
+
'scheme_code': '120503', 'category': 'Large Cap Equity',
|
280 |
+
'invested_amount': 30000, 'current_value': 33000, 'units': 600,
|
281 |
+
'investment_type': 'SIP (Monthly)'
|
282 |
+
},
|
283 |
+
'HDFC Corporate Bond Fund': {
|
284 |
+
'scheme_code': '101762', 'category': 'Debt Funds',
|
285 |
+
'invested_amount': 20000, 'current_value': 21000, 'units': 400,
|
286 |
+
'investment_type': 'Lump Sum'
|
287 |
+
},
|
288 |
+
'HDFC Tax Saver': {
|
289 |
+
'scheme_code': '100277', 'category': 'ELSS (Tax Saving)',
|
290 |
+
'invested_amount': 20000, 'current_value': 23000, 'units': 400,
|
291 |
+
'investment_type': 'SIP (Monthly)'
|
292 |
+
}
|
293 |
+
}
|
294 |
+
}
|
295 |
+
|
296 |
+
return templates.get(template, {})
|
app/services/sip_calculator.py
ADDED
@@ -0,0 +1,115 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import pandas as pd
|
2 |
+
from datetime import datetime
|
3 |
+
from typing import Optional, Dict, Any, List
|
4 |
+
from app.models.goal_models import SIPCalculationRequest, SIPCalculationResponse, RequiredSIPRequest, RequiredSIPResponse
|
5 |
+
|
6 |
+
class SIPCalculator:
|
7 |
+
"""Calculate SIP returns and goal-based investments"""
|
8 |
+
|
9 |
+
@staticmethod
|
10 |
+
def calculate_sip_maturity(monthly_amount: float, annual_return: float, years: int) -> float:
|
11 |
+
"""Calculate SIP maturity amount"""
|
12 |
+
if annual_return == 0:
|
13 |
+
return monthly_amount * years * 12
|
14 |
+
|
15 |
+
monthly_return = annual_return / 12 / 100
|
16 |
+
months = years * 12
|
17 |
+
|
18 |
+
maturity_amount = monthly_amount * (
|
19 |
+
((1 + monthly_return) ** months - 1) / monthly_return
|
20 |
+
) * (1 + monthly_return)
|
21 |
+
|
22 |
+
return maturity_amount
|
23 |
+
|
24 |
+
@staticmethod
|
25 |
+
def calculate_required_sip(target_amount: float, years: int, expected_return: float) -> float:
|
26 |
+
"""Calculate required SIP amount for a target"""
|
27 |
+
if expected_return == 0:
|
28 |
+
return target_amount / (years * 12)
|
29 |
+
|
30 |
+
monthly_return = expected_return / 12 / 100
|
31 |
+
months = years * 12
|
32 |
+
|
33 |
+
required_sip = target_amount / (
|
34 |
+
((1 + monthly_return) ** months - 1) / monthly_return
|
35 |
+
) / (1 + monthly_return)
|
36 |
+
|
37 |
+
return required_sip
|
38 |
+
|
39 |
+
@staticmethod
|
40 |
+
def calculate_fund_returns(nav_data: pd.DataFrame, investment_amount: float,
|
41 |
+
start_date: datetime, end_date: datetime) -> Optional[Dict[str, Any]]:
|
42 |
+
"""Calculate returns for a fund investment"""
|
43 |
+
try:
|
44 |
+
filtered_data = nav_data[
|
45 |
+
(nav_data['date'] >= pd.to_datetime(start_date)) &
|
46 |
+
(nav_data['date'] <= pd.to_datetime(end_date))
|
47 |
+
]
|
48 |
+
|
49 |
+
if len(filtered_data) < 2:
|
50 |
+
return None
|
51 |
+
|
52 |
+
start_nav = filtered_data.iloc[0]['nav']
|
53 |
+
end_nav = filtered_data.iloc[-1]['nav']
|
54 |
+
|
55 |
+
units = investment_amount / start_nav
|
56 |
+
final_value = units * end_nav
|
57 |
+
total_return = ((final_value - investment_amount) / investment_amount) * 100
|
58 |
+
|
59 |
+
return {
|
60 |
+
'start_nav': start_nav,
|
61 |
+
'end_nav': end_nav,
|
62 |
+
'units': units,
|
63 |
+
'final_value': final_value,
|
64 |
+
'total_return': total_return,
|
65 |
+
'investment_amount': investment_amount
|
66 |
+
}
|
67 |
+
except Exception as e:
|
68 |
+
print(f"Error calculating returns: {e}")
|
69 |
+
return None
|
70 |
+
|
71 |
+
@staticmethod
|
72 |
+
def get_sip_calculation(request: SIPCalculationRequest, include_yearly_breakdown: bool = False) -> SIPCalculationResponse:
|
73 |
+
"""Get SIP calculation with optional yearly breakdown"""
|
74 |
+
maturity_amount = SIPCalculator.calculate_sip_maturity(
|
75 |
+
request.monthly_amount, request.annual_return, request.years
|
76 |
+
)
|
77 |
+
|
78 |
+
total_invested = request.monthly_amount * request.years * 12
|
79 |
+
gains = maturity_amount - total_invested
|
80 |
+
return_multiple = maturity_amount / total_invested if total_invested > 0 else 0
|
81 |
+
|
82 |
+
yearly_breakdown = None
|
83 |
+
if include_yearly_breakdown:
|
84 |
+
yearly_breakdown = []
|
85 |
+
invested_cumulative = 0
|
86 |
+
|
87 |
+
for year in range(1, request.years + 1):
|
88 |
+
maturity_year = SIPCalculator.calculate_sip_maturity(
|
89 |
+
request.monthly_amount, request.annual_return, year
|
90 |
+
)
|
91 |
+
invested_year = request.monthly_amount * year * 12
|
92 |
+
|
93 |
+
yearly_breakdown.append({
|
94 |
+
'Year': year,
|
95 |
+
'Invested': invested_year,
|
96 |
+
'Maturity Value': maturity_year,
|
97 |
+
'Gains': maturity_year - invested_year
|
98 |
+
})
|
99 |
+
|
100 |
+
return SIPCalculationResponse(
|
101 |
+
maturity_amount=maturity_amount,
|
102 |
+
total_invested=total_invested,
|
103 |
+
gains=gains,
|
104 |
+
return_multiple=return_multiple,
|
105 |
+
yearly_breakdown=yearly_breakdown
|
106 |
+
)
|
107 |
+
|
108 |
+
@staticmethod
|
109 |
+
def get_required_sip(request: RequiredSIPRequest) -> RequiredSIPResponse:
|
110 |
+
"""Get required SIP amount for a target"""
|
111 |
+
required_sip = SIPCalculator.calculate_required_sip(
|
112 |
+
request.target_amount, request.years, request.expected_return
|
113 |
+
)
|
114 |
+
|
115 |
+
return RequiredSIPResponse(required_sip=required_sip)
|
app/utils/__init__.py
ADDED
File without changes
|
app/utils/helpers.py
ADDED
File without changes
|