mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
refactor: rename decision package to kernel
This commit is contained in:
+1773
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,643 @@
|
||||
package kernel
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"nofx/market"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// AI Data Formatter - AI数据格式化器
|
||||
// ============================================================================
|
||||
// 将交易上下文转换为AI友好的格式,确保AI能够100%理解数据
|
||||
// ============================================================================
|
||||
|
||||
// FormatContextForAI 将交易上下文格式化为AI可理解的文本(包含Schema)
|
||||
func FormatContextForAI(ctx *Context, lang Language) string {
|
||||
var sb strings.Builder
|
||||
|
||||
// 1. 添加Schema说明(让AI理解数据格式)
|
||||
sb.WriteString(GetSchemaPrompt(lang))
|
||||
sb.WriteString("\n---\n\n")
|
||||
|
||||
// 2. 当前状态概览
|
||||
sb.WriteString(formatContextData(ctx, lang))
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// FormatContextDataOnly 仅格式化上下文数据,不包含Schema(用于已有Schema的场景)
|
||||
func FormatContextDataOnly(ctx *Context, lang Language) string {
|
||||
return formatContextData(ctx, lang)
|
||||
}
|
||||
|
||||
// formatContextData 格式化核心数据部分
|
||||
func formatContextData(ctx *Context, lang Language) string {
|
||||
var sb strings.Builder
|
||||
|
||||
// 1. 当前状态概览
|
||||
if lang == LangChinese {
|
||||
sb.WriteString(formatHeaderZH(ctx))
|
||||
} else {
|
||||
sb.WriteString(formatHeaderEN(ctx))
|
||||
}
|
||||
|
||||
// 3. 账户信息
|
||||
if lang == LangChinese {
|
||||
sb.WriteString(formatAccountZH(ctx))
|
||||
} else {
|
||||
sb.WriteString(formatAccountEN(ctx))
|
||||
}
|
||||
|
||||
// 4. 历史交易统计
|
||||
if ctx.TradingStats != nil && ctx.TradingStats.TotalTrades > 0 {
|
||||
if lang == LangChinese {
|
||||
sb.WriteString(formatTradingStatsZH(ctx.TradingStats))
|
||||
} else {
|
||||
sb.WriteString(formatTradingStatsEN(ctx.TradingStats))
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 最近交易记录
|
||||
if len(ctx.RecentOrders) > 0 {
|
||||
if lang == LangChinese {
|
||||
sb.WriteString(formatRecentTradesZH(ctx.RecentOrders))
|
||||
} else {
|
||||
sb.WriteString(formatRecentTradesEN(ctx.RecentOrders))
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 当前持仓
|
||||
if len(ctx.Positions) > 0 {
|
||||
if lang == LangChinese {
|
||||
sb.WriteString(formatCurrentPositionsZH(ctx))
|
||||
} else {
|
||||
sb.WriteString(formatCurrentPositionsEN(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
// 6. 候选币种(带市场数据)
|
||||
if len(ctx.CandidateCoins) > 0 {
|
||||
if lang == LangChinese {
|
||||
sb.WriteString(formatCandidateCoinsZH(ctx))
|
||||
} else {
|
||||
sb.WriteString(formatCandidateCoinsEN(ctx))
|
||||
}
|
||||
}
|
||||
|
||||
// 7. OI排名数据(如果有)
|
||||
if ctx.OIRankingData != nil {
|
||||
if lang == LangChinese {
|
||||
sb.WriteString(formatOIRankingZH(ctx.OIRankingData))
|
||||
} else {
|
||||
sb.WriteString(formatOIRankingEN(ctx.OIRankingData))
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// ========== 中文格式化函数 ==========
|
||||
|
||||
// formatHeaderZH 格式化头部信息(中文)
|
||||
func formatHeaderZH(ctx *Context) string {
|
||||
return fmt.Sprintf("# 📊 交易决策请求\n\n时间: %s | 周期: #%d | 运行时长: %d 分钟\n\n",
|
||||
ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes)
|
||||
}
|
||||
|
||||
// formatAccountZH 格式化账户信息(中文)
|
||||
func formatAccountZH(ctx *Context) string {
|
||||
acc := ctx.Account
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString("## 账户状态\n\n")
|
||||
sb.WriteString(fmt.Sprintf("总权益: %.2f USDT | ", acc.TotalEquity))
|
||||
sb.WriteString(fmt.Sprintf("可用余额: %.2f USDT (%.1f%%) | ", acc.AvailableBalance, (acc.AvailableBalance/acc.TotalEquity)*100))
|
||||
sb.WriteString(fmt.Sprintf("总盈亏: %+.2f%% | ", acc.TotalPnLPct))
|
||||
sb.WriteString(fmt.Sprintf("保证金使用率: %.1f%% | ", acc.MarginUsedPct))
|
||||
sb.WriteString(fmt.Sprintf("持仓数: %d\n\n", acc.PositionCount))
|
||||
|
||||
// 添加风险提示
|
||||
if acc.MarginUsedPct > 70 {
|
||||
sb.WriteString("⚠️ **风险警告**: 保证金使用率 > 70%,处于高风险状态!\n\n")
|
||||
} else if acc.MarginUsedPct > 50 {
|
||||
sb.WriteString("⚠️ **风险提示**: 保证金使用率 > 50%,建议谨慎开仓\n\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatTradingStatsZH 格式化历史交易统计(中文)
|
||||
func formatTradingStatsZH(stats *TradingStats) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("## 历史交易统计\n\n")
|
||||
|
||||
// 盈亏比计算
|
||||
var winLossRatio float64
|
||||
if stats.AvgLoss > 0 {
|
||||
winLossRatio = stats.AvgWin / stats.AvgLoss
|
||||
}
|
||||
|
||||
// 指标定义说明(去掉胜率,聚焦核心指标)
|
||||
sb.WriteString("**指标说明**:\n")
|
||||
sb.WriteString("- 盈利因子: 总盈利 ÷ 总亏损(>1表示盈利,>1.5为良好,>2为优秀)\n")
|
||||
sb.WriteString("- 夏普比率: (平均收益 - 无风险收益) ÷ 收益标准差(>1良好,>2优秀)\n")
|
||||
sb.WriteString("- 盈亏比: 平均盈利 ÷ 平均亏损(>1.5为良好,>2为优秀)\n")
|
||||
sb.WriteString("- 最大回撤: 资金曲线从峰值到谷底的最大跌幅(<20%为低风险)\n\n")
|
||||
|
||||
// 数据值
|
||||
sb.WriteString("**当前数据**:\n")
|
||||
sb.WriteString(fmt.Sprintf("- 总交易: %d 笔\n", stats.TotalTrades))
|
||||
sb.WriteString(fmt.Sprintf("- 盈利因子: %.2f\n", stats.ProfitFactor))
|
||||
sb.WriteString(fmt.Sprintf("- 夏普比率: %.2f\n", stats.SharpeRatio))
|
||||
sb.WriteString(fmt.Sprintf("- 盈亏比: %.2f\n", winLossRatio))
|
||||
sb.WriteString(fmt.Sprintf("- 总盈亏: %+.2f USDT\n", stats.TotalPnL))
|
||||
sb.WriteString(fmt.Sprintf("- 平均盈利: +%.2f USDT\n", stats.AvgWin))
|
||||
sb.WriteString(fmt.Sprintf("- 平均亏损: -%.2f USDT\n", stats.AvgLoss))
|
||||
sb.WriteString(fmt.Sprintf("- 最大回撤: %.1f%%\n\n", stats.MaxDrawdownPct))
|
||||
|
||||
// 综合分析和决策建议
|
||||
sb.WriteString("**决策参考**:\n")
|
||||
|
||||
// 根据统计数据给出具体建议
|
||||
if stats.TotalTrades < 10 {
|
||||
sb.WriteString("- 样本量较小(<10笔),统计结果参考意义有限\n")
|
||||
}
|
||||
|
||||
if stats.ProfitFactor >= 1.5 && stats.SharpeRatio >= 1 {
|
||||
sb.WriteString("- 📈 表现良好: 可以维持当前策略风格\n")
|
||||
} else if stats.ProfitFactor >= 1.0 {
|
||||
sb.WriteString("- 📊 表现正常: 策略可行但有优化空间\n")
|
||||
}
|
||||
|
||||
if stats.ProfitFactor < 1.0 {
|
||||
sb.WriteString("- ⚠️ 盈利因子<1: 亏损大于盈利,需要提高盈亏比,优化止盈止损\n")
|
||||
}
|
||||
|
||||
if winLossRatio > 0 && winLossRatio < 1.5 {
|
||||
sb.WriteString("- ⚠️ 盈亏比偏低: 建议让利润奔跑,提高止盈目标\n")
|
||||
}
|
||||
|
||||
if stats.MaxDrawdownPct > 30 {
|
||||
sb.WriteString("- ⚠️ 最大回撤过高: 建议降低仓位大小控制风险\n")
|
||||
} else if stats.MaxDrawdownPct < 10 {
|
||||
sb.WriteString("- ✅ 回撤控制良好: 风险管理有效\n")
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatRecentTradesZH 格式化最近交易(中文)
|
||||
func formatRecentTradesZH(orders []RecentOrder) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("## 最近完成的交易\n\n")
|
||||
|
||||
for i, order := range orders {
|
||||
// 判断盈亏
|
||||
profitOrLoss := "盈利"
|
||||
if order.RealizedPnL < 0 {
|
||||
profitOrLoss = "亏损"
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%d. %s %s | 进场 %.4f 出场 %.4f | %s: %+.2f USDT (%+.2f%%) | %s → %s (%s)\n",
|
||||
i+1,
|
||||
order.Symbol,
|
||||
order.Side,
|
||||
order.EntryPrice,
|
||||
order.ExitPrice,
|
||||
profitOrLoss,
|
||||
order.RealizedPnL,
|
||||
order.PnLPct,
|
||||
order.EntryTime,
|
||||
order.ExitTime,
|
||||
order.HoldDuration,
|
||||
))
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatCurrentPositionsZH 格式化当前持仓(中文)
|
||||
func formatCurrentPositionsZH(ctx *Context) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("## 当前持仓\n\n")
|
||||
|
||||
for i, pos := range ctx.Positions {
|
||||
// 计算回撤
|
||||
drawdown := pos.UnrealizedPnLPct - pos.PeakPnLPct
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%d. %s %s | ", i+1, pos.Symbol, strings.ToUpper(pos.Side)))
|
||||
sb.WriteString(fmt.Sprintf("进场 %.4f 当前 %.4f | ", pos.EntryPrice, pos.MarkPrice))
|
||||
sb.WriteString(fmt.Sprintf("数量 %.4f | ", pos.Quantity))
|
||||
sb.WriteString(fmt.Sprintf("仓位价值 %.2f USDT | ", pos.Quantity*pos.MarkPrice))
|
||||
sb.WriteString(fmt.Sprintf("盈亏 %+.2f%% | ", pos.UnrealizedPnLPct))
|
||||
sb.WriteString(fmt.Sprintf("盈亏金额 %+.2f USDT | ", pos.UnrealizedPnL))
|
||||
sb.WriteString(fmt.Sprintf("峰值盈亏 %.2f%% | ", pos.PeakPnLPct))
|
||||
sb.WriteString(fmt.Sprintf("杠杆 %dx | ", pos.Leverage))
|
||||
sb.WriteString(fmt.Sprintf("保证金 %.0f USDT | ", pos.MarginUsed))
|
||||
sb.WriteString(fmt.Sprintf("强平价 %.4f\n", pos.LiquidationPrice))
|
||||
|
||||
// 添加分析提示
|
||||
if drawdown < -0.30*pos.PeakPnLPct && pos.PeakPnLPct > 0.02 {
|
||||
sb.WriteString(fmt.Sprintf(" ⚠️ **止盈提示**: 当前盈亏从峰值 %.2f%% 回撤到 %.2f%%,回撤幅度 %.2f%%,建议考虑止盈\n",
|
||||
pos.PeakPnLPct, pos.UnrealizedPnLPct, (drawdown/pos.PeakPnLPct)*100))
|
||||
}
|
||||
|
||||
if pos.UnrealizedPnLPct < -4.0 {
|
||||
sb.WriteString(" ⚠️ **止损提示**: 亏损接近-5%止损线,建议考虑止损\n")
|
||||
}
|
||||
|
||||
// 显示当前价格(如果有市场数据)
|
||||
if ctx.MarketDataMap != nil {
|
||||
if mdata, ok := ctx.MarketDataMap[pos.Symbol]; ok {
|
||||
sb.WriteString(fmt.Sprintf(" 📈 当前价格: %.4f\n", mdata.CurrentPrice))
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatCandidateCoinsZH 格式化候选币种(中文)
|
||||
func formatCandidateCoinsZH(ctx *Context) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("## 候选币种\n\n")
|
||||
|
||||
for i, coin := range ctx.CandidateCoins {
|
||||
sb.WriteString(fmt.Sprintf("### %d. %s\n\n", i+1, coin.Symbol))
|
||||
|
||||
// 当前价格
|
||||
if ctx.MarketDataMap != nil {
|
||||
if mdata, ok := ctx.MarketDataMap[coin.Symbol]; ok {
|
||||
sb.WriteString(fmt.Sprintf("当前价格: %.4f\n\n", mdata.CurrentPrice))
|
||||
|
||||
// K线数据(多时间框架)
|
||||
if mdata.TimeframeData != nil {
|
||||
sb.WriteString(formatKlineDataZH(coin.Symbol, mdata.TimeframeData, ctx.Timeframes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// OI数据(如果有)
|
||||
if ctx.OITopDataMap != nil {
|
||||
if oiData, ok := ctx.OITopDataMap[coin.Symbol]; ok {
|
||||
sb.WriteString(fmt.Sprintf("**持仓量变化**: OI排名 #%d | 变化 %+.2f%% (%+.2fM USDT) | 价格变化 %+.2f%%\n\n",
|
||||
oiData.Rank,
|
||||
oiData.OIDeltaPercent,
|
||||
oiData.OIDeltaValue/1_000_000,
|
||||
oiData.PriceDeltaPercent,
|
||||
))
|
||||
|
||||
// OI解读
|
||||
oiChange := "增加"
|
||||
if oiData.OIDeltaPercent < 0 {
|
||||
oiChange = "减少"
|
||||
}
|
||||
priceChange := "上涨"
|
||||
if oiData.PriceDeltaPercent < 0 {
|
||||
priceChange = "下跌"
|
||||
}
|
||||
|
||||
interpretation := getOIInterpretationZH(oiChange, priceChange)
|
||||
sb.WriteString(fmt.Sprintf("**市场解读**: %s\n\n", interpretation))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatKlineDataZH 格式化K线数据(中文)
|
||||
func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesData, timeframes []string) string {
|
||||
var sb strings.Builder
|
||||
|
||||
for _, tf := range timeframes {
|
||||
if data, ok := tfData[tf]; ok && len(data.Klines) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("#### %s 时间框架 (从旧到新)\n\n", tf))
|
||||
sb.WriteString("```\n")
|
||||
sb.WriteString("时间(UTC) 开盘 最高 最低 收盘 成交量\n")
|
||||
|
||||
// 只显示最近30根K线
|
||||
startIdx := 0
|
||||
if len(data.Klines) > 30 {
|
||||
startIdx = len(data.Klines) - 30
|
||||
}
|
||||
|
||||
for i := startIdx; i < len(data.Klines); i++ {
|
||||
k := data.Klines[i]
|
||||
t := time.UnixMilli(k.Time).UTC()
|
||||
sb.WriteString(fmt.Sprintf("%s %.4f %.4f %.4f %.4f %.2f\n",
|
||||
t.Format("01-02 15:04"),
|
||||
k.Open,
|
||||
k.High,
|
||||
k.Low,
|
||||
k.Close,
|
||||
k.Volume,
|
||||
))
|
||||
}
|
||||
|
||||
// 标记最后一根K线
|
||||
if len(data.Klines) > 0 {
|
||||
sb.WriteString(" <- 当前\n")
|
||||
}
|
||||
|
||||
sb.WriteString("```\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatOIRankingZH 格式化OI排名数据(中文)
|
||||
func formatOIRankingZH(oiData interface{}) string {
|
||||
// TODO: 根据实际OIRankingData结构实现
|
||||
return "## 市场持仓量排名\n\n(数据加载中...)\n\n"
|
||||
}
|
||||
|
||||
// getOIInterpretationZH 获取OI变化解读(中文)
|
||||
func getOIInterpretationZH(oiChange, priceChange string) string {
|
||||
if oiChange == "增加" && priceChange == "上涨" {
|
||||
return OIInterpretation.OIUp_PriceUp.ZH
|
||||
} else if oiChange == "增加" && priceChange == "下跌" {
|
||||
return OIInterpretation.OIUp_PriceDown.ZH
|
||||
} else if oiChange == "减少" && priceChange == "上涨" {
|
||||
return OIInterpretation.OIDown_PriceUp.ZH
|
||||
} else {
|
||||
return OIInterpretation.OIDown_PriceDown.ZH
|
||||
}
|
||||
}
|
||||
|
||||
// ========== 英文格式化函数 ==========
|
||||
|
||||
// formatHeaderEN 格式化头部信息(英文)
|
||||
func formatHeaderEN(ctx *Context) string {
|
||||
return fmt.Sprintf("# 📊 Trading Decision Request\n\nTime: %s | Period: #%d | Runtime: %d minutes\n\n",
|
||||
ctx.CurrentTime, ctx.CallCount, ctx.RuntimeMinutes)
|
||||
}
|
||||
|
||||
// formatAccountEN 格式化账户信息(英文)
|
||||
func formatAccountEN(ctx *Context) string {
|
||||
acc := ctx.Account
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString("## Account Status\n\n")
|
||||
sb.WriteString(fmt.Sprintf("Total Equity: %.2f USDT | ", acc.TotalEquity))
|
||||
sb.WriteString(fmt.Sprintf("Available Balance: %.2f USDT (%.1f%%) | ", acc.AvailableBalance, (acc.AvailableBalance/acc.TotalEquity)*100))
|
||||
sb.WriteString(fmt.Sprintf("Total PnL: %+.2f%% | ", acc.TotalPnLPct))
|
||||
sb.WriteString(fmt.Sprintf("Margin Usage: %.1f%% | ", acc.MarginUsedPct))
|
||||
sb.WriteString(fmt.Sprintf("Positions: %d\n\n", acc.PositionCount))
|
||||
|
||||
// Risk warning
|
||||
if acc.MarginUsedPct > 70 {
|
||||
sb.WriteString("⚠️ **Risk Alert**: Margin usage > 70%, high risk!\n\n")
|
||||
} else if acc.MarginUsedPct > 50 {
|
||||
sb.WriteString("⚠️ **Risk Notice**: Margin usage > 50%, be cautious with new positions\n\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatTradingStatsEN 格式化历史交易统计(英文)
|
||||
func formatTradingStatsEN(stats *TradingStats) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("## Historical Trading Statistics\n\n")
|
||||
|
||||
// Win/Loss ratio calculation
|
||||
var winLossRatio float64
|
||||
if stats.AvgLoss > 0 {
|
||||
winLossRatio = stats.AvgWin / stats.AvgLoss
|
||||
}
|
||||
|
||||
// Metric definitions (focus on core metrics, remove win rate)
|
||||
sb.WriteString("**Metric Definitions**:\n")
|
||||
sb.WriteString("- Profit Factor: Total profits ÷ Total losses (>1 = profitable, >1.5 = good, >2 = excellent)\n")
|
||||
sb.WriteString("- Sharpe Ratio: (Avg return - Risk-free rate) ÷ Std dev of returns (>1 = good, >2 = excellent)\n")
|
||||
sb.WriteString("- Win/Loss Ratio: Avg win ÷ Avg loss (>1.5 = good, >2 = excellent)\n")
|
||||
sb.WriteString("- Max Drawdown: Largest peak-to-trough decline in equity curve (<20% = low risk)\n\n")
|
||||
|
||||
// Data values
|
||||
sb.WriteString("**Current Data**:\n")
|
||||
sb.WriteString(fmt.Sprintf("- Total Trades: %d\n", stats.TotalTrades))
|
||||
sb.WriteString(fmt.Sprintf("- Profit Factor: %.2f\n", stats.ProfitFactor))
|
||||
sb.WriteString(fmt.Sprintf("- Sharpe Ratio: %.2f\n", stats.SharpeRatio))
|
||||
sb.WriteString(fmt.Sprintf("- Win/Loss Ratio: %.2f\n", winLossRatio))
|
||||
sb.WriteString(fmt.Sprintf("- Total PnL: %+.2f USDT\n", stats.TotalPnL))
|
||||
sb.WriteString(fmt.Sprintf("- Avg Win: +%.2f USDT\n", stats.AvgWin))
|
||||
sb.WriteString(fmt.Sprintf("- Avg Loss: -%.2f USDT\n", stats.AvgLoss))
|
||||
sb.WriteString(fmt.Sprintf("- Max Drawdown: %.1f%%\n\n", stats.MaxDrawdownPct))
|
||||
|
||||
// Analysis and decision guidance
|
||||
sb.WriteString("**Decision Guidance**:\n")
|
||||
|
||||
// Specific recommendations based on stats
|
||||
if stats.TotalTrades < 10 {
|
||||
sb.WriteString("- Small sample size (<10 trades), statistics have limited significance\n")
|
||||
}
|
||||
|
||||
if stats.ProfitFactor >= 1.5 && stats.SharpeRatio >= 1 {
|
||||
sb.WriteString("- 📈 Good performance: Maintain current strategy approach\n")
|
||||
} else if stats.ProfitFactor >= 1.0 {
|
||||
sb.WriteString("- 📊 Normal performance: Strategy viable but has room for optimization\n")
|
||||
}
|
||||
|
||||
if stats.ProfitFactor < 1.0 {
|
||||
sb.WriteString("- ⚠️ Profit factor <1: Losses exceed profits, improve win/loss ratio, optimize TP/SL\n")
|
||||
}
|
||||
|
||||
if winLossRatio > 0 && winLossRatio < 1.5 {
|
||||
sb.WriteString("- ⚠️ Low win/loss ratio: Let profits run, increase take-profit targets\n")
|
||||
}
|
||||
|
||||
if stats.MaxDrawdownPct > 30 {
|
||||
sb.WriteString("- ⚠️ High max drawdown: Consider reducing position sizes to control risk\n")
|
||||
} else if stats.MaxDrawdownPct < 10 {
|
||||
sb.WriteString("- ✅ Good drawdown control: Risk management is effective\n")
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatRecentTradesEN 格式化最近交易(英文)
|
||||
func formatRecentTradesEN(orders []RecentOrder) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("## Recent Completed Trades\n\n")
|
||||
|
||||
for i, order := range orders {
|
||||
profitOrLoss := "Profit"
|
||||
if order.RealizedPnL < 0 {
|
||||
profitOrLoss = "Loss"
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%d. %s %s | Entry %.4f Exit %.4f | %s: %+.2f USDT (%+.2f%%) | %s → %s (%s)\n",
|
||||
i+1,
|
||||
order.Symbol,
|
||||
order.Side,
|
||||
order.EntryPrice,
|
||||
order.ExitPrice,
|
||||
profitOrLoss,
|
||||
order.RealizedPnL,
|
||||
order.PnLPct,
|
||||
order.EntryTime,
|
||||
order.ExitTime,
|
||||
order.HoldDuration,
|
||||
))
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatCurrentPositionsEN 格式化当前持仓(英文)
|
||||
func formatCurrentPositionsEN(ctx *Context) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("## Current Positions\n\n")
|
||||
|
||||
for i, pos := range ctx.Positions {
|
||||
drawdown := pos.UnrealizedPnLPct - pos.PeakPnLPct
|
||||
|
||||
sb.WriteString(fmt.Sprintf("%d. %s %s | ", i+1, pos.Symbol, strings.ToUpper(pos.Side)))
|
||||
sb.WriteString(fmt.Sprintf("Entry %.4f Current %.4f | ", pos.EntryPrice, pos.MarkPrice))
|
||||
sb.WriteString(fmt.Sprintf("Qty %.4f | ", pos.Quantity))
|
||||
sb.WriteString(fmt.Sprintf("Value %.2f USDT | ", pos.Quantity*pos.MarkPrice))
|
||||
sb.WriteString(fmt.Sprintf("PnL %+.2f%% | ", pos.UnrealizedPnLPct))
|
||||
sb.WriteString(fmt.Sprintf("PnL Amount %+.2f USDT | ", pos.UnrealizedPnL))
|
||||
sb.WriteString(fmt.Sprintf("Peak PnL %.2f%% | ", pos.PeakPnLPct))
|
||||
sb.WriteString(fmt.Sprintf("Leverage %dx | ", pos.Leverage))
|
||||
sb.WriteString(fmt.Sprintf("Margin %.0f USDT | ", pos.MarginUsed))
|
||||
sb.WriteString(fmt.Sprintf("Liq Price %.4f\n", pos.LiquidationPrice))
|
||||
|
||||
// Analysis hints
|
||||
if drawdown < -0.30*pos.PeakPnLPct && pos.PeakPnLPct > 0.02 {
|
||||
sb.WriteString(fmt.Sprintf(" ⚠️ **Take Profit Alert**: PnL dropped from peak %.2f%% to %.2f%%, drawdown %.2f%%, consider taking profit\n",
|
||||
pos.PeakPnLPct, pos.UnrealizedPnLPct, (drawdown/pos.PeakPnLPct)*100))
|
||||
}
|
||||
|
||||
if pos.UnrealizedPnLPct < -4.0 {
|
||||
sb.WriteString(" ⚠️ **Stop Loss Alert**: Loss approaching -5% threshold, consider cutting loss\n")
|
||||
}
|
||||
|
||||
if ctx.MarketDataMap != nil {
|
||||
if mdata, ok := ctx.MarketDataMap[pos.Symbol]; ok {
|
||||
sb.WriteString(fmt.Sprintf(" 📈 Current Price: %.4f\n", mdata.CurrentPrice))
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatCandidateCoinsEN 格式化候选币种(英文)
|
||||
func formatCandidateCoinsEN(ctx *Context) string {
|
||||
var sb strings.Builder
|
||||
sb.WriteString("## Candidate Coins\n\n")
|
||||
|
||||
for i, coin := range ctx.CandidateCoins {
|
||||
sb.WriteString(fmt.Sprintf("### %d. %s\n\n", i+1, coin.Symbol))
|
||||
|
||||
if ctx.MarketDataMap != nil {
|
||||
if mdata, ok := ctx.MarketDataMap[coin.Symbol]; ok {
|
||||
sb.WriteString(fmt.Sprintf("Current Price: %.4f\n\n", mdata.CurrentPrice))
|
||||
|
||||
if mdata.TimeframeData != nil {
|
||||
sb.WriteString(formatKlineDataEN(coin.Symbol, mdata.TimeframeData, ctx.Timeframes))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if ctx.OITopDataMap != nil {
|
||||
if oiData, ok := ctx.OITopDataMap[coin.Symbol]; ok {
|
||||
sb.WriteString(fmt.Sprintf("**OI Change**: Rank #%d | Change %+.2f%% (%+.2fM USDT) | Price Change %+.2f%%\n\n",
|
||||
oiData.Rank,
|
||||
oiData.OIDeltaPercent,
|
||||
oiData.OIDeltaValue/1_000_000,
|
||||
oiData.PriceDeltaPercent,
|
||||
))
|
||||
|
||||
oiChange := "increase"
|
||||
if oiData.OIDeltaPercent < 0 {
|
||||
oiChange = "decrease"
|
||||
}
|
||||
priceChange := "up"
|
||||
if oiData.PriceDeltaPercent < 0 {
|
||||
priceChange = "down"
|
||||
}
|
||||
|
||||
interpretation := getOIInterpretationEN(oiChange, priceChange)
|
||||
sb.WriteString(fmt.Sprintf("**Market Interpretation**: %s\n\n", interpretation))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatKlineDataEN 格式化K线数据(英文)
|
||||
func formatKlineDataEN(symbol string, tfData map[string]*market.TimeframeSeriesData, timeframes []string) string {
|
||||
var sb strings.Builder
|
||||
|
||||
// Sort timeframes for consistent output
|
||||
sortedTF := make([]string, len(timeframes))
|
||||
copy(sortedTF, timeframes)
|
||||
sort.Strings(sortedTF)
|
||||
|
||||
for _, tf := range sortedTF {
|
||||
if data, ok := tfData[tf]; ok && len(data.Klines) > 0 {
|
||||
sb.WriteString(fmt.Sprintf("#### %s Timeframe (oldest → latest)\n\n", tf))
|
||||
sb.WriteString("```\n")
|
||||
sb.WriteString("Time(UTC) Open High Low Close Volume\n")
|
||||
|
||||
startIdx := 0
|
||||
if len(data.Klines) > 30 {
|
||||
startIdx = len(data.Klines) - 30
|
||||
}
|
||||
|
||||
for i := startIdx; i < len(data.Klines); i++ {
|
||||
k := data.Klines[i]
|
||||
t := time.UnixMilli(k.Time).UTC()
|
||||
sb.WriteString(fmt.Sprintf("%s %.4f %.4f %.4f %.4f %.2f\n",
|
||||
t.Format("01-02 15:04"),
|
||||
k.Open,
|
||||
k.High,
|
||||
k.Low,
|
||||
k.Close,
|
||||
k.Volume,
|
||||
))
|
||||
}
|
||||
|
||||
if len(data.Klines) > 0 {
|
||||
sb.WriteString(" <- current\n")
|
||||
}
|
||||
|
||||
sb.WriteString("```\n\n")
|
||||
}
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatOIRankingEN 格式化OI排名数据(英文)
|
||||
func formatOIRankingEN(oiData interface{}) string {
|
||||
return "## Market-wide OI Ranking\n\n(Loading data...)\n\n"
|
||||
}
|
||||
|
||||
// getOIInterpretationEN 获取OI变化解读(英文)
|
||||
func getOIInterpretationEN(oiChange, priceChange string) string {
|
||||
if oiChange == "increase" && priceChange == "up" {
|
||||
return OIInterpretation.OIUp_PriceUp.EN
|
||||
} else if oiChange == "increase" && priceChange == "down" {
|
||||
return OIInterpretation.OIUp_PriceDown.EN
|
||||
} else if oiChange == "decrease" && priceChange == "up" {
|
||||
return OIInterpretation.OIDown_PriceUp.EN
|
||||
} else {
|
||||
return OIInterpretation.OIDown_PriceDown.EN
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,376 @@
|
||||
package kernel
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
// ============================================================================
|
||||
// AI Prompt Builder - AI提示词构建器
|
||||
// ============================================================================
|
||||
// 构建完整的AI提示词,包括系统提示词和用户提示词
|
||||
// ============================================================================
|
||||
|
||||
// PromptBuilder 提示词构建器
|
||||
type PromptBuilder struct {
|
||||
lang Language
|
||||
}
|
||||
|
||||
// NewPromptBuilder 创建提示词构建器
|
||||
func NewPromptBuilder(lang Language) *PromptBuilder {
|
||||
return &PromptBuilder{lang: lang}
|
||||
}
|
||||
|
||||
// BuildSystemPrompt 构建系统提示词
|
||||
func (pb *PromptBuilder) BuildSystemPrompt() string {
|
||||
if pb.lang == LangChinese {
|
||||
return pb.buildSystemPromptZH()
|
||||
}
|
||||
return pb.buildSystemPromptEN()
|
||||
}
|
||||
|
||||
// BuildUserPrompt 构建用户提示词(包含完整的交易上下文)
|
||||
func (pb *PromptBuilder) BuildUserPrompt(ctx *Context) string {
|
||||
// 使用Formatter格式化交易上下文
|
||||
formattedData := FormatContextForAI(ctx, pb.lang)
|
||||
|
||||
// 添加决策要求
|
||||
if pb.lang == LangChinese {
|
||||
return formattedData + pb.getDecisionRequirementsZH()
|
||||
}
|
||||
return formattedData + pb.getDecisionRequirementsEN()
|
||||
}
|
||||
|
||||
// ========== 中文提示词 ==========
|
||||
|
||||
func (pb *PromptBuilder) buildSystemPromptZH() string {
|
||||
return `你是一个专业的量化交易AI助手,负责分析市场数据并做出交易决策。
|
||||
|
||||
## 你的任务
|
||||
|
||||
1. **分析账户状态**: 评估当前风险水平、保证金使用率、持仓情况
|
||||
2. **分析当前持仓**: 判断是否需要止盈、止损、加仓或持有
|
||||
3. **分析候选币种**: 评估新的交易机会,结合技术分析和资金流向
|
||||
4. **做出决策**: 输出明确的交易决策,包含详细的推理过程
|
||||
|
||||
## 决策原则
|
||||
|
||||
### 风险优先
|
||||
- 保证金使用率不得超过30%
|
||||
- 单个持仓亏损达到-5%必须止损
|
||||
- 优先保护资本,再考虑盈利
|
||||
|
||||
### 跟踪止盈
|
||||
- 当持仓盈亏从峰值回撤30%时,考虑部分或全部止盈
|
||||
- 例如:Peak PnL +5%,Current PnL +3.5% → 回撤了30%,应该止盈
|
||||
|
||||
### 顺势交易
|
||||
- 只在多个时间框架趋势一致时进场
|
||||
- 结合持仓量(OI)变化判断资金流向真实性
|
||||
- OI增加+价格上涨 = 强多头趋势
|
||||
- OI减少+价格上涨 = 空头平仓(可能反转)
|
||||
|
||||
### 分批操作
|
||||
- 分批建仓:第一次开仓不超过目标仓位的50%
|
||||
- 分批止盈:盈利3%平33%,盈利5%平50%,盈利8%全平
|
||||
- 只在盈利仓位上加仓,永远不要追亏损
|
||||
|
||||
## 输出格式要求
|
||||
|
||||
**必须**使用以下JSON格式输出决策:
|
||||
|
||||
` + "```json" + `
|
||||
[
|
||||
{
|
||||
"symbol": "BTCUSDT",
|
||||
"action": "HOLD|PARTIAL_CLOSE|FULL_CLOSE|ADD_POSITION|OPEN_NEW|WAIT",
|
||||
"leverage": 3,
|
||||
"position_size_usd": 1000,
|
||||
"stop_loss": 42000,
|
||||
"take_profit": 48000,
|
||||
"confidence": 85,
|
||||
"reasoning": "详细的推理过程,说明为什么做出这个决策"
|
||||
}
|
||||
]
|
||||
` + "```" + `
|
||||
|
||||
### 字段说明
|
||||
|
||||
- **symbol**: 交易对(必需)
|
||||
- **action**: 动作类型(必需)
|
||||
- HOLD: 持有当前仓位
|
||||
- PARTIAL_CLOSE: 部分平仓
|
||||
- FULL_CLOSE: 全部平仓
|
||||
- ADD_POSITION: 在现有仓位上加仓
|
||||
- OPEN_NEW: 开设新仓位
|
||||
- WAIT: 等待,不采取任何行动
|
||||
- **leverage**: 杠杆倍数(开新仓时必需)
|
||||
- **position_size_usd**: 仓位大小(USDT,开新仓时必需)
|
||||
- **stop_loss**: 止损价格(开新仓时建议提供)
|
||||
- **take_profit**: 止盈价格(开新仓时建议提供)
|
||||
- **confidence**: 信心度(0-100)
|
||||
- **reasoning**: 推理过程(必需,必须详细说明决策依据)
|
||||
|
||||
## 重要提醒
|
||||
|
||||
1. **永远不要**混淆已实现盈亏和未实现盈亏
|
||||
2. **永远记得**考虑杠杆对盈亏的放大作用
|
||||
3. **永远关注**Peak PnL,这是判断止盈的关键指标
|
||||
4. **永远结合**持仓量(OI)变化来判断趋势真实性
|
||||
5. **永远遵守**风险管理规则,保护资本是第一位的
|
||||
|
||||
现在,请仔细分析接下来提供的交易数据,并做出专业的决策。`
|
||||
}
|
||||
|
||||
func (pb *PromptBuilder) getDecisionRequirementsZH() string {
|
||||
return `
|
||||
|
||||
---
|
||||
|
||||
## 📝 现在请做出决策
|
||||
|
||||
### 决策步骤
|
||||
|
||||
1. **分析账户风险**:
|
||||
- 当前保证金使用率是否在安全范围?
|
||||
- 是否有足够资金开新仓?
|
||||
|
||||
2. **分析现有持仓**(如果有):
|
||||
- 是否触发止损条件?
|
||||
- 是否触发跟踪止盈条件?
|
||||
- 是否适合加仓?
|
||||
|
||||
3. **分析候选币种**(如果有):
|
||||
- 技术形态是否符合进场条件?
|
||||
- 持仓量变化是否支持趋势?
|
||||
- 多个时间框架是否共振?
|
||||
|
||||
4. **输出决策**:
|
||||
- 使用规定的JSON格式
|
||||
- 提供详细的推理过程
|
||||
- 给出明确的行动指令
|
||||
|
||||
### 输出示例
|
||||
|
||||
` + "```json" + `
|
||||
[
|
||||
{
|
||||
"symbol": "PIPPINUSDT",
|
||||
"action": "PARTIAL_CLOSE",
|
||||
"confidence": 85,
|
||||
"reasoning": "当前PnL +2.96%,接近历史峰值+2.99%(回撤仅0.03%)。建议部分平仓锁定利润,因为:1) 持仓时间仅11分钟,已获得3%收益;2) 5分钟K线显示价格接近短期阻力位;3) 成交量开始萎缩,上涨动能减弱。建议平仓50%,剩余仓位设置跟踪止盈在峰值回撤20%处。"
|
||||
},
|
||||
{
|
||||
"symbol": "HUSDT",
|
||||
"action": "OPEN_NEW",
|
||||
"leverage": 3,
|
||||
"position_size_usd": 500,
|
||||
"stop_loss": 0.1560,
|
||||
"take_profit": 0.1720,
|
||||
"confidence": 75,
|
||||
"reasoning": "HUSDT在5分钟时间框架突破关键阻力位0.1630,持仓量1小时内增加+1.57M (+0.89%),配合价格上涨+4.92%,符合'OI增加+价格上涨'的强多头模式。15分钟和1小时时间框架均呈现上涨趋势,多周期共振。建议开仓做多,止损设在突破点下方-5%,止盈目标+8%。"
|
||||
}
|
||||
]
|
||||
` + "```" + `
|
||||
|
||||
**请立即输出你的决策(JSON格式)**:`
|
||||
}
|
||||
|
||||
// ========== 英文提示词 ==========
|
||||
|
||||
func (pb *PromptBuilder) buildSystemPromptEN() string {
|
||||
return `You are a professional quantitative trading AI assistant responsible for analyzing market data and making trading decisions.
|
||||
|
||||
## Your Mission
|
||||
|
||||
1. **Analyze Account Status**: Evaluate current risk level, margin usage, and positions
|
||||
2. **Analyze Current Positions**: Determine if stop-loss, take-profit, scaling, or holding is needed
|
||||
3. **Analyze Candidate Coins**: Assess new trading opportunities using technical analysis and capital flows
|
||||
4. **Make Decisions**: Output clear trading decisions with detailed reasoning
|
||||
|
||||
## Decision Principles
|
||||
|
||||
### Risk First
|
||||
- Margin usage must not exceed 30%
|
||||
- Must stop-loss when single position loss reaches -5%
|
||||
- Capital protection first, profit second
|
||||
|
||||
### Trailing Take-Profit
|
||||
- Consider partial/full profit-taking when PnL pulls back 30% from peak
|
||||
- Example: Peak PnL +5%, Current PnL +3.5% → 30% drawdown, should take profit
|
||||
|
||||
### Trend Following
|
||||
- Only enter when trends align across multiple timeframes
|
||||
- Use Open Interest (OI) changes to validate capital flow authenticity
|
||||
- OI up + Price up = Strong bullish trend
|
||||
- OI down + Price up = Shorts covering (potential reversal)
|
||||
|
||||
### Scale Operations
|
||||
- Scale-in: First entry max 50% of target position
|
||||
- Scale-out: Close 33% at +3%, 50% at +5%, 100% at +8%
|
||||
- Only add to winning positions, never average down losers
|
||||
|
||||
## Output Format Requirements
|
||||
|
||||
**Must** use the following JSON format:
|
||||
|
||||
` + "```json" + `
|
||||
[
|
||||
{
|
||||
"symbol": "BTCUSDT",
|
||||
"action": "HOLD|PARTIAL_CLOSE|FULL_CLOSE|ADD_POSITION|OPEN_NEW|WAIT",
|
||||
"leverage": 3,
|
||||
"position_size_usd": 1000,
|
||||
"stop_loss": 42000,
|
||||
"take_profit": 48000,
|
||||
"confidence": 85,
|
||||
"reasoning": "Detailed reasoning explaining why this decision was made"
|
||||
}
|
||||
]
|
||||
` + "```" + `
|
||||
|
||||
### Field Descriptions
|
||||
|
||||
- **symbol**: Trading pair (required)
|
||||
- **action**: Action type (required)
|
||||
- HOLD: Hold current position
|
||||
- PARTIAL_CLOSE: Partially close position
|
||||
- FULL_CLOSE: Fully close position
|
||||
- ADD_POSITION: Add to existing position
|
||||
- OPEN_NEW: Open new position
|
||||
- WAIT: Wait, take no action
|
||||
- **leverage**: Leverage multiplier (required for new positions)
|
||||
- **position_size_usd**: Position size in USDT (required for new positions)
|
||||
- **stop_loss**: Stop-loss price (recommended for new positions)
|
||||
- **take_profit**: Take-profit price (recommended for new positions)
|
||||
- **confidence**: Confidence level (0-100)
|
||||
- **reasoning**: Detailed reasoning (required, must explain decision basis)
|
||||
|
||||
## Critical Reminders
|
||||
|
||||
1. **Never** confuse realized and unrealized P&L
|
||||
2. **Always remember** leverage amplifies both gains and losses
|
||||
3. **Always watch** Peak PnL - it's key for take-profit decisions
|
||||
4. **Always combine** OI changes to validate trend authenticity
|
||||
5. **Always follow** risk management rules - capital protection is priority #1
|
||||
|
||||
Now, please carefully analyze the trading data provided next and make professional decisions.`
|
||||
}
|
||||
|
||||
func (pb *PromptBuilder) getDecisionRequirementsEN() string {
|
||||
return `
|
||||
|
||||
---
|
||||
|
||||
## 📝 Make Your Decision Now
|
||||
|
||||
### Decision Steps
|
||||
|
||||
1. **Analyze Account Risk**:
|
||||
- Is margin usage within safe range?
|
||||
- Is there enough capital for new positions?
|
||||
|
||||
2. **Analyze Existing Positions** (if any):
|
||||
- Is stop-loss triggered?
|
||||
- Is trailing take-profit triggered?
|
||||
- Is it suitable to scale-in?
|
||||
|
||||
3. **Analyze Candidate Coins** (if any):
|
||||
- Does technical pattern meet entry criteria?
|
||||
- Do OI changes support the trend?
|
||||
- Do multiple timeframes align?
|
||||
|
||||
4. **Output Decision**:
|
||||
- Use the specified JSON format
|
||||
- Provide detailed reasoning
|
||||
- Give clear action instructions
|
||||
|
||||
### Output Example
|
||||
|
||||
` + "```json" + `
|
||||
[
|
||||
{
|
||||
"symbol": "PIPPINUSDT",
|
||||
"action": "PARTIAL_CLOSE",
|
||||
"confidence": 85,
|
||||
"reasoning": "Current PnL +2.96%, near historical peak +2.99% (only 0.03% pullback). Suggest partial close to lock profits because: 1) Only 11 minutes holding time with 3% gain; 2) 5M chart shows price approaching short-term resistance; 3) Volume declining, upward momentum weakening. Recommend closing 50%, set trailing stop at 20% pullback from peak for remainder."
|
||||
},
|
||||
{
|
||||
"symbol": "HUSDT",
|
||||
"action": "OPEN_NEW",
|
||||
"leverage": 3,
|
||||
"position_size_usd": 500,
|
||||
"stop_loss": 0.1560,
|
||||
"take_profit": 0.1720,
|
||||
"confidence": 75,
|
||||
"reasoning": "HUSDT broke key resistance 0.1630 on 5M timeframe. OI increased +1.57M (+0.89%) in 1H paired with price +4.92%, matching 'OI up + price up' strong bullish pattern. Both 15M and 1H timeframes show uptrend, multi-timeframe resonance confirmed. Recommend long entry, stop-loss -5% below breakout, target +8% profit."
|
||||
}
|
||||
]
|
||||
` + "```" + `
|
||||
|
||||
**Please output your decision (JSON format) immediately**:`
|
||||
}
|
||||
|
||||
// ========== 辅助函数 ==========
|
||||
|
||||
// FormatDecisionExample 格式化决策示例(用于文档)
|
||||
func FormatDecisionExample(lang Language) string {
|
||||
example := Decision{
|
||||
Symbol: "BTCUSDT",
|
||||
Action: "OPEN_NEW",
|
||||
Leverage: 3,
|
||||
PositionSizeUSD: 1000,
|
||||
StopLoss: 42000,
|
||||
TakeProfit: 48000,
|
||||
Confidence: 85,
|
||||
Reasoning: "详细的推理过程...",
|
||||
}
|
||||
|
||||
data, _ := json.MarshalIndent([]Decision{example}, "", " ")
|
||||
return string(data)
|
||||
}
|
||||
|
||||
// ValidateDecisionFormat 验证决策格式是否正确
|
||||
func ValidateDecisionFormat(decisions []Decision) error {
|
||||
if len(decisions) == 0 {
|
||||
return fmt.Errorf("决策列表不能为空")
|
||||
}
|
||||
|
||||
for i, d := range decisions {
|
||||
// 必需字段检查
|
||||
if d.Symbol == "" {
|
||||
return fmt.Errorf("决策#%d: symbol不能为空", i+1)
|
||||
}
|
||||
if d.Action == "" {
|
||||
return fmt.Errorf("决策#%d: action不能为空", i+1)
|
||||
}
|
||||
if d.Reasoning == "" {
|
||||
return fmt.Errorf("决策#%d: reasoning不能为空", i+1)
|
||||
}
|
||||
|
||||
// 动作类型检查
|
||||
validActions := map[string]bool{
|
||||
"HOLD": true,
|
||||
"PARTIAL_CLOSE": true,
|
||||
"FULL_CLOSE": true,
|
||||
"ADD_POSITION": true,
|
||||
"OPEN_NEW": true,
|
||||
"WAIT": true,
|
||||
}
|
||||
if !validActions[d.Action] {
|
||||
return fmt.Errorf("决策#%d: 无效的action类型: %s", i+1, d.Action)
|
||||
}
|
||||
|
||||
// 开新仓位的必需参数检查
|
||||
if d.Action == "OPEN_NEW" {
|
||||
if d.Leverage == 0 {
|
||||
return fmt.Errorf("决策#%d: OPEN_NEW动作需要提供leverage", i+1)
|
||||
}
|
||||
if d.PositionSizeUSD == 0 {
|
||||
return fmt.Errorf("决策#%d: OPEN_NEW动作需要提供position_size_usd", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,463 @@
|
||||
package kernel
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
// TestPromptBuilder 测试提示词构建器
|
||||
func TestPromptBuilder(t *testing.T) {
|
||||
t.Run("NewPromptBuilder", func(t *testing.T) {
|
||||
builderZH := NewPromptBuilder(LangChinese)
|
||||
if builderZH == nil {
|
||||
t.Fatal("NewPromptBuilder returned nil")
|
||||
}
|
||||
if builderZH.lang != LangChinese {
|
||||
t.Error("Language not set correctly")
|
||||
}
|
||||
|
||||
builderEN := NewPromptBuilder(LangEnglish)
|
||||
if builderEN.lang != LangEnglish {
|
||||
t.Error("Language not set correctly")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("BuildSystemPrompt_Chinese", func(t *testing.T) {
|
||||
builder := NewPromptBuilder(LangChinese)
|
||||
systemPrompt := builder.BuildSystemPrompt()
|
||||
|
||||
if systemPrompt == "" {
|
||||
t.Fatal("System prompt is empty")
|
||||
}
|
||||
|
||||
// 验证包含关键内容
|
||||
mustContain := []string{
|
||||
"量化交易AI助手",
|
||||
"分析账户状态",
|
||||
"分析当前持仓",
|
||||
"分析候选币种",
|
||||
"做出决策",
|
||||
"风险优先",
|
||||
"跟踪止盈",
|
||||
"顺势交易",
|
||||
"分批操作",
|
||||
"JSON",
|
||||
"symbol",
|
||||
"action",
|
||||
"reasoning",
|
||||
}
|
||||
|
||||
for _, keyword := range mustContain {
|
||||
if !strings.Contains(systemPrompt, keyword) {
|
||||
t.Errorf("System prompt should contain '%s'", keyword)
|
||||
}
|
||||
}
|
||||
|
||||
// 验证包含所有有效的action类型
|
||||
actions := []string{"HOLD", "PARTIAL_CLOSE", "FULL_CLOSE", "ADD_POSITION", "OPEN_NEW", "WAIT"}
|
||||
for _, action := range actions {
|
||||
if !strings.Contains(systemPrompt, action) {
|
||||
t.Errorf("System prompt should mention action type '%s'", action)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("BuildSystemPrompt_English", func(t *testing.T) {
|
||||
builder := NewPromptBuilder(LangEnglish)
|
||||
systemPrompt := builder.BuildSystemPrompt()
|
||||
|
||||
if systemPrompt == "" {
|
||||
t.Fatal("System prompt is empty")
|
||||
}
|
||||
|
||||
// 验证包含关键内容
|
||||
mustContain := []string{
|
||||
"quantitative trading AI",
|
||||
"Analyze Account Status",
|
||||
"Analyze Current Positions",
|
||||
"Analyze Candidate Coins",
|
||||
"Make Decisions",
|
||||
"Risk First",
|
||||
"Trailing Take-Profit",
|
||||
"Trend Following",
|
||||
"Scale Operations",
|
||||
"JSON",
|
||||
"symbol",
|
||||
"action",
|
||||
"reasoning",
|
||||
}
|
||||
|
||||
for _, keyword := range mustContain {
|
||||
if !strings.Contains(systemPrompt, keyword) {
|
||||
t.Errorf("System prompt should contain '%s'", keyword)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("BuildUserPrompt", func(t *testing.T) {
|
||||
// 创建测试上下文
|
||||
ctx := createTestContext()
|
||||
|
||||
builderZH := NewPromptBuilder(LangChinese)
|
||||
userPromptZH := builderZH.BuildUserPrompt(ctx)
|
||||
|
||||
if userPromptZH == "" {
|
||||
t.Fatal("User prompt is empty")
|
||||
}
|
||||
|
||||
// 验证包含数据字典
|
||||
if !strings.Contains(userPromptZH, "数据字典") {
|
||||
t.Error("User prompt should contain data dictionary")
|
||||
}
|
||||
|
||||
// 验证包含账户信息
|
||||
if !strings.Contains(userPromptZH, "3079.40") { // Equity
|
||||
t.Error("User prompt should contain account equity")
|
||||
}
|
||||
|
||||
// 验证包含持仓信息
|
||||
if !strings.Contains(userPromptZH, "PIPPINUSDT") {
|
||||
t.Error("User prompt should contain position symbol")
|
||||
}
|
||||
|
||||
// 验证包含决策要求
|
||||
if !strings.Contains(userPromptZH, "现在请做出决策") {
|
||||
t.Error("User prompt should contain decision requirements")
|
||||
}
|
||||
|
||||
// 英文版本
|
||||
builderEN := NewPromptBuilder(LangEnglish)
|
||||
userPromptEN := builderEN.BuildUserPrompt(ctx)
|
||||
|
||||
if !strings.Contains(userPromptEN, "Data Dictionary") {
|
||||
t.Error("English user prompt should contain data dictionary")
|
||||
}
|
||||
|
||||
if !strings.Contains(userPromptEN, "Make Your Decision Now") {
|
||||
t.Error("English user prompt should contain decision requirements")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestValidateDecisionFormat 测试决策格式验证
|
||||
func TestValidateDecisionFormat(t *testing.T) {
|
||||
t.Run("ValidDecision", func(t *testing.T) {
|
||||
decisions := []Decision{
|
||||
{
|
||||
Symbol: "BTCUSDT",
|
||||
Action: "OPEN_NEW",
|
||||
Leverage: 3,
|
||||
PositionSizeUSD: 1000,
|
||||
StopLoss: 42000,
|
||||
TakeProfit: 48000,
|
||||
Confidence: 85,
|
||||
Reasoning: "详细的推理过程",
|
||||
},
|
||||
}
|
||||
|
||||
err := ValidateDecisionFormat(decisions)
|
||||
if err != nil {
|
||||
t.Errorf("Valid decision should not return error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("EmptyDecisions", func(t *testing.T) {
|
||||
decisions := []Decision{}
|
||||
|
||||
err := ValidateDecisionFormat(decisions)
|
||||
if err == nil {
|
||||
t.Error("Empty decisions should return error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "不能为空") {
|
||||
t.Errorf("Error message should mention '不能为空', got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MissingSymbol", func(t *testing.T) {
|
||||
decisions := []Decision{
|
||||
{
|
||||
Symbol: "", // Missing
|
||||
Action: "HOLD",
|
||||
Reasoning: "Test",
|
||||
},
|
||||
}
|
||||
|
||||
err := ValidateDecisionFormat(decisions)
|
||||
if err == nil {
|
||||
t.Error("Missing symbol should return error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "symbol") {
|
||||
t.Errorf("Error should mention 'symbol', got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MissingAction", func(t *testing.T) {
|
||||
decisions := []Decision{
|
||||
{
|
||||
Symbol: "BTCUSDT",
|
||||
Action: "", // Missing
|
||||
Reasoning: "Test",
|
||||
},
|
||||
}
|
||||
|
||||
err := ValidateDecisionFormat(decisions)
|
||||
if err == nil {
|
||||
t.Error("Missing action should return error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MissingReasoning", func(t *testing.T) {
|
||||
decisions := []Decision{
|
||||
{
|
||||
Symbol: "BTCUSDT",
|
||||
Action: "HOLD",
|
||||
Reasoning: "", // Missing
|
||||
},
|
||||
}
|
||||
|
||||
err := ValidateDecisionFormat(decisions)
|
||||
if err == nil {
|
||||
t.Error("Missing reasoning should return error")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("InvalidAction", func(t *testing.T) {
|
||||
decisions := []Decision{
|
||||
{
|
||||
Symbol: "BTCUSDT",
|
||||
Action: "INVALID_ACTION",
|
||||
Reasoning: "Test",
|
||||
},
|
||||
}
|
||||
|
||||
err := ValidateDecisionFormat(decisions)
|
||||
if err == nil {
|
||||
t.Error("Invalid action should return error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "无效的action") {
|
||||
t.Errorf("Error should mention '无效的action', got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("OpenNewMissingLeverage", func(t *testing.T) {
|
||||
decisions := []Decision{
|
||||
{
|
||||
Symbol: "BTCUSDT",
|
||||
Action: "OPEN_NEW",
|
||||
Leverage: 0, // Missing
|
||||
PositionSizeUSD: 1000,
|
||||
Reasoning: "Test",
|
||||
},
|
||||
}
|
||||
|
||||
err := ValidateDecisionFormat(decisions)
|
||||
if err == nil {
|
||||
t.Error("OPEN_NEW without leverage should return error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "leverage") {
|
||||
t.Errorf("Error should mention 'leverage', got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("OpenNewMissingPositionSize", func(t *testing.T) {
|
||||
decisions := []Decision{
|
||||
{
|
||||
Symbol: "BTCUSDT",
|
||||
Action: "OPEN_NEW",
|
||||
Leverage: 3,
|
||||
PositionSizeUSD: 0, // Missing
|
||||
Reasoning: "Test",
|
||||
},
|
||||
}
|
||||
|
||||
err := ValidateDecisionFormat(decisions)
|
||||
if err == nil {
|
||||
t.Error("OPEN_NEW without position_size_usd should return error")
|
||||
}
|
||||
|
||||
if !strings.Contains(err.Error(), "position_size_usd") {
|
||||
t.Errorf("Error should mention 'position_size_usd', got: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("MultipleDecisions", func(t *testing.T) {
|
||||
decisions := []Decision{
|
||||
{
|
||||
Symbol: "BTCUSDT",
|
||||
Action: "HOLD",
|
||||
Reasoning: "Hold BTC",
|
||||
},
|
||||
{
|
||||
Symbol: "ETHUSDT",
|
||||
Action: "OPEN_NEW",
|
||||
Leverage: 3,
|
||||
PositionSizeUSD: 500,
|
||||
Reasoning: "Open ETH",
|
||||
},
|
||||
}
|
||||
|
||||
err := ValidateDecisionFormat(decisions)
|
||||
if err != nil {
|
||||
t.Errorf("Multiple valid decisions should not return error: %v", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ValidActions", func(t *testing.T) {
|
||||
validActions := []string{"HOLD", "PARTIAL_CLOSE", "FULL_CLOSE", "ADD_POSITION", "OPEN_NEW", "WAIT"}
|
||||
|
||||
for _, action := range validActions {
|
||||
decisions := []Decision{
|
||||
{
|
||||
Symbol: "BTCUSDT",
|
||||
Action: action,
|
||||
Reasoning: "Test " + action,
|
||||
},
|
||||
}
|
||||
|
||||
// OPEN_NEW需要额外字段
|
||||
if action == "OPEN_NEW" {
|
||||
decisions[0].Leverage = 3
|
||||
decisions[0].PositionSizeUSD = 1000
|
||||
}
|
||||
|
||||
err := ValidateDecisionFormat(decisions)
|
||||
if err != nil {
|
||||
t.Errorf("Valid action '%s' should not return error: %v", action, err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestFormatDecisionExample 测试决策示例格式化
|
||||
func TestFormatDecisionExample(t *testing.T) {
|
||||
t.Run("Chinese", func(t *testing.T) {
|
||||
example := FormatDecisionExample(LangChinese)
|
||||
|
||||
if example == "" {
|
||||
t.Fatal("Decision example is empty")
|
||||
}
|
||||
|
||||
// 应该是有效的JSON
|
||||
if !strings.HasPrefix(strings.TrimSpace(example), "[") {
|
||||
t.Error("Example should be a JSON array")
|
||||
}
|
||||
|
||||
if !strings.Contains(example, "BTCUSDT") {
|
||||
t.Error("Example should contain BTCUSDT")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("English", func(t *testing.T) {
|
||||
example := FormatDecisionExample(LangEnglish)
|
||||
|
||||
if example == "" {
|
||||
t.Fatal("Decision example is empty")
|
||||
}
|
||||
|
||||
// 验证是有效的JSON格式
|
||||
if !strings.HasPrefix(strings.TrimSpace(example), "[") {
|
||||
t.Error("Example should be a JSON array")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkBuildSystemPrompt 性能测试
|
||||
func BenchmarkBuildSystemPrompt(b *testing.B) {
|
||||
builder := NewPromptBuilder(LangChinese)
|
||||
|
||||
b.Run("Chinese", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = builder.BuildSystemPrompt()
|
||||
}
|
||||
})
|
||||
|
||||
builderEN := NewPromptBuilder(LangEnglish)
|
||||
b.Run("English", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = builderEN.BuildSystemPrompt()
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkBuildUserPrompt 性能测试
|
||||
func BenchmarkBuildUserPrompt(b *testing.B) {
|
||||
builder := NewPromptBuilder(LangChinese)
|
||||
ctx := createTestContext()
|
||||
|
||||
b.Run("Chinese", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = builder.BuildUserPrompt(ctx)
|
||||
}
|
||||
})
|
||||
|
||||
builderEN := NewPromptBuilder(LangEnglish)
|
||||
b.Run("English", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = builderEN.BuildUserPrompt(ctx)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// createTestContext 创建测试用的交易上下文
|
||||
func createTestContext() *Context {
|
||||
return &Context{
|
||||
CurrentTime: time.Now().UTC().Format("2006-01-02 15:04:05 UTC"),
|
||||
RuntimeMinutes: 78,
|
||||
CallCount: 27,
|
||||
Account: AccountInfo{
|
||||
TotalEquity: 3079.40,
|
||||
AvailableBalance: 2353.02,
|
||||
UnrealizedPnL: 21.48,
|
||||
TotalPnL: 470.89,
|
||||
TotalPnLPct: 15.87,
|
||||
MarginUsed: 726.38,
|
||||
MarginUsedPct: 23.6,
|
||||
PositionCount: 1,
|
||||
},
|
||||
Positions: []PositionInfo{
|
||||
{
|
||||
Symbol: "PIPPINUSDT",
|
||||
Side: "long",
|
||||
EntryPrice: 0.4888,
|
||||
MarkPrice: 0.4937,
|
||||
Quantity: 4414.0,
|
||||
Leverage: 3,
|
||||
UnrealizedPnL: 21.48,
|
||||
UnrealizedPnLPct: 2.96,
|
||||
PeakPnLPct: 2.99,
|
||||
LiquidationPrice: 0.0000,
|
||||
MarginUsed: 726.0,
|
||||
UpdateTime: time.Now().UnixMilli(),
|
||||
},
|
||||
},
|
||||
RecentOrders: []RecentOrder{
|
||||
{
|
||||
Symbol: "PIPPINUSDT",
|
||||
Side: "long",
|
||||
EntryPrice: 0.4756,
|
||||
ExitPrice: 0.4862,
|
||||
RealizedPnL: 46.10,
|
||||
PnLPct: 6.71,
|
||||
EntryTime: "12-24 04:36 UTC",
|
||||
ExitTime: "12-24 05:35 UTC",
|
||||
HoldDuration: "58m",
|
||||
},
|
||||
},
|
||||
CandidateCoins: []CandidateCoin{
|
||||
{
|
||||
Symbol: "BTCUSDT",
|
||||
Sources: []string{"ai500"},
|
||||
},
|
||||
{
|
||||
Symbol: "ETHUSDT",
|
||||
Sources: []string{"oi_top"},
|
||||
},
|
||||
},
|
||||
Timeframes: []string{"5M", "15M", "1H", "4H"},
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,597 @@
|
||||
package kernel
|
||||
|
||||
import "fmt"
|
||||
|
||||
// ============================================================================
|
||||
// Trading Data Schema - 交易数据字典
|
||||
// ============================================================================
|
||||
// 双语数据字典,支持中文和英文
|
||||
// 确保AI能够100%理解数据格式,无论使用哪种语言
|
||||
// ============================================================================
|
||||
|
||||
const (
|
||||
SchemaVersion = "1.0.0"
|
||||
)
|
||||
|
||||
// Language 语言类型
|
||||
type Language string
|
||||
|
||||
const (
|
||||
LangChinese Language = "zh-CN"
|
||||
LangEnglish Language = "en-US"
|
||||
)
|
||||
|
||||
// ========== 双语字段定义 ==========
|
||||
|
||||
// BilingualFieldDef 双语字段定义
|
||||
type BilingualFieldDef struct {
|
||||
NameZH string // 中文名称
|
||||
NameEN string // English name
|
||||
Unit string // 单位
|
||||
FormulaZH string // 中文公式
|
||||
FormulaEN string // English formula
|
||||
DescZH string // 中文描述
|
||||
DescEN string // English description
|
||||
}
|
||||
|
||||
// GetName 获取字段名称(根据语言)
|
||||
func (d BilingualFieldDef) GetName(lang Language) string {
|
||||
if lang == LangChinese {
|
||||
return d.NameZH
|
||||
}
|
||||
return d.NameEN
|
||||
}
|
||||
|
||||
// GetFormula 获取公式(根据语言)
|
||||
func (d BilingualFieldDef) GetFormula(lang Language) string {
|
||||
if lang == LangChinese {
|
||||
return d.FormulaZH
|
||||
}
|
||||
return d.FormulaEN
|
||||
}
|
||||
|
||||
// GetDesc 获取描述(根据语言)
|
||||
func (d BilingualFieldDef) GetDesc(lang Language) string {
|
||||
if lang == LangChinese {
|
||||
return d.DescZH
|
||||
}
|
||||
return d.DescEN
|
||||
}
|
||||
|
||||
// ========== 数据字典 ==========
|
||||
|
||||
// DataDictionary 数据字典:定义所有字段的含义
|
||||
var DataDictionary = map[string]map[string]BilingualFieldDef{
|
||||
"AccountMetrics": {
|
||||
"Equity": {
|
||||
NameZH: "总权益",
|
||||
NameEN: "Total Equity",
|
||||
Unit: "USDT",
|
||||
FormulaZH: "可用余额 + 未实现盈亏",
|
||||
FormulaEN: "Available Balance + Unrealized PnL",
|
||||
DescZH: "账户的实际净值,包含所有持仓的浮动盈亏",
|
||||
DescEN: "Actual account value including all unrealized P&L from positions",
|
||||
},
|
||||
"Balance": {
|
||||
NameZH: "可用余额",
|
||||
NameEN: "Available Balance",
|
||||
Unit: "USDT",
|
||||
FormulaZH: "初始资金 + 已实现盈亏",
|
||||
FormulaEN: "Initial Capital + Realized PnL",
|
||||
DescZH: "可用于开新仓位的资金,不包括已用保证金",
|
||||
DescEN: "Available funds for opening new positions, excluding used margin",
|
||||
},
|
||||
"PnL": {
|
||||
NameZH: "总盈亏百分比",
|
||||
NameEN: "Total PnL Percentage",
|
||||
Unit: "%",
|
||||
FormulaZH: "(总权益 - 初始资金) / 初始资金 × 100",
|
||||
FormulaEN: "(Total Equity - Initial Capital) / Initial Capital × 100",
|
||||
DescZH: "自系统启动以来的总收益率,+15.87%表示盈利15.87%",
|
||||
DescEN: "Total return since inception, +15.87% means 15.87% profit",
|
||||
},
|
||||
"Margin": {
|
||||
NameZH: "保证金使用率",
|
||||
NameEN: "Margin Usage Rate",
|
||||
Unit: "%",
|
||||
FormulaZH: "已用保证金合计 / 总权益 × 100",
|
||||
FormulaEN: "Total Used Margin / Total Equity × 100",
|
||||
DescZH: "该值越高,账户风险越大。安全值<30%,危险值>70%",
|
||||
DescEN: "Higher value = higher risk. Safe <30%, Dangerous >70%",
|
||||
},
|
||||
},
|
||||
|
||||
"TradeMetrics": {
|
||||
"Entry": {
|
||||
NameZH: "进场价",
|
||||
NameEN: "Entry Price",
|
||||
Unit: "USDT",
|
||||
DescZH: "开仓时的平均价格",
|
||||
DescEN: "Average price when opening position",
|
||||
},
|
||||
"Exit": {
|
||||
NameZH: "出场价",
|
||||
NameEN: "Exit Price",
|
||||
Unit: "USDT",
|
||||
DescZH: "平仓时的平均价格",
|
||||
DescEN: "Average price when closing position",
|
||||
},
|
||||
"Profit": {
|
||||
NameZH: "已实现盈亏",
|
||||
NameEN: "Realized PnL",
|
||||
Unit: "USDT",
|
||||
FormulaZH: "(出场价 - 进场价) / 进场价 × 杠杆 × 仓位价值",
|
||||
FormulaEN: "(Exit Price - Entry Price) / Entry Price × Leverage × Position Value",
|
||||
DescZH: "已平仓交易的实际盈亏,包含手续费。正值=盈利,负值=亏损",
|
||||
DescEN: "Actual profit/loss of closed trades including fees. Positive=profit, Negative=loss",
|
||||
},
|
||||
"PnL%": {
|
||||
NameZH: "盈亏百分比",
|
||||
NameEN: "PnL Percentage",
|
||||
Unit: "%",
|
||||
FormulaZH: "(出场价 - 进场价) / 进场价 × 杠杆 × 100",
|
||||
FormulaEN: "(Exit - Entry) / Entry × Leverage × 100",
|
||||
DescZH: "已平仓交易的收益率,+6.71%表示盈利6.71%",
|
||||
DescEN: "Return on closed trade, +6.71% means 6.71% profit",
|
||||
},
|
||||
"HoldDuration": {
|
||||
NameZH: "持仓时长",
|
||||
NameEN: "Holding Duration",
|
||||
Unit: "minutes",
|
||||
DescZH: "从开仓到平仓的时间。<15分钟=超短线,15分钟-4小时=日内,>4小时=波段",
|
||||
DescEN: "Time from open to close. <15min=scalping, 15min-4h=intraday, >4h=swing",
|
||||
},
|
||||
},
|
||||
|
||||
"PositionMetrics": {
|
||||
"UnrealizedPnL%": {
|
||||
NameZH: "未实现盈亏百分比",
|
||||
NameEN: "Unrealized PnL Percentage",
|
||||
Unit: "%",
|
||||
FormulaZH: "(当前价 - 进场价) / 进场价 × 杠杆 × 100",
|
||||
FormulaEN: "(Current Price - Entry Price) / Entry Price × Leverage × 100",
|
||||
DescZH: "当前持仓的浮动盈亏,未平仓前是浮动的",
|
||||
DescEN: "Floating P&L of current position, not realized until closed",
|
||||
},
|
||||
"PeakPnL%": {
|
||||
NameZH: "峰值盈亏百分比",
|
||||
NameEN: "Peak PnL Percentage",
|
||||
Unit: "%",
|
||||
DescZH: "该持仓曾经达到的最高未实现盈亏。用于判断是否需要止盈",
|
||||
DescEN: "Historical max unrealized PnL for this position. Used for take-profit decisions",
|
||||
},
|
||||
"Drawdown": {
|
||||
NameZH: "从峰值回撤",
|
||||
NameEN: "Drawdown from Peak",
|
||||
Unit: "%",
|
||||
FormulaZH: "当前盈亏% - 峰值盈亏%",
|
||||
FormulaEN: "Current PnL% - Peak PnL%",
|
||||
DescZH: "负值表示正在回撤。例如:峰值+5%,当前+3%,回撤=-2%",
|
||||
DescEN: "Negative = pulling back. E.g., Peak +5%, Current +3%, Drawdown = -2%",
|
||||
},
|
||||
"Leverage": {
|
||||
NameZH: "杠杆倍数",
|
||||
NameEN: "Leverage",
|
||||
Unit: "x",
|
||||
DescZH: "3x表示价格变动1%,持仓盈亏变动3%。杠杆越高,风险越大",
|
||||
DescEN: "3x means 1% price move = 3% position PnL. Higher leverage = higher risk",
|
||||
},
|
||||
"Margin": {
|
||||
NameZH: "占用保证金",
|
||||
NameEN: "Margin Used",
|
||||
Unit: "USDT",
|
||||
FormulaZH: "仓位价值 / 杠杆",
|
||||
FormulaEN: "Position Value / Leverage",
|
||||
DescZH: "该仓位锁定的保证金金额",
|
||||
DescEN: "Collateral locked for this position",
|
||||
},
|
||||
"LiqPrice": {
|
||||
NameZH: "强平价格",
|
||||
NameEN: "Liquidation Price",
|
||||
Unit: "USDT",
|
||||
DescZH: "价格触及此值时会被强制平仓。0.0000表示无爆仓风险",
|
||||
DescEN: "Price at which position will be force-closed. 0.0000 = no liquidation risk",
|
||||
},
|
||||
},
|
||||
|
||||
"MarketData": {
|
||||
"Volume": {
|
||||
NameZH: "成交量",
|
||||
NameEN: "Volume",
|
||||
Unit: "base asset",
|
||||
DescZH: "该时间段的交易量",
|
||||
DescEN: "Trading volume in this period",
|
||||
},
|
||||
"OI": {
|
||||
NameZH: "持仓量",
|
||||
NameEN: "Open Interest",
|
||||
Unit: "USDT",
|
||||
DescZH: "未平仓合约的总价值。持仓量增加=资金流入,减少=资金流出",
|
||||
DescEN: "Total value of open contracts. Increasing OI = capital inflow, decreasing = outflow",
|
||||
},
|
||||
"OIChange": {
|
||||
NameZH: "持仓量变化",
|
||||
NameEN: "OI Change",
|
||||
Unit: "USDT & %",
|
||||
DescZH: "1小时内持仓量的变化。用于判断市场真实资金流向",
|
||||
DescEN: "OI change in 1 hour. Used to determine real capital flow direction",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ========== 双语规则定义 ==========
|
||||
|
||||
// BilingualRuleDef 双语规则定义
|
||||
type BilingualRuleDef struct {
|
||||
Value interface{} // 规则值
|
||||
DescZH string // 中文描述
|
||||
DescEN string // English description
|
||||
ReasonZH string // 中文原因
|
||||
ReasonEN string // English reason
|
||||
}
|
||||
|
||||
// GetDesc 获取描述(根据语言)
|
||||
func (d BilingualRuleDef) GetDesc(lang Language) string {
|
||||
if lang == LangChinese {
|
||||
return d.DescZH
|
||||
}
|
||||
return d.DescEN
|
||||
}
|
||||
|
||||
// GetReason 获取原因(根据语言)
|
||||
func (d BilingualRuleDef) GetReason(lang Language) string {
|
||||
if lang == LangChinese {
|
||||
return d.ReasonZH
|
||||
}
|
||||
return d.ReasonEN
|
||||
}
|
||||
|
||||
// ========== 交易规则 ==========
|
||||
|
||||
// TradingRules 交易规则定义
|
||||
var TradingRules = struct {
|
||||
RiskManagement map[string]BilingualRuleDef
|
||||
EntrySignals map[string]BilingualRuleDef
|
||||
ExitSignals map[string]BilingualRuleDef
|
||||
PositionControl map[string]BilingualRuleDef
|
||||
}{
|
||||
RiskManagement: map[string]BilingualRuleDef{
|
||||
"MaxMarginUsage": {
|
||||
Value: 0.30,
|
||||
DescZH: "保证金使用率不得超过30%",
|
||||
DescEN: "Margin usage must not exceed 30%",
|
||||
ReasonZH: "保留70%的资金应对极端行情和追加保证金",
|
||||
ReasonEN: "Reserve 70% capital for extreme market conditions and margin calls",
|
||||
},
|
||||
"MaxPositionLoss": {
|
||||
Value: -0.05,
|
||||
DescZH: "单个持仓亏损达到-5%时必须止损",
|
||||
DescEN: "Must stop-loss when single position loss reaches -5%",
|
||||
ReasonZH: "避免单笔交易造成过大损失",
|
||||
ReasonEN: "Prevent excessive loss from single trade",
|
||||
},
|
||||
"MaxDailyLoss": {
|
||||
Value: -0.10,
|
||||
DescZH: "单日亏损达到-10%时停止交易",
|
||||
DescEN: "Stop trading when daily loss reaches -10%",
|
||||
ReasonZH: "防止情绪化交易导致连续亏损",
|
||||
ReasonEN: "Prevent emotional trading leading to consecutive losses",
|
||||
},
|
||||
"PositionSizeLimit": {
|
||||
Value: 0.15,
|
||||
DescZH: "单个仓位不得超过总权益的15%",
|
||||
DescEN: "Single position must not exceed 15% of total equity",
|
||||
ReasonZH: "避免过度集中风险",
|
||||
ReasonEN: "Avoid excessive risk concentration",
|
||||
},
|
||||
},
|
||||
|
||||
EntrySignals: map[string]BilingualRuleDef{
|
||||
"VolumeSpike": {
|
||||
Value: 2.0,
|
||||
DescZH: "成交量是平均值的2倍以上时考虑进场",
|
||||
DescEN: "Consider entry when volume is 2x above average",
|
||||
ReasonZH: "放量突破通常意味着强趋势",
|
||||
ReasonEN: "Volume breakout usually indicates strong trend",
|
||||
},
|
||||
"OIChangeThreshold": {
|
||||
Value: 0.02,
|
||||
DescZH: "持仓量1小时内变化超过2%视为显著变化",
|
||||
DescEN: "OI change >2% in 1 hour is considered significant",
|
||||
ReasonZH: "大额资金进出会导致持仓量显著变化",
|
||||
ReasonEN: "Large capital flows cause significant OI changes",
|
||||
},
|
||||
},
|
||||
|
||||
ExitSignals: map[string]BilingualRuleDef{
|
||||
"TrailingStop": {
|
||||
Value: 0.30,
|
||||
DescZH: "当盈亏从峰值回撤30%时平仓止盈",
|
||||
DescEN: "Close position when PnL pulls back 30% from peak",
|
||||
ReasonZH: "锁定大部分利润,避免盈利回吐。例如:峰值+5%,回撤到+3.5%时平仓",
|
||||
ReasonEN: "Lock in most profits, avoid profit giveback. E.g., Peak +5%, close at +3.5%",
|
||||
},
|
||||
"StopLoss": {
|
||||
Value: -0.05,
|
||||
DescZH: "硬止损设置在-5%",
|
||||
DescEN: "Hard stop-loss at -5%",
|
||||
ReasonZH: "严格控制单笔最大损失",
|
||||
ReasonEN: "Strictly control maximum single-trade loss",
|
||||
},
|
||||
},
|
||||
|
||||
PositionControl: map[string]BilingualRuleDef{
|
||||
"ScaleIn": {
|
||||
Value: map[string]interface{}{"enabled": true, "max_additions": 2, "price_requirement": 0.01},
|
||||
DescZH: "只在盈利仓位上加仓,最多加2次,价格需比平均成本高1%",
|
||||
DescEN: "Only add to winning positions, max 2 additions, price must be 1% above avg cost",
|
||||
ReasonZH: "顺势加仓,不追亏损",
|
||||
ReasonEN: "Add to winners, never average down losers",
|
||||
},
|
||||
"ScaleOut": {
|
||||
Value: []map[string]interface{}{
|
||||
{"pnl": 0.03, "close_pct": 0.33},
|
||||
{"pnl": 0.05, "close_pct": 0.50},
|
||||
{"pnl": 0.08, "close_pct": 1.00},
|
||||
},
|
||||
DescZH: "分批止盈:盈利3%时平33%,5%时平50%,8%时全平",
|
||||
DescEN: "Scale-out: Close 33% at +3%, 50% at +5%, 100% at +8%",
|
||||
ReasonZH: "在保证利润的同时让盈利奔跑",
|
||||
ReasonEN: "Lock profits while letting winners run",
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
// ========== OI解读 ==========
|
||||
|
||||
// OIInterpretation OI变化的市场解读(双语)
|
||||
type OIInterpretationType struct {
|
||||
OIUp_PriceUp struct {
|
||||
ZH string
|
||||
EN string
|
||||
}
|
||||
OIUp_PriceDown struct {
|
||||
ZH string
|
||||
EN string
|
||||
}
|
||||
OIDown_PriceUp struct {
|
||||
ZH string
|
||||
EN string
|
||||
}
|
||||
OIDown_PriceDown struct {
|
||||
ZH string
|
||||
EN string
|
||||
}
|
||||
}
|
||||
|
||||
var OIInterpretation = OIInterpretationType{
|
||||
OIUp_PriceUp: struct {
|
||||
ZH string
|
||||
EN string
|
||||
}{
|
||||
ZH: "强多头趋势(新多单开仓,资金流入做多)",
|
||||
EN: "Strong bullish trend (new longs opening, capital flowing into long positions)",
|
||||
},
|
||||
OIUp_PriceDown: struct {
|
||||
ZH string
|
||||
EN string
|
||||
}{
|
||||
ZH: "强空头趋势(新空单开仓,资金流入做空)",
|
||||
EN: "Strong bearish trend (new shorts opening, capital flowing into short positions)",
|
||||
},
|
||||
OIDown_PriceUp: struct {
|
||||
ZH string
|
||||
EN string
|
||||
}{
|
||||
ZH: "空头平仓(空头止损离场,可能出现反转)",
|
||||
EN: "Shorts covering (shorts stopped out, potential reversal)",
|
||||
},
|
||||
OIDown_PriceDown: struct {
|
||||
ZH string
|
||||
EN string
|
||||
}{
|
||||
ZH: "多头平仓(多头止损离场,可能出现反转)",
|
||||
EN: "Longs closing (longs stopped out, potential reversal)",
|
||||
},
|
||||
}
|
||||
|
||||
// ========== 常见错误 ==========
|
||||
|
||||
// CommonMistake 常见错误定义
|
||||
type CommonMistake struct {
|
||||
ErrorZH string
|
||||
ErrorEN string
|
||||
ExampleZH string
|
||||
ExampleEN string
|
||||
CorrectZH string
|
||||
CorrectEN string
|
||||
}
|
||||
|
||||
var CommonMistakes = []CommonMistake{
|
||||
{
|
||||
ErrorZH: "混淆已实现盈亏和未实现盈亏",
|
||||
ErrorEN: "Confusing realized and unrealized P&L",
|
||||
ExampleZH: "将历史交易的盈亏与当前持仓的盈亏相加",
|
||||
ExampleEN: "Adding historical trade P&L with current position P&L",
|
||||
CorrectZH: "已实现盈亏已经计入账户余额,不应重复计算",
|
||||
CorrectEN: "Realized P&L is already included in account balance, don't double count",
|
||||
},
|
||||
{
|
||||
ErrorZH: "忽略杠杆对盈亏的影响",
|
||||
ErrorEN: "Ignoring leverage's impact on P&L",
|
||||
ExampleZH: "价格涨1%,认为盈利1%",
|
||||
ExampleEN: "Price up 1%, thinking profit is 1%",
|
||||
CorrectZH: "3x杠杆时,价格涨1%,实际盈利约3%",
|
||||
CorrectEN: "With 3x leverage, 1% price move = ~3% P&L",
|
||||
},
|
||||
{
|
||||
ErrorZH: "不理解Peak PnL的重要性",
|
||||
ErrorEN: "Not understanding Peak PnL's importance",
|
||||
ExampleZH: "只关注当前PnL,不关注回撤",
|
||||
ExampleEN: "Only watching current PnL, ignoring drawdown",
|
||||
CorrectZH: "当前PnL接近Peak PnL时,应考虑止盈以锁定利润",
|
||||
CorrectEN: "When current PnL near Peak PnL, consider taking profit to lock in gains",
|
||||
},
|
||||
{
|
||||
ErrorZH: "忽略持仓量(OI)变化",
|
||||
ErrorEN: "Ignoring Open Interest changes",
|
||||
ExampleZH: "只看价格K线,不看资金流向",
|
||||
ExampleEN: "Only watching price candles, not capital flows",
|
||||
CorrectZH: "结合OI变化判断趋势的真实性和持续性",
|
||||
CorrectEN: "Use OI changes to validate trend authenticity and sustainability",
|
||||
},
|
||||
}
|
||||
|
||||
// ========== Prompt生成函数 ==========
|
||||
|
||||
// GetSchemaPrompt 生成Schema说明文本,用于AI Prompt
|
||||
func GetSchemaPrompt(lang Language) string {
|
||||
if lang == LangChinese {
|
||||
return getSchemaPromptZH()
|
||||
}
|
||||
return getSchemaPromptEN()
|
||||
}
|
||||
|
||||
// getSchemaPromptZH 生成中文Prompt
|
||||
func getSchemaPromptZH() string {
|
||||
prompt := "# 📖 数据字典与交易规则\n\n"
|
||||
prompt += "## 📊 字段含义说明\n\n"
|
||||
|
||||
// 账户指标
|
||||
prompt += "### 账户指标\n"
|
||||
for key, field := range DataDictionary["AccountMetrics"] {
|
||||
prompt += formatFieldDefZH(key, field)
|
||||
}
|
||||
|
||||
// 交易指标
|
||||
prompt += "\n### 交易指标\n"
|
||||
for key, field := range DataDictionary["TradeMetrics"] {
|
||||
prompt += formatFieldDefZH(key, field)
|
||||
}
|
||||
|
||||
// 持仓指标
|
||||
prompt += "\n### 持仓指标\n"
|
||||
for key, field := range DataDictionary["PositionMetrics"] {
|
||||
prompt += formatFieldDefZH(key, field)
|
||||
}
|
||||
|
||||
// 市场数据
|
||||
prompt += "\n### 市场数据\n"
|
||||
for key, field := range DataDictionary["MarketData"] {
|
||||
prompt += formatFieldDefZH(key, field)
|
||||
}
|
||||
|
||||
// 交易规则
|
||||
prompt += "\n## ⚖️ 交易规则\n\n"
|
||||
prompt += "### 风险管理\n"
|
||||
for name, rule := range TradingRules.RiskManagement {
|
||||
prompt += "- **" + name + "**: " + rule.DescZH + "\n 理由:" + rule.ReasonZH + "\n"
|
||||
}
|
||||
|
||||
prompt += "\n### 出场信号\n"
|
||||
for name, rule := range TradingRules.ExitSignals {
|
||||
prompt += "- **" + name + "**: " + rule.DescZH + "\n 理由:" + rule.ReasonZH + "\n"
|
||||
}
|
||||
|
||||
// OI解读
|
||||
prompt += "\n## 💹 持仓量(OI)变化解读\n\n"
|
||||
prompt += "- **OI增加 + 价格上涨**: " + OIInterpretation.OIUp_PriceUp.ZH + "\n"
|
||||
prompt += "- **OI增加 + 价格下跌**: " + OIInterpretation.OIUp_PriceDown.ZH + "\n"
|
||||
prompt += "- **OI减少 + 价格上涨**: " + OIInterpretation.OIDown_PriceUp.ZH + "\n"
|
||||
prompt += "- **OI减少 + 价格下跌**: " + OIInterpretation.OIDown_PriceDown.ZH + "\n"
|
||||
|
||||
// 常见错误
|
||||
prompt += "\n## ⚠️ 常见错误(请避免)\n\n"
|
||||
for i, mistake := range CommonMistakes {
|
||||
prompt += fmt.Sprintf("**错误%d**: %s\n", i+1, mistake.ErrorZH)
|
||||
prompt += "- 错误示例:" + mistake.ExampleZH + "\n"
|
||||
prompt += "- 正确做法:" + mistake.CorrectZH + "\n\n"
|
||||
}
|
||||
|
||||
return prompt
|
||||
}
|
||||
|
||||
// getSchemaPromptEN 生成英文Prompt
|
||||
func getSchemaPromptEN() string {
|
||||
prompt := "# 📖 Data Dictionary & Trading Rules\n\n"
|
||||
prompt += "## 📊 Field Definitions\n\n"
|
||||
|
||||
// Account Metrics
|
||||
prompt += "### Account Metrics\n"
|
||||
for key, field := range DataDictionary["AccountMetrics"] {
|
||||
prompt += formatFieldDefEN(key, field)
|
||||
}
|
||||
|
||||
// Trade Metrics
|
||||
prompt += "\n### Trade Metrics\n"
|
||||
for key, field := range DataDictionary["TradeMetrics"] {
|
||||
prompt += formatFieldDefEN(key, field)
|
||||
}
|
||||
|
||||
// Position Metrics
|
||||
prompt += "\n### Position Metrics\n"
|
||||
for key, field := range DataDictionary["PositionMetrics"] {
|
||||
prompt += formatFieldDefEN(key, field)
|
||||
}
|
||||
|
||||
// Market Data
|
||||
prompt += "\n### Market Data\n"
|
||||
for key, field := range DataDictionary["MarketData"] {
|
||||
prompt += formatFieldDefEN(key, field)
|
||||
}
|
||||
|
||||
// Trading Rules
|
||||
prompt += "\n## ⚖️ Trading Rules\n\n"
|
||||
prompt += "### Risk Management\n"
|
||||
for name, rule := range TradingRules.RiskManagement {
|
||||
prompt += "- **" + name + "**: " + rule.DescEN + "\n Reason: " + rule.ReasonEN + "\n"
|
||||
}
|
||||
|
||||
prompt += "\n### Exit Signals\n"
|
||||
for name, rule := range TradingRules.ExitSignals {
|
||||
prompt += "- **" + name + "**: " + rule.DescEN + "\n Reason: " + rule.ReasonEN + "\n"
|
||||
}
|
||||
|
||||
// OI Interpretation
|
||||
prompt += "\n## 💹 Open Interest (OI) Change Interpretation\n\n"
|
||||
prompt += "- **OI Up + Price Up**: " + OIInterpretation.OIUp_PriceUp.EN + "\n"
|
||||
prompt += "- **OI Up + Price Down**: " + OIInterpretation.OIUp_PriceDown.EN + "\n"
|
||||
prompt += "- **OI Down + Price Up**: " + OIInterpretation.OIDown_PriceUp.EN + "\n"
|
||||
prompt += "- **OI Down + Price Down**: " + OIInterpretation.OIDown_PriceDown.EN + "\n"
|
||||
|
||||
// Common Mistakes
|
||||
prompt += "\n## ⚠️ Common Mistakes to Avoid\n\n"
|
||||
for i, mistake := range CommonMistakes {
|
||||
prompt += fmt.Sprintf("**Mistake %d**: %s\n", i+1, mistake.ErrorEN)
|
||||
prompt += "- Bad Example: " + mistake.ExampleEN + "\n"
|
||||
prompt += "- Correct Approach: " + mistake.CorrectEN + "\n\n"
|
||||
}
|
||||
|
||||
return prompt
|
||||
}
|
||||
|
||||
// formatFieldDefZH 格式化中文字段定义
|
||||
func formatFieldDefZH(key string, field BilingualFieldDef) string {
|
||||
result := "- **" + key + "**(" + field.NameZH + "): " + field.DescZH
|
||||
if field.FormulaZH != "" {
|
||||
result += " | 公式: `" + field.FormulaZH + "`"
|
||||
}
|
||||
if field.Unit != "" {
|
||||
result += " | 单位: " + field.Unit
|
||||
}
|
||||
result += "\n"
|
||||
return result
|
||||
}
|
||||
|
||||
// formatFieldDefEN 格式化英文字段定义
|
||||
func formatFieldDefEN(key string, field BilingualFieldDef) string {
|
||||
result := "- **" + key + "** (" + field.NameEN + "): " + field.DescEN
|
||||
if field.FormulaEN != "" {
|
||||
result += " | Formula: `" + field.FormulaEN + "`"
|
||||
}
|
||||
if field.Unit != "" {
|
||||
result += " | Unit: " + field.Unit
|
||||
}
|
||||
result += "\n"
|
||||
return result
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package kernel
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestDataDictionary 测试数据字典定义
|
||||
func TestDataDictionary(t *testing.T) {
|
||||
// 测试账户指标字典
|
||||
t.Run("AccountMetrics", func(t *testing.T) {
|
||||
equity := DataDictionary["AccountMetrics"]["Equity"]
|
||||
|
||||
if equity.NameZH != "总权益" {
|
||||
t.Errorf("Expected NameZH='总权益', got '%s'", equity.NameZH)
|
||||
}
|
||||
|
||||
if equity.NameEN != "Total Equity" {
|
||||
t.Errorf("Expected NameEN='Total Equity', got '%s'", equity.NameEN)
|
||||
}
|
||||
|
||||
if equity.Unit != "USDT" {
|
||||
t.Errorf("Expected Unit='USDT', got '%s'", equity.Unit)
|
||||
}
|
||||
|
||||
if equity.GetName(LangChinese) != "总权益" {
|
||||
t.Errorf("GetName(Chinese) failed")
|
||||
}
|
||||
|
||||
if equity.GetName(LangEnglish) != "Total Equity" {
|
||||
t.Errorf("GetName(English) failed")
|
||||
}
|
||||
})
|
||||
|
||||
// 测试持仓指标字典
|
||||
t.Run("PositionMetrics", func(t *testing.T) {
|
||||
peakPnL := DataDictionary["PositionMetrics"]["PeakPnL%"]
|
||||
|
||||
if peakPnL.NameZH == "" {
|
||||
t.Error("PeakPnL% NameZH is empty")
|
||||
}
|
||||
|
||||
if peakPnL.NameEN == "" {
|
||||
t.Error("PeakPnL% NameEN is empty")
|
||||
}
|
||||
|
||||
if !strings.Contains(peakPnL.DescZH, "峰值") {
|
||||
t.Error("PeakPnL% DescZH should contain '峰值'")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestTradingRules 测试交易规则定义
|
||||
func TestTradingRules(t *testing.T) {
|
||||
t.Run("RiskManagement", func(t *testing.T) {
|
||||
maxMargin := TradingRules.RiskManagement["MaxMarginUsage"]
|
||||
|
||||
if maxMargin.Value != 0.30 {
|
||||
t.Errorf("Expected MaxMarginUsage=0.30, got %v", maxMargin.Value)
|
||||
}
|
||||
|
||||
if maxMargin.GetDesc(LangChinese) == "" {
|
||||
t.Error("MaxMarginUsage DescZH is empty")
|
||||
}
|
||||
|
||||
if maxMargin.GetDesc(LangEnglish) == "" {
|
||||
t.Error("MaxMarginUsage DescEN is empty")
|
||||
}
|
||||
|
||||
if !strings.Contains(maxMargin.DescZH, "30%") {
|
||||
t.Error("MaxMarginUsage DescZH should mention 30%")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("ExitSignals", func(t *testing.T) {
|
||||
trailing := TradingRules.ExitSignals["TrailingStop"]
|
||||
|
||||
if trailing.Value != 0.30 {
|
||||
t.Errorf("Expected TrailingStop=0.30, got %v", trailing.Value)
|
||||
}
|
||||
|
||||
if !strings.Contains(trailing.ReasonZH, "止盈") {
|
||||
t.Error("TrailingStop ReasonZH should mention '止盈'")
|
||||
}
|
||||
|
||||
if !strings.Contains(trailing.ReasonEN, "profit") {
|
||||
t.Error("TrailingStop ReasonEN should mention 'profit'")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestOIInterpretation 测试OI解读
|
||||
func TestOIInterpretation(t *testing.T) {
|
||||
t.Run("OI_Up_Price_Up", func(t *testing.T) {
|
||||
if OIInterpretation.OIUp_PriceUp.ZH == "" {
|
||||
t.Error("OI Up + Price Up ZH is empty")
|
||||
}
|
||||
|
||||
if OIInterpretation.OIUp_PriceUp.EN == "" {
|
||||
t.Error("OI Up + Price Up EN is empty")
|
||||
}
|
||||
|
||||
if !strings.Contains(OIInterpretation.OIUp_PriceUp.ZH, "多头") {
|
||||
t.Error("OI Up + Price Up should indicate bullish trend")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestCommonMistakes 测试常见错误定义
|
||||
func TestCommonMistakes(t *testing.T) {
|
||||
if len(CommonMistakes) == 0 {
|
||||
t.Error("CommonMistakes should not be empty")
|
||||
}
|
||||
|
||||
for i, mistake := range CommonMistakes {
|
||||
if mistake.ErrorZH == "" {
|
||||
t.Errorf("Mistake #%d ErrorZH is empty", i+1)
|
||||
}
|
||||
|
||||
if mistake.ErrorEN == "" {
|
||||
t.Errorf("Mistake #%d ErrorEN is empty", i+1)
|
||||
}
|
||||
|
||||
if mistake.CorrectZH == "" {
|
||||
t.Errorf("Mistake #%d CorrectZH is empty", i+1)
|
||||
}
|
||||
|
||||
if mistake.CorrectEN == "" {
|
||||
t.Errorf("Mistake #%d CorrectEN is empty", i+1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// TestGetSchemaPrompt 测试Schema提示词生成
|
||||
func TestGetSchemaPrompt(t *testing.T) {
|
||||
t.Run("Chinese", func(t *testing.T) {
|
||||
prompt := GetSchemaPrompt(LangChinese)
|
||||
|
||||
if prompt == "" {
|
||||
t.Fatal("Chinese schema prompt is empty")
|
||||
}
|
||||
|
||||
// 验证包含关键内容
|
||||
mustContain := []string{
|
||||
"数据字典",
|
||||
"账户指标",
|
||||
"交易指标",
|
||||
"持仓指标",
|
||||
"市场数据",
|
||||
"交易规则",
|
||||
"风险管理",
|
||||
"持仓量(OI)变化解读",
|
||||
"常见错误",
|
||||
}
|
||||
|
||||
for _, keyword := range mustContain {
|
||||
if !strings.Contains(prompt, keyword) {
|
||||
t.Errorf("Chinese prompt should contain '%s'", keyword)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("English", func(t *testing.T) {
|
||||
prompt := GetSchemaPrompt(LangEnglish)
|
||||
|
||||
if prompt == "" {
|
||||
t.Fatal("English schema prompt is empty")
|
||||
}
|
||||
|
||||
// 验证包含关键内容
|
||||
mustContain := []string{
|
||||
"Data Dictionary",
|
||||
"Account Metrics",
|
||||
"Trade Metrics",
|
||||
"Position Metrics",
|
||||
"Market Data",
|
||||
"Trading Rules",
|
||||
"Risk Management",
|
||||
"Open Interest",
|
||||
"Common Mistakes",
|
||||
}
|
||||
|
||||
for _, keyword := range mustContain {
|
||||
if !strings.Contains(prompt, keyword) {
|
||||
t.Errorf("English prompt should contain '%s'", keyword)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("Consistency", func(t *testing.T) {
|
||||
promptZH := GetSchemaPrompt(LangChinese)
|
||||
promptEN := GetSchemaPrompt(LangEnglish)
|
||||
|
||||
// 两个版本都应该包含相同数量的字段定义
|
||||
// 虽然内容不同,但结构应该相似
|
||||
|
||||
zhLines := strings.Split(promptZH, "\n")
|
||||
enLines := strings.Split(promptEN, "\n")
|
||||
|
||||
// 行数应该大致相当(允许10%的差异)
|
||||
ratio := float64(len(zhLines)) / float64(len(enLines))
|
||||
if ratio < 0.9 || ratio > 1.1 {
|
||||
t.Logf("Warning: Line count difference is significant (ZH: %d, EN: %d)",
|
||||
len(zhLines), len(enLines))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// BenchmarkGetSchemaPrompt 性能测试
|
||||
func BenchmarkGetSchemaPrompt(b *testing.B) {
|
||||
b.Run("Chinese", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = GetSchemaPrompt(LangChinese)
|
||||
}
|
||||
})
|
||||
|
||||
b.Run("English", func(b *testing.B) {
|
||||
for i := 0; i < b.N; i++ {
|
||||
_ = GetSchemaPrompt(LangEnglish)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
// TestFieldDefinitionMethods 测试字段定义方法
|
||||
func TestFieldDefinitionMethods(t *testing.T) {
|
||||
field := BilingualFieldDef{
|
||||
NameZH: "测试字段",
|
||||
NameEN: "Test Field",
|
||||
Unit: "USDT",
|
||||
FormulaZH: "中文公式",
|
||||
FormulaEN: "English formula",
|
||||
DescZH: "中文描述",
|
||||
DescEN: "English description",
|
||||
}
|
||||
|
||||
// 测试GetName
|
||||
if field.GetName(LangChinese) != "测试字段" {
|
||||
t.Error("GetName(Chinese) failed")
|
||||
}
|
||||
if field.GetName(LangEnglish) != "Test Field" {
|
||||
t.Error("GetName(English) failed")
|
||||
}
|
||||
|
||||
// 测试GetFormula
|
||||
if field.GetFormula(LangChinese) != "中文公式" {
|
||||
t.Error("GetFormula(Chinese) failed")
|
||||
}
|
||||
if field.GetFormula(LangEnglish) != "English formula" {
|
||||
t.Error("GetFormula(English) failed")
|
||||
}
|
||||
|
||||
// 测试GetDesc
|
||||
if field.GetDesc(LangChinese) != "中文描述" {
|
||||
t.Error("GetDesc(Chinese) failed")
|
||||
}
|
||||
if field.GetDesc(LangEnglish) != "English description" {
|
||||
t.Error("GetDesc(English) failed")
|
||||
}
|
||||
}
|
||||
|
||||
// TestRuleDefinitionMethods 测试规则定义方法
|
||||
func TestRuleDefinitionMethods(t *testing.T) {
|
||||
rule := BilingualRuleDef{
|
||||
Value: 0.30,
|
||||
DescZH: "中文描述",
|
||||
DescEN: "English description",
|
||||
ReasonZH: "中文原因",
|
||||
ReasonEN: "English reason",
|
||||
}
|
||||
|
||||
if rule.GetDesc(LangChinese) != "中文描述" {
|
||||
t.Error("GetDesc(Chinese) failed")
|
||||
}
|
||||
if rule.GetDesc(LangEnglish) != "English description" {
|
||||
t.Error("GetDesc(English) failed")
|
||||
}
|
||||
|
||||
if rule.GetReason(LangChinese) != "中文原因" {
|
||||
t.Error("GetReason(Chinese) failed")
|
||||
}
|
||||
if rule.GetReason(LangEnglish) != "English reason" {
|
||||
t.Error("GetReason(English) failed")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package kernel
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestLeverageFallback tests automatic correction when leverage exceeds limit
|
||||
func TestLeverageFallback(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
decision Decision
|
||||
accountEquity float64
|
||||
btcEthLeverage int
|
||||
altcoinLeverage int
|
||||
wantLeverage int // Expected leverage after correction
|
||||
wantError bool
|
||||
}{
|
||||
{
|
||||
name: "Altcoin leverage exceeded - auto-correct to limit",
|
||||
decision: Decision{
|
||||
Symbol: "SOLUSDT",
|
||||
Action: "open_long",
|
||||
Leverage: 20, // Exceeds limit
|
||||
PositionSizeUSD: 100,
|
||||
StopLoss: 50,
|
||||
TakeProfit: 200,
|
||||
},
|
||||
accountEquity: 100,
|
||||
btcEthLeverage: 10,
|
||||
altcoinLeverage: 5, // Limit 5x
|
||||
wantLeverage: 5, // Should be corrected to 5
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "BTC leverage exceeded - auto-correct to limit",
|
||||
decision: Decision{
|
||||
Symbol: "BTCUSDT",
|
||||
Action: "open_long",
|
||||
Leverage: 20, // Exceeds limit
|
||||
PositionSizeUSD: 1000,
|
||||
StopLoss: 90000,
|
||||
TakeProfit: 110000,
|
||||
},
|
||||
accountEquity: 100,
|
||||
btcEthLeverage: 10, // Limit 10x
|
||||
altcoinLeverage: 5,
|
||||
wantLeverage: 10, // Should be corrected to 10
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Leverage within limit - no correction",
|
||||
decision: Decision{
|
||||
Symbol: "ETHUSDT",
|
||||
Action: "open_short",
|
||||
Leverage: 5, // Not exceeded
|
||||
PositionSizeUSD: 500,
|
||||
StopLoss: 4000,
|
||||
TakeProfit: 3000,
|
||||
},
|
||||
accountEquity: 100,
|
||||
btcEthLeverage: 10,
|
||||
altcoinLeverage: 5,
|
||||
wantLeverage: 5, // Stays unchanged
|
||||
wantError: false,
|
||||
},
|
||||
{
|
||||
name: "Leverage is 0 - should error",
|
||||
decision: Decision{
|
||||
Symbol: "SOLUSDT",
|
||||
Action: "open_long",
|
||||
Leverage: 0, // Invalid
|
||||
PositionSizeUSD: 100,
|
||||
StopLoss: 50,
|
||||
TakeProfit: 200,
|
||||
},
|
||||
accountEquity: 100,
|
||||
btcEthLeverage: 10,
|
||||
altcoinLeverage: 5,
|
||||
wantLeverage: 0,
|
||||
wantError: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Use default position value ratios for testing (10x for BTC/ETH, 1.5x for altcoins)
|
||||
err := validateDecision(&tt.decision, tt.accountEquity, tt.btcEthLeverage, tt.altcoinLeverage, 10.0, 1.5)
|
||||
|
||||
// Check error status
|
||||
if (err != nil) != tt.wantError {
|
||||
t.Errorf("validateDecision() error = %v, wantError %v", err, tt.wantError)
|
||||
return
|
||||
}
|
||||
|
||||
// If shouldn't error, check if leverage was correctly corrected
|
||||
if !tt.wantError && tt.decision.Leverage != tt.wantLeverage {
|
||||
t.Errorf("Leverage not corrected: got %d, want %d", tt.decision.Leverage, tt.wantLeverage)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// contains checks if string contains substring (helper function)
|
||||
func contains(s, substr string) bool {
|
||||
return len(s) >= len(substr) && (s == substr || len(substr) == 0 ||
|
||||
(len(s) > 0 && len(substr) > 0 && stringContains(s, substr)))
|
||||
}
|
||||
|
||||
func stringContains(s, substr string) bool {
|
||||
for i := 0; i <= len(s)-len(substr); i++ {
|
||||
if s[i:i+len(substr)] == substr {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
Reference in New Issue
Block a user