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
This commit is contained in:
tinkle-community
2025-12-06 07:20:11 +08:00
committed by GitHub
parent afb2d158ac
commit 5cff32e4f2
37 changed files with 4965 additions and 1051 deletions

452
store/strategy.go Normal file
View File

@@ -0,0 +1,452 @@
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
}