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)