mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-06-06 05:51:19 +08:00
feat(strategy): support Hyperliquid stock strategy editing
- Extend strategy storage and engine analysis for Hyperliquid defaults - Rework coin source and indicator editors for the stock strategy flow - Update Strategy Studio translations and page wiring
This commit is contained in:
126
kernel/engine.go
126
kernel/engine.go
@@ -6,13 +6,14 @@ import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"nofx/logger"
|
||||
"nofx/market"
|
||||
"nofx/provider/hyperliquid"
|
||||
"nofx/provider/nofxos"
|
||||
"nofx/security"
|
||||
"nofx/store"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
@@ -224,6 +225,22 @@ func NewStrategyEngine(config *store.StrategyConfig, claw402WalletKey ...string)
|
||||
}
|
||||
}
|
||||
|
||||
func (e *StrategyEngine) usesHyperliquidNativeUniverse() bool {
|
||||
if e == nil || e.config == nil {
|
||||
return false
|
||||
}
|
||||
source := e.config.CoinSource
|
||||
if source.SourceType == "hyper_all" || source.SourceType == "hyper_main" || source.SourceType == "hyper_rank" || source.UseHyperAll || source.UseHyperMain {
|
||||
return true
|
||||
}
|
||||
for _, symbol := range source.StaticCoins {
|
||||
if market.IsXyzDexAsset(symbol) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// GetRiskControlConfig gets risk control configuration
|
||||
func (e *StrategyEngine) GetRiskControlConfig() store.RiskControlConfig {
|
||||
return e.config.RiskControl
|
||||
@@ -368,6 +385,13 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
||||
}
|
||||
return e.filterExcludedCoins(coins), nil
|
||||
|
||||
case "hyper_rank":
|
||||
coins, err := e.getHyperRankCoins(coinSource.HyperRankCategory, coinSource.HyperRankDirection, coinSource.HyperRankLimit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return e.filterExcludedCoins(coins), nil
|
||||
|
||||
case "mixed":
|
||||
if coinSource.UseAI500 {
|
||||
poolCoins, err := e.getAI500Coins(coinSource.AI500Limit)
|
||||
@@ -586,6 +610,90 @@ func (e *StrategyEngine) getHyperMainCoins(limit int) ([]CandidateCoin, error) {
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
func clampHyperRankLimit(limit int) int {
|
||||
if limit <= 0 {
|
||||
return 5
|
||||
}
|
||||
if limit > 10 {
|
||||
return 10
|
||||
}
|
||||
return limit
|
||||
}
|
||||
|
||||
func (e *StrategyEngine) getHyperRankCoins(category, direction string, limit int) ([]CandidateCoin, error) {
|
||||
category = strings.ToLower(strings.TrimSpace(category))
|
||||
if category == "" {
|
||||
category = "stock"
|
||||
}
|
||||
direction = strings.ToLower(strings.TrimSpace(direction))
|
||||
if direction == "" {
|
||||
direction = "gainers"
|
||||
}
|
||||
limit = clampHyperRankLimit(limit)
|
||||
|
||||
ctx := context.Background()
|
||||
var ranked []struct {
|
||||
symbol string
|
||||
info hyperliquid.CoinInfo
|
||||
cat string
|
||||
}
|
||||
|
||||
if category == "crypto" || category == "all" {
|
||||
coins, err := hyperliquid.GetPerpDexCoins(ctx, "")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Hyperliquid crypto ranking: %w", err)
|
||||
}
|
||||
for _, coin := range coins {
|
||||
ranked = append(ranked, struct {
|
||||
symbol string
|
||||
info hyperliquid.CoinInfo
|
||||
cat string
|
||||
}{symbol: market.Normalize(coin.Symbol + "USDT"), info: coin, cat: "crypto"})
|
||||
}
|
||||
}
|
||||
|
||||
if category != "crypto" {
|
||||
coins, err := hyperliquid.GetPerpDexCoins(ctx, "xyz")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get Hyperliquid XYZ ranking: %w", err)
|
||||
}
|
||||
for _, coin := range coins {
|
||||
base := strings.TrimPrefix(coin.Symbol, "xyz:")
|
||||
cat := hyperliquid.XYZCategory(base)
|
||||
if category != "all" && cat != category {
|
||||
continue
|
||||
}
|
||||
ranked = append(ranked, struct {
|
||||
symbol string
|
||||
info hyperliquid.CoinInfo
|
||||
cat string
|
||||
}{symbol: hyperliquid.FormatCoinForAPI("xyz:" + base), info: coin, cat: cat})
|
||||
}
|
||||
}
|
||||
|
||||
sort.SliceStable(ranked, func(i, j int) bool {
|
||||
switch direction {
|
||||
case "losers":
|
||||
return ranked[i].info.Change24hPct < ranked[j].info.Change24hPct
|
||||
case "volume":
|
||||
return ranked[i].info.Volume24h > ranked[j].info.Volume24h
|
||||
default:
|
||||
return ranked[i].info.Change24hPct > ranked[j].info.Change24hPct
|
||||
}
|
||||
})
|
||||
|
||||
if len(ranked) > limit {
|
||||
ranked = ranked[:limit]
|
||||
}
|
||||
candidates := make([]CandidateCoin, 0, len(ranked))
|
||||
source := fmt.Sprintf("hyper_rank_%s_%s", category, direction)
|
||||
for _, item := range ranked {
|
||||
candidates = append(candidates, CandidateCoin{Symbol: item.symbol, Sources: []string{source}})
|
||||
}
|
||||
logger.Infof("✅ Loaded %d Hyperliquid rank coins (%s/%s, capped at %d)", len(candidates), category, direction, limit)
|
||||
return candidates, nil
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// External & Quant Data
|
||||
// ============================================================================
|
||||
@@ -677,6 +785,10 @@ func (e *StrategyEngine) FetchQuantData(symbol string) (*QuantData, error) {
|
||||
if !e.config.Indicators.EnableQuantData {
|
||||
return nil, nil
|
||||
}
|
||||
if e.usesHyperliquidNativeUniverse() || market.IsXyzDexAsset(symbol) {
|
||||
logger.Infof("⏭️ Skipping NofxOS quant data for Hyperliquid symbol %s; using native Hyperliquid klines/mark data only", symbol)
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
// Use nofxos client with unified API key
|
||||
include := "oi,price"
|
||||
@@ -773,6 +885,10 @@ func (e *StrategyEngine) FetchOIRankingData() *nofxos.OIRankingData {
|
||||
if !indicators.EnableOIRanking {
|
||||
return nil
|
||||
}
|
||||
if e.usesHyperliquidNativeUniverse() {
|
||||
logger.Infof("⏭️ Skipping NofxOS OI ranking for Hyperliquid strategy; native Hyperliquid universe is the source of truth")
|
||||
return nil
|
||||
}
|
||||
|
||||
duration := indicators.OIRankingDuration
|
||||
if duration == "" {
|
||||
@@ -804,6 +920,10 @@ func (e *StrategyEngine) FetchNetFlowRankingData() *nofxos.NetFlowRankingData {
|
||||
if !indicators.EnableNetFlowRanking {
|
||||
return nil
|
||||
}
|
||||
if e.usesHyperliquidNativeUniverse() {
|
||||
logger.Infof("⏭️ Skipping NofxOS netflow ranking for Hyperliquid strategy; native Hyperliquid universe is the source of truth")
|
||||
return nil
|
||||
}
|
||||
|
||||
duration := indicators.NetFlowRankingDuration
|
||||
if duration == "" {
|
||||
@@ -836,6 +956,10 @@ func (e *StrategyEngine) FetchPriceRankingData() *nofxos.PriceRankingData {
|
||||
if !indicators.EnablePriceRanking {
|
||||
return nil
|
||||
}
|
||||
if e.usesHyperliquidNativeUniverse() {
|
||||
logger.Infof("⏭️ Skipping NofxOS price ranking for Hyperliquid strategy; native Hyperliquid universe is the source of truth")
|
||||
return nil
|
||||
}
|
||||
|
||||
durations := indicators.PriceRankingDuration
|
||||
if durations == "" {
|
||||
|
||||
@@ -84,6 +84,7 @@ func GetFullDecisionWithStrategy(ctx *Context, mcpClient mcp.AIClient, engine *S
|
||||
return nil, fmt.Errorf("failed to fetch market data: %w", err)
|
||||
}
|
||||
}
|
||||
pruneCandidateCoinsWithoutMarketData(ctx)
|
||||
|
||||
// Ensure OITopDataMap is initialized
|
||||
if ctx.OITopDataMap == nil {
|
||||
@@ -223,6 +224,21 @@ func fetchMarketDataWithStrategy(ctx *Context, engine *StrategyEngine) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func pruneCandidateCoinsWithoutMarketData(ctx *Context) {
|
||||
if ctx == nil || len(ctx.CandidateCoins) == 0 || len(ctx.MarketDataMap) == 0 {
|
||||
return
|
||||
}
|
||||
kept := make([]CandidateCoin, 0, len(ctx.CandidateCoins))
|
||||
for _, coin := range ctx.CandidateCoins {
|
||||
if _, ok := ctx.MarketDataMap[coin.Symbol]; ok {
|
||||
kept = append(kept, coin)
|
||||
continue
|
||||
}
|
||||
logger.Infof("⚠️ Skipping candidate %s in AI prompt: no valid market/K-line data", coin.Symbol)
|
||||
}
|
||||
ctx.CandidateCoins = kept
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// AI Response Parsing
|
||||
// ============================================================================
|
||||
|
||||
Reference in New Issue
Block a user