Files
nofx/store/strategy.go
T
tinkle-community 5cff32e4f2 Feature/custom strategy (#1172)
* feat: add Strategy Studio with multi-timeframe support
- Add Strategy Studio page with three-column layout for strategy management
- Support multi-timeframe K-line data selection (5m, 15m, 1h, 4h, etc.)
- Add GetWithTimeframes() function in market package for fetching multiple timeframes
- Add TimeframeSeriesData struct for storing per-timeframe technical indicators
- Update formatMarketData() to display all selected timeframes in AI prompt
- Add strategy API endpoints for CRUD operations and test run
- Integrate real AI test runs with configured AI models
- Support custom AI500 and OI Top API URLs from strategy config
* docs: add Strategy Studio screenshot to README files
* fix: correct strategy-studio.png filename case in README
* refactor: remove legacy signal source config and simplify trader creation
- Remove signal source configuration from traders page (now handled by strategy)
- Remove advanced options (legacy config) from TraderConfigModal
- Rename default strategy to "默认山寨策略" with AI500 coin pool URL
- Delete SignalSourceModal and SignalSourceWarning components
- Clean up related stores, hooks, and page components
2025-12-06 07:20:11 +08:00

453 lines
14 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 (
"database/sql"
"encoding/json"
"fmt"
"time"
)
// StrategyStore 策略存储
type StrategyStore struct {
db *sql.DB
}
// Strategy 策略配置
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"` // 是否激活(一个用户只能有一个激活的策略)
IsDefault bool `json:"is_default"` // 是否为系统默认策略
Config string `json:"config"` // JSON 格式的策略配置
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// StrategyConfig 策略配置详情(JSON 结构)
type StrategyConfig struct {
// 币种来源配置
CoinSource CoinSourceConfig `json:"coin_source"`
// 量化数据配置
Indicators IndicatorConfig `json:"indicators"`
// 自定义 Prompt(附加在最后)
CustomPrompt string `json:"custom_prompt,omitempty"`
// 风险控制配置
RiskControl RiskControlConfig `json:"risk_control"`
// System Prompt 可编辑部分
PromptSections PromptSectionsConfig `json:"prompt_sections,omitempty"`
}
// PromptSectionsConfig System Prompt 可编辑部分
type PromptSectionsConfig struct {
// 角色定义(标题+描述)
RoleDefinition string `json:"role_definition,omitempty"`
// 交易频率认知
TradingFrequency string `json:"trading_frequency,omitempty"`
// 开仓标准
EntryStandards string `json:"entry_standards,omitempty"`
// 决策流程
DecisionProcess string `json:"decision_process,omitempty"`
}
// CoinSourceConfig 币种来源配置
type CoinSourceConfig struct {
// 来源类型: "static" | "coinpool" | "oi_top" | "mixed"
SourceType string `json:"source_type"`
// 静态币种列表(当 source_type = "static" 时使用)
StaticCoins []string `json:"static_coins,omitempty"`
// 是否使用 AI500 币种池
UseCoinPool bool `json:"use_coin_pool"`
// AI500 币种池最大数量
CoinPoolLimit int `json:"coin_pool_limit,omitempty"`
// AI500 币种池 API URL(策略级别配置)
CoinPoolAPIURL string `json:"coin_pool_api_url,omitempty"`
// 是否使用 OI Top
UseOITop bool `json:"use_oi_top"`
// OI Top 最大数量
OITopLimit int `json:"oi_top_limit,omitempty"`
// OI Top API URL(策略级别配置)
OITopAPIURL string `json:"oi_top_api_url,omitempty"`
}
// IndicatorConfig 指标配置
type IndicatorConfig struct {
// K线配置
Klines KlineConfig `json:"klines"`
// 技术指标开关
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"` // 持仓量
EnableFundingRate bool `json:"enable_funding_rate"` // 资金费率
// EMA 周期配置
EMAPeriods []int `json:"ema_periods,omitempty"` // 默认 [20, 50]
// RSI 周期配置
RSIPeriods []int `json:"rsi_periods,omitempty"` // 默认 [7, 14]
// ATR 周期配置
ATRPeriods []int `json:"atr_periods,omitempty"` // 默认 [14]
// 外部数据源
ExternalDataSources []ExternalDataSource `json:"external_data_sources,omitempty"`
}
// KlineConfig K线配置
type KlineConfig struct {
// 主时间周期: "1m", "3m", "5m", "15m", "1h", "4h"
PrimaryTimeframe string `json:"primary_timeframe"`
// 主时间周期 K 线数量
PrimaryCount int `json:"primary_count"`
// 长周期时间框架
LongerTimeframe string `json:"longer_timeframe,omitempty"`
// 长周期 K 线数量
LongerCount int `json:"longer_count,omitempty"`
// 是否启用多时间框架分析
EnableMultiTimeframe bool `json:"enable_multi_timeframe"`
// 选中的时间周期列表(新增:支持多周期选择)
SelectedTimeframes []string `json:"selected_timeframes,omitempty"`
}
// ExternalDataSource 外部数据源配置
type ExternalDataSource struct {
Name string `json:"name"` // 数据源名称
Type string `json:"type"` // 类型: "api" | "webhook"
URL string `json:"url"` // API URL
Method string `json:"method"` // HTTP 方法
Headers map[string]string `json:"headers,omitempty"`
DataPath string `json:"data_path,omitempty"` // JSON 数据路径
RefreshSecs int `json:"refresh_secs,omitempty"` // 刷新间隔(秒)
}
// RiskControlConfig 风险控制配置
type RiskControlConfig struct {
// 最大持仓数量
MaxPositions int `json:"max_positions"`
// BTC/ETH 最大杠杆
BTCETHMaxLeverage int `json:"btc_eth_max_leverage"`
// 山寨币最大杠杆
AltcoinMaxLeverage int `json:"altcoin_max_leverage"`
// 最小风险回报比
MinRiskRewardRatio float64 `json:"min_risk_reward_ratio"`
// 最大保证金使用率
MaxMarginUsage float64 `json:"max_margin_usage"`
// 单币种最大仓位比例(相对账户净值)
MaxPositionRatio float64 `json:"max_position_ratio"`
// 最小开仓金额(USDT
MinPositionSize float64 `json:"min_position_size"`
// 最小信心度
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
}
// 创建索引
_, _ = 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)`)
// 触发器:更新时自动更新 updated_at
_, 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 {
// 检查是否已有默认策略
var count int
s.db.QueryRow(`SELECT COUNT(*) FROM strategies WHERE is_default = 1`).Scan(&count)
if count > 0 {
return nil
}
// 创建系统默认策略
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},
},
RiskControl: RiskControlConfig{
MaxPositions: 3,
BTCETHMaxLeverage: 5,
AltcoinMaxLeverage: 5,
MinRiskRewardRatio: 3.0,
MaxMarginUsage: 0.9,
MaxPositionRatio: 1.5,
MinPositionSize: 12,
MinConfidence: 75,
},
PromptSections: PromptSectionsConfig{
RoleDefinition: `# 你是专业的加密货币交易AI
你的任务是根据提供的市场数据做出交易决策。你是一位经验丰富的量化交易员,擅长技术分析和风险管理。`,
TradingFrequency: `# ⏱️ 交易频率认知
- 优秀交易员:每天2-4笔 ≈ 每小时0.1-0.2笔
- 每小时>2笔 = 过度交易
- 单笔持仓时间≥30-60分钟
如果你发现自己每个周期都在交易 → 标准过低;若持仓<30分钟就平仓 → 过于急躁。`,
EntryStandards: `# 🎯 开仓标准(严格)
只在多重信号共振时开仓。自由运用任何有效的分析方法,避免单一指标、信号矛盾、横盘震荡、刚平仓即重启等低质量行为。`,
DecisionProcess: `# 📋 决策流程
1. 检查持仓 → 是否该止盈/止损
2. 扫描候选币 + 多时间框 → 是否存在强信号
3. 先写思维链,再输出结构化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', '默认山寨策略', '系统默认的山寨币交易策略,使用 AI500 币种池,包含完整的技术指标', 0, 1, ?)
`, string(configJSON))
return err
}
// Create 创建策略
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 更新策略
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 删除策略
func (s *StrategyStore) Delete(userID, id string) error {
// 不允许删除系统默认策略
var isDefault bool
s.db.QueryRow(`SELECT is_default FROM strategies WHERE id = ?`, id).Scan(&isDefault)
if isDefault {
return fmt.Errorf("不能删除系统默认策略")
}
_, err := s.db.Exec(`DELETE FROM strategies WHERE id = ? AND user_id = ?`, id, userID)
return err
}
// List 获取用户的策略列表
func (s *StrategyStore) List(userID string) ([]*Strategy, error) {
// 获取用户自己的策略 + 系统默认策略
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 获取单个策略
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 获取用户当前激活的策略
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 {
// 没有激活的策略,返回系统默认策略
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 获取系统默认策略
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 设置激活策略(会先取消其他策略的激活状态)
func (s *StrategyStore) SetActive(userID, strategyID string) error {
// 开启事务
tx, err := s.db.Begin()
if err != nil {
return err
}
defer tx.Rollback()
// 先取消该用户所有策略的激活状态
_, err = tx.Exec(`UPDATE strategies SET is_active = 0 WHERE user_id = ?`, userID)
if err != nil {
return err
}
// 激活指定策略
_, 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 复制策略(用于基于默认策略创建自定义策略)
func (s *StrategyStore) Duplicate(userID, sourceID, newID, newName string) error {
// 获取源策略
source, err := s.Get(userID, sourceID)
if err != nil {
return fmt.Errorf("获取源策略失败: %w", err)
}
// 创建新策略
newStrategy := &Strategy{
ID: newID,
UserID: userID,
Name: newName,
Description: "基于 [" + source.Name + "] 创建",
IsActive: false,
IsDefault: false,
Config: source.Config,
}
return s.Create(newStrategy)
}
// ParseConfig 解析策略配置 JSON
func (s *Strategy) ParseConfig() (*StrategyConfig, error) {
var config StrategyConfig
if err := json.Unmarshal([]byte(s.Config), &config); err != nil {
return nil, fmt.Errorf("解析策略配置失败: %w", err)
}
return &config, nil
}
// SetConfig 设置策略配置
func (s *Strategy) SetConfig(config *StrategyConfig) error {
data, err := json.Marshal(config)
if err != nil {
return fmt.Errorf("序列化策略配置失败: %w", err)
}
s.Config = string(data)
return nil
}