mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 01:48:22 +08:00
feat: unify NofxOS data provider and fix language consistency
- Add unified NofxOS API key configuration in IndicatorEditor - Add language field to StrategyConfig for consistent prompt generation - Auto-update prompt sections when interface language changes - Remove scattered URL inputs from CoinSourceEditor and IndicatorEditor - Create nofxos provider package with formatted data output - Update kernel engine to use config-based language setting
This commit is contained in:
+15
-23
@@ -15,7 +15,7 @@ import (
|
||||
"nofx/backtest"
|
||||
"nofx/logger"
|
||||
"nofx/market"
|
||||
"nofx/provider"
|
||||
"nofx/provider/nofxos"
|
||||
"nofx/store"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -92,9 +92,9 @@ func (s *Server) handleBacktestStart(c *gin.Context) {
|
||||
}
|
||||
cfg.SetLoadedStrategy(&strategyConfig)
|
||||
logger.Infof("📊 Backtest using saved strategy: %s (%s)", strategy.Name, strategy.ID)
|
||||
logger.Infof("📊 Strategy coin source: type=%s, use_coin_pool=%v, use_oi_top=%v, static_coins=%v",
|
||||
logger.Infof("📊 Strategy coin source: type=%s, use_ai500=%v, use_oi_top=%v, static_coins=%v",
|
||||
strategyConfig.CoinSource.SourceType,
|
||||
strategyConfig.CoinSource.UseCoinPool,
|
||||
strategyConfig.CoinSource.UseAI500,
|
||||
strategyConfig.CoinSource.UseOITop,
|
||||
strategyConfig.CoinSource.StaticCoins)
|
||||
|
||||
@@ -638,21 +638,13 @@ func (s *Server) resolveStrategyCoins(strategyConfig *store.StrategyConfig) ([]s
|
||||
var symbols []string
|
||||
symbolSet := make(map[string]bool)
|
||||
|
||||
// Set custom API URLs if provided
|
||||
if coinSource.CoinPoolAPIURL != "" {
|
||||
provider.SetCoinPoolAPI(coinSource.CoinPoolAPIURL)
|
||||
}
|
||||
if coinSource.OITopAPIURL != "" {
|
||||
provider.SetOITopAPI(coinSource.OITopAPIURL)
|
||||
}
|
||||
|
||||
// Handle empty source_type - check flags for backward compatibility
|
||||
sourceType := coinSource.SourceType
|
||||
if sourceType == "" {
|
||||
if coinSource.UseCoinPool && coinSource.UseOITop {
|
||||
if coinSource.UseAI500 && coinSource.UseOITop {
|
||||
sourceType = "mixed"
|
||||
} else if coinSource.UseCoinPool {
|
||||
sourceType = "coinpool"
|
||||
} else if coinSource.UseAI500 {
|
||||
sourceType = "ai500"
|
||||
} else if coinSource.UseOITop {
|
||||
sourceType = "oi_top"
|
||||
} else if len(coinSource.StaticCoins) > 0 {
|
||||
@@ -673,13 +665,13 @@ func (s *Server) resolveStrategyCoins(strategyConfig *store.StrategyConfig) ([]s
|
||||
}
|
||||
}
|
||||
|
||||
case "coinpool":
|
||||
limit := coinSource.CoinPoolLimit
|
||||
case "ai500":
|
||||
limit := coinSource.AI500Limit
|
||||
if limit <= 0 {
|
||||
limit = 30
|
||||
}
|
||||
logger.Infof("📊 Fetching AI500 coins with limit=%d", limit)
|
||||
coins, err := provider.GetTopRatedCoins(limit)
|
||||
coins, err := nofxos.DefaultClient().GetTopRatedCoins(limit)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get AI500 coins: %w", err)
|
||||
}
|
||||
@@ -693,7 +685,7 @@ func (s *Server) resolveStrategyCoins(strategyConfig *store.StrategyConfig) ([]s
|
||||
}
|
||||
|
||||
case "oi_top":
|
||||
coins, err := provider.GetOITopSymbols()
|
||||
coins, err := nofxos.DefaultClient().GetOITopSymbols()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get OI Top coins: %w", err)
|
||||
}
|
||||
@@ -713,13 +705,13 @@ func (s *Server) resolveStrategyCoins(strategyConfig *store.StrategyConfig) ([]s
|
||||
}
|
||||
|
||||
case "mixed":
|
||||
// Get from coin pool
|
||||
if coinSource.UseCoinPool {
|
||||
limit := coinSource.CoinPoolLimit
|
||||
// Get from AI500
|
||||
if coinSource.UseAI500 {
|
||||
limit := coinSource.AI500Limit
|
||||
if limit <= 0 {
|
||||
limit = 30
|
||||
}
|
||||
coins, err := provider.GetTopRatedCoins(limit)
|
||||
coins, err := nofxos.DefaultClient().GetTopRatedCoins(limit)
|
||||
if err != nil {
|
||||
logger.Warnf("Failed to get AI500 coins: %v", err)
|
||||
} else {
|
||||
@@ -735,7 +727,7 @@ func (s *Server) resolveStrategyCoins(strategyConfig *store.StrategyConfig) ([]s
|
||||
|
||||
// Get from OI Top
|
||||
if coinSource.UseOITop {
|
||||
coins, err := provider.GetOITopSymbols()
|
||||
coins, err := nofxos.DefaultClient().GetOITopSymbols()
|
||||
if err != nil {
|
||||
logger.Warnf("Failed to get OI Top coins: %v", err)
|
||||
} else {
|
||||
|
||||
+12
-20
@@ -8,7 +8,7 @@ import (
|
||||
|
||||
"nofx/debate"
|
||||
"nofx/logger"
|
||||
"nofx/provider"
|
||||
"nofx/provider/nofxos"
|
||||
"nofx/store"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -158,35 +158,27 @@ func (h *DebateHandler) HandleCreateDebate(c *gin.Context) {
|
||||
if len(coinSource.StaticCoins) > 0 {
|
||||
req.Symbol = coinSource.StaticCoins[0]
|
||||
}
|
||||
case "coinpool":
|
||||
// Fetch from coin pool API
|
||||
if coinSource.CoinPoolAPIURL != "" {
|
||||
provider.SetCoinPoolAPI(coinSource.CoinPoolAPIURL)
|
||||
}
|
||||
if coins, err := provider.GetTopRatedCoins(1); err == nil && len(coins) > 0 {
|
||||
case "ai500":
|
||||
// Fetch from AI500 API
|
||||
if coins, err := nofxos.DefaultClient().GetTopRatedCoins(1); err == nil && len(coins) > 0 {
|
||||
req.Symbol = coins[0]
|
||||
logger.Infof("Fetched coin from pool API: %s", req.Symbol)
|
||||
logger.Infof("Fetched coin from AI500 API: %s", req.Symbol)
|
||||
}
|
||||
case "oi_top":
|
||||
// Fetch from OI top API
|
||||
if coinSource.OITopAPIURL != "" {
|
||||
provider.SetOITopAPI(coinSource.OITopAPIURL)
|
||||
}
|
||||
if coins, err := provider.GetOITopSymbols(); err == nil && len(coins) > 0 {
|
||||
if coins, err := nofxos.DefaultClient().GetOITopSymbols(); err == nil && len(coins) > 0 {
|
||||
req.Symbol = coins[0]
|
||||
logger.Infof("Fetched coin from OI Top API: %s", req.Symbol)
|
||||
}
|
||||
case "mixed":
|
||||
// Try coin pool first, then OI top
|
||||
if coinSource.UseCoinPool && coinSource.CoinPoolAPIURL != "" {
|
||||
provider.SetCoinPoolAPI(coinSource.CoinPoolAPIURL)
|
||||
if coins, err := provider.GetTopRatedCoins(1); err == nil && len(coins) > 0 {
|
||||
// Try AI500 first, then OI top
|
||||
if coinSource.UseAI500 {
|
||||
if coins, err := nofxos.DefaultClient().GetTopRatedCoins(1); err == nil && len(coins) > 0 {
|
||||
req.Symbol = coins[0]
|
||||
logger.Infof("Fetched coin from pool API (mixed): %s", req.Symbol)
|
||||
logger.Infof("Fetched coin from AI500 API (mixed): %s", req.Symbol)
|
||||
}
|
||||
} else if coinSource.UseOITop && coinSource.OITopAPIURL != "" {
|
||||
provider.SetOITopAPI(coinSource.OITopAPIURL)
|
||||
if coins, err := provider.GetOITopSymbols(); err == nil && len(coins) > 0 {
|
||||
} else if coinSource.UseOITop {
|
||||
if coins, err := nofxos.DefaultClient().GetOITopSymbols(); err == nil && len(coins) > 0 {
|
||||
req.Symbol = coins[0]
|
||||
logger.Infof("Fetched coin from OI Top API (mixed): %s", req.Symbol)
|
||||
}
|
||||
|
||||
+3
-3
@@ -406,7 +406,7 @@ type CreateTraderRequest struct {
|
||||
CustomPrompt string `json:"custom_prompt"`
|
||||
OverrideBasePrompt bool `json:"override_base_prompt"`
|
||||
SystemPromptTemplate string `json:"system_prompt_template"` // System prompt template name
|
||||
UseCoinPool bool `json:"use_coin_pool"`
|
||||
UseAI500 bool `json:"use_ai500"`
|
||||
UseOITop bool `json:"use_oi_top"`
|
||||
}
|
||||
|
||||
@@ -666,7 +666,7 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
|
||||
BTCETHLeverage: btcEthLeverage,
|
||||
AltcoinLeverage: altcoinLeverage,
|
||||
TradingSymbols: req.TradingSymbols,
|
||||
UseCoinPool: req.UseCoinPool,
|
||||
UseAI500: req.UseAI500,
|
||||
UseOITop: req.UseOITop,
|
||||
CustomPrompt: req.CustomPrompt,
|
||||
OverrideBasePrompt: req.OverrideBasePrompt,
|
||||
@@ -2049,7 +2049,7 @@ func (s *Server) handleGetTraderConfig(c *gin.Context) {
|
||||
"custom_prompt": traderConfig.CustomPrompt,
|
||||
"override_base_prompt": traderConfig.OverrideBasePrompt,
|
||||
"is_cross_margin": traderConfig.IsCrossMargin,
|
||||
"use_coin_pool": traderConfig.UseCoinPool,
|
||||
"use_ai500": traderConfig.UseAI500,
|
||||
"use_oi_top": traderConfig.UseOITop,
|
||||
"is_running": isRunning,
|
||||
}
|
||||
|
||||
+19
-12
@@ -9,7 +9,6 @@ import (
|
||||
"nofx/market"
|
||||
"nofx/mcp"
|
||||
"nofx/store"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -20,11 +19,11 @@ import (
|
||||
func validateStrategyConfig(config *store.StrategyConfig) []string {
|
||||
var warnings []string
|
||||
|
||||
// Validate quant data URL if enabled
|
||||
if config.Indicators.EnableQuantData && config.Indicators.QuantDataAPIURL != "" {
|
||||
if !strings.Contains(config.Indicators.QuantDataAPIURL, "{symbol}") {
|
||||
warnings = append(warnings, "Quant data URL does not contain {symbol} placeholder. The same data will be used for all coins, which may not be correct.")
|
||||
}
|
||||
// Validate NofxOS API key if any NofxOS feature is enabled
|
||||
if (config.Indicators.EnableQuantData || config.Indicators.EnableOIRanking ||
|
||||
config.Indicators.EnableNetFlowRanking || config.Indicators.EnablePriceRanking) &&
|
||||
config.Indicators.NofxOSAPIKey == "" {
|
||||
warnings = append(warnings, "NofxOS API key is not configured. NofxOS data sources may not work properly.")
|
||||
}
|
||||
|
||||
return warnings
|
||||
@@ -504,6 +503,12 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) {
|
||||
// Fetch OI ranking data (market-wide position changes)
|
||||
oiRankingData := engine.FetchOIRankingData()
|
||||
|
||||
// Fetch NetFlow ranking data (market-wide fund flow)
|
||||
netFlowRankingData := engine.FetchNetFlowRankingData()
|
||||
|
||||
// Fetch Price ranking data (market-wide gainers/losers)
|
||||
priceRankingData := engine.FetchPriceRankingData()
|
||||
|
||||
// Build real context (for generating User Prompt)
|
||||
testContext := &kernel.Context{
|
||||
CurrentTime: time.Now().UTC().Format("2006-01-02 15:04:05 UTC"),
|
||||
@@ -519,12 +524,14 @@ func (s *Server) handleStrategyTestRun(c *gin.Context) {
|
||||
MarginUsedPct: 0,
|
||||
PositionCount: 0,
|
||||
},
|
||||
Positions: []kernel.PositionInfo{},
|
||||
CandidateCoins: candidates,
|
||||
PromptVariant: req.PromptVariant,
|
||||
MarketDataMap: marketDataMap,
|
||||
QuantDataMap: quantDataMap,
|
||||
OIRankingData: oiRankingData,
|
||||
Positions: []kernel.PositionInfo{},
|
||||
CandidateCoins: candidates,
|
||||
PromptVariant: req.PromptVariant,
|
||||
MarketDataMap: marketDataMap,
|
||||
QuantDataMap: quantDataMap,
|
||||
OIRankingData: oiRankingData,
|
||||
NetFlowRankingData: netFlowRankingData,
|
||||
PriceRankingData: priceRankingData,
|
||||
}
|
||||
|
||||
// Build System Prompt
|
||||
|
||||
+7
-7
@@ -199,7 +199,7 @@ func (cfg *BacktestConfig) ToStrategyConfig() *store.StrategyConfig {
|
||||
if len(cfg.Symbols) > 0 {
|
||||
result.CoinSource.SourceType = "static"
|
||||
result.CoinSource.StaticCoins = cfg.Symbols
|
||||
result.CoinSource.UseCoinPool = false
|
||||
result.CoinSource.UseAI500 = false
|
||||
result.CoinSource.UseOITop = false
|
||||
}
|
||||
|
||||
@@ -241,12 +241,12 @@ func (cfg *BacktestConfig) ToStrategyConfig() *store.StrategyConfig {
|
||||
|
||||
return &store.StrategyConfig{
|
||||
CoinSource: store.CoinSourceConfig{
|
||||
SourceType: "static",
|
||||
StaticCoins: cfg.Symbols,
|
||||
UseCoinPool: false,
|
||||
CoinPoolLimit: len(cfg.Symbols),
|
||||
UseOITop: false,
|
||||
OITopLimit: 0,
|
||||
SourceType: "static",
|
||||
StaticCoins: cfg.Symbols,
|
||||
UseAI500: false,
|
||||
AI500Limit: len(cfg.Symbols),
|
||||
UseOITop: false,
|
||||
OITopLimit: 0,
|
||||
},
|
||||
Indicators: store.IndicatorConfig{
|
||||
Klines: store.KlineConfig{
|
||||
|
||||
+19
-1
@@ -519,7 +519,7 @@ func (r *Runner) buildDecisionContext(ts int64, marketData map[string]*market.Da
|
||||
|
||||
// Fetch quantitative data if enabled in strategy (uses current data as approximation)
|
||||
strategyConfig := r.strategyEngine.GetConfig()
|
||||
if strategyConfig.Indicators.EnableQuantData && strategyConfig.Indicators.QuantDataAPIURL != "" {
|
||||
if strategyConfig.Indicators.EnableQuantData {
|
||||
// Collect symbols to query (candidate coins + position coins)
|
||||
symbolSet := make(map[string]bool)
|
||||
for _, sym := range r.cfg.Symbols {
|
||||
@@ -547,6 +547,24 @@ func (r *Runner) buildDecisionContext(ts int64, marketData map[string]*market.Da
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch NetFlow ranking data if enabled in strategy
|
||||
if strategyConfig.Indicators.EnableNetFlowRanking {
|
||||
ctx.NetFlowRankingData = r.strategyEngine.FetchNetFlowRankingData()
|
||||
if ctx.NetFlowRankingData != nil {
|
||||
logger.Infof("💰 Backtest: NetFlow ranking data ready: inst_in=%d, inst_out=%d",
|
||||
len(ctx.NetFlowRankingData.InstitutionFutureTop), len(ctx.NetFlowRankingData.InstitutionFutureLow))
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch Price ranking data if enabled in strategy
|
||||
if strategyConfig.Indicators.EnablePriceRanking {
|
||||
ctx.PriceRankingData = r.strategyEngine.FetchPriceRankingData()
|
||||
if ctx.PriceRankingData != nil {
|
||||
logger.Infof("📈 Backtest: Price ranking data ready for %d durations",
|
||||
len(ctx.PriceRankingData.Durations))
|
||||
}
|
||||
}
|
||||
|
||||
record := &store.DecisionRecord{
|
||||
AccountState: store.AccountSnapshot{
|
||||
TotalBalance: accountInfo.TotalEquity,
|
||||
|
||||
+14
-6
@@ -335,6 +335,12 @@ func (e *DebateEngine) buildMarketContext(session *store.DebateSessionWithDetail
|
||||
// Fetch OI ranking data (market-wide position changes)
|
||||
oiRankingData := strategyEngine.FetchOIRankingData()
|
||||
|
||||
// Fetch NetFlow ranking data (market-wide fund flow)
|
||||
netFlowRankingData := strategyEngine.FetchNetFlowRankingData()
|
||||
|
||||
// Fetch Price ranking data (market-wide gainers/losers)
|
||||
priceRankingData := strategyEngine.FetchPriceRankingData()
|
||||
|
||||
// Build context
|
||||
ctx := &kernel.Context{
|
||||
CurrentTime: time.Now().UTC().Format("2006-01-02 15:04:05 UTC"),
|
||||
@@ -350,12 +356,14 @@ func (e *DebateEngine) buildMarketContext(session *store.DebateSessionWithDetail
|
||||
MarginUsedPct: 0,
|
||||
PositionCount: 0,
|
||||
},
|
||||
Positions: []kernel.PositionInfo{},
|
||||
CandidateCoins: candidates,
|
||||
PromptVariant: session.PromptVariant,
|
||||
MarketDataMap: marketDataMap,
|
||||
QuantDataMap: quantDataMap,
|
||||
OIRankingData: oiRankingData,
|
||||
Positions: []kernel.PositionInfo{},
|
||||
CandidateCoins: candidates,
|
||||
PromptVariant: session.PromptVariant,
|
||||
MarketDataMap: marketDataMap,
|
||||
QuantDataMap: quantDataMap,
|
||||
OIRankingData: oiRankingData,
|
||||
NetFlowRankingData: netFlowRankingData,
|
||||
PriceRankingData: priceRankingData,
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
|
||||
@@ -0,0 +1,852 @@
|
||||
# CryptoMaster API 接口文档
|
||||
|
||||
## 概述
|
||||
|
||||
### 基础信息
|
||||
- **Base URL**: `https://nofxos.ai`
|
||||
- **响应格式**: JSON
|
||||
- **缓存时间**: 15秒(所有数据接口)
|
||||
- **限流**: 每个IP每秒最多30次请求
|
||||
|
||||
### 认证方式
|
||||
所有数据接口需要认证,支持两种方式:
|
||||
|
||||
#### 方式1: Query参数(推荐)
|
||||
```
|
||||
GET /api/ai500/list?auth=your_api_key
|
||||
```
|
||||
|
||||
#### 方式2: Authorization Header
|
||||
```
|
||||
GET /api/ai500/list
|
||||
Authorization: Bearer your_api_key
|
||||
```
|
||||
|
||||
### 响应格式
|
||||
|
||||
**成功响应:**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": { ... }
|
||||
}
|
||||
```
|
||||
|
||||
**错误响应:**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "错误信息"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 重要:数值格式说明
|
||||
|
||||
### 百分比字段格式
|
||||
|
||||
不同接口的百分比字段使用不同的格式,请注意区分:
|
||||
|
||||
| 字段名 | 格式 | 示例 | 说明 |
|
||||
|--------|------|------|------|
|
||||
| `price_delta` (涨跌幅榜/币种详情) | **小数** | `0.05` = 5% | 需要 ×100 转换为百分比 |
|
||||
| `oi_delta_percent` | **已×100** | `5.0` = 5% | 直接使用,无需转换 |
|
||||
| `price_delta_percent` (OI接口) | **已×100** | `5.0` = 5% | 直接使用,无需转换 |
|
||||
| `increase_percent` (AI500) | **已×100** | `7.14` = 7.14% | 直接使用,无需转换 |
|
||||
|
||||
### 金额字段
|
||||
|
||||
| 字段名 | 单位 | 说明 |
|
||||
|--------|------|------|
|
||||
| `oi_delta_value` | USDT | 持仓价值变化 |
|
||||
| `amount` / `future_flow` / `spot_flow` | USDT | 资金流量 |
|
||||
| `price` | USDT | 当前价格 |
|
||||
|
||||
### 持仓量字段
|
||||
|
||||
| 字段名 | 单位 | 说明 |
|
||||
|--------|------|------|
|
||||
| `oi_delta` | 张/个 | 持仓量变化 |
|
||||
| `current_oi` / `oi` | 张/个 | 当前持仓量 |
|
||||
| `net_long` / `net_short` | 张/个 | 净多头/空头持仓 |
|
||||
|
||||
---
|
||||
|
||||
## 时间范围参数说明
|
||||
|
||||
所有接口支持的 `duration` 参数值:
|
||||
|
||||
| 参数值 | 说明 | 备注 |
|
||||
|--------|------|------|
|
||||
| `1m` | 1分钟 | |
|
||||
| `5m` | 5分钟 | |
|
||||
| `15m` | 15分钟 | |
|
||||
| `30m` | 30分钟 | |
|
||||
| `1h` | 1小时 | 默认值 |
|
||||
| `4h` | 4小时 | |
|
||||
| `8h` | 8小时 | |
|
||||
| `12h` | 12小时 | |
|
||||
| `24h` / `1d` | 24小时 | 两种写法均可 |
|
||||
| `2d` | 2天 | |
|
||||
| `3d` | 3天 | |
|
||||
| `5d` | 5天 | |
|
||||
| `7d` | 7天 | |
|
||||
|
||||
---
|
||||
|
||||
## 1. AI500 智能评分接口
|
||||
|
||||
AI500 是基于多维度量化指标的智能评分系统,用于筛选具有上涨潜力的币种。
|
||||
|
||||
### 1.1 获取AI500推荐币种列表
|
||||
|
||||
获取经过严格筛选的优质币种列表。
|
||||
|
||||
**请求**
|
||||
```
|
||||
GET /api/ai500/list
|
||||
```
|
||||
|
||||
**过滤条件**
|
||||
- AI评分 > 70
|
||||
- 币安OI持仓价值 > 15M USDT
|
||||
- 现价 > 上榜起始价格(只返回上涨中的币种)
|
||||
- 资金没有持续流出(1h/4h/12h/24h不能全为负)
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"count": 5,
|
||||
"coins": [
|
||||
{
|
||||
"pair": "BTCUSDT",
|
||||
"score": 85.234,
|
||||
"start_time": 1704067200,
|
||||
"start_price": 42000.5,
|
||||
"last_score": 83.5,
|
||||
"max_score": 87.2,
|
||||
"max_price": 45000.0,
|
||||
"increase_percent": 7.14
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**
|
||||
| 字段 | 类型 | 说明 |
|
||||
|------|------|------|
|
||||
| `pair` | string | 交易对名称,如 BTCUSDT |
|
||||
| `score` | float | 当前AI评分(0-100) |
|
||||
| `start_time` | int64 | 上榜时间戳(Unix秒) |
|
||||
| `start_price` | float | 上榜时价格(USDT) |
|
||||
| `last_score` | float | 上次记录的评分 |
|
||||
| `max_score` | float | 在榜期间最高评分 |
|
||||
| `max_price` | float | 在榜期间最高价格(USDT) |
|
||||
| `increase_percent` | float | 最大涨幅百分比(**已×100**,7.14 = 7.14%) |
|
||||
|
||||
---
|
||||
|
||||
### 1.2 获取单个币种AI500信息
|
||||
|
||||
**请求**
|
||||
```
|
||||
GET /api/ai500/:symbol
|
||||
```
|
||||
|
||||
**路径参数**
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `symbol` | string | 是 | 币种符号,支持 `BTCUSDT` 或 `BTC` 格式 |
|
||||
|
||||
**示例**
|
||||
```
|
||||
GET /api/ai500/BTC
|
||||
GET /api/ai500/ETHUSDT
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"info": {
|
||||
"pair": "BTCUSDT",
|
||||
"score": 85.234,
|
||||
"start_time": 1704067200,
|
||||
"start_price": 42000.5,
|
||||
"last_score": 83.5,
|
||||
"max_score": 87.2,
|
||||
"max_price": 45000.0,
|
||||
"increase_percent": 7.14
|
||||
},
|
||||
"current_price": 44500.0,
|
||||
"score": 85.234
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 1.3 获取AI500统计信息
|
||||
|
||||
获取AI500整体统计数据。
|
||||
|
||||
**请求**
|
||||
```
|
||||
GET /api/ai500/stats
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"statistics": {
|
||||
"total_count": 50,
|
||||
"average_score": 72.5,
|
||||
"max_score": 95.2,
|
||||
"min_score": 55.3,
|
||||
"average_increase": 12.5
|
||||
},
|
||||
"top_coins": [...],
|
||||
"bottom_coins": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. 持仓量(OI)排行接口
|
||||
|
||||
监控各币种的合约持仓量变化,用于判断市场资金动向。
|
||||
|
||||
### 2.1 获取OI增加排行榜
|
||||
|
||||
返回持仓价值增加最多的币种排行。
|
||||
|
||||
**请求**
|
||||
```
|
||||
GET /api/oi/top-ranking
|
||||
```
|
||||
|
||||
**查询参数**
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `limit` | int | 20 | 返回数量,最大100 |
|
||||
| `duration` | string | `1h` | 时间范围,见[时间范围参数](#时间范围参数说明) |
|
||||
|
||||
**示例**
|
||||
```
|
||||
GET /api/oi/top-ranking?limit=50&duration=4h
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"count": 20,
|
||||
"exchange": "binance",
|
||||
"time_range": "4小时",
|
||||
"time_range_param": "4h",
|
||||
"rank_type": "top",
|
||||
"limit": 50,
|
||||
"positions": [
|
||||
{
|
||||
"rank": 1,
|
||||
"symbol": "BTCUSDT",
|
||||
"price": 44500.0,
|
||||
"oi_delta": 1500.5,
|
||||
"oi_delta_value": 65000000,
|
||||
"oi_delta_percent": 2.5,
|
||||
"current_oi": 62000,
|
||||
"price_delta_percent": 1.2,
|
||||
"net_long": 35000,
|
||||
"net_short": 27000
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**
|
||||
| 字段 | 类型 | 格式 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `rank` | int | - | 排名 |
|
||||
| `symbol` | string | - | 交易对名称 |
|
||||
| `price` | float | USDT | 当前价格 |
|
||||
| `oi_delta` | float | 张/个 | 持仓量变化 |
|
||||
| `oi_delta_value` | float | USDT | 持仓价值变化(**排序依据**) |
|
||||
| `oi_delta_percent` | float | **已×100** | 持仓量变化百分比,2.5 = 2.5% |
|
||||
| `current_oi` | float | 张/个 | 当前持仓量 |
|
||||
| `price_delta_percent` | float | **已×100** | 价格变化百分比,1.2 = 1.2% |
|
||||
| `net_long` | float | 张/个 | 净多头持仓 |
|
||||
| `net_short` | float | 张/个 | 净空头持仓 |
|
||||
|
||||
---
|
||||
|
||||
### 2.2 获取OI减少排行榜
|
||||
|
||||
返回持仓价值减少最多的币种排行。
|
||||
|
||||
**请求**
|
||||
```
|
||||
GET /api/oi/low-ranking
|
||||
```
|
||||
|
||||
**查询参数**
|
||||
同 [OI增加排行榜](#21-获取oi增加排行榜)
|
||||
|
||||
**示例**
|
||||
```
|
||||
GET /api/oi/low-ranking?limit=30&duration=24h
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 2.3 获取OI Top20(向后兼容)
|
||||
|
||||
**请求**
|
||||
```
|
||||
GET /api/oi/top
|
||||
```
|
||||
|
||||
固定返回1小时内OI增加最多的Top20,用于向后兼容。
|
||||
|
||||
---
|
||||
|
||||
## 3. 资金流量(NetFlow)排行接口
|
||||
|
||||
监控机构和散户的资金流向。
|
||||
|
||||
### 3.1 获取资金流入排行榜
|
||||
|
||||
**请求**
|
||||
```
|
||||
GET /api/netflow/top-ranking
|
||||
```
|
||||
|
||||
**查询参数**
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `limit` | int | 20 | 返回数量,最大100 |
|
||||
| `duration` | string | `1h` | 时间范围,见[时间范围参数](#时间范围参数说明) |
|
||||
| `type` | string | `institution` | 资金类型:`institution`(机构), `personal`(散户) |
|
||||
| `trade` | string | `future` | 交易类型:`future`(合约), `spot`(现货) |
|
||||
|
||||
**示例**
|
||||
```
|
||||
GET /api/netflow/top-ranking?limit=30&duration=4h&type=institution&trade=future
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"count": 30,
|
||||
"type": "institution",
|
||||
"trade": "合约",
|
||||
"time_range": "4h",
|
||||
"rank_type": "top",
|
||||
"limit": 30,
|
||||
"netflows": [
|
||||
{
|
||||
"rank": 1,
|
||||
"symbol": "BTCUSDT",
|
||||
"amount": 15000000.5,
|
||||
"price": 44500.0
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**
|
||||
| 字段 | 类型 | 格式 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `rank` | int | - | 排名 |
|
||||
| `symbol` | string | - | 交易对名称 |
|
||||
| `amount` | float | USDT | 资金流量,**正数=流入,负数=流出** |
|
||||
| `price` | float | USDT | 当前价格 |
|
||||
|
||||
---
|
||||
|
||||
### 3.2 获取资金流出排行榜
|
||||
|
||||
**请求**
|
||||
```
|
||||
GET /api/netflow/low-ranking
|
||||
```
|
||||
|
||||
**查询参数**
|
||||
同 [资金流入排行榜](#31-获取资金流入排行榜)
|
||||
|
||||
**示例**
|
||||
```
|
||||
GET /api/netflow/low-ranking?limit=20&duration=1h&type=personal&trade=spot
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
### 3.3 获取资金流入Top20(向后兼容)
|
||||
|
||||
**请求**
|
||||
```
|
||||
GET /api/netflow/top
|
||||
```
|
||||
|
||||
固定返回1小时内机构合约资金流入最多的Top20。
|
||||
|
||||
---
|
||||
|
||||
## 4. 涨跌幅榜接口
|
||||
|
||||
### 4.1 获取涨跌幅榜
|
||||
|
||||
同时返回涨幅榜(top)和跌幅榜(low),支持多个时间周期同时查询。
|
||||
|
||||
**请求**
|
||||
```
|
||||
GET /api/price/ranking
|
||||
```
|
||||
|
||||
**查询参数**
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `duration` | string | `1h` | 时间范围,可多选逗号分隔:`1h,4h,24h` |
|
||||
| `limit` | int | 20 | 每个榜单返回数量,最大100 |
|
||||
| `exchange` | string | `binance` | 交易所 |
|
||||
|
||||
**示例**
|
||||
```
|
||||
GET /api/price/ranking?duration=1h,4h,24h&limit=20
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"durations": ["1h", "4h", "24h"],
|
||||
"limit": 20,
|
||||
"data": {
|
||||
"1h": {
|
||||
"top": [
|
||||
{
|
||||
"pair": "MOGUSDT",
|
||||
"symbol": "MOG",
|
||||
"price_delta": 0.0723,
|
||||
"price": 0.00123,
|
||||
"future_flow": 201500,
|
||||
"spot_flow": 0,
|
||||
"oi": 15000000,
|
||||
"oi_delta": 500000,
|
||||
"oi_delta_value": 615
|
||||
}
|
||||
],
|
||||
"low": [
|
||||
{
|
||||
"pair": "XYZUSDT",
|
||||
"symbol": "XYZ",
|
||||
"price_delta": -0.0512,
|
||||
"price": 1.234,
|
||||
"future_flow": -50000,
|
||||
"spot_flow": -10000,
|
||||
"oi": 8000000,
|
||||
"oi_delta": -200000,
|
||||
"oi_delta_value": -246800
|
||||
}
|
||||
]
|
||||
},
|
||||
"4h": { ... },
|
||||
"24h": { ... }
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**
|
||||
| 字段 | 类型 | 格式 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `pair` | string | - | 完整交易对名称,如 BTCUSDT |
|
||||
| `symbol` | string | - | 币种符号(去除USDT),如 BTC |
|
||||
| `price_delta` | float | **小数** | 价格变动比例,**0.0723 = 7.23%**(需×100显示) |
|
||||
| `price` | float | USDT | 当前价格 |
|
||||
| `future_flow` | float | USDT | 合约资金流量,正数=流入 |
|
||||
| `spot_flow` | float | USDT | 现货资金流量,正数=流入 |
|
||||
| `oi` | float | 张/个 | 当前持仓量 |
|
||||
| `oi_delta` | float | 张/个 | 持仓变化量 |
|
||||
| `oi_delta_value` | float | USDT | 持仓变化价值 |
|
||||
|
||||
> **注意**:`price_delta` 使用小数格式,与 OI 接口的 `price_delta_percent` 不同!
|
||||
|
||||
---
|
||||
|
||||
## 5. 币种详情接口
|
||||
|
||||
### 5.1 获取单币种完整数据
|
||||
|
||||
获取指定币种的所有统计信息,一次调用获取全部数据。
|
||||
|
||||
**请求**
|
||||
```
|
||||
GET /api/coin/:symbol
|
||||
```
|
||||
|
||||
**路径参数**
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `symbol` | string | 是 | 币种符号,支持 `BTC` 或 `BTCUSDT` 格式 |
|
||||
|
||||
**查询参数**
|
||||
| 参数 | 类型 | 默认值 | 说明 |
|
||||
|------|------|--------|------|
|
||||
| `include` | string | `netflow,oi,price,ai500` | 包含的数据类型,逗号分隔 |
|
||||
|
||||
**include 参数选项**
|
||||
| 值 | 说明 |
|
||||
|------|------|
|
||||
| `netflow` | 资金流量数据(机构/散户,合约/现货) |
|
||||
| `oi` | 持仓量数据(币安/Bybit) |
|
||||
| `price` | 价格变化数据 |
|
||||
| `ai500` | AI500评分 |
|
||||
|
||||
**示例**
|
||||
```
|
||||
GET /api/coin/BTC?include=netflow,oi,price,ai500
|
||||
GET /api/coin/ETHUSDT?include=netflow,oi
|
||||
```
|
||||
|
||||
**响应示例**
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"data": {
|
||||
"symbol": "BTCUSDT",
|
||||
"price": 44500.0,
|
||||
"ai500": {
|
||||
"score": 85.234,
|
||||
"is_active": true,
|
||||
"start_time": 1704067200,
|
||||
"start_price": 42000.5,
|
||||
"increase_percent": 5.95
|
||||
},
|
||||
"netflow": {
|
||||
"institution": {
|
||||
"future": {
|
||||
"1m": 50000,
|
||||
"5m": 200000,
|
||||
"15m": 500000,
|
||||
"30m": 800000,
|
||||
"1h": 1500000,
|
||||
"4h": 5000000,
|
||||
"8h": 8000000,
|
||||
"12h": 10000000,
|
||||
"24h": 15000000,
|
||||
"2d": 25000000,
|
||||
"3d": 35000000,
|
||||
"5d": 50000000,
|
||||
"7d": 75000000
|
||||
},
|
||||
"spot": { ... }
|
||||
},
|
||||
"personal": {
|
||||
"future": { ... },
|
||||
"spot": { ... }
|
||||
}
|
||||
},
|
||||
"oi": {
|
||||
"binance": {
|
||||
"current_oi": 62000,
|
||||
"net_long": 35000,
|
||||
"net_short": 27000,
|
||||
"delta": {
|
||||
"1m": {
|
||||
"oi_delta": 50,
|
||||
"oi_delta_value": 2225000,
|
||||
"oi_delta_percent": 0.08
|
||||
},
|
||||
"5m": { ... },
|
||||
"1h": { ... },
|
||||
"4h": { ... },
|
||||
"24h": { ... }
|
||||
}
|
||||
},
|
||||
"bybit": { ... }
|
||||
},
|
||||
"price_change": {
|
||||
"1m": 0.001,
|
||||
"5m": 0.005,
|
||||
"15m": 0.008,
|
||||
"30m": 0.012,
|
||||
"1h": 0.015,
|
||||
"4h": 0.025,
|
||||
"8h": 0.035,
|
||||
"12h": 0.042,
|
||||
"24h": 0.055,
|
||||
"2d": 0.08,
|
||||
"3d": 0.12,
|
||||
"5d": 0.18,
|
||||
"7d": 0.25
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**字段说明**
|
||||
|
||||
**price_change 对象**
|
||||
| 字段 | 类型 | 格式 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `{duration}` | float | **小数** | 价格变化比例,**0.015 = 1.5%**(需×100显示) |
|
||||
|
||||
**netflow 对象**
|
||||
| 路径 | 类型 | 格式 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `institution.future.{duration}` | float | USDT | 机构合约资金流量 |
|
||||
| `institution.spot.{duration}` | float | USDT | 机构现货资金流量 |
|
||||
| `personal.future.{duration}` | float | USDT | 散户合约资金流量 |
|
||||
| `personal.spot.{duration}` | float | USDT | 散户现货资金流量 |
|
||||
|
||||
**oi 对象**
|
||||
| 路径 | 类型 | 格式 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `binance.current_oi` | float | 张/个 | 币安当前持仓量 |
|
||||
| `binance.net_long` | float | 张/个 | 币安净多头 |
|
||||
| `binance.net_short` | float | 张/个 | 币安净空头 |
|
||||
| `binance.delta.{duration}.oi_delta` | float | 张/个 | 持仓量变化 |
|
||||
| `binance.delta.{duration}.oi_delta_value` | float | USDT | 持仓价值变化 |
|
||||
| `binance.delta.{duration}.oi_delta_percent` | float | **已×100** | 持仓变化百分比,0.08 = 0.08% |
|
||||
| `bybit.*` | - | - | Bybit数据,结构同上 |
|
||||
|
||||
**ai500 对象**
|
||||
| 字段 | 类型 | 格式 | 说明 |
|
||||
|------|------|------|------|
|
||||
| `score` | float | 0-100 | AI综合评分 |
|
||||
| `is_active` | bool | - | 是否为活跃高分币种 |
|
||||
| `start_time` | int64 | Unix秒 | 上榜时间 |
|
||||
| `start_price` | float | USDT | 上榜时价格 |
|
||||
| `increase_percent` | float | **已×100** | 最大涨幅,5.95 = 5.95% |
|
||||
|
||||
---
|
||||
|
||||
## 错误码说明
|
||||
|
||||
| HTTP状态码 | 说明 | 常见原因 |
|
||||
|------------|------|----------|
|
||||
| 200 | 成功 | - |
|
||||
| 400 | 请求参数错误 | 参数格式不正确、缺少必填参数 |
|
||||
| 401 | 未授权 | 缺少认证信息或API Key无效 |
|
||||
| 404 | 资源不存在 | 币种不存在或未被追踪 |
|
||||
| 429 | 请求过于频繁 | 超过限流阈值(30次/秒) |
|
||||
| 500 | 服务器内部错误 | 服务端异常 |
|
||||
|
||||
**错误响应示例**
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"error": "unauthorized"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 使用示例
|
||||
|
||||
### cURL 示例
|
||||
|
||||
```bash
|
||||
# 方式1: Query参数认证
|
||||
curl "https://nofxos.ai/api/ai500/list?auth=your_api_key"
|
||||
|
||||
# 方式2: Header认证
|
||||
curl "https://nofxos.ai/api/ai500/list" \
|
||||
-H "Authorization: Bearer your_api_key"
|
||||
|
||||
# 获取1小时涨跌幅榜
|
||||
curl "https://nofxos.ai/api/price/ranking?duration=1h&limit=20&auth=your_api_key"
|
||||
|
||||
# 获取多个时间周期涨跌幅榜
|
||||
curl "https://nofxos.ai/api/price/ranking?duration=1h,4h,24h&limit=10&auth=your_api_key"
|
||||
|
||||
# 获取BTC详细数据
|
||||
curl "https://nofxos.ai/api/coin/BTC?auth=your_api_key"
|
||||
|
||||
# 只获取BTC的资金流和OI数据
|
||||
curl "https://nofxos.ai/api/coin/BTC?include=netflow,oi&auth=your_api_key"
|
||||
|
||||
# 获取4小时OI增加排行Top50
|
||||
curl "https://nofxos.ai/api/oi/top-ranking?duration=4h&limit=50&auth=your_api_key"
|
||||
|
||||
# 获取24小时OI减少排行Top30
|
||||
curl "https://nofxos.ai/api/oi/low-ranking?duration=24h&limit=30&auth=your_api_key"
|
||||
|
||||
# 获取机构合约资金流入排行
|
||||
curl "https://nofxos.ai/api/netflow/top-ranking?type=institution&trade=future&duration=1h&auth=your_api_key"
|
||||
|
||||
# 获取散户现货资金流出排行
|
||||
curl "https://nofxos.ai/api/netflow/low-ranking?type=personal&trade=spot&duration=4h&auth=your_api_key"
|
||||
```
|
||||
|
||||
### Python 示例
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
BASE_URL = "https://nofxos.ai"
|
||||
API_KEY = "your_api_key"
|
||||
|
||||
# 方式1: Query参数认证
|
||||
def get_with_query_auth(endpoint, params=None):
|
||||
if params is None:
|
||||
params = {}
|
||||
params["auth"] = API_KEY
|
||||
response = requests.get(f"{BASE_URL}{endpoint}", params=params)
|
||||
return response.json()
|
||||
|
||||
# 方式2: Header认证
|
||||
def get_with_header_auth(endpoint, params=None):
|
||||
headers = {"Authorization": f"Bearer {API_KEY}"}
|
||||
response = requests.get(f"{BASE_URL}{endpoint}", params=params, headers=headers)
|
||||
return response.json()
|
||||
|
||||
# 获取AI500列表
|
||||
def get_ai500_list():
|
||||
return get_with_query_auth("/api/ai500/list")
|
||||
|
||||
# 获取涨跌幅榜
|
||||
def get_price_ranking(durations="1h,4h,24h", limit=20):
|
||||
return get_with_query_auth("/api/price/ranking", {
|
||||
"duration": durations,
|
||||
"limit": limit
|
||||
})
|
||||
|
||||
# 获取币种详情
|
||||
def get_coin_stats(symbol, include="netflow,oi,price,ai500"):
|
||||
return get_with_query_auth(f"/api/coin/{symbol}", {
|
||||
"include": include
|
||||
})
|
||||
|
||||
# 获取OI排行
|
||||
def get_oi_ranking(rank_type="top", duration="1h", limit=20):
|
||||
endpoint = f"/api/oi/{rank_type}-ranking"
|
||||
return get_with_query_auth(endpoint, {
|
||||
"duration": duration,
|
||||
"limit": limit
|
||||
})
|
||||
|
||||
# 获取资金流排行
|
||||
def get_netflow_ranking(rank_type="top", duration="1h", limit=20,
|
||||
flow_type="institution", trade="future"):
|
||||
endpoint = f"/api/netflow/{rank_type}-ranking"
|
||||
return get_with_query_auth(endpoint, {
|
||||
"duration": duration,
|
||||
"limit": limit,
|
||||
"type": flow_type,
|
||||
"trade": trade
|
||||
})
|
||||
|
||||
# 使用示例
|
||||
if __name__ == "__main__":
|
||||
# 获取AI500推荐币种
|
||||
ai500 = get_ai500_list()
|
||||
print(f"AI500推荐币种数量: {ai500['data']['count']}")
|
||||
|
||||
# 获取1小时涨幅榜前10
|
||||
ranking = get_price_ranking("1h", 10)
|
||||
for coin in ranking['data']['data']['1h']['top'][:3]:
|
||||
# 注意: price_delta 是小数,需要×100
|
||||
pct = coin['price_delta'] * 100
|
||||
print(f"{coin['symbol']}: {pct:.2f}%")
|
||||
|
||||
# 获取BTC详情
|
||||
btc = get_coin_stats("BTC")
|
||||
# 注意: price_change 是小数
|
||||
print(f"BTC 1小时涨跌: {btc['data']['price_change']['1h'] * 100:.2f}%")
|
||||
|
||||
# 获取4小时OI增加Top20
|
||||
oi = get_oi_ranking("top", "4h", 20)
|
||||
for pos in oi['data']['positions'][:3]:
|
||||
# 注意: oi_delta_percent 已×100
|
||||
print(f"{pos['symbol']}: OI变化 {pos['oi_delta_percent']:.2f}%")
|
||||
```
|
||||
|
||||
### JavaScript/TypeScript 示例
|
||||
|
||||
```typescript
|
||||
const BASE_URL = "https://nofxos.ai";
|
||||
const API_KEY = "your_api_key";
|
||||
|
||||
// 通用请求函数
|
||||
async function apiRequest<T>(endpoint: string, params: Record<string, any> = {}): Promise<T> {
|
||||
const url = new URL(`${BASE_URL}${endpoint}`);
|
||||
params.auth = API_KEY;
|
||||
Object.entries(params).forEach(([key, value]) => {
|
||||
url.searchParams.append(key, String(value));
|
||||
});
|
||||
|
||||
const response = await fetch(url.toString());
|
||||
return response.json();
|
||||
}
|
||||
|
||||
// 获取涨跌幅榜
|
||||
interface PriceRankingItem {
|
||||
pair: string;
|
||||
symbol: string;
|
||||
price_delta: number; // 小数格式,0.05 = 5%
|
||||
price: number;
|
||||
future_flow: number;
|
||||
spot_flow: number;
|
||||
}
|
||||
|
||||
async function getPriceRanking(durations = "1h", limit = 20) {
|
||||
const data = await apiRequest<any>("/api/price/ranking", { duration: durations, limit });
|
||||
return data;
|
||||
}
|
||||
|
||||
// 使用示例
|
||||
async function main() {
|
||||
const ranking = await getPriceRanking("1h,4h", 10);
|
||||
|
||||
for (const coin of ranking.data.data["1h"].top) {
|
||||
// 转换为百分比显示
|
||||
const pctChange = (coin.price_delta * 100).toFixed(2);
|
||||
console.log(`${coin.symbol}: ${pctChange}%`);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 常见问题
|
||||
|
||||
### Q: 为什么有些百分比字段格式不同?
|
||||
|
||||
A: 这是历史原因造成的:
|
||||
- **OI接口**的 `oi_delta_percent` 和 `price_delta_percent` 是**已乘100**的格式(5.0 = 5%)
|
||||
- **涨跌幅榜和币种详情**的 `price_delta` / `price_change` 是**小数**格式(0.05 = 5%)
|
||||
|
||||
建议在前端显示时统一处理。
|
||||
|
||||
### Q: duration 参数支持哪些值?
|
||||
|
||||
A: 支持以下值:`1m`, `5m`, `15m`, `30m`, `1h`, `4h`, `8h`, `12h`, `24h`(或`1d`), `2d`, `3d`, `5d`, `7d`
|
||||
|
||||
### Q: 如何判断资金是流入还是流出?
|
||||
|
||||
A: `amount`、`future_flow`、`spot_flow` 等字段:
|
||||
- **正数** = 资金流入
|
||||
- **负数** = 资金流出
|
||||
|
||||
### Q: API缓存时间是多久?
|
||||
|
||||
A: 所有数据接口缓存15秒,相同请求在15秒内返回缓存数据。
|
||||
|
||||
### Q: 限流规则是什么?
|
||||
|
||||
A: 每个IP每秒最多30次请求,超过会返回 429 错误。
|
||||
@@ -1,350 +0,0 @@
|
||||
# 币种综合数据接口文档
|
||||
|
||||
## 接口概述
|
||||
|
||||
该接口提供单个币种的综合数据查询,一次请求即可获取资金净流入、持仓变化、价格变化等多维度数据。
|
||||
|
||||
## 请求信息
|
||||
|
||||
### 接口地址
|
||||
|
||||
```
|
||||
GET /api/coin/{symbol}
|
||||
```
|
||||
|
||||
### 完整示例
|
||||
|
||||
```
|
||||
http://nofxaios.com:30006/api/coin/PIPPINUSDT?include=netflow,oi,price&auth=cm_568c67eae410d912c54c
|
||||
```
|
||||
|
||||
### 请求参数
|
||||
|
||||
| 参数 | 位置 | 类型 | 必填 | 说明 |
|
||||
|-----|------|------|-----|------|
|
||||
| symbol | path | string | 是 | 币种符号,如 `PIPPINUSDT`、`ETH`(会自动补全USDT后缀) |
|
||||
| include | query | string | 否 | 返回数据类型,逗号分隔。可选值:`netflow,oi,price`。默认返回全部 |
|
||||
| auth | query | string | 是 | 认证密钥 |
|
||||
|
||||
### include 参数说明
|
||||
|
||||
| 值 | 说明 |
|
||||
|---|------|
|
||||
| netflow | 资金净流入数据(机构/散户、合约/现货) |
|
||||
| oi | 持仓数据(币安、Bybit) |
|
||||
| price | 价格变化百分比 |
|
||||
|
||||
---
|
||||
|
||||
## 返回数据
|
||||
|
||||
### 完整响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"symbol": "PIPPINUSDT",
|
||||
"price": 0.085,
|
||||
"netflow": {
|
||||
"institution": {
|
||||
"future": {
|
||||
"1m": 120000,
|
||||
"5m": 580000,
|
||||
"15m": 1200000,
|
||||
"30m": 2500000,
|
||||
"1h": 5800000,
|
||||
"4h": 12000000,
|
||||
"8h": 25000000,
|
||||
"12h": 38000000,
|
||||
"24h": 65000000,
|
||||
"2d": 120000000,
|
||||
"3d": 180000000
|
||||
},
|
||||
"spot": {
|
||||
"1m": 50000,
|
||||
"5m": 280000,
|
||||
"15m": 600000,
|
||||
"30m": 1200000,
|
||||
"1h": 2800000,
|
||||
"4h": 6000000,
|
||||
"8h": 12000000,
|
||||
"12h": 18000000,
|
||||
"24h": 32000000,
|
||||
"2d": 60000000,
|
||||
"3d": 90000000
|
||||
}
|
||||
},
|
||||
"personal": {
|
||||
"future": {
|
||||
"1m": -80000,
|
||||
"5m": -350000,
|
||||
"15m": -800000,
|
||||
"30m": -1500000,
|
||||
"1h": -3200000,
|
||||
"4h": -8000000,
|
||||
"8h": -15000000,
|
||||
"12h": -22000000,
|
||||
"24h": -40000000,
|
||||
"2d": -75000000,
|
||||
"3d": -110000000
|
||||
},
|
||||
"spot": {
|
||||
"1m": -30000,
|
||||
"5m": -150000,
|
||||
"15m": -400000,
|
||||
"30m": -800000,
|
||||
"1h": -1800000,
|
||||
"4h": -4000000,
|
||||
"8h": -8000000,
|
||||
"12h": -12000000,
|
||||
"24h": -22000000,
|
||||
"2d": -40000000,
|
||||
"3d": -60000000
|
||||
}
|
||||
}
|
||||
},
|
||||
"oi": {
|
||||
"binance": {
|
||||
"current_oi": 85000,
|
||||
"net_long": 48000,
|
||||
"net_short": 37000,
|
||||
"delta": {
|
||||
"1m": {
|
||||
"oi_delta": 150,
|
||||
"oi_delta_value": 14550000,
|
||||
"oi_delta_percent": 0.18
|
||||
},
|
||||
"5m": {
|
||||
"oi_delta": 680,
|
||||
"oi_delta_value": 65960000,
|
||||
"oi_delta_percent": 0.8
|
||||
},
|
||||
"1h": {
|
||||
"oi_delta": 2500,
|
||||
"oi_delta_value": 242500000,
|
||||
"oi_delta_percent": 2.94
|
||||
},
|
||||
"4h": {
|
||||
"oi_delta": 5200,
|
||||
"oi_delta_value": 504400000,
|
||||
"oi_delta_percent": 6.12
|
||||
},
|
||||
"24h": {
|
||||
"oi_delta": 8500,
|
||||
"oi_delta_value": 824500000,
|
||||
"oi_delta_percent": 10.0
|
||||
}
|
||||
}
|
||||
},
|
||||
"bybit": {
|
||||
"current_oi": 42000,
|
||||
"net_long": 24000,
|
||||
"net_short": 18000,
|
||||
"delta": {
|
||||
"1h": {
|
||||
"oi_delta": 1200,
|
||||
"oi_delta_value": 116400000,
|
||||
"oi_delta_percent": 2.86
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"price_change": {
|
||||
"1m": 0.05,
|
||||
"5m": 0.18,
|
||||
"15m": 0.35,
|
||||
"30m": 0.62,
|
||||
"1h": 1.25,
|
||||
"4h": 2.80,
|
||||
"8h": 3.50,
|
||||
"12h": 2.95,
|
||||
"24h": 4.80,
|
||||
"2d": 6.50,
|
||||
"3d": 8.20
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 字段详细说明
|
||||
|
||||
### 基础字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|-----|------|------|
|
||||
| symbol | string | 币种交易对,如 `PIPPINUSDT` |
|
||||
| price | float | 当前期货价格(单位:USDT) |
|
||||
|
||||
---
|
||||
|
||||
### netflow - 资金净流入
|
||||
|
||||
资金净流入数据,**正数表示资金流入,负数表示资金流出**,单位为 USDT。
|
||||
|
||||
#### 数据结构
|
||||
|
||||
```
|
||||
netflow
|
||||
├── institution # 机构资金
|
||||
│ ├── future # 合约市场
|
||||
│ └── spot # 现货市场
|
||||
└── personal # 散户资金
|
||||
├── future # 合约市场
|
||||
└── spot # 现货市场
|
||||
```
|
||||
|
||||
#### 分类说明
|
||||
|
||||
| 字段 | 说明 |
|
||||
|-----|------|
|
||||
| institution.future | 机构在合约市场的资金净流入 |
|
||||
| institution.spot | 机构在现货市场的资金净流入 |
|
||||
| personal.future | 散户在合约市场的资金净流入 |
|
||||
| personal.spot | 散户在现货市场的资金净流入 |
|
||||
|
||||
#### 时间周期
|
||||
|
||||
| 字段 | 说明 |
|
||||
|-----|------|
|
||||
| 1m | 最近 1 分钟 |
|
||||
| 5m | 最近 5 分钟 |
|
||||
| 15m | 最近 15 分钟 |
|
||||
| 30m | 最近 30 分钟 |
|
||||
| 1h | 最近 1 小时 |
|
||||
| 4h | 最近 4 小时 |
|
||||
| 8h | 最近 8 小时 |
|
||||
| 12h | 最近 12 小时 |
|
||||
| 24h | 最近 24 小时 |
|
||||
| 2d | 最近 2 天 |
|
||||
| 3d | 最近 3 天 |
|
||||
|
||||
#### 使用建议
|
||||
|
||||
- **机构资金流入 + 散户资金流出** = 典型的主力吸筹信号
|
||||
- **机构资金流出 + 散户资金流入** = 典型的主力出货信号
|
||||
- 关注 **合约与现货的资金流向是否一致**,判断市场情绪
|
||||
|
||||
---
|
||||
|
||||
### oi - 持仓数据
|
||||
|
||||
持仓量(Open Interest)数据,来源于币安和 Bybit 交易所。
|
||||
|
||||
#### 字段说明
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|-----|------|------|
|
||||
| current_oi | float | 当前总持仓量(单位:币) |
|
||||
| net_long | float | 净多头持仓量 |
|
||||
| net_short | float | 净空头持仓量 |
|
||||
| delta | object | 各时间周期的持仓变化 |
|
||||
|
||||
#### delta 子字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|-----|------|------|
|
||||
| oi_delta | float | 持仓量变化(单位:币) |
|
||||
| oi_delta_value | float | 持仓价值变化(单位:USDT) |
|
||||
| oi_delta_percent | float | 持仓量变化百分比(%) |
|
||||
|
||||
#### 使用建议
|
||||
|
||||
- **持仓量增加 + 价格上涨** = 多头主导,趋势可能延续
|
||||
- **持仓量增加 + 价格下跌** = 空头主导,下跌趋势可能延续
|
||||
- **持仓量减少 + 价格变化** = 平仓为主,趋势可能反转
|
||||
- **net_long > net_short** = 市场整体偏多
|
||||
|
||||
---
|
||||
|
||||
### price_change - 价格变化
|
||||
|
||||
各时间周期的价格涨跌幅,**单位为百分比(%)**,正数表示上涨,负数表示下跌。
|
||||
|
||||
| 字段 | 说明 |
|
||||
|-----|------|
|
||||
| 1m | 最近 1 分钟涨跌幅 |
|
||||
| 5m | 最近 5 分钟涨跌幅 |
|
||||
| 15m | 最近 15 分钟涨跌幅 |
|
||||
| 30m | 最近 30 分钟涨跌幅 |
|
||||
| 1h | 最近 1 小时涨跌幅 |
|
||||
| 4h | 最近 4 小时涨跌幅 |
|
||||
| 8h | 最近 8 小时涨跌幅 |
|
||||
| 12h | 最近 12 小时涨跌幅 |
|
||||
| 24h | 最近 24 小时涨跌幅 |
|
||||
| 2d | 最近 2 天涨跌幅 |
|
||||
| 3d | 最近 3 天涨跌幅 |
|
||||
|
||||
---
|
||||
|
||||
## 错误响应
|
||||
|
||||
| code | 说明 |
|
||||
|------|------|
|
||||
| 0 | 成功 |
|
||||
| 400 | 参数错误(如缺少 symbol) |
|
||||
| 401 | 认证失败(auth 无效) |
|
||||
| 500 | 服务器内部错误 |
|
||||
|
||||
错误响应示例:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 400,
|
||||
"message": "symbol parameter is required"
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 调用示例
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
curl -X GET "http://nofxaios.com:30006/api/coin/PIPPINUSDT?include=netflow,oi,price&auth=cm_568c67eae410d912c54c"
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
url = "http://nofxaios.com:30006/api/coin/PIPPINUSDT"
|
||||
params = {
|
||||
"include": "netflow,oi,price",
|
||||
"auth": "cm_568c67eae410d912c54c"
|
||||
}
|
||||
|
||||
response = requests.get(url, params=params)
|
||||
data = response.json()
|
||||
|
||||
print(f"当前价格: {data['data']['price']}")
|
||||
print(f"1小时机构合约净流入: {data['data']['netflow']['institution']['future']['1h']}")
|
||||
print(f"24小时价格涨跌幅: {data['data']['price_change']['24h']}%")
|
||||
```
|
||||
|
||||
### JavaScript
|
||||
|
||||
```javascript
|
||||
const url = 'http://nofxaios.com:30006/api/coin/PIPPINUSDT?include=netflow,oi,price&auth=cm_568c67eae410d912c54c';
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
console.log('当前价格:', data.data.price);
|
||||
console.log('1小时机构合约净流入:', data.data.netflow.institution.future['1h']);
|
||||
console.log('24小时价格涨跌幅:', data.data.price_change['24h'], '%');
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **symbol 参数**:支持带或不带 `USDT` 后缀,如 `PIPPIN` 和 `PIPPINUSDT` 等效
|
||||
2. **include 参数**:可按需选择返回数据,减少不必要的数据传输
|
||||
3. **数据更新频率**:数据实时更新,建议轮询间隔不低于 1 秒
|
||||
4. **资金流向解读**:机构与散户的资金流向通常呈相反趋势,可作为市场情绪判断依据
|
||||
@@ -1,254 +0,0 @@
|
||||
# OI 持仓数据接口文档
|
||||
|
||||
## 接口概述
|
||||
|
||||
该接口提供币安交易所的合约持仓量(Open Interest)排行数据,支持查询持仓增加和减少排行榜。
|
||||
|
||||
## 接口列表
|
||||
|
||||
| 接口 | 说明 |
|
||||
|-----|------|
|
||||
| `/api/oi/top` | 持仓增加排行 Top20(固定参数,向后兼容) |
|
||||
| `/api/oi/top-ranking` | 持仓增加排行(支持自定义参数) |
|
||||
| `/api/oi/low-ranking` | 持仓减少排行(支持自定义参数) |
|
||||
|
||||
---
|
||||
|
||||
## 1. 持仓增加排行 Top20
|
||||
|
||||
### 请求
|
||||
|
||||
```
|
||||
GET /api/oi/top
|
||||
```
|
||||
|
||||
### 完整示例
|
||||
|
||||
```
|
||||
http://nofxaios.com:30006/api/oi/top?auth=cm_568c67eae410d912c54c
|
||||
```
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 说明 |
|
||||
|-----|------|-----|------|
|
||||
| auth | string | 是 | 认证密钥 |
|
||||
|
||||
### 说明
|
||||
|
||||
固定返回 1 小时内持仓价值增加最多的前 20 个币种,向后兼容接口。
|
||||
|
||||
---
|
||||
|
||||
## 2. 持仓增加排行(自定义参数)
|
||||
|
||||
### 请求
|
||||
|
||||
```
|
||||
GET /api/oi/top-ranking
|
||||
```
|
||||
|
||||
### 完整示例
|
||||
|
||||
```
|
||||
http://nofxaios.com:30006/api/oi/top-ranking?limit=50&duration=4h&auth=cm_568c67eae410d912c54c
|
||||
```
|
||||
|
||||
### 参数
|
||||
|
||||
| 参数 | 类型 | 必填 | 默认值 | 说明 |
|
||||
|-----|------|-----|-------|------|
|
||||
| limit | int | 否 | 20 | 获取数量,范围 1-100 |
|
||||
| duration | string | 否 | 1h | 时间范围 |
|
||||
| auth | string | 是 | - | 认证密钥 |
|
||||
|
||||
---
|
||||
|
||||
## 3. 持仓减少排行
|
||||
|
||||
### 请求
|
||||
|
||||
```
|
||||
GET /api/oi/low-ranking
|
||||
```
|
||||
|
||||
### 完整示例
|
||||
|
||||
```
|
||||
http://nofxaios.com:30006/api/oi/low-ranking?limit=30&duration=24h&auth=cm_568c67eae410d912c54c
|
||||
```
|
||||
|
||||
### 参数
|
||||
|
||||
同持仓增加排行接口。
|
||||
|
||||
---
|
||||
|
||||
## duration 时间范围参数
|
||||
|
||||
| 值 | 说明 |
|
||||
|---|------|
|
||||
| 1m | 1 分钟 |
|
||||
| 5m | 5 分钟 |
|
||||
| 15m | 15 分钟 |
|
||||
| 30m | 30 分钟 |
|
||||
| 1h | 1 小时(默认) |
|
||||
| 4h | 4 小时 |
|
||||
| 8h | 8 小时 |
|
||||
| 12h | 12 小时 |
|
||||
| 24h | 24 小时 |
|
||||
| 1d | 1 天(同 24h) |
|
||||
| 2d | 2 天 |
|
||||
| 3d | 3 天 |
|
||||
|
||||
---
|
||||
|
||||
## 返回数据
|
||||
|
||||
### 响应示例
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"data": {
|
||||
"count": 20,
|
||||
"exchange": "binance",
|
||||
"time_range": "4小时",
|
||||
"time_range_param": "4h",
|
||||
"rank_type": "top",
|
||||
"limit": 20,
|
||||
"positions": [
|
||||
{
|
||||
"rank": 1,
|
||||
"symbol": "BTCUSDT",
|
||||
"oi_delta": 1500.5,
|
||||
"oi_delta_value": 145500000,
|
||||
"oi_delta_percent": 3.52,
|
||||
"current_oi": 44000,
|
||||
"price_delta_percent": 2.15,
|
||||
"net_long": 26000,
|
||||
"net_short": 18000
|
||||
},
|
||||
{
|
||||
"rank": 2,
|
||||
"symbol": "ETHUSDT",
|
||||
"oi_delta": 25000,
|
||||
"oi_delta_value": 87500000,
|
||||
"oi_delta_percent": 2.85,
|
||||
"current_oi": 900000,
|
||||
"price_delta_percent": 1.80,
|
||||
"net_long": 520000,
|
||||
"net_short": 380000
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 字段说明
|
||||
|
||||
#### 外层字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|-----|------|------|
|
||||
| count | int | 返回的币种数量 |
|
||||
| exchange | string | 交易所,固定为 `binance` |
|
||||
| time_range | string | 时间范围显示名称 |
|
||||
| time_range_param | string | 时间范围参数值 |
|
||||
| rank_type | string | 排行类型:`top` 增加 / `low` 减少 |
|
||||
| limit | int | 请求的数量限制 |
|
||||
| positions | array | 持仓数据列表 |
|
||||
|
||||
#### positions 数组字段
|
||||
|
||||
| 字段 | 类型 | 说明 |
|
||||
|-----|------|------|
|
||||
| rank | int | 排名 |
|
||||
| symbol | string | 币种交易对,如 `BTCUSDT` |
|
||||
| oi_delta | float | 持仓量变化(单位:币) |
|
||||
| oi_delta_value | float | 持仓价值变化(单位:USDT),**排序依据** |
|
||||
| oi_delta_percent | float | 持仓量变化百分比(%) |
|
||||
| current_oi | float | 当前持仓量(单位:币) |
|
||||
| price_delta_percent | float | 价格变化百分比(%) |
|
||||
| net_long | float | 净多头持仓量 |
|
||||
| net_short | float | 净空头持仓量 |
|
||||
|
||||
---
|
||||
|
||||
## 数据解读
|
||||
|
||||
### 持仓量与价格的关系
|
||||
|
||||
| 持仓变化 | 价格变化 | 市场含义 |
|
||||
|---------|---------|---------|
|
||||
| 增加 | 上涨 | 多头主导,上涨趋势可能延续 |
|
||||
| 增加 | 下跌 | 空头主导,下跌趋势可能延续 |
|
||||
| 减少 | 上涨 | 空头平仓,可能是反弹 |
|
||||
| 减少 | 下跌 | 多头平仓,可能是回调 |
|
||||
|
||||
### 多空比例
|
||||
|
||||
- `net_long > net_short`:市场整体偏多
|
||||
- `net_long < net_short`:市场整体偏空
|
||||
|
||||
---
|
||||
|
||||
## 调用示例
|
||||
|
||||
### cURL
|
||||
|
||||
```bash
|
||||
curl -X GET "http://nofxaios.com:30006/api/oi/top-ranking?limit=50&duration=4h&auth=cm_568c67eae410d912c54c"
|
||||
```
|
||||
|
||||
### Python
|
||||
|
||||
```python
|
||||
import requests
|
||||
|
||||
url = "http://nofxaios.com:30006/api/oi/top-ranking"
|
||||
params = {
|
||||
"limit": 50,
|
||||
"duration": "4h",
|
||||
"auth": "cm_568c67eae410d912c54c"
|
||||
}
|
||||
|
||||
response = requests.get(url, params=params)
|
||||
data = response.json()
|
||||
|
||||
for pos in data['data']['positions']:
|
||||
print(f"#{pos['rank']} {pos['symbol']}: 持仓价值变化 ${pos['oi_delta_value']:,.0f}")
|
||||
```
|
||||
|
||||
### JavaScript
|
||||
|
||||
```javascript
|
||||
const url = 'http://nofxaios.com:30006/api/oi/top-ranking?limit=50&duration=4h&auth=cm_568c67eae410d912c54c';
|
||||
|
||||
fetch(url)
|
||||
.then(response => response.json())
|
||||
.then(data => {
|
||||
data.data.positions.forEach(pos => {
|
||||
console.log(`#${pos.rank} ${pos.symbol}: 持仓价值变化 $${pos.oi_delta_value.toLocaleString()}`);
|
||||
});
|
||||
});
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 错误响应
|
||||
|
||||
| code | 说明 |
|
||||
|------|------|
|
||||
| 0 | 成功 |
|
||||
| 401 | 认证失败(auth 无效) |
|
||||
| 500 | 服务器内部错误 |
|
||||
|
||||
---
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. 数据来源为币安交易所
|
||||
2. 排行依据为 `oi_delta_value`(持仓价值变化),非持仓量变化
|
||||
3. 数据缓存 2 秒,高频请求会命中缓存
|
||||
4. `limit` 最大值为 100
|
||||
@@ -112,7 +112,7 @@ func (e *StrategyEngine) getCoinPoolCoins(limit int) []CandidateCoin {
|
||||
}
|
||||
```
|
||||
|
||||
- **API:** `config.CoinSource.CoinPoolAPIURL` (默认: `http://nofxaios.com:30006/api/ai500/list`)
|
||||
- **API:** `config.CoinSource.CoinPoolAPIURL` (默认: `https://nofxos.ai/api/ai500/list`)
|
||||
- **用途:** 获取 AI 评分最高的 N 个币种
|
||||
- **标签:** `["ai500"]`
|
||||
|
||||
|
||||
+183
-70
@@ -8,7 +8,7 @@ import (
|
||||
"nofx/logger"
|
||||
"nofx/market"
|
||||
"nofx/mcp"
|
||||
"nofx/provider"
|
||||
"nofx/provider/nofxos"
|
||||
"nofx/security"
|
||||
"nofx/store"
|
||||
"regexp"
|
||||
@@ -119,8 +119,10 @@ type Context struct {
|
||||
MultiTFMarket map[string]map[string]*market.Data `json:"-"`
|
||||
OITopDataMap map[string]*OITopData `json:"-"`
|
||||
QuantDataMap map[string]*QuantData `json:"-"`
|
||||
OIRankingData *provider.OIRankingData `json:"-"` // Market-wide OI ranking data
|
||||
BTCETHLeverage int `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:"-"`
|
||||
}
|
||||
@@ -189,12 +191,23 @@ type OIDeltaData struct {
|
||||
|
||||
// StrategyEngine strategy execution engine
|
||||
type StrategyEngine struct {
|
||||
config *store.StrategyConfig
|
||||
config *store.StrategyConfig
|
||||
nofxosClient *nofxos.Client
|
||||
}
|
||||
|
||||
// NewStrategyEngine creates strategy execution engine
|
||||
func NewStrategyEngine(config *store.StrategyConfig) *StrategyEngine {
|
||||
return &StrategyEngine{config: config}
|
||||
// Create NofxOS client with API key from config
|
||||
apiKey := config.Indicators.NofxOSAPIKey
|
||||
if apiKey == "" {
|
||||
apiKey = nofxos.DefaultAuthKey
|
||||
}
|
||||
client := nofxos.NewClient(nofxos.DefaultBaseURL, apiKey)
|
||||
|
||||
return &StrategyEngine{
|
||||
config: config,
|
||||
nofxosClient: client,
|
||||
}
|
||||
}
|
||||
|
||||
// GetRiskControlConfig gets risk control configuration
|
||||
@@ -202,6 +215,19 @@ 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
|
||||
@@ -239,7 +265,7 @@ func GetFullDecisionWithStrategy(ctx *Context, mcpClient mcp.AIClient, engine *S
|
||||
// Ensure OITopDataMap is initialized
|
||||
if ctx.OITopDataMap == nil {
|
||||
ctx.OITopDataMap = make(map[string]*OITopData)
|
||||
oiPositions, err := provider.GetOITopPositions()
|
||||
oiPositions, err := engine.nofxosClient.GetOITopPositions()
|
||||
if err == nil {
|
||||
for _, pos := range oiPositions {
|
||||
ctx.OITopDataMap[pos.Symbol] = &OITopData{
|
||||
@@ -385,13 +411,6 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
||||
|
||||
coinSource := e.config.CoinSource
|
||||
|
||||
if coinSource.CoinPoolAPIURL != "" {
|
||||
provider.SetCoinPoolAPI(coinSource.CoinPoolAPIURL)
|
||||
}
|
||||
if coinSource.OITopAPIURL != "" {
|
||||
provider.SetOITopAPI(coinSource.OITopAPIURL)
|
||||
}
|
||||
|
||||
switch coinSource.SourceType {
|
||||
case "static":
|
||||
for _, symbol := range coinSource.StaticCoins {
|
||||
@@ -404,10 +423,10 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
||||
|
||||
return e.filterExcludedCoins(candidates), nil
|
||||
|
||||
case "coinpool":
|
||||
// 检查 use_coin_pool 标志,如果为 false 则回退到静态币种
|
||||
if !coinSource.UseCoinPool {
|
||||
logger.Infof("⚠️ source_type is 'coinpool' but use_coin_pool is false, falling back to static coins")
|
||||
case "ai500":
|
||||
// 检查 use_ai500 标志,如果为 false 则回退到静态币种
|
||||
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{
|
||||
@@ -417,7 +436,7 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
||||
}
|
||||
return e.filterExcludedCoins(candidates), nil
|
||||
}
|
||||
coins, err := e.getCoinPoolCoins(coinSource.CoinPoolLimit)
|
||||
coins, err := e.getAI500Coins(coinSource.AI500Limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -443,10 +462,10 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
||||
return e.filterExcludedCoins(coins), nil
|
||||
|
||||
case "mixed":
|
||||
if coinSource.UseCoinPool {
|
||||
poolCoins, err := e.getCoinPoolCoins(coinSource.CoinPoolLimit)
|
||||
if coinSource.UseAI500 {
|
||||
poolCoins, err := e.getAI500Coins(coinSource.AI500Limit)
|
||||
if err != nil {
|
||||
logger.Infof("⚠️ Failed to get AI500 coin pool: %v", err)
|
||||
logger.Infof("⚠️ Failed to get AI500 coins: %v", err)
|
||||
} else {
|
||||
for _, coin := range poolCoins {
|
||||
symbolSources[coin.Symbol] = append(symbolSources[coin.Symbol], "ai500")
|
||||
@@ -513,12 +532,12 @@ func (e *StrategyEngine) filterExcludedCoins(candidates []CandidateCoin) []Candi
|
||||
return filtered
|
||||
}
|
||||
|
||||
func (e *StrategyEngine) getCoinPoolCoins(limit int) ([]CandidateCoin, error) {
|
||||
func (e *StrategyEngine) getAI500Coins(limit int) ([]CandidateCoin, error) {
|
||||
if limit <= 0 {
|
||||
limit = 30
|
||||
}
|
||||
|
||||
symbols, err := provider.GetTopRatedCoins(limit)
|
||||
symbols, err := e.nofxosClient.GetTopRatedCoins(limit)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -538,7 +557,7 @@ func (e *StrategyEngine) getOITopCoins(limit int) ([]CandidateCoin, error) {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
positions, err := provider.GetOITopPositions()
|
||||
positions, err := e.nofxosClient.GetOITopPositions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -645,50 +664,82 @@ func extractJSONPath(data interface{}, path string) interface{} {
|
||||
|
||||
// FetchQuantData fetches quantitative data for a single coin
|
||||
func (e *StrategyEngine) FetchQuantData(symbol string) (*QuantData, error) {
|
||||
if !e.config.Indicators.EnableQuantData || e.config.Indicators.QuantDataAPIURL == "" {
|
||||
if !e.config.Indicators.EnableQuantData {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
apiURL := e.config.Indicators.QuantDataAPIURL
|
||||
url := strings.Replace(apiURL, "{symbol}", symbol, -1)
|
||||
// Use nofxos client with unified API key
|
||||
include := "oi,price"
|
||||
if e.config.Indicators.EnableQuantNetflow {
|
||||
include = "netflow,oi,price"
|
||||
}
|
||||
|
||||
// SSRF Protection: Validate URL before making request
|
||||
resp, err := security.SafeGet(url, 10*time.Second)
|
||||
nofxosData, err := e.nofxosClient.GetCoinData(symbol, include)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("HTTP status code: %d", resp.StatusCode)
|
||||
return nil, fmt.Errorf("failed to fetch quant data: %w", err)
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
if nofxosData == nil {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
var apiResp struct {
|
||||
Code int `json:"code"`
|
||||
Data *QuantData `json:"data"`
|
||||
// Convert nofxos.QuantData to kernel.QuantData
|
||||
quantData := &QuantData{
|
||||
Symbol: nofxosData.Symbol,
|
||||
Price: nofxosData.Price,
|
||||
PriceChange: nofxosData.PriceChange,
|
||||
}
|
||||
|
||||
if err := json.Unmarshal(body, &apiResp); err != nil {
|
||||
return nil, fmt.Errorf("failed to parse JSON: %w", err)
|
||||
// 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if apiResp.Code != 0 {
|
||||
return nil, fmt.Errorf("API returned error code: %d", apiResp.Code)
|
||||
// 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 apiResp.Data, nil
|
||||
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 || e.config.Indicators.QuantDataAPIURL == "" {
|
||||
if !e.config.Indicators.EnableQuantData {
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -707,28 +758,12 @@ func (e *StrategyEngine) FetchQuantDataBatch(symbols []string) map[string]*Quant
|
||||
}
|
||||
|
||||
// FetchOIRankingData fetches market-wide OI ranking data
|
||||
func (e *StrategyEngine) FetchOIRankingData() *provider.OIRankingData {
|
||||
func (e *StrategyEngine) FetchOIRankingData() *nofxos.OIRankingData {
|
||||
indicators := e.config.Indicators
|
||||
if !indicators.EnableOIRanking {
|
||||
return nil
|
||||
}
|
||||
|
||||
baseURL := indicators.OIRankingAPIURL
|
||||
if baseURL == "" {
|
||||
baseURL = "http://nofxaios.com:30006"
|
||||
}
|
||||
|
||||
// Get auth key from existing API URL or use default
|
||||
authKey := "cm_568c67eae410d912c54c"
|
||||
if indicators.QuantDataAPIURL != "" {
|
||||
if idx := strings.Index(indicators.QuantDataAPIURL, "auth="); idx != -1 {
|
||||
authKey = indicators.QuantDataAPIURL[idx+5:]
|
||||
if ampIdx := strings.Index(authKey, "&"); ampIdx != -1 {
|
||||
authKey = authKey[:ampIdx]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
duration := indicators.OIRankingDuration
|
||||
if duration == "" {
|
||||
duration = "1h"
|
||||
@@ -741,7 +776,7 @@ func (e *StrategyEngine) FetchOIRankingData() *provider.OIRankingData {
|
||||
|
||||
logger.Infof("📊 Fetching OI ranking data (duration: %s, limit: %d)", duration, limit)
|
||||
|
||||
data, err := provider.GetOIRankingData(baseURL, authKey, duration, limit)
|
||||
data, err := e.nofxosClient.GetOIRanking(duration, limit)
|
||||
if err != nil {
|
||||
logger.Warnf("⚠️ Failed to fetch OI ranking data: %v", err)
|
||||
return nil
|
||||
@@ -753,6 +788,68 @@ func (e *StrategyEngine) FetchOIRankingData() *provider.OIRankingData {
|
||||
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
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Prompt Building - System Prompt
|
||||
// ============================================================================
|
||||
@@ -764,7 +861,7 @@ func (e *StrategyEngine) BuildSystemPrompt(accountEquity float64, variant string
|
||||
promptSections := e.config.PromptSections
|
||||
|
||||
// 0. Data Dictionary & Schema (ensure AI understands all fields)
|
||||
lang := detectLanguage(promptSections.RoleDefinition)
|
||||
lang := e.GetLanguage()
|
||||
schemaPrompt := GetSchemaPrompt(lang)
|
||||
sb.WriteString(schemaPrompt)
|
||||
sb.WriteString("\n\n")
|
||||
@@ -955,7 +1052,7 @@ func (e *StrategyEngine) writeAvailableIndicators(sb *strings.Builder) {
|
||||
sb.WriteString("- Funding rate\n")
|
||||
}
|
||||
|
||||
if len(e.config.CoinSource.StaticCoins) > 0 || e.config.CoinSource.UseCoinPool || e.config.CoinSource.UseOITop {
|
||||
if len(e.config.CoinSource.StaticCoins) > 0 || e.config.CoinSource.UseAI500 || e.config.CoinSource.UseOITop {
|
||||
sb.WriteString("- AI500 / OI_Top filter tags (if available)\n")
|
||||
}
|
||||
|
||||
@@ -1011,8 +1108,8 @@ func (e *StrategyEngine) BuildUserPrompt(ctx *Context) string {
|
||||
|
||||
// Historical trading statistics (helps AI understand past performance)
|
||||
if ctx.TradingStats != nil && ctx.TradingStats.TotalTrades > 0 {
|
||||
// Detect language from strategy config
|
||||
lang := detectLanguage(e.config.PromptSections.RoleDefinition)
|
||||
// Get language from strategy config
|
||||
lang := e.GetLanguage()
|
||||
|
||||
// Win/Loss ratio
|
||||
var winLossRatio float64
|
||||
@@ -1116,9 +1213,25 @@ func (e *StrategyEngine) BuildUserPrompt(ctx *Context) string {
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
|
||||
// Get language for market data formatting
|
||||
nofxosLang := nofxos.LangEnglish
|
||||
if e.GetLanguage() == LangChinese {
|
||||
nofxosLang = nofxos.LangChinese
|
||||
}
|
||||
|
||||
// OI Ranking data (market-wide open interest changes)
|
||||
if ctx.OIRankingData != nil {
|
||||
sb.WriteString(provider.FormatOIRankingForAI(ctx.OIRankingData))
|
||||
sb.WriteString(nofxos.FormatOIRankingForAI(ctx.OIRankingData, nofxosLang))
|
||||
}
|
||||
|
||||
// NetFlow Ranking data (market-wide fund flow)
|
||||
if ctx.NetFlowRankingData != nil {
|
||||
sb.WriteString(nofxos.FormatNetFlowRankingForAI(ctx.NetFlowRankingData, nofxosLang))
|
||||
}
|
||||
|
||||
// Price Ranking data (market-wide gainers/losers)
|
||||
if ctx.PriceRankingData != nil {
|
||||
sb.WriteString(nofxos.FormatPriceRankingForAI(ctx.PriceRankingData, nofxosLang))
|
||||
}
|
||||
|
||||
sb.WriteString("---\n\n")
|
||||
|
||||
+4
-12
@@ -3,6 +3,7 @@ package kernel
|
||||
import (
|
||||
"fmt"
|
||||
"nofx/market"
|
||||
"nofx/provider/nofxos"
|
||||
"sort"
|
||||
"strings"
|
||||
"time"
|
||||
@@ -89,11 +90,11 @@ func formatContextData(ctx *Context, lang Language) string {
|
||||
|
||||
// 7. OI排名数据(如果有)
|
||||
if ctx.OIRankingData != nil {
|
||||
nofxosLang := nofxos.LangEnglish
|
||||
if lang == LangChinese {
|
||||
sb.WriteString(formatOIRankingZH(ctx.OIRankingData))
|
||||
} else {
|
||||
sb.WriteString(formatOIRankingEN(ctx.OIRankingData))
|
||||
nofxosLang = nofxos.LangChinese
|
||||
}
|
||||
sb.WriteString(nofxos.FormatOIRankingForAI(ctx.OIRankingData, nofxosLang))
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
@@ -354,11 +355,6 @@ func formatKlineDataZH(symbol string, tfData map[string]*market.TimeframeSeriesD
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatOIRankingZH 格式化OI排名数据(中文)
|
||||
func formatOIRankingZH(oiData interface{}) string {
|
||||
// TODO: 根据实际OIRankingData结构实现
|
||||
return "## 市场持仓量排名\n\n(数据加载中...)\n\n"
|
||||
}
|
||||
|
||||
// getOIInterpretationZH 获取OI变化解读(中文)
|
||||
func getOIInterpretationZH(oiChange, priceChange string) string {
|
||||
@@ -624,10 +620,6 @@ func formatKlineDataEN(symbol string, tfData map[string]*market.TimeframeSeriesD
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatOIRankingEN 格式化OI排名数据(英文)
|
||||
func formatOIRankingEN(oiData interface{}) string {
|
||||
return "## Market-wide OI Ranking\n\n(Loading data...)\n\n"
|
||||
}
|
||||
|
||||
// getOIInterpretationEN 获取OI变化解读(英文)
|
||||
func getOIInterpretationEN(oiChange, priceChange string) string {
|
||||
|
||||
@@ -606,7 +606,7 @@ func (tm *TraderManager) LoadTradersFromStore(st *store.Store) error {
|
||||
continue
|
||||
}
|
||||
|
||||
// Add to TraderManager (coinPoolURL/oiTopURL already obtained from strategy config)
|
||||
// Add to TraderManager (ai500APIURL/oiTopAPIURL already obtained from strategy config)
|
||||
err = tm.addTraderFromStore(traderCfg, aiModelCfg, exchangeCfg, st)
|
||||
if err != nil {
|
||||
logger.Infof("❌ Failed to add trader %s: %v", traderCfg.Name, err)
|
||||
@@ -641,7 +641,7 @@ func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg
|
||||
return fmt.Errorf("trader %s has no strategy configured", traderCfg.Name)
|
||||
}
|
||||
|
||||
// Build AutoTraderConfig (coinPoolURL/oiTopURL obtained from strategy config, used in StrategyEngine)
|
||||
// Build AutoTraderConfig (ai500APIURL/oiTopAPIURL obtained from strategy config, used in StrategyEngine)
|
||||
traderConfig := trader.AutoTraderConfig{
|
||||
ID: traderCfg.ID,
|
||||
Name: traderCfg.Name,
|
||||
|
||||
@@ -1,593 +0,0 @@
|
||||
package provider
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"net/http"
|
||||
"nofx/security"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// AI500Config AI500 data provider configuration
|
||||
type AI500Config struct {
|
||||
APIURL string
|
||||
Timeout time.Duration
|
||||
}
|
||||
|
||||
var ai500Config = AI500Config{
|
||||
APIURL: "",
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
// CoinData coin information
|
||||
type CoinData struct {
|
||||
Pair string `json:"pair"` // Trading pair symbol (e.g.: BTCUSDT)
|
||||
Score float64 `json:"score"` // Current score
|
||||
StartTime int64 `json:"start_time"` // Start time (Unix timestamp)
|
||||
StartPrice float64 `json:"start_price"` // Start price
|
||||
LastScore float64 `json:"last_score"` // Latest score
|
||||
MaxScore float64 `json:"max_score"` // Highest score
|
||||
MaxPrice float64 `json:"max_price"` // Highest price
|
||||
IncreasePercent float64 `json:"increase_percent"` // Increase percentage
|
||||
IsAvailable bool `json:"-"` // Whether tradable (internal use)
|
||||
}
|
||||
|
||||
// AI500APIResponse raw data structure returned by AI500 API
|
||||
type AI500APIResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data struct {
|
||||
Coins []CoinData `json:"coins"`
|
||||
Count int `json:"count"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// SetAI500API sets AI500 data provider API
|
||||
func SetAI500API(apiURL string) {
|
||||
ai500Config.APIURL = apiURL
|
||||
}
|
||||
|
||||
// SetOITopAPI sets OI Top API
|
||||
func SetOITopAPI(apiURL string) {
|
||||
oiTopConfig.APIURL = apiURL
|
||||
}
|
||||
|
||||
|
||||
// GetAI500Data retrieves AI500 coin list (with retry mechanism)
|
||||
func GetAI500Data() ([]CoinData, error) {
|
||||
// Check if API URL is configured
|
||||
if strings.TrimSpace(ai500Config.APIURL) == "" {
|
||||
return nil, fmt.Errorf("AI500 API URL not configured")
|
||||
}
|
||||
|
||||
maxRetries := 3
|
||||
var lastErr error
|
||||
|
||||
// Try to fetch from API
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 1 {
|
||||
log.Printf("⚠️ Retry attempt %d of %d to fetch AI500 data...", attempt, maxRetries)
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
coins, err := fetchAI500()
|
||||
if err == nil {
|
||||
if attempt > 1 {
|
||||
log.Printf("✓ Retry attempt %d succeeded", attempt)
|
||||
}
|
||||
return coins, nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
log.Printf("❌ Request attempt %d failed: %v", attempt, err)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("all API requests failed: %w", lastErr)
|
||||
}
|
||||
|
||||
// fetchAI500 actually executes AI500 request
|
||||
func fetchAI500() ([]CoinData, error) {
|
||||
log.Printf("🔄 Requesting AI500 data...")
|
||||
|
||||
// SSRF Protection: Validate URL before making request
|
||||
resp, err := security.SafeGet(ai500Config.APIURL, ai500Config.Timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to request AI500 API: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("API returned error (status %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
// Parse API response
|
||||
var response AI500APIResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, fmt.Errorf("JSON parsing failed: %w", err)
|
||||
}
|
||||
|
||||
if !response.Success {
|
||||
return nil, fmt.Errorf("API returned failure status")
|
||||
}
|
||||
|
||||
if len(response.Data.Coins) == 0 {
|
||||
return nil, fmt.Errorf("coin list is empty")
|
||||
}
|
||||
|
||||
// Set IsAvailable flag
|
||||
coins := response.Data.Coins
|
||||
for i := range coins {
|
||||
coins[i].IsAvailable = true
|
||||
}
|
||||
|
||||
log.Printf("✓ Successfully fetched %d coins", len(coins))
|
||||
return coins, nil
|
||||
}
|
||||
|
||||
// GetAvailableCoins retrieves available coin list (filters out unavailable ones)
|
||||
func GetAvailableCoins() ([]string, error) {
|
||||
coins, err := GetAI500Data()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var symbols []string
|
||||
for _, coin := range coins {
|
||||
if coin.IsAvailable {
|
||||
symbol := normalizeSymbol(coin.Pair)
|
||||
symbols = append(symbols, symbol)
|
||||
}
|
||||
}
|
||||
|
||||
if len(symbols) == 0 {
|
||||
return nil, fmt.Errorf("no available coins")
|
||||
}
|
||||
|
||||
return symbols, nil
|
||||
}
|
||||
|
||||
// GetTopRatedCoins retrieves top N coins by score (sorted by score descending)
|
||||
func GetTopRatedCoins(limit int) ([]string, error) {
|
||||
coins, err := GetAI500Data()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filter available coins
|
||||
var availableCoins []CoinData
|
||||
for _, coin := range coins {
|
||||
if coin.IsAvailable {
|
||||
availableCoins = append(availableCoins, coin)
|
||||
}
|
||||
}
|
||||
|
||||
if len(availableCoins) == 0 {
|
||||
return nil, fmt.Errorf("no available coins")
|
||||
}
|
||||
|
||||
// Sort by Score descending (bubble sort)
|
||||
for i := 0; i < len(availableCoins); i++ {
|
||||
for j := i + 1; j < len(availableCoins); j++ {
|
||||
if availableCoins[i].Score < availableCoins[j].Score {
|
||||
availableCoins[i], availableCoins[j] = availableCoins[j], availableCoins[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Take top N
|
||||
maxCount := limit
|
||||
if len(availableCoins) < maxCount {
|
||||
maxCount = len(availableCoins)
|
||||
}
|
||||
|
||||
var symbols []string
|
||||
for i := 0; i < maxCount; i++ {
|
||||
symbol := normalizeSymbol(availableCoins[i].Pair)
|
||||
symbols = append(symbols, symbol)
|
||||
}
|
||||
|
||||
return symbols, nil
|
||||
}
|
||||
|
||||
// normalizeSymbol normalizes coin symbol
|
||||
func normalizeSymbol(symbol string) string {
|
||||
symbol = trimSpaces(symbol)
|
||||
symbol = toUpper(symbol)
|
||||
if !endsWith(symbol, "USDT") {
|
||||
symbol = symbol + "USDT"
|
||||
}
|
||||
return symbol
|
||||
}
|
||||
|
||||
// Helper functions
|
||||
func trimSpaces(s string) string {
|
||||
result := ""
|
||||
for i := 0; i < len(s); i++ {
|
||||
if s[i] != ' ' {
|
||||
result += string(s[i])
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func toUpper(s string) string {
|
||||
result := ""
|
||||
for i := 0; i < len(s); i++ {
|
||||
c := s[i]
|
||||
if c >= 'a' && c <= 'z' {
|
||||
c = c - 'a' + 'A'
|
||||
}
|
||||
result += string(c)
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func endsWith(s, suffix string) bool {
|
||||
if len(s) < len(suffix) {
|
||||
return false
|
||||
}
|
||||
return s[len(s)-len(suffix):] == suffix
|
||||
}
|
||||
|
||||
|
||||
// ========== OI Top (Open Interest Growth Top 20) Data ==========
|
||||
|
||||
// OIPosition open interest data
|
||||
type OIPosition struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Rank int `json:"rank"`
|
||||
CurrentOI float64 `json:"current_oi"`
|
||||
OIDelta float64 `json:"oi_delta"`
|
||||
OIDeltaPercent float64 `json:"oi_delta_percent"`
|
||||
OIDeltaValue float64 `json:"oi_delta_value"`
|
||||
PriceDeltaPercent float64 `json:"price_delta_percent"`
|
||||
NetLong float64 `json:"net_long"`
|
||||
NetShort float64 `json:"net_short"`
|
||||
}
|
||||
|
||||
// OITopAPIResponse data structure returned by OI Top API
|
||||
type OITopAPIResponse struct {
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
Positions []OIPosition `json:"positions"`
|
||||
Count int `json:"count"`
|
||||
Exchange string `json:"exchange"`
|
||||
TimeRange string `json:"time_range"`
|
||||
TimeRangeParam string `json:"time_range_param"`
|
||||
RankType string `json:"rank_type"`
|
||||
Limit int `json:"limit"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
var oiTopConfig = struct {
|
||||
APIURL string
|
||||
Timeout time.Duration
|
||||
}{
|
||||
APIURL: "",
|
||||
Timeout: 30 * time.Second,
|
||||
}
|
||||
|
||||
// GetOITopPositions retrieves OI Top 20 data (with retry)
|
||||
func GetOITopPositions() ([]OIPosition, error) {
|
||||
if strings.TrimSpace(oiTopConfig.APIURL) == "" {
|
||||
log.Printf("⚠️ OI Top API URL not configured, skipping OI Top data fetch")
|
||||
return []OIPosition{}, nil
|
||||
}
|
||||
|
||||
maxRetries := 3
|
||||
var lastErr error
|
||||
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 1 {
|
||||
log.Printf("⚠️ Retry attempt %d of %d to fetch OI Top data...", attempt, maxRetries)
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
positions, err := fetchOITop()
|
||||
if err == nil {
|
||||
if attempt > 1 {
|
||||
log.Printf("✓ Retry attempt %d succeeded", attempt)
|
||||
}
|
||||
return positions, nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
log.Printf("❌ OI Top request attempt %d failed: %v", attempt, err)
|
||||
}
|
||||
|
||||
log.Printf("⚠️ All OI Top API requests failed (last error: %v), skipping OI Top data", lastErr)
|
||||
return []OIPosition{}, nil
|
||||
}
|
||||
|
||||
// fetchOITop actually executes OI Top request
|
||||
func fetchOITop() ([]OIPosition, error) {
|
||||
log.Printf("🔄 Requesting OI Top data...")
|
||||
|
||||
// SSRF Protection: Validate URL before making request
|
||||
resp, err := security.SafeGet(oiTopConfig.APIURL, oiTopConfig.Timeout)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to request OI Top API: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to read OI Top response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, fmt.Errorf("OI Top API returned error (status %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var response OITopAPIResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, fmt.Errorf("OI Top JSON parsing failed: %w", err)
|
||||
}
|
||||
|
||||
if response.Code != 0 {
|
||||
return nil, fmt.Errorf("OI Top API returned error code: %d", response.Code)
|
||||
}
|
||||
|
||||
if len(response.Data.Positions) == 0 {
|
||||
return nil, fmt.Errorf("OI Top position list is empty")
|
||||
}
|
||||
|
||||
log.Printf("✓ Successfully fetched %d OI Top coins (time range: %s, type: %s)",
|
||||
len(response.Data.Positions), response.Data.TimeRange, response.Data.RankType)
|
||||
return response.Data.Positions, nil
|
||||
}
|
||||
|
||||
// GetOITopSymbols retrieves OI Top coin symbol list
|
||||
func GetOITopSymbols() ([]string, error) {
|
||||
positions, err := GetOITopPositions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var symbols []string
|
||||
for _, pos := range positions {
|
||||
symbol := normalizeSymbol(pos.Symbol)
|
||||
symbols = append(symbols, symbol)
|
||||
}
|
||||
|
||||
return symbols, nil
|
||||
}
|
||||
|
||||
// MergedData merged data (AI500 + OI Top)
|
||||
type MergedData struct {
|
||||
AI500Coins []CoinData
|
||||
OITopCoins []OIPosition
|
||||
AllSymbols []string
|
||||
SymbolSources map[string][]string
|
||||
}
|
||||
|
||||
// OIRankingData OI ranking data for debate (includes both top and low)
|
||||
type OIRankingData struct {
|
||||
TimeRange string `json:"time_range"`
|
||||
Duration string `json:"duration"`
|
||||
TopPositions []OIPosition `json:"top_positions"`
|
||||
LowPositions []OIPosition `json:"low_positions"`
|
||||
FetchedAt time.Time `json:"fetched_at"`
|
||||
}
|
||||
|
||||
// GetOIRankingData retrieves OI ranking data (both top increase and low decrease)
|
||||
func GetOIRankingData(baseURL, authKey string, duration string, limit int) (*OIRankingData, error) {
|
||||
if baseURL == "" || authKey == "" {
|
||||
return nil, fmt.Errorf("OI API URL or auth key not configured")
|
||||
}
|
||||
|
||||
if duration == "" {
|
||||
duration = "1h"
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
result := &OIRankingData{
|
||||
Duration: duration,
|
||||
FetchedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Fetch top ranking
|
||||
topURL := fmt.Sprintf("%s/api/oi/top-ranking?limit=%d&duration=%s&auth=%s", baseURL, limit, duration, authKey)
|
||||
topPositions, timeRange, err := fetchOIRanking(topURL)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Failed to fetch OI top ranking: %v", err)
|
||||
} else {
|
||||
result.TopPositions = topPositions
|
||||
result.TimeRange = timeRange
|
||||
}
|
||||
|
||||
// Fetch low ranking
|
||||
lowURL := fmt.Sprintf("%s/api/oi/low-ranking?limit=%d&duration=%s&auth=%s", baseURL, limit, duration, authKey)
|
||||
lowPositions, _, err := fetchOIRanking(lowURL)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Failed to fetch OI low ranking: %v", err)
|
||||
} else {
|
||||
result.LowPositions = lowPositions
|
||||
}
|
||||
|
||||
log.Printf("✓ Fetched OI ranking data: %d top, %d low (duration: %s)",
|
||||
len(result.TopPositions), len(result.LowPositions), duration)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// fetchOIRanking fetches OI ranking from a single endpoint
|
||||
func fetchOIRanking(url string) ([]OIPosition, string, error) {
|
||||
// SSRF Protection: Validate URL before making request
|
||||
resp, err := security.SafeGet(url, 30*time.Second)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("failed to read response: %w", err)
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return nil, "", fmt.Errorf("API returned error (status %d): %s", resp.StatusCode, string(body))
|
||||
}
|
||||
|
||||
var response OITopAPIResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, "", fmt.Errorf("JSON parsing failed: %w", err)
|
||||
}
|
||||
|
||||
if response.Code != 0 {
|
||||
return nil, "", fmt.Errorf("API returned error code: %d", response.Code)
|
||||
}
|
||||
|
||||
return response.Data.Positions, response.Data.TimeRange, nil
|
||||
}
|
||||
|
||||
// FormatOIRankingForAI formats OI ranking data for AI consumption
|
||||
func FormatOIRankingForAI(data *OIRankingData) string {
|
||||
if data == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("## 📊 市场持仓量变化数据 (Open Interest Changes in %s / %s)\n\n", data.TimeRange, data.Duration))
|
||||
|
||||
if len(data.TopPositions) > 0 {
|
||||
sb.WriteString("### 🔺 持仓量增加排行 (OI Increase Ranking)\n")
|
||||
sb.WriteString("市场资金正在流入以下币种,可能表示趋势延续或新仓位建立:\n\n")
|
||||
sb.WriteString("| 排名 | 币种 | 持仓变化值(USDT) | 变化幅度 | 价格变化 |\n")
|
||||
sb.WriteString("|------|------|------------------|----------|----------|\n")
|
||||
for _, pos := range data.TopPositions {
|
||||
sb.WriteString(fmt.Sprintf("| #%d | %s | %s | %+.2f%% | %+.2f%% |\n",
|
||||
pos.Rank,
|
||||
pos.Symbol,
|
||||
formatOIValue(pos.OIDeltaValue),
|
||||
pos.OIDeltaPercent,
|
||||
pos.PriceDeltaPercent,
|
||||
))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString("**解读**: 持仓增加 + 价格上涨 = 多头主导; 持仓增加 + 价格下跌 = 空头主导\n\n")
|
||||
}
|
||||
|
||||
if len(data.LowPositions) > 0 {
|
||||
sb.WriteString("### 🔻 持仓量减少排行 (OI Decrease Ranking)\n")
|
||||
sb.WriteString("市场资金正在流出以下币种,可能表示趋势反转或仓位平仓:\n\n")
|
||||
sb.WriteString("| 排名 | 币种 | 持仓变化值(USDT) | 变化幅度 | 价格变化 |\n")
|
||||
sb.WriteString("|------|------|------------------|----------|----------|\n")
|
||||
for _, pos := range data.LowPositions {
|
||||
sb.WriteString(fmt.Sprintf("| #%d | %s | %s | %+.2f%% | %+.2f%% |\n",
|
||||
pos.Rank,
|
||||
pos.Symbol,
|
||||
formatOIValue(pos.OIDeltaValue),
|
||||
pos.OIDeltaPercent,
|
||||
pos.PriceDeltaPercent,
|
||||
))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
sb.WriteString("**解读**: 持仓减少 + 价格上涨 = 空头平仓(反弹); 持仓减少 + 价格下跌 = 多头平仓(回调)\n\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
// formatOIValue formats OI value for display
|
||||
func formatOIValue(v float64) string {
|
||||
sign := ""
|
||||
if v >= 0 {
|
||||
sign = "+"
|
||||
}
|
||||
absV := v
|
||||
if absV < 0 {
|
||||
absV = -absV
|
||||
}
|
||||
if absV >= 1e9 {
|
||||
return fmt.Sprintf("%s%.2fB", sign, v/1e9)
|
||||
} else if absV >= 1e6 {
|
||||
return fmt.Sprintf("%s%.2fM", sign, v/1e6)
|
||||
} else if absV >= 1e3 {
|
||||
return fmt.Sprintf("%s%.2fK", sign, v/1e3)
|
||||
}
|
||||
return fmt.Sprintf("%s%.2f", sign, v)
|
||||
}
|
||||
|
||||
// GetMergedData retrieves merged data (AI500 + OI Top, deduplicated)
|
||||
func GetMergedData(ai500Limit int) (*MergedData, error) {
|
||||
ai500TopSymbols, err := GetTopRatedCoins(ai500Limit)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Failed to get AI500 data: %v", err)
|
||||
ai500TopSymbols = []string{}
|
||||
}
|
||||
|
||||
oiTopSymbols, err := GetOITopSymbols()
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Failed to get OI Top data: %v", err)
|
||||
oiTopSymbols = []string{}
|
||||
}
|
||||
|
||||
symbolSet := make(map[string]bool)
|
||||
symbolSources := make(map[string][]string)
|
||||
|
||||
for _, symbol := range ai500TopSymbols {
|
||||
symbolSet[symbol] = true
|
||||
symbolSources[symbol] = append(symbolSources[symbol], "ai500")
|
||||
}
|
||||
|
||||
for _, symbol := range oiTopSymbols {
|
||||
if !symbolSet[symbol] {
|
||||
symbolSet[symbol] = true
|
||||
}
|
||||
symbolSources[symbol] = append(symbolSources[symbol], "oi_top")
|
||||
}
|
||||
|
||||
var allSymbols []string
|
||||
for symbol := range symbolSet {
|
||||
allSymbols = append(allSymbols, symbol)
|
||||
}
|
||||
|
||||
ai500Coins, _ := GetAI500Data()
|
||||
oiTopPositions, _ := GetOITopPositions()
|
||||
|
||||
merged := &MergedData{
|
||||
AI500Coins: ai500Coins,
|
||||
OITopCoins: oiTopPositions,
|
||||
AllSymbols: allSymbols,
|
||||
SymbolSources: symbolSources,
|
||||
}
|
||||
|
||||
log.Printf("📊 Data merge complete: AI500=%d, OI_Top=%d, Total(deduplicated)=%d",
|
||||
len(ai500TopSymbols), len(oiTopSymbols), len(allSymbols))
|
||||
|
||||
return merged, nil
|
||||
}
|
||||
|
||||
// ========== Backward Compatibility Aliases ==========
|
||||
|
||||
// Deprecated: Use SetAI500API instead
|
||||
func SetCoinPoolAPI(apiURL string) {
|
||||
SetAI500API(apiURL)
|
||||
}
|
||||
|
||||
// Deprecated: Use GetAI500Data instead
|
||||
func GetCoinPool() ([]CoinData, error) {
|
||||
return GetAI500Data()
|
||||
}
|
||||
|
||||
// Deprecated: Use MergedData instead
|
||||
type MergedCoinPool = MergedData
|
||||
|
||||
// Deprecated: Use GetMergedData instead
|
||||
func GetMergedCoinPool(ai500Limit int) (*MergedData, error) {
|
||||
return GetMergedData(ai500Limit)
|
||||
}
|
||||
|
||||
// Deprecated: Use CoinData instead
|
||||
type CoinInfo = CoinData
|
||||
@@ -0,0 +1,163 @@
|
||||
package nofxos
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// CoinData represents AI500 coin information
|
||||
type CoinData struct {
|
||||
Pair string `json:"pair"` // Trading pair symbol (e.g.: BTCUSDT)
|
||||
Score float64 `json:"score"` // Current AI score (0-100)
|
||||
StartTime int64 `json:"start_time"` // Start time (Unix timestamp)
|
||||
StartPrice float64 `json:"start_price"` // Start price
|
||||
LastScore float64 `json:"last_score"` // Latest score
|
||||
MaxScore float64 `json:"max_score"` // Highest score
|
||||
MaxPrice float64 `json:"max_price"` // Highest price
|
||||
IncreasePercent float64 `json:"increase_percent"` // Increase percentage (already x100)
|
||||
IsAvailable bool `json:"-"` // Whether tradable (internal use)
|
||||
}
|
||||
|
||||
// AI500Response is the API response structure
|
||||
type AI500Response struct {
|
||||
Success bool `json:"success"`
|
||||
Data struct {
|
||||
Coins []CoinData `json:"coins"`
|
||||
Count int `json:"count"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// GetAI500List retrieves AI500 coin list with retry mechanism
|
||||
func (c *Client) GetAI500List() ([]CoinData, error) {
|
||||
maxRetries := 3
|
||||
var lastErr error
|
||||
|
||||
for attempt := 1; attempt <= maxRetries; attempt++ {
|
||||
if attempt > 1 {
|
||||
log.Printf("⚠️ Retry attempt %d of %d to fetch AI500 data...", attempt, maxRetries)
|
||||
time.Sleep(2 * time.Second)
|
||||
}
|
||||
|
||||
coins, err := c.fetchAI500()
|
||||
if err == nil {
|
||||
if attempt > 1 {
|
||||
log.Printf("✓ Retry attempt %d succeeded", attempt)
|
||||
}
|
||||
return coins, nil
|
||||
}
|
||||
|
||||
lastErr = err
|
||||
log.Printf("❌ AI500 request attempt %d failed: %v", attempt, err)
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("all AI500 API requests failed: %w", lastErr)
|
||||
}
|
||||
|
||||
func (c *Client) fetchAI500() ([]CoinData, error) {
|
||||
log.Printf("🔄 Requesting AI500 data from %s...", c.GetBaseURL())
|
||||
|
||||
body, err := c.doRequest("/api/ai500/list")
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to request AI500 API: %w", err)
|
||||
}
|
||||
|
||||
var response AI500Response
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, fmt.Errorf("JSON parsing failed: %w", err)
|
||||
}
|
||||
|
||||
if !response.Success {
|
||||
return nil, fmt.Errorf("API returned failure status")
|
||||
}
|
||||
|
||||
if len(response.Data.Coins) == 0 {
|
||||
return nil, fmt.Errorf("coin list is empty")
|
||||
}
|
||||
|
||||
// Set IsAvailable flag
|
||||
coins := response.Data.Coins
|
||||
for i := range coins {
|
||||
coins[i].IsAvailable = true
|
||||
}
|
||||
|
||||
log.Printf("✓ Successfully fetched %d AI500 coins", len(coins))
|
||||
return coins, nil
|
||||
}
|
||||
|
||||
// GetTopRatedCoins retrieves top N coins by score (sorted descending)
|
||||
func (c *Client) GetTopRatedCoins(limit int) ([]string, error) {
|
||||
coins, err := c.GetAI500List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Filter available coins
|
||||
var availableCoins []CoinData
|
||||
for _, coin := range coins {
|
||||
if coin.IsAvailable {
|
||||
availableCoins = append(availableCoins, coin)
|
||||
}
|
||||
}
|
||||
|
||||
if len(availableCoins) == 0 {
|
||||
return nil, fmt.Errorf("no available coins")
|
||||
}
|
||||
|
||||
// Sort by Score descending (bubble sort)
|
||||
for i := 0; i < len(availableCoins); i++ {
|
||||
for j := i + 1; j < len(availableCoins); j++ {
|
||||
if availableCoins[i].Score < availableCoins[j].Score {
|
||||
availableCoins[i], availableCoins[j] = availableCoins[j], availableCoins[i]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Take top N
|
||||
maxCount := limit
|
||||
if len(availableCoins) < maxCount {
|
||||
maxCount = len(availableCoins)
|
||||
}
|
||||
|
||||
var symbols []string
|
||||
for i := 0; i < maxCount; i++ {
|
||||
symbol := NormalizeSymbol(availableCoins[i].Pair)
|
||||
symbols = append(symbols, symbol)
|
||||
}
|
||||
|
||||
return symbols, nil
|
||||
}
|
||||
|
||||
// GetAvailableCoins retrieves all available coin symbols
|
||||
func (c *Client) GetAvailableCoins() ([]string, error) {
|
||||
coins, err := c.GetAI500List()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var symbols []string
|
||||
for _, coin := range coins {
|
||||
if coin.IsAvailable {
|
||||
symbol := NormalizeSymbol(coin.Pair)
|
||||
symbols = append(symbols, symbol)
|
||||
}
|
||||
}
|
||||
|
||||
if len(symbols) == 0 {
|
||||
return nil, fmt.Errorf("no available coins")
|
||||
}
|
||||
|
||||
return symbols, nil
|
||||
}
|
||||
|
||||
// NormalizeSymbol normalizes coin symbol to XXXUSDT format
|
||||
func NormalizeSymbol(symbol string) string {
|
||||
symbol = strings.TrimSpace(symbol)
|
||||
symbol = strings.ToUpper(symbol)
|
||||
if !strings.HasSuffix(symbol, "USDT") {
|
||||
symbol = symbol + "USDT"
|
||||
}
|
||||
return symbol
|
||||
}
|
||||
@@ -0,0 +1,146 @@
|
||||
// Package nofxos provides data access to the NofxOS API (https://nofxos.ai)
|
||||
// for quantitative trading data including AI500 scores, OI rankings,
|
||||
// fund flow (NetFlow), price rankings, and coin details.
|
||||
package nofxos
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"net/http"
|
||||
"nofx/security"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Default configuration
|
||||
const (
|
||||
DefaultBaseURL = "https://nofxos.ai"
|
||||
DefaultTimeout = 30 * time.Second
|
||||
DefaultAuthKey = "cm_568c67eae410d912c54c"
|
||||
)
|
||||
|
||||
// Client is the NofxOS API client
|
||||
type Client struct {
|
||||
BaseURL string
|
||||
AuthKey string
|
||||
Timeout time.Duration
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
var (
|
||||
defaultClient *Client
|
||||
clientOnce sync.Once
|
||||
)
|
||||
|
||||
// DefaultClient returns the singleton default client
|
||||
func DefaultClient() *Client {
|
||||
clientOnce.Do(func() {
|
||||
defaultClient = &Client{
|
||||
BaseURL: DefaultBaseURL,
|
||||
AuthKey: DefaultAuthKey,
|
||||
Timeout: DefaultTimeout,
|
||||
}
|
||||
})
|
||||
return defaultClient
|
||||
}
|
||||
|
||||
// NewClient creates a new NofxOS API client
|
||||
func NewClient(baseURL, authKey string) *Client {
|
||||
if baseURL == "" {
|
||||
baseURL = DefaultBaseURL
|
||||
}
|
||||
if authKey == "" {
|
||||
authKey = DefaultAuthKey
|
||||
}
|
||||
return &Client{
|
||||
BaseURL: baseURL,
|
||||
AuthKey: authKey,
|
||||
Timeout: DefaultTimeout,
|
||||
}
|
||||
}
|
||||
|
||||
// SetConfig updates client configuration
|
||||
func (c *Client) SetConfig(baseURL, authKey string) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
if baseURL != "" {
|
||||
c.BaseURL = baseURL
|
||||
}
|
||||
if authKey != "" {
|
||||
c.AuthKey = authKey
|
||||
}
|
||||
}
|
||||
|
||||
// GetBaseURL returns the current base URL
|
||||
func (c *Client) GetBaseURL() string {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.BaseURL
|
||||
}
|
||||
|
||||
// GetAuthKey returns the current auth key
|
||||
func (c *Client) GetAuthKey() string {
|
||||
c.mu.RLock()
|
||||
defer c.mu.RUnlock()
|
||||
return c.AuthKey
|
||||
}
|
||||
|
||||
// doRequest performs an HTTP GET request with authentication
|
||||
func (c *Client) doRequest(endpoint string) ([]byte, error) {
|
||||
c.mu.RLock()
|
||||
baseURL := c.BaseURL
|
||||
authKey := c.AuthKey
|
||||
timeout := c.Timeout
|
||||
c.mu.RUnlock()
|
||||
|
||||
url := baseURL + endpoint
|
||||
if !strings.Contains(url, "auth=") {
|
||||
if strings.Contains(url, "?") {
|
||||
url += "&auth=" + authKey
|
||||
} else {
|
||||
url += "?auth=" + authKey
|
||||
}
|
||||
}
|
||||
|
||||
resp, err := security.SafeGet(url, timeout)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
return body, &APIError{
|
||||
StatusCode: resp.StatusCode,
|
||||
Message: string(body),
|
||||
}
|
||||
}
|
||||
|
||||
return body, nil
|
||||
}
|
||||
|
||||
// APIError represents an API error response
|
||||
type APIError struct {
|
||||
StatusCode int
|
||||
Message string
|
||||
}
|
||||
|
||||
func (e *APIError) Error() string {
|
||||
return e.Message
|
||||
}
|
||||
|
||||
// ExtractAuthKey extracts auth key from a URL string
|
||||
func ExtractAuthKey(url string) string {
|
||||
if idx := strings.Index(url, "auth="); idx != -1 {
|
||||
authKey := url[idx+5:]
|
||||
if ampIdx := strings.Index(authKey, "&"); ampIdx != -1 {
|
||||
authKey = authKey[:ampIdx]
|
||||
}
|
||||
return authKey
|
||||
}
|
||||
return ""
|
||||
}
|
||||
@@ -0,0 +1,216 @@
|
||||
package nofxos
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// QuantData represents quantitative data for a single coin
|
||||
type QuantData struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Price float64 `json:"price"`
|
||||
Netflow *NetflowData `json:"netflow,omitempty"`
|
||||
OI map[string]*OIData `json:"oi,omitempty"` // keyed by exchange: "binance", "bybit"
|
||||
PriceChange map[string]float64 `json:"price_change,omitempty"` // keyed by duration: "1h", "4h", etc.
|
||||
}
|
||||
|
||||
// NetflowData contains fund flow data
|
||||
type NetflowData struct {
|
||||
Institution *FlowTypeData `json:"institution,omitempty"`
|
||||
Personal *FlowTypeData `json:"personal,omitempty"`
|
||||
}
|
||||
|
||||
// FlowTypeData contains flow data by trade type
|
||||
type FlowTypeData struct {
|
||||
Future map[string]float64 `json:"future,omitempty"` // keyed by duration
|
||||
Spot map[string]float64 `json:"spot,omitempty"` // keyed by duration
|
||||
}
|
||||
|
||||
// OIData contains open interest data for an exchange
|
||||
type OIData struct {
|
||||
CurrentOI float64 `json:"current_oi"`
|
||||
NetLong float64 `json:"net_long"`
|
||||
NetShort float64 `json:"net_short"`
|
||||
Delta map[string]*OIDeltaData `json:"delta,omitempty"` // keyed by duration
|
||||
}
|
||||
|
||||
// OIDeltaData contains OI change data
|
||||
type OIDeltaData struct {
|
||||
OIDelta float64 `json:"oi_delta"`
|
||||
OIDeltaValue float64 `json:"oi_delta_value"`
|
||||
OIDeltaPercent float64 `json:"oi_delta_percent"` // Already x100
|
||||
}
|
||||
|
||||
// CoinResponse is the API response structure for coin details
|
||||
type CoinResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Code int `json:"code"`
|
||||
Data *QuantData `json:"data"`
|
||||
}
|
||||
|
||||
// GetCoinData retrieves quantitative data for a single coin
|
||||
func (c *Client) GetCoinData(symbol string, include string) (*QuantData, error) {
|
||||
if symbol == "" {
|
||||
return nil, fmt.Errorf("symbol is required")
|
||||
}
|
||||
|
||||
if include == "" {
|
||||
include = "netflow,oi,price"
|
||||
}
|
||||
|
||||
// Normalize symbol (remove USDT suffix for API call if needed)
|
||||
symbol = strings.TrimSuffix(strings.ToUpper(symbol), "USDT")
|
||||
|
||||
endpoint := fmt.Sprintf("/api/coin/%s?include=%s", symbol, include)
|
||||
|
||||
body, err := c.doRequest(endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
|
||||
var response CoinResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, fmt.Errorf("JSON parsing failed: %w", err)
|
||||
}
|
||||
|
||||
// Check for success (support both success field and code field)
|
||||
if !response.Success && response.Code != 0 {
|
||||
return nil, fmt.Errorf("API returned error code: %d", response.Code)
|
||||
}
|
||||
|
||||
return response.Data, nil
|
||||
}
|
||||
|
||||
// GetCoinDataBatch retrieves quantitative data for multiple coins
|
||||
func (c *Client) GetCoinDataBatch(symbols []string, include string) map[string]*QuantData {
|
||||
result := make(map[string]*QuantData)
|
||||
|
||||
for _, symbol := range symbols {
|
||||
data, err := c.GetCoinData(symbol, include)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Failed to fetch coin data for %s: %v", symbol, err)
|
||||
continue
|
||||
}
|
||||
if data != nil {
|
||||
// Use normalized symbol as key
|
||||
normalizedSymbol := NormalizeSymbol(symbol)
|
||||
result[normalizedSymbol] = data
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// FormatQuantDataForAI formats single coin quant data for AI consumption
|
||||
func FormatQuantDataForAI(symbol string, data *QuantData, lang Language) string {
|
||||
if data == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if lang == LangChinese {
|
||||
return formatQuantDataZH(symbol, data)
|
||||
}
|
||||
return formatQuantDataEN(symbol, data)
|
||||
}
|
||||
|
||||
func formatQuantDataZH(symbol string, data *QuantData) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("### %s 量化数据\n", symbol))
|
||||
sb.WriteString(fmt.Sprintf("价格: $%.4f\n\n", data.Price))
|
||||
|
||||
if len(data.PriceChange) > 0 {
|
||||
sb.WriteString("**价格变化**:\n")
|
||||
durations := []string{"1h", "4h", "8h", "12h", "24h"}
|
||||
for _, d := range durations {
|
||||
if change, ok := data.PriceChange[d]; ok {
|
||||
sb.WriteString(fmt.Sprintf("- %s: %+.2f%%\n", d, change*100))
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
if len(data.OI) > 0 {
|
||||
for exchange, oiData := range data.OI {
|
||||
if oiData != nil {
|
||||
sb.WriteString(fmt.Sprintf("**%s持仓**:\n", strings.ToUpper(exchange)))
|
||||
sb.WriteString(fmt.Sprintf("- OI: %.2f\n", oiData.CurrentOI))
|
||||
if oiData.NetLong > 0 || oiData.NetShort > 0 {
|
||||
sb.WriteString(fmt.Sprintf("- 多头: %.2f, 空头: %.2f\n", oiData.NetLong, oiData.NetShort))
|
||||
}
|
||||
if oiData.Delta != nil {
|
||||
if delta, ok := oiData.Delta["1h"]; ok && delta != nil {
|
||||
sb.WriteString(fmt.Sprintf("- 1h变化: %s (%.2f%%)\n",
|
||||
formatValue(delta.OIDeltaValue), delta.OIDeltaPercent))
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if data.Netflow != nil && data.Netflow.Institution != nil && data.Netflow.Institution.Future != nil {
|
||||
sb.WriteString("**机构资金流**:\n")
|
||||
durations := []string{"1h", "4h", "24h"}
|
||||
for _, d := range durations {
|
||||
if flow, ok := data.Netflow.Institution.Future[d]; ok {
|
||||
sb.WriteString(fmt.Sprintf("- %s: %s\n", d, formatValue(flow)))
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func formatQuantDataEN(symbol string, data *QuantData) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("### %s Quant Data\n", symbol))
|
||||
sb.WriteString(fmt.Sprintf("Price: $%.4f\n\n", data.Price))
|
||||
|
||||
if len(data.PriceChange) > 0 {
|
||||
sb.WriteString("**Price Change**:\n")
|
||||
durations := []string{"1h", "4h", "8h", "12h", "24h"}
|
||||
for _, d := range durations {
|
||||
if change, ok := data.PriceChange[d]; ok {
|
||||
sb.WriteString(fmt.Sprintf("- %s: %+.2f%%\n", d, change*100))
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
if len(data.OI) > 0 {
|
||||
for exchange, oiData := range data.OI {
|
||||
if oiData != nil {
|
||||
sb.WriteString(fmt.Sprintf("**%s OI**:\n", strings.ToUpper(exchange)))
|
||||
sb.WriteString(fmt.Sprintf("- Current OI: %.2f\n", oiData.CurrentOI))
|
||||
if oiData.NetLong > 0 || oiData.NetShort > 0 {
|
||||
sb.WriteString(fmt.Sprintf("- Net Long: %.2f, Net Short: %.2f\n", oiData.NetLong, oiData.NetShort))
|
||||
}
|
||||
if oiData.Delta != nil {
|
||||
if delta, ok := oiData.Delta["1h"]; ok && delta != nil {
|
||||
sb.WriteString(fmt.Sprintf("- 1h Change: %s (%.2f%%)\n",
|
||||
formatValue(delta.OIDeltaValue), delta.OIDeltaPercent))
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if data.Netflow != nil && data.Netflow.Institution != nil && data.Netflow.Institution.Future != nil {
|
||||
sb.WriteString("**Institution Fund Flow**:\n")
|
||||
durations := []string{"1h", "4h", "24h"}
|
||||
for _, d := range durations {
|
||||
if flow, ok := data.Netflow.Institution.Future[d]; ok {
|
||||
sb.WriteString(fmt.Sprintf("- %s: %s\n", d, formatValue(flow)))
|
||||
}
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
return sb.String()
|
||||
}
|
||||
@@ -0,0 +1,263 @@
|
||||
package nofxos
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// NetFlowPosition represents fund flow data for a single coin
|
||||
type NetFlowPosition struct {
|
||||
Rank int `json:"rank"`
|
||||
Symbol string `json:"symbol"`
|
||||
Amount float64 `json:"amount"` // Fund flow amount in USDT (positive=inflow, negative=outflow)
|
||||
Price float64 `json:"price"`
|
||||
}
|
||||
|
||||
// NetFlowResponse is the API response structure
|
||||
type NetFlowResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data struct {
|
||||
Netflows []NetFlowPosition `json:"netflows"`
|
||||
Count int `json:"count"`
|
||||
Type string `json:"type"` // institution or personal
|
||||
Trade string `json:"trade"` // 合约 or 现货
|
||||
TimeRange string `json:"time_range"`
|
||||
RankType string `json:"rank_type"` // top or low
|
||||
Limit int `json:"limit"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// NetFlowRankingData contains institution and personal fund flow rankings
|
||||
type NetFlowRankingData struct {
|
||||
Duration string `json:"duration"`
|
||||
TimeRange string `json:"time_range"`
|
||||
InstitutionFutureTop []NetFlowPosition `json:"institution_future_top"`
|
||||
InstitutionFutureLow []NetFlowPosition `json:"institution_future_low"`
|
||||
PersonalFutureTop []NetFlowPosition `json:"personal_future_top"`
|
||||
PersonalFutureLow []NetFlowPosition `json:"personal_future_low"`
|
||||
FetchedAt time.Time `json:"fetched_at"`
|
||||
}
|
||||
|
||||
// GetNetFlowRanking retrieves NetFlow ranking data (institution/personal, top/low)
|
||||
func (c *Client) GetNetFlowRanking(duration string, limit int) (*NetFlowRankingData, error) {
|
||||
if duration == "" {
|
||||
duration = "1h"
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
result := &NetFlowRankingData{
|
||||
Duration: duration,
|
||||
FetchedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Fetch institution futures top (inflow)
|
||||
positions, timeRange, err := c.fetchNetFlowRanking("top", duration, limit, "institution", "future")
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Failed to fetch institution future inflow ranking: %v", err)
|
||||
} else {
|
||||
result.InstitutionFutureTop = positions
|
||||
result.TimeRange = timeRange
|
||||
}
|
||||
|
||||
// Fetch institution futures low (outflow)
|
||||
positions, _, err = c.fetchNetFlowRanking("low", duration, limit, "institution", "future")
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Failed to fetch institution future outflow ranking: %v", err)
|
||||
} else {
|
||||
result.InstitutionFutureLow = positions
|
||||
}
|
||||
|
||||
// Fetch personal futures top (retail inflow)
|
||||
positions, _, err = c.fetchNetFlowRanking("top", duration, limit, "personal", "future")
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Failed to fetch personal future inflow ranking: %v", err)
|
||||
} else {
|
||||
result.PersonalFutureTop = positions
|
||||
}
|
||||
|
||||
// Fetch personal futures low (retail outflow)
|
||||
positions, _, err = c.fetchNetFlowRanking("low", duration, limit, "personal", "future")
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Failed to fetch personal future outflow ranking: %v", err)
|
||||
} else {
|
||||
result.PersonalFutureLow = positions
|
||||
}
|
||||
|
||||
log.Printf("✓ Fetched NetFlow ranking data: inst_in=%d, inst_out=%d, retail_in=%d, retail_out=%d (duration: %s)",
|
||||
len(result.InstitutionFutureTop), len(result.InstitutionFutureLow),
|
||||
len(result.PersonalFutureTop), len(result.PersonalFutureLow), duration)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) fetchNetFlowRanking(rankType, duration string, limit int, flowType, trade string) ([]NetFlowPosition, string, error) {
|
||||
endpoint := fmt.Sprintf("/api/netflow/%s-ranking?limit=%d&duration=%s&type=%s&trade=%s",
|
||||
rankType, limit, duration, flowType, trade)
|
||||
|
||||
body, err := c.doRequest(endpoint)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
|
||||
var response NetFlowResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, "", fmt.Errorf("JSON parsing failed: %w", err)
|
||||
}
|
||||
|
||||
if !response.Success {
|
||||
return nil, "", fmt.Errorf("API returned failure status")
|
||||
}
|
||||
|
||||
return response.Data.Netflows, response.Data.TimeRange, nil
|
||||
}
|
||||
|
||||
// FormatNetFlowRankingForAI formats NetFlow ranking data for AI consumption
|
||||
func FormatNetFlowRankingForAI(data *NetFlowRankingData, lang Language) string {
|
||||
if data == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if lang == LangChinese {
|
||||
return formatNetFlowRankingZH(data)
|
||||
}
|
||||
return formatNetFlowRankingEN(data)
|
||||
}
|
||||
|
||||
func formatNetFlowRankingZH(data *NetFlowRankingData) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("## 资金流向排行 (%s)\n\n", data.Duration))
|
||||
|
||||
// Institution inflow
|
||||
if len(data.InstitutionFutureTop) > 0 {
|
||||
sb.WriteString("### 机构资金流入榜\n")
|
||||
sb.WriteString("Smart Money买入信号:\n\n")
|
||||
sb.WriteString("| 排名 | 币种 | 流入金额(USDT) | 价格 |\n")
|
||||
sb.WriteString("|------|------|----------------|------|\n")
|
||||
for _, pos := range data.InstitutionFutureTop {
|
||||
sb.WriteString(fmt.Sprintf("| %d | %s | %s | $%.4f |\n",
|
||||
pos.Rank, pos.Symbol, formatValue(pos.Amount), pos.Price))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// Institution outflow
|
||||
if len(data.InstitutionFutureLow) > 0 {
|
||||
sb.WriteString("### 机构资金流出榜\n")
|
||||
sb.WriteString("Smart Money卖出信号:\n\n")
|
||||
sb.WriteString("| 排名 | 币种 | 流出金额(USDT) | 价格 |\n")
|
||||
sb.WriteString("|------|------|----------------|------|\n")
|
||||
for _, pos := range data.InstitutionFutureLow {
|
||||
sb.WriteString(fmt.Sprintf("| %d | %s | %s | $%.4f |\n",
|
||||
pos.Rank, pos.Symbol, formatValue(pos.Amount), pos.Price))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// Retail flow summary
|
||||
if len(data.PersonalFutureTop) > 0 || len(data.PersonalFutureLow) > 0 {
|
||||
sb.WriteString("### 散户资金动向\n")
|
||||
if len(data.PersonalFutureTop) > 0 {
|
||||
sb.WriteString("散户买入: ")
|
||||
for i, pos := range data.PersonalFutureTop {
|
||||
if i >= 3 {
|
||||
break
|
||||
}
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s(%s)", pos.Symbol, formatValue(pos.Amount)))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
if len(data.PersonalFutureLow) > 0 {
|
||||
sb.WriteString("散户卖出: ")
|
||||
for i, pos := range data.PersonalFutureLow {
|
||||
if i >= 3 {
|
||||
break
|
||||
}
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s(%s)", pos.Symbol, formatValue(pos.Amount)))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
sb.WriteString("**解读**: 机构买入+散户卖出=强烈看多 | 机构卖出+散户买入=强烈看空\n\n")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func formatNetFlowRankingEN(data *NetFlowRankingData) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("## Fund Flow Ranking (%s)\n\n", data.Duration))
|
||||
|
||||
// Institution inflow
|
||||
if len(data.InstitutionFutureTop) > 0 {
|
||||
sb.WriteString("### Institution Inflow\n")
|
||||
sb.WriteString("Smart Money buying signals:\n\n")
|
||||
sb.WriteString("| Rank | Symbol | Inflow (USDT) | Price |\n")
|
||||
sb.WriteString("|------|--------|---------------|-------|\n")
|
||||
for _, pos := range data.InstitutionFutureTop {
|
||||
sb.WriteString(fmt.Sprintf("| %d | %s | %s | $%.4f |\n",
|
||||
pos.Rank, pos.Symbol, formatValue(pos.Amount), pos.Price))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// Institution outflow
|
||||
if len(data.InstitutionFutureLow) > 0 {
|
||||
sb.WriteString("### Institution Outflow\n")
|
||||
sb.WriteString("Smart Money selling signals:\n\n")
|
||||
sb.WriteString("| Rank | Symbol | Outflow (USDT) | Price |\n")
|
||||
sb.WriteString("|------|--------|----------------|-------|\n")
|
||||
for _, pos := range data.InstitutionFutureLow {
|
||||
sb.WriteString(fmt.Sprintf("| %d | %s | %s | $%.4f |\n",
|
||||
pos.Rank, pos.Symbol, formatValue(pos.Amount), pos.Price))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
// Retail flow summary
|
||||
if len(data.PersonalFutureTop) > 0 || len(data.PersonalFutureLow) > 0 {
|
||||
sb.WriteString("### Retail Flow\n")
|
||||
if len(data.PersonalFutureTop) > 0 {
|
||||
sb.WriteString("Retail buying: ")
|
||||
for i, pos := range data.PersonalFutureTop {
|
||||
if i >= 3 {
|
||||
break
|
||||
}
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s(%s)", pos.Symbol, formatValue(pos.Amount)))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
if len(data.PersonalFutureLow) > 0 {
|
||||
sb.WriteString("Retail selling: ")
|
||||
for i, pos := range data.PersonalFutureLow {
|
||||
if i >= 3 {
|
||||
break
|
||||
}
|
||||
if i > 0 {
|
||||
sb.WriteString(", ")
|
||||
}
|
||||
sb.WriteString(fmt.Sprintf("%s(%s)", pos.Symbol, formatValue(pos.Amount)))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
sb.WriteString("**Key**: Institution buy + Retail sell = Strong bullish | Institution sell + Retail buy = Strong bearish\n\n")
|
||||
return sb.String()
|
||||
}
|
||||
@@ -0,0 +1,212 @@
|
||||
package nofxos
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// OIPosition represents open interest data for a single coin
|
||||
type OIPosition struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Rank int `json:"rank"`
|
||||
Price float64 `json:"price"`
|
||||
CurrentOI float64 `json:"current_oi"`
|
||||
OIDelta float64 `json:"oi_delta"`
|
||||
OIDeltaPercent float64 `json:"oi_delta_percent"` // Already x100 (5.0 = 5%)
|
||||
OIDeltaValue float64 `json:"oi_delta_value"` // USDT value
|
||||
PriceDeltaPercent float64 `json:"price_delta_percent"` // Already x100 (5.0 = 5%)
|
||||
NetLong float64 `json:"net_long"`
|
||||
NetShort float64 `json:"net_short"`
|
||||
}
|
||||
|
||||
// OIRankingResponse is the API response structure for OI ranking
|
||||
type OIRankingResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Code int `json:"code"`
|
||||
Data struct {
|
||||
Positions []OIPosition `json:"positions"`
|
||||
Count int `json:"count"`
|
||||
Exchange string `json:"exchange"`
|
||||
TimeRange string `json:"time_range"`
|
||||
TimeRangeParam string `json:"time_range_param"`
|
||||
RankType string `json:"rank_type"`
|
||||
Limit int `json:"limit"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// OIRankingData contains both top and low OI rankings
|
||||
type OIRankingData struct {
|
||||
TimeRange string `json:"time_range"`
|
||||
Duration string `json:"duration"`
|
||||
TopPositions []OIPosition `json:"top_positions"`
|
||||
LowPositions []OIPosition `json:"low_positions"`
|
||||
FetchedAt time.Time `json:"fetched_at"`
|
||||
}
|
||||
|
||||
// GetOIRanking retrieves OI ranking data (both top increase and low decrease)
|
||||
func (c *Client) GetOIRanking(duration string, limit int) (*OIRankingData, error) {
|
||||
if duration == "" {
|
||||
duration = "1h"
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
}
|
||||
|
||||
result := &OIRankingData{
|
||||
Duration: duration,
|
||||
FetchedAt: time.Now(),
|
||||
}
|
||||
|
||||
// Fetch top ranking (OI increase)
|
||||
topPositions, timeRange, err := c.fetchOIRanking("top", duration, limit)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Failed to fetch OI top ranking: %v", err)
|
||||
} else {
|
||||
result.TopPositions = topPositions
|
||||
result.TimeRange = timeRange
|
||||
}
|
||||
|
||||
// Fetch low ranking (OI decrease)
|
||||
lowPositions, _, err := c.fetchOIRanking("low", duration, limit)
|
||||
if err != nil {
|
||||
log.Printf("⚠️ Failed to fetch OI low ranking: %v", err)
|
||||
} else {
|
||||
result.LowPositions = lowPositions
|
||||
}
|
||||
|
||||
log.Printf("✓ Fetched OI ranking data: %d top, %d low (duration: %s)",
|
||||
len(result.TopPositions), len(result.LowPositions), duration)
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (c *Client) fetchOIRanking(rankType, duration string, limit int) ([]OIPosition, string, error) {
|
||||
endpoint := fmt.Sprintf("/api/oi/%s-ranking?limit=%d&duration=%s", rankType, limit, duration)
|
||||
|
||||
body, err := c.doRequest(endpoint)
|
||||
if err != nil {
|
||||
return nil, "", fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
|
||||
var response OIRankingResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, "", fmt.Errorf("JSON parsing failed: %w", err)
|
||||
}
|
||||
|
||||
// Check for success (support both success field and code field)
|
||||
if !response.Success && response.Code != 0 {
|
||||
return nil, "", fmt.Errorf("API returned error code: %d", response.Code)
|
||||
}
|
||||
|
||||
return response.Data.Positions, response.Data.TimeRange, nil
|
||||
}
|
||||
|
||||
// GetOITopPositions retrieves top OI increase positions (legacy compatibility)
|
||||
func (c *Client) GetOITopPositions() ([]OIPosition, error) {
|
||||
data, err := c.GetOIRanking("1h", 20)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data.TopPositions, nil
|
||||
}
|
||||
|
||||
// GetOITopSymbols retrieves OI top coin symbol list
|
||||
func (c *Client) GetOITopSymbols() ([]string, error) {
|
||||
positions, err := c.GetOITopPositions()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var symbols []string
|
||||
for _, pos := range positions {
|
||||
symbol := NormalizeSymbol(pos.Symbol)
|
||||
symbols = append(symbols, symbol)
|
||||
}
|
||||
|
||||
return symbols, nil
|
||||
}
|
||||
|
||||
// FormatOIRankingForAI formats OI ranking data for AI consumption
|
||||
func FormatOIRankingForAI(data *OIRankingData, lang Language) string {
|
||||
if data == nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
if lang == LangChinese {
|
||||
return formatOIRankingZH(data)
|
||||
}
|
||||
return formatOIRankingEN(data)
|
||||
}
|
||||
|
||||
func formatOIRankingZH(data *OIRankingData) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("## 持仓量变化排行 (%s)\n\n", data.Duration))
|
||||
|
||||
if len(data.TopPositions) > 0 {
|
||||
sb.WriteString("### 持仓增加榜\n")
|
||||
sb.WriteString("资金流入,趋势延续或新仓建立信号:\n\n")
|
||||
sb.WriteString("| 排名 | 币种 | 持仓变化(USDT) | OI变化% | 价格变化% |\n")
|
||||
sb.WriteString("|------|------|----------------|---------|----------|\n")
|
||||
for _, pos := range data.TopPositions {
|
||||
sb.WriteString(fmt.Sprintf("| %d | %s | %s | %+.2f%% | %+.2f%% |\n",
|
||||
pos.Rank, pos.Symbol, formatValue(pos.OIDeltaValue),
|
||||
pos.OIDeltaPercent, pos.PriceDeltaPercent))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
if len(data.LowPositions) > 0 {
|
||||
sb.WriteString("### 持仓减少榜\n")
|
||||
sb.WriteString("资金流出,趋势反转或仓位平仓信号:\n\n")
|
||||
sb.WriteString("| 排名 | 币种 | 持仓变化(USDT) | OI变化% | 价格变化% |\n")
|
||||
sb.WriteString("|------|------|----------------|---------|----------|\n")
|
||||
for _, pos := range data.LowPositions {
|
||||
sb.WriteString(fmt.Sprintf("| %d | %s | %s | %+.2f%% | %+.2f%% |\n",
|
||||
pos.Rank, pos.Symbol, formatValue(pos.OIDeltaValue),
|
||||
pos.OIDeltaPercent, pos.PriceDeltaPercent))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
sb.WriteString("**解读**: OI增+价涨=多头主导 | OI增+价跌=空头主导 | OI减+价涨=空头平仓 | OI减+价跌=多头平仓\n\n")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func formatOIRankingEN(data *OIRankingData) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString(fmt.Sprintf("## Open Interest Changes (%s)\n\n", data.Duration))
|
||||
|
||||
if len(data.TopPositions) > 0 {
|
||||
sb.WriteString("### OI Increase Ranking\n")
|
||||
sb.WriteString("Capital inflow signals - trend continuation or new positions:\n\n")
|
||||
sb.WriteString("| Rank | Symbol | OI Change (USDT) | OI Change % | Price Change % |\n")
|
||||
sb.WriteString("|------|--------|------------------|-------------|----------------|\n")
|
||||
for _, pos := range data.TopPositions {
|
||||
sb.WriteString(fmt.Sprintf("| %d | %s | %s | %+.2f%% | %+.2f%% |\n",
|
||||
pos.Rank, pos.Symbol, formatValue(pos.OIDeltaValue),
|
||||
pos.OIDeltaPercent, pos.PriceDeltaPercent))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
if len(data.LowPositions) > 0 {
|
||||
sb.WriteString("### OI Decrease Ranking\n")
|
||||
sb.WriteString("Capital outflow signals - trend reversal or position closing:\n\n")
|
||||
sb.WriteString("| Rank | Symbol | OI Change (USDT) | OI Change % | Price Change % |\n")
|
||||
sb.WriteString("|------|--------|------------------|-------------|----------------|\n")
|
||||
for _, pos := range data.LowPositions {
|
||||
sb.WriteString(fmt.Sprintf("| %d | %s | %s | %+.2f%% | %+.2f%% |\n",
|
||||
pos.Rank, pos.Symbol, formatValue(pos.OIDeltaValue),
|
||||
pos.OIDeltaPercent, pos.PriceDeltaPercent))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
sb.WriteString("**Key**: OI up + Price up = Bulls dominant | OI up + Price down = Bears dominant | OI down + Price up = Short covering | OI down + Price down = Long liquidation\n\n")
|
||||
return sb.String()
|
||||
}
|
||||
@@ -0,0 +1,182 @@
|
||||
package nofxos
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"log"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// PriceRankingItem represents single coin price ranking data
|
||||
type PriceRankingItem struct {
|
||||
Pair string `json:"pair"`
|
||||
Symbol string `json:"symbol"`
|
||||
PriceDelta float64 `json:"price_delta"` // Decimal format: 0.0723 = 7.23%
|
||||
Price float64 `json:"price"`
|
||||
FutureFlow float64 `json:"future_flow"`
|
||||
SpotFlow float64 `json:"spot_flow"`
|
||||
OI float64 `json:"oi"`
|
||||
OIDelta float64 `json:"oi_delta"`
|
||||
OIDeltaValue float64 `json:"oi_delta_value"`
|
||||
}
|
||||
|
||||
// PriceRankingDuration contains top gainers and losers for a single duration
|
||||
type PriceRankingDuration struct {
|
||||
Top []PriceRankingItem `json:"top"`
|
||||
Low []PriceRankingItem `json:"low"`
|
||||
}
|
||||
|
||||
// PriceRankingResponse is the API response structure
|
||||
type PriceRankingResponse struct {
|
||||
Success bool `json:"success"`
|
||||
Data struct {
|
||||
Durations []string `json:"durations"`
|
||||
Limit int `json:"limit"`
|
||||
Data map[string]PriceRankingDuration `json:"data"`
|
||||
} `json:"data"`
|
||||
}
|
||||
|
||||
// PriceRankingData contains price ranking data for multiple durations
|
||||
type PriceRankingData struct {
|
||||
Durations map[string]*PriceRankingDuration `json:"durations"`
|
||||
FetchedAt time.Time `json:"fetched_at"`
|
||||
}
|
||||
|
||||
// GetPriceRanking retrieves price ranking data (gainers/losers)
|
||||
func (c *Client) GetPriceRanking(durations string, limit int) (*PriceRankingData, error) {
|
||||
if durations == "" {
|
||||
durations = "1h"
|
||||
}
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
endpoint := fmt.Sprintf("/api/price/ranking?duration=%s&limit=%d", durations, limit)
|
||||
|
||||
body, err := c.doRequest(endpoint)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("request failed: %w", err)
|
||||
}
|
||||
|
||||
var response PriceRankingResponse
|
||||
if err := json.Unmarshal(body, &response); err != nil {
|
||||
return nil, fmt.Errorf("JSON parsing failed: %w", err)
|
||||
}
|
||||
|
||||
if !response.Success {
|
||||
return nil, fmt.Errorf("API returned failure status")
|
||||
}
|
||||
|
||||
result := &PriceRankingData{
|
||||
Durations: make(map[string]*PriceRankingDuration),
|
||||
FetchedAt: time.Now(),
|
||||
}
|
||||
|
||||
for duration, data := range response.Data.Data {
|
||||
d := data // Create a copy to avoid pointer issues
|
||||
result.Durations[duration] = &d
|
||||
}
|
||||
|
||||
log.Printf("✓ Fetched Price ranking data for %d durations", len(result.Durations))
|
||||
|
||||
return result, nil
|
||||
}
|
||||
|
||||
// FormatPriceRankingForAI formats Price ranking data for AI consumption
|
||||
func FormatPriceRankingForAI(data *PriceRankingData, lang Language) string {
|
||||
if data == nil || len(data.Durations) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
if lang == LangChinese {
|
||||
return formatPriceRankingZH(data)
|
||||
}
|
||||
return formatPriceRankingEN(data)
|
||||
}
|
||||
|
||||
func formatPriceRankingZH(data *PriceRankingData) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString("## 涨跌幅排行\n\n")
|
||||
|
||||
durationOrder := []string{"1h", "4h", "24h"}
|
||||
for _, duration := range durationOrder {
|
||||
durationData, exists := data.Durations[duration]
|
||||
if !exists || durationData == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("### %s 涨跌幅\n\n", duration))
|
||||
|
||||
if len(durationData.Top) > 0 {
|
||||
sb.WriteString("**涨幅榜**\n")
|
||||
sb.WriteString("| 币种 | 涨幅 | 价格 | 资金流 | OI变化 |\n")
|
||||
sb.WriteString("|------|------|------|--------|--------|\n")
|
||||
for _, item := range durationData.Top {
|
||||
sb.WriteString(fmt.Sprintf("| %s | %+.2f%% | $%.4f | %s | %s |\n",
|
||||
item.Symbol, item.PriceDelta*100, item.Price,
|
||||
formatValue(item.FutureFlow), formatValue(item.OIDeltaValue)))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
if len(durationData.Low) > 0 {
|
||||
sb.WriteString("**跌幅榜**\n")
|
||||
sb.WriteString("| 币种 | 跌幅 | 价格 | 资金流 | OI变化 |\n")
|
||||
sb.WriteString("|------|------|------|--------|--------|\n")
|
||||
for _, item := range durationData.Low {
|
||||
sb.WriteString(fmt.Sprintf("| %s | %.2f%% | $%.4f | %s | %s |\n",
|
||||
item.Symbol, item.PriceDelta*100, item.Price,
|
||||
formatValue(item.FutureFlow), formatValue(item.OIDeltaValue)))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("**解读**: 涨幅大+资金流入+OI增加=强势上涨 | 跌幅大+资金流出+OI减少=弱势下跌\n\n")
|
||||
return sb.String()
|
||||
}
|
||||
|
||||
func formatPriceRankingEN(data *PriceRankingData) string {
|
||||
var sb strings.Builder
|
||||
|
||||
sb.WriteString("## Price Gainers/Losers\n\n")
|
||||
|
||||
durationOrder := []string{"1h", "4h", "24h"}
|
||||
for _, duration := range durationOrder {
|
||||
durationData, exists := data.Durations[duration]
|
||||
if !exists || durationData == nil {
|
||||
continue
|
||||
}
|
||||
|
||||
sb.WriteString(fmt.Sprintf("### %s Price Change\n\n", duration))
|
||||
|
||||
if len(durationData.Top) > 0 {
|
||||
sb.WriteString("**Top Gainers**\n")
|
||||
sb.WriteString("| Symbol | Change | Price | Fund Flow | OI Change |\n")
|
||||
sb.WriteString("|--------|--------|-------|-----------|----------|\n")
|
||||
for _, item := range durationData.Top {
|
||||
sb.WriteString(fmt.Sprintf("| %s | %+.2f%% | $%.4f | %s | %s |\n",
|
||||
item.Symbol, item.PriceDelta*100, item.Price,
|
||||
formatValue(item.FutureFlow), formatValue(item.OIDeltaValue)))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
|
||||
if len(durationData.Low) > 0 {
|
||||
sb.WriteString("**Top Losers**\n")
|
||||
sb.WriteString("| Symbol | Change | Price | Fund Flow | OI Change |\n")
|
||||
sb.WriteString("|--------|--------|-------|-----------|----------|\n")
|
||||
for _, item := range durationData.Low {
|
||||
sb.WriteString(fmt.Sprintf("| %s | %.2f%% | $%.4f | %s | %s |\n",
|
||||
item.Symbol, item.PriceDelta*100, item.Price,
|
||||
formatValue(item.FutureFlow), formatValue(item.OIDeltaValue)))
|
||||
}
|
||||
sb.WriteString("\n")
|
||||
}
|
||||
}
|
||||
|
||||
sb.WriteString("**Key**: Big gain + Fund inflow + OI increase = Strong bullish | Big loss + Fund outflow + OI decrease = Strong bearish\n\n")
|
||||
return sb.String()
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
package nofxos
|
||||
|
||||
import "fmt"
|
||||
|
||||
// Language represents the language for formatting output
|
||||
type Language string
|
||||
|
||||
const (
|
||||
LangChinese Language = "zh-CN"
|
||||
LangEnglish Language = "en-US"
|
||||
)
|
||||
|
||||
// formatValue formats a numeric value with sign and appropriate suffix
|
||||
func formatValue(v float64) string {
|
||||
sign := "+"
|
||||
if v < 0 {
|
||||
sign = ""
|
||||
}
|
||||
absV := v
|
||||
if absV < 0 {
|
||||
absV = -absV
|
||||
}
|
||||
if absV >= 1e9 {
|
||||
return fmt.Sprintf("%s%.2fB", sign, v/1e9)
|
||||
} else if absV >= 1e6 {
|
||||
return fmt.Sprintf("%s%.2fM", sign, v/1e6)
|
||||
} else if absV >= 1e3 {
|
||||
return fmt.Sprintf("%s%.2fK", sign, v/1e3)
|
||||
}
|
||||
return fmt.Sprintf("%s%.2f", sign, v)
|
||||
}
|
||||
+50
-22
@@ -32,6 +32,9 @@ func (Strategy) TableName() string { return "strategies" }
|
||||
|
||||
// StrategyConfig strategy configuration details (JSON structure)
|
||||
type StrategyConfig struct {
|
||||
// language setting: "zh" for Chinese, "en" for English
|
||||
// This determines the language used for data formatting and prompt generation
|
||||
Language string `json:"language,omitempty"`
|
||||
// coin source configuration
|
||||
CoinSource CoinSourceConfig `json:"coin_source"`
|
||||
// quantitative data configuration
|
||||
@@ -58,24 +61,21 @@ type PromptSectionsConfig struct {
|
||||
|
||||
// CoinSourceConfig coin source configuration
|
||||
type CoinSourceConfig struct {
|
||||
// source type: "static" | "coinpool" | "oi_top" | "mixed"
|
||||
// source type: "static" | "ai500" | "oi_top" | "mixed"
|
||||
SourceType string `json:"source_type"`
|
||||
// static coin list (used when source_type = "static")
|
||||
StaticCoins []string `json:"static_coins,omitempty"`
|
||||
// excluded coins list (filtered out from all sources)
|
||||
ExcludedCoins []string `json:"excluded_coins,omitempty"`
|
||||
// whether to use AI500 coin pool
|
||||
UseCoinPool bool `json:"use_coin_pool"`
|
||||
UseAI500 bool `json:"use_ai500"`
|
||||
// AI500 coin pool maximum count
|
||||
CoinPoolLimit int `json:"coin_pool_limit,omitempty"`
|
||||
// AI500 coin pool API URL (strategy-level configuration)
|
||||
CoinPoolAPIURL string `json:"coin_pool_api_url,omitempty"`
|
||||
AI500Limit int `json:"ai500_limit,omitempty"`
|
||||
// whether to use OI Top
|
||||
UseOITop bool `json:"use_oi_top"`
|
||||
// OI Top maximum count
|
||||
OITopLimit int `json:"oi_top_limit,omitempty"`
|
||||
// OI Top API URL (strategy-level configuration)
|
||||
OITopAPIURL string `json:"oi_top_api_url,omitempty"`
|
||||
// Note: API URLs are now built automatically using NofxOSAPIKey from IndicatorConfig
|
||||
}
|
||||
|
||||
// IndicatorConfig indicator configuration
|
||||
@@ -103,16 +103,30 @@ type IndicatorConfig struct {
|
||||
BOLLPeriods []int `json:"boll_periods,omitempty"` // default [20] - can select multiple timeframes
|
||||
// external data sources
|
||||
ExternalDataSources []ExternalDataSource `json:"external_data_sources,omitempty"`
|
||||
|
||||
// ========== NofxOS Unified API Configuration ==========
|
||||
// Unified API Key for all NofxOS data sources
|
||||
NofxOSAPIKey string `json:"nofxos_api_key,omitempty"`
|
||||
|
||||
// quantitative data sources (capital flow, position changes, price changes)
|
||||
EnableQuantData bool `json:"enable_quant_data"` // whether to enable quantitative data
|
||||
QuantDataAPIURL string `json:"quant_data_api_url,omitempty"` // quantitative data API address
|
||||
EnableQuantOI bool `json:"enable_quant_oi"` // whether to show OI data
|
||||
EnableQuantNetflow bool `json:"enable_quant_netflow"` // whether to show Netflow data
|
||||
EnableQuantData bool `json:"enable_quant_data"` // whether to enable quantitative data
|
||||
EnableQuantOI bool `json:"enable_quant_oi"` // whether to show OI data
|
||||
EnableQuantNetflow bool `json:"enable_quant_netflow"` // whether to show Netflow data
|
||||
|
||||
// OI ranking data (market-wide open interest increase/decrease rankings)
|
||||
EnableOIRanking bool `json:"enable_oi_ranking"` // whether to enable OI ranking data
|
||||
OIRankingAPIURL string `json:"oi_ranking_api_url,omitempty"` // OI ranking API base URL
|
||||
OIRankingDuration string `json:"oi_ranking_duration,omitempty"` // duration: 1h, 4h, 24h
|
||||
OIRankingLimit int `json:"oi_ranking_limit,omitempty"` // number of entries (default 10)
|
||||
|
||||
// NetFlow ranking data (market-wide fund flow rankings - institution/personal)
|
||||
EnableNetFlowRanking bool `json:"enable_netflow_ranking"` // whether to enable NetFlow ranking data
|
||||
NetFlowRankingDuration string `json:"netflow_ranking_duration,omitempty"` // duration: 1h, 4h, 24h
|
||||
NetFlowRankingLimit int `json:"netflow_ranking_limit,omitempty"` // number of entries (default 10)
|
||||
|
||||
// Price ranking data (market-wide gainers/losers)
|
||||
EnablePriceRanking bool `json:"enable_price_ranking"` // whether to enable price ranking data
|
||||
PriceRankingDuration string `json:"price_ranking_duration,omitempty"` // durations: "1h" or "1h,4h,24h"
|
||||
PriceRankingLimit int `json:"price_ranking_limit,omitempty"` // number of entries per ranking (default 10)
|
||||
}
|
||||
|
||||
// KlineConfig K-line configuration
|
||||
@@ -185,15 +199,20 @@ func (s *StrategyStore) initDefaultData() error {
|
||||
|
||||
// GetDefaultStrategyConfig returns the default strategy configuration for the given language
|
||||
func GetDefaultStrategyConfig(lang string) StrategyConfig {
|
||||
// Normalize language to "zh" or "en"
|
||||
normalizedLang := "en"
|
||||
if lang == "zh" {
|
||||
normalizedLang = "zh"
|
||||
}
|
||||
|
||||
config := StrategyConfig{
|
||||
Language: normalizedLang,
|
||||
CoinSource: CoinSourceConfig{
|
||||
SourceType: "coinpool",
|
||||
UseCoinPool: true,
|
||||
CoinPoolLimit: 10,
|
||||
CoinPoolAPIURL: "http://nofxaios.com:30006/api/ai500/list?auth=cm_568c67eae410d912c54c",
|
||||
UseOITop: false,
|
||||
OITopLimit: 20,
|
||||
OITopAPIURL: "http://nofxaios.com:30006/api/oi/top-ranking?limit=20&duration=1h&auth=cm_568c67eae410d912c54c",
|
||||
SourceType: "ai500",
|
||||
UseAI500: true,
|
||||
AI500Limit: 10,
|
||||
UseOITop: false,
|
||||
OITopLimit: 20,
|
||||
},
|
||||
Indicators: IndicatorConfig{
|
||||
Klines: KlineConfig{
|
||||
@@ -217,15 +236,24 @@ func GetDefaultStrategyConfig(lang string) StrategyConfig {
|
||||
RSIPeriods: []int{7, 14},
|
||||
ATRPeriods: []int{14},
|
||||
BOLLPeriods: []int{20},
|
||||
// NofxOS unified API key
|
||||
NofxOSAPIKey: "cm_568c67eae410d912c54c",
|
||||
// Quant data
|
||||
EnableQuantData: true,
|
||||
QuantDataAPIURL: "http://nofxaios.com:30006/api/coin/{symbol}?include=netflow,oi,price&auth=cm_568c67eae410d912c54c",
|
||||
EnableQuantOI: true,
|
||||
EnableQuantNetflow: true,
|
||||
// OI ranking data - market-wide OI increase/decrease rankings
|
||||
// OI ranking data
|
||||
EnableOIRanking: true,
|
||||
OIRankingAPIURL: "http://nofxaios.com:30006",
|
||||
OIRankingDuration: "1h",
|
||||
OIRankingLimit: 10,
|
||||
// NetFlow ranking data
|
||||
EnableNetFlowRanking: true,
|
||||
NetFlowRankingDuration: "1h",
|
||||
NetFlowRankingLimit: 10,
|
||||
// Price ranking data
|
||||
EnablePriceRanking: true,
|
||||
PriceRankingDuration: "1h,4h,24h",
|
||||
PriceRankingLimit: 10,
|
||||
},
|
||||
RiskControl: RiskControlConfig{
|
||||
MaxPositions: 3, // Max 3 coins simultaneously (CODE ENFORCED)
|
||||
|
||||
+1
-1
@@ -37,7 +37,7 @@ type Trader struct {
|
||||
BTCETHLeverage int `gorm:"column:btc_eth_leverage;default:5" json:"btc_eth_leverage,omitempty"`
|
||||
AltcoinLeverage int `gorm:"column:altcoin_leverage;default:5" json:"altcoin_leverage,omitempty"`
|
||||
TradingSymbols string `gorm:"column:trading_symbols;default:''" json:"trading_symbols,omitempty"`
|
||||
UseCoinPool bool `gorm:"column:use_coin_pool;default:false" json:"use_coin_pool,omitempty"`
|
||||
UseAI500 bool `gorm:"column:use_coin_pool;default:false" json:"use_ai500,omitempty"`
|
||||
UseOITop bool `gorm:"column:use_oi_top;default:false" json:"use_oi_top,omitempty"`
|
||||
CustomPrompt string `gorm:"column:custom_prompt;default:''" json:"custom_prompt,omitempty"`
|
||||
OverrideBasePrompt bool `gorm:"column:override_base_prompt;default:false" json:"override_base_prompt,omitempty"`
|
||||
|
||||
+21
-1
@@ -899,7 +899,7 @@ func (at *AutoTrader) buildTradingContext() (*kernel.Context, error) {
|
||||
}
|
||||
|
||||
// 8. Get quantitative data (if enabled in strategy config)
|
||||
if strategyConfig.Indicators.EnableQuantData && strategyConfig.Indicators.QuantDataAPIURL != "" {
|
||||
if strategyConfig.Indicators.EnableQuantData {
|
||||
// Collect symbols to query (candidate coins + position coins)
|
||||
symbolsToQuery := make(map[string]bool)
|
||||
for _, coin := range candidateCoins {
|
||||
@@ -929,6 +929,26 @@ func (at *AutoTrader) buildTradingContext() (*kernel.Context, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// 10. Get NetFlow ranking data (market-wide fund flow)
|
||||
if strategyConfig.Indicators.EnableNetFlowRanking {
|
||||
logger.Infof("💰 [%s] Fetching NetFlow ranking data...", at.name)
|
||||
ctx.NetFlowRankingData = at.strategyEngine.FetchNetFlowRankingData()
|
||||
if ctx.NetFlowRankingData != nil {
|
||||
logger.Infof("💰 [%s] NetFlow ranking data ready: inst_in=%d, inst_out=%d",
|
||||
at.name, len(ctx.NetFlowRankingData.InstitutionFutureTop), len(ctx.NetFlowRankingData.InstitutionFutureLow))
|
||||
}
|
||||
}
|
||||
|
||||
// 11. Get Price ranking data (market-wide gainers/losers)
|
||||
if strategyConfig.Indicators.EnablePriceRanking {
|
||||
logger.Infof("📈 [%s] Fetching Price ranking data...", at.name)
|
||||
ctx.PriceRankingData = at.strategyEngine.FetchPriceRankingData()
|
||||
if ctx.PriceRankingData != nil {
|
||||
logger.Infof("📈 [%s] Price ranking data ready for %d durations",
|
||||
at.name, len(ctx.PriceRankingData.Durations))
|
||||
}
|
||||
}
|
||||
|
||||
return ctx, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -832,17 +832,17 @@ export function BacktestPage() {
|
||||
if (!coinSource) return false
|
||||
|
||||
// Check explicit source_type
|
||||
if (coinSource.source_type === 'coinpool' || coinSource.source_type === 'oi_top') {
|
||||
if (coinSource.source_type === 'ai500' || coinSource.source_type === 'oi_top') {
|
||||
return true
|
||||
}
|
||||
if (coinSource.source_type === 'mixed' && (coinSource.use_coin_pool || coinSource.use_oi_top)) {
|
||||
if (coinSource.source_type === 'mixed' && (coinSource.use_ai500 || coinSource.use_oi_top)) {
|
||||
return true
|
||||
}
|
||||
|
||||
// Also check flags for backward compatibility (when source_type is empty or not set)
|
||||
const srcType = coinSource.source_type as string
|
||||
if (!srcType) {
|
||||
if (coinSource.use_coin_pool || coinSource.use_oi_top) {
|
||||
if (coinSource.use_ai500 || coinSource.use_oi_top) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
@@ -858,10 +858,10 @@ export function BacktestPage() {
|
||||
// Infer source_type from flags if empty (backward compatibility)
|
||||
let sourceType = cs.source_type as string
|
||||
if (!sourceType) {
|
||||
if (cs.use_coin_pool && cs.use_oi_top) {
|
||||
if (cs.use_ai500 && cs.use_oi_top) {
|
||||
sourceType = 'mixed'
|
||||
} else if (cs.use_coin_pool) {
|
||||
sourceType = 'coinpool'
|
||||
} else if (cs.use_ai500) {
|
||||
sourceType = 'ai500'
|
||||
} else if (cs.use_oi_top) {
|
||||
sourceType = 'oi_top'
|
||||
} else if (cs.static_coins?.length) {
|
||||
@@ -870,13 +870,13 @@ export function BacktestPage() {
|
||||
}
|
||||
|
||||
switch (sourceType) {
|
||||
case 'coinpool':
|
||||
return { type: 'AI500', limit: cs.coin_pool_limit || 30 }
|
||||
case 'ai500':
|
||||
return { type: 'AI500', limit: cs.ai500_limit || 30 }
|
||||
case 'oi_top':
|
||||
return { type: 'OI Top', limit: cs.oi_top_limit || 30 }
|
||||
case 'mixed':
|
||||
const sources = []
|
||||
if (cs.use_coin_pool) sources.push(`AI500(${cs.coin_pool_limit || 30})`)
|
||||
if (cs.use_ai500) sources.push(`AI500(${cs.ai500_limit || 30})`)
|
||||
if (cs.use_oi_top) sources.push(`OI Top(${cs.oi_top_limit || 30})`)
|
||||
if (cs.static_coins?.length) sources.push(`Static(${cs.static_coins.length})`)
|
||||
return { type: 'Mixed', desc: sources.join(' + ') }
|
||||
|
||||
@@ -368,7 +368,7 @@ export function TraderConfigModal({
|
||||
<div className="grid grid-cols-2 gap-2 text-xs text-[#848E9C]">
|
||||
<div>
|
||||
币种来源: {selectedStrategy.config.coin_source.source_type === 'static' ? '固定币种' :
|
||||
selectedStrategy.config.coin_source.source_type === 'coinpool' ? 'Coin Pool' :
|
||||
selectedStrategy.config.coin_source.source_type === 'ai500' ? 'AI500' :
|
||||
selectedStrategy.config.coin_source.source_type === 'oi_top' ? 'OI Top' : '混合'}
|
||||
</div>
|
||||
<div>
|
||||
|
||||
@@ -1,11 +1,7 @@
|
||||
import { useState } from 'react'
|
||||
import { Plus, X, Database, TrendingUp, List, Link, AlertCircle, Ban } from 'lucide-react'
|
||||
import { Plus, X, Database, TrendingUp, List, Ban, Zap } from 'lucide-react'
|
||||
import type { CoinSourceConfig } from '../../types'
|
||||
|
||||
// Default API URLs for data sources
|
||||
const DEFAULT_COIN_POOL_API_URL = 'http://nofxaios.com:30006/api/ai500/list?auth=cm_568c67eae410d912c54c'
|
||||
const DEFAULT_OI_TOP_API_URL = 'http://nofxaios.com:30006/api/oi/top-ranking?limit=20&duration=1h&auth=cm_568c67eae410d912c54c'
|
||||
|
||||
interface CoinSourceEditorProps {
|
||||
config: CoinSourceConfig
|
||||
onChange: (config: CoinSourceConfig) => void
|
||||
@@ -26,21 +22,17 @@ export function CoinSourceEditor({
|
||||
const translations: Record<string, Record<string, string>> = {
|
||||
sourceType: { zh: '数据来源类型', en: 'Source Type' },
|
||||
static: { zh: '静态列表', en: 'Static List' },
|
||||
coinpool: { zh: 'AI500 数据源', en: 'AI500 Data Provider' },
|
||||
ai500: { zh: 'AI500 数据源', en: 'AI500 Data Provider' },
|
||||
oi_top: { zh: 'OI Top 持仓增长', en: 'OI Top' },
|
||||
mixed: { zh: '混合模式', en: 'Mixed Mode' },
|
||||
staticCoins: { zh: '自定义币种', en: 'Custom Coins' },
|
||||
addCoin: { zh: '添加币种', en: 'Add Coin' },
|
||||
useCoinPool: { zh: '启用 AI500 数据源', en: 'Enable AI500 Data Provider' },
|
||||
coinPoolLimit: { zh: '数据源数量上限', en: 'Data Provider Limit' },
|
||||
coinPoolApiUrl: { zh: 'AI500 API URL', en: 'AI500 API URL' },
|
||||
coinPoolApiUrlPlaceholder: { zh: '输入 AI500 数据源 API 地址...', en: 'Enter AI500 data provider API URL...' },
|
||||
useAI500: { zh: '启用 AI500 数据源', en: 'Enable AI500 Data Provider' },
|
||||
ai500Limit: { zh: '数量上限', en: 'Limit' },
|
||||
useOITop: { zh: '启用 OI Top 数据', en: 'Enable OI Top' },
|
||||
oiTopLimit: { zh: 'OI Top 数量上限', en: 'OI Top Limit' },
|
||||
oiTopApiUrl: { zh: 'OI Top API URL', en: 'OI Top API URL' },
|
||||
oiTopApiUrlPlaceholder: { zh: '输入 OI Top 持仓数据 API 地址...', en: 'Enter OI Top API URL...' },
|
||||
oiTopLimit: { zh: '数量上限', en: 'Limit' },
|
||||
staticDesc: { zh: '手动指定交易币种列表', en: 'Manually specify trading coins' },
|
||||
coinpoolDesc: {
|
||||
ai500Desc: {
|
||||
zh: '使用 AI500 智能筛选的热门币种',
|
||||
en: 'Use AI500 smart-filtered popular coins',
|
||||
},
|
||||
@@ -52,19 +44,18 @@ export function CoinSourceEditor({
|
||||
zh: '组合多种数据源,AI500 + OI Top + 自定义',
|
||||
en: 'Combine multiple sources: AI500 + OI Top + Custom',
|
||||
},
|
||||
apiUrlRequired: { zh: '需要填写 API URL 才能获取数据', en: 'API URL required to fetch data' },
|
||||
dataSourceConfig: { zh: '数据源配置', en: 'Data Source Configuration' },
|
||||
fillDefault: { zh: '填入默认', en: 'Fill Default' },
|
||||
excludedCoins: { zh: '排除币种', en: 'Excluded Coins' },
|
||||
excludedCoinsDesc: { zh: '这些币种将从所有数据源中排除,不会被交易', en: 'These coins will be excluded from all sources and will not be traded' },
|
||||
addExcludedCoin: { zh: '添加排除', en: 'Add Excluded' },
|
||||
nofxosNote: { zh: '使用 NofxOS API Key(在指标配置中设置)', en: 'Uses NofxOS API Key (set in Indicators config)' },
|
||||
}
|
||||
return translations[key]?.[language] || key
|
||||
}
|
||||
|
||||
const sourceTypes = [
|
||||
{ value: 'static', icon: List, color: '#848E9C' },
|
||||
{ value: 'coinpool', icon: Database, color: '#F0B90B' },
|
||||
{ value: 'ai500', icon: Database, color: '#F0B90B' },
|
||||
{ value: 'oi_top', icon: TrendingUp, color: '#0ECB81' },
|
||||
{ value: 'mixed', icon: Database, color: '#60a5fa' },
|
||||
] as const
|
||||
@@ -149,6 +140,20 @@ export function CoinSourceEditor({
|
||||
})
|
||||
}
|
||||
|
||||
// NofxOS badge component
|
||||
const NofxOSBadge = () => (
|
||||
<span
|
||||
className="text-[9px] px-1.5 py-0.5 rounded font-medium"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.2), rgba(168, 85, 247, 0.2))',
|
||||
color: '#a855f7',
|
||||
border: '1px solid rgba(139, 92, 246, 0.3)'
|
||||
}}
|
||||
>
|
||||
NofxOS
|
||||
</span>
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="space-y-6">
|
||||
{/* Source Type Selector */}
|
||||
@@ -305,198 +310,137 @@ export function CoinSourceEditor({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Coin Pool Options */}
|
||||
{(config.source_type === 'coinpool' || config.source_type === 'mixed') && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Link className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||
{t('dataSourceConfig')} - AI500
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="flex items-center gap-3 mb-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.use_coin_pool}
|
||||
onChange={(e) =>
|
||||
!disabled && onChange({ ...config, use_coin_pool: e.target.checked })
|
||||
}
|
||||
disabled={disabled}
|
||||
className="w-5 h-5 rounded accent-yellow-500"
|
||||
/>
|
||||
<span style={{ color: '#EAECEF' }}>{t('useCoinPool')}</span>
|
||||
</label>
|
||||
{config.use_coin_pool && (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm" style={{ color: '#848E9C' }}>
|
||||
{t('coinPoolLimit')}:
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
value={config.coin_pool_limit || 10}
|
||||
onChange={(e) =>
|
||||
!disabled &&
|
||||
onChange({ ...config, coin_pool_limit: parseInt(e.target.value) || 10 })
|
||||
}
|
||||
disabled={disabled}
|
||||
min={1}
|
||||
max={100}
|
||||
className="w-20 px-3 py-1.5 rounded"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
{/* AI500 Options */}
|
||||
{(config.source_type === 'ai500' || config.source_type === 'mixed') && (
|
||||
<div
|
||||
className="p-4 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(240, 185, 11, 0.05)',
|
||||
border: '1px solid rgba(240, 185, 11, 0.2)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<Zap className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||
AI500 {t('dataSourceConfig')}
|
||||
</span>
|
||||
<NofxOSBadge />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config.use_coin_pool && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-sm" style={{ color: '#848E9C' }}>
|
||||
{t('coinPoolApiUrl')}
|
||||
</label>
|
||||
{!disabled && !config.coin_pool_api_url && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange({ ...config, coin_pool_api_url: DEFAULT_COIN_POOL_API_URL })}
|
||||
className="text-xs px-2 py-1 rounded"
|
||||
style={{ background: '#F0B90B20', color: '#F0B90B' }}
|
||||
>
|
||||
{t('fillDefault')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="url"
|
||||
value={config.coin_pool_api_url || ''}
|
||||
type="checkbox"
|
||||
checked={config.use_ai500}
|
||||
onChange={(e) =>
|
||||
!disabled && onChange({ ...config, coin_pool_api_url: e.target.value })
|
||||
!disabled && onChange({ ...config, use_ai500: e.target.checked })
|
||||
}
|
||||
disabled={disabled}
|
||||
placeholder={t('coinPoolApiUrlPlaceholder')}
|
||||
className="w-full px-4 py-2.5 rounded-lg font-mono text-sm"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
className="w-5 h-5 rounded accent-yellow-500"
|
||||
/>
|
||||
{!config.coin_pool_api_url && (
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<AlertCircle className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-xs" style={{ color: '#F0B90B' }}>
|
||||
{t('apiUrlRequired')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span style={{ color: '#EAECEF' }}>{t('useAI500')}</span>
|
||||
</label>
|
||||
|
||||
{config.use_ai500 && (
|
||||
<div className="flex items-center gap-3 pl-8">
|
||||
<span className="text-sm" style={{ color: '#848E9C' }}>
|
||||
{t('ai500Limit')}:
|
||||
</span>
|
||||
<select
|
||||
value={config.ai500_limit || 10}
|
||||
onChange={(e) =>
|
||||
!disabled &&
|
||||
onChange({ ...config, ai500_limit: parseInt(e.target.value) || 10 })
|
||||
}
|
||||
disabled={disabled}
|
||||
className="px-3 py-1.5 rounded"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
>
|
||||
{[5, 10, 15, 20, 30, 50].map(n => (
|
||||
<option key={n} value={n}>{n}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs pl-8" style={{ color: '#5E6673' }}>
|
||||
{t('nofxosNote')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OI Top Options */}
|
||||
{(config.source_type === 'oi_top' || config.source_type === 'mixed') && (
|
||||
<div className="space-y-4">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<Link className="w-4 h-4" style={{ color: '#0ECB81' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||
{t('dataSourceConfig')} - OI Top
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div className="grid grid-cols-2 gap-4">
|
||||
<div>
|
||||
<label className="flex items-center gap-3 mb-3 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.use_oi_top}
|
||||
onChange={(e) =>
|
||||
!disabled && onChange({ ...config, use_oi_top: e.target.checked })
|
||||
}
|
||||
disabled={disabled}
|
||||
className="w-5 h-5 rounded accent-yellow-500"
|
||||
/>
|
||||
<span style={{ color: '#EAECEF' }}>{t('useOITop')}</span>
|
||||
</label>
|
||||
{config.use_oi_top && (
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm" style={{ color: '#848E9C' }}>
|
||||
{t('oiTopLimit')}:
|
||||
</span>
|
||||
<input
|
||||
type="number"
|
||||
value={config.oi_top_limit || 20}
|
||||
onChange={(e) =>
|
||||
!disabled &&
|
||||
onChange({ ...config, oi_top_limit: parseInt(e.target.value) || 20 })
|
||||
}
|
||||
disabled={disabled}
|
||||
min={1}
|
||||
max={50}
|
||||
className="w-20 px-3 py-1.5 rounded"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<div
|
||||
className="p-4 rounded-lg"
|
||||
style={{
|
||||
background: 'rgba(14, 203, 129, 0.05)',
|
||||
border: '1px solid rgba(14, 203, 129, 0.2)',
|
||||
}}
|
||||
>
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<TrendingUp className="w-4 h-4" style={{ color: '#0ECB81' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||
OI Top {t('dataSourceConfig')}
|
||||
</span>
|
||||
<NofxOSBadge />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{config.use_oi_top && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-2">
|
||||
<label className="text-sm" style={{ color: '#848E9C' }}>
|
||||
{t('oiTopApiUrl')}
|
||||
</label>
|
||||
{!disabled && !config.oi_top_api_url && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange({ ...config, oi_top_api_url: DEFAULT_OI_TOP_API_URL })}
|
||||
className="text-xs px-2 py-1 rounded"
|
||||
style={{ background: '#0ECB8120', color: '#0ECB81' }}
|
||||
>
|
||||
{t('fillDefault')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<div className="space-y-3">
|
||||
<label className="flex items-center gap-3 cursor-pointer">
|
||||
<input
|
||||
type="url"
|
||||
value={config.oi_top_api_url || ''}
|
||||
type="checkbox"
|
||||
checked={config.use_oi_top}
|
||||
onChange={(e) =>
|
||||
!disabled && onChange({ ...config, oi_top_api_url: e.target.value })
|
||||
!disabled && onChange({ ...config, use_oi_top: e.target.checked })
|
||||
}
|
||||
disabled={disabled}
|
||||
placeholder={t('oiTopApiUrlPlaceholder')}
|
||||
className="w-full px-4 py-2.5 rounded-lg font-mono text-sm"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
className="w-5 h-5 rounded accent-green-500"
|
||||
/>
|
||||
{!config.oi_top_api_url && (
|
||||
<div className="flex items-center gap-2 mt-2">
|
||||
<AlertCircle className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
<span className="text-xs" style={{ color: '#F0B90B' }}>
|
||||
{t('apiUrlRequired')}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<span style={{ color: '#EAECEF' }}>{t('useOITop')}</span>
|
||||
</label>
|
||||
|
||||
{config.use_oi_top && (
|
||||
<div className="flex items-center gap-3 pl-8">
|
||||
<span className="text-sm" style={{ color: '#848E9C' }}>
|
||||
{t('oiTopLimit')}:
|
||||
</span>
|
||||
<select
|
||||
value={config.oi_top_limit || 20}
|
||||
onChange={(e) =>
|
||||
!disabled &&
|
||||
onChange({ ...config, oi_top_limit: parseInt(e.target.value) || 20 })
|
||||
}
|
||||
disabled={disabled}
|
||||
className="px-3 py-1.5 rounded"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
>
|
||||
{[5, 10, 15, 20, 30, 50].map(n => (
|
||||
<option key={n} value={n}>{n}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<p className="text-xs pl-8" style={{ color: '#5E6673' }}>
|
||||
{t('nofxosNote')}
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
import { Clock, Activity, Database, TrendingUp, BarChart2, Info, Lock, LineChart } from 'lucide-react'
|
||||
import { Clock, Activity, TrendingUp, BarChart2, Info, Lock, ExternalLink, Zap, Check, AlertCircle, Key } from 'lucide-react'
|
||||
import type { IndicatorConfig } from '../../types'
|
||||
|
||||
// Default API URL for quant data (must contain {symbol} placeholder)
|
||||
const DEFAULT_QUANT_DATA_API_URL = 'http://nofxaios.com:30006/api/coin/{symbol}?include=netflow,oi,price&auth=cm_568c67eae410d912c54c'
|
||||
// Default API base URL for OI ranking data
|
||||
const DEFAULT_OI_RANKING_API_URL = 'http://nofxaios.com:30006'
|
||||
// Default NofxOS API Key
|
||||
const DEFAULT_NOFXOS_API_KEY = 'cm_568c67eae410d912c54c'
|
||||
|
||||
interface IndicatorEditorProps {
|
||||
config: IndicatorConfig
|
||||
@@ -47,7 +45,7 @@ export function IndicatorEditor({
|
||||
marketSentiment: { zh: '市场情绪', en: 'Market Sentiment' },
|
||||
marketSentimentDesc: { zh: '持仓量、资金费率等市场情绪数据', en: 'OI, funding rate and market sentiment data' },
|
||||
quantData: { zh: '量化数据', en: 'Quant Data' },
|
||||
quantDataDesc: { zh: '第三方数据源:资金流向、大户动向', en: 'Third-party: netflow, whale movements' },
|
||||
quantDataDesc: { zh: '资金流向、大户动向', en: 'Netflow, whale movements' },
|
||||
|
||||
// Timeframes
|
||||
timeframes: { zh: '时间周期', en: 'Timeframes' },
|
||||
@@ -81,20 +79,40 @@ export function IndicatorEditor({
|
||||
fundingRate: { zh: '资金费率', en: 'Funding Rate' },
|
||||
fundingRateDesc: { zh: '永续合约资金费率', en: 'Perpetual funding rate' },
|
||||
|
||||
// Quant data
|
||||
quantDataUrl: { zh: '数据接口 URL', en: 'Data API URL' },
|
||||
fillDefault: { zh: '填入默认', en: 'Fill Default' },
|
||||
symbolPlaceholder: { zh: '{symbol} 会被替换为币种', en: '{symbol} will be replaced with coin' },
|
||||
|
||||
// OI Ranking
|
||||
oiRanking: { zh: 'OI 排行数据', en: 'OI Ranking Data' },
|
||||
oiRankingDesc: { zh: '市场持仓量增减排行,反映资金流向', en: 'Market-wide OI changes, reflects capital flow' },
|
||||
oiRankingDuration: { zh: '时间周期', en: 'Duration' },
|
||||
oiRankingLimit: { zh: '排行数量', en: 'Top N' },
|
||||
oiRanking: { zh: 'OI 排行', en: 'OI Ranking' },
|
||||
oiRankingDesc: { zh: '持仓量增减排行', en: 'OI change ranking' },
|
||||
oiRankingNote: { zh: '显示持仓量增加/减少的币种排行,帮助发现资金流向', en: 'Shows coins with OI increase/decrease, helps identify capital flow' },
|
||||
|
||||
// NetFlow Ranking
|
||||
netflowRanking: { zh: '资金流向', en: 'NetFlow' },
|
||||
netflowRankingDesc: { zh: '机构/散户资金流向', en: 'Institution/retail fund flow' },
|
||||
netflowRankingNote: { zh: '显示机构资金流入/流出排行,散户动向对比,发现聪明钱信号', en: 'Shows institution inflow/outflow ranking, retail flow comparison, Smart Money signals' },
|
||||
|
||||
// Price Ranking
|
||||
priceRanking: { zh: '涨跌幅排行', en: 'Price Ranking' },
|
||||
priceRankingDesc: { zh: '涨跌幅排行榜', en: 'Gainers/losers ranking' },
|
||||
priceRankingNote: { zh: '显示涨幅/跌幅排行,结合资金流和持仓变化分析趋势强度', en: 'Shows top gainers/losers, combined with fund flow and OI for trend analysis' },
|
||||
priceRankingMulti: { zh: '多周期', en: 'Multi-period' },
|
||||
|
||||
// Common settings
|
||||
duration: { zh: '周期', en: 'Duration' },
|
||||
limit: { zh: '数量', en: 'Limit' },
|
||||
|
||||
// Tips
|
||||
aiCanCalculate: { zh: '💡 提示:AI 可自行计算这些指标,开启可减少 AI 计算量', en: '💡 Tip: AI can calculate these, enabling reduces AI workload' },
|
||||
|
||||
// NofxOS Data Provider
|
||||
nofxosTitle: { zh: 'NofxOS 量化数据源', en: 'NofxOS Data Provider' },
|
||||
nofxosDesc: { zh: '专业加密货币量化数据服务', en: 'Professional crypto quant data service' },
|
||||
nofxosFeatures: { zh: 'AI500 · OI排行 · 资金流向 · 涨跌榜', en: 'AI500 · OI Ranking · Fund Flow · Price Ranking' },
|
||||
viewApiDocs: { zh: 'API 文档', en: 'API Docs' },
|
||||
apiKey: { zh: 'API Key', en: 'API Key' },
|
||||
apiKeyPlaceholder: { zh: '输入 NofxOS API Key', en: 'Enter NofxOS API Key' },
|
||||
fillDefault: { zh: '填入默认', en: 'Fill Default' },
|
||||
connected: { zh: '已配置', en: 'Configured' },
|
||||
notConfigured: { zh: '未配置', en: 'Not Configured' },
|
||||
nofxosDataSources: { zh: 'NofxOS 数据源', en: 'NofxOS Data Sources' },
|
||||
}
|
||||
return translations[key]?.[language] || key
|
||||
}
|
||||
@@ -166,9 +184,364 @@ export function IndicatorEditor({
|
||||
ensureRawKlines()
|
||||
}
|
||||
|
||||
// Check if any NofxOS feature is enabled
|
||||
const hasNofxosEnabled = config.enable_quant_data || config.enable_oi_ranking || config.enable_netflow_ranking || config.enable_price_ranking
|
||||
const hasApiKey = !!config.nofxos_api_key
|
||||
|
||||
return (
|
||||
<div className="space-y-5">
|
||||
{/* Section 1: Market Data (Required) */}
|
||||
{/* ============================================ */}
|
||||
{/* NofxOS Data Provider - Top Configuration */}
|
||||
{/* ============================================ */}
|
||||
<div
|
||||
className="rounded-lg overflow-hidden relative"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, rgba(99, 102, 241, 0.08) 0%, rgba(168, 85, 247, 0.08) 50%, rgba(236, 72, 153, 0.08) 100%)',
|
||||
border: '1px solid rgba(139, 92, 246, 0.3)',
|
||||
}}
|
||||
>
|
||||
{/* Decorative gradient line at top */}
|
||||
<div
|
||||
className="absolute top-0 left-0 right-0 h-[2px]"
|
||||
style={{ background: 'linear-gradient(90deg, #6366f1, #a855f7, #ec4899)' }}
|
||||
/>
|
||||
|
||||
<div className="p-4">
|
||||
{/* Header Row */}
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="flex items-center gap-2">
|
||||
<div
|
||||
className="w-8 h-8 rounded-lg flex items-center justify-center"
|
||||
style={{ background: 'linear-gradient(135deg, #6366f1, #a855f7)' }}
|
||||
>
|
||||
<Zap className="w-4 h-4 text-white" />
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-sm font-semibold" style={{ color: '#EAECEF' }}>
|
||||
{t('nofxosTitle')}
|
||||
</h3>
|
||||
<span className="text-[10px]" style={{ color: '#848E9C' }}>
|
||||
{t('nofxosFeatures')}
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Status & API Docs */}
|
||||
<div className="flex items-center gap-2">
|
||||
{hasApiKey ? (
|
||||
<span className="flex items-center gap-1 text-[10px] px-2 py-1 rounded-full" style={{ background: 'rgba(14, 203, 129, 0.15)', color: '#0ECB81' }}>
|
||||
<Check className="w-3 h-3" />
|
||||
{t('connected')}
|
||||
</span>
|
||||
) : (
|
||||
<span className="flex items-center gap-1 text-[10px] px-2 py-1 rounded-full" style={{ background: 'rgba(246, 70, 93, 0.15)', color: '#F6465D' }}>
|
||||
<AlertCircle className="w-3 h-3" />
|
||||
{t('notConfigured')}
|
||||
</span>
|
||||
)}
|
||||
<a
|
||||
href="https://nofxos.ai/api-docs"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="flex items-center gap-1 text-[10px] px-2 py-1 rounded-full transition-all hover:scale-[1.02]"
|
||||
style={{
|
||||
background: 'rgba(139, 92, 246, 0.2)',
|
||||
color: '#a855f7',
|
||||
}}
|
||||
>
|
||||
<ExternalLink className="w-3 h-3" />
|
||||
{t('viewApiDocs')}
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* API Key Input */}
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="flex-1 relative">
|
||||
<Key className="absolute left-3 top-1/2 -translate-y-1/2 w-4 h-4" style={{ color: '#848E9C' }} />
|
||||
<input
|
||||
type="text"
|
||||
value={config.nofxos_api_key || ''}
|
||||
onChange={(e) => !disabled && onChange({ ...config, nofxos_api_key: e.target.value })}
|
||||
disabled={disabled}
|
||||
placeholder={t('apiKeyPlaceholder')}
|
||||
className="w-full pl-9 pr-3 py-2 rounded-lg text-sm font-mono"
|
||||
style={{
|
||||
background: 'rgba(30, 35, 41, 0.8)',
|
||||
border: hasApiKey ? '1px solid rgba(14, 203, 129, 0.3)' : '1px solid rgba(139, 92, 246, 0.3)',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
{!disabled && !config.nofxos_api_key && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange({ ...config, nofxos_api_key: DEFAULT_NOFXOS_API_KEY })}
|
||||
className="px-3 py-2 rounded-lg text-xs font-medium transition-all hover:scale-[1.02]"
|
||||
style={{
|
||||
background: 'linear-gradient(135deg, #6366f1, #a855f7)',
|
||||
color: '#fff',
|
||||
}}
|
||||
>
|
||||
{t('fillDefault')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* NofxOS Data Sources Grid */}
|
||||
<div className="mt-4">
|
||||
<div className="text-[10px] font-medium mb-2" style={{ color: '#848E9C' }}>
|
||||
{t('nofxosDataSources')}
|
||||
</div>
|
||||
<div className="grid grid-cols-2 gap-2">
|
||||
{/* Quant Data */}
|
||||
<div
|
||||
className="p-2.5 rounded-lg transition-all cursor-pointer"
|
||||
style={{
|
||||
background: config.enable_quant_data ? 'rgba(96, 165, 250, 0.1)' : 'rgba(30, 35, 41, 0.5)',
|
||||
border: config.enable_quant_data ? '1px solid rgba(96, 165, 250, 0.3)' : '1px solid rgba(43, 49, 57, 0.5)',
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
}}
|
||||
onClick={() => !disabled && onChange({ ...config, enable_quant_data: !config.enable_quant_data })}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: '#60a5fa' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('quantData')}</span>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enable_quant_data || false}
|
||||
onChange={(e) => { e.stopPropagation(); !disabled && onChange({ ...config, enable_quant_data: e.target.checked }) }}
|
||||
disabled={disabled}
|
||||
className="w-3.5 h-3.5 rounded accent-blue-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('quantDataDesc')}</p>
|
||||
{config.enable_quant_data && (
|
||||
<div className="flex gap-3 mt-2">
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enable_quant_oi !== false}
|
||||
onChange={(e) => { e.stopPropagation(); !disabled && onChange({ ...config, enable_quant_oi: e.target.checked }) }}
|
||||
disabled={disabled}
|
||||
className="w-3 h-3 rounded accent-blue-500"
|
||||
/>
|
||||
<span className="text-[10px]" style={{ color: '#EAECEF' }}>OI</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-1.5 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enable_quant_netflow !== false}
|
||||
onChange={(e) => { e.stopPropagation(); !disabled && onChange({ ...config, enable_quant_netflow: e.target.checked }) }}
|
||||
disabled={disabled}
|
||||
className="w-3 h-3 rounded accent-blue-500"
|
||||
/>
|
||||
<span className="text-[10px]" style={{ color: '#EAECEF' }}>Netflow</span>
|
||||
</label>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* OI Ranking */}
|
||||
<div
|
||||
className="p-2.5 rounded-lg transition-all cursor-pointer"
|
||||
style={{
|
||||
background: config.enable_oi_ranking ? 'rgba(34, 197, 94, 0.1)' : 'rgba(30, 35, 41, 0.5)',
|
||||
border: config.enable_oi_ranking ? '1px solid rgba(34, 197, 94, 0.3)' : '1px solid rgba(43, 49, 57, 0.5)',
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
}}
|
||||
onClick={() => !disabled && onChange({
|
||||
...config,
|
||||
enable_oi_ranking: !config.enable_oi_ranking,
|
||||
...(!config.enable_oi_ranking && !config.oi_ranking_duration ? { oi_ranking_duration: '1h' } : {}),
|
||||
...(!config.enable_oi_ranking && !config.oi_ranking_limit ? { oi_ranking_limit: 10 } : {}),
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: '#22c55e' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('oiRanking')}</span>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enable_oi_ranking || false}
|
||||
onChange={(e) => { e.stopPropagation(); !disabled && onChange({
|
||||
...config,
|
||||
enable_oi_ranking: e.target.checked,
|
||||
...(e.target.checked && !config.oi_ranking_duration ? { oi_ranking_duration: '1h' } : {}),
|
||||
...(e.target.checked && !config.oi_ranking_limit ? { oi_ranking_limit: 10 } : {}),
|
||||
}) }}
|
||||
disabled={disabled}
|
||||
className="w-3.5 h-3.5 rounded accent-green-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('oiRankingDesc')}</p>
|
||||
{config.enable_oi_ranking && (
|
||||
<div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}>
|
||||
<select
|
||||
value={config.oi_ranking_duration || '1h'}
|
||||
onChange={(e) => !disabled && onChange({ ...config, oi_ranking_duration: e.target.value })}
|
||||
disabled={disabled}
|
||||
className="flex-1 px-2 py-1 rounded text-[10px]"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
>
|
||||
<option value="1h">1h</option>
|
||||
<option value="4h">4h</option>
|
||||
<option value="24h">24h</option>
|
||||
</select>
|
||||
<select
|
||||
value={config.oi_ranking_limit || 10}
|
||||
onChange={(e) => !disabled && onChange({ ...config, oi_ranking_limit: parseInt(e.target.value) })}
|
||||
disabled={disabled}
|
||||
className="w-14 px-2 py-1 rounded text-[10px]"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
>
|
||||
{[5, 10, 15, 20].map(n => <option key={n} value={n}>{n}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* NetFlow Ranking */}
|
||||
<div
|
||||
className="p-2.5 rounded-lg transition-all cursor-pointer"
|
||||
style={{
|
||||
background: config.enable_netflow_ranking ? 'rgba(245, 158, 11, 0.1)' : 'rgba(30, 35, 41, 0.5)',
|
||||
border: config.enable_netflow_ranking ? '1px solid rgba(245, 158, 11, 0.3)' : '1px solid rgba(43, 49, 57, 0.5)',
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
}}
|
||||
onClick={() => !disabled && onChange({
|
||||
...config,
|
||||
enable_netflow_ranking: !config.enable_netflow_ranking,
|
||||
...(!config.enable_netflow_ranking && !config.netflow_ranking_duration ? { netflow_ranking_duration: '1h' } : {}),
|
||||
...(!config.enable_netflow_ranking && !config.netflow_ranking_limit ? { netflow_ranking_limit: 10 } : {}),
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: '#f59e0b' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('netflowRanking')}</span>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enable_netflow_ranking || false}
|
||||
onChange={(e) => { e.stopPropagation(); !disabled && onChange({
|
||||
...config,
|
||||
enable_netflow_ranking: e.target.checked,
|
||||
...(e.target.checked && !config.netflow_ranking_duration ? { netflow_ranking_duration: '1h' } : {}),
|
||||
...(e.target.checked && !config.netflow_ranking_limit ? { netflow_ranking_limit: 10 } : {}),
|
||||
}) }}
|
||||
disabled={disabled}
|
||||
className="w-3.5 h-3.5 rounded accent-amber-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('netflowRankingDesc')}</p>
|
||||
{config.enable_netflow_ranking && (
|
||||
<div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}>
|
||||
<select
|
||||
value={config.netflow_ranking_duration || '1h'}
|
||||
onChange={(e) => !disabled && onChange({ ...config, netflow_ranking_duration: e.target.value })}
|
||||
disabled={disabled}
|
||||
className="flex-1 px-2 py-1 rounded text-[10px]"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
>
|
||||
<option value="1h">1h</option>
|
||||
<option value="4h">4h</option>
|
||||
<option value="24h">24h</option>
|
||||
</select>
|
||||
<select
|
||||
value={config.netflow_ranking_limit || 10}
|
||||
onChange={(e) => !disabled && onChange({ ...config, netflow_ranking_limit: parseInt(e.target.value) })}
|
||||
disabled={disabled}
|
||||
className="w-14 px-2 py-1 rounded text-[10px]"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
>
|
||||
{[5, 10, 15, 20].map(n => <option key={n} value={n}>{n}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Price Ranking */}
|
||||
<div
|
||||
className="p-2.5 rounded-lg transition-all cursor-pointer"
|
||||
style={{
|
||||
background: config.enable_price_ranking ? 'rgba(236, 72, 153, 0.1)' : 'rgba(30, 35, 41, 0.5)',
|
||||
border: config.enable_price_ranking ? '1px solid rgba(236, 72, 153, 0.3)' : '1px solid rgba(43, 49, 57, 0.5)',
|
||||
opacity: disabled ? 0.5 : 1,
|
||||
}}
|
||||
onClick={() => !disabled && onChange({
|
||||
...config,
|
||||
enable_price_ranking: !config.enable_price_ranking,
|
||||
...(!config.enable_price_ranking && !config.price_ranking_duration ? { price_ranking_duration: '1h,4h,24h' } : {}),
|
||||
...(!config.enable_price_ranking && !config.price_ranking_limit ? { price_ranking_limit: 10 } : {}),
|
||||
})}
|
||||
>
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: '#ec4899' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('priceRanking')}</span>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enable_price_ranking || false}
|
||||
onChange={(e) => { e.stopPropagation(); !disabled && onChange({
|
||||
...config,
|
||||
enable_price_ranking: e.target.checked,
|
||||
...(e.target.checked && !config.price_ranking_duration ? { price_ranking_duration: '1h,4h,24h' } : {}),
|
||||
...(e.target.checked && !config.price_ranking_limit ? { price_ranking_limit: 10 } : {}),
|
||||
}) }}
|
||||
disabled={disabled}
|
||||
className="w-3.5 h-3.5 rounded accent-pink-500"
|
||||
/>
|
||||
</div>
|
||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('priceRankingDesc')}</p>
|
||||
{config.enable_price_ranking && (
|
||||
<div className="flex gap-2 mt-2" onClick={(e) => e.stopPropagation()}>
|
||||
<select
|
||||
value={config.price_ranking_duration || '1h,4h,24h'}
|
||||
onChange={(e) => !disabled && onChange({ ...config, price_ranking_duration: e.target.value })}
|
||||
disabled={disabled}
|
||||
className="flex-1 px-2 py-1 rounded text-[10px]"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
>
|
||||
<option value="1h">1h</option>
|
||||
<option value="4h">4h</option>
|
||||
<option value="24h">24h</option>
|
||||
<option value="1h,4h,24h">{t('priceRankingMulti')}</option>
|
||||
</select>
|
||||
<select
|
||||
value={config.price_ranking_limit || 10}
|
||||
onChange={(e) => !disabled && onChange({ ...config, price_ranking_limit: parseInt(e.target.value) })}
|
||||
disabled={disabled}
|
||||
className="w-14 px-2 py-1 rounded text-[10px]"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
>
|
||||
{[5, 10, 15, 20].map(n => <option key={n} value={n}>{n}</option>)}
|
||||
</select>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Warning if features enabled but no API key */}
|
||||
{hasNofxosEnabled && !hasApiKey && (
|
||||
<div className="flex items-center gap-2 mt-3 p-2 rounded-lg" style={{ background: 'rgba(246, 70, 93, 0.1)', border: '1px solid rgba(246, 70, 93, 0.2)' }}>
|
||||
<AlertCircle className="w-4 h-4 flex-shrink-0" style={{ color: '#F6465D' }} />
|
||||
<span className="text-[10px]" style={{ color: '#F6465D' }}>
|
||||
{language === 'zh' ? '请配置 API Key 以启用 NofxOS 数据源' : 'Please configure API Key to enable NofxOS data sources'}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* ============================================ */}
|
||||
{/* Section 1: Market Data (Required) */}
|
||||
{/* ============================================ */}
|
||||
<div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||
<div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}>
|
||||
<BarChart2 className="w-4 h-4" style={{ color: '#F0B90B' }} />
|
||||
@@ -275,7 +648,9 @@ export function IndicatorEditor({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 2: Technical Indicators (Optional) */}
|
||||
{/* ============================================ */}
|
||||
{/* Section 2: Technical Indicators (Optional) */}
|
||||
{/* ============================================ */}
|
||||
<div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||
<div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}>
|
||||
<Activity className="w-4 h-4" style={{ color: '#0ECB81' }} />
|
||||
@@ -345,7 +720,9 @@ export function IndicatorEditor({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 3: Market Sentiment */}
|
||||
{/* ============================================ */}
|
||||
{/* Section 3: Market Sentiment */}
|
||||
{/* ============================================ */}
|
||||
<div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||
<div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}>
|
||||
<TrendingUp className="w-4 h-4" style={{ color: '#22c55e' }} />
|
||||
@@ -387,163 +764,6 @@ export function IndicatorEditor({
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 4: Quant Data (External API) */}
|
||||
<div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||
<div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}>
|
||||
<Database className="w-4 h-4" style={{ color: '#60a5fa' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('quantData')}</span>
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>- {t('quantDataDesc')}</span>
|
||||
</div>
|
||||
|
||||
<div className="p-3 space-y-3">
|
||||
{/* Enable Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: '#60a5fa' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('quantData')}</span>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enable_quant_data || false}
|
||||
onChange={(e) => !disabled && onChange({ ...config, enable_quant_data: e.target.checked })}
|
||||
disabled={disabled}
|
||||
className="w-4 h-4 rounded accent-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* API URL */}
|
||||
{config.enable_quant_data && (
|
||||
<div>
|
||||
<div className="flex items-center justify-between mb-1">
|
||||
<label className="text-[10px]" style={{ color: '#848E9C' }}>
|
||||
{t('quantDataUrl')}
|
||||
</label>
|
||||
{!disabled && !config.quant_data_api_url && (
|
||||
<button
|
||||
type="button"
|
||||
onClick={() => onChange({ ...config, quant_data_api_url: DEFAULT_QUANT_DATA_API_URL })}
|
||||
className="text-[10px] px-2 py-0.5 rounded"
|
||||
style={{ background: '#60a5fa20', color: '#60a5fa' }}
|
||||
>
|
||||
{t('fillDefault')}
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
<input
|
||||
type="text"
|
||||
value={config.quant_data_api_url || ''}
|
||||
onChange={(e) => !disabled && onChange({ ...config, quant_data_api_url: e.target.value })}
|
||||
disabled={disabled}
|
||||
placeholder="http://example.com/api/coin/{symbol}?include=netflow,oi"
|
||||
className="w-full px-2 py-1.5 rounded text-xs font-mono"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
/>
|
||||
<p className="text-[10px] mt-1" style={{ color: '#5E6673' }}>{t('symbolPlaceholder')}</p>
|
||||
|
||||
{/* OI and Netflow toggles */}
|
||||
<div className="flex gap-4 mt-3">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enable_quant_oi !== false}
|
||||
onChange={(e) => !disabled && onChange({ ...config, enable_quant_oi: e.target.checked })}
|
||||
disabled={disabled}
|
||||
className="w-3.5 h-3.5 rounded accent-blue-500"
|
||||
/>
|
||||
<span className="text-xs" style={{ color: '#EAECEF' }}>OI</span>
|
||||
</label>
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enable_quant_netflow !== false}
|
||||
onChange={(e) => !disabled && onChange({ ...config, enable_quant_netflow: e.target.checked })}
|
||||
disabled={disabled}
|
||||
className="w-3.5 h-3.5 rounded accent-blue-500"
|
||||
/>
|
||||
<span className="text-xs" style={{ color: '#EAECEF' }}>Netflow</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Section 5: OI Ranking Data (Market-wide) */}
|
||||
<div className="rounded-lg overflow-hidden" style={{ background: '#0B0E11', border: '1px solid #2B3139' }}>
|
||||
<div className="px-3 py-2 flex items-center gap-2" style={{ background: '#1E2329', borderBottom: '1px solid #2B3139' }}>
|
||||
<LineChart className="w-4 h-4" style={{ color: '#22c55e' }} />
|
||||
<span className="text-sm font-medium" style={{ color: '#EAECEF' }}>{t('oiRanking')}</span>
|
||||
<span className="text-xs" style={{ color: '#848E9C' }}>- {t('oiRankingDesc')}</span>
|
||||
</div>
|
||||
|
||||
<div className="p-3 space-y-3">
|
||||
{/* Enable Toggle */}
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex items-center gap-2">
|
||||
<div className="w-2 h-2 rounded-full" style={{ background: '#22c55e' }} />
|
||||
<span className="text-xs font-medium" style={{ color: '#EAECEF' }}>{t('oiRanking')}</span>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enable_oi_ranking || false}
|
||||
onChange={(e) => !disabled && onChange({
|
||||
...config,
|
||||
enable_oi_ranking: e.target.checked,
|
||||
// Set defaults when enabling
|
||||
...(e.target.checked && !config.oi_ranking_api_url ? { oi_ranking_api_url: DEFAULT_OI_RANKING_API_URL } : {}),
|
||||
...(e.target.checked && !config.oi_ranking_duration ? { oi_ranking_duration: '1h' } : {}),
|
||||
...(e.target.checked && !config.oi_ranking_limit ? { oi_ranking_limit: 10 } : {}),
|
||||
})}
|
||||
disabled={disabled}
|
||||
className="w-4 h-4 rounded accent-green-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
{/* Settings */}
|
||||
{config.enable_oi_ranking && (
|
||||
<div className="space-y-3">
|
||||
<div className="flex gap-3">
|
||||
{/* Duration */}
|
||||
<div className="flex-1">
|
||||
<label className="text-[10px] mb-1 block" style={{ color: '#848E9C' }}>
|
||||
{t('oiRankingDuration')}
|
||||
</label>
|
||||
<select
|
||||
value={config.oi_ranking_duration || '1h'}
|
||||
onChange={(e) => !disabled && onChange({ ...config, oi_ranking_duration: e.target.value })}
|
||||
disabled={disabled}
|
||||
className="w-full px-2 py-1.5 rounded text-xs"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
>
|
||||
<option value="1h">{language === 'zh' ? '1小时' : '1 Hour'}</option>
|
||||
<option value="4h">{language === 'zh' ? '4小时' : '4 Hours'}</option>
|
||||
<option value="24h">{language === 'zh' ? '24小时' : '24 Hours'}</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* Limit */}
|
||||
<div className="flex-1">
|
||||
<label className="text-[10px] mb-1 block" style={{ color: '#848E9C' }}>
|
||||
{t('oiRankingLimit')}
|
||||
</label>
|
||||
<select
|
||||
value={config.oi_ranking_limit || 10}
|
||||
onChange={(e) => !disabled && onChange({ ...config, oi_ranking_limit: parseInt(e.target.value) })}
|
||||
disabled={disabled}
|
||||
className="w-full px-2 py-1.5 rounded text-xs"
|
||||
style={{ background: '#1E2329', border: '1px solid #2B3139', color: '#EAECEF' }}
|
||||
>
|
||||
{[5, 10, 15, 20].map(n => (
|
||||
<option key={n} value={n}>{n}</option>
|
||||
))}
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
<p className="text-[10px]" style={{ color: '#5E6673' }}>{t('oiRankingNote')}</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -504,7 +504,7 @@ export const translations = {
|
||||
noExchangesConfigured: 'No configured exchanges',
|
||||
signalSource: 'Signal Source',
|
||||
signalSourceConfig: 'Signal Source Configuration',
|
||||
coinPoolDescription:
|
||||
ai500Description:
|
||||
'API endpoint for AI500 data provider, leave blank to disable this signal source',
|
||||
oiTopDescription:
|
||||
'API endpoint for open interest rankings, leave blank to disable this signal source',
|
||||
@@ -784,7 +784,7 @@ export const translations = {
|
||||
candidateCoins: 'Candidate Coins',
|
||||
candidateCoinsZeroWarning: 'Candidate Coins Count is 0',
|
||||
possibleReasons: 'Possible Reasons:',
|
||||
coinPoolApiNotConfigured:
|
||||
ai500ApiNotConfigured:
|
||||
'AI500 data provider API not configured or inaccessible (check signal source settings)',
|
||||
apiConnectionTimeout: 'API connection timeout or returned empty data',
|
||||
noCustomCoinsAndApiFailed:
|
||||
@@ -792,7 +792,7 @@ export const translations = {
|
||||
solutions: 'Solutions:',
|
||||
setCustomCoinsInConfig: 'Set custom coin list in trader configuration',
|
||||
orConfigureCorrectApiUrl: 'Or configure correct data provider API address',
|
||||
orDisableCoinPoolOptions:
|
||||
orDisableAI500Options:
|
||||
'Or disable "Use AI500 Data Provider" and "Use OI Top" options',
|
||||
signalSourceNotConfigured: 'Signal Source Not Configured',
|
||||
signalSourceWarningMessage:
|
||||
@@ -1691,7 +1691,7 @@ export const translations = {
|
||||
noExchangesConfigured: '暂无已配置的交易所',
|
||||
signalSource: '信号源',
|
||||
signalSourceConfig: '信号源配置',
|
||||
coinPoolDescription:
|
||||
ai500Description:
|
||||
'用于获取 AI500 数据源的 API 地址,留空则不使用此数据源',
|
||||
oiTopDescription: '用于获取持仓量排行数据的API地址,留空则不使用此信号源',
|
||||
information: '说明',
|
||||
@@ -1939,14 +1939,14 @@ export const translations = {
|
||||
candidateCoins: '候选币种',
|
||||
candidateCoinsZeroWarning: '候选币种数量为 0',
|
||||
possibleReasons: '可能原因:',
|
||||
coinPoolApiNotConfigured:
|
||||
ai500ApiNotConfigured:
|
||||
'AI500 数据源 API 未配置或无法访问(请检查信号源设置)',
|
||||
apiConnectionTimeout: 'API连接超时或返回数据为空',
|
||||
noCustomCoinsAndApiFailed: '未配置自定义币种且API获取失败',
|
||||
solutions: '解决方案:',
|
||||
setCustomCoinsInConfig: '在交易员配置中设置自定义币种列表',
|
||||
orConfigureCorrectApiUrl: '或者配置正确的数据源 API 地址',
|
||||
orDisableCoinPoolOptions: '或者禁用"使用 AI500 数据源"和"使用 OI Top"选项',
|
||||
orDisableAI500Options: '或者禁用"使用 AI500 数据源"和"使用 OI Top"选项',
|
||||
signalSourceNotConfigured: '信号源未配置',
|
||||
signalSourceWarningMessage:
|
||||
'您有交易员启用了"使用 AI500 数据源"或"使用 OI Top",但尚未配置信号源 API 地址。这将导致候选币种数量为 0,交易员无法正常工作。',
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { useState, useEffect, useCallback } from 'react'
|
||||
import { useState, useEffect, useCallback, useRef } from 'react'
|
||||
import { useAuth } from '../contexts/AuthContext'
|
||||
import { useLanguage } from '../contexts/LanguageContext'
|
||||
import {
|
||||
@@ -150,6 +150,45 @@ export function StrategyStudioPage() {
|
||||
fetchAiModels()
|
||||
}, [fetchStrategies, fetchAiModels])
|
||||
|
||||
// Track previous language to detect actual changes
|
||||
const prevLanguageRef = useRef(language)
|
||||
|
||||
// When language changes, update prompt sections to match the new language
|
||||
useEffect(() => {
|
||||
const updatePromptSectionsForLanguage = async () => {
|
||||
// Only update if language actually changed (not on initial mount)
|
||||
if (prevLanguageRef.current === language) return
|
||||
prevLanguageRef.current = language
|
||||
|
||||
if (!token) return
|
||||
|
||||
try {
|
||||
// Fetch default config for the new language
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/strategies/default-config?lang=${language}`,
|
||||
{ headers: { Authorization: `Bearer ${token}` } }
|
||||
)
|
||||
if (!response.ok) return
|
||||
const defaultConfig = await response.json()
|
||||
|
||||
// Update only the prompt sections and language field
|
||||
setEditingConfig(prev => {
|
||||
if (!prev) return prev
|
||||
return {
|
||||
...prev,
|
||||
language: language as 'zh' | 'en',
|
||||
prompt_sections: defaultConfig.prompt_sections,
|
||||
}
|
||||
})
|
||||
setHasChanges(true)
|
||||
} catch (err) {
|
||||
console.error('Failed to update prompt sections for language:', err)
|
||||
}
|
||||
}
|
||||
|
||||
updatePromptSectionsForLanguage()
|
||||
}, [language, token]) // Only trigger when language changes
|
||||
|
||||
// Create new strategy
|
||||
const handleCreateStrategy = async () => {
|
||||
if (!token) return
|
||||
@@ -336,6 +375,11 @@ export function StrategyStudioPage() {
|
||||
if (!token || !selectedStrategy || !editingConfig) return
|
||||
setIsSaving(true)
|
||||
try {
|
||||
// Always sync the config language with the current interface language
|
||||
const configWithLanguage = {
|
||||
...editingConfig,
|
||||
language: language as 'zh' | 'en',
|
||||
}
|
||||
const response = await fetch(
|
||||
`${API_BASE}/api/strategies/${selectedStrategy.id}`,
|
||||
{
|
||||
@@ -347,7 +391,7 @@ export function StrategyStudioPage() {
|
||||
body: JSON.stringify({
|
||||
name: selectedStrategy.name,
|
||||
description: selectedStrategy.description,
|
||||
config: editingConfig,
|
||||
config: configWithLanguage,
|
||||
is_public: selectedStrategy.is_public,
|
||||
config_visible: selectedStrategy.config_visible,
|
||||
}),
|
||||
|
||||
+26
-10
@@ -99,7 +99,7 @@ export interface TraderInfo {
|
||||
strategy_id?: string
|
||||
strategy_name?: string
|
||||
custom_prompt?: string
|
||||
use_coin_pool?: boolean
|
||||
use_ai500?: boolean
|
||||
use_oi_top?: boolean
|
||||
system_prompt_template?: string
|
||||
}
|
||||
@@ -172,7 +172,7 @@ export interface CreateTraderRequest {
|
||||
custom_prompt?: string
|
||||
override_base_prompt?: boolean
|
||||
system_prompt_template?: string
|
||||
use_coin_pool?: boolean
|
||||
use_ai500?: boolean
|
||||
use_oi_top?: boolean
|
||||
}
|
||||
|
||||
@@ -249,7 +249,7 @@ export interface TraderConfigData {
|
||||
custom_prompt?: string
|
||||
override_base_prompt?: boolean
|
||||
system_prompt_template?: string
|
||||
use_coin_pool?: boolean
|
||||
use_ai500?: boolean
|
||||
use_oi_top?: boolean
|
||||
}
|
||||
|
||||
@@ -462,6 +462,9 @@ export interface PromptSectionsConfig {
|
||||
}
|
||||
|
||||
export interface StrategyConfig {
|
||||
// Language setting: "zh" for Chinese, "en" for English
|
||||
// Determines the language used for data formatting and prompt generation
|
||||
language?: 'zh' | 'en';
|
||||
coin_source: CoinSourceConfig;
|
||||
indicators: IndicatorConfig;
|
||||
custom_prompt?: string;
|
||||
@@ -470,15 +473,14 @@ export interface StrategyConfig {
|
||||
}
|
||||
|
||||
export interface CoinSourceConfig {
|
||||
source_type: 'static' | 'coinpool' | 'oi_top' | 'mixed';
|
||||
source_type: 'static' | 'ai500' | 'oi_top' | 'mixed';
|
||||
static_coins?: string[];
|
||||
excluded_coins?: string[]; // 排除的币种列表
|
||||
use_coin_pool: boolean;
|
||||
coin_pool_limit?: number;
|
||||
coin_pool_api_url?: string; // AI500 币种池 API URL
|
||||
use_ai500: boolean;
|
||||
ai500_limit?: number;
|
||||
use_oi_top: boolean;
|
||||
oi_top_limit?: number;
|
||||
oi_top_api_url?: string; // OI Top API URL
|
||||
// Note: API URLs are now built automatically using nofxos_api_key from IndicatorConfig
|
||||
}
|
||||
|
||||
export interface IndicatorConfig {
|
||||
@@ -499,16 +501,30 @@ export interface IndicatorConfig {
|
||||
atr_periods?: number[];
|
||||
boll_periods?: number[];
|
||||
external_data_sources?: ExternalDataSource[];
|
||||
|
||||
// ========== NofxOS 数据源统一配置 ==========
|
||||
// Unified NofxOS API Key - used for all NofxOS data sources
|
||||
nofxos_api_key?: string;
|
||||
|
||||
// 量化数据源(资金流向、持仓变化、价格变化)
|
||||
enable_quant_data?: boolean;
|
||||
quant_data_api_url?: string;
|
||||
enable_quant_oi?: boolean;
|
||||
enable_quant_netflow?: boolean;
|
||||
|
||||
// OI 排行数据(市场持仓量增减排行)
|
||||
enable_oi_ranking?: boolean;
|
||||
oi_ranking_api_url?: string;
|
||||
oi_ranking_duration?: string; // "1h", "4h", "24h"
|
||||
oi_ranking_limit?: number;
|
||||
|
||||
// NetFlow 排行数据(机构/散户资金流向排行)
|
||||
enable_netflow_ranking?: boolean;
|
||||
netflow_ranking_duration?: string; // "1h", "4h", "24h"
|
||||
netflow_ranking_limit?: number;
|
||||
|
||||
// Price 排行数据(涨跌幅排行)
|
||||
enable_price_ranking?: boolean;
|
||||
price_ranking_duration?: string; // "1h", "4h", "24h" or "1h,4h,24h"
|
||||
price_ranking_limit?: number;
|
||||
}
|
||||
|
||||
export interface KlineConfig {
|
||||
|
||||
Reference in New Issue
Block a user