@@ -35,24 +35,20 @@ def check_basket_account_isolation(basket_names, config, mode: str) -> list[str]
3535 """여러 enabled 바스켓이 같은 계좌(자본 풀)를 공유하는지 검사한다. fail-closed.
3636
3737 각 BasketRebalancer는 자기 목표 비중을 '총자산 × stock_fraction'에 독립적으로
38- 배분한다. 두 바스켓이 같은 자본을 공유하면 예컨대 80% + 80% = 160%를 배분하려
39- 들어 과배분·상호 간섭이 생기고, paper에서는 동일 default 계정의 NAV 시계열이
40- 섞여 60영업일 트랙레코드가 오염된다. 바스켓을 여러 개 운영하려면 계좌를
41- 분리해야 한다(live: kis_api.accounts에 basket_rebalance:<name>별 계좌 지정).
38+ 배분한다. 두 바스켓이 같은 자본(계좌)을 공유하면 예컨대 80% + 80% = 160%를
39+ 배분하려 들어 과배분·상호 간섭이 생긴다.
40+
41+ paper: 바스켓별 가상 계정 키(basket_rebalance:<name>)로 DB가 격리되고 각자
42+ initial_capital 기준으로 독립 집계되므로 자연 격리 — 통과.
43+ live: 같은 KIS 실계좌(잔고)를 공유하면 차단. 다중 바스켓 live 운영은
44+ kis_api.accounts에 basket_rebalance:<name>별 계좌를 분리 지정해야 한다.
4245
4346 반환: 이슈 문자열 리스트 (빈 리스트 = 통과).
4447 """
4548 names = list (basket_names or [])
46- if len (names ) <= 1 :
49+ if len (names ) <= 1 or str ( mode ). lower () != "live" :
4750 return []
4851
49- if str (mode ).lower () != "live" :
50- return [
51- f"enabled 바스켓 { len (names )} 개({ ', ' .join (names )} )가 paper 기본 계정(자본 풀)을 "
52- "공유합니다 — 각 바스켓이 같은 자본에 목표 비중을 독립 배분해 과배분되고 "
53- "트랙레코드(NAV 시계열)가 섞입니다. 하나만 enabled로 두거나 운영을 분리하세요."
54- ]
55-
5652 by_account : dict [str , list [str ]] = {}
5753 for name in names :
5854 try :
@@ -98,11 +94,16 @@ def __init__(
9894 basket_name : str ,
9995 config : Config = None ,
10096 account_key : str = "" ,
101- execution_strategy : str = "basket_rebalance " ,
97+ execution_strategy : str = "" ,
10298 ):
10399 self .config = config or Config .get ()
104- self .account_key = account_key
105- self .execution_strategy = execution_strategy or "basket_rebalance"
100+ # 계정·귀속 키 기본값: paper/live 공통으로 바스켓 전용 키(basket_rebalance:<name>).
101+ # 기본 계정("")은 전 계정 합산 뷰라서 다른 전략의 paper 거래 한 건에도 NAV·드리프트·
102+ # 평가가 오염되고, 이름 없는 strategy("basket_rebalance")로는 어느 바스켓의 트랙레코드
103+ # 인지 귀속이 불가능하다(다른 바스켓 기록으로 승격되는 구멍). 키로 격리·귀속한다.
104+ default_key = rebalance_live_strategy_id (basket_name )
105+ self .account_key = account_key or default_key
106+ self .execution_strategy = execution_strategy or default_key
106107 self .basket_name = basket_name
107108
108109 baskets_cfg = self ._load_baskets_config ()
@@ -146,6 +147,58 @@ def _is_live(self) -> bool:
146147 """실전(live) 모드 여부."""
147148 return str (self .config .trading .get ("mode" , "paper" )).lower () == "live"
148149
150+ def save_daily_nav_snapshot (self ) -> bool :
151+ """바스켓 계정의 일일 NAV 스냅샷 저장. 트랙레코드 시계열의 1행.
152+
153+ 보유 종목 가격이 전부 확보됐을 때만 저장한다 — 가격 미확보 시
154+ avg_price 폴백으로 평가된 가짜 NAV가 '커버된 영업일'로 집계되는 것보다,
155+ 스킵하고 health의 끊김 감지에 노출되는 편이 정직하다.
156+
157+ 귀속 날짜는 NAV의 가격 기준일이다: 비거래일(주말·휴장일) 보충 실행에서
158+ 조회되는 가격은 직전 거래일 종가이므로 그 거래일로 귀속한다 — PC가 꺼져
159+ 있던 거래일을 다음날 보충 실행이 정당하게 커버한다(주말 날짜 스냅샷은
160+ 커버리지에 영원히 안 잡히는 낭비였다). (account_key, date) upsert 멱등.
161+ """
162+ try :
163+ snapshot = getattr (self , "_market_snapshot" , None ) or self ._fetch_market_snapshot ()
164+ prices = {s : v ["price" ] for s , v in snapshot .items ()}
165+ positions = get_all_positions (account_key = self .account_key )
166+ missing = [p .symbol for p in positions if p .symbol not in prices ]
167+ if missing :
168+ logger .warning (
169+ "바스켓 '{}' NAV 스냅샷 스킵 — 가격 미확보 종목: {} (가짜 NAV 방지)" ,
170+ self .basket_name , missing ,
171+ )
172+ return False
173+ self .portfolio_mgr .save_daily_snapshot (
174+ current_prices = prices or None ,
175+ snapshot_date = self ._nav_attribution_date (),
176+ )
177+ return True
178+ except Exception as e :
179+ logger .warning ("바스켓 '{}' NAV 스냅샷 저장 실패: {}" , self .basket_name , e )
180+ return False
181+
182+ def _nav_attribution_date (self ) -> datetime :
183+ """NAV 스냅샷 귀속 날짜: 오늘이 거래일이면 오늘, 아니면 직전 거래일.
184+
185+ 조회 가격이 직전 거래일 종가이므로 그 날짜가 정직한 귀속일이다.
186+ 거래일 판정 실패 시 오늘로 폴백(보수적 — 기존 동작).
187+ """
188+ now = datetime .now (_KST ).replace (tzinfo = None )
189+ try :
190+ from core .trading_hours import TradingHours
191+
192+ th = TradingHours (self .config )
193+ d = now
194+ for _ in range (15 ): # 최장 연휴 커버
195+ if th .is_trading_day (d ):
196+ return d
197+ d -= timedelta (days = 1 )
198+ except Exception as e :
199+ logger .debug ("거래일 판정 실패 — 오늘로 귀속: {}" , e )
200+ return now
201+
149202 # ------------------------------------------------------------------
150203 # Config
151204 # ------------------------------------------------------------------
0 commit comments