mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-18 09:54:35 +08:00
Refine strategy creation flow and diagnostics
This commit is contained in:
@@ -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_trading(AI 策略)。类型确认后我会继续收集对应配置,配置好后再创建。"
|
||||
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_trading(AI 策略)。类型确认后我会继续收集对应配置,配置好后再创建。"
|
||||
}
|
||||
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 最大杠杆:1~20 倍"
|
||||
case "altcoin_max_leverage":
|
||||
return "山寨币最大杠杆:1~20 倍"
|
||||
case "min_confidence":
|
||||
return "最低置信度:50~100,越高越谨慎"
|
||||
case "min_risk_reward_ratio":
|
||||
return "最小盈亏比:1~10,步进 0.5"
|
||||
case "trading_frequency":
|
||||
return "交易频率规则:文本,例如“每天最多 2~4 笔,避免连续追单”"
|
||||
case "entry_standards":
|
||||
return "开仓标准:文本,例如“趋势明确、成交量配合、风险收益合理才开仓”"
|
||||
case "symbol":
|
||||
return "交易对:BTCUSDT / ETHUSDT / SOLUSDT / BNBUSDT / XRPUSDT / DOGEUSDT"
|
||||
case "grid_count":
|
||||
return "网格数量:5~50"
|
||||
case "total_investment":
|
||||
return "总投入:用户实际投入/保证金预算,最低 100 USDT;不是杠杆后的名义仓位"
|
||||
case "leverage":
|
||||
return "杠杆:1~5 倍"
|
||||
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) + "范围是 1~20 倍。数值越高风险越大。"
|
||||
case "min_confidence":
|
||||
return "最低置信度范围是 50~100。数值越高越谨慎,开单会更少。"
|
||||
case "min_risk_reward_ratio":
|
||||
return "最小盈亏比范围是 1~10,步进 0.5。比如 1.5 表示预期收益至少是风险的 1.5 倍。"
|
||||
case "trading_frequency":
|
||||
return "交易频率规则是文本规则,例如“每天最多 2~4 笔,避免连续追单”。你也可以说“你帮我按高频但不过度交易来写”。"
|
||||
case "entry_standards":
|
||||
return "开仓标准是文本规则,例如“只在趋势明确、成交量配合、风险收益合理时开仓”。你也可以说“你帮我写一版稳健开仓标准”。"
|
||||
case "symbol":
|
||||
return "网格交易对可选:BTCUSDT、ETHUSDT、SOLUSDT、BNBUSDT、XRPUSDT、DOGEUSDT。"
|
||||
case "grid_count":
|
||||
return "网格数量范围是 5~50。数量越多越密,交易更频繁;数量越少,每格空间更大。"
|
||||
case "total_investment":
|
||||
return "网格总投入是用户实际投入/保证金预算,不是杠杆后的名义仓位;最小 100 USDT,按 100 USDT 步进。"
|
||||
case "leverage":
|
||||
return "网格杠杆范围是 1~5 倍。稳健一般用 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),
|
||||
|
||||
Reference in New Issue
Block a user