[2026-05-15] rate limit·전일데이터·TR ID 등 버그 수정
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -23,8 +23,11 @@ def update_trade_exit(ticker, exit_time, exit_price, exit_reason, pnl, fee):
|
|||||||
conn.execute("""
|
conn.execute("""
|
||||||
UPDATE trades SET exit_time=?, exit_price=?,
|
UPDATE trades SET exit_time=?, exit_price=?,
|
||||||
exit_reason=?, pnl=?, fee=?
|
exit_reason=?, pnl=?, fee=?
|
||||||
|
WHERE id = (
|
||||||
|
SELECT id FROM trades
|
||||||
WHERE ticker=? AND exit_time IS NULL
|
WHERE ticker=? AND exit_time IS NULL
|
||||||
ORDER BY id DESC LIMIT 1
|
ORDER BY id DESC LIMIT 1
|
||||||
|
)
|
||||||
""", (exit_time, exit_price, exit_reason, pnl, fee, ticker))
|
""", (exit_time, exit_price, exit_reason, pnl, fee, ticker))
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -286,7 +286,7 @@ class KISClient:
|
|||||||
|
|
||||||
async def get_balance(self) -> Dict:
|
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(
|
data = await self._request(
|
||||||
method = "GET",
|
method = "GET",
|
||||||
|
|||||||
@@ -91,8 +91,11 @@ class OrderExecutor:
|
|||||||
conn.execute("""
|
conn.execute("""
|
||||||
UPDATE trades
|
UPDATE trades
|
||||||
SET exit_time=?, exit_price=?, exit_reason=?, fee=fee+?
|
SET exit_time=?, exit_price=?, exit_reason=?, fee=fee+?
|
||||||
|
WHERE id = (
|
||||||
|
SELECT id FROM trades
|
||||||
WHERE ticker=? AND exit_time IS NULL
|
WHERE ticker=? AND exit_time IS NULL
|
||||||
ORDER BY id DESC LIMIT 1
|
ORDER BY id DESC LIMIT 1
|
||||||
|
)
|
||||||
""", (
|
""", (
|
||||||
datetime.now().strftime("%H:%M:%S"),
|
datetime.now().strftime("%H:%M:%S"),
|
||||||
exit_price, reason, fee, ticker,
|
exit_price, reason, fee, ticker,
|
||||||
|
|||||||
+35
-13
@@ -31,12 +31,21 @@ def load_env():
|
|||||||
continue
|
continue
|
||||||
k, _, v = line.partition("=")
|
k, _, v = line.partition("=")
|
||||||
k = k.strip()
|
k = k.strip()
|
||||||
|
v = v.strip()
|
||||||
|
# 인라인 주석 제거 (예: true # 모의투자 → true)
|
||||||
|
if " #" in v:
|
||||||
|
v = v[:v.index(" #")]
|
||||||
v = v.strip().strip('"').strip("'")
|
v = v.strip().strip('"').strip("'")
|
||||||
if k and v and k not in os.environ:
|
if k and v and k not in os.environ:
|
||||||
os.environ[k] = v
|
os.environ[k] = v
|
||||||
|
|
||||||
load_env()
|
load_env()
|
||||||
|
|
||||||
|
# 프로젝트 루트를 sys.path에 추가 (로컬 실행 시 필요)
|
||||||
|
ROOT = Path(__file__).parent.parent
|
||||||
|
if str(ROOT) not in sys.path:
|
||||||
|
sys.path.insert(0, str(ROOT))
|
||||||
|
|
||||||
# 로깅 설정
|
# 로깅 설정
|
||||||
logging.basicConfig(
|
logging.basicConfig(
|
||||||
level=getattr(logging, os.getenv("LOG_LEVEL", "INFO")),
|
level=getattr(logging, os.getenv("LOG_LEVEL", "INFO")),
|
||||||
@@ -104,16 +113,13 @@ class StockBot:
|
|||||||
"""종목 풀 갱신 + 전일 데이터 수집"""
|
"""종목 풀 갱신 + 전일 데이터 수집"""
|
||||||
logger.info("유니버스 갱신 시작")
|
logger.info("유니버스 갱신 시작")
|
||||||
try:
|
try:
|
||||||
# 거래량 순위 상위 30종목
|
|
||||||
rank = await self.kis.get_volume_rank(top_n=MAX_UNIVERSE)
|
rank = await self.kis.get_volume_rank(top_n=MAX_UNIVERSE)
|
||||||
tickers = [r["ticker"] for r in rank]
|
tickers = [r["ticker"] for r in rank]
|
||||||
|
|
||||||
# AI 블랙리스트 제거
|
|
||||||
ctx = self.strategy.context
|
ctx = self.strategy.context
|
||||||
blacklist = ctx.get("blacklist_tickers", [])
|
blacklist = ctx.get("blacklist_tickers", [])
|
||||||
tickers = [t for t in tickers if t not in blacklist]
|
tickers = [t for t in tickers if t not in blacklist]
|
||||||
|
|
||||||
# boosted 종목 상단 배치
|
|
||||||
boosted = ctx.get("boosted_tickers", [])
|
boosted = ctx.get("boosted_tickers", [])
|
||||||
tickers = (
|
tickers = (
|
||||||
[t for t in boosted if t in tickers] +
|
[t for t in boosted if t in tickers] +
|
||||||
@@ -123,17 +129,25 @@ class StockBot:
|
|||||||
self.universe = tickers
|
self.universe = tickers
|
||||||
logger.info(f"유니버스: {len(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:
|
for ticker in self.universe:
|
||||||
|
# 이미 전일 데이터 있으면 skip
|
||||||
|
if self.strategy.has_prev_data(ticker):
|
||||||
|
continue
|
||||||
try:
|
try:
|
||||||
ohlcv = await self.kis.get_ohlcv_daily(
|
ohlcv = await self.kis.get_ohlcv_daily(
|
||||||
ticker,
|
ticker,
|
||||||
start=today,
|
start=prev_date,
|
||||||
end=today,
|
end=prev_date,
|
||||||
)
|
)
|
||||||
if len(ohlcv) >= 2:
|
if ohlcv:
|
||||||
prev = ohlcv[-2]
|
prev = ohlcv[-1]
|
||||||
self.strategy.set_prev_data(
|
self.strategy.set_prev_data(
|
||||||
ticker,
|
ticker,
|
||||||
high = prev["high"],
|
high = prev["high"],
|
||||||
@@ -143,7 +157,7 @@ class StockBot:
|
|||||||
)
|
)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"전일 데이터 실패 {ticker}: {e}")
|
logger.warning(f"전일 데이터 실패 {ticker}: {e}")
|
||||||
await asyncio.sleep(0.1) # rate limit
|
await asyncio.sleep(1.1) # 초당 2.5건으로 제한
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"유니버스 갱신 실패: {e}")
|
logger.error(f"유니버스 갱신 실패: {e}")
|
||||||
@@ -162,7 +176,7 @@ class StockBot:
|
|||||||
target = self.strategy.get_target(ticker)
|
target = self.strategy.get_target(ticker)
|
||||||
if target > 0:
|
if target > 0:
|
||||||
logger.debug(f"{ticker} 목표가: {target:,.0f}원")
|
logger.debug(f"{ticker} 목표가: {target:,.0f}원")
|
||||||
await asyncio.sleep(0.05)
|
await asyncio.sleep(1.1)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.warning(f"시가 수집 실패 {ticker}: {e}")
|
logger.warning(f"시가 수집 실패 {ticker}: {e}")
|
||||||
|
|
||||||
@@ -279,7 +293,7 @@ class StockBot:
|
|||||||
boosted=signal.get("boosted", False),
|
boosted=signal.get("boosted", False),
|
||||||
)
|
)
|
||||||
|
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(1.1)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"진입 체크 오류 {ticker}: {e}")
|
logger.error(f"진입 체크 오류 {ticker}: {e}")
|
||||||
@@ -319,7 +333,7 @@ class StockBot:
|
|||||||
reason=signal["reason"],
|
reason=signal["reason"],
|
||||||
)
|
)
|
||||||
|
|
||||||
await asyncio.sleep(0.05)
|
await asyncio.sleep(1.1)
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
logger.error(f"청산 체크 오류 {ticker}: {e}")
|
logger.error(f"청산 체크 오류 {ticker}: {e}")
|
||||||
@@ -412,6 +426,14 @@ async def run():
|
|||||||
bot = StockBot()
|
bot = StockBot()
|
||||||
await bot.initialize()
|
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:
|
while True:
|
||||||
now = datetime.now().strftime("%H:%M")
|
now = datetime.now().strftime("%H:%M")
|
||||||
|
|
||||||
|
|||||||
@@ -90,6 +90,10 @@ class VolatilityBreakout:
|
|||||||
"amount": amount,
|
"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):
|
def set_today_open(self, ticker: str, open_price: float):
|
||||||
"""당일 시가로 목표가 계산"""
|
"""당일 시가로 목표가 계산"""
|
||||||
prev = self.prev_data.get(ticker)
|
prev = self.prev_data.get(ticker)
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ async def test_connection():
|
|||||||
print(" → .env의 KIS 키를 확인해주세요")
|
print(" → .env의 KIS 키를 확인해주세요")
|
||||||
return
|
return
|
||||||
|
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
# ── 2. 삼성전자 현재가 ──
|
# ── 2. 삼성전자 현재가 ──
|
||||||
print("\n[2] 삼성전자(005930) 현재가 조회...")
|
print("\n[2] 삼성전자(005930) 현재가 조회...")
|
||||||
try:
|
try:
|
||||||
@@ -53,6 +55,8 @@ async def test_connection():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ❌ 실패: {e}")
|
print(f" ❌ 실패: {e}")
|
||||||
|
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
# ── 3. 잔고 조회 ──
|
# ── 3. 잔고 조회 ──
|
||||||
print("\n[3] 계좌 잔고 조회...")
|
print("\n[3] 계좌 잔고 조회...")
|
||||||
try:
|
try:
|
||||||
@@ -66,6 +70,8 @@ async def test_connection():
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
print(f" ❌ 실패: {e}")
|
print(f" ❌ 실패: {e}")
|
||||||
|
|
||||||
|
await asyncio.sleep(0.5)
|
||||||
|
|
||||||
# ── 4. 거래량 순위 ──
|
# ── 4. 거래량 순위 ──
|
||||||
print("\n[4] 거래량 순위 상위 5종목...")
|
print("\n[4] 거래량 순위 상위 5종목...")
|
||||||
try:
|
try:
|
||||||
|
|||||||
@@ -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())
|
||||||
Reference in New Issue
Block a user