Files
exchange-monitor-go/trend_test.go
T
jackyu66gitandClaude Opus 4.6 b7767c95ae feat: 添加OKX行情接入+趋势检测+累积变动系统+界面重构
- 新增OKX WebSocket行情连接器,扩展4交易所价格监控
- 新增z-score趋势检测引擎(TrendDetector),识别价格异动/趋势启动
- 新增累积变动跟踪(CumulativeTracker),基于1min/5min多交易所共识
- 趋势事件和累积变动事件持久化到SQLite
- 新增Binance/OKX动量检测字段,扩展前端动量卡片至15列
- 迁移至macOS(darwin-arm64),更新前端依赖
- Dashboard网格重构:非交易卡片置顶,交易卡片置底
- TrackedCoin添加OK字段,添加ExBinance/ExOKX常量
- 前端新增趋势检测卡片、趋势历史卡片、累积变动卡片

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-06 13:26:05 +08:00

120 lines
2.9 KiB
Go

package main
import (
"math"
"testing"
)
func TestMeanStdDev(t *testing.T) {
changes := []exchangeChange{
{name: "A", change: 0.1},
{name: "B", change: 0.2},
{name: "C", change: 0.3},
{name: "D", change: 0.4},
}
mean, std := meanStdDev(changes)
if math.Abs(mean-0.25) > 0.001 {
t.Errorf("mean = %.4f, want 0.2500", mean)
}
if math.Abs(std-0.1118) > 0.01 {
t.Errorf("std = %.4f, want ~0.1118", std)
}
}
func TestMeanStdDevSingleValue(t *testing.T) {
changes := []exchangeChange{
{name: "A", change: 0.1},
}
mean, std := meanStdDev(changes)
if mean != 0.1 {
t.Errorf("mean = %.4f, want 0.1000", mean)
}
if std != 0 {
t.Errorf("std = %.4f, want 0.0000", std)
}
}
func TestMeanStdDevZeroValues(t *testing.T) {
changes := []exchangeChange{
{name: "A", change: 0},
{name: "B", change: 0},
}
mean, std := meanStdDev(changes)
if mean != 0 {
t.Errorf("mean = %.4f, want 0.0000", mean)
}
if std != 0 {
t.Errorf("std = %.4f, want 0.0000", std)
}
}
func TestMeanStdDevEmpty(t *testing.T) {
mean, std := meanStdDev(nil)
if mean != 0 || std != 0 {
t.Errorf("expected 0,0 for empty input, got %.4f, %.4f", mean, std)
}
}
func TestZScoreCalculation(t *testing.T) {
// One exchange strongly diverging from the others
// Three exchanges nearly flat, one moves 1.5%
changes := []exchangeChange{
{name: "BG", change: 0.01},
{name: "HL", change: 0.01},
{name: "BN", change: 0.02},
{name: "OK", change: 1.50}, // anomalous!
}
_, std := meanStdDev(changes)
maxAbs := 1.50
zScore := maxAbs / std
if zScore < 2.0 {
t.Errorf("z-score = %.2f, expected > 2.0 for divergent exchange", zScore)
}
t.Logf("Divergent exchange (1 of 4): z-score = %.2f (std = %.4f)", zScore, std)
}
func TestCoordinatedMovement(t *testing.T) {
// All exchanges moving together = also a trend (consensus, not anomaly)
changes := []exchangeChange{
{name: "BG", change: 0.05},
{name: "HL", change: 0.06},
{name: "BN", change: 0.04},
{name: "OK", change: 0.07},
}
_, std := meanStdDev(changes)
maxAbs := 0.07
zScore := maxAbs / std
// Tight cluster → std is small, z-score is high → valid trend signal
t.Logf("Co-movement: z-score = %.2f (std = %.4f) — high z-score + 4/4 agreement = trend", zScore, std)
}
func TestTrendDetectorConfigure(t *testing.T) {
td := NewTrendDetector(nil)
if td.anomalyMul != 3.0 {
t.Errorf("default anomalyMul = %.1f, want 3.0", td.anomalyMul)
}
if td.confirmTicks != 3 {
t.Errorf("default confirmTicks = %d, want 3", td.confirmTicks)
}
td.Configure(300, 2.5, 2, 30000)
if td.anomalyMul != 2.5 {
t.Errorf("anomalyMul after configure = %.1f, want 2.5", td.anomalyMul)
}
if td.confirmTicks != 2 {
t.Errorf("confirmTicks after configure = %d, want 2", td.confirmTicks)
}
}
func TestTrendDetectorEmptyConfigure(t *testing.T) {
td := NewTrendDetector(nil)
// Passing zeros should keep defaults
td.Configure(0, 0, 0, 0)
if td.anomalyMul != 3.0 {
t.Errorf("anomalyMul = %.1f, expected default 3.0", td.anomalyMul)
}
}