2
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?

Streamlit × Claude LLM で作る「AI Sentiment Index」付きマーケットダッシュボード

2
Posted at

はじめに

「株価を見るたびに複数のサイトを開き直している」「Fear & Greed Indexって結局どこで見ればいいの?」

そんな課題を解決するために、Streamlit + Python + 複数AI APIを組み合わせたマーケットダッシュボードを個人開発しました。

最大の特徴は、Claude LLMをベースに設計した独自の AI Sentiment Index (Claude Edition) です。単なるFear & Greedの焼き直しではなく、9指標を加重合成した複合センチメントスコアを日次で計算・可視化しています。
この記事では設計思想から実装のポイント、SEO対策・セキュリティ強化まで一通りご紹介します。

https://windex.streamlit.app/

目次

  1. ダッシュボード全体像
  2. 技術スタック
  3. AI Sentiment Index (Claude Edition) の設計
  4. 主要機能の実装解説
  5. SEO・OGP対応
  6. パフォーマンス最適化
  7. まとめ・今後の展望

ダッシュボード全体像

セクション 内容
🧠 AI Sentiment Index 9指標加重合成の独自センチメントスコア(0〜100)
😱 Fear & Greed Index CNN米国版+日本版(独自計算)の2軸
📡 NAAIM Exposure Index 機関投資家ポジション(バックグラウンド並行取得)
📈 方向性予測スコア 日経平均・S&P500・NASDAQ・ダウの翌日/今週予測
🔗 日米業種リードラグ 部分空間正則化PCAによる業種間先行シグナル
🔬 高度市場解析 セクターローテーション・マクロレジーム・相関ヒートマップ
📄 TDnet自動解析 決算PDF → Gemini/Groq/OpenRouterでAI要約
📊 市場データ 日米欧アジア・個別株・為替・コモディティ・暗号資産

技術スタック

# 主要ライブラリ
streamlit          # UIフレームワーク
yfinance           # 市場データ取得
pandas / numpy     # データ処理
matplotlib         # チャート描画
requests           # HTTP通信
beautifulsoup4     # Webスクレイピング
pdfplumber         # PDF文字抽出
pytz               # タイムゾーン

# AI API(優先度順にフォールバック)
google-generativeai   # Gemini(第1候補)
groq                  # Groq LLaMA(第2候補)
openrouter            # OpenRouter(第3候補)

AIの呼び出しは call_ai_with_fallback() に統一し、APIキー未設定・クォータ超過・モデル廃止に自動対応します。

def call_ai_with_fallback(prompt, max_output_tokens=1500, temperature=0.3):
    """Gemini → Groq → OpenRouter の順でフォールバック"""
    MODEL_FALLBACKS = [
        "gemini-2.0-flash",
        "gemini-2.0-flash-lite",
        "gemini-1.5-flash",
        "gemini-1.5-pro",
    ]
    # Gemini試行 → quota超過なら即Groqへ
    if GENAI_AVAILABLE and GEMINI_API_KEY:
        for model_name in MODEL_FALLBACKS:
            try:
                response = genai.GenerativeModel(model_name).generate_content(prompt)
                if response.text:
                    return response.text.strip(), f"Gemini ({model_name})"
            except Exception as e:
                if is_gemini_quota_error(e):
                    break   # クォータ超過 → 即Groqへ
                continue

    # Groq / OpenRouter へフォールバック
    ...

AI Sentiment Index (Claude Edition) の設計

このダッシュボードの核心部分です。

なぜ独自スコアが必要か

CNN Fear & Greed Indexは有名ですが、米国株に特化しており日本市場を反映しないという課題があります。また7指標の構成や重みが非公開で再現性に欠けます。

そこでClaude LLMの知見をベースに指標選定・重み設計を行い、透明性のある9指標複合スコアとして実装しました。

指標構成と重み

# 指標 重み データソース
F&G Index(CNN参照) 15% CNN API
VIX水準 12% ^VIX
VIX期間構造(VIX3M/VIX) 13% ^VIX3M / ^VIX
Put/Call比率(VXX代替) 10% VXX
価格モメンタム(S&P500 20日) 15% ^GSPC
Safe Haven需要(株vs債券) 10% ^GSPC / TLT
セクター配分(成長vsディフェンシブ) 10% XLK/XLY/XLU/XLP
信用リスク選好(HYG vs LQD) 10% HYG / LQD
ブレッドス(市場の広がり) 5% SPY/QQQ/IWM/DIA/MDY

スコア計算の実装

@st.cache_data(ttl=3600, show_spinner=False)
def compute_composite_sentiment() -> Dict[str, Any]:
    # 各指標を 0〜100 にノーマライズ
    def _score_clip(x):
        return float(np.clip(x, 0, 100))

    # 例: VIX水準スコア(VIX=12でスコア100、VIX=35でスコア0)
    vix_val = float(vix_c.iloc[-1])
    vix_score = _score_clip((35 - vix_val) / (35 - 12) * 100)

    # 例: VIX期間構造(VIX3M/VIX > 1.0 = コンタンゴ = 強気)
    ratio = vix3m_now / vix_now
    term_score = _score_clip((ratio - 0.95) / 0.20 * 100)

    # 加重平均で合成
    total_weight = sum(c["weight"] for c in components.values())
    composite = sum(
        c["normalized"] * c["weight"] for c in components.values()
    ) / total_weight

過去約3年分のヒストリカルスコアも実データから再計算し、3年/1年/3か月タブで推移チャートとして表示します。


主要機能の実装解説

Fear & Greed Index(米国・日本版)

米国版

CNN公式APIから取得。取得失敗時は Yahoo Finance の7指標から独自計算するフォールバックを実装しています。

日本版(独自計算)

日本市場向けに以下の指標から算出します。

def fetch_japan_fear_greed_index():
    """日本版 Fear & Greed Index を独自計算"""
    # 日経VI(ボラティリティ)/ 騰落レシオ / 信用倍率
    # 日経平均モメンタム / セクター配分 / 空売り比率
    # → 各指標を 0〜100 正規化して加重平均

NAAIM Exposure Index(並行取得対応)

NAAIMの公式サイトから毎週水曜に更新されるExcelを取得します。取得に時間がかかるため、スレッドで並行取得してUIをブロックしない実装にしています。

def render_naaim_section():
    import threading as _threading

    _cache_key  = "_naaim_df_cache"
    _status_key = "_naaim_status"

    status = st.session_state.get(_status_key)

    if status in ("done", "error"):
        # キャッシュから即描画(待機ゼロ)
        _cached = st.session_state.get(_cache_key)
        df_naaim = _cached if isinstance(_cached, pd.DataFrame) else pd.DataFrame()
    else:
        # バックグラウンドスレッドで取得(最大10秒で打ち切り)
        _result = {"df": None}

        def _bg_fetch():
            _result["df"] = fetch_naaim_data()  # 内部に28秒ハードタイムアウト

        _t = _threading.Thread(target=_bg_fetch, daemon=True)
        _t.start()

        with st.spinner("NAAIM データを取得中...(最大10秒)"):
            _t.join(timeout=9)  # 10秒で強制打ち切り

        df_naaim = _result["df"] or pd.DataFrame()
        st.session_state[_cache_key]  = df_naaim
        st.session_state[_status_key] = "done" if not df_naaim.empty else "error"

取得手法も3段階のフォールバックを実装し、XLSXリンク変更・ネットワーク障害にも対応しています。


日米業種リードラグシグナル

人工知能学会の研究論文(SIG-FIN-036-13「部分空間正則化付き主成分分析を用いた日米業種リードラグ投資戦略」)をベースに実装しています。

米国S&P500業種ETF(XLK/XLF/XLE...)の当日終値から、翌日の日本TOPIX-17業種の日中リターンを予測するシグナルです。

実績(論文バックテスト):

戦略 年率リターン リスク R/R比 最大DD
PCA_SUB(提案手法) 23.79% 10.70% 2.22 9.58%
DOUBLE(モメンタム×PCA) 18.86% 11.16% 1.69 12.10%
MOM(単純モメンタム) 5.63% 10.59% 0.53 16.97%

日経平均・S&P500 予測スコア

VIX・原油・ドル円・モメンタム・RSI・移動平均乖離率など複数シグナルを合成し、翌日・今週の上昇/下降確率を算出します。

def render_nikkei_prediction():
    """
    シグナル構成:
      - VIX水準(逆張り)
      - 価格モメンタム(5日・20日)
      - RSI(14日)
      - 移動平均乖離率(25日・75日)
      - ドル円の方向性
      - 米国先物(CME NKD)の時間外動向
      - Safe Haven需要(TLT vs SPY)
    → ロジスティック回帰 + 単純アンサンブルで確率化
    """

TDnet決算自動解析(Gemini/Groq連携)

TDnet(東証適時開示情報サービス)から当日の決算PDFを一覧取得し、AIで自動要約します。

① TDnet一覧ページをスクレイピング
② 決算キーワードでフィルタリング
③ pdfplumber でPDFからテキスト抽出
④ Gemini/Groq/OpenRouterで要約
⑤ 結果をsession_stateにキャッシュ(再解析コスト節約)

「まとめて解析」ボタンで複数件を一括処理でき、進捗バーでリアルタイム表示します。


セキュリティ対策(国ブロック)

セキュリティリスクの高い国(中国・ロシア・北朝鮮)からのアクセスをブロックします。

BLOCKED_COUNTRIES = {"CN", "RU", "KP"}

def check_country_block() -> bool:
    # セッション内1回だけ判定(毎描画でAPI叩かない)
    if st.session_state.get("_country_checked"):
        return st.session_state.get("_country_blocked", False)

    st.session_state["_country_checked"] = True

    # X-Forwarded-For からIPを取得
    ip = st.context.headers.get("x-forwarded-for", "").split(",")[0].strip()

    # ipinfo.io でIP → 国コードに変換
    r = requests.get(f"https://ipinfo.io/{ip}/json", timeout=4)
    country = r.json().get("country", "??")

    blocked = country in BLOCKED_COUNTRIES
    st.session_state["_country_blocked"] = blocked
    return blocked

def main():
    # 最優先でブロックチェック
    if check_country_block():
        st.error("🚫 Access Denied")
        st.stop()
        return
    ...

IPINFO_TOKENsecrets.toml に設定するとAPI精度が向上します。

# .streamlit/secrets.toml
IPINFO_TOKEN = "your_token_here"

SEO・OGP対応

Streamlitアプリはデフォルトではメタタグが貧弱です。st.markdown(unsafe_allow_html=True) でページ本体にメタタグを注入します。

_SEO_META = """
<meta name="description" content="Market Dashboard — 日米株式・為替・AI Sentiment Index...">
<meta name="keywords"    content="Market Dashboard, 株価, Fear Greed Index, AI Sentiment Index, Claude...">
<meta name="robots"      content="index, follow">

<!-- OGP -->
<meta property="og:title"       content="Market Dashboard | リアルタイム株価・AIセンチメント">
<meta property="og:description" content="AI Sentiment Index (Claude Edition)・Fear&Greed...">
<meta property="og:image"       content="https://your-app.streamlit.app/app/static/og_image.png">

<!-- Twitter Card -->
<meta name="twitter:card" content="summary_large_image">

<!-- JSON-LD構造化データ -->
<script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "WebApplication",
  "name": "Market Dashboard",
  "applicationCategory": "FinanceApplication"
}
</script>
"""

def inject_seo_meta():
    st.markdown(_SEO_META, unsafe_allow_html=True)

サイトマップ・robots.txt

Streamlitは静的ファイルサーブが制限されるため、クエリパラメータで配信する方法を採用しました。

# ?sitemap=1 → sitemap.xml を表示
# ?robots=1  → robots.txt を表示
if st.query_params.get("sitemap") == "1":
    st.markdown(f"```xml\n{sitemap_xml}\n```")
    st.stop()
elif st.query_params.get("robots") == "1":
    st.code(robots_txt, language="text")
    st.stop()

Google Search Consoleには https://your-app.streamlit.app/?sitemap=1 を登録します。


パフォーマンス最適化

キャッシュ戦略

TTL_DAILY    = 3600   # 1時間(日次データ)
TTL_INTRADAY = 300    # 5分(分足データ)
TTL_RSS      = 1800   # 30分(RSSニュース)
TTL_CHART    = 600    # 10分(チャート)

@st.cache_data(ttl=TTL_DAILY, show_spinner=False)
def compute_composite_sentiment(): ...

@st.cache_data(ttl=TTL_INTRADAY, show_spinner=False)
def fetch_market_price(symbol): ...

重い処理の遅延読み込み

高度市場解析(セクターローテーション・マクロレジーム・相関ヒートマップ)はボタン押下時のみ実行します。

def render_advanced_analytics():
    if not st.session_state.get("show_advanced_analytics"):
        if st.button("🔬 高度市場解析を読み込む"):
            st.session_state["show_advanced_analytics"] = True
            st.rerun()
        return
    # 以下、重い処理
    render_sector_rotation()
    render_macro_regime()
    render_correlation_heatmap()

Google翻訳の永続化

components.html() はStreamlit再描画のたびにiframeがリセットされ翻訳ウィジェットが消える問題があります。st.markdown でページ本体に直接注入し、二重初期化防止フラグで安定化しました。

if (!window._gtransInitialized) {
    window._gtransInitialized = true;
    // スクリプトをdocument.headに動的追加
    var s = document.createElement('script');
    s.src = '//translate.google.com/translate_a/element.js?cb=googleTranslateElementInit';
    document.head.appendChild(s);
}

まとめ・今後の展望

実装したこと:

  • ✅ 9指標加重合成の AI Sentiment Index (Claude Edition)
  • ✅ 米国・日本版 Fear & Greed Index(CNNフォールバック付き)
  • ✅ NAAIM Exposure Indexの並行取得(10秒タイムアウト)
  • ✅ 日米業種リードラグシグナル(部分空間正則化PCA)
  • ✅ TDnet決算PDF → AI自動要約(Gemini/Groq/OpenRouterフォールバック)
  • ✅ 国ブロック(中国・ロシア・北朝鮮)
  • ✅ SEO/OGP/JSON-LD構造化データ/robots.txt/サイトマップ

今後の展望:

  • Anthropic Claude API への完全移行(Claude Sonnet 4.6)
  • リアルタイムWebSocket対応(価格の自動更新)
  • LINE/Slack通知との連携(スコア閾値アラート)
  • ユーザー別ポートフォリオ管理機能

参考文献・リソース


この記事が参考になったらいいねをお願いします!
ご意見・ご質問はコメント欄まで 🙏

2
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
2
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?