diff --git a/.gitignore b/.gitignore index be4efe6..06f0ccd 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,11 @@ +# Environment variables +.env +#.env.* + +# Cache directories +cache/ +**/cache/ + # Python __pycache__/ *.py[cod] @@ -25,8 +33,8 @@ wheels/ # Virtual Environment venv/ -env/ ENV/ +env/ # IDE .idea/ @@ -35,18 +43,13 @@ ENV/ *.swo # Logs -logs/ *.log - -# Cache -.cache/ -__pycache__/ +logs/ # Local development -.env .env.local .env.*.local # System .DS_Store -Thumbs.db \ No newline at end of file +Thumbs.db diff --git a/Caddyfile b/Caddyfile index 7ed860f..2565b9a 100644 --- a/Caddyfile +++ b/Caddyfile @@ -8,7 +8,7 @@ invest.trader-lab.com { # Main ETF Suite Launcher handle / { - reverse_proxy etf-launcher:8500 { + reverse_proxy etf_portal-etf-launcher-1:8500 { header_up Host {host} header_up X-Real-IP {remote} header_up X-Forwarded-For {remote} @@ -18,7 +18,7 @@ invest.trader-lab.com { # Static resources for Streamlit handle /_stcore/* { - reverse_proxy etf-launcher:8500 { + reverse_proxy etf_portal-etf-launcher-1:8500 { header_up Host {host} header_up X-Real-IP {remote} header_up X-Forwarded-For {remote} @@ -27,7 +27,7 @@ invest.trader-lab.com { } handle /static/* { - reverse_proxy etf-launcher:8500 { + reverse_proxy etf_portal-etf-launcher-1:8500 { header_up Host {host} header_up X-Real-IP {remote} header_up X-Forwarded-For {remote} diff --git a/README.md b/README.md index 05ff28e..3d83032 100644 --- a/README.md +++ b/README.md @@ -23,6 +23,21 @@ source venv/bin/activate # On Linux/Mac pip install -e . ``` +## Environment Setup + +1. Copy the environment template: + ```bash + cp .env.template .env + ``` + +2. Edit the `.env` file and add your API keys: + ``` + FMP_API_KEY=your_api_key_here + CACHE_DURATION_HOURS=24 + ``` + +3. Never commit the `.env` file to version control. + ## Usage The ETF Portal provides a command-line interface for managing the application: diff --git a/cache/48cf9534181b3f9151f2c27d08ecf5c0.json b/cache/48cf9534181b3f9151f2c27d08ecf5c0.json deleted file mode 100644 index ea61f16..0000000 --- a/cache/48cf9534181b3f9151f2c27d08ecf5c0.json +++ /dev/null @@ -1 +0,0 @@ -{"data": {"__pandas_series__": true, "index": [ \ No newline at end of file diff --git a/cache/4ba6cb61b51b694187cfb678fc62dd84.json b/cache/4ba6cb61b51b694187cfb678fc62dd84.json deleted file mode 100644 index 806f13b..0000000 --- a/cache/4ba6cb61b51b694187cfb678fc62dd84.json +++ /dev/null @@ -1 +0,0 @@ -{"data": {"longBusinessSummary": "The fund is an actively managed exchange-traded fund that seeks current income while providing direct and/or indirect exposure to the share price of select U.S. listed securities, subject to a limit on potential investment gains. It uses both traditional and synthetic covered call strategies that are designed to produce higher income levels when the underlying securities experience more volatility. The fund is non-diversified.", "companyOfficers": [], "executiveTeam": [], "maxAge": 86400, "priceHint": 2, "previousClose": 6.1, "open": 6.02, "dayLow": 6.02, "dayHigh": 6.13, "regularMarketPreviousClose": 6.1, "regularMarketOpen": 6.02, "regularMarketDayLow": 6.02, "regularMarketDayHigh": 6.13, "trailingPE": 26.701918, "volume": 2869944, "regularMarketVolume": 2869944, "averageVolume": 1994822, "averageVolume10days": 3513250, "averageDailyVolume10Day": 3513250, "bid": 6.1, "ask": 6.12, "bidSize": 8, "askSize": 30, "yield": 1.6552, "totalAssets": 226379696, "fiftyTwoWeekLow": 5.23, "fiftyTwoWeekHigh": 15.22, "fiftyDayAverage": 6.0538, "twoHundredDayAverage": 8.74755, "navPrice": 6.0906, "currency": "USD", "tradeable": false, "category": "Derivative Income", "ytdReturn": -11.21162, "beta3Year": 0.0, "fundFamily": "YieldMax ETFs", "fundInceptionDate": 1709078400, "legalType": "Exchange Traded Fund", "quoteType": "ETF", "symbol": "ULTY", "language": "en-US", "region": "US", "typeDisp": "ETF", "quoteSourceName": "Delayed Quote", "triggerable": true, "customPriceAlertConfidence": "HIGH", "shortName": "Tidal Trust II YieldMax Ultra O", "longName": "YieldMax Ultra Option Income Strategy ETF", "fiftyTwoWeekLowChange": 0.8699999, "fiftyTwoWeekLowChangePercent": 0.16634797, "fiftyTwoWeekRange": "5.23 - 15.22", "fiftyTwoWeekHighChange": -9.120001, "fiftyTwoWeekHighChangePercent": -0.59921163, "fiftyTwoWeekChangePercent": -57.282913, "dividendYield": 165.52, "trailingThreeMonthReturns": -13.87, "trailingThreeMonthNavReturns": -13.87, "netAssets": 226379696.0, "epsTrailingTwelveMonths": 0.228448, "marketState": "CLOSED", "regularMarketChangePercent": 0.0, "regularMarketPrice": 6.1, "corporateActions": [], "postMarketTime": 1748044768, "regularMarketTime": 1748030400, "exchange": "PCX", "messageBoardId": "finmb_1869805004", "exchangeTimezoneName": "America/New_York", "exchangeTimezoneShortName": "EDT", "gmtOffSetMilliseconds": -14400000, "market": "us_market", "esgPopulated": false, "fiftyDayAverageChange": 0.0461998, "fiftyDayAverageChangePercent": 0.007631537, "twoHundredDayAverageChange": -2.64755, "twoHundredDayAverageChangePercent": -0.3026619, "netExpenseRatio": 1.3, "sourceInterval": 15, "exchangeDataDelayedBy": 0, "cryptoTradeable": false, "hasPrePostMarketData": true, "firstTradeDateMilliseconds": 1709217000000, "postMarketChangePercent": 0.49180672, "postMarketPrice": 6.13, "postMarketChange": 0.03000021, "regularMarketChange": 0.0, "regularMarketDayRange": "6.02 - 6.13", "fullExchangeName": "NYSEArca", "averageDailyVolume3Month": 1994822, "trailingPegRatio": null}, "timestamp": "2025-05-24T13:52:58.466664"} \ No newline at end of file diff --git a/cache/86bd26d004d7951035b2e678a3fa6ccf.json b/cache/86bd26d004d7951035b2e678a3fa6ccf.json deleted file mode 100644 index ea61f16..0000000 --- a/cache/86bd26d004d7951035b2e678a3fa6ccf.json +++ /dev/null @@ -1 +0,0 @@ -{"data": {"__pandas_series__": true, "index": [ \ No newline at end of file diff --git a/cache/86cc9fb558c9ab4da62c8a741e919889.json b/cache/86cc9fb558c9ab4da62c8a741e919889.json deleted file mode 100644 index ea61f16..0000000 --- a/cache/86cc9fb558c9ab4da62c8a741e919889.json +++ /dev/null @@ -1 +0,0 @@ -{"data": {"__pandas_series__": true, "index": [ \ No newline at end of file diff --git a/cache/a4d5861d1e785dcd29a4155b49156709.json b/cache/a4d5861d1e785dcd29a4155b49156709.json deleted file mode 100644 index a9616f3..0000000 --- a/cache/a4d5861d1e785dcd29a4155b49156709.json +++ /dev/null @@ -1 +0,0 @@ -{"data": {"longBusinessSummary": "The fund is an actively managed exchange-traded fund (\u201cETF\u201d) that seeks current income while maintaining the opportunity for exposure to the share price (i.e., the price returns) of the securities of the companies comprising the Solactive FANG Innovation Index. The fund advisor seeks to employ the fund's investment strategy regardless of whether there are periods of adverse market, economic, or other conditions and will not seek to take temporary defensive positions during such periods. It is non-diversified.", "companyOfficers": [], "executiveTeam": [], "maxAge": 86400, "priceHint": 2, "previousClose": 43.66, "open": 43.12, "dayLow": 43.02, "dayHigh": 43.55, "regularMarketPreviousClose": 43.66, "regularMarketOpen": 43.12, "regularMarketDayLow": 43.02, "regularMarketDayHigh": 43.55, "trailingPE": 36.65259, "volume": 129662, "regularMarketVolume": 129662, "averageVolume": 147330, "averageVolume10days": 111310, "averageDailyVolume10Day": 111310, "bid": 43.11, "ask": 43.35, "bidSize": 2, "askSize": 2, "yield": 0.3039, "totalAssets": 426724160, "fiftyTwoWeekLow": 35.44, "fiftyTwoWeekHigh": 56.44, "fiftyDayAverage": 41.907, "twoHundredDayAverage": 48.12085, "navPrice": 43.62, "currency": "USD", "tradeable": false, "category": "Derivative Income", "ytdReturn": -8.89758, "beta3Year": 0.0, "fundFamily": "REX Advisers, LLC", "fundInceptionDate": 1696982400, "legalType": "Exchange Traded Fund", "quoteType": "ETF", "symbol": "FEPI", "language": "en-US", "region": "US", "typeDisp": "ETF", "quoteSourceName": "Nasdaq Real Time Price", "triggerable": true, "customPriceAlertConfidence": "HIGH", "corporateActions": [], "postMarketTime": 1748040939, "regularMarketTime": 1748030400, "hasPrePostMarketData": true, "firstTradeDateMilliseconds": 1697031000000, "postMarketChangePercent": 0.716766, "postMarketPrice": 43.56, "postMarketChange": 0.310001, "regularMarketChange": -0.40999985, "regularMarketDayRange": "43.02 - 43.55", "fullExchangeName": "NasdaqGM", "averageDailyVolume3Month": 147330, "fiftyTwoWeekLowChange": 7.8100014, "fiftyTwoWeekLowChangePercent": 0.22037251, "fiftyTwoWeekRange": "35.44 - 56.44", "fiftyTwoWeekHighChange": -13.189999, "fiftyTwoWeekHighChangePercent": -0.23369949, "fiftyTwoWeekChangePercent": -20.72947, "dividendYield": 30.39, "trailingThreeMonthReturns": -9.55848, "trailingThreeMonthNavReturns": -9.55848, "netAssets": 426724160.0, "epsTrailingTwelveMonths": 1.1799984, "fiftyDayAverageChange": 1.3429985, "fiftyDayAverageChangePercent": 0.032047115, "twoHundredDayAverageChange": -4.8708496, "twoHundredDayAverageChangePercent": -0.10122119, "netExpenseRatio": 0.65, "sourceInterval": 15, "exchangeDataDelayedBy": 0, "ipoExpectedDate": "2023-10-11", "cryptoTradeable": false, "marketState": "CLOSED", "shortName": "REX FANG & Innovation Equity Pr", "longName": "REX FANG & Innovation Equity Premium Income ETF", "exchange": "NGM", "messageBoardId": "finmb_1843173608", "exchangeTimezoneName": "America/New_York", "exchangeTimezoneShortName": "EDT", "gmtOffSetMilliseconds": -14400000, "market": "us_market", "esgPopulated": false, "regularMarketChangePercent": -0.93907434, "regularMarketPrice": 43.25, "trailingPegRatio": null}, "timestamp": "2025-05-24T13:52:58.472581"} \ No newline at end of file diff --git a/cache/f039bfbd574b918f6ccf351f22a2c0c8.json b/cache/f039bfbd574b918f6ccf351f22a2c0c8.json deleted file mode 100644 index 69cef41..0000000 --- a/cache/f039bfbd574b918f6ccf351f22a2c0c8.json +++ /dev/null @@ -1 +0,0 @@ -{"data": {"longBusinessSummary": "The fund is an actively managed ETF that seeks current income while maintaining the opportunity for exposure to the share price of the common stock of MicroStrategy Incorporated, subject to a limit on potential investment gains. It will seek to employ its investment strategy as it relates to MSTR regardless of whether there are periods of adverse market, economic, or other conditions and will not seek to take temporary defensive positions during such periods. The fund is non-diversified.", "companyOfficers": [], "executiveTeam": [], "maxAge": 86400, "priceHint": 2, "previousClose": 23.08, "open": 22.68, "dayLow": 21.2901, "dayHigh": 22.7, "regularMarketPreviousClose": 23.08, "regularMarketOpen": 22.68, "regularMarketDayLow": 21.2901, "regularMarketDayHigh": 22.7, "volume": 18359202, "regularMarketVolume": 18359202, "averageVolume": 7904033, "averageVolume10days": 11083420, "averageDailyVolume10Day": 11083420, "bid": 21.5, "ask": 21.63, "bidSize": 12, "askSize": 32, "yield": 1.2471, "totalAssets": 3270944256, "fiftyTwoWeekLow": 17.1, "fiftyTwoWeekHigh": 46.5, "fiftyDayAverage": 22.1824, "twoHundredDayAverage": 26.1411, "navPrice": 23.0514, "currency": "USD", "tradeable": false, "category": "Derivative Income", "ytdReturn": 24.89181, "beta3Year": 0.0, "fundFamily": "YieldMax ETFs", "fundInceptionDate": 1708473600, "legalType": "Exchange Traded Fund", "quoteType": "ETF", "symbol": "MSTY", "language": "en-US", "region": "US", "typeDisp": "ETF", "quoteSourceName": "Delayed Quote", "triggerable": true, "customPriceAlertConfidence": "HIGH", "marketState": "CLOSED", "shortName": "Tidal Trust II YieldMax MSTR Op", "regularMarketChangePercent": -6.7591, "regularMarketPrice": 21.52, "corporateActions": [], "longName": "Yieldmax MSTR Option Income Strategy ETF", "postMarketTime": 1748044797, "regularMarketTime": 1748030400, "regularMarketDayRange": "21.2901 - 22.7", "fullExchangeName": "NYSEArca", "averageDailyVolume3Month": 7904033, "fiftyTwoWeekLowChange": 4.42, "fiftyTwoWeekLowChangePercent": 0.25847954, "fiftyTwoWeekRange": "17.1 - 46.5", "fiftyTwoWeekHighChange": -24.98, "fiftyTwoWeekHighChangePercent": -0.53720427, "fiftyTwoWeekChangePercent": -38.654507, "dividendYield": 124.71, "trailingThreeMonthReturns": 13.21012, "trailingThreeMonthNavReturns": 13.21012, "netAssets": 3270944260.0, "fiftyDayAverageChange": -0.6623993, "fiftyDayAverageChangePercent": -0.02986148, "twoHundredDayAverageChange": -4.6210995, "twoHundredDayAverageChangePercent": -0.17677525, "netExpenseRatio": 0.99, "sourceInterval": 15, "exchangeDataDelayedBy": 0, "cryptoTradeable": false, "hasPrePostMarketData": true, "firstTradeDateMilliseconds": 1708612200000, "postMarketChangePercent": -0.18587786, "postMarketPrice": 21.48, "postMarketChange": -0.040000916, "regularMarketChange": -1.56, "exchange": "PCX", "messageBoardId": "finmb_1850981069", "exchangeTimezoneName": "America/New_York", "exchangeTimezoneShortName": "EDT", "gmtOffSetMilliseconds": -14400000, "market": "us_market", "esgPopulated": false, "trailingPegRatio": null}, "timestamp": "2025-05-24T13:52:58.456018"} \ No newline at end of file diff --git a/docker-compose.yml b/docker-compose.yml index 7670248..00e8cc5 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -6,6 +6,8 @@ services: command: streamlit run ETF_Suite_Launcher.py --server.port=8500 volumes: - .:/app + ports: + - "8500:8500" networks: - etf_network environment: diff --git a/requirements.txt b/requirements.txt index b761e75..ad49c31 100644 --- a/requirements.txt +++ b/requirements.txt @@ -16,4 +16,5 @@ plotly>=5.14.1 requests>=2.31.0 reportlab>=3.6.13 psutil>=5.9.0 -click>=8.1.0 \ No newline at end of file +click>=8.1.0 +yfinance>=0.2.36 \ No newline at end of file diff --git a/scripts/setup_secrets.py b/scripts/setup_secrets.py new file mode 100644 index 0000000..c3d987e --- /dev/null +++ b/scripts/setup_secrets.py @@ -0,0 +1,71 @@ +import os +import sys +import logging +from pathlib import Path + +def setup_secrets(): + """Set up secrets for the ETF Portal application.""" + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + + try: + # Get the FMP API key from user + fmp_api_key = input("Enter your FMP API key: ").strip() + if not fmp_api_key: + logger.error("āŒ FMP API key is required") + return False + + # Create .streamlit directory if it doesn't exist + streamlit_dir = Path(".streamlit") + streamlit_dir.mkdir(exist_ok=True) + + # Create secrets.toml + secrets_path = streamlit_dir / "secrets.toml" + with open(secrets_path, "w") as f: + f.write(f'# FMP API Configuration\n') + f.write(f'FMP_API_KEY = "{fmp_api_key}"\n\n') + f.write(f'# Cache Configuration\n') + f.write(f'CACHE_DURATION_HOURS = 24\n') + + # Set proper permissions + secrets_path.chmod(0o600) # Only owner can read/write + + logger.info(f"āœ… Secrets file created at {secrets_path}") + logger.info("šŸ”’ File permissions set to 600 (owner read/write only)") + + # Create cache directories + cache_dirs = [ + Path("cache/FMP_cache"), + Path("cache/yfinance_cache") + ] + + for cache_dir in cache_dirs: + cache_dir.mkdir(parents=True, exist_ok=True) + cache_dir.chmod(0o755) # Owner can read/write/execute, others can read/execute + logger.info(f"āœ… Cache directory created: {cache_dir}") + + return True + + except Exception as e: + logger.error(f"āŒ Setup failed: {str(e)}") + return False + +if __name__ == "__main__": + print("šŸ”§ ETF Portal Secrets Setup") + print("===========================") + print("This script will help you set up the secrets for the ETF Portal application.") + print("Make sure you have your FMP API key ready.") + print() + + success = setup_secrets() + if success: + print("\nāœ… Setup completed successfully!") + print("\nNext steps:") + print("1. Run the test script to verify the configuration:") + print(" python -m ETF_Portal.tests.test_api_config") + print("\n2. If you're deploying to a server, make sure to:") + print(" - Set the secrets in your Streamlit dashboard") + print(" - Create the cache directories with proper permissions") + else: + print("\nāŒ Setup failed. Check the logs for details.") + sys.exit(1) \ No newline at end of file diff --git a/scripts/setup_vps.py b/scripts/setup_vps.py new file mode 100644 index 0000000..49794da --- /dev/null +++ b/scripts/setup_vps.py @@ -0,0 +1,102 @@ +import os +import sys +import logging +from pathlib import Path +import subprocess + +def setup_vps_environment(): + """Set up the environment for the ETF Portal on VPS.""" + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + + try: + # Get the FMP API key from user + fmp_api_key = input("Enter your FMP API key: ").strip() + if not fmp_api_key: + logger.error("āŒ FMP API key is required") + return False + + # Add to environment file + env_file = Path("/etc/environment") + if not env_file.exists(): + env_file = Path.home() / ".bashrc" + + # Check if key already exists + with open(env_file, 'r') as f: + content = f.read() + + if f"FMP_API_KEY={fmp_api_key}" not in content: + with open(env_file, 'a') as f: + f.write(f'\n# ETF Portal Configuration\n') + f.write(f'export FMP_API_KEY="{fmp_api_key}"\n') + logger.info(f"āœ… Added FMP_API_KEY to {env_file}") + else: + logger.info("āœ… FMP_API_KEY already exists in environment file") + + # Create cache directories + cache_dirs = [ + Path("cache/FMP_cache"), + Path("cache/yfinance_cache") + ] + + for cache_dir in cache_dirs: + cache_dir.mkdir(parents=True, exist_ok=True) + cache_dir.chmod(0o755) # Owner can read/write/execute, others can read/execute + logger.info(f"āœ… Cache directory created: {cache_dir}") + + # Set up systemd service (if needed) + if input("Do you want to set up a systemd service for the ETF Portal? (y/n): ").lower() == 'y': + service_content = f"""[Unit] +Description=ETF Portal Streamlit App +After=network.target + +[Service] +User={os.getenv('USER')} +WorkingDirectory={Path.cwd()} +Environment="FMP_API_KEY={fmp_api_key}" +ExecStart=/usr/local/bin/streamlit run ETF_Portal/pages/ETF_Analyzer.py +Restart=always + +[Install] +WantedBy=multi-user.target +""" + service_path = Path("/etc/systemd/system/etf-portal.service") + if not service_path.exists(): + with open(service_path, 'w') as f: + f.write(service_content) + logger.info("āœ… Created systemd service file") + + # Reload systemd and enable service + subprocess.run(["sudo", "systemctl", "daemon-reload"]) + subprocess.run(["sudo", "systemctl", "enable", "etf-portal"]) + logger.info("āœ… Enabled ETF Portal service") + else: + logger.info("āœ… Service file already exists") + + return True + + except Exception as e: + logger.error(f"āŒ Setup failed: {str(e)}") + return False + +if __name__ == "__main__": + print("šŸ”§ ETF Portal VPS Setup") + print("======================") + print("This script will help you set up the ETF Portal on your VPS.") + print("Make sure you have your FMP API key ready.") + print() + + success = setup_vps_environment() + if success: + print("\nāœ… Setup completed successfully!") + print("\nNext steps:") + print("1. Source your environment file:") + print(" source /etc/environment # or source ~/.bashrc") + print("\n2. Run the test script to verify the configuration:") + print(" python -m ETF_Portal.tests.test_api_config") + print("\n3. If you set up the systemd service:") + print(" sudo systemctl start etf-portal") + print(" sudo systemctl status etf-portal # Check status") + else: + print("\nāŒ Setup failed. Check the logs for details.") + sys.exit(1) \ No newline at end of file diff --git a/setup.py b/setup.py index 019c76b..2475bf7 100644 --- a/setup.py +++ b/setup.py @@ -2,14 +2,20 @@ from setuptools import setup, find_packages setup( - name="etf-portal", + name="ETF_Portal", version="0.1.0", packages=find_packages(), include_package_data=True, install_requires=[ - "click", - "psutil", - "streamlit", + "streamlit>=1.28.0", + "pandas>=1.5.3", + "numpy>=1.24.3", + "matplotlib>=3.7.1", + "seaborn>=0.12.2", + "fmp-python>=0.1.5", + "plotly>=5.14.1", + "requests>=2.31.0", + "yfinance>=0.2.36", ], entry_points={ "console_scripts": [ diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..a4adccf --- /dev/null +++ b/tests/__init__.py @@ -0,0 +1 @@ +"""Test package for ETF Portal.""" \ No newline at end of file diff --git a/tests/test_api_config.py b/tests/test_api_config.py new file mode 100644 index 0000000..ac4ed3d --- /dev/null +++ b/tests/test_api_config.py @@ -0,0 +1,65 @@ +import streamlit as st +import logging +from api import APIFactory +import pandas as pd + +def test_api_configuration(): + """Test the API configuration and secrets.""" + logging.basicConfig(level=logging.INFO) + logger = logging.getLogger(__name__) + + try: + # Initialize API factory + api_factory = APIFactory() + + # Test FMP client + logger.info("Testing FMP client...") + fmp_client = api_factory.get_client('fmp') + + # Test with a known ETF + test_symbol = "SPY" + + # Test profile data + logger.info(f"Getting profile data for {test_symbol}...") + profile = fmp_client.get_etf_profile(test_symbol) + if isinstance(profile, dict) and not profile.get('error'): + logger.info("āœ… Profile data retrieved successfully") + else: + logger.error("āŒ Failed to get profile data") + logger.error(f"Error: {profile.get('message', 'Unknown error')}") + + # Test historical data + logger.info(f"Getting historical data for {test_symbol}...") + historical = fmp_client.get_historical_data(test_symbol, period='1mo') + if isinstance(historical, pd.DataFrame) and not historical.empty: + logger.info("āœ… Historical data retrieved successfully") + logger.info(f"Data points: {len(historical)}") + else: + logger.error("āŒ Failed to get historical data") + + # Test cache + logger.info("Testing cache...") + cache_stats = api_factory.get_cache_stats() + logger.info(f"Cache stats: {cache_stats}") + + # Test fallback to yfinance + logger.info("Testing fallback to yfinance...") + yfinance_data = api_factory.get_data(test_symbol, 'etf_profile', provider='yfinance') + if isinstance(yfinance_data, dict) and not yfinance_data.get('error'): + logger.info("āœ… YFinance fallback working") + else: + logger.error("āŒ YFinance fallback failed") + logger.error(f"Error: {yfinance_data.get('message', 'Unknown error')}") + + return True + + except Exception as e: + logger.error(f"āŒ Test failed: {str(e)}") + return False + +if __name__ == "__main__": + success = test_api_configuration() + if success: + print("\nāœ… All tests passed!") + else: + print("\nāŒ Some tests failed. Check the logs for details.") \ No newline at end of file