Files
nofx/web/src/components/strategy/RiskControlEditor.tsx
T
tinkle-community 5cff32e4f2 Feature/custom strategy (#1172)
* feat: add Strategy Studio with multi-timeframe support
- Add Strategy Studio page with three-column layout for strategy management
- Support multi-timeframe K-line data selection (5m, 15m, 1h, 4h, etc.)
- Add GetWithTimeframes() function in market package for fetching multiple timeframes
- Add TimeframeSeriesData struct for storing per-timeframe technical indicators
- Update formatMarketData() to display all selected timeframes in AI prompt
- Add strategy API endpoints for CRUD operations and test run
- Integrate real AI test runs with configured AI models
- Support custom AI500 and OI Top API URLs from strategy config
* docs: add Strategy Studio screenshot to README files
* fix: correct strategy-studio.png filename case in README
* refactor: remove legacy signal source config and simplify trader creation
- Remove signal source configuration from traders page (now handled by strategy)
- Remove advanced options (legacy config) from TraderConfigModal
- Rename default strategy to "默认山寨策略" with AI500 coin pool URL
- Delete SignalSourceModal and SignalSourceWarning components
- Clean up related stores, hooks, and page components
2025-12-06 07:20:11 +08:00

328 lines
12 KiB
TypeScript

import { Shield, AlertTriangle } from 'lucide-react'
import type { RiskControlConfig } from '../../types'
interface RiskControlEditorProps {
config: RiskControlConfig
onChange: (config: RiskControlConfig) => void
disabled?: boolean
language: string
}
export function RiskControlEditor({
config,
onChange,
disabled,
language,
}: RiskControlEditorProps) {
const t = (key: string) => {
const translations: Record<string, Record<string, string>> = {
positionLimits: { zh: '仓位限制', en: 'Position Limits' },
maxPositions: { zh: '最大持仓数量', en: 'Max Positions' },
maxPositionsDesc: { zh: '同时持有的最大币种数量', en: 'Maximum coins held simultaneously' },
btcEthLeverage: { zh: 'BTC/ETH 最大杠杆', en: 'BTC/ETH Max Leverage' },
altcoinLeverage: { zh: '山寨币最大杠杆', en: 'Altcoin Max Leverage' },
riskParameters: { zh: '风险参数', en: 'Risk Parameters' },
minRiskReward: { zh: '最小风险回报比', en: 'Min Risk/Reward Ratio' },
minRiskRewardDesc: { zh: '开仓要求的最低盈亏比', en: 'Minimum profit ratio for opening' },
maxMarginUsage: { zh: '最大保证金使用率', en: 'Max Margin Usage' },
maxMarginUsageDesc: { zh: '保证金使用率上限', en: 'Maximum margin utilization' },
maxPositionRatio: { zh: '单币最大仓位比', en: 'Max Position Ratio' },
maxPositionRatioDesc: { zh: '相对账户净值的倍数', en: 'Multiple of account equity' },
entryRequirements: { zh: '开仓要求', en: 'Entry Requirements' },
minPositionSize: { zh: '最小开仓金额', en: 'Min Position Size' },
minPositionSizeDesc: { zh: 'USDT 最小名义价值', en: 'Minimum notional value in USDT' },
minConfidence: { zh: '最小信心度', en: 'Min Confidence' },
minConfidenceDesc: { zh: 'AI 开仓信心度阈值', en: 'AI confidence threshold for entry' },
}
return translations[key]?.[language] || key
}
const updateField = <K extends keyof RiskControlConfig>(
key: K,
value: RiskControlConfig[K]
) => {
if (!disabled) {
onChange({ ...config, [key]: value })
}
}
return (
<div className="space-y-6">
{/* Position Limits */}
<div>
<div className="flex items-center gap-2 mb-4">
<Shield className="w-5 h-5" style={{ color: '#F0B90B' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('positionLimits')}
</h3>
</div>
<div className="grid grid-cols-3 gap-4">
<div
className="p-4 rounded-lg"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('maxPositions')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('maxPositionsDesc')}
</p>
<input
type="number"
value={config.max_positions}
onChange={(e) =>
updateField('max_positions', parseInt(e.target.value) || 3)
}
disabled={disabled}
min={1}
max={10}
className="w-full px-3 py-2 rounded"
style={{
background: '#1E2329',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
/>
</div>
<div
className="p-4 rounded-lg"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('btcEthLeverage')}
</label>
<div className="flex items-center gap-2">
<input
type="range"
value={config.btc_eth_max_leverage}
onChange={(e) =>
updateField('btc_eth_max_leverage', parseInt(e.target.value))
}
disabled={disabled}
min={1}
max={20}
className="flex-1 accent-yellow-500"
/>
<span
className="w-12 text-center font-mono"
style={{ color: '#F0B90B' }}
>
{config.btc_eth_max_leverage}x
</span>
</div>
</div>
<div
className="p-4 rounded-lg"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('altcoinLeverage')}
</label>
<div className="flex items-center gap-2">
<input
type="range"
value={config.altcoin_max_leverage}
onChange={(e) =>
updateField('altcoin_max_leverage', parseInt(e.target.value))
}
disabled={disabled}
min={1}
max={20}
className="flex-1 accent-yellow-500"
/>
<span
className="w-12 text-center font-mono"
style={{ color: '#F0B90B' }}
>
{config.altcoin_max_leverage}x
</span>
</div>
</div>
</div>
</div>
{/* Risk Parameters */}
<div>
<div className="flex items-center gap-2 mb-4">
<AlertTriangle className="w-5 h-5" style={{ color: '#F6465D' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('riskParameters')}
</h3>
</div>
<div className="grid grid-cols-3 gap-4">
<div
className="p-4 rounded-lg"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('minRiskReward')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('minRiskRewardDesc')}
</p>
<div className="flex items-center">
<span style={{ color: '#848E9C' }}>1:</span>
<input
type="number"
value={config.min_risk_reward_ratio}
onChange={(e) =>
updateField('min_risk_reward_ratio', parseFloat(e.target.value) || 3)
}
disabled={disabled}
min={1}
max={10}
step={0.5}
className="w-20 px-3 py-2 rounded ml-2"
style={{
background: '#1E2329',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
/>
</div>
</div>
<div
className="p-4 rounded-lg"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('maxMarginUsage')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('maxMarginUsageDesc')}
</p>
<div className="flex items-center gap-2">
<input
type="range"
value={config.max_margin_usage * 100}
onChange={(e) =>
updateField('max_margin_usage', parseInt(e.target.value) / 100)
}
disabled={disabled}
min={10}
max={100}
className="flex-1 accent-red-500"
/>
<span className="w-12 text-center font-mono" style={{ color: '#F6465D' }}>
{Math.round(config.max_margin_usage * 100)}%
</span>
</div>
</div>
<div
className="p-4 rounded-lg"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('maxPositionRatio')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('maxPositionRatioDesc')}
</p>
<div className="flex items-center">
<input
type="number"
value={config.max_position_ratio}
onChange={(e) =>
updateField('max_position_ratio', parseFloat(e.target.value) || 1.5)
}
disabled={disabled}
min={0.5}
max={5}
step={0.1}
className="w-20 px-3 py-2 rounded"
style={{
background: '#1E2329',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
/>
<span className="ml-2" style={{ color: '#848E9C' }}>
x
</span>
</div>
</div>
</div>
</div>
{/* Entry Requirements */}
<div>
<div className="flex items-center gap-2 mb-4">
<Shield className="w-5 h-5" style={{ color: '#0ECB81' }} />
<h3 className="font-medium" style={{ color: '#EAECEF' }}>
{t('entryRequirements')}
</h3>
</div>
<div className="grid grid-cols-2 gap-4">
<div
className="p-4 rounded-lg"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('minPositionSize')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('minPositionSizeDesc')}
</p>
<div className="flex items-center">
<input
type="number"
value={config.min_position_size}
onChange={(e) =>
updateField('min_position_size', parseFloat(e.target.value) || 12)
}
disabled={disabled}
min={10}
max={1000}
className="w-24 px-3 py-2 rounded"
style={{
background: '#1E2329',
border: '1px solid #2B3139',
color: '#EAECEF',
}}
/>
<span className="ml-2" style={{ color: '#848E9C' }}>
USDT
</span>
</div>
</div>
<div
className="p-4 rounded-lg"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('minConfidence')}
</label>
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
{t('minConfidenceDesc')}
</p>
<div className="flex items-center gap-2">
<input
type="range"
value={config.min_confidence}
onChange={(e) =>
updateField('min_confidence', parseInt(e.target.value))
}
disabled={disabled}
min={50}
max={100}
className="flex-1 accent-green-500"
/>
<span className="w-12 text-center font-mono" style={{ color: '#0ECB81' }}>
{config.min_confidence}
</span>
</div>
</div>
</div>
</div>
</div>
)
}