29 KiB
29 KiB
In [1]:
from pathlib import Path
import numpy as np
import pandas as pd
def find_project_root(start: Path | None = None) -> Path:
current = (start or Path.cwd()).resolve()
for candidate in [current, *current.parents]:
if (candidate / "pyproject.toml").exists():
return candidate
raise RuntimeError("Could not find project root containing pyproject.toml")
PROJECT_ROOT = find_project_root()
RAW_DATA_DIR = PROJECT_ROOT / "data" / "ibkr" / "daily"
OUTPUT_PATH = PROJECT_ROOT / "data" / "training" / "spy_direction_5d.parquet"
SPY_SYMBOL = "SPY"
VIX_INPUT_SYMBOL = "VIXY"
TLT_SYMBOL = "TLT"
USO_SYMBOL = "USO"
TRAIN_FRACTION = 0.70
VALIDATION_FRACTION = 0.15
TEST_FRACTION = 0.15
PROJECT_ROOT, RAW_DATA_DIR, OUTPUT_PATHOut [1]:
(PosixPath('/home/jarno/repos/trading-bot'),
PosixPath('/home/jarno/repos/trading-bot/data/ibkr/daily'),
PosixPath('/home/jarno/repos/trading-bot/data/training/spy_direction_5d.parquet'))In [2]:
def load_close(symbol: str, alias: str | None = None) -> pd.DataFrame:
alias = alias or symbol
path = RAW_DATA_DIR / f"{symbol}.parquet"
if not path.exists():
raise FileNotFoundError(f"Missing raw data file: {path}")
frame = pd.read_parquet(path)
if "date" in frame.columns:
frame = frame.set_index("date")
if "close" not in frame.columns:
raise ValueError(f"{path} does not contain a close column")
frame = frame.copy()
frame.index = pd.to_datetime(frame.index)
frame.index.name = "date"
frame = frame.sort_index()
return frame[["close"]].rename(columns={"close": f"{alias}_close"})
prices = pd.concat(
[
load_close(SPY_SYMBOL, "SPY"),
load_close(VIX_INPUT_SYMBOL, "VIX"),
load_close(TLT_SYMBOL, "TLT"),
load_close(USO_SYMBOL, "USO"),
],
axis=1,
join="inner",
)
prices.info()
prices.head()Out [2]:
<class 'pandas.DataFrame'> DatetimeIndex: 1251 entries, 2021-07-30 to 2026-07-24 Data columns (total 4 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 SPY_close 1251 non-null float64 1 VIX_close 1251 non-null float64 2 TLT_close 1251 non-null float64 3 USO_close 1251 non-null float64 dtypes: float64(4) memory usage: 48.9 KB
| SPY_close | VIX_close | TLT_close | USO_close | |
|---|---|---|---|---|
| date | ||||
| 2021-07-30 | 438.51 | 495.4 | 149.52 | 50.66 |
| 2021-08-02 | 437.59 | 513.6 | 150.67 | 49.18 |
| 2021-08-03 | 441.15 | 488.0 | 150.75 | 48.85 |
| 2021-08-04 | 438.98 | 487.6 | 151.06 | 47.20 |
| 2021-08-05 | 441.76 | 474.8 | 150.29 | 48.10 |
In [3]:
df = prices.copy()
df["SPY_ret_5"] = df["SPY_close"].pct_change(5)
df["SPY_ret_20"] = df["SPY_close"].pct_change(20)
sma_50 = df["SPY_close"].rolling(50).mean()
df["SPY_dist_sma50"] = (df["SPY_close"] - sma_50) / sma_50
df["VIX_change_5"] = df["VIX_close"].pct_change(5)
df["VIX_rank_20"] = df["VIX_close"].rolling(20).rank(pct=True)
df["TLT_ret_10"] = df["TLT_close"].pct_change(10)
df["USO_ret_5"] = df["USO_close"].pct_change(5)
df["SPY_TLT_ratio_ret"] = (df["SPY_close"] / df["TLT_close"]).pct_change(5)
spy_forward_close = df["SPY_close"].shift(-5)
df["spy_up_5d"] = np.nan
df.loc[spy_forward_close > df["SPY_close"], "spy_up_5d"] = 1.0
df.loc[spy_forward_close < df["SPY_close"], "spy_up_5d"] = 0.0
df.loc[spy_forward_close == df["SPY_close"], "spy_up_5d"] = 0.5
FEATURE_COLUMNS = [
"SPY_ret_5",
"SPY_ret_20",
"SPY_dist_sma50",
"VIX_change_5",
"VIX_rank_20",
"TLT_ret_10",
"USO_ret_5",
"SPY_TLT_ratio_ret",
]
TARGET_COLUMN = "spy_up_5d"
df_model = df[FEATURE_COLUMNS + [TARGET_COLUMN]].dropna().copy()
df_model.head()Out [3]:
| SPY_ret_5 | SPY_ret_20 | SPY_dist_sma50 | VIX_change_5 | VIX_rank_20 | TLT_ret_10 | USO_ret_5 | SPY_TLT_ratio_ret | spy_up_5d | |
|---|---|---|---|---|---|---|---|---|---|
| date | |||||||||
| 2021-10-08 | 0.008336 | -0.017017 | -0.011155 | -0.075239 | 0.125 | -0.034239 | 0.041495 | 0.032998 | 1.0 |
| 2021-10-11 | 0.014114 | -0.026625 | -0.018145 | -0.090869 | 0.200 | -0.033135 | 0.031015 | 0.038908 | 1.0 |
| 2021-10-12 | 0.001201 | -0.023752 | -0.020386 | -0.074091 | 0.100 | -0.001041 | 0.008628 | -0.001303 | 1.0 |
| 2021-10-13 | 0.000644 | -0.028356 | -0.016597 | -0.081006 | 0.050 | 0.006928 | 0.036928 | -0.005897 | 1.0 |
| 2021-10-14 | 0.008754 | -0.010443 | -0.000214 | -0.096759 | 0.050 | 0.010809 | 0.026192 | -0.011991 | 1.0 |
In [4]:
def assign_chronological_splits(
frame: pd.DataFrame,
train_fraction: float,
validation_fraction: float,
test_fraction: float,
) -> pd.Series:
total_fraction = train_fraction + validation_fraction + test_fraction
if not np.isclose(total_fraction, 1.0):
raise ValueError(f"Split fractions must sum to 1.0, got {total_fraction}")
n_rows = len(frame)
train_end = int(n_rows * train_fraction)
validation_end = train_end + int(n_rows * validation_fraction)
split = pd.Series(index=frame.index, dtype="object")
split.iloc[:train_end] = "train"
split.iloc[train_end:validation_end] = "validation"
split.iloc[validation_end:] = "test"
return split
df_model["split"] = assign_chronological_splits(
df_model,
TRAIN_FRACTION,
VALIDATION_FRACTION,
TEST_FRACTION,
)
split_summary = df_model.groupby("split", sort=False).agg(
rows=(TARGET_COLUMN, "size"),
start_date=(TARGET_COLUMN, lambda values: values.index.min().date()),
end_date=(TARGET_COLUMN, lambda values: values.index.max().date()),
target_mean=(TARGET_COLUMN, "mean"),
)
split_summaryOut [4]:
| rows | start_date | end_date | target_mean | |
|---|---|---|---|---|
| split | ||||
| train | 837 | 2021-10-08 | 2025-02-07 | 0.583035 |
| validation | 179 | 2025-02-10 | 2025-10-24 | 0.653631 |
| test | 181 | 2025-10-27 | 2026-07-17 | 0.558011 |
In [5]:
model_input_columns = FEATURE_COLUMNS
model_target_column = TARGET_COLUMN
X_train = df_model.loc[df_model["split"] == "train", model_input_columns]
y_train = df_model.loc[df_model["split"] == "train", model_target_column]
print(f"Rows: {len(df_model):,}")
print(f"Feature columns: {model_input_columns}")
print(f"Target column: {model_target_column}")
print(f"Training matrix shape: {X_train.shape}")
df_model.describe(include="all")Out [5]:
Rows: 1,197 Feature columns: ['SPY_ret_5', 'SPY_ret_20', 'SPY_dist_sma50', 'VIX_change_5', 'VIX_rank_20', 'TLT_ret_10', 'USO_ret_5', 'SPY_TLT_ratio_ret'] Target column: spy_up_5d Training matrix shape: (837, 8)
| SPY_ret_5 | SPY_ret_20 | SPY_dist_sma50 | VIX_change_5 | VIX_rank_20 | TLT_ret_10 | USO_ret_5 | SPY_TLT_ratio_ret | spy_up_5d | split | |
|---|---|---|---|---|---|---|---|---|---|---|
| count | 1197.000000 | 1197.000000 | 1197.000000 | 1197.000000 | 1197.000000 | 1197.000000 | 1197.000000 | 1197.000000 | 1197.000000 | 1197 |
| unique | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 3 |
| top | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | train |
| freq | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | 837 |
| mean | 0.002556 | 0.009841 | 0.011067 | -0.008646 | 0.387009 | -0.004083 | 0.004662 | 0.004927 | 0.589808 | NaN |
| std | 0.023081 | 0.043634 | 0.037371 | 0.092413 | 0.333697 | 0.028557 | 0.052319 | 0.027656 | 0.492074 | NaN |
| min | -0.114962 | -0.123975 | -0.141995 | -0.387709 | 0.050000 | -0.092880 | -0.196652 | -0.117208 | 0.000000 | NaN |
| 25% | -0.009535 | -0.017017 | -0.008847 | -0.058376 | 0.075000 | -0.023517 | -0.026200 | -0.010013 | 0.000000 | NaN |
| 50% | 0.003869 | 0.015797 | 0.017906 | -0.020655 | 0.250000 | -0.004339 | 0.004817 | 0.005710 | 1.000000 | NaN |
| 75% | 0.015957 | 0.038385 | 0.038332 | 0.027778 | 0.700000 | 0.014272 | 0.031883 | 0.021406 | 1.000000 | NaN |
| max | 0.082843 | 0.157566 | 0.088103 | 0.937828 | 1.000000 | 0.091322 | 0.327273 | 0.129204 | 1.000000 | NaN |
In [6]:
OUTPUT_PATH.parent.mkdir(parents=True, exist_ok=True)
dataset_to_save = df_model.reset_index()
dataset_to_save.to_parquet(OUTPUT_PATH, index=False)
OUTPUT_PATH, dataset_to_save.shape, dataset_to_save.head()Out [6]:
(PosixPath('/home/jarno/repos/trading-bot/data/training/spy_direction_5d.parquet'),
(1197, 11),
date SPY_ret_5 SPY_ret_20 SPY_dist_sma50 VIX_change_5 \
0 2021-10-08 0.008336 -0.017017 -0.011155 -0.075239
1 2021-10-11 0.014114 -0.026625 -0.018145 -0.090869
2 2021-10-12 0.001201 -0.023752 -0.020386 -0.074091
3 2021-10-13 0.000644 -0.028356 -0.016597 -0.081006
4 2021-10-14 0.008754 -0.010443 -0.000214 -0.096759
VIX_rank_20 TLT_ret_10 USO_ret_5 SPY_TLT_ratio_ret spy_up_5d split
0 0.125 -0.034239 0.041495 0.032998 1.0 train
1 0.200 -0.033135 0.031015 0.038908 1.0 train
2 0.100 -0.001041 0.008628 -0.001303 1.0 train
3 0.050 0.006928 0.036928 -0.005897 1.0 train
4 0.050 0.010809 0.026192 -0.011991 1.0 train )