mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-18 01:44:38 +08:00
v2
This commit is contained in:
@@ -1,6 +1,7 @@
|
||||
package agent
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"log/slog"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -23,6 +24,69 @@ func TestToolManageModelConfigCreateRequiresCredential(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolManageModelConfigCreateDefaultsToEnabledLikeManualPage(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "model-create-enabled.db")
|
||||
st, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("create store: %v", err)
|
||||
}
|
||||
a := New(nil, st, DefaultConfig(), slog.Default())
|
||||
|
||||
resp := a.toolManageModelConfig("default", `{"action":"create","provider":"qwen","name":"qwen","api_key":"sk-test-qwen-123456","custom_model_name":"qwen3-max"}`)
|
||||
if strings.Contains(resp, `"error"`) {
|
||||
t.Fatalf("expected create to succeed, got: %s", resp)
|
||||
}
|
||||
|
||||
model, err := st.AIModel().Get("default", "default_qwen")
|
||||
if err != nil {
|
||||
t.Fatalf("load created model: %v", err)
|
||||
}
|
||||
if !model.Enabled {
|
||||
t.Fatalf("expected agent-created model to default to enabled so it matches manual creation")
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolManageModelConfigCreateReusesExistingProviderRecord(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "model-create-upsert.db")
|
||||
st, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("create store: %v", err)
|
||||
}
|
||||
a := New(nil, st, DefaultConfig(), slog.Default())
|
||||
|
||||
if err := st.AIModel().UpdateWithName("default", "default_qwen", "qwen1", false, "sk-old-qwen-123456", "", "qwen3-max"); err != nil {
|
||||
t.Fatalf("seed existing qwen model: %v", err)
|
||||
}
|
||||
|
||||
resp := a.toolManageModelConfig("default", `{"action":"create","provider":"qwen","name":"Qwen","api_key":"sk-new-qwen-123456","custom_model_name":"qwen3-max"}`)
|
||||
if strings.Contains(resp, `"error"`) {
|
||||
t.Fatalf("expected create to reuse existing qwen config instead of failing, got: %s", resp)
|
||||
}
|
||||
|
||||
models, err := st.AIModel().List("default")
|
||||
if err != nil {
|
||||
t.Fatalf("list models: %v", err)
|
||||
}
|
||||
qwenCount := 0
|
||||
for _, model := range models {
|
||||
if model != nil && model.Provider == "qwen" {
|
||||
qwenCount++
|
||||
if model.ID != "default_qwen" {
|
||||
t.Fatalf("expected existing qwen record to be reused, got model id %q", model.ID)
|
||||
}
|
||||
if model.Name != "Qwen" {
|
||||
t.Fatalf("expected reused qwen record to be renamed, got %q", model.Name)
|
||||
}
|
||||
if !model.Enabled {
|
||||
t.Fatalf("expected reused qwen record to be enabled after agent create")
|
||||
}
|
||||
}
|
||||
}
|
||||
if qwenCount != 1 {
|
||||
t.Fatalf("expected exactly one qwen record after reuse, got %d", qwenCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolGetModelConfigsHidesIncompleteRows(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "visibility-list.db")
|
||||
st, err := store.New(dbPath)
|
||||
@@ -47,6 +111,50 @@ func TestToolGetModelConfigsHidesIncompleteRows(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolManageStrategyUpdateRejectsOutOfRangeLeverageBeforeSave(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "strategy-risk-guard.db")
|
||||
st, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("create store: %v", err)
|
||||
}
|
||||
a := New(nil, st, DefaultConfig(), slog.Default())
|
||||
|
||||
cfg := store.GetDefaultStrategyConfig("zh")
|
||||
rawCfg, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal strategy config: %v", err)
|
||||
}
|
||||
strategy := &store.Strategy{
|
||||
ID: "strategy-risk-guard",
|
||||
UserID: "default",
|
||||
Name: "AI500稳重策略",
|
||||
Description: "test",
|
||||
IsPublic: false,
|
||||
ConfigVisible: true,
|
||||
Config: string(rawCfg),
|
||||
}
|
||||
if err := st.Strategy().Create(strategy); err != nil {
|
||||
t.Fatalf("create strategy: %v", err)
|
||||
}
|
||||
|
||||
resp := a.toolManageStrategy("default", `{"action":"update","strategy_id":"strategy-risk-guard","config":{"risk_control":{"btc_eth_max_leverage":100,"altcoin_max_leverage":100}}}`)
|
||||
if !strings.Contains(resp, `不会按你给的原值直接保存`) {
|
||||
t.Fatalf("expected out-of-range leverage update to be rejected before save, got: %s", resp)
|
||||
}
|
||||
|
||||
updated, err := st.Strategy().Get("default", strategy.ID)
|
||||
if err != nil {
|
||||
t.Fatalf("reload strategy: %v", err)
|
||||
}
|
||||
parsed, err := updated.ParseConfig()
|
||||
if err != nil {
|
||||
t.Fatalf("parse updated strategy config: %v", err)
|
||||
}
|
||||
if parsed.RiskControl.BTCETHMaxLeverage != 5 || parsed.RiskControl.AltcoinMaxLeverage != 5 {
|
||||
t.Fatalf("expected stored leverage to remain unchanged at safe defaults, got btc_eth=%d alt=%d", parsed.RiskControl.BTCETHMaxLeverage, parsed.RiskControl.AltcoinMaxLeverage)
|
||||
}
|
||||
}
|
||||
|
||||
func TestExchangeSkillOptionSummaryMatchesManualPage(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "exchange-options.db")
|
||||
st, err := store.New(dbPath)
|
||||
@@ -123,3 +231,174 @@ func TestSkillVisibleFieldSummaryForExchangeUsesReadableNames(t *testing.T) {
|
||||
t.Fatalf("field summary should use readable labels instead of raw keys: %s", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkillVisibleFieldSummaryForStrategyCoversManualPageFields(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "strategy-field-summary.db")
|
||||
st, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("create store: %v", err)
|
||||
}
|
||||
a := New(nil, st, DefaultConfig(), slog.Default())
|
||||
|
||||
summary := a.skillVisibleFieldSummary("default", "zh", "strategy_management", "update_config")
|
||||
for _, expected := range []string{"发布到市场", "配置可见", "交易对", "杠杆", "主周期", "多周期时间框架", "NofxOS API key", "角色定义", "自定义 Prompt"} {
|
||||
if !strings.Contains(summary, expected) {
|
||||
t.Fatalf("expected field label %q in summary, got: %s", expected, summary)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestSkillVisibleFieldSummaryForTraderExcludesManualBalanceEditing(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "trader-field-summary.db")
|
||||
st, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("create store: %v", err)
|
||||
}
|
||||
a := New(nil, st, DefaultConfig(), slog.Default())
|
||||
|
||||
summary := a.skillVisibleFieldSummary("default", "zh", "trader_management", "update")
|
||||
for _, expected := range []string{"名称", "交易所", "模型", "策略", "扫描间隔"} {
|
||||
if !strings.Contains(summary, expected) {
|
||||
t.Fatalf("expected trader field label %q in summary, got: %s", expected, summary)
|
||||
}
|
||||
}
|
||||
if strings.Contains(summary, "初始资金") || strings.Contains(summary, "初始余额") {
|
||||
t.Fatalf("trader field summary should not expose manual balance editing: %s", summary)
|
||||
}
|
||||
}
|
||||
|
||||
func TestToolCreateTraderAutoReadsInitialBalanceFromExchange(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "trader-auto-balance.db")
|
||||
st, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("create store: %v", err)
|
||||
}
|
||||
a := New(nil, st, DefaultConfig(), slog.Default())
|
||||
|
||||
if err := st.AIModel().UpdateWithName("default", "default_deepseek", "DeepSeek", true, "sk-test-12345", "", "deepseek-chat"); err != nil {
|
||||
t.Fatalf("seed model: %v", err)
|
||||
}
|
||||
exchangeID, err := st.Exchange().Create("default", "binance", "Main", true, "api-test", "secret-test", "", false, "", false, "", "", "", "", "", "", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("seed exchange: %v", err)
|
||||
}
|
||||
cfg := store.GetDefaultStrategyConfig("zh")
|
||||
rawCfg, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal strategy config: %v", err)
|
||||
}
|
||||
if err := st.Strategy().Create(&store.Strategy{
|
||||
ID: "strategy-auto-balance",
|
||||
UserID: "default",
|
||||
Name: "Auto Balance Strategy",
|
||||
Description: "test",
|
||||
IsPublic: false,
|
||||
ConfigVisible: true,
|
||||
Config: string(rawCfg),
|
||||
}); err != nil {
|
||||
t.Fatalf("seed strategy: %v", err)
|
||||
}
|
||||
|
||||
originalFetcher := traderInitialBalanceFetcher
|
||||
traderInitialBalanceFetcher = func(exchangeCfg *store.Exchange, userID string) (float64, bool, error) {
|
||||
if exchangeCfg == nil || exchangeCfg.ID != exchangeID {
|
||||
t.Fatalf("unexpected exchange config passed to balance fetcher: %#v", exchangeCfg)
|
||||
}
|
||||
if userID != "default" {
|
||||
t.Fatalf("unexpected user id %q", userID)
|
||||
}
|
||||
return 4321.25, true, nil
|
||||
}
|
||||
defer func() {
|
||||
traderInitialBalanceFetcher = originalFetcher
|
||||
}()
|
||||
|
||||
resp := a.toolManageTrader("default", `{"action":"create","name":"奶茶","ai_model_id":"default_deepseek","exchange_id":"`+exchangeID+`","strategy_id":"strategy-auto-balance","initial_balance":999}`)
|
||||
if strings.Contains(resp, `"error"`) {
|
||||
t.Fatalf("expected trader create to succeed, got: %s", resp)
|
||||
}
|
||||
|
||||
traders, err := st.Trader().List("default")
|
||||
if err != nil {
|
||||
t.Fatalf("list traders: %v", err)
|
||||
}
|
||||
if len(traders) != 1 {
|
||||
t.Fatalf("expected one trader, got %d", len(traders))
|
||||
}
|
||||
if traders[0].InitialBalance != 4321.25 {
|
||||
t.Fatalf("expected initial balance to be auto-read from exchange, got %.2f", traders[0].InitialBalance)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDescribeStrategyIncludesManualPageSections(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "strategy-detail.db")
|
||||
st, err := store.New(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("create store: %v", err)
|
||||
}
|
||||
a := New(nil, st, DefaultConfig(), slog.Default())
|
||||
|
||||
cfg := store.GetDefaultStrategyConfig("zh")
|
||||
cfg.StrategyType = "grid_trading"
|
||||
cfg.GridConfig = &store.GridStrategyConfig{
|
||||
Symbol: "BTCUSDT",
|
||||
GridCount: 12,
|
||||
TotalInvestment: 1500,
|
||||
Leverage: 4,
|
||||
UpperPrice: 120000,
|
||||
LowerPrice: 90000,
|
||||
UseATRBounds: false,
|
||||
ATRMultiplier: 2,
|
||||
Distribution: "gaussian",
|
||||
MaxDrawdownPct: 15,
|
||||
StopLossPct: 5,
|
||||
DailyLossLimitPct: 10,
|
||||
UseMakerOnly: true,
|
||||
EnableDirectionAdjust: true,
|
||||
DirectionBiasRatio: 0.7,
|
||||
}
|
||||
cfg.CoinSource.SourceType = "mixed"
|
||||
cfg.CoinSource.StaticCoins = []string{"BTCUSDT", "ETHUSDT"}
|
||||
cfg.CoinSource.ExcludedCoins = []string{"DOGEUSDT"}
|
||||
cfg.Indicators.EnableOIRanking = true
|
||||
cfg.Indicators.EnableNetFlowRanking = true
|
||||
cfg.Indicators.EnablePriceRanking = true
|
||||
rawCfg, err := json.Marshal(cfg)
|
||||
if err != nil {
|
||||
t.Fatalf("marshal strategy config: %v", err)
|
||||
}
|
||||
|
||||
strategy := &store.Strategy{
|
||||
ID: "strategy-detail-1",
|
||||
UserID: "default",
|
||||
Name: "Grid Alpha",
|
||||
Description: "grid strategy for regression",
|
||||
IsPublic: true,
|
||||
ConfigVisible: true,
|
||||
Config: string(rawCfg),
|
||||
}
|
||||
if err := st.Strategy().Create(strategy); err != nil {
|
||||
t.Fatalf("create strategy: %v", err)
|
||||
}
|
||||
strategy.ConfigVisible = false
|
||||
if err := st.Strategy().Update(strategy); err != nil {
|
||||
t.Fatalf("update strategy visibility: %v", err)
|
||||
}
|
||||
|
||||
detail, ok := a.describeStrategy("default", "zh", &EntityReference{ID: strategy.ID})
|
||||
if !ok {
|
||||
t.Fatal("expected describeStrategy to resolve seeded strategy")
|
||||
}
|
||||
for _, expected := range []string{
|
||||
"策略“Grid Alpha”概览",
|
||||
"发布设置:已发布到市场;配置隐藏",
|
||||
"网格参数:交易对 BTCUSDT;网格 12;总投资 1500.00;杠杆 4;分布 gaussian",
|
||||
"网格边界:上沿 120000.0000,下沿 90000.0000",
|
||||
"标的来源:mixed | AI500=3 | static=BTCUSDT,ETHUSDT | excluded=DOGEUSDT",
|
||||
"NofxOS 数据:API Key=true,量化数据=true,OI 排行=true,净流入排行=true,价格排行=true",
|
||||
} {
|
||||
if !strings.Contains(detail, expected) {
|
||||
t.Fatalf("expected strategy detail to contain %q, got: %s", expected, detail)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user