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>
This commit is contained in:
jackyu66git
2026-05-06 13:26:05 +08:00
co-authored by Claude Opus 4.6
parent 047571921e
commit b7767c95ae
24 changed files with 2705 additions and 273 deletions
+102 -2
View File
@@ -189,6 +189,7 @@ type Dashboard struct {
trader *Trader
db *db.DB
addr string
cfg *Config
// cached arb scan results
mu sync.RWMutex
@@ -198,10 +199,19 @@ type Dashboard struct {
// P3-5: connection status — exchange -> last update time
connMu sync.RWMutex
connMap map[string]time.Time // exchange name -> last price timestamp
// Momentum tracker
momentumTracker *MomentumTracker
// Trend detector
trendDetector *TrendDetector
// Cumulative tracker (1m/5m multi-exchange consensus)
cumulativeTracker *CumulativeTracker
}
func NewDashboard(store *PriceStore, trader *Trader, database *db.DB, addr string) *Dashboard {
return &Dashboard{
func NewDashboard(store *PriceStore, trader *Trader, database *db.DB, addr string, cfg *Config, momentumTracker *MomentumTracker, trendDetector *TrendDetector, cumulativeTracker *CumulativeTracker) *Dashboard {
d := &Dashboard{
hub: NewSSEHub(),
history: newPriceHistory(),
spreads: newSpreadHistory(),
@@ -209,8 +219,33 @@ func NewDashboard(store *PriceStore, trader *Trader, database *db.DB, addr strin
trader: trader,
db: database,
addr: addr,
cfg: cfg,
connMap: make(map[string]time.Time),
momentumTracker: momentumTracker,
trendDetector: trendDetector,
cumulativeTracker: cumulativeTracker,
}
// Wire trend event persistence to SQLite
if trendDetector != nil && database != nil {
trendDetector.OnEvent = func(ev TrendEvent) {
database.InsertTrendEvent(ev.Coin, ev.PrevState, ev.NewState, ev.Direction,
ev.ZScore, ev.Volatility, ev.BGChange, ev.HLChange, ev.BNChange, ev.OKXChange,
ev.ExAgree, ev.ExTotal)
}
}
// Wire cumulative event persistence to SQLite
if cumulativeTracker != nil && database != nil {
cumulativeTracker.OnEvent = func(ev CmEvent) {
database.InsertCmEvent(ev.Coin, ev.PrevState, ev.NewState, ev.Direction,
ev.Score, ev.AvgChange, ev.ExAgree, ev.ExTotal,
ev.BGChange1m, ev.HLChange1m, ev.BNChange1m, ev.OKXChange1m,
ev.BGChange5m, ev.HLChange5m, ev.BNChange5m, ev.OKXChange5m)
}
}
return d
}
func (d *Dashboard) Run() {
@@ -239,6 +274,8 @@ func (d *Dashboard) Run() {
mux.HandleFunc("GET /api/trades", d.handleTrades)
mux.HandleFunc("GET /api/trade/", d.handleTradeDetail)
mux.HandleFunc("GET /api/connections", d.handleConnStatus) // P3-5
mux.HandleFunc("GET /api/trend-history", d.handleTrendHistory)
mux.HandleFunc("GET /api/cm-history", d.handleCmHistory)
mux.HandleFunc("GET /events", d.handleSSE)
mux.HandleFunc("POST /api/stop", d.handleStop)
mux.HandleFunc("POST /api/start", d.handleStart)
@@ -534,6 +571,32 @@ func (d *Dashboard) broadcastLoop() {
stats["blacklist"] = blList
d.hub.Broadcast("stats", stats)
// 5. Momentum data (if enabled and tracker is available)
if d.momentumTracker != nil && d.cfg.MomentumEnabled {
momentumData := d.momentumTracker.Snapshot(d.cfg.MomentumThresholdPct)
if len(momentumData) > 0 {
d.hub.Broadcast("momentum", momentumData)
}
}
// 6. Trend detection (if enabled)
if d.trendDetector != nil && d.cfg.TrendEnabled {
d.trendDetector.Tick()
trendData := d.trendDetector.Snapshot()
if len(trendData) > 0 {
d.hub.Broadcast("trend", trendData)
}
}
// 7. Cumulative change tracking (always on if tracker exists)
if d.cumulativeTracker != nil {
d.cumulativeTracker.Tick()
cmData := d.cumulativeTracker.GetTopCoins(30)
if len(cmData) > 0 {
d.hub.Broadcast("cumulative", cmData)
}
}
}
}
@@ -664,6 +727,43 @@ func (d *Dashboard) handleConnStatus(w http.ResponseWriter, r *http.Request) {
writeJSON(w, conns)
}
func (d *Dashboard) handleTrendHistory(w http.ResponseWriter, r *http.Request) {
var events interface{}
if d.db != nil {
records, err := d.db.GetTrendEvents(200)
if err == nil {
events = records
}
}
if events == nil {
// Fallback to in-memory ring buffer
if d.trendDetector != nil {
events = d.trendDetector.GetEvents(200)
} else {
events = []interface{}{}
}
}
writeJSON(w, map[string]interface{}{"events": events})
}
func (d *Dashboard) handleCmHistory(w http.ResponseWriter, r *http.Request) {
var events interface{}
if d.db != nil {
records, err := d.db.GetCmEvents(200)
if err == nil {
events = records
}
}
if events == nil {
if d.cumulativeTracker != nil {
events = d.cumulativeTracker.GetEvents(200)
} else {
events = []interface{}{}
}
}
writeJSON(w, map[string]interface{}{"events": events})
}
func (d *Dashboard) handleTrades(w http.ResponseWriter, r *http.Request) {
if d.db == nil {
writeJSON(w, map[string]interface{}{"trades": []interface{}{}, "total": 0})