195 lines
6.4 KiB
Go
195 lines
6.4 KiB
Go
package factorzoo
|
|
|
|
import (
|
|
"context"
|
|
|
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/datatype/floats"
|
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/fixedpoint"
|
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/indicator"
|
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/qbtrade"
|
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/strategy/factorzoo/factors"
|
|
"git.qtrade.icu/lychiyu/qbtrade/pkg/types"
|
|
)
|
|
|
|
type Linear struct {
|
|
Symbol string
|
|
Market types.Market `json:"-"`
|
|
types.IntervalWindow
|
|
|
|
// MarketOrder is the option to enable market order short.
|
|
MarketOrder bool `json:"marketOrder"`
|
|
|
|
Quantity fixedpoint.Value `json:"quantity"`
|
|
StopEMARange fixedpoint.Value `json:"stopEMARange"`
|
|
StopEMA *types.IntervalWindow `json:"stopEMA"`
|
|
|
|
// Xs (input), factors & indicators
|
|
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
|
|
|
|
// Y (output), internal rate of return
|
|
irr *factorzoo.RR
|
|
|
|
orderExecutor *qbtrade.GeneralOrderExecutor
|
|
session *qbtrade.ExchangeSession
|
|
activeOrders *qbtrade.ActiveOrderBook
|
|
|
|
qbtrade.QuantityOrAmount
|
|
}
|
|
|
|
func (s *Linear) Subscribe(session *qbtrade.ExchangeSession) {
|
|
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
|
|
}
|
|
|
|
func (s *Linear) Bind(session *qbtrade.ExchangeSession, orderExecutor *qbtrade.GeneralOrderExecutor) {
|
|
s.session = session
|
|
s.orderExecutor = orderExecutor
|
|
|
|
position := orderExecutor.Position()
|
|
symbol := position.Symbol
|
|
store, _ := session.MarketDataStore(symbol)
|
|
|
|
// initialize factor indicators
|
|
s.divergence = &factorzoo.PVD{IntervalWindow: types.IntervalWindow{Window: 60, Interval: s.Interval}}
|
|
s.divergence.Bind(store)
|
|
s.reversion = &factorzoo.PMR{IntervalWindow: types.IntervalWindow{Window: 60, Interval: s.Interval}}
|
|
s.reversion.Bind(store)
|
|
s.drift = &indicator.Drift{IntervalWindow: types.IntervalWindow{Window: 7, Interval: s.Interval}}
|
|
s.drift.Bind(store)
|
|
s.momentum = &factorzoo.MOM{IntervalWindow: types.IntervalWindow{Window: 1, Interval: s.Interval}}
|
|
s.momentum.Bind(store)
|
|
s.volume = &factorzoo.VMOM{IntervalWindow: types.IntervalWindow{Window: 90, Interval: s.Interval}}
|
|
s.volume.Bind(store)
|
|
|
|
s.irr = &factorzoo.RR{IntervalWindow: types.IntervalWindow{Window: 2, Interval: s.Interval}}
|
|
s.irr.Bind(store)
|
|
|
|
predLst := types.NewQueue(s.Window)
|
|
session.MarketDataStream.OnKLineClosed(types.KLineWith(symbol, s.Interval, func(kline types.KLine) {
|
|
|
|
ctx := context.Background()
|
|
|
|
// graceful cancel all active orders
|
|
_ = orderExecutor.GracefulCancel(ctx)
|
|
|
|
// take past window days' values to predict future return
|
|
// (e.g., 5 here in default configuration file)
|
|
a := []floats.Slice{
|
|
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],
|
|
}
|
|
// 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 := []floats.Slice{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
|
|
|
|
x = append(x, &a[0])
|
|
x = append(x, &a[1])
|
|
x = append(x, &a[2])
|
|
x = append(x, &a[3])
|
|
x = append(x, &a[4])
|
|
//x = append(x, &a[5])
|
|
|
|
y = append(y, &b[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.Last(0),
|
|
s.reversion.Last(0),
|
|
s.drift.Last(0),
|
|
s.momentum.Last(0),
|
|
s.volume.Last(0),
|
|
}
|
|
pred := model.Predict(input)
|
|
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, qty, symbol)
|
|
} else if position.IsClosed() {
|
|
s.placeMarketOrder(ctx, types.SideTypeBuy, qty, symbol)
|
|
}
|
|
} else if pred < predLst.Mean() {
|
|
if position.IsLong() {
|
|
s.ClosePosition(ctx, one)
|
|
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 !qbtrade.IsBackTesting {
|
|
session.MarketDataStream.OnMarketTrade(func(trade types.Trade) {
|
|
})
|
|
}
|
|
}
|
|
|
|
func (s *Linear) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error {
|
|
return s.orderExecutor.ClosePosition(ctx, percentage)
|
|
}
|
|
|
|
func (s *Linear) placeMarketOrder(ctx context.Context, side types.SideType, quantity fixedpoint.Value, symbol string) {
|
|
market, _ := s.session.Market(symbol)
|
|
_, err := s.orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
|
Symbol: symbol,
|
|
Market: market,
|
|
Side: side,
|
|
Type: types.OrderTypeMarket,
|
|
Quantity: quantity,
|
|
//TimeInForce: types.TimeInForceGTC,
|
|
Tag: "linear",
|
|
})
|
|
if err != nil {
|
|
log.WithError(err).Errorf("can not place market order")
|
|
}
|
|
}
|
|
|
|
func binary(val float64) float64 {
|
|
if val > 0. {
|
|
return 1.
|
|
} else {
|
|
return 0.
|
|
}
|
|
}
|
|
|
|
func filter(data []float64, f func(float64) float64) []float64 {
|
|
fltd := make([]float64, 0)
|
|
for _, e := range data {
|
|
//if f(e) >= 0. {
|
|
fltd = append(fltd, f(e))
|
|
//}
|
|
}
|
|
return fltd
|
|
}
|