add sma ema macd
This commit is contained in:
parent
df849d98b7
commit
66b734350d
|
@ -1,11 +1,8 @@
|
||||||
package strategy
|
package strategy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/shopspring/decimal"
|
"github.com/shopspring/decimal"
|
||||||
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// --------------------------
|
// --------------------------
|
||||||
|
@ -121,37 +118,53 @@ func TestMACD_WarmupAndStepByStep(t *testing.T) {
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestMACD_HistogramSignFlip_CrossSignal(t *testing.T) {
|
func TestMACD_HistogramSignFlip_CrossSignal(t *testing.T) {
|
||||||
// 讓交叉更容易出現:短週期
|
// 讓交叉更容易出現:更短的週期 + 更長、先上後下的序列
|
||||||
fast, slow, signal := uint(3), uint(6), uint(3)
|
fast, slow, signal := uint(2), uint(4), uint(2)
|
||||||
m := NewMACD(fast, slow, signal)
|
m := NewMACD(fast, slow, signal)
|
||||||
|
|
||||||
// 人工設計一段先上後下的序列,觀察 Histogram 正負翻轉
|
// 這段序列會:上漲 -> 回落 -> 再回升,通常至少會觸發一次交叉
|
||||||
seq := []float64{10, 11, 12, 13, 14, 13, 12, 11, 10, 9}
|
seq := []float64{
|
||||||
var prevReady bool
|
10, 10.5, 11, 11.5, 12, 12.5, // 上
|
||||||
var prevHist decimal.Decimal
|
12, 11.5, 11, 10.5, 10, 9.5, // 下
|
||||||
foundFlip := false
|
10, 10.5, 11, // 再上
|
||||||
|
}
|
||||||
|
|
||||||
|
// 等 ready 之後再看柱狀圖正負翻轉
|
||||||
|
var (
|
||||||
|
seenReady bool
|
||||||
|
prevHist decimal.Decimal
|
||||||
|
foundFlip bool
|
||||||
|
)
|
||||||
|
|
||||||
for i, px := range seq {
|
for i, px := range seq {
|
||||||
out := m.Update(d(int64(px)))
|
out := m.Update(d(int64(px)))
|
||||||
|
|
||||||
|
// 還沒 ready 就繼續推進
|
||||||
if !out.Ready {
|
if !out.Ready {
|
||||||
prevReady = out.Ready
|
|
||||||
prevHist = out.Histogram
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if prevReady {
|
|
||||||
// 正 -> 負 或 負 -> 正 視為切換近似(對應 MACDLine 與 SignalLine 交叉)
|
if !seenReady {
|
||||||
if (prevHist.IsPositive() && out.Histogram.IsNegative()) ||
|
// 第一次 ready,初始化 prevHist
|
||||||
(prevHist.IsNegative() && out.Histogram.IsPositive()) {
|
prevHist = out.Histogram
|
||||||
foundFlip = true
|
seenReady = true
|
||||||
break
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 檢查正負翻轉(對應 MACDLine 與 SignalLine 的交叉)
|
||||||
|
if (prevHist.IsPositive() && out.Histogram.IsNegative()) ||
|
||||||
|
(prevHist.IsNegative() && out.Histogram.IsPositive()) {
|
||||||
|
foundFlip = true
|
||||||
|
break
|
||||||
}
|
}
|
||||||
prevReady = out.Ready
|
|
||||||
prevHist = out.Histogram
|
prevHist = out.Histogram
|
||||||
if i > 1000 { // 安全閥
|
|
||||||
|
// 安全閥(理論上不會到)
|
||||||
|
if i > 2000 {
|
||||||
break
|
break
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !foundFlip {
|
if !foundFlip {
|
||||||
t.Fatalf("expected at least one histogram sign flip (cross), but not found")
|
t.Fatalf("expected at least one histogram sign flip (cross), but not found")
|
||||||
}
|
}
|
||||||
|
@ -178,46 +191,3 @@ func TestMACD_ParamsValidation(t *testing.T) {
|
||||||
_ = NewMACD(0, 26, 9)
|
_ = 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")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
|
@ -1,55 +0,0 @@
|
||||||
package strategy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"container/list"
|
|
||||||
"github.com/shopspring/decimal"
|
|
||||||
)
|
|
||||||
|
|
||||||
/************** 基礎的固定長度隊列,用來計算移動平均 **************/
|
|
||||||
// 請注意: 目前為非併發版本,使用場警還不需要
|
|
||||||
|
|
||||||
type ringQD struct {
|
|
||||||
N uint // 窗口大小(需要保留的資料數量)需要保留的數量沒有負數
|
|
||||||
l *list.List // 用於儲存資料的雙向鏈表
|
|
||||||
sum decimal.Decimal // 當前窗口的總和,方便快速計算平均值
|
|
||||||
}
|
|
||||||
|
|
||||||
// 建立一個固定長度的隊列
|
|
||||||
func newRingQD(n uint) *ringQD {
|
|
||||||
return &ringQD{N: n, l: list.New(), sum: decimal.Zero}
|
|
||||||
}
|
|
||||||
|
|
||||||
// push:將新的數值放入隊列,並維護總和
|
|
||||||
func (q *ringQD) push(x decimal.Decimal) {
|
|
||||||
q.l.PushBack(x)
|
|
||||||
q.sum = q.sum.Add(x)
|
|
||||||
// 如果超出最大長度,移除最舊的數值
|
|
||||||
|
|
||||||
if uint(q.l.Len()) > q.N {
|
|
||||||
f := q.l.Front()
|
|
||||||
q.sum = q.sum.Sub(f.Value.(decimal.Decimal))
|
|
||||||
q.l.Remove(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ready:判斷隊列是否已經填滿
|
|
||||||
func (q *ringQD) ready() bool { return q.N > 0 && uint(q.l.Len()) == q.N }
|
|
||||||
|
|
||||||
// mean:計算平均值
|
|
||||||
func (q *ringQD) mean() decimal.Decimal {
|
|
||||||
if q.l.Len() == 0 {
|
|
||||||
return decimal.Zero
|
|
||||||
}
|
|
||||||
|
|
||||||
return q.sum.Div(decimal.NewFromInt(int64(q.l.Len())))
|
|
||||||
}
|
|
||||||
|
|
||||||
// values:返回隊列中所有的值(複製一份,不影響原資料)
|
|
||||||
func (q *ringQD) values() []decimal.Decimal {
|
|
||||||
out := make([]decimal.Decimal, 0, q.l.Len())
|
|
||||||
for e := q.l.Front(); e != nil; e = e.Next() {
|
|
||||||
out = append(out, e.Value.(decimal.Decimal))
|
|
||||||
}
|
|
||||||
|
|
||||||
return out
|
|
||||||
}
|
|
|
@ -1,129 +0,0 @@
|
||||||
package strategy
|
|
||||||
|
|
||||||
import (
|
|
||||||
"github.com/shopspring/decimal"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// --- 表格式驅動測試 (Table-Driven Test) ---
|
|
||||||
|
|
||||||
func TestRingQD(t *testing.T) {
|
|
||||||
// 為了方便測試,預先建立幾個 decimal 數值
|
|
||||||
d10 := decimal.NewFromInt(10)
|
|
||||||
d20 := decimal.NewFromInt(20)
|
|
||||||
d30 := decimal.NewFromInt(30)
|
|
||||||
d40 := decimal.NewFromInt(40)
|
|
||||||
d50 := decimal.NewFromInt(50)
|
|
||||||
d_neg5 := decimal.NewFromInt(-5)
|
|
||||||
|
|
||||||
// 定義測試案例的結構
|
|
||||||
testCases := []struct {
|
|
||||||
name string // 測試案例的名稱
|
|
||||||
n uint // ringQD 的大小
|
|
||||||
inputs []decimal.Decimal // 輸入的數值序列
|
|
||||||
wantSum decimal.Decimal // 預期的總和
|
|
||||||
wantMean decimal.Decimal // 預期的平均值
|
|
||||||
wantValues []decimal.Decimal // 預期隊列中最後的值
|
|
||||||
wantReady bool // 預期的就緒狀態
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "未滿載的情況",
|
|
||||||
n: 5,
|
|
||||||
inputs: []decimal.Decimal{d10, d20},
|
|
||||||
wantSum: decimal.NewFromInt(30),
|
|
||||||
wantMean: decimal.NewFromInt(15),
|
|
||||||
wantValues: []decimal.Decimal{d10, d20},
|
|
||||||
wantReady: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "剛好滿載的情況",
|
|
||||||
n: 3,
|
|
||||||
inputs: []decimal.Decimal{d10, d20, d30},
|
|
||||||
wantSum: decimal.NewFromInt(60),
|
|
||||||
wantMean: decimal.NewFromInt(20),
|
|
||||||
wantValues: []decimal.Decimal{d10, d20, d30},
|
|
||||||
wantReady: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "超出容量,舊資料被移除",
|
|
||||||
n: 3,
|
|
||||||
inputs: []decimal.Decimal{d10, d20, d30, d40, d50},
|
|
||||||
wantSum: decimal.NewFromInt(120), // 30 + 40 + 50
|
|
||||||
wantMean: decimal.NewFromInt(40),
|
|
||||||
wantValues: []decimal.Decimal{d30, d40, d50},
|
|
||||||
wantReady: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "包含零與負數",
|
|
||||||
n: 4,
|
|
||||||
inputs: []decimal.Decimal{d10, d_neg5, decimal.Zero, d30, d_neg5},
|
|
||||||
wantSum: decimal.NewFromInt(20), // -5 + 0 + 30 + (-5)
|
|
||||||
wantMean: decimal.NewFromInt(5),
|
|
||||||
wantValues: []decimal.Decimal{d_neg5, decimal.Zero, d30, d_neg5},
|
|
||||||
wantReady: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "初始為空",
|
|
||||||
n: 5,
|
|
||||||
inputs: []decimal.Decimal{},
|
|
||||||
wantSum: decimal.Zero,
|
|
||||||
wantMean: decimal.Zero,
|
|
||||||
wantValues: []decimal.Decimal{},
|
|
||||||
wantReady: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "N 為 1 的邊界情況",
|
|
||||||
n: 1,
|
|
||||||
inputs: []decimal.Decimal{d10, d20, d30},
|
|
||||||
wantSum: d30,
|
|
||||||
wantMean: d30,
|
|
||||||
wantValues: []decimal.Decimal{d30},
|
|
||||||
wantReady: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "N 為 0 的無效情況",
|
|
||||||
n: 0,
|
|
||||||
inputs: []decimal.Decimal{d10, d20, d30},
|
|
||||||
wantSum: decimal.Zero,
|
|
||||||
wantMean: decimal.Zero,
|
|
||||||
wantValues: []decimal.Decimal{},
|
|
||||||
wantReady: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 遍歷所有測試案例
|
|
||||||
for _, tc := range testCases {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
// 針對每個案例建立一個新的 ringQD
|
|
||||||
q := newRingQD(tc.n)
|
|
||||||
|
|
||||||
// 依序推入資料
|
|
||||||
for _, val := range tc.inputs {
|
|
||||||
q.push(val)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 驗證總和
|
|
||||||
if !q.sum.Equals(tc.wantSum) {
|
|
||||||
t.Errorf("Sum 錯誤:got %v, want %v", q.sum, tc.wantSum)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 驗證平均值
|
|
||||||
gotMean := q.mean()
|
|
||||||
if !gotMean.Equals(tc.wantMean) {
|
|
||||||
t.Errorf("Mean 錯誤:got %v, want %v", gotMean, tc.wantMean)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 驗證就緒狀態
|
|
||||||
if q.ready() != tc.wantReady {
|
|
||||||
t.Errorf("Ready 狀態錯誤:got %v, want %v", q.ready(), tc.wantReady)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 驗證隊列中的值
|
|
||||||
gotValues := q.values()
|
|
||||||
if !reflect.DeepEqual(gotValues, tc.wantValues) {
|
|
||||||
t.Errorf("Values 錯誤:got %v, want %v", gotValues, tc.wantValues)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
Loading…
Reference in New Issue