feat: 데이트레이딩 조회 API를 실제 엔진 상태에 연결 + 대시보드 엔진 상태 바

/daytrading/{status,watchlist,positions}(v1)는 main.py가 실행하는 엔진
(services/daytrading)과 공유되지 않는 별개 인메모리 dict를 읽어 항상 0을 반환했음.
조회는 entry_service 감시목록·day_trading_state 기준으로 응답하고, POST /watchlist는
엔진에도 반영. 프론트 보유종목 패널 하단에 감시/ORB/포지션/청산/당일손익·현재 단계
(ORB 수집중·신호 대기·쿨다운 등)를 30초 폴링으로 표시.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-09-22 11:23:13 +09:00
parent 8b8c7a423e
commit b2a1b884d4
3 changed files with 133 additions and 24 deletions

View File

@@ -777,22 +777,36 @@ async def restore_positions_from_db() -> None:
# ── API 엔드포인트 ────────────────────────────────────────────────────────────
# ── 실제 매매 엔진(services/daytrading) 상태 연결 ─────────────────────────────
# main.py 는 services/daytrading/scheduler.daytrading_scheduler() 를 실행한다.
# 이 모듈의 _watchlist/_positions/_orb_cache 는 그 엔진과 공유되지 않는 별개 dict라
# 조회 API가 항상 0을 돌려주는 문제가 있었음(2026-09-22). 조회/설정 엔드포인트는
# 엔진 상태(entry_service._watchlist, day_trading_state)를 기준으로 응답한다.
def _engine():
from app.services.daytrading import entry_service as _es
from app.services.daytrading.position_manager import day_trading_state as _st
return _es, _st
@router.get("/watchlist")
async def get_watchlist():
"""감시 종목 목록 조회"""
"""감시 종목 목록 조회 (실제 엔진 기준)"""
_reset_if_new_day()
_es, _st = _engine()
_st.reset_if_new_day()
wl = _es.get_watchlist()
return {
"watchlist": [
{"code": k, "name": v, "has_orb": k in _orb_cache}
for k, v in _watchlist.items()
{"code": k, "name": v, "has_orb": _st.has_orb(k)}
for k, v in wl.items()
],
"count": len(_watchlist),
"count": len(wl),
}
@router.post("/watchlist")
async def set_watchlist(req: WatchlistRequest):
"""감시 종목 설정 (최대 20종목)
"""감시 종목 설정 (최대 20종목) — 실제 엔진과 이 모듈 양쪽에 반영
body: {"stocks": [{"code": "005930", "name": "삼성전자"}, ...]}
"""
@@ -801,6 +815,8 @@ async def set_watchlist(req: WatchlistRequest):
raise HTTPException(400, "감시 종목은 최대 20종목까지만 설정 가능합니다.")
_watchlist = {s["code"]: s.get("name", s["code"]) for s in req.stocks}
_es, _ = _engine()
_es.set_watchlist(req.stocks)
logger.info(f"📋 감시 종목 설정: {list(_watchlist.keys())}")
return {"status": "ok", "watchlist": list(_watchlist.keys())}
@@ -921,36 +937,40 @@ async def execute_day_trade(req: ExecuteRequest):
@router.get("/positions")
async def get_positions():
"""활성 데이트레이딩 포지션 조회"""
"""활성 데이트레이딩 포지션 조회 (실제 엔진 기준)"""
_reset_if_new_day()
_, _st = _engine()
_st.reset_if_new_day()
result = []
for code, pos in _positions.items():
for code, pos in _st.positions.items():
entry_price = float(pos.get("entry_price", 0) or 0)
quantity = int(pos.get("quantity", 0) or 0)
try:
current_price = await _get_current_price(code)
except Exception:
current_price = pos["entry_price"]
pnl = (current_price - pos["entry_price"]) * pos["quantity"]
pnl_pct = (current_price - pos["entry_price"]) / pos["entry_price"] * 100
current_price = entry_price
pnl = (current_price - entry_price) * quantity
pnl_pct = ((current_price - entry_price) / entry_price * 100) if entry_price > 0 else 0.0
result.append({
"stock_code": code,
"stock_name": pos["stock_name"],
"strategy": pos["strategy"],
"entry_price": pos["entry_price"],
"stock_name": pos.get("stock_name", code),
"strategy": pos.get("strategy", ""),
"entry_price": entry_price,
"current_price": current_price,
"stop_loss": pos["stop_loss"],
"target_price": pos["target_price"],
"quantity": pos["quantity"],
"stop_loss": pos.get("stop_loss"),
"target_price": pos.get("target_price"),
"quantity": quantity,
"pnl": round(pnl, 0),
"pnl_pct": round(pnl_pct, 2),
"entry_time": pos["entry_time"],
"entry_time": str(pos.get("entry_time", "")),
})
return {
"positions": result,
"count": len(result),
"daily_pnl": round(_daily_pnl, 0),
"daily_pnl": round(_st.daily_pnl, 0),
"is_daily_loss_exceeded": is_daily_loss_exceeded(
_last_total_assets,
_daily_pnl,
_st.last_total_assets,
_st.daily_pnl,
),
}
@@ -979,11 +999,15 @@ async def manual_cycle():
async def get_status():
"""데이트레이딩 시스템 상태 요약"""
_reset_if_new_day()
_es, _st = _engine()
_st.reset_if_new_day()
return {
"watchlist_count": len(_watchlist),
"active_positions": len(_positions),
"orb_collected": len(_orb_cache),
"daily_pnl": round(_daily_pnl, 0),
"watchlist_count": len(_es.get_watchlist()),
"active_positions": _st.position_count(),
"orb_collected": len(_st.orb_cache),
"daily_pnl": round(_st.daily_pnl, 0),
"closed_today": len(_st.closed_today),
"cooldown_active": bool(_st.is_cooldown_active()),
"is_market_open": is_market_open(),
"is_opening_range": is_opening_range_period(),
"is_entry_window": is_day_trade_entry_window(),