1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

元ゲームデザイナーが家族のためウェブサイトを作った話(1)

1
Last updated at Posted at 2026-04-07

0.まずはスプレッドシートで簡単にプロトタイピング

本編では、このプロジェクトを始めたきっかけから、設計・実装・デプロイに至るまでの流れを順を追ってご紹介します。

1. きっかけ:休養中の小さなアウトプット

激務が続いたことで体調を崩し、しばらく仕事を離れて休養していた時期が続いていました。時間に余裕はできたものの、「せっかくなら何か手を動かしながら勉強したい」と思い、小さなプロトタイプを作ってみることにしました。

2. 引退した家族のささやかな趣味:株とYouTube

最近、家族が株に夢中になっていて、毎日のように翌日の相場予想をYouTubeで何時間も見ています。

それを見ていて、ふとこんなことを思いました。

「指数で予測する程度なら、データを引き出してAIに分析させればいいのでは?」

さらに、

「株式系YouTuberも裏では似たようなことをしているのでは…?」

と考えるようになり、
参考程度の分析ならAIに任せてもいいのではと思いました。

3. とりあえず作る:スプレッドシートでプロトタイピングのプロトタイピング

最近の韓国市場は半導体関連株が強い印象ですので、

「株価に影響しそうな指数をいくつかまとめて取得できれば、それっぽい分析ができるのでは?」

と思い、以下のような構成でデータを集めてみました。

VIX:恐怖指数
SOX:半導体関連指数
USD/KRW:為替
Brent / WTI:原油価格

とはいえ、いきなり大げさなものを作るつもりはなく、
まずは半日くらいでプロトタイプを作ることにしました。

image.png

こういう時にゲームデザイナーにとって一番強いのはやはりExcelです。
Googleスプレッドシートも仕事で使ったことがあり、今回はそちらで試しました。

「Analyse Start」のボタンを押すと、Gemini簡易AI分析ツール)で簡単な分析結果を表示する仕様にしました。
このコードはGoogleスプレッドシートのApps Scriptで作成しました。

function runAiBriefing() {
  const ss = SpreadsheetApp.getActiveSpreadsheet();
  const sheet = ss.getSheetByName("Sheet1"); // シート名を確認
  
  // 1. 分析するデータ範囲を取得
  const data = sheet.getRange("A1:E7").getValues();
  const apiKey = "YOUR_API_KEY";
  const url = `https://generativelanguage.googleapis.com/v1beta/models/gemini-2.5-pro:generateContent?key=${apiKey}`;
  
  // データ整形
  const dataString = data.map(row => row.join(" | ")).join("\n");
  
  const prompt = `
あなたは家族のための経済アシスタントです。
以下のデータをもとに、今日の市場状況を家族に説明するように、わかりやすく丁寧に分析してください。
あなたは専門的な金融アナリストであり、株式市場の戦略家でもあります。

以下の指標をもとに、韓国および米国の株式市場が翌日にどのように動くかを専門的に分析してください。
各指標の前日比の変化が市場に与える影響と、その理由を具体的に説明してください。

- VIXY(米国ボラティリティ指数ETF、投資家心理)
- SOXX(半導体ETF、テクノロジー株の動向)
- USD/KRW(ウォン/ドル為替レート)
- Brent(ブレント原油、国際原油価格)
- WTI(WTI原油価格)

分析には以下を含めてください:
1. 原油(Brent, WTI)が上昇/下落した場合、素材・エネルギー関連株への影響
2. VIXYの変化による投資家の恐怖心理への影響
3. 為替の変動が輸出入企業およびKOSPI/KOSDAQに与える影響
4. SOXXおよび半導体株の動向がテクノロジー株全体に与える影響
5. 最後に「前日比で翌日の株式市場は全体的に上昇/下落/ボラティリティ拡大」などの一文要約

専門的でありながら理解しやすく、3〜4文でまとめてください。
可能であれば数値やパーセンテージの変化にも言及し、具体的に説明してください。

データ:
VIXY: {VIXY値}
SOXX: {SOXX値}
USD/KRW: {USDKRW値}
Brent: {Brent値}
WTI: {WTI値}

特に変動率(D列)が大きい指標については、忘れずに重点的に言及してください。

繰り返しになりますが、専門的でありながら理解しやすく、3〜4文でまとめてください。
可能であれば数値やパーセンテージの変化にも言及し、具体的に説明してください。

また、1文が長くなりすぎないようにし、7セル以上に収まらない場合は適切に改行してください。

現在、SOXXとVIXYを取り違えて出力する問題があるため、そのような誤りが起きないよう、
シートのデータと生成した文章を出力前に必ず検証してください。
`;
...

Geminiには以下のようなプロンプトを送っています:

  • 市場指数(VIX, SOX など)
  • 為替・原油価格
  • 当日のニュース要約

これらをまとめて「翌日の市場傾向」を出力するようにしています。

function GET_FINNHUB_PRICE(symbol) {
  const apiKey = "APIKeyを勝手に出せばいけません"; 
  
  if (!symbol) return "シンボルを入力してください!";
  
  let cleanSymbol = symbol.toString().toUpperCase();
  if (cleanSymbol === "VIX") cleanSymbol = "^VIX"; 

  // 重要な修正:特殊文字(^など)を安全にエンコードします
  const encodedSymbol = encodeURIComponent(cleanSymbol);
  const url = `https://finnhub.io/api/v1/quote?symbol=${encodedSymbol}&token=${apiKey}`;
  
  try {
    const response = UrlFetchApp.fetch(url, { 'muteHttpExceptions': true });
    const content = response.getContentText();
    const data = JSON.parse(content);
    
    // c: 現在価格
    if (data) {
      // 第2引数に "pc" を渡すと前日終値、そうでなければ現在価格(c)を返す
      if (arguments[1] === "pc") return data.pc; 
      return data.c;
    }
  } catch (e) {
    return "エラー: " + e.toString();
  }
}

この時はデータをfinnhub(株式・指数データ提供API)から取得していましたが、Finnhubは無料プランでは取得回数やデータに制限があり、継続運用には不向きだったためYahoo Financeに切り替えました。

4. 可視化:とりあえず静的ページにする

image.png

そして、このスプレッドシートをそのまま静的ページとしてまとめたのがこちらです。

いわゆるダッシュボード的なものです。

この時点では、

「状態管理もほとんどないし、このくらいなら静的ページで十分では?」

というかなりラフな設計でしたが、
まずは形にすることを優先しました。

5. デプロイ

実は私は元ゲームデザイナーで、ウェブ開発者への転職を考えていました。
面接の際、韓国の会社でこんな質問を受けました。

「実際の運営経験はありますか?」

塾やブートキャンプでは、費用の関係で運営経験まで積めませんでした。
そこで今回は、プロジェクトの運営経験も得るため、最初からVercel(静的ページデプロイサービス)を使ってデプロイまで行うことにしました。

なぜVercelを選んだかというと、簡単にデプロイできる上、後でクラウド上でサーバーを構築することも可能だからです。

6. 静的ページのアーキテクチャー

実にシンプルです。

image.png

この時はまず家族に何とか結果を出して見せるのがすべてだったので、簡単に作りました。
ただ、無駄にGeminiのリクエストを受けるにはできませんでした。
なのでVercelの先にredisを設置して、一分でN回以上のリクエストを受けないようにしました。

import { kv } from '@vercel/kv';

const ip = req.headers['x-forwarded-for']?.split(',')[0]?.trim() ?? 'unknown';
const rateKey = `rl:${ip}`;

// レート制限チェック(フェイルオープン方式)
const rateLimitPromise = kv.incr(rateKey)
  .then(count => {
    if (count === 1) {
      // 初回アクセス時にTTL(60秒)を設定
      return kv.expire(rateKey, 60).then(() => count);
    }
    return count;
  })
  .catch(e => {
    console.error('KVレート制限エラー(フェイルオープン):', e.message);
    return 0; // KVでエラーが発生した場合は制限をスキップ
  });

return rateLimitPromise.then(count => {
  if (count > 20) {
    return res.status(429).json({ error: 'リクエスト数が多すぎます' });
  }
  // ...
});

Redisを用いてIPごとにリクエスト数をカウントし、TTLを60秒に設定して自動リセットすることで、1分間に一定回数を超えた場合はリクエストを拒否するシンプルなレート制限を実装しました。

そして分析のクオリティを高めるためにニュースを集めることにしました。
まだRSSは無料で受けることができたので、そういうニュースを集めました。

7.この後は?

marketradar-omega.vercel.app_.png

スプレッドシートから始めた小さなプロトタイプでしたが、
データ取得・AI分析・デプロイまで一通りの流れを実装することができました。

次回は、このプロトタイプをベースにReactで再構築したWebアプリ版について紹介します。

まだ日本語には対応していませんが、もし興味がある方は、こちらを参考にしていただければ幸いです。

MarketRadar
https://marketradar-omega.vercel.app/

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?