mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
Split strategy config by strategy type
This commit is contained in:
@@ -161,6 +161,32 @@ func normalizeStrategyConfigPatch(patch map[string]any) {
|
||||
if patch == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if gridConfig, hasGrid := patch["grid_config"]; hasGrid && gridConfig != nil {
|
||||
if _, hasType := patch["strategy_type"]; !hasType {
|
||||
patch["strategy_type"] = "grid_trading"
|
||||
}
|
||||
}
|
||||
|
||||
aiKeys := []string{"coin_source", "indicators", "risk_control", "prompt_sections", "custom_prompt"}
|
||||
for _, key := range aiKeys {
|
||||
value, ok := patch[key]
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
aiConfig, _ := patch["ai_config"].(map[string]any)
|
||||
if aiConfig == nil {
|
||||
aiConfig = map[string]any{}
|
||||
patch["ai_config"] = aiConfig
|
||||
}
|
||||
aiConfig[key] = value
|
||||
delete(patch, key)
|
||||
}
|
||||
|
||||
if fmt.Sprint(patch["strategy_type"]) == "grid_trading" {
|
||||
delete(patch, "ai_config")
|
||||
}
|
||||
|
||||
if _, hasType := patch["strategy_type"]; hasType {
|
||||
return
|
||||
}
|
||||
@@ -249,19 +275,128 @@ type StrategyConfig struct {
|
||||
// language setting: "zh" for Chinese, "en" for English
|
||||
// This determines the language used for data formatting and prompt generation
|
||||
Language string `json:"language,omitempty"`
|
||||
// coin source configuration
|
||||
CoinSource CoinSourceConfig `json:"coin_source"`
|
||||
// quantitative data configuration
|
||||
Indicators IndicatorConfig `json:"indicators"`
|
||||
// custom prompt (appended at the end)
|
||||
CustomPrompt string `json:"custom_prompt,omitempty"`
|
||||
// risk control configuration
|
||||
RiskControl RiskControlConfig `json:"risk_control"`
|
||||
// editable sections of System Prompt
|
||||
PromptSections PromptSectionsConfig `json:"prompt_sections,omitempty"`
|
||||
// AI trading configuration fields are kept on the Go struct for engine
|
||||
// compatibility, but JSON persistence nests them under ai_config.
|
||||
CoinSource CoinSourceConfig `json:"-"`
|
||||
Indicators IndicatorConfig `json:"-"`
|
||||
CustomPrompt string `json:"-"`
|
||||
RiskControl RiskControlConfig `json:"-"`
|
||||
PromptSections PromptSectionsConfig `json:"-"`
|
||||
|
||||
// Grid trading configuration (only used when StrategyType == "grid_trading")
|
||||
GridConfig *GridStrategyConfig `json:"grid_config,omitempty"`
|
||||
|
||||
// Publish settings are shared by AI and grid strategies. The database still
|
||||
// stores the authoritative booleans on Strategy, but config JSON may carry
|
||||
// this object for agent/frontend schema consistency.
|
||||
PublishConfig *PublishStrategyConfig `json:"publish_config,omitempty"`
|
||||
}
|
||||
|
||||
// AIStrategyConfig contains fields only used by AI trading strategies.
|
||||
type AIStrategyConfig struct {
|
||||
CoinSource CoinSourceConfig `json:"coin_source"`
|
||||
Indicators IndicatorConfig `json:"indicators"`
|
||||
CustomPrompt string `json:"custom_prompt,omitempty"`
|
||||
RiskControl RiskControlConfig `json:"risk_control"`
|
||||
PromptSections PromptSectionsConfig `json:"prompt_sections,omitempty"`
|
||||
}
|
||||
|
||||
// PublishStrategyConfig contains settings shared by all strategy types.
|
||||
type PublishStrategyConfig struct {
|
||||
IsPublic bool `json:"is_public"`
|
||||
ConfigVisible bool `json:"config_visible"`
|
||||
}
|
||||
|
||||
// MarshalJSON writes the product-facing strategy schema:
|
||||
// strategy_type + grid_config or ai_config + shared publish_config.
|
||||
func (c StrategyConfig) MarshalJSON() ([]byte, error) {
|
||||
strategyType := strings.TrimSpace(c.StrategyType)
|
||||
if strategyType == "" {
|
||||
strategyType = "ai_trading"
|
||||
}
|
||||
|
||||
out := struct {
|
||||
StrategyType string `json:"strategy_type"`
|
||||
Language string `json:"language,omitempty"`
|
||||
AIConfig *AIStrategyConfig `json:"ai_config,omitempty"`
|
||||
GridConfig *GridStrategyConfig `json:"grid_config,omitempty"`
|
||||
PublishConfig *PublishStrategyConfig `json:"publish_config,omitempty"`
|
||||
}{
|
||||
StrategyType: strategyType,
|
||||
Language: c.Language,
|
||||
PublishConfig: c.PublishConfig,
|
||||
}
|
||||
|
||||
if strategyType == "grid_trading" {
|
||||
out.GridConfig = c.GridConfig
|
||||
} else {
|
||||
out.AIConfig = &AIStrategyConfig{
|
||||
CoinSource: c.CoinSource,
|
||||
Indicators: c.Indicators,
|
||||
CustomPrompt: c.CustomPrompt,
|
||||
RiskControl: c.RiskControl,
|
||||
PromptSections: c.PromptSections,
|
||||
}
|
||||
}
|
||||
|
||||
return json.Marshal(out)
|
||||
}
|
||||
|
||||
// UnmarshalJSON accepts both the new nested schema and old flat configs. Old
|
||||
// top-level AI fields are normalized into the Go compatibility fields.
|
||||
func (c *StrategyConfig) UnmarshalJSON(data []byte) error {
|
||||
type rawStrategyConfig struct {
|
||||
StrategyType string `json:"strategy_type"`
|
||||
Language string `json:"language"`
|
||||
AIConfig *AIStrategyConfig `json:"ai_config"`
|
||||
GridConfig *GridStrategyConfig `json:"grid_config"`
|
||||
PublishConfig *PublishStrategyConfig `json:"publish_config"`
|
||||
|
||||
CoinSource *CoinSourceConfig `json:"coin_source"`
|
||||
Indicators *IndicatorConfig `json:"indicators"`
|
||||
CustomPrompt *string `json:"custom_prompt"`
|
||||
RiskControl *RiskControlConfig `json:"risk_control"`
|
||||
PromptSections *PromptSectionsConfig `json:"prompt_sections"`
|
||||
}
|
||||
|
||||
var raw rawStrategyConfig
|
||||
if err := json.Unmarshal(data, &raw); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.StrategyType = raw.StrategyType
|
||||
c.Language = raw.Language
|
||||
c.GridConfig = raw.GridConfig
|
||||
c.PublishConfig = raw.PublishConfig
|
||||
|
||||
if raw.AIConfig != nil {
|
||||
c.CoinSource = raw.AIConfig.CoinSource
|
||||
c.Indicators = raw.AIConfig.Indicators
|
||||
c.CustomPrompt = raw.AIConfig.CustomPrompt
|
||||
c.RiskControl = raw.AIConfig.RiskControl
|
||||
c.PromptSections = raw.AIConfig.PromptSections
|
||||
} else {
|
||||
if raw.CoinSource != nil {
|
||||
c.CoinSource = *raw.CoinSource
|
||||
}
|
||||
if raw.Indicators != nil {
|
||||
c.Indicators = *raw.Indicators
|
||||
}
|
||||
if raw.CustomPrompt != nil {
|
||||
c.CustomPrompt = *raw.CustomPrompt
|
||||
}
|
||||
if raw.RiskControl != nil {
|
||||
c.RiskControl = *raw.RiskControl
|
||||
}
|
||||
if raw.PromptSections != nil {
|
||||
c.PromptSections = *raw.PromptSections
|
||||
}
|
||||
}
|
||||
|
||||
if strings.TrimSpace(c.StrategyType) == "" && c.GridConfig != nil {
|
||||
c.StrategyType = "grid_trading"
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// GridStrategyConfig grid trading specific configuration
|
||||
|
||||
78
store/strategy_schema_test.go
Normal file
78
store/strategy_schema_test.go
Normal file
@@ -0,0 +1,78 @@
|
||||
package store
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestStrategyConfigMarshalSeparatesGridAndAIConfig(t *testing.T) {
|
||||
cfg := GetDefaultStrategyConfig("zh")
|
||||
cfg.StrategyType = "grid_trading"
|
||||
cfg.GridConfig = &GridStrategyConfig{
|
||||
Symbol: "BTCUSDT",
|
||||
GridCount: 20,
|
||||
TotalInvestment: 200,
|
||||
Leverage: 2,
|
||||
UseATRBounds: true,
|
||||
ATRMultiplier: 2,
|
||||
Distribution: "uniform",
|
||||
}
|
||||
|
||||
raw, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal grid config: %v", err)
|
||||
}
|
||||
|
||||
var asMap map[string]any
|
||||
if err := json.Unmarshal(raw, &asMap); err != nil {
|
||||
t.Fatalf("unmarshal grid config map: %v", err)
|
||||
}
|
||||
if asMap["strategy_type"] != "grid_trading" {
|
||||
t.Fatalf("expected grid strategy_type, got %v", asMap["strategy_type"])
|
||||
}
|
||||
if _, ok := asMap["grid_config"]; !ok {
|
||||
t.Fatalf("expected grid_config in grid strategy JSON: %s", string(raw))
|
||||
}
|
||||
for _, key := range []string{"ai_config", "coin_source", "indicators", "risk_control", "prompt_sections", "custom_prompt"} {
|
||||
if _, ok := asMap[key]; ok {
|
||||
t.Fatalf("did not expect %s in grid strategy JSON: %s", key, string(raw))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestStrategyConfigUnmarshalLegacyFlatAIConfig(t *testing.T) {
|
||||
raw := []byte(`{
|
||||
"strategy_type":"ai_trading",
|
||||
"coin_source":{"source_type":"static","static_coins":["ETHUSDT"]},
|
||||
"indicators":{"klines":{"primary_timeframe":"15m"}},
|
||||
"risk_control":{"max_positions":2,"min_confidence":80},
|
||||
"prompt_sections":{"entry_standards":"trend only"},
|
||||
"custom_prompt":"prefer ETH"
|
||||
}`)
|
||||
|
||||
var cfg StrategyConfig
|
||||
if err := json.Unmarshal(raw, &cfg); err != nil {
|
||||
t.Fatalf("unmarshal legacy flat config: %v", err)
|
||||
}
|
||||
if cfg.CoinSource.SourceType != "static" || len(cfg.CoinSource.StaticCoins) != 1 || cfg.CoinSource.StaticCoins[0] != "ETHUSDT" {
|
||||
t.Fatalf("legacy coin source was not normalized: %+v", cfg.CoinSource)
|
||||
}
|
||||
if cfg.Indicators.Klines.PrimaryTimeframe != "15m" {
|
||||
t.Fatalf("legacy indicators were not normalized: %+v", cfg.Indicators.Klines)
|
||||
}
|
||||
|
||||
normalized, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal normalized config: %v", err)
|
||||
}
|
||||
var asMap map[string]any
|
||||
if err := json.Unmarshal(normalized, &asMap); err != nil {
|
||||
t.Fatalf("unmarshal normalized map: %v", err)
|
||||
}
|
||||
if _, ok := asMap["ai_config"]; !ok {
|
||||
t.Fatalf("expected ai_config after normalizing legacy config: %s", string(normalized))
|
||||
}
|
||||
if _, ok := asMap["coin_source"]; ok {
|
||||
t.Fatalf("did not expect legacy coin_source at top level: %s", string(normalized))
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user