mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-02 02:21:19 +08:00
- Redesign dashboard into a cream-paper + vermilion IBM Plex Mono terminal (live L2 order book, cost/liq map, WS K-line, signal matrix, orchestration topology, risk radar, execution log, current positions, equity curve) - Convert all user-facing UI and backend strings/prompts from Chinese to English (multi-language retained, default English) - Add /api/statistics/full endpoint + full-stats frontend wiring - Fix Autopilot launch: reuse the existing trader instead of creating duplicates (eliminates repeat ~35s create cost and stale-trader 404s); launch sends 5m scan interval - Fix unreadable toasts: cream theme with high-contrast text + per-type accent - Silence background dashboard polls (getTraderConfig) to stop error-toast spam
619 lines
23 KiB
Go
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(`# You are a Professional Grid Trading AI
|
|
|
|
## Role Definition
|
|
You are an experienced grid trading expert responsible for managing the grid trading strategy for %s. Your tasks are:
|
|
1. Determine the current market state (ranging/trending/high volatility)
|
|
2. Decide whether to adjust the grid or pause trading
|
|
3. Manage orders at each grid level
|
|
|
|
## Grid Configuration
|
|
- Trading Pair: %s
|
|
- Grid Levels: %d
|
|
- Total Investment: %.2f USDT
|
|
- Leverage: %dx
|
|
- Price Distribution: %s
|
|
|
|
## Decision Rules
|
|
|
|
### Market State Judgment
|
|
- **Ranging Market** (suitable for grid): Bollinger band width < 3%%, EMA20/50 distance < 1%%, price near the Bollinger middle band
|
|
- **Trending Market** (pause grid): Bollinger band width > 4%%, EMA20/50 distance > 2%%, price continuously breaking out of the Bollinger bands
|
|
- **High Volatility Market** (caution): abnormally expanding ATR, sharp price swings
|
|
|
|
### Available Actions
|
|
- place_buy_limit: place a buy limit order at the specified price
|
|
- place_sell_limit: place a sell limit order at the specified price
|
|
- cancel_order: cancel a specified order
|
|
- cancel_all_orders: cancel all orders
|
|
- pause_grid: pause grid trading (in a trending market)
|
|
- resume_grid: resume grid trading (in a ranging market)
|
|
- adjust_grid: adjust the grid boundaries
|
|
- hold: keep the current state, take no action
|
|
|
|
## Output Format
|
|
Output a 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 canceling orders)
|
|
- confidence: confidence 0-100
|
|
- reasoning: decision reasoning
|
|
|
|
Example:
|
|
[
|
|
{"symbol": "BTCUSDT", "action": "place_buy_limit", "price": 94000, "quantity": 0.01, "level_index": 2, "confidence": 85, "reasoning": "Level 2 price is near, place a buy order"},
|
|
{"symbol": "BTCUSDT", "action": "hold", "confidence": 90, "reasoning": "Market is ranging, keep the current grid"}
|
|
]
|
|
`, 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("## 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 Indicator (Donchian Channel)\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 broke out of the long-term box!\n")
|
|
} else if price > ctx.BoxData.MidUpper || price < ctx.BoxData.MidLower {
|
|
sb.WriteString("⚠️ Warning: price is approaching the 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 position)\n", ctx.CurrentPosition))
|
|
sb.WriteString(fmt.Sprintf("- Unrealized PnL: $%.2f\n", ctx.UnrealizedPnL))
|
|
sb.WriteString("\n")
|
|
|
|
// Grid state section
|
|
sb.WriteString("## Grid State\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 != "" {
|
|
directionDescZh := 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 := directionDescZh[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 Level Details\n")
|
|
sb.WriteString("| Level | Price | State | Side | Order Qty | Position Qty | 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 Statistics\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("- Today's PnL: $%.2f\n", ctx.DailyPnL))
|
|
sb.WriteString("\n")
|
|
|
|
sb.WriteString("## Analyze the data above and make grid trading decisions\n")
|
|
sb.WriteString("Output the decision list as a JSON array.\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
|
|
}
|