Streamlit入門 第2部:美しいチャートと可視化で仮想通貨を分析しよう

前回の記事で基本的な価格表示ができるようになりました。今回は、プロのトレーダーが使うような美しいチャートを追加して、本格的な分析ツールに仕上げていきます。難しそうに見えますが、実は数行のコードで驚くほど高機能なチャートが作れるんです!

Plotlyとは?なぜグラフライブラリの中で最強なのか

従来のグラフライブラリとの違い

Pythonでグラフを作る代表的なライブラリの比較:

Matplotlib(従来の定番)

import matplotlib.pyplot as plt

# 10行以上のコードでシンプルなグラフ
plt.figure(figsize=(10, 6))
plt.plot(dates, prices)
plt.title('Price Chart')
plt.xlabel('Date')
plt.ylabel('Price')
plt.show()
# しかも、インタラクティブ機能なし...

Plotly(最新の高機能)

import plotly.express as px

# 1行でインタラクティブなグラフ完成!
fig = px.line(df, x='date', y='price', title='Price Chart')

Plotlyの驚くべき機能

  1. ズーム・パン:マウスで自由に拡大・移動
  2. ホバー情報:グラフ上をマウスオーバーで詳細表示
  3. レスポンシブ:画面サイズに自動対応
  4. 美しいデザイン:デフォルトでプロ級の見た目
  5. アニメーション:データの変化を動的に表現

環境準備:Plotlyを追加インストール

pip install plotly

インストール確認

簡単なテストでPlotlyが動作するか確認しましょう:

# test_plotly.py
import streamlit as st
import plotly.express as px
import pandas as pd

st.title("📊 Plotly動作テスト")

# サンプルデータ作成
df = pd.DataFrame({
    'x': [1, 2, 3, 4, 5],
    'y': [2, 4, 1, 5, 3]
})

# グラフ作成
fig = px.line(df, x='x', y='y', title='テストグラフ')

# Streamlitで表示
st.plotly_chart(fig)

st.success("Plotlyが正常に動作しています!")

実行してインタラクティブなグラフが表示されれば成功です。

ローソク足チャートの作成

仮想通貨分析に欠かせないローソク足チャートを作ってみましょう:

# candlestick_chart.py
import streamlit as st
import ccxt
import pandas as pd
import plotly.graph_objects as go
from datetime import datetime

st.title("📈 ローソク足チャート")

# OHLCV データ取得関数
@st.cache_data(ttl=300)  # 5分間キャッシュ
def get_ohlcv_data(symbol='BTC/USDT', timeframe='1h', limit=100):
    """
    OHLCV(Open, High, Low, Close, Volume)データを取得

    Args:
        symbol: 通貨ペア(例:'BTC/USDT')
        timeframe: 時間足(例:'1h', '1d')
        limit: 取得する期間数

    Returns:
        DataFrame: 日時、始値、高値、安値、終値、出来高
    """
    try:
        exchange = ccxt.binance()

        # OHLCVデータを取得
        ohlcv = exchange.fetch_ohlcv(symbol, timeframe, limit=limit)

        # DataFrameに変換
        df = pd.DataFrame(
            ohlcv, 
            columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']
        )

        # タイムスタンプを日時に変換
        df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')

        return df

    except Exception as e:
        st.error(f"データ取得エラー: {e}")
        return pd.DataFrame()

# サイドバーで設定
st.sidebar.header("📊 チャート設定")

# 通貨選択
symbol = st.sidebar.selectbox(
    "通貨ペアを選択",
    ['BTC/USDT', 'ETH/USDT', 'XRP/USDT', 'ADA/USDT', 'DOT/USDT']
)

# 時間足選択
timeframe = st.sidebar.selectbox(
    "時間足を選択",
    ['1m', '5m', '15m', '1h', '4h', '1d'],
    index=3  # デフォルトは1h
)

# 表示期間
limit = st.sidebar.slider(
    "表示期間(本数)", 
    min_value=50, 
    max_value=500, 
    value=100,
    step=50
)

# データ取得
st.write(f"**{symbol}** のチャートを表示中...")

with st.spinner('データを取得中...'):
    df = get_ohlcv_data(symbol, timeframe, limit)

if not df.empty:
    # ローソク足チャート作成
    fig = go.Figure(data=go.Candlestick(
        x=df['timestamp'],
        open=df['open'],
        high=df['high'],
        low=df['low'],
        close=df['close'],
        name=symbol
    ))

    # レイアウト設定
    fig.update_layout(
        title=f"{symbol} ローソク足チャート ({timeframe})",
        xaxis_title="日時",
        yaxis_title="価格 (USDT)",
        height=600,
        xaxis_rangeslider_visible=False  # 下部のスライダーを非表示
    )

    # チャートを表示
    st.plotly_chart(fig, use_container_width=True)

    # 統計情報表示
    st.subheader("📊 統計情報")

    col1, col2, col3, col4 = st.columns(4)

    current_price = df['close'].iloc[-1]
    price_change = current_price - df['close'].iloc[-2]
    price_change_pct = (price_change / df['close'].iloc[-2]) * 100

    with col1:
        st.metric("現在価格", f"${current_price:,.2f}")

    with col2:
        st.metric(
            "前回比", 
            f"${price_change:+,.2f}",
            delta=f"{price_change_pct:+.2f}%"
        )

    with col3:
        highest = df['high'].max()
        st.metric("期間最高値", f"${highest:,.2f}")

    with col4:
        lowest = df['low'].min()
        st.metric("期間最安値", f"${lowest:,.2f}")

else:
    st.error("チャートデータを取得できませんでした")

出来高付きチャートで本格分析

価格と出来高を組み合わせた本格的な分析チャートを作成:

# advanced_chart.py
import streamlit as st
import ccxt
import pandas as pd
import plotly.graph_objects as go
from plotly.subplots import make_subplots

st.title("📊 高度なチャート分析")

@st.cache_data(ttl=300)
def get_detailed_data(symbol='BTC/USDT', timeframe='1h', limit=200):
    """詳細なOHLCVデータと移動平均線を取得"""
    try:
        exchange = ccxt.binance()
        ohlcv = exchange.fetch_ohlcv(symbol, timeframe, limit=limit)

        df = pd.DataFrame(
            ohlcv, 
            columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']
        )
        df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')

        # 移動平均線を計算
        df['sma_20'] = df['close'].rolling(window=20).mean()  # 20期間移動平均
        df['sma_50'] = df['close'].rolling(window=50).mean()  # 50期間移動平均

        return df

    except Exception as e:
        st.error(f"データ取得エラー: {e}")
        return pd.DataFrame()

# 設定パネル
st.sidebar.header("🛠️ 詳細設定")

symbol = st.sidebar.selectbox(
    "分析する通貨", 
    ['BTC/USDT', 'ETH/USDT', 'XRP/USDT', 'ADA/USDT']
)

timeframe = st.sidebar.selectbox(
    "時間足", 
    ['1h', '4h', '1d'],
    index=1
)

# 表示する要素の選択
show_volume = st.sidebar.checkbox("出来高を表示", True)
show_ma = st.sidebar.checkbox("移動平均線を表示", True)

# データ取得
df = get_detailed_data(symbol, timeframe)

if not df.empty:
    # サブプロット作成(価格チャートと出来高チャート)
    if show_volume:
        fig = make_subplots(
            rows=2, cols=1,
            shared_xaxes=True,
            vertical_spacing=0.1,
            subplot_titles=(f'{symbol} 価格チャート', '出来高'),
            row_width=[0.7, 0.3]
        )
    else:
        fig = make_subplots(rows=1, cols=1)

    # ローソク足チャート
    fig.add_trace(
        go.Candlestick(
            x=df['timestamp'],
            open=df['open'],
            high=df['high'],
            low=df['low'],
            close=df['close'],
            name="価格"
        ),
        row=1, col=1
    )

    # 移動平均線
    if show_ma:
        # 20期間移動平均
        fig.add_trace(
            go.Scatter(
                x=df['timestamp'],
                y=df['sma_20'],
                mode='lines',
                name='20期間移動平均',
                line=dict(color='orange', width=2)
            ),
            row=1, col=1
        )

        # 50期間移動平均
        fig.add_trace(
            go.Scatter(
                x=df['timestamp'],
                y=df['sma_50'],
                mode='lines',
                name='50期間移動平均',
                line=dict(color='blue', width=2)
            ),
            row=1, col=1
        )

    # 出来高バーチャート
    if show_volume:
        # 価格が上昇した日は緑、下降した日は赤
        colors = ['green' if close >= open else 'red' 
                 for close, open in zip(df['close'], df['open'])]

        fig.add_trace(
            go.Bar(
                x=df['timestamp'],
                y=df['volume'],
                name="出来高",
                marker_color=colors,
                opacity=0.7
            ),
            row=2, col=1
        )

    # レイアウト設定
    fig.update_layout(
        title=f"{symbol} 詳細チャート ({timeframe})",
        yaxis_title="価格 (USDT)",
        height=800 if show_volume else 600,
        xaxis_rangeslider_visible=False
    )

    if show_volume:
        fig.update_yaxes(title_text="出来高", row=2, col=1)

    # チャート表示
    st.plotly_chart(fig, use_container_width=True)

    # トレンド分析
    st.subheader("🔍 トレンド分析")

    # 最新の移動平均線の値
    latest_price = df['close'].iloc[-1]
    latest_sma20 = df['sma_20'].iloc[-1]
    latest_sma50 = df['sma_50'].iloc[-1]

    col1, col2 = st.columns(2)

    with col1:
        st.write("**短期トレンド(20期間移動平均):**")
        if latest_price > latest_sma20:
            st.success("🟢 上昇トレンド")
        else:
            st.error("🔴 下降トレンド")

    with col2:
        st.write("**中期トレンド(50期間移動平均):**")
        if latest_price > latest_sma50:
            st.success("🟢 上昇トレンド")
        else:
            st.error("🔴 下降トレンド")

    # ゴールデンクロス・デッドクロス判定
    if not pd.isna(latest_sma20) and not pd.isna(latest_sma50):
        if latest_sma20 > latest_sma50:
            st.info("✨ ゴールデンクロス状態(買いシグナル)")
        else:
            st.warning("💀 デッドクロス状態(売りシグナル)")

else:
    st.error("データを取得できませんでした")

複数通貨の比較チャート

複数の仮想通貨を同時に比較できるチャートを作成:

# comparison_chart.py
import streamlit as st
import ccxt
import pandas as pd
import plotly.express as px
import plotly.graph_objects as go

st.title("⚖️ 複数通貨比較チャート")

@st.cache_data(ttl=300)
def get_comparison_data(symbols, timeframe='1d', limit=30):
    """複数通貨の比較データを取得"""
    exchange = ccxt.binance()
    all_data = []

    for symbol in symbols:
        try:
            ohlcv = exchange.fetch_ohlcv(symbol, timeframe, limit=limit)
            df = pd.DataFrame(
                ohlcv, 
                columns=['timestamp', 'open', 'high', 'low', 'close', 'volume']
            )
            df['timestamp'] = pd.to_datetime(df['timestamp'], unit='ms')
            df['symbol'] = symbol

            # 正規化(最初の価格を100として相対変化率を計算)
            first_price = df['close'].iloc[0]
            df['normalized'] = (df['close'] / first_price) * 100

            all_data.append(df)

        except Exception as e:
            st.warning(f"{symbol} のデータ取得に失敗: {e}")

    if all_data:
        return pd.concat(all_data, ignore_index=True)
    else:
        return pd.DataFrame()

# 比較する通貨を選択
st.sidebar.header("🔄 比較設定")

available_currencies = ['BTC/USDT', 'ETH/USDT', 'XRP/USDT', 'ADA/USDT', 'DOT/USDT', 'LINK/USDT']

selected_currencies = st.sidebar.multiselect(
    "比較する通貨を選択(複数選択可)",
    available_currencies,
    default=['BTC/USDT', 'ETH/USDT']
)

timeframe = st.sidebar.selectbox(
    "期間",
    ['1h', '4h', '1d', '1w'],
    index=2
)

limit = st.sidebar.slider("表示期間", 7, 90, 30)

if selected_currencies:
    # データ取得
    comparison_df = get_comparison_data(selected_currencies, timeframe, limit)

    if not comparison_df.empty:
        # 正規化価格の比較チャート
        st.subheader("📈 相対パフォーマンス比較")
        st.caption("最初の時点を100として、そこからの変化率を表示")

        fig_normalized = px.line(
            comparison_df,
            x='timestamp',
            y='normalized',
            color='symbol',
            title="相対パフォーマンス(正規化)",
            labels={'normalized': '相対価格 (基準=100)', 'timestamp': '日時'}
        )

        fig_normalized.update_layout(height=500)
        st.plotly_chart(fig_normalized, use_container_width=True)

        # 実際の価格比較(別々のスケール)
        st.subheader("💰 実際の価格推移")

        fig_actual = go.Figure()

        for symbol in selected_currencies:
            symbol_data = comparison_df[comparison_df['symbol'] == symbol]
            fig_actual.add_trace(
                go.Scatter(
                    x=symbol_data['timestamp'],
                    y=symbol_data['close'],
                    mode='lines',
                    name=symbol,
                    yaxis='y1' if symbol == selected_currencies[0] else 'y2'
                )
            )

        # デュアル軸設定(2つの通貨のスケールが大きく異なる場合)
        if len(selected_currencies) >= 2:
            fig_actual.update_layout(
                title="実際の価格推移(デュアル軸)",
                xaxis_title="日時",
                yaxis=dict(title=f"{selected_currencies[0]} 価格", side="left"),
                yaxis2=dict(title=f"{selected_currencies[1]} 価格", side="right", overlaying="y"),
                height=500
            )
        else:
            fig_actual.update_layout(
                title="実際の価格推移",
                xaxis_title="日時",
                yaxis_title="価格 (USDT)",
                height=500
            )

        st.plotly_chart(fig_actual, use_container_width=True)

        # パフォーマンス統計
        st.subheader("📊 パフォーマンス統計")

        stats_data = []
        for symbol in selected_currencies:
            symbol_data = comparison_df[comparison_df['symbol'] == symbol]
            if len(symbol_data) > 1:
                first_price = symbol_data['close'].iloc[0]
                last_price = symbol_data['close'].iloc[-1]
                total_return = ((last_price - first_price) / first_price) * 100

                stats_data.append({
                    '通貨': symbol,
                    '開始価格': f"${first_price:,.2f}",
                    '最終価格': f"${last_price:,.2f}",
                    'リターン': f"{total_return:+.2f}%"
                })

        if stats_data:
            stats_df = pd.DataFrame(stats_data)
            st.dataframe(stats_df, use_container_width=True)

            # 最高・最低パフォーマンス
            returns = [float(stat['リターン'].replace('%', '').replace('+', '')) 
                      for stat in stats_data]
            best_idx = returns.index(max(returns))
            worst_idx = returns.index(min(returns))

            col1, col2 = st.columns(2)
            with col1:
                st.success(f"🏆 最高パフォーマンス: {stats_data[best_idx]['通貨']}")
            with col2:
                st.error(f"📉 最低パフォーマンス: {stats_data[worst_idx]['通貨']}")

else:
    st.info("サイドバーから比較したい通貨を選択してください")

# 使い方ガイド
with st.expander("💡 チャートの見方・使い方"):
    st.write("""
    **相対パフォーマンス比較:**
    - 全通貨を同じ基準(100)からスタートして比較
    - どの通貨が最も成長したかが一目で分かる
    - 線が上に行くほど良いパフォーマンス

    **実際の価格推移:**
    - 各通貨の実際の価格変動を表示
    - 価格スケールが異なる場合はデュアル軸で表示

    **チャートの操作方法:**
    - 🔍 ドラッグで範囲選択してズーム
    - 📱 ダブルクリックでズームリセット
    - 👆 マウスオーバーで詳細データ表示
    - 📱 凡例クリックで表示/非表示切り替え
    """)

まとめ

この記事では以下の機能を実装しました:

🎯 実装した機能

  1. ローソク足チャート:プロトレーダー級の分析
  2. 移動平均線:トレンド分析機能
  3. 出来高表示:取引の活発さを可視化
  4. 複数通貨比較:相対パフォーマンス分析
  5. インタラクティブ操作:ズーム・ホバー情報

🔧 使用した技術

  • Streamlit:Webアプリフレームワーク
  • Plotly:高機能グラフライブラリ
  • CCXT:仮想通貨データ取得
  • Pandas:データ処理・分析

次回の第3部では、ポートフォリオ管理とアラート機能を追加して、完全な投資管理ツールに仕上げます。

関連記事

コメントする