mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-15 16:56:56 +08:00
feat: add excluded coins filter for strategy
- Add excluded_coins field to CoinSourceConfig - Filter excluded coins in GetCandidateCoins function - Add excluded coins UI in CoinSourceEditor
This commit is contained in:
@@ -119,7 +119,7 @@ type Context struct {
|
|||||||
MultiTFMarket map[string]map[string]*market.Data `json:"-"`
|
MultiTFMarket map[string]map[string]*market.Data `json:"-"`
|
||||||
OITopDataMap map[string]*OITopData `json:"-"`
|
OITopDataMap map[string]*OITopData `json:"-"`
|
||||||
QuantDataMap map[string]*QuantData `json:"-"`
|
QuantDataMap map[string]*QuantData `json:"-"`
|
||||||
OIRankingData *provider.OIRankingData `json:"-"` // Market-wide OI ranking data
|
OIRankingData *provider.OIRankingData `json:"-"` // Market-wide OI ranking data
|
||||||
BTCETHLeverage int `json:"-"`
|
BTCETHLeverage int `json:"-"`
|
||||||
AltcoinLeverage int `json:"-"`
|
AltcoinLeverage int `json:"-"`
|
||||||
Timeframes []string `json:"-"`
|
Timeframes []string `json:"-"`
|
||||||
@@ -401,7 +401,8 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
|||||||
Sources: []string{"static"},
|
Sources: []string{"static"},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return candidates, nil
|
|
||||||
|
return e.filterExcludedCoins(candidates), nil
|
||||||
|
|
||||||
case "coinpool":
|
case "coinpool":
|
||||||
// 检查 use_coin_pool 标志,如果为 false 则回退到静态币种
|
// 检查 use_coin_pool 标志,如果为 false 则回退到静态币种
|
||||||
@@ -414,9 +415,13 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
|||||||
Sources: []string{"static"},
|
Sources: []string{"static"},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return candidates, nil
|
return e.filterExcludedCoins(candidates), nil
|
||||||
}
|
}
|
||||||
return e.getCoinPoolCoins(coinSource.CoinPoolLimit)
|
coins, err := e.getCoinPoolCoins(coinSource.CoinPoolLimit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return e.filterExcludedCoins(coins), nil
|
||||||
|
|
||||||
case "oi_top":
|
case "oi_top":
|
||||||
// 检查 use_oi_top 标志,如果为 false 则回退到静态币种
|
// 检查 use_oi_top 标志,如果为 false 则回退到静态币种
|
||||||
@@ -429,9 +434,13 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
|||||||
Sources: []string{"static"},
|
Sources: []string{"static"},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return candidates, nil
|
return e.filterExcludedCoins(candidates), nil
|
||||||
}
|
}
|
||||||
return e.getOITopCoins(coinSource.OITopLimit)
|
coins, err := e.getOITopCoins(coinSource.OITopLimit)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return e.filterExcludedCoins(coins), nil
|
||||||
|
|
||||||
case "mixed":
|
case "mixed":
|
||||||
if coinSource.UseCoinPool {
|
if coinSource.UseCoinPool {
|
||||||
@@ -471,13 +480,39 @@ func (e *StrategyEngine) GetCandidateCoins() ([]CandidateCoin, error) {
|
|||||||
Sources: sources,
|
Sources: sources,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
return candidates, nil
|
return e.filterExcludedCoins(candidates), nil
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unknown coin source type: %s", coinSource.SourceType)
|
return nil, fmt.Errorf("unknown coin source type: %s", coinSource.SourceType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// filterExcludedCoins removes excluded coins from the candidates list
|
||||||
|
func (e *StrategyEngine) filterExcludedCoins(candidates []CandidateCoin) []CandidateCoin {
|
||||||
|
if len(e.config.CoinSource.ExcludedCoins) == 0 {
|
||||||
|
return candidates
|
||||||
|
}
|
||||||
|
|
||||||
|
// Build excluded set for O(1) lookup
|
||||||
|
excluded := make(map[string]bool)
|
||||||
|
for _, coin := range e.config.CoinSource.ExcludedCoins {
|
||||||
|
normalized := market.Normalize(coin)
|
||||||
|
excluded[normalized] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
// Filter out excluded coins
|
||||||
|
filtered := make([]CandidateCoin, 0, len(candidates))
|
||||||
|
for _, c := range candidates {
|
||||||
|
if !excluded[c.Symbol] {
|
||||||
|
filtered = append(filtered, c)
|
||||||
|
} else {
|
||||||
|
logger.Infof("🚫 Excluded coin: %s", c.Symbol)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return filtered
|
||||||
|
}
|
||||||
|
|
||||||
func (e *StrategyEngine) getCoinPoolCoins(limit int) ([]CandidateCoin, error) {
|
func (e *StrategyEngine) getCoinPoolCoins(limit int) ([]CandidateCoin, error) {
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
limit = 30
|
limit = 30
|
||||||
|
|||||||
@@ -62,6 +62,8 @@ type CoinSourceConfig struct {
|
|||||||
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"`
|
||||||
|
// excluded coins list (filtered out from all sources)
|
||||||
|
ExcludedCoins []string `json:"excluded_coins,omitempty"`
|
||||||
// whether to use AI500 coin pool
|
// whether to use AI500 coin pool
|
||||||
UseCoinPool bool `json:"use_coin_pool"`
|
UseCoinPool bool `json:"use_coin_pool"`
|
||||||
// AI500 coin pool maximum count
|
// AI500 coin pool maximum count
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import { useState } from 'react'
|
import { useState } from 'react'
|
||||||
import { Plus, X, Database, TrendingUp, List, Link, AlertCircle } from 'lucide-react'
|
import { Plus, X, Database, TrendingUp, List, Link, AlertCircle, Ban } from 'lucide-react'
|
||||||
import type { CoinSourceConfig } from '../../types'
|
import type { CoinSourceConfig } from '../../types'
|
||||||
|
|
||||||
// Default API URLs for data sources
|
// Default API URLs for data sources
|
||||||
@@ -20,6 +20,7 @@ export function CoinSourceEditor({
|
|||||||
language,
|
language,
|
||||||
}: CoinSourceEditorProps) {
|
}: CoinSourceEditorProps) {
|
||||||
const [newCoin, setNewCoin] = useState('')
|
const [newCoin, setNewCoin] = useState('')
|
||||||
|
const [newExcludedCoin, setNewExcludedCoin] = useState('')
|
||||||
|
|
||||||
const t = (key: string) => {
|
const t = (key: string) => {
|
||||||
const translations: Record<string, Record<string, string>> = {
|
const translations: Record<string, Record<string, string>> = {
|
||||||
@@ -54,6 +55,9 @@ export function CoinSourceEditor({
|
|||||||
apiUrlRequired: { zh: '需要填写 API URL 才能获取数据', en: 'API URL required to fetch data' },
|
apiUrlRequired: { zh: '需要填写 API URL 才能获取数据', en: 'API URL required to fetch data' },
|
||||||
dataSourceConfig: { zh: '数据源配置', en: 'Data Source Configuration' },
|
dataSourceConfig: { zh: '数据源配置', en: 'Data Source Configuration' },
|
||||||
fillDefault: { zh: '填入默认', en: 'Fill Default' },
|
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' },
|
||||||
}
|
}
|
||||||
return translations[key]?.[language] || key
|
return translations[key]?.[language] || key
|
||||||
}
|
}
|
||||||
@@ -115,6 +119,36 @@ export function CoinSourceEditor({
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const handleAddExcludedCoin = () => {
|
||||||
|
if (!newExcludedCoin.trim()) return
|
||||||
|
const symbol = newExcludedCoin.toUpperCase().trim()
|
||||||
|
|
||||||
|
// For xyz dex assets, use xyz: prefix without USDT
|
||||||
|
let formattedSymbol: string
|
||||||
|
if (isXyzDexAsset(symbol)) {
|
||||||
|
const base = symbol.replace(/^xyz:/i, '').replace(/USDT$|USD$|-USDC$/i, '')
|
||||||
|
formattedSymbol = `xyz:${base}`
|
||||||
|
} else {
|
||||||
|
formattedSymbol = symbol.endsWith('USDT') ? symbol : `${symbol}USDT`
|
||||||
|
}
|
||||||
|
|
||||||
|
const currentExcluded = config.excluded_coins || []
|
||||||
|
if (!currentExcluded.includes(formattedSymbol)) {
|
||||||
|
onChange({
|
||||||
|
...config,
|
||||||
|
excluded_coins: [...currentExcluded, formattedSymbol],
|
||||||
|
})
|
||||||
|
}
|
||||||
|
setNewExcludedCoin('')
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleRemoveExcludedCoin = (coin: string) => {
|
||||||
|
onChange({
|
||||||
|
...config,
|
||||||
|
excluded_coins: (config.excluded_coins || []).filter((c) => c !== coin),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
{/* Source Type Selector */}
|
{/* Source Type Selector */}
|
||||||
@@ -209,6 +243,68 @@ export function CoinSourceEditor({
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Excluded Coins */}
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 mb-3">
|
||||||
|
<Ban className="w-4 h-4" style={{ color: '#F6465D' }} />
|
||||||
|
<label className="text-sm font-medium" style={{ color: '#EAECEF' }}>
|
||||||
|
{t('excludedCoins')}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<p className="text-xs mb-3" style={{ color: '#848E9C' }}>
|
||||||
|
{t('excludedCoinsDesc')}
|
||||||
|
</p>
|
||||||
|
<div className="flex flex-wrap gap-2 mb-3">
|
||||||
|
{(config.excluded_coins || []).map((coin) => (
|
||||||
|
<span
|
||||||
|
key={coin}
|
||||||
|
className="flex items-center gap-1 px-3 py-1.5 rounded-full text-sm"
|
||||||
|
style={{ background: 'rgba(246, 70, 93, 0.15)', color: '#F6465D' }}
|
||||||
|
>
|
||||||
|
{coin}
|
||||||
|
{!disabled && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleRemoveExcludedCoin(coin)}
|
||||||
|
className="ml-1 hover:text-white transition-colors"
|
||||||
|
>
|
||||||
|
<X className="w-3 h-3" />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
))}
|
||||||
|
{(config.excluded_coins || []).length === 0 && (
|
||||||
|
<span className="text-xs italic" style={{ color: '#5E6673' }}>
|
||||||
|
{language === 'zh' ? '无' : 'None'}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
{!disabled && (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={newExcludedCoin}
|
||||||
|
onChange={(e) => setNewExcludedCoin(e.target.value)}
|
||||||
|
onKeyDown={(e) => e.key === 'Enter' && handleAddExcludedCoin()}
|
||||||
|
placeholder="BTC, ETH, DOGE..."
|
||||||
|
className="flex-1 px-4 py-2 rounded-lg text-sm"
|
||||||
|
style={{
|
||||||
|
background: '#0B0E11',
|
||||||
|
border: '1px solid #2B3139',
|
||||||
|
color: '#EAECEF',
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<button
|
||||||
|
onClick={handleAddExcludedCoin}
|
||||||
|
className="px-4 py-2 rounded-lg flex items-center gap-2 transition-colors text-sm"
|
||||||
|
style={{ background: '#F6465D', color: '#EAECEF' }}
|
||||||
|
>
|
||||||
|
<Ban className="w-4 h-4" />
|
||||||
|
{t('addExcludedCoin')}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
{/* Coin Pool Options */}
|
{/* Coin Pool Options */}
|
||||||
{(config.source_type === 'coinpool' || config.source_type === 'mixed') && (
|
{(config.source_type === 'coinpool' || config.source_type === 'mixed') && (
|
||||||
<div className="space-y-4">
|
<div className="space-y-4">
|
||||||
@@ -400,6 +496,7 @@ export function CoinSourceEditor({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -472,6 +472,7 @@ export interface StrategyConfig {
|
|||||||
export interface CoinSourceConfig {
|
export interface CoinSourceConfig {
|
||||||
source_type: 'static' | 'coinpool' | 'oi_top' | 'mixed';
|
source_type: 'static' | 'coinpool' | 'oi_top' | 'mixed';
|
||||||
static_coins?: string[];
|
static_coins?: string[];
|
||||||
|
excluded_coins?: string[]; // 排除的币种列表
|
||||||
use_coin_pool: boolean;
|
use_coin_pool: boolean;
|
||||||
coin_pool_limit?: number;
|
coin_pool_limit?: number;
|
||||||
coin_pool_api_url?: string; // AI500 币种池 API URL
|
coin_pool_api_url?: string; // AI500 币种池 API URL
|
||||||
|
|||||||
Reference in New Issue
Block a user