feat: add Bitget futures trading support

- Add BitgetTrader with full trading implementation
- Support one-way position mode with proper API parameters
- Add Bitget to all exchange switch statements
- Update exchange icons (Bybit, OKX, Bitget, Lighter)
- Add Bitget to frontend exchange config modal
This commit is contained in:
tinkle-community
2025-12-12 18:59:09 +08:00
parent 76574aacb2
commit dcc16fec82
10 changed files with 1253 additions and 39 deletions

View File

@@ -22,7 +22,7 @@
### Core Features
- **Multi-AI Support**: Run DeepSeek, Qwen, GPT, Claude, Gemini, Grok, Kimi - switch models anytime
- **Multi-Exchange**: Trade on Binance, Bybit, OKX, Hyperliquid, Aster DEX, Lighter from one platform
- **Multi-Exchange**: Trade on Binance, Bybit, OKX, Bitget, Hyperliquid, Aster DEX, Lighter from one platform
- **Strategy Studio**: Visual strategy builder with coin sources, indicators, and risk controls
- **AI Debate Arena**: Multiple AI models debate trading decisions with different roles (Bull, Bear, Analyst)
- **AI Competition Mode**: Multiple AI traders compete in real-time, track performance side by side
@@ -90,6 +90,7 @@ Join our Telegram developer community: **[NOFX Developer Community](https://t.me
| **Binance** | ✅ Supported | [Register](https://www.binance.com/join?ref=NOFXENG) |
| **Bybit** | ✅ Supported | [Register](https://partner.bybit.com/b/83856) |
| **OKX** | ✅ Supported | [Register](https://www.okx.com/join/1865360) |
| **Bitget** | ✅ Supported | [Register](https://www.bitget.com/referral/register?from=referral&clacCode=c8a43172) |
### Perp-DEX (Decentralized Perpetual Exchanges)

View File

@@ -581,6 +581,12 @@ func (s *Server) handleCreateTrader(c *gin.Context) {
exchangeCfg.SecretKey,
exchangeCfg.Passphrase,
)
case "bitget":
tempTrader = trader.NewBitgetTrader(
exchangeCfg.APIKey,
exchangeCfg.SecretKey,
exchangeCfg.Passphrase,
)
case "lighter":
if exchangeCfg.LighterAPIKeyPrivateKey != "" {
tempTrader, createErr = trader.NewLighterTraderV2(
@@ -1086,6 +1092,33 @@ func (s *Server) handleSyncBalance(c *gin.Context) {
exchangeCfg.APIKey,
exchangeCfg.SecretKey,
)
case "okx":
tempTrader = trader.NewOKXTrader(
exchangeCfg.APIKey,
exchangeCfg.SecretKey,
exchangeCfg.Passphrase,
)
case "bitget":
tempTrader = trader.NewBitgetTrader(
exchangeCfg.APIKey,
exchangeCfg.SecretKey,
exchangeCfg.Passphrase,
)
case "lighter":
if exchangeCfg.LighterAPIKeyPrivateKey != "" {
tempTrader, createErr = trader.NewLighterTraderV2(
exchangeCfg.LighterPrivateKey,
exchangeCfg.LighterWalletAddr,
exchangeCfg.LighterAPIKeyPrivateKey,
exchangeCfg.Testnet,
)
} else {
tempTrader, createErr = trader.NewLighterTrader(
exchangeCfg.LighterPrivateKey,
exchangeCfg.LighterWalletAddr,
exchangeCfg.Testnet,
)
}
default:
c.JSON(http.StatusBadRequest, gin.H{"error": "Unsupported exchange type"})
return
@@ -1219,6 +1252,12 @@ func (s *Server) handleClosePosition(c *gin.Context) {
exchangeCfg.SecretKey,
exchangeCfg.Passphrase,
)
case "bitget":
tempTrader = trader.NewBitgetTrader(
exchangeCfg.APIKey,
exchangeCfg.SecretKey,
exchangeCfg.Passphrase,
)
case "lighter":
if exchangeCfg.LighterAPIKeyPrivateKey != "" {
tempTrader, createErr = trader.NewLighterTraderV2(
@@ -1590,7 +1629,7 @@ func (s *Server) handleCreateExchange(c *gin.Context) {
// Validate exchange type
validTypes := map[string]bool{
"binance": true, "bybit": true, "okx": true,
"binance": true, "bybit": true, "okx": true, "bitget": true,
"hyperliquid": true, "aster": true, "lighter": true,
}
if !validTypes[req.ExchangeType] {

View File

@@ -662,6 +662,10 @@ func (tm *TraderManager) addTraderFromStore(traderCfg *store.Trader, aiModelCfg
traderConfig.OKXAPIKey = exchangeCfg.APIKey
traderConfig.OKXSecretKey = exchangeCfg.SecretKey
traderConfig.OKXPassphrase = exchangeCfg.Passphrase
case "bitget":
traderConfig.BitgetAPIKey = exchangeCfg.APIKey
traderConfig.BitgetSecretKey = exchangeCfg.SecretKey
traderConfig.BitgetPassphrase = exchangeCfg.Passphrase
case "hyperliquid":
traderConfig.HyperliquidPrivateKey = exchangeCfg.APIKey
traderConfig.HyperliquidWalletAddr = exchangeCfg.HyperliquidWalletAddr

View File

@@ -102,7 +102,7 @@ func (s *ExchangeStore) migrateToMultiAccount() error {
var count int
err := s.db.QueryRow(`
SELECT COUNT(*) FROM exchanges
WHERE exchange_type = '' AND id IN ('binance', 'bybit', 'okx', 'hyperliquid', 'aster', 'lighter')
WHERE exchange_type = '' AND id IN ('binance', 'bybit', 'okx', 'bitget', 'hyperliquid', 'aster', 'lighter')
`).Scan(&count)
if err != nil {
return err
@@ -127,7 +127,7 @@ func (s *ExchangeStore) migrateToMultiAccount() error {
COALESCE(lighter_private_key, '') as lighter_private_key,
COALESCE(lighter_api_key_private_key, '') as lighter_api_key_private_key
FROM exchanges
WHERE exchange_type = '' AND id IN ('binance', 'bybit', 'okx', 'hyperliquid', 'aster', 'lighter')
WHERE exchange_type = '' AND id IN ('binance', 'bybit', 'okx', 'bitget', 'hyperliquid', 'aster', 'lighter')
`)
if err != nil {
return err
@@ -314,6 +314,8 @@ func getExchangeNameAndType(exchangeType string) (name string, typ string) {
return "Bybit Futures", "cex"
case "okx":
return "OKX Futures", "cex"
case "bitget":
return "Bitget Futures", "cex"
case "hyperliquid":
return "Hyperliquid", "dex"
case "aster":
@@ -451,7 +453,7 @@ func (s *ExchangeStore) CreateLegacy(userID, id, name, typ string, enabled bool,
hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey string) error {
// Check if this is an old-style ID (exchange type as ID)
if id == "binance" || id == "bybit" || id == "okx" || id == "hyperliquid" || id == "aster" || id == "lighter" {
if id == "binance" || id == "bybit" || id == "okx" || id == "bitget" || id == "hyperliquid" || id == "aster" || id == "lighter" {
// Use new Create method with exchange type
_, err := s.Create(userID, id, "Default", enabled, apiKey, secretKey, "", testnet,
hyperliquidWalletAddr, asterUser, asterSigner, asterPrivateKey, "", "", "")

View File

@@ -22,7 +22,7 @@ type AutoTraderConfig struct {
AIModel string // AI model: "qwen" or "deepseek"
// Trading platform selection
Exchange string // Exchange type: "binance", "bybit", "okx", "hyperliquid", "aster" or "lighter"
Exchange string // Exchange type: "binance", "bybit", "okx", "bitget", "hyperliquid", "aster" or "lighter"
ExchangeID string // Exchange account UUID (for multi-account support)
// Binance API configuration
@@ -38,6 +38,11 @@ type AutoTraderConfig struct {
OKXSecretKey string
OKXPassphrase string
// Bitget API configuration
BitgetAPIKey string
BitgetSecretKey string
BitgetPassphrase string
// Hyperliquid configuration
HyperliquidPrivateKey string
HyperliquidWalletAddr string
@@ -222,6 +227,9 @@ func NewAutoTrader(config AutoTraderConfig, st *store.Store, userID string) (*Au
case "okx":
logger.Infof("🏦 [%s] Using OKX Futures trading", config.Name)
trader = NewOKXTrader(config.OKXAPIKey, config.OKXSecretKey, config.OKXPassphrase)
case "bitget":
logger.Infof("🏦 [%s] Using Bitget Futures trading", config.Name)
trader = NewBitgetTrader(config.BitgetAPIKey, config.BitgetSecretKey, config.BitgetPassphrase)
case "hyperliquid":
logger.Infof("🏦 [%s] Using Hyperliquid trading", config.Name)
trader, err = NewHyperliquidTrader(config.HyperliquidPrivateKey, config.HyperliquidWalletAddr, config.HyperliquidTestnet)

1098
trader/bitget_trader.go Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -500,6 +500,9 @@ func (m *PositionSyncManager) createTrader(config *store.TraderFullConfig) (Trad
case "okx":
return NewOKXTrader(exchange.APIKey, exchange.SecretKey, exchange.Passphrase), nil
case "bitget":
return NewBitgetTrader(exchange.APIKey, exchange.SecretKey, exchange.Passphrase), nil
case "hyperliquid":
return NewHyperliquidTrader(exchange.SecretKey, exchange.HyperliquidWalletAddr, exchange.Testnet)

View File

@@ -47,7 +47,7 @@ const HyperliquidIcon: React.FC<IconProps> = ({
</svg>
)
// Bybit SVG 图标组件
// Bybit SVG 图标组件 (Official from bybit-web3.github.io)
const BybitIcon: React.FC<IconProps> = ({
width = 24,
height = 24,
@@ -56,27 +56,26 @@ const BybitIcon: React.FC<IconProps> = ({
<svg
width={width}
height={height}
viewBox="0 0 200 200"
viewBox="0 0 88 88"
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"
/>
<path d="M0 18.7C0 8.37227 8.37228 0 18.7 0H69.3C79.6277 0 88 8.37228 88 18.7V69.3C88 79.6277 79.6277 88 69.3 88H18.7C8.37227 88 0 79.6277 0 69.3V18.7Z" fill="#404347"/>
<path d="M7.57617 26.8067C6.78516 24.0787 8.4775 21.2531 11.2559 20.663L57.6087 10.8173C59.809 10.35 62.0443 11.4443 63.0247 13.4689L83.8443 56.4657L25.1776 87.5101L7.57617 26.8067Z" fill="url(#bybit_grad)"/>
<path d="M8.18242 30.1618C7.35049 27.2838 9.27925 24.3413 12.2502 23.9559L73.6865 15.9881C76.2391 15.6571 78.6111 17.3618 79.1111 19.8867L88.0003 64.7771L24.6892 87.2665L8.18242 30.1618Z" fill="white"/>
<path d="M0 34.2222C0 28.8221 4.37766 24.4445 9.77778 24.4445H68.4444C79.2447 24.4445 88 33.1998 88 44V68.4445C88 79.2447 79.2447 88 68.4444 88H19.5556C8.75532 88 0 79.2447 0 68.4445V34.2222Z" fill="black"/>
<path d="M58.2201 61.1959V42.8755H61.7937V61.1959H58.2201Z" fill="#F7A600"/>
<path d="M17.4395 66.6637H9.77795V48.3434H17.1313C20.7049 48.3434 22.7874 50.3505 22.7874 53.4893C22.7874 55.5215 21.4504 56.8345 20.5257 57.2721C21.6315 57.7869 23.0456 58.9438 23.0456 61.3885C23.0456 64.8108 20.7049 66.6637 17.4395 66.6637ZM16.8481 51.5343H13.3516V55.7548H16.8481C18.3642 55.7548 19.2138 54.9064 19.2138 53.6455C19.2138 52.3826 18.3662 51.5343 16.8481 51.5343ZM17.0793 58.9708H13.3516V63.4728H17.0793C18.6994 63.4728 19.47 62.4432 19.47 61.2092C19.472 59.9733 18.6994 58.9708 17.0793 58.9708Z" fill="white"/>
<path d="M32.8925 59.1501V66.6637H29.3439V59.1501L23.8419 48.3434H27.7238L31.1432 55.7278L34.5107 48.3434H38.3926L32.8925 59.1501Z" fill="white"/>
<path d="M48.5633 66.6637H40.9017V48.3434H48.2551C51.8287 48.3434 53.9112 50.3505 53.9112 53.4893C53.9112 55.5215 52.5742 56.8345 51.6495 57.2721C52.7553 57.7869 54.1693 58.9438 54.1693 61.3885C54.1674 64.8108 51.8268 66.6637 48.5633 66.6637ZM47.9719 51.5343H44.4753V55.7548H47.9719C49.488 55.7548 50.3376 54.9064 50.3376 53.6455C50.3357 52.3826 49.488 51.5343 47.9719 51.5343ZM48.2031 58.9708H44.4753V63.4728H48.2031C49.8232 63.4728 50.5938 62.4432 50.5938 61.2092C50.5938 59.9734 49.8213 58.9708 48.2031 58.9708Z" fill="white"/>
<path d="M73.439 51.5343V66.6637H69.8654V51.5343H65.0839V48.3434H78.2224V51.5343H73.439Z" fill="white"/>
<defs>
<linearGradient id="bybit_grad" x1="7.33308" y1="25.594" x2="84.6381" y2="21.7216" gradientUnits="userSpaceOnUse">
<stop stopColor="#FFD748"/>
<stop offset="1" stopColor="#F7A600"/>
</linearGradient>
</defs>
</svg>
)
@@ -94,11 +93,32 @@ const OKXIcon: React.FC<IconProps> = ({
xmlns="http://www.w3.org/2000/svg"
className={className}
>
<rect width="200" height="200" rx="24" fill="#000"/>
<rect x="40" y="40" width="50" height="50" rx="8" fill="#fff"/>
<rect x="110" y="40" width="50" height="50" rx="8" fill="#fff"/>
<rect x="40" y="110" width="50" height="50" rx="8" fill="#fff"/>
<rect x="110" y="110" width="50" height="50" rx="8" fill="#fff"/>
<rect width="200" height="200" rx="40" fill="#000"/>
<rect x="35" y="35" width="40" height="40" rx="4" fill="#fff"/>
<rect x="80" y="80" width="40" height="40" rx="4" fill="#fff"/>
<rect x="125" y="35" width="40" height="40" rx="4" fill="#fff"/>
<rect x="35" y="125" width="40" height="40" rx="4" fill="#fff"/>
<rect x="125" y="125" width="40" height="40" rx="4" fill="#fff"/>
</svg>
)
// Bitget SVG 图标组件
const BitgetIcon: 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}
>
<rect width="200" height="200" rx="40" fill="#00F0FF"/>
<path d="M60 45L100 45C127.614 45 150 67.3858 150 95C150 122.614 127.614 145 100 145L80 145L80 155L60 155L60 45ZM80 65L80 125L100 125C116.569 125 130 111.569 130 95C130 78.4315 116.569 65 100 65L80 65Z" fill="#000"/>
<path d="M75 80L75 110L95 110C103.284 110 110 103.284 110 95C110 86.7157 103.284 80 95 80L75 80Z" fill="#00F0FF"/>
</svg>
)
@@ -180,6 +200,26 @@ const AsterIcon: React.FC<IconProps> = ({
</svg>
)
// Lighter SVG 图标组件
const LighterIcon: 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}
>
<rect width="200" height="200" rx="40" fill="#1A1A2E"/>
<path d="M70 50L70 130L130 130L130 150L50 150L50 50L70 50Z" fill="#00D9FF"/>
<circle cx="115" cy="65" r="20" fill="#00D9FF"/>
</svg>
)
// 获取交易所图标的函数
export const getExchangeIcon = (
exchangeType: string,
@@ -192,11 +232,15 @@ export const getExchangeIcon = (
? 'bybit'
: exchangeType.toLowerCase().includes('okx')
? 'okx'
: exchangeType.toLowerCase().includes('hyperliquid')
? 'hyperliquid'
: exchangeType.toLowerCase().includes('aster')
? 'aster'
: exchangeType.toLowerCase()
: exchangeType.toLowerCase().includes('bitget')
? 'bitget'
: exchangeType.toLowerCase().includes('hyperliquid')
? 'hyperliquid'
: exchangeType.toLowerCase().includes('aster')
? 'aster'
: exchangeType.toLowerCase().includes('lighter')
? 'lighter'
: exchangeType.toLowerCase()
const iconProps = {
width: props.width || 24,
@@ -211,11 +255,15 @@ export const getExchangeIcon = (
return <BybitIcon {...iconProps} />
case 'okx':
return <OKXIcon {...iconProps} />
case 'bitget':
return <BitgetIcon {...iconProps} />
case 'hyperliquid':
case 'dex':
return <HyperliquidIcon {...iconProps} />
case 'aster':
return <AsterIcon {...iconProps} />
case 'lighter':
return <LighterIcon {...iconProps} />
case 'cex':
default:
return (

View File

@@ -21,6 +21,7 @@ const SUPPORTED_EXCHANGE_TEMPLATES = [
{ exchange_type: 'binance', name: 'Binance Futures', type: 'cex' as const },
{ exchange_type: 'bybit', name: 'Bybit Futures', type: 'cex' as const },
{ exchange_type: 'okx', name: 'OKX Futures', type: 'cex' as const },
{ exchange_type: 'bitget', name: 'Bitget Futures', type: 'cex' as const },
{ exchange_type: 'hyperliquid', name: 'Hyperliquid', type: 'dex' as const },
{ exchange_type: 'aster', name: 'Aster DEX', type: 'dex' as const },
{ exchange_type: 'lighter', name: 'Lighter', type: 'dex' as const },
@@ -123,6 +124,7 @@ export function ExchangeConfigModal({
binance: { url: 'https://www.binance.com/join?ref=NOFXENG', hasReferral: true },
okx: { url: 'https://www.okx.com/join/1865360', hasReferral: true },
bybit: { url: 'https://partner.bybit.com/b/83856', hasReferral: true },
bitget: { url: 'https://www.bitget.com/referral/register?from=referral&clacCode=c8a43172', hasReferral: true },
hyperliquid: { url: 'https://app.hyperliquid.xyz/join/AITRADING', hasReferral: true },
aster: { url: 'https://www.asterdex.com/en/referral/fdfc0e', hasReferral: true },
lighter: { url: 'https://lighter.xyz', hasReferral: false },
@@ -282,6 +284,9 @@ export function ExchangeConfigModal({
} else if (currentExchangeType === 'okx') {
if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return
await onSave(exchangeId, exchangeType, trimmedAccountName, apiKey.trim(), secretKey.trim(), passphrase.trim(), testnet)
} else if (currentExchangeType === 'bitget') {
if (!apiKey.trim() || !secretKey.trim() || !passphrase.trim()) return
await onSave(exchangeId, exchangeType, trimmedAccountName, apiKey.trim(), secretKey.trim(), passphrase.trim(), testnet)
} else if (currentExchangeType === 'hyperliquid') {
if (!apiKey.trim() || !hyperliquidWalletAddr.trim()) return // 验证私钥和钱包地址
await onSave(
@@ -533,10 +538,11 @@ export function ExchangeConfigModal({
{selectedTemplate && (
<>
{/* Binance/Bybit/OKX 的输入字段 */}
{/* Binance/Bybit/OKX/Bitget 的输入字段 */}
{(currentExchangeType === 'binance' ||
currentExchangeType === 'bybit' ||
currentExchangeType === 'okx') && (
currentExchangeType === 'okx' ||
currentExchangeType === 'bitget') && (
<>
{/* 币安用户配置提示 (D1 方案) */}
{currentExchangeType === 'binance' && (
@@ -681,7 +687,7 @@ export function ExchangeConfigModal({
/>
</div>
{currentExchangeType === 'okx' && (
{(currentExchangeType === 'okx' || currentExchangeType === 'bitget') && (
<div>
<label
className="block text-sm font-semibold mb-2"
@@ -1184,6 +1190,10 @@ export function ExchangeConfigModal({
(!apiKey.trim() ||
!secretKey.trim() ||
!passphrase.trim())) ||
(currentExchangeType === 'bitget' &&
(!apiKey.trim() ||
!secretKey.trim() ||
!passphrase.trim())) ||
(currentExchangeType === 'hyperliquid' &&
(!apiKey.trim() || !hyperliquidWalletAddr.trim())) || // 验证私钥和钱包地址
(currentExchangeType === 'aster' &&
@@ -1201,6 +1211,7 @@ export function ExchangeConfigModal({
currentExchangeType !== 'binance' &&
currentExchangeType !== 'bybit' &&
currentExchangeType !== 'okx' &&
currentExchangeType !== 'bitget' &&
(!apiKey.trim() || !secretKey.trim()))
}
className="flex-1 px-4 py-2 rounded text-sm font-semibold disabled:opacity-50"

View File

@@ -336,7 +336,7 @@ export const translations = {
enterUser: 'Enter User',
enterSigner: 'Enter Signer Address',
enterSecretKey: 'Enter Secret Key',
enterPassphrase: 'Enter Passphrase (Required for OKX)',
enterPassphrase: 'Enter Passphrase',
hyperliquidPrivateKeyDesc:
'Hyperliquid uses private key for trading authentication',
hyperliquidWalletAddressDesc:
@@ -1420,7 +1420,7 @@ export const translations = {
enterWalletAddress: '输入钱包地址',
enterUser: '输入用户名',
enterSigner: '输入签名者地址',
enterPassphrase: '输入Passphrase (OKX必填)',
enterPassphrase: '输入Passphrase',
hyperliquidPrivateKeyDesc: 'Hyperliquid 使用私钥进行交易认证',
hyperliquidWalletAddressDesc: '与私钥对应的钱包地址',
// Hyperliquid 代理钱包 (新安全模型)