feat: use OHLCV table format for kline data in AI prompts

- Add KlineBar struct with full OHLCV data and timestamp
- Store complete kline data in TimeframeSeriesData.Klines
- Format klines as readable table with Time, Open, High, Low, Close, Volume
- Mark current (latest) bar for clarity
- Use kline count from strategy config instead of hardcoded 10
- Keep MidPrices/Volume for backward compatibility
- Update both market/data.go and decision/strategy_engine.go formatters
This commit is contained in:
tinkle-community
2025-12-08 12:27:27 +08:00
parent 8a5744e0a0
commit 24717d8589
3 changed files with 106 additions and 43 deletions
+28 -11
View File
@@ -702,39 +702,56 @@ func (e *StrategyEngine) formatMarketData(data *market.Data) string {
// formatTimeframeSeriesData formats series data for a single timeframe
func (e *StrategyEngine) formatTimeframeSeriesData(sb *strings.Builder, data *market.TimeframeSeriesData, indicators store.IndicatorConfig) {
if len(data.MidPrices) > 0 {
// Use OHLCV table format if kline data is available
if len(data.Klines) > 0 {
sb.WriteString("Time(UTC) Open High Low Close Volume\n")
for i, k := range data.Klines {
t := time.Unix(k.Time/1000, 0).UTC()
timeStr := t.Format("01-02 15:04")
marker := ""
if i == len(data.Klines)-1 {
marker = " <- current"
}
sb.WriteString(fmt.Sprintf("%-14s %-9.4f %-9.4f %-9.4f %-9.4f %-12.2f%s\n",
timeStr, k.Open, k.High, k.Low, k.Close, k.Volume, marker))
}
sb.WriteString("\n")
} else if len(data.MidPrices) > 0 {
// Fallback to old format for backward compatibility
sb.WriteString(fmt.Sprintf("Mid prices: %s\n\n", formatFloatSlice(data.MidPrices)))
if indicators.EnableVolume && len(data.Volume) > 0 {
sb.WriteString(fmt.Sprintf("Volume: %s\n\n", formatFloatSlice(data.Volume)))
}
}
// Technical indicators (only show if enabled and data available)
if indicators.EnableEMA {
if len(data.EMA20Values) > 0 {
sb.WriteString(fmt.Sprintf("EMA indicators (20-period): %s\n\n", formatFloatSlice(data.EMA20Values)))
sb.WriteString(fmt.Sprintf("EMA20: %s\n", formatFloatSlice(data.EMA20Values)))
}
if len(data.EMA50Values) > 0 {
sb.WriteString(fmt.Sprintf("EMA indicators (50-period): %s\n\n", formatFloatSlice(data.EMA50Values)))
sb.WriteString(fmt.Sprintf("EMA50: %s\n", formatFloatSlice(data.EMA50Values)))
}
}
if indicators.EnableMACD && len(data.MACDValues) > 0 {
sb.WriteString(fmt.Sprintf("MACD indicators: %s\n\n", formatFloatSlice(data.MACDValues)))
sb.WriteString(fmt.Sprintf("MACD: %s\n", formatFloatSlice(data.MACDValues)))
}
if indicators.EnableRSI {
if len(data.RSI7Values) > 0 {
sb.WriteString(fmt.Sprintf("RSI indicators (7-Period): %s\n\n", formatFloatSlice(data.RSI7Values)))
sb.WriteString(fmt.Sprintf("RSI7: %s\n", formatFloatSlice(data.RSI7Values)))
}
if len(data.RSI14Values) > 0 {
sb.WriteString(fmt.Sprintf("RSI indicators (14-Period): %s\n\n", formatFloatSlice(data.RSI14Values)))
sb.WriteString(fmt.Sprintf("RSI14: %s\n", formatFloatSlice(data.RSI14Values)))
}
}
if indicators.EnableVolume && len(data.Volume) > 0 {
sb.WriteString(fmt.Sprintf("Volume: %s\n\n", formatFloatSlice(data.Volume)))
if indicators.EnableATR && data.ATR14 > 0 {
sb.WriteString(fmt.Sprintf("ATR14: %.4f\n", data.ATR14))
}
if indicators.EnableATR {
sb.WriteString(fmt.Sprintf("ATR (14-period): %.3f\n\n", data.ATR14))
}
sb.WriteString("\n")
}
// formatFloatSlice formats float slice
+73 -38
View File
@@ -162,8 +162,8 @@ func GetWithTimeframes(symbol string, timeframes []string, primaryTimeframe stri
primaryKlines = klines
}
// Calculate series data for this timeframe
seriesData := calculateTimeframeSeries(klines, tf)
// Calculate series data for this timeframe (use count from config)
seriesData := calculateTimeframeSeries(klines, tf, count)
timeframeData[tf] = seriesData
}
@@ -212,25 +212,41 @@ func GetWithTimeframes(symbol string, timeframes []string, primaryTimeframe stri
}
// calculateTimeframeSeries calculates series data for a single timeframe
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),
func calculateTimeframeSeries(klines []Kline, timeframe string, count int) *TimeframeSeriesData {
if count <= 0 {
count = 10 // default
}
// Get latest 10 data points
start := len(klines) - 10
data := &TimeframeSeriesData{
Timeframe: timeframe,
Klines: make([]KlineBar, 0, count),
MidPrices: make([]float64, 0, count),
EMA20Values: make([]float64, 0, count),
EMA50Values: make([]float64, 0, count),
MACDValues: make([]float64, 0, count),
RSI7Values: make([]float64, 0, count),
RSI14Values: make([]float64, 0, count),
Volume: make([]float64, 0, count),
}
// Get latest N data points based on count from config
start := len(klines) - count
if start < 0 {
start = 0
}
for i := start; i < len(klines); i++ {
// Store full OHLCV kline data
data.Klines = append(data.Klines, KlineBar{
Time: klines[i].OpenTime,
Open: klines[i].Open,
High: klines[i].High,
Low: klines[i].Low,
Close: klines[i].Close,
Volume: klines[i].Volume,
})
// Keep MidPrices and Volume for backward compatibility
data.MidPrices = append(data.MidPrices, klines[i].Close)
data.Volume = append(data.Volume, klines[i].Volume)
@@ -722,35 +738,54 @@ func Format(data *Data) string {
// formatTimeframeData formats data for a single timeframe
func formatTimeframeData(sb *strings.Builder, data *TimeframeSeriesData) {
if len(data.MidPrices) > 0 {
// Use OHLCV table format if kline data is available
if len(data.Klines) > 0 {
sb.WriteString("Time(UTC) Open High Low Close Volume\n")
for i, k := range data.Klines {
t := time.Unix(k.Time/1000, 0).UTC()
timeStr := t.Format("01-02 15:04")
marker := ""
if i == len(data.Klines)-1 {
marker = " <- current"
}
sb.WriteString(fmt.Sprintf("%-14s %-9.4f %-9.4f %-9.4f %-9.4f %-12.2f%s\n",
timeStr, k.Open, k.High, k.Low, k.Close, k.Volume, marker))
}
sb.WriteString("\n")
} else if len(data.MidPrices) > 0 {
// Fallback to old format for backward compatibility
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))
// Technical indicators
if len(data.EMA20Values) > 0 {
sb.WriteString(fmt.Sprintf("EMA20: %s\n", formatFloatSlice(data.EMA20Values)))
}
if len(data.EMA50Values) > 0 {
sb.WriteString(fmt.Sprintf("EMA50: %s\n", formatFloatSlice(data.EMA50Values)))
}
if len(data.MACDValues) > 0 {
sb.WriteString(fmt.Sprintf("MACD: %s\n", formatFloatSlice(data.MACDValues)))
}
if len(data.RSI7Values) > 0 {
sb.WriteString(fmt.Sprintf("RSI7: %s\n", formatFloatSlice(data.RSI7Values)))
}
if len(data.RSI14Values) > 0 {
sb.WriteString(fmt.Sprintf("RSI14: %s\n", formatFloatSlice(data.RSI14Values)))
}
if data.ATR14 > 0 {
sb.WriteString(fmt.Sprintf("ATR14: %.4f\n", data.ATR14))
}
sb.WriteString("\n")
}
// formatPriceWithDynamicPrecision dynamically selects precision based on price range
+13 -2
View File
@@ -19,16 +19,27 @@ type Data struct {
TimeframeData map[string]*TimeframeSeriesData `json:"timeframe_data,omitempty"`
}
// KlineBar single kline bar with OHLCV data
type KlineBar struct {
Time int64 `json:"time"` // Unix timestamp in milliseconds
Open float64 `json:"open"` // Open price
High float64 `json:"high"` // High price
Low float64 `json:"low"` // Low price
Close float64 `json:"close"` // Close price
Volume float64 `json:"volume"` // Volume
}
// TimeframeSeriesData series data for a single timeframe
type TimeframeSeriesData struct {
Timeframe string `json:"timeframe"` // Timeframe identifier, e.g. "5m", "15m", "1h"
MidPrices []float64 `json:"mid_prices"` // Price series
Klines []KlineBar `json:"klines"` // Full OHLCV kline data
MidPrices []float64 `json:"mid_prices"` // Price series (deprecated, kept for compatibility)
EMA20Values []float64 `json:"ema20_values"` // EMA20 series
EMA50Values []float64 `json:"ema50_values"` // EMA50 series
MACDValues []float64 `json:"macd_values"` // MACD series
RSI7Values []float64 `json:"rsi7_values"` // RSI7 series
RSI14Values []float64 `json:"rsi14_values"` // RSI14 series
Volume []float64 `json:"volume"` // Volume series
Volume []float64 `json:"volume"` // Volume series (deprecated, use Klines)
ATR14 float64 `json:"atr14"` // ATR14
}