Spaces:
Sleeping
Sleeping
Update streamlit_app.py
Browse files- 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": "๐ก ๊ธ๋ก๋ฒ ์์ฅ ์ ๊ดํ (
|
| 42 |
-
"scanner_caption": "โป ์ฝ 100๊ฐ ๊ธฐ์
์ ๋ถ์ํ์ฌ ์์ 10๊ฐ๋ฅผ ์ ๋ณํฉ๋๋ค.",
|
| 43 |
-
"btn_scan": "
|
| 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 (
|
| 67 |
-
"scanner_caption": "โป Analyzing 100 companies to show Top 10.",
|
| 68 |
-
"btn_scan": "
|
| 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 |
-
# ๐พ [๋ฐ์ดํฐ] ํฐ์ปค
|
| 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 |
-
"
|
| 88 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 |
-
"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 93 |
}
|
| 94 |
-
|
| 95 |
MARKET_SAMPLES = {
|
| 96 |
-
"๐ป๐ณ Vietnam (Max 100)": [
|
| 97 |
-
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 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 = {
|
|
|
|
|
|
|
|
|
|
| 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]
|
| 192 |
-
[TASK]
|
| 193 |
-
|
| 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 = []
|
| 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 |
-
|
| 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 |
-
|
| 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 |
-
|
| 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]
|
| 314 |
-
[
|
| 315 |
-
[
|
| 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
|