Files
exchange-monitor-go/db/db.go
T
jackyu66git b09314f317 Phase 1: SQLite persistence layer
- Add modernc.org/sqlite (pure Go, no CGO)
- db/ package: trades, orders, config_log tables + CRUD
- Trade persistence: every closed trade saved to SQLite
- Restart recovery: open positions restored from DB
- Automatic migration on startup
2026-05-03 17:28:08 +08:00

110 lines
2.4 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);
`
_, err := d.Exec(schema)
if err != nil {
return 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()
}