|
import yfinance as yf |
|
import pandas as pd |
|
import numpy as np |
|
import matplotlib.pyplot as plt |
|
import gradio as gr |
|
import time |
|
|
|
|
|
try: |
|
options_dates = stock.options |
|
except: |
|
time.sleep(5) |
|
options_dates = stock.options |
|
|
|
|
|
def calculate_true_range_half(ticker_symbol): |
|
stock = yf.Ticker(ticker_symbol) |
|
data = stock.history(period="7d", interval="1d") |
|
|
|
if data.empty or len(data) < 5: |
|
return None, None |
|
|
|
|
|
data["True Range"] = data["High"] - data["Low"] |
|
|
|
|
|
total_tr_5 = data["True Range"].tail(5).sum() |
|
|
|
|
|
atr_5 = total_tr_5 / 5 |
|
|
|
|
|
atr_5_half = atr_5 / 2 |
|
|
|
return atr_5, atr_5_half |
|
|
|
|
|
def expected_move_analysis(ticker_symbol): |
|
try: |
|
|
|
stock = yf.Ticker(ticker_symbol) |
|
current_price = stock.fast_info["last_price"] |
|
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] |
|
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 |
|
|
|
|
|
atm_strike = min(calls["strike"], key=lambda x: abs(x - current_price)) |
|
|
|
|
|
atm_call_price = calls[calls["strike"] == atm_strike]["lastPrice"].values[0] |
|
atm_put_price = puts[puts["strike"] == atm_strike]["lastPrice"].values[0] |
|
|
|
|
|
expected_move = (atm_call_price + atm_put_price) * 0.85 |
|
upper_price = current_price + expected_move |
|
lower_price = current_price - expected_move |
|
|
|
|
|
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 |
|
|
|
|
|
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 |
|
|
|
|
|
prob_up, prob_down = 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)) |
|
|
|
|
|
atr_5, atr_5_half = calculate_true_range_half(ticker_symbol) |
|
|
|
|
|
upper_atr5_2 = current_price + atr_5_half |
|
lower_atr5_2 = current_price - atr_5_half |
|
|
|
|
|
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") |
|
|
|
|
|
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() |
|
|
|
|
|
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 |
|
|
|
|
|
|
|
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) |
|
|
|
|
|
import gradio as gr |
|
|
|
|
|
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(): |
|
|
|
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] |
|
) |
|
|
|
|
|
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) |