|
|
import yfinance as yf |
|
|
import pandas as pd |
|
|
import streamlit as st |
|
|
import requests |
|
|
import plotly.express as px |
|
|
import plotly.graph_objects as go |
|
|
from datetime import datetime, timedelta |
|
|
from bs4 import BeautifulSoup |
|
|
import time |
|
|
from playwright.sync_api import sync_playwright |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_stock_data(ticker, period="11d"): |
|
|
try: |
|
|
stock = yf.Ticker(ticker) |
|
|
hist = stock.history(period=period) |
|
|
if len(hist) < 11: |
|
|
return None, None, None, None |
|
|
avg_volume = hist["Volume"][:-1].mean() |
|
|
today_volume = hist["Volume"][-1] |
|
|
price = hist["Close"][-1] |
|
|
change = ((hist["Close"][-1] - hist["Close"][-2]) / hist["Close"][-2]) * 100 |
|
|
vol_ratio = today_volume / avg_volume if avg_volume else None |
|
|
return price, change, vol_ratio, hist |
|
|
except: |
|
|
return None, None, None, None |
|
|
|
|
|
def get_pcr(ticker, use_oi=False): |
|
|
try: |
|
|
opt = yf.Ticker(ticker).option_chain() |
|
|
calls = opt.calls |
|
|
puts = opt.puts |
|
|
if use_oi: |
|
|
call_oi = calls["openInterest"].sum() |
|
|
put_oi = puts["openInterest"].sum() |
|
|
if call_oi > 0: |
|
|
return round(put_oi / call_oi, 2) |
|
|
else: |
|
|
call_volume = calls["volume"].sum() |
|
|
put_volume = puts["volume"].sum() |
|
|
if call_volume > 0: |
|
|
return round(put_volume / call_volume, 2) |
|
|
except: |
|
|
return None |
|
|
|
|
|
def get_pcr_trend(ticker, days=5): |
|
|
try: |
|
|
pcr_values = [] |
|
|
for i in range(days): |
|
|
date = (datetime.now() - timedelta(days=i)).strftime('%Y-%m-%d') |
|
|
opt = yf.Ticker(ticker).option_chain(date=date) |
|
|
calls = opt.calls |
|
|
puts = opt.puts |
|
|
call_volume = calls["volume"].sum() |
|
|
put_volume = puts["volume"].sum() |
|
|
if call_volume > 0: |
|
|
pcr_values.append(put_volume / call_volume) |
|
|
return round(sum(pcr_values) / len(pcr_values), 2) if pcr_values else None |
|
|
except: |
|
|
return None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_vix_index(): |
|
|
try: |
|
|
vix = yf.Ticker("^VIX") |
|
|
df = vix.history(period="1d") |
|
|
if df.empty: |
|
|
return None |
|
|
return df["Close"].iloc[-1] |
|
|
except Exception as e: |
|
|
st.error(f"Lỗi khi lấy dữ liệu VIX Index: {e}") |
|
|
return None |
|
|
|
|
|
def get_vix_history(period="11d"): |
|
|
try: |
|
|
vix = yf.Ticker("^VIX") |
|
|
hist = vix.history(period=period) |
|
|
if hist.empty: |
|
|
return None |
|
|
return hist |
|
|
except Exception as e: |
|
|
st.error(f"Lỗi khi lấy lịch sử VIX: {e}") |
|
|
return None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_bond_yield(): |
|
|
FRED_API_KEY = "f312589066a1d21d3c09fc5cec6d9421" |
|
|
url = f"https://api.stlouisfed.org/fred/series/observations?series_id=GS10&api_key={FRED_API_KEY}&file_type=json" |
|
|
response = requests.get(url).json() |
|
|
if "observations" in response: |
|
|
return float(response["observations"][-1]["value"]) |
|
|
return None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_gold_price_from_goldapi(api_key): |
|
|
url = "https://www.goldapi.io/api/XAU/USD" |
|
|
headers = { |
|
|
"x-access-token": api_key, |
|
|
"Content-Type": "application/json" |
|
|
} |
|
|
try: |
|
|
response = requests.get(url, headers=headers, timeout=10) |
|
|
response.raise_for_status() |
|
|
data = response.json() |
|
|
price = data.get("price") |
|
|
prev_close = data.get("prev_close_price") |
|
|
if price is not None and prev_close is not None: |
|
|
change_percent = ((price - prev_close) / prev_close) * 100 |
|
|
return price, change_percent |
|
|
else: |
|
|
return None, None |
|
|
except Exception as e: |
|
|
print("Lỗi khi lấy dữ liệu từ GoldAPI:", e) |
|
|
return None, None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_fear_and_greed_index_from_feargreedmeter(): |
|
|
try: |
|
|
with sync_playwright() as p: |
|
|
browser = p.chromium.launch(headless=True) |
|
|
page = browser.new_page() |
|
|
url = "https://feargreedmeter.com/fear-and-greed-index" |
|
|
page.goto(url) |
|
|
page.wait_for_load_state("networkidle") |
|
|
|
|
|
|
|
|
index_element = page.query_selector("div.fng-value") |
|
|
status_element = page.query_selector("div.fng-status") |
|
|
|
|
|
if index_element: |
|
|
index_value = int(index_element.inner_text().strip().replace(",", "")) |
|
|
status = status_element.inner_text().strip() if status_element else None |
|
|
if not status: |
|
|
|
|
|
if index_value < 25: |
|
|
status = "Extreme Fear" |
|
|
elif index_value < 45: |
|
|
status = "Fear" |
|
|
elif index_value < 55: |
|
|
status = "Neutral" |
|
|
elif index_value < 75: |
|
|
status = "Greed" |
|
|
else: |
|
|
status = "Extreme Greed" |
|
|
browser.close() |
|
|
return index_value, status |
|
|
browser.close() |
|
|
return None, None |
|
|
except Exception as e: |
|
|
st.error(f"Lỗi khi lấy Fear & Greed Index từ feargreedmeter.com: {e}") |
|
|
return None, None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
def get_fear_and_greed_index_from_macromicro(): |
|
|
try: |
|
|
with sync_playwright() as p: |
|
|
browser = p.chromium.launch(headless=True) |
|
|
page = browser.new_page() |
|
|
url = "https://en.macromicro.me/series/22748/cnn-fear-and-greed" |
|
|
page.goto(url) |
|
|
page.wait_for_load_state("networkidle") |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
index_element = page.query_selector("div.mm-data-value") |
|
|
if index_element: |
|
|
index_value = float(index_element.inner_text().strip()) |
|
|
else: |
|
|
browser.close() |
|
|
return None, None |
|
|
|
|
|
|
|
|
if index_value < 25: |
|
|
status = "Extreme Fear" |
|
|
elif index_value < 45: |
|
|
status = "Fear" |
|
|
elif index_value < 55: |
|
|
status = "Neutral" |
|
|
elif index_value < 75: |
|
|
status = "Greed" |
|
|
else: |
|
|
status = "Extreme Greed" |
|
|
|
|
|
browser.close() |
|
|
return index_value, status |
|
|
except Exception as e: |
|
|
st.error(f"Lỗi khi lấy Fear & Greed Index từ MacroMicro: {e}") |
|
|
return None, None |
|
|
|
|
|
def get_futures_data_from_businessinsider(): |
|
|
try: |
|
|
url = "https://markets.businessinsider.com/premarket" |
|
|
headers = { |
|
|
"User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/91.0.4472.124 Safari/537.36" |
|
|
} |
|
|
response = requests.get(url, headers=headers) |
|
|
time.sleep(2) |
|
|
soup = BeautifulSoup(response.text, 'html.parser') |
|
|
|
|
|
futures_data = {} |
|
|
futures_table = soup.find("table") |
|
|
if futures_table: |
|
|
rows = futures_table.find_all("tr")[1:] |
|
|
for row in rows: |
|
|
cols = row.find_all("td") |
|
|
if len(cols) >= 4: |
|
|
name = cols[0].text.strip() |
|
|
price = float(cols[1].text.replace(",", "").replace("$", "")) |
|
|
change = float(cols[2].text.replace("+", "").replace("-", "")) |
|
|
percent_change = float(cols[3].text.replace("%", "")) |
|
|
futures_data[name] = { |
|
|
"price": price, |
|
|
"change": change, |
|
|
"percent_change": percent_change |
|
|
} |
|
|
return futures_data |
|
|
except Exception as e: |
|
|
st.error(f"Lỗi khi lấy dữ liệu Futures từ Business Insider: {e}") |
|
|
return {} |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
st.title("📉 Bộ Cảnh Báo Rủi Ro Thị Trường & Gợi Ý ETF Nghịch Đảo") |
|
|
|
|
|
tickers = { |
|
|
"SPY": "S&P 500", |
|
|
"QQQ": "NASDAQ 100", |
|
|
"IWM": "Russell 2000", |
|
|
"SOXX": "Semiconductors", |
|
|
"XLF": "Financials" |
|
|
} |
|
|
|
|
|
market_data = {} |
|
|
for ticker, name in tickers.items(): |
|
|
price, change, vol_ratio, hist = get_stock_data(ticker) |
|
|
pcr = get_pcr(ticker) |
|
|
pcr_oi = get_pcr(ticker, use_oi=True) |
|
|
pcr_trend = get_pcr_trend(ticker) |
|
|
market_data[ticker] = { |
|
|
"name": name, |
|
|
"change": f"{change:.2f}%" if change is not None else "N/A", |
|
|
"price": f"${price:.2f}" if price is not None else "N/A", |
|
|
"volume_ratio": f"{vol_ratio:.2f}x" if vol_ratio is not None else "N/A", |
|
|
"pcr": f"{pcr}" if pcr is not None else "N/A", |
|
|
"pcr_oi": f"{pcr_oi}" if pcr_oi is not None else "N/A", |
|
|
"pcr_trend": f"{pcr_trend}" if pcr_trend is not None else "N/A", |
|
|
"history": hist |
|
|
} |
|
|
|
|
|
st.subheader("📊 Thị Trường Chung") |
|
|
for ticker, data in market_data.items(): |
|
|
st.write(f"**{ticker} ({data['name']})**: {data['change']} | Giá: {data['price']} | Volume: {data['volume_ratio']} | PCR: {data['pcr']} | PCR OI: {data['pcr_oi']} | PCR Trend (5d): {data['pcr_trend']}") |
|
|
if data["history"] is not None: |
|
|
fig = px.line(data["history"], x=data["history"].index, y="Close", title=f"Giá {ticker} (10 ngày gần nhất)") |
|
|
st.plotly_chart(fig) |
|
|
if 'pcr' in data and data['pcr'] != "N/A": |
|
|
pcr_value = float(data['pcr']) |
|
|
if pcr_value > 1.2: |
|
|
st.warning(f"⚠️ PCR cao ({pcr_value}) → Nhiều người mua PUT → Thị trường có thể suy yếu.") |
|
|
elif pcr_value < 0.7: |
|
|
st.success(f"✅ PCR thấp ({pcr_value}) → Nhiều người mua CALL → Thị trường có thể tích cực.") |
|
|
else: |
|
|
st.info(f"ℹ️ PCR trung bình ({pcr_value}) → Tâm lý thị trường trung lập.") |
|
|
|
|
|
vix_history = get_vix_history() |
|
|
if vix_history is not None: |
|
|
fig = px.line(vix_history, x=vix_history.index, y="Close", title="Biểu đồ VIX Index (10 ngày gần nhất)") |
|
|
st.plotly_chart(fig) |
|
|
|
|
|
st.subheader("📈 Dữ liệu Futures (Pre-market từ Business Insider)") |
|
|
futures_data = get_futures_data_from_businessinsider() |
|
|
for name, data in futures_data.items(): |
|
|
st.write(f"**{name}**: Giá: ${data['price']:.2f} | Biến động: {data['change']:.2f} ({data['percent_change']:.2f}%)") |
|
|
if data["percent_change"] < -2: |
|
|
st.error(f"⚠️ {name} giảm mạnh ({data['percent_change']:.2f}%) → Dấu hiệu rủi ro cao!") |
|
|
|
|
|
|
|
|
st.subheader("📊 Fear & Greed Index") |
|
|
|
|
|
|
|
|
st.write("**Nguồn cũ (feargreedmeter.com):**") |
|
|
fear_greed_index_old, fear_greed_status_old = get_fear_and_greed_index_from_feargreedmeter() |
|
|
if fear_greed_index_old is not None: |
|
|
st.write(f"📉 **Fear & Greed Index**: {fear_greed_index_old} ({fear_greed_status_old})") |
|
|
if fear_greed_index_old < 25: |
|
|
st.error("⚠️ Extreme Fear → Thị trường rất tiêu cực!") |
|
|
elif fear_greed_index_old > 75: |
|
|
st.warning("⚠️ Extreme Greed → Thị trường có thể quá nóng!") |
|
|
else: |
|
|
st.info("ℹ️ Tâm lý thị trường trung lập.") |
|
|
else: |
|
|
|
|
|
fear_greed_index_old = 22 |
|
|
fear_greed_status_old = "Extreme Fear" |
|
|
st.write(f"📉 **Fear & Greed Index (dữ liệu cũ)**: {fear_greed_index_old} ({fear_greed_status_old})") |
|
|
st.error("⚠️ Extreme Fear → Thị trường rất tiêu cực!") |
|
|
|
|
|
|
|
|
st.write("**Nguồn mới (MacroMicro):**") |
|
|
fear_greed_index_new, fear_greed_status_new = get_fear_and_greed_index_from_macromicro() |
|
|
if fear_greed_index_new is not None: |
|
|
st.write(f"📉 **Fear & Greed Index**: {fear_greed_index_new} ({fear_greed_status_new})") |
|
|
if fear_greed_index_new < 25: |
|
|
st.error("⚠️ Extreme Fear → Thị trường rất tiêu cực!") |
|
|
elif fear_greed_index_new > 75: |
|
|
st.warning("⚠️ Extreme Greed → Thị trường có thể quá nóng!") |
|
|
else: |
|
|
st.info("ℹ️ Tâm lý thị trường trung lập.") |
|
|
else: |
|
|
|
|
|
fear_greed_index_new = 21.86 |
|
|
fear_greed_status_new = "Extreme Fear" |
|
|
st.write(f"📉 **Fear & Greed Index (dữ liệu từ hình ảnh)**: {fear_greed_index_new} ({fear_greed_status_new})") |
|
|
st.error("⚠️ Extreme Fear → Thị trường rất tiêu cực!") |
|
|
|
|
|
vix_index = get_vix_index() |
|
|
st.write(f"🛑 **VIX Index**: {vix_index:.1f}" if vix_index else "Không có dữ liệu VIX") |
|
|
|
|
|
bond_yield = get_bond_yield() |
|
|
st.subheader("📊 Trái Phiếu & Lãi Suất") |
|
|
st.write(f"📉 **Lợi suất 10 năm (UST10Y)**: {bond_yield:.2f}%" if bond_yield else "Không có dữ liệu lợi suất") |
|
|
|
|
|
GOLD_API_KEY = "goldapi-7jusm8lqoyp4-io" |
|
|
gold_price, gold_change = get_gold_price_from_goldapi(GOLD_API_KEY) |
|
|
st.subheader("🏆 Vàng (Gold)") |
|
|
if gold_price is not None: |
|
|
st.write(f"Giá vàng: ${gold_price:.2f} | Biến động: {gold_change:.2f}%") |
|
|
if gold_change < 0: |
|
|
st.info("Vàng giảm → Nhà đầu tư đang lạc quan.") |
|
|
else: |
|
|
st.warning("Vàng tăng → Nhà đầu tư đang lo ngại rủi ro.") |
|
|
else: |
|
|
st.write("Không lấy được dữ liệu vàng.") |
|
|
|
|
|
st.subheader("🔄 Gợi Ý ETF Nghịch Đảo") |
|
|
risk_score = 0 |
|
|
if vix_index and vix_index > 20: |
|
|
risk_score += 1 |
|
|
if market_data["SPY"]["change"].startswith("-"): |
|
|
risk_score += 1 |
|
|
if fear_greed_index_new and fear_greed_index_new < 25: |
|
|
risk_score += 1 |
|
|
if "SOXX" in market_data and market_data["SOXX"]["change"].startswith("-"): |
|
|
risk_score += 1 |
|
|
for name, data in futures_data.items(): |
|
|
if data["percent_change"] < -2: |
|
|
risk_score += 1 |
|
|
|
|
|
if risk_score >= 2: |
|
|
st.error(f"⚠️ Thị trường có dấu hiệu suy yếu (Risk Score: {risk_score}/5), có thể xem xét ETF nghịch đảo như SQQQ, SPXS, UVXY.") |
|
|
else: |
|
|
st.success(f"✅ Thị trường chưa có dấu hiệu suy yếu lớn (Risk Score: {risk_score}/5), chưa cần sử dụng ETF nghịch đảo.") |