보유종목 1분마다 호출하는부분 수정
This commit is contained in:
@@ -101,10 +101,17 @@ _last_reset_date: Optional[date] = None
|
||||
# 매매 실행 lock — _positions/_daily_pnl/_closed_today 변경 시 반드시 획득
|
||||
_trade_lock = asyncio.Lock()
|
||||
|
||||
# 포트폴리오 요약 캐시 (get_balance() 호출 최소화)
|
||||
_portfolio_summary_cache: Dict[str, float] = {}
|
||||
_portfolio_summary_ts: Optional[datetime] = None
|
||||
_portfolio_cache_dirty: bool = True # 최초 또는 매매 발생 시 강제 갱신
|
||||
_PORTFOLIO_CACHE_TTL: float = 300.0 # 매매 없을 때 최대 5분 캐시 유지
|
||||
|
||||
|
||||
def _reset_if_new_day() -> None:
|
||||
"""날짜가 바뀌면 일일 상태 초기화"""
|
||||
global _daily_pnl, _orb_cache, _last_reset_date, _last_total_assets, _closed_today
|
||||
global _portfolio_summary_cache, _portfolio_summary_ts, _portfolio_cache_dirty
|
||||
today = _today_kst()
|
||||
if _last_reset_date != today:
|
||||
_daily_pnl = 0.0
|
||||
@@ -112,6 +119,9 @@ def _reset_if_new_day() -> None:
|
||||
_last_total_assets = 0.0
|
||||
_closed_today = set()
|
||||
_last_reset_date = today
|
||||
_portfolio_summary_cache = {}
|
||||
_portfolio_summary_ts = None
|
||||
_portfolio_cache_dirty = True
|
||||
logger.info(f"📅 데이트레이딩 상태 일일 초기화: {today}")
|
||||
|
||||
|
||||
@@ -143,18 +153,39 @@ async def _get_current_price(stock_code: str) -> float:
|
||||
return 0.0
|
||||
|
||||
|
||||
async def _get_portfolio_summary() -> Dict[str, float]:
|
||||
"""포트폴리오 요약 (총자산, 가용 현금)"""
|
||||
async def _get_portfolio_summary(force: bool = False) -> Dict[str, float]:
|
||||
"""포트폴리오 요약 (총자산, 가용 현금).
|
||||
|
||||
매매가 발생했거나 캐시 TTL이 만료된 경우에만 KIS get_balance() 호출.
|
||||
그 외에는 인메모리 캐시 반환 (API 호출 최소화).
|
||||
"""
|
||||
global _portfolio_summary_cache, _portfolio_summary_ts, _portfolio_cache_dirty
|
||||
|
||||
now = _kst_now()
|
||||
cache_age = (now - _portfolio_summary_ts).total_seconds() if _portfolio_summary_ts else 9999.0
|
||||
cache_valid = (
|
||||
not _portfolio_cache_dirty
|
||||
and cache_age < _PORTFOLIO_CACHE_TTL
|
||||
and bool(_portfolio_summary_cache)
|
||||
)
|
||||
|
||||
if not force and cache_valid:
|
||||
logger.debug(f"⏭️ Portfolio summary cache hit (age={cache_age:.0f}s) — skipping get_balance()")
|
||||
return _portfolio_summary_cache
|
||||
|
||||
try:
|
||||
balance = await korea_investment_api.get_balance()
|
||||
output2 = balance.get("output2", [{}])
|
||||
summary = output2[0] if output2 else {}
|
||||
total_assets = float(summary.get("tot_evlu_amt", 0) or 0)
|
||||
available_cash = float(summary.get("prvs_rcdl_excc_amt", 0) or 0)
|
||||
return {"total_assets": total_assets, "available_cash": available_cash}
|
||||
_portfolio_summary_cache = {"total_assets": total_assets, "available_cash": available_cash}
|
||||
_portfolio_summary_ts = now
|
||||
_portfolio_cache_dirty = False
|
||||
return _portfolio_summary_cache
|
||||
except Exception as e:
|
||||
logger.error(f"포트폴리오 조회 실패: {e}")
|
||||
return {"total_assets": 0.0, "available_cash": 0.0}
|
||||
return _portfolio_summary_cache or {"total_assets": 0.0, "available_cash": 0.0}
|
||||
|
||||
|
||||
async def _place_buy_order(
|
||||
@@ -412,6 +443,7 @@ async def _check_exits(
|
||||
ok = await _place_sell_order(code, quantity, price=0) # 시장가
|
||||
if ok:
|
||||
pnl = (current_price - pos["entry_price"]) * quantity
|
||||
_portfolio_cache_dirty = True # 매도 체결 → 잔고 변경됨
|
||||
async with _trade_lock:
|
||||
_daily_pnl += pnl
|
||||
_positions.pop(code, None)
|
||||
@@ -580,6 +612,9 @@ async def _execute_entry(signal: DayTradeSignal, quantity: int) -> Optional[Dict
|
||||
if not order_no:
|
||||
return None
|
||||
|
||||
global _portfolio_cache_dirty
|
||||
_portfolio_cache_dirty = True # 매수 체결 → 잔고 변경됨
|
||||
|
||||
_positions[signal.stock_code] = {
|
||||
"stock_name": signal.stock_name,
|
||||
"strategy": signal.strategy,
|
||||
@@ -643,6 +678,7 @@ async def _execute_eod_flat() -> None:
|
||||
ok = await _place_sell_order(code, pos["quantity"], price=0)
|
||||
if ok:
|
||||
pnl = (current_price - pos["entry_price"]) * pos["quantity"]
|
||||
_portfolio_cache_dirty = True # EOD 청산 → 잔고 변경됨
|
||||
async with _trade_lock:
|
||||
_daily_pnl += pnl
|
||||
_positions.pop(code, None)
|
||||
|
||||
@@ -606,7 +606,13 @@ async def check_and_execute_exit_trades(db: AsyncSession):
|
||||
from datetime import datetime
|
||||
|
||||
try:
|
||||
await sync_portfolio(db, force=True)
|
||||
# 매매가 발생했거나 캐시가 비어있을 때만 KIS 잔고 조회 (API 호출 최소화)
|
||||
latest_trade_id = await _get_latest_executed_trade_id(db)
|
||||
if latest_trade_id != portfolio_cache.last_trade_id or not portfolio_cache.holdings:
|
||||
logger.info(f"🔄 New trade detected (prev={portfolio_cache.last_trade_id}, cur={latest_trade_id}) — syncing balance")
|
||||
await sync_portfolio(db, force=True)
|
||||
else:
|
||||
logger.debug("⏭️ No new trade since last sync — skipping get_balance()")
|
||||
|
||||
# MDD 업데이트 (async — thread-safe lock 내부)
|
||||
summary = await _get_portfolio_summary(db)
|
||||
|
||||
Reference in New Issue
Block a user