Análise de Sentimento com Google Gemini
Analisando o sentimento de notícias relacionadas ao mercado financeiro.
Olá pessoal,
Análise de Sentimento é uma área bastante conhecida dos algotraders. Ela consiste em extrair o sentimento de um determinado texto – por exemplo uma notícia. É fácil perceber como essa área se popularizou com o advento dos LLMs, uma vez que analisar textos é sua principal habilidade.
No episódio #004 do Code Night falei justamente como podemos usar a IA do Google, o Gemini 2.0 Flash, para realizar a análise de sentimento de notícias relacionadas a ações.
Falta muito pouco para atingirmos os 1000 inscritos no canal. Quem sabe com a sua ajuda não chegamos nessa marca antes do Natal?
Objetivo
Quando temos uma tarefa relativamente complexa, é prudente estabelecer o objetivo e realizar o breakdown dos passos necessários para atingi-lo.
Nesse caso, queremos:
Abrir um portal de notícias do mercado financeiro;
Iterar sobre suas notícias;
Identificar os ativos mencionados na notícia;
Identificar o sentimento associado a cada ativo.
Ao final desses 4 passos, seremos capazes de criar um script que automatizaria o processo.
Plano de ação
Porém, por mais que esse seja o objetivo, não significa que obrigatoriamente devemos seguir essa ordem. Pelo contrário – aqui, o ideal seria começarmos de forma mais simples.
Assim, as etapas seguem naturalmente sem que haja um salto de complexidade. Nesse caso, optei por seguir a seguinte ordem:
Identificar o sentimento associado a cada ativo mencionado em uma notícia;
A partir de 1, extrair o texto através da URL de uma notícia;
A partir de 1 e 2, extrair o sentimento de todas as notícias de um portal específico.
Começamos portanto focando exclusivamente na parte de AI, assumindo que o texto da notícia é um dado. Depois, partimos para buscar o texto através da URL específica da notícia. Por fim, pegamos a URL específica a partir de uma URL constante que contém todas as notícias.
Notem como cada etapa é uma sequência lógica da outra, e como em cada uma delas focamos em uma parte específica: AI (análise de sentimento) e scraping. Se essa tarefa fosse assinalada a um time de desenvolvedores, isso permitiria que duas pessoas diferentes trabalhassem em paralelo – uma focando na parte de AI, outra na de scraping.
Escolhendo um LLM
O primeiro passo para se extrair o sentimento de um texto usando LLM (Large Language Model) é a escolha do modelo. Existem diversos modelos, que podem variar de qualidade, preço, velocidade, limite, escopo, etc.
Sem dúvidas o mais famoso no momento que escrevo esse texto é o GPT-4o, da OpenAI, que é a base do ChatGPT. Dentre os programadores, o queridinho costuma ser o Claude 3.5 Sonnet. Hoje, no entanto, iremos utilizar o Gemini 2.0 Flash.
Qual o motivo da escolha? Em primeiro lugar, pois os resultados do modelo são comparados aos dois mencionados acima. E em segundo lugar, também no momento que escrevo, o uso desse modelo é free. Isso é extremamente relevante àqueles que pretendem operar em escala, ainda mais quando os custos de se usar um LLM são em dólar.
Tudo que você precisa é de uma API key, que pode ser conseguida direto no AI Studio. Em seguida, instale o SDK Google Gen AI.
Analisando o sentimento de um texto
Por incrível que pareça, o código para se extrair o sentimento é bem simples:
import os
from typing import Literal
import dotenv
from google import genai
from google.genai import types
from pydantic import BaseModel
dotenv.load_dotenv()
client = genai.Client(api_key=os.environ["GEMINI_API_KEY"])
class TickerSentiment(BaseModel):
ticker: str
sentiment: Literal["positive", "negative", "neutral"]
reason: str
prompt = """
PETR4 descobriu petróleo...
"""
response = client.models.generate_content(
model="gemini-2.0-flash-exp",
contents=prompt,
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=TickerSentiment,
),
)
return response.text
Além de inicializar o client
Gemini, escolher o modelo e passar o prompt
, a parte mais relevante é o uso de um modelo Pydantic para forçar a resposta a se conformar a um determinado schema.
Um dos difíceis problemas do início da popularização dos LLMs era justamente essa previsibilidade. Quando trabalhamos com sistemas, esperamos que inputs e outputs sejam bem-definidos, algo que a natureza estocástica das IAs tem dificuldade. Porém, a própria SDK do Google Gemini aceita esse parâmetro response_schema
, o que facilita em muito esse trabalho.
Com o código acima forçamos o Gemini a retornar um objeto contendo os parâmetros ticker
, sentiment
e reason
. Definimos que o sentimento pode ser apenas 1 entre os 3 valores possíveis (positivo, neutro ou negativo), e por fim solicitamos uma justificativa para a escolha. Essa justificativa pode ser utilizada para auditar o “raciocínio” feito pelo modelo, que pode ser corrigido mais tarde no prompt engineering.
Criando um prompt específico
No jargão dos LLMs, o prompt é a instrução utilizada para inicializar um modelo. Assim, fazemos com que o modelo “entenda” os seus limites e o seu objetivo, ainda que de forma imperfeita.
Combinamos o código acima com uma função que é capaz de receber um texto, um ticker, e instruir o modelo a calcular o sentimento respeitando o schema definido pelo Pydantic.
def get_news_sentiment(text: str, ticker: str) -> TickerSentiment:
prompt = f"""
You are a financial analyst. You are given a news article and a stock ticker.
You need to analyze the news article and determine the sentiment of the stock ticker.
The sentiment can be positive, negative or neutral. Only say it is positive if the news article is about the stock ticker and the news is good for the stock. Only say it is negative if the news article is about the stock ticker and the news is bad for the stock.
You need to return a JSON object with the following fields:
- ticker: the stock ticker
- sentiment: the sentiment of the stock ticker
- reason: the reason for the sentiment
The news article is:
{text}
The stock ticker is:
{ticker}
"""
response = client.models.generate_content(
model="gemini-2.0-flash-exp",
contents=prompt,
config=types.GenerateContentConfig(
response_mime_type="application/json",
response_schema=TickerSentiment,
),
)
return response.text
Existem diversas formas de se extrair o ticker de um texto. Como nosso escopo são as ações brasileiras, optei por uma simples regex:
import re
def extract_tickers(text: str) -> List[str]:
"""Extract stock tickers from text using regex pattern."""
# Pattern for Brazilian stock tickers (4-6 characters followed by 1-2 numbers)
pattern = r"\b[A-Z]{4,6}[0-9]{1,2}\b"
return list(set(re.findall(pattern, text)))
Pronto! A etapa 1 já está feita, e aqui se encerra o trabalho da IA. Já somos capazes de, recebido um texto, extrair seus tickers, e para cada ticker extraído, computar o sentimento através da função get_news_sentiment.
Extraindo o texto de uma notícia
Daqui em diante o trabalho é bastante semelhante com o que foi feito no episódio 001, onde utilizamos o Playwright para realizar o scraping de uma página da B3.
Aqui, também utilizaremos o Playwright, mas dessa vez para extrairmos o corpo da notícia. Como exemplo, o feed de notícias de ações brasileiras do TradingView foi utilizado.
Antes de iterar sobre todas as notícias, seguimos nosso plano e confirmaremos que somos capazes de calcular o sentimento para uma notícia.
O scraping é tão simples quanto:
def extract_news_text(url: str) -> str:
with sync_playwright() as p:
browser = p.chromium.launch()
page = browser.new_page()
page.goto(url)
text = page.content()
soup = BeautifulSoup(text, "html.parser")
browser.close()
# get element with tag <article> and parse with BeautifulSoup
article = soup.find("article")
return article.get_text()
Por acaso, tanto o TradingView quanto o Infomoney utilizam a tag HTML article
para renderizar seus textos. Isso simplifica nossa vida, pois podemos simplesmente extrair o texto dessa tag.
Note que o texto não necessariamente virá perfeito, isolado. Pode ter outro conteúdo, ou diferenças de espaçamento e pontuação. No entanto, LLMs são especialmente poderosas em entender esse tipo de texto.
Podemos unir o que fizemos até agora nessa função principal:
def get_sentiments_for_url(url: str) -> List[TickerSentiment]:
print(f"Getting sentiments for url: {url}")
text = extract_news_text(url)
tickers = extract_tickers(text)
print(f"Found tickers: {tickers}")
sentiments = []
for ticker in tickers:
sentiment = get_news_sentiment(text, ticker)
sentiments.append(sentiment)
print(f"Sentiment for {ticker}: {sentiment}")
return sentiments
Ou seja: recebemos uma URL, extraímos o texto, identificamos os tickers, iteramos sobre cada ticker computando o sentimento.
Estando tudo funcionando, agora sim podemos automatizar o processo, passando a página de notícias do TradingView e iterando sobre cada notícia. Para cada notícia, chamaremos então a função get_sentiments_for_url.
O código completo pode ser consultado no GitHub.
Com o código acima, somos capazes de automatizar a identificação de sentimentos de notícias vindas de diferentes provedores (no vídeo mostro como adaptar para o Investing.com). Assim, seria possível compilar o sentimento “médio” para um determinado ativo, de acordo com várias fontes.
A análise de sentimento por si só não significa muita coisa. Mas tendo essa informação podemos trabalhar em backtests de estratégias de trend following ou mean reversion, na esteira da percepção geral do mercado.
Esse tema, embora fascinante, fica para outro vídeo.
Abraços,
Rafael