Files
nofx/web/src/components/ExchangeIcons.tsx
0xYYBB | ZYY | Bobo 46ec8f1d04 feat(exchange): add Bybit Futures support (#1100)
* feat(exchange): add Bybit Futures support

- Add Bybit Go SDK dependency (github.com/bybit-exchange/bybit.go.api)
- Create trader/bybit_trader.go implementing Trader interface for USDT perpetual futures
- Update config/database.go to include Bybit in default exchanges
- Update manager/trader_manager.go to handle Bybit API key configuration
- Update trader/auto_trader.go to add BybitAPIKey/BybitSecretKey fields and bybit case
- Add Bybit icon to frontend ExchangeIcons.tsx

Bybit uses standard API Key/Secret Key authentication (similar to Binance).
Only USDT perpetual futures (category=linear) are supported.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: tinkle-community <tinklefund@gmail.com>

* test(bybit): add comprehensive unit tests for Bybit trader

- Add BybitTraderTestSuite following existing test patterns
- Interface compliance test (Trader interface)
- Symbol format validation tests
- FormatQuantity tests with 3-decimal precision
- API response parsing tests (success, error, permission denied)
- Position side conversion tests (Buy->long, Sell->short)
- Cache duration verification test
- Mock server integration tests for API endpoints

All 12 Bybit tests pass.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: tinkle-community <tinklefund@gmail.com>

* fix(frontend): add Bybit support to exchange config forms

修復前端對 Bybit 交易所的支持:
- 添加 Bybit 到 API Key/Secret Key 輸入欄位顯示邏輯
- 添加 Bybit 的表單驗證邏輯
- 修復 ExchangeConfigModal.tsx 和 AITradersPage.tsx

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: tinkle-community <tinklefund@gmail.com>

---------

Co-authored-by: the-dev-z <the-dev-z@users.noreply.github.com>
Co-authored-by: tinkle-community <tinklefund@gmail.com>
2025-11-23 19:23:53 +08:00

216 lines
5.8 KiB
TypeScript

import React from 'react'
interface IconProps {
width?: number
height?: number
className?: string
}
// Binance SVG 图标组件
const BinanceIcon: React.FC<IconProps> = ({
width = 24,
height = 24,
className,
}) => (
<svg
xmlns="http://www.w3.org/2000/svg"
width={width}
height={height}
viewBox="-52.785 -88 457.47 528"
className={className}
>
<path
d="M79.5 176l-39.7 39.7L0 176l39.7-39.7zM176 79.5l68.1 68.1 39.7-39.7L176 0 68.1 107.9l39.7 39.7zm136.2 56.8L272.5 176l39.7 39.7 39.7-39.7zM176 272.5l-68.1-68.1-39.7 39.7L176 352l107.8-107.9-39.7-39.7zm0-56.8l39.7-39.7-39.7-39.7-39.8 39.7z"
fill="#f0b90b"
/>
</svg>
)
// Hyperliquid SVG 图标组件
const HyperliquidIcon: React.FC<IconProps> = ({
width = 24,
height = 24,
className,
}) => (
<svg
width={width}
height={height}
viewBox="0 0 144 144"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M144 71.6991C144 119.306 114.866 134.582 99.5156 120.98C86.8804 109.889 83.1211 86.4521 64.116 84.0456C39.9942 81.0113 37.9057 113.133 22.0334 113.133C3.5504 113.133 0 86.2428 0 72.4315C0 58.3063 3.96809 39.0542 19.736 39.0542C38.1146 39.0542 39.1588 66.5722 62.132 65.1073C85.0007 63.5379 85.4184 34.8689 100.247 22.6271C113.195 12.0593 144 23.4641 144 71.6991Z"
fill="#97FCE4"
/>
</svg>
)
// Bybit SVG 图标组件
const BybitIcon: React.FC<IconProps> = ({
width = 24,
height = 24,
className,
}) => (
<svg
width={width}
height={height}
viewBox="0 0 200 200"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<path
d="M50.5 53.3H75.5L100 77.8V122.2L75.5 146.7H50.5V53.3Z"
fill="#F7A600"
/>
<path
d="M149.5 53.3H124.5L100 77.8V122.2L124.5 146.7H149.5V53.3Z"
fill="#F7A600"
/>
<path
d="M75.5 53.3H124.5V77.8H75.5V53.3Z"
fill="#F7A600"
/>
<path
d="M75.5 122.2H124.5V146.7H75.5V122.2Z"
fill="#F7A600"
/>
</svg>
)
// Aster SVG 图标组件
const AsterIcon: React.FC<IconProps> = ({
width = 24,
height = 24,
className,
}) => (
<svg
width={width}
height={height}
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<defs>
<linearGradient
id="paint0_linear_428_3535"
x1="18.9416"
y1="4.14314e-07"
x2="12.6408"
y2="32.0507"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F4D5B1" />
<stop offset="1" stopColor="#FFD29F" />
</linearGradient>
<linearGradient
id="paint1_linear_428_3535"
x1="18.9416"
y1="4.14314e-07"
x2="12.6408"
y2="32.0507"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F4D5B1" />
<stop offset="1" stopColor="#FFD29F" />
</linearGradient>
<linearGradient
id="paint2_linear_428_3535"
x1="18.9416"
y1="4.14314e-07"
x2="12.6408"
y2="32.0507"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F4D5B1" />
<stop offset="1" stopColor="#FFD29F" />
</linearGradient>
<linearGradient
id="paint3_linear_428_3535"
x1="18.9416"
y1="4.14314e-07"
x2="12.6408"
y2="32.0507"
gradientUnits="userSpaceOnUse"
>
<stop stopColor="#F4D5B1" />
</linearGradient>
</defs>
<path
d="M9.13309 30.4398L9.88315 26.9871C10.7197 23.1362 7.77521 19.4988 3.82118 19.4988H0.385363C1.4689 24.3374 4.75127 28.3496 9.13309 30.4398Z"
fill="url(#paint0_linear_428_3535)"
/>
<path
d="M10.64 31.0663C12.3326 31.6707 14.1567 32 16.0579 32C23.7199 32 30.1285 26.6527 31.7305 19.4988H21.249C16.5244 19.4988 12.4396 22.7824 11.44 27.3838L10.64 31.0663Z"
fill="url(#paint1_linear_428_3535)"
/>
<path
d="M32.0038 17.8987C32.0778 17.2756 32.1159 16.6415 32.1159 15.9985C32.1159 7.60402 25.629 0.719287 17.3779 0.0503251L15.1273 10.4105C14.2907 14.2614 17.2352 17.8987 21.1892 17.8987H32.0038Z"
fill="url(#paint2_linear_428_3535)"
/>
<path
d="M15.7459 0C7.02134 0.165717 0 7.26504 0 15.9985C0 16.6415 0.0380539 17.2756 0.112041 17.8987H3.76146C8.48603 17.8987 12.5709 14.6151 13.5705 10.0137L15.7459 0Z"
fill="url(#paint3_linear_428_3535)"
/>
</svg>
)
// 获取交易所图标的函数
export const getExchangeIcon = (
exchangeType: string,
props: IconProps = {}
) => {
// 支持完整ID或类型名
const type = exchangeType.toLowerCase().includes('binance')
? 'binance'
: exchangeType.toLowerCase().includes('bybit')
? 'bybit'
: exchangeType.toLowerCase().includes('hyperliquid')
? 'hyperliquid'
: exchangeType.toLowerCase().includes('aster')
? 'aster'
: exchangeType.toLowerCase()
const iconProps = {
width: props.width || 24,
height: props.height || 24,
className: props.className,
}
switch (type) {
case 'binance':
return <BinanceIcon {...iconProps} />
case 'bybit':
return <BybitIcon {...iconProps} />
case 'hyperliquid':
case 'dex':
return <HyperliquidIcon {...iconProps} />
case 'aster':
return <AsterIcon {...iconProps} />
case 'cex':
default:
return (
<div
className={props.className}
style={{
width: props.width || 24,
height: props.height || 24,
borderRadius: '50%',
background: '#2B3139',
display: 'flex',
alignItems: 'center',
justifyContent: 'center',
fontSize: '12px',
fontWeight: 'bold',
color: '#EAECEF',
}}
>
{type[0]?.toUpperCase() || '?'}
</div>
)
}
}