| |
| """ |
| LLM ์
๋ ฅ์ฉ ์ปจํ
์คํธ ๋น๋ ํจ์ ๋ชจ์ |
| """ |
|
|
| def _fmt_krw(val): |
| return "N/A" if val is None else f"{val:.2f}" |
|
|
|
|
| def _fmt_num(val): |
| if val is None or val != val: |
| return "N/A" |
| if isinstance(val, int): |
| return f"{val:,}" |
| if isinstance(val, float): |
| return f"{val:,.2f}" |
| return str(val) |
|
|
|
|
| def _pct(val): |
| if val is None or val != val: |
| return "N/A" |
| return f"{float(val) * 100:.1f}%" |
|
|
|
|
| def _upside(current, target): |
| if not current or not target: |
| return "N/A" |
| return f"{(float(target) / float(current) - 1) * 100:.1f}%" |
|
|
|
|
| def build_company_section(fd, ticker): |
| location = " / ".join(x for x in [fd.get("city"), fd.get("country")] if x) |
| return f"""[๊ธฐ์
์ ๋ณด]\nํ์ฌ๋ช
: {fd.get('company_name', ticker)} ({fd.get('symbol', ticker)})\n์นํฐ: {fd.get('sector', 'N/A')} / {fd.get('industry', 'N/A')}\n๊ฑฐ๋์: {fd.get('exchange', 'N/A')} / ํตํ: {fd.get('currency', 'N/A')}\n๋ณธ์ฌ: {location or 'N/A'} / ์ง์ ์: {_fmt_num(fd.get('full_time_employees'))}\n์๊ฐ์ด์ก: ${fd.get('market_cap_b', 'N/A')}B / EV: ${fd.get('enterprise_value_b', 'N/A')}B\n์ฌ์
์ค๋ช
: {fd.get('description', 'N/A')}""" |
|
|
|
|
| def build_price_section(pd): |
| return f"""[์ฃผ๊ฐ ํผํฌ๋จผ์ค]\nํ์ฌ๊ฐ: ${pd.get('current_price', 'N/A')}\n1์ฃผ ์์ต๋ฅ : {pd.get('change_1w_pct', 'N/A')}% / 1๊ฐ์: {pd.get('change_1m_pct', 'N/A')}% / YTD: {pd.get('change_ytd_pct', 'N/A')}%\n52์ฃผ ๊ณ ๊ฐ: ${pd.get('52w_high', 'N/A')} / ์ ๊ฐ: ${pd.get('52w_low', 'N/A')}\n30์ผ ํ๊ท ๊ฑฐ๋๋: {pd.get('avg_volume_30d', 'N/A'):,}""" |
|
|
|
|
| def build_valuation_section(fd): |
| return f"""[๋ฐธ๋ฅ์์ด์
& ์ฌ๋ฌด]\nPER(TTM): {fd.get('pe_ratio', 'N/A')} / ์ ํ PER: {fd.get('forward_pe', 'N/A')}\nPBR: {fd.get('pb_ratio', 'N/A')} / PSR: {fd.get('ps_ratio', 'N/A')} / EV/EBITDA: {fd.get('ev_to_ebitda', 'N/A')}\nEPS(TTM): {fd.get('trailing_eps', 'N/A')} / ์ ํ EPS: {fd.get('forward_eps', 'N/A')}\nROE: {_pct(fd.get('roe'))} / ROA: {_pct(fd.get('roa'))}\n๋งค์ถ์ด์ด์ต๋ฅ : {_pct(fd.get('gross_margin'))} / ์์
์ด์ต๋ฅ : {_pct(fd.get('operating_margin'))} / EBITDA ๋ง์ง: {_pct(fd.get('ebitda_margin'))}\n์์ด์ต๋ฅ : {_pct(fd.get('profit_margin'))}\n๋งค์ถ ์ฑ์ฅ๋ฅ : {_pct(fd.get('revenue_growth'))} / ์ด์ต ์ฑ์ฅ๋ฅ : {_pct(fd.get('earnings_growth'))}\n๋ถ์ฑ๋น์จ: {fd.get('debt_to_equity', 'N/A')} / ์ ๋๋น์จ: {fd.get('current_ratio', 'N/A')} / ๋น์ข๋น์จ: {fd.get('quick_ratio', 'N/A')}\n์ดํ๊ธ: ${fd.get('total_cash_b', 'N/A')}B / ์ด๋ถ์ฑ: ${fd.get('total_debt_b', 'N/A')}B\n์์
ํ๊ธํ๋ฆ: ${fd.get('operating_cashflow_b', 'N/A')}B / ์์ฌํ๊ธํ๋ฆ: ${fd.get('free_cashflow_b', 'N/A')}B\n๋ฐฐ๋น์์ต๋ฅ : {_pct(fd.get('dividend_yield'))} / ๋ฐฐ๋น์ฑํฅ: {_pct(fd.get('payout_ratio'))} / ๋ฒ ํ: {fd.get('beta', 'N/A')}""" |
|
|
|
|
| def build_analyst_section(fd, pd): |
| rec = fd.get("recommendation", "") |
| return f"""[์ ๋๋ฆฌ์คํธ ์ปจ์ผ์์ค]\nํ๊ท ๋ชฉํ์ฃผ๊ฐ: ${fd.get('analyst_target', 'N/A')} / ์๋จ: ${fd.get('target_high_price', 'N/A')} / ํ๋จ: ${fd.get('target_low_price', 'N/A')}\nํฌ์์๊ฒฌ: {rec.upper() if rec else 'N/A'} / ์ปค๋ฒ ์ ๋๋ฆฌ์คํธ ์: {_fmt_num(fd.get('analyst_opinion_count'))}\nํ์ฌ๊ฐ ๋๋น ์์น์ฌ๋ ฅ: {_upside(pd.get('current_price'), fd.get('analyst_target'))}""" |
|
|
|
|
| def build_capital_structure_section(fd): |
| return f"""[์๊ธ & ์๋ณธ๊ตฌ์กฐ]\n๋ฐํ์ฃผ์์: {_fmt_num(fd.get('shares_outstanding_b'))}B์ฃผ / ์ ํต์ฃผ์์: {_fmt_num(fd.get('float_shares_b'))}B์ฃผ\n๋ด๋ถ์ ๋ณด์ ์จ: {_pct(fd.get('held_percent_insiders'))} / ๊ธฐ๊ด ๋ณด์ ์จ: {_pct(fd.get('held_percent_institutions'))}\n๊ณต๋งค๋ ๋น์จ(Short Ratio): {fd.get('short_ratio', 'N/A')} / ์ ํต์ฃผ์ ๋๋น ๊ณต๋งค๋: {_pct(fd.get('short_percent_float'))}\n์ต๊ทผ ๋ฐฐ๋น๊ธ: {fd.get('dividend_rate', 'N/A')} / ๋ฐฐ๋น๋ฝ์ผ: {fd.get('ex_dividend_date', 'N/A')}""" |
|
|
|
|
| def build_technicals_section(td): |
| return f"""[๊ธฐ์ ์ ์งํ]\nMA20: ${td.get('ma20', 'N/A')} (ํ์ฌ๊ฐ MA20 {td.get('price_vs_ma20', 'N/A')})\nMA50: ${td.get('ma50', 'N/A')} / MA200: ${td.get('ma200', 'N/A')}\nRSI(14): {td.get('rsi_14', 'N/A')} โ {td.get('rsi_signal', 'N/A')}\nMACD ํ์คํ ๊ทธ๋จ: {td.get('macd_histogram', 'N/A')} โ {td.get('macd_signal', 'N/A')}\n๋ณผ๋ฆฐ์ ๋ฐด๋ ์์น: {td.get('bb_position', 'N/A')}\n๊ฑฐ๋๋ ๋น์จ(vs 20์ผ ํ๊ท ): {td.get('volume_ratio', 'N/A')}x""" |
|
|
|
|
| def build_earnings_section(ed, intent): |
| lines = ["[์ค์ ๋ฐ์ดํฐ]"] |
| if ed.get("next_earnings_date"): |
| lines.append(f"๋ค์ ์ค์ ๋ฐํ ์์ ์ผ: {ed['next_earnings_date']}") |
| filtered = ed.get("filtered_quarter") |
| if filtered and filtered.get("found") and filtered.get("data"): |
| lines.extend(_format_filtered_earnings(filtered)) |
| elif filtered and not filtered.get("found"): |
| lines.append(f"\nโป {filtered['period']} ์ค์ ๋ฐ์ดํฐ๋ฅผ ์ฐพ์ ์ ์์ต๋๋ค.") |
| lines.extend(_format_quarterly_table(ed.get("quarterly_results", []))) |
| lines.extend(_format_annual_table(ed.get("annual_results", []))) |
| lines.extend(_format_eps_surprise(ed.get("earnings_surprise", []))) |
| return "\n".join(lines) |
|
|
|
|
| def _format_filtered_earnings(filtered): |
| lines = [f"\nโ {filtered['period']} ์ค์ (์กฐํ ๋์)"] |
| d = filtered["data"] |
| if filtered["type"] == "quarter": |
| lines += [ |
| f" ๋งค์ถ: {_fmt_krw(d.get('revenue_b'))} ์กฐ์", |
| f" ์์
์ด์ต: {_fmt_krw(d.get('operating_income_b'))} ์กฐ์ (OPM: {d.get('operating_margin', 'N/A')}%)", |
| f" ์์ด์ต: {_fmt_krw(d.get('net_income_b'))} ์กฐ์", |
| ] |
| if d.get("revenue_yoy_pct") is not None: |
| lines.append(f" ๋งค์ถ YoY: {d['revenue_yoy_pct']:+.1f}%") |
| if d.get("op_income_yoy_pct") is not None: |
| lines.append(f" ์์
์ด์ต YoY: {d['op_income_yoy_pct']:+.1f}%") |
| else: |
| lines += [ |
| f" ๋งค์ถ: {_fmt_krw(d.get('revenue_t'))} ์กฐ์", |
| f" ์์
์ด์ต: {_fmt_krw(d.get('operating_income_t'))} ์กฐ์ (OPM: {d.get('operating_margin', 'N/A')}%)", |
| f" ์์ด์ต: {_fmt_krw(d.get('net_income_t'))} ์กฐ์", |
| ] |
| return lines |
|
|
|
|
| def _format_quarterly_table(quarters): |
| if not quarters: |
| return [] |
| lines = [ |
| "\nโ ์ต๊ทผ ๋ถ๊ธฐ๋ณ ์ค์ ์ถ์ด", |
| f" {'๋ถ๊ธฐ':<8} {'๋งค์ถ(์กฐ์)':>10} {'์์
์ด์ต(์กฐ์)':>14} {'OPM':>6} {'๋งค์ถYoY':>8}", |
| " " + "-" * 54, |
| ] |
| for q in quarters[:6]: |
| yoy = f"{q['revenue_yoy_pct']:+.1f}%" if q.get("revenue_yoy_pct") is not None else "N/A" |
| lines.append( |
| f" {q['period']:<8} " |
| f"{_fmt_krw(q.get('revenue_b')):>10} " |
| f"{_fmt_krw(q.get('operating_income_b')):>14} " |
| f"{str(q.get('operating_margin', 'N/A')) + '%':>6} " |
| f"{yoy:>8}" |
| ) |
| return lines |
|
|
|
|
| def _format_annual_table(annual): |
| if not annual: |
| return [] |
| lines = [ |
| "\nโ ์ฐ๊ฐ ์ค์ ์ถ์ด", |
| f" {'์ฐ๋':<6} {'๋งค์ถ(์กฐ์)':>10} {'์์
์ด์ต(์กฐ์)':>14} {'OPM':>6}", |
| " " + "-" * 40, |
| ] |
| for a in annual[:4]: |
| lines.append( |
| f" {a['year']:<6} " |
| f"{_fmt_krw(a.get('revenue_t')):>10} " |
| f"{_fmt_krw(a.get('operating_income_t')):>14} " |
| f"{str(a.get('operating_margin', 'N/A')) + '%':>6}" |
| ) |
| return lines |
|
|
|
|
| def _format_eps_surprise(surprises): |
| if not surprises: |
| return [] |
| lines = ["\nโ EPS ์ํ๋ผ์ด์ฆ (์ต๊ทผ)"] |
| for s in surprises[:4]: |
| surp = f"{s['surprise_pct']:+.1f}%" if s.get("surprise_pct") is not None else "N/A" |
| lines.append( |
| f" {s['period']:<10} " |
| f"์ค์ : {s.get('eps_actual', 'N/A')} " |
| f"์์: {s.get('eps_estimate', 'N/A')} " |
| f"์ํ๋ผ์ด์ฆ: {surp}" |
| ) |
| return lines |
|
|
|
|
| def build_web_search_section(ws_results): |
| lines = ["[์น ๊ฒ์ ์ต์ ์ ๋ณด (์ค์๊ฐ)]"] |
| for i, block in enumerate(ws_results, 1): |
| text = block.get("text", "").strip() |
| citations = block.get("citations", []) |
| if text: |
| snippet = text[:800] + ("..." if len(text) > 800 else "") |
| lines.append(f"\nโ ๊ฒ์ ๊ฒฐ๊ณผ {i}") |
| lines.append(snippet) |
| if citations: |
| lines.append(" [์ถ์ฒ]") |
| for c in citations[:5]: |
| title = c.get("title", "") |
| url = c.get("url", "") |
| if title or url: |
| lines.append(f" โข {title} {url}") |
| return "\n".join(lines) |
|
|
|
|
| def build_context(market_data, intent, news_str=None): |
| sections = [] |
| fd = market_data.fundamentals |
| pd = market_data.price_data |
| td = market_data.technicals |
| ed = market_data.earnings_data |
| ws = market_data.web_search_results |
|
|
| if fd: |
| sections.append(build_company_section(fd, market_data.ticker)) |
| if pd: |
| sections.append(build_price_section(pd)) |
| if fd and any(fd.get(k) for k in ["pe_ratio", "pb_ratio", "roe"]): |
| sections.append(build_valuation_section(fd)) |
| if fd.get("analyst_target"): |
| sections.append(build_analyst_section(fd, pd)) |
| if fd and any(fd.get(k) is not None for k in [ |
| "shares_outstanding_b", |
| "held_percent_institutions", |
| "short_ratio", |
| "dividend_rate", |
| ]): |
| sections.append(build_capital_structure_section(fd)) |
| if td: |
| sections.append(build_technicals_section(td)) |
| if ed: |
| sections.append(build_earnings_section(ed, intent)) |
| if market_data.news_snippets: |
| news_text = "\n".join(f" โข {n}" for n in market_data.news_snippets[:6]) |
| sections.append(f"[์ต๊ทผ ๋ด์ค ํค๋๋ผ์ธ (yfinance)]\n{news_text}") |
| if news_str: |
| sections.append(f"[๊ตฌ๊ธ ๋ด์ค ์์ฝ]\n{news_str}") |
| if ws: |
| sections.append(build_web_search_section(ws)) |
|
|
| return "\n\n".join(sections) |
|
|