fix: drift bias on long entry position condition, make cancel faster

This commit is contained in:
zenix 2022-07-14 12:46:19 +09:00
parent 55704fdd21
commit f2d37650a5
5 changed files with 153 additions and 110 deletions

View File

@ -16,6 +16,8 @@ exchangeStrategies:
interval: 15m
window: 3
stoploss: 2%
source: hl2
predictOffset: 5
#exits:
#- roiStopLoss:
# percentage: 0.8%
@ -48,7 +50,7 @@ sync:
- ETHUSDT
backtest:
startTime: "2022-04-01"
startTime: "2022-01-01"
endTime: "2022-06-18"
symbols:
- ETHUSDT

View File

@ -114,6 +114,9 @@ func (e *GeneralOrderExecutor) SubmitOrders(ctx context.Context, submitOrders ..
// GracefulCancelActiveOrderBook cancels the orders from the active orderbook.
func (e *GeneralOrderExecutor) GracefulCancelActiveOrderBook(ctx context.Context, activeOrders *ActiveOrderBook) error {
if activeOrders.NumOfOrders() == 0 {
return nil
}
if err := activeOrders.GracefulCancel(ctx, e.session.Exchange); err != nil {
return fmt.Errorf("graceful cancel order error: %w", err)
}

View File

@ -15,7 +15,7 @@ type Drift struct {
types.IntervalWindow
chng *types.Queue
Values types.Float64Slice
SMA *SMA
MA types.UpdatableSeriesExtend
LastValue float64
UpdateCallbacks []func(value float64)
@ -24,7 +24,9 @@ type Drift struct {
func (inc *Drift) Update(value float64) {
if inc.chng == nil {
inc.SeriesBase.Series = inc
inc.SMA = &SMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: inc.Window}}
if inc.MA == nil {
inc.MA = &SMA{IntervalWindow: types.IntervalWindow{Interval: inc.Interval, Window: inc.Window}}
}
inc.chng = types.NewQueue(inc.Window)
inc.LastValue = value
return
@ -36,11 +38,11 @@ func (inc *Drift) Update(value float64) {
chng = math.Log(value / inc.LastValue)
inc.LastValue = value
}
inc.SMA.Update(chng)
inc.MA.Update(chng)
inc.chng.Update(chng)
if inc.chng.Length() >= inc.Window {
stdev := types.Stdev(inc.chng, inc.Window)
drift := inc.SMA.Last() - stdev*stdev*0.5
drift := inc.MA.Last() - stdev*stdev*0.5
inc.Values.Push(drift)
}
}
@ -50,7 +52,7 @@ func (inc *Drift) Clone() (out *Drift) {
IntervalWindow: inc.IntervalWindow,
chng: inc.chng.Clone(),
Values: inc.Values[:],
SMA: inc.SMA.Clone().(*SMA),
MA: types.Clone(inc.MA),
LastValue: inc.LastValue,
}
out.SeriesBase.Series = out

View File

@ -2,10 +2,14 @@ package drift
import (
"context"
"encoding/json"
"fmt"
"math"
"os"
"strings"
"sync"
"github.com/fatih/color"
"github.com/sirupsen/logrus"
"github.com/wcharczuk/go-chart/v2"
@ -19,11 +23,17 @@ import (
const ID = "drift"
var log = logrus.WithField("strategy", ID)
var Four fixedpoint.Value = fixedpoint.NewFromInt(4)
var Three fixedpoint.Value = fixedpoint.NewFromInt(3)
var Two fixedpoint.Value = fixedpoint.NewFromInt(2)
var Delta fixedpoint.Value = fixedpoint.NewFromFloat(0.01)
func init() {
bbgo.RegisterStrategy(ID, &Strategy{})
}
type SourceFunc func(*types.KLine) fixedpoint.Value
type Strategy struct {
Symbol string `json:"symbol"`
@ -41,13 +51,30 @@ type Strategy struct {
midPrice fixedpoint.Value
lock sync.RWMutex
Stoploss fixedpoint.Value `json:"stoploss"`
CanvasPath string `json:"canvasPath"`
Source string `json:"source"`
Stoploss fixedpoint.Value `json:"stoploss"`
CanvasPath string `json:"canvasPath"`
PredictOffset int `json:"predictOffset"`
ExitMethods bbgo.ExitMethodSet `json:"exits"`
Session *bbgo.ExchangeSession
*bbgo.GeneralOrderExecutor
*bbgo.ActiveOrderBook
getLastPrice func() fixedpoint.Value
}
func (s *Strategy) Print() {
b, _ := json.MarshalIndent(s.ExitMethods, " ", " ")
hiyellow := color.New(color.FgHiYellow).FprintfFunc()
hiyellow(os.Stderr, "------ %s Settings ------\n", s.InstanceID())
hiyellow(os.Stderr, "canvasPath: %s\n", s.CanvasPath)
hiyellow(os.Stderr, "source: %s\n", s.Source)
hiyellow(os.Stderr, "stoploss: %v\n", s.Stoploss)
hiyellow(os.Stderr, "predictOffset: %d\n", s.PredictOffset)
hiyellow(os.Stderr, "exits:\n %s\n", string(b))
hiyellow(os.Stderr, "symbol: %s\n", s.Symbol)
hiyellow(os.Stderr, "interval: %s\n", s.Interval)
hiyellow(os.Stderr, "window: %d\n", s.Window)
}
func (s *Strategy) ID() string {
@ -72,35 +99,6 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
s.ExitMethods.SetAndSubscribe(session, s)
}
var Three fixedpoint.Value = fixedpoint.NewFromInt(3)
var Two fixedpoint.Value = fixedpoint.NewFromInt(2)
func (s *Strategy) GetLastPrice() (lastPrice fixedpoint.Value) {
var ok bool
if s.Environment.IsBackTesting() {
lastPrice, ok = s.Session.LastPrice(s.Symbol)
if !ok {
log.Error("cannot get lastprice")
return lastPrice
}
} else {
s.lock.RLock()
if s.midPrice.IsZero() {
lastPrice, ok = s.Session.LastPrice(s.Symbol)
if !ok {
log.Error("cannot get lastprice")
return lastPrice
}
} else {
lastPrice = s.midPrice
}
s.lock.RUnlock()
}
return lastPrice
}
var Delta fixedpoint.Value = fixedpoint.NewFromFloat(0.01)
func (s *Strategy) ClosePosition(ctx context.Context) (*types.Order, bool) {
order := s.Position.NewMarketCloseOrder(fixedpoint.One)
if order == nil {
@ -109,7 +107,7 @@ func (s *Strategy) ClosePosition(ctx context.Context) (*types.Order, bool) {
order.TimeInForce = ""
balances := s.Session.GetAccount().Balances()
baseBalance := balances[s.Market.BaseCurrency].Available
price := s.GetLastPrice()
price := s.getLastPrice()
if order.Side == types.SideTypeBuy {
quoteAmount := balances[s.Market.QuoteCurrency].Available.Div(price)
if order.Quantity.Compare(quoteAmount) > 0 {
@ -131,6 +129,38 @@ func (s *Strategy) ClosePosition(ctx context.Context) (*types.Order, bool) {
}
}
func (s *Strategy) SourceFuncGenerator() SourceFunc {
switch strings.ToLower(s.Source) {
case "close":
return func(kline *types.KLine) fixedpoint.Value { return kline.Close }
case "high":
return func(kline *types.KLine) fixedpoint.Value { return kline.High }
case "low":
return func(kline *types.KLine) fixedpoint.Value { return kline.Low }
case "hl2":
return func(kline *types.KLine) fixedpoint.Value {
return kline.High.Add(kline.Low).Div(Two)
}
case "hlc3":
return func(kline *types.KLine) fixedpoint.Value {
return kline.High.Add(kline.Low).Add(kline.Close).Div(Three)
}
case "ohlc4":
return func(kline *types.KLine) fixedpoint.Value {
return kline.Open.Add(kline.High).Add(kline.Low).Add(kline.Close).Div(Four)
}
case "open":
return func(kline *types.KLine) fixedpoint.Value { return kline.Open }
case "":
return func(kline *types.KLine) fixedpoint.Value {
log.Infof("source not set, use hl2 by default")
return kline.High.Add(kline.Low).Div(Two)
}
default:
panic(fmt.Sprintf("Unable to parse: %s", s.Source))
}
}
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
instanceID := s.InstanceID()
if s.Position == nil {
@ -168,18 +198,15 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
for _, method := range s.ExitMethods {
method.Bind(session, s.GeneralOrderExecutor)
}
s.ActiveOrderBook = bbgo.NewActiveOrderBook(s.Symbol)
s.ActiveOrderBook.BindStream(session.UserDataStream)
store, _ := session.MarketDataStore(s.Symbol)
getSource := func(kline *types.KLine) fixedpoint.Value {
//return kline.High.Add(kline.Low).Div(Two)
//return kline.Close
return kline.High.Add(kline.Low).Add(kline.Close).Div(Three)
}
getSource := s.SourceFuncGenerator()
s.drift = &indicator.Drift{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.Window}}
s.drift = &indicator.Drift{
MA: &indicator.SMA{IntervalWindow: s.IntervalWindow},
IntervalWindow: s.IntervalWindow,
}
s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: 14}}
klines, ok := store.KLinesOfInterval(s.Interval)
@ -187,33 +214,57 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
log.Errorf("klines not exists")
return nil
}
dynamicKLine := &types.KLine{}
for _, kline := range *klines {
source := getSource(&kline).Float64()
s.drift.Update(source)
s.atr.Update(kline.High.Float64(), kline.Low.Float64(), kline.Close.Float64())
}
session.MarketDataStream.OnBookTickerUpdate(func(ticker types.BookTicker) {
if s.Environment.IsBackTesting() {
return
}
bestBid := ticker.Buy
bestAsk := ticker.Sell
if util.TryLock(&s.lock) {
if !bestAsk.IsZero() && !bestBid.IsZero() {
s.midPrice = bestAsk.Add(bestBid).Div(types.Two)
} else if !bestAsk.IsZero() {
s.midPrice = bestAsk
} else {
s.midPrice = bestBid
if s.Environment.IsBackTesting() {
s.getLastPrice = func() fixedpoint.Value {
lastPrice, ok := s.Session.LastPrice(s.Symbol)
if !ok {
log.Error("cannot get lastprice")
}
s.lock.Unlock()
return lastPrice
}
})
} else {
session.MarketDataStream.OnBookTickerUpdate(func(ticker types.BookTicker) {
bestBid := ticker.Buy
bestAsk := ticker.Sell
if util.TryLock(&s.lock) {
if !bestAsk.IsZero() && !bestBid.IsZero() {
s.midPrice = bestAsk.Add(bestBid).Div(Two)
} else if !bestAsk.IsZero() {
s.midPrice = bestAsk
} else {
s.midPrice = bestBid
}
s.lock.Unlock()
}
})
s.getLastPrice = func() (lastPrice fixedpoint.Value) {
var ok bool
s.lock.RLock()
if s.midPrice.IsZero() {
lastPrice, ok = s.Session.LastPrice(s.Symbol)
if !ok {
log.Error("cannot get lastprice")
return lastPrice
}
} else {
lastPrice = s.midPrice
}
s.lock.RUnlock()
return lastPrice
}
}
dynamicKLine := &types.KLine{}
priceLine := types.NewQueue(100)
stoploss := s.Stoploss.Float64()
session.MarketDataStream.OnKLineClosed(func(kline types.KLine) {
if s.Status != types.StrategyStatusRunning {
@ -236,15 +287,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
source := getSource(dynamicKLine)
sourcef := source.Float64()
priceLine.Update(sourcef)
dynamicKLine.Closed = false
s.drift.Update(sourcef)
drift = s.drift.Array(2)
driftPred = s.drift.Predict(3)
driftPred = s.drift.Predict(s.PredictOffset)
atr = s.atr.Last()
price := s.GetLastPrice()
price := s.getLastPrice()
pricef := price.Float64()
avg := s.Position.AverageCost.Float64()
stoploss := s.Stoploss.Float64()
shortCondition := (driftPred <= 0 && drift[0] <= 0)
longCondition := (driftPred >= 0 && drift[0] >= 0)
@ -253,30 +302,17 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
exitLongCondition := ((drift[1] > 0 && drift[0] < 0) || avg-atr/2 >= pricef || avg*(1.-stoploss) >= pricef) &&
(!s.Position.IsClosed() && !s.Position.IsDust(fixedpoint.Min(price, source))) && !shortCondition
if exitShortCondition {
if s.ActiveOrderBook.NumOfOrders() > 0 {
if err := s.GeneralOrderExecutor.GracefulCancelActiveOrderBook(ctx, s.ActiveOrderBook); err != nil {
log.WithError(err).Errorf("cannot cancel orders")
return
}
}
_, _ = s.ClosePosition(ctx)
}
if exitLongCondition {
if s.ActiveOrderBook.NumOfOrders() > 0 {
if err := s.GeneralOrderExecutor.GracefulCancelActiveOrderBook(ctx, s.ActiveOrderBook); err != nil {
log.WithError(err).Errorf("cannot cancel orders")
return
}
if exitShortCondition || exitLongCondition {
if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil {
log.WithError(err).Errorf("cannot cancel orders")
return
}
_, _ = s.ClosePosition(ctx)
}
if shortCondition {
if s.ActiveOrderBook.NumOfOrders() > 0 {
if err := s.GeneralOrderExecutor.GracefulCancelActiveOrderBook(ctx, s.ActiveOrderBook); err != nil {
log.WithError(err).Errorf("cannot cancel orders")
return
}
if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil {
log.WithError(err).Errorf("cannot cancel orders")
return
}
baseBalance, ok := s.Session.GetAccount().Balance(s.Market.BaseCurrency)
if !ok {
@ -295,7 +331,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
Side: types.SideTypeSell,
Type: types.OrderTypeLimitMaker,
Price: source,
StopPrice: fixedpoint.NewFromFloat(sourcef + atr/2),
StopPrice: fixedpoint.NewFromFloat(math.Min(sourcef+atr/2, sourcef*(1.+stoploss))),
Quantity: baseBalance.Available,
})
if err != nil {
@ -304,11 +340,9 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
}
}
if longCondition {
if s.ActiveOrderBook.NumOfOrders() > 0 {
if err := s.GeneralOrderExecutor.GracefulCancelActiveOrderBook(ctx, s.ActiveOrderBook); err != nil {
log.WithError(err).Errorf("cannot cancel orders")
return
}
if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil {
log.WithError(err).Errorf("cannot cancel orders")
return
}
if source.Compare(price) > 0 {
source = price
@ -322,15 +356,12 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
quoteBalance.Available.Div(source), source) {
return
}
if !s.Position.IsClosed() && !s.Position.IsDust(source) {
return
}
_, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeLimitMaker,
Price: source,
StopPrice: fixedpoint.NewFromFloat(sourcef - atr/2),
StopPrice: fixedpoint.NewFromFloat(math.Max(sourcef-atr/2, sourcef*(1.-stoploss))),
Quantity: quoteBalance.Available.Div(source),
})
if err != nil {
@ -342,8 +373,8 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
_, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String())
s.Print()
canvas := types.NewCanvas(s.InstanceID(), s.Interval)
fmt.Println(dynamicKLine.StartTime, dynamicKLine.EndTime)
mean := priceLine.Mean(100)
highestPrice := priceLine.Minus(mean).Highest(100)
highestDrift := s.drift.Highest(100)

View File

@ -3,11 +3,11 @@ package types
import (
"fmt"
"math"
"time"
"reflect"
"time"
"gonum.org/v1/gonum/stat"
"github.com/wcharczuk/go-chart/v2"
"gonum.org/v1/gonum/stat"
)
// Super basic Series type that simply holds the float64 data
@ -46,8 +46,8 @@ func (inc *Queue) Length() int {
}
func (inc *Queue) Clone() *Queue {
out := &Queue {
arr: inc.arr[:],
out := &Queue{
arr: inc.arr[:],
size: inc.size,
}
out.SeriesBase.Series = out
@ -213,7 +213,7 @@ func Abs(a Series) SeriesExtend {
var _ Series = &AbsResult{}
func Predict(a Series, lookback int, offset ...int) float64 {
func LinearRegression(a Series, lookback int) (alpha float64, beta float64) {
if a.Length() < lookback {
lookback = a.Length()
}
@ -224,7 +224,12 @@ func Predict(a Series, lookback int, offset ...int) float64 {
x[i] = float64(i)
y[i] = a.Index(i)
}
alpha, beta := stat.LinearRegression(x, y, weights, false)
alpha, beta = stat.LinearRegression(x, y, weights, false)
return
}
func Predict(a Series, lookback int, offset ...int) float64 {
alpha, beta := LinearRegression(a, lookback)
o := -1.0
if len(offset) > 0 {
o = -float64(offset[0])
@ -1167,15 +1172,15 @@ type Canvas struct {
func NewCanvas(title string, interval Interval) *Canvas {
valueFormatter := chart.TimeValueFormatter
if interval.Minutes() > 24 * 60 {
if interval.Minutes() > 24*60 {
valueFormatter = chart.TimeDateValueFormatter
} else if interval.Minutes() > 60 {
valueFormatter = chart.TimeHourValueFormatter
} else {
valueFormatter = chart.TimeMinuteValueFormatter
}
out := &Canvas {
Chart: chart.Chart {
out := &Canvas{
Chart: chart.Chart{
Title: title,
XAxis: chart.XAxis{
ValueFormatter: valueFormatter,
@ -1193,11 +1198,11 @@ func (canvas *Canvas) Plot(tag string, a Series, endTime Time, length int) {
var timeline []time.Time
e := endTime.Time()
for i := length - 1; i >= 0; i-- {
shiftedT := e.Add(-time.Duration(i * canvas.Interval.Minutes()) * time.Minute)
shiftedT := e.Add(-time.Duration(i*canvas.Interval.Minutes()) * time.Minute)
timeline = append(timeline, shiftedT)
}
canvas.Series = append(canvas.Series, chart.TimeSeries{
Name: tag,
Name: tag,
YValues: Reverse(a, length),
XValues: timeline,
})