benjamin5607 commited on
Commit
cb71305
ยท
verified ยท
1 Parent(s): d19200a

Update streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +142 -72
streamlit_app.py CHANGED
@@ -30,7 +30,7 @@ UI_TEXT = {
30
  "menu_search": "๐Ÿ” AI ์Šค๋งˆํŠธ ๊ฒ€์ƒ‰",
31
  "menu_scanner": "๐Ÿ“ก ๊ธ€๋กœ๋ฒŒ ์‹œ์žฅ ์Šค์บ๋„ˆ",
32
  "search_title": "๐Ÿง  ๋‹ค๊ตญ์–ด AI ์ฃผ์‹ ๋น„์„œ",
33
- "search_placeholder": "์˜ˆ: ๋ฒ ํŠธ๋‚จ ๋น„๋‚˜๋ฐ€ํฌ, ์‚ผ์„ฑ์ „์ž...",
34
  "btn_analyze": "๋ถ„์„ ์‹œ์ž‘",
35
  "status_thinking": "๐Ÿง  AI๊ฐ€ ๋ถ„์„ ์ค‘์ž…๋‹ˆ๋‹ค...",
36
  "info_target": "ํƒ€๊ฒŸ",
@@ -41,11 +41,11 @@ UI_TEXT = {
41
  "metric_source": "์ถœ์ฒ˜",
42
  "expander_news": "๐Ÿ“ฐ ๋‰ด์Šค ์›๋ฌธ ๋ณด๊ธฐ",
43
  "insight_title": "๐Ÿง  AI ํˆฌ์ž ๋ฆฌํฌํŠธ",
44
- "scanner_title": "๐Ÿ“ก ๊ธ€๋กœ๋ฒŒ ์‹œ์žฅ ์ „๊ด‘ํŒ",
45
- "scanner_caption": "โ€ป ์‚ฌ๋žŒ์ฒ˜๋Ÿผ ์ฒœ์ฒœํžˆ ์ •๋ฐ€ ๊ฒ€์ƒ‰ํ•ฉ๋‹ˆ๋‹ค. (์†๋„: ๋А๋ฆผ)",
46
- "btn_scan": "์Šค์บ” ์‹œ์ž‘",
47
- "tab_gainers": "๐Ÿ”ฅ ๊ธ‰๋“ฑ (Top Gainers)",
48
- "tab_losers": "๐Ÿ’ง ๊ธ‰๋ฝ (Top Losers)",
49
  "col_name": "๊ธฐ์—…๋ช…",
50
  "col_price": "๊ฐ€๊ฒฉ",
51
  "col_change": "๋“ฑ๋ฝ๋ฅ ",
@@ -58,7 +58,7 @@ UI_TEXT = {
58
  "menu_search": "๐Ÿ” AI Smart Search",
59
  "menu_scanner": "๐Ÿ“ก Global Market Scanner",
60
  "search_title": "๐Ÿง  AI Stock Assistant",
61
- "search_placeholder": "e.g. Vinamilk, Samsung Electronics...",
62
  "btn_analyze": "Analyze",
63
  "status_thinking": "๐Ÿง  AI is analyzing...",
64
  "info_target": "Target",
@@ -69,11 +69,11 @@ UI_TEXT = {
69
  "metric_source": "Source",
70
  "expander_news": "๐Ÿ“ฐ View Source News",
71
  "insight_title": "๐Ÿง  AI Investment Report",
72
- "scanner_title": "๐Ÿ“ก Global Market Scanner",
73
- "scanner_caption": "โ€ป Deep search with human-like delay. (Speed: Slow)",
74
- "btn_scan": "Start Scan",
75
- "tab_gainers": "๐Ÿ”ฅ Top Gainers",
76
- "tab_losers": "๐Ÿ’ง Top Losers",
77
  "col_name": "Company",
78
  "col_price": "Price",
79
  "col_change": "Change(%)",
@@ -84,38 +84,85 @@ UI_TEXT = {
84
  }
85
 
86
  # ==============================================================================
87
- # ๐Ÿ’พ [๋ฐ์ดํ„ฐ] ํ‹ฐ์ปค ๋งคํ•‘
88
  # ==============================================================================
89
  TICKER_NAMES = {
 
90
  "VIC.VN": "Vingroup", "VHM.VN": "Vinhomes", "VCB.VN": "Vietcombank", "VNM.VN": "Vinamilk",
91
- "HPG.VN": "Hoa Phat", "MSN.VN": "Masan Group", "GAS.VN": "PV Gas",
92
- "BBCA.JK": "BCA Bank", "BBRI.JK": "BRI Bank", "TLKM.JK": "Telkom Indo", "BMRI.JK": "Mandiri",
93
- "ASII.JK": "Astra Intl", "GOTO.JK": "GoTo Group",
 
 
 
 
 
 
 
 
 
 
 
94
  "005930.KS": "Samsung Elec", "000660.KS": "SK Hynix", "035420.KS": "NAVER", "035720.KS": "Kakao",
95
- "005380.KS": "Hyundai Motor", "207940.KS": "Samsung Bio",
 
 
 
 
 
 
96
  "AAPL": "Apple", "NVDA": "NVIDIA", "TSLA": "Tesla", "AMZN": "Amazon", "MSFT": "Microsoft",
97
- "GOOGL": "Google", "META": "Meta", "AMD": "AMD",
 
 
 
 
 
 
 
98
  "7203.T": "Toyota", "6758.T": "Sony", "9984.T": "SoftBank", "8035.T": "Tokyo Elec",
99
- "6861.T": "Keyence", "7974.T": "Nintendo",
100
- "9988.HK": "Alibaba", "0700.HK": "Tencent", "3690.HK": "Meituan", "1211.HK": "BYD", "1810.HK": "Xiaomi",
 
 
 
 
 
 
101
  "2330.TW": "TSMC", "2454.TW": "MediaTek", "2317.TW": "Foxconn", "2308.TW": "Delta Elec",
102
- "RELIANCE.NS": "Reliance Ind", "TCS.NS": "TCS", "HDFCBANK.NS": "HDFC Bank", "INFY.NS": "Infosys"
103
  }
104
 
105
  MARKET_SAMPLES = {
106
- "๐Ÿ‡ป๐Ÿ‡ณ Vietnam": ["VIC.VN", "VHM.VN", "VCB.VN", "VNM.VN", "HPG.VN", "MSN.VN", "GAS.VN"],
107
- "๐Ÿ‡ฎ๐Ÿ‡ฉ Indonesia": ["BBCA.JK", "BBRI.JK", "TLKM.JK", "BMRI.JK", "ASII.JK", "GOTO.JK"],
108
- "๐Ÿ‡ฐ๐Ÿ‡ท Korea": ["005930.KS", "000660.KS", "035420.KS", "035720.KS", "005380.KS", "207940.KS"],
109
- "๐Ÿ‡บ๐Ÿ‡ธ USA": ["AAPL", "NVDA", "TSLA", "AMZN", "MSFT", "GOOGL", "META", "AMD"],
110
- "๐Ÿ‡ฏ๐Ÿ‡ต Japan": ["7203.T", "6758.T", "9984.T", "8035.T", "6861.T", "7974.T"],
111
- "๐Ÿ‡จ๐Ÿ‡ณ China/HK": ["9988.HK", "0700.HK", "3690.HK", "1211.HK", "1810.HK"],
112
- "๐Ÿ‡น๐Ÿ‡ผ Taiwan": ["2330.TW", "2454.TW", "2317.TW", "2308.TW"],
113
- "๐Ÿ‡ฎ๐Ÿ‡ณ India": ["RELIANCE.NS", "TCS.NS", "HDFCBANK.NS", "INFY.NS"]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  }
115
 
116
  # ==============================================================================
117
- # ๐Ÿ•ธ๏ธ [์—”์ง„] ๋‰ด์Šค ๋ฐ ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘
118
  # ==============================================================================
 
 
119
  def fetch_news_robust(keyword):
120
  summary = ""
121
  seen_urls = set()
@@ -128,38 +175,36 @@ def fetch_news_robust(keyword):
128
  if not results:
129
  time.sleep(random.uniform(0.5, 1.0))
130
  results = list(ddgs.text(f"{keyword} latest news", max_results=3))
131
-
132
  for r in results:
133
  url = r.get('url') or r.get('href')
134
  if url not in seen_urls:
135
- date = r.get('date', 'Unknown')
136
  title = r.get('title', '')
137
  summary += f"[{date}] {title}\n"
138
  seen_urls.add(url)
139
- except Exception as e: return f"News Error: {e}"
140
- return summary if summary else "No news found."
141
 
142
  def scrape_price_from_web(name, ticker):
143
  try:
144
- time.sleep(random.uniform(1.0, 1.5))
145
  with DDGS() as ddgs:
146
  query = f"{name} {ticker} stock price quote today"
147
  results = ddgs.text(query, max_results=2)
148
  blob = " ".join([r['body'] for r in results])
149
  match = re.search(r'(\d{1,3}(,\d{3})*(\.\d+)?)', blob)
150
- if match:
151
- price = float(match.group(0).replace(",", ""))
152
- return price, 0.0
153
- return None, None
154
- except: return None, None
155
 
156
  def get_price_data_robust(ticker, name):
 
157
  try:
158
  fdr_symbol = ticker
159
  if ".VN" in ticker: fdr_symbol = ticker.split('.')[0]
160
  elif ".JK" in ticker: fdr_symbol = f"IDX:{ticker.split('.')[0]}"
161
  elif ".KS" in ticker or ".KQ" in ticker: fdr_symbol = ticker.split('.')[0]
162
- hist = fdr.DataReader(fdr_symbol, start=(datetime.datetime.now() - datetime.timedelta(days=10)))
163
  if not hist.empty:
164
  curr = hist['Close'].iloc[-1]
165
  prev = hist['Close'].iloc[-2] if len(hist) > 1 else curr
@@ -167,18 +212,16 @@ def get_price_data_robust(ticker, name):
167
  if prev != 0: pct = ((curr - prev)/prev)*100
168
  return curr, pct, "FDR"
169
  except: pass
170
-
171
  try:
172
  stock = yf.Ticker(ticker)
173
  price = stock.fast_info.last_price
174
  prev = stock.fast_info.previous_close
175
- if price and prev:
176
- pct = ((price - prev)/prev)*100
177
- return price, pct, "Yahoo"
178
  except: pass
179
-
180
  price, pct = scrape_price_from_web(name, ticker)
181
- if price is not None: return price, pct, "Web Search"
182
  return None, None, "Fail"
183
 
184
  def get_chart_data(ticker):
@@ -201,7 +244,6 @@ def identify_target_with_ai(user_query):
201
  try:
202
  prompt = f"""
203
  [ROLE] Entity Resolver. [QUERY] "{user_query}"
204
- [TASK] Identify company, ticker, dual keywords.
205
  [OUTPUT JSON] {{ "name": "Eng Name", "ticker": "TICKER", "eng_key": "Name stock news", "native_key": "Local Name + news keyword" }}
206
  """
207
  messages = [{"role": "user", "content": prompt}]
@@ -211,39 +253,26 @@ def identify_target_with_ai(user_query):
211
  except: return None
212
 
213
  def get_polyglot_news(eng_key, native_key):
214
- summary = "=== Global ===\n" + fetch_news_robust(eng_key)
215
- if eng_key != native_key:
216
- summary += "\n\n=== Local ===\n" + fetch_news_robust(native_key)
217
- return summary
218
 
219
  def parse_stream(stream):
220
  for chunk in stream:
221
  if chunk.choices: yield chunk.choices[0].delta.content or ""
222
 
223
- # ==============================================================================
224
- # ๐Ÿ“Š [์‹œ๊ฐํ™”] ์ฐจํŠธ ํ•จ์ˆ˜ (๋ฒ„๊ทธ ์ˆ˜์ •๋จ)
225
- # ==============================================================================
226
  def plot_candle_chart(hist, title, source):
227
  fig = go.Figure(data=[go.Candlestick(x=hist.index, open=hist['Open'], high=hist['High'], low=hist['Low'], close=hist['Close'], name="Price")])
228
  fig.update_layout(title=f"{title} ({source})", height=350, margin=dict(l=10, r=10, t=30, b=10), template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)')
229
  return fig
230
 
231
  def plot_bar_chart(df, lang_data):
232
- # [์ˆ˜์ •] df['Name'] ๋Œ€์‹  ํ˜„์žฌ ์–ธ์–ด ์„ค์ •์— ๋งž๋Š” ์ปฌ๋Ÿผ(lang_data['col_name']) ์‚ฌ์šฉ
233
  target_col = lang_data['col_name']
234
-
235
  colors = ['#00FF00' if x > 0 else '#FF0000' for x in df['Change(%)']]
236
- fig = go.Figure(go.Bar(
237
- x=df[target_col], # <--- ์—ฌ๊ธฐ์„œ ์—๋Ÿฌ๊ฐ€ ๋‚ฌ์—ˆ์Šต๋‹ˆ๋‹ค. ๋™์  ์ปฌ๋Ÿผ๋ช… ์‚ฌ์šฉ!
238
- y=df['Change(%)'],
239
- marker_color=colors,
240
- text=df['Change(%)'].apply(lambda x: f"{x:.2f}%")
241
- ))
242
  fig.update_layout(title="Market Heatmap", height=350, margin=dict(l=10, r=10, t=30, b=10), template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)')
243
  return fig
244
 
245
  # ==============================================================================
246
- # ๐Ÿ–ฅ๏ธ UI & Sidebar
247
  # ==============================================================================
248
  with st.sidebar:
249
  lang_code = st.selectbox("Language / ์–ธ์–ด", ["KR", "EN"])
@@ -264,7 +293,6 @@ if menu == T['menu_search']:
264
  if info:
265
  name, ticker = info.get('name'), info.get('ticker')
266
  eng_key, native_key = info.get('eng_key'), info.get('native_key')
267
-
268
  st.info(f"๐Ÿ’ก {T['info_target']}: **{name} ({ticker})**")
269
 
270
  h, src = get_chart_data(ticker)
@@ -301,8 +329,7 @@ if menu == T['menu_search']:
301
  prompt = f"""
302
  [ROLE] Analyst. [TARGET] {name} ({ticker}). [QUERY] "{query}"
303
  [DATA] Price:{curr}, Trend:{pct:.2f}%. [NEWS] {news_data}
304
- [TASK] Summarize, Sentiment, Verdict.
305
- [OUTPUT LANGUAGE] **{target_lang}** (Strictly output in {target_lang})
306
  """
307
  msg = [{"role": "user", "content": prompt}]
308
  stream = client.chat.completions.create(model="Qwen/Qwen2.5-72B-Instruct", messages=msg, stream=True)
@@ -322,9 +349,38 @@ elif menu == T['menu_scanner']:
322
  results = []
323
  bar = st.progress(0)
324
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
325
  for i, t in enumerate(tickers):
326
  name_display = TICKER_NAMES.get(t, t)
327
- p, c, s = get_price_data_robust(t, name_display)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
328
  if p is not None:
329
  results.append({
330
  T['col_name']: name_display,
@@ -335,19 +391,33 @@ elif menu == T['menu_scanner']:
335
  })
336
  bar.progress((i+1)/len(tickers))
337
 
 
 
338
  if results:
339
  df = pd.DataFrame(results).sort_values('Change(%)', ascending=False)
340
-
341
- # [์ˆ˜์ •] ์ฐจํŠธ ํ•จ์ˆ˜์— ํ˜„์žฌ ์–ธ์–ด์„ค์ •(T)์„ ์ „๋‹ฌ
342
  st.plotly_chart(plot_bar_chart(df, T), use_container_width=True)
343
 
344
  disp_cols = [T['col_name'], 'Ticker', T['col_price'], 'Change(%)', T['col_source']]
345
 
 
 
 
 
 
 
 
346
  c_up, c_down = st.columns(2)
347
  with c_up:
348
  st.success(T['tab_gainers'])
349
- st.dataframe(df.head(10)[disp_cols].style.format({T['col_price']: "{:,.2f}", "Change(%)": "{:,.2f}%"}), use_container_width=True)
 
 
 
 
350
  with c_down:
351
  st.error(T['tab_losers'])
352
- st.dataframe(df.tail(10)[disp_cols].sort_values('Change(%)').style.format({T['col_price']: "{:,.2f}", "Change(%)": "{:,.2f}%"}), use_container_width=True)
 
 
 
353
  else: st.warning(T['msg_fail'])
 
30
  "menu_search": "๐Ÿ” AI ์Šค๋งˆํŠธ ๊ฒ€์ƒ‰",
31
  "menu_scanner": "๐Ÿ“ก ๊ธ€๋กœ๋ฒŒ ์‹œ์žฅ ์Šค์บ๋„ˆ",
32
  "search_title": "๐Ÿง  ๋‹ค๊ตญ์–ด AI ์ฃผ์‹ ๋น„์„œ",
33
+ "search_placeholder": "์˜ˆ: ๋ฒ ํŠธ๋‚จ ์บ‰๋””์—”, ๋ฏธ๊ตญ ํŒ”๋ž€ํ‹ฐ์–ด...",
34
  "btn_analyze": "๋ถ„์„ ์‹œ์ž‘",
35
  "status_thinking": "๐Ÿง  AI๊ฐ€ ๋ถ„์„ ์ค‘์ž…๋‹ˆ๋‹ค...",
36
  "info_target": "ํƒ€๊ฒŸ",
 
41
  "metric_source": "์ถœ์ฒ˜",
42
  "expander_news": "๐Ÿ“ฐ ๋‰ด์Šค ์›๋ฌธ ๋ณด๊ธฐ",
43
  "insight_title": "๐Ÿง  AI ํˆฌ์ž ๋ฆฌํฌํŠธ",
44
+ "scanner_title": "๐Ÿ“ก ๊ธ€๋กœ๋ฒŒ ์‹œ์žฅ ์ „๊ด‘ํŒ (Max 100)",
45
+ "scanner_caption": "โ€ป ์•ฝ 100๊ฐœ ์ข…๋ชฉ์„ 1์ฐจ ๊ณ ์† ์Šค์บ” ํ›„, ์‹คํŒจ ์ข…๋ชฉ๋งŒ ์ •๋ฐ€ ์Šค์บ”ํ•ฉ๋‹ˆ๋‹ค.",
46
+ "btn_scan": "๊ด‘๋Œ€์—ญ ์Šค์บ” ์‹œ์ž‘",
47
+ "tab_gainers": "๐Ÿ”ฅ ๊ธ‰๋“ฑ (Pure Gainers)",
48
+ "tab_losers": "๐Ÿ’ง ๊ธ‰๋ฝ (Pure Losers)",
49
  "col_name": "๊ธฐ์—…๋ช…",
50
  "col_price": "๊ฐ€๊ฒฉ",
51
  "col_change": "๋“ฑ๋ฝ๋ฅ ",
 
58
  "menu_search": "๐Ÿ” AI Smart Search",
59
  "menu_scanner": "๐Ÿ“ก Global Market Scanner",
60
  "search_title": "๐Ÿง  AI Stock Assistant",
61
+ "search_placeholder": "e.g. Vinamilk, Palantir...",
62
  "btn_analyze": "Analyze",
63
  "status_thinking": "๐Ÿง  AI is analyzing...",
64
  "info_target": "Target",
 
69
  "metric_source": "Source",
70
  "expander_news": "๐Ÿ“ฐ View Source News",
71
  "insight_title": "๐Ÿง  AI Investment Report",
72
+ "scanner_title": "๐Ÿ“ก Global Market Scanner (Max 100)",
73
+ "scanner_caption": "โ€ป Hybrid Scanning (Batch High-Speed + Deep Robust).",
74
+ "btn_scan": "Start Full Scan",
75
+ "tab_gainers": "๐Ÿ”ฅ Top Gainers (>0%)",
76
+ "tab_losers": "๐Ÿ’ง Top Losers (<0%)",
77
  "col_name": "Company",
78
  "col_price": "Price",
79
  "col_change": "Change(%)",
 
84
  }
85
 
86
  # ==============================================================================
87
+ # ๐Ÿ’พ [๋ฐ์ดํ„ฐ] ๋Œ€๊ทœ๋ชจ ํ™•์žฅ ํ‹ฐ์ปค (์•ฝ 100๊ฐœ ๊ทœ๋ชจ)
88
  # ==============================================================================
89
  TICKER_NAMES = {
90
+ # ๐Ÿ‡ป๐Ÿ‡ณ ๋ฒ ํŠธ๋‚จ
91
  "VIC.VN": "Vingroup", "VHM.VN": "Vinhomes", "VCB.VN": "Vietcombank", "VNM.VN": "Vinamilk",
92
+ "HPG.VN": "Hoa Phat", "MSN.VN": "Masan Group", "GAS.VN": "PV Gas", "NVL.VN": "Novaland",
93
+ "PDR.VN": "Phat Dat", "DIG.VN": "DIC Corp", "CEO.VN": "CEO Group", "SSI.VN": "SSI Sec",
94
+ "VND.VN": "VNDirect", "MWG.VN": "Mobile World", "FRT.VN": "FPT Retail", "FPT.VN": "FPT Corp",
95
+ "STB.VN": "Sacombank", "MBB.VN": "MB Bank", "TCB.VN": "Techcombank", "VPB.VN": "VPBank",
96
+ "VRE.VN": "Vincom Retail", "DGC.VN": "Duc Giang Chem", "VHC.VN": "Vinh Hoan", "KBC.VN": "Kinh Bac",
97
+ "GVR.VN": "Vietnam Rubber", "SAB.VN": "Sabeco", "BID.VN": "BIDV", "CTG.VN": "VietinBank",
98
+
99
+ # ๐Ÿ‡ฎ๐Ÿ‡ฉ ์ธ๋„๋„ค์‹œ์•„
100
+ "BBCA.JK": "BCA Bank", "BBRI.JK": "BRI Bank", "TLKM.JK": "Telkom", "BMRI.JK": "Mandiri",
101
+ "ASII.JK": "Astra Intl", "GOTO.JK": "GoTo", "UNVR.JK": "Unilever", "ADRO.JK": "Adaro Energy",
102
+ "MDKA.JK": "Merdeka Copper", "ANTM.JK": "Aneka Tambang", "ICBP.JK": "Indofood CBP",
103
+ "KLBF.JK": "Kalbe Farma", "BUKA.JK": "Bukalapak", "PGAS.JK": "Perusahaan Gas",
104
+
105
+ # ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ
106
  "005930.KS": "Samsung Elec", "000660.KS": "SK Hynix", "035420.KS": "NAVER", "035720.KS": "Kakao",
107
+ "005380.KS": "Hyundai Motor", "207940.KS": "Samsung Bio", "068270.KS": "Celltrion",
108
+ "086520.KQ": "Ecopro", "247540.KQ": "Ecopro BM", "005490.KS": "POSCO Holdings", "010120.KS": "LS ELECTRIC",
109
+ "012450.KS": "Hanwha Aero", "042700.KS": "Hanmi Semi", "196170.KQ": "Alteogen",
110
+ "352820.KS": "HYBE", "035900.KQ": "JYP Ent", "000270.KS": "Kia", "010950.KS": "S-Oil",
111
+ "032830.KQ": "Samsung Life", "329180.KS": "Hyundai Heavy", "015760.KS": "KEPCO",
112
+
113
+ # ๐Ÿ‡บ๐Ÿ‡ธ ๋ฏธ๊ตญ (M7 + ์„ฑ์žฅ + ๋ฐ”์ด์˜ค + ์—๋„ˆ์ง€)
114
  "AAPL": "Apple", "NVDA": "NVIDIA", "TSLA": "Tesla", "AMZN": "Amazon", "MSFT": "Microsoft",
115
+ "GOOGL": "Google", "META": "Meta", "AMD": "AMD", "NFLX": "Netflix", "INTC": "Intel",
116
+ "PLTR": "Palantir", "COIN": "Coinbase", "U": "Unity", "RBLX": "Roblox", "SOFI": "SoFi",
117
+ "PYPL": "PayPal", "SQ": "Block", "UBER": "Uber", "ABNB": "Airbnb", "HOOD": "Robinhood",
118
+ "LLY": "Eli Lilly", "NVO": "Novo Nordisk", "PFE": "Pfizer", "MRK": "Merck",
119
+ "XOM": "Exxon", "CVX": "Chevron", "KO": "Coca-Cola", "PEP": "Pepsi", "COST": "Costco",
120
+ "DIS": "Disney", "NKE": "Nike", "SBUX": "Starbucks", "MCD": "McDonalds",
121
+
122
+ # ๐Ÿ‡ฏ๐Ÿ‡ต ์ผ๋ณธ
123
  "7203.T": "Toyota", "6758.T": "Sony", "9984.T": "SoftBank", "8035.T": "Tokyo Elec",
124
+ "6861.T": "Keyence", "7974.T": "Nintendo", "8306.T": "MUFG", "6501.T": "Hitachi",
125
+ "8058.T": "Mitsubishi", "8001.T": "Itochu", "8031.T": "Mitsui", "6920.T": "Lasertec",
126
+ "4063.T": "Shin-Etsu", "7741.T": "HOYA", "6146.T": "Disco", "7267.T": "Honda",
127
+
128
+ # ๐Ÿ‡จ๐Ÿ‡ณ ์ค‘๊ตญ/ํ™์ฝฉ/๋Œ€๋งŒ/์ธ๋„
129
+ "9988.HK": "Alibaba", "0700.HK": "Tencent", "3690.HK": "Meituan", "1211.HK": "BYD",
130
+ "1810.HK": "Xiaomi", "0941.HK": "China Mobile", "9888.HK": "Baidu", "9618.HK": "JD.com",
131
+ "2015.HK": "Li Auto", "9868.HK": "Xpeng", "0981.HK": "SMIC",
132
  "2330.TW": "TSMC", "2454.TW": "MediaTek", "2317.TW": "Foxconn", "2308.TW": "Delta Elec",
133
+ "RELIANCE.NS": "Reliance", "TCS.NS": "TCS", "HDFCBANK.NS": "HDFC Bank", "INFY.NS": "Infosys"
134
  }
135
 
136
  MARKET_SAMPLES = {
137
+ "๐Ÿ‡ป๐Ÿ‡ณ Vietnam (Max 50)": [
138
+ "VIC.VN", "VHM.VN", "VRE.VN", "VNM.VN", "MSN.VN", "GAS.VN", "HPG.VN", "HSG.VN",
139
+ "NVL.VN", "PDR.VN", "DIG.VN", "CEO.VN", "DXG.VN", "KBC.VN", "GVR.VN",
140
+ "VCB.VN", "TCB.VN", "VPB.VN", "MBB.VN", "STB.VN", "ACB.VN", "BID.VN", "CTG.VN",
141
+ "SSI.VN", "VND.VN", "VCI.VN", "MWG.VN", "FRT.VN", "DGW.VN", "FPT.VN", "DGC.VN", "VHC.VN", "SAB.VN"
142
+ ],
143
+ "๐Ÿ‡บ๐Ÿ‡ธ USA (Max 50)": [
144
+ "AAPL", "MSFT", "GOOGL", "AMZN", "NVDA", "META", "TSLA", "AMD", "INTC", "QCOM",
145
+ "AVGO", "MU", "PLTR", "COIN", "U", "RBLX", "SOFI", "UBER", "ABNB", "HOOD", "PYPL", "SQ",
146
+ "LLY", "NVO", "JNJ", "PFE", "MRK", "UNH", "XOM", "CVX", "JPM", "V", "MA",
147
+ "KO", "PEP", "COST", "WMT", "DIS", "NKE", "SBUX", "MCD"
148
+ ],
149
+ "๐Ÿ‡ฐ๐Ÿ‡ท Korea (Max 50)": [
150
+ "005930.KS", "000660.KS", "005380.KS", "000270.KS", "035420.KS", "035720.KS",
151
+ "005490.KS", "086520.KQ", "247540.KQ", "010120.KS", "207940.KS", "068270.KS",
152
+ "196170.KQ", "012450.KS", "042700.KS", "352820.KS", "010950.KS", "032830.KQ", "329180.KS", "015760.KS"
153
+ ],
154
+ "๐Ÿ‡ฎ๐Ÿ‡ฉ Indonesia": ["BBCA.JK", "BBRI.JK", "BMRI.JK", "BBNI.JK", "TLKM.JK", "ASII.JK", "GOTO.JK", "ADRO.JK", "MDKA.JK", "ANTM.JK", "UNVR.JK", "ICBP.JK", "KLBF.JK", "BUKA.JK", "PGAS.JK"],
155
+ "๐Ÿ‡ฏ๐Ÿ‡ต Japan": ["7203.T", "6758.T", "9984.T", "8035.T", "6861.T", "7974.T", "8306.T", "6501.T", "8058.T", "8001.T", "8031.T", "6920.T", "4063.T", "7741.T", "6146.T", "7267.T"],
156
+ "๐Ÿ‡จ๐Ÿ‡ณ China/HK": ["9988.HK", "0700.HK", "3690.HK", "1211.HK", "1810.HK", "0941.HK", "9888.HK", "9618.HK", "2015.HK", "9868.HK", "0981.HK"],
157
+ "๐Ÿ‡น๐Ÿ‡ผ Taiwan": ["2330.TW", "2454.TW", "2317.TW", "2308.TW", "2603.TW", "2881.TW", "1301.TW"],
158
+ "๐Ÿ‡ฎ๐Ÿ‡ณ India": ["RELIANCE.NS", "TCS.NS", "HDFCBANK.NS", "INFY.NS", "ICICIBANK.NS", "TATAMOTORS.NS", "BHARTIARTL.NS"]
159
  }
160
 
161
  # ==============================================================================
162
+ # ๐Ÿ› ๏ธ [์—”์ง„] ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํ•จ์ˆ˜๋“ค
163
  # ==============================================================================
164
+ FAKE_HEADERS = {'User-Agent': 'Mozilla/5.0 ...'}
165
+
166
  def fetch_news_robust(keyword):
167
  summary = ""
168
  seen_urls = set()
 
175
  if not results:
176
  time.sleep(random.uniform(0.5, 1.0))
177
  results = list(ddgs.text(f"{keyword} latest news", max_results=3))
 
178
  for r in results:
179
  url = r.get('url') or r.get('href')
180
  if url not in seen_urls:
181
+ date = r.get('date', '?')
182
  title = r.get('title', '')
183
  summary += f"[{date}] {title}\n"
184
  seen_urls.add(url)
185
+ except: return "No news."
186
+ return summary if summary else "No news."
187
 
188
  def scrape_price_from_web(name, ticker):
189
  try:
190
+ time.sleep(random.uniform(0.5, 1.0))
191
  with DDGS() as ddgs:
192
  query = f"{name} {ticker} stock price quote today"
193
  results = ddgs.text(query, max_results=2)
194
  blob = " ".join([r['body'] for r in results])
195
  match = re.search(r'(\d{1,3}(,\d{3})*(\.\d+)?)', blob)
196
+ if match: return float(match.group(0).replace(",", "")), 0.0
197
+ except: pass
198
+ return None, None
 
 
199
 
200
  def get_price_data_robust(ticker, name):
201
+ # FDR
202
  try:
203
  fdr_symbol = ticker
204
  if ".VN" in ticker: fdr_symbol = ticker.split('.')[0]
205
  elif ".JK" in ticker: fdr_symbol = f"IDX:{ticker.split('.')[0]}"
206
  elif ".KS" in ticker or ".KQ" in ticker: fdr_symbol = ticker.split('.')[0]
207
+ hist = fdr.DataReader(fdr_symbol, start=(datetime.datetime.now() - datetime.timedelta(days=7)))
208
  if not hist.empty:
209
  curr = hist['Close'].iloc[-1]
210
  prev = hist['Close'].iloc[-2] if len(hist) > 1 else curr
 
212
  if prev != 0: pct = ((curr - prev)/prev)*100
213
  return curr, pct, "FDR"
214
  except: pass
215
+ # Yahoo
216
  try:
217
  stock = yf.Ticker(ticker)
218
  price = stock.fast_info.last_price
219
  prev = stock.fast_info.previous_close
220
+ if price and prev: return price, ((price - prev)/prev)*100, "Yahoo"
 
 
221
  except: pass
222
+ # Web
223
  price, pct = scrape_price_from_web(name, ticker)
224
+ if price is not None: return price, pct, "Web"
225
  return None, None, "Fail"
226
 
227
  def get_chart_data(ticker):
 
244
  try:
245
  prompt = f"""
246
  [ROLE] Entity Resolver. [QUERY] "{user_query}"
 
247
  [OUTPUT JSON] {{ "name": "Eng Name", "ticker": "TICKER", "eng_key": "Name stock news", "native_key": "Local Name + news keyword" }}
248
  """
249
  messages = [{"role": "user", "content": prompt}]
 
253
  except: return None
254
 
255
  def get_polyglot_news(eng_key, native_key):
256
+ return f"Global:\n{fetch_news_robust(eng_key)}\nLocal:\n{fetch_news_robust(native_key)}"
 
 
 
257
 
258
  def parse_stream(stream):
259
  for chunk in stream:
260
  if chunk.choices: yield chunk.choices[0].delta.content or ""
261
 
 
 
 
262
  def plot_candle_chart(hist, title, source):
263
  fig = go.Figure(data=[go.Candlestick(x=hist.index, open=hist['Open'], high=hist['High'], low=hist['Low'], close=hist['Close'], name="Price")])
264
  fig.update_layout(title=f"{title} ({source})", height=350, margin=dict(l=10, r=10, t=30, b=10), template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)')
265
  return fig
266
 
267
  def plot_bar_chart(df, lang_data):
 
268
  target_col = lang_data['col_name']
 
269
  colors = ['#00FF00' if x > 0 else '#FF0000' for x in df['Change(%)']]
270
+ fig = go.Figure(go.Bar(x=df[target_col], y=df['Change(%)'], marker_color=colors, text=df['Change(%)'].apply(lambda x: f"{x:.2f}%")))
 
 
 
 
 
271
  fig.update_layout(title="Market Heatmap", height=350, margin=dict(l=10, r=10, t=30, b=10), template="plotly_dark", paper_bgcolor='rgba(0,0,0,0)')
272
  return fig
273
 
274
  # ==============================================================================
275
+ # UI
276
  # ==============================================================================
277
  with st.sidebar:
278
  lang_code = st.selectbox("Language / ์–ธ์–ด", ["KR", "EN"])
 
293
  if info:
294
  name, ticker = info.get('name'), info.get('ticker')
295
  eng_key, native_key = info.get('eng_key'), info.get('native_key')
 
296
  st.info(f"๐Ÿ’ก {T['info_target']}: **{name} ({ticker})**")
297
 
298
  h, src = get_chart_data(ticker)
 
329
  prompt = f"""
330
  [ROLE] Analyst. [TARGET] {name} ({ticker}). [QUERY] "{query}"
331
  [DATA] Price:{curr}, Trend:{pct:.2f}%. [NEWS] {news_data}
332
+ [TASK] Summarize, Sentiment, Verdict. [LANG] {target_lang}.
 
333
  """
334
  msg = [{"role": "user", "content": prompt}]
335
  stream = client.chat.completions.create(model="Qwen/Qwen2.5-72B-Instruct", messages=msg, stream=True)
 
349
  results = []
350
  bar = st.progress(0)
351
 
352
+ # [์ตœ์ ํ™”] 1์ฐจ: Batch Download (๋น ๋ฆ„)
353
+ # 100๊ฐœ์”ฉ ๋‚˜๋ˆ ์„œ ๋‹ค์šด๋กœ๋“œ (Yfinance ์ œํ•œ ๊ณ ๋ ค)
354
+ status_text = st.empty()
355
+ status_text.text("๐Ÿš€ Phase 1: Batch Speed Scan...")
356
+
357
+ try:
358
+ # Batch๋กœ ๊ธ์–ด์˜ค๊ธฐ
359
+ batch_data = yf.download(tickers, period="5d", progress=False)['Close']
360
+ except:
361
+ batch_data = pd.DataFrame()
362
+
363
+ # 2์ฐจ: ์‹คํŒจํ•œ ์ข…๋ชฉ๋งŒ Robust Scan (FDR/Web)
364
+ status_text.text("๐Ÿ•ต๏ธ Phase 2: Deep Robust Scan...")
365
+
366
  for i, t in enumerate(tickers):
367
  name_display = TICKER_NAMES.get(t, t)
368
+ p, c, s = None, None, None
369
+
370
+ # 1. Batch ๊ฒฐ๊ณผ ํ™•์ธ
371
+ if not batch_data.empty and t in batch_data.columns:
372
+ series = batch_data[t].dropna()
373
+ if len(series) >= 2:
374
+ p = series.iloc[-1]
375
+ prev = series.iloc[-2]
376
+ if prev != 0:
377
+ c = ((p - prev)/prev)*100
378
+ s = "Yahoo (Batch)"
379
+
380
+ # 2. Batch ์‹คํŒจ ์‹œ Robust ์‹คํ–‰
381
+ if p is None:
382
+ p, c, s = get_price_data_robust(t, name_display)
383
+
384
  if p is not None:
385
  results.append({
386
  T['col_name']: name_display,
 
391
  })
392
  bar.progress((i+1)/len(tickers))
393
 
394
+ status_text.empty()
395
+
396
  if results:
397
  df = pd.DataFrame(results).sort_values('Change(%)', ascending=False)
 
 
398
  st.plotly_chart(plot_bar_chart(df, T), use_container_width=True)
399
 
400
  disp_cols = [T['col_name'], 'Ticker', T['col_price'], 'Change(%)', T['col_source']]
401
 
402
+ # [ํ•„ํ„ฐ๋ง ๋กœ์ง ๊ฐœ์„ ]
403
+ # Gainers: 0๋ณด๋‹ค ํฐ ๊ฒƒ๋งŒ, ์•ž์—์„œ 10๊ฐœ
404
+ df_gainers = df[df['Change(%)'] > 0].head(10)
405
+
406
+ # Losers: 0๋ณด๋‹ค ์ž‘์€ ๊ฒƒ๋งŒ, ๋’ค์—์„œ๋ถ€ํ„ฐ ์ •๋ ฌํ•ด์„œ 10๊ฐœ (๊ฐ€์žฅ ๋งŽ์ด ๋–จ์–ด์ง„ ์ˆœ์„œ)
407
+ df_losers = df[df['Change(%)'] < 0].sort_values('Change(%)', ascending=True).head(10)
408
+
409
  c_up, c_down = st.columns(2)
410
  with c_up:
411
  st.success(T['tab_gainers'])
412
+ if not df_gainers.empty:
413
+ st.dataframe(df_gainers[disp_cols].style.format({T['col_price']: "{:,.2f}", "Change(%)": "{:,.2f}%"}), use_container_width=True)
414
+ else:
415
+ st.info("No gainers found.")
416
+
417
  with c_down:
418
  st.error(T['tab_losers'])
419
+ if not df_losers.empty:
420
+ st.dataframe(df_losers[disp_cols].style.format({T['col_price']: "{:,.2f}", "Change(%)": "{:,.2f}%"}), use_container_width=True)
421
+ else:
422
+ st.info("No losers found.")
423
  else: st.warning(T['msg_fail'])