1313# 🚨 [V27.18 팩트 교정] 주식 정수 매매 원칙에 따른 소수점 내림(Truncation) 및 잔여 예산 이월 로직 100% 원상 복구
1414# 🚨 [V26.06 그랜드 수술] 심층 결함 비파괴(Fail-Safe) 방어막 이식 및 코파일럿 오진(결함 #4) 영구 차단 락아웃 적용
1515# 🚨 [V26.06 3차 심층수술] VWAP 다일 누적 오염, 유령 주문 페이징, Null 붕괴 등 7대 마이크로 엣지 케이스 전면 교정
16+ # 🚨 [V26.06 4차 심층수술] 파일 I/O 클린업, 이종 거래소 분할 결제 수량 합산, DST 경계 타임스탬프 등 6대 엣지 케이스 전면 교정
1617# ==========================================================
1718
1819import requests
@@ -75,13 +76,20 @@ def _get_access_token(self, force=False):
7576 if dir_name and not os .path .exists (dir_name ):
7677 os .makedirs (dir_name , exist_ok = True )
7778 fd , temp_path = tempfile .mkstemp (dir = dir_name , text = True )
78- with os .fdopen (fd , 'w' , encoding = 'utf-8' ) as f :
79- json .dump ({'token' : self .token , 'expire' : expire_str }, f )
80- f .flush ()
81- # MODIFIED: [1차 수술] fsync 스레드 경합 방어 (원자적 쓰기 동기화 위치 및 대상 교정)
82- os .fsync (f .fileno ())
8379
84- shutil .move (temp_path , self .token_file )
80+ try :
81+ with os .fdopen (fd , 'w' , encoding = 'utf-8' ) as f :
82+ json .dump ({'token' : self .token , 'expire' : expire_str }, f )
83+ f .flush ()
84+ # MODIFIED: [1차 수술] fsync 스레드 경합 방어 (원자적 쓰기 동기화 위치 및 대상 교정)
85+ os .fsync (f .fileno ())
86+
87+ shutil .move (temp_path , self .token_file )
88+ finally :
89+ # MODIFIED: [4차 수술 - 결함 #1] shutil.move 실패 시 고아 임시 파일(Orphan Temp File) 누적 방어 클린업
90+ if os .path .exists (temp_path ):
91+ try : os .remove (temp_path )
92+ except Exception : pass
8593 else :
8694 print (f"❌ [Broker] 토큰 발급 실패: { data .get ('error_description' , '알 수 없는 오류' )} " )
8795 except Exception as e :
@@ -244,8 +252,18 @@ def get_account_balance(self):
244252 print (f"⚠️ [Broker] ord_psbl_qty 0주 응답 감지 ({ ticker } ). KIS T+1 결제 지연 엣지 케이스 방어를 위해 보유 수량({ qty } )으로 안전 폴백(Fallback) 처리." )
245253 ord_psbl_qty = qty
246254
247- if qty > 0 and ticker not in holdings :
248- holdings [ticker ] = {'qty' : qty , 'ord_psbl_qty' : ord_psbl_qty , 'avg' : avg }
255+ # MODIFIED: [4차 수술 - 결함 #2] 이종 거래소 분할 결제 시 보유 수량 덮어쓰기 무시 방어 (누적 합산 및 평단가 가중평균 이식)
256+ if qty > 0 :
257+ if ticker not in holdings :
258+ holdings [ticker ] = {'qty' : qty , 'ord_psbl_qty' : ord_psbl_qty , 'avg' : avg }
259+ else :
260+ prev = holdings [ticker ]
261+ total_qty = prev ['qty' ] + qty
262+ new_avg = ((prev ['avg' ] * prev ['qty' ]) + (avg * qty )) / total_qty if total_qty > 0 else avg
263+
264+ holdings [ticker ]['qty' ] = total_qty
265+ holdings [ticker ]['ord_psbl_qty' ] += ord_psbl_qty
266+ holdings [ticker ]['avg' ] = new_avg
249267
250268 if api_success : return cash , holdings
251269 else : return cash , None
@@ -272,9 +290,9 @@ def get_current_5min_candle(self, ticker):
272290
273291 if regular_market .empty : return None
274292
275- # MODIFIED: [결함 A] 다일 누적 VWAP 오염 방어 (당일 세션 데이터로 앵커링 필터링)
276- today_date = datetime . datetime .now (est ).date ()
277- regular_market = regular_market [regular_market .index . date = = today_date ]
293+ # MODIFIED: [4차 수술 - 결함 #3] DST 경계 시간대 타임스탬프 붕괴 방어 (normalize 필터링 이식 )
294+ today_date = pd . Timestamp .now (tz = est ).normalize ()
295+ regular_market = regular_market [regular_market .index > = today_date ]
278296
279297 if regular_market .empty : return None
280298
@@ -466,18 +484,27 @@ def get_unfilled_orders_detail(self, ticker):
466484 continue
467485 else : break
468486 else :
469- break
487+ # MODIFIED: [4차 수술 - 결함 #4] 페이징 중 통신 타임아웃 발생 시 불완전한 결과 반환 방어 (False 플래그 반환으로 덫 철거 실패 강제)
488+ print (f"🚨 [Broker] { ticker } 미체결 내역 페이징 중 통신 단절. 불완전한 덫 철거를 방지하기 위해 False 반환." )
489+ return False
470490
471491 return valid_orders
472492
473493 def get_unfilled_orders (self , ticker ):
474494 # MODIFIED: [3차 수술 - 결함 F] 페이징 방어막이 이식된 detail 함수를 호출하여 구조적 중복 제거 및 무결성 확보
475495 details = self .get_unfilled_orders_detail (ticker )
496+ # MODIFIED: [4차 수술 - 결함 #4] detail에서 통신 오류로 False 반환 시 상위로 전파
497+ if details is False :
498+ return []
476499 return [item .get ('odno' ) for item in details ]
477500
478501 def cancel_all_orders_safe (self , ticker , side = None ):
479502 for i in range (3 ):
480503 orders = self .get_unfilled_orders_detail (ticker )
504+ # MODIFIED: [4차 수술 - 결함 #4] orders가 False(통신 실패)일 경우 즉각 실패 처리하여 유령 덫 잔류 방어
505+ if orders is False :
506+ print (f"🚨 [Broker] { ticker } 미체결 내역 조회 실패로 취소 프로세스 강제 중단." )
507+ return False
481508 if not orders : return True
482509
483510 target_orders = orders
@@ -490,8 +517,10 @@ def cancel_all_orders_safe(self, ticker, side=None):
490517 time .sleep (5 )
491518
492519 final_orders = self .get_unfilled_orders_detail (ticker )
520+ if final_orders is False :
521+ return False
522+
493523 failed_orders = []
494-
495524 if side == "BUY" : failed_orders = [o for o in final_orders if o .get ('sll_buy_dvsn_cd' ) == '02' ]
496525 elif side == "SELL" : failed_orders = [o for o in final_orders if o .get ('sll_buy_dvsn_cd' ) == '01' ]
497526 else : failed_orders = final_orders
@@ -508,7 +537,7 @@ def cancel_all_orders_safe(self, ticker, side=None):
508537 def cancel_targeted_orders (self , ticker , side , target_ord_dvsn ):
509538 sll_buy_cd = '02' if side == "BUY" else '01'
510539 orders = self .get_unfilled_orders_detail (ticker )
511- if not orders : return 0
540+ if orders is False or not orders : return 0
512541
513542 target_orders = []
514543 for o in orders :
@@ -525,7 +554,7 @@ def cancel_targeted_orders(self, ticker, side, target_ord_dvsn):
525554 def cancel_orders_by_price (self , ticker , side , target_prices ):
526555 sll_buy_cd = '02' if side == "BUY" else '01'
527556 orders = self .get_unfilled_orders_detail (ticker )
528- if not orders : return 0
557+ if orders is False or not orders : return 0
529558
530559 target_orders = []
531560 for o in orders :
@@ -575,6 +604,7 @@ def send_order(self, ticker, side, qty, price, order_type="LIMIT"):
575604 elif order_type == "MOO" : ord_dvsn = "31"
576605 elif order_type == "AFTER_LIMIT" :
577606 # MODIFIED: [1차 수술] AFTER_LIMIT은 지정가(00)와 동일 취급되므로 반드시 0초과 가격 필수 요구됨을 강제 명시
607+ # MODIFIED: [Copilot Audit Exception - 4차 기각] SOXL/TQQQ 전용 0.01 하드락 적용 의도됨. OTC 티어 호가 훼손 지적은 타당하나 V26.06 아키텍처 규격에 따라 예외(Exception) 처리 및 락아웃 적용. 임의 수정 절대 금지.
578608 ord_dvsn = "00"
579609 else : ord_dvsn = "00"
580610
@@ -685,7 +715,10 @@ def get_genesis_ledger(self, ticker, limit_date_str=None):
685715 loop_counter = 0
686716
687717 while curr_qty > 0 and not genesis_reached and loop_counter < 365 :
688- loop_counter += 1
718+ # MODIFIED: [4차 수술 - 결함 #5] 장부 복원 시 주말(휴장일)을 배제하고 순수 영업일(Trading Days) 기준으로 365일을 추적하도록 앵커링 연장
719+ if target_date .weekday () < 5 :
720+ loop_counter += 1
721+
689722 date_str = target_date .strftime ('%Y%m%d' )
690723
691724 if limit_date_str and date_str < limit_date_str : break
0 commit comments