mirror of
https://github.com/NoFxAiOS/nofx.git
synced 2026-07-11 06:46:59 +08:00
feat: improve order sync and add xyz dex trigger orders
- Add incremental sync for Binance trades using COMMISSION detection and fromId - Add stop loss and take profit order support for xyz dex assets - Add pagination for current positions and position history in UI - Fix chart market type auto-selection based on exchange
This commit is contained in:
440
web/src/App.tsx
440
web/src/App.tsx
@@ -799,6 +799,23 @@ function TraderDetailsPage({
|
||||
const [showWalletAddress, setShowWalletAddress] = useState<boolean>(false)
|
||||
const [copiedAddress, setCopiedAddress] = useState<boolean>(false)
|
||||
|
||||
// Current positions pagination
|
||||
const [positionsPageSize, setPositionsPageSize] = useState<number>(20)
|
||||
const [positionsCurrentPage, setPositionsCurrentPage] = useState<number>(1)
|
||||
|
||||
// Calculate paginated positions
|
||||
const totalPositions = positions?.length || 0
|
||||
const totalPositionPages = Math.ceil(totalPositions / positionsPageSize)
|
||||
const paginatedPositions = positions?.slice(
|
||||
(positionsCurrentPage - 1) * positionsPageSize,
|
||||
positionsCurrentPage * positionsPageSize
|
||||
) || []
|
||||
|
||||
// Reset page when positions change
|
||||
useEffect(() => {
|
||||
setPositionsCurrentPage(1)
|
||||
}, [selectedTraderId, positionsPageSize])
|
||||
|
||||
// Get current exchange info for perp-dex wallet display
|
||||
const currentExchange = exchanges?.find(
|
||||
(e) => e.id === selectedTrader?.exchange_id
|
||||
@@ -1250,180 +1267,269 @@ function TraderDetailsPage({
|
||||
)}
|
||||
</div>
|
||||
{positions && positions.length > 0 ? (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="text-left border-b border-gray-800">
|
||||
<tr>
|
||||
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-left">
|
||||
{t('symbol', language)}
|
||||
</th>
|
||||
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-center">
|
||||
{t('side', language)}
|
||||
</th>
|
||||
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-center">
|
||||
{language === 'zh' ? '操作' : 'Action'}
|
||||
</th>
|
||||
<th
|
||||
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right"
|
||||
title={t('entryPrice', language)}
|
||||
>
|
||||
{language === 'zh' ? '入场价' : 'Entry'}
|
||||
</th>
|
||||
<th
|
||||
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right"
|
||||
title={t('markPrice', language)}
|
||||
>
|
||||
{language === 'zh' ? '标记价' : 'Mark'}
|
||||
</th>
|
||||
<th
|
||||
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right"
|
||||
title={t('quantity', language)}
|
||||
>
|
||||
{language === 'zh' ? '数量' : 'Qty'}
|
||||
</th>
|
||||
<th
|
||||
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right"
|
||||
title={t('positionValue', language)}
|
||||
>
|
||||
{language === 'zh' ? '价值' : 'Value'}
|
||||
</th>
|
||||
<th
|
||||
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-center"
|
||||
title={t('leverage', language)}
|
||||
>
|
||||
{language === 'zh' ? '杠杆' : 'Lev.'}
|
||||
</th>
|
||||
<th
|
||||
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right"
|
||||
title={t('unrealizedPnL', language)}
|
||||
>
|
||||
{language === 'zh' ? '未实现盈亏' : 'uPnL'}
|
||||
</th>
|
||||
<th
|
||||
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right"
|
||||
title={t('liqPrice', language)}
|
||||
>
|
||||
{language === 'zh' ? '强平价' : 'Liq.'}
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{positions.map((pos, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
className="border-b border-gray-800 last:border-0 transition-colors hover:bg-opacity-10 hover:bg-yellow-500 cursor-pointer"
|
||||
onClick={() => {
|
||||
setSelectedChartSymbol(pos.symbol)
|
||||
setChartUpdateKey(Date.now())
|
||||
// Smooth scroll to chart with ref
|
||||
if (chartSectionRef.current) {
|
||||
chartSectionRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<td className="px-1 py-3 font-mono font-semibold whitespace-nowrap text-left">
|
||||
{pos.symbol}
|
||||
</td>
|
||||
<td className="px-1 py-3 whitespace-nowrap text-center">
|
||||
<span
|
||||
className="px-1.5 py-0.5 rounded text-[10px] font-bold"
|
||||
style={
|
||||
pos.side === 'long'
|
||||
? {
|
||||
background: 'rgba(14, 203, 129, 0.1)',
|
||||
color: '#0ECB81',
|
||||
}
|
||||
: {
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
color: '#F6465D',
|
||||
}
|
||||
}
|
||||
>
|
||||
{t(
|
||||
pos.side === 'long' ? 'long' : 'short',
|
||||
language
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-1 py-3 whitespace-nowrap text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation() // Prevent row click
|
||||
handleClosePosition(
|
||||
pos.symbol,
|
||||
pos.side.toUpperCase()
|
||||
)
|
||||
}}
|
||||
disabled={closingPosition === pos.symbol}
|
||||
className="btn-danger inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed mx-auto"
|
||||
title={
|
||||
language === 'zh' ? '平仓' : 'Close Position'
|
||||
}
|
||||
>
|
||||
{closingPosition === pos.symbol ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<LogOut className="w-3 h-3" />
|
||||
)}
|
||||
{language === 'zh' ? '平仓' : 'Close'}
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
className="px-1 py-3 font-mono whitespace-nowrap text-right"
|
||||
style={{ color: '#EAECEF' }}
|
||||
<div>
|
||||
<div className="overflow-x-auto">
|
||||
<table className="w-full text-xs">
|
||||
<thead className="text-left border-b border-gray-800">
|
||||
<tr>
|
||||
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-left">
|
||||
{t('symbol', language)}
|
||||
</th>
|
||||
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-center">
|
||||
{t('side', language)}
|
||||
</th>
|
||||
<th className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-center">
|
||||
{language === 'zh' ? '操作' : 'Action'}
|
||||
</th>
|
||||
<th
|
||||
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right"
|
||||
title={t('entryPrice', language)}
|
||||
>
|
||||
{pos.entry_price.toFixed(4)}
|
||||
</td>
|
||||
<td
|
||||
className="px-1 py-3 font-mono whitespace-nowrap text-right"
|
||||
style={{ color: '#EAECEF' }}
|
||||
{language === 'zh' ? '入场价' : 'Entry'}
|
||||
</th>
|
||||
<th
|
||||
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right"
|
||||
title={t('markPrice', language)}
|
||||
>
|
||||
{pos.mark_price.toFixed(4)}
|
||||
</td>
|
||||
<td
|
||||
className="px-1 py-3 font-mono whitespace-nowrap text-right"
|
||||
style={{ color: '#EAECEF' }}
|
||||
{language === 'zh' ? '标记价' : 'Mark'}
|
||||
</th>
|
||||
<th
|
||||
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right"
|
||||
title={t('quantity', language)}
|
||||
>
|
||||
{pos.quantity.toFixed(4)}
|
||||
</td>
|
||||
<td
|
||||
className="px-1 py-3 font-mono font-bold whitespace-nowrap text-right"
|
||||
style={{ color: '#EAECEF' }}
|
||||
{language === 'zh' ? '数量' : 'Qty'}
|
||||
</th>
|
||||
<th
|
||||
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right"
|
||||
title={t('positionValue', language)}
|
||||
>
|
||||
{(pos.quantity * pos.mark_price).toFixed(2)}
|
||||
</td>
|
||||
<td
|
||||
className="px-1 py-3 font-mono whitespace-nowrap text-center"
|
||||
style={{ color: '#F0B90B' }}
|
||||
{language === 'zh' ? '价值' : 'Value'}
|
||||
</th>
|
||||
<th
|
||||
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-center"
|
||||
title={t('leverage', language)}
|
||||
>
|
||||
{pos.leverage}x
|
||||
</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right">
|
||||
<span
|
||||
style={{
|
||||
color:
|
||||
pos.unrealized_pnl >= 0 ? '#0ECB81' : '#F6465D',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
{pos.unrealized_pnl >= 0 ? '+' : ''}
|
||||
{pos.unrealized_pnl.toFixed(2)}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
className="px-1 py-3 font-mono whitespace-nowrap text-right"
|
||||
style={{ color: '#848E9C' }}
|
||||
{language === 'zh' ? '杠杆' : 'Lev.'}
|
||||
</th>
|
||||
<th
|
||||
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right"
|
||||
title={t('unrealizedPnL', language)}
|
||||
>
|
||||
{pos.liquidation_price.toFixed(4)}
|
||||
</td>
|
||||
{language === 'zh' ? '未实现盈亏' : 'uPnL'}
|
||||
</th>
|
||||
<th
|
||||
className="px-1 pb-3 font-semibold text-gray-400 whitespace-nowrap text-right"
|
||||
title={t('liqPrice', language)}
|
||||
>
|
||||
{language === 'zh' ? '强平价' : 'Liq.'}
|
||||
</th>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paginatedPositions.map((pos, i) => (
|
||||
<tr
|
||||
key={i}
|
||||
className="border-b border-gray-800 last:border-0 transition-colors hover:bg-opacity-10 hover:bg-yellow-500 cursor-pointer"
|
||||
onClick={() => {
|
||||
setSelectedChartSymbol(pos.symbol)
|
||||
setChartUpdateKey(Date.now())
|
||||
// Smooth scroll to chart with ref
|
||||
if (chartSectionRef.current) {
|
||||
chartSectionRef.current.scrollIntoView({
|
||||
behavior: 'smooth',
|
||||
block: 'start',
|
||||
})
|
||||
}
|
||||
}}
|
||||
>
|
||||
<td className="px-1 py-3 font-mono font-semibold whitespace-nowrap text-left">
|
||||
{pos.symbol}
|
||||
</td>
|
||||
<td className="px-1 py-3 whitespace-nowrap text-center">
|
||||
<span
|
||||
className="px-1.5 py-0.5 rounded text-[10px] font-bold"
|
||||
style={
|
||||
pos.side === 'long'
|
||||
? {
|
||||
background: 'rgba(14, 203, 129, 0.1)',
|
||||
color: '#0ECB81',
|
||||
}
|
||||
: {
|
||||
background: 'rgba(246, 70, 93, 0.1)',
|
||||
color: '#F6465D',
|
||||
}
|
||||
}
|
||||
>
|
||||
{t(
|
||||
pos.side === 'long' ? 'long' : 'short',
|
||||
language
|
||||
)}
|
||||
</span>
|
||||
</td>
|
||||
<td className="px-1 py-3 whitespace-nowrap text-center">
|
||||
<button
|
||||
type="button"
|
||||
onClick={(e) => {
|
||||
e.stopPropagation() // Prevent row click
|
||||
handleClosePosition(
|
||||
pos.symbol,
|
||||
pos.side.toUpperCase()
|
||||
)
|
||||
}}
|
||||
disabled={closingPosition === pos.symbol}
|
||||
className="btn-danger inline-flex items-center gap-1 px-1.5 py-0.5 rounded text-[10px] font-semibold transition-all hover:scale-105 disabled:opacity-50 disabled:cursor-not-allowed mx-auto"
|
||||
title={
|
||||
language === 'zh' ? '平仓' : 'Close Position'
|
||||
}
|
||||
>
|
||||
{closingPosition === pos.symbol ? (
|
||||
<Loader2 className="w-3 h-3 animate-spin" />
|
||||
) : (
|
||||
<LogOut className="w-3 h-3" />
|
||||
)}
|
||||
{language === 'zh' ? '平仓' : 'Close'}
|
||||
</button>
|
||||
</td>
|
||||
<td
|
||||
className="px-1 py-3 font-mono whitespace-nowrap text-right"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{pos.entry_price.toFixed(4)}
|
||||
</td>
|
||||
<td
|
||||
className="px-1 py-3 font-mono whitespace-nowrap text-right"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{pos.mark_price.toFixed(4)}
|
||||
</td>
|
||||
<td
|
||||
className="px-1 py-3 font-mono whitespace-nowrap text-right"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{pos.quantity.toFixed(4)}
|
||||
</td>
|
||||
<td
|
||||
className="px-1 py-3 font-mono font-bold whitespace-nowrap text-right"
|
||||
style={{ color: '#EAECEF' }}
|
||||
>
|
||||
{(pos.quantity * pos.mark_price).toFixed(2)}
|
||||
</td>
|
||||
<td
|
||||
className="px-1 py-3 font-mono whitespace-nowrap text-center"
|
||||
style={{ color: '#F0B90B' }}
|
||||
>
|
||||
{pos.leverage}x
|
||||
</td>
|
||||
<td className="px-1 py-3 font-mono whitespace-nowrap text-right">
|
||||
<span
|
||||
style={{
|
||||
color:
|
||||
pos.unrealized_pnl >= 0 ? '#0ECB81' : '#F6465D',
|
||||
fontWeight: 'bold',
|
||||
}}
|
||||
>
|
||||
{pos.unrealized_pnl >= 0 ? '+' : ''}
|
||||
{pos.unrealized_pnl.toFixed(2)}
|
||||
</span>
|
||||
</td>
|
||||
<td
|
||||
className="px-1 py-3 font-mono whitespace-nowrap text-right"
|
||||
style={{ color: '#848E9C' }}
|
||||
>
|
||||
{pos.liquidation_price.toFixed(4)}
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
{/* Pagination footer - only show when there are many positions */}
|
||||
{totalPositions > 10 && (
|
||||
<div
|
||||
className="flex flex-wrap items-center justify-between gap-3 pt-4 mt-4 text-xs"
|
||||
style={{ borderTop: '1px solid #2B3139', color: '#848E9C' }}
|
||||
>
|
||||
<span>
|
||||
{language === 'zh'
|
||||
? `显示 ${paginatedPositions.length} / ${totalPositions} 个持仓`
|
||||
: `Showing ${paginatedPositions.length} of ${totalPositions} positions`}
|
||||
</span>
|
||||
<div className="flex items-center gap-3">
|
||||
{/* Page size selector */}
|
||||
<div className="flex items-center gap-2">
|
||||
<span style={{ color: '#848E9C' }}>
|
||||
{language === 'zh' ? '每页' : 'Per page'}:
|
||||
</span>
|
||||
<select
|
||||
value={positionsPageSize}
|
||||
onChange={(e) => setPositionsPageSize(Number(e.target.value))}
|
||||
className="rounded px-2 py-1 text-xs"
|
||||
style={{
|
||||
background: '#0B0E11',
|
||||
border: '1px solid #2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
>
|
||||
<option value={20}>20</option>
|
||||
<option value={50}>50</option>
|
||||
<option value={100}>100</option>
|
||||
</select>
|
||||
</div>
|
||||
{/* Page navigation */}
|
||||
{totalPositionPages > 1 && (
|
||||
<div className="flex items-center gap-1">
|
||||
<button
|
||||
onClick={() => setPositionsCurrentPage(1)}
|
||||
disabled={positionsCurrentPage === 1}
|
||||
className="px-2 py-1 rounded transition-colors disabled:opacity-30"
|
||||
style={{
|
||||
background: positionsCurrentPage === 1 ? 'transparent' : '#2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
>
|
||||
«
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPositionsCurrentPage((p) => Math.max(1, p - 1))}
|
||||
disabled={positionsCurrentPage === 1}
|
||||
className="px-2 py-1 rounded transition-colors disabled:opacity-30"
|
||||
style={{
|
||||
background: positionsCurrentPage === 1 ? 'transparent' : '#2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
>
|
||||
‹
|
||||
</button>
|
||||
<span className="px-3" style={{ color: '#EAECEF' }}>
|
||||
{positionsCurrentPage} / {totalPositionPages}
|
||||
</span>
|
||||
<button
|
||||
onClick={() => setPositionsCurrentPage((p) => Math.min(totalPositionPages, p + 1))}
|
||||
disabled={positionsCurrentPage === totalPositionPages}
|
||||
className="px-2 py-1 rounded transition-colors disabled:opacity-30"
|
||||
style={{
|
||||
background: positionsCurrentPage === totalPositionPages ? 'transparent' : '#2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
>
|
||||
›
|
||||
</button>
|
||||
<button
|
||||
onClick={() => setPositionsCurrentPage(totalPositionPages)}
|
||||
disabled={positionsCurrentPage === totalPositionPages}
|
||||
className="px-2 py-1 rounded transition-colors disabled:opacity-30"
|
||||
style={{
|
||||
background: positionsCurrentPage === totalPositionPages ? 'transparent' : '#2B3139',
|
||||
color: '#EAECEF',
|
||||
}}
|
||||
>
|
||||
»
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-center py-16" style={{ color: '#848E9C' }}>
|
||||
|
||||
Reference in New Issue
Block a user