Files
tinkle-community cb31782be4 refactor: split large files and clean up project structure
- Rename experience/ to telemetry/ for clarity
- Split 15+ large Go files (800-2200 lines) into focused modules:
  kernel/engine.go, backtest/runner.go, market/data.go, store/position.go,
  api/handler_trader.go, trader/auto_trader_grid.go, and 9 exchange traders
- Split frontend monoliths: types.ts, api.ts, AITradersPage.tsx, BacktestPage.tsx
  into domain-specific modules with barrel re-exports
- Remove stale files: screenshots, .yml.old, pyproject.toml
- Remove unused scripts/ and cmd/ directories
- Remove broken/outdated test files (network-dependent, stale expectations)
2026-03-12 12:53:57 +08:00

712 lines
19 KiB
Go

package bitget
import (
"encoding/json"
"fmt"
"nofx/logger"
"nofx/trader/types"
"strconv"
"strings"
)
// OpenLong opens long position
func (t *BitgetTrader) OpenLong(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
symbol = t.convertSymbol(symbol)
// Cancel old orders first
t.CancelAllOrders(symbol)
// Set leverage
if err := t.SetLeverage(symbol, leverage); err != nil {
logger.Infof(" ⚠️ Failed to set leverage: %v", err)
}
// Format quantity
qtyStr, _ := t.FormatQuantity(symbol, quantity)
body := map[string]interface{}{
"symbol": symbol,
"productType": "USDT-FUTURES",
"marginMode": "crossed",
"marginCoin": "USDT",
"side": "buy",
"orderType": "market",
"size": qtyStr,
"clientOid": genBitgetClientOid(),
}
logger.Infof(" 📊 Bitget OpenLong: symbol=%s, qty=%s, leverage=%d", symbol, qtyStr, leverage)
data, err := t.doRequest("POST", bitgetOrderPath, body)
if err != nil {
return nil, fmt.Errorf("failed to open long position: %w", err)
}
var order struct {
OrderId string `json:"orderId"`
ClientOid string `json:"clientOid"`
}
if err := json.Unmarshal(data, &order); err != nil {
return nil, fmt.Errorf("failed to parse order response: %w", err)
}
// Clear cache
t.clearCache()
logger.Infof("✓ Bitget opened long position successfully: %s", symbol)
return map[string]interface{}{
"orderId": order.OrderId,
"symbol": symbol,
"status": "FILLED",
}, nil
}
// OpenShort opens short position
func (t *BitgetTrader) OpenShort(symbol string, quantity float64, leverage int) (map[string]interface{}, error) {
symbol = t.convertSymbol(symbol)
// Cancel old orders first
t.CancelAllOrders(symbol)
// Set leverage
if err := t.SetLeverage(symbol, leverage); err != nil {
logger.Infof(" ⚠️ Failed to set leverage: %v", err)
}
// Format quantity
qtyStr, _ := t.FormatQuantity(symbol, quantity)
body := map[string]interface{}{
"symbol": symbol,
"productType": "USDT-FUTURES",
"marginMode": "crossed",
"marginCoin": "USDT",
"side": "sell",
"orderType": "market",
"size": qtyStr,
"clientOid": genBitgetClientOid(),
}
logger.Infof(" 📊 Bitget OpenShort: symbol=%s, qty=%s, leverage=%d", symbol, qtyStr, leverage)
data, err := t.doRequest("POST", bitgetOrderPath, body)
if err != nil {
return nil, fmt.Errorf("failed to open short position: %w", err)
}
var order struct {
OrderId string `json:"orderId"`
ClientOid string `json:"clientOid"`
}
if err := json.Unmarshal(data, &order); err != nil {
return nil, fmt.Errorf("failed to parse order response: %w", err)
}
// Clear cache
t.clearCache()
logger.Infof("✓ Bitget opened short position successfully: %s", symbol)
return map[string]interface{}{
"orderId": order.OrderId,
"symbol": symbol,
"status": "FILLED",
}, nil
}
// CloseLong closes long position
func (t *BitgetTrader) CloseLong(symbol string, quantity float64) (map[string]interface{}, error) {
symbol = t.convertSymbol(symbol)
// If quantity is 0, get current position
if quantity == 0 {
positions, err := t.GetPositions()
if err != nil {
return nil, err
}
for _, pos := range positions {
if pos["symbol"] == symbol && pos["side"] == "long" {
quantity = pos["positionAmt"].(float64)
break
}
}
if quantity == 0 {
return nil, fmt.Errorf("long position not found for %s", symbol)
}
}
// Format quantity
qtyStr, _ := t.FormatQuantity(symbol, quantity)
body := map[string]interface{}{
"symbol": symbol,
"productType": "USDT-FUTURES",
"marginMode": "crossed",
"marginCoin": "USDT",
"side": "sell",
"orderType": "market",
"size": qtyStr,
"reduceOnly": "YES",
"clientOid": genBitgetClientOid(),
}
logger.Infof(" 📊 Bitget CloseLong: symbol=%s, qty=%s", symbol, qtyStr)
data, err := t.doRequest("POST", bitgetOrderPath, body)
if err != nil {
return nil, fmt.Errorf("failed to close long position: %w", err)
}
var order struct {
OrderId string `json:"orderId"`
}
if err := json.Unmarshal(data, &order); err != nil {
return nil, err
}
// Clear cache
t.clearCache()
logger.Infof("✓ Bitget closed long position successfully: %s", symbol)
return map[string]interface{}{
"orderId": order.OrderId,
"symbol": symbol,
"status": "FILLED",
}, nil
}
// CloseShort closes short position
func (t *BitgetTrader) CloseShort(symbol string, quantity float64) (map[string]interface{}, error) {
symbol = t.convertSymbol(symbol)
// If quantity is 0, get current position
if quantity == 0 {
positions, err := t.GetPositions()
if err != nil {
return nil, err
}
for _, pos := range positions {
if pos["symbol"] == symbol && pos["side"] == "short" {
quantity = pos["positionAmt"].(float64)
break
}
}
if quantity == 0 {
return nil, fmt.Errorf("short position not found for %s", symbol)
}
}
// Ensure quantity is positive
if quantity < 0 {
quantity = -quantity
}
// Format quantity
qtyStr, _ := t.FormatQuantity(symbol, quantity)
body := map[string]interface{}{
"symbol": symbol,
"productType": "USDT-FUTURES",
"marginMode": "crossed",
"marginCoin": "USDT",
"side": "buy",
"orderType": "market",
"size": qtyStr,
"reduceOnly": "YES",
"clientOid": genBitgetClientOid(),
}
logger.Infof(" 📊 Bitget CloseShort: symbol=%s, qty=%s", symbol, qtyStr)
data, err := t.doRequest("POST", bitgetOrderPath, body)
if err != nil {
return nil, fmt.Errorf("failed to close short position: %w", err)
}
var order struct {
OrderId string `json:"orderId"`
}
if err := json.Unmarshal(data, &order); err != nil {
return nil, err
}
// Clear cache
t.clearCache()
logger.Infof("✓ Bitget closed short position successfully: %s", symbol)
return map[string]interface{}{
"orderId": order.OrderId,
"symbol": symbol,
"status": "FILLED",
}, nil
}
// SetStopLoss sets stop loss order
func (t *BitgetTrader) SetStopLoss(symbol string, positionSide string, quantity, stopPrice float64) error {
// Bitget V2 uses plan order for stop loss
symbol = t.convertSymbol(symbol)
side := "sell"
holdSide := "long"
if strings.ToUpper(positionSide) == "SHORT" {
side = "buy"
holdSide = "short"
}
qtyStr, _ := t.FormatQuantity(symbol, quantity)
body := map[string]interface{}{
"planType": "loss_plan",
"symbol": symbol,
"productType": "USDT-FUTURES",
"marginMode": "crossed",
"marginCoin": "USDT",
"triggerPrice": fmt.Sprintf("%.8f", stopPrice),
"triggerType": "mark_price",
"side": side,
"tradeSide": "close",
"orderType": "market",
"size": qtyStr,
"holdSide": holdSide,
"clientOid": genBitgetClientOid(),
}
_, err := t.doRequest("POST", "/api/v2/mix/order/place-plan-order", body)
if err != nil {
return fmt.Errorf("failed to set stop loss: %w", err)
}
logger.Infof(" ✓ [Bitget] Stop loss set: %s @ %.4f", symbol, stopPrice)
return nil
}
// SetTakeProfit sets take profit order
func (t *BitgetTrader) SetTakeProfit(symbol string, positionSide string, quantity, takeProfitPrice float64) error {
// Bitget V2 uses plan order for take profit
symbol = t.convertSymbol(symbol)
side := "sell"
holdSide := "long"
if strings.ToUpper(positionSide) == "SHORT" {
side = "buy"
holdSide = "short"
}
qtyStr, _ := t.FormatQuantity(symbol, quantity)
body := map[string]interface{}{
"planType": "profit_plan",
"symbol": symbol,
"productType": "USDT-FUTURES",
"marginMode": "crossed",
"marginCoin": "USDT",
"triggerPrice": fmt.Sprintf("%.8f", takeProfitPrice),
"triggerType": "mark_price",
"side": side,
"tradeSide": "close",
"orderType": "market",
"size": qtyStr,
"holdSide": holdSide,
"clientOid": genBitgetClientOid(),
}
_, err := t.doRequest("POST", "/api/v2/mix/order/place-plan-order", body)
if err != nil {
return fmt.Errorf("failed to set take profit: %w", err)
}
logger.Infof(" ✓ [Bitget] Take profit set: %s @ %.4f", symbol, takeProfitPrice)
return nil
}
// CancelStopLossOrders cancels stop loss orders
func (t *BitgetTrader) CancelStopLossOrders(symbol string) error {
return t.cancelPlanOrders(symbol, "loss_plan")
}
// CancelTakeProfitOrders cancels take profit orders
func (t *BitgetTrader) CancelTakeProfitOrders(symbol string) error {
return t.cancelPlanOrders(symbol, "profit_plan")
}
// cancelPlanOrders cancels plan orders
func (t *BitgetTrader) cancelPlanOrders(symbol string, planType string) error {
symbol = t.convertSymbol(symbol)
// Get pending plan orders
params := map[string]interface{}{
"symbol": symbol,
"productType": "USDT-FUTURES",
"planType": planType,
}
data, err := t.doRequest("GET", "/api/v2/mix/order/orders-plan-pending", params)
if err != nil {
return err
}
var orders struct {
EntrustedList []struct {
OrderId string `json:"orderId"`
} `json:"entrustedList"`
}
if err := json.Unmarshal(data, &orders); err != nil {
return err
}
// Cancel each order
for _, order := range orders.EntrustedList {
body := map[string]interface{}{
"symbol": symbol,
"productType": "USDT-FUTURES",
"marginCoin": "USDT",
"orderId": order.OrderId,
}
t.doRequest("POST", "/api/v2/mix/order/cancel-plan-order", body)
}
return nil
}
// CancelAllOrders cancels all pending orders
func (t *BitgetTrader) CancelAllOrders(symbol string) error {
symbol = t.convertSymbol(symbol)
// Get pending orders
params := map[string]interface{}{
"symbol": symbol,
"productType": "USDT-FUTURES",
}
data, err := t.doRequest("GET", bitgetPendingPath, params)
if err != nil {
return err
}
var orders struct {
EntrustedList []struct {
OrderId string `json:"orderId"`
} `json:"entrustedList"`
}
if err := json.Unmarshal(data, &orders); err != nil {
return err
}
// Cancel each order
for _, order := range orders.EntrustedList {
body := map[string]interface{}{
"symbol": symbol,
"productType": "USDT-FUTURES",
"marginCoin": "USDT",
"orderId": order.OrderId,
}
t.doRequest("POST", bitgetCancelOrderPath, body)
}
// Also cancel plan orders
t.cancelPlanOrders(symbol, "loss_plan")
t.cancelPlanOrders(symbol, "profit_plan")
return nil
}
// CancelStopOrders cancels stop loss and take profit orders
func (t *BitgetTrader) CancelStopOrders(symbol string) error {
t.CancelStopLossOrders(symbol)
t.CancelTakeProfitOrders(symbol)
return nil
}
// GetOrderStatus gets order status
func (t *BitgetTrader) GetOrderStatus(symbol string, orderID string) (map[string]interface{}, error) {
symbol = t.convertSymbol(symbol)
params := map[string]interface{}{
"symbol": symbol,
"productType": "USDT-FUTURES",
"orderId": orderID,
}
data, err := t.doRequest("GET", "/api/v2/mix/order/detail", params)
if err != nil {
return nil, fmt.Errorf("failed to get order status: %w", err)
}
var order struct {
OrderId string `json:"orderId"`
State string `json:"state"` // filled, canceled, partially_filled, new
PriceAvg string `json:"priceAvg"` // Average fill price
BaseVolume string `json:"baseVolume"` // Filled quantity
Fee string `json:"fee"` // Fee
Side string `json:"side"`
OrderType string `json:"orderType"`
CTime string `json:"cTime"`
UTime string `json:"uTime"`
}
if err := json.Unmarshal(data, &order); err != nil {
return nil, err
}
avgPrice, _ := strconv.ParseFloat(order.PriceAvg, 64)
fillQty, _ := strconv.ParseFloat(order.BaseVolume, 64)
fee, _ := strconv.ParseFloat(order.Fee, 64)
cTime, _ := strconv.ParseInt(order.CTime, 10, 64)
uTime, _ := strconv.ParseInt(order.UTime, 10, 64)
// Status mapping
statusMap := map[string]string{
"filled": "FILLED",
"new": "NEW",
"partially_filled": "PARTIALLY_FILLED",
"canceled": "CANCELED",
}
status := statusMap[order.State]
if status == "" {
status = order.State
}
return map[string]interface{}{
"orderId": order.OrderId,
"symbol": symbol,
"status": status,
"avgPrice": avgPrice,
"executedQty": fillQty,
"side": order.Side,
"type": order.OrderType,
"time": cTime,
"updateTime": uTime,
"commission": -fee,
}, nil
}
// GetOpenOrders gets all open/pending orders for a symbol
func (t *BitgetTrader) GetOpenOrders(symbol string) ([]types.OpenOrder, error) {
symbol = t.convertSymbol(symbol)
var result []types.OpenOrder
// 1. Get pending limit orders
params := map[string]interface{}{
"symbol": symbol,
"productType": "USDT-FUTURES",
}
data, err := t.doRequest("GET", bitgetPendingPath, params)
if err != nil {
logger.Warnf("[Bitget] Failed to get pending orders: %v", err)
}
if err == nil && data != nil {
var orders struct {
EntrustedList []struct {
OrderId string `json:"orderId"`
Symbol string `json:"symbol"`
Side string `json:"side"` // buy/sell
TradeSide string `json:"tradeSide"` // open/close
PosSide string `json:"posSide"` // long/short
OrderType string `json:"orderType"` // limit/market
Price string `json:"price"`
Size string `json:"size"`
State string `json:"state"`
} `json:"entrustedList"`
}
if err := json.Unmarshal(data, &orders); err == nil {
for _, order := range orders.EntrustedList {
price, _ := strconv.ParseFloat(order.Price, 64)
quantity, _ := strconv.ParseFloat(order.Size, 64)
// Convert side to standard format
side := strings.ToUpper(order.Side)
positionSide := strings.ToUpper(order.PosSide)
result = append(result, types.OpenOrder{
OrderID: order.OrderId,
Symbol: symbol,
Side: side,
PositionSide: positionSide,
Type: strings.ToUpper(order.OrderType),
Price: price,
StopPrice: 0,
Quantity: quantity,
Status: "NEW",
})
}
}
}
// 2. Get pending plan orders (stop-loss/take-profit)
// Bitget V2 API requires planType parameter: profit_loss for SL/TP orders
planParams := map[string]interface{}{
"productType": "USDT-FUTURES",
"planType": "profit_loss",
}
planData, err := t.doRequest("GET", "/api/v2/mix/order/orders-plan-pending", planParams)
if err != nil {
logger.Warnf("[Bitget] Failed to get plan orders: %v", err)
}
if err == nil && planData != nil {
var planOrders struct {
EntrustedList []struct {
OrderId string `json:"orderId"`
Symbol string `json:"symbol"`
Side string `json:"side"`
PosSide string `json:"posSide"`
PlanType string `json:"planType"` // pos_loss, pos_profit
TriggerPrice string `json:"triggerPrice"`
StopLossTriggerPrice string `json:"stopLossTriggerPrice"`
StopSurplusTriggerPrice string `json:"stopSurplusTriggerPrice"`
Size string `json:"size"`
PlanStatus string `json:"planStatus"`
} `json:"entrustedList"`
}
if err := json.Unmarshal(planData, &planOrders); err == nil {
for _, order := range planOrders.EntrustedList {
// Filter by symbol if specified
if symbol != "" && order.Symbol != symbol {
continue
}
// Determine trigger price based on plan type
var triggerPrice float64
orderType := "STOP_MARKET"
if order.PlanType == "pos_profit" {
// Take profit order
orderType = "TAKE_PROFIT_MARKET"
if order.StopSurplusTriggerPrice != "" {
triggerPrice, _ = strconv.ParseFloat(order.StopSurplusTriggerPrice, 64)
} else {
triggerPrice, _ = strconv.ParseFloat(order.TriggerPrice, 64)
}
} else {
// Stop loss order (pos_loss)
if order.StopLossTriggerPrice != "" {
triggerPrice, _ = strconv.ParseFloat(order.StopLossTriggerPrice, 64)
} else {
triggerPrice, _ = strconv.ParseFloat(order.TriggerPrice, 64)
}
}
quantity, _ := strconv.ParseFloat(order.Size, 64)
side := strings.ToUpper(order.Side)
positionSide := strings.ToUpper(order.PosSide)
result = append(result, types.OpenOrder{
OrderID: order.OrderId,
Symbol: order.Symbol,
Side: side,
PositionSide: positionSide,
Type: orderType,
Price: 0,
StopPrice: triggerPrice,
Quantity: quantity,
Status: "NEW",
})
}
}
}
logger.Infof("✓ BITGET GetOpenOrders: found %d open orders for %s", len(result), symbol)
return result, nil
}
// PlaceLimitOrder places a limit order for grid trading
// Implements GridTrader interface
func (t *BitgetTrader) PlaceLimitOrder(req *types.LimitOrderRequest) (*types.LimitOrderResult, error) {
symbol := t.convertSymbol(req.Symbol)
// Set leverage if specified
if req.Leverage > 0 {
if err := t.SetLeverage(symbol, req.Leverage); err != nil {
logger.Warnf("[Bitget] Failed to set leverage: %v", err)
}
}
// Format quantity
qtyStr, _ := t.FormatQuantity(symbol, req.Quantity)
// Determine side
side := "buy"
if req.Side == "SELL" {
side = "sell"
}
body := map[string]interface{}{
"symbol": symbol,
"productType": "USDT-FUTURES",
"marginMode": "crossed",
"marginCoin": "USDT",
"side": side,
"orderType": "limit",
"size": qtyStr,
"price": fmt.Sprintf("%.8f", req.Price),
"force": "GTC", // Good Till Cancel
"clientOid": genBitgetClientOid(),
}
// Add reduce only if specified
if req.ReduceOnly {
body["reduceOnly"] = "YES"
}
logger.Infof("[Bitget] PlaceLimitOrder: %s %s @ %.4f, qty=%s", symbol, side, req.Price, qtyStr)
data, err := t.doRequest("POST", bitgetOrderPath, body)
if err != nil {
return nil, fmt.Errorf("failed to place limit order: %w", err)
}
var order struct {
OrderId string `json:"orderId"`
ClientOid string `json:"clientOid"`
}
if err := json.Unmarshal(data, &order); err != nil {
return nil, fmt.Errorf("failed to parse order response: %w", err)
}
logger.Infof("✓ [Bitget] Limit order placed: %s %s @ %.4f, orderID=%s",
symbol, side, req.Price, order.OrderId)
return &types.LimitOrderResult{
OrderID: order.OrderId,
ClientID: order.ClientOid,
Symbol: req.Symbol,
Side: req.Side,
PositionSide: req.PositionSide,
Price: req.Price,
Quantity: req.Quantity,
Status: "NEW",
}, nil
}
// CancelOrder cancels a specific order by ID
// Implements GridTrader interface
func (t *BitgetTrader) CancelOrder(symbol, orderID string) error {
symbol = t.convertSymbol(symbol)
body := map[string]interface{}{
"symbol": symbol,
"productType": "USDT-FUTURES",
"orderId": orderID,
}
_, err := t.doRequest("POST", "/api/v2/mix/order/cancel-order", body)
if err != nil {
return fmt.Errorf("failed to cancel order: %w", err)
}
logger.Infof("✓ [Bitget] Order cancelled: %s %s", symbol, orderID)
return nil
}