diff --git a/pkg/strategy/factorzoo/linear_regression.go b/pkg/strategy/factorzoo/linear_regression.go index 7cea50cf2..d3986a34e 100644 --- a/pkg/strategy/factorzoo/linear_regression.go +++ b/pkg/strategy/factorzoo/linear_regression.go @@ -2,6 +2,7 @@ package factorzoo import ( "context" + "github.com/c9s/bbgo/pkg/bbgo" "github.com/c9s/bbgo/pkg/fixedpoint" "github.com/c9s/bbgo/pkg/indicator" @@ -22,12 +23,11 @@ type Linear struct { StopEMA *types.IntervalWindow `json:"stopEMA"` // Xs (input), factors & indicators - divergence *factorzoo.AVD // amplitude volume divergence - reversion *factorzoo.PMR // price mean reversion - momentum *factorzoo.MOM // price momentum from WorldQuant's paper, alpha 101 - drift *indicator.Drift + divergence *factorzoo.PVD // price volume divergence + reversion *factorzoo.PMR // price mean reversion + momentum *factorzoo.MOM // price momentum from paper, alpha 101 + drift *indicator.Drift // GBM volume *factorzoo.VMOM // quarterly volume momentum - bars *factorzoo.LSBAR // long short bar accumulation // Y (output), internal rate of return irr *factorzoo.RR @@ -51,28 +51,22 @@ func (s *Linear) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.General symbol := position.Symbol store, _ := session.MarketDataStore(symbol) - s.divergence = &factorzoo.AVD{IntervalWindow: types.IntervalWindow{Window: s.Window, Interval: s.Interval}} + // initialize factor indicators + s.divergence = &factorzoo.PVD{IntervalWindow: types.IntervalWindow{Window: 60, Interval: s.Interval}} s.divergence.Bind(store) - preloadDivergence(s.divergence, store) - s.reversion = &factorzoo.PMR{IntervalWindow: types.IntervalWindow{Window: s.Window, Interval: s.Interval}} + s.reversion = &factorzoo.PMR{IntervalWindow: types.IntervalWindow{Window: 60, Interval: s.Interval}} s.reversion.Bind(store) - preloadReversion(s.reversion, store) - s.drift = &indicator.Drift{IntervalWindow: types.IntervalWindow{Window: 5, Interval: s.Interval}} + s.drift = &indicator.Drift{IntervalWindow: types.IntervalWindow{Window: 7, Interval: s.Interval}} s.drift.Bind(store) - preloadDrift(s.drift, store) s.momentum = &factorzoo.MOM{IntervalWindow: types.IntervalWindow{Window: 1, Interval: s.Interval}} s.momentum.Bind(store) - preloadMomentum(s.momentum, store) s.volume = &factorzoo.VMOM{IntervalWindow: types.IntervalWindow{Window: 90, Interval: s.Interval}} s.volume.Bind(store) - preloadVolume(s.volume, store) - s.bars = &factorzoo.LSBAR{IntervalWindow: types.IntervalWindow{Window: 360, Interval: s.Interval}} - s.bars.Bind(store) - preloadBars(s.bars, store) s.irr = &factorzoo.RR{IntervalWindow: types.IntervalWindow{Window: 2, Interval: s.Interval}} s.irr.Bind(store) - preloadIRR(s.irr, store) + + predLst := types.NewQueue(s.Window) session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) { ctx := context.Background() @@ -80,14 +74,20 @@ func (s *Linear) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.General // graceful cancel all active orders _ = orderExecutor.GracefulCancel(ctx) - a := []types.Float64Slice{s.divergence.Values[len(s.divergence.Values)-s.Window : len(s.divergence.Values)-2], - s.reversion.Values[len(s.reversion.Values)-s.Window : len(s.reversion.Values)-2], - s.drift.Values[len(s.drift.Values)-s.Window : len(s.drift.Values)-2], - s.momentum.Values[len(s.momentum.Values)-s.Window : len(s.momentum.Values)-2], - s.volume.Values[len(s.volume.Values)-s.Window : len(s.volume.Values)-2], - //s.bars.Values[len(s.bars.Values)-s.Window : len(s.bars.Values)-2], + // take past window days' values to predict future return + // (e.g., 5 here in default configuration file) + a := []types.Float64Slice{ + s.divergence.Values[len(s.divergence.Values)-s.Window-2 : len(s.divergence.Values)-2], + s.reversion.Values[len(s.reversion.Values)-s.Window-2 : len(s.reversion.Values)-2], + s.drift.Values[len(s.drift.Values)-s.Window-2 : len(s.drift.Values)-2], + s.momentum.Values[len(s.momentum.Values)-s.Window-2 : len(s.momentum.Values)-2], + s.volume.Values[len(s.volume.Values)-s.Window-2 : len(s.volume.Values)-2], } - b := []types.Float64Slice{filter(s.irr.Values[len(s.irr.Values)-(s.Window-1):len(s.irr.Values)-(2-1)], binary)} + // e.g., s.window is 5 + // factors array from day -4 to day 0, [[0.1, 0.2, 0.35, 0.3 , 0.25], [1.1, -0.2, 1.35, -0.3 , -0.25], ...] + // the binary(+/-) daily return rate from day -3 to day 1, [0, 1, 1, 0, 0] + // then we take the latest available factors array into linear regression model + b := []types.Float64Slice{filter(s.irr.Values[len(s.irr.Values)-s.Window-1:len(s.irr.Values)-1], binary)} var x []types.Series var y []types.Series @@ -99,37 +99,53 @@ func (s *Linear) Bind(session *bbgo.ExchangeSession, orderExecutor *bbgo.General //x = append(x, &a[5]) y = append(y, &b[0]) - //log.Infof("actual: %f", y[0]) - model := types.LogisticRegression(x, y[0], s.Window, 8000, 0.0001) + // use the last value from indicators, or the SeriesExtends' predict function. (e.g., look back: 5) input := []float64{ - s.divergence.Predict(5, 20), - s.reversion.Predict(5, 20), - s.drift.Predict(5, 20), - s.momentum.Predict(5, 20), - s.volume.Predict(5, 20), - //s.bars.Predict(5, 20), + s.divergence.Last(), + s.reversion.Last(), + s.drift.Last(), + s.momentum.Last(), + s.volume.Last(), } - //log.Info(input) pred := model.Predict(input) - //log.Infof("prediction: %f", pred) - //qty := s.QuantityOrAmount.CalculateQuantity(kline.Close) - if pred > 0.5 { - if position.IsShort() && !position.IsDust(kline.Close) { + predLst.Update(pred) + + qty := s.Quantity //s.QuantityOrAmount.CalculateQuantity(kline.Close) + + // the scale of pred is from 0.0 to 1.0 + // 0.5 can be used as the threshold + // we use the time-series rolling prediction values here + if pred > predLst.Mean() { + if position.IsShort() { s.ClosePosition(ctx, one) - s.placeMarketOrder(ctx, types.SideTypeBuy, s.Quantity, symbol) - } else if position.IsClosed() || position.IsDust(kline.Close) { - s.placeMarketOrder(ctx, types.SideTypeBuy, s.Quantity, symbol) + s.placeMarketOrder(ctx, types.SideTypeBuy, qty, symbol) + } else if position.IsClosed() { + s.placeMarketOrder(ctx, types.SideTypeBuy, qty, symbol) } - } else if pred < 0.5 { - if position.IsLong() && !position.IsDust(kline.Close) { + } else if pred < predLst.Mean() { + if position.IsLong() { s.ClosePosition(ctx, one) - s.placeMarketOrder(ctx, types.SideTypeSell, s.Quantity, symbol) - } else if position.IsClosed() || position.IsDust(kline.Close) { - s.placeMarketOrder(ctx, types.SideTypeSell, s.Quantity, symbol) + s.placeMarketOrder(ctx, types.SideTypeSell, qty, symbol) + } else if position.IsClosed() { + s.placeMarketOrder(ctx, types.SideTypeSell, qty, symbol) } } + // pass if position is opened and not dust, and remain the same direction with alpha signal + + // alpha-weighted inventory and cash + //alpha := fixedpoint.NewFromFloat(s.r1.Last()) + //targetBase := s.QuantityOrAmount.CalculateQuantity(kline.Close).Mul(alpha) + ////s.ClosePosition(ctx, one) + //diffQty := targetBase.Sub(position.Base) + //log.Info(alpha.Float64(), position.Base, diffQty.Float64()) + // + //if diffQty.Sign() > 0 { + // s.placeMarketOrder(ctx, types.SideTypeBuy, diffQty.Abs(), symbol) + //} else if diffQty.Sign() < 0 { + // s.placeMarketOrder(ctx, types.SideTypeSell, diffQty.Abs(), symbol) + //} })) if !bbgo.IsBackTesting { @@ -158,103 +174,6 @@ func (s *Linear) placeMarketOrder(ctx context.Context, side types.SideType, quan } } -func (s *Linear) placeLimitOrder(ctx context.Context, side types.SideType, quantity fixedpoint.Value, price fixedpoint.Value, symbol string) { - market, _ := s.session.Market(symbol) - _, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{ - Symbol: symbol, - Market: market, - Side: side, - Type: types.OrderTypeLimitMaker, - Quantity: quantity, - Price: price, - //TimeInForce: types.TimeInForceGTC, - Tag: "linearLimit", - }) - if err != nil { - log.WithError(err).Errorf("can not place market order") - } -} - -func preloadDivergence(divergence *factorzoo.AVD, store *bbgo.MarketDataStore) { - klines, _ := store.KLinesOfInterval(divergence.Interval) - - log.Debugf("updating divergence indicator: %d klines", len(*klines)) - - for i := 0; i < len(*klines); i++ { - divergence.Update(*klines) - } -} - -func preloadReversion(reversion *factorzoo.PMR, store *bbgo.MarketDataStore) { - klines, _ := store.KLinesOfInterval(reversion.Interval) - - log.Debugf("updating reversion indicator: %d klines", len(*klines)) - - for i := 0; i < len(*klines); i++ { - reversion.Update(*klines) - } -} - -func preloadDrift(drift *indicator.Drift, store *bbgo.MarketDataStore) { - klines, _ := store.KLinesOfInterval(drift.Interval) - - log.Debugf("updating drift indicator: %d klines", len(*klines)) - - for i := 0; i < len(*klines); i++ { - drift.CalculateAndUpdate(*klines) - } -} - -func preloadMomentum(momentum *factorzoo.MOM, store *bbgo.MarketDataStore) { - klines, _ := store.KLinesOfInterval(momentum.Interval) - - log.Debugf("updating momentum indicator: %d klines", len(*klines)) - - for i := 0; i < len(*klines); i++ { - momentum.Update(*klines) - } -} - -func preloadMomentum2(momentum *factorzoo.MOM2, store *bbgo.MarketDataStore) { - klines, _ := store.KLinesOfInterval(momentum.Interval) - - log.Debugf("updating momentum2 indicator: %d klines", len(*klines)) - - for i := 0; i < len(*klines); i++ { - momentum.Update(*klines) - } -} - -func preloadVolume(momentum *factorzoo.VMOM, store *bbgo.MarketDataStore) { - klines, _ := store.KLinesOfInterval(momentum.Interval) - - log.Debugf("updating volume momentum indicator: %d klines", len(*klines)) - - for i := 0; i < len(*klines); i++ { - momentum.Update(*klines) - } -} - -func preloadBars(bars *factorzoo.LSBAR, store *bbgo.MarketDataStore) { - klines, _ := store.KLinesOfInterval(bars.Interval) - - log.Debugf("updating long short bars indicator: %d klines", len(*klines)) - - for i := 0; i < len(*klines); i++ { - bars.Update(*klines) - } -} - -func preloadIRR(irr *factorzoo.RR, store *bbgo.MarketDataStore) { - klines, _ := store.KLinesOfInterval(irr.Interval) - - log.Debugf("updating irr indicator: %d klines", len(*klines)) - - for i := 0; i < len(*klines); i++ { - irr.CalculateAndUpdate(*klines) - } -} - func binary(val float64) float64 { if val > 0. { return 1.