Skip to content

Commit be6cd76

Browse files
Add files via upload
1 parent 1685d24 commit be6cd76

1 file changed

Lines changed: 49 additions & 16 deletions

File tree

‎broker.py‎

Lines changed: 49 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@
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

1819
import 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

Comments
 (0)