test1 / context /context_builder.py
davidkim205's picture
fundamental ๋‚ด์šฉ ์ถ”๊ฐ€
2cc39e6
# context/context_builder.py
"""
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)