fix: initial balance calculation and UI improvements

- 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
This commit is contained in:
tinkle-community
2025-12-10 14:40:08 +08:00
parent c19ee51dee
commit 319ccb8ca3
45 changed files with 2951 additions and 3392 deletions

View File

@@ -807,7 +807,7 @@ function TraderDetailsPage({
)}
</div>
<div
className="flex items-center gap-4 text-sm"
className="flex items-center gap-4 text-sm flex-wrap"
style={{ color: '#848E9C' }}
>
<span>
@@ -826,6 +826,20 @@ function TraderDetailsPage({
)}
</span>
</span>
<span></span>
<span>
Exchange:{' '}
<span className="font-semibold" style={{ color: '#EAECEF' }}>
{selectedTrader.exchange_id?.toUpperCase() || 'N/A'}
</span>
</span>
<span></span>
<span>
Strategy:{' '}
<span className="font-semibold" style={{ color: '#F0B90B' }}>
{selectedTrader.strategy_name || 'No Strategy'}
</span>
</span>
{status && (
<>
<span></span>

View File

@@ -368,7 +368,7 @@ export function TraderConfigModal({
selectedStrategy.config.coin_source.source_type === 'oi_top' ? 'OI Top' : '混合'}
</div>
<div>
: {((selectedStrategy.config.risk_control?.max_position_ratio || 0.3) * 100).toFixed(0)}%
: {((selectedStrategy.config.risk_control?.max_margin_usage || 0.9) * 100).toFixed(0)}%
</div>
</div>
</div>

View File

@@ -211,10 +211,10 @@ export function CoinSourceEditor({
</span>
<input
type="number"
value={config.coin_pool_limit || 30}
value={config.coin_pool_limit || 10}
onChange={(e) =>
!disabled &&
onChange({ ...config, coin_pool_limit: parseInt(e.target.value) || 30 })
onChange({ ...config, coin_pool_limit: parseInt(e.target.value) || 10 })
}
disabled={disabled}
min={1}

View File

@@ -19,15 +19,24 @@ export function RiskControlEditor({
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' },
// 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' },
maxMarginUsageDesc: { zh: '保证金使用率上限', en: 'Maximum margin utilization' },
maxPositionRatio: { zh: '单币最大仓位比', en: 'Max Position Ratio' },
maxPositionRatioDesc: { zh: '相对账户净值的倍数', en: 'Multiple of account equity' },
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' },
@@ -57,7 +66,7 @@ export function RiskControlEditor({
</h3>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="grid grid-cols-1 gap-4 mb-4">
<div
className="p-4 rounded-lg"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
@@ -70,14 +79,14 @@ export function RiskControlEditor({
</p>
<input
type="number"
value={config.max_positions}
value={config.max_positions ?? 3}
onChange={(e) =>
updateField('max_positions', parseInt(e.target.value) || 3)
}
disabled={disabled}
min={1}
max={10}
className="w-full px-3 py-2 rounded"
className="w-32 px-3 py-2 rounded"
style={{
background: '#1E2329',
border: '1px solid #2B3139',
@@ -85,7 +94,15 @@ export function RiskControlEditor({
}}
/>
</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' }}
@@ -93,10 +110,13 @@ export function RiskControlEditor({
<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}
value={config.btc_eth_max_leverage ?? 5}
onChange={(e) =>
updateField('btc_eth_max_leverage', parseInt(e.target.value))
}
@@ -109,7 +129,7 @@ export function RiskControlEditor({
className="w-12 text-center font-mono"
style={{ color: '#F0B90B' }}
>
{config.btc_eth_max_leverage}x
{config.btc_eth_max_leverage ?? 5}x
</span>
</div>
</div>
@@ -121,10 +141,13 @@ export function RiskControlEditor({
<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}
value={config.altcoin_max_leverage ?? 5}
onChange={(e) =>
updateField('altcoin_max_leverage', parseInt(e.target.value))
}
@@ -137,7 +160,82 @@ export function RiskControlEditor({
className="w-12 text-center font-mono"
style={{ color: '#F0B90B' }}
>
{config.altcoin_max_leverage}x
{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>
@@ -153,7 +251,7 @@ export function RiskControlEditor({
</h3>
</div>
<div className="grid grid-cols-3 gap-4">
<div className="grid grid-cols-2 gap-4">
<div
className="p-4 rounded-lg"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
@@ -168,7 +266,7 @@ export function RiskControlEditor({
<span style={{ color: '#848E9C' }}>1:</span>
<input
type="number"
value={config.min_risk_reward_ratio}
value={config.min_risk_reward_ratio ?? 3}
onChange={(e) =>
updateField('min_risk_reward_ratio', parseFloat(e.target.value) || 3)
}
@@ -188,7 +286,7 @@ export function RiskControlEditor({
<div
className="p-4 rounded-lg"
style={{ background: '#0B0E11', border: '1px solid #2B3139' }}
style={{ background: '#0B0E11', border: '1px solid #0ECB81' }}
>
<label className="block text-sm mb-1" style={{ color: '#EAECEF' }}>
{t('maxMarginUsage')}
@@ -199,51 +297,17 @@ export function RiskControlEditor({
<div className="flex items-center gap-2">
<input
type="range"
value={config.max_margin_usage * 100}
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-red-500"
className="flex-1 accent-green-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 className="w-12 text-center font-mono" style={{ color: '#0ECB81' }}>
{Math.round((config.max_margin_usage ?? 0.9) * 100)}%
</span>
</div>
</div>
@@ -273,7 +337,7 @@ export function RiskControlEditor({
<div className="flex items-center">
<input
type="number"
value={config.min_position_size}
value={config.min_position_size ?? 12}
onChange={(e) =>
updateField('min_position_size', parseFloat(e.target.value) || 12)
}
@@ -306,7 +370,7 @@ export function RiskControlEditor({
<div className="flex items-center gap-2">
<input
type="range"
value={config.min_confidence}
value={config.min_confidence ?? 75}
onChange={(e) =>
updateField('min_confidence', parseInt(e.target.value))
}
@@ -316,7 +380,7 @@ export function RiskControlEditor({
className="flex-1 accent-green-500"
/>
<span className="w-12 text-center font-mono" style={{ color: '#0ECB81' }}>
{config.min_confidence}
{config.min_confidence ?? 75}
</span>
</div>
</div>

View File

@@ -692,12 +692,42 @@ export function ExchangeConfigModal({
{/* Aster 交易所的字段 */}
{selectedExchange.id === 'aster' && (
<>
{/* API Pro 代理钱包说明 banner */}
<div
className="p-3 rounded mb-4"
style={{
background: 'rgba(240, 185, 11, 0.1)',
border: '1px solid rgba(240, 185, 11, 0.3)',
}}
>
<div className="flex items-start gap-2">
<span style={{ color: '#F0B90B', fontSize: '16px' }}>
🔐
</span>
<div className="flex-1">
<div
className="text-sm font-semibold mb-1"
style={{ color: '#F0B90B' }}
>
{t('asterApiProTitle', language)}
</div>
<div
className="text-xs"
style={{ color: '#848E9C', lineHeight: '1.5' }}
>
{t('asterApiProDesc', language)}
</div>
</div>
</div>
</div>
{/* 主钱包地址 */}
<div>
<label
className="block text-sm font-semibold mb-2 flex items-center gap-2"
style={{ color: '#EAECEF' }}
>
{t('user', language)}
{t('asterUserLabel', language)}
<Tooltip content={t('asterUserDesc', language)}>
<HelpCircle
className="w-4 h-4 cursor-help"
@@ -709,7 +739,7 @@ export function ExchangeConfigModal({
type="text"
value={asterUser}
onChange={(e) => setAsterUser(e.target.value)}
placeholder={t('enterUser', language)}
placeholder={t('enterAsterUser', language)}
className="w-full px-3 py-2 rounded"
style={{
background: '#0B0E11',
@@ -718,14 +748,21 @@ export function ExchangeConfigModal({
}}
required
/>
<div
className="text-xs mt-1"
style={{ color: '#848E9C' }}
>
{t('asterUserDesc', language)}
</div>
</div>
{/* API Pro 代理钱包地址 */}
<div>
<label
className="block text-sm font-semibold mb-2 flex items-center gap-2"
style={{ color: '#EAECEF' }}
>
{t('signer', language)}
{t('asterSignerLabel', language)}
<Tooltip content={t('asterSignerDesc', language)}>
<HelpCircle
className="w-4 h-4 cursor-help"
@@ -737,7 +774,7 @@ export function ExchangeConfigModal({
type="text"
value={asterSigner}
onChange={(e) => setAsterSigner(e.target.value)}
placeholder={t('enterSigner', language)}
placeholder={t('enterAsterSigner', language)}
className="w-full px-3 py-2 rounded"
style={{
background: '#0B0E11',
@@ -746,14 +783,21 @@ export function ExchangeConfigModal({
}}
required
/>
<div
className="text-xs mt-1"
style={{ color: '#848E9C' }}
>
{t('asterSignerDesc', language)}
</div>
</div>
{/* API Pro 代理钱包私钥 */}
<div>
<label
className="block text-sm font-semibold mb-2 flex items-center gap-2"
style={{ color: '#EAECEF' }}
>
{t('privateKey', language)}
{t('asterPrivateKeyLabel', language)}
<Tooltip content={t('asterPrivateKeyDesc', language)}>
<HelpCircle
className="w-4 h-4 cursor-help"
@@ -765,7 +809,7 @@ export function ExchangeConfigModal({
type="password"
value={asterPrivateKey}
onChange={(e) => setAsterPrivateKey(e.target.value)}
placeholder={t('enterPrivateKey', language)}
placeholder={t('enterAsterPrivateKey', language)}
className="w-full px-3 py-2 rounded"
style={{
background: '#0B0E11',
@@ -774,6 +818,12 @@ export function ExchangeConfigModal({
}}
required
/>
<div
className="text-xs mt-1"
style={{ color: '#848E9C' }}
>
{t('asterPrivateKeyDesc', language)}
</div>
</div>
</>
)}

View File

@@ -75,6 +75,7 @@ export function TradersGrid({
trader.ai_model.split('_').pop() || trader.ai_model
)}{' '}
Model {trader.exchange_id?.toUpperCase()}
<span style={{ color: '#F0B90B' }}> {trader.strategy_name || 'No Strategy'}</span>
</div>
</div>
</div>

View File

@@ -352,14 +352,24 @@ export const translations = {
enterHyperliquidMainWalletAddress: 'Enter Main wallet address',
hyperliquidMainWalletAddressDesc:
'Main wallet address that holds your trading funds (never expose its private key)',
// Aster API Pro Configuration
asterApiProTitle: 'Aster API Pro Wallet Configuration',
asterApiProDesc:
'Use API Pro wallet for secure trading: API wallet signs transactions, main wallet holds funds (never expose main wallet private key)',
asterUserDesc:
'Main wallet address - The EVM wallet address you use to log in to Aster (Note: Only EVM wallets are supported, Solana wallets are not supported)',
'Main wallet address - The EVM wallet address you use to log in to Aster (Note: Only EVM wallets are supported)',
asterSignerDesc:
'API wallet address - Generate from https://www.asterdex.com/en/api-wallet',
'API Pro wallet address (0x...) - Generate from https://www.asterdex.com/en/api-wallet',
asterPrivateKeyDesc:
'API wallet private key - Get from https://www.asterdex.com/en/api-wallet (only used locally for signing, never transmitted)',
'API Pro wallet private key - Get from https://www.asterdex.com/en/api-wallet (only used locally for signing, never transmitted)',
asterUsdtWarning:
'Important: Aster only tracks USDT balance. Please ensure you use USDT as margin currency to avoid P&L calculation errors caused by price fluctuations of other assets (BNB, ETH, etc.)',
asterUserLabel: 'Main Wallet Address',
asterSignerLabel: 'API Pro Wallet Address',
asterPrivateKeyLabel: 'API Pro Wallet Private Key',
enterAsterUser: 'Enter main wallet address (0x...)',
enterAsterSigner: 'Enter API Pro wallet address (0x...)',
enterAsterPrivateKey: 'Enter API Pro wallet private key',
// LIGHTER Configuration
lighterWalletAddress: 'L1 Wallet Address',
@@ -1347,14 +1357,24 @@ export const translations = {
enterHyperliquidMainWalletAddress: '输入主钱包地址',
hyperliquidMainWalletAddressDesc:
'持有交易资金的主钱包地址(永不暴露其私钥)',
// Aster API Pro 配置
asterApiProTitle: 'Aster API Pro 代理钱包配置',
asterApiProDesc:
'使用 API Pro 代理钱包安全交易:代理钱包用于签名交易,主钱包持有资金(永不暴露主钱包私钥)',
asterUserDesc:
'主钱包地址 - 您用于登录 Aster 的 EVM 钱包地址(注意:仅支持 EVM 钱包,不支持 Solana 钱包)',
'主钱包地址 - 您用于登录 Aster 的 EVM 钱包地址(仅支持 EVM 钱包)',
asterSignerDesc:
'API 钱包地址 - 从 https://www.asterdex.com/zh-CN/api-wallet 生成',
'API Pro 代理钱包地址 (0x...) - 从 https://www.asterdex.com/zh-CN/api-wallet 生成',
asterPrivateKeyDesc:
'API 钱包私钥 - 从 https://www.asterdex.com/zh-CN/api-wallet 获取(仅在本地用于签名,不会被传输)',
'API Pro 代理钱包私钥 - 从 https://www.asterdex.com/zh-CN/api-wallet 获取(仅在本地用于签名,不会被传输)',
asterUsdtWarning:
'重要提示Aster 仅统计 USDT 余额。请确保您使用 USDT 作为保证金币种避免其他资产BNB、ETH等的价格波动导致盈亏统计错误',
asterUserLabel: '主钱包地址',
asterSignerLabel: 'API Pro 代理钱包地址',
asterPrivateKeyLabel: 'API Pro 代理钱包私钥',
enterAsterUser: '输入主钱包地址 (0x...)',
enterAsterSigner: '输入 API Pro 代理钱包地址 (0x...)',
enterAsterPrivateKey: '输入 API Pro 代理钱包私钥',
// LIGHTER 配置
lighterWalletAddress: 'L1 錢包地址',

View File

@@ -91,6 +91,8 @@ export interface TraderInfo {
ai_model: string
exchange_id?: string
is_running?: boolean
strategy_id?: string
strategy_name?: string
custom_prompt?: string
use_coin_pool?: boolean
use_oi_top?: boolean
@@ -437,12 +439,21 @@ export interface ExternalDataSource {
}
export interface RiskControlConfig {
// Max number of coins held simultaneously (CODE ENFORCED)
max_positions: number;
btc_eth_max_leverage: number;
altcoin_max_leverage: number;
min_risk_reward_ratio: number;
max_margin_usage: number;
max_position_ratio: number;
min_position_size: number;
min_confidence: number;
// Trading Leverage - exchange leverage for opening positions (AI guided)
btc_eth_max_leverage: number; // BTC/ETH max exchange leverage
altcoin_max_leverage: number; // Altcoin max exchange leverage
// Position Value Ratio - single position notional value / account equity (CODE ENFORCED)
// Max position value = equity × this ratio
btc_eth_max_position_value_ratio?: number; // default: 5 (BTC/ETH max position = 5x equity)
altcoin_max_position_value_ratio?: number; // default: 1 (Altcoin max position = 1x equity)
// Risk Parameters
max_margin_usage: number; // Max margin utilization, e.g. 0.9 = 90% (CODE ENFORCED)
min_position_size: number; // Min position size in USDT (CODE ENFORCED)
min_risk_reward_ratio: number; // Min take_profit / stop_loss ratio (AI guided)
min_confidence: number; // Min AI confidence to open position (AI guided)
}