Spaces:
Running
Running
| ๏ปฟimport os | |
| import sys | |
| import json | |
| import uuid | |
| import time | |
| import random | |
| import builtins | |
| from pathlib import Path | |
| from datetime import datetime | |
| from flask import Flask, abort, render_template, request, jsonify, send_from_directory, session | |
| from flask_cors import CORS | |
| from dotenv import load_dotenv | |
| from collections import defaultdict | |
| from werkzeug.middleware.proxy_fix import ProxyFix | |
| from agent_service import agent_is_configured, run_agent_message | |
| from market_demo import MARKET_POOL, generate_kline, generate_news, generate_quotes | |
| from runtime_config import ( | |
| CHROMA_DB_DIR, | |
| CHECKPOINTS_DB_PATH, | |
| DATA_DIR, | |
| KNOWLEDGE_BASE_PATH, | |
| ROOT_DIR, | |
| ) | |
| def safe_print(*args, **kwargs): | |
| try: | |
| builtins.print(*args, **kwargs) | |
| except UnicodeEncodeError: | |
| file = kwargs.get("file", sys.stdout) | |
| encoding = getattr(file, "encoding", None) or "utf-8" | |
| cleaned_args = [ | |
| str(arg).encode(encoding, errors="ignore").decode(encoding, errors="ignore") | |
| for arg in args | |
| ] | |
| builtins.print(*cleaned_args, **kwargs) | |
| print = safe_print | |
| # Load environment variables | |
| load_dotenv() | |
| app = Flask(__name__) | |
| if os.getenv("TRUST_PROXY_HEADERS", "true").lower() == "true": | |
| app.wsgi_app = ProxyFix(app.wsgi_app, x_for=1, x_proto=1, x_host=1, x_port=1) | |
| # Use environment variable for secret key (SECURITY BEST PRACTICE) | |
| app.secret_key = os.getenv('FLASK_SECRET_KEY') | |
| if not app.secret_key: | |
| print("WARNING: FLASK_SECRET_KEY not set.") | |
| print("Using a development secret key. Set FLASK_SECRET_KEY before deploying.") | |
| app.secret_key = 'dev-key-CHANGE-ME-in-production' | |
| app.config['SESSION_COOKIE_SAMESITE'] = os.getenv('SESSION_COOKIE_SAMESITE', 'Lax') | |
| app.config['SESSION_COOKIE_SECURE'] = os.getenv('SESSION_COOKIE_SECURE', 'false').lower() == 'true' | |
| app.config['SESSION_COOKIE_HTTPONLY'] = True | |
| def get_cors_origins(): | |
| defaults = [ | |
| 'http://127.0.0.1:5173', | |
| 'http://localhost:5173', | |
| 'http://127.0.0.1:5000', | |
| 'http://localhost:5000', | |
| ] | |
| extra = [ | |
| origin.strip() | |
| for origin in os.getenv('FRONTEND_ORIGINS', '').split(',') | |
| if origin.strip() | |
| ] | |
| return sorted(set(defaults + extra)) | |
| CORS(app, supports_credentials=True, origins=get_cors_origins()) | |
| UI_DIST_DIR = ROOT_DIR / "ui improvement" / "dist" | |
| VIDEO_DIR = ROOT_DIR / "video" | |
| CONTACT_VIDEO_DIR = ROOT_DIR / "contact" | |
| HERO_VIDEO_EXTENSIONS = {".mp4", ".webm", ".ogg", ".m4v", ".mov"} | |
| def get_positive_float_env(name): | |
| value = os.getenv(name, "").strip() | |
| if not value: | |
| return None | |
| try: | |
| parsed = float(value) | |
| except ValueError: | |
| print(f"WARNING: Invalid value for {name}: {value}") | |
| return None | |
| return parsed if parsed > 0 else None | |
| def get_positive_int_env(name): | |
| value = os.getenv(name, "").strip() | |
| if not value: | |
| return None | |
| try: | |
| parsed = int(value) | |
| except ValueError: | |
| print(f"WARNING: Invalid value for {name}: {value}") | |
| return None | |
| return parsed if parsed > 0 else None | |
| def list_media_files(directory): | |
| if not directory.exists(): | |
| return [] | |
| media_files = [ | |
| path | |
| for path in directory.iterdir() | |
| if path.is_file() and path.suffix.lower() in HERO_VIDEO_EXTENSIONS | |
| ] | |
| max_file_mb = get_positive_float_env("BACKGROUND_VIDEO_MAX_MB") | |
| if max_file_mb is not None: | |
| max_bytes = max_file_mb * 1024 * 1024 | |
| filtered_files = [path for path in media_files if path.stat().st_size <= max_bytes] | |
| if filtered_files: | |
| media_files = filtered_files | |
| max_count = get_positive_int_env("BACKGROUND_VIDEO_MAX_COUNT") | |
| if max_count is not None and len(media_files) > max_count: | |
| media_files = sorted( | |
| media_files, | |
| key=lambda path: (path.stat().st_size, path.name.lower()), | |
| )[:max_count] | |
| return [path.name for path in sorted(media_files, key=lambda path: path.name.lower())] | |
| def directory_is_writable(directory): | |
| try: | |
| directory.mkdir(parents=True, exist_ok=True) | |
| probe_path = directory / ".write-test" | |
| probe_path.write_text("ok", encoding="utf-8") | |
| probe_path.unlink(missing_ok=True) | |
| return True, None | |
| except OSError as error: | |
| return False, str(error) | |
| # --- Academy Database (50 Modules for Beginners) --- | |
| ACADEMY_DATA = [ | |
| {"id": 1, "parent": None, "cat": "Foundations", "difficulty": 1, "name": "Time Value of Money", "video": "cy4PiY5ERTI", "source": "Patrick Boyle", "views": "280k", "completed": False, "ratings": [5, 4, 5], "video_intro": "The foundation of all valuation.", "outcomes": ["Calculate Present & Future Value", "Understand Discount Rates"], "takeaways": ["Money today is worth more than tomorrow", "Compounding is exponential"]}, | |
| {"id": 2, "parent": 1, "cat": "Economics", "difficulty": 1, "name": "Market Microstructure", "video": "zi78J-h1XgY", "source": "The Plain Bagel", "views": "142k", "completed": False, "ratings": [4, 5], "video_intro": "Order book dynamics.", "outcomes": ["Analyze Bid-Ask Spreads", "Understand Liquidity Providers"], "takeaways": ["Market depth impacts trade execution", "Spreads reflect liquidity costs"]}, | |
| {"id": 3, "parent": 2, "cat": "Foundations", "difficulty": 1, "name": "Stock Market for Beginners", "video": "T_2b2d_y64Q", "source": "Humphrey Yang", "views": "1.2M", "completed": False, "ratings": [5], "video_intro": "A complete guide to starting your journey.", "outcomes": ["Open Brokerage Accounts", "Place First Trade"], "takeaways": ["Start early to maximize growth", "Consistency is key to success"]}, | |
| {"id": 4, "parent": 3, "cat": "Foundations", "difficulty": 1, "name": "What is a Stock?", "video": "C_3f_3_J_00", "source": "Khan Academy", "views": "850k", "completed": False, "ratings": [5], "video_intro": "Understanding equity ownership.", "outcomes": ["Define Equity Ownership", "Differentiate Common vs Preferred"], "takeaways": ["Stocks represent real business ownership", "Shareholders have voting rights"]}, | |
| {"id": 5, "parent": 4, "cat": "Foundations", "difficulty": 1, "name": "How the Stock Market Works", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "2.1M", "completed": False, "ratings": [4], "video_intro": "The mechanics of the exchange.", "outcomes": ["Explain Exchange Mechanisms", "Understand IPO Process"], "takeaways": ["Markets efficiently allocate capital", "Supply and demand drive prices"]}, | |
| {"id": 6, "parent": 5, "cat": "Foundations", "difficulty": 1, "name": "Types of Investments", "video": "f5px_b_y_1Q", "source": "The Plain Bagel", "views": "600k", "completed": False, "ratings": [5], "video_intro": "Stocks, Bonds, and more.", "outcomes": ["Compare Asset Classes", "Assess Risk vs Reward"], "takeaways": ["Diversification reduces portfolio risk", "Different assets serve different goals"]}, | |
| {"id": 7, "parent": 6, "cat": "Foundations", "difficulty": 1, "name": "Power of Compound Interest", "video": "X_D_J_y_Q_g_Q", "source": "Khan Academy", "views": "900k", "completed": False, "ratings": [5], "video_intro": "The 8th wonder of the world.", "outcomes": ["Calculate Compound Returns", "Project Long-term Growth"], "takeaways": ["Time in market beats timing market", "Reinvesting earnings accelerates wealth"]}, | |
| {"id": 8, "parent": 7, "cat": "Analysis", "difficulty": 2, "name": "Understanding Financials", "video": "21STUhQ-iP0", "source": "The Plain Bagel", "views": "450k", "completed": False, "ratings": [5], "video_intro": "How to read the big three statements.", "outcomes": ["Read Balance Sheets", "Analyze Income Statements"], "takeaways": ["Financials reveal true company health", "Look for consistent performance"]}, | |
| {"id": 9, "parent": 8, "cat": "Analysis", "difficulty": 2, "name": "P/E Ratio Explained", "video": "4KkTGx2bK_4", "source": "Investopedia", "views": "300k", "completed": False, "ratings": [4], "video_intro": "Valuing a company via earnings.", "outcomes": ["Calculate Price-to-Earnings", "Compare Valuation Metrics"], "takeaways": ["High P/E implies high growth expectations", "Price alone tells you nothing"]}, | |
| {"id": 10, "parent": 9, "cat": "Analysis", "difficulty": 2, "name": "EPS & Earnings Growth", "video": "_u_J9g_D_FM", "source": "Patrick Boyle", "views": "120k", "completed": False, "ratings": [4], "video_intro": "Tracking profitability per share.", "outcomes": ["Calculate Earnings Per Share", "Analyze Growth Trends"], "takeaways": ["EPS growth drives stock prices", "Watch for dilution risks"]}, | |
| {"id": 11, "parent": 10, "cat": "Analysis", "difficulty": 2, "name": "Technical Analysis Basics", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "500k", "completed": False, "ratings": [5], "video_intro": "Reading charts and patterns.", "outcomes": ["Identify Support & Resistance", "Recognize Chart Patterns"], "takeaways": ["Price action reflects market psychology", "Trend is your friend until it bends"]}, | |
| {"id": 12, "parent": 11, "cat": "Analysis", "difficulty": 2, "name": "Fundamental Analysis", "video": "21STUhQ-iP0", "source": "The Plain Bagel", "views": "350k", "completed": False, "ratings": [5], "video_intro": "Digging into company value.", "outcomes": ["Evaluate Business Moats", "Assess Management Quality"], "takeaways": ["Value depends on future cash flows", "Buy quality businesses at fair prices"]}, | |
| {"id": 13, "parent": 12, "cat": "Strategy", "difficulty": 2, "name": "Portfolio Management", "video": "f5px_b_y_1Q", "source": "The Plain Bagel", "views": "200k", "completed": False, "ratings": [4], "video_intro": "Organizing your investments.", "outcomes": ["Construct Balanced Portfolios", "Execute Rebalancing"], "takeaways": ["Asset allocation drives returns", "Regular maintenance reduces risk"]}, | |
| {"id": 14, "parent": 13, "cat": "Strategy", "difficulty": 2, "name": "Risk Management", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "180k", "completed": False, "ratings": [5], "video_intro": "Avoiding catastrophic losses.", "outcomes": ["Determine Position Sizing", "Set Stop-Loss Levels"], "takeaways": ["Capital preservation is priority #1", "Never risk more than you can lose"]}, | |
| {"id": 15, "parent": 14, "cat": "Strategy", "difficulty": 1, "name": "Long-Term vs Short-Term", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "400k", "completed": False, "ratings": [4], "video_intro": "Investing vs Trading.", "outcomes": ["Define Investment Horizon", "Understand Tax Implications"], "takeaways": ["Investing is for wealth building", "Trading is for income generation"]}, | |
| {"id": 16, "parent": 15, "cat": "Foundations", "difficulty": 1, "name": "Bonds, Funds, and ETFs", "video": "f5px_b_y_1Q", "source": "The Plain Bagel", "views": "320k", "completed": False, "ratings": [5], "video_intro": "Diverse investment vehicles.", "outcomes": ["Distinguish ETFs vs Mutual Funds", "Understand Fixed Income"], "takeaways": ["ETFs offer low-cost diversification", "Bonds provide stability and income"]}, | |
| {"id": 17, "parent": 16, "cat": "Foundations", "difficulty": 1, "name": "S&P 500 Explained", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "1.1M", "completed": False, "ratings": [5], "video_intro": "The benchmark of the US market.", "outcomes": ["Understand Market Indices", "Analyze Index Weighting"], "takeaways": ["S&P 500 represents US economy", "Passive indexing beats most active funds"]}, | |
| {"id": 18, "parent": 17, "cat": "Analysis", "difficulty": 1, "name": "Market Capitalization", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "250k", "completed": False, "ratings": [4], "video_intro": "Understanding company size.", "outcomes": ["Categorize Cap Sizes", "Evaluate Growth vs Stability"], "takeaways": ["Small caps offer higher growth potential", "Large caps offer stability and dividends"]}, | |
| {"id": 19, "parent": 18, "cat": "Analysis", "difficulty": 2, "name": "Reading a Balance Sheet", "video": "21STUhQ-iP0", "source": "The Plain Bagel", "views": "150k", "completed": False, "ratings": [5], "video_intro": "What a company owns and owes.", "outcomes": ["Calculate Working Capital", "Assess Debt-to-Equity"], "takeaways": ["Assets = Liabilities + Equity", "Solvency is key to survival"]}, | |
| {"id": 20, "parent": 19, "cat": "Analysis", "difficulty": 2, "name": "Income Statement Basics", "video": "21STUhQ-iP0", "source": "The Plain Bagel", "views": "180k", "completed": False, "ratings": [4], "video_intro": "Revenue, expenses, and profit.", "outcomes": ["Analyze Revenue Streams", "Calculate Profit Margins"], "takeaways": ["Top line is vanity, bottom line is sanity", "Margins indicate competitive advantage"]}, | |
| {"id": 21, "parent": 20, "cat": "Analysis", "difficulty": 2, "name": "Cash Flow Statement", "video": "21STUhQ-iP0", "source": "The Plain Bagel", "views": "110k", "completed": False, "ratings": [4], "video_intro": "Tracking the movement of money.", "outcomes": ["Analyze Operating Cash Flow", "Calculate Free Cash Flow"], "takeaways": ["Cash flow creates shareholder value", "Profits can be manipulated, cash cannot"]}, | |
| {"id": 22, "parent": 21, "cat": "Analysis", "difficulty": 2, "name": "Candlestick Patterns", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "600k", "completed": False, "ratings": [5], "video_intro": "Decoding price action.", "outcomes": ["Identify Reversal Patterns", "Interpret Market Sentiment"], "takeaways": ["Candlesticks reveal buyer/seller battle", "Context is crucial for pattern accuracy"]}, | |
| {"id": 23, "parent": 22, "cat": "Economics", "difficulty": 1, "name": "Bull vs Bear Markets", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "3.5M", "completed": False, "ratings": [5], "video_intro": "Cycles of optimism and pessimism.", "outcomes": ["Identify Market Cycles", "Adjust Strategy per Cycle"], "takeaways": ["Bull markets climb a wall of worry", "Bear markets transfer wealth to patient"]}, | |
| {"id": 24, "parent": 23, "cat": "Strategy", "difficulty": 2, "name": "Dividend Investing", "video": "f5px_b_y_1Q", "source": "The Plain Bagel", "views": "1.5M", "completed": False, "ratings": [5], "video_intro": "Building passive income.", "outcomes": ["Calculate Dividend Yield", "Evaluate Payout Ratios"], "takeaways": ["Dividends provide passive income stream", "Dividend growth signals company health"]}, | |
| {"id": 25, "parent": 24, "cat": "Strategy", "difficulty": 1, "name": "Index Fund Investing", "video": "f5px_b_y_1Q", "source": "The Plain Bagel", "views": "700k", "completed": False, "ratings": [5], "video_intro": "Low-cost wealth building.", "outcomes": ["Minimize Expense Ratios", "Automate Monthly Investing"], "takeaways": ["You don't need to beat the market", "Low costs lead to higher net returns"]}, | |
| {"id": 26, "parent": 25, "cat": "Analysis", "difficulty": 2, "name": "How to Research Stocks", "video": "21STUhQ-iP0", "source": "The Plain Bagel", "views": "400k", "completed": False, "ratings": [5], "video_intro": "Step-by-step due diligence.", "outcomes": ["Find Reliable Data Sources", "Perform SWOT Analysis"], "takeaways": ["Do your own due diligence (DYODD)", "Information asymmetry is your enemy"]}, | |
| {"id": 27, "parent": 26, "cat": "Strategy", "difficulty": 2, "name": "Stock Trading Basics", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "250k", "completed": False, "ratings": [4], "video_intro": "Buying and selling mechanics.", "outcomes": ["Understand Order Types", "Execute Trades Efficiently"], "takeaways": ["Market orders for speed, limit for price", "Emotional discipline is mandatory"]}, | |
| {"id": 28, "parent": 27, "cat": "Advanced", "difficulty": 3, "name": "Options Trading 101", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "1.2M", "completed": False, "ratings": [5], "video_intro": "Leveraging your capital.", "outcomes": ["Differentiate Calls vs Puts", "Understand Option Greeks"], "takeaways": ["Options offer leverage and hedging", "Time decay works against buyers"]}, | |
| {"id": 29, "parent": 28, "cat": "Strategy", "difficulty": 2, "name": "REITs Explained", "video": "f5px_b_y_1Q", "source": "The Plain Bagel", "views": "190k", "completed": False, "ratings": [4], "video_intro": "Investing in real estate via stocks.", "outcomes": ["Analyze FFO vs EPS", "Assess Property Sectors"], "takeaways": ["Real estate exposure with liquidity", "REITs must pay 90% of income as dividends"]}, | |
| {"id": 30, "parent": 29, "cat": "Foundations", "difficulty": 2, "name": "Cryptocurrency Intro", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "5M", "completed": False, "ratings": [5], "video_intro": "Bitcoin, Blockchain, and more.", "outcomes": ["Understand Blockchain Tech", "Secure Digital Wallets"], "takeaways": ["Decentralization removes intermediaries", "High volatility requires high risk tolerance"]}, | |
| {"id": 31, "parent": 30, "cat": "Psychology", "difficulty": 2, "name": "Behavioral Finance", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "100k", "completed": False, "ratings": [5], "video_intro": "Why we make bad money choices.", "outcomes": ["Identify Cognitive Biases", "Overcome FOMO & Panic"], "takeaways": ["Investing is 90% emotional control", "Self-awareness improves decision making"]}, | |
| {"id": 32, "parent": 31, "cat": "Strategy", "difficulty": 1, "name": "Dollar Cost Averaging", "video": "f5px_b_y_1Q", "source": "The Plain Bagel", "views": "300k", "completed": False, "ratings": [5], "video_intro": "Smoothing out market entry.", "outcomes": ["Implement Automatic Deposits", "Reduce Timing Risk"], "takeaways": ["Buy more shares when prices are low", "Removes emotion from investing"]}, | |
| {"id": 33, "parent": 32, "cat": "Strategy", "difficulty": 2, "name": "Asset Allocation", "video": "f5px_b_y_1Q", "source": "The Plain Bagel", "views": "150k", "completed": False, "ratings": [5], "video_intro": "The only free lunch in finance.", "outcomes": ["Determine Risk Tolerance", "Diversify Across Sectors"], "takeaways": ["Diversification reduces unsystematic risk", "Rebalance to maintain target allocation"]}, | |
| {"id": 34, "parent": 33, "cat": "Economics", "difficulty": 2, "name": "Market Cycles", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "8M", "completed": False, "ratings": [5], "video_intro": "How the economic machine works.", "outcomes": ["Recognize Cycle Stages", "Anticipate Sector Rotation"], "takeaways": ["Economies expand and contract naturally", "Positioning depends on the cycle phase"]}, | |
| {"id": 35, "parent": 34, "cat": "Strategy", "difficulty": 2, "name": "Growth vs Value", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "220k", "completed": False, "ratings": [4], "video_intro": "Two distinct paths to profit.", "outcomes": ["Screen for Growth Stocks", "Identify Value Traps"], "takeaways": ["Growth focuses on future potential", "Value focuses on current bargains"]}, | |
| {"id": 36, "parent": 35, "cat": "Economics", "difficulty": 2, "name": "Inflation & Stocks", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "1M", "completed": False, "ratings": [4], "video_intro": "The hidden tax on savings.", "outcomes": ["Assess Purchasing Power", "Identify Inflation Hedges"], "takeaways": ["Cash loses value over time", "Real assets protect against inflation"]}, | |
| {"id": 37, "parent": 36, "cat": "Economics", "difficulty": 2, "name": "Interest Rates & Fed", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "240k", "completed": False, "ratings": [5], "video_intro": "The engine of the economy.", "outcomes": ["Monitor Fed Policy", "Understand Yield Curve"], "takeaways": ["Rates act as gravity on asset prices", "Don't fight the Federal Reserve"]}, | |
| {"id": 38, "parent": 37, "cat": "Foundations", "difficulty": 2, "name": "IPOs Explained", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "300k", "completed": False, "ratings": [4], "video_intro": "Going public.", "outcomes": ["Analyze S-1 Filings", "Understand Lock-up Periods"], "takeaways": ["IPOs often underperform initially", "Insiders sell into liquidity events"]}, | |
| {"id": 39, "parent": 38, "cat": "Foundations", "difficulty": 1, "name": "Stock Splits & Buybacks", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "150k", "completed": False, "ratings": [4], "video_intro": "Corporate actions.", "outcomes": ["Calculate Adjusted Price", "Evaluate Shareholder Yield"], "takeaways": ["Splits are cosmetic, buybacks are real", "Buybacks increase ownership share"]}, | |
| {"id": 40, "parent": 39, "cat": "Strategy", "difficulty": 3, "name": "Short Selling Basics", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "200k", "completed": False, "ratings": [3], "video_intro": "Profiting from declines.", "outcomes": ["Understand Borrowing Costs", "Identify Short Squeezes"], "takeaways": ["Shorting has unlimited downside risk", "Timing is critical for short sellers"]}, | |
| {"id": 41, "parent": 40, "cat": "Economics", "difficulty": 2, "name": "Economic Indicators", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "120k", "completed": False, "ratings": [4], "video_intro": "Reading the dashboard.", "outcomes": ["Interpret GDP & Jobs Data", "Track Consumer Confidence"], "takeaways": ["Macro data drives market trends", "Leading indicators predict turning points"]}, | |
| {"id": 42, "parent": 41, "cat": "Strategy", "difficulty": 2, "name": "Recession Strategies", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "500k", "completed": False, "ratings": [5], "video_intro": "Investing in downturns.", "outcomes": ["Select Defensive Stocks", "Build Cash Reserves"], "takeaways": ["Recessions create generation buying opps", "Cash provides optionality in crashes"]}, | |
| {"id": 43, "parent": 42, "cat": "Foundations", "difficulty": 1, "name": "Tax-Advantaged Accounts", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "100k", "completed": False, "ratings": [5], "video_intro": "Saving on taxes.", "outcomes": ["Compare 401k vs IRA", "Maximize Tax Savings"], "takeaways": ["Taxes are your biggest expense", "Asset location matters as much as allocation"]}, | |
| {"id": 44, "parent": 43, "cat": "Regulations", "difficulty": 2, "name": "Capital Gains Taxes", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "80k", "completed": False, "ratings": [4], "video_intro": "Selling for a profit.", "outcomes": ["Calculate Tax Liability", "Harvest Tax Losses"], "takeaways": ["Long-term rates are lower than short-term", "Never let taxes dictate a bad trade"]}, | |
| {"id": 45, "parent": 44, "cat": "Strategy", "difficulty": 1, "name": "Market vs Limit Orders", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "140k", "completed": False, "ratings": [5], "video_intro": "How to execute trades.", "outcomes": ["Choose Correct Order Type", "Avoid Slippage Costs"], "takeaways": ["Limit orders guarantee price, not execution", "Market orders guarantee execution, not price"]}, | |
| {"id": 46, "parent": 45, "cat": "Strategy", "difficulty": 2, "name": "Stop Loss Orders", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "110k", "completed": False, "ratings": [4], "video_intro": "Automation for protection.", "outcomes": ["Set Trailing Stops", "Manage Downside Risk"], "takeaways": ["Cut losers short, let winners run", "Automated stops remove emotional hesitation"]}, | |
| {"id": 47, "parent": 46, "cat": "Analysis", "difficulty": 2, "name": "Analyzing Tech Stocks", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "300k", "completed": False, "ratings": [5], "video_intro": "Valuing innovation.", "outcomes": ["Analyze SaaS Metrics", "Evaluate Total Addressable Market"], "takeaways": ["Growth rates justify higher valuations", "Profitability may be delayed for scale"]}, | |
| {"id": 48, "parent": 47, "cat": "Strategy", "difficulty": 2, "name": "Global Investing", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "90k", "completed": False, "ratings": [4], "video_intro": "Emerging markets.", "outcomes": ["Invest in Emerging Markets", "Manage Currency Risk"], "takeaways": ["Home bias limits your opportunity set", "Global diversification smooths returns"]}, | |
| {"id": 49, "parent": 48, "cat": "Psychology", "difficulty": 2, "name": "Psychology of Trading", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "200k", "completed": False, "ratings": [5], "video_intro": "Mastering your mind.", "outcomes": ["Develop Trading Discipline", "Maintain Emotional Balance"], "takeaways": ["Your mindset is your biggest asset", "Stick to your plan when emotions run high"]}, | |
| {"id": 50, "parent": 49, "cat": "Foundations", "difficulty": 2, "name": "Investment Banking", "video": "p7HKvqRI_Bo", "source": "TED-Ed", "views": "150k", "completed": False, "ratings": [4], "video_intro": "Inside Wall Street.", "outcomes": ["Understand M&A Process", "Analyze Capital Markets"], "takeaways": ["Investment banks facilitate global flows", "Understanding incentives explains market moves"]}, | |
| ] | |
| ACADEMY_DB = {m["id"]: m for m in ACADEMY_DATA} | |
| COMMENTS_STORE = {i: [ | |
| {"id": f"init-{i}", "user": "Analyst_Pro", "text": "Essential for any serious trader.", "likes": random.randint(1, 20), "timestamp": 1738900000, "replies": [ | |
| {"id": f"r{i}", "user": f"User_{random.randint(1000, 9999)}", "text": "Agree, very helpful!", "likes": random.randint(0, 5)} | |
| ]} | |
| ] for i in range(1, 51)} | |
| # --- Lab: Stock Pool (US equities, ETFs, and crypto) --- | |
| STOCK_POOL = MARKET_POOL | |
| USER_PORTFOLIOS = defaultdict(lambda: { | |
| "cash": 100000.0, | |
| "holdings": {}, | |
| "history": [], | |
| }) | |
| CHAT_SESSIONS = defaultdict(lambda: {}) | |
| INQUIRY_STORE = [] | |
| def ensure_user_session(): | |
| if 'user_id' not in session: | |
| session['user_id'] = str(uuid.uuid4()) | |
| def assign_user_session(): | |
| ensure_user_session() | |
| def build_fallback_news(symbol): | |
| return generate_news(symbol) | |
| def serve_spa_entry(path=""): | |
| if UI_DIST_DIR.exists(): | |
| candidate = UI_DIST_DIR / path if path else UI_DIST_DIR / "index.html" | |
| if path and candidate.is_file(): | |
| return send_from_directory(UI_DIST_DIR, path) | |
| index_file = UI_DIST_DIR / "index.html" | |
| if index_file.exists(): | |
| return send_from_directory(UI_DIST_DIR, "index.html") | |
| if path in ("", "index.html"): | |
| return render_template('index.html') | |
| return abort(404) | |
| def get_hero_videos(): | |
| return jsonify({"videos": list_media_files(VIDEO_DIR)}) | |
| def get_contact_videos(): | |
| return jsonify({"videos": list_media_files(CONTACT_VIDEO_DIR)}) | |
| def health_check(): | |
| writable, error = directory_is_writable(DATA_DIR) | |
| status = { | |
| "status": "ok" if writable else "degraded", | |
| "service": "u2invest", | |
| "timestamp": datetime.utcnow().isoformat() + "Z", | |
| "agent_configured": agent_is_configured(), | |
| "storage": { | |
| "data_dir": str(DATA_DIR), | |
| "checkpoints_db": str(CHECKPOINTS_DB_PATH), | |
| "chroma_db_dir": str(CHROMA_DB_DIR), | |
| "knowledge_base_path": str(KNOWLEDGE_BASE_PATH), | |
| "writable": writable, | |
| }, | |
| "frontend": { | |
| "ui_dist_exists": UI_DIST_DIR.exists(), | |
| "hero_video_count": len(list_media_files(VIDEO_DIR)), | |
| "contact_video_count": len(list_media_files(CONTACT_VIDEO_DIR)), | |
| }, | |
| } | |
| if error: | |
| status["storage"]["error"] = error | |
| return jsonify(status), 200 if writable else 503 | |
| def serve_media_file(directory, filename): | |
| requested_path = Path(filename) | |
| if ".." in requested_path.parts: | |
| return abort(404) | |
| if requested_path.suffix.lower() not in HERO_VIDEO_EXTENSIONS: | |
| return abort(404) | |
| target = directory / requested_path | |
| if not target.is_file(): | |
| return abort(404) | |
| return send_from_directory(directory, str(requested_path), conditional=True) | |
| def serve_hero_video(filename): | |
| return serve_media_file(VIDEO_DIR, filename) | |
| def serve_contact_video(filename): | |
| return serve_media_file(CONTACT_VIDEO_DIR, filename) | |
| # ==================== Academy APIs (100% Preserved) ==================== | |
| def get_academy(): | |
| return jsonify(list(ACADEMY_DB.values())) | |
| def get_course(cid): | |
| course = ACADEMY_DB.get(cid).copy() | |
| rates = course.get('ratings', [5]) | |
| course['avg_rating'] = round(sum(rates) / len(rates), 1) | |
| course['comments'] = COMMENTS_STORE.get(cid, []) | |
| return jsonify(course) | |
| def post_comment(): | |
| data = request.json | |
| cid = data.get('id') | |
| text = data.get('text') | |
| parent_id = data.get('parentId') | |
| if not cid or not text: | |
| return jsonify({"status": "error", "message": "Missing content"}), 400 | |
| new_id = str(uuid.uuid4())[:8] | |
| user_name = f"User_{uuid.uuid4().hex[:4]}" | |
| if parent_id: | |
| for c in COMMENTS_STORE.get(cid, []): | |
| if c['id'] == parent_id: | |
| c['replies'].append({ | |
| "id": new_id, | |
| "user": user_name, | |
| "text": text, | |
| "likes": 0, | |
| "timestamp": int(time.time()) | |
| }) | |
| break | |
| else: | |
| COMMENTS_STORE[cid].insert(0, { | |
| "id": new_id, | |
| "user": user_name, | |
| "text": text, | |
| "likes": 0, | |
| "replies": [], | |
| "timestamp": int(time.time()) | |
| }) | |
| return jsonify({"status": "success", "comments": COMMENTS_STORE[cid]}) | |
| def like_comment_main(): | |
| data = request.json | |
| cid, cmid, action = data.get('courseId'), data.get('commentId'), data.get('action') | |
| for c in COMMENTS_STORE[cid]: | |
| if c['id'] == cmid: | |
| c['likes'] += 1 if action == 'inc' else -1 | |
| return jsonify({"status": "success", "likes": c['likes']}) | |
| return jsonify({"status": "error"}), 404 | |
| def like_reply_unique(): | |
| data = request.json | |
| cid, cmid, rid, action = data.get('courseId'), data.get('commentId'), data.get('replyId'), data.get('action') | |
| for c in COMMENTS_STORE[cid]: | |
| if c['id'] == cmid: | |
| for r in c['replies']: | |
| if r['id'] == rid: | |
| r['likes'] = r.get('likes', 0) + (1 if action == 'inc' else -1) | |
| return jsonify({"status": "success", "comments": COMMENTS_STORE[cid]}) | |
| return jsonify({"status": "error"}), 404 | |
| def toggle_complete(): | |
| data = request.json | |
| cid, status = data.get('id'), data.get('status') | |
| if cid in ACADEMY_DB: | |
| ACADEMY_DB[cid]['completed'] = status | |
| return jsonify({"status": "success"}) | |
| return jsonify({"status": "error"}), 404 | |
| def rate_course(): | |
| data = request.json | |
| cid, score = data.get('id'), data.get('score') | |
| ACADEMY_DB[cid]['ratings'].append(score) | |
| avg = round(sum(ACADEMY_DB[cid]['ratings']) / len(ACADEMY_DB[cid]['ratings']), 1) | |
| return jsonify({"status": "success", "avg": avg}) | |
| def get_market_data(): | |
| symbol = request.args.get('symbol', 'AAPL').strip() or 'AAPL' | |
| data = [] | |
| for index, point in enumerate(generate_kline(symbol, 120)): | |
| data.append({ | |
| "time": index, | |
| "open": point["open"], | |
| "close": point["close"], | |
| "high": point["high"], | |
| "low": point["low"], | |
| "vol": point["volume"], | |
| }) | |
| return jsonify(data) | |
| def get_stock_pool(): | |
| """Return the Trading Lab asset universe.""" | |
| print(f"Stock pool requested: {list(STOCK_POOL.keys())}") | |
| return jsonify(STOCK_POOL) | |
| def get_real_quote(): | |
| """Return demo quotes for supported US stocks, ETFs, and crypto assets.""" | |
| symbols = [symbol.strip() for symbol in request.args.get('symbols', 'AAPL').split(',') if symbol.strip()] | |
| print(f"Quote requested for: {symbols}") | |
| return jsonify({"status": "success", "data": generate_quotes(symbols)}) | |
| def get_kline_data(): | |
| """Return demo K-line data for supported US stocks, ETFs, and crypto assets.""" | |
| symbol = request.args.get('symbol', 'AAPL') | |
| days = int(request.args.get('days', 60)) | |
| print(f"K-line requested: {symbol}, {days} days") | |
| return jsonify({"status": "success", "symbol": symbol, "data": generate_kline(symbol, days)}) | |
| def get_stock_news_feed(): | |
| """Return demo news for supported US stocks, ETFs, and crypto assets.""" | |
| symbol = request.args.get('symbol', 'AAPL').strip() | |
| return jsonify({"status": "success", "symbol": symbol, "data": build_fallback_news(symbol)}) | |
| def get_portfolio(): | |
| """Get portfolio""" | |
| user_id = session.get('user_id', 'default') | |
| return jsonify(USER_PORTFOLIOS[user_id]) | |
| def execute_trade(): | |
| """Execute trade""" | |
| user_id = session.get('user_id', 'default') | |
| portfolio = USER_PORTFOLIOS[user_id] | |
| data = request.json | |
| action = data.get('action') | |
| symbol = data.get('symbol') | |
| shares = int(data.get('shares', 0)) | |
| price = float(data.get('price', 0)) | |
| if shares <= 0 or price <= 0: | |
| return jsonify({"status": "error", "message": "Invalid input"}), 400 | |
| total_value = shares * price | |
| if action == 'buy': | |
| if portfolio["cash"] < total_value: | |
| return jsonify({"status": "error", "message": "Insufficient cash"}), 400 | |
| portfolio["cash"] -= total_value | |
| if symbol in portfolio["holdings"]: | |
| old_shares = portfolio["holdings"][symbol]["shares"] | |
| old_cost = portfolio["holdings"][symbol]["cost_basis"] | |
| new_shares = old_shares + shares | |
| new_cost = old_cost + total_value | |
| portfolio["holdings"][symbol] = { | |
| "shares": new_shares, | |
| "avg_price": new_cost / new_shares, | |
| "cost_basis": new_cost | |
| } | |
| else: | |
| portfolio["holdings"][symbol] = { | |
| "shares": shares, | |
| "avg_price": price, | |
| "cost_basis": total_value | |
| } | |
| portfolio["history"].append({ | |
| "id": str(uuid.uuid4())[:8], | |
| "timestamp": datetime.now().isoformat(), | |
| "action": "BUY", | |
| "symbol": symbol, | |
| "shares": shares, | |
| "price": price, | |
| "total": total_value | |
| }) | |
| print(f"้?BUY: {shares} x {symbol} @ ${price}") | |
| return jsonify({"status": "success", "message": f"Bought {shares} shares"}) | |
| elif action == 'sell': | |
| if symbol not in portfolio["holdings"]: | |
| return jsonify({"status": "error", "message": "No holdings"}), 400 | |
| if portfolio["holdings"][symbol]["shares"] < shares: | |
| return jsonify({"status": "error", "message": "Insufficient shares"}), 400 | |
| portfolio["cash"] += total_value | |
| portfolio["holdings"][symbol]["shares"] -= shares | |
| portfolio["holdings"][symbol]["cost_basis"] -= shares * portfolio["holdings"][symbol]["avg_price"] | |
| if portfolio["holdings"][symbol]["shares"] == 0: | |
| del portfolio["holdings"][symbol] | |
| portfolio["history"].append({ | |
| "id": str(uuid.uuid4())[:8], | |
| "timestamp": datetime.now().isoformat(), | |
| "action": "SELL", | |
| "symbol": symbol, | |
| "shares": shares, | |
| "price": price, | |
| "total": total_value | |
| }) | |
| print(f"้?SELL: {shares} x {symbol} @ ${price}") | |
| return jsonify({"status": "success", "message": f"Sold {shares} shares"}) | |
| return jsonify({"status": "error", "message": "Invalid action"}), 400 | |
| def reset_portfolio(): | |
| """Reset portfolio""" | |
| user_id = session.get('user_id', 'default') | |
| USER_PORTFOLIOS[user_id] = { | |
| "cash": 100000.0, | |
| "holdings": {}, | |
| "history": [], | |
| } | |
| print(f"้ฆๆง Portfolio reset for {user_id}") | |
| return jsonify({"status": "success"}) | |
| # ==================== Agent APIs ==================== | |
| def agent_chat(): | |
| """Agent chat""" | |
| user_id = session.get('user_id', 'default') | |
| data = request.json | |
| user_message = data.get('message', '') | |
| session_id = data.get('session_id') | |
| if not user_message.strip(): | |
| return jsonify({"status": "error", "message": "Empty message"}), 400 | |
| # Generate new session if not provided or doesn't exist | |
| if not session_id or session_id not in CHAT_SESSIONS[user_id]: | |
| session_id = str(uuid.uuid4()) | |
| CHAT_SESSIONS[user_id][session_id] = { | |
| "id": session_id, | |
| "title": (user_message[:30] + "...") if len(user_message) > 30 else user_message, | |
| "messages": [], | |
| "timestamp": datetime.now().isoformat() | |
| } | |
| current_session = CHAT_SESSIONS[user_id][session_id] | |
| if not agent_is_configured(): | |
| current_session["messages"].append({ | |
| "role": "user", | |
| "content": user_message, | |
| "timestamp": datetime.now().isoformat() | |
| }) | |
| error_msg = "The agent backend is not configured. Set the required API key in your cloud service environment." | |
| current_session["messages"].append({ | |
| "role": "assistant", | |
| "content": error_msg, | |
| "timestamp": datetime.now().isoformat(), | |
| "tools_used": [] | |
| }) | |
| return jsonify({"status": "error", "response": error_msg}), 503 | |
| current_session["messages"].append({ | |
| "role": "user", | |
| "content": user_message, | |
| "timestamp": datetime.now().isoformat() | |
| }) | |
| print(f"้ฆๆฐ User message ({session_id}): {user_message}") | |
| # Try real agent | |
| try: | |
| response_content, tool_results, backend_used = run_agent_message( | |
| user_message, | |
| session_id, | |
| ) | |
| current_session["messages"].append({ | |
| "role": "assistant", | |
| "content": response_content or "I analyzed your question. Please try rephrasing.", | |
| "timestamp": datetime.now().isoformat(), | |
| "tools_used": tool_results | |
| }) | |
| print(f"้?Agent response ({backend_used}): {response_content[:100]}...") | |
| return jsonify({ | |
| "status": "success", | |
| "session_id": session_id, | |
| "response": response_content or "Please rephrase your question.", | |
| "tools_used": tool_results | |
| }) | |
| except ImportError as e: | |
| error_msg = f"้ฟ็ ็ฌ Agent not configured. Check agent_graph.py and .env file. Error: {str(e)}" | |
| print(error_msg) | |
| current_session["messages"].append({ | |
| "role": "assistant", | |
| "content": error_msg, | |
| "timestamp": datetime.now().isoformat() | |
| }) | |
| return jsonify({"status": "error", "response": error_msg}), 500 | |
| except Exception as e: | |
| error_msg = f"Agent error: {str(e)}" | |
| print(f"้?{error_msg}") | |
| current_session["messages"].append({ | |
| "role": "assistant", | |
| "content": error_msg, | |
| "timestamp": datetime.now().isoformat() | |
| }) | |
| return jsonify({"status": "error", "response": error_msg}), 500 | |
| def get_chat_sessions(): | |
| """Get list of chat sessions""" | |
| user_id = session.get('user_id', 'default') | |
| user_sessions = CHAT_SESSIONS[user_id] | |
| # Return list sorted by timestamp desc | |
| session_list = sorted( | |
| [{"id": s["id"], "title": s["title"], "timestamp": s["timestamp"]} for s in user_sessions.values()], | |
| key=lambda x: x["timestamp"], | |
| reverse=True | |
| ) | |
| return jsonify({"status": "success", "sessions": session_list}) | |
| def get_chat_history(): | |
| """Get chat history for a specific session""" | |
| user_id = session.get('user_id', 'default') | |
| session_id = request.args.get('session_id') | |
| if not session_id or session_id not in CHAT_SESSIONS[user_id]: | |
| return jsonify({"status": "error", "message": "Session not found"}), 404 | |
| return jsonify({"status": "success", "history": CHAT_SESSIONS[user_id][session_id]["messages"]}) | |
| def clear_chat_history(): | |
| """Clear chat history (specific session or all)""" | |
| user_id = session.get('user_id', 'default') | |
| data = request.json or {} | |
| session_id = data.get('session_id') | |
| if session_id: | |
| if session_id in CHAT_SESSIONS[user_id]: | |
| del CHAT_SESSIONS[user_id][session_id] | |
| print(f"้ฆๆฃ้? Session {session_id} cleared for {user_id}") | |
| else: | |
| CHAT_SESSIONS[user_id] = {} | |
| print(f"้ฆๆฃ้? All chats cleared for {user_id}") | |
| return jsonify({"status": "success"}) | |
| def submit_inquiry(): | |
| """Record public-site enquiries from the redesigned UI.""" | |
| data = request.json or {} | |
| name = str(data.get('name', '')).strip() | |
| email = str(data.get('email', '')).strip() | |
| consent = bool(data.get('consent')) | |
| if not name or not email or not consent: | |
| return jsonify({"status": "error", "message": "Name, email, and consent are required."}), 400 | |
| inquiry = { | |
| "id": str(uuid.uuid4())[:8], | |
| "timestamp": datetime.now().isoformat(), | |
| "source": str(data.get('source', 'website')).strip() or "website", | |
| "inquiry_type": str(data.get('inquiry_type', 'general')).strip() or "general", | |
| "name": name, | |
| "email": email, | |
| "company": str(data.get('company', '')).strip(), | |
| "role": str(data.get('role', '')).strip(), | |
| "message": str(data.get('message', '')).strip(), | |
| "user_id": session.get('user_id'), | |
| } | |
| INQUIRY_STORE.insert(0, inquiry) | |
| del INQUIRY_STORE[200:] | |
| print(f"Inquiry received: {inquiry['id']} ({inquiry['inquiry_type']}) from {email}") | |
| return jsonify({"status": "success", "id": inquiry["id"]}) | |
| def serve_frontend(path): | |
| if path.startswith('api/'): | |
| return abort(404) | |
| return serve_spa_entry(path) | |
| if __name__ == '__main__': | |
| print("\n" + "="*60) | |
| print("U2INVEST Server Starting...") | |
| print("="*60) | |
| if not os.getenv('FLASK_SECRET_KEY'): | |
| print("WARNING: Using development secret key.") | |
| print(f"Stock Pool: {list(STOCK_POOL.keys())}") | |
| print(f"Academy Modules: {len(ACADEMY_DB)}") | |
| print("="*60 + "\n") | |
| debug_default = 'false' if os.getenv('PORT') else 'true' | |
| app.run( | |
| debug=os.getenv('FLASK_DEBUG', debug_default).lower() == 'true', | |
| host=os.getenv('FLASK_HOST', '0.0.0.0'), | |
| port=int(os.getenv('PORT', os.getenv('FLASK_PORT', '5000'))) | |
| ) | |