定投策略代码
1 linux环境部署
2 含发邮件代码
3 命令行调用
python /dingtou1.py 159941
代码如下
import argparse
import io
from datetime import datetime
import akshare as ak
import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import yagmail
from openpyxl import Workbook
from openpyxl.drawing.image import Image as ExcelImage
# # 设置 Matplotlib 的字体为支持中文的字体
# plt.rcParams['font.sans-serif'] = ['SimHei'] # 设置字体为 SimHei(黑体)
# plt.rcParams['axes.unicode_minus'] = False # 解决负号显示问题
import matplotlib.pyplot as plt
import matplotlib.font_manager as fm
#指定字体文件路径
font_path = '/usr/share/fonts/google-noto-cjk/NotoSansCJK-DemiLight.ttc'
# 加载字体属性
font_prop = fm.FontProperties(fname=font_path)
# 设置全局字体
plt.rcParams['font.family'] = font_prop.get_name()
# 解决负号显示问题
plt.rcParams['axes.unicode_minus'] = False
class DCACalculator:
def __init__(self, code, amount, frequency):
self.code = code
self.amount = amount
self.frequency = frequency
self.data = None
self.investment_df = None
self.annual_return_df = None
self.asset_name = self.get_asset_name(code) # 获取资产名称
self.result_text_dca = None
self.result_text_lump_sum = None
def get_asset_name(self, code):
"""
从 akshare 获取 ETF 的名称
"""
try:
etf_info = ak.fund_etf_spot_em()
etf_name = etf_info[etf_info['代码'] == code]['名称'].iloc[0]
return etf_name
except Exception as e:
print(f"获取资产名称时出错: {e}")
return None
def fetch_data(self):
"""获取基金数据"""
try:
self.data = ak.fund_etf_hist_em(symbol=self.code, adjust='qfq')
self.data.rename(columns={'收盘': '价格'}, inplace=True)
self.data['date'] = pd.to_datetime(self.data['日期'])
self.data.set_index('date', inplace=True)
if self.data.empty:
raise ValueError("获取的数据为空,请检查代码是否正确或网络连接是否正常。")
except Exception as e:
raise ValueError(f"获取数据失败: {str(e)}")
def calculate_investment(self):
"""计算定投收益"""
if self.data is None:
raise ValueError("数据未初始化,请先调用 fetch_data 方法。")
investment_dates = self._get_investment_dates()
adjusted_dates = self._adjust_dates(investment_dates)
investment_records = self._calculate_investment_records(adjusted_dates)
self.investment_df = pd.DataFrame(investment_records)
self._calculate_annual_returns()
self._generate_result_text()
def _get_investment_dates(self):
"""根据定投频率生成定投日期"""
start_date = self.data.index[0]
end_date = self.data.index[-1]
if self.frequency == '按月':
return pd.date_range(start=start_date, end=end_date, freq='MS')
elif self.frequency == '按周':
return pd.date_range(start=start_date, end=end_date, freq='W-MON')
elif self.frequency == '按年':
return pd.date_range(start=start_date, end=end_date, freq='YS')
else:
raise ValueError("无效的定投方式")
def _adjust_dates(self, investment_dates):
"""将定投日期调整为最近的交易日"""
adjusted_dates = []
monthly_investment = {}
for date in investment_dates:
if date not in self.data.index:
next_date = self.data.index[self.data.index >= date].min()
if not next_date:
raise ValueError(f"无法找到 {date} 之后的交易日")
adjusted_date = next_date
else:
adjusted_date = date
month_key = (adjusted_date.year, adjusted_date.month)
if month_key not in monthly_investment:
monthly_investment[month_key] = adjusted_date
adjusted_dates.append(adjusted_date)
return adjusted_dates
def _calculate_investment_records(self, adjusted_dates):
"""计算每次定投的记录"""
total_shares = 0
total_investment = 0
records = []
for date in adjusted_dates:
price = self.data.loc[date, '价格']
shares = self.amount / price
total_shares += shares
total_investment += self.amount
current_value = total_shares * price
records.append({
'日期': date,
'价格': price,
'份额': shares,
'累计份额': total_shares,
'累计投入': total_investment,
'当前市值': current_value,
'累计收益率': (current_value - total_investment) / total_investment
})
return records
def _calculate_annual_returns(self):
"""计算每年的收益率"""
self.investment_df['年份'] = self.investment_df['日期'].dt.year
annual_return = self.investment_df.groupby('年份')['累计收益率'].last()
annual_return = annual_return.diff().fillna(annual_return.iloc[0])
self.data['收益率'] = self.data['价格'].pct_change().fillna(0)
asset_annual_return = self.data.groupby(self.data.index.year)['收益率'].apply(lambda x: (1 + x).prod() - 1)
self.annual_return_df = pd.DataFrame({
'年份': annual_return.index,
'定投策略收益率': annual_return.values,
'标的收益率': asset_annual_return.values
})
def _generate_result_text(self):
"""生成结果文本"""
total_investment = self.investment_df['累计投入'].iloc[-1]
final_value = self.investment_df['当前市值'].iloc[-1]
total_return = self.investment_df['累计收益率'].iloc[-1]
annualized_return = (1 + total_return) ** (365 / (self.investment_df['日期'].iloc[-1] - self.investment_df['日期'].iloc[0]).days) - 1
lump_sum_shares = total_investment / self.data['价格'].iloc[0]
lump_sum_value = lump_sum_shares * self.data['价格'].iloc[-1]
lump_sum_return = (lump_sum_value - total_investment) / total_investment
lump_sum_annualized_return = (1 + lump_sum_return) ** (365 / (self.data.index[-1] - self.data.index[0]).days) - 1
self.result_text_dca = (
f"定投累计投入金额: {total_investment:.2f} 元\n"
f"定投最终市值: {final_value:.2f} 元\n"
f"定投累计收益率: {total_return:.2%}\n"
f"定投年化收益率: {annualized_return:.2%}"
)
self.result_text_lump_sum = (
f"一次性投入金额: {total_investment:.2f} 元\n"
f"一次性投入最终市值: {lump_sum_value:.2f} 元\n"
f"一次性投入累计收益率: {lump_sum_return:.2%}\n"
f"一次性投入年化收益率: {lump_sum_annualized_return:.2%}"
)
def export_to_excel(self, file_path):
"""导出结果到 Excel 文件"""
wb = Workbook()
ws1 = wb.active
ws1.title = "每年收益"
ws2 = wb.create_sheet("交易明细")
ws3 = wb.create_sheet("结果摘要")
self._write_annual_returns_to_excel(ws1)
self._write_investment_records_to_excel(ws2)
self._write_summary_to_excel(ws3)
img_data = io.BytesIO()
self.plot_investment_curve().savefig(img_data, format='png', bbox_inches='tight', dpi=96)
img_data.seek(0)
img = ExcelImage(img_data)
ws1.add_image(img, 'E2')
wb.save(file_path)
print(f"数据已成功导出到 {file_path}")
def _write_annual_returns_to_excel(self, ws):
"""将每年收益率写入 Excel"""
ws.append(["年份", "定投策略收益率", "标的收益率"])
for _, row in self.annual_return_df.iterrows():
ws.append([row['年份'], row['定投策略收益率'], row['标的收益率']])
def _write_investment_records_to_excel(self, ws):
"""将交易明细写入 Excel"""
ws.append(["日期", "价格", "份额", "累计份额", "累计投入", "当前市值", "累计收益率"])
for _, row in self.investment_df.iterrows():
ws.append([row['日期'], row['价格'], row['份额'], row['累计份额'], row['累计投入'], row['当前市值'], row['累计收益率']])
def _write_summary_to_excel(self, ws):
"""将结果摘要写入 Excel"""
ws.append(["定投结果"])
ws.append([self.result_text_dca])
ws.append(["一次性投入结果"])
ws.append([self.result_text_lump_sum])
def plot_investment_curve(self):
"""绘制定投收益曲线"""
fig, ax = plt.subplots(figsize=(6, 4))
ax.plot(self.investment_df['日期'], self.investment_df['累计收益率'], label='定投策略收益率', color='red')
ax.plot(self.data.index, self.data['价格'] / self.data['价格'].iloc[0] - 1, label=f'{self.asset_name} 收益率', color='blue', alpha=0.7)
ax.set_title(f'定投收益曲线 - {self.asset_name}')
ax.set_xlabel('日期')
ax.set_ylabel('收益率')
ax.legend()
ax.grid(alpha=0.3)
return fig
def calculate_volatility(self):
"""计算年化波动率"""
self.data['对数收益率'] = np.log(self.data['价格'] / self.data['价格'].shift(1))
daily_volatility = self.data['对数收益率'].std()
return daily_volatility * np.sqrt(252)
def calculate_three_year_return_distribution(self):
"""计算前三年涨跌幅分布"""
three_years_ago = self.data.index[-1] - pd.DateOffset(years=3)
recent_data = self.data[self.data.index >= three_years_ago]
recent_data['涨跌幅'] = recent_data['价格'].pct_change().dropna()
return recent_data['涨跌幅']
def plot_return_distribution(self, returns):
"""绘制涨跌幅分布直方图"""
fig, ax = plt.subplots(figsize=(6, 4))
ax.hist(returns, bins=50, color='blue', alpha=0.7)
ax.set_title('前三年涨跌幅分布')
ax.set_xlabel('涨跌幅')
ax.set_ylabel('频率')
ax.xaxis.set_major_formatter(plt.FuncFormatter(lambda x, _: f'{x:.1%}'))
ax.grid(alpha=0.3)
return fig
def send_email(content, today, attachments=None):
QQ_EMAIL = "xx@qq.com" # 发件人邮箱
QQ_PASSWORD = "xx" # 发件人邮箱密码或授权码
TO_EMAIL= "xx@qq.com" # 收件人邮箱
"""发送邮件"""
try:
yag = yagmail.SMTP(user=QQ_EMAIL, password=QQ_PASSWORD, host='smtp.qq.com', port=465, smtp_ssl=True)
subject = f"定投收益报告 - {today}"
text_content = f"日期: {today}\n\n定投结果:\n{content}\n\n附件中包含详细的定投收益数据和图表。"
contents = [text_content]
if attachments:
contents.extend(attachments) # 确保附件列表被正确添加
yag.send(to=TO_EMAIL, subject=subject, contents=contents)
print("邮件发送成功")
except Exception as e:
print(f"邮件发送失败: {e}")
def main():
parser = argparse.ArgumentParser(description="定投收益计算器")
parser.add_argument("code", type=str, help="定投代码(如 510300 或 000001)") # 添加默认值
parser.add_argument("--amount", type=float, help="定投金额(元)", nargs='?', default=1000.0) # 添加默认值
parser.add_argument("--frequency", type=str, choices=["按月", "按周", "按年"], help="定投频率", nargs='?', default="按月") # 添加默认值
parser.add_argument("--output", type=str, help="导出 Excel 文件的路径", default="result.xlsx") # 修改默认值
args = parser.parse_args()
print("定投代码:", args.code)
print("定投金额:", args.amount)
print("定投频率:", args.frequency)
print("输出文件:", args.output)
calculator = DCACalculator(args.code, args.amount, args.frequency)
calculator.fetch_data()
calculator.calculate_investment()
print("定投结果:")
print(calculator.result_text_dca)
print("\n一次性投入结果:")
print(calculator.result_text_lump_sum)
calculator.plot_investment_curve().savefig("收益曲线.png", bbox_inches='tight', dpi=96)
print("收益曲线已保存为 '收益曲线.png'")
if args.output:
calculator.export_to_excel(args.output)
annualized_volatility = calculator.calculate_volatility()
print(f"\n年化波动率: {annualized_volatility:.2%}")
returns = calculator.calculate_three_year_return_distribution()
calculator.plot_return_distribution(returns).savefig("涨跌幅分布.png", bbox_inches='tight', dpi=96)
print("涨跌幅分布图已保存为 '涨跌幅分布.png'")
today =datetime.now().strftime("%Y-%m-%d")
content = calculator.result_text_dca + "\n\n" + calculator.result_text_lump_sum
attachments = [args.output, "收益曲线.png", "涨跌幅分布.png"]
send_email(content, today, attachments=attachments)
if __name__ == '__main__':
main()