6 min read

使用Grok製作一份台灣ETF的bar chart race

使用Grok製作一份台灣ETF的bar chart race
20250403-image3

最近想透過Grok來製作這份台灣ETF的bar chart race,我把我的想法告訴給AI,他回給我了最陽春的程式碼:

https://www.instagram.com/reel/DHoFx0bTD5f

import yfinance as yf
import pandas as pd
from datetime import datetime
import numpy as np

# 步驟1:收集使用者輸入
def get_user_input():
    stock_codes = []
    while True:
        code = input("請輸入股票代碼(輸入 'done' 結束):")
        if code.lower() == 'done':
            break
        stock_codes.append(code + '.TW')  # 自動加上 .TW 後綴,適用於台股

    invest_day = int(input("請輸入每月定期定額的日期(1-31):"))
    
    while True:
        start_date_input = input("請輸入開始定期定額的日期(格式:YYYY-MM-DD,例如 2020-01-01):")
        try:
            start_date = pd.to_datetime(start_date_input).tz_localize('UTC')
            break
        except ValueError:
            print("日期格式錯誤,請重新輸入(例如 2020-01-01)。")
    
    while True:
        try:
            monthly_investment = float(input("請輸入每次定期定額的金額(例如 1000):"))
            if monthly_investment <= 0:
                print("金額必須大於 0,請重新輸入。")
                continue
            break
        except ValueError:
            print("輸入無效,請輸入一個數字。")

    print(f"\n你選擇的股票代碼:{stock_codes}")
    print(f"每月定期定額日期:{invest_day}號")
    print(f"開始日期:{start_date.strftime('%Y-%m-%d')}")
    print(f"每次定期定額金額:{monthly_investment} 元")
    return stock_codes, invest_day, start_date, monthly_investment

# 步驟2 & 3:收集數據並計算定期定額(含股息再投入)
def process_data(stock_codes, invest_day, start_date, monthly_investment):
    end_date = pd.to_datetime(datetime.today().strftime('%Y-%m-%d')).tz_localize('UTC')

    # 獲取所有股票的歷史數據(包括股息)並提取名稱
    data = {}
    dividends_data = {}
    stock_names = {}  # 儲存股票名稱
    for code in stock_codes:
        stock = yf.Ticker(code)
        df = stock.history(start=start_date.strftime('%Y-%m-%d'), end=end_date.strftime('%Y-%m-%d'))
        if df.empty:
            print(f"警告:無法獲取 {code} 的數據,請檢查股票代碼或網路連線。")
            continue
        df.index = df.index.tz_convert('UTC')  # 統一轉換為 UTC 時區
        dataundefined = df['Close']
    dividends_dataundefined = df['Dividends']
    # 提取股票名稱
    try:
        stock_info = stock.info
        stock_names[code.split('.')[0]] = stock_info.get('longName', stock_info.get('shortName', '未知名稱'))
    except Exception as e:
        print(f"警告:無法獲取 {code} 的名稱,錯誤:{e}")
        stock_names[code.split('.')[0]] = '未知名稱'

if not data:
    raise ValueError("所有股票數據均無法獲取,請檢查輸入的股票代碼或網路連線。")

# 獲取所有唯一的交易日
all_trading_days = pd.concat([df.index.to_series() for df in data.values()]).unique()
all_trading_days = pd.DatetimeIndex(all_trading_days).sort_values()
print(f"交易日數量:{len(all_trading_days)}")

# 生成每月的定期定額日期
invest_dates = []
current_date = start_date.replace(day=1)  # 從開始月份的第一天開始
while current_date <= end_date:
    try:
        invest_date = pd.Timestamp(year=current_date.year, month=current_date.month, day=invest_day, tz='UTC')
    except ValueError:
        current_date += pd.DateOffset(months=1)
        continue

    if invest_date in all_trading_days:
        invest_dates.append(invest_date)
    else:
        next_trading_day_idx = np.searchsorted(all_trading_days, invest_date, side='right')
        if next_trading_day_idx < len(all_trading_days):
            next_trading_day = all_trading_days[next_trading_day_idx]
            if next_trading_day <= end_date:
                invest_dates.append(next_trading_day)

    current_date += pd.DateOffset(months=1)

invest_dates = pd.to_datetime(invest_dates).unique()
print(f"生成的定期定額日期數量:{len(invest_dates)}")
if len(invest_dates) == 0:
    raise ValueError("無法生成有效的定期定額日期,請檢查日期範圍或交易日數據。")

# 計算每檔股票的累積價值(含股息再投入)
cumulative_values = pd.DataFrame(index=invest_dates)
for code in stock_codes:
    if code not in data:
        continue
    prices = dataundefined[dataundefined.index.isin(invest_dates)]
    dividends = dividends_dataundefined[dividends_dataundefined.index.isin(invest_dates)]
    
    units = 0  # 持有單位數
    values = []
    for date in invest_dates:
        price = prices.get(date)
        dividend = dividends.get(date, 0)  # 若無股息,預設為 0
        if pd.notna(price):
            # 計算當天股息金額(持有單位數 × 每單位股息)
            dividend_amount = units * dividend if dividend > 0 else 0
            # 總投入金額 = 使用者輸入的固定金額 + 股息再投入
            total_investment = monthly_investment + dividend_amount
            # 購買單位數
            units += total_investment / price
            # 當前總價值
            total_value = units * price
            values.append(total_value)
        else:
            values.append(None)
    cumulative_values[code.split('.')[0]] = values

# 將累積價值取整到個位數
cumulative_values = cumulative_values.round(0).astype(int)

# 添加總投入成本(使用者輸入的金額 × 投資次數)
total_investments = len(invest_dates)  # 總投資次數
total_cost = total_investments * monthly_investment
cumulative_values.loc['總投入成本'] = total_cost

# 格式化日期索引
cumulative_values.index = cumulative_values.index.map(lambda x: x.strftime('%Y-%m-%d') if isinstance(x, pd.Timestamp) else x)

return cumulative_values, stock_names

主程式
def main():
stock_codes, invest_day, start_date, monthly_investment = get_user_input()
cumulative_values, stock_names = process_data(stock_codes, invest_day, start_date, monthly_investment)
output_file = 'etf_cumulative_values.csv'
cumulative_values.to_csv(output_file)
print(f"\n數據已儲存為 {output_file}")
print("請將此檔案手動上傳至 Flourish,選擇 'Bar Chart Race' 模板進行調整。")
print("\n數據預覽(含總投入成本):")
print(cumulative_values)
print("\n股票代號與名稱:")
print("股票代號:", ", ".join(stock_names.keys()))
print("股票名稱:", ", ".join(stock_names.values()))

這個程式主要功能有:

1.可以輸入很多ETF代碼

2.每個月幾號做定期定額

3.定期定額開始日

4.每次投入的金額

然後,會自動股息再投入(當天)

最終結果會產生CSV檔,如下:

我只需要把這個CSV檔,匯入到專門做這種bar chart race 的網站Flourish去做就可以了。

在Flourish簡單的設定行列代表的名稱,例如Lable標籤是代號、Value是月份對應的現在的資產;Categories分類就填中文名稱。

簡單的設定好之後,就會有競賽圖出現囉。

做這可以幹嘛?

我想可以給投資新手看如果簡單的定期定額,例如我做的是每個月無腦投10000,從2020年8月投資到現在,如果不投資放銀行就只有56萬,但是丟到0050卻可以有85萬的價值,直接多出51%的價值!更何況時下跌時加碼投資後,績效會更佳。

簡單來說就是圖示化投資績效,讓投資者更了解定期定額的魅力。

不過這個程式碼有個小問題,就是沒辦法做到配股,我嘗試試了幾回AI,到最後股數會莫名其妙翻倍,所以,如果拿來做銀行股,或其他有配股的公司計算定期定額,可能會稍微不準。

供給讀者們參考~