From c8e189e9c76acae39a29b92ebd3f621a01f03546 Mon Sep 17 00:00:00 2001 From: yanweidong Date: Fri, 9 Jan 2026 15:48:31 +0800 Subject: [PATCH] initial --- account/ac.go | 99 ++++++++++++++ go.mod | 3 + index/args.go | 7 + index/line_reg_slope.go | 128 +++++++++++++++++ market/kline.go | 73 ++++++++++ market/setting.go | 166 ++++++++++++++++++++++ trade/account.go | 47 +++++++ trade/binance_klines.go | 77 +++++++++++ trade/binance_new.go | 84 ++++++++++++ trade/binance_order.go | 274 +++++++++++++++++++++++++++++++++++++ trade/binance_positions.go | 176 ++++++++++++++++++++++++ trade/bitget_new.go | 270 ++++++++++++++++++++++++++++++++++++ trade/bitget_order.go | 219 +++++++++++++++++++++++++++++ trade/bitget_positions.go | 223 ++++++++++++++++++++++++++++++ trade/calc_profit.go | 57 ++++++++ trade/calc_qty.go | 95 +++++++++++++ trade/calc_tech.go | 123 +++++++++++++++++ trade/history.go | 31 +++++ trade/lock.go | 30 ++++ trade/new.go | 6 + trade/print.go | 59 ++++++++ trade/subscribe.go | 54 ++++++++ trade/tech_index.go | 69 ++++++++++ trade/times.go | 50 +++++++ trade/types.go | 61 +++++++++ trade/utils.go | 108 +++++++++++++++ usmarket/baidu.go | 104 ++++++++++++++ usmarket/watch.go | 102 ++++++++++++++ 28 files changed, 2795 insertions(+) create mode 100644 account/ac.go create mode 100644 go.mod create mode 100644 index/args.go create mode 100644 index/line_reg_slope.go create mode 100644 market/kline.go create mode 100644 market/setting.go create mode 100644 trade/account.go create mode 100644 trade/binance_klines.go create mode 100644 trade/binance_new.go create mode 100644 trade/binance_order.go create mode 100644 trade/binance_positions.go create mode 100644 trade/bitget_new.go create mode 100644 trade/bitget_order.go create mode 100644 trade/bitget_positions.go create mode 100644 trade/calc_profit.go create mode 100644 trade/calc_qty.go create mode 100644 trade/calc_tech.go create mode 100644 trade/history.go create mode 100644 trade/lock.go create mode 100644 trade/new.go create mode 100644 trade/print.go create mode 100644 trade/subscribe.go create mode 100644 trade/tech_index.go create mode 100644 trade/times.go create mode 100644 trade/types.go create mode 100644 trade/utils.go create mode 100644 usmarket/baidu.go create mode 100644 usmarket/watch.go diff --git a/account/ac.go b/account/ac.go new file mode 100644 index 0000000..dd4ce92 --- /dev/null +++ b/account/ac.go @@ -0,0 +1,99 @@ +package account + +import ( + "encoding/json" + "log" + "time" + + "git.apinb.com/quant/strategy/internal/core/trade" + "git.apinb.com/quant/strategy/internal/models" +) + +var ( + BaseAssets string + PlanKeyName string + IsDebug bool = false + UpdHistory bool = false +) + +// 根据基本币,监控帐号可用资金变动,仓位,以及最近7天的交易情况 +func NewAccountByBinance(base string, p *trade.Spec, debug, updHistory bool) { + BaseAssets = base + PlanKeyName = p.PlanKeyName + IsDebug = debug + UpdHistory = updHistory + + trade.NewAccounts() + + if p.Api.Exchange == "BINANCE" { + //trade.InitExchange_Binance(p.BinanceFuturesClient, p.AllowSymbols, p.StrategyConf.Leverage) + InitAccount_Binance(p) + } + + go WatchPositions(p) + if UpdHistory { + // go WatchHistory(p) + } +} + +func InitAccount_Binance(p *trade.Spec) { + var err error + ac, err := p.BinanceClient.GetFuturesAccountBalance() + if err != nil { + log.Println(err) + return + } else { + trade.AccountsAssets.Set(BaseAssets, ac[BaseAssets]) + } + + log.Println("WatchAccount", PlanKeyName, BaseAssets, "Account Equity:", ac[BaseAssets].AccountEquity, "Account Available:", ac[BaseAssets].Available) + + orders, err := trade.RefreshPositions(p) + if err != nil { + log.Println(err) + return + } + log.Println("Positions Number", len(orders)) +} + +func WatchAccount_Binance(p *trade.Spec) { + for { + var err error + ac, err := p.BinanceClient.GetFuturesAccountBalance() + if err != nil { + log.Println(err) + } else { + trade.AccountsAssets.Set(BaseAssets, ac[BaseAssets]) + log.Println("WatchAccount", PlanKeyName, BaseAssets, "Account Equity:", ac[BaseAssets].AccountEquity, "Account Available:", ac[BaseAssets].Available) + } + + time.Sleep(1 * time.Hour) + } +} + +func WatchPositions(p *trade.Spec) { + for { + orders, err := trade.RefreshPositions(p) + if err != nil { + log.Println(err) + } + + if IsDebug { + jsonBytes, _ := json.Marshal(orders) + log.Println("WatchPositions", string(jsonBytes)) + } + + time.Sleep(2 * time.Second) + } +} + +func WatchHistory(p *trade.Spec) { + for { + trade, pl := trade.RefreshHistoryTotal(p) + models.UpdateTotalTrans(PlanKeyName, trade, pl) + if IsDebug { + log.Println("WatchHistory", "Trade Number", trade, "Trade RealizedPnl", pl) + } + time.Sleep(10 * time.Minute) + } +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..104a1f0 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module git.apinb.com/qt-sdk/coin + +go 1.25.1 diff --git a/index/args.go b/index/args.go new file mode 100644 index 0000000..164c160 --- /dev/null +++ b/index/args.go @@ -0,0 +1,7 @@ +package index + +type Args struct { + InReal []float64 + Period int + Debug bool +} diff --git a/index/line_reg_slope.go b/index/line_reg_slope.go new file mode 100644 index 0000000..2335078 --- /dev/null +++ b/index/line_reg_slope.go @@ -0,0 +1,128 @@ +package index + +import ( + "log" + + "git.apinb.com/quant/strategy/internal/core/trade" + "github.com/markcheno/go-talib" +) + +// LineRegSlope 根据线性回归斜率分析最新4条K线是否存在反转情况 +// 改进版本:修复了逻辑错误,增加了边界检查,优化了性能 +func LineRegSlope(a *Args) (action string) { + // 边界检查:确保有足够的数据 + if len(a.InReal) < a.Period { + if a.Debug { + log.Println("LineRegSlope: 数据不足,需要至少", a.Period, "条数据") + } + return + } + + result := talib.LinearRegSlope(a.InReal, a.Period) + + // 边界检查:确保结果数组有足够的数据 + if len(result) < 4 { + if a.Debug { + log.Println("LineRegSlope: 线性回归结果不足4条") + } + return + } + + // 获取最新4条K线的斜率值 + origin := result[len(result)-4:] + + // 处理斜率值,保留所有值(移除过于严格的过滤) + var k4 []float64 + for _, val := range origin { + if val >= 0.001 || val <= -0.001 { + roundedVal := trade.FloatRound(val, 6) + k4 = append(k4, roundedVal) + } + } + + if a.Debug { + log.Println("Origin", result) + log.Println("Kline4", k4) + } + + // 检查是否有足够的数据 + if len(k4) < 4 { + return + } + + // 判断反转信号 + // 下降反转:从正斜率转为负斜率,且斜率递减 + if k4[0] > 0 && k4[3] < 0 { + // 检查斜率是否呈现递减趋势(从正到负的平滑过渡) + if isDecreasingTrend(k4) { + action = "DOWN" + } + } + + // 上升反转:从负斜率转为正斜率,且斜率递增 + if k4[0] < 0 && k4[3] > 0 { + // 检查斜率是否呈现递增趋势(从负到正的平滑过渡) + if isIncreasingTrend(k4) { + action = "UP" + } + } + + return +} + +// isDecreasingTrend 检查斜率是否呈现递减趋势 +func isDecreasingTrend(slopes []float64) bool { + // 简化的递减判断:确保整体趋势是递减的 + // 允许中间有小的波动,但整体方向是向下的 + for i := 1; i < len(slopes); i++ { + if slopes[i] > slopes[i-1] { + // 如果发现上升,检查是否只是小幅波动 + if slopes[i]-slopes[i-1] > 0.01 { // 允许小幅波动 + return false + } + } + } + return true +} + +// isIncreasingTrend 检查斜率是否呈现递增趋势 +func isIncreasingTrend(slopes []float64) bool { + // 简化的递增判断:确保整体趋势是递增的 + // 允许中间有小的波动,但整体方向是向上的 + for i := 1; i < len(slopes); i++ { + if slopes[i] < slopes[i-1] { + // 如果发现下降,检查是否只是小幅波动 + if slopes[i-1]-slopes[i] > 0.01 { // 允许小幅波动 + return false + } + } + } + return true +} + +// CurrentSlopeDirection 基于最新线性回归斜率给出当前方向(非反转) +// 返回:"UP" / "DOWN" / ""(无明显方向) +func CurrentSlopeDirection(a *Args) string { + if len(a.InReal) < a.Period { + if a.Debug { + log.Println("CurrentSlopeDirection: 数据不足,需要至少", a.Period, "条数据") + } + return "" + } + + s := talib.LinearRegSlope(a.InReal, a.Period) + if len(s) == 0 { + return "" + } + + latest := s[len(s)-1] + // 轻微阈值,避免噪声抖动;可按回测调整 + const threshold = 0.0002 + if latest > threshold { + return "UP" + } + if latest < -threshold { + return "DOWN" + } + return "" +} \ No newline at end of file diff --git a/market/kline.go b/market/kline.go new file mode 100644 index 0000000..4cd1e73 --- /dev/null +++ b/market/kline.go @@ -0,0 +1,73 @@ +package market + +import ( + "encoding/json" + "errors" + "strconv" + + "git.apinb.com/bsm-sdk/core/utils" + "git.apinb.com/quant/strategy/types" +) + +var ( + KlineUrl = "http://market.senlin.ai/ticker/klines" +) + +func FetchKlines(symbol, interval string, limit int) ([]*types.KLine, []float64, error) { + url := KlineUrl + "?symbol=" + symbol + "&interval=" + interval + "&limit=" + strconv.Itoa(limit) + body, err := utils.HttpGet(url) + if err != nil { + return nil, nil, err + } + + var data []*types.KLine + err = json.Unmarshal(body, &data) + if err != nil { + return nil, nil, err + } + + if len(data) < 1 { + return nil, nil, errors.New("no data") + } + + var closes []float64 + for _, kline := range data { + closes = append(closes, kline.Close) + } + + return data, closes, nil +} + +// 获取最后两个K线 +// 返回:上一个K线, 当前K线, 错误 +func GetLastKline(symbol, interval string) (*types.KLine, *types.KLine, error) { + url := KlineUrl + "?symbol=" + symbol + "&interval=" + interval + "&limit=2" + body, err := utils.HttpGet(url) + if err != nil { + return nil, nil, err + } + + var data []*types.KLine + err = json.Unmarshal(body, &data) + if err != nil { + return nil, nil, err + } + + if len(data) < 2 { + return nil, nil, errors.New("no data") + } + + data[1].PriceRate = (data[1].Close - data[1].Open) / data[1].Open + data[0].PriceRate = (data[0].Close - data[0].Open) / data[0].Open + + return data[0], data[1], nil +} + +func UnmarshalKline(body string) (*types.KLine, error) { + var data types.KLine + err := json.Unmarshal([]byte(body), &data) + if err != nil { + return nil, err + } + return &data, nil +} diff --git a/market/setting.go b/market/setting.go new file mode 100644 index 0000000..e8614fc --- /dev/null +++ b/market/setting.go @@ -0,0 +1,166 @@ +package market + +import ( + "encoding/json" + "log" + "sync" + + "git.apinb.com/bsm-sdk/core/utils" +) + +var ( + AllowSymbolsEndpoint string = "http://market.senlin.ai/symbols/allow" + SymbolsSettingEndpoint string = "http://market.senlin.ai/symbols/get/" + AllowSymbols *symbols +) + +type PairSetting struct { + Symbol string `json:"symbol"` + PricePrecision int `json:"pricePrecision"` // 价格小数点位数 + QuantityPrecision int `json:"quantityPrecision"` // 数量小数点位数 + MinNotional float64 `json:"minNotional"` // 最小名义价值 + MinTradeNum float64 `json:"minTradeNum"` // 最小开单数量(基础币) + BaseAssetPrecision int `json:"baseAssetPrecision"` // 标的资产精度 + Commit string `json:"commit"` // 备注 + Level int `json:"level"` // 仓位等级 + MinClosePrecision float64 `json:"minClosePrecision"` // 平仓利润率百分比 + TickSize float64 `json:"tickSize"` // 最小价格变动单位 + //LowestPrice float64 `json:"lowestPrice"` // 最低价,低位不做空 + //HightPrice float64 `json:"hightPrice"` // 最高价,高位不做多 + //MaxMarginSize float64 `json:"maxMarginSize"` // 最大仓位 +} + +type symbols struct { + sync.RWMutex + Data map[string]*PairSetting +} + +func NewAllowAllSymbols() { + AllowSymbols = &symbols{ + Data: make(map[string]*PairSetting), + } +} + +func (m *symbols) Set(symbol string, setting *PairSetting) { + m.Lock() + defer m.Unlock() + m.Data[symbol] = setting +} + +func (m *symbols) Get(symbol string) *PairSetting { + m.RLock() + defer m.RUnlock() + return m.Data[symbol] +} + +func (m *symbols) Del(symbol string) { + m.Lock() + defer m.Unlock() + delete(m.Data, symbol) +} + +func (m *symbols) SetData(Data map[string]*PairSetting) { + m.RLock() + defer m.RUnlock() + m.Data = Data +} + +func (m *symbols) All() map[string]*PairSetting { + m.RLock() + defer m.RUnlock() + return m.Data +} + +// 读取行情接口,返回行情接口的最小止盈百分比 +func FetchMarketSymbolsSetting(leverage int) ([]string, map[string]*PairSetting, error) { + var data map[string]*PairSetting + body, err := utils.HttpGet(AllowSymbolsEndpoint) + if err != nil { + return nil, nil, err + } + log.Println("AllowSymbolsSetting", string(body)) + + err = json.Unmarshal(body, &data) + if err != nil { + return nil, nil, err + } + + var symboles []string + for k, val := range data { + data[k].MinClosePrecision = val.MinClosePrecision * float64(leverage) // 根据杠杆调整最小平仓利润率 + symboles = append(symboles, k) + } + return symboles, data, nil +} + +// 读取行情接口,不处理最小止盈百分比 +func FetchMarketSymbolsSetting_V2(leverage int) (map[string]*PairSetting, error) { + var data map[string]*PairSetting + body, err := utils.HttpGet(AllowSymbolsEndpoint) + if err != nil { + return nil, err + } + log.Println("AllowSymbolsSetting", string(body)) + + err = json.Unmarshal(body, &data) + if err != nil { + return nil, err + } + + return data, nil +} + +func InitAllowSymbolsSetting(posSymbols []string) (map[string]*PairSetting, error) { + data := make(map[string]*PairSetting) + for _, symbol := range posSymbols { + if _, ok := data[symbol]; !ok { + var pairSetting PairSetting + body, err := utils.HttpGet(SymbolsSettingEndpoint + symbol) + if err != nil { + return nil, err + } + err = json.Unmarshal(body, &pairSetting) + if err != nil { + return nil, err + } + data[symbol] = &pairSetting + } + } + + AllowSymbols.SetData(data) + log.Println("GetAllowSymbolsSetting Lenght:", len(data)) + return data, nil +} + +func RefreshAllowSymbolsSetting(posSymbols []string) error { + var data map[string]*PairSetting + body, err := utils.HttpGet(AllowSymbolsEndpoint) + if err != nil { + return err + } + log.Println("AllowSymbolsSetting", string(body)) + + err = json.Unmarshal(body, &data) + if err != nil { + return err + } + + for _, symbol := range posSymbols { + if _, ok := data[symbol]; !ok { + var pairSetting PairSetting + body, err := utils.HttpGet(SymbolsSettingEndpoint + symbol) + if err != nil { + return err + } + err = json.Unmarshal(body, &pairSetting) + if err != nil { + return err + } + data[symbol] = &pairSetting + } + } + + AllowSymbols.SetData(data) + log.Println("RefreshAllowSymbolsSetting Lenght:", len(data)) + return nil +} diff --git a/trade/account.go b/trade/account.go new file mode 100644 index 0000000..b6a277d --- /dev/null +++ b/trade/account.go @@ -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] +} diff --git a/trade/binance_klines.go b/trade/binance_klines.go new file mode 100644 index 0000000..a94227e --- /dev/null +++ b/trade/binance_klines.go @@ -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 +} diff --git a/trade/binance_new.go b/trade/binance_new.go new file mode 100644 index 0000000..ac09f19 --- /dev/null +++ b/trade/binance_new.go @@ -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) +} diff --git a/trade/binance_order.go b/trade/binance_order.go new file mode 100644 index 0000000..985b23c --- /dev/null +++ b/trade/binance_order.go @@ -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, + }) +} diff --git a/trade/binance_positions.go b/trade/binance_positions.go new file mode 100644 index 0000000..a974c42 --- /dev/null +++ b/trade/binance_positions.go @@ -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 +} diff --git a/trade/bitget_new.go b/trade/bitget_new.go new file mode 100644 index 0000000..37bca70 --- /dev/null +++ b/trade/bitget_new.go @@ -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 +} diff --git a/trade/bitget_order.go b/trade/bitget_order.go new file mode 100644 index 0000000..582a983 --- /dev/null +++ b/trade/bitget_order.go @@ -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 +} diff --git a/trade/bitget_positions.go b/trade/bitget_positions.go new file mode 100644 index 0000000..688e945 --- /dev/null +++ b/trade/bitget_positions.go @@ -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) + } +} diff --git a/trade/calc_profit.go b/trade/calc_profit.go new file mode 100644 index 0000000..6d0cb62 --- /dev/null +++ b/trade/calc_profit.go @@ -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 +} diff --git a/trade/calc_qty.go b/trade/calc_qty.go new file mode 100644 index 0000000..3aa1cf6 --- /dev/null +++ b/trade/calc_qty.go @@ -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) +} diff --git a/trade/calc_tech.go b/trade/calc_tech.go new file mode 100644 index 0000000..4976920 --- /dev/null +++ b/trade/calc_tech.go @@ -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 +} diff --git a/trade/history.go b/trade/history.go new file mode 100644 index 0000000..80609c6 --- /dev/null +++ b/trade/history.go @@ -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 +} diff --git a/trade/lock.go b/trade/lock.go new file mode 100644 index 0000000..2d6532f --- /dev/null +++ b/trade/lock.go @@ -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) +} diff --git a/trade/new.go b/trade/new.go new file mode 100644 index 0000000..f60e5a2 --- /dev/null +++ b/trade/new.go @@ -0,0 +1,6 @@ +package trade + +func NewTrade() { + NewLock() + NewAccounts() +} diff --git a/trade/print.go b/trade/print.go new file mode 100644 index 0000000..482c0b4 --- /dev/null +++ b/trade/print.go @@ -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 +} diff --git a/trade/subscribe.go b/trade/subscribe.go new file mode 100644 index 0000000..cabb42f --- /dev/null +++ b/trade/subscribe.go @@ -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 + } +} diff --git a/trade/tech_index.go b/trade/tech_index.go new file mode 100644 index 0000000..c735719 --- /dev/null +++ b/trade/tech_index.go @@ -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) +} diff --git a/trade/times.go b/trade/times.go new file mode 100644 index 0000000..35e0e1e --- /dev/null +++ b/trade/times.go @@ -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 +} diff --git a/trade/types.go b/trade/types.go new file mode 100644 index 0000000..b8d4a38 --- /dev/null +++ b/trade/types.go @@ -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" +} diff --git a/trade/utils.go b/trade/utils.go new file mode 100644 index 0000000..d3bb537 --- /dev/null +++ b/trade/utils.go @@ -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 +} diff --git a/usmarket/baidu.go b/usmarket/baidu.go new file mode 100644 index 0000000..6984327 --- /dev/null +++ b/usmarket/baidu.go @@ -0,0 +1,104 @@ +package usmarket + +import ( + "encoding/json" + "errors" + "io" + "log" + "net/http" +) + +type JSONData struct { + QueryID string `json:"QueryID"` + ResultCode string `json:"ResultCode"` + Result Result `json:"Result"` +} +type StockDataIndex struct { + P string `json:"p"` + LastPrice string `json:"lastPrice"` + Status string `json:"status"` + Ratio string `json:"ratio"` + Increase string `json:"increase"` + Code string `json:"code"` + Name string `json:"name"` + Market string `json:"market"` +} +type Tabs struct { + Text string `json:"text"` + Market string `json:"market"` +} +type Result struct { + List []StockDataIndex `json:"list"` + Tabs []Tabs `json:"tabs"` + Curtab string `json:"curtab"` +} + +func Baidu_UsIndex() (map[string]*StockDataIndex, error) { + body, err := Baidu_Http("https://finance.pae.baidu.com/api/getbanner?market=america&finClientType=pc") + if err != nil { + return nil, err + } + + var data JSONData + err = json.Unmarshal(body, &data) + if err != nil { + return nil, err + } + + if data.ResultCode != "0" { + return nil, errors.New("Baidue ResultCode:" + data.ResultCode) + } + + indexMap := make(map[string]*StockDataIndex) + + for _, v := range data.Result.List { + if v.Code == "IXIC" { // IXIC 纳斯达克 + indexMap[v.Code] = &v + } + + if v.Code == "DJI" { // DJI 道琼斯 + indexMap[v.Code] = &v + } + + if v.Code == "SPX" { // SPX 标普500 + indexMap[v.Code] = &v + } + + } + + return indexMap, nil + +} + +func Baidu_Http(url string) ([]byte, error) { + client := &http.Client{} + req, err := http.NewRequest("GET", url, nil) + if err != nil { + log.Fatal(err) + } + + req.Header.Set("accept", "application/json, text/plain, */*") + req.Header.Set("accept-language", "zh-CN,zh;q=0.9,en;q=0.8,en-GB;q=0.7,en-US;q=0.6") + req.Header.Set("origin", "https://gushitong.baidu.com") + req.Header.Set("priority", "u=1, i") + req.Header.Set("referer", "https://gushitong.baidu.com/") + req.Header.Set("sec-ch-ua", `"Chromium";v="128", "Not;A=Brand";v="24", "Microsoft Edge";v="128"`) + req.Header.Set("sec-ch-ua-mobile", "?0") + req.Header.Set("sec-ch-ua-platform", `"Windows"`) + req.Header.Set("sec-fetch-dest", "empty") + req.Header.Set("sec-fetch-mode", "cors") + req.Header.Set("sec-fetch-site", "cross-site") + req.Header.Set("site-channel", "001") + req.Header.Set("user-agent", "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/128.0.0.0 Safari/537.36 Edg/128.0.0.0") + resp, err := client.Do(req) + if err != nil { + log.Fatal(err) + } + defer resp.Body.Close() + bodyText, err := io.ReadAll(resp.Body) + if err != nil { + return nil, err + } + + return bodyText, nil +} diff --git a/usmarket/watch.go b/usmarket/watch.go new file mode 100644 index 0000000..5960d45 --- /dev/null +++ b/usmarket/watch.go @@ -0,0 +1,102 @@ +package usmarket + +import ( + "log" + "sync" + "time" +) + +var ( + UsMarket *MarketIndex +) + +// lock +type MarketIndex struct { + sync.RWMutex + LastIndex map[string]*StockDataIndex + Stats int // -1:未开市,0:开市,数据不正常,1: 开市,数据正常 +} + +func NewMarket() { + UsMarket = &MarketIndex{ + LastIndex: make(map[string]*StockDataIndex), + Stats: -1, + } +} + +func (m *MarketIndex) Set(stats int, index map[string]*StockDataIndex) { + m.Lock() + defer m.Unlock() + + m.Stats = stats + m.LastIndex = index +} + +func (m *MarketIndex) Get() (int, map[string]*StockDataIndex) { + m.Lock() + defer m.Unlock() + + return m.Stats, m.LastIndex +} + +func CheckUsMarketOpen() int { + var err error + if !IsUSMarketOpen() { + UsMarket.Set(-1, nil) + return -1 + } + + newIndex, err := Baidu_UsIndex() + if err != nil { + log.Println("Baidu_UsIndex Error:", err) + UsMarket.Set(-1, nil) + return -1 + } + + _, LastStockIndex := UsMarket.Get() + if len(LastStockIndex) == 0 { + UsMarket.Set(0, newIndex) + return 0 + } + + // IXIC 纳斯达克,DJI 道琼斯,SPX 标普500 + if LastStockIndex["IXIC"].LastPrice == newIndex["IXIC"].LastPrice && LastStockIndex["DJI"].LastPrice == newIndex["DJI"].LastPrice && LastStockIndex["SPX"].LastPrice == newIndex["SPX"].LastPrice { + // log.Println("ERROR #103, IXIC,DJI,SPX LastPrice no change") + UsMarket.Set(-1, LastStockIndex) + return 0 + } + + if newIndex["IXIC"].Increase == "0.00" && newIndex["DJI"].Increase == "0.00" && newIndex["SPX"].Increase == "0.00" { + // log.Println("ERROR #104, IXIC,DJI,SPX Increase is 0") + UsMarket.Set(-1, LastStockIndex) + return 0 + } + + // jsonBytes, _ := json.Marshal(newIndex) + // log.Println("USMarket:", string(jsonBytes)) + UsMarket.Set(1, newIndex) + return 1 +} + +// 判断美股是否开市 +func IsUSMarketOpen() bool { + // 获取当前时间 + now := time.Now() + + // 设置美国东部时间 + loc, _ := time.LoadLocation("America/New_York") + + // 将当前时间转换为美国东部时间 + nowInNY := now.In(loc) + + // 判断是否为周末 + if nowInNY.Weekday() == time.Saturday || nowInNY.Weekday() == time.Sunday { + return false + } + + // 判断是否在交易时间内 + openTime := time.Date(nowInNY.Year(), nowInNY.Month(), nowInNY.Day(), 9, 30, 0, 0, loc) + closeTime := time.Date(nowInNY.Year(), nowInNY.Month(), nowInNY.Day(), 16, 0, 0, 0, loc) + + return nowInNY.After(openTime) && nowInNY.Before(closeTime) +}