删除 HyperLiquid + 全部交易功能,构建自适应 surge 检测器。 - 新增 surge_detector.go: 每币独立滚动窗口基线,检测三所价差异常飙升 - 新增 SpreadCard/SurgeCard 前端组件 - 保留 momentum/trend/cumulative/trend_filter 扫描功能 - 更新文档和配置以反映新系统 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
215 lines
5.7 KiB
Go
215 lines
5.7 KiB
Go
package db
|
|
|
|
import (
|
|
"database/sql"
|
|
"log"
|
|
"os"
|
|
"path/filepath"
|
|
"time"
|
|
|
|
_ "modernc.org/sqlite"
|
|
)
|
|
|
|
// DB wraps the sql.DB connection with our schema.
|
|
type DB struct {
|
|
*sql.DB
|
|
Path string
|
|
}
|
|
|
|
// Open opens or creates the SQLite database at the given path.
|
|
func Open(path string) (*DB, error) {
|
|
if path == "" {
|
|
home, _ := os.UserHomeDir()
|
|
path = filepath.Join(home, "Project", "exchange-monitor-go", "data", "trades.db")
|
|
}
|
|
|
|
// Ensure directory exists
|
|
os.MkdirAll(filepath.Dir(path), 0755)
|
|
|
|
db, err := sql.Open("sqlite", path)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
db.SetMaxOpenConns(1) // SQLite single-writer
|
|
db.SetMaxIdleConns(1)
|
|
|
|
d := &DB{DB: db, Path: path}
|
|
if err := d.migrate(); err != nil {
|
|
return nil, err
|
|
}
|
|
return d, nil
|
|
}
|
|
|
|
func (d *DB) migrate() error {
|
|
schema := `
|
|
CREATE TABLE IF NOT EXISTS trades (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
coin TEXT NOT NULL,
|
|
direction TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'open',
|
|
entry_spread REAL,
|
|
exit_spread REAL,
|
|
long_exchange TEXT,
|
|
short_exchange TEXT,
|
|
long_entry REAL,
|
|
long_exit REAL,
|
|
short_entry REAL,
|
|
short_exit REAL,
|
|
long_pnl REAL,
|
|
short_pnl REAL,
|
|
fee_entry REAL,
|
|
fee_exit REAL,
|
|
net_pnl REAL,
|
|
amount_usd REAL,
|
|
scale_count INTEGER DEFAULT 0,
|
|
exit_reason TEXT,
|
|
convergence TEXT,
|
|
opened_at DATETIME NOT NULL,
|
|
closed_at DATETIME
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS orders (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
trade_id INTEGER NOT NULL REFERENCES trades(id),
|
|
leg TEXT NOT NULL,
|
|
type TEXT NOT NULL,
|
|
exchange TEXT NOT NULL,
|
|
side TEXT NOT NULL,
|
|
price REAL,
|
|
size REAL,
|
|
fee REAL,
|
|
order_id TEXT,
|
|
status TEXT,
|
|
created_at DATETIME NOT NULL
|
|
);
|
|
|
|
CREATE TABLE IF NOT EXISTS config_log (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
key TEXT NOT NULL,
|
|
value TEXT NOT NULL,
|
|
changed_at DATETIME NOT NULL
|
|
);
|
|
|
|
CREATE INDEX IF NOT EXISTS idx_trades_coin ON trades(coin);
|
|
CREATE INDEX IF NOT EXISTS idx_trades_status ON trades(status);
|
|
CREATE INDEX IF NOT EXISTS idx_trades_opened ON trades(opened_at);
|
|
CREATE INDEX IF NOT EXISTS idx_orders_trade_id ON orders(trade_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS system_orders (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
trade_id INTEGER NOT NULL REFERENCES trades(id),
|
|
type TEXT NOT NULL,
|
|
status TEXT NOT NULL DEFAULT 'filled',
|
|
spread REAL,
|
|
long_price REAL,
|
|
short_price REAL,
|
|
long_order_id INTEGER REFERENCES orders(id),
|
|
short_order_id INTEGER REFERENCES orders(id),
|
|
created_at DATETIME NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_system_orders_trade ON system_orders(trade_id);
|
|
|
|
CREATE TABLE IF NOT EXISTS trend_events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
coin TEXT NOT NULL,
|
|
prev_state TEXT NOT NULL,
|
|
new_state TEXT NOT NULL,
|
|
direction TEXT NOT NULL,
|
|
z_score REAL,
|
|
volatility REAL,
|
|
bg_change REAL,
|
|
hl_change REAL,
|
|
bn_change REAL,
|
|
okx_change REAL,
|
|
ex_agree INTEGER,
|
|
ex_total INTEGER,
|
|
created_at DATETIME NOT NULL
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_trend_events_coin ON trend_events(coin);
|
|
CREATE INDEX IF NOT EXISTS idx_trend_events_created ON trend_events(created_at);
|
|
|
|
CREATE TABLE IF NOT EXISTS cm_events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
coin TEXT NOT NULL,
|
|
prev_state TEXT NOT NULL,
|
|
new_state TEXT NOT NULL,
|
|
direction TEXT NOT NULL,
|
|
score REAL,
|
|
avg_change REAL,
|
|
ex_agree INTEGER,
|
|
ex_total INTEGER,
|
|
bg_1m REAL,
|
|
hl_1m REAL,
|
|
bn_1m REAL,
|
|
okx_1m REAL,
|
|
bg_5m REAL,
|
|
hl_5m REAL,
|
|
bn_5m REAL,
|
|
okx_5m REAL,
|
|
created_at DATETIME NOT NULL
|
|
);
|
|
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);
|
|
|
|
CREATE TABLE IF NOT EXISTS surge_events (
|
|
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
coin TEXT NOT NULL,
|
|
timestamp DATETIME NOT NULL,
|
|
bn_price REAL,
|
|
okx_price REAL,
|
|
bg_price REAL,
|
|
spread_pct REAL NOT NULL,
|
|
baseline_pct REAL,
|
|
threshold_pct REAL,
|
|
ratio REAL,
|
|
direction TEXT NOT NULL,
|
|
leading_exchange TEXT NOT NULL,
|
|
mid_price REAL,
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
);
|
|
CREATE INDEX IF NOT EXISTS idx_surge_events_coin ON surge_events(coin);
|
|
CREATE INDEX IF NOT EXISTS idx_surge_events_created ON surge_events(created_at);
|
|
`
|
|
_, err := d.Exec(schema)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Migration v2: add per-exchange fee/pnl columns (idempotent)
|
|
for _, col := range []string{"pnl_long_usd", "pnl_short_usd", "fee_long_usd", "fee_short_usd"} {
|
|
var found int
|
|
d.QueryRow("SELECT COUNT(*) FROM pragma_table_info('trades') WHERE name=?", col).Scan(&found)
|
|
if found == 0 {
|
|
if _, err := d.Exec("ALTER TABLE trades ADD COLUMN " + col + " REAL"); err != nil {
|
|
log.Printf("[DB] Migration: add column %s: %v", col, err)
|
|
}
|
|
}
|
|
}
|
|
|
|
log.Printf("[DB] SQLite ready: %s", d.Path)
|
|
return nil
|
|
}
|
|
|
|
// Now returns the current time in UTC.
|
|
func Now() time.Time {
|
|
return time.Now().UTC()
|
|
}
|