feat: add ema strategy

This commit is contained in:
王性驊 2025-08-15 09:36:36 +08:00
parent 50db76e093
commit 777a5952b8
6 changed files with 295 additions and 16 deletions

View File

@ -2,26 +2,65 @@ package strategy
import "github.com/shopspring/decimal" import "github.com/shopspring/decimal"
/************** EMA 指數移動平均 **************/ /*
EMA全名為指數移動平均線Exponential Moving Average用於平滑價格波動幫助識別市場趨勢
它與簡單移動平均線SMA不同EMA 更注重近期價格因此對價格變動的反應更迅速能更快地反映市場趨勢
EMA 的主要特點和作用
更快速的反應
EMA SMA 更快地反映價格變動因為它給予近期數據更高的權重
識別趨勢
通過平滑價格波動EMA 有助於識別市場的整體趨勢判斷是上升趨勢還是下降趨勢
輔助交易決策
EMA 的使用可以幫助交易者判斷買入和賣出的時機例如當股價高於EMA 可能被視為買入信號反之則可能被視為賣出信號
適合短線交易
由於EMA 對價格變動的敏感性它更適合於短線交易者能更快地捕捉市場的短期波動
EMA 的計算方法
EMA 的計算涉及一個平滑因子和一個初始值然後每天更新 具體公式可以參考專業的金融網站或交易平台提供的資料
總結
EMA 是一種有用的技術分析工具尤其適合於快速變動的市場它可以幫助交易者更好地理解市場趨勢並制定相應的交易策略
*/
type EMA struct { type EMA struct {
n int n uint
alp decimal.Decimal // 平滑係數 α = 2 / (n + 1) alp decimal.Decimal // 平滑係數 α = 2 / (n + 1)
val decimal.Decimal // 當前EMA值 val decimal.Decimal // 當前EMA值
ok bool ok bool // 內部旗標,用於判斷是否為第一筆資料
} }
func NewEMA(n int) *EMA { // NewEMA 建立EMA計算器
return &EMA{n: n, alp: decimal.NewFromFloat(2.0).Div(decimal.NewFromInt(int64(n + 1)))} func NewEMA(n uint) *EMA {
return &EMA{
n: n,
alp: decimal.NewFromInt(2).Div(decimal.NewFromInt(int64(n + 1))),
ok: false,
}
} }
// Push 輸入收盤價返回當前EMA值
func (e *EMA) Push(close decimal.Decimal) (decimal.Decimal, bool) { func (e *EMA) Push(close decimal.Decimal) (decimal.Decimal, bool) {
// 如果 n 無效,永遠回傳無效狀態
if e.n == 0 {
return decimal.Zero, false
}
if !e.ok { if !e.ok {
// 第一筆資料直接當作EMA初始值 // 第一筆資料直接當作EMA初始值,並將狀態設為 ok
e.val = close e.val = close
e.ok = true e.ok = true
return e.val, false } 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
}
// GetEMA 取得目前 EMA 值
func (e *EMA) GetEMA() (decimal.Decimal, bool) {
if !e.ok {
return decimal.Zero, false // 尚未初始化
} }
// EMA計算公式
e.val = e.alp.Mul(close).Add(decimal.NewFromInt(1).Sub(e.alp).Mul(e.val))
return e.val, true return e.val, true
} }

View File

@ -0,0 +1,102 @@
package strategy
import (
"github.com/shopspring/decimal"
"testing"
)
// --- EMA 的表格式驅動測試 (新增) ---
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)
type pushCheck struct {
wantEMA decimal.Decimal
wantOK bool
}
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,
},
}
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)
}
// 使用 String() 進行比較,避免浮點數精度問題
if gotEMA.String() != check.wantEMA.String() {
t.Errorf("Push #%d 的 EMA 值錯誤: got %s, want %s", i+1, gotEMA.String(), check.wantEMA.String())
}
}
}
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())
}
})
}
}

View File

@ -6,14 +6,16 @@ import (
) )
/************** 基礎的固定長度隊列,用來計算移動平均 **************/ /************** 基礎的固定長度隊列,用來計算移動平均 **************/
// 請注意: 目前為非併發版本,使用場警還不需要
type ringQD struct { type ringQD struct {
N int // 窗口大小(需要保留的資料數量) N uint // 窗口大小(需要保留的資料數量)需要保留的數量沒有負數
l *list.List // 用於儲存資料的雙向鏈表 l *list.List // 用於儲存資料的雙向鏈表
sum decimal.Decimal // 當前窗口的總和,方便快速計算平均值 sum decimal.Decimal // 當前窗口的總和,方便快速計算平均值
} }
// 建立一個固定長度的隊列 // 建立一個固定長度的隊列
func newRingQD(n int) *ringQD { func newRingQD(n uint) *ringQD {
return &ringQD{N: n, l: list.New(), sum: decimal.Zero} return &ringQD{N: n, l: list.New(), sum: decimal.Zero}
} }
@ -22,7 +24,8 @@ func (q *ringQD) push(x decimal.Decimal) {
q.l.PushBack(x) q.l.PushBack(x)
q.sum = q.sum.Add(x) q.sum = q.sum.Add(x)
// 如果超出最大長度,移除最舊的數值 // 如果超出最大長度,移除最舊的數值
if q.l.Len() > q.N {
if uint(q.l.Len()) > q.N {
f := q.l.Front() f := q.l.Front()
q.sum = q.sum.Sub(f.Value.(decimal.Decimal)) q.sum = q.sum.Sub(f.Value.(decimal.Decimal))
q.l.Remove(f) q.l.Remove(f)
@ -30,13 +33,14 @@ func (q *ringQD) push(x decimal.Decimal) {
} }
// ready判斷隊列是否已經填滿 // ready判斷隊列是否已經填滿
func (q *ringQD) ready() bool { return q.l.Len() == q.N } func (q *ringQD) ready() bool { return q.N > 0 && uint(q.l.Len()) == q.N }
// mean計算平均值 // mean計算平均值
func (q *ringQD) mean() decimal.Decimal { func (q *ringQD) mean() decimal.Decimal {
if q.l.Len() == 0 { if q.l.Len() == 0 {
return decimal.Zero return decimal.Zero
} }
return q.sum.Div(decimal.NewFromInt(int64(q.l.Len()))) return q.sum.Div(decimal.NewFromInt(int64(q.l.Len())))
} }
@ -46,5 +50,6 @@ func (q *ringQD) values() []decimal.Decimal {
for e := q.l.Front(); e != nil; e = e.Next() { for e := q.l.Front(); e != nil; e = e.Next() {
out = append(out, e.Value.(decimal.Decimal)) out = append(out, e.Value.(decimal.Decimal))
} }
return out return out
} }

View File

@ -20,7 +20,7 @@ func TestRingQD(t *testing.T) {
// 定義測試案例的結構 // 定義測試案例的結構
testCases := []struct { testCases := []struct {
name string // 測試案例的名稱 name string // 測試案例的名稱
n int // ringQD 的大小 n uint // ringQD 的大小
inputs []decimal.Decimal // 輸入的數值序列 inputs []decimal.Decimal // 輸入的數值序列
wantSum decimal.Decimal // 預期的總和 wantSum decimal.Decimal // 預期的總和
wantMean decimal.Decimal // 預期的平均值 wantMean decimal.Decimal // 預期的平均值
@ -88,7 +88,7 @@ func TestRingQD(t *testing.T) {
wantSum: decimal.Zero, wantSum: decimal.Zero,
wantMean: decimal.Zero, wantMean: decimal.Zero,
wantValues: []decimal.Decimal{}, wantValues: []decimal.Decimal{},
wantReady: true, wantReady: false,
}, },
} }

View File

@ -25,7 +25,7 @@ type SMA struct {
} }
// NewSMA 建立SMA計算器 // NewSMA 建立SMA計算器
func NewSMA(n int) *SMA { return &SMA{q: newRingQD(n)} } func NewSMA(n uint) *SMA { return &SMA{q: newRingQD(n)} }
// Push 輸入收盤價返回當前SMA值 // Push 輸入收盤價返回當前SMA值
func (s *SMA) Push(close decimal.Decimal) (decimal.Decimal, bool) { func (s *SMA) Push(close decimal.Decimal) (decimal.Decimal, bool) {
@ -41,5 +41,6 @@ func (s *SMA) GetSMA() (decimal.Decimal, bool) {
if !s.q.ready() { if !s.q.ready() {
return decimal.Zero, false // 尚未湊滿資料 return decimal.Zero, false // 尚未湊滿資料
} }
return s.q.mean(), true return s.q.mean(), true
} }

View File

@ -0,0 +1,132 @@
package strategy
import (
"github.com/shopspring/decimal"
"testing"
)
// --- SMA 的表格式驅動測試 ---
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
}
testCases := []struct {
name string
n uint
inputs []decimal.Decimal
pushChecks []pushCheck // 驗證每一次 Push 的回傳值
wantFinalSMA decimal.Decimal // 驗證最後 GetSMA 的回傳值
wantFinalOK bool
}{
{
name: "SMA-5 未滿載",
n: 5,
inputs: []decimal.Decimal{d10, d20, d30},
pushChecks: []pushCheck{
{decimal.Zero, false},
{decimal.Zero, false},
{decimal.Zero, false},
},
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
},
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 {
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)
}
if !gotSMA.Equals(check.wantSMA) {
t.Errorf("Push #%d 的 SMA 值錯誤: got %s, want %s", i+1, gotSMA.String(), check.wantSMA.String())
}
}
}
// 在所有 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())
}
})
}
}