Created
April 24, 2026 17:41
-
-
Save rogermaragh/6d3f1cf0c4192c251012975193f06e90 to your computer and use it in GitHub Desktop.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import uvicorn | |
| from fastapi import FastAPI, HTTPException, Query | |
| from fastapi.responses import HTMLResponse | |
| from contextlib import asynccontextmanager | |
| import pandas as pd | |
| import requests | |
| from datetime import datetime | |
| # ========================================================= | |
| # 1. GLOBAL MEMORY (The Full Market) | |
| # ========================================================= | |
| market_df = pd.DataFrame() | |
| # ========================================================= | |
| # 2. DATA SCRAPERS | |
| # ========================================================= | |
| def fetch_nasdaq_universe(): | |
| """Scrapes the ENTIRE US stock market to allow for sector and mover analysis.""" | |
| url = "https://api.nasdaq.com/api/screener/stocks?tableonly=true&limit=25&offset=0&download=true" | |
| headers = {"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36"} | |
| response = requests.get(url, headers=headers, timeout=15) | |
| response.raise_for_status() | |
| rows = response.json().get('data', {}).get('rows', []) | |
| if not rows: | |
| raise ValueError("No data returned from NASDAQ API.") | |
| processed_data = [] | |
| for row in rows: | |
| # Clean Market Cap | |
| try: mcap = float(str(row.get('marketCap', '')).replace('$', '').replace(',', '').strip()) | |
| except ValueError: mcap = 0.0 | |
| # Clean Percentage Change (Remove the '%' symbol so we can sort it mathematically) | |
| try: pct = float(str(row.get('pctchange', '')).replace('%', '').strip()) | |
| except ValueError: pct = 0.0 | |
| # Handle empty sectors | |
| sector = str(row.get('sector', '')).strip() | |
| if not sector: sector = "Unknown/Unclassified" | |
| processed_data.append({ | |
| "symbol": str(row.get('symbol', '')).strip(), | |
| "name": str(row.get('name', '')).strip(), | |
| "price": str(row.get('lastsale', '')).strip(), | |
| "pctchange": pct, | |
| "marketCap": mcap, | |
| "volume": str(row.get('volume', '')).strip(), | |
| "sector": sector | |
| }) | |
| return pd.DataFrame(processed_data) | |
| def scrape_yahoo_historical_data(symbol: str, range_period: str = "1d", interval: str = "1m"): | |
| """Scrapes historical OHLCV data directly from Yahoo Finance's frontend API.""" | |
| url = f"https://query2.finance.yahoo.com/v8/finance/chart/{symbol}?interval={interval}&range={range_period}" | |
| headers = {"User-Agent": "Mozilla/5.0"} | |
| response = requests.get(url, headers=headers, timeout=10) | |
| if response.status_code != 200: | |
| raise ValueError(f"Yahoo Finance rejected the request. HTTP {response.status_code}") | |
| result = response.json().get('chart', {}).get('result', []) | |
| if not result: raise ValueError("No historical data returned.") | |
| timestamps = result[0].get('timestamp', []) | |
| quote = result[0].get('indicators', {}).get('quote', [{}])[0] | |
| valid_data = [] | |
| for i in range(len(timestamps)): | |
| try: | |
| o, h, l, c, v = quote.get('open')[i], quote.get('high')[i], quote.get('low')[i], quote.get('close')[i], quote.get('volume')[i] | |
| if None in (o, h, l, c, v): continue | |
| dt_str = datetime.fromtimestamp(timestamps[i]).strftime('%Y-%m-%d %H:%M:%S') | |
| valid_data.append({ | |
| "date": dt_str, "open": round(float(o), 2), "high": round(float(h), 2), | |
| "low": round(float(l), 2), "close": round(float(c), 2), "volume": int(v) | |
| }) | |
| except (IndexError, TypeError, ValueError): continue | |
| return valid_data | |
| # ========================================================= | |
| # 3. SERVER LIFECYCLE | |
| # ========================================================= | |
| @asynccontextmanager | |
| async def lifespan(app: FastAPI): | |
| global market_df | |
| print("\n🌐 Fetching full US Market from NASDAQ...", flush=True) | |
| try: | |
| market_df = fetch_nasdaq_universe() | |
| print(f"✅ Loaded {len(market_df)} stocks into memory!\n", flush=True) | |
| except Exception as e: | |
| print(f"❌ Failed to fetch NASDAQ data: {e}") | |
| yield | |
| app = FastAPI(lifespan=lifespan, title="Ultimate Stock Monitor API") | |
| # ========================================================= | |
| # 4. NEW: SECTORS & MOVERS ENDPOINTS | |
| # ========================================================= | |
| @app.get("/") | |
| def read_root(): | |
| return {"message": "API Running. Check /docs for the Swagger UI."} | |
| @app.get("/refresh") | |
| def refresh_market_data(): | |
| """Updates the in-memory dataframe with fresh market data.""" | |
| global market_df | |
| try: | |
| market_df = fetch_nasdaq_universe() | |
| return {"status": "success", "message": f"Memory updated. Loaded {len(market_df)} stocks."} | |
| except Exception as e: | |
| raise HTTPException(status_code=500, detail=str(e)) | |
| @app.get("/sectors") | |
| def get_top_by_sector(limit_per_sector: int = Query(20, description="Number of top stocks per sector")): | |
| """Groups the market by Sector and returns the top N largest companies for each.""" | |
| if market_df.empty: raise HTTPException(status_code=500, detail="Data not loaded. Hit /refresh.") | |
| # Exclude unknowns to keep the data clean | |
| clean_df = market_df[market_df['sector'] != 'Unknown/Unclassified'] | |
| # Group by sector, sort by Market Cap, and take the top N | |
| top_per_sector = clean_df.groupby('sector', group_keys=False).apply( | |
| lambda x: x.nlargest(limit_per_sector, 'marketCap') | |
| ) | |
| # Format into a clean dictionary | |
| response = {} | |
| for sector, group in top_per_sector.groupby('sector'): | |
| response[sector] = group.drop(columns=['sector']).to_dict(orient="records") | |
| return {"total_sectors": len(response), "data": response} | |
| @app.get("/movers") | |
| def get_top_movers(limit: int = Query(10, description="Number of gainers/losers to show")): | |
| """Returns the top gainers and losers in the market (Filtered to > $300M Market Cap).""" | |
| if market_df.empty: raise HTTPException(status_code=500, detail="Data not loaded. Hit /refresh.") | |
| # FILTER: Only look at companies worth more than $300 Million to avoid penny stock noise | |
| reliable_stocks = market_df[market_df['marketCap'] >= 300000000] | |
| gainers = reliable_stocks.nlargest(limit, 'pctchange').to_dict(orient="records") | |
| losers = reliable_stocks.nsmallest(limit, 'pctchange').to_dict(orient="records") | |
| return { | |
| "market_cap_filter": "> $300 Million", | |
| "top_gainers": gainers, | |
| "top_losers": losers | |
| } | |
| # ========================================================= | |
| # 5. ANALYTICS & GRAPHING ENDPOINTS | |
| # ========================================================= | |
| @app.get("/darkpool/proxy/{symbol}") | |
| def dark_pool_proxy_analysis(symbol: str): | |
| try: | |
| hist_data = scrape_yahoo_historical_data(symbol.upper(), range_period="15d", interval="1d") | |
| if len(hist_data) < 11: raise ValueError("Not enough clean data.") | |
| volumes = [day['volume'] for day in hist_data] | |
| current_volume = volumes[-1] | |
| avg_10d_volume = sum(volumes[-11:-1]) / 10 | |
| volume_ratio = current_volume / avg_10d_volume if avg_10d_volume > 0 else 0 | |
| signal = "Normal Retail Activity" | |
| if volume_ratio > 2.0: signal = "HIGH INSTITUTIONAL ACTIVITY (Potential Dark Pool Print)" | |
| elif volume_ratio > 1.3: signal = "Elevated Block Trading" | |
| return { | |
| "symbol": symbol.upper(), "current_volume": current_volume, | |
| "average_10d_volume": int(avg_10d_volume), "volume_spike_ratio": round(volume_ratio, 2), | |
| "market_signal": signal | |
| } | |
| except Exception as e: | |
| raise HTTPException(status_code=404, detail=str(e)) | |
| @app.get("/api/historical/{symbol}") | |
| def get_historical_json(symbol: str, period: str = Query("1d"), interval: str = Query("1m")): | |
| try: return scrape_yahoo_historical_data(symbol.upper(), range_period=period, interval=interval) | |
| except Exception as e: raise HTTPException(status_code=404, detail=str(e)) | |
| @app.get("/graph/{symbol}", response_class=HTMLResponse) | |
| def get_interactive_graph(symbol: str): | |
| html_content = f""" | |
| <!DOCTYPE html> | |
| <html> | |
| <head> | |
| <title>{symbol.upper()} - Intraday Chart</title> | |
| <script src="https://cdn.plot.ly/plotly-latest.min.js"></script> | |
| <style> | |
| body {{ background-color: #121212; color: white; font-family: sans-serif; text-align: center; margin-top: 20px; }} | |
| #chart {{ width: 90%; height: 75vh; margin: auto; }} | |
| .controls {{ margin-bottom: 20px; }} | |
| select {{ background: #333; color: white; padding: 10px; border: 1px solid #555; border-radius: 5px; cursor: pointer; font-size: 16px; }} | |
| #error-msg {{ color: #ff5555; display: none; font-weight: bold; margin-top: 20px; }} | |
| </style> | |
| </head> | |
| <body> | |
| <h2>{symbol.upper()} Advanced Intraday Chart</h2> | |
| <div class="controls"> | |
| <label>Select Timeframe & Resolution: </label> | |
| <select id="presetSelector" onchange="loadGraph()"> | |
| <option value="1d|1m" selected>1 Day (1-Minute Candles)</option> | |
| <option value="5d|15m">5 Days (15-Minute Candles)</option> | |
| <option value="1mo|1h">1 Month (1-Hour Candles)</option> | |
| <option value="3mo|1d">3 Months (Daily Candles)</option> | |
| <option value="1y|1d">1 Year (Daily Candles)</option> | |
| </select> | |
| </div> | |
| <div id="chart">Loading chart data...</div> | |
| <div id="error-msg"></div> | |
| <script> | |
| const symbol = "{symbol.upper()}"; | |
| async function loadGraph() {{ | |
| const selectedValue = document.getElementById('presetSelector').value; | |
| const [period, interval] = selectedValue.split('|'); | |
| const chartDiv = document.getElementById('chart'); | |
| const errorDiv = document.getElementById('error-msg'); | |
| chartDiv.innerHTML = '<h3 style="color: #888;">Fetching ' + interval + ' candles for ' + symbol + '...</h3>'; | |
| errorDiv.style.display = 'none'; | |
| try {{ | |
| const response = await fetch(`/api/historical/${{symbol}}?period=${{period}}&interval=${{interval}}`); | |
| const data = await response.json(); | |
| if (!response.ok) throw new Error(data.detail || "Failed to load data"); | |
| if (data.length === 0) throw new Error("No data available for this timeframe."); | |
| const trace = {{ | |
| x: data.map(d => d.date), close: data.map(d => d.close), | |
| high: data.map(d => d.high), low: data.map(d => d.low), open: data.map(d => d.open), | |
| type: 'candlestick', xaxis: 'x', yaxis: 'y' | |
| }}; | |
| const layout = {{ | |
| dragmode: 'zoom', margin: {{ r: 20, t: 30, b: 40, l: 60 }}, | |
| xaxis: {{ type: 'category', rangeslider: {{visible: false}}, nticks: 10 }}, | |
| yaxis: {{ autorange: true, type: 'linear', title: 'Price (USD)' }}, | |
| plot_bgcolor: '#121212', paper_bgcolor: '#121212', font: {{ color: '#ffffff' }} | |
| }}; | |
| chartDiv.innerHTML = ''; | |
| Plotly.newPlot('chart', [trace], layout); | |
| }} catch (error) {{ | |
| chartDiv.innerHTML = ''; | |
| errorDiv.innerText = "Error: " + error.message; | |
| errorDiv.style.display = 'block'; | |
| }} | |
| }} | |
| window.onload = loadGraph; | |
| </script> | |
| </body> | |
| </html> | |
| """ | |
| return HTMLResponse(content=html_content, status_code=200) | |
| if __name__ == "__main__": | |
| uvicorn.run("stock_monitor:app", host="127.0.0.1", port=8000, reload=True) |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment