Documentação completa da arquitetura, fluxos e componentes da Stock Prediction LSTM API.
- Visão Geral
- Arquitetura
- Fluxo de Requisição
- Endpoints
- Serviços
- Sistema de Exceções
- Validadores
- Segurança
- Performance
- Troubleshooting
API REST em Flask para servir predições de preços de ações utilizando modelo LSTM treinado. Fornece endpoints para health check, informações do modelo e predições em tempo real.
- Framework: Flask 3.x (porta 5001)
- CORS: Flask-CORS
- ML: PyTorch 2.2 (CPU-only)
- Data Source: Yahoo Finance (yfinance)
- Python: 3.11+
-
Desenvolvimento:
http://localhost:5001 -
Produção: Configurável via variável de ambiente
-
Produção: Configurável via variável de ambiente
POST /predict
Content-Type: application/json
Body: {"ticker": "PETR4.SA"}
│ ├─ Campo "ticker" presente? ✓
│ └─ validate_ticker("PETR4.SA")
def create_app(config=None):
│ 4. BUSCA DE DADOS (DataService) │
│ fetch_data("PETR4.SA") │
app = Flask(__name__)
# Configuração
# Registro de blueprints
│ ├─ Calcula período: hoje - ~120 dias até hoje (para preencher 60 úteis) │
│ ├─ yf.Ticker("PETR4.SA").history(start, end) │
# Handlers de erro
return appBenefícios: │ 5. FEATURE ENGINEERING (18 colunas) │ │ ├─ OHLCV: Open, High, Low, Close, Volume │ │ ├─ Indicadores: SMA_20, SMA_50, EMA_12, EMA_26, RSI_14, MACD, MACD_signal, MACD_hist, BB_middle, BB_upper, BB_lower, ATR_14, Returns │ └─ current_price = último Close │
- Múltiplas instâncias para diferentes ambientes
- Facilita testes unitários
- Separação de configuração e inicialização
│ 6. NORMALIZAÇÃO │
│ ModelService.get_scaler() │
│ ├─ MinMaxScaler range [0, 1] │
│ └─ scaled_data = scaler.transform(features[18 col]) │
class ModelService:
_instance = None
def __new__(cls):
if cls._instance is None:
cls._instance = super().__new__(cls)
return cls._instance
│ 7. CONVERSÃO PARA TENSOR │
│ X = torch.FloatTensor(scaled_data[-60:]).unsqueeze(0) │
│ Shape: [1, 60, 18] │
│ ├─ batch_size: 1 │
│ ├─ sequence_length: 60 │
│ └─ features: 18 │Benefícios:
- Uma única instância do modelo em memória (~500MB)
- Evita recarregar modelo a cada requisição
- Aumenta performance significativamente
│ 8. INFERÊNCIA DO MODELO │ │ ModelService.get_model() │ │ with torch.no_grad(): │ │ prediction_scaled = model(X) │ │ ├─ LSTM processa sequência │ │ └─ Linear layer gera predição │ │ → Tensor [batch, 1] (normalizado) │
health_bp = Blueprint('health', __name__)
model_info_bp = Blueprint('model_info', __name__)
prediction_bp = Blueprint('prediction', __name__)Benefícios:
- Organização modular de endpoints
- Facilita manutenção e escalabilidade
- Permite registro condicional
src/api/
├── main.py # Application Factory
│
├── routes/ # Endpoints HTTP (Blueprints)
│ ├── __init__.py
│ ├── health.py # GET /health
│ ├── model_info.py # GET /model/info
│ └── prediction.py # POST /predict
│
├── services/ # Lógica de negócio
│ ├── __init__.py
│ ├── model_service.py # Gerencia modelo LSTM (Singleton)
│ ├── data_service.py # Busca dados do Yahoo Finance
│ └── predict_service.py # Orquestra pipeline de predição
│
├── models/ # Definições de modelos ML
│ ├── __init__.py
│ └── lstm_model.py # Arquitetura LSTM PyTorch
│
└── utils/ # Utilitários
├── __init__.py
├── exceptions.py # Exceções customizadas
└── validators.py # Validação de entrada
┌─────────────────────────────────────────────────────────────────┐
│ 1. ENTRADA DO CLIENTE │
│ POST /predict │
│ Content-Type: application/json │
│ Body: {"ticker": "AAPL"} │
└────────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 2. VALIDAÇÃO (prediction.py) │
│ ├─ Content-Type é application/json? ✓ │
│ ├─ Campo "ticker" presente? ✓ │
│ └─ validate_ticker("AAPL") │
│ ├─ 2-10 caracteres? ✓ │
│ ├─ Formato [A-Z][A-Z0-9.-]? ✓ │
│ └─ Não começa com número? ✓ │
└────────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 3. PREDICT SERVICE │
│ PredictService.predict("AAPL") │
│ ├─ normalize_ticker("AAPL") → "AAPL" │
│ └─ Inicia pipeline │
└────────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 4. BUSCA DE DADOS (DataService) │
│ fetch_data("AAPL") │
│ ├─ Calcula período: hoje - 90 dias até hoje │
│ ├─ yf.Ticker("AAPL").history(start, end) │
│ ├─ Valida DataFrame não vazio │
│ ├─ Valida >= 60 dias de dados │
│ └─ Retorna últimos 60 dias │
│ → DataFrame[Open, High, Low, Close, Volume] │
└────────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 5. PREPARAÇÃO DOS DADOS │
│ ├─ Extrai Close prices: [175.1, 176.3, ..., 175.2] │
│ ├─ Reshape para (60, 1) │
│ └─ current_price = 175.20 │
└────────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 6. NORMALIZAÇÃO │
│ ModelService.get_scaler() │
│ ├─ MinMaxScaler range [0, 1] │
│ └─ scaled_data = scaler.transform(close_prices) │
│ → [0.523, 0.541, ..., 0.520] │
└────────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 7. CONVERSÃO PARA TENSOR │
│ X = torch.FloatTensor(scaled_data).unsqueeze(0) │
│ Shape: [1, 60, 1] │
│ ├─ batch_size: 1 │
│ ├─ sequence_length: 60 │
│ └─ features: 1 │
└────────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 8. INFERÊNCIA DO MODELO │
│ ModelService.get_model() │
│ with torch.no_grad(): │
│ prediction_scaled = model(X) │
│ ├─ LSTM processa sequência │
│ └─ Linear layer gera predição │
│ → Tensor [0.535] (normalizado) │
└────────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 9. DESNORMALIZAÇÃO │
│ predicted_price = scaler.inverse_transform(prediction_scaled)│
│ → 178.45 │
└────────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 10. CÁLCULO DE MÉTRICAS │
│ ├─ change_percent = ((178.45 - 175.20) / 175.20) * 100 │
│ │ → 1.85% │
│ ├─ change_direction = "up" │
│ ├─ prediction_date = hoje + 1 dia │
│ └─ confidence = "medium" (|1.85%| está entre 2-5%) │
└────────────────────┬────────────────────────────────────────────┘
│
▼
┌─────────────────────────────────────────────────────────────────┐
│ 11. RESPOSTA AO CLIENTE │
│ HTTP 200 OK │
│ { │
│ "success": true, │
│ "data": { │
│ "ticker": "AAPL", │
│ "predicted_price": 178.45, │
│ "current_price": 175.20, │
│ "change_percent": 1.85, │
│ "change_direction": "up", │
│ "prediction_date": "2025-12-31", │
│ "confidence": "medium", │
│ "timestamp": "2025-12-30T10:30:00" │
│ } │
│ } │
└─────────────────────────────────────────────────────────────────┘
Exception levantada em qualquer ponto
│
▼
Tipo da Exception?
│
├─ InvalidTickerError
│ └─→ HTTP 400 {"error": "Invalid Ticker", ...}
│
├─ TickerNotFoundError
│ └─→ HTTP 404 {"error": "Ticker Not Found", ...}
│
├─ InsufficientDataError
│ └─→ HTTP 400 {"error": "Insufficient Data", ...}
│
├─ ServiceUnavailableError
│ └─→ HTTP 503 {"error": "Service Unavailable", ...}
│
├─ ModelInferenceError
│ └─→ HTTP 500 {"error": "Model Inference Error", ...}
│
└─ Exception genérica
└─→ HTTP 500 {"error": "Internal Server Error"}
Verifica se a API está rodando e operacional.
Request:
curl http://localhost:5001/healthResponse 200 OK:
{
"status": "healthy",
"timestamp": "2025-12-30T10:30:00.123456",
"service": "stock-prediction-lstm-api"
}Uso:
- Health checks de containers Docker
- Monitoramento de disponibilidade
- Load balancers e orchestrators
Latência: ~5ms
Retorna metadados e configuração do modelo LSTM carregado.
Request:
curl http://localhost:5001/model/infoResponse 200 OK:
{
"model_type": "LSTM",
"architecture": "LSTM-1x16",
"input_size": 1,
"hidden_size": 16,
"num_layers": 1,
"dropout": 0.2,
"sequence_length": 60,
"target_column": "Close",
"metrics": {
"test_mae": 2.34,
"test_mse": 8.92,
"test_rmse": 2.99,
"test_mape": 1.21,
"test_r2": 0.90
},
"training_info": {
"dataset": "AAPL (2020-2024)",
"trained_on": "2024-12-15",
"epochs": 100,
"batch_size": 32,
"optimizer": "Adam",
"learning_rate": 0.001
}
}Response 404 Not Found:
{
"error": "Config Not Found",
"message": "Arquivo de configuração do modelo não encontrado",
"status": 404
}Response 500 Internal Server Error:
{
"error": "Internal Server Error",
"message": "Erro ao carregar configurações: Invalid JSON",
"status": 500
}Latência: ~10ms
Realiza predição de preço de fechamento para o próximo dia útil.
Query Parameters:
| Parâmetro | Tipo | Obrigatório | Padrão | Descrição |
|---|---|---|---|---|
| include_history | boolean | ❌ | false | Inclui dados históricos dos últimos 30 dias (OHLCV) na resposta |
Request (sem histórico):
curl -X POST http://localhost:5001/predict \
-H "Content-Type: application/json" \
-d '{"ticker": "AAPL"}'Request (com histórico):
curl -X POST "http://localhost:5001/predict?include_history=true" \
-H "Content-Type: application/json" \
-d '{"ticker": "AAPL"}'Request Body Schema:
{
"ticker": "string (2-10 chars, uppercase, alphanumeric + . -)"
}Validações:
| Campo | Tipo | Obrigatório | Validação |
|---|---|---|---|
| ticker | string | ✅ | 2-10 caracteres, começa com letra, apenas [A-Z0-9.-] |
Exemplos Válidos:
AAPL(Apple)MSFT(Microsoft)PETR4.SA(Petrobras - B3)BRK-B(Berkshire Hathaway Class B)
Response 200 OK (sem histórico):
{
"success": true,
"data": {
"ticker": "AAPL",
"predicted_price": 178.45,
"current_price": 175.20,
"change_percent": 1.85,
"change_direction": "up",
"prediction_date": "2025-12-31",
"confidence": "medium",
"timestamp": "2025-12-30T10:30:00.123456"
}
}Response 200 OK (com histórico - ?include_history=true):
{
"success": true,
"data": {
"ticker": "AAPL",
"predicted_price": 178.45,
"current_price": 175.20,
"change_percent": 1.85,
"change_direction": "up",
"prediction_date": "2025-12-31",
"confidence": "medium",
"timestamp": "2025-12-30T10:30:00.123456",
"historical_data": [
{
"date": "2025-11-28",
"open": 172.50,
"high": 174.20,
"low": 171.80,
"close": 173.90,
"volume": 52000000
},
{
"date": "2025-11-29",
"open": 173.95,
"high": 175.60,
"low": 173.50,
"close": 175.20,
"volume": 48500000
}
// ... 28 dias adicionais
]
}
}Campos de Response:
| Campo | Tipo | Opcional | Descrição |
|---|---|---|---|
| ticker | string | ❌ | Símbolo normalizado (uppercase) |
| predicted_price | float | ❌ | Preço previsto para próximo dia (arredondado 2 casas) |
| current_price | float | ❌ | Último preço de fechamento conhecido |
| change_percent | float | ❌ | Variação % esperada (positivo = alta, negativo = baixa) |
| change_direction | string | ❌ | "up", "down" ou "neutral" |
| prediction_date | string | ❌ | Data da previsão (formato YYYY-MM-DD) |
| confidence | string | ❌ | "high" (<2%), "medium" (2-5%), "low" (>5%) |
| timestamp | string | ❌ | ISO 8601 timestamp da predição |
| historical_data | array | ✅ | Array com últimos 30 dias (OHLCV). Incluído apenas se include_history=true |
Campos de historical_data (quando incluído):
| Campo | Tipo | Descrição |
|---|---|---|
| date | string | Data no formato YYYY-MM-DD |
| open | float | Preço de abertura (2 casas decimais) |
| high | float | Preço máximo do dia (2 casas decimais) |
| low | float | Preço mínimo do dia (2 casas decimais) |
| close | float | Preço de fechamento (2 casas decimais) |
| volume | integer | Volume negociado |
Response 400 Bad Request (Content-Type inválido):
{
"error": "Invalid Content-Type",
"message": "Content-Type deve ser application/json",
"status": 400
}Response 400 Bad Request (Campo ausente):
{
"error": "Missing Field",
"message": "Campo 'ticker' é obrigatório",
"status": 400
}Response 400 Bad Request (Ticker inválido):
{
"error": "Invalid Ticker",
"message": "Formato de ticker inválido",
"ticker": "A",
"suggestion": "Ticker deve ter entre 2 e 10 caracteres",
"status": 400
}Response 404 Not Found (Ticker não existe):
{
"error": "Ticker Not Found",
"message": "Ticker INVALID não encontrado no Yahoo Finance",
"ticker": "INVALID",
"suggestion": "Verifique se o símbolo está correto. Para ações brasileiras, use o sufixo .SA (ex: PETR4.SA)",
"status": 404
}Response 400 Bad Request (Dados insuficientes):
{
"error": "Insufficient Data",
"message": "Dados históricos insuficientes para NEWCO",
"ticker": "NEWCO",
"days_available": 25,
"days_required": 60,
"suggestion": "Modelo requer pelo menos 60 dias de histórico",
"status": 400
}Response 503 Service Unavailable (Yahoo Finance offline):
{
"error": "Service Unavailable",
"message": "Serviço Yahoo Finance temporariamente indisponível",
"service": "Yahoo Finance",
"retry_after": 60,
"status": 503
}Response 500 Internal Server Error (Erro de inferência):
{
"error": "Model Inference Error",
"message": "Erro ao processar previsão para AAPL",
"ticker": "AAPL",
"details": "Tensor shape mismatch: expected [1, 60, 1], got [1, 59, 1]",
"status": 500
}Latência: ~470ms (média)
- 400ms: Yahoo Finance
- 50ms: Inferência do modelo
- 20ms: Processamento restante
Tamanho do Payload:
- Sem histórico: ~250 bytes
- Com histórico (
include_history=true): ~3.5 KB
💡 Dica: Use include_history=true apenas quando necessário exibir gráfico histórico, para economizar banda.
Arquivo: src/api/services/model_service.py
Responsabilidade: Gerenciar modelo LSTM e MinMaxScaler em memória.
Padrão: Singleton (instância única compartilhada)
Ciclo de Vida:
# Primeira chamada - inicialização
service = ModelService()
├─ Carrega artifacts/model_config.json
├─ Cria StockLSTM(input_size=1, hidden_size=16, ...)
├─ Carrega artifacts/model_lstm_1x16.pt
├─ Define model.eval() (modo inferência)
└─ Carrega artifacts/scaler_corrected.pkl
# Chamadas subsequentes - reutiliza instância
service = ModelService() # Retorna mesma instânciaMétodos Públicos:
get_model() -> torch.nn.Module
"""Retorna modelo LSTM em modo eval."""
# Raises: RuntimeError se modelo não carregado
get_scaler() -> MinMaxScaler
"""Retorna scaler para normalização."""
# Raises: RuntimeError se scaler não carregado
get_config() -> Dict[str, Any]
"""Retorna configuração completa do modelo."""
# Raises: RuntimeError se config não carregada
is_ready() -> bool
"""Verifica se modelo, scaler e config estão carregados."""
# Returns: True se tudo OK, False caso contrárioThread-Safety: ✅ Sim (singleton com inicialização única)
Memory Footprint: ~500MB (modelo + scaler)
Tratamento de Erros:
FileNotFoundError→RuntimeError("Artefato necessário não encontrado")- Qualquer outro erro →
RuntimeError("Falha ao inicializar modelo")
Arquivo: src/api/services/data_service.py
Responsabilidade: Buscar dados históricos de ações via Yahoo Finance.
Padrão: Instância simples (não singleton, pode ter múltiplas)
Inicialização:
service = DataService(lookback_days=60)Métodos Públicos:
fetch_data(ticker: str) -> pd.DataFrame
"""
Busca últimos N dias de dados históricos.
Returns: DataFrame com [Open, High, Low, Close, Volume]
Raises:
TickerNotFoundError: Ticker não existe no Yahoo Finance
InsufficientDataError: Menos de lookback_days disponíveis
ServiceUnavailableError: Yahoo Finance offline/timeout
"""Fluxo Interno de fetch_data:
1. Calcula período
end_date = datetime.now()
start_date = end_date - timedelta(days=lookback_days + 30)
# +30 dias para compensar fins de semana/feriados
2. Busca dados
stock = yf.Ticker(ticker)
df = stock.history(start=start_date, end=end_date)
3. Valida dados
if df.empty:
raise TickerNotFoundError(ticker)
if len(df) < lookback_days:
raise InsufficientDataError(...)
4. Retorna últimos N dias
return df.tail(lookback_days)Tratamento de Exceções:
| Exceção Python | Ação |
|---|---|
ConnectionError |
ServiceUnavailableError(retry_after=60) |
TimeoutError |
ServiceUnavailableError(retry_after=30) |
| String contém "connection"/"timeout" | ServiceUnavailableError |
| Outras exceções | TickerNotFoundError (assume ticker inválido) |
Método Auxiliar:
get_latest_price(ticker: str) -> Optional[float]
"""
Retorna último preço de fechamento.
Returns: float ou None se erro
"""Arquivo: src/api/services/predict_service.py
Responsabilidade: Orquestrar pipeline completo de predição.
Dependências:
ModelService(singleton)DataService(instância própria com lookback_days=60)
Inicialização:
service = PredictService()
├─ self.model_service = ModelService()
├─ self.data_service = DataService(lookback_days=60)
└─ Valida model_service.is_ready()Método Principal:
predict(ticker: str) -> Dict[str, Any]
"""
Pipeline completo de predição.
Steps:
1. Normaliza ticker (uppercase, strip)
2. Busca dados históricos (60 dias)
3. Extrai Close prices
4. Normaliza com scaler
5. Converte para tensor PyTorch [1, 60, 1]
6. Inferência do modelo
7. Desnormaliza resultado
8. Calcula métricas (change_percent, confidence)
9. Retorna resultado estruturado
Returns: dict com previsão e metadados
Raises: Todas as custom exceptions do pipeline
"""Cálculo de Confiança:
abs_change = abs(change_percent)
if abs_change < 2:
confidence = "high" # Variação pequena, alta confiança
elif abs_change < 5:
confidence = "medium" # Variação moderada, média confiança
else:
confidence = "low" # Variação grande, baixa confiançaTratamento de Exceções:
# Custom exceptions - re-raise (já estão corretas)
except (InvalidTickerError, TickerNotFoundError,
InsufficientDataError, ServiceUnavailableError,
ModelInferenceError):
raise
# Exceções genéricas - tenta inferir o tipo
except Exception as e:
error_msg = str(e).lower()
# Erros relacionados a tensores PyTorch
if "tensor" in error_msg or "shape" in error_msg:
raise ModelInferenceError(ticker, error_detail=str(e))
# Outros erros - genérico
raise ModelInferenceError(ticker, error_detail=str(e))Hierarquia de exceções customizadas para tratamento granular de erros.
Arquivo: src/api/utils/exceptions.py
class APIException(Exception):
status_code = 500
error_type = "API Error"
def to_dict(self) -> dict:
"""Converte exceção para JSON response."""
return {
"error": self.error_type,
"message": str(self),
"status": self.status_code
}APIException (500)
├── InvalidTickerError (400)
├── TickerNotFoundError (404)
├── InsufficientDataError (400)
├── ServiceUnavailableError (503)
└── ModelInferenceError (500)
Quando usar: Formato de ticker não atende aos requisitos.
Construtor:
InvalidTickerError(ticker: str, suggestion: str)Exemplo:
raise InvalidTickerError(
ticker="A",
suggestion="Ticker deve ter entre 2 e 10 caracteres"
)JSON Response:
{
"error": "Invalid Ticker",
"message": "Formato de ticker inválido",
"ticker": "A",
"suggestion": "Ticker deve ter entre 2 e 10 caracteres",
"status": 400
}Quando usar: Ticker não existe no Yahoo Finance.
Construtor:
TickerNotFoundError(ticker: str)Exemplo:
raise TickerNotFoundError(ticker="INVALID")JSON Response:
{
"error": "Ticker Not Found",
"message": "Ticker INVALID não encontrado no Yahoo Finance",
"ticker": "INVALID",
"suggestion": "Verifique se o símbolo está correto. Para ações brasileiras, use o sufixo .SA (ex: PETR4.SA)",
"status": 404
}Quando usar: Ticker existe mas tem menos de 60 dias de histórico.
Construtor:
InsufficientDataError(
ticker: str,
days_available: int,
days_required: int
)Exemplo:
raise InsufficientDataError(
ticker="NEWCO",
days_available=25,
days_required=60
)JSON Response:
{
"error": "Insufficient Data",
"message": "Dados históricos insuficientes para NEWCO",
"ticker": "NEWCO",
"days_available": 25,
"days_required": 60,
"suggestion": "Modelo requer pelo menos 60 dias de histórico",
"status": 400
}Quando usar: Yahoo Finance está offline, timeout ou erro de rede.
Construtor:
ServiceUnavailableError(
service: str = "External Service",
retry_after: int = 60
)Exemplo:
raise ServiceUnavailableError(
service="Yahoo Finance",
retry_after=60
)JSON Response:
{
"error": "Service Unavailable",
"message": "Serviço Yahoo Finance temporariamente indisponível",
"service": "Yahoo Finance",
"retry_after": 60,
"status": 503
}Headers HTTP:
Retry-After: 60
Quando usar: Erro durante inferência do modelo PyTorch.
Construtor:
ModelInferenceError(
ticker: str,
error_detail: str = "Erro desconhecido"
)Exemplo:
raise ModelInferenceError(
ticker="AAPL",
error_detail="Tensor shape mismatch"
)JSON Response:
{
"error": "Model Inference Error",
"message": "Erro ao processar previsão para AAPL",
"ticker": "AAPL",
"details": "Tensor shape mismatch",
"status": 500
}Arquivo: src/api/utils/validators.py
Valida formato de ticker de ação.
Assinatura:
def validate_ticker(ticker: str) -> Tuple[bool, str]:
"""
Returns: (is_valid, error_message)
"""Regras de Validação:
| Regra | Descrição | Exemplo Inválido |
|---|---|---|
| Tipo | Deve ser string | 123 (int) |
| Tamanho mínimo | >= 2 caracteres | "A" |
| Tamanho máximo | <= 10 caracteres | "VERYLONGNAME" |
| Formato | [A-Z][A-Z0-9.-]{1,9} |
"123ABC" (começa com número) |
| Caracteres permitidos | Letras, números, ponto, hífen | "AAPL@" (@ inválido) |
Exemplos:
# Válidos
validate_ticker("AAPL") # (True, "")
validate_ticker("PETR4.SA") # (True, "")
validate_ticker("BRK-B") # (True, "")
# Inválidos
validate_ticker("A") # (False, "Ticker deve ter entre 2 e 10 caracteres")
validate_ticker("123") # (False, "Ticker deve conter apenas letras...")
validate_ticker("") # (False, "Ticker não pode ser vazio")Fluxo de Validação:
1. Tipo string? → Se não: erro
2. Strip espaços
3. Vazio? → Se sim: erro
4. Tamanho < 2? → Se sim: erro
5. Tamanho > 10? → Se sim: erro
6. Match regex [A-Z][A-Z0-9.-]{1,9}? → Se não: erro
7. Retorna (True, "")Normaliza ticker para formato padrão.
Assinatura:
def normalize_ticker(ticker: str) -> str:
"""
Returns: ticker normalizado (uppercase, sem espaços)
"""Transformações:
.strip()- Remove espaços nas extremidades.upper()- Converte para maiúsculas
Exemplos:
normalize_ticker(" aapl ") # "AAPL"
normalize_ticker("petr4.sa") # "PETR4.SA"
normalize_ticker("BrK-b") # "BRK-B"Configuração Atual (Desenvolvimento):
CORS(app, resources={
r"/*": {
"origins": "*", # ⚠️ Permite qualquer origin
"methods": ["GET", "POST", "OPTIONS"],
"allow_headers": ["Content-Type"]
}
})Configuração Recomendada (Produção):
CORS(app, resources={
r"/*": {
"origins": [
"https://seu-frontend.com",
"https://app.seu-dominio.com"
],
"methods": ["GET", "POST"],
"allow_headers": ["Content-Type", "Authorization"],
"expose_headers": ["X-Request-ID"],
"max_age": 3600 # Cache preflight por 1 hora
}
})Implementado: ✅ Sim
Validação em POST /predict:
if not request.is_json:
return jsonify({
"error": "Invalid Content-Type",
"message": "Content-Type deve ser application/json",
"status": 400
}), 400Headers aceitos:
Content-Type: application/jsonContent-Type: application/json; charset=utf-8
Status:
Recomendação para Produção:
from flask_limiter import Limiter
from flask_limiter.util import get_remote_address
limiter = Limiter(
app,
key_func=get_remote_address,
default_limits=["200 per day", "50 per hour"],
storage_uri="redis://localhost:6379"
)
# Limites específicos por endpoint
@limiter.limit("10 per minute")
@prediction_bp.route('/predict', methods=['POST'])
def predict():
...Limites Sugeridos:
| Endpoint | Limite | Justificativa |
|---|---|---|
| /health | Ilimitado | Health checks frequentes |
| /model/info | 100/hora | Raramente muda |
| /predict | 10/minuto | Operação custosa (~470ms) |
Implementado: ✅ Sim
Validações:
- Tipo de dados (ticker deve ser string)
- Formato (regex
[A-Z][A-Z0-9.-]{1,9}) - Tamanho (2-10 caracteres)
- Normalização (uppercase, strip)
Proteção contra:
- SQL Injection: N/A (não usa SQL)
- XSS: Validação de formato previne
- Path Traversal: N/A (não acessa filesystem com input)
| Endpoint | P50 | P95 | P99 |
|---|---|---|---|
| GET /health | 5ms | 8ms | 12ms |
| GET /model/info | 10ms | 15ms | 25ms |
| POST /predict | 470ms | 650ms | 900ms |
Yahoo Finance API: 400ms (85%) [Maior gargalo]
Normalização (scaler): 5ms (1%)
Inferência LSTM: 50ms (11%)
Cálculos/Métricas: 5ms (1%)
Serialização JSON: 9ms (2%)
Overhead Flask: 1ms (<1%)
Benefício: -400ms (redução de ~85%)
import redis
redis_client = redis.Redis(host='localhost', port=6379)
def fetch_data_cached(ticker: str) -> pd.DataFrame:
cache_key = f"stock_data:{ticker}:{date.today()}"
# Tenta buscar do cache
cached = redis_client.get(cache_key)
if cached:
return pickle.loads(cached)
# Se não está no cache, busca do Yahoo Finance
df = yf.Ticker(ticker).history(...)
# Armazena no cache (expira em 1 dia)
redis_client.setex(cache_key, 86400, pickle.dumps(df))
return dfLatência Final: ~70ms
Benefício: -25ms (redução de ~50% na inferência)
import torch.quantization
# Quantizar modelo para int8
model_quantized = torch.quantization.quantize_dynamic(
model,
{torch.nn.LSTM, torch.nn.Linear},
dtype=torch.qint8
)Trade-off:
- ✅ Mais rápido (2x)
- ✅ Menor uso de memória (4x)
⚠️ Pequena perda de precisão (~0.5%)
Benefício: Processar múltiplos tickers em paralelo
def predict_batch(tickers: List[str]) -> List[dict]:
# Buscar dados em paralelo
with ThreadPoolExecutor(max_workers=5) as executor:
data_futures = {
executor.submit(fetch_data, t): t
for t in tickers
}
# Empilhar tensores
X_batch = torch.stack([
prepare_tensor(data) for data in data_list
])
# Inferência em batch
with torch.no_grad():
predictions = model(X_batch)
return process_results(predictions)| Componente | Tamanho | Lifecycle |
|---|---|---|
| Modelo LSTM | ~450MB | Persistente (singleton) |
| MinMaxScaler | ~50MB | Persistente (singleton) |
| Flask App | ~30MB | Persistente |
| Request Buffer | ~1MB | Por request |
| Total Base | ~530MB | Mínimo |
| Com 10 requests simultâneos | ~540MB | Típico |
Recomendações:
- Desenvolvimento: 1GB RAM
- Produção (baixo tráfego): 1GB RAM
- Produção (alto tráfego): 2GB RAM + scaling horizontal
Sintoma:
RuntimeError: Modelo não foi carregado corretamente
Causas Possíveis:
- Arquivo
artifacts/model_lstm_1x16.ptnão existe - Arquivo corrompido
- Permissões de leitura
Diagnóstico:
# Verificar se arquivo existe
ls -lh artifacts/model_lstm_1x16.pt
# Verificar permissões
stat artifacts/model_lstm_1x16.pt
# Verificar integridade (se tiver checksum)
shasum -a 256 artifacts/model_lstm_1x16.ptSolução:
- Re-baixar modelo do repositório
- Re-treinar modelo se necessário
- Corrigir permissões:
chmod 644 artifacts/model_lstm_1x16.pt
Sintoma:
{
"error": "Ticker Not Found",
"ticker": "AAPL"
}Causas Possíveis:
- Yahoo Finance temporariamente indisponível
- Ticker foi deslistado recentemente
- Problema de conectividade
Diagnóstico:
# Testar conectividade Yahoo Finance
curl -I https://finance.yahoo.com
# Testar ticker manualmente
curl "https://query1.finance.yahoo.com/v8/finance/chart/AAPL"Solução:
- Aguardar e tentar novamente (retry_after: 60s)
- Verificar se ticker ainda está ativo
- Verificar firewall/proxy
Sintoma:
ModelInferenceError: Tensor shape mismatch: expected [1, 60, 1], got [1, 59, 1]
Causas Possíveis:
- Yahoo Finance retornou menos de 60 dias
- Dados contêm valores NaN
- DataFrame foi filtrado incorretamente
Diagnóstico:
# Adicionar logging em data_service.py
logger.info(f"DataFrame shape: {df.shape}")
logger.info(f"NaN values: {df.isna().sum()}")Solução:
- Aumentar margem de dias (60 + 30 → 60 + 60)
- Filtrar NaN antes de processar
- Validar
len(df) >= 60antes de inferência
Sintoma: Latência alta consistente
Diagnóstico:
import time
# Adicionar timing em predict_service.py
start = time.time()
df = self.data_service.fetch_data(ticker)
logger.info(f"fetch_data: {time.time() - start:.2f}s")
start = time.time()
prediction = model(X)
logger.info(f"inference: {time.time() - start:.2f}s")Causas e Soluções:
| Causa | Solução |
|---|---|
| Yahoo Finance lento | Implementar cache Redis |
| Modelo não em eval() | Verificar model.eval() |
| CPU throttling | Aumentar recursos do servidor |
| Múltiplas requisições simultâneas | Implementar rate limiting |
Sintoma: Uso de memória cresce continuamente
Diagnóstico:
# Monitorar memória
watch -n 1 'ps aux | grep flask'
# Profiling com memory_profiler
pip install memory_profiler
python -m memory_profiler app.pyCausas Possíveis:
- Tensores não sendo liberados
- DataFrame sendo acumulado
- Cache sem limite
Solução:
# Garantir uso de torch.no_grad()
with torch.no_grad():
prediction = model(X)
# Limpar variáveis explicitamente
del X, prediction_scaled, df
torch.cuda.empty_cache() # Se usar GPU- README.md: Guia de uso e instalação
- ML_DOCUMENTATION.md: Documentação do modelo LSTM
- DEPLOY.md: Guias de deployment
- PLANO_PESSOA_B.md: Roadmap de implementação
- Flask: https://flask.palletsprojects.com/
- PyTorch: https://pytorch.org/docs/stable/index.html
- yfinance: https://github.com/ranaroussi/yfinance
- Yahoo Finance API: https://www.yahoofinanceapi.com/
- GitHub Issues: Reportar Bug
- Pull Requests: Contribuir