mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-18 09:54:35 +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:
@@ -594,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()
|
||||||
@@ -618,7 +618,7 @@ func (e *StrategyEngine) getOITopCoins(limit int) ([]CandidateCoin, error) {
|
|||||||
|
|
||||||
func (e *StrategyEngine) getOILowCoins(limit int) ([]CandidateCoin, error) {
|
func (e *StrategyEngine) getOILowCoins(limit int) ([]CandidateCoin, error) {
|
||||||
if limit <= 0 {
|
if limit <= 0 {
|
||||||
limit = 20
|
limit = 10
|
||||||
}
|
}
|
||||||
|
|
||||||
positions, err := e.nofxosClient.GetOILowPositions()
|
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)
|
// 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
|
||||||
@@ -131,11 +131,11 @@ func (c *Client) GetOITopSymbols() ([]string, error) {
|
|||||||
|
|
||||||
// GetOILowPositions retrieves OI decrease positions (for short opportunities)
|
// GetOILowPositions retrieves OI decrease positions (for short opportunities)
|
||||||
func (c *Client) GetOILowPositions() ([]OIPosition, error) {
|
func (c *Client) GetOILowPositions() ([]OIPosition, error) {
|
||||||
data, err := c.GetOIRanking("1h", 20)
|
positions, _, err := c.fetchOIRanking("low", "1h", 20)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
return data.LowPositions, nil
|
return positions, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetOILowSymbols retrieves OI low coin symbol list
|
// GetOILowSymbols retrieves OI low coin symbol list
|
||||||
|
|||||||
@@ -252,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{
|
||||||
|
|||||||
@@ -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