Skip to content

Instantly share code, notes, and snippets.

@rogermaragh
Created April 24, 2026 17:41
Show Gist options
  • Select an option

  • Save rogermaragh/6d3f1cf0c4192c251012975193f06e90 to your computer and use it in GitHub Desktop.

Select an option

Save rogermaragh/6d3f1cf0c4192c251012975193f06e90 to your computer and use it in GitHub Desktop.
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