Notionで保有銘柄を管理しているが、株価の更新だけは毎回手動だった。
最初はPythonで自動化していた。Yahoo FinanceのAPIから株価を取得し、Notion APIでDBを更新する仕組みだ。
ただ、Macの環境では定期実行が安定しなかった。
cronを試してみたがうまくいかず、結局手動実行に戻ってしまっていた。
そこでGASに移行することにした。
時間トリガーで毎朝自動実行でき、コードはGoogleのサーバーで動くのでMacを開く必要がない。
Notion APIキーもスクリプトプロパティで管理できるため、コードに直書きしなくて済む。
この記事ではYahoo Finance → GAS → Notionの流れで、保有銘柄DBの「価格」と「価格取得日」を毎朝自動更新する仕組みを紹介する。
実際に使っているコードも全文公開する。
動作は自己責任でお願いしたい。
- Notion で保有銘柄を管理していて、株価の更新が毎回手間になっている
- GAS と Notion API を連携させたいが、やり方がわからない
- API キーをコードに直書きせず安全に管理したい
- Python で自動化を試みたが、定期実行が安定しなかった
1. やりたいこと、そしてなぜPythonからGASにしたか
やりたいことはシンプルで、Notion の銘柄管理 DB にある「価格」「価格取得日」プロパティを毎朝自動で更新したい。
以前は Python で書いていたが、2 つの問題があった。
1 つ目は APIキーのハードコード。
Notion のインテグレーションキーをコードに直書きしていて、GitHub に上げるたびに気になっていた。
セキュリティ的に良くないからだ。
2 つ目は 手動実行。
スクリプトを動かすには MacBook を開いて `python` コマンドを叩く必要があり、忘れたらそのまま放置になっていた。
GAS に移行すると、どちらも解決できる。
APIキーは `PropertiesService` という GAS 標準の仕組みで管理できる。
コードには `getProperty(‘NOTION_API_KEY’)` と書くだけで、実際のキーの値は GAS の管理画面(スクリプトプロパティ)に登録する。
コードを GitHub に上げてもキーは別の場所にあるので、漏れる心配がない。
実行は時間トリガーを設定すれば毎朝自動で動く。
コードは Google のサーバーで動くので、手元のマシンを開く必要すらない。
2. 仕組みの全体像(Yahoo Finance → GAS → Notion)
処理の流れはこうなっている。
- Notion DB をクエリ
保有銘柄の一覧をページネーション付きで全件取得する - Yahoo Finance API で株価を取得
ティッカーシンボルごとに直近価格を取得する(市場オープン中は現在値、クローズ後は終値) - Notion DB を更新
取得した価格と日付を各ページの `価格` / `価格取得日` プロパティに書き込む
Yahoo Finance の内部 API(`query1.finance.yahoo.com/v8/finance/chart/`)は認証不要で使える。
米国株(`AAPL` 等)も日本株(`7203.T` 等)も同じエンドポイントで対応できるため、コードの分岐がいらない。
なお、このエンドポイントは非公式のため、仕様変更や取得失敗の可能性がある。
個人で保有銘柄を毎朝1回更新する程度であれば、基本的には無料枠内に収まる想定だ。
GAS の `UrlFetchApp` は 1 日 20,000 回まで無料、Notion API は 1 秒 3 リクエストまで無料、Yahoo Finance の内部 API は制限が緩めとなっている。
なお、Notion DB 側には以下の 3 つのプロパティが必要になる。
| プロパティ名 | 型 |
|---|---|
| ティッカーシンボル | テキスト |
| 価格 | 数値 |
| |価格取得日 | 日付 |
日本株は `7203.T` のように末尾に `.T` を付ける。米国株は `AAPL` や `MSFT` のようなティッカーをそのまま入れればいい。
プロパティ名を変える場合は `config.js` の `PROPERTY_KEYS` を合わせて書き換えればいい。
3. コード全文(5ファイル)
GAS プロジェクトは 5 ファイル構成で作っている。
それぞれの役割を簡単に説明してからコードを載せる。
config.js ― 定数をまとめて管理
API のエンドポイント、プロパティ名、リトライ設定などを 1 ファイルに集約している。
ここを変えるだけで別の API に切り替えられる。
DB IDとAPIキーはスクリプトプロパティで管理するため、コードには含めない。
const CONFIG = {
NOTION_VERSION: '2022-06-28',
NOTION_API_BASE: 'https://api.notion.com/v1',
YAHOO_CHART_ENDPOINT: 'https://query1.finance.yahoo.com/v8/finance/chart/',
YAHOO_RANGE: '5d',
YAHOO_INTERVAL: '1d',
PROPERTY_KEYS: {
TICKER: 'ティッカーシンボル',
PRICE: '価格',
PRICE_DATE: '価格取得日'
},
RETRY_COUNT: 3,
RETRY_WAIT_MS: 2000,
NOTION_THROTTLE_MS: 400
};
main.js ― エントリポイント
実行するのはこの関数だけ。
Notion から銘柄一覧を取得し、ティッカーごとに株価を取得して更新する。
1 件ごとに 400ms の待機を入れているのは Notion API の 3 req/sec 制限を確実に避けるためだ。
function syncStockPrices() {
var pages = queryAllPages(getNotionDatabaseId_());
console.log('対象ページ数: ' + pages.length);
var ok = 0;
var ng = 0;
pages.forEach(function(page) {
var ticker = extractTicker(page);
if (!ticker) {
console.warn('ティッカー取得失敗: ' + page.id);
ng++;
return;
}
try {
var result = fetchLatestClose(ticker);
var props = buildPriceProperties(result.price, result.time);
updatePageProperty(page.id, props);
console.log(ticker + ': ' + result.price + ' を更新');
ok++;
} catch (e) {
console.error(ticker + ' でエラー: ' + e.message);
ng++;
}
Utilities.sleep(CONFIG.NOTION_THROTTLE_MS);
});
console.log('done: ok=' + ok + ', ng=' + ng);
}
yahoo.js ― Yahoo Finance API で株価取得
`5d` レンジで取得して `regularMarketPrice` を使う。
5 日分取ることで週末や祝日でも値を拾える。
取得される価格は実行タイミングによって変わる。
市場がオープン中なら現在値に近い値、クローズ後なら終値相当の値として扱える。
失敗したときは最大 3 回まで自動でリトライする仕様とした。
function fetchLatestClose(ticker) {
const url = CONFIG.YAHOO_CHART_ENDPOINT + encodeURIComponent(ticker)
+ '?interval=' + CONFIG.YAHOO_INTERVAL
+ '&range=' + CONFIG.YAHOO_RANGE;
var lastErr = null;
for (var attempt = 0; attempt < CONFIG.RETRY_COUNT; attempt++) {
try {
var res = UrlFetchApp.fetch(url, { muteHttpExceptions: true });
var code = res.getResponseCode();
if (code < 200 || code >= 300) {
throw new Error('Yahoo HTTP ' + code + ': ' + res.getContentText().slice(0, 200));
}
var json = JSON.parse(res.getContentText());
var result = json.chart && json.chart.result && json.chart.result[0];
if (!result || !result.meta) {
throw new Error('Yahoo レスポンス形式不正: ' + ticker);
}
var price = result.meta.regularMarketPrice;
var time = result.meta.regularMarketTime;
if (typeof price !== 'number' || typeof time !== 'number') {
throw new Error('regularMarketPrice / regularMarketTime が取れない: ' + ticker);
}
return { price: price, time: time };
} catch (e) {
lastErr = e;
if (attempt < CONFIG.RETRY_COUNT - 1) {
Utilities.sleep(CONFIG.RETRY_WAIT_MS);
}
}
}
throw lastErr;
}
notion.js ― Notion API ラッパー
GAS には Notion の公式 SDK がないため、`UrlFetchApp` で REST API を直接叩く。
APIキーは `PropertiesService`で管理しているので、コードにシークレットが含まれない。
`queryAllPages` はページネーションに対応しており、銘柄数が 100 件を超えても全件取得できる。
function getNotionDatabaseId_() {
const databaseId = PropertiesService.getScriptProperties().getProperty('NOTION_DATABASE_ID');
if (!databaseId) {
throw new Error('NOTION_DATABASE_ID がスクリプトプロパティに未設定です。GAS UI → プロジェクト設定 → スクリプトプロパティで登録してください。');
}
return databaseId;
}
function getNotionToken_() {
const token = PropertiesService.getScriptProperties().getProperty('NOTION_API_KEY');
if (!token) {
throw new Error('NOTION_API_KEY がスクリプトプロパティに未設定です。GAS UI → プロジェクト設定 → スクリプトプロパティで登録してください。');
}
return token;
}
function notionHeaders_() {
return {
'Authorization': 'Bearer ' + getNotionToken_(),
'Notion-Version': CONFIG.NOTION_VERSION,
'Content-Type': 'application/json'
};
}
function queryAllPages(databaseId) {
const url = CONFIG.NOTION_API_BASE + '/databases/' + databaseId + '/query';
const results = [];
let startCursor = null;
do {
const payload = { page_size: 100 };
if (startCursor) payload.start_cursor = startCursor;
const res = UrlFetchApp.fetch(url, {
method: 'post',
headers: notionHeaders_(),
payload: JSON.stringify(payload),
muteHttpExceptions: true
});
const code = res.getResponseCode();
if (code < 200 || code >= 300) {
throw new Error('Notion query 失敗 (' + code + '): ' + res.getContentText());
}
const json = JSON.parse(res.getContentText());
(json.results || []).forEach(function(p) { results.push(p); });
startCursor = json.has_more ? json.next_cursor : null;
} while (startCursor);
return results;
}
function updatePageProperty(pageId, properties) {
const url = CONFIG.NOTION_API_BASE + '/pages/' + pageId;
const res = UrlFetchApp.fetch(url, {
method: 'patch',
headers: notionHeaders_(),
payload: JSON.stringify({ properties: properties }),
muteHttpExceptions: true
});
const code = res.getResponseCode();
if (code < 200 || code >= 300) {
throw new Error('Notion update 失敗 (' + code + '): ' + res.getContentText());
}
}
utils.js ― ティッカー抽出とプロパティ組み立て
Notion のプロパティ構造から値を取り出す処理と、更新用オブジェクトを作る処理を分離している。
`extractTicker` は値が取れなかった場合に `null` を返すので、`main.js` 側でスキップ判定できる。
function extractTicker(page) {
const richText = page &&
page.properties &&
page.properties[CONFIG.PROPERTY_KEYS.TICKER] &&
page.properties[CONFIG.PROPERTY_KEYS.TICKER].rich_text;
if (!richText || richText.length === 0) return null;
const content = richText[0].text && richText[0].text.content;
if (!content) return null;
return content.trim();
}
function buildPriceProperties(price, unixTime) {
const iso = new Date(unixTime * 1000).toISOString();
const props = {};
props[CONFIG.PROPERTY_KEYS.PRICE] = { number: price };
props[CONFIG.PROPERTY_KEYS.PRICE_DATE] = { date: { start: iso } };
return props;
}4. セットアップ手順と動作確認
GAS エディタの画面操作だけで完結する。コマンドラインは不要だ。
1. GAS プロジェクトを作成してコードを貼り付ける
[Google Apps Script](https://script.google.com) を開き、「新しいプロジェクト」でプロジェクトを作成する。
プロジェクト名は `NotionStockSync` など、わかりやすいものにしておく。
デフォルトで `コード.gs` が 1 つ作成されているので、これを `config` にリネームして `config.js` のコードを貼り付ける。
続けて「ファイルを追加」→「スクリプト」で `main`・`yahoo`・`notion`・`utils` の 4 ファイルを追加し、それぞれ対応するコードを貼り付ける。
ファイルが揃ったら保存する。

2. Notion API キーを登録する
左メニュー「プロジェクトの設定」→「スクリプトプロパティ」を開く。
以下の 2 つを登録する。
| プロパティ名 | 値 |
|---|---|
| NOTION_API_KEY | Notion インテグレーションのシークレットトークン |
| NOTION_DATABASE_ID | 対象 DB の ID (URL の `notion.so/` 以降のハイフンなし32文字) |

3. 時間トリガーを設定する
左メニュー「トリガー」→「トリガーを追加」を開く。
| 設定項目 | 値 |
|---|---|
| 実行する関数 | syncStockPrices |
| イベントのソース | 時間主導型 |
| タイプ | 日付ベースのタイマー |
| 時刻 | 任意の時間帯 |

4. 動作確認
設定が終わったら、エディタ上で関数名 `syncStockPrices` を選んで「実行」を押す。
実行ログに 下の画像のように表示されれば成功だ。

Notion の DB を開いて `価格` / `価格取得日` が今日付で更新されていれば完成。
【参考情報】
コードをローカルで管理したい場合は [clasp](https://github.com/google/clasp) というツールを使う方法もある。
5. 注意点
Yahoo Finance API は非公式
この記事で使っているエンドポイントは認証不要で便利だが、公式に安定動作が保証された API ではない。
仕様変更や一時的な取得失敗はあり得るため、動かなくなったときは代替手段を検討する必要がある。
Notion API にはレート制限がある
Notion API には 1 秒あたり 3 リクエストの制限がある。
`main.js` では 1 件ごとに `Utilities.sleep(CONFIG.NOTION_THROTTLE_MS)`(400ms)を入れて制限を避けている。
銘柄数が多い場合は待機時間を長めに調整するとよい。
価格取得日は UTC で入る
`utils.js` の `buildPriceProperties` で `new Date(unixTime * 1000).toISOString()` を使っているため、Notion に渡す日付は UTC 形式になる。
日本時間で見ると「前日付」になるケースがある点には注意してほしい。
日本時間に合わせたい場合はタイムゾーンを考慮した変換が必要になる。
6. まとめ
GAS に移行してから、Notion を開くと株価が更新されている状態になった。
地味だが、これが思ったより便利だ。
気になったときに手動で `syncStockPrices` を実行すれば即時更新もできるので、
「今の直近価格をすぐ確認したい」という場面にも対応できる。
Python でも同じことはできるが、Mac のスリープ問題やキー管理の手間を考えると、
GAS のほうが「放っておいても動く」という点でずっと楽になった。
同じようにNotionで保有資産を管理している方の参考になれば。

