| import os, sys, copy, joblib
|
| import numpy as np
|
| import pandas as pd
|
| from tqdm import tqdm
|
| import MetaTrader5 as mt5
|
| import torch
|
| import torch.nn as nn
|
| import torch.optim as optim
|
| from sklearn.model_selection import TimeSeriesSplit
|
| from sklearn.preprocessing import MinMaxScaler, RobustScaler, PowerTransformer
|
| from torch.utils.data import DataLoader
|
| from pathlib import Path
|
|
|
|
|
| LOOKBACK = 5
|
| BATCH_SIZE = 4
|
| EPOCHS = 50
|
| LR = 1e-2
|
| PATIENCE = 10
|
| N_SPLITS = 3
|
| DEVICE = torch.device("cuda" if torch.cuda.is_available() else "cpu")
|
| datestr = pd.Timestamp.now().strftime("%d%m%Y")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| from AIBaseClass import *
|
| from ClassUtils import *
|
|
|
|
|
| class DynamicBetaSmoothL1Loss(nn.Module):
|
| def __init__(self, init_beta=0.5, alpha=0.9, reduction='mean', eps=1e-6):
|
| super().__init__()
|
| self.register_buffer('beta', torch.tensor(init_beta))
|
| self.alpha = alpha
|
| self.reduction = reduction
|
| self.eps = eps
|
|
|
| def forward(self, pred, target):
|
| diff = torch.abs(pred - target)
|
| batch_beta = diff.mean().detach() + self.eps
|
| self.beta = self.alpha * self.beta + (1 - self.alpha) * batch_beta
|
| beta = self.beta
|
| loss = torch.where(diff < beta, 0.5 * diff**2 / beta, diff - 0.5*beta)
|
| if self.reduction=='mean':
|
| return loss.mean()
|
| elif self.reduction=='sum':
|
| return loss.sum()
|
| else:
|
| return loss
|
|
|
|
|
| symbollist = ['DXYm','EURUSDm','GBPUSDm','USDJPYm','USDCADm']
|
| for symbol in tqdm(symbollist):
|
| SAVE_DIR = Path(__file__).resolve().parent.parent / f"Models/"
|
|
|
| os.makedirs(SAVE_DIR, exist_ok=True)
|
|
|
| CHECKPOINT_DIR = Path(__file__).resolve().parent.parent / f"Checkpoint/"
|
| os.makedirs(CHECKPOINT_DIR, exist_ok=True)
|
|
|
| try:
|
|
|
| data_dir = Path(__file__).resolve().parent.parent / "Datasource"
|
|
|
| if not data_dir.exists():
|
| raise ValueError(f"Folder {data_dir} ไม่พบ")
|
|
|
| csv_file = os.path.join(data_dir,f'{symbol}.csv')
|
|
|
| df = pd.read_csv(csv_file)
|
|
|
|
|
| required_cols = ["time", "open", "high", "low", "close", "tick_volume"]
|
| for col in required_cols:
|
| if col not in df.columns:
|
| raise ValueError(f"{col} ไม่พบใน {csv_file}")
|
|
|
|
|
| df['time'] = pd.to_datetime(df['time'])
|
|
|
|
|
| df = df.sort_values(by='time')
|
|
|
|
|
| df.set_index('time', inplace=True)
|
|
|
|
|
| df['hour'] = df.index.hour / 23.0
|
| df['weekday'] = df.index.weekday / 6.0
|
| df['month'] = (df.index.month - 1) / 11.0
|
|
|
| df = df.copy()
|
| N = 3
|
|
|
| df['target_high'] = df['high'].shift(-1).ewm(span=N).mean()
|
| df['target_low'] = df['low'].shift(-1).ewm(span=N).mean()
|
| df.dropna(inplace=True)
|
| df.reset_index(drop=True, inplace=True)
|
|
|
| price_data = df[['open','high','low','close']].values
|
| target_high_data = df[['target_high']].values
|
| target_low_data = df[['target_low']].values
|
| tick_data = df[['tick_volume']].values
|
| time_data = df[['hour','weekday','month']].values
|
|
|
|
|
| scaler_price = MinMaxScaler(feature_range=(-1,1))
|
| scaler_target_high = MinMaxScaler(feature_range=(-1,1))
|
| scaler_target_low = MinMaxScaler(feature_range=(-1,1))
|
| scaler_tick = RobustScaler()
|
|
|
| tscv = TimeSeriesSplit(n_splits=N_SPLITS)
|
| for fold, (train_idx, val_idx) in enumerate(tscv.split(price_data)):
|
| print(f"[{symbol}] Fold {fold+1}/{N_SPLITS}")
|
|
|
|
|
| train_price, val_price = price_data[train_idx], price_data[val_idx]
|
| train_target_high, val_target_high = target_high_data[train_idx], target_high_data[val_idx]
|
| train_target_low, val_target_low = target_low_data[train_idx], target_low_data[val_idx]
|
| train_tick, val_tick = tick_data[train_idx], tick_data[val_idx]
|
| train_time, val_time = time_data[train_idx], time_data[val_idx]
|
|
|
|
|
| scaled_train_price = scaler_price.fit_transform(train_price)
|
| scaled_val_price = scaler_price.transform(val_price)
|
| scaled_train_target_high = scaler_target_high.fit_transform(train_target_high)
|
| scaled_val_target_high = scaler_target_high.transform(val_target_high)
|
| scaled_train_target_low = scaler_target_low.fit_transform(train_target_low)
|
| scaled_val_target_low = scaler_target_low.transform(val_target_low)
|
| scaled_train_tick = scaler_tick.fit_transform(train_tick)
|
| scaled_val_tick = scaler_tick.transform(val_tick)
|
|
|
|
|
| X_train_scaled = np.concatenate([
|
| scaled_train_price, scaled_train_tick, train_time
|
| ], axis=1)
|
| X_val_scaled = np.concatenate([
|
| scaled_val_price, scaled_val_tick, val_time
|
| ], axis=1)
|
|
|
| y_train_high, y_train_low = scaled_train_target_high, scaled_train_target_low
|
| y_val_high, y_val_low = scaled_val_target_high, scaled_val_target_low
|
|
|
|
|
| train_dataset = SinglestepDataset(X_train_scaled, y_train_high, y_train_low)
|
| val_dataset = SinglestepDataset(X_val_scaled, y_val_high, y_val_low)
|
| train_loader = DataLoader(train_dataset, batch_size=BATCH_SIZE, shuffle=False)
|
| val_loader = DataLoader(val_dataset, batch_size=BATCH_SIZE, shuffle=False)
|
|
|
|
|
| model = ConvGRUTransformerHLV10(input_dim=8, seq_len=LOOKBACK, kernel_size=LOOKBACK).to(DEVICE)
|
| optimizer = optim.Adam(model.parameters(), lr=LR, weight_decay=0.001)
|
| scheduler = torch.optim.lr_scheduler.OneCycleLR(
|
| optimizer,
|
| max_lr=LR,
|
| steps_per_epoch=len(train_loader),
|
| epochs=EPOCHS,
|
| pct_start=0.3,
|
| anneal_strategy='cos'
|
| )
|
| loss_fn = DynamicBetaSmoothL1Loss(init_beta=0.5, alpha=0.1)
|
| early_stopping = EarlyStopping(patience=PATIENCE, verbose=True)
|
|
|
| best_val_loss = float('inf')
|
| best_model_weights = None
|
|
|
|
|
| for epoch in range(EPOCHS):
|
| model.train()
|
| train_loss = 0
|
| for xb, yb_high, yb_low in train_loader:
|
| xb, yb_high, yb_low = xb.to(DEVICE), yb_high.to(DEVICE), yb_low.to(DEVICE)
|
| optimizer.zero_grad()
|
| pred_high, pred_low = model(xb)
|
| loss = (loss_fn(pred_high, yb_high) + loss_fn(pred_low, yb_low)) / 2
|
| loss.backward()
|
| torch.nn.utils.clip_grad_norm_(model.parameters(), 1)
|
| optimizer.step()
|
| train_loss += loss.item()
|
|
|
| train_loss /= len(train_loader)
|
|
|
|
|
| model.eval()
|
| val_loss = 0
|
| with torch.no_grad():
|
| for xb, yb_high, yb_low in val_loader:
|
| xb, yb_high, yb_low = xb.to(DEVICE), yb_high.to(DEVICE), yb_low.to(DEVICE)
|
| pred_high, pred_low = model(xb)
|
| loss = (loss_fn(pred_high, yb_high) + loss_fn(pred_low, yb_low)) / 2
|
| val_loss += loss.item()
|
|
|
| val_loss /= len(val_loader)
|
| print(f"[{symbol}] Epoch {epoch+1} | TrainLoss: {train_loss:.6f} | ValLoss: {val_loss:.6f}")
|
|
|
| scheduler.step()
|
| model_path = os.path.join(f'{CHECKPOINT_DIR}',f'{symbol}_transformer_checkpoint_fold{fold+1}.pth')
|
| early_stopping(val_loss, model, model_path)
|
|
|
| if val_loss < best_val_loss:
|
| best_val_loss = val_loss
|
| best_model_weights = copy.deepcopy(model.state_dict())
|
|
|
| if early_stopping.early_stop:
|
| print("Early stopping triggered")
|
| break
|
|
|
|
|
| if best_model_weights is not None:
|
| torch.save(best_model_weights, os.path.join(f'{SAVE_DIR}',f'{symbol}_best_fold{fold+1}.pth'))
|
| print(f"[{symbol}] ✅ Fold {fold+1} saved with val_loss: {best_val_loss:.6f}")
|
|
|
| model.load_state_dict(best_model_weights)
|
| torch.save({
|
| 'model_state_dict': best_model_weights,
|
| 'scalers': {
|
| 'scaler_price': scaler_price,
|
| 'scaler_tick': scaler_tick,
|
| 'scaler_target_high': scaler_target_high,
|
| 'scaler_target_low': scaler_target_low
|
| }
|
| }, os.path.join(SAVE_DIR, f"{symbol}_fold{fold+1}.pth"))
|
|
|
| except Exception as e:
|
| print(f"[{symbol}] ❌ Error: {e}")
|
|
|