fix: 자동매매 미작동 근본 원인 4개 수정
## 버그 수정 요약
### 1. 거래량 필터 오작동 (market_analyzer.py)
- acml_vol(당일 누적량, 장초 2~10만주)을 100만주 기준으로 필터
→ 전 종목이 필터에 걸려 추천 종목 0개
- 수정: 20일 평균 거래량(avg_vol_20) 기준 10만주 미만 필터로 교체
### 2. min_volume_ratio 기본값 1.0 미설정 (recommendations.py)
- get_rule_based_recommendations_realtime()에서 min_volume_ratio 누락
→ 기본값 1.0 적용, 장중 acml_vol/avg_vol_20≈0.1 → 전 종목 None 반환
- 수정: min_volume_ratio=0 명시 설정
### 3. KRX 호가단위 오류 (korea_investment_api.py)
- 계산된 주문가격이 호가단위(50/100/500/1000원 등) 불일치
→ 모의투자 "호가단위 오류"로 모든 주문 실패
- 수정: _round_to_krx_tick() 헬퍼 추가, 주문 전 올림 처리
### 4. MCP 서버 AI_PROVIDER 미전달 (docker-compose.dev.yml, mcp-server/config.py)
- mcp-server 서비스에 AI_PROVIDER 환경변수 누락 → 항상 Gemini 사용
- Gemini 무료 쿼터 소진 → /analyze 500 에러 → fail-open
- 수정: AI_PROVIDER=${AI_PROVIDER:-claude} 추가
- 수정: load_dotenv(override=True)로 빈 env var 덮어쓰기 허용
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
23
.claude/launch.json
Normal file
23
.claude/launch.json
Normal file
@@ -0,0 +1,23 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "frontend",
|
||||
"runtimeExecutable": "npm",
|
||||
"runtimeArgs": ["run", "dev"],
|
||||
"port": 3000
|
||||
},
|
||||
{
|
||||
"name": "backend",
|
||||
"runtimeExecutable": "uvicorn",
|
||||
"runtimeArgs": ["app.main:app", "--host", "0.0.0.0", "--port", "8000", "--reload"],
|
||||
"port": 8000
|
||||
},
|
||||
{
|
||||
"name": "docker-dev",
|
||||
"runtimeExecutable": "docker-compose",
|
||||
"runtimeArgs": ["-f", "docker-compose.dev.yml", "up"],
|
||||
"port": 8090
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -7,7 +7,7 @@
|
||||
## 1. 프로젝트 개요
|
||||
|
||||
### 1.1 기술 스택
|
||||
- **백엔드**: Python 3.11, FastAPI, SQLAlchemy (Async), PostgreSQL, Redis
|
||||
- **백엔드**: Python 3.11, FastAPI, SQLAlchemy (Async), PostgreSQL, Redis, Rabbitmq
|
||||
- **프론트엔드**: React, TypeScript, Vite, Recharts
|
||||
- **인프라**: Docker Compose, Nginx
|
||||
- **AI 계층**: Claude 3.5 Sonnet / Gemini 2.0 Flash (`.env`에서 전환)
|
||||
|
||||
@@ -497,6 +497,7 @@ async def get_rule_based_recommendations_realtime():
|
||||
use_bollinger=True,
|
||||
use_stochastic=True,
|
||||
for_batch=True,
|
||||
min_volume_ratio=0, # intraday acml_vol << avg_vol_20 → 장중엔 항상 0으로 고정
|
||||
)
|
||||
|
||||
batch_size = 1
|
||||
|
||||
@@ -149,6 +149,26 @@ async def _cache_set(key: str, ttl: int, data: Any) -> None:
|
||||
pass
|
||||
|
||||
|
||||
def _round_to_krx_tick(price: int) -> int:
|
||||
"""KRX 호가단위에 맞춰 가격을 올림(ceil) 처리. 모의투자 호가단위 오류 방지."""
|
||||
if price < 1_000:
|
||||
tick = 1
|
||||
elif price < 5_000:
|
||||
tick = 5
|
||||
elif price < 10_000:
|
||||
tick = 10
|
||||
elif price < 50_000:
|
||||
tick = 50
|
||||
elif price < 100_000:
|
||||
tick = 100
|
||||
elif price < 500_000:
|
||||
tick = 500
|
||||
else:
|
||||
tick = 1_000
|
||||
remainder = price % tick
|
||||
return price if remainder == 0 else price + (tick - remainder)
|
||||
|
||||
|
||||
class KoreaInvestmentAPI:
|
||||
"""한국투자증권 API 클라이언트 (안전한 재시도 및 쓰로틀링 적용)"""
|
||||
|
||||
@@ -1012,7 +1032,7 @@ class KoreaInvestmentAPI:
|
||||
tr_id = "VTTC0801U" if is_virtual else "TTTC0801U"
|
||||
|
||||
order_dvsn = "01" if price is None else "00"
|
||||
order_price = "0" if price is None else str(int(price))
|
||||
order_price = "0" if price is None else str(_round_to_krx_tick(int(price)))
|
||||
|
||||
logger.info(f"📤 주문 요청: {order_type} {stock_code} {quantity}주 @{order_price}원 (tr_id={tr_id}, is_virtual={is_virtual})")
|
||||
|
||||
|
||||
@@ -219,11 +219,6 @@ def _analyze_chart_items(
|
||||
logger.info("필터[저가] %s: %.0f원 < %d원", stock_code, current_price, settings.MIN_PRICE_KRW)
|
||||
return None
|
||||
|
||||
# 전일 거래량 필터: 100만주 미만 종목 제외 (유동성 부족)
|
||||
if volume < 1_000_000:
|
||||
logger.info("필터[거래량] %s: %d주 < 100만주", stock_code, volume)
|
||||
return None
|
||||
|
||||
if len(chart_data) >= 2:
|
||||
prev_price = float(chart_data[-2].get("stck_clpr", current_price))
|
||||
change_rate = ((current_price - prev_price) / prev_price) * 100 if prev_price > 0 else 0
|
||||
@@ -240,6 +235,14 @@ def _analyze_chart_items(
|
||||
avg_vol_20 = sum(volumes[-20:]) / 20 if len(volumes) >= 20 else 0
|
||||
volume_ratio = (volume / avg_vol_20) if avg_vol_20 > 0 else 0
|
||||
|
||||
# 유동성 필터: 20일 평균 거래량 기준으로 판단 (intraday 누적량 대신 사용)
|
||||
# chart_data[-1].acml_vol은 장 초반에 당일 누적량이므로 실제 유동성을 반영 못함
|
||||
# avg_vol_20 기준 10만주 미만 = 진입하기 어려운 비유동 종목
|
||||
liquidity_vol = avg_vol_20 if avg_vol_20 > 0 else volume
|
||||
if liquidity_vol < 100_000:
|
||||
logger.info("필터[거래량] %s: 20일평균 %.0f주 < 10만주", stock_code, liquidity_vol)
|
||||
return None
|
||||
|
||||
if not criteria.for_batch and avg_vol_20 < 50000 and current_price < 50000:
|
||||
return None
|
||||
|
||||
|
||||
@@ -82,6 +82,8 @@ services:
|
||||
- "8001:8001"
|
||||
environment:
|
||||
- ANTHROPIC_API_KEY=${ANTHROPIC_API_KEY}
|
||||
- GEMINI_API_KEY=${GEMINI_API_KEY}
|
||||
- AI_PROVIDER=${AI_PROVIDER:-claude}
|
||||
- LOG_LEVEL=debug
|
||||
- DATABASE_URL=postgresql://${POSTGRES_USER:-stock}:${POSTGRES_PASSWORD:-stock1234}@postgres:5432/${POSTGRES_DB:-stock}
|
||||
command: uvicorn main:app --host 0.0.0.0 --port 8001 --reload
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import os
|
||||
from dotenv import load_dotenv
|
||||
|
||||
load_dotenv()
|
||||
load_dotenv(override=True) # 컨테이너 빈 env var보다 .env 파일 우선
|
||||
|
||||
|
||||
class Settings:
|
||||
|
||||
Reference in New Issue
Block a user