mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
feat: add OI Low coin source and improve Mixed mode UI
- Add oi_low as independent source_type for short opportunities - Redesign Mixed mode with card-based selector (2x2 grid) - Show combination summary with total coin limit - Support both Chinese and English languages - Change default limits to 10 for OI Top and OI Low
This commit is contained in:
+2
-2
@@ -594,7 +594,7 @@ func (e *StrategyEngine) getAI500Coins(limit int) ([]CandidateCoin, error) {
|
||||
|
||||
func (e *StrategyEngine) getOITopCoins(limit int) ([]CandidateCoin, error) {
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
limit = 10
|
||||
}
|
||||
|
||||
positions, err := e.nofxosClient.GetOITopPositions()
|
||||
@@ -618,7 +618,7 @@ func (e *StrategyEngine) getOITopCoins(limit int) ([]CandidateCoin, error) {
|
||||
|
||||
func (e *StrategyEngine) getOILowCoins(limit int) ([]CandidateCoin, error) {
|
||||
if limit <= 0 {
|
||||
limit = 20
|
||||
limit = 10
|
||||
}
|
||||
|
||||
positions, err := e.nofxosClient.GetOILowPositions()
|
||||
|
||||
@@ -106,11 +106,11 @@ func (c *Client) fetchOIRanking(rankType, duration string, limit int) ([]OIPosit
|
||||
|
||||
// GetOITopPositions retrieves top OI increase positions (legacy compatibility)
|
||||
func (c *Client) GetOITopPositions() ([]OIPosition, error) {
|
||||
data, err := c.GetOIRanking("1h", 20)
|
||||
positions, _, err := c.fetchOIRanking("top", "1h", 20)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data.TopPositions, nil
|
||||
return positions, nil
|
||||
}
|
||||
|
||||
// GetOITopSymbols retrieves OI top coin symbol list
|
||||
@@ -131,11 +131,11 @@ func (c *Client) GetOITopSymbols() ([]string, error) {
|
||||
|
||||
// GetOILowPositions retrieves OI decrease positions (for short opportunities)
|
||||
func (c *Client) GetOILowPositions() ([]OIPosition, error) {
|
||||
data, err := c.GetOIRanking("1h", 20)
|
||||
positions, _, err := c.fetchOIRanking("low", "1h", 20)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return data.LowPositions, nil
|
||||
return positions, nil
|
||||
}
|
||||
|
||||
// GetOILowSymbols retrieves OI low coin symbol list
|
||||
|
||||
+3
-1
@@ -252,7 +252,9 @@ func GetDefaultStrategyConfig(lang string) StrategyConfig {
|
||||
UseAI500: true,
|
||||
AI500Limit: 10,
|
||||
UseOITop: false,
|
||||
OITopLimit: 20,
|
||||
OITopLimit: 10,
|
||||
UseOILow: false,
|
||||
OILowLimit: 10,
|
||||
},
|
||||
Indicators: IndicatorConfig{
|
||||
Klines: KlineConfig{
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
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'
|
||||
|
||||
interface CoinSourceEditorProps {
|
||||
@@ -23,27 +23,38 @@ export function CoinSourceEditor({
|
||||
sourceType: { zh: '数据来源类型', en: 'Source Type' },
|
||||
static: { zh: '静态列表', en: 'Static List' },
|
||||
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' },
|
||||
staticCoins: { zh: '自定义币种', en: 'Custom Coins' },
|
||||
addCoin: { zh: '添加币种', en: 'Add Coin' },
|
||||
useAI500: { zh: '启用 AI500 数据源', en: 'Enable AI500 Data Provider' },
|
||||
ai500Limit: { zh: '数量上限', en: 'Limit' },
|
||||
useOITop: { zh: '启用 OI Top 数据', en: 'Enable OI Top' },
|
||||
useOITop: { zh: '启用 OI 持仓增加榜', en: 'Enable OI Increase' },
|
||||
oiTopLimit: { zh: '数量上限', en: 'Limit' },
|
||||
useOILow: { zh: '启用 OI 持仓减少榜', en: 'Enable OI Decrease' },
|
||||
oiLowLimit: { zh: '数量上限', en: 'Limit' },
|
||||
staticDesc: { zh: '手动指定交易币种列表', en: 'Manually specify trading coins' },
|
||||
ai500Desc: {
|
||||
zh: '使用 AI500 智能筛选的热门币种',
|
||||
en: 'Use AI500 smart-filtered popular coins',
|
||||
},
|
||||
oiTopDesc: {
|
||||
zh: '使用持仓量增长最快的币种',
|
||||
en: 'Use coins with fastest OI growth',
|
||||
zh: '持仓增加榜,适合做多',
|
||||
en: 'OI increase ranking, for long',
|
||||
},
|
||||
oi_lowDesc: {
|
||||
zh: '持仓减少榜,适合做空',
|
||||
en: 'OI decrease ranking, for short',
|
||||
},
|
||||
mixedDesc: {
|
||||
zh: '组合多种数据源,AI500 + OI Top + 自定义',
|
||||
en: 'Combine multiple sources: AI500 + OI Top + Custom',
|
||||
zh: '组合多种数据源',
|
||||
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' },
|
||||
excludedCoins: { zh: '排除币种', en: 'Excluded Coins' },
|
||||
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: 'ai500', icon: Database, color: '#F0B90B' },
|
||||
{ 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
|
||||
|
||||
// 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
|
||||
const xyzDexAssets = new Set([
|
||||
// Stocks
|
||||
@@ -156,7 +193,7 @@ export function CoinSourceEditor({
|
||||
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
||||
{t('sourceType')}
|
||||
</label>
|
||||
<div className="grid grid-cols-4 gap-3">
|
||||
<div className="grid grid-cols-5 gap-2">
|
||||
{sourceTypes.map(({ value, icon: Icon, color }) => (
|
||||
<button
|
||||
key={value}
|
||||
@@ -182,8 +219,8 @@ export function CoinSourceEditor({
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Static Coins */}
|
||||
{(config.source_type === 'static' || config.source_type === 'mixed') && (
|
||||
{/* Static Coins - only for static mode */}
|
||||
{config.source_type === 'static' && (
|
||||
<div>
|
||||
<label className="block text-sm font-medium mb-3 text-nofx-text">
|
||||
{t('staticCoins')}
|
||||
@@ -283,8 +320,8 @@ export function CoinSourceEditor({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* AI500 Options */}
|
||||
{(config.source_type === 'ai500' || config.source_type === 'mixed') && (
|
||||
{/* AI500 Options - only for ai500 mode */}
|
||||
{config.source_type === 'ai500' && (
|
||||
<div
|
||||
className="p-4 rounded-lg bg-nofx-gold/5 border border-nofx-gold/20"
|
||||
>
|
||||
@@ -340,8 +377,8 @@ export function CoinSourceEditor({
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* OI Top Options */}
|
||||
{(config.source_type === 'oi_top' || config.source_type === 'mixed') && (
|
||||
{/* OI Top Options - only for oi_top mode */}
|
||||
{config.source_type === 'oi_top' && (
|
||||
<div
|
||||
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">
|
||||
<TrendingUp className="w-4 h-4 text-nofx-success" />
|
||||
<span className="text-sm font-medium text-nofx-text">
|
||||
OI Top {t('dataSourceConfig')}
|
||||
OI {language === 'zh' ? '持仓增加榜' : 'Increase'} {t('dataSourceConfig')}
|
||||
</span>
|
||||
<NofxOSBadge />
|
||||
</div>
|
||||
@@ -375,10 +412,10 @@ export function CoinSourceEditor({
|
||||
{t('oiTopLimit')}:
|
||||
</span>
|
||||
<select
|
||||
value={config.oi_top_limit || 20}
|
||||
value={config.oi_top_limit || 10}
|
||||
onChange={(e) =>
|
||||
!disabled &&
|
||||
onChange({ ...config, oi_top_limit: parseInt(e.target.value) || 20 })
|
||||
onChange({ ...config, oi_top_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"
|
||||
@@ -396,6 +433,306 @@ export function CoinSourceEditor({
|
||||
</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>
|
||||
)
|
||||
}
|
||||
|
||||
+3
-1
@@ -509,13 +509,15 @@ export interface GridStrategyConfig {
|
||||
}
|
||||
|
||||
export interface CoinSourceConfig {
|
||||
source_type: 'static' | 'ai500' | 'oi_top' | 'mixed';
|
||||
source_type: 'static' | 'ai500' | 'oi_top' | 'oi_low' | 'mixed';
|
||||
static_coins?: string[];
|
||||
excluded_coins?: string[]; // 排除的币种列表
|
||||
use_ai500: boolean;
|
||||
ai500_limit?: number;
|
||||
use_oi_top: boolean;
|
||||
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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user