Refine strategy creation flow and diagnostics

This commit is contained in:
lky-spec
2026-05-09 14:48:24 +08:00
parent 0f11be77f8
commit e67a927a4f
29 changed files with 3410 additions and 1305 deletions

View File

@@ -21,25 +21,6 @@ func hasExplicitCreateIntentForDomain(text, domain string) bool {
return containsAny(lower, []string{"创建", "新建", "创一个", "创个", "建一个", "create", "new"})
}
func hasExplicitDiagnosisIntentForDomain(text, domain string) bool {
lower := strings.ToLower(strings.TrimSpace(text))
if lower == "" || !hasExplicitManagementDomainCue(text, domain) {
return false
}
switch strings.TrimSpace(domain) {
case "trader":
return containsAny(lower, []string{"启动失败", "不交易", "没开仓", "无法启动", "诊断", "报错", "错误", "diagnose", "not trading"})
case "strategy":
return containsAny(lower, []string{"不生效", "没生效", "失效", "不一致", "诊断", "报错", "错误", "diagnose"})
case "model":
return containsAny(lower, []string{"api key", "base url", "custom_api_url", "模型配置失败", "模型不可用", "ai unavailable", "无效", "报错", "错误", "失败", "不可用", "invalid", "error", "failed", "诊断", "diagnose"})
case "exchange":
return containsAny(lower, []string{"invalid signature", "timestamp", "ip not allowed", "permission denied", "签名错误", "签名失败", "时间戳", "白名单", "权限不足", "交易所 api 报错", "交易所连接不上", "报错", "错误", "失败", "诊断", "diagnose"})
default:
return false
}
}
func extractURL(text string) string {
return strings.TrimSpace(urlPattern.FindString(text))
}
@@ -345,14 +326,12 @@ func (a *Agent) handleStrategyManagementSkill(storeUserID string, userID int64,
}
}
// strategyCreateDraftConfigField stores the materialized, product-normalized
// draft between turns. User-visible strategy proposals should still be rendered
// from the post-merge structured config, not from free-form LLM text.
const strategyCreateDraftConfigField = "strategy_create_draft_config"
const strategyCreateConfigPatchField = "config_patch"
func applyStrategyCreateIntentToConfig(cfg *store.StrategyConfig, text, lang string) []string {
draft := applyStrategyDraftText(strategyDraft{}, text)
return applyStrategyDraftToConfig(cfg, draft)
}
func marshalStrategyCreateDraft(cfg store.StrategyConfig) string {
raw, err := json.Marshal(cfg)
if err != nil {
@@ -373,18 +352,8 @@ func unmarshalStrategyCreateDraft(raw, lang string) store.StrategyConfig {
}
func strategyCreateConfigFromSession(session skillSession, lang string) (store.StrategyConfig, map[string]any, []string, error) {
normalizeLegacyStrategyCreateSession(&session)
cfg := unmarshalStrategyCreateDraft(fieldValue(session, strategyCreateDraftConfigField), lang)
for _, key := range manualStrategyEditableFieldKeys() {
switch key {
case "name", "description", "is_public", "config_visible":
continue
}
if value := fieldValue(session, key); strings.TrimSpace(value) != "" {
if err := applyStrategyConfigPatch(&cfg, key, value); err != nil {
return cfg, nil, nil, err
}
}
}
patchRaw := strings.TrimSpace(fieldValue(session, strategyCreateConfigPatchField))
var patch map[string]any
if patchRaw != "" {
@@ -397,15 +366,9 @@ func strategyCreateConfigFromSession(session skillSession, lang string) (store.S
}
cfg = merged
}
if draftRaw := strings.TrimSpace(fieldValue(session, strategyCreateDraftIntentField)); draftRaw != "" {
applyStrategyDraftToConfig(&cfg, unmarshalStrategyDraft(draftRaw))
}
applyStrategyCreateTypeDefaults(&cfg)
beforeClamp := cfg
cfg.ClampLimits()
if strings.TrimSpace(cfg.StrategyType) == "" {
cfg.StrategyType = "ai_trading"
}
rawCfg, _ := json.Marshal(cfg)
var configMap map[string]any
_ = json.Unmarshal(rawCfg, &configMap)
@@ -418,11 +381,6 @@ func resolveStrategyCreateName(session *skillSession, text string) string {
return ""
}
name := strings.TrimSpace(fieldValue(*session, "name"))
if name == "" {
if draft := unmarshalStrategyDraft(fieldValue(*session, strategyCreateDraftIntentField)); strings.TrimSpace(draft.Name) != "" {
name = strings.TrimSpace(draft.Name)
}
}
if name == "" {
if inferred := inferStandaloneStrategyName(text); inferred != "" {
name = inferred
@@ -434,6 +392,78 @@ func resolveStrategyCreateName(session *skillSession, text string) string {
return name
}
func normalizeLegacyStrategyCreateSession(session *skillSession) {
if session == nil || session.Action != "create" {
return
}
strategyType := explicitStrategyCreateType(*session)
if strategyType == "" {
return
}
filterLegacyStrategyCreateFieldsForType(session, strategyType)
if patchRaw := strings.TrimSpace(fieldValue(*session, strategyCreateConfigPatchField)); patchRaw != "" {
if sanitized := sanitizeStrategyCreateConfigPatchForType(patchRaw, strategyType); len(sanitized) > 0 {
raw, _ := json.Marshal(sanitized)
setField(session, strategyCreateConfigPatchField, string(raw))
} else {
delete(session.Fields, strategyCreateConfigPatchField)
}
}
}
func filterLegacyStrategyCreateFieldsForType(session *skillSession, strategyType string) {
if session == nil || len(session.Fields) == 0 {
return
}
allowed := map[string]struct{}{}
for _, key := range []string{
"name",
"description",
"is_public",
"config_visible",
"lang",
"strategy_type",
strategyCreateDraftConfigField,
strategyCreateConfigPatchField,
skillDAGStepField,
"awaiting_final_confirmation",
} {
allowed[key] = struct{}{}
}
for key := range session.Fields {
if _, ok := allowed[key]; !ok {
delete(session.Fields, key)
}
}
}
func resetLegacyStrategyCreateSessionForType(session *skillSession, strategyType string) {
if session == nil {
return
}
keep := map[string]string{}
for _, key := range []string{"name", "description", "is_public", "config_visible", "lang"} {
if value := fieldValue(*session, key); strings.TrimSpace(value) != "" {
keep[key] = value
}
}
session.Fields = keep
setField(session, "strategy_type", strategyType)
}
func setStrategyCreateType(session *skillSession, strategyType string) {
if session == nil || strategyType == "" {
return
}
current := explicitStrategyCreateType(*session)
if current != "" && current != strategyType {
resetLegacyStrategyCreateSessionForType(session, strategyType)
return
}
setField(session, "strategy_type", strategyType)
filterLegacyStrategyCreateFieldsForType(session, strategyType)
}
func applyStrategyCreateTypeDefaults(cfg *store.StrategyConfig) {
if cfg == nil {
return
@@ -488,9 +518,22 @@ func removeLockedStrategyCreateFields(configMap map[string]any) {
return
}
risk, ok := configMap["risk_control"].(map[string]any)
if !ok {
return
if ok {
removeLockedAIRiskFields(risk)
}
if aiConfig, ok := configMap["ai_config"].(map[string]any); ok {
if risk, ok := aiConfig["risk_control"].(map[string]any); ok {
removeLockedAIRiskFields(risk)
}
}
}
func removeLockedAIRiskFields(risk map[string]any) {
delete(risk, "max_positions")
delete(risk, "btc_eth_max_position_value_ratio")
delete(risk, "btceth_max_position_value_ratio")
delete(risk, "altcoin_max_position_value_ratio")
delete(risk, "max_margin_usage")
delete(risk, "min_position_size")
}
@@ -499,7 +542,10 @@ func strategyCreateConfirmationReply(text string) bool {
if lower == "" {
return false
}
for _, exact := range []string{"确认创建", "创建吧", "就按这个创建", "按这个创建", "确认应用", "就按这个应用"} {
for _, exact := range []string{
"确认创建", "确认", "创建吧", "就按这个创建", "按这个创建", "确认应用", "就按这个应用",
"可以", "好的", "好", "没问题", "就这样", "按这个", "ok", "okay", "yes", "yep", "looks good",
} {
if lower == exact {
return true
}
@@ -522,9 +568,6 @@ func explicitStrategyCreateType(session skillSession) string {
if value := strings.TrimSpace(fieldValue(session, "strategy_type")); value != "" {
return value
}
if draft := unmarshalStrategyDraft(fieldValue(session, strategyCreateDraftIntentField)); draft.StrategyKind != "" || len(draft.Symbols) > 0 || draft.Timeframe != "" || draft.Leverage > 0 {
return "ai_trading"
}
patchRaw := strings.TrimSpace(fieldValue(session, strategyCreateConfigPatchField))
if patchRaw == "" {
return ""
@@ -539,6 +582,9 @@ func explicitStrategyCreateType(session skillSession) string {
if gridConfig, ok := patch["grid_config"]; ok && gridConfig != nil {
return "grid_trading"
}
if aiConfig, ok := patch["ai_config"]; ok && aiConfig != nil {
return "ai_trading"
}
return ""
}
@@ -547,33 +593,10 @@ func strategyCreateConfigReady(session skillSession, cfg store.StrategyConfig, t
if strategyType == "" {
return false, "strategy_type"
}
if strategyCreateDefaultConfigReply(text) || strategyCreateConfirmationReply(text) || strategyCreateFinalConfirmationReady(session) {
return true, ""
}
if !strategyCreateHasExplicitConfigBeyondType(session) {
return false, strategyType
}
switch strategyType {
case "grid_trading":
grid := cfg.GridConfig
if grid == nil {
return false, "grid_trading"
}
if strings.TrimSpace(grid.Symbol) == "" || grid.GridCount <= 0 || grid.TotalInvestment <= 0 || grid.Leverage <= 0 {
return false, "grid_trading"
}
if !grid.UseATRBounds && (grid.UpperPrice <= 0 || grid.LowerPrice <= 0) {
return false, "grid_trading"
}
return true, ""
case "ai_trading":
if strings.TrimSpace(cfg.CoinSource.SourceType) == "" || strings.TrimSpace(cfg.Indicators.Klines.PrimaryTimeframe) == "" {
return false, "ai_trading"
}
return true, ""
default:
return false, "strategy_type"
if missing := strategyCreateMissingTemplateFields(session, cfg); len(missing) > 0 {
return false, strings.Join(missing, ",")
}
return true, ""
}
func strategyCreateFinalConfirmationReady(session skillSession) bool {
@@ -590,9 +613,6 @@ func strategyCreateHasExplicitConfigBeyondType(session skillSession) bool {
return true
}
}
if draft := unmarshalStrategyDraft(fieldValue(session, strategyCreateDraftIntentField)); len(draft.Symbols) > 0 || draft.Timeframe != "" || draft.Leverage > 0 || draft.CoinSourceIntent != "" {
return true
}
patchRaw := strings.TrimSpace(fieldValue(session, strategyCreateConfigPatchField))
if patchRaw == "" {
return false
@@ -609,25 +629,415 @@ func strategyCreateHasExplicitConfigBeyondType(session skillSession) bool {
return false
}
func formatStrategyCreateConfigNeeded(lang, strategyType string) string {
if lang == "zh" {
switch strategyType {
case "grid_trading":
return "我先按一套安全默认网格参数整理完整草稿,不逐项问你。你可以直接改任何一项,或者确认后我创建。"
case "ai_trading":
return "我先按一套安全默认 AI 策略参数整理完整草稿,不逐项问你。你可以直接改任何一项,或者确认后我创建。"
default:
return "先选择策略类型grid_trading网格策略或 ai_tradingAI 策略)。类型确认后我会继续收集对应配置,配置好后再创建。"
func strategyCreateMissingTemplateFields(session skillSession, cfg store.StrategyConfig) []string {
switch explicitStrategyCreateType(session) {
case "ai_trading":
return strategyCreateMissingAIFields(session, cfg)
case "grid_trading":
return strategyCreateMissingGridFields(session)
default:
return []string{"strategy_type"}
}
}
func strategyCreateMissingAIFields(session skillSession, cfg store.StrategyConfig) []string {
required := []string{
"source_type",
"primary_timeframe",
"selected_timeframes",
"btceth_max_leverage",
"altcoin_max_leverage",
"min_confidence",
"min_risk_reward_ratio",
"trading_frequency",
"entry_standards",
}
missing := make([]string, 0, len(required)+1)
for _, field := range required {
if !strategyCreateFieldExplicit(session, field) {
missing = append(missing, field)
}
}
switch strategyType {
case "grid_trading":
return "I prepared a complete safe default grid draft instead of asking field by field. You can change any field or confirm to create it."
case "ai_trading":
return "I prepared a complete safe default AI draft instead of asking field by field. You can change any field or confirm to create it."
if strings.EqualFold(strings.TrimSpace(cfg.CoinSource.SourceType), "static") && !strategyCreateFieldExplicit(session, "static_coins") {
missing = append(missing, "static_coins")
}
return missing
}
func strategyCreateMissingGridFields(session skillSession) []string {
required := []string{
"symbol",
"grid_count",
"total_investment",
"leverage",
"distribution",
"max_drawdown_pct",
"stop_loss_pct",
"daily_loss_limit_pct",
"use_maker_only",
}
missing := make([]string, 0, len(required)+1)
for _, field := range required {
if !strategyCreateFieldExplicit(session, field) {
missing = append(missing, field)
}
}
if !strategyCreateFieldExplicit(session, "use_atr_bounds") && (!strategyCreateFieldExplicit(session, "upper_price") || !strategyCreateFieldExplicit(session, "lower_price")) {
missing = append(missing, "use_atr_bounds 或 upper_price/lower_price")
}
return missing
}
func strategyCreateFieldExplicit(session skillSession, field string) bool {
field = strings.TrimSpace(field)
if field == "" {
return false
}
if strings.TrimSpace(fieldValue(session, field)) != "" {
return true
}
patchRaw := strings.TrimSpace(fieldValue(session, strategyCreateConfigPatchField))
if patchRaw == "" {
return false
}
var patch map[string]any
if err := json.Unmarshal([]byte(patchRaw), &patch); err != nil {
return false
}
for _, path := range strategyCreatePatchPaths(field) {
if strategyCreatePatchHasPath(patch, path...) {
return true
}
}
return false
}
func strategyCreatePatchPaths(field string) [][]string {
switch strings.TrimSpace(field) {
case "strategy_type":
return [][]string{{"strategy_type"}}
case "source_type":
return [][]string{
{"ai_config", "coin_source", "source_type"}, {"coin_source", "source_type"},
{"ai_config", "coin_source", "static_coins"}, {"coin_source", "static_coins"},
{"ai_config", "coin_source", "use_ai500"}, {"coin_source", "use_ai500"},
{"ai_config", "coin_source", "use_oi_top"}, {"coin_source", "use_oi_top"},
{"ai_config", "coin_source", "use_oi_low"}, {"coin_source", "use_oi_low"},
}
case "static_coins":
return [][]string{{"ai_config", "coin_source", "static_coins"}, {"coin_source", "static_coins"}}
case "primary_timeframe":
return [][]string{{"ai_config", "indicators", "klines", "primary_timeframe"}, {"indicators", "klines", "primary_timeframe"}}
case "selected_timeframes":
return [][]string{{"ai_config", "indicators", "klines", "selected_timeframes"}, {"indicators", "klines", "selected_timeframes"}}
case "btceth_max_leverage":
return [][]string{{"ai_config", "risk_control", "btc_eth_max_leverage"}, {"risk_control", "btc_eth_max_leverage"}, {"ai_config", "risk_control", "btceth_max_leverage"}, {"risk_control", "btceth_max_leverage"}}
case "altcoin_max_leverage":
return [][]string{{"ai_config", "risk_control", "altcoin_max_leverage"}, {"risk_control", "altcoin_max_leverage"}}
case "min_confidence":
return [][]string{{"ai_config", "risk_control", "min_confidence"}, {"risk_control", "min_confidence"}}
case "min_risk_reward_ratio":
return [][]string{{"ai_config", "risk_control", "min_risk_reward_ratio"}, {"risk_control", "min_risk_reward_ratio"}}
case "trading_frequency":
return [][]string{{"ai_config", "prompt_sections", "trading_frequency"}, {"prompt_sections", "trading_frequency"}}
case "entry_standards":
return [][]string{{"ai_config", "prompt_sections", "entry_standards"}, {"prompt_sections", "entry_standards"}}
case "symbol", "grid_count", "total_investment", "leverage", "distribution", "max_drawdown_pct", "stop_loss_pct", "daily_loss_limit_pct", "use_maker_only", "use_atr_bounds", "upper_price", "lower_price":
return [][]string{{"grid_config", field}}
default:
return [][]string{{field}}
}
}
func strategyCreatePatchHasPath(value any, path ...string) bool {
current := value
for _, part := range path {
obj, ok := current.(map[string]any)
if !ok {
return false
}
next, ok := obj[part]
if !ok {
return false
}
current = next
}
return true
}
func formatStrategyCreateConfigNeeded(lang, missingKind string) string {
if lang == "zh" {
if missingKind == "strategy_type" {
return "先选择策略类型grid_trading网格策略或 ai_tradingAI 策略)。类型确认后我会继续收集对应配置,配置好后再创建。"
}
if hints := formatStrategyMissingFieldHints(lang, missingKind); hints != "" {
return "这份策略模板还没填完整,还缺这些字段。你可以按下面选,也可以直接说“你帮我按稳健/高频/激进来推荐”:\n" + hints
}
return "这份策略模板还没填完整,还缺:" + formatStrategyMissingFieldNames(lang, missingKind) + "。你可以一句话告诉我这些字段,我会继续填模板。"
}
if missingKind == "strategy_type" {
return "Choose the strategy type first: grid_trading or ai_trading. I will collect the matching config before creating it."
}
if hints := formatStrategyMissingFieldHints(lang, missingKind); hints != "" {
return "This strategy template is not complete yet. You can choose from these options, or ask me to recommend a conservative/balanced/high-frequency setup:\n" + hints
}
return "This strategy template is not complete yet. Missing: " + formatStrategyMissingFieldNames(lang, missingKind) + ". Tell me these fields in one message and I will keep filling the template."
}
func formatStrategyMissingFieldHints(lang, missingKind string) string {
parts := strings.Split(missingKind, ",")
lines := make([]string, 0, len(parts))
for _, part := range parts {
field := strings.TrimSpace(part)
if field == "" {
continue
}
hint := strategyCreateFieldInlineHint(lang, field)
if hint == "" {
hint = strategyCreateFieldDisplayName(lang, field)
}
if lang == "zh" {
lines = append(lines, "- "+hint)
} else {
lines = append(lines, "- "+hint)
}
}
return strings.Join(lines, "\n")
}
func strategyCreateFieldInlineHint(lang, field string) string {
field = strings.TrimSpace(field)
if lang != "zh" {
switch field {
case "source_type":
return "Coin source: ai500 / oi_top / oi_low / static"
case "static_coins":
return "Static coins: up to 10 symbols, e.g. BTCUSDT, ETHUSDT"
case "primary_timeframe":
return "Primary timeframe: 1m / 3m / 5m / 15m / 30m / 1h / 2h / 4h / 6h / 8h / 12h / 1d / 3d / 1w"
case "selected_timeframes":
return "Multi-timeframes: up to 4, e.g. 5m,15m,1h"
case "btceth_max_leverage", "altcoin_max_leverage":
return strategyCreateFieldDisplayName(lang, field) + ": 1-20"
case "min_confidence":
return "Minimum confidence: 50-100"
case "min_risk_reward_ratio":
return "Minimum risk/reward ratio: 1-10, step 0.5"
case "trading_frequency":
return "Trading frequency rule: free text, e.g. max 2-4 trades per day"
case "entry_standards":
return "Entry standards: free text, e.g. enter only when trend and risk/reward align"
case "symbol":
return "Symbol: BTCUSDT / ETHUSDT / SOLUSDT / BNBUSDT / XRPUSDT / DOGEUSDT"
case "grid_count":
return "Grid count: 5-50"
case "total_investment":
return "Total investment: user's capital/margin budget, minimum 100 USDT; not leveraged notional exposure"
case "leverage":
return "Grid leverage: 1-5"
case "distribution":
return "Distribution: uniform / gaussian / pyramid"
case "max_drawdown_pct":
return "Max drawdown: 5%-50%"
case "stop_loss_pct":
return "Stop loss: 1%-20%"
case "daily_loss_limit_pct":
return "Daily loss limit: 1%-30%"
case "use_maker_only":
return "Maker only: on / off"
}
return ""
}
switch field {
case "source_type":
return "选币来源AI500 / OI Top / OI Low / 静态币种(没有混合模式)"
case "static_coins":
return "静态币种:最多 10 个,例如 BTCUSDT、ETHUSDT"
case "primary_timeframe":
return "主周期1m / 3m / 5m / 15m / 30m / 1h / 2h / 4h / 6h / 8h / 12h / 1d / 3d / 1w"
case "selected_timeframes":
return "多周期时间框架:最多 4 个,例如 5m,15m,1h"
case "btceth_max_leverage":
return "BTC/ETH 最大杠杆120 倍"
case "altcoin_max_leverage":
return "山寨币最大杠杆120 倍"
case "min_confidence":
return "最低置信度50100越高越谨慎"
case "min_risk_reward_ratio":
return "最小盈亏比110步进 0.5"
case "trading_frequency":
return "交易频率规则:文本,例如“每天最多 24 笔,避免连续追单”"
case "entry_standards":
return "开仓标准:文本,例如“趋势明确、成交量配合、风险收益合理才开仓”"
case "symbol":
return "交易对BTCUSDT / ETHUSDT / SOLUSDT / BNBUSDT / XRPUSDT / DOGEUSDT"
case "grid_count":
return "网格数量550"
case "total_investment":
return "总投入:用户实际投入/保证金预算,最低 100 USDT不是杠杆后的名义仓位"
case "leverage":
return "杠杆15 倍"
case "distribution":
return "网格分布uniform均匀/ gaussian正态/ pyramid金字塔"
case "max_drawdown_pct":
return "最大回撤5%50%"
case "stop_loss_pct":
return "止损1%20%"
case "daily_loss_limit_pct":
return "日亏损限制1%30%"
case "use_maker_only":
return "只挂 Maker开启 / 关闭"
case "use_atr_bounds 或 upper_price/lower_price":
return "价格边界:开启 ATR 自动边界,或手动填写上边界/下边界"
}
return ""
}
func formatStrategyCreateFieldOptionsReply(lang, text, missingKind string) string {
if !strategyCreateAsksFieldOptions(text) {
return ""
}
field := firstStrategyMissingField(missingKind)
if field == "" {
return ""
}
if lang != "zh" {
switch field {
case "source_type":
return "Coin source options: ai500, oi_top, oi_low, or static. Pick one and I will continue filling the AI strategy template."
case "primary_timeframe", "selected_timeframes":
return "Timeframe options: 1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d, 1w."
}
return "For " + strategyCreateFieldDisplayName(lang, field) + ", tell me the value you want and I will keep filling the selected strategy template."
}
switch field {
case "strategy_type":
return "策略类型只有两个:\n- AI 策略:让 AI 根据行情和策略规则判断开平仓。\n- 网格策略:在价格区间内按网格低买高卖。\n你直接回复“AI 策略”或“网格策略”就行。"
case "source_type":
return "AI 策略的选币来源有 4 个:\n- AI500从 NOFX AI500 榜单自动选币。\n- OI Top选持仓量靠前/更活跃的币。\n- OI Low选持仓量较低或变化较弱的币。\n- 静态币种:你指定固定币种,比如 BTCUSDT、ETHUSDT。\n没有混合模式。你选一个我继续填模板。"
case "primary_timeframe":
return "主周期可选1m、3m、5m、15m、30m、1h、2h、4h、6h、8h、12h、1d、3d、1w。高频一般偏 1m/3m/5m稳健一点可以用 15m/1h。"
case "selected_timeframes":
return "多周期最多选 4 个可选1m、3m、5m、15m、30m、1h、2h、4h、6h、8h、12h、1d、3d、1w。常见组合比如 5m,15m,1h。"
case "btceth_max_leverage", "altcoin_max_leverage":
return strategyCreateFieldDisplayName(lang, field) + "范围是 120 倍。数值越高风险越大。"
case "min_confidence":
return "最低置信度范围是 50100。数值越高越谨慎开单会更少。"
case "min_risk_reward_ratio":
return "最小盈亏比范围是 110步进 0.5。比如 1.5 表示预期收益至少是风险的 1.5 倍。"
case "trading_frequency":
return "交易频率规则是文本规则,例如“每天最多 24 笔,避免连续追单”。你也可以说“你帮我按高频但不过度交易来写”。"
case "entry_standards":
return "开仓标准是文本规则,例如“只在趋势明确、成交量配合、风险收益合理时开仓”。你也可以说“你帮我写一版稳健开仓标准”。"
case "symbol":
return "网格交易对可选BTCUSDT、ETHUSDT、SOLUSDT、BNBUSDT、XRPUSDT、DOGEUSDT。"
case "grid_count":
return "网格数量范围是 550。数量越多越密交易更频繁数量越少每格空间更大。"
case "total_investment":
return "网格总投入是用户实际投入/保证金预算,不是杠杆后的名义仓位;最小 100 USDT按 100 USDT 步进。"
case "leverage":
return "网格杠杆范围是 15 倍。稳健一般用 1 倍。"
case "distribution":
return "网格分布可选uniform均匀、gaussian正态、pyramid金字塔。"
case "max_drawdown_pct":
return "最大回撤范围是 5%50%。"
case "stop_loss_pct":
return "止损范围是 1%20%。"
case "daily_loss_limit_pct":
return "日亏损限制范围是 1%30%。"
case "use_maker_only":
return "只挂 Maker 是开关项:开启会更偏向低手续费挂单,成交可能慢一些;关闭则更灵活。"
}
return strategyCreateFieldDisplayName(lang, field) + "是当前模板字段。你告诉我想怎么设置,我继续填模板。"
}
func strategyCreateAsksFieldOptions(text string) bool {
lower := strings.ToLower(strings.TrimSpace(text))
if lower == "" {
return false
}
return containsAny(lower, []string{
"有哪些", "有什么", "可选", "选项", "怎么选", "怎么填", "不知道", "不会填",
"what options", "which options", "options", "how to choose", "how should i fill",
})
}
func firstStrategyMissingField(missingKind string) string {
for _, part := range strings.Split(missingKind, ",") {
part = strings.TrimSpace(part)
if part != "" {
return part
}
}
return ""
}
func formatStrategyMissingFieldNames(lang, missingKind string) string {
parts := strings.Split(missingKind, ",")
names := make([]string, 0, len(parts))
for _, part := range parts {
part = strings.TrimSpace(part)
if part == "" {
continue
}
if strings.Contains(part, "或") || strings.Contains(part, "/") {
names = append(names, part)
continue
}
names = append(names, strategyCreateFieldDisplayName(lang, part))
}
if lang == "zh" {
return strings.Join(names, "、")
}
return strings.Join(names, ", ")
}
func strategyCreateFieldDisplayName(lang, field string) string {
if lang != "zh" {
return field
}
switch strings.TrimSpace(field) {
case "source_type":
return "选币来源"
case "static_coins":
return "静态币种"
case "primary_timeframe":
return "主周期"
case "selected_timeframes":
return "多周期时间框架"
case "btceth_max_leverage":
return "BTC/ETH 最大杠杆"
case "altcoin_max_leverage":
return "山寨币最大杠杆"
case "min_confidence":
return "最低置信度"
case "min_risk_reward_ratio":
return "最小盈亏比"
case "trading_frequency":
return "交易频率规则"
case "entry_standards":
return "开仓标准"
case "symbol":
return "交易对"
case "grid_count":
return "网格数量"
case "total_investment":
return "总投入"
case "leverage":
return "杠杆"
case "distribution":
return "网格分布"
case "max_drawdown_pct":
return "最大回撤"
case "stop_loss_pct":
return "止损"
case "daily_loss_limit_pct":
return "日亏损限制"
case "use_maker_only":
return "只挂 Maker"
default:
return field
}
}
func formatStrategyCreateDraftSummary(lang, name, strategyType string, changedFields, warnings []string) string {
@@ -659,9 +1069,9 @@ func formatStrategyCreateDraftSummary(lang, name, strategyType string, changedFi
}
switch strategyType {
case "grid_trading":
lines = append(lines, "这是网格策略草稿。你可以继续补充交易对、网格数量、总投入、杠杆、价格区间和网格风控;如果想让我按默认值补齐,直接说“用默认配置创建”。")
lines = append(lines, "这是网格策略草稿。继续补充交易对、网格数量、总投入、杠杆、价格区间和网格风控;我只会按产品编辑页模板填你明确给出或明确委托我设计的字段。")
case "ai_trading":
lines = append(lines, "这是 AI 策略草稿。你可以继续补充选币来源、时间周期、风险参数和提示词方向;如果想让我按默认值补齐,直接说“用默认配置创建”。")
lines = append(lines, "这是 AI 策略草稿。继续补充选币来源、时间周期、风险参数和提示词方向;我只会按产品编辑页模板填你明确给出或明确委托我设计的字段。")
default:
lines = append(lines, "你可以继续补充策略类型和对应参数;如果现在就创建,直接回复“确认创建”。")
}
@@ -687,9 +1097,9 @@ func formatStrategyCreateDraftSummary(lang, name, strategyType string, changedFi
}
switch strategyType {
case "grid_trading":
lines = append(lines, "This is a grid strategy draft. You can keep refining symbol, grid count, total investment, leverage, price bounds, and grid risk settings, or say 'use defaults' before creating it.")
lines = append(lines, "This is a grid strategy draft. Keep refining symbol, grid count, total investment, leverage, price bounds, and grid risk settings; I will only fill fields you explicitly provide or ask me to design.")
case "ai_trading":
lines = append(lines, "This is an AI strategy draft. You can keep refining coin source, timeframes, risk settings, and prompt direction, or say 'use defaults' before creating it.")
lines = append(lines, "This is an AI strategy draft. Keep refining coin source, timeframes, risk settings, and prompt direction; I will only fill fields you explicitly provide or ask me to design.")
default:
lines = append(lines, "You can keep refining the strategy type and matching parameters, or reply 'confirm' to create it now.")
}
@@ -711,6 +1121,8 @@ func formatStrategyCreateFinalConfirmation(lang string, session skillSession, cf
}
lines = append(lines,
"- 类型:网格策略",
fmt.Sprintf("- 发布到策略市场:%t", fieldValue(session, "is_public") == "true"),
fmt.Sprintf("- 发布后配置可见:%t", fieldValue(session, "config_visible") != "false"),
fmt.Sprintf("- 交易对:%s", defaultIfEmpty(grid.Symbol, "未设置")),
fmt.Sprintf("- 网格数量:%d", grid.GridCount),
fmt.Sprintf("- 总投入:%.2f USDT", grid.TotalInvestment),
@@ -730,10 +1142,34 @@ func formatStrategyCreateFinalConfirmation(lang string, session skillSession, cf
default:
lines = append(lines,
"- 类型AI 策略",
fmt.Sprintf("- 发布到策略市场:%t", fieldValue(session, "is_public") == "true"),
fmt.Sprintf("- 发布后配置可见:%t", fieldValue(session, "config_visible") != "false"),
fmt.Sprintf("- 选币来源:%s", defaultIfEmpty(cfg.CoinSource.SourceType, "未设置")),
)
lines = append(lines, formatAICoinSourceSummaryZH(cfg)...)
lines = append(lines,
fmt.Sprintf("- 主周期:%s", defaultIfEmpty(cfg.Indicators.Klines.PrimaryTimeframe, "未设置")),
fmt.Sprintf("- K线数量%d", cfg.Indicators.Klines.PrimaryCount),
fmt.Sprintf("- 多周期:%s", defaultIfEmpty(strings.Join(cfg.Indicators.Klines.SelectedTimeframes, ","), "未设置")),
fmt.Sprintf("- 指标:%s", formatEnabledAIIndicatorsZH(cfg)),
fmt.Sprintf("- NofxOS 量化数据:%t", cfg.Indicators.EnableQuantData),
fmt.Sprintf("- OI 排行数据:%t%s / %d", cfg.Indicators.EnableOIRanking, defaultIfEmpty(cfg.Indicators.OIRankingDuration, "未设置"), cfg.Indicators.OIRankingLimit),
fmt.Sprintf("- 资金流排行数据:%t%s / %d", cfg.Indicators.EnableNetFlowRanking, defaultIfEmpty(cfg.Indicators.NetFlowRankingDuration, "未设置"), cfg.Indicators.NetFlowRankingLimit),
fmt.Sprintf("- 涨跌幅排行数据:%t%s / %d", cfg.Indicators.EnablePriceRanking, defaultIfEmpty(cfg.Indicators.PriceRankingDuration, "未设置"), cfg.Indicators.PriceRankingLimit),
fmt.Sprintf("- BTC/ETH 最大杠杆:%d倍", cfg.RiskControl.BTCETHMaxLeverage),
fmt.Sprintf("- 山寨币最大杠杆:%d倍", cfg.RiskControl.AltcoinMaxLeverage),
fmt.Sprintf("- 最小置信度:%d", cfg.RiskControl.MinConfidence),
fmt.Sprintf("- 最小盈亏比:%.2f", cfg.RiskControl.MinRiskRewardRatio),
fmt.Sprintf("- 最大持仓数System enforced%d", cfg.RiskControl.MaxPositions),
fmt.Sprintf("- BTC/ETH 单币仓位上限System enforced账户权益 %.2f 倍", cfg.RiskControl.BTCETHMaxPositionValueRatio),
fmt.Sprintf("- 山寨币单币仓位上限System enforced账户权益 %.2f 倍", cfg.RiskControl.AltcoinMaxPositionValueRatio),
fmt.Sprintf("- 最大保证金使用率System enforced%.0f%%", cfg.RiskControl.MaxMarginUsage*100),
fmt.Sprintf("- 最小开仓金额System enforced%.2f USDT", cfg.RiskControl.MinPositionSize),
fmt.Sprintf("- 角色定义:%s", compactSummaryText(cfg.PromptSections.RoleDefinition)),
fmt.Sprintf("- 交易频率规则:%s", compactSummaryText(cfg.PromptSections.TradingFrequency)),
fmt.Sprintf("- 开仓标准:%s", compactSummaryText(cfg.PromptSections.EntryStandards)),
fmt.Sprintf("- 决策流程:%s", compactSummaryText(cfg.PromptSections.DecisionProcess)),
fmt.Sprintf("- 自定义 Prompt%s", compactSummaryText(cfg.CustomPrompt)),
)
}
lines = append(lines, "确认创建的话,直接回复“确认创建”。要调整也可以直接说改哪项。")
@@ -756,6 +1192,83 @@ func formatStrategyCreateFinalConfirmation(lang string, session skillSession, cf
return strings.Join(lines, "\n")
}
func formatEnabledAIIndicatorsZH(cfg store.StrategyConfig) string {
enabled := make([]string, 0, 8)
if cfg.Indicators.EnableRawKlines {
enabled = append(enabled, "K线")
}
if cfg.Indicators.EnableVolume {
enabled = append(enabled, "成交量")
}
if cfg.Indicators.EnableOI {
enabled = append(enabled, "OI")
}
if cfg.Indicators.EnableFundingRate {
enabled = append(enabled, "资金费率")
}
if cfg.Indicators.EnableEMA {
enabled = append(enabled, "EMA")
}
if cfg.Indicators.EnableMACD {
enabled = append(enabled, "MACD")
}
if cfg.Indicators.EnableRSI {
enabled = append(enabled, "RSI")
}
if cfg.Indicators.EnableATR {
enabled = append(enabled, "ATR")
}
if cfg.Indicators.EnableBOLL {
enabled = append(enabled, "BOLL")
}
if len(enabled) == 0 {
return "无"
}
return strings.Join(enabled, ",")
}
func formatAICoinSourceSummaryZH(cfg store.StrategyConfig) []string {
lines := make([]string, 0, 4)
sourceType := strings.ToLower(strings.TrimSpace(cfg.CoinSource.SourceType))
switch sourceType {
case "static":
lines = append(lines, fmt.Sprintf("- 静态币种:%s", defaultIfEmpty(strings.Join(cfg.CoinSource.StaticCoins, ","), "未设置")))
case "ai500":
lines = append(lines, fmt.Sprintf("- AI500 数量:%d", cfg.CoinSource.AI500Limit))
case "oi_top":
lines = append(lines, fmt.Sprintf("- OI Top 数量:%d", cfg.CoinSource.OITopLimit))
case "oi_low":
lines = append(lines, fmt.Sprintf("- OI Low 数量:%d", cfg.CoinSource.OILowLimit))
default:
if cfg.CoinSource.UseAI500 {
lines = append(lines, fmt.Sprintf("- AI500 数量:%d", cfg.CoinSource.AI500Limit))
}
if cfg.CoinSource.UseOITop {
lines = append(lines, fmt.Sprintf("- OI Top 数量:%d", cfg.CoinSource.OITopLimit))
}
if cfg.CoinSource.UseOILow {
lines = append(lines, fmt.Sprintf("- OI Low 数量:%d", cfg.CoinSource.OILowLimit))
}
}
if len(cfg.CoinSource.ExcludedCoins) > 0 {
lines = append(lines, fmt.Sprintf("- 排除币种:%s", strings.Join(cfg.CoinSource.ExcludedCoins, ",")))
}
return lines
}
func compactSummaryText(value string) string {
value = strings.Join(strings.Fields(strings.TrimSpace(value)), " ")
if value == "" {
return "未设置"
}
const maxLen = 120
runes := []rune(value)
if len(runes) <= maxLen {
return value
}
return string(runes[:maxLen]) + "..."
}
func createConfirmationReply(text string) bool {
return strategyCreateConfirmationReply(text)
}
@@ -1025,78 +1538,6 @@ func formatTraderCreateDraftSummary(lang string, session skillSession) string {
return strings.Join(lines, "\n")
}
func (a *Agent) continueStrategyCreateDraft(storeUserID string, userID int64, lang, text string, session skillSession) string {
name := resolveStrategyCreateName(&session, text)
if actionRequiresSlot("strategy_management", "create", "name") && strings.TrimSpace(name) == "" {
setSkillDAGStep(&session, "resolve_name")
a.saveSkillSession(userID, session)
if lang == "zh" {
return "要创建策略我还需要策略名。你可以直接说创建一个叫“趋势策略A”的策略。"
}
return "One more thing: give this strategy a name."
}
if fieldValue(session, "strategy_type") == "" {
if strategyType := parseStrategyTypeValue(text); strategyType != "" {
setField(&session, "strategy_type", strategyType)
}
}
cfg := unmarshalStrategyCreateDraft(fieldValue(session, strategyCreateDraftConfigField), lang)
changedFields := applyStrategyCreateIntentToConfig(&cfg, text, lang)
if fieldValue(session, strategyCreateDraftConfigField) == "" && len(changedFields) == 0 {
cfg = store.GetDefaultStrategyConfig(lang)
}
beforeClamp := cfg
cfg.ClampLimits()
warnings := store.StrategyClampWarnings(beforeClamp, cfg, cfg.Language)
setField(&session, strategyCreateDraftConfigField, marshalStrategyCreateDraft(cfg))
setSkillDAGStep(&session, "await_create_confirmation")
session.Phase = "draft_create"
if strategyCreateConfirmationReply(text) || strategyCreateFinalConfirmationReady(session) {
if ready, missingKind := strategyCreateConfigReady(session, cfg, text); !ready {
if missingKind != "strategy_type" {
setField(&session, strategyCreateDraftConfigField, marshalStrategyCreateDraft(cfg))
setField(&session, "awaiting_final_confirmation", "true")
a.saveSkillSession(userID, session)
return formatStrategyCreateFinalConfirmation(lang, session, cfg)
}
a.saveSkillSession(userID, session)
return formatStrategyCreateConfigNeeded(lang, missingKind)
}
args := map[string]any{
"action": "create",
"name": name,
"lang": defaultIfEmpty(lang, "zh"),
"confirmed": true,
}
rawCfg, _ := json.Marshal(cfg)
var configMap map[string]any
if err := json.Unmarshal(rawCfg, &configMap); err == nil && len(configMap) > 0 {
args["config"] = configMap
}
raw, _ := json.Marshal(args)
resp := a.toolManageStrategy(storeUserID, string(raw))
if errMsg := parseSkillError(resp); strings.Contains(resp, `"error"`) {
a.saveSkillSession(userID, session)
if lang == "zh" {
return "创建策略失败:" + errMsg
}
return "That create request did not go through: " + errMsg
}
a.clearSkillSession(userID)
a.rememberReferencesFromToolResult(userID, "manage_strategy", resp)
if lang == "zh" {
return formatCreatedStrategyReply(lang, name, cfg, warnings)
}
return formatCreatedStrategyReply(lang, name, cfg, warnings)
}
a.saveSkillSession(userID, session)
return formatStrategyCreateDraftSummary(lang, name, explicitStrategyCreateType(session), changedFields, warnings)
}
func hasExplicitStrategyDetailIntent(text string) bool {
lower := strings.ToLower(strings.TrimSpace(text))
if lower == "" {
@@ -1948,10 +2389,6 @@ func (a *Agent) handleStrategyCreateSkill(storeUserID string, userID int64, lang
return "Cancelled the current strategy creation flow."
}
name := resolveStrategyCreateName(&session, text)
hasDescriptiveDraftIntent := session.Phase == "draft_create"
if hasDescriptiveDraftIntent {
return a.continueStrategyCreateDraft(storeUserID, userID, lang, text, session)
}
if actionRequiresSlot("strategy_management", "create", "name") && name == "" {
setSkillDAGStep(&session, "resolve_name")
a.saveSkillSession(userID, session)
@@ -1962,8 +2399,10 @@ func (a *Agent) handleStrategyCreateSkill(storeUserID string, userID int64, lang
}
if fieldValue(session, "strategy_type") == "" {
if strategyType := parseStrategyTypeValue(text); strategyType != "" {
setField(&session, "strategy_type", strategyType)
setStrategyCreateType(&session, strategyType)
}
} else if strategyType := parseStrategyTypeValue(text); strategyType != "" {
setStrategyCreateType(&session, strategyType)
}
cfg, configMap, warnings, cfgErr := strategyCreateConfigFromSession(session, lang)
if cfgErr != nil {
@@ -1976,15 +2415,21 @@ func (a *Agent) handleStrategyCreateSkill(storeUserID string, userID int64, lang
if ready, missingKind := strategyCreateConfigReady(session, cfg, text); !ready {
setField(&session, strategyCreateDraftConfigField, marshalStrategyCreateDraft(cfg))
setSkillDAGStep(&session, "collect_config")
session.Phase = "draft_create"
if missingKind != "strategy_type" {
setField(&session, "awaiting_final_confirmation", "true")
a.saveSkillSession(userID, session)
return formatStrategyCreateFinalConfirmation(lang, session, cfg)
}
session.Phase = "collecting"
a.saveSkillSession(userID, session)
if reply := formatStrategyCreateFieldOptionsReply(lang, text, missingKind); reply != "" {
return reply
}
return formatStrategyCreateConfigNeeded(lang, missingKind)
}
if !strategyCreateConfirmationReply(text) && !strategyCreateFinalConfirmationReady(session) {
setField(&session, strategyCreateDraftConfigField, marshalStrategyCreateDraft(cfg))
setField(&session, "awaiting_final_confirmation", "true")
setSkillDAGStep(&session, "await_create_confirmation")
session.Phase = "await_create_confirmation"
a.saveSkillSession(userID, session)
return formatStrategyCreateFinalConfirmation(lang, session, cfg)
}
setSkillDAGStep(&session, "execute_create")
args := map[string]any{
@@ -2044,7 +2489,6 @@ func formatCreatedStrategyReply(lang, name string, cfg store.StrategyConfig, war
lines = append(lines,
"- 类型AI 策略",
fmt.Sprintf("- 选币来源:%s", defaultIfEmpty(cfg.CoinSource.SourceType, "未设置")),
fmt.Sprintf("- 静态币种:%s", strings.Join(cfg.CoinSource.StaticCoins, ", ")),
fmt.Sprintf("- 主周期:%s", defaultIfEmpty(cfg.Indicators.Klines.PrimaryTimeframe, "未设置")),
fmt.Sprintf("- BTC/ETH 最大杠杆:%d倍", cfg.RiskControl.BTCETHMaxLeverage),
fmt.Sprintf("- 山寨币最大杠杆:%d倍", cfg.RiskControl.AltcoinMaxLeverage),