fix: unlimited length of indicators, add draw elapsed to drift

This commit is contained in:
zenix 2022-10-27 17:31:39 +09:00
parent 493b81f16c
commit b2e867e51c
10 changed files with 271 additions and 172 deletions

View File

@ -68,6 +68,7 @@ exchangeStrategies:
graphPNLDeductFee: false graphPNLDeductFee: false
graphPNLPath: "./pnl.png" graphPNLPath: "./pnl.png"
graphCumPNLPath: "./cumpnl.png" graphCumPNLPath: "./cumpnl.png"
graphElapsedPath: "./elapsed.png"
#exits: #exits:
# - roiStopLoss: # - roiStopLoss:
# percentage: 0.35% # percentage: 0.35%

View File

@ -8,6 +8,9 @@ import (
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
const MaxNumOfATR = 1000
const MaxNumOfATRTruncateSize = 500
//go:generate callbackgen -type ATR //go:generate callbackgen -type ATR
type ATR struct { type ATR struct {
types.SeriesBase types.SeriesBase
@ -73,6 +76,9 @@ func (inc *ATR) Update(high, low, cloze float64) {
inc.RMA.Update(trueRange) inc.RMA.Update(trueRange)
atr := inc.RMA.Last() atr := inc.RMA.Last()
inc.PercentageVolatility.Push(atr / cloze) inc.PercentageVolatility.Push(atr / cloze)
if len(inc.PercentageVolatility) > MaxNumOfATR {
inc.PercentageVolatility = inc.PercentageVolatility[MaxNumOfATRTruncateSize-1:]
}
} }
func (inc *ATR) Last() float64 { func (inc *ATR) Last() float64 {

View File

@ -8,8 +8,8 @@ import (
) )
// These numbers should be aligned with bbgo MaxNumOfKLines and MaxNumOfKLinesTruncate // These numbers should be aligned with bbgo MaxNumOfKLines and MaxNumOfKLinesTruncate
const MaxNumOfEWMA = 5_000 const MaxNumOfEWMA = 1_000
const MaxNumOfEWMATruncateSize = 100 const MaxNumOfEWMATruncateSize = 500
//go:generate callbackgen -type EWMA //go:generate callbackgen -type EWMA
type EWMA struct { type EWMA struct {

View File

@ -7,6 +7,9 @@ import (
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
const MaxNumOfRMA = 1000
const MaxNumOfRMATruncateSize = 500
// Running Moving Average // Running Moving Average
// Refer: https://github.com/twopirllc/pandas-ta/blob/main/pandas_ta/overlap/rma.py#L5 // Refer: https://github.com/twopirllc/pandas-ta/blob/main/pandas_ta/overlap/rma.py#L5
// Refer: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.ewm.html#pandas-dataframe-ewm // Refer: https://pandas.pydata.org/docs/reference/api/pandas.DataFrame.ewm.html#pandas-dataframe-ewm
@ -62,6 +65,9 @@ func (inc *RMA) Update(x float64) {
} }
inc.Values.Push(inc.tmp) inc.Values.Push(inc.tmp)
if len(inc.Values) > MaxNumOfRMA {
inc.Values = inc.Values[MaxNumOfRMATruncateSize-1:]
}
} }
func (inc *RMA) Last() float64 { func (inc *RMA) Last() float64 {

View File

@ -65,6 +65,9 @@ func (inc *SMA) Update(value float64) {
} }
inc.Values.Push(types.Mean(inc.rawValues)) inc.Values.Push(types.Mean(inc.rawValues))
if len(inc.Values) > MaxNumOfSMA {
inc.Values = inc.Values[MaxNumOfSMATruncateSize-1:]
}
} }
func (inc *SMA) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) { func (inc *SMA) BindK(target KLineClosedEmitter, symbol string, interval types.Interval) {

View File

@ -7,6 +7,9 @@ import (
"github.com/c9s/bbgo/pkg/types" "github.com/c9s/bbgo/pkg/types"
) )
const MaxNumOfStdev = 600
const MaxNumOfStdevTruncateSize = 300
//go:generate callbackgen -type StdDev //go:generate callbackgen -type StdDev
type StdDev struct { type StdDev struct {
types.SeriesBase types.SeriesBase
@ -49,6 +52,9 @@ func (inc *StdDev) Update(value float64) {
var std = inc.rawValues.Stdev() var std = inc.rawValues.Stdev()
inc.Values.Push(std) inc.Values.Push(std)
if len(inc.Values) > MaxNumOfStdev {
inc.Values = inc.Values[MaxNumOfStdevTruncateSize-1:]
}
} }
func (inc *StdDev) PushK(k types.KLine) { func (inc *StdDev) PushK(k types.KLine) {

175
pkg/strategy/drift/draw.go Normal file
View File

@ -0,0 +1,175 @@
package drift
import (
"bytes"
"fmt"
"os"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/interact"
"github.com/c9s/bbgo/pkg/types"
"github.com/wcharczuk/go-chart/v2"
)
func (s *Strategy) InitDrawCommands(profit, cumProfit types.Series) {
bbgo.RegisterCommand("/draw", "Draw Indicators", func(reply interact.Reply) {
go func() {
canvas := s.DrawIndicators(s.frameKLine.StartTime)
var buffer bytes.Buffer
if err := canvas.Render(chart.PNG, &buffer); err != nil {
log.WithError(err).Errorf("cannot render indicators in drift")
return
}
bbgo.SendPhoto(&buffer)
}()
})
bbgo.RegisterCommand("/pnl", "Draw PNL(%) per trade", func(reply interact.Reply) {
go func() {
canvas := s.DrawPNL(profit)
var buffer bytes.Buffer
if err := canvas.Render(chart.PNG, &buffer); err != nil {
log.WithError(err).Errorf("cannot render pnl in drift")
return
}
bbgo.SendPhoto(&buffer)
}()
})
bbgo.RegisterCommand("/cumpnl", "Draw Cummulative PNL(Quote)", func(reply interact.Reply) {
go func() {
canvas := s.DrawCumPNL(cumProfit)
var buffer bytes.Buffer
if err := canvas.Render(chart.PNG, &buffer); err != nil {
log.WithError(err).Errorf("cannot render cumpnl in drift")
return
}
bbgo.SendPhoto(&buffer)
}()
})
bbgo.RegisterCommand("/elapsed", "Draw Elapsed time for handlers for each kline close event", func(reply interact.Reply) {
go func() {
canvas := s.DrawElapsed()
var buffer bytes.Buffer
if err := canvas.Render(chart.PNG, &buffer); err != nil {
log.WithError(err).Errorf("cannot render elapsed in drift")
return
}
bbgo.SendPhoto(&buffer)
}()
})
}
func (s *Strategy) DrawIndicators(time types.Time) *types.Canvas {
canvas := types.NewCanvas(s.InstanceID(), s.Interval)
Length := s.priceLines.Length()
if Length > 300 {
Length = 300
}
log.Infof("draw indicators with %d data", Length)
mean := s.priceLines.Mean(Length)
highestPrice := s.priceLines.Minus(mean).Abs().Highest(Length)
highestDrift := s.drift.Abs().Highest(Length)
hi := s.drift.drift.Abs().Highest(Length)
ratio := highestPrice / highestDrift
// canvas.Plot("upband", s.ma.Add(s.stdevHigh), time, Length)
canvas.Plot("ma", s.ma, time, Length)
// canvas.Plot("downband", s.ma.Minus(s.stdevLow), time, Length)
fmt.Printf("%f %f\n", highestPrice, hi)
canvas.Plot("trend", s.trendLine, time, Length)
canvas.Plot("drift", s.drift.Mul(ratio).Add(mean), time, Length)
canvas.Plot("driftOrig", s.drift.drift.Mul(highestPrice/hi).Add(mean), time, Length)
canvas.Plot("zero", types.NumberSeries(mean), time, Length)
canvas.Plot("price", s.priceLines, time, Length)
return canvas
}
func (s *Strategy) DrawPNL(profit types.Series) *types.Canvas {
canvas := types.NewCanvas(s.InstanceID())
log.Errorf("pnl Highest: %f, Lowest: %f", types.Highest(profit, profit.Length()), types.Lowest(profit, profit.Length()))
length := profit.Length()
if s.GraphPNLDeductFee {
canvas.PlotRaw("pnl % (with Fee Deducted)", profit, length)
} else {
canvas.PlotRaw("pnl %", profit, length)
}
canvas.YAxis = chart.YAxis{
ValueFormatter: func(v interface{}) string {
if vf, isFloat := v.(float64); isFloat {
return fmt.Sprintf("%.4f", vf)
}
return ""
},
}
canvas.PlotRaw("1", types.NumberSeries(1), length)
return canvas
}
func (s *Strategy) DrawCumPNL(cumProfit types.Series) *types.Canvas {
canvas := types.NewCanvas(s.InstanceID())
canvas.PlotRaw("cummulative pnl", cumProfit, cumProfit.Length())
canvas.YAxis = chart.YAxis{
ValueFormatter: func(v interface{}) string {
if vf, isFloat := v.(float64); isFloat {
return fmt.Sprintf("%.4f", vf)
}
return ""
},
}
return canvas
}
func (s *Strategy) DrawElapsed() *types.Canvas {
canvas := types.NewCanvas(s.InstanceID())
canvas.PlotRaw("elapsed time(ms)", s.elapsed, s.elapsed.Length())
return canvas
}
func (s *Strategy) Draw(time types.Time, profit types.Series, cumProfit types.Series) {
canvas := s.DrawIndicators(time)
f, err := os.Create(s.CanvasPath)
if err != nil {
log.WithError(err).Errorf("cannot create on %s", s.CanvasPath)
return
}
if err := canvas.Render(chart.PNG, f); err != nil {
log.WithError(err).Errorf("cannot render in drift")
}
f.Close()
canvas = s.DrawPNL(profit)
f, err = os.Create(s.GraphPNLPath)
if err != nil {
log.WithError(err).Errorf("open pnl")
return
}
if err := canvas.Render(chart.PNG, f); err != nil {
log.WithError(err).Errorf("render pnl")
}
f.Close()
canvas = s.DrawCumPNL(cumProfit)
f, err = os.Create(s.GraphCumPNLPath)
if err != nil {
log.WithError(err).Errorf("open cumpnl")
return
}
if err := canvas.Render(chart.PNG, f); err != nil {
log.WithError(err).Errorf("render cumpnl")
}
f.Close()
canvas = s.DrawElapsed()
f, err = os.Create(s.GraphElapsedPath)
if err != nil {
log.WithError(err).Errorf("open elapsed")
return
}
if err := canvas.Render(chart.PNG, f); err != nil {
log.WithError(err).Errorf("render elapsed")
}
f.Close()
}

View File

@ -12,7 +12,6 @@ import (
"time" "time"
"github.com/sirupsen/logrus" "github.com/sirupsen/logrus"
"github.com/wcharczuk/go-chart/v2"
"go.uber.org/multierr" "go.uber.org/multierr"
"github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/bbgo"
@ -68,6 +67,7 @@ type Strategy struct {
p *types.Position p *types.Position
MinInterval types.Interval `json:"MinInterval"` // minimum interval referred for doing stoploss/trailing exists and updating highest/lowest MinInterval types.Interval `json:"MinInterval"` // minimum interval referred for doing stoploss/trailing exists and updating highest/lowest
elapsed *types.Queue
priceLines *types.Queue priceLines *types.Queue
trendLine types.UpdatableSeriesExtend trendLine types.UpdatableSeriesExtend
ma types.UpdatableSeriesExtend ma types.UpdatableSeriesExtend
@ -78,8 +78,8 @@ type Strategy struct {
midPrice fixedpoint.Value // the midPrice is the average of bestBid and bestAsk in public orderbook midPrice fixedpoint.Value // the midPrice is the average of bestBid and bestAsk in public orderbook
lock sync.RWMutex `ignore:"true"` // lock for midPrice lock sync.RWMutex `ignore:"true"` // lock for midPrice
positionLock sync.RWMutex `ignore:"true"` // lock for highest/lowest and p positionLock sync.RWMutex `ignore:"true"` // lock for highest/lowest and p
pendingLock sync.Mutex `ignore:"true"`
startTime time.Time // trading start time startTime time.Time // trading start time
counter int // number of MinInterval since startTime
maxCounterBuyCanceled int // the largest counter of the order on the buy side been cancelled. meaning the latest cancelled buy order. maxCounterBuyCanceled int // the largest counter of the order on the buy side been cancelled. meaning the latest cancelled buy order.
maxCounterSellCanceled int // the largest counter of the order on the sell side been cancelled. meaning the latest cancelled sell order. maxCounterSellCanceled int // the largest counter of the order on the sell side been cancelled. meaning the latest cancelled sell order.
orderPendingCounter map[uint64]int // records the timepoint when the orders are created, using the counter at the time. orderPendingCounter map[uint64]int // records the timepoint when the orders are created, using the counter at the time.
@ -114,10 +114,11 @@ type Strategy struct {
// This is not related to trade but for statistics graph generation // This is not related to trade but for statistics graph generation
// Will deduct fee in percentage from every trade // Will deduct fee in percentage from every trade
GraphPNLDeductFee bool `json:"graphPNLDeductFee"` GraphPNLDeductFee bool `json:"graphPNLDeductFee"`
CanvasPath string `json:"canvasPath"` // backtest related. the path to store the indicator graph CanvasPath string `json:"canvasPath"` // backtest related. the path to store the indicator graph
GraphPNLPath string `json:"graphPNLPath"` // backtest related. the path to store the pnl % graph per trade graph. GraphPNLPath string `json:"graphPNLPath"` // backtest related. the path to store the pnl % graph per trade graph.
GraphCumPNLPath string `json:"graphCumPNLPath"` // backtest related. the path to store the asset changes in graph GraphCumPNLPath string `json:"graphCumPNLPath"` // backtest related. the path to store the asset changes in graph
GenerateGraph bool `json:"generateGraph"` // whether to generate graph when shutdown GraphElapsedPath string `json:"graphElapsedPath"` // the path to store the elapsed time in ms
GenerateGraph bool `json:"generateGraph"` // whether to generate graph when shutdown
ExitMethods bbgo.ExitMethodSet `json:"exits"` ExitMethods bbgo.ExitMethodSet `json:"exits"`
Session *bbgo.ExchangeSession Session *bbgo.ExchangeSession
@ -264,7 +265,7 @@ func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error {
return nil return nil
} }
func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64) (int, error) { func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64, syscounter int) (int, error) {
nonTraded := s.GeneralOrderExecutor.ActiveMakerOrders().Orders() nonTraded := s.GeneralOrderExecutor.ActiveMakerOrders().Orders()
if len(nonTraded) > 0 { if len(nonTraded) > 0 {
if len(nonTraded) > 1 { if len(nonTraded) > 1 {
@ -276,17 +277,21 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64) (int, e
if order.Status != types.OrderStatusNew && order.Status != types.OrderStatusPartiallyFilled { if order.Status != types.OrderStatusNew && order.Status != types.OrderStatusPartiallyFilled {
continue continue
} }
log.Warnf("%v | counter: %d, system: %d", order, s.orderPendingCounter[order.OrderID], s.counter) s.pendingLock.Lock()
if s.counter-s.orderPendingCounter[order.OrderID] > s.PendingMinInterval { counter := s.orderPendingCounter[order.OrderID]
s.pendingLock.Unlock()
log.Warnf("%v | counter: %d, system: %d", order, counter, syscounter)
if syscounter-counter > s.PendingMinInterval {
toCancel = true toCancel = true
} else if order.Side == types.SideTypeBuy { } else if order.Side == types.SideTypeBuy {
// 75% of the probability // 75% of the probability
if order.Price.Float64()+s.stdevHigh.Last()*2 <= pricef { if order.Price.Float64()+atr*2 <= pricef {
toCancel = true toCancel = true
} }
} else if order.Side == types.SideTypeSell { } else if order.Side == types.SideTypeSell {
// 75% of the probability // 75% of the probability
if order.Price.Float64()-s.stdevLow.Last()*2 >= pricef { if order.Price.Float64()-atr*2 >= pricef {
toCancel = true toCancel = true
} }
} else { } else {
@ -297,16 +302,19 @@ func (s *Strategy) smartCancel(ctx context.Context, pricef, atr float64) (int, e
err := s.GeneralOrderExecutor.CancelNoWait(ctx) err := s.GeneralOrderExecutor.CancelNoWait(ctx)
// TODO: clean orderPendingCounter on cancel/trade // TODO: clean orderPendingCounter on cancel/trade
for _, order := range nonTraded { for _, order := range nonTraded {
s.pendingLock.Lock()
counter := s.orderPendingCounter[order.OrderID]
delete(s.orderPendingCounter, order.OrderID)
s.pendingLock.Unlock()
if order.Side == types.SideTypeSell { if order.Side == types.SideTypeSell {
if s.maxCounterSellCanceled < s.orderPendingCounter[order.OrderID] { if s.maxCounterSellCanceled < counter {
s.maxCounterSellCanceled = s.orderPendingCounter[order.OrderID] s.maxCounterSellCanceled = counter
} }
} else { } else {
if s.maxCounterBuyCanceled < s.orderPendingCounter[order.OrderID] { if s.maxCounterBuyCanceled < counter {
s.maxCounterBuyCanceled = s.orderPendingCounter[order.OrderID] s.maxCounterBuyCanceled = counter
} }
} }
delete(s.orderPendingCounter, order.OrderID)
} }
log.Warnf("cancel all %v", err) log.Warnf("cancel all %v", err)
return 0, err return 0, err
@ -390,102 +398,6 @@ func (s *Strategy) initTickerFunctions(ctx context.Context) {
} }
func (s *Strategy) DrawIndicators(time types.Time) *types.Canvas {
canvas := types.NewCanvas(s.InstanceID(), s.Interval)
Length := s.priceLines.Length()
if Length > 300 {
Length = 300
}
log.Infof("draw indicators with %d data", Length)
mean := s.priceLines.Mean(Length)
highestPrice := s.priceLines.Minus(mean).Abs().Highest(Length)
highestDrift := s.drift.Abs().Highest(Length)
hi := s.drift.drift.Abs().Highest(Length)
ratio := highestPrice / highestDrift
// canvas.Plot("upband", s.ma.Add(s.stdevHigh), time, Length)
canvas.Plot("ma", s.ma, time, Length)
// canvas.Plot("downband", s.ma.Minus(s.stdevLow), time, Length)
fmt.Printf("%f %f\n", highestPrice, hi)
canvas.Plot("trend", s.trendLine, time, Length)
canvas.Plot("drift", s.drift.Mul(ratio).Add(mean), time, Length)
canvas.Plot("driftOrig", s.drift.drift.Mul(highestPrice/hi).Add(mean), time, Length)
canvas.Plot("zero", types.NumberSeries(mean), time, Length)
canvas.Plot("price", s.priceLines, time, Length)
return canvas
}
func (s *Strategy) DrawPNL(profit types.Series) *types.Canvas {
canvas := types.NewCanvas(s.InstanceID())
log.Errorf("pnl Highest: %f, Lowest: %f", types.Highest(profit, profit.Length()), types.Lowest(profit, profit.Length()))
length := profit.Length()
if s.GraphPNLDeductFee {
canvas.PlotRaw("pnl % (with Fee Deducted)", profit, length)
} else {
canvas.PlotRaw("pnl %", profit, length)
}
canvas.YAxis = chart.YAxis{
ValueFormatter: func(v interface{}) string {
if vf, isFloat := v.(float64); isFloat {
return fmt.Sprintf("%.4f", vf)
}
return ""
},
}
canvas.PlotRaw("1", types.NumberSeries(1), length)
return canvas
}
func (s *Strategy) DrawCumPNL(cumProfit types.Series) *types.Canvas {
canvas := types.NewCanvas(s.InstanceID())
canvas.PlotRaw("cummulative pnl", cumProfit, cumProfit.Length())
canvas.YAxis = chart.YAxis{
ValueFormatter: func(v interface{}) string {
if vf, isFloat := v.(float64); isFloat {
return fmt.Sprintf("%.4f", vf)
}
return ""
},
}
return canvas
}
func (s *Strategy) Draw(time types.Time, profit types.Series, cumProfit types.Series) {
canvas := s.DrawIndicators(time)
f, err := os.Create(s.CanvasPath)
if err != nil {
log.WithError(err).Errorf("cannot create on %s", s.CanvasPath)
return
}
defer f.Close()
if err := canvas.Render(chart.PNG, f); err != nil {
log.WithError(err).Errorf("cannot render in drift")
}
canvas = s.DrawPNL(profit)
f, err = os.Create(s.GraphPNLPath)
if err != nil {
log.WithError(err).Errorf("open pnl")
return
}
defer f.Close()
if err := canvas.Render(chart.PNG, f); err != nil {
log.WithError(err).Errorf("render pnl")
}
canvas = s.DrawCumPNL(cumProfit)
f, err = os.Create(s.GraphCumPNLPath)
if err != nil {
log.WithError(err).Errorf("open cumpnl")
return
}
defer f.Close()
if err := canvas.Render(chart.PNG, f); err != nil {
log.WithError(err).Errorf("render cumpnl")
}
}
// Sending new rebalance orders cost too much. // Sending new rebalance orders cost too much.
// Modify the position instead to expect the strategy itself rebalance on Close // Modify the position instead to expect the strategy itself rebalance on Close
func (s *Strategy) Rebalance(ctx context.Context) { func (s *Strategy) Rebalance(ctx context.Context) {
@ -548,7 +460,7 @@ func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value {
return balances[s.Market.BaseCurrency].Total().Mul(price).Add(balances[s.Market.QuoteCurrency].Total()) return balances[s.Market.BaseCurrency].Total().Mul(price).Add(balances[s.Market.QuoteCurrency].Total())
} }
func (s *Strategy) klineHandlerMin(ctx context.Context, kline types.KLine) { func (s *Strategy) klineHandlerMin(ctx context.Context, kline types.KLine, counter int) {
s.klineMin.Set(&kline) s.klineMin.Set(&kline)
if s.Status != types.StrategyStatusRunning { if s.Status != types.StrategyStatusRunning {
return return
@ -571,7 +483,7 @@ func (s *Strategy) klineHandlerMin(ctx context.Context, kline types.KLine) {
numPending := 0 numPending := 0
var err error var err error
if numPending, err = s.smartCancel(ctx, pricef, atr); err != nil { if numPending, err = s.smartCancel(ctx, pricef, atr, counter); err != nil {
log.WithError(err).Errorf("cannot cancel orders") log.WithError(err).Errorf("cannot cancel orders")
return return
} }
@ -589,28 +501,37 @@ func (s *Strategy) klineHandlerMin(ctx context.Context, kline types.KLine) {
} }
} }
func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) { func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine, counter int) {
start := time.Now()
defer func() {
end := time.Now()
elapsed := end.Sub(start)
s.elapsed.Update(float64(elapsed) / 1000000)
}()
s.frameKLine.Set(&kline) s.frameKLine.Set(&kline)
source := s.GetSource(&kline) 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)
s.trendLine.Update(sourcef) s.trendLine.Update(sourcef)
s.drift.Update(sourcef, kline.Volume.Abs().Float64()) s.drift.Update(sourcef, kline.Volume.Abs().Float64())
s.atr.PushK(kline) s.atr.PushK(kline)
atr := s.atr.Last() atr := s.atr.Last()
price := s.getLastPrice()
price := kline.Close //s.getLastPrice()
pricef := price.Float64() pricef := price.Float64()
lowf := math.Min(kline.Low.Float64(), pricef) lowf := math.Min(kline.Low.Float64(), pricef)
highf := math.Max(kline.High.Float64(), pricef) highf := math.Max(kline.High.Float64(), pricef)
lowdiff := s.ma.Last() - lowf lowdiff := pricef - lowf
s.stdevLow.Update(lowdiff) s.stdevLow.Update(lowdiff)
highdiff := highf - s.ma.Last() highdiff := highf - pricef
s.stdevHigh.Update(highdiff) s.stdevHigh.Update(highdiff)
drift := s.drift.Array(2) drift := s.drift.Array(2)
if len(drift) < 2 || len(drift) < s.PredictOffset { if len(drift) < 2 || len(drift) < s.PredictOffset {
return return
} }
@ -623,7 +544,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
return return
} }
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) log.Infof("highdiff: %3.2f open: %8v, close: %8v, high: %8v, low: %8v, time: %v %v", s.stdevHigh.Last(), kline.Open, kline.Close, kline.High, kline.Low, kline.StartTime, kline.EndTime)
s.positionLock.Lock() s.positionLock.Lock()
if s.lowestPrice > 0 && lowf < s.lowestPrice { if s.lowestPrice > 0 && lowf < s.lowestPrice {
@ -667,14 +588,14 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
if exitCondition || longCondition || shortCondition { if exitCondition || longCondition || shortCondition {
var err error var err error
var hold int var hold int
if hold, err = s.smartCancel(ctx, sourcef, atr); err != nil { if hold, err = s.smartCancel(ctx, pricef, atr, counter); err != nil {
log.WithError(err).Errorf("cannot cancel orders") log.WithError(err).Errorf("cannot cancel orders")
} }
if hold > 0 { if hold > 0 {
return return
} }
} else { } else {
if _, err := s.smartCancel(ctx, sourcef, atr); err != nil { if _, err := s.smartCancel(ctx, pricef, atr, counter); err != nil {
log.WithError(err).Errorf("cannot cancel orders") log.WithError(err).Errorf("cannot cancel orders")
} }
return return
@ -692,11 +613,12 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
opt.Long = true opt.Long = true
opt.LimitOrder = true opt.LimitOrder = true
// force to use market taker // force to use market taker
if s.counter-s.maxCounterBuyCanceled <= 1 { if counter-s.maxCounterBuyCanceled <= s.PendingMinInterval {
opt.LimitOrder = false opt.LimitOrder = false
} }
opt.Price = source opt.Price = source
opt.Tags = []string{"long"} opt.Tags = []string{"long"}
createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt) createdOrders, err := s.GeneralOrderExecutor.OpenPosition(ctx, opt)
if err != nil { if err != nil {
errs := filterErrors(multierr.Errors(err)) errs := filterErrors(multierr.Errors(err))
@ -706,9 +628,16 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
} }
return return
} }
log.Infof("orders %v", createdOrders) log.Infof("orders %v", createdOrders)
if createdOrders != nil { if createdOrders != nil {
s.orderPendingCounter[createdOrders[0].OrderID] = s.counter for _, o := range createdOrders {
if o.Status == types.OrderStatusNew || o.Status == types.OrderStatusPartiallyFilled {
s.pendingLock.Lock()
s.orderPendingCounter[o.OrderID] = counter
s.pendingLock.Unlock()
}
}
} }
return return
} }
@ -724,7 +653,7 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
opt.Short = true opt.Short = true
opt.Price = source opt.Price = source
opt.LimitOrder = true opt.LimitOrder = true
if s.counter-s.maxCounterSellCanceled <= 1 { if counter-s.maxCounterSellCanceled <= s.PendingMinInterval {
opt.LimitOrder = false opt.LimitOrder = false
} }
opt.Tags = []string{"short"} opt.Tags = []string{"short"}
@ -738,7 +667,13 @@ func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
} }
log.Infof("orders %v", createdOrders) log.Infof("orders %v", createdOrders)
if createdOrders != nil { if createdOrders != nil {
s.orderPendingCounter[createdOrders[0].OrderID] = s.counter for _, o := range createdOrders {
if o.Status == types.OrderStatusNew || o.Status == types.OrderStatusPartiallyFilled {
s.pendingLock.Lock()
s.orderPendingCounter[o.OrderID] = counter
s.pendingLock.Unlock()
}
}
} }
return return
} }
@ -787,7 +722,6 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.GeneralOrderExecutor.Bind() s.GeneralOrderExecutor.Bind()
s.orderPendingCounter = make(map[uint64]int) s.orderPendingCounter = make(map[uint64]int)
s.counter = 0
// Exit methods from config // Exit methods from config
for _, method := range s.ExitMethods { for _, method := range s.ExitMethods {
@ -845,47 +779,14 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
s.frameKLine = &types.KLine{} s.frameKLine = &types.KLine{}
s.klineMin = &types.KLine{} s.klineMin = &types.KLine{}
s.priceLines = types.NewQueue(300) s.priceLines = types.NewQueue(300)
s.elapsed = types.NewQueue(60000)
s.initTickerFunctions(ctx) s.initTickerFunctions(ctx)
s.startTime = s.Environment.StartTime() s.startTime = s.Environment.StartTime()
s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, s.startTime)) s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, s.startTime))
s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, s.startTime)) s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, s.startTime))
bbgo.RegisterCommand("/draw", "Draw Indicators", func(reply interact.Reply) { s.InitDrawCommands(&profit, &cumProfit)
go func() {
canvas := s.DrawIndicators(s.frameKLine.StartTime)
var buffer bytes.Buffer
if err := canvas.Render(chart.PNG, &buffer); err != nil {
log.WithError(err).Errorf("cannot render indicators in drift")
return
}
bbgo.SendPhoto(&buffer)
}()
})
bbgo.RegisterCommand("/pnl", "Draw PNL(%) per trade", func(reply interact.Reply) {
go func() {
canvas := s.DrawPNL(&profit)
var buffer bytes.Buffer
if err := canvas.Render(chart.PNG, &buffer); err != nil {
log.WithError(err).Errorf("cannot render pnl in drift")
return
}
bbgo.SendPhoto(&buffer)
}()
})
bbgo.RegisterCommand("/cumpnl", "Draw Cummulative PNL(Quote)", func(reply interact.Reply) {
go func() {
canvas := s.DrawCumPNL(&cumProfit)
var buffer bytes.Buffer
if err := canvas.Render(chart.PNG, &buffer); err != nil {
log.WithError(err).Errorf("cannot render cumpnl in drift")
return
}
bbgo.SendPhoto(&buffer)
}()
})
bbgo.RegisterCommand("/config", "Show latest config", func(reply interact.Reply) { bbgo.RegisterCommand("/config", "Show latest config", func(reply interact.Reply) {
var buffer bytes.Buffer var buffer bytes.Buffer
@ -922,12 +823,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
return nil return nil
} }
//var lastK types.KLine
store.OnKLineClosed(func(kline types.KLine) { store.OnKLineClosed(func(kline types.KLine) {
s.counter = int(kline.StartTime.Time().Add(kline.Interval.Duration()).Sub(s.startTime).Milliseconds()) / s.MinInterval.Milliseconds() counter := int(kline.StartTime.Time().Add(kline.Interval.Duration()).Sub(s.startTime).Milliseconds()) / s.MinInterval.Milliseconds()
if kline.Interval == s.Interval { if kline.Interval == s.Interval {
s.klineHandler(ctx, kline) s.klineHandler(ctx, kline, counter)
} else if kline.Interval == s.MinInterval { } else if kline.Interval == s.MinInterval {
s.klineHandlerMin(ctx, kline) s.klineHandlerMin(ctx, kline, counter)
} }
}) })

View File

@ -16,7 +16,7 @@ func (s *Strategy) InitDrawCommands(store *bbgo.SerialMarketDataStore, profit, c
go func() { go func() {
canvas := s.DrawIndicators(store) canvas := s.DrawIndicators(store)
if canvas == nil { if canvas == nil {
reply.Message("cannot render indicators") reply.Send("cannot render indicators")
return return
} }
var buffer bytes.Buffer var buffer bytes.Buffer

View File

@ -145,7 +145,7 @@ func (inc *CCISTOCH) Update(cloze float64) {
func (inc *CCISTOCH) BuySignal() bool { func (inc *CCISTOCH) BuySignal() bool {
hasGrey := false hasGrey := false
for i := 0; i < len(inc.ma.Values); i++ { for i := 0; i < inc.ma.Values.Length(); i++ {
v := inc.ma.Index(i) v := inc.ma.Index(i)
if v > inc.filterHigh { if v > inc.filterHigh {
return false return false
@ -161,7 +161,7 @@ func (inc *CCISTOCH) BuySignal() bool {
func (inc *CCISTOCH) SellSignal() bool { func (inc *CCISTOCH) SellSignal() bool {
hasGrey := false hasGrey := false
for i := 0; i < len(inc.ma.Values); i++ { for i := 0; i < inc.ma.Values.Length(); i++ {
v := inc.ma.Index(i) v := inc.ma.Index(i)
if v < inc.filterLow { if v < inc.filterLow {
return false return false