Triển khai chiến lược đầu tư cổ tức với Python
2024-03-07 — QuantTrade
Giới Thiệu
Đầu tư cổ tức là một chiến lược đầu tư phổ biến, tập trung vào việc xây dựng danh mục các cổ phiếu trả cổ tức đều đặn và hấp dẫn. Chiến lược này đặc biệt phù hợp với nhà đầu tư dài hạn, những người tìm kiếm dòng thu nhập ổn định và sự tăng trưởng vốn từ từ theo thời gian. Python, với hệ sinh thái phong phú các thư viện phân tích dữ liệu và tài chính, là công cụ lý tưởng để triển khai, backtest và tối ưu hóa chiến lược đầu tư cổ tức.
Trong bài viết này, chúng ta sẽ khám phá cách sử dụng Python để:
- Phân tích và lựa chọn cổ phiếu trả cổ tức tốt
- Xây dựng và tối ưu hóa danh mục đầu tư cổ tức
- Backtest chiến lược theo dữ liệu lịch sử
- Theo dõi và tái cân bằng danh mục theo thời gian
- Tự động hóa quy trình xây dựng và quản lý danh mục đầu tư cổ tức
Phần 1: Chuẩn Bị Dữ Liệu và Công Cụ
1.1 Cài Đặt Các Thư Viện Cần Thiết
# Cài đặt các thư viện
# pip install pandas numpy matplotlib seaborn yfinance pandas-datareader plotly scikit-learn
# Import các thư viện
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import yfinance as yf
import pandas_datareader.data as web
import plotly.express as px
import plotly.graph_objects as go
from plotly.subplots import make_subplots
from datetime import datetime, timedelta
from sklearn.preprocessing import StandardScaler
1.2 Thu Thập Dữ Liệu Cổ Phiếu và Cổ Tức
Sử dụng Yahoo Finance API để lấy dữ liệu
def get_stock_data(ticker, start_date, end_date=None):
"""
Lấy dữ liệu giá và cổ tức của một cổ phiếu
Tham số:
---------
ticker : str
Mã cổ phiếu
start_date : str
Ngày bắt đầu (định dạng: 'YYYY-MM-DD')
end_date : str, tùy chọn
Ngày kết thúc (định dạng: 'YYYY-MM-DD'), mặc định là ngày hiện tại
Trả về:
--------
tuple
(df_price, df_dividends) - Dữ liệu giá và dữ liệu cổ tức
"""
if end_date is None:
end_date = datetime.now().strftime('%Y-%m-%d')
# Lấy thông tin cổ phiếu từ yfinance
stock = yf.Ticker(ticker)
# Lấy dữ liệu giá
df_price = stock.history(start=start_date, end=end_date)
# Lấy dữ liệu cổ tức
df_dividends = stock.dividends.to_frame()
df_dividends.columns = ['Dividend']
df_dividends = df_dividends.loc[start_date:end_date]
return df_price, df_dividends
# Ví dụ sử dụng hàm
ticker = 'VNM.HM' # Vinamilk
start_date = '2018-01-01'
price_data, dividend_data = get_stock_data(ticker, start_date)
print(f"Dữ liệu giá {ticker}:")
print(price_data.head())
print(f"\nDữ liệu cổ tức {ticker}:")
print(dividend_data)
Thu thập dữ liệu cho nhiều cổ phiếu
def get_multiple_stocks_data(tickers, start_date, end_date=None):
"""
Lấy dữ liệu giá và cổ tức cho nhiều cổ phiếu
Tham số:
---------
tickers : list
Danh sách mã cổ phiếu
start_date : str
Ngày bắt đầu (định dạng: 'YYYY-MM-DD')
end_date : str, tùy chọn
Ngày kết thúc (định dạng: 'YYYY-MM-DD'), mặc định là ngày hiện tại
Trả về:
--------
dict
Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
"""
if end_date is None:
end_date = datetime.now().strftime('%Y-%m-%d')
data = {}
for ticker in tickers:
try:
price_data, dividend_data = get_stock_data(ticker, start_date, end_date)
data[ticker] = {
'price': price_data,
'dividends': dividend_data
}
print(f"Đã lấy dữ liệu cho {ticker}")
except Exception as e:
print(f"Lỗi khi lấy dữ liệu cho {ticker}: {e}")
return data
# Ví dụ với một vài cổ phiếu trả cổ tức tốt tại Việt Nam
vn_dividend_tickers = ['VNM.HM', 'MSN.HM', 'FPT.HM', 'PNJ.HM', 'MWG.HM']
stocks_data = get_multiple_stocks_data(vn_dividend_tickers, '2018-01-01')
1.3 Tính Toán Các Chỉ Số Cổ Tức Quan Trọng
def calculate_dividend_metrics(ticker, price_data, dividend_data):
"""
Tính toán các chỉ số liên quan đến cổ tức
Tham số:
---------
ticker : str
Mã cổ phiếu
price_data : DataFrame
Dữ liệu giá
dividend_data : DataFrame
Dữ liệu cổ tức
Trả về:
--------
dict
Từ điển chứa các chỉ số cổ tức
"""
# Nếu không có dữ liệu cổ tức
if dividend_data.empty:
return {
'ticker': ticker,
'current_dividend_yield': 0,
'average_dividend_yield': 0,
'annual_dividend': 0,
'payout_frequency': 0,
'dividend_growth_rate': 0,
'years_of_dividend_growth': 0,
'payout_ratio': 0
}
# Giá hiện tại
current_price = price_data['Close'].iloc[-1]
# Tổng cổ tức trong 12 tháng gần nhất
one_year_ago = datetime.now() - timedelta(days=365)
recent_dividends = dividend_data[dividend_data.index >= one_year_ago.strftime('%Y-%m-%d')]
ttm_dividend = recent_dividends['Dividend'].sum()
# Tính tỷ suất cổ tức hiện tại
current_dividend_yield = (ttm_dividend / current_price) * 100 if ttm_dividend > 0 else 0
# Tính tần suất chi trả cổ tức (số lần/năm)
years = (dividend_data.index[-1] - dividend_data.index[0]).days / 365
payout_frequency = len(dividend_data) / years if years > 0 else 0
# Tính tỷ suất cổ tức trung bình trong lịch sử
dividend_yields = []
for date, row in dividend_data.iterrows():
# Tìm giá đóng cửa gần nhất trước ngày chia cổ tức
closest_price_idx = price_data.index.get_indexer([date], method='pad')[0]
if closest_price_idx >= 0:
price = price_data['Close'].iloc[closest_price_idx]
dividend_yield = (row['Dividend'] / price) * 100
dividend_yields.append(dividend_yield)
average_dividend_yield = np.mean(dividend_yields) if dividend_yields else 0
# Tính tốc độ tăng trưởng cổ tức
if len(dividend_data) > 1:
# Nhóm theo năm và tính tổng cổ tức hàng năm
annual_dividends = dividend_data.groupby(dividend_data.index.year)['Dividend'].sum()
if len(annual_dividends) > 1:
# Tính tốc độ tăng trưởng hàng năm
first_year_div = annual_dividends.iloc[0]
last_year_div = annual_dividends.iloc[-1]
years_passed = len(annual_dividends) - 1
# Sử dụng CAGR (Compound Annual Growth Rate)
dividend_growth_rate = ((last_year_div / first_year_div) ** (1 / years_passed) - 1) * 100 if first_year_div > 0 else 0
years_of_consecutive_growth = 0
# Đếm số năm tăng trưởng cổ tức liên tiếp
for i in range(1, len(annual_dividends)):
if annual_dividends.iloc[i] > annual_dividends.iloc[i-1]:
years_of_consecutive_growth += 1
else:
break
else:
dividend_growth_rate = 0
years_of_consecutive_growth = 0
else:
dividend_growth_rate = 0
years_of_consecutive_growth = 0
# Lấy thông tin tài chính để tính tỷ lệ chi trả
try:
stock = yf.Ticker(ticker)
info = stock.info
# Tính tỷ lệ chi trả (Payout ratio)
if 'trailingEps' in info and info['trailingEps'] and info['trailingEps'] > 0:
payout_ratio = (ttm_dividend / info['trailingEps']) * 100
else:
payout_ratio = 0
except:
payout_ratio = 0
return {
'ticker': ticker,
'current_dividend_yield': current_dividend_yield,
'average_dividend_yield': average_dividend_yield,
'annual_dividend': ttm_dividend,
'payout_frequency': payout_frequency,
'dividend_growth_rate': dividend_growth_rate,
'years_of_dividend_growth': years_of_consecutive_growth,
'payout_ratio': payout_ratio
}
# Ví dụ tính toán chỉ số cổ tức cho một cổ phiếu
metrics = calculate_dividend_metrics('VNM.HM', price_data, dividend_data)
print(metrics)
Phần 2: Phân Tích và Lựa Chọn Cổ Phiếu Trả Cổ Tức
2.1 Tạo Bảng So Sánh Các Cổ Phiếu Theo Tiêu Chí Cổ Tức
def create_dividend_comparison_table(stocks_data):
"""
Tạo bảng so sánh các cổ phiếu theo tiêu chí cổ tức
Tham số:
---------
stocks_data : dict
Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
Trả về:
--------
DataFrame
Bảng so sánh các cổ phiếu
"""
metrics_list = []
for ticker, data in stocks_data.items():
metrics = calculate_dividend_metrics(ticker, data['price'], data['dividends'])
metrics_list.append(metrics)
# Tạo DataFrame từ list các metrics
comparison_df = pd.DataFrame(metrics_list)
# Sắp xếp theo tỷ suất cổ tức giảm dần
comparison_df = comparison_df.sort_values('current_dividend_yield', ascending=False)
return comparison_df
# Tạo bảng so sánh
comparison_table = create_dividend_comparison_table(stocks_data)
print(comparison_table)
# Hiển thị dạng biểu đồ cột
plt.figure(figsize=(14, 8))
sns.barplot(x='ticker', y='current_dividend_yield', data=comparison_table)
plt.title('So sánh tỷ suất cổ tức hiện tại giữa các cổ phiếu')
plt.xlabel('Mã cổ phiếu')
plt.ylabel('Tỷ suất cổ tức (%)')
plt.xticks(rotation=45)
plt.tight_layout()
plt.show()
2.2 Phân Tích Lịch Sử Chi Trả Cổ Tức
def analyze_dividend_history(ticker, price_data, dividend_data):
"""
Phân tích lịch sử chi trả cổ tức của một cổ phiếu
Tham số:
---------
ticker : str
Mã cổ phiếu
price_data : DataFrame
Dữ liệu giá
dividend_data : DataFrame
Dữ liệu cổ tức
"""
if dividend_data.empty:
print(f"{ticker} không có dữ liệu cổ tức trong khoảng thời gian đã chọn.")
return
# Tính tỷ suất cổ tức tại thời điểm chi trả
dividend_yields = []
for date, row in dividend_data.iterrows():
# Tìm giá đóng cửa gần nhất trước ngày chia cổ tức
closest_price_idx = price_data.index.get_indexer([date], method='pad')[0]
if closest_price_idx >= 0:
price = price_data['Close'].iloc[closest_price_idx]
dividend_yield = (row['Dividend'] / price) * 100
dividend_yields.append({
'date': date,
'dividend': row['Dividend'],
'price': price,
'yield': dividend_yield
})
# Tạo DataFrame từ danh sách
dividend_history = pd.DataFrame(dividend_yields)
# Nhóm theo năm để phân tích xu hướng
annual_dividends = dividend_data.groupby(dividend_data.index.year)['Dividend'].sum()
# Vẽ biểu đồ lịch sử cổ tức
fig = make_subplots(rows=2, cols=1,
shared_xaxes=True,
subplot_titles=('Cổ tức theo thời gian', 'Tỷ suất cổ tức tại thời điểm chi trả'))
# Cổ tức theo thời gian
fig.add_trace(
go.Bar(x=dividend_data.index, y=dividend_data['Dividend'], name='Cổ tức'),
row=1, col=1
)
# Tỷ suất cổ tức
if not dividend_history.empty:
fig.add_trace(
go.Scatter(x=dividend_history['date'], y=dividend_history['yield'],
mode='markers+lines', name='Tỷ suất cổ tức (%)'),
row=2, col=1
)
fig.update_layout(
title=f'Lịch sử chi trả cổ tức của {ticker}',
height=600,
width=1000
)
fig.show()
# In thống kê
print(f"Thống kê cổ tức của {ticker}:")
print(f"Tổng số lần chi trả cổ tức: {len(dividend_data)}")
print(f"Cổ tức trung bình: {dividend_data['Dividend'].mean():.2f}")
print(f"Tỷ suất cổ tức trung bình tại thời điểm chi trả: {dividend_history['yield'].mean():.2f}%")
# In xu hướng cổ tức hàng năm
print("\nCổ tức hàng năm:")
for year, dividend in annual_dividends.items():
print(f"{year}: {dividend:.2f}")
# Phân tích lịch sử chi trả cổ tức cho một cổ phiếu
analyze_dividend_history('VNM.HM', price_data, dividend_data)
2.3 Xây Dựng Bộ Lọc Cổ Phiếu Trả Cổ Tức
def filter_dividend_stocks(comparison_df, min_yield=3.0, min_frequency=2, min_growth_rate=0, max_payout_ratio=80):
"""
Lọc cổ phiếu theo các tiêu chí về cổ tức
Tham số:
---------
comparison_df : DataFrame
Bảng so sánh các cổ phiếu
min_yield : float
Tỷ suất cổ tức tối thiểu (%)
min_frequency : int
Tần suất chi trả tối thiểu (số lần/năm)
min_growth_rate : float
Tốc độ tăng trưởng cổ tức tối thiểu (%)
max_payout_ratio : float
Tỷ lệ chi trả tối đa (%)
Trả về:
--------
DataFrame
Các cổ phiếu thỏa mãn điều kiện
"""
filtered = comparison_df[
(comparison_df['current_dividend_yield'] >= min_yield) &
(comparison_df['payout_frequency'] >= min_frequency) &
(comparison_df['dividend_growth_rate'] >= min_growth_rate) &
((comparison_df['payout_ratio'] <= max_payout_ratio) | (comparison_df['payout_ratio'] == 0))
]
return filtered
# Lọc cổ phiếu theo tiêu chí
filtered_stocks = filter_dividend_stocks(comparison_table, min_yield=2.0, min_frequency=1)
print("Các cổ phiếu thỏa mãn tiêu chí:")
print(filtered_stocks)
Phần 3: Xây Dựng và Tối Ưu Hóa Danh Mục Đầu Tư Cổ Tức
3.1 Phân Tích Tương Quan Giữa Các Cổ Phiếu
def analyze_correlation(stocks_data, selected_tickers):
"""
Phân tích tương quan giữa các cổ phiếu
Tham số:
---------
stocks_data : dict
Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
selected_tickers : list
Danh sách mã cổ phiếu được chọn
Trả về:
--------
DataFrame
Ma trận tương quan
"""
# Tạo DataFrame chứa giá đóng cửa của các cổ phiếu
close_prices = pd.DataFrame()
for ticker in selected_tickers:
if ticker in stocks_data:
close_prices[ticker] = stocks_data[ticker]['price']['Close']
# Tính toán lợi nhuận hàng ngày
returns = close_prices.pct_change().dropna()
# Tính ma trận tương quan
correlation_matrix = returns.corr()
# Vẽ heatmap tương quan
plt.figure(figsize=(10, 8))
sns.heatmap(correlation_matrix, annot=True, cmap='coolwarm', linewidths=0.5)
plt.title('Ma trận tương quan giữa các cổ phiếu')
plt.tight_layout()
plt.show()
return correlation_matrix
# Phân tích tương quan giữa các cổ phiếu đã lọc
selected_tickers = filtered_stocks['ticker'].tolist()
correlation_matrix = analyze_correlation(stocks_data, selected_tickers)
3.2 Tối Ưu Hóa Danh Mục Đầu Tư Theo Tiêu Chí Đa Dạng Hóa và Tối Đa Cổ Tức
from scipy.optimize import minimize
def optimize_dividend_portfolio(stocks_data, selected_tickers, risk_tolerance=0.5):
"""
Tối ưu hóa danh mục đầu tư cổ tức
Tham số:
---------
stocks_data : dict
Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
selected_tickers : list
Danh sách mã cổ phiếu được chọn
risk_tolerance : float
Mức chấp nhận rủi ro (0-1), 0 = ưu tiên giảm rủi ro, 1 = ưu tiên tối đa cổ tức
Trả về:
--------
dict
Trọng số của các cổ phiếu trong danh mục tối ưu
"""
# Lấy dữ liệu tỷ suất cổ tức và tỷ suất lợi nhuận
dividend_yields = []
returns_data = pd.DataFrame()
for ticker in selected_tickers:
if ticker in stocks_data:
# Lấy tỷ suất cổ tức
price_data = stocks_data[ticker]['price']
dividend_data = stocks_data[ticker]['dividends']
metrics = calculate_dividend_metrics(ticker, price_data, dividend_data)
dividend_yields.append(metrics['current_dividend_yield'])
# Lấy lợi nhuận hàng ngày
returns_data[ticker] = price_data['Close'].pct_change()
# Xử lý dữ liệu thiếu
returns_data = returns_data.dropna()
# Tính ma trận hiệp phương sai
cov_matrix = returns_data.cov() * 252 # Chuyển về hiệp phương sai hàng năm
# Số lượng cổ phiếu
n = len(selected_tickers)
# Tạo hàm mục tiêu để tối đa hóa tỷ suất cổ tức và giảm thiểu rủi ro
def objective(weights):
# Tỷ suất cổ tức của danh mục
portfolio_yield = np.sum(weights * dividend_yields)
# Rủi ro (độ lệch chuẩn) của danh mục
portfolio_risk = np.sqrt(np.dot(weights.T, np.dot(cov_matrix, weights)))
# Hàm mục tiêu kết hợp: tối đa hóa tỷ suất cổ tức và giảm thiểu rủi ro
# Sử dụng risk_tolerance để điều chỉnh mức độ ưu tiên
return -(risk_tolerance * portfolio_yield - (1 - risk_tolerance) * portfolio_risk)
# Ràng buộc: tổng trọng số = 1 và trọng số không âm
constraints = ({'type': 'eq', 'fun': lambda x: np.sum(x) - 1})
bounds = tuple((0, 1) for _ in range(n))
# Trọng số ban đầu (đều nhau)
initial_weights = np.array([1/n] * n)
# Thực hiện tối ưu hóa
result = minimize(objective, initial_weights, method='SLSQP', bounds=bounds, constraints=constraints)
# Trả về trọng số tối ưu
optimal_weights = result['x']
# Tính toán các chỉ số của danh mục tối ưu
portfolio_yield = np.sum(optimal_weights * dividend_yields)
portfolio_risk = np.sqrt(np.dot(optimal_weights.T, np.dot(cov_matrix, optimal_weights)))
print("Danh mục đầu tư tối ưu:")
print(f"Tỷ suất cổ tức ước tính: {portfolio_yield:.2f}%")
print(f"Độ lệch chuẩn hàng năm: {portfolio_risk:.2f}%")
# Tạo từ điển kết quả
portfolio = {ticker: weight for ticker, weight in zip(selected_tickers, optimal_weights)}
# Vẽ biểu đồ phân bổ
plt.figure(figsize=(12, 6))
plt.pie(optimal_weights, labels=selected_tickers, autopct='%1.1f%%', startangle=90, shadow=True)
plt.title('Phân bổ danh mục đầu tư cổ tức tối ưu')
plt.axis('equal')
plt.tight_layout()
plt.show()
return portfolio
# Tối ưu hóa danh mục đầu tư
optimal_portfolio = optimize_dividend_portfolio(stocks_data, selected_tickers, risk_tolerance=0.7)
print("Trọng số tối ưu:")
for ticker, weight in sorted(optimal_portfolio.items(), key=lambda x: x[1], reverse=True):
print(f"{ticker}: {weight*100:.2f}%")
3.3 Đánh Giá Hiệu Suất Lịch Sử của Danh Mục Đầu Tư
def evaluate_portfolio_performance(stocks_data, portfolio, start_date):
"""
Đánh giá hiệu suất lịch sử của danh mục đầu tư
Tham số:
---------
stocks_data : dict
Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
portfolio : dict
Trọng số của các cổ phiếu trong danh mục
start_date : str
Ngày bắt đầu đánh giá (định dạng: 'YYYY-MM-DD')
Trả về:
--------
DataFrame
Dữ liệu hiệu suất của danh mục theo thời gian
"""
# Lấy danh sách cổ phiếu trong danh mục
tickers = list(portfolio.keys())
# Tạo DataFrame chứa giá đóng cửa và cổ tức của các cổ phiếu
portfolio_data = pd.DataFrame()
# Lấy dữ liệu giá
for ticker in tickers:
if ticker in stocks_data:
portfolio_data[f'{ticker}_price'] = stocks_data[ticker]['price']['Close']
# Tính toán giá trị danh mục (giả định bắt đầu với 1 tỷ đồng)
initial_investment = 1_000_000_000 # 1 tỷ đồng
# Tính số lượng cổ phiếu ban đầu
start_date_pd = pd.to_datetime(start_date)
# Tìm ngày gần nhất sau start_date có dữ liệu
closest_date = portfolio_data.index[portfolio_data.index >= start_date_pd][0]
# Tính số lượng cổ phiếu mua ban đầu
stock_quantities = {}
for ticker in tickers:
price = portfolio_data[f'{ticker}_price'].loc[closest_date]
allocation = portfolio[ticker] * initial_investment
quantity = allocation / price
stock_quantities[ticker] = quantity
# Tạo chuỗi giá trị danh mục theo thời gian
portfolio_values = pd.Series(index=portfolio_data.index[portfolio_data.index >= closest_date])
# Tính giá trị danh mục không bao gồm cổ tức
for date in portfolio_values.index:
value = 0
for ticker in tickers:
if date in portfolio_data.index:
price = portfolio_data[f'{ticker}_price'].loc[date]
value += stock_quantities[ticker] * price
portfolio_values[date] = value
# Tính tổng giá trị cổ tức nhận được
total_dividends = 0
dividend_cashflow = pd.Series(0, index=portfolio_values.index)
for ticker in tickers:
if ticker in stocks_data and not stocks_data[ticker]['dividends'].empty:
div_data = stocks_data[ticker]['dividends']
# Chỉ xét các cổ tức sau ngày bắt đầu
div_data = div_data[div_data.index >= closest_date]
for date, row in div_data.iterrows():
dividend_per_share = row['Dividend']
dividend_amount = stock_quantities[ticker] * dividend_per_share
total_dividends += dividend_amount
# Thêm vào chuỗi dòng tiền cổ tức
if date in dividend_cashflow.index:
dividend_cashflow[date] += dividend_amount
# Tạo DataFrame kết quả
result = pd.DataFrame({
'Portfolio_Value': portfolio_values,
'Dividend_Cashflow': dividend_cashflow
})
# Tính lợi nhuận lũy kế từ cổ tức
result['Cumulative_Dividends'] = dividend_cashflow.cumsum()
# Tính tổng giá trị (bao gồm cả cổ tức)
result['Total_Value'] = result['Portfolio_Value'] + result['Cumulative_Dividends']
# Tính lợi nhuận
result['Return'] = result['Total_Value'] / initial_investment - 1
# Vẽ biểu đồ hiệu suất
fig = make_subplots(rows=2, cols=1,
shared_xaxes=True,
subplot_titles=('Giá trị danh mục theo thời gian', 'Dòng tiền cổ tức'))
# Giá trị danh mục
fig.add_trace(
go.Scatter(x=result.index, y=result['Portfolio_Value'], name='Giá trị danh mục'),
row=1, col=1
)
fig.add_trace(
go.Scatter(x=result.index, y=result['Total_Value'], name='Tổng giá trị (bao gồm cổ tức)'),
row=1, col=1
)
# Dòng tiền cổ tức
fig.add_trace(
go.Bar(x=result.index, y=result['Dividend_Cashflow'], name='Cổ tức theo kỳ'),
row=2, col=1
)
fig.add_trace(
go.Scatter(x=result.index, y=result['Cumulative_Dividends'], name='Cổ tức lũy kế'),
row=2, col=1
)
fig.update_layout(
title='Hiệu suất danh mục đầu tư cổ tức',
height=800,
width=1000
)
fig.show()
# Tính các chỉ số hiệu suất
years = (result.index[-1] - result.index[0]).days / 365
annualized_return = (result['Return'].iloc[-1] + 1) ** (1 / years) - 1 if years > 0 else 0
dividend_yield = total_dividends / initial_investment / years * 100 if years > 0 else 0
print("Hiệu suất danh mục đầu tư:")
print(f"Tổng lợi nhuận: {result['Return'].iloc[-1]*100:.2f}%")
print(f"Lợi nhuận hàng năm: {annualized_return*100:.2f}%")
print(f"Tổng cổ tức nhận được: {total_dividends:,.0f} đồng")
print(f"Tỷ suất cổ tức thực tế hàng năm: {dividend_yield:.2f}%")
print(f"Tỷ lệ cổ tức/tổng lợi nhuận: {(total_dividends / (result['Total_Value'].iloc[-1] - initial_investment) * 100):.2f}% (nếu tổng lợi nhuận > 0)")
return result
# Đánh giá hiệu suất danh mục
portfolio_performance = evaluate_portfolio_performance(stocks_data, optimal_portfolio, '2018-01-01')
Phần 4: Theo Dõi và Tái Cân Bằng Danh Mục
4.1 Theo Dõi Chỉ Số Cổ Tức Theo Thời Gian
def track_dividend_metrics_over_time(stocks_data, portfolio, window=90):
"""
Theo dõi các chỉ số cổ tức của danh mục theo thời gian
Tham số:
---------
stocks_data : dict
Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
portfolio : dict
Trọng số của các cổ phiếu trong danh mục
window : int
Cửa sổ thời gian (số ngày) để tính tỷ suất cổ tức
Trả về:
--------
DataFrame
Dữ liệu các chỉ số cổ tức theo thời gian
"""
# Lấy danh sách cổ phiếu trong danh mục
tickers = list(portfolio.keys())
# Tạo DataFrame để lưu trữ kết quả
all_dates = set()
for ticker in tickers:
if ticker in stocks_data:
all_dates.update(stocks_data[ticker]['price'].index)
all_dates = sorted(all_dates)
metrics_over_time = pd.DataFrame(index=all_dates)
# Tính tỷ suất cổ tức của danh mục theo thời gian
portfolio_yield = pd.Series(index=all_dates)
for date in all_dates:
# Xem xét cửa sổ thời gian trước ngày hiện tại
start_window = date - pd.Timedelta(days=window)
weighted_yield = 0
total_weight = 0
for ticker in tickers:
if ticker in stocks_data:
# Lấy dữ liệu trong cửa sổ thời gian
price_data = stocks_data[ticker]['price']
dividend_data = stocks_data[ticker]['dividends']
if date in price_data.index:
# Lấy giá đóng cửa tại ngày hiện tại
current_price = price_data.loc[date, 'Close']
# Tính tổng cổ tức trong cửa sổ thời gian
window_dividends = dividend_data[(dividend_data.index >= start_window) & (dividend_data.index <= date)]
total_dividend = window_dividends['Dividend'].sum() if not window_dividends.empty else 0
# Tính tỷ suất cổ tức (đã điều chỉnh theo năm)
annualized_factor = 365 / window
dividend_yield = (total_dividend / current_price) * 100 * annualized_factor if total_dividend > 0 else 0
# Tính tỷ suất cổ tức có trọng số
weighted_yield += dividend_yield * portfolio[ticker]
total_weight += portfolio[ticker]
# Lưu tỷ suất cổ tức của danh mục
if total_weight > 0:
portfolio_yield[date] = weighted_yield / total_weight
else:
portfolio_yield[date] = 0
# Thêm vào DataFrame kết quả
metrics_over_time['Portfolio_Dividend_Yield'] = portfolio_yield
# Vẽ biểu đồ tỷ suất cổ tức theo thời gian
plt.figure(figsize=(14, 6))
plt.plot(metrics_over_time.index, metrics_over_time['Portfolio_Dividend_Yield'])
plt.title(f'Tỷ suất cổ tức của danh mục theo thời gian (cửa sổ {window} ngày)')
plt.xlabel('Ngày')
plt.ylabel('Tỷ suất cổ tức (%)')
plt.grid(True)
plt.tight_layout()
plt.show()
return metrics_over_time
# Theo dõi chỉ số cổ tức theo thời gian
dividend_metrics = track_dividend_metrics_over_time(stocks_data, optimal_portfolio)
4.2 Tái Cân Bằng Danh Mục Đầu Tư
def rebalance_portfolio(stocks_data, current_portfolio, rebalance_date, risk_tolerance=0.5):
"""
Tái cân bằng danh mục đầu tư
Tham số:
---------
stocks_data : dict
Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
current_portfolio : dict
Trọng số hiện tại của các cổ phiếu trong danh mục
rebalance_date : str
Ngày tái cân bằng (định dạng: 'YYYY-MM-DD')
risk_tolerance : float
Mức chấp nhận rủi ro (0-1)
Trả về:
--------
dict
Trọng số mới của các cổ phiếu trong danh mục
"""
# Lọc dữ liệu đến ngày tái cân bằng
rebalance_date_pd = pd.to_datetime(rebalance_date)
filtered_data = {}
for ticker, data in stocks_data.items():
if ticker in current_portfolio:
price_data = data['price'][data['price'].index <= rebalance_date_pd].copy()
dividend_data = data['dividends'][data['dividends'].index <= rebalance_date_pd].copy()
if not price_data.empty:
filtered_data[ticker] = {
'price': price_data,
'dividends': dividend_data
}
# Tái tối ưu hóa danh mục
tickers = list(current_portfolio.keys())
new_portfolio = optimize_dividend_portfolio(filtered_data, tickers, risk_tolerance)
# So sánh danh mục cũ và mới
comparison = pd.DataFrame({
'Trọng số cũ': pd.Series(current_portfolio),
'Trọng số mới': pd.Series(new_portfolio)
})
comparison['Thay đổi'] = comparison['Trọng số mới'] - comparison['Trọng số cũ']
comparison['Thay đổi (%)'] = comparison['Thay đổi'] * 100
print(f"So sánh trọng số danh mục trước và sau khi tái cân bằng (ngày {rebalance_date}):")
print(comparison)
# Vẽ biểu đồ so sánh
plt.figure(figsize=(12, 6))
x = np.arange(len(tickers))
width = 0.35
plt.bar(x - width/2, [current_portfolio[t] * 100 for t in tickers], width, label='Trọng số cũ')
plt.bar(x + width/2, [new_portfolio[t] * 100 for t in tickers], width, label='Trọng số mới')
plt.xlabel('Cổ phiếu')
plt.ylabel('Trọng số (%)')
plt.title('So sánh trọng số danh mục trước và sau khi tái cân bằng')
plt.xticks(x, tickers)
plt.legend()
plt.grid(True, axis='y')
plt.tight_layout()
plt.show()
return new_portfolio
# Tái cân bằng danh mục sau 1 năm
rebalanced_portfolio = rebalance_portfolio(stocks_data, optimal_portfolio, '2019-01-01')
4.3 Đánh Giá Hiệu Suất Trước và Sau Khi Tái Cân Bằng
def compare_portfolio_performance(stocks_data, original_portfolio, rebalanced_portfolio, start_date, rebalance_date, end_date=None):
"""
So sánh hiệu suất danh mục trước và sau khi tái cân bằng
Tham số:
---------
stocks_data : dict
Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
original_portfolio : dict
Trọng số ban đầu của các cổ phiếu trong danh mục
rebalanced_portfolio : dict
Trọng số sau khi tái cân bằng
start_date : str
Ngày bắt đầu (định dạng: 'YYYY-MM-DD')
rebalance_date : str
Ngày tái cân bằng (định dạng: 'YYYY-MM-DD')
end_date : str, tùy chọn
Ngày kết thúc (định dạng: 'YYYY-MM-DD'), mặc định là ngày hiện tại
Trả về:
--------
tuple
(original_performance, rebalanced_performance)
"""
if end_date is None:
end_date = datetime.now().strftime('%Y-%m-%d')
# Đánh giá hiệu suất danh mục ban đầu (không tái cân bằng)
original_performance = evaluate_portfolio_performance(stocks_data, original_portfolio, start_date)
# Mô phỏng danh mục tái cân bằng
# 1. Hiệu suất đến ngày tái cân bằng
performance_to_rebalance = evaluate_portfolio_performance(stocks_data, original_portfolio, start_date)
performance_to_rebalance = performance_to_rebalance[performance_to_rebalance.index <= pd.to_datetime(rebalance_date)]
# 2. Hiệu suất sau khi tái cân bằng
performance_after_rebalance = evaluate_portfolio_performance(stocks_data, rebalanced_portfolio, rebalance_date)
# 3. Kết hợp hai khoảng thời gian
# Giả định: Giá trị cuối cùng trước khi tái cân bằng = giá trị ban đầu sau khi tái cân bằng
scaling_factor = performance_to_rebalance['Total_Value'].iloc[-1] / performance_after_rebalance['Total_Value'].iloc[0]
rebalanced_performance = pd.concat([
performance_to_rebalance,
performance_after_rebalance[['Portfolio_Value', 'Dividend_Cashflow', 'Cumulative_Dividends', 'Total_Value']] * scaling_factor
])
# Tính lại lợi nhuận
rebalanced_performance['Return'] = rebalanced_performance['Total_Value'] / performance_to_rebalance['Total_Value'].iloc[0] - 1
# Vẽ biểu đồ so sánh
plt.figure(figsize=(14, 7))
plt.plot(original_performance.index, original_performance['Return'] * 100, label='Danh mục không tái cân bằng')
plt.plot(rebalanced_performance.index, rebalanced_performance['Return'] * 100, label='Danh mục có tái cân bằng')
# Đánh dấu thời điểm tái cân bằng
plt.axvline(x=pd.to_datetime(rebalance_date), color='r', linestyle='--', label='Thời điểm tái cân bằng')
plt.title('So sánh hiệu suất danh mục trước và sau khi tái cân bằng')
plt.xlabel('Ngày')
plt.ylabel('Lợi nhuận (%)')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
# So sánh chỉ số hiệu suất
original_final_return = original_performance['Return'].iloc[-1]
rebalanced_final_return = rebalanced_performance['Return'].iloc[-1]
original_years = (original_performance.index[-1] - original_performance.index[0]).days / 365
rebalanced_years = (rebalanced_performance.index[-1] - rebalanced_performance.index[0]).days / 365
original_annualized = (original_final_return + 1) ** (1 / original_years) - 1 if original_years > 0 else 0
rebalanced_annualized = (rebalanced_final_return + 1) ** (1 / rebalanced_years) - 1 if rebalanced_years > 0 else 0
print("So sánh hiệu suất:")
print(f"Tổng lợi nhuận (không tái cân bằng): {original_final_return*100:.2f}%")
print(f"Tổng lợi nhuận (có tái cân bằng): {rebalanced_final_return*100:.2f}%")
print(f"Lợi nhuận hàng năm (không tái cân bằng): {original_annualized*100:.2f}%")
print(f"Lợi nhuận hàng năm (có tái cân bằng): {rebalanced_annualized*100:.2f}%")
return original_performance, rebalanced_performance
# So sánh hiệu suất trước và sau khi tái cân bằng
original_perf, rebalanced_perf = compare_portfolio_performance(
stocks_data, optimal_portfolio, rebalanced_portfolio, '2018-01-01', '2019-01-01', '2020-01-01'
)
Phần 5: Tự Động Hóa Quy Trình
5.1 Xây Dựng Lịch Trình Tự Động Cập Nhật Dữ Liệu
def create_data_update_schedule(tickers, update_interval=7, data_file='dividend_stocks_data.pkl'):
"""
Tạo lịch trình tự động cập nhật dữ liệu
Tham số:
---------
tickers : list
Danh sách mã cổ phiếu cần theo dõi
update_interval : int
Số ngày giữa các lần cập nhật
data_file : str
Tên file để lưu dữ liệu
Trả về:
--------
dict
Lịch trình cập nhật
"""
# Tạo lịch trình
schedule = {
'tickers': tickers,
'update_interval': update_interval,
'last_update': datetime.now().strftime('%Y-%m-%d'),
'next_update': (datetime.now() + timedelta(days=update_interval)).strftime('%Y-%m-%d'),
'data_file': data_file
}
# Lưu lịch trình vào file
import pickle
with open('dividend_portfolio_schedule.pkl', 'wb') as f:
pickle.dump(schedule, f)
print(f"Đã tạo lịch trình cập nhật dữ liệu:")
print(f"- Danh sách cổ phiếu: {tickers}")
print(f"- Tần suất cập nhật: {update_interval} ngày")
print(f"- Lần cập nhật tiếp theo: {schedule['next_update']}")
return schedule
def update_stock_data(schedule):
"""
Cập nhật dữ liệu cổ phiếu theo lịch trình
Tham số:
---------
schedule : dict
Lịch trình cập nhật
Trả về:
--------
dict
Dữ liệu cổ phiếu đã cập nhật
"""
import pickle
from datetime import datetime
today = datetime.now().strftime('%Y-%m-%d')
next_update = schedule['next_update']
# Kiểm tra xem có cần cập nhật không
if pd.to_datetime(today) < pd.to_datetime(next_update):
print(f"Chưa đến thời điểm cập nhật tiếp theo ({next_update})")
# Thử tải dữ liệu từ file nếu có
try:
with open(schedule['data_file'], 'rb') as f:
stocks_data = pickle.load(f)
print(f"Đã tải dữ liệu từ file {schedule['data_file']}")
return stocks_data
except:
print(f"Không tìm thấy file dữ liệu {schedule['data_file']}, sẽ tải dữ liệu mới")
else:
print(f"Đến thời điểm cập nhật dữ liệu ({today} >= {next_update})")
# Tải dữ liệu mới
start_date = '2018-01-01' # Có thể điều chỉnh tùy theo nhu cầu
stocks_data = get_multiple_stocks_data(schedule['tickers'], start_date)
# Lưu dữ liệu vào file
with open(schedule['data_file'], 'wb') as f:
pickle.dump(stocks_data, f)
# Cập nhật lịch trình
schedule['last_update'] = today
schedule['next_update'] = (datetime.now() + timedelta(days=schedule['update_interval'])).strftime('%Y-%m-%d')
# Lưu lịch trình đã cập nhật
with open('dividend_portfolio_schedule.pkl', 'wb') as f:
pickle.dump(schedule, f)
print(f"Đã cập nhật dữ liệu thành công")
print(f"Lần cập nhật tiếp theo: {schedule['next_update']}")
return stocks_data
# Tạo lịch trình tự động cập nhật
all_tickers = vn_dividend_tickers
schedule = create_data_update_schedule(all_tickers, update_interval=30)
# Cập nhật dữ liệu
updated_stocks_data = update_stock_data(schedule)
5.2 Tự Động Hóa Báo Cáo Hiệu Suất và Tái Cân Bằng
def generate_portfolio_report(stocks_data, portfolio, start_date, title="Báo Cáo Danh Mục Đầu Tư Cổ Tức"):
"""
Tạo báo cáo hiệu suất danh mục đầu tư
Tham số:
---------
stocks_data : dict
Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
portfolio : dict
Trọng số của các cổ phiếu trong danh mục
start_date : str
Ngày bắt đầu đánh giá (định dạng: 'YYYY-MM-DD')
title : str
Tiêu đề báo cáo
Trả về:
--------
str
Đường dẫn đến file báo cáo HTML
"""
from datetime import datetime
import os
# Tạo thời gian báo cáo
report_time = datetime.now().strftime('%Y-%m-%d %H:%M:%S')
# Tính toán hiệu suất
performance = evaluate_portfolio_performance(stocks_data, portfolio, start_date)
# Tạo bảng phân bổ danh mục
allocation_data = []
for ticker, weight in portfolio.items():
if ticker in stocks_data:
current_price = stocks_data[ticker]['price']['Close'].iloc[-1]
# Tính tỷ suất cổ tức
price_data = stocks_data[ticker]['price']
dividend_data = stocks_data[ticker]['dividends']
metrics = calculate_dividend_metrics(ticker, price_data, dividend_data)
allocation_data.append({
'Cổ phiếu': ticker,
'Trọng số': f"{weight*100:.2f}%",
'Giá hiện tại': f"{current_price:,.0f}",
'Tỷ suất cổ tức': f"{metrics['current_dividend_yield']:.2f}%",
'Tăng trưởng cổ tức': f"{metrics['dividend_growth_rate']:.2f}%"
})
allocation_df = pd.DataFrame(allocation_data)
# Tính các chỉ số hiệu suất
years = (performance.index[-1] - performance.index[0]).days / 365
annualized_return = (performance['Return'].iloc[-1] + 1) ** (1 / years) - 1 if years > 0 else 0
total_dividends = performance['Cumulative_Dividends'].iloc[-1]
initial_investment = performance['Portfolio_Value'].iloc[0]
current_value = performance['Total_Value'].iloc[-1]
dividend_yield = total_dividends / initial_investment / years * 100 if years > 0 else 0
# Tạo nội dung HTML
html_content = f"""
<!DOCTYPE html>
<html>
<head>
<title>{title}</title>
<style>
body {{ font-family: Arial, sans-serif; margin: 20px; }}
.header {{ background-color: #4CAF50; color: white; padding: 10px; text-align: center; }}
.section {{ margin-top: 20px; }}
table {{ border-collapse: collapse; width: 100%; }}
th, td {{ border: 1px solid #ddd; padding: 8px; text-align: left; }}
th {{ background-color: #f2f2f2; }}
tr:nth-child(even) {{ background-color: #f9f9f9; }}
.performance {{ display: flex; flex-wrap: wrap; }}
.metric {{ flex: 1; min-width: 200px; border: 1px solid #ddd; padding: 10px; margin: 5px; text-align: center; }}
.metric h3 {{ margin-top: 0; color: #333; }}
.metric p {{ font-size: 24px; font-weight: bold; color: #4CAF50; }}
</style>
</head>
<body>
<div class="header">
<h1>{title}</h1>
<p>Ngày báo cáo: {report_time}</p>
</div>
<div class="section">
<h2>Phân bổ danh mục</h2>
<table>
<tr>
{''.join([f"<th>{col}</th>" for col in allocation_df.columns])}
</tr>
{''.join([f"<tr>{''.join([f'<td>{value}</td>' for value in row.values])}</tr>" for _, row in allocation_df.iterrows()])}
</table>
</div>
<div class="section">
<h2>Hiệu suất danh mục</h2>
<div class="performance">
<div class="metric">
<h3>Tổng lợi nhuận</h3>
<p>{performance['Return'].iloc[-1]*100:.2f}%</p>
</div>
<div class="metric">
<h3>Lợi nhuận hàng năm</h3>
<p>{annualized_return*100:.2f}%</p>
</div>
<div class="metric">
<h3>Tổng cổ tức</h3>
<p>{total_dividends:,.0f} đồng</p>
</div>
<div class="metric">
<h3>Tỷ suất cổ tức thực tế</h3>
<p>{dividend_yield:.2f}%/năm</p>
</div>
</div>
</div>
<div class="section">
<h2>Giá trị danh mục</h2>
<div class="performance">
<div class="metric">
<h3>Vốn ban đầu</h3>
<p>{initial_investment:,.0f} đồng</p>
</div>
<div class="metric">
<h3>Giá trị hiện tại</h3>
<p>{current_value:,.0f} đồng</p>
</div>
<div class="metric">
<h3>Tăng trưởng giá trị</h3>
<p>{(current_value - initial_investment):,.0f} đồng</p>
</div>
<div class="metric">
<h3>% Cổ tức / Tổng lợi nhuận</h3>
<p>{(total_dividends / (current_value - initial_investment) * 100) if current_value > initial_investment else 0:.2f}%</p>
</div>
</div>
</div>
<div class="section">
<h2>Khuyến nghị</h2>
<p>Dựa trên phân tích hiện tại, danh mục đầu tư cổ tức của bạn:</p>
<ul>
<li>{'<span style="color: green;">Đang hoạt động tốt với tỷ suất cổ tức cao hơn thị trường</span>' if dividend_yield > 3 else '<span style="color: orange;">Có tỷ suất cổ tức thấp hơn mức mục tiêu 3%</span>'}</li>
<li>{'<span style="color: green;">Đang tạo ra lợi nhuận tích cực</span>' if performance['Return'].iloc[-1] > 0 else '<span style="color: red;">Đang gặp khó khăn với lợi nhuận âm</span>'}</li>
<li>{'<span style="color: green;">Đóng góp cổ tức chiếm tỷ trọng lớn trong tổng lợi nhuận</span>' if total_dividends > 0 and current_value > initial_investment and total_dividends / (current_value - initial_investment) > 0.3 else '<span style="color: orange;">Đóng góp từ cổ tức còn hạn chế trong tổng lợi nhuận</span>'}</li>
</ul>
<p><strong>Hành động khuyến nghị:</strong></p>
<ul>
<li>{'<span style="color: green;">Duy trì danh mục hiện tại</span>' if performance['Return'].iloc[-1] > 0 and dividend_yield > 3 else '<span style="color: orange;">Xem xét tái cân bằng danh mục để tối ưu hóa tỷ suất cổ tức</span>' if dividend_yield < 3 else '<span style="color: orange;">Xem xét bổ sung các cổ phiếu có tiềm năng tăng trưởng cổ tức tốt hơn</span>'}</li>
<li>Theo dõi chặt chẽ lịch trả cổ tức sắp tới và các thông báo về thay đổi chính sách cổ tức</li>
<li>Xem xét tái đầu tư cổ tức nhận được để tận dụng sức mạnh của lãi kép</li>
</ul>
</div>
</body>
</html>
"""
# Lưu nội dung HTML vào file
report_file = f"dividend_portfolio_report_{datetime.now().strftime('%Y%m%d')}.html"
with open(report_file, 'w', encoding='utf-8') as f:
f.write(html_content)
print(f"Đã tạo báo cáo: {report_file}")
return report_file
def schedule_portfolio_maintenance(portfolio, rebalance_interval=180, report_interval=90):
"""
Lên lịch bảo trì danh mục đầu tư
Tham số:
---------
portfolio : dict
Trọng số của các cổ phiếu trong danh mục
rebalance_interval : int
Số ngày giữa các lần tái cân bằng
report_interval : int
Số ngày giữa các báo cáo
Trả về:
--------
dict
Lịch trình bảo trì
"""
today = datetime.now()
maintenance_schedule = {
'portfolio': portfolio,
'last_rebalance': today.strftime('%Y-%m-%d'),
'next_rebalance': (today + timedelta(days=rebalance_interval)).strftime('%Y-%m-%d'),
'last_report': today.strftime('%Y-%m-%d'),
'next_report': (today + timedelta(days=report_interval)).strftime('%Y-%m-%d'),
'rebalance_interval': rebalance_interval,
'report_interval': report_interval
}
# Lưu lịch trình
import pickle
with open('portfolio_maintenance_schedule.pkl', 'wb') as f:
pickle.dump(maintenance_schedule, f)
print(f"Đã lên lịch bảo trì danh mục đầu tư:")
print(f"- Tái cân bằng kế tiếp: {maintenance_schedule['next_rebalance']} (mỗi {rebalance_interval} ngày)")
print(f"- Báo cáo kế tiếp: {maintenance_schedule['next_report']} (mỗi {report_interval} ngày)")
return maintenance_schedule
def check_and_perform_maintenance(schedule, stocks_data, start_date):
"""
Kiểm tra và thực hiện bảo trì danh mục theo lịch trình
Tham số:
---------
schedule : dict
Lịch trình bảo trì
stocks_data : dict
Dữ liệu cổ phiếu
start_date : str
Ngày bắt đầu đánh giá
Trả về:
--------
dict
Lịch trình đã cập nhật
"""
import pickle
from datetime import datetime
today = datetime.now().strftime('%Y-%m-%d')
portfolio = schedule['portfolio']
# Kiểm tra báo cáo
if pd.to_datetime(today) >= pd.to_datetime(schedule['next_report']):
print(f"Đến thời điểm tạo báo cáo ({today} >= {schedule['next_report']})")
# Tạo báo cáo
report_file = generate_portfolio_report(stocks_data, portfolio, start_date)
# Cập nhật lịch trình báo cáo
schedule['last_report'] = today
schedule['next_report'] = (datetime.now() + timedelta(days=schedule['report_interval'])).strftime('%Y-%m-%d')
print(f"Báo cáo kế tiếp: {schedule['next_report']}")
# Kiểm tra tái cân bằng
if pd.to_datetime(today) >= pd.to_datetime(schedule['next_rebalance']):
print(f"Đến thời điểm tái cân bằng danh mục ({today} >= {schedule['next_rebalance']})")
# Tái cân bằng danh mục
new_portfolio = rebalance_portfolio(stocks_data, portfolio, today)
# Cập nhật danh mục
schedule['portfolio'] = new_portfolio
# Cập nhật lịch trình tái cân bằng
schedule['last_rebalance'] = today
schedule['next_rebalance'] = (datetime.now() + timedelta(days=schedule['rebalance_interval'])).strftime('%Y-%m-%d')
print(f"Tái cân bằng kế tiếp: {schedule['next_rebalance']}")
# Lưu lịch trình đã cập nhật
with open('portfolio_maintenance_schedule.pkl', 'wb') as f:
pickle.dump(schedule, f)
return schedule
# Tạo lịch trình bảo trì
maintenance_schedule = schedule_portfolio_maintenance(optimal_portfolio, rebalance_interval=180, report_interval=90)
# Kiểm tra và thực hiện bảo trì
updated_schedule = check_and_perform_maintenance(maintenance_schedule, updated_stocks_data, '2018-01-01')
5.3 Tạo Script Chạy Định Kỳ
def create_automation_script():
"""
Tạo script tự động hóa toàn bộ quy trình
"""
script_content = """
import os
import pickle
import pandas as pd
from datetime import datetime, timedelta
# Import các hàm đã định nghĩa
# (Giả sử đã lưu tất cả các hàm vào file dividend_portfolio_functions.py)
from dividend_portfolio_functions import (
get_multiple_stocks_data, update_stock_data, check_and_perform_maintenance
)
def run_automation():
# Kiểm tra và tải lịch trình
try:
with open('dividend_portfolio_schedule.pkl', 'rb') as f:
data_schedule = pickle.load(f)
with open('portfolio_maintenance_schedule.pkl', 'rb') as f:
maintenance_schedule = pickle.load(f)
except FileNotFoundError:
print("Không tìm thấy file lịch trình. Hãy thiết lập lịch trình trước.")
return
# Cập nhật dữ liệu
stocks_data = update_stock_data(data_schedule)
# Thực hiện bảo trì danh mục
start_date = '2018-01-01' # Hoặc bất kỳ ngày nào phù hợp
updated_schedule = check_and_perform_maintenance(maintenance_schedule, stocks_data, start_date)
print("Tự động hóa hoàn tất!")
if __name__ == "__main__":
print(f"Bắt đầu tự động hóa quy trình lúc {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
run_automation()
"""
# Lưu script
with open('run_dividend_portfolio_automation.py', 'w') as f:
f.write(script_content)
# Tạo file chứa các hàm
functions_content = """
# Export tất cả các hàm đã định nghĩa
# (Sao chép tất cả các định nghĩa hàm vào đây)
"""
with open('dividend_portfolio_functions.py', 'w') as f:
f.write(functions_content)
print("Đã tạo các file tự động hóa:")
print("1. run_dividend_portfolio_automation.py - Script chính để chạy tự động")
print("2. dividend_portfolio_functions.py - File chứa các hàm (cần bổ sung các định nghĩa hàm)")
# Hướng dẫn thiết lập cron job
print("\nĐể chạy script định kỳ, bạn có thể thiết lập cron job (Linux/Mac) hoặc Task Scheduler (Windows).")
print("Ví dụ crontab để chạy mỗi ngày lúc 8 giờ sáng:")
print("0 8 * * * /usr/bin/python3 /đường/dẫn/đến/run_dividend_portfolio_automation.py >> /đường/dẫn/đến/dividend_automation.log 2>&1")
# Tạo script tự động hóa
create_automation_script()
Phần 6: Phân Tích Nâng Cao
6.1 Phân Tích Độ Nhạy của Tham Số Tối Ưu
def sensitivity_analysis(stocks_data, selected_tickers, parameter_ranges):
"""
Phân tích độ nhạy của các tham số tối ưu
Tham số:
---------
stocks_data : dict
Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
selected_tickers : list
Danh sách mã cổ phiếu được chọn
parameter_ranges : dict
Phạm vi các tham số cần phân tích (ví dụ: {'risk_tolerance': [0.1, 0.3, 0.5, 0.7, 0.9]})
Trả về:
--------
DataFrame
Kết quả phân tích
"""
import itertools
# Khởi tạo list để lưu kết quả
results = []
# Tạo tất cả tổ hợp tham số
param_names = list(parameter_ranges.keys())
param_values = list(parameter_ranges.values())
param_combinations = list(itertools.product(*param_values))
for combo in param_combinations:
# Tạo từ điển tham số
params = {name: value for name, value in zip(param_names, combo)}
# Thực hiện tối ưu hóa với tham số hiện tại
# Ví dụ với risk_tolerance
if 'risk_tolerance' in params:
portfolio = optimize_dividend_portfolio(
stocks_data, selected_tickers, risk_tolerance=params['risk_tolerance']
)
# Đánh giá hiệu suất
performance = evaluate_portfolio_performance(stocks_data, portfolio, '2018-01-01')
# Tính các chỉ số hiệu suất
total_return = performance['Return'].iloc[-1] * 100
years = (performance.index[-1] - performance.index[0]).days / 365
annualized_return = ((total_return / 100 + 1) ** (1 / years) - 1) * 100 if years > 0 else 0
# Tính tỷ suất cổ tức danh mục
weighted_yield = 0
for ticker, weight in portfolio.items():
if ticker in stocks_data:
price_data = stocks_data[ticker]['price']
dividend_data = stocks_data[ticker]['dividends']
metrics = calculate_dividend_metrics(ticker, price_data, dividend_data)
weighted_yield += metrics['current_dividend_yield'] * weight
# Lưu kết quả
result = {
'risk_tolerance': params['risk_tolerance'],
'dividend_yield': weighted_yield,
'total_return': total_return,
'annualized_return': annualized_return
}
# Thêm trọng số của từng cổ phiếu
for ticker in selected_tickers:
result[f'{ticker}_weight'] = portfolio.get(ticker, 0) * 100
results.append(result)
# Tạo DataFrame từ kết quả
sensitivity_df = pd.DataFrame(results)
# Vẽ biểu đồ kết quả
plt.figure(figsize=(15, 10))
# 1. Biểu đồ quan hệ giữa risk_tolerance và tỷ suất cổ tức
plt.subplot(2, 2, 1)
plt.plot(sensitivity_df['risk_tolerance'], sensitivity_df['dividend_yield'], marker='o')
plt.title('Risk Tolerance vs Dividend Yield')
plt.xlabel('Risk Tolerance')
plt.ylabel('Dividend Yield (%)')
plt.grid(True)
# 2. Biểu đồ quan hệ giữa risk_tolerance và lợi nhuận hàng năm
plt.subplot(2, 2, 2)
plt.plot(sensitivity_df['risk_tolerance'], sensitivity_df['annualized_return'], marker='o')
plt.title('Risk Tolerance vs Annualized Return')
plt.xlabel('Risk Tolerance')
plt.ylabel('Annualized Return (%)')
plt.grid(True)
# 3. Biểu đồ stacked cho phân bổ danh mục
plt.subplot(2, 2, 3)
weight_columns = [col for col in sensitivity_df.columns if '_weight' in col]
sensitivity_df[weight_columns].plot(kind='bar', stacked=True, ax=plt.gca())
plt.title('Portfolio Allocation by Risk Tolerance')
plt.xlabel('Sample Index')
plt.ylabel('Allocation (%)')
plt.legend(loc='upper center', bbox_to_anchor=(0.5, -0.15), ncol=3)
# 4. Biểu đồ lợi nhuận vs tỷ suất cổ tức
plt.subplot(2, 2, 4)
plt.scatter(sensitivity_df['dividend_yield'], sensitivity_df['annualized_return'])
# Thêm chú thích cho từng điểm
for i, row in sensitivity_df.iterrows():
plt.annotate(f"RT={row['risk_tolerance']}",
(row['dividend_yield'], row['annualized_return']),
textcoords="offset points",
xytext=(0,10),
ha='center')
plt.title('Dividend Yield vs Annualized Return')
plt.xlabel('Dividend Yield (%)')
plt.ylabel('Annualized Return (%)')
plt.grid(True)
plt.tight_layout()
plt.show()
return sensitivity_df
# Phân tích độ nhạy
parameter_ranges = {
'risk_tolerance': [0.1, 0.3, 0.5, 0.7, 0.9]
}
sensitivity_results = sensitivity_analysis(stocks_data, selected_tickers, parameter_ranges)
6.2 So Sánh Với Chiến Lược Đầu Tư Khác
def compare_with_other_strategies(stocks_data, dividend_portfolio, start_date, end_date=None):
"""
So sánh chiến lược đầu tư cổ tức với các chiến lược khác
Tham số:
---------
stocks_data : dict
Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
dividend_portfolio : dict
Trọng số của các cổ phiếu trong danh mục cổ tức
start_date : str
Ngày bắt đầu (định dạng: 'YYYY-MM-DD')
end_date : str, tùy chọn
Ngày kết thúc (định dạng: 'YYYY-MM-DD'), mặc định là ngày hiện tại
Trả về:
--------
DataFrame
Kết quả so sánh các chiến lược
"""
if end_date is None:
end_date = datetime.now().strftime('%Y-%m-%d')
# Đánh giá hiệu suất danh mục cổ tức
dividend_performance = evaluate_portfolio_performance(stocks_data, dividend_portfolio, start_date)
# Tạo danh mục theo chiến lược tăng trưởng (chọn các cổ phiếu có tăng trưởng giá cao nhất)
growth_tickers = []
growth_rates = {}
for ticker in stocks_data:
price_data = stocks_data[ticker]['price']
if not price_data.empty:
start_price = price_data[price_data.index >= pd.to_datetime(start_date)]['Close'].iloc[0]
end_price = price_data[price_data.index <= pd.to_datetime(end_date)]['Close'].iloc[-1]
growth_rate = (end_price / start_price - 1) * 100
growth_rates[ticker] = growth_rate
growth_tickers.append(ticker)
# Sắp xếp theo tốc độ tăng trưởng giảm dần
growth_tickers = sorted(growth_tickers, key=lambda x: growth_rates.get(x, 0), reverse=True)
# Chọn top 5 cổ phiếu tăng trưởng mạnh nhất
top_growth_tickers = growth_tickers[:5]
# Tạo danh mục tăng trưởng (trọng số bằng nhau)
growth_portfolio = {ticker: 1/len(top_growth_tickers) for ticker in top_growth_tickers}
# Đánh giá hiệu suất danh mục tăng trưởng
growth_performance = evaluate_portfolio_performance(stocks_data, growth_portfolio, start_date)
# Tạo danh mục theo chiến lược cân bằng (kết hợp giữa tăng trưởng và cổ tức)
# Chọn cả cổ phiếu trả cổ tức và cổ phiếu tăng trưởng
balanced_tickers = list(set(list(dividend_portfolio.keys()) + top_growth_tickers))
balanced_portfolio = {ticker: 1/len(balanced_tickers) for ticker in balanced_tickers}
# Đánh giá hiệu suất danh mục cân bằng
balanced_performance = evaluate_portfolio_performance(stocks_data, balanced_portfolio, start_date)
# Vẽ biểu đồ so sánh
plt.figure(figsize=(12, 6))
plt.plot(dividend_performance.index, dividend_performance['Return'] * 100, label='Chiến lược cổ tức')
plt.plot(growth_performance.index, growth_performance['Return'] * 100, label='Chiến lược tăng trưởng')
plt.plot(balanced_performance.index, balanced_performance['Return'] * 100, label='Chiến lược cân bằng')
plt.title('So sánh hiệu suất các chiến lược đầu tư')
plt.xlabel('Ngày')
plt.ylabel('Lợi nhuận (%)')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
# Tính các chỉ số hiệu suất
years = (dividend_performance.index[-1] - dividend_performance.index[0]).days / 365
strategies = ['Cổ tức', 'Tăng trưởng', 'Cân bằng']
performances = [dividend_performance, growth_performance, balanced_performance]
comparison_data = []
for strategy, perf in zip(strategies, performances):
total_return = perf['Return'].iloc[-1] * 100
annual_return = ((total_return / 100 + 1) ** (1 / years) - 1) * 100 if years > 0 else 0
# Tính độ biến động (volatility)
daily_returns = perf['Total_Value'].pct_change().dropna()
volatility = daily_returns.std() * np.sqrt(252) * 100 # Annualized
# Tính Sharpe Ratio (giả định lãi suất không rủi ro = 0)
sharpe_ratio = annual_return / volatility if volatility > 0 else 0
# Tính Maximum Drawdown
cumulative = perf['Total_Value']
running_max = cumulative.cummax()
drawdown = (cumulative - running_max) / running_max
max_drawdown = drawdown.min() * 100
comparison_data.append({
'Strategy': strategy,
'Total Return (%)': total_return,
'Annual Return (%)': annual_return,
'Volatility (%)': volatility,
'Sharpe Ratio': sharpe_ratio,
'Max Drawdown (%)': max_drawdown
})
comparison_df = pd.DataFrame(comparison_data)
return comparison_df
# So sánh với các chiến lược khác
comparison_results = compare_with_other_strategies(stocks_data, optimal_portfolio, '2018-01-01')
print(comparison_results)
6.3 Phân Tích Tác Động của Giai Đoạn Thị Trường
def analyze_market_phase_impact(stocks_data, portfolio, market_ticker='^VNINDEX'):
"""
Phân tích tác động của các giai đoạn thị trường khác nhau
Tham số:
---------
stocks_data : dict
Từ điển chứa dữ liệu giá và cổ tức cho mỗi cổ phiếu
portfolio : dict
Trọng số của các cổ phiếu trong danh mục
market_ticker : str
Mã chỉ số thị trường để phân tích giai đoạn
Trả về:
--------
DataFrame
Kết quả phân tích theo giai đoạn thị trường
"""
# Tải dữ liệu chỉ số thị trường
try:
market_data = yf.download(market_ticker, start='2018-01-01')
# Tính SMA 200 ngày để xác định xu hướng
market_data['SMA_200'] = market_data['Close'].rolling(window=200).mean()
# Tính biến động 20 ngày
market_data['Volatility'] = market_data['Close'].pct_change().rolling(window=20).std() * np.sqrt(252)
# Xác định các giai đoạn thị trường
market_data['Market_Phase'] = 'Sideway' # Mặc định
# Thị trường tăng: giá trên SMA200 và tăng hơn 20% trong 1 năm
market_data['Annual_Return'] = market_data['Close'].pct_change(periods=252)
bull_market = (market_data['Close'] > market_data['SMA_200']) & (market_data['Annual_Return'] > 0.2)
market_data.loc[bull_market, 'Market_Phase'] = 'Bull'
# Thị trường giảm: giá dưới SMA200 và giảm hơn 20% trong 1 năm
bear_market = (market_data['Close'] < market_data['SMA_200']) & (market_data['Annual_Return'] < -0.2)
market_data.loc[bear_market, 'Market_Phase'] = 'Bear'
# Thị trường biến động cao: biến động > 25%
high_volatility = market_data['Volatility'] > 0.25
market_data.loc[high_volatility, 'Market_Phase'] = 'High Volatility'
# Đánh giá hiệu suất danh mục
performance = evaluate_portfolio_performance(stocks_data, portfolio, '2018-01-01')
# Kết hợp dữ liệu hiệu suất với giai đoạn thị trường
combined_data = pd.DataFrame()
combined_data['Portfolio_Return'] = performance['Return']
combined_data['Market_Phase'] = market_data['Market_Phase']
# Phân tích hiệu suất theo từng giai đoạn
phase_analysis = []
for phase in ['Bull', 'Bear', 'Sideway', 'High Volatility']:
phase_data = combined_data[combined_data['Market_Phase'] == phase]
if not phase_data.empty:
# Tính lợi nhuận trung bình hàng ngày trong giai đoạn
phase_daily_return = phase_data['Portfolio_Return'].pct_change().mean() * 100
# Tính tổng số ngày trong giai đoạn
phase_days = len(phase_data)
# Tính phần trăm thời gian trong giai đoạn
total_days = len(combined_data)
phase_percentage = phase_days / total_days * 100 if total_days > 0 else 0
phase_analysis.append({
'Market_Phase': phase,
'Days': phase_days,
'Percentage_of_Time': phase_percentage,
'Avg_Daily_Return': phase_daily_return
})
phase_df = pd.DataFrame(phase_analysis)
# Vẽ biểu đồ phân tích
plt.figure(figsize=(15, 10))
# 1. Hiệu suất theo giai đoạn thị trường
plt.subplot(2, 2, 1)
sns.barplot(x='Market_Phase', y='Avg_Daily_Return', data=phase_df)
plt.title('Lợi nhuận trung bình hàng ngày theo giai đoạn thị trường')
plt.xlabel('Giai đoạn thị trường')
plt.ylabel('Lợi nhuận hàng ngày (%)')
# 2. Phân bổ thời gian theo giai đoạn
plt.subplot(2, 2, 2)
plt.pie(phase_df['Percentage_of_Time'], labels=phase_df['Market_Phase'], autopct='%1.1f%%')
plt.title('Phân bổ thời gian theo giai đoạn thị trường')
# 3. Hiệu suất theo thời gian với màu sắc theo giai đoạn
plt.subplot(2, 1, 2)
# Vẽ hiệu suất danh mục
plt.plot(performance.index, performance['Return'] * 100, label='Hiệu suất danh mục')
# Tô màu nền theo giai đoạn thị trường
unique_phases = combined_data['Market_Phase'].unique()
colors = {'Bull': 'lightgreen', 'Bear': 'lightcoral', 'Sideway': 'lightgray', 'High Volatility': 'lightyellow'}
for phase in unique_phases:
phase_periods = combined_data[combined_data['Market_Phase'] == phase]
if not phase_periods.empty:
for i in range(len(phase_periods) - 1):
start_date = phase_periods.index[i]
end_date = phase_periods.index[i+1]
plt.axvspan(start_date, end_date, alpha=0.3, color=colors.get(phase, 'white'))
plt.title('Hiệu suất danh mục theo giai đoạn thị trường')
plt.xlabel('Ngày')
plt.ylabel('Lợi nhuận (%)')
plt.legend()
plt.grid(True)
plt.tight_layout()
plt.show()
return phase_df
except Exception as e:
print(f"Lỗi khi phân tích tác động của giai đoạn thị trường: {e}")
return pd.DataFrame()
# Phân tích tác động của giai đoạn thị trường
market_phase_analysis = analyze_market_phase_impact(stocks_data, optimal_portfolio)
print(market_phase_analysis)
Kết Luận
Bài viết đã trình bày cách triển khai một chiến lược đầu tư cổ tức toàn diện bằng Python, từ việc phân tích và lựa chọn cổ phiếu trả cổ tức, xây dựng danh mục đầu tư tối ưu, theo dõi và tái cân bằng danh mục, đến việc tự động hóa toàn bộ quy trình. Các kỹ thuật phân tích nâng cao cũng được giới thiệu để giúp nhà đầu tư hiểu rõ hơn về các yếu tố ảnh hưởng đến hiệu suất của chiến lược đầu tư cổ tức.
Đầu tư cổ tức là một chiến lược phù hợp với mục tiêu tài chính dài hạn, đặc biệt là việc xây dựng dòng thu nhập thụ động. Với sự hỗ trợ của các công cụ phân tích định lượng và tự động hóa từ Python, nhà đầu tư có thể xây dựng và quản lý danh mục đầu tư cổ tức một cách hiệu quả và tiết kiệm thời gian.
Tuy nhiên, cần lưu ý rằng không có chiến lược đầu tư nào là hoàn hảo cho mọi thị trường và mọi giai đoạn. Việc kết hợp giữa phân tích kỹ thuật, phân tích cơ bản, và liên tục điều chỉnh chiến lược theo điều kiện thị trường là chìa khóa để đạt được thành công trong đầu tư cổ tức dài hạn.