Skip to content

Instantly share code, notes, and snippets.

@danperrout
Last active December 8, 2025 04:18
Show Gist options
  • Select an option

  • Save danperrout/b27197056fa38d0d669332647ab89d7a to your computer and use it in GitHub Desktop.

Select an option

Save danperrout/b27197056fa38d0d669332647ab89d7a to your computer and use it in GitHub Desktop.
API Função TESOURODIRETO Google Sheets
/*
* @return Acesse radaropcoes.com Retorna a cotação atual de um título específico do Tesouro Direto.
* API: https://radaropcoes.com/
* Fonte: https://www.tesourodireto.com.br/titulos/precos-e-taxas.htm
**/
function TESOURODIRETO(bondName, argumento="r") {
let srcURL = "https://api.radaropcoes.com/bonds.json";
let jsondata = UrlFetchApp.fetch(srcURL);
let parsedData = JSON.parse(jsondata.getContentText()).response;
for(let bond of parsedData.TrsrBdTradgList) {
let currBondName = bond.TrsrBd.nm;
if (currBondName.toLowerCase() === bondName.toLowerCase())
if(argumento == "r")
return bond.TrsrBd.untrRedVal;
else
return bond.TrsrBd.untrInvstmtVal;
}
throw new Error("Título não encontrado.");
}
@nozeedd
Copy link

nozeedd commented Sep 11, 2025

Era o que eu precisava. Um tanto básico, peguei a API do @gabrielgasp e importei pelo Excel > Power Query
Converter a Lista para tabela, Renomear coluna, Expandir para diversas colunas.

let Source = Json.Document(Web.Contents("https://tesouro.gabrielgaspar.com.br/bonds")), bonds1 = Source[bonds], #"Converted to Table" = Table.FromList(bonds1, Splitter.SplitByNothing(), null, null, ExtraValues.Error), #"Renamed Columns" = Table.RenameColumns(#"Converted to Table",{{"Column1", "TD"}}), #"Expanded TD" = Table.ExpandRecordColumn(#"Renamed Columns", "TD", {"name", "investable", "annual_investment_rate", "unitary_investment_value", "minimum_investment_amount", "annual_redemption_rate", "unitary_redemption_value", "maturity"}, {"TD.name", "TD.investable", "TD.annual_investment_rate", "TD.unitary_investment_value", "TD.minimum_investment_amount", "TD.annual_redemption_rate", "TD.unitary_redemption_value", "TD.maturity"}) in #"Expanded TD"

@diegodemorais
Copy link

Pessoal, funcionou perfeitamente! Muitíssimo obrigado @gabrielgasp
Fiz um pequeno ajuste pra retornar somente o Juros pós-fixado, sem o texto "Selic" ou "IPCA".

Assim:

function GET_TITULO_TESOURO(bondName) {
  const srcURL = `https://tesouro.gabrielgaspar.com.br/bonds/${bondName}`;
  const jsondata = UrlFetchApp.fetch(srcURL);

  if (jsondata.getResponseCode() !== 200) {
    throw new Error("Erro ao buscar título");
  } 

  const parsedData = JSON.parse(jsondata.getContentText());
  const rate = parsedData.bond.annual_investment_rate; // Ex: "SELIC + 0,05%"

  // Extrai o primeiro número com sinal de % da string
  const match = rate.match(/-?\d+,\d+%/);

  return match ? match[0] : "";
}

Abraços!

@mathsebastiany
Copy link

mathsebastiany commented Sep 19, 2025

Fala, pessoal!
Fiz adaptações para incluir algumas funções que eu uso na minha planilha e vou disponibilizar para vocês aqui:
txcompra(bondName) → taxa de compra (annual_investment_rate)
txvenda(bondName) → taxa de venda (annual_redemption_rate)
pucompra(bondName) → preço de compra (unitary_investment_value)
puvenda(bondName) → preço de venda (unitary_redemption_value)

function txcompra(bondName) {
  var tesouro = getCachedTesouro();
  if (!tesouro) updateTesouroCache();
  tesouro = getCachedTesouro();

  for (let bond of tesouro.bonds) {
    if (bond.name.toLowerCase() === bondName.toLowerCase()) {
      return extrairTaxaNumerica(bond.annual_investment_rate);
    }
  }

  return "Título não encontrado";
}

function txvenda(bondName) {
  var tesouro = getCachedTesouro();
  if (!tesouro) updateTesouroCache();
  tesouro = getCachedTesouro();

  for (let bond of tesouro.bonds) {
    if (bond.name.toLowerCase() === bondName.toLowerCase()) {
      return extrairTaxaNumerica(bond.annual_redemption_rate);
    }
  }

  return "Título não encontrado";
}

/**
 * Extrai apenas a parte numérica da taxa"
 */
function extrairTaxaNumerica(taxa) {
  if (!taxa) return "";
  var partes = taxa.split("+");
  return partes.length > 1 ? partes[1].trim() : taxa.trim();
}

function puvenda(bondName) {
  var tesouro = getCachedTesouro();

  if (!tesouro) {
    Logger.log("No cached tesouro info found");
    updateTesouroCache();
  }

  tesouro = getCachedTesouro();
  console.log(tesouro);

  for (let bond of tesouro.bonds) {
    console.log(bond);
    if (bond.name.toLowerCase() === bondName.toLowerCase())
      return bond.unitary_redemption_value;
  }

  return 0;
}

function pucompra(bondName) {
  var tesouro = getCachedTesouro();

  if (!tesouro) {
    Logger.log("No cached tesouro info found");
    updateTesouroCache();
  }

  tesouro = getCachedTesouro();
  console.log(tesouro);

  for (let bond of tesouro.bonds) {
    console.log(bond);
    if (bond.name.toLowerCase() === bondName.toLowerCase())
      return bond.unitary_investment_value;
  }

  return 0;
}

/**
 * Função para buscar o tesouro armazenado em cache.
 * @return {Object} O objeto do tesouro armazenado.
 */
function getCachedTesouro() {
  var cached = cache.get("tesouro");
  if (cached) {
    try {
      var tesouro = JSON.parse(cached);
      return tesouro;
    } catch (e) {
      Logger.log("Error while parsing tesouro from cache");
    }
  }
  return null;
}

/**
 * Função para atualizar o cache com as informações mais recentes do Tesouro Direto.
 */
function updateTesouroCache() {
  Logger.log("Updating tesouro cached information");

  var url = "https://tesouro.gabrielgaspar.com.br/bonds";
  var response = UrlFetchApp.fetch(url);
  console.log(JSON.parse(response.getContentText()));
  var content = response.getContentText();
  try {
    var data = JSON.parse(content);
    cache.put("tesouro", JSON.stringify(data), 21600);
  } catch (e) {
    Logger.log("Error while parsing response from tesouro: " + content);
  }
}

@hugobrilhante
Copy link

Criei essa versão que busca só o preço. Demora um pouco por conta do tamanho do arquivo mas funciona.

/**
 * Função auxiliar para converter data DD/MM/AAAA para um formato comparável (AAAA-MM-DD).
 * @param {string} dateString Data no formato DD/MM/AAAA.
 * @return {string} Data no formato AAAA-MM-DD.
 */
function parseDateForComparison(dateString) {
  if (!dateString) return '';
  const parts = dateString.split('/');
  if (parts.length === 3) {
    // Retorna YYYY-MM-DD, que pode ser comparado como string
    return `${parts[2]}-${parts[1]}-${parts[0]}`;
  }
  return '';
}

/**
 * Busca o preço de um título do Tesouro.
 *
 * @param {string} nomeDoTitulo O nome do título a ser buscado (Ex: "Tesouro IPCA+ 2029").
 * @return {number|string} O preço de investimento do título ou uma mensagem de erro.
 * @customfunction
 */
function TESOURODIRETO(nomeDoTitulo) {
  const URL_CSV =
    "https://www.tesourotransparente.gov.br/ckan/dataset/df56aa42-484a-4a59-8184-7676580c81e3/resource/796d2059-14e9-44e3-80c9-2d9e30b405c1/download/precotaxatesourodireto.csv";

  const options = {
    muteHttpExceptions: true
  };

  const termoBuscado = nomeDoTitulo.trim().toLowerCase();

  let latestMatch = null; // Armazenará {dateSortable: string, columns: array}

  try {
    const response = UrlFetchApp.fetch(URL_CSV, options);
    const csvContent = response.getContentText();

    if (response.getResponseCode() !== 200) {
      return "ERRO: Não foi possível baixar o CSV. Código: " + response.getResponseCode();
    }

    const linhas = csvContent.split('\n');

    for (let i = 1; i < linhas.length; i++) {
      const linha = linhas[i];
      if (linha.trim() === "") continue;

      const colunas = linha.split(';');

      if (colunas.length < 8) continue;

      // Colunas: [0]Tipo Titulo, [1]Data Vencimento, [2]Data Base, [5]PU Compra Manha
      const tipoTitulo = colunas[0].trim();
      const dataVencimento = colunas[1].trim();
      const dataBase = colunas[2].trim();

      const anoVencimento = dataVencimento.slice(-4);
      const nomeCompletoNoArquivo = `${tipoTitulo} ${anoVencimento}`.trim().toLowerCase();

      if (nomeCompletoNoArquivo === termoBuscado) {
        const sortableDate = parseDateForComparison(dataBase);

        if (sortableDate) {
          if (!latestMatch || sortableDate > latestMatch.dateSortable) {
            latestMatch = {
              dateSortable: sortableDate,
              columns: colunas
            };
          }
        }
      }
    }

    if (latestMatch) {
      let precoTexto = latestMatch.columns[5].trim();

      // Limpa e converte o preço para número
      precoTexto = precoTexto
        .replace("R$", "")
        .replace(/\./g, "")
        .replace(",", ".")
        .trim();

      return parseFloat(precoTexto);
    }

    return `Título "${nomeDoTitulo}" não encontrado. Verifique o Tipo Título e o Ano de Vencimento.`;

  } catch (e) {
    return "ERRO: " + e.toString();
  }
}

@nogueiradario
Copy link

nogueiradario commented Dec 8, 2025

Criei essa versão que busca só o preço. Demora um pouco por conta do tamanho do arquivo mas funciona.

/**
 * Função auxiliar para converter data DD/MM/AAAA para um formato comparável (AAAA-MM-DD).
 * @param {string} dateString Data no formato DD/MM/AAAA.
 * @return {string} Data no formato AAAA-MM-DD.
 */
function parseDateForComparison(dateString) {
  if (!dateString) return '';
  const parts = dateString.split('/');
  if (parts.length === 3) {
    // Retorna YYYY-MM-DD, que pode ser comparado como string
    return `${parts[2]}-${parts[1]}-${parts[0]}`;
  }
  return '';
}

/**
 * Busca o preço de um título do Tesouro.
 *
 * @param {string} nomeDoTitulo O nome do título a ser buscado (Ex: "Tesouro IPCA+ 2029").
 * @return {number|string} O preço de investimento do título ou uma mensagem de erro.
 * @customfunction
 */
function TESOURODIRETO(nomeDoTitulo) {
  const URL_CSV =
    "https://www.tesourotransparente.gov.br/ckan/dataset/df56aa42-484a-4a59-8184-7676580c81e3/resource/796d2059-14e9-44e3-80c9-2d9e30b405c1/download/precotaxatesourodireto.csv";

  const options = {
    muteHttpExceptions: true
  };

  const termoBuscado = nomeDoTitulo.trim().toLowerCase();

  let latestMatch = null; // Armazenará {dateSortable: string, columns: array}

  try {
    const response = UrlFetchApp.fetch(URL_CSV, options);
    const csvContent = response.getContentText();

    if (response.getResponseCode() !== 200) {
      return "ERRO: Não foi possível baixar o CSV. Código: " + response.getResponseCode();
    }

    const linhas = csvContent.split('\n');

    for (let i = 1; i < linhas.length; i++) {
      const linha = linhas[i];
      if (linha.trim() === "") continue;

      const colunas = linha.split(';');

      if (colunas.length < 8) continue;

      // Colunas: [0]Tipo Titulo, [1]Data Vencimento, [2]Data Base, [5]PU Compra Manha
      const tipoTitulo = colunas[0].trim();
      const dataVencimento = colunas[1].trim();
      const dataBase = colunas[2].trim();

      const anoVencimento = dataVencimento.slice(-4);
      const nomeCompletoNoArquivo = `${tipoTitulo} ${anoVencimento}`.trim().toLowerCase();

      if (nomeCompletoNoArquivo === termoBuscado) {
        const sortableDate = parseDateForComparison(dataBase);

        if (sortableDate) {
          if (!latestMatch || sortableDate > latestMatch.dateSortable) {
            latestMatch = {
              dateSortable: sortableDate,
              columns: colunas
            };
          }
        }
      }
    }

    if (latestMatch) {
      let precoTexto = latestMatch.columns[5].trim();

      // Limpa e converte o preço para número
      precoTexto = precoTexto
        .replace("R$", "")
        .replace(/\./g, "")
        .replace(",", ".")
        .trim();

      return parseFloat(precoTexto);
    }

    return `Título "${nomeDoTitulo}" não encontrado. Verifique o Tipo Título e o Ano de Vencimento.`;

  } catch (e) {
    return "ERRO: " + e.toString();
  }
}

Sensacional, funcionou perfeitamente, muito obrigado @hugobrilhante! e creio que sua solução vai ser duradoura. E pra quem quiser buscar outros valores é só trocar o 5 no "let precoTexto = latestMatch.columns[5].trim();" que voce vai ter outros resultados, o 5 por padrão é o valor de compra, como está comentado " [0]Tipo Titulo, [1]Data Vencimento, [2]Data Base, [5]PU Compra Manha" e acrescento que o 6 é o valor de venda. Não testei, mas creio que 3 e 4 são as taxas de compra e venda, respectivamente, vale o teste para quem quiser.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment