From 4f99110d2b6dfbad30f236a5e3205cb49f21b847 Mon Sep 17 00:00:00 2001 From: austin362667 Date: Mon, 19 Sep 2022 14:45:13 +0800 Subject: [PATCH] stratgy: add oneliner --- pkg/cmd/strategy/builtin.go | 1 + pkg/strategy/oneliner/neg_return_rate.go | 124 +++++++ pkg/strategy/oneliner/nrr_callbacks.go | 15 + pkg/strategy/oneliner/strategy.go | 420 +++++++++++++++++++++++ 4 files changed, 560 insertions(+) create mode 100644 pkg/strategy/oneliner/neg_return_rate.go create mode 100644 pkg/strategy/oneliner/nrr_callbacks.go create mode 100644 pkg/strategy/oneliner/strategy.go diff --git a/pkg/cmd/strategy/builtin.go b/pkg/cmd/strategy/builtin.go index f136d2f54..fdcda4d86 100644 --- a/pkg/cmd/strategy/builtin.go +++ b/pkg/cmd/strategy/builtin.go @@ -19,6 +19,7 @@ import ( _ "github.com/c9s/bbgo/pkg/strategy/grid" _ "github.com/c9s/bbgo/pkg/strategy/kline" _ "github.com/c9s/bbgo/pkg/strategy/marketcap" + _ "github.com/c9s/bbgo/pkg/strategy/oneliner" _ "github.com/c9s/bbgo/pkg/strategy/pivotshort" _ "github.com/c9s/bbgo/pkg/strategy/pricealert" _ "github.com/c9s/bbgo/pkg/strategy/pricedrop" diff --git a/pkg/strategy/oneliner/neg_return_rate.go b/pkg/strategy/oneliner/neg_return_rate.go new file mode 100644 index 000000000..2e27c4b61 --- /dev/null +++ b/pkg/strategy/oneliner/neg_return_rate.go @@ -0,0 +1,124 @@ +package oneliner + +import ( + "time" + + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/types" +) + +var zeroTime time.Time + +// simple negative internal return rate over certain timeframe(interval) + +//go:generate callbackgen -type NRR +type NRR struct { + types.IntervalWindow + types.SeriesBase + + RankingWindow int + + Prices *types.Queue + Values floats.Slice + RankedValues floats.Slice + + EndTime time.Time + + updateCallbacks []func(value float64) +} + +var _ types.SeriesExtend = &NRR{} + +func (inc *NRR) Update(price float64) { + if inc.SeriesBase.Series == nil { + inc.SeriesBase.Series = inc + inc.Prices = types.NewQueue(inc.Window) + } + inc.Prices.Update(price) + if inc.Prices.Length() < inc.Window { + return + } + irr := (inc.Prices.Last() / inc.Prices.Index(inc.Window-1)) - 1 + + inc.Values.Push(-irr) // neg ret here + inc.RankedValues.Push(inc.Rank(inc.RankingWindow).Last() / float64(inc.RankingWindow)) // ranked neg ret here + +} + +func (inc *NRR) Last() float64 { + if len(inc.Values) == 0 { + return 0 + } + + return inc.Values[len(inc.Values)-1] +} + +func (inc *NRR) Index(i int) float64 { + if i >= len(inc.Values) { + return 0 + } + + return inc.Values[len(inc.Values)-1-i] +} + +func (inc *NRR) Length() int { + return len(inc.Values) +} + +func (inc *NRR) CalculateAndUpdate(allKLines []types.KLine) { + if len(inc.Values) == 0 { + for _, k := range allKLines { + inc.PushK(k) + } + inc.EmitUpdate(inc.Last()) + } else { + k := allKLines[len(allKLines)-1] + inc.PushK(k) + inc.EmitUpdate(inc.Last()) + } +} + +func (inc *NRR) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) { + if inc.Interval != interval { + return + } + + inc.CalculateAndUpdate(window) +} + +func (inc *NRR) Bind(updater indicator.KLineWindowUpdater) { + updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate) +} + +func (inc *NRR) BindK(target indicator.KLineClosedEmitter, symbol string, interval types.Interval) { + target.OnKLineClosed(types.KLineWith(symbol, interval, inc.PushK)) +} + +func (inc *NRR) PushK(k types.KLine) { + if inc.EndTime != zeroTime && k.EndTime.Before(inc.EndTime) { + return + } + + inc.Update(indicator.KLineClosePriceMapper(k)) + inc.EndTime = k.EndTime.Time() + inc.EmitUpdate(inc.Last()) +} + +func (inc *NRR) LoadK(allKLines []types.KLine) { + for _, k := range allKLines { + inc.PushK(k) + } + inc.EmitUpdate(inc.Last()) +} + +//func calculateReturn(klines []types.KLine, window int, val KLineValueMapper) (float64, error) { +// length := len(klines) +// if length == 0 || length < window { +// return 0.0, fmt.Errorf("insufficient elements for calculating VOL with window = %d", window) +// } +// +// rate := val(klines[length-1])/val(klines[length-2]) - 1 +// +// return rate, nil +//} diff --git a/pkg/strategy/oneliner/nrr_callbacks.go b/pkg/strategy/oneliner/nrr_callbacks.go new file mode 100644 index 000000000..07b180005 --- /dev/null +++ b/pkg/strategy/oneliner/nrr_callbacks.go @@ -0,0 +1,15 @@ +// Code generated by "callbackgen -type NRR"; DO NOT EDIT. + +package oneliner + +import () + +func (inc *NRR) OnUpdate(cb func(value float64)) { + inc.updateCallbacks = append(inc.updateCallbacks, cb) +} + +func (inc *NRR) EmitUpdate(value float64) { + for _, cb := range inc.updateCallbacks { + cb(value) + } +} diff --git a/pkg/strategy/oneliner/strategy.go b/pkg/strategy/oneliner/strategy.go new file mode 100644 index 000000000..1801668a7 --- /dev/null +++ b/pkg/strategy/oneliner/strategy.go @@ -0,0 +1,420 @@ +package oneliner + +import ( + "bytes" + "context" + "errors" + "fmt" + "os" + "sync" + "time" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/datatype/floats" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/indicator" + "github.com/c9s/bbgo/pkg/interact" + "github.com/c9s/bbgo/pkg/types" + + "github.com/sirupsen/logrus" + "github.com/wcharczuk/go-chart/v2" +) + +const ID = "oneliner" + +var one = fixedpoint.One +var zero = fixedpoint.Zero +var Fee = 0.0008 // taker fee % * 2, for upper bound + +var log = logrus.WithField("strategy", ID) + +func init() { + bbgo.RegisterStrategy(ID, &Strategy{}) +} + +type IntervalWindowSetting struct { + types.IntervalWindow +} + +type Strategy struct { + Environment *bbgo.Environment + Symbol string `json:"symbol"` + Market types.Market + + types.IntervalWindow + + // persistence fields + Position *types.Position `persistence:"position"` + ProfitStats *types.ProfitStats `persistence:"profit_stats"` + TradeStats *types.TradeStats `persistence:"trade_stats"` + + activeOrders *bbgo.ActiveOrderBook + + ExitMethods bbgo.ExitMethodSet `json:"exits"` + + session *bbgo.ExchangeSession + orderExecutor *bbgo.GeneralOrderExecutor + + bbgo.QuantityOrAmount + nrr *NRR + + // StrategyController + bbgo.StrategyController + + // plotting + bbgo.SourceSelector + alpha *NRR + priceLines *types.Queue + trendLine types.UpdatableSeriesExtend + ma types.UpdatableSeriesExtend + stdevHigh *indicator.StdDev + stdevLow *indicator.StdDev + atr *indicator.ATR + midPrice fixedpoint.Value + lock sync.RWMutex `ignore:"true"` + positionLock sync.RWMutex `ignore:"true"` + startTime time.Time + minutesCounter int + orderPendingCounter map[uint64]int + frameKLine *types.KLine + kline1m *types.KLine + + beta float64 + + StopLoss fixedpoint.Value `json:"stoploss"` + CanvasPath string `json:"canvasPath"` + PredictOffset int `json:"predictOffset"` + HighLowVarianceMultiplier float64 `json:"hlVarianceMultiplier"` + NoTrailingStopLoss bool `json:"noTrailingStopLoss"` + TrailingStopLossType string `json:"trailingStopLossType"` // trailing stop sources. Possible options are `kline` for 1m kline and `realtime` from order updates + HLRangeWindow int `json:"hlRangeWindow"` + Window1m int `json:"window1m"` + FisherTransformWindow1m int `json:"fisherTransformWindow1m"` + SmootherWindow1m int `json:"smootherWindow1m"` + SmootherWindow int `json:"smootherWindow"` + FisherTransformWindow int `json:"fisherTransformWindow"` + ATRWindow int `json:"atrWindow"` + PendingMinutes int `json:"pendingMinutes"` // if order not be traded for pendingMinutes of time, cancel it. + NoRebalance bool `json:"noRebalance"` // disable rebalance + TrendWindow int `json:"trendWindow"` // trendLine is used for rebalancing the position. When trendLine goes up, hold base, otherwise hold quote + RebalanceFilter float64 `json:"rebalanceFilter"` // beta filter on the Linear Regression of trendLine + TrailingCallbackRate []float64 `json:"trailingCallbackRate"` + TrailingActivationRatio []float64 `json:"trailingActivationRatio"` + + DriftFilterNeg float64 `json:"driftFilterNeg"` + DriftFilterPos float64 `json:"driftFilterPos"` + DDriftFilterNeg float64 `json:"ddriftFilterNeg"` + DDriftFilterPos float64 `json:"ddriftFilterPos"` + + buyPrice float64 `persistence:"buy_price"` + sellPrice float64 `persistence:"sell_price"` + highestPrice float64 `persistence:"highest_price"` + lowestPrice float64 `persistence:"lowest_price"` + + // This is not related to trade but for statistics graph generation + // Will deduct fee in percentage from every trade + GraphPNLDeductFee bool `json:"graphPNLDeductFee"` + GraphPNLPath string `json:"graphPNLPath"` + GraphCumPNLPath string `json:"graphCumPNLPath"` + // Whether to generate graph when shutdown + GenerateGraph bool `json:"generateGraph"` +} + +func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval}) + + if !bbgo.IsBackTesting { + session.Subscribe(types.MarketTradeChannel, s.Symbol, types.SubscribeOptions{}) + } + + s.ExitMethods.SetAndSubscribe(session, s) +} + +func (s *Strategy) ID() string { + return ID +} + +func (s *Strategy) InstanceID() string { + return fmt.Sprintf("%s:%s", ID, s.Symbol) +} + +func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error { + var instanceID = s.InstanceID() + + if s.Position == nil { + s.Position = types.NewPositionFromMarket(s.Market) + } + + if s.ProfitStats == nil { + s.ProfitStats = types.NewProfitStats(s.Market) + } + + if s.TradeStats == nil { + s.TradeStats = types.NewTradeStats(s.Symbol) + } + + // StrategyController + s.Status = types.StrategyStatusRunning + + s.OnSuspend(func() { + // Cancel active orders + _ = s.orderExecutor.GracefulCancel(ctx) + }) + + s.OnEmergencyStop(func() { + // Cancel active orders + _ = s.orderExecutor.GracefulCancel(ctx) + // Close 100% position + //_ = s.ClosePosition(ctx, fixedpoint.One) + }) + + // initial required information + s.session = session + + s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position) + s.orderExecutor.BindEnvironment(s.Environment) + s.orderExecutor.BindProfitStats(s.ProfitStats) + s.orderExecutor.BindTradeStats(s.TradeStats) + + //modify := func(p float64) float64 { + // return p + //} + //if s.GraphPNLDeductFee { + // modify = func(p float64) float64 { + // return p * (1. - Fee) + // } + //} + profit := floats.Slice{1., 1.} + price, _ := s.session.LastPrice(s.Symbol) + initAsset := s.CalcAssetValue(price).Float64() + cumProfit := floats.Slice{initAsset, initAsset} + s.orderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _profit, netProfit fixedpoint.Value) { + profit.Update(netProfit.Float64()) + cumProfit.Update(s.CalcAssetValue(trade.Price).Float64()) + }) + s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) { + bbgo.Sync(s) + }) + s.orderExecutor.Bind() + s.activeOrders = bbgo.NewActiveOrderBook(s.Symbol) + + for _, method := range s.ExitMethods { + method.Bind(session, s.orderExecutor) + } + + store, _ := session.MarketDataStore(s.Symbol) + s.nrr = &NRR{IntervalWindow: types.IntervalWindow{Window: 2, Interval: s.Interval}, RankingWindow: s.Window} + s.nrr.Bind(store) + + //startTime := s.Environment.StartTime() + //s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1h, startTime)) + + // queued first signal as its initial process + //alphaLst := types.NewQueue(2) + + s.session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.Interval, func(kline types.KLine) { + + // transformed to [0~1] which divided equally + //alpha := s.nrr.RankedValues.Last() + + // alpha-weighted assets (inventory and capital) + targetBase := s.QuantityOrAmount.CalculateQuantity(kline.Close).Mul(fixedpoint.NewFromFloat(s.nrr.RankedValues.Index(1))) + diffQty := targetBase.Sub(s.Position.Base) + + log.Infof("decision alpah: %f, ranked negative return: %f, current position: %f, target position diff: %f", s.nrr.RankedValues.Index(1), s.nrr.RankedValues.Last(), s.Position.Base.Float64(), diffQty.Float64()) + + // use kline direction to prevent reversing position too soon + if diffQty.Sign() > 0 { // && kline.Direction() >= 0 + s.orderExecutor.OpenPosition(context.Background(), bbgo.OpenPositionOptions{Quantity: diffQty.Abs(), Long: true, MarketOrder: true}) + } else if diffQty.Sign() < 0 { // && kline.Direction() <= 0 + s.orderExecutor.OpenPosition(context.Background(), bbgo.OpenPositionOptions{Quantity: diffQty.Abs(), Short: true, MarketOrder: true}) + } + + // important: delayed signal in order to submit order at current kline close (a.k.a. next open while in production) + // instead of right in current kline open + //alphaLst.Update(alpha) + })) + + bbgo.RegisterCommand("/draw", "Draw Indicators", func(reply interact.Reply) { + 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 oneliner") + reply.Message(fmt.Sprintf("[error] cannot render indicators in drift: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }) + + bbgo.RegisterCommand("/pnl", "Draw PNL(%) per trade", func(reply interact.Reply) { + 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 oneliner") + reply.Message(fmt.Sprintf("[error] cannot render pnl in drift: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }) + + bbgo.RegisterCommand("/cumpnl", "Draw Cumulative PNL(Quote)", func(reply interact.Reply) { + 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 oneliner") + reply.Message(fmt.Sprintf("[error] canot render cumpnl in drift: %v", err)) + return + } + bbgo.SendPhoto(&buffer) + }) + + bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) { + defer wg.Done() + + _, _ = fmt.Fprintln(os.Stderr, s.TradeStats.String()) + _ = s.orderExecutor.GracefulCancel(ctx) + }) + + return nil +} + +func (s *Strategy) CalcAssetValue(price fixedpoint.Value) fixedpoint.Value { + balances := s.session.GetAccount().Balances() + return balances[s.Market.BaseCurrency].Total().Mul(price).Add(balances[s.Market.QuoteCurrency].Total()) +} + +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("cumulative 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) initIndicators(store *bbgo.SerialMarketDataStore) error { + s.ma = &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.HLRangeWindow}} + s.stdevHigh = &indicator.StdDev{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.HLRangeWindow}} + s.stdevLow = &indicator.StdDev{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.HLRangeWindow}} + s.alpha = &NRR{ + IntervalWindow: types.IntervalWindow{Window: 2, Interval: s.Interval}, + } + s.alpha.SeriesBase.Series = s.alpha + s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.ATRWindow}} + s.trendLine = &indicator.EWMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.TrendWindow}} + + klines, ok := store.KLinesOfInterval(s.Interval) + klinesLength := len(*klines) + if !ok || klinesLength == 0 { + return errors.New("klines not exists") + } + for _, kline := range *klines { + source := s.GetSource(&kline).Float64() + high := kline.High.Float64() + low := kline.Low.Float64() + s.ma.Update(source) + s.stdevHigh.Update(high - s.ma.Last()) + s.stdevLow.Update(s.ma.Last() - low) + s.alpha.Update(kline.Close.Float64()) + s.trendLine.Update(source) + s.atr.PushK(kline) + s.priceLines.Update(source) + } + if s.frameKLine != nil && klines != nil { + s.frameKLine.Set(&(*klines)[len(*klines)-1]) + } + klines, ok = store.KLinesOfInterval(types.Interval1m) + klinesLength = len(*klines) + if !ok || klinesLength == 0 { + return errors.New("klines not exists") + } + if s.kline1m != nil && klines != nil { + s.kline1m.Set(&(*klines)[len(*klines)-1]) + } + s.startTime = s.kline1m.StartTime.Time().Add(s.kline1m.Interval.Duration()) + return nil +} + +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.alpha.Abs().Highest(Length) + hi := s.alpha.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) + canvas.Plot("drift", s.alpha.Mul(ratio).Add(mean), time, Length) + canvas.Plot("driftOrig", s.alpha.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) 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") + } +}