Files
exchange-monitor-go/db/db.go
T
jackyu66gitandClaude Opus 4.6 d38782490c feat: 重构为三所价差异动监控系统
删除 HyperLiquid + 全部交易功能,构建自适应 surge 检测器。
- 新增 surge_detector.go: 每币独立滚动窗口基线,检测三所价差异常飙升
- 新增 SpreadCard/SurgeCard 前端组件
- 保留 momentum/trend/cumulative/trend_filter 扫描功能
- 更新文档和配置以反映新系统

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-05-08 02:01:18 +08:00

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()
}