Files
tinkle-community 773857351f feat(grid): auto-adjust grid direction based on box breakouts
Add GridDirection type with 5 states:
- neutral (50% buy + 50% sell)
- long/short (100% one direction)
- long_bias/short_bias (70%/30% configurable)

Direction adjustment logic:
- Short box breakout → bias direction (long_bias/short_bias)
- Mid box breakout → full direction (long/short)
- Long box breakout → emergency handling (unchanged)
- Recovery: long → long_bias → neutral ← short_bias ← short

Config options:
- EnableDirectionAdjust (default: false)
- DirectionBiasRatio (default: 0.7)

Includes unit tests for all direction-related functions.
2026-02-04 11:25:47 +08:00

619 lines
23 KiB
Go

package kernel
import (
"encoding/json"
"fmt"
"nofx/logger"
"nofx/market"
"nofx/mcp"
"nofx/store"
"strings"
"time"
)
// ============================================================================
// Grid Trading Context and Types
// ============================================================================
// GridLevelInfo represents a single grid level's current state
type GridLevelInfo struct {
Index int `json:"index"` // Level index (0 = lowest)
Price float64 `json:"price"` // Target price for this level
State string `json:"state"` // "empty", "pending", "filled"
Side string `json:"side"` // "buy" or "sell"
OrderID string `json:"order_id"` // Current order ID (if pending)
OrderQuantity float64 `json:"order_quantity"` // Order quantity
PositionSize float64 `json:"position_size"` // Position size (if filled)
PositionEntry float64 `json:"position_entry"` // Entry price (if filled)
AllocatedUSD float64 `json:"allocated_usd"` // USD allocated to this level
UnrealizedPnL float64 `json:"unrealized_pnl"` // Unrealized P&L (if filled)
}
// GridContext contains all information needed for AI grid decision making
type GridContext struct {
// Basic info
Symbol string `json:"symbol"`
CurrentTime string `json:"current_time"`
CurrentPrice float64 `json:"current_price"`
// Grid configuration
GridCount int `json:"grid_count"`
TotalInvestment float64 `json:"total_investment"`
Leverage int `json:"leverage"`
UpperPrice float64 `json:"upper_price"`
LowerPrice float64 `json:"lower_price"`
GridSpacing float64 `json:"grid_spacing"`
Distribution string `json:"distribution"`
// Grid state
Levels []GridLevelInfo `json:"levels"`
ActiveOrderCount int `json:"active_order_count"`
FilledLevelCount int `json:"filled_level_count"`
IsPaused bool `json:"is_paused"`
// Market data
ATR14 float64 `json:"atr14"`
BollingerUpper float64 `json:"bollinger_upper"`
BollingerMiddle float64 `json:"bollinger_middle"`
BollingerLower float64 `json:"bollinger_lower"`
BollingerWidth float64 `json:"bollinger_width"` // Percentage
EMA20 float64 `json:"ema20"`
EMA50 float64 `json:"ema50"`
EMADistance float64 `json:"ema_distance"` // Percentage
RSI14 float64 `json:"rsi14"`
MACD float64 `json:"macd"`
MACDSignal float64 `json:"macd_signal"`
MACDHistogram float64 `json:"macd_histogram"`
FundingRate float64 `json:"funding_rate"`
Volume24h float64 `json:"volume_24h"`
PriceChange1h float64 `json:"price_change_1h"`
PriceChange4h float64 `json:"price_change_4h"`
// Account info
TotalEquity float64 `json:"total_equity"`
AvailableBalance float64 `json:"available_balance"`
CurrentPosition float64 `json:"current_position"` // Net position size
UnrealizedPnL float64 `json:"unrealized_pnl"`
// Performance
TotalProfit float64 `json:"total_profit"`
TotalTrades int `json:"total_trades"`
WinningTrades int `json:"winning_trades"`
MaxDrawdown float64 `json:"max_drawdown"`
DailyPnL float64 `json:"daily_pnl"`
// Box indicators (Donchian Channels)
BoxData *market.BoxData `json:"box_data,omitempty"`
// Grid direction (neutral, long, short, long_bias, short_bias)
CurrentDirection string `json:"current_direction,omitempty"`
}
// ============================================================================
// Grid Prompt Building
// ============================================================================
// BuildGridSystemPrompt builds the system prompt for grid trading AI
func BuildGridSystemPrompt(config *store.GridStrategyConfig, lang string) string {
if lang == "zh" {
return buildGridSystemPromptZh(config)
}
return buildGridSystemPromptEn(config)
}
func buildGridSystemPromptZh(config *store.GridStrategyConfig) string {
return fmt.Sprintf(`# 你是一个专业的网格交易AI
## 角色定义
你是一个经验丰富的网格交易专家,负责管理 %s 的网格交易策略。你的任务是:
1. 判断当前市场状态(震荡/趋势/高波动)
2. 决定是否需要调整网格或暂停交易
3. 管理每个网格层级的订单
## 网格配置
- 交易对: %s
- 网格层数: %d
- 总投资: %.2f USDT
- 杠杆: %dx
- 价格分布: %s
## 决策规则
### 市场状态判断
- **震荡市场** (适合网格): 布林带宽度 < 3%%, EMA20/50 距离 < 1%%, 价格在布林带中轨附近
- **趋势市场** (暂停网格): 布林带宽度 > 4%%, EMA20/50 距离 > 2%%, 价格持续突破布林带
- **高波动市场** (谨慎): ATR异常放大, 价格剧烈波动
### 可执行的操作
- place_buy_limit: 在指定价格下买入限价单
- place_sell_limit: 在指定价格下卖出限价单
- cancel_order: 取消指定订单
- cancel_all_orders: 取消所有订单
- pause_grid: 暂停网格交易(趋势市场时)
- resume_grid: 恢复网格交易(震荡市场时)
- adjust_grid: 调整网格边界
- hold: 保持当前状态不操作
## 输出格式
输出JSON数组,每个决策包含:
- symbol: 交易对
- action: 操作类型
- price: 价格(限价单用)
- quantity: 数量
- level_index: 网格层级索引
- order_id: 订单ID(取消订单用)
- confidence: 置信度 0-100
- reasoning: 决策理由
示例:
[
{"symbol": "BTCUSDT", "action": "place_buy_limit", "price": 94000, "quantity": 0.01, "level_index": 2, "confidence": 85, "reasoning": "第2层价格接近,下买单"},
{"symbol": "BTCUSDT", "action": "hold", "confidence": 90, "reasoning": "市场震荡,保持当前网格"}
]
`, config.Symbol, config.Symbol, config.GridCount, config.TotalInvestment, config.Leverage, config.Distribution)
}
func buildGridSystemPromptEn(config *store.GridStrategyConfig) string {
return fmt.Sprintf(`# You are a Professional Grid Trading AI
## Role Definition
You are an experienced grid trading expert managing a grid strategy for %s. Your tasks are:
1. Assess current market regime (ranging/trending/volatile)
2. Decide whether to adjust grid or pause trading
3. Manage orders at each grid level
## Grid Configuration
- Symbol: %s
- Grid Levels: %d
- Total Investment: %.2f USDT
- Leverage: %dx
- Distribution: %s
## Decision Rules
### Market Regime Assessment
- **Ranging Market** (ideal for grid): Bollinger width < 3%%, EMA20/50 distance < 1%%, price near middle band
- **Trending Market** (pause grid): Bollinger width > 4%%, EMA20/50 distance > 2%%, price breaking bands
- **High Volatility** (caution): ATR spike, erratic price movement
### Available Actions
- place_buy_limit: Place buy limit order at specified price
- place_sell_limit: Place sell limit order at specified price
- cancel_order: Cancel specific order
- cancel_all_orders: Cancel all orders
- pause_grid: Pause grid trading (in trending market)
- resume_grid: Resume grid trading (in ranging market)
- adjust_grid: Adjust grid boundaries
- hold: Maintain current state
## Output Format
Output JSON array, each decision contains:
- symbol: Trading pair
- action: Action type
- price: Price (for limit orders)
- quantity: Quantity
- level_index: Grid level index
- order_id: Order ID (for cancel)
- confidence: Confidence 0-100
- reasoning: Decision reason
Example:
[
{"symbol": "BTCUSDT", "action": "place_buy_limit", "price": 94000, "quantity": 0.01, "level_index": 2, "confidence": 85, "reasoning": "Level 2 price approaching, place buy order"},
{"symbol": "BTCUSDT", "action": "hold", "confidence": 90, "reasoning": "Market ranging, maintain current grid"}
]
`, config.Symbol, config.Symbol, config.GridCount, config.TotalInvestment, config.Leverage, config.Distribution)
}
// BuildGridUserPrompt builds the user prompt with current grid context
func BuildGridUserPrompt(ctx *GridContext, lang string) string {
if lang == "zh" {
return buildGridUserPromptZh(ctx)
}
return buildGridUserPromptEn(ctx)
}
func buildGridUserPromptZh(ctx *GridContext) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("## 当前时间: %s\n\n", ctx.CurrentTime))
// Market data section
sb.WriteString("## 市场数据\n")
sb.WriteString(fmt.Sprintf("- 当前价格: $%.2f\n", ctx.CurrentPrice))
sb.WriteString(fmt.Sprintf("- 1小时涨跌: %.2f%%\n", ctx.PriceChange1h))
sb.WriteString(fmt.Sprintf("- 4小时涨跌: %.2f%%\n", ctx.PriceChange4h))
sb.WriteString(fmt.Sprintf("- ATR14: $%.2f (%.2f%%)\n", ctx.ATR14, ctx.ATR14/ctx.CurrentPrice*100))
sb.WriteString(fmt.Sprintf("- 布林带: 上轨 $%.2f, 中轨 $%.2f, 下轨 $%.2f\n", ctx.BollingerUpper, ctx.BollingerMiddle, ctx.BollingerLower))
sb.WriteString(fmt.Sprintf("- 布林带宽度: %.2f%%\n", ctx.BollingerWidth))
sb.WriteString(fmt.Sprintf("- EMA20: $%.2f, EMA50: $%.2f, 距离: %.2f%%\n", ctx.EMA20, ctx.EMA50, ctx.EMADistance))
sb.WriteString(fmt.Sprintf("- RSI14: %.1f\n", ctx.RSI14))
sb.WriteString(fmt.Sprintf("- MACD: %.4f, Signal: %.4f, Histogram: %.4f\n", ctx.MACD, ctx.MACDSignal, ctx.MACDHistogram))
sb.WriteString(fmt.Sprintf("- 资金费率: %.4f%%\n", ctx.FundingRate*100))
sb.WriteString("\n")
// Box Indicator Section
if ctx.BoxData != nil {
sb.WriteString("## 箱体指标 (唐奇安通道)\n\n")
sb.WriteString("| 箱体级别 | 上轨 | 下轨 | 宽度 |\n")
sb.WriteString("|----------|------|------|------|\n")
shortWidth := 0.0
midWidth := 0.0
longWidth := 0.0
if ctx.BoxData.CurrentPrice > 0 {
shortWidth = (ctx.BoxData.ShortUpper - ctx.BoxData.ShortLower) / ctx.BoxData.CurrentPrice * 100
midWidth = (ctx.BoxData.MidUpper - ctx.BoxData.MidLower) / ctx.BoxData.CurrentPrice * 100
longWidth = (ctx.BoxData.LongUpper - ctx.BoxData.LongLower) / ctx.BoxData.CurrentPrice * 100
}
sb.WriteString(fmt.Sprintf("| 短期 (3天) | %.2f | %.2f | %.2f%% |\n",
ctx.BoxData.ShortUpper, ctx.BoxData.ShortLower, shortWidth))
sb.WriteString(fmt.Sprintf("| 中期 (10天) | %.2f | %.2f | %.2f%% |\n",
ctx.BoxData.MidUpper, ctx.BoxData.MidLower, midWidth))
sb.WriteString(fmt.Sprintf("| 长期 (21天) | %.2f | %.2f | %.2f%% |\n",
ctx.BoxData.LongUpper, ctx.BoxData.LongLower, longWidth))
sb.WriteString(fmt.Sprintf("\n当前价格: %.2f\n", ctx.BoxData.CurrentPrice))
// Check position relative to boxes
price := ctx.BoxData.CurrentPrice
if price > ctx.BoxData.LongUpper || price < ctx.BoxData.LongLower {
sb.WriteString("⚠️ 突破: 价格突破长期箱体!\n")
} else if price > ctx.BoxData.MidUpper || price < ctx.BoxData.MidLower {
sb.WriteString("⚠️ 警告: 价格接近长期箱体边界\n")
}
sb.WriteString("\n")
}
// Account section
sb.WriteString("## 账户状态\n")
sb.WriteString(fmt.Sprintf("- 总权益: $%.2f\n", ctx.TotalEquity))
sb.WriteString(fmt.Sprintf("- 可用余额: $%.2f\n", ctx.AvailableBalance))
sb.WriteString(fmt.Sprintf("- 当前持仓: %.4f (净头寸)\n", ctx.CurrentPosition))
sb.WriteString(fmt.Sprintf("- 未实现盈亏: $%.2f\n", ctx.UnrealizedPnL))
sb.WriteString("\n")
// Grid state section
sb.WriteString("## 网格状态\n")
sb.WriteString(fmt.Sprintf("- 网格范围: $%.2f - $%.2f\n", ctx.LowerPrice, ctx.UpperPrice))
sb.WriteString(fmt.Sprintf("- 网格间距: $%.2f\n", ctx.GridSpacing))
sb.WriteString(fmt.Sprintf("- 活跃订单数: %d\n", ctx.ActiveOrderCount))
sb.WriteString(fmt.Sprintf("- 已成交层数: %d\n", ctx.FilledLevelCount))
sb.WriteString(fmt.Sprintf("- 网格已暂停: %v\n", ctx.IsPaused))
if ctx.CurrentDirection != "" {
directionDescZh := map[string]string{
"neutral": "中性 (50%买+50%卖)",
"long": "做多 (100%买)",
"short": "做空 (100%卖)",
"long_bias": "偏多 (70%买+30%卖)",
"short_bias": "偏空 (30%买+70%卖)",
}
desc := directionDescZh[ctx.CurrentDirection]
if desc == "" {
desc = ctx.CurrentDirection
}
sb.WriteString(fmt.Sprintf("- 网格方向: %s\n", desc))
}
sb.WriteString("\n")
// Grid levels detail
sb.WriteString("## 网格层级详情\n")
sb.WriteString("| 层级 | 价格 | 状态 | 方向 | 订单数量 | 持仓数量 | 未实现盈亏 |\n")
sb.WriteString("|------|------|------|------|----------|----------|------------|\n")
for _, level := range ctx.Levels {
sb.WriteString(fmt.Sprintf("| %d | $%.2f | %s | %s | %.4f | %.4f | $%.2f |\n",
level.Index, level.Price, level.State, level.Side,
level.OrderQuantity, level.PositionSize, level.UnrealizedPnL))
}
sb.WriteString("\n")
// Performance section
sb.WriteString("## 绩效统计\n")
sb.WriteString(fmt.Sprintf("- 总利润: $%.2f\n", ctx.TotalProfit))
sb.WriteString(fmt.Sprintf("- 总交易次数: %d\n", ctx.TotalTrades))
sb.WriteString(fmt.Sprintf("- 胜率: %.1f%%\n", float64(ctx.WinningTrades)/float64(max(ctx.TotalTrades, 1))*100))
sb.WriteString(fmt.Sprintf("- 最大回撤: %.2f%%\n", ctx.MaxDrawdown))
sb.WriteString(fmt.Sprintf("- 今日盈亏: $%.2f\n", ctx.DailyPnL))
sb.WriteString("\n")
sb.WriteString("## 请分析以上数据,做出网格交易决策\n")
sb.WriteString("输出JSON数组格式的决策列表。\n")
return sb.String()
}
func buildGridUserPromptEn(ctx *GridContext) string {
var sb strings.Builder
sb.WriteString(fmt.Sprintf("## Current Time: %s\n\n", ctx.CurrentTime))
// Market data section
sb.WriteString("## Market Data\n")
sb.WriteString(fmt.Sprintf("- Current Price: $%.2f\n", ctx.CurrentPrice))
sb.WriteString(fmt.Sprintf("- 1h Change: %.2f%%\n", ctx.PriceChange1h))
sb.WriteString(fmt.Sprintf("- 4h Change: %.2f%%\n", ctx.PriceChange4h))
sb.WriteString(fmt.Sprintf("- ATR14: $%.2f (%.2f%%)\n", ctx.ATR14, ctx.ATR14/ctx.CurrentPrice*100))
sb.WriteString(fmt.Sprintf("- Bollinger Bands: Upper $%.2f, Middle $%.2f, Lower $%.2f\n", ctx.BollingerUpper, ctx.BollingerMiddle, ctx.BollingerLower))
sb.WriteString(fmt.Sprintf("- Bollinger Width: %.2f%%\n", ctx.BollingerWidth))
sb.WriteString(fmt.Sprintf("- EMA20: $%.2f, EMA50: $%.2f, Distance: %.2f%%\n", ctx.EMA20, ctx.EMA50, ctx.EMADistance))
sb.WriteString(fmt.Sprintf("- RSI14: %.1f\n", ctx.RSI14))
sb.WriteString(fmt.Sprintf("- MACD: %.4f, Signal: %.4f, Histogram: %.4f\n", ctx.MACD, ctx.MACDSignal, ctx.MACDHistogram))
sb.WriteString(fmt.Sprintf("- Funding Rate: %.4f%%\n", ctx.FundingRate*100))
sb.WriteString("\n")
// Box Indicator Section
if ctx.BoxData != nil {
sb.WriteString("## Box Indicators (Donchian Channels)\n\n")
sb.WriteString("| Box Level | Upper | Lower | Width |\n")
sb.WriteString("|-----------|-------|-------|-------|\n")
shortWidth := 0.0
midWidth := 0.0
longWidth := 0.0
if ctx.BoxData.CurrentPrice > 0 {
shortWidth = (ctx.BoxData.ShortUpper - ctx.BoxData.ShortLower) / ctx.BoxData.CurrentPrice * 100
midWidth = (ctx.BoxData.MidUpper - ctx.BoxData.MidLower) / ctx.BoxData.CurrentPrice * 100
longWidth = (ctx.BoxData.LongUpper - ctx.BoxData.LongLower) / ctx.BoxData.CurrentPrice * 100
}
sb.WriteString(fmt.Sprintf("| Short (3d) | %.2f | %.2f | %.2f%% |\n",
ctx.BoxData.ShortUpper, ctx.BoxData.ShortLower, shortWidth))
sb.WriteString(fmt.Sprintf("| Mid (10d) | %.2f | %.2f | %.2f%% |\n",
ctx.BoxData.MidUpper, ctx.BoxData.MidLower, midWidth))
sb.WriteString(fmt.Sprintf("| Long (21d) | %.2f | %.2f | %.2f%% |\n",
ctx.BoxData.LongUpper, ctx.BoxData.LongLower, longWidth))
sb.WriteString(fmt.Sprintf("\nCurrent Price: %.2f\n", ctx.BoxData.CurrentPrice))
// Check position relative to boxes
price := ctx.BoxData.CurrentPrice
if price > ctx.BoxData.LongUpper || price < ctx.BoxData.LongLower {
sb.WriteString("⚠️ BREAKOUT: Price outside long-term box!\n")
} else if price > ctx.BoxData.MidUpper || price < ctx.BoxData.MidLower {
sb.WriteString("⚠️ WARNING: Price approaching long-term box boundary\n")
}
sb.WriteString("\n")
}
// Account section
sb.WriteString("## Account Status\n")
sb.WriteString(fmt.Sprintf("- Total Equity: $%.2f\n", ctx.TotalEquity))
sb.WriteString(fmt.Sprintf("- Available Balance: $%.2f\n", ctx.AvailableBalance))
sb.WriteString(fmt.Sprintf("- Current Position: %.4f (net)\n", ctx.CurrentPosition))
sb.WriteString(fmt.Sprintf("- Unrealized PnL: $%.2f\n", ctx.UnrealizedPnL))
sb.WriteString("\n")
// Grid state section
sb.WriteString("## Grid Status\n")
sb.WriteString(fmt.Sprintf("- Grid Range: $%.2f - $%.2f\n", ctx.LowerPrice, ctx.UpperPrice))
sb.WriteString(fmt.Sprintf("- Grid Spacing: $%.2f\n", ctx.GridSpacing))
sb.WriteString(fmt.Sprintf("- Active Orders: %d\n", ctx.ActiveOrderCount))
sb.WriteString(fmt.Sprintf("- Filled Levels: %d\n", ctx.FilledLevelCount))
sb.WriteString(fmt.Sprintf("- Grid Paused: %v\n", ctx.IsPaused))
if ctx.CurrentDirection != "" {
directionDescEn := map[string]string{
"neutral": "Neutral (50% buy + 50% sell)",
"long": "Long (100% buy)",
"short": "Short (100% sell)",
"long_bias": "Long Bias (70% buy + 30% sell)",
"short_bias": "Short Bias (30% buy + 70% sell)",
}
desc := directionDescEn[ctx.CurrentDirection]
if desc == "" {
desc = ctx.CurrentDirection
}
sb.WriteString(fmt.Sprintf("- Grid Direction: %s\n", desc))
}
sb.WriteString("\n")
// Grid levels detail
sb.WriteString("## Grid Levels Detail\n")
sb.WriteString("| Level | Price | State | Side | Order Qty | Position | Unrealized PnL |\n")
sb.WriteString("|-------|-------|-------|------|-----------|----------|----------------|\n")
for _, level := range ctx.Levels {
sb.WriteString(fmt.Sprintf("| %d | $%.2f | %s | %s | %.4f | %.4f | $%.2f |\n",
level.Index, level.Price, level.State, level.Side,
level.OrderQuantity, level.PositionSize, level.UnrealizedPnL))
}
sb.WriteString("\n")
// Performance section
sb.WriteString("## Performance Stats\n")
sb.WriteString(fmt.Sprintf("- Total Profit: $%.2f\n", ctx.TotalProfit))
sb.WriteString(fmt.Sprintf("- Total Trades: %d\n", ctx.TotalTrades))
sb.WriteString(fmt.Sprintf("- Win Rate: %.1f%%\n", float64(ctx.WinningTrades)/float64(max(ctx.TotalTrades, 1))*100))
sb.WriteString(fmt.Sprintf("- Max Drawdown: %.2f%%\n", ctx.MaxDrawdown))
sb.WriteString(fmt.Sprintf("- Daily PnL: $%.2f\n", ctx.DailyPnL))
sb.WriteString("\n")
sb.WriteString("## Please analyze the data above and make grid trading decisions\n")
sb.WriteString("Output a JSON array of decisions.\n")
return sb.String()
}
// ============================================================================
// Grid Decision Functions
// ============================================================================
// GetGridDecisions gets AI decisions for grid trading
func GetGridDecisions(ctx *GridContext, mcpClient mcp.AIClient, config *store.GridStrategyConfig, lang string) (*FullDecision, error) {
startTime := time.Now()
// Build prompts
systemPrompt := BuildGridSystemPrompt(config, lang)
userPrompt := BuildGridUserPrompt(ctx, lang)
logger.Infof("🤖 [Grid] Calling AI for grid decisions...")
// Call AI
response, err := mcpClient.CallWithMessages(systemPrompt, userPrompt)
if err != nil {
return nil, fmt.Errorf("AI call failed: %w", err)
}
// Parse decisions from response
decisions, err := parseGridDecisions(response, ctx.Symbol)
if err != nil {
logger.Warnf("Failed to parse grid decisions: %v", err)
// Return hold decision as fallback
decisions = []Decision{{
Symbol: ctx.Symbol,
Action: "hold",
Confidence: 50,
Reasoning: "Failed to parse AI response, holding current state",
}}
}
duration := time.Since(startTime).Milliseconds()
logger.Infof("⏱️ [Grid] AI call duration: %d ms, decisions: %d", duration, len(decisions))
// Extract chain of thought from response
cotTrace := extractCoTTrace(response)
return &FullDecision{
SystemPrompt: systemPrompt,
UserPrompt: userPrompt,
CoTTrace: cotTrace,
Decisions: decisions,
RawResponse: response,
AIRequestDurationMs: duration,
Timestamp: time.Now(),
}, nil
}
// parseGridDecisions parses AI response into grid decisions
func parseGridDecisions(response string, symbol string) ([]Decision, error) {
// Try to find JSON array in response
jsonStr := extractJSONArray(response)
if jsonStr == "" {
return nil, fmt.Errorf("no JSON array found in response")
}
var decisions []Decision
if err := json.Unmarshal([]byte(jsonStr), &decisions); err != nil {
return nil, fmt.Errorf("failed to parse JSON: %w", err)
}
// Validate and set default symbol
for i := range decisions {
if decisions[i].Symbol == "" {
decisions[i].Symbol = symbol
}
// Validate action
if !isValidGridAction(decisions[i].Action) {
logger.Warnf("Invalid grid action: %s", decisions[i].Action)
}
}
return decisions, nil
}
// extractJSONArray extracts JSON array from AI response
func extractJSONArray(response string) string {
// Try to find ```json code block first
matches := reJSONFence.FindStringSubmatch(response)
if len(matches) > 1 {
return matches[1]
}
// Try to find raw JSON array
matches = reJSONArray.FindStringSubmatch(response)
if len(matches) > 0 {
return matches[0]
}
return ""
}
// isValidGridAction checks if action is a valid grid action
func isValidGridAction(action string) bool {
validActions := map[string]bool{
"place_buy_limit": true,
"place_sell_limit": true,
"cancel_order": true,
"cancel_all_orders": true,
"pause_grid": true,
"resume_grid": true,
"adjust_grid": true,
"hold": true,
// Also support standard actions for compatibility
"open_long": true,
"open_short": true,
"close_long": true,
"close_short": true,
}
return validActions[action]
}
// ============================================================================
// Grid Context Builder Helpers
// ============================================================================
// BuildGridContextFromMarketData builds grid context from market data
func BuildGridContextFromMarketData(mktData *market.Data, config *store.GridStrategyConfig) *GridContext {
ctx := &GridContext{
Symbol: config.Symbol,
CurrentTime: time.Now().Format("2006-01-02 15:04:05"),
CurrentPrice: mktData.CurrentPrice,
// Grid config
GridCount: config.GridCount,
TotalInvestment: config.TotalInvestment,
Leverage: config.Leverage,
Distribution: config.Distribution,
// Market data
PriceChange1h: mktData.PriceChange1h,
PriceChange4h: mktData.PriceChange4h,
FundingRate: mktData.FundingRate,
}
// Extract indicators from timeframe data
if mktData.TimeframeData != nil {
if tf5m, ok := mktData.TimeframeData["5m"]; ok {
if len(tf5m.BOLLUpper) > 0 {
ctx.BollingerUpper = tf5m.BOLLUpper[len(tf5m.BOLLUpper)-1]
ctx.BollingerMiddle = tf5m.BOLLMiddle[len(tf5m.BOLLMiddle)-1]
ctx.BollingerLower = tf5m.BOLLLower[len(tf5m.BOLLLower)-1]
if ctx.BollingerMiddle > 0 {
ctx.BollingerWidth = (ctx.BollingerUpper - ctx.BollingerLower) / ctx.BollingerMiddle * 100
}
}
ctx.ATR14 = tf5m.ATR14
if len(tf5m.RSI14Values) > 0 {
ctx.RSI14 = tf5m.RSI14Values[len(tf5m.RSI14Values)-1]
}
}
}
// Extract longer term context
if mktData.LongerTermContext != nil {
if ctx.ATR14 == 0 {
ctx.ATR14 = mktData.LongerTermContext.ATR14
}
ctx.EMA50 = mktData.LongerTermContext.EMA50
}
ctx.EMA20 = mktData.CurrentEMA20
ctx.MACD = mktData.CurrentMACD
// Calculate EMA distance
if ctx.EMA50 > 0 {
ctx.EMADistance = (ctx.EMA20 - ctx.EMA50) / ctx.EMA50 * 100
}
return ctx
}
// Helper function for max
func max(a, b int) int {
if a > b {
return a
}
return b
}