|
|
|
|
|
|
|
|
import pandas as pd |
|
|
import numpy as np |
|
|
import yfinance as yf |
|
|
import pandas_ta as ta |
|
|
from datetime import datetime, timedelta, timezone |
|
|
from typing import List, Dict, Optional |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
MULTI_ASSET_SYMBOLS = { |
|
|
'crypto_eth': 'ETH-USD', |
|
|
'crypto_ada': 'ADA-USD', |
|
|
'stock_aapl': 'AAPL', |
|
|
'stock_petr': 'PETR4.SA' |
|
|
} |
|
|
TIMEFRAME_YFINANCE = '1h' |
|
|
|
|
|
|
|
|
DAYS_TO_FETCH = 365 * 2 |
|
|
|
|
|
|
|
|
|
|
|
INDIVIDUAL_ASSET_BASE_FEATURES = [ |
|
|
'open', 'high', 'low', 'close', 'volume', |
|
|
'sma_10', 'rsi_14', 'macd', 'macds', 'atr', 'bbp', 'cci_37', 'mfi_37', 'adx_14', |
|
|
'volume_zscore', 'body_size', 'body_size_norm_atr', 'body_vs_avg_body', |
|
|
'log_return', 'buy_condition_v1', |
|
|
|
|
|
] |
|
|
|
|
|
|
|
|
COLS_TO_NORM_BY_ATR = ['open', 'high', 'low', 'close', 'volume', 'sma_10', 'macd', 'body_size'] |
|
|
|
|
|
|
|
|
def fetch_single_asset_ohlcv_yf(ticker_symbol: str, period: str = "2y", interval: str = "1h") -> pd.DataFrame: |
|
|
""" Adaptação da sua função fetch_historical_ohlcv de financial_data_agent.py """ |
|
|
print(f"Buscando dados para {ticker_symbol} com yfinance (period: {period}, interval: {interval})...") |
|
|
try: |
|
|
ticker = yf.Ticker(ticker_symbol) |
|
|
|
|
|
|
|
|
if interval == '1h' and period.endswith('y') and int(period[:-1]) * 365 > 730: |
|
|
print(f"AVISO: yfinance pode limitar dados horários a 730 dias. Buscando 'max' para {interval} e depois fatiando.") |
|
|
data = ticker.history(interval=interval, period="730d") |
|
|
elif interval == '1d' and period.endswith('y'): |
|
|
data = ticker.history(period=period, interval=interval) |
|
|
else: |
|
|
data = ticker.history(period=period, interval=interval) |
|
|
|
|
|
if data.empty: |
|
|
print(f"Nenhum dado encontrado para {ticker_symbol}.") |
|
|
return pd.DataFrame() |
|
|
|
|
|
data.rename(columns={ |
|
|
"Open": "open", "High": "high", "Low": "low", |
|
|
"Close": "close", "Volume": "volume", "Adj Close": "adj_close" |
|
|
}, inplace=True) |
|
|
|
|
|
|
|
|
data = data[['open', 'high', 'low', 'close', 'volume']] |
|
|
if data.index.tz is None: |
|
|
data.index = data.index.tz_localize('UTC') |
|
|
else: |
|
|
data.index = data.index.tz_convert('UTC') |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
print(f"Dados coletados para {ticker_symbol}: {len(data)} linhas.") |
|
|
return data |
|
|
except Exception as e: |
|
|
print(f"Erro ao buscar dados para {ticker_symbol} com yfinance: {e}") |
|
|
return pd.DataFrame() |
|
|
|
|
|
|
|
|
def calculate_all_features_for_single_asset(ohlcv_df: pd.DataFrame) -> Optional[pd.DataFrame]: |
|
|
"""Calcula todas as features base para um único ativo.""" |
|
|
if ohlcv_df.empty: return None |
|
|
df = ohlcv_df.copy() |
|
|
print(f"Calculando features para ativo (shape inicial: {df.shape})...") |
|
|
|
|
|
if ta: |
|
|
df.ta.sma(length=10, close='close', append=True, col_names=('sma_10',)) |
|
|
df.ta.rsi(length=14, close='close', append=True, col_names=('rsi_14',)) |
|
|
macd_out = df.ta.macd(close='close', append=False) |
|
|
if macd_out is not None and not macd_out.empty: |
|
|
df['macd'] = macd_out.iloc[:,0] |
|
|
df['macds'] = macd_out.iloc[:,2] |
|
|
df.ta.atr(length=14, append=True, col_names=('atr',)) |
|
|
df.ta.bbands(length=20, close='close', append=True, col_names=('bbl', 'bbm', 'bbu', 'bbb', 'bbp')) |
|
|
df.ta.cci(length=37, append=True, col_names=('cci_37',)) |
|
|
df.ta.mfi(length=37, append=True, col_names=('mfi_37',)) |
|
|
adx_out = df.ta.adx(length=14, append=False) |
|
|
if adx_out is not None and not adx_out.empty: |
|
|
df['adx_14'] = adx_out.iloc[:,0] |
|
|
|
|
|
rolling_vol_mean = df['volume'].rolling(window=20).mean() |
|
|
rolling_vol_std = df['volume'].rolling(window=20).std() |
|
|
df['volume_zscore'] = (df['volume'] - rolling_vol_mean) / (rolling_vol_std + 1e-9) |
|
|
|
|
|
df['body_size'] = abs(df['close'] - df['open']) |
|
|
|
|
|
|
|
|
df.dropna(subset=['atr'], inplace=True) |
|
|
df_atr_valid = df[df['atr'] > 1e-9].copy() |
|
|
if df_atr_valid.empty: |
|
|
print("AVISO: ATR inválido para todas as linhas restantes, features _div_atr e body_size_norm_atr podem ser todas NaN ou vazias.") |
|
|
|
|
|
df['body_size_norm_atr'] = np.nan |
|
|
for col in COLS_TO_NORM_BY_ATR: |
|
|
df[f'{col}_div_atr'] = np.nan |
|
|
else: |
|
|
df['body_size_norm_atr'] = df['body_size'] / df['atr'] |
|
|
for col in COLS_TO_NORM_BY_ATR: |
|
|
if col in df.columns: |
|
|
df[f'{col}_div_atr'] = df[col] / (df['atr'] + 1e-9) |
|
|
else: |
|
|
df[f'{col}_div_atr'] = np.nan |
|
|
|
|
|
|
|
|
df['body_vs_avg_body'] = df['body_size'] / (df['body_size'].rolling(window=20).mean() + 1e-9) |
|
|
df['log_return'] = np.log(df['close'] / df['close'].shift(1)) |
|
|
|
|
|
sma_50_series = df.ta.sma(length=50, close='close', append=False) |
|
|
if sma_50_series is not None: df['sma_50'] = sma_50_series |
|
|
else: df['sma_50'] = np.nan |
|
|
|
|
|
if all(col in df.columns for col in ['macd', 'macds', 'rsi_14', 'close', 'sma_50']): |
|
|
df['buy_condition_v1'] = ((df['macd'] > df['macds']) & (df['rsi_14'] > 50) & (df['close'] > df['sma_50'])).astype(int) |
|
|
else: |
|
|
df['buy_condition_v1'] = 0 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
current_feature_cols = [col for col in INDIVIDUAL_ASSET_BASE_FEATURES if col in df.columns] |
|
|
missing_cols = [col for col in INDIVIDUAL_ASSET_BASE_FEATURES if col not in df.columns] |
|
|
if missing_cols: |
|
|
print(f"AVISO: Colunas de features ausentes após cálculo: {missing_cols}. Usando apenas as disponíveis: {current_feature_cols}") |
|
|
|
|
|
df_final_features = df[current_feature_cols].copy() |
|
|
df_final_features.dropna(inplace=True) |
|
|
print(f"Features calculadas. Shape após dropna: {df_final_features.shape}. Colunas: {df_final_features.columns.tolist()}") |
|
|
return df_final_features |
|
|
else: |
|
|
print("pandas_ta não está disponível.") |
|
|
return None |
|
|
|
|
|
|
|
|
def get_multi_asset_data_for_rl( |
|
|
asset_symbols_map: Dict[str, str], |
|
|
timeframe_yf: str, |
|
|
days_to_fetch: int |
|
|
) -> Optional[pd.DataFrame]: |
|
|
""" |
|
|
Busca, processa e combina dados de múltiplos ativos em um DataFrame achatado. |
|
|
""" |
|
|
all_asset_features_list = [] |
|
|
min_data_length = float('inf') |
|
|
|
|
|
|
|
|
for asset_key, yf_ticker in asset_symbols_map.items(): |
|
|
print(f"\n--- Processando {asset_key} ({yf_ticker}) ---") |
|
|
|
|
|
|
|
|
period_yf = f"{days_to_fetch}d" |
|
|
if timeframe_yf == '1h' and days_to_fetch > 730: |
|
|
print(f"AVISO: Para {timeframe_yf}, buscando no máximo 730 dias com yfinance para {yf_ticker}.") |
|
|
period_yf = "730d" |
|
|
|
|
|
single_asset_ohlcv = fetch_single_asset_ohlcv_yf(yf_ticker, period=period_yf, interval=timeframe_yf) |
|
|
if single_asset_ohlcv.empty: |
|
|
print(f"AVISO: Sem dados OHLCV para {yf_ticker}, pulando este ativo.") |
|
|
continue |
|
|
|
|
|
single_asset_features = calculate_all_features_for_single_asset(single_asset_ohlcv) |
|
|
if single_asset_features is None or single_asset_features.empty: |
|
|
print(f"AVISO: Sem features calculadas para {yf_ticker}, pulando este ativo.") |
|
|
continue |
|
|
|
|
|
|
|
|
single_asset_features = single_asset_features.add_prefix(f"{asset_key}_") |
|
|
all_asset_features_list.append(single_asset_features) |
|
|
min_data_length = min(min_data_length, len(single_asset_features)) |
|
|
|
|
|
if not all_asset_features_list: |
|
|
print("ERRO: Nenhum dado de feature de ativo foi processado com sucesso.") |
|
|
return None |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
if min_data_length == float('inf') or min_data_length == 0 : |
|
|
print("ERRO: min_data_length inválido, não é possível truncar DataFrames.") |
|
|
return None |
|
|
|
|
|
truncated_asset_features_list = [df.tail(min_data_length) for df in all_asset_features_list] |
|
|
|
|
|
|
|
|
|
|
|
combined_df = pd.concat(truncated_asset_features_list, axis=1, join='inner') |
|
|
|
|
|
|
|
|
if combined_df.empty: |
|
|
print("ERRO: DataFrame combinado está vazio após concatenação e join. Verifique os dados dos ativos.") |
|
|
return None |
|
|
|
|
|
|
|
|
combined_df.dropna(inplace=True) |
|
|
|
|
|
if combined_df.empty: |
|
|
print("ERRO: DataFrame combinado está vazio após dropna final.") |
|
|
return None |
|
|
|
|
|
print(f"\nDataFrame multi-ativo final gerado com shape: {combined_df.shape}") |
|
|
print(f"Exemplo de colunas: {combined_df.columns.tolist()[:10]}...") |
|
|
return combined_df |
|
|
|
|
|
|
|
|
if __name__ == '__main__': |
|
|
print("Testando data_handler_multi_asset.py...") |
|
|
|
|
|
test_assets = { |
|
|
'eth': 'ETH-USD', |
|
|
'btc': 'BTC-USD', |
|
|
|
|
|
} |
|
|
multi_asset_data = get_multi_asset_data_for_rl( |
|
|
test_assets, |
|
|
timeframe_yf='1h', |
|
|
days_to_fetch=90 |
|
|
) |
|
|
|
|
|
if multi_asset_data is not None and not multi_asset_data.empty: |
|
|
print("\n--- Exemplo do DataFrame Multi-Ativo Gerado ---") |
|
|
print(multi_asset_data.head()) |
|
|
print(f"\nShape: {multi_asset_data.shape}") |
|
|
print(f"\nInfo:") |
|
|
multi_asset_data.info() |
|
|
else: |
|
|
print("\nFalha ao gerar DataFrame multi-ativo.") |