This commit is contained in:
2026-01-09 15:48:31 +08:00
parent e32eabbf95
commit c8e189e9c7
28 changed files with 2795 additions and 0 deletions

47
trade/account.go Normal file
View File

@@ -0,0 +1,47 @@
package trade
import (
"sync"
)
var (
AccountsAssets *Assets
)
type AccountsBalance struct {
AccountEquity float64 // 账户权益(保证金币种)包含未实现盈亏根据mark price计算
Available float64 // 账户可用数量
}
// lock
type Assets struct {
sync.RWMutex
Data map[string]*AccountsBalance
}
func NewAccounts() {
AccountsAssets = &Assets{
Data: make(map[string]*AccountsBalance),
}
}
func (ac *Assets) Set(assets string, balance *AccountsBalance) {
ac.Lock()
defer ac.Unlock()
ac.Data[assets] = balance
}
func (ac *Assets) SetData(data map[string]*AccountsBalance) {
ac.Lock()
defer ac.Unlock()
ac.Data = data
}
func (ac *Assets) Get(assets string) *AccountsBalance {
ac.Lock()
defer ac.Unlock()
return ac.Data[assets]
}

77
trade/binance_klines.go Normal file
View File

@@ -0,0 +1,77 @@
package trade
import (
"context"
"errors"
"fmt"
"time"
"git.apinb.com/bsm-sdk/core/utils"
"git.apinb.com/quant/strategy/types"
)
const (
MaxKlinesFetch = 500
)
func (bn *BinanceClient) FetchKlines(symbol, interval string, limit int, debug bool) ([]*types.KLine, []float64, error) {
if limit > MaxKlinesFetch {
limit = MaxKlinesFetch
}
ctx := context.Background()
klines, err := bn.Futures.NewKlinesService().Symbol(symbol).Interval(interval).Limit(limit).Do(ctx)
if err != nil {
return nil, nil, err
}
if debug {
fmt.Println("FetchKlines:", symbol, interval, limit)
}
k := make([]*types.KLine, 0)
closes := make([]float64, len(klines))
for i, val := range klines {
c := utils.String2Float64(val.Close)
closes[i] = c
k = append(k, &types.KLine{
Timestamp: val.OpenTime,
Open: utils.String2Float64(val.Open),
High: utils.String2Float64(val.High),
Low: utils.String2Float64(val.Low),
Close: c,
Volume: utils.String2Float64(val.QuoteAssetVolume),
})
if debug {
fmt.Println(time.Unix(val.OpenTime/1000, 0).Format(time.DateTime), val.Open, val.High, val.Low, val.Close, val.Volume)
}
}
if len(k) == 0 {
return nil, nil, errors.New("no klines")
}
return k, closes, nil
}
func (bn *BinanceClient) FetchSymbolsPrice() (map[string]float64, error) {
ctx := context.Background()
prices, err := bn.Futures.NewListPricesService().Do(ctx)
if err != nil {
return nil, err
}
priceMap := make(map[string]float64)
for _, p := range prices {
priceMap[p.Symbol] = utils.String2Float64(p.Price)
}
return priceMap, nil
}
func (bn *BinanceClient) BookTicker(symbol string) (bidPrice, askPrice string, err error) {
res, err := bn.Futures.NewListBookTickersService().Symbol(symbol).Do(context.Background())
if len(res) == 0 || err != nil {
return "", "", errors.New("BookTicker: No Data")
}
return res[0].BidPrice, res[0].AskPrice, nil
}

84
trade/binance_new.go Normal file
View File

@@ -0,0 +1,84 @@
package trade
import (
"context"
"log"
"time"
"git.apinb.com/bsm-sdk/core/utils"
"github.com/adshao/go-binance/v2"
"github.com/adshao/go-binance/v2/futures"
)
type BinanceClient struct {
LastUPL map[string]float64
Cli *binance.Client
Futures *futures.Client
}
func NewBinanceClient(apiKey, apiSecret string) *BinanceClient {
return &BinanceClient{
LastUPL: make(map[string]float64),
Cli: binance.NewClient(apiKey, apiSecret),
Futures: futures.NewClient(apiKey, apiSecret),
}
}
// 通过 API 获取账户的合约可用余额
func (bn *BinanceClient) GetFuturesAccountBalance() (map[string]*AccountsBalance, error) {
res, err := bn.Futures.NewGetAccountService().Do(context.Background())
if err != nil {
return nil, err
}
// assets, _ := json.Marshal(res.Assets)
// log.Println("==>", string(assets))
ac := make(map[string]*AccountsBalance)
for _, v := range res.Assets {
ac[v.Asset] = &AccountsBalance{
AccountEquity: utils.String2Float64(v.WalletBalance),
Available: utils.String2Float64(v.AvailableBalance),
}
}
return ac, nil
}
// 设置开仓杠杆
func (bn *BinanceClient) SetLeverage(symbols []string, leverage int) {
// 调整开仓杠杆:
for _, symbol := range symbols {
res, err := bn.Futures.NewChangeLeverageService().Symbol(symbol).Leverage(leverage).Do(context.Background())
if err != nil {
log.Println("[ERROR]", symbol, "ChangeLeverage:", leverage, "res", res, "Err", err)
} else {
log.Println("[INFO]", symbol, "ChangeLeverage:", leverage, "res", res)
}
time.Sleep(1 * time.Second)
}
}
// 设置保证金模式
func (bn *BinanceClient) SetMarginType(symbols []string, marginType string) {
for _, symbol := range symbols {
err := bn.Futures.NewChangeMarginTypeService().Symbol(symbol).MarginType(futures.MarginType(marginType)).Do(context.Background())
if err != nil {
log.Println("[ERROR]", symbol, "ChangeMarginType:", marginType, "Err", err)
} else {
log.Println("[INFO]", symbol, "ChangeMarginType:", marginType)
}
time.Sleep(1 * time.Second)
}
}
// 设置持仓模式
func (bn *BinanceClient) SetDual(dual bool) {
err := bn.Futures.NewChangePositionModeService().DualSide(dual).Do(context.Background())
if err != nil {
log.Println("[ERROR]", "ChangePositionMode:", dual, "Err", err)
} else {
log.Println("[INFO]", "ChangePositionMode:", dual)
}
time.Sleep(1 * time.Second)
}

274
trade/binance_order.go Normal file
View File

@@ -0,0 +1,274 @@
package trade
import (
"context"
"fmt"
"log"
"math"
"git.apinb.com/bsm-sdk/core/utils"
"git.apinb.com/quant/strategy/internal/impl"
"git.apinb.com/quant/strategy/internal/models"
"github.com/adshao/go-binance/v2/futures"
)
const (
OrderSide_Long string = "BUY"
OrderSide_Short string = "SELL"
)
func (bn *BinanceClient) AppendOrder(symbol string, side string, quantity string) {
// 判断:开仓锁,开仓信号
if IsLock(symbol, "OPEN."+side) {
Error("103", "判断:开仓锁,开仓信号")
return
}
var res *futures.CreateOrderResponse
var err error
// 做多
if side == "LONG" {
bid, _, err := bn.BookTicker(symbol)
if err != nil {
Error("113", "BookTicker", err.Error())
return
}
res, err = bn.CreateFuturesLimitOrder(
symbol,
quantity,
"BUY",
bid,
futures.PositionSideTypeLong,
)
}
if side == "SHORT" {
_, ask, err := bn.BookTicker(symbol)
if err != nil {
Error("113", "BookTicker", err.Error())
return
}
res, err = bn.CreateFuturesLimitOrder(
symbol,
quantity,
"SELL",
ask,
futures.PositionSideTypeShort,
)
}
if err != nil {
Error("104", "CreateFuturesOrder_Limit_Binance", err.Error())
return
}
if res != nil {
Debug("Order Result", utils.Int642String(res.OrderID))
SetLock(symbol, "OPEN."+side, 3)
}
}
// CreateOrder 创建订单
// symbol: 交易对
// side: 买卖方向: BUY 做多 / SELL 做空
// positionSide: 持仓方向: LONG 多仓 / SHORT 空仓
// quantity: 数量
// orderType: 订单类型:LIMIT, MARKET
func (bn *BinanceClient) CreateFuturesMarketOrder(symbol, quantity, side string, positionSide futures.PositionSideType) (res *futures.CreateOrderResponse, err error) {
if IsLock(symbol, "OPEN."+side) {
return nil, fmt.Errorf("symbol %s side %s is lock", symbol, side)
}
srv := bn.Futures.NewCreateOrderService().Symbol(symbol).Side(futures.SideType(side)).PositionSide(positionSide).Quantity(quantity).Type(futures.OrderTypeMarket)
result, err := srv.Do(context.Background())
if err != nil {
Error("120", "CreateOrder:", err.Error())
return nil, err
}
SetLock(symbol, "OPEN."+side, 3)
Info("[SUCCESS] Create Order, OrderID:", utils.Int642String(result.OrderID))
return result, nil
}
// CreateOrder 创建订单
// symbol: 交易对
// side: 买卖方向: BUY 做多 / SELL 做空
// positionSide: 持仓方向: LONG 多仓 / SHORT 空仓
// quantity: 数量
// orderType: 订单类型:LIMIT, MARKET
func (bn *BinanceClient) CreateFuturesLimitOrder(symbol, quantity, side, price string, positionSide futures.PositionSideType) (res *futures.CreateOrderResponse, err error) {
if IsLock(symbol, "OPEN."+side) {
return nil, fmt.Errorf("symbol %s side %s is lock", symbol, side)
}
srv := bn.Futures.NewCreateOrderService().Symbol(symbol).Side(futures.SideType(side)).PositionSide(positionSide).Quantity(quantity).Type(futures.OrderTypeLimit).Price(price).TimeInForce(futures.TimeInForceTypeGTX)
extra := map[string]any{
// "priceMatch": "OPPONENT_5",
}
result, err := srv.Do(context.Background(), futures.WithExtraForm(extra))
if err != nil {
Error("120", "CreateOrder:", err.Error())
return nil, err
}
SetLock(symbol, "OPEN."+side, 3)
Info("[SUCCESS] Create Order, OrderID:", utils.Int642String(result.OrderID))
return result, nil
}
func (bn *BinanceClient) CloseFuturesMarketOrder(symbol, orderType, quantity string, positionSide futures.PositionSideType) (res *futures.CreateOrderResponse, err error) {
Debug("CloseFuturesOrder_Binance", symbol, orderType, quantity, string(positionSide))
var side string
if positionSide == futures.PositionSideTypeLong {
side = OrderSide_Short
} else {
side = OrderSide_Long
}
result, err := bn.Futures.NewCreateOrderService().Symbol(symbol).Side(futures.SideType(side)).PositionSide(positionSide).Type(futures.OrderType(orderType)).Quantity(quantity).Do(context.Background())
if err != nil {
Error("121", "CloseOrder:", err.Error())
return nil, err
}
// 锁定仓位
SetLock(symbol, "CLOSE."+string(positionSide), 3)
Info("[SUCCESS] Close Order, OrderID:", utils.Int642String(result.OrderID))
return res, nil
}
func (bn *BinanceClient) CloseFuturesLimitOrder(symbol, quantity, price string, positionSide futures.PositionSideType) (res *futures.CreateOrderResponse, err error) {
Debug("CloseFuturesOrder_Limit_Binance", symbol, string(positionSide), price, quantity)
var side string
if positionSide == futures.PositionSideTypeLong {
side = OrderSide_Short
} else {
side = OrderSide_Long
}
result, err := bn.Futures.NewCreateOrderService().Symbol(symbol).Side(futures.SideType(side)).PositionSide(positionSide).Quantity(quantity).Type(futures.OrderTypeLimit).Price(price).TimeInForce(futures.TimeInForceTypeGTX).Do(context.Background())
if err != nil {
Error("121", "CloseOrder:", err.Error())
return nil, err
}
Info("[SUCCESS] Close Order, OrderID:", utils.Int642String(result.OrderID))
return res, nil
}
func (bn *BinanceClient) QuickMarketClose(PlanKeyName, symbol string, side string) {
Debug(PlanKeyName, "Quick closeing")
orders, _ := GetPositions(PlanKeyName, symbol)
if len(orders) == 0 {
return
}
volume := utils.Float64ToString(orders[0].Volume)
bn.CloseFuturesMarketOrder(symbol, "MARKET", volume, futures.PositionSideType(side))
SetLock(symbol, "CLOSE."+side, 5)
//content := fmt.Sprintf("快速平仓 %s %s Volume:%s", symbol, side, volume)
//models.WriteSimpleLog(models.LogType_Close, p.Strategy.KeyName, content)
}
// 网格动态平仓
func (bn *BinanceClient) Dynamic(p *Spec, symbol string, currentPrice float64, orders []*models.QuantOrders, closeingProfitRate, qty float64) {
// 最小持仓保证金
equity := AccountsAssets.Get("USDT").AccountEquity
newMargin := utils.FloatRound(equity*p.StrategyConf.MarginMultipleByBalance, 1)
if newMargin < p.StrategyConf.Margin {
newMargin = p.StrategyConf.Margin
}
// 最大保证金:账户保证金 * 0.220% / 交易币对数 = 最大持仓保证金
newMaxMargin := math.Ceil(equity * 0.2 / float64(len(p.AllowSymbols)))
// 设置止盈率
for _, row := range orders {
// 计算利润,计算回报率
profit, profitRate := calProfitRate_V2(symbol, row.Side, float64(p.StrategyConf.Leverage), row.OpenPrice, currentPrice, row.Volume)
profit = utils.FloatRound(profit, 3)
profitRate = utils.FloatRound(profitRate, 3)
// 开仓保证金计算
if qty <= 0 {
qty = QtyBalByFloat(
currentPrice,
newMargin,
p.StrategyConf.Leverage,
p.SymbolsSetting[symbol],
)
}
quantity := utils.Float64ToString(qty)
// cache key.
CacheKey := fmt.Sprintf("%s_%s", row.Symbol, row.Side)
// 网格计算
NewProfitCell := profitRate / p.StrategyConf.DefCloseingGridPrice
NewProfitCell = math.Ceil(NewProfitCell)
log.Println("Dynamic", row.Symbol, row.Side, "Profit:", profit, "ProfitRate", profitRate, "NewProfitCell", NewProfitCell, "closeingProfitRate", closeingProfitRate)
// 增持处理,持仓的利润达到设定的增持仓位百分比
if p.StrategyConf.AddPositionOn && profitRate <= p.StrategyConf.AddThreshold && row.MarginSize < newMaxMargin {
// 增持处理,持仓的利润达到设定的增持仓位百分比
newAddThreshold := p.StrategyConf.AddThreshold
if row.Volume > (qty * 1.95) {
newAddThreshold = newAddThreshold * 2
}
if profitRate <= newAddThreshold {
if !IsLock(symbol, "OPEN."+row.Side) {
bn.AppendOrder(symbol, row.Side, quantity)
SetLock(symbol, "OPEN."+row.Side, 5)
continue
}
}
}
// 收益处理: 收益率没达到默认收益率,跳过
// 以下是收益率》=默认收益率的逻辑的业务逻辑
// 读取缓存
lastProfitCell, exists := bn.LastUPL[CacheKey]
if !exists {
// 缓存处理: 缓存不存在
// 收益处理: 收益率达到默认收益率
if profitRate >= closeingProfitRate {
log.Println("###", CacheKey, "profit", profit, "profitRate", profitRate, ">=", "closeingProfitRate", closeingProfitRate, "NewProfitCell", NewProfitCell)
bn.LastUPL[CacheKey] = NewProfitCell
}
} else {
log.Println("$$$", CacheKey, "profit", profit, "profitRate", profitRate, ">", "closeingProfitRate", closeingProfitRate, "NewProfitCell", NewProfitCell, "lastProfitCell", lastProfitCell)
if profitRate >= closeingProfitRate {
// 缓存处理: 存在缓存
// 判断: NewProfitCell < lastProfitCell 当前最新网格值 < 缓存的上一次网格值
if NewProfitCell < lastProfitCell {
// 缓存的收益低于当前的CELL则平仓。
log.Println("~~~", CacheKey, "profit:", profit, "ProfitRate", profitRate, " > CloseProfitRate", closeingProfitRate, "LastProfitCell", lastProfitCell, "< NewProfitCell", NewProfitCell)
// 平仓
bn.CloseFuturesMarketOrder(symbol, "MARKET", utils.Float64ToString(row.Volume), futures.PositionSideType(row.Side))
delete(bn.LastUPL, CacheKey)
} else {
// 高于上次盈利CELL更新缓存
log.Println("^^^ Up", CacheKey, "profit:", profit, "ProfitRate", profitRate, " > CloseProfitRate", closeingProfitRate, "ProfitCell", NewProfitCell)
// 更新缓存
bn.LastUPL[CacheKey] = NewProfitCell
}
}
}
}
}
func CreateSignal(identity, signalHex, orderId string) {
impl.DBService.Create(&models.QuantSignal{
Identity: identity,
Md5Hex: signalHex,
OrderId: orderId,
})
}

176
trade/binance_positions.go Normal file
View File

@@ -0,0 +1,176 @@
package trade
import (
"context"
"fmt"
"log"
"math"
"strings"
"git.apinb.com/bsm-sdk/core/utils"
"git.apinb.com/quant/strategy/internal/impl"
"git.apinb.com/quant/strategy/internal/models"
"github.com/vmihailenco/msgpack/v5"
)
var (
PositionsTotal int = 0
)
func GetPosSummary(PlanKeyName string) []string {
cacheBytes, err := impl.RedisService.Client.Get(impl.RedisService.Ctx, PlanKeyName+".PosSummary").Bytes()
if err != nil {
return nil
}
data := strings.Split(string(cacheBytes), ",")
// log.Println("GetPosSummary", PlanKeyName+".PosSummary", data)
if len(data) == 0 {
return nil
}
return data
}
func GetPosSymbols(PlanKeyName string) []string {
cacheBytes, err := impl.RedisService.Client.Get(impl.RedisService.Ctx, PlanKeyName+".PosSummary").Bytes()
if err != nil {
return nil
}
data := strings.Split(string(cacheBytes), ",")
if len(data) == 0 {
return nil
}
symbols := make([]string, 0)
for _, row := range data {
symbols = append(symbols, strings.Split(row, ".")[0])
}
return utils.ArrayRemoveRepeatString(symbols)
}
func ExistsPosition(PlanKeyName string, symbol string) PositionStatus {
data := GetPosSummary(PlanKeyName)
if len(data) == 0 {
return NoPositions
}
LongExists := utils.ArrayInString(symbol+".LONG", data)
ShortExists := utils.ArrayInString(symbol+".SHORT", data)
if LongExists && ShortExists {
return BothPositions
}
//log.Println("ExistsPosition", symbol, LongExists, ShortExists)
return GetPositionStats(LongExists, ShortExists)
}
func GetPositions(PlanKeyName string, symbol string) ([]*models.QuantOrders, error) {
cacheBytes, err := impl.RedisService.Client.Get(impl.RedisService.Ctx, PlanKeyName+".PosOrders").Bytes()
if err != nil {
return nil, err
}
var orders map[string][]*models.QuantOrders
err = msgpack.Unmarshal(cacheBytes, &orders)
if err != nil {
return nil, err
}
if _, ok := orders[symbol]; !ok {
return nil, fmt.Errorf("%s %s Not Found Position", PlanKeyName, symbol)
}
return orders[symbol], nil
}
func RefreshPositions(p *Spec) (map[string][]*models.QuantOrders, error) {
orders := make(map[string][]*models.QuantOrders, 0)
var summary []string
switch p.Api.Exchange {
case "BINANCE":
summary, orders = p.BinanceClient.GetPositions()
break
case "BITGET":
//summary, orders, _ := GetPositions_Bitget(api)
break
default:
return nil, fmt.Errorf("Not Found Exchange", p.Api.Exchange)
}
// 统地订单数量
PositionsTotal = len(summary)
if PositionsTotal == 0 {
impl.RedisService.Client.Del(impl.RedisService.Ctx, p.PlanKeyName+".PosSummary").Result()
impl.RedisService.Client.Del(impl.RedisService.Ctx, p.PlanKeyName+".PosOrders").Result()
return nil, nil
}
_, err := impl.RedisService.Client.Set(impl.RedisService.Ctx, p.PlanKeyName+".PosSummary", strings.Join(summary, ","), 0).Result()
if err != nil {
return nil, err
}
// 序列化为 MessagePack
ordersPack, _ := msgpack.Marshal(orders)
_, err = impl.RedisService.Client.Set(impl.RedisService.Ctx, p.PlanKeyName+".PosOrders", ordersPack, 0).Result()
if err != nil {
return nil, err
}
return orders, nil
}
func (bn *BinanceClient) GetPositions() ([]string, map[string][]*models.QuantOrders) {
data, err := bn.Futures.NewGetPositionRiskV3Service().Do(context.Background())
if err != nil {
log.Println("GetPositions_Binance", err)
return nil, nil
}
// jsonBytes, _ := json.Marshal(data)
// fmt.Println(string(jsonBytes))
positionData := make(map[string][]*models.QuantOrders, 0)
var PositionSummary []string
for _, row := range data {
amt := utils.String2Float64(row.PositionAmt)
if amt == 0 {
continue
}
side := strings.ToUpper(row.PositionSide)
PositionSummary = append(PositionSummary, row.Symbol+"."+side)
record := &models.QuantOrders{
Symbol: row.Symbol,
Side: side,
OpenPrice: utils.String2Float64(row.BreakEvenPrice), // 开仓成本价
Volume: math.Abs(utils.String2Float64(row.PositionAmt)), // 交易币成交数量
MarginSize: utils.String2Float64(row.InitialMargin), // 计价币成交数量
Status: "1", // 成交
}
/* TODO: 暂时不计算包括成本的均价
// 总成本 = (开仓价格 *开仓数量)+手续费
decOpenPrice := decimal.NewFromFloat(record.OpenPrice)
decVolume := decimal.NewFromFloat(record.Volume)
decFee := decimal.NewFromFloat(record.Fee)
totalCost := decOpenPrice.Mul(decVolume)
totalCost = totalCost.Add(decFee)
// 均价 = 总成本 / 总开仓数量
record.AvgPrice, _ = totalCost.Div(decVolume).Float64()
*/
positionData[row.Symbol] = append(positionData[row.Symbol], record)
}
return PositionSummary, positionData
}

270
trade/bitget_new.go Normal file
View File

@@ -0,0 +1,270 @@
package trade
import (
"encoding/json"
"errors"
"log"
"strings"
"git.apinb.com/bsm-sdk/core/utils"
"git.apinb.com/quant/strategy/internal/core/market"
"github.com/bitget-golang/sdk-api/config"
bitget "github.com/bitget-golang/sdk-api/pkg/client/v2"
)
type BitgetClient struct {
LastUPL map[string]float64
AccountClient *bitget.MixAccountClient
MarketClient *bitget.MixMarketClient
OrderClient *bitget.MixOrderClient
}
type SymbolResp struct {
Code string `json:"code"`
Data []SymbolDataResp `json:"data"`
Msg string `json:"msg"`
RequestTime int64 `json:"requestTime"`
}
type SymbolDataResp struct {
BaseCoin string `json:"baseCoin"`
BuyLimitPriceRatio string `json:"buyLimitPriceRatio"`
DeliveryStartTime string `json:"deliveryStartTime"`
DeliveryTime string `json:"deliveryTime"`
FeeRateUpRatio string `json:"feeRateUpRatio"`
FundInterval string `json:"fundInterval"`
LaunchTime string `json:"launchTime"`
LimitOpenTime string `json:"limitOpenTime"`
MaintainTime string `json:"maintainTime"`
MakerFeeRate string `json:"makerFeeRate"`
MaxLever string `json:"maxLever"`
MaxPositionNum string `json:"maxPositionNum"`
MaxProductOrderNum string `json:"maxProductOrderNum"`
MaxSymbolOrderNum string `json:"maxSymbolOrderNum"`
MinLever string `json:"minLever"`
MinTradeNum string `json:"minTradeNum"`
MinTradeUSDT string `json:"minTradeUSDT"`
OffTime string `json:"offTime"`
OpenCostUpRatio string `json:"openCostUpRatio"`
PosLimit string `json:"posLimit"`
PriceEndStep string `json:"priceEndStep"`
PricePlace string `json:"pricePlace"`
QuoteCoin string `json:"quoteCoin"`
SellLimitPriceRatio string `json:"sellLimitPriceRatio"`
SizeMultiplier string `json:"sizeMultiplier"`
SupportMarginCoins []string `json:"supportMarginCoins"`
Symbol string `json:"symbol"`
SymbolStatus string `json:"symbolStatus"`
SymbolType string `json:"symbolType"`
TakerFeeRate string `json:"takerFeeRate"`
VolumePlace string `json:"volumePlace"`
}
type AccountsResp struct {
Code string `json:"code"`
Data []struct {
MarginCoin string `json:"marginCoin"`
Locked string `json:"locked"`
Available string `json:"available"`
CrossedMaxAvailable string `json:"crossedMaxAvailable"`
IsolatedMaxAvailable string `json:"isolatedMaxAvailable"`
MaxTransferOut string `json:"maxTransferOut"`
AccountEquity string `json:"accountEquity"`
UsdtEquity string `json:"usdtEquity"`
BtcEquity string `json:"btcEquity"`
CrossedRiskRate string `json:"crossedRiskRate"`
UnrealizedPL string `json:"unrealizedPL"`
Coupon string `json:"coupon"`
UnionTotalMagin string `json:"unionTotalMagin"`
UnionAvailable string `json:"unionAvailable"`
UnionMm string `json:"unionMm"`
AssetList []struct {
Coin string `json:"coin"`
Balance string `json:"balance"`
Available string `json:"available"`
} `json:"assetList"`
IsolatedMargin string `json:"isolatedMargin"`
CrossedMargin string `json:"crossedMargin"`
CrossedUnrealizedPL string `json:"crossedUnrealizedPL"`
IsolatedUnrealizedPL string `json:"isolatedUnrealizedPL"`
AssetMode string `json:"assetMode"`
} `json:"data"`
Msg string `json:"msg"`
RequestTime int64 `json:"requestTime"`
}
type AllPositionResp struct {
Code string `json:"code"`
Data []*BitgetPositionData `json:"data"`
Msg string `json:"msg"`
RequestTime int64 `json:"requestTime"`
}
type BitgetPositionData struct {
AchievedProfits string `json:"achievedProfits"`
Available string `json:"available"`
BreakEvenPrice string `json:"breakEvenPrice"`
CTime string `json:"cTime"`
DeductedFee string `json:"deductedFee"`
HoldSide string `json:"holdSide"`
KeepMarginRate string `json:"keepMarginRate"`
Leverage string `json:"leverage"`
LiquidationPrice string `json:"liquidationPrice"`
Locked string `json:"locked"`
MarginCoin string `json:"marginCoin"`
MarginMode string `json:"marginMode"`
MarginRatio string `json:"marginRatio"`
MarginSize string `json:"marginSize"`
MarkPrice string `json:"markPrice"`
OpenDelegateSize string `json:"openDelegateSize"`
OpenPriceAvg string `json:"openPriceAvg"`
PosMode string `json:"posMode"`
Symbol string `json:"symbol"`
Total string `json:"total"`
TotalFee string `json:"totalFee"`
UnrealizedPL string `json:"unrealizedPL"`
UnrealizedPLR string `json:"unrealizedPLR"`
}
type RespMapString struct {
Code string `json:"code"`
Data []map[string]string `json:"data"`
Msg string `json:"msg"`
RequestTime int64 `json:"requestTime"`
}
func NewBitgetClient(apiKey, apiSecret, passphrase string) *BitgetClient {
bg := &BitgetClient{
LastUPL: make(map[string]float64),
}
config.ApiKey = apiKey
config.SecretKey = apiSecret
config.PASSPHRASE = passphrase
config.BaseUrl = "https://api.bitget.com"
config.WsUrl = "wss://ws.bitget.com/v2/ws/private"
bg.AccountClient = new(bitget.MixAccountClient).Init()
bg.AccountClient.BitgetRestClient.ApiKey = config.ApiKey
bg.AccountClient.BitgetRestClient.ApiSecretKey = config.SecretKey
bg.AccountClient.BitgetRestClient.Passphrase = config.PASSPHRASE
bg.MarketClient = new(bitget.MixMarketClient).Init()
bg.MarketClient.BitgetRestClient.ApiKey = config.ApiKey
bg.MarketClient.BitgetRestClient.ApiSecretKey = config.SecretKey
bg.MarketClient.BitgetRestClient.Passphrase = config.PASSPHRASE
bg.OrderClient = new(bitget.MixOrderClient).Init()
bg.OrderClient.BitgetRestClient.ApiKey = config.ApiKey
bg.OrderClient.BitgetRestClient.ApiSecretKey = config.SecretKey
bg.OrderClient.BitgetRestClient.Passphrase = config.PASSPHRASE
Info("BitgetClient init ok")
return bg
}
func (bg *BitgetClient) GetFuturesAccountBalance() (string, map[string]*AccountsBalance, error) {
args := map[string]string{"productType": "USDT-FUTURES"}
res, err := bg.AccountClient.Accounts(args)
if err != nil {
return res, nil, err
}
// assets, _ := json.Marshal(res.Assets)
// log.Println("==>", string(assets))
var acResp AccountsResp
err = json.Unmarshal([]byte(res), &acResp)
if err != nil {
return res, nil, err
}
if acResp.Code != "00000" {
return res, nil, errors.New(acResp.Msg)
}
ac := make(map[string]*AccountsBalance)
for _, v := range acResp.Data {
ac[strings.ToUpper(v.MarginCoin)] = &AccountsBalance{
AccountEquity: utils.String2Float64(v.AccountEquity),
Available: utils.String2Float64(v.Available),
}
}
return res, ac, nil
}
func (bg *BitgetClient) GetSymbolSetting(cli *bitget.MixMarketClient, symbols []string) (map[string]*market.PairSetting, error) {
args := map[string]string{
"productType": "USDT-FUTURES",
}
resp, err := bg.MarketClient.Contracts(args)
if err != nil {
return nil, err
}
var reply SymbolResp
json.Unmarshal([]byte(resp), &reply)
if err != nil {
return nil, err
}
if len(reply.Data) == 0 {
return nil, errors.New("data is empty")
}
data := make(map[string]*market.PairSetting)
for _, item := range reply.Data {
if utils.ArrayInString(item.Symbol, symbols) {
data[item.Symbol] = &market.PairSetting{
Symbol: item.Symbol,
BaseAssetPrecision: 0,
QuantityPrecision: utils.String2Int(item.VolumePlace),
PricePrecision: utils.String2Int(item.PricePlace),
MinTradeNum: utils.String2Float64(item.MinTradeNum),
MinNotional: utils.String2Float64(item.MinTradeUSDT),
Commit: item.MaxLever,
}
}
}
return data, nil
}
func (bg *BitgetClient) SetLeverage(symbol string, leve string) {
args := map[string]string{
"productType": "USDT-FUTURES",
"marginCoin": "USDT",
"symbol": symbol,
"leverage": leve,
}
_, err := bg.AccountClient.SetLeverage(args)
if err != nil {
panic(err)
}
log.Println("[INFO]", symbol, "SetLeverage:", leve)
}
// 获取交易对价格,限速规则: 20次/1s (IP)
func (bg *BitgetClient) GetSymbolPrice(symbol string) (map[string]string, error) {
args := map[string]string{
"productType": "USDT-FUTURES",
"symbol": symbol,
}
resp, err := bg.MarketClient.SymbolPrice(args)
if err != nil {
return nil, err
}
var reply RespMapString
json.Unmarshal([]byte(resp), &reply)
if err != nil {
return nil, err
}
if reply.Code != "00000" {
return nil, errors.New(reply.Msg)
}
return reply.Data[0], nil
}

219
trade/bitget_order.go Normal file
View File

@@ -0,0 +1,219 @@
package trade
import (
"errors"
"fmt"
"log"
"math"
"git.apinb.com/bsm-sdk/core/utils"
"git.apinb.com/quant/strategy/internal/models"
)
func (bg *BitgetClient) OpenOrder_market(symbol, side, size string) error {
// 上涨计算
// 判断:开仓锁,开仓信号
if IsLock(symbol, "OPEN."+side) {
log.Println("103", "判断:开仓锁,开仓信号")
return errors.New("开仓锁")
}
args := map[string]string{
"productType": "USDT-FUTURES",
"symbol": symbol,
"marginMode": "crossed",
"marginCoin": "USDT",
"orderType": "market", // 订单类型,limit: 限价单market: 市价单
"size": size,
"side": GetSide(side), // 下单方向: buy,long,多仓sell,short,卖
"tradeSide": "open", // 开仓
"force": "fok", // ioc: 无法立即成交的部分就撤销,fok: 无法全部立即成交就撤销,gtc: 普通订单, 订单会一直有效,直到被成交或者取消,限价单limit时必填若省略则默认为gtc
"clientOid": GenClientId(),
}
resp, err := bg.OrderClient.PlaceOrder(args)
if err != nil {
log.Println("[ERROR] #001 OpenOrder", err)
return err
}
log.Println("[INFO] OpenOrder", resp)
// 加锁
SetLock(symbol, "OPEN."+side, 5)
return nil
}
func (bg *BitgetClient) OpenOrder_limit(symbol, side, size, price string) error {
// 上涨计算
// 判断:开仓锁,开仓信号
if IsLock(symbol, "OPEN."+side) {
log.Println("103", "判断:开仓锁,开仓信号")
return errors.New("开仓锁")
}
args := map[string]string{
"productType": "USDT-FUTURES",
"symbol": symbol,
"marginMode": "crossed",
"marginCoin": "USDT",
"orderType": "limit", // 订单类型,limit: 限价单market: 市价单
"size": size,
"side": GetSide(side), // 下单方向: buy,long,多仓sell,short,卖
"tradeSide": "open", // 开仓
"force": "fok", // ioc: 无法立即成交的部分就撤销,fok: 无法全部立即成交就撤销,gtc: 普通订单, 订单会一直有效,直到被成交或者取消,限价单limit时必填若省略则默认为gtc
"price": price,
"clientOid": GenClientId(),
}
resp, err := bg.OrderClient.PlaceOrder(args)
if err != nil {
log.Println("[ERROR] #001 OpenOrder", err)
return err
}
log.Println("[INFO] OpenOrder", resp)
// 加锁
SetLock(symbol, "OPEN."+side, 5)
return nil
}
func (bg *BitgetClient) CloseOrder(symbol, side, size string) error {
args := map[string]string{
"productType": "USDT-FUTURES",
"symbol": symbol,
"size": size,
"side": GetSide(side), // 下单方向: buy,long,多仓sell,short,卖
"tradeSide": "close", // 平仓
"marginMode": "crossed",
"marginCoin": "USDT",
"orderType": "market",
}
_, err := bg.OrderClient.PlaceOrder(args)
if err != nil {
log.Println("[ERROR] #004 CloseOrder", err)
return err
}
return nil
}
// 网格动态平仓
func (bg *BitgetClient) Dynamic(scfg *models.StrategyConf, symbol string, qty, currentPrice float64, orders []*models.QuantOrders, closeingProfitRate float64) {
// 利润率 <= 0时亏损并没有达到加仓的百分比时或是超过最大仓位的投入时,返回不做处理
newMaxMargin := AccountsAssets.Get("USDT").AccountEquity * 0.015
newMaxMargin = utils.FloatRound(newMaxMargin, 2)
// 设置止盈率
for _, row := range orders {
// 计算利润,计算回报率
profit, profitRate := bg.calProfitRate_V2(symbol, row.Side, float64(scfg.Leverage), row.OpenPrice, currentPrice, row.Volume)
// 得到持仓与开仓数量的倍数
multiple := math.Round(row.Volume / qty)
if multiple < 1 {
multiple = 1
}
// 根据倍数计算补仓百分比。
maxAddThreshold := scfg.AddThreshold * multiple * 1.2
if multiple > 3 {
maxAddThreshold = scfg.AddThreshold * multiple * 1.6
}
// cache key.
CacheKey := fmt.Sprintf("%s_%s", row.Symbol, row.Side)
// 网格计算
NewProfitCell := profitRate / scfg.DefCloseingGridPrice
NewProfitCell = math.Ceil(NewProfitCell)
// 打印日志
log.Println("Dynamic", row.Symbol, row.Side, "Volume:", row.Volume, "OpenPrice:", row.OpenPrice, "Profit:", profit, "ProfitRate", profitRate, "NewProfitCell", NewProfitCell, "closeingProfitRate", closeingProfitRate, "newMaxMargin", newMaxMargin, "maxAddThreshold", maxAddThreshold)
// 止损处理
if scfg.StopLossOn && profitRate <= scfg.StopThreshold {
log.Println("!!! Stop", CacheKey, "StopThreshold", scfg.StopThreshold, "profitRate", profitRate)
// 平仓
bg.CloseOrder(row.Symbol, row.Side, utils.Float64ToString(row.Volume))
continue
}
if scfg.AddPositionOn && profitRate <= maxAddThreshold && row.MarginSize < newMaxMargin {
if !IsLock(symbol, "OPEN."+row.Side) {
qtyStr := utils.Float64ToString(qty)
bg.OpenOrder_market(symbol, row.Side, qtyStr)
SetLock(symbol, "OPEN."+row.Side, 5)
continue
}
}
// 读取缓存
lastProfitCell, exists := bg.LastUPL[CacheKey]
if !exists {
// 缓存处理: 缓存不存在
// 收益处理: 收益率达到默认收益率
if profitRate >= closeingProfitRate {
log.Println("###", CacheKey, "profit", profit, "profitRate", profitRate, ">=", "closeingProfitRate", closeingProfitRate, "NewProfitCell", NewProfitCell, "newMaxMargin", newMaxMargin)
bg.LastUPL[CacheKey] = NewProfitCell
}
} else {
log.Println("$$$", CacheKey, "profit", profit, "profitRate", profitRate, ">", "closeingProfitRate", closeingProfitRate, "NewProfitCell", NewProfitCell, "lastProfitCell", lastProfitCell, "newMaxMargin", newMaxMargin)
if profitRate >= closeingProfitRate {
// 缓存处理: 存在缓存
// 判断: NewProfitCell < lastProfitCell 当前最新网格值 < 缓存的上一次网格值
if NewProfitCell < lastProfitCell {
// 缓存的收益低于当前的CELL则平仓。
log.Println("~~~", CacheKey, "profit:", profit, "ProfitRate", profitRate, " > CloseProfitRate", closeingProfitRate, "LastProfitCell", lastProfitCell, "< NewProfitCell", NewProfitCell)
// 平仓
bg.CloseOrder(symbol, row.Side, utils.Float64ToString(row.Volume))
delete(bg.LastUPL, CacheKey)
// 更新帐号资金
_, ac, err := bg.GetFuturesAccountBalance()
if err != nil {
log.Println("bg.GetFuturesAccountBalance", err.Error())
} else {
AccountsAssets.SetData(ac)
}
} else {
// 高于上次盈利CELL更新缓存
log.Println("^^^ Up", CacheKey, "profit:", profit, "ProfitRate", profitRate, " > CloseProfitRate", closeingProfitRate, "ProfitCell", NewProfitCell)
// 更新缓存
bg.LastUPL[CacheKey] = NewProfitCell
}
}
}
}
}
func (bg *BitgetClient) calProfitRate_V2(symbol, side string, leverage, avgPrice, currentPrice, volume float64) (profit, profitRate float64) {
// 计算利润
switch side {
case "LONG":
profit = (currentPrice - avgPrice) * volume
//Debug("CalculateProfit_Long", symbol, side, "AvgPrice:", avgPrice.String(), "CurrentPrice:", currentPrice.String(), "volume:", volume.String())
case "SHORT":
//Debug("CalculateProfit_Short", symbol, side, "AvgPrice:", avgPrice.String(), "CurrentPrice:", currentPrice.String(), "volume:", volume.String())
profit = (avgPrice - currentPrice) * volume
}
// 计算回报率
if profit == 0 {
return profit, profitRate
}
cost := avgPrice * volume
actualInvestment := cost / leverage
profitRate = profit / actualInvestment
profitRate = utils.FloatRound(profitRate, 3)
return profit, profitRate
}

223
trade/bitget_positions.go Normal file
View File

@@ -0,0 +1,223 @@
package trade
import (
"encoding/json"
"log"
"strings"
"time"
"git.apinb.com/bsm-sdk/core/utils"
"git.apinb.com/quant/strategy/internal/impl"
"git.apinb.com/quant/strategy/internal/models"
"github.com/bitget-golang/sdk-api/pkg/client/ws"
"github.com/bitget-golang/sdk-api/types"
"github.com/robfig/cron/v3"
"github.com/vmihailenco/msgpack/v5"
)
type WebsocketPositionMessage struct {
Action string `json:"action"`
Arg struct {
Channel string `json:"channel"`
InstID string `json:"instId"`
InstType string `json:"instType"`
} `json:"arg"`
Data []struct {
AchievedProfits string `json:"achievedProfits"`
AssetMode string `json:"assetMode"`
AutoMargin string `json:"autoMargin"`
Available string `json:"available"`
BreakEvenPrice string `json:"breakEvenPrice"`
CTime string `json:"cTime"`
DeductedFee string `json:"deductedFee"`
Frozen string `json:"frozen"`
HoldSide string `json:"holdSide"`
InstID string `json:"instId"` // symbol
KeepMarginRate string `json:"keepMarginRate"`
Leverage int64 `json:"leverage"`
LiquidationPrice string `json:"liquidationPrice"`
MarginCoin string `json:"marginCoin"`
MarginMode string `json:"marginMode"`
MarginRate string `json:"marginRate"`
MarginSize string `json:"marginSize"`
MarkPrice string `json:"markPrice"`
OpenPriceAvg string `json:"openPriceAvg"`
PosID string `json:"posId"`
PosMode string `json:"posMode"`
Total string `json:"total"`
TotalFee string `json:"totalFee"`
UTime string `json:"uTime"`
UnrealizedPL string `json:"unrealizedPL"`
UnrealizedPLR string `json:"unrealizedPLR"`
} `json:"data"`
Ts int64 `json:"ts"`
}
var PlanKeyName string
func (bg *BitgetClient) RefreshByApi(planKeyName string) {
PlanKeyName = planKeyName
// 根据基本币监控帐号可用资金变动仓位以及最近7天的交易情况
c := cron.New(cron.WithSeconds())
c.AddFunc("@every 1s", func() {
bg.PositionsByApi(planKeyName)
})
c.Start()
}
func (bg *BitgetClient) RefreshByWebSocket(planKeyName string) {
// 初始获取持仓
PlanKeyName = planKeyName
bg.PositionsByApi(planKeyName)
// 根据WebSocket推送实时更新持仓添加重连机制
go func() {
for {
var channels []types.SubscribeReq
positions := types.SubscribeReq{
InstType: "USDT-FUTURES",
Channel: "positions",
InstId: "default",
}
channels = append(channels, positions)
wsClient := new(ws.BitgetWsClient).Init(true, receiveHandler, errorHandler)
wsClient.SubscribeDef(channels)
log.Println("Bitget Websocket Connect...")
// Connect() 是阻塞调用,当连接断开时会返回
wsClient.Connect()
// 连接断开后等待5秒后重连
log.Println("Bitget Websocket disconnected, will reconnect in 5 seconds...")
time.Sleep(5 * time.Second)
// 重新获取持仓数据
bg.PositionsByApi(planKeyName)
}
}()
}
func receiveHandler(message string) {
var reply WebsocketPositionMessage
err := json.Unmarshal([]byte(message), &reply)
if err != nil {
log.Println("WatchPositions JSON Unmarshal Error:", err)
return
}
if len(reply.Data) == 0 {
impl.RedisService.Client.Del(impl.RedisService.Ctx, PlanKeyName+".PosSummary").Result()
impl.RedisService.Client.Del(impl.RedisService.Ctx, PlanKeyName+".PosOrders").Result()
log.Println("WatchPositions:", "No Positions")
return
}
orders := make(map[string][]*models.QuantOrders, 0)
var summary []string
for _, v := range reply.Data {
side := strings.ToUpper(v.HoldSide)
// 假设 v.CTime 是一个表示毫秒时间戳的字符串
t := time.UnixMilli(utils.String2Int64(v.CTime))
record := &models.QuantOrders{
Exchange: "BITGET",
Symbol: v.InstID,
Side: side,
Fee: utils.String2Float64(v.TotalFee),
OpenPrice: utils.String2Float64(v.OpenPriceAvg), // 开仓均价
Volume: utils.String2Float64(v.Total), // 交易币成交数量
MarginSize: utils.String2Float64(v.MarginSize), // 计价币成交数量
Leverage: int(v.Leverage),
UnrealizedPL: utils.String2Float64(v.UnrealizedPL),
CreatedAt: t, // 毫秒转time.Time
}
orders[v.InstID] = append(orders[v.InstID], record)
log.Println("Record", record)
summary = append(summary, v.InstID+"."+side)
}
_, err = impl.RedisService.Client.Set(impl.RedisService.Ctx, PlanKeyName+".PosSummary", strings.Join(summary, ","), 0).Result()
if err != nil {
log.Println("WatchPositions:", err)
}
// 序列化为 MessagePack
ordersPack, _ := msgpack.Marshal(orders)
_, err = impl.RedisService.Client.Set(impl.RedisService.Ctx, PlanKeyName+".PosOrders", ordersPack, 0).Result()
if err != nil {
log.Println("WatchPositions:", err)
}
}
func errorHandler(message string) {
log.Println("WebSocket Error:", message)
}
func (bg *BitgetClient) PositionsByApi(planKeyName string) {
args := map[string]string{
"productType": "USDT-FUTURES",
"marginCoin": "USDT",
}
log.Println("===", "RefreshPositions:", planKeyName, "===")
resp, err := bg.AccountClient.AllPosition(args)
if err != nil {
log.Println("WatchPositions:", err)
return
}
var reply AllPositionResp
json.Unmarshal([]byte(resp), &reply)
if err != nil {
log.Println("WatchPositions:", err)
return
}
if reply.Code != "00000" {
log.Println("WatchPositions:", reply.Code+" "+reply.Msg)
return
}
if len(reply.Data) == 0 {
impl.RedisService.Client.Del(impl.RedisService.Ctx, planKeyName+".PosSummary").Result()
impl.RedisService.Client.Del(impl.RedisService.Ctx, planKeyName+".PosOrders").Result()
log.Println("WatchPositions:", "No Positions")
return
}
orders := make(map[string][]*models.QuantOrders, 0)
var summary []string
for _, v := range reply.Data {
side := strings.ToUpper(v.HoldSide)
// 假设 v.CTime 是一个表示毫秒时间戳的字符串
t := time.UnixMilli(utils.String2Int64(v.CTime))
record := &models.QuantOrders{
Exchange: "BITGET",
Symbol: v.Symbol,
Side: side,
Fee: utils.String2Float64(v.TotalFee),
OpenPrice: utils.String2Float64(v.OpenPriceAvg), // 开仓均价
Volume: utils.String2Float64(v.Total), // 交易币成交数量
MarginSize: utils.String2Float64(v.MarginSize), // 计价币成交数量
Leverage: utils.String2Int(v.Leverage),
UnrealizedPL: utils.String2Float64(v.UnrealizedPL),
CreatedAt: t, // 毫秒转time.Time
}
orders[v.Symbol] = append(orders[v.Symbol], record)
log.Println("Record", record)
summary = append(summary, v.Symbol+"."+side)
}
_, err = impl.RedisService.Client.Set(impl.RedisService.Ctx, planKeyName+".PosSummary", strings.Join(summary, ","), 0).Result()
if err != nil {
log.Println("WatchPositions:", err)
}
// 序列化为 MessagePack
ordersPack, _ := msgpack.Marshal(orders)
_, err = impl.RedisService.Client.Set(impl.RedisService.Ctx, planKeyName+".PosOrders", ordersPack, 0).Result()
if err != nil {
log.Println("WatchPositions:", err)
}
}

57
trade/calc_profit.go Normal file
View File

@@ -0,0 +1,57 @@
package trade
import (
"git.apinb.com/bsm-sdk/core/utils"
"github.com/shopspring/decimal"
)
// 计算收益及收益率
func calProfitRate(symbol, side string, leverage, avgPrice, currentPrice, volume decimal.Decimal) (decimal.Decimal, decimal.Decimal) {
var profit decimal.Decimal = decimal.Zero
var profitRate decimal.Decimal = decimal.Zero
// 计算利润
switch side {
case "LONG":
profit = CalculateProfit_Long(avgPrice, currentPrice, volume)
//Debug("CalculateProfit_Long", symbol, side, "AvgPrice:", avgPrice.String(), "CurrentPrice:", currentPrice.String(), "volume:", volume.String())
case "SHORT":
//Debug("CalculateProfit_Short", symbol, side, "AvgPrice:", avgPrice.String(), "CurrentPrice:", currentPrice.String(), "volume:", volume.String())
profit = CalculateProfit_Short(avgPrice, currentPrice, volume)
}
// 计算回报率
if profit.IsZero() {
return profit, profitRate
}
cost := avgPrice.Mul(volume)
actualInvestment := cost.Div(leverage)
profitRate = profit.Div(actualInvestment)
return profit, profitRate
}
// 计算收益及收益率
func calProfitRate_V2(symbol, side string, leverage, avgPrice, currentPrice, volume float64) (profit, profitRate float64) {
// 计算利润
switch side {
case "LONG":
profit = (currentPrice - avgPrice) * volume
//Debug("CalculateProfit_Long", symbol, side, "AvgPrice:", avgPrice.String(), "CurrentPrice:", currentPrice.String(), "volume:", volume.String())
case "SHORT":
//Debug("CalculateProfit_Short", symbol, side, "AvgPrice:", avgPrice.String(), "CurrentPrice:", currentPrice.String(), "volume:", volume.String())
profit = (avgPrice - currentPrice) * volume
}
// 计算回报率
if profit == 0 {
return profit, profitRate
}
cost := avgPrice * volume
actualInvestment := cost / leverage
profitRate = profit / actualInvestment
profitRate = utils.FloatRound(profitRate, 3)
return profit, profitRate
}

95
trade/calc_qty.go Normal file
View File

@@ -0,0 +1,95 @@
package trade
import (
"errors"
"log"
"math"
"git.apinb.com/bsm-sdk/core/utils"
"git.apinb.com/quant/strategy/internal/core/market"
)
// 计算下单数量
// 计算逻辑
// 保证金 = 可用余额 × 杠杆倍数 = 6U × 2 = 12U
// 下单数量 = 可用保证金 / 价格
func QtyBal(price, margin float64, leverage int, cfg *market.PairSetting) string {
qty := QtyBalByFloat(price, margin, leverage, cfg)
return utils.Float64ToString(qty)
}
func QtyBalByFloat(price, margin float64, leverage int, cfg *market.PairSetting) float64 {
// 计算可用保证金
availableMargin := margin * float64(leverage)
// 计算下单数量 (减去手续费考虑)
// Binance 现货杠杆交易手续费通常是 0.1% 或更低
quantity := availableMargin / price // 考虑手续费
if quantity < cfg.MinTradeNum {
quantity = cfg.MinTradeNum
}
// 确保下单数量的价值未超过最小名义价值
if quantity*price < cfg.MinNotional {
quantity = quantity * 2
}
return utils.FloatRound(quantity, cfg.QuantityPrecision)
}
// 计算下单数量 V2
// 计算逻辑
// 保证金 = 可用余额 × 10% = 10U × 0.1 = 1U
// 下单数量 = 保证金 / 价格
func QtyPer(margin, price, per float64, leverage int, cfg *market.PairSetting) (string, error) {
// 计算可用保证金
availableMargin := margin * float64(leverage) * per
// 计算下单数量 (减去手续费考虑)
// Binance 现货杠杆交易手续费通常是 0.1% 或更低
quantity := availableMargin / price // 考虑手续费
if quantity < cfg.MinTradeNum {
quantity = cfg.MinTradeNum
}
// 确保下单数量的价值超过20
if quantity*price < cfg.MinNotional {
quantity = quantity + cfg.MinTradeNum
}
qty := FmtNumber(quantity, cfg.QuantityPrecision)
if qty == "0" {
return "0", errors.New("qty is zero")
}
log.Println("QtyPer:", "margin", "price", "per", "leverage", "MinTradeNum", "MinNotional", "qty")
log.Println("QtyPer:", margin, price, per, leverage, cfg.MinTradeNum, cfg.MinNotional, qty)
return qty, nil
}
func QtyMin(price float64, leverage int, cfg *market.PairSetting) string {
// 计算下单数量 (减去手续费考虑)
// Binance 现货杠杆交易手续费通常是 0.1% 或更低
quantity := cfg.MinNotional / price
if quantity < cfg.MinTradeNum {
quantity = cfg.MinTradeNum
}
quantity = quantity + cfg.MinTradeNum // 多加一个最小交易数量,确保够下单
qty := FmtNumber(quantity, cfg.QuantityPrecision)
return qty
}
func CalcMinQty_Binance(lotSize, minNotional, price float64) float64 {
minQtyByLot := lotSize
minQtyByNotional := minNotional / price
return math.Max(minQtyByLot, minQtyByNotional)
}

123
trade/calc_tech.go Normal file
View File

@@ -0,0 +1,123 @@
package trade
import (
"fmt"
"math"
"git.apinb.com/quant/strategy/types"
"github.com/markcheno/go-talib"
"github.com/shopspring/decimal"
)
// 计算合约的利润:多头
func CalculateProfit_Long(openPrice decimal.Decimal, currentPrice decimal.Decimal, quantity decimal.Decimal) decimal.Decimal {
// 多头头寸:利润 = (当前价 - 开仓价) * 数量
return (currentPrice.Sub(openPrice)).Mul(quantity)
}
// 计算合约的利润:空头
func CalculateProfit_Short(openPrice decimal.Decimal, currentPrice decimal.Decimal, quantity decimal.Decimal) decimal.Decimal {
// 空头头寸:利润 = (开仓价 - 当前价) * 数量
return (openPrice.Sub(currentPrice)).Mul(quantity)
}
// 这个方法的逻辑是合理的:它对输入的 value 进行“降噪”处理,只有当绝对值大于等于 threshold 时才认为有意义,否则视为 0。
// 然后根据降噪后的值判断正负性。
// threshold 由调用方传入,灵活性较高。
// 但注释有误,应该是“输入值绝对值小于 threshold 时视为0”而不是 0.03。
// 返回值语义清晰1 表示正,-1 表示负0 表示零。
func DenoiseAndJudge(value, threshold float64) int {
if math.Abs(value) < threshold {
return 0
}
if value > 0 {
return 1
}
if value < 0 {
return -1
}
return 0
}
// isOutOfRange 判断传入的 float64 是否大于 0.001 或小于 -0.001
func IsOutOfRange(f float64) int {
if f >= 0.001 {
return 1
}
if f <= -0.001 {
return -1
}
return 0
}
func EMA(inReal []float64, inTimePeriod int, round int) []float64 {
var newResult []float64
emaResult := talib.Ema(inReal, inTimePeriod)
for _, val := range emaResult {
if val == 0 {
continue
}
newResult = append(newResult, FloatRound(val, round))
}
return newResult
}
func ATR(k []*types.KLine, period int) float64 {
high := make([]float64, len(k))
low := make([]float64, len(k))
closes := make([]float64, len(k))
for _, line := range k {
high = append(high, line.High)
low = append(low, line.Low)
closes = append(closes, line.Close)
}
atr := talib.Atr(high, low, closes, period)
return atr[len(atr)-1]
}
func CheckCross(emaFast, emaSlow []float64, MinCrossStrength float64) (bool, string, string) {
if len(emaFast) != 2 || len(emaSlow) != 2 {
return false, "", "参数错误,emaFast或emaSlow长度必须为2"
}
// 检查是否有交叉
prevDiff := emaFast[0] - emaSlow[0] // 前一个点的差值
currentDiff := emaFast[1] - emaSlow[1] // 当前点的差值
// 检查是否有交叉(从负到正或从正到负)
if prevDiff*currentDiff >= 0 {
return false, "", "无交叉:" + fmt.Sprintf("prevDiff: %f, currentDiff: %f", prevDiff, currentDiff)
}
// 降噪:检查交叉强度是否足够大
diffChange := currentDiff - prevDiff
if abs(diffChange) < MinCrossStrength {
return false, "", "交叉强度太小,可能是噪音:" + fmt.Sprintf("diffChange: %f, MinCrossStrength: %f", diffChange, MinCrossStrength)
}
// 简化的趋势判断 - 放宽过于严格的条件
var trend string
if currentDiff > 0 {
// 快线上穿慢线,判断为上升趋势
// 不再要求快线和慢线都必须同时上升因为EMA交叉本身就表明趋势变化
trend = "UP"
} else {
// 快线下穿慢线,判断为下降趋势
// 不再要求快线和慢线都必须同时下降因为EMA交叉本身就表明趋势变化
trend = "DOWN"
}
return true, trend, ""
}
// 辅助函数:计算绝对值
func abs(x float64) float64 {
if x < 0 {
return -x
}
return x
}

31
trade/history.go Normal file
View File

@@ -0,0 +1,31 @@
package trade
import (
"context"
"time"
"git.apinb.com/bsm-sdk/core/utils"
)
func RefreshHistoryTotal(p *Spec) (tradeNum int64, pl float64) {
switch p.Api.Exchange {
case "BINANCE":
for _, v := range p.AllowSymbols {
data, err := p.BinanceClient.Futures.NewListAccountTradeService().Symbol(v).Do(context.Background())
if err == nil {
for _, res := range data {
tradeNum++
pl += utils.String2Float64(res.RealizedPnl)
}
}
time.Sleep(1 * time.Second)
}
break
case "BITGET":
//summary, orders, _ := GetPositions_Bitget(api)
break
default:
}
return
}

30
trade/lock.go Normal file
View File

@@ -0,0 +1,30 @@
package trade
import (
"time"
cache "github.com/patrickmn/go-cache"
)
var MemCache *cache.Cache
func NewLock() {
if MemCache != nil {
return
}
MemCache = cache.New(5*time.Minute, 10*time.Minute)
}
// 锁仓可以采用MemCache,Redis,File等。
func IsLock(symbol, side string) bool {
lockKey := symbol + ":" + side
_, found := MemCache.Get(lockKey)
if found {
return true
}
return false
}
func SetLock(symbol, side string, duration int64) {
MemCache.Set(symbol+":"+side, true, time.Duration(duration)*time.Second)
}

6
trade/new.go Normal file
View File

@@ -0,0 +1,6 @@
package trade
func NewTrade() {
NewLock()
NewAccounts()
}

59
trade/print.go Normal file
View File

@@ -0,0 +1,59 @@
package trade
import (
"log"
"strings"
)
var (
last_debug string
last_info string
last_warn string
last_error string
)
func Debug(msg ...string) string {
info := strings.Join(msg, " ")
if last_debug == info {
return ""
}
log.Println("[DEBUG]", info)
last_debug = info
return info
}
func Info(msg ...string) {
i := strings.Join(msg, " ")
if last_info == i {
return
}
log.Println("[INFO]", i)
last_info = i
}
func Error(code string, msg ...string) {
info := strings.Join(msg, " ")
if last_error == info {
return
}
log.Println("[ERROR] #", code, info)
last_error = info
}
func Watch(msg ...string) {
info := strings.Join(msg, " ")
if last_warn == info {
return
}
log.Println("[Watch] #", info)
last_warn = info
}
func Warn(msg ...string) {
info := strings.Join(msg, " ")
if last_warn == info {
return
}
log.Println("[WARN] #", info)
last_warn = info
}

54
trade/subscribe.go Normal file
View File

@@ -0,0 +1,54 @@
package trade
import (
"context"
"log"
"strings"
"git.apinb.com/quant/strategy/internal/impl"
)
// 接收行情
var tickerCancel context.CancelFunc
// 启动行情接收
func StartOnTicker(topics []string, do func(payload string, args []string)) {
ctx, cancel := context.WithCancel(context.Background())
tickerCancel = cancel
go func() {
for {
select {
case <-ctx.Done():
log.Println("OnTicker stopped")
return
default:
if len(topics) == 0 {
log.Println("No Topics")
return
}
sub := impl.RedisService.Client.Subscribe(impl.RedisService.Ctx, topics...)
for {
msg, err := sub.ReceiveMessage(impl.RedisService.Ctx)
if err != nil {
log.Println("Subscribe Error:", err)
// 连接断开时,关闭当前订阅并跳出内层循环,重新创建订阅
sub.Close()
break
}
args := strings.Split(msg.Channel, ":")
do(msg.Payload, args)
}
}
}
}()
}
// 停止行情接收
func StopOnTicker() {
if tickerCancel != nil {
tickerCancel()
tickerCancel = nil
}
}

69
trade/tech_index.go Normal file
View File

@@ -0,0 +1,69 @@
package trade
import (
"encoding/json"
"github.com/adshao/go-binance/v2/futures"
)
// 技术指标结果
type TechnicalsIndex struct {
NewPrice float64 `json:"new_price"`
Rsi float64 `json:"rsi"`
PriceRate []float64 `json:"price_rate"`
Today *TodayIndex `json:"today"`
Ema *EmaIndex `json:"ema"`
Boll *BollBandsIndex `json:"boll"`
TopBottom *TopBottomIndex `json:"top_bottom"`
Timeseq int64 `json:"timeseq"`
KLines []*futures.Kline `json:"k_lines"`
}
type TodayIndex struct {
Open float64 `json:"open"`
High float64 `json:"high"`
Low float64 `json:"low"`
Rate float64 `json:"rate"`
Status string `json:"status"`
}
type EmaIndex struct {
Max float64 `json:"max"`
Top float64 `json:"top"`
Avg float64 `json:"avg"`
End float64 `json:"end"`
Min float64 `json:"min"`
}
type TopBottomIndex struct {
High float64 `json:"high"`
Low float64 `json:"low"`
}
type BollBandsIndex struct {
Mid float64 `json:"mid"`
Upper float64 `json:"upper"`
Lower float64 `json:"lower"`
}
func NewTechnicalsIndex(from string) (*TechnicalsIndex, error) {
var ret TechnicalsIndex
err := json.Unmarshal([]byte(from), &ret)
return &ret, err
}
func (t *TechnicalsIndex) String() string {
if t == nil {
return ""
}
res, _ := json.Marshal(t)
return string(res)
}
func (t *TechnicalsIndex) KLineString() string {
if t.KLines == nil {
return ""
}
res, _ := json.Marshal(t.KLines)
return string(res)
}

50
trade/times.go Normal file
View File

@@ -0,0 +1,50 @@
package trade
import "time"
// IsTradingTime 判断当前是否为交易时间
// 交易时间星期一至星期六早上8点
func IsTradingTime() bool {
now := time.Now()
// 获取星期几 (Sunday = 0, Monday = 1, ..., Saturday = 6)
weekday := now.Weekday()
// 获取小时数
hour := now.Hour()
// 判断是否为星期一至星期五
if weekday >= time.Monday && weekday <= time.Friday {
return true
} else if weekday == time.Saturday && hour <= 8 { // 星期6并且早上8点前
return true
}
return false
}
func Int64ToTime(ms int64) time.Time {
milliseconds := int64(ms) // 2021-10-01 00:00:00 UTC
seconds := milliseconds / 1000
nanos := (milliseconds % 1000) * 1000000
return time.Unix(seconds, nanos)
}
// IsCurrentHourInRange 判断当前小时是否在指定的开始和结束小时区间内
// startHour: 开始小时 (0-23)
// endHour: 结束小时 (0-23)
// 返回: 如果在区间内返回true否则返回false
func IsCurrentHourInRange(startHour, endHour int) bool {
now := time.Now()
currentHour := now.Hour()
// 处理同一天的情况
if startHour <= endHour {
return currentHour >= startHour && currentHour < endHour
}
// 处理跨天的情况(例如 22:00 - 06:00
return currentHour >= startHour || currentHour < endHour
}

61
trade/types.go Normal file
View File

@@ -0,0 +1,61 @@
package trade
import (
"git.apinb.com/quant/strategy/internal/core/market"
"git.apinb.com/quant/strategy/internal/models"
)
type Spec struct {
AllowSymbols []string
PlanKeyName string
Api *models.ExchangeApi
StrategyConf *models.StrategyConf
SymbolsSetting map[string]*market.PairSetting
BinanceClient *BinanceClient `json:"-"`
BitgetClient *BitgetClient `json:"-"`
}
type Positions struct {
Long []*PositionData `json:"long"`
Short []*PositionData `json:"short"`
Data []*PositionData `json:"data"`
}
type PositionData struct {
Symbol string `json:"symbol"` // 交易对
Side string `json:"side"` // 持仓方向
Amt float64 `json:"amt"` // 持仓数量
AvgPrice float64 `json:"avgPrice"` // 持仓均价
MarkPrice float64 `json:"markPrice"` // 当前标记价格
UnRealizedProfit float64 `json:"unRealizedProfit"` // 未实现盈亏
UnRealizedProfitRate float64 `json:"unRealizedProfitRate"` // 未实现盈亏率
MarginType string `json:"marginType"` // 保证金模式
MarginSize float64 `json:"marginSize"` // 保证金
Leverage int `json:"leverage"` // 杠杆倍数
IsMaxMarginSize bool `json:"isMaxMarginSize"` // 是否达到最大保证金
}
// 持仓状态
type PositionStatus int
const (
NoPositions PositionStatus = iota // 空头,多头均无持仓
LongOnly // 多头持仓
ShortOnly // 空头持仓
BothPositions // 双向持仓
Unknown // 未知状态
)
func (ps PositionStatus) String() string {
switch ps {
case NoPositions:
return "NoPositions"
case LongOnly:
return "LONG"
case ShortOnly:
return "SHORT"
case BothPositions:
return "BothPositions"
}
return "Unknown"
}

108
trade/utils.go Normal file
View File

@@ -0,0 +1,108 @@
package trade
import (
"bytes"
"encoding/json"
"fmt"
"math/rand"
"os"
"strconv"
"strings"
"time"
)
func GetSide(in string) string {
in = strings.TrimSpace(in)
if in == "buy" || in == "sell" {
return in
}
switch strings.ToUpper(in) {
case "LONG":
return "buy"
case "SHORT":
return "sell"
}
return ""
}
// 根据持仓状态执行相应的方法
func GetPositionStats(LongExists, ShortExists bool) PositionStatus {
switch {
case !LongExists && !ShortExists:
return NoPositions
case LongExists && !ShortExists:
return LongOnly
case !LongExists && ShortExists:
return ShortOnly
case LongExists && ShortExists:
return BothPositions
}
return -1
}
func FmtNumber(in float64, place int) string {
if place == 0 {
return fmt.Sprintf("%.0f", in)
}
if place == 1 {
return fmt.Sprintf("%.1f", in)
}
if place == 2 {
return fmt.Sprintf("%.2f", in)
}
if place == 3 {
return fmt.Sprintf("%.3f", in)
}
if place == 4 {
return fmt.Sprintf("%.4f", in)
}
if place == 5 {
return fmt.Sprintf("%.5f", in)
}
return fmt.Sprintf("%.6f", in)
}
func FloatRound(in float64, place int) float64 {
// 限制 place 范围在合理区间
if place < 0 {
place = 0
}
// 使用 strconv.FormatFloat 直接格式化,避免多次条件判断
str := strconv.FormatFloat(in, 'f', place, 64)
num, _ := strconv.ParseFloat(str, 64)
return num
}
func RateToFloat64(s string) float64 {
// 去掉百分号
s = strings.TrimSuffix(s, "%")
// 转换为 float64
f, err := strconv.ParseFloat(s, 64)
if err != nil {
return 0
}
return f
}
func Output(v any) {
jsonBy, _ := json.Marshal(v)
var out bytes.Buffer
json.Indent(&out, jsonBy, "", "\t")
out.WriteTo(os.Stdout)
fmt.Printf("\n")
}
func GenClientId() string {
cTime := time.Now().Format("20060102150405")
rand.Seed(time.Now().UnixNano())
random := fmt.Sprintf("%04d", rand.Intn(10000))
return cTime + random
}