benjamin5607 commited on
Commit
5f57416
ยท
verified ยท
1 Parent(s): 135647f

Update streamlit_app.py

Browse files
Files changed (1) hide show
  1. streamlit_app.py +90 -84
streamlit_app.py CHANGED
@@ -38,9 +38,9 @@ UI_TEXT = {
38
  "metric_source": "์ถœ์ฒ˜",
39
  "expander_news": "๐Ÿ“ฐ ๋‰ด์Šค ์›๋ฌธ ๋ณด๊ธฐ",
40
  "insight_title": "๐Ÿง  AI ์ข…ํ•ฉ ๋ถ„์„ ๋ฆฌํฌํŠธ",
41
- "scanner_title": "๐Ÿ“ก ๊ธ€๋กœ๋ฒŒ ์‹œ์žฅ ์ „๊ด‘ํŒ (Max 100)",
42
- "scanner_caption": "โ€ป ์•ฝ 100๊ฐœ ๊ธฐ์—…์„ ๋ถ„์„ํ•˜์—ฌ ์ƒ์œ„ 10๊ฐœ๋ฅผ ์„ ๋ณ„ํ•ฉ๋‹ˆ๋‹ค.",
43
- "btn_scan": "100๊ฐœ ์ข…๋ชฉ ์Šค์บ” ์‹œ์ž‘",
44
  "tab_gainers": "๐Ÿ”ฅ ๊ธ‰๋“ฑ Top 10 (์ƒ์Šน์žฅ)",
45
  "tab_losers": "๐Ÿ’ง ๊ธ‰๋ฝ Top 10 (ํ•˜๋ฝ์žฅ)",
46
  "col_name": "๊ธฐ์—…๋ช…",
@@ -63,9 +63,9 @@ UI_TEXT = {
63
  "metric_source": "Source",
64
  "expander_news": "๐Ÿ“ฐ View Source News",
65
  "insight_title": "๐Ÿง  AI Comprehensive Report",
66
- "scanner_title": "๐Ÿ“ก Global Market Scanner (Max 100)",
67
- "scanner_caption": "โ€ป Analyzing 100 companies to show Top 10.",
68
- "btn_scan": "Scan 100 Stocks",
69
  "tab_gainers": "๐Ÿ”ฅ Top 10 Gainers",
70
  "tab_losers": "๐Ÿ’ง Top 10 Losers",
71
  "col_name": "Company",
@@ -78,35 +78,87 @@ UI_TEXT = {
78
  }
79
 
80
  # ==============================================================================
81
- # ๐Ÿ’พ [๋ฐ์ดํ„ฐ] ํ‹ฐ์ปค ๋งคํ•‘ (์Šค์บ๋„ˆ์šฉ) - ๊ฒ€์ƒ‰์€ AI๊ฐ€ ์•Œ์•„์„œ ํ•จ
82
  # ==============================================================================
83
- # (์Šค์บ๋„ˆ์šฉ ๋ฆฌ์ŠคํŠธ๋Š” ๊ธฐ์กด ์œ ์ง€ - ๋„ˆ๋ฌด ๊ธธ์–ด์„œ ์ƒ๋žตํ•˜์ง€ ์•Š๊ณ  ๊ทธ๋Œ€๋กœ ๋‘ก๋‹ˆ๋‹ค)
84
  TICKER_NAMES = {
 
85
  "VIC.VN": "Vingroup", "VHM.VN": "Vinhomes", "VCB.VN": "Vietcombank", "VNM.VN": "Vinamilk",
86
- "HPG.VN": "Hoa Phat", "MSN.VN": "Masan Group", "GAS.VN": "PV Gas", "NVL.VN": "Novaland",
87
- "BBCA.JK": "BCA Bank", "BBRI.JK": "BRI Bank", "TLKM.JK": "Telkom", "GOTO.JK": "GoTo",
88
- "005930.KS": "Samsung Elec", "000660.KS": "SK Hynix", "035420.KS": "NAVER", "207940.KS": "Samsung Bio",
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
89
  "AAPL": "Apple", "NVDA": "NVIDIA", "TSLA": "Tesla", "AMZN": "Amazon", "MSFT": "Microsoft",
90
  "GOOGL": "Google", "META": "Meta", "AMD": "AMD", "NFLX": "Netflix", "INTC": "Intel",
 
 
 
 
 
 
 
91
  "7203.T": "Toyota", "6758.T": "Sony", "9984.T": "SoftBank", "8035.T": "Tokyo Elec",
92
- "9988.HK": "Alibaba", "0700.HK": "Tencent", "2330.TW": "TSMC", "RELIANCE.NS": "Reliance"
 
 
 
 
 
 
 
 
 
93
  }
94
- # (์Šค์บ๋„ˆ์šฉ MARKET_SAMPLES๋„ ๊ธฐ์กด ์ฝ”๋“œ ์œ ์ง€)
95
  MARKET_SAMPLES = {
96
- "๐Ÿ‡ป๐Ÿ‡ณ Vietnam (Max 100)": ["VIC.VN", "VHM.VN", "VCB.VN", "VNM.VN", "HPG.VN", "MSN.VN", "GAS.VN", "NVL.VN", "PDR.VN", "DIG.VN", "SSI.VN", "VND.VN", "MWG.VN", "FRT.VN", "FPT.VN", "STB.VN", "TCB.VN", "VPB.VN", "VRE.VN", "DGC.VN"],
97
- "๐Ÿ‡บ๐Ÿ‡ธ USA (Max 100)": ["AAPL", "NVDA", "TSLA", "AMZN", "MSFT", "GOOGL", "META", "AMD", "INTC", "PLTR", "COIN", "U", "RBLX", "SOFI", "PYPL", "SQ", "UBER", "LLY", "NVO", "JNJ", "PFE", "MRK", "XOM", "CVX", "KO", "PEP", "COST", "DIS"],
98
- "๐Ÿ‡ฐ๐Ÿ‡ท Korea (Max 100)": ["005930.KS", "000660.KS", "005380.KS", "000270.KS", "035420.KS", "035720.KS", "005490.KS", "086520.KQ", "247540.KQ", "207940.KS", "068270.KS", "196170.KQ", "012450.KS", "352820.KS", "032830.KQ"],
99
- "๐Ÿ‡ฎ๐Ÿ‡ฉ Indonesia (Max 50)": ["BBCA.JK", "BBRI.JK", "BMRI.JK", "BBNI.JK", "TLKM.JK", "ASII.JK", "GOTO.JK", "ADRO.JK", "MDKA.JK", "UNVR.JK", "ICBP.JK"],
100
- "๐Ÿ‡ฏ๐Ÿ‡ต Japan (Max 50)": ["7203.T", "6758.T", "9984.T", "8035.T", "6861.T", "7974.T", "8306.T", "6501.T", "8058.T", "6920.T"],
101
- "๐Ÿ‡จ๐Ÿ‡ณ China/HK (Max 50)": ["9988.HK", "0700.HK", "3690.HK", "1211.HK", "1810.HK", "0941.HK", "9888.HK", "9618.HK"],
 
 
 
 
 
 
 
 
 
 
 
 
 
 
102
  "๐Ÿ‡น๐Ÿ‡ผ Taiwan (Max 30)": ["2330.TW", "2454.TW", "2317.TW", "2308.TW", "2603.TW", "2881.TW", "1301.TW"],
103
- "๐Ÿ‡ฎ๐Ÿ‡ณ India (Max 30)": ["RELIANCE.NS", "TCS.NS", "HDFCBANK.NS", "INFY.NS", "ICICIBANK.NS", "TATAMOTORS.NS"]
104
  }
105
 
106
  # ==============================================================================
107
  # ๐Ÿ› ๏ธ [์—”์ง„] ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํ•จ์ˆ˜๋“ค (Robust)
108
  # ==============================================================================
109
- FAKE_HEADERS = {'User-Agent': 'Mozilla/5.0 ...'}
 
 
 
110
 
111
  def fetch_news_robust(keyword):
112
  summary = ""
@@ -143,6 +195,7 @@ def scrape_price_from_web(name, ticker):
143
  return None, None
144
 
145
  def get_price_data_robust(ticker, name):
 
146
  try:
147
  fdr_symbol = ticker
148
  if ".VN" in ticker: fdr_symbol = ticker.split('.')[0]
@@ -156,12 +209,14 @@ def get_price_data_robust(ticker, name):
156
  if prev != 0: pct = ((curr - prev)/prev)*100
157
  return curr, pct, "FDR"
158
  except: pass
 
159
  try:
160
  stock = yf.Ticker(ticker)
161
  price = stock.fast_info.last_price
162
  prev = stock.fast_info.previous_close
163
  if price and prev: return price, ((price - prev)/prev)*100, "Yahoo"
164
  except: pass
 
165
  price, pct = scrape_price_from_web(name, ticker)
166
  if price is not None: return price, pct, "Web"
167
  return None, None, "Fail"
@@ -182,23 +237,12 @@ def get_chart_data(ticker):
182
  except: pass
183
  return pd.DataFrame(), "None"
184
 
185
- # ==============================================================================
186
- # ๐Ÿง  [์—”์ง„ 1] ๋ฉ€ํ‹ฐ ํƒ€๊ฒŸ AI ์ธ์‹๊ธฐ (ํ•ต์‹ฌ ์—…๊ทธ๋ ˆ์ด๋“œ)
187
- # ==============================================================================
188
  def identify_targets_with_ai(user_query):
189
  try:
190
  prompt = f"""
191
- [ROLE] Financial Entity Resolver. [QUERY] "{user_query}"
192
- [TASK]
193
- 1. Identify ALL companies mentioned in the query. (e.g., "Samsung vs Apple" -> 2 companies)
194
- 2. If no specific company is mentioned but a sector is (e.g., "Vietnam Banks"), pick top 2-3 representative companies.
195
- 3. For EACH company, determine Name, Ticker, English Keyword, Native Keyword.
196
-
197
- [OUTPUT JSON LIST]
198
- [
199
- {{ "name": "Eng Name 1", "ticker": "TICKER1", "eng_key": "...", "native_key": "..." }},
200
- {{ "name": "Eng Name 2", "ticker": "TICKER2", "eng_key": "...", "native_key": "..." }}
201
- ]
202
  """
203
  messages = [{"role": "user", "content": prompt}]
204
  response = client.chat.completions.create(model="Qwen/Qwen2.5-72B-Instruct", messages=messages, max_tokens=300)
@@ -235,7 +279,6 @@ with st.sidebar:
235
  st.divider()
236
  menu = st.radio("MENU", [T['menu_search'], T['menu_scanner']], index=0)
237
 
238
- # --- 1. AI ๋ฉ€ํ‹ฐ ์Šค๋งˆํŠธ ๊ฒ€์ƒ‰ ---
239
  if menu == T['menu_search']:
240
  st.subheader(T['search_title'])
241
  c1, c2 = st.columns([3, 1])
@@ -244,46 +287,30 @@ if menu == T['menu_search']:
244
 
245
  if btn:
246
  with st.status(T['status_thinking'], expanded=True) as status:
247
- # 1. ๋‹ค์ค‘ ํƒ€๊ฒŸ ์ธ์‹
248
  targets = identify_targets_with_ai(query)
249
-
250
  if targets:
251
- collected_data = [] # AI ๋ถ„์„์šฉ ํ†ตํ•ฉ ๋ฐ์ดํ„ฐ
252
-
253
- # ํƒญ ์ƒ์„ฑ (๊ธฐ์—… ์ˆ˜๋งŒํผ)
254
- tab_names = [t.get('name', 'Unknown') for t in targets]
255
- tabs = st.tabs(tab_names)
256
 
257
  for i, target in enumerate(targets):
258
- name = target.get('name')
259
- ticker = target.get('ticker')
260
- eng_key = target.get('eng_key')
261
- native_key = target.get('native_key')
262
 
263
  with tabs[i]:
264
  st.info(f"๐Ÿ“ **{name} ({ticker})**")
265
-
266
- # ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘
267
  h, src = get_chart_data(ticker)
268
  news_data = get_polyglot_news(eng_key, native_key)
269
-
270
  curr, pct = "N/A", 0
271
- has_data = False
272
 
273
  if not h.empty:
274
  curr = h['Close'].iloc[-1]
275
  if len(h) >= 2:
276
  prev = h['Close'].iloc[-2]
277
  if prev != 0: pct = ((curr - prev)/prev)*100
278
- has_data = True
279
  else:
280
- # ์ฐจํŠธ ์—†์œผ๋ฉด ์›น ๊ฐ€๊ฒฉ ์‹œ๋„
281
  c_web, p_web, s_web = get_price_data_robust(ticker, name)
282
- if c_web:
283
- curr, pct, src = c_web, p_web, s_web
284
- has_data = True
285
 
286
- # UI ํ‘œ์‹œ
287
  m1, m2 = st.columns(2)
288
  m1.metric(T['metric_price'], f"{curr:,.0f}" if isinstance(curr, (int, float)) else curr, f"{pct:.2f}%")
289
  m2.metric(T['metric_source'], src)
@@ -293,42 +320,23 @@ if menu == T['menu_search']:
293
 
294
  with st.expander(T['expander_news']): st.text(news_data)
295
 
296
- # AI ๋ถ„์„์šฉ ๋ฐ์ดํ„ฐ ์ถ•์ 
297
- collected_data.append(f"""
298
- [TARGET {i+1}] {name} ({ticker})
299
- - Price: {curr}, Change: {pct:.2f}%
300
- - News Summary: {news_data[:500]}...
301
- """)
302
-
303
- status.update(label="โœ… ๋ถ„์„ ์™„๋ฃŒ", state="complete", expanded=False)
304
 
305
- # 3. ์ข…ํ•ฉ ๋ถ„์„ ๋ฆฌํฌํŠธ
306
  st.divider()
307
  st.subheader(T['insight_title'])
308
 
309
  target_lang = T['llm_lang_instruction']
310
- context_str = "\n".join(collected_data)
311
-
312
  prompt = f"""
313
- [ROLE] Global Investment Analyst.
314
- [USER QUERY] "{query}"
315
- [COLLECTED DATA OF MULTIPLE TARGETS]
316
- {context_str}
317
-
318
- [TASK]
319
- 1. Provide a synthesized analysis or comparison based on the user's query.
320
- 2. If multiple companies, compare their recent sentiment and performance.
321
- 3. Provide investment verdicts for each.
322
-
323
- [OUTPUT LANGUAGE] **{target_lang}** (Strictly output in {target_lang})
324
  """
325
  msg = [{"role": "user", "content": prompt}]
326
  stream = client.chat.completions.create(model="Qwen/Qwen2.5-72B-Instruct", messages=msg, stream=True)
327
  st.write_stream(parse_stream(stream))
328
-
329
- else: st.error("AI๊ฐ€ ๊ธฐ์—…์„ ์ฐพ์ง€ ๋ชปํ–ˆ์Šต๋‹ˆ๋‹ค.")
330
 
331
- # --- 2. ์‹œ์žฅ ์Šค์บ๋„ˆ ---
332
  elif menu == T['menu_scanner']:
333
  st.subheader(T['scanner_title'])
334
  st.caption(T['scanner_caption'])
@@ -342,11 +350,9 @@ elif menu == T['menu_scanner']:
342
  results = []
343
  bar = st.progress(0)
344
 
345
- # Phase 1: Batch
346
  try: batch_data = yf.download(tickers, period="5d", progress=False)['Close']
347
  except: batch_data = pd.DataFrame()
348
 
349
- # Phase 2: Loop
350
  for i, t in enumerate(tickers):
351
  name_display = TICKER_NAMES.get(t, t)
352
  p, c, s = None, None, None
 
38
  "metric_source": "์ถœ์ฒ˜",
39
  "expander_news": "๐Ÿ“ฐ ๋‰ด์Šค ์›๋ฌธ ๋ณด๊ธฐ",
40
  "insight_title": "๐Ÿง  AI ์ข…ํ•ฉ ๋ถ„์„ ๋ฆฌํฌํŠธ",
41
+ "scanner_title": "๐Ÿ“ก ๊ธ€๋กœ๋ฒŒ ์‹œ์žฅ ์ „๊ด‘ํŒ (Full Version)",
42
+ "scanner_caption": "โ€ป ์•ฝ 100๊ฐœ ๊ธฐ์—…์„ ์ •๋ฐ€ ๋ถ„์„ํ•˜์—ฌ ์ƒ์œ„ 10๊ฐœ๋ฅผ ์„ ๋ณ„ํ•ฉ๋‹ˆ๋‹ค.",
43
+ "btn_scan": "์ „์ฒด ์ข…๋ชฉ ์Šค์บ” ์‹œ์ž‘",
44
  "tab_gainers": "๐Ÿ”ฅ ๊ธ‰๋“ฑ Top 10 (์ƒ์Šน์žฅ)",
45
  "tab_losers": "๐Ÿ’ง ๊ธ‰๋ฝ Top 10 (ํ•˜๋ฝ์žฅ)",
46
  "col_name": "๊ธฐ์—…๋ช…",
 
63
  "metric_source": "Source",
64
  "expander_news": "๐Ÿ“ฐ View Source News",
65
  "insight_title": "๐Ÿง  AI Comprehensive Report",
66
+ "scanner_title": "๐Ÿ“ก Global Market Scanner (Full Version)",
67
+ "scanner_caption": "โ€ป Analyzing 100+ companies to show Top 10.",
68
+ "btn_scan": "Start Full Scan",
69
  "tab_gainers": "๐Ÿ”ฅ Top 10 Gainers",
70
  "tab_losers": "๐Ÿ’ง Top 10 Losers",
71
  "col_name": "Company",
 
78
  }
79
 
80
  # ==============================================================================
81
+ # ๐Ÿ’พ [๋ฐ์ดํ„ฐ] ๋Œ€๊ทœ๋ชจ ํ™•์žฅ ํ‹ฐ์ปค (Full List) - ์ ˆ๋Œ€ ์ค„์ด์ง€ ์•Š์Œ!
82
  # ==============================================================================
 
83
  TICKER_NAMES = {
84
+ # ๐Ÿ‡ป๐Ÿ‡ณ ๋ฒ ํŠธ๋‚จ (๋ถ€๋™์‚ฐ/๊ฑด์„ค/์ฆ๊ถŒ/์†Œ๋งค ํฌํ•จ)
85
  "VIC.VN": "Vingroup", "VHM.VN": "Vinhomes", "VCB.VN": "Vietcombank", "VNM.VN": "Vinamilk",
86
+ "HPG.VN": "Hoa Phat", "MSN.VN": "Masan Group", "GAS.VN": "PV Gas", "NVL.VN": "Novaland",
87
+ "PDR.VN": "Phat Dat", "DIG.VN": "DIC Corp", "CEO.VN": "CEO Group", "SSI.VN": "SSI Sec",
88
+ "VND.VN": "VNDirect", "MWG.VN": "Mobile World", "FRT.VN": "FPT Retail", "FPT.VN": "FPT Corp",
89
+ "STB.VN": "Sacombank", "MBB.VN": "MB Bank", "TCB.VN": "Techcombank", "VPB.VN": "VPBank",
90
+ "VRE.VN": "Vincom Retail", "DGC.VN": "Duc Giang Chem", "VHC.VN": "Vinh Hoan", "KBC.VN": "Kinh Bac",
91
+ "GVR.VN": "Vietnam Rubber", "SAB.VN": "Sabeco", "BID.VN": "BIDV", "CTG.VN": "VietinBank",
92
+
93
+ # ๐Ÿ‡ฎ๐Ÿ‡ฉ ์ธ๋„๋„ค์‹œ์•„
94
+ "BBCA.JK": "BCA Bank", "BBRI.JK": "BRI Bank", "TLKM.JK": "Telkom", "BMRI.JK": "Mandiri",
95
+ "ASII.JK": "Astra Intl", "GOTO.JK": "GoTo", "UNVR.JK": "Unilever", "ADRO.JK": "Adaro Energy",
96
+ "MDKA.JK": "Merdeka Copper", "ANTM.JK": "Aneka Tambang", "ICBP.JK": "Indofood CBP",
97
+ "KLBF.JK": "Kalbe Farma", "BUKA.JK": "Bukalapak", "PGAS.JK": "Perusahaan Gas",
98
+
99
+ # ๐Ÿ‡ฐ๐Ÿ‡ท ํ•œ๊ตญ
100
+ "005930.KS": "Samsung Elec", "000660.KS": "SK Hynix", "035420.KS": "NAVER", "035720.KS": "Kakao",
101
+ "005380.KS": "Hyundai Motor", "207940.KS": "Samsung Bio", "068270.KS": "Celltrion",
102
+ "086520.KQ": "Ecopro", "247540.KQ": "Ecopro BM", "005490.KS": "POSCO Holdings", "010120.KS": "LS ELECTRIC",
103
+ "012450.KS": "Hanwha Aero", "042700.KS": "Hanmi Semi", "196170.KQ": "Alteogen",
104
+ "352820.KS": "HYBE", "035900.KQ": "JYP Ent", "000270.KS": "Kia", "010950.KS": "S-Oil",
105
+ "032830.KQ": "Samsung Life", "329180.KS": "Hyundai Heavy", "015760.KS": "KEPCO",
106
+
107
+ # ๐Ÿ‡บ๐Ÿ‡ธ ๋ฏธ๊ตญ (M7 + ์„ฑ์žฅ + ๋ฐ”์ด์˜ค + ์—๋„ˆ์ง€)
108
  "AAPL": "Apple", "NVDA": "NVIDIA", "TSLA": "Tesla", "AMZN": "Amazon", "MSFT": "Microsoft",
109
  "GOOGL": "Google", "META": "Meta", "AMD": "AMD", "NFLX": "Netflix", "INTC": "Intel",
110
+ "PLTR": "Palantir", "COIN": "Coinbase", "U": "Unity", "RBLX": "Roblox", "SOFI": "SoFi",
111
+ "PYPL": "PayPal", "SQ": "Block", "UBER": "Uber", "ABNB": "Airbnb", "HOOD": "Robinhood",
112
+ "LLY": "Eli Lilly", "NVO": "Novo Nordisk", "PFE": "Pfizer", "MRK": "Merck",
113
+ "XOM": "Exxon", "CVX": "Chevron", "KO": "Coca-Cola", "PEP": "Pepsi", "COST": "Costco",
114
+ "DIS": "Disney", "NKE": "Nike", "SBUX": "Starbucks", "MCD": "McDonalds",
115
+
116
+ # ๐Ÿ‡ฏ๐Ÿ‡ต ์ผ๋ณธ
117
  "7203.T": "Toyota", "6758.T": "Sony", "9984.T": "SoftBank", "8035.T": "Tokyo Elec",
118
+ "6861.T": "Keyence", "7974.T": "Nintendo", "8306.T": "MUFG", "6501.T": "Hitachi",
119
+ "8058.T": "Mitsubishi", "8001.T": "Itochu", "8031.T": "Mitsui", "6920.T": "Lasertec",
120
+ "4063.T": "Shin-Etsu", "7741.T": "HOYA", "6146.T": "Disco", "7267.T": "Honda",
121
+
122
+ # ๐Ÿ‡จ๐Ÿ‡ณ ์ค‘๊ตญ/ํ™์ฝฉ/๋Œ€๋งŒ/์ธ๋„
123
+ "9988.HK": "Alibaba", "0700.HK": "Tencent", "3690.HK": "Meituan", "1211.HK": "BYD",
124
+ "1810.HK": "Xiaomi", "0941.HK": "China Mobile", "9888.HK": "Baidu", "9618.HK": "JD.com",
125
+ "2015.HK": "Li Auto", "9868.HK": "Xpeng", "0981.HK": "SMIC",
126
+ "2330.TW": "TSMC", "2454.TW": "MediaTek", "2317.TW": "Foxconn", "2308.TW": "Delta Elec",
127
+ "RELIANCE.NS": "Reliance", "TCS.NS": "TCS", "HDFCBANK.NS": "HDFC Bank", "INFY.NS": "Infosys"
128
  }
129
+
130
  MARKET_SAMPLES = {
131
+ "๐Ÿ‡ป๐Ÿ‡ณ Vietnam (Max 100)": [
132
+ "VIC.VN", "VHM.VN", "VRE.VN", "VNM.VN", "MSN.VN", "GAS.VN", "HPG.VN", "HSG.VN",
133
+ "NVL.VN", "PDR.VN", "DIG.VN", "CEO.VN", "DXG.VN", "KBC.VN", "GVR.VN",
134
+ "VCB.VN", "TCB.VN", "VPB.VN", "MBB.VN", "STB.VN", "ACB.VN", "BID.VN", "CTG.VN",
135
+ "SSI.VN", "VND.VN", "VCI.VN", "MWG.VN", "FRT.VN", "DGW.VN", "FPT.VN", "DGC.VN", "VHC.VN", "SAB.VN"
136
+ ],
137
+ "๐Ÿ‡บ๐Ÿ‡ธ USA (Max 100)": [
138
+ "AAPL", "MSFT", "GOOGL", "AMZN", "NVDA", "META", "TSLA", "AMD", "INTC", "QCOM",
139
+ "AVGO", "MU", "PLTR", "COIN", "U", "RBLX", "SOFI", "UBER", "ABNB", "HOOD", "PYPL", "SQ",
140
+ "LLY", "NVO", "JNJ", "PFE", "MRK", "UNH", "XOM", "CVX", "JPM", "V", "MA",
141
+ "KO", "PEP", "COST", "WMT", "DIS", "NKE", "SBUX", "MCD"
142
+ ],
143
+ "๐Ÿ‡ฐ๐Ÿ‡ท Korea (Max 100)": [
144
+ "005930.KS", "000660.KS", "005380.KS", "000270.KS", "035420.KS", "035720.KS",
145
+ "005490.KS", "086520.KQ", "247540.KQ", "010120.KS", "207940.KS", "068270.KS",
146
+ "196170.KQ", "012450.KS", "042700.KS", "352820.KS", "010950.KS", "032830.KQ", "329180.KS", "015760.KS"
147
+ ],
148
+ "๐Ÿ‡ฎ๐Ÿ‡ฉ Indonesia (Max 50)": ["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"],
149
+ "๐Ÿ‡ฏ๐Ÿ‡ต Japan (Max 50)": ["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"],
150
+ "๐Ÿ‡จ๐Ÿ‡ณ China/HK (Max 50)": ["9988.HK", "0700.HK", "3690.HK", "1211.HK", "1810.HK", "0941.HK", "9888.HK", "9618.HK", "2015.HK", "9868.HK", "0981.HK"],
151
  "๐Ÿ‡น๐Ÿ‡ผ Taiwan (Max 30)": ["2330.TW", "2454.TW", "2317.TW", "2308.TW", "2603.TW", "2881.TW", "1301.TW"],
152
+ "๐Ÿ‡ฎ๐Ÿ‡ณ India (Max 30)": ["RELIANCE.NS", "TCS.NS", "HDFCBANK.NS", "INFY.NS", "ICICIBANK.NS", "TATAMOTORS.NS", "BHARTIARTL.NS"]
153
  }
154
 
155
  # ==============================================================================
156
  # ๐Ÿ› ๏ธ [์—”์ง„] ๋ฐ์ดํ„ฐ ์ˆ˜์ง‘ ํ•จ์ˆ˜๋“ค (Robust)
157
  # ==============================================================================
158
+ FAKE_HEADERS = {
159
+ 'User-Agent': 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/120.0.0.0 Safari/537.36',
160
+ 'Referer': 'https://www.google.com/'
161
+ }
162
 
163
  def fetch_news_robust(keyword):
164
  summary = ""
 
195
  return None, None
196
 
197
  def get_price_data_robust(ticker, name):
198
+ # FDR
199
  try:
200
  fdr_symbol = ticker
201
  if ".VN" in ticker: fdr_symbol = ticker.split('.')[0]
 
209
  if prev != 0: pct = ((curr - prev)/prev)*100
210
  return curr, pct, "FDR"
211
  except: pass
212
+ # Yahoo
213
  try:
214
  stock = yf.Ticker(ticker)
215
  price = stock.fast_info.last_price
216
  prev = stock.fast_info.previous_close
217
  if price and prev: return price, ((price - prev)/prev)*100, "Yahoo"
218
  except: pass
219
+ # Web
220
  price, pct = scrape_price_from_web(name, ticker)
221
  if price is not None: return price, pct, "Web"
222
  return None, None, "Fail"
 
237
  except: pass
238
  return pd.DataFrame(), "None"
239
 
 
 
 
240
  def identify_targets_with_ai(user_query):
241
  try:
242
  prompt = f"""
243
+ [ROLE] Entity Resolver. [QUERY] "{user_query}"
244
+ [TASK] Identify ALL companies. [OUTPUT JSON LIST]
245
+ [ {{ "name": "EngName", "ticker": "TICKER", "eng_key": "Name news", "native_key": "LocalName news" }} ]
 
 
 
 
 
 
 
 
246
  """
247
  messages = [{"role": "user", "content": prompt}]
248
  response = client.chat.completions.create(model="Qwen/Qwen2.5-72B-Instruct", messages=messages, max_tokens=300)
 
279
  st.divider()
280
  menu = st.radio("MENU", [T['menu_search'], T['menu_scanner']], index=0)
281
 
 
282
  if menu == T['menu_search']:
283
  st.subheader(T['search_title'])
284
  c1, c2 = st.columns([3, 1])
 
287
 
288
  if btn:
289
  with st.status(T['status_thinking'], expanded=True) as status:
 
290
  targets = identify_targets_with_ai(query)
 
291
  if targets:
292
+ collected_data = []
293
+ tabs = st.tabs([t.get('name', 'Unknown') for t in targets])
 
 
 
294
 
295
  for i, target in enumerate(targets):
296
+ name, ticker = target.get('name'), target.get('ticker')
297
+ eng_key, native_key = target.get('eng_key'), target.get('native_key')
 
 
298
 
299
  with tabs[i]:
300
  st.info(f"๐Ÿ“ **{name} ({ticker})**")
 
 
301
  h, src = get_chart_data(ticker)
302
  news_data = get_polyglot_news(eng_key, native_key)
 
303
  curr, pct = "N/A", 0
 
304
 
305
  if not h.empty:
306
  curr = h['Close'].iloc[-1]
307
  if len(h) >= 2:
308
  prev = h['Close'].iloc[-2]
309
  if prev != 0: pct = ((curr - prev)/prev)*100
 
310
  else:
 
311
  c_web, p_web, s_web = get_price_data_robust(ticker, name)
312
+ if c_web: curr, pct, src = c_web, p_web, s_web
 
 
313
 
 
314
  m1, m2 = st.columns(2)
315
  m1.metric(T['metric_price'], f"{curr:,.0f}" if isinstance(curr, (int, float)) else curr, f"{pct:.2f}%")
316
  m2.metric(T['metric_source'], src)
 
320
 
321
  with st.expander(T['expander_news']): st.text(news_data)
322
 
323
+ collected_data.append(f"[TARGET {i+1}] {name} ({ticker})\nPrice: {curr}, Change: {pct:.2f}%\nNews: {news_data[:500]}...")
 
 
 
 
 
 
 
324
 
325
+ status.update(label="โœ… OK", state="complete", expanded=False)
326
  st.divider()
327
  st.subheader(T['insight_title'])
328
 
329
  target_lang = T['llm_lang_instruction']
 
 
330
  prompt = f"""
331
+ [ROLE] Analyst. [QUERY] "{query}"
332
+ [DATA] {chr(10).join(collected_data)}
333
+ [TASK] Comparative Analysis & Verdict. [LANG] {target_lang}.
 
 
 
 
 
 
 
 
334
  """
335
  msg = [{"role": "user", "content": prompt}]
336
  stream = client.chat.completions.create(model="Qwen/Qwen2.5-72B-Instruct", messages=msg, stream=True)
337
  st.write_stream(parse_stream(stream))
338
+ else: st.error("AI Error")
 
339
 
 
340
  elif menu == T['menu_scanner']:
341
  st.subheader(T['scanner_title'])
342
  st.caption(T['scanner_caption'])
 
350
  results = []
351
  bar = st.progress(0)
352
 
 
353
  try: batch_data = yf.download(tickers, period="5d", progress=False)['Close']
354
  except: batch_data = pd.DataFrame()
355
 
 
356
  for i, t in enumerate(tickers):
357
  name_display = TICKER_NAMES.get(t, t)
358
  p, c, s = None, None, None