Spaces:
Sleeping
Sleeping
Upload 53 files
Browse filesThis view is limited to 50 files because it contains too many changes.
See raw diff
- Reference/aboutdata.txt +118 -0
- Reference/basemodel.py +46 -0
- Reference/basic_portfolio_model.pkl +3 -0
- Reference/enhanced_portfolio_model.pkl +3 -0
- Reference/enhancedmodel.py +56 -0
- Reference/rulebasedmodel.py +314 -0
- app/__init__.py +1 -0
- app/__pycache__/__init__.cpython-312.pyc +0 -0
- app/__pycache__/admin.cpython-312.pyc +0 -0
- app/__pycache__/auth.cpython-312.pyc +0 -0
- app/__pycache__/crud.cpython-312.pyc +0 -0
- app/__pycache__/database.cpython-312.pyc +0 -0
- app/__pycache__/main.cpython-312.pyc +0 -0
- app/__pycache__/models.cpython-312.pyc +0 -0
- app/__pycache__/schemas.cpython-312.pyc +0 -0
- app/__pycache__/user.cpython-312.pyc +0 -0
- app/admin.py +73 -0
- app/auth.py +99 -0
- app/core/__pycache__/security.cpython-312.pyc +0 -0
- app/core/security.py +40 -0
- app/crud.py +48 -0
- app/database.py +26 -0
- app/main.py +146 -0
- app/ml_models/basic_portfolio_model.pkl +3 -0
- app/ml_models/enhanced_portfolio_model.pkl +3 -0
- app/ml_models/enhanced_portfolio_model111.pkl +3 -0
- app/ml_models/financial_advisor.db +0 -0
- app/models.py +28 -0
- app/schemas.py +139 -0
- app/services/__pycache__/chatbot_service.cpython-312.pyc +0 -0
- app/services/__pycache__/data_service.cpython-312.pyc +0 -0
- app/services/chatbot_service.py +312 -0
- app/services/data_service.py +129 -0
- app/static/css/custom_styles.css +0 -0
- app/static/css/style.css +112 -0
- app/static/js/script.js +61 -0
- app/templates/admin/dashboard.html +175 -0
- app/templates/admin/user_details.html +170 -0
- app/templates/auth/login.html +133 -0
- app/templates/auth/signup.html +121 -0
- app/templates/partials/base.html +30 -0
- app/templates/partials/footer.html +6 -0
- app/templates/partials/navbar.html +44 -0
- app/templates/user/chatbot.html +362 -0
- app/templates/user/homepage.html +137 -0
- app/templates/user/recommendations.html +240 -0
- app/tickers.txt +2099 -0
- app/user.py +227 -0
- app/user_financial_data.csv +14 -0
- diagramcontent.md +54 -0
Reference/aboutdata.txt
ADDED
|
@@ -0,0 +1,118 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
Columns in the dataset:
|
| 2 |
+
['Name', 'Age', 'City', 'Profession', 'Salary', 'Expenses', 'Savings', 'Lifecycle Stage', 'Risk Appetite', 'Investment Horizon', 'Equity (%)', 'Debt (%)', 'Gold (%)', 'FD/Cash (%)']
|
| 3 |
+
|
| 4 |
+
Preview of the data:
|
| 5 |
+
Name Age City Profession Salary Expenses Savings \
|
| 6 |
+
0 Rohan Sharma 28 Mumbai Software Engineer 85000 65000 20000
|
| 7 |
+
1 Priya Patel 45 Delhi Doctor 220000 150000 70000
|
| 8 |
+
2 Arjun Singh 60 Bangalore Retired Banker 45000 40000 5000
|
| 9 |
+
3 Anika Reddy 22 Hyderabad Student 15000 14000 1000
|
| 10 |
+
4 Vikram Mehta 35 Pune Marketing Manager 140000 100000 40000
|
| 11 |
+
|
| 12 |
+
Lifecycle Stage Risk Appetite Investment Horizon Equity (%) Debt (%) \
|
| 13 |
+
0 Early Career Medium Long-term 45 25
|
| 14 |
+
1 Mid-Career Medium Medium-term 40 30
|
| 15 |
+
2 Retired Low Short-term 10 40
|
| 16 |
+
3 Student Low Short-term 0 30
|
| 17 |
+
4 Early Career High Long-term 70 15
|
| 18 |
+
|
| 19 |
+
Gold (%) FD/Cash (%)
|
| 20 |
+
0 10 20
|
| 21 |
+
1 10 20
|
| 22 |
+
2 15 35
|
| 23 |
+
3 5 65
|
| 24 |
+
4 5 10
|
| 25 |
+
|
| 26 |
+
Data types:
|
| 27 |
+
Name object
|
| 28 |
+
Age int64
|
| 29 |
+
City object
|
| 30 |
+
Profession object
|
| 31 |
+
Salary int64
|
| 32 |
+
Expenses int64
|
| 33 |
+
Savings int64
|
| 34 |
+
Lifecycle Stage object
|
| 35 |
+
Risk Appetite object
|
| 36 |
+
Investment Horizon object
|
| 37 |
+
Equity (%) int64
|
| 38 |
+
Debt (%) int64
|
| 39 |
+
Gold (%) int64
|
| 40 |
+
FD/Cash (%) int64
|
| 41 |
+
dtype: object
|
| 42 |
+
|
| 43 |
+
Unique values in 'City':
|
| 44 |
+
['Mumbai' 'Delhi' 'Bangalore' 'Hyderabad' 'Pune' 'Chennai' 'Jaipur'
|
| 45 |
+
'Kochi' 'Kolkata' 'Ahmedabad' 'Gurgaon' 'Lucknow' 'Nagpur' 'Chandigarh'
|
| 46 |
+
'Surat' 'Indore' 'Bhopal']
|
| 47 |
+
|
| 48 |
+
Unique values in 'Profession':
|
| 49 |
+
['Software Engineer' 'Doctor' 'Retired Banker' 'Student'
|
| 50 |
+
'Marketing Manager' 'Teacher' 'Freelancer' 'Architect' 'Business Owner'
|
| 51 |
+
'Nurse' 'Product Manager' 'CA' 'Data Analyst' 'Retired Professor'
|
| 52 |
+
'Journalist' 'Graphic Designer' 'Sales Executive' 'HR Manager' 'Intern'
|
| 53 |
+
'Dentist' 'Lawyer' 'Content Creator' 'Pensioner' 'UX Designer'
|
| 54 |
+
'Government Employee' 'Fashion Designer' 'Startup Founder'
|
| 55 |
+
'Homemaker (Investor)' 'Investment Banker' 'IT Consultant'
|
| 56 |
+
'College Student' 'Pharmacist' 'Textile Business Owner' 'Event Planner'
|
| 57 |
+
'Film Producer' 'Nutritionist' 'Retired Army Officer'
|
| 58 |
+
'Social Media Manager' 'Airline Pilot' 'Biotech Researcher'
|
| 59 |
+
'Real Estate Agent' 'AI Engineer' 'Retired Teacher' 'Financial Analyst'
|
| 60 |
+
'Software Developer' 'NGO Director' 'Fitness Trainer' 'Digital Marketer'
|
| 61 |
+
'Content Strategist' 'Retired Engineer' 'UI Developer'
|
| 62 |
+
'Operations Manager' 'Corporate Lawyer' 'School Principal'
|
| 63 |
+
'AI Researcher' 'Junior Doctor' 'Finance Manager' 'Fashion Blogger'
|
| 64 |
+
'HR Consultant' 'Video Editor' 'Small Business Owner' 'Dietitian'
|
| 65 |
+
'Supply Chain Manager' 'Interior Designer' 'Sales Manager' 'PR Executive'
|
| 66 |
+
'Logistics Head' 'Content Writer' 'Retired Govt. Employee'
|
| 67 |
+
'Marketing Lead' 'IT Manager' 'Startup Intern' 'Export Manager'
|
| 68 |
+
'Fitness Instructor' 'Real Estate Broker' 'Event Manager'
|
| 69 |
+
'Software Trainee' 'Data Scientist' 'HR Director' 'Hotel Manager'
|
| 70 |
+
'Social Worker' 'Cybersecurity Expert' 'E-commerce Manager'
|
| 71 |
+
'Bank Manager' 'Retired IT Manager' 'UX Researcher' 'Product Designer'
|
| 72 |
+
'Cybersecurity Analyst' 'Podcast Producer' 'E-commerce Seller'
|
| 73 |
+
'Cloud Architect' 'Corporate Trainer' 'AI Trainer' 'Supply Chain Head'
|
| 74 |
+
'Product Owner' 'UI/UX Designer' 'Finance Director'
|
| 75 |
+
'Sustainability Consultant' 'Retired Bank Manager'
|
| 76 |
+
'Social Media Influencer' 'Blockchain Developer' 'NGO Head'
|
| 77 |
+
'Data Engineer' 'Event Curator' 'CTO' 'Content Marketer'
|
| 78 |
+
'Retired Army Major' 'AR/VR Developer' 'Logistics Manager' 'VP Sales'
|
| 79 |
+
'EdTech Founder' 'SEO Specialist' 'Yoga Instructor' 'IT Director'
|
| 80 |
+
'App Developer' 'Consultant Cardiologist' 'Freelance Writer'
|
| 81 |
+
'Cloud Engineer' 'Retired CA' 'Digital Artist' 'Retired Pilot'
|
| 82 |
+
'Graphic Animator' 'AI Ethicist' 'School Trustee' 'Robotics Engineer'
|
| 83 |
+
'Fashion Stylist' 'Retired Nurse' 'AI Ethics Consultant' 'Drone Engineer'
|
| 84 |
+
'Retired Bank Clerk' 'Digital Nomad' 'CFO' 'Sustainability Analyst'
|
| 85 |
+
'Retired Journalist' 'Social Entrepreneur' 'DevOps Engineer'
|
| 86 |
+
'School Counselor' 'Retired Army Colonel' 'AR Developer' 'Sales Director'
|
| 87 |
+
'EdTech Consultant' 'SEO Expert' 'Cardiologist' '3D Artist' 'HR Head'
|
| 88 |
+
'Animator' 'Fashion Influencer']
|
| 89 |
+
|
| 90 |
+
Unique values in 'Lifecycle Stage':
|
| 91 |
+
['Early Career' 'Mid-Career' 'Retired' 'Student' 'Late Career']
|
| 92 |
+
|
| 93 |
+
Unique values in 'Risk Appetite':
|
| 94 |
+
['Medium' 'Low' 'High']
|
| 95 |
+
|
| 96 |
+
Unique values in 'Investment Horizon':
|
| 97 |
+
['Long-term' 'Medium-term' 'Short-term']
|
| 98 |
+
|
| 99 |
+
Numerical columns summary:
|
| 100 |
+
Salary Expenses Savings Equity (%) Debt (%) \
|
| 101 |
+
count 198.000000 198.000000 198.000000 198.000000 198.000000
|
| 102 |
+
mean 115075.757576 84686.868687 30388.888889 39.469697 28.308081
|
| 103 |
+
std 69388.842695 48499.494260 21425.713424 20.713089 11.534149
|
| 104 |
+
min 8000.000000 7500.000000 500.000000 0.000000 10.000000
|
| 105 |
+
25% 60000.000000 48000.000000 10000.000000 25.000000 20.000000
|
| 106 |
+
50% 95000.000000 70000.000000 25000.000000 45.000000 25.000000
|
| 107 |
+
75% 163750.000000 118750.000000 45000.000000 50.000000 30.000000
|
| 108 |
+
max 310000.000000 230000.000000 90000.000000 70.000000 55.000000
|
| 109 |
+
|
| 110 |
+
Gold (%) FD/Cash (%)
|
| 111 |
+
count 198.000000 198.000000
|
| 112 |
+
mean 8.434343 23.787879
|
| 113 |
+
std 3.427751 16.426902
|
| 114 |
+
min 0.000000 10.000000
|
| 115 |
+
25% 5.000000 15.000000
|
| 116 |
+
50% 10.000000 20.000000
|
| 117 |
+
75% 10.000000 20.000000
|
| 118 |
+
max 15.000000 80.000000
|
Reference/basemodel.py
ADDED
|
@@ -0,0 +1,46 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import joblib
|
| 2 |
+
|
| 3 |
+
# Load the basic model pipeline
|
| 4 |
+
pipeline = joblib.load('basic_portfolio_model.pkl')
|
| 5 |
+
|
| 6 |
+
input_data = {
|
| 7 |
+
'Salary': 150000,
|
| 8 |
+
'Expenses': 100000,
|
| 9 |
+
'Savings': 50000,
|
| 10 |
+
'Lifecycle Stage': 'Mid-Career',
|
| 11 |
+
'Risk Appetite': 'Medium',
|
| 12 |
+
'Investment Horizon': 'Long-term'
|
| 13 |
+
}
|
| 14 |
+
|
| 15 |
+
# Convert categorical features (FIXED SYNTAX)
|
| 16 |
+
input_data['Lifecycle Stage'] = pipeline['mappings']['lifecycle'][input_data['Lifecycle Stage']] # Added closing ]
|
| 17 |
+
input_data['Risk Appetite'] = pipeline['mappings']['risk'][input_data['Risk Appetite']] # Added closing ]
|
| 18 |
+
input_data['Investment Horizon'] = pipeline['mappings']['horizon'][input_data['Investment Horizon']] # Added closing ]
|
| 19 |
+
|
| 20 |
+
# Create feature array in correct order
|
| 21 |
+
X = [
|
| 22 |
+
input_data['Salary'],
|
| 23 |
+
input_data['Expenses'],
|
| 24 |
+
input_data['Savings'],
|
| 25 |
+
input_data['Lifecycle Stage'],
|
| 26 |
+
input_data['Risk Appetite'],
|
| 27 |
+
input_data['Investment Horizon']
|
| 28 |
+
]
|
| 29 |
+
|
| 30 |
+
# Scale and predict
|
| 31 |
+
X_scaled = pipeline['scaler'].transform([X])
|
| 32 |
+
pred = pipeline['model'].predict(X_scaled)[0]
|
| 33 |
+
|
| 34 |
+
# Normalize and format
|
| 35 |
+
total = pred.sum()
|
| 36 |
+
final_allocation = {
|
| 37 |
+
'Equity': round((pred[0]/total)*100, 1),
|
| 38 |
+
'Debt': round((pred[1]/total)*100, 1),
|
| 39 |
+
'Gold': round((pred[2]/total)*100, 1),
|
| 40 |
+
'FD/Cash': round((pred[3]/total)*100, 1)
|
| 41 |
+
}
|
| 42 |
+
|
| 43 |
+
print("Recommended Portfolio:")
|
| 44 |
+
for asset, perc in final_allocation.items():
|
| 45 |
+
print(f"{asset}: {perc}%")
|
| 46 |
+
print(f"Total: {sum(final_allocation.values())}%")
|
Reference/basic_portfolio_model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ae20f4cf2e1cb3f75623f692aa11f947c83529dbe6335a5634365052bd8d15ca
|
| 3 |
+
size 1817898
|
Reference/enhanced_portfolio_model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2560b79a9705ca3674cd96820fd31fd6799dde89b12d4815afc6887111b52746
|
| 3 |
+
size 1691633
|
Reference/enhancedmodel.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import joblib
|
| 2 |
+
import pandas as pd
|
| 3 |
+
|
| 4 |
+
# Load enhanced model pipeline
|
| 5 |
+
pipeline = joblib.load('enhanced_portfolio_model.pkl')
|
| 6 |
+
|
| 7 |
+
# Sample input (MUST include Profession/City)
|
| 8 |
+
test_case = {
|
| 9 |
+
'Profession': 'Software Engineer',
|
| 10 |
+
'City': 'Mumbai',
|
| 11 |
+
'Salary': 150000,
|
| 12 |
+
'Expenses': 100000,
|
| 13 |
+
'Savings': 50000,
|
| 14 |
+
'Lifecycle Stage': 'Mid-Career',
|
| 15 |
+
'Risk Appetite': 'Medium',
|
| 16 |
+
'Investment Horizon': 'Long-term'
|
| 17 |
+
}
|
| 18 |
+
|
| 19 |
+
# Convert categorical features
|
| 20 |
+
test_case['Profession'] = pipeline['profession_encoder'].transform([test_case['Profession']])[0]
|
| 21 |
+
test_case['City'] = pipeline['city_encoder'].transform([test_case['City']])[0]
|
| 22 |
+
test_case['Lifecycle Stage'] = pipeline['mappings']['lifecycle'][test_case['Lifecycle Stage']]
|
| 23 |
+
test_case['Risk Appetite'] = pipeline['mappings']['risk'][test_case['Risk Appetite']]
|
| 24 |
+
test_case['Investment Horizon'] = pipeline['mappings']['horizon'][test_case['Investment Horizon']]
|
| 25 |
+
|
| 26 |
+
# Create feature array IN EXACT ORDER:
|
| 27 |
+
# ['Profession', 'City', 'Salary', 'Expenses', 'Savings',
|
| 28 |
+
# 'Lifecycle Stage', 'Risk Appetite', 'Investment Horizon']
|
| 29 |
+
X = [
|
| 30 |
+
test_case['Profession'],
|
| 31 |
+
test_case['City'],
|
| 32 |
+
test_case['Salary'],
|
| 33 |
+
test_case['Expenses'],
|
| 34 |
+
test_case['Savings'],
|
| 35 |
+
test_case['Lifecycle Stage'],
|
| 36 |
+
test_case['Risk Appetite'],
|
| 37 |
+
test_case['Investment Horizon']
|
| 38 |
+
]
|
| 39 |
+
|
| 40 |
+
# Scale and predict
|
| 41 |
+
X_scaled = pipeline['scaler'].transform([X])
|
| 42 |
+
pred = pipeline['model'].predict(X_scaled)[0]
|
| 43 |
+
|
| 44 |
+
# Normalize to 100%
|
| 45 |
+
total = pred.sum()
|
| 46 |
+
final_allocation = {
|
| 47 |
+
'Equity': round((pred[0]/total)*100, 1),
|
| 48 |
+
'Debt': round((pred[1]/total)*100, 1),
|
| 49 |
+
'Gold': round((pred[2]/total)*100, 1),
|
| 50 |
+
'FD/Cash': round((pred[3]/total)*100, 1)
|
| 51 |
+
}
|
| 52 |
+
|
| 53 |
+
print("Enhanced Model Recommended Portfolio:")
|
| 54 |
+
for asset, perc in final_allocation.items():
|
| 55 |
+
print(f"{asset}: {perc}%")
|
| 56 |
+
print(f"Total: {sum(final_allocation.values())}%")
|
Reference/rulebasedmodel.py
ADDED
|
@@ -0,0 +1,314 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import datetime
|
| 2 |
+
import math
|
| 3 |
+
|
| 4 |
+
# --- Helper Functions (mostly unchanged) ---
|
| 5 |
+
def get_float(prompt):
|
| 6 |
+
"""Helper to get a non-negative float from the user."""
|
| 7 |
+
while True:
|
| 8 |
+
try:
|
| 9 |
+
val_str = input(prompt).strip()
|
| 10 |
+
if not val_str:
|
| 11 |
+
raise ValueError("Input cannot be empty.")
|
| 12 |
+
val = float(val_str)
|
| 13 |
+
if val < 0:
|
| 14 |
+
raise ValueError("Please enter a non-negative number.")
|
| 15 |
+
return val
|
| 16 |
+
except ValueError as e:
|
| 17 |
+
print(f"Invalid input: {e}")
|
| 18 |
+
|
| 19 |
+
def get_int(prompt):
|
| 20 |
+
"""Helper to get a non-negative integer from the user."""
|
| 21 |
+
while True:
|
| 22 |
+
try:
|
| 23 |
+
val_str = input(prompt).strip()
|
| 24 |
+
if not val_str:
|
| 25 |
+
raise ValueError("Input cannot be empty.")
|
| 26 |
+
val = int(val_str)
|
| 27 |
+
if val < 0:
|
| 28 |
+
raise ValueError("Please enter a non-negative whole number.")
|
| 29 |
+
return val
|
| 30 |
+
except ValueError as e:
|
| 31 |
+
print(f"Invalid input: {e}")
|
| 32 |
+
|
| 33 |
+
def yes_no(prompt):
|
| 34 |
+
"""Helper to get a yes/no response."""
|
| 35 |
+
while True:
|
| 36 |
+
resp = input(prompt + " (y/n): ").strip().lower()
|
| 37 |
+
if resp in ("y", "yes"):
|
| 38 |
+
return True
|
| 39 |
+
if resp in ("n", "no"):
|
| 40 |
+
return False
|
| 41 |
+
print("Please answer 'y' or 'n'.")
|
| 42 |
+
|
| 43 |
+
# --- Simplified Profile and Financials Gathering ---
|
| 44 |
+
|
| 45 |
+
def get_user_profile():
|
| 46 |
+
"""Gathers lifecycle stage, risk appetite, and investment horizon."""
|
| 47 |
+
print("\n--- Let's Understand Your Investor Profile ---")
|
| 48 |
+
|
| 49 |
+
# 1. Lifecycle Stage
|
| 50 |
+
print("\nWhich of these best describes your current life stage?")
|
| 51 |
+
lifecycle_options = {
|
| 52 |
+
"1": "Student (Focus: Learning, initial savings)",
|
| 53 |
+
"2": "Early Career (20s-early 30s, Focus: Growth, accumulation)",
|
| 54 |
+
"3": "Mid-Career (Mid 30s-late 40s, Focus: Balancing growth & responsibilities)",
|
| 55 |
+
"4": "Late Career/Pre-Retirement (50s+, Focus: Capital preservation, income generation)",
|
| 56 |
+
"5": "Retired (Focus: Sustainable income, capital preservation)"
|
| 57 |
+
}
|
| 58 |
+
for key, value in lifecycle_options.items():
|
| 59 |
+
print(f"{key}. {value}")
|
| 60 |
+
while True:
|
| 61 |
+
choice = input("Enter number (1-5): ").strip()
|
| 62 |
+
if choice in lifecycle_options:
|
| 63 |
+
lifecycle_stage = lifecycle_options[choice].split('(')[0].strip() # Get "Student", "Early Career" etc.
|
| 64 |
+
break
|
| 65 |
+
else:
|
| 66 |
+
print("Invalid choice. Please enter a number between 1 and 5.")
|
| 67 |
+
|
| 68 |
+
# 2. Risk Appetite
|
| 69 |
+
print("\nHow would you describe your risk tolerance for investments?")
|
| 70 |
+
risk_options = {
|
| 71 |
+
"1": "Low (Prefer safety and capital preservation, okay with lower returns)",
|
| 72 |
+
"2": "Medium (Willing to take some risk for moderate growth, comfortable with some fluctuations)",
|
| 73 |
+
"3": "High (Willing to take significant risk for potentially high returns, can handle large fluctuations)"
|
| 74 |
+
}
|
| 75 |
+
for key, value in risk_options.items():
|
| 76 |
+
print(f"{key}. {value}")
|
| 77 |
+
while True:
|
| 78 |
+
choice = input("Enter number (1-3): ").strip()
|
| 79 |
+
if choice == "1": risk_appetite = "low"; break
|
| 80 |
+
elif choice == "2": risk_appetite = "medium"; break
|
| 81 |
+
elif choice == "3": risk_appetite = "high"; break
|
| 82 |
+
else:
|
| 83 |
+
print("Invalid choice. Please enter 1, 2, or 3.")
|
| 84 |
+
|
| 85 |
+
# 3. Investment Horizon
|
| 86 |
+
print("\nWhat is your general investment time horizon for the majority of your savings?")
|
| 87 |
+
horizon_options = {
|
| 88 |
+
"1": "Short-term (Less than 3 years)",
|
| 89 |
+
"2": "Medium-term (3-7 years)",
|
| 90 |
+
"3": "Long-term (More than 7 years)"
|
| 91 |
+
}
|
| 92 |
+
for key, value in horizon_options.items():
|
| 93 |
+
print(f"{key}. {value}")
|
| 94 |
+
while True:
|
| 95 |
+
choice = input("Enter number (1-3): ").strip()
|
| 96 |
+
if choice == "1": investment_horizon = "short-term"; break
|
| 97 |
+
elif choice == "2": investment_horizon = "medium-term"; break
|
| 98 |
+
elif choice == "3": investment_horizon = "long-term"; break
|
| 99 |
+
else:
|
| 100 |
+
print("Invalid choice. Please enter 1, 2, or 3.")
|
| 101 |
+
|
| 102 |
+
return lifecycle_stage, risk_appetite, investment_horizon
|
| 103 |
+
|
| 104 |
+
def get_financial_details():
|
| 105 |
+
"""Gets annual package, monthly in-hand salary, and total monthly expenses."""
|
| 106 |
+
print("\n--- Let's Get Your Financial Details ---")
|
| 107 |
+
annual_package = get_float("What is your approximate annual salary package (CTC)? \u20B9 ")
|
| 108 |
+
|
| 109 |
+
# Simplified monthly in-hand: Ask directly or provide a rough estimate and ask for correction
|
| 110 |
+
print(f"\nBased on an annual package of \u20B9 {annual_package:.2f}, your monthly in-hand salary might be around \u20B9 {annual_package / 14:.2f} to \u20B9 {annual_package / 13:.2f} (this is a rough estimate).")
|
| 111 |
+
monthly_in_hand_salary = get_float("What is your actual average monthly in-hand salary? \u20B9 ")
|
| 112 |
+
|
| 113 |
+
total_monthly_expenses = get_float("What are your total estimated monthly expenses (all inclusive)? \u20B9 ")
|
| 114 |
+
|
| 115 |
+
monthly_savings = monthly_in_hand_salary - total_monthly_expenses
|
| 116 |
+
|
| 117 |
+
print(f"\nBased on your input:")
|
| 118 |
+
print(f" Monthly In-hand Salary: \u20B9 {monthly_in_hand_salary:.2f}")
|
| 119 |
+
print(f" Total Monthly Expenses: \u20B9 {total_monthly_expenses:.2f}")
|
| 120 |
+
if monthly_savings > 0:
|
| 121 |
+
print(f" Calculated Monthly Savings: \u20B9 {monthly_savings:.2f}")
|
| 122 |
+
else:
|
| 123 |
+
print(f" Calculated Monthly Deficit: \u20B9 {abs(monthly_savings):.2f}")
|
| 124 |
+
print(" It seems your expenses exceed your income. Please review your budget.")
|
| 125 |
+
|
| 126 |
+
return monthly_in_hand_salary, total_monthly_expenses, monthly_savings
|
| 127 |
+
|
| 128 |
+
# --- Simplified Investment Allocation Logic ---
|
| 129 |
+
def allocate_savings_simplified(savings, risk_profile, horizon, lifecycle):
|
| 130 |
+
"""
|
| 131 |
+
Determines investment allocation percentages based on simplified profile.
|
| 132 |
+
Returns a dictionary of { "Asset Class": percentage, ... }
|
| 133 |
+
"""
|
| 134 |
+
allocations = {}
|
| 135 |
+
justification_points = [
|
| 136 |
+
f"This allocation considers your {risk_profile} risk profile, {horizon} horizon, and {lifecycle} stage."
|
| 137 |
+
]
|
| 138 |
+
|
| 139 |
+
# Prioritize Emergency Fund if not explicitly covered or if savings are first-time
|
| 140 |
+
# For simplicity, this version assumes user will manage EF separately or this is for surplus post-EF.
|
| 141 |
+
# A more robust version would inquire about EF status.
|
| 142 |
+
|
| 143 |
+
if risk_profile == "low":
|
| 144 |
+
allocations = {
|
| 145 |
+
"Fixed Deposits / Recurring Deposits (Safety & Stability)": 0.35,
|
| 146 |
+
"Debt Mutual Funds (Liquid/Short Duration - Better than savings account returns)": 0.30,
|
| 147 |
+
"Gold (SGBs/ETFs - Inflation Hedge, Diversification)": 0.10,
|
| 148 |
+
"Equity MFs (Large Cap/Index Funds - Long-term inflation beating, low volatility equity)": 0.15,
|
| 149 |
+
"Cash / Savings Account (Immediate Liquidity)": 0.10
|
| 150 |
+
}
|
| 151 |
+
justification_points.append("- Focus on capital preservation and stable returns.")
|
| 152 |
+
justification_points.append("- Suitable for short-term goals or very conservative investors.")
|
| 153 |
+
if horizon != "short-term":
|
| 154 |
+
justification_points.append("- Even with a longer horizon, a 'low' risk choice emphasizes safety above all.")
|
| 155 |
+
|
| 156 |
+
|
| 157 |
+
elif risk_profile == "medium":
|
| 158 |
+
allocations = {
|
| 159 |
+
"Equity MFs (Diversified - Large & Mid Cap/Flexi Cap SIPs for growth)": 0.45,
|
| 160 |
+
"Debt Mutual Funds (For stability and portfolio balance)": 0.25,
|
| 161 |
+
"Fixed Deposits / PPF (Secure, long-term component)": 0.10,
|
| 162 |
+
"Gold (SGBs/ETFs - Diversification)": 0.10,
|
| 163 |
+
"Equity MFs (International - For global diversification, if comfortable)": 0.05, # Optional
|
| 164 |
+
"Cash / Savings Account (Buffer)": 0.05
|
| 165 |
+
}
|
| 166 |
+
justification_points.append("- Aims for a balance between growth (equity) and stability (debt, gold).")
|
| 167 |
+
justification_points.append("- Suitable for medium to long-term goals.")
|
| 168 |
+
if lifecycle in ["Early Career", "Student"] and horizon == "long-term":
|
| 169 |
+
justification_points.append("- Your stage and horizon allow for good equity exposure for wealth creation.")
|
| 170 |
+
# Could slightly increase equity for very young, long-term, medium risk.
|
| 171 |
+
# allocations["Equity MFs (Diversified - Large & Mid Cap/Flexi Cap SIPs for growth)"] = 0.50
|
| 172 |
+
# allocations["Debt Mutual Funds (For stability and portfolio balance)"] = 0.20
|
| 173 |
+
|
| 174 |
+
elif risk_profile == "high":
|
| 175 |
+
allocations = {
|
| 176 |
+
"Equity MFs (Aggressive Growth - Mid/Small Cap, Thematic SIPs, with research)": 0.60,
|
| 177 |
+
"Equity MFs (International - Global growth opportunities)": 0.15,
|
| 178 |
+
"Direct Stocks (If experienced & well-researched, otherwise add to Equity MFs)": 0.10, # User needs to self-assess this.
|
| 179 |
+
"Debt Mutual Funds (Strategic, for some diversification)": 0.05,
|
| 180 |
+
"Gold (SGBs/ETFs - Tactical Diversification)": 0.05,
|
| 181 |
+
"Alternative Inv. (REITs/InvITs - very small, if understood & suitable, else to Equity)": 0.05
|
| 182 |
+
}
|
| 183 |
+
justification_points.append("- Focuses on maximizing long-term growth potential, accepting higher volatility.")
|
| 184 |
+
justification_points.append("- Best suited for long-term goals and investors comfortable with significant market swings.")
|
| 185 |
+
if lifecycle not in ["Early Career", "Mid-Career"] and horizon == "long-term":
|
| 186 |
+
justification_points.append(f"- CAUTION: High risk at {lifecycle} stage needs careful consideration of your overall financial stability and nearness to needing funds.")
|
| 187 |
+
if horizon != "long-term":
|
| 188 |
+
justification_points.append(f"- CAUTION: High risk for a {horizon} horizon is generally not advisable. Ensure goals truly allow for this risk.")
|
| 189 |
+
|
| 190 |
+
|
| 191 |
+
# Normalize percentages to ensure they sum to 100%
|
| 192 |
+
current_total_percentage = sum(allocations.values())
|
| 193 |
+
if abs(current_total_percentage - 1.0) > 0.001: # If not already 100%
|
| 194 |
+
factor = 1.0 / current_total_percentage
|
| 195 |
+
normalized_allocations = {k: v * factor for k, v in allocations.items()}
|
| 196 |
+
# Small check to ensure the largest component doesn't become negative if factor is weird (shouldn't happen with positive inputs)
|
| 197 |
+
# And ensure no tiny values are left that make no sense, e.g. less than 0.5% could be merged.
|
| 198 |
+
# For simplicity, we'll assume initial definitions are close enough.
|
| 199 |
+
final_allocations = {}
|
| 200 |
+
temp_sum = 0
|
| 201 |
+
for asset, perc in normalized_allocations.items():
|
| 202 |
+
# Round to sensible points e.g. 1 decimal for percentage display
|
| 203 |
+
# but use more precision for calculation
|
| 204 |
+
final_allocations[asset] = perc
|
| 205 |
+
temp_sum += perc
|
| 206 |
+
|
| 207 |
+
# Final check and adjustment of the largest item if sum isn't perfect due to rounding during normalization.
|
| 208 |
+
if abs(temp_sum - 1.0) > 0.0001:
|
| 209 |
+
diff = 1.0 - temp_sum
|
| 210 |
+
if final_allocations: # Check if dict is not empty
|
| 211 |
+
# Find largest item to adjust
|
| 212 |
+
largest_item_key = max(final_allocations, key=final_allocations.get)
|
| 213 |
+
final_allocations[largest_item_key] += diff
|
| 214 |
+
allocations = final_allocations
|
| 215 |
+
|
| 216 |
+
|
| 217 |
+
return allocations, justification_points
|
| 218 |
+
|
| 219 |
+
def display_investment_plan(monthly_savings, allocations, justification):
|
| 220 |
+
"""Displays the final investment plan and justification."""
|
| 221 |
+
print("\n\n--- Your Personalized Investment Allocation Plan ---")
|
| 222 |
+
print(f"Based on your monthly savings of \u20B9 {monthly_savings:.2f}:\n")
|
| 223 |
+
|
| 224 |
+
print("Suggested Allocation:")
|
| 225 |
+
print("---------------------------------------------------------------------------")
|
| 226 |
+
print(f"{'Asset Class/Instrument':<50} | {'Percentage':>12} | {'Amount (₹)':>12}")
|
| 227 |
+
print("---------------------------------------------------------------------------")
|
| 228 |
+
if not allocations: # Should not happen if logic is correct
|
| 229 |
+
print("No specific allocation generated. Please review inputs or consult an advisor.")
|
| 230 |
+
return
|
| 231 |
+
|
| 232 |
+
total_allocated_amount_check = 0
|
| 233 |
+
for asset, percentage in allocations.items():
|
| 234 |
+
amount = monthly_savings * percentage
|
| 235 |
+
total_allocated_amount_check += amount
|
| 236 |
+
print(f"{asset:<50} | {percentage*100:>11.1f}% | {amount:>11.2f}")
|
| 237 |
+
print("---------------------------------------------------------------------------")
|
| 238 |
+
print(f"{'TOTAL':<50} | {'100.0%':>12} | {total_allocated_amount_check:>11.2f}")
|
| 239 |
+
|
| 240 |
+
|
| 241 |
+
print("\nWhy these allocations?")
|
| 242 |
+
for point in justification:
|
| 243 |
+
print(f"- {point}")
|
| 244 |
+
print("\n- Learn more about general investing principles at: https://www.investopedia.com/financial-advisor/asset-allocation/\n"
|
| 245 |
+
" and for Indian context: https://www.amfiindia.com (Investor Education section)")
|
| 246 |
+
|
| 247 |
+
|
| 248 |
+
def provide_general_financial_tips(lifecycle_stage, monthly_savings, original_salary):
|
| 249 |
+
"""Provides general financial tips."""
|
| 250 |
+
print("\n--- General Financial Tips ---")
|
| 251 |
+
tips = []
|
| 252 |
+
if monthly_savings <= 0 and original_salary > 0 :
|
| 253 |
+
tips.append("- Your expenses currently meet or exceed your income. Focus on creating a budget to identify areas to cut back or explore ways to increase your income.")
|
| 254 |
+
elif original_salary > 0:
|
| 255 |
+
savings_rate = (monthly_savings / original_salary) * 100
|
| 256 |
+
tips.append(f"- Your current savings rate is approximately {savings_rate:.1f}%. Aim for at least 20-30% if possible, especially in accumulation stages.")
|
| 257 |
+
if savings_rate < 15:
|
| 258 |
+
tips.append("- Consider reviewing discretionary spending to boost your savings rate.")
|
| 259 |
+
|
| 260 |
+
tips.append("- Build & Maintain an Emergency Fund: Aim for 3-6 months of essential living expenses in a safe, liquid account (e.g., savings account, liquid mutual fund). This is your top priority before aggressive investing.")
|
| 261 |
+
tips.append("- Get Adequately Insured: Ensure you have sufficient health insurance for yourself and your family. If you have dependents, a term life insurance policy is crucial.")
|
| 262 |
+
tips.append("- Invest Regularly & Be Disciplined: Consistency through SIPs (Systematic Investment Plans) is key, especially for long-term goals. Don't try to time the market.")
|
| 263 |
+
tips.append("- Review Periodically: Revisit your financial plan and investments at least annually, or when major life events occur (marriage, new job, child, etc.).")
|
| 264 |
+
tips.append("- Understand Your Investments: Before investing in any product, understand its risks, costs (like expense ratios for MFs), and how it fits your goals.")
|
| 265 |
+
|
| 266 |
+
if lifecycle_stage == "Student" or lifecycle_stage == "Early Career":
|
| 267 |
+
tips.append("- Focus on upskilling and career growth to increase your earning potential. Your human capital is your biggest asset at this stage.")
|
| 268 |
+
if lifecycle_stage == "Late Career/Pre-Retirement" or lifecycle_stage == "Retired":
|
| 269 |
+
tips.append("- Plan for healthcare expenses in retirement. Consider if your current investments align with generating regular income if needed.")
|
| 270 |
+
|
| 271 |
+
for tip in tips:
|
| 272 |
+
print(f"* {tip}")
|
| 273 |
+
|
| 274 |
+
# --- Main Program Flow (Simplified) ---
|
| 275 |
+
def main_simplified():
|
| 276 |
+
print("╔══════════════════════════════════════════════════════════════╗")
|
| 277 |
+
print("║ Simplified Monthly Savings Allocator ║")
|
| 278 |
+
print(f"║ Today's Date: {datetime.date.today().strftime('%Y-%m-%d')} ║")
|
| 279 |
+
print("╚══════════════════════════════════════════════════════════════╝")
|
| 280 |
+
print("\nWelcome! This tool will help you allocate your monthly savings.")
|
| 281 |
+
print("This is for educational purposes and NOT financial advice.\n")
|
| 282 |
+
|
| 283 |
+
# 1. Get User Profile
|
| 284 |
+
lifecycle, risk, horizon = get_user_profile()
|
| 285 |
+
print(f"\nYour Profile Summary: Lifecycle: {lifecycle}, Risk: {risk.capitalize()}, Horizon: {horizon.capitalize()}")
|
| 286 |
+
|
| 287 |
+
# 2. Get Financial Details
|
| 288 |
+
salary, expenses, savings = get_financial_details()
|
| 289 |
+
|
| 290 |
+
if savings <= 0:
|
| 291 |
+
print("\nSince you don't have a monthly surplus, investment allocation is not applicable now.")
|
| 292 |
+
print("Focus on budgeting and increasing savings first.")
|
| 293 |
+
else:
|
| 294 |
+
# 3. Allocate Savings
|
| 295 |
+
allocations_dict, justification_list = allocate_savings_simplified(savings, risk, horizon, lifecycle)
|
| 296 |
+
|
| 297 |
+
# 4. Display Plan
|
| 298 |
+
display_investment_plan(savings, allocations_dict, justification_list)
|
| 299 |
+
|
| 300 |
+
# 5. Provide General Tips
|
| 301 |
+
provide_general_financial_tips(lifecycle, savings, salary)
|
| 302 |
+
|
| 303 |
+
# 6. Disclaimer
|
| 304 |
+
print("\n\n--- Disclaimer ---")
|
| 305 |
+
print("The information and suggestions provided by this script are for general guidance and educational purposes ONLY.")
|
| 306 |
+
print("This does NOT constitute financial, investment, tax, or legal advice.")
|
| 307 |
+
print("Investment decisions involve risks. Past performance is not indicative of future results.")
|
| 308 |
+
print("Asset allocation models are generalized and may not be suitable for your individual circumstances.")
|
| 309 |
+
print("It is strongly recommended to consult with a SEBI-registered Investment Adviser or a qualified financial professional for personalized advice.")
|
| 310 |
+
|
| 311 |
+
print("\nBudget calculation and investment suggestions complete. Plan wisely!\n")
|
| 312 |
+
|
| 313 |
+
if __name__ == "__main__":
|
| 314 |
+
main_simplified()
|
app/__init__.py
ADDED
|
@@ -0,0 +1 @@
|
|
|
|
|
|
|
| 1 |
+
# This file makes the 'app' directory a Python package.
|
app/__pycache__/__init__.cpython-312.pyc
ADDED
|
Binary file (156 Bytes). View file
|
|
|
app/__pycache__/admin.cpython-312.pyc
ADDED
|
Binary file (3.61 kB). View file
|
|
|
app/__pycache__/auth.cpython-312.pyc
ADDED
|
Binary file (5.6 kB). View file
|
|
|
app/__pycache__/crud.cpython-312.pyc
ADDED
|
Binary file (4.75 kB). View file
|
|
|
app/__pycache__/database.cpython-312.pyc
ADDED
|
Binary file (1.36 kB). View file
|
|
|
app/__pycache__/main.cpython-312.pyc
ADDED
|
Binary file (6.33 kB). View file
|
|
|
app/__pycache__/models.cpython-312.pyc
ADDED
|
Binary file (1.74 kB). View file
|
|
|
app/__pycache__/schemas.cpython-312.pyc
ADDED
|
Binary file (7.08 kB). View file
|
|
|
app/__pycache__/user.cpython-312.pyc
ADDED
|
Binary file (10.8 kB). View file
|
|
|
app/admin.py
ADDED
|
@@ -0,0 +1,73 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Request, Depends, HTTPException, Query
|
| 2 |
+
from fastapi.responses import HTMLResponse
|
| 3 |
+
from sqlalchemy.orm import Session
|
| 4 |
+
from typing import List
|
| 5 |
+
|
| 6 |
+
from app import models, schemas, crud
|
| 7 |
+
from app.database import get_db
|
| 8 |
+
from app.auth import get_current_admin_user, get_current_active_user # Import both
|
| 9 |
+
from app.main import templates # Import templates from main.py
|
| 10 |
+
|
| 11 |
+
router = APIRouter()
|
| 12 |
+
|
| 13 |
+
# Route to serve the HTML shell for the dashboard
|
| 14 |
+
@router.get("/dashboard", response_class=HTMLResponse)
|
| 15 |
+
async def admin_dashboard_shell(request: Request):
|
| 16 |
+
# This route loads the page structure.
|
| 17 |
+
# Client-side JS will verify admin status and fetch data.
|
| 18 |
+
return templates.TemplateResponse("admin/dashboard.html", {
|
| 19 |
+
"request": request,
|
| 20 |
+
"title": "Admin Dashboard"
|
| 21 |
+
# No user or users_list passed here initially
|
| 22 |
+
})
|
| 23 |
+
|
| 24 |
+
# New API endpoint to fetch dashboard data (protected)
|
| 25 |
+
@router.get("/api/dashboard-data")
|
| 26 |
+
async def get_admin_dashboard_data(
|
| 27 |
+
db: Session = Depends(get_db),
|
| 28 |
+
current_user: models.User = Depends(get_current_admin_user), # Ensures only admin can access
|
| 29 |
+
search_query: str = Query(None, alias="search")
|
| 30 |
+
):
|
| 31 |
+
if search_query:
|
| 32 |
+
user = crud.get_user_by_email(db, email=search_query)
|
| 33 |
+
users_list = [schemas.User.from_orm(user)] if user else [] # Convert to schema
|
| 34 |
+
else:
|
| 35 |
+
users = crud.get_users(db, limit=100)
|
| 36 |
+
users_list = [schemas.User.from_orm(u) for u in users] # Convert list to schema
|
| 37 |
+
|
| 38 |
+
# Return data needed by the dashboard template's JS
|
| 39 |
+
return {"users_list": users_list, "search_query": search_query, "admin_email": current_user.email}
|
| 40 |
+
|
| 41 |
+
|
| 42 |
+
# Route to view specific user details (protected)
|
| 43 |
+
# This serves an HTML page, so it will also need the client-side auth check pattern
|
| 44 |
+
@router.get("/users/{user_id}", response_class=HTMLResponse)
|
| 45 |
+
async def admin_view_user_details_shell(request: Request, user_id: int):
|
| 46 |
+
# Serve the shell page. JS will fetch details.
|
| 47 |
+
return templates.TemplateResponse("admin/user_details.html", {
|
| 48 |
+
"request": request,
|
| 49 |
+
"user_id": user_id, # Pass user_id for JS to use
|
| 50 |
+
"title": f"User Details" # Generic title initially
|
| 51 |
+
})
|
| 52 |
+
|
| 53 |
+
# New API endpoint to fetch specific user details (protected)
|
| 54 |
+
@router.get("/api/users/{user_id}")
|
| 55 |
+
async def get_admin_user_details_data(
|
| 56 |
+
user_id: int,
|
| 57 |
+
db: Session = Depends(get_db),
|
| 58 |
+
current_admin: models.User = Depends(get_current_admin_user) # Ensure admin access
|
| 59 |
+
):
|
| 60 |
+
target_user = crud.get_user(db, user_id=user_id)
|
| 61 |
+
if not target_user:
|
| 62 |
+
raise HTTPException(status_code=404, detail="User not found")
|
| 63 |
+
|
| 64 |
+
user_inputs_orm = crud.get_user_data_inputs_by_user_id(db, user_id=user_id, limit=100)
|
| 65 |
+
|
| 66 |
+
# Convert ORM objects to Pydantic schemas for JSON response
|
| 67 |
+
target_user_schema = schemas.User.from_orm(target_user)
|
| 68 |
+
user_inputs_schema = [schemas.UserDataInputResponse.from_orm(item) for item in user_inputs_orm]
|
| 69 |
+
|
| 70 |
+
return {
|
| 71 |
+
"target_user": target_user_schema,
|
| 72 |
+
"user_inputs": user_inputs_schema
|
| 73 |
+
}
|
app/auth.py
ADDED
|
@@ -0,0 +1,99 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Depends, HTTPException, status, Form
|
| 2 |
+
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
|
| 3 |
+
from sqlalchemy.orm import Session
|
| 4 |
+
from datetime import timedelta
|
| 5 |
+
|
| 6 |
+
from . import crud, models, schemas
|
| 7 |
+
from .core import security
|
| 8 |
+
from .database import get_db
|
| 9 |
+
|
| 10 |
+
router = APIRouter()
|
| 11 |
+
|
| 12 |
+
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token") # Points to the token login endpoint
|
| 13 |
+
|
| 14 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = security.ACCESS_TOKEN_EXPIRE_MINUTES
|
| 15 |
+
|
| 16 |
+
async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> models.User:
|
| 17 |
+
credentials_exception = HTTPException(
|
| 18 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 19 |
+
detail="Could not validate credentials",
|
| 20 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 21 |
+
)
|
| 22 |
+
payload = security.decode_access_token(token)
|
| 23 |
+
if payload is None:
|
| 24 |
+
raise credentials_exception
|
| 25 |
+
email: str = payload.get("sub")
|
| 26 |
+
if email is None:
|
| 27 |
+
raise credentials_exception
|
| 28 |
+
|
| 29 |
+
user = crud.get_user_by_email(db, email=email)
|
| 30 |
+
if user is None:
|
| 31 |
+
raise credentials_exception
|
| 32 |
+
return user
|
| 33 |
+
|
| 34 |
+
async def get_current_active_user(current_user: models.User = Depends(get_current_user)) -> models.User:
|
| 35 |
+
# Add any active/inactive checks here if needed
|
| 36 |
+
# if not current_user.is_active:
|
| 37 |
+
# raise HTTPException(status_code=400, detail="Inactive user")
|
| 38 |
+
return current_user
|
| 39 |
+
|
| 40 |
+
async def get_current_admin_user(current_user: models.User = Depends(get_current_active_user)) -> models.User:
|
| 41 |
+
if not current_user.is_admin:
|
| 42 |
+
raise HTTPException(
|
| 43 |
+
status_code=status.HTTP_403_FORBIDDEN,
|
| 44 |
+
detail="The user doesn't have enough privileges"
|
| 45 |
+
)
|
| 46 |
+
return current_user
|
| 47 |
+
|
| 48 |
+
|
| 49 |
+
@router.post("/signup", response_model=schemas.User)
|
| 50 |
+
async def signup_user(user: schemas.UserCreate, db: Session = Depends(get_db)):
|
| 51 |
+
db_user = crud.get_user_by_email(db, email=user.email)
|
| 52 |
+
if db_user:
|
| 53 |
+
raise HTTPException(status_code=400, detail="Email already registered")
|
| 54 |
+
if user.password != user.confirm_password:
|
| 55 |
+
raise HTTPException(status_code=400, detail="Passwords do not match")
|
| 56 |
+
return crud.create_user(db=db, user=user)
|
| 57 |
+
|
| 58 |
+
@router.post("/token", response_model=schemas.Token)
|
| 59 |
+
async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)):
|
| 60 |
+
user = crud.get_user_by_email(db, email=form_data.username) # OAuth2 form uses 'username' for email
|
| 61 |
+
if not user or not security.verify_password(form_data.password, user.hashed_password):
|
| 62 |
+
raise HTTPException(
|
| 63 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 64 |
+
detail="Incorrect email or password",
|
| 65 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 66 |
+
)
|
| 67 |
+
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 68 |
+
access_token = security.create_access_token(
|
| 69 |
+
data={"sub": user.email}, expires_delta=access_token_expires
|
| 70 |
+
)
|
| 71 |
+
return {"access_token": access_token, "token_type": "bearer"}
|
| 72 |
+
|
| 73 |
+
# This is a utility endpoint, usually not directly called by frontend for login
|
| 74 |
+
# It's what OAuth2PasswordRequestForm uses internally.
|
| 75 |
+
# The actual login form should post to /auth/token.
|
| 76 |
+
@router.post("/login", response_model=schemas.Token)
|
| 77 |
+
async def login_user_form(email: str = Form(...), password: str = Form(...), db: Session = Depends(get_db)):
|
| 78 |
+
"""
|
| 79 |
+
Login endpoint that takes email and password from form data.
|
| 80 |
+
This is an alternative if not using OAuth2PasswordRequestForm directly in frontend JS.
|
| 81 |
+
Frontend forms would post to this.
|
| 82 |
+
"""
|
| 83 |
+
user = crud.get_user_by_email(db, email=email)
|
| 84 |
+
if not user or not security.verify_password(password, user.hashed_password):
|
| 85 |
+
raise HTTPException(
|
| 86 |
+
status_code=status.HTTP_401_UNAUTHORIZED,
|
| 87 |
+
detail="Incorrect email or password",
|
| 88 |
+
headers={"WWW-Authenticate": "Bearer"},
|
| 89 |
+
)
|
| 90 |
+
access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 91 |
+
access_token = security.create_access_token(
|
| 92 |
+
data={"sub": user.email}, expires_delta=access_token_expires
|
| 93 |
+
)
|
| 94 |
+
return {"access_token": access_token, "token_type": "bearer"}
|
| 95 |
+
|
| 96 |
+
|
| 97 |
+
@router.get("/users/me", response_model=schemas.User)
|
| 98 |
+
async def read_users_me(current_user: models.User = Depends(get_current_active_user)):
|
| 99 |
+
return current_user
|
app/core/__pycache__/security.cpython-312.pyc
ADDED
|
Binary file (2.44 kB). View file
|
|
|
app/core/security.py
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from datetime import datetime, timedelta
|
| 2 |
+
from typing import Optional
|
| 3 |
+
from jose import JWTError, jwt
|
| 4 |
+
from passlib.context import CryptContext
|
| 5 |
+
from pydantic import BaseModel
|
| 6 |
+
|
| 7 |
+
# Password Hashing
|
| 8 |
+
pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto")
|
| 9 |
+
|
| 10 |
+
# JWT Configuration (Consider moving to a .env file or config management)
|
| 11 |
+
SECRET_KEY = "your-super-secret-key" # CHANGE THIS IN PRODUCTION!
|
| 12 |
+
ALGORITHM = "HS256"
|
| 13 |
+
ACCESS_TOKEN_EXPIRE_MINUTES = 30 # Token validity period
|
| 14 |
+
|
| 15 |
+
def verify_password(plain_password: str, hashed_password: str) -> bool:
|
| 16 |
+
return pwd_context.verify(plain_password, hashed_password)
|
| 17 |
+
|
| 18 |
+
def get_password_hash(password: str) -> str:
|
| 19 |
+
return pwd_context.hash(password)
|
| 20 |
+
|
| 21 |
+
def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str:
|
| 22 |
+
to_encode = data.copy()
|
| 23 |
+
if expires_delta:
|
| 24 |
+
expire = datetime.utcnow() + expires_delta
|
| 25 |
+
else:
|
| 26 |
+
expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES)
|
| 27 |
+
to_encode.update({"exp": expire})
|
| 28 |
+
encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
|
| 29 |
+
return encoded_jwt
|
| 30 |
+
|
| 31 |
+
def decode_access_token(token: str) -> Optional[dict]:
|
| 32 |
+
try:
|
| 33 |
+
payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
|
| 34 |
+
return payload
|
| 35 |
+
except JWTError:
|
| 36 |
+
return None
|
| 37 |
+
|
| 38 |
+
class TokenPayload(BaseModel):
|
| 39 |
+
sub: Optional[str] = None # 'sub' is standard for subject (e.g., user email or ID)
|
| 40 |
+
exp: Optional[datetime] = None
|
app/crud.py
ADDED
|
@@ -0,0 +1,48 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy.orm import Session
|
| 2 |
+
from . import models, schemas
|
| 3 |
+
from .core.security import get_password_hash
|
| 4 |
+
from typing import List, Optional
|
| 5 |
+
|
| 6 |
+
# --- User CRUD ---
|
| 7 |
+
def get_user(db: Session, user_id: int) -> Optional[models.User]:
|
| 8 |
+
return db.query(models.User).filter(models.User.id == user_id).first()
|
| 9 |
+
|
| 10 |
+
def get_user_by_email(db: Session, email: str) -> Optional[models.User]:
|
| 11 |
+
return db.query(models.User).filter(models.User.email == email).first()
|
| 12 |
+
|
| 13 |
+
def get_users(db: Session, skip: int = 0, limit: int = 100) -> List[models.User]:
|
| 14 |
+
return db.query(models.User).offset(skip).limit(limit).all()
|
| 15 |
+
|
| 16 |
+
def create_user(db: Session, user: schemas.UserCreate) -> models.User:
|
| 17 |
+
hashed_password = get_password_hash(user.password)
|
| 18 |
+
db_user = models.User(email=user.email, hashed_password=hashed_password)
|
| 19 |
+
db.add(db_user)
|
| 20 |
+
db.commit()
|
| 21 |
+
db.refresh(db_user)
|
| 22 |
+
return db_user
|
| 23 |
+
|
| 24 |
+
def create_admin_user(db: Session, user: schemas.UserCreate) -> models.User:
|
| 25 |
+
hashed_password = get_password_hash(user.password)
|
| 26 |
+
db_user = models.User(email=user.email, hashed_password=hashed_password, is_admin=True)
|
| 27 |
+
db.add(db_user)
|
| 28 |
+
db.commit()
|
| 29 |
+
db.refresh(db_user)
|
| 30 |
+
return db_user
|
| 31 |
+
|
| 32 |
+
# --- UserDataInput CRUD ---
|
| 33 |
+
def create_user_data_input(db: Session, item: schemas.UserDataInputCreate, user_id: int) -> models.UserDataInput:
|
| 34 |
+
db_item = models.UserDataInput(**item.dict(), user_id=user_id)
|
| 35 |
+
db.add(db_item)
|
| 36 |
+
db.commit()
|
| 37 |
+
db.refresh(db_item)
|
| 38 |
+
return db_item
|
| 39 |
+
|
| 40 |
+
def get_user_data_inputs_by_user_id(db: Session, user_id: int, skip: int = 0, limit: int = 100) -> List[models.UserDataInput]:
|
| 41 |
+
return db.query(models.UserDataInput).filter(models.UserDataInput.user_id == user_id).offset(skip).limit(limit).all()
|
| 42 |
+
|
| 43 |
+
def get_all_user_data_inputs(db: Session, skip: int = 0, limit: int = 1000) -> List[models.UserDataInput]:
|
| 44 |
+
"""
|
| 45 |
+
Fetches all user data inputs, primarily for admin use.
|
| 46 |
+
Consider pagination for very large datasets.
|
| 47 |
+
"""
|
| 48 |
+
return db.query(models.UserDataInput).order_by(models.UserDataInput.timestamp.desc()).offset(skip).limit(limit).all()
|
app/database.py
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import create_engine
|
| 2 |
+
from sqlalchemy.ext.declarative import declarative_base
|
| 3 |
+
from sqlalchemy.orm import sessionmaker
|
| 4 |
+
import os
|
| 5 |
+
|
| 6 |
+
DATABASE_URL = "sqlite:///./app/ml_models/financial_advisor.db"
|
| 7 |
+
|
| 8 |
+
# Create the directory for the database if it doesn't exist
|
| 9 |
+
os.makedirs(os.path.dirname(DATABASE_URL.split("///")[-1]), exist_ok=True)
|
| 10 |
+
|
| 11 |
+
engine = create_engine(
|
| 12 |
+
DATABASE_URL, connect_args={"check_same_thread": False}
|
| 13 |
+
)
|
| 14 |
+
SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine)
|
| 15 |
+
|
| 16 |
+
Base = declarative_base()
|
| 17 |
+
|
| 18 |
+
def get_db():
|
| 19 |
+
db = SessionLocal()
|
| 20 |
+
try:
|
| 21 |
+
yield db
|
| 22 |
+
finally:
|
| 23 |
+
db.close()
|
| 24 |
+
|
| 25 |
+
def create_db_and_tables():
|
| 26 |
+
Base.metadata.create_all(bind=engine)
|
app/main.py
ADDED
|
@@ -0,0 +1,146 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import FastAPI, Request, Depends, HTTPException, status
|
| 2 |
+
from fastapi.staticfiles import StaticFiles
|
| 3 |
+
from fastapi.templating import Jinja2Templates
|
| 4 |
+
from fastapi.responses import HTMLResponse, RedirectResponse
|
| 5 |
+
from pathlib import Path
|
| 6 |
+
from sqlalchemy.orm import Session
|
| 7 |
+
|
| 8 |
+
from app import models, schemas, crud, auth # app. is used if main.py is outside app/
|
| 9 |
+
from app.database import engine, get_db, create_db_and_tables
|
| 10 |
+
from app.services import chatbot_service, data_service # app.services
|
| 11 |
+
from app.auth import get_current_active_user, get_current_admin_user, oauth2_scheme # app.auth
|
| 12 |
+
|
| 13 |
+
# Determine the base directory of the 'app' package
|
| 14 |
+
BASE_DIR = Path(__file__).resolve().parent
|
| 15 |
+
|
| 16 |
+
app = FastAPI(title="Financial Advisor Chatbot")
|
| 17 |
+
|
| 18 |
+
# Mount static files (CSS, JS, images)
|
| 19 |
+
# The path "static" here is relative to where main.py is.
|
| 20 |
+
# If main.py is in app/, then app/static/
|
| 21 |
+
app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static")
|
| 22 |
+
|
| 23 |
+
# Setup Jinja2 templates
|
| 24 |
+
# If main.py is in app/, then app/templates/
|
| 25 |
+
templates = Jinja2Templates(directory=BASE_DIR / "templates")
|
| 26 |
+
|
| 27 |
+
# --- Event Handlers ---
|
| 28 |
+
@app.on_event("startup")
|
| 29 |
+
async def startup_event():
|
| 30 |
+
create_db_and_tables()
|
| 31 |
+
# Initialize CSV file with headers if it doesn't exist
|
| 32 |
+
data_service.initialize_csv()
|
| 33 |
+
# Load ML models
|
| 34 |
+
chatbot_service.load_models()
|
| 35 |
+
# Create a default admin user if one doesn't exist (optional)
|
| 36 |
+
db = next(get_db()) # Get a DB session
|
| 37 |
+
try:
|
| 38 |
+
admin_email = "admin@example.com"
|
| 39 |
+
admin_password = "adminpassword" # Change this!
|
| 40 |
+
|
| 41 |
+
admin_user_obj = crud.get_user_by_email(db, email=admin_email)
|
| 42 |
+
|
| 43 |
+
if not admin_user_obj:
|
| 44 |
+
print(f"Admin user '{admin_email}' not found, creating...")
|
| 45 |
+
admin_user_schema = schemas.UserCreate(email=admin_email, password=admin_password, confirm_password=admin_password)
|
| 46 |
+
crud.create_admin_user(db, admin_user_schema)
|
| 47 |
+
print(f"Default admin user '{admin_email}' created with password '{admin_password}'. PLEASE CHANGE THE PASSWORD.")
|
| 48 |
+
elif not admin_user_obj.is_admin:
|
| 49 |
+
# If user exists but is not admin, update them (or log warning)
|
| 50 |
+
print(f"User '{admin_email}' exists but is not admin. Updating status...")
|
| 51 |
+
admin_user_obj.is_admin = True
|
| 52 |
+
# If you also want to ensure the password is set, you might reset it here:
|
| 53 |
+
# admin_user_obj.hashed_password = security.get_password_hash(admin_password)
|
| 54 |
+
db.add(admin_user_obj) # Add the existing object to the session to track changes
|
| 55 |
+
db.commit()
|
| 56 |
+
print(f"User '{admin_email}' updated to be an admin.")
|
| 57 |
+
else:
|
| 58 |
+
print(f"Admin user '{admin_email}' already exists.")
|
| 59 |
+
|
| 60 |
+
except Exception as e:
|
| 61 |
+
print(f"Error during admin user setup: {e}")
|
| 62 |
+
finally:
|
| 63 |
+
db.close()
|
| 64 |
+
|
| 65 |
+
|
| 66 |
+
# --- Include Routers ---
|
| 67 |
+
# Assuming auth.py, user.py, admin.py are in the same directory as main.py (i.e. in 'app')
|
| 68 |
+
# If main.py is in the root, these would be from app.auth, app.user, app.admin
|
| 69 |
+
from . import auth as auth_router # Renaming to avoid conflict with 'auth' module
|
| 70 |
+
# We will create user_router and admin_router later
|
| 71 |
+
from . import user as user_router
|
| 72 |
+
from . import admin as admin_router
|
| 73 |
+
|
| 74 |
+
app.include_router(auth_router.router, prefix="/auth", tags=["Authentication"])
|
| 75 |
+
app.include_router(user_router.router, prefix="/user", tags=["User Pages"]) # User specific pages like /user/home
|
| 76 |
+
app.include_router(admin_router.router, prefix="/admin", tags=["Admin Pages"]) # Admin specific pages like /admin/dashboard
|
| 77 |
+
|
| 78 |
+
# --- Root and Basic Page Routes ---
|
| 79 |
+
@app.get("/", response_class=HTMLResponse)
|
| 80 |
+
async def read_root(request: Request):
|
| 81 |
+
# Redirect to login page by default, or to home if logged in (more complex logic for latter)
|
| 82 |
+
return templates.TemplateResponse("auth/login.html", {"request": request, "title": "Login"})
|
| 83 |
+
|
| 84 |
+
@app.get("/signup-page", response_class=HTMLResponse) # Renamed to avoid conflict if auth router has /signup
|
| 85 |
+
async def signup_page_render(request: Request):
|
| 86 |
+
return templates.TemplateResponse("auth/signup.html", {"request": request, "title": "Sign Up"})
|
| 87 |
+
|
| 88 |
+
@app.get("/login-page", response_class=HTMLResponse) # Renamed to avoid conflict if auth router has /login
|
| 89 |
+
async def login_page_render(request: Request):
|
| 90 |
+
return templates.TemplateResponse("auth/login.html", {"request": request, "title": "Login"})
|
| 91 |
+
|
| 92 |
+
# The actual /home and /admin/dashboard routes are now in user.py and admin.py respectively.
|
| 93 |
+
|
| 94 |
+
# --- Chatbot API endpoint ---
|
| 95 |
+
@app.post("/api/chatbot", response_model=schemas.ChatbotInteractionResponse)
|
| 96 |
+
async def api_chatbot_interact(
|
| 97 |
+
request_data: schemas.ChatbotInteractionRequest,
|
| 98 |
+
db: Session = Depends(get_db),
|
| 99 |
+
current_user: models.User = Depends(get_current_active_user)
|
| 100 |
+
):
|
| 101 |
+
# Process the interaction using the chatbot service
|
| 102 |
+
response = chatbot_service.process_chatbot_interaction(request_data)
|
| 103 |
+
|
| 104 |
+
# If no error in recommendation, save input and potentially output to DB and CSV
|
| 105 |
+
if "error" not in response.recommendation:
|
| 106 |
+
# Save to DB
|
| 107 |
+
user_input_db = schemas.UserDataInputCreate(
|
| 108 |
+
model_type=request_data.model_type,
|
| 109 |
+
input_data=request_data.inputs
|
| 110 |
+
# output_data=response.recommendation # Optionally store output
|
| 111 |
+
)
|
| 112 |
+
crud.create_user_data_input(db=db, item=user_input_db, user_id=current_user.id)
|
| 113 |
+
|
| 114 |
+
# Save to CSV
|
| 115 |
+
data_service.append_data_to_csv(
|
| 116 |
+
user_id=current_user.id,
|
| 117 |
+
user_email=current_user.email,
|
| 118 |
+
model_type=request_data.model_type,
|
| 119 |
+
input_data=request_data.inputs,
|
| 120 |
+
output_data=response.recommendation # Pass the allocation part
|
| 121 |
+
)
|
| 122 |
+
|
| 123 |
+
return response
|
| 124 |
+
|
| 125 |
+
# --- Logout ---
|
| 126 |
+
@app.get("/logout")
|
| 127 |
+
async def logout(request: Request):
|
| 128 |
+
# For token-based auth, logout is primarily a client-side operation (deleting the token).
|
| 129 |
+
# Server-side, you might blacklist the token if using a more complex setup.
|
| 130 |
+
# Here, we'll just redirect to the root path which serves the login page.
|
| 131 |
+
response = RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) # Changed url to "/"
|
| 132 |
+
# Instruct browser to clear any relevant cookies if they were set by server (not typical for Bearer tokens)
|
| 133 |
+
# response.delete_cookie("access_token_cookie") # If you were setting it as a cookie
|
| 134 |
+
return response
|
| 135 |
+
|
| 136 |
+
|
| 137 |
+
# To run the app (example, usually done with uvicorn command line):
|
| 138 |
+
# if __name__ == "__main__":
|
| 139 |
+
# import uvicorn
|
| 140 |
+
# # Note: Uvicorn should typically be run from the project root, not from within app/
|
| 141 |
+
# # e.g., uvicorn app.main:app --reload
|
| 142 |
+
# # For direct execution from app/main.py (less common for FastAPI projects):
|
| 143 |
+
# # uvicorn.run(app, host="0.0.0.0", port=8000)
|
| 144 |
+
# # Better to run with: uvicorn financial_advisor.app.main:app --reload from the project root
|
| 145 |
+
# # Or if main.py is in the root: uvicorn main:app --reload
|
| 146 |
+
# pass
|
app/ml_models/basic_portfolio_model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:ae20f4cf2e1cb3f75623f692aa11f947c83529dbe6335a5634365052bd8d15ca
|
| 3 |
+
size 1817898
|
app/ml_models/enhanced_portfolio_model.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:a780fe27d28ffee39b771925d3746551b30417e09d513f1899a38091e607e5d0
|
| 3 |
+
size 1738333
|
app/ml_models/enhanced_portfolio_model111.pkl
ADDED
|
@@ -0,0 +1,3 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
version https://git-lfs.github.com/spec/v1
|
| 2 |
+
oid sha256:2560b79a9705ca3674cd96820fd31fd6799dde89b12d4815afc6887111b52746
|
| 3 |
+
size 1691633
|
app/ml_models/financial_advisor.db
ADDED
|
Binary file (28.7 kB). View file
|
|
|
app/models.py
ADDED
|
@@ -0,0 +1,28 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from sqlalchemy import Column, Integer, String, Boolean, DateTime, JSON, ForeignKey
|
| 2 |
+
from sqlalchemy.orm import relationship
|
| 3 |
+
from sqlalchemy.sql import func
|
| 4 |
+
from .database import Base
|
| 5 |
+
|
| 6 |
+
class User(Base):
|
| 7 |
+
__tablename__ = "users"
|
| 8 |
+
|
| 9 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 10 |
+
email = Column(String, unique=True, index=True, nullable=False)
|
| 11 |
+
hashed_password = Column(String, nullable=False)
|
| 12 |
+
is_admin = Column(Boolean, default=False)
|
| 13 |
+
created_at = Column(DateTime(timezone=True), server_default=func.now())
|
| 14 |
+
|
| 15 |
+
inputs = relationship("UserDataInput", back_populates="owner")
|
| 16 |
+
|
| 17 |
+
class UserDataInput(Base):
|
| 18 |
+
__tablename__ = "user_data_inputs"
|
| 19 |
+
|
| 20 |
+
id = Column(Integer, primary_key=True, index=True)
|
| 21 |
+
user_id = Column(Integer, ForeignKey("users.id"))
|
| 22 |
+
model_type = Column(String, index=True) # "base", "enhanced", "rule_based"
|
| 23 |
+
input_data = Column(JSON) # Stores the dictionary of inputs
|
| 24 |
+
# Output data can also be stored if needed, e.g., portfolio allocation
|
| 25 |
+
# output_data = Column(JSON)
|
| 26 |
+
timestamp = Column(DateTime(timezone=True), server_default=func.now())
|
| 27 |
+
|
| 28 |
+
owner = relationship("User", back_populates="inputs")
|
app/schemas.py
ADDED
|
@@ -0,0 +1,139 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from pydantic import BaseModel, EmailStr, field_validator, model_validator, ValidationInfo
|
| 2 |
+
from typing import Optional, Dict, Any, List
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
|
| 5 |
+
# --- User Schemas ---
|
| 6 |
+
class UserBase(BaseModel):
|
| 7 |
+
email: EmailStr
|
| 8 |
+
|
| 9 |
+
class UserCreate(UserBase):
|
| 10 |
+
password: str
|
| 11 |
+
confirm_password: Optional[str] = None # For signup form
|
| 12 |
+
|
| 13 |
+
@model_validator(mode='after')
|
| 14 |
+
def passwords_match(self) -> 'UserCreate':
|
| 15 |
+
if self.password is not None and self.confirm_password is not None and self.password != self.confirm_password:
|
| 16 |
+
raise ValueError('Passwords do not match')
|
| 17 |
+
# Ensure confirm_password is not passed to the User model if it's just for validation
|
| 18 |
+
# However, our User model doesn't have confirm_password, so it's fine.
|
| 19 |
+
return self
|
| 20 |
+
|
| 21 |
+
class User(UserBase):
|
| 22 |
+
id: int
|
| 23 |
+
is_admin: bool
|
| 24 |
+
created_at: datetime
|
| 25 |
+
|
| 26 |
+
class Config:
|
| 27 |
+
from_attributes = True
|
| 28 |
+
|
| 29 |
+
# --- UserDataInput Schemas ---
|
| 30 |
+
class UserDataInputBase(BaseModel):
|
| 31 |
+
model_type: str
|
| 32 |
+
input_data: Dict[str, Any]
|
| 33 |
+
|
| 34 |
+
class UserDataInputCreate(UserDataInputBase):
|
| 35 |
+
pass
|
| 36 |
+
|
| 37 |
+
class UserDataInputResponse(UserDataInputBase):
|
| 38 |
+
id: int
|
| 39 |
+
user_id: int
|
| 40 |
+
timestamp: datetime
|
| 41 |
+
# output_data: Optional[Dict[str, Any]] = None # If storing output
|
| 42 |
+
|
| 43 |
+
class Config:
|
| 44 |
+
from_attributes = True
|
| 45 |
+
|
| 46 |
+
class UserWithInputs(User):
|
| 47 |
+
inputs: List[UserDataInputResponse] = []
|
| 48 |
+
|
| 49 |
+
class Config: # Added for consistency, though User already has it.
|
| 50 |
+
from_attributes = True
|
| 51 |
+
|
| 52 |
+
|
| 53 |
+
# --- Token Schemas ---
|
| 54 |
+
class Token(BaseModel):
|
| 55 |
+
access_token: str
|
| 56 |
+
token_type: str
|
| 57 |
+
|
| 58 |
+
class TokenData(BaseModel):
|
| 59 |
+
email: Optional[str] = None
|
| 60 |
+
|
| 61 |
+
|
| 62 |
+
# --- Chatbot Input Schemas (Specific to each model for validation) ---
|
| 63 |
+
# These can be more specific if needed, for now using a generic Dict
|
| 64 |
+
# For example, for Base Model:
|
| 65 |
+
class BaseModeInputSchema(BaseModel):
|
| 66 |
+
Salary: float
|
| 67 |
+
Expenses: float
|
| 68 |
+
Savings: float
|
| 69 |
+
Lifecycle_Stage: str
|
| 70 |
+
Risk_Appetite: str
|
| 71 |
+
Investment_Horizon: str
|
| 72 |
+
|
| 73 |
+
@field_validator('Salary', 'Expenses', 'Savings')
|
| 74 |
+
@classmethod
|
| 75 |
+
def check_non_negative_numeric(cls, v: Any, info: ValidationInfo) -> Any:
|
| 76 |
+
if not isinstance(v, (int, float)):
|
| 77 |
+
raise ValueError(f"{info.field_name} must be a number")
|
| 78 |
+
if v < 0:
|
| 79 |
+
raise ValueError(f"{info.field_name} cannot be negative")
|
| 80 |
+
return v
|
| 81 |
+
|
| 82 |
+
class Config:
|
| 83 |
+
from_attributes = True
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
class EnhancedModelInputSchema(BaseModel):
|
| 87 |
+
Profession: str
|
| 88 |
+
City: str
|
| 89 |
+
Salary: float
|
| 90 |
+
Expenses: float
|
| 91 |
+
Savings: float
|
| 92 |
+
Lifecycle_Stage: str
|
| 93 |
+
Risk_Appetite: str
|
| 94 |
+
Investment_Horizon: str
|
| 95 |
+
|
| 96 |
+
@field_validator('Salary', 'Expenses', 'Savings')
|
| 97 |
+
@classmethod
|
| 98 |
+
def check_non_negative_numeric(cls, v: Any, info: ValidationInfo) -> Any:
|
| 99 |
+
if not isinstance(v, (int, float)):
|
| 100 |
+
raise ValueError(f"{info.field_name} must be a number")
|
| 101 |
+
if v < 0:
|
| 102 |
+
raise ValueError(f"{info.field_name} cannot be negative")
|
| 103 |
+
return v
|
| 104 |
+
|
| 105 |
+
class Config:
|
| 106 |
+
from_attributes = True
|
| 107 |
+
|
| 108 |
+
class RuleBasedModelInputSchema(BaseModel):
|
| 109 |
+
Lifecycle_Stage: str
|
| 110 |
+
Risk_Appetite: str
|
| 111 |
+
Investment_Horizon: str
|
| 112 |
+
Annual_Salary_Package: float
|
| 113 |
+
Monthly_In_hand_Salary: float
|
| 114 |
+
Total_Monthly_Expenses: float
|
| 115 |
+
|
| 116 |
+
@field_validator('Annual_Salary_Package', 'Monthly_In_hand_Salary', 'Total_Monthly_Expenses')
|
| 117 |
+
@classmethod
|
| 118 |
+
def check_non_negative_numeric(cls, v: Any, info: ValidationInfo) -> Any:
|
| 119 |
+
if not isinstance(v, (int, float)):
|
| 120 |
+
raise ValueError(f"{info.field_name} must be a number")
|
| 121 |
+
if v < 0:
|
| 122 |
+
raise ValueError(f"{info.field_name} cannot be negative")
|
| 123 |
+
return v
|
| 124 |
+
|
| 125 |
+
class Config:
|
| 126 |
+
from_attributes = True
|
| 127 |
+
|
| 128 |
+
# For the chatbot interaction, the form will likely submit a dictionary.
|
| 129 |
+
# The specific schema can be used within the service layer before passing to the model.
|
| 130 |
+
class ChatbotInteractionRequest(BaseModel):
|
| 131 |
+
model_type: str # "base", "enhanced", "rule_based"
|
| 132 |
+
inputs: Dict[str, Any]
|
| 133 |
+
|
| 134 |
+
class ChatbotInteractionResponse(BaseModel):
|
| 135 |
+
model_type: str
|
| 136 |
+
user_inputs: Dict[str, Any]
|
| 137 |
+
recommendation: Dict[str, Any] # e.g., portfolio allocation
|
| 138 |
+
justification: Optional[List[str]] = None # For rule-based
|
| 139 |
+
tips: Optional[List[str]] = None # For rule-based
|
app/services/__pycache__/chatbot_service.cpython-312.pyc
ADDED
|
Binary file (13.5 kB). View file
|
|
|
app/services/__pycache__/data_service.cpython-312.pyc
ADDED
|
Binary file (5.82 kB). View file
|
|
|
app/services/chatbot_service.py
ADDED
|
@@ -0,0 +1,312 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import joblib
|
| 2 |
+
import pandas as pd
|
| 3 |
+
import os
|
| 4 |
+
from typing import Dict, Any, Tuple, List, Optional
|
| 5 |
+
from app import schemas # Assuming schemas.py is in the same 'app' directory
|
| 6 |
+
import logging
|
| 7 |
+
|
| 8 |
+
logger = logging.getLogger(__name__)
|
| 9 |
+
|
| 10 |
+
# --- Model Loading ---
|
| 11 |
+
# Define paths to the model files, assuming they are in app/ml_models/
|
| 12 |
+
MODEL_DIR = os.path.join(os.path.dirname(__file__), "..", "ml_models") # app/ml_models
|
| 13 |
+
BASE_MODEL_PATH = os.path.join(MODEL_DIR, "basic_portfolio_model.pkl")
|
| 14 |
+
ENHANCED_MODEL_PATH = os.path.join(MODEL_DIR, "enhanced_portfolio_model.pkl")
|
| 15 |
+
|
| 16 |
+
# Ensure the ml_models directory exists
|
| 17 |
+
os.makedirs(MODEL_DIR, exist_ok=True)
|
| 18 |
+
|
| 19 |
+
# Placeholder for loaded models
|
| 20 |
+
base_model_pipeline = None
|
| 21 |
+
enhanced_model_pipeline = None
|
| 22 |
+
|
| 23 |
+
def load_models():
|
| 24 |
+
global base_model_pipeline, enhanced_model_pipeline
|
| 25 |
+
try:
|
| 26 |
+
if os.path.exists(BASE_MODEL_PATH):
|
| 27 |
+
base_model_pipeline = joblib.load(BASE_MODEL_PATH)
|
| 28 |
+
logger.info("Base model loaded successfully.")
|
| 29 |
+
else:
|
| 30 |
+
logger.error(f"Base model file not found at {BASE_MODEL_PATH}")
|
| 31 |
+
|
| 32 |
+
if os.path.exists(ENHANCED_MODEL_PATH):
|
| 33 |
+
enhanced_model_pipeline = joblib.load(ENHANCED_MODEL_PATH)
|
| 34 |
+
logger.info("Enhanced model loaded successfully.")
|
| 35 |
+
else:
|
| 36 |
+
logger.error(f"Enhanced model file not found at {ENHANCED_MODEL_PATH}")
|
| 37 |
+
except Exception as e:
|
| 38 |
+
logger.error(f"Error loading ML models: {e}")
|
| 39 |
+
|
| 40 |
+
# Call load_models when this module is imported so they are ready.
|
| 41 |
+
# load_models() # Will be called from main.py or an init step
|
| 42 |
+
|
| 43 |
+
# --- Base Model Prediction ---
|
| 44 |
+
def predict_base_model(input_data: schemas.BaseModeInputSchema) -> Dict[str, float]:
|
| 45 |
+
if not base_model_pipeline:
|
| 46 |
+
logger.error("Base model not loaded. Cannot predict.")
|
| 47 |
+
# Consider raising an exception or returning an error state
|
| 48 |
+
return {"error": "Base model not available"}
|
| 49 |
+
|
| 50 |
+
try:
|
| 51 |
+
data = input_data.dict()
|
| 52 |
+
|
| 53 |
+
# Convert categorical features using mappings from the loaded pipeline
|
| 54 |
+
# These mappings should exist in the .pkl file if saved correctly
|
| 55 |
+
processed_data = data.copy()
|
| 56 |
+
processed_data['Lifecycle_Stage'] = base_model_pipeline['mappings']['lifecycle'][data['Lifecycle_Stage']]
|
| 57 |
+
processed_data['Risk_Appetite'] = base_model_pipeline['mappings']['risk'][data['Risk_Appetite']]
|
| 58 |
+
processed_data['Investment_Horizon'] = base_model_pipeline['mappings']['horizon'][data['Investment_Horizon']]
|
| 59 |
+
|
| 60 |
+
# Create feature array in correct order (must match training)
|
| 61 |
+
# Order from Reference/basemodel.py: Salary, Expenses, Savings, Lifecycle Stage, Risk Appetite, Investment Horizon
|
| 62 |
+
X = [
|
| 63 |
+
processed_data['Salary'],
|
| 64 |
+
processed_data['Expenses'],
|
| 65 |
+
processed_data['Savings'],
|
| 66 |
+
processed_data['Lifecycle_Stage'], # This is now numerical
|
| 67 |
+
processed_data['Risk_Appetite'], # This is now numerical
|
| 68 |
+
processed_data['Investment_Horizon'] # This is now numerical
|
| 69 |
+
]
|
| 70 |
+
|
| 71 |
+
X_scaled = base_model_pipeline['scaler'].transform([X])
|
| 72 |
+
pred = base_model_pipeline['model'].predict(X_scaled)[0]
|
| 73 |
+
|
| 74 |
+
total = pred.sum()
|
| 75 |
+
if total == 0: # Avoid division by zero
|
| 76 |
+
return {'Equity': 0, 'Debt': 0, 'Gold': 0, 'FD/Cash': 0, "error": "Prediction resulted in zero total"}
|
| 77 |
+
|
| 78 |
+
final_allocation = {
|
| 79 |
+
'Equity': round((pred[0]/total)*100, 1),
|
| 80 |
+
'Debt': round((pred[1]/total)*100, 1),
|
| 81 |
+
'Gold': round((pred[2]/total)*100, 1),
|
| 82 |
+
'FD/Cash': round((pred[3]/total)*100, 1)
|
| 83 |
+
}
|
| 84 |
+
return final_allocation
|
| 85 |
+
except KeyError as e:
|
| 86 |
+
logger.error(f"KeyError during base model prediction: {e}. Check mappings in pipeline or input data keys.")
|
| 87 |
+
return {"error": f"Missing data or incorrect mapping for {e}"}
|
| 88 |
+
except Exception as e:
|
| 89 |
+
logger.error(f"Error in base model prediction: {e}")
|
| 90 |
+
return {"error": str(e)}
|
| 91 |
+
|
| 92 |
+
|
| 93 |
+
# --- Enhanced Model Prediction ---
|
| 94 |
+
def predict_enhanced_model(input_data: schemas.EnhancedModelInputSchema) -> Dict[str, float]:
|
| 95 |
+
if not enhanced_model_pipeline:
|
| 96 |
+
logger.error("Enhanced model not loaded. Cannot predict.")
|
| 97 |
+
return {"error": "Enhanced model not available"}
|
| 98 |
+
|
| 99 |
+
try:
|
| 100 |
+
data = input_data.dict()
|
| 101 |
+
processed_data = data.copy()
|
| 102 |
+
|
| 103 |
+
# Convert categorical features
|
| 104 |
+
processed_data['Profession'] = enhanced_model_pipeline['profession_encoder'].transform([data['Profession']])[0]
|
| 105 |
+
processed_data['City'] = enhanced_model_pipeline['city_encoder'].transform([data['City']])[0]
|
| 106 |
+
processed_data['Lifecycle_Stage'] = enhanced_model_pipeline['mappings']['lifecycle'][data['Lifecycle_Stage']]
|
| 107 |
+
processed_data['Risk_Appetite'] = enhanced_model_pipeline['mappings']['risk'][data['Risk_Appetite']]
|
| 108 |
+
processed_data['Investment_Horizon'] = enhanced_model_pipeline['mappings']['horizon'][data['Investment_Horizon']]
|
| 109 |
+
|
| 110 |
+
# Order from Reference/enhancedmodel.py:
|
| 111 |
+
# ['Profession', 'City', 'Salary', 'Expenses', 'Savings', 'Lifecycle Stage', 'Risk Appetite', 'Investment Horizon']
|
| 112 |
+
X = [
|
| 113 |
+
processed_data['Profession'],
|
| 114 |
+
processed_data['City'],
|
| 115 |
+
processed_data['Salary'],
|
| 116 |
+
processed_data['Expenses'],
|
| 117 |
+
processed_data['Savings'],
|
| 118 |
+
processed_data['Lifecycle_Stage'],
|
| 119 |
+
processed_data['Risk_Appetite'],
|
| 120 |
+
processed_data['Investment_Horizon']
|
| 121 |
+
]
|
| 122 |
+
|
| 123 |
+
X_scaled = enhanced_model_pipeline['scaler'].transform([X])
|
| 124 |
+
pred = enhanced_model_pipeline['model'].predict(X_scaled)[0]
|
| 125 |
+
|
| 126 |
+
total = pred.sum()
|
| 127 |
+
if total == 0:
|
| 128 |
+
return {'Equity': 0, 'Debt': 0, 'Gold': 0, 'FD/Cash': 0, "error": "Prediction resulted in zero total"}
|
| 129 |
+
|
| 130 |
+
final_allocation = {
|
| 131 |
+
'Equity': round((pred[0]/total)*100, 1),
|
| 132 |
+
'Debt': round((pred[1]/total)*100, 1),
|
| 133 |
+
'Gold': round((pred[2]/total)*100, 1),
|
| 134 |
+
'FD/Cash': round((pred[3]/total)*100, 1)
|
| 135 |
+
}
|
| 136 |
+
return final_allocation
|
| 137 |
+
except KeyError as e:
|
| 138 |
+
logger.error(f"KeyError during enhanced model prediction: {e}. Check mappings/encoders in pipeline or input data keys.")
|
| 139 |
+
return {"error": f"Missing data or incorrect mapping/encoding for {e}"}
|
| 140 |
+
except Exception as e:
|
| 141 |
+
logger.error(f"Error in enhanced model prediction: {e}")
|
| 142 |
+
return {"error": str(e)}
|
| 143 |
+
|
| 144 |
+
# --- Rule-Based Model Logic ---
|
| 145 |
+
# (Adapted from Reference/rulebasedmodel.py)
|
| 146 |
+
|
| 147 |
+
def _allocate_savings_rule_based(
|
| 148 |
+
monthly_savings: float,
|
| 149 |
+
risk_profile: str,
|
| 150 |
+
horizon: str,
|
| 151 |
+
lifecycle: str
|
| 152 |
+
) -> Tuple[Dict[str, float], List[str]]:
|
| 153 |
+
allocations = {}
|
| 154 |
+
justification_points = [
|
| 155 |
+
f"This allocation considers your {risk_profile} risk profile, {horizon} horizon, and {lifecycle} stage."
|
| 156 |
+
]
|
| 157 |
+
risk_profile = risk_profile.lower() # Ensure consistency
|
| 158 |
+
horizon = horizon.lower()
|
| 159 |
+
# Lifecycle stage is already in a good format from schema e.g. "Early Career"
|
| 160 |
+
|
| 161 |
+
if risk_profile == "low":
|
| 162 |
+
allocations = {
|
| 163 |
+
"Fixed Deposits / Recurring Deposits": 0.35,
|
| 164 |
+
"Debt Mutual Funds (Liquid/Short Duration)": 0.30,
|
| 165 |
+
"Gold (SGBs/ETFs)": 0.10,
|
| 166 |
+
"Equity MFs (Large Cap/Index Funds)": 0.15,
|
| 167 |
+
"Cash / Savings Account": 0.10
|
| 168 |
+
}
|
| 169 |
+
justification_points.append("- Focus on capital preservation and stable returns.")
|
| 170 |
+
elif risk_profile == "medium":
|
| 171 |
+
allocations = {
|
| 172 |
+
"Equity MFs (Diversified - Large & Mid Cap/Flexi Cap)": 0.45,
|
| 173 |
+
"Debt Mutual Funds": 0.25,
|
| 174 |
+
"Fixed Deposits / PPF": 0.10,
|
| 175 |
+
"Gold (SGBs/ETFs)": 0.10,
|
| 176 |
+
"Equity MFs (International)": 0.05,
|
| 177 |
+
"Cash / Savings Account": 0.05
|
| 178 |
+
}
|
| 179 |
+
justification_points.append("- Aims for a balance between growth and stability.")
|
| 180 |
+
elif risk_profile == "high":
|
| 181 |
+
allocations = {
|
| 182 |
+
"Equity MFs (Aggressive Growth - Mid/Small Cap, Thematic)": 0.60,
|
| 183 |
+
"Equity MFs (International)": 0.15,
|
| 184 |
+
"Direct Stocks (If experienced)": 0.10,
|
| 185 |
+
"Debt Mutual Funds (Strategic)": 0.05,
|
| 186 |
+
"Gold (SGBs/ETFs - Tactical)": 0.05,
|
| 187 |
+
"Alternative Inv. (REITs/InvITs)": 0.05
|
| 188 |
+
}
|
| 189 |
+
justification_points.append("- Focuses on maximizing long-term growth potential.")
|
| 190 |
+
|
| 191 |
+
# Normalize percentages (simplified for brevity, original script had more robust normalization)
|
| 192 |
+
current_total_percentage = sum(allocations.values())
|
| 193 |
+
if abs(current_total_percentage - 1.0) > 0.001 and current_total_percentage != 0:
|
| 194 |
+
factor = 1.0 / current_total_percentage
|
| 195 |
+
allocations = {k: round(v * factor, 3) for k, v in allocations.items()}
|
| 196 |
+
# Adjust last item to make sum exactly 1.0 if needed due to rounding
|
| 197 |
+
sum_val = sum(allocations.values())
|
| 198 |
+
if sum_val != 1.0 and allocations:
|
| 199 |
+
key_to_adjust = list(allocations.keys())[-1]
|
| 200 |
+
allocations[key_to_adjust] += (1.0 - sum_val)
|
| 201 |
+
allocations[key_to_adjust] = round(allocations[key_to_adjust], 3)
|
| 202 |
+
|
| 203 |
+
|
| 204 |
+
# Convert percentage values to actual amounts based on monthly_savings
|
| 205 |
+
# The chatbot response schema expects percentages for recommendation, so we return percentages.
|
| 206 |
+
# The display logic in the template can calculate amounts.
|
| 207 |
+
|
| 208 |
+
# For the ChatbotInteractionResponse, recommendation should be percentages
|
| 209 |
+
# Let's ensure the values are percentages (0 to 100)
|
| 210 |
+
final_percentage_allocations = {k: round(v * 100, 1) for k,v in allocations.items()}
|
| 211 |
+
|
| 212 |
+
return final_percentage_allocations, justification_points
|
| 213 |
+
|
| 214 |
+
|
| 215 |
+
def _get_general_financial_tips(lifecycle_stage: str, monthly_savings: float, monthly_in_hand_salary: float) -> List[str]:
|
| 216 |
+
tips = []
|
| 217 |
+
if monthly_in_hand_salary > 0: # Avoid division by zero if salary is 0
|
| 218 |
+
if monthly_savings <= 0 :
|
| 219 |
+
tips.append("- Your expenses currently meet or exceed your income. Focus on creating a budget.")
|
| 220 |
+
else:
|
| 221 |
+
savings_rate = (monthly_savings / monthly_in_hand_salary) * 100
|
| 222 |
+
tips.append(f"- Your current savings rate is approximately {savings_rate:.1f}%. Aim for at least 20-30%.")
|
| 223 |
+
else: # No salary info or zero salary
|
| 224 |
+
if monthly_savings <= 0:
|
| 225 |
+
tips.append("- Your expenses currently meet or exceed your income. Focus on creating a budget.")
|
| 226 |
+
|
| 227 |
+
|
| 228 |
+
tips.extend([
|
| 229 |
+
"- Build & Maintain an Emergency Fund: Aim for 3-6 months of essential living expenses.",
|
| 230 |
+
"- Get Adequately Insured: Health and Term Life Insurance are crucial.",
|
| 231 |
+
"- Invest Regularly & Be Disciplined (e.g. SIPs).",
|
| 232 |
+
"- Review Periodically: Revisit your financial plan annually or on major life events.",
|
| 233 |
+
"- Understand Your Investments: Know the risks and costs."
|
| 234 |
+
])
|
| 235 |
+
if lifecycle_stage == "Student" or lifecycle_stage == "Early Career":
|
| 236 |
+
tips.append("- Focus on upskilling and career growth.")
|
| 237 |
+
if lifecycle_stage == "Late Career/Pre-Retirement" or lifecycle_stage == "Retired": # Assuming "Retired" is a lifecycle stage
|
| 238 |
+
tips.append("- Plan for healthcare expenses in retirement.")
|
| 239 |
+
return tips
|
| 240 |
+
|
| 241 |
+
def predict_rule_based_model(input_data: schemas.RuleBasedModelInputSchema) -> Tuple[Dict[str, float], List[str], List[str]]:
|
| 242 |
+
data = input_data.dict()
|
| 243 |
+
|
| 244 |
+
monthly_savings = data['Monthly_In_hand_Salary'] - data['Total_Monthly_Expenses']
|
| 245 |
+
|
| 246 |
+
if monthly_savings <= 0:
|
| 247 |
+
allocation = {"message": "Savings are zero or negative. Focus on budgeting first."}
|
| 248 |
+
justification = ["Investment allocation is not applicable without positive savings."]
|
| 249 |
+
tips = _get_general_financial_tips(data['Lifecycle_Stage'], monthly_savings, data['Monthly_In_hand_Salary'])
|
| 250 |
+
tips.insert(0, "Priority: Increase savings or reduce expenses.")
|
| 251 |
+
return allocation, justification, tips
|
| 252 |
+
|
| 253 |
+
allocation, justification = _allocate_savings_rule_based(
|
| 254 |
+
monthly_savings,
|
| 255 |
+
data['Risk_Appetite'],
|
| 256 |
+
data['Investment_Horizon'],
|
| 257 |
+
data['Lifecycle_Stage']
|
| 258 |
+
)
|
| 259 |
+
tips = _get_general_financial_tips(data['Lifecycle_Stage'], monthly_savings, data['Monthly_In_hand_Salary'])
|
| 260 |
+
|
| 261 |
+
return allocation, justification, tips
|
| 262 |
+
|
| 263 |
+
|
| 264 |
+
# --- Main Chatbot Interaction Logic ---
|
| 265 |
+
def process_chatbot_interaction(
|
| 266 |
+
request: schemas.ChatbotInteractionRequest
|
| 267 |
+
) -> schemas.ChatbotInteractionResponse:
|
| 268 |
+
|
| 269 |
+
model_type = request.model_type.lower()
|
| 270 |
+
inputs = request.inputs
|
| 271 |
+
|
| 272 |
+
recommendation: Dict[str, Any] = {}
|
| 273 |
+
justification: Optional[List[str]] = None
|
| 274 |
+
tips: Optional[List[str]] = None
|
| 275 |
+
|
| 276 |
+
try:
|
| 277 |
+
if model_type == "base":
|
| 278 |
+
# Validate inputs against BaseModeInputSchema
|
| 279 |
+
validated_inputs = schemas.BaseModeInputSchema(**inputs)
|
| 280 |
+
recommendation = predict_base_model(validated_inputs)
|
| 281 |
+
elif model_type == "enhanced":
|
| 282 |
+
validated_inputs = schemas.EnhancedModelInputSchema(**inputs)
|
| 283 |
+
recommendation = predict_enhanced_model(validated_inputs)
|
| 284 |
+
elif model_type == "rule_based":
|
| 285 |
+
validated_inputs = schemas.RuleBasedModelInputSchema(**inputs)
|
| 286 |
+
recommendation, justification, tips = predict_rule_based_model(validated_inputs)
|
| 287 |
+
else:
|
| 288 |
+
raise ValueError("Invalid model type specified")
|
| 289 |
+
|
| 290 |
+
# Check for errors from prediction functions
|
| 291 |
+
if "error" in recommendation:
|
| 292 |
+
# Propagate the error message
|
| 293 |
+
return schemas.ChatbotInteractionResponse(
|
| 294 |
+
model_type=model_type,
|
| 295 |
+
user_inputs=inputs,
|
| 296 |
+
recommendation=recommendation, # Contains the error message
|
| 297 |
+
justification=None,
|
| 298 |
+
tips=None
|
| 299 |
+
)
|
| 300 |
+
|
| 301 |
+
except Exception as e: # Catch validation errors or other issues
|
| 302 |
+
logger.error(f"Error processing chatbot interaction for {model_type}: {e}")
|
| 303 |
+
recommendation = {"error": f"Failed to process request: {str(e)}"}
|
| 304 |
+
|
| 305 |
+
|
| 306 |
+
return schemas.ChatbotInteractionResponse(
|
| 307 |
+
model_type=model_type,
|
| 308 |
+
user_inputs=inputs, # Return the original inputs for display
|
| 309 |
+
recommendation=recommendation,
|
| 310 |
+
justification=justification,
|
| 311 |
+
tips=tips
|
| 312 |
+
)
|
app/services/data_service.py
ADDED
|
@@ -0,0 +1,129 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
import csv
|
| 2 |
+
import os
|
| 3 |
+
from datetime import datetime
|
| 4 |
+
from typing import Dict, Any, List, Optional
|
| 5 |
+
import logging
|
| 6 |
+
|
| 7 |
+
# Configure logging
|
| 8 |
+
logging.basicConfig(level=logging.INFO)
|
| 9 |
+
logger = logging.getLogger(__name__)
|
| 10 |
+
|
| 11 |
+
# Define the path for the CSV file
|
| 12 |
+
# Ensure this path is correct relative to where the application runs
|
| 13 |
+
# For example, if main.py is in app/, and this service is called from there.
|
| 14 |
+
CSV_FILE_PATH = "user_financial_data.csv"
|
| 15 |
+
# This will create the file in the same directory as main.py if it's in the root,
|
| 16 |
+
# or in the 'app' directory if main.py is in 'app' and this path is used as is.
|
| 17 |
+
# For consistency with the sqlite DB being in the root, let's adjust:
|
| 18 |
+
CSV_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "user_financial_data.csv")
|
| 19 |
+
# This places it in the project root directory (financial_advisor/)
|
| 20 |
+
|
| 21 |
+
# Define the headers for the CSV file based on 'aboutdata.txt' and additional fields
|
| 22 |
+
# Ensure all possible fields from all models are covered.
|
| 23 |
+
CSV_HEADERS = [
|
| 24 |
+
'UserID', 'UserEmail', 'ModelType', 'Timestamp',
|
| 25 |
+
'Name', 'Age', 'City', 'Profession', 'Salary', 'Expenses', 'Savings',
|
| 26 |
+
'Lifecycle Stage', 'Risk Appetite', 'Investment Horizon',
|
| 27 |
+
'Annual Salary Package', 'Monthly In-hand Salary', 'Total Monthly Expenses', # For Rule-based
|
| 28 |
+
# Output fields from models (optional, but good for record keeping)
|
| 29 |
+
'Equity (%)', 'Debt (%)', 'Gold (%)', 'FD/Cash (%)'
|
| 30 |
+
]
|
| 31 |
+
|
| 32 |
+
|
| 33 |
+
def initialize_csv():
|
| 34 |
+
"""Initializes the CSV file with headers if it doesn't exist."""
|
| 35 |
+
# Ensure the directory for the CSV file exists
|
| 36 |
+
os.makedirs(os.path.dirname(CSV_FILE_PATH), exist_ok=True)
|
| 37 |
+
|
| 38 |
+
if not os.path.exists(CSV_FILE_PATH):
|
| 39 |
+
try:
|
| 40 |
+
with open(CSV_FILE_PATH, mode='w', newline='', encoding='utf-8') as file:
|
| 41 |
+
writer = csv.writer(file)
|
| 42 |
+
writer.writerow(CSV_HEADERS)
|
| 43 |
+
logger.info(f"CSV file initialized at {CSV_FILE_PATH}")
|
| 44 |
+
except IOError as e:
|
| 45 |
+
logger.error(f"Error initializing CSV file: {e}")
|
| 46 |
+
|
| 47 |
+
def append_data_to_csv(user_id: int, user_email: str, model_type: str, input_data: Dict[str, Any], output_data: Optional[Dict[str, Any]] = None):
|
| 48 |
+
"""
|
| 49 |
+
Appends a new row of data to the CSV file.
|
| 50 |
+
input_data should be a flat dictionary.
|
| 51 |
+
output_data is the portfolio allocation.
|
| 52 |
+
"""
|
| 53 |
+
if not os.path.exists(CSV_FILE_PATH):
|
| 54 |
+
initialize_csv()
|
| 55 |
+
|
| 56 |
+
row_data = {header: '' for header in CSV_HEADERS} # Initialize with empty strings
|
| 57 |
+
|
| 58 |
+
# Populate common fields
|
| 59 |
+
row_data['UserID'] = user_id
|
| 60 |
+
row_data['UserEmail'] = user_email
|
| 61 |
+
row_data['ModelType'] = model_type
|
| 62 |
+
row_data['Timestamp'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
| 63 |
+
|
| 64 |
+
# Populate fields from input_data
|
| 65 |
+
# Normalize keys from input_data (e.g., 'Lifecycle_Stage' to 'Lifecycle Stage')
|
| 66 |
+
normalized_input_data = {key.replace('_', ' '): value for key, value in input_data.items()}
|
| 67 |
+
|
| 68 |
+
for key, value in normalized_input_data.items():
|
| 69 |
+
if key in row_data:
|
| 70 |
+
row_data[key] = value
|
| 71 |
+
# Handle specific keys that might have different names in input_data
|
| 72 |
+
elif key == "Annual Salary Package" and "Annual_Salary_Package" in normalized_input_data: # from RuleBasedModelInputSchema
|
| 73 |
+
row_data["Annual Salary Package"] = normalized_input_data["Annual_Salary_Package"]
|
| 74 |
+
# Add more specific mappings if needed
|
| 75 |
+
|
| 76 |
+
# Populate fields from output_data (portfolio allocation)
|
| 77 |
+
if output_data:
|
| 78 |
+
for key, value in output_data.items():
|
| 79 |
+
# Ensure the key from output_data matches a header like "Equity (%)"
|
| 80 |
+
header_key = f"{key} (%)"
|
| 81 |
+
if header_key in row_data:
|
| 82 |
+
row_data[header_key] = value
|
| 83 |
+
elif key in row_data: # For keys like 'Equity', 'Debt' if not with '(%)'
|
| 84 |
+
row_data[key] = value
|
| 85 |
+
|
| 86 |
+
|
| 87 |
+
# Ensure the order of data matches CSV_HEADERS
|
| 88 |
+
ordered_row_values = [row_data.get(header, '') for header in CSV_HEADERS]
|
| 89 |
+
|
| 90 |
+
try:
|
| 91 |
+
with open(CSV_FILE_PATH, mode='a', newline='', encoding='utf-8') as file:
|
| 92 |
+
writer = csv.writer(file)
|
| 93 |
+
writer.writerow(ordered_row_values)
|
| 94 |
+
logger.info(f"Data appended to CSV for user {user_email}, model {model_type}")
|
| 95 |
+
except IOError as e:
|
| 96 |
+
logger.error(f"Error appending data to CSV: {e}")
|
| 97 |
+
except Exception as e:
|
| 98 |
+
logger.error(f"An unexpected error occurred while writing to CSV: {e}")
|
| 99 |
+
|
| 100 |
+
# Example usage (for testing this module independently):
|
| 101 |
+
if __name__ == "__main__":
|
| 102 |
+
initialize_csv()
|
| 103 |
+
|
| 104 |
+
# Test data
|
| 105 |
+
sample_input_base = {
|
| 106 |
+
'Salary': 60000, 'Expenses': 40000, 'Savings': 20000,
|
| 107 |
+
'Lifecycle_Stage': 'Early Career', 'Risk_Appetite': 'Medium', 'Investment_Horizon': 'Long-term'
|
| 108 |
+
}
|
| 109 |
+
sample_output = {'Equity': 50, 'Debt': 30, 'Gold': 10, 'FD/Cash': 10}
|
| 110 |
+
append_data_to_csv(1, "test@example.com", "base", sample_input_base, sample_output)
|
| 111 |
+
|
| 112 |
+
sample_input_enhanced = {
|
| 113 |
+
'Profession': 'Engineer', 'City': 'Metropolis',
|
| 114 |
+
'Salary': 120000, 'Expenses': 70000, 'Savings': 50000,
|
| 115 |
+
'Lifecycle_Stage': 'Mid-Career', 'Risk_Appetite': 'High', 'Investment_Horizon': 'Long-term'
|
| 116 |
+
}
|
| 117 |
+
append_data_to_csv(2, "another@example.com", "enhanced", sample_input_enhanced, sample_output)
|
| 118 |
+
|
| 119 |
+
sample_input_rule = {
|
| 120 |
+
'Lifecycle_Stage': 'Late Career',
|
| 121 |
+
'Risk_Appetite': 'Low',
|
| 122 |
+
'Investment_Horizon': 'Short-term',
|
| 123 |
+
'Annual_Salary_Package': 1000000,
|
| 124 |
+
'Monthly_In_hand_Salary': 60000,
|
| 125 |
+
'Total_Monthly_Expenses': 40000
|
| 126 |
+
}
|
| 127 |
+
append_data_to_csv(3, "ruleuser@example.com", "rule_based", sample_input_rule, sample_output)
|
| 128 |
+
|
| 129 |
+
logger.info(f"Test data written to {CSV_FILE_PATH}")
|
app/static/css/custom_styles.css
ADDED
|
File without changes
|
app/static/css/style.css
ADDED
|
@@ -0,0 +1,112 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
body {
|
| 2 |
+
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
|
| 3 |
+
background-color: #f8f9fa; /* Light grey background */
|
| 4 |
+
display: flex;
|
| 5 |
+
flex-direction: column;
|
| 6 |
+
min-height: 100vh;
|
| 7 |
+
}
|
| 8 |
+
|
| 9 |
+
.container {
|
| 10 |
+
flex: 1;
|
| 11 |
+
}
|
| 12 |
+
|
| 13 |
+
.navbar {
|
| 14 |
+
box-shadow: 0 2px 4px rgba(0,0,0,.1);
|
| 15 |
+
}
|
| 16 |
+
|
| 17 |
+
.card {
|
| 18 |
+
border: none; /* Softer look, rely on shadow */
|
| 19 |
+
transition: transform .2s ease-in-out, box-shadow .2s ease-in-out;
|
| 20 |
+
}
|
| 21 |
+
|
| 22 |
+
.card:hover {
|
| 23 |
+
transform: translateY(-5px);
|
| 24 |
+
box-shadow: 0 0.5rem 1rem rgba(0,0,0,.15) !important;
|
| 25 |
+
}
|
| 26 |
+
|
| 27 |
+
.card-header {
|
| 28 |
+
font-weight: 500;
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
.btn-primary {
|
| 32 |
+
background-color: #007bff;
|
| 33 |
+
border-color: #007bff;
|
| 34 |
+
}
|
| 35 |
+
.btn-primary:hover {
|
| 36 |
+
background-color: #0056b3;
|
| 37 |
+
border-color: #0056b3;
|
| 38 |
+
}
|
| 39 |
+
|
| 40 |
+
.btn-success {
|
| 41 |
+
background-color: #28a745;
|
| 42 |
+
border-color: #28a745;
|
| 43 |
+
}
|
| 44 |
+
.btn-success:hover {
|
| 45 |
+
background-color: #1e7e34;
|
| 46 |
+
border-color: #1e7e34;
|
| 47 |
+
}
|
| 48 |
+
|
| 49 |
+
.btn-info {
|
| 50 |
+
background-color: #17a2b8;
|
| 51 |
+
border-color: #17a2b8;
|
| 52 |
+
}
|
| 53 |
+
.btn-info:hover {
|
| 54 |
+
background-color: #117a8b;
|
| 55 |
+
border-color: #117a8b;
|
| 56 |
+
}
|
| 57 |
+
|
| 58 |
+
|
| 59 |
+
/* Specific to chatbot results for better visual separation */
|
| 60 |
+
#recommendationDetails ul .list-group-item span.badge {
|
| 61 |
+
font-size: 0.9em;
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
/* Footer styling */
|
| 65 |
+
footer.bg-light {
|
| 66 |
+
background-color: #e9ecef !important; /* A slightly different shade for footer */
|
| 67 |
+
padding-top: 1rem;
|
| 68 |
+
padding-bottom: 1rem;
|
| 69 |
+
}
|
| 70 |
+
|
| 71 |
+
/* Responsive adjustments */
|
| 72 |
+
@media (max-width: 768px) {
|
| 73 |
+
.display-4 {
|
| 74 |
+
font-size: 2.5rem;
|
| 75 |
+
}
|
| 76 |
+
h1 {
|
| 77 |
+
font-size: 1.75rem;
|
| 78 |
+
}
|
| 79 |
+
}
|
| 80 |
+
|
| 81 |
+
/* Ensure body takes full height for sticky footer */
|
| 82 |
+
html, body {
|
| 83 |
+
height: 100%;
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
/* --- Chatbot Interface Styles --- */
|
| 87 |
+
#chatbox {
|
| 88 |
+
height: 400px;
|
| 89 |
+
overflow-y: auto;
|
| 90 |
+
border: 1px solid #ccc;
|
| 91 |
+
padding: 10px;
|
| 92 |
+
margin-bottom: 15px;
|
| 93 |
+
background-color: #f9f9f9;
|
| 94 |
+
border-radius: 5px;
|
| 95 |
+
}
|
| 96 |
+
.chat-message { margin-bottom: 10px; }
|
| 97 |
+
.bot-message { text-align: left; }
|
| 98 |
+
.user-message { text-align: right; }
|
| 99 |
+
.message-bubble {
|
| 100 |
+
display: inline-block;
|
| 101 |
+
padding: 8px 12px;
|
| 102 |
+
border-radius: 15px;
|
| 103 |
+
max-width: 80%;
|
| 104 |
+
word-wrap: break-word; /* Ensure long words break */
|
| 105 |
+
}
|
| 106 |
+
.bot-message .message-bubble { background-color: #e9ecef; color: #333; }
|
| 107 |
+
.user-message .message-bubble { background-color: #0d6efd; color: white; }
|
| 108 |
+
#userInputArea button.option-button { margin: 3px; }
|
| 109 |
+
#userInputArea { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } /* Added wrap */
|
| 110 |
+
#userInputArea input, #userInputArea select { flex-grow: 1; min-width: 150px; } /* Added min-width */
|
| 111 |
+
#optionsContainer { display: flex; flex-wrap: wrap; gap: 5px; } /* Styling for button container */
|
| 112 |
+
.spinner-border-sm { width: 1rem; height: 1rem; }
|
app/static/js/script.js
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
// Global JavaScript functions can be added here.
|
| 2 |
+
|
| 3 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 4 |
+
// Example: Add a class to the body to indicate JS is enabled
|
| 5 |
+
document.body.classList.add('js-enabled');
|
| 6 |
+
|
| 7 |
+
// You could centralize common functions here if needed,
|
| 8 |
+
// for example, a function to make API calls with error handling.
|
| 9 |
+
// const apiRequest = async (url, method, body = null, headers = {}) => { ... }
|
| 10 |
+
|
| 11 |
+
const logoutButton = document.getElementById('logoutButton');
|
| 12 |
+
if (logoutButton) {
|
| 13 |
+
logoutButton.addEventListener('click', function (event) {
|
| 14 |
+
event.preventDefault(); // Prevent default anchor action
|
| 15 |
+
|
| 16 |
+
localStorage.removeItem('accessToken');
|
| 17 |
+
localStorage.removeItem('tokenType');
|
| 18 |
+
|
| 19 |
+
// Redirect to the server's /logout endpoint, which then redirects to login page
|
| 20 |
+
// This ensures any server-side session cleanup (if implemented later) could also occur.
|
| 21 |
+
window.location.href = '/logout';
|
| 22 |
+
});
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
+
// Client-side check to update navbar based on token presence
|
| 26 |
+
// This runs on every page load to set the correct initial state for navbar items.
|
| 27 |
+
const accessToken = localStorage.getItem('accessToken');
|
| 28 |
+
const navWelcomeItem = document.getElementById('navWelcomeItem');
|
| 29 |
+
const navLogoutItem = document.getElementById('navLogoutItem');
|
| 30 |
+
const navLoginItem = document.getElementById('navLoginItem');
|
| 31 |
+
const navSignupItem = document.getElementById('navSignupItem');
|
| 32 |
+
|
| 33 |
+
if (accessToken) {
|
| 34 |
+
// User is logged in (according to localStorage)
|
| 35 |
+
if (navWelcomeItem) navWelcomeItem.style.display = 'block'; // Or 'list-item' if needed
|
| 36 |
+
if (navLogoutItem) navLogoutItem.style.display = 'block';
|
| 37 |
+
if (navLoginItem) navLoginItem.style.display = 'none';
|
| 38 |
+
if (navSignupItem) navSignupItem.style.display = 'none';
|
| 39 |
+
|
| 40 |
+
// Optional: Fetch user email to display in welcome message
|
| 41 |
+
// Could be done here or within specific page scripts like homepage.html
|
| 42 |
+
// fetch('/auth/users/me', { headers: {'Authorization': `Bearer ${accessToken}`} })
|
| 43 |
+
// .then(response => response.ok ? response.json() : Promise.reject('Failed'))
|
| 44 |
+
// .then(data => {
|
| 45 |
+
// const welcomeSpan = document.getElementById('navUserWelcome');
|
| 46 |
+
// if (welcomeSpan) welcomeSpan.textContent = `Welcome, ${data.email}`;
|
| 47 |
+
// })
|
| 48 |
+
// .catch(err => console.error("Error fetching user for navbar:", err));
|
| 49 |
+
|
| 50 |
+
} else {
|
| 51 |
+
// User is logged out
|
| 52 |
+
if (navWelcomeItem) navWelcomeItem.style.display = 'none';
|
| 53 |
+
if (navLogoutItem) navLogoutItem.style.display = 'none';
|
| 54 |
+
if (navLoginItem) navLoginItem.style.display = 'block';
|
| 55 |
+
if (navSignupItem) navSignupItem.style.display = 'block';
|
| 56 |
+
}
|
| 57 |
+
});
|
| 58 |
+
|
| 59 |
+
// Function to get CSRF token if using CSRF protection with forms not handled by FastAPI's default
|
| 60 |
+
// function getCookie(name) { ... }
|
| 61 |
+
// Not strictly needed for this JWT setup unless forms post directly without JS.
|
app/templates/admin/dashboard.html
ADDED
|
@@ -0,0 +1,175 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "partials/base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Admin Dashboard - Financial Advisor{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="container mt-5">
|
| 7 |
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
| 8 |
+
<h1>Admin Dashboard</h1>
|
| 9 |
+
<span id="adminWelcomeMessage">Welcome, Admin!</span> {# Placeholder #}
|
| 10 |
+
</div>
|
| 11 |
+
<div id="authErrorMessage" class="alert alert-danger" role="alert" style="display: none;">
|
| 12 |
+
Authentication failed or insufficient privileges. Redirecting...
|
| 13 |
+
</div>
|
| 14 |
+
<div id="loadingMessage" class="alert alert-info" role="alert">
|
| 15 |
+
Loading dashboard data...
|
| 16 |
+
</div>
|
| 17 |
+
|
| 18 |
+
<!-- Search Users -->
|
| 19 |
+
<div id="searchCard" class="card shadow-sm mb-4" style="display: none;">
|
| 20 |
+
<div class="card-body">
|
| 21 |
+
<h5 class="card-title">Search Users</h5>
|
| 22 |
+
<form id="searchForm" method="get"> {# Changed action, will be handled by JS or page reload #}
|
| 23 |
+
<div class="input-group">
|
| 24 |
+
<input type="text" class="form-control" placeholder="Search by user email..." name="search" id="searchInput">
|
| 25 |
+
<button class="btn btn-outline-primary" type="submit">Search</button>
|
| 26 |
+
</div>
|
| 27 |
+
</form>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
|
| 31 |
+
<!-- Users Table -->
|
| 32 |
+
<div id="usersTableCard" class="card shadow-sm" style="display: none;">
|
| 33 |
+
<div class="card-header bg-secondary text-white">
|
| 34 |
+
<h4 class="mb-0">Registered Users</h4>
|
| 35 |
+
</div>
|
| 36 |
+
<div class="card-body">
|
| 37 |
+
<div class="table-responsive">
|
| 38 |
+
<table class="table table-striped table-hover">
|
| 39 |
+
<thead>
|
| 40 |
+
<tr>
|
| 41 |
+
<th scope="col">ID</th>
|
| 42 |
+
<th scope="col">Email</th>
|
| 43 |
+
<th scope="col">Is Admin?</th>
|
| 44 |
+
<th scope="col">Registered At</th>
|
| 45 |
+
<th scope="col">Actions</th>
|
| 46 |
+
</tr>
|
| 47 |
+
</thead>
|
| 48 |
+
<tbody id="usersTableBody">
|
| 49 |
+
<!-- User rows will be inserted here by JS -->
|
| 50 |
+
</tbody>
|
| 51 |
+
</table>
|
| 52 |
+
</div>
|
| 53 |
+
<p id="noUsersMessage" class="text-muted" style="display: none;"></p>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
|
| 57 |
+
<!-- Potentially add other admin functionalities here, like viewing all data inputs -->
|
| 58 |
+
|
| 59 |
+
</div>
|
| 60 |
+
{% endblock %}
|
| 61 |
+
|
| 62 |
+
{% block scripts_extra %}
|
| 63 |
+
<script>
|
| 64 |
+
document.addEventListener('DOMContentLoaded', async function () {
|
| 65 |
+
const accessToken = localStorage.getItem('accessToken');
|
| 66 |
+
const loadingMessage = document.getElementById('loadingMessage');
|
| 67 |
+
const authErrorMessage = document.getElementById('authErrorMessage');
|
| 68 |
+
const searchCard = document.getElementById('searchCard');
|
| 69 |
+
const usersTableCard = document.getElementById('usersTableCard');
|
| 70 |
+
const usersTableBody = document.getElementById('usersTableBody');
|
| 71 |
+
const noUsersMessage = document.getElementById('noUsersMessage');
|
| 72 |
+
const adminWelcomeMessage = document.getElementById('adminWelcomeMessage');
|
| 73 |
+
const searchForm = document.getElementById('searchForm');
|
| 74 |
+
const searchInput = document.getElementById('searchInput');
|
| 75 |
+
|
| 76 |
+
// 1. Check Authentication and Admin Status
|
| 77 |
+
if (!accessToken) {
|
| 78 |
+
authErrorMessage.textContent = 'No authentication token found. Redirecting to login...';
|
| 79 |
+
authErrorMessage.style.display = 'block';
|
| 80 |
+
loadingMessage.style.display = 'none';
|
| 81 |
+
setTimeout(() => { window.location.href = "{{ url_for('login_page_render') }}"; }, 2000);
|
| 82 |
+
return;
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
try {
|
| 86 |
+
const userMeResponse = await fetch("{{ url_for('read_users_me') }}", {
|
| 87 |
+
headers: { 'Authorization': `Bearer ${accessToken}` }
|
| 88 |
+
});
|
| 89 |
+
|
| 90 |
+
if (!userMeResponse.ok) {
|
| 91 |
+
throw new Error(`Authentication failed: ${userMeResponse.status}`);
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
const currentUser = await userMeResponse.json();
|
| 95 |
+
if (!currentUser.is_admin) {
|
| 96 |
+
throw new Error('User is not authorized to view this page.');
|
| 97 |
+
}
|
| 98 |
+
|
| 99 |
+
// Update welcome message
|
| 100 |
+
if(adminWelcomeMessage) adminWelcomeMessage.textContent = `Welcome, ${currentUser.email} (Admin)`;
|
| 101 |
+
|
| 102 |
+
// 2. Fetch Dashboard Data (handle search query)
|
| 103 |
+
const urlParams = new URLSearchParams(window.location.search);
|
| 104 |
+
const searchQuery = urlParams.get('search');
|
| 105 |
+
if (searchQuery && searchInput) {
|
| 106 |
+
searchInput.value = searchQuery; // Populate search box if query exists
|
| 107 |
+
}
|
| 108 |
+
|
| 109 |
+
const dataUrl = searchQuery
|
| 110 |
+
? `/admin/api/dashboard-data?search=${encodeURIComponent(searchQuery)}`
|
| 111 |
+
: "/admin/api/dashboard-data";
|
| 112 |
+
|
| 113 |
+
const dashboardDataResponse = await fetch(dataUrl, {
|
| 114 |
+
headers: { 'Authorization': `Bearer ${accessToken}` }
|
| 115 |
+
});
|
| 116 |
+
|
| 117 |
+
if (!dashboardDataResponse.ok) {
|
| 118 |
+
throw new Error(`Failed to fetch dashboard data: ${dashboardDataResponse.status}`);
|
| 119 |
+
}
|
| 120 |
+
|
| 121 |
+
const data = await dashboardDataResponse.json();
|
| 122 |
+
|
| 123 |
+
// 3. Populate Table
|
| 124 |
+
usersTableBody.innerHTML = ''; // Clear previous results
|
| 125 |
+
if (data.users_list && data.users_list.length > 0) {
|
| 126 |
+
noUsersMessage.style.display = 'none';
|
| 127 |
+
data.users_list.forEach(user => {
|
| 128 |
+
const row = usersTableBody.insertRow();
|
| 129 |
+
row.innerHTML = `
|
| 130 |
+
<th scope="row">${user.id}</th>
|
| 131 |
+
<td>${user.email}</td>
|
| 132 |
+
<td>
|
| 133 |
+
<span class="badge bg-${user.is_admin ? 'success' : 'secondary'}">
|
| 134 |
+
${user.is_admin ? 'Yes' : 'No'}
|
| 135 |
+
</span>
|
| 136 |
+
</td>
|
| 137 |
+
<td>${new Date(user.created_at).toLocaleString()}</td>
|
| 138 |
+
<td>
|
| 139 |
+
<a href="/admin/users/${user.id}" class="btn btn-sm btn-info">View Details</a>
|
| 140 |
+
</td>
|
| 141 |
+
`;
|
| 142 |
+
// Note: Using hardcoded URL /admin/users/${user.id} as url_for isn't available client-side easily.
|
| 143 |
+
// Ensure this matches the route defined in admin.py for admin_view_user_details_shell
|
| 144 |
+
});
|
| 145 |
+
} else {
|
| 146 |
+
noUsersMessage.textContent = searchQuery ? 'No users found matching your search.' : 'No users registered yet.';
|
| 147 |
+
noUsersMessage.style.display = 'block';
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
// Show content now that data is loaded
|
| 151 |
+
loadingMessage.style.display = 'none';
|
| 152 |
+
searchCard.style.display = 'block';
|
| 153 |
+
usersTableCard.style.display = 'block';
|
| 154 |
+
|
| 155 |
+
} catch (error) {
|
| 156 |
+
console.error('Error loading admin dashboard:', error);
|
| 157 |
+
loadingMessage.style.display = 'none';
|
| 158 |
+
authErrorMessage.textContent = `Error: ${error.message}. Redirecting...`;
|
| 159 |
+
authErrorMessage.style.display = 'block';
|
| 160 |
+
localStorage.removeItem('accessToken'); // Clear token on error
|
| 161 |
+
localStorage.removeItem('tokenType');
|
| 162 |
+
setTimeout(() => { window.location.href = "{{ url_for('login_page_render') }}"; }, 3000);
|
| 163 |
+
}
|
| 164 |
+
|
| 165 |
+
// Handle search form submission (reloads the page with query param)
|
| 166 |
+
if(searchForm) {
|
| 167 |
+
searchForm.addEventListener('submit', function(event) {
|
| 168 |
+
// Default form submission with GET will reload the page with the query param
|
| 169 |
+
// The page load logic above will then handle fetching the filtered data.
|
| 170 |
+
// No preventDefault needed unless doing a fetch-based update.
|
| 171 |
+
});
|
| 172 |
+
}
|
| 173 |
+
});
|
| 174 |
+
</script>
|
| 175 |
+
{% endblock %}
|
app/templates/admin/user_details.html
ADDED
|
@@ -0,0 +1,170 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "partials/base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}{{ title }} - Financial Advisor{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="container mt-5">
|
| 7 |
+
<div class="d-flex justify-content-between align-items-center mb-4">
|
| 8 |
+
<h1 id="pageTitle">User Details</h1> {# Placeholder #}
|
| 9 |
+
<a href="{{ url_for('admin_dashboard_shell') }}" class="btn btn-secondary">« Back to Admin Dashboard</a>
|
| 10 |
+
</div>
|
| 11 |
+
<div id="authErrorMessage" class="alert alert-danger" role="alert" style="display: none;">
|
| 12 |
+
Authentication failed or insufficient privileges. Redirecting...
|
| 13 |
+
</div>
|
| 14 |
+
<div id="loadingMessage" class="alert alert-info" role="alert">
|
| 15 |
+
Loading user details...
|
| 16 |
+
</div>
|
| 17 |
+
|
| 18 |
+
<div id="userInfoCard" class="card shadow-sm mb-4" style="display: none;">
|
| 19 |
+
<div class="card-body">
|
| 20 |
+
<h5 class="card-title">User Information</h5>
|
| 21 |
+
<dl class="row" id="userInfoList">
|
| 22 |
+
{# User info will be populated here by JS #}
|
| 23 |
+
</dl>
|
| 24 |
+
</div>
|
| 25 |
+
</div>
|
| 26 |
+
|
| 27 |
+
<div id="userInputsCard" class="card shadow-sm" style="display: none;">
|
| 28 |
+
<div class="card-header bg-info text-white">
|
| 29 |
+
<h4 class="mb-0">Submitted Financial Inputs</h4>
|
| 30 |
+
</div>
|
| 31 |
+
<div class="card-body">
|
| 32 |
+
<div class="accordion" id="userDataAccordion">
|
| 33 |
+
{# Accordion items will be populated here by JS #}
|
| 34 |
+
</div>
|
| 35 |
+
<p id="noInputsMessage" class="text-muted" style="display: none;">This user has not submitted any financial inputs yet.</p>
|
| 36 |
+
</div>
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
{% endblock %}
|
| 40 |
+
|
| 41 |
+
{% block scripts_extra %}
|
| 42 |
+
<script>
|
| 43 |
+
document.addEventListener('DOMContentLoaded', async function () {
|
| 44 |
+
const accessToken = localStorage.getItem('accessToken');
|
| 45 |
+
const loadingMessage = document.getElementById('loadingMessage');
|
| 46 |
+
const authErrorMessage = document.getElementById('authErrorMessage');
|
| 47 |
+
const userInfoCard = document.getElementById('userInfoCard');
|
| 48 |
+
const userInputsCard = document.getElementById('userInputsCard');
|
| 49 |
+
const userInfoList = document.getElementById('userInfoList');
|
| 50 |
+
const userDataAccordion = document.getElementById('userDataAccordion');
|
| 51 |
+
const noInputsMessage = document.getElementById('noInputsMessage');
|
| 52 |
+
const pageTitle = document.getElementById('pageTitle');
|
| 53 |
+
|
| 54 |
+
const targetUserIdStr = "{{ user_id }}"; // Get user_id as string from server context
|
| 55 |
+
const targetUserId = parseInt(targetUserIdStr); // Parse it to integer
|
| 56 |
+
|
| 57 |
+
// 1. Check Authentication and Admin Status
|
| 58 |
+
if (!accessToken || isNaN(targetUserId)) { // Also check if targetUserId is valid
|
| 59 |
+
authErrorMessage.textContent = 'No authentication token found. Redirecting to login...';
|
| 60 |
+
authErrorMessage.style.display = 'block';
|
| 61 |
+
loadingMessage.style.display = 'none';
|
| 62 |
+
setTimeout(() => { window.location.href = "{{ url_for('login_page_render') }}"; }, 2000);
|
| 63 |
+
return;
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
try {
|
| 67 |
+
// Verify admin status first (optional but good practice)
|
| 68 |
+
const userMeResponse = await fetch("{{ url_for('read_users_me') }}", {
|
| 69 |
+
headers: { 'Authorization': `Bearer ${accessToken}` }
|
| 70 |
+
});
|
| 71 |
+
if (!userMeResponse.ok) throw new Error(`Authentication failed: ${userMeResponse.status}`);
|
| 72 |
+
const currentUser = await userMeResponse.json();
|
| 73 |
+
if (!currentUser.is_admin) throw new Error('User is not authorized to view this page.');
|
| 74 |
+
|
| 75 |
+
// 2. Fetch User Details Data
|
| 76 |
+
const detailsUrl = `/admin/api/users/${targetUserId}`;
|
| 77 |
+
const detailsResponse = await fetch(detailsUrl, {
|
| 78 |
+
headers: { 'Authorization': `Bearer ${accessToken}` }
|
| 79 |
+
});
|
| 80 |
+
|
| 81 |
+
if (!detailsResponse.ok) {
|
| 82 |
+
throw new Error(`Failed to fetch user details: ${detailsResponse.status}`);
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
const data = await detailsResponse.json();
|
| 86 |
+
const targetUser = data.target_user;
|
| 87 |
+
const userInputs = data.user_inputs;
|
| 88 |
+
|
| 89 |
+
// 3. Populate Page Content
|
| 90 |
+
loadingMessage.style.display = 'none';
|
| 91 |
+
|
| 92 |
+
// Update Title
|
| 93 |
+
if(pageTitle && targetUser) pageTitle.textContent = `User Details: ${targetUser.email}`;
|
| 94 |
+
document.title = `User Details: ${targetUser.email} - Financial Advisor`; // Update browser tab title
|
| 95 |
+
|
| 96 |
+
// Populate User Info Card
|
| 97 |
+
if (userInfoList && targetUser) {
|
| 98 |
+
userInfoList.innerHTML = `
|
| 99 |
+
<dt class="col-sm-3">User ID:</dt>
|
| 100 |
+
<dd class="col-sm-9">${targetUser.id}</dd>
|
| 101 |
+
<dt class="col-sm-3">Email:</dt>
|
| 102 |
+
<dd class="col-sm-9">${targetUser.email}</dd>
|
| 103 |
+
<dt class="col-sm-3">Admin Status:</dt>
|
| 104 |
+
<dd class="col-sm-9">
|
| 105 |
+
<span class="badge bg-${targetUser.is_admin ? 'success' : 'secondary'}">
|
| 106 |
+
${targetUser.is_admin ? 'Yes' : 'No'}
|
| 107 |
+
</span>
|
| 108 |
+
</dd>
|
| 109 |
+
<dt class="col-sm-3">Registered At:</dt>
|
| 110 |
+
<dd class="col-sm-9">${new Date(targetUser.created_at).toLocaleString()}</dd>
|
| 111 |
+
`;
|
| 112 |
+
userInfoCard.style.display = 'block';
|
| 113 |
+
}
|
| 114 |
+
|
| 115 |
+
// Populate User Inputs Accordion
|
| 116 |
+
if (userDataAccordion) {
|
| 117 |
+
userDataAccordion.innerHTML = ''; // Clear previous
|
| 118 |
+
if (userInputs && userInputs.length > 0) {
|
| 119 |
+
noInputsMessage.style.display = 'none';
|
| 120 |
+
userInputs.forEach((inputItem, index) => {
|
| 121 |
+
const itemId = `item-${index}`;
|
| 122 |
+
const collapseId = `collapse-${index}`;
|
| 123 |
+
const headingId = `heading-${index}`;
|
| 124 |
+
|
| 125 |
+
let inputHtml = '<ul class="list-group">';
|
| 126 |
+
for (const [key, value] of Object.entries(inputItem.input_data)) {
|
| 127 |
+
inputHtml += `<li class="list-group-item"><strong>${key.replace(/_/g, ' ').replace(/\b\w/g, l => l.toUpperCase())}:</strong> ${value}</li>`;
|
| 128 |
+
}
|
| 129 |
+
inputHtml += '</ul>';
|
| 130 |
+
|
| 131 |
+
// Add output data if available and needed
|
| 132 |
+
// let outputHtml = '';
|
| 133 |
+
// if (inputItem.output_data) { ... }
|
| 134 |
+
|
| 135 |
+
const accordionItem = document.createElement('div');
|
| 136 |
+
accordionItem.className = 'accordion-item';
|
| 137 |
+
accordionItem.innerHTML = `
|
| 138 |
+
<h2 class="accordion-header" id="${headingId}">
|
| 139 |
+
<button class="accordion-button collapsed" type="button" data-bs-toggle="collapse" data-bs-target="#${collapseId}" aria-expanded="false" aria-controls="${collapseId}">
|
| 140 |
+
Input #${inputItem.id} - Model: <strong>${inputItem.model_type.charAt(0).toUpperCase() + inputItem.model_type.slice(1)}</strong> - Submitted: ${new Date(inputItem.timestamp).toLocaleString()}
|
| 141 |
+
</button>
|
| 142 |
+
</h2>
|
| 143 |
+
<div id="${collapseId}" class="accordion-collapse collapse" aria-labelledby="${headingId}" data-bs-parent="#userDataAccordion">
|
| 144 |
+
<div class="accordion-body">
|
| 145 |
+
<h5>Input Data:</h5>
|
| 146 |
+
${inputHtml}
|
| 147 |
+
<!-- Add outputHtml here if implemented -->
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
`; // Removed Jinja2 comment from JS string literal
|
| 151 |
+
userDataAccordion.appendChild(accordionItem);
|
| 152 |
+
});
|
| 153 |
+
} else {
|
| 154 |
+
noInputsMessage.style.display = 'block';
|
| 155 |
+
}
|
| 156 |
+
userInputsCard.style.display = 'block';
|
| 157 |
+
}
|
| 158 |
+
|
| 159 |
+
} catch (error) {
|
| 160 |
+
console.error('Error loading user details:', error);
|
| 161 |
+
loadingMessage.style.display = 'none';
|
| 162 |
+
authErrorMessage.textContent = `Error: ${error.message}. Redirecting...`;
|
| 163 |
+
authErrorMessage.style.display = 'block';
|
| 164 |
+
localStorage.removeItem('accessToken'); // Clear token on error
|
| 165 |
+
localStorage.removeItem('tokenType');
|
| 166 |
+
setTimeout(() => { window.location.href = "{{ url_for('login_page_render') }}"; }, 3000);
|
| 167 |
+
}
|
| 168 |
+
});
|
| 169 |
+
</script>
|
| 170 |
+
{% endblock %}
|
app/templates/auth/login.html
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "partials/base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Login - Financial Advisor{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="row justify-content-center">
|
| 7 |
+
<div class="col-md-6 col-lg-4">
|
| 8 |
+
<div class="card shadow-sm">
|
| 9 |
+
<div class="card-body">
|
| 10 |
+
<h2 class="card-title text-center mb-4">Login</h2>
|
| 11 |
+
|
| 12 |
+
{% if request.query_params.get("error") %}
|
| 13 |
+
<div class="alert alert-danger" role="alert">
|
| 14 |
+
{{ request.query_params.get("error") }}
|
| 15 |
+
</div>
|
| 16 |
+
{% endif %}
|
| 17 |
+
{% if request.query_params.get("message") %}
|
| 18 |
+
<div class="alert alert-info" role="alert">
|
| 19 |
+
{{ request.query_params.get("message") }}
|
| 20 |
+
</div>
|
| 21 |
+
{% endif %}
|
| 22 |
+
|
| 23 |
+
<form id="loginForm" method="post">
|
| 24 |
+
<!-- FastAPI's OAuth2PasswordRequestForm expects 'username' and 'password' -->
|
| 25 |
+
<div class="mb-3">
|
| 26 |
+
<label for="email" class="form-label">Email address</label>
|
| 27 |
+
<input type="email" class="form-control" id="email" name="username" required>
|
| 28 |
+
</div>
|
| 29 |
+
<div class="mb-3">
|
| 30 |
+
<label for="password" class="form-label">Password</label>
|
| 31 |
+
<div class="input-group">
|
| 32 |
+
<input type="password" class="form-control" id="password" name="password" required>
|
| 33 |
+
<button class="btn btn-outline-secondary" type="button" id="togglePassword">Show</button>
|
| 34 |
+
</div>
|
| 35 |
+
</div>
|
| 36 |
+
<div class="d-grid">
|
| 37 |
+
<button type="submit" class="btn btn-primary">Login</button>
|
| 38 |
+
</div>
|
| 39 |
+
</form>
|
| 40 |
+
<p class="mt-3 text-center">
|
| 41 |
+
Don't have an account? <a href="{{ url_for('signup_page_render') }}">Sign up here</a>
|
| 42 |
+
</p>
|
| 43 |
+
</div>
|
| 44 |
+
</div>
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
{% endblock %}
|
| 48 |
+
|
| 49 |
+
{% block scripts_extra %}
|
| 50 |
+
<script>
|
| 51 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 52 |
+
const loginForm = document.getElementById('loginForm');
|
| 53 |
+
const togglePasswordButton = document.getElementById('togglePassword');
|
| 54 |
+
const passwordInput = document.getElementById('password');
|
| 55 |
+
|
| 56 |
+
if (togglePasswordButton && passwordInput) {
|
| 57 |
+
togglePasswordButton.addEventListener('click', function () {
|
| 58 |
+
const type = passwordInput.getAttribute('type') === 'password' ? 'text' : 'password';
|
| 59 |
+
passwordInput.setAttribute('type', type);
|
| 60 |
+
this.textContent = type === 'password' ? 'Show' : 'Hide';
|
| 61 |
+
});
|
| 62 |
+
}
|
| 63 |
+
|
| 64 |
+
if (loginForm) {
|
| 65 |
+
loginForm.addEventListener('submit', async function (event) {
|
| 66 |
+
event.preventDefault();
|
| 67 |
+
const formData = new FormData(loginForm);
|
| 68 |
+
|
| 69 |
+
// We will post to /auth/token as expected by OAuth2PasswordRequestForm
|
| 70 |
+
try {
|
| 71 |
+
const response = await fetch("{{ url_for('login_for_access_token') }}", {
|
| 72 |
+
method: 'POST',
|
| 73 |
+
body: formData // FormData will be correctly encoded as application/x-www-form-urlencoded
|
| 74 |
+
});
|
| 75 |
+
|
| 76 |
+
const result = await response.json();
|
| 77 |
+
|
| 78 |
+
if (response.ok) {
|
| 79 |
+
localStorage.setItem('accessToken', result.access_token);
|
| 80 |
+
localStorage.setItem('tokenType', result.token_type);
|
| 81 |
+
|
| 82 |
+
// Fetch user details to check if admin for redirect
|
| 83 |
+
try {
|
| 84 |
+
const userMeResponse = await fetch("{{ url_for('read_users_me') }}", {
|
| 85 |
+
method: 'GET',
|
| 86 |
+
headers: {
|
| 87 |
+
'Authorization': `Bearer ${result.access_token}`
|
| 88 |
+
}
|
| 89 |
+
});
|
| 90 |
+
if (userMeResponse.ok) {
|
| 91 |
+
const userData = await userMeResponse.json();
|
| 92 |
+
if (userData.is_admin) {
|
| 93 |
+
window.location.href = "{{ url_for('admin_dashboard_shell') }}"; // Corrected route name
|
| 94 |
+
} else {
|
| 95 |
+
window.location.href = "{{ url_for('user_homepage') }}";
|
| 96 |
+
}
|
| 97 |
+
} else {
|
| 98 |
+
// Fallback to user homepage if /users/me fails, or show error
|
| 99 |
+
console.error("Failed to fetch user details for redirect.");
|
| 100 |
+
window.location.href = "{{ url_for('user_homepage') }}";
|
| 101 |
+
}
|
| 102 |
+
} catch (e) {
|
| 103 |
+
console.error("Error fetching user details:", e);
|
| 104 |
+
window.location.href = "{{ url_for('user_homepage') }}"; // Fallback
|
| 105 |
+
}
|
| 106 |
+
} else {
|
| 107 |
+
// Display error message
|
| 108 |
+
const errorDiv = document.createElement('div');
|
| 109 |
+
errorDiv.className = 'alert alert-danger mt-3';
|
| 110 |
+
errorDiv.textContent = result.detail || 'Login failed. Please check your credentials.';
|
| 111 |
+
// Clear previous errors
|
| 112 |
+
const existingError = loginForm.querySelector('.alert-danger');
|
| 113 |
+
if (existingError) {
|
| 114 |
+
existingError.remove();
|
| 115 |
+
}
|
| 116 |
+
loginForm.prepend(errorDiv);
|
| 117 |
+
}
|
| 118 |
+
} catch (error) {
|
| 119 |
+
console.error('Login error:', error);
|
| 120 |
+
const errorDiv = document.createElement('div');
|
| 121 |
+
errorDiv.className = 'alert alert-danger mt-3';
|
| 122 |
+
errorDiv.textContent = 'An unexpected error occurred. Please try again.';
|
| 123 |
+
const existingError = loginForm.querySelector('.alert-danger');
|
| 124 |
+
if (existingError) {
|
| 125 |
+
existingError.remove();
|
| 126 |
+
}
|
| 127 |
+
loginForm.prepend(errorDiv);
|
| 128 |
+
}
|
| 129 |
+
});
|
| 130 |
+
}
|
| 131 |
+
});
|
| 132 |
+
</script>
|
| 133 |
+
{% endblock %}
|
app/templates/auth/signup.html
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "partials/base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Sign Up - Financial Advisor{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="row justify-content-center">
|
| 7 |
+
<div class="col-md-6 col-lg-4">
|
| 8 |
+
<div class="card shadow-sm">
|
| 9 |
+
<div class="card-body">
|
| 10 |
+
<h2 class="card-title text-center mb-4">Create Account</h2>
|
| 11 |
+
<form id="signupForm" method="post">
|
| 12 |
+
<div class="mb-3">
|
| 13 |
+
<label for="email" class="form-label">Email address</label>
|
| 14 |
+
<input type="email" class="form-control" id="email" name="email" required>
|
| 15 |
+
</div>
|
| 16 |
+
<div class="mb-3">
|
| 17 |
+
<label for="password" class="form-label">Password</label>
|
| 18 |
+
<div class="input-group">
|
| 19 |
+
<input type="password" class="form-control" id="password" name="password" required>
|
| 20 |
+
<button class="btn btn-outline-secondary" type="button" id="togglePassword">Show</button>
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
<div class="mb-3">
|
| 24 |
+
<label for="confirm_password" class="form-label">Confirm Password</label>
|
| 25 |
+
<div class="input-group">
|
| 26 |
+
<input type="password" class="form-control" id="confirm_password" name="confirm_password" required>
|
| 27 |
+
<button class="btn btn-outline-secondary" type="button" id="toggleConfirmPassword">Show</button>
|
| 28 |
+
</div>
|
| 29 |
+
</div>
|
| 30 |
+
<div class="d-grid">
|
| 31 |
+
<button type="submit" class="btn btn-primary">Sign Up</button>
|
| 32 |
+
</div>
|
| 33 |
+
</form>
|
| 34 |
+
<p class="mt-3 text-center">
|
| 35 |
+
Already have an account? <a href="{{ url_for('login_page_render') }}">Login here</a>
|
| 36 |
+
</p>
|
| 37 |
+
</div>
|
| 38 |
+
</div>
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
{% endblock %}
|
| 42 |
+
|
| 43 |
+
{% block scripts_extra %}
|
| 44 |
+
<script>
|
| 45 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 46 |
+
const signupForm = document.getElementById('signupForm');
|
| 47 |
+
|
| 48 |
+
const togglePasswordButton = document.getElementById('togglePassword');
|
| 49 |
+
const passwordInput = document.getElementById('password');
|
| 50 |
+
const toggleConfirmPasswordButton = document.getElementById('toggleConfirmPassword');
|
| 51 |
+
const confirmPasswordInput = document.getElementById('confirm_password');
|
| 52 |
+
|
| 53 |
+
function setupPasswordToggle(button, input) {
|
| 54 |
+
if (button && input) {
|
| 55 |
+
button.addEventListener('click', function () {
|
| 56 |
+
const type = input.getAttribute('type') === 'password' ? 'text' : 'password';
|
| 57 |
+
input.setAttribute('type', type);
|
| 58 |
+
this.textContent = type === 'password' ? 'Show' : 'Hide';
|
| 59 |
+
});
|
| 60 |
+
}
|
| 61 |
+
}
|
| 62 |
+
setupPasswordToggle(togglePasswordButton, passwordInput);
|
| 63 |
+
setupPasswordToggle(toggleConfirmPasswordButton, confirmPasswordInput);
|
| 64 |
+
|
| 65 |
+
if (signupForm) {
|
| 66 |
+
signupForm.addEventListener('submit', async function (event) {
|
| 67 |
+
event.preventDefault();
|
| 68 |
+
|
| 69 |
+
const email = document.getElementById('email').value;
|
| 70 |
+
const password = passwordInput.value;
|
| 71 |
+
const confirm_password = confirmPasswordInput.value;
|
| 72 |
+
|
| 73 |
+
if (password !== confirm_password) {
|
| 74 |
+
displayError('Passwords do not match.');
|
| 75 |
+
return;
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
const formData = {
|
| 79 |
+
email: email,
|
| 80 |
+
password: password,
|
| 81 |
+
confirm_password: confirm_password
|
| 82 |
+
};
|
| 83 |
+
|
| 84 |
+
try {
|
| 85 |
+
const response = await fetch("{{ url_for('signup_user') }}", { // Endpoint from auth.py
|
| 86 |
+
method: 'POST',
|
| 87 |
+
headers: {
|
| 88 |
+
'Content-Type': 'application/json',
|
| 89 |
+
},
|
| 90 |
+
body: JSON.stringify(formData)
|
| 91 |
+
});
|
| 92 |
+
|
| 93 |
+
const result = await response.json();
|
| 94 |
+
|
| 95 |
+
if (response.ok) {
|
| 96 |
+
// Redirect to login page with a success message
|
| 97 |
+
window.location.href = "{{ url_for('login_page_render') }}?message=Signup successful! Please login.";
|
| 98 |
+
} else {
|
| 99 |
+
displayError(result.detail || 'Signup failed. Please try again.');
|
| 100 |
+
}
|
| 101 |
+
} catch (error) {
|
| 102 |
+
console.error('Signup error:', error);
|
| 103 |
+
displayError('An unexpected error occurred. Please try again.');
|
| 104 |
+
}
|
| 105 |
+
});
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
function displayError(message) {
|
| 109 |
+
const errorDiv = document.createElement('div');
|
| 110 |
+
errorDiv.className = 'alert alert-danger mt-3';
|
| 111 |
+
errorDiv.textContent = message;
|
| 112 |
+
// Clear previous errors
|
| 113 |
+
const existingError = signupForm.querySelector('.alert-danger');
|
| 114 |
+
if (existingError) {
|
| 115 |
+
existingError.remove();
|
| 116 |
+
}
|
| 117 |
+
signupForm.prepend(errorDiv);
|
| 118 |
+
}
|
| 119 |
+
});
|
| 120 |
+
</script>
|
| 121 |
+
{% endblock %}
|
app/templates/partials/base.html
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<!DOCTYPE html>
|
| 2 |
+
<html lang="en">
|
| 3 |
+
<head>
|
| 4 |
+
<meta charset="UTF-8">
|
| 5 |
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
+
<title>{{ title }} - Financial Advisor</title>
|
| 7 |
+
<!-- Bootstrap CSS CDN -->
|
| 8 |
+
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/css/bootstrap.min.css" rel="stylesheet">
|
| 9 |
+
<!-- Custom CSS -->
|
| 10 |
+
<link rel="stylesheet" href="{{ url_for('static', path='/css/style.css') }}">
|
| 11 |
+
{% block head_extra %}{% endblock %}
|
| 12 |
+
</head>
|
| 13 |
+
<body>
|
| 14 |
+
{% include 'partials/navbar.html' %}
|
| 15 |
+
|
| 16 |
+
<div class="container mt-4">
|
| 17 |
+
{# Removed get_flashed_messages block as it's not standard in FastAPI/Jinja2 alone #}
|
| 18 |
+
{# Specific pages can handle messages via query params or JS #}
|
| 19 |
+
{% block content %}{% endblock %}
|
| 20 |
+
</div>
|
| 21 |
+
|
| 22 |
+
{% include 'partials/footer.html' %}
|
| 23 |
+
|
| 24 |
+
<!-- Bootstrap JS Bundle CDN (includes Popper) -->
|
| 25 |
+
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0-alpha1/dist/js/bootstrap.bundle.min.js"></script>
|
| 26 |
+
<!-- Custom JS -->
|
| 27 |
+
<script src="{{ url_for('static', path='/js/script.js') }}"></script>
|
| 28 |
+
{% block scripts_extra %}{% endblock %}
|
| 29 |
+
</body>
|
| 30 |
+
</html>
|
app/templates/partials/footer.html
ADDED
|
@@ -0,0 +1,6 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<footer class="bg-light text-center text-lg-start mt-auto py-3">
|
| 2 |
+
<div class="text-center p-3" style="background-color: rgba(0, 0, 0, 0.05);">
|
| 3 |
+
© 2024 Financial Advisor Chatbot. All Rights Reserved.
|
| 4 |
+
<p class="small mt-1">Disclaimer: This tool is for educational purposes only and does not constitute financial advice. Consult with a qualified professional before making investment decisions.</p>
|
| 5 |
+
</div>
|
| 6 |
+
</footer>
|
app/templates/partials/navbar.html
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<nav class="navbar navbar-expand-lg navbar-dark bg-primary">
|
| 2 |
+
<div class="container-fluid">
|
| 3 |
+
<a class="navbar-brand" href="{{ url_for('read_root') }}">FinAdvisor</a>
|
| 4 |
+
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarNav" aria-controls="navbarNav" aria-expanded="false" aria-label="Toggle navigation">
|
| 5 |
+
<span class="navbar-toggler-icon"></span>
|
| 6 |
+
</button>
|
| 7 |
+
<div class="collapse navbar-collapse" id="navbarNav">
|
| 8 |
+
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
|
| 9 |
+
{% if request.state.user %} {# Check if user is available in request state (set by middleware or dependency) #}
|
| 10 |
+
<li class="nav-item">
|
| 11 |
+
<a class="nav-link {% if request.url.path == url_for('user_homepage') %}active{% endif %}" href="{{ url_for('user_homepage') }}">Home</a>
|
| 12 |
+
</li>
|
| 13 |
+
<li class="nav-item">
|
| 14 |
+
<a class="nav-link {% if request.url.path == url_for('recommendations_page') %}active{% endif %}" href="{{ url_for('recommendations_page') }}">Market Trends</a>
|
| 15 |
+
</li>
|
| 16 |
+
{% if request.state.user.is_admin %}
|
| 17 |
+
<li class="nav-item">
|
| 18 |
+
<a class="nav-link {% if request.url.path.startswith(url_for('admin_dashboard')) %}active{% endif %}" href="{{ url_for('admin_dashboard') }}">Admin Dashboard</a>
|
| 19 |
+
</li>
|
| 20 |
+
{% endif %}
|
| 21 |
+
{% endif %}
|
| 22 |
+
</ul>
|
| 23 |
+
<ul class="navbar-nav ms-auto">
|
| 24 |
+
{# Logged-in user items - Initially hidden, shown by JS if token exists #}
|
| 25 |
+
<li class="nav-item" id="navWelcomeItem" style="display: none;">
|
| 26 |
+
<span class="navbar-text me-2" id="navUserWelcome">
|
| 27 |
+
Welcome!
|
| 28 |
+
</span>
|
| 29 |
+
</li>
|
| 30 |
+
<li class="nav-item" id="navLogoutItem" style="display: none;">
|
| 31 |
+
<a class="nav-link" href="#" id="logoutButton">Logout</a>
|
| 32 |
+
</li>
|
| 33 |
+
|
| 34 |
+
{# Logged-out user items - Initially shown, hidden by JS if token exists #}
|
| 35 |
+
<li class="nav-item" id="navLoginItem">
|
| 36 |
+
<a class="nav-link {% if request.url.path == url_for('login_page_render') or request.url.path == url_for('read_root') %}active{% endif %}" href="{{ url_for('login_page_render') }}">Login</a>
|
| 37 |
+
</li>
|
| 38 |
+
<li class="nav-item" id="navSignupItem">
|
| 39 |
+
<a class="nav-link {% if request.url.path == url_for('signup_page_render') %}active{% endif %}" href="{{ url_for('signup_page_render') }}">Sign Up</a>
|
| 40 |
+
</li>
|
| 41 |
+
</ul>
|
| 42 |
+
</div>
|
| 43 |
+
</div>
|
| 44 |
+
</nav>
|
app/templates/user/chatbot.html
ADDED
|
@@ -0,0 +1,362 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "partials/base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}{{ title }} - Chatbot{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block head_extra %}
|
| 6 |
+
{# Chat styles moved to static/css/style.css #}
|
| 7 |
+
{% endblock %}
|
| 8 |
+
|
| 9 |
+
{% block content %}
|
| 10 |
+
<div class="container mt-5">
|
| 11 |
+
<div class="row justify-content-center">
|
| 12 |
+
<div class="col-md-8 col-lg-7">
|
| 13 |
+
<div class="card shadow-sm">
|
| 14 |
+
<div class="card-header bg-primary text-white d-flex justify-content-between align-items-center">
|
| 15 |
+
<h4 class="mb-0">{{ title }}</h4>
|
| 16 |
+
<button id="cancelButton" class="btn btn-sm btn-danger" style="display: none;">Cancel</button>
|
| 17 |
+
</div>
|
| 18 |
+
<div class="card-body">
|
| 19 |
+
<div id="chatbox">
|
| 20 |
+
<!-- Chat messages appear here -->
|
| 21 |
+
</div>
|
| 22 |
+
|
| 23 |
+
<div id="startArea">
|
| 24 |
+
<p>Click "Start" to begin the financial assessment.</p>
|
| 25 |
+
<button id="startButton" class="btn btn-success">Start</button>
|
| 26 |
+
</div>
|
| 27 |
+
|
| 28 |
+
<div id="userInputArea" class="mt-3" style="display: none;">
|
| 29 |
+
{# Input elements will be dynamically added here by JS #}
|
| 30 |
+
<input type="text" id="textInput" class="form-control" placeholder="Type your answer...">
|
| 31 |
+
<div id="optionsContainer"></div> {# For buttons #}
|
| 32 |
+
<button id="sendButton" class="btn btn-primary">Send</button>
|
| 33 |
+
</div>
|
| 34 |
+
|
| 35 |
+
<div id="resultArea" class="mt-4" style="display:none;">
|
| 36 |
+
<h4>Recommendation:</h4>
|
| 37 |
+
<div id="recommendationDetails" class="p-3 bg-light rounded"></div>
|
| 38 |
+
<div id="justificationArea" class="mt-3" style="display:none;">
|
| 39 |
+
<h5>Justification:</h5>
|
| 40 |
+
<ul id="justificationList" class="list-group"></ul>
|
| 41 |
+
</div>
|
| 42 |
+
<div id="tipsArea" class="mt-3" style="display:none;">
|
| 43 |
+
<h5>Financial Tips:</h5>
|
| 44 |
+
<ul id="tipsList" class="list-group"></ul>
|
| 45 |
+
</div>
|
| 46 |
+
<div id="errorArea" class="alert alert-danger mt-3" style="display:none;"></div>
|
| 47 |
+
</div>
|
| 48 |
+
</div>
|
| 49 |
+
</div>
|
| 50 |
+
<div class="mt-3">
|
| 51 |
+
<a href="{{ url_for('user_homepage') }}" class="btn btn-secondary">« Back to Model Selection</a>
|
| 52 |
+
</div>
|
| 53 |
+
</div>
|
| 54 |
+
</div>
|
| 55 |
+
</div>
|
| 56 |
+
|
| 57 |
+
{# Pass form fields data to JavaScript #}
|
| 58 |
+
<script id="formFieldsData" type="application/json">
|
| 59 |
+
{{ form_fields | tojson | safe }}
|
| 60 |
+
</script>
|
| 61 |
+
{% endblock %}
|
| 62 |
+
|
| 63 |
+
{% block scripts_extra %}
|
| 64 |
+
<script>
|
| 65 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 66 |
+
// --- DOM Elements ---
|
| 67 |
+
const chatbox = document.getElementById('chatbox');
|
| 68 |
+
const startArea = document.getElementById('startArea');
|
| 69 |
+
const startButton = document.getElementById('startButton');
|
| 70 |
+
const userInputArea = document.getElementById('userInputArea');
|
| 71 |
+
const textInput = document.getElementById('textInput');
|
| 72 |
+
const optionsContainer = document.getElementById('optionsContainer');
|
| 73 |
+
const sendButton = document.getElementById('sendButton');
|
| 74 |
+
const cancelButton = document.getElementById('cancelButton');
|
| 75 |
+
const resultArea = document.getElementById('resultArea');
|
| 76 |
+
const recommendationDetails = document.getElementById('recommendationDetails');
|
| 77 |
+
const justificationArea = document.getElementById('justificationArea');
|
| 78 |
+
const justificationList = document.getElementById('justificationList');
|
| 79 |
+
const tipsArea = document.getElementById('tipsArea');
|
| 80 |
+
const tipsList = document.getElementById('tipsList');
|
| 81 |
+
const errorArea = document.getElementById('errorArea');
|
| 82 |
+
|
| 83 |
+
// --- State Variables ---
|
| 84 |
+
let formFields = [];
|
| 85 |
+
let currentQuestionIndex = -1;
|
| 86 |
+
let collectedAnswers = {};
|
| 87 |
+
let isWaitingForUserInput = false;
|
| 88 |
+
const modelType = "{{ model_type }}"; // Get model type from Jinja2 context
|
| 89 |
+
|
| 90 |
+
// --- Initialization ---
|
| 91 |
+
function initializeChat() {
|
| 92 |
+
try {
|
| 93 |
+
const fieldsDataElement = document.getElementById('formFieldsData');
|
| 94 |
+
if (fieldsDataElement) {
|
| 95 |
+
formFields = JSON.parse(fieldsDataElement.textContent);
|
| 96 |
+
} else {
|
| 97 |
+
console.error("Form fields data not found.");
|
| 98 |
+
addBotMessage("Sorry, I couldn't load the questions. Please try again later.");
|
| 99 |
+
return;
|
| 100 |
+
}
|
| 101 |
+
} catch (e) {
|
| 102 |
+
console.error("Error parsing form fields data:", e);
|
| 103 |
+
addBotMessage("Sorry, there was an error setting up the chat. Please try again later.");
|
| 104 |
+
return;
|
| 105 |
+
}
|
| 106 |
+
|
| 107 |
+
addBotMessage(`Hello! I'm the ${modelType.charAt(0).toUpperCase() + modelType.slice(1)} Model Advisor. Let's get started.`);
|
| 108 |
+
startArea.style.display = 'block';
|
| 109 |
+
userInputArea.style.display = 'none';
|
| 110 |
+
cancelButton.style.display = 'none';
|
| 111 |
+
resultArea.style.display = 'none';
|
| 112 |
+
currentQuestionIndex = -1;
|
| 113 |
+
collectedAnswers = {};
|
| 114 |
+
isWaitingForUserInput = false;
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
// --- Chat Helper Functions ---
|
| 118 |
+
function addMessage(sender, text) {
|
| 119 |
+
const messageDiv = document.createElement('div');
|
| 120 |
+
messageDiv.classList.add('chat-message', sender === 'bot' ? 'bot-message' : 'user-message');
|
| 121 |
+
|
| 122 |
+
const bubble = document.createElement('div');
|
| 123 |
+
bubble.classList.add('message-bubble');
|
| 124 |
+
bubble.textContent = text;
|
| 125 |
+
|
| 126 |
+
messageDiv.appendChild(bubble);
|
| 127 |
+
chatbox.appendChild(messageDiv);
|
| 128 |
+
chatbox.scrollTop = chatbox.scrollHeight; // Auto-scroll
|
| 129 |
+
}
|
| 130 |
+
|
| 131 |
+
function addBotMessage(text) {
|
| 132 |
+
addMessage('bot', text);
|
| 133 |
+
}
|
| 134 |
+
|
| 135 |
+
function addUserMessage(text) {
|
| 136 |
+
addMessage('user', text);
|
| 137 |
+
}
|
| 138 |
+
|
| 139 |
+
// --- Chat Flow Functions ---
|
| 140 |
+
function startChat() {
|
| 141 |
+
startArea.style.display = 'none';
|
| 142 |
+
cancelButton.style.display = 'block';
|
| 143 |
+
currentQuestionIndex = 0;
|
| 144 |
+
collectedAnswers = {}; // Reset answers
|
| 145 |
+
askNextQuestion();
|
| 146 |
+
}
|
| 147 |
+
|
| 148 |
+
function askNextQuestion() {
|
| 149 |
+
if (currentQuestionIndex >= formFields.length) {
|
| 150 |
+
submitAnswers();
|
| 151 |
+
return;
|
| 152 |
+
}
|
| 153 |
+
|
| 154 |
+
const field = formFields[currentQuestionIndex];
|
| 155 |
+
addBotMessage(field.label + (field.required ? "" : " (Optional)")); // Don't add asterisk, just ask
|
| 156 |
+
|
| 157 |
+
setupUserInput(field);
|
| 158 |
+
isWaitingForUserInput = true;
|
| 159 |
+
}
|
| 160 |
+
|
| 161 |
+
function setupUserInput(field) {
|
| 162 |
+
userInputArea.style.display = 'flex';
|
| 163 |
+
textInput.style.display = 'none';
|
| 164 |
+
optionsContainer.innerHTML = ''; // Clear previous options
|
| 165 |
+
optionsContainer.style.display = 'none';
|
| 166 |
+
sendButton.style.display = 'none'; // Hide send button by default
|
| 167 |
+
|
| 168 |
+
textInput.type = field.type === 'number' ? 'number' : 'text';
|
| 169 |
+
textInput.value = ''; // Clear previous input
|
| 170 |
+
textInput.min = field.min !== undefined ? field.min : '';
|
| 171 |
+
textInput.placeholder = `Enter ${field.label}...`;
|
| 172 |
+
|
| 173 |
+
if (field.type === 'select') {
|
| 174 |
+
optionsContainer.style.display = 'block';
|
| 175 |
+
field.options.forEach(option => {
|
| 176 |
+
const button = document.createElement('button');
|
| 177 |
+
button.classList.add('btn', 'btn-outline-secondary', 'option-button');
|
| 178 |
+
button.textContent = option;
|
| 179 |
+
button.type = 'button'; // Prevent form submission
|
| 180 |
+
button.onclick = () => handleUserInput(option);
|
| 181 |
+
optionsContainer.appendChild(button);
|
| 182 |
+
});
|
| 183 |
+
} else {
|
| 184 |
+
textInput.style.display = 'block';
|
| 185 |
+
sendButton.style.display = 'block';
|
| 186 |
+
textInput.focus();
|
| 187 |
+
}
|
| 188 |
+
}
|
| 189 |
+
|
| 190 |
+
function handleUserInput(value) {
|
| 191 |
+
if (!isWaitingForUserInput) return;
|
| 192 |
+
|
| 193 |
+
const field = formFields[currentQuestionIndex];
|
| 194 |
+
let processedValue = value;
|
| 195 |
+
|
| 196 |
+
// --- Client-side Validation ---
|
| 197 |
+
if (field.required && (value === null || value === undefined || value === '')) {
|
| 198 |
+
addBotMessage("This field is required. Please provide a value.");
|
| 199 |
+
return; // Re-ask same question implicitly by not advancing index
|
| 200 |
+
}
|
| 201 |
+
|
| 202 |
+
if (field.type === 'number') {
|
| 203 |
+
processedValue = parseFloat(value);
|
| 204 |
+
if (isNaN(processedValue)) {
|
| 205 |
+
addBotMessage("Please enter a valid number.");
|
| 206 |
+
return;
|
| 207 |
+
}
|
| 208 |
+
if (field.min !== undefined && processedValue < field.min) {
|
| 209 |
+
addBotMessage(`Value cannot be less than ${field.min}.`);
|
| 210 |
+
return;
|
| 211 |
+
}
|
| 212 |
+
// Specific check for Salary/Expenses
|
| 213 |
+
if (field.name === 'Expenses' && (modelType === 'base' || modelType === 'enhanced')) {
|
| 214 |
+
const salary = collectedAnswers['Salary']; // Get previously collected salary
|
| 215 |
+
if (salary !== undefined && processedValue > salary) {
|
| 216 |
+
addBotMessage("Warning: Your monthly expenses exceed your monthly salary. Please review your budget or update the values.");
|
| 217 |
+
// Optional: Force re-entry or ask for confirmation
|
| 218 |
+
// For now, just warn and proceed. A better UX might involve more steps.
|
| 219 |
+
}
|
| 220 |
+
}
|
| 221 |
+
}
|
| 222 |
+
|
| 223 |
+
// --- Store Answer and Proceed ---
|
| 224 |
+
isWaitingForUserInput = false; // Stop accepting input until next question
|
| 225 |
+
userInputArea.style.display = 'none'; // Hide input area while processing/asking next
|
| 226 |
+
|
| 227 |
+
addUserMessage(value); // Show user's valid input in chat
|
| 228 |
+
collectedAnswers[field.name] = processedValue;
|
| 229 |
+
|
| 230 |
+
currentQuestionIndex++;
|
| 231 |
+
|
| 232 |
+
// Add a slight delay before asking next question
|
| 233 |
+
setTimeout(askNextQuestion, 500);
|
| 234 |
+
}
|
| 235 |
+
|
| 236 |
+
async function submitAnswers() {
|
| 237 |
+
addBotMessage("Thanks! Processing your information...");
|
| 238 |
+
cancelButton.style.display = 'none'; // Can't cancel now
|
| 239 |
+
userInputArea.style.display = 'none';
|
| 240 |
+
|
| 241 |
+
const requestPayload = {
|
| 242 |
+
model_type: modelType,
|
| 243 |
+
inputs: collectedAnswers
|
| 244 |
+
};
|
| 245 |
+
|
| 246 |
+
const accessToken = localStorage.getItem('accessToken');
|
| 247 |
+
if (!accessToken) {
|
| 248 |
+
addBotMessage('Authentication error. Please login again.');
|
| 249 |
+
// Redirect?
|
| 250 |
+
return;
|
| 251 |
+
}
|
| 252 |
+
|
| 253 |
+
try {
|
| 254 |
+
const response = await fetch("{{ url_for('api_chatbot_interact') }}", {
|
| 255 |
+
method: 'POST',
|
| 256 |
+
headers: {
|
| 257 |
+
'Content-Type': 'application/json',
|
| 258 |
+
'Authorization': `Bearer ${accessToken}`
|
| 259 |
+
},
|
| 260 |
+
body: JSON.stringify(requestPayload)
|
| 261 |
+
});
|
| 262 |
+
|
| 263 |
+
const result = await response.json();
|
| 264 |
+
displayResults(result);
|
| 265 |
+
|
| 266 |
+
} catch (error) {
|
| 267 |
+
console.error('Chatbot submission error:', error);
|
| 268 |
+
displayError('An unexpected error occurred while getting your advice.');
|
| 269 |
+
}
|
| 270 |
+
}
|
| 271 |
+
|
| 272 |
+
function displayResults(result) {
|
| 273 |
+
resultArea.style.display = 'block';
|
| 274 |
+
errorArea.style.display = 'none';
|
| 275 |
+
justificationArea.style.display = 'none';
|
| 276 |
+
tipsArea.style.display = 'none';
|
| 277 |
+
recommendationDetails.innerHTML = '';
|
| 278 |
+
justificationList.innerHTML = '';
|
| 279 |
+
tipsList.innerHTML = '';
|
| 280 |
+
|
| 281 |
+
if (result.recommendation && result.recommendation.error) {
|
| 282 |
+
displayError(`Error: ${result.recommendation.error}`);
|
| 283 |
+
} else if (result.detail) { // Handle FastAPI validation errors etc.
|
| 284 |
+
displayError(`Error: ${result.detail}`);
|
| 285 |
+
} else {
|
| 286 |
+
addBotMessage("Here is your personalized recommendation:");
|
| 287 |
+
|
| 288 |
+
// Display Recommendation
|
| 289 |
+
if (result.recommendation.message) { // For cases like no savings
|
| 290 |
+
recommendationDetails.innerHTML = `<p class="text-info">${result.recommendation.message}</p>`;
|
| 291 |
+
} else {
|
| 292 |
+
const ul = document.createElement('ul');
|
| 293 |
+
ul.className = 'list-group';
|
| 294 |
+
for (const [asset, percentage] of Object.entries(result.recommendation)) {
|
| 295 |
+
const li = document.createElement('li');
|
| 296 |
+
li.className = 'list-group-item d-flex justify-content-between align-items-center';
|
| 297 |
+
li.textContent = asset;
|
| 298 |
+
const span = document.createElement('span');
|
| 299 |
+
span.className = 'badge bg-primary rounded-pill';
|
| 300 |
+
span.textContent = `${percentage}%`;
|
| 301 |
+
li.appendChild(span);
|
| 302 |
+
ul.appendChild(li);
|
| 303 |
+
}
|
| 304 |
+
recommendationDetails.appendChild(ul);
|
| 305 |
+
}
|
| 306 |
+
|
| 307 |
+
// Display Justification (Rule-based)
|
| 308 |
+
if (result.justification && result.justification.length > 0) {
|
| 309 |
+
result.justification.forEach(item => {
|
| 310 |
+
const li = document.createElement('li');
|
| 311 |
+
li.className = 'list-group-item';
|
| 312 |
+
li.textContent = item;
|
| 313 |
+
justificationList.appendChild(li);
|
| 314 |
+
});
|
| 315 |
+
justificationArea.style.display = 'block';
|
| 316 |
+
}
|
| 317 |
+
|
| 318 |
+
// Display Tips (Rule-based)
|
| 319 |
+
if (result.tips && result.tips.length > 0) {
|
| 320 |
+
result.tips.forEach(item => {
|
| 321 |
+
const li = document.createElement('li');
|
| 322 |
+
li.className = 'list-group-item';
|
| 323 |
+
li.textContent = item;
|
| 324 |
+
tipsList.appendChild(li);
|
| 325 |
+
});
|
| 326 |
+
tipsArea.style.display = 'block';
|
| 327 |
+
}
|
| 328 |
+
}
|
| 329 |
+
chatbox.scrollTop = chatbox.scrollHeight; // Scroll to show results
|
| 330 |
+
}
|
| 331 |
+
|
| 332 |
+
function displayError(message) {
|
| 333 |
+
errorArea.textContent = message;
|
| 334 |
+
errorArea.style.display = 'block';
|
| 335 |
+
resultArea.style.display = 'block'; // Show result area to display the error
|
| 336 |
+
recommendationDetails.innerHTML = ''; // Clear any partial results
|
| 337 |
+
justificationArea.style.display = 'none';
|
| 338 |
+
tipsArea.style.display = 'none';
|
| 339 |
+
chatbox.scrollTop = chatbox.scrollHeight;
|
| 340 |
+
}
|
| 341 |
+
|
| 342 |
+
function cancelChat() {
|
| 343 |
+
addBotMessage("Chat cancelled. No data was saved.");
|
| 344 |
+
initializeChat(); // Reset to initial state
|
| 345 |
+
}
|
| 346 |
+
|
| 347 |
+
// --- Event Listeners ---
|
| 348 |
+
startButton.addEventListener('click', startChat);
|
| 349 |
+
cancelButton.addEventListener('click', cancelChat);
|
| 350 |
+
sendButton.addEventListener('click', () => handleUserInput(textInput.value));
|
| 351 |
+
textInput.addEventListener('keypress', function (e) {
|
| 352 |
+
if (e.key === 'Enter') {
|
| 353 |
+
e.preventDefault(); // Prevent form submission if inside a form tag
|
| 354 |
+
handleUserInput(textInput.value);
|
| 355 |
+
}
|
| 356 |
+
});
|
| 357 |
+
|
| 358 |
+
// --- Initial Setup ---
|
| 359 |
+
initializeChat();
|
| 360 |
+
});
|
| 361 |
+
</script>
|
| 362 |
+
{% endblock %}
|
app/templates/user/homepage.html
ADDED
|
@@ -0,0 +1,137 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "partials/base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}User Dashboard - Financial Advisor{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="container mt-5">
|
| 7 |
+
<div class="row mb-4">
|
| 8 |
+
<div class="col">
|
| 9 |
+
<h1>Welcome, <span id="userEmailPlaceholder"></span>!</h1>
|
| 10 |
+
<p>Choose a financial advisory model to get started.</p>
|
| 11 |
+
</div>
|
| 12 |
+
</div>
|
| 13 |
+
|
| 14 |
+
<div id="modelSelectionCards" class="row row-cols-1 row-cols-md-3 g-4" style="display: none;">
|
| 15 |
+
<div class="col">
|
| 16 |
+
<div class="card h-100 shadow-sm">
|
| 17 |
+
<div class="card-body d-flex flex-column">
|
| 18 |
+
<h5 class="card-title">Base Model Advisor</h5>
|
| 19 |
+
<p class="card-text">Get a fundamental portfolio recommendation based on your salary, expenses, savings, lifecycle stage, risk appetite, and investment horizon.</p>
|
| 20 |
+
<a href="{{ url_for('chatbot_page', model_type='base') }}" class="btn btn-primary mt-auto">Start with Base Model</a>
|
| 21 |
+
</div>
|
| 22 |
+
</div>
|
| 23 |
+
</div>
|
| 24 |
+
<div class="col">
|
| 25 |
+
<div class="card h-100 shadow-sm">
|
| 26 |
+
<div class="card-body d-flex flex-column">
|
| 27 |
+
<h5 class="card-title">Enhanced Model Advisor</h5>
|
| 28 |
+
<p class="card-text">Receive a more detailed portfolio recommendation, incorporating your profession and city in addition to the base model inputs.</p>
|
| 29 |
+
<a href="{{ url_for('chatbot_page', model_type='enhanced') }}" class="btn btn-success mt-auto">Start with Enhanced Model</a>
|
| 30 |
+
</div>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
<div class="col">
|
| 34 |
+
<div class="card h-100 shadow-sm">
|
| 35 |
+
<div class="card-body d-flex flex-column">
|
| 36 |
+
<h5 class="card-title">Rule-Based Advisor</h5>
|
| 37 |
+
<p class="card-text">Get advice based on a predefined set of financial rules and best practices, considering your profile and financial details.</p>
|
| 38 |
+
<a href="{{ url_for('chatbot_page', model_type='rule_based') }}" class="btn btn-info mt-auto">Start with Rule-Based Model</a>
|
| 39 |
+
</div>
|
| 40 |
+
</div>
|
| 41 |
+
</div>
|
| 42 |
+
</div>
|
| 43 |
+
|
| 44 |
+
<div id="quickLinksSection" class="row mt-5" style="display: none;">
|
| 45 |
+
<div class="col">
|
| 46 |
+
<h4>Quick Links</h4>
|
| 47 |
+
<ul class="list-group">
|
| 48 |
+
<li class="list-group-item"><a href="{{ url_for('recommendations_page') }}">View Market Trends & Recommendations</a></li>
|
| 49 |
+
<!-- Add more links here if needed, e.g., to view past interactions -->
|
| 50 |
+
</ul>
|
| 51 |
+
</div>
|
| 52 |
+
</div>
|
| 53 |
+
<div id="loadingMessage" class="alert alert-info" role="alert" style="display: none;">
|
| 54 |
+
Loading user data...
|
| 55 |
+
</div>
|
| 56 |
+
<div id="authErrorMessage" class="alert alert-danger" role="alert" style="display: none;">
|
| 57 |
+
Authentication failed. Redirecting to login...
|
| 58 |
+
</div>
|
| 59 |
+
</div>
|
| 60 |
+
{% endblock %}
|
| 61 |
+
|
| 62 |
+
{% block scripts_extra %}
|
| 63 |
+
<script>
|
| 64 |
+
document.addEventListener('DOMContentLoaded', async function () {
|
| 65 |
+
const userEmailPlaceholder = document.getElementById('userEmailPlaceholder');
|
| 66 |
+
const modelSelectionCards = document.getElementById('modelSelectionCards');
|
| 67 |
+
const quickLinksSection = document.getElementById('quickLinksSection');
|
| 68 |
+
const loadingMessage = document.getElementById('loadingMessage');
|
| 69 |
+
const authErrorMessage = document.getElementById('authErrorMessage');
|
| 70 |
+
|
| 71 |
+
const accessToken = localStorage.getItem('accessToken');
|
| 72 |
+
|
| 73 |
+
if (!accessToken) {
|
| 74 |
+
authErrorMessage.textContent = 'No authentication token found. Redirecting to login...';
|
| 75 |
+
authErrorMessage.style.display = 'block';
|
| 76 |
+
setTimeout(() => {
|
| 77 |
+
window.location.href = "{{ url_for('login_page_render') }}";
|
| 78 |
+
}, 2000);
|
| 79 |
+
return;
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
loadingMessage.style.display = 'block';
|
| 83 |
+
|
| 84 |
+
try {
|
| 85 |
+
const response = await fetch("{{ url_for('read_users_me') }}", { // /auth/users/me
|
| 86 |
+
method: 'GET',
|
| 87 |
+
headers: {
|
| 88 |
+
'Authorization': `Bearer ${accessToken}`,
|
| 89 |
+
'Accept': 'application/json'
|
| 90 |
+
}
|
| 91 |
+
});
|
| 92 |
+
|
| 93 |
+
loadingMessage.style.display = 'none';
|
| 94 |
+
|
| 95 |
+
if (response.ok) {
|
| 96 |
+
const userData = await response.json();
|
| 97 |
+
if (userEmailPlaceholder) {
|
| 98 |
+
userEmailPlaceholder.textContent = userData.email;
|
| 99 |
+
}
|
| 100 |
+
// Show content that depends on authentication
|
| 101 |
+
if(modelSelectionCards) modelSelectionCards.style.display = 'flex'; // Or 'block' if that's better for layout
|
| 102 |
+
if(quickLinksSection) quickLinksSection.style.display = 'block';
|
| 103 |
+
|
| 104 |
+
// Update navbar if needed (more complex, requires navbar to have placeholders)
|
| 105 |
+
// For example, if navbar had <span id="navUserEmail"></span>
|
| 106 |
+
// const navUserEmail = document.getElementById('navUserEmail');
|
| 107 |
+
// if (navUserEmail) navUserEmail.textContent = `Welcome, ${userData.email}`;
|
| 108 |
+
// const logoutLink = document.getElementById('navLogoutLink'); // Assuming logout link has an ID
|
| 109 |
+
// if (logoutLink) logoutLink.style.display = 'block';
|
| 110 |
+
// const loginLink = document.getElementById('navLoginLink');
|
| 111 |
+
// if (loginLink) loginLink.style.display = 'none';
|
| 112 |
+
|
| 113 |
+
|
| 114 |
+
} else {
|
| 115 |
+
// Handle 401 or other errors from /auth/users/me
|
| 116 |
+
localStorage.removeItem('accessToken');
|
| 117 |
+
localStorage.removeItem('tokenType');
|
| 118 |
+
authErrorMessage.textContent = `Session expired or invalid. Redirecting to login... (Status: ${response.status})`;
|
| 119 |
+
authErrorMessage.style.display = 'block';
|
| 120 |
+
setTimeout(() => {
|
| 121 |
+
window.location.href = "{{ url_for('login_page_render') }}";
|
| 122 |
+
}, 3000);
|
| 123 |
+
}
|
| 124 |
+
} catch (error) {
|
| 125 |
+
console.error('Error fetching user data:', error);
|
| 126 |
+
loadingMessage.style.display = 'none';
|
| 127 |
+
authErrorMessage.textContent = 'Error loading user data. Redirecting to login...';
|
| 128 |
+
authErrorMessage.style.display = 'block';
|
| 129 |
+
localStorage.removeItem('accessToken');
|
| 130 |
+
localStorage.removeItem('tokenType');
|
| 131 |
+
setTimeout(() => {
|
| 132 |
+
window.location.href = "{{ url_for('login_page_render') }}";
|
| 133 |
+
}, 3000);
|
| 134 |
+
}
|
| 135 |
+
});
|
| 136 |
+
</script>
|
| 137 |
+
{% endblock %}
|
app/templates/user/recommendations.html
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
{% extends "partials/base.html" %}
|
| 2 |
+
|
| 3 |
+
{% block title %}Market Trends & Recommendations - Financial Advisor{% endblock %}
|
| 4 |
+
|
| 5 |
+
{% block content %}
|
| 6 |
+
<div class="container mt-5">
|
| 7 |
+
<div class="row mb-4">
|
| 8 |
+
<div class="col">
|
| 9 |
+
<h1>Market Trends & Recommendations</h1>
|
| 10 |
+
<p>Stay updated with the latest market information and our curated recommendations.</p>
|
| 11 |
+
</div>
|
| 12 |
+
</div>
|
| 13 |
+
|
| 14 |
+
<!-- Current Market Prices -->
|
| 15 |
+
<div class="row mb-4">
|
| 16 |
+
<div class="col-md-6">
|
| 17 |
+
<div class="card shadow-sm">
|
| 18 |
+
<div class="card-header bg-info text-white">
|
| 19 |
+
<h4 class="mb-0">Current Stock Prices</h4>
|
| 20 |
+
</div>
|
| 21 |
+
<div class="card-body">
|
| 22 |
+
<input type="text" id="stockSearch" class="form-control mb-3" placeholder="Search stocks...">
|
| 23 |
+
</div>
|
| 24 |
+
<ul class="list-group list-group-flush" id="stockList">
|
| 25 |
+
<!-- Stock search results will be populated here -->
|
| 26 |
+
</ul>
|
| 27 |
+
<!-- Placeholder for Price Information -->
|
| 28 |
+
<div id="stockPriceInfo" class="card-body" style="display: none;">
|
| 29 |
+
<h5>Price Information:</h5>
|
| 30 |
+
<div id="priceDetails"></div>
|
| 31 |
+
</div>
|
| 32 |
+
</div>
|
| 33 |
+
</div>
|
| 34 |
+
<div class="col-md-6 mt-3 mt-md-0">
|
| 35 |
+
<div class="card shadow-sm">
|
| 36 |
+
<div class="card-header bg-warning text-dark">
|
| 37 |
+
<h4 class="mb-0">Gold Price</h4>
|
| 38 |
+
</div>
|
| 39 |
+
<div class="card-body">
|
| 40 |
+
{% if market_data.gold_price %}
|
| 41 |
+
<p class="card-text fs-5">{{ market_data.gold_price }}</p>
|
| 42 |
+
{% else %}
|
| 43 |
+
<p class="card-text">Gold price data not available.</p>
|
| 44 |
+
{% endif %}
|
| 45 |
+
</div>
|
| 46 |
+
</div>
|
| 47 |
+
</div>
|
| 48 |
+
</div>
|
| 49 |
+
|
| 50 |
+
<!-- Recommended Stocks -->
|
| 51 |
+
<div class="row">
|
| 52 |
+
<div class="col">
|
| 53 |
+
<div class="card shadow-sm">
|
| 54 |
+
<div class="card-header bg-success text-white">
|
| 55 |
+
<h4 class="mb-0">Recommended Stocks</h4>
|
| 56 |
+
</div>
|
| 57 |
+
<div class="card-body">
|
| 58 |
+
{% if market_data.recommended_stocks %}
|
| 59 |
+
{% for stock in market_data.recommended_stocks %}
|
| 60 |
+
<div class="mb-3 p-2 border-bottom">
|
| 61 |
+
<h5>{{ stock.name }}</h5>
|
| 62 |
+
<p class="mb-1">{{ stock.reason }}</p>
|
| 63 |
+
</div>
|
| 64 |
+
{% endfor %}
|
| 65 |
+
{% else %}
|
| 66 |
+
<p>No specific stock recommendations at this time.</p>
|
| 67 |
+
{% endif %}
|
| 68 |
+
</div>
|
| 69 |
+
</div>
|
| 70 |
+
</div>
|
| 71 |
+
</div>
|
| 72 |
+
<div class="mt-4">
|
| 73 |
+
<a href="{{ url_for('user_homepage') }}" class="btn btn-secondary">« Back to Homepage</a>
|
| 74 |
+
</div>
|
| 75 |
+
</div>
|
| 76 |
+
|
| 77 |
+
<!-- Fixed Deposits Section -->
|
| 78 |
+
<div class="container mt-5">
|
| 79 |
+
<div class="row mb-4">
|
| 80 |
+
<div class="col">
|
| 81 |
+
<div class="card shadow-sm">
|
| 82 |
+
<div class="card-header bg-primary text-white">
|
| 83 |
+
<h4 class="mb-0">Fixed Deposit (FD) Interest Rates in India</h4>
|
| 84 |
+
</div>
|
| 85 |
+
<div class="table-responsive">
|
| 86 |
+
<table class="table table-hover mb-0">
|
| 87 |
+
<thead class="table-light">
|
| 88 |
+
<tr>
|
| 89 |
+
<th scope="col">Bank Name</th>
|
| 90 |
+
<th scope="col">Highest Interest Rate</th>
|
| 91 |
+
<th scope="col">Tenure</th>
|
| 92 |
+
<th scope="col">Senior Citizen Rate (Up to)</th>
|
| 93 |
+
</tr>
|
| 94 |
+
</thead>
|
| 95 |
+
<tbody>
|
| 96 |
+
<tr>
|
| 97 |
+
<td>Airtel Bank</td>
|
| 98 |
+
<td>9.1%</td>
|
| 99 |
+
<td>36 to 60 months</td>
|
| 100 |
+
<td>Varies (up to 9.1%)</td>
|
| 101 |
+
</tr>
|
| 102 |
+
<tr>
|
| 103 |
+
<td>SBI (State Bank of India)</td>
|
| 104 |
+
<td>7.25%</td>
|
| 105 |
+
<td>2 to 3 years</td>
|
| 106 |
+
<td>Up to 7.40%</td>
|
| 107 |
+
</tr>
|
| 108 |
+
<tr>
|
| 109 |
+
<td>Axis Bank</td>
|
| 110 |
+
<td>7.25%</td>
|
| 111 |
+
<td>15 months to <2 years</td>
|
| 112 |
+
<td>Up to 7.95%</td>
|
| 113 |
+
</tr>
|
| 114 |
+
<tr>
|
| 115 |
+
<td>Indian Post (Post Office)</td>
|
| 116 |
+
<td>7.50%</td>
|
| 117 |
+
<td>5 years (Post Office TD)</td>
|
| 118 |
+
<td>Not specified</td>
|
| 119 |
+
</tr>
|
| 120 |
+
<tr>
|
| 121 |
+
<td>HDFC Bank</td>
|
| 122 |
+
<td>7.20%</td>
|
| 123 |
+
<td>4 years 7 months (55 months)</td>
|
| 124 |
+
<td>Up to 7.75%</td>
|
| 125 |
+
</tr>
|
| 126 |
+
<tr>
|
| 127 |
+
<td>ICICI Bank</td>
|
| 128 |
+
<td>7.10%</td>
|
| 129 |
+
<td>15 months to 2 years</td>
|
| 130 |
+
<td>Up to 7.60%</td>
|
| 131 |
+
</tr>
|
| 132 |
+
<tr>
|
| 133 |
+
<td>Canara Bank</td>
|
| 134 |
+
<td>6.70%</td>
|
| 135 |
+
<td>2 to 3 years</td>
|
| 136 |
+
<td>Up to 7.20%</td>
|
| 137 |
+
</tr>
|
| 138 |
+
<tr>
|
| 139 |
+
<td>PNB (Punjab National Bank)</td>
|
| 140 |
+
<td>7.05%</td>
|
| 141 |
+
<td>300 days</td>
|
| 142 |
+
<td>Up to 7.55%</td>
|
| 143 |
+
</tr>
|
| 144 |
+
</tbody>
|
| 145 |
+
</table>
|
| 146 |
+
</div>
|
| 147 |
+
</div>
|
| 148 |
+
</div>
|
| 149 |
+
</div>
|
| 150 |
+
</div>
|
| 151 |
+
|
| 152 |
+
<script>
|
| 153 |
+
document.addEventListener('DOMContentLoaded', function () {
|
| 154 |
+
const stockSearchInput = document.getElementById('stockSearch');
|
| 155 |
+
const stockList = document.getElementById('stockList');
|
| 156 |
+
const stockPriceInfoDiv = document.getElementById('stockPriceInfo');
|
| 157 |
+
const priceDetailsDiv = document.getElementById('priceDetails');
|
| 158 |
+
|
| 159 |
+
// Clear the initial mock stock list items
|
| 160 |
+
stockList.innerHTML = '';
|
| 161 |
+
|
| 162 |
+
if (stockSearchInput) {
|
| 163 |
+
stockSearchInput.addEventListener('input', async function () {
|
| 164 |
+
const searchTerm = stockSearchInput.value;
|
| 165 |
+
if (searchTerm.length > 1) { // Start searching after 2 characters
|
| 166 |
+
try {
|
| 167 |
+
const response = await fetch(`/user/api/stocks/search?query=${encodeURIComponent(searchTerm)}`);
|
| 168 |
+
if (!response.ok) {
|
| 169 |
+
throw new Error(`HTTP error! status: ${response.status}`);
|
| 170 |
+
}
|
| 171 |
+
const tickers = await response.json();
|
| 172 |
+
displayStockSearchResults(tickers);
|
| 173 |
+
} catch (error) {
|
| 174 |
+
console.error("Error fetching stock search results:", error);
|
| 175 |
+
stockList.innerHTML = '<li class="list-group-item">Error fetching results.</li>';
|
| 176 |
+
}
|
| 177 |
+
} else {
|
| 178 |
+
stockList.innerHTML = ''; // Clear list if search term is too short
|
| 179 |
+
stockPriceInfoDiv.style.display = 'none'; // Hide price info
|
| 180 |
+
}
|
| 181 |
+
});
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
function displayStockSearchResults(tickers) {
|
| 185 |
+
stockList.innerHTML = ''; // Clear previous results
|
| 186 |
+
if (tickers.length > 0) {
|
| 187 |
+
tickers.forEach(ticker => {
|
| 188 |
+
const listItem = document.createElement('li');
|
| 189 |
+
listItem.classList.add('list-group-item', 'stock-item');
|
| 190 |
+
listItem.textContent = ticker;
|
| 191 |
+
listItem.style.cursor = 'pointer'; // Indicate clickable
|
| 192 |
+
listItem.addEventListener('click', () => selectStock(ticker));
|
| 193 |
+
stockList.appendChild(listItem);
|
| 194 |
+
});
|
| 195 |
+
} else {
|
| 196 |
+
stockList.innerHTML = '<li class="list-group-item">No matching stocks found.</li>';
|
| 197 |
+
}
|
| 198 |
+
}
|
| 199 |
+
|
| 200 |
+
async function selectStock(tickerSymbol) {
|
| 201 |
+
stockSearchInput.value = tickerSymbol; // Set input value to selected ticker
|
| 202 |
+
stockList.innerHTML = ''; // Clear search results
|
| 203 |
+
stockPriceInfoDiv.style.display = 'block'; // Show price info section
|
| 204 |
+
priceDetailsDiv.innerHTML = 'Loading price data...'; // Loading message
|
| 205 |
+
|
| 206 |
+
try {
|
| 207 |
+
const response = await fetch(`/user/api/stocks/price/${encodeURIComponent(tickerSymbol)}`);
|
| 208 |
+
if (!response.ok) {
|
| 209 |
+
// Attempt to read error message from response body
|
| 210 |
+
const errorText = await response.text();
|
| 211 |
+
throw new Error(`HTTP error! status: ${response.status} - ${errorText}`);
|
| 212 |
+
}
|
| 213 |
+
const priceData = await response.json();
|
| 214 |
+
displayStockPrice(tickerSymbol, priceData);
|
| 215 |
+
} catch (error) {
|
| 216 |
+
console.error("Error fetching stock price:", error);
|
| 217 |
+
priceDetailsDiv.innerHTML = `Error fetching price for ${tickerSymbol}.`;
|
| 218 |
+
}
|
| 219 |
+
}
|
| 220 |
+
|
| 221 |
+
function displayStockPrice(tickerSymbol, priceData) {
|
| 222 |
+
let html = `<h6>${tickerSymbol}</h6>`;
|
| 223 |
+
for (const label in priceData) {
|
| 224 |
+
const [price, date] = priceData[label];
|
| 225 |
+
if (date) {
|
| 226 |
+
html += `<p>${label} price on ${date}: <strong>${price}</strong></p>`;
|
| 227 |
+
} else {
|
| 228 |
+
html += `<p>${label} price: <strong>${price}</strong></p>`;
|
| 229 |
+
}
|
| 230 |
+
}
|
| 231 |
+
priceDetailsDiv.innerHTML = html;
|
| 232 |
+
}
|
| 233 |
+
|
| 234 |
+
// Initial state: clear the list and hide price info on page load
|
| 235 |
+
stockList.innerHTML = '';
|
| 236 |
+
stockPriceInfoDiv.style.display = 'none';
|
| 237 |
+
|
| 238 |
+
});
|
| 239 |
+
</script>
|
| 240 |
+
{% endblock %}
|
app/tickers.txt
ADDED
|
@@ -0,0 +1,2099 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
20MICRONS.NS
|
| 2 |
+
21STCENMGM.NS
|
| 3 |
+
360ONE.NS
|
| 4 |
+
3IINFOLTD.NS
|
| 5 |
+
3MINDIA.NS
|
| 6 |
+
3PLAND.NS
|
| 7 |
+
5PAISA.NS
|
| 8 |
+
63MOONS.NS
|
| 9 |
+
A2ZINFRA.NS
|
| 10 |
+
AAATECH.NS
|
| 11 |
+
AADHARHFC.NS
|
| 12 |
+
AAKASH.NS
|
| 13 |
+
AAREYDRUGS.NS
|
| 14 |
+
AARON.NS
|
| 15 |
+
AARTECH.NS
|
| 16 |
+
AARTIDRUGS.NS
|
| 17 |
+
AARTIIND.NS
|
| 18 |
+
AARTIPHARM.NS
|
| 19 |
+
AARTISURF.NS
|
| 20 |
+
AARVEEDEN.NS
|
| 21 |
+
AARVI.NS
|
| 22 |
+
AAVAS.NS
|
| 23 |
+
ABAN.NS
|
| 24 |
+
ABB.NS
|
| 25 |
+
ABBOTINDIA.NS
|
| 26 |
+
ABCAPITAL.NS
|
| 27 |
+
ABDL.NS
|
| 28 |
+
ABFRL.NS
|
| 29 |
+
ABINFRA.NS
|
| 30 |
+
ABMINTLLTD.NS
|
| 31 |
+
ABREL.NS
|
| 32 |
+
ABSLAMC.NS
|
| 33 |
+
ACC.NS
|
| 34 |
+
ACCELYA.NS
|
| 35 |
+
ACCURACY.NS
|
| 36 |
+
ACE.NS
|
| 37 |
+
ACEINTEG.NS
|
| 38 |
+
ACI.NS
|
| 39 |
+
ACL.NS
|
| 40 |
+
ACLGATI.NS
|
| 41 |
+
ACMESOLAR.NS
|
| 42 |
+
ADANIENSOL.NS
|
| 43 |
+
ADANIENT.NS
|
| 44 |
+
ADANIGREEN.NS
|
| 45 |
+
ADANIPORTS.NS
|
| 46 |
+
ADANIPOWER.NS
|
| 47 |
+
ADFFOODS.NS
|
| 48 |
+
ADL.NS
|
| 49 |
+
ADOR.NS
|
| 50 |
+
ADROITINFO.NS
|
| 51 |
+
ADSL.NS
|
| 52 |
+
ADVANIHOTR.NS
|
| 53 |
+
ADVENZYMES.NS
|
| 54 |
+
AEGISLOG.NS
|
| 55 |
+
AEROFLEX.NS
|
| 56 |
+
AETHER.NS
|
| 57 |
+
AFCONS.NS
|
| 58 |
+
AFFLE.NS
|
| 59 |
+
AFFORDABLE.NS
|
| 60 |
+
AFIL.NS
|
| 61 |
+
AFSL.NS
|
| 62 |
+
AGARIND.NS
|
| 63 |
+
AGARWALEYE.NS
|
| 64 |
+
AGI.NS
|
| 65 |
+
AGIIL.NS
|
| 66 |
+
AGRITECH.NS
|
| 67 |
+
AGROPHOS.NS
|
| 68 |
+
AGSTRA.NS
|
| 69 |
+
AHLADA.NS
|
| 70 |
+
AHLEAST.NS
|
| 71 |
+
AHLUCONT.NS
|
| 72 |
+
AIAENG.NS
|
| 73 |
+
AIIL.NS
|
| 74 |
+
AIRAN.NS
|
| 75 |
+
AIROLAM.NS
|
| 76 |
+
AJANTPHARM.NS
|
| 77 |
+
AJAXENGG.NS
|
| 78 |
+
AJMERA.NS
|
| 79 |
+
AJOONI.NS
|
| 80 |
+
AKASH.NS
|
| 81 |
+
AKG.NS
|
| 82 |
+
AKI.NS
|
| 83 |
+
AKSHAR.NS
|
| 84 |
+
AKSHARCHEM.NS
|
| 85 |
+
AKSHOPTFBR.NS
|
| 86 |
+
AKUMS.NS
|
| 87 |
+
AKZOINDIA.NS
|
| 88 |
+
ALANKIT.NS
|
| 89 |
+
ALBERTDAVD.NS
|
| 90 |
+
ALEMBICLTD.NS
|
| 91 |
+
ALICON.NS
|
| 92 |
+
ALIVUS.NS
|
| 93 |
+
ALKALI.NS
|
| 94 |
+
ALKEM.NS
|
| 95 |
+
ALKYLAMINE.NS
|
| 96 |
+
ALLCARGO.NS
|
| 97 |
+
ALLDIGI.NS
|
| 98 |
+
ALMONDZ.NS
|
| 99 |
+
ALOKINDS.NS
|
| 100 |
+
ALPA.NS
|
| 101 |
+
ALPHAGEO.NS
|
| 102 |
+
ALPSINDUS.NS
|
| 103 |
+
AMBER.NS
|
| 104 |
+
AMBICAAGAR.NS
|
| 105 |
+
AMBIKCO.NS
|
| 106 |
+
AMBUJACEM.NS
|
| 107 |
+
AMDIND.NS
|
| 108 |
+
AMIORG.NS
|
| 109 |
+
AMJLAND.NS
|
| 110 |
+
AMNPLST.NS
|
| 111 |
+
AMRUTANJAN.NS
|
| 112 |
+
ANANDRATHI.NS
|
| 113 |
+
ANANTRAJ.NS
|
| 114 |
+
ANDHRAPAP.NS
|
| 115 |
+
ANDHRSUGAR.NS
|
| 116 |
+
ANGELONE.NS
|
| 117 |
+
ANIKINDS.NS
|
| 118 |
+
ANKITMETAL.NS
|
| 119 |
+
ANMOL.NS
|
| 120 |
+
ANSALAPI.NS
|
| 121 |
+
ANTGRAPHIC.NS
|
| 122 |
+
ANUHPHR.NS
|
| 123 |
+
ANUP.NS
|
| 124 |
+
ANURAS.NS
|
| 125 |
+
APARINDS.NS
|
| 126 |
+
APCL.NS
|
| 127 |
+
APCOTEXIND.NS
|
| 128 |
+
APEX.NS
|
| 129 |
+
APLAPOLLO.NS
|
| 130 |
+
APLLTD.NS
|
| 131 |
+
APOLLO.NS
|
| 132 |
+
APOLLOHOSP.NS
|
| 133 |
+
APOLLOPIPE.NS
|
| 134 |
+
APOLLOTYRE.NS
|
| 135 |
+
APOLSINHOT.NS
|
| 136 |
+
APTECHT.NS
|
| 137 |
+
APTUS.NS
|
| 138 |
+
ARCHIDPLY.NS
|
| 139 |
+
ARCHIES.NS
|
| 140 |
+
ARE&M.NS
|
| 141 |
+
ARENTERP.NS
|
| 142 |
+
ARIES.NS
|
| 143 |
+
ARIHANTCAP.NS
|
| 144 |
+
ARIHANTSUP.NS
|
| 145 |
+
ARKADE.NS
|
| 146 |
+
ARMANFIN.NS
|
| 147 |
+
AROGRANITE.NS
|
| 148 |
+
ARROWGREEN.NS
|
| 149 |
+
ARSHIYA.NS
|
| 150 |
+
ARSSINFRA.NS
|
| 151 |
+
ARTEMISMED.NS
|
| 152 |
+
ARTNIRMAN.NS
|
| 153 |
+
ARVEE.NS
|
| 154 |
+
ARVIND.NS
|
| 155 |
+
ARVINDFASN.NS
|
| 156 |
+
ARVSMART.NS
|
| 157 |
+
ASAHIINDIA.NS
|
| 158 |
+
ASAHISONG.NS
|
| 159 |
+
ASAL.NS
|
| 160 |
+
ASALCBR.NS
|
| 161 |
+
ASHAPURMIN.NS
|
| 162 |
+
ASHIANA.NS
|
| 163 |
+
ASHIMASYN.NS
|
| 164 |
+
ASHOKA.NS
|
| 165 |
+
ASHOKAMET.NS
|
| 166 |
+
ASHOKLEY.NS
|
| 167 |
+
ASIANENE.NS
|
| 168 |
+
ASIANHOTNR.NS
|
| 169 |
+
ASIANPAINT.NS
|
| 170 |
+
ASIANTILES.NS
|
| 171 |
+
ASKAUTOLTD.NS
|
| 172 |
+
ASMS.NS
|
| 173 |
+
ASPINWALL.NS
|
| 174 |
+
ASTEC.NS
|
| 175 |
+
ASTERDM.NS
|
| 176 |
+
ASTRAL.NS
|
| 177 |
+
ASTRAMICRO.NS
|
| 178 |
+
ASTRAZEN.NS
|
| 179 |
+
ASTRON.NS
|
| 180 |
+
ATALREAL.NS
|
| 181 |
+
ATAM.NS
|
| 182 |
+
ATGL.NS
|
| 183 |
+
ATHERENERG.NS
|
| 184 |
+
ATL.NS
|
| 185 |
+
ATLANTAA.NS
|
| 186 |
+
ATLASCYCLE.NS
|
| 187 |
+
ATUL.NS
|
| 188 |
+
ATULAUTO.NS
|
| 189 |
+
AUBANK.NS
|
| 190 |
+
AURIONPRO.NS
|
| 191 |
+
AUROPHARMA.NS
|
| 192 |
+
AURUM.NS
|
| 193 |
+
AUSOMENT.NS
|
| 194 |
+
AUTOAXLES.NS
|
| 195 |
+
AUTOIND.NS
|
| 196 |
+
AVADHSUGAR.NS
|
| 197 |
+
AVALON.NS
|
| 198 |
+
AVANTEL.NS
|
| 199 |
+
AVANTEL-RE.NS
|
| 200 |
+
AVANTIFEED.NS
|
| 201 |
+
AVG.NS
|
| 202 |
+
AVL.NS
|
| 203 |
+
AVONMORE.NS
|
| 204 |
+
AVROIND.NS
|
| 205 |
+
AVTNPL.NS
|
| 206 |
+
AWFIS.NS
|
| 207 |
+
AWHCL.NS
|
| 208 |
+
AWL.NS
|
| 209 |
+
AXISBANK.NS
|
| 210 |
+
AXISCADES.NS
|
| 211 |
+
AXITA.NS
|
| 212 |
+
AYMSYNTEX.NS
|
| 213 |
+
AZAD.NS
|
| 214 |
+
BAFNAPH.NS
|
| 215 |
+
BAGFILMS.NS
|
| 216 |
+
BAIDFIN.NS
|
| 217 |
+
BAJAJ-AUTO.NS
|
| 218 |
+
BAJAJCON.NS
|
| 219 |
+
BAJAJELEC.NS
|
| 220 |
+
BAJAJFINSV.NS
|
| 221 |
+
BAJAJHCARE.NS
|
| 222 |
+
BAJAJHFL.NS
|
| 223 |
+
BAJAJHIND.NS
|
| 224 |
+
BAJAJHLDNG.NS
|
| 225 |
+
BAJAJINDEF.NS
|
| 226 |
+
BAJEL.NS
|
| 227 |
+
BAJFINANCE.NS
|
| 228 |
+
BALAJEE.NS
|
| 229 |
+
BALAJITELE.NS
|
| 230 |
+
BALAMINES.NS
|
| 231 |
+
BALAXI.NS
|
| 232 |
+
BALKRISHNA.NS
|
| 233 |
+
BALKRISIND.NS
|
| 234 |
+
BALMLAWRIE.NS
|
| 235 |
+
BALPHARMA.NS
|
| 236 |
+
BALRAMCHIN.NS
|
| 237 |
+
BALUFORGE.NS
|
| 238 |
+
BANARBEADS.NS
|
| 239 |
+
BANARISUG.NS
|
| 240 |
+
BANCOINDIA.NS
|
| 241 |
+
BANDHANBNK.NS
|
| 242 |
+
BANG.NS
|
| 243 |
+
BANKA.NS
|
| 244 |
+
BANKBARODA.NS
|
| 245 |
+
BANKINDIA.NS
|
| 246 |
+
BANSALWIRE.NS
|
| 247 |
+
BANSWRAS.NS
|
| 248 |
+
BARBEQUE.NS
|
| 249 |
+
BASF.NS
|
| 250 |
+
BASML.NS
|
| 251 |
+
BASML-RE1.NS
|
| 252 |
+
BATAINDIA.NS
|
| 253 |
+
BAYERCROP.NS
|
| 254 |
+
BBL.NS
|
| 255 |
+
BBOX.NS
|
| 256 |
+
BBTC.NS
|
| 257 |
+
BBTCL.NS
|
| 258 |
+
BCLIND.NS
|
| 259 |
+
BCONCEPTS.NS
|
| 260 |
+
BDL.NS
|
| 261 |
+
BEARDSELL.NS
|
| 262 |
+
BECTORFOOD.NS
|
| 263 |
+
BEDMUTHA.NS
|
| 264 |
+
BEL.NS
|
| 265 |
+
BEML.NS
|
| 266 |
+
BEPL.NS
|
| 267 |
+
BERGEPAINT.NS
|
| 268 |
+
BESTAGRO.NS
|
| 269 |
+
BFINVEST.NS
|
| 270 |
+
BFUTILITIE.NS
|
| 271 |
+
BGRENERGY.NS
|
| 272 |
+
BHAGCHEM.NS
|
| 273 |
+
BHAGERIA.NS
|
| 274 |
+
BHAGYANGR.NS
|
| 275 |
+
BHANDARI.NS
|
| 276 |
+
BHARATFORG.NS
|
| 277 |
+
BHARATGEAR.NS
|
| 278 |
+
BHARATRAS.NS
|
| 279 |
+
BHARATSE.NS
|
| 280 |
+
BHARATWIRE.NS
|
| 281 |
+
BHARTIARTL.NS
|
| 282 |
+
BHARTIHEXA.NS
|
| 283 |
+
BHEL.NS
|
| 284 |
+
BIGBLOC.NS
|
| 285 |
+
BIKAJI.NS
|
| 286 |
+
BIL.NS
|
| 287 |
+
BINANIIND.NS
|
| 288 |
+
BIOCON.NS
|
| 289 |
+
BIOFILCHEM.NS
|
| 290 |
+
BIRLACABLE.NS
|
| 291 |
+
BIRLACORPN.NS
|
| 292 |
+
BIRLAMONEY.NS
|
| 293 |
+
BIRLANU.NS
|
| 294 |
+
BLACKBUCK.NS
|
| 295 |
+
BLAL.NS
|
| 296 |
+
BLBLIMITED.NS
|
| 297 |
+
BLISSGVS.NS
|
| 298 |
+
BLKASHYAP.NS
|
| 299 |
+
BLS.NS
|
| 300 |
+
BLSE.NS
|
| 301 |
+
BLUECOAST.NS
|
| 302 |
+
BLUEDART.NS
|
| 303 |
+
BLUEJET.NS
|
| 304 |
+
BLUESTARCO.NS
|
| 305 |
+
BODALCHEM.NS
|
| 306 |
+
BOHRAIND.NS
|
| 307 |
+
BOMDYEING.NS
|
| 308 |
+
BOROLTD.NS
|
| 309 |
+
BORORENEW.NS
|
| 310 |
+
BOROSCI.NS
|
| 311 |
+
BOSCHLTD.NS
|
| 312 |
+
BPCL.NS
|
| 313 |
+
BPL.NS
|
| 314 |
+
BRIGADE.NS
|
| 315 |
+
BRITANNIA.NS
|
| 316 |
+
BRNL.NS
|
| 317 |
+
BROOKS.NS
|
| 318 |
+
BSE.NS
|
| 319 |
+
BSHSL.NS
|
| 320 |
+
BSL.NS
|
| 321 |
+
BSOFT.NS
|
| 322 |
+
BTML.NS
|
| 323 |
+
BUTTERFLY.NS
|
| 324 |
+
BVCL.NS
|
| 325 |
+
BYKE.NS
|
| 326 |
+
CALSOFT.NS
|
| 327 |
+
CAMLINFINE.NS
|
| 328 |
+
CAMPUS.NS
|
| 329 |
+
CAMS.NS
|
| 330 |
+
CANBK.NS
|
| 331 |
+
CANFINHOME.NS
|
| 332 |
+
CANTABIL.NS
|
| 333 |
+
CAPACITE.NS
|
| 334 |
+
CAPITALSFB.NS
|
| 335 |
+
CAPLIPOINT.NS
|
| 336 |
+
CAPTRUST.NS
|
| 337 |
+
CARBORUNIV.NS
|
| 338 |
+
CARERATING.NS
|
| 339 |
+
CARRARO.NS
|
| 340 |
+
CARTRADE.NS
|
| 341 |
+
CARYSIL.NS
|
| 342 |
+
CASTROLIND.NS
|
| 343 |
+
CCCL.NS
|
| 344 |
+
CCHHL.NS
|
| 345 |
+
CCL.NS
|
| 346 |
+
CDSL.NS
|
| 347 |
+
CEATLTD.NS
|
| 348 |
+
CEIGALL.NS
|
| 349 |
+
CELEBRITY.NS
|
| 350 |
+
CELLO.NS
|
| 351 |
+
CENTENKA.NS
|
| 352 |
+
CENTEXT.NS
|
| 353 |
+
CENTRALBK.NS
|
| 354 |
+
CENTRUM.NS
|
| 355 |
+
CENTUM.NS
|
| 356 |
+
CENTURYPLY.NS
|
| 357 |
+
CERA.NS
|
| 358 |
+
CEREBRAINT.NS
|
| 359 |
+
CESC.NS
|
| 360 |
+
CEWATER.NS
|
| 361 |
+
CGCL.NS
|
| 362 |
+
CGPOWER.NS
|
| 363 |
+
CHALET.NS
|
| 364 |
+
CHAMBLFERT.NS
|
| 365 |
+
CHEMBOND.NS
|
| 366 |
+
CHEMCON.NS
|
| 367 |
+
CHEMFAB.NS
|
| 368 |
+
CHEMPLASTS.NS
|
| 369 |
+
CHENNPETRO.NS
|
| 370 |
+
CHEVIOT.NS
|
| 371 |
+
CHOICEIN.NS
|
| 372 |
+
CHOLAFIN.NS
|
| 373 |
+
CHOLAHLDNG.NS
|
| 374 |
+
CIEINDIA.NS
|
| 375 |
+
CIFL.NS
|
| 376 |
+
CIGNITITEC.NS
|
| 377 |
+
CINELINE.NS
|
| 378 |
+
CINEVISTA.NS
|
| 379 |
+
CIPLA.NS
|
| 380 |
+
CLEAN.NS
|
| 381 |
+
CLEDUCATE.NS
|
| 382 |
+
CLSEL.NS
|
| 383 |
+
CMSINFO.NS
|
| 384 |
+
COALINDIA.NS
|
| 385 |
+
COASTCORP.NS
|
| 386 |
+
COCHINSHIP.NS
|
| 387 |
+
COFFEEDAY.NS
|
| 388 |
+
COFORGE.NS
|
| 389 |
+
COLPAL.NS
|
| 390 |
+
COMPUSOFT.NS
|
| 391 |
+
COMSYN.NS
|
| 392 |
+
CONCOR.NS
|
| 393 |
+
CONCORDBIO.NS
|
| 394 |
+
CONFIPET.NS
|
| 395 |
+
CONSOFINVT.NS
|
| 396 |
+
CONTROLPR.NS
|
| 397 |
+
CORALFINAC.NS
|
| 398 |
+
CORDSCABLE.NS
|
| 399 |
+
COROMANDEL.NS
|
| 400 |
+
COSMOFIRST.NS
|
| 401 |
+
COUNCODOS.NS
|
| 402 |
+
CPCAP.NS
|
| 403 |
+
CRAFTSMAN.NS
|
| 404 |
+
CREATIVE.NS
|
| 405 |
+
CREATIVEYE.NS
|
| 406 |
+
CREDITACC.NS
|
| 407 |
+
CREST.NS
|
| 408 |
+
CRISIL.NS
|
| 409 |
+
CROMPTON.NS
|
| 410 |
+
CROWN.NS
|
| 411 |
+
CSBBANK.NS
|
| 412 |
+
CSLFINANCE.NS
|
| 413 |
+
CTE.NS
|
| 414 |
+
CUB.NS
|
| 415 |
+
CUBEXTUB.NS
|
| 416 |
+
CUMMINSIND.NS
|
| 417 |
+
CUPID.NS
|
| 418 |
+
CURAA.NS
|
| 419 |
+
CYBERMEDIA.NS
|
| 420 |
+
CYBERTECH.NS
|
| 421 |
+
CYIENT.NS
|
| 422 |
+
CYIENTDLM.NS
|
| 423 |
+
DABUR.NS
|
| 424 |
+
DALBHARAT.NS
|
| 425 |
+
DALMIASUG.NS
|
| 426 |
+
DAMCAPITAL.NS
|
| 427 |
+
DAMODARIND.NS
|
| 428 |
+
DANGEE.NS
|
| 429 |
+
DATAMATICS.NS
|
| 430 |
+
DATAPATTNS.NS
|
| 431 |
+
DAVANGERE.NS
|
| 432 |
+
DBCORP.NS
|
| 433 |
+
DBEIL.NS
|
| 434 |
+
DBL.NS
|
| 435 |
+
DBOL.NS
|
| 436 |
+
DBREALTY.NS
|
| 437 |
+
DBSTOCKBRO.NS
|
| 438 |
+
DCAL.NS
|
| 439 |
+
DCBBANK.NS
|
| 440 |
+
DCI.NS
|
| 441 |
+
DCM.NS
|
| 442 |
+
DCMFINSERV.NS
|
| 443 |
+
DCMNVL.NS
|
| 444 |
+
DCMSHRIRAM.NS
|
| 445 |
+
DCMSRIND.NS
|
| 446 |
+
DCW.NS
|
| 447 |
+
DCXINDIA.NS
|
| 448 |
+
DDEVPLSTIK.NS
|
| 449 |
+
DECCANCE.NS
|
| 450 |
+
DEEDEV.NS
|
| 451 |
+
DEEPAKFERT.NS
|
| 452 |
+
DEEPAKNTR.NS
|
| 453 |
+
DEEPINDS.NS
|
| 454 |
+
DELHIVERY.NS
|
| 455 |
+
DELPHIFX.NS
|
| 456 |
+
DELTACORP.NS
|
| 457 |
+
DELTAMAGNT.NS
|
| 458 |
+
DEN.NS
|
| 459 |
+
DENORA.NS
|
| 460 |
+
DENTA.NS
|
| 461 |
+
DEVIT.NS
|
| 462 |
+
DEVYANI.NS
|
| 463 |
+
DGCONTENT.NS
|
| 464 |
+
DHAMPURSUG.NS
|
| 465 |
+
DHANBANK.NS
|
| 466 |
+
DHANI.NS
|
| 467 |
+
DHANUKA.NS
|
| 468 |
+
DHARMAJ.NS
|
| 469 |
+
DHRUV.NS
|
| 470 |
+
DHUNINV.NS
|
| 471 |
+
DIACABS.NS
|
| 472 |
+
DIAMINESQ.NS
|
| 473 |
+
DIAMONDYD.NS
|
| 474 |
+
DICIND.NS
|
| 475 |
+
DIFFNKG.NS
|
| 476 |
+
DIGIDRIVE.NS
|
| 477 |
+
DIGISPICE.NS
|
| 478 |
+
DIGJAMLMTD.NS
|
| 479 |
+
DIL.NS
|
| 480 |
+
DISHTV.NS
|
| 481 |
+
DIVGIITTS.NS
|
| 482 |
+
DIVISLAB.NS
|
| 483 |
+
DIXON.NS
|
| 484 |
+
DJML.NS
|
| 485 |
+
DLF.NS
|
| 486 |
+
DLINKINDIA.NS
|
| 487 |
+
DMART.NS
|
| 488 |
+
DMCC.NS
|
| 489 |
+
DNAMEDIA.NS
|
| 490 |
+
DODLA.NS
|
| 491 |
+
DOLATALGO.NS
|
| 492 |
+
DOLLAR.NS
|
| 493 |
+
DOLPHIN.NS
|
| 494 |
+
DOMS.NS
|
| 495 |
+
DONEAR.NS
|
| 496 |
+
DPABHUSHAN.NS
|
| 497 |
+
DPSCLTD.NS
|
| 498 |
+
DPWIRES.NS
|
| 499 |
+
DRCSYSTEMS.NS
|
| 500 |
+
DREAMFOLKS.NS
|
| 501 |
+
DREDGECORP.NS
|
| 502 |
+
DRREDDY.NS
|
| 503 |
+
DSSL.NS
|
| 504 |
+
DTIL.NS
|
| 505 |
+
DUCON.NS
|
| 506 |
+
DVL.NS
|
| 507 |
+
DWARKESH.NS
|
| 508 |
+
DYCL.NS
|
| 509 |
+
DYNAMATECH.NS
|
| 510 |
+
DYNPRO.NS
|
| 511 |
+
E2E.NS
|
| 512 |
+
EASEMYTRIP.NS
|
| 513 |
+
ECLERX.NS
|
| 514 |
+
ECOSMOBLTY.NS
|
| 515 |
+
EDELWEISS.NS
|
| 516 |
+
EICHERMOT.NS
|
| 517 |
+
EIDPARRY.NS
|
| 518 |
+
EIEL.NS
|
| 519 |
+
EIFFL.NS
|
| 520 |
+
EIHAHOTELS.NS
|
| 521 |
+
EIHOTEL.NS
|
| 522 |
+
EIMCOELECO.NS
|
| 523 |
+
EKC.NS
|
| 524 |
+
ELDEHSG.NS
|
| 525 |
+
ELECON.NS
|
| 526 |
+
ELECTCAST.NS
|
| 527 |
+
ELECTHERM.NS
|
| 528 |
+
ELGIEQUIP.NS
|
| 529 |
+
ELGIRUBCO.NS
|
| 530 |
+
ELIN.NS
|
| 531 |
+
EMAMILTD.NS
|
| 532 |
+
EMAMIPAP.NS
|
| 533 |
+
EMAMIREAL.NS
|
| 534 |
+
EMBDL.NS
|
| 535 |
+
EMCURE.NS
|
| 536 |
+
EMIL.NS
|
| 537 |
+
EMKAY.NS
|
| 538 |
+
EMMBI.NS
|
| 539 |
+
EMSLIMITED.NS
|
| 540 |
+
EMUDHRA.NS
|
| 541 |
+
ENDURANCE.NS
|
| 542 |
+
ENERGYDEV.NS
|
| 543 |
+
ENGINERSIN.NS
|
| 544 |
+
ENIL.NS
|
| 545 |
+
ENTERO.NS
|
| 546 |
+
EPACK.NS
|
| 547 |
+
EPIGRAL.NS
|
| 548 |
+
EPL.NS
|
| 549 |
+
EQUIPPP.NS
|
| 550 |
+
EQUITASBNK.NS
|
| 551 |
+
ERIS.NS
|
| 552 |
+
ESABINDIA.NS
|
| 553 |
+
ESAFSFB.NS
|
| 554 |
+
ESCORTS.NS
|
| 555 |
+
ESSARSHPNG.NS
|
| 556 |
+
ESSENTIA.NS
|
| 557 |
+
ESTER.NS
|
| 558 |
+
ETERNAL.NS
|
| 559 |
+
ETHOSLTD.NS
|
| 560 |
+
EUREKAFORB.NS
|
| 561 |
+
EUROTEXIND.NS
|
| 562 |
+
EVEREADY.NS
|
| 563 |
+
EVERESTIND.NS
|
| 564 |
+
EXCEL.NS
|
| 565 |
+
EXCELINDUS.NS
|
| 566 |
+
EXICOM.NS
|
| 567 |
+
EXIDEIND.NS
|
| 568 |
+
EXPLEOSOL.NS
|
| 569 |
+
EXXARO.NS
|
| 570 |
+
FACT.NS
|
| 571 |
+
FAIRCHEMOR.NS
|
| 572 |
+
FAZE3Q.NS
|
| 573 |
+
FCL.NS
|
| 574 |
+
FCSSOFT.NS
|
| 575 |
+
FDC.NS
|
| 576 |
+
FEDERALBNK.NS
|
| 577 |
+
FEDFINA.NS
|
| 578 |
+
FEL.NS
|
| 579 |
+
FELDVR.NS
|
| 580 |
+
FIBERWEB.NS
|
| 581 |
+
FIEMIND.NS
|
| 582 |
+
FILATEX.NS
|
| 583 |
+
FILATFASH.NS
|
| 584 |
+
FINCABLES.NS
|
| 585 |
+
FINEORG.NS
|
| 586 |
+
FINOPB.NS
|
| 587 |
+
FINPIPE.NS
|
| 588 |
+
FIRSTCRY.NS
|
| 589 |
+
FIVESTAR.NS
|
| 590 |
+
FLAIR.NS
|
| 591 |
+
FLEXITUFF.NS
|
| 592 |
+
FLUOROCHEM.NS
|
| 593 |
+
FMGOETZE.NS
|
| 594 |
+
FMNL.NS
|
| 595 |
+
FOCUS.NS
|
| 596 |
+
FOODSIN.NS
|
| 597 |
+
FORCEMOT.NS
|
| 598 |
+
FORTIS.NS
|
| 599 |
+
FOSECOIND.NS
|
| 600 |
+
FSC.NS
|
| 601 |
+
FSL.NS
|
| 602 |
+
FUSION.NS
|
| 603 |
+
GABRIEL.NS
|
| 604 |
+
GAEL.NS
|
| 605 |
+
GAIL.NS
|
| 606 |
+
GALAPREC.NS
|
| 607 |
+
GALAXYSURF.NS
|
| 608 |
+
GALLANTT.NS
|
| 609 |
+
GANDHAR.NS
|
| 610 |
+
GANDHITUBE.NS
|
| 611 |
+
GANECOS.NS
|
| 612 |
+
GANESHBE.NS
|
| 613 |
+
GANESHHOUC.NS
|
| 614 |
+
GANGAFORGE.NS
|
| 615 |
+
GANGESSECU.NS
|
| 616 |
+
GARFIBRES.NS
|
| 617 |
+
GARUDA.NS
|
| 618 |
+
GATDVR-RE.NS
|
| 619 |
+
GATECH.NS
|
| 620 |
+
GATECH-RE1.NS
|
| 621 |
+
GATECHDVR.NS
|
| 622 |
+
GATEWAY.NS
|
| 623 |
+
GAYAHWS.NS
|
| 624 |
+
GEECEE.NS
|
| 625 |
+
GEEKAYWIRE.NS
|
| 626 |
+
GENCON.NS
|
| 627 |
+
GENESYS.NS
|
| 628 |
+
GENSOL.NS
|
| 629 |
+
GENUSPAPER.NS
|
| 630 |
+
GENUSPOWER.NS
|
| 631 |
+
GEOJITFSL.NS
|
| 632 |
+
GEPIL.NS
|
| 633 |
+
GESHIP.NS
|
| 634 |
+
GFLLIMITED.NS
|
| 635 |
+
GHCL.NS
|
| 636 |
+
GHCLTEXTIL.NS
|
| 637 |
+
GICHSGFIN.NS
|
| 638 |
+
GICRE.NS
|
| 639 |
+
GILLANDERS.NS
|
| 640 |
+
GILLETTE.NS
|
| 641 |
+
GINNIFILA.NS
|
| 642 |
+
GIPCL.NS
|
| 643 |
+
GKWLIMITED.NS
|
| 644 |
+
GLAND.NS
|
| 645 |
+
GLAXO.NS
|
| 646 |
+
GLENMARK.NS
|
| 647 |
+
GLFL.NS
|
| 648 |
+
GLOBAL.NS
|
| 649 |
+
GLOBALE.NS
|
| 650 |
+
GLOBALVECT.NS
|
| 651 |
+
GLOBE.NS
|
| 652 |
+
GLOBUSSPR.NS
|
| 653 |
+
GLOSTERLTD.NS
|
| 654 |
+
GMBREW.NS
|
| 655 |
+
GMDCLTD.NS
|
| 656 |
+
GMMPFAUDLR.NS
|
| 657 |
+
GMRAIRPORT.NS
|
| 658 |
+
GMRP&UI.NS
|
| 659 |
+
GNA.NS
|
| 660 |
+
GNFC.NS
|
| 661 |
+
GOACARBON.NS
|
| 662 |
+
GOCLCORP.NS
|
| 663 |
+
GOCOLORS.NS
|
| 664 |
+
GODAVARIB.NS
|
| 665 |
+
GODFRYPHLP.NS
|
| 666 |
+
GODHA.NS
|
| 667 |
+
GODIGIT.NS
|
| 668 |
+
GODREJAGRO.NS
|
| 669 |
+
GODREJCP.NS
|
| 670 |
+
GODREJIND.NS
|
| 671 |
+
GODREJPROP.NS
|
| 672 |
+
GOENKA.NS
|
| 673 |
+
GOKEX.NS
|
| 674 |
+
GOKUL.NS
|
| 675 |
+
GOKULAGRO.NS
|
| 676 |
+
GOLDENTOBC.NS
|
| 677 |
+
GOLDIAM.NS
|
| 678 |
+
GOLDTECH.NS
|
| 679 |
+
GOODLUCK.NS
|
| 680 |
+
GOPAL.NS
|
| 681 |
+
GOYALALUM.NS
|
| 682 |
+
GPIL.NS
|
| 683 |
+
GPPL.NS
|
| 684 |
+
GPTHEALTH.NS
|
| 685 |
+
GPTINFRA.NS
|
| 686 |
+
GRANULES.NS
|
| 687 |
+
GRAPHITE.NS
|
| 688 |
+
GRASIM.NS
|
| 689 |
+
GRAVITA.NS
|
| 690 |
+
GREAVESCOT.NS
|
| 691 |
+
GREENLAM.NS
|
| 692 |
+
GREENPANEL.NS
|
| 693 |
+
GREENPLY.NS
|
| 694 |
+
GREENPOWER.NS
|
| 695 |
+
GRINDWELL.NS
|
| 696 |
+
GRINFRA.NS
|
| 697 |
+
GRMOVER.NS
|
| 698 |
+
GROBTEA.NS
|
| 699 |
+
GRPLTD.NS
|
| 700 |
+
GRSE.NS
|
| 701 |
+
GRWRHITECH.NS
|
| 702 |
+
GSFC.NS
|
| 703 |
+
GSLSU.NS
|
| 704 |
+
GSPL.NS
|
| 705 |
+
GSS.NS
|
| 706 |
+
GTECJAINX.NS
|
| 707 |
+
GTL.NS
|
| 708 |
+
GTLINFRA.NS
|
| 709 |
+
GTPL.NS
|
| 710 |
+
GUFICBIO.NS
|
| 711 |
+
GUJALKALI.NS
|
| 712 |
+
GUJAPOLLO.NS
|
| 713 |
+
GUJGASLTD.NS
|
| 714 |
+
GUJRAFFIA.NS
|
| 715 |
+
GUJTHEM.NS
|
| 716 |
+
GULFOILLUB.NS
|
| 717 |
+
GULFPETRO.NS
|
| 718 |
+
GULPOLY.NS
|
| 719 |
+
GVKPIL.NS
|
| 720 |
+
GVPTECH.NS
|
| 721 |
+
GVT&D.NS
|
| 722 |
+
HAL.NS
|
| 723 |
+
HAPPSTMNDS.NS
|
| 724 |
+
HAPPYFORGE.NS
|
| 725 |
+
HARDWYN.NS
|
| 726 |
+
HARIOMPIPE.NS
|
| 727 |
+
HARRMALAYA.NS
|
| 728 |
+
HARSHA.NS
|
| 729 |
+
HATHWAY.NS
|
| 730 |
+
HATSUN.NS
|
| 731 |
+
HAVELLS.NS
|
| 732 |
+
HAVISHA.NS
|
| 733 |
+
HBLENGINE.NS
|
| 734 |
+
HBSL.NS
|
| 735 |
+
HCC.NS
|
| 736 |
+
HCG.NS
|
| 737 |
+
HCL-INSYS.NS
|
| 738 |
+
HCLTECH.NS
|
| 739 |
+
HDFCAMC.NS
|
| 740 |
+
HDFCBANK.NS
|
| 741 |
+
HDFCLIFE.NS
|
| 742 |
+
HEADSUP.NS
|
| 743 |
+
HECPROJECT.NS
|
| 744 |
+
HEG.NS
|
| 745 |
+
HEIDELBERG.NS
|
| 746 |
+
HEMIPROP.NS
|
| 747 |
+
HERANBA.NS
|
| 748 |
+
HERCULES.NS
|
| 749 |
+
HERITGFOOD.NS
|
| 750 |
+
HEROMOTOCO.NS
|
| 751 |
+
HESTERBIO.NS
|
| 752 |
+
HEUBACHIND.NS
|
| 753 |
+
HEXATRADEX.NS
|
| 754 |
+
HEXT.NS
|
| 755 |
+
HFCL.NS
|
| 756 |
+
HGINFRA.NS
|
| 757 |
+
HGS.NS
|
| 758 |
+
HIKAL.NS
|
| 759 |
+
HILTON.NS
|
| 760 |
+
HIMATSEIDE.NS
|
| 761 |
+
HINDALCO.NS
|
| 762 |
+
HINDCOMPOS.NS
|
| 763 |
+
HINDCON.NS
|
| 764 |
+
HINDCOPPER.NS
|
| 765 |
+
HINDMOTORS.NS
|
| 766 |
+
HINDNATGLS.NS
|
| 767 |
+
HINDOILEXP.NS
|
| 768 |
+
HINDPETRO.NS
|
| 769 |
+
HINDUNILVR.NS
|
| 770 |
+
HINDWAREAP.NS
|
| 771 |
+
HINDZINC.NS
|
| 772 |
+
HIRECT.NS
|
| 773 |
+
HISARMETAL.NS
|
| 774 |
+
HITECH.NS
|
| 775 |
+
HITECHCORP.NS
|
| 776 |
+
HITECHGEAR.NS
|
| 777 |
+
HLEGLAS.NS
|
| 778 |
+
HLVLTD.NS
|
| 779 |
+
HMAAGRO.NS
|
| 780 |
+
HMT.NS
|
| 781 |
+
HMVL.NS
|
| 782 |
+
HNDFDS.NS
|
| 783 |
+
HOMEFIRST.NS
|
| 784 |
+
HONASA.NS
|
| 785 |
+
HONAUT.NS
|
| 786 |
+
HONDAPOWER.NS
|
| 787 |
+
HOVS.NS
|
| 788 |
+
HPAL.NS
|
| 789 |
+
HPIL.NS
|
| 790 |
+
HPL.NS
|
| 791 |
+
HSCL.NS
|
| 792 |
+
HTMEDIA.NS
|
| 793 |
+
HUBTOWN.NS
|
| 794 |
+
HUDCO.NS
|
| 795 |
+
HUHTAMAKI.NS
|
| 796 |
+
HYBRIDFIN.NS
|
| 797 |
+
HYUNDAI.NS
|
| 798 |
+
ICDSLTD.NS
|
| 799 |
+
ICEMAKE.NS
|
| 800 |
+
ICICIBANK.NS
|
| 801 |
+
ICICIGI.NS
|
| 802 |
+
ICICIPRULI.NS
|
| 803 |
+
ICIL.NS
|
| 804 |
+
ICRA.NS
|
| 805 |
+
IDBI.NS
|
| 806 |
+
IDEA.NS
|
| 807 |
+
IDEAFORGE.NS
|
| 808 |
+
IDFCFIRSTB.NS
|
| 809 |
+
IEL.NS
|
| 810 |
+
IEX.NS
|
| 811 |
+
IFBAGRO.NS
|
| 812 |
+
IFBIND.NS
|
| 813 |
+
IFCI.NS
|
| 814 |
+
IFGLEXPOR.NS
|
| 815 |
+
IGARASHI.NS
|
| 816 |
+
IGIL.NS
|
| 817 |
+
IGL.NS
|
| 818 |
+
IGPL.NS
|
| 819 |
+
IIFL.NS
|
| 820 |
+
IIFLCAPS.NS
|
| 821 |
+
IITL.NS
|
| 822 |
+
IKIO.NS
|
| 823 |
+
IKS.NS
|
| 824 |
+
IL&FSENGG.NS
|
| 825 |
+
IL&FSTRANS.NS
|
| 826 |
+
IMAGICAA.NS
|
| 827 |
+
IMFA.NS
|
| 828 |
+
IMPAL.NS
|
| 829 |
+
IMPEXFERRO.NS
|
| 830 |
+
INCREDIBLE.NS
|
| 831 |
+
INDBANK.NS
|
| 832 |
+
INDGN.NS
|
| 833 |
+
INDHOTEL.NS
|
| 834 |
+
INDIACEM.NS
|
| 835 |
+
INDIAGLYCO.NS
|
| 836 |
+
INDIAMART.NS
|
| 837 |
+
INDIANB.NS
|
| 838 |
+
INDIANCARD.NS
|
| 839 |
+
INDIANHUME.NS
|
| 840 |
+
INDIASHLTR.NS
|
| 841 |
+
INDIGO.NS
|
| 842 |
+
INDIGOPNTS.NS
|
| 843 |
+
INDNIPPON.NS
|
| 844 |
+
INDOAMIN.NS
|
| 845 |
+
INDOBORAX.NS
|
| 846 |
+
INDOCO.NS
|
| 847 |
+
INDOFARM.NS
|
| 848 |
+
INDORAMA.NS
|
| 849 |
+
INDOSTAR.NS
|
| 850 |
+
INDOTECH.NS
|
| 851 |
+
INDOTHAI.NS
|
| 852 |
+
INDOUS.NS
|
| 853 |
+
INDOWIND.NS
|
| 854 |
+
INDRAMEDCO.NS
|
| 855 |
+
INDSWFTLAB.NS
|
| 856 |
+
INDSWFTLTD.NS
|
| 857 |
+
INDTERRAIN.NS
|
| 858 |
+
INDUSINDBK.NS
|
| 859 |
+
INDUSTOWER.NS
|
| 860 |
+
INFIBEAM.NS
|
| 861 |
+
INFOBEAN.NS
|
| 862 |
+
INFOMEDIA.NS
|
| 863 |
+
INFY.NS
|
| 864 |
+
INGERRAND.NS
|
| 865 |
+
INNOVACAP.NS
|
| 866 |
+
INNOVANA.NS
|
| 867 |
+
INOXGREEN.NS
|
| 868 |
+
INOXINDIA.NS
|
| 869 |
+
INOXWIND.NS
|
| 870 |
+
INSECTICID.NS
|
| 871 |
+
INSPIRISYS.NS
|
| 872 |
+
INTELLECT.NS
|
| 873 |
+
INTENTECH.NS
|
| 874 |
+
INTERARCH.NS
|
| 875 |
+
INTLCONV.NS
|
| 876 |
+
INVENTURE.NS
|
| 877 |
+
IOB.NS
|
| 878 |
+
IOC.NS
|
| 879 |
+
IOLCP.NS
|
| 880 |
+
IONEXCHANG.NS
|
| 881 |
+
IPCALAB.NS
|
| 882 |
+
IPL.NS
|
| 883 |
+
IRB.NS
|
| 884 |
+
IRCON.NS
|
| 885 |
+
IRCTC.NS
|
| 886 |
+
IREDA.NS
|
| 887 |
+
IRFC.NS
|
| 888 |
+
IRIS.NS
|
| 889 |
+
IRISDOREME.NS
|
| 890 |
+
IRMENERGY.NS
|
| 891 |
+
ISFT.NS
|
| 892 |
+
ISGEC.NS
|
| 893 |
+
ISHANCH.NS
|
| 894 |
+
ITC.NS
|
| 895 |
+
ITCHOTELS.NS
|
| 896 |
+
ITDC.NS
|
| 897 |
+
ITDCEM.NS
|
| 898 |
+
ITI.NS
|
| 899 |
+
IVC.NS
|
| 900 |
+
IVP.NS
|
| 901 |
+
IWEL.NS
|
| 902 |
+
IXIGO.NS
|
| 903 |
+
IZMO.NS
|
| 904 |
+
J&KBANK.NS
|
| 905 |
+
JAGRAN.NS
|
| 906 |
+
JAGSNPHARM.NS
|
| 907 |
+
JAIBALAJI.NS
|
| 908 |
+
JAICORPLTD.NS
|
| 909 |
+
JAIPURKURT.NS
|
| 910 |
+
JAMNAAUTO.NS
|
| 911 |
+
JASH.NS
|
| 912 |
+
JAYAGROGN.NS
|
| 913 |
+
JAYBARMARU.NS
|
| 914 |
+
JAYNECOIND.NS
|
| 915 |
+
JAYSREETEA.NS
|
| 916 |
+
JBCHEPHARM.NS
|
| 917 |
+
JBMA.NS
|
| 918 |
+
JCHAC.NS
|
| 919 |
+
JETFREIGHT.NS
|
| 920 |
+
JGCHEM.NS
|
| 921 |
+
JHS.NS
|
| 922 |
+
JINDALPHOT.NS
|
| 923 |
+
JINDALPOLY.NS
|
| 924 |
+
JINDALSAW.NS
|
| 925 |
+
JINDALSTEL.NS
|
| 926 |
+
JINDRILL.NS
|
| 927 |
+
JINDWORLD.NS
|
| 928 |
+
JIOFIN.NS
|
| 929 |
+
JISLDVREQS.NS
|
| 930 |
+
JISLJALEQS.NS
|
| 931 |
+
JITFINFRA.NS
|
| 932 |
+
JKCEMENT.NS
|
| 933 |
+
JKIL.NS
|
| 934 |
+
JKLAKSHMI.NS
|
| 935 |
+
JKPAPER.NS
|
| 936 |
+
JKTYRE.NS
|
| 937 |
+
JLHL.NS
|
| 938 |
+
JMA.NS
|
| 939 |
+
JMFINANCIL.NS
|
| 940 |
+
JNKINDIA.NS
|
| 941 |
+
JOCIL.NS
|
| 942 |
+
JPOLYINVST.NS
|
| 943 |
+
JPPOWER.NS
|
| 944 |
+
JSFB.NS
|
| 945 |
+
JSL.NS
|
| 946 |
+
JSWENERGY.NS
|
| 947 |
+
JSWHL.NS
|
| 948 |
+
JSWINFRA.NS
|
| 949 |
+
JSWSTEEL.NS
|
| 950 |
+
JTEKTINDIA.NS
|
| 951 |
+
JTLIND.NS
|
| 952 |
+
JUBLCPL.NS
|
| 953 |
+
JUBLFOOD.NS
|
| 954 |
+
JUBLINGREA.NS
|
| 955 |
+
JUBLPHARMA.NS
|
| 956 |
+
JUNIPER.NS
|
| 957 |
+
JUSTDIAL.NS
|
| 958 |
+
JWL.NS
|
| 959 |
+
JYOTHYLAB.NS
|
| 960 |
+
JYOTICNC.NS
|
| 961 |
+
JYOTISTRUC.NS
|
| 962 |
+
KABRAEXTRU.NS
|
| 963 |
+
KAJARIACER.NS
|
| 964 |
+
KAKATCEM.NS
|
| 965 |
+
KALAMANDIR.NS
|
| 966 |
+
KALYANI.NS
|
| 967 |
+
KALYANIFRG.NS
|
| 968 |
+
KALYANKJIL.NS
|
| 969 |
+
KAMATHOTEL.NS
|
| 970 |
+
KAMDHENU.NS
|
| 971 |
+
KAMOPAINTS.NS
|
| 972 |
+
KANANIIND.NS
|
| 973 |
+
KANORICHEM.NS
|
| 974 |
+
KANPRPLA.NS
|
| 975 |
+
KANSAINER.NS
|
| 976 |
+
KAPSTON.NS
|
| 977 |
+
KARMAENG.NS
|
| 978 |
+
KARURVYSYA.NS
|
| 979 |
+
KAUSHALYA.NS
|
| 980 |
+
KAVVERITEL.NS
|
| 981 |
+
KAYA.NS
|
| 982 |
+
KAYNES.NS
|
| 983 |
+
KBCGLOBAL.NS
|
| 984 |
+
KCP.NS
|
| 985 |
+
KCPSUGIND.NS
|
| 986 |
+
KDDL.NS
|
| 987 |
+
KEC.NS
|
| 988 |
+
KECL.NS
|
| 989 |
+
KEEPLEARN.NS
|
| 990 |
+
KEI.NS
|
| 991 |
+
KELLTONTEC.NS
|
| 992 |
+
KERNEX.NS
|
| 993 |
+
KESORAMIND.NS
|
| 994 |
+
KEYFINSERV.NS
|
| 995 |
+
KFINTECH.NS
|
| 996 |
+
KHADIM.NS
|
| 997 |
+
KHAICHEM.NS
|
| 998 |
+
KHAITANLTD.NS
|
| 999 |
+
KHANDSE.NS
|
| 1000 |
+
KICL.NS
|
| 1001 |
+
KILITCH.NS
|
| 1002 |
+
KIMS.NS
|
| 1003 |
+
KINGFA.NS
|
| 1004 |
+
KIOCL.NS
|
| 1005 |
+
KIRIINDUS.NS
|
| 1006 |
+
KIRLOSBROS.NS
|
| 1007 |
+
KIRLOSENG.NS
|
| 1008 |
+
KIRLOSIND.NS
|
| 1009 |
+
KIRLPNU.NS
|
| 1010 |
+
KITEX.NS
|
| 1011 |
+
KKCL.NS
|
| 1012 |
+
KMEW.NS
|
| 1013 |
+
KMSUGAR.NS
|
| 1014 |
+
KNRCON.NS
|
| 1015 |
+
KOHINOOR.NS
|
| 1016 |
+
KOKUYOCMLN.NS
|
| 1017 |
+
KOLTEPATIL.NS
|
| 1018 |
+
KOPRAN.NS
|
| 1019 |
+
KOTAKBANK.NS
|
| 1020 |
+
KOTARISUG.NS
|
| 1021 |
+
KOTHARIPET.NS
|
| 1022 |
+
KOTHARIPRO.NS
|
| 1023 |
+
KPEL.NS
|
| 1024 |
+
KPIGREEN.NS
|
| 1025 |
+
KPIL.NS
|
| 1026 |
+
KPITTECH.NS
|
| 1027 |
+
KPRMILL.NS
|
| 1028 |
+
KRBL.NS
|
| 1029 |
+
KREBSBIO.NS
|
| 1030 |
+
KRIDHANINF.NS
|
| 1031 |
+
KRISHANA.NS
|
| 1032 |
+
KRITI.NS
|
| 1033 |
+
KRITIKA.NS
|
| 1034 |
+
KRITINUT.NS
|
| 1035 |
+
KRN.NS
|
| 1036 |
+
KRONOX.NS
|
| 1037 |
+
KROSS.NS
|
| 1038 |
+
KRSNAA.NS
|
| 1039 |
+
KRYSTAL.NS
|
| 1040 |
+
KSB.NS
|
| 1041 |
+
KSCL.NS
|
| 1042 |
+
KSHITIJPOL.NS
|
| 1043 |
+
KSL.NS
|
| 1044 |
+
KSOLVES.NS
|
| 1045 |
+
KTKBANK.NS
|
| 1046 |
+
KUANTUM.NS
|
| 1047 |
+
LAGNAM.NS
|
| 1048 |
+
LAKPRE.NS
|
| 1049 |
+
LAL.NS
|
| 1050 |
+
LALPATHLAB.NS
|
| 1051 |
+
LAMBODHARA.NS
|
| 1052 |
+
LANCORHOL.NS
|
| 1053 |
+
LANDMARK.NS
|
| 1054 |
+
LAOPALA.NS
|
| 1055 |
+
LASA.NS
|
| 1056 |
+
LATENTVIEW.NS
|
| 1057 |
+
LATTEYS.NS
|
| 1058 |
+
LAURUSLABS.NS
|
| 1059 |
+
LAXMICOT.NS
|
| 1060 |
+
LAXMIDENTL.NS
|
| 1061 |
+
LCCINFOTEC.NS
|
| 1062 |
+
LEMONTREE.NS
|
| 1063 |
+
LEXUS.NS
|
| 1064 |
+
LFIC.NS
|
| 1065 |
+
LGBBROSLTD.NS
|
| 1066 |
+
LGHL.NS
|
| 1067 |
+
LIBAS.NS
|
| 1068 |
+
LIBERTSHOE.NS
|
| 1069 |
+
LICHSGFIN.NS
|
| 1070 |
+
LICI.NS
|
| 1071 |
+
LIKHITHA.NS
|
| 1072 |
+
LINC.NS
|
| 1073 |
+
LINCOLN.NS
|
| 1074 |
+
LINDEINDIA.NS
|
| 1075 |
+
LLOYDS-RE1.NS
|
| 1076 |
+
LLOYDSENGG.NS
|
| 1077 |
+
LLOYDSENT.NS
|
| 1078 |
+
LLOYDSME.NS
|
| 1079 |
+
LMW.NS
|
| 1080 |
+
LODHA.NS
|
| 1081 |
+
LOKESHMACH.NS
|
| 1082 |
+
LORDSCHLO.NS
|
| 1083 |
+
LOTUSEYE.NS
|
| 1084 |
+
LOVABLE.NS
|
| 1085 |
+
LOYALTEX.NS
|
| 1086 |
+
LPDC.NS
|
| 1087 |
+
LT.NS
|
| 1088 |
+
LTF.NS
|
| 1089 |
+
LTFOODS.NS
|
| 1090 |
+
LTIM.NS
|
| 1091 |
+
LTTS.NS
|
| 1092 |
+
LUMAXIND.NS
|
| 1093 |
+
LUMAXTECH.NS
|
| 1094 |
+
LUPIN.NS
|
| 1095 |
+
LUXIND.NS
|
| 1096 |
+
LXCHEM.NS
|
| 1097 |
+
LYKALABS.NS
|
| 1098 |
+
LYPSAGEMS.NS
|
| 1099 |
+
M&M.NS
|
| 1100 |
+
M&MFIN.NS
|
| 1101 |
+
MAANALU.NS
|
| 1102 |
+
MACPOWER.NS
|
| 1103 |
+
MADHAV.NS
|
| 1104 |
+
MADHUCON.NS
|
| 1105 |
+
MADRASFERT.NS
|
| 1106 |
+
MAGADSUGAR.NS
|
| 1107 |
+
MAGNUM.NS
|
| 1108 |
+
MAHABANK.NS
|
| 1109 |
+
MAHAPEXLTD.NS
|
| 1110 |
+
MAHASTEEL.NS
|
| 1111 |
+
MAHEPC.NS
|
| 1112 |
+
MAHESHWARI.NS
|
| 1113 |
+
MAHLIFE.NS
|
| 1114 |
+
MAHLOG.NS
|
| 1115 |
+
MAHSCOOTER.NS
|
| 1116 |
+
MAHSEAMLES.NS
|
| 1117 |
+
MAITHANALL.NS
|
| 1118 |
+
MALLCOM.NS
|
| 1119 |
+
MALUPAPER.NS
|
| 1120 |
+
MAMATA.NS
|
| 1121 |
+
MANAKALUCO.NS
|
| 1122 |
+
MANAKCOAT.NS
|
| 1123 |
+
MANAKSIA.NS
|
| 1124 |
+
MANAKSTEEL.NS
|
| 1125 |
+
MANALIPETC.NS
|
| 1126 |
+
MANAPPURAM.NS
|
| 1127 |
+
MANBA.NS
|
| 1128 |
+
MANCREDIT.NS
|
| 1129 |
+
MANGALAM.NS
|
| 1130 |
+
MANGCHEFER.NS
|
| 1131 |
+
MANGLMCEM.NS
|
| 1132 |
+
MANINDS.NS
|
| 1133 |
+
MANINFRA.NS
|
| 1134 |
+
MANKIND.NS
|
| 1135 |
+
MANOMAY.NS
|
| 1136 |
+
MANORAMA.NS
|
| 1137 |
+
MANORG.NS
|
| 1138 |
+
MANUGRAPH.NS
|
| 1139 |
+
MANYAVAR.NS
|
| 1140 |
+
MAPMYINDIA.NS
|
| 1141 |
+
MARALOVER.NS
|
| 1142 |
+
MARATHON.NS
|
| 1143 |
+
MARICO.NS
|
| 1144 |
+
MARINE.NS
|
| 1145 |
+
MARKSANS.NS
|
| 1146 |
+
MARSHALL.NS
|
| 1147 |
+
MARUTI.NS
|
| 1148 |
+
MASFIN.NS
|
| 1149 |
+
MASKINVEST.NS
|
| 1150 |
+
MASTEK.NS
|
| 1151 |
+
MASTERTR.NS
|
| 1152 |
+
MATRIMONY.NS
|
| 1153 |
+
MAWANASUG.NS
|
| 1154 |
+
MAXESTATES.NS
|
| 1155 |
+
MAXHEALTH.NS
|
| 1156 |
+
MAXIND.NS
|
| 1157 |
+
MAXIND-RE.NS
|
| 1158 |
+
MAYURUNIQ.NS
|
| 1159 |
+
MAZDA.NS
|
| 1160 |
+
MAZDOCK.NS
|
| 1161 |
+
MBAPL.NS
|
| 1162 |
+
MBLINFRA.NS
|
| 1163 |
+
MCL.NS
|
| 1164 |
+
MCLEODRUSS.NS
|
| 1165 |
+
MCLOUD.NS
|
| 1166 |
+
MCX.NS
|
| 1167 |
+
MEDANTA.NS
|
| 1168 |
+
MEDIASSIST.NS
|
| 1169 |
+
MEDICAMEQ.NS
|
| 1170 |
+
MEDICO.NS
|
| 1171 |
+
MEDPLUS.NS
|
| 1172 |
+
MEGASOFT.NS
|
| 1173 |
+
MEGASTAR.NS
|
| 1174 |
+
MENONBE.NS
|
| 1175 |
+
MEP.NS
|
| 1176 |
+
METROBRAND.NS
|
| 1177 |
+
METROPOLIS.NS
|
| 1178 |
+
MFML.NS
|
| 1179 |
+
MFSL.NS
|
| 1180 |
+
MGEL.NS
|
| 1181 |
+
MGL.NS
|
| 1182 |
+
MHLXMIRU.NS
|
| 1183 |
+
MHRIL.NS
|
| 1184 |
+
MICEL.NS
|
| 1185 |
+
MIDHANI.NS
|
| 1186 |
+
MINDACORP.NS
|
| 1187 |
+
MINDTECK.NS
|
| 1188 |
+
MIRCELECTR.NS
|
| 1189 |
+
MIRZAINT.NS
|
| 1190 |
+
MITCON.NS
|
| 1191 |
+
MITTAL.NS
|
| 1192 |
+
MKPL.NS
|
| 1193 |
+
MMFL.NS
|
| 1194 |
+
MMP.NS
|
| 1195 |
+
MMTC.NS
|
| 1196 |
+
MOBIKWIK.NS
|
| 1197 |
+
MODIRUBBER.NS
|
| 1198 |
+
MODISONLTD.NS
|
| 1199 |
+
MODTHREAD.NS
|
| 1200 |
+
MOHITIND.NS
|
| 1201 |
+
MOIL.NS
|
| 1202 |
+
MOKSH.NS
|
| 1203 |
+
MOL.NS
|
| 1204 |
+
MOLDTECH.NS
|
| 1205 |
+
MOLDTKPAC.NS
|
| 1206 |
+
MONARCH.NS
|
| 1207 |
+
MONTECARLO.NS
|
| 1208 |
+
MOREPENLAB.NS
|
| 1209 |
+
MOSCHIP.NS
|
| 1210 |
+
MOTHERSON.NS
|
| 1211 |
+
MOTILALOFS.NS
|
| 1212 |
+
MOTISONS.NS
|
| 1213 |
+
MOTOGENFIN.NS
|
| 1214 |
+
MPHASIS.NS
|
| 1215 |
+
MPSLTD.NS
|
| 1216 |
+
MRF.NS
|
| 1217 |
+
MRPL.NS
|
| 1218 |
+
MSPL.NS
|
| 1219 |
+
MSTCLTD.NS
|
| 1220 |
+
MSUMI.NS
|
| 1221 |
+
MTARTECH.NS
|
| 1222 |
+
MTEDUCARE.NS
|
| 1223 |
+
MTNL.NS
|
| 1224 |
+
MUFIN.NS
|
| 1225 |
+
MUFTI.NS
|
| 1226 |
+
MUKANDLTD.NS
|
| 1227 |
+
MUKKA.NS
|
| 1228 |
+
MUKTAARTS.NS
|
| 1229 |
+
MUNJALAU.NS
|
| 1230 |
+
MUNJALSHOW.NS
|
| 1231 |
+
MURUDCERA.NS
|
| 1232 |
+
MUTHOOTCAP.NS
|
| 1233 |
+
MUTHOOTFIN.NS
|
| 1234 |
+
MUTHOOTMF.NS
|
| 1235 |
+
MVGJL.NS
|
| 1236 |
+
NACLIND.NS
|
| 1237 |
+
NAGAFERT.NS
|
| 1238 |
+
NAGREEKCAP.NS
|
| 1239 |
+
NAGREEKEXP.NS
|
| 1240 |
+
NAHARCAP.NS
|
| 1241 |
+
NAHARINDUS.NS
|
| 1242 |
+
NAHARPOLY.NS
|
| 1243 |
+
NAHARSPING.NS
|
| 1244 |
+
NAM-INDIA.NS
|
| 1245 |
+
NARMADA.NS
|
| 1246 |
+
NATCAPSUQ.NS
|
| 1247 |
+
NATCOPHARM.NS
|
| 1248 |
+
NATHBIOGEN.NS
|
| 1249 |
+
NATIONALUM.NS
|
| 1250 |
+
NAUKRI.NS
|
| 1251 |
+
NAVA.NS
|
| 1252 |
+
NAVINFLUOR.NS
|
| 1253 |
+
NAVKARCORP.NS
|
| 1254 |
+
NAVKARURB.NS
|
| 1255 |
+
NAVNETEDUL.NS
|
| 1256 |
+
NAZARA.NS
|
| 1257 |
+
NBCC.NS
|
| 1258 |
+
NBIFIN.NS
|
| 1259 |
+
NCC.NS
|
| 1260 |
+
NCLIND.NS
|
| 1261 |
+
NDGL.NS
|
| 1262 |
+
NDL.NS
|
| 1263 |
+
NDLVENTURE.NS
|
| 1264 |
+
NDRAUTO.NS
|
| 1265 |
+
NDTV.NS
|
| 1266 |
+
NECCLTD.NS
|
| 1267 |
+
NECLIFE.NS
|
| 1268 |
+
NELCAST.NS
|
| 1269 |
+
NELCO.NS
|
| 1270 |
+
NEOGEN.NS
|
| 1271 |
+
NESCO.NS
|
| 1272 |
+
NESTLEIND.NS
|
| 1273 |
+
NETWEB.NS
|
| 1274 |
+
NETWORK18.NS
|
| 1275 |
+
NEULANDLAB.NS
|
| 1276 |
+
NEWGEN.NS
|
| 1277 |
+
NEXTMEDIA.NS
|
| 1278 |
+
NFL.NS
|
| 1279 |
+
NGIL.NS
|
| 1280 |
+
NGLFINE.NS
|
| 1281 |
+
NH.NS
|
| 1282 |
+
NHPC.NS
|
| 1283 |
+
NIACL.NS
|
| 1284 |
+
NIBE.NS
|
| 1285 |
+
NIBL.NS
|
| 1286 |
+
NIITLTD.NS
|
| 1287 |
+
NIITMTS.NS
|
| 1288 |
+
NILAINFRA.NS
|
| 1289 |
+
NILASPACES.NS
|
| 1290 |
+
NILKAMAL.NS
|
| 1291 |
+
NINSYS.NS
|
| 1292 |
+
NIPPOBATRY.NS
|
| 1293 |
+
NIRAJ.NS
|
| 1294 |
+
NIRAJISPAT.NS
|
| 1295 |
+
NITCO.NS
|
| 1296 |
+
NITINSPIN.NS
|
| 1297 |
+
NITIRAJ.NS
|
| 1298 |
+
NIVABUPA.NS
|
| 1299 |
+
NKIND.NS
|
| 1300 |
+
NLCINDIA.NS
|
| 1301 |
+
NMDC.NS
|
| 1302 |
+
NOCIL.NS
|
| 1303 |
+
NOIDATOLL.NS
|
| 1304 |
+
NORBTEAEXP.NS
|
| 1305 |
+
NORTHARC.NS
|
| 1306 |
+
NOVAAGRI.NS
|
| 1307 |
+
NPST.NS
|
| 1308 |
+
NRAIL.NS
|
| 1309 |
+
NRBBEARING.NS
|
| 1310 |
+
NRL.NS
|
| 1311 |
+
NSIL.NS
|
| 1312 |
+
NSLNISP.NS
|
| 1313 |
+
NTPC.NS
|
| 1314 |
+
NTPCGREEN.NS
|
| 1315 |
+
NUCLEUS.NS
|
| 1316 |
+
NURECA.NS
|
| 1317 |
+
NUVAMA.NS
|
| 1318 |
+
NUVOCO.NS
|
| 1319 |
+
NYKAA.NS
|
| 1320 |
+
OAL.NS
|
| 1321 |
+
OBCL.NS
|
| 1322 |
+
OBEROIRLTY.NS
|
| 1323 |
+
OCCL.NS
|
| 1324 |
+
OCCLLTD.NS
|
| 1325 |
+
ODIGMA.NS
|
| 1326 |
+
OFSS.NS
|
| 1327 |
+
OIL.NS
|
| 1328 |
+
OILCOUNTUB.NS
|
| 1329 |
+
OLAELEC.NS
|
| 1330 |
+
OLECTRA.NS
|
| 1331 |
+
OMAXAUTO.NS
|
| 1332 |
+
OMAXE.NS
|
| 1333 |
+
OMINFRAL.NS
|
| 1334 |
+
OMKARCHEM.NS
|
| 1335 |
+
ONELIFECAP.NS
|
| 1336 |
+
ONEPOINT.NS
|
| 1337 |
+
ONESOURCE.NS
|
| 1338 |
+
ONGC.NS
|
| 1339 |
+
ONMOBILE.NS
|
| 1340 |
+
ONWARDTEC.NS
|
| 1341 |
+
OPTIEMUS.NS
|
| 1342 |
+
ORBTEXP.NS
|
| 1343 |
+
ORCHASP.NS
|
| 1344 |
+
ORCHPHARMA.NS
|
| 1345 |
+
ORICONENT.NS
|
| 1346 |
+
ORIENTALTL.NS
|
| 1347 |
+
ORIENTBELL.NS
|
| 1348 |
+
ORIENTCEM.NS
|
| 1349 |
+
ORIENTCER.NS
|
| 1350 |
+
ORIENTELEC.NS
|
| 1351 |
+
ORIENTHOT.NS
|
| 1352 |
+
ORIENTLTD.NS
|
| 1353 |
+
ORIENTPPR.NS
|
| 1354 |
+
ORIENTTECH.NS
|
| 1355 |
+
ORISSAMINE.NS
|
| 1356 |
+
ORTEL.NS
|
| 1357 |
+
ORTINGLOBE.NS
|
| 1358 |
+
OSIAHYPER.NS
|
| 1359 |
+
OSWALAGRO.NS
|
| 1360 |
+
OSWALGREEN.NS
|
| 1361 |
+
OSWALSEEDS.NS
|
| 1362 |
+
PAGEIND.NS
|
| 1363 |
+
PAISALO.NS
|
| 1364 |
+
PAKKA.NS
|
| 1365 |
+
PALASHSECU.NS
|
| 1366 |
+
PALREDTEC.NS
|
| 1367 |
+
PANACEABIO.NS
|
| 1368 |
+
PANACHE.NS
|
| 1369 |
+
PANAMAPET.NS
|
| 1370 |
+
PANSARI.NS
|
| 1371 |
+
PAR.NS
|
| 1372 |
+
PARACABLES.NS
|
| 1373 |
+
PARADEEP.NS
|
| 1374 |
+
PARAGMILK.NS
|
| 1375 |
+
PARAS.NS
|
| 1376 |
+
PARASPETRO.NS
|
| 1377 |
+
PARKHOTELS.NS
|
| 1378 |
+
PARSVNATH.NS
|
| 1379 |
+
PASUPTAC.NS
|
| 1380 |
+
PATANJALI.NS
|
| 1381 |
+
PATELENG.NS
|
| 1382 |
+
PATINTLOG.NS
|
| 1383 |
+
PAVNAIND.NS
|
| 1384 |
+
PAYTM.NS
|
| 1385 |
+
PCBL.NS
|
| 1386 |
+
PCJEWELLER.NS
|
| 1387 |
+
PDMJEPAPER.NS
|
| 1388 |
+
PDSL.NS
|
| 1389 |
+
PEARLPOLY.NS
|
| 1390 |
+
PEL.NS
|
| 1391 |
+
PENIND.NS
|
| 1392 |
+
PENINLAND.NS
|
| 1393 |
+
PERSISTENT.NS
|
| 1394 |
+
PETRONET.NS
|
| 1395 |
+
PFC.NS
|
| 1396 |
+
PFIZER.NS
|
| 1397 |
+
PFOCUS.NS
|
| 1398 |
+
PFS.NS
|
| 1399 |
+
PGEL.NS
|
| 1400 |
+
PGHH.NS
|
| 1401 |
+
PGHL.NS
|
| 1402 |
+
PGIL.NS
|
| 1403 |
+
PHOENIXLTD.NS
|
| 1404 |
+
PIDILITIND.NS
|
| 1405 |
+
PIGL.NS
|
| 1406 |
+
PIIND.NS
|
| 1407 |
+
PILANIINVS.NS
|
| 1408 |
+
PILITA.NS
|
| 1409 |
+
PIONEEREMB.NS
|
| 1410 |
+
PITTIENG.NS
|
| 1411 |
+
PIXTRANS.NS
|
| 1412 |
+
PKTEA.NS
|
| 1413 |
+
PLASTIBLEN.NS
|
| 1414 |
+
PLATIND.NS
|
| 1415 |
+
PLAZACABLE.NS
|
| 1416 |
+
PNB.NS
|
| 1417 |
+
PNBGILTS.NS
|
| 1418 |
+
PNBHOUSING.NS
|
| 1419 |
+
PNC.NS
|
| 1420 |
+
PNCINFRA.NS
|
| 1421 |
+
PNGJL.NS
|
| 1422 |
+
POCL.NS
|
| 1423 |
+
PODDARMENT.NS
|
| 1424 |
+
POKARNA.NS
|
| 1425 |
+
POLICYBZR.NS
|
| 1426 |
+
POLYCAB.NS
|
| 1427 |
+
POLYMED.NS
|
| 1428 |
+
POLYPLEX.NS
|
| 1429 |
+
PONNIERODE.NS
|
| 1430 |
+
POONAWALLA.NS
|
| 1431 |
+
POWERGRID.NS
|
| 1432 |
+
POWERINDIA.NS
|
| 1433 |
+
POWERMECH.NS
|
| 1434 |
+
PPAP.NS
|
| 1435 |
+
PPL.NS
|
| 1436 |
+
PPLPHARMA.NS
|
| 1437 |
+
PRABHA.NS
|
| 1438 |
+
PRAENG.NS
|
| 1439 |
+
PRAJIND.NS
|
| 1440 |
+
PRAKASH.NS
|
| 1441 |
+
PRAKASHSTL.NS
|
| 1442 |
+
PRAXIS.NS
|
| 1443 |
+
PRECAM.NS
|
| 1444 |
+
PRECOT.NS
|
| 1445 |
+
PRECWIRE.NS
|
| 1446 |
+
PREMEXPLN.NS
|
| 1447 |
+
PREMIER.NS
|
| 1448 |
+
PREMIERENE.NS
|
| 1449 |
+
PREMIERPOL.NS
|
| 1450 |
+
PRESTIGE.NS
|
| 1451 |
+
PRICOLLTD.NS
|
| 1452 |
+
PRIMESECU.NS
|
| 1453 |
+
PRIMO.NS
|
| 1454 |
+
PRINCEPIPE.NS
|
| 1455 |
+
PRITI.NS
|
| 1456 |
+
PRITIKAUTO.NS
|
| 1457 |
+
PRIVISCL.NS
|
| 1458 |
+
PROTEAN.NS
|
| 1459 |
+
PROZONER.NS
|
| 1460 |
+
PRSMJOHNSN.NS
|
| 1461 |
+
PRUDENT.NS
|
| 1462 |
+
PRUDMOULI.NS
|
| 1463 |
+
PSB.NS
|
| 1464 |
+
PSPPROJECT.NS
|
| 1465 |
+
PTC.NS
|
| 1466 |
+
PTCIL.NS
|
| 1467 |
+
PTL.NS
|
| 1468 |
+
PUNJABCHEM.NS
|
| 1469 |
+
PURVA.NS
|
| 1470 |
+
PVP.NS
|
| 1471 |
+
PVRINOX.NS
|
| 1472 |
+
PVSL.NS
|
| 1473 |
+
PYRAMID.NS
|
| 1474 |
+
QPOWER.NS
|
| 1475 |
+
QUADFUTURE.NS
|
| 1476 |
+
QUESS.NS
|
| 1477 |
+
QUICKHEAL.NS
|
| 1478 |
+
RACE.NS
|
| 1479 |
+
RACLGEAR.NS
|
| 1480 |
+
RADAAN.NS
|
| 1481 |
+
RADHIKAJWE.NS
|
| 1482 |
+
RADIANTCMS.NS
|
| 1483 |
+
RADICO.NS
|
| 1484 |
+
RADIOCITY.NS
|
| 1485 |
+
RAILTEL.NS
|
| 1486 |
+
RAIN.NS
|
| 1487 |
+
RAINBOW.NS
|
| 1488 |
+
RAJESHEXPO.NS
|
| 1489 |
+
RAJMET.NS
|
| 1490 |
+
RAJRATAN.NS
|
| 1491 |
+
RAJRILTD.NS
|
| 1492 |
+
RAJSREESUG.NS
|
| 1493 |
+
RAJTV.NS
|
| 1494 |
+
RALLIS.NS
|
| 1495 |
+
RAMANEWS.NS
|
| 1496 |
+
RAMAPHO.NS
|
| 1497 |
+
RAMASTEEL.NS
|
| 1498 |
+
RAMCOCEM.NS
|
| 1499 |
+
RAMCOIND.NS
|
| 1500 |
+
RAMCOSYS.NS
|
| 1501 |
+
RAMKY.NS
|
| 1502 |
+
RAMRAT.NS
|
| 1503 |
+
RANASUG.NS
|
| 1504 |
+
RANEHOLDIN.NS
|
| 1505 |
+
RATEGAIN.NS
|
| 1506 |
+
RATNAMANI.NS
|
| 1507 |
+
RATNAVEER.NS
|
| 1508 |
+
RAYMOND.NS
|
| 1509 |
+
RAYMONDLSL.NS
|
| 1510 |
+
RBA.NS
|
| 1511 |
+
RBLBANK.NS
|
| 1512 |
+
RBZJEWEL.NS
|
| 1513 |
+
RCF.NS
|
| 1514 |
+
RCOM.NS
|
| 1515 |
+
RECLTD.NS
|
| 1516 |
+
REDINGTON.NS
|
| 1517 |
+
REDTAPE.NS
|
| 1518 |
+
REFEX.NS
|
| 1519 |
+
REGENCERAM.NS
|
| 1520 |
+
RELAXO.NS
|
| 1521 |
+
RELCHEMQ.NS
|
| 1522 |
+
RELIABLE.NS
|
| 1523 |
+
RELIANCE.NS
|
| 1524 |
+
RELIGARE.NS
|
| 1525 |
+
RELINFRA.NS
|
| 1526 |
+
RELTD.NS
|
| 1527 |
+
REMSONSIND.NS
|
| 1528 |
+
RENUKA.NS
|
| 1529 |
+
REPCOHOME.NS
|
| 1530 |
+
REPL.NS
|
| 1531 |
+
REPRO.NS
|
| 1532 |
+
RESPONIND.NS
|
| 1533 |
+
RETAIL.NS
|
| 1534 |
+
RGL.NS
|
| 1535 |
+
RHFL.NS
|
| 1536 |
+
RHIM.NS
|
| 1537 |
+
RHL.NS
|
| 1538 |
+
RICOAUTO.NS
|
| 1539 |
+
RIIL.NS
|
| 1540 |
+
RISHABH.NS
|
| 1541 |
+
RITCO.NS
|
| 1542 |
+
RITES.NS
|
| 1543 |
+
RKDL.NS
|
| 1544 |
+
RKEC.NS
|
| 1545 |
+
RKFORGE.NS
|
| 1546 |
+
RKSWAMY.NS
|
| 1547 |
+
RML.NS
|
| 1548 |
+
ROHLTD.NS
|
| 1549 |
+
ROLEXRINGS.NS
|
| 1550 |
+
ROLLT.NS
|
| 1551 |
+
ROLTA.NS
|
| 1552 |
+
ROML.NS
|
| 1553 |
+
ROSSARI.NS
|
| 1554 |
+
ROSSELLIND.NS
|
| 1555 |
+
ROSSTECH.NS
|
| 1556 |
+
ROTO.NS
|
| 1557 |
+
ROUTE.NS
|
| 1558 |
+
RPEL.NS
|
| 1559 |
+
RPGLIFE.NS
|
| 1560 |
+
RPOWER.NS
|
| 1561 |
+
RPPINFRA.NS
|
| 1562 |
+
RPPL.NS
|
| 1563 |
+
RPSGVENT.NS
|
| 1564 |
+
RPTECH.NS
|
| 1565 |
+
RRKABEL.NS
|
| 1566 |
+
RSSOFTWARE.NS
|
| 1567 |
+
RSWM.NS
|
| 1568 |
+
RSYSTEMS.NS
|
| 1569 |
+
RTNINDIA.NS
|
| 1570 |
+
RTNPOWER.NS
|
| 1571 |
+
RUBFILA.NS
|
| 1572 |
+
RUBYMILLS.NS
|
| 1573 |
+
RUCHINFRA.NS
|
| 1574 |
+
RUCHIRA.NS
|
| 1575 |
+
RUPA.NS
|
| 1576 |
+
RUSHIL.NS
|
| 1577 |
+
RUSTOMJEE.NS
|
| 1578 |
+
RVHL.NS
|
| 1579 |
+
RVNL.NS
|
| 1580 |
+
RVTH.NS
|
| 1581 |
+
S&SPOWER.NS
|
| 1582 |
+
SABEVENTS.NS
|
| 1583 |
+
SABTNL.NS
|
| 1584 |
+
SADBHAV.NS
|
| 1585 |
+
SADBHIN.NS
|
| 1586 |
+
SADHNANIQ.NS
|
| 1587 |
+
SAFARI.NS
|
| 1588 |
+
SAGARDEEP.NS
|
| 1589 |
+
SAGCEM.NS
|
| 1590 |
+
SAGILITY.NS
|
| 1591 |
+
SAH.NS
|
| 1592 |
+
SAHYADRI.NS
|
| 1593 |
+
SAIL.NS
|
| 1594 |
+
SAILIFE.NS
|
| 1595 |
+
SAKAR.NS
|
| 1596 |
+
SAKHTISUG.NS
|
| 1597 |
+
SAKSOFT.NS
|
| 1598 |
+
SAKUMA.NS
|
| 1599 |
+
SALASAR.NS
|
| 1600 |
+
SALONA.NS
|
| 1601 |
+
SALSTEEL.NS
|
| 1602 |
+
SALZERELEC.NS
|
| 1603 |
+
SAMBHAAV.NS
|
| 1604 |
+
SAMHI.NS
|
| 1605 |
+
SAMMAANCAP.NS
|
| 1606 |
+
SAMPANN.NS
|
| 1607 |
+
SANATHAN.NS
|
| 1608 |
+
SANCO.NS
|
| 1609 |
+
SANDESH.NS
|
| 1610 |
+
SANDHAR.NS
|
| 1611 |
+
SANDUMA.NS
|
| 1612 |
+
SANGAMIND.NS
|
| 1613 |
+
SANGHIIND.NS
|
| 1614 |
+
SANGHVIMOV.NS
|
| 1615 |
+
SANGINITA.NS
|
| 1616 |
+
SANOFI.NS
|
| 1617 |
+
SANOFICONR.NS
|
| 1618 |
+
SANSERA.NS
|
| 1619 |
+
SANSTAR.NS
|
| 1620 |
+
SANWARIA.NS
|
| 1621 |
+
SAPPHIRE.NS
|
| 1622 |
+
SARDAEN.NS
|
| 1623 |
+
SAREGAMA.NS
|
| 1624 |
+
SARLAPOLY.NS
|
| 1625 |
+
SARVESHWAR.NS
|
| 1626 |
+
SASKEN.NS
|
| 1627 |
+
SASTASUNDR.NS
|
| 1628 |
+
SATIA.NS
|
| 1629 |
+
SATIN.NS
|
| 1630 |
+
SATINDLTD.NS
|
| 1631 |
+
SAURASHCEM.NS
|
| 1632 |
+
SBC.NS
|
| 1633 |
+
SBCL.NS
|
| 1634 |
+
SBFC.NS
|
| 1635 |
+
SBGLP.NS
|
| 1636 |
+
SBICARD.NS
|
| 1637 |
+
SBILIFE.NS
|
| 1638 |
+
SBIN.NS
|
| 1639 |
+
SCHAEFFLER.NS
|
| 1640 |
+
SCHAND.NS
|
| 1641 |
+
SCHNEIDER.NS
|
| 1642 |
+
SCI.NS
|
| 1643 |
+
SCILAL.NS
|
| 1644 |
+
SCPL.NS
|
| 1645 |
+
SDBL.NS
|
| 1646 |
+
SEAMECLTD.NS
|
| 1647 |
+
SECMARK.NS
|
| 1648 |
+
SECURKLOUD.NS
|
| 1649 |
+
SEJALLTD.NS
|
| 1650 |
+
SELAN.NS
|
| 1651 |
+
SELMC.NS
|
| 1652 |
+
SEMAC.NS
|
| 1653 |
+
SENCO.NS
|
| 1654 |
+
SENORES.NS
|
| 1655 |
+
SEPC.NS
|
| 1656 |
+
SEQUENT.NS
|
| 1657 |
+
SERVOTECH.NS
|
| 1658 |
+
SESHAPAPER.NS
|
| 1659 |
+
SETCO.NS
|
| 1660 |
+
SETUINFRA.NS
|
| 1661 |
+
SFL.NS
|
| 1662 |
+
SGIL.NS
|
| 1663 |
+
SGL.NS
|
| 1664 |
+
SGLTL.NS
|
| 1665 |
+
SHAH.NS
|
| 1666 |
+
SHAHALLOYS.NS
|
| 1667 |
+
SHAILY.NS
|
| 1668 |
+
SHAKTIPUMP.NS
|
| 1669 |
+
SHALBY.NS
|
| 1670 |
+
SHALPAINTS.NS
|
| 1671 |
+
SHANKARA.NS
|
| 1672 |
+
SHANTI.NS
|
| 1673 |
+
SHANTIGEAR.NS
|
| 1674 |
+
SHARDACROP.NS
|
| 1675 |
+
SHARDAMOTR.NS
|
| 1676 |
+
SHAREINDIA.NS
|
| 1677 |
+
SHEKHAWATI.NS
|
| 1678 |
+
SHEMAROO.NS
|
| 1679 |
+
SHILPAMED.NS
|
| 1680 |
+
SHIVALIK.NS
|
| 1681 |
+
SHIVAMAUTO.NS
|
| 1682 |
+
SHIVAMILLS.NS
|
| 1683 |
+
SHIVATEX.NS
|
| 1684 |
+
SHK.NS
|
| 1685 |
+
SHOPERSTOP.NS
|
| 1686 |
+
SHRADHA.NS
|
| 1687 |
+
SHREDIGCEM.NS
|
| 1688 |
+
SHREECEM.NS
|
| 1689 |
+
SHREEPUSHK.NS
|
| 1690 |
+
SHREERAMA.NS
|
| 1691 |
+
SHRENIK.NS
|
| 1692 |
+
SHREYANIND.NS
|
| 1693 |
+
SHRIPISTON.NS
|
| 1694 |
+
SHRIRAMFIN.NS
|
| 1695 |
+
SHRIRAMPPS.NS
|
| 1696 |
+
SHYAMCENT.NS
|
| 1697 |
+
SHYAMMETL.NS
|
| 1698 |
+
SHYAMTEL.NS
|
| 1699 |
+
SIEMENS.NS
|
| 1700 |
+
SIGACHI.NS
|
| 1701 |
+
SIGIND.NS
|
| 1702 |
+
SIGMA.NS
|
| 1703 |
+
SIGNATURE.NS
|
| 1704 |
+
SIGNPOST.NS
|
| 1705 |
+
SIKKO.NS
|
| 1706 |
+
SIL.NS
|
| 1707 |
+
SILGO.NS
|
| 1708 |
+
SILINV.NS
|
| 1709 |
+
SILLYMONKS.NS
|
| 1710 |
+
SILVERTUC.NS
|
| 1711 |
+
SIMBHALS.NS
|
| 1712 |
+
SIMPLEXINF.NS
|
| 1713 |
+
SINCLAIR.NS
|
| 1714 |
+
SINDHUTRAD.NS
|
| 1715 |
+
SINTERCOM.NS
|
| 1716 |
+
SIRCA.NS
|
| 1717 |
+
SIS.NS
|
| 1718 |
+
SITINET.NS
|
| 1719 |
+
SIYSIL.NS
|
| 1720 |
+
SJS.NS
|
| 1721 |
+
SJVN.NS
|
| 1722 |
+
SKFINDIA.NS
|
| 1723 |
+
SKIPPER.NS
|
| 1724 |
+
SKMEGGPROD.NS
|
| 1725 |
+
SKYGOLD.NS
|
| 1726 |
+
SMARTLINK.NS
|
| 1727 |
+
SMCGLOBAL.NS
|
| 1728 |
+
SMLISUZU.NS
|
| 1729 |
+
SMLT.NS
|
| 1730 |
+
SMSLIFE.NS
|
| 1731 |
+
SMSPHARMA.NS
|
| 1732 |
+
SNOWMAN.NS
|
| 1733 |
+
SOBHA.NS
|
| 1734 |
+
SOFTTECH.NS
|
| 1735 |
+
SOLARA.NS
|
| 1736 |
+
SOLARINDS.NS
|
| 1737 |
+
SOMANYCERA.NS
|
| 1738 |
+
SOMATEX.NS
|
| 1739 |
+
SOMICONVEY.NS
|
| 1740 |
+
SONACOMS.NS
|
| 1741 |
+
SONAMLTD.NS
|
| 1742 |
+
SONATSOFTW.NS
|
| 1743 |
+
SOTL.NS
|
| 1744 |
+
SOUTHBANK.NS
|
| 1745 |
+
SOUTHWEST.NS
|
| 1746 |
+
SPAL.NS
|
| 1747 |
+
SPANDANA.NS
|
| 1748 |
+
SPARC.NS
|
| 1749 |
+
SPCENET.NS
|
| 1750 |
+
SPECIALITY.NS
|
| 1751 |
+
SPECTRUM.NS
|
| 1752 |
+
SPENCERS.NS
|
| 1753 |
+
SPIC.NS
|
| 1754 |
+
SPLIL.NS
|
| 1755 |
+
SPLPETRO.NS
|
| 1756 |
+
SPMLINFRA.NS
|
| 1757 |
+
SPORTKING.NS
|
| 1758 |
+
SRD.NS
|
| 1759 |
+
SREEL.NS
|
| 1760 |
+
SRF.NS
|
| 1761 |
+
SRGHFL.NS
|
| 1762 |
+
SRHHYPOLTD.NS
|
| 1763 |
+
SRM.NS
|
| 1764 |
+
SRPL.NS
|
| 1765 |
+
SSDL.NS
|
| 1766 |
+
SSWL.NS
|
| 1767 |
+
STALLION.NS
|
| 1768 |
+
STANLEY.NS
|
| 1769 |
+
STAR.NS
|
| 1770 |
+
STARCEMENT.NS
|
| 1771 |
+
STARHEALTH.NS
|
| 1772 |
+
STARPAPER.NS
|
| 1773 |
+
STARTECK.NS
|
| 1774 |
+
STCINDIA.NS
|
| 1775 |
+
STEELCAS.NS
|
| 1776 |
+
STEELCITY.NS
|
| 1777 |
+
STEELXIND.NS
|
| 1778 |
+
STEL.NS
|
| 1779 |
+
STERTOOLS.NS
|
| 1780 |
+
STLTECH.NS
|
| 1781 |
+
STOVEKRAFT.NS
|
| 1782 |
+
STYLAMIND.NS
|
| 1783 |
+
STYLEBAAZA.NS
|
| 1784 |
+
STYRENIX.NS
|
| 1785 |
+
SUBEXLTD.NS
|
| 1786 |
+
SUBROS.NS
|
| 1787 |
+
SUDARSCHEM.NS
|
| 1788 |
+
SUKHJITS.NS
|
| 1789 |
+
SULA.NS
|
| 1790 |
+
SUMICHEM.NS
|
| 1791 |
+
SUMIT.NS
|
| 1792 |
+
SUMMITSEC.NS
|
| 1793 |
+
SUNCLAY.NS
|
| 1794 |
+
SUNDARAM.NS
|
| 1795 |
+
SUNDARMFIN.NS
|
| 1796 |
+
SUNDARMHLD.NS
|
| 1797 |
+
SUNDRMBRAK.NS
|
| 1798 |
+
SUNDRMFAST.NS
|
| 1799 |
+
SUNDROP.NS
|
| 1800 |
+
SUNFLAG.NS
|
| 1801 |
+
SUNPHARMA.NS
|
| 1802 |
+
SUNTECK.NS
|
| 1803 |
+
SUNTV.NS
|
| 1804 |
+
SUPERHOUSE.NS
|
| 1805 |
+
SUPERSPIN.NS
|
| 1806 |
+
SUPRAJIT.NS
|
| 1807 |
+
SUPREME.NS
|
| 1808 |
+
SUPREMEENG.NS
|
| 1809 |
+
SUPREMEIND.NS
|
| 1810 |
+
SUPREMEINF.NS
|
| 1811 |
+
SUPRIYA.NS
|
| 1812 |
+
SURAJEST.NS
|
| 1813 |
+
SURAJLTD.NS
|
| 1814 |
+
SURAKSHA.NS
|
| 1815 |
+
SURANASOL.NS
|
| 1816 |
+
SURANAT&P.NS
|
| 1817 |
+
SURYALAXMI.NS
|
| 1818 |
+
SURYAROSNI.NS
|
| 1819 |
+
SURYODAY.NS
|
| 1820 |
+
SUTLEJTEX.NS
|
| 1821 |
+
SUULD.NS
|
| 1822 |
+
SUVEN.NS
|
| 1823 |
+
SUVENPHAR.NS
|
| 1824 |
+
SUVIDHAA.NS
|
| 1825 |
+
SUYOG.NS
|
| 1826 |
+
SUZLON.NS
|
| 1827 |
+
SVLL.NS
|
| 1828 |
+
SVPGLOB.NS
|
| 1829 |
+
SWANENERGY.NS
|
| 1830 |
+
SWARAJENG.NS
|
| 1831 |
+
SWELECTES.NS
|
| 1832 |
+
SWIGGY.NS
|
| 1833 |
+
SWSOLAR.NS
|
| 1834 |
+
SYMPHONY.NS
|
| 1835 |
+
SYNCOMF.NS
|
| 1836 |
+
SYNGENE.NS
|
| 1837 |
+
SYRMA.NS
|
| 1838 |
+
TAINWALCHM.NS
|
| 1839 |
+
TAJGVK.NS
|
| 1840 |
+
TAKE.NS
|
| 1841 |
+
TALBROAUTO.NS
|
| 1842 |
+
TANLA.NS
|
| 1843 |
+
TARACHAND.NS
|
| 1844 |
+
TARAPUR.NS
|
| 1845 |
+
TARC.NS
|
| 1846 |
+
TARIL.NS
|
| 1847 |
+
TARMAT.NS
|
| 1848 |
+
TARSONS.NS
|
| 1849 |
+
TASTYBITE.NS
|
| 1850 |
+
TATACHEM.NS
|
| 1851 |
+
TATACOMM.NS
|
| 1852 |
+
TATACONSUM.NS
|
| 1853 |
+
TATAELXSI.NS
|
| 1854 |
+
TATAINVEST.NS
|
| 1855 |
+
TATAMOTORS.NS
|
| 1856 |
+
TATAPOWER.NS
|
| 1857 |
+
TATASTEEL.NS
|
| 1858 |
+
TATATECH.NS
|
| 1859 |
+
TATVA.NS
|
| 1860 |
+
TBOTEK.NS
|
| 1861 |
+
TBZ.NS
|
| 1862 |
+
TCI.NS
|
| 1863 |
+
TCIEXP.NS
|
| 1864 |
+
TCIFINANCE.NS
|
| 1865 |
+
TCPLPACK.NS
|
| 1866 |
+
TCS.NS
|
| 1867 |
+
TDPOWERSYS.NS
|
| 1868 |
+
TEAMLEASE.NS
|
| 1869 |
+
TECHM.NS
|
| 1870 |
+
TECHNOE.NS
|
| 1871 |
+
TEGA.NS
|
| 1872 |
+
TEJASNET.NS
|
| 1873 |
+
TEMBO.NS
|
| 1874 |
+
TERASOFT.NS
|
| 1875 |
+
TEXINFRA.NS
|
| 1876 |
+
TEXMOPIPES.NS
|
| 1877 |
+
TEXRAIL.NS
|
| 1878 |
+
TFCILTD.NS
|
| 1879 |
+
TFL.NS
|
| 1880 |
+
TGBHOTELS.NS
|
| 1881 |
+
THANGAMAYL.NS
|
| 1882 |
+
THEINVEST.NS
|
| 1883 |
+
THEJO.NS
|
| 1884 |
+
THEMISMED.NS
|
| 1885 |
+
THERMAX.NS
|
| 1886 |
+
THOMASCOOK.NS
|
| 1887 |
+
THOMASCOTT.NS
|
| 1888 |
+
THYROCARE.NS
|
| 1889 |
+
TI.NS
|
| 1890 |
+
TICL.NS
|
| 1891 |
+
TIIL.NS
|
| 1892 |
+
TIINDIA.NS
|
| 1893 |
+
TIJARIA.NS
|
| 1894 |
+
TIL.NS
|
| 1895 |
+
TIMESGTY.NS
|
| 1896 |
+
TIMETECHNO.NS
|
| 1897 |
+
TIMKEN.NS
|
| 1898 |
+
TINNARUBR.NS
|
| 1899 |
+
TIPSFILMS.NS
|
| 1900 |
+
TIPSMUSIC.NS
|
| 1901 |
+
TIRUMALCHM.NS
|
| 1902 |
+
TIRUPATIFL.NS
|
| 1903 |
+
TITAGARH.NS
|
| 1904 |
+
TITAN.NS
|
| 1905 |
+
TMB.NS
|
| 1906 |
+
TNPETRO.NS
|
| 1907 |
+
TNPL.NS
|
| 1908 |
+
TNTELE.NS
|
| 1909 |
+
TOKYOPLAST.NS
|
| 1910 |
+
TOLINS.NS
|
| 1911 |
+
TORNTPHARM.NS
|
| 1912 |
+
TORNTPOWER.NS
|
| 1913 |
+
TOTAL.NS
|
| 1914 |
+
TOUCHWOOD.NS
|
| 1915 |
+
TPHQ.NS
|
| 1916 |
+
TPLPLASTEH.NS
|
| 1917 |
+
TRACXN.NS
|
| 1918 |
+
TRANSRAILL.NS
|
| 1919 |
+
TRANSWORLD.NS
|
| 1920 |
+
TREEHOUSE.NS
|
| 1921 |
+
TREJHARA.NS
|
| 1922 |
+
TREL.NS
|
| 1923 |
+
TRENT.NS
|
| 1924 |
+
TRF.NS
|
| 1925 |
+
TRIDENT.NS
|
| 1926 |
+
TRIGYN.NS
|
| 1927 |
+
TRITURBINE.NS
|
| 1928 |
+
TRIVENI.NS
|
| 1929 |
+
TRU.NS
|
| 1930 |
+
TTKHLTCARE.NS
|
| 1931 |
+
TTKPRESTIG.NS
|
| 1932 |
+
TTL.NS
|
| 1933 |
+
TTML.NS
|
| 1934 |
+
TVSELECT.NS
|
| 1935 |
+
TVSHLTD.NS
|
| 1936 |
+
TVSMOTOR.NS
|
| 1937 |
+
TVSSCS.NS
|
| 1938 |
+
TVSSRICHAK.NS
|
| 1939 |
+
TVTODAY.NS
|
| 1940 |
+
TVVISION.NS
|
| 1941 |
+
UBL.NS
|
| 1942 |
+
UCAL.NS
|
| 1943 |
+
UCOBANK.NS
|
| 1944 |
+
UDAICEMENT.NS
|
| 1945 |
+
UDS.NS
|
| 1946 |
+
UFLEX.NS
|
| 1947 |
+
UFO.NS
|
| 1948 |
+
UGARSUGAR.NS
|
| 1949 |
+
UGROCAP.NS
|
| 1950 |
+
UJJIVANSFB.NS
|
| 1951 |
+
ULTRACEMCO.NS
|
| 1952 |
+
UMAEXPORTS.NS
|
| 1953 |
+
UMANGDAIRY.NS
|
| 1954 |
+
UMESLTD.NS
|
| 1955 |
+
UMIYA-MRO.NS
|
| 1956 |
+
UNICHEMLAB.NS
|
| 1957 |
+
UNIDT.NS
|
| 1958 |
+
UNIECOM.NS
|
| 1959 |
+
UNIENTER.NS
|
| 1960 |
+
UNIINFO.NS
|
| 1961 |
+
UNIMECH.NS
|
| 1962 |
+
UNIONBANK.NS
|
| 1963 |
+
UNIPARTS.NS
|
| 1964 |
+
UNITDSPR.NS
|
| 1965 |
+
UNITECH.NS
|
| 1966 |
+
UNITEDPOLY.NS
|
| 1967 |
+
UNITEDTEA.NS
|
| 1968 |
+
UNIVASTU.NS
|
| 1969 |
+
UNIVCABLES.NS
|
| 1970 |
+
UNIVPHOTO.NS
|
| 1971 |
+
UNOMINDA.NS
|
| 1972 |
+
UPL.NS
|
| 1973 |
+
URAVIDEF.NS
|
| 1974 |
+
URJA.NS
|
| 1975 |
+
USHAMART.NS
|
| 1976 |
+
USK.NS
|
| 1977 |
+
UTIAMC.NS
|
| 1978 |
+
UTKARSHBNK.NS
|
| 1979 |
+
UTTAMSUGAR.NS
|
| 1980 |
+
UYFINCORP.NS
|
| 1981 |
+
V2RETAIL.NS
|
| 1982 |
+
VADILALIND.NS
|
| 1983 |
+
VAIBHAVGBL.NS
|
| 1984 |
+
VAISHALI.NS
|
| 1985 |
+
VAKRANGEE.NS
|
| 1986 |
+
VALIANTLAB.NS
|
| 1987 |
+
VALIANTORG.NS
|
| 1988 |
+
VARDHACRLC.NS
|
| 1989 |
+
VARDMNPOLY.NS
|
| 1990 |
+
VARROC.NS
|
| 1991 |
+
VASCONEQ.NS
|
| 1992 |
+
VASWANI.NS
|
| 1993 |
+
VBL.NS
|
| 1994 |
+
VCL.NS
|
| 1995 |
+
VEDL.NS
|
| 1996 |
+
VEEDOL.NS
|
| 1997 |
+
VENKEYS.NS
|
| 1998 |
+
VENTIVE.NS
|
| 1999 |
+
VENUSPIPES.NS
|
| 2000 |
+
VENUSREM.NS
|
| 2001 |
+
VERANDA.NS
|
| 2002 |
+
VERTOZ.NS
|
| 2003 |
+
VESUVIUS.NS
|
| 2004 |
+
VETO.NS
|
| 2005 |
+
VGUARD.NS
|
| 2006 |
+
VHL.NS
|
| 2007 |
+
VHLTD.NS
|
| 2008 |
+
VIDHIING.NS
|
| 2009 |
+
VIJAYA.NS
|
| 2010 |
+
VIJIFIN.NS
|
| 2011 |
+
VIKASECO.NS
|
| 2012 |
+
VIKASLIFE.NS
|
| 2013 |
+
VIMTALABS.NS
|
| 2014 |
+
VINATIORGA.NS
|
| 2015 |
+
VINCOFE.NS
|
| 2016 |
+
VINDHYATEL.NS
|
| 2017 |
+
VINEETLAB.NS
|
| 2018 |
+
VINNY.NS
|
| 2019 |
+
VINYLINDIA.NS
|
| 2020 |
+
VIPCLOTHNG.NS
|
| 2021 |
+
VIPIND.NS
|
| 2022 |
+
VIPULLTD.NS
|
| 2023 |
+
VIRINCHI.NS
|
| 2024 |
+
VISAKAIND.NS
|
| 2025 |
+
VISASTEEL.NS
|
| 2026 |
+
VISHNU.NS
|
| 2027 |
+
VISHWARAJ.NS
|
| 2028 |
+
VIVIDHA.NS
|
| 2029 |
+
VLEGOV.NS
|
| 2030 |
+
VLSFINANCE.NS
|
| 2031 |
+
VMART.NS
|
| 2032 |
+
VMM.NS
|
| 2033 |
+
VOLTAMP.NS
|
| 2034 |
+
VOLTAS.NS
|
| 2035 |
+
VPRPL.NS
|
| 2036 |
+
VRAJ.NS
|
| 2037 |
+
VRLLOG.NS
|
| 2038 |
+
VSSL.NS
|
| 2039 |
+
VSTIND.NS
|
| 2040 |
+
VSTL.NS
|
| 2041 |
+
VSTTILLERS.NS
|
| 2042 |
+
VTL.NS
|
| 2043 |
+
WAAREEENER.NS
|
| 2044 |
+
WAAREERTL.NS
|
| 2045 |
+
WABAG.NS
|
| 2046 |
+
WALCHANNAG.NS
|
| 2047 |
+
WANBURY.NS
|
| 2048 |
+
WCIL.NS
|
| 2049 |
+
WEALTH.NS
|
| 2050 |
+
WEBELSOLAR.NS
|
| 2051 |
+
WEIZMANIND.NS
|
| 2052 |
+
WEL.NS
|
| 2053 |
+
WELCORP.NS
|
| 2054 |
+
WELENT.NS
|
| 2055 |
+
WELINV.NS
|
| 2056 |
+
WELSPUNLIV.NS
|
| 2057 |
+
WENDT.NS
|
| 2058 |
+
WESTLIFE.NS
|
| 2059 |
+
WEWIN.NS
|
| 2060 |
+
WHEELS.NS
|
| 2061 |
+
WHIRLPOOL.NS
|
| 2062 |
+
WILLAMAGOR.NS
|
| 2063 |
+
WINDLAS.NS
|
| 2064 |
+
WINDMACHIN.NS
|
| 2065 |
+
WINSOME.NS
|
| 2066 |
+
WIPL.NS
|
| 2067 |
+
WIPRO.NS
|
| 2068 |
+
WOCKPHARMA.NS
|
| 2069 |
+
WONDERLA.NS
|
| 2070 |
+
WORTH.NS
|
| 2071 |
+
WSI.NS
|
| 2072 |
+
WSTCSTPAPR.NS
|
| 2073 |
+
XCHANGING.NS
|
| 2074 |
+
XELPMOC.NS
|
| 2075 |
+
XPROINDIA.NS
|
| 2076 |
+
XTGLOBAL.NS
|
| 2077 |
+
YAARI.NS
|
| 2078 |
+
YASHO.NS
|
| 2079 |
+
YATHARTH.NS
|
| 2080 |
+
YATRA.NS
|
| 2081 |
+
YESBANK.NS
|
| 2082 |
+
YUKEN.NS
|
| 2083 |
+
ZAGGLE.NS
|
| 2084 |
+
ZEEL.NS
|
| 2085 |
+
ZEELEARN.NS
|
| 2086 |
+
ZEEMEDIA.NS
|
| 2087 |
+
ZENITHEXPO.NS
|
| 2088 |
+
ZENITHSTL.NS
|
| 2089 |
+
ZENSARTECH.NS
|
| 2090 |
+
ZENTEC.NS
|
| 2091 |
+
ZFCVINDIA.NS
|
| 2092 |
+
ZIMLAB.NS
|
| 2093 |
+
ZODIAC.NS
|
| 2094 |
+
ZODIACLOTH.NS
|
| 2095 |
+
ZOTA.NS
|
| 2096 |
+
ZUARI.NS
|
| 2097 |
+
ZUARIIND.NS
|
| 2098 |
+
ZYDUSLIFE.NS
|
| 2099 |
+
ZYDUSWELL.NS
|
app/user.py
ADDED
|
@@ -0,0 +1,227 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
from fastapi import APIRouter, Request, Depends, HTTPException
|
| 2 |
+
from fastapi.responses import HTMLResponse, RedirectResponse
|
| 3 |
+
from sqlalchemy.orm import Session
|
| 4 |
+
|
| 5 |
+
from app import models, schemas, crud
|
| 6 |
+
from app.database import get_db
|
| 7 |
+
from app.auth import get_current_active_user
|
| 8 |
+
from app.main import templates # Import templates from main.py
|
| 9 |
+
import logging
|
| 10 |
+
import yfinance as yf
|
| 11 |
+
from datetime import datetime, timedelta
|
| 12 |
+
import pytz # for timezone handling
|
| 13 |
+
from typing import List
|
| 14 |
+
|
| 15 |
+
logger = logging.getLogger(__name__)
|
| 16 |
+
router = APIRouter()
|
| 17 |
+
|
| 18 |
+
# Define timezone (Asia/Kolkata)
|
| 19 |
+
tz = pytz.timezone('Asia/Kolkata')
|
| 20 |
+
|
| 21 |
+
# Function to read tickers from tickers.txt
|
| 22 |
+
def get_all_tickers():
|
| 23 |
+
tickers = []
|
| 24 |
+
try:
|
| 25 |
+
with open("app/tickers.txt", "r") as f:
|
| 26 |
+
tickers = [line.strip() for line in f if line.strip()]
|
| 27 |
+
except FileNotFoundError:
|
| 28 |
+
logger.error("tickers.txt not found.")
|
| 29 |
+
return tickers
|
| 30 |
+
|
| 31 |
+
# Function to find closest available trading day price on or before the target date
|
| 32 |
+
def get_closest_price(hist, date):
|
| 33 |
+
# Filter dates in hist index that are <= target date
|
| 34 |
+
available_dates = hist.index[hist.index <= date]
|
| 35 |
+
if not available_dates.empty:
|
| 36 |
+
closest_date = available_dates[-1]
|
| 37 |
+
return hist.loc[closest_date]['Close'], closest_date.date()
|
| 38 |
+
else:
|
| 39 |
+
return None, None
|
| 40 |
+
|
| 41 |
+
# Function to fetch historical data and calculate prices
|
| 42 |
+
def fetch_stock_prices(ticker_symbol: str):
|
| 43 |
+
ticker = yf.Ticker(ticker_symbol)
|
| 44 |
+
|
| 45 |
+
# Get today's date as timezone-aware datetime
|
| 46 |
+
today = datetime.now(tz)
|
| 47 |
+
|
| 48 |
+
# Define the dates for 1 month, 6 months, 1 year, and 3 years ago (timezone-aware)
|
| 49 |
+
dates = {
|
| 50 |
+
"Current": today,
|
| 51 |
+
"1 Month Ago": today - timedelta(days=30),
|
| 52 |
+
"6 Months Ago": today - timedelta(days=182),
|
| 53 |
+
"1 Year Ago": today - timedelta(days=365),
|
| 54 |
+
"3 Years Ago": today - timedelta(days=3*365),
|
| 55 |
+
}
|
| 56 |
+
|
| 57 |
+
# Fetch historical data for the last 4 years to cover all dates
|
| 58 |
+
start_date = (today - timedelta(days=4*365)).strftime('%Y-%m-%d')
|
| 59 |
+
end_date = today.strftime('%Y-%m-%d')
|
| 60 |
+
hist = ticker.history(start=start_date, end=end_date)
|
| 61 |
+
|
| 62 |
+
# The index of hist is timezone-aware, ensure it matches tz
|
| 63 |
+
if hist.index.tz is None:
|
| 64 |
+
# If index is naive, localize it
|
| 65 |
+
hist.index = hist.index.tz_localize(tz)
|
| 66 |
+
else:
|
| 67 |
+
# Convert to desired timezone
|
| 68 |
+
hist.index = hist.index.tz_convert(tz)
|
| 69 |
+
|
| 70 |
+
# Retrieve prices for each date
|
| 71 |
+
prices = {}
|
| 72 |
+
for label, date in dates.items():
|
| 73 |
+
price, actual_date = get_closest_price(hist, date)
|
| 74 |
+
if price is not None:
|
| 75 |
+
prices[label] = (f"₹{price:.2f}", actual_date.strftime('%Y-%m-%d') if actual_date else None)
|
| 76 |
+
else:
|
| 77 |
+
prices[label] = ("No data", None)
|
| 78 |
+
|
| 79 |
+
return prices
|
| 80 |
+
|
| 81 |
+
# API endpoint for searching tickers
|
| 82 |
+
@router.get("/api/stocks/search", response_model=List[str])
|
| 83 |
+
async def search_stocks(query: str):
|
| 84 |
+
all_tickers = get_all_tickers()
|
| 85 |
+
# Simple case-insensitive search
|
| 86 |
+
matching_tickers = [ticker for ticker in all_tickers if query.lower() in ticker.lower()]
|
| 87 |
+
return matching_tickers
|
| 88 |
+
|
| 89 |
+
# API endpoint for fetching stock prices
|
| 90 |
+
@router.get("/api/stocks/price/{ticker_symbol}")
|
| 91 |
+
async def get_stock_price(ticker_symbol: str):
|
| 92 |
+
try:
|
| 93 |
+
prices = fetch_stock_prices(ticker_symbol)
|
| 94 |
+
return prices
|
| 95 |
+
except Exception as e:
|
| 96 |
+
logger.error(f"Error fetching price for {ticker_symbol}: {e}")
|
| 97 |
+
raise HTTPException(status_code=500, detail="Could not fetch stock price")
|
| 98 |
+
|
| 99 |
+
|
| 100 |
+
@router.get("/home", response_class=HTMLResponse)
|
| 101 |
+
async def user_homepage(request: Request): # Removed: current_user: models.User = Depends(get_current_active_user)
|
| 102 |
+
# User data will be fetched and displayed by client-side JavaScript
|
| 103 |
+
return templates.TemplateResponse("user/homepage.html", {
|
| 104 |
+
"request": request,
|
| 105 |
+
# "user": current_user, # This will be handled by client-side JS
|
| 106 |
+
"title": "User Homepage"
|
| 107 |
+
})
|
| 108 |
+
|
| 109 |
+
@router.get("/chatbot/{model_type}", response_class=HTMLResponse)
|
| 110 |
+
async def chatbot_page(request: Request, model_type: str): # Removed: current_user: models.User = Depends(get_current_active_user)
|
| 111 |
+
# Client-side JS on chatbot.html already handles token for API submission.
|
| 112 |
+
# This route now just serves the page structure.
|
| 113 |
+
valid_models = ["base", "enhanced", "rule_based"]
|
| 114 |
+
if model_type.lower() not in valid_models:
|
| 115 |
+
raise HTTPException(status_code=404, detail="Model type not found")
|
| 116 |
+
|
| 117 |
+
# Prepare context for the template based on model_type
|
| 118 |
+
# This context will help the template render the correct form fields
|
| 119 |
+
form_fields = []
|
| 120 |
+
page_title = ""
|
| 121 |
+
|
| 122 |
+
if model_type == "base":
|
| 123 |
+
page_title = "Base Model Advisor"
|
| 124 |
+
form_fields = [
|
| 125 |
+
{"name": "Salary", "label": "Monthly Salary (₹)", "type": "number", "required": True, "min": 0},
|
| 126 |
+
{"name": "Expenses", "label": "Monthly Expenses (₹)", "type": "number", "required": True, "min": 0},
|
| 127 |
+
{"name": "Savings", "label": "Monthly Savings (₹)", "type": "number", "required": True, "min": 0},
|
| 128 |
+
{"name": "Lifecycle_Stage", "label": "Lifecycle Stage", "type": "select", "required": True, "options": ["Student", "Early Career", "Mid-Career", "Late Career", "Retired"]},
|
| 129 |
+
{"name": "Risk_Appetite", "label": "Risk Appetite", "type": "select", "required": True, "options": ["Low", "Medium", "High"]},
|
| 130 |
+
{"name": "Investment_Horizon", "label": "Investment Horizon", "type": "select", "required": True, "options": ["Short-term", "Medium-term", "Long-term"]},
|
| 131 |
+
]
|
| 132 |
+
elif model_type == "enhanced":
|
| 133 |
+
page_title = "Enhanced Model Advisor"
|
| 134 |
+
|
| 135 |
+
# Define the options for dropdowns based on provided lists
|
| 136 |
+
cities = sorted([
|
| 137 |
+
'Mumbai', 'Delhi', 'Bangalore', 'Hyderabad', 'Pune', 'Chennai', 'Jaipur',
|
| 138 |
+
'Kochi', 'Kolkata', 'Ahmedabad', 'Gurgaon', 'Lucknow', 'Nagpur', 'Chandigarh',
|
| 139 |
+
'Surat', 'Indore', 'Bhopal'
|
| 140 |
+
])
|
| 141 |
+
|
| 142 |
+
professions = sorted([
|
| 143 |
+
'Software Engineer', 'Doctor', 'Retired Banker', 'Student', 'Marketing Manager', 'Teacher',
|
| 144 |
+
'Freelancer', 'Architect', 'Business Owner', 'Nurse', 'Product Manager', 'CA', 'Data Analyst',
|
| 145 |
+
'Retired Professor', 'Journalist', 'Graphic Designer', 'Sales Executive', 'HR Manager', 'Intern',
|
| 146 |
+
'Dentist', 'Lawyer', 'Content Creator', 'Pensioner', 'UX Designer', 'Government Employee',
|
| 147 |
+
'Fashion Designer', 'Startup Founder', 'Homemaker (Investor)', 'Investment Banker', 'IT Consultant',
|
| 148 |
+
'College Student', 'Pharmacist', 'Textile Business Owner', 'Event Planner', 'Film Producer',
|
| 149 |
+
'Nutritionist', 'Retired Army Officer', 'Social Media Manager', 'Airline Pilot', 'Biotech Researcher',
|
| 150 |
+
'Real Estate Agent', 'AI Engineer', 'Retired Teacher', 'Financial Analyst', 'Software Developer',
|
| 151 |
+
'NGO Director', 'Fitness Trainer', 'Digital Marketer', 'Content Strategist', 'Retired Engineer',
|
| 152 |
+
'UI Developer', 'Operations Manager', 'Corporate Lawyer', 'School Principal', 'AI Researcher',
|
| 153 |
+
'Junior Doctor', 'Finance Manager', 'Fashion Blogger', 'HR Consultant', 'Video Editor',
|
| 154 |
+
'Small Business Owner', 'Dietitian', 'Supply Chain Manager', 'Interior Designer', 'Sales Manager',
|
| 155 |
+
'PR Executive', 'Logistics Head', 'Content Writer', 'Retired Govt. Employee', 'Marketing Lead',
|
| 156 |
+
'IT Manager', 'Startup Intern', 'Export Manager', 'Fitness Instructor', 'Real Estate Broker',
|
| 157 |
+
'Event Manager', 'Software Trainee', 'Data Scientist', 'HR Director', 'Hotel Manager',
|
| 158 |
+
'Social Worker', 'Cybersecurity Expert', 'E-commerce Manager', 'Bank Manager', 'Retired IT Manager',
|
| 159 |
+
'UX Researcher', 'Product Designer', 'Cybersecurity Analyst', 'Podcast Producer', 'E-commerce Seller',
|
| 160 |
+
'Cloud Architect', 'Corporate Trainer', 'AI Trainer', 'Supply Chain Head', 'Product Owner',
|
| 161 |
+
'UI/UX Designer', 'Finance Director', 'Sustainability Consultant', 'Retired Bank Manager',
|
| 162 |
+
'Social Media Influencer', 'Blockchain Developer', 'NGO Head', 'Data Engineer', 'Event Curator',
|
| 163 |
+
'CTO', 'Content Marketer', 'Retired Army Major', 'AR/VR Developer', 'Logistics Manager', 'VP Sales',
|
| 164 |
+
'EdTech Founder', 'SEO Specialist', 'Yoga Instructor', 'IT Director', 'App Developer',
|
| 165 |
+
'Consultant Cardiologist', 'Freelance Writer', 'Cloud Engineer', 'Retired CA', 'Digital Artist',
|
| 166 |
+
'Retired Pilot', 'Graphic Animator', 'AI Ethicist', 'School Trustee', 'Robotics Engineer',
|
| 167 |
+
'Fashion Stylist', 'Retired Nurse', 'AI Ethics Consultant', 'Drone Engineer', 'Retired Bank Clerk',
|
| 168 |
+
'Digital Nomad', 'CFO', 'Sustainability Analyst', 'Retired Journalist', 'Social Entrepreneur',
|
| 169 |
+
'DevOps Engineer', 'School Counselor', 'Retired Army Colonel', 'AR Developer', 'Sales Director',
|
| 170 |
+
'EdTech Consultant', 'SEO Expert', 'Cardiologist', '3D Artist', 'HR Head', 'Animator','Other',
|
| 171 |
+
'Fashion Influencer'
|
| 172 |
+
])
|
| 173 |
+
|
| 174 |
+
form_fields = [
|
| 175 |
+
{"name": "Profession", "label": "Profession", "type": "select", "required": True, "options": professions},
|
| 176 |
+
{"name": "City", "label": "City", "type": "select", "required": True, "options": cities},
|
| 177 |
+
{"name": "Salary", "label": "Monthly Salary (₹)", "type": "number", "required": True, "min": 0},
|
| 178 |
+
{"name": "Expenses", "label": "Monthly Expenses (₹)", "type": "number", "required": True, "min": 0},
|
| 179 |
+
{"name": "Savings", "label": "Monthly Savings (₹)", "type": "number", "required": True, "min": 0},
|
| 180 |
+
{"name": "Lifecycle_Stage", "label": "Lifecycle Stage", "type": "select", "required": True, "options": ["Student", "Early Career", "Mid-Career", "Late Career", "Retired"]},
|
| 181 |
+
{"name": "Risk_Appetite", "label": "Risk Appetite", "type": "select", "required": True, "options": ["Low", "Medium", "High"]},
|
| 182 |
+
{"name": "Investment_Horizon", "label": "Investment Horizon", "type": "select", "required": True, "options": ["Short-term", "Medium-term", "Long-term"]},
|
| 183 |
+
]
|
| 184 |
+
elif model_type == "rule_based":
|
| 185 |
+
page_title = "Rule-Based Advisor"
|
| 186 |
+
form_fields = [
|
| 187 |
+
{"name": "Lifecycle_Stage", "label": "Lifecycle Stage", "type": "select", "required": True, "options": ["Student", "Early Career", "Mid-Career", "Late Career", "Retired"]},
|
| 188 |
+
{"name": "Risk_Appetite", "label": "Risk Appetite", "type": "select", "required": True, "options": ["Low", "Medium", "High"]},
|
| 189 |
+
{"name": "Investment_Horizon", "label": "Investment Horizon", "type": "select", "required": True, "options": ["Short-term", "Medium-term", "Long-term"]},
|
| 190 |
+
{"name": "Annual_Salary_Package", "label": "Annual Salary Package (CTC) (₹)", "type": "number", "required": True, "min": 0},
|
| 191 |
+
{"name": "Monthly_In_hand_Salary", "label": "Actual Monthly In-hand Salary (₹)", "type": "number", "required": True, "min": 0},
|
| 192 |
+
{"name": "Total_Monthly_Expenses", "label": "Total Estimated Monthly Expenses (₹)", "type": "number", "required": True, "min": 0},
|
| 193 |
+
]
|
| 194 |
+
|
| 195 |
+
return templates.TemplateResponse("user/chatbot.html", {
|
| 196 |
+
"request": request,
|
| 197 |
+
# "user": current_user, # Client-side JS on this page handles API calls with token
|
| 198 |
+
"title": page_title,
|
| 199 |
+
"model_type": model_type,
|
| 200 |
+
"form_fields": form_fields
|
| 201 |
+
})
|
| 202 |
+
|
| 203 |
+
@router.get("/recommendations", response_class=HTMLResponse)
|
| 204 |
+
async def recommendations_page(request: Request): # Removed: current_user: models.User = Depends(get_current_active_user)
|
| 205 |
+
# Client-side JS could be added to this page if it needs to display user-specific info
|
| 206 |
+
# or make authenticated API calls for dynamic market data.
|
| 207 |
+
# For now, it serves static structure with mocked data.
|
| 208 |
+
# Data fetching logic would go into services/market_data_service.py
|
| 209 |
+
# For now, just rendering a template.
|
| 210 |
+
market_data = { # Mock data
|
| 211 |
+
"stocks": [
|
| 212 |
+
{"name": "Reliance Industries", "price": "2,850.50 INR", "change": "+0.5%"},
|
| 213 |
+
{"name": "Tata Consultancy Services", "price": "3,900.75 INR", "change": "-0.2%"},
|
| 214 |
+
{"name": "HDFC Bank", "price": "1,500.20 INR", "change": "+1.1%"},
|
| 215 |
+
],
|
| 216 |
+
"gold_price": "96,172.27 -745.25 INR per 10g",
|
| 217 |
+
"recommended_stocks": [
|
| 218 |
+
{"name": "Infosys", "reason": "Strong growth potential in IT sector."},
|
| 219 |
+
{"name": "ICICI Bank", "reason": "Good financial performance and outlook."}
|
| 220 |
+
]
|
| 221 |
+
}
|
| 222 |
+
return templates.TemplateResponse("user/recommendations.html", {
|
| 223 |
+
"request": request,
|
| 224 |
+
# "user": current_user, # Can be fetched by client-side JS if needed
|
| 225 |
+
"title": "Market Trends & Recommendations",
|
| 226 |
+
"market_data": market_data
|
| 227 |
+
})
|
app/user_financial_data.csv
ADDED
|
@@ -0,0 +1,14 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
UserID,UserEmail,ModelType,Timestamp,Name,Age,City,Profession,Salary,Expenses,Savings,Lifecycle Stage,Risk Appetite,Investment Horizon,Annual Salary Package,Monthly In-hand Salary,Total Monthly Expenses,Equity (%),Debt (%),Gold (%),FD/Cash (%)
|
| 2 |
+
2,user1@gmail.com,base,2025-05-08 14:48:49,,,,,60000,20000,40000,Early Career,Medium,Medium-term,,,,35.6,26.3,7.1,31.0
|
| 3 |
+
2,user1@gmail.com,enhanced,2025-05-08 14:50:49,,,Indore,Software Engineer,60000,40000,20000,Late Career,Medium,Medium-term,,,,32.3,40.7,9.0,18.0
|
| 4 |
+
2,user1@gmail.com,rule_based,2025-05-08 14:52:36,,,,,,,,Early Career,Medium,Medium-term,700000,,30000,,,,
|
| 5 |
+
3,user2@gmail.com,base,2025-05-08 15:25:39,,,,,90000,40000,50000,Mid-Career,Medium,Short-term,,,,36.9,27.2,10.5,25.4
|
| 6 |
+
3,user2@gmail.com,enhanced,2025-05-08 15:28:12,,,Pune,Software Engineer,80000,60000,20000,Late Career,High,Long-term,,,,49.5,34.8,3.9,11.8
|
| 7 |
+
4,user3@gmail.com,enhanced,2025-05-08 15:36:16,,,Pune,AI Ethics Consultant,90000,60000,30000,Mid-Career,Medium,Short-term,,,,37.2,27.6,11.5,23.7
|
| 8 |
+
5,user7@gmail.com,base,2025-05-09 19:10:38,,,,,70000,50000,20000,Mid-Career,Medium,Short-term,,,,37.9,26.9,11.7,23.5
|
| 9 |
+
5,user7@gmail.com,enhanced,2025-05-09 19:11:47,,,Bangalore,AI Engineer,80000,40000,40000,Late Career,High,Medium-term,,,,49.8,34.0,4.0,12.2
|
| 10 |
+
5,user7@gmail.com,rule_based,2025-05-09 19:13:52,,,,,,,,Mid-Career,High,Short-term,700000,,40000,,,,
|
| 11 |
+
6,mandar@gmail.com,base,2025-05-09 19:55:19,,,,,60000,40000,20000,Mid-Career,High,Long-term,,,,60.7,19.1,5.0,15.2
|
| 12 |
+
6,mandar@gmail.com,enhanced,2025-05-09 19:56:30,,,Pune,AI Researcher,70000,40000,30000,Mid-Career,High,Short-term,,,,57.4,15.0,4.5,23.1
|
| 13 |
+
6,mandar@gmail.com,rule_based,2025-05-09 19:57:28,,,,,,,,Mid-Career,Medium,Short-term,700000,,40000,,,,
|
| 14 |
+
2,user1@gmail.com,enhanced,2025-05-26 20:39:38,,,Pune,Other,80000,50000,30000,Mid-Career,Medium,Short-term,,,,37.1,27.3,10.7,24.9
|
diagramcontent.md
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
# Project Module Block Diagrams
|
| 2 |
+
|
| 3 |
+
## Overall Project
|
| 4 |
+
|
| 5 |
+
```mermaid
|
| 6 |
+
graph LR
|
| 7 |
+
A[admin.py] --> B(auth.py: get_current_user, get_current_active_user)
|
| 8 |
+
A --> C(crud.py: get_user, get_users)
|
| 9 |
+
B --> D(database.py: get_db)
|
| 10 |
+
C --> D
|
| 11 |
+
E[main.py] --> B
|
| 12 |
+
E --> C
|
| 13 |
+
E --> H[user.py]
|
| 14 |
+
F(models.py: User, UserDataInput) --> D
|
| 15 |
+
G(schemas.py: UserCreate, User) --> F
|
| 16 |
+
H --> B
|
| 17 |
+
H --> C
|
| 18 |
+
I[services: chatbot_service.py, data_service.py] --> E
|
| 19 |
+
J[ml_models] --> I
|
| 20 |
+
subgraph app
|
| 21 |
+
A
|
| 22 |
+
B
|
| 23 |
+
C
|
| 24 |
+
D
|
| 25 |
+
E
|
| 26 |
+
F
|
| 27 |
+
G
|
| 28 |
+
H
|
| 29 |
+
I
|
| 30 |
+
J
|
| 31 |
+
end
|
| 32 |
+
```
|
| 33 |
+
|
| 34 |
+
## Admin Side
|
| 35 |
+
|
| 36 |
+
```mermaid
|
| 37 |
+
graph LR
|
| 38 |
+
A[admin.py: admin_dashboard_shell, admin_view_user_details_shell] --> B(auth.py: get_current_admin_user)
|
| 39 |
+
A --> C(crud.py: get_user, get_users)
|
| 40 |
+
C --> D(database.py: get_db)
|
| 41 |
+
B --> D
|
| 42 |
+
style A fill:#f9f,stroke:#333,stroke-width:2px
|
| 43 |
+
```
|
| 44 |
+
|
| 45 |
+
## User Side
|
| 46 |
+
|
| 47 |
+
```mermaid
|
| 48 |
+
graph LR
|
| 49 |
+
H[user.py: user_homepage, chatbot_page, recommendations_page] --> B(auth.py: get_current_active_user)
|
| 50 |
+
H --> C(crud.py: get_user_data_inputs_by_user_id)
|
| 51 |
+
C --> D(database.py: get_db)
|
| 52 |
+
B --> D
|
| 53 |
+
E[main.py: api_chatbot_interact] --> H
|
| 54 |
+
style H fill:#ccf,stroke:#333,stroke-width:2px
|