import torch import torch.nn as nn import torch.nn.functional as F from torch.utils.data import Dataset, DataLoader, Subset from datetime import datetime import pandas as pd #import tensorflow as tf import numpy as np from datetime import datetime, timedelta import joblib,random,os from collections import deque __all__ = [ "PositionalEncoding", "LearnablePositionalEncoding", "TransformerSingleStep", "TransformerMultiStep", "EarlyStopping", "SinglestepDataset", "ReplayBuffer", "CalibratorSingleStep", "CalibratorSingleStepV3", "CalibratorMultiStep", "MultiStepDataset", "TransformerMultistepMonth", "HybridTransformerModelV4", "MultiTimeframeTransformer", "TransformerSingleStepForEA", "ConvGRUTransformerHL", "Seq2SeqAttentionHL", "Seq2SeqAttentionHLWithConv", "HybridMultiBranchMultiStepAttention", "HybridMultiBranchMultiStepAttention", "LRFinderWrapper", "ConvGRUTransformerHLV2", "RobustScalerSimple", "MinMaxScaler" ] #+-------------------------------------------------------------------+ #| Use in Training loop | #+-------------------------------------------------------------------+ class RobustScalerSimple: def __init__(self, quantile_range=(25,75)): self.quantile_range = quantile_range self.median_ = None self.iqr_ = None def fit(self, X): q_min, q_max = self.quantile_range self.median_ = np.median(X, axis=0) q1 = np.percentile(X, q_min, axis=0) q3 = np.percentile(X, q_max, axis=0) self.iqr_ = q3 - q1 # ป้องกันหารด้วย 0 self.iqr_[self.iqr_ == 0] = 1.0 return self def transform(self, X): return (X - self.median_) / self.iqr_ def fit_transform(self, X): self.fit(X) return self.transform(X) def inverse_transform(self, X_scaled): return X_scaled * self.iqr_ + self.median_ class MinMaxScaler: def __init__(self, feature_range=(-1,1)): self.min_val, self.max_val = feature_range self.min_ = None self.max_ = None self.scale_ = None def fit(self, X): self.min_ = X.min(axis=0) self.max_ = X.max(axis=0) self.scale_ = (self.max_val - self.min_val) / (self.max_ - self.min_ + 1e-8) return self def transform(self, X): return (X - self.min_) * self.scale_ + self.min_val def fit_transform(self, X): self.fit(X) return self.transform(X) def inverse_transform(self, X_scaled): return (X_scaled - self.min_val) / self.scale_ + self.min_ class EarlyStopping: def __init__(self, patience=7, delta=0, verbose=False): self.patience = patience self.delta = delta self.verbose = verbose self.counter = 0 self.best_score = None self.early_stop = False self.val_loss_min = np.Inf def __call__(self, val_loss, model, path): score = -val_loss if self.best_score is None: self.best_score = score self.save_checkpoint(val_loss, model, path) elif score < self.best_score + self.delta: self.counter += 1 print(f'EarlyStopping counter: {self.counter} out of {self.patience}') if self.counter >= self.patience: self.early_stop = True else: self.best_score = score self.save_checkpoint(val_loss, model, path) self.counter = 0 def save_checkpoint(self, val_loss, model, path): if self.verbose: print(f'Validation loss decreased ({self.val_loss_min:.6f} --> {val_loss:.6f}). Saving model...') torch.save(model.state_dict(), path) self.val_loss_min = val_loss class PositionalEncoding(nn.Module): def __init__(self, d_model, max_len=5000): super().__init__() pe = torch.zeros(max_len, d_model) pos = torch.arange(0, max_len).unsqueeze(1) div = torch.exp(torch.arange(0, d_model, 2) * -(np.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(pos * div) pe[:, 1::2] = torch.cos(pos * div) self.pe = pe.unsqueeze(0) def forward(self, x): x = x + self.pe[:, :x.size(1)].to(x.device) return x #+-------------------------------------------------------------------+ #| 24082025 Inherit from nn.Module | #| Work with Transformer | #+-------------------------------------------------------------------+ class LearnablePositionalEncoding(nn.Module): def __init__(self, d_model, max_len=10000000): super().__init__() self.pos_embedding = nn.Embedding(max_len, d_model) self._init_weights() def _init_weights(self): # initialization ที่เหมาะสมสำหรับ positional encoding nn.init.uniform_(self.pos_embedding.weight, -0.1, 0.1) def forward(self, x): # x.shape = [batch_size, seq_len, d_model] seq_len = x.size(1) pos = torch.arange(0, seq_len, device=x.device).unsqueeze(0) # [1, seq_len] pos_embed = self.pos_embedding(pos) # [1, seq_len, d_model] return x + pos_embed class TransformerSingleStep(nn.Module): def __init__(self, input_size, d_model=64, nhead=4, num_layers=2, dropout=0.1, max_len=500): super().__init__() self.input_fc = nn.Linear(input_size, d_model) self.tanh = nn.Tanh() self.pos_encoder = LearnablePositionalEncoding(d_model, max_len=max_len) encoder_layer = nn.TransformerEncoderLayer(d_model, nhead, dim_feedforward=128, dropout=dropout, batch_first=True) self.transformer = nn.TransformerEncoder(encoder_layer, num_layers) self.output = nn.Linear(d_model, 2) def forward(self, x): # x.shape = [batch_size, seq_len, input_size] x = self.input_fc(x) x = self.tanh(x) x = self.pos_encoder(x) x = self.transformer(x) # [B, seq_len, d_model] # ใช้เฉพาะ output ของ time step สุดท้าย last_output = x[:, -1, :] # [B, d_model] # output layer แบ่ง high/low out = self.output(last_output) # [B, 2] → col0=high, col1=low pred_high = out[:, 0].unsqueeze(1) # [B,1] pred_low = out[:, 1].unsqueeze(1) # [B,1] return pred_high, pred_low class TransformerMultiStep(nn.Module): def __init__(self, input_size, d_model=64, nhead=4, num_layers=2, dropout=0.1, horizon=5, output_dropout=0.2, max_seq_len=12): super().__init__() self.horizon = horizon self.d_model = d_model # Input projection self.input_fc = nn.Linear(input_size, d_model) self.tanh = nn.Tanh() # Learnable positional encoding self.pos_encoding = nn.Parameter(torch.zeros(1, max_seq_len, d_model)) nn.init.normal_(self.pos_encoding, mean=0.0, std=0.02) # นี้ถูกต้องแล้ว # Transformer with causal masking encoder_layer = nn.TransformerEncoderLayer( d_model, nhead, dim_feedforward=128, dropout=dropout, batch_first=True ) self.transformer = nn.TransformerEncoder(encoder_layer, num_layers) # Output layers self.output_high = nn.Sequential( nn.Linear(d_model, d_model//2), nn.Tanh(), nn.Dropout(output_dropout), nn.Linear(d_model//2, horizon) ) self.output_low = nn.Sequential( nn.Linear(d_model, d_model//2), nn.Tanh(), nn.Dropout(output_dropout), nn.Linear(d_model//2, horizon) ) def forward(self, x): batch_size, seq_len = x.size(0), x.size(1) x = self.input_fc(x) x = self.tanh(x) # Add positional encoding x = x + self.pos_encoding[:, :seq_len, :] # Causal mask mask = torch.triu(torch.ones(seq_len, seq_len) * float('-inf'), diagonal=1).to(x.device) x = self.transformer(x, mask=mask) last_output = x[:, -1, :] high_pred = self.output_high(last_output) low_pred = self.output_low(last_output) return high_pred, low_pred #+-------------------------------------------------------------------+ #| 24082025 | #| Work with Online update | #+-------------------------------------------------------------------+ class ReplayBuffer: def __init__(self, capacity=200): self.capacity = capacity self.buffer = [] def push(self, x, y): if len(self.buffer) >= self.capacity: self.buffer.pop(0) self.buffer.append((x.detach().clone(), y.detach().clone())) def sample(self, batch_size=5): if len(self.buffer) == 0: return None, None sample = random.sample(self.buffer, min(batch_size, len(self.buffer))) xs, ys = zip(*sample) return torch.stack(xs), torch.stack(ys) #+-------------------------------------------------------------------+ #| 24082025 | #| Work with predict process and update predict price near real price| #+-------------------------------------------------------------------+ class CalibratorSingleStep: def __init__(self, method='EMA', alpha=0.3): """ method: 'EMA' หรือ 'linear' alpha: smoothing factor สำหรับ EMA """ self.method = method self.alpha = alpha self.history_pred_high = [] self.history_pred_low = [] self.history_true_high = [] self.history_true_low = [] self.bias_high = 0.0 self.bias_low = 0.0 self.model_high = None self.model_low = None def update_history(self, pred_high, pred_low, true_high, true_low): self.history_pred_high.append(pred_high) self.history_pred_low.append(pred_low) self.history_true_high.append(true_high) self.history_true_low.append(true_low) def refit(self): if self.method == 'linear': if len(self.history_pred_high) >= 2: Xh = np.array(self.history_pred_high).reshape(-1,1) Yh = np.array(self.history_true_high) self.model_high = LinearRegression().fit(Xh, Yh) if len(self.history_pred_low) >= 2: Xl = np.array(self.history_pred_low).reshape(-1,1) Yl = np.array(self.history_true_low) self.model_low = LinearRegression().fit(Xl, Yl) # สำหรับ EMA ไม่ต้อง refit def calibrate(self, pred_high, pred_low): if self.method == 'EMA': ch = pred_high + self.alpha*(self.history_true_high[-1]-pred_high) if self.history_true_high else pred_high cl = pred_low + self.alpha*(self.history_true_low[-1]-pred_low) if self.history_true_low else pred_low ch += self.bias_high cl += self.bias_low elif self.method == 'linear': ch = self.model_high.predict(np.array([[pred_high]]))[0] + self.bias_high if self.model_high else pred_high cl = self.model_low.predict(np.array([[pred_low]]))[0] + self.bias_low if self.model_low else pred_low else: ch, cl = pred_high, pred_low return ch, cl def update_bias(self, pred_high_calib, pred_low_calib, true_high, true_low): # update bias ด้วยค่า error ล่าสุด self.bias_high = true_high - pred_high_calib self.bias_low = true_low - pred_low_calib #+-------------------------------------------------------------------+ #| 24082025 | #| Work with predict process and update predict price near real price| #+-------------------------------------------------------------------+ class CalibratorSingleStepV3: """ Bias calibration สำหรับ Online Update V3 """ def __init__(self, method='EMA', alpha=0.1): self.method = method self.alpha = alpha self.high_history = [] self.low_history = [] self.bias_high = 0.0 self.bias_low = 0.0 def update_history(self, pred_high, pred_low, true_high, true_low): self.high_history.append(true_high - pred_high) self.low_history.append(true_low - pred_low) def refit(self): if self.high_history: if self.method == 'EMA': self.bias_high = self.alpha * self.high_history[-1] + (1 - self.alpha) * self.bias_high self.bias_low = self.alpha * self.low_history[-1] + (1 - self.alpha) * self.bias_low elif self.method == 'mean': self.bias_high = sum(self.high_history)/len(self.high_history) self.bias_low = sum(self.low_history)/len(self.low_history) def calibrate(self, pred_high, pred_low): return pred_high + self.bias_high, pred_low + self.bias_low def update_bias(self, pred_high_calib, pred_low_calib, true_high, true_low): self.update_history(pred_high_calib, pred_low_calib, true_high, true_low) self.refit() #+-------------------------------------------------------------------+ #| 24082025 Inherit from Dataset | #| get Scaled data and create rolling window before use loader | #+-------------------------------------------------------------------+ class SinglestepDataset(Dataset): def __init__(self, X, y_high, y_low, lookback=4): """ Dataset สำหรับ Time Series Forecasting แบบ Single-step (Rolling window) """ self.X = torch.tensor(X, dtype=torch.float32) self.y_high = torch.tensor(y_high, dtype=torch.float32).squeeze(-1) # ลบ dimension เกิน self.y_low = torch.tensor(y_low, dtype=torch.float32).squeeze(-1) self.lookback = lookback # index สุดท้ายที่ยังสร้าง sample ได้ self.max_idx = len(self.X) - lookback def __len__(self): return self.max_idx def __getitem__(self, idx): x_seq = self.X[idx: idx + self.lookback] # (lookback, features) y_h = self.y_high[idx + self.lookback].unsqueeze(0) # shape: (1,) y_l = self.y_low[idx + self.lookback].unsqueeze(0) return x_seq, y_h, y_l #+-------------------------------------------------------------------+ #| 24082025 Inherit from Dataset | #| get Scaled data and create rolling window before use loader | #+-------------------------------------------------------------------+ class MultiStepDataset(Dataset): def __init__(self, X, y_high, y_low, lookback=4, horizon=5): """ X : np.ndarray or torch.Tensor, shape (N, features) y_high : np.ndarray or torch.Tensor, shape (N,) y_low : np.ndarray or torch.Tensor, shape (N,) lookback: จำนวน step ที่ใช้เป็น input horizon : จำนวน step ข้างหน้าที่ต้องการทำนาย """ self.X = torch.tensor(X, dtype=torch.float32) self.y_high = torch.tensor(y_high, dtype=torch.float32) self.y_low = torch.tensor(y_low, dtype=torch.float32) self.lookback = lookback self.horizon = horizon # index สุดท้ายที่ยังสร้าง sample ได้ self.max_idx = len(self.X) - lookback - horizon + 1 def __len__(self): return self.max_idx # Crete rolling window def __getitem__(self, idx): x_seq = self.X[idx: idx + self.lookback] # (lookback, features) y_h_seq = self.y_high[idx + self.lookback : idx + self.lookback + self.horizon].squeeze(-1) # (horizon,) y_l_seq = self.y_low[idx + self.lookback : idx + self.lookback + self.horizon].squeeze(-1) # (horizon,) return x_seq, y_h_seq, y_l_seq class CalibratorMultiStep: """ Calibrator สำหรับ multistep/recursive prediction - เก็บ history ของ high/low - ปรับ bias ให้ step แรก match last_close - สามารถ smooth prediction ด้วย Gaussian filter """ def __init__(self, last_close=None, max_history=50, sigma=1.5): self.last_close = last_close self.history_high = deque(maxlen=max_history) self.history_low = deque(maxlen=max_history) self.sigma = sigma def update_history(self, raw_high, raw_low, calibrated_high=None, calibrated_low=None): """ เก็บ history ของ step ล่าสุด - calibrated_high/low: optional สำหรับเก็บ version ปรับแล้ว """ self.history_high.append(calibrated_high if calibrated_high is not None else raw_high) self.history_low.append(calibrated_low if calibrated_low is not None else raw_low) def refit(self): """ ปรับค่าภายในถ้าจำเป็น (placeholder) - future: สามารถปรับ weight history, scale หรือ smoothing factor """ pass def calibrate(self, raw_high, raw_low): """ ปรับค่า bias ให้ step แรกตรง last_close - สำหรับ step ต่อไปจะ smooth ด้วย history """ h, l = raw_high, raw_low # Step แรกของ fold → match last_close if self.last_close is not None and len(self.history_high) == 0: delta_h = h - self.last_close delta_l = l - self.last_close h -= delta_h l -= delta_l # Smooth ด้วย history (ถ้ามีมากกว่า 2 step) if len(self.history_high) > 1: h_array = np.array(list(self.history_high) + [h]) l_array = np.array(list(self.history_low) + [l]) h_smooth = gaussian_filter1d(h_array, sigma=self.sigma)[-1] l_smooth = gaussian_filter1d(l_array, sigma=self.sigma)[-1] h, l = h_smooth, l_smooth return h, l class TransformerMultistepMonth(nn.Module): def __init__(self, input_size, d_model=64, nhead=4, num_layers=2, dropout=0.1, horizon=4, conv_dropout=0.1, conv_kernel=2, post_trans_dropout=0.1, output_dropout=0.1, dim_feedforward=128): super().__init__() self.horizon = horizon # Causal padding for conv1d self.causal_pad = nn.ConstantPad1d((conv_kernel - 1, 0), 0) self.conv1d = nn.Conv1d(in_channels=input_size, out_channels=d_model, kernel_size=conv_kernel, padding=0) self.tanh = nn.Tanh() self.norm1 = nn.LayerNorm(d_model) self.dropout1 = nn.Dropout(conv_dropout) # Positional encoding self.pos_encoder = LearnablePositionalEncoding(d_model, max_len=200) # Transformer encoder encoder_layer = nn.TransformerEncoderLayer(d_model=d_model, nhead=nhead, dim_feedforward=dim_feedforward, dropout=dropout, batch_first=True) self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) self.post_trans_dropout = nn.Dropout(post_trans_dropout) # Output linear layers (sequence-wise) self.output_dropout_high = nn.Dropout(output_dropout) self.output_dropout_low = nn.Dropout(output_dropout) self.output_high = nn.Linear(d_model, horizon) self.output_low = nn.Linear(d_model, horizon) def forward(self, x): x = x.permute(0, 2, 1) x = self.causal_pad(x) x = self.conv1d(x) x = self.tanh(x) x = x.permute(0, 2, 1) x = self.norm1(x) x = self.dropout1(x) x = self.pos_encoder(x) x = self.transformer(x) x = self.post_trans_dropout(x) # last timestep x_last = x[:, -1, :] # (batch, d_model) out_high = self.output_dropout_high(x_last) out_low = self.output_dropout_low(x_last) return self.output_high(out_high), self.output_low(out_low) class LRFinderWrapper(nn.Module): def __init__(self, model): super().__init__() self.model = model def forward(self, x): out_high, out_low = self.model(x) out = (out_high + out_low) / 2.0 return out.squeeze(-1) # --- Hybrid Transformer + LSTM + Causal CNN (optimized) --- class HybridTransformerModelV4(nn.Module): def __init__(self, input_size, seq_len, d_model=32, nhead=2, num_layers=1, lstm_hidden=32, cnn_out=16, dropout=0.1): super().__init__() # --- CNN Branch --- self.cnn = CausalConv1d(in_channels=input_size, out_channels=cnn_out, kernel_size=3) self.cnn_bn = nn.BatchNorm1d(cnn_out) self.cnn_dropout = nn.Dropout(dropout) # --- LSTM Branch --- self.lstm = nn.LSTM(input_size, lstm_hidden, batch_first=True) self.lstm_dropout = nn.Dropout(dropout) # --- Transformer Branch --- self.input_projection = nn.Linear(input_size, d_model) encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, batch_first=True, dropout=dropout, activation='gelu' ) self.transformer = nn.TransformerEncoder(encoder_layer, num_layers) self.trans_ln = nn.LayerNorm(d_model) # --- Fusion & Output --- fusion_size = cnn_out + lstm_hidden + d_model self.fc1 = nn.Linear(fusion_size, 64) self.fc1_ln = nn.LayerNorm(64) self.fc1_dropout = nn.Dropout(dropout) self.fc2 = nn.Linear(64, 2) # high & low def forward(self, x): # CNN Branch - LeakyReLU cnn_x = x.transpose(1, 2) cnn_x = F.leaky_relu(self.cnn_bn(self.cnn(cnn_x)), 0.01) cnn_x = self.cnn_dropout(torch.mean(cnn_x, dim=2)) # LSTM Branch - Tanh lstm_out, _ = self.lstm(x) lstm_x = self.lstm_dropout(lstm_out[:, -1, :]) # Transformer Branch - GELU + LayerNorm trans_x = self.input_projection(x) trans_x = torch.mean(self.transformer(trans_x), dim=1) trans_x = self.trans_ln(trans_x) # Fusion - SELU fused = torch.cat([cnn_x, lstm_x, trans_x], dim=1) out = F.selu(self.fc1_ln(self.fc1(fused))) out = self.fc1_dropout(out) out = self.fc2(out) return out class MultiTimeframeTransformer(nn.Module): """ Multi-Timeframe Transformer: - 3 branches: Day / Week / Month - Residual connection ในแต่ละ branch + Fusion - Dropout + LayerNorm แทน BatchNorm1d เพื่อ train batch=1 ได้ """ def __init__(self, input_size, seq_len_list=[5,4,3], d_model=32, nhead=2, num_layers=2, dropout=0.1): super().__init__() self.seq_len_day, self.seq_len_week, self.seq_len_month = seq_len_list # Input projection + Dropout self.input_proj_day = nn.Sequential(nn.Linear(input_size, d_model), nn.Dropout(dropout)) self.input_proj_week = nn.Sequential(nn.Linear(input_size, d_model), nn.Dropout(dropout)) self.input_proj_month = nn.Sequential(nn.Linear(input_size, d_model), nn.Dropout(dropout)) # Positional Encoding self.pos_day = LearnablePositionalEncoding(d_model, max_len=self.seq_len_day) self.pos_week = LearnablePositionalEncoding(d_model, max_len=self.seq_len_week) self.pos_month = LearnablePositionalEncoding(d_model, max_len=self.seq_len_month) # Transformer Encoder per branch encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, dropout=dropout, batch_first=True, activation='gelu' ) self.trans_day = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) self.trans_week = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) self.trans_month = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) # LayerNorm per branch self.ln_day = nn.LayerNorm(d_model) self.ln_week = nn.LayerNorm(d_model) self.ln_month = nn.LayerNorm(d_model) # Fusion Layer + LayerNorm + Dropout + Residual fusion_size = d_model * 3 self.ln_fusion = nn.LayerNorm(fusion_size) self.dropout_fusion = nn.Dropout(dropout) self.fc1 = nn.Linear(fusion_size, 64) self.fc1_ln = nn.LayerNorm(64) self.fc2 = nn.Linear(64, 2) def forward_branch(self, x_proj, x_trans, ln): """ x_proj: (B, seq_len, d_model) x_trans: Transformer Encoder ln: LayerNorm """ # Transformer + Residual + LayerNorm + mean pooling res = x_proj out = x_trans(x_proj) out = out + res out = ln(out) out = torch.mean(out, dim=1) return out def forward(self, x_day, x_week, x_month): # Input projection + Positional Encoding day_proj = self.pos_day(self.input_proj_day(x_day)) week_proj = self.pos_week(self.input_proj_week(x_week)) month_proj = self.pos_month(self.input_proj_month(x_month)) # Forward branch with residual day_feat = self.forward_branch(day_proj, self.trans_day, self.ln_day) week_feat = self.forward_branch(week_proj, self.trans_week, self.ln_week) month_feat = self.forward_branch(month_proj, self.trans_month, self.ln_month) # Concatenate branch features fused = torch.cat([day_feat, week_feat, month_feat], dim=1) fused_res = fused # residual fused = self.ln_fusion(fused) fused = self.dropout_fusion(fused) fused = fused + fused_res # fusion residual # Fully connected out = F.selu(self.fc1_ln(self.fc1(fused))) out = self.fc2(out) return out class NormalizationLayer(nn.Module): """ ใช้ normalize feature ด้วย mean/std ของ training data mean/std ต้องเป็น list/array ตามจำนวน feature """ def __init__(self, mean, std): super().__init__() self.register_buffer("mean", torch.tensor(mean, dtype=torch.float32)) self.register_buffer("std", torch.tensor(std, dtype=torch.float32)) def forward(self, x): # x.shape = [batch, seq_len, n_features] return (x - self.mean) / self.std class TransformerSingleStepForEA(nn.Module): def __init__(self, input_size, mean, std, d_model=64, nhead=4, num_layers=2, dropout=0.1, max_len=500): super().__init__() # --- normalization layer --- self.norm_layer = NormalizationLayer(mean, std) # --- original backbone --- self.input_fc = nn.Linear(input_size, d_model) self.tanh = nn.Tanh() self.pos_encoder = LearnablePositionalEncoding(d_model, max_len=max_len) encoder_layer = nn.TransformerEncoderLayer( d_model=d_model, nhead=nhead, dim_feedforward=128, dropout=dropout, batch_first=True ) self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) self.global_pool = nn.AdaptiveAvgPool1d(1) self.output = nn.Linear(d_model, 2) def forward(self, x): # x.shape = [batch_size, seq_len, input_size] x = self.norm_layer(x) # <-- normalize raw input x = self.input_fc(x) x = self.tanh(x) x = self.pos_encoder(x) x = self.transformer(x) x = x.permute(0, 2, 1) x = self.global_pool(x).squeeze(2) return self.output(x).unsqueeze(1) # [B, 1, 2] class PositionEncodeLearnable(nn.Module): def __init__(self, seq_len, d_model): super().__init__() self.pos_embedding = nn.Parameter(torch.randn(1, seq_len, d_model)) def forward(self, x): return x + self.pos_embedding class ConvGRUTransformerHL(nn.Module): def __init__(self, input_dim, # features dimension conv_channels=32, # Conv1 output gru_hidden=64, # GRU Neturon nhead=4, # num_head num_encoder_layers=1, dim_feedforward=128, seq_len=3, # time step kernel_size=3, dropout=0.1, output_steps=1 ): super().__init__() self.seq_len = seq_len self.kernel_size = kernel_size self.output_steps = output_steps # --- Conv1D Layer (Causal) --- self.conv1 = nn.Conv1d( in_channels=input_dim, out_channels=conv_channels, kernel_size=self.kernel_size, dilation=1, # fixed padding=0 # causal padding ใส่เอง ) self.conv_bn = nn.BatchNorm1d(conv_channels) # --- GRU Layer --- self.gru = nn.GRU( input_size=conv_channels, hidden_size=gru_hidden, batch_first=True ) # --- Learnable Positional Encoding --- self.pos_encoder = PositionEncodeLearnable(seq_len, gru_hidden) # --- Transformer Encoder --- encoder_layer = nn.TransformerEncoderLayer( d_model=gru_hidden, nhead=nhead, dim_feedforward=dim_feedforward, dropout=dropout, batch_first=True, activation="gelu" # fixed GELU ) self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_encoder_layers) # --- Output Head (High / Low) --- self.fc_high = nn.Linear(gru_hidden, output_steps) self.fc_low = nn.Linear(gru_hidden, output_steps) def forward(self, x): # x: (batch, seq_len, features) batch_size, seq_len, features = x.size() # --- Conv1D --- x_conv = x.permute(0, 2, 1) # (batch, features, seq_len) # Causal padding (dilation=1) pad = self.kernel_size - 1 x_conv = F.pad(x_conv, (pad, 0)) # pad left only x_conv = self.conv1(x_conv) # (batch, conv_channels, L_out) x_conv = self.conv_bn(x_conv) x_conv = torch.tanh(x_conv) # keep tanh here # Slice ให้ตรง seq_len x_conv = x_conv[:, :, -seq_len:] x_conv = x_conv.permute(0, 2, 1) # (batch, seq_len, conv_channels) # --- GRU --- gru_out, _ = self.gru(x_conv) # (batch, seq_len, gru_hidden) # Align sequence length if gru_out.size(1) != self.seq_len: current_seq_len = gru_out.size(1) if current_seq_len < self.seq_len: pad_size = self.seq_len - current_seq_len gru_out = F.pad(gru_out, (0, 0, 0, pad_size)) else: gru_out = gru_out[:, :self.seq_len, :] # --- Positional Encoding --- gru_out = self.pos_encoder(gru_out) # --- Transformer Encoder --- trans_out = self.transformer(gru_out) # (batch, seq_len, gru_hidden) # --- Output (last steps) --- last_steps = trans_out[:, -self.output_steps:, :] # (batch, output_steps, hidden) high_out = self.fc_high(last_steps) # (batch, output_steps, 1) low_out = self.fc_low(last_steps) # (batch, output_steps, 1) return high_out.squeeze(-1), low_out.squeeze(-1) class ConvGRUTransformerHLV2(nn.Module): def __init__(self, input_dim, conv_channels=32, gru_hidden=64, nhead=4, num_encoder_layers=1, dim_feedforward=128, seq_len=3, kernel_size=3, dropout=0.1, output_steps=1): super().__init__() self.seq_len = seq_len self.kernel_size = kernel_size self.output_steps = output_steps # --- Conv1D Layer (Causal) --- self.conv1 = nn.Conv1d( in_channels=input_dim, out_channels=conv_channels, kernel_size=self.kernel_size, dilation=1, padding=0 ) self.conv_bn = nn.BatchNorm1d(conv_channels) # --- GRU Layer --- self.gru = nn.GRU( input_size=conv_channels, hidden_size=gru_hidden, batch_first=True ) # --- Learnable Positional Encoding --- self.pos_encoder = PositionEncodeLearnable(seq_len, gru_hidden) # --- Transformer Encoder --- encoder_layer = nn.TransformerEncoderLayer( d_model=gru_hidden, nhead=nhead, dim_feedforward=dim_feedforward, dropout=dropout, batch_first=True, activation="gelu" ) self.transformer = nn.TransformerEncoder(encoder_layer, num_layers=num_encoder_layers) # --- Output Head (High / Low) --- self.fc_high = nn.Linear(gru_hidden, output_steps) self.fc_low = nn.Linear(gru_hidden, output_steps) # --- Learnable bias for high/low --- self.bias_high = nn.Parameter(torch.zeros(1)) self.bias_low = nn.Parameter(torch.zeros(1)) def forward(self, x): # x: (batch, seq_len, features) batch_size, seq_len, features = x.size() # --- Conv1D --- x_conv = x.permute(0, 2, 1) # (batch, features, seq_len) pad = self.kernel_size - 1 x_conv = F.pad(x_conv, (pad, 0)) # causal padding x_conv = self.conv1(x_conv) x_conv = self.conv_bn(x_conv) x_conv = torch.tanh(x_conv) x_conv = x_conv[:, :, -seq_len:] x_conv = x_conv.permute(0, 2, 1) # --- GRU --- gru_out, _ = self.gru(x_conv) # Align sequence length if gru_out.size(1) != self.seq_len: current_seq_len = gru_out.size(1) if current_seq_len < self.seq_len: pad_size = self.seq_len - current_seq_len gru_out = F.pad(gru_out, (0, 0, 0, pad_size)) else: gru_out = gru_out[:, :self.seq_len, :] # --- Positional Encoding --- gru_out = self.pos_encoder(gru_out) # --- Transformer Encoder --- trans_out = self.transformer(gru_out) # --- Output (last steps) --- last_steps = trans_out[:, -self.output_steps:, :] high_out = self.fc_high(last_steps) + self.bias_high low_out = self.fc_low(last_steps) + self.bias_low return high_out.squeeze(-1), low_out.squeeze(-1) # ---------------------------- # Utility: ensure batch dim # ---------------------------- def ensure_batch_dim(x): if x.dim() == 2: x = x.unsqueeze(0) return x # ---------------------------- # Model (Seq2SeqAttentionHL) - เอาไว้ครั้งเดียว # ---------------------------- class Seq2SeqAttentionHL(torch.nn.Module): def __init__(self, input_size, hidden_size, num_layers=1, dropout=0.2, device='cpu'): super().__init__() self.hidden_size = hidden_size self.num_layers = num_layers self.device = device self.encoder = torch.nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=0) self.decoder = torch.nn.LSTM(2, hidden_size, num_layers, batch_first=True, dropout=0) self.fc_high = torch.nn.Linear(hidden_size, 1) self.fc_low = torch.nn.Linear(hidden_size, 1) self.dropout = torch.nn.Dropout(dropout) self.attention = torch.nn.Linear(hidden_size * 2, 1) def forward(self, src, horizon=4, tgt=None, teacher_forcing_ratio=0.5): src = ensure_batch_dim(src) batch_size, seq_len, _ = src.shape # Encoder encoder_outputs, (hidden, cell) = self.encoder(src) encoder_outputs = self.dropout(encoder_outputs) # Decoder initialization decoder_input = src[:, -1, :2].unsqueeze(1) out_high_seq = torch.zeros(batch_size, horizon, 1, device=self.device) out_low_seq = torch.zeros(batch_size, horizon, 1, device=self.device) for t in range(horizon): # Decoder step decoder_output, (hidden, cell) = self.decoder(decoder_input, (hidden, cell)) # Attention mechanism decoder_output_expanded = decoder_output.expand(-1, seq_len, -1) attention_scores = torch.tanh(self.attention( torch.cat([decoder_output_expanded, encoder_outputs], dim=-1) )) attention_weights = torch.softmax(attention_scores, dim=1) context = torch.sum(attention_weights * encoder_outputs, dim=1, keepdim=True) # Combine context and decoder output combined = decoder_output + context # Output projections high = torch.tanh(self.fc_high(combined)) low = torch.tanh(self.fc_low(combined)) out_high_seq[:, t:t+1] = high out_low_seq[:, t:t+1] = low # Teacher forcing or use own predictions if tgt is not None and torch.rand(1) < teacher_forcing_ratio: decoder_input = tgt[:, t:t+1, :2] else: decoder_input = torch.cat([high, low], dim=-1) return out_high_seq, out_low_seq class CausalConvBlockSafe(nn.Module): def __init__(self, input_size, dropout=0.1): super().__init__() self.kernel_size1 = 4 self.kernel_size2 = 4 self.conv1 = nn.Sequential( nn.ConstantPad1d((self.kernel_size1-1, 0), 0), nn.Conv1d(input_size, 128, kernel_size=self.kernel_size1), nn.BatchNorm1d(128), nn.Tanh(), # <-- เปลี่ยนจาก LeakyReLU เป็น tanh nn.Dropout(dropout) ) self.conv2 = nn.Sequential( nn.ConstantPad1d((self.kernel_size2-1, 0), 0), nn.Conv1d(128, 64, kernel_size=self.kernel_size2), nn.BatchNorm1d(64), nn.Tanh(), # <-- เปลี่ยนจาก LeakyReLU เป็น tanh nn.Dropout(dropout) ) self.layernorm = nn.LayerNorm(64) # LayerNorm หลัง conv block def forward(self, x): conv_out = self.conv1(x) conv_out = self.conv2(conv_out) seq_len_in = x.size(2) seq_len_out = conv_out.size(2) if seq_len_out < seq_len_in: raise ValueError(f"Conv output shorter than input! {seq_len_out} < {seq_len_in}") conv_out = conv_out[:, :, -seq_len_in:] conv_out = conv_out.transpose(1,2) conv_out = self.layernorm(conv_out) return conv_out.transpose(1,2) class Seq2SeqAttentionHLWithConv(nn.Module): def __init__(self, input_size, hidden_size, num_layers=2, dropout=0.2, device='cpu'): super().__init__() self.hidden_size = hidden_size self.device = device # --- Causal Conv block --- self.conv_block = CausalConvBlockSafe(input_size, dropout=dropout) # --- LSTM layers --- self.encoder = nn.LSTM(64, hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout if num_layers > 1 else 0.0) self.decoder = nn.LSTM(2, hidden_size, num_layers=num_layers, batch_first=True, dropout=dropout if num_layers > 1 else 0.0) # --- LayerNorm for LSTM outputs --- self.layernorm_enc = nn.LayerNorm(hidden_size) self.layernorm_dec = nn.LayerNorm(hidden_size) # --- Attention mechanism --- self.attention = nn.MultiheadAttention(hidden_size, num_heads=4, dropout=dropout, batch_first=True) self.layernorm_attn = nn.LayerNorm(hidden_size) # --- Output layers --- self.fc_high = nn.Sequential( nn.Linear(hidden_size, 32), nn.Tanh(), # <-- เปลี่ยนจาก ReLU เป็น tanh nn.Dropout(dropout), nn.Linear(32, 1), nn.Tanh() ) self.fc_low = nn.Sequential( nn.Linear(hidden_size, 32), nn.Tanh(), # <-- เปลี่ยนจาก ReLU เป็น tanh nn.Dropout(dropout), nn.Linear(32, 1), nn.Tanh() ) def forward(self, src, horizon=4, tgt=None, teacher_forcing_ratio=1): batch_size, seq_len, _ = src.shape # --- Conv block --- src_conv = src.transpose(1, 2) # (B, features, seq_len) conv_out = self.conv_block(src_conv) # (B, features, seq_len) conv_out = conv_out.transpose(1, 2) # (B, seq_len, features) # --- Encoder --- encoder_outputs, (hidden, cell) = self.encoder(conv_out) encoder_outputs = self.layernorm_enc(encoder_outputs) # --- Decoder --- decoder_input = src[:, -1, :2].unsqueeze(1) # initial input outputs_high, outputs_low = [], [] for t in range(horizon): decoder_out, (hidden, cell) = self.decoder(decoder_input, (hidden, cell)) decoder_out = self.layernorm_dec(decoder_out) # Multi-head attention with residual + LayerNorm attn_output, _ = self.attention(decoder_out, encoder_outputs, encoder_outputs) attn_output = self.layernorm_attn(attn_output + decoder_out) # Predict high = self.fc_high(attn_output) low = self.fc_low(attn_output) outputs_high.append(high) outputs_low.append(low) # Next input if tgt is not None and torch.rand(1) < teacher_forcing_ratio: decoder_input = tgt[:, t:t+1, :2] else: decoder_input = torch.cat([high, low], dim=-1) return torch.cat(outputs_high, dim=1), torch.cat(outputs_low, dim=1) # --- Causal Conv1D --- class CausalConv1d(nn.Conv1d): def __init__(self, in_channels, out_channels, kernel_size, **kwargs): super().__init__(in_channels, out_channels, kernel_size, **kwargs) self.left_padding = kernel_size - 1 def forward(self, x): x = F.pad(x, (self.left_padding, 0)) return super().forward(x) # # ---------------------------- # # Causal Conv1d # # ---------------------------- class CausalConv1d(nn.Module): def __init__(self, in_channels, out_channels, kernel_size, dropout=0.1): super().__init__() self.padding = kernel_size - 1 # causal padding self.conv = nn.Conv1d(in_channels, out_channels, kernel_size) self.dropout = nn.Dropout(dropout) self.ln = nn.LayerNorm(out_channels) def forward(self, x): # x: [batch, seq_len, channels] x = x.permute(0, 2, 1) # -> [batch, channels, seq_len] x = F.pad(x, (self.padding, 0)) # causal padding x = self.conv(x) # -> [batch, out_channels, seq_len] x = x.permute(0, 2, 1) # -> [batch, seq_len, out_channels] x = self.dropout(x) x = self.ln(x) return torch.tanh(x) # # ---------------------------- # # Seq2Seq Branch # # ---------------------------- class Seq2SeqBranch(nn.Module): def __init__(self, in_channels, cnn_channels=32, lstm_hidden=64, kernel_size=3, dropout=0.1): super().__init__() self.cnn = CausalConv1d(in_channels, cnn_channels, kernel_size, dropout) self.lstm = nn.LSTM(cnn_channels, lstm_hidden, batch_first=True) self.pos_embed = nn.Embedding(1000, cnn_channels) # positional embedding def forward(self, x): x = self.cnn(x) # [batch, seq_len, cnn_channels] batch_size, seq_len, _ = x.shape positions = torch.arange(seq_len, device=x.device).unsqueeze(0).expand(batch_size, -1) x = x + self.pos_embed(positions) output, _ = self.lstm(x) return output # [batch, seq_len, lstm_hidden] # ---------------------------- # Hybrid Multi-Branch Multi-Step Attention # ---------------------------- class HybridMultiBranchMultiStepAttention(nn.Module): def __init__(self, horizon=4, cnn_channels=32, lstm_hidden=64, num_heads=4, dropout=0.1): super().__init__() self.horizon = horizon self.lstm_hidden = lstm_hidden # Branches: OHLC, ADX, ATR, Tick self.branches = nn.ModuleList([ Seq2SeqBranch(4, cnn_channels, lstm_hidden, dropout=dropout), # OHLC Seq2SeqBranch(1, cnn_channels, lstm_hidden, dropout=dropout), # ADX Seq2SeqBranch(1, cnn_channels, lstm_hidden, dropout=dropout), # ATR Seq2SeqBranch(1, cnn_channels, lstm_hidden, dropout=dropout), # Tick ]) # Multi-head attention across features self.multihead = nn.MultiheadAttention( embed_dim=lstm_hidden, num_heads=num_heads, batch_first=True, dropout=dropout ) self.final_ln = nn.LayerNorm(lstm_hidden) # Decoder LSTM สำหรับ sequence-to-sequence prediction self.decoder_lstm = nn.LSTM(lstm_hidden, lstm_hidden, batch_first=True) # TimeDistributed FC layers สำหรับ multi-step prediction self.fc_high = nn.Linear(lstm_hidden, 1) self.fc_low = nn.Linear(lstm_hidden, 1) def forward(self, x): # x shape: [batch, seq_len, 7] -> split features head_inputs = [ x[:, :, 0:4], # OHLC x[:, :, 4:5], # ADX x[:, :, 5:6], # ATR x[:, :, 6:7] # Tick ] # Process each branch branch_outs = [branch(h) for branch, h in zip(self.branches, head_inputs)] # Stack along new dimension for features: [batch, seq_len, hidden, num_branches] branch_stack = torch.stack(branch_outs, dim=-1) # Merge branches (mean across branch dimension) branch_merged = branch_stack.mean(dim=-1) # [batch, seq_len, hidden] # Multihead attention across time steps attn_out, _ = self.multihead(branch_merged, branch_merged, branch_merged) # Context: hidden state สุดท้าย context = attn_out[:, -1:, :] # [batch, 1, hidden] # Repeat context สำหรับ horizon steps decoder_input = context.repeat(1, self.horizon, 1) # [batch, horizon, hidden] # Decoder LSTM decoder_out, _ = self.decoder_lstm(decoder_input) # [batch, horizon, hidden] decoder_out = self.final_ln(decoder_out) # --- TimeDistributed FC --- # reshape [batch*horizon, hidden] เพื่อให้ Linear ทำงานทีละ timestep batch, horizon, hidden = decoder_out.shape decoder_flat = decoder_out.reshape(batch*horizon, hidden) out_high_flat = self.fc_high(decoder_flat) out_low_flat = self.fc_low(decoder_flat) # reshape กลับ [batch, horizon] out_high = out_high_flat.reshape(batch, horizon) out_low = out_low_flat.reshape(batch, horizon) return out_high, out_low