diff --git a/dashboard-api/index.html b/dashboard-api/index.html index b82213c..356bcb6 100644 --- a/dashboard-api/index.html +++ b/dashboard-api/index.html @@ -201,6 +201,10 @@ body:not(.auth-required) #login-screen { display:none !important; } + + + +
@@ -212,6 +216,10 @@ body:not(.auth-required) #login-screen { display:none !important; } + + + + diff --git a/dashboard-api/main.py b/dashboard-api/main.py index c6e9ab2..4034454 100644 --- a/dashboard-api/main.py +++ b/dashboard-api/main.py @@ -9,10 +9,13 @@ from fastapi.staticfiles import StaticFiles from pydantic import BaseModel, EmailStr, Field from auth import hash_password, verify_password, create_token, current_user, dummy_verify -TA_ENGINE_URL = os.getenv("TA_ENGINE_URL", "http://ta-engine:8484") -KIS_API_URL = os.getenv("KIS_API_URL", "http://kis-api:8585") -OLLAMA_URL = os.getenv("OLLAMA_URL", "http://ollama:11434") -CHAT_MODEL = os.getenv("CHAT_MODEL", "exaone3.5:7.8b") +TA_ENGINE_URL = os.getenv("TA_ENGINE_URL", "http://ta-engine:8484") +KIS_API_URL = os.getenv("KIS_API_URL", "http://kis-api:8585") +OLLAMA_URL = os.getenv("OLLAMA_URL", "http://ollama:11434") +SCORE_ENGINE_URL = os.getenv("SCORE_ENGINE_URL", "http://score-engine:8686") +US_MARKET_URL = os.getenv("US_MARKET_URL", "http://us-market:8383") +AUX_SIGNAL_URL = os.getenv("AUX_SIGNAL_URL", "http://aux-signal:8282") +CHAT_MODEL = os.getenv("CHAT_MODEL", "exaone3.5:7.8b") class PositionReq(BaseModel): code: str @@ -1994,6 +1997,79 @@ async def calendar_api(days: int = Query(default=30, ge=1, le=180)): "recent_dart": [dict(r) for r in recent][:30]} +async def _proxy_get(url: str, timeout: float = 10.0): + """์™ธ๋ถ€ ์„œ๋น„์Šค GET ํ”„๋ก์‹œ (์‹คํŒจ ์‹œ ๋นˆ ์‘๋‹ต)""" + try: + async with httpx.AsyncClient(timeout=timeout) as c: + r = await c.get(url) + r.raise_for_status() + return r.json() + except Exception as e: + return {"error": str(e), "source": url} + + +@app.get("/api/formulas/matrix") +async def formulas_matrix(limit: int = Query(default=50, ge=5, le=200)): + """์ข…๋ชฉ ร— 10๊ณต์‹ ์‹ ํ˜ธ ๋งคํŠธ๋ฆญ์Šค (์˜ค๋Š˜ ์Šค์ฝ”์–ด ๊ธฐ์ค€ ์ƒ์œ„ N)""" + async with pg_pool.acquire() as c: + rows = await c.fetch(""" + SELECT s.stock_code, COALESCE(d.corp_name, s.stock_name) AS stock_name, + s.total_score, s.recommendation, s.buy_votes, s.sell_votes, + s.magic_score, s.f_score, s.altman_z, s.peg, s.momentum_pct, + s.beneish_score, s.gpa_pct, s.g_score, s.amihud_illiq, s.market_beta, + s.signals, s.sector + FROM stock_scores s + LEFT JOIN dart_corps d ON d.stock_code = s.stock_code + WHERE s.score_date = (SELECT MAX(score_date) FROM stock_scores) + AND COALESCE(d.is_active, true) = true + ORDER BY s.total_score DESC + LIMIT $1 + """, limit) + return [dict(r) for r in rows] + + +@app.get("/api/weights") +async def proxy_weights(): + """๊ณต์‹๋ณ„ ํ•™์Šต ๊ฐ€์ค‘์น˜ (score-engine /learn-weights)""" + return await _proxy_get(f"{SCORE_ENGINE_URL}/learn-weights") + + +@app.get("/api/backtest") +async def proxy_backtest(days: int = Query(default=180, ge=30, le=720)): + """์ถ”์ฒœ ๋ฐฑํ…Œ์ŠคํŠธ (score-engine /backtest)""" + return await _proxy_get(f"{SCORE_ENGINE_URL}/backtest?days={days}", timeout=30.0) + + +@app.get("/api/sector-concentration") +async def proxy_sector_concentration(): + """์„นํ„ฐ ์ง‘์ค‘๋„ + 30% ์ดˆ๊ณผ ๊ฒฝ๊ณ  (score-engine /sector/concentration)""" + return await _proxy_get(f"{SCORE_ENGINE_URL}/sector/concentration") + + +@app.get("/api/usmarket/etfs") +async def proxy_us_etfs(): + """๋ฏธ๊ตญ ์„นํ„ฐ ETF ๋ชฉ๋ก""" + return await _proxy_get(f"{US_MARKET_URL}/etfs") + + +@app.get("/api/usmarket/pairs") +async def proxy_us_pairs(): + """KRโ†”US ํŽ˜์–ด + 60์ผ ํšŒ๊ท€ ๋ฒ ํƒ€""" + return await _proxy_get(f"{US_MARKET_URL}/pairs") + + +@app.get("/api/usmarket/signals") +async def proxy_us_signals(): + """๋ฏธ์ฆ์‹œ ๋™์กฐ ์‹œ๊ทธ๋„ (์ „์ฒด)""" + return await _proxy_get(f"{US_MARKET_URL}/signal/latest") + + +@app.get("/api/macro/ecos") +async def proxy_macro_ecos(): + """ํ•œ๊ตญ ๋งคํฌ๋กœ (aux-signal /macro/latest: USD/KRW, ๊ตญ๊ณ ์ฑ„ 10๋…„, KOSPI)""" + return await _proxy_get(f"{AUX_SIGNAL_URL}/macro/latest") + + @app.get("/") async def index(): return FileResponse("/app/index.html")