BridgeMatrix Lab
AI-Powered Investment Intelligence
MarketMacro

[03/31 특징주] 중동 지정학적 위기 고조에 따른 국제 유가 급등 및 환율 변동성 확대

import google.generativeai as genai import yfinance as yf import requests, json, logging, re, os, time, sqlite3, feedparser, io, base64 import shutil from datetime import datetime import pytz from PIL import Image from config import settings from prompts.news_gemini_prompts import GEMINI_NEWS_PROMPT class NewsAnalyzerGemini: def __init__(self): """ [V285.0-FINAL] BridgeMatrix Lab 자율형 에이전트 - 엔진: Gemini 2.5 Flash (추론 비용 0원) - 지표: 7대 핵심 지표 수집 및 차트 날짜(%m/%d) 라벨링 자동화 - 이미지: Imagen 4.0 최신 모델 및 소유권(nginx) 자동 이양 - 로그: GPT 관련 불필요 메시지 삭제 및 정제 """ genai.configure(api_key=settings.GOOGLE_API_KEY) model_id = 'gemini-2.5-flash' # 🚩 [V285.0] 불필요 로그 삭제 및 Gemini 엔진 시동 확인 try: self.model = genai.GenerativeModel(model_name=model_id, tools=['google_search']) self.model.generate_content("ping", generation_config={"max_output_tokens": 1}) logging.info(f"🚀 BridgeMatrix Lab: Gemini 2.5 Flash 분석 엔진 시동 완료") except: self.model = genai.GenerativeModel(model_name=model_id) logging.warning("🚩 [Safe Mode] 도구 없이 Gemini 2.5 Flash 단독 가동") self.KST = pytz.timezone('Asia/Seoul') self.db_path = os.path.join(settings.BASE_DIR, 'database', 'bridgematrix_lab.db') self.image_dir = "/var/www/html/images" self.web_base_url = "https://demoportal.kr/images" # 폴더 자동 생성 및 권한 설정 if not os.path.exists(self.image_dir): os.makedirs(self.image_dir, mode=0o755, exist_ok=True) try: shutil.chown(self.image_dir, user="nginx", group="nginx") except: pass self._init_db() def _init_db(self): with sqlite3.connect(self.db_path) as conn: conn.execute("CREATE TABLE IF NOT EXISTS processed_news (link TEXT PRIMARY KEY, title TEXT, processed_at DATETIME DEFAULT CURRENT_TIMESTAMP)") def analyze_news(self, news_item): """Gemini 2.5 Flash 기반 3,500자 분석 및 7대 지표 통합 리포트 생성""" raw_title = news_item['title'].replace("특이주", "특징주") try: logging.info(f"🔍 단계 1: 제미나이 분석 엔진 가동 ({raw_title})") response = self.model.generate_content(f"분석 대상 속보: {raw_title}\n\n{GEMINI_NEWS_PROMPT}") match = re.search(r'\{.*\}', response.text, re.DOTALL) data = json.loads(match.group()) if match else {"analysis_ko": response.text} logging.info("🎨 단계 2: Imagen 4.0 기반 맞춤형 배너 생성 중...") thumb_url = self._generate_optimized_banner(raw_title) return { "headline": f"[{datetime.now(self.KST).strftime('%m/%d 특징주')}] {raw_title}", "analysis_ko": self._format_analysis(data.get("analysis_ko", "")), "market_data": self._get_market_intelligence(), "summary_3_lines": data.get("summary_3_lines", ""), "sector_strategy": data.get("sector_strategy", []), "formatted_date": datetime.now(self.KST).strftime("%Y-%m-%d %H:%M"), "thumbnail_url": thumb_url } except Exception as e: logging.error(f"❌ 분석 오류 발생: {e}"); return None def _get_market_intelligence(self): """📈 7대 핵심 지표 수집 및 차트 날짜 라벨(%m/%d) 동적 생성""" intel = { "indices": {}, "korea": {}, "global": {}, "labels": [] # 🚩 차트 X축 날짜 라벨 } target_map = { "KOSPI": "^KS11", "KOSDAQ": "^KQ11", "NASDAQ": "^IXIC", "S&P500": "^GSPC", "USD/KRW": "USDKRW=X", "WTI유가": "CL=F", "필라델피아반도체": "^SOX" } for name, ticker in target_map.items(): try: tk = yf.Ticker(ticker) # 7일치를 가져와 주말 제외 최근 5거래일 추출 data = tk.history(period="7d").tail(5) if not data.empty: # 🚩 [V285.0 핵심] 차트 라벨을 '3/22' 형식의 실제 날짜로 구성 if name == "KOSPI" and not intel["labels"]: intel["labels"] = [d.strftime('%m/%d') for d in data.index] curr, prev = data['Close'].iloc[-1], data['Close'].iloc[-2] rate = ((curr / prev) - 1) * 100 item_data = { "val": f"{curr:,.2f}", "rate": f"{rate:+.2f}%", "trend": "up" if rate > 0 else "down", "bottom_color": "#dc2626" if rate > 0 else "#2563eb", "history": [round(x, 2) for x in data['Close'].tolist()] } intel["indices"][name] = item_data if name in ["KOSPI", "KOSDAQ", "USD/KRW"]: intel["korea"][name] = item_data else: intel["global"][name] = item_data except Exception as e: logging.error(f"지표 수집 실패 ({name}): {e}") if not intel["labels"]: intel["labels"] = ["-", "-", "-", "-", "Now"] return intel def _generate_optimized_banner(self, headline): """Imagen 4.0 배너 생성 및 소유권(nginx) 강제 이양""" default_img = f"{self.web_base_url}/default_news.webp" # 🚩 [V285.0] Imagen 4.0 엔드포인트 적용 imagen_model = "imagen-4.0-generate-001" url = f"https://generativelanguage.googleapis.com/v1beta/models/{imagen_model}:predict?key={settings.GOOGLE_API_KEY}" try: payload = {"instances": [{"prompt": f"Professional financial 3D conceptual art for: {headline}, dark navy style"}]} res = requests.post(url, json=payload, timeout=40) res_data = res.json() img_b64 = None if 'predictions' in res_data and len(res_data['predictions']) > 0: img_b64 = res_data['predictions'][0].get('bytesBase64Encoded') elif 'images' in res_data and len(res_data['images']) > 0: img_b64 = res_data['images'][0].get('bytesBase64Encoded') if img_b64: fname = f"bml_{int(time.time())}.webp" fpath = os.path.join(self.image_dir, fname) img = Image.open(io.BytesIO(base64.b64decode(img_b64))).convert("RGB") img.thumbnail((1200, 675)) img.save(fpath, "WEBP", quality=85) # 🚩 Nginx 읽기 권한 및 소유권 즉시 설정 os.chmod(fpath, 0o644) try: shutil.chown(fpath, user="nginx", group="nginx") except: pass logging.info(f"✅ 배너 생성 완료 및 권한 설정 성공: {fname}") return f"{self.web_base_url}/{fname}" return default_img except: return default_img def _format_analysis(self, text): """가독성 최적화: 볼드체 처리 및 섹션 디자인 적용""" if not text: return "" summary_bar = f"
Quick View: 실시간 지표와 AI 통찰을 결합한 투자 전략입니다.
" # 소제목 📍 스타일링 text = re.sub(r'📍([^<>\n]+)', r'

\1

', text) paragraphs = text.split('\n') html_list = [] for p in paragraphs: p = p.strip() if not p or "\1', p) html_list.append(f"

{p}

") return summary_bar + "".join(html_list)