Files
nofx/kernel/engine.go
shinchan-zhai af6f6d5930 feat: auto-reuse claw402 wallet for nofxos data — no extra config needed
When a trader uses claw402 as AI provider, the same wallet private key
is now automatically used to route nofxos data API calls (AI500, OI,
NetFlow, etc.) through claw402 payment as well.

Users don't need to configure anything extra — if they already set up
claw402 for AI, data APIs automatically go through claw402 too.
2026-03-25 10:08:55 +08:00

877 lines
26 KiB
Go

package kernel
import (
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"nofx/logger"
"nofx/market"
"nofx/provider/hyperliquid"
"nofx/provider/nofxos"
"nofx/security"
"nofx/store"
"strings"
"time"
)
// ============================================================================
// Type Definitions
// ============================================================================
// PositionInfo position information
type PositionInfo struct {
Symbol string `json:"symbol"`
Side string `json:"side"` // "long" or "short"
EntryPrice float64 `json:"entry_price"`
MarkPrice float64 `json:"mark_price"`
Quantity float64 `json:"quantity"`
Leverage int `json:"leverage"`
UnrealizedPnL float64 `json:"unrealized_pnl"`
UnrealizedPnLPct float64 `json:"unrealized_pnl_pct"`
PeakPnLPct float64 `json:"peak_pnl_pct"` // Historical peak profit percentage
LiquidationPrice float64 `json:"liquidation_price"`
MarginUsed float64 `json:"margin_used"`
UpdateTime int64 `json:"update_time"` // Position update timestamp (milliseconds)
}
// AccountInfo account information
type AccountInfo struct {
TotalEquity float64 `json:"total_equity"` // Account equity
AvailableBalance float64 `json:"available_balance"` // Available balance
UnrealizedPnL float64 `json:"unrealized_pnl"` // Unrealized profit/loss
TotalPnL float64 `json:"total_pnl"` // Total profit/loss
TotalPnLPct float64 `json:"total_pnl_pct"` // Total profit/loss percentage
MarginUsed float64 `json:"margin_used"` // Used margin
MarginUsedPct float64 `json:"margin_used_pct"` // Margin usage rate
PositionCount int `json:"position_count"` // Number of positions
}
// CandidateCoin candidate coin (from coin pool)
type CandidateCoin struct {
Symbol string `json:"symbol"`
Sources []string `json:"sources"` // Sources: "ai500" and/or "oi_top"
}
// OITopData open interest growth top data (for AI decision reference)
type OITopData struct {
Rank int // OI Top ranking
OIDeltaPercent float64 // Open interest change percentage (1 hour)
OIDeltaValue float64 // Open interest change value
PriceDeltaPercent float64 // Price change percentage
}
// TradingStats trading statistics (for AI input)
type TradingStats struct {
TotalTrades int `json:"total_trades"` // Total number of trades (closed)
WinRate float64 `json:"win_rate"` // Win rate (%)
ProfitFactor float64 `json:"profit_factor"` // Profit factor
SharpeRatio float64 `json:"sharpe_ratio"` // Sharpe ratio
TotalPnL float64 `json:"total_pnl"` // Total profit/loss
AvgWin float64 `json:"avg_win"` // Average win
AvgLoss float64 `json:"avg_loss"` // Average loss
MaxDrawdownPct float64 `json:"max_drawdown_pct"` // Maximum drawdown (%)
}
// RecentOrder recently completed order (for AI input)
type RecentOrder struct {
Symbol string `json:"symbol"` // Trading pair
Side string `json:"side"` // long/short
EntryPrice float64 `json:"entry_price"` // Entry price
ExitPrice float64 `json:"exit_price"` // Exit price
RealizedPnL float64 `json:"realized_pnl"` // Realized profit/loss
PnLPct float64 `json:"pnl_pct"` // Profit/loss percentage
EntryTime string `json:"entry_time"` // Entry time
ExitTime string `json:"exit_time"` // Exit time
HoldDuration string `json:"hold_duration"` // Hold duration, e.g. "2h30m"
}
// Context trading context (complete information passed to AI)
type Context struct {
CurrentTime string `json:"current_time"`
RuntimeMinutes int `json:"runtime_minutes"`
CallCount int `json:"call_count"`
Account AccountInfo `json:"account"`
Positions []PositionInfo `json:"positions"`
CandidateCoins []CandidateCoin `json:"candidate_coins"`
PromptVariant string `json:"prompt_variant,omitempty"`
TradingStats *TradingStats `json:"trading_stats,omitempty"`
RecentOrders []RecentOrder `json:"recent_orders,omitempty"`
MarketDataMap map[string]*market.Data `json:"-"`
MultiTFMarket map[string]map[string]*market.Data `json:"-"`
OITopDataMap map[string]*OITopData `json:"-"`
QuantDataMap map[string]*QuantData `json:"-"`
OIRankingData *nofxos.OIRankingData `json:"-"` // Market-wide OI ranking data
NetFlowRankingData *nofxos.NetFlowRankingData `json:"-"` // Market-wide fund flow ranking data
PriceRankingData *nofxos.PriceRankingData `json:"-"` // Market-wide price gainers/losers
BTCETHLeverage int `json:"-"`
AltcoinLeverage int `json:"-"`
Timeframes []string `json:"-"`
}
// Decision AI trading decision
type Decision struct {
Symbol string `json:"symbol"`
Action string `json:"action"` // Standard: "open_long", "open_short", "close_long", "close_short", "hold", "wait"
// Grid actions: "place_buy_limit", "place_sell_limit", "cancel_order", "cancel_all_orders", "pause_grid", "resume_grid", "adjust_grid"
// Opening position parameters
Leverage int `json:"leverage,omitempty"`
PositionSizeUSD float64 `json:"position_size_usd,omitempty"`
StopLoss float64 `json:"stop_loss,omitempty"`
TakeProfit float64 `json:"take_profit,omitempty"`
// Grid trading parameters
Price float64 `json:"price,omitempty"` // Limit order price (for grid)
Quantity float64 `json:"quantity,omitempty"` // Order quantity (for grid)
LevelIndex int `json:"level_index,omitempty"` // Grid level index
OrderID string `json:"order_id,omitempty"` // Order ID (for cancel)
// Common parameters
Confidence int `json:"confidence,omitempty"` // Confidence level (0-100)
RiskUSD float64 `json:"risk_usd,omitempty"` // Maximum USD risk
Reasoning string `json:"reasoning"`
}
// FullDecision AI's complete decision (including chain of thought)
type FullDecision struct {
SystemPrompt string `json:"system_prompt"`
UserPrompt string `json:"user_prompt"`
CoTTrace string `json:"cot_trace"`
Decisions []Decision `json:"decisions"`
RawResponse string `json:"raw_response"`
Timestamp time.Time `json:"timestamp"`
AIRequestDurationMs int64 `json:"ai_request_duration_ms,omitempty"`
}
// QuantData quantitative data structure (fund flow, position changes, price changes)
type QuantData struct {
Symbol string `json:"symbol"`
Price float64 `json:"price"`
Netflow *NetflowData `json:"netflow,omitempty"`
OI map[string]*OIData `json:"oi,omitempty"`
PriceChange map[string]float64 `json:"price_change,omitempty"`
}
type NetflowData struct {
Institution *FlowTypeData `json:"institution,omitempty"`
Personal *FlowTypeData `json:"personal,omitempty"`
}
type FlowTypeData struct {
Future map[string]float64 `json:"future,omitempty"`
Spot map[string]float64 `json:"spot,omitempty"`
}
type OIData struct {
CurrentOI float64 `json:"current_oi"`
Delta map[string]*OIDeltaData `json:"delta,omitempty"`
}
type OIDeltaData struct {
OIDelta float64 `json:"oi_delta"`
OIDeltaValue float64 `json:"oi_delta_value"`
OIDeltaPercent float64 `json:"oi_delta_percent"`
}
// ============================================================================
// StrategyEngine - Core Strategy Execution Engine
// ============================================================================
// StrategyEngine strategy execution engine
type StrategyEngine struct {
config *store.StrategyConfig
nofxosClient *nofxos.Client
}
// NewStrategyEngine creates strategy execution engine.
// claw402WalletKey is optional — if provided, nofxos data requests are routed through claw402.
func NewStrategyEngine(config *store.StrategyConfig, claw402WalletKey ...string) *StrategyEngine {
// Create NofxOS client with API key from config
apiKey := config.Indicators.NofxOSAPIKey
if apiKey == "" {
apiKey = nofxos.DefaultAuthKey
}
client := nofxos.NewClient(nofxos.DefaultBaseURL, apiKey)
// If claw402 wallet key is provided (from trader's AI config), route through claw402
walletKey := ""
if len(claw402WalletKey) > 0 {
walletKey = claw402WalletKey[0]
}
if walletKey == "" {
walletKey = os.Getenv("CLAW402_WALLET_KEY")
}
if walletKey != "" {
claw402URL := os.Getenv("CLAW402_URL")
if claw402URL == "" {
claw402URL = "https://claw402.ai"
}
claw402Client, err := nofxos.NewClaw402DataClient(claw402URL, walletKey, nil)
if err == nil {
client.SetClaw402(claw402Client)
logger.Infof("🔗 NofxOS data routed through claw402 (%s)", claw402URL)
} else {
logger.Warnf("⚠️ Failed to init claw402 data client: %v (using direct nofxos.ai)", err)
}
}
return &StrategyEngine{
config: config,
nofxosClient: client,
}
}
// GetRiskControlConfig gets risk control configuration
func (e *StrategyEngine) GetRiskControlConfig() store.RiskControlConfig {
return e.config.RiskControl
}
// GetLanguage returns the language from config or falls back to auto-detection
func (e *StrategyEngine) GetLanguage() Language {
switch e.config.Language {
case "zh":
return LangChinese
case "en":
return LangEnglish
default:
// Fall back to auto-detection from prompt content for backward compatibility
return detectLanguage(e.config.PromptSections.RoleDefinition)
}
}
// GetConfig gets complete strategy configuration
func (e *StrategyEngine) GetConfig() *store.StrategyConfig {
return e.config
}
// ============================================================================
// Candidate Coins
// ============================================================================
// GetCandidateCoins gets candidate coins based on strategy configuration
func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
var candidates []CandidateCoin
symbolSources := make(map[string][]string)
coinSource := e.config.CoinSource
switch coinSource.SourceType {
case "static":
for _, symbol := range coinSource.StaticCoins {
symbol = market.Normalize(symbol)
candidates = append(candidates, CandidateCoin{
Symbol: symbol,
Sources: []string{"static"},
})
}
return e.filterExcludedCoins(candidates), nil
case "ai500":
// Check use_ai500 flag; if false, fall back to static coins
if !coinSource.UseAI500 {
logger.Infof("⚠️ source_type is 'ai500' but use_ai500 is false, falling back to static coins")
for _, symbol := range coinSource.StaticCoins {
symbol = market.Normalize(symbol)
candidates = append(candidates, CandidateCoin{
Symbol: symbol,
Sources: []string{"static"},
})
}
return e.filterExcludedCoins(candidates), nil
}
coins, err := e.getAI500Coins(coinSource.AI500Limit)
if err != nil {
return nil, err
}
// Empty list is a normal condition, return directly
return e.filterExcludedCoins(coins), nil
case "oi_top":
// Check use_oi_top flag; if false, fall back to static coins
if !coinSource.UseOITop {
logger.Infof("⚠️ source_type is 'oi_top' but use_oi_top is false, falling back to static coins")
for _, symbol := range coinSource.StaticCoins {
symbol = market.Normalize(symbol)
candidates = append(candidates, CandidateCoin{
Symbol: symbol,
Sources: []string{"static"},
})
}
return e.filterExcludedCoins(candidates), nil
}
coins, err := e.getOITopCoins(coinSource.OITopLimit)
if err != nil {
return nil, err
}
// Empty list is a normal condition, return directly
return e.filterExcludedCoins(coins), nil
case "oi_low":
// OI decrease ranking, suitable for short positions
if !coinSource.UseOILow {
logger.Infof("⚠️ source_type is 'oi_low' but use_oi_low is false, falling back to static coins")
for _, symbol := range coinSource.StaticCoins {
symbol = market.Normalize(symbol)
candidates = append(candidates, CandidateCoin{
Symbol: symbol,
Sources: []string{"static"},
})
}
return e.filterExcludedCoins(candidates), nil
}
coins, err := e.getOILowCoins(coinSource.OILowLimit)
if err != nil {
return nil, err
}
// Empty list is a normal condition, return directly
return e.filterExcludedCoins(coins), nil
case "hyper_all":
// All Hyperliquid perp coins
if !coinSource.UseHyperAll {
logger.Infof("⚠️ source_type is 'hyper_all' but use_hyper_all is false, falling back to static coins")
for _, symbol := range coinSource.StaticCoins {
symbol = market.Normalize(symbol)
candidates = append(candidates, CandidateCoin{
Symbol: symbol,
Sources: []string{"static"},
})
}
return e.filterExcludedCoins(candidates), nil
}
coins, err := e.getHyperAllCoins()
if err != nil {
return nil, err
}
return e.filterExcludedCoins(coins), nil
case "hyper_main":
// Top N Hyperliquid coins by 24h volume
if !coinSource.UseHyperMain {
logger.Infof("⚠️ source_type is 'hyper_main' but use_hyper_main is false, falling back to static coins")
for _, symbol := range coinSource.StaticCoins {
symbol = market.Normalize(symbol)
candidates = append(candidates, CandidateCoin{
Symbol: symbol,
Sources: []string{"static"},
})
}
return e.filterExcludedCoins(candidates), nil
}
coins, err := e.getHyperMainCoins(coinSource.HyperMainLimit)
if err != nil {
return nil, err
}
return e.filterExcludedCoins(coins), nil
case "mixed":
if coinSource.UseAI500 {
poolCoins, err := e.getAI500Coins(coinSource.AI500Limit)
if err != nil {
logger.Infof("⚠️ Failed to get AI500 coins: %v", err)
} else {
for _, coin := range poolCoins {
symbolSources[coin.Symbol] = append(symbolSources[coin.Symbol], "ai500")
}
}
}
if coinSource.UseOITop {
oiCoins, err := e.getOITopCoins(coinSource.OITopLimit)
if err != nil {
logger.Infof("⚠️ Failed to get OI Top: %v", err)
} else {
for _, coin := range oiCoins {
symbolSources[coin.Symbol] = append(symbolSources[coin.Symbol], "oi_top")
}
}
}
if coinSource.UseOILow {
oiLowCoins, err := e.getOILowCoins(coinSource.OILowLimit)
if err != nil {
logger.Infof("⚠️ Failed to get OI Low: %v", err)
} else {
for _, coin := range oiLowCoins {
symbolSources[coin.Symbol] = append(symbolSources[coin.Symbol], "oi_low")
}
}
}
if coinSource.UseHyperAll {
hyperCoins, err := e.getHyperAllCoins()
if err != nil {
logger.Infof("⚠️ Failed to get Hyperliquid All coins: %v", err)
} else {
for _, coin := range hyperCoins {
symbolSources[coin.Symbol] = append(symbolSources[coin.Symbol], "hyper_all")
}
}
}
if coinSource.UseHyperMain {
hyperMainCoins, err := e.getHyperMainCoins(coinSource.HyperMainLimit)
if err != nil {
logger.Infof("⚠️ Failed to get Hyperliquid Main coins: %v", err)
} else {
for _, coin := range hyperMainCoins {
symbolSources[coin.Symbol] = append(symbolSources[coin.Symbol], "hyper_main")
}
}
}
for _, symbol := range coinSource.StaticCoins {
symbol = market.Normalize(symbol)
if _, exists := symbolSources[symbol]; !exists {
symbolSources[symbol] = []string{"static"}
} else {
symbolSources[symbol] = append(symbolSources[symbol], "static")
}
}
for symbol, sources := range symbolSources {
candidates = append(candidates, CandidateCoin{
Symbol: symbol,
Sources: sources,
})
}
return e.filterExcludedCoins(candidates), nil
default:
return nil, fmt.Errorf("unknown coin source type: %s", coinSource.SourceType)
}
}
// filterExcludedCoins removes excluded coins from the candidates list
func (e *StrategyEngine) filterExcludedCoins(candidates []CandidateCoin) []CandidateCoin {
if len(e.config.CoinSource.ExcludedCoins) == 0 {
return candidates
}
// Build excluded set for O(1) lookup
excluded := make(map[string]bool)
for _, coin := range e.config.CoinSource.ExcludedCoins {
normalized := market.Normalize(coin)
excluded[normalized] = true
}
// Filter out excluded coins
filtered := make([]CandidateCoin, 0, len(candidates))
for _, c := range candidates {
if !excluded[c.Symbol] {
filtered = append(filtered, c)
} else {
logger.Infof("🚫 Excluded coin: %s", c.Symbol)
}
}
return filtered
}
func (e *StrategyEngine) getAI500Coins(limit int) ([]CandidateCoin, error) {
if limit <= 0 {
limit = 30
}
symbols, err := e.nofxosClient.GetTopRatedCoins(limit)
if err != nil {
return nil, err
}
var candidates []CandidateCoin
for _, symbol := range symbols {
candidates = append(candidates, CandidateCoin{
Symbol: symbol,
Sources: []string{"ai500"},
})
}
return candidates, nil
}
func (e *StrategyEngine) getOITopCoins(limit int) ([]CandidateCoin, error) {
if limit <= 0 {
limit = 10
}
positions, err := e.nofxosClient.GetOITopPositions()
if err != nil {
return nil, err
}
var candidates []CandidateCoin
for i, pos := range positions {
if i >= limit {
break
}
symbol := market.Normalize(pos.Symbol)
candidates = append(candidates, CandidateCoin{
Symbol: symbol,
Sources: []string{"oi_top"},
})
}
return candidates, nil
}
func (e *StrategyEngine) getOILowCoins(limit int) ([]CandidateCoin, error) {
if limit <= 0 {
limit = 10
}
positions, err := e.nofxosClient.GetOILowPositions()
if err != nil {
return nil, err
}
var candidates []CandidateCoin
for i, pos := range positions {
if i >= limit {
break
}
symbol := market.Normalize(pos.Symbol)
candidates = append(candidates, CandidateCoin{
Symbol: symbol,
Sources: []string{"oi_low"},
})
}
return candidates, nil
}
// getHyperAllCoins returns all available Hyperliquid perpetual coins
func (e *StrategyEngine) getHyperAllCoins() ([]CandidateCoin, error) {
ctx := context.Background()
symbols, err := hyperliquid.GetAllCoinSymbols(ctx)
if err != nil {
return nil, fmt.Errorf("failed to get Hyperliquid coins: %w", err)
}
var candidates []CandidateCoin
for _, symbol := range symbols {
// Add USDT suffix for compatibility
normalizedSymbol := market.Normalize(symbol + "USDT")
candidates = append(candidates, CandidateCoin{
Symbol: normalizedSymbol,
Sources: []string{"hyper_all"},
})
}
logger.Infof("✅ Loaded %d Hyperliquid coins (hyper_all)", len(candidates))
return candidates, nil
}
// getHyperMainCoins returns top N Hyperliquid coins by 24h volume
func (e *StrategyEngine) getHyperMainCoins(limit int) ([]CandidateCoin, error) {
if limit <= 0 {
limit = 20
}
ctx := context.Background()
symbols, err := hyperliquid.GetMainCoinSymbols(ctx, limit)
if err != nil {
return nil, fmt.Errorf("failed to get Hyperliquid main coins: %w", err)
}
var candidates []CandidateCoin
for _, symbol := range symbols {
// Add USDT suffix for compatibility
normalizedSymbol := market.Normalize(symbol + "USDT")
candidates = append(candidates, CandidateCoin{
Symbol: normalizedSymbol,
Sources: []string{"hyper_main"},
})
}
logger.Infof("✅ Loaded %d Hyperliquid main coins (hyper_main) by 24h volume", len(candidates))
return candidates, nil
}
// ============================================================================
// External & Quant Data
// ============================================================================
// FetchMarketData fetches market data based on strategy configuration
func (e *StrategyEngine) FetchMarketData(symbol string) (*market.Data, error) {
return market.Get(symbol)
}
// FetchExternalData fetches external data sources
func (e *StrategyEngine) FetchExternalData() (map[string]interface{}, error) {
externalData := make(map[string]interface{})
for _, source := range e.config.Indicators.ExternalDataSources {
data, err := e.fetchSingleExternalSource(source)
if err != nil {
logger.Infof("⚠️ Failed to fetch external data source [%s]: %v", source.Name, err)
continue
}
externalData[source.Name] = data
}
return externalData, nil
}
func (e *StrategyEngine) fetchSingleExternalSource(source store.ExternalDataSource) (interface{}, error) {
// SSRF Protection: Validate URL before making request
if err := security.ValidateURL(source.URL); err != nil {
return nil, fmt.Errorf("external source URL validation failed: %w", err)
}
timeout := time.Duration(source.RefreshSecs) * time.Second
if timeout == 0 {
timeout = 30 * time.Second
}
// Use SSRF-safe HTTP client
client := security.SafeHTTPClient(timeout)
req, err := http.NewRequest(source.Method, source.URL, nil)
if err != nil {
return nil, err
}
for k, v := range source.Headers {
req.Header.Set(k, v)
}
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, err
}
var result interface{}
if err := json.Unmarshal(body, &result); err != nil {
return nil, err
}
if source.DataPath != "" {
result = extractJSONPath(result, source.DataPath)
}
return result, nil
}
func extractJSONPath(data interface{}, path string) interface{} {
parts := strings.Split(path, ".")
current := data
for _, part := range parts {
if m, ok := current.(map[string]interface{}); ok {
current = m[part]
} else {
return nil
}
}
return current
}
// FetchQuantData fetches quantitative data for a single coin
func (e *StrategyEngine) FetchQuantData(symbol string) (*QuantData, error) {
if !e.config.Indicators.EnableQuantData {
return nil, nil
}
// Use nofxos client with unified API key
include := "oi,price"
if e.config.Indicators.EnableQuantNetflow {
include = "netflow,oi,price"
}
nofxosData, err := e.nofxosClient.GetCoinData(symbol, include)
if err != nil {
return nil, fmt.Errorf("failed to fetch quant data: %w", err)
}
if nofxosData == nil {
return nil, nil
}
// Convert nofxos.QuantData to kernel.QuantData
quantData := &QuantData{
Symbol: nofxosData.Symbol,
Price: nofxosData.Price,
PriceChange: nofxosData.PriceChange,
}
// Convert OI data
if nofxosData.OI != nil {
quantData.OI = make(map[string]*OIData)
for exchange, oiData := range nofxosData.OI {
if oiData != nil {
kData := &OIData{
CurrentOI: oiData.CurrentOI,
}
if oiData.Delta != nil {
kData.Delta = make(map[string]*OIDeltaData)
for dur, delta := range oiData.Delta {
if delta != nil {
kData.Delta[dur] = &OIDeltaData{
OIDelta: delta.OIDelta,
OIDeltaValue: delta.OIDeltaValue,
OIDeltaPercent: delta.OIDeltaPercent,
}
}
}
}
quantData.OI[exchange] = kData
}
}
}
// Convert Netflow data
if nofxosData.Netflow != nil {
quantData.Netflow = &NetflowData{}
if nofxosData.Netflow.Institution != nil {
quantData.Netflow.Institution = &FlowTypeData{
Future: nofxosData.Netflow.Institution.Future,
Spot: nofxosData.Netflow.Institution.Spot,
}
}
if nofxosData.Netflow.Personal != nil {
quantData.Netflow.Personal = &FlowTypeData{
Future: nofxosData.Netflow.Personal.Future,
Spot: nofxosData.Netflow.Personal.Spot,
}
}
}
return quantData, nil
}
// FetchQuantDataBatch batch fetches quantitative data
func (e *StrategyEngine) FetchQuantDataBatch(symbols []string) map[string]*QuantData {
result := make(map[string]*QuantData)
if !e.config.Indicators.EnableQuantData {
return result
}
for _, symbol := range symbols {
data, err := e.FetchQuantData(symbol)
if err != nil {
logger.Infof("⚠️ Failed to fetch quantitative data for %s: %v", symbol, err)
continue
}
if data != nil {
result[symbol] = data
}
}
return result
}
// FetchOIRankingData fetches market-wide OI ranking data
func (e *StrategyEngine) FetchOIRankingData() *nofxos.OIRankingData {
indicators := e.config.Indicators
if !indicators.EnableOIRanking {
return nil
}
duration := indicators.OIRankingDuration
if duration == "" {
duration = "1h"
}
limit := indicators.OIRankingLimit
if limit <= 0 {
limit = 10
}
logger.Infof("📊 Fetching OI ranking data (duration: %s, limit: %d)", duration, limit)
data, err := e.nofxosClient.GetOIRanking(duration, limit)
if err != nil {
logger.Warnf("⚠️ Failed to fetch OI ranking data: %v", err)
return nil
}
logger.Infof("✓ OI ranking data ready: %d top, %d low positions",
len(data.TopPositions), len(data.LowPositions))
return data
}
// FetchNetFlowRankingData fetches market-wide NetFlow ranking data
func (e *StrategyEngine) FetchNetFlowRankingData() *nofxos.NetFlowRankingData {
indicators := e.config.Indicators
if !indicators.EnableNetFlowRanking {
return nil
}
duration := indicators.NetFlowRankingDuration
if duration == "" {
duration = "1h"
}
limit := indicators.NetFlowRankingLimit
if limit <= 0 {
limit = 10
}
logger.Infof("💰 Fetching NetFlow ranking data (duration: %s, limit: %d)", duration, limit)
data, err := e.nofxosClient.GetNetFlowRanking(duration, limit)
if err != nil {
logger.Warnf("⚠️ Failed to fetch NetFlow ranking data: %v", err)
return nil
}
logger.Infof("✓ NetFlow ranking data ready: inst_in=%d, inst_out=%d, retail_in=%d, retail_out=%d",
len(data.InstitutionFutureTop), len(data.InstitutionFutureLow),
len(data.PersonalFutureTop), len(data.PersonalFutureLow))
return data
}
// FetchPriceRankingData fetches market-wide price ranking data (gainers/losers)
func (e *StrategyEngine) FetchPriceRankingData() *nofxos.PriceRankingData {
indicators := e.config.Indicators
if !indicators.EnablePriceRanking {
return nil
}
durations := indicators.PriceRankingDuration
if durations == "" {
durations = "1h"
}
limit := indicators.PriceRankingLimit
if limit <= 0 {
limit = 10
}
logger.Infof("📈 Fetching Price ranking data (durations: %s, limit: %d)", durations, limit)
data, err := e.nofxosClient.GetPriceRanking(durations, limit)
if err != nil {
logger.Warnf("⚠️ Failed to fetch Price ranking data: %v", err)
return nil
}
logger.Infof("✓ Price ranking data ready for %d durations", len(data.Durations))
return data
}
// ============================================================================
// Helper Functions
// ============================================================================
// detectLanguage detects language from text content
// Returns LangChinese if text contains Chinese characters, otherwise LangEnglish
func detectLanguage(text string) Language {
for _, r := range text {
if r >= 0x4E00 && r <= 0x9FFF {
return LangChinese
}
}
return LangEnglish
}