mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-19 02:14:35 +08:00
Compare commits
4 Commits
2f54d1d4c0
...
fcc0267a46
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
fcc0267a46 | ||
|
|
c9150e8273 | ||
|
|
fcaabea6cb | ||
|
|
b5716ff3cb |
@@ -500,6 +500,9 @@ Thanks to all our sponsors!
|
|||||||
<a href="https://github.com/vip3001003"><img src="https://github.com/vip3001003.png" width="60" height="60" style="border-radius:50%" alt="vip3001003" /></a>
|
<a href="https://github.com/vip3001003"><img src="https://github.com/vip3001003.png" width="60" height="60" style="border-radius:50%" alt="vip3001003" /></a>
|
||||||
<a href="https://github.com/mrtluh"><img src="https://github.com/mrtluh.png" width="60" height="60" style="border-radius:50%" alt="mrtluh" /></a>
|
<a href="https://github.com/mrtluh"><img src="https://github.com/mrtluh.png" width="60" height="60" style="border-radius:50%" alt="mrtluh" /></a>
|
||||||
<a href="https://github.com/cpcp1117-source"><img src="https://github.com/cpcp1117-source.png" width="60" height="60" style="border-radius:50%" alt="cpcp1117-source" /></a>
|
<a href="https://github.com/cpcp1117-source"><img src="https://github.com/cpcp1117-source.png" width="60" height="60" style="border-radius:50%" alt="cpcp1117-source" /></a>
|
||||||
|
<a href="https://github.com/match-007"><img src="https://github.com/match-007.png" width="60" height="60" style="border-radius:50%" alt="match-007" /></a>
|
||||||
|
<a href="https://github.com/leiwuhen1715"><img src="https://github.com/leiwuhen1715.png" width="60" height="60" style="border-radius:50%" alt="leiwuhen1715" /></a>
|
||||||
|
<a href="https://github.com/SHAOXIA1991"><img src="https://github.com/SHAOXIA1991.png" width="60" height="60" style="border-radius:50%" alt="SHAOXIA1991" /></a>
|
||||||
|
|
||||||
[Become a sponsor](https://github.com/sponsors/NoFxAiOS)
|
[Become a sponsor](https://github.com/sponsors/NoFxAiOS)
|
||||||
|
|
||||||
|
|||||||
@@ -447,6 +447,7 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// 空列表是正常情况,直接返回
|
||||||
return e.filterExcludedCoins(coins), nil
|
return e.filterExcludedCoins(coins), nil
|
||||||
|
|
||||||
case "oi_top":
|
case "oi_top":
|
||||||
@@ -466,6 +467,27 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
// 空列表是正常情况,直接返回
|
||||||
|
return e.filterExcludedCoins(coins), nil
|
||||||
|
|
||||||
|
case "oi_low":
|
||||||
|
// 持仓减少榜,适合做空
|
||||||
|
if !coinSource.UseOILow {
|
||||||
|
logger.Infof("⚠️ source_type is 'oi_low' but use_oi_low is false, falling back to static coins")
|
||||||
|
for _, symbol := range coinSource.StaticCoins {
|
||||||
|
symbol = market.Normalize(symbol)
|
||||||
|
candidates = append(candidates, CandidateCoin{
|
||||||
|
Symbol: symbol,
|
||||||
|
Sources: []string{"static"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return e.filterExcludedCoins(candidates), nil
|
||||||
|
}
|
||||||
|
coins, err := e.getOILowCoins(coinSource.OILowLimit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
// 空列表是正常情况,直接返回
|
||||||
return e.filterExcludedCoins(coins), nil
|
return e.filterExcludedCoins(coins), nil
|
||||||
|
|
||||||
case "mixed":
|
case "mixed":
|
||||||
@@ -491,6 +513,17 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if coinSource.UseOILow {
|
||||||
|
oiLowCoins, err := e.getOILowCoins(coinSource.OILowLimit)
|
||||||
|
if err != nil {
|
||||||
|
logger.Infof("⚠️ Failed to get OI Low: %v", err)
|
||||||
|
} else {
|
||||||
|
for _, coin := range oiLowCoins {
|
||||||
|
symbolSources[coin.Symbol] = append(symbolSources[coin.Symbol], "oi_low")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
for _, symbol := range coinSource.StaticCoins {
|
for _, symbol := range coinSource.StaticCoins {
|
||||||
symbol = market.Normalize(symbol)
|
symbol = market.Normalize(symbol)
|
||||||
if _, exists := symbolSources[symbol]; !exists {
|
if _, exists := symbolSources[symbol]; !exists {
|
||||||
@@ -561,7 +594,7 @@ func (e *StrategyEngine) getAI500Coins(limit int) ([]CandidateCoin, error) {
|
|||||||
|
|
||||||
func (e *StrategyEngine) getOITopCoins(limit int) ([]CandidateCoin, error) {
|
func (e *StrategyEngine) getOITopCoins(limit int) ([]CandidateCoin, error) {
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
limit = 20
|
limit = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
positions, err := e.nofxosClient.GetOITopPositions()
|
positions, err := e.nofxosClient.GetOITopPositions()
|
||||||
@@ -583,6 +616,30 @@ func (e *StrategyEngine) getOITopCoins(limit int) ([]CandidateCoin, error) {
|
|||||||
return candidates, nil
|
return candidates, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (e *StrategyEngine) getOILowCoins(limit int) ([]CandidateCoin, error) {
|
||||||
|
if limit <= 0 {
|
||||||
|
limit = 10
|
||||||
|
}
|
||||||
|
|
||||||
|
positions, err := e.nofxosClient.GetOILowPositions()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var candidates []CandidateCoin
|
||||||
|
for i, pos := range positions {
|
||||||
|
if i >= limit {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
symbol := market.Normalize(pos.Symbol)
|
||||||
|
candidates = append(candidates, CandidateCoin{
|
||||||
|
Symbol: symbol,
|
||||||
|
Sources: []string{"oi_low"},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return candidates, nil
|
||||||
|
}
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// External & Quant Data
|
// External & Quant Data
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
@@ -1289,13 +1346,38 @@ func (e *StrategyEngine) formatPositionInfo(index int, pos PositionInfo, ctx *Co
|
|||||||
|
|
||||||
func (e *StrategyEngine) formatCoinSourceTag(sources []string) string {
|
func (e *StrategyEngine) formatCoinSourceTag(sources []string) string {
|
||||||
if len(sources) > 1 {
|
if len(sources) > 1 {
|
||||||
return " (AI500+OI_Top dual signal)"
|
// 多信号源组合
|
||||||
|
hasAI500 := false
|
||||||
|
hasOITop := false
|
||||||
|
hasOILow := false
|
||||||
|
for _, s := range sources {
|
||||||
|
switch s {
|
||||||
|
case "ai500":
|
||||||
|
hasAI500 = true
|
||||||
|
case "oi_top":
|
||||||
|
hasOITop = true
|
||||||
|
case "oi_low":
|
||||||
|
hasOILow = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if hasAI500 && hasOITop {
|
||||||
|
return " (AI500+OI_Top dual signal)"
|
||||||
|
}
|
||||||
|
if hasAI500 && hasOILow {
|
||||||
|
return " (AI500+OI_Low dual signal)"
|
||||||
|
}
|
||||||
|
if hasOITop && hasOILow {
|
||||||
|
return " (OI_Top+OI_Low)"
|
||||||
|
}
|
||||||
|
return " (Multiple sources)"
|
||||||
} else if len(sources) == 1 {
|
} else if len(sources) == 1 {
|
||||||
switch sources[0] {
|
switch sources[0] {
|
||||||
case "ai500":
|
case "ai500":
|
||||||
return " (AI500)"
|
return " (AI500)"
|
||||||
case "oi_top":
|
case "oi_top":
|
||||||
return " (OI_Top position growth)"
|
return " (OI_Top 持仓增加)"
|
||||||
|
case "oi_low":
|
||||||
|
return " (OI_Low 持仓减少)"
|
||||||
case "static":
|
case "static":
|
||||||
return " (Manual selection)"
|
return " (Manual selection)"
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,8 +73,10 @@ func (c *Client) fetchAI500() ([]CoinData, error) {
|
|||||||
return nil, fmt.Errorf("API returned failure status")
|
return nil, fmt.Errorf("API returned failure status")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 空列表是正常情况,不是错误
|
||||||
if len(response.Data.Coins) == 0 {
|
if len(response.Data.Coins) == 0 {
|
||||||
return nil, fmt.Errorf("coin list is empty")
|
log.Printf("ℹ️ AI500 returned empty coin list (no coins meet criteria currently)")
|
||||||
|
return []CoinData{}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Set IsAvailable flag
|
// Set IsAvailable flag
|
||||||
|
|||||||
@@ -106,11 +106,11 @@ func (c *Client) fetchOIRanking(rankType, duration string, limit int) ([]OIPosit
|
|||||||
|
|
||||||
// GetOITopPositions retrieves top OI increase positions (legacy compatibility)
|
// GetOITopPositions retrieves top OI increase positions (legacy compatibility)
|
||||||
func (c *Client) GetOITopPositions() ([]OIPosition, error) {
|
func (c *Client) GetOITopPositions() ([]OIPosition, error) {
|
||||||
data, err := c.GetOIRanking("1h", 20)
|
positions, _, err := c.fetchOIRanking("top", "1h", 20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return data.TopPositions, nil
|
return positions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOITopSymbols retrieves OI top coin symbol list
|
// GetOITopSymbols retrieves OI top coin symbol list
|
||||||
@@ -129,6 +129,31 @@ func (c *Client) GetOITopSymbols() ([]string, error) {
|
|||||||
return symbols, nil
|
return symbols, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// GetOILowPositions retrieves OI decrease positions (for short opportunities)
|
||||||
|
func (c *Client) GetOILowPositions() ([]OIPosition, error) {
|
||||||
|
positions, _, err := c.fetchOIRanking("low", "1h", 20)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return positions, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetOILowSymbols retrieves OI low coin symbol list
|
||||||
|
func (c *Client) GetOILowSymbols() ([]string, error) {
|
||||||
|
positions, err := c.GetOILowPositions()
|
||||||
|
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
|
// FormatOIRankingForAI formats OI ranking data for AI consumption
|
||||||
func FormatOIRankingForAI(data *OIRankingData, lang Language) string {
|
func FormatOIRankingForAI(data *OIRankingData, lang Language) string {
|
||||||
if data == nil {
|
if data == nil {
|
||||||
|
|||||||
@@ -97,7 +97,7 @@ type PromptSectionsConfig struct {
|
|||||||
|
|
||||||
// CoinSourceConfig coin source configuration
|
// CoinSourceConfig coin source configuration
|
||||||
type CoinSourceConfig struct {
|
type CoinSourceConfig struct {
|
||||||
// source type: "static" | "ai500" | "oi_top" | "mixed"
|
// source type: "static" | "ai500" | "oi_top" | "oi_low" | "mixed"
|
||||||
SourceType string `json:"source_type"`
|
SourceType string `json:"source_type"`
|
||||||
// static coin list (used when source_type = "static")
|
// static coin list (used when source_type = "static")
|
||||||
StaticCoins []string `json:"static_coins,omitempty"`
|
StaticCoins []string `json:"static_coins,omitempty"`
|
||||||
@@ -107,10 +107,14 @@ type CoinSourceConfig struct {
|
|||||||
UseAI500 bool `json:"use_ai500"`
|
UseAI500 bool `json:"use_ai500"`
|
||||||
// AI500 coin pool maximum count
|
// AI500 coin pool maximum count
|
||||||
AI500Limit int `json:"ai500_limit,omitempty"`
|
AI500Limit int `json:"ai500_limit,omitempty"`
|
||||||
// whether to use OI Top
|
// whether to use OI Top (持仓增加榜,适合做多)
|
||||||
UseOITop bool `json:"use_oi_top"`
|
UseOITop bool `json:"use_oi_top"`
|
||||||
// OI Top maximum count
|
// OI Top maximum count
|
||||||
OITopLimit int `json:"oi_top_limit,omitempty"`
|
OITopLimit int `json:"oi_top_limit,omitempty"`
|
||||||
|
// whether to use OI Low (持仓减少榜,适合做空)
|
||||||
|
UseOILow bool `json:"use_oi_low"`
|
||||||
|
// OI Low maximum count
|
||||||
|
OILowLimit int `json:"oi_low_limit,omitempty"`
|
||||||
// Note: API URLs are now built automatically using NofxOSAPIKey from IndicatorConfig
|
// Note: API URLs are now built automatically using NofxOSAPIKey from IndicatorConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -248,7 +252,9 @@ func GetDefaultStrategyConfig(lang string) StrategyConfig {
|
|||||||
UseAI500: true,
|
UseAI500: true,
|
||||||
AI500Limit: 10,
|
AI500Limit: 10,
|
||||||
UseOITop: false,
|
UseOITop: false,
|
||||||
OITopLimit: 20,
|
OITopLimit: 10,
|
||||||
|
UseOILow: false,
|
||||||
|
OILowLimit: 10,
|
||||||
},
|
},
|
||||||
Indicators: IndicatorConfig{
|
Indicators: IndicatorConfig{
|
||||||
Klines: KlineConfig{
|
Klines: KlineConfig{
|
||||||
|
|||||||
@@ -534,6 +534,12 @@ func (at *AutoTrader) runCycle() error {
|
|||||||
return fmt.Errorf("failed to build trading context: %w", err)
|
return fmt.Errorf("failed to build trading context: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 如果没有候选币种,友好提示并跳过本周期
|
||||||
|
if len(ctx.CandidateCoins) == 0 {
|
||||||
|
logger.Infof("ℹ️ No candidate coins available, skipping this cycle")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
// Save equity snapshot independently (decoupled from AI decision, used for drawing profit curve)
|
// Save equity snapshot independently (decoupled from AI decision, used for drawing profit curve)
|
||||||
at.saveEquitySnapshot(ctx)
|
at.saveEquitySnapshot(ctx)
|
||||||
|
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Plus, X, Database, TrendingUp, List, Ban, Zap } from 'lucide-react'
|
import { Plus, X, Database, TrendingUp, TrendingDown, List, Ban, Zap, Shuffle } from 'lucide-react'
|
||||||
import type { CoinSourceConfig } from '../../types'
|
import type { CoinSourceConfig } from '../../types'
|
||||||
|
|
||||||
interface CoinSourceEditorProps {
|
interface CoinSourceEditorProps {
|
||||||
@@ -23,27 +23,38 @@ export function CoinSourceEditor({
|
|||||||
sourceType: { zh: '数据来源类型', en: 'Source Type' },
|
sourceType: { zh: '数据来源类型', en: 'Source Type' },
|
||||||
static: { zh: '静态列表', en: 'Static List' },
|
static: { zh: '静态列表', en: 'Static List' },
|
||||||
ai500: { zh: 'AI500 数据源', en: 'AI500 Data Provider' },
|
ai500: { zh: 'AI500 数据源', en: 'AI500 Data Provider' },
|
||||||
oi_top: { zh: 'OI Top 持仓增长', en: 'OI Top' },
|
oi_top: { zh: 'OI 持仓增加', en: 'OI Increase' },
|
||||||
|
oi_low: { zh: 'OI 持仓减少', en: 'OI Decrease' },
|
||||||
mixed: { zh: '混合模式', en: 'Mixed Mode' },
|
mixed: { zh: '混合模式', en: 'Mixed Mode' },
|
||||||
staticCoins: { zh: '自定义币种', en: 'Custom Coins' },
|
staticCoins: { zh: '自定义币种', en: 'Custom Coins' },
|
||||||
addCoin: { zh: '添加币种', en: 'Add Coin' },
|
addCoin: { zh: '添加币种', en: 'Add Coin' },
|
||||||
useAI500: { zh: '启用 AI500 数据源', en: 'Enable AI500 Data Provider' },
|
useAI500: { zh: '启用 AI500 数据源', en: 'Enable AI500 Data Provider' },
|
||||||
ai500Limit: { zh: '数量上限', en: 'Limit' },
|
ai500Limit: { zh: '数量上限', en: 'Limit' },
|
||||||
useOITop: { zh: '启用 OI Top 数据', en: 'Enable OI Top' },
|
useOITop: { zh: '启用 OI 持仓增加榜', en: 'Enable OI Increase' },
|
||||||
oiTopLimit: { zh: '数量上限', en: 'Limit' },
|
oiTopLimit: { zh: '数量上限', en: 'Limit' },
|
||||||
|
useOILow: { zh: '启用 OI 持仓减少榜', en: 'Enable OI Decrease' },
|
||||||
|
oiLowLimit: { zh: '数量上限', en: 'Limit' },
|
||||||
staticDesc: { zh: '手动指定交易币种列表', en: 'Manually specify trading coins' },
|
staticDesc: { zh: '手动指定交易币种列表', en: 'Manually specify trading coins' },
|
||||||
ai500Desc: {
|
ai500Desc: {
|
||||||
zh: '使用 AI500 智能筛选的热门币种',
|
zh: '使用 AI500 智能筛选的热门币种',
|
||||||
en: 'Use AI500 smart-filtered popular coins',
|
en: 'Use AI500 smart-filtered popular coins',
|
||||||
},
|
},
|
||||||
oiTopDesc: {
|
oiTopDesc: {
|
||||||
zh: '使用持仓量增长最快的币种',
|
zh: '持仓增加榜,适合做多',
|
||||||
en: 'Use coins with fastest OI growth',
|
en: 'OI increase ranking, for long',
|
||||||
|
},
|
||||||
|
oi_lowDesc: {
|
||||||
|
zh: '持仓减少榜,适合做空',
|
||||||
|
en: 'OI decrease ranking, for short',
|
||||||
},
|
},
|
||||||
mixedDesc: {
|
mixedDesc: {
|
||||||
zh: '组合多种数据源,AI500 + OI Top + 自定义',
|
zh: '组合多种数据源',
|
||||||
en: 'Combine multiple sources: AI500 + OI Top + Custom',
|
en: 'Combine multiple sources',
|
||||||
},
|
},
|
||||||
|
mixedConfig: { zh: '组合数据源配置', en: 'Combined Sources Configuration' },
|
||||||
|
mixedSummary: { zh: '已选组合', en: 'Selected Sources' },
|
||||||
|
maxCoins: { zh: '最多', en: 'Up to' },
|
||||||
|
coins: { zh: '个币种', en: 'coins' },
|
||||||
dataSourceConfig: { zh: '数据源配置', en: 'Data Source Configuration' },
|
dataSourceConfig: { zh: '数据源配置', en: 'Data Source Configuration' },
|
||||||
excludedCoins: { zh: '排除币种', en: 'Excluded Coins' },
|
excludedCoins: { zh: '排除币种', en: 'Excluded Coins' },
|
||||||
excludedCoinsDesc: { zh: '这些币种将从所有数据源中排除,不会被交易', en: 'These coins will be excluded from all sources and will not be traded' },
|
excludedCoinsDesc: { zh: '这些币种将从所有数据源中排除,不会被交易', en: 'These coins will be excluded from all sources and will not be traded' },
|
||||||
@@ -57,9 +68,35 @@ export function CoinSourceEditor({
|
|||||||
{ value: 'static', icon: List, color: '#848E9C' },
|
{ value: 'static', icon: List, color: '#848E9C' },
|
||||||
{ value: 'ai500', icon: Database, color: '#F0B90B' },
|
{ value: 'ai500', icon: Database, color: '#F0B90B' },
|
||||||
{ value: 'oi_top', icon: TrendingUp, color: '#0ECB81' },
|
{ value: 'oi_top', icon: TrendingUp, color: '#0ECB81' },
|
||||||
{ value: 'mixed', icon: Database, color: '#60a5fa' },
|
{ value: 'oi_low', icon: TrendingDown, color: '#F6465D' },
|
||||||
|
{ value: 'mixed', icon: Shuffle, color: '#60a5fa' },
|
||||||
] as const
|
] as const
|
||||||
|
|
||||||
|
// Calculate mixed mode summary
|
||||||
|
const getMixedSummary = () => {
|
||||||
|
const sources: string[] = []
|
||||||
|
let totalLimit = 0
|
||||||
|
|
||||||
|
if (config.use_ai500) {
|
||||||
|
sources.push(`AI500(${config.ai500_limit || 10})`)
|
||||||
|
totalLimit += config.ai500_limit || 10
|
||||||
|
}
|
||||||
|
if (config.use_oi_top) {
|
||||||
|
sources.push(`${language === 'zh' ? 'OI增' : 'OI↑'}(${config.oi_top_limit || 10})`)
|
||||||
|
totalLimit += config.oi_top_limit || 10
|
||||||
|
}
|
||||||
|
if (config.use_oi_low) {
|
||||||
|
sources.push(`${language === 'zh' ? 'OI减' : 'OI↓'}(${config.oi_low_limit || 10})`)
|
||||||
|
totalLimit += config.oi_low_limit || 10
|
||||||
|
}
|
||||||
|
if ((config.static_coins || []).length > 0) {
|
||||||
|
sources.push(`${language === 'zh' ? '自定义' : 'Custom'}(${config.static_coins?.length || 0})`)
|
||||||
|
totalLimit += config.static_coins?.length || 0
|
||||||
|
}
|
||||||
|
|
||||||
|
return { sources, totalLimit }
|
||||||
|
}
|
||||||
|
|
||||||
// xyz dex assets (stocks, forex, commodities) - should NOT get USDT suffix
|
// xyz dex assets (stocks, forex, commodities) - should NOT get USDT suffix
|
||||||
const xyzDexAssets = new Set([
|
const xyzDexAssets = new Set([
|
||||||
// Stocks
|
// Stocks
|
||||||
@@ -156,7 +193,7 @@ export function CoinSourceEditor({
|
|||||||
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
||||||
{t('sourceType')}
|
{t('sourceType')}
|
||||||
</label>
|
</label>
|
||||||
<div className="grid grid-cols-4 gap-3">
|
<div className="grid grid-cols-5 gap-2">
|
||||||
{sourceTypes.map(({ value, icon: Icon, color }) => (
|
{sourceTypes.map(({ value, icon: Icon, color }) => (
|
||||||
<button
|
<button
|
||||||
key={value}
|
key={value}
|
||||||
@@ -182,8 +219,8 @@ export function CoinSourceEditor({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Static Coins */}
|
{/* Static Coins - only for static mode */}
|
||||||
{(config.source_type === 'static' || config.source_type === 'mixed') && (
|
{config.source_type === 'static' && (
|
||||||
<div>
|
<div>
|
||||||
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
||||||
{t('staticCoins')}
|
{t('staticCoins')}
|
||||||
@@ -283,8 +320,8 @@ export function CoinSourceEditor({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* AI500 Options */}
|
{/* AI500 Options - only for ai500 mode */}
|
||||||
{(config.source_type === 'ai500' || config.source_type === 'mixed') && (
|
{config.source_type === 'ai500' && (
|
||||||
<div
|
<div
|
||||||
className="p-4 rounded-lg bg-nofx-gold/5 border border-nofx-gold/20"
|
className="p-4 rounded-lg bg-nofx-gold/5 border border-nofx-gold/20"
|
||||||
>
|
>
|
||||||
@@ -340,8 +377,8 @@ export function CoinSourceEditor({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
{/* OI Top Options */}
|
{/* OI Top Options - only for oi_top mode */}
|
||||||
{(config.source_type === 'oi_top' || config.source_type === 'mixed') && (
|
{config.source_type === 'oi_top' && (
|
||||||
<div
|
<div
|
||||||
className="p-4 rounded-lg bg-nofx-success/5 border border-nofx-success/20"
|
className="p-4 rounded-lg bg-nofx-success/5 border border-nofx-success/20"
|
||||||
>
|
>
|
||||||
@@ -349,7 +386,7 @@ export function CoinSourceEditor({
|
|||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
<TrendingUp className="w-4 h-4 text-nofx-success" />
|
<TrendingUp className="w-4 h-4 text-nofx-success" />
|
||||||
<span className="text-sm font-medium text-nofx-text">
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
OI Top {t('dataSourceConfig')}
|
OI {language === 'zh' ? '持仓增加榜' : 'Increase'} {t('dataSourceConfig')}
|
||||||
</span>
|
</span>
|
||||||
<NofxOSBadge />
|
<NofxOSBadge />
|
||||||
</div>
|
</div>
|
||||||
@@ -375,10 +412,10 @@ export function CoinSourceEditor({
|
|||||||
{t('oiTopLimit')}:
|
{t('oiTopLimit')}:
|
||||||
</span>
|
</span>
|
||||||
<select
|
<select
|
||||||
value={config.oi_top_limit || 20}
|
value={config.oi_top_limit || 10}
|
||||||
onChange={(e) =>
|
onChange={(e) =>
|
||||||
!disabled &&
|
!disabled &&
|
||||||
onChange({ ...config, oi_top_limit: parseInt(e.target.value) || 20 })
|
onChange({ ...config, oi_top_limit: parseInt(e.target.value) || 10 })
|
||||||
}
|
}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
className="px-3 py-1.5 rounded bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
className="px-3 py-1.5 rounded bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
@@ -396,6 +433,306 @@ export function CoinSourceEditor({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* OI Low Options - only for oi_low mode */}
|
||||||
|
{config.source_type === 'oi_low' && (
|
||||||
|
<div
|
||||||
|
className="p-4 rounded-lg bg-nofx-danger/5 border border-nofx-danger/20"
|
||||||
|
>
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<TrendingDown className="w-4 h-4 text-nofx-danger" />
|
||||||
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
|
OI {language === 'zh' ? '持仓减少榜' : 'Decrease'} {t('dataSourceConfig')}
|
||||||
|
</span>
|
||||||
|
<NofxOSBadge />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="space-y-3">
|
||||||
|
<label className="flex items-center gap-3 cursor-pointer">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={config.use_oi_low}
|
||||||
|
onChange={(e) =>
|
||||||
|
!disabled && onChange({ ...config, use_oi_low: e.target.checked })
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
className="w-5 h-5 rounded accent-red-500"
|
||||||
|
/>
|
||||||
|
<span className="text-nofx-text">{t('useOILow')}</span>
|
||||||
|
</label>
|
||||||
|
|
||||||
|
{config.use_oi_low && (
|
||||||
|
<div className="flex items-center gap-3 pl-8">
|
||||||
|
<span className="text-sm text-nofx-text-muted">
|
||||||
|
{t('oiLowLimit')}:
|
||||||
|
</span>
|
||||||
|
<select
|
||||||
|
value={config.oi_low_limit || 10}
|
||||||
|
onChange={(e) =>
|
||||||
|
!disabled &&
|
||||||
|
onChange({ ...config, oi_low_limit: parseInt(e.target.value) || 10 })
|
||||||
|
}
|
||||||
|
disabled={disabled}
|
||||||
|
className="px-3 py-1.5 rounded bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
|
>
|
||||||
|
{[5, 10, 15, 20, 30, 50].map(n => (
|
||||||
|
<option key={n} value={n}>{n}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<p className="text-xs pl-8 text-nofx-text-muted">
|
||||||
|
{t('nofxosNote')}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Mixed Mode - Unified Card Selector */}
|
||||||
|
{config.source_type === 'mixed' && (
|
||||||
|
<div className="p-4 rounded-lg bg-blue-500/5 border border-blue-500/20">
|
||||||
|
<div className="flex items-center gap-2 mb-4">
|
||||||
|
<Shuffle className="w-4 h-4 text-blue-400" />
|
||||||
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
|
{t('mixedConfig')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 4 Source Cards in 2x2 Grid */}
|
||||||
|
<div className="grid grid-cols-2 gap-3 mb-4">
|
||||||
|
{/* AI500 Card */}
|
||||||
|
<div
|
||||||
|
className={`p-3 rounded-lg border transition-all cursor-pointer ${
|
||||||
|
config.use_ai500
|
||||||
|
? 'bg-nofx-gold/10 border-nofx-gold/50'
|
||||||
|
: 'bg-nofx-bg border-nofx-border hover:border-nofx-gold/30'
|
||||||
|
}`}
|
||||||
|
onClick={() => !disabled && onChange({ ...config, use_ai500: !config.use_ai500 })}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={config.use_ai500}
|
||||||
|
onChange={(e) => !disabled && onChange({ ...config, use_ai500: e.target.checked })}
|
||||||
|
disabled={disabled}
|
||||||
|
className="w-4 h-4 rounded accent-nofx-gold"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
<Database className="w-4 h-4 text-nofx-gold" />
|
||||||
|
<span className="text-sm font-medium text-nofx-text">AI500</span>
|
||||||
|
<NofxOSBadge />
|
||||||
|
</div>
|
||||||
|
{config.use_ai500 && (
|
||||||
|
<div className="flex items-center gap-2 mt-2 pl-6">
|
||||||
|
<span className="text-xs text-nofx-text-muted">Limit:</span>
|
||||||
|
<select
|
||||||
|
value={config.ai500_limit || 10}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
!disabled && onChange({ ...config, ai500_limit: parseInt(e.target.value) || 10 })
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
className="px-2 py-1 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{[5, 10, 15, 20, 30, 50].map(n => (
|
||||||
|
<option key={n} value={n}>{n}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* OI Top Card */}
|
||||||
|
<div
|
||||||
|
className={`p-3 rounded-lg border transition-all cursor-pointer ${
|
||||||
|
config.use_oi_top
|
||||||
|
? 'bg-nofx-success/10 border-nofx-success/50'
|
||||||
|
: 'bg-nofx-bg border-nofx-border hover:border-nofx-success/30'
|
||||||
|
}`}
|
||||||
|
onClick={() => !disabled && onChange({ ...config, use_oi_top: !config.use_oi_top })}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={config.use_oi_top}
|
||||||
|
onChange={(e) => !disabled && onChange({ ...config, use_oi_top: e.target.checked })}
|
||||||
|
disabled={disabled}
|
||||||
|
className="w-4 h-4 rounded accent-nofx-success"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
<TrendingUp className="w-4 h-4 text-nofx-success" />
|
||||||
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
|
{language === 'zh' ? 'OI 增加' : 'OI Increase'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-nofx-text-muted pl-6 mb-1">
|
||||||
|
{language === 'zh' ? '适合做多' : 'For long'}
|
||||||
|
</p>
|
||||||
|
{config.use_oi_top && (
|
||||||
|
<div className="flex items-center gap-2 mt-2 pl-6">
|
||||||
|
<span className="text-xs text-nofx-text-muted">Limit:</span>
|
||||||
|
<select
|
||||||
|
value={config.oi_top_limit || 10}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
!disabled && onChange({ ...config, oi_top_limit: parseInt(e.target.value) || 10 })
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
className="px-2 py-1 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{[5, 10, 15, 20, 30, 50].map(n => (
|
||||||
|
<option key={n} value={n}>{n}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* OI Low Card */}
|
||||||
|
<div
|
||||||
|
className={`p-3 rounded-lg border transition-all cursor-pointer ${
|
||||||
|
config.use_oi_low
|
||||||
|
? 'bg-nofx-danger/10 border-nofx-danger/50'
|
||||||
|
: 'bg-nofx-bg border-nofx-border hover:border-nofx-danger/30'
|
||||||
|
}`}
|
||||||
|
onClick={() => !disabled && onChange({ ...config, use_oi_low: !config.use_oi_low })}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={config.use_oi_low}
|
||||||
|
onChange={(e) => !disabled && onChange({ ...config, use_oi_low: e.target.checked })}
|
||||||
|
disabled={disabled}
|
||||||
|
className="w-4 h-4 rounded accent-red-500"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
<TrendingDown className="w-4 h-4 text-nofx-danger" />
|
||||||
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
|
{language === 'zh' ? 'OI 减少' : 'OI Decrease'}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs text-nofx-text-muted pl-6 mb-1">
|
||||||
|
{language === 'zh' ? '适合做空' : 'For short'}
|
||||||
|
</p>
|
||||||
|
{config.use_oi_low && (
|
||||||
|
<div className="flex items-center gap-2 mt-2 pl-6">
|
||||||
|
<span className="text-xs text-nofx-text-muted">Limit:</span>
|
||||||
|
<select
|
||||||
|
value={config.oi_low_limit || 10}
|
||||||
|
onChange={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
!disabled && onChange({ ...config, oi_low_limit: parseInt(e.target.value) || 10 })
|
||||||
|
}}
|
||||||
|
disabled={disabled}
|
||||||
|
className="px-2 py-1 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
>
|
||||||
|
{[5, 10, 15, 20, 30, 50].map(n => (
|
||||||
|
<option key={n} value={n}>{n}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Static/Custom Card */}
|
||||||
|
<div
|
||||||
|
className={`p-3 rounded-lg border transition-all cursor-pointer ${
|
||||||
|
(config.static_coins || []).length > 0
|
||||||
|
? 'bg-gray-500/10 border-gray-500/50'
|
||||||
|
: 'bg-nofx-bg border-nofx-border hover:border-gray-500/30'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<div className="flex items-center gap-2 mb-2">
|
||||||
|
<List className="w-4 h-4 text-gray-400" />
|
||||||
|
<span className="text-sm font-medium text-nofx-text">
|
||||||
|
{language === 'zh' ? '自定义' : 'Custom'}
|
||||||
|
</span>
|
||||||
|
{(config.static_coins || []).length > 0 && (
|
||||||
|
<span className="text-xs px-1.5 py-0.5 rounded bg-gray-500/20 text-gray-400">
|
||||||
|
{config.static_coins?.length}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className="flex flex-wrap gap-1 mt-2">
|
||||||
|
{(config.static_coins || []).slice(0, 3).map((coin) => (
|
||||||
|
<span
|
||||||
|
key={coin}
|
||||||
|
className="flex items-center gap-1 px-2 py-0.5 rounded text-xs bg-nofx-bg-lighter text-nofx-text"
|
||||||
|
>
|
||||||
|
{coin}
|
||||||
|
{!disabled && (
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleRemoveCoin(coin)
|
||||||
|
}}
|
||||||
|
className="hover:text-red-400 transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-2.5 h-2.5" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{(config.static_coins || []).length > 3 && (
|
||||||
|
<span className="text-xs text-nofx-text-muted">
|
||||||
|
+{(config.static_coins?.length || 0) - 3}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!disabled && (
|
||||||
|
<div className="flex gap-1 mt-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newCoin}
|
||||||
|
onChange={(e) => setNewCoin(e.target.value)}
|
||||||
|
onKeyDown={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
if (e.key === 'Enter') handleAddCoin()
|
||||||
|
}}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
placeholder="BTC, ETH..."
|
||||||
|
className="flex-1 px-2 py-1 rounded text-xs bg-nofx-bg border border-nofx-gold/20 text-nofx-text"
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={(e) => {
|
||||||
|
e.stopPropagation()
|
||||||
|
handleAddCoin()
|
||||||
|
}}
|
||||||
|
className="px-2 py-1 rounded text-xs bg-nofx-gold text-black hover:bg-yellow-500"
|
||||||
|
>
|
||||||
|
<Plus className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Summary */}
|
||||||
|
{(() => {
|
||||||
|
const { sources, totalLimit } = getMixedSummary()
|
||||||
|
if (sources.length === 0) return null
|
||||||
|
return (
|
||||||
|
<div className="p-2 rounded bg-nofx-bg border border-nofx-border">
|
||||||
|
<div className="flex items-center justify-between text-xs">
|
||||||
|
<span className="text-nofx-text-muted">{t('mixedSummary')}:</span>
|
||||||
|
<span className="text-nofx-text font-medium">
|
||||||
|
{sources.join(' + ')}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-xs text-nofx-text-muted mt-1">
|
||||||
|
{t('maxCoins')} {totalLimit} {t('coins')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)
|
||||||
|
})()}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -509,13 +509,15 @@ export interface GridStrategyConfig {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface CoinSourceConfig {
|
export interface CoinSourceConfig {
|
||||||
source_type: 'static' | 'ai500' | 'oi_top' | 'mixed';
|
source_type: 'static' | 'ai500' | 'oi_top' | 'oi_low' | 'mixed';
|
||||||
static_coins?: string[];
|
static_coins?: string[];
|
||||||
excluded_coins?: string[]; // 排除的币种列表
|
excluded_coins?: string[]; // 排除的币种列表
|
||||||
use_ai500: boolean;
|
use_ai500: boolean;
|
||||||
ai500_limit?: number;
|
ai500_limit?: number;
|
||||||
use_oi_top: boolean;
|
use_oi_top: boolean;
|
||||||
oi_top_limit?: number;
|
oi_top_limit?: number;
|
||||||
|
use_oi_low: boolean;
|
||||||
|
oi_low_limit?: number;
|
||||||
// Note: API URLs are now built automatically using nofxos_api_key from IndicatorConfig
|
// Note: API URLs are now built automatically using nofxos_api_key from IndicatorConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user