diff --git a/kernel/engine.go b/kernel/engine.go
index 8a937865..d4010070 100644
--- a/kernel/engine.go
+++ b/kernel/engine.go
@@ -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()
diff --git a/provider/nofxos/oi.go b/provider/nofxos/oi.go
index a1fe7408..2819379e 100644
--- a/provider/nofxos/oi.go
+++ b/provider/nofxos/oi.go
@@ -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
diff --git a/store/strategy.go b/store/strategy.go
index 8f7d86ee..7fbdef99 100644
--- a/store/strategy.go
+++ b/store/strategy.go
@@ -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{
diff --git a/web/src/components/strategy/CoinSourceEditor.tsx b/web/src/components/strategy/CoinSourceEditor.tsx
index 8c8d9801..68d48d2b 100644
--- a/web/src/components/strategy/CoinSourceEditor.tsx
+++ b/web/src/components/strategy/CoinSourceEditor.tsx
@@ -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({
-
+
{sourceTypes.map(({ value, icon: Icon, color }) => (
- {/* Static Coins */}
- {(config.source_type === 'static' || config.source_type === 'mixed') && (
+ {/* Static Coins - only for static mode */}
+ {config.source_type === 'static' && (
- {/* AI500 Options */}
- {(config.source_type === 'ai500' || config.source_type === 'mixed') && (
+ {/* AI500 Options - only for ai500 mode */}
+ {config.source_type === 'ai500' && (
@@ -340,8 +377,8 @@ export function CoinSourceEditor({
)}
- {/* 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' && (
@@ -349,7 +386,7 @@ export function CoinSourceEditor({
- OI Top {t('dataSourceConfig')}
+ OI {language === 'zh' ? '持仓增加榜' : 'Increase'} {t('dataSourceConfig')}
@@ -375,10 +412,10 @@ export function CoinSourceEditor({
{t('oiTopLimit')}:
)}
+
+ {/* OI Low Options - only for oi_low mode */}
+ {config.source_type === 'oi_low' && (
+
+
+
+
+
+ OI {language === 'zh' ? '持仓减少榜' : 'Decrease'} {t('dataSourceConfig')}
+
+
+
+
+
+
+
+
+ {config.use_oi_low && (
+
+
+ {t('oiLowLimit')}:
+
+
+
+ )}
+
+
+ {t('nofxosNote')}
+
+
+
+ )}
+
+ {/* Mixed Mode - Unified Card Selector */}
+ {config.source_type === 'mixed' && (
+
+
+
+
+ {t('mixedConfig')}
+
+
+
+ {/* 4 Source Cards in 2x2 Grid */}
+
+ {/* AI500 Card */}
+
!disabled && onChange({ ...config, use_ai500: !config.use_ai500 })}
+ >
+
+ !disabled && onChange({ ...config, use_ai500: e.target.checked })}
+ disabled={disabled}
+ className="w-4 h-4 rounded accent-nofx-gold"
+ onClick={(e) => e.stopPropagation()}
+ />
+
+ AI500
+
+
+ {config.use_ai500 && (
+
+ Limit:
+
+
+ )}
+
+
+ {/* OI Top Card */}
+
!disabled && onChange({ ...config, use_oi_top: !config.use_oi_top })}
+ >
+
+ !disabled && onChange({ ...config, use_oi_top: e.target.checked })}
+ disabled={disabled}
+ className="w-4 h-4 rounded accent-nofx-success"
+ onClick={(e) => e.stopPropagation()}
+ />
+
+
+ {language === 'zh' ? 'OI 增加' : 'OI Increase'}
+
+
+
+ {language === 'zh' ? '适合做多' : 'For long'}
+
+ {config.use_oi_top && (
+
+ Limit:
+
+
+ )}
+
+
+ {/* OI Low Card */}
+
!disabled && onChange({ ...config, use_oi_low: !config.use_oi_low })}
+ >
+
+ !disabled && onChange({ ...config, use_oi_low: e.target.checked })}
+ disabled={disabled}
+ className="w-4 h-4 rounded accent-red-500"
+ onClick={(e) => e.stopPropagation()}
+ />
+
+
+ {language === 'zh' ? 'OI 减少' : 'OI Decrease'}
+
+
+
+ {language === 'zh' ? '适合做空' : 'For short'}
+
+ {config.use_oi_low && (
+
+ Limit:
+
+
+ )}
+
+
+ {/* Static/Custom Card */}
+
0
+ ? 'bg-gray-500/10 border-gray-500/50'
+ : 'bg-nofx-bg border-nofx-border hover:border-gray-500/30'
+ }`}
+ >
+
+
+
+ {language === 'zh' ? '自定义' : 'Custom'}
+
+ {(config.static_coins || []).length > 0 && (
+
+ {config.static_coins?.length}
+
+ )}
+
+
+ {(config.static_coins || []).slice(0, 3).map((coin) => (
+
+ {coin}
+ {!disabled && (
+
+ )}
+
+ ))}
+ {(config.static_coins || []).length > 3 && (
+
+ +{(config.static_coins?.length || 0) - 3}
+
+ )}
+
+ {!disabled && (
+
+
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"
+ />
+
+
+ )}
+
+
+
+ {/* Summary */}
+ {(() => {
+ const { sources, totalLimit } = getMixedSummary()
+ if (sources.length === 0) return null
+ return (
+
+
+ {t('mixedSummary')}:
+
+ {sources.join(' + ')}
+
+
+
+ {t('maxCoins')} {totalLimit} {t('coins')}
+
+
+ )
+ })()}
+
+ )}
)
}
diff --git a/web/src/types.ts b/web/src/types.ts
index d2e1d1b0..1f08c09c 100644
--- a/web/src/types.ts
+++ b/web/src/types.ts
@@ -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
}