add sma ema macd

This commit is contained in:
王性驊 2025-08-17 22:48:13 +08:00
parent 777a5952b8
commit df849d98b7
7 changed files with 885 additions and 283 deletions

View File

@ -1,66 +1,107 @@
package strategy
import "github.com/shopspring/decimal"
import (
"sync/atomic"
/*
EMA全名為指數移動平均線Exponential Moving Average用於平滑價格波動幫助識別市場趨勢
它與簡單移動平均線SMA不同EMA 更注重近期價格因此對價格變動的反應更迅速能更快地反映市場趨勢
"github.com/shopspring/decimal"
)
EMA 的主要特點和作用
更快速的反應
EMA SMA 更快地反映價格變動因為它給予近期數據更高的權重
識別趨勢
通過平滑價格波動EMA 有助於識別市場的整體趨勢判斷是上升趨勢還是下降趨勢
輔助交易決策
EMA 的使用可以幫助交易者判斷買入和賣出的時機例如當股價高於EMA 可能被視為買入信號反之則可能被視為賣出信號
適合短線交易
由於EMA 對價格變動的敏感性它更適合於短線交易者能更快地捕捉市場的短期波動
EMA 的計算方法
EMA 的計算涉及一個平滑因子和一個初始值然後每天更新 具體公式可以參考專業的金融網站或交易平台提供的資料
總結
EMA 是一種有用的技術分析工具尤其適合於快速變動的市場它可以幫助交易者更好地理解市場趨勢並制定相應的交易策略
*/
// α = 2 / (N+1) 平滑係數
func alphaFromPeriod(n uint) decimal.Decimal {
return decimal.NewFromInt(2).
Div(decimal.NewFromInt(int64(n)).Add(decimal.NewFromInt(1)))
}
// =========================
// EMA單寫快照多讀
// Seed: SMA 作為初始化
// =========================
type emaCore struct {
period uint
alpha decimal.Decimal
// 狀態(單 writer 更新)
value decimal.Decimal
ready bool
count uint
sum decimal.Decimal // warm-up 期間用於 SMA seed
}
func newEMACore(period uint) *emaCore {
if period == 0 {
panic("EMA period must be > 0")
}
return &emaCore{
period: period,
alpha: alphaFromPeriod(period),
}
}
// Push 僅供單 writer 呼叫
// 回傳 (當前 EMA, ready)
func (e *emaCore) Push(x decimal.Decimal) (decimal.Decimal, bool) {
e.count++
// Warm-up用 SMA 作為 seed
if !e.ready {
e.sum = e.sum.Add(x)
if e.count == e.period {
e.value = e.sum.Div(decimal.NewFromInt(int64(e.period)))
e.ready = true
}
return e.value, e.ready
}
oneMinus := decimal.NewFromInt(1).Sub(e.alpha)
e.value = e.value.Mul(oneMinus).Add(x.Mul(e.alpha))
return e.value, true
}
// ===== 快照發布(多讀零鎖) =====
type EMASnapshot struct {
Period uint
Alpha string // 十進位字串,方便落盤或序列化
Value decimal.Decimal // 目前 EMA
Ready bool
LastInput decimal.Decimal // 最近一次輸入值
Count uint // 已處理資料點
}
type EMA struct {
n uint
alp decimal.Decimal // 平滑係數 α = 2 / (n + 1)
val decimal.Decimal // 當前EMA值
ok bool // 內部旗標,用於判斷是否為第一筆資料
core *emaCore
last decimal.Decimal
snap atomic.Value // holds EMASnapshot
}
// NewEMA 建立EMA計算器
func NewEMA(n uint) *EMA {
return &EMA{
n: n,
alp: decimal.NewFromInt(2).Div(decimal.NewFromInt(int64(n + 1))),
ok: false,
}
func NewEMA(period uint) *EMA {
c := newEMACore(period)
e := &EMA{core: c}
e.snap.Store(EMASnapshot{
Period: period,
Alpha: c.alpha.String(),
})
return e
}
// Push 輸入收盤價返回當前EMA值
func (e *EMA) Push(close decimal.Decimal) (decimal.Decimal, bool) {
// 如果 n 無效,永遠回傳無效狀態
if e.n == 0 {
return decimal.Zero, false
// Update 單 writer 呼叫
func (e *EMA) Update(x decimal.Decimal) EMASnapshot {
val, ready := e.core.Push(x)
e.last = x
snap := EMASnapshot{
Period: e.core.period,
Alpha: e.core.alpha.String(),
Value: val,
Ready: ready,
LastInput: x,
Count: e.core.count,
}
if !e.ok {
// 第一筆資料直接當作EMA初始值並將狀態設為 ok
e.val = close
e.ok = true
} else {
// 後續資料使用 EMA 計算公式
// EMA = α * close + (1 - α) * prev_EMA
e.val = e.alp.Mul(close).Add(decimal.NewFromInt(1).Sub(e.alp).Mul(e.val))
}
// EMA 從第一筆資料開始就是有效的
return e.val, true
e.snap.Store(snap)
return snap
}
// GetEMA 取得目前 EMA 值
func (e *EMA) GetEMA() (decimal.Decimal, bool) {
if !e.ok {
return decimal.Zero, false // 尚未初始化
}
return e.val, true
// Load 多 reader 零鎖讀
func (e *EMA) Load() EMASnapshot {
return e.snap.Load().(EMASnapshot)
}

View File

@ -1,102 +1,170 @@
package strategy
import (
"github.com/shopspring/decimal"
"sync"
"testing"
"time"
"github.com/shopspring/decimal"
)
// --- EMA 的表格式驅動測試 (新增) ---
func almostEqual(a, b decimal.Decimal, tol float64) bool {
if a.Equal(b) {
return true
}
diff := a.Sub(b).Abs()
return diff.LessThanOrEqual(decimal.NewFromFloat(tol))
}
func TestEMA(t *testing.T) {
d10 := decimal.NewFromInt(10)
d11 := decimal.NewFromInt(11)
d12 := decimal.NewFromInt(12)
d13 := decimal.NewFromInt(13)
d20 := decimal.NewFromInt(20)
// 產生參考 EMASMA-seed前 period 筆做 SMA 當種子,之後用 α 遞迴
func emaReferenceSMASeed(seq []float64, period uint) []decimal.Decimal {
if period == 0 {
panic("period must be > 0")
}
alpha := decimal.NewFromInt(2).
Div(decimal.NewFromInt(int64(period)).Add(decimal.NewFromInt(1)))
type pushCheck struct {
wantEMA decimal.Decimal
wantOK bool
out := make([]decimal.Decimal, len(seq))
var sum decimal.Decimal
var ready bool
var emaVal decimal.Decimal
for i, x := range seq {
px := d(int64(x))
if !ready {
sum = sum.Add(px)
if uint(i+1) == period {
emaVal = sum.Div(decimal.NewFromInt(int64(period))) // SMA seed
ready = true
}
out[i] = emaVal
continue
}
emaVal = emaVal.Mul(decimal.NewFromInt(1).Sub(alpha)).Add(px.Mul(alpha))
out[i] = emaVal
}
return out
}
func TestEMA_WarmupAndStepByStep(t *testing.T) {
type step struct {
in float64
want float64
wantReady bool
}
alpha := 2.0 / (3.0 + 1.0) // period=3 => α=0.5
_ = alpha
// 序列1,2,3seed 完成 => 2.0),下一筆 4 => 0.5*4 + 0.5*2 = 3.0
steps := []step{
{in: 1, want: 0, wantReady: false}, // value 在未 ready 前不重要,但實作會回 seed 累積值;這步不檢查值
{in: 2, want: 0, wantReady: false},
{in: 3, want: 2.0, wantReady: true},
{in: 4, want: 3.0, wantReady: true},
}
testCases := []struct {
name string
n uint
inputs []decimal.Decimal
pushChecks []pushCheck
wantFinalEMA decimal.Decimal
wantFinalOK bool
}{
{
name: "EMA-3 標準計算",
n: 3, // α = 2 / (3 + 1) = 0.5
inputs: []decimal.Decimal{d10, d11, d12},
pushChecks: []pushCheck{
{d10, true}, // 第一次, EMA = 10
{decimal.NewFromFloat(10.5), true}, // 第二次, 0.5*11 + (1-0.5)*10 = 5.5 + 5 = 10.5
{decimal.NewFromFloat(11.25), true}, // 第三次, 0.5*12 + (1-0.5)*10.5 = 6 + 5.25 = 11.25
},
wantFinalEMA: decimal.NewFromFloat(11.25),
wantFinalOK: true,
},
{
name: "EMA-1 邊界情況",
n: 1, // α = 2 / (1 + 1) = 1
inputs: []decimal.Decimal{d10, d13, d11},
pushChecks: []pushCheck{
{d10, true}, // 第一次, EMA = 10
{d13, true}, // 第二次, 1*13 + 0*10 = 13
{d11, true}, // 第三次, 1*11 + 0*13 = 11
},
wantFinalEMA: d11,
wantFinalOK: true,
},
{
name: "EMA-0 無效情況",
n: 0,
inputs: []decimal.Decimal{d10, d20},
pushChecks: []pushCheck{
{decimal.Zero, false},
{decimal.Zero, false},
},
wantFinalEMA: decimal.Zero,
wantFinalOK: false,
},
{
name: "在空實例上呼叫 GetEMA",
n: 5,
inputs: []decimal.Decimal{},
pushChecks: []pushCheck{},
wantFinalEMA: decimal.Zero,
wantFinalOK: false,
},
}
e := NewEMA(3)
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
ema := NewEMA(tc.n)
for i, input := range tc.inputs {
gotEMA, gotOK := ema.Push(input)
if i < len(tc.pushChecks) {
check := tc.pushChecks[i]
if gotOK != check.wantOK {
t.Errorf("Push #%d 的 OK 狀態錯誤: got %v, want %v", i+1, gotOK, check.wantOK)
for i, st := range steps {
got := e.Update(d(int64(st.in)))
// 檢查 ready
if got.Ready != st.wantReady {
t.Fatalf("step %d: ready got=%v want=%v", i, got.Ready, st.wantReady)
}
// 使用 String() 進行比較,避免浮點數精度問題
if gotEMA.String() != check.wantEMA.String() {
t.Errorf("Push #%d 的 EMA 值錯誤: got %s, want %s", i+1, gotEMA.String(), check.wantEMA.String())
// 檢查值(只有 ready 後才精準檢查)
if got.Ready {
if !almostEqual(got.Value, d(int64(st.want)), 1e-12) {
t.Fatalf("step %d: value got=%s want=%v", i, got.Value, st.want)
}
}
// Load() 與 Update() 一致
ld := e.Load()
if ld.Ready != got.Ready || !ld.Value.Equal(got.Value) {
t.Fatalf("step %d: Load() mismatch", i)
}
finalEMA, finalOK := ema.GetEMA()
if finalOK != tc.wantFinalOK {
t.Errorf("最終 GetEMA 的 OK 狀態錯誤: got %v, want %v", finalOK, tc.wantFinalOK)
}
if finalEMA.String() != tc.wantFinalEMA.String() {
t.Errorf("最終 GetEMA 的 EMA 值錯誤: got %s, want %s", finalEMA.String(), tc.wantFinalEMA.String())
}
})
}
}
func TestEMA_MatchesReference_SMASeed(t *testing.T) {
period := uint(10)
seq := []float64{10, 11, 12, 13, 12, 11, 12, 13, 14, 15, 16, 15, 14, 13, 12, 11, 10, 11, 12, 13}
ref := emaReferenceSMASeed(seq, period)
e := NewEMA(period)
for i, x := range seq {
out := e.Update(d(int64(x)))
// 只有在 ready 後才對齊 ref
if out.Ready {
if !almostEqual(out.Value, ref[i], 1e-9) {
t.Fatalf("i=%d: EMA value got=%s ref=%s", i, out.Value, ref[i])
}
} else {
// 未 ready 時,檢查準備進度是否合理
if uint(i+1) == period && !out.Ready {
t.Fatalf("i=%d: should be ready at the period boundary", i)
}
}
}
}
func TestEMA_PeriodOne_DegenerateCase(t *testing.T) {
// period=1 => α=1第一筆即 ready之後永遠等於最新價
e := NewEMA(1)
seq := []float64{10, 11, 12, 9, 8, 15}
for i, x := range seq {
out := e.Update(d(int64(x)))
if !out.Ready {
t.Fatalf("i=%d: period=1 should be ready immediately", i)
}
if !out.Value.Equal(d(int64(x))) {
t.Fatalf("i=%d: value got=%s want=%v", i, out.Value, x)
}
// Load() 同步
if !e.Load().Value.Equal(out.Value) {
t.Fatalf("i=%d: Load() mismatch", i)
}
}
}
func TestEMA_MultiReadersSingleWriter(t *testing.T) {
e := NewEMA(5)
var wg sync.WaitGroup
stop := make(chan struct{})
// 多 reader不斷 Load(),不應當出現資料競態或 panic
reader := func() {
defer wg.Done()
for {
select {
case <-stop:
return
default:
_ = e.Load()
}
}
}
// 啟動多個讀者
for i := 0; i < 8; i++ {
wg.Add(1)
go reader()
}
// 單 writer依序更新
prices := []float64{1, 2, 3, 4, 5, 6, 7}
for _, px := range prices {
e.Update(d(int64(px)))
time.Sleep(1 * time.Millisecond)
}
// 收攤
close(stop)
wg.Wait()
// 最後至少應該是 ready
final := e.Load()
if !final.Ready {
t.Fatalf("final snapshot should be ready, got=%v", final.Ready)
}
}

View File

@ -0,0 +1,89 @@
package strategy
import (
"sync/atomic"
"github.com/shopspring/decimal"
)
// =========================
// MACD單寫快照多讀
// fastEMA / slowEMA 吃「收盤價」signalEMA 吃「MACDLine」
// 與 EMA/SMA 一致風格Update()單寫、Load()(多讀)
// =========================
// MACDSnapshot 對外發佈的不可變快照
type MACDSnapshot struct {
Params [3]uint // (fast, slow, signal)
LastPrice decimal.Decimal // 最近一次喂入的收盤價
MACDLine decimal.Decimal // DIF = fastEMA - slowEMA
SignalLine decimal.Decimal // DEA = EMA(MACDLine, signalPeriod)
Histogram decimal.Decimal // Hist = MACDLine - SignalLine
Ready bool // 三條線皆 ready 才會 true
}
// MACD 快照發佈器(單寫多讀)
type MACD struct {
// 內部用你已完成的 EMA其本身就含 SMA-seed 與 ready 狀態)
fastEMA *EMA
slowEMA *EMA
signalEMA *EMA
fastPeriod, slowPeriod, signalPeriod uint
last decimal.Decimal // 只作紀錄debug/觀察用)
snap atomic.Value // holds MACDSnapshot
}
// NewMACD 建立 MACD預設 12,26,9也可自訂
func NewMACD(fast, slow, signal uint) *MACD {
if !(fast > 0 && slow > 0 && signal > 0) {
panic("MACD periods must be > 0")
}
if fast >= slow {
panic("MACD requires fast < slow (e.g., 12 < 26)")
}
m := &MACD{
fastEMA: NewEMA(fast),
slowEMA: NewEMA(slow),
signalEMA: NewEMA(signal),
fastPeriod: fast,
slowPeriod: slow,
signalPeriod: signal,
}
// 初始化一個空快照
m.snap.Store(MACDSnapshot{Params: [3]uint{fast, slow, signal}})
return m
}
// Update 僅供單一 writer 呼叫;回傳並發佈最新快照
func (m *MACD) Update(close decimal.Decimal) MACDSnapshot {
// 先用 EMA你已改好的版本各自更新快、慢線
fast := m.fastEMA.Update(close)
slow := m.slowEMA.Update(close)
macdLine := fast.Value.Sub(slow.Value)
// 信號線吃的是 MACDLine非收盤價
sig := m.signalEMA.Update(macdLine)
ready := fast.Ready && slow.Ready && sig.Ready
snap := MACDSnapshot{
Params: [3]uint{m.fastPeriod, m.slowPeriod, m.signalPeriod},
LastPrice: close,
MACDLine: macdLine,
SignalLine: sig.Value,
Histogram: macdLine.Sub(sig.Value),
Ready: ready,
}
m.last = close
m.snap.Store(snap)
return snap
}
// Load 多 goroutine 零鎖讀取最新快照
func (m *MACD) Load() MACDSnapshot {
return m.snap.Load().(MACDSnapshot)
}

View File

@ -0,0 +1,223 @@
package strategy
import (
"sync"
"testing"
"time"
"github.com/shopspring/decimal"
)
// --------------------------
// 參考實作SMA-seed 的 EMA / MACD
// --------------------------
type refEMA struct {
period uint
alpha decimal.Decimal
count uint
sum decimal.Decimal
ready bool
value decimal.Decimal
}
func newRefEMA(period uint) *refEMA {
if period == 0 {
panic("period must be > 0")
}
return &refEMA{
period: period,
alpha: decimal.NewFromInt(2).
Div(decimal.NewFromInt(int64(period)).Add(decimal.NewFromInt(1))),
}
}
func (e *refEMA) Update(x decimal.Decimal) (val decimal.Decimal, ready bool) {
e.count++
if !e.ready {
e.sum = e.sum.Add(x)
if e.count == e.period {
e.value = e.sum.Div(decimal.NewFromInt(int64(e.period))) // SMA seed
e.ready = true
}
return e.value, e.ready
}
e.value = e.value.Mul(decimal.NewFromInt(1).Sub(e.alpha)).Add(x.Mul(e.alpha))
return e.value, true
}
type refMACD struct {
fast, slow, signal *refEMA
}
type refMACDOut struct {
MACDLine, SignalLine, Hist decimal.Decimal
Ready bool
}
func newRefMACD(fast, slow, signal uint) *refMACD {
return &refMACD{
fast: newRefEMA(fast),
slow: newRefEMA(slow),
signal: newRefEMA(signal),
}
}
func (m *refMACD) Update(close decimal.Decimal) refMACDOut {
fv, fr := m.fast.Update(close)
sv, sr := m.slow.Update(close)
macd := fv.Sub(sv)
sig, sready := m.signal.Update(macd)
ready := fr && sr && sready
return refMACDOut{
MACDLine: macd,
SignalLine: sig,
Hist: macd.Sub(sig),
Ready: ready,
}
}
// --------------------------
// Tests
// --------------------------
func TestMACD_WarmupAndStepByStep(t *testing.T) {
// 用較短的參數讓 warm-up 與變化更明顯
fast, slow, signal := uint(3), uint(6), uint(3)
m := NewMACD(fast, slow, signal)
ref := newRefMACD(fast, slow, signal)
seq := []float64{10, 11, 12, 13, 12, 11, 12, 13, 14}
for i, f := range seq {
out := m.Update(d(int64(f)))
ro := ref.Update(d(int64(f)))
// Ready 條件:三條 EMA 都完成各自 seed
if out.Ready != ro.Ready {
t.Fatalf("i=%d: ready mismatch got=%v want(ref)=%v", i, out.Ready, ro.Ready)
}
// Ready 後數值應與參考一致decimal 精準,可直接 Equal
if out.Ready {
if !out.MACDLine.Equal(ro.MACDLine) ||
!out.SignalLine.Equal(ro.SignalLine) ||
!out.Histogram.Equal(ro.Hist) {
t.Fatalf("i=%d: values mismatch\ngot: dif=%s dea=%s hist=%s\nwant: dif=%s dea=%s hist=%s",
i, out.MACDLine, out.SignalLine, out.Histogram,
ro.MACDLine, ro.SignalLine, ro.Hist)
}
}
// Load 與 Update 發佈的一致
ld := m.Load()
if ld.Ready != out.Ready ||
!ld.MACDLine.Equal(out.MACDLine) ||
!ld.SignalLine.Equal(out.SignalLine) ||
!ld.Histogram.Equal(out.Histogram) {
t.Fatalf("i=%d: Load() snapshot mismatch", i)
}
}
}
func TestMACD_HistogramSignFlip_CrossSignal(t *testing.T) {
// 讓交叉更容易出現:短週期
fast, slow, signal := uint(3), uint(6), uint(3)
m := NewMACD(fast, slow, signal)
// 人工設計一段先上後下的序列,觀察 Histogram 正負翻轉
seq := []float64{10, 11, 12, 13, 14, 13, 12, 11, 10, 9}
var prevReady bool
var prevHist decimal.Decimal
foundFlip := false
for i, px := range seq {
out := m.Update(d(int64(px)))
if !out.Ready {
prevReady = out.Ready
prevHist = out.Histogram
continue
}
if prevReady {
// 正 -> 負 或 負 -> 正 視為切換近似(對應 MACDLine 與 SignalLine 交叉)
if (prevHist.IsPositive() && out.Histogram.IsNegative()) ||
(prevHist.IsNegative() && out.Histogram.IsPositive()) {
foundFlip = true
break
}
}
prevReady = out.Ready
prevHist = out.Histogram
if i > 1000 { // 安全閥
break
}
}
if !foundFlip {
t.Fatalf("expected at least one histogram sign flip (cross), but not found")
}
}
func TestMACD_ParamsValidation(t *testing.T) {
// fast >= slow 應 panic
func() {
defer func() {
if r := recover(); r == nil {
t.Fatalf("expected panic when fast >= slow")
}
}()
_ = NewMACD(12, 12, 9)
}()
// 任一參數為 0 應 panic
func() {
defer func() {
if r := recover(); r == nil {
t.Fatalf("expected panic when any period is 0")
}
}()
_ = NewMACD(0, 26, 9)
}()
}
func TestMACD_MultiReadersSingleWriter(t *testing.T) {
m := NewMACD(12, 26, 9)
var wg sync.WaitGroup
stop := make(chan struct{})
// 多 reader不斷 Load(),不應出現競態或 panic
reader := func() {
defer wg.Done()
for {
select {
case <-stop:
return
default:
_ = m.Load()
}
}
}
// 啟動多個讀者
for i := 0; i < 8; i++ {
wg.Add(1)
go reader()
}
// 單 writer依序更新
prices := []float64{10, 11, 12, 13, 12, 11, 12, 13, 14, 15, 16, 15, 14}
for _, px := range prices {
m.Update(d(int64(px)))
time.Sleep(1 * time.Millisecond)
}
// 收攤
close(stop)
wg.Wait()
// 最後應該已經 ready
final := m.Load()
if !final.Ready {
t.Fatalf("final snapshot should be ready")
}
}

View File

@ -0,0 +1,136 @@
# 技術指標整理SMA、EMA、MACD
## 1. SMA (Simple Moving Average)
**原理**
- 計算某段時間內價格的「算術平均值」。
- 例如20 日 SMA = 最近 20 天收盤價的平均值。
### 公式
* 簡單地將一段時間內的收盤價加總,再除以該期間的交易日數。 例如5 日SMA 就是將過去 5 個交易日的收盤價加總後除以 5。
**使用時機**
- 想要觀察「長期趨勢」時(例如 50 日、200 日)。
- 作為 **支撐 / 壓力線** 參考。
**優點**
- 簡單易懂,市場上最常見的基準。
- 適合長線投資者,看清楚大趨勢。
**缺點**
- 對價格變動反應慢(容易「滯後」)。
- 在震盪盤中容易給出假訊號。
### SMA 大趨勢濾網
- 使用時機:想要知道市場長期偏多還是偏空。
- 案例:台股站上 200 日 SMA → 牛市傾向。
---
## 2. EMA (Exponential Moving Average)
**原理**
- 對「近期價格」給予更高權重的移動平均。
- 例如20 日 EMA 會比 20 日 SMA 更快跟上價格。
## EMA指數移動平均設計的目標是
* 越新的資料 → 權重越大。
* 越舊的資料 → 權重遞減,但不會突然歸零。
### 公式
* EMA = α * close + (1 - α) * prev_EMA
### 其中:
* close = 當期價格
* prev_EMA = 上期 EMA
* α = 平滑係數 (0~1 之間)
### 問題在:怎麼選 α 才合理?
這裡的依據是要讓 EMA 的「有效週期」接近 N。
* 如果用 SMA簡單移動平均每一筆資料在 N 期內的權重相等 = 1/N。
* 但 EMA 要設計成:最近資料權重大,舊資料權重指數衰減。
透過數學推導(加權和 = 1且平均壽命接近 N得到
α = 2/(n+1)
### 👉 這樣設計的結果是:
1. EMA 的「記憶長度」大約等於 N。
2. 和 N 日 SMA 的「平滑程度」接近,但又能更快反應新價格。
* **使用時機**
- 想要「更快捕捉趨勢」的交易者。
- 常用於短中期判斷,例如 12 日、26 日 EMA。
**優點**
- 反應快,能更快抓到趨勢轉折。
- 適合短線與波段交易者。
**缺點**
- 容易被假突破影響,訊號較「吵」。
- 在盤整時誤導訊號比 SMA 更多。
### EMA 快速抓轉折
- 使用時機:提早嗅到行情的變化,適合短線/波段。
- 案例BTC 出現 12/26 EMA 黃金交叉 → 多頭信號。
---
## 3. MACD (Moving Average Convergence Divergence)
**原理**
- 由兩條 EMA (快線、慢線) 的差值,再加上訊號線組成。
- MACD 線 = 12 日 EMA 26 日 EMA
- 訊號線 = MACD 線的 9 日 EMA
- 用來判斷「動能」與「趨勢強弱」。
**使用時機**
- 當價格趨勢明顯時MACD 很有用。
- 適合判斷「多空動能轉換」、「背離現象」。
- 常搭配交叉訊號使用:
- MACD 上穿訊號線 → 看多
- MACD 下穿訊號線 → 看空
**優點**
- 不只是趨勢,還能判斷「動能強弱」。
- 有交叉、背離、柱狀圖多種訊號。
**缺點**
- 還是屬於「落後指標」,轉折不會在第一時間。
- 在盤整行情中也會有很多假訊號。
### MACD 動能 & 背離
- 使用時機:想知道趨勢是否有力氣繼續。
- 案例:價格創新高,但 MACD 沒創新高 → 頂背離,可能反轉。
---
## 4. 使用上的限制
1. **都是「落後指標」**
- 不會在第一時間告訴你轉折,只是確認趨勢。
2. **震盪行情容易誤導**
- 當價格在小區間來回SMA/EMA 會不斷交叉MACD 也會亂跳。
3. **參數選擇影響很大**
- 太短 → 訊號過於頻繁。太長 → 錯過行情。
## 5. 三者組合策略
1. **SMA (200 日)**:判斷長期方向。
- 價格在 200 日 SMA 上方 → 偏多操作。
2. **EMA (12 日 & 26 日)**:判斷短中期趨勢。
- 12 日 EMA 上穿 26 日 EMA → 留意進場。
3. **MACD**:確認動能。
- MACD 黃金交叉,柱狀圖翻正 → 動能支持。
**實戰例子**
- 2020 年 4 月,比特幣站上 200 日 SMA + EMA 黃金交叉 + MACD 翻正 → 開啟牛市行情。
---
## 6. 總結比喻
- **SMA** = 老師傅,看大方向。
- **EMA** = 年輕駕駛,反應快但容易緊張。
- **MACD** = 汽車轉速表,看力道強不強。
組合起來就像開車上高速公路:
- 先看導航SMA
- 再看方向盤反應EMA
- 最後看轉速表MACD

View File

@ -1,46 +1,103 @@
package strategy
import "github.com/shopspring/decimal"
import (
"sync/atomic"
/* SMA (Simple Moving Average) 簡單移動平均線 它透過計算一段時間內股價的平均值來判斷趨勢和提供交易信號
"github.com/shopspring/decimal"
)
SMA 的計算方式
SMA 簡單地將一段時間內的收盤價加總再除以該期間的交易日數 例如5 日SMA 就是將過去 5 個交易日的收盤價加總後除以 5
SMA 的應用
1. 趨勢判斷:
SMA 可以用來判斷價格的趨勢 當股價在 SMA 上方 SMA 向上移動時表示股價處於上升趨勢
反之當股價在 SMA 下方且SMA 向下移動時表示股價處於下降趨勢
2. 支撐與阻力:
SMA 也可以被視為支撐位和阻力位 當股價下跌到SMA 附近時SMA 可能會提供支撐而當股價上漲到SMA 附近時SMA 可能會提供阻力
3. 交易信號:
移動平均線的交叉也可以產生交易信號 例如當短期SMA 線向上穿越長期SMA 線時被稱為黃金交叉可能是一個買入信號
反之當短期SMA 線向下穿越長期SMA 線時被稱為死亡交叉可能是一個賣出信號
SMA 的優缺點:
優點: SMA 計算簡單易於理解適合新手使用
缺點: SMA 對於價格變動的反應較為遲鈍可能會落後於市場特別是短期波動時可能不如其他移動平均線指標準確
*/
// =========================
// SMA固定窗寬單寫多讀 + atomic 快照)
// =========================
// smaCore單 writer 的核心計算ring buffer + 滑動平均)
type smaCore struct {
window uint
// 狀態(單 writer
buf []decimal.Decimal // ring buffer
head int // 下一個覆蓋位置
size uint // 目前填充數量(<= window
sum decimal.Decimal
ready bool
}
func newSMACore(window uint) *smaCore {
if window == 0 {
panic("SMA window must be > 0")
}
return &smaCore{
window: window,
buf: make([]decimal.Decimal, window),
}
}
// Push 只能被**單一** goroutinewriter呼叫。
func (s *smaCore) Push(x decimal.Decimal) (decimal.Decimal, bool) {
// 填充期
if s.size < s.window {
s.buf[s.head] = x
s.sum = s.sum.Add(x)
s.head = (s.head + 1) % int(s.window)
s.size++
if s.size == s.window {
s.ready = true
}
return s.sum.Div(decimal.NewFromInt(int64(s.size))), s.ready
}
// 滿窗:移除最舊,加入最新
old := s.buf[s.head]
s.buf[s.head] = x
s.head = (s.head + 1) % int(s.window)
s.sum = s.sum.Sub(old).Add(x)
avg := s.sum.Div(decimal.NewFromInt(int64(s.window)))
return avg, true
}
// SMASnapshot 對外發佈的不可變快照(多 reader 零鎖 Load
type SMASnapshot struct {
Window uint
Value decimal.Decimal
Ready bool
LastInput decimal.Decimal
Size uint // 當前填充數量(<=Window
}
// SMA 對外使用的快照發佈器(單寫多讀)
type SMA struct {
q *ringQD
core *smaCore
last decimal.Decimal
snap atomic.Value // holds SMASnapshot
}
// NewSMA 建立SMA計算器
func NewSMA(n uint) *SMA { return &SMA{q: newRingQD(n)} }
func NewSMA(window uint) *SMA {
s := &SMA{core: newSMACore(window)}
s.snap.Store(SMASnapshot{Window: window})
return s
}
// Push 輸入收盤價返回當前SMA值
func (s *SMA) Push(close decimal.Decimal) (decimal.Decimal, bool) {
s.q.push(close)
if !s.q.ready() {
return decimal.Zero, false // 尚未湊滿資料
// Update 僅允許**單一 writer**呼叫
func (s *SMA) Update(x decimal.Decimal) SMASnapshot {
val, ready := s.core.Push(x)
s.last = x
ss := SMASnapshot{
Window: s.core.window,
Value: val,
Ready: ready,
LastInput: x,
Size: s.core.size,
}
return s.q.mean(), true
s.snap.Store(ss)
return ss
}
// GetSMA 取得目前 SMA 值
func (s *SMA) GetSMA() (decimal.Decimal, bool) {
if !s.q.ready() {
return decimal.Zero, false // 尚未湊滿資料
}
return s.q.mean(), true
// Load 多 reader 零鎖讀最新快照
func (s *SMA) Load() SMASnapshot {
return s.snap.Load().(SMASnapshot)
}

View File

@ -1,132 +1,120 @@
package strategy
import (
"github.com/shopspring/decimal"
"sync"
"testing"
"time"
"github.com/shopspring/decimal"
)
// --- SMA 的表格式驅動測試 ---
func d(i int64) decimal.Decimal { return decimal.NewFromInt(i) }
func TestSMA(t *testing.T) {
d10 := decimal.NewFromInt(10)
d20 := decimal.NewFromInt(20)
d30 := decimal.NewFromInt(30)
d40 := decimal.NewFromInt(40)
d50 := decimal.NewFromInt(50)
// 定義 Push 過程中的檢查點結構
type pushCheck struct {
wantSMA decimal.Decimal
wantOK bool
func TestSMA_WarmupAndSliding(t *testing.T) {
type step struct {
in int64
wantVal string // 用字串比對可避免浮點誤差decimal 本就精準)
ready bool
}
testCases := []struct {
tests := []struct {
name string
n uint
inputs []decimal.Decimal
pushChecks []pushCheck // 驗證每一次 Push 的回傳值
wantFinalSMA decimal.Decimal // 驗證最後 GetSMA 的回傳值
wantFinalOK bool
window uint
steps []step
}{
{
name: "SMA-5 未滿載",
n: 5,
inputs: []decimal.Decimal{d10, d20, d30},
pushChecks: []pushCheck{
{decimal.Zero, false},
{decimal.Zero, false},
{decimal.Zero, false},
name: "warmup_then_ready_and_slide",
window: 3,
steps: []step{
{in: 1, wantVal: "1", ready: false}, // [1] avg=1
{in: 2, wantVal: "1.5", ready: false}, // [1,2] avg=1.5
{in: 3, wantVal: "2", ready: true}, // [1,2,3] avg=2
{in: 4, wantVal: "3", ready: true}, // [2,3,4] avg=3
{in: 5, wantVal: "4", ready: true}, // [3,4,5] avg=4
{in: 6, wantVal: "5", ready: true}, // [4,5,6] avg=5
{in: 7, wantVal: "6", ready: true}, // [5,6,7] avg=6
},
wantFinalSMA: decimal.Zero,
wantFinalOK: false,
},
{
name: "SMA-3 剛好滿載",
n: 3,
inputs: []decimal.Decimal{d10, d20, d30},
pushChecks: []pushCheck{
{decimal.Zero, false},
{decimal.Zero, false},
{decimal.NewFromInt(20), true}, // (10+20+30)/3
name: "window_1_behaves_as_latest_value",
window: 1,
steps: []step{
{in: 10, wantVal: "10", ready: true},
{in: 11, wantVal: "11", ready: true},
{in: 12, wantVal: "12", ready: true},
},
wantFinalSMA: decimal.NewFromInt(20),
wantFinalOK: true,
},
{
name: "SMA-3 滾動計算",
n: 3,
inputs: []decimal.Decimal{d10, d20, d30, d40, d50},
pushChecks: []pushCheck{
{decimal.Zero, false},
{decimal.Zero, false},
{decimal.NewFromInt(20), true}, // (10+20+30)/3
{decimal.NewFromInt(30), true}, // (20+30+40)/3
{decimal.NewFromInt(40), true}, // (30+40+50)/3
},
wantFinalSMA: decimal.NewFromInt(40),
wantFinalOK: true,
},
{
name: "SMA-1 邊界情況",
n: 1,
inputs: []decimal.Decimal{d10, d20, d30},
pushChecks: []pushCheck{
{d10, true},
{d20, true},
{d30, true},
},
wantFinalSMA: d30,
wantFinalOK: true,
},
{
name: "SMA-0 無效情況",
n: 0,
inputs: []decimal.Decimal{d10, d20, d30},
pushChecks: []pushCheck{
{decimal.Zero, false},
{decimal.Zero, false},
{decimal.Zero, false},
},
wantFinalSMA: decimal.Zero,
wantFinalOK: false,
},
{
name: "在空實例上呼叫 GetSMA",
n: 5,
inputs: []decimal.Decimal{},
pushChecks: []pushCheck{},
wantFinalSMA: decimal.Zero,
wantFinalOK: false,
},
}
for _, tc := range testCases {
for _, tc := range tests {
tc := tc
t.Run(tc.name, func(t *testing.T) {
sma := NewSMA(tc.n)
// 驗證每一次 Push 的結果
for i, input := range tc.inputs {
gotSMA, gotOK := sma.Push(input)
// 確保 pushChecks 陣列不會索引越界
if i < len(tc.pushChecks) {
check := tc.pushChecks[i]
if gotOK != check.wantOK {
t.Errorf("Push #%d 的 OK 狀態錯誤: got %v, want %v", i+1, gotOK, check.wantOK)
s := NewSMA(tc.window)
for i, st := range tc.steps {
got := s.Update(d(st.in))
if got.Value.String() != st.wantVal {
t.Fatalf("step %d: got value %s, want %s", i, got.Value, st.wantVal)
}
if !gotSMA.Equals(check.wantSMA) {
t.Errorf("Push #%d 的 SMA 值錯誤: got %s, want %s", i+1, gotSMA.String(), check.wantSMA.String())
if got.Ready != st.ready {
t.Fatalf("step %d: ready mismatch, got %v, want %v", i, got.Ready, st.ready)
}
// Load() 應該等於最新快照
ld := s.Load()
if ld.Value.String() != st.wantVal || ld.Ready != st.ready {
t.Fatalf("step %d: Load() not latest snapshot", i)
}
}
// 在所有 Push 操作完成後,驗證最終 GetSMA 的結果
finalSMA, finalOK := sma.GetSMA()
if finalOK != tc.wantFinalOK {
t.Errorf("最終 GetSMA 的 OK 狀態錯誤: got %v, want %v", finalOK, tc.wantFinalOK)
}
if !finalSMA.Equals(tc.wantFinalSMA) {
t.Errorf("最終 GetSMA 的 SMA 值錯誤: got %s, want %s", finalSMA.String(), tc.wantFinalSMA.String())
}
})
}
}
func TestSMA_NewZeroWindowShouldPanic(t *testing.T) {
defer func() {
if r := recover(); r == nil {
t.Fatalf("expected panic when window=0")
}
}()
_ = NewSMA(0)
}
func TestSMA_MultiReadersSingleWriter(t *testing.T) {
s := NewSMA(3)
var wg sync.WaitGroup
stop := make(chan struct{})
// 多 reader 併發讀取(零鎖)
reader := func() {
defer wg.Done()
for {
select {
case <-stop:
return
default:
_ = s.Load() // 不做判斷,重點是不得 panic
}
}
}
// 啟動多個讀者
for i := 0; i < 8; i++ {
wg.Add(1)
go reader()
}
// 單 writer 更新
prices := []int64{1, 2, 3, 4, 5, 6, 7}
for _, p := range prices {
s.Update(d(p))
time.Sleep(1 * time.Millisecond)
}
// 停 reader
close(stop)
wg.Wait()
// 最終應為 [5,6,7] 的平均=6且 ready=true
got := s.Load()
if got.Value.String() != "6" || !got.Ready {
t.Fatalf("final snapshot mismatch: got value=%s ready=%v", got.Value, got.Ready)
}
}