369 lines
11 KiB
Markdown
369 lines
11 KiB
Markdown
|
|
---
|
|||
|
|
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`)
|
|||
|
|
- [ ] 每種型態**獨立一張圖**,不要混在一起
|