bbgo/pkg/strategy/bolladxema/strategy.go

703 lines
22 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 bolladxema
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"
)
/*
布林带+ADX+EMA策略
1. 布林带,判断是否在布林带内,在布林带上,做多,在布林带下,做空。
2. ADX判断是否在ADX区间内在区间内做多在区间外做空。
3. EMA判断是否在EMA区间内在区间内做多在区间外做空。
4. 默认开仓量,默认止盈止损。
*/
const ID = "bolladxema"
var log = logrus.WithField("strategy", ID)
var ten = fixedpoint.NewFromInt(10)
var Two = fixedpoint.NewFromInt(2)
func init() {
bbgo.RegisterStrategy(ID, &Strategy{})
}
type State struct {
Counter int `json:"counter,omitempty"`
}
type BollingerSetting struct {
types.IntervalWindow
BandWidth float64 `json:"bandWidth"`
}
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"`
DryRun bool `json:"dryRun"`
Symbol string `json:"symbol"`
Interval types.Interval `json:"interval"`
Leverage fixedpoint.Value `json:"leverage,omitempty"`
ProfitType int `json:"profitType"`
PlaceOrderType int `json:"placeOrderType"`
EnablePause bool `json:"enablePause"`
TradeStartHour int `json:"tradeStartHour"`
TradeEndHour int `json:"tradeEndHour"`
PauseTradeLoss fixedpoint.Value `json:"pauseTradeLoss"`
ProfitHRange fixedpoint.Value `json:"profitHRange"`
LossHRange fixedpoint.Value `json:"lossHRange"`
ProfitMRange fixedpoint.Value `json:"profitMRange"`
LossMRange fixedpoint.Value `json:"lossMRange"`
ProfitLRange fixedpoint.Value `json:"profitLRange"`
LossLRange fixedpoint.Value `json:"lossLRange"`
AtrProfitMultiple float64 `json:"atrProfitMultiple"`
AtrLossMultiple float64 `json:"atrLossMultiple"`
EnableADX bool `json:"enableADX"`
ADXHSingle float64 `json:"adxHSingle"`
ADXMSingle float64 `json:"adxMSingle"`
ADXLSingle float64 `json:"adxLSingle"`
LongCCI fixedpoint.Value `json:"longCCI"`
ShortCCI fixedpoint.Value `json:"shortCCI"`
State *State `persistence:"state"`
Bollinger *BollingerSetting `json:"bollinger"`
EMASetting types.IntervalWindow `json:"emaSetting"`
ADXSetting types.IntervalWindow `json:"adxSetting"`
ATRSetting types.IntervalWindow `json:"atrSetting"`
CCISetting types.IntervalWindow `json:"cciSetting"`
StageHalfAmount []fixedpoint.Value `json:"stageHalfAmount"`
bbgo.QuantityOrAmount
// 当前的盈利阶段
CurrentStage int
Traded bool
TradeSignal 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
ADX *indicatorv2.ADXStream
EMA *indicatorv2.EWMAStream
BOLL *indicatorv2.BOLLStream
ATR *indicatorv2.ATRStream
CCI *indicatorv2.CCIStream
bbgo.StrategyController
}
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
s.Traded = false
s.TradeSignal = ""
return nil
}
// ID should return the identity of this strategy
func (s *Strategy) ID() string {
return ID
}
func (s *Strategy) InstanceID() string {
return ID + ":" + s.Symbol
}
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
if s.Bollinger != nil && s.Bollinger.Interval != "" && s.Bollinger.Interval != s.Interval {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Bollinger.Interval})
}
if s.EMASetting.Interval != "" && s.EMASetting.Interval != s.Interval {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.EMASetting.Interval})
}
if s.ADXSetting.Interval != "" && s.ADXSetting.Interval != s.Interval {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.ADXSetting.Interval})
}
if s.ATRSetting.Interval != "" && s.ATRSetting.Interval != s.Interval {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.ATRSetting.Interval})
}
if !bbgo.IsBackTesting {
session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{})
}
}
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.TradeSignal = ""
s.TradeRetry = 0
}
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) GetTradeSignal(k types.KLine, adx, bollUp, bollDown, ema, cciV float64) string {
if k.High.Float64() >= bollUp && k.Low.Float64() <= bollDown {
// k线跨越布林带不入场
return ""
}
// 小于最小ADX信号
if s.EnableADX && adx < s.ADXLSingle {
return ""
}
if k.Open >= k.Close && k.Low.Float64() <= bollDown && k.High.Float64() >= bollDown && k.Close.Float64() <= ema && cciV <= s.LongCCI.Float64() {
// k线收跌且触及下轨但是最高价会在下轨上并小于ema开多
return "long"
}
if k.Open <= k.Close && k.High.Float64() >= bollUp && k.Low.Float64() <= bollUp && k.Close.Float64() >= ema && cciV >= s.ShortCCI.Float64() {
// k线收涨且触及上轨但是最高价会在上轨下并大于ema开空
return "short"
}
return ""
}
func (s *Strategy) generateOrders(k types.KLine, bollDiff, adx float64) ([]types.SubmitOrder, error) {
var orders []types.SubmitOrder
symbol := k.Symbol
// 止盈订单类型
profitOrderType := types.OrderTypeTakeProfitMarket
// 止损订单类型
lossOrderType := types.OrderTypeStopMarket
if s.PlaceOrderType == 1 {
profitOrderType = types.OrderTypeStopMarket
}
if bbgo.IsBackTesting {
profitOrderType = types.OrderTypeStopLimit
lossOrderType = types.OrderTypeStopLimit
}
// 下单价格
placePrice := k.Close
// 计算止损止盈价格以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
}
// 依据不同的adx来设置止盈止损
profitRange := s.ProfitLRange
lossRange := s.LossLRange
if adx >= s.ADXHSingle {
profitRange = s.ProfitHRange
lossRange = s.LossHRange
} else if adx >= s.ADXMSingle {
profitRange = s.ProfitMRange
lossRange = s.LossMRange
}
//if bollDiff >= 0.03 {
// profitRange = profitRange.Mul(fixedpoint.NewFromFloat(1.5))
//}
if s.TradeSignal == "long" {
// 做多
if s.ProfitType == 0 || s.ATR.Last(0) == 0.0 {
lossPrice = placePrice.Mul(fixedpoint.One.Sub(lossRange))
profitPrice = placePrice.Mul(fixedpoint.One.Add(profitRange))
} else {
lossPrice = placePrice.Sub(fixedpoint.Value(1e8 * lastATR * s.AtrLossMultiple))
profitPrice = placePrice.Add(fixedpoint.Value(1e8 * lastATR * s.AtrProfitMultiple))
}
} else {
//做空
if s.ProfitType == 0 || s.ATR.Last(0) == 0.0 {
lossPrice = placePrice.Mul(fixedpoint.One.Add(lossRange))
profitPrice = placePrice.Mul(fixedpoint.One.Sub(profitRange))
} else {
lossPrice = placePrice.Add(fixedpoint.Value(1e8 * lastATR * s.AtrLossMultiple))
profitPrice = placePrice.Sub(fixedpoint.Value(1e8 * lastATR * s.AtrProfitMultiple))
}
}
// 下单数量
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)
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.TradeSignal == "short" {
// 挂空单
orders = append(orders, s.ShortOrder)
// 空单止盈
orders = append(orders, s.ShortProfitOrder)
// 空单止损
orders = append(orders, s.ShortLossOrder)
}
if s.TradeSignal == "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, k types.KLine, bollDiff, adx float64) {
if s.TradeSignal == "" {
return
}
symbol := k.Symbol
orders, err := s.generateOrders(k, bollDiff, adx)
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("price%v, quantity%v, fee%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("price%v, quantity%v, fee%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("Trade finish\n symbol: %s, signal%v, profit%v, fee%v \n Trade details\n OpenTrade\n %s\n CloseTrade\n %s",
symbol, s.TradeSignal, 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, Total Count%v, Profit%v, Fee%v, Profit Count%v, Loss Count%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
}
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
instanceID := s.InstanceID()
// Initialize the default value for state
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.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滚仓布林带策略开始运行")
s.BOLL = session.Indicators(s.Symbol).BOLL(s.Bollinger.IntervalWindow, s.Bollinger.BandWidth)
s.EMA = session.Indicators(s.Symbol).EMA(s.EMASetting)
s.ADX = session.Indicators(s.Symbol).ADX(s.ADXSetting.Interval, s.ADXSetting.Window)
s.ATR = session.Indicators(s.Symbol).ATR(s.ATRSetting.Interval, s.ATRSetting.Window)
s.CCI = session.Indicators(s.Symbol).CCI(s.CCISetting.Interval, s.CCISetting.Window)
session.MarketDataStream.OnKLineClosed(func(k types.KLine) {
if k.Symbol != s.Symbol {
return
}
adx := s.ADX.Last(0)
// 小于最小ADX信号
if s.EnableADX && adx < s.ADXLSingle {
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
}
if !s.Traded {
// 如若在下一根k线未成交 则取消订单
if s.TradeSignal != "" && s.TradeRetry > 1 {
bbgo.Notify(fmt.Sprintf("Trade signal not traded, cancel orders: %s", s.Symbol))
s.cancelOrders(ctx, s.Symbol)
}
if s.TradeSignal != "" && s.TradeRetry <= 1 {
s.TradeRetry = s.TradeRetry + 1
}
}
if s.TradeSignal != "" {
return
}
bollUp := s.BOLL.UpBand.Last(0)
bolldown := s.BOLL.DownBand.Last(0)
ema := s.EMA.Last(0)
cciV := s.CCI.Last(0)
signal := s.GetTradeSignal(k, adx, bollUp, bolldown, ema, cciV)
if signal == "" {
return
}
s.TradeSignal = signal
msg := fmt.Sprintf("trade singal info, symbol%s, single %s, time: %s,open%fclose%f, high%flow:%f, ema: %v, adx: %v, bollUp %f, bollDown %f, cci %f",
s.Symbol, signal, k.EndTime, k.Open.Float64(), k.Close.Float64(), k.High.Float64(), k.Low.Float64(), ema, adx, bollUp, bolldown, cciV)
bollDiff := (bollUp - bolldown) / bolldown
s.placeOrders(ctx, k, bollDiff, adx)
bbgo.Notify(msg)
})
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("Order traded notify\n symbol%s, signal%s, price%s, quantity%s", order.Symbol, s.TradeSignal,
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("Order traded notify\n symbol%s, signal%s, price%s, quantity%s", order.Symbol, s.TradeSignal,
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("Order stop profit or loss notify\n %s", order.Symbol)
s.Traded = false
s.TradeRetry = 0
s.TradeSignal = ""
} 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.TradeSignal == "long") || (trade.Side == types.SideTypeSell && s.TradeSignal == "short") {
s.OpenTrade = append(s.OpenTrade, trade)
s.OpenQuantity = s.OpenQuantity.Add(trade.Quantity)
}
if (trade.Side == types.SideTypeSell && s.TradeSignal == "long") || (trade.Side == types.SideTypeBuy && s.TradeSignal == "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
}