Tune model config for price-only training and add walk-forward CV
Downsize GNN hidden channels/heads (128→64, 16→4) to match the 30-stock price-only universe before alternative data processors are ready. Add USE_ALTERNATIVE_DATA flag (default False) that skips news/social branches so the model trains on real data rather than zero-filled stubs. Standardize target returns per-batch in the data pipeline to stabilize training. Introduce walk-forward cross-validation with expanding windows: configurable fold count and out-of-sample years. Add IC loss weighting (0.7) to complement MSE, and wire it into the trainer alongside the new loss function.
This commit is contained in:
+65
-2
@@ -779,7 +779,12 @@ class StockDataPipeline:
|
||||
)
|
||||
node_tickers = valid_tickers_for_y
|
||||
|
||||
y = torch.tensor(y_vals, dtype=torch.float32).unsqueeze(1)
|
||||
y_arr = np.array(y_vals, dtype=np.float32)
|
||||
if len(y_arr) > 1:
|
||||
mu, sigma = y_arr.mean(), y_arr.std()
|
||||
if sigma > 1e-8:
|
||||
y_arr = (y_arr - mu) / sigma
|
||||
y = torch.tensor(y_arr, dtype=torch.float32).unsqueeze(1)
|
||||
|
||||
data = Data(x=x, edge_index=edge_index, edge_attr=edge_weight, y=y)
|
||||
data.date = date
|
||||
@@ -1001,7 +1006,12 @@ class StockDataPipeline:
|
||||
ret = future_price / current_price - 1
|
||||
y_vals.append(ret)
|
||||
|
||||
y = torch.tensor(y_vals, dtype=torch.float32).unsqueeze(1)
|
||||
y_arr = np.array(y_vals, dtype=np.float32)
|
||||
if len(y_arr) > 1:
|
||||
mu, sigma = y_arr.mean(), y_arr.std()
|
||||
if sigma > 1e-8:
|
||||
y_arr = (y_arr - mu) / sigma
|
||||
y = torch.tensor(y_arr, dtype=torch.float32).unsqueeze(1)
|
||||
|
||||
# Create Data object
|
||||
data = Data(x=x, edge_index=edge_index, edge_attr=edge_weight, y=y)
|
||||
@@ -1180,3 +1190,56 @@ class StockDataPipeline:
|
||||
"""Create a validation dataset for the given date range."""
|
||||
logger.info(f"Creating validation dataset {start_date} → {end_date}")
|
||||
return self._create_dataset(start_date, end_date)
|
||||
|
||||
def create_walk_forward_splits(self) -> List[Tuple]:
|
||||
"""
|
||||
Build walk-forward CV splits as expanding-window train / fixed OOS val pairs.
|
||||
|
||||
Each fold's training window starts at config.START_DATE and ends at a boundary
|
||||
that advances by config.WALK_FORWARD_TEST_YEARS each fold. The val window is
|
||||
the following config.WALK_FORWARD_TEST_YEARS of data. Folds that would push
|
||||
the val end beyond config.TRAIN_END_DATE (or available data) are dropped.
|
||||
|
||||
Returns a list of (train_dataset, val_dataset, meta_dict) tuples.
|
||||
"""
|
||||
from datetime import datetime, timedelta
|
||||
|
||||
train_start = config.START_DATE
|
||||
full_end = datetime.strptime(config.TRAIN_END_DATE, "%Y-%m-%d")
|
||||
test_years = config.WALK_FORWARD_TEST_YEARS
|
||||
n_folds = config.WALK_FORWARD_FOLDS
|
||||
|
||||
splits: List[Tuple] = []
|
||||
for fold in range(n_folds):
|
||||
# Each fold's val window is offset by fold * test_years from the full_end
|
||||
# minus (n_folds - fold) * test_years so folds are evenly spaced
|
||||
offset_years = (n_folds - fold) * test_years
|
||||
val_end_dt = full_end - timedelta(days=int((offset_years - test_years) * 365))
|
||||
train_end_dt = val_end_dt - timedelta(days=int(test_years * 365))
|
||||
val_start_dt = train_end_dt + timedelta(days=1)
|
||||
|
||||
if train_end_dt <= datetime.strptime(train_start, "%Y-%m-%d"):
|
||||
logger.warning(f"Skipping fold {fold}: train window too short")
|
||||
continue
|
||||
|
||||
train_end_str = train_end_dt.strftime("%Y-%m-%d")
|
||||
val_start_str = val_start_dt.strftime("%Y-%m-%d")
|
||||
val_end_str = val_end_dt.strftime("%Y-%m-%d")
|
||||
|
||||
logger.info(
|
||||
f"Walk-forward fold {fold + 1}/{n_folds}: "
|
||||
f"train {train_start}→{train_end_str}, val {val_start_str}→{val_end_str}"
|
||||
)
|
||||
|
||||
train_ds = self._create_dataset(train_start, train_end_str)
|
||||
val_ds = self._create_dataset(val_start_str, val_end_str)
|
||||
meta = {
|
||||
"fold": fold,
|
||||
"train_start": train_start,
|
||||
"train_end": train_end_str,
|
||||
"val_start": val_start_str,
|
||||
"val_end": val_end_str,
|
||||
}
|
||||
splits.append((train_ds, val_ds, meta))
|
||||
|
||||
return splits
|
||||
|
||||
Reference in New Issue
Block a user