- 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
110 lines
2.4 KiB
Go
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()
|
|
}
|