mirror of
https://github.com/laoxong/nofx.git
synced 2026-06-04 09:58:22 +08:00
319ccb8ca3
- Fix initial balance using available_balance instead of total_equity - Fix WSMonitor nil pointer by starting market monitor before loading traders - Add strategy name display on traders list and dashboard pages - Various position sync and trading improvements
392 lines
15 KiB
TypeScript
392 lines
15 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' },
|
||
// Trading leverage (exchange leverage)
|
||
tradingLeverage: { zh: '交易杠杆(交易所杠杆)', en: 'Trading Leverage (Exchange)' },
|
||
btcEthLeverage: { zh: 'BTC/ETH 交易杠杆', en: 'BTC/ETH Trading Leverage' },
|
||
btcEthLeverageDesc: { zh: '交易所开仓使用的杠杆倍数', en: 'Exchange leverage for opening positions' },
|
||
altcoinLeverage: { zh: '山寨币交易杠杆', en: 'Altcoin Trading Leverage' },
|
||
altcoinLeverageDesc: { zh: '交易所开仓使用的杠杆倍数', en: 'Exchange leverage for opening positions' },
|
||
// Position value ratio (risk control) - CODE ENFORCED
|
||
positionValueRatio: { zh: '仓位价值比例(代码强制)', en: 'Position Value Ratio (CODE ENFORCED)' },
|
||
positionValueRatioDesc: { zh: '单仓位名义价值 / 账户净值,由代码强制执行', en: 'Position notional value / equity, enforced by code' },
|
||
btcEthPositionValueRatio: { zh: 'BTC/ETH 仓位价值比例', en: 'BTC/ETH Position Value Ratio' },
|
||
btcEthPositionValueRatioDesc: { zh: '单仓最大名义价值 = 净值 × 此值(代码强制)', en: 'Max position value = equity × this ratio (CODE ENFORCED)' },
|
||
altcoinPositionValueRatio: { zh: '山寨币仓位价值比例', en: 'Altcoin Position Value Ratio' },
|
||
altcoinPositionValueRatioDesc: { zh: '单仓最大名义价值 = 净值 × 此值(代码强制)', en: 'Max position value = equity × this ratio (CODE ENFORCED)' },
|
||
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 (CODE ENFORCED)' },
|
||
maxMarginUsageDesc: { zh: '保证金使用率上限,由代码强制执行', en: 'Maximum margin utilization, enforced by code' },
|
||
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-1 gap-4 mb-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 ?? 3}
|
||
onChange={(e) =>
|
||
updateField('max_positions', parseInt(e.target.value) || 3)
|
||
}
|
||
disabled={disabled}
|
||
min={1}
|
||
max={10}
|
||
className="w-32 px-3 py-2 rounded"
|
||
style={{
|
||
background: '#1E2329',
|
||
border: '1px solid #2B3139',
|
||
color: '#EAECEF',
|
||
}}
|
||
/>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Trading Leverage (Exchange) */}
|
||
<div className="mb-2">
|
||
<p className="text-xs font-medium mb-2" style={{ color: '#F0B90B' }}>
|
||
{t('tradingLeverage')}
|
||
</p>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-4 mb-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('btcEthLeverage')}
|
||
</label>
|
||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||
{t('btcEthLeverageDesc')}
|
||
</p>
|
||
<div className="flex items-center gap-2">
|
||
<input
|
||
type="range"
|
||
value={config.btc_eth_max_leverage ?? 5}
|
||
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 ?? 5}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>
|
||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||
{t('altcoinLeverageDesc')}
|
||
</p>
|
||
<div className="flex items-center gap-2">
|
||
<input
|
||
type="range"
|
||
value={config.altcoin_max_leverage ?? 5}
|
||
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 ?? 5}x
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
{/* Position Value Ratio (Risk Control - CODE ENFORCED) */}
|
||
<div className="mb-2">
|
||
<p className="text-xs font-medium" style={{ color: '#0ECB81' }}>
|
||
{t('positionValueRatio')}
|
||
</p>
|
||
<p className="text-xs mt-1" style={{ color: '#848E9C' }}>
|
||
{t('positionValueRatioDesc')}
|
||
</p>
|
||
</div>
|
||
<div className="grid grid-cols-2 gap-4">
|
||
<div
|
||
className="p-4 rounded-lg"
|
||
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
|
||
>
|
||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||
{t('btcEthPositionValueRatio')}
|
||
</label>
|
||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||
{t('btcEthPositionValueRatioDesc')}
|
||
</p>
|
||
<div className="flex items-center gap-2">
|
||
<input
|
||
type="range"
|
||
value={config.btc_eth_max_position_value_ratio ?? 5}
|
||
onChange={(e) =>
|
||
updateField('btc_eth_max_position_value_ratio', parseFloat(e.target.value))
|
||
}
|
||
disabled={disabled}
|
||
min={0.5}
|
||
max={10}
|
||
step={0.5}
|
||
className="flex-1 accent-green-500"
|
||
/>
|
||
<span
|
||
className="w-12 text-center font-mono"
|
||
style={{ color: '#0ECB81' }}
|
||
>
|
||
{config.btc_eth_max_position_value_ratio ?? 5}x
|
||
</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div
|
||
className="p-4 rounded-lg"
|
||
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
|
||
>
|
||
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
|
||
{t('altcoinPositionValueRatio')}
|
||
</label>
|
||
<p className="text-xs mb-2" style={{ color: '#848E9C' }}>
|
||
{t('altcoinPositionValueRatioDesc')}
|
||
</p>
|
||
<div className="flex items-center gap-2">
|
||
<input
|
||
type="range"
|
||
value={config.altcoin_max_position_value_ratio ?? 1}
|
||
onChange={(e) =>
|
||
updateField('altcoin_max_position_value_ratio', parseFloat(e.target.value))
|
||
}
|
||
disabled={disabled}
|
||
min={0.5}
|
||
max={10}
|
||
step={0.5}
|
||
className="flex-1 accent-green-500"
|
||
/>
|
||
<span
|
||
className="w-12 text-center font-mono"
|
||
style={{ color: '#0ECB81' }}
|
||
>
|
||
{config.altcoin_max_position_value_ratio ?? 1}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-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('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 ?? 3}
|
||
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 #0ECB81' }}
|
||
>
|
||
<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 ?? 0.9) * 100}
|
||
onChange={(e) =>
|
||
updateField('max_margin_usage', parseInt(e.target.value) / 100)
|
||
}
|
||
disabled={disabled}
|
||
min={10}
|
||
max={100}
|
||
className="flex-1 accent-green-500"
|
||
/>
|
||
<span className="w-12 text-center font-mono" style={{ color: '#0ECB81' }}>
|
||
{Math.round((config.max_margin_usage ?? 0.9) * 100)}%
|
||
</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 ?? 12}
|
||
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 ?? 75}
|
||
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 ?? 75}
|
||
</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
)
|
||
}
|