File size: 10,409 Bytes
933e34e
 
 
 
 
e9f7a8e
 
 
 
 
 
 
 
933e34e
da58fa7
933e34e
da58fa7
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
933e34e
 
da58fa7
933e34e
da58fa7
933e34e
da58fa7
933e34e
da58fa7
933e34e
da58fa7
933e34e
 
 
 
 
da58fa7
933e34e
da58fa7
933e34e
da58fa7
 
933e34e
 
 
da58fa7
933e34e
 
 
 
da58fa7
933e34e
 
da58fa7
933e34e
da58fa7
933e34e
 
da58fa7
 
 
 
933e34e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
da58fa7
933e34e
 
da58fa7
933e34e
 
 
da58fa7
933e34e
 
 
 
da58fa7
 
 
 
 
 
 
 
933e34e
 
 
 
 
da58fa7
933e34e
 
 
 
 
 
 
 
 
 
da58fa7
 
 
 
933e34e
da58fa7
 
933e34e
 
 
 
 
da58fa7
 
933e34e
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
d3991b4
0b86578
7a17d26
f6a63fd
366eb47
7a17d26
366eb47
7a17d26
 
 
 
 
f6a63fd
 
7a17d26
 
 
f6a63fd
 
7a17d26
366eb47
a68b264
2c04fa0
a68b264
7a17d26
d3991b4
7a17d26
7d93a61
d3991b4
 
2c04fa0
f6a63fd
7a17d26
f6a63fd
 
 
 
 
 
d3991b4
7a17d26
d3991b4
 
f6a63fd
7a17d26
 
 
 
 
f6a63fd
7a17d26
f6a63fd
 
 
 
 
 
3d661dc
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
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
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
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)