lucifer7210 commited on
Commit
298b0e7
·
verified ·
1 Parent(s): d9c4a07

Upload 18 files

Browse files
.env ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ SWARMS_API_KEY=sk-f763a9c6f898aa3ec74bf49047736a0201c293eefc36d5a04be8af647b93b711
2
+ FRED_API_KEY=d421178827e3d9c621210bc6f0303439
.gitignore ADDED
@@ -0,0 +1 @@
 
 
1
+ __pycache__/
app/__init__.py ADDED
File without changes
app/api/__init__.py ADDED
File without changes
app/api/dependencies.py ADDED
@@ -0,0 +1,17 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import HTTPException, Depends
2
+ from app.services.data_fetcher import PortfolioDataFetcher, EconomicDataFetcher
3
+ from app.services.portfolio_analyzer import PortfolioAnalyzer
4
+ from app.services.backtester import AdvancedBacktester
5
+ from app.core.exceptions import PortfolioException
6
+
7
+ def get_data_fetcher() -> PortfolioDataFetcher:
8
+ return PortfolioDataFetcher()
9
+
10
+ def get_economic_fetcher() -> EconomicDataFetcher:
11
+ return EconomicDataFetcher()
12
+
13
+ def get_portfolio_analyzer() -> PortfolioAnalyzer:
14
+ return PortfolioAnalyzer()
15
+
16
+ def get_backtester() -> AdvancedBacktester:
17
+ return AdvancedBacktester()
app/api/routes.py ADDED
@@ -0,0 +1,161 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import APIRouter, Depends, HTTPException
2
+ from typing import Dict, List, Any
3
+ from app.services.data_fetcher import PortfolioDataFetcher, EconomicDataFetcher
4
+ from app.services.portfolio_analyzer import PortfolioAnalyzer
5
+ from app.services.backtester import AdvancedBacktester
6
+ from app.services.swarm_service import create_portfolio_swarm
7
+ from app.models.response import (
8
+ EconomicDataResponse,
9
+ MarketIndicesResponse,
10
+ StockDataResponse,
11
+ PortfolioAnalysisResponse,
12
+ BacktestResponse,
13
+ SwarmAnalysisResponse
14
+ )
15
+ from app.models.portfolio import Portfolio, BacktestRequest, ClientProfile
16
+ from app.api.dependencies import (
17
+ get_data_fetcher,
18
+ get_economic_fetcher,
19
+ get_portfolio_analyzer,
20
+ get_backtester
21
+ )
22
+ from app.core.exceptions import PortfolioException
23
+
24
+ router = APIRouter()
25
+
26
+ @router.get("/economic-data", response_model=EconomicDataResponse)
27
+ async def get_economic_data(fetcher: EconomicDataFetcher = Depends(get_economic_fetcher)):
28
+ """Get current economic indicators"""
29
+ try:
30
+ rbi_repo_rate = fetcher.get_rbi_repo_rate()
31
+ indian_inflation_rate = fetcher.get_indian_inflation_rate()
32
+ real_interest_rate = rbi_repo_rate - indian_inflation_rate
33
+
34
+ return EconomicDataResponse(
35
+ rbi_repo_rate=rbi_repo_rate,
36
+ indian_inflation_rate=indian_inflation_rate,
37
+ real_interest_rate=real_interest_rate
38
+ )
39
+ except Exception as e:
40
+ raise HTTPException(status_code=500, detail=str(e))
41
+
42
+ @router.get("/market-indices", response_model=MarketIndicesResponse)
43
+ async def get_market_indices(fetcher: PortfolioDataFetcher = Depends(get_data_fetcher)):
44
+ """Get live market indices data"""
45
+ try:
46
+ indices_data = fetcher.get_market_indices()
47
+ return MarketIndicesResponse(indices=indices_data)
48
+ except Exception as e:
49
+ raise HTTPException(status_code=500, detail=str(e))
50
+
51
+ @router.get("/stock-data/{symbol}", response_model=StockDataResponse)
52
+ async def get_stock_data(symbol: str, fetcher: PortfolioDataFetcher = Depends(get_data_fetcher)):
53
+ """Get stock data for a specific symbol"""
54
+ try:
55
+ stock_data = fetcher.get_stock_data([symbol])
56
+ if symbol not in stock_data:
57
+ raise HTTPException(status_code=404, detail=f"Stock data not found for {symbol}")
58
+
59
+ data = stock_data[symbol]
60
+ return StockDataResponse(
61
+ symbol=symbol,
62
+ current_price=data['current_price'],
63
+ volume=data['volume'],
64
+ returns_1y=data['returns_1y'],
65
+ volatility=data['volatility'],
66
+ market_cap=data['market_cap'],
67
+ pe_ratio=data['pe_ratio'],
68
+ dividend_yield=data['dividend_yield']
69
+ )
70
+ except Exception as e:
71
+ raise HTTPException(status_code=500, detail=str(e))
72
+
73
+ @router.post("/portfolio-analysis", response_model=PortfolioAnalysisResponse)
74
+ async def analyze_portfolio(
75
+ portfolio: Portfolio,
76
+ analyzer: PortfolioAnalyzer = Depends(get_portfolio_analyzer),
77
+ fetcher: PortfolioDataFetcher = Depends(get_data_fetcher)
78
+ ):
79
+ """Analyze portfolio performance and allocation"""
80
+ try:
81
+ # Convert portfolio to holdings dict
82
+ holdings = {}
83
+ for holding in portfolio.holdings:
84
+ holdings[holding.symbol] = {
85
+ 'quantity': holding.quantity,
86
+ 'current_price': holding.current_price,
87
+ 'name': holding.name
88
+ }
89
+
90
+ # Analyze portfolio
91
+ analysis = analyzer.analyze_portfolio(holdings, portfolio.current_portfolio_value)
92
+
93
+ return PortfolioAnalysisResponse(
94
+ portfolio_value=analysis['portfolio_value'],
95
+ number_of_holdings=analysis['number_of_holdings'],
96
+ sector_allocation=analysis['sector_allocation'],
97
+ holdings=analysis['holdings']
98
+ )
99
+ except Exception as e:
100
+ raise HTTPException(status_code=500, detail=str(e))
101
+
102
+ @router.post("/backtest", response_model=BacktestResponse)
103
+ async def backtest_portfolio(
104
+ request: BacktestRequest,
105
+ backtester: AdvancedBacktester = Depends(get_backtester)
106
+ ):
107
+ """Run backtesting on a portfolio"""
108
+ try:
109
+ results, error = backtester.backtest_portfolio(
110
+ request.holdings,
111
+ request.start_date.strftime("%Y-%m-%d"),
112
+ request.end_date.strftime("%Y-%m-%d"),
113
+ request.benchmark
114
+ )
115
+
116
+ if error:
117
+ raise HTTPException(status_code=400, detail=error)
118
+
119
+ return BacktestResponse(**results)
120
+ except Exception as e:
121
+ raise HTTPException(status_code=500, detail=str(e))
122
+
123
+ @router.post("/ai-analysis", response_model=SwarmAnalysisResponse)
124
+ async def run_ai_analysis(
125
+ portfolio: Portfolio,
126
+ client_profile: ClientProfile
127
+ ):
128
+ """Run AI analysis using the swarm"""
129
+ try:
130
+ # Prepare portfolio data for analysis
131
+ portfolio_summary = f"""
132
+ Portfolio Value: ₹{portfolio.current_portfolio_value:,.0f}
133
+ Number of Holdings: {len(portfolio.holdings)}
134
+ Holdings: {portfolio.holdings}
135
+ """
136
+
137
+ client_profile_str = f"""
138
+ Age: {client_profile.age}
139
+ Investment Horizon: {client_profile.investment_horizon}
140
+ Risk Tolerance: {client_profile.risk_tolerance}
141
+ Investment Objective: {client_profile.investment_objective}
142
+ Annual Income: ₹{client_profile.annual_income:,.0f}
143
+ """
144
+
145
+ # Get economic data
146
+ economic_fetcher = EconomicDataFetcher()
147
+ rbi_repo_rate = economic_fetcher.get_rbi_repo_rate()
148
+ indian_inflation_rate = economic_fetcher.get_indian_inflation_rate()
149
+
150
+ economic_context = f"""
151
+ Current RBI Repo Rate: {rbi_repo_rate:.2f}%
152
+ Current Indian Inflation Rate: {indian_inflation_rate:.2f}%
153
+ Real Interest Rate: {rbi_repo_rate - indian_inflation_rate:.2f}%
154
+ """
155
+
156
+ # Run swarm analysis
157
+ swarm_result = create_portfolio_swarm(portfolio_summary, client_profile_str, economic_context)
158
+
159
+ return SwarmAnalysisResponse(**swarm_result)
160
+ except Exception as e:
161
+ raise HTTPException(status_code=500, detail=str(e))
app/core/__init__.py ADDED
File without changes
app/core/config.py ADDED
@@ -0,0 +1,169 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import os
2
+ from dotenv import load_dotenv
3
+ from pydantic_settings import BaseSettings
4
+
5
+ load_dotenv()
6
+
7
+ class Settings(BaseSettings):
8
+ SWARMS_API_KEY: str = os.getenv("SWARMS_API_KEY", "")
9
+ FRED_API_KEY: str = os.getenv("FRED_API_KEY", "")
10
+ BASE_URL: str = "https://api.swarms.world"
11
+
12
+ # Nifty 50 symbols and names
13
+ NIFTY50_SYMBOLS: list = [
14
+ 'ADANIENT.NS', 'ADANIPORTS.NS', 'APOLLOHOSP.NS', 'ASIANPAINT.NS',
15
+ 'AXISBANK.NS', 'BAJAJ-AUTO.NS', 'BAJFINANCE.NS', 'BAJAJFINSV.NS',
16
+ 'BEL.NS', 'BHARTIARTL.NS', 'CIPLA.NS', 'COALINDIA.NS', 'DRREDDY.NS',
17
+ 'EICHERMOT.NS', 'GRASIM.NS', 'HCLTECH.NS', 'HDFCBANK.NS', 'HDFCLIFE.NS',
18
+ 'HEROMOTOCO.NS', 'HINDALCO.NS', 'HINDUNILVR.NS', 'ICICIBANK.NS',
19
+ 'INDUSINDBK.NS', 'INFY.NS', 'ITC.NS', 'JIOFIN.NS', 'JSWSTEEL.NS',
20
+ 'KOTAKBANK.NS', 'LT.NS', 'M&M.NS', 'MARUTI.NS', 'NESTLEIND.NS',
21
+ 'NTPC.NS', 'ONGC.NS', 'POWERGRID.NS', 'RELIANCE.NS', 'SBILIFE.NS',
22
+ 'SHRIRAMFIN.NS', 'SBIN.NS', 'SUNPHARMA.NS', 'TATACONSUM.NS', 'TCS.NS',
23
+ 'TATAMOTORS.NS', 'TATASTEEL.NS', 'TECHM.NS', 'TITAN.NS', 'TRENT.NS',
24
+ 'ULTRACEMCO.NS', 'WIPRO.NS'
25
+ ]
26
+
27
+ STOCK_NAMES: dict = {
28
+ 'ADANIENT.NS': 'Adani Enterprises', 'ADANIPORTS.NS': 'Adani Ports',
29
+ 'APOLLOHOSP.NS': 'Apollo Hospitals', 'ASIANPAINT.NS': 'Asian Paints',
30
+ 'AXISBANK.NS': 'Axis Bank', 'BAJAJ-AUTO.NS': 'Bajaj Auto',
31
+ 'BAJFINANCE.NS': 'Bajaj Finance', 'BAJAJFINSV.NS': 'Bajaj Finserv',
32
+ 'BEL.NS': 'Bharat Electronics', 'BHARTIARTL.NS': 'Bharti Airtel',
33
+ 'CIPLA.NS': 'Cipla', 'COALINDIA.NS': 'Coal India', 'DRREDDY.NS': 'Dr Reddy Labs',
34
+ 'EICHERMOT.NS': 'Eicher Motors', 'GRASIM.NS': 'Grasim Industries',
35
+ 'HCLTECH.NS': 'HCL Technologies', 'HDFCBANK.NS': 'HDFC Bank',
36
+ 'HDFCLIFE.NS': 'HDFC Life', 'HEROMOTOCO.NS': 'Hero MotoCorp',
37
+ 'HINDALCO.NS': 'Hindalco', 'HINDUNILVR.NS': 'Hindustan Unilever',
38
+ 'ICICIBANK.NS': 'ICICI Bank', 'INDUSINDBK.NS': 'IndusInd Bank',
39
+ 'INFY.NS': 'Infosys', 'ITC.NS': 'ITC', 'JIOFIN.NS': 'Jio Financial',
40
+ 'JSWSTEEL.NS': 'JSW Steel', 'KOTAKBANK.NS': 'Kotak Mahindra Bank',
41
+ 'LT.NS': 'Larsen & Toubro', 'M&M.NS': 'Mahindra & Mahindra',
42
+ 'MARUTI.NS': 'Maruti Suzuki', 'NESTLEIND.NS': 'Nestle India',
43
+ 'NTPC.NS': 'NTPC', 'ONGC.NS': 'ONGC', 'POWERGRID.NS': 'Power Grid Corp',
44
+ 'RELIANCE.NS': 'Reliance Industries', 'SBILIFE.NS': 'SBI Life',
45
+ 'SHRIRAMFIN.NS': 'Shriram Finance', 'SBIN.NS': 'State Bank of India',
46
+ 'SUNPHARMA.NS': 'Sun Pharma', 'TATACONSUM.NS': 'Tata Consumer',
47
+ 'TCS.NS': 'Tata Consultancy Services', 'TATAMOTORS.NS': 'Tata Motors',
48
+ 'TATASTEEL.NS': 'Tata Steel', 'TECHM.NS': 'Tech Mahindra',
49
+ 'TITAN.NS': 'Titan Company', 'TRENT.NS': 'Trent',
50
+ 'ULTRACEMCO.NS': 'UltraTech Cement', 'WIPRO.NS': 'Wipro'
51
+ }
52
+
53
+ # Agent prompts
54
+ INVESTMENT_MANAGER_PROMPT: str = """
55
+ You are an experienced Indian Investment Portfolio Manager, responsible for coordinating a team of specialized financial AI agents to provide comprehensive investment recommendations for Indian equity markets.
56
+ Your responsibilities include:
57
+ 1. Analyzing Indian portfolios and investment objectives in the context of NSE/BSE markets
58
+ 2. Routing specific questions to appropriate specialist agents
59
+ 3. Synthesizing inputs from various specialists into coherent recommendations
60
+ 4. Ensuring recommendations follow sound investment principles for Indian markets
61
+ 5. Considering Indian market factors: RBI policy, monsoons, FII/DII flows, government policies
62
+ 6. Prioritizing recommendations based on client objectives and Indian market conditions
63
+ Always consider Indian market context:
64
+ - RBI repo rate, inflation, and monetary policy
65
+ - Sectoral performance and rotation in Indian markets
66
+ - Tax implications (LTCG/STCG, dividend tax)
67
+ - Currency (INR) considerations
68
+ - Government policy impacts and budget announcements
69
+ Your final recommendations should include:
70
+ - Clear assessment of current portfolio in Indian market context
71
+ - Prioritized investment recommendations with rationale
72
+ - Asset allocation adjustments with specific percentages
73
+ - Risk management considerations for Indian market volatility
74
+ - Implementation timeline suitable for Indian markets
75
+ """
76
+
77
+ ASSET_ALLOCATION_PROMPT: str = """
78
+ You are an experienced Indian asset allocation specialist with deep knowledge of Indian equity markets, portfolio construction for NSE/BSE stocks, and Modern Portfolio Theory adapted for Indian markets.
79
+ Your responsibilities include:
80
+ 1. Analyzing current portfolio allocations across Indian sectors and market caps
81
+ 2. Assessing portfolio efficiency and diversification within Indian markets
82
+ 3. Recommending optimal asset allocation for Indian equity portfolios
83
+ 4. Balancing risk and return expectations in Indian market context
84
+ 5. Considering sector rotation opportunities in Indian markets
85
+ When analyzing Indian portfolios:
86
+ - Evaluate allocation across sectors (IT, Banking, Pharma, Auto, etc.)
87
+ - Assess market cap distribution (Large, Mid, Small cap)
88
+ - Consider geographical exposure (domestic vs export-oriented companies)
89
+ - Analyze correlation with Indian market indices (Nifty, Sensex)
90
+ - Recommend specific allocation changes with percentage targets
91
+ - Consider Indian market liquidity and trading volumes
92
+ Base recommendations on Indian market characteristics and regulatory environment.
93
+ """
94
+
95
+ MARKET_ANALYST_PROMPT: str = """
96
+ You are an experienced Indian market analyst with expertise in Indian economic cycles, government policies, and NSE/BSE market trends.
97
+ Your responsibilities include:
98
+ 1. Analyzing Indian economic indicators and their market impact
99
+ 2. Identifying current Indian market trends and sector rotation opportunities
100
+ 3. Assessing valuation levels across Indian sectors and stocks
101
+ 4. Recognizing potential risks and catalysts in Indian markets
102
+ 5. Providing outlook for different Indian sectors and market segments
103
+ Focus on Indian market specific factors:
104
+ - RBI monetary policy and interest rate cycle
105
+ - Government policies, budget announcements, and reforms
106
+ - Monsoon predictions and agricultural sector impact
107
+ - FII/DII investment flows and market sentiment
108
+ - Corporate earnings growth and sector performance
109
+ - Currency (INR) movements and their sector impact
110
+ Provide actionable insights for Indian equity investors.
111
+ """
112
+
113
+ RISK_ANALYST_PROMPT: str = """
114
+ You are an expert in Indian market risk analysis with extensive knowledge of Indian equity market risks, volatility patterns, and risk management strategies.
115
+ Your responsibilities include:
116
+ 1. Identifying and quantifying risks specific to Indian equity portfolios
117
+ 2. Analyzing portfolio vulnerability to Indian market scenarios
118
+ 3. Recommending risk mitigation strategies for Indian market conditions
119
+ 4. Assessing concentration risks in Indian portfolios
120
+ 5. Evaluating currency and regulatory risks
121
+ Focus on Indian market risks:
122
+ - High volatility and market sentiment swings
123
+ - Sector concentration and diversification needs
124
+ - Regulatory risks from SEBI, RBI, and government policy changes
125
+ - Liquidity risks in mid and small-cap stocks
126
+ - Currency (INR) volatility impact on different sectors
127
+ - Political and policy uncertainty effects
128
+ Recommend practical risk management approaches suitable for Indian market conditions.
129
+ """
130
+
131
+ FIXED_INCOME_PROMPT: str = """
132
+ You are an Indian fixed income specialist with expertise in Indian government bonds, corporate bonds, and fixed income strategies suitable for Indian investors.
133
+ Your responsibilities include:
134
+ 1. Analyzing fixed income opportunities in Indian markets
135
+ 2. Assessing interest rate risks in context of RBI policy
136
+ 3. Evaluating credit risks in Indian corporate bonds
137
+ 4. Recommending fixed income allocation for Indian portfolios
138
+ 5. Identifying opportunities in Indian government securities, corporate bonds, and debt mutual funds
139
+ Consider Indian fixed income landscape:
140
+ - RBI monetary policy and repo rate changes
141
+ - Government borrowing programs and bond issuances
142
+ - Corporate credit quality and rating changes
143
+ - Tax implications of different fixed income instruments
144
+ - Inflation expectations and real returns
145
+ Provide recommendations suitable for Indian regulatory and tax environment.
146
+ """
147
+
148
+ EQUITY_PROMPT: str = """
149
+ You are an Indian equity market specialist with deep expertise in NSE/BSE stocks, sector analysis, and Indian equity strategies.
150
+ Your responsibilities include:
151
+ 1. Analyzing Indian equity positioning and sector allocation
152
+ 2. Evaluating opportunities across market caps and sectors
153
+ 3. Identifying Indian equity market opportunities and risks
154
+ 4. Recommending equity strategies aligned with Indian market cycles
155
+ 5. Assessing valuation levels of Indian stocks and sectors
156
+ Focus on Indian equity characteristics:
157
+ - Sector rotation patterns in Indian markets
158
+ - Market cap preferences (Large/Mid/Small cap dynamics)
159
+ - Export vs domestic-oriented company performance
160
+ - Seasonal patterns and festival season effects
161
+ - Corporate governance and quality factors
162
+ - Dividend yields and growth characteristics
163
+ Base recommendations on Indian market cycles, sectoral trends, and corporate fundamentals.
164
+ """
165
+
166
+ class Config:
167
+ env_file = ".env"
168
+
169
+ settings = Settings()
app/core/exceptions.py ADDED
@@ -0,0 +1,21 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import HTTPException
2
+
3
+ class PortfolioException(HTTPException):
4
+ def __init__(self, status_code: int, detail: str):
5
+ super().__init__(status_code=status_code, detail=detail)
6
+
7
+ class DataFetchException(PortfolioException):
8
+ def __init__(self, detail: str):
9
+ super().__init__(status_code=500, detail=f"Data fetch error: {detail}")
10
+
11
+ class AnalysisException(PortfolioException):
12
+ def __init__(self, detail: str):
13
+ super().__init__(status_code=500, detail=f"Analysis error: {detail}")
14
+
15
+ class BacktestException(PortfolioException):
16
+ def __init__(self, detail: str):
17
+ super().__init__(status_code=500, detail=f"Backtest error: {detail}")
18
+
19
+ class SwarmException(PortfolioException):
20
+ def __init__(self, detail: str):
21
+ super().__init__(status_code=500, detail=f"Swarm analysis error: {detail}")
app/models/__init__.py ADDED
File without changes
app/models/portfolio.py ADDED
@@ -0,0 +1,32 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import Dict, List, Optional, Any
3
+ from datetime import datetime
4
+
5
+ class StockHolding(BaseModel):
6
+ symbol: str
7
+ name: str
8
+ quantity: int
9
+ current_price: float
10
+
11
+ class Portfolio(BaseModel):
12
+ holdings: List[StockHolding]
13
+ current_portfolio_value: float
14
+ annual_income: float
15
+ age: int
16
+ investment_horizon: str
17
+ risk_tolerance: str
18
+ investment_objective: str
19
+
20
+ class BacktestRequest(BaseModel):
21
+ holdings: Dict[str, int] # symbol: quantity
22
+ start_date: datetime
23
+ end_date: datetime
24
+ benchmark: str = "^NSEI"
25
+
26
+ class ClientProfile(BaseModel):
27
+ age: int
28
+ investment_horizon: str
29
+ risk_tolerance: str
30
+ investment_objective: str
31
+ annual_income: float
32
+ current_portfolio_value: float
app/models/response.py ADDED
@@ -0,0 +1,46 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from pydantic import BaseModel, Field
2
+ from typing import Dict, List, Optional, Any
3
+ from datetime import datetime
4
+
5
+ class EconomicDataResponse(BaseModel):
6
+ rbi_repo_rate: float
7
+ indian_inflation_rate: float
8
+ real_interest_rate: float
9
+
10
+ class MarketIndicesResponse(BaseModel):
11
+ indices: Dict[str, Dict[str, float]]
12
+
13
+ class StockDataResponse(BaseModel):
14
+ symbol: str
15
+ current_price: float
16
+ volume: float
17
+ returns_1y: float
18
+ volatility: float
19
+ market_cap: float
20
+ pe_ratio: float
21
+ dividend_yield: float
22
+
23
+ class PortfolioAnalysisResponse(BaseModel):
24
+ portfolio_value: float
25
+ number_of_holdings: int
26
+ sector_allocation: Dict[str, float]
27
+ holdings: List[Dict[str, Any]]
28
+
29
+ class BacktestResponse(BaseModel):
30
+ total_return: float
31
+ annual_return: float
32
+ max_drawdown: float
33
+ annual_volatility: float
34
+ sharpe_ratio: float
35
+ sortino_ratio: float
36
+ beta: float
37
+ alpha: float
38
+ information_ratio: float
39
+ portfolio_cumulative: List[Dict[str, Any]]
40
+ benchmark_cumulative: List[Dict[str, Any]]
41
+ drawdown: List[Dict[str, Any]]
42
+
43
+ class SwarmAnalysisResponse(BaseModel):
44
+ status: str
45
+ output: List[Dict[str, Any]]
46
+ error: Optional[str] = None
app/services/__init__.py ADDED
File without changes
app/services/backtester.py ADDED
@@ -0,0 +1,109 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import yfinance as yf
2
+ import pandas as pd
3
+ import numpy as np
4
+ from typing import Dict, Tuple, Any
5
+ from datetime import datetime
6
+ from app.core.exceptions import BacktestException
7
+
8
+ class AdvancedBacktester:
9
+ """Advanced portfolio backtesting"""
10
+
11
+ def backtest_portfolio(self, holdings: Dict[str, int], start_date: str, end_date: str, benchmark: str = '^NSEI') -> Tuple[Dict[str, Any], str]:
12
+ """Backtest portfolio performance"""
13
+ symbols = list(holdings.keys())
14
+
15
+ try:
16
+ # Fetch data for all symbols + benchmark
17
+ all_symbols = symbols + [benchmark]
18
+ data_dict = {}
19
+
20
+ for symbol in all_symbols:
21
+ ticker = yf.Ticker(symbol)
22
+ hist = ticker.history(start=start_date, end=end_date)
23
+ if not hist.empty:
24
+ data_dict[symbol] = hist['Close']
25
+
26
+ if not data_dict:
27
+ return None, "No data available for backtesting period"
28
+
29
+ # Convert to DataFrame
30
+ data = pd.DataFrame(data_dict)
31
+ data = data.dropna()
32
+
33
+ if data.empty:
34
+ return None, "No overlapping data available"
35
+
36
+ # Calculate portfolio weights based on initial investment
37
+ initial_prices = data.iloc[0][symbols]
38
+ total_investment = sum(holdings[symbol] * initial_prices[symbol] for symbol in symbols)
39
+ weights = {symbol: (holdings[symbol] * initial_prices[symbol]) / total_investment for symbol in symbols}
40
+
41
+ # Portfolio returns
42
+ returns = data.pct_change().dropna()
43
+ portfolio_returns = pd.Series(0, index=returns.index)
44
+
45
+ for symbol in symbols:
46
+ if symbol in returns.columns:
47
+ portfolio_returns += returns[symbol] * weights[symbol]
48
+
49
+ # Benchmark returns
50
+ benchmark_returns = returns[benchmark] if benchmark in returns.columns else pd.Series(0, index=returns.index)
51
+
52
+ # Calculate cumulative returns
53
+ portfolio_cumulative = (1 + portfolio_returns).cumprod()
54
+ benchmark_cumulative = (1 + benchmark_returns).cumprod()
55
+
56
+ # Calculate advanced metrics
57
+ annual_return = portfolio_returns.mean() * 252
58
+ annual_volatility = portfolio_returns.std() * np.sqrt(252)
59
+ sharpe_ratio = (annual_return - 0.05) / annual_volatility if annual_volatility > 0 else 0
60
+
61
+ # Maximum drawdown
62
+ running_max = portfolio_cumulative.expanding().max()
63
+ drawdown = (portfolio_cumulative - running_max) / running_max
64
+ max_drawdown = drawdown.min()
65
+
66
+ # Beta calculation
67
+ covariance = np.cov(portfolio_returns.dropna(), benchmark_returns.dropna())[0][1]
68
+ benchmark_variance = np.var(benchmark_returns.dropna())
69
+ beta = covariance / benchmark_variance if benchmark_variance > 0 else 0
70
+
71
+ # Alpha calculation
72
+ benchmark_annual_return = benchmark_returns.mean() * 252
73
+ alpha = annual_return - (0.05 + beta * (benchmark_annual_return - 0.05))
74
+
75
+ # Information ratio
76
+ active_returns = portfolio_returns - benchmark_returns
77
+ tracking_error = active_returns.std() * np.sqrt(252)
78
+ information_ratio = (annual_return - benchmark_annual_return) / tracking_error if tracking_error > 0 else 0
79
+
80
+ # Sortino ratio
81
+ downside_returns = portfolio_returns[portfolio_returns < 0]
82
+ downside_deviation = downside_returns.std() * np.sqrt(252) if len(downside_returns) > 0 else annual_volatility
83
+ sortino_ratio = (annual_return - 0.05) / downside_deviation if downside_deviation > 0 else 0
84
+
85
+ # Convert cumulative returns and drawdown to list of dicts for JSON serialization
86
+ portfolio_cumulative_list = [{'date': str(date), 'value': value} for date, value in portfolio_cumulative.items()]
87
+ benchmark_cumulative_list = [{'date': str(date), 'value': value} for date, value in benchmark_cumulative.items()]
88
+ drawdown_list = [{'date': str(date), 'value': value * 100} for date, value in drawdown.items()]
89
+
90
+ results = {
91
+ 'portfolio_cumulative': portfolio_cumulative_list,
92
+ 'benchmark_cumulative': benchmark_cumulative_list,
93
+ 'drawdown': drawdown_list,
94
+ 'annual_return': annual_return * 100,
95
+ 'annual_volatility': annual_volatility * 100,
96
+ 'sharpe_ratio': sharpe_ratio,
97
+ 'sortino_ratio': sortino_ratio,
98
+ 'max_drawdown': max_drawdown * 100,
99
+ 'beta': beta,
100
+ 'alpha': alpha * 100,
101
+ 'information_ratio': information_ratio,
102
+ 'total_return': (portfolio_cumulative.iloc[-1] - 1) * 100,
103
+ 'benchmark_return': (benchmark_cumulative.iloc[-1] - 1) * 100
104
+ }
105
+
106
+ return results, None
107
+
108
+ except Exception as e:
109
+ return None, str(e)
app/services/data_fetcher.py ADDED
@@ -0,0 +1,125 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ import yfinance as yf
3
+ import pandas as pd
4
+ import numpy as np
5
+ from typing import Dict, List, Optional
6
+ from app.core.config import settings
7
+ from app.core.exceptions import DataFetchException
8
+
9
+ class EconomicDataFetcher:
10
+ """Fetch economic data from FRED API"""
11
+
12
+ def __init__(self):
13
+ self.fred_api_key = settings.FRED_API_KEY
14
+ self.base_url = "https://api.stlouisfed.org/fred/series/observations"
15
+
16
+ def get_rbi_repo_rate(self) -> float:
17
+ """Fetch RBI repo rate from FRED"""
18
+ try:
19
+ params = {
20
+ 'series_id': 'INTDSRINM193N', # India Interest Rate
21
+ 'api_key': self.fred_api_key,
22
+ 'file_type': 'json',
23
+ 'sort_order': 'desc',
24
+ 'limit': 1
25
+ }
26
+
27
+ response = requests.get(self.base_url, params=params)
28
+ data = response.json()
29
+
30
+ if 'observations' in data and len(data['observations']) > 0:
31
+ return float(data['observations'][0]['value'])
32
+ else:
33
+ return 6.5 # Default fallback value
34
+ except Exception as e:
35
+ raise DataFetchException(f"Could not fetch repo rate from FRED: {str(e)}")
36
+
37
+ def get_indian_inflation_rate(self) -> float:
38
+ """Fetch Indian inflation rate from FRED"""
39
+ try:
40
+ params = {
41
+ 'series_id': 'FPCPITOTLZGIND',
42
+ 'api_key': self.fred_api_key,
43
+ 'file_type': 'json',
44
+ 'sort_order': 'desc',
45
+ 'limit': 1
46
+ }
47
+
48
+ response = requests.get(self.base_url, params=params)
49
+ data = response.json()
50
+
51
+ if 'observations' in data and len(data['observations']) > 0:
52
+ return float(data['observations'][0]['value'])
53
+ else:
54
+ return 5.7 # Default fallback value
55
+ except Exception as e:
56
+ raise DataFetchException(f"Could not fetch inflation rate from FRED: {str(e)}")
57
+
58
+ class PortfolioDataFetcher:
59
+ """Fetch real-time portfolio and market data"""
60
+
61
+ def __init__(self):
62
+ self.economic_fetcher = EconomicDataFetcher()
63
+ self.rbi_repo_rate = self.economic_fetcher.get_rbi_repo_rate()
64
+ self.indian_inflation_rate = self.economic_fetcher.get_indian_inflation_rate()
65
+
66
+ def get_stock_data(self, symbols: List[str], period: str = "1y") -> Dict[str, Dict]:
67
+ """Fetch stock data for analysis"""
68
+ stock_data = {}
69
+ for symbol in symbols:
70
+ try:
71
+ ticker = yf.Ticker(symbol)
72
+ hist = ticker.history(period=period)
73
+ info = ticker.info
74
+
75
+ if not hist.empty:
76
+ # Calculate additional metrics
77
+ returns_1y = ((hist['Close'].iloc[-1] - hist['Close'].iloc[0]) / hist['Close'].iloc[0]) * 100 if len(hist) > 252 else 0
78
+ volatility = hist['Close'].pct_change().std() * np.sqrt(252) * 100
79
+
80
+ stock_data[symbol] = {
81
+ 'history': hist,
82
+ 'info': info,
83
+ 'current_price': hist['Close'].iloc[-1],
84
+ 'volume': hist['Volume'].iloc[-1],
85
+ 'returns_1y': returns_1y,
86
+ 'volatility': volatility,
87
+ 'market_cap': info.get('marketCap', 0),
88
+ 'pe_ratio': info.get('forwardPE', info.get('trailingPE', 'N/A')),
89
+ 'dividend_yield': info.get('dividendYield', 0) * 100 if info.get('dividendYield') else 0
90
+ }
91
+ except Exception as e:
92
+ raise DataFetchException(f"Error fetching {symbol}: {str(e)}")
93
+ return stock_data
94
+
95
+ def get_market_indices(self) -> Dict[str, Dict]:
96
+ """Fetch Indian market indices data"""
97
+ indices = {
98
+ '^NSEI': 'Nifty 50',
99
+ '^BSESN': 'BSE Sensex',
100
+ '^NSEBANK': 'Nifty Bank',
101
+ '^CNXIT': 'Nifty IT',
102
+ }
103
+
104
+ indices_data = {}
105
+ for symbol, name in indices.items():
106
+ try:
107
+ ticker = yf.Ticker(symbol)
108
+ hist = ticker.history(period="5d")
109
+
110
+ if not hist.empty:
111
+ current_price = hist['Close'].iloc[-1]
112
+ previous_close = hist['Close'].iloc[-2] if len(hist) > 1 else current_price
113
+ change = current_price - previous_close
114
+ change_pct = (change / previous_close) * 100
115
+
116
+ indices_data[name] = {
117
+ 'current_price': current_price,
118
+ 'change': change,
119
+ 'change_pct': change_pct,
120
+ 'volume': hist['Volume'].iloc[-1] if 'Volume' in hist else 0
121
+ }
122
+ except Exception as e:
123
+ raise DataFetchException(f"Could not fetch data for {name}: {str(e)}")
124
+
125
+ return indices_data
app/services/portfolio_analyzer.py ADDED
@@ -0,0 +1,91 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import pandas as pd
2
+ import numpy as np
3
+ from typing import Dict, List
4
+ from app.services.data_fetcher import EconomicDataFetcher
5
+ from app.core.exceptions import AnalysisException
6
+
7
+ class PortfolioAnalyzer:
8
+ """Analyze portfolio performance and risk"""
9
+
10
+ def __init__(self):
11
+ self.economic_fetcher = EconomicDataFetcher()
12
+ self.risk_free_rate = self.economic_fetcher.get_rbi_repo_rate()
13
+
14
+ def calculate_portfolio_metrics(self, weights, returns_df):
15
+ """Calculate portfolio metrics"""
16
+ portfolio_returns = (returns_df * weights).sum(axis=1)
17
+ expected_return = portfolio_returns.mean() * 252
18
+ volatility = portfolio_returns.std() * np.sqrt(252)
19
+ sharpe_ratio = (expected_return - self.risk_free_rate) / volatility if volatility > 0 else 0
20
+
21
+ return {
22
+ 'expected_return': expected_return,
23
+ 'volatility': volatility,
24
+ 'sharpe_ratio': sharpe_ratio,
25
+ 'cumulative_return': (1 + portfolio_returns).prod() - 1
26
+ }
27
+
28
+ def calculate_portfolio_value(self, holdings: Dict[str, Dict]) -> float:
29
+ """Calculate total portfolio value"""
30
+ total_value = 0
31
+ for symbol, data in holdings.items():
32
+ total_value += data['quantity'] * data['current_price']
33
+ return total_value
34
+
35
+ def get_sector_allocation(self, holdings: Dict[str, Dict]) -> Dict[str, float]:
36
+ """Get sector allocation (simplified)"""
37
+ # In a real implementation, you would map stocks to sectors
38
+ sectors = {
39
+ 'IT': ['TCS.NS', 'INFY.NS', 'HCLTECH.NS', 'TECHM.NS'],
40
+ 'Banking': ['HDFCBANK.NS', 'ICICIBANK.NS', 'KOTAKBANK.NS', 'AXISBANK.NS', 'SBIN.NS'],
41
+ 'FMCG': ['HINDUNILVR.NS', 'NESTLEIND.NS', 'ITC.NS', 'TITAN.NS'],
42
+ 'Automobile': ['MARUTI.NS', 'TATAMOTORS.NS', 'M&M.NS', 'HEROMOTOCO.NS', 'BAJAJ-AUTO.NS'],
43
+ 'Pharma': ['SUNPHARMA.NS', 'DRREDDY.NS', 'CIPLA.NS'],
44
+ 'Energy': ['RELIANCE.NS', 'NTPC.NS', 'ONGC.NS', 'COALINDIA.NS'],
45
+ 'Metals': ['TATASTEEL.NS', 'HINDALCO.NS', 'JSWSTEEL.NS'],
46
+ 'Telecom': ['BHARTIARTL.NS'],
47
+ 'Consumer Services': ['TRENT.NS', 'ASIANPAINT.NS'],
48
+ 'Capital Goods': ['LT.NS', 'ULTRACEMCO.NS', 'GRASIM.NS'],
49
+ 'Financial Services': ['BAJFINANCE.NS', 'BAJAJFINSV.NS', 'HDFCLIFE.NS', 'SBILIFE.NS', 'INDUSINDBK.NS'],
50
+ 'Others': []
51
+ }
52
+
53
+ sector_weights = {}
54
+ total_value = self.calculate_portfolio_value(holdings)
55
+
56
+ for sector, stocks in sectors.items():
57
+ sector_value = 0
58
+ for stock in stocks:
59
+ if stock in holdings:
60
+ sector_value += holdings[stock]['quantity'] * holdings[stock]['current_price']
61
+ sector_weights[sector] = (sector_value / total_value) * 100 if total_value > 0 else 0
62
+
63
+ return sector_weights
64
+
65
+ def analyze_portfolio(self, holdings: Dict[str, Dict], current_portfolio_value: float) -> Dict:
66
+ """Perform complete portfolio analysis"""
67
+ try:
68
+ portfolio_value = self.calculate_portfolio_value(holdings)
69
+ sector_allocation = self.get_sector_allocation(holdings)
70
+
71
+ # Prepare holdings data for response
72
+ holdings_list = []
73
+ for symbol, data in holdings.items():
74
+ holdings_list.append({
75
+ 'Stock': data['name'],
76
+ 'Symbol': symbol,
77
+ 'Quantity': data['quantity'],
78
+ 'Current Price': f"₹{data['current_price']:.2f}",
79
+ 'Value': f"₹{data['quantity'] * data['current_price']:,.0f}",
80
+ 'Allocation': f"{(data['quantity'] * data['current_price'] / portfolio_value) * 100:.1f}%"
81
+ })
82
+
83
+ return {
84
+ 'portfolio_value': portfolio_value,
85
+ 'number_of_holdings': len(holdings),
86
+ 'sector_allocation': sector_allocation,
87
+ 'holdings': holdings_list,
88
+ 'portfolio_vs_benchmark': ((portfolio_value/current_portfolio_value - 1) * 100)
89
+ }
90
+ except Exception as e:
91
+ raise AnalysisException(f"Portfolio analysis failed: {str(e)}")
app/services/swarm_service.py ADDED
@@ -0,0 +1,89 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ import requests
2
+ from typing import Dict, Any
3
+ from app.core.config import settings
4
+ from app.core.exceptions import SwarmException
5
+
6
+ def create_portfolio_swarm(portfolio_data: str, client_profile: str, economic_context: str) -> Dict[str, Any]:
7
+ """Create swarm for portfolio analysis"""
8
+
9
+ headers = {
10
+ "x-api-key": settings.SWARMS_API_KEY,
11
+ "Content-Type": "application/json"
12
+ }
13
+
14
+ swarm_config = {
15
+ "name": "Investment Portfolio Analysis Swarm",
16
+ "description": "AI swarm specialized for Indian investment portfolio analysis",
17
+ "agents": [
18
+ {
19
+ "agent_name": "Asset Allocation Specialist",
20
+ "system_prompt": settings.ASSET_ALLOCATION_PROMPT,
21
+ "model_name": "gpt-4o",
22
+ "role": "worker",
23
+ "max_loops": 1,
24
+ "max_tokens": 4096,
25
+ "temperature": 0.5,
26
+ },
27
+ {
28
+ "agent_name": "Market Analyst",
29
+ "system_prompt": settings.MARKET_ANALYST_PROMPT,
30
+ "model_name": "gpt-4o",
31
+ "role": "worker",
32
+ "max_loops": 1,
33
+ "max_tokens": 4096,
34
+ "temperature": 0.5,
35
+ },
36
+ {
37
+ "agent_name": "Risk Analyst",
38
+ "system_prompt": settings.RISK_ANALYST_PROMPT,
39
+ "model_name": "gpt-4o",
40
+ "role": "worker",
41
+ "max_loops": 1,
42
+ "max_tokens": 4096,
43
+ "temperature": 0.5,
44
+ },
45
+ {
46
+ "agent_name": "Fixed Income Specialist",
47
+ "system_prompt": settings.FIXED_INCOME_PROMPT,
48
+ "model_name": "gpt-4o",
49
+ "role": "worker",
50
+ "max_loops": 1,
51
+ "max_tokens": 4096,
52
+ "temperature": 0.5,
53
+ },
54
+ {
55
+ "agent_name": "Equity Specialist",
56
+ "system_prompt": settings.EQUITY_PROMPT,
57
+ "model_name": "gpt-4o",
58
+ "role": "worker",
59
+ "max_loops": 2,
60
+ "max_tokens": 4096,
61
+ "temperature": 0.5,
62
+ },
63
+ {
64
+ "agent_name": "Investment Manager",
65
+ "system_prompt": settings.INVESTMENT_MANAGER_PROMPT,
66
+ "model_name": "gpt-4o",
67
+ "role": "worker",
68
+ "max_loops": 1,
69
+ "max_tokens": 4096,
70
+ "temperature": 0.3,
71
+ },
72
+ ],
73
+ "max_loops": 2,
74
+ "swarm_type": "ConcurrentWorkflow",
75
+ "task": f"""Analyze the following Indian investment portfolio:\n\n{portfolio_data}\n\n
76
+ Client Profile:\n{client_profile}\n\n
77
+ Economic Context:\n{economic_context}"""
78
+ }
79
+
80
+ try:
81
+ response = requests.post(
82
+ f"{settings.BASE_URL}/v1/swarm/completions",
83
+ headers=headers,
84
+ json=swarm_config,
85
+ timeout=120
86
+ )
87
+ return response.json()
88
+ except Exception as e:
89
+ raise SwarmException(f"Swarm analysis failed: {str(e)}")
main.py ADDED
@@ -0,0 +1,66 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ from fastapi import FastAPI, Request
2
+ from fastapi.middleware.cors import CORSMiddleware
3
+ from fastapi.responses import JSONResponse
4
+ from fastapi.staticfiles import StaticFiles
5
+ import time
6
+ import logging
7
+
8
+ from app.api.routes import router
9
+ from app.core.exceptions import PortfolioException
10
+
11
+ # Configure logging
12
+ logging.basicConfig(level=logging.INFO)
13
+ logger = logging.getLogger(__name__)
14
+
15
+ app = FastAPI(
16
+ title="Investment Portfolio Decision Support System API",
17
+ description="API for AI-powered portfolio analysis for Indian markets",
18
+ version="1.0.0"
19
+ )
20
+
21
+ # Add CORS middleware
22
+ app.add_middleware(
23
+ CORSMiddleware,
24
+ allow_origins=["*"], # Adjust this in production
25
+ allow_credentials=True,
26
+ allow_methods=["*"],
27
+ allow_headers=["*"],
28
+ )
29
+
30
+ # Add request logging middleware
31
+ @app.middleware("http")
32
+ async def log_requests(request: Request, call_next):
33
+ start_time = time.time()
34
+ response = await call_next(request)
35
+ process_time = time.time() - start_time
36
+ logger.info(f"Request: {request.method} {request.url} - Completed in {process_time:.4f}s")
37
+ return response
38
+
39
+ # Exception handler
40
+ @app.exception_handler(PortfolioException)
41
+ async def portfolio_exception_handler(request: Request, exc: PortfolioException):
42
+ return JSONResponse(
43
+ status_code=exc.status_code,
44
+ content={"message": exc.detail}
45
+ )
46
+
47
+ # Include API routes
48
+ app.include_router(router, prefix="/api/v1")
49
+
50
+ # Health check endpoint
51
+ @app.get("/health")
52
+ async def health_check():
53
+ return {"status": "healthy", "message": "API is running"}
54
+
55
+ # Root endpoint
56
+ @app.get("/")
57
+ async def root():
58
+ return {
59
+ "message": "Investment Portfolio Decision Support System API",
60
+ "version": "1.0.0",
61
+ "docs": "/docs"
62
+ }
63
+
64
+ if __name__ == "__main__":
65
+ import uvicorn
66
+ uvicorn.run("main:app", host="0.0.0.0", port=8000, reload=True)