update the test strategy

This commit is contained in:
lychiyu 2024-07-21 22:42:15 +08:00
parent 165d184d4a
commit ef261d01f3
22 changed files with 1156 additions and 35 deletions

View File

@ -9,13 +9,13 @@ notifications:
submitOrder: true
sessions:
# binance:
# exchange: binance
# envVarPrefix: binance
binance:
exchange: binance
envVarPrefix: binance
max:
exchange: max
envVarPrefix: MAX
# max:
# exchange: max
# envVarPrefix: MAX
riskControls:
# This is the session-based risk controller, which let you configure different risk controller by session.
@ -26,7 +26,7 @@ riskControls:
orderExecutor:
# symbol-routed order executor
bySymbol:
BTCUSDT:
ARUSDT:
# basic risk control order executor
basic:
minQuoteBalance: 1000.0

40
config/ccinr.yaml Normal file
View File

@ -0,0 +1,40 @@
sessions:
binance_futures:
exchange: binance
envVarPrefix: BINANCE
futures: true
exchangeStrategies:
- on: binance_futures
ccinr:
# symbols:
# - ARUSDT
# - BNBUSDT
# - BTCUSDT
# - ETHUSDT
# - ORDIUSDT
# - OPUSDT
# - OMUSDT
# - SOLUSDT
# - WIFUSDT
# - DYDXUSDT
# - XRPUSDT
# - PEOPLEUSDT
# - STXUSDT
# - WLDUSDT
# - FILUSDT
# - DOGEUSDT
# - MKRUSDT
# - NOTUSDT
# - ENSUSDT
interval: 1m
symbol: ARUSDT
# recalculate: false
# nr_count: 4
# dry_run: false
# # quantity: 3
# amount: 20
# leverage: 5.0
# profitRange: 0.5%
# lossRange: 10%
# strict_mode: true

View File

@ -21,7 +21,7 @@ sync:
filledOrders: false
# since is the start date of your trading data
since: 2019-01-01
since: 2024-01-01
# sessions is the list of session names you want to sync
# by default, qbtrade sync all your available sessions.
@ -31,7 +31,11 @@ sync:
# symbols is the list of symbols you want to sync
# by default, qbtrade try to guess your symbols by your existing account balances.
symbols:
- BTCUSDT
- ARUSDT
- OPUSDT
- ORDIUSDT
- CFXUSDT
- BNXUSDT
# example command:
# go run ./cmd/qbtrade backtest --config config/grid2.yaml --base-asset-baseline

40
config/new_test.yaml Normal file
View File

@ -0,0 +1,40 @@
sessions:
binance_futures:
exchange: binance
envVarPrefix: BINANCE
futures: true
exchangeStrategies:
- on: binance_futures
new_test:
symbols:
- ARUSDT
# - BNBUSDT
# - BTCUSDT
# - ETHUSDT
# - ORDIUSDT
# - OPUSDT
# - OMUSDT
# - SOLUSDT
# - WIFUSDT
# - DYDXUSDT
# - XRPUSDT
# - PEOPLEUSDT
# - STXUSDT
# - WLDUSDT
# - FILUSDT
# - DOGEUSDT
# - MKRUSDT
# - NOTUSDT
# - ENSUSDT
interval: 1m
# symbol: ARUSDT
recalculate: false
nr_count: 4
dry_run: false
# quantity: 3
amount: 20
leverage: 5.0
profitRange: 0.5%
lossRange: 10%
strict_mode: true

BIN
otp.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

View File

@ -190,6 +190,8 @@ func init() {
RootCmd.PersistentFlags().String("telegram-bot-token", "", "telegram bot token from bot father")
RootCmd.PersistentFlags().String("telegram-bot-auth-token", "", "telegram auth token")
RootCmd.PersistentFlags().String("lark-bot-token", "", "lark bot token")
RootCmd.PersistentFlags().String("binance-api-key", "", "binance api key")
RootCmd.PersistentFlags().String("binance-api-secret", "", "binance api secret")

View File

@ -8,6 +8,7 @@ import (
_ "git.qtrade.icu/lychiyu/qbtrade/pkg/strategy/autobuy"
_ "git.qtrade.icu/lychiyu/qbtrade/pkg/strategy/bollgrid"
_ "git.qtrade.icu/lychiyu/qbtrade/pkg/strategy/bollmaker"
_ "git.qtrade.icu/lychiyu/qbtrade/pkg/strategy/ccinr"
_ "git.qtrade.icu/lychiyu/qbtrade/pkg/strategy/convert"
_ "git.qtrade.icu/lychiyu/qbtrade/pkg/strategy/dca"
_ "git.qtrade.icu/lychiyu/qbtrade/pkg/strategy/deposit2transfer"
@ -29,6 +30,7 @@ import (
_ "git.qtrade.icu/lychiyu/qbtrade/pkg/strategy/linregmaker"
_ "git.qtrade.icu/lychiyu/qbtrade/pkg/strategy/liquiditymaker"
_ "git.qtrade.icu/lychiyu/qbtrade/pkg/strategy/marketcap"
_ "git.qtrade.icu/lychiyu/qbtrade/pkg/strategy/newTest"
_ "git.qtrade.icu/lychiyu/qbtrade/pkg/strategy/pivotshort"
_ "git.qtrade.icu/lychiyu/qbtrade/pkg/strategy/pricealert"
_ "git.qtrade.icu/lychiyu/qbtrade/pkg/strategy/pricedrop"

View File

@ -90,8 +90,11 @@ func toLocalFuturesOrderType(orderType types.OrderType) (futures.OrderType, erro
// case types.OrderTypeStopLimit:
// return futures.OrderTypeStopLossLimit, nil //TODO
// case types.OrderTypeStopMarket:
// return futures.OrderTypeStopLoss, nil //TODO
case types.OrderTypeTakeProfitMarket:
return futures.OrderTypeTakeProfitMarket, nil
case types.OrderTypeStopMarket:
return futures.OrderTypeStopMarket, nil //TODO
case types.OrderTypeMarket:
return futures.OrderTypeMarket, nil
@ -207,12 +210,8 @@ func toGlobalFuturesOrderType(orderType futures.OrderType) types.OrderType {
// FIXME: handle this order type
// case futures.OrderTypeTrailingStopMarket:
case futures.OrderTypeTakeProfit:
return types.OrderTypeStopLimit
case futures.OrderTypeTakeProfitMarket:
return types.OrderTypeStopMarket
return types.OrderTypeTakeProfitMarket
case futures.OrderTypeStopMarket:
return types.OrderTypeStopMarket

View File

@ -3,6 +3,8 @@ package binance
import (
"context"
"fmt"
"net/http"
"net/url"
"strconv"
"strings"
"sync"
@ -90,7 +92,7 @@ type Exchange struct {
var timeSetterOnce sync.Once
func New(key, secret string) *Exchange {
func New(key, secret, proxy string) *Exchange {
if util.IsPaperTrade() {
binance.UseTestnet = true
}
@ -101,7 +103,6 @@ func New(key, secret string) *Exchange {
var futuresClient = binance.NewFuturesClient(key, secret)
futuresClient.HTTPClient = binanceapi.DefaultHttpClient
futuresClient.Debug = viper.GetBool("debug-binance-futures-client")
if isBinanceUs() {
client.BaseURL = BinanceUSBaseURL
}
@ -109,6 +110,19 @@ func New(key, secret string) *Exchange {
client2 := binanceapi.NewClient(client.BaseURL)
futuresClient2 := binanceapi.NewFuturesRestClient(futuresClient.BaseURL)
if proxy != "" {
proxyURL, err := url.Parse(proxy)
if err == nil {
proxyTransport := &http.Transport{
Proxy: http.ProxyURL(proxyURL),
}
client.HTTPClient.Transport = proxyTransport
futuresClient.HTTPClient.Transport = proxyTransport
client2.HttpClient.Transport = proxyTransport
futuresClient2.HttpClient.Transport = proxyTransport
}
}
ex := &Exchange{
key: key,
secret: secret,

View File

@ -139,7 +139,8 @@ func (e *Exchange) submitFuturesOrder(ctx context.Context, order types.SubmitOrd
req := e.futuresClient.NewCreateOrderService().
Symbol(order.Symbol).
Type(orderType).
Side(futures.SideType(order.Side))
Side(futures.SideType(order.Side)).
PositionSide(futures.PositionSideType(order.PositionSide))
if order.ReduceOnly {
req.ReduceOnly(order.ReduceOnly)
@ -178,7 +179,7 @@ func (e *Exchange) submitFuturesOrder(ctx context.Context, order types.SubmitOrd
// set stop price
switch order.Type {
case types.OrderTypeStopLimit, types.OrderTypeStopMarket:
case types.OrderTypeStopLimit, types.OrderTypeStopMarket, types.OrderTypeTakeProfitMarket:
if order.Market.Symbol != "" {
req.StopPrice(order.Market.FormatPrice(order.StopPrice))
} else {

View File

@ -15,7 +15,7 @@ import (
)
func NewPublic(exchangeName types.ExchangeName) (types.Exchange, error) {
exMinimal, err := New(exchangeName, "", "", "")
exMinimal, err := New(exchangeName, "", "", "", "")
if err != nil {
return nil, err
}
@ -27,11 +27,11 @@ func NewPublic(exchangeName types.ExchangeName) (types.Exchange, error) {
return nil, fmt.Errorf("exchange %T does not implement types.Exchange", exMinimal)
}
func New(n types.ExchangeName, key, secret, passphrase string) (types.ExchangeMinimal, error) {
func New(n types.ExchangeName, key, secret, passphrase, proxy string) (types.ExchangeMinimal, error) {
switch n {
case types.ExchangeBinance:
return binance.New(key, secret), nil
return binance.New(key, secret, proxy), nil
case types.ExchangeMax:
return max.New(key, secret), nil
@ -70,5 +70,6 @@ func NewWithEnvVarPrefix(n types.ExchangeName, varPrefix string) (types.Exchange
}
passphrase := os.Getenv(varPrefix + "_API_PASSPHRASE")
return New(n, key, secret, passphrase)
proxy := os.Getenv(varPrefix + "_PROXY")
return New(n, key, secret, passphrase, proxy)
}

70
pkg/indicator/v2/nr.go Normal file
View File

@ -0,0 +1,70 @@
package indicatorv2
import (
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
)
type NRStrean struct {
*types.Float64Series
nrCount int
kLines []types.KLine
strictMode bool
NrKLine types.KLine
}
func NR(source KLineSubscription, nrCount int, strictMode bool) *NRStrean {
s := &NRStrean{
nrCount: nrCount,
Float64Series: types.NewFloat64Series(),
}
source.AddSubscriber(func(k types.KLine) {
s.calculateAndPush(k)
})
return s
}
func (s *NRStrean) calculateAndPush(k types.KLine) {
s.kLines = append(s.kLines, k)
if len(s.kLines) < s.nrCount {
return
}
nr := s.kLines[len(s.kLines)-1]
preNr := s.kLines[len(s.kLines)-2]
isNR := true
if preNr.High < nr.High || preNr.Low > nr.Low {
isNR = false
return
}
for i := len(s.kLines) - s.nrCount; i < len(s.kLines); i++ {
// 这种是所有的kline都要高于nr
//if s.CalKLines[i].High > nr.High || s.CalKLines[i].Low < nr.Low {
// isNR = false
// break
//}
if s.strictMode {
if s.kLines[i].High-s.kLines[i].Low < nr.High-nr.Low {
isNR = false
break
}
} else {
if (s.kLines[i].High-s.kLines[i].Low)/s.kLines[i].Low < (nr.High-nr.Low)/nr.Low {
isNR = false
break
}
}
}
if isNR {
s.NrKLine = nr
s.PushAndEmit(nr.High.Float64())
}
return
}

View File

@ -0,0 +1,195 @@
package larknotifier
import (
"bytes"
"context"
"encoding/json"
"fmt"
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
"github.com/sirupsen/logrus"
"golang.org/x/time/rate"
"net/http"
"reflect"
"time"
)
var apiLimiter = rate.NewLimiter(rate.Every(time.Second), 5)
var log = logrus.WithField("service", "lark")
type notifyTask struct {
message string
}
type Notifier struct {
token string
taskC chan notifyTask
}
type Option func(notifier *Notifier)
type TextMessage struct {
MsgType string `json:"msg_type"`
Content struct {
Text string `json:"text"`
} `json:"content"`
}
func getLocDateTimeString() string {
loc, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
fmt.Println("Error loading location:", err)
return ""
}
// 获取当前东八区时间
currentTime := time.Now().In(loc)
// 格式化时间
formattedTime := currentTime.Format("2006-01-02 15:04:05")
return formattedTime
}
func sendLarkMessage(token, text string) error {
// 创建消息结构体
message := TextMessage{
MsgType: "text",
Content: struct {
Text string `json:"text"`
}{
Text: fmt.Sprintf("通知时间:%s\n内容\n%s", getLocDateTimeString(), text),
},
}
// 将消息结构体序列化为 JSON
jsonData, err := json.Marshal(message)
if err != nil {
return fmt.Errorf("failed to marshal JSON: %v", err)
}
webhookURL := fmt.Sprintf("https://open.larksuite.com/open-apis/bot/v2/hook/%s", token)
// 创建 HTTP POST 请求
req, err := http.NewRequest("POST", webhookURL, bytes.NewBuffer(jsonData))
if err != nil {
return fmt.Errorf("failed to create request: %v", err)
}
req.Header.Set("Content-Type", "application/json")
// 发送 HTTP 请求
client := &http.Client{}
resp, err := client.Do(req)
if err != nil {
return fmt.Errorf("failed to send request: %v", err)
}
defer resp.Body.Close()
// 检查响应状态码
if resp.StatusCode != http.StatusOK {
return fmt.Errorf("request failed with status code: %d", resp.StatusCode)
}
return nil
}
func New(token string, options ...Option) *Notifier {
notifier := &Notifier{
token: token,
taskC: make(chan notifyTask, 100),
}
for _, o := range options {
o(notifier)
}
go notifier.worker()
return notifier
}
func (n *Notifier) worker() {
ctx := context.Background()
for {
select {
case <-ctx.Done():
return
case task := <-n.taskC:
apiLimiter.Wait(ctx)
n.consume(task)
}
}
}
func (n *Notifier) consume(task notifyTask) {
if task.message != "" {
if err := sendLarkMessage(n.token, task.message); err != nil {
log.WithError(err).Error("lark send error")
}
}
}
func (n *Notifier) Notify(obj interface{}, args ...interface{}) {
n.NotifyTo("", obj, args...)
}
func filterPlaintextMessages(args []interface{}) (texts []string, pureArgs []interface{}) {
var firstObjectOffset = -1
for idx, arg := range args {
rt := reflect.TypeOf(arg)
if rt.Kind() == reflect.Ptr {
switch a := arg.(type) {
case nil:
texts = append(texts, "nil")
if firstObjectOffset == -1 {
firstObjectOffset = idx
}
case types.PlainText:
texts = append(texts, a.PlainText())
if firstObjectOffset == -1 {
firstObjectOffset = idx
}
case types.Stringer:
texts = append(texts, a.String())
if firstObjectOffset == -1 {
firstObjectOffset = idx
}
}
}
}
pureArgs = args
if firstObjectOffset > -1 {
pureArgs = args[:firstObjectOffset]
}
return texts, pureArgs
}
func (n *Notifier) NotifyTo(channel string, obj interface{}, args ...interface{}) {
var message string
switch a := obj.(type) {
case string:
message = fmt.Sprintf(a, args...)
default:
log.Errorf("unsupported notification format: %T %+v", a, a)
}
select {
case n.taskC <- notifyTask{
message: message,
}:
default:
log.Error("[lark] cannot send task to notify")
}
}
func (n *Notifier) SendPhoto(buffer *bytes.Buffer) {
n.SendPhotoTo("", buffer)
}
func (n *Notifier) SendPhotoTo(channel string, buffer *bytes.Buffer) {
// TODO
}

View File

@ -0,0 +1,45 @@
package larknotifier
import (
"fmt"
"time"
"github.com/sirupsen/logrus"
"golang.org/x/time/rate"
)
var limiter = rate.NewLimiter(rate.Every(time.Minute), 3)
type LogHook struct {
notifier *Notifier
}
func LarkNewLogHook(notifier *Notifier) *LogHook {
return &LogHook{
notifier: notifier,
}
}
func (t *LogHook) Levels() []logrus.Level {
return []logrus.Level{
logrus.ErrorLevel,
logrus.FatalLevel,
logrus.PanicLevel,
}
}
func (t *LogHook) Fire(e *logrus.Entry) error {
if !limiter.Allow() {
return nil
}
var message = fmt.Sprintf("[%s] %s", e.Level.String(), e.Message)
if errData, ok := e.Data[logrus.ErrorKey]; ok && errData != nil {
if err, isErr := errData.(error); isErr {
message += " Error: " + err.Error()
}
}
t.notifier.Notify(message)
return nil
}

View File

@ -4,6 +4,7 @@ import (
"bytes"
"context"
"fmt"
"git.qtrade.icu/lychiyu/qbtrade/pkg/notifier/larknotifier"
"image/png"
"io/ioutil"
stdlog "log"
@ -676,6 +677,12 @@ func (environ *Environment) ConfigureNotificationSystem(ctx context.Context, use
}
}
larkBotToken := viper.GetString("lark-bot-token")
if len(larkBotToken) > 0 {
if err := environ.setupLark(userConfig, larkBotToken, persistence); err != nil {
}
}
if userConfig.Notifications != nil {
if err := environ.ConfigureNotification(userConfig.Notifications); err != nil {
return err
@ -947,6 +954,15 @@ func (environ *Environment) setupTelegram(
return nil
}
func (environ *Environment) setupLark(userConfig *Config, larkBotToken string, persistence service.PersistenceService,
) error {
var notifier = larknotifier.New(larkBotToken)
Notification.AddNotifier(notifier)
log.AddHook(larknotifier.LarkNewLogHook(notifier))
return nil
}
func writeOTPKeyAsQRCodePNG(key *otp.Key, imagePath string) error {
// Convert TOTP key into a PNG
var buf bytes.Buffer

View File

@ -112,3 +112,7 @@ func (i *IndicatorSet) ATRP(interval types.Interval, window int) *indicatorv2.AT
func (i *IndicatorSet) ADX(interval types.Interval, window int) *indicatorv2.ADXStream {
return indicatorv2.ADX(i.KLines(interval), window)
}
func (i *IndicatorSet) NR(interval types.Interval, nrCount int, strictMode bool) *indicatorv2.NRStrean {
return indicatorv2.NR(i.KLines(interval), nrCount, strictMode)
}

View File

@ -42,6 +42,7 @@ type ExchangeSession struct {
Key string `json:"key,omitempty" yaml:"key,omitempty"`
Secret string `json:"secret,omitempty" yaml:"secret,omitempty"`
Passphrase string `json:"passphrase,omitempty" yaml:"passphrase,omitempty"`
Proxy string `json:"proxy,omitempty" yaml:"proxy,omitempty"`
SubAccount string `json:"subAccount,omitempty" yaml:"subAccount,omitempty"`
// Withdrawal is used for enabling withdrawal functions
@ -793,7 +794,7 @@ func (session *ExchangeSession) newBasicPrivateExchange(exchangeName types.Excha
var err error
var exMinimal types.ExchangeMinimal
if session.Key != "" && session.Secret != "" {
exMinimal, err = exchange2.New(exchangeName, session.Key, session.Secret, session.Passphrase)
exMinimal, err = exchange2.New(exchangeName, session.Key, session.Secret, session.Passphrase, session.Proxy)
} else {
exMinimal, err = exchange2.NewWithEnvVarPrefix(exchangeName, session.EnvVarPrefix)
}

View File

@ -80,17 +80,20 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor qbtrade.OrderExecutor,
atr := session.Indicators(s.Symbol).ATR(s.Interval, s.Window)
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(k types.KLine) {
// 取消订单
if err := s.Strategy.OrderExecutor.GracefulCancel(ctx); err != nil {
s.logger.WithError(err).Error("unable to cancel open orders...")
return
}
// 更新并获取account信息
account, err := session.UpdateAccount(ctx)
if err != nil {
s.logger.WithError(err).Error("unable to update account")
return
}
// 获取balance信息
baseBalance, ok := account.Balance(s.Market.BaseCurrency)
if !ok {
s.logger.Errorf("%s balance not found", s.Market.BaseCurrency)

View File

@ -0,0 +1,128 @@
package ccinr
import (
"context"
"fmt"
"git.qtrade.icu/lychiyu/qbtrade/pkg/exchange/binance"
"git.qtrade.icu/lychiyu/qbtrade/pkg/qbtrade"
"git.qtrade.icu/lychiyu/qbtrade/pkg/strategy/common"
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
log "github.com/sirupsen/logrus"
"sync"
)
const ID = "ccinr"
func init() {
qbtrade.RegisterStrategy(ID, &Strategy{})
}
type Strategy struct {
*common.Strategy
Symbol string `json:"symbol"`
Interval types.Interval `json:"interval"`
ExchangeSession *qbtrade.ExchangeSession
}
func (s *Strategy) ID() string {
return ID
}
func (s *Strategy) Subscribe(session *qbtrade.ExchangeSession) {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
if !qbtrade.IsBackTesting {
session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{})
}
}
func (s *Strategy) Initialize() error {
if s.Strategy == nil {
s.Strategy = &common.Strategy{}
}
return nil
}
func (s *Strategy) Run(ctx context.Context, orderExecutor qbtrade.OrderExecutor, session *qbtrade.ExchangeSession) error {
s.ExchangeSession = session
nr := session.Indicators(s.Symbol).NR(s.Interval, 4, true)
nr.OnUpdate(func(v float64) {
msg := fmt.Sprintf("交易信号:时间: %s, 最高价:%f最低价:%f", nr.NrKLine.GetStartTime(), nr.NrKLine.High.Float64(), nr.NrKLine.Low.Float64())
qbtrade.Notify(msg)
fmt.Println(v)
})
//session.MarketDataStream.OnKLineClosed(func(k types.KLine) {
// if k.Symbol != s.Symbol || k.Interval != s.Interval {
// return
// }
// fmt.Println(k)
//})
//
//session.MarketDataStream.OnMarketTrade(func(trade types.Trade) {
// // handle market trade event here
// fmt.Println(trade)
//})
b, ok := s.getBalance(ctx)
fmt.Println(b, ok)
session.UserDataStream.OnOrderUpdate(func(order types.Order) {
if order.Status == types.OrderStatusFilled {
log.Infof("your order is filled: %+v", order)
}
})
session.UserDataStream.OnTradeUpdate(func(trade types.Trade) {
log.Infof("trade price %f, fee %f %s", trade.Price.Float64(), trade.Fee.Float64(), trade.FeeCurrency)
})
qbtrade.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
if err := s.Strategy.OrderExecutor.GracefulCancel(ctx); err != nil {
log.WithError(err).Error("unable to cancel open orders...")
}
qbtrade.Sync(ctx, s)
})
return nil
}
func (s *Strategy) handleBalanceUpdate(balances types.BalanceMap) {
for _, b := range balances {
if b.Available.IsZero() && b.Borrowed.IsZero() {
continue
}
}
}
func (s *Strategy) handleBinanceBalanceUpdateEvent(event *binance.BalanceUpdateEvent) {
qbtrade.Notify(event)
account := s.ExchangeSession.GetAccount()
fmt.Println(account)
delta := event.Delta
// ignore outflow
if delta.Sign() < 0 {
return
}
}
// getBalance 获取账户余额
func (s *Strategy) getBalance(ctx context.Context) (balance types.Balance, ok bool) {
// 更新并获取account信息
account, err := s.ExchangeSession.UpdateAccount(ctx)
if err != nil {
log.WithError(err).Error("unable to update account")
return
}
// 获取balance信息
return account.Balance("USDT")
}

View File

@ -0,0 +1,470 @@
package newTest
import (
"context"
"fmt"
"git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint"
"git.qtrade.icu/lychiyu/qbtrade/pkg/qbtrade"
"git.qtrade.icu/lychiyu/qbtrade/pkg/strategy/common"
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
"git.qtrade.icu/lychiyu/qbtrade/pkg/util"
log "github.com/sirupsen/logrus"
"strings"
"sync"
)
const ID = "new_test"
const (
ShortTag = "short"
ShortProfitTag = "short_profit"
ShortLossTag = "short_loss"
LongTag = "long"
LongProfitTag = "long_profit"
LongLossTag = "long_loss"
)
func init() {
qbtrade.RegisterStrategy(ID, &Strategy{})
}
type Strategy struct {
*common.Strategy
Environment *qbtrade.Environment
markets map[string]types.Market
// persistence fields
Positions map[string]*types.Position `persistence:"position"`
ProfitStats *types.ProfitStats `persistence:"profit_stats"`
//TradeStats *types.TradeStats `persistence:"trade_stats"`
//配置文件
ReCalculate bool `json:"recalculate"`
OrderType types.OrderType `json:"orderType"`
Symbols []string `json:"symbols"`
Interval types.Interval `json:"interval"`
NRCount int `json:"nr_count"`
DryRun bool `json:"dry_run"`
ProfitRange fixedpoint.Value `json:"profitRange"`
LossRange fixedpoint.Value `json:"lossRange"`
StrictMode bool `json:"strict_mode"`
Leverage fixedpoint.Value `json:"leverage"`
qbtrade.QuantityOrAmount
// 计算NR的历史kline
CalKLines map[string][]types.KLine
// 符合NR的kline
LastNRCandles map[string]*types.KLine
session *qbtrade.ExchangeSession
orderExecutors map[string]*qbtrade.GeneralOrderExecutor
//AccountValueCalculator *qbtrade.AccountValueCalculator
qbtrade.StrategyController
ordered map[string]bool // 是否已经下单
orderedSide map[string]string // 成交单的方向
LongOrder map[string]types.SubmitOrder
LongProfitOrder map[string]types.SubmitOrder
LongLossOrder map[string]types.SubmitOrder
ShortOrder map[string]types.SubmitOrder
ShortProfitOrder map[string]types.SubmitOrder
ShortLossOrder map[string]types.SubmitOrder
}
func (s *Strategy) Defaults() error {
if s.OrderType == "" {
log.Infof("order type is not set, using limit maker order type")
s.OrderType = types.OrderTypeLimit
//s.OrderType = types.OrderTypeStopLimit
}
return nil
}
func (s *Strategy) Initialize() error {
if s.Strategy == nil {
s.Strategy = &common.Strategy{}
}
return nil
}
func (s *Strategy) ID() string {
return ID
}
func (s *Strategy) InstanceID() string {
return fmt.Sprintf("%s:%s:%s", ID, strings.Join(s.Symbols, "-"), s.Interval)
}
func (s *Strategy) Subscribe(session *qbtrade.ExchangeSession) {
for _, symbol := range s.Symbols {
session.Subscribe(types.KLineChannel, symbol, types.SubscribeOptions{Interval: s.Interval})
session.Subscribe(types.MarketTradeChannel, symbol, types.SubscribeOptions{})
}
}
func (s *Strategy) OnKLineClosed(ctx context.Context, kline types.KLine, symbol string) {
if s.ordered[symbol] {
return
}
calKLines := s.CalKLines[symbol]
if len(s.CalKLines) < s.NRCount {
return
}
nr := calKLines[len(calKLines)-1]
preNr := calKLines[len(calKLines)-2]
isNR := true
if preNr.High < nr.High || preNr.Low > nr.Low {
isNR = false
return
}
for i := len(calKLines) - s.NRCount; i < len(calKLines); i++ {
// 这种是所有的kline都要高于nr
//if s.CalKLines[i].High > nr.High || s.CalKLines[i].Low < nr.Low {
// isNR = false
// break
//}
if s.StrictMode {
if calKLines[i].High-calKLines[i].Low < nr.High-nr.Low {
isNR = false
break
}
} else {
if (calKLines[i].High-calKLines[i].Low)/calKLines[i].Low < (nr.High-nr.Low)/nr.Low {
isNR = false
break
}
}
}
if isNR {
s.LastNRCandles[symbol] = &nr
log.Infof("交易信号(%s)%+v", symbol, kline)
s.placeOrders(ctx, symbol)
return
}
}
func (s *Strategy) cancelSideOrder(ctx context.Context, symbol string) {
if s.orderedSide[symbol] == "" || len(s.orderExecutors[symbol].ActiveMakerOrders().Orders()) <= 0 {
return
}
if s.orderedSide[symbol] == LongTag {
log.Infof("the long order is filled (%s), will cancel short order", symbol)
s.orderExecutors[symbol].CancelOrders(ctx, types.Order{SubmitOrder: s.ShortOrder[symbol]})
s.orderExecutors[symbol].CancelOrders(ctx, types.Order{SubmitOrder: s.ShortLossOrder[symbol]})
s.orderExecutors[symbol].CancelOrders(ctx, types.Order{SubmitOrder: s.ShortProfitOrder[symbol]})
} else {
log.Infof("the short order is filled (%s), will cancel short order", symbol)
s.orderExecutors[symbol].CancelOrders(ctx, types.Order{SubmitOrder: s.LongOrder[symbol]})
s.orderExecutors[symbol].CancelOrders(ctx, types.Order{SubmitOrder: s.LongLossOrder[symbol]})
s.orderExecutors[symbol].CancelOrders(ctx, types.Order{SubmitOrder: s.LongProfitOrder[symbol]})
}
}
func (s *Strategy) Run(ctx context.Context, orderExecutor qbtrade.OrderExecutor, session *qbtrade.ExchangeSession) error {
s.session = session
s.markets = s.session.Markets()
s.Positions = make(map[string]*types.Position)
s.CalKLines = make(map[string][]types.KLine)
s.LastNRCandles = make(map[string]*types.KLine)
s.orderExecutors = make(map[string]*qbtrade.GeneralOrderExecutor)
s.ordered = make(map[string]bool)
s.orderedSide = make(map[string]string)
s.LongOrder = make(map[string]types.SubmitOrder)
s.LongLossOrder = make(map[string]types.SubmitOrder)
s.LongProfitOrder = make(map[string]types.SubmitOrder)
s.ShortOrder = make(map[string]types.SubmitOrder)
s.ShortLossOrder = make(map[string]types.SubmitOrder)
s.ShortProfitOrder = make(map[string]types.SubmitOrder)
qbtrade.Notify("NR4策略开始执行...")
//for _, symbol := range s.Symbols {
// s.Strategy.Initialize(ctx, s.Environment, session, s.markets[symbol], ID, s.InstanceID())
//}
//
for _, symbol := range s.Symbols {
s.Positions[symbol] = types.NewPositionFromMarket(s.markets[symbol])
}
//
//if s.ProfitStats == nil {
// s.ProfitStats = types.NewProfitStats(s.Market)
//}
//if s.TradeStats == nil {
// s.TradeStats = types.NewTradeStats(s.Symbol)
//}
s.OnSuspend(func() {
// Cancel active orders
for _, symbol := range s.Symbols {
_ = s.orderExecutors[symbol].GracefulCancel(ctx)
}
})
s.OnEmergencyStop(func() {
// Cancel active orders
for _, symbol := range s.Symbols {
_ = s.orderExecutors[symbol].GracefulCancel(ctx)
}
// Close 100% position
//_ = s.ClosePosition(ctx, fixedpoint.One)
})
for _, symbol := range s.Symbols {
s.orderExecutors[symbol] = qbtrade.NewGeneralOrderExecutor(session, symbol, ID, s.InstanceID(), s.Positions[symbol])
s.orderExecutors[symbol].BindEnvironment(s.Environment)
_ = s.orderExecutors[symbol].GracefulCancel(ctx)
//s.orderExecutors[symbol].BindProfitStats(s.ProfitStats)
//s.orderExecutor.BindTradeStats(s.TradeStats)
//s.orderExecutors[symbol].TradeCollector().OnPositionUpdate(func(position *types.Position) {
// log.Infof("position is updated, symbol (%s): %+v", symbol, position)
//})
}
// AccountValueCalculator
//s.AccountValueCalculator = qbtrade.NewAccountValueCalculator(s.session, s.Market.QuoteCurrency)
s.session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
for _, symbol := range s.Symbols {
if kline.Symbol != symbol {
continue
}
if !s.ordered[symbol] {
// 在下一根k线时没有成交订单则取消所有订单
s.cancelOrders(ctx, symbol)
} else {
// 如果有订单则不再进行NR的计算
return
}
s.CalKLines[symbol] = []types.KLine{}
// 获取历史最近的4根K线
if !s.ReCalculate {
lines, err := s.session.Exchange.QueryKLines(ctx, symbol, s.Interval, types.KLineQueryOptions{Limit: s.NRCount})
s.CalKLines[symbol] = lines
if err != nil {
util.LogErr(err, fmt.Sprintf("failed to close position %s", symbol))
}
}
if len(s.CalKLines) < s.NRCount {
s.CalKLines[symbol] = append(s.CalKLines[symbol], kline)
} else {
s.OnKLineClosed(ctx, kline, symbol)
}
}
})
// 监听市场的交易事件
//session.MarketDataStream.OnMarketTrade(func(trade types.Trade) {
// // handle market trade event here
// fmt.Println(trade)
//})
session.UserDataStream.OnOrderUpdate(func(order types.Order) {
orderSymbol := order.Symbol
log.Infof("the order is: %+v,id is %d type is %s, status is %s", order, order.OrderID, order.Type, order.Status)
s.cancelSideOrder(ctx, orderSymbol)
if order.Status == types.OrderStatusFilled {
if order.Type == types.OrderTypeLimit && order.Side == types.SideTypeBuy {
log.Infof("the long order is filled: %+v,id is %d, symbol is %s, type is %s, status is %s",
order, order.OrderID, orderSymbol, order.Type, order.Status)
s.ordered[orderSymbol] = true
s.orderedSide[orderSymbol] = LongTag
qbtrade.Notify("订单成交通知:\n 币种:%s, 方向:%s, 价格:%s, 数量:%s", order.Symbol, LongTag, order.Price, order.Quantity)
}
if order.Type == types.OrderTypeLimit && order.Side == types.SideTypeSell {
log.Infof("the short order is filled: %+v,id is %d, symbol is %s, type is %s, status is %s",
order, order.OrderID, orderSymbol, order.Type, order.Status)
s.ordered[orderSymbol] = true
s.orderedSide[orderSymbol] = ShortTag
qbtrade.Notify("订单成交通知:\n 币种:%s, 方向:%s, 价格:%s, 数量:%s", order.Symbol, ShortTag, order.Price, order.Quantity)
}
if order.Type == types.OrderTypeMarket {
log.Infof("the loss or profit order is filled: %+v,id is %d, symbol is %s, type is %s, status is %s",
order, order.OrderID, orderSymbol, order.Type, order.Status)
qbtrade.Notify("订单止盈或止损通知:\n %s:", order.Symbol, order.Price)
s.ordered[orderSymbol] = false
}
} else {
log.Infof("the order is: %+v,id is %d, symbol is %s, type is %s, status is %s",
order, order.OrderID, orderSymbol, order.Type, order.Status)
}
})
session.UserDataStream.OnTradeUpdate(func(trade types.Trade) {
log.Infof("trade price %f, fee %f %s", trade.Price.Float64(), trade.Fee.Float64(), trade.FeeCurrency)
})
session.UserDataStream.OnBalanceUpdate(func(balances types.BalanceMap) {
log.Infof("balance update: %+v", balances)
})
qbtrade.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
qbtrade.Sync(ctx, s)
})
return nil
}
func (s *Strategy) cancelOrders(ctx context.Context, symbol string) {
if len(s.orderExecutors[symbol].ActiveMakerOrders().Orders()) <= 0 {
return
}
log.Infof("the order is not filled, will cancel all orders")
if err := s.orderExecutors[symbol].GracefulCancel(ctx); err != nil {
log.WithError(err).Errorf("failed to cancel orders")
}
}
func (s *Strategy) placeOrders(ctx context.Context, symbol string) {
orders, err := s.generateOrders(ctx, symbol)
if err != nil {
log.WithError(err).Error(fmt.Sprintf("failed to generate orders (%s)", symbol))
return
}
log.Infof("orders: %+v", orders)
if s.DryRun {
log.Infof("dry run, not submitting orders (%s)", symbol)
return
}
createdOrders, err := s.orderExecutors[symbol].SubmitOrders(ctx, orders...)
if err != nil {
log.WithError(err).Error(fmt.Sprintf("failed to submit orders (%s)", symbol))
return
}
log.Infof("created orders (%s): %+v", symbol, createdOrders)
}
func (s *Strategy) generateOrders(ctx context.Context, symbol string) ([]types.SubmitOrder, error) {
var orders []types.SubmitOrder
// 卖价
sellPrice := fixedpoint.NewFromFloat(s.LastNRCandles[symbol].High.Float64())
// 买价
buyPrice := fixedpoint.NewFromFloat(s.LastNRCandles[symbol].Low.Float64())
buyQuantity := s.QuantityOrAmount.CalculateQuantity(buyPrice).Mul(s.Leverage)
sellQuantity := s.QuantityOrAmount.CalculateQuantity(sellPrice).Mul(s.Leverage)
log.Infof("generateOrders (%s), sellPrice is %s, sellQuantity is %s, "+
"buyPrice is %s, buyQuantity is %s", symbol, sellPrice, sellQuantity, buyPrice, buyQuantity)
s.ShortOrder[symbol] = types.SubmitOrder{
Symbol: symbol,
Side: types.SideTypeSell,
Type: s.OrderType,
Price: sellPrice,
PositionSide: types.PositionSideTypeShort,
Quantity: sellQuantity,
TimeInForce: types.TimeInForceGTC,
Market: s.markets[symbol],
Tag: ShortTag,
}
s.ShortProfitOrder[symbol] = types.SubmitOrder{
Symbol: symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeTakeProfitMarket,
PositionSide: types.PositionSideTypeShort,
StopPrice: sellPrice.Sub(sellPrice.Mul(s.ProfitRange)),
TimeInForce: types.TimeInForceGTC,
Market: s.markets[symbol],
Tag: ShortProfitTag,
ClosePosition: true,
}
s.ShortLossOrder[symbol] = types.SubmitOrder{
Symbol: symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeStopMarket,
PositionSide: types.PositionSideTypeShort,
StopPrice: buyPrice.Add(sellPrice.Mul(s.LossRange)),
TimeInForce: types.TimeInForceGTC,
Market: s.markets[symbol],
Tag: ShortLossTag,
ClosePosition: true,
}
s.LongOrder[symbol] = types.SubmitOrder{
Symbol: symbol,
Side: types.SideTypeBuy,
Type: s.OrderType,
Price: buyPrice,
PositionSide: types.PositionSideTypeLong,
Quantity: buyQuantity,
TimeInForce: types.TimeInForceGTC,
Market: s.markets[symbol],
Tag: LongTag,
}
s.LongProfitOrder[symbol] = types.SubmitOrder{
Symbol: symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeTakeProfitMarket,
PositionSide: types.PositionSideTypeLong,
StopPrice: buyPrice.Add(buyPrice.Mul(s.ProfitRange)),
TimeInForce: types.TimeInForceGTC,
Market: s.markets[symbol],
Tag: LongProfitTag,
ClosePosition: true,
}
s.LongLossOrder[symbol] = types.SubmitOrder{
Symbol: symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeStopMarket,
PositionSide: types.PositionSideTypeLong,
StopPrice: sellPrice.Sub(buyPrice.Mul(s.LossRange)),
TimeInForce: types.TimeInForceGTC,
Market: s.markets[symbol],
Tag: LongLossTag,
ClosePosition: true,
}
//// 挂空单
//orders = append(orders, s.ShortOrder[symbol])
//// 挂多单
//orders = append(orders, s.LongOrder[symbol])
//
//// 空单止盈
//orders = append(orders, s.ShortProfitOrder[symbol])
//// 空单止损
//orders = append(orders, s.ShortLossOrder[symbol])
//
//// 多单止盈
//orders = append(orders, s.LongProfitOrder[symbol])
//// 多单止损
//orders = append(orders, s.LongLossOrder[symbol])
if s.LastNRCandles[symbol].Open > s.LastNRCandles[symbol].Close {
// 挂空单
orders = append(orders, s.ShortOrder[symbol])
// 空单止盈
orders = append(orders, s.ShortProfitOrder[symbol])
// 空单止损
orders = append(orders, s.ShortLossOrder[symbol])
}
if s.LastNRCandles[symbol].Open < s.LastNRCandles[symbol].Close {
// 挂多单
orders = append(orders, s.LongOrder[symbol])
// 多单止盈
orders = append(orders, s.LongProfitOrder[symbol])
// 多单止损
orders = append(orders, s.LongLossOrder[symbol])
}
return orders, nil
}

View File

@ -79,6 +79,7 @@ const (
OrderTypeMarket OrderType = "MARKET"
OrderTypeStopLimit OrderType = "STOP_LIMIT"
OrderTypeStopMarket OrderType = "STOP_MARKET"
OrderTypeTakeProfitMarket OrderType = "TAKE_PROFIT_MARKET"
)
/*
@ -127,6 +128,7 @@ type SubmitOrder struct {
Symbol string `json:"symbol" db:"symbol"`
Side SideType `json:"side" db:"side"`
PositionSide PositionSideType `json:"positionSide" db:"positionSide"`
Type OrderType `json:"orderType" db:"order_type"`
Quantity fixedpoint.Value `json:"quantity" db:"quantity"`

84
pkg/types/positionSide.go Normal file
View File

@ -0,0 +1,84 @@
package types
import (
"encoding/json"
"strings"
"github.com/pkg/errors"
"git.qtrade.icu/lychiyu/qbtrade/pkg/style"
)
// PositionSideType define side type of order
type PositionSideType string
const (
PositionSideTypeShort = PositionSideType("SHORT")
PositionSideTypeLong = PositionSideType("LONG")
)
var ErrInvalidPositionSideType = errors.New("invalid position side type")
func StrToPositionSideType(s string) (side PositionSideType, err error) {
switch strings.ToLower(s) {
case "LONG":
side = PositionSideTypeLong
case "SHORT":
side = PositionSideTypeShort
default:
err = ErrInvalidSideType
return side, err
}
return side, err
}
func (side *PositionSideType) UnmarshalJSON(data []byte) error {
var s string
if err := json.Unmarshal(data, &s); err != nil {
return err
}
ss, err := StrToPositionSideType(s)
if err != nil {
return err
}
*side = ss
return nil
}
func (side PositionSideType) Reverse() PositionSideType {
switch side {
case PositionSideTypeShort:
return PositionSideTypeShort
case PositionSideTypeLong:
return PositionSideTypeLong
}
return side
}
func (side PositionSideType) String() string {
return string(side)
}
func (side PositionSideType) Color() string {
if side == PositionSideTypeShort {
return style.GreenColor
}
if side == PositionSideTypeLong {
return style.RedColor
}
return style.GrayColor
}
func PositionSideToColorName(side PositionSideType) string {
return side.Color()
}