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
This commit is contained in:
@@ -0,0 +1,109 @@
|
||||
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()
|
||||
}
|
||||
Reference in New Issue
Block a user