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 import os
from contextlib import asynccontextmanager from contextlib import asynccontextmanager
from fastapi import FastAPI, HTTPException from fastapi import FastAPI, HTTPException, Request
from fastapi.middleware.cors import CORSMiddleware from fastapi.middleware.cors import CORSMiddleware
from fastapi.responses import RedirectResponse, HTMLResponse from fastapi.responses import RedirectResponse, HTMLResponse
@ -62,350 +62,119 @@ app.add_middleware(
# Include API router # Include API router
app.include_router(api_router, prefix=settings.API_PREFIX) 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) @app.get("/", response_class=HTMLResponse, include_in_schema=False)
async def root_documentation(): async def root_documentation(request: Request):
"""
Display comprehensive API documentation at root path
"""
try: try:
# Simple working version schema = request.app.openapi()
simple_html = f""" paths = schema.get("paths", {})
<!DOCTYPE html> api_prefix = settings.API_PREFIX
<html lang="en">
<head> # 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 charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0"> <meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Stock Oracle API Documentation</title> <title>{title}</title>
<style> <style>
body {{ body {{ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif; line-height: 1.6; max-width: 1100px; margin: 0 auto; padding: 24px; color: #333; }}
line-height: 1.6; h1 {{ color: #1a1a2e; border-bottom: 3px solid #007acc; padding-bottom: 10px; }}
max-width: 1000px; h3 {{ color: #2c3e50; margin-top: 2em; text-transform: capitalize; border-left: 4px solid #007acc; padding-left: 10px; }}
margin: 0 auto; .nav {{ margin: 20px 0; display: flex; gap: 10px; flex-wrap: wrap; }}
padding: 20px; .nav a {{ padding: 8px 18px; background: #007acc; color: white; text-decoration: none;
color: #333; border-radius: 6px; font-size: 0.9em; }}
}} .nav a:hover {{ background: #005fa3; }}
h1 {{ color: #007acc; border-bottom: 2px solid #007acc; padding-bottom: 10px; }} .meta {{ color: #666; font-size: 0.9em; margin-bottom: 1.5em; }}
h2 {{ color: #2c3e50; margin-top: 2em; }} table {{ width: 100%; border-collapse: collapse; margin-bottom: 1em; font-size: 0.9em; }}
.nav-links {{ margin: 20px 0; }} tr:hover {{ background: #f8f9fa; }}
.nav-links a {{ td {{ padding: 7px 10px; border-bottom: 1px solid #eee; vertical-align: top; }}
display: inline-block; td:first-child {{ width: 72px; }}
margin-right: 15px; td:nth-child(2) {{ width: 38%; }}
padding: 10px 20px; code {{ background: #f4f4f4; padding: 2px 6px; border-radius: 3px; font-size: 0.85em; word-break: break-all; }}
background: #007acc; .method {{ display: inline-block; color: white; font-weight: bold; font-size: 0.75em;
color: white; padding: 3px 8px; border-radius: 4px; letter-spacing: 0.5px; }}
text-decoration: none; footer {{ margin-top: 3em; padding-top: 1.5em; border-top: 1px solid #eee;
border-radius: 5px; text-align: center; color: #888; font-size: 0.85em; }}
}}
.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; }}
</style> </style>
</head> </head>
<body> <body>
<h1>🔮 Stock Oracle API Documentation</h1> <h1>🔮 {title}</h1>
<p><strong>Comprehensive Investment Data Analysis API</strong></p> <div class="nav">
<a href="{api_prefix}/docs">📋 Swagger UI</a>
<div class="nav-links"> <a href="{api_prefix}/redoc">📖 ReDoc</a>
<a href="{settings.API_PREFIX}/docs">📋 Interactive API Docs</a> <a href="{api_prefix}/health">💚 Health</a>
<a href="{settings.API_PREFIX}/redoc">📖 ReDoc</a> <a href="{api_prefix}/openapi.json"> OpenAPI JSON</a>
<a href="{settings.API_PREFIX}/health">💚 Health Check</a>
<a href="{settings.API_PREFIX}/openapi.json"> OpenAPI Schema</a>
</div> </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> return HTMLResponse(content=html)
<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>
<h3>SEC Filings <span class="new-badge">NEW</span></h3> except Exception:
<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
return RedirectResponse(url=f"{settings.API_PREFIX}/docs") return RedirectResponse(url=f"{settings.API_PREFIX}/docs")
# Additional metadata for OpenAPI # Additional metadata for OpenAPI

Loading…
Cancel
Save