왜: 체결이 없어도 마지막가는 유지인데, TTL로 None 만들고 매도/EOD를 건너뛰어 8/5 돌파·금요일 leftover가 남음. 호가필터 TTL 구멍과 같은 병. 넣을 파일 신규: kis_trader/engine/live_sell_price.py kis_trader/strategies/base.py (_ws_last_quote, _resolve_sell_price) 전략: momentum.py scalping.py tail_catch.py breakout.py range_break.py dart_strategy.py updow_strategy.py updown_feed.py us_momentum.py WS: ws_manager.py kis_ws.py kiwoom_ws.py ls_ws.py kis_ws_overseas.py kis_trader/web/live_config_schema.py (WS_PRICE_MAX_AGE_SEC 기본 0) database.py (키 주석 + legacy/ sys.path) kis_trader/execution/order_manager.py (잔고 있는데 40240000 ghost_purge 금지 — 같은 EOD 사고) EOD가 min_hold에 안 막히게 손본 momentum_hts_logic.py / scalping_engine.py / tail_engine.py (이 대화에서 손본 부분만 확인 후) 문서: docs/like_mcp.md/db_erd.md code_architecture.md (가격 TTL 문구) 메시지 초안 fix: 매수·매도 현재가를 TTL로 버리지 않음 (마지막 RAM) 횡보·체결 공백을 죽은 캐시로 오인해 None 처리하면 손절·EOD가 스킵된다. 호가필터와 같이 나이는 무시하고 마지막 체결가를 유지한다. EOD는 매수가 폴백. 영향: 실매 O / 백테·옵투나 봉 경로 거의 무관 (엔진 식 변경 아님) 커밋 2 — 루트 정리 (remove/ vs legacy/) 왜: 루트 단독봇·테스트는 지울 보관함으로. 웹·알람이 아직 쓰는 모듈은 remove에 두면 나중에 폴더째 삭제 때 깨짐. 넣을 파일 이동: 미사용 → remove/legacy_root/ (래퍼, ETF/키움 옛봇, 테스트, kiwoom_rest_api 등) 이동: 사용 중 → legacy/ (holding_bot kis_holding_ver1 news_analyzer kis_long_ver1/2) 신규: kis_trader/utils/legacy_root.py legacy/README.md remove/README.md import 경로: backtest_web.py mm_butler.py mm_remote.py updow_holding_cfg.py dbband_stock_cfg.py param_search_updow*.py dbband_param_search.py param_search_apply_snapshot.py verify_three_paths.py docs/like_mcp.md/code_architecture.md 수동 노트 메시지 초안 chore: 미사용 루트는 remove/, 웹·알람 구모듈은 legacy/ remove는 나중에 통째 삭제 예정. holding_bot·news_analyzer·kis_long은 ensure_legacy_root로 legacy/만 본다. 빼기: scratch/set_ws_price_max_age_zero.py (일회성)
264 lines
7.0 KiB
Python
Executable File
264 lines
7.0 KiB
Python
Executable File
#!/usr/bin/env python3
|
||
"""
|
||
자동 AI 분석 보고서
|
||
- 매일 13:00에 최근 10건 거래 분석
|
||
- AI가 문제점 진단 및 .env 수정 권장
|
||
"""
|
||
|
||
import os
|
||
import sys
|
||
import time
|
||
import json
|
||
import sqlite3
|
||
import datetime
|
||
import warnings
|
||
import requests
|
||
from dotenv import load_dotenv
|
||
|
||
# .env 로드
|
||
current_dir = os.path.dirname(os.path.abspath(__file__))
|
||
env_file = os.path.join(current_dir, '.env')
|
||
load_dotenv(env_file)
|
||
|
||
# Gemini deprecated 경고 억제
|
||
warnings.filterwarnings("ignore", message=".*google.generativeai.*")
|
||
|
||
# Gemini API
|
||
try:
|
||
import google.generativeai as genai
|
||
GEMINI_API_KEY = os.getenv("GEMINI_API_KEY")
|
||
if GEMINI_API_KEY:
|
||
genai.configure(api_key=GEMINI_API_KEY)
|
||
model = genai.GenerativeModel('gemini-2.5-flash')
|
||
else:
|
||
model = None
|
||
except Exception as e:
|
||
print(f"❌ Gemini 초기화 실패: {e}")
|
||
model = None
|
||
|
||
# Mattermost (Ver2와 동일: mm_config.json + MM_BOT_TOKEN_)
|
||
MM_SERVER_URL = os.getenv("MM_SERVER_URL", "https://mattermost.hoonfam.org")
|
||
MM_TOKEN = os.environ.get("MM_BOT_TOKEN_", os.getenv("MATTERMOST_TOKEN", "")).strip()
|
||
MM_CONFIG_FILE = os.path.join(current_dir, "mm_config.json")
|
||
MM_CHANNEL = os.getenv("MATTERMOST_CHANNEL", "stock")
|
||
|
||
def _load_mm_channels():
|
||
try:
|
||
if os.path.exists(MM_CONFIG_FILE):
|
||
with open(MM_CONFIG_FILE, 'r', encoding='utf-8') as f:
|
||
return json.load(f).get("channels", {})
|
||
except Exception:
|
||
pass
|
||
return {}
|
||
|
||
def send_mm(message):
|
||
"""Mattermost 전송"""
|
||
channels = _load_mm_channels()
|
||
channel_id = channels.get(MM_CHANNEL)
|
||
if not channel_id or not MM_TOKEN:
|
||
print("❌ MM 설정 없음 (mm_config.json 또는 MM_BOT_TOKEN_)")
|
||
return False
|
||
api_url = f"{MM_SERVER_URL.rstrip('/')}/api/v4/posts"
|
||
headers = {"Authorization": f"Bearer {MM_TOKEN}", "Content-Type": "application/json"}
|
||
payload = {"channel_id": channel_id, "message": message}
|
||
try:
|
||
r = requests.post(api_url, headers=headers, json=payload, timeout=5)
|
||
r.raise_for_status()
|
||
return True
|
||
except Exception as e:
|
||
print(f"❌ MM 전송 에러: {e}")
|
||
return False
|
||
|
||
def get_recent_trades(limit=10):
|
||
"""최근 거래 N건 조회"""
|
||
db_path = os.path.join(os.path.dirname(__file__), 'quant_bot.db')
|
||
conn = sqlite3.connect(db_path)
|
||
|
||
cursor = conn.execute(f"""
|
||
SELECT
|
||
code, name,
|
||
buy_price, sell_price, qty,
|
||
profit_rate, realized_pnl,
|
||
strategy, sell_reason,
|
||
buy_date, sell_date, hold_minutes
|
||
FROM trade_history
|
||
ORDER BY id DESC
|
||
LIMIT {limit}
|
||
""")
|
||
|
||
trades = []
|
||
for row in cursor.fetchall():
|
||
trades.append({
|
||
'code': row[0],
|
||
'name': row[1],
|
||
'buy_price': row[2],
|
||
'sell_price': row[3],
|
||
'qty': row[4],
|
||
'profit_rate': row[5],
|
||
'realized_pnl': row[6],
|
||
'strategy': row[7],
|
||
'sell_reason': row[8],
|
||
'buy_date': row[9],
|
||
'sell_date': row[10],
|
||
'hold_minutes': row[11]
|
||
})
|
||
|
||
conn.close()
|
||
return trades
|
||
|
||
def get_trade_summary(trades):
|
||
"""거래 통계 요약"""
|
||
if not trades:
|
||
return "거래 없음"
|
||
|
||
total = len(trades)
|
||
wins = sum(1 for t in trades if t['profit_rate'] > 0)
|
||
losses = total - wins
|
||
win_rate = wins / total * 100 if total > 0 else 0
|
||
|
||
avg_profit = sum(t['profit_rate'] for t in trades) / total
|
||
total_pnl = sum(t['realized_pnl'] for t in trades)
|
||
|
||
avg_hold = sum(t['hold_minutes'] for t in trades) / total
|
||
|
||
return f"""
|
||
📊 최근 {total}건 거래 통계
|
||
- 승률: {win_rate:.1f}% ({wins}승 {losses}패)
|
||
- 평균 수익률: {avg_profit:.2f}%
|
||
- 총 손익: {total_pnl:,.0f}원
|
||
- 평균 보유: {avg_hold:.0f}분
|
||
"""
|
||
|
||
def analyze_with_ai(trades):
|
||
"""AI로 거래 분석 및 권장사항"""
|
||
if not model:
|
||
return "❌ AI 분석 불가 (Gemini API 키 없음)"
|
||
|
||
trades_text = ""
|
||
for i, t in enumerate(trades, 1):
|
||
trades_text += f"""
|
||
[거래 {i}] {t['name']} ({t['strategy']})
|
||
- 매수: {t['buy_price']:,.0f}원 × {t['qty']}주
|
||
- 매도: {t['sell_price']:,.0f}원
|
||
- 손익: {t['profit_rate']:+.2f}% ({t['realized_pnl']:,.0f}원)
|
||
- 보유: {t['hold_minutes']}분
|
||
- 사유: {t['sell_reason']}
|
||
"""
|
||
|
||
prompt = f"""당신은 퀀트 트레이딩 전문가입니다.
|
||
|
||
다음은 최근 {len(trades)}건의 거래 내역입니다:
|
||
|
||
{trades_text}
|
||
|
||
{get_trade_summary(trades)}
|
||
|
||
**당신의 임무:**
|
||
1. 문제점 3가지 진단 (구체적으로)
|
||
2. .env 수정 권장사항 (변수명=값 형식)
|
||
3. 예상 효과
|
||
|
||
**출력 형식:**
|
||
## 🔍 문제점
|
||
1. [구체적 문제 1]
|
||
2. [구체적 문제 2]
|
||
3. [구체적 문제 3]
|
||
|
||
## 💡 권장 수정사항
|
||
```
|
||
RSI_OVERHEAT_THRESHOLD=XX
|
||
HIGH_PRICE_CHASE_THRESHOLD=X.XX
|
||
STOP_LOSS_PCT=-X.XX
|
||
...
|
||
```
|
||
|
||
## 📈 예상 효과
|
||
- [효과 1]
|
||
- [효과 2]
|
||
|
||
**간결하고 명확하게 답변하세요.**
|
||
"""
|
||
|
||
try:
|
||
response = model.generate_content(prompt)
|
||
return response.text
|
||
except Exception as e:
|
||
return f"❌ AI 분석 실패: {e}"
|
||
|
||
def send_daily_report():
|
||
"""13시 정기 보고서"""
|
||
print(f"\n{'='*80}")
|
||
print(f"🤖 자동 AI 분석 시작: {datetime.datetime.now()}")
|
||
print(f"{'='*80}\n")
|
||
|
||
trades = get_recent_trades(10)
|
||
|
||
if not trades:
|
||
print("❌ 거래 내역 없음")
|
||
return
|
||
|
||
print("🧠 AI 분석 중...")
|
||
analysis = analyze_with_ai(trades)
|
||
|
||
summary = get_trade_summary(trades)
|
||
|
||
message = f"""🤖 **[13시 AI 자동 분석]**
|
||
|
||
{summary}
|
||
|
||
{analysis}
|
||
|
||
---
|
||
💬 명령어 사용법:
|
||
- `!env 보기` - 현재 설정 확인
|
||
- `!env RSI_OVERHEAT_THRESHOLD=75` - 설정 변경
|
||
- `!ai 왜 승률이 낮아?` - AI 질문
|
||
"""
|
||
|
||
if send_mm(message):
|
||
print("✅ 보고서 전송 완료")
|
||
else:
|
||
print("❌ 전송 실패")
|
||
|
||
def main():
|
||
"""메인 루프"""
|
||
print("🤖 자동 AI 분석 보고서 시작")
|
||
print(f"- 보고 시간: 매일 13:00")
|
||
print(f"- 분석 대상: 최근 10건 거래")
|
||
print(f"- AI 모델: Gemini 2.5 Flash")
|
||
print()
|
||
|
||
if not model:
|
||
print("❌ Gemini API 키가 없습니다.")
|
||
return
|
||
|
||
reported_today = False
|
||
main.last_date = datetime.date.today()
|
||
|
||
while True:
|
||
now = datetime.datetime.now()
|
||
current_date = now.date()
|
||
|
||
if main.last_date != current_date:
|
||
reported_today = False
|
||
print(f"\n📅 날짜 변경: {current_date}")
|
||
|
||
main.last_date = current_date
|
||
|
||
if now.hour == 13 and now.minute == 0 and not reported_today:
|
||
send_daily_report()
|
||
reported_today = True
|
||
time.sleep(60)
|
||
|
||
time.sleep(60)
|
||
|
||
if __name__ == "__main__":
|
||
try:
|
||
main()
|
||
except KeyboardInterrupt:
|
||
print("\n\n👋 종료")
|
||
except Exception as e:
|
||
print(f"\n\n❌ 에러: {e}")
|
||
import traceback
|
||
traceback.print_exc()
|