""" Flask Microservice for Stock Sentiment Analysis Provides stock price data and news sentiment analysis using GNews (Dual-Key) and custom fine-tuned DistilBERT. Implements in-memory caching with 5-minute TTL to prevent rate limiting. """ import sys import os import time from datetime import datetime, timedelta import pandas as pd import requests from flask import Flask, jsonify, request from flask_cors import CORS from dotenv import load_dotenv import yfinance as yf # Add the project root to sys.path to allow importing from 'brain' sys.path.append(os.path.join(os.path.dirname(__file__), '..')) from brain.sentiment.news import fetch_gnews from backend.database import NewsDatabase from brain.service import BrainService from brain.core.types import StockDataPoint, Article # Load environment variables from root .env load_dotenv(os.path.join(os.path.dirname(__file__), '..', '.env')) # Initialize Brain Service brain_service = BrainService() # Initialize Flask app app = Flask(__name__) CORS(app, origins="*") @app.before_request def log_request_info(): if request.path.startswith("/api/"): print(f"[DEBUG] Request: {request.method} {request.url}") @app.errorhandler(Exception) def handle_exception(e): if isinstance(e, requests.exceptions.HTTPError): return jsonify({"error": str(e)}), e.response.status_code return jsonify({"error": str(e)}), 500 @app.route("/", methods=["GET"]) def index(): return jsonify({ "status": "online", "message": "Stock Analysis API is running", "endpoints": ["/api/analyze", "/api/market-movers", "/api/general-news", "/health"] }) # Twelve Data API Key TWELVE_DATA_KEY = os.getenv("TWELVE_DATA_KEY") if not TWELVE_DATA_KEY: print("Warning: TWELVE_DATA_KEY not found in environment variables.") # Initialize DB db = NewsDatabase() # In-memory cache cache = {} CACHE_TTL_SECONDS = 5 * 60 # 5 minutes import json # All background compute (movers, news, screener) is handled by GitHub Actions worker.py. # HF Space only serves pre-cached data from Supabase — zero heavy compute on startup. def get_cached_data(ticker: str) -> dict | None: ticker_upper = ticker.upper() if ticker_upper in cache: cached_entry = cache[ticker_upper] age = time.time() - cached_entry["timestamp"] if age < CACHE_TTL_SECONDS: return cached_entry["data"] return None def set_cached_data(ticker: str, data: dict) -> None: cache[ticker.upper()] = { "data": data, "timestamp": time.time() } def fetch_stock_data(ticker, range_str="1W", force_refresh=False, company_name=None, skip_news_fetch=False): print(f"Checking DB for {ticker}...") analyzed_news = [] cached_news = [] if not force_refresh: cached_news = db.get_latest_news(ticker, limit=20) use_db_cache = False if cached_news and len(cached_news) >= 10: valid_cached_articles = [] for article in cached_news: if abs(article['sentiment_score']) >= 0.05: valid_cached_articles.append({ "title": article['title'], "published": article['published'], "sentiment": article['sentiment_score'], "link": article['link'], "publisher": article['source'], "debug": article['debug_metadata'] or {} }) if len(valid_cached_articles) >= 5: analyzed_news = valid_cached_articles use_db_cache = True if not use_db_cache: if not skip_news_fetch: print("Live scraping (Blocking)...") analyzed_news, _ = fetch_gnews(ticker, company_name) for article in analyzed_news: db.upsert_article(ticker, None, article) else: print(f"Skipping live news scrape for {ticker} (Pre-warm mode)") if analyzed_news: total_weighted_score = 0.0 total_weights = 0.0 for n in analyzed_news: weight = n['debug'].get('weight', 1.0) total_weighted_score += (n['sentiment'] * weight) total_weights += weight current_sentiment = total_weighted_score / total_weights if total_weights > 0 else 0.0 else: current_sentiment = 0.0 days_ytd = (datetime.now() - datetime(datetime.now().year, 1, 1)).days + 1 range_map = { "1W": "7", "1M": "30", "3M": "90", "6M": "180", "YTD": str(days_ytd), "1Y": "365", "MAX": "5000" } requested_size_str = range_map.get(range_str, "7") try: req_int = int(requested_size_str) fetch_size = max(req_int, 300) except: fetch_size = 5000; req_int = 5000 url = "https://api.twelvedata.com/time_series" params = {"symbol": ticker, "interval": "1day", "outputsize": str(fetch_size), "apikey": TWELVE_DATA_KEY} response = requests.get(url, params=params) data = response.json() if "values" not in data: raise ValueError(f"Twelve Data Error: {data.get('message', 'Unknown error')}") # Generate synthetic sentiment if news was skipped to populate the screener natively if not analyzed_news and skip_news_fetch and len(data["values"]) >= 5: try: latest = float(data["values"][0]["close"]) older = float(data["values"][4]["close"]) momentum_pct = (latest - older) / older # Map +/- 5% move to +/- 1.0 sentiment score synthetic = momentum_pct * 20.0 current_sentiment = max(-1.0, min(1.0, synthetic)) except Exception: pass full_history_data = [{ "date": d["datetime"], "open": float(d["open"]), "high": float(d["high"]), "low": float(d["low"]), "close": float(d["close"]), "volume": int(d["volume"]), "price": float(d["close"]), "sentiment": round(current_sentiment, 4) } for d in data["values"]] full_history_data.reverse() if req_int < len(full_history_data): graph_data = full_history_data[-req_int:] else: graph_data = full_history_data try: p_history = [ StockDataPoint( datetime=d["date"], open=d["open"], high=d["high"], low=d["low"], close=d["close"], volume=d["volume"] ) for d in full_history_data ] p_news = [ Article( title=n["title"], link=n["link"], published=n["published"], publisher=n["publisher"], sentiment_score=n["sentiment"], metadata=n["debug"] ) for n in analyzed_news ] analysis = brain_service.analyze_ticker(ticker, p_history, current_sentiment, p_news) tech_vals = analysis.components["technical"]["values"] tech_scores = analysis.components["technical"]["scores"] macd_vals = tech_vals.get("macd", {}) or {} quant_result = { "final_score": analysis.final_score, "signal": analysis.signal.value, "confidence": analysis.confidence, "breakdown": { "rsi_val": round(tech_vals["rsi"], 2) if tech_vals.get("rsi") is not None and not pd.isna(tech_vals["rsi"]) else None, "rsi_normalized": round(tech_scores["rsi"], 2), "sma_val": round(tech_vals["sma"], 2) if tech_vals.get("sma") is not None and not pd.isna(tech_vals["sma"]) else None, "trend_normalized": round(tech_scores["trend"], 2), "current_price": tech_vals["current_price"], "sentiment_input": analysis.sentiment_score, "sentiment_normalized": analysis.sentiment_score * 100, "macd": { "macd_line": round(macd_vals.get("macd"), 4) if macd_vals.get("macd") is not None and not pd.isna(macd_vals.get("macd")) else None, "signal_line": round(macd_vals.get("signal"), 4) if macd_vals.get("signal") is not None and not pd.isna(macd_vals.get("signal")) else None, "histogram": round(macd_vals.get("hist"), 4) if macd_vals.get("hist") is not None and not pd.isna(macd_vals.get("hist")) else None } }, "volatility": analysis.volatility or {}, "earnings": analysis.earnings or {}, "sentiment_trend": analysis.sentiment_trend or {}, "sentiment_analysis": analysis.components.get("sentiment_analysis", {}), "deep_insight": analysis.components.get("deep_insight", {}), "cross_sectional": analysis.components.get("cross_sectional", {}) } # Inject Strategy Status try: strategy_live = db.get_cache("strategy_live_signals") if not strategy_live: from backend.strategy_signals import get_strategy_live_signals strategy_live = get_strategy_live_signals() strategy_picks = {p["ticker"]: p for p in strategy_live["picks"]} is_strategy_buy = ticker in strategy_picks quant_result["strategy_scorecard"] = { "is_buy": is_strategy_buy, "regime": strategy_live["regime"], "vol_scalar": strategy_live["vol_scalar"], "momentum_175d": strategy_picks[ticker]["momentum_175d"] if is_strategy_buy else None, "target_vol": strategy_live["target_vol"], "realized_vol": strategy_live["realized_vol"] } except Exception as ve: print(f"Strategy Injection Error: {ve}") quant_result["strategy_scorecard"] = None except Exception as e: print(f"Brain Service Error: {e}") raise e scraping_stats = { "total": len(analyzed_news), "full_text": sum(1 for n in analyzed_news if n['debug'].get('content_source') == 'full_text'), "snippet": sum(1 for n in analyzed_news if n['debug'].get('content_source') != 'full_text'), "timeouts": 0 } return { "current_sentiment": round(current_sentiment, 4), "news": analyzed_news, "graph_data": graph_data, "quant_analysis": quant_result, "debug": scraping_stats } @app.route("/api/analyze", methods=["GET", "OPTIONS"]) def analyze(): if request.method == "OPTIONS": return jsonify({"status": "ok"}), 200 ticker = request.args.get("ticker") if not ticker: return jsonify({"error": "Missing required parameter: ticker"}), 400 ticker = ticker.upper().strip() range_param = request.args.get("range", "1W") force_refresh = request.args.get("force", "false").lower() == "true" company_name = request.args.get("name") cache_key = f"{ticker}_{range_param}" cached_data = get_cached_data(cache_key) if cached_data and not force_refresh: if 'news' in cached_data: cached_data['news'] = [n for n in cached_data['news'] if abs(n['sentiment']) >= 0.05] return jsonify({**cached_data, "cached": True}) try: data = fetch_stock_data(ticker, range_param, force_refresh=force_refresh, company_name=company_name) set_cached_data(cache_key, data) return jsonify({**data, "cached": False}) except Exception as e: print(f"Warning: Data provider blocked or failed. Switching to Circuit Breaker. Error: {e}") circuit_breaker_data = { "ticker": ticker, "current_sentiment": 0.0, "news": [], "graph_data": [], "circuit_breaker": True, "error": str(e) } return jsonify(circuit_breaker_data) @app.route("/api/market-movers", methods=["GET"]) def market_movers(): data = db.get_cache("market-movers") if data and "gainers" in data and len(data["gainers"]) > 0: return jsonify(data) # Fallback dummy data for presentation/dummy mode dummy_data = { "gainers": [ {"symbol": "NVDA", "name": "NVIDIA", "price": "$1250.00", "change": 5.4, "raw_change": 5.4}, {"symbol": "AMD", "name": "Advanced Micro Devices", "price": "$165.20", "change": 4.2, "raw_change": 4.2}, {"symbol": "META", "name": "Meta Platforms", "price": "$510.50", "change": 3.8, "raw_change": 3.8}, {"symbol": "AMZN", "name": "Amazon", "price": "$185.30", "change": 2.5, "raw_change": 2.5}, {"symbol": "NFLX", "name": "Netflix", "price": "$640.10", "change": 2.1, "raw_change": 2.1} ], "losers": [ {"symbol": "TSLA", "name": "Tesla", "price": "$175.40", "change": -3.5, "raw_change": -3.5}, {"symbol": "BA", "name": "Boeing", "price": "$180.20", "change": -2.8, "raw_change": -2.8}, {"symbol": "INTC", "name": "Intel", "price": "$30.50", "change": -2.1, "raw_change": -2.1}, {"symbol": "T", "name": "AT&T", "price": "$16.80", "change": -1.5, "raw_change": -1.5}, {"symbol": "VZ", "name": "Verizon", "price": "$38.90", "change": -1.2, "raw_change": -1.2} ], "active": [ {"symbol": "AAPL", "name": "Apple", "price": "$190.50", "change": 1.2, "raw_change": 1.2}, {"symbol": "NVDA", "name": "NVIDIA", "price": "$1250.00", "change": 5.4, "raw_change": 5.4}, {"symbol": "TSLA", "name": "Tesla", "price": "$175.40", "change": -3.5, "raw_change": -3.5}, {"symbol": "AMD", "name": "Advanced Micro Devices", "price": "$165.20", "change": 4.2, "raw_change": 4.2}, {"symbol": "AMZN", "name": "Amazon", "price": "$185.30", "change": 2.5, "raw_change": 2.5} ] } return jsonify(dummy_data) @app.route("/api/general-news", methods=["GET"]) def general_news(): data = db.get_cache("general-news") if data and "news" in data and len(data["news"]) > 0: return jsonify(data["news"]) # Fallback: 50 unique financial news articles for presentation mode from datetime import timedelta dummy_news = [ {"title": "S&P 500 Closes at Record High as Investors Shrug Off Recession Fears", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1611974765270-ca1258634369?q=80&w=500&auto=format&fit=crop", "publisher": "Wall Street Journal", "sentiment": 0.85}, {"title": "NVIDIA Surpasses $3 Trillion Market Cap on AI Chip Demand Surge", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1518770660439-4636190af475?q=80&w=500&auto=format&fit=crop", "publisher": "Bloomberg", "sentiment": 0.92}, {"title": "Federal Reserve Holds Rates Steady, Signals Two Cuts Before Year-End", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?q=80&w=500&auto=format&fit=crop", "publisher": "CNBC", "sentiment": 0.55}, {"title": "Tesla Shares Drop 8% After Missing Q2 Delivery Estimates", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1593941707882-a5bba14938cb?q=80&w=500&auto=format&fit=crop", "publisher": "Reuters", "sentiment": -0.65}, {"title": "Apple Unveils New AI-Powered Features at WWDC, Stock Jumps 4%", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1517694712202-14dd9538aa97?q=80&w=500&auto=format&fit=crop", "publisher": "TechCrunch", "sentiment": 0.78}, {"title": "Crude Oil Breaks Above $85 as OPEC+ Extends Production Cuts", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1518186285589-2f7649de83e0?q=80&w=500&auto=format&fit=crop", "publisher": "Financial Times", "sentiment": -0.30}, {"title": "US Unemployment Claims Fall to 52-Week Low, Labor Market Stays Tight", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1556740738-b6a63e27c4df?q=80&w=500&auto=format&fit=crop", "publisher": "MarketWatch", "sentiment": 0.60}, {"title": "Amazon Web Services Revenue Grows 19% Year-Over-Year in Cloud Push", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1526304640581-d334cdbbf45e?q=80&w=500&auto=format&fit=crop", "publisher": "Bloomberg", "sentiment": 0.72}, {"title": "China's Central Bank Cuts Key Lending Rate to Boost Slowing Economy", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1547981609-4b6bfe67ca0b?q=80&w=500&auto=format&fit=crop", "publisher": "Reuters", "sentiment": -0.20}, {"title": "JPMorgan Beats Earnings Expectations, Trading Revenue Up 21%", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1501167786227-4cba60f6d58f?q=80&w=500&auto=format&fit=crop", "publisher": "Wall Street Journal", "sentiment": 0.80}, {"title": "Gold Hits New All-Time High Above $2,500 Amid Safe-Haven Demand", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1610375461246-83df859d849d?q=80&w=500&auto=format&fit=crop", "publisher": "Financial Times", "sentiment": 0.45}, {"title": "Microsoft Azure Growth Accelerates to 29%, Beating Wall Street Forecasts", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1633419461186-7d40a38105ec?q=80&w=500&auto=format&fit=crop", "publisher": "CNBC", "sentiment": 0.88}, {"title": "European Markets Slide as German Manufacturing Data Disappoints", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1569025690938-a00729c9e1f9?q=80&w=500&auto=format&fit=crop", "publisher": "Bloomberg", "sentiment": -0.55}, {"title": "Bitcoin Rallies Past $72,000 Following Spot ETF Inflow Record", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1518546305927-5a555bb7020d?q=80&w=500&auto=format&fit=crop", "publisher": "CoinDesk", "sentiment": 0.75}, {"title": "Pfizer Shares Plunge After Cutting Full-Year Revenue Guidance", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1584308666744-24d5c474f2ae?q=80&w=500&auto=format&fit=crop", "publisher": "Reuters", "sentiment": -0.70}, {"title": "US Housing Starts Fall 5.5% as Mortgage Rates Remain Elevated", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1560518883-ce09059eeffa?q=80&w=500&auto=format&fit=crop", "publisher": "MarketWatch", "sentiment": -0.40}, {"title": "Meta Platforms Reports 27% Revenue Jump Driven by Reels Advertising", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1611162616305-c69b3fa7fbe0?q=80&w=500&auto=format&fit=crop", "publisher": "TechCrunch", "sentiment": 0.82}, {"title": "Toyota Overtakes GM in US Sales for First Time in History", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1549317661-bd32c8ce0afa?q=80&w=500&auto=format&fit=crop", "publisher": "Wall Street Journal", "sentiment": 0.35}, {"title": "US 10-Year Treasury Yield Falls Below 4% on Soft Inflation Print", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1611974789855-9c2a0a7236a3?q=80&w=500&auto=format&fit=crop", "publisher": "Bloomberg", "sentiment": 0.50}, {"title": "Alphabet Announces $70 Billion Share Buyback, Stock Rallies 6%", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1573804633927-bfcbcd909acd?q=80&w=500&auto=format&fit=crop", "publisher": "CNBC", "sentiment": 0.90}, {"title": "Boeing Faces New FAA Investigation After Latest Quality Control Failures", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1556388158-158ea5ccacbd?q=80&w=500&auto=format&fit=crop", "publisher": "Reuters", "sentiment": -0.75}, {"title": "Walmart Reports Strong Q1, Raises Full-Year Outlook on Consumer Resilience", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1604719312566-8912e9227c6a?q=80&w=500&auto=format&fit=crop", "publisher": "Financial Times", "sentiment": 0.68}, {"title": "Broadcom Stock Surges 12% After AI Revenue Doubles Year-Over-Year", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=500&auto=format&fit=crop", "publisher": "MarketWatch", "sentiment": 0.88}, {"title": "Japan's Nikkei 225 Hits 40-Year High as Yen Weakens Further", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1480796927426-f609979314bd?q=80&w=500&auto=format&fit=crop", "publisher": "Bloomberg", "sentiment": 0.55}, {"title": "UnitedHealth Group Under DOJ Antitrust Scrutiny, Shares Fall 9%", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1576091160550-2173dba999ef?q=80&w=500&auto=format&fit=crop", "publisher": "Wall Street Journal", "sentiment": -0.62}, {"title": "Copper Prices Hit Record High on AI Data Center Power Demand", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1605792657660-596af9009e82?q=80&w=500&auto=format&fit=crop", "publisher": "Reuters", "sentiment": 0.48}, {"title": "Netflix Adds 9.3 Million Subscribers in Q2, Beating Every Estimate", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1574375927938-d5a98e8d7e28?q=80&w=500&auto=format&fit=crop", "publisher": "CNBC", "sentiment": 0.85}, {"title": "ExxonMobil Completes $60 Billion Pioneer Natural Resources Acquisition", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1513828583688-c52646db42da?q=80&w=500&auto=format&fit=crop", "publisher": "Financial Times", "sentiment": 0.30}, {"title": "CrowdStrike Outage Grounds Airlines Worldwide, Stock Drops 11%", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1558494949-ef010cbdcc31?q=80&w=500&auto=format&fit=crop", "publisher": "TechCrunch", "sentiment": -0.80}, {"title": "US Consumer Confidence Index Rises to 8-Month High in June", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1556742049-0cfed4f6a45d?q=80&w=500&auto=format&fit=crop", "publisher": "MarketWatch", "sentiment": 0.62}, {"title": "Eli Lilly Weight-Loss Drug Zepbound Sales Exceed $1 Billion in Q2", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1587854692152-cbe660dbde88?q=80&w=500&auto=format&fit=crop", "publisher": "Bloomberg", "sentiment": 0.78}, {"title": "Regional Banks Face Renewed Pressure as Commercial Real Estate Defaults Rise", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1486406146926-c627a92ad1ab?q=80&w=500&auto=format&fit=crop", "publisher": "Wall Street Journal", "sentiment": -0.58}, {"title": "India's Sensex Crosses 80,000 Milestone on Foreign Fund Inflows", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1524492412937-b28074a5d7da?q=80&w=500&auto=format&fit=crop", "publisher": "Reuters", "sentiment": 0.70}, {"title": "AMD Unveils New MI350 AI Chips to Challenge NVIDIA's Dominance", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1591799264318-7e6ef8ddb7ea?q=80&w=500&auto=format&fit=crop", "publisher": "TechCrunch", "sentiment": 0.65}, {"title": "US Trade Deficit Widens to $75 Billion as Imports Surge from Asia", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1494412574643-ff11b0a5eb19?q=80&w=500&auto=format&fit=crop", "publisher": "Financial Times", "sentiment": -0.35}, {"title": "Costco Same-Store Sales Growth Beats Expectations for 8th Straight Quarter", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1604719312566-8912e9227c6a?q=80&w=500&auto=format&fit=crop", "publisher": "CNBC", "sentiment": 0.72}, {"title": "Ethereum Spot ETF Approved by SEC, Crypto Market Rallies Broadly", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1639762681485-074b7f938ba0?q=80&w=500&auto=format&fit=crop", "publisher": "CoinDesk", "sentiment": 0.80}, {"title": "Disney+ Reaches Profitability for First Time Since Launch", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1616530940355-351fabd9524b?q=80&w=500&auto=format&fit=crop", "publisher": "Bloomberg", "sentiment": 0.75}, {"title": "Natural Gas Prices Collapse 15% on Warmer-Than-Expected Winter Forecast", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1532601224476-15c79f2f7a51?q=80&w=500&auto=format&fit=crop", "publisher": "MarketWatch", "sentiment": -0.45}, {"title": "Goldman Sachs Upgrades Semiconductor Sector to Overweight Amid AI Tailwinds", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1518770660439-4636190af475?q=80&w=500&auto=format&fit=crop", "publisher": "Wall Street Journal", "sentiment": 0.68}, {"title": "UK Inflation Falls to 2.0% Target, Bank of England Rate Cut Expected", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1526304640581-d334cdbbf45e?q=80&w=500&auto=format&fit=crop", "publisher": "Reuters", "sentiment": 0.52}, {"title": "Palantir Wins $480 Million US Army Contract, Shares Jump 7%", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1550751827-4bd374c3f58b?q=80&w=500&auto=format&fit=crop", "publisher": "CNBC", "sentiment": 0.73}, {"title": "Starbucks CEO Steps Down After Sales Decline in Key US and China Markets", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1453614512568-c4024d13c247?q=80&w=500&auto=format&fit=crop", "publisher": "Bloomberg", "sentiment": -0.50}, {"title": "Solar Energy Installations Hit Record in H1 2026, SolarEdge Rallies", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1508514177221-188b1cf16e9d?q=80&w=500&auto=format&fit=crop", "publisher": "Financial Times", "sentiment": 0.65}, {"title": "Dollar Index Falls to 3-Month Low as Rate Cut Bets Increase", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1611974789855-9c2a0a7236a3?q=80&w=500&auto=format&fit=crop", "publisher": "MarketWatch", "sentiment": -0.25}, {"title": "Uber Reports First Full-Year Profit, Announces $7 Billion Buyback", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1549317661-bd32c8ce0afa?q=80&w=500&auto=format&fit=crop", "publisher": "TechCrunch", "sentiment": 0.82}, {"title": "Lockheed Martin Secures $11 Billion F-35 Contract Expansion", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1556388158-158ea5ccacbd?q=80&w=500&auto=format&fit=crop", "publisher": "Wall Street Journal", "sentiment": 0.58}, {"title": "Cathie Wood's ARK Invest Sells $200M in Tesla, Loads Up on Coinbase", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1590283603385-17ffb3a7f29f?q=80&w=500&auto=format&fit=crop", "publisher": "Bloomberg", "sentiment": 0.15}, {"title": "Samsung Reports Record Chip Profits as AI Memory Demand Soars", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1591799264318-7e6ef8ddb7ea?q=80&w=500&auto=format&fit=crop", "publisher": "Reuters", "sentiment": 0.77}, {"title": "US Core PCE Inflation Cools to 2.6%, Reinforcing Fed Pivot Narrative", "link": "https://finance.yahoo.com", "image": "https://images.unsplash.com/photo-1611974765270-ca1258634369?q=80&w=500&auto=format&fit=crop", "publisher": "CNBC", "sentiment": 0.60}, ] for i, item in enumerate(dummy_news): item["published"] = (datetime.now() - timedelta(minutes=i * 30)).strftime('%Y-%m-%d %H:%M') return jsonify(dummy_news) @app.route("/api/screener", methods=["GET"]) def screener_data(): """ Returns the pre-calculated daily screener data from Supabase cache to bypass local disk wipes. """ data = db.get_cache("screener") if data: return jsonify(data) # Final fallback if Supabase is wiped file_path = "/tmp/screener_data.json" if os.path.exists(file_path): try: with open(file_path, "r") as f: return jsonify(json.load(f)) except Exception as e: pass return jsonify({"stocks": []}) @app.route("/api/search", methods=["GET"]) def search_symbols(): query = request.args.get("q") if not query: return jsonify({"data": []}) url = "https://api.twelvedata.com/symbol_search" params = {"symbol": query, "apikey": TWELVE_DATA_KEY} try: response = requests.get(url, params=params) data = response.json() if "data" in data: return jsonify(data) else: return jsonify({"data": []}) except Exception as e: print(f"Search API Error: {e}") return jsonify({"data": []}) @app.route("/api/sentiment-history", methods=["GET"]) def sentiment_history(): """Returns sentiment time-series for a ticker.""" ticker = request.args.get("ticker") if not ticker: return jsonify({"error": "Missing required parameter: ticker"}), 400 from brain.sentiment.tracker import SentimentTracker limit = int(request.args.get("limit", 30)) history = SentimentTracker.get_history(ticker.upper(), limit=limit) return jsonify({"ticker": ticker.upper(), "history": history}) @app.route("/api/sector-sentiment", methods=["GET"]) def sector_sentiment(): """Returns sector-level sentiment aggregation.""" from brain.sentiment.sector import SectorSentiment result = SectorSentiment.get_hottest_coldest() return jsonify(result) @app.route("/api/quant/rank", methods=["GET"]) def cross_sectional_rank(): try: strategy_data = db.get_cache("strategy_live_signals") if not strategy_data: from backend.strategy_signals import get_strategy_live_signals strategy_data = get_strategy_live_signals() # Format for ScreenerPage and RankingDashboard return jsonify({ "rankings": strategy_data["all_universe"], # For ScreenerPage "universe_size": len(strategy_data["all_universe"]), # For RankingDashboard "picks": strategy_data["picks"], # For RankingDashboard Top 15 "regime": strategy_data["regime"], "vol_scalar": strategy_data["vol_scalar"] }) except Exception as e: print(f"Rank API Error: {e}") return jsonify({"rankings": [], "universe_size": 0, "picks": [], "error": "Screener data not yet available"}) @app.route("/api/strategy/live", methods=["GET"]) def strategy_live_signals(): """Returns live Strategy signals: Top 15 175-day picks, regime status, vol scalar.""" try: signals = db.get_cache("strategy_live_signals") if not signals: from backend.strategy_signals import get_strategy_live_signals signals = get_strategy_live_signals() return jsonify(signals) except Exception as e: print(f"Strategy Signal Error: {e}") return jsonify({"error": str(e)}), 500 @app.route("/api/strategy/portfolio", methods=["GET"]) def strategy_portfolio(): """Returns the live paper trading portfolio state directly from Alpaca.""" try: # Check cache first cached = get_cached_data("STRATEGY_PORTFOLIO") if cached: return jsonify(cached) from backend import alpaca_executor client = alpaca_executor.get_client() account = client.get_account() positions = alpaca_executor.get_current_positions(client) # Try to read trade log trade_log = [] log_path = os.path.join(os.path.dirname(__file__), "alpaca_trade_log.json") if os.path.exists(log_path): with open(log_path, "r") as f: raw_logs = json.load(f) for log in raw_logs: trade_log.append({ "date": log["timestamp"].split("T")[0], "action": "REBALANCE", "orders": log.get("orders", []), "equity": log.get("portfolio_value", 0), "vol_scalar": log.get("vol_scalar", 1.0) }) # Construct equity curve from logs (naive approach, but better than nothing) equity_curve = [{"date": l["date"], "equity": l["equity"]} for l in trade_log] if not equity_curve: equity_curve = [{"date": datetime.now().strftime("%Y-%m-%d"), "equity": float(account.portfolio_value)}] # Determine days since rebalance days_since = 0 if trade_log: last_date = datetime.strptime(trade_log[-1]["date"], "%Y-%m-%d") days_since = (datetime.now() - last_date).days # Try to get latest status try: from backend.strategy_signals import get_strategy_live_signals sig = get_strategy_live_signals() status = {"regime": sig["regime"], "effective_scalar": sig["vol_scalar"]} except: status = {"regime": "UNKNOWN", "effective_scalar": 1.0} port_data = { "start_date": trade_log[0]["date"] if trade_log else datetime.now().strftime("%Y-%m-%d"), "equity": float(account.portfolio_value), "cash": float(account.cash), "status": status, "days_since_rebalance": days_since, "equity_curve": equity_curve, "trade_log": trade_log, "positions": positions } set_cached_data("STRATEGY_PORTFOLIO", port_data) return jsonify(port_data) except Exception as e: print(f"Alpaca API Error: {e}") return jsonify({"error": f"Failed to connect to Alpaca: {str(e)}"}), 500 @app.route("/api/strategy/force_run", methods=["POST"]) def strategy_force_run(): """Manually triggers the Strategy Alpaca Executor in dry-run mode (or live if configured).""" try: from backend.alpaca_executor import main as run_virtual_broker from backend.alpaca_executor import calculate_strategy_signals calculate_strategy_signals() return jsonify({"status": "success", "message": "Alpaca engine connection successful."}) except Exception as e: print(f"Alpaca Engine Error: {e}") return jsonify({"error": str(e)}), 500 @app.route("/health", methods=["GET"]) def health(): return jsonify({"status": "healthy"}) if __name__ == "__main__": port = int(os.getenv("PORT", 5000)) debug = os.getenv("FLASK_DEBUG", "false").lower() == "true" app.run(host="0.0.0.0", port=port, debug=debug, use_reloader=False)