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:
@@ -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(),
|
||||
|
||||
Reference in New Issue
Block a user