import argparse import csv import html import json import os from openai import OpenAI client = OpenAI() def parse_stock_input(stock_arg): """입력 문자열 → 종목 리스트 변환""" if not isinstance(stock_arg, str): return [] items = stock_arg.split(",") result = [] for item in items: s = item.strip() if (s.startswith('"') and s.endswith('"')) or (s.startswith("'") and s.endswith("'")): s = s[1:-1] if s: result.append(s) return result class theme_info: def __init__(self, data): self.path = data self._store = {} self._raw = [] if not os.path.exists(data): raise FileNotFoundError(f"Data file not found: {data}") # CSV 로드 with open(data, "r", encoding="utf-8") as f: reader = csv.DictReader(f) required_cols = {"NAME", "THEME", "THEME_2", "THEME_3"} headers = set(n.strip() for n in (reader.fieldnames or [])) if not required_cols.issubset(headers): raise ValueError( f"CSV must contain headers {sorted(required_cols)}. Found: {reader.fieldnames}" ) for row in reader: name = (row.get("NAME") or "").strip() theme = html.unescape((row.get("THEME") or "").strip()) theme2 = html.unescape((row.get("THEME_2") or "").strip()) theme3 = html.unescape((row.get("THEME_3") or "").strip()) desc = f"Theme: {theme} | Theme 2: {theme2} | Theme 3: {theme3}" self._store[name] = desc self._raw.append({ "NAME": name, "THEME": theme, "THEME_2": theme2, "THEME_3": theme3 }) self._ci_index = {name.lower(): name for name in self._store.keys()} def get_ticker_and_name(self, stock): """GPT로 티커와 공식 종목명 조회""" prompt = f""" 아래 종목명에 대해 Yahoo Finance 기준: 1) 티커(symbol) 2) 공식 종목명(full name) 단, 중요한 조건: - 사용자가 입력한 종목명이 한국어라면 → 공식 종목명도 한국어로 반환하세요. - 사용자가 입력한 종목명에 한국어가 조금이라도 포함되어 있다면 → 공식 종목명도 한국어로 반환하세요. - 사용자가 입력한 종목명이 영어라면 → 공식 종목명도 영어로 반환하세요. - 한국 종목은 '.KS', '.KQ' 등 KRX 형식의 티커. - 해외 종목(미국/유럽/홍콩 등)은 해당 시장의 일반 티커를 사용. 예시: 사용자 입력: 'SK하이닉스' → {{ "ticker": "000660.KS", "name": "SK하이닉스" }} 사용자 입력: 'SKHynix' → {{ "ticker": "000660.KS", "name": "SK Hynix Inc." }} 사용자 입력: '현대자동차' → {{ "ticker": "000660.KS", "name": "현대차" }} 종목명: {stock} 오직 JSON으로만 답변하세요. """ resp = client.responses.create(model="gpt-5.1", input=prompt) try: parsed = json.loads(resp.output_text) return parsed.get("ticker"), parsed.get("name") except: print("[ERROR] GPT 티커/풀네임 JSON 파싱 실패") return None, None def find_existing_row(self, full_name): """CSV 내 기존 종목 여부 확인""" return next( (r for r in self._raw if r["NAME"].lower() == full_name.lower()), None ) def generate_theme_gpt(self, stock_name): """GPT로 테마 3개 생성""" prompt = f""" 종목 '{stock_name}'과 관련된 투자 테마 3개 추천 괄호 없이 간단하게 핵심 테마 표현 - 사용자가 입력한 종목명이 한국어라면 → 테마도 한국어로 반환하세요. - 사용자가 입력한 종목명에 한국어가 조금이라도 포함되어 있다면 → 테마도 한국어로 반환하세요. - 사용자가 입력한 종목명이 영어라면 → 테마도 영어로 반환하세요. JSON 형식으로 출력: {{"theme": "...", "theme2": "...", "theme3": "..."}} """ try: response = client.responses.create(model="gpt-5.1", input=prompt) return json.loads(response.output_text) except: print(f"[WARN] GPT 테마 생성 실패({stock_name})") return {"theme": "", "theme2": "", "theme3": ""} def _save_to_csv(self, row): """단일 row를 CSV에 append 저장""" file_exists = os.path.exists(self.path) with open(self.path, "a", newline="", encoding="utf-8") as f: writer = csv.DictWriter( f, fieldnames=["NAME", "THEME", "THEME_2", "THEME_3"], quoting=csv.QUOTE_ALL ) if not file_exists: writer.writeheader() writer.writerow(row) print(f"CSV append 완료: {row['NAME']}") def make(self, stock): """종목명 → 티커/공식명 조회 → 기존 CSV 확인 → GPT로 테마 생성 → CSV 저장""" # 1) 티커 + 공식명 조회 ticker_symbol, full_name = self.get_ticker_and_name(stock) if not ticker_symbol: print("[ERROR] 티커 조회 실패") return None print(f"GPT 결과 → ticker: {ticker_symbol}, full name: {full_name}") # 2) 기존 CSV 체크 existing = self.find_existing_row(full_name) if existing: print(f"'{full_name}' 기존 CSV 존재 → 기존값 반환") return [existing] # 3) GPT로 테마 생성 theme_data = self.generate_theme_gpt(full_name) theme = theme_data.get("theme", "").strip() theme2 = theme_data.get("theme2", "").strip() theme3 = theme_data.get("theme3", "").strip() new_row = { "NAME": full_name, "THEME": theme, "THEME_2": theme2, "THEME_3": theme3 } # CSV 저장 self._save_to_csv(new_row) # _store 업데이트 desc = f"Theme: {new_row['THEME']} | Theme 2: {new_row['THEME_2']} | Theme 3: {new_row['THEME_3']}" self._store[full_name] = desc self._raw.append(new_row) print("[STEP] make() 완료") return [new_row] def get(self, stock): """종목명으로 description 조회""" if not stock or not isinstance(stock, str): return None key = stock.strip() if key in self._store: return self._store[key] canonical = self._ci_index.get(key.lower()) if canonical: return self._store[canonical] # 부분 일치 1개만 반환 candidates = [n for n in self._store if key.lower() in n.lower()] if len(candidates) == 1: return self._store[candidates[0]] return None def ensure_parent_dir(json_path): """파일 저장 전 상위 디렉토리 생성""" parent = os.path.dirname(os.path.abspath(json_path)) if parent and not os.path.exists(parent): os.makedirs(parent, exist_ok=True) def append_to_json(records, json_path, class_name="theme_info"): """records → JSON append 저장""" ensure_parent_dir(json_path) if os.path.exists(json_path): try: with open(json_path, "r", encoding="utf-8") as rf: data = json.load(rf) except: print("[WARN] 기존 JSON 로드 실패. 새 파일 생성") data = {class_name: []} else: data = {class_name: []} data.setdefault(class_name, []) data[class_name].extend(records) with open(json_path, "w", encoding="utf-8") as wf: json.dump(data, wf, ensure_ascii=False, indent=2) print(f"[5/5] {json_path} 업데이트 완료.\n") def main(): parser = argparse.ArgumentParser(description="종목의 테마 검색 도구") parser.add_argument("--stock", default="Meta", nargs="+", help="종목 입력. 여러 개는 공백 또는 콤마(,) 로 구분 가능") parser.add_argument("--data", default="/work/portfolio/data/theme_info.csv") parser.add_argument("--output", default="output.json") args = parser.parse_args() # --- 1. args 확인 --- stock_str = " ".join(args.stock) stock_list = parse_stock_input(stock_str) if not stock_list: print("[ERROR] --stock에 유효한 종목 없음") return print("[1/5] 인자(args) 확인.") print(f"[INFO] --stock {args.stock}") print(f"[INFO] --data {args.data}") print(f"[INFO] --output {args.output}") print(f"[INFO] 입력된 종목 리스트: {stock_list}\n") # --- 2. 객체 생성 --- ti = theme_info(args.data) print("[2/5] theme_info 객체 생성 완료.\n") # --- 3. make 함수 실행 --- print("[3/5] make() 실행.") make_results = [] for stock in stock_list: print(f"[INFO] 처리 중: {stock}") rows = ti.make(stock) if rows: make_results.extend(rows) print() # --- 4. get 함수 실행 --- print("[4/5] get() 실행.") all_records = [] for row in make_results: official_name = row["NAME"] desc = ti.get(official_name) all_records.append({"stock": official_name, "desc": desc}) print(f"[INFO] {official_name} 조회 완료") print(f"{desc}") print() # --- 5. 저장 --- if all_records: append_to_json(all_records, args.output, class_name="theme_info") else: print("[INFO] 저장할 JSON 데이터 없음") if __name__ == "__main__": main()