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

View File

@@ -18,32 +18,36 @@ type TraderStore struct {
// Trader 交易员配置
type Trader struct {
ID string `json:"id"`
UserID string `json:"user_id"`
Name string `json:"name"`
AIModelID string `json:"ai_model_id"`
ExchangeID string `json:"exchange_id"`
InitialBalance float64 `json:"initial_balance"`
ScanIntervalMinutes int `json:"scan_interval_minutes"`
IsRunning bool `json:"is_running"`
BTCETHLeverage int `json:"btc_eth_leverage"`
AltcoinLeverage int `json:"altcoin_leverage"`
TradingSymbols string `json:"trading_symbols"`
UseCoinPool bool `json:"use_coin_pool"`
UseOITop bool `json:"use_oi_top"`
CustomPrompt string `json:"custom_prompt"`
OverrideBasePrompt bool `json:"override_base_prompt"`
SystemPromptTemplate string `json:"system_prompt_template"`
IsCrossMargin bool `json:"is_cross_margin"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
ID string `json:"id"`
UserID string `json:"user_id"`
Name string `json:"name"`
AIModelID string `json:"ai_model_id"`
ExchangeID string `json:"exchange_id"`
StrategyID string `json:"strategy_id"` // 关联策略ID
InitialBalance float64 `json:"initial_balance"`
ScanIntervalMinutes int `json:"scan_interval_minutes"`
IsRunning bool `json:"is_running"`
IsCrossMargin bool `json:"is_cross_margin"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
// 以下字段已废弃,保留用于向后兼容,新交易员应使用 StrategyID
BTCETHLeverage int `json:"btc_eth_leverage,omitempty"`
AltcoinLeverage int `json:"altcoin_leverage,omitempty"`
TradingSymbols string `json:"trading_symbols,omitempty"`
UseCoinPool bool `json:"use_coin_pool,omitempty"`
UseOITop bool `json:"use_oi_top,omitempty"`
CustomPrompt string `json:"custom_prompt,omitempty"`
OverrideBasePrompt bool `json:"override_base_prompt,omitempty"`
SystemPromptTemplate string `json:"system_prompt_template,omitempty"`
}
// TraderFullConfig 交易员完整配置包含AI模型交易所)
// TraderFullConfig 交易员完整配置包含AI模型交易所和策略
type TraderFullConfig struct {
Trader *Trader
AIModel *AIModel
Exchange *Exchange
Strategy *Strategy // 关联的策略配置
}
func (s *TraderStore) initTables() error {
@@ -98,6 +102,7 @@ func (s *TraderStore) initTables() error {
`ALTER TABLE traders ADD COLUMN use_coin_pool BOOLEAN DEFAULT 0`,
`ALTER TABLE traders ADD COLUMN use_oi_top BOOLEAN DEFAULT 0`,
`ALTER TABLE traders ADD COLUMN system_prompt_template TEXT DEFAULT 'default'`,
`ALTER TABLE traders ADD COLUMN strategy_id TEXT DEFAULT ''`,
}
for _, q := range alterQueries {
s.db.Exec(q)
@@ -116,25 +121,27 @@ func (s *TraderStore) decrypt(encrypted string) string {
// Create 创建交易员
func (s *TraderStore) Create(trader *Trader) error {
_, err := s.db.Exec(`
INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes,
is_running, btc_eth_leverage, altcoin_leverage, trading_symbols, use_coin_pool,
use_oi_top, custom_prompt, override_base_prompt, system_prompt_template, is_cross_margin)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, trader.ID, trader.UserID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.InitialBalance,
trader.ScanIntervalMinutes, trader.IsRunning, trader.BTCETHLeverage, trader.AltcoinLeverage,
trader.TradingSymbols, trader.UseCoinPool, trader.UseOITop, trader.CustomPrompt,
trader.OverrideBasePrompt, trader.SystemPromptTemplate, trader.IsCrossMargin)
INSERT INTO traders (id, user_id, name, ai_model_id, exchange_id, strategy_id, initial_balance,
scan_interval_minutes, is_running, is_cross_margin,
btc_eth_leverage, altcoin_leverage, trading_symbols, use_coin_pool,
use_oi_top, custom_prompt, override_base_prompt, system_prompt_template)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
`, trader.ID, trader.UserID, trader.Name, trader.AIModelID, trader.ExchangeID, trader.StrategyID,
trader.InitialBalance, trader.ScanIntervalMinutes, trader.IsRunning, trader.IsCrossMargin,
trader.BTCETHLeverage, trader.AltcoinLeverage, trader.TradingSymbols, trader.UseCoinPool,
trader.UseOITop, trader.CustomPrompt, trader.OverrideBasePrompt, trader.SystemPromptTemplate)
return err
}
// List 获取用户的交易员列表
func (s *TraderStore) List(userID string) ([]*Trader, error) {
rows, err := s.db.Query(`
SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running,
SELECT id, user_id, name, ai_model_id, exchange_id, COALESCE(strategy_id, ''),
initial_balance, scan_interval_minutes, is_running, COALESCE(is_cross_margin, 1),
COALESCE(btc_eth_leverage, 5), COALESCE(altcoin_leverage, 5), COALESCE(trading_symbols, ''),
COALESCE(use_coin_pool, 0), COALESCE(use_oi_top, 0), COALESCE(custom_prompt, ''),
COALESCE(override_base_prompt, 0), COALESCE(system_prompt_template, 'default'),
COALESCE(is_cross_margin, 1), created_at, updated_at
created_at, updated_at
FROM traders WHERE user_id = ? ORDER BY created_at DESC
`, userID)
if err != nil {
@@ -147,11 +154,11 @@ func (s *TraderStore) List(userID string) ([]*Trader, error) {
var t Trader
var createdAt, updatedAt string
err := rows.Scan(
&t.ID, &t.UserID, &t.Name, &t.AIModelID, &t.ExchangeID,
&t.InitialBalance, &t.ScanIntervalMinutes, &t.IsRunning,
&t.ID, &t.UserID, &t.Name, &t.AIModelID, &t.ExchangeID, &t.StrategyID,
&t.InitialBalance, &t.ScanIntervalMinutes, &t.IsRunning, &t.IsCrossMargin,
&t.BTCETHLeverage, &t.AltcoinLeverage, &t.TradingSymbols,
&t.UseCoinPool, &t.UseOITop, &t.CustomPrompt, &t.OverrideBasePrompt,
&t.SystemPromptTemplate, &t.IsCrossMargin, &createdAt, &updatedAt,
&t.SystemPromptTemplate, &createdAt, &updatedAt,
)
if err != nil {
return nil, err
@@ -173,15 +180,12 @@ func (s *TraderStore) UpdateStatus(userID, id string, isRunning bool) error {
func (s *TraderStore) Update(trader *Trader) error {
_, err := s.db.Exec(`
UPDATE traders SET
name = ?, ai_model_id = ?, exchange_id = ?, scan_interval_minutes = ?,
btc_eth_leverage = ?, altcoin_leverage = ?, trading_symbols = ?,
custom_prompt = ?, override_base_prompt = ?, system_prompt_template = ?,
is_cross_margin = ?, updated_at = CURRENT_TIMESTAMP
name = ?, ai_model_id = ?, exchange_id = ?, strategy_id = ?,
scan_interval_minutes = ?, is_cross_margin = ?,
updated_at = CURRENT_TIMESTAMP
WHERE id = ? AND user_id = ?
`, trader.Name, trader.AIModelID, trader.ExchangeID, trader.ScanIntervalMinutes,
trader.BTCETHLeverage, trader.AltcoinLeverage, trader.TradingSymbols,
trader.CustomPrompt, trader.OverrideBasePrompt, trader.SystemPromptTemplate,
trader.IsCrossMargin, trader.ID, trader.UserID)
`, trader.Name, trader.AIModelID, trader.ExchangeID, trader.StrategyID,
trader.ScanIntervalMinutes, trader.IsCrossMargin, trader.ID, trader.UserID)
return err
}
@@ -215,11 +219,12 @@ func (s *TraderStore) GetFullConfig(userID, traderID string) (*TraderFullConfig,
err := s.db.QueryRow(`
SELECT
t.id, t.user_id, t.name, t.ai_model_id, t.exchange_id, t.initial_balance, t.scan_interval_minutes, t.is_running,
t.id, t.user_id, t.name, t.ai_model_id, t.exchange_id, COALESCE(t.strategy_id, ''),
t.initial_balance, t.scan_interval_minutes, t.is_running, COALESCE(t.is_cross_margin, 1),
COALESCE(t.btc_eth_leverage, 5), COALESCE(t.altcoin_leverage, 5), COALESCE(t.trading_symbols, ''),
COALESCE(t.use_coin_pool, 0), COALESCE(t.use_oi_top, 0), COALESCE(t.custom_prompt, ''),
COALESCE(t.override_base_prompt, 0), COALESCE(t.system_prompt_template, 'default'),
COALESCE(t.is_cross_margin, 1), t.created_at, t.updated_at,
t.created_at, t.updated_at,
a.id, a.user_id, a.name, a.provider, a.enabled, a.api_key,
COALESCE(a.custom_api_url, ''), COALESCE(a.custom_model_name, ''), a.created_at, a.updated_at,
e.id, e.user_id, e.name, e.type, e.enabled, e.api_key, e.secret_key, e.testnet,
@@ -231,11 +236,11 @@ func (s *TraderStore) GetFullConfig(userID, traderID string) (*TraderFullConfig,
JOIN exchanges e ON t.exchange_id = e.id AND t.user_id = e.user_id
WHERE t.id = ? AND t.user_id = ?
`, traderID, userID).Scan(
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID,
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning,
&trader.ID, &trader.UserID, &trader.Name, &trader.AIModelID, &trader.ExchangeID, &trader.StrategyID,
&trader.InitialBalance, &trader.ScanIntervalMinutes, &trader.IsRunning, &trader.IsCrossMargin,
&trader.BTCETHLeverage, &trader.AltcoinLeverage, &trader.TradingSymbols,
&trader.UseCoinPool, &trader.UseOITop, &trader.CustomPrompt, &trader.OverrideBasePrompt,
&trader.SystemPromptTemplate, &trader.IsCrossMargin, &traderCreatedAt, &traderUpdatedAt,
&trader.SystemPromptTemplate, &traderCreatedAt, &traderUpdatedAt,
&aiModel.ID, &aiModel.UserID, &aiModel.Name, &aiModel.Provider, &aiModel.Enabled, &aiModel.APIKey,
&aiModel.CustomAPIURL, &aiModel.CustomModelName, &aiModelCreatedAt, &aiModelUpdatedAt,
&exchange.ID, &exchange.UserID, &exchange.Name, &exchange.Type, &exchange.Enabled,
@@ -263,13 +268,78 @@ func (s *TraderStore) GetFullConfig(userID, traderID string) (*TraderFullConfig,
exchange.LighterPrivateKey = s.decrypt(exchange.LighterPrivateKey)
exchange.LighterAPIKeyPrivateKey = s.decrypt(exchange.LighterAPIKeyPrivateKey)
// 加载关联的策略
var strategy *Strategy
if trader.StrategyID != "" {
strategy, _ = s.getStrategyByID(userID, trader.StrategyID)
}
// 如果没有关联策略,获取用户的激活策略或默认策略
if strategy == nil {
strategy, _ = s.getActiveOrDefaultStrategy(userID)
}
return &TraderFullConfig{
Trader: &trader,
AIModel: &aiModel,
Exchange: &exchange,
Strategy: strategy,
}, nil
}
// getStrategyByID 内部方法根据ID获取策略
func (s *TraderStore) getStrategyByID(userID, strategyID string) (*Strategy, error) {
var strategy 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)
`, strategyID, userID).Scan(
&strategy.ID, &strategy.UserID, &strategy.Name, &strategy.Description,
&strategy.IsActive, &strategy.IsDefault, &strategy.Config, &createdAt, &updatedAt,
)
if err != nil {
return nil, err
}
strategy.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
strategy.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
return &strategy, nil
}
// getActiveOrDefaultStrategy 内部方法:获取用户激活的策略或系统默认策略
func (s *TraderStore) getActiveOrDefaultStrategy(userID string) (*Strategy, error) {
var strategy 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(
&strategy.ID, &strategy.UserID, &strategy.Name, &strategy.Description,
&strategy.IsActive, &strategy.IsDefault, &strategy.Config, &createdAt, &updatedAt,
)
if err == nil {
strategy.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
strategy.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
return &strategy, nil
}
// 回退到系统默认策略
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(
&strategy.ID, &strategy.UserID, &strategy.Name, &strategy.Description,
&strategy.IsActive, &strategy.IsDefault, &strategy.Config, &createdAt, &updatedAt,
)
if err != nil {
return nil, err
}
strategy.CreatedAt, _ = time.Parse("2006-01-02 15:04:05", createdAt)
strategy.UpdatedAt, _ = time.Parse("2006-01-02 15:04:05", updatedAt)
return &strategy, nil
}
// GetCustomCoins 获取所有交易员自定义币种
func (s *TraderStore) GetCustomCoins() []string {
var symbol string
@@ -310,11 +380,12 @@ func (s *TraderStore) GetCustomCoins() []string {
// ListAll 获取所有用户的交易员列表
func (s *TraderStore) ListAll() ([]*Trader, error) {
rows, err := s.db.Query(`
SELECT id, user_id, name, ai_model_id, exchange_id, initial_balance, scan_interval_minutes, is_running,
SELECT id, user_id, name, ai_model_id, exchange_id, COALESCE(strategy_id, ''),
initial_balance, scan_interval_minutes, is_running, COALESCE(is_cross_margin, 1),
COALESCE(btc_eth_leverage, 5), COALESCE(altcoin_leverage, 5), COALESCE(trading_symbols, ''),
COALESCE(use_coin_pool, 0), COALESCE(use_oi_top, 0), COALESCE(custom_prompt, ''),
COALESCE(override_base_prompt, 0), COALESCE(system_prompt_template, 'default'),
COALESCE(is_cross_margin, 1), created_at, updated_at
created_at, updated_at
FROM traders ORDER BY created_at DESC
`)
if err != nil {
@@ -327,11 +398,11 @@ func (s *TraderStore) ListAll() ([]*Trader, error) {
var t Trader
var createdAt, updatedAt string
err := rows.Scan(
&t.ID, &t.UserID, &t.Name, &t.AIModelID, &t.ExchangeID,
&t.InitialBalance, &t.ScanIntervalMinutes, &t.IsRunning,
&t.ID, &t.UserID, &t.Name, &t.AIModelID, &t.ExchangeID, &t.StrategyID,
&t.InitialBalance, &t.ScanIntervalMinutes, &t.IsRunning, &t.IsCrossMargin,
&t.BTCETHLeverage, &t.AltcoinLeverage, &t.TradingSymbols,
&t.UseCoinPool, &t.UseOITop, &t.CustomPrompt, &t.OverrideBasePrompt,
&t.SystemPromptTemplate, &t.IsCrossMargin, &createdAt, &updatedAt,
&t.SystemPromptTemplate, &createdAt, &updatedAt,
)
if err != nil {
return nil, err