簡體   English   中英

觸發運行 Google Sheets 腳本以批量從 Yahoo Finance 獲取 URL 數據

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

Prelude - 這是一個相當長的帖子,但主要是因為有很多圖片來澄清我的問題:)

我一直在從 Yahoo! 提取公司數據! 金融,最初只針對幾只股票,但目前針對數百只股票(很快將達到數千只)。 我目前正在實時提取這些數據,每次加載電子表格時每個股票代碼都有一個 urlFetch。 這提出了三個問題:

  • 加載工作表需要很長時間,因為它向 Yahoo! 發出了數百個請求。
  • 我經常在 Google 方面受到速率限制(每天最多 20k 的 url 獲取)
  • 我可能會在 Yahoo! 上獲得速率限制! 財務方面

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

  • 我不會在每次加載電子表格時為每個代碼調用 Yahoo,而是運行一個谷歌腳本,每天一次為每個代碼獲取數據

考慮以下簡化的示例表(tab 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()只是返回一個數組。 實際上, yahoo()函數從 Yahoo! 中提取JSON數據。 運行腳本后,電子表格如下所示:

在此處輸入圖像描述

到目前為止,一切都很好。 但是,如果列表不是 3 個而是 5000 個代碼長,那么按原樣運行腳本會讓我快速限制速率(或者等待很長時間才能加載電子表格)。 因此,我想到了以下幾點:

  • 每次運行腳本時,它只會拉取 5 個股票代碼的數據。 該腳本將每分鍾運行一次。 (就本主題而言,每分鍾 5 個代碼)
  • 該腳本將跟蹤上次下載每個代碼的數據的時間

假設列表當前如下所示:

在此處輸入圖像描述

假設今天是 5 月 31 日,運行腳本:

在此處輸入圖像描述

從列表頂部開始,腳本應該要更新今天尚未更新的 5 個代碼:

  • 第 2 行的BLK今天已經更新,所以省略
  • AAPL最后一次更新是昨天,所以它是第一個被 Yahoo! 查詢的股票代碼。
  • 等等

現在第 2 到 9 行已更新數據。 第二次運行腳本時,它應該更新接下來的五個。 再次從頂部開始,查找前 5 個代碼(1)沒有“最后運行”日期或(2)今天之前的運行日期:

在此處輸入圖像描述

如您所見,第 11 到 15 行現在也更新了。 TSLA被跳過,因為(無論出於何種原因)它今天已經更新。

這里又是同一個列表,只是多了 2 個代碼。 如果腳本在 6 月 1 日運行幾次,結果將是這樣的:

在此處輸入圖像描述

  • 3 批 5 行情
  • 一批 2 行情

如果 Yahoo! 財務服務將始終返回每個代碼的數據。 但是,它不會。 例如因為:

  • 它沒有特定代碼的數據
  • 請求超時

我相信我需要一個解決方案來在下載數據時跟蹤錯誤。 假設腳本在 6 月 2 日再次運行幾次(由 Google 腳本中每分鍾一次的觸發器觸發),結果如下:

在此處輸入圖像描述

我們看到兩個代碼( JPMORCL )的數據無法更新。 兩者都標記在錯誤列中,由待編寫的腳本填充。

假設我們在 6 月 3 日再次運行腳本。 這一天, JPM數據正在完美下載,但ORCL再次出現錯誤。 雅虎! 沒有返回任何數據。 error列更新為2

在此處輸入圖像描述

如果代碼連續 2 次嘗試未返回數據( error = 2 ),則應永遠跳過它。 我會在某個時候手動填寫它,或者查看我是否輸入了一個不存在的代碼。

保留錯誤下載可以防止腳本卡住。 如果沒有它,如果列表頂部有 5 個不斷拋出錯誤的代碼,腳本將永遠不會超過這 5 個。它會嘗試一遍又一遍地嘗試從 Yahoo 下載這些代碼的數據。

在最后一張圖片中,我們看到了腳本在 6 月 4 日運行的結果。 我再次為每運行/分鍾更新的批次(5 個代碼)着色。

在此處輸入圖像描述

我盡力解釋了我是如何考慮從 Yahoo! 構建防錯下載的。 金融。 在我的電子表格的其余部分,每當我需要來自公司的元數據時,我都可以簡單地從這個db選項卡中獲取它,而不是查詢 Yahoo! 一遍又一遍。

我的問題是我的腳本技能有限。 我不是在監督如何開始構建它。 有人可以請:

  • 給我一個開始(偽)代碼或
  • 反饋我的想法。 我在這里遺漏了一些可能會出現問題的東西嗎?

PS。 我知道每次運行腳本時我仍在進行 5 次 urlfetches。 有人建議我將這 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] 此處的電子表格示例

[EDIT3] 示例電子表格中的當前腳本:

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] 運行腳本 4 次后的結果:

在此處輸入圖像描述

[EDIT5] BC列中的現有數據被覆蓋

在運行腳本之前: 在此處輸入圖像描述

運行腳本后: 在此處輸入圖像描述

[編輯6]:

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”列的值是日期對象。 並且,您要檢查年、月和日。
    • 在這種情況下,您希望每次運行只執行 5 次yahoo的功能。
    • yahoo發生錯誤時,您希望計算列“F”。 當沒有發生錯誤時,您希望將“F”列設置為null
  • 您想通過時間驅動的觸發器來執行腳本。
    • 您可以自己安裝時間驅動的觸發器。

當我看到你的腳本時,沒有包含實現上述目標的腳本。 並且, setValues在循環中使用。 在這種情況下,處理成本會變高。

那么,在您的情況下,為了實現您的目標,下面的示例腳本怎么樣?

示例腳本:

這個腳本可以直接用腳本編輯器運行。 因此,在您使用時間驅動觸發器運行此腳本之前,我建議您測試此腳本。

測試此腳本並確認輸出情況后,請在函數中安裝時間驅動觸發器。 這樣,當您將時間驅動觸發器安裝到此函數時,腳本將由觸發器運行。

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)實際腳本。 所以,請注意這一點。

  • 從您的問題和顯示圖像中,我了解到您想檢查年、月和日。 請注意這一點。

參考:

添加:

根據您的以下附加問題,

另外,如果我可以請您簡要看一下,我已經在示例表中添加了第二個選項卡 (db2)。 在這里,我在代碼和 yahoo() 返回的其余數據之間添加了 2 列。 假設我要在這里填寫其他數據。 是否可以調整您的腳本以使其不理會這些列,因此僅適用於 A 和 D 到 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功能都進行了修改。 而且,為了使用您提供的電子表格的第二個選項卡,工作表名稱也更改為db2 請注意這一點。

暫無
暫無

聲明:本站的技術帖子網頁,遵循CC BY-SA 4.0協議,如果您需要轉載,請注明本站網址或者原文地址。任何問題請咨詢:yoyou2525@163.com.

 
粵ICP備18138465號  © 2020-2024 STACKOOM.COM