Spaces:
Sleeping
Sleeping
Update streamlit_app.py
Browse files- 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 |
-
|
| 15 |
-
|
| 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("๐จ ์ค์
|
| 34 |
st.stop()
|
| 35 |
|
| 36 |
-
# --- [ํต์ฌ
|
| 37 |
-
def
|
| 38 |
"""
|
| 39 |
-
|
| 40 |
-
|
| 41 |
-
|
|
|
|
|
|
|
| 42 |
"""
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 47 |
try:
|
| 48 |
stock = yf.Ticker(ticker)
|
| 49 |
-
# ์ผํ๋ period ์ธ์ ์ฌ์ฉ
|
| 50 |
hist = stock.history(period="3mo")
|
| 51 |
-
if not hist.empty:
|
| 52 |
-
|
| 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 |
-
#
|
| 74 |
-
|
|
|
|
|
|
|
|
|
|
| 75 |
|
| 76 |
-
|
| 77 |
-
|
| 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 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
print(f"FDR Failed: {e}")
|
| 89 |
-
pass
|
| 90 |
-
|
| 91 |
return pd.DataFrame(), "None"
|
| 92 |
|
| 93 |
-
# --- [
|
| 94 |
-
def
|
| 95 |
-
|
| 96 |
try:
|
| 97 |
with DDGS() as ddgs:
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
|
| 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
|
| 144 |
-
fig = go.Figure(data=[go.Candlestick(
|
| 145 |
-
|
| 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 |
-
|
| 169 |
-
|
| 170 |
-
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
st.
|
| 174 |
-
|
| 175 |
-
|
| 176 |
-
|
| 177 |
-
|
| 178 |
-
|
| 179 |
-
|
| 180 |
-
|
| 181 |
-
|
| 182 |
-
|
| 183 |
-
|
| 184 |
-
|
|
|
|
|
|
|
| 185 |
|
| 186 |
-
|
| 187 |
-
|
| 188 |
-
|
| 189 |
-
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
|
| 197 |
-
|
| 198 |
-
|
| 199 |
-
|
| 200 |
-
|
| 201 |
-
|
| 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="
|
| 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 |
-
|
| 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 |
-
|
| 264 |
-
|
| 265 |
-
|
| 266 |
-
|
| 267 |
-
|
| 268 |
-
|
| 269 |
-
|
| 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๊ฐ ์ด๋ค ๊ธฐ์
์ธ์ง ํ์
ํ์ง ๋ชปํ์ต๋๋ค. ์กฐ๊ธ ๋ ๊ตฌ์ฒด์ ์ผ๋ก ์
๋ ฅํด์ฃผ์ธ์.")
|