DeepFin / agents /scripts /train_rnn_model.py
amos-fernandes's picture
Upload 151 files
b3a7985 verified
# train_rnn_model.py
import os
import numpy as np
import pandas as pd
import tensorflow as tf
from sklearn.model_selection import train_test_split
from sklearn.preprocessing import MinMaxScaler
import joblib
import matplotlib.pyplot as plt
from datetime import timezone # Adicionado para datetime.now(timezone.utc)
# Importar configurações e módulos
from config import (
NUM_FEATURES, SYMBOL, TIMEFRAME, DAYS_OF_DATA_TO_FETCH, LIMIT_PER_FETCH,
WINDOW_SIZE, BASE_FEATURE_COLS, # NUM_FEATURES é derivado no model_builder
PREDICTION_HORIZON, PRICE_CHANGE_THRESHOLD,
EPOCHS, BATCH_SIZE, # LSTM_UNITS, DENSE_UNITS, etc., são usados em model_builder
MODEL_SAVE_DIR, MODEL_NAME,
PRICE_VOL_SCALER_NAME, INDICATOR_SCALER_NAME, PRICE_VOL_SCALER_NAME, INDICATOR_SCALER_NAME, # Nomes dos arquivos
EXPECTED_SCALED_FEATURES_FOR_MODEL, EXPECTED_FEATURES_ORDER # Usaremos esta para as colunas de entrada das sequências
)
# Note: LEARNING_RATE é usado em model_builder, não precisa importar aqui diretamente se não for para os callbacks.
from data_handler import fetch_ohlcv_data_ccxt, calculate_technical_indicators, calculate_targets, create_sequences
from model_builder import build_lstm_model
# Para métricas de classificação
from sklearn.metrics import classification_report, confusion_matrix
from sklearn.utils.class_weight import compute_class_weight
def main():
print("Iniciando script de treinamento da RNN...")
os.makedirs(MODEL_SAVE_DIR, exist_ok=True)
# --- 1. Obter e Pré-processar Dados ---
try:
ohlcv_df_raw = fetch_ohlcv_data_ccxt(SYMBOL, TIMEFRAME, DAYS_OF_DATA_TO_FETCH, LIMIT_PER_FETCH)
except Exception as e:
print(f"Falha ao buscar dados com CCXT: {e}")
# ... (lógica de fallback para CSV como antes) ...
return
if ohlcv_df_raw.empty: print("DataFrame de dados raw está vazio."); return
ohlcv_df_with_ta = calculate_technical_indicators(ohlcv_df_raw)
if ohlcv_df_with_ta.empty: print("DataFrame vazio após cálculo de indicadores."); return
ohlcv_df_final_features = calculate_targets(ohlcv_df_with_ta, PREDICTION_HORIZON, PRICE_CHANGE_THRESHOLD)
if ohlcv_df_final_features.empty: print("DataFrame vazio após cálculo de alvos."); return
# --- 2. Salvar Scalers para API (Fitados nas Colunas Base) ---
api_price_vol_atr_cols = [ # Features que já estão normalizadas ou são valores de preço/vol normalizados
'open_div_atr', 'high_div_atr', 'low_div_atr', 'close_div_atr',
'volume_div_atr', 'body_size_norm_atr'
]
# Garante que só pegamos o que está em BASE_FEATURE_COLS
api_price_vol_cols = [col for col in api_price_vol_atr_cols if col in BASE_FEATURE_COLS]
api_indicator_cols = [ # Features que são indicadores brutos ou taxas
'log_return_1', 'rsi_14', 'atr', 'bbp', 'cci_37', 'mfi_37',
'body_vs_avg_body', 'macd', 'sma_10_div_atr'
]
# Garante que só pegamos o que está em BASE_FEATURE_COLS e não está no outro grupo
api_indicator_cols = [col for col in api_indicator_cols if col in BASE_FEATURE_COLS and col not in api_price_vol_cols]
print("Preparando e salvando scalers para a API...")
# Garantir que todas as colunas de BASE_FEATURE_COLS existem ANTES de tentar fitar os scalers
missing_base_for_api_scalers = [col for col in BASE_FEATURE_COLS if col not in ohlcv_df_final_features.columns]
if missing_base_for_api_scalers:
print(f"ERRO: Colunas de BASE_FEATURE_COLS ({missing_base_for_api_scalers}) não encontradas para fitar scalers da API.")
print(f"Disponíveis: {ohlcv_df_final_features.columns.tolist()}")
return
api_price_vol_cols = [col for col in BASE_FEATURE_COLS if 'close_div_atr' in col or 'volume_div_atr' in col]
api_indicator_cols = [col for col in BASE_FEATURE_COLS if col not in api_price_vol_cols]
if api_price_vol_cols:
price_volume_scaler_api = MinMaxScaler()
price_volume_scaler_api.fit(ohlcv_df_final_features[api_price_vol_cols])
joblib.dump(price_volume_scaler_api, os.path.join(MODEL_SAVE_DIR, PRICE_VOL_SCALER_NAME))
print(f"Scaler de Preço/Volume (API: {api_price_vol_cols}) salvo.")
else:
print("Aviso: Nenhuma coluna de preço/volume definida para o scaler da API.")
if api_indicator_cols:
indicator_scaler_api = MinMaxScaler()
indicator_scaler_api.fit(ohlcv_df_final_features[api_indicator_cols])
joblib.dump(indicator_scaler_api, os.path.join(MODEL_SAVE_DIR, INDICATOR_SCALER_NAME))
print(f"Scaler de Indicadores (API: {api_indicator_cols}) salvo.")
else:
print("Aviso: Nenhuma coluna de indicador definida para o scaler da API.")
# --- 3. Escalonar TODAS as Features para Treinamento do Modelo Atual ---
# Este scaler é apenas para o processo de treinamento aqui.
# Ele é treinado em TODAS as BASE_FEATURE_COLS juntas.
print(f"Escalonando features para treinamento (colunas: {BASE_FEATURE_COLS})...")
training_features_df = ohlcv_df_final_features[BASE_FEATURE_COLS].copy()
general_training_scaler = MinMaxScaler()
scaled_values_for_training = general_training_scaler.fit_transform(training_features_df)
# DataFrame com colunas escaladas, ex: 'close_div_atr_scaled', 'rsi_14_scaled'
# Usa EXPECTED_SCALED_FEATURES_FOR_MODEL do config.py
df_scaled_for_sequences = pd.DataFrame(
scaled_values_for_training,
columns=EXPECTED_SCALED_FEATURES_FOR_MODEL,
index=training_features_df.index
)
# Juntar o target de volta
df_for_sequences = df_scaled_for_sequences.join(ohlcv_df_final_features[['target']])
df_for_sequences.dropna(inplace=True) # Se o join criar NaNs (improvável)
if df_for_sequences.empty: print("DataFrame para sequências vazio após escalonamento/join."); return
# --- 4. Criar Sequências ---
# As colunas para as sequências são as EXPECTED_SCALED_FEATURES_FOR_MODEL
X, y = create_sequences(df_for_sequences, "target", WINDOW_SIZE, EXPECTED_SCALED_FEATURES_FOR_MODEL)
if X.shape[0] == 0: print("Nenhuma sequência criada."); return
print("Preparando, fitando e salvando scalers...")
os.makedirs(MODEL_SAVE_DIR, exist_ok=True)
# Garantir que todas as colunas de BASE_FEATURE_COLS existem em ohlcv_df_final_features
missing_base_cols = [col for col in BASE_FEATURE_COLS if col not in ohlcv_df_final_features.columns]
if missing_base_cols:
print(f"ERRO FATAL: Colunas de BASE_FEATURE_COLS ({missing_base_cols}) não encontradas em ohlcv_df_final_features.")
return
# Definir quais colunas de BASE_FEATURE_COLS vão para cada scaler
# Estas são as colunas que representam valores normalizados pelo ATR (preço, volume, corpo)
price_vol_atr_norm_cols = [
'open_div_atr', 'high_div_atr', 'low_div_atr', 'close_div_atr',
'volume_div_atr', 'body_size_norm_atr'
]
# Remover colunas que podem não existir se você não as adicionou a BASE_FEATURE_COLS
price_vol_atr_norm_cols = [col for col in price_vol_atr_norm_cols if col in BASE_FEATURE_COLS]
# As colunas restantes de BASE_FEATURE_COLS são os "outros indicadores"
other_indicator_cols = [col for col in BASE_FEATURE_COLS if col not in price_vol_atr_norm_cols]
# DataFrames para fitar os scalers
df_for_pv_scaler = ohlcv_df_final_features[price_vol_atr_norm_cols].copy()
df_for_ind_scaler = ohlcv_df_final_features[other_indicator_cols].copy()
# Fitar e Salvar o Price/Volume (ATR Normalized) Scaler
if not df_for_pv_scaler.empty:
pv_atr_scaler = MinMaxScaler()
pv_atr_scaler.fit(df_for_pv_scaler)
joblib.dump(pv_atr_scaler, os.path.join(MODEL_SAVE_DIR, PRICE_VOL_SCALER_NAME))
print(f"Scaler de Preço/Volume (ATR Norm) (API: {price_vol_atr_norm_cols}) salvo.")
# Transformar os dados para o treinamento com este scaler
scaled_pv_data = pv_atr_scaler.transform(df_for_pv_scaler)
else:
print(f"AVISO: Sem dados para o scaler de Preço/Volume (ATR Norm) ({price_vol_atr_norm_cols}).")
scaled_pv_data = pd.DataFrame() # DataFrame vazio para evitar erro de concatenação
# Fitar e Salvar o Other Indicators Scaler
if not df_for_ind_scaler.empty:
other_ind_scaler = MinMaxScaler()
other_ind_scaler.fit(df_for_ind_scaler)
joblib.dump(other_ind_scaler, os.path.join(MODEL_SAVE_DIR, INDICATOR_SCALER_NAME))
print(f"Scaler de Outros Indicadores (API: {other_indicator_cols}) salvo.")
# Transformar os dados para o treinamento com este scaler
scaled_other_ind_data = other_ind_scaler.transform(df_for_ind_scaler)
else:
print(f"AVISO: Sem dados para o scaler de Outros Indicadores ({other_indicator_cols}).")
scaled_other_ind_data = pd.DataFrame()
# --- Montar o DataFrame com TODAS as features escaladas para o treinamento ---
# As colunas devem estar na ORDEM DE EXPECTED_SCALED_FEATURES_FOR_MODEL
# (que é derivada da ordem de BASE_FEATURE_COLS)
df_scaled_for_sequences = pd.DataFrame(index=ohlcv_df_final_features.index)
# Adicionar colunas escaladas de preço/volume
if not df_for_pv_scaler.empty:
for i, col_name in enumerate(price_vol_atr_norm_cols):
df_scaled_for_sequences[f"{col_name}_scaled"] = scaled_pv_data[:, i]
# Adicionar colunas escaladas de outros indicadores
if not df_for_ind_scaler.empty:
for i, col_name in enumerate(other_indicator_cols):
df_scaled_for_sequences[f"{col_name}_scaled"] = scaled_other_ind_data[:, i]
# Verificar se todas as colunas escaladas esperadas foram criadas
# `EXPECTED_SCALED_FEATURES_FOR_MODEL` vem do config.py
missing_scaled_cols = [col for col in EXPECTED_SCALED_FEATURES_FOR_MODEL if col not in df_scaled_for_sequences.columns]
if missing_scaled_cols:
print(f"ERRO FATAL: Colunas escaladas esperadas ({missing_scaled_cols}) não foram criadas para as sequências.")
print(f"Colunas escaladas disponíveis: {df_scaled_for_sequences.columns.tolist()}")
return
# Reordenar colunas para garantir a ordem de EXPECTED_SCALED_FEATURES_FOR_MODEL
df_scaled_for_sequences = df_scaled_for_sequences[EXPECTED_SCALED_FEATURES_FOR_MODEL]
# Juntar o target de volta
df_for_sequences = df_scaled_for_sequences.join(ohlcv_df_final_features[['target']])
df_for_sequences.dropna(inplace=True)
if df_for_sequences.empty: print("DataFrame para sequências vazio após escalonamento/join."); return
# --- 5. Dividir Dados ---
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
print(f"Treino: {X_train.shape[0]} amostras, Teste: {X_test.shape[0]} amostras.")
X, y = create_sequences(df_for_sequences, "target", WINDOW_SIZE, EXPECTED_SCALED_FEATURES_FOR_MODEL)
if X.shape[0] == 0: print("Nenhuma sequência criada."); return
# --- 6. Dividir Dados ---
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, random_state=42, stratify=y)
print(f"Treino: {X_train.shape[0]} amostras, Teste: {X_test.shape[0]} amostras.")
# --- 7. Construir e Treinar Modelo ---
print("Construindo modelo LSTM...")
# NUM_FEATURES é importado de config.py e deve ser len(BASE_FEATURE_COLS)
# EXPECTED_SCALED_FEATURES_FOR_MODEL também é de config.py e é [f"{col}_scaled" for col in BASE_FEATURE_COLS]
# O número de colunas em X_train (X_train.shape[2]) DEVE ser igual a NUM_FEATURES.
# E NUM_FEATURES DEVE ser igual a len(EXPECTED_SCALED_FEATURES_FOR_MODEL).
actual_num_features = NUM_FEATURES # Se estiverem consistentes
model_input_shape = (WINDOW_SIZE, actual_num_features)
model = build_lstm_model(model_input_shape) # model_builder.py usa LEARNING_RATE do config
print("Iniciando treinamento do modelo...")
reduce_lr_cb = tf.keras.callbacks.ReduceLROnPlateau(
monitor='val_loss',
factor=0.5, # Reduz pela metade, um pouco menos agressivo que 0.2
patience=7,
min_lr=1e-7, # Pode ser 1e-6 também
verbose=1
)
early_stopping_cb = tf.keras.callbacks.EarlyStopping(
monitor='val_loss',
patience=25, # Mantém sua paciência maior
restore_best_weights=True # MUITO IMPORTANTE
)
callbacks_list = [early_stopping_cb, reduce_lr_cb]
class_weights_map = {
0: 0.4, # Diminui o peso da classe majoritária
1: 4.0 # Aumenta BASTANTE o peso da classe minoritária (Rise)
}
unique_classes, counts = np.unique(y_train, return_counts=True)
print(f"Distribuição das classes no treino: {dict(zip(unique_classes, counts))}")
class_weights_vals = compute_class_weight('balanced', classes=unique_classes, y=y_train)
class_weights_map = {cls: weight for cls, weight in zip(unique_classes, class_weights_vals)}
class_weights_map = {0: 0.5, 1: 3.5}
print(f"Pesos de Classe: {class_weights_map}")
""" class_weights_map = {
0: 0.4, # Diminui o peso da classe majoritária
1: 4.0 # Aumenta BASTANTE o peso da classe minoritária (Rise)
} """
history = model.fit(X_train, y_train, epochs=EPOCHS, batch_size=BATCH_SIZE,
validation_split=0.1, # Usa 10% do X_train/y_train para validação
callbacks=callbacks_list, class_weight=class_weights_map, verbose=1)
# --- 8. Avaliar o Modelo TREINADO ---
# O modelo 'model' aqui é o treinado, com os melhores pesos restaurados pelo EarlyStopping
print("Avaliando modelo treinado no conjunto de teste...")
loss, accuracy_keras = model.evaluate(X_test, y_test, verbose=0)
print(f"Perda no Teste (Keras): {loss:.4f}")
print(f"Acurácia no Teste (Keras, thr=0.5): {accuracy_keras:.4f}")
# Após model.evaluate()
from sklearn.metrics import classification_report, confusion_matrix
y_pred_probs = model.predict(X_test)
y_pred_classes = (y_pred_probs > 0.65).astype(int) # 0.5, 0.6, 0.65, 0.7, 0.75 Valores de referencia par Thresholud
print("\nRelatório de Classificação no Conjunto de Teste:")
print(classification_report(y_test, y_pred_classes, target_names=['No Rise (0)', 'Rise (1)']))
print("\nMatriz de Confusão no Conjunto de Teste:")
cm = confusion_matrix(y_test, y_pred_classes)
print(cm)
import seaborn as sns # Para um plot mais bonito da matriz
plt.figure(figsize=(6,5))
sns.heatmap(cm, annot=True, fmt="d", cmap="Blues", xticklabels=['No Rise', 'Rise'], yticklabels=['No Rise', 'Rise'])
plt.xlabel('Predito')
plt.ylabel('Verdadeiro')
plt.title('Matriz de Confusão')
plt.savefig(os.path.join(MODEL_SAVE_DIR, "confusion_matrix.png"))
# --- 9. Salvar Modelo e Scalers ---
print("Salvando modelo e scalers...")
os.makedirs(MODEL_SAVE_DIR, exist_ok=True) # Cria o diretório se não existir
model_path = os.path.join(MODEL_SAVE_DIR, MODEL_NAME)
model.save(model_path)
print(f"Modelo salvo em: {model_path}")
# --- 9. Salvar o Modelo TREINADO ---
# Não reconstrua o modelo aqui! Salve o que foi treinado.
model_save_path = os.path.join(MODEL_SAVE_DIR, MODEL_NAME)
model.save(model_save_path)
print(f"Modelo TREINADO salvo em: {model_save_path}")
# --- 10. Plotar Histórico de Treinamento (Opcional) ---
plt.figure(figsize=(12, 6))
plt.subplot(1, 2, 1)
plt.plot(history.history['accuracy'], label='Acurácia Treino')
plt.plot(history.history['val_accuracy'], label='Acurácia Validação')
plt.title('Acurácia do Modelo')
plt.xlabel('Época')
plt.ylabel('Acurácia')
plt.legend()
plt.subplot(1, 2, 2)
plt.plot(history.history['loss'], label='Perda Treino')
plt.plot(history.history['val_loss'], label='Perda Validação')
plt.title('Perda do Modelo')
plt.xlabel('Época')
plt.ylabel('Perda')
plt.legend()
# Salvar o gráfico
plot_path = os.path.join(MODEL_SAVE_DIR, "training_history.png")
plt.savefig(plot_path)
print(f"Gráfico do histórico de treinamento salvo em: {plot_path}")
plt.show() # Descomente se quiser ver o gráfico imediatamente
# --- Imprimindo rodade de Tresh
print("\nAnálise com diferentes thresholds de predição no conjunto de teste:")
y_pred_probs = model.predict(X_test)
thresholds_to_test = [0.50, 0.55, 0.60, 0.65, 0.70, 0.75]
for thresh in thresholds_to_test:
print(f"\n--- Resultados com Threshold: {thresh:.2f} ---")
y_pred_classes = (y_pred_probs > thresh).astype(int)
# Use target_names se suas classes forem 0 e 1. Se y_test tiver outros valores, ajuste.
# Supondo que 0 é 'No Rise' e 1 é 'Rise'
print(classification_report(y_test, y_pred_classes, target_names=['No Rise (0)', 'Rise (1)'], zero_division=0))
print("Matriz de Confusão:")
print(confusion_matrix(y_test, y_pred_classes))
print("Script de treinamento concluído.")
if __name__ == "__main__":
main()
# Configurar GPU (Opcional, se tiver TensorFlow com suporte a GPU)
print("Iniciando configuração de GPU")
gpus = tf.config.experimental.list_physical_devices('GPU')
if gpus:
try:
for gpu in gpus:
tf.config.experimental.set_memory_growth(gpu, True)
logical_gpus = tf.config.experimental.list_logical_devices('GPU')
print(len(gpus), "Physical GPUs,", len(logical_gpus), "Logical GPUs")
except RuntimeError as e:
print(e)