Merge pull request #964 from zenixls2/fix/order_executor_general

a lot of fixes in api / const
This commit is contained in:
Yo-An Lin 2022-09-23 13:31:39 +08:00 committed by GitHub
commit 8efc948046
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
16 changed files with 331 additions and 169 deletions

View File

@ -25,35 +25,36 @@ exchangeStrategies:
drift: drift:
canvasPath: "./output.png" canvasPath: "./output.png"
symbol: ETHBUSD symbol: ETHBUSD
limitOrder: false
# kline interval for indicators # kline interval for indicators
interval: 15m interval: 2m
window: 2 window: 6
stoploss: 4.3% stoploss: 0.23%
source: close source: ohlc4
predictOffset: 2 predictOffset: 2
noTrailingStopLoss: true noTrailingStopLoss: false
trailingStopLossType: kline trailingStopLossType: kline
# stddev on high/low-source # stddev on high/low-source
hlVarianceMultiplier: 0.1 hlVarianceMultiplier: 0.03
hlRangeWindow: 5 hlRangeWindow: 4
window1m: 49
smootherWindow1m: 80
fisherTransformWindow1m: 74
smootherWindow: 3 smootherWindow: 3
fisherTransformWindow: 160 fisherTransformWindow: 117
window1m: 42
smootherWindow1m: 118
fisherTransformWindow1m: 319
atrWindow: 14 atrWindow: 14
# orders not been traded will be canceled after `pendingMinutes` minutes # orders not been traded will be canceled after `pendingMinutes` minutes
pendingMinutes: 10 pendingMinutes: 3
noRebalance: true noRebalance: true
trendWindow: 12 trendWindow: 12
rebalanceFilter: 1.5 rebalanceFilter: 2
trailingActivationRatio: [0.003] trailingActivationRatio: [0.0015, 0.002, 0.004, 0.01]
trailingCallbackRate: [0.0006] trailingCallbackRate: [0.0001, 0.00012, 0.001, 0.002]
driftFilterPos: 1.2 #driftFilterPos: 0.4
driftFilterNeg: -1.2 #driftFilterNeg: -0.42
ddriftFilterPos: 0.4 #ddriftFilterPos: 0
ddriftFilterNeg: -0.4 #ddriftFilterNeg: 0
generateGraph: true generateGraph: true
graphPNLDeductFee: true graphPNLDeductFee: true
@ -91,15 +92,15 @@ sync:
- ETHBUSD - ETHBUSD
backtest: backtest:
startTime: "2022-01-01" startTime: "2022-09-01"
endTime: "2022-08-30" endTime: "2022-09-30"
symbols: symbols:
- ETHBUSD - ETHBUSD
sessions: [binance] sessions: [binance]
accounts: accounts:
binance: binance:
makerFeeRate: 0.0000 makerFeeRate: 0.0000
#takerFeeRate: 0.00001 takerFeeRate: 0.0000
balances: balances:
ETH: 10 ETH: 0
BUSD: 5000.0 BUSD: 1000.0

View File

@ -8,7 +8,10 @@ persistence:
sessions: sessions:
binance: binance:
exchange: binance exchange: binance
futures: false #futures: true
#margin: true
#isolatedMargin: true
#isolatedMarginSymbol: BTCUSDT
envVarPrefix: binance envVarPrefix: binance
heikinAshi: false heikinAshi: false
@ -23,34 +26,35 @@ exchangeStrategies:
- on: binance - on: binance
drift: drift:
limitOrder: false
canvasPath: "./output.png" canvasPath: "./output.png"
symbol: BTCUSDT symbol: BTCUSDT
# kline interval for indicators # kline interval for indicators
interval: 4m interval: 4m
window: 1 window: 2
stoploss: 0.22% stoploss: 0.13%
source: hl2 source: ohlc4
predictOffset: 2 predictOffset: 2
noTrailingStopLoss: false noTrailingStopLoss: false
trailingStopLossType: realtime trailingStopLossType: kline
# stddev on high/low-source # stddev on high/low-source
hlVarianceMultiplier: 0.01 hlVarianceMultiplier: 0.22
hlRangeWindow: 5 hlRangeWindow: 4
smootherWindow: 2 smootherWindow: 1
fisherTransformWindow: 27 fisherTransformWindow: 96
window1m: 58 window1m: 8
smootherWindow1m: 118 smootherWindow1m: 4
fisherTransformWindow1m: 319 fisherTransformWindow1m: 320
atrWindow: 14 atrWindow: 14
# orders not been traded will be canceled after `pendingMinutes` minutes # orders not been traded will be canceled after `pendingMinutes` minutes
pendingMinutes: 2 pendingMinutes: 10
noRebalance: true noRebalance: true
trendWindow: 576 trendWindow: 576
rebalanceFilter: 0 rebalanceFilter: 1.2
driftFilterPos: 0.6 #driftFilterPos: 0.5
driftFilterNeg: -0.6 #driftFilterNeg: -0.5
ddriftFilterPos: 0.00008 #ddriftFilterPos: 0.0008
ddriftFilterNeg: -0.00008 #ddriftFilterNeg: -0.0008
# ActivationRatio should be increasing order # ActivationRatio should be increasing order
# when farest price from entry goes over that ratio, start using the callback ratio accordingly to do trailingstop # 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 takerFeeRate: 0.000
balances: balances:
BTC: 0 BTC: 0
USDT: 21 USDT: 1000

View File

@ -197,17 +197,14 @@ type OpenPositionOptions struct {
// Leverage is used for leveraged position and account // Leverage is used for leveraged position and account
// Leverage is not effected when using non-leverage spot 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 will be used first, it will override the leverage if it's given
Quantity fixedpoint.Value `json:"quantity,omitempty"` 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 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. // 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. // 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:"-"` 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 price := options.Price
submitOrder := types.SubmitOrder{ submitOrder := types.SubmitOrder{
Symbol: e.position.Symbol, Symbol: e.position.Symbol,
@ -246,9 +261,7 @@ func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPos
} }
} }
if options.MarketOrder { if options.LimitOrder {
submitOrder.Type = types.OrderTypeMarket
} else if options.LimitOrder {
submitOrder.Type = types.OrderTypeLimit submitOrder.Type = types.OrderTypeLimit
submitOrder.Price = price submitOrder.Price = price
} }
@ -259,11 +272,15 @@ func (e *GeneralOrderExecutor) OpenPosition(ctx context.Context, options OpenPos
if quantity.IsZero() { if quantity.IsZero() {
quoteQuantity, err := CalculateQuoteQuantity(ctx, e.session, e.position.QuoteCurrency, options.Leverage) quoteQuantity, err := CalculateQuoteQuantity(ctx, e.session, e.position.QuoteCurrency, options.Leverage)
if err != nil { if err != nil {
return err return nil, err
} }
quantity = quoteQuantity.Div(price) quantity = quoteQuantity.Div(price)
} }
if e.position.Market.IsDustQuantity(quantity, price) {
log.Warnf("dust quantity: %v", quantity)
return nil, nil
}
quoteQuantity := quantity.Mul(price) quoteQuantity := quantity.Mul(price)
if e.session.Margin && !e.marginQuoteMaxBorrowable.IsZero() && quoteQuantity.Compare(e.marginQuoteMaxBorrowable) > 0 { 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.Side = types.SideTypeBuy
submitOrder.Quantity = quantity submitOrder.Quantity = quantity
Notify("Opening %s long position with quantity %f at price %f", e.position.Symbol, quantity.Float64(), price.Float64()) Notify("Opening %s long position with quantity %v at price %v", e.position.Symbol, quantity, price)
createdOrder, err2 := e.SubmitOrders(ctx, submitOrder) return e.reduceQuantityAndSubmitOrder(ctx, price, submitOrder)
if err2 != nil {
return err2
}
log.Infof("created order: %+v", createdOrder)
return nil
} else if options.Short { } else if options.Short {
if quantity.IsZero() { if quantity.IsZero() {
var err error var err error
quantity, err = CalculateBaseQuantity(e.session, e.position.Market, price, quantity, options.Leverage) quantity, err = CalculateBaseQuantity(e.session, e.position.Market, price, quantity, options.Leverage)
if err != nil { 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 { 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()) 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.Side = types.SideTypeSell
submitOrder.Quantity = quantity submitOrder.Quantity = quantity
Notify("Opening %s short position with quantity %f at price %f", e.position.Symbol, quantity.Float64(), price.Float64()) Notify("Opening %s short position with quantity %v at price %v", e.position.Symbol, quantity, price)
createdOrder, err2 := e.SubmitOrders(ctx, submitOrder) return e.reduceQuantityAndSubmitOrder(ctx, price, submitOrder)
if err2 != nil {
return err2
} }
log.Infof("created order: %+v", createdOrder) return nil, errors.New("options Long or Short must be set")
return nil
}
return errors.New("options Long or Short must be set")
} }
// GracefulCancelActiveOrderBook cancels the orders from the active orderbook. // GracefulCancelActiveOrderBook cancels the orders from the active orderbook.

View File

@ -5,6 +5,7 @@ import (
"fmt" "fmt"
"time" "time"
"github.com/pkg/errors"
log "github.com/sirupsen/logrus" log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/fixedpoint" "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) usdBalances, restBalances := usdFiatBalances(balances)
@ -329,7 +331,8 @@ func CalculateBaseQuantity(session *ExchangeSession, market types.Market, price,
return maxPositionQuantity, nil 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) { func CalculateQuoteQuantity(ctx context.Context, session *ExchangeSession, quoteCurrency string, leverage fixedpoint.Value) (fixedpoint.Value, error) {

View File

@ -6,7 +6,7 @@ import (
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
) )
const Delta = 1e-9 const delta = 1e-9
func TestExponentialScale(t *testing.T) { func TestExponentialScale(t *testing.T) {
// graph see: https://www.desmos.com/calculator/ip0ijbcbbf // graph see: https://www.desmos.com/calculator/ip0ijbcbbf
@ -19,8 +19,8 @@ func TestExponentialScale(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "f(x) = 0.001000 * 1.002305 ^ (x - 1000.000000)", scale.String()) 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.001, scale.Call(1000.0), delta)
assert.InDelta(t, 0.01, scale.Call(2000.0), Delta) assert.InDelta(t, 0.01, scale.Call(2000.0), delta)
for x := 1000; x <= 2000; x += 100 { for x := 1000; x <= 2000; x += 100 {
y := scale.Call(float64(x)) y := scale.Call(float64(x))
@ -38,8 +38,8 @@ func TestExponentialScale_Reverse(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "f(x) = 0.100000 * 0.995405 ^ (x - 1000.000000)", scale.String()) 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.1, scale.Call(1000.0), delta)
assert.InDelta(t, 0.001, scale.Call(2000.0), Delta) assert.InDelta(t, 0.001, scale.Call(2000.0), delta)
for x := 1000; x <= 2000; x += 100 { for x := 1000; x <= 2000; x += 100 {
y := scale.Call(float64(x)) y := scale.Call(float64(x))
@ -57,8 +57,8 @@ func TestLogScale(t *testing.T) {
err := scale.Solve() err := scale.Solve()
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "f(x) = 0.001303 * log(x - 999.000000) + 0.001000", scale.String()) 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.001, scale.Call(1000.0), delta)
assert.InDelta(t, 0.01, scale.Call(2000.0), Delta) assert.InDelta(t, 0.01, scale.Call(2000.0), delta)
for x := 1000; x <= 2000; x += 100 { for x := 1000; x <= 2000; x += 100 {
y := scale.Call(float64(x)) y := scale.Call(float64(x))
t.Logf("%s = %f", scale.FormulaOf(float64(x)), y) t.Logf("%s = %f", scale.FormulaOf(float64(x)), y)
@ -74,8 +74,8 @@ func TestLinearScale(t *testing.T) {
err := scale.Solve() err := scale.Solve()
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "f(x) = 0.007000 * x + -4.000000", scale.String()) assert.Equal(t, "f(x) = 0.007000 * x + -4.000000", scale.String())
assert.InDelta(t, 3, scale.Call(1000), Delta) assert.InDelta(t, 3, scale.Call(1000), delta)
assert.InDelta(t, 10, scale.Call(2000), Delta) assert.InDelta(t, 10, scale.Call(2000), delta)
for x := 1000; x <= 2000; x += 100 { for x := 1000; x <= 2000; x += 100 {
y := scale.Call(float64(x)) y := scale.Call(float64(x))
t.Logf("%s = %f", scale.FormulaOf(float64(x)), y) t.Logf("%s = %f", scale.FormulaOf(float64(x)), y)
@ -91,8 +91,8 @@ func TestLinearScale2(t *testing.T) {
err := scale.Solve() err := scale.Solve()
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "f(x) = 0.150000 * x + -0.050000", scale.String()) 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.1, scale.Call(1), delta)
assert.InDelta(t, 0.4, scale.Call(3), Delta) assert.InDelta(t, 0.4, scale.Call(3), delta)
} }
func TestQuadraticScale(t *testing.T) { func TestQuadraticScale(t *testing.T) {
@ -105,9 +105,9 @@ func TestQuadraticScale(t *testing.T) {
err := scale.Solve() err := scale.Solve()
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, "f(x) = 0.000550 * x ^ 2 + 0.135000 * x + 1.000000", scale.String()) 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, 1, scale.Call(0), delta)
assert.InDelta(t, 20, scale.Call(100.0), Delta) assert.InDelta(t, 20, scale.Call(100.0), delta)
assert.InDelta(t, 50.0, scale.Call(200.0), Delta) assert.InDelta(t, 50.0, scale.Call(200.0), delta)
for x := 0; x <= 200; x += 1 { for x := 0; x <= 200; x += 1 {
y := scale.Call(float64(x)) y := scale.Call(float64(x))
t.Logf("%s = %f", scale.FormulaOf(float64(x)), y) t.Logf("%s = %f", scale.FormulaOf(float64(x)), y)
@ -127,11 +127,11 @@ func TestPercentageScale(t *testing.T) {
v, err := s.Scale(0.0) v, err := s.Scale(0.0)
assert.NoError(t, err) assert.NoError(t, err)
assert.InDelta(t, 1.0, v, Delta) assert.InDelta(t, 1.0, v, delta)
v, err = s.Scale(1.0) v, err = s.Scale(1.0)
assert.NoError(t, err) 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) { 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) v, err := s.Scale(-1.0)
assert.NoError(t, err) assert.NoError(t, err)
assert.InDelta(t, 10.0, v, Delta) assert.InDelta(t, 10.0, v, delta)
v, err = s.Scale(1.0) v, err = s.Scale(1.0)
assert.NoError(t, err) 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) { 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) v, err := s.Scale(-1.0)
assert.NoError(t, err) assert.NoError(t, err)
assert.InDelta(t, 100.0, v, Delta) assert.InDelta(t, 100.0, v, delta)
v, err = s.Scale(1.0) v, err = s.Scale(1.0)
assert.NoError(t, err) assert.NoError(t, err)
assert.InDelta(t, 10.0, v, Delta) assert.InDelta(t, 10.0, v, delta)
v, err = s.Scale(2.0) v, err = s.Scale(2.0)
assert.NoError(t, err) assert.NoError(t, err)
assert.InDelta(t, 10.0, v, Delta) assert.InDelta(t, 10.0, v, delta)
v, err = s.Scale(-2.0) v, err = s.Scale(-2.0)
assert.NoError(t, err) 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) { t.Run("negative range", func(t *testing.T) {
@ -192,10 +192,10 @@ func TestPercentageScale(t *testing.T) {
v, err := s.Scale(0.0) v, err := s.Scale(0.0)
assert.NoError(t, err) assert.NoError(t, err)
assert.InDelta(t, -100.0, v, Delta) assert.InDelta(t, -100.0, v, delta)
v, err = s.Scale(1.0) v, err = s.Scale(1.0)
assert.NoError(t, err) assert.NoError(t, err)
assert.InDelta(t, 100.0, v, Delta) assert.InDelta(t, 100.0, v, delta)
}) })
} }

View File

@ -1,6 +1,7 @@
package dynamic package dynamic
import ( import (
"errors"
"reflect" "reflect"
"strings" "strings"
) )
@ -29,11 +30,21 @@ func LookupSymbolField(rs reflect.Value) (string, bool) {
// Used by bbgo/interact_modify.go // Used by bbgo/interact_modify.go
func GetModifiableFields(val reflect.Value, callback func(tagName, name string)) { 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) t := val.Type().Field(i)
if !t.IsExported() { if !t.IsExported() {
continue continue
} }
if t.Anonymous {
GetModifiableFields(val.Field(i), callback)
}
modifiable := t.Tag.Get("modifiable") modifiable := t.Tag.Get("modifiable")
if modifiable != "true" { if modifiable != "true" {
continue continue
@ -50,6 +61,17 @@ func GetModifiableFields(val reflect.Value, callback func(tagName, name string))
var zeroValue reflect.Value = reflect.Zero(reflect.TypeOf(0)) var zeroValue reflect.Value = reflect.Zero(reflect.TypeOf(0))
func GetModifiableField(val reflect.Value, name string) (reflect.Value, bool) { 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) field, ok := val.Type().FieldByName(name)
if !ok { if !ok {
return zeroValue, ok return zeroValue, ok
@ -61,5 +83,31 @@ func GetModifiableField(val reflect.Value, name string) (reflect.Value, bool) {
if jsonTag == "" || jsonTag == "-" { if jsonTag == "" || jsonTag == "-" {
return zeroValue, false 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
} }

View File

@ -9,7 +9,17 @@ import (
"github.com/stretchr/testify/assert" "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 { type Strategy struct {
Inner
*InnerPointer
Field1 fixedpoint.Value `json:"field1" modifiable:"true"` Field1 fixedpoint.Value `json:"field1" modifiable:"true"`
Field2 float64 `json:"field2"` Field2 float64 `json:"field2"`
field3 float64 `json:"field3" modifiable:"true"` field3 float64 `json:"field3" modifiable:"true"`
@ -24,7 +34,6 @@ func TestGetModifiableFields(t *testing.T) {
assert.NotEqual(t, name, "Field2") assert.NotEqual(t, name, "Field2")
assert.NotEqual(t, tagName, "field3") assert.NotEqual(t, tagName, "field3")
assert.NotEqual(t, name, "Field3") assert.NotEqual(t, name, "Field3")
}) })
} }
@ -34,6 +43,13 @@ func TestGetModifiableField(t *testing.T) {
val := reflect.ValueOf(s).Elem() val := reflect.ValueOf(s).Elem()
_, ok := GetModifiableField(val, "Field1") _, ok := GetModifiableField(val, "Field1")
assert.True(t, ok) 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") _, ok = GetModifiableField(val, "Field2")
assert.False(t, ok) assert.False(t, ok)
_, ok = GetModifiableField(val, "Field3") _, ok = GetModifiableField(val, "Field3")

View File

@ -3,5 +3,5 @@ package fixedpoint
var ( var (
Two Value = NewFromInt(2) Two Value = NewFromInt(2)
Three Value = NewFromInt(3) Three Value = NewFromInt(3)
Four Value = NewFromInt(3) Four Value = NewFromInt(4)
) )

View File

@ -39,6 +39,7 @@ func init() {
type Strategy struct { type Strategy struct {
Symbol string `json:"symbol"` Symbol string `json:"symbol"`
bbgo.OpenPositionOptions
bbgo.StrategyController bbgo.StrategyController
types.Market types.Market
types.IntervalWindow types.IntervalWindow
@ -90,10 +91,10 @@ type Strategy struct {
TrailingCallbackRate []float64 `json:"trailingCallbackRate" modifiable:"true"` TrailingCallbackRate []float64 `json:"trailingCallbackRate" modifiable:"true"`
TrailingActivationRatio []float64 `json:"trailingActivationRatio" modifiable:"true"` TrailingActivationRatio []float64 `json:"trailingActivationRatio" modifiable:"true"`
DriftFilterNeg float64 `json:"driftFilterNeg" modifiable:"true"` DriftFilterNeg float64 //`json:"driftFilterNeg" modifiable:"true"`
DriftFilterPos float64 `json:"driftFilterPos" modifiable:"true"` DriftFilterPos float64 //`json:"driftFilterPos" modifiable:"true"`
DDriftFilterNeg float64 `json:"ddriftFilterNeg" modifiable:"true"` DDriftFilterNeg float64 //`json:"ddriftFilterNeg" modifiable:"true"`
DDriftFilterPos float64 `json:"ddriftFilterPos" modifiable:"true"` DDriftFilterPos float64 //`json:"ddriftFilterPos" modifiable:"true"`
buyPrice float64 `persistence:"buy_price"` buyPrice float64 `persistence:"buy_price"`
sellPrice float64 `persistence:"sell_price"` sellPrice float64 `persistence:"sell_price"`
@ -147,10 +148,11 @@ func (s *Strategy) CurrentPosition() *types.Position {
return s.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) order := s.p.NewMarketCloseOrder(percentage)
if order == nil { if order == nil {
return nil s.positionLock.Unlock()
return false
} }
order.Tag = "close" order.Tag = "close"
order.TimeInForce = "" 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 { } else if order.Side == types.SideTypeSell && order.Quantity.Compare(baseBalance) > 0 {
order.Quantity = baseBalance order.Quantity = baseBalance
} }
order.MarginSideEffect = types.SideEffectTypeAutoRepay
s.positionLock.Unlock()
for { for {
if s.Market.IsDustQuantity(order.Quantity, price) { if s.Market.IsDustQuantity(order.Quantity, price) {
return nil return false
} }
_, err := s.GeneralOrderExecutor.SubmitOrders(ctx, *order) _, err := s.GeneralOrderExecutor.SubmitOrders(ctx, *order)
if err != nil { if err != nil {
order.Quantity = order.Quantity.Mul(fixedpoint.One.Sub(Delta)) order.Quantity = order.Quantity.Mul(fixedpoint.One.Sub(Delta))
continue continue
} }
return nil return true
} }
} }
@ -217,6 +221,7 @@ func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error {
if !ok || klinesLength == 0 { if !ok || klinesLength == 0 {
return errors.New("klines not exists") return errors.New("klines not exists")
} }
log.Infof("loaded %d klines", klinesLength)
for _, kline := range *klines { for _, kline := range *klines {
source := s.GetSource(&kline).Float64() source := s.GetSource(&kline).Float64()
high := kline.High.Float64() high := kline.High.Float64()
@ -237,6 +242,7 @@ func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error {
if !ok || klinesLength == 0 { if !ok || klinesLength == 0 {
return errors.New("klines not exists") return errors.New("klines not exists")
} }
log.Infof("loaded %d klines1m", klinesLength)
for _, kline := range *klines { for _, kline := range *klines {
source := s.GetSource(&kline).Float64() source := s.GetSource(&kline).Float64()
s.drift1m.Update(source, kline.Volume.Abs().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 || exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= pricef ||
s.trailingCheck(pricef, "long")) s.trailingCheck(pricef, "long"))
if exitShortCondition || exitLongCondition { if exitShortCondition || exitLongCondition {
log.Infof("Close position by orderbook changes") if s.ClosePosition(ctx, fixedpoint.One) {
s.positionLock.Unlock() log.Infof("close position by orderbook changes")
_ = s.ClosePosition(ctx, fixedpoint.One) }
} else { } else {
s.positionLock.Unlock() 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 || exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= lowf ||
s.trailingCheck(lowf, "long") /* || s.drift1m.Last() < 0*/) s.trailingCheck(lowf, "long") /* || s.drift1m.Last() < 0*/)
if exitShortCondition || exitLongCondition { if exitShortCondition || exitLongCondition {
s.positionLock.Unlock()
_ = s.ClosePosition(ctx, fixedpoint.One) _ = s.ClosePosition(ctx, fixedpoint.One)
} else { } else {
s.positionLock.Unlock() s.positionLock.Unlock()
@ -629,7 +634,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
s.frameKLine.Set(&kline) s.frameKLine.Set(&kline)
source := s.GetSource(s.frameKLine) source := s.GetSource(&kline)
sourcef := source.Float64() sourcef := source.Float64()
s.priceLines.Update(sourcef) s.priceLines.Update(sourcef)
s.ma.Update(sourcef) s.ma.Update(sourcef)
@ -664,7 +669,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
stoploss := s.StopLoss.Float64() stoploss := s.StopLoss.Float64()
s.positionLock.Lock() 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 { if s.lowestPrice > 0 && lowf < s.lowestPrice {
s.lowestPrice = lowf s.lowestPrice = lowf
} }
@ -688,6 +693,18 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
s.Market.QuoteCurrency, s.Market.QuoteCurrency,
balances[s.Market.QuoteCurrency].String(), 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 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 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() s.positionLock.Unlock()
return return
} }
s.positionLock.Unlock()
_ = s.ClosePosition(ctx, fixedpoint.One) _ = s.ClosePosition(ctx, fixedpoint.One)
} }
if longCondition { if longCondition {
if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil {
log.WithError(err).Errorf("cannot cancel orders") log.WithError(err).Errorf("cannot cancel orders")
s.positionLock.Unlock() s.positionLock.Unlock()
return 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 { if source.Compare(price) > 0 {
source = price source = price
} }
sourcef = source.Float64() sourcef = source.Float64()
log.Infof("source in long %v %v %f", source, price, s.stdevLow.Last()) 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() s.positionLock.Unlock()
return opt := s.OpenPositionOptions
} opt.Long = true
if s.Market.IsDustQuantity( opt.Price = source
quoteBalance.Available.Div(source), source) { opt.Tags = []string{"long"}
s.positionLock.Unlock() createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt)
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)
if err != nil { if err != nil {
if _, ok := err.(types.ZeroAssetError); ok {
return
}
log.WithError(err).Errorf("cannot place buy order") log.WithError(err).Errorf("cannot place buy order")
return return
} }
log.Infof("orders %v", createdOrders)
if createdOrders != nil {
s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter
}
return return
} }
if shortCondition { if shortCondition {
@ -760,13 +771,11 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
s.positionLock.Unlock() s.positionLock.Unlock()
return return
} }
baseBalance, ok := s.Session.GetAccount().Balance(s.Market.BaseCurrency) /*source = source.Add(fixedpoint.NewFromFloat(s.stdevHigh.Last() * s.HighLowVarianceMultiplier))
if !ok { if source.Compare(price) < 0 {
log.Errorf("unable to get baseBalance") source = price
s.positionLock.Unlock() }*/
return source = fixedpoint.NewFromFloat(s.ma.Last() + s.stdevHigh.Last()*s.HighLowVarianceMultiplier)
}
source = source.Add(fixedpoint.NewFromFloat(s.stdevHigh.Last() * s.HighLowVarianceMultiplier))
if source.Compare(price) < 0 { if source.Compare(price) < 0 {
source = price source = price
} }
@ -774,32 +783,32 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
log.Infof("source in short: %v", source) log.Infof("source in short: %v", source)
if s.Market.IsDustQuantity(baseBalance.Available, source) {
s.positionLock.Unlock() s.positionLock.Unlock()
return opt := s.OpenPositionOptions
} opt.Short = true
s.positionLock.Unlock() opt.Price = source
// Cleanup pending StopOrders opt.Tags = []string{"long"}
quantity := baseBalance.Available createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt)
createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeLimit,
Price: source,
Quantity: quantity,
Tag: "short",
})
if err != nil { if err != nil {
log.WithError(err).Errorf("cannot place sell order") if _, ok := err.(types.ZeroAssetError); ok {
return return
} }
log.WithError(err).Errorf("cannot place buy order")
return
}
log.Infof("orders %v", createdOrders)
if createdOrders != nil {
s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter
}
return return
} }
s.positionLock.Unlock() s.positionLock.Unlock()
} }
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { 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() instanceID := s.InstanceID()
// Will be set by persistence if there's any from DB // Will be set by persistence if there's any from DB
if s.Position == nil { if s.Position == nil {
@ -885,15 +894,11 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.highestPrice = 0 s.highestPrice = 0
s.lowestPrice = 0 s.lowestPrice = 0
} else if s.p.IsLong() { } else if s.p.IsLong() {
s.buyPrice = trade.Price.Float64()
s.sellPrice = 0 s.sellPrice = 0
s.highestPrice = s.buyPrice
s.lowestPrice = 0 s.lowestPrice = 0
} else { } else {
s.sellPrice = trade.Price.Float64()
s.buyPrice = 0 s.buyPrice = 0
s.highestPrice = 0 s.highestPrice = 0
s.lowestPrice = s.sellPrice
} }
} else if tag == "long" { } else if tag == "long" {
if s.p.IsDust(trade.Price) { if s.p.IsDust(trade.Price) {

View File

@ -230,7 +230,7 @@ func (s *BreakLow) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.Gener
opts.Price = previousLow.Mul(fixedpoint.One.Add(s.BounceRatio)) 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") log.WithError(err).Errorf("failed to open short position")
} }
})) }))

View File

@ -289,7 +289,7 @@ func (s *FailedBreakHigh) Bind(session *bbgo.ExchangeSession, orderExecutor *bbg
opts.Short = true opts.Short = true
opts.Price = closePrice opts.Price = closePrice
opts.Tags = []string{"FailedBreakHighMarket"} 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") log.WithError(err).Errorf("failed to open short position")
} }
})) }))

View File

@ -21,3 +21,13 @@ func NewOrderError(e error, o Order) error {
order: o, order: o,
} }
} }
type ZeroAssetError struct {
error
}
func NewZeroAssetError(e error) ZeroAssetError {
return ZeroAssetError{
error: e,
}
}

51
pkg/types/filter.go Normal file
View File

@ -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})
}

View File

@ -112,6 +112,7 @@ type SeriesExtend interface {
Softmax(window int) SeriesExtend Softmax(window int) SeriesExtend
Entropy(window int) float64 Entropy(window int) float64
CrossEntropy(b Series, window int) float64 CrossEntropy(b Series, window int) float64
Filter(b func(i int, value float64) bool, length int) SeriesExtend
} }
type SeriesBase struct { type SeriesBase struct {

View File

@ -170,3 +170,13 @@ func TestPlot(t *testing.T) {
//defer f.Close() //defer f.Close()
//ct.Render(chart.PNG, f) //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.)
}

View File

@ -146,3 +146,7 @@ func (s *SeriesBase) Entropy(window int) float64 {
func (s *SeriesBase) CrossEntropy(b Series, window int) float64 { func (s *SeriesBase) CrossEntropy(b Series, window int) float64 {
return CrossEntropy(s, b, window) return CrossEntropy(s, b, window)
} }
func (s *SeriesBase) Filter(b func(int, float64) bool, length int) SeriesExtend {
return Filter(s, b, length)
}