NHAT_TRAN / app.py
nick5363's picture
Update app.py
e9f7a8e verified
import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import gradio as gr
import time # Thêm dòng này ở đầu file nếu chưa có
# Thử lại nếu bị lỗi rate limit
try:
options_dates = stock.options
except:
time.sleep(5)
options_dates = stock.options
# 📌 Hàm tính True Range trung bình 5 ngày gần nhất và chia 2
def calculate_true_range_half(ticker_symbol):
stock = yf.Ticker(ticker_symbol)
data = stock.history(period="7d", interval="1d") # Lấy dữ liệu 7 ngày để đảm bảo có đủ 5 ngày giao dịch
if data.empty or len(data) < 5:
return None, None # Trả về None nếu không đủ dữ liệu
# 📌 Tính True Range của mỗi ngày
data["True Range"] = data["High"] - data["Low"]
# 📌 Tổng True Range của 5 ngày
total_tr_5 = data["True Range"].tail(5).sum()
# 📌 Tính ATR 5 ngày (TRUNG BÌNH của Tổng TR 5 ngày)
atr_5 = total_tr_5 / 5
# 📌 Chia đôi ATR 5
atr_5_half = atr_5 / 2
return atr_5, atr_5_half
# 📌 Hàm phân tích Expected Move và so sánh với True Range
def expected_move_analysis(ticker_symbol):
try:
# 🟢 Lấy dữ liệu cổ phiếu
stock = yf.Ticker(ticker_symbol)
current_price = stock.fast_info["last_price"] # Giá mới nhất từ phiên giao dịch chính + ngoài giờ
options_dates = stock.options
if not options_dates:
return f"❌ Không có dữ liệu quyền chọn cho {ticker_symbol}.", None
nearest_expiry = options_dates[0] # Ngày hết hạn gần nhất
options_chain = stock.option_chain(nearest_expiry)
calls = options_chain.calls
puts = options_chain.puts
if calls.empty or puts.empty:
return f"❌ Không có dữ liệu quyền chọn tại ngày hết hạn {nearest_expiry}.", None
# 🟢 Xác định giá thực hiện ATM gần nhất
atm_strike = min(calls["strike"], key=lambda x: abs(x - current_price))
# 🟢 Lấy giá quyền chọn CALL và PUT tại ATM Strike
atm_call_price = calls[calls["strike"] == atm_strike]["lastPrice"].values[0]
atm_put_price = puts[puts["strike"] == atm_strike]["lastPrice"].values[0]
# 🟢 Tính Expected Move
expected_move = (atm_call_price + atm_put_price) * 0.85
upper_price = current_price + expected_move
lower_price = current_price - expected_move
# 🟢 Lấy IV của quyền chọn ATM
atm_call_iv = calls[calls["strike"] == atm_strike]["impliedVolatility"].values[0]
atm_put_iv = puts[puts["strike"] == atm_strike]["impliedVolatility"].values[0]
iv_skew = atm_call_iv - atm_put_iv # Chênh lệch IV
# 🟢 Tính Put/Call Ratio (PCR)
total_call_volume = calls["volume"].sum()
total_put_volume = puts["volume"].sum()
pcr = total_put_volume / total_call_volume if total_call_volume > 0 else None
# 🟢 Dự đoán xác suất giá tăng / giảm
prob_up, prob_down = 50, 50 # Mặc định 50/50
if pcr is not None:
if pcr < 0.7:
prob_up += 20
prob_down -= 20
elif pcr > 1.0:
prob_up -= 20
prob_down += 20
if iv_skew > 0:
prob_up += 10
prob_down -= 10
else:
prob_up -= 10
prob_down += 10
prob_up = max(0, min(100, prob_up))
prob_down = max(0, min(100, prob_down))
# 📌 Tính True Range trung bình 5 ngày gần nhất và chia 2
atr_5, atr_5_half = calculate_true_range_half(ticker_symbol)
# 📌 Tính (Giá hiện tại + ATR 5/2) và (Giá hiện tại - ATR 5/2)
upper_atr5_2 = current_price + atr_5_half
lower_atr5_2 = current_price - atr_5_half
# 📈 Vẽ biểu đồ Expected Move & True Range
fig, ax = plt.subplots(figsize=(8, 5))
ax.plot(["Lower Bound", "Current Price", "Upper Bound"],
[lower_price, current_price, upper_price], 'ro-', label="Expected Move")
ax.axhline(y=current_price, color='g', linestyle='--', label="Current Price")
# 🟢 Vẽ đường ATR 5 ngày và ATR 5/2
if atr_5:
ax.axhline(y=current_price + atr_5, color='b', linestyle='-.', label="ATR 5 - Upper")
ax.axhline(y=current_price - atr_5, color='b', linestyle='-.', label="ATR 5 - Lower")
ax.axhline(y=upper_atr5_2, color='purple', linestyle='-.', label="ATR 5/2 - Upper")
ax.axhline(y=lower_atr5_2, color='purple', linestyle='-.', label="ATR 5/2 - Lower")
ax.set_title(f"Expected Move vs True Range for {ticker_symbol}")
ax.set_ylabel("Price")
ax.legend()
ax.grid()
# 📊 Kết quả hiển thị
result_text = f"""
📊 **Dự đoán giá cho {ticker_symbol}**
- **Giá hiện tại**: {current_price:.2f} USD
- **Biên độ**: {expected_move:.2f} USD
- **Giá dự đoán cao nhất**: {upper_price:.2f} USD
- **Giá dự đoán thấp nhất**: {lower_price:.2f} USD
- **ATR 5/2**: {atr_5_half:.2f} USD
- **(+ ATR 5/2)**: {upper_atr5_2:.2f} USD
- **(- ATR 5/2)**: {lower_atr5_2:.2f} USD
🔥 **Put/Call Ratio (PCR)**: {pcr:.2f}
- **IV Call ATM**: {atm_call_iv:.2%}
- **IV Put ATM**: {atm_put_iv:.2%}
- **IV Skew**: {iv_skew:.2%}
📈 **Xác suất giá tăng phiên kế**: {prob_up}%
📉 **Xác suất giá giảm phiên kế**: {prob_down}%
"""
return result_text, fig
except Exception as e:
return f"❌ Lỗi: {str(e)}", None
# Hàm so sánh Outperform/Underperform theo Sector
def compare_sector_performance(symbols_input, period):
sector_to_etf = {
'Technology': 'XLK',
'Health Care': 'XLV',
'Financial Services': 'XLF',
'Consumer Cyclical': 'XLY',
'Energy': 'XLE',
'Industrials': 'XLI',
'Consumer Defensive': 'XLP',
'Utilities': 'XLU',
'Materials': 'XLB',
'Real Estate': 'XLRE',
'Communication Services': 'XLC'
}
def get_sector_etf(symbol):
try:
ticker = yf.Ticker(symbol)
info = ticker.info
sector = info.get("sector", "Không rõ")
etf = sector_to_etf.get(sector, "Không rõ")
return sector, etf
except Exception as e:
print(f"Lỗi khi lấy sector: {e}")
return "Không rõ", "Không rõ"
if not symbols_input:
return pd.DataFrame([{"Symbol": "N/A", "Sector ETF": "N/A", "Kết luận": "Vui lòng nhập mã cổ phiếu"}])
symbols = [s.strip().upper() for s in symbols_input.split(",")]
results = []
for symbol in symbols:
sector, etf = get_sector_etf(symbol)
if etf == "Không rõ":
results.append({"Symbol": symbol, "Sector ETF": "Không rõ", "Kết luận": "Không phân tích được"})
continue
try:
stock_data = yf.download(symbol, period=period)['Close']
etf_data = yf.download(etf, period=period)['Close']
if stock_data.empty or etf_data.empty or len(stock_data) < 2 or len(etf_data) < 2:
results.append({"Symbol": symbol, "Sector ETF": etf, "Kết luận": "Dữ liệu không đủ"})
continue
pct_stock = float((stock_data.iloc[-1] - stock_data.iloc[0]) / stock_data.iloc[0] * 100)
pct_sector = float((etf_data.iloc[-1] - etf_data.iloc[0]) / etf_data.iloc[0] * 100)
verdict = "Outperform" if pct_stock > pct_sector else "Underperform"
results.append({
"Symbol": symbol,
"Sector ETF": etf,
"% Stock": f"{pct_stock:.2f}%",
"% Sector": f"{pct_sector:.2f}%",
"Kết luận": verdict
})
except Exception as e:
results.append({"Symbol": symbol, "Sector ETF": etf, "Kết luận": f"Lỗi: {str(e)}"})
return pd.DataFrame(results)
# Giao diện Gradio
import gradio as gr
# Custom CSS để làm nền đen, text trắng, border cam
custom_css = """
body {
background-color: #0f0f0f !important;
}
.gr-textbox, .gr-dropdown, .gr-button, .gr-dataframe, .gr-plot, .gr-markdown, .gr-tabs {
background-color: #1f1f1f !important;
color: #ffffff !important;
border: 1.5px solid #ff7f00 !important;
border-radius: 6px !important;
}
.gr-button {
background-color: #ff7f00 !important;
color: #000 !important;
font-weight: bold;
}
.gr-button:hover {
background-color: #e66e00 !important;
}
"""
with gr.Blocks(css=custom_css, title="Dashboard Phân Tích") as app:
with gr.Tabs():
# Tab 1: Expected Move
with gr.Tab("Expected Move"):
gr.Markdown("### Dự đoán biến động (Expected Move)")
with gr.Row():
ticker_input = gr.Textbox(label="Nhập mã cổ phiếu", value="NVDA")
expected_button = gr.Button("Phân Tích")
expected_output = gr.Textbox(label="Kết quả", lines=10)
expected_plot = gr.Plot(label="Biểu đồ")
expected_button.click(
fn=expected_move_analysis,
inputs=ticker_input,
outputs=[expected_output, expected_plot]
)
# Tab 2: So sánh Sector
with gr.Tab("Sector Comparison"):
gr.Markdown("### So sánh hiệu suất giữa các cổ phiếu hoặc ngành")
symbols_input = gr.Textbox(label="Nhập mã cổ phiếu (cách nhau bằng dấu phẩy)", value="AAPL,TSLA")
period = gr.Dropdown(
label="Chọn khoảng thời gian",
choices=['5d', '1mo', '3mo', '6mo'],
value='1mo'
)
sector_button = gr.Button("Phân Tích")
sector_output = gr.Dataframe(label="Kết quả")
sector_button.click(
fn=compare_sector_performance,
inputs=[symbols_input, period],
outputs=sector_output
)
app.launch(share=True)