From a64a3f017ba32775457831c5fffbfc81bb524bf2 Mon Sep 17 00:00:00 2001 From: jongjae Date: Fri, 15 May 2026 13:38:40 +0900 Subject: [PATCH] =?UTF-8?q?[2026-05-15]=20rate=20limit=C2=B7=EC=A0=84?= =?UTF-8?q?=EC=9D=BC=EB=8D=B0=EC=9D=B4=ED=84=B0=C2=B7TR=20ID=20=EB=93=B1?= =?UTF-8?q?=20=EB=B2=84=EA=B7=B8=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - main.py: sleep 0.05/0.1 → 1.1초 (KIS rate limit 준수) - main.py: 전일 날짜 계산 수정 (월요일→금요일), 인라인 주석 env 파싱, 장 중 재시작 즉시 루프 진입 - strategy/volatility_breakout.py: has_prev_data() 추가, 중복 수집 skip - db/repository.py, order_executor.py: UPDATE ORDER BY → 서브쿼리 수정 (SQLite 호환) - kis_client.py: get_balance TR ID VTTC8001R → VTTC8434R - test_connection.py: API 호출 간 sleep 추가 Co-Authored-By: Claude Sonnet 4.6 --- app/db/repository.py | 7 ++-- app/execution/kis_client.py | 2 +- app/execution/order_executor.py | 7 ++-- app/main.py | 50 +++++++++++++++++++++-------- app/strategy/volatility_breakout.py | 4 +++ test_connection.py | 6 ++++ test_discord.py | 20 ++++++++++++ 7 files changed, 77 insertions(+), 19 deletions(-) create mode 100644 test_discord.py diff --git a/app/db/repository.py b/app/db/repository.py index 0dbd419..2521c66 100644 --- a/app/db/repository.py +++ b/app/db/repository.py @@ -23,8 +23,11 @@ def update_trade_exit(ticker, exit_time, exit_price, exit_reason, pnl, fee): conn.execute(""" UPDATE trades SET exit_time=?, exit_price=?, exit_reason=?, pnl=?, fee=? - WHERE ticker=? AND exit_time IS NULL - ORDER BY id DESC LIMIT 1 + WHERE id = ( + SELECT id FROM trades + WHERE ticker=? AND exit_time IS NULL + ORDER BY id DESC LIMIT 1 + ) """, (exit_time, exit_price, exit_reason, pnl, fee, ticker)) diff --git a/app/execution/kis_client.py b/app/execution/kis_client.py index 2a414bc..64b5a58 100644 --- a/app/execution/kis_client.py +++ b/app/execution/kis_client.py @@ -286,7 +286,7 @@ class KISClient: async def get_balance(self) -> Dict: """주식 잔고 조회 (보유 종목 + 예수금)""" - tr_id = "VTTC8001R" if self.is_mock else "TTTC8001R" + tr_id = "VTTC8434R" if self.is_mock else "TTTC8434R" data = await self._request( method = "GET", diff --git a/app/execution/order_executor.py b/app/execution/order_executor.py index afa87e2..8f137a1 100644 --- a/app/execution/order_executor.py +++ b/app/execution/order_executor.py @@ -91,8 +91,11 @@ class OrderExecutor: conn.execute(""" UPDATE trades SET exit_time=?, exit_price=?, exit_reason=?, fee=fee+? - WHERE ticker=? AND exit_time IS NULL - ORDER BY id DESC LIMIT 1 + WHERE id = ( + SELECT id FROM trades + WHERE ticker=? AND exit_time IS NULL + ORDER BY id DESC LIMIT 1 + ) """, ( datetime.now().strftime("%H:%M:%S"), exit_price, reason, fee, ticker, diff --git a/app/main.py b/app/main.py index 4d9d5e4..25e821e 100644 --- a/app/main.py +++ b/app/main.py @@ -31,12 +31,21 @@ def load_env(): continue k, _, v = line.partition("=") k = k.strip() + v = v.strip() + # 인라인 주석 제거 (예: true # 모의투자 → true) + if " #" in v: + v = v[:v.index(" #")] v = v.strip().strip('"').strip("'") if k and v and k not in os.environ: os.environ[k] = v load_env() +# 프로젝트 루트를 sys.path에 추가 (로컬 실행 시 필요) +ROOT = Path(__file__).parent.parent +if str(ROOT) not in sys.path: + sys.path.insert(0, str(ROOT)) + # 로깅 설정 logging.basicConfig( level=getattr(logging, os.getenv("LOG_LEVEL", "INFO")), @@ -104,16 +113,13 @@ class StockBot: """종목 풀 갱신 + 전일 데이터 수집""" logger.info("유니버스 갱신 시작") try: - # 거래량 순위 상위 30종목 rank = await self.kis.get_volume_rank(top_n=MAX_UNIVERSE) tickers = [r["ticker"] for r in rank] - # AI 블랙리스트 제거 ctx = self.strategy.context blacklist = ctx.get("blacklist_tickers", []) tickers = [t for t in tickers if t not in blacklist] - # boosted 종목 상단 배치 boosted = ctx.get("boosted_tickers", []) tickers = ( [t for t in boosted if t in tickers] + @@ -123,27 +129,35 @@ class StockBot: self.universe = tickers logger.info(f"유니버스: {len(tickers)}종목") - # 전일 OHLCV 수집 (목표가 계산용) - today = datetime.now().strftime("%Y%m%d") + # 전일 날짜 계산 + from datetime import timedelta + today = datetime.now() + # 월요일이면 금요일로 + offset = 3 if today.weekday() == 0 else 1 + prev_date = (today - timedelta(days=offset)).strftime("%Y%m%d") + for ticker in self.universe: + # 이미 전일 데이터 있으면 skip + if self.strategy.has_prev_data(ticker): + continue try: ohlcv = await self.kis.get_ohlcv_daily( ticker, - start=today, - end=today, + start=prev_date, + end=prev_date, ) - if len(ohlcv) >= 2: - prev = ohlcv[-2] + if ohlcv: + prev = ohlcv[-1] self.strategy.set_prev_data( ticker, high = prev["high"], low = prev["low"], amount= prev.get("amount", - prev.get("volume",0) * prev.get("close",0)) + prev.get("volume", 0) * prev.get("close", 0)) ) except Exception as e: logger.warning(f"전일 데이터 실패 {ticker}: {e}") - await asyncio.sleep(0.1) # rate limit + await asyncio.sleep(1.1) # 초당 2.5건으로 제한 except Exception as e: logger.error(f"유니버스 갱신 실패: {e}") @@ -162,7 +176,7 @@ class StockBot: target = self.strategy.get_target(ticker) if target > 0: logger.debug(f"{ticker} 목표가: {target:,.0f}원") - await asyncio.sleep(0.05) + await asyncio.sleep(1.1) except Exception as e: logger.warning(f"시가 수집 실패 {ticker}: {e}") @@ -279,7 +293,7 @@ class StockBot: boosted=signal.get("boosted", False), ) - await asyncio.sleep(0.1) + await asyncio.sleep(1.1) except Exception as e: logger.error(f"진입 체크 오류 {ticker}: {e}") @@ -319,7 +333,7 @@ class StockBot: reason=signal["reason"], ) - await asyncio.sleep(0.05) + await asyncio.sleep(1.1) except Exception as e: logger.error(f"청산 체크 오류 {ticker}: {e}") @@ -412,6 +426,14 @@ async def run(): bot = StockBot() await bot.initialize() + now = datetime.now().strftime("%H:%M") + if "09:00" <= now <= "14:30": + logger.info("장 중 재시작 감지 → 유니버스/목표가 즉시 계산") + await bot.update_universe() + await bot.calc_targets() + await bot.trading_loop() # 바로 매매루프 진입 + return + while True: now = datetime.now().strftime("%H:%M") diff --git a/app/strategy/volatility_breakout.py b/app/strategy/volatility_breakout.py index a30c1a1..4132ead 100644 --- a/app/strategy/volatility_breakout.py +++ b/app/strategy/volatility_breakout.py @@ -89,6 +89,10 @@ class VolatilityBreakout: "low" : low, "amount": amount, } + + def has_prev_data(self, ticker: str) -> bool: + """전일 데이터 캐시 여부 확인""" + return ticker in self.prev_data def set_today_open(self, ticker: str, open_price: float): """당일 시가로 목표가 계산""" diff --git a/test_connection.py b/test_connection.py index 64c1b9d..791ab06 100644 --- a/test_connection.py +++ b/test_connection.py @@ -43,6 +43,8 @@ async def test_connection(): print(" → .env의 KIS 키를 확인해주세요") return + await asyncio.sleep(0.5) + # ── 2. 삼성전자 현재가 ── print("\n[2] 삼성전자(005930) 현재가 조회...") try: @@ -53,6 +55,8 @@ async def test_connection(): except Exception as e: print(f" ❌ 실패: {e}") + await asyncio.sleep(0.5) + # ── 3. 잔고 조회 ── print("\n[3] 계좌 잔고 조회...") try: @@ -66,6 +70,8 @@ async def test_connection(): except Exception as e: print(f" ❌ 실패: {e}") + await asyncio.sleep(0.5) + # ── 4. 거래량 순위 ── print("\n[4] 거래량 순위 상위 5종목...") try: diff --git a/test_discord.py b/test_discord.py new file mode 100644 index 0000000..a920d4c --- /dev/null +++ b/test_discord.py @@ -0,0 +1,20 @@ +import asyncio +import aiohttp +import os +from dotenv import load_dotenv + +load_dotenv() + +async def test(): + url = os.getenv("DISCORD_WEBHOOK_URL") + if not url: + print("❌ DISCORD_WEBHOOK_URL 이 .env에 없습니다") + return + async with aiohttp.ClientSession() as s: + r = await s.post(url, json={"content": "[테스트] KIS 연결 완료 ✅ 단타봇 준비 중"}) + if r.status in (200, 204): + print("✅ Discord 전송 완료 - 채널 확인하세요") + else: + print(f"❌ 전송 실패: {r.status}") + +asyncio.run(test())