initial
This commit is contained in:
99
account/ac.go
Normal file
99
account/ac.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
7
index/args.go
Normal file
7
index/args.go
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
package index
|
||||||
|
|
||||||
|
type Args struct {
|
||||||
|
InReal []float64
|
||||||
|
Period int
|
||||||
|
Debug bool
|
||||||
|
}
|
||||||
128
index/line_reg_slope.go
Normal file
128
index/line_reg_slope.go
Normal file
@@ -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 ""
|
||||||
|
}
|
||||||
73
market/kline.go
Normal file
73
market/kline.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
166
market/setting.go
Normal file
166
market/setting.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
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
|
||||||
|
}
|
||||||
104
usmarket/baidu.go
Normal file
104
usmarket/baidu.go
Normal file
@@ -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
|
||||||
|
}
|
||||||
102
usmarket/watch.go
Normal file
102
usmarket/watch.go
Normal file
@@ -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)
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user