mirror of
https://github.com/Arianhgh/fx-quant-research.git
synced 2026-08-25 08:38:06 +00:00
Add project scaffold with config and dependencies
This commit is contained in:
@@ -0,0 +1,10 @@
|
||||
from tradingbot.models.tree_ensemble import TreeEnsemblePredictor, run_model
|
||||
from tradingbot.models.neural_ensemble import NeuralEnsemblePredictor
|
||||
from tradingbot.models.model_manager import GoldModelManager
|
||||
|
||||
__all__ = [
|
||||
"TreeEnsemblePredictor",
|
||||
"run_model",
|
||||
"NeuralEnsemblePredictor",
|
||||
"GoldModelManager",
|
||||
]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,340 @@
|
||||
"""Neural + tree hybrid ensemble predictor.
|
||||
|
||||
Stacks gradient-boosted trees with a Transformer/BiLSTM network and a Bayesian
|
||||
network (Monte-Carlo dropout) for uncertainty-aware signal filtering.
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.preprocessing import RobustScaler
|
||||
from sklearn.model_selection import TimeSeriesSplit
|
||||
import xgboost as xgb
|
||||
import lightgbm as lgb
|
||||
from catboost import CatBoostClassifier
|
||||
from hmmlearn import hmm
|
||||
import talib
|
||||
import torch
|
||||
import torch.nn as nn
|
||||
import torch.optim as optim
|
||||
from torch.utils.data import Dataset, DataLoader
|
||||
|
||||
|
||||
class TimeSeriesDataset(Dataset):
|
||||
def __init__(self, X, y):
|
||||
self.X = torch.tensor(X, dtype=torch.float32)
|
||||
self.y = torch.tensor(y, dtype=torch.long)
|
||||
|
||||
def __len__(self):
|
||||
return len(self.y)
|
||||
|
||||
def __getitem__(self, idx):
|
||||
return self.X[idx], self.y[idx]
|
||||
|
||||
class TransformerBlock(nn.Module):
|
||||
def __init__(self, d_model, nhead, dropout=0.3):
|
||||
super().__init__()
|
||||
self.attn = nn.MultiheadAttention(d_model, nhead, dropout=dropout, batch_first=True)
|
||||
self.norm1 = nn.LayerNorm(d_model)
|
||||
self.ff = nn.Sequential(
|
||||
nn.Linear(d_model, 64),
|
||||
nn.ReLU(),
|
||||
nn.Dropout(dropout),
|
||||
nn.Linear(64, d_model)
|
||||
)
|
||||
self.norm2 = nn.LayerNorm(d_model)
|
||||
self.dropout = nn.Dropout(dropout)
|
||||
|
||||
def forward(self, x):
|
||||
attn_out, _ = self.attn(x, x, x)
|
||||
x = self.norm1(x + self.dropout(attn_out))
|
||||
ff_out = self.ff(x)
|
||||
x = self.norm2(x + self.dropout(ff_out))
|
||||
return x
|
||||
|
||||
class NeuralEnsemblePredictor:
|
||||
def __init__(self, forecast_period=12, confidence_threshold=0.6, device='cuda' if torch.cuda.is_available() else 'cpu'):
|
||||
self.forecast_period = forecast_period
|
||||
self.confidence_threshold = confidence_threshold
|
||||
self.device = device
|
||||
self.scaler = RobustScaler()
|
||||
self.regime_model = hmm.GaussianHMM(n_components=3, random_state=42)
|
||||
|
||||
# Tree-based models with pre-tuned parameters
|
||||
self.models = {
|
||||
'xgboost': xgb.XGBClassifier(
|
||||
colsample_bytree=0.9821911945239713,
|
||||
learning_rate=0.07305593001592295,
|
||||
max_depth=9,
|
||||
min_child_weight=5,
|
||||
subsample=0.7524259059189972,
|
||||
random_state=42,
|
||||
eval_metric='logloss'
|
||||
),
|
||||
'lightgbm': lgb.LGBMClassifier(
|
||||
feature_fraction=0.9249583953429453,
|
||||
learning_rate=0.025468440525690465,
|
||||
max_depth=7,
|
||||
min_child_samples=41,
|
||||
subsample=0.8092209312217534,
|
||||
random_state=44
|
||||
),
|
||||
'catboost': CatBoostClassifier(
|
||||
colsample_bylevel=0.7762414086064304,
|
||||
depth=4,
|
||||
learning_rate=0.08067885793496102,
|
||||
subsample=0.858134636983408,
|
||||
random_state=45,
|
||||
verbose=0
|
||||
)
|
||||
}
|
||||
|
||||
# PyTorch neural networks
|
||||
self.nn_model = self._build_neural_network().to(self.device)
|
||||
self.bnn_model = self._build_bayesian_network().to(self.device)
|
||||
|
||||
# Non-linear meta-model
|
||||
self.meta_model = lgb.LGBMClassifier(
|
||||
n_estimators=100, max_depth=3, learning_rate=0.05, random_state=46
|
||||
)
|
||||
|
||||
def _build_neural_network(self):
|
||||
"""Transformer-based neural network"""
|
||||
class Net(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.transformer = nn.ModuleList([
|
||||
TransformerBlock(d_model=6, nhead=2, dropout=0.3)
|
||||
for _ in range(2)
|
||||
])
|
||||
self.lstm = nn.LSTM(6, 32, batch_first=True, bidirectional=True)
|
||||
self.attn = nn.Linear(64, 1) # Attention over 64 from bidirectional LSTM
|
||||
self.fc = nn.Sequential(
|
||||
nn.Linear(64, 64),
|
||||
nn.ReLU(),
|
||||
nn.Dropout(0.4),
|
||||
nn.Linear(64, 2)
|
||||
)
|
||||
|
||||
def forward(self, x):
|
||||
for t in self.transformer:
|
||||
x = t(x)
|
||||
x, _ = self.lstm(x) # Shape: (batch, 20, 64)
|
||||
attn_weights = torch.softmax(self.attn(x), dim=1) # Shape: (batch, 20, 1)
|
||||
x = (x * attn_weights).sum(dim=1) # Shape: (batch, 64)
|
||||
x = self.fc(x)
|
||||
return x
|
||||
|
||||
return Net()
|
||||
|
||||
def _build_bayesian_network(self):
|
||||
"""Bayesian neural network with Monte Carlo dropout"""
|
||||
class BNN(nn.Module):
|
||||
def __init__(self):
|
||||
super().__init__()
|
||||
self.flatten = nn.Flatten()
|
||||
self.fc1 = nn.Linear(20 * 6, 64)
|
||||
self.fc2 = nn.Linear(64, 32)
|
||||
self.fc3 = nn.Linear(32, 2)
|
||||
self.dropout = nn.Dropout(0.3)
|
||||
self.relu = nn.ReLU()
|
||||
|
||||
def forward(self, x, training=False):
|
||||
x = self.flatten(x)
|
||||
x = self.relu(self.fc1(x))
|
||||
x = self.dropout(x) if training else x
|
||||
x = self.relu(self.fc2(x))
|
||||
x = self.dropout(x) if training else x
|
||||
x = self.fc3(x)
|
||||
return x
|
||||
|
||||
return BNN()
|
||||
|
||||
def detect_market_regime(self, data):
|
||||
returns = np.log(data['close'] / data['close'].shift(1))
|
||||
volatility = returns.rolling(window=20).std()
|
||||
combined = pd.DataFrame({'returns': returns, 'volatility': volatility}).dropna()
|
||||
self.regime_model.fit(combined.values)
|
||||
regimes = self.regime_model.predict(combined.values)
|
||||
regime_series = pd.Series(index=data.index, dtype='float64')
|
||||
regime_series.iloc[len(data)-len(regimes):] = regimes
|
||||
return regime_series
|
||||
|
||||
def create_advanced_features(self, df):
|
||||
data = df.copy()
|
||||
data['market_regime'] = self.detect_market_regime(data)
|
||||
for period in [21, 55]:
|
||||
data[f'ema_{period}'] = talib.EMA(data['close'], timeperiod=period)
|
||||
data[f'trend_{period}'] = (data[f'ema_{period}'] - data[f'ema_{period}'].shift(period)) / data[f'ema_{period}'].shift(period)
|
||||
data['atr_ratio'] = talib.ATR(data['high'], data['low'], data['close'], 14) / data['close']
|
||||
data['rsi'] = talib.RSI(data['close'], 14)
|
||||
data['volume_ma'] = talib.EMA(data['volume'], timeperiod=20)
|
||||
data['volume_ratio'] = data['volume'] / data['volume_ma']
|
||||
|
||||
returns = data['close'].shift(-self.forecast_period) / data['close'] - 1
|
||||
data['target'] = np.where(returns > 0.005, 1, np.where(returns < -0.005, 0, None))
|
||||
return data.dropna()
|
||||
|
||||
def prepare_features(self, data, for_nn=False):
|
||||
feature_columns = [
|
||||
'market_regime', 'atr_ratio', 'rsi', 'volume_ratio',
|
||||
'trend_21', 'trend_55'
|
||||
]
|
||||
X = data[feature_columns]
|
||||
y = data['target'].astype(int)
|
||||
|
||||
if for_nn:
|
||||
X_3d = np.array([X.iloc[i-20:i].values for i in range(20, len(X))])
|
||||
y_3d = y.iloc[20:].values
|
||||
return X_3d, y_3d
|
||||
return X, y
|
||||
|
||||
def train_nn(self, X_nn, y_nn, model, epochs=50, batch_size=32):
|
||||
dataset = TimeSeriesDataset(X_nn, y_nn)
|
||||
train_size = int(0.8 * len(dataset))
|
||||
val_size = len(dataset) - train_size
|
||||
train_dataset, val_dataset = torch.utils.data.random_split(dataset, [train_size, val_size])
|
||||
train_loader = DataLoader(train_dataset, batch_size=batch_size, shuffle=True)
|
||||
val_loader = DataLoader(val_dataset, batch_size=batch_size)
|
||||
|
||||
criterion = nn.CrossEntropyLoss()
|
||||
optimizer = optim.Adam(model.parameters(), lr=0.0001)
|
||||
|
||||
best_val_loss = float('inf')
|
||||
patience = 15
|
||||
patience_counter = 0
|
||||
|
||||
for epoch in range(epochs):
|
||||
model.train()
|
||||
train_loss = 0
|
||||
for X_batch, y_batch in train_loader:
|
||||
X_batch, y_batch = X_batch.to(self.device), y_batch.to(self.device)
|
||||
optimizer.zero_grad()
|
||||
outputs = model(X_batch)
|
||||
loss = criterion(outputs, y_batch)
|
||||
loss.backward()
|
||||
optimizer.step()
|
||||
train_loss += loss.item()
|
||||
|
||||
model.eval()
|
||||
val_loss = 0
|
||||
with torch.no_grad():
|
||||
for X_batch, y_batch in val_loader:
|
||||
X_batch, y_batch = X_batch.to(self.device), y_batch.to(self.device)
|
||||
outputs = model(X_batch)
|
||||
val_loss += criterion(outputs, y_batch).item()
|
||||
|
||||
train_loss /= len(train_loader)
|
||||
val_loss /= len(val_loader)
|
||||
print(f"Epoch {epoch+1}/{epochs}, Train Loss: {train_loss:.4f}, Val Loss: {val_loss:.4f}")
|
||||
|
||||
if val_loss < best_val_loss:
|
||||
best_val_loss = val_loss
|
||||
patience_counter = 0
|
||||
torch.save(model.state_dict(), 'nn_best.pth')
|
||||
else:
|
||||
patience_counter += 1
|
||||
if patience_counter >= patience:
|
||||
print("Early stopping")
|
||||
break
|
||||
|
||||
model.load_state_dict(torch.load('nn_best.pth'))
|
||||
return model
|
||||
|
||||
def fit(self, train_data):
|
||||
processed_data = self.create_advanced_features(train_data)
|
||||
X, y = self.prepare_features(processed_data)
|
||||
X_nn, y_nn = self.prepare_features(processed_data, for_nn=True)
|
||||
X_scaled = self.scaler.fit_transform(X)
|
||||
X_scaled = pd.DataFrame(X_scaled, columns=X.columns)
|
||||
|
||||
# Train tree-based models (no tuning)
|
||||
tscv = TimeSeriesSplit(n_splits=5)
|
||||
oof_preds = np.zeros((len(X_scaled), len(self.models) + 2))
|
||||
|
||||
for fold, (train_idx, val_idx) in enumerate(tscv.split(X_scaled)):
|
||||
X_train, X_val = X_scaled.iloc[train_idx], X_scaled.iloc[val_idx]
|
||||
y_train, y_val = y.iloc[train_idx], y.iloc[val_idx]
|
||||
|
||||
for i, (name, model) in enumerate(self.models.items()):
|
||||
if name == 'xgboost':
|
||||
model.fit(X_train, y_train, eval_set=[(X_val, y_val)],verbose=False)
|
||||
elif name == 'lightgbm':
|
||||
model.fit(X_train, y_train, eval_set=[(X_val, y_val)])
|
||||
elif name == 'catboost':
|
||||
model.fit(X_train, y_train, eval_set=(X_val, y_val))
|
||||
oof_preds[val_idx, i] = model.predict_proba(X_val)[:, 1]
|
||||
|
||||
# Train neural network
|
||||
nn_idx_shift = len(X_scaled) - len(X_nn)
|
||||
self.nn_model = self.train_nn(X_nn, y_nn, self.nn_model)
|
||||
self.nn_model.eval()
|
||||
with torch.no_grad():
|
||||
nn_preds = torch.softmax(self.nn_model(torch.tensor(X_nn, dtype=torch.float32).to(self.device)), dim=1)[:, 1].cpu().numpy()
|
||||
oof_preds[nn_idx_shift:, len(self.models)] = nn_preds
|
||||
|
||||
# Train Bayesian NN with Monte Carlo dropout
|
||||
self.bnn_model = self.train_nn(X_nn, y_nn, self.bnn_model)
|
||||
self.bnn_model.train() # Enable dropout for MC estimation
|
||||
mc_preds = []
|
||||
with torch.no_grad():
|
||||
for _ in range(10): # 10 Monte Carlo samples
|
||||
preds = torch.softmax(self.bnn_model(torch.tensor(X_nn, dtype=torch.float32).to(self.device), training=True), dim=1)[:, 1].cpu().numpy()
|
||||
mc_preds.append(preds)
|
||||
bnn_mean = np.mean(mc_preds, axis=0)
|
||||
bnn_std = np.std(mc_preds, axis=0)
|
||||
oof_preds[nn_idx_shift:, len(self.models) + 1] = bnn_mean
|
||||
|
||||
# Train meta-model
|
||||
self.meta_model.fit(oof_preds, y, sample_weight=np.exp(np.linspace(-1, 0, len(y))))
|
||||
|
||||
# Final training of tree models
|
||||
for name, model in self.models.items():
|
||||
if name in ['xgboost']:
|
||||
model.fit(X_scaled, y, eval_set=[(X_scaled, y)], verbose=False)
|
||||
elif name in ['lightgbm']:
|
||||
model.fit(X_scaled, y, eval_set=[(X_scaled, y)])
|
||||
else:
|
||||
model.fit(X_scaled, y, eval_set=(X_scaled, y))
|
||||
|
||||
return processed_data
|
||||
|
||||
def predict(self, data):
|
||||
processed_data = self.create_advanced_features(data)
|
||||
X, _ = self.prepare_features(processed_data)
|
||||
X_nn, _ = self.prepare_features(processed_data, for_nn=True)
|
||||
X_scaled = self.scaler.transform(X)
|
||||
X_scaled = pd.DataFrame(X_scaled, columns=X.columns)
|
||||
|
||||
base_preds = np.zeros((len(X_scaled), len(self.models) + 2))
|
||||
for i, (name, model) in enumerate(self.models.items()):
|
||||
base_preds[:, i] = model.predict_proba(X_scaled)[:, 1]
|
||||
|
||||
nn_idx_shift = len(X_scaled) - len(X_nn)
|
||||
self.nn_model.eval()
|
||||
with torch.no_grad():
|
||||
nn_preds = torch.softmax(self.nn_model(torch.tensor(X_nn, dtype=torch.float32).to(self.device)), dim=1)[:, 1].cpu().numpy()
|
||||
base_preds[nn_idx_shift:, len(self.models)] = nn_preds
|
||||
|
||||
# Bayesian NN predictions with uncertainty
|
||||
self.bnn_model.train() # Enable dropout
|
||||
mc_preds = []
|
||||
with torch.no_grad():
|
||||
for _ in range(10):
|
||||
preds = torch.softmax(self.bnn_model(torch.tensor(X_nn, dtype=torch.float32).to(self.device), training=True), dim=1)[:, 1].cpu().numpy()
|
||||
mc_preds.append(preds)
|
||||
bnn_mean = np.mean(mc_preds, axis=0)
|
||||
bnn_std = np.std(mc_preds, axis=0)
|
||||
base_preds[nn_idx_shift:, len(self.models) + 1] = bnn_mean
|
||||
|
||||
# Meta-model prediction
|
||||
meta_proba = self.meta_model.predict_proba(base_preds)
|
||||
|
||||
signals = pd.Series(0, index=processed_data.index)
|
||||
valid_indices = processed_data.index[20:] # Skip first 20 due to lookback
|
||||
meta_proba_valid = meta_proba[nn_idx_shift:] # Align with NN predictions
|
||||
|
||||
long_mask = (meta_proba_valid[:, 1] > self.confidence_threshold) & (bnn_std < 0.2)
|
||||
short_mask = (meta_proba_valid[:, 0] > self.confidence_threshold) & (bnn_std < 0.2)
|
||||
signals.loc[valid_indices[long_mask]] = 1
|
||||
signals.loc[valid_indices[short_mask]] = -1
|
||||
|
||||
return signals
|
||||
@@ -0,0 +1,562 @@
|
||||
"""Stacked tree-ensemble predictor for the 5-minute timeframe.
|
||||
|
||||
A binary (up/down) classifier that stacks five gradient-boosted / bagged tree
|
||||
models with two meta-models and an HMM market-regime filter.
|
||||
"""
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
from sklearn.preprocessing import RobustScaler
|
||||
from sklearn.ensemble import RandomForestClassifier, ExtraTreesClassifier
|
||||
from sklearn.linear_model import LogisticRegression
|
||||
from sklearn.metrics import roc_auc_score
|
||||
from sklearn.model_selection import TimeSeriesSplit
|
||||
import xgboost as xgb
|
||||
import lightgbm as lgb
|
||||
from catboost import CatBoostClassifier
|
||||
from hmmlearn import hmm
|
||||
import talib
|
||||
|
||||
|
||||
class TreeEnsemblePredictor:
|
||||
def __init__(self, forecast_bars=24, confidence_threshold=0.65):
|
||||
"""
|
||||
Optimized gold price prediction model for 5-minute timeframe
|
||||
|
||||
Parameters:
|
||||
forecast_bars (int): Number of future 5-min bars to predict (default=24, which is 2 hours)
|
||||
confidence_threshold (float): Minimum probability threshold for signal generation
|
||||
"""
|
||||
self.forecast_bars = forecast_bars
|
||||
self.confidence_threshold = confidence_threshold
|
||||
self.scaler = RobustScaler()
|
||||
self.regime_model = hmm.GaussianHMM(n_components=3, random_state=42)
|
||||
|
||||
# Tree-based models with parameters optimized for 5-min timeframe
|
||||
self.models = {
|
||||
'xgboost': xgb.XGBClassifier(
|
||||
colsample_bytree=0.85,
|
||||
learning_rate=0.05,
|
||||
max_depth=6, # Reduced depth for faster market dynamics
|
||||
min_child_weight=3,
|
||||
subsample=0.8,
|
||||
n_estimators=100,
|
||||
random_state=42,
|
||||
eval_metric='logloss',
|
||||
use_label_encoder=False
|
||||
),
|
||||
'lightgbm': lgb.LGBMClassifier(
|
||||
feature_fraction=0.9,
|
||||
learning_rate=0.02,
|
||||
max_depth=5,
|
||||
min_child_samples=20, # Smaller sample for 5-min data
|
||||
subsample=0.8,
|
||||
n_estimators=100,
|
||||
random_state=44,
|
||||
boosting_type='dart' # More robust to noise in high-frequency data
|
||||
),
|
||||
'catboost': CatBoostClassifier(
|
||||
depth=4,
|
||||
learning_rate=0.07,
|
||||
subsample=0.85,
|
||||
n_estimators=100,
|
||||
random_state=45,
|
||||
verbose=0
|
||||
),
|
||||
'randomforest': RandomForestClassifier(
|
||||
n_estimators=100,
|
||||
max_depth=5,
|
||||
max_features='sqrt',
|
||||
min_samples_leaf=5, # Captures more granular patterns
|
||||
random_state=46
|
||||
),
|
||||
'extratrees': ExtraTreesClassifier(
|
||||
n_estimators=100,
|
||||
max_depth=5,
|
||||
max_features='sqrt',
|
||||
min_samples_leaf=5,
|
||||
random_state=47
|
||||
)
|
||||
}
|
||||
|
||||
# Primary meta-model
|
||||
self.meta_model = lgb.LGBMClassifier(
|
||||
n_estimators=100,
|
||||
max_depth=3,
|
||||
learning_rate=0.03,
|
||||
random_state=48,
|
||||
boosting_type='gbdt'
|
||||
)
|
||||
|
||||
# Secondary meta-model for consensus validation
|
||||
self.meta_model_backup = LogisticRegression(
|
||||
C=0.1,
|
||||
solver='liblinear',
|
||||
random_state=49
|
||||
)
|
||||
|
||||
# Track feature importance
|
||||
self.feature_importances = {}
|
||||
self.feature_names = []
|
||||
|
||||
def detect_market_regime(self, data):
|
||||
"""
|
||||
Detects market regimes using HMM on returns and volatility
|
||||
Optimized for 5-minute data with shorter lookback windows
|
||||
|
||||
Returns regime classifications (0=low vol, 1=normal, 2=high vol)
|
||||
"""
|
||||
# Calculate returns and volatility
|
||||
returns = np.log(data['close'] / data['close'].shift(1))
|
||||
# Shorter window for 5-minute data (60 periods = 5 hours)
|
||||
volatility = returns.rolling(window=60).std()
|
||||
combined = pd.DataFrame({'returns': returns, 'volatility': volatility}).dropna()
|
||||
|
||||
# Fit HMM model if we have enough data
|
||||
if len(combined) > 100:
|
||||
self.regime_model.fit(combined.values)
|
||||
regimes = self.regime_model.predict(combined.values)
|
||||
regime_series = pd.Series(index=data.index, dtype='float64')
|
||||
regime_series.iloc[len(data)-len(regimes):] = regimes
|
||||
return regime_series
|
||||
else:
|
||||
# Default to moderate regime if not enough data
|
||||
return pd.Series(1, index=data.index)
|
||||
|
||||
def create_advanced_features(self, df):
|
||||
"""
|
||||
Create features optimized for 5-minute gold price prediction
|
||||
|
||||
Features are organized in categories:
|
||||
1. Market regime
|
||||
2. Time-based features
|
||||
3. Price action features
|
||||
4. Volatility indicators
|
||||
5. Momentum indicators
|
||||
6. Volume indicators
|
||||
7. Support/Resistance
|
||||
8. Pattern recognition
|
||||
"""
|
||||
data = df.copy()
|
||||
|
||||
# 1. Market regime detection
|
||||
data['market_regime'] = self.detect_market_regime(data)
|
||||
|
||||
# 2. Time-based features for intraday seasonality
|
||||
data['hour'] = data.index.hour
|
||||
data['minute'] = data.index.minute
|
||||
data['day_of_week'] = data.index.dayofweek
|
||||
# Cyclical encoding of time (circular features)
|
||||
data['hour_sin'] = np.sin(2 * np.pi * data['hour']/24)
|
||||
data['hour_cos'] = np.cos(2 * np.pi * data['hour']/24)
|
||||
|
||||
# 3. Short-term price action features
|
||||
# Moving averages adapted for 5-min timeframe
|
||||
for period in [12, 24, 48, 96, 144]: # 1h, 2h, 4h, 8h, 12h in 5-minute bars
|
||||
# Exponential moving averages
|
||||
data[f'ema_{period}'] = talib.EMA(data['close'], timeperiod=period)
|
||||
|
||||
# Price relative to moving average (normalized distance)
|
||||
data[f'price_to_ema_{period}'] = data['close'] / data[f'ema_{period}'] - 1
|
||||
|
||||
# Trend strength
|
||||
data[f'trend_{period}'] = (data[f'ema_{period}'] - data[f'ema_{period}'].shift(period//4)) / data[f'ema_{period}'].shift(period//4)
|
||||
|
||||
# 4. Volatility indicators
|
||||
# ATR with periods suitable for 5-minute bars
|
||||
for period in [12, 24, 48, 96]: # 1h, 2h, a
|
||||
data[f'atr_{period}'] = talib.ATR(data['high'], data['low'], data['close'], timeperiod=period)
|
||||
data[f'atr_ratio_{period}'] = data[f'atr_{period}'] / data['close']
|
||||
|
||||
# Bollinger Bands - essential for mean-reversion detection
|
||||
for period in [24, 48, 96]:
|
||||
upper, middle, lower = talib.BBANDS(data['close'], timeperiod=period, nbdevup=2, nbdevdn=2)
|
||||
data[f'bb_width_{period}'] = (upper - lower) / middle
|
||||
data[f'bb_position_{period}'] = (data['close'] - lower) / (upper - lower)
|
||||
|
||||
# 5. Momentum indicators
|
||||
# RSI with different lookback periods for 5-min data
|
||||
for period in [12, 24, 48, 96]:
|
||||
data[f'rsi_{period}'] = talib.RSI(data['close'], timeperiod=period)
|
||||
|
||||
# MACD for 5-minute data (faster parameters)
|
||||
macd, macd_signal, macd_hist = talib.MACD(
|
||||
data['close'],
|
||||
fastperiod=6, # Faster for 5-min data
|
||||
slowperiod=19, # Faster for 5-min data
|
||||
signalperiod=5 # Faster for 5-min data
|
||||
)
|
||||
data['macd'] = macd
|
||||
data['macd_signal'] = macd_signal
|
||||
data['macd_hist'] = macd_hist
|
||||
|
||||
# 6. Volume indicators (crucial for 5-minute signals)
|
||||
# Volume relative to moving average
|
||||
for period in [12, 24, 48]:
|
||||
data[f'volume_ma_{period}'] = talib.SMA(data['volume'], timeperiod=period)
|
||||
data[f'volume_ratio_{period}'] = data['volume'] / data[f'volume_ma_{period}']
|
||||
|
||||
# On-balance volume - good for measuring buying/selling pressure
|
||||
data['obv'] = talib.OBV(data['close'], data['volume'])
|
||||
data['obv_ma'] = talib.SMA(data['obv'], timeperiod=24)
|
||||
data['obv_ratio'] = data['obv'] / data['obv_ma']
|
||||
|
||||
# 7. Support/Resistance levels
|
||||
# Pivot points for 5-min (using 96 periods = 8 hours)
|
||||
data['pivot'] = (data['high'].rolling(96).max() + data['low'].rolling(96).min() + data['close'].rolling(96).mean()) / 3
|
||||
data['dist_to_pivot'] = (data['close'] - data['pivot']) / data['close']
|
||||
|
||||
# 8. Candlestick pattern features
|
||||
# Candle size metrics
|
||||
data['candle_range'] = (data['high'] - data['low']) / data['close']
|
||||
data['candle_body'] = abs(data['open'] - data['close']) / data['close']
|
||||
|
||||
# Rate of change - important for 5-min momentum
|
||||
for period in [6, 12, 24]:
|
||||
data[f'roc_{period}'] = talib.ROC(data['close'], timeperiod=period)
|
||||
|
||||
# Target definition for 5-minute timeframe
|
||||
# Using appropriate thresholds for smaller price moves
|
||||
future_return = data['close'].shift(-self.forecast_bars) / data['close'] - 1
|
||||
# Lower threshold for 5-minute bars (approximately 0.1-0.15% move)
|
||||
data['target'] = np.where(future_return > 0.0012, 1, np.where(future_return < -0.0012, 0, None))
|
||||
|
||||
# Drop rows with missing data
|
||||
return data.dropna()
|
||||
|
||||
def prepare_features(self, data):
|
||||
"""
|
||||
Prepare and select optimal features for the model
|
||||
Features are grouped by category for easier selection
|
||||
"""
|
||||
# Most important features for 5-minute gold prediction
|
||||
feature_columns = [
|
||||
# Market regime
|
||||
'market_regime',
|
||||
|
||||
# Time features for intraday patterns
|
||||
'hour_sin', 'hour_cos', 'day_of_week',
|
||||
|
||||
# Price action features
|
||||
'price_to_ema_12', 'price_to_ema_24', 'price_to_ema_48',
|
||||
'trend_24', 'trend_48', 'trend_96',
|
||||
|
||||
# Volatility indicators
|
||||
'atr_ratio_12', 'atr_ratio_24',
|
||||
'bb_width_24', 'bb_width_48',
|
||||
'bb_position_24', 'bb_position_48',
|
||||
|
||||
# Momentum indicators
|
||||
'rsi_12', 'rsi_24', 'rsi_48',
|
||||
'macd', 'macd_hist',
|
||||
|
||||
# Volume indicators
|
||||
'volume_ratio_12', 'volume_ratio_24',
|
||||
'obv_ratio',
|
||||
|
||||
# Support/Resistance
|
||||
'dist_to_pivot',
|
||||
|
||||
# Pattern recognition
|
||||
'candle_range', 'candle_body',
|
||||
'roc_6', 'roc_12'
|
||||
]
|
||||
|
||||
X = data[feature_columns]
|
||||
|
||||
if 'target' in data.columns:
|
||||
y = data['target'].astype(int)
|
||||
return X, y
|
||||
else:
|
||||
return X, None
|
||||
|
||||
def fit(self, train_data):
|
||||
"""Train the ensemble model on historical data"""
|
||||
print("Creating features...")
|
||||
processed_data = self.create_advanced_features(train_data)
|
||||
X, y = self.prepare_features(processed_data)
|
||||
|
||||
# Store feature names for importance tracking
|
||||
self.feature_names = X.columns.tolist()
|
||||
|
||||
# Scale features
|
||||
X_scaled = self.scaler.fit_transform(X)
|
||||
X_scaled = pd.DataFrame(X_scaled, columns=X.columns)
|
||||
|
||||
# Time series cross-validation
|
||||
print("Performing time series cross-validation...")
|
||||
tscv = TimeSeriesSplit(n_splits=5)
|
||||
oof_preds = np.zeros((len(X_scaled), len(self.models)))
|
||||
|
||||
for fold, (train_idx, val_idx) in enumerate(tscv.split(X_scaled)):
|
||||
X_train, X_val = X_scaled.iloc[train_idx], X_scaled.iloc[val_idx]
|
||||
y_train, y_val = y.iloc[train_idx], y.iloc[val_idx]
|
||||
|
||||
for i, (name, model) in enumerate(self.models.items()):
|
||||
print(f"Training {name} for fold {fold+1}/5...")
|
||||
|
||||
if name == 'xgboost':
|
||||
model.fit(X_train, y_train, eval_set=[(X_val, y_val)], verbose=False)
|
||||
elif name == 'lightgbm':
|
||||
model.fit(X_train, y_train, eval_set=[(X_val, y_val)])
|
||||
elif name == 'catboost':
|
||||
model.fit(X_train, y_train, eval_set=(X_val, y_val))
|
||||
else:
|
||||
model.fit(X_train, y_train)
|
||||
|
||||
# Store out-of-fold predictions
|
||||
oof_preds[val_idx, i] = model.predict_proba(X_val)[:, 1]
|
||||
|
||||
# Track feature importance
|
||||
if hasattr(model, 'feature_importances_'):
|
||||
if name not in self.feature_importances:
|
||||
self.feature_importances[name] = np.zeros(len(self.feature_names))
|
||||
self.feature_importances[name] += model.feature_importances_
|
||||
|
||||
# Evaluate base models
|
||||
for i, name in enumerate(self.models.keys()):
|
||||
auc = roc_auc_score(y, oof_preds[:, i])
|
||||
print(f"{name} Out-of-fold AUC: {auc:.4f}")
|
||||
|
||||
# Train meta-models on out-of-fold predictions
|
||||
print("Training meta-models...")
|
||||
# Weight recent data more heavily for financial time series
|
||||
sample_weights = np.exp(np.linspace(-1, 0, len(y)))
|
||||
|
||||
# Train primary meta-model
|
||||
self.meta_model.fit(oof_preds, y, sample_weight=sample_weights)
|
||||
|
||||
# Train backup meta-model for consensus
|
||||
self.meta_model_backup.fit(oof_preds, y, sample_weight=sample_weights)
|
||||
|
||||
# Display feature importance summary
|
||||
self._print_feature_importance()
|
||||
|
||||
# Final training of all models on full dataset
|
||||
print("Final training on complete dataset...")
|
||||
for name, model in self.models.items():
|
||||
if name in ['xgboost']:
|
||||
model.fit(X_scaled, y, eval_set=[(X_scaled, y)], verbose=False)
|
||||
elif name in ['lightgbm']:
|
||||
model.fit(X_scaled, y, eval_set=[(X_scaled, y)])
|
||||
elif name in ['catboost']:
|
||||
model.fit(X_scaled, y, eval_set=(X_scaled, y))
|
||||
else:
|
||||
model.fit(X_scaled, y)
|
||||
|
||||
return processed_data
|
||||
|
||||
def _print_feature_importance(self):
|
||||
"""Display top features by importance for each model"""
|
||||
print("\n=== Feature Importance Analysis ===")
|
||||
for name, importances in self.feature_importances.items():
|
||||
# Normalize importances to percentages
|
||||
importances = importances / np.sum(importances) * 100
|
||||
# Sort by importance
|
||||
sorted_idx = np.argsort(importances)[::-1]
|
||||
print(f"\n{name.upper()} Top 10 Features:")
|
||||
for i in range(min(10, len(sorted_idx))):
|
||||
idx = sorted_idx[i]
|
||||
print(f" {self.feature_names[idx]}: {importances[idx]:.2f}%")
|
||||
|
||||
def predict(self, data):
|
||||
"""
|
||||
Generate trading signals for 5-minute gold price data
|
||||
|
||||
Returns:
|
||||
DataFrame with columns:
|
||||
- signal: Trading signal (-1=short, 0=neutral, 1=long)
|
||||
- strength: Signal strength (0-100%)
|
||||
- proba_up: Probability of price increase
|
||||
- proba_down: Probability of price decrease
|
||||
- model_agreement: Agreement level between meta-models
|
||||
- market_regime: Detected market regime
|
||||
"""
|
||||
processed_data = self.create_advanced_features(data)
|
||||
X, _ = self.prepare_features(processed_data)
|
||||
X_scaled = self.scaler.transform(X)
|
||||
X_scaled = pd.DataFrame(X_scaled, columns=X.columns)
|
||||
|
||||
# Get predictions from base models
|
||||
base_preds = np.zeros((len(X_scaled), len(self.models)))
|
||||
for i, (name, model) in enumerate(self.models.items()):
|
||||
base_preds[:, i] = model.predict_proba(X_scaled)[:, 1]
|
||||
|
||||
# Get predictions from both meta-models
|
||||
meta_proba = self.meta_model.predict_proba(base_preds)
|
||||
backup_proba = self.meta_model_backup.predict_proba(base_preds)
|
||||
|
||||
# Calculate consensus level between meta-models (0-1)
|
||||
models_agreement = 1 - np.abs(meta_proba[:, 1] - backup_proba[:, 1])
|
||||
|
||||
# Initialize signals
|
||||
signals = pd.Series(0, index=processed_data.index)
|
||||
|
||||
# Generate signals with sophisticated filtering
|
||||
# Long signal: high probability of price increase + high model agreement
|
||||
long_mask = (meta_proba[:, 1] > self.confidence_threshold) & (models_agreement > 0.8)
|
||||
|
||||
# Short signal: high probability of price decrease + high model agreement
|
||||
short_mask = (meta_proba[:, 0] > self.confidence_threshold) & (models_agreement > 0.8)
|
||||
|
||||
# Add market regime filter - only take signals in appropriate regimes
|
||||
market_regimes = processed_data['market_regime']
|
||||
|
||||
# Only generate long signals in trending or normal regimes (0 or 1)
|
||||
long_regime_mask = (market_regimes == 0) | (market_regimes == 1)
|
||||
|
||||
# Only generate short signals in trending or high volatility regimes (0 or 2)
|
||||
short_regime_mask = (market_regimes == 0) | (market_regimes == 2)
|
||||
|
||||
# Apply regime filters to signals
|
||||
signals.loc[long_mask & long_regime_mask] = 1
|
||||
signals.loc[short_mask & short_regime_mask] = -1
|
||||
|
||||
# Calculate signal strength (0-100%) based on prediction confidence
|
||||
signal_strength = pd.Series(0.0, index=processed_data.index)
|
||||
signal_strength.loc[long_mask] = (meta_proba[long_mask, 1] - self.confidence_threshold) * (1 / (1 - self.confidence_threshold)) * 100
|
||||
signal_strength.loc[short_mask] = (meta_proba[short_mask, 0] - self.confidence_threshold) * (1 / (1 - self.confidence_threshold)) * 100
|
||||
|
||||
# Risk management: additional signal filters
|
||||
# 1. Minimum signal duration (prevent rapid flipping)
|
||||
min_bars = 3 # Minimum 15 minutes
|
||||
for i in range(min_bars, len(signals)):
|
||||
if signals.iloc[i] != 0 and signals.iloc[i] == -signals.iloc[i-1]:
|
||||
# If signal flips too soon, maintain previous signal
|
||||
if sum(signals.iloc[i-min_bars:i] == signals.iloc[i-1]) >= min_bars-1:
|
||||
signals.iloc[i] = signals.iloc[i-1]
|
||||
|
||||
# 2. Filter out signals during extreme volatility
|
||||
high_vol_mask = processed_data['atr_ratio_24'] > processed_data['atr_ratio_24'].quantile(0.95)
|
||||
signals.loc[high_vol_mask] = 0
|
||||
|
||||
# Combine results into a DataFrame
|
||||
results = pd.DataFrame({
|
||||
'signal': signals,
|
||||
'strength': signal_strength,
|
||||
'proba_up': meta_proba[:, 1],
|
||||
'proba_down': meta_proba[:, 0],
|
||||
'model_agreement': models_agreement,
|
||||
'market_regime': processed_data['market_regime']
|
||||
}, index=processed_data.index)
|
||||
|
||||
return results
|
||||
|
||||
def evaluate_performance(self, test_data):
|
||||
"""
|
||||
Evaluate model performance with trading simulation - corrected version
|
||||
"""
|
||||
results = self.predict(test_data)
|
||||
|
||||
# Calculate forward returns for evaluation period
|
||||
close_prices = test_data['close']
|
||||
forward_returns = close_prices.shift(-self.forecast_bars) / close_prices - 1
|
||||
|
||||
# Apply signals to returns (long = 1x return, short = -1x return)
|
||||
strategy_returns = results['signal'] * forward_returns
|
||||
|
||||
# Properly handle NaN values that might appear from shifts
|
||||
strategy_returns = strategy_returns.dropna()
|
||||
|
||||
# Calculate performance metrics
|
||||
total_return = strategy_returns.sum()
|
||||
|
||||
# Correct annualization factor: 252 trading days, 12 hours per day, 12 bars per hour
|
||||
annualization_factor = np.sqrt(252 * 12 * 12)
|
||||
sharpe_ratio = strategy_returns.mean() / strategy_returns.std() * annualization_factor
|
||||
|
||||
# Correct win rate calculation - account for signal direction
|
||||
wins = ((strategy_returns > 0) & (results['signal'] != 0)).sum()
|
||||
total_trades = (results['signal'] != 0).sum()
|
||||
win_rate = wins / total_trades if total_trades > 0 else 0
|
||||
|
||||
# Correct drawdown calculation
|
||||
cumulative_returns = strategy_returns.cumsum()
|
||||
drawdowns = cumulative_returns - cumulative_returns.cummax()
|
||||
max_drawdown = drawdowns.min()
|
||||
|
||||
# Signal statistics
|
||||
signal_count = (results['signal'] != 0).sum()
|
||||
signal_changes = results['signal'].diff().abs()
|
||||
signal_changes = signal_changes[signal_changes > 0].sum() / 2 # Each change counts twice in diff
|
||||
|
||||
# Correct trading days calculation (typically 5 days a week for forex)
|
||||
# Assuming 12 hours of active trading per day and 12 5-min bars per hour
|
||||
trading_days = len(results) / (12 * 12)
|
||||
avg_signals_per_day = signal_count / trading_days
|
||||
|
||||
# Calculate profit factor
|
||||
profitable_trades = strategy_returns[strategy_returns > 0].sum()
|
||||
losing_trades = abs(strategy_returns[strategy_returns < 0].sum())
|
||||
profit_factor = profitable_trades / losing_trades if losing_trades != 0 else float('inf')
|
||||
|
||||
# Calculate average profit per trade
|
||||
avg_profit_per_trade = total_return / total_trades if total_trades > 0 else 0
|
||||
|
||||
# Compile metrics
|
||||
metrics = {
|
||||
'total_return': total_return,
|
||||
'annualized_return': total_return * (252 / trading_days),
|
||||
'sharpe_ratio': sharpe_ratio,
|
||||
'win_rate': win_rate,
|
||||
'max_drawdown': max_drawdown,
|
||||
'profit_factor': profit_factor,
|
||||
'signal_count': signal_count,
|
||||
'signal_changes': signal_changes,
|
||||
'avg_signals_per_day': avg_signals_per_day,
|
||||
'avg_profit_per_trade': avg_profit_per_trade
|
||||
}
|
||||
|
||||
print("\n=== Performance Evaluation ===")
|
||||
print(f"Total Return: {total_return:.2%}")
|
||||
print(f"Annualized Return: {metrics['annualized_return']:.2%}")
|
||||
print(f"Annualized Sharpe Ratio: {sharpe_ratio:.2f}")
|
||||
print(f"Win Rate: {win_rate:.2%}")
|
||||
print(f"Maximum Drawdown: {max_drawdown:.2%}")
|
||||
print(f"Profit Factor: {profit_factor:.2f}")
|
||||
print(f"Total Signals: {signal_count}")
|
||||
print(f"Signal Changes: {signal_changes}")
|
||||
print(f"Average Signals Per Day: {avg_signals_per_day:.2f}")
|
||||
print(f"Average Profit Per Trade: {avg_profit_per_trade:.4%}")
|
||||
|
||||
return metrics, results, strategy_returns
|
||||
|
||||
# Example usage
|
||||
def run_model(data_path, forecast_bars=24, confidence_threshold=0.65):
|
||||
"""
|
||||
Run the model on input data
|
||||
|
||||
Parameters:
|
||||
data_path: Path to CSV file with OHLCV data
|
||||
forecast_bars: Number of 5-min bars to forecast
|
||||
confidence_threshold: Threshold for signal generation
|
||||
|
||||
Returns:
|
||||
predictor: Trained model
|
||||
metrics: Performance metrics
|
||||
results: Signal results
|
||||
returns: Strategy returns
|
||||
"""
|
||||
# Load and prepare data
|
||||
data = pd.read_csv(data_path)
|
||||
data['timestamp'] = pd.to_datetime(data['timestamp'])
|
||||
data = data.drop_duplicates(subset=['timestamp'])
|
||||
data.set_index('timestamp', inplace=True)
|
||||
|
||||
# Split into train/test
|
||||
#pick the last 40% of data before the last 80% from 40% to 80%
|
||||
train_size = int(len(data) * 0.95)
|
||||
train_data = data[int(len(data) * 0.80):train_size].copy()
|
||||
test_data = data[train_size:].copy()
|
||||
|
||||
# Create and train model
|
||||
predictor = TreeEnsemblePredictor(
|
||||
forecast_bars=forecast_bars,
|
||||
confidence_threshold=confidence_threshold
|
||||
)
|
||||
print(f"Training model with forecast_bars={forecast_bars}, confidence_threshold={confidence_threshold}")
|
||||
predictor.fit(train_data)
|
||||
|
||||
# Evaluate model
|
||||
metrics, results, returns = predictor.evaluate_performance(test_data)
|
||||
|
||||
return predictor, metrics, results, returns
|
||||
Reference in New Issue
Block a user