diff --git a/Reference/aboutdata.txt b/Reference/aboutdata.txt new file mode 100644 index 0000000000000000000000000000000000000000..374d352f4926e0d5e9cf20fad4ca02ce6f859da2 --- /dev/null +++ b/Reference/aboutdata.txt @@ -0,0 +1,118 @@ +Columns in the dataset: +['Name', 'Age', 'City', 'Profession', 'Salary', 'Expenses', 'Savings', 'Lifecycle Stage', 'Risk Appetite', 'Investment Horizon', 'Equity (%)', 'Debt (%)', 'Gold (%)', 'FD/Cash (%)'] + +Preview of the data: + Name Age City Profession Salary Expenses Savings \ +0 Rohan Sharma 28 Mumbai Software Engineer 85000 65000 20000 +1 Priya Patel 45 Delhi Doctor 220000 150000 70000 +2 Arjun Singh 60 Bangalore Retired Banker 45000 40000 5000 +3 Anika Reddy 22 Hyderabad Student 15000 14000 1000 +4 Vikram Mehta 35 Pune Marketing Manager 140000 100000 40000 + + Lifecycle Stage Risk Appetite Investment Horizon Equity (%) Debt (%) \ +0 Early Career Medium Long-term 45 25 +1 Mid-Career Medium Medium-term 40 30 +2 Retired Low Short-term 10 40 +3 Student Low Short-term 0 30 +4 Early Career High Long-term 70 15 + + Gold (%) FD/Cash (%) +0 10 20 +1 10 20 +2 15 35 +3 5 65 +4 5 10 + +Data types: +Name object +Age int64 +City object +Profession object +Salary int64 +Expenses int64 +Savings int64 +Lifecycle Stage object +Risk Appetite object +Investment Horizon object +Equity (%) int64 +Debt (%) int64 +Gold (%) int64 +FD/Cash (%) int64 +dtype: object + +Unique values in 'City': +['Mumbai' 'Delhi' 'Bangalore' 'Hyderabad' 'Pune' 'Chennai' 'Jaipur' + 'Kochi' 'Kolkata' 'Ahmedabad' 'Gurgaon' 'Lucknow' 'Nagpur' 'Chandigarh' + 'Surat' 'Indore' 'Bhopal'] + +Unique values in 'Profession': +['Software Engineer' 'Doctor' 'Retired Banker' 'Student' + 'Marketing Manager' 'Teacher' 'Freelancer' 'Architect' 'Business Owner' + 'Nurse' 'Product Manager' 'CA' 'Data Analyst' 'Retired Professor' + 'Journalist' 'Graphic Designer' 'Sales Executive' 'HR Manager' 'Intern' + 'Dentist' 'Lawyer' 'Content Creator' 'Pensioner' 'UX Designer' + 'Government Employee' 'Fashion Designer' 'Startup Founder' + 'Homemaker (Investor)' 'Investment Banker' 'IT Consultant' + 'College Student' 'Pharmacist' 'Textile Business Owner' 'Event Planner' + 'Film Producer' 'Nutritionist' 'Retired Army Officer' + 'Social Media Manager' 'Airline Pilot' 'Biotech Researcher' + 'Real Estate Agent' 'AI Engineer' 'Retired Teacher' 'Financial Analyst' + 'Software Developer' 'NGO Director' 'Fitness Trainer' 'Digital Marketer' + 'Content Strategist' 'Retired Engineer' 'UI Developer' + 'Operations Manager' 'Corporate Lawyer' 'School Principal' + 'AI Researcher' 'Junior Doctor' 'Finance Manager' 'Fashion Blogger' + 'HR Consultant' 'Video Editor' 'Small Business Owner' 'Dietitian' + 'Supply Chain Manager' 'Interior Designer' 'Sales Manager' 'PR Executive' + 'Logistics Head' 'Content Writer' 'Retired Govt. Employee' + 'Marketing Lead' 'IT Manager' 'Startup Intern' 'Export Manager' + 'Fitness Instructor' 'Real Estate Broker' 'Event Manager' + 'Software Trainee' 'Data Scientist' 'HR Director' 'Hotel Manager' + 'Social Worker' 'Cybersecurity Expert' 'E-commerce Manager' + 'Bank Manager' 'Retired IT Manager' 'UX Researcher' 'Product Designer' + 'Cybersecurity Analyst' 'Podcast Producer' 'E-commerce Seller' + 'Cloud Architect' 'Corporate Trainer' 'AI Trainer' 'Supply Chain Head' + 'Product Owner' 'UI/UX Designer' 'Finance Director' + 'Sustainability Consultant' 'Retired Bank Manager' + 'Social Media Influencer' 'Blockchain Developer' 'NGO Head' + 'Data Engineer' 'Event Curator' 'CTO' 'Content Marketer' + 'Retired Army Major' 'AR/VR Developer' 'Logistics Manager' 'VP Sales' + 'EdTech Founder' 'SEO Specialist' 'Yoga Instructor' 'IT Director' + 'App Developer' 'Consultant Cardiologist' 'Freelance Writer' + 'Cloud Engineer' 'Retired CA' 'Digital Artist' 'Retired Pilot' + 'Graphic Animator' 'AI Ethicist' 'School Trustee' 'Robotics Engineer' + 'Fashion Stylist' 'Retired Nurse' 'AI Ethics Consultant' 'Drone Engineer' + 'Retired Bank Clerk' 'Digital Nomad' 'CFO' 'Sustainability Analyst' + 'Retired Journalist' 'Social Entrepreneur' 'DevOps Engineer' + 'School Counselor' 'Retired Army Colonel' 'AR Developer' 'Sales Director' + 'EdTech Consultant' 'SEO Expert' 'Cardiologist' '3D Artist' 'HR Head' + 'Animator' 'Fashion Influencer'] + +Unique values in 'Lifecycle Stage': +['Early Career' 'Mid-Career' 'Retired' 'Student' 'Late Career'] + +Unique values in 'Risk Appetite': +['Medium' 'Low' 'High'] + +Unique values in 'Investment Horizon': +['Long-term' 'Medium-term' 'Short-term'] + +Numerical columns summary: + Salary Expenses Savings Equity (%) Debt (%) \ +count 198.000000 198.000000 198.000000 198.000000 198.000000 +mean 115075.757576 84686.868687 30388.888889 39.469697 28.308081 +std 69388.842695 48499.494260 21425.713424 20.713089 11.534149 +min 8000.000000 7500.000000 500.000000 0.000000 10.000000 +25% 60000.000000 48000.000000 10000.000000 25.000000 20.000000 +50% 95000.000000 70000.000000 25000.000000 45.000000 25.000000 +75% 163750.000000 118750.000000 45000.000000 50.000000 30.000000 +max 310000.000000 230000.000000 90000.000000 70.000000 55.000000 + + Gold (%) FD/Cash (%) +count 198.000000 198.000000 +mean 8.434343 23.787879 +std 3.427751 16.426902 +min 0.000000 10.000000 +25% 5.000000 15.000000 +50% 10.000000 20.000000 +75% 10.000000 20.000000 +max 15.000000 80.000000 \ No newline at end of file diff --git a/Reference/basemodel.py b/Reference/basemodel.py new file mode 100644 index 0000000000000000000000000000000000000000..e77dbf1c6778f5066bcb576c021126f1c779a625 --- /dev/null +++ b/Reference/basemodel.py @@ -0,0 +1,46 @@ +import joblib + +# Load the basic model pipeline +pipeline = joblib.load('basic_portfolio_model.pkl') + +input_data = { + 'Salary': 150000, + 'Expenses': 100000, + 'Savings': 50000, + 'Lifecycle Stage': 'Mid-Career', + 'Risk Appetite': 'Medium', + 'Investment Horizon': 'Long-term' +} + +# Convert categorical features (FIXED SYNTAX) +input_data['Lifecycle Stage'] = pipeline['mappings']['lifecycle'][input_data['Lifecycle Stage']] # Added closing ] +input_data['Risk Appetite'] = pipeline['mappings']['risk'][input_data['Risk Appetite']] # Added closing ] +input_data['Investment Horizon'] = pipeline['mappings']['horizon'][input_data['Investment Horizon']] # Added closing ] + +# Create feature array in correct order +X = [ + input_data['Salary'], + input_data['Expenses'], + input_data['Savings'], + input_data['Lifecycle Stage'], + input_data['Risk Appetite'], + input_data['Investment Horizon'] +] + +# Scale and predict +X_scaled = pipeline['scaler'].transform([X]) +pred = pipeline['model'].predict(X_scaled)[0] + +# Normalize and format +total = pred.sum() +final_allocation = { + 'Equity': round((pred[0]/total)*100, 1), + 'Debt': round((pred[1]/total)*100, 1), + 'Gold': round((pred[2]/total)*100, 1), + 'FD/Cash': round((pred[3]/total)*100, 1) +} + +print("Recommended Portfolio:") +for asset, perc in final_allocation.items(): + print(f"{asset}: {perc}%") +print(f"Total: {sum(final_allocation.values())}%") \ No newline at end of file diff --git a/Reference/basic_portfolio_model.pkl b/Reference/basic_portfolio_model.pkl new file mode 100644 index 0000000000000000000000000000000000000000..5c8f03f07bf70bd370dbc2918d095f362ef17b0c --- /dev/null +++ b/Reference/basic_portfolio_model.pkl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae20f4cf2e1cb3f75623f692aa11f947c83529dbe6335a5634365052bd8d15ca +size 1817898 diff --git a/Reference/enhanced_portfolio_model.pkl b/Reference/enhanced_portfolio_model.pkl new file mode 100644 index 0000000000000000000000000000000000000000..18ac3c732becc718c8ee34b4ce3b7fdbc0710520 --- /dev/null +++ b/Reference/enhanced_portfolio_model.pkl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2560b79a9705ca3674cd96820fd31fd6799dde89b12d4815afc6887111b52746 +size 1691633 diff --git a/Reference/enhancedmodel.py b/Reference/enhancedmodel.py new file mode 100644 index 0000000000000000000000000000000000000000..f64294e588d1794b22bc5a90f67191d411fc4af2 --- /dev/null +++ b/Reference/enhancedmodel.py @@ -0,0 +1,56 @@ +import joblib +import pandas as pd + +# Load enhanced model pipeline +pipeline = joblib.load('enhanced_portfolio_model.pkl') + +# Sample input (MUST include Profession/City) +test_case = { + 'Profession': 'Software Engineer', + 'City': 'Mumbai', + 'Salary': 150000, + 'Expenses': 100000, + 'Savings': 50000, + 'Lifecycle Stage': 'Mid-Career', + 'Risk Appetite': 'Medium', + 'Investment Horizon': 'Long-term' +} + +# Convert categorical features +test_case['Profession'] = pipeline['profession_encoder'].transform([test_case['Profession']])[0] +test_case['City'] = pipeline['city_encoder'].transform([test_case['City']])[0] +test_case['Lifecycle Stage'] = pipeline['mappings']['lifecycle'][test_case['Lifecycle Stage']] +test_case['Risk Appetite'] = pipeline['mappings']['risk'][test_case['Risk Appetite']] +test_case['Investment Horizon'] = pipeline['mappings']['horizon'][test_case['Investment Horizon']] + +# Create feature array IN EXACT ORDER: +# ['Profession', 'City', 'Salary', 'Expenses', 'Savings', +# 'Lifecycle Stage', 'Risk Appetite', 'Investment Horizon'] +X = [ + test_case['Profession'], + test_case['City'], + test_case['Salary'], + test_case['Expenses'], + test_case['Savings'], + test_case['Lifecycle Stage'], + test_case['Risk Appetite'], + test_case['Investment Horizon'] +] + +# Scale and predict +X_scaled = pipeline['scaler'].transform([X]) +pred = pipeline['model'].predict(X_scaled)[0] + +# Normalize to 100% +total = pred.sum() +final_allocation = { + 'Equity': round((pred[0]/total)*100, 1), + 'Debt': round((pred[1]/total)*100, 1), + 'Gold': round((pred[2]/total)*100, 1), + 'FD/Cash': round((pred[3]/total)*100, 1) +} + +print("Enhanced Model Recommended Portfolio:") +for asset, perc in final_allocation.items(): + print(f"{asset}: {perc}%") +print(f"Total: {sum(final_allocation.values())}%") \ No newline at end of file diff --git a/Reference/rulebasedmodel.py b/Reference/rulebasedmodel.py new file mode 100644 index 0000000000000000000000000000000000000000..4c1b703f04ea3320f9f7f06979eb4645c6edcbe7 --- /dev/null +++ b/Reference/rulebasedmodel.py @@ -0,0 +1,314 @@ +import datetime +import math + +# --- Helper Functions (mostly unchanged) --- +def get_float(prompt): + """Helper to get a non-negative float from the user.""" + while True: + try: + val_str = input(prompt).strip() + if not val_str: + raise ValueError("Input cannot be empty.") + val = float(val_str) + if val < 0: + raise ValueError("Please enter a non-negative number.") + return val + except ValueError as e: + print(f"Invalid input: {e}") + +def get_int(prompt): + """Helper to get a non-negative integer from the user.""" + while True: + try: + val_str = input(prompt).strip() + if not val_str: + raise ValueError("Input cannot be empty.") + val = int(val_str) + if val < 0: + raise ValueError("Please enter a non-negative whole number.") + return val + except ValueError as e: + print(f"Invalid input: {e}") + +def yes_no(prompt): + """Helper to get a yes/no response.""" + while True: + resp = input(prompt + " (y/n): ").strip().lower() + if resp in ("y", "yes"): + return True + if resp in ("n", "no"): + return False + print("Please answer 'y' or 'n'.") + +# --- Simplified Profile and Financials Gathering --- + +def get_user_profile(): + """Gathers lifecycle stage, risk appetite, and investment horizon.""" + print("\n--- Let's Understand Your Investor Profile ---") + + # 1. Lifecycle Stage + print("\nWhich of these best describes your current life stage?") + lifecycle_options = { + "1": "Student (Focus: Learning, initial savings)", + "2": "Early Career (20s-early 30s, Focus: Growth, accumulation)", + "3": "Mid-Career (Mid 30s-late 40s, Focus: Balancing growth & responsibilities)", + "4": "Late Career/Pre-Retirement (50s+, Focus: Capital preservation, income generation)", + "5": "Retired (Focus: Sustainable income, capital preservation)" + } + for key, value in lifecycle_options.items(): + print(f"{key}. {value}") + while True: + choice = input("Enter number (1-5): ").strip() + if choice in lifecycle_options: + lifecycle_stage = lifecycle_options[choice].split('(')[0].strip() # Get "Student", "Early Career" etc. + break + else: + print("Invalid choice. Please enter a number between 1 and 5.") + + # 2. Risk Appetite + print("\nHow would you describe your risk tolerance for investments?") + risk_options = { + "1": "Low (Prefer safety and capital preservation, okay with lower returns)", + "2": "Medium (Willing to take some risk for moderate growth, comfortable with some fluctuations)", + "3": "High (Willing to take significant risk for potentially high returns, can handle large fluctuations)" + } + for key, value in risk_options.items(): + print(f"{key}. {value}") + while True: + choice = input("Enter number (1-3): ").strip() + if choice == "1": risk_appetite = "low"; break + elif choice == "2": risk_appetite = "medium"; break + elif choice == "3": risk_appetite = "high"; break + else: + print("Invalid choice. Please enter 1, 2, or 3.") + + # 3. Investment Horizon + print("\nWhat is your general investment time horizon for the majority of your savings?") + horizon_options = { + "1": "Short-term (Less than 3 years)", + "2": "Medium-term (3-7 years)", + "3": "Long-term (More than 7 years)" + } + for key, value in horizon_options.items(): + print(f"{key}. {value}") + while True: + choice = input("Enter number (1-3): ").strip() + if choice == "1": investment_horizon = "short-term"; break + elif choice == "2": investment_horizon = "medium-term"; break + elif choice == "3": investment_horizon = "long-term"; break + else: + print("Invalid choice. Please enter 1, 2, or 3.") + + return lifecycle_stage, risk_appetite, investment_horizon + +def get_financial_details(): + """Gets annual package, monthly in-hand salary, and total monthly expenses.""" + print("\n--- Let's Get Your Financial Details ---") + annual_package = get_float("What is your approximate annual salary package (CTC)? \u20B9 ") + + # Simplified monthly in-hand: Ask directly or provide a rough estimate and ask for correction + 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).") + monthly_in_hand_salary = get_float("What is your actual average monthly in-hand salary? \u20B9 ") + + total_monthly_expenses = get_float("What are your total estimated monthly expenses (all inclusive)? \u20B9 ") + + monthly_savings = monthly_in_hand_salary - total_monthly_expenses + + print(f"\nBased on your input:") + print(f" Monthly In-hand Salary: \u20B9 {monthly_in_hand_salary:.2f}") + print(f" Total Monthly Expenses: \u20B9 {total_monthly_expenses:.2f}") + if monthly_savings > 0: + print(f" Calculated Monthly Savings: \u20B9 {monthly_savings:.2f}") + else: + print(f" Calculated Monthly Deficit: \u20B9 {abs(monthly_savings):.2f}") + print(" It seems your expenses exceed your income. Please review your budget.") + + return monthly_in_hand_salary, total_monthly_expenses, monthly_savings + +# --- Simplified Investment Allocation Logic --- +def allocate_savings_simplified(savings, risk_profile, horizon, lifecycle): + """ + Determines investment allocation percentages based on simplified profile. + Returns a dictionary of { "Asset Class": percentage, ... } + """ + allocations = {} + justification_points = [ + f"This allocation considers your {risk_profile} risk profile, {horizon} horizon, and {lifecycle} stage." + ] + + # Prioritize Emergency Fund if not explicitly covered or if savings are first-time + # For simplicity, this version assumes user will manage EF separately or this is for surplus post-EF. + # A more robust version would inquire about EF status. + + if risk_profile == "low": + allocations = { + "Fixed Deposits / Recurring Deposits (Safety & Stability)": 0.35, + "Debt Mutual Funds (Liquid/Short Duration - Better than savings account returns)": 0.30, + "Gold (SGBs/ETFs - Inflation Hedge, Diversification)": 0.10, + "Equity MFs (Large Cap/Index Funds - Long-term inflation beating, low volatility equity)": 0.15, + "Cash / Savings Account (Immediate Liquidity)": 0.10 + } + justification_points.append("- Focus on capital preservation and stable returns.") + justification_points.append("- Suitable for short-term goals or very conservative investors.") + if horizon != "short-term": + justification_points.append("- Even with a longer horizon, a 'low' risk choice emphasizes safety above all.") + + + elif risk_profile == "medium": + allocations = { + "Equity MFs (Diversified - Large & Mid Cap/Flexi Cap SIPs for growth)": 0.45, + "Debt Mutual Funds (For stability and portfolio balance)": 0.25, + "Fixed Deposits / PPF (Secure, long-term component)": 0.10, + "Gold (SGBs/ETFs - Diversification)": 0.10, + "Equity MFs (International - For global diversification, if comfortable)": 0.05, # Optional + "Cash / Savings Account (Buffer)": 0.05 + } + justification_points.append("- Aims for a balance between growth (equity) and stability (debt, gold).") + justification_points.append("- Suitable for medium to long-term goals.") + if lifecycle in ["Early Career", "Student"] and horizon == "long-term": + justification_points.append("- Your stage and horizon allow for good equity exposure for wealth creation.") + # Could slightly increase equity for very young, long-term, medium risk. + # allocations["Equity MFs (Diversified - Large & Mid Cap/Flexi Cap SIPs for growth)"] = 0.50 + # allocations["Debt Mutual Funds (For stability and portfolio balance)"] = 0.20 + + elif risk_profile == "high": + allocations = { + "Equity MFs (Aggressive Growth - Mid/Small Cap, Thematic SIPs, with research)": 0.60, + "Equity MFs (International - Global growth opportunities)": 0.15, + "Direct Stocks (If experienced & well-researched, otherwise add to Equity MFs)": 0.10, # User needs to self-assess this. + "Debt Mutual Funds (Strategic, for some diversification)": 0.05, + "Gold (SGBs/ETFs - Tactical Diversification)": 0.05, + "Alternative Inv. (REITs/InvITs - very small, if understood & suitable, else to Equity)": 0.05 + } + justification_points.append("- Focuses on maximizing long-term growth potential, accepting higher volatility.") + justification_points.append("- Best suited for long-term goals and investors comfortable with significant market swings.") + if lifecycle not in ["Early Career", "Mid-Career"] and horizon == "long-term": + justification_points.append(f"- CAUTION: High risk at {lifecycle} stage needs careful consideration of your overall financial stability and nearness to needing funds.") + if horizon != "long-term": + justification_points.append(f"- CAUTION: High risk for a {horizon} horizon is generally not advisable. Ensure goals truly allow for this risk.") + + + # Normalize percentages to ensure they sum to 100% + current_total_percentage = sum(allocations.values()) + if abs(current_total_percentage - 1.0) > 0.001: # If not already 100% + factor = 1.0 / current_total_percentage + normalized_allocations = {k: v * factor for k, v in allocations.items()} + # Small check to ensure the largest component doesn't become negative if factor is weird (shouldn't happen with positive inputs) + # And ensure no tiny values are left that make no sense, e.g. less than 0.5% could be merged. + # For simplicity, we'll assume initial definitions are close enough. + final_allocations = {} + temp_sum = 0 + for asset, perc in normalized_allocations.items(): + # Round to sensible points e.g. 1 decimal for percentage display + # but use more precision for calculation + final_allocations[asset] = perc + temp_sum += perc + + # Final check and adjustment of the largest item if sum isn't perfect due to rounding during normalization. + if abs(temp_sum - 1.0) > 0.0001: + diff = 1.0 - temp_sum + if final_allocations: # Check if dict is not empty + # Find largest item to adjust + largest_item_key = max(final_allocations, key=final_allocations.get) + final_allocations[largest_item_key] += diff + allocations = final_allocations + + + return allocations, justification_points + +def display_investment_plan(monthly_savings, allocations, justification): + """Displays the final investment plan and justification.""" + print("\n\n--- Your Personalized Investment Allocation Plan ---") + print(f"Based on your monthly savings of \u20B9 {monthly_savings:.2f}:\n") + + print("Suggested Allocation:") + print("---------------------------------------------------------------------------") + print(f"{'Asset Class/Instrument':<50} | {'Percentage':>12} | {'Amount (₹)':>12}") + print("---------------------------------------------------------------------------") + if not allocations: # Should not happen if logic is correct + print("No specific allocation generated. Please review inputs or consult an advisor.") + return + + total_allocated_amount_check = 0 + for asset, percentage in allocations.items(): + amount = monthly_savings * percentage + total_allocated_amount_check += amount + print(f"{asset:<50} | {percentage*100:>11.1f}% | {amount:>11.2f}") + print("---------------------------------------------------------------------------") + print(f"{'TOTAL':<50} | {'100.0%':>12} | {total_allocated_amount_check:>11.2f}") + + + print("\nWhy these allocations?") + for point in justification: + print(f"- {point}") + print("\n- Learn more about general investing principles at: https://www.investopedia.com/financial-advisor/asset-allocation/\n" + " and for Indian context: https://www.amfiindia.com (Investor Education section)") + + +def provide_general_financial_tips(lifecycle_stage, monthly_savings, original_salary): + """Provides general financial tips.""" + print("\n--- General Financial Tips ---") + tips = [] + if monthly_savings <= 0 and original_salary > 0 : + 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.") + elif original_salary > 0: + savings_rate = (monthly_savings / original_salary) * 100 + tips.append(f"- Your current savings rate is approximately {savings_rate:.1f}%. Aim for at least 20-30% if possible, especially in accumulation stages.") + if savings_rate < 15: + tips.append("- Consider reviewing discretionary spending to boost your savings rate.") + + 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.") + 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.") + 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.") + tips.append("- Review Periodically: Revisit your financial plan and investments at least annually, or when major life events occur (marriage, new job, child, etc.).") + 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.") + + if lifecycle_stage == "Student" or lifecycle_stage == "Early Career": + tips.append("- Focus on upskilling and career growth to increase your earning potential. Your human capital is your biggest asset at this stage.") + if lifecycle_stage == "Late Career/Pre-Retirement" or lifecycle_stage == "Retired": + tips.append("- Plan for healthcare expenses in retirement. Consider if your current investments align with generating regular income if needed.") + + for tip in tips: + print(f"* {tip}") + +# --- Main Program Flow (Simplified) --- +def main_simplified(): + print("╔══════════════════════════════════════════════════════════════╗") + print("║ Simplified Monthly Savings Allocator ║") + print(f"║ Today's Date: {datetime.date.today().strftime('%Y-%m-%d')} ║") + print("╚══════════════════════════════════════════════════════════════╝") + print("\nWelcome! This tool will help you allocate your monthly savings.") + print("This is for educational purposes and NOT financial advice.\n") + + # 1. Get User Profile + lifecycle, risk, horizon = get_user_profile() + print(f"\nYour Profile Summary: Lifecycle: {lifecycle}, Risk: {risk.capitalize()}, Horizon: {horizon.capitalize()}") + + # 2. Get Financial Details + salary, expenses, savings = get_financial_details() + + if savings <= 0: + print("\nSince you don't have a monthly surplus, investment allocation is not applicable now.") + print("Focus on budgeting and increasing savings first.") + else: + # 3. Allocate Savings + allocations_dict, justification_list = allocate_savings_simplified(savings, risk, horizon, lifecycle) + + # 4. Display Plan + display_investment_plan(savings, allocations_dict, justification_list) + + # 5. Provide General Tips + provide_general_financial_tips(lifecycle, savings, salary) + + # 6. Disclaimer + print("\n\n--- Disclaimer ---") + print("The information and suggestions provided by this script are for general guidance and educational purposes ONLY.") + print("This does NOT constitute financial, investment, tax, or legal advice.") + print("Investment decisions involve risks. Past performance is not indicative of future results.") + print("Asset allocation models are generalized and may not be suitable for your individual circumstances.") + print("It is strongly recommended to consult with a SEBI-registered Investment Adviser or a qualified financial professional for personalized advice.") + + print("\nBudget calculation and investment suggestions complete. Plan wisely!\n") + +if __name__ == "__main__": + main_simplified() \ No newline at end of file diff --git a/app/__init__.py b/app/__init__.py new file mode 100644 index 0000000000000000000000000000000000000000..b9a1248313aabe9eba944a1a6a768d2d7380246c --- /dev/null +++ b/app/__init__.py @@ -0,0 +1 @@ +# This file makes the 'app' directory a Python package. diff --git a/app/__pycache__/__init__.cpython-312.pyc b/app/__pycache__/__init__.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..3fc9f753d9e09a4e7701d51756d93dd43122fa96 Binary files /dev/null and b/app/__pycache__/__init__.cpython-312.pyc differ diff --git a/app/__pycache__/admin.cpython-312.pyc b/app/__pycache__/admin.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..fc8f02c59f66260d8984ac3ac609e1fe5b4ec2f1 Binary files /dev/null and b/app/__pycache__/admin.cpython-312.pyc differ diff --git a/app/__pycache__/auth.cpython-312.pyc b/app/__pycache__/auth.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..0356022548ac880e30b66dfade573cae074d5bc4 Binary files /dev/null and b/app/__pycache__/auth.cpython-312.pyc differ diff --git a/app/__pycache__/crud.cpython-312.pyc b/app/__pycache__/crud.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..54525169f2a67e63b1d5c2051fa09ce26a6f2e22 Binary files /dev/null and b/app/__pycache__/crud.cpython-312.pyc differ diff --git a/app/__pycache__/database.cpython-312.pyc b/app/__pycache__/database.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c5ad2f1abad3a985cfd9f6e896db0b333e5533d3 Binary files /dev/null and b/app/__pycache__/database.cpython-312.pyc differ diff --git a/app/__pycache__/main.cpython-312.pyc b/app/__pycache__/main.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..8503ebf30a3291979abdd47b36795b39419e35bb Binary files /dev/null and b/app/__pycache__/main.cpython-312.pyc differ diff --git a/app/__pycache__/models.cpython-312.pyc b/app/__pycache__/models.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..399c1dbea2f089f129b51f66e2178e3ec844b1b9 Binary files /dev/null and b/app/__pycache__/models.cpython-312.pyc differ diff --git a/app/__pycache__/schemas.cpython-312.pyc b/app/__pycache__/schemas.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..39774fb9d7521ee07d07dc15f6e3a0a0086f42d6 Binary files /dev/null and b/app/__pycache__/schemas.cpython-312.pyc differ diff --git a/app/__pycache__/user.cpython-312.pyc b/app/__pycache__/user.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9d8240ec857c82acfb7ab304bbe76c440d39463a Binary files /dev/null and b/app/__pycache__/user.cpython-312.pyc differ diff --git a/app/admin.py b/app/admin.py new file mode 100644 index 0000000000000000000000000000000000000000..5273e0250db33bcd6e47be98d0a0770c62c46a6a --- /dev/null +++ b/app/admin.py @@ -0,0 +1,73 @@ +from fastapi import APIRouter, Request, Depends, HTTPException, Query +from fastapi.responses import HTMLResponse +from sqlalchemy.orm import Session +from typing import List + +from app import models, schemas, crud +from app.database import get_db +from app.auth import get_current_admin_user, get_current_active_user # Import both +from app.main import templates # Import templates from main.py + +router = APIRouter() + +# Route to serve the HTML shell for the dashboard +@router.get("/dashboard", response_class=HTMLResponse) +async def admin_dashboard_shell(request: Request): + # This route loads the page structure. + # Client-side JS will verify admin status and fetch data. + return templates.TemplateResponse("admin/dashboard.html", { + "request": request, + "title": "Admin Dashboard" + # No user or users_list passed here initially + }) + +# New API endpoint to fetch dashboard data (protected) +@router.get("/api/dashboard-data") +async def get_admin_dashboard_data( + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_admin_user), # Ensures only admin can access + search_query: str = Query(None, alias="search") +): + if search_query: + user = crud.get_user_by_email(db, email=search_query) + users_list = [schemas.User.from_orm(user)] if user else [] # Convert to schema + else: + users = crud.get_users(db, limit=100) + users_list = [schemas.User.from_orm(u) for u in users] # Convert list to schema + + # Return data needed by the dashboard template's JS + return {"users_list": users_list, "search_query": search_query, "admin_email": current_user.email} + + +# Route to view specific user details (protected) +# This serves an HTML page, so it will also need the client-side auth check pattern +@router.get("/users/{user_id}", response_class=HTMLResponse) +async def admin_view_user_details_shell(request: Request, user_id: int): + # Serve the shell page. JS will fetch details. + return templates.TemplateResponse("admin/user_details.html", { + "request": request, + "user_id": user_id, # Pass user_id for JS to use + "title": f"User Details" # Generic title initially + }) + +# New API endpoint to fetch specific user details (protected) +@router.get("/api/users/{user_id}") +async def get_admin_user_details_data( + user_id: int, + db: Session = Depends(get_db), + current_admin: models.User = Depends(get_current_admin_user) # Ensure admin access +): + target_user = crud.get_user(db, user_id=user_id) + if not target_user: + raise HTTPException(status_code=404, detail="User not found") + + user_inputs_orm = crud.get_user_data_inputs_by_user_id(db, user_id=user_id, limit=100) + + # Convert ORM objects to Pydantic schemas for JSON response + target_user_schema = schemas.User.from_orm(target_user) + user_inputs_schema = [schemas.UserDataInputResponse.from_orm(item) for item in user_inputs_orm] + + return { + "target_user": target_user_schema, + "user_inputs": user_inputs_schema + } diff --git a/app/auth.py b/app/auth.py new file mode 100644 index 0000000000000000000000000000000000000000..e93fe3c7a1d9ebafa9e966eeddae775766fd2a7f --- /dev/null +++ b/app/auth.py @@ -0,0 +1,99 @@ +from fastapi import APIRouter, Depends, HTTPException, status, Form +from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm +from sqlalchemy.orm import Session +from datetime import timedelta + +from . import crud, models, schemas +from .core import security +from .database import get_db + +router = APIRouter() + +oauth2_scheme = OAuth2PasswordBearer(tokenUrl="/auth/token") # Points to the token login endpoint + +ACCESS_TOKEN_EXPIRE_MINUTES = security.ACCESS_TOKEN_EXPIRE_MINUTES + +async def get_current_user(token: str = Depends(oauth2_scheme), db: Session = Depends(get_db)) -> models.User: + credentials_exception = HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Could not validate credentials", + headers={"WWW-Authenticate": "Bearer"}, + ) + payload = security.decode_access_token(token) + if payload is None: + raise credentials_exception + email: str = payload.get("sub") + if email is None: + raise credentials_exception + + user = crud.get_user_by_email(db, email=email) + if user is None: + raise credentials_exception + return user + +async def get_current_active_user(current_user: models.User = Depends(get_current_user)) -> models.User: + # Add any active/inactive checks here if needed + # if not current_user.is_active: + # raise HTTPException(status_code=400, detail="Inactive user") + return current_user + +async def get_current_admin_user(current_user: models.User = Depends(get_current_active_user)) -> models.User: + if not current_user.is_admin: + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail="The user doesn't have enough privileges" + ) + return current_user + + +@router.post("/signup", response_model=schemas.User) +async def signup_user(user: schemas.UserCreate, db: Session = Depends(get_db)): + db_user = crud.get_user_by_email(db, email=user.email) + if db_user: + raise HTTPException(status_code=400, detail="Email already registered") + if user.password != user.confirm_password: + raise HTTPException(status_code=400, detail="Passwords do not match") + return crud.create_user(db=db, user=user) + +@router.post("/token", response_model=schemas.Token) +async def login_for_access_token(form_data: OAuth2PasswordRequestForm = Depends(), db: Session = Depends(get_db)): + user = crud.get_user_by_email(db, email=form_data.username) # OAuth2 form uses 'username' for email + if not user or not security.verify_password(form_data.password, user.hashed_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = security.create_access_token( + data={"sub": user.email}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} + +# This is a utility endpoint, usually not directly called by frontend for login +# It's what OAuth2PasswordRequestForm uses internally. +# The actual login form should post to /auth/token. +@router.post("/login", response_model=schemas.Token) +async def login_user_form(email: str = Form(...), password: str = Form(...), db: Session = Depends(get_db)): + """ + Login endpoint that takes email and password from form data. + This is an alternative if not using OAuth2PasswordRequestForm directly in frontend JS. + Frontend forms would post to this. + """ + user = crud.get_user_by_email(db, email=email) + if not user or not security.verify_password(password, user.hashed_password): + raise HTTPException( + status_code=status.HTTP_401_UNAUTHORIZED, + detail="Incorrect email or password", + headers={"WWW-Authenticate": "Bearer"}, + ) + access_token_expires = timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + access_token = security.create_access_token( + data={"sub": user.email}, expires_delta=access_token_expires + ) + return {"access_token": access_token, "token_type": "bearer"} + + +@router.get("/users/me", response_model=schemas.User) +async def read_users_me(current_user: models.User = Depends(get_current_active_user)): + return current_user diff --git a/app/core/__pycache__/security.cpython-312.pyc b/app/core/__pycache__/security.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..9a52e3af1446a3ce2cb0d86aea2011f200d21366 Binary files /dev/null and b/app/core/__pycache__/security.cpython-312.pyc differ diff --git a/app/core/security.py b/app/core/security.py new file mode 100644 index 0000000000000000000000000000000000000000..08aebe8ec7865c6caf0a119809988134648ba8d2 --- /dev/null +++ b/app/core/security.py @@ -0,0 +1,40 @@ +from datetime import datetime, timedelta +from typing import Optional +from jose import JWTError, jwt +from passlib.context import CryptContext +from pydantic import BaseModel + +# Password Hashing +pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") + +# JWT Configuration (Consider moving to a .env file or config management) +SECRET_KEY = "your-super-secret-key" # CHANGE THIS IN PRODUCTION! +ALGORITHM = "HS256" +ACCESS_TOKEN_EXPIRE_MINUTES = 30 # Token validity period + +def verify_password(plain_password: str, hashed_password: str) -> bool: + return pwd_context.verify(plain_password, hashed_password) + +def get_password_hash(password: str) -> str: + return pwd_context.hash(password) + +def create_access_token(data: dict, expires_delta: Optional[timedelta] = None) -> str: + to_encode = data.copy() + if expires_delta: + expire = datetime.utcnow() + expires_delta + else: + expire = datetime.utcnow() + timedelta(minutes=ACCESS_TOKEN_EXPIRE_MINUTES) + to_encode.update({"exp": expire}) + encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM) + return encoded_jwt + +def decode_access_token(token: str) -> Optional[dict]: + try: + payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM]) + return payload + except JWTError: + return None + +class TokenPayload(BaseModel): + sub: Optional[str] = None # 'sub' is standard for subject (e.g., user email or ID) + exp: Optional[datetime] = None diff --git a/app/crud.py b/app/crud.py new file mode 100644 index 0000000000000000000000000000000000000000..c84ec20b6d871cf1dffa425170420c569b73ddc3 --- /dev/null +++ b/app/crud.py @@ -0,0 +1,48 @@ +from sqlalchemy.orm import Session +from . import models, schemas +from .core.security import get_password_hash +from typing import List, Optional + +# --- User CRUD --- +def get_user(db: Session, user_id: int) -> Optional[models.User]: + return db.query(models.User).filter(models.User.id == user_id).first() + +def get_user_by_email(db: Session, email: str) -> Optional[models.User]: + return db.query(models.User).filter(models.User.email == email).first() + +def get_users(db: Session, skip: int = 0, limit: int = 100) -> List[models.User]: + return db.query(models.User).offset(skip).limit(limit).all() + +def create_user(db: Session, user: schemas.UserCreate) -> models.User: + hashed_password = get_password_hash(user.password) + db_user = models.User(email=user.email, hashed_password=hashed_password) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + +def create_admin_user(db: Session, user: schemas.UserCreate) -> models.User: + hashed_password = get_password_hash(user.password) + db_user = models.User(email=user.email, hashed_password=hashed_password, is_admin=True) + db.add(db_user) + db.commit() + db.refresh(db_user) + return db_user + +# --- UserDataInput CRUD --- +def create_user_data_input(db: Session, item: schemas.UserDataInputCreate, user_id: int) -> models.UserDataInput: + db_item = models.UserDataInput(**item.dict(), user_id=user_id) + db.add(db_item) + db.commit() + db.refresh(db_item) + return db_item + +def get_user_data_inputs_by_user_id(db: Session, user_id: int, skip: int = 0, limit: int = 100) -> List[models.UserDataInput]: + return db.query(models.UserDataInput).filter(models.UserDataInput.user_id == user_id).offset(skip).limit(limit).all() + +def get_all_user_data_inputs(db: Session, skip: int = 0, limit: int = 1000) -> List[models.UserDataInput]: + """ + Fetches all user data inputs, primarily for admin use. + Consider pagination for very large datasets. + """ + return db.query(models.UserDataInput).order_by(models.UserDataInput.timestamp.desc()).offset(skip).limit(limit).all() diff --git a/app/database.py b/app/database.py new file mode 100644 index 0000000000000000000000000000000000000000..f40aad0da5a40db4d6668c53f7c8f5f48d36e85b --- /dev/null +++ b/app/database.py @@ -0,0 +1,26 @@ +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +import os + +DATABASE_URL = "sqlite:///./app/ml_models/financial_advisor.db" + +# Create the directory for the database if it doesn't exist +os.makedirs(os.path.dirname(DATABASE_URL.split("///")[-1]), exist_ok=True) + +engine = create_engine( + DATABASE_URL, connect_args={"check_same_thread": False} +) +SessionLocal = sessionmaker(autocommit=False, autoflush=False, bind=engine) + +Base = declarative_base() + +def get_db(): + db = SessionLocal() + try: + yield db + finally: + db.close() + +def create_db_and_tables(): + Base.metadata.create_all(bind=engine) diff --git a/app/main.py b/app/main.py new file mode 100644 index 0000000000000000000000000000000000000000..fde1f026c14a915e1671efcd79330c7abb4bae63 --- /dev/null +++ b/app/main.py @@ -0,0 +1,146 @@ +from fastapi import FastAPI, Request, Depends, HTTPException, status +from fastapi.staticfiles import StaticFiles +from fastapi.templating import Jinja2Templates +from fastapi.responses import HTMLResponse, RedirectResponse +from pathlib import Path +from sqlalchemy.orm import Session + +from app import models, schemas, crud, auth # app. is used if main.py is outside app/ +from app.database import engine, get_db, create_db_and_tables +from app.services import chatbot_service, data_service # app.services +from app.auth import get_current_active_user, get_current_admin_user, oauth2_scheme # app.auth + +# Determine the base directory of the 'app' package +BASE_DIR = Path(__file__).resolve().parent + +app = FastAPI(title="Financial Advisor Chatbot") + +# Mount static files (CSS, JS, images) +# The path "static" here is relative to where main.py is. +# If main.py is in app/, then app/static/ +app.mount("/static", StaticFiles(directory=BASE_DIR / "static"), name="static") + +# Setup Jinja2 templates +# If main.py is in app/, then app/templates/ +templates = Jinja2Templates(directory=BASE_DIR / "templates") + +# --- Event Handlers --- +@app.on_event("startup") +async def startup_event(): + create_db_and_tables() + # Initialize CSV file with headers if it doesn't exist + data_service.initialize_csv() + # Load ML models + chatbot_service.load_models() + # Create a default admin user if one doesn't exist (optional) + db = next(get_db()) # Get a DB session + try: + admin_email = "admin@example.com" + admin_password = "adminpassword" # Change this! + + admin_user_obj = crud.get_user_by_email(db, email=admin_email) + + if not admin_user_obj: + print(f"Admin user '{admin_email}' not found, creating...") + admin_user_schema = schemas.UserCreate(email=admin_email, password=admin_password, confirm_password=admin_password) + crud.create_admin_user(db, admin_user_schema) + print(f"Default admin user '{admin_email}' created with password '{admin_password}'. PLEASE CHANGE THE PASSWORD.") + elif not admin_user_obj.is_admin: + # If user exists but is not admin, update them (or log warning) + print(f"User '{admin_email}' exists but is not admin. Updating status...") + admin_user_obj.is_admin = True + # If you also want to ensure the password is set, you might reset it here: + # admin_user_obj.hashed_password = security.get_password_hash(admin_password) + db.add(admin_user_obj) # Add the existing object to the session to track changes + db.commit() + print(f"User '{admin_email}' updated to be an admin.") + else: + print(f"Admin user '{admin_email}' already exists.") + + except Exception as e: + print(f"Error during admin user setup: {e}") + finally: + db.close() + + +# --- Include Routers --- +# Assuming auth.py, user.py, admin.py are in the same directory as main.py (i.e. in 'app') +# If main.py is in the root, these would be from app.auth, app.user, app.admin +from . import auth as auth_router # Renaming to avoid conflict with 'auth' module +# We will create user_router and admin_router later +from . import user as user_router +from . import admin as admin_router + +app.include_router(auth_router.router, prefix="/auth", tags=["Authentication"]) +app.include_router(user_router.router, prefix="/user", tags=["User Pages"]) # User specific pages like /user/home +app.include_router(admin_router.router, prefix="/admin", tags=["Admin Pages"]) # Admin specific pages like /admin/dashboard + +# --- Root and Basic Page Routes --- +@app.get("/", response_class=HTMLResponse) +async def read_root(request: Request): + # Redirect to login page by default, or to home if logged in (more complex logic for latter) + return templates.TemplateResponse("auth/login.html", {"request": request, "title": "Login"}) + +@app.get("/signup-page", response_class=HTMLResponse) # Renamed to avoid conflict if auth router has /signup +async def signup_page_render(request: Request): + return templates.TemplateResponse("auth/signup.html", {"request": request, "title": "Sign Up"}) + +@app.get("/login-page", response_class=HTMLResponse) # Renamed to avoid conflict if auth router has /login +async def login_page_render(request: Request): + return templates.TemplateResponse("auth/login.html", {"request": request, "title": "Login"}) + +# The actual /home and /admin/dashboard routes are now in user.py and admin.py respectively. + +# --- Chatbot API endpoint --- +@app.post("/api/chatbot", response_model=schemas.ChatbotInteractionResponse) +async def api_chatbot_interact( + request_data: schemas.ChatbotInteractionRequest, + db: Session = Depends(get_db), + current_user: models.User = Depends(get_current_active_user) +): + # Process the interaction using the chatbot service + response = chatbot_service.process_chatbot_interaction(request_data) + + # If no error in recommendation, save input and potentially output to DB and CSV + if "error" not in response.recommendation: + # Save to DB + user_input_db = schemas.UserDataInputCreate( + model_type=request_data.model_type, + input_data=request_data.inputs + # output_data=response.recommendation # Optionally store output + ) + crud.create_user_data_input(db=db, item=user_input_db, user_id=current_user.id) + + # Save to CSV + data_service.append_data_to_csv( + user_id=current_user.id, + user_email=current_user.email, + model_type=request_data.model_type, + input_data=request_data.inputs, + output_data=response.recommendation # Pass the allocation part + ) + + return response + +# --- Logout --- +@app.get("/logout") +async def logout(request: Request): + # For token-based auth, logout is primarily a client-side operation (deleting the token). + # Server-side, you might blacklist the token if using a more complex setup. + # Here, we'll just redirect to the root path which serves the login page. + response = RedirectResponse(url="/", status_code=status.HTTP_302_FOUND) # Changed url to "/" + # Instruct browser to clear any relevant cookies if they were set by server (not typical for Bearer tokens) + # response.delete_cookie("access_token_cookie") # If you were setting it as a cookie + return response + + +# To run the app (example, usually done with uvicorn command line): +# if __name__ == "__main__": +# import uvicorn +# # Note: Uvicorn should typically be run from the project root, not from within app/ +# # e.g., uvicorn app.main:app --reload +# # For direct execution from app/main.py (less common for FastAPI projects): +# # uvicorn.run(app, host="0.0.0.0", port=8000) +# # Better to run with: uvicorn financial_advisor.app.main:app --reload from the project root +# # Or if main.py is in the root: uvicorn main:app --reload +# pass diff --git a/app/ml_models/basic_portfolio_model.pkl b/app/ml_models/basic_portfolio_model.pkl new file mode 100644 index 0000000000000000000000000000000000000000..5c8f03f07bf70bd370dbc2918d095f362ef17b0c --- /dev/null +++ b/app/ml_models/basic_portfolio_model.pkl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:ae20f4cf2e1cb3f75623f692aa11f947c83529dbe6335a5634365052bd8d15ca +size 1817898 diff --git a/app/ml_models/enhanced_portfolio_model.pkl b/app/ml_models/enhanced_portfolio_model.pkl new file mode 100644 index 0000000000000000000000000000000000000000..a310f99af511bd22ac8f88312ff076de1b4d796e --- /dev/null +++ b/app/ml_models/enhanced_portfolio_model.pkl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:a780fe27d28ffee39b771925d3746551b30417e09d513f1899a38091e607e5d0 +size 1738333 diff --git a/app/ml_models/enhanced_portfolio_model111.pkl b/app/ml_models/enhanced_portfolio_model111.pkl new file mode 100644 index 0000000000000000000000000000000000000000..18ac3c732becc718c8ee34b4ce3b7fdbc0710520 --- /dev/null +++ b/app/ml_models/enhanced_portfolio_model111.pkl @@ -0,0 +1,3 @@ +version https://git-lfs.github.com/spec/v1 +oid sha256:2560b79a9705ca3674cd96820fd31fd6799dde89b12d4815afc6887111b52746 +size 1691633 diff --git a/app/ml_models/financial_advisor.db b/app/ml_models/financial_advisor.db new file mode 100644 index 0000000000000000000000000000000000000000..6cfbdfb56225914156014cf752f592d9e77e98d5 Binary files /dev/null and b/app/ml_models/financial_advisor.db differ diff --git a/app/models.py b/app/models.py new file mode 100644 index 0000000000000000000000000000000000000000..6b75388a603d28fd88a0cac72d3fd15199850de8 --- /dev/null +++ b/app/models.py @@ -0,0 +1,28 @@ +from sqlalchemy import Column, Integer, String, Boolean, DateTime, JSON, ForeignKey +from sqlalchemy.orm import relationship +from sqlalchemy.sql import func +from .database import Base + +class User(Base): + __tablename__ = "users" + + id = Column(Integer, primary_key=True, index=True) + email = Column(String, unique=True, index=True, nullable=False) + hashed_password = Column(String, nullable=False) + is_admin = Column(Boolean, default=False) + created_at = Column(DateTime(timezone=True), server_default=func.now()) + + inputs = relationship("UserDataInput", back_populates="owner") + +class UserDataInput(Base): + __tablename__ = "user_data_inputs" + + id = Column(Integer, primary_key=True, index=True) + user_id = Column(Integer, ForeignKey("users.id")) + model_type = Column(String, index=True) # "base", "enhanced", "rule_based" + input_data = Column(JSON) # Stores the dictionary of inputs + # Output data can also be stored if needed, e.g., portfolio allocation + # output_data = Column(JSON) + timestamp = Column(DateTime(timezone=True), server_default=func.now()) + + owner = relationship("User", back_populates="inputs") diff --git a/app/schemas.py b/app/schemas.py new file mode 100644 index 0000000000000000000000000000000000000000..10cbd3063b38d38751b40e682c245b04c658b688 --- /dev/null +++ b/app/schemas.py @@ -0,0 +1,139 @@ +from pydantic import BaseModel, EmailStr, field_validator, model_validator, ValidationInfo +from typing import Optional, Dict, Any, List +from datetime import datetime + +# --- User Schemas --- +class UserBase(BaseModel): + email: EmailStr + +class UserCreate(UserBase): + password: str + confirm_password: Optional[str] = None # For signup form + + @model_validator(mode='after') + def passwords_match(self) -> 'UserCreate': + if self.password is not None and self.confirm_password is not None and self.password != self.confirm_password: + raise ValueError('Passwords do not match') + # Ensure confirm_password is not passed to the User model if it's just for validation + # However, our User model doesn't have confirm_password, so it's fine. + return self + +class User(UserBase): + id: int + is_admin: bool + created_at: datetime + + class Config: + from_attributes = True + +# --- UserDataInput Schemas --- +class UserDataInputBase(BaseModel): + model_type: str + input_data: Dict[str, Any] + +class UserDataInputCreate(UserDataInputBase): + pass + +class UserDataInputResponse(UserDataInputBase): + id: int + user_id: int + timestamp: datetime + # output_data: Optional[Dict[str, Any]] = None # If storing output + + class Config: + from_attributes = True + +class UserWithInputs(User): + inputs: List[UserDataInputResponse] = [] + + class Config: # Added for consistency, though User already has it. + from_attributes = True + + +# --- Token Schemas --- +class Token(BaseModel): + access_token: str + token_type: str + +class TokenData(BaseModel): + email: Optional[str] = None + + +# --- Chatbot Input Schemas (Specific to each model for validation) --- +# These can be more specific if needed, for now using a generic Dict +# For example, for Base Model: +class BaseModeInputSchema(BaseModel): + Salary: float + Expenses: float + Savings: float + Lifecycle_Stage: str + Risk_Appetite: str + Investment_Horizon: str + + @field_validator('Salary', 'Expenses', 'Savings') + @classmethod + def check_non_negative_numeric(cls, v: Any, info: ValidationInfo) -> Any: + if not isinstance(v, (int, float)): + raise ValueError(f"{info.field_name} must be a number") + if v < 0: + raise ValueError(f"{info.field_name} cannot be negative") + return v + + class Config: + from_attributes = True + + +class EnhancedModelInputSchema(BaseModel): + Profession: str + City: str + Salary: float + Expenses: float + Savings: float + Lifecycle_Stage: str + Risk_Appetite: str + Investment_Horizon: str + + @field_validator('Salary', 'Expenses', 'Savings') + @classmethod + def check_non_negative_numeric(cls, v: Any, info: ValidationInfo) -> Any: + if not isinstance(v, (int, float)): + raise ValueError(f"{info.field_name} must be a number") + if v < 0: + raise ValueError(f"{info.field_name} cannot be negative") + return v + + class Config: + from_attributes = True + +class RuleBasedModelInputSchema(BaseModel): + Lifecycle_Stage: str + Risk_Appetite: str + Investment_Horizon: str + Annual_Salary_Package: float + Monthly_In_hand_Salary: float + Total_Monthly_Expenses: float + + @field_validator('Annual_Salary_Package', 'Monthly_In_hand_Salary', 'Total_Monthly_Expenses') + @classmethod + def check_non_negative_numeric(cls, v: Any, info: ValidationInfo) -> Any: + if not isinstance(v, (int, float)): + raise ValueError(f"{info.field_name} must be a number") + if v < 0: + raise ValueError(f"{info.field_name} cannot be negative") + return v + + class Config: + from_attributes = True + +# For the chatbot interaction, the form will likely submit a dictionary. +# The specific schema can be used within the service layer before passing to the model. +class ChatbotInteractionRequest(BaseModel): + model_type: str # "base", "enhanced", "rule_based" + inputs: Dict[str, Any] + +class ChatbotInteractionResponse(BaseModel): + model_type: str + user_inputs: Dict[str, Any] + recommendation: Dict[str, Any] # e.g., portfolio allocation + justification: Optional[List[str]] = None # For rule-based + tips: Optional[List[str]] = None # For rule-based diff --git a/app/services/__pycache__/chatbot_service.cpython-312.pyc b/app/services/__pycache__/chatbot_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..c108c2c591ff3786bcaa338234191c23b202ccf8 Binary files /dev/null and b/app/services/__pycache__/chatbot_service.cpython-312.pyc differ diff --git a/app/services/__pycache__/data_service.cpython-312.pyc b/app/services/__pycache__/data_service.cpython-312.pyc new file mode 100644 index 0000000000000000000000000000000000000000..99012d7a547188fb083ac871ecccb67dc972d531 Binary files /dev/null and b/app/services/__pycache__/data_service.cpython-312.pyc differ diff --git a/app/services/chatbot_service.py b/app/services/chatbot_service.py new file mode 100644 index 0000000000000000000000000000000000000000..39f30d4fb0e30a5c18427f894a4697bf6b80512a --- /dev/null +++ b/app/services/chatbot_service.py @@ -0,0 +1,312 @@ +import joblib +import pandas as pd +import os +from typing import Dict, Any, Tuple, List, Optional +from app import schemas # Assuming schemas.py is in the same 'app' directory +import logging + +logger = logging.getLogger(__name__) + +# --- Model Loading --- +# Define paths to the model files, assuming they are in app/ml_models/ +MODEL_DIR = os.path.join(os.path.dirname(__file__), "..", "ml_models") # app/ml_models +BASE_MODEL_PATH = os.path.join(MODEL_DIR, "basic_portfolio_model.pkl") +ENHANCED_MODEL_PATH = os.path.join(MODEL_DIR, "enhanced_portfolio_model.pkl") + +# Ensure the ml_models directory exists +os.makedirs(MODEL_DIR, exist_ok=True) + +# Placeholder for loaded models +base_model_pipeline = None +enhanced_model_pipeline = None + +def load_models(): + global base_model_pipeline, enhanced_model_pipeline + try: + if os.path.exists(BASE_MODEL_PATH): + base_model_pipeline = joblib.load(BASE_MODEL_PATH) + logger.info("Base model loaded successfully.") + else: + logger.error(f"Base model file not found at {BASE_MODEL_PATH}") + + if os.path.exists(ENHANCED_MODEL_PATH): + enhanced_model_pipeline = joblib.load(ENHANCED_MODEL_PATH) + logger.info("Enhanced model loaded successfully.") + else: + logger.error(f"Enhanced model file not found at {ENHANCED_MODEL_PATH}") + except Exception as e: + logger.error(f"Error loading ML models: {e}") + +# Call load_models when this module is imported so they are ready. +# load_models() # Will be called from main.py or an init step + +# --- Base Model Prediction --- +def predict_base_model(input_data: schemas.BaseModeInputSchema) -> Dict[str, float]: + if not base_model_pipeline: + logger.error("Base model not loaded. Cannot predict.") + # Consider raising an exception or returning an error state + return {"error": "Base model not available"} + + try: + data = input_data.dict() + + # Convert categorical features using mappings from the loaded pipeline + # These mappings should exist in the .pkl file if saved correctly + processed_data = data.copy() + processed_data['Lifecycle_Stage'] = base_model_pipeline['mappings']['lifecycle'][data['Lifecycle_Stage']] + processed_data['Risk_Appetite'] = base_model_pipeline['mappings']['risk'][data['Risk_Appetite']] + processed_data['Investment_Horizon'] = base_model_pipeline['mappings']['horizon'][data['Investment_Horizon']] + + # Create feature array in correct order (must match training) + # Order from Reference/basemodel.py: Salary, Expenses, Savings, Lifecycle Stage, Risk Appetite, Investment Horizon + X = [ + processed_data['Salary'], + processed_data['Expenses'], + processed_data['Savings'], + processed_data['Lifecycle_Stage'], # This is now numerical + processed_data['Risk_Appetite'], # This is now numerical + processed_data['Investment_Horizon'] # This is now numerical + ] + + X_scaled = base_model_pipeline['scaler'].transform([X]) + pred = base_model_pipeline['model'].predict(X_scaled)[0] + + total = pred.sum() + if total == 0: # Avoid division by zero + return {'Equity': 0, 'Debt': 0, 'Gold': 0, 'FD/Cash': 0, "error": "Prediction resulted in zero total"} + + final_allocation = { + 'Equity': round((pred[0]/total)*100, 1), + 'Debt': round((pred[1]/total)*100, 1), + 'Gold': round((pred[2]/total)*100, 1), + 'FD/Cash': round((pred[3]/total)*100, 1) + } + return final_allocation + except KeyError as e: + logger.error(f"KeyError during base model prediction: {e}. Check mappings in pipeline or input data keys.") + return {"error": f"Missing data or incorrect mapping for {e}"} + except Exception as e: + logger.error(f"Error in base model prediction: {e}") + return {"error": str(e)} + + +# --- Enhanced Model Prediction --- +def predict_enhanced_model(input_data: schemas.EnhancedModelInputSchema) -> Dict[str, float]: + if not enhanced_model_pipeline: + logger.error("Enhanced model not loaded. Cannot predict.") + return {"error": "Enhanced model not available"} + + try: + data = input_data.dict() + processed_data = data.copy() + + # Convert categorical features + processed_data['Profession'] = enhanced_model_pipeline['profession_encoder'].transform([data['Profession']])[0] + processed_data['City'] = enhanced_model_pipeline['city_encoder'].transform([data['City']])[0] + processed_data['Lifecycle_Stage'] = enhanced_model_pipeline['mappings']['lifecycle'][data['Lifecycle_Stage']] + processed_data['Risk_Appetite'] = enhanced_model_pipeline['mappings']['risk'][data['Risk_Appetite']] + processed_data['Investment_Horizon'] = enhanced_model_pipeline['mappings']['horizon'][data['Investment_Horizon']] + + # Order from Reference/enhancedmodel.py: + # ['Profession', 'City', 'Salary', 'Expenses', 'Savings', 'Lifecycle Stage', 'Risk Appetite', 'Investment Horizon'] + X = [ + processed_data['Profession'], + processed_data['City'], + processed_data['Salary'], + processed_data['Expenses'], + processed_data['Savings'], + processed_data['Lifecycle_Stage'], + processed_data['Risk_Appetite'], + processed_data['Investment_Horizon'] + ] + + X_scaled = enhanced_model_pipeline['scaler'].transform([X]) + pred = enhanced_model_pipeline['model'].predict(X_scaled)[0] + + total = pred.sum() + if total == 0: + return {'Equity': 0, 'Debt': 0, 'Gold': 0, 'FD/Cash': 0, "error": "Prediction resulted in zero total"} + + final_allocation = { + 'Equity': round((pred[0]/total)*100, 1), + 'Debt': round((pred[1]/total)*100, 1), + 'Gold': round((pred[2]/total)*100, 1), + 'FD/Cash': round((pred[3]/total)*100, 1) + } + return final_allocation + except KeyError as e: + logger.error(f"KeyError during enhanced model prediction: {e}. Check mappings/encoders in pipeline or input data keys.") + return {"error": f"Missing data or incorrect mapping/encoding for {e}"} + except Exception as e: + logger.error(f"Error in enhanced model prediction: {e}") + return {"error": str(e)} + +# --- Rule-Based Model Logic --- +# (Adapted from Reference/rulebasedmodel.py) + +def _allocate_savings_rule_based( + monthly_savings: float, + risk_profile: str, + horizon: str, + lifecycle: str +) -> Tuple[Dict[str, float], List[str]]: + allocations = {} + justification_points = [ + f"This allocation considers your {risk_profile} risk profile, {horizon} horizon, and {lifecycle} stage." + ] + risk_profile = risk_profile.lower() # Ensure consistency + horizon = horizon.lower() + # Lifecycle stage is already in a good format from schema e.g. "Early Career" + + if risk_profile == "low": + allocations = { + "Fixed Deposits / Recurring Deposits": 0.35, + "Debt Mutual Funds (Liquid/Short Duration)": 0.30, + "Gold (SGBs/ETFs)": 0.10, + "Equity MFs (Large Cap/Index Funds)": 0.15, + "Cash / Savings Account": 0.10 + } + justification_points.append("- Focus on capital preservation and stable returns.") + elif risk_profile == "medium": + allocations = { + "Equity MFs (Diversified - Large & Mid Cap/Flexi Cap)": 0.45, + "Debt Mutual Funds": 0.25, + "Fixed Deposits / PPF": 0.10, + "Gold (SGBs/ETFs)": 0.10, + "Equity MFs (International)": 0.05, + "Cash / Savings Account": 0.05 + } + justification_points.append("- Aims for a balance between growth and stability.") + elif risk_profile == "high": + allocations = { + "Equity MFs (Aggressive Growth - Mid/Small Cap, Thematic)": 0.60, + "Equity MFs (International)": 0.15, + "Direct Stocks (If experienced)": 0.10, + "Debt Mutual Funds (Strategic)": 0.05, + "Gold (SGBs/ETFs - Tactical)": 0.05, + "Alternative Inv. (REITs/InvITs)": 0.05 + } + justification_points.append("- Focuses on maximizing long-term growth potential.") + + # Normalize percentages (simplified for brevity, original script had more robust normalization) + current_total_percentage = sum(allocations.values()) + if abs(current_total_percentage - 1.0) > 0.001 and current_total_percentage != 0: + factor = 1.0 / current_total_percentage + allocations = {k: round(v * factor, 3) for k, v in allocations.items()} + # Adjust last item to make sum exactly 1.0 if needed due to rounding + sum_val = sum(allocations.values()) + if sum_val != 1.0 and allocations: + key_to_adjust = list(allocations.keys())[-1] + allocations[key_to_adjust] += (1.0 - sum_val) + allocations[key_to_adjust] = round(allocations[key_to_adjust], 3) + + + # Convert percentage values to actual amounts based on monthly_savings + # The chatbot response schema expects percentages for recommendation, so we return percentages. + # The display logic in the template can calculate amounts. + + # For the ChatbotInteractionResponse, recommendation should be percentages + # Let's ensure the values are percentages (0 to 100) + final_percentage_allocations = {k: round(v * 100, 1) for k,v in allocations.items()} + + return final_percentage_allocations, justification_points + + +def _get_general_financial_tips(lifecycle_stage: str, monthly_savings: float, monthly_in_hand_salary: float) -> List[str]: + tips = [] + if monthly_in_hand_salary > 0: # Avoid division by zero if salary is 0 + if monthly_savings <= 0 : + tips.append("- Your expenses currently meet or exceed your income. Focus on creating a budget.") + else: + savings_rate = (monthly_savings / monthly_in_hand_salary) * 100 + tips.append(f"- Your current savings rate is approximately {savings_rate:.1f}%. Aim for at least 20-30%.") + else: # No salary info or zero salary + if monthly_savings <= 0: + tips.append("- Your expenses currently meet or exceed your income. Focus on creating a budget.") + + + tips.extend([ + "- Build & Maintain an Emergency Fund: Aim for 3-6 months of essential living expenses.", + "- Get Adequately Insured: Health and Term Life Insurance are crucial.", + "- Invest Regularly & Be Disciplined (e.g. SIPs).", + "- Review Periodically: Revisit your financial plan annually or on major life events.", + "- Understand Your Investments: Know the risks and costs." + ]) + if lifecycle_stage == "Student" or lifecycle_stage == "Early Career": + tips.append("- Focus on upskilling and career growth.") + if lifecycle_stage == "Late Career/Pre-Retirement" or lifecycle_stage == "Retired": # Assuming "Retired" is a lifecycle stage + tips.append("- Plan for healthcare expenses in retirement.") + return tips + +def predict_rule_based_model(input_data: schemas.RuleBasedModelInputSchema) -> Tuple[Dict[str, float], List[str], List[str]]: + data = input_data.dict() + + monthly_savings = data['Monthly_In_hand_Salary'] - data['Total_Monthly_Expenses'] + + if monthly_savings <= 0: + allocation = {"message": "Savings are zero or negative. Focus on budgeting first."} + justification = ["Investment allocation is not applicable without positive savings."] + tips = _get_general_financial_tips(data['Lifecycle_Stage'], monthly_savings, data['Monthly_In_hand_Salary']) + tips.insert(0, "Priority: Increase savings or reduce expenses.") + return allocation, justification, tips + + allocation, justification = _allocate_savings_rule_based( + monthly_savings, + data['Risk_Appetite'], + data['Investment_Horizon'], + data['Lifecycle_Stage'] + ) + tips = _get_general_financial_tips(data['Lifecycle_Stage'], monthly_savings, data['Monthly_In_hand_Salary']) + + return allocation, justification, tips + + +# --- Main Chatbot Interaction Logic --- +def process_chatbot_interaction( + request: schemas.ChatbotInteractionRequest +) -> schemas.ChatbotInteractionResponse: + + model_type = request.model_type.lower() + inputs = request.inputs + + recommendation: Dict[str, Any] = {} + justification: Optional[List[str]] = None + tips: Optional[List[str]] = None + + try: + if model_type == "base": + # Validate inputs against BaseModeInputSchema + validated_inputs = schemas.BaseModeInputSchema(**inputs) + recommendation = predict_base_model(validated_inputs) + elif model_type == "enhanced": + validated_inputs = schemas.EnhancedModelInputSchema(**inputs) + recommendation = predict_enhanced_model(validated_inputs) + elif model_type == "rule_based": + validated_inputs = schemas.RuleBasedModelInputSchema(**inputs) + recommendation, justification, tips = predict_rule_based_model(validated_inputs) + else: + raise ValueError("Invalid model type specified") + + # Check for errors from prediction functions + if "error" in recommendation: + # Propagate the error message + return schemas.ChatbotInteractionResponse( + model_type=model_type, + user_inputs=inputs, + recommendation=recommendation, # Contains the error message + justification=None, + tips=None + ) + + except Exception as e: # Catch validation errors or other issues + logger.error(f"Error processing chatbot interaction for {model_type}: {e}") + recommendation = {"error": f"Failed to process request: {str(e)}"} + + + return schemas.ChatbotInteractionResponse( + model_type=model_type, + user_inputs=inputs, # Return the original inputs for display + recommendation=recommendation, + justification=justification, + tips=tips + ) diff --git a/app/services/data_service.py b/app/services/data_service.py new file mode 100644 index 0000000000000000000000000000000000000000..3d645d0fd4e1cc21b43ae4edbad21fbdba4c1f93 --- /dev/null +++ b/app/services/data_service.py @@ -0,0 +1,129 @@ +import csv +import os +from datetime import datetime +from typing import Dict, Any, List, Optional +import logging + +# Configure logging +logging.basicConfig(level=logging.INFO) +logger = logging.getLogger(__name__) + +# Define the path for the CSV file +# Ensure this path is correct relative to where the application runs +# For example, if main.py is in app/, and this service is called from there. +CSV_FILE_PATH = "user_financial_data.csv" +# This will create the file in the same directory as main.py if it's in the root, +# or in the 'app' directory if main.py is in 'app' and this path is used as is. +# For consistency with the sqlite DB being in the root, let's adjust: +CSV_FILE_PATH = os.path.join(os.path.dirname(os.path.dirname(__file__)), "user_financial_data.csv") +# This places it in the project root directory (financial_advisor/) + +# Define the headers for the CSV file based on 'aboutdata.txt' and additional fields +# Ensure all possible fields from all models are covered. +CSV_HEADERS = [ + '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', # For Rule-based + # Output fields from models (optional, but good for record keeping) + 'Equity (%)', 'Debt (%)', 'Gold (%)', 'FD/Cash (%)' +] + + +def initialize_csv(): + """Initializes the CSV file with headers if it doesn't exist.""" + # Ensure the directory for the CSV file exists + os.makedirs(os.path.dirname(CSV_FILE_PATH), exist_ok=True) + + if not os.path.exists(CSV_FILE_PATH): + try: + with open(CSV_FILE_PATH, mode='w', newline='', encoding='utf-8') as file: + writer = csv.writer(file) + writer.writerow(CSV_HEADERS) + logger.info(f"CSV file initialized at {CSV_FILE_PATH}") + except IOError as e: + logger.error(f"Error initializing CSV file: {e}") + +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): + """ + Appends a new row of data to the CSV file. + input_data should be a flat dictionary. + output_data is the portfolio allocation. + """ + if not os.path.exists(CSV_FILE_PATH): + initialize_csv() + + row_data = {header: '' for header in CSV_HEADERS} # Initialize with empty strings + + # Populate common fields + row_data['UserID'] = user_id + row_data['UserEmail'] = user_email + row_data['ModelType'] = model_type + row_data['Timestamp'] = datetime.now().strftime("%Y-%m-%d %H:%M:%S") + + # Populate fields from input_data + # Normalize keys from input_data (e.g., 'Lifecycle_Stage' to 'Lifecycle Stage') + normalized_input_data = {key.replace('_', ' '): value for key, value in input_data.items()} + + for key, value in normalized_input_data.items(): + if key in row_data: + row_data[key] = value + # Handle specific keys that might have different names in input_data + elif key == "Annual Salary Package" and "Annual_Salary_Package" in normalized_input_data: # from RuleBasedModelInputSchema + row_data["Annual Salary Package"] = normalized_input_data["Annual_Salary_Package"] + # Add more specific mappings if needed + + # Populate fields from output_data (portfolio allocation) + if output_data: + for key, value in output_data.items(): + # Ensure the key from output_data matches a header like "Equity (%)" + header_key = f"{key} (%)" + if header_key in row_data: + row_data[header_key] = value + elif key in row_data: # For keys like 'Equity', 'Debt' if not with '(%)' + row_data[key] = value + + + # Ensure the order of data matches CSV_HEADERS + ordered_row_values = [row_data.get(header, '') for header in CSV_HEADERS] + + try: + with open(CSV_FILE_PATH, mode='a', newline='', encoding='utf-8') as file: + writer = csv.writer(file) + writer.writerow(ordered_row_values) + logger.info(f"Data appended to CSV for user {user_email}, model {model_type}") + except IOError as e: + logger.error(f"Error appending data to CSV: {e}") + except Exception as e: + logger.error(f"An unexpected error occurred while writing to CSV: {e}") + +# Example usage (for testing this module independently): +if __name__ == "__main__": + initialize_csv() + + # Test data + sample_input_base = { + 'Salary': 60000, 'Expenses': 40000, 'Savings': 20000, + 'Lifecycle_Stage': 'Early Career', 'Risk_Appetite': 'Medium', 'Investment_Horizon': 'Long-term' + } + sample_output = {'Equity': 50, 'Debt': 30, 'Gold': 10, 'FD/Cash': 10} + append_data_to_csv(1, "test@example.com", "base", sample_input_base, sample_output) + + sample_input_enhanced = { + 'Profession': 'Engineer', 'City': 'Metropolis', + 'Salary': 120000, 'Expenses': 70000, 'Savings': 50000, + 'Lifecycle_Stage': 'Mid-Career', 'Risk_Appetite': 'High', 'Investment_Horizon': 'Long-term' + } + append_data_to_csv(2, "another@example.com", "enhanced", sample_input_enhanced, sample_output) + + sample_input_rule = { + 'Lifecycle_Stage': 'Late Career', + 'Risk_Appetite': 'Low', + 'Investment_Horizon': 'Short-term', + 'Annual_Salary_Package': 1000000, + 'Monthly_In_hand_Salary': 60000, + 'Total_Monthly_Expenses': 40000 + } + append_data_to_csv(3, "ruleuser@example.com", "rule_based", sample_input_rule, sample_output) + + logger.info(f"Test data written to {CSV_FILE_PATH}") diff --git a/app/static/css/custom_styles.css b/app/static/css/custom_styles.css new file mode 100644 index 0000000000000000000000000000000000000000..e69de29bb2d1d6434b8b29ae775ad8c2e48c5391 diff --git a/app/static/css/style.css b/app/static/css/style.css new file mode 100644 index 0000000000000000000000000000000000000000..530419136356d3c4409d15aa6f2f7267417b3ce0 --- /dev/null +++ b/app/static/css/style.css @@ -0,0 +1,112 @@ +body { + font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; + background-color: #f8f9fa; /* Light grey background */ + display: flex; + flex-direction: column; + min-height: 100vh; +} + +.container { + flex: 1; +} + +.navbar { + box-shadow: 0 2px 4px rgba(0,0,0,.1); +} + +.card { + border: none; /* Softer look, rely on shadow */ + transition: transform .2s ease-in-out, box-shadow .2s ease-in-out; +} + +.card:hover { + transform: translateY(-5px); + box-shadow: 0 0.5rem 1rem rgba(0,0,0,.15) !important; +} + +.card-header { + font-weight: 500; +} + +.btn-primary { + background-color: #007bff; + border-color: #007bff; +} +.btn-primary:hover { + background-color: #0056b3; + border-color: #0056b3; +} + +.btn-success { + background-color: #28a745; + border-color: #28a745; +} +.btn-success:hover { + background-color: #1e7e34; + border-color: #1e7e34; +} + +.btn-info { + background-color: #17a2b8; + border-color: #17a2b8; +} +.btn-info:hover { + background-color: #117a8b; + border-color: #117a8b; +} + + +/* Specific to chatbot results for better visual separation */ +#recommendationDetails ul .list-group-item span.badge { + font-size: 0.9em; +} + +/* Footer styling */ +footer.bg-light { + background-color: #e9ecef !important; /* A slightly different shade for footer */ + padding-top: 1rem; + padding-bottom: 1rem; +} + +/* Responsive adjustments */ +@media (max-width: 768px) { + .display-4 { + font-size: 2.5rem; + } + h1 { + font-size: 1.75rem; + } +} + +/* Ensure body takes full height for sticky footer */ +html, body { + height: 100%; +} + +/* --- Chatbot Interface Styles --- */ +#chatbox { + height: 400px; + overflow-y: auto; + border: 1px solid #ccc; + padding: 10px; + margin-bottom: 15px; + background-color: #f9f9f9; + border-radius: 5px; +} +.chat-message { margin-bottom: 10px; } +.bot-message { text-align: left; } +.user-message { text-align: right; } +.message-bubble { + display: inline-block; + padding: 8px 12px; + border-radius: 15px; + max-width: 80%; + word-wrap: break-word; /* Ensure long words break */ +} +.bot-message .message-bubble { background-color: #e9ecef; color: #333; } +.user-message .message-bubble { background-color: #0d6efd; color: white; } +#userInputArea button.option-button { margin: 3px; } +#userInputArea { display: flex; gap: 10px; align-items: center; flex-wrap: wrap; } /* Added wrap */ +#userInputArea input, #userInputArea select { flex-grow: 1; min-width: 150px; } /* Added min-width */ +#optionsContainer { display: flex; flex-wrap: wrap; gap: 5px; } /* Styling for button container */ +.spinner-border-sm { width: 1rem; height: 1rem; } diff --git a/app/static/js/script.js b/app/static/js/script.js new file mode 100644 index 0000000000000000000000000000000000000000..7264fbb4435b78c5a3e9962537643d918617b78e --- /dev/null +++ b/app/static/js/script.js @@ -0,0 +1,61 @@ +// Global JavaScript functions can be added here. + +document.addEventListener('DOMContentLoaded', function () { + // Example: Add a class to the body to indicate JS is enabled + document.body.classList.add('js-enabled'); + + // You could centralize common functions here if needed, + // for example, a function to make API calls with error handling. + // const apiRequest = async (url, method, body = null, headers = {}) => { ... } + + const logoutButton = document.getElementById('logoutButton'); + if (logoutButton) { + logoutButton.addEventListener('click', function (event) { + event.preventDefault(); // Prevent default anchor action + + localStorage.removeItem('accessToken'); + localStorage.removeItem('tokenType'); + + // Redirect to the server's /logout endpoint, which then redirects to login page + // This ensures any server-side session cleanup (if implemented later) could also occur. + window.location.href = '/logout'; + }); + } + + // Client-side check to update navbar based on token presence + // This runs on every page load to set the correct initial state for navbar items. + const accessToken = localStorage.getItem('accessToken'); + const navWelcomeItem = document.getElementById('navWelcomeItem'); + const navLogoutItem = document.getElementById('navLogoutItem'); + const navLoginItem = document.getElementById('navLoginItem'); + const navSignupItem = document.getElementById('navSignupItem'); + + if (accessToken) { + // User is logged in (according to localStorage) + if (navWelcomeItem) navWelcomeItem.style.display = 'block'; // Or 'list-item' if needed + if (navLogoutItem) navLogoutItem.style.display = 'block'; + if (navLoginItem) navLoginItem.style.display = 'none'; + if (navSignupItem) navSignupItem.style.display = 'none'; + + // Optional: Fetch user email to display in welcome message + // Could be done here or within specific page scripts like homepage.html + // fetch('/auth/users/me', { headers: {'Authorization': `Bearer ${accessToken}`} }) + // .then(response => response.ok ? response.json() : Promise.reject('Failed')) + // .then(data => { + // const welcomeSpan = document.getElementById('navUserWelcome'); + // if (welcomeSpan) welcomeSpan.textContent = `Welcome, ${data.email}`; + // }) + // .catch(err => console.error("Error fetching user for navbar:", err)); + + } else { + // User is logged out + if (navWelcomeItem) navWelcomeItem.style.display = 'none'; + if (navLogoutItem) navLogoutItem.style.display = 'none'; + if (navLoginItem) navLoginItem.style.display = 'block'; + if (navSignupItem) navSignupItem.style.display = 'block'; + } +}); + +// Function to get CSRF token if using CSRF protection with forms not handled by FastAPI's default +// function getCookie(name) { ... } +// Not strictly needed for this JWT setup unless forms post directly without JS. diff --git a/app/templates/admin/dashboard.html b/app/templates/admin/dashboard.html new file mode 100644 index 0000000000000000000000000000000000000000..8815651b8c9be10763025f7ec28f6e3ca9451f46 --- /dev/null +++ b/app/templates/admin/dashboard.html @@ -0,0 +1,175 @@ +{% extends "partials/base.html" %} + +{% block title %}Admin Dashboard - Financial Advisor{% endblock %} + +{% block content %} +
+
+

Admin Dashboard

+ Welcome, Admin! {# Placeholder #} +
+ + + + + + + + + + + +
+{% endblock %} + +{% block scripts_extra %} + +{% endblock %} diff --git a/app/templates/admin/user_details.html b/app/templates/admin/user_details.html new file mode 100644 index 0000000000000000000000000000000000000000..fac136cd22f99a967f6ca9694444eb7a4c1817dd --- /dev/null +++ b/app/templates/admin/user_details.html @@ -0,0 +1,170 @@ +{% extends "partials/base.html" %} + +{% block title %}{{ title }} - Financial Advisor{% endblock %} + +{% block content %} +
+
+

User Details

{# Placeholder #} + « Back to Admin Dashboard +
+ + + + + + +
+{% endblock %} + +{% block scripts_extra %} + +{% endblock %} diff --git a/app/templates/auth/login.html b/app/templates/auth/login.html new file mode 100644 index 0000000000000000000000000000000000000000..74d4497f584961f57a2a926a2e9d7402cc2a7acc --- /dev/null +++ b/app/templates/auth/login.html @@ -0,0 +1,133 @@ +{% extends "partials/base.html" %} + +{% block title %}Login - Financial Advisor{% endblock %} + +{% block content %} +
+
+
+
+

Login

+ + {% if request.query_params.get("error") %} + + {% endif %} + {% if request.query_params.get("message") %} + + {% endif %} + +
+ +
+ + +
+
+ +
+ + +
+
+
+ +
+
+

+ Don't have an account? Sign up here +

+
+
+
+
+{% endblock %} + +{% block scripts_extra %} + +{% endblock %} diff --git a/app/templates/auth/signup.html b/app/templates/auth/signup.html new file mode 100644 index 0000000000000000000000000000000000000000..7ee59ba0a841230d053ece2b61f393fb7eaa22d3 --- /dev/null +++ b/app/templates/auth/signup.html @@ -0,0 +1,121 @@ +{% extends "partials/base.html" %} + +{% block title %}Sign Up - Financial Advisor{% endblock %} + +{% block content %} +
+
+
+
+

Create Account

+
+
+ + +
+
+ +
+ + +
+
+
+ +
+ + +
+
+
+ +
+
+

+ Already have an account? Login here +

+
+
+
+
+{% endblock %} + +{% block scripts_extra %} + +{% endblock %} diff --git a/app/templates/partials/base.html b/app/templates/partials/base.html new file mode 100644 index 0000000000000000000000000000000000000000..2439f02df27dafe28188e76268446c8deafcbd04 --- /dev/null +++ b/app/templates/partials/base.html @@ -0,0 +1,30 @@ + + + + + + {{ title }} - Financial Advisor + + + + + {% block head_extra %}{% endblock %} + + + {% include 'partials/navbar.html' %} + +
+ {# Removed get_flashed_messages block as it's not standard in FastAPI/Jinja2 alone #} + {# Specific pages can handle messages via query params or JS #} + {% block content %}{% endblock %} +
+ + {% include 'partials/footer.html' %} + + + + + + {% block scripts_extra %}{% endblock %} + + diff --git a/app/templates/partials/footer.html b/app/templates/partials/footer.html new file mode 100644 index 0000000000000000000000000000000000000000..133f845d63a0d253543d12dc3814be7fbcdaa16f --- /dev/null +++ b/app/templates/partials/footer.html @@ -0,0 +1,6 @@ + diff --git a/app/templates/partials/navbar.html b/app/templates/partials/navbar.html new file mode 100644 index 0000000000000000000000000000000000000000..c404404a8950e7d939441d3f1ccb0ce1b731e3ff --- /dev/null +++ b/app/templates/partials/navbar.html @@ -0,0 +1,44 @@ + diff --git a/app/templates/user/chatbot.html b/app/templates/user/chatbot.html new file mode 100644 index 0000000000000000000000000000000000000000..369bcbc28fe6bb6b289b4ed68baa7653f93ad537 --- /dev/null +++ b/app/templates/user/chatbot.html @@ -0,0 +1,362 @@ +{% extends "partials/base.html" %} + +{% block title %}{{ title }} - Chatbot{% endblock %} + +{% block head_extra %} +{# Chat styles moved to static/css/style.css #} +{% endblock %} + +{% block content %} +
+
+
+
+
+

{{ title }}

+ +
+
+
+ +
+ +
+

Click "Start" to begin the financial assessment.

+ +
+ + + + +
+
+ +
+
+
+ +{# Pass form fields data to JavaScript #} + +{% endblock %} + +{% block scripts_extra %} + +{% endblock %} diff --git a/app/templates/user/homepage.html b/app/templates/user/homepage.html new file mode 100644 index 0000000000000000000000000000000000000000..693253bd9f9fa044a073d23c53f0f15af8bf4aa4 --- /dev/null +++ b/app/templates/user/homepage.html @@ -0,0 +1,137 @@ +{% extends "partials/base.html" %} + +{% block title %}User Dashboard - Financial Advisor{% endblock %} + +{% block content %} +
+
+
+

Welcome, !

+

Choose a financial advisory model to get started.

+
+
+ + + + + + +
+{% endblock %} + +{% block scripts_extra %} + +{% endblock %} diff --git a/app/templates/user/recommendations.html b/app/templates/user/recommendations.html new file mode 100644 index 0000000000000000000000000000000000000000..1aa5846345e94cec2a6437860a51776cfd7d6df3 --- /dev/null +++ b/app/templates/user/recommendations.html @@ -0,0 +1,240 @@ +{% extends "partials/base.html" %} + +{% block title %}Market Trends & Recommendations - Financial Advisor{% endblock %} + +{% block content %} +
+
+
+

Market Trends & Recommendations

+

Stay updated with the latest market information and our curated recommendations.

+
+
+ + +
+
+
+
+

Current Stock Prices

+
+
+ +
+
    + +
+ + +
+
+
+
+
+

Gold Price

+
+
+ {% if market_data.gold_price %} +

{{ market_data.gold_price }}

+ {% else %} +

Gold price data not available.

+ {% endif %} +
+
+
+
+ + +
+
+
+
+

Recommended Stocks

+
+
+ {% if market_data.recommended_stocks %} + {% for stock in market_data.recommended_stocks %} +
+
{{ stock.name }}
+

{{ stock.reason }}

+
+ {% endfor %} + {% else %} +

No specific stock recommendations at this time.

+ {% endif %} +
+
+
+
+
+ « Back to Homepage +
+
+ + +
+
+
+
+
+

Fixed Deposit (FD) Interest Rates in India

+
+
+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
Bank NameHighest Interest RateTenureSenior Citizen Rate (Up to)
Airtel Bank9.1%36 to 60 monthsVaries (up to 9.1%)
SBI (State Bank of India)7.25%2 to 3 yearsUp to 7.40%
Axis Bank7.25%15 months to <2 yearsUp to 7.95%
Indian Post (Post Office)7.50%5 years (Post Office TD)Not specified
HDFC Bank7.20%4 years 7 months (55 months)Up to 7.75%
ICICI Bank7.10%15 months to 2 yearsUp to 7.60%
Canara Bank6.70%2 to 3 yearsUp to 7.20%
PNB (Punjab National Bank)7.05%300 daysUp to 7.55%
+
+
+
+
+
+ + +{% endblock %} diff --git a/app/tickers.txt b/app/tickers.txt new file mode 100644 index 0000000000000000000000000000000000000000..4c3e0b3edd3e221225db66060ab9fb5ee3b8861d --- /dev/null +++ b/app/tickers.txt @@ -0,0 +1,2099 @@ +20MICRONS.NS +21STCENMGM.NS +360ONE.NS +3IINFOLTD.NS +3MINDIA.NS +3PLAND.NS +5PAISA.NS +63MOONS.NS +A2ZINFRA.NS +AAATECH.NS +AADHARHFC.NS +AAKASH.NS +AAREYDRUGS.NS +AARON.NS +AARTECH.NS +AARTIDRUGS.NS +AARTIIND.NS +AARTIPHARM.NS +AARTISURF.NS +AARVEEDEN.NS +AARVI.NS +AAVAS.NS +ABAN.NS +ABB.NS +ABBOTINDIA.NS +ABCAPITAL.NS +ABDL.NS +ABFRL.NS +ABINFRA.NS +ABMINTLLTD.NS +ABREL.NS +ABSLAMC.NS +ACC.NS +ACCELYA.NS +ACCURACY.NS +ACE.NS +ACEINTEG.NS +ACI.NS +ACL.NS +ACLGATI.NS +ACMESOLAR.NS +ADANIENSOL.NS +ADANIENT.NS +ADANIGREEN.NS +ADANIPORTS.NS +ADANIPOWER.NS +ADFFOODS.NS +ADL.NS +ADOR.NS +ADROITINFO.NS +ADSL.NS +ADVANIHOTR.NS +ADVENZYMES.NS +AEGISLOG.NS +AEROFLEX.NS +AETHER.NS +AFCONS.NS +AFFLE.NS +AFFORDABLE.NS +AFIL.NS +AFSL.NS +AGARIND.NS +AGARWALEYE.NS +AGI.NS +AGIIL.NS +AGRITECH.NS +AGROPHOS.NS +AGSTRA.NS +AHLADA.NS +AHLEAST.NS +AHLUCONT.NS +AIAENG.NS +AIIL.NS +AIRAN.NS +AIROLAM.NS +AJANTPHARM.NS +AJAXENGG.NS +AJMERA.NS +AJOONI.NS +AKASH.NS +AKG.NS +AKI.NS +AKSHAR.NS +AKSHARCHEM.NS +AKSHOPTFBR.NS +AKUMS.NS +AKZOINDIA.NS +ALANKIT.NS +ALBERTDAVD.NS +ALEMBICLTD.NS +ALICON.NS +ALIVUS.NS +ALKALI.NS +ALKEM.NS +ALKYLAMINE.NS +ALLCARGO.NS +ALLDIGI.NS +ALMONDZ.NS +ALOKINDS.NS +ALPA.NS +ALPHAGEO.NS +ALPSINDUS.NS +AMBER.NS +AMBICAAGAR.NS +AMBIKCO.NS +AMBUJACEM.NS +AMDIND.NS +AMIORG.NS +AMJLAND.NS +AMNPLST.NS +AMRUTANJAN.NS +ANANDRATHI.NS +ANANTRAJ.NS +ANDHRAPAP.NS +ANDHRSUGAR.NS +ANGELONE.NS +ANIKINDS.NS +ANKITMETAL.NS +ANMOL.NS +ANSALAPI.NS +ANTGRAPHIC.NS +ANUHPHR.NS +ANUP.NS +ANURAS.NS +APARINDS.NS +APCL.NS +APCOTEXIND.NS +APEX.NS +APLAPOLLO.NS +APLLTD.NS +APOLLO.NS +APOLLOHOSP.NS +APOLLOPIPE.NS +APOLLOTYRE.NS +APOLSINHOT.NS +APTECHT.NS +APTUS.NS +ARCHIDPLY.NS +ARCHIES.NS +ARE&M.NS +ARENTERP.NS +ARIES.NS +ARIHANTCAP.NS +ARIHANTSUP.NS +ARKADE.NS +ARMANFIN.NS +AROGRANITE.NS +ARROWGREEN.NS +ARSHIYA.NS +ARSSINFRA.NS +ARTEMISMED.NS +ARTNIRMAN.NS +ARVEE.NS +ARVIND.NS +ARVINDFASN.NS +ARVSMART.NS +ASAHIINDIA.NS +ASAHISONG.NS +ASAL.NS +ASALCBR.NS +ASHAPURMIN.NS +ASHIANA.NS +ASHIMASYN.NS +ASHOKA.NS +ASHOKAMET.NS +ASHOKLEY.NS +ASIANENE.NS +ASIANHOTNR.NS +ASIANPAINT.NS +ASIANTILES.NS +ASKAUTOLTD.NS +ASMS.NS +ASPINWALL.NS +ASTEC.NS +ASTERDM.NS +ASTRAL.NS +ASTRAMICRO.NS +ASTRAZEN.NS +ASTRON.NS +ATALREAL.NS +ATAM.NS +ATGL.NS +ATHERENERG.NS +ATL.NS +ATLANTAA.NS +ATLASCYCLE.NS +ATUL.NS +ATULAUTO.NS +AUBANK.NS +AURIONPRO.NS +AUROPHARMA.NS +AURUM.NS +AUSOMENT.NS +AUTOAXLES.NS +AUTOIND.NS +AVADHSUGAR.NS +AVALON.NS +AVANTEL.NS +AVANTEL-RE.NS +AVANTIFEED.NS +AVG.NS +AVL.NS +AVONMORE.NS +AVROIND.NS +AVTNPL.NS +AWFIS.NS +AWHCL.NS +AWL.NS +AXISBANK.NS +AXISCADES.NS +AXITA.NS +AYMSYNTEX.NS +AZAD.NS +BAFNAPH.NS +BAGFILMS.NS +BAIDFIN.NS +BAJAJ-AUTO.NS +BAJAJCON.NS +BAJAJELEC.NS +BAJAJFINSV.NS +BAJAJHCARE.NS +BAJAJHFL.NS +BAJAJHIND.NS +BAJAJHLDNG.NS +BAJAJINDEF.NS +BAJEL.NS +BAJFINANCE.NS +BALAJEE.NS +BALAJITELE.NS +BALAMINES.NS +BALAXI.NS +BALKRISHNA.NS +BALKRISIND.NS +BALMLAWRIE.NS +BALPHARMA.NS +BALRAMCHIN.NS +BALUFORGE.NS +BANARBEADS.NS +BANARISUG.NS +BANCOINDIA.NS +BANDHANBNK.NS +BANG.NS +BANKA.NS +BANKBARODA.NS +BANKINDIA.NS +BANSALWIRE.NS +BANSWRAS.NS +BARBEQUE.NS +BASF.NS +BASML.NS +BASML-RE1.NS +BATAINDIA.NS +BAYERCROP.NS +BBL.NS +BBOX.NS +BBTC.NS +BBTCL.NS +BCLIND.NS +BCONCEPTS.NS +BDL.NS +BEARDSELL.NS +BECTORFOOD.NS +BEDMUTHA.NS +BEL.NS +BEML.NS +BEPL.NS +BERGEPAINT.NS +BESTAGRO.NS +BFINVEST.NS +BFUTILITIE.NS +BGRENERGY.NS +BHAGCHEM.NS +BHAGERIA.NS +BHAGYANGR.NS +BHANDARI.NS +BHARATFORG.NS +BHARATGEAR.NS +BHARATRAS.NS +BHARATSE.NS +BHARATWIRE.NS +BHARTIARTL.NS +BHARTIHEXA.NS +BHEL.NS +BIGBLOC.NS +BIKAJI.NS +BIL.NS +BINANIIND.NS +BIOCON.NS +BIOFILCHEM.NS +BIRLACABLE.NS +BIRLACORPN.NS +BIRLAMONEY.NS +BIRLANU.NS +BLACKBUCK.NS +BLAL.NS +BLBLIMITED.NS +BLISSGVS.NS +BLKASHYAP.NS +BLS.NS +BLSE.NS +BLUECOAST.NS +BLUEDART.NS +BLUEJET.NS +BLUESTARCO.NS +BODALCHEM.NS +BOHRAIND.NS +BOMDYEING.NS +BOROLTD.NS +BORORENEW.NS +BOROSCI.NS +BOSCHLTD.NS +BPCL.NS +BPL.NS +BRIGADE.NS +BRITANNIA.NS +BRNL.NS +BROOKS.NS +BSE.NS +BSHSL.NS +BSL.NS +BSOFT.NS +BTML.NS +BUTTERFLY.NS +BVCL.NS +BYKE.NS +CALSOFT.NS +CAMLINFINE.NS +CAMPUS.NS +CAMS.NS +CANBK.NS +CANFINHOME.NS +CANTABIL.NS +CAPACITE.NS +CAPITALSFB.NS +CAPLIPOINT.NS +CAPTRUST.NS +CARBORUNIV.NS +CARERATING.NS +CARRARO.NS +CARTRADE.NS +CARYSIL.NS +CASTROLIND.NS +CCCL.NS +CCHHL.NS +CCL.NS +CDSL.NS +CEATLTD.NS +CEIGALL.NS +CELEBRITY.NS +CELLO.NS +CENTENKA.NS +CENTEXT.NS +CENTRALBK.NS +CENTRUM.NS +CENTUM.NS +CENTURYPLY.NS +CERA.NS +CEREBRAINT.NS +CESC.NS +CEWATER.NS +CGCL.NS +CGPOWER.NS +CHALET.NS +CHAMBLFERT.NS +CHEMBOND.NS +CHEMCON.NS +CHEMFAB.NS +CHEMPLASTS.NS +CHENNPETRO.NS +CHEVIOT.NS +CHOICEIN.NS +CHOLAFIN.NS +CHOLAHLDNG.NS +CIEINDIA.NS +CIFL.NS +CIGNITITEC.NS +CINELINE.NS +CINEVISTA.NS +CIPLA.NS +CLEAN.NS +CLEDUCATE.NS +CLSEL.NS +CMSINFO.NS +COALINDIA.NS +COASTCORP.NS +COCHINSHIP.NS +COFFEEDAY.NS +COFORGE.NS +COLPAL.NS +COMPUSOFT.NS +COMSYN.NS +CONCOR.NS +CONCORDBIO.NS +CONFIPET.NS +CONSOFINVT.NS +CONTROLPR.NS +CORALFINAC.NS +CORDSCABLE.NS +COROMANDEL.NS +COSMOFIRST.NS +COUNCODOS.NS +CPCAP.NS +CRAFTSMAN.NS +CREATIVE.NS +CREATIVEYE.NS +CREDITACC.NS +CREST.NS +CRISIL.NS +CROMPTON.NS +CROWN.NS +CSBBANK.NS +CSLFINANCE.NS +CTE.NS +CUB.NS +CUBEXTUB.NS +CUMMINSIND.NS +CUPID.NS +CURAA.NS +CYBERMEDIA.NS +CYBERTECH.NS +CYIENT.NS +CYIENTDLM.NS +DABUR.NS +DALBHARAT.NS +DALMIASUG.NS +DAMCAPITAL.NS +DAMODARIND.NS +DANGEE.NS +DATAMATICS.NS +DATAPATTNS.NS +DAVANGERE.NS +DBCORP.NS +DBEIL.NS +DBL.NS +DBOL.NS +DBREALTY.NS +DBSTOCKBRO.NS +DCAL.NS +DCBBANK.NS +DCI.NS +DCM.NS +DCMFINSERV.NS +DCMNVL.NS +DCMSHRIRAM.NS +DCMSRIND.NS +DCW.NS +DCXINDIA.NS +DDEVPLSTIK.NS +DECCANCE.NS +DEEDEV.NS +DEEPAKFERT.NS +DEEPAKNTR.NS +DEEPINDS.NS +DELHIVERY.NS +DELPHIFX.NS +DELTACORP.NS +DELTAMAGNT.NS +DEN.NS +DENORA.NS +DENTA.NS +DEVIT.NS +DEVYANI.NS +DGCONTENT.NS +DHAMPURSUG.NS +DHANBANK.NS +DHANI.NS +DHANUKA.NS +DHARMAJ.NS +DHRUV.NS +DHUNINV.NS +DIACABS.NS +DIAMINESQ.NS +DIAMONDYD.NS +DICIND.NS +DIFFNKG.NS +DIGIDRIVE.NS +DIGISPICE.NS +DIGJAMLMTD.NS +DIL.NS +DISHTV.NS +DIVGIITTS.NS +DIVISLAB.NS +DIXON.NS +DJML.NS +DLF.NS +DLINKINDIA.NS +DMART.NS +DMCC.NS +DNAMEDIA.NS +DODLA.NS +DOLATALGO.NS +DOLLAR.NS +DOLPHIN.NS +DOMS.NS +DONEAR.NS +DPABHUSHAN.NS +DPSCLTD.NS +DPWIRES.NS +DRCSYSTEMS.NS +DREAMFOLKS.NS +DREDGECORP.NS +DRREDDY.NS +DSSL.NS +DTIL.NS +DUCON.NS +DVL.NS +DWARKESH.NS +DYCL.NS +DYNAMATECH.NS +DYNPRO.NS +E2E.NS +EASEMYTRIP.NS +ECLERX.NS +ECOSMOBLTY.NS +EDELWEISS.NS +EICHERMOT.NS +EIDPARRY.NS +EIEL.NS +EIFFL.NS +EIHAHOTELS.NS +EIHOTEL.NS +EIMCOELECO.NS +EKC.NS +ELDEHSG.NS +ELECON.NS +ELECTCAST.NS +ELECTHERM.NS +ELGIEQUIP.NS +ELGIRUBCO.NS +ELIN.NS +EMAMILTD.NS +EMAMIPAP.NS +EMAMIREAL.NS +EMBDL.NS +EMCURE.NS +EMIL.NS +EMKAY.NS +EMMBI.NS +EMSLIMITED.NS +EMUDHRA.NS +ENDURANCE.NS +ENERGYDEV.NS +ENGINERSIN.NS +ENIL.NS +ENTERO.NS +EPACK.NS +EPIGRAL.NS +EPL.NS +EQUIPPP.NS +EQUITASBNK.NS +ERIS.NS +ESABINDIA.NS +ESAFSFB.NS +ESCORTS.NS +ESSARSHPNG.NS +ESSENTIA.NS +ESTER.NS +ETERNAL.NS +ETHOSLTD.NS +EUREKAFORB.NS +EUROTEXIND.NS +EVEREADY.NS +EVERESTIND.NS +EXCEL.NS +EXCELINDUS.NS +EXICOM.NS +EXIDEIND.NS +EXPLEOSOL.NS +EXXARO.NS +FACT.NS +FAIRCHEMOR.NS +FAZE3Q.NS +FCL.NS +FCSSOFT.NS +FDC.NS +FEDERALBNK.NS +FEDFINA.NS +FEL.NS +FELDVR.NS +FIBERWEB.NS +FIEMIND.NS +FILATEX.NS +FILATFASH.NS +FINCABLES.NS +FINEORG.NS +FINOPB.NS +FINPIPE.NS +FIRSTCRY.NS +FIVESTAR.NS +FLAIR.NS +FLEXITUFF.NS +FLUOROCHEM.NS +FMGOETZE.NS +FMNL.NS +FOCUS.NS +FOODSIN.NS +FORCEMOT.NS +FORTIS.NS +FOSECOIND.NS +FSC.NS +FSL.NS +FUSION.NS +GABRIEL.NS +GAEL.NS +GAIL.NS +GALAPREC.NS +GALAXYSURF.NS +GALLANTT.NS +GANDHAR.NS +GANDHITUBE.NS +GANECOS.NS +GANESHBE.NS +GANESHHOUC.NS +GANGAFORGE.NS +GANGESSECU.NS +GARFIBRES.NS +GARUDA.NS +GATDVR-RE.NS +GATECH.NS +GATECH-RE1.NS +GATECHDVR.NS +GATEWAY.NS +GAYAHWS.NS +GEECEE.NS +GEEKAYWIRE.NS +GENCON.NS +GENESYS.NS +GENSOL.NS +GENUSPAPER.NS +GENUSPOWER.NS +GEOJITFSL.NS +GEPIL.NS +GESHIP.NS +GFLLIMITED.NS +GHCL.NS +GHCLTEXTIL.NS +GICHSGFIN.NS +GICRE.NS +GILLANDERS.NS +GILLETTE.NS +GINNIFILA.NS +GIPCL.NS +GKWLIMITED.NS +GLAND.NS +GLAXO.NS +GLENMARK.NS +GLFL.NS +GLOBAL.NS +GLOBALE.NS +GLOBALVECT.NS +GLOBE.NS +GLOBUSSPR.NS +GLOSTERLTD.NS +GMBREW.NS +GMDCLTD.NS +GMMPFAUDLR.NS +GMRAIRPORT.NS +GMRP&UI.NS +GNA.NS +GNFC.NS +GOACARBON.NS +GOCLCORP.NS +GOCOLORS.NS +GODAVARIB.NS +GODFRYPHLP.NS +GODHA.NS +GODIGIT.NS +GODREJAGRO.NS +GODREJCP.NS +GODREJIND.NS +GODREJPROP.NS +GOENKA.NS +GOKEX.NS +GOKUL.NS +GOKULAGRO.NS +GOLDENTOBC.NS +GOLDIAM.NS +GOLDTECH.NS +GOODLUCK.NS +GOPAL.NS +GOYALALUM.NS +GPIL.NS +GPPL.NS +GPTHEALTH.NS +GPTINFRA.NS +GRANULES.NS +GRAPHITE.NS +GRASIM.NS +GRAVITA.NS +GREAVESCOT.NS +GREENLAM.NS +GREENPANEL.NS +GREENPLY.NS +GREENPOWER.NS +GRINDWELL.NS +GRINFRA.NS +GRMOVER.NS +GROBTEA.NS +GRPLTD.NS +GRSE.NS +GRWRHITECH.NS +GSFC.NS +GSLSU.NS +GSPL.NS +GSS.NS +GTECJAINX.NS +GTL.NS +GTLINFRA.NS +GTPL.NS +GUFICBIO.NS +GUJALKALI.NS +GUJAPOLLO.NS +GUJGASLTD.NS +GUJRAFFIA.NS +GUJTHEM.NS +GULFOILLUB.NS +GULFPETRO.NS +GULPOLY.NS +GVKPIL.NS +GVPTECH.NS +GVT&D.NS +HAL.NS +HAPPSTMNDS.NS +HAPPYFORGE.NS +HARDWYN.NS +HARIOMPIPE.NS +HARRMALAYA.NS +HARSHA.NS +HATHWAY.NS +HATSUN.NS +HAVELLS.NS +HAVISHA.NS +HBLENGINE.NS +HBSL.NS +HCC.NS +HCG.NS +HCL-INSYS.NS +HCLTECH.NS +HDFCAMC.NS +HDFCBANK.NS +HDFCLIFE.NS +HEADSUP.NS +HECPROJECT.NS +HEG.NS +HEIDELBERG.NS +HEMIPROP.NS +HERANBA.NS +HERCULES.NS +HERITGFOOD.NS +HEROMOTOCO.NS +HESTERBIO.NS +HEUBACHIND.NS +HEXATRADEX.NS +HEXT.NS +HFCL.NS +HGINFRA.NS +HGS.NS +HIKAL.NS +HILTON.NS +HIMATSEIDE.NS +HINDALCO.NS +HINDCOMPOS.NS +HINDCON.NS +HINDCOPPER.NS +HINDMOTORS.NS +HINDNATGLS.NS +HINDOILEXP.NS +HINDPETRO.NS +HINDUNILVR.NS +HINDWAREAP.NS +HINDZINC.NS +HIRECT.NS +HISARMETAL.NS +HITECH.NS +HITECHCORP.NS +HITECHGEAR.NS +HLEGLAS.NS +HLVLTD.NS +HMAAGRO.NS +HMT.NS +HMVL.NS +HNDFDS.NS +HOMEFIRST.NS +HONASA.NS +HONAUT.NS +HONDAPOWER.NS +HOVS.NS +HPAL.NS +HPIL.NS +HPL.NS +HSCL.NS +HTMEDIA.NS +HUBTOWN.NS +HUDCO.NS +HUHTAMAKI.NS +HYBRIDFIN.NS +HYUNDAI.NS +ICDSLTD.NS +ICEMAKE.NS +ICICIBANK.NS +ICICIGI.NS +ICICIPRULI.NS +ICIL.NS +ICRA.NS +IDBI.NS +IDEA.NS +IDEAFORGE.NS +IDFCFIRSTB.NS +IEL.NS +IEX.NS +IFBAGRO.NS +IFBIND.NS +IFCI.NS +IFGLEXPOR.NS +IGARASHI.NS +IGIL.NS +IGL.NS +IGPL.NS +IIFL.NS +IIFLCAPS.NS +IITL.NS +IKIO.NS +IKS.NS +IL&FSENGG.NS +IL&FSTRANS.NS +IMAGICAA.NS +IMFA.NS +IMPAL.NS +IMPEXFERRO.NS +INCREDIBLE.NS +INDBANK.NS +INDGN.NS +INDHOTEL.NS +INDIACEM.NS +INDIAGLYCO.NS +INDIAMART.NS +INDIANB.NS +INDIANCARD.NS +INDIANHUME.NS +INDIASHLTR.NS +INDIGO.NS +INDIGOPNTS.NS +INDNIPPON.NS +INDOAMIN.NS +INDOBORAX.NS +INDOCO.NS +INDOFARM.NS +INDORAMA.NS +INDOSTAR.NS +INDOTECH.NS +INDOTHAI.NS +INDOUS.NS +INDOWIND.NS +INDRAMEDCO.NS +INDSWFTLAB.NS +INDSWFTLTD.NS +INDTERRAIN.NS +INDUSINDBK.NS +INDUSTOWER.NS +INFIBEAM.NS +INFOBEAN.NS +INFOMEDIA.NS +INFY.NS +INGERRAND.NS +INNOVACAP.NS +INNOVANA.NS +INOXGREEN.NS +INOXINDIA.NS +INOXWIND.NS +INSECTICID.NS +INSPIRISYS.NS +INTELLECT.NS +INTENTECH.NS +INTERARCH.NS +INTLCONV.NS +INVENTURE.NS +IOB.NS +IOC.NS +IOLCP.NS +IONEXCHANG.NS +IPCALAB.NS +IPL.NS +IRB.NS +IRCON.NS +IRCTC.NS +IREDA.NS +IRFC.NS +IRIS.NS +IRISDOREME.NS +IRMENERGY.NS +ISFT.NS +ISGEC.NS +ISHANCH.NS +ITC.NS +ITCHOTELS.NS +ITDC.NS +ITDCEM.NS +ITI.NS +IVC.NS +IVP.NS +IWEL.NS +IXIGO.NS +IZMO.NS +J&KBANK.NS +JAGRAN.NS +JAGSNPHARM.NS +JAIBALAJI.NS +JAICORPLTD.NS +JAIPURKURT.NS +JAMNAAUTO.NS +JASH.NS +JAYAGROGN.NS +JAYBARMARU.NS +JAYNECOIND.NS +JAYSREETEA.NS +JBCHEPHARM.NS +JBMA.NS +JCHAC.NS +JETFREIGHT.NS +JGCHEM.NS +JHS.NS +JINDALPHOT.NS +JINDALPOLY.NS +JINDALSAW.NS +JINDALSTEL.NS +JINDRILL.NS +JINDWORLD.NS +JIOFIN.NS +JISLDVREQS.NS +JISLJALEQS.NS +JITFINFRA.NS +JKCEMENT.NS +JKIL.NS +JKLAKSHMI.NS +JKPAPER.NS +JKTYRE.NS +JLHL.NS +JMA.NS +JMFINANCIL.NS +JNKINDIA.NS +JOCIL.NS +JPOLYINVST.NS +JPPOWER.NS +JSFB.NS +JSL.NS +JSWENERGY.NS +JSWHL.NS +JSWINFRA.NS +JSWSTEEL.NS +JTEKTINDIA.NS +JTLIND.NS +JUBLCPL.NS +JUBLFOOD.NS +JUBLINGREA.NS +JUBLPHARMA.NS +JUNIPER.NS +JUSTDIAL.NS +JWL.NS +JYOTHYLAB.NS +JYOTICNC.NS +JYOTISTRUC.NS +KABRAEXTRU.NS +KAJARIACER.NS +KAKATCEM.NS +KALAMANDIR.NS +KALYANI.NS +KALYANIFRG.NS +KALYANKJIL.NS +KAMATHOTEL.NS +KAMDHENU.NS +KAMOPAINTS.NS +KANANIIND.NS +KANORICHEM.NS +KANPRPLA.NS +KANSAINER.NS +KAPSTON.NS +KARMAENG.NS +KARURVYSYA.NS +KAUSHALYA.NS +KAVVERITEL.NS +KAYA.NS +KAYNES.NS +KBCGLOBAL.NS +KCP.NS +KCPSUGIND.NS +KDDL.NS +KEC.NS +KECL.NS +KEEPLEARN.NS +KEI.NS +KELLTONTEC.NS +KERNEX.NS +KESORAMIND.NS +KEYFINSERV.NS +KFINTECH.NS +KHADIM.NS +KHAICHEM.NS +KHAITANLTD.NS +KHANDSE.NS +KICL.NS +KILITCH.NS +KIMS.NS +KINGFA.NS +KIOCL.NS +KIRIINDUS.NS +KIRLOSBROS.NS +KIRLOSENG.NS +KIRLOSIND.NS +KIRLPNU.NS +KITEX.NS +KKCL.NS +KMEW.NS +KMSUGAR.NS +KNRCON.NS +KOHINOOR.NS +KOKUYOCMLN.NS +KOLTEPATIL.NS +KOPRAN.NS +KOTAKBANK.NS +KOTARISUG.NS +KOTHARIPET.NS +KOTHARIPRO.NS +KPEL.NS +KPIGREEN.NS +KPIL.NS +KPITTECH.NS +KPRMILL.NS +KRBL.NS +KREBSBIO.NS +KRIDHANINF.NS +KRISHANA.NS +KRITI.NS +KRITIKA.NS +KRITINUT.NS +KRN.NS +KRONOX.NS +KROSS.NS +KRSNAA.NS +KRYSTAL.NS +KSB.NS +KSCL.NS +KSHITIJPOL.NS +KSL.NS +KSOLVES.NS +KTKBANK.NS +KUANTUM.NS +LAGNAM.NS +LAKPRE.NS +LAL.NS +LALPATHLAB.NS +LAMBODHARA.NS +LANCORHOL.NS +LANDMARK.NS +LAOPALA.NS +LASA.NS +LATENTVIEW.NS +LATTEYS.NS +LAURUSLABS.NS +LAXMICOT.NS +LAXMIDENTL.NS +LCCINFOTEC.NS +LEMONTREE.NS +LEXUS.NS +LFIC.NS +LGBBROSLTD.NS +LGHL.NS +LIBAS.NS +LIBERTSHOE.NS +LICHSGFIN.NS +LICI.NS +LIKHITHA.NS +LINC.NS +LINCOLN.NS +LINDEINDIA.NS +LLOYDS-RE1.NS +LLOYDSENGG.NS +LLOYDSENT.NS +LLOYDSME.NS +LMW.NS +LODHA.NS +LOKESHMACH.NS +LORDSCHLO.NS +LOTUSEYE.NS +LOVABLE.NS +LOYALTEX.NS +LPDC.NS +LT.NS +LTF.NS +LTFOODS.NS +LTIM.NS +LTTS.NS +LUMAXIND.NS +LUMAXTECH.NS +LUPIN.NS +LUXIND.NS +LXCHEM.NS +LYKALABS.NS +LYPSAGEMS.NS +M&M.NS +M&MFIN.NS +MAANALU.NS +MACPOWER.NS +MADHAV.NS +MADHUCON.NS +MADRASFERT.NS +MAGADSUGAR.NS +MAGNUM.NS +MAHABANK.NS +MAHAPEXLTD.NS +MAHASTEEL.NS +MAHEPC.NS +MAHESHWARI.NS +MAHLIFE.NS +MAHLOG.NS +MAHSCOOTER.NS +MAHSEAMLES.NS +MAITHANALL.NS +MALLCOM.NS +MALUPAPER.NS +MAMATA.NS +MANAKALUCO.NS +MANAKCOAT.NS +MANAKSIA.NS +MANAKSTEEL.NS +MANALIPETC.NS +MANAPPURAM.NS +MANBA.NS +MANCREDIT.NS +MANGALAM.NS +MANGCHEFER.NS +MANGLMCEM.NS +MANINDS.NS +MANINFRA.NS +MANKIND.NS +MANOMAY.NS +MANORAMA.NS +MANORG.NS +MANUGRAPH.NS +MANYAVAR.NS +MAPMYINDIA.NS +MARALOVER.NS +MARATHON.NS +MARICO.NS +MARINE.NS +MARKSANS.NS +MARSHALL.NS +MARUTI.NS +MASFIN.NS +MASKINVEST.NS +MASTEK.NS +MASTERTR.NS +MATRIMONY.NS +MAWANASUG.NS +MAXESTATES.NS +MAXHEALTH.NS +MAXIND.NS +MAXIND-RE.NS +MAYURUNIQ.NS +MAZDA.NS +MAZDOCK.NS +MBAPL.NS +MBLINFRA.NS +MCL.NS +MCLEODRUSS.NS +MCLOUD.NS +MCX.NS +MEDANTA.NS +MEDIASSIST.NS +MEDICAMEQ.NS +MEDICO.NS +MEDPLUS.NS +MEGASOFT.NS +MEGASTAR.NS +MENONBE.NS +MEP.NS +METROBRAND.NS +METROPOLIS.NS +MFML.NS +MFSL.NS +MGEL.NS +MGL.NS +MHLXMIRU.NS +MHRIL.NS +MICEL.NS +MIDHANI.NS +MINDACORP.NS +MINDTECK.NS +MIRCELECTR.NS +MIRZAINT.NS +MITCON.NS +MITTAL.NS +MKPL.NS +MMFL.NS +MMP.NS +MMTC.NS +MOBIKWIK.NS +MODIRUBBER.NS +MODISONLTD.NS +MODTHREAD.NS +MOHITIND.NS +MOIL.NS +MOKSH.NS +MOL.NS +MOLDTECH.NS +MOLDTKPAC.NS +MONARCH.NS +MONTECARLO.NS +MOREPENLAB.NS +MOSCHIP.NS +MOTHERSON.NS +MOTILALOFS.NS +MOTISONS.NS +MOTOGENFIN.NS +MPHASIS.NS +MPSLTD.NS +MRF.NS +MRPL.NS +MSPL.NS +MSTCLTD.NS +MSUMI.NS +MTARTECH.NS +MTEDUCARE.NS +MTNL.NS +MUFIN.NS +MUFTI.NS +MUKANDLTD.NS +MUKKA.NS +MUKTAARTS.NS +MUNJALAU.NS +MUNJALSHOW.NS +MURUDCERA.NS +MUTHOOTCAP.NS +MUTHOOTFIN.NS +MUTHOOTMF.NS +MVGJL.NS +NACLIND.NS +NAGAFERT.NS +NAGREEKCAP.NS +NAGREEKEXP.NS +NAHARCAP.NS +NAHARINDUS.NS +NAHARPOLY.NS +NAHARSPING.NS +NAM-INDIA.NS +NARMADA.NS +NATCAPSUQ.NS +NATCOPHARM.NS +NATHBIOGEN.NS +NATIONALUM.NS +NAUKRI.NS +NAVA.NS +NAVINFLUOR.NS +NAVKARCORP.NS +NAVKARURB.NS +NAVNETEDUL.NS +NAZARA.NS +NBCC.NS +NBIFIN.NS +NCC.NS +NCLIND.NS +NDGL.NS +NDL.NS +NDLVENTURE.NS +NDRAUTO.NS +NDTV.NS +NECCLTD.NS +NECLIFE.NS +NELCAST.NS +NELCO.NS +NEOGEN.NS +NESCO.NS +NESTLEIND.NS +NETWEB.NS +NETWORK18.NS +NEULANDLAB.NS +NEWGEN.NS +NEXTMEDIA.NS +NFL.NS +NGIL.NS +NGLFINE.NS +NH.NS +NHPC.NS +NIACL.NS +NIBE.NS +NIBL.NS +NIITLTD.NS +NIITMTS.NS +NILAINFRA.NS +NILASPACES.NS +NILKAMAL.NS +NINSYS.NS +NIPPOBATRY.NS +NIRAJ.NS +NIRAJISPAT.NS +NITCO.NS +NITINSPIN.NS +NITIRAJ.NS +NIVABUPA.NS +NKIND.NS +NLCINDIA.NS +NMDC.NS +NOCIL.NS +NOIDATOLL.NS +NORBTEAEXP.NS +NORTHARC.NS +NOVAAGRI.NS +NPST.NS +NRAIL.NS +NRBBEARING.NS +NRL.NS +NSIL.NS +NSLNISP.NS +NTPC.NS +NTPCGREEN.NS +NUCLEUS.NS +NURECA.NS +NUVAMA.NS +NUVOCO.NS +NYKAA.NS +OAL.NS +OBCL.NS +OBEROIRLTY.NS +OCCL.NS +OCCLLTD.NS +ODIGMA.NS +OFSS.NS +OIL.NS +OILCOUNTUB.NS +OLAELEC.NS +OLECTRA.NS +OMAXAUTO.NS +OMAXE.NS +OMINFRAL.NS +OMKARCHEM.NS +ONELIFECAP.NS +ONEPOINT.NS +ONESOURCE.NS +ONGC.NS +ONMOBILE.NS +ONWARDTEC.NS +OPTIEMUS.NS +ORBTEXP.NS +ORCHASP.NS +ORCHPHARMA.NS +ORICONENT.NS +ORIENTALTL.NS +ORIENTBELL.NS +ORIENTCEM.NS +ORIENTCER.NS +ORIENTELEC.NS +ORIENTHOT.NS +ORIENTLTD.NS +ORIENTPPR.NS +ORIENTTECH.NS +ORISSAMINE.NS +ORTEL.NS +ORTINGLOBE.NS +OSIAHYPER.NS +OSWALAGRO.NS +OSWALGREEN.NS +OSWALSEEDS.NS +PAGEIND.NS +PAISALO.NS +PAKKA.NS +PALASHSECU.NS +PALREDTEC.NS +PANACEABIO.NS +PANACHE.NS +PANAMAPET.NS +PANSARI.NS +PAR.NS +PARACABLES.NS +PARADEEP.NS +PARAGMILK.NS +PARAS.NS +PARASPETRO.NS +PARKHOTELS.NS +PARSVNATH.NS +PASUPTAC.NS +PATANJALI.NS +PATELENG.NS +PATINTLOG.NS +PAVNAIND.NS +PAYTM.NS +PCBL.NS +PCJEWELLER.NS +PDMJEPAPER.NS +PDSL.NS +PEARLPOLY.NS +PEL.NS +PENIND.NS +PENINLAND.NS +PERSISTENT.NS +PETRONET.NS +PFC.NS +PFIZER.NS +PFOCUS.NS +PFS.NS +PGEL.NS +PGHH.NS +PGHL.NS +PGIL.NS +PHOENIXLTD.NS +PIDILITIND.NS +PIGL.NS +PIIND.NS +PILANIINVS.NS +PILITA.NS +PIONEEREMB.NS +PITTIENG.NS +PIXTRANS.NS +PKTEA.NS +PLASTIBLEN.NS +PLATIND.NS +PLAZACABLE.NS +PNB.NS +PNBGILTS.NS +PNBHOUSING.NS +PNC.NS +PNCINFRA.NS +PNGJL.NS +POCL.NS +PODDARMENT.NS +POKARNA.NS +POLICYBZR.NS +POLYCAB.NS +POLYMED.NS +POLYPLEX.NS +PONNIERODE.NS +POONAWALLA.NS +POWERGRID.NS +POWERINDIA.NS +POWERMECH.NS +PPAP.NS +PPL.NS +PPLPHARMA.NS +PRABHA.NS +PRAENG.NS +PRAJIND.NS +PRAKASH.NS +PRAKASHSTL.NS +PRAXIS.NS +PRECAM.NS +PRECOT.NS +PRECWIRE.NS +PREMEXPLN.NS +PREMIER.NS +PREMIERENE.NS +PREMIERPOL.NS +PRESTIGE.NS +PRICOLLTD.NS +PRIMESECU.NS +PRIMO.NS +PRINCEPIPE.NS +PRITI.NS +PRITIKAUTO.NS +PRIVISCL.NS +PROTEAN.NS +PROZONER.NS +PRSMJOHNSN.NS +PRUDENT.NS +PRUDMOULI.NS +PSB.NS +PSPPROJECT.NS +PTC.NS +PTCIL.NS +PTL.NS +PUNJABCHEM.NS +PURVA.NS +PVP.NS +PVRINOX.NS +PVSL.NS +PYRAMID.NS +QPOWER.NS +QUADFUTURE.NS +QUESS.NS +QUICKHEAL.NS +RACE.NS +RACLGEAR.NS +RADAAN.NS +RADHIKAJWE.NS +RADIANTCMS.NS +RADICO.NS +RADIOCITY.NS +RAILTEL.NS +RAIN.NS +RAINBOW.NS +RAJESHEXPO.NS +RAJMET.NS +RAJRATAN.NS +RAJRILTD.NS +RAJSREESUG.NS +RAJTV.NS +RALLIS.NS +RAMANEWS.NS +RAMAPHO.NS +RAMASTEEL.NS +RAMCOCEM.NS +RAMCOIND.NS +RAMCOSYS.NS +RAMKY.NS +RAMRAT.NS +RANASUG.NS +RANEHOLDIN.NS +RATEGAIN.NS +RATNAMANI.NS +RATNAVEER.NS +RAYMOND.NS +RAYMONDLSL.NS +RBA.NS +RBLBANK.NS +RBZJEWEL.NS +RCF.NS +RCOM.NS +RECLTD.NS +REDINGTON.NS +REDTAPE.NS +REFEX.NS +REGENCERAM.NS +RELAXO.NS +RELCHEMQ.NS +RELIABLE.NS +RELIANCE.NS +RELIGARE.NS +RELINFRA.NS +RELTD.NS +REMSONSIND.NS +RENUKA.NS +REPCOHOME.NS +REPL.NS +REPRO.NS +RESPONIND.NS +RETAIL.NS +RGL.NS +RHFL.NS +RHIM.NS +RHL.NS +RICOAUTO.NS +RIIL.NS +RISHABH.NS +RITCO.NS +RITES.NS +RKDL.NS +RKEC.NS +RKFORGE.NS +RKSWAMY.NS +RML.NS +ROHLTD.NS +ROLEXRINGS.NS +ROLLT.NS +ROLTA.NS +ROML.NS +ROSSARI.NS +ROSSELLIND.NS +ROSSTECH.NS +ROTO.NS +ROUTE.NS +RPEL.NS +RPGLIFE.NS +RPOWER.NS +RPPINFRA.NS +RPPL.NS +RPSGVENT.NS +RPTECH.NS +RRKABEL.NS +RSSOFTWARE.NS +RSWM.NS +RSYSTEMS.NS +RTNINDIA.NS +RTNPOWER.NS +RUBFILA.NS +RUBYMILLS.NS +RUCHINFRA.NS +RUCHIRA.NS +RUPA.NS +RUSHIL.NS +RUSTOMJEE.NS +RVHL.NS +RVNL.NS +RVTH.NS +S&SPOWER.NS +SABEVENTS.NS +SABTNL.NS +SADBHAV.NS +SADBHIN.NS +SADHNANIQ.NS +SAFARI.NS +SAGARDEEP.NS +SAGCEM.NS +SAGILITY.NS +SAH.NS +SAHYADRI.NS +SAIL.NS +SAILIFE.NS +SAKAR.NS +SAKHTISUG.NS +SAKSOFT.NS +SAKUMA.NS +SALASAR.NS +SALONA.NS +SALSTEEL.NS +SALZERELEC.NS +SAMBHAAV.NS +SAMHI.NS +SAMMAANCAP.NS +SAMPANN.NS +SANATHAN.NS +SANCO.NS +SANDESH.NS +SANDHAR.NS +SANDUMA.NS +SANGAMIND.NS +SANGHIIND.NS +SANGHVIMOV.NS +SANGINITA.NS +SANOFI.NS +SANOFICONR.NS +SANSERA.NS +SANSTAR.NS +SANWARIA.NS +SAPPHIRE.NS +SARDAEN.NS +SAREGAMA.NS +SARLAPOLY.NS +SARVESHWAR.NS +SASKEN.NS +SASTASUNDR.NS +SATIA.NS +SATIN.NS +SATINDLTD.NS +SAURASHCEM.NS +SBC.NS +SBCL.NS +SBFC.NS +SBGLP.NS +SBICARD.NS +SBILIFE.NS +SBIN.NS +SCHAEFFLER.NS +SCHAND.NS +SCHNEIDER.NS +SCI.NS +SCILAL.NS +SCPL.NS +SDBL.NS +SEAMECLTD.NS +SECMARK.NS +SECURKLOUD.NS +SEJALLTD.NS +SELAN.NS +SELMC.NS +SEMAC.NS +SENCO.NS +SENORES.NS +SEPC.NS +SEQUENT.NS +SERVOTECH.NS +SESHAPAPER.NS +SETCO.NS +SETUINFRA.NS +SFL.NS +SGIL.NS +SGL.NS +SGLTL.NS +SHAH.NS +SHAHALLOYS.NS +SHAILY.NS +SHAKTIPUMP.NS +SHALBY.NS +SHALPAINTS.NS +SHANKARA.NS +SHANTI.NS +SHANTIGEAR.NS +SHARDACROP.NS +SHARDAMOTR.NS +SHAREINDIA.NS +SHEKHAWATI.NS +SHEMAROO.NS +SHILPAMED.NS +SHIVALIK.NS +SHIVAMAUTO.NS +SHIVAMILLS.NS +SHIVATEX.NS +SHK.NS +SHOPERSTOP.NS +SHRADHA.NS +SHREDIGCEM.NS +SHREECEM.NS +SHREEPUSHK.NS +SHREERAMA.NS +SHRENIK.NS +SHREYANIND.NS +SHRIPISTON.NS +SHRIRAMFIN.NS +SHRIRAMPPS.NS +SHYAMCENT.NS +SHYAMMETL.NS +SHYAMTEL.NS +SIEMENS.NS +SIGACHI.NS +SIGIND.NS +SIGMA.NS +SIGNATURE.NS +SIGNPOST.NS +SIKKO.NS +SIL.NS +SILGO.NS +SILINV.NS +SILLYMONKS.NS +SILVERTUC.NS +SIMBHALS.NS +SIMPLEXINF.NS +SINCLAIR.NS +SINDHUTRAD.NS +SINTERCOM.NS +SIRCA.NS +SIS.NS +SITINET.NS +SIYSIL.NS +SJS.NS +SJVN.NS +SKFINDIA.NS +SKIPPER.NS +SKMEGGPROD.NS +SKYGOLD.NS +SMARTLINK.NS +SMCGLOBAL.NS +SMLISUZU.NS +SMLT.NS +SMSLIFE.NS +SMSPHARMA.NS +SNOWMAN.NS +SOBHA.NS +SOFTTECH.NS +SOLARA.NS +SOLARINDS.NS +SOMANYCERA.NS +SOMATEX.NS +SOMICONVEY.NS +SONACOMS.NS +SONAMLTD.NS +SONATSOFTW.NS +SOTL.NS +SOUTHBANK.NS +SOUTHWEST.NS +SPAL.NS +SPANDANA.NS +SPARC.NS +SPCENET.NS +SPECIALITY.NS +SPECTRUM.NS +SPENCERS.NS +SPIC.NS +SPLIL.NS +SPLPETRO.NS +SPMLINFRA.NS +SPORTKING.NS +SRD.NS +SREEL.NS +SRF.NS +SRGHFL.NS +SRHHYPOLTD.NS +SRM.NS +SRPL.NS +SSDL.NS +SSWL.NS +STALLION.NS +STANLEY.NS +STAR.NS +STARCEMENT.NS +STARHEALTH.NS +STARPAPER.NS +STARTECK.NS +STCINDIA.NS +STEELCAS.NS +STEELCITY.NS +STEELXIND.NS +STEL.NS +STERTOOLS.NS +STLTECH.NS +STOVEKRAFT.NS +STYLAMIND.NS +STYLEBAAZA.NS +STYRENIX.NS +SUBEXLTD.NS +SUBROS.NS +SUDARSCHEM.NS +SUKHJITS.NS +SULA.NS +SUMICHEM.NS +SUMIT.NS +SUMMITSEC.NS +SUNCLAY.NS +SUNDARAM.NS +SUNDARMFIN.NS +SUNDARMHLD.NS +SUNDRMBRAK.NS +SUNDRMFAST.NS +SUNDROP.NS +SUNFLAG.NS +SUNPHARMA.NS +SUNTECK.NS +SUNTV.NS +SUPERHOUSE.NS +SUPERSPIN.NS +SUPRAJIT.NS +SUPREME.NS +SUPREMEENG.NS +SUPREMEIND.NS +SUPREMEINF.NS +SUPRIYA.NS +SURAJEST.NS +SURAJLTD.NS +SURAKSHA.NS +SURANASOL.NS +SURANAT&P.NS +SURYALAXMI.NS +SURYAROSNI.NS +SURYODAY.NS +SUTLEJTEX.NS +SUULD.NS +SUVEN.NS +SUVENPHAR.NS +SUVIDHAA.NS +SUYOG.NS +SUZLON.NS +SVLL.NS +SVPGLOB.NS +SWANENERGY.NS +SWARAJENG.NS +SWELECTES.NS +SWIGGY.NS +SWSOLAR.NS +SYMPHONY.NS +SYNCOMF.NS +SYNGENE.NS +SYRMA.NS +TAINWALCHM.NS +TAJGVK.NS +TAKE.NS +TALBROAUTO.NS +TANLA.NS +TARACHAND.NS +TARAPUR.NS +TARC.NS +TARIL.NS +TARMAT.NS +TARSONS.NS +TASTYBITE.NS +TATACHEM.NS +TATACOMM.NS +TATACONSUM.NS +TATAELXSI.NS +TATAINVEST.NS +TATAMOTORS.NS +TATAPOWER.NS +TATASTEEL.NS +TATATECH.NS +TATVA.NS +TBOTEK.NS +TBZ.NS +TCI.NS +TCIEXP.NS +TCIFINANCE.NS +TCPLPACK.NS +TCS.NS +TDPOWERSYS.NS +TEAMLEASE.NS +TECHM.NS +TECHNOE.NS +TEGA.NS +TEJASNET.NS +TEMBO.NS +TERASOFT.NS +TEXINFRA.NS +TEXMOPIPES.NS +TEXRAIL.NS +TFCILTD.NS +TFL.NS +TGBHOTELS.NS +THANGAMAYL.NS +THEINVEST.NS +THEJO.NS +THEMISMED.NS +THERMAX.NS +THOMASCOOK.NS +THOMASCOTT.NS +THYROCARE.NS +TI.NS +TICL.NS +TIIL.NS +TIINDIA.NS +TIJARIA.NS +TIL.NS +TIMESGTY.NS +TIMETECHNO.NS +TIMKEN.NS +TINNARUBR.NS +TIPSFILMS.NS +TIPSMUSIC.NS +TIRUMALCHM.NS +TIRUPATIFL.NS +TITAGARH.NS +TITAN.NS +TMB.NS +TNPETRO.NS +TNPL.NS +TNTELE.NS +TOKYOPLAST.NS +TOLINS.NS +TORNTPHARM.NS +TORNTPOWER.NS +TOTAL.NS +TOUCHWOOD.NS +TPHQ.NS +TPLPLASTEH.NS +TRACXN.NS +TRANSRAILL.NS +TRANSWORLD.NS +TREEHOUSE.NS +TREJHARA.NS +TREL.NS +TRENT.NS +TRF.NS +TRIDENT.NS +TRIGYN.NS +TRITURBINE.NS +TRIVENI.NS +TRU.NS +TTKHLTCARE.NS +TTKPRESTIG.NS +TTL.NS +TTML.NS +TVSELECT.NS +TVSHLTD.NS +TVSMOTOR.NS +TVSSCS.NS +TVSSRICHAK.NS +TVTODAY.NS +TVVISION.NS +UBL.NS +UCAL.NS +UCOBANK.NS +UDAICEMENT.NS +UDS.NS +UFLEX.NS +UFO.NS +UGARSUGAR.NS +UGROCAP.NS +UJJIVANSFB.NS +ULTRACEMCO.NS +UMAEXPORTS.NS +UMANGDAIRY.NS +UMESLTD.NS +UMIYA-MRO.NS +UNICHEMLAB.NS +UNIDT.NS +UNIECOM.NS +UNIENTER.NS +UNIINFO.NS +UNIMECH.NS +UNIONBANK.NS +UNIPARTS.NS +UNITDSPR.NS +UNITECH.NS +UNITEDPOLY.NS +UNITEDTEA.NS +UNIVASTU.NS +UNIVCABLES.NS +UNIVPHOTO.NS +UNOMINDA.NS +UPL.NS +URAVIDEF.NS +URJA.NS +USHAMART.NS +USK.NS +UTIAMC.NS +UTKARSHBNK.NS +UTTAMSUGAR.NS +UYFINCORP.NS +V2RETAIL.NS +VADILALIND.NS +VAIBHAVGBL.NS +VAISHALI.NS +VAKRANGEE.NS +VALIANTLAB.NS +VALIANTORG.NS +VARDHACRLC.NS +VARDMNPOLY.NS +VARROC.NS +VASCONEQ.NS +VASWANI.NS +VBL.NS +VCL.NS +VEDL.NS +VEEDOL.NS +VENKEYS.NS +VENTIVE.NS +VENUSPIPES.NS +VENUSREM.NS +VERANDA.NS +VERTOZ.NS +VESUVIUS.NS +VETO.NS +VGUARD.NS +VHL.NS +VHLTD.NS +VIDHIING.NS +VIJAYA.NS +VIJIFIN.NS +VIKASECO.NS +VIKASLIFE.NS +VIMTALABS.NS +VINATIORGA.NS +VINCOFE.NS +VINDHYATEL.NS +VINEETLAB.NS +VINNY.NS +VINYLINDIA.NS +VIPCLOTHNG.NS +VIPIND.NS +VIPULLTD.NS +VIRINCHI.NS +VISAKAIND.NS +VISASTEEL.NS +VISHNU.NS +VISHWARAJ.NS +VIVIDHA.NS +VLEGOV.NS +VLSFINANCE.NS +VMART.NS +VMM.NS +VOLTAMP.NS +VOLTAS.NS +VPRPL.NS +VRAJ.NS +VRLLOG.NS +VSSL.NS +VSTIND.NS +VSTL.NS +VSTTILLERS.NS +VTL.NS +WAAREEENER.NS +WAAREERTL.NS +WABAG.NS +WALCHANNAG.NS +WANBURY.NS +WCIL.NS +WEALTH.NS +WEBELSOLAR.NS +WEIZMANIND.NS +WEL.NS +WELCORP.NS +WELENT.NS +WELINV.NS +WELSPUNLIV.NS +WENDT.NS +WESTLIFE.NS +WEWIN.NS +WHEELS.NS +WHIRLPOOL.NS +WILLAMAGOR.NS +WINDLAS.NS +WINDMACHIN.NS +WINSOME.NS +WIPL.NS +WIPRO.NS +WOCKPHARMA.NS +WONDERLA.NS +WORTH.NS +WSI.NS +WSTCSTPAPR.NS +XCHANGING.NS +XELPMOC.NS +XPROINDIA.NS +XTGLOBAL.NS +YAARI.NS +YASHO.NS +YATHARTH.NS +YATRA.NS +YESBANK.NS +YUKEN.NS +ZAGGLE.NS +ZEEL.NS +ZEELEARN.NS +ZEEMEDIA.NS +ZENITHEXPO.NS +ZENITHSTL.NS +ZENSARTECH.NS +ZENTEC.NS +ZFCVINDIA.NS +ZIMLAB.NS +ZODIAC.NS +ZODIACLOTH.NS +ZOTA.NS +ZUARI.NS +ZUARIIND.NS +ZYDUSLIFE.NS +ZYDUSWELL.NS \ No newline at end of file diff --git a/app/user.py b/app/user.py new file mode 100644 index 0000000000000000000000000000000000000000..574052f7abbc35ee2b4ff1aadd2be368b5010598 --- /dev/null +++ b/app/user.py @@ -0,0 +1,227 @@ +from fastapi import APIRouter, Request, Depends, HTTPException +from fastapi.responses import HTMLResponse, RedirectResponse +from sqlalchemy.orm import Session + +from app import models, schemas, crud +from app.database import get_db +from app.auth import get_current_active_user +from app.main import templates # Import templates from main.py +import logging +import yfinance as yf +from datetime import datetime, timedelta +import pytz # for timezone handling +from typing import List + +logger = logging.getLogger(__name__) +router = APIRouter() + +# Define timezone (Asia/Kolkata) +tz = pytz.timezone('Asia/Kolkata') + +# Function to read tickers from tickers.txt +def get_all_tickers(): + tickers = [] + try: + with open("app/tickers.txt", "r") as f: + tickers = [line.strip() for line in f if line.strip()] + except FileNotFoundError: + logger.error("tickers.txt not found.") + return tickers + +# Function to find closest available trading day price on or before the target date +def get_closest_price(hist, date): + # Filter dates in hist index that are <= target date + available_dates = hist.index[hist.index <= date] + if not available_dates.empty: + closest_date = available_dates[-1] + return hist.loc[closest_date]['Close'], closest_date.date() + else: + return None, None + +# Function to fetch historical data and calculate prices +def fetch_stock_prices(ticker_symbol: str): + ticker = yf.Ticker(ticker_symbol) + + # Get today's date as timezone-aware datetime + today = datetime.now(tz) + + # Define the dates for 1 month, 6 months, 1 year, and 3 years ago (timezone-aware) + dates = { + "Current": today, + "1 Month Ago": today - timedelta(days=30), + "6 Months Ago": today - timedelta(days=182), + "1 Year Ago": today - timedelta(days=365), + "3 Years Ago": today - timedelta(days=3*365), + } + + # Fetch historical data for the last 4 years to cover all dates + start_date = (today - timedelta(days=4*365)).strftime('%Y-%m-%d') + end_date = today.strftime('%Y-%m-%d') + hist = ticker.history(start=start_date, end=end_date) + + # The index of hist is timezone-aware, ensure it matches tz + if hist.index.tz is None: + # If index is naive, localize it + hist.index = hist.index.tz_localize(tz) + else: + # Convert to desired timezone + hist.index = hist.index.tz_convert(tz) + + # Retrieve prices for each date + prices = {} + for label, date in dates.items(): + price, actual_date = get_closest_price(hist, date) + if price is not None: + prices[label] = (f"₹{price:.2f}", actual_date.strftime('%Y-%m-%d') if actual_date else None) + else: + prices[label] = ("No data", None) + + return prices + +# API endpoint for searching tickers +@router.get("/api/stocks/search", response_model=List[str]) +async def search_stocks(query: str): + all_tickers = get_all_tickers() + # Simple case-insensitive search + matching_tickers = [ticker for ticker in all_tickers if query.lower() in ticker.lower()] + return matching_tickers + +# API endpoint for fetching stock prices +@router.get("/api/stocks/price/{ticker_symbol}") +async def get_stock_price(ticker_symbol: str): + try: + prices = fetch_stock_prices(ticker_symbol) + return prices + except Exception as e: + logger.error(f"Error fetching price for {ticker_symbol}: {e}") + raise HTTPException(status_code=500, detail="Could not fetch stock price") + + +@router.get("/home", response_class=HTMLResponse) +async def user_homepage(request: Request): # Removed: current_user: models.User = Depends(get_current_active_user) + # User data will be fetched and displayed by client-side JavaScript + return templates.TemplateResponse("user/homepage.html", { + "request": request, + # "user": current_user, # This will be handled by client-side JS + "title": "User Homepage" + }) + +@router.get("/chatbot/{model_type}", response_class=HTMLResponse) +async def chatbot_page(request: Request, model_type: str): # Removed: current_user: models.User = Depends(get_current_active_user) + # Client-side JS on chatbot.html already handles token for API submission. + # This route now just serves the page structure. + valid_models = ["base", "enhanced", "rule_based"] + if model_type.lower() not in valid_models: + raise HTTPException(status_code=404, detail="Model type not found") + + # Prepare context for the template based on model_type + # This context will help the template render the correct form fields + form_fields = [] + page_title = "" + + if model_type == "base": + page_title = "Base Model Advisor" + form_fields = [ + {"name": "Salary", "label": "Monthly Salary (₹)", "type": "number", "required": True, "min": 0}, + {"name": "Expenses", "label": "Monthly Expenses (₹)", "type": "number", "required": True, "min": 0}, + {"name": "Savings", "label": "Monthly Savings (₹)", "type": "number", "required": True, "min": 0}, + {"name": "Lifecycle_Stage", "label": "Lifecycle Stage", "type": "select", "required": True, "options": ["Student", "Early Career", "Mid-Career", "Late Career", "Retired"]}, + {"name": "Risk_Appetite", "label": "Risk Appetite", "type": "select", "required": True, "options": ["Low", "Medium", "High"]}, + {"name": "Investment_Horizon", "label": "Investment Horizon", "type": "select", "required": True, "options": ["Short-term", "Medium-term", "Long-term"]}, + ] + elif model_type == "enhanced": + page_title = "Enhanced Model Advisor" + + # Define the options for dropdowns based on provided lists + cities = sorted([ + 'Mumbai', 'Delhi', 'Bangalore', 'Hyderabad', 'Pune', 'Chennai', 'Jaipur', + 'Kochi', 'Kolkata', 'Ahmedabad', 'Gurgaon', 'Lucknow', 'Nagpur', 'Chandigarh', + 'Surat', 'Indore', 'Bhopal' + ]) + + professions = sorted([ + 'Software Engineer', 'Doctor', 'Retired Banker', 'Student', 'Marketing Manager', 'Teacher', + 'Freelancer', 'Architect', 'Business Owner', 'Nurse', 'Product Manager', 'CA', 'Data Analyst', + 'Retired Professor', 'Journalist', 'Graphic Designer', 'Sales Executive', 'HR Manager', 'Intern', + 'Dentist', 'Lawyer', 'Content Creator', 'Pensioner', 'UX Designer', 'Government Employee', + 'Fashion Designer', 'Startup Founder', 'Homemaker (Investor)', 'Investment Banker', 'IT Consultant', + 'College Student', 'Pharmacist', 'Textile Business Owner', 'Event Planner', 'Film Producer', + 'Nutritionist', 'Retired Army Officer', 'Social Media Manager', 'Airline Pilot', 'Biotech Researcher', + 'Real Estate Agent', 'AI Engineer', 'Retired Teacher', 'Financial Analyst', 'Software Developer', + 'NGO Director', 'Fitness Trainer', 'Digital Marketer', 'Content Strategist', 'Retired Engineer', + 'UI Developer', 'Operations Manager', 'Corporate Lawyer', 'School Principal', 'AI Researcher', + 'Junior Doctor', 'Finance Manager', 'Fashion Blogger', 'HR Consultant', 'Video Editor', + 'Small Business Owner', 'Dietitian', 'Supply Chain Manager', 'Interior Designer', 'Sales Manager', + 'PR Executive', 'Logistics Head', 'Content Writer', 'Retired Govt. Employee', 'Marketing Lead', + 'IT Manager', 'Startup Intern', 'Export Manager', 'Fitness Instructor', 'Real Estate Broker', + 'Event Manager', 'Software Trainee', 'Data Scientist', 'HR Director', 'Hotel Manager', + 'Social Worker', 'Cybersecurity Expert', 'E-commerce Manager', 'Bank Manager', 'Retired IT Manager', + 'UX Researcher', 'Product Designer', 'Cybersecurity Analyst', 'Podcast Producer', 'E-commerce Seller', + 'Cloud Architect', 'Corporate Trainer', 'AI Trainer', 'Supply Chain Head', 'Product Owner', + 'UI/UX Designer', 'Finance Director', 'Sustainability Consultant', 'Retired Bank Manager', + 'Social Media Influencer', 'Blockchain Developer', 'NGO Head', 'Data Engineer', 'Event Curator', + 'CTO', 'Content Marketer', 'Retired Army Major', 'AR/VR Developer', 'Logistics Manager', 'VP Sales', + 'EdTech Founder', 'SEO Specialist', 'Yoga Instructor', 'IT Director', 'App Developer', + 'Consultant Cardiologist', 'Freelance Writer', 'Cloud Engineer', 'Retired CA', 'Digital Artist', + 'Retired Pilot', 'Graphic Animator', 'AI Ethicist', 'School Trustee', 'Robotics Engineer', + 'Fashion Stylist', 'Retired Nurse', 'AI Ethics Consultant', 'Drone Engineer', 'Retired Bank Clerk', + 'Digital Nomad', 'CFO', 'Sustainability Analyst', 'Retired Journalist', 'Social Entrepreneur', + 'DevOps Engineer', 'School Counselor', 'Retired Army Colonel', 'AR Developer', 'Sales Director', + 'EdTech Consultant', 'SEO Expert', 'Cardiologist', '3D Artist', 'HR Head', 'Animator','Other', + 'Fashion Influencer' + ]) + + form_fields = [ + {"name": "Profession", "label": "Profession", "type": "select", "required": True, "options": professions}, + {"name": "City", "label": "City", "type": "select", "required": True, "options": cities}, + {"name": "Salary", "label": "Monthly Salary (₹)", "type": "number", "required": True, "min": 0}, + {"name": "Expenses", "label": "Monthly Expenses (₹)", "type": "number", "required": True, "min": 0}, + {"name": "Savings", "label": "Monthly Savings (₹)", "type": "number", "required": True, "min": 0}, + {"name": "Lifecycle_Stage", "label": "Lifecycle Stage", "type": "select", "required": True, "options": ["Student", "Early Career", "Mid-Career", "Late Career", "Retired"]}, + {"name": "Risk_Appetite", "label": "Risk Appetite", "type": "select", "required": True, "options": ["Low", "Medium", "High"]}, + {"name": "Investment_Horizon", "label": "Investment Horizon", "type": "select", "required": True, "options": ["Short-term", "Medium-term", "Long-term"]}, + ] + elif model_type == "rule_based": + page_title = "Rule-Based Advisor" + form_fields = [ + {"name": "Lifecycle_Stage", "label": "Lifecycle Stage", "type": "select", "required": True, "options": ["Student", "Early Career", "Mid-Career", "Late Career", "Retired"]}, + {"name": "Risk_Appetite", "label": "Risk Appetite", "type": "select", "required": True, "options": ["Low", "Medium", "High"]}, + {"name": "Investment_Horizon", "label": "Investment Horizon", "type": "select", "required": True, "options": ["Short-term", "Medium-term", "Long-term"]}, + {"name": "Annual_Salary_Package", "label": "Annual Salary Package (CTC) (₹)", "type": "number", "required": True, "min": 0}, + {"name": "Monthly_In_hand_Salary", "label": "Actual Monthly In-hand Salary (₹)", "type": "number", "required": True, "min": 0}, + {"name": "Total_Monthly_Expenses", "label": "Total Estimated Monthly Expenses (₹)", "type": "number", "required": True, "min": 0}, + ] + + return templates.TemplateResponse("user/chatbot.html", { + "request": request, + # "user": current_user, # Client-side JS on this page handles API calls with token + "title": page_title, + "model_type": model_type, + "form_fields": form_fields + }) + +@router.get("/recommendations", response_class=HTMLResponse) +async def recommendations_page(request: Request): # Removed: current_user: models.User = Depends(get_current_active_user) + # Client-side JS could be added to this page if it needs to display user-specific info + # or make authenticated API calls for dynamic market data. + # For now, it serves static structure with mocked data. + # Data fetching logic would go into services/market_data_service.py + # For now, just rendering a template. + market_data = { # Mock data + "stocks": [ + {"name": "Reliance Industries", "price": "2,850.50 INR", "change": "+0.5%"}, + {"name": "Tata Consultancy Services", "price": "3,900.75 INR", "change": "-0.2%"}, + {"name": "HDFC Bank", "price": "1,500.20 INR", "change": "+1.1%"}, + ], + "gold_price": "96,172.27 -745.25 INR per 10g", + "recommended_stocks": [ + {"name": "Infosys", "reason": "Strong growth potential in IT sector."}, + {"name": "ICICI Bank", "reason": "Good financial performance and outlook."} + ] + } + return templates.TemplateResponse("user/recommendations.html", { + "request": request, + # "user": current_user, # Can be fetched by client-side JS if needed + "title": "Market Trends & Recommendations", + "market_data": market_data + }) diff --git a/app/user_financial_data.csv b/app/user_financial_data.csv new file mode 100644 index 0000000000000000000000000000000000000000..eb3501353eea3f52308f2b699dad268bbbb35262 --- /dev/null +++ b/app/user_financial_data.csv @@ -0,0 +1,14 @@ +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,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 +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 +2,user1@gmail.com,rule_based,2025-05-08 14:52:36,,,,,,,,Early Career,Medium,Medium-term,700000,,30000,,,, +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 +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 +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 +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 +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 +5,user7@gmail.com,rule_based,2025-05-09 19:13:52,,,,,,,,Mid-Career,High,Short-term,700000,,40000,,,, +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 +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 +6,mandar@gmail.com,rule_based,2025-05-09 19:57:28,,,,,,,,Mid-Career,Medium,Short-term,700000,,40000,,,, +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 diff --git a/diagramcontent.md b/diagramcontent.md new file mode 100644 index 0000000000000000000000000000000000000000..6d569e6e4a3ad5e1b7d026d27b7a27096345a9a0 --- /dev/null +++ b/diagramcontent.md @@ -0,0 +1,54 @@ +# Project Module Block Diagrams + +## Overall Project + +```mermaid +graph LR + A[admin.py] --> B(auth.py: get_current_user, get_current_active_user) + A --> C(crud.py: get_user, get_users) + B --> D(database.py: get_db) + C --> D + E[main.py] --> B + E --> C + E --> H[user.py] + F(models.py: User, UserDataInput) --> D + G(schemas.py: UserCreate, User) --> F + H --> B + H --> C + I[services: chatbot_service.py, data_service.py] --> E + J[ml_models] --> I + subgraph app + A + B + C + D + E + F + G + H + I + J + end +``` + +## Admin Side + +```mermaid +graph LR + A[admin.py: admin_dashboard_shell, admin_view_user_details_shell] --> B(auth.py: get_current_admin_user) + A --> C(crud.py: get_user, get_users) + C --> D(database.py: get_db) + B --> D + style A fill:#f9f,stroke:#333,stroke-width:2px +``` + +## User Side + +```mermaid +graph LR + H[user.py: user_homepage, chatbot_page, recommendations_page] --> B(auth.py: get_current_active_user) + H --> C(crud.py: get_user_data_inputs_by_user_id) + C --> D(database.py: get_db) + B --> D + E[main.py: api_chatbot_interact] --> H + style H fill:#ccf,stroke:#333,stroke-width:2px diff --git a/financial_advisor.db b/financial_advisor.db new file mode 100644 index 0000000000000000000000000000000000000000..495f646d01f9586c41136d6affbec9df3e66d940 Binary files /dev/null and b/financial_advisor.db differ diff --git a/readme.md b/readme.md new file mode 100644 index 0000000000000000000000000000000000000000..9d2374f8f7fea0d3ac0565ebbff4acbf7441df87 --- /dev/null +++ b/readme.md @@ -0,0 +1,97 @@ +# Financial Advisor + +## Overview + +The Financial Advisor is a web application that provides personalized financial advice and recommendations to users. It uses machine learning models and rule-based logic to suggest optimal investment allocations based on user inputs. The application is built using FastAPI, a modern, high-performance web framework for building APIs with Python. + +## Features + +* **User Authentication:** Secure user registration and login system. +* **Admin Dashboard:** A dedicated dashboard for administrators to manage users and view system data. +* **Chatbot Interface:** An interactive chatbot that guides users through the process of providing financial information and receiving recommendations. +* **Personalized Recommendations:** Investment recommendations tailored to individual user profiles, risk tolerance, and financial goals. +* **Multiple Models:** Support for different recommendation models, including: + * Base Model: A basic model that considers salary, expenses, savings, lifecycle stage, risk appetite, and investment horizon. + * Enhanced Model: An advanced model that incorporates profession and city to provide more refined recommendations. + * Rule-Based Model: A model that uses predefined rules to generate recommendations based on user inputs. +* **Stock Data:** Real-time stock data fetching and display. + +## Modules + +### Admin Module + +The Admin module provides functionalities for administrators to manage the application and its users. + +* **admin.py:** Contains the API endpoints and logic for the admin dashboard and user management. + * `admin_dashboard_shell`: Renders the admin dashboard HTML page. + * `get_admin_dashboard_data`: Fetches data for the admin dashboard, including user lists and search functionality. + * `admin_view_user_details_shell`: Renders the user details HTML page. + * `get_admin_user_details_data`: Fetches detailed information for a specific user. + +### User Module + +The User module provides functionalities for users to interact with the application and receive financial advice. + +* **user.py:** Contains the API endpoints and logic for user-related features, such as the chatbot and recommendations. + * `user_homepage`: Renders the user homepage HTML page. + * `chatbot_page`: Renders the chatbot HTML page, allowing users to interact with the financial advisor. + * `recommendations_page`: Renders the recommendations HTML page, displaying market trends and stock recommendations. + * `search_stocks`: API endpoint for searching tickers. + * `get_stock_price`: API endpoint for fetching stock prices. + +### Authentication Module + +The Authentication module handles user registration, login, and authentication. + +* **auth.py:** Contains the API endpoints and logic for user authentication. + * `get_current_user`: Retrieves the currently logged-in user. + * `get_current_active_user`: Retrieves the currently active user. + * `get_current_admin_user`: Retrieves the currently active admin user. + * `signup_user`: Registers a new user. + * `login_for_access_token`: Logs in a user and generates an access token. + +### Core Module + +The Core module provides core functionalities and utilities for the application. + +* **core/security.py:** Contains security-related functions, such as password hashing. + +### Database Module + +The Database module handles database connections and operations. + +* **database.py:** Contains functions for connecting to the database and creating tables. + * `get_db`: Provides a database session. + * `create_db_and_tables`: Creates the database and tables if they don't exist. + +### Models Module + +The Models module defines the database models. + +* **models.py:** Contains the database models for users and user data inputs. + * `User`: Represents a user in the system. + * `UserDataInput`: Represents user input data for the chatbot. + +### Schemas Module + +The Schemas module defines the data schemas for API requests and responses. + +* **schemas.py:** Contains the data schemas for users, user data inputs, and chatbot interactions. + +### Services Module + +The Services module provides business logic and external integrations. + +* **services/chatbot\_service.py:** Contains the logic for processing chatbot interactions and generating recommendations. + * `process_chatbot_interaction`: Processes user input and generates investment recommendations based on the selected model. + * `predict_base_model`: Predicts investment allocations using the base model. + * `predict_enhanced_model`: Predicts investment allocations using the enhanced model. + * `predict_rule_based_model`: Predicts investment allocations using the rule-based model. +* **services/data\_service.py:** Contains functions for data handling, such as initializing the CSV file and appending data to it. + +### ML Models Module + +The ML Models module contains the machine learning models used for generating recommendations. + +* **ml\_models/basic\_portfolio\_model.pkl:** The base machine learning model. +* **ml\_models/enhanced\_portfolio\_model.pkl:** The enhanced machine learning model. diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000000000000000000000000000000000000..8e695ee5afd0fd6b56367e482ab02631a6f983b1 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,15 @@ +fastapi +uvicorn[standard] +sqlalchemy +pydantic[email] +python-jose[cryptography] +passlib[bcrypt] +joblib +pandas +python-multipart +Jinja2 +yfinance +scikit-learn +# psycopg2-binary # Add if using PostgreSQL +# alembic # Add if using Alembic for migrations +# python-dotenv # Add if using .env files for configuration