initial
This commit is contained in:
47
trade/account.go
Normal file
47
trade/account.go
Normal 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
77
trade/binance_klines.go
Normal 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
84
trade/binance_new.go
Normal 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
274
trade/binance_order.go
Normal 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.2(20%) / 交易币对数 = 最大持仓保证金
|
||||
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
176
trade/binance_positions.go
Normal 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
270
trade/bitget_new.go
Normal 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
219
trade/bitget_order.go
Normal 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
223
trade/bitget_positions.go
Normal 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
57
trade/calc_profit.go
Normal 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
95
trade/calc_qty.go
Normal 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
123
trade/calc_tech.go
Normal 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
31
trade/history.go
Normal 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
30
trade/lock.go
Normal 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
6
trade/new.go
Normal file
@@ -0,0 +1,6 @@
|
||||
package trade
|
||||
|
||||
func NewTrade() {
|
||||
NewLock()
|
||||
NewAccounts()
|
||||
}
|
||||
59
trade/print.go
Normal file
59
trade/print.go
Normal 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
54
trade/subscribe.go
Normal 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
69
trade/tech_index.go
Normal 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
50
trade/times.go
Normal 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
61
trade/types.go
Normal 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
108
trade/utils.go
Normal 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
|
||||
}
|
||||
Reference in New Issue
Block a user