claude-code/claude-finance/skills/chart-drawing/SKILL.md

369 lines
11 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
name: chart-drawing
description: 技術分析圖表繪製知識庫。用 Python matplotlib 繪製各種技術型態圖,每種型態分開畫,輸出 PNG 圖片。
---
# 技術分析圖表繪製
## 環境需求
```bash
pip install yfinance matplotlib mplfinance pandas numpy
```
## ⚠️ 繪圖必讀規則(每次畫圖前必須遵守)
**以下 5 條規則缺一不可,否則圖片會壞掉或看不到:**
### 規則 1必須在最開頭設定 Agg backend無 GUI 環境)
```python
import matplotlib
matplotlib.use('Agg') # 必須在 import pyplot 之前!
import matplotlib.pyplot as plt
```
### 規則 2必須設定中文字體否則中文標題變方框
```python
import matplotlib.pyplot as plt
# macOS 中文字體設定
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Heiti TC', 'PingFang TC', 'STHeiti']
plt.rcParams['axes.unicode_minus'] = False # 負號正常顯示
```
### 規則 3禁止使用 plt.show()(會卡住或報錯)
```python
# ❌ 錯誤
plt.show()
# ✅ 正確 — 只用 savefig
plt.savefig('docs/fin/charts/NVDA-kline.png', dpi=150, bbox_inches='tight')
plt.close('all') # 必須關閉,釋放記憶體
```
### 規則 4每張圖結尾必須 plt.close('all')
```python
plt.savefig(output_path, dpi=150, bbox_inches='tight')
plt.close('all') # 不加這行,下一張圖會疊在上面
```
### 規則 5繪圖前必須建立目錄
```python
import os
os.makedirs('docs/fin/charts', exist_ok=True)
```
## 完整繪圖模板(通用前置碼)
**每次繪圖都必須以這段開頭:**
```python
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import yfinance as yf
import pandas as pd
import numpy as np
import os
# 中文字體
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Heiti TC', 'PingFang TC', 'STHeiti']
plt.rcParams['axes.unicode_minus'] = False
# 建立輸出目錄
os.makedirs('docs/fin/charts', exist_ok=True)
# 下載數據(美股)
ticker = "NVDA"
df = yf.download(ticker, period="6mo", interval="1d")
close = df['Close'].squeeze()
dates = df.index
```
## 數據取得
```python
# 美股
df = yf.download("NVDA", period="1y", interval="1d")
# 台股(代號加 .TW
df = yf.download("2330.TW", period="1y", interval="1d")
# 注意yfinance 回傳的 DataFrame 可能是 MultiIndex
# 取單一欄位時用 .squeeze() 確保是 Series
close = df['Close'].squeeze()
```
## 圖表類型與範本
### 1. K 線圖 + 均線(基礎圖)
```python
import matplotlib
matplotlib.use('Agg')
import mplfinance as mpf
import yfinance as yf
import os
os.makedirs('docs/fin/charts', exist_ok=True)
ticker = "NVDA"
df = yf.download(ticker, period="6mo", interval="1d")
# mplfinance 的 savefig 要用 dict 格式
save_config = dict(fname=f'docs/fin/charts/{ticker}-kline.png', dpi=150, bbox_inches='tight')
mpf.plot(df, type='candle', style='charles',
mav=(20, 50, 200),
volume=True,
title=f'{ticker} K線圖 + 均線',
figsize=(14, 8),
savefig=save_config)
# mplfinance 會自動 close
print(f"✅ 圖表已儲存: docs/fin/charts/{ticker}-kline.png")
```
### 2. 支撐壓力圖
```python
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import yfinance as yf
import os
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Heiti TC', 'PingFang TC', 'STHeiti']
plt.rcParams['axes.unicode_minus'] = False
os.makedirs('docs/fin/charts', exist_ok=True)
ticker = "NVDA"
df = yf.download(ticker, period="6mo", interval="1d")
close = df['Close'].squeeze()
fig, ax = plt.subplots(figsize=(14, 8))
ax.plot(df.index, close, 'b-', linewidth=1.5, label='收盤價')
# 標註支撐壓力(由 technical-analyst 提供具體數值)
support = 120 # 替換為實際值
resistance = 150 # 替換為實際值
ax.axhline(y=support, color='green', linestyle='--', linewidth=2, label=f'支撐 ${support}')
ax.axhline(y=resistance, color='red', linestyle='--', linewidth=2, label=f'壓力 ${resistance}')
ax.set_title(f'{ticker} 支撐壓力圖', fontsize=16)
ax.set_ylabel('價格 (USD)', fontsize=12)
ax.legend(fontsize=12)
ax.grid(True, alpha=0.3)
output_path = f'docs/fin/charts/{ticker}-support-resistance.png'
plt.savefig(output_path, dpi=150, bbox_inches='tight')
plt.close('all')
print(f"✅ 圖表已儲存: {output_path}")
```
### 3. RSI 圖
```python
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import yfinance as yf
import os
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Heiti TC', 'PingFang TC', 'STHeiti']
plt.rcParams['axes.unicode_minus'] = False
os.makedirs('docs/fin/charts', exist_ok=True)
ticker = "NVDA"
df = yf.download(ticker, period="6mo", interval="1d")
close = df['Close'].squeeze()
delta = close.diff()
gain = delta.where(delta > 0, 0).rolling(14).mean()
loss = (-delta.where(delta < 0, 0)).rolling(14).mean()
rs = gain / loss
rsi = 100 - (100 / (1 + rs))
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), height_ratios=[3, 1])
ax1.plot(df.index, close, 'b-', linewidth=1.5)
ax1.set_title(f'{ticker} 股價', fontsize=14)
ax1.set_ylabel('價格 (USD)', fontsize=12)
ax1.grid(True, alpha=0.3)
ax2.plot(df.index, rsi, 'purple', linewidth=1.5)
ax2.axhline(y=70, color='red', linestyle='--', alpha=0.7, label='超買 70')
ax2.axhline(y=30, color='green', linestyle='--', alpha=0.7, label='超賣 30')
ax2.fill_between(df.index, 70, 100, alpha=0.1, color='red')
ax2.fill_between(df.index, 0, 30, alpha=0.1, color='green')
ax2.set_title('RSI(14)', fontsize=14)
ax2.set_ylim(0, 100)
ax2.legend(fontsize=10)
ax2.grid(True, alpha=0.3)
output_path = f'docs/fin/charts/{ticker}-rsi.png'
plt.tight_layout()
plt.savefig(output_path, dpi=150, bbox_inches='tight')
plt.close('all')
print(f"✅ 圖表已儲存: {output_path}")
```
### 4. MACD 圖
```python
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import yfinance as yf
import os
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Heiti TC', 'PingFang TC', 'STHeiti']
plt.rcParams['axes.unicode_minus'] = False
os.makedirs('docs/fin/charts', exist_ok=True)
ticker = "NVDA"
df = yf.download(ticker, period="6mo", interval="1d")
close = df['Close'].squeeze()
ema12 = close.ewm(span=12).mean()
ema26 = close.ewm(span=26).mean()
macd_line = ema12 - ema26
signal = macd_line.ewm(span=9).mean()
histogram = macd_line - signal
fig, (ax1, ax2) = plt.subplots(2, 1, figsize=(14, 10), height_ratios=[3, 1])
ax1.plot(df.index, close, 'b-', linewidth=1.5)
ax1.set_title(f'{ticker} 股價', fontsize=14)
ax1.set_ylabel('價格 (USD)', fontsize=12)
ax1.grid(True, alpha=0.3)
ax2.plot(df.index, macd_line, 'b-', label='MACD', linewidth=1.5)
ax2.plot(df.index, signal, 'r-', label='Signal', linewidth=1.5)
colors = ['green' if v >= 0 else 'red' for v in histogram]
ax2.bar(df.index, histogram, color=colors, alpha=0.5, label='Histogram')
ax2.set_title('MACD (12, 26, 9)', fontsize=14)
ax2.legend(fontsize=10)
ax2.grid(True, alpha=0.3)
output_path = f'docs/fin/charts/{ticker}-macd.png'
plt.tight_layout()
plt.savefig(output_path, dpi=150, bbox_inches='tight')
plt.close('all')
print(f"✅ 圖表已儲存: {output_path}")
```
### 5. 布林通道圖
```python
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import yfinance as yf
import os
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Heiti TC', 'PingFang TC', 'STHeiti']
plt.rcParams['axes.unicode_minus'] = False
os.makedirs('docs/fin/charts', exist_ok=True)
ticker = "NVDA"
df = yf.download(ticker, period="6mo", interval="1d")
close = df['Close'].squeeze()
sma20 = close.rolling(20).mean()
std20 = close.rolling(20).std()
upper = sma20 + 2 * std20
lower = sma20 - 2 * std20
fig, ax = plt.subplots(figsize=(14, 8))
ax.plot(df.index, close, 'b-', linewidth=1.5, label='收盤價')
ax.plot(df.index, sma20, 'orange', linewidth=1, label='SMA(20)')
ax.plot(df.index, upper, 'red', linewidth=0.8, linestyle='--', label='上軌')
ax.plot(df.index, lower, 'green', linewidth=0.8, linestyle='--', label='下軌')
ax.fill_between(df.index, upper, lower, alpha=0.1, color='gray')
ax.set_title(f'{ticker} 布林通道 (20, 2)', fontsize=16)
ax.set_ylabel('價格 (USD)', fontsize=12)
ax.legend(fontsize=12)
ax.grid(True, alpha=0.3)
output_path = f'docs/fin/charts/{ticker}-bollinger.png'
plt.tight_layout()
plt.savefig(output_path, dpi=150, bbox_inches='tight')
plt.close('all')
print(f"✅ 圖表已儲存: {output_path}")
```
## 型態辨識圖(手動標註)
當 technical-analyst 識別出型態時,用以下模板繪製:
### 頭肩頂/底、雙頂/底、三角收斂等
```python
import matplotlib
matplotlib.use('Agg')
import matplotlib.pyplot as plt
import yfinance as yf
import numpy as np
import os
plt.rcParams['font.sans-serif'] = ['Arial Unicode MS', 'Heiti TC', 'PingFang TC', 'STHeiti']
plt.rcParams['axes.unicode_minus'] = False
os.makedirs('docs/fin/charts', exist_ok=True)
ticker = "NVDA"
pattern_name = "double-bottom" # 替換為實際型態名
pattern_label = "雙底" # 替換為中文名
df = yf.download(ticker, period="6mo", interval="1d")
close = df['Close'].squeeze()
fig, ax = plt.subplots(figsize=(14, 8))
ax.plot(df.index, close, 'b-', linewidth=1.5)
# 標註型態關鍵點(由 technical-analyst 提供具體座標)
# 範例:雙底
# bottom1_date = df.index[50]
# bottom2_date = df.index[80]
# bottom1_price = close.iloc[50]
# bottom2_price = close.iloc[80]
# neckline = 150
#
# ax.scatter([bottom1_date, bottom2_date],
# [bottom1_price, bottom2_price],
# color='green', s=150, zorder=5, marker='^', label=f'{pattern_label}底部')
# ax.axhline(y=neckline, color='orange', linestyle='--', linewidth=2, label=f'頸線 ${neckline}')
ax.set_title(f'{ticker} 型態辨識 — {pattern_label}', fontsize=16)
ax.set_ylabel('價格 (USD)', fontsize=12)
ax.legend(fontsize=12)
ax.grid(True, alpha=0.3)
output_path = f'docs/fin/charts/{ticker}-pattern-{pattern_name}.png'
plt.tight_layout()
plt.savefig(output_path, dpi=150, bbox_inches='tight')
plt.close('all')
print(f"✅ 圖表已儲存: {output_path}")
```
## 圖表命名規則
```
docs/fin/charts/
├── [TICKER]-kline.png # K 線 + 均線
├── [TICKER]-support-resistance.png # 支撐壓力
├── [TICKER]-rsi.png # RSI
├── [TICKER]-macd.png # MACD
├── [TICKER]-bollinger.png # 布林通道
├── [TICKER]-pattern-[型態名].png # 型態辨識
└── [TICKER]-volume.png # 量能分析
```
## 注意事項(必讀 Checklist
每次繪圖前,確認以下 checklist 全部打勾:
- [ ] `matplotlib.use('Agg')` 在最開頭import pyplot 之前)
- [ ] `plt.rcParams['font.sans-serif']` 已設定中文字體
- [ ] `plt.rcParams['axes.unicode_minus'] = False`
- [ ] `os.makedirs('docs/fin/charts', exist_ok=True)`
- [ ] 使用 `df['Close'].squeeze()` 取得 Series避免 MultiIndex 問題)
- [ ] `plt.savefig(path, dpi=150, bbox_inches='tight')` 而非 `plt.show()`
- [ ] `plt.close('all')` 在 savefig 之後
- [ ] `print(f"✅ 圖表已儲存: {output_path}")` 確認輸出
- [ ] 台股代號用數字(如 `2330-kline.png`
- [ ] 每種型態**獨立一張圖**,不要混在一起