- 新增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>
120 lines
2.9 KiB
Go
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)
|
|
}
|
|
}
|