fix: 감사 잔여 항목 — 잔량 출처 DB화·ORB 영속화·미국 부분청산·듀얼전략 태깅·NULL 가격
- portfolio_cache_bridge: 잔량 산출을 Portfolio(DB) 기준으로. 캐시는 sync 실패 시 최대 5분 낡아 실제와 다른 잔량이 기록될 수 있었음(감사 #14). 조회 실패 시 캐시 폴백. - ORB 캐시 Redis 영속화(save/load_orb_cache) — 재배포 시 인메모리가 날아가 당일 ORB 전략이 통째로 불가능했음. 수집 후 저장, 스케줄러 기동·수집 시 복원. - us_market_scheduler: 부분청산이 Portfolio.quantity 를 갱신하지 않던 문제 → reflect_sell 연동. - 듀얼 전략(MOMENTUM_BREAKOUT/PULLBACK_BUY)이 ActiveTrade.strategy_name 에 저장되지 않아 CLAUDE.md §8.4 모멘텀 전용 청산(+10%/+20% 분할)이 한 번도 실행된 적 없었음 → 태깅 전달. 매도 시에는 실제 행의 strategy_name 으로 close (swing 으로 닫으면 유령 행 잔존). - 수동매도: current_price NULL 시 TypeError 로 주문 후 상태 갱신이 통째로 누락되던 엣지 수정. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -175,6 +175,10 @@ async def collect_opening_ranges() -> None:
|
||||
KIS API 호출 간격: 0.3초 (레이트 리밋 준수)
|
||||
"""
|
||||
day_trading_state.reset_if_new_day()
|
||||
# 재기동으로 인메모리가 비었을 수 있으므로 먼저 Redis 캐시를 복원한다
|
||||
await day_trading_state.load_orb_cache()
|
||||
|
||||
_collected_before = len(day_trading_state.orb_cache)
|
||||
|
||||
for stock_code in list(_watchlist.keys()):
|
||||
# 이미 ORB가 수집된 종목은 건너뜀
|
||||
@@ -194,6 +198,9 @@ async def collect_opening_ranges() -> None:
|
||||
except Exception as e:
|
||||
logger.debug(f"[DayEntry] ORB 수집 실패 ({stock_code}): {e}")
|
||||
|
||||
if len(day_trading_state.orb_cache) != _collected_before:
|
||||
await day_trading_state.save_orb_cache()
|
||||
|
||||
|
||||
async def scan_and_enter() -> list[dict]:
|
||||
"""감시 종목의 5분봉 데이터를 스캔하여 진입 신호 발생 시 매수 실행.
|
||||
|
||||
@@ -230,6 +230,58 @@ class DayTradingState:
|
||||
"""
|
||||
return stock_code in self.orb_cache
|
||||
|
||||
async def save_orb_cache(self) -> bool:
|
||||
"""ORB 캐시를 Redis 에 저장 (당일 키, 자정까지 TTL).
|
||||
|
||||
컨테이너 재기동 시 인메모리 캐시가 사라져 당일 ORB 전략이 불가능해지는 것을 막는다.
|
||||
"""
|
||||
try:
|
||||
from app.core.redis import setex_json
|
||||
payload = {
|
||||
code: {
|
||||
"high": float(getattr(o, "high", 0) or 0),
|
||||
"low": float(getattr(o, "low", 0) or 0),
|
||||
"total_volume": int(getattr(o, "total_volume", 0) or 0),
|
||||
"avg_vol_per_min": float(getattr(o, "avg_vol_per_min", 0) or 0),
|
||||
"open_price": float(getattr(o, "open_price", 0) or 0),
|
||||
}
|
||||
for code, o in self.orb_cache.items()
|
||||
}
|
||||
if not payload:
|
||||
return False
|
||||
return await setex_json(f"daytrading:orb:{_today_kst()}", 86400, payload)
|
||||
except Exception as e:
|
||||
logger.warning(f"[DayTrading] ORB 캐시 저장 실패: {e}")
|
||||
return False
|
||||
|
||||
async def load_orb_cache(self) -> int:
|
||||
"""Redis 에서 당일 ORB 캐시 복원. 복원한 종목 수 반환."""
|
||||
try:
|
||||
from app.core.redis import get_json
|
||||
from app.services.day_trading_strategy import OpeningRange
|
||||
|
||||
data = await get_json(f"daytrading:orb:{_today_kst()}")
|
||||
if not data:
|
||||
return 0
|
||||
restored = 0
|
||||
for code, o in data.items():
|
||||
if code in self.orb_cache:
|
||||
continue
|
||||
self.orb_cache[code] = OpeningRange(
|
||||
high=float(o.get("high", 0) or 0),
|
||||
low=float(o.get("low", 0) or 0),
|
||||
total_volume=int(o.get("total_volume", 0) or 0),
|
||||
avg_vol_per_min=float(o.get("avg_vol_per_min", 0) or 0),
|
||||
open_price=float(o.get("open_price", 0) or 0),
|
||||
)
|
||||
restored += 1
|
||||
if restored:
|
||||
logger.info(f"[DayTrading] ORB 캐시 복원: {restored}개 종목")
|
||||
return restored
|
||||
except Exception as e:
|
||||
logger.warning(f"[DayTrading] ORB 캐시 복원 실패: {e}")
|
||||
return 0
|
||||
|
||||
def set_orb(self, stock_code: str, orb: Any) -> None:
|
||||
"""ORB(시초가 레인지) 데이터 저장.
|
||||
|
||||
|
||||
@@ -77,6 +77,12 @@ async def daytrading_scheduler() -> None:
|
||||
await asyncio.sleep(5)
|
||||
logger.info("⚡ [DayTrading] Scheduler started (every 30s)")
|
||||
|
||||
# 재기동 시 당일 ORB 캐시 복원 — 없으면 그날 ORB 전략이 통째로 불가능해진다
|
||||
try:
|
||||
await day_trading_state.load_orb_cache()
|
||||
except Exception as e:
|
||||
logger.warning(f"[DayTrading] ORB 캐시 복원 실패: {e}")
|
||||
|
||||
# 앱 재시작 시 DB에서 당일 데이트레이딩 포지션 복원 (CRITICAL 버그 수정)
|
||||
try:
|
||||
from app.core.database import AsyncSessionLocal
|
||||
|
||||
Reference in New Issue
Block a user