From c9150e827382931a6ce637ad10b83c47b0f8f622 Mon Sep 17 00:00:00 2001 From: tinkle-community Date: Fri, 23 Jan 2026 20:50:23 +0800 Subject: [PATCH] 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 --- kernel/engine.go | 4 +- provider/nofxos/oi.go | 8 +- store/strategy.go | 4 +- .../components/strategy/CoinSourceEditor.tsx | 373 +++++++++++++++++- web/src/types.ts | 4 +- 5 files changed, 367 insertions(+), 26 deletions(-) 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')}: + !disabled && onChange({ ...config, use_oi_low: e.target.checked }) + } + disabled={disabled} + className="w-5 h-5 rounded accent-red-500" + /> + {t('useOILow')} + + + {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 }