Files
exchange-monitor-go/db/db.go
T

136 lines
3.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);
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);
`
_, 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()
}