Files
cryptoHermes/internal/usecase/indicator_test.go
dela fa769331c2 feat(market): Phase 1 扩展 — 更多 symbol + Bollinger/Vegas + 衍生品信号
- collector.symbols 与 supportedSymbols 加 SOL/BNB/DOGE,env-default 与 README 同步
- entity 追加 TechnicalStructure.Intervals(每周期 Bollinger + Vegas,omitempty 不破坏既有字段)与 DerivativesBundle.Signal
- 新增纯解析 usecase derivatives_signal.go:fundingBias 绝对阈值、oiSignal 用 P20 baseline + 价格方向(OI up + 价格 up/down → long/short_building,funding 不参与方向)、lsrRegime 绝对阈值;signal 数据不足走 warnings 进 DataQuality
- indicator.go 加 sma/stddev/ema/bollinger/vegas + computeIntervalTechnicals;IndicatorComputer 签名收 klines map
- harness 同步:G12 扩入 derivatives_signal.go(含 Makefile G12.5/6 + ADR-0002 适用范围 + project-map / harness-health 更新)
2026-05-24 23:19:57 +08:00

634 lines
20 KiB
Go
Raw 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.
package usecase
import (
"strconv"
"testing"
"cryptoHermes/internal/entity"
"github.com/stretchr/testify/require"
)
// ---- 测试辅助 ----------------------------------------------------------
// mkKline 构造一根 1h K 线;只填指标关心的字段。
// openTime 单位毫秒hour 序号映射到 1h 时基。
func mkKline(hourIdx int64, open, high, low, close string) entity.Kline {
const hourMs = int64(3600 * 1000)
return entity.Kline{
Symbol: "BTCUSDT",
Interval: "1h",
OpenTime: hourIdx * hourMs,
CloseTime: hourIdx*hourMs + hourMs - 1,
Open: open,
High: high,
Low: low,
Close: close,
IsClosed: true,
}
}
func mkLSR(hourIdx int64, ratio string) entity.LongShortRatio {
const hourMs = int64(3600 * 1000)
return entity.LongShortRatio{
Symbol: "BTCUSDT",
Period: "1h",
RatioType: entity.RatioTypeGlobalAccount,
Timestamp: hourIdx * hourMs,
LongShortRatio: ratio,
}
}
// 单调上升序列:从 baseHigh 起,每根 K 线 high/low/close 各 +stepUSD。
// 共 n 根hourIdx 从 0 起。
func ascending(n int, baseLow, step float64) []entity.Kline {
out := make([]entity.Kline, n)
for i := 0; i < n; i++ {
lo := baseLow + float64(i)*step
hi := lo + step
mid := (lo + hi) / 2
out[i] = mkKline(int64(i),
strconv.FormatFloat(lo, 'f', -1, 64),
strconv.FormatFloat(hi, 'f', -1, 64),
strconv.FormatFloat(lo, 'f', -1, 64),
strconv.FormatFloat(mid, 'f', -1, 64),
)
}
return out
}
func descending(n int, baseHigh, step float64) []entity.Kline {
out := make([]entity.Kline, n)
for i := 0; i < n; i++ {
hi := baseHigh - float64(i)*step
lo := hi - step
mid := (lo + hi) / 2
out[i] = mkKline(int64(i),
strconv.FormatFloat(hi, 'f', -1, 64),
strconv.FormatFloat(hi, 'f', -1, 64),
strconv.FormatFloat(lo, 'f', -1, 64),
strconv.FormatFloat(mid, 'f', -1, 64),
)
}
return out
}
// ---- parsePrice -------------------------------------------------------
func TestParsePrice(t *testing.T) {
cases := []struct {
name string
in string
want float64
ok bool
}{
{"normal", "108000.12", 108000.12, true},
{"zero", "0", 0, true},
{"negative", "-1.5", -1.5, true},
{"scientific", "1e3", 1000, true},
{"empty", "", 0, false},
{"non-numeric", "abc", 0, false},
{"whitespace", " 1.0 ", 0, false},
{"nan", "NaN", 0, false},
{"posinf", "Inf", 0, false},
{"neginf", "-Inf", 0, false},
}
for _, c := range cases {
t.Run(c.name, func(t *testing.T) {
got, ok := parsePrice(c.in)
require.Equal(t, c.ok, ok, "ok mismatch for %q", c.in)
if c.ok {
require.InDelta(t, c.want, got, 1e-9)
}
})
}
}
// ---- formatPrice ------------------------------------------------------
func TestFormatPrice(t *testing.T) {
require.Equal(t, "0", formatPrice(0))
require.Equal(t, "1.5", formatPrice(1.5))
require.Equal(t, "108000.12", formatPrice(108000.12))
require.Equal(t, "-3", formatPrice(-3))
}
// ---- percentile -------------------------------------------------------
func TestPercentile(t *testing.T) {
// type-7 linear interpolation (NumPy default)
require.Equal(t, 0.0, percentile(nil, 0.5), "empty → 0")
require.Equal(t, 42.0, percentile([]float64{42}, 0.95), "N=1")
// N=2: P95: rank=(2-1)*0.95=0.95, between [0]=10 and [1]=20 → 10 + 0.95*10 = 19.5
require.InDelta(t, 19.5, percentile([]float64{10, 20}, 0.95), 1e-9)
require.InDelta(t, 10.5, percentile([]float64{10, 20}, 0.05), 1e-9)
// N=20: rank=19*0.95=18.05, between [18] and [19]
xs := make([]float64, 20)
for i := range xs {
xs[i] = float64(i + 1) // 1..20
}
// rank=18.05 → 19 + 0.05*(20-19) = 19.05
require.InDelta(t, 19.05, percentile(xs, 0.95), 1e-9)
// rank=0.95 → 1 + 0.95*(2-1) = 1.95
require.InDelta(t, 1.95, percentile(xs, 0.05), 1e-9)
// 边界 pp<=0 → 第一个元素p>=1 → 最后元素
require.Equal(t, 1.0, percentile(xs, 0.0))
require.Equal(t, 1.0, percentile(xs, -0.5))
require.Equal(t, 20.0, percentile(xs, 1.0))
require.Equal(t, 20.0, percentile(xs, 1.5))
}
// ---- medianFloat ------------------------------------------------------
func TestMedianFloat(t *testing.T) {
require.Equal(t, 0.0, medianFloat(nil))
require.Equal(t, 5.0, medianFloat([]float64{5}))
require.InDelta(t, 7.5, medianFloat([]float64{5, 10}), 1e-9)
require.InDelta(t, 5.0, medianFloat([]float64{1, 5, 9}), 1e-9)
require.InDelta(t, 4.0, medianFloat([]float64{9, 1, 5, 3}), 1e-9) // unsorted input
}
// ---- pivotHighs / pivotLows ------------------------------------------
func TestPivot_EmptyAndShortInputs(t *testing.T) {
require.Empty(t, pivotHighs(nil, 5, 5))
require.Empty(t, pivotLows(nil, 5, 5))
require.Empty(t, pivotHighs(ascending(5, 100, 1), 5, 5), "N < 2L+1")
require.Empty(t, pivotLows(ascending(10, 100, 1), 5, 5), "N < 2L+1")
}
func TestPivot_MonotonicNoPivots(t *testing.T) {
asc := ascending(30, 100, 10)
require.Empty(t, pivotHighs(asc, 5, 5), "ascending has no swing high in interior")
require.Empty(t, pivotLows(asc, 5, 5), "ascending has no swing low in interior")
desc := descending(30, 100, 1)
require.Empty(t, pivotHighs(desc, 5, 5))
require.Empty(t, pivotLows(desc, 5, 5))
}
func TestPivot_VShape(t *testing.T) {
// 构造 V 形:前 10 根 high 递减、后 10 根递增;中间最低点应为唯一 pivotLow
klines := make([]entity.Kline, 21)
for i := 0; i < 21; i++ {
// price 形态100 -> 90 (index 10) -> 100
var p float64
if i <= 10 {
p = 100 - float64(i)*1.0
} else {
p = 90 + float64(i-10)*1.0
}
hi := p + 0.5
lo := p
mid := p + 0.25
klines[i] = mkKline(int64(i),
strconv.FormatFloat(p, 'f', -1, 64),
strconv.FormatFloat(hi, 'f', -1, 64),
strconv.FormatFloat(lo, 'f', -1, 64),
strconv.FormatFloat(mid, 'f', -1, 64),
)
}
highs := pivotHighs(klines, 5, 5)
lows := pivotLows(klines, 5, 5)
require.Empty(t, highs, "V-shape has no pivot high")
require.Len(t, lows, 1, "V-shape has exactly one pivot low")
require.Equal(t, 10, lows[0].Index)
require.InDelta(t, 90.0, lows[0].Price, 1e-9)
}
func TestPivot_DoubleTop(t *testing.T) {
// 两个明确的 pivot highindex 7 和 index 15
// 用价格谷-峰-谷-峰-谷 结构
klines := make([]entity.Kline, 23)
// 默认平的低位 100
for i := range klines {
klines[i] = mkKline(int64(i), "100", "100.5", "99.5", "100")
}
// peak1 at i=7, price 110
klines[7] = mkKline(7, "109", "110", "108", "109.5")
// peak2 at i=15, price 110.2 (0.18% 距离,<0.3% 阈值)
klines[15] = mkKline(15, "109", "110.2", "108", "109.5")
highs := pivotHighs(klines, 5, 5)
require.Len(t, highs, 2)
require.Equal(t, 7, highs[0].Index)
require.Equal(t, 15, highs[1].Index)
}
// ---- clusterLevels ----------------------------------------------------
func TestClusterLevels_Empty(t *testing.T) {
require.Empty(t, clusterLevels(nil, 0.003, sourceResistancePivot))
}
func TestClusterLevels_DoubleTopWithinThreshold(t *testing.T) {
// 两个点相差 0.18%< 0.3%)→ 合并为 1 个 levelStrength="2"
pts := []pivotPoint{
{Price: 110.0, Index: 7},
{Price: 110.2, Index: 15},
}
out := clusterLevels(pts, 0.003, sourceResistancePivot)
require.Len(t, out, 1)
require.Equal(t, "2", out[0].Strength)
require.Equal(t, sourceResistancePivot, out[0].Source)
// 均价 = 110.1
p, ok := parsePrice(out[0].Price)
require.True(t, ok)
require.InDelta(t, 110.1, p, 1e-6)
}
func TestClusterLevels_DoubleTopOutsideThreshold(t *testing.T) {
// 两个点相差 0.45%> 0.3%)→ 不合并
pts := []pivotPoint{
{Price: 110.0, Index: 7},
{Price: 110.5, Index: 15},
}
out := clusterLevels(pts, 0.003, sourceResistancePivot)
require.Len(t, out, 2)
require.Equal(t, "1", out[0].Strength)
require.Equal(t, "1", out[1].Strength)
}
func TestClusterLevels_TripleCluster(t *testing.T) {
pts := []pivotPoint{
{Price: 100.0},
{Price: 100.15},
{Price: 100.25},
}
// 100/100.25 相差 0.25%,都在阈值内
out := clusterLevels(pts, 0.003, sourceResistancePivot)
require.Len(t, out, 1)
require.Equal(t, "3", out[0].Strength)
}
// ---- rangeHighLow -----------------------------------------------------
func TestRangeHighLow_Empty(t *testing.T) {
hi, lo := rangeHighLow(nil)
require.Nil(t, hi)
require.Nil(t, lo)
}
func TestRangeHighLow_SingleCandle(t *testing.T) {
k := []entity.Kline{mkKline(0, "100", "105", "95", "102")}
hi, lo := rangeHighLow(k)
require.NotNil(t, hi)
require.NotNil(t, lo)
require.Equal(t, "105", *hi, "P95 of single value is the value")
require.Equal(t, "95", *lo)
}
func TestRangeHighLow_Twenty(t *testing.T) {
// highs 1..20、lows 1..20P95(high)=19.05、P5(low)=1.95
klines := make([]entity.Kline, 20)
for i := 0; i < 20; i++ {
v := float64(i + 1)
klines[i] = mkKline(int64(i),
strconv.FormatFloat(v, 'f', -1, 64),
strconv.FormatFloat(v, 'f', -1, 64),
strconv.FormatFloat(v, 'f', -1, 64),
strconv.FormatFloat(v, 'f', -1, 64),
)
}
hi, lo := rangeHighLow(klines)
require.NotNil(t, hi)
require.NotNil(t, lo)
hiF, _ := parsePrice(*hi)
loF, _ := parsePrice(*lo)
require.InDelta(t, 19.05, hiF, 1e-6)
require.InDelta(t, 1.95, loF, 1e-6)
}
func TestRangeHighLow_SkipsMalformed(t *testing.T) {
klines := []entity.Kline{
mkKline(0, "100", "abc", "99", "100"), // high malformed → skipped
mkKline(1, "100", "200", "xyz", "100"), // low malformed → skipped
mkKline(2, "100", "150", "50", "100"), // good
}
hi, lo := rangeHighLow(klines)
require.NotNil(t, hi)
require.NotNil(t, lo)
// 有效 highs = [200, 150],有效 lows = [99, 50]
hiF, _ := parsePrice(*hi)
loF, _ := parsePrice(*lo)
// P95 of {150,200}: sorted=[150,200], rank=0.95 → 150 + 0.95*50 = 197.5
// P5 of {50,99}: rank=0.05 → 50 + 0.05*49 = 52.45
require.InDelta(t, 197.5, hiF, 1e-6)
require.InDelta(t, 52.45, loF, 1e-6)
}
// ---- longShortCrossings -----------------------------------------------
func TestLongShortCrossings_Empty(t *testing.T) {
require.Nil(t, longShortCrossings(nil, nil, 20))
require.Nil(t, longShortCrossings([]entity.LongShortRatio{mkLSR(0, "1.5")}, nil, 20))
}
func TestLongShortCrossings_AllAbove(t *testing.T) {
ls := []entity.LongShortRatio{
mkLSR(0, "1.2"), mkLSR(1, "1.3"), mkLSR(2, "1.5"),
}
klines := []entity.Kline{
mkKline(0, "100", "101", "99", "100"),
mkKline(1, "100", "101", "99", "100"),
mkKline(2, "100", "101", "99", "100"),
}
require.Nil(t, longShortCrossings(ls, klines, 20))
}
func TestLongShortCrossings_AllBelow(t *testing.T) {
ls := []entity.LongShortRatio{
mkLSR(0, "0.5"), mkLSR(1, "0.7"), mkLSR(2, "0.9"),
}
klines := []entity.Kline{
mkKline(0, "100", "101", "99", "100"),
mkKline(1, "100", "101", "99", "100"),
mkKline(2, "100", "101", "99", "100"),
}
require.Nil(t, longShortCrossings(ls, klines, 20))
}
func TestLongShortCrossings_SingleCrossing(t *testing.T) {
// LSR0.9 → 1.1(穿越发生在 index=1
ls := []entity.LongShortRatio{
mkLSR(0, "0.9"),
mkLSR(1, "1.1"),
}
klines := []entity.Kline{
mkKline(0, "100", "101", "99", "100"),
mkKline(1, "120", "121", "119", "120"),
}
got := longShortCrossings(ls, klines, 20)
require.NotNil(t, got)
// 穿越价位估计:取 ls[1] 时刻 kline 的 (open+close)/2 = 120
v, ok := parsePrice(*got)
require.True(t, ok)
require.InDelta(t, 120.0, v, 1e-6)
}
func TestLongShortCrossings_MultipleCrossings(t *testing.T) {
// 来回穿越多次:取近 N 次的价位中位数
// 序列0.9, 1.1, 0.95, 1.05 → 三次穿越i=1, i=2, i=3
ls := []entity.LongShortRatio{
mkLSR(0, "0.9"),
mkLSR(1, "1.1"),
mkLSR(2, "0.95"),
mkLSR(3, "1.05"),
}
klines := []entity.Kline{
mkKline(0, "100", "101", "99", "100"),
mkKline(1, "110", "111", "109", "110"), // 穿越价 110
mkKline(2, "120", "121", "119", "120"), // 穿越价 120
mkKline(3, "130", "131", "129", "130"), // 穿越价 130
}
got := longShortCrossings(ls, klines, 20)
require.NotNil(t, got)
// 中位数 = 120
v, ok := parsePrice(*got)
require.True(t, ok)
require.InDelta(t, 120.0, v, 1e-6)
}
func TestLongShortCrossings_KeepNLimit(t *testing.T) {
// 24 个穿越价 1..24keepN=20 → 保留最近 20 个(价 5..24),中位 14.5
ls := []entity.LongShortRatio{}
klines := []entity.Kline{}
for i := 0; i < 25; i++ {
var r string
if i%2 == 0 {
r = "0.9"
} else {
r = "1.1"
}
ls = append(ls, mkLSR(int64(i), r))
v := float64(i + 1)
klines = append(klines, mkKline(int64(i),
strconv.FormatFloat(v, 'f', -1, 64),
strconv.FormatFloat(v, 'f', -1, 64),
strconv.FormatFloat(v, 'f', -1, 64),
strconv.FormatFloat(v, 'f', -1, 64),
))
}
got := longShortCrossings(ls, klines, 20)
require.NotNil(t, got)
// 24 个穿越i=1..24),保留最近 20 个对应 klines[5..24] → price 6..25
// 中位 = (15+16)/2 = 15.5
v, ok := parsePrice(*got)
require.True(t, ok)
require.InDelta(t, 15.5, v, 1e-6)
}
func TestLongShortCrossings_KeepNZero(t *testing.T) {
ls := []entity.LongShortRatio{mkLSR(0, "0.9"), mkLSR(1, "1.1")}
klines := []entity.Kline{mkKline(0, "100", "100", "100", "100"), mkKline(1, "110", "110", "110", "110")}
require.Nil(t, longShortCrossings(ls, klines, 0))
require.Nil(t, longShortCrossings(ls, klines, -1))
}
func TestLongShortCrossings_MalformedRatio(t *testing.T) {
// 中间一条 LSR ratio 是 "abc",相邻两次穿越都被跳过
ls := []entity.LongShortRatio{
mkLSR(0, "0.9"),
mkLSR(1, "abc"),
mkLSR(2, "1.1"),
}
klines := []entity.Kline{
mkKline(0, "100", "101", "99", "100"),
mkKline(1, "110", "111", "109", "110"),
mkKline(2, "120", "121", "119", "120"),
}
require.Nil(t, longShortCrossings(ls, klines, 20))
}
func TestLongShortCrossings_KlineParseFailure(t *testing.T) {
// 穿越发生,但对应 kline 的 open/close 是 malformed → 该穿越被跳过
ls := []entity.LongShortRatio{
mkLSR(0, "0.9"),
mkLSR(1, "1.1"),
}
klines := []entity.Kline{
mkKline(0, "100", "101", "99", "100"),
mkKline(1, "abc", "101", "99", "xyz"),
}
require.Nil(t, longShortCrossings(ls, klines, 20))
}
func TestLongShortCrossings_TimestampBeyondKlines(t *testing.T) {
// LSR 时间戳全部小于第一根 kline → nearestKlineIndex 返回 0
const hourMs = int64(3600 * 1000)
ls := []entity.LongShortRatio{
{Symbol: "X", Period: "1h", Timestamp: -1000, LongShortRatio: "0.9"},
{Symbol: "X", Period: "1h", Timestamp: -500, LongShortRatio: "1.1"},
}
klines := []entity.Kline{
{Symbol: "X", Interval: "1h", OpenTime: 0, CloseTime: hourMs - 1, Open: "100", High: "101", Low: "99", Close: "110", IsClosed: true},
{Symbol: "X", Interval: "1h", OpenTime: hourMs, CloseTime: 2*hourMs - 1, Open: "100", High: "101", Low: "99", Close: "110", IsClosed: true},
}
got := longShortCrossings(ls, klines, 20)
require.NotNil(t, got)
v, _ := parsePrice(*got)
require.InDelta(t, 105.0, v, 1e-6, "kline[0] (open+close)/2 = 105")
}
// nearestKlineIndex 直接覆盖空、ts 在前、ts 在后、ts 居中
func TestNearestKlineIndex(t *testing.T) {
const hourMs = int64(3600 * 1000)
require.Equal(t, -1, nearestKlineIndex(nil, 0))
klines := []entity.Kline{
{OpenTime: 0},
{OpenTime: hourMs},
{OpenTime: 2 * hourMs},
}
require.Equal(t, 0, nearestKlineIndex(klines, -1000), "before all → first")
require.Equal(t, 2, nearestKlineIndex(klines, 10*hourMs), "after all → last")
require.Equal(t, 0, nearestKlineIndex(klines, hourMs/2-1), "lo 更近")
require.Equal(t, 1, nearestKlineIndex(klines, hourMs/2+1), "hi 更近")
require.Equal(t, 0, nearestKlineIndex(klines, hourMs/2), "等距时偏向 lo<=")
require.Equal(t, 1, nearestKlineIndex(klines, hourMs), "ts 等于某点 → 命中")
}
// ---- Compute端到端编排 --------------------------------------------
func TestCompute_EmptyInputs(t *testing.T) {
u := NewIndicatorUsecase()
out := u.Compute(nil, "1h", nil)
require.NotNil(t, out.Support)
require.NotNil(t, out.Resistance)
require.Empty(t, out.Support)
require.Empty(t, out.Resistance)
require.Nil(t, out.RangeHigh)
require.Nil(t, out.RangeLow)
require.Nil(t, out.LongShortLine)
require.NotNil(t, out.Intervals)
require.Empty(t, out.Intervals)
}
func TestCompute_SmokeWithVShape(t *testing.T) {
u := NewIndicatorUsecase()
// V-shape 21 根,应有 1 个 support、0 个 resistance、有 range
klines := make([]entity.Kline, 21)
for i := 0; i < 21; i++ {
var p float64
if i <= 10 {
p = 100 - float64(i)*1.0
} else {
p = 90 + float64(i-10)*1.0
}
klines[i] = mkKline(int64(i),
strconv.FormatFloat(p, 'f', -1, 64),
strconv.FormatFloat(p+0.5, 'f', -1, 64),
strconv.FormatFloat(p, 'f', -1, 64),
strconv.FormatFloat(p+0.25, 'f', -1, 64),
)
}
out := u.Compute(map[string][]entity.Kline{"1h": klines}, "1h", nil)
require.Len(t, out.Support, 1)
require.Empty(t, out.Resistance)
require.NotNil(t, out.RangeHigh)
require.NotNil(t, out.RangeLow)
require.Nil(t, out.LongShortLine, "no LSR data → nil")
require.Contains(t, out.Intervals, "1h")
}
// ---- Bollinger / Vegas -------------------------------------------------
// flatCloses 生成 n 根价位恒为 v 的 closes用于 Squeeze=tight 验证)。
func flatCloses(n int, v float64) []float64 {
out := make([]float64, n)
for i := range out {
out[i] = v
}
return out
}
// ascCloses 生成等差递增 closesbase, base+step, ...
func ascCloses(n int, base, step float64) []float64 {
out := make([]float64, n)
for i := range out {
out[i] = base + float64(i)*step
}
return out
}
func TestBollinger_FlatSeries_Tight(t *testing.T) {
bb := bollinger(flatCloses(20, 100))
require.NotNil(t, bb)
require.Equal(t, "100", bb.Mid)
require.Equal(t, "100", bb.Upper)
require.Equal(t, "100", bb.Lower)
require.Equal(t, "0", bb.BandwidthPct)
require.Equal(t, "tight", bb.Squeeze)
}
func TestBollinger_AscendingSeries_Expanded(t *testing.T) {
// 0..29 等差,末 20 根 mid=19.5、σ ≈ 5.77、bw ≈ 118%
bb := bollinger(ascCloses(30, 0, 1))
require.NotNil(t, bb)
require.Equal(t, "expanded", bb.Squeeze)
mid, _ := strconv.ParseFloat(bb.Mid, 64)
require.InDelta(t, 19.5, mid, 1e-9)
}
func TestBollinger_InsufficientData(t *testing.T) {
require.Nil(t, bollinger(flatCloses(19, 100)))
require.Nil(t, bollinger(nil))
}
func TestVegas_BullishStack(t *testing.T) {
// 上升序列 → EMA12 > EMA144 > EMA169趋势 bull
v := vegas(ascCloses(200, 100, 1))
require.NotNil(t, v)
e12, _ := strconv.ParseFloat(v.EMA12, 64)
e144, _ := strconv.ParseFloat(v.EMA144, 64)
e169, _ := strconv.ParseFloat(v.EMA169, 64)
require.Greater(t, e12, e144)
require.Greater(t, e144, e169)
require.Equal(t, "bull", v.Trend)
}
func TestVegas_BearishStack(t *testing.T) {
// 下降序列
v := vegas(ascCloses(200, 300, -1))
require.NotNil(t, v)
require.Equal(t, "bear", v.Trend)
}
func TestVegas_FlatRange(t *testing.T) {
// 平盘:三条线相等
v := vegas(flatCloses(200, 100))
require.NotNil(t, v)
require.Equal(t, "range", v.Trend)
}
func TestVegas_InsufficientData(t *testing.T) {
require.Nil(t, vegas(ascCloses(168, 100, 1)))
}
func TestCompute_IntervalsPopulated(t *testing.T) {
u := NewIndicatorUsecase()
// 给 1h25 根) 与 4h200 根) 喂上升序列
mk := func(n int) []entity.Kline {
out := make([]entity.Kline, n)
for i := 0; i < n; i++ {
p := 100 + float64(i)
out[i] = mkKline(int64(i),
strconv.FormatFloat(p, 'f', -1, 64),
strconv.FormatFloat(p+0.5, 'f', -1, 64),
strconv.FormatFloat(p, 'f', -1, 64),
strconv.FormatFloat(p+0.25, 'f', -1, 64),
)
}
return out
}
out := u.Compute(map[string][]entity.Kline{
"1h": mk(25),
"4h": mk(200),
}, "1h", nil)
require.Contains(t, out.Intervals, "1h")
require.Contains(t, out.Intervals, "4h")
require.NotNil(t, out.Intervals["1h"].Bollinger, "25 closes 够算 BB")
require.Nil(t, out.Intervals["1h"].Vegas, "25 closes 不够算 EMA169")
require.NotNil(t, out.Intervals["4h"].Bollinger)
require.NotNil(t, out.Intervals["4h"].Vegas)
}