수정본
This commit is contained in:
169
mcp-server/trade_monitor.py
Normal file
169
mcp-server/trade_monitor.py
Normal file
@@ -0,0 +1,169 @@
|
||||
import asyncio
|
||||
import json
|
||||
import os
|
||||
from datetime import datetime
|
||||
import logging
|
||||
from decimal import Decimal
|
||||
|
||||
# backend 경로를 sys.path에 추가하여 서비스 모듈 임포트
|
||||
import sys
|
||||
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), '../backend')))
|
||||
|
||||
from sqlalchemy import select, update
|
||||
from sqlalchemy.ext.asyncio import create_async_engine, AsyncSession
|
||||
from sqlalchemy.orm import sessionmaker
|
||||
|
||||
from app.core.config import settings
|
||||
from app.models.models import ActiveTrade
|
||||
from app.services.korea_investment_api import korea_investment_api
|
||||
|
||||
# 로깅 설정
|
||||
logging.basicConfig(level=logging.INFO, format='%(asctime)s - %(levelname)s - %(message)s')
|
||||
|
||||
# --- DB 설정 ---
|
||||
engine = create_async_engine(settings.DATABASE_URL, echo=False)
|
||||
AsyncSessionLocal = sessionmaker(engine, class_=AsyncSession, expire_on_commit=False)
|
||||
# ----------------
|
||||
|
||||
ACTIVE_TRADES_FILE = os.path.join(os.path.dirname(__file__), 'active_trades.json')
|
||||
MONITORING_INTERVAL_SECONDS = 60 # 60초마다 확인
|
||||
|
||||
async def sync_json_from_db(db: AsyncSession):
|
||||
"""DB의 active_trades 목록을 JSON 파일에 동기화(덮어쓰기)합니다."""
|
||||
try:
|
||||
result = await db.execute(select(ActiveTrade).where(ActiveTrade.status == 'active'))
|
||||
active_trades_from_db = result.scalars().all()
|
||||
|
||||
json_trades = [
|
||||
{
|
||||
'stock_code': trade.stock_code,
|
||||
'stock_name': trade.stock_name,
|
||||
'quantity': trade.quantity,
|
||||
'entry_price': float(trade.entry_price),
|
||||
'stop_loss': float(trade.stop_loss),
|
||||
'take_profit': float(trade.take_profit),
|
||||
'order_id': trade.order_id,
|
||||
'reason': trade.entry_reason,
|
||||
'timestamp': trade.created_at.isoformat()
|
||||
}
|
||||
for trade in active_trades_from_db
|
||||
]
|
||||
|
||||
with open(ACTIVE_TRADES_FILE, 'w', encoding='utf-8') as f:
|
||||
json.dump(json_trades, f, indent=4, ensure_ascii=False)
|
||||
logging.info(f"💾 [JSON] DB와 동기화 완료. 활성 거래 {len(json_trades)}건.")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"❌ [JSON] DB 동기화 실패: {e}")
|
||||
|
||||
|
||||
async def check_and_execute_trades(db: AsyncSession):
|
||||
"""
|
||||
DB에서 활성화된 거래를 확인하고 손절/익절 조건에 따라 매도 주문을 실행합니다.
|
||||
"""
|
||||
logging.info(" 거래 모니터링 시작 (DB 기반)...")
|
||||
|
||||
result = await db.execute(select(ActiveTrade).where(ActiveTrade.status == 'active'))
|
||||
active_trades = result.scalars().all()
|
||||
|
||||
if not active_trades:
|
||||
logging.info("현재 활성화된 거래가 없습니다.")
|
||||
return
|
||||
|
||||
for trade in active_trades:
|
||||
stock_code = trade.stock_code
|
||||
stop_loss = float(trade.stop_loss)
|
||||
take_profit = float(trade.take_profit)
|
||||
quantity = trade.quantity
|
||||
entry_price = float(trade.entry_price)
|
||||
|
||||
should_sell = False
|
||||
sell_reason = ""
|
||||
|
||||
try:
|
||||
# 현재가 조회
|
||||
price_info, _ = await korea_investment_api._make_request(
|
||||
"GET",
|
||||
"/uapi/domestic-stock/v1/quotations/inquire-price",
|
||||
"FHKST01010100",
|
||||
params={"FID_COND_MRKT_DIV_CODE": "J", "FID_INPUT_ISCD": stock_code}
|
||||
)
|
||||
|
||||
if price_info and price_info.get('rt_cd') == '0':
|
||||
current_price_str = price_info.get('output', {}).get('stck_prpr')
|
||||
if not current_price_str:
|
||||
logging.error(f"[{stock_code}] 현재가 정보를 응답에서 찾을 수 없습니다: {price_info}")
|
||||
continue
|
||||
|
||||
current_price = int(current_price_str)
|
||||
logging.info(f"[{stock_code}] 현재가: {current_price} (매수가: {entry_price}, 손절가: {stop_loss}, 목표가: {take_profit})")
|
||||
|
||||
# 손절 조건 확인
|
||||
if stop_loss and current_price <= stop_loss:
|
||||
should_sell = True
|
||||
sell_reason = f"손절가 도달 (현재가: {current_price} <= 손절가: {stop_loss})"
|
||||
|
||||
# 익절 조건 확인
|
||||
elif take_profit and current_price >= take_profit:
|
||||
should_sell = True
|
||||
sell_reason = f"목표가 도달 (현재가: {current_price} >= 목표가: {take_profit})"
|
||||
|
||||
else:
|
||||
logging.error(f"[{stock_code}] 현재가 조회 실패: {price_info.get('msg1', '알 수 없는 오류')}")
|
||||
continue
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"[{stock_code}] 처리 중 예외 발생: {e}")
|
||||
continue
|
||||
|
||||
if should_sell:
|
||||
logging.warning(f"🚨 [{stock_code}] 매도 조건 충족: {sell_reason}")
|
||||
try:
|
||||
# 시장가 매도 주문 (price=None)
|
||||
sell_result = await korea_investment_api.place_order(
|
||||
stock_code=stock_code,
|
||||
order_type='SELL',
|
||||
quantity=quantity,
|
||||
price=None
|
||||
)
|
||||
logging.info(f"[{stock_code}] 매도 주문 결과: {sell_result}")
|
||||
|
||||
if sell_result and sell_result.get('rt_cd') == '0':
|
||||
logging.info(f"✅ [{stock_code}] 매도 주문 성공. DB 상태를 'closed'로 변경합니다.")
|
||||
trade.status = 'closed'
|
||||
trade.updated_at = datetime.utcnow()
|
||||
db.add(trade)
|
||||
await db.commit()
|
||||
else:
|
||||
logging.error(f"❌ [{stock_code}] 매도 주문 실패: {sell_result.get('msg1', '알 수 없는 오류')}")
|
||||
|
||||
except Exception as e:
|
||||
logging.error(f"[{stock_code}] 매도 주문 중 예외 발생: {e}")
|
||||
await db.rollback()
|
||||
|
||||
async def main():
|
||||
"""메인 실행 루프"""
|
||||
logging.info("🚀 자동 매도 모니터링 시스템을 시작합니다. (DB 모드)")
|
||||
logging.info(f"점검 주기: {MONITORING_INTERVAL_SECONDS}초")
|
||||
|
||||
# 시작 시 DB와 JSON 동기화
|
||||
async with AsyncSessionLocal() as db:
|
||||
await sync_json_from_db(db)
|
||||
|
||||
while True:
|
||||
try:
|
||||
async with AsyncSessionLocal() as db:
|
||||
await check_and_execute_trades(db)
|
||||
# 매 사이클 종료 후 DB와 JSON 동기화
|
||||
await sync_json_from_db(db)
|
||||
except Exception as e:
|
||||
logging.critical(f"모니터링 루프에서 치명적 오류 발생: {e}", exc_info=True)
|
||||
|
||||
logging.info(f"{MONITORING_INTERVAL_SECONDS}초 후 다음 점검을 시작합니다.\n")
|
||||
await asyncio.sleep(MONITORING_INTERVAL_SECONDS)
|
||||
|
||||
if __name__ == "__main__":
|
||||
try:
|
||||
asyncio.run(main())
|
||||
except KeyboardInterrupt:
|
||||
logging.info("사용자에 의해 프로그램이 종료되었습니다.")
|
||||
Reference in New Issue
Block a user