ExpB:測報酬

項目 Experiment A Experiment B
目標值 Close Price Log Return
公式 (P_t) ((P_t/P_{t-1}))
Scaler StandardScaler StandardScaler
輸出 預測股價 預測報酬率
優點 接近一般時間序列 benchmark 比較符合金融建模
缺點 模型可能被價格趨勢影響 需要 inverse transform 回價格

載入資料

import yfinance as yf
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt


ticker = "AAPL"

data = yf.download(
    ticker,
    start="2010-01-01",
    end="2026-01-01"
)


df = data[["Close"]].copy()


# 處理 yfinance multi-index
if isinstance(df.columns, pd.MultiIndex):
    df.columns = df.columns.get_level_values(0)


df = df.reset_index()


df.columns = [
    "ds",
    "close"
]


df.head()
[*********************100%***********************]  1 of 1 completed
ds close
0 2010-01-04 6.406480
1 2010-01-05 6.417557
2 2010-01-06 6.315477
3 2010-01-07 6.303801
4 2010-01-08 6.345714
# log return
df["y"] = np.log(
    df["close"] /
    df["close"].shift(1)
)


df = df.dropna()


df.head()
ds close y
1 2010-01-05 6.417557 0.001727
2 2010-01-06 6.315477 -0.016034
3 2010-01-07 6.303801 -0.001851
4 2010-01-08 6.345714 0.006627
5 2010-01-11 6.289732 -0.008861
split = int(len(df)*0.8)


train = df.iloc[:split].copy()

test = df.iloc[split:].copy()


print(train.shape)
print(test.shape)
(3218, 3)
(805, 3)
# Normalize Return
from sklearn.preprocessing import StandardScaler


scaler = StandardScaler()


train["y_scaled"] = scaler.fit_transform(
    train[["y"]]
)


test["y_scaled"] = scaler.transform(
    test[["y"]]
)

PatchTST

from neuralforecast import NeuralForecast
from neuralforecast.models import PatchTST


patch_train = train[
    ["ds","y_scaled"]
].copy()


patch_train.columns=[
    "ds",
    "y"
]


patch_train["unique_id"]="AAPL"


model = PatchTST(
    h=15,
    input_size=120,
    patch_len=12,
    stride=6,
    max_steps=300
)


nf = NeuralForecast(
    models=[model],
    freq="D"
)


nf.fit(
    patch_train
)
Seed set to 1
GPU available: True (cuda), used: True
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs
You are using a CUDA device ('NVIDIA GeForce RTX 4090') that has Tensor Cores. To properly utilize them, you should set `torch.set_float32_matmul_precision('medium' | 'high')` which will trade-off precision for performance. For more details, read https://pytorch.org/docs/stable/generated/torch.set_float32_matmul_precision.html#torch.set_float32_matmul_precision
LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]

  | Name         | Type              | Params | Mode 
-----------------------------------------------------------
0 | loss         | MAE               | 0      | train
1 | padder_train | ConstantPad1d     | 0      | train
2 | scaler       | TemporalNorm      | 0      | train
3 | model        | PatchTST_backbone | 440 K  | train
-----------------------------------------------------------
440 K     Trainable params
3         Non-trainable params
440 K     Total params
1.760     Total estimated model params size (MB)
90        Modules in train mode
0         Modules in eval mode
`Trainer.fit` stopped: `max_steps=300` reached.
patch_forecast = nf.predict()


patch_return_scaled = (
    patch_forecast["PatchTST"]
    .values
)


print(patch_return_scaled)
GPU available: True (cuda), used: True
TPU available: False, using: 0 TPU cores
HPU available: False, using: 0 HPUs
LOCAL_RANK: 0 - CUDA_VISIBLE_DEVICES: [0]
[ 0.055035   -0.06732416  0.11186814 -0.09216654 -0.19092381 -0.13989282
 -0.07315946 -0.11535895 -0.38285828  0.21823967 -0.31677127 -0.02535093
 -0.3823135  -0.47748888 -0.12180007]

Chronos

import torch
from chronos import ChronosPipeline


context = torch.tensor(
    train["y_scaled"].values,
    dtype=torch.float32
)


chronos = ChronosPipeline.from_pretrained(
    "amazon/chronos-t5-small",
    device_map="auto"
)


forecast = chronos.predict(
    context,
    prediction_length=15
)


chronos_return_scaled = (
    forecast
    .mean(dim=1)
    .squeeze()
    .numpy()
)


print(chronos_return_scaled)
[-0.04900789  0.04841743  0.00472365  0.01180913 -0.00619979 -0.02125644
  0.00295228 -0.02066598 -0.03837968 -0.05107449 -0.06199793 -0.03395125
 -0.06524545 -0.04103672 -0.05904565]

TimesFM

import timesfm
import numpy as np


timesfm_model = timesfm.TimesFM_2p5_200M_torch.from_pretrained(
    "google/timesfm-2.5-200m-pytorch"
)


timesfm_model.compile(
    timesfm.ForecastConfig(
        max_context=512,
        max_horizon=128
    )
)


input_series=[
    train["y_scaled"]
    .values.astype(np.float32)
]


point_forecast, quantile_forecast = (
    timesfm_model.forecast(
        horizon=15,
        inputs=input_series
    )
)


timesfm_return_scaled = point_forecast[0]


print(timesfm_return_scaled)
[-0.12856379  0.00036838  0.05585762 -0.06376063 -0.07890935 -0.03879329
 -0.03868837  0.00240039 -0.01284213 -0.00977794 -0.03651828 -0.03141294
 -0.03970855 -0.04355175 -0.04146475]

還原 return

patch_return = scaler.inverse_transform(
    patch_return_scaled.reshape(-1,1)
).flatten()


chronos_return = scaler.inverse_transform(
    chronos_return_scaled.reshape(-1,1)
).flatten()


timesfm_return = scaler.inverse_transform(
    timesfm_return_scaled.reshape(-1,1)
).flatten()
# Return → Price
# 最後一天 training close:
last_price = train["close"].iloc[-1]

print(last_price)
135.84523010253906
def return_to_price(last_price, returns):

    prices=[]

    price=last_price

    for r in returns:
        price = price*np.exp(r)
        prices.append(price)

    return np.array(prices)
patch_price = return_to_price(
    last_price,
    patch_return
)


chronos_price = return_to_price(
    last_price,
    chronos_return
)


timesfm_price = return_to_price(
    last_price,
    timesfm_return
)
real_price = test["close"].iloc[:15]

future_date = test["ds"].iloc[:15]

畫圖

plt.figure(figsize=(12,5))


plt.plot(
    future_date,
    real_price,
    marker="o",
    label="Real"
)


plt.plot(
    future_date,
    patch_price,
    marker="o",
    label="PatchTST"
)


plt.plot(
    future_date,
    chronos_price,
    marker="o",
    label="Chronos"
)


plt.plot(
    future_date,
    timesfm_price,
    marker="o",
    label="TimesFM"
)


plt.title("AAPL 15-day Forecast (Log Return)")


plt.legend()
plt.grid()

plt.show()

def direction_accuracy(real, pred):

    real_change = np.diff(real)
    pred_change = np.diff(pred)

    return np.mean(
        np.sign(real_change)
        ==
        np.sign(pred_change)
    )


print(
    "PatchTST:",
    direction_accuracy(
        real_price,
        patch_price
    )
)


print(
    "Chronos:",
    direction_accuracy(
        real_price,
        chronos_price
    )
)


print(
    "TimesFM:",
    direction_accuracy(
        real_price,
        timesfm_price
    )
)
PatchTST: 0.6428571428571429
Chronos: 0.6428571428571429
TimesFM: 0.42857142857142855
result = pd.DataFrame({
    "real": real_price,
    "patch": patch_price,
    "chronos": chronos_price,
    "timesfm": timesfm_price
})


result["real_direction"] = np.sign(
    result.real.diff()
)


result["patch_direction"] = np.sign(
    result.patch.diff()
)


result
real patch chronos timesfm real_direction patch_direction
3219 139.801453 136.108626 135.854655 135.660756 NaN NaN
3220 141.116882 136.073303 136.101906 135.790472 1.0 -1.0
3221 141.224854 136.476319 136.242687 136.055768 1.0 1.0
3222 140.763443 136.380070 136.400976 136.029161 -1.0 -1.0
3223 144.572433 136.042494 136.515302 135.965595 1.0 -1.0
3224 146.712494 135.830125 136.592798 135.999956 1.0 -1.0
3225 149.549545 135.780658 136.729739 136.034586 1.0 -1.0
3226 146.614288 135.628417 136.808808 136.169638 -1.0 -1.0
3227 142.147659 134.827333 136.844410 136.267521 -1.0 -1.0
3228 152.887253 135.485139 136.848815 136.372979 1.0 1.0
3229 150.531219 134.844773 136.826391 136.413004 -1.0 -1.0
3230 147.890488 134.911403 136.872861 136.465545 -1.0 1.0
3231 142.373459 134.115856 136.842456 136.497788 -1.0 -1.0
3232 136.336060 133.097406 136.871525 136.520618 -1.0 -1.0
3233 136.070587 132.932807 136.856351 136.548561 -1.0 -1.0

結論

模型 方向 價格追蹤
PatchTST 較好
Chronos 較好
TimesFM 普通
import numpy as np
import pandas as pd

from sklearn.metrics import mean_squared_error, mean_absolute_error

def RMSE(y_true, y_pred):
    return np.sqrt(
        mean_squared_error(y_true, y_pred)
    )

last_price = train["y"].iloc[-1]

naive_price = np.repeat(
    last_price,
    len(real_price)
)

print(naive_price)

results = []


models = { "Naive": naive_price,
    "PatchTST": patch_price,
    "Chronos": chronos_price,
    "TimesFM": timesfm_price}


for name, pred in models.items():

    rmse = RMSE(real_price,pred)

    mae = mean_absolute_error(real_price,pred)

    direction = np.mean(np.sign(np.diff(real_price))==np.sign(np.diff(pred)))


    results.append(
        [
            name,
            rmse,
            mae,
            direction
        ]
    )


metrics = pd.DataFrame(
    results,
    columns=[
        "Model",
        "RMSE",
        "MAE",
        "Direction_Accuracy"
    ]
)


metrics
[-0.03277108 -0.03277108 -0.03277108 -0.03277108 -0.03277108 -0.03277108
 -0.03277108 -0.03277108 -0.03277108 -0.03277108 -0.03277108 -0.03277108
 -0.03277108 -0.03277108 -0.03277108]
Model RMSE MAE Direction_Accuracy
0 Naive 144.020814 143.938912 0.000000
1 PatchTST 9.803942 8.670493 0.642857
2 Chronos 8.711920 7.468326 0.642857
3 TimesFM 9.118357 7.808349 0.428571
print(train.tail())
print(real_price[:5])
             ds       close         y  y_scaled
3214 2022-10-10  137.847870  0.002353  0.078190
3215 2022-10-11  136.434235 -0.010308 -0.627064
3216 2022-10-12  135.805969 -0.004616 -0.309972
3217 2022-10-13  140.370773  0.033060  1.788710
3218 2022-10-14  135.845230 -0.032771 -1.878346
3219    139.801453
3220    141.116882
3221    141.224854
3222    140.763443
3223    144.572433
Name: close, dtype: float64
無符合的項目