refactor(main): 루트 문서 페이지 OpenAPI 스키마에서 자동 생성

하드코딩된 330줄 HTML을 제거하고 request.app.openapi()에서
동적으로 엔드포인트 목록을 렌더링하도록 변경.
새 라우터 추가 시 / 페이지를 별도로 수정할 필요 없음.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
main
I Luk Kim 5 months ago
parent 8c9abe3e31
commit afd133e027

@ -4,7 +4,7 @@ Main FastAPI application
import os
from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException
from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse, HTMLResponse
@ -62,350 +62,119 @@ app.add_middleware(
# Include API router
app.include_router(api_router, prefix=settings.API_PREFIX)
# Root documentation endpoint
# Root documentation endpoint — auto-generated from OpenAPI schema
@app.get("/", response_class=HTMLResponse, include_in_schema=False)
async def root_documentation():
"""
Display comprehensive API documentation at root path
"""
async def root_documentation(request: Request):
try:
# Simple working version
simple_html = f"""
<!DOCTYPE html>
<html lang="en">
<head>
schema = request.app.openapi()
paths = schema.get("paths", {})
api_prefix = settings.API_PREFIX
# Collect all tags defined in schema (preserves order)
tag_order = [t["name"] for t in schema.get("tags", [])]
# Group routes by first tag
from collections import defaultdict
tag_routes: dict = defaultdict(list)
untagged: list = []
for path, methods in paths.items():
for method, op in methods.items():
if method.upper() not in ("GET", "POST", "PUT", "DELETE", "PATCH"):
continue
tags = op.get("tags", [])
entry = {
"method": method.upper(),
"path": path,
"summary": op.get("summary", ""),
"deprecated": op.get("deprecated", False),
}
if tags:
tag_routes[tags[0]].append(entry)
else:
untagged.append(entry)
# Build tag sections — respect schema tag order, then alphabetical remainder
all_tags = tag_order + sorted(t for t in tag_routes if t not in tag_order)
if untagged:
all_tags.append("other")
tag_routes["other"] = untagged
method_colors = {
"GET": "#61affe",
"POST": "#49cc90",
"PUT": "#fca130",
"DELETE": "#f93e3e",
"PATCH": "#50e3c2",
}
sections_html = ""
for tag in all_tags:
routes = tag_routes.get(tag, [])
if not routes:
continue
rows = ""
for r in routes:
color = method_colors.get(r["method"], "#999")
deprecated = " style='opacity:0.5;text-decoration:line-through'" if r["deprecated"] else ""
display_path = r["path"].removeprefix(api_prefix)
rows += (
f"<tr{deprecated}>"
f"<td><span class='method' style='background:{color}'>{r['method']}</span></td>"
f"<td><code>{display_path}</code></td>"
f"<td>{r['summary']}</td>"
f"</tr>"
)
sections_html += f"<h3>{tag}</h3><table>{rows}</table>"
title = schema.get("info", {}).get("title", "API")
version = schema.get("info", {}).get("version", "")
total = sum(len(v) for v in tag_routes.values())
html = f"""<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stock Oracle API Documentation</title>
<title>{title}</title>
<style>
body {{
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6;
max-width: 1000px;
margin: 0 auto;
padding: 20px;
color: #333;
}}
h1 {{ color: #007acc; border-bottom: 2px solid #007acc; padding-bottom: 10px; }}
h2 {{ color: #2c3e50; margin-top: 2em; }}
.nav-links {{ margin: 20px 0; }}
.nav-links a {{
display: inline-block;
margin-right: 15px;
padding: 10px 20px;
background: #007acc;
color: white;
text-decoration: none;
border-radius: 5px;
}}
.nav-links a:hover {{ background: #0066aa; }}
code {{ background: #f4f4f4; padding: 2px 4px; border-radius: 3px; }}
pre {{ background: #f4f4f4; padding: 15px; border-radius: 5px; overflow-x: auto; }}
.new-badge {{ background: #e74c3c; color: white; padding: 2px 6px; border-radius: 3px; font-size: 0.8em; }}
body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
line-height: 1.6; max-width: 1100px; margin: 0 auto; padding: 24px; color: #333; }}
h1 {{ color: #1a1a2e; border-bottom: 3px solid #007acc; padding-bottom: 10px; }}
h3 {{ color: #2c3e50; margin-top: 2em; text-transform: capitalize; border-left: 4px solid #007acc; padding-left: 10px; }}
.nav {{ margin: 20px 0; display: flex; gap: 10px; flex-wrap: wrap; }}
.nav a {{ padding: 8px 18px; background: #007acc; color: white; text-decoration: none;
border-radius: 6px; font-size: 0.9em; }}
.nav a:hover {{ background: #005fa3; }}
.meta {{ color: #666; font-size: 0.9em; margin-bottom: 1.5em; }}
table {{ width: 100%; border-collapse: collapse; margin-bottom: 1em; font-size: 0.9em; }}
tr:hover {{ background: #f8f9fa; }}
td {{ padding: 7px 10px; border-bottom: 1px solid #eee; vertical-align: top; }}
td:first-child {{ width: 72px; }}
td:nth-child(2) {{ width: 38%; }}
code {{ background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.85em; word-break: break-all; }}
.method {{ display: inline-block; color: white; font-weight: bold; font-size: 0.75em;
padding: 3px 8px; border-radius: 4px; letter-spacing: 0.5px; }}
footer {{ margin-top: 3em; padding-top: 1.5em; border-top: 1px solid #eee;
text-align: center; color: #888; font-size: 0.85em; }}
</style>
</head>
<body>
<h1>🔮 Stock Oracle API Documentation</h1>
<p><strong>Comprehensive Investment Data Analysis API</strong></p>
<div class="nav-links">
<a href="{settings.API_PREFIX}/docs">📋 Interactive API Docs</a>
<a href="{settings.API_PREFIX}/redoc">📖 ReDoc</a>
<a href="{settings.API_PREFIX}/health">💚 Health Check</a>
<a href="{settings.API_PREFIX}/openapi.json"> OpenAPI Schema</a>
</head>
<body>
<h1>🔮 {title}</h1>
<div class="nav">
<a href="{api_prefix}/docs">📋 Swagger UI</a>
<a href="{api_prefix}/redoc">📖 ReDoc</a>
<a href="{api_prefix}/health">💚 Health</a>
<a href="{api_prefix}/openapi.json"> OpenAPI JSON</a>
</div>
<p class="meta">v{version} &nbsp;·&nbsp; Base URL: <code>{api_prefix}</code> &nbsp;·&nbsp; {total} endpoints</p>
{sections_html}
<footer>Auto-generated from OpenAPI schema · <a href="{api_prefix}/docs">Full interactive docs</a></footer>
</body>
</html>"""
<h2>🚀 Quick Start</h2>
<p><strong>Base URL:</strong> <code>http://localhost:18001/api/v1</code></p>
<h2>🎯 Main Endpoints</h2>
<h3>Health & System</h3>
<ul>
<li><code>GET /health</code> - API health status</li>
<li><code>GET /health/detailed</code> - Detailed system health</li>
</ul>
<h3>Financial Data</h3>
<ul>
<li><code>POST /financial/data</code> - Get comprehensive financial data</li>
<li><code>GET /financial/data/{{ticker}}</code> - Simple financial data</li>
<li><code>POST /financial/data/bulk</code> - Bulk financial data</li>
</ul>
<h3>Price Data</h3>
<ul>
<li><code>POST /price/data</code> - Get historical price data (OHLCV)</li>
<li><code>GET /price/data/{{ticker}}</code> - Simple price data</li>
<li><code>POST /price/data/bulk</code> - Bulk price data</li>
<li><code>GET /price/quote/{{ticker}}</code> - Latest quote (regular/pre/post market)</li>
<li><code>GET /price/intraday/{{ticker}}</code> - Intraday candles (interval, period)</li>
<li><code>GET /price/today/{{ticker}}</code> - Today's OHLC (daily or 1m aggregate)</li>
</ul>
<h3>Stock Market Data <span class="new-badge">NEW</span></h3>
<ul>
<li><code>GET /stocks/trending</code> - Trending stocks with intelligent parameter coordination (n=500 default)</li>
<li><code>GET /stocks/most-active</code> - Most actively traded stocks</li>
<li><code>GET /stocks/52-week-gainers</code> - Top 52-week gaining stocks</li>
<li><code>GET /stocks/index/{{index_name}}</code> - S&P 500 / Nasdaq 100 constituents from Wikipedia (24h cache) <code>sp500</code> | <code>nasdaq100</code></li>
</ul>
<h3>FRED Economic Data <span class="new-badge">NEW</span></h3>
<ul>
<li><code>GET /fred/proxy/{{endpoint}}</code> - Universal FRED API proxy with caching</li>
<li><code>GET /fred/endpoints</code> - List all supported FRED API endpoints</li>
<li><code>GET /fred/stats/usage</code> - API usage statistics and monitoring</li>
</ul>
<h3>News & Social Media <span class="new-badge">NEW</span></h3>
<ul>
<li><code>GET /news/{{ticker}}</code> - Complete news and social media data</li>
<li><code>GET /news/{{ticker}}/news-only</code> - News articles only (faster)</li>
<li><code>GET /news/{{ticker}}/social-only</code> - Social media posts only</li>
</ul>
<h3>SEC Filings <span class="new-badge">NEW</span></h3>
<ul>
<li><code>GET /filings/search/{{ticker}}</code> - Search SEC filings (8-K, 6-K, 20-F, 40-F, 10-K, 10-Q)</li>
<li><code>GET /filings/documents/{{accession_number}}</code> - List all documents in a filing</li>
<li><code>GET /filings/exhibit/{{accession_number}}</code> - Extract exhibit content (e.g., EX-99.1 press releases)</li>
</ul>
<h3>Alpaca Market Data <span class="new-badge">NEW</span></h3>
<ul>
<li><code>GET /alpaca/status</code> - Alpaca connection status and key validity</li>
<li><code>GET /alpaca/bars/{{ticker}}</code> - Fetch bars directly from Alpaca (raw, no DB)</li>
</ul>
<h3>FINRA Short Volume <span class="new-badge">NEW</span></h3>
<ul>
<li><code>GET /finra/short-volume/{{symbol}}</code> - Short sale volume data for a symbol</li>
<li><code>GET /finra/short-ratio/{{symbol}}</code> - Short ratio history (aggregated)</li>
<li><code>POST /finra/admin/ingest</code> - Manually ingest FINRA data for a date/range</li>
</ul>
<h3>ETF Holdings</h3>
<ul>
<li><code>GET /etf/holdings/{{ticker}}</code> - ETF holdings at date or most recent</li>
<li><code>POST /etf/admin/refresh-maps</code> - Refresh CUSIP/CIK maps</li>
</ul>
<h3>Stock Screener <span class="new-badge">NEW</span></h3>
<ul>
<li><code>GET /screener/stocks</code> - Filter stocks by market cap, volume, price, P/E, sector, exchange</li>
<li><code>GET /screener/fields</code> - Available filter options, sectors, sort fields (metadata)</li>
</ul>
<h3>Attention Overlay <span class="new-badge">NEW</span></h3>
<ul>
<li><code>GET /overlay/{{symbol}}</code> - Overlay score + features + source details (0~1 score, band, hints)</li>
<li><code>GET /overlay/bulk?symbols=AAPL,TSLA,NVDA</code> - Bulk overlay scores (max 50 symbols)</li>
<li><code>GET /overlay/top-movers</code> - Symbols with highest overlay scores in last 24h</li>
<li><code>GET /overlay/{{symbol}}/headlines</code> - Recent news headlines matched to symbol</li>
<li><code>GET /overlay/{{symbol}}/youtube</code> - YouTube video mentions from investing channels</li>
<li><code>GET /overlay/{{symbol}}/wiki</code> - Wikipedia pageview time series</li>
<li><code>GET /overlay/{{symbol}}/crowding</code> - FINRA short-sale crowding metrics</li>
<li><code>GET /overlay/{{symbol}}/trends</code> - Google Trends interest data (if enabled)</li>
<li><code>GET /overlay/{{symbol}}/history</code> - Historical overlay scores (backtesting)</li>
<li><code>GET /overlay/admin/health</code> - Data source health and last collection status</li>
<li><code>POST /overlay/admin/trigger-pipeline</code> - Manually trigger full data collection + scoring</li>
<li><code>GET /overlay/admin/job-log</code> - Pipeline job execution log</li>
</ul>
<h2>📊 Example Requests</h2>
<h3>Financial Data</h3>
<pre><code>curl -X POST "http://localhost:18001/api/v1/financial/data" \\
-H "Content-Type: application/json" \\
-d '{{"ticker": "AAPL", "period": "1y", "include_metrics": true}}'</code></pre>
<h3>Trending Stocks <span class="new-badge">NEW</span></h3>
<pre><code># Get trending stocks (default: 500 total stocks with intelligent coordination)
curl "http://localhost:18001/api/v1/stocks/trending"
# Custom total count
curl "http://localhost:18001/api/v1/stocks/trending?n=200"</code></pre>
<h3>Index Constituents <span class="new-badge">NEW</span></h3>
<pre><code># S&P 500 constituents (~503 stocks, cached 24h)
curl "http://localhost:18001/api/v1/stocks/index/sp500"
# Nasdaq 100 constituents (~101 stocks, cached 24h)
curl "http://localhost:18001/api/v1/stocks/index/nasdaq100"
# Force refresh (bypass cache)
curl "http://localhost:18001/api/v1/stocks/index/sp500?force_refresh=true"</code></pre>
<h3>FRED Economic Data <span class="new-badge">NEW</span></h3>
<pre><code># Get GDP series information
curl "http://localhost:18001/api/v1/fred/proxy/series?series_id=GDP"
# Get unemployment rate observations
curl "http://localhost:18001/api/v1/fred/proxy/series/observations?series_id=UNRATE&limit=12"</code></pre>
<h3>News & Social Data <span class="new-badge">NEW</span></h3>
<pre><code>curl "http://localhost:18001/api/v1/news/AAPL?days_back=7&max_articles=20"</code></pre>
<h3>Price - Quote/Intraday/Today <span class="new-badge">NEW</span></h3>
<pre><code># Quote (latest regular/pre/post)
curl "http://localhost:18001/api/v1/price/quote/AAPL?use_prepost=true"
# Intraday 1m candles for 1 day
curl "http://localhost:18001/api/v1/price/intraday/AAPL?interval=1m&period=1d"
# Today's OHLC (daily if available; otherwise 1m aggregate)
curl "http://localhost:18001/api/v1/price/today/AAPL"</code></pre>
return HTMLResponse(content=html)
<h3>SEC Filings <span class="new-badge">NEW</span></h3>
<pre><code># Search AAPL 8-K filings (earnings announcements, material events)
curl "http://localhost:18001/api/v1/filings/search/AAPL?form_type=8-K&limit=5"
# Search foreign issuer filings
curl "http://localhost:18001/api/v1/filings/search/TSM?form_type=20-F"
curl "http://localhost:18001/api/v1/filings/search/SAP?form_type=6-K"
# List all documents in a filing
curl "http://localhost:18001/api/v1/filings/documents/0000320193-26-000005"
# Extract press release (Exhibit 99.1) from an 8-K
curl "http://localhost:18001/api/v1/filings/exhibit/0000320193-26-000005?exhibit_type=EX-99.1"</code></pre>
<h3>ETF Holdings</h3>
<pre><code>curl "http://localhost:18001/api/v1/etf/holdings/QQQ"</code></pre>
<h2>🐍 Python Client</h2>
<pre><code>from stock_oracle_client import StockOracleClient
client = StockOracleClient("http://localhost:18001")
# Check health
health = client.get_health()
print("API Status:", health["status"])
# Get financial data
data = client.get_financial_data("AAPL", period="1y")
# Get news data (NEW!)
news = client.get_news_social_data("AAPL", days_back=7)</code></pre>
<h2>📈 Key Features</h2>
<ul>
<li><strong>SEC EDGAR Data</strong> - Official company filings (10-K, 10-Q) with XBRL financial data</li>
<li><strong>SEC Filings Search</strong> - Index and search 8-K, 6-K, 20-F, 40-F filings with exhibit extraction</li>
<li><strong>Real-time News</strong> - Yahoo Finance + NewsAPI integration</li>
<li><strong>Social Sentiment</strong> - Reddit discussions and sentiment analysis</li>
<li><strong>ETF Holdings</strong> - Complete ETF portfolio analysis via N-PORT</li>
<li><strong>Price Data</strong> - Historical OHLCV data from Yahoo Finance</li>
<li><strong>Investment Metrics</strong> - P/E, ROE, debt ratios, growth metrics</li>
<li><strong>Attention Overlay</strong> - Multi-source retail interest signal (news burst, Wikipedia views, YouTube mentions, FINRA crowding)</li>
</ul>
<h3>Attention Overlay Response Example <span class="new-badge">NEW</span></h3>
<pre><code>GET /api/v1/overlay/AAPL
{{
"symbol": "AAPL",
"as_of_ts": "2026-03-13T20:30:00Z",
"overlay_score": 0.72, // 0~1 (higher = more retail attention)
"overlay_confidence": 0.80, // fraction of sources with data
"overlay_band": "supportive", // silent / tepid / supportive / loud / frenzied
"hold_extension_hint": "neutral", // extend / neutral / trim
"add_on_eligibility": false,
"features": {{
"headline_burst_z": 1.2, // z-score vs 30-day window
"youtube_influence_z": 0.8,
"wiki_attention_z": null,
"theme_heat_z": null,
"crowding_stress_z": -0.3
}},
"source_presence": {{
"yahoo": true, "youtube": true,
"wikimedia": false, "google_trends": false, "finra": true
}},
"source_details": {{
"yahoo": {{"headline_count_6h": 5, "headline_count_24h": 12, "publisher_breadth_24h": 4}},
"finra": {{"short_volume_ratio": 0.42, "short_volume_spike_zscore": -0.3}}
}}
}}</code></pre>
<h3>Alpaca Market Data <span class="new-badge">NEW</span></h3>
<pre><code># Check Alpaca connection status
curl "http://localhost:18001/api/v1/alpaca/status"
# Get daily bars from Alpaca
curl "http://localhost:18001/api/v1/alpaca/bars/AAPL?interval=1d&start_date=2025-01-01&end_date=2025-01-31"</code></pre>
<h3>FINRA Short Volume <span class="new-badge">NEW</span></h3>
<pre><code># Ingest FINRA data for a specific date
curl -X POST "http://localhost:18001/api/v1/finra/admin/ingest?date=2025-03-10"
# Get short volume for AAPL (last 30 days)
curl "http://localhost:18001/api/v1/finra/short-volume/AAPL?days=30"
# Get short ratio history
curl "http://localhost:18001/api/v1/finra/short-ratio/AAPL?days=60"</code></pre>
<h3>Stock Screener <span class="new-badge">NEW</span></h3>
<pre><code># Small/mid-cap stocks on NYSE+NASDAQ with avg volume &gt; 500K, sorted by market cap
curl "http://localhost:18001/api/v1/screener/stocks?market_cap_min=500000000&market_cap_max=10000000000&exchange=NYSE,NASDAQ&min_avg_volume=500000&exclude_types=ETF,FUND"
# Technology sector only
curl "http://localhost:18001/api/v1/screener/stocks?market_cap_min=500000000&exchange=NASDAQ&sector=Technology&page=1&page_size=50"
# Available filter metadata
curl "http://localhost:18001/api/v1/screener/fields"</code></pre>
<h3>Attention Overlay <span class="new-badge">NEW</span></h3>
<pre><code># Overlay score for a single symbol
curl "http://localhost:18001/api/v1/overlay/AAPL"
# Bulk scores (comma-separated, max 50)
curl "http://localhost:18001/api/v1/overlay/bulk?symbols=AAPL,TSLA,NVDA,AMD,META"
# Top movers by overlay score (last 24h)
curl "http://localhost:18001/api/v1/overlay/top-movers?limit=10"
# Recent headlines matched to AAPL
curl "http://localhost:18001/api/v1/overlay/AAPL/headlines?hours=24"
# Wikipedia pageview trend (last 30 days)
curl "http://localhost:18001/api/v1/overlay/AAPL/wiki?days=30"
# FINRA crowding metrics
curl "http://localhost:18001/api/v1/overlay/AAPL/crowding"
# Historical overlay scores (backtesting)
curl "http://localhost:18001/api/v1/overlay/AAPL/history?days=90"
# Manually trigger full pipeline
curl -X POST "http://localhost:18001/api/v1/overlay/admin/trigger-pipeline"</code></pre>
<h2>🔧 Data Sources</h2>
<ul>
<li><strong>SEC EDGAR</strong> - Official company filings and ETF holdings</li>
<li><strong>Yahoo Finance</strong> - Price data, financial news, and RSS headlines</li>
<li><strong>Alpaca</strong> - Market price data (OHLCV) with optional API key</li>
<li><strong>FINRA</strong> - RegSHO short sale volume data (public CDN, no API key)</li>
<li><strong>Wikimedia</strong> - Wikipedia daily page view counts per company</li>
<li><strong>YouTube Data API</strong> - Video mentions from curated investing channels</li>
<li><strong>Google Trends</strong> - Theme interest data (experimental, opt-in)</li>
<li><strong>NewsAPI</strong> - Professional news aggregation</li>
<li><strong>Reddit API</strong> - Social media sentiment from investing subreddits</li>
</ul>
<footer style="margin-top: 3em; padding-top: 2em; border-top: 1px solid #eee; text-align: center; color: #666;">
<p><strong>Stock Oracle API</strong> - Built with FastAPI, powered by SEC EDGAR data</p>
<p>For complete interactive documentation, visit <a href="{settings.API_PREFIX}/docs">Swagger UI</a></p>
<p><small>Version {settings.APP_VERSION}</small></p>
</footer>
</body>
</html>
"""
return HTMLResponse(content=simple_html)
except Exception as e:
# Fallback to Swagger UI if anything goes wrong
except Exception:
return RedirectResponse(url=f"{settings.API_PREFIX}/docs")
# Additional metadata for OpenAPI

Loading…
Cancel
Save