Files
nofx/store/strategy.go
T
tinkle-community 2d272bb7b8 feat: migrate store layer to GORM with PostgreSQL support
- Migrate all store packages from raw database/sql to GORM ORM
- Add PostgreSQL support alongside SQLite
- Move EncryptedString type to crypto package for cleaner architecture
- Add automatic encryption/decryption for sensitive fields (API keys, secrets)
- Fix PostgreSQL AutoMigrate conflicts by skipping existing tables
- Fix duplicate /klines route registration
- Update tests to use GORM database connections
- Add database configuration support in config package
2026-01-01 19:32:49 +08:00

422 lines
16 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package store
import (
"encoding/json"
"fmt"
"time"
"gorm.io/gorm"
)
// StrategyStore strategy storage
type StrategyStore struct {
db *gorm.DB
}
// Strategy strategy configuration
type Strategy struct {
ID string `gorm:"primaryKey" json:"id"`
UserID string `gorm:"column:user_id;not null;default:'';index" json:"user_id"`
Name string `gorm:"not null" json:"name"`
Description string `gorm:"default:''" json:"description"`
IsActive bool `gorm:"column:is_active;default:false;index" json:"is_active"`
IsDefault bool `gorm:"column:is_default;default:false" json:"is_default"`
Config string `gorm:"not null;default:'{}'" json:"config"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
func (Strategy) TableName() string { return "strategies" }
// StrategyConfig strategy configuration details (JSON structure)
type StrategyConfig struct {
// coin source configuration
CoinSource CoinSourceConfig `json:"coin_source"`
// quantitative data configuration
Indicators IndicatorConfig `json:"indicators"`
// custom prompt (appended at the end)
CustomPrompt string `json:"custom_prompt,omitempty"`
// risk control configuration
RiskControl RiskControlConfig `json:"risk_control"`
// editable sections of System Prompt
PromptSections PromptSectionsConfig `json:"prompt_sections,omitempty"`
}
// PromptSectionsConfig editable sections of System Prompt
type PromptSectionsConfig struct {
// role definition (title + description)
RoleDefinition string `json:"role_definition,omitempty"`
// trading frequency awareness
TradingFrequency string `json:"trading_frequency,omitempty"`
// entry standards
EntryStandards string `json:"entry_standards,omitempty"`
// decision process
DecisionProcess string `json:"decision_process,omitempty"`
}
// CoinSourceConfig coin source configuration
type CoinSourceConfig struct {
// source type: "static" | "coinpool" | "oi_top" | "mixed"
SourceType string `json:"source_type"`
// static coin list (used when source_type = "static")
StaticCoins []string `json:"static_coins,omitempty"`
// whether to use AI500 coin pool
UseCoinPool bool `json:"use_coin_pool"`
// AI500 coin pool maximum count
CoinPoolLimit int `json:"coin_pool_limit,omitempty"`
// AI500 coin pool API URL (strategy-level configuration)
CoinPoolAPIURL string `json:"coin_pool_api_url,omitempty"`
// whether to use OI Top
UseOITop bool `json:"use_oi_top"`
// OI Top maximum count
OITopLimit int `json:"oi_top_limit,omitempty"`
// OI Top API URL (strategy-level configuration)
OITopAPIURL string `json:"oi_top_api_url,omitempty"`
}
// IndicatorConfig indicator configuration
type IndicatorConfig struct {
// K-line configuration
Klines KlineConfig `json:"klines"`
// raw kline data (OHLCV) - always enabled, required for AI analysis
EnableRawKlines bool `json:"enable_raw_klines"`
// technical indicator switches
EnableEMA bool `json:"enable_ema"`
EnableMACD bool `json:"enable_macd"`
EnableRSI bool `json:"enable_rsi"`
EnableATR bool `json:"enable_atr"`
EnableBOLL bool `json:"enable_boll"` // Bollinger Bands
EnableVolume bool `json:"enable_volume"`
EnableOI bool `json:"enable_oi"` // open interest
EnableFundingRate bool `json:"enable_funding_rate"` // funding rate
// EMA period configuration
EMAPeriods []int `json:"ema_periods,omitempty"` // default [20, 50]
// RSI period configuration
RSIPeriods []int `json:"rsi_periods,omitempty"` // default [7, 14]
// ATR period configuration
ATRPeriods []int `json:"atr_periods,omitempty"` // default [14]
// BOLL period configuration (period, standard deviation multiplier is fixed at 2)
BOLLPeriods []int `json:"boll_periods,omitempty"` // default [20] - can select multiple timeframes
// external data sources
ExternalDataSources []ExternalDataSource `json:"external_data_sources,omitempty"`
// quantitative data sources (capital flow, position changes, price changes)
EnableQuantData bool `json:"enable_quant_data"` // whether to enable quantitative data
QuantDataAPIURL string `json:"quant_data_api_url,omitempty"` // quantitative data API address
EnableQuantOI bool `json:"enable_quant_oi"` // whether to show OI data
EnableQuantNetflow bool `json:"enable_quant_netflow"` // whether to show Netflow data
// OI ranking data (market-wide open interest increase/decrease rankings)
EnableOIRanking bool `json:"enable_oi_ranking"` // whether to enable OI ranking data
OIRankingAPIURL string `json:"oi_ranking_api_url,omitempty"` // OI ranking API base URL
OIRankingDuration string `json:"oi_ranking_duration,omitempty"` // duration: 1h, 4h, 24h
OIRankingLimit int `json:"oi_ranking_limit,omitempty"` // number of entries (default 10)
}
// KlineConfig K-line configuration
type KlineConfig struct {
// primary timeframe: "1m", "3m", "5m", "15m", "1h", "4h"
PrimaryTimeframe string `json:"primary_timeframe"`
// primary timeframe K-line count
PrimaryCount int `json:"primary_count"`
// longer timeframe
LongerTimeframe string `json:"longer_timeframe,omitempty"`
// longer timeframe K-line count
LongerCount int `json:"longer_count,omitempty"`
// whether to enable multi-timeframe analysis
EnableMultiTimeframe bool `json:"enable_multi_timeframe"`
// selected timeframe list (new: supports multi-timeframe selection)
SelectedTimeframes []string `json:"selected_timeframes,omitempty"`
}
// ExternalDataSource external data source configuration
type ExternalDataSource struct {
Name string `json:"name"` // data source name
Type string `json:"type"` // type: "api" | "webhook"
URL string `json:"url"` // API URL
Method string `json:"method"` // HTTP method
Headers map[string]string `json:"headers,omitempty"`
DataPath string `json:"data_path,omitempty"` // JSON data path
RefreshSecs int `json:"refresh_secs,omitempty"` // refresh interval (seconds)
}
// RiskControlConfig risk control configuration
type RiskControlConfig struct {
// Max number of coins held simultaneously (CODE ENFORCED)
MaxPositions int `json:"max_positions"`
// BTC/ETH exchange leverage for opening positions (AI guided)
BTCETHMaxLeverage int `json:"btc_eth_max_leverage"`
// Altcoin exchange leverage for opening positions (AI guided)
AltcoinMaxLeverage int `json:"altcoin_max_leverage"`
// BTC/ETH single position max value = equity × this ratio (CODE ENFORCED, default: 5)
BTCETHMaxPositionValueRatio float64 `json:"btc_eth_max_position_value_ratio"`
// Altcoin single position max value = equity × this ratio (CODE ENFORCED, default: 1)
AltcoinMaxPositionValueRatio float64 `json:"altcoin_max_position_value_ratio"`
// Max margin utilization (e.g. 0.9 = 90%) (CODE ENFORCED)
MaxMarginUsage float64 `json:"max_margin_usage"`
// Min position size in USDT (CODE ENFORCED)
MinPositionSize float64 `json:"min_position_size"`
// Min take_profit / stop_loss ratio (AI guided)
MinRiskRewardRatio float64 `json:"min_risk_reward_ratio"`
// Min AI confidence to open position (AI guided)
MinConfidence int `json:"min_confidence"`
}
// NewStrategyStore creates a new StrategyStore
func NewStrategyStore(db *gorm.DB) *StrategyStore {
return &StrategyStore{db: db}
}
func (s *StrategyStore) initTables() error {
// For PostgreSQL with existing table, skip AutoMigrate
if s.db.Dialector.Name() == "postgres" {
var tableExists int64
s.db.Raw(`SELECT COUNT(*) FROM information_schema.tables WHERE table_name = 'strategies'`).Scan(&tableExists)
if tableExists > 0 {
return nil
}
}
return s.db.AutoMigrate(&Strategy{})
}
func (s *StrategyStore) initDefaultData() error {
// No longer pre-populate strategies - create on demand when user configures
return nil
}
// GetDefaultStrategyConfig returns the default strategy configuration for the given language
func GetDefaultStrategyConfig(lang string) StrategyConfig {
config := StrategyConfig{
CoinSource: CoinSourceConfig{
SourceType: "coinpool",
UseCoinPool: true,
CoinPoolLimit: 10,
CoinPoolAPIURL: "http://nofxaios.com:30006/api/ai500/list?auth=cm_568c67eae410d912c54c",
UseOITop: false,
OITopLimit: 20,
OITopAPIURL: "http://nofxaios.com:30006/api/oi/top-ranking?limit=20&duration=1h&auth=cm_568c67eae410d912c54c",
},
Indicators: IndicatorConfig{
Klines: KlineConfig{
PrimaryTimeframe: "5m",
PrimaryCount: 30,
LongerTimeframe: "4h",
LongerCount: 10,
EnableMultiTimeframe: true,
SelectedTimeframes: []string{"5m", "15m", "1h", "4h"},
},
EnableRawKlines: true, // Required - raw OHLCV data for AI analysis
EnableEMA: false,
EnableMACD: false,
EnableRSI: false,
EnableATR: false,
EnableBOLL: false,
EnableVolume: true,
EnableOI: true,
EnableFundingRate: true,
EMAPeriods: []int{20, 50},
RSIPeriods: []int{7, 14},
ATRPeriods: []int{14},
BOLLPeriods: []int{20},
EnableQuantData: true,
QuantDataAPIURL: "http://nofxaios.com:30006/api/coin/{symbol}?include=netflow,oi,price&auth=cm_568c67eae410d912c54c",
EnableQuantOI: true,
EnableQuantNetflow: true,
// OI ranking data - market-wide OI increase/decrease rankings
EnableOIRanking: true,
OIRankingAPIURL: "http://nofxaios.com:30006",
OIRankingDuration: "1h",
OIRankingLimit: 10,
},
RiskControl: RiskControlConfig{
MaxPositions: 3, // Max 3 coins simultaneously (CODE ENFORCED)
BTCETHMaxLeverage: 5, // BTC/ETH exchange leverage (AI guided)
AltcoinMaxLeverage: 5, // Altcoin exchange leverage (AI guided)
BTCETHMaxPositionValueRatio: 5.0, // BTC/ETH: max position = 5x equity (CODE ENFORCED)
AltcoinMaxPositionValueRatio: 1.0, // Altcoin: max position = 1x equity (CODE ENFORCED)
MaxMarginUsage: 0.9, // Max 90% margin usage (CODE ENFORCED)
MinPositionSize: 12, // Min 12 USDT per position (CODE ENFORCED)
MinRiskRewardRatio: 3.0, // Min 3:1 profit/loss ratio (AI guided)
MinConfidence: 75, // Min 75% confidence (AI guided)
},
}
if lang == "zh" {
config.PromptSections = PromptSectionsConfig{
RoleDefinition: `# 你是一个专业的加密货币交易AI
你的任务是根据提供的市场数据做出交易决策。你是一个经验丰富的量化交易员,擅长技术分析和风险管理。`,
TradingFrequency: `# ⏱️ 交易频率意识
- 优秀交易员:每天2-4笔 ≈ 每小时0.1-0.2笔
- 每小时超过2笔 = 过度交易
- 单笔持仓时间 ≥ 30-60分钟
如果你发现自己每个周期都在交易 → 标准太低;如果持仓不到30分钟就平仓 → 太冲动。`,
EntryStandards: `# 🎯 入场标准(严格)
只在多个信号共振时入场。自由使用任何有效的分析方法,避免单一指标、信号矛盾、横盘震荡、或平仓后立即重新开仓等低质量行为。`,
DecisionProcess: `# 📋 决策流程
1. 检查持仓 → 是否止盈/止损
2. 扫描候选币种 + 多时间框架 → 是否存在强信号
3. 先写思维链,再输出结构化JSON`,
}
} else {
config.PromptSections = PromptSectionsConfig{
RoleDefinition: `# You are a professional cryptocurrency trading AI
Your task is to make trading decisions based on the provided market data. You are an experienced quantitative trader skilled in technical analysis and risk management.`,
TradingFrequency: `# ⏱️ Trading Frequency Awareness
- Excellent trader: 2-4 trades per day ≈ 0.1-0.2 trades per hour
- >2 trades per hour = overtrading
- Single position holding time ≥ 30-60 minutes
If you find yourself trading every cycle → standards are too low; if closing positions in <30 minutes → too impulsive.`,
EntryStandards: `# 🎯 Entry Standards (Strict)
Only enter positions when multiple signals resonate. Freely use any effective analysis methods, avoid low-quality behaviors such as single indicators, contradictory signals, sideways oscillation, or immediately restarting after closing positions.`,
DecisionProcess: `# 📋 Decision Process
1. Check positions → whether to take profit/stop loss
2. Scan candidate coins + multi-timeframe → whether strong signals exist
3. Write chain of thought first, then output structured JSON`,
}
}
return config
}
// Create create a strategy
func (s *StrategyStore) Create(strategy *Strategy) error {
return s.db.Create(strategy).Error
}
// Update update a strategy
func (s *StrategyStore) Update(strategy *Strategy) error {
return s.db.Model(&Strategy{}).
Where("id = ? AND user_id = ?", strategy.ID, strategy.UserID).
Updates(map[string]interface{}{
"name": strategy.Name,
"description": strategy.Description,
"config": strategy.Config,
"updated_at": time.Now(),
}).Error
}
// Delete delete a strategy
func (s *StrategyStore) Delete(userID, id string) error {
// do not allow deleting system default strategy
var st Strategy
if err := s.db.Where("id = ?", id).First(&st).Error; err == nil && st.IsDefault {
return fmt.Errorf("cannot delete system default strategy")
}
return s.db.Where("id = ? AND user_id = ?", id, userID).Delete(&Strategy{}).Error
}
// List get user's strategy list
func (s *StrategyStore) List(userID string) ([]*Strategy, error) {
var strategies []*Strategy
err := s.db.Where("user_id = ? OR is_default = ?", userID, true).
Order("is_default DESC, created_at DESC").
Find(&strategies).Error
if err != nil {
return nil, err
}
return strategies, nil
}
// Get get a single strategy
func (s *StrategyStore) Get(userID, id string) (*Strategy, error) {
var st Strategy
err := s.db.Where("id = ? AND (user_id = ? OR is_default = ?)", id, userID, true).
First(&st).Error
if err != nil {
return nil, err
}
return &st, nil
}
// GetActive get user's currently active strategy
func (s *StrategyStore) GetActive(userID string) (*Strategy, error) {
var st Strategy
err := s.db.Where("user_id = ? AND is_active = ?", userID, true).First(&st).Error
if err == gorm.ErrRecordNotFound {
// no active strategy, return system default strategy
return s.GetDefault()
}
if err != nil {
return nil, err
}
return &st, nil
}
// GetDefault get system default strategy
func (s *StrategyStore) GetDefault() (*Strategy, error) {
var st Strategy
err := s.db.Where("is_default = ?", true).First(&st).Error
if err != nil {
return nil, err
}
return &st, nil
}
// SetActive set active strategy (will first deactivate other strategies)
func (s *StrategyStore) SetActive(userID, strategyID string) error {
return s.db.Transaction(func(tx *gorm.DB) error {
// first deactivate all strategies for the user
if err := tx.Model(&Strategy{}).Where("user_id = ?", userID).
Update("is_active", false).Error; err != nil {
return err
}
// activate specified strategy
return tx.Model(&Strategy{}).
Where("id = ? AND (user_id = ? OR is_default = ?)", strategyID, userID, true).
Update("is_active", true).Error
})
}
// Duplicate duplicate a strategy (used to create custom strategy based on default strategy)
func (s *StrategyStore) Duplicate(userID, sourceID, newID, newName string) error {
// get source strategy
source, err := s.Get(userID, sourceID)
if err != nil {
return fmt.Errorf("failed to get source strategy: %w", err)
}
// create new strategy
newStrategy := &Strategy{
ID: newID,
UserID: userID,
Name: newName,
Description: "Created based on [" + source.Name + "]",
IsActive: false,
IsDefault: false,
Config: source.Config,
}
return s.Create(newStrategy)
}
// ParseConfig parse strategy configuration JSON
func (s *Strategy) ParseConfig() (*StrategyConfig, error) {
var config StrategyConfig
if err := json.Unmarshal([]byte(s.Config), &config); err != nil {
return nil, fmt.Errorf("failed to parse strategy configuration: %w", err)
}
return &config, nil
}
// SetConfig set strategy configuration
func (s *Strategy) SetConfig(config *StrategyConfig) error {
data, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("failed to serialize strategy configuration: %w", err)
}
s.Config = string(data)
return nil
}