diff --git a/config/drift.yaml b/config/drift.yaml index 3d2a5a23e..fcbd29373 100644 --- a/config/drift.yaml +++ b/config/drift.yaml @@ -25,35 +25,36 @@ exchangeStrategies: drift: canvasPath: "./output.png" symbol: ETHBUSD + limitOrder: false # kline interval for indicators - interval: 15m - window: 2 - stoploss: 4.3% - source: close + interval: 2m + window: 6 + stoploss: 0.23% + source: ohlc4 predictOffset: 2 - noTrailingStopLoss: true + noTrailingStopLoss: false trailingStopLossType: kline # stddev on high/low-source - hlVarianceMultiplier: 0.1 - hlRangeWindow: 5 - window1m: 49 - smootherWindow1m: 80 - fisherTransformWindow1m: 74 + hlVarianceMultiplier: 0.03 + hlRangeWindow: 4 smootherWindow: 3 - fisherTransformWindow: 160 + fisherTransformWindow: 117 + window1m: 42 + smootherWindow1m: 118 + fisherTransformWindow1m: 319 atrWindow: 14 # orders not been traded will be canceled after `pendingMinutes` minutes - pendingMinutes: 10 + pendingMinutes: 3 noRebalance: true trendWindow: 12 - rebalanceFilter: 1.5 + rebalanceFilter: 2 - trailingActivationRatio: [0.003] - trailingCallbackRate: [0.0006] - driftFilterPos: 1.2 - driftFilterNeg: -1.2 - ddriftFilterPos: 0.4 - ddriftFilterNeg: -0.4 + trailingActivationRatio: [0.0015, 0.002, 0.004, 0.01] + trailingCallbackRate: [0.0001, 0.00012, 0.001, 0.002] + #driftFilterPos: 0.4 + #driftFilterNeg: -0.42 + #ddriftFilterPos: 0 + #ddriftFilterNeg: 0 generateGraph: true graphPNLDeductFee: true @@ -91,15 +92,15 @@ sync: - ETHBUSD backtest: - startTime: "2022-01-01" - endTime: "2022-08-30" + startTime: "2022-09-01" + endTime: "2022-09-30" symbols: - ETHBUSD sessions: [binance] accounts: binance: makerFeeRate: 0.0000 - #takerFeeRate: 0.00001 + takerFeeRate: 0.0000 balances: - ETH: 10 - BUSD: 5000.0 + ETH: 0 + BUSD: 1000.0 diff --git a/config/driftBTC.yaml b/config/driftBTC.yaml index 80b7a7b66..f2baf4d68 100644 --- a/config/driftBTC.yaml +++ b/config/driftBTC.yaml @@ -8,7 +8,10 @@ persistence: sessions: binance: exchange: binance - futures: false + #futures: true + #margin: true + #isolatedMargin: true + #isolatedMarginSymbol: BTCUSDT envVarPrefix: binance heikinAshi: false @@ -23,34 +26,35 @@ exchangeStrategies: - on: binance drift: + limitOrder: false canvasPath: "./output.png" symbol: BTCUSDT # kline interval for indicators interval: 4m - window: 1 - stoploss: 0.22% - source: hl2 + window: 2 + stoploss: 0.13% + source: ohlc4 predictOffset: 2 noTrailingStopLoss: false - trailingStopLossType: realtime + trailingStopLossType: kline # stddev on high/low-source - hlVarianceMultiplier: 0.01 - hlRangeWindow: 5 - smootherWindow: 2 - fisherTransformWindow: 27 - window1m: 58 - smootherWindow1m: 118 - fisherTransformWindow1m: 319 + hlVarianceMultiplier: 0.22 + hlRangeWindow: 4 + smootherWindow: 1 + fisherTransformWindow: 96 + window1m: 8 + smootherWindow1m: 4 + fisherTransformWindow1m: 320 atrWindow: 14 # orders not been traded will be canceled after `pendingMinutes` minutes - pendingMinutes: 2 + pendingMinutes: 10 noRebalance: true trendWindow: 576 - rebalanceFilter: 0 - driftFilterPos: 0.6 - driftFilterNeg: -0.6 - ddriftFilterPos: 0.00008 - ddriftFilterNeg: -0.00008 + rebalanceFilter: 1.2 + #driftFilterPos: 0.5 + #driftFilterNeg: -0.5 + #ddriftFilterPos: 0.0008 + #ddriftFilterNeg: -0.0008 # ActivationRatio should be increasing order # when farest price from entry goes over that ratio, start using the callback ratio accordingly to do trailingstop @@ -135,4 +139,4 @@ backtest: takerFeeRate: 0.000 balances: BTC: 0 - USDT: 21 + USDT: 1000 diff --git a/pkg/bbgo/order_executor_general.go b/pkg/bbgo/order_executor_general.go index decf0dc0b..ca94919c7 100644 --- a/pkg/bbgo/order_executor_general.go +++ b/pkg/bbgo/order_executor_general.go @@ -197,17 +197,14 @@ type OpenPositionOptions struct { // Leverage is used for leveraged position and account // Leverage is not effected when using non-leverage spot account - Leverage fixedpoint.Value `json:"leverage,omitempty"` + Leverage fixedpoint.Value `json:"leverage,omitempty" modifiable:"true"` // Quantity will be used first, it will override the leverage if it's given Quantity fixedpoint.Value `json:"quantity,omitempty"` - // MarketOrder set to true to open a position with a market order - // default is MarketOrder = true - MarketOrder bool `json:"marketOrder,omitempty"` - // LimitOrder set to true to open a position with a limit order - LimitOrder bool `json:"limitOrder,omitempty"` + // default is false, and will send MarketOrder + LimitOrder bool `json:"limitOrder,omitempty" modifiable:"true"` // LimitOrderTakerRatio is used when LimitOrder = true, it adjusts the price of the limit order with a ratio. // So you can ensure that the limit order can be a taker order. Higher the ratio, higher the chance it could be a taker order. @@ -222,7 +219,25 @@ type OpenPositionOptions struct { Tags []string `json:"-" yaml:"-"` } -func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPositionOptions) error { +// Delta used to modify the order to submit, especially for the market order +var QuantityReduceDelta fixedpoint.Value = fixedpoint.NewFromFloat(0.005) + +func (e *GeneralOrderExecutor) reduceQuantityAndSubmitOrder(ctx context.Context, price fixedpoint.Value, submitOrder types.SubmitOrder) (types.OrderSlice, error) { + for { + createdOrder, err2 := e.SubmitOrders(ctx, submitOrder) + if err2 != nil { + submitOrder.Quantity = submitOrder.Quantity.Mul(fixedpoint.One.Sub(QuantityReduceDelta)) + if e.position.Market.IsDustQuantity(submitOrder.Quantity, price) { + return nil, err2 + } + continue + } + log.Infof("created order: %+v", createdOrder) + return createdOrder, nil + } +} + +func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPositionOptions) (types.OrderSlice, error) { price := options.Price submitOrder := types.SubmitOrder{ Symbol: e.position.Symbol, @@ -246,9 +261,7 @@ func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPos } } - if options.MarketOrder { - submitOrder.Type = types.OrderTypeMarket - } else if options.LimitOrder { + if options.LimitOrder { submitOrder.Type = types.OrderTypeLimit submitOrder.Price = price } @@ -259,11 +272,15 @@ func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPos if quantity.IsZero() { quoteQuantity, err := CalculateQuoteQuantity(ctx, e.session, e.position.QuoteCurrency, options.Leverage) if err != nil { - return err + return nil, err } quantity = quoteQuantity.Div(price) } + if e.position.Market.IsDustQuantity(quantity, price) { + log.Warnf("dust quantity: %v", quantity) + return nil, nil + } quoteQuantity := quantity.Mul(price) if e.session.Margin && !e.marginQuoteMaxBorrowable.IsZero() && quoteQuantity.Compare(e.marginQuoteMaxBorrowable) > 0 { @@ -274,22 +291,20 @@ func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPos submitOrder.Side = types.SideTypeBuy submitOrder.Quantity = quantity - Notify("Opening %s long position with quantity %f at price %f", e.position.Symbol, quantity.Float64(), price.Float64()) - createdOrder, err2 := e.SubmitOrders(ctx, submitOrder) - if err2 != nil { - return err2 - } - - log.Infof("created order: %+v", createdOrder) - return nil + Notify("Opening %s long position with quantity %v at price %v", e.position.Symbol, quantity, price) + return e.reduceQuantityAndSubmitOrder(ctx, price, submitOrder) } else if options.Short { if quantity.IsZero() { var err error quantity, err = CalculateBaseQuantity(e.session, e.position.Market, price, quantity, options.Leverage) if err != nil { - return err + return nil, err } } + if e.position.Market.IsDustQuantity(quantity, price) { + log.Warnf("dust quantity: %v", quantity) + return nil, nil + } if e.session.Margin && !e.marginBaseMaxBorrowable.IsZero() && quantity.Sub(baseBalance.Available).Compare(e.marginBaseMaxBorrowable) > 0 { log.Warnf("adjusting %f quantity according to the max margin base borrowable amount: %f", quantity.Float64(), e.marginBaseMaxBorrowable.Float64()) @@ -300,17 +315,11 @@ func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPos submitOrder.Side = types.SideTypeSell submitOrder.Quantity = quantity - Notify("Opening %s short position with quantity %f at price %f", e.position.Symbol, quantity.Float64(), price.Float64()) - createdOrder, err2 := e.SubmitOrders(ctx, submitOrder) - if err2 != nil { - return err2 - } - - log.Infof("created order: %+v", createdOrder) - return nil + Notify("Opening %s short position with quantity %v at price %v", e.position.Symbol, quantity, price) + return e.reduceQuantityAndSubmitOrder(ctx, price, submitOrder) } - return errors.New("options Long or Short must be set") + return nil, errors.New("options Long or Short must be set") } // GracefulCancelActiveOrderBook cancels the orders from the active orderbook. diff --git a/pkg/bbgo/risk.go b/pkg/bbgo/risk.go index 633ae42e2..8648424d9 100644 --- a/pkg/bbgo/risk.go +++ b/pkg/bbgo/risk.go @@ -5,6 +5,7 @@ import ( "fmt" "time" + "github.com/pkg/errors" log "github.com/sirupsen/logrus" "github.com/c9s/bbgo/pkg/fixedpoint" @@ -243,7 +244,8 @@ func CalculateBaseQuantity(session *ExchangeSession, market types.Market, price, } } - return quantity, fmt.Errorf("quantity is zero, can not submit sell order, please check your quantity settings, your account balances: %+v", balances) + return quantity, types.NewZeroAssetError( + fmt.Errorf("quantity is zero, can not submit sell order, please check your quantity settings, your account balances: %+v", balances)) } usdBalances, restBalances := usdFiatBalances(balances) @@ -329,7 +331,8 @@ func CalculateBaseQuantity(session *ExchangeSession, market types.Market, price, return maxPositionQuantity, nil } - return quantity, fmt.Errorf("quantity is zero, can not submit sell order, please check your settings") + return quantity, types.NewZeroAssetError( + errors.New("quantity is zero, can not submit sell order, please check your settings")) } func CalculateQuoteQuantity(ctx context.Context, session *ExchangeSession, quoteCurrency string, leverage fixedpoint.Value) (fixedpoint.Value, error) { diff --git a/pkg/bbgo/scale_test.go b/pkg/bbgo/scale_test.go index 66e8544e3..ab7ac2cf8 100644 --- a/pkg/bbgo/scale_test.go +++ b/pkg/bbgo/scale_test.go @@ -6,7 +6,7 @@ import ( "github.com/stretchr/testify/assert" ) -const Delta = 1e-9 +const delta = 1e-9 func TestExponentialScale(t *testing.T) { // graph see: https://www.desmos.com/calculator/ip0ijbcbbf @@ -19,8 +19,8 @@ func TestExponentialScale(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "f(x) = 0.001000 * 1.002305 ^ (x - 1000.000000)", scale.String()) - assert.InDelta(t, 0.001, scale.Call(1000.0), Delta) - assert.InDelta(t, 0.01, scale.Call(2000.0), Delta) + assert.InDelta(t, 0.001, scale.Call(1000.0), delta) + assert.InDelta(t, 0.01, scale.Call(2000.0), delta) for x := 1000; x <= 2000; x += 100 { y := scale.Call(float64(x)) @@ -38,8 +38,8 @@ func TestExponentialScale_Reverse(t *testing.T) { assert.NoError(t, err) assert.Equal(t, "f(x) = 0.100000 * 0.995405 ^ (x - 1000.000000)", scale.String()) - assert.InDelta(t, 0.1, scale.Call(1000.0), Delta) - assert.InDelta(t, 0.001, scale.Call(2000.0), Delta) + assert.InDelta(t, 0.1, scale.Call(1000.0), delta) + assert.InDelta(t, 0.001, scale.Call(2000.0), delta) for x := 1000; x <= 2000; x += 100 { y := scale.Call(float64(x)) @@ -57,8 +57,8 @@ func TestLogScale(t *testing.T) { err := scale.Solve() assert.NoError(t, err) assert.Equal(t, "f(x) = 0.001303 * log(x - 999.000000) + 0.001000", scale.String()) - assert.InDelta(t, 0.001, scale.Call(1000.0), Delta) - assert.InDelta(t, 0.01, scale.Call(2000.0), Delta) + assert.InDelta(t, 0.001, scale.Call(1000.0), delta) + assert.InDelta(t, 0.01, scale.Call(2000.0), delta) for x := 1000; x <= 2000; x += 100 { y := scale.Call(float64(x)) t.Logf("%s = %f", scale.FormulaOf(float64(x)), y) @@ -74,8 +74,8 @@ func TestLinearScale(t *testing.T) { err := scale.Solve() assert.NoError(t, err) assert.Equal(t, "f(x) = 0.007000 * x + -4.000000", scale.String()) - assert.InDelta(t, 3, scale.Call(1000), Delta) - assert.InDelta(t, 10, scale.Call(2000), Delta) + assert.InDelta(t, 3, scale.Call(1000), delta) + assert.InDelta(t, 10, scale.Call(2000), delta) for x := 1000; x <= 2000; x += 100 { y := scale.Call(float64(x)) t.Logf("%s = %f", scale.FormulaOf(float64(x)), y) @@ -91,8 +91,8 @@ func TestLinearScale2(t *testing.T) { err := scale.Solve() assert.NoError(t, err) assert.Equal(t, "f(x) = 0.150000 * x + -0.050000", scale.String()) - assert.InDelta(t, 0.1, scale.Call(1), Delta) - assert.InDelta(t, 0.4, scale.Call(3), Delta) + assert.InDelta(t, 0.1, scale.Call(1), delta) + assert.InDelta(t, 0.4, scale.Call(3), delta) } func TestQuadraticScale(t *testing.T) { @@ -105,9 +105,9 @@ func TestQuadraticScale(t *testing.T) { err := scale.Solve() assert.NoError(t, err) assert.Equal(t, "f(x) = 0.000550 * x ^ 2 + 0.135000 * x + 1.000000", scale.String()) - assert.InDelta(t, 1, scale.Call(0), Delta) - assert.InDelta(t, 20, scale.Call(100.0), Delta) - assert.InDelta(t, 50.0, scale.Call(200.0), Delta) + assert.InDelta(t, 1, scale.Call(0), delta) + assert.InDelta(t, 20, scale.Call(100.0), delta) + assert.InDelta(t, 50.0, scale.Call(200.0), delta) for x := 0; x <= 200; x += 1 { y := scale.Call(float64(x)) t.Logf("%s = %f", scale.FormulaOf(float64(x)), y) @@ -127,11 +127,11 @@ func TestPercentageScale(t *testing.T) { v, err := s.Scale(0.0) assert.NoError(t, err) - assert.InDelta(t, 1.0, v, Delta) + assert.InDelta(t, 1.0, v, delta) v, err = s.Scale(1.0) assert.NoError(t, err) - assert.InDelta(t, 100.0, v, Delta) + assert.InDelta(t, 100.0, v, delta) }) t.Run("from -1.0 to 1.0", func(t *testing.T) { @@ -146,11 +146,11 @@ func TestPercentageScale(t *testing.T) { v, err := s.Scale(-1.0) assert.NoError(t, err) - assert.InDelta(t, 10.0, v, Delta) + assert.InDelta(t, 10.0, v, delta) v, err = s.Scale(1.0) assert.NoError(t, err) - assert.InDelta(t, 100.0, v, Delta) + assert.InDelta(t, 100.0, v, delta) }) t.Run("reverse -1.0 to 1.0", func(t *testing.T) { @@ -165,19 +165,19 @@ func TestPercentageScale(t *testing.T) { v, err := s.Scale(-1.0) assert.NoError(t, err) - assert.InDelta(t, 100.0, v, Delta) + assert.InDelta(t, 100.0, v, delta) v, err = s.Scale(1.0) assert.NoError(t, err) - assert.InDelta(t, 10.0, v, Delta) + assert.InDelta(t, 10.0, v, delta) v, err = s.Scale(2.0) assert.NoError(t, err) - assert.InDelta(t, 10.0, v, Delta) + assert.InDelta(t, 10.0, v, delta) v, err = s.Scale(-2.0) assert.NoError(t, err) - assert.InDelta(t, 100.0, v, Delta) + assert.InDelta(t, 100.0, v, delta) }) t.Run("negative range", func(t *testing.T) { @@ -192,10 +192,10 @@ func TestPercentageScale(t *testing.T) { v, err := s.Scale(0.0) assert.NoError(t, err) - assert.InDelta(t, -100.0, v, Delta) + assert.InDelta(t, -100.0, v, delta) v, err = s.Scale(1.0) assert.NoError(t, err) - assert.InDelta(t, 100.0, v, Delta) + assert.InDelta(t, 100.0, v, delta) }) } diff --git a/pkg/dynamic/field.go b/pkg/dynamic/field.go index 990ad4982..c004458de 100644 --- a/pkg/dynamic/field.go +++ b/pkg/dynamic/field.go @@ -1,6 +1,7 @@ package dynamic import ( + "errors" "reflect" "strings" ) @@ -29,11 +30,21 @@ func LookupSymbolField(rs reflect.Value) (string, bool) { // Used by bbgo/interact_modify.go func GetModifiableFields(val reflect.Value, callback func(tagName, name string)) { - for i := 0; i < val.Type().NumField(); i++ { + if val.Kind() == reflect.Ptr { + val = val.Elem() + } + if !val.IsValid() { + return + } + num := val.Type().NumField() + for i := 0; i < num; i++ { t := val.Type().Field(i) if !t.IsExported() { continue } + if t.Anonymous { + GetModifiableFields(val.Field(i), callback) + } modifiable := t.Tag.Get("modifiable") if modifiable != "true" { continue @@ -50,6 +61,17 @@ func GetModifiableFields(val reflect.Value, callback func(tagName, name string)) var zeroValue reflect.Value = reflect.Zero(reflect.TypeOf(0)) func GetModifiableField(val reflect.Value, name string) (reflect.Value, bool) { + if val.Kind() == reflect.Ptr { + if val.IsNil() { + return zeroValue, false + } + } + if val.Kind() != reflect.Struct { + return zeroValue, false + } + if !val.IsValid() { + return zeroValue, false + } field, ok := val.Type().FieldByName(name) if !ok { return zeroValue, ok @@ -61,5 +83,31 @@ func GetModifiableField(val reflect.Value, name string) (reflect.Value, bool) { if jsonTag == "" || jsonTag == "-" { return zeroValue, false } - return val.FieldByName(name), true + value, err := FieldByIndexErr(val, field.Index) + if err != nil { + return zeroValue, false + } + return value, true +} + +// Modified from golang 1.19.1 reflect to eliminate all possible panic +func FieldByIndexErr(v reflect.Value, index []int) (reflect.Value, error) { + if len(index) == 1 { + return v.Field(index[0]), nil + } + if v.Kind() != reflect.Struct { + return zeroValue, errors.New("should receive a Struct") + } + for i, x := range index { + if i > 0 { + if v.Kind() == reflect.Ptr && v.Type().Elem().Kind() == reflect.Struct { + if v.IsNil() { + return zeroValue, errors.New("reflect: indirection through nil pointer to embedded struct field ") + } + v = v.Elem() + } + } + v = v.Field(x) + } + return v, nil } diff --git a/pkg/dynamic/field_test.go b/pkg/dynamic/field_test.go index bbdd39056..a57ae9228 100644 --- a/pkg/dynamic/field_test.go +++ b/pkg/dynamic/field_test.go @@ -9,7 +9,17 @@ import ( "github.com/stretchr/testify/assert" ) +type Inner struct { + Field5 float64 `json:"field5,omitempty" modifiable:"true"` +} + +type InnerPointer struct { + Field6 float64 `json:"field6" modifiable:"true"` +} + type Strategy struct { + Inner + *InnerPointer Field1 fixedpoint.Value `json:"field1" modifiable:"true"` Field2 float64 `json:"field2"` field3 float64 `json:"field3" modifiable:"true"` @@ -24,7 +34,6 @@ func TestGetModifiableFields(t *testing.T) { assert.NotEqual(t, name, "Field2") assert.NotEqual(t, tagName, "field3") assert.NotEqual(t, name, "Field3") - }) } @@ -34,6 +43,13 @@ func TestGetModifiableField(t *testing.T) { val := reflect.ValueOf(s).Elem() _, ok := GetModifiableField(val, "Field1") assert.True(t, ok) + _, ok = GetModifiableField(val, "Field5") + assert.True(t, ok) + _, ok = GetModifiableField(val, "Field6") + assert.False(t, ok) + s.InnerPointer = &InnerPointer{} + _, ok = GetModifiableField(val, "Field6") + assert.True(t, ok) _, ok = GetModifiableField(val, "Field2") assert.False(t, ok) _, ok = GetModifiableField(val, "Field3") diff --git a/pkg/fixedpoint/const.go b/pkg/fixedpoint/const.go index e00c3d1e3..86e63cd23 100644 --- a/pkg/fixedpoint/const.go +++ b/pkg/fixedpoint/const.go @@ -3,5 +3,5 @@ package fixedpoint var ( Two Value = NewFromInt(2) Three Value = NewFromInt(3) - Four Value = NewFromInt(3) + Four Value = NewFromInt(4) ) diff --git a/pkg/strategy/drift/strategy.go b/pkg/strategy/drift/strategy.go index 5d82174be..fb49f689b 100644 --- a/pkg/strategy/drift/strategy.go +++ b/pkg/strategy/drift/strategy.go @@ -39,6 +39,7 @@ func init() { type Strategy struct { Symbol string `json:"symbol"` + bbgo.OpenPositionOptions bbgo.StrategyController types.Market types.IntervalWindow @@ -90,10 +91,10 @@ type Strategy struct { TrailingCallbackRate []float64 `json:"trailingCallbackRate" modifiable:"true"` TrailingActivationRatio []float64 `json:"trailingActivationRatio" modifiable:"true"` - DriftFilterNeg float64 `json:"driftFilterNeg" modifiable:"true"` - DriftFilterPos float64 `json:"driftFilterPos" modifiable:"true"` - DDriftFilterNeg float64 `json:"ddriftFilterNeg" modifiable:"true"` - DDriftFilterPos float64 `json:"ddriftFilterPos" modifiable:"true"` + DriftFilterNeg float64 //`json:"driftFilterNeg" modifiable:"true"` + DriftFilterPos float64 //`json:"driftFilterPos" modifiable:"true"` + DDriftFilterNeg float64 //`json:"ddriftFilterNeg" modifiable:"true"` + DDriftFilterPos float64 //`json:"ddriftFilterPos" modifiable:"true"` buyPrice float64 `persistence:"buy_price"` sellPrice float64 `persistence:"sell_price"` @@ -147,10 +148,11 @@ func (s *Strategy) CurrentPosition() *types.Position { return s.Position } -func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error { +func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) bool { order := s.p.NewMarketCloseOrder(percentage) if order == nil { - return nil + s.positionLock.Unlock() + return false } order.Tag = "close" order.TimeInForce = "" @@ -165,16 +167,18 @@ func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Valu } else if order.Side == types.SideTypeSell && order.Quantity.Compare(baseBalance) > 0 { order.Quantity = baseBalance } + order.MarginSideEffect = types.SideEffectTypeAutoRepay + s.positionLock.Unlock() for { if s.Market.IsDustQuantity(order.Quantity, price) { - return nil + return false } _, err := s.GeneralOrderExecutor.SubmitOrders(ctx, *order) if err != nil { order.Quantity = order.Quantity.Mul(fixedpoint.One.Sub(Delta)) continue } - return nil + return true } } @@ -217,6 +221,7 @@ func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error { if !ok || klinesLength == 0 { return errors.New("klines not exists") } + log.Infof("loaded %d klines", klinesLength) for _, kline := range *klines { source := s.GetSource(&kline).Float64() high := kline.High.Float64() @@ -237,6 +242,7 @@ func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error { if !ok || klinesLength == 0 { return errors.New("klines not exists") } + log.Infof("loaded %d klines1m", klinesLength) for _, kline := range *klines { source := s.GetSource(&kline).Float64() s.drift1m.Update(source, kline.Volume.Abs().Float64()) @@ -377,9 +383,9 @@ func (s *Strategy) initTickerFunctions(ctx context.Context) { exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= pricef || s.trailingCheck(pricef, "long")) if exitShortCondition || exitLongCondition { - log.Infof("Close position by orderbook changes") - s.positionLock.Unlock() - _ = s.ClosePosition(ctx, fixedpoint.One) + if s.ClosePosition(ctx, fixedpoint.One) { + log.Infof("close position by orderbook changes") + } } else { s.positionLock.Unlock() } @@ -616,7 +622,6 @@ func (s *Strategy) klineHandler1m(ctx context.Context, kline types.KLine) { exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= lowf || s.trailingCheck(lowf, "long") /* || s.drift1m.Last() < 0*/) if exitShortCondition || exitLongCondition { - s.positionLock.Unlock() _ = s.ClosePosition(ctx, fixedpoint.One) } else { s.positionLock.Unlock() @@ -629,7 +634,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { s.frameKLine.Set(&kline) - source := s.GetSource(s.frameKLine) + source := s.GetSource(&kline) sourcef := source.Float64() s.priceLines.Update(sourcef) s.ma.Update(sourcef) @@ -664,7 +669,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { stoploss := s.StopLoss.Float64() s.positionLock.Lock() - log.Infof("highdiff: %3.2f ma: %.2f, close: %8v, high: %8v, low: %8v, time: %v %v", s.stdevHigh.Last(), s.ma.Last(), kline.Close, kline.High, kline.Low, kline.StartTime, kline.EndTime) + log.Infof("highdiff: %3.2f ma: %.2f, open: %8v, close: %8v, high: %8v, low: %8v, time: %v %v", s.stdevHigh.Last(), s.ma.Last(), kline.Open, kline.Close, kline.High, kline.Low, kline.StartTime, kline.EndTime) if s.lowestPrice > 0 && lowf < s.lowestPrice { s.lowestPrice = lowf } @@ -688,6 +693,18 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { s.Market.QuoteCurrency, balances[s.Market.QuoteCurrency].String(), ) + s.DriftFilterPos = s.drift.Filter(func(i int, v float64) bool { + return v >= 0 + }, 30).Mean(30) + s.DriftFilterNeg = s.drift.Filter(func(i int, v float64) bool { + return v <= 0 + }, 30).Mean(30) + s.DDriftFilterPos = s.drift.drift.Filter(func(i int, v float64) bool { + return v >= 0 + }, 30).Mean(30) + s.DDriftFilterNeg = s.drift.drift.Filter(func(i int, v float64) bool { + return v <= 0 + }, 30).Mean(30) shortCondition := (drift[1] >= s.DriftFilterNeg || ddrift[1] >= 0) && (driftPred <= s.DDriftFilterNeg || ddriftPred <= 0) || drift[1] < 0 && drift[0] < 0 longCondition := (drift[1] <= s.DriftFilterPos || ddrift[1] <= 0) && (driftPred >= s.DDriftFilterPos || ddriftPred >= 0) || drift[1] > 0 && drift[0] > 0 @@ -709,49 +726,43 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { s.positionLock.Unlock() return } - s.positionLock.Unlock() _ = s.ClosePosition(ctx, fixedpoint.One) } + if longCondition { if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { log.WithError(err).Errorf("cannot cancel orders") s.positionLock.Unlock() return } - source = source.Sub(fixedpoint.NewFromFloat(s.stdevLow.Last() * s.HighLowVarianceMultiplier)) + /*source = source.Sub(fixedpoint.NewFromFloat(s.stdevLow.Last() * s.HighLowVarianceMultiplier)) + if source.Compare(price) > 0 { + source = price + }*/ + source = fixedpoint.NewFromFloat(s.ma.Last() - s.stdevLow.Last()*s.HighLowVarianceMultiplier) if source.Compare(price) > 0 { source = price } sourcef = source.Float64() log.Infof("source in long %v %v %f", source, price, s.stdevLow.Last()) - quoteBalance, ok := s.Session.GetAccount().Balance(s.Market.QuoteCurrency) - if !ok { - log.Errorf("unable to get quoteCurrency") - s.positionLock.Unlock() - return - } - if s.Market.IsDustQuantity( - quoteBalance.Available.Div(source), source) { - s.positionLock.Unlock() - return - } s.positionLock.Unlock() - quantity := quoteBalance.Available.Div(source) - createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeBuy, - Type: types.OrderTypeLimit, - Price: source, - Quantity: quantity, - Tag: "long", - }) - log.Infof("orders %v", createdOrders) + opt := s.OpenPositionOptions + opt.Long = true + opt.Price = source + opt.Tags = []string{"long"} + createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt) if err != nil { + if _, ok := err.(types.ZeroAssetError); ok { + return + } log.WithError(err).Errorf("cannot place buy order") return } - s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + log.Infof("orders %v", createdOrders) + if createdOrders != nil { + s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + } return } if shortCondition { @@ -760,13 +771,11 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { s.positionLock.Unlock() return } - baseBalance, ok := s.Session.GetAccount().Balance(s.Market.BaseCurrency) - if !ok { - log.Errorf("unable to get baseBalance") - s.positionLock.Unlock() - return - } - source = source.Add(fixedpoint.NewFromFloat(s.stdevHigh.Last() * s.HighLowVarianceMultiplier)) + /*source = source.Add(fixedpoint.NewFromFloat(s.stdevHigh.Last() * s.HighLowVarianceMultiplier)) + if source.Compare(price) < 0 { + source = price + }*/ + source = fixedpoint.NewFromFloat(s.ma.Last() + s.stdevHigh.Last()*s.HighLowVarianceMultiplier) if source.Compare(price) < 0 { source = price } @@ -774,32 +783,32 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { log.Infof("source in short: %v", source) - if s.Market.IsDustQuantity(baseBalance.Available, source) { - s.positionLock.Unlock() - return - } s.positionLock.Unlock() - // Cleanup pending StopOrders - quantity := baseBalance.Available - createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: s.Symbol, - Side: types.SideTypeSell, - Type: types.OrderTypeLimit, - Price: source, - Quantity: quantity, - Tag: "short", - }) + opt := s.OpenPositionOptions + opt.Short = true + opt.Price = source + opt.Tags = []string{"long"} + createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt) if err != nil { - log.WithError(err).Errorf("cannot place sell order") + if _, ok := err.(types.ZeroAssetError); ok { + return + } + log.WithError(err).Errorf("cannot place buy order") return } - s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + log.Infof("orders %v", createdOrders) + if createdOrders != nil { + s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter + } return } s.positionLock.Unlock() } func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + if s.Leverage == fixedpoint.Zero { + s.Leverage = fixedpoint.One + } instanceID := s.InstanceID() // Will be set by persistence if there's any from DB if s.Position == nil { @@ -885,15 +894,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se s.highestPrice = 0 s.lowestPrice = 0 } else if s.p.IsLong() { - s.buyPrice = trade.Price.Float64() s.sellPrice = 0 - s.highestPrice = s.buyPrice s.lowestPrice = 0 } else { - s.sellPrice = trade.Price.Float64() s.buyPrice = 0 s.highestPrice = 0 - s.lowestPrice = s.sellPrice } } else if tag == "long" { if s.p.IsDust(trade.Price) { diff --git a/pkg/strategy/pivotshort/breaklow.go b/pkg/strategy/pivotshort/breaklow.go index 42070f339..91f9f0eed 100644 --- a/pkg/strategy/pivotshort/breaklow.go +++ b/pkg/strategy/pivotshort/breaklow.go @@ -230,7 +230,7 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener opts.Price = previousLow.Mul(fixedpoint.One.Add(s.BounceRatio)) } - if err := s.orderExecutor.OpenPosition(ctx, opts); err != nil { + if _, err := s.orderExecutor.OpenPosition(ctx, opts); err != nil { log.WithError(err).Errorf("failed to open short position") } })) diff --git a/pkg/strategy/pivotshort/failedbreakhigh.go b/pkg/strategy/pivotshort/failedbreakhigh.go index 28575f543..eb193e994 100644 --- a/pkg/strategy/pivotshort/failedbreakhigh.go +++ b/pkg/strategy/pivotshort/failedbreakhigh.go @@ -289,7 +289,7 @@ func (s *FailedBreakHigh) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg opts.Short = true opts.Price = closePrice opts.Tags = []string{"FailedBreakHighMarket"} - if err := s.orderExecutor.OpenPosition(ctx, opts); err != nil { + if _, err := s.orderExecutor.OpenPosition(ctx, opts); err != nil { log.WithError(err).Errorf("failed to open short position") } })) diff --git a/pkg/types/error.go b/pkg/types/error.go index 48ce67fac..5d58b57b8 100644 --- a/pkg/types/error.go +++ b/pkg/types/error.go @@ -21,3 +21,13 @@ func NewOrderError(e error, o Order) error { order: o, } } + +type ZeroAssetError struct { + error +} + +func NewZeroAssetError(e error) ZeroAssetError { + return ZeroAssetError{ + error: e, + } +} diff --git a/pkg/types/filter.go b/pkg/types/filter.go new file mode 100644 index 000000000..58e0a966e --- /dev/null +++ b/pkg/types/filter.go @@ -0,0 +1,51 @@ +package types + +type FilterResult struct { + a Series + b func(int, float64) bool + length int + c []int +} + +func (f *FilterResult) Last() float64 { + return f.Index(0) +} + +func (f *FilterResult) Index(j int) float64 { + if j >= f.length { + return 0 + } + if len(f.c) > j { + return f.a.Index(f.c[j]) + } + l := f.a.Length() + k := len(f.c) + i := 0 + if k > 0 { + i = f.c[k-1] + 1 + } + for ; i < l; i++ { + tmp := f.a.Index(i) + if f.b(i, tmp) { + f.c = append(f.c, i) + if j == k { + return tmp + } + k++ + } + } + return 0 +} + +func (f *FilterResult) Length() int { + return f.length +} + +// Filter function filters Series by using a boolean function. +// When the boolean function returns true, the Series value at index i will be included in the returned Series. +// The returned Series will find at most `length` latest matching elements from the input Series. +// Query index larger or equal than length from the returned Series will return 0 instead. +// Notice that any Update on the input Series will make the previously returned Series outdated. +func Filter(a Series, b func(i int, value float64) bool, length int) SeriesExtend { + return NewSeries(&FilterResult{a, b, length, nil}) +} diff --git a/pkg/types/indicator.go b/pkg/types/indicator.go index 4d78f129c..aed7330ac 100644 --- a/pkg/types/indicator.go +++ b/pkg/types/indicator.go @@ -112,6 +112,7 @@ type SeriesExtend interface { Softmax(window int) SeriesExtend Entropy(window int) float64 CrossEntropy(b Series, window int) float64 + Filter(b func(i int, value float64) bool, length int) SeriesExtend } type SeriesBase struct { diff --git a/pkg/types/indicator_test.go b/pkg/types/indicator_test.go index d14de03c6..7da2c17c5 100644 --- a/pkg/types/indicator_test.go +++ b/pkg/types/indicator_test.go @@ -170,3 +170,13 @@ func TestPlot(t *testing.T) { //defer f.Close() //ct.Render(chart.PNG, f) } + +func TestFilter(t *testing.T) { + a := floats.Slice{200., -200, 0, 1000, -100} + b := Filter(&a, func(i int, val float64) bool { + return val > 0 + }, 4) + assert.Equal(t, b.Length(), 4) + assert.Equal(t, b.Last(), 1000.) + assert.Equal(t, b.Sum(3), 1200.) +} diff --git a/pkg/types/seriesbase_imp.go b/pkg/types/seriesbase_imp.go index b952f7e5c..98329ad29 100644 --- a/pkg/types/seriesbase_imp.go +++ b/pkg/types/seriesbase_imp.go @@ -146,3 +146,7 @@ func (s *SeriesBase) Entropy(window int) float64 { func (s *SeriesBase) CrossEntropy(b Series, window int) float64 { return CrossEntropy(s, b, window) } + +func (s *SeriesBase) Filter(b func(int, float64) bool, length int) SeriesExtend { + return Filter(s, b, length) +}