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
+269
View File
@@ -112,6 +112,230 @@ func Get(symbol string) (*Data, error) {
}, nil
}
// GetWithTimeframes 获取指定多个时间周期的市场数据
// timeframes: 时间周期列表,如 ["5m", "15m", "1h", "4h"]
// primaryTimeframe: 主时间周期(用于计算当前指标),默认使用 timeframes[0]
// count: 每个时间周期的 K 线数量
func GetWithTimeframes(symbol string, timeframes []string, primaryTimeframe string, count int) (*Data, error) {
symbol = Normalize(symbol)
if len(timeframes) == 0 {
return nil, fmt.Errorf("至少需要一个时间周期")
}
// 如果未指定主周期,使用第一个
if primaryTimeframe == "" {
primaryTimeframe = timeframes[0]
}
// 确保主周期在列表中
hasPrimary := false
for _, tf := range timeframes {
if tf == primaryTimeframe {
hasPrimary = true
break
}
}
if !hasPrimary {
timeframes = append([]string{primaryTimeframe}, timeframes...)
}
// 存储所有时间周期的数据
timeframeData := make(map[string]*TimeframeSeriesData)
var primaryKlines []Kline
// 获取每个时间周期的 K 线数据
for _, tf := range timeframes {
klines, err := WSMonitorCli.GetCurrentKlines(symbol, tf)
if err != nil {
logger.Infof("⚠️ 获取 %s %s K线失败: %v", symbol, tf, err)
continue
}
if len(klines) == 0 {
logger.Infof("⚠️ %s %s K线数据为空", symbol, tf)
continue
}
// 保存主周期的 K 线用于计算基础指标
if tf == primaryTimeframe {
primaryKlines = klines
}
// 计算该时间周期的系列数据
seriesData := calculateTimeframeSeries(klines, tf)
timeframeData[tf] = seriesData
}
// 如果主周期数据为空,返回错误
if len(primaryKlines) == 0 {
return nil, fmt.Errorf("主时间周期 %s K线数据为空", primaryTimeframe)
}
// Data staleness detection
if isStaleData(primaryKlines, symbol) {
logger.Infof("⚠️ WARNING: %s detected stale data (consecutive price freeze), skipping symbol", symbol)
return nil, fmt.Errorf("%s data is stale, possible cache failure", symbol)
}
// 计算当前指标 (基于主周期最新数据)
currentPrice := primaryKlines[len(primaryKlines)-1].Close
currentEMA20 := calculateEMA(primaryKlines, 20)
currentMACD := calculateMACD(primaryKlines)
currentRSI7 := calculateRSI(primaryKlines, 7)
// 计算价格变化
priceChange1h := calculatePriceChangeByBars(primaryKlines, primaryTimeframe, 60) // 1小时
priceChange4h := calculatePriceChangeByBars(primaryKlines, primaryTimeframe, 240) // 4小时
// 获取OI数据
oiData, err := getOpenInterestData(symbol)
if err != nil {
oiData = &OIData{Latest: 0, Average: 0}
}
// 获取Funding Rate
fundingRate, _ := getFundingRate(symbol)
return &Data{
Symbol: symbol,
CurrentPrice: currentPrice,
PriceChange1h: priceChange1h,
PriceChange4h: priceChange4h,
CurrentEMA20: currentEMA20,
CurrentMACD: currentMACD,
CurrentRSI7: currentRSI7,
OpenInterest: oiData,
FundingRate: fundingRate,
TimeframeData: timeframeData,
}, nil
}
// calculateTimeframeSeries 计算单个时间周期的系列数据
func calculateTimeframeSeries(klines []Kline, timeframe string) *TimeframeSeriesData {
data := &TimeframeSeriesData{
Timeframe: timeframe,
MidPrices: make([]float64, 0, 10),
EMA20Values: make([]float64, 0, 10),
EMA50Values: make([]float64, 0, 10),
MACDValues: make([]float64, 0, 10),
RSI7Values: make([]float64, 0, 10),
RSI14Values: make([]float64, 0, 10),
Volume: make([]float64, 0, 10),
}
// 获取最近10个数据点
start := len(klines) - 10
if start < 0 {
start = 0
}
for i := start; i < len(klines); i++ {
data.MidPrices = append(data.MidPrices, klines[i].Close)
data.Volume = append(data.Volume, klines[i].Volume)
// 计算每个点的 EMA20
if i >= 19 {
ema20 := calculateEMA(klines[:i+1], 20)
data.EMA20Values = append(data.EMA20Values, ema20)
}
// 计算每个点的 EMA50
if i >= 49 {
ema50 := calculateEMA(klines[:i+1], 50)
data.EMA50Values = append(data.EMA50Values, ema50)
}
// 计算每个点的 MACD
if i >= 25 {
macd := calculateMACD(klines[:i+1])
data.MACDValues = append(data.MACDValues, macd)
}
// 计算每个点的 RSI
if i >= 7 {
rsi7 := calculateRSI(klines[:i+1], 7)
data.RSI7Values = append(data.RSI7Values, rsi7)
}
if i >= 14 {
rsi14 := calculateRSI(klines[:i+1], 14)
data.RSI14Values = append(data.RSI14Values, rsi14)
}
}
// 计算 ATR14
data.ATR14 = calculateATR(klines, 14)
return data
}
// calculatePriceChangeByBars 根据时间周期计算需要回溯多少根 K 线来计算价格变化
func calculatePriceChangeByBars(klines []Kline, timeframe string, targetMinutes int) float64 {
if len(klines) < 2 {
return 0
}
// 解析时间周期为分钟数
tfMinutes := parseTimeframeToMinutes(timeframe)
if tfMinutes <= 0 {
return 0
}
// 计算需要回溯多少根 K 线
barsBack := targetMinutes / tfMinutes
if barsBack < 1 {
barsBack = 1
}
currentPrice := klines[len(klines)-1].Close
idx := len(klines) - 1 - barsBack
if idx < 0 {
idx = 0
}
oldPrice := klines[idx].Close
if oldPrice > 0 {
return ((currentPrice - oldPrice) / oldPrice) * 100
}
return 0
}
// parseTimeframeToMinutes 将时间周期字符串解析为分钟数
func parseTimeframeToMinutes(tf string) int {
switch tf {
case "1m":
return 1
case "3m":
return 3
case "5m":
return 5
case "15m":
return 15
case "30m":
return 30
case "1h":
return 60
case "2h":
return 120
case "4h":
return 240
case "6h":
return 360
case "8h":
return 480
case "12h":
return 720
case "1d":
return 1440
case "3d":
return 4320
case "1w":
return 10080
default:
return 0
}
}
// calculateEMA 计算EMA
func calculateEMA(klines []Kline, period int) float64 {
if len(klines) < period {
@@ -481,9 +705,54 @@ func Format(data *Data) string {
}
}
// 多时间周期数据(新增)
if len(data.TimeframeData) > 0 {
// 按时间周期排序输出
timeframeOrder := []string{"1m", "3m", "5m", "15m", "30m", "1h", "2h", "4h", "6h", "8h", "12h", "1d", "3d", "1w"}
for _, tf := range timeframeOrder {
if tfData, ok := data.TimeframeData[tf]; ok {
sb.WriteString(fmt.Sprintf("=== %s Timeframe ===\n\n", strings.ToUpper(tf)))
formatTimeframeData(&sb, tfData)
}
}
}
return sb.String()
}
// formatTimeframeData 格式化单个时间周期的数据
func formatTimeframeData(sb *strings.Builder, data *TimeframeSeriesData) {
if len(data.MidPrices) > 0 {
sb.WriteString(fmt.Sprintf("Mid prices: %s\n\n", formatFloatSlice(data.MidPrices)))
}
if len(data.EMA20Values) > 0 {
sb.WriteString(fmt.Sprintf("EMA indicators (20period): %s\n\n", formatFloatSlice(data.EMA20Values)))
}
if len(data.EMA50Values) > 0 {
sb.WriteString(fmt.Sprintf("EMA indicators (50period): %s\n\n", formatFloatSlice(data.EMA50Values)))
}
if len(data.MACDValues) > 0 {
sb.WriteString(fmt.Sprintf("MACD indicators: %s\n\n", formatFloatSlice(data.MACDValues)))
}
if len(data.RSI7Values) > 0 {
sb.WriteString(fmt.Sprintf("RSI indicators (7Period): %s\n\n", formatFloatSlice(data.RSI7Values)))
}
if len(data.RSI14Values) > 0 {
sb.WriteString(fmt.Sprintf("RSI indicators (14Period): %s\n\n", formatFloatSlice(data.RSI14Values)))
}
if len(data.Volume) > 0 {
sb.WriteString(fmt.Sprintf("Volume: %s\n\n", formatFloatSlice(data.Volume)))
}
sb.WriteString(fmt.Sprintf("ATR (14period): %.3f\n\n", data.ATR14))
}
// formatPriceWithDynamicPrecision 根据价格区间动态选择精度
// 这样可以完美支持从超低价 meme coin (< 0.0001) 到 BTC/ETH 的所有币种
func formatPriceWithDynamicPrecision(price float64) string {
+15
View File
@@ -15,6 +15,21 @@ type Data struct {
FundingRate float64
IntradaySeries *IntradayData
LongerTermContext *LongerTermData
// 多时间周期数据(新增)
TimeframeData map[string]*TimeframeSeriesData `json:"timeframe_data,omitempty"`
}
// TimeframeSeriesData 单个时间周期的序列数据
type TimeframeSeriesData struct {
Timeframe string `json:"timeframe"` // 时间周期标识,如 "5m", "15m", "1h"
MidPrices []float64 `json:"mid_prices"` // 价格序列
EMA20Values []float64 `json:"ema20_values"` // EMA20 序列
EMA50Values []float64 `json:"ema50_values"` // EMA50 序列
MACDValues []float64 `json:"macd_values"` // MACD 序列
RSI7Values []float64 `json:"rsi7_values"` // RSI7 序列
RSI14Values []float64 `json:"rsi14_values"` // RSI14 序列
Volume []float64 `json:"volume"` // 成交量序列
ATR14 float64 `json:"atr14"` // ATR14
}
// OIData Open Interest数据