feat: 趋势过滤信号记录系统 + 实时涨跌方向判断
- 新增 TrendFilter 信号记录(enter/exit),按完整信号和高分信号两档分类 - 信号持久化到 data/trend_signals_cache.json,开机自动恢复 - 新增 /api/trend-signals API + SSE trend_signal 实时广播 - 前端新增完整信号卡片和高分信号卡片,移除旧趋势检测卡片 - 评分加入 1h 涨跌方向和实时 drift 惩罚,下跌币不触发信号 - OKX 交易所支持(累积变动、动量、趋势检测) - 修复 trend_filter.go 编译错误 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 4.6
parent
73dac50a36
commit
559d7bb870
@@ -150,6 +150,24 @@ func (d *DB) migrate() error {
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_cm_events_coin ON cm_events(coin);
|
||||
CREATE INDEX IF NOT EXISTS idx_cm_events_created ON cm_events(created_at);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS trend_signals (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
coin TEXT NOT NULL,
|
||||
type TEXT NOT NULL,
|
||||
signal_score REAL,
|
||||
price REAL,
|
||||
ema_52 REAL,
|
||||
ema_slope REAL,
|
||||
volume_ratio REAL,
|
||||
range_24h REAL,
|
||||
vol_baseline REAL,
|
||||
price_above_ema INTEGER,
|
||||
state TEXT,
|
||||
created_at DATETIME NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_trend_signals_coin ON trend_signals(coin);
|
||||
CREATE INDEX IF NOT EXISTS idx_trend_signals_created ON trend_signals(created_at);
|
||||
`
|
||||
_, err := d.Exec(schema)
|
||||
if err != nil {
|
||||
|
||||
@@ -0,0 +1,64 @@
|
||||
package db
|
||||
|
||||
import "time"
|
||||
|
||||
// TrendSignalRecord mirrors the trend_signals table row.
|
||||
type TrendSignalRecord struct {
|
||||
ID int64
|
||||
Coin string
|
||||
Type string // "enter" or "exit"
|
||||
SignalScore *float64
|
||||
Price *float64
|
||||
EMA52 *float64
|
||||
EMASlope *float64
|
||||
VolumeRatio *float64
|
||||
Range24h *float64
|
||||
VolBaseline *float64
|
||||
PriceAboveEMA bool
|
||||
State *string
|
||||
CreatedAt time.Time
|
||||
}
|
||||
|
||||
// SaveTrendSignal inserts a new trend signal record.
|
||||
func (d *DB) SaveTrendSignal(s *TrendSignalRecord) (int64, error) {
|
||||
pa := 0
|
||||
if s.PriceAboveEMA {
|
||||
pa = 1
|
||||
}
|
||||
res, err := d.Exec(`INSERT INTO trend_signals
|
||||
(coin, type, signal_score, price, ema_52, ema_slope, volume_ratio,
|
||||
range_24h, vol_baseline, price_above_ema, state, created_at)
|
||||
VALUES (?,?,?,?,?,?,?,?,?,?,?,?)`,
|
||||
s.Coin, s.Type, s.SignalScore, s.Price, s.EMA52, s.EMASlope, s.VolumeRatio,
|
||||
s.Range24h, s.VolBaseline, pa, s.State, s.CreatedAt,
|
||||
)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return res.LastInsertId()
|
||||
}
|
||||
|
||||
// GetTrendSignals returns the most recent N trend signal records.
|
||||
func (d *DB) GetTrendSignals(limit int) ([]TrendSignalRecord, error) {
|
||||
rows, err := d.Query(`SELECT id, coin, type, signal_score, price, ema_52, ema_slope,
|
||||
volume_ratio, range_24h, vol_baseline, price_above_ema, state, created_at
|
||||
FROM trend_signals ORDER BY id DESC LIMIT ?`, limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
var signals []TrendSignalRecord
|
||||
for rows.Next() {
|
||||
var s TrendSignalRecord
|
||||
var pa int
|
||||
if err := rows.Scan(&s.ID, &s.Coin, &s.Type, &s.SignalScore, &s.Price,
|
||||
&s.EMA52, &s.EMASlope, &s.VolumeRatio, &s.Range24h, &s.VolBaseline,
|
||||
&pa, &s.State, &s.CreatedAt); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
s.PriceAboveEMA = pa == 1
|
||||
signals = append(signals, s)
|
||||
}
|
||||
return signals, rows.Err()
|
||||
}
|
||||
Reference in New Issue
Block a user