Files
exchange-monitor-go/db/db.go
T
jackyu66gitandClaude Opus 4.6 b7767c95ae 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>
2026-05-06 13:26:05 +08:00

178 lines
4.5 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);
`
_, 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()
}