benjamin5607 commited on
Commit
a8bbcce
ยท
verified ยท
1 Parent(s): 1c4b7cd

Update streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +156 -228
streamlit_app.py CHANGED
@@ -2,270 +2,198 @@ import streamlit as st
2
  import yfinance as yf
3
  import FinanceDataReader as fdr
4
  import pandas as pd
5
- import requests
6
  import plotly.graph_objects as go
7
  from huggingface_hub import InferenceClient
8
  from duckduckgo_search import DDGS
9
  import json
10
  import datetime
 
11
 
12
  # 1. ํŽ˜์ด์ง€ ์„ค์ •
13
- st.set_page_config(
14
- page_title="Pocket Quant Pro",
15
- page_icon="๐ŸŒ",
16
- layout="wide",
17
- initial_sidebar_state="expanded"
18
- )
19
-
20
- # --- ์Šคํƒ€์ผ๋ง ---
21
- st.markdown("""
22
- <style>
23
- [data-testid="stMetricValue"] { font-size: 24px; }
24
- div[data-testid="stStatusWidget"] div button { display: none; }
25
- .st-emotion-cache-1r6slb0 { border: 1px solid #333; border-radius: 10px; padding: 15px; }
26
- </style>
27
- """, unsafe_allow_html=True)
28
 
29
  # 2. ํ† ํฐ ํ™•์ธ
30
  if "HF_TOKEN" in st.secrets:
31
  client = InferenceClient(api_key=st.secrets["HF_TOKEN"])
32
  else:
33
- st.error("๐Ÿšจ ์„ค์ • ์˜ค๋ฅ˜: Secrets์— 'HF_TOKEN'์ด ํ•„์š”ํ•ฉ๋‹ˆ๋‹ค.")
34
  st.stop()
35
 
36
- # --- [ํ•ต์‹ฌ ๊ธฐ๋Šฅ] 3์ค‘ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ (Yahoo -> FDR -> Web) ---
37
- def get_stock_data_robust(ticker, period_days=90):
38
  """
39
- 1. Yahoo Finance ์‹œ๋„
40
- 2. ์‹คํŒจ ์‹œ FinanceDataReader(Investing.com/Naver) ์‹œ๋„
41
- 3. ๋ฐ์ดํ„ฐํ”„๋ ˆ์ž„(History)๊ณผ ํ˜„์žฌ๊ฐ€ ๋ฆฌํ„ด
 
 
42
  """
43
- hist = pd.DataFrame()
44
- source = ""
45
-
46
- # [1๋‹จ๊ณ„] Yahoo Finance
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
47
  try:
48
  stock = yf.Ticker(ticker)
49
- # ์•ผํ›„๋Š” period ์ธ์ž ์‚ฌ์šฉ
50
  hist = stock.history(period="3mo")
51
- if not hist.empty:
52
- return hist, "Yahoo Finance"
53
- except:
54
- pass
55
-
56
- # [2๋‹จ๊ณ„] FinanceDataReader (Plan B)
57
- # ์•ผํ›„ ํ‹ฐ์ปค๋ฅผ FDR์šฉ ์‹ฌ๋ณผ๋กœ ๋ณ€ํ™˜ํ•ด์•ผ ํ•จ
58
- fdr_symbol = ticker
59
- exchange = None
60
-
61
- # ๋ฒ ํŠธ๋‚จ: VIC.VN -> VIC (HOSE)
62
- if ".VN" in ticker:
63
- fdr_symbol = ticker.split('.')[0]
64
- exchange = 'VN' # Vietnam
65
- # ์ธ๋„๋„ค์‹œ์•„: .JK -> Investing.com ์‹ฌ๋ณผ ๋งคํ•‘ ํ•„์š” (FDR์€ 'IDX:BBCA' ํ˜•์‹์„ ์”€)
66
- elif ".JK" in ticker:
67
- fdr_symbol = f"IDX:{ticker.split('.')[0]}"
68
- # ํƒœ๊ตญ, ๋ง๋ ˆ์ด์‹œ์•„ ๋“ฑ
69
- elif ".BK" in ticker: fdr_symbol = f"SET:{ticker.split('.')[0]}"
70
- elif ".KL" in ticker: fdr_symbol = f"KLS:{ticker.split('.')[0]}"
71
 
 
72
  try:
73
- # ์ตœ๊ทผ 3๊ฐœ์›” ๋‚ ์งœ ๊ณ„์‚ฐ
74
- start_date = (datetime.datetime.now() - datetime.timedelta(days=period_days)).strftime('%Y-%m-%d')
 
 
 
75
 
76
- # FDR๋กœ ๋ฐ์ดํ„ฐ ๊ธ์–ด์˜ค๊ธฐ
77
- if exchange == 'VN':
78
- # ๋ฒ ํŠธ๋‚จ์€ ๋ณ„๋„ ์ฒ˜๋ฆฌ
79
- hist = fdr.DataReader(fdr_symbol, start_date) # ๋ฒ ํŠธ๋‚จ์€ ๊ฑฐ๋ž˜์†Œ ์ฝ”๋“œ ์—†์ด ์ข…๋ชฉ์ฝ”๋“œ๋งŒ ๋„ฃ์œผ๋ฉด ์ž˜ ๋จ (KRX/Naver์†Œ์Šค)
80
- else:
81
- hist = fdr.DataReader(fdr_symbol, start_date)
82
-
83
  if not hist.empty:
84
- # FDR ์ปฌ๋Ÿผ๋ช…์„ Yahoo ํฌ๋งท(Open, High, Low, Close, Volume)์œผ๋กœ ํ†ต์ผ
85
- hist = hist.rename(columns={'Close': 'Close', 'Open': 'Open', 'High': 'High', 'Low': 'Low', 'Volume': 'Volume'})
86
- return hist, "FinanceDataReader (Investing/Naver)"
87
- except Exception as e:
88
- print(f"FDR Failed: {e}")
89
- pass
90
-
91
  return pd.DataFrame(), "None"
92
 
93
- # --- [๋ณด์กฐ ๊ธฐ๋Šฅ] ๋‰ด์Šค ๊ฒ€์ƒ‰ ---
94
- def get_recent_news(keyword):
95
- news_summary = ""
96
  try:
97
  with DDGS() as ddgs:
98
- results = ddgs.news(f"{keyword} stock business", timelimit="m", max_results=5)
99
- for i, r in enumerate(results):
100
- news_summary += f"[{r.get('date', '?')}] {r['title']}\n"
101
- return news_summary if news_summary else "No recent news."
102
- except: return "News Search Error"
103
-
104
- # --- ํ‹ฐ์ปค ๋งคํ•‘ ---
105
- TICKER_NAMES = {
106
- "AAPL": "Apple", "NVDA": "NVIDIA", "TSLA": "Tesla", "005930.KS": "์‚ผ์„ฑ์ „์ž",
107
- "VIC.VN": "Vingroup", "VHM.VN": "Vinhomes", "VCB.VN": "Vietcombank",
108
- "BBCA.JK": "BCA", "BBRI.JK": "BRI", "GOTO.JK": "GoTo",
109
- "1155.KL": "Maybank", "PTT.BK": "PTT"
110
- }
111
-
112
- MARKET_SAMPLES = {
113
- "๐Ÿ‡ป๐Ÿ‡ณ ๋ฒ ํŠธ๋‚จ (Vietnam)": ["VIC.VN", "VHM.VN", "VCB.VN", "VNM.VN", "HPG.VN", "MSN.VN"],
114
- "๐Ÿ‡ฎ๐Ÿ‡ฉ ์ธ๋„๋„ค์‹œ์•„": ["BBCA.JK", "BBRI.JK", "TLKM.JK", "BMRI.JK", "ASII.JK", "GOTO.JK"],
115
- "๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ": ["005930.KS", "000660.KS", "035420.KS", "207940.KS"],
116
- "๐Ÿ‡บ๐Ÿ‡ธ ๋ฏธ๊ตญ": ["AAPL", "NVDA", "TSLA", "MSFT", "GOOGL", "AMD"],
117
- "๐Ÿ‡จ๐Ÿ‡ณ ์ค‘๊ตญ/ํ™์ฝฉ": ["9988.HK", "0700.HK", "3690.HK", "1211.HK"],
118
- "๐Ÿ‡ฏ๐Ÿ‡ต ์ผ๋ณธ": ["7203.T", "6758.T", "9984.T", "8035.T"]
119
- }
120
-
121
- GLOBAL_TICKER_MAP = {
122
- "TSMC": "2330.TW", "VINGROUP": "VIC.VN", "VINFAST": "VFS",
123
- "SAMSUNG": "005930.KS", "TOYOTA": "7203.T"
124
- }
125
-
126
- def get_ticker_from_ai(name):
127
- try:
128
- prompt = f"Find Yahoo Finance ticker for '{name}'. Return ONLY ticker."
129
- messages = [{"role": "user", "content": prompt}]
130
- response = client.chat.completions.create(model="Qwen/Qwen2.5-72B-Instruct", messages=messages, max_tokens=20)
131
- return response.choices[0].message.content.strip().replace("`", "").upper().split()[0]
132
- except: return None
133
-
134
- def search_ticker(query):
135
- return GLOBAL_TICKER_MAP.get(query.upper().strip(), get_ticker_from_ai(query))
136
 
137
  def parse_stream(stream):
138
  for chunk in stream:
139
- if chunk.choices:
140
- yield chunk.choices[0].delta.content or ""
141
 
142
  # --- ์ฐจํŠธ ๊ทธ๋ฆฌ๊ธฐ ---
143
- def plot_candle_chart(hist, title, source):
144
- fig = go.Figure(data=[go.Candlestick(
145
- x=hist.index, open=hist['Open'], high=hist['High'], low=hist['Low'], close=hist['Close'], name="Price"
146
- )])
147
- fig.update_layout(
148
- title=dict(text=f"{title} Trend (Source: {source})", x=0),
149
- height=350, margin=dict(l=10, r=10, t=40, b=10),
150
- xaxis_rangeslider_visible=False, template="plotly_dark",
151
- paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)'
152
- )
153
- return fig
154
-
155
- def plot_bar_chart(df):
156
- colors = ['#00FF00' if x > 0 else '#FF0000' for x in df['Change(%)']]
157
- fig = go.Figure(go.Bar(
158
- x=df['Name'], y=df['Change(%)'], marker_color=colors,
159
- text=df['Change(%)'].apply(lambda x: f"{x:.2f}%"), textposition='auto'
160
- ))
161
- fig.update_layout(
162
- title="Market Heatmap", height=350, margin=dict(l=10, r=10, t=40, b=10),
163
- template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)', plot_bgcolor='rgba(0,0,0,0)'
164
- )
165
  return fig
166
 
167
- # --- ์‚ฌ์ด๋“œ๋ฐ” ---
168
- with st.sidebar:
169
- st.header("๐ŸŒ Pocket Quant Pro")
170
- st.divider()
171
- menu = st.radio("MENU", ["๐Ÿ” ์ข…๋ชฉ ๋ถ„์„ (AI Analysis)", "๐Ÿ“ก ์‹œ์žฅ ์Šค์บ๋„ˆ (Market Watch)"], index=0)
172
- st.divider()
173
- st.caption("Auto-Fallback: Yahoo -> FDR")
174
-
175
- # --- ์ข…๋ชฉ ๋ถ„์„ ---
176
- if "์ข…๋ชฉ ๋ถ„์„" in menu:
177
- st.subheader("๐Ÿ” AI Investment Analyst (Historical Data Mining)")
178
- c1, c2 = st.columns([3, 1])
179
- with c1: user_input = st.text_input("Ticker", "Vingroup", label_visibility="collapsed", placeholder="์ข…๋ชฉ๋ช…")
180
- with c2: analyze_btn = st.button("Deep Analyze", use_container_width=True)
181
-
182
- if analyze_btn:
183
- with st.status("๐Ÿš€ 3์ค‘ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์‹œ์Šคํ…œ ๊ฐ€๋™...", expanded=True) as status:
184
- ticker = search_ticker(user_input)
 
 
185
 
186
- if ticker:
187
- st.write(f"๐ŸŽฏ ํƒ€๊ฒŸ ํ™•์ธ: {ticker}")
188
-
189
- # [ํ•ต์‹ฌ] Robust Data Fetching
190
- hist, data_source = get_stock_data_robust(ticker)
191
-
192
- # ๋‰ด์Šค ์ˆ˜์ง‘
193
- news_data = get_recent_news(user_input)
194
-
195
- if not hist.empty:
196
- # ๋ฐ์ดํ„ฐ ํ™•๋ณด ์„ฑ๊ณต
197
- status.update(label=f"โœ… ๋ฐ์ดํ„ฐ ํ™•๋ณด ์„ฑ๊ณต! (์ถœ์ฒ˜: {data_source})", state="complete", expanded=False)
198
-
199
- curr = hist['Close'].iloc[-1]
200
- prev = hist['Close'].iloc[-2]
201
- change_pct = ((curr - prev) / prev) * 100
202
- vol = hist['Volume'].iloc[-1] if 'Volume' in hist.columns else 0
203
-
204
- st.divider()
205
-
206
- # ์ƒ๋‹จ ๋ฉ”ํŠธ๋ฆญ
207
- m1, m2, m3 = st.columns(3)
208
- m1.metric("Price", f"{curr:,.0f}", f"{change_pct:.2f}%")
209
- m2.metric("Source", data_source)
210
- m3.metric("Volume", f"{vol:,.0f}")
211
-
212
- # ์ฐจํŠธ ๊ทธ๋ฆฌ๊ธฐ (FDR ๋ฐ์ดํ„ฐ๋กœ๋„ ๊ทธ๋ ค์ง!)
213
- st.plotly_chart(plot_candle_chart(hist, ticker, data_source), use_container_width=True)
214
-
215
- # ๋‰ด์Šค & AI ๋ถ„์„
216
- with st.expander(f"๐Ÿ“ฐ '{user_input}' ๊ด€๋ จ ์ตœ์‹  ๋‰ด์Šค ๋ณด๊ธฐ"):
217
- st.text(news_data)
218
-
219
- st.subheader(f"๐Ÿง  Deep Insight: {user_input}")
220
- prompt = f"""
221
- [ROLE] Global Analyst. [TASK] Analyze {user_input} ({ticker}).
222
- [DATA] Price: {curr}, Trend: {change_pct:.2f}% (Source: {data_source})
223
- [NEWS] {news_data}
224
- [LANG] Korean.
225
- [OUT] Markdown. 1.Technical Analysis 2.News Sentiment 3.Strategy
226
- """
227
- messages = [{"role": "user", "content": prompt}]
228
- stream = client.chat.completions.create(model="Qwen/Qwen2.5-72B-Instruct", messages=messages, stream=True)
229
- with st.container(border=True): st.write_stream(parse_stream(stream))
230
-
231
- else:
232
- status.update(label="โŒ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์‹คํŒจ", state="error")
233
- st.error("๋ชจ๋“  ์†Œ์Šค(Yahoo, Investing, Naver)์—์„œ ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์„ ์ˆ˜ ์—†์Šต๋‹ˆ๋‹ค.")
234
- st.warning("๊ฐ€๋Šฅ์„ฑ 1: ํ‹ฐ์ปค๊ฐ€ ์ž˜๋ชป๋จ\n๊ฐ€๋Šฅ์„ฑ 2: ํ•ด๋‹น ๊ตญ๊ฐ€ ๋ฐ์ดํ„ฐ๊ฐ€ ์œ ๋ฃŒํ™”๋จ")
235
  else:
236
- status.update(label="โŒ ํ‹ฐ์ปค ๋ชป ์ฐพ์Œ", state="error")
237
-
238
- # --- ์‹œ์žฅ ์Šค์บ๋„ˆ ---
239
- elif "์‹œ์žฅ ์Šค์บ๋„ˆ" in menu:
240
- st.subheader("๐Ÿ“ก Global Market Watch")
241
-
242
- col_sel, col_btn = st.columns([3, 1])
243
- with col_sel: target_market = st.selectbox("Select Country", list(MARKET_SAMPLES.keys()), label_visibility="collapsed")
244
- with col_btn: scan_trigger = st.button("Scan", use_container_width=True)
245
-
246
- if scan_trigger:
247
- tickers = MARKET_SAMPLES[target_market]
248
- with st.spinner(f"Scanning {target_market} stocks (Robust Mode)..."):
249
- results = []
250
 
251
- # ์Šค์บ๋„ˆ๋„ Robust ๋ชจ๋“œ๋กœ ๋™์ž‘ (์กฐ๊ธˆ ๋А๋ ค๋„ ํ™•์‹คํ•˜๊ฒŒ)
252
- progress_bar = st.progress(0)
253
- for i, ticker in enumerate(tickers):
254
- hist, src = get_stock_data_robust(ticker, period_days=5)
255
- if not hist.empty:
256
- last = hist['Close'].iloc[-1]
257
- prev = hist['Close'].iloc[-2]
258
- pct = ((last - prev)/prev)*100
259
- name = TICKER_NAMES.get(ticker, ticker)
260
- results.append({'Name': name, 'Price': last, 'Change(%)': pct})
261
- progress_bar.progress((i + 1) / len(tickers))
262
 
263
- if results:
264
- df = pd.DataFrame(results).sort_values('Change(%)', ascending=False)
265
- st.plotly_chart(plot_bar_chart(df), use_container_width=True)
266
-
267
- c1, c2 = st.columns(2)
268
- with c1: st.success("๐Ÿ”ฅ Top Gainers"); st.dataframe(df.head(10).style.format({"Price": "{:,.2f}", "Change(%)": "{:,.2f}%"}), use_container_width=True)
269
- with c2: st.error("๐Ÿ’ง Top Losers"); st.dataframe(df.tail(10).sort_values('Change(%)').style.format({"Price": "{:,.2f}", "Change(%)": "{:,.2f}%"}), use_container_width=True)
270
  else:
271
- st.warning("๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์‹คํŒจ.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2
  import yfinance as yf
3
  import FinanceDataReader as fdr
4
  import pandas as pd
 
5
  import plotly.graph_objects as go
6
  from huggingface_hub import InferenceClient
7
  from duckduckgo_search import DDGS
8
  import json
9
  import datetime
10
+ import re
11
 
12
  # 1. ํŽ˜์ด์ง€ ์„ค์ •
13
+ st.set_page_config(page_title="Pocket Quant AI", page_icon="๐Ÿง ", layout="wide")
14
+ st.title("๐Ÿง  ์ง„์งœ AI ์ฃผ์‹ ๋น„์„œ")
15
+ st.caption("์‚ฌ์šฉ์ž์˜ ์˜๋„๋ฅผ ๋จผ์ € ํŒŒ์•…ํ•˜๊ณ  -> ์ตœ์ ์˜ ๊ฒ€์ƒ‰์–ด๋กœ ๋ฐ์ดํ„ฐ๋ฅผ ์ฐพ์Šต๋‹ˆ๋‹ค.")
 
 
 
 
 
 
 
 
 
 
 
 
16
 
17
  # 2. ํ† ํฐ ํ™•์ธ
18
  if "HF_TOKEN" in st.secrets:
19
  client = InferenceClient(api_key=st.secrets["HF_TOKEN"])
20
  else:
21
+ st.error("๐Ÿšจ HF_TOKEN ์„ค์ • ํ•„์š”")
22
  st.stop()
23
 
24
+ # --- [ํ•ต์‹ฌ ์—”์ง„] 1๋‹จ๊ณ„: AI๊ฐ€ ๋จผ์ € '๋ฌด์—‡์„ ์ฐพ์„์ง€' ํŒ๋‹จํ•˜๋Š” ํ•จ์ˆ˜ ---
25
+ def identify_target_with_ai(user_query):
26
  """
27
+ ์‚ฌ์šฉ์ž์˜ ์ž…๋ ฅ(์–ด๋–ค ์–ธ์–ด๋“ )์„ ๋ฐ›์•„์„œ
28
+ 1. ์ •ํ™•ํ•œ ๊ธฐ์—… ์˜๋ฌธ๋ช…
29
+ 2. ์•ผํ›„/FDR ํ‹ฐ์ปค
30
+ 3. ๋‰ด์Šค ๊ฒ€์ƒ‰์šฉ ์ตœ์  ํ‚ค์›Œ๋“œ(์˜์–ด)
31
+ ๋ฅผ JSON์œผ๋กœ ๋ฆฌํ„ด๋ฐ›์Šต๋‹ˆ๋‹ค.
32
  """
33
+ try:
34
+ prompt = f"""
35
+ [ROLE]
36
+ You are a Financial Entity Resolver. Your job is to identify the company from the user's query and prepare search parameters.
37
+
38
+ [USER QUERY]
39
+ "{user_query}"
40
+
41
+ [TASK]
42
+ 1. Identify the company clearly. (e.g., "๋น„๋‚˜๋ฐ€ํฌ" -> "Vinamilk", "๋ฒ ํŠธ๋‚จ ์ฐจ 1๋“ฑ" -> "VinFast" or "Truong Hai")
43
+ 2. Find the best Ticker Symbol. Prefer Yahoo Finance format.
44
+ - Vietnam: Ends with .VN (e.g., VNM.VN, VIC.VN)
45
+ - Indonesia: Ends with .JK (e.g., BBCA.JK)
46
+ - Korea: .KS or .KQ
47
+ - Japan: .T
48
+ - USA: Ticker only (e.g., AAPL)
49
+ 3. Create a specific ENGLISH search keyword for news (to avoid unrelated results like NVIDIA).
50
+
51
+ [OUTPUT FORMAT]
52
+ Return ONLY a JSON object. No markdown. No explanations.
53
+ {{
54
+ "name": "Official English Company Name",
55
+ "ticker": "TICKER",
56
+ "search_keyword": "Company Name + 'stock business news' (e.g., 'Vinamilk Vietnam stock business')"
57
+ }}
58
+ """
59
+
60
+ messages = [{"role": "user", "content": prompt}]
61
+ response = client.chat.completions.create(
62
+ model="Qwen/Qwen2.5-72B-Instruct", messages=messages, max_tokens=150
63
+ )
64
+
65
+ # JSON ํŒŒ์‹ฑ (AI๊ฐ€ ๊ฐ€๋” ```json ๋“ฑ์„ ๋ถ™์ผ ์ˆ˜ ์žˆ์œผ๋ฏ€๋กœ ์ œ๊ฑฐ)
66
+ content = response.choices[0].message.content.strip()
67
+ content = re.sub(r"```json|```", "", content).strip()
68
+ return json.loads(content)
69
+
70
+ except Exception as e:
71
+ st.error(f"AI ํŒ๋‹จ ์‹คํŒจ: {e}")
72
+ return None
73
+
74
+ # --- [ํ•ต์‹ฌ ์—”์ง„] 2๋‹จ๊ณ„: ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ (FDR + Yahoo) ---
75
+ def get_market_data(ticker):
76
+ # 1. Yahoo ์‹œ๋„
77
  try:
78
  stock = yf.Ticker(ticker)
 
79
  hist = stock.history(period="3mo")
80
+ if not hist.empty: return hist, "Yahoo Finance"
81
+ except: pass
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
82
 
83
+ # 2. FDR ์‹œ๋„ (Yahoo ์‹คํŒจ์‹œ)
84
  try:
85
+ # FDR์šฉ ์‹ฌ๋ณผ ๋ณ€ํ™˜
86
+ fdr_symbol = ticker
87
+ if ".VN" in ticker: fdr_symbol = ticker.split('.')[0] # ๋ฒ ํŠธ๋‚จ
88
+ elif ".JK" in ticker: fdr_symbol = f"IDX:{ticker.split('.')[0]}"
89
+ elif ".T" in ticker: fdr_symbol = f"TSE:{ticker.split('.')[0]}"
90
 
91
+ start = (datetime.datetime.now() - datetime.timedelta(days=90)).strftime('%Y-%m-%d')
92
+ hist = fdr.DataReader(fdr_symbol, start)
 
 
 
 
 
93
  if not hist.empty:
94
+ hist = hist.rename(columns={'Close':'Close', 'Open':'Open', 'High':'High', 'Low':'Low', 'Volume':'Volume'})
95
+ return hist, "FinanceDataReader"
96
+ except: pass
97
+
 
 
 
98
  return pd.DataFrame(), "None"
99
 
100
+ # --- [ํ•ต์‹ฌ ์—”์ง„] 3๋‹จ๊ณ„: ๋‰ด์Šค ๊ฒ€์ƒ‰ (AI๊ฐ€ ์ค€ ํ‚ค์›Œ๋“œ๋กœ ์ˆ˜ํ–‰) ---
101
+ def get_news(keyword):
102
+ summary = ""
103
  try:
104
  with DDGS() as ddgs:
105
+ # AI๊ฐ€ ๋งŒ๋“ค์–ด์ค€ '์ตœ์ ์˜ ์˜์–ด ํ‚ค์›Œ๋“œ'๋กœ ๊ฒ€์ƒ‰ -> ์ •ํ™•๋„ ์ƒ์Šน
106
+ results = ddgs.news(keyword, timelimit="m", max_results=5)
107
+ for r in results:
108
+ summary += f"[{r.get('date','?')}] {r['title']}\n"
109
+ return summary if summary else "No recent news found."
110
+ except: return "Search Error"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
 
112
  def parse_stream(stream):
113
  for chunk in stream:
114
+ if chunk.choices: yield chunk.choices[0].delta.content or ""
 
115
 
116
  # --- ์ฐจํŠธ ๊ทธ๋ฆฌ๊ธฐ ---
117
+ def plot_chart(hist, name, source):
118
+ fig = go.Figure(data=[go.Candlestick(x=hist.index, open=hist['Open'], high=hist['High'], low=hist['Low'], close=hist['Close'], name="Price")])
119
+ fig.update_layout(title=f"{name} ({source})", height=350, margin=dict(l=10, r=10, t=30, b=10), template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)')
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  return fig
121
 
122
+ # ==============================================================================
123
+ # ๋ฉ”์ธ ๋กœ์ง
124
+ # ==============================================================================
125
+ col1, col2 = st.columns([3, 1])
126
+ with col1:
127
+ # ํžŒํŠธ๋ฅผ ์ฃผ์ง€ ์•Š์Œ. AI๊ฐ€ ์•Œ์•„์„œ ํ•ด์•ผ ํ•จ.
128
+ user_query = st.text_input("๊ถ๊ธˆํ•œ ๊ธฐ์—…์„ ์ž…๋ ฅํ•˜์„ธ์š” (์˜ˆ: ๋น„๋‚˜๋ฐ€ํฌ, ๋ฒ ํŠธ๋‚จ ๋นˆ๊ทธ๋ฃน, ์ผ๋ณธ ๋„์š”ํƒ€...)", "๋น„๋‚˜๋ฐ€ํฌ")
129
+ with col2:
130
+ btn = st.button("๋ถ„์„ ์‹œ์ž‘", use_container_width=True)
131
+
132
+ if btn:
133
+ with st.status("๐Ÿง  AI๊ฐ€ ์‚ฌ์šฉ์ž์˜ ์˜๋„๋ฅผ ๋ถ„์„ ์ค‘์ž…๋‹ˆ๋‹ค...", expanded=True) as status:
134
+
135
+ # 1. [๋‡Œ] AI์—๊ฒŒ "์ด๊ฒŒ ๋ฌด์Šจ ๊ธฐ์—…์ด๊ณ  ๊ฒ€์ƒ‰์–ด๋Š” ๋ญ˜๋กœ ํ• ๊นŒ?" ๋ฌผ์–ด๋ด„
136
+ target_info = identify_target_with_ai(user_query)
137
+
138
+ if target_info:
139
+ name = target_info.get('name', 'Unknown')
140
+ ticker = target_info.get('ticker', '')
141
+ search_keyword = target_info.get('search_keyword', '')
142
 
143
+ st.info(f"๐Ÿ’ก AI ํŒŒ์•… ์™„๋ฃŒ!\n- ๊ธฐ์—…๋ช…: **{name}**\n- ํ‹ฐ์ปค: **{ticker}**\n- ๊ฒ€์ƒ‰ ํ‚ค์›Œ๋“œ: **'{search_keyword}'**")
144
+
145
+ # 2. [์†๋ฐœ] ๋ฐ์ดํ„ฐ ๋ฐ ๋‰ด์Šค ์ˆ˜์ง‘
146
+ st.write("๐Ÿ“‰ ์ฐจํŠธ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ์ค‘...")
147
+ hist, src = get_market_data(ticker)
148
+
149
+ st.write(f"๐Ÿ“ฐ ๋‰ด์Šค ๊ฒ€์ƒ‰ ์ค‘ ('{search_keyword}'๋กœ ๊ฒ€์ƒ‰)...")
150
+ news_data = get_news(search_keyword)
151
+
152
+ # ๋ฐ์ดํ„ฐ ์ค€๋น„
153
+ curr, pct = "N/A", 0
154
+ if not hist.empty:
155
+ curr = hist['Close'].iloc[-1]
156
+ prev = hist['Close'].iloc[-2]
157
+ pct = ((curr - prev)/prev)*100
158
+ status.update(label="โœ… ๋ถ„์„ ์ค€๋น„ ์™„๋ฃŒ!", state="complete", expanded=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
159
  else:
160
+ status.update(label="โš ๏ธ ์ฐจํŠธ ๋ฐ์ดํ„ฐ ์—†์Œ (๋‰ด์Šค ๋ถ„์„ ์ง„ํ–‰)", state="complete", expanded=False)
 
 
 
 
 
 
 
 
 
 
 
 
 
161
 
162
+ st.divider()
 
 
 
 
 
 
 
 
 
 
163
 
164
+ # ๊ฒฐ๊ณผ ํ™”๋ฉด
165
+ m1, m2 = st.columns(2)
166
+ m1.metric("Current Price", f"{curr:,.0f}" if isinstance(curr, float) else curr, f"{pct:.2f}%")
167
+ m2.metric("Data Source", src)
168
+
169
+ if not hist.empty:
170
+ st.plotly_chart(plot_chart(hist, name, src), use_container_width=True)
171
  else:
172
+ st.warning("์ฐจํŠธ ๋ฐ์ดํ„ฐ๋ฅผ ๋ถˆ๋Ÿฌ์˜ค์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.")
173
+
174
+ with st.expander("๐Ÿ“ฐ AI๊ฐ€ ์ฝ์€ ๋‰ด์Šค ์›๋ฌธ ๋ณด๊ธฐ"):
175
+ st.text(news_data)
176
+
177
+ # 3. [์ž…] ์ตœ์ข… ๋ณด๊ณ 
178
+ st.subheader(f"๐Ÿง  AI Insight: {name}")
179
+ prompt = f"""
180
+ [ROLE] Global Investment Analyst.
181
+ [TARGET] {name} ({ticker})
182
+ [USER QUERY] "{user_query}"
183
+ [DATA] Price: {curr}, Trend: {pct:.2f}%
184
+ [NEWS CONTEXT] {news_data}
185
+
186
+ [TASK]
187
+ 1. Briefly explain what this company does (based on user query context).
188
+ 2. Analyze the recent news sentiment.
189
+ 3. Provide an investment verdict (Buy/Hold/Sell).
190
+
191
+ [LANGUAGE] Korean.
192
+ """
193
+ messages = [{"role": "user", "content": prompt}]
194
+ stream = client.chat.completions.create(model="Qwen/Qwen2.5-72B-Instruct", messages=messages, stream=True)
195
+ st.write_stream(parse_stream(stream))
196
+
197
+ else:
198
+ status.update(label="โŒ ์ธ์‹ ์‹คํŒจ", state="error")
199
+ st.error("AI๊ฐ€ ์–ด๋–ค ๊ธฐ์—…์ธ์ง€ ํŒŒ์•…ํ•˜์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค. ์กฐ๊ธˆ ๋” ๊ตฌ์ฒด์ ์œผ๋กœ ์ž…๋ ฅํ•ด์ฃผ์„ธ์š”.")