Files
nofx/store/store.go
T
tinkle-community 8a5744e0a0 fix: use actual fill price from exchange API for position records
- Remove trader_orders table and OrderSyncManager (never worked correctly)
- Poll GetOrderStatus to get actual avgPrice, executedQty, and commission
- Get entry price from exchange GetPositions API when closing positions
- Pass fee to trader_positions table on close
- Move TraderStats type to position.go
2025-12-08 12:15:41 +08:00

298 lines
7.1 KiB
Go

// Package store provides unified database storage layer
// All database operations should go through this package
package store
import (
"database/sql"
"fmt"
"nofx/logger"
"sync"
_ "modernc.org/sqlite"
)
// Store unified data storage interface
type Store struct {
db *sql.DB
// Sub-stores (lazy initialization)
user *UserStore
aiModel *AIModelStore
exchange *ExchangeStore
trader *TraderStore
decision *DecisionStore
backtest *BacktestStore
position *PositionStore
strategy *StrategyStore
equity *EquityStore
// Encryption functions
encryptFunc func(string) string
decryptFunc func(string) string
mu sync.RWMutex
}
// New creates new Store instance
func New(dbPath string) (*Store, error) {
db, err := sql.Open("sqlite", dbPath)
if err != nil {
return nil, fmt.Errorf("failed to open database: %w", err)
}
// SQLite configuration
db.SetMaxOpenConns(1)
db.SetMaxIdleConns(1)
// Enable foreign key constraints
if _, err := db.Exec(`PRAGMA foreign_keys = ON`); err != nil {
db.Close()
return nil, fmt.Errorf("failed to enable foreign keys: %w", err)
}
// Use DELETE mode (traditional mode) to ensure Docker bind mount compatibility
// Note: WAL mode causes data sync issues on macOS Docker
if _, err := db.Exec("PRAGMA journal_mode=DELETE"); err != nil {
db.Close()
return nil, fmt.Errorf("failed to set journal_mode: %w", err)
}
// Set synchronous=FULL
if _, err := db.Exec("PRAGMA synchronous=FULL"); err != nil {
db.Close()
return nil, fmt.Errorf("failed to set synchronous: %w", err)
}
// Set busy_timeout
if _, err := db.Exec("PRAGMA busy_timeout = 5000"); err != nil {
db.Close()
return nil, fmt.Errorf("failed to set busy_timeout: %w", err)
}
s := &Store{db: db}
// Initialize all table structures
if err := s.initTables(); err != nil {
db.Close()
return nil, fmt.Errorf("failed to initialize table structure: %w", err)
}
// Initialize default data
if err := s.initDefaultData(); err != nil {
db.Close()
return nil, fmt.Errorf("failed to initialize default data: %w", err)
}
logger.Info("✅ Database enabled DELETE mode and FULL sync")
return s, nil
}
// NewFromDB creates Store from existing database connection
func NewFromDB(db *sql.DB) *Store {
return &Store{db: db}
}
// SetCryptoFuncs sets encryption/decryption functions
func (s *Store) SetCryptoFuncs(encrypt, decrypt func(string) string) {
s.mu.Lock()
defer s.mu.Unlock()
s.encryptFunc = encrypt
s.decryptFunc = decrypt
// Update already initialized sub-stores
if s.aiModel != nil {
s.aiModel.encryptFunc = encrypt
s.aiModel.decryptFunc = decrypt
}
if s.exchange != nil {
s.exchange.encryptFunc = encrypt
s.exchange.decryptFunc = decrypt
}
if s.trader != nil {
s.trader.decryptFunc = decrypt
}
}
// initTables initializes all database tables
func (s *Store) initTables() error {
// Initialize in dependency order
if err := s.User().initTables(); err != nil {
return fmt.Errorf("failed to initialize user tables: %w", err)
}
if err := s.AIModel().initTables(); err != nil {
return fmt.Errorf("failed to initialize AI model tables: %w", err)
}
if err := s.Exchange().initTables(); err != nil {
return fmt.Errorf("failed to initialize exchange tables: %w", err)
}
if err := s.Trader().initTables(); err != nil {
return fmt.Errorf("failed to initialize trader tables: %w", err)
}
if err := s.Decision().initTables(); err != nil {
return fmt.Errorf("failed to initialize decision log tables: %w", err)
}
if err := s.Backtest().initTables(); err != nil {
return fmt.Errorf("failed to initialize backtest tables: %w", err)
}
if err := s.Position().InitTables(); err != nil {
return fmt.Errorf("failed to initialize position tables: %w", err)
}
if err := s.Strategy().initTables(); err != nil {
return fmt.Errorf("failed to initialize strategy tables: %w", err)
}
if err := s.Equity().initTables(); err != nil {
return fmt.Errorf("failed to initialize equity tables: %w", err)
}
return nil
}
// initDefaultData initializes default data
func (s *Store) initDefaultData() error {
if err := s.AIModel().initDefaultData(); err != nil {
return err
}
if err := s.Exchange().initDefaultData(); err != nil {
return err
}
if err := s.Strategy().initDefaultData(); err != nil {
return err
}
// Migrate old decision_account_snapshots data to new trader_equity_snapshots table
if migrated, err := s.Equity().MigrateFromDecision(); err != nil {
logger.Warnf("failed to migrate equity data: %v", err)
} else if migrated > 0 {
logger.Infof("✅ Migrated %d equity records to new table", migrated)
}
return nil
}
// User gets user storage
func (s *Store) User() *UserStore {
s.mu.Lock()
defer s.mu.Unlock()
if s.user == nil {
s.user = &UserStore{db: s.db}
}
return s.user
}
// AIModel gets AI model storage
func (s *Store) AIModel() *AIModelStore {
s.mu.Lock()
defer s.mu.Unlock()
if s.aiModel == nil {
s.aiModel = &AIModelStore{
db: s.db,
encryptFunc: s.encryptFunc,
decryptFunc: s.decryptFunc,
}
}
return s.aiModel
}
// Exchange gets exchange storage
func (s *Store) Exchange() *ExchangeStore {
s.mu.Lock()
defer s.mu.Unlock()
if s.exchange == nil {
s.exchange = &ExchangeStore{
db: s.db,
encryptFunc: s.encryptFunc,
decryptFunc: s.decryptFunc,
}
}
return s.exchange
}
// Trader gets trader storage
func (s *Store) Trader() *TraderStore {
s.mu.Lock()
defer s.mu.Unlock()
if s.trader == nil {
s.trader = &TraderStore{
db: s.db,
decryptFunc: s.decryptFunc,
}
}
return s.trader
}
// Decision gets decision log storage
func (s *Store) Decision() *DecisionStore {
s.mu.Lock()
defer s.mu.Unlock()
if s.decision == nil {
s.decision = &DecisionStore{db: s.db}
}
return s.decision
}
// Backtest gets backtest data storage
func (s *Store) Backtest() *BacktestStore {
s.mu.Lock()
defer s.mu.Unlock()
if s.backtest == nil {
s.backtest = &BacktestStore{db: s.db}
}
return s.backtest
}
// Position gets position storage
func (s *Store) Position() *PositionStore {
s.mu.Lock()
defer s.mu.Unlock()
if s.position == nil {
s.position = NewPositionStore(s.db)
}
return s.position
}
// Strategy gets strategy storage
func (s *Store) Strategy() *StrategyStore {
s.mu.Lock()
defer s.mu.Unlock()
if s.strategy == nil {
s.strategy = &StrategyStore{db: s.db}
}
return s.strategy
}
// Equity gets equity storage
func (s *Store) Equity() *EquityStore {
s.mu.Lock()
defer s.mu.Unlock()
if s.equity == nil {
s.equity = &EquityStore{db: s.db}
}
return s.equity
}
// Close closes database connection
func (s *Store) Close() error {
return s.db.Close()
}
// DB gets underlying database connection (for legacy code compatibility, gradually deprecated)
// Deprecated: use Store methods instead
func (s *Store) DB() *sql.DB {
return s.db
}
// Transaction executes transaction
func (s *Store) Transaction(fn func(tx *sql.Tx) error) error {
tx, err := s.db.Begin()
if err != nil {
return fmt.Errorf("failed to begin transaction: %w", err)
}
if err := fn(tx); err != nil {
tx.Rollback()
return err
}
if err := tx.Commit(); err != nil {
return fmt.Errorf("failed to commit transaction: %w", err)
}
return nil
}