fix: drift exit condition, trade_stats serialization in redis

This commit is contained in:
zenix 2022-07-20 19:49:56 +09:00
parent a5039de6aa
commit a8fe20ae3a
3 changed files with 73 additions and 54 deletions

View File

@ -1,8 +1,14 @@
--- ---
persistence:
redis:
host: 127.0.0.1
port: 6379
db: 0
sessions: sessions:
binance: binance:
exchange: binance exchange: binance
futures: true futures: false
envVarPrefix: binance envVarPrefix: binance
heikinAshi: false heikinAshi: false
@ -15,13 +21,15 @@ exchangeStrategies:
# kline interval for indicators # kline interval for indicators
interval: 15m interval: 15m
window: 2 window: 2
stoploss: 3% stoploss: 2%
source: close source: close
predictOffset: 14 predictOffset: 3
# position avg +- takeProfitFactor * atr as take profit price
takeProfitFactor: 1
noStopPrice: true noStopPrice: true
noTrailingStopLoss: false noTrailingStopLoss: false
# stddev on high/low-source # stddev on high/low-source
hlVarianceMultiplier: 0.35 hlVarianceMultiplier: 0.34
generateGraph: true generateGraph: true
graphPNLDeductFee: false graphPNLDeductFee: false

View File

@ -57,6 +57,7 @@ type Strategy struct {
lock sync.RWMutex lock sync.RWMutex
Source string `json:"source"` Source string `json:"source"`
TakeProfitFactor float64 `json:"takeProfitFactor"`
StopLoss fixedpoint.Value `json:"stoploss"` StopLoss fixedpoint.Value `json:"stoploss"`
CanvasPath string `json:"canvasPath"` CanvasPath string `json:"canvasPath"`
PredictOffset int `json:"predictOffset"` PredictOffset int `json:"predictOffset"`
@ -72,7 +73,7 @@ type Strategy struct {
// Whether to generate graph when shutdown // Whether to generate graph when shutdown
GenerateGraph bool `json:"generateGraph"` GenerateGraph bool `json:"generateGraph"`
StopOrders map[uint64]types.SubmitOrder StopOrders map[uint64]*types.SubmitOrder
ExitMethods bbgo.ExitMethodSet `json:"exits"` ExitMethods bbgo.ExitMethodSet `json:"exits"`
Session *bbgo.ExchangeSession Session *bbgo.ExchangeSession
@ -91,6 +92,7 @@ func (s *Strategy) Print(o *os.File) {
hiyellow(f, "canvasPath: %s\n", s.CanvasPath) hiyellow(f, "canvasPath: %s\n", s.CanvasPath)
hiyellow(f, "source: %s\n", s.Source) hiyellow(f, "source: %s\n", s.Source)
hiyellow(f, "stoploss: %v\n", s.StopLoss) hiyellow(f, "stoploss: %v\n", s.StopLoss)
hiyellow(f, "takeProfitFactor: %f\n", s.TakeProfitFactor)
hiyellow(f, "predictOffset: %d\n", s.PredictOffset) hiyellow(f, "predictOffset: %d\n", s.PredictOffset)
hiyellow(f, "exits:\n %s\n", string(b)) hiyellow(f, "exits:\n %s\n", string(b))
hiyellow(f, "symbol: %s\n", s.Symbol) hiyellow(f, "symbol: %s\n", s.Symbol)
@ -124,12 +126,23 @@ func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
s.ExitMethods.SetAndSubscribe(session, s) s.ExitMethods.SetAndSubscribe(session, s)
} }
func (s *Strategy) ClosePosition(ctx context.Context) (*types.Order, bool) { func (s *Strategy) CurrentPosition() *types.Position {
// Cleanup pending StopOrders return s.Position
s.StopOrders = make(map[uint64]types.SubmitOrder) }
order := s.Position.NewMarketCloseOrder(fixedpoint.One)
func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error {
order := s.Position.NewMarketCloseOrder(percentage)
if order == nil { if order == nil {
return nil, false return nil
}
if percentage.Compare(fixedpoint.One) == 0 {
// Cleanup pending StopOrders
s.StopOrders = make(map[uint64]*types.SubmitOrder)
} else {
// Should only have one stop order
for _, o := range s.StopOrders {
o.Quantity = o.Quantity.Mul(fixedpoint.One.Sub(percentage))
}
} }
order.Tag = "close" order.Tag = "close"
order.TimeInForce = "" order.TimeInForce = ""
@ -146,14 +159,14 @@ func (s *Strategy) ClosePosition(ctx context.Context) (*types.Order, bool) {
} }
for { for {
if s.Market.IsDustQuantity(order.Quantity, price) { if s.Market.IsDustQuantity(order.Quantity, price) {
return nil, true return nil
} }
createdOrders, 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 &createdOrders[0], true return nil
} }
} }
@ -190,7 +203,7 @@ func (s *Strategy) SourceFuncGenerator() SourceFunc {
} }
func (s *Strategy) BindStopLoss(ctx context.Context) { func (s *Strategy) BindStopLoss(ctx context.Context) {
s.StopOrders = make(map[uint64]types.SubmitOrder) s.StopOrders = make(map[uint64]*types.SubmitOrder)
s.Session.UserDataStream.OnOrderUpdate(func(order types.Order) { s.Session.UserDataStream.OnOrderUpdate(func(order types.Order) {
if len(s.StopOrders) == 0 { if len(s.StopOrders) == 0 {
return return
@ -222,7 +235,7 @@ func (s *Strategy) BindStopLoss(ctx context.Context) {
} }
o.Quantity = baseBalance.Available o.Quantity = baseBalance.Available
} }
if _, err := s.GeneralOrderExecutor.SubmitOrders(ctx, o); err != nil { if _, err := s.GeneralOrderExecutor.SubmitOrders(ctx, *o); err != nil {
log.WithError(err).Errorf("cannot send stop order: %v", order) log.WithError(err).Errorf("cannot send stop order: %v", order)
} }
} }
@ -295,16 +308,16 @@ func (s *Strategy) InitTickerFunctions(ctx context.Context) {
atr = s.atr.Last() atr = s.atr.Last()
avg = s.Position.AverageCost.Float64() avg = s.Position.AverageCost.Float64()
stoploss = s.StopLoss.Float64() stoploss = s.StopLoss.Float64()
exitShortCondition := (avg+atr/2 <= pricef || avg*(1.+stoploss) <= pricef) && exitShortCondition := (avg+atr/2 <= pricef || avg*(1.+stoploss) <= pricef || avg-atr*s.TakeProfitFactor >= pricef) &&
(!s.Position.IsClosed() && !s.Position.IsDust(price)) (s.Position.IsShort() && !s.Position.IsDust(price))
exitLongCondition := (avg-atr/2 >= pricef || avg*(1.-stoploss) >= pricef) && exitLongCondition := (avg-atr/2 >= pricef || avg*(1.-stoploss) >= pricef || avg+atr*s.TakeProfitFactor <= pricef) &&
(!s.Position.IsClosed() && !s.Position.IsDust(price)) (!s.Position.IsLong() && !s.Position.IsDust(price))
if exitShortCondition || exitLongCondition { if exitShortCondition || exitLongCondition {
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")
return return
} }
_, _ = s.ClosePosition(ctx) _ = s.ClosePosition(ctx, fixedpoint.One)
} }
}) })
@ -360,11 +373,12 @@ func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend, profit ty
} }
f, err = os.Create(s.GraphPNLPath) f, err = os.Create(s.GraphPNLPath)
if err != nil { if err != nil {
panic("open pnl") log.WithError(err).Errorf("open pnl")
return
} }
defer f.Close() defer f.Close()
if err := canvas.Render(chart.PNG, f); err != nil { if err := canvas.Render(chart.PNG, f); err != nil {
panic("render pnl") log.WithError(err).Errorf("render pnl")
} }
canvas = types.NewCanvas(s.InstanceID()) canvas = types.NewCanvas(s.InstanceID())
@ -375,11 +389,12 @@ func (s *Strategy) Draw(time types.Time, priceLine types.SeriesExtend, profit ty
} }
f, err = os.Create(s.GraphCumPNLPath) f, err = os.Create(s.GraphCumPNLPath)
if err != nil { if err != nil {
panic("open cumpnl") log.WithError(err).Errorf("open cumpnl")
return
} }
defer f.Close() defer f.Close()
if err := canvas.Render(chart.PNG, f); err != nil { if err := canvas.Render(chart.PNG, f); err != nil {
panic("render cumpnl") log.WithError(err).Errorf("render cumpnl")
} }
} }
@ -410,7 +425,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.OnEmergencyStop(func() { s.OnEmergencyStop(func() {
_ = s.GeneralOrderExecutor.GracefulCancel(ctx) _ = s.GeneralOrderExecutor.GracefulCancel(ctx)
_, _ = s.ClosePosition(ctx) _ = s.ClosePosition(ctx, fixedpoint.One)
}) })
s.GeneralOrderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) s.GeneralOrderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)
@ -544,16 +559,16 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
highf := math.Max(kline.High.Float64(), pricef) highf := math.Max(kline.High.Float64(), pricef)
avg := s.Position.AverageCost.Float64() avg := s.Position.AverageCost.Float64()
exitShortCondition := (avg+atr/2 <= highf || avg*(1.+stoploss) <= highf) && exitShortCondition := (avg+atr/2 <= highf || avg*(1.+stoploss) <= highf || avg-atr*s.TakeProfitFactor >= lowf) &&
(!s.Position.IsClosed() && !s.Position.IsDust(price)) (s.Position.IsShort() && !s.Position.IsDust(price))
exitLongCondition := (avg-atr/2 >= lowf || avg*(1.-stoploss) >= lowf) && exitLongCondition := (avg-atr/2 >= lowf || avg*(1.-stoploss) >= lowf || avg+atr*s.TakeProfitFactor <= highf) &&
(!s.Position.IsClosed() && !s.Position.IsDust(price)) (s.Position.IsLong() && !s.Position.IsDust(price))
if exitShortCondition || exitLongCondition { if exitShortCondition || exitLongCondition {
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")
return return
} }
_, _ = s.ClosePosition(ctx) _ = s.ClosePosition(ctx, fixedpoint.One)
} }
return return
} }
@ -578,19 +593,26 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.stdevHigh.Update(highdiff) s.stdevHigh.Update(highdiff)
avg := s.Position.AverageCost.Float64() avg := s.Position.AverageCost.Float64()
if !s.IsBackTesting() {
balances := s.Session.GetAccount().Balances()
log.Infof("source: %.4f, price: %.4f, driftPred: %.4f, drift: %.4f, drift[1]: %.4f, atr: %.4f, avg: %.4f",
sourcef, pricef, driftPred, drift[0], drift[1], atr, avg)
log.Infof("balances: [Base] %v [Quote] %v", balances[s.Market.BaseCurrency], balances[s.Market.QuoteCurrency])
}
shortCondition := (driftPred <= 0 && drift[0] <= 0) shortCondition := (driftPred <= 0 && drift[0] <= 0)
longCondition := (driftPred >= 0 && drift[0] >= 0) longCondition := (driftPred >= 0 && drift[0] >= 0)
exitShortCondition := ((drift[1] < 0 && drift[0] >= 0) || avg+atr/2 <= highf || avg*(1.+stoploss) <= highf) && exitShortCondition := ((drift[1] < 0 && drift[0] >= 0) || avg+atr/2 <= highf || avg*(1.+stoploss) <= highf || avg-atr*s.TakeProfitFactor >= lowf) &&
(!s.Position.IsClosed() && !s.Position.IsDust(fixedpoint.Max(price, source))) && !longCondition (s.Position.IsShort() && !s.Position.IsDust(fixedpoint.Max(price, source))) && !longCondition
exitLongCondition := ((drift[1] > 0 && drift[0] < 0) || avg-atr/2 >= lowf || avg*(1.-stoploss) >= lowf) && exitLongCondition := ((drift[1] > 0 && drift[0] < 0) || avg-atr/2 >= lowf || avg*(1.-stoploss) >= lowf || avg+atr*s.TakeProfitFactor <= highf) &&
(!s.Position.IsClosed() && !s.Position.IsDust(fixedpoint.Min(price, source))) && !shortCondition (s.Position.IsLong() && !s.Position.IsDust(fixedpoint.Min(price, source))) && !shortCondition
if exitShortCondition || exitLongCondition { if exitShortCondition || exitLongCondition {
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")
return return
} }
_, _ = s.ClosePosition(ctx) _ = s.ClosePosition(ctx, fixedpoint.One)
} }
if shortCondition { if shortCondition {
if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil {
@ -611,7 +633,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
return return
} }
// Cleanup pending StopOrders // Cleanup pending StopOrders
s.StopOrders = make(map[uint64]types.SubmitOrder) s.StopOrders = make(map[uint64]*types.SubmitOrder)
quantity := baseBalance.Available quantity := baseBalance.Available
stopPrice := fixedpoint.NewFromFloat(math.Min(sourcef+atr/2, sourcef*(1.+stoploss))) stopPrice := fixedpoint.NewFromFloat(math.Min(sourcef+atr/2, sourcef*(1.+stoploss)))
stopOrder := types.SubmitOrder{ stopOrder := types.SubmitOrder{
@ -642,7 +664,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.GeneralOrderExecutor.SubmitOrders(ctx, stopOrder) s.GeneralOrderExecutor.SubmitOrders(ctx, stopOrder)
return return
} }
s.StopOrders[createdOrders[0].OrderID] = stopOrder s.StopOrders[createdOrders[0].OrderID] = &stopOrder
} }
if longCondition { if longCondition {
if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil { if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil {
@ -663,7 +685,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
return return
} }
// Cleanup pending StopOrders // Cleanup pending StopOrders
s.StopOrders = make(map[uint64]types.SubmitOrder) s.StopOrders = make(map[uint64]*types.SubmitOrder)
quantity := quoteBalance.Available.Div(source) quantity := quoteBalance.Available.Div(source)
stopPrice := fixedpoint.NewFromFloat(math.Max(sourcef-atr/2, sourcef*(1.-stoploss))) stopPrice := fixedpoint.NewFromFloat(math.Max(sourcef-atr/2, sourcef*(1.-stoploss)))
stopOrder := types.SubmitOrder{ stopOrder := types.SubmitOrder{
@ -695,7 +717,7 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.GeneralOrderExecutor.SubmitOrders(ctx, stopOrder) s.GeneralOrderExecutor.SubmitOrders(ctx, stopOrder)
return return
} }
s.StopOrders[createdOrders[0].OrderID] = stopOrder s.StopOrders[createdOrders[0].OrderID] = &stopOrder
} }
}) })

View File

@ -1,7 +1,6 @@
package types package types
import ( import (
"encoding/json"
"time" "time"
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
@ -10,9 +9,9 @@ import (
) )
type IntervalProfitCollector struct { type IntervalProfitCollector struct {
Interval Interval Interval Interval `json:"interval"`
Profits *Float64Slice Profits *Float64Slice `json:"profits"`
tmpTime time.Time tmpTime time.Time `json:"tmpTime"`
} }
func NewIntervalProfitCollector(i Interval, startTime time.Time) *IntervalProfitCollector { func NewIntervalProfitCollector(i Interval, startTime time.Time) *IntervalProfitCollector {
@ -92,16 +91,6 @@ func (s IntervalProfitCollector) MarshalYAML() (interface{}, error) {
return result, nil return result, nil
} }
func (s *IntervalProfitCollector) MarshalJSON() ([]byte, error) {
result := make(map[string]interface{})
result["Sharpe Ratio"] = s.GetSharpe()
result["Omega Ratio"] = s.GetOmega()
result["Profitable Count"] = s.GetNumOfProfitableIntervals()
result["NonProfitable Count"] = s.GetNumOfNonProfitableIntervals()
return json.Marshal(result)
}
// TODO: Add more stats from the reference: // TODO: Add more stats from the reference:
// See https://www.metatrader5.com/en/terminal/help/algotrading/testing_report // See https://www.metatrader5.com/en/terminal/help/algotrading/testing_report
type TradeStats struct { type TradeStats struct {
@ -117,7 +106,7 @@ type TradeStats struct {
MostLossTrade fixedpoint.Value `json:"mostLossTrade" yaml:"mostLossTrade"` MostLossTrade fixedpoint.Value `json:"mostLossTrade" yaml:"mostLossTrade"`
ProfitFactor fixedpoint.Value `json:"profitFactor" yaml:"profitFactor"` ProfitFactor fixedpoint.Value `json:"profitFactor" yaml:"profitFactor"`
TotalNetProfit fixedpoint.Value `json:"totalNetProfit" yaml:"totalNetProfit"` TotalNetProfit fixedpoint.Value `json:"totalNetProfit" yaml:"totalNetProfit"`
IntervalProfits map[Interval]*IntervalProfitCollector `json:"intervalProfits,omitempty" yaml: "intervalProfits,omitempty"` IntervalProfits map[Interval]*IntervalProfitCollector `jons:"intervalProfits,omitempty" yaml: "intervalProfits,omitempty"`
} }
func NewTradeStats(symbol string) *TradeStats { func NewTradeStats(symbol string) *TradeStats {