Files
zhutoutoutousan 98a87a69ca Update
2026-02-13 08:03:25 +01:00

338 lines
12 KiB
Python

"""
ONNX Model Training Script for RSI Divergence Classification
Trains a neural network to identify genuine RSI divergences and exports to ONNX format.
"""
import argparse
import os
import sys
import numpy as np
import pandas as pd
import tensorflow as tf
from tensorflow import keras
from tensorflow.keras import layers
from sklearn.preprocessing import MinMaxScaler, LabelEncoder
from sklearn.model_selection import train_test_split
from sklearn.metrics import classification_report, confusion_matrix
import tf2onnx
import onnx
import pickle
from tqdm import tqdm
class RSIDivergenceTrainer:
"""
Trainer class for creating ONNX models to classify RSI divergences.
"""
def __init__(self, lookback: int = 60, num_classes: int = 5):
"""
Initialize the trainer.
Args:
lookback: Number of bars to look back for prediction
num_classes: Number of divergence classes (5: NONE + 4 divergence types)
"""
self.lookback = lookback
self.num_classes = num_classes
self.scaler = MinMaxScaler()
self.label_encoder = LabelEncoder()
self.model = None
def load_data(self, data_path: str) -> tuple:
"""
Load labeled data from CSV file.
Args:
data_path: Path to labeled CSV file
Returns:
Tuple of (X, y) where X is features and y is labels
"""
print(f"Loading data from {data_path}...")
df = pd.read_csv(data_path, index_col=0, parse_dates=True)
# Exclude label columns from features
exclude_cols = ['divergence_type', 'divergence_confidence', 'divergence_strength']
feature_cols = [col for col in df.columns if col not in exclude_cols]
# Remove any remaining non-numeric columns
feature_cols = [col for col in feature_cols if df[col].dtype in [np.float64, np.int64, np.float32, np.int32]]
print(f"Using {len(feature_cols)} features: {feature_cols[:10]}...")
# Prepare sequences
X, y = [], []
for i in range(self.lookback, len(df)):
# Get feature sequence
X.append(df[feature_cols].iloc[i - self.lookback:i].values)
# Get label (divergence type at current bar)
y.append(int(df['divergence_type'].iloc[i]))
X = np.array(X)
y = np.array(y)
print(f"Created {len(X)} sequences")
print(f"Label distribution: {np.bincount(y)}")
return X, y, feature_cols
def prepare_data(self, X: np.ndarray, y: np.ndarray) -> tuple:
"""
Prepare and scale data for training.
Args:
X: Feature sequences
y: Labels
Returns:
Tuple of (X_scaled, y_encoded, X_train, X_test, y_train, y_test)
"""
# Scale features
print("Scaling features...")
original_shape = X.shape
X_reshaped = X.reshape(-1, X.shape[-1])
X_scaled = self.scaler.fit_transform(X_reshaped)
X_scaled = X_scaled.reshape(original_shape)
# Encode labels (already integers, but ensure they're 0-4)
y_encoded = y.astype(int)
# Split data (no shuffle to preserve temporal order)
X_train, X_test, y_train, y_test = train_test_split(
X_scaled, y_encoded, test_size=0.2, shuffle=False
)
print(f"Training samples: {len(X_train)}")
print(f"Test samples: {len(X_test)}")
return X_scaled, y_encoded, X_train, X_test, y_train, y_test
def build_model(self, input_shape: tuple) -> keras.Model:
"""
Build the neural network model for classification.
Args:
input_shape: Shape of input data (lookback, features)
Returns:
Compiled Keras model
"""
model = keras.Sequential([
# LSTM layers for sequence learning
layers.LSTM(128, return_sequences=True, input_shape=input_shape),
layers.Dropout(0.3),
layers.LSTM(64, return_sequences=True),
layers.Dropout(0.3),
layers.LSTM(32),
layers.Dropout(0.3),
# Dense layers for classification
layers.Dense(64, activation='relu'),
layers.Dropout(0.2),
layers.Dense(32, activation='relu'),
layers.Dropout(0.2),
layers.Dense(self.num_classes, activation='softmax') # Multi-class classification
])
model.compile(
optimizer=keras.optimizers.Adam(learning_rate=0.001),
loss='sparse_categorical_crossentropy',
metrics=['accuracy']
)
return model
def train(self, X_train: np.ndarray, y_train: np.ndarray,
X_test: np.ndarray, y_test: np.ndarray,
epochs: int = 50, batch_size: int = 32, verbose: int = 1):
"""
Train the model.
Args:
X_train: Training features
y_train: Training labels
X_test: Test features
y_test: Test labels
epochs: Number of training epochs
batch_size: Batch size for training
verbose: Verbosity level
"""
# Build model
self.model = self.build_model((X_train.shape[1], X_train.shape[2]))
print("\nModel architecture:")
self.model.summary()
# Handle class imbalance with class weights
from sklearn.utils.class_weight import compute_class_weight
class_weights = compute_class_weight(
'balanced',
classes=np.unique(y_train),
y=y_train
)
class_weight_dict = {i: weight for i, weight in enumerate(class_weights)}
print(f"\nClass weights: {class_weight_dict}")
# Train model
print("\nTraining model...")
history = self.model.fit(
X_train, y_train,
batch_size=batch_size,
epochs=epochs,
validation_data=(X_test, y_test),
verbose=verbose,
class_weight=class_weight_dict,
callbacks=[
keras.callbacks.EarlyStopping(
monitor='val_loss',
patience=15,
restore_best_weights=True,
verbose=1
),
keras.callbacks.ReduceLROnPlateau(
monitor='val_loss',
factor=0.5,
patience=5,
min_lr=0.0001,
verbose=1
)
]
)
# Evaluate
train_loss, train_acc = self.model.evaluate(X_train, y_train, verbose=0)
test_loss, test_acc = self.model.evaluate(X_test, y_test, verbose=0)
print(f"\nTraining - Loss: {train_loss:.4f}, Accuracy: {train_acc:.4f}")
print(f"Test - Loss: {test_loss:.4f}, Accuracy: {test_acc:.4f}")
# Classification report
y_pred = self.model.predict(X_test, verbose=0)
y_pred_classes = np.argmax(y_pred, axis=1)
print("\nClassification Report:")
print(classification_report(y_test, y_pred_classes,
target_names=['NONE', 'REGULAR_BULLISH', 'REGULAR_BEARISH',
'HIDDEN_BULLISH', 'HIDDEN_BEARISH']))
return history
def export_to_onnx(self, output_path: str, num_features: int):
"""
Export the trained model to ONNX format.
Args:
output_path: Path to save ONNX model
num_features: Number of input features
"""
if self.model is None:
raise ValueError("Model must be trained before exporting")
print(f"\nExporting model to ONNX format: {output_path}")
# Create functional model from Sequential
input_layer = keras.Input(shape=(self.lookback, num_features), name="input")
x = input_layer
# Rebuild model as functional
for layer in self.model.layers:
x = layer(x)
functional_model = keras.Model(inputs=input_layer, outputs=x)
# Convert to ONNX
spec = (tf.TensorSpec((None, self.lookback, num_features), tf.float32, name="input"),)
try:
onnx_model_proto, _ = tf2onnx.convert.from_keras(
functional_model,
input_signature=spec,
opset=13
)
onnx.save_model(onnx_model_proto, output_path)
print(f"ONNX model saved to: {output_path}")
# Verify ONNX model
onnx_model = onnx.load(output_path)
onnx.checker.check_model(onnx_model)
print("ONNX model validation passed")
except Exception as e:
raise RuntimeError(f"Failed to export ONNX model: {str(e)}")
def save_scaler(self, output_path: str):
"""Save the scaler for consistent normalization."""
with open(output_path, 'wb') as f:
pickle.dump(self.scaler, f)
print(f"Scaler saved to: {output_path}")
def main():
"""Main function."""
parser = argparse.ArgumentParser(description='Train ONNX model for RSI divergence classification')
parser.add_argument('--data', type=str, required=True,
help='Path to labeled CSV data file')
parser.add_argument('--lookback', type=int, default=60,
help='Number of bars to look back')
parser.add_argument('--epochs', type=int, default=50, help='Training epochs')
parser.add_argument('--batch-size', type=int, default=32, help='Batch size')
parser.add_argument('--output', type=str, default='models',
help='Output directory for ONNX model')
args = parser.parse_args()
# Create output directory
os.makedirs(args.output, exist_ok=True)
# Create trainer
trainer = RSIDivergenceTrainer(lookback=args.lookback)
try:
# Load data
X, y, feature_cols = trainer.load_data(args.data)
# Prepare data
X_scaled, y_encoded, X_train, X_test, y_train, y_test = trainer.prepare_data(X, y)
# Train model
trainer.train(X_train, y_train, X_test, y_test,
epochs=args.epochs, batch_size=args.batch_size)
# Export to ONNX
num_features = len(feature_cols)
model_name = "BTCUSD_H1_rsi_divergence_model.onnx"
output_path = os.path.join(args.output, model_name)
trainer.export_to_onnx(output_path, num_features)
# Save scaler
scaler_name = "BTCUSD_H1_rsi_divergence_scaler.pkl"
scaler_path = os.path.join(args.output, scaler_name)
trainer.save_scaler(scaler_path)
# Save feature list
features_name = "BTCUSD_H1_rsi_divergence_features.pkl"
features_path = os.path.join(args.output, features_name)
with open(features_path, 'wb') as f:
pickle.dump(feature_cols, f)
print(f"Feature list saved to: {features_path}")
print(f"\n{'='*60}")
print("Training completed successfully!")
print(f"ONNX model saved to: {output_path}")
print(f"{'='*60}\n")
except Exception as e:
print(f"\nError: {e}")
import traceback
traceback.print_exc()
sys.exit(1)
if __name__ == '__main__':
main()