Trigger-运行 Google Sheets 脚本从 Yahoo Finance 批量获取 URL 数据

Trigger-run a Google Sheets script to fetch URL data from Yahoo Finance in batches

序曲 - 这很长 post 但主要是因为有很多图片可以阐明我的问题:)

我一直在从 Yahoo! 中提取公司数据金融,最初只针对少数股票,但目前针对数百只股票(很快就会达到数千只)。我目前正在实时提取这些数据,每次我加载价差 sheet 时,每个股票代码都会提取一个 url。这带来了三个问题:

因此我正在寻找更好的方法:

考虑以下简化示例 sheet(选项卡 db):

当前脚本为:

function trigger() {
  const db = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('db');
  const tickers = db.getRange('A2:A' + db.getLastRow()).getValues().flat();

  for (var row = 0; row < tickers.length; row++) {
    var data = yahoo(tickers[row]);
    db.getRange(row + 2, 2, 1, 3).setValues(data);
  }
}

function yahoo(ticker) {
  return [ticker, "SOME", "DATA", "MORE"]
}

请注意,出于测试目的,yahoo() 只是 return 一个数组。实际上,yahoo() 函数从 Yahoo! 中提取 JSON 数据。在 运行 脚本之后,传播sheet 看起来像:

到目前为止一切顺利。但是,如果列表不是 3,而是 5000 个代码,运行按原样设置脚本将使我快速限制速率(或者等待传播sheet 加载很长时间)。因此,我想到了以下几点:

假设列表当前如下所示:

假设今天是5月31日,脚本是运行:

从列表顶部开始,脚本应该要更新今天尚未更新的 5 个代码:

现在第 2 行到第 9 行已更新数据。第二次脚本是 运行,它应该更新接下来的五个。再次从顶部开始,寻找前 5 个代码(1)没有“最后 运行”日期或(2)今天之前的 运行 日期:

如您所见,第 11 至 15 行现在也已更新。 TSLA 被跳过了,因为(不管什么原因)今天已经更新了。

这里又是同一个列表,只是多了 2 个代码。如果脚本在6月1日运行几次,结果会是这样:

如果 Yahoo!财务服务始终会为每个代码提供 return 数据。然而,它不会。例如因为:

我认为我需要一个解决方案来跟踪下载数据时的错误。假设脚本在 6 月 2 日再次 运行 几次(由 Google 脚本中每分钟一次的触发器触发),并得到以下结果:

我们看到两个代码(JPMORCL)无法更新数据。两者都在错误栏中标出,由待编写的脚本填写。

假设我们在 6 月 3 日再次 运行 脚本。这一天,JPM 数据下载完美,但 ORCL 再次产生错误。雅虎!没有 return 任何数据。 error 列更新为 2

如果代码连续 2 次尝试 (error = 2) 未获取数据 return,则应永远跳过它。我会在某个时候手动填写,或者查看我是否输入了一个不存在的代码。

保留错误下载的地址可以防止脚本卡住。没有它,如果列表顶部有 5 个代码不断抛出错误,脚本将永远不会超出这 5 个。它将尝试为这些代码一遍又一遍地尝试从 Yahoo 下载数据。

在最后一张图片中,我们看到脚本在 6 月 4 日 运行 的结果。我再次为每 运行/ 分钟更新的批次(5 个代码)着色。

我已尽力解释我是如何考虑从 Yahoo! 构建防错下载的。金融。在我传播的其余部分sheet,每当我需要来自公司的元数据时,我可以简单地从这个 db 选项卡中获取它,而不是查询 Yahoo!一遍又一遍。

我的问题是我的脚本技能有限。我没有监督如何开始构建它。有人可以吗:

PS。我知道每次脚本 运行 时,我仍在进行 5 次 url 提取。有人向我建议我应该将这 5 个批处理在一起(这至少可以防止我在 Google 方面受到速率限制)。这是一个好主意,但我发现很难理解它是如何工作的,所以我宁愿首先有一个可以工作并且我可以遵循的脚本。在后期,我一定会升级/使其更高效:)

如果您一直阅读到这里,非常感谢。非常感谢任何帮助!

[EDIT1]:实际上,yahoo() 看起来像这样:

function yahoo(ticker) {
  const url = 'https://query2.finance.yahoo.com/v10/finance/quoteSummary/' + encodeURI(ticker) + '?modules=price,assetProfile,summaryDetail';
  
  let response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
  if (response.getResponseCode() == 200) {
      var object = JSON.parse(response.getContentText());
  }

  let fwdPE  = object.quoteSummary.result[0]?.summaryDetail?.forwardPE?.fmt || '-';
  let sector = object.quoteSummary.result[0]?.assetProfile?.sector || '-';
  let mktCap = object.quoteSummary.result[0]?.price?.marketCap?.fmt || '-';

  return [[fwdPE, sector, mktCap]];
}

[EDIT2] Example spreadsheet here.

[EDIT3] 示例中的当前脚本传播sheet:

function trigger() {
  const max = 5; // From your question, maximum execution of "yahoo" is 5.

  const today = Utilities.formatDate(new Date(), Session.getScriptTimeZone(), "yyyyMMdd");
  const db    = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('db');
  const range = db.getRange('A2:F' + db.getLastRow());
  
  const { values } = range.getValues().reduce((o, r) => {
    const [ticker, b, c, d, e, f] = r;
    if (o.c < max && (e.toString() == "" || Utilities.formatDate(e, Session.getScriptTimeZone(), "yyyyMMdd") != today)) {
      try {
        o.c++;
        o.values.push([...yahoo(ticker), today, null]);
      } catch (_) {
        o.values.push([ticker, b, c, d, today, ["", "0"].includes(f.toString()) ? 1 : f + 1]);
      }
    } else {
      o.values.push(r);
    }
    return o;
  }, { values: [], c: 0 });
  range.setValues(values);
}


function yahoo(ticker) {
  const url = 'https://query2.finance.yahoo.com/v10/finance/quoteSummary/' + encodeURI(ticker) + '?modules=price,assetProfile,summaryDetail';
  
  let response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
  if (response.getResponseCode() == 200) {
      var object = JSON.parse(response.getContentText());
  }

  let fwdPE  = object.quoteSummary.result[0]?.summaryDetail?.forwardPE?.fmt || '-';
  let sector = object.quoteSummary.result[0]?.assetProfile?.sector || '-';
  let mktCap = object.quoteSummary.result[0]?.price?.marketCap?.fmt || '-';

  return [[ticker, fwdPE, sector, mktCap]];
}

[EDIT4] 运行ning 脚本 4 次后的结果:

[EDIT5] BC 列中的现有数据被覆盖

在 运行 脚本之前:

在 运行 脚本之后:

[EDIT6]:

function trigger() {
  const max = 5; // From your question, maximum execution of "yahoo" is 5.

  const todayObj = new Date();
  const today = Utilities.formatDate(todayObj, Session.getScriptTimeZone(), "yyyyMMdd");
  const db = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('db2');
  const range = db.getRange('A2:AO' + db.getLastRow());

  const { values } = range.getValues().reduce((zo, zr) => {
    const [ticker, b, c, d, e, f, g, h, i, j, k, l, m, n, r, o, p, q, r, s, t, u, v, w, x, y, z, aa, ab, ac, ad, ae, af, ag, ah, ai, aj, ak, al, am, an, ao] = zr;
    if (zo.zc < max && (g.toString() == "" || Utilities.formatDate(an, Session.getScriptTimeZone(), "yyyyMMdd") != today)) {
      try {
        zo.zc++;
        zo.values.push([ticker, b, c, ...yahoo(ticker), todayObj, null]);
      } catch (_) {
        zo.values.push([ticker, b, c, d, e, f, g, h, i, j, k, l, m, n, r, o, p, q, r, s, t, u, v, w, x, y, z, aa, ab, ac, ad, ae, af, ag, ah, ai, aj, ak, al, am, todayObj, ["", "0"].includes(an.toString()) ? 1 : ao + 1]);
      }
    } else {
      zo.values.push(zr);
    }
    return zo;
  }, { values: [], zc: 0 });
  range.setValues(values);
}


function yahoo(ticker) {
  const url = 'https://query2.finance.yahoo.com/v10/finance/quoteSummary/' + encodeURI(ticker) + '?modules=summaryDetail,financialData,defaultKeyStatistics';

  let response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
  if (response.getResponseCode() == 200) {
    var object = JSON.parse(response.getContentText());
  }

  // misc
  let marketCap               = object.quoteSummary.result[0]?.summaryDetail?.marketCap?.raw                       || '-';
  let dividendRate            = object.quoteSummary.result[0]?.summaryDetail?.dividendRate?.raw                    || '-';
  let dividendYield           = object.quoteSummary.result[0]?.summaryDetail?.dividendYield?.raw                   || '-';
  let payoutRatio             = object.quoteSummary.result[0]?.summaryDetail?.payoutRatio?.raw                     || '-';
  let fiveYAvgDivYield        = object.quoteSummary.result[0]?.summaryDetail?.fiveYearAvgDividendYield?.raw        || '-';
  let insidersPercentHeld     = object.quoteSummary.result[0]?.majorHoldersBreakdown?.insidersPercentHeld?.raw     || '-';
  let institutionsPercentHeld = object.quoteSummary.result[0]?.majorHoldersBreakdown?.institutionsPercentHeld?.raw || '-';
  
  // dates
  let earningsDate            = object.quoteSummary.result[0]?.calendarEvents?.earnings?.earningsDate[0]?.raw      || '-';
  let exDividendDate          = object.quoteSummary.result[0]?.calendarEvents?.exDividendDate?.raw                 || '-';
  let dividendDate            = object.quoteSummary.result[0]?.calendarEvents?.dividendDate?.raw                   || '-';

  // earnings
  let totalRevenue            = object.quoteSummary.result[0]?.financialData?.totalRevenue?.raw                    || '-';
  let revenueGrowth           = object.quoteSummary.result[0]?.financialData?.revenueGrowth?.raw                   || '-';
  let revenuePerShare         = object.quoteSummary.result[0]?.financialData?.revenuePerShare?.raw                 || '-';
  let ebitda                  = object.quoteSummary.result[0]?.financialData?.ebitda?.raw                          || '-';
  let grossProfits            = object.quoteSummary.result[0]?.financialData?.grossProfits?.raw                    || '-';
  let earningsGrowth          = object.quoteSummary.result[0]?.financialData?.earningsGrowth?.raw                  || '-';
  let grossMargins            = object.quoteSummary.result[0]?.financialData?.grossMargins?.raw                    || '-';
  let ebitdaMargins           = object.quoteSummary.result[0]?.financialData?.ebitdaMargins?.raw                   || '-';
  let operatingMargins        = object.quoteSummary.result[0]?.financialData?.operatingMargins?.raw                || '-';
  let profitMargins           = object.quoteSummary.result[0]?.financialData?.profitMargins?.raw                   || '-';

  // cash
  let totalCash               = object.quoteSummary.result[0]?.financialData?.totalCash?.raw                       || '-';
  let freeCashflow            = object.quoteSummary.result[0]?.financialData?.freeCashflow?.raw                    || '-';
  let opCashflow              = object.quoteSummary.result[0]?.financialData?.operatingCashflow?.raw               || '-';
  let cashPerShare            = object.quoteSummary.result[0]?.financialData?.totalCashPerShare?.raw               || '-';

  // debt
  let totalDebt               = object.quoteSummary.result[0]?.financialData?.totalDebt?.raw                       || '-';
  let debtToEquity            = object.quoteSummary.result[0]?.financialData?.debtToEquity?.raw                    || '-';

  // ratios
  let quickRatio              = object.quoteSummary.result[0]?.financialData?.quickRatio?.raw                      || '-';
  let currentRatio            = object.quoteSummary.result[0]?.financialData?.currentRatio?.raw                    || '-';
  let trailingEps             = object.quoteSummary.result[0]?.defaultKeyStatistics?.trailingEps?.raw              || '-';
  let forwardEps              = object.quoteSummary.result[0]?.defaultKeyStatistics?.forwardEps?.raw               || '-';
  let pegRatio                = object.quoteSummary.result[0]?.defaultKeyStatistics?.pegRatio?.raw                 || '-';
  let priceToBook             = object.quoteSummary.result[0]?.defaultKeyStatistics?.priceToBook?.raw              || '-';
  let returnOnAssets          = object.quoteSummary.result[0]?.financialData?.returnOnAssets?.raw                  || '-';
  let returnOnEquity          = object.quoteSummary.result[0]?.financialData?.returnOnEquity?.raw                  || '-';

  let enterpriseValue         = object.quoteSummary.result[0]?.defaultKeyStatistics?.enterpriseValue?.raw          || '-';
  let bookValue               = object.quoteSummary.result[0]?.defaultKeyStatistics?.bookValue?.raw                || '-';

  return [
    marketCap, dividendRate, dividendYield, payoutRatio, fiveYAvgDivYield, insidersPercentHeld, institutionsPercentHeld,
    earningsDate, exDividendDate, dividendDate,
    totalRevenue, revenueGrowth, revenuePerShare, ebitda, grossProfits, earningsGrowth, grossMargins, ebitdaMargins, operatingMargins, profitMargins,
    totalCash, freeCashflow, opCashflow, cashPerShare,
    totalDebt, debtToEquity,
    quickRatio, currentRatio, trailingEps, forwardEps, pegRatio, priceToBook, returnOnAssets, returnOnEquity,
    enterpriseValue, bookValue
  ];
}

我相信你的目标如下。

  • 通过从“A”列检索值,您想要 运行 函数 yahoo,并希望将“B”列更新为“F”。
    • 通过检查“E”栏的日期,你想执行yahoo的功能。
      • 从您的问题和显示的图片来看,“E”列的值是日期对象。而且,你要查看年月日。
    • 在这种情况下,您希望每个 运行ning 仅执行 5 次 yahoo 的函数。
    • yahoo发生错误时,您想对“F”列进行计数。当没有错误发生时,您想将 null 设置为“F”列。
  • 您想通过 time-driven 触发器执行脚本。
    • 您可以自行安装time-driven触发器。

当我看到你的脚本时,没有包含实现上述目标的脚本。并且, setValues 用于循环。这样的话,工艺成本就会变高

那么,在您的情况下,为了实现您的目标,下面的示例脚本怎么样?

示例脚本:

这个脚本可以直接用脚本编辑器运行。所以,在你 运行 这个脚本使用 time-driven 触发器之前,我想推荐测试这个脚本。

在您测试该脚本并确认输出情况后,请为该函数安装time-driven触发器。这样,当您将 time-driven 触发器安装到此函数时,脚本将被触发器 运行。

function trigger() {
  const max = 5; // From your question, maximum execution of "yahoo" is 5.

  const todayObj = new Date();
  const today = Utilities.formatDate(todayObj, Session.getScriptTimeZone(), "yyyyMMdd");
  const db = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('Sheet1');
  const range = db.getRange('A2:F' + db.getLastRow());

  const { values } = range.getValues().reduce((o, r) => {
    const [ticker, b, c, d, e, f] = r;
    if (o.c < max && (e.toString() == "" || Utilities.formatDate(e, Session.getScriptTimeZone(), "yyyyMMdd") != today)) {
      try {
        o.c++;
        o.values.push([...yahoo(ticker), todayObj, null]);
      } catch (_) {
        o.values.push([ticker, b, c, d, todayObj, ["", "0"].includes(f.toString()) ? 1 : f + 1]);
      }
    } else {
      o.values.push(r);
    }
    return o;
  }, { values: [], c: 0 });
  range.setValues(values);
}


function yahoo(ticker) {
  const url = 'https://query2.finance.yahoo.com/v10/finance/quoteSummary/' + encodeURI(ticker) + '?modules=price,assetProfile,summaryDetail';

  let response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
  if (response.getResponseCode() == 200) {
    var object = JSON.parse(response.getContentText());
  }

  let fwdPE = object.quoteSummary.result[0]?.summaryDetail?.forwardPE?.fmt || '-';
  let sector = object.quoteSummary.result[0]?.assetProfile?.sector || '-';
  let mktCap = object.quoteSummary.result[0]?.price?.marketCap?.fmt || '-';

  return [ticker, fwdPE, sector, mktCap];
}
  • 当这个脚本是运行的时候,我想你的上述目标或许可以实现。所以,

  • 根据您的实际 yahoo,返回的值与您的第一个脚本不同。所以,我也修改了一下。

注:

  • 遗憾的是,我无法想象yahoo(ticker)的真实剧本。所以,为了检查错误,我使用了try-catch。 在这种情况下,假设当未检索到值时,yahoo(ticker) 中发生错误。请注意这一点。

  • 我无法理解你的 yahoo(ticker) 的实际脚本。所以,请注意这一点。

  • 从你的问题和展示的图片来看,我了解到你想查看年月日。请注意这一点。

参考:

已添加:

根据您的以下附加问题,

also, I have added to the example sheet a second tab (db2) if I could ask you to have a brief look. Here, I have added 2 columns in between ticker and the rest of the data that yahoo() is returning. Assume that I want to fill in other data here. Would it be possible to adjust your script so that it leaves these columns alone, so only works on columns A and D to H?

我了解到您想将空的 2 列“B”和“C”添加到结果数组。在这种情况下,请测试以下示例脚本。

示例脚本:

function trigger() {
  const max = 5; // From your question, maximum execution of "yahoo" is 5.

  const todayObj = new Date();
  const today = Utilities.formatDate(todayObj, Session.getScriptTimeZone(), "yyyyMMdd");
  const db = SpreadsheetApp.getActiveSpreadsheet().getSheetByName('db2');
  const range = db.getRange('A2:H' + db.getLastRow());

  const { values } = range.getValues().reduce((o, r) => {
    const [ticker, b, c, d, e, f, g, h] = r;
    if (o.c < max && (g.toString() == "" || Utilities.formatDate(g, Session.getScriptTimeZone(), "yyyyMMdd") != today)) {
      try {
        o.c++;
        o.values.push([ticker, b, c, ...yahoo(ticker), todayObj, null]);
      } catch (_) {
        o.values.push([ticker, b, c, d, e, f, todayObj, ["", "0"].includes(f.toString()) ? 1 : h + 1]);
      }
    } else {
      o.values.push(r);
    }
    return o;
  }, { values: [], c: 0 });
  range.setValues(values);
}


function yahoo(ticker) {
  const url = 'https://query2.finance.yahoo.com/v10/finance/quoteSummary/' + encodeURI(ticker) + '?modules=price,assetProfile,summaryDetail';

  let response = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
  if (response.getResponseCode() == 200) {
    var object = JSON.parse(response.getContentText());
  }

  let fwdPE = object.quoteSummary.result[0]?.summaryDetail?.forwardPE?.fmt || '-';
  let sector = object.quoteSummary.result[0]?.assetProfile?.sector || '-';
  let mktCap = object.quoteSummary.result[0]?.price?.marketCap?.fmt || '-';

  return [fwdPE, sector, mktCap];
}
  • triggeryahoo 函数都已修改。而且,为了使用您提供的 Spreadsheet 的第二个选项卡,sheet 名称也更改为 db2。请注意这一点。