mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
458 lines
15 KiB
Go
458 lines
15 KiB
Go
package store
|
|
|
|
import (
|
|
"database/sql"
|
|
"encoding/json"
|
|
"fmt"
|
|
"time"
|
|
)
|
|
|
|
// StrategyStore strategy storage
|
|
type StrategyStore struct {
|
|
db *sql.DB
|
|
}
|
|
|
|
// Strategy strategy configuration
|
|
type Strategy struct {
|
|
ID string `json:"id"`
|
|
UserID string `json:"user_id"`
|
|
Name string `json:"name"`
|
|
Description string `json:"description"`
|
|
IsActive bool `json:"is_active"` // whether it is active (a user can only have one active strategy)
|
|
IsDefault bool `json:"is_default"` // whether it is a system default strategy
|
|
Config string `json:"config"` // strategy configuration in JSON format
|
|
CreatedAt time.Time `json:"created_at"`
|
|
UpdatedAt time.Time `json:"updated_at"`
|
|
}
|
|
|
|
// 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"`
|
|
// technical indicator switches
|
|
EnableEMA bool `json:"enable_ema"`
|
|
EnableMACD bool `json:"enable_macd"`
|
|
EnableRSI bool `json:"enable_rsi"`
|
|
EnableATR bool `json:"enable_atr"`
|
|
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]
|
|
// 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
|
|
}
|
|
|
|
// 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 {
|
|
// maximum number of positions
|
|
MaxPositions int `json:"max_positions"`
|
|
// BTC/ETH maximum leverage
|
|
BTCETHMaxLeverage int `json:"btc_eth_max_leverage"`
|
|
// altcoin maximum leverage
|
|
AltcoinMaxLeverage int `json:"altcoin_max_leverage"`
|
|
// minimum risk-reward ratio
|
|
MinRiskRewardRatio float64 `json:"min_risk_reward_ratio"`
|
|
// maximum margin usage
|
|
MaxMarginUsage float64 `json:"max_margin_usage"`
|
|
// maximum position ratio per coin (relative to account equity)
|
|
MaxPositionRatio float64 `json:"max_position_ratio"`
|
|
// minimum position size (USDT)
|
|
MinPositionSize float64 `json:"min_position_size"`
|
|
// minimum confidence level
|
|
MinConfidence int `json:"min_confidence"`
|
|
}
|
|
|
|
func (s *StrategyStore) initTables() error {
|
|
_, err := s.db.Exec(`
|
|
CREATE TABLE IF NOT EXISTS strategies (
|
|
id TEXT PRIMARY KEY,
|
|
user_id TEXT NOT NULL DEFAULT '',
|
|
name TEXT NOT NULL,
|
|
description TEXT DEFAULT '',
|
|
is_active BOOLEAN DEFAULT 0,
|
|
is_default BOOLEAN DEFAULT 0,
|
|
config TEXT NOT NULL DEFAULT '{}',
|
|
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
|
|
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP
|
|
)
|
|
`)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// create indexes
|
|
_, _ = s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_strategies_user_id ON strategies(user_id)`)
|
|
_, _ = s.db.Exec(`CREATE INDEX IF NOT EXISTS idx_strategies_is_active ON strategies(is_active)`)
|
|
|
|
// trigger: automatically update updated_at on update
|
|
_, err = s.db.Exec(`
|
|
CREATE TRIGGER IF NOT EXISTS update_strategies_updated_at
|
|
AFTER UPDATE ON strategies
|
|
BEGIN
|
|
UPDATE strategies SET updated_at = CURRENT_TIMESTAMP WHERE id = NEW.id;
|
|
END
|
|
`)
|
|
|
|
return err
|
|
}
|
|
|
|
func (s *StrategyStore) initDefaultData() error {
|
|
// check if default strategy already exists
|
|
var count int
|
|
s.db.QueryRow(`SELECT COUNT(*) FROM strategies WHERE is_default = 1`).Scan(&count)
|
|
if count > 0 {
|
|
return nil
|
|
}
|
|
|
|
// create system default strategy
|
|
defaultConfig := StrategyConfig{
|
|
CoinSource: CoinSourceConfig{
|
|
SourceType: "coinpool",
|
|
UseCoinPool: true,
|
|
CoinPoolLimit: 30,
|
|
CoinPoolAPIURL: "http://nofxaios.com:30006/api/ai500/list?auth=cm_568c67eae410d912c54c",
|
|
UseOITop: false,
|
|
OITopLimit: 0,
|
|
},
|
|
Indicators: IndicatorConfig{
|
|
Klines: KlineConfig{
|
|
PrimaryTimeframe: "5m",
|
|
PrimaryCount: 30,
|
|
LongerTimeframe: "4h",
|
|
LongerCount: 10,
|
|
EnableMultiTimeframe: true,
|
|
SelectedTimeframes: []string{"5m", "15m", "1h", "4h"},
|
|
},
|
|
EnableEMA: true,
|
|
EnableMACD: true,
|
|
EnableRSI: true,
|
|
EnableATR: true,
|
|
EnableVolume: true,
|
|
EnableOI: true,
|
|
EnableFundingRate: true,
|
|
EMAPeriods: []int{20, 50},
|
|
RSIPeriods: []int{7, 14},
|
|
ATRPeriods: []int{14},
|
|
EnableQuantData: true,
|
|
QuantDataAPIURL: "http://nofxaios.com:30006/api/coin/{symbol}?include=netflow,oi,price&auth=cm_568c67eae410d912c54c",
|
|
},
|
|
RiskControl: RiskControlConfig{
|
|
MaxPositions: 3,
|
|
BTCETHMaxLeverage: 5,
|
|
AltcoinMaxLeverage: 5,
|
|
MinRiskRewardRatio: 3.0,
|
|
MaxMarginUsage: 0.9,
|
|
MaxPositionRatio: 1.5,
|
|
MinPositionSize: 12,
|
|
MinConfidence: 75,
|
|
},
|
|
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`,
|
|
},
|
|
}
|
|
|
|
configJSON, _ := json.Marshal(defaultConfig)
|
|
|
|
_, err := s.db.Exec(`
|
|
INSERT INTO strategies (id, user_id, name, description, is_active, is_default, config)
|
|
VALUES ('default', 'system', 'Default Altcoin Strategy', 'System default altcoin trading strategy, uses AI500 coin pool, includes complete technical indicators', 0, 1, ?)
|
|
`, string(configJSON))
|
|
|
|
return err
|
|
}
|
|
|
|
// Create create a strategy
|
|
func (s *StrategyStore) Create(strategy *Strategy) error {
|
|
_, err := s.db.Exec(`
|
|
INSERT INTO strategies (id, user_id, name, description, is_active, is_default, config)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?)
|
|
`, strategy.ID, strategy.UserID, strategy.Name, strategy.Description, strategy.IsActive, strategy.IsDefault, strategy.Config)
|
|
return err
|
|
}
|
|
|
|
// Update update a strategy
|
|
func (s *StrategyStore) Update(strategy *Strategy) error {
|
|
_, err := s.db.Exec(`
|
|
UPDATE strategies SET
|
|
name = ?, description = ?, config = ?, updated_at = CURRENT_TIMESTAMP
|
|
WHERE id = ? AND user_id = ?
|
|
`, strategy.Name, strategy.Description, strategy.Config, strategy.ID, strategy.UserID)
|
|
return err
|
|
}
|
|
|
|
// Delete delete a strategy
|
|
func (s *StrategyStore) Delete(userID, id string) error {
|
|
// do not allow deleting system default strategy
|
|
var isDefault bool
|
|
s.db.QueryRow(`SELECT is_default FROM strategies WHERE id = ?`, id).Scan(&isDefault)
|
|
if isDefault {
|
|
return fmt.Errorf("cannot delete system default strategy")
|
|
}
|
|
|
|
_, err := s.db.Exec(`DELETE FROM strategies WHERE id = ? AND user_id = ?`, id, userID)
|
|
return err
|
|
}
|
|
|
|
// List get user's strategy list
|
|
func (s *StrategyStore) List(userID string) ([]*Strategy, error) {
|
|
// get user's own strategies + system default strategy
|
|
rows, err := s.db.Query(`
|
|
SELECT id, user_id, name, description, is_active, is_default, config, created_at, updated_at
|
|
FROM strategies
|
|
WHERE user_id = ? OR is_default = 1
|
|
ORDER BY is_default DESC, created_at DESC
|
|
`, userID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer rows.Close()
|
|
|
|
var strategies []*Strategy
|
|
for rows.Next() {
|
|
var st Strategy
|
|
var createdAt, updatedAt string
|
|
err := rows.Scan(
|
|
&st.ID, &st.UserID, &st.Name, &st.Description,
|
|
&st.IsActive, &st.IsDefault, &st.Config,
|
|
&createdAt, &updatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
st.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
|
st.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
|
|
strategies = append(strategies, &st)
|
|
}
|
|
return strategies, nil
|
|
}
|
|
|
|
// Get get a single strategy
|
|
func (s *StrategyStore) Get(userID, id string) (*Strategy, error) {
|
|
var st Strategy
|
|
var createdAt, updatedAt string
|
|
err := s.db.QueryRow(`
|
|
SELECT id, user_id, name, description, is_active, is_default, config, created_at, updated_at
|
|
FROM strategies
|
|
WHERE id = ? AND (user_id = ? OR is_default = 1)
|
|
`, id, userID).Scan(
|
|
&st.ID, &st.UserID, &st.Name, &st.Description,
|
|
&st.IsActive, &st.IsDefault, &st.Config,
|
|
&createdAt, &updatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
st.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
|
st.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
|
|
return &st, nil
|
|
}
|
|
|
|
// GetActive get user's currently active strategy
|
|
func (s *StrategyStore) GetActive(userID string) (*Strategy, error) {
|
|
var st Strategy
|
|
var createdAt, updatedAt string
|
|
err := s.db.QueryRow(`
|
|
SELECT id, user_id, name, description, is_active, is_default, config, created_at, updated_at
|
|
FROM strategies
|
|
WHERE user_id = ? AND is_active = 1
|
|
`, userID).Scan(
|
|
&st.ID, &st.UserID, &st.Name, &st.Description,
|
|
&st.IsActive, &st.IsDefault, &st.Config,
|
|
&createdAt, &updatedAt,
|
|
)
|
|
if err == sql.ErrNoRows {
|
|
// no active strategy, return system default strategy
|
|
return s.GetDefault()
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
st.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
|
st.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
|
|
return &st, nil
|
|
}
|
|
|
|
// GetDefault get system default strategy
|
|
func (s *StrategyStore) GetDefault() (*Strategy, error) {
|
|
var st Strategy
|
|
var createdAt, updatedAt string
|
|
err := s.db.QueryRow(`
|
|
SELECT id, user_id, name, description, is_active, is_default, config, created_at, updated_at
|
|
FROM strategies
|
|
WHERE is_default = 1
|
|
LIMIT 1
|
|
`).Scan(
|
|
&st.ID, &st.UserID, &st.Name, &st.Description,
|
|
&st.IsActive, &st.IsDefault, &st.Config,
|
|
&createdAt, &updatedAt,
|
|
)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
st.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
|
|
st.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
|
|
return &st, nil
|
|
}
|
|
|
|
// SetActive set active strategy (will first deactivate other strategies)
|
|
func (s *StrategyStore) SetActive(userID, strategyID string) error {
|
|
// begin transaction
|
|
tx, err := s.db.Begin()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer tx.Rollback()
|
|
|
|
// first deactivate all strategies for the user
|
|
_, err = tx.Exec(`UPDATE strategies SET is_active = 0 WHERE user_id = ?`, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// activate specified strategy
|
|
_, err = tx.Exec(`UPDATE strategies SET is_active = 1 WHERE id = ? AND (user_id = ? OR is_default = 1)`, strategyID, userID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
return tx.Commit()
|
|
}
|
|
|
|
// 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
|
|
}
|