bbgo/pkg/strategy/roll/strategy.go

752 lines
23 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package roll
import (
"context"
"errors"
"fmt"
"git.qtrade.icu/lychiyu/bbgo/pkg/bbgo"
"git.qtrade.icu/lychiyu/bbgo/pkg/exchange/binance"
"git.qtrade.icu/lychiyu/bbgo/pkg/fixedpoint"
indicatorv2 "git.qtrade.icu/lychiyu/bbgo/pkg/indicator/v2"
"git.qtrade.icu/lychiyu/bbgo/pkg/types"
"github.com/sirupsen/logrus"
"strconv"
"strings"
"sync"
"time"
)
const ID = "roll"
var log = logrus.WithField("strategy", ID)
var ten = fixedpoint.NewFromInt(10)
var Two = fixedpoint.NewFromInt(2)
var Delta = fixedpoint.NewFromFloat(0.00001)
func init() {
bbgo.RegisterStrategy(ID, &Strategy{})
}
type State struct {
Counter int `json:"counter,omitempty"`
}
type Strategy struct {
Environment *bbgo.Environment
Market types.Market
session *bbgo.ExchangeSession
orderExecutor *bbgo.GeneralOrderExecutor
exchange *binance.Exchange
// persistence fields
Position *types.Position `persistence:"position"`
ProfitStats *types.ProfitStats `persistence:"profit_stats"`
TradeStats *types.TradeStats `persistence:"trade_stats"`
Symbol string `json:"symbol"`
Interval types.Interval `json:"interval"`
Leverage fixedpoint.Value `json:"leverage,omitempty"`
NRInterval types.Interval `json:"nrInterval" modifiable:"true"`
CCIInterval types.Interval `json:"cciInterval" modifiable:"true"`
ATRInterval types.Interval `json:"atrInterval" modifiable:"true"`
NrCount int `json:"nrCount" modifiable:"true"`
CCIWindow int `json:"cciWindow"`
ATRWindow int `json:"atrWindow"`
StrictMode bool `json:"strictMode" modifiable:"true"`
TradeStartHour int `json:"tradeStartHour"`
TradeEndHour int `json:"tradeEndHour"`
PauseTradeLoss fixedpoint.Value `json:"pauseTradeLoss"`
LongCCI fixedpoint.Value `json:"longCCI"`
ShortCCI fixedpoint.Value `json:"shortCCI"`
DryRun bool `json:"dryRun"`
EnablePause bool `json:"enablePause"`
PlacePriceType int `json:"placePriceType"`
ProfitOrderType int `json:"profitOrderType"`
LossType int `json:"lossType"`
ProfitRange fixedpoint.Value `json:"profitRange"`
LossRange fixedpoint.Value `json:"lossRange"`
MidPriceRange float64 `json:"midPriceRange"`
AtrProfitRange float64 `json:"atrProfitRange"`
AtrLossRange float64 `json:"atrLossRange"`
StageHalfAmount []fixedpoint.Value `json:"stageHalfAmount"`
bbgo.QuantityOrAmount
nr *indicatorv2.NRStrean
cci *indicatorv2.CCIStream
atr *indicatorv2.ATRStream
// 当前的盈利阶段
CurrentStage int
Traded bool
TradeType string
TradeRetry int
PauseTradeCount fixedpoint.Value
// 最近一次暂停交易的时间
PauseTradeTime time.Time
// 总盈利
TotalProfit fixedpoint.Value
// 总手续费
TotalFree fixedpoint.Value
// 总交易次数
TotalOrderCount int
TotalProfitCount int
TotalLossCount int
LongOrder types.SubmitOrder
LongProfitOrder types.SubmitOrder
LongLossOrder types.SubmitOrder
ShortOrder types.SubmitOrder
ShortProfitOrder types.SubmitOrder
ShortLossOrder types.SubmitOrder
// 开仓
OpenTrade []types.Trade
// 清仓
EndTrade []types.Trade
OpenQuantity fixedpoint.Value
EndQuantity fixedpoint.Value
// State is a state of your strategy
// When BBGO shuts down, everything in the memory will be dropped
// If you need to store something and restore this information back,
// Simply define the "persistence" tag
State *State `persistence:"state"`
bbgo.StrategyController
getLastPrice func() fixedpoint.Value
}
func (s *Strategy) Defaults() error {
s.PauseTradeCount = fixedpoint.Zero
s.TotalProfit = fixedpoint.Zero
s.TotalFree = fixedpoint.Zero
s.OpenQuantity = fixedpoint.Zero
s.EndQuantity = fixedpoint.Zero
s.PauseTradeTime = time.Now().Add(-24 * time.Hour)
s.TradeRetry = 0
return nil
}
// ID should return the identity of this strategy
func (s *Strategy) ID() string {
return ID
}
// InstanceID returns the identity of the current instance of this strategy.
// You may have multiple instance of a strategy, with different symbols and settings.
// This value will be used for persistence layer to separate the storage.
//
// Run:
//
// redis-cli KEYS "*"
//
// And you will see how this instance ID is used in redis.
func (s *Strategy) InstanceID() string {
return ID + ":" + s.Symbol
}
func (s *Strategy) Initialize() error {
return nil
}
// Subscribe method subscribes specific market data from the given session.
// Before BBGO is connected to the exchange, we need to collect what we want to subscribe.
// Here the strategy needs kline data, so it adds the kline subscription.
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
// We want 1m kline data of the symbol
// It will be BTCUSDT 1m if our s.Symbol is BTCUSDT
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
if !bbgo.IsBackTesting {
session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{})
}
}
// Position control
func (s *Strategy) CurrentPosition() *types.Position {
return s.Position
}
func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error {
order := s.Position.NewMarketCloseOrder(percentage)
if order == nil {
return nil
}
order.Tag = "close"
order.TimeInForce = ""
balances := s.orderExecutor.Session().GetAccount().Balances()
baseBalance := balances[s.Market.BaseCurrency].Available
price := s.getLastPrice()
if order.Side == types.SideTypeBuy {
quoteAmount := balances[s.Market.QuoteCurrency].Available.Div(price)
if order.Quantity.Compare(quoteAmount) > 0 {
order.Quantity = quoteAmount
}
} else if order.Side == types.SideTypeSell && order.Quantity.Compare(baseBalance) > 0 {
order.Quantity = baseBalance
}
order.MarginSideEffect = types.SideEffectTypeAutoRepay
for {
if s.Market.IsDustQuantity(order.Quantity, price) {
return nil
}
_, err := s.orderExecutor.SubmitOrders(ctx, *order)
if err != nil {
order.Quantity = order.Quantity.Mul(fixedpoint.One.Sub(Delta))
continue
}
return nil
}
}
func (s *Strategy) cancelOrders(ctx context.Context, symbol string) {
if len(s.orderExecutor.ActiveMakerOrders().Orders()) <= 0 {
return
}
log.Infof(fmt.Sprintf("[%s] the order is not filled, will cancel all orders", symbol))
if err := s.orderExecutor.GracefulCancel(ctx); err != nil {
log.WithError(err).Errorf("failed to cancel orders")
}
s.Traded = false
s.TradeType = ""
s.TradeRetry = 0
}
// isTradeTime 是否交易时间
func (s *Strategy) isTradeTime(ctx context.Context) bool {
// 如果时间一致则表示不限制交易时间
if s.TradeEndHour == s.TradeStartHour {
return true
}
location, err := time.LoadLocation("Asia/Shanghai")
if err != nil {
return false
}
now := time.Now().In(location)
hour := now.Hour()
return !(hour >= s.TradeStartHour && hour < s.TradeEndHour)
}
func (s *Strategy) isPauseTrade(ctx context.Context) bool {
if !s.EnablePause {
return false
}
// 被暂停次数不为0且最近一次的暂停时间和今天一致则表示暂停
if s.PauseTradeCount != fixedpoint.Zero && s.PauseTradeTime.Day() == time.Now().Day() {
return true
}
// 总收益大于(暂停次数+1)*暂停亏损,则表示暂停
if s.TotalProfit < s.PauseTradeLoss.Mul(s.PauseTradeCount.Add(fixedpoint.One)) {
s.PauseTradeCount.Add(fixedpoint.One)
s.PauseTradeTime = time.Now()
return true
}
return false
}
func (s *Strategy) setInitialLeverage(ctx context.Context) error {
log.Infof("setting futures leverage to %d", s.Leverage.Int()+1)
var ok bool
s.exchange, ok = s.session.Exchange.(*binance.Exchange)
if !ok {
return errors.New("not binance exchange, currently only support binance exchange")
}
futuresClient := s.exchange.GetFuturesClient()
req := futuresClient.NewFuturesChangeInitialLeverageRequest()
req.Symbol(s.Symbol)
req.Leverage(s.Leverage.Int() + 1)
resp, err := req.Do(ctx)
if err != nil {
return err
}
log.Infof("adjusted initial leverage: %+v", resp)
return nil
}
func (s *Strategy) getPlacePrice(ctx context.Context, kline types.KLine) fixedpoint.Value {
placePrice := fixedpoint.Zero
midPrice := (kline.High.Add(kline.Low)).Div(fixedpoint.One * 2)
shouldMid := (((kline.High.Sub(kline.Low)).Div(kline.Low)).Abs()).Float64() <= s.MidPriceRange
switch s.PlacePriceType {
case 0:
if s.TradeType == "long" {
placePrice = kline.High
} else if s.TradeType == "short" {
placePrice = kline.Low
}
case 1:
if s.TradeType == "long" {
if !shouldMid {
placePrice = kline.Low
} else {
placePrice = midPrice
}
} else if s.TradeType == "short" {
if !shouldMid {
placePrice = kline.High
} else {
placePrice = midPrice
}
}
case 2:
if s.TradeType == "long" {
placePrice = midPrice
} else if s.TradeType == "short" {
placePrice = midPrice
}
}
return placePrice
}
func (s *Strategy) generateOrders(ctx context.Context, kline types.KLine) ([]types.SubmitOrder, error) {
var orders []types.SubmitOrder
symbol := kline.Symbol
log.Infof(fmt.Sprintf("place order keline info: symbol %s, high %v, low %v, open %v, close %v", symbol,
kline.High.Float64(), kline.Low.Float64(), kline.Open.Float64(), kline.Close.Float64()))
// 获取下单价格
if s.TradeType == "" {
return orders, nil
}
// 获取下单价格
placePrice := s.getPlacePrice(ctx, kline)
// 止盈订单类型
profitOrderType := types.OrderTypeTakeProfitMarket
// 止损订单类型
lossOrderType := types.OrderTypeStopMarket
if s.ProfitOrderType == 1 {
profitOrderType = types.OrderTypeStopMarket
}
if bbgo.IsBackTesting {
profitOrderType = types.OrderTypeStopLimit
lossOrderType = types.OrderTypeStopLimit
}
// 计算止损止盈价格以ATR为基准或者固定百分比
lossPrice := fixedpoint.Zero
profitPrice := fixedpoint.Zero
lastATR, err := strconv.ParseFloat(strconv.FormatFloat(s.atr.Last(0), 'f', 6, 64), 64)
if err != nil {
log.WithError(err).Error("failed parse atr last value float")
lastATR = 0.0
}
if s.TradeType == "long" {
if s.LossType == 0 || s.atr.Last(0) == 0.0 {
lossPrice = placePrice.Sub(placePrice.Mul(s.LossRange))
profitPrice = placePrice.Add(placePrice.Mul(s.ProfitRange))
} else if s.LossType == 1 {
lossPrice = placePrice.Sub(fixedpoint.Value(1e8 * lastATR * s.AtrLossRange))
profitPrice = placePrice.Add(fixedpoint.Value(1e8 * lastATR * s.AtrProfitRange))
}
} else if s.TradeType == "short" {
if s.LossType == 0 || s.atr.Last(0) == 0.0 {
lossPrice = placePrice.Add(placePrice.Mul(s.LossRange))
profitPrice = placePrice.Sub(placePrice.Mul(s.ProfitRange))
} else if s.LossType == 1 {
lossPrice = placePrice.Add(fixedpoint.Value(1e8 * lastATR * s.AtrLossRange))
profitPrice = placePrice.Sub(fixedpoint.Value(1e8 * lastATR * s.AtrProfitRange))
}
}
// 下单数量
placeQuantity := s.QuantityOrAmount.CalculateQuantity(placePrice).Mul(s.Leverage)
msg := fmt.Sprintf("%v, will place order, amount %v, price %v, quantity %v, lossprice %v, profitprice: %v, atr: %v", s.Symbol,
s.QuantityOrAmount.Amount.Float64(), placePrice.Float64(), placeQuantity.Float64(), lossPrice.Float64(), profitPrice.Float64(),
lastATR)
log.Infof(msg)
bbgo.Notify(msg)
s.ShortOrder = types.SubmitOrder{
Symbol: symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeLimit,
Price: placePrice,
PositionSide: types.PositionSideTypeShort,
Quantity: placeQuantity,
TimeInForce: types.TimeInForceGTC,
Market: s.Market,
}
s.ShortProfitOrder = types.SubmitOrder{
Symbol: symbol,
Side: types.SideTypeBuy,
Type: profitOrderType,
PositionSide: types.PositionSideTypeShort,
StopPrice: profitPrice,
TimeInForce: types.TimeInForceGTC,
Market: s.Market,
ClosePosition: true,
}
s.ShortLossOrder = types.SubmitOrder{
Symbol: symbol,
Side: types.SideTypeBuy,
Type: lossOrderType,
PositionSide: types.PositionSideTypeShort,
StopPrice: lossPrice,
TimeInForce: types.TimeInForceGTC,
Market: s.Market,
ClosePosition: true,
}
s.LongOrder = types.SubmitOrder{
Symbol: symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeLimit,
Price: placePrice,
PositionSide: types.PositionSideTypeLong,
Quantity: placeQuantity,
TimeInForce: types.TimeInForceGTC,
Market: s.Market,
}
s.LongProfitOrder = types.SubmitOrder{
Symbol: symbol,
Side: types.SideTypeSell,
Type: profitOrderType,
PositionSide: types.PositionSideTypeLong,
StopPrice: profitPrice,
TimeInForce: types.TimeInForceGTC,
Market: s.Market,
ClosePosition: true,
}
s.LongLossOrder = types.SubmitOrder{
Symbol: symbol,
Side: types.SideTypeSell,
Type: lossOrderType,
PositionSide: types.PositionSideTypeLong,
StopPrice: lossPrice,
TimeInForce: types.TimeInForceGTC,
Market: s.Market,
ClosePosition: true,
}
if s.TradeType == "short" {
// 挂空单
orders = append(orders, s.ShortOrder)
// 空单止盈
orders = append(orders, s.ShortProfitOrder)
// 空单止损
orders = append(orders, s.ShortLossOrder)
}
if s.TradeType == "long" {
// 挂多单
orders = append(orders, s.LongOrder)
// 多单止盈
orders = append(orders, s.LongProfitOrder)
// 多单止损
orders = append(orders, s.LongLossOrder)
}
return orders, nil
}
func (s *Strategy) placeOrders(ctx context.Context, kline types.KLine) {
symbol := kline.Symbol
orders, err := s.generateOrders(ctx, kline)
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.orderExecutor.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)
return
}
func (s *Strategy) notifyProfit(ctx context.Context, symbol string) {
if s.EndQuantity != s.OpenQuantity {
return
}
profit := fixedpoint.Zero
openProfit := fixedpoint.Zero
endProfit := fixedpoint.Zero
free := fixedpoint.Zero
var openMsgs []string
var endMsgs []string
// 开仓成本
for _, trade := range s.OpenTrade {
openProfit = openProfit.Add(trade.Price.Mul(trade.Quantity))
free = free.Add(trade.Fee)
openMsgs = append(openMsgs, fmt.Sprintf("价格:%v, 数量:%v, 手续费:%v",
trade.Price.Float64(), trade.Quantity.Float64(), trade.Fee.Float64()))
}
// 清仓资产
for _, trade := range s.EndTrade {
endProfit = endProfit.Add(trade.Price.Mul(trade.Quantity))
free = free.Add(trade.Fee)
endMsgs = append(endMsgs, fmt.Sprintf("价格:%v, 数量:%v, 手续费:%v",
trade.Price.Float64(), trade.Quantity.Float64(), trade.Fee.Float64()))
}
side := s.OpenTrade[0].Side
// 做多
if side == types.SideTypeBuy {
profit = endProfit.Sub(openProfit).Sub(free)
}
// 做空
if side == types.SideTypeSell {
profit = openProfit.Sub(endProfit).Sub(free)
}
msg := fmt.Sprintf("交易完成:\n 币种: %s, 方向:%v, 收益:%v, 手续费:%v \n Trade详情\n 开仓Trade\n %s\n 清仓Trade\n %s",
symbol, s.TradeType, profit.Float64(), free.Float64(), strings.Join(openMsgs, "\n"), strings.Join(endMsgs, "\n"))
s.updateAmount(ctx, profit)
s.TotalProfit = s.TotalProfit.Add(profit)
s.TotalFree = s.TotalFree.Add(free)
s.TotalOrderCount += 1
if profit > fixedpoint.Zero {
s.TotalProfitCount += 1
} else {
s.TotalLossCount += 1
}
log.Infof(msg)
bbgo.Notify(msg)
// 重置
s.OpenTrade = []types.Trade{}
s.EndTrade = []types.Trade{}
s.OpenQuantity = fixedpoint.Zero
s.EndQuantity = fixedpoint.Zero
// 记得取消订单
s.cancelOrders(ctx, symbol)
bbgo.Notify(fmt.Sprintf("%v, 总交易次数:%v, 总收益:%v, 总手续费:%v, 盈利次数:%v, 亏损次数:%v", s.Symbol,
s.TotalOrderCount, s.TotalProfit.Float64(), s.TotalFree.Float64(), s.TotalProfitCount, s.TotalLossCount))
}
func (s *Strategy) updateAmount(ctx context.Context, profit fixedpoint.Value) {
// 更新amount
newAmount := s.QuantityOrAmount.Amount.Add(profit)
// 如果当前的总金额大于阶梯上的某一个值,则更新为减半
if newAmount >= s.StageHalfAmount[s.CurrentStage] {
s.QuantityOrAmount.Amount = newAmount.Div(Two)
bbgo.Notify(fmt.Sprintf("%v 结余资金:%v", s.Symbol, s.QuantityOrAmount.Amount.Float64()))
s.CurrentStage += 1
bbgo.Sync(ctx, s)
return
}
s.QuantityOrAmount.Amount = newAmount
//for i, stage := range s.StageHalfAmount {
// if newAmount <= stage {
// s.CurrentStage = i
// s.QuantityOrAmount.Amount = newAmount
// bbgo.Sync(ctx, s)
// return
// }
//}
}
// This strategy simply spent all available quote currency to buy the symbol whenever kline gets closed
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
instanceID := s.InstanceID()
if s.State == nil {
s.State = &State{Counter: 1}
}
if s.Position == nil {
s.Position = types.NewPositionFromMarket(s.Market)
}
if s.ProfitStats == nil {
s.ProfitStats = types.NewProfitStats(s.Market)
}
if s.TradeStats == nil {
s.TradeStats = types.NewTradeStats(s.Symbol)
}
s.Status = types.StrategyStatusRunning
//s.OnSuspend(func() {
// _ = s.orderExecutor.GracefulCancel(ctx)
//})
//s.OnEmergencyStop(func() {
// _ = s.orderExecutor.GracefulCancel(ctx)
// _ = s.ClosePosition(ctx, fixedpoint.One)
//})
s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)
s.orderExecutor.BindEnvironment(s.Environment)
s.orderExecutor.BindProfitStats(s.ProfitStats)
// s.orderExecutor.BindTradeStats(s.TradeStats)
s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
bbgo.Sync(ctx, s)
})
s.orderExecutor.Bind()
bbgo.Notify("BTC滚仓CCINR策略开始运行")
s.nr = session.Indicators(s.Symbol).NR(s.NRInterval, s.NrCount, s.StrictMode)
s.cci = session.Indicators(s.Symbol).CCI(s.CCIInterval, s.CCIWindow)
s.atr = session.Indicators(s.Symbol).ATR(s.ATRInterval, s.ATRWindow)
session.MarketDataStream.OnKLineClosed(func(k types.KLine) {
if k.Symbol != s.Symbol {
return
}
if !s.Traded && k.Interval == s.NRInterval {
// 如若在下一根k线未成交 则取消订单
if s.TradeType != "" && s.TradeRetry > 1 {
bbgo.Notify(fmt.Sprintf("交易信号未成交,取消订单: %s", s.Symbol))
s.cancelOrders(ctx, s.Symbol)
}
if s.TradeType != "" && s.TradeRetry <= 1 {
s.TradeRetry = s.TradeRetry + 1
}
}
})
s.nr.OnUpdate(func(v float64) {
if s.Traded || s.nr.NrKLine.Symbol != s.Symbol {
return
}
if !s.isTradeTime(ctx) || s.isPauseTrade(ctx) {
//pauseMsg := fmt.Sprintf("暂停交易:总收益:%v, 暂停次数:%v, 暂停时间:%v; 暂停时间段:[%v, %v)",
// s.TotalProfit.Float64(), s.PauseTradeCount.Float64(), s.PauseTradeTime, s.TradeStartHour,
// s.TradeEndHour)
//bbgo.Notify(pauseMsg)
return
}
cciV := s.cci.Last(0)
//if cciV > 150 || cciV < -150 {
// testMsg := fmt.Sprintf("Test交易信号币种%s, 方向 %s, 时间: %s, 最高价:%f最低价:%f, CCI: %v, ATR: %v",
// s.Symbol, s.TradeType, s.nr.NrKLine.GetStartTime(), s.nr.NrKLine.High.Float64(),
// s.nr.NrKLine.Low.Float64(), cciV, s.atr.Last(0))
// bbgo.Notify(testMsg)
//}
if cciV <= s.LongCCI.Float64() {
s.TradeType = "long"
} else if cciV >= s.ShortCCI.Float64() {
s.TradeType = "short"
} else {
return
}
msg := fmt.Sprintf("交易信号:币种:%s, 方向 %s, 时间: %s, 最高价:%f最低价:%f, CCI: %v, ATR: %v",
s.Symbol, s.TradeType, s.nr.NrKLine.GetStartTime(), s.nr.NrKLine.High.Float64(),
s.nr.NrKLine.Low.Float64(), cciV, s.atr.Last(0))
bbgo.Notify(msg)
tk := s.nr.NrKLine
s.placeOrders(ctx, tk)
})
session.UserDataStream.OnOrderUpdate(func(order types.Order) {
orderSymbol := order.Symbol
if orderSymbol != s.Symbol {
return
}
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.Traded = true
s.TradeRetry = 0
bbgo.Notify("订单成交通知:\n 币种:%s, 方向:%s, 价格:%s, 数量:%s", order.Symbol, s.TradeType,
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.Traded = true
s.TradeRetry = 0
bbgo.Notify("订单成交通知:\n 币种:%s, 方向:%s, 价格:%s, 数量:%s", order.Symbol, s.TradeType,
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)
bbgo.Notify("订单止盈或止损通知:\n %s", order.Symbol)
s.Traded = false
s.TradeRetry = 0
s.TradeType = ""
} 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)
}
} else if order.Status == types.OrderStatusCanceled {
log.Infof("canceled order %+v", order)
}
})
session.UserDataStream.OnTradeUpdate(func(trade types.Trade) {
symbol := trade.Symbol
if symbol != s.Symbol {
return
}
if (trade.Side == types.SideTypeBuy && s.TradeType == "long") || (trade.Side == types.SideTypeSell && s.TradeType == "short") {
s.OpenTrade = append(s.OpenTrade, trade)
s.OpenQuantity = s.OpenQuantity.Add(trade.Quantity)
}
if (trade.Side == types.SideTypeSell && s.TradeType == "long") || (trade.Side == types.SideTypeBuy && s.TradeType == "short") {
s.EndTrade = append(s.EndTrade, trade)
s.EndQuantity = s.EndQuantity.Add(trade.Quantity)
s.notifyProfit(ctx, symbol)
}
log.Infof("trade: symbol %s, side %s, price %f, fee %f, quantity %f, buyer %v, maker %v",
symbol, trade.Side, trade.Price.Float64(), trade.Fee.Float64(), trade.Quantity.Float64(),
trade.IsBuyer, trade.IsMaker)
})
s.OnSuspend(func() {
// Cancel active orders
_ = s.orderExecutor.GracefulCancel(ctx)
})
s.OnEmergencyStop(func() {
// Cancel active orders
_ = s.orderExecutor.GracefulCancel(ctx)
// Close 100% position
//_ = s.ClosePosition(ctx, fixedpoint.One)
})
bbgo.OnShutdown(ctx, func(ctx context.Context, wg *sync.WaitGroup) {
defer wg.Done()
if err := s.orderExecutor.GracefulCancel(ctx); err != nil {
log.WithError(err).Error("unable to cancel open orders...")
}
bbgo.Sync(ctx, s)
})
return nil
}