feat: simplify Claw402 autopilot trading flow

This commit is contained in:
tinkle-community
2026-06-27 00:37:59 +08:00
parent 961e016d33
commit 24f6421a73
151 changed files with 7453 additions and 41117 deletions

View File

@@ -157,7 +157,7 @@ func IsClaw402Config(aiModel string) bool {
// EstimateRunway estimates how many days the given USDC balance will last
func EstimateRunway(usdcBalance float64, modelName string, scanIntervalMinutes int) (dailyCost float64, runwayDays float64) {
if scanIntervalMinutes <= 0 {
scanIntervalMinutes = 3
scanIntervalMinutes = 15
}
callsPerDay := float64(24*60) / float64(scanIntervalMinutes)
pricePerCall := GetModelPrice(modelName)

View File

@@ -116,8 +116,8 @@ type TraderPosition struct {
Status string `gorm:"column:status;default:OPEN;index:idx_positions_status" json:"status"`
CloseReason string `gorm:"column:close_reason;default:''" json:"close_reason"`
Source string `gorm:"column:source;default:system" json:"source"`
CreatedAt int64 `gorm:"column:created_at" json:"created_at"` // Unix milliseconds UTC
UpdatedAt int64 `gorm:"column:updated_at" json:"updated_at"` // Unix milliseconds UTC
CreatedAt int64 `gorm:"column:created_at" json:"created_at"` // Unix milliseconds UTC
UpdatedAt int64 `gorm:"column:updated_at" json:"updated_at"` // Unix milliseconds UTC
}
// TableName returns the table name
@@ -198,14 +198,14 @@ func (s *PositionStore) Create(pos *TraderPosition) error {
func (s *PositionStore) ClosePosition(id int64, exitPrice float64, exitOrderID string, realizedPnL float64, fee float64, closeReason string) error {
nowMs := time.Now().UTC().UnixMilli()
return s.db.Model(&TraderPosition{}).Where("id = ?", id).Updates(map[string]interface{}{
"exit_price": exitPrice,
"exit_price": exitPrice,
"exit_order_id": exitOrderID,
"exit_time": nowMs,
"realized_pnl": realizedPnL,
"fee": fee,
"status": "CLOSED",
"close_reason": closeReason,
"updated_at": nowMs,
"exit_time": nowMs,
"realized_pnl": realizedPnL,
"fee": fee,
"status": "CLOSED",
"close_reason": closeReason,
"updated_at": nowMs,
}).Error
}
@@ -311,15 +311,15 @@ func (s *PositionStore) ClosePositionFully(id int64, exitPrice float64, exitOrde
}
return s.db.Model(&TraderPosition{}).Where("id = ?", id).Updates(map[string]interface{}{
"quantity": quantity,
"exit_price": exitPrice,
"exit_order_id": exitOrderID,
"exit_time": exitTimeMs,
"realized_pnl": totalRealizedPnL,
"fee": totalFee,
"status": "CLOSED",
"close_reason": closeReason,
"updated_at": time.Now().UTC().UnixMilli(),
"quantity": quantity,
"exit_price": exitPrice,
"exit_order_id": exitOrderID,
"exit_time": exitTimeMs,
"realized_pnl": totalRealizedPnL,
"fee": totalFee,
"status": "CLOSED",
"close_reason": closeReason,
"updated_at": time.Now().UTC().UnixMilli(),
}).Error
}
@@ -350,7 +350,7 @@ func (s *PositionStore) GetOpenPositions(traderID string) ([]*TraderPosition, er
// GetOpenPositionBySymbol gets open position for specified symbol and direction
func (s *PositionStore) GetOpenPositionBySymbol(traderID, symbol, side string) (*TraderPosition, error) {
var pos TraderPosition
err := s.db.Where("trader_id = ? AND symbol = ? AND side = ? AND status = ?", traderID, symbol, side, "OPEN").
err := s.db.Where("trader_id = ? AND symbol = ? AND UPPER(side) = UPPER(?) AND status = ?", traderID, symbol, side, "OPEN").
Order("entry_time DESC").
First(&pos).Error
@@ -365,7 +365,7 @@ func (s *PositionStore) GetOpenPositionBySymbol(traderID, symbol, side string) (
// Try without USDT suffix for backward compatibility
if strings.HasSuffix(symbol, "USDT") {
baseSymbol := strings.TrimSuffix(symbol, "USDT")
err = s.db.Where("trader_id = ? AND symbol = ? AND side = ? AND status = ?", traderID, baseSymbol, side, "OPEN").
err = s.db.Where("trader_id = ? AND symbol = ? AND UPPER(side) = UPPER(?) AND status = ?", traderID, baseSymbol, side, "OPEN").
Order("entry_time DESC").
First(&pos).Error
if err == nil {

44
store/position_test.go Normal file
View File

@@ -0,0 +1,44 @@
package store
import (
"testing"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func TestGetOpenPositionBySymbolMatchesSideCaseInsensitively(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatalf("open in-memory sqlite: %v", err)
}
positions := NewPositionStore(db)
if err := positions.InitTables(); err != nil {
t.Fatalf("init position table: %v", err)
}
entryTime := time.Now().Add(-5 * time.Minute).UnixMilli()
if err := positions.Create(&TraderPosition{
TraderID: "trader-1",
Symbol: "AAVEUSDT",
Side: "LONG",
Quantity: 0.27,
EntryPrice: 88.519,
EntryTime: entryTime,
}); err != nil {
t.Fatalf("create position: %v", err)
}
got, err := positions.GetOpenPositionBySymbol("trader-1", "AAVEUSDT", "long")
if err != nil {
t.Fatalf("get open position: %v", err)
}
if got == nil {
t.Fatal("expected open position")
}
if got.EntryTime != entryTime {
t.Fatalf("entry time mismatch: got %d want %d", got.EntryTime, entryTime)
}
}

View File

@@ -46,6 +46,9 @@ func (c *StrategyConfig) ClampLimits() {
if c.CoinSource.OILowLimit > MaxCandidateCoins {
c.CoinSource.OILowLimit = MaxCandidateCoins
}
if c.CoinSource.VergexLimit > MaxCandidateCoins {
c.CoinSource.VergexLimit = MaxCandidateCoins
}
// Clamp static coins
if len(c.CoinSource.StaticCoins) > MaxCandidateCoins {
@@ -136,6 +139,8 @@ func (c *StrategyConfig) ClampLimits() {
// must use the exact frontend/backend enum values.
func (c *StrategyConfig) NormalizeProductSchema() {
c.StrategyType = normalizeStrategyType(c.StrategyType)
c.CoinSource.StaticCoins = normalizeSymbols(c.CoinSource.StaticCoins)
c.CoinSource.ExcludedCoins = normalizeSymbols(c.CoinSource.ExcludedCoins)
c.CoinSource.SourceType = normalizeCoinSourceType(c.CoinSource.SourceType)
if c.CoinSource.SourceType == "" {
c.CoinSource.SourceType = inferCoinSourceType(c.CoinSource)
@@ -205,26 +210,53 @@ func (c *StrategyConfig) NormalizeProductSchema() {
if c.CoinSource.HyperRankLimit <= 0 {
c.CoinSource.HyperRankLimit = 5
}
default:
c.CoinSource.SourceType = "hyper_rank"
case "vergex_signal":
c.CoinSource.UseAI500 = false
c.CoinSource.UseOITop = false
c.CoinSource.UseOILow = false
c.CoinSource.UseHyperAll = false
c.CoinSource.UseHyperMain = false
if c.CoinSource.HyperRankCategory == "" {
c.CoinSource.HyperRankCategory = "stock"
minLimit := 10
if len(c.CoinSource.StaticCoins) > 0 {
minLimit = len(c.CoinSource.StaticCoins)
if minLimit > MaxCandidateCoins {
minLimit = MaxCandidateCoins
}
}
if c.CoinSource.HyperRankDirection == "" {
c.CoinSource.HyperRankDirection = "gainers"
if c.CoinSource.VergexLimit < minLimit {
c.CoinSource.VergexLimit = minLimit
}
if c.CoinSource.HyperRankLimit <= 0 {
c.CoinSource.HyperRankLimit = 5
if c.CoinSource.VergexMarketType == "" {
c.CoinSource.VergexMarketType = "all"
}
if c.CoinSource.VergexChain == "" {
c.CoinSource.VergexChain = "hyperliquid"
}
default:
c.CoinSource.SourceType = "vergex_signal"
c.CoinSource.UseAI500 = false
c.CoinSource.UseOITop = false
c.CoinSource.UseOILow = false
c.CoinSource.UseHyperAll = false
c.CoinSource.UseHyperMain = false
minLimit := 10
if len(c.CoinSource.StaticCoins) > 0 {
minLimit = len(c.CoinSource.StaticCoins)
if minLimit > MaxCandidateCoins {
minLimit = MaxCandidateCoins
}
}
if c.CoinSource.VergexLimit < minLimit {
c.CoinSource.VergexLimit = minLimit
}
if c.CoinSource.VergexMarketType == "" {
c.CoinSource.VergexMarketType = "all"
}
if c.CoinSource.VergexChain == "" {
c.CoinSource.VergexChain = "hyperliquid"
}
}
c.CoinSource.StaticCoins = normalizeSymbols(c.CoinSource.StaticCoins)
c.CoinSource.ExcludedCoins = normalizeSymbols(c.CoinSource.ExcludedCoins)
c.Indicators.Klines.PrimaryTimeframe = normalizeTimeframe(c.Indicators.Klines.PrimaryTimeframe)
c.Indicators.Klines.LongerTimeframe = normalizeTimeframe(c.Indicators.Klines.LongerTimeframe)
c.Indicators.Klines.SelectedTimeframes = normalizeTimeframes(c.Indicators.Klines.SelectedTimeframes)
@@ -257,8 +289,10 @@ func normalizeCoinSourceType(value string) string {
return "oi_top"
case strings.Contains(compact, "oilow") || strings.Contains(value, "oi low") || strings.Contains(value, "持仓量最低") || strings.Contains(value, "持仓量较低"):
return "oi_low"
case strings.Contains(compact, "hyperrank") || strings.Contains(compact, "dynamicranking") || strings.Contains(value, "动态榜单") || strings.Contains(value, "涨幅榜"):
case strings.Contains(compact, "hyperrank"):
return "hyper_rank"
case strings.Contains(compact, "vergex") || strings.Contains(compact, "claw402") || strings.Contains(compact, "dynamicranking") || strings.Contains(value, "动态榜单") || strings.Contains(value, "涨幅榜") || strings.Contains(value, "信号榜"):
return "vergex_signal"
case strings.Contains(compact, "hyperall"):
return "hyper_all"
case strings.Contains(compact, "hypermain"):
@@ -284,10 +318,12 @@ func inferCoinSourceType(source CoinSourceConfig) string {
return "hyper_all"
case source.UseHyperMain:
return "hyper_main"
case source.VergexLimit > 0 || source.VergexMarketType != "" || source.VergexChain != "" || source.VergexLiqBand != "":
return "vergex_signal"
case source.HyperRankCategory != "" || source.HyperRankDirection != "" || source.HyperRankLimit > 0:
return "hyper_rank"
default:
return "hyper_rank"
return "vergex_signal"
}
}
@@ -783,6 +819,14 @@ type CoinSourceConfig struct {
HyperRankDirection string `json:"hyper_rank_direction,omitempty"`
// Hyperliquid dynamic ranking maximum count. Defaults to 5 and is hard capped at 10 for AI context safety.
HyperRankLimit int `json:"hyper_rank_limit,omitempty"`
// Vergex signal-ranking maximum count. Defaults to 5 and is hard capped at 10.
VergexLimit int `json:"vergex_limit,omitempty"`
// Vergex market type for detail endpoints, e.g. hip3_perp for Hyperliquid TradeFi perps.
VergexMarketType string `json:"vergex_market_type,omitempty"`
// Vergex chain query parameter. Defaults to hyperliquid.
VergexChain string `json:"vergex_chain,omitempty"`
// Vergex liquidation band query parameter.
VergexLiqBand string `json:"vergex_liq_band,omitempty"`
// Note: API URLs are now built automatically using NofxOSAPIKey from IndicatorConfig
}
@@ -916,28 +960,29 @@ func GetDefaultStrategyConfig(lang string) StrategyConfig {
config := StrategyConfig{
Language: normalizedLang,
CoinSource: CoinSourceConfig{
SourceType: "hyper_rank",
UseAI500: false,
AI500Limit: 3,
UseOITop: false,
OITopLimit: 3,
UseOILow: false,
OILowLimit: 3,
UseHyperAll: false,
UseHyperMain: false,
HyperMainLimit: 30,
HyperRankCategory: "stock",
HyperRankDirection: "gainers",
HyperRankLimit: 5,
SourceType: "vergex_signal",
UseAI500: false,
AI500Limit: 3,
UseOITop: false,
OITopLimit: 3,
UseOILow: false,
OILowLimit: 3,
UseHyperAll: false,
UseHyperMain: false,
HyperMainLimit: 30,
HyperRankCategory: "all",
VergexLimit: 10,
VergexMarketType: "all",
VergexChain: "hyperliquid",
},
Indicators: IndicatorConfig{
Klines: KlineConfig{
PrimaryTimeframe: "5m",
PrimaryCount: 20,
LongerTimeframe: "4h",
LongerCount: 10,
EnableMultiTimeframe: true,
SelectedTimeframes: []string{"5m", "15m", "1h"},
PrimaryTimeframe: "15m",
PrimaryCount: 30,
LongerTimeframe: "",
LongerCount: 0,
EnableMultiTimeframe: false,
SelectedTimeframes: []string{"15m"},
},
EnableRawKlines: true, // Required - raw OHLCV data for AI analysis
EnableEMA: false,
@@ -945,9 +990,9 @@ func GetDefaultStrategyConfig(lang string) StrategyConfig {
EnableRSI: false,
EnableATR: false,
EnableBOLL: false,
EnableVolume: true,
EnableOI: true,
EnableFundingRate: true,
EnableVolume: false,
EnableOI: false,
EnableFundingRate: false,
EMAPeriods: []int{20, 50},
RSIPeriods: []int{7, 14},
ATRPeriods: []int{14},
@@ -969,57 +1014,57 @@ func GetDefaultStrategyConfig(lang string) StrategyConfig {
PriceRankingLimit: 10,
},
RiskControl: RiskControlConfig{
MaxPositions: 3, // Max 3 coins simultaneously (CODE ENFORCED)
BTCETHMaxLeverage: 5, // BTC/ETH exchange leverage (AI guided)
AltcoinMaxLeverage: 5, // Altcoin exchange leverage (AI guided)
BTCETHMaxPositionValueRatio: 5.0, // BTC/ETH: max position = 5x equity (CODE ENFORCED)
AltcoinMaxPositionValueRatio: 1.0, // Altcoin: max position = 1x equity (CODE ENFORCED)
MaxMarginUsage: 0.9, // Max 90% margin usage (CODE ENFORCED)
MinPositionSize: 12, // Min 12 USDT per position (CODE ENFORCED)
MinRiskRewardRatio: 3.0, // Min 3:1 profit/loss ratio (AI guided)
MinConfidence: 75, // Min 75% confidence (AI guided)
MaxPositions: 2, // Max 2 instruments simultaneously (CODE ENFORCED)
BTCETHMaxLeverage: 10, // BTC/ETH exchange leverage (AI guided)
AltcoinMaxLeverage: 10, // TradeFi exchange leverage (AI guided)
BTCETHMaxPositionValueRatio: 1.0, // Claw402 default: same cap across assets
AltcoinMaxPositionValueRatio: 1.0, // Claw402 default: same cap across assets
MaxMarginUsage: 0.35, // Max 35% margin usage (CODE ENFORCED)
MinPositionSize: 12, // Min 12 USDT per position (CODE ENFORCED)
MinRiskRewardRatio: 3.0, // Min 3:1 profit/loss ratio (AI guided)
MinConfidence: 78, // Min 78% confidence (AI guided)
},
}
if lang == "zh" {
config.PromptSections = PromptSectionsConfig{
RoleDefinition: `# 你是一个专业的 Hyperliquid USDC 多资产交易AI
RoleDefinition: `# 你是 NOFX Claw402 自动交易员
的任务是根据提供的市场数据做出交易决策。你可以分析并交易 Hyperliquid 上线的 USDC 永续合约,包括美股、大宗商品和加密资产。你是一个经验丰富的量化交易员,擅长跨资产技术分析和风险管理`,
TradingFrequency: `# ⏱️ 交易频率意识
只交易 Claw402.ai/Vergex 本轮榜单返回的 Hyperliquid 可交易标的。候选池来自 Claw402.ai/Vergex开仓前必须结合 Signal Lab、成本/清算热力图和原始 K 线判断`,
TradingFrequency: `# 交易频率
- 优秀交易员每天2-4笔 ≈ 每小时0.1-0.2笔
- 每小时超过2笔 = 过度交易
- 单笔持仓时间 ≥ 30-60分钟
如果你发现自己每个周期都在交易 → 标准太低如果持仓不到30分钟就平仓 → 太冲动。`,
EntryStandards: `# 🎯 入场标准(严格)
- 优先等待高质量机会,不需要每轮都交易。
- 先管理已有持仓,再考虑新开仓。
- 同一轮不要频繁开平同一标的。`,
EntryStandards: `# 入场标准
在多个信号共振时入场。自由使用任何有效的分析方法,避免单一指标、信号矛盾、横盘震荡、或平仓后立即重新开仓等低质量行为`,
DecisionProcess: `# 📋 决策流程
有 Claw402 Signal Lab、成本/清算热力图和原始 K 线大体一致时才开仓。Claw402 排名只是候选池,不是单独买入理由。任一关键数据缺失或冲突时,默认等待`,
DecisionProcess: `# 决策流程
1. 检查持仓 → 是否止盈/止损
2. 扫描候选币种 + 多时间框架 → 是否存在强信号
3. 先写思维链再输出结构化JSON`,
1. 检查已有持仓,先决定止盈止损或继续持有。
2. 从 Claw402 榜单取本轮候选,并对每个候选读取 Claw402 Ranking、Signal Lab、Cost/Liquidation Heatmap。
3. 用原始 K 线确认入场位置、止损和止盈。
4. 输出简洁 reasoning 和严格 JSON。`,
}
} else {
config.PromptSections = PromptSectionsConfig{
RoleDefinition: `# You are a professional Hyperliquid USDC multi-asset trading AI
RoleDefinition: `# You are the NOFX Claw402 auto-trader
Your task is to make trading decisions based on the provided market data. You can analyze and trade Hyperliquid-listed USDC perpetual markets, including US equities, commodities and crypto assets. You are an experienced quantitative trader skilled in cross-asset technical analysis and risk management.`,
TradingFrequency: `# ⏱️ Trading Frequency Awareness
Trade Hyperliquid Claw402-ranked instruments only. The candidate pool comes from Claw402.ai/Vergex; before opening a position, combine Signal Lab, cost/liquidation heatmap and raw candles.`,
TradingFrequency: `# Trading Frequency
- Excellent trader: 2-4 trades per day ≈ 0.1-0.2 trades per hour
- >2 trades per hour = overtrading
- Single position holding time ≥ 30-60 minutes
If you find yourself trading every cycle → standards are too low; if closing positions in <30 minutes → too impulsive.`,
EntryStandards: `# 🎯 Entry Standards (Strict)
- Wait for quality; you do not need to trade every cycle.
- Manage existing positions before opening new ones.
- Do not churn in and out of the same symbol in one cycle.`,
EntryStandards: `# Entry Standards
Only enter positions when multiple signals resonate. Freely use any effective analysis methods, avoid low-quality behaviors such as single indicators, contradictory signals, sideways oscillation, or immediately restarting after closing positions.`,
DecisionProcess: `# 📋 Decision Process
Open only when Claw402 Signal Lab, cost/liquidation heatmap and raw candles broadly agree. Ranking defines the candidate pool, not a standalone entry reason. Wait when key data is missing or contradictory.`,
DecisionProcess: `# Decision Process
1. Check positions → whether to take profit/stop loss
2. Scan candidate coins + multi-timeframe → whether strong signals exist
3. Write chain of thought first, then output structured JSON`,
1. Check current positions first: take profit, stop loss or hold.
2. Pull this cycle's Claw402 board and read Claw402 Ranking, Signal Lab and Cost/Liquidation Heatmap for each candidate.
3. Use raw candles to confirm entry, stop and target.
4. Output concise reasoning and strict JSON.`,
}
}
@@ -1461,6 +1506,8 @@ func (c *StrategyConfig) getEffectiveCoinCount() int {
count = c.CoinSource.OILowLimit
case "hyper_rank":
count = c.CoinSource.HyperRankLimit
case "vergex_signal":
count = c.CoinSource.VergexLimit
case "hyper_main":
count = c.CoinSource.HyperMainLimit
case "hyper_all":

View File

@@ -2,41 +2,41 @@ package store
import "testing"
func TestDefaultHyperliquidStrategyDoesNotEnableNofxOSData(t *testing.T) {
func TestDefaultVergexStrategyDoesNotEnableNofxOSData(t *testing.T) {
cfg := GetDefaultStrategyConfig("zh")
assertHyperliquidStockRankDefault(t, cfg)
assertVergexSignalDefault(t, cfg)
ind := cfg.Indicators
if ind.NofxOSAPIKey != "" {
t.Fatalf("default should not include a NofxOS API key for Hyperliquid strategies")
t.Fatalf("default should not include a NofxOS API key for Claw402/Vergex strategies")
}
if ind.EnableQuantData || ind.EnableQuantOI || ind.EnableQuantNetflow || ind.EnableOIRanking || ind.EnableNetFlowRanking || ind.EnablePriceRanking {
t.Fatalf("default Hyperliquid strategy must not enable NofxOS datasets: %+v", ind)
t.Fatalf("default Claw402/Vergex strategy must not enable NofxOS datasets: %+v", ind)
}
if !ind.EnableRawKlines {
t.Fatalf("raw Hyperliquid klines must stay enabled")
}
}
func TestHyperliquidRankDefaultSurvivesClampAndNormalize(t *testing.T) {
func TestVergexSignalDefaultSurvivesClampAndNormalize(t *testing.T) {
cfg := GetDefaultStrategyConfig("zh")
cfg.CoinSource.UseAI500 = true
cfg.ClampLimits()
assertHyperliquidStockRankDefault(t, cfg)
assertVergexSignalDefault(t, cfg)
if cfg.CoinSource.UseAI500 {
t.Fatalf("Hyperliquid rank strategy must clear stale AI500 flag: %+v", cfg.CoinSource)
t.Fatalf("Claw402/Vergex signal strategy must clear stale AI500 flag: %+v", cfg.CoinSource)
}
}
func TestEmptyCoinSourceInfersHyperliquidRankNotAI500(t *testing.T) {
func TestEmptyCoinSourceInfersVergexSignalNotAI500(t *testing.T) {
cfg := GetDefaultStrategyConfig("zh")
cfg.CoinSource = CoinSourceConfig{}
cfg.NormalizeProductSchema()
assertHyperliquidStockRankDefault(t, cfg)
assertVergexSignalDefault(t, cfg)
}
func assertHyperliquidStockRankDefault(t *testing.T, cfg StrategyConfig) {
func assertVergexSignalDefault(t *testing.T, cfg StrategyConfig) {
t.Helper()
if cfg.CoinSource.SourceType != "hyper_rank" || cfg.CoinSource.HyperRankCategory != "stock" || cfg.CoinSource.HyperRankDirection != "gainers" || cfg.CoinSource.HyperRankLimit != 5 {
t.Fatalf("coin source = %+v, want Hyperliquid dynamic stock gainers top 5", cfg.CoinSource)
if cfg.CoinSource.SourceType != "vergex_signal" || cfg.CoinSource.VergexLimit != 10 || cfg.CoinSource.VergexMarketType != "all" || cfg.CoinSource.VergexChain != "hyperliquid" {
t.Fatalf("coin source = %+v, want Claw402/Vergex all-market signal top 10", cfg.CoinSource)
}
}

View File

@@ -122,3 +122,62 @@ func TestStrategyConfigNormalizeProductSchemaForLLMLabels(t *testing.T) {
}
}
}
func TestStrategyConfigNormalizeProductSchemaForVergexSignal(t *testing.T) {
cfg := GetDefaultStrategyConfig("zh")
cfg.CoinSource = CoinSourceConfig{
SourceType: "Claw402 Vergex 信号榜",
}
cfg.NormalizeProductSchema()
if cfg.CoinSource.SourceType != "vergex_signal" {
t.Fatalf("source_type = %q, want vergex_signal", cfg.CoinSource.SourceType)
}
if cfg.CoinSource.VergexLimit != 10 {
t.Fatalf("vergex_limit = %d, want 10", cfg.CoinSource.VergexLimit)
}
if cfg.CoinSource.VergexMarketType != "all" {
t.Fatalf("vergex_market_type = %q, want all", cfg.CoinSource.VergexMarketType)
}
if cfg.CoinSource.VergexChain != "hyperliquid" {
t.Fatalf("vergex_chain = %q, want hyperliquid", cfg.CoinSource.VergexChain)
}
}
func TestStrategyConfigNormalizeProductSchemaForVergexSignalLimits(t *testing.T) {
t.Run("dynamic board keeps the one built-in strategy candidate depth", func(t *testing.T) {
cfg := GetDefaultStrategyConfig("zh")
cfg.CoinSource = CoinSourceConfig{
SourceType: "vergex_signal",
VergexLimit: 1,
StaticCoins: nil,
VergexChain: "hyperliquid",
VergexLiqBand: "",
}
cfg.NormalizeProductSchema()
if cfg.CoinSource.VergexLimit != 10 {
t.Fatalf("vergex_limit = %d, want 10", cfg.CoinSource.VergexLimit)
}
})
t.Run("manual picks keep selected count", func(t *testing.T) {
cfg := GetDefaultStrategyConfig("zh")
cfg.CoinSource = CoinSourceConfig{
SourceType: "vergex_signal",
VergexLimit: 1,
StaticCoins: []string{"xyz:nvda", "XYZ:AAPL"},
}
cfg.NormalizeProductSchema()
if cfg.CoinSource.VergexLimit != 2 {
t.Fatalf("vergex_limit = %d, want 2", cfg.CoinSource.VergexLimit)
}
if got := cfg.CoinSource.StaticCoins; len(got) != 2 || got[0] != "XYZ:NVDA" || got[1] != "XYZ:AAPL" {
t.Fatalf("static_coins = %+v, want normalized xyz symbols", got)
}
})
}

View File

@@ -26,7 +26,7 @@ type Trader struct {
ExchangeID string `gorm:"column:exchange_id;not null" json:"exchange_id"`
StrategyID string `gorm:"column:strategy_id;default:''" json:"strategy_id"`
InitialBalance float64 `gorm:"column:initial_balance;not null" json:"initial_balance"`
ScanIntervalMinutes int `gorm:"column:scan_interval_minutes;default:3" json:"scan_interval_minutes"`
ScanIntervalMinutes int `gorm:"column:scan_interval_minutes;default:15" json:"scan_interval_minutes"`
IsRunning bool `gorm:"column:is_running;default:false" json:"is_running"`
IsCrossMargin bool `gorm:"column:is_cross_margin;default:true" json:"is_cross_margin"`
ShowInCompetition bool `gorm:"column:show_in_competition;default:true" json:"show_in_competition"`