Spaces:
Running
Running
Upload 18 files
Browse files- .env +2 -0
- .gitignore +1 -0
- app/__init__.py +0 -0
- app/api/__init__.py +0 -0
- app/api/dependencies.py +17 -0
- app/api/routes.py +161 -0
- app/core/__init__.py +0 -0
- app/core/config.py +169 -0
- app/core/exceptions.py +21 -0
- app/models/__init__.py +0 -0
- app/models/portfolio.py +32 -0
- app/models/response.py +46 -0
- app/services/__init__.py +0 -0
- app/services/backtester.py +109 -0
- app/services/data_fetcher.py +125 -0
- app/services/portfolio_analyzer.py +91 -0
- app/services/swarm_service.py +89 -0
- main.py +66 -0
.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)
|