mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-18 09:54:35 +08:00
feat: simplify Claw402 autopilot trading flow
This commit is contained in:
@@ -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)
|
||||
|
||||
@@ -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
44
store/position_test.go
Normal 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)
|
||||
}
|
||||
}
|
||||
@@ -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":
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user