mirror of
https://github.com/c9s/bbgo.git
synced 2024-09-20 08:11:08 +00:00
feature: add serialmarketdatastore, add elliottwave strategy to replace ewoDgtrd, add active cancel on general order executor, add pca
This commit is contained in:
parent
067f1274b3
commit
938dc3c497
116
config/elliottwave.yaml
Normal file
116
config/elliottwave.yaml
Normal file
|
@ -0,0 +1,116 @@
|
|||
---
|
||||
persistence:
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
db: 0
|
||||
|
||||
sessions:
|
||||
binance:
|
||||
exchange: binance
|
||||
futures: false
|
||||
envVarPrefix: binance
|
||||
heikinAshi: false
|
||||
|
||||
# Drift strategy intends to place buy/sell orders as much value mas it could be. To exchanges that requires to
|
||||
# calculate fees before placing limit orders (e.g. FTX Pro), make sure the fee rate is configured correctly and
|
||||
# enable modifyOrderAmountForFee to prevent order rejection.
|
||||
makerFeeRate: 0.0002
|
||||
takerFeeRate: 0.0007
|
||||
modifyOrderAmountForFee: false
|
||||
|
||||
exchangeStrategies:
|
||||
|
||||
- on: binance
|
||||
elliottwave:
|
||||
symbol: BTCUSDT
|
||||
# kline interval for indicators
|
||||
interval: 3m
|
||||
stoploss: 0.15%
|
||||
windowATR: 14
|
||||
windowQuick: 3
|
||||
windowSlow: 19
|
||||
source: hl2
|
||||
pendingMinutes: 8
|
||||
|
||||
|
||||
# ActivationRatio should be increasing order
|
||||
# when farest price from entry goes over that ratio, start using the callback ratio accordingly to do trailingstop
|
||||
#trailingActivationRatio: [0.01, 0.016, 0.05]
|
||||
#trailingActivationRatio: [0.001, 0.0081, 0.022]
|
||||
trailingActivationRatio: [0.0006, 0.0008, 0.0012, 0.0017, 0.01]
|
||||
#trailingActivationRatio: []
|
||||
#trailingCallbackRate: []
|
||||
#trailingCallbackRate: [0.002, 0.01, 0.1]
|
||||
#trailingCallbackRate: [0.0004, 0.0009, 0.018]
|
||||
trailingCallbackRate: [0.0001, 0.0002, 0.0003, 0.0006, 0.0049]
|
||||
|
||||
#exits:
|
||||
# - roiStopLoss:
|
||||
# percentage: 0.35%
|
||||
#- roiTakeProfit:
|
||||
# percentage: 0.7%
|
||||
#- protectiveStopLoss:
|
||||
# activationRatio: 0.5%
|
||||
# stopLossRatio: 0.2%
|
||||
# placeStopOrder: false
|
||||
#- trailingStop:
|
||||
# callbackRate: 0.3%
|
||||
# activationRatio is relative to the average cost,
|
||||
# when side is buy, 1% means lower 1% than the average cost.
|
||||
# when side is sell, 1% means higher 1% than the average cost.
|
||||
# activationRatio: 0.7%
|
||||
# minProfit uses the position ROI to calculate the profit ratio
|
||||
|
||||
# minProfit: 1.5%
|
||||
# interval: 1m
|
||||
# side: sell
|
||||
# closePosition: 100%
|
||||
|
||||
#- trailingStop:
|
||||
# callbackRate: 0.3%
|
||||
# activationRatio is relative to the average cost,
|
||||
# when side is buy, 1% means lower 1% than the average cost.
|
||||
# when side is sell, 1% means higher 1% than the average cost.
|
||||
# activationRatio: 0.7%
|
||||
# minProfit uses the position ROI to calculate the profit ratio
|
||||
|
||||
# minProfit: 1.5%
|
||||
# interval: 1m
|
||||
# side: buy
|
||||
# closePosition: 100%
|
||||
#- protectiveStopLoss:
|
||||
# activationRatio: 5%
|
||||
# stopLossRatio: 1%
|
||||
# placeStopOrder: false
|
||||
#- cumulatedVolumeTakeProfit:
|
||||
# interval: 5m
|
||||
# window: 2
|
||||
# minQuoteVolume: 200_000_000
|
||||
#- protectiveStopLoss:
|
||||
# activationRatio: 2%
|
||||
# stopLossRatio: 1%
|
||||
# placeStopOrder: false
|
||||
|
||||
sync:
|
||||
userDataStream:
|
||||
trades: true
|
||||
filledOrders: true
|
||||
sessions:
|
||||
- binance
|
||||
symbols:
|
||||
- BTCUSDT
|
||||
|
||||
backtest:
|
||||
startTime: "2022-08-30"
|
||||
endTime: "2022-09-30"
|
||||
symbols:
|
||||
- BTCUSDT
|
||||
sessions: [binance]
|
||||
accounts:
|
||||
binance:
|
||||
makerFeeRate: 0.000
|
||||
#takerFeeRate: 0.000
|
||||
balances:
|
||||
BTC: 0
|
||||
USDT: 5000
|
|
@ -3,6 +3,7 @@ package bbgo
|
|||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
@ -40,6 +41,30 @@ func (b *ActiveOrderBook) BindStream(stream types.Stream) {
|
|||
stream.OnOrderUpdate(b.orderUpdateHandler)
|
||||
}
|
||||
|
||||
func (b *ActiveOrderBook) waitClear(ctx context.Context, order types.Order, waitTime, timeout time.Duration) (bool, error) {
|
||||
if !b.Exists(order) {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
timeoutC := time.After(timeout)
|
||||
for {
|
||||
time.Sleep(waitTime)
|
||||
clear := !b.Exists(order)
|
||||
select {
|
||||
case <-timeoutC:
|
||||
return clear, nil
|
||||
|
||||
case <-ctx.Done():
|
||||
return clear, ctx.Err()
|
||||
|
||||
default:
|
||||
if clear {
|
||||
return clear, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *ActiveOrderBook) waitAllClear(ctx context.Context, waitTime, timeout time.Duration) (bool, error) {
|
||||
numOfOrders := b.NumOfOrders()
|
||||
clear := numOfOrders == 0
|
||||
|
@ -67,6 +92,55 @@ func (b *ActiveOrderBook) waitAllClear(ctx context.Context, waitTime, timeout ti
|
|||
}
|
||||
}
|
||||
|
||||
// Cancel cancels the given order from activeOrderBook gracefully
|
||||
func (b *ActiveOrderBook) Cancel(ctx context.Context, ex types.Exchange, order types.Order) error {
|
||||
if !b.Exists(order) {
|
||||
return fmt.Errorf("cannot find %v in orderbook", order)
|
||||
}
|
||||
// optimize order cancel for back-testing
|
||||
if IsBackTesting {
|
||||
return ex.CancelOrders(context.Background(), order)
|
||||
}
|
||||
log.Debugf("[ActiveOrderBook] gracefully cancelling %s order...", order.OrderID)
|
||||
waitTime := CancelOrderWaitTime
|
||||
|
||||
startTime := time.Now()
|
||||
// ensure order is cancelled
|
||||
for {
|
||||
// Some orders in the variable are not created on the server side yet,
|
||||
// If we cancel these orders directly, we will get an unsent order error
|
||||
// We wait here for a while for server to create these orders.
|
||||
// time.Sleep(SentOrderWaitTime)
|
||||
|
||||
// since ctx might be canceled, we should use background context here
|
||||
|
||||
if err := ex.CancelOrders(context.Background(), order); err != nil {
|
||||
log.WithError(err).Errorf("[ActiveORderBook] can not cancel %s order", order.OrderID)
|
||||
}
|
||||
log.Debugf("[ActiveOrderBook] waiting %s for %s order to be cancelled...", waitTime, order.OrderID)
|
||||
clear, err := b.waitClear(ctx, order, waitTime, 5*time.Second)
|
||||
if clear || err != nil {
|
||||
break
|
||||
}
|
||||
b.Print()
|
||||
|
||||
openOrders, err := ex.QueryOpenOrders(ctx, order.Symbol)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("can not query %s open orders", order.Symbol)
|
||||
continue
|
||||
}
|
||||
|
||||
openOrderStore := NewOrderStore(order.Symbol)
|
||||
openOrderStore.Add(openOrders...)
|
||||
// if it's not on the order book (open orders), we should remove it from our local side
|
||||
if !openOrderStore.Exists(order.OrderID) {
|
||||
b.Remove(order)
|
||||
}
|
||||
}
|
||||
log.Debugf("[ActiveOrderBook] %s order is cancelled successfully in %s", order.OrderID, b.Symbol, time.Since(startTime))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GracefulCancel cancels the active orders gracefully
|
||||
func (b *ActiveOrderBook) GracefulCancel(ctx context.Context, ex types.Exchange) error {
|
||||
// optimize order cancel for back-testing
|
||||
|
|
|
@ -144,6 +144,20 @@ func (e *GeneralOrderExecutor) GracefulCancelActiveOrderBook(ctx context.Context
|
|||
return nil
|
||||
}
|
||||
|
||||
func (e *GeneralOrderExecutor) Cancel(ctx context.Context, order types.Order) error {
|
||||
if e.activeMakerOrders.NumOfOrders() == 0 {
|
||||
return nil
|
||||
}
|
||||
if err := e.activeMakerOrders.Cancel(ctx, e.session.Exchange, order); err != nil {
|
||||
// Retry once
|
||||
if err = e.activeMakerOrders.Cancel(ctx, e.session.Exchange, order); err != nil {
|
||||
return fmt.Errorf("cancel order error: %w", err)
|
||||
}
|
||||
}
|
||||
e.tradeCollector.Process()
|
||||
return nil
|
||||
}
|
||||
|
||||
// GracefulCancel cancels all active maker orders
|
||||
func (e *GeneralOrderExecutor) GracefulCancel(ctx context.Context) error {
|
||||
return e.GracefulCancelActiveOrderBook(ctx, e.activeMakerOrders)
|
||||
|
|
69
pkg/bbgo/serialmarketdatastore.go
Normal file
69
pkg/bbgo/serialmarketdatastore.go
Normal file
|
@ -0,0 +1,69 @@
|
|||
package bbgo
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
type SerialMarketDataStore struct {
|
||||
*MarketDataStore
|
||||
KLines map[types.Interval]*types.KLine
|
||||
Subscription []types.Interval
|
||||
}
|
||||
|
||||
func NewSerialMarketDataStore(symbol string) *SerialMarketDataStore {
|
||||
return &SerialMarketDataStore{
|
||||
MarketDataStore: NewMarketDataStore(symbol),
|
||||
KLines: make(map[types.Interval]*types.KLine),
|
||||
Subscription: []types.Interval{},
|
||||
}
|
||||
}
|
||||
|
||||
func (store *SerialMarketDataStore) Subscribe(interval types.Interval) {
|
||||
// dedup
|
||||
for _, i := range store.Subscription {
|
||||
if i == interval {
|
||||
return
|
||||
}
|
||||
}
|
||||
store.Subscription = append(store.Subscription, interval)
|
||||
}
|
||||
|
||||
func (store *SerialMarketDataStore) BindStream(stream types.Stream) {
|
||||
stream.OnKLineClosed(store.handleKLineClosed)
|
||||
}
|
||||
|
||||
func (store *SerialMarketDataStore) handleKLineClosed(kline types.KLine) {
|
||||
store.AddKLine(kline)
|
||||
}
|
||||
|
||||
func (store *SerialMarketDataStore) AddKLine(kline types.KLine) {
|
||||
if kline.Symbol != store.Symbol {
|
||||
return
|
||||
}
|
||||
// only consumes kline1m
|
||||
if kline.Interval != types.Interval1m {
|
||||
return
|
||||
}
|
||||
// endtime in minutes
|
||||
timestamp := kline.StartTime.Time().Add(time.Minute)
|
||||
for _, val := range store.Subscription {
|
||||
k, ok := store.KLines[val]
|
||||
if !ok {
|
||||
k = &types.KLine{}
|
||||
k.Set(&kline)
|
||||
k.Interval = val
|
||||
k.Closed = false
|
||||
store.KLines[val] = k
|
||||
} else {
|
||||
k.Merge(&kline)
|
||||
k.Closed = false
|
||||
}
|
||||
if timestamp.Truncate(val.Duration()) == timestamp {
|
||||
k.Closed = true
|
||||
store.MarketDataStore.AddKLine(*k)
|
||||
delete(store.KLines, val)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -7,6 +7,7 @@ import (
|
|||
_ "github.com/c9s/bbgo/pkg/strategy/bollmaker"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/dca"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/drift"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/elliottwave"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/emastop"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/etf"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/ewoDgtrd"
|
||||
|
|
25
pkg/strategy/elliottwave/ewo.go
Normal file
25
pkg/strategy/elliottwave/ewo.go
Normal file
|
@ -0,0 +1,25 @@
|
|||
package elliottwave
|
||||
|
||||
import "github.com/c9s/bbgo/pkg/indicator"
|
||||
|
||||
type ElliottWave struct {
|
||||
maSlow *indicator.SMA
|
||||
maQuick *indicator.SMA
|
||||
}
|
||||
|
||||
func (s *ElliottWave) Index(i int) float64 {
|
||||
return s.maQuick.Index(i)/s.maSlow.Index(i) - 1.0
|
||||
}
|
||||
|
||||
func (s *ElliottWave) Last() float64 {
|
||||
return s.maQuick.Last()/s.maSlow.Last() - 1.0
|
||||
}
|
||||
|
||||
func (s *ElliottWave) Length() int {
|
||||
return s.maSlow.Length()
|
||||
}
|
||||
|
||||
func (s *ElliottWave) Update(v float64) {
|
||||
s.maSlow.Update(v)
|
||||
s.maQuick.Update(v)
|
||||
}
|
495
pkg/strategy/elliottwave/strategy.go
Normal file
495
pkg/strategy/elliottwave/strategy.go
Normal file
|
@ -0,0 +1,495 @@
|
|||
package elliottwave
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"os"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/indicator"
|
||||
"github.com/c9s/bbgo/pkg/strategy"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
"github.com/c9s/bbgo/pkg/util"
|
||||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
const ID = "elliottwave"
|
||||
|
||||
var log = logrus.WithField("strategy", ID)
|
||||
var Two fixedpoint.Value = fixedpoint.NewFromInt(2)
|
||||
var Three fixedpoint.Value = fixedpoint.NewFromInt(3)
|
||||
var Four fixedpoint.Value = fixedpoint.NewFromInt(4)
|
||||
var Delta fixedpoint.Value = fixedpoint.NewFromFloat(0.00001)
|
||||
|
||||
func init() {
|
||||
bbgo.RegisterStrategy(ID, &Strategy{})
|
||||
}
|
||||
|
||||
type SourceFunc func(*types.KLine) fixedpoint.Value
|
||||
|
||||
type Strategy struct {
|
||||
Symbol string `json:"symbol"`
|
||||
|
||||
bbgo.StrategyController
|
||||
types.Market
|
||||
strategy.SourceSelector
|
||||
Session *bbgo.ExchangeSession
|
||||
|
||||
Interval types.Interval `json:"interval"`
|
||||
Stoploss fixedpoint.Value `json:"stoploss"`
|
||||
WindowATR int `json:"windowATR"`
|
||||
WindowQuick int `json:"windowQuick"`
|
||||
WindowSlow int `json:"windowSlow"`
|
||||
PendingMinutes int `json:"pendingMinutes"`
|
||||
|
||||
*bbgo.Environment
|
||||
*bbgo.GeneralOrderExecutor
|
||||
*types.Position `persistence:"position"`
|
||||
*types.ProfitStats `persistence:"profit_stats"`
|
||||
*types.TradeStats `persistence:"trade_stats"`
|
||||
|
||||
ewo *ElliottWave
|
||||
atr *indicator.ATR
|
||||
|
||||
getLastPrice func() fixedpoint.Value
|
||||
|
||||
// for smart cancel
|
||||
orderPendingCounter map[uint64]int
|
||||
startTime time.Time
|
||||
minutesCounter int
|
||||
|
||||
// for position
|
||||
buyPrice float64 `persistence:"buy_price"`
|
||||
sellPrice float64 `persistence:"sell_price"`
|
||||
highestPrice float64 `persistence:"highest_price"`
|
||||
lowestPrice float64 `persistence:"lowest_price"`
|
||||
|
||||
TrailingCallbackRate []float64 `json:"trailingCallbackRate"`
|
||||
TrailingActivationRatio []float64 `json:"trailingActivationRatio"`
|
||||
ExitMethods bbgo.ExitMethodSet `json:"exits"`
|
||||
|
||||
midPrice fixedpoint.Value
|
||||
lock sync.RWMutex `ignore:"true"`
|
||||
}
|
||||
|
||||
func (s *Strategy) ID() string {
|
||||
return ID
|
||||
}
|
||||
|
||||
func (s *Strategy) InstanceID() string {
|
||||
return fmt.Sprintf("%s:%s:%v", ID, s.Symbol, bbgo.IsBackTesting)
|
||||
}
|
||||
|
||||
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
||||
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{
|
||||
Interval: types.Interval1m,
|
||||
})
|
||||
if !bbgo.IsBackTesting {
|
||||
session.Subscribe(types.BookTickerChannel, s.Symbol, types.SubscribeOptions{})
|
||||
}
|
||||
s.ExitMethods.SetAndSubscribe(session, s)
|
||||
}
|
||||
|
||||
func (s *Strategy) CurrentPosition() *types.Position {
|
||||
return s.Position
|
||||
}
|
||||
|
||||
func (s *Strategy) ClosePosition(ctx context.Context, percentage fixedpoint.Value) error {
|
||||
order := s.Position.NewMarketCloseOrder(percentage)
|
||||
if order == nil {
|
||||
return nil
|
||||
}
|
||||
order.Tag = "close"
|
||||
order.TimeInForce = ""
|
||||
balances := s.GeneralOrderExecutor.Session().GetAccount().Balances()
|
||||
baseBalance := balances[s.Market.BaseCurrency].Available
|
||||
price := s.getLastPrice()
|
||||
if order.Side == types.SideTypeBuy {
|
||||
quoteAmount := balances[s.Market.QuoteCurrency].Available.Div(price)
|
||||
if order.Quantity.Compare(quoteAmount) > 0 {
|
||||
order.Quantity = quoteAmount
|
||||
}
|
||||
} else if order.Side == types.SideTypeSell && order.Quantity.Compare(baseBalance) > 0 {
|
||||
order.Quantity = baseBalance
|
||||
}
|
||||
for {
|
||||
if s.Market.IsDustQuantity(order.Quantity, price) {
|
||||
return nil
|
||||
}
|
||||
_, err := s.GeneralOrderExecutor.SubmitOrders(ctx, *order)
|
||||
if err != nil {
|
||||
order.Quantity = order.Quantity.Mul(fixedpoint.One.Sub(Delta))
|
||||
continue
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func (s *Strategy) initIndicators(store *bbgo.SerialMarketDataStore) error {
|
||||
maSlow := &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.WindowSlow}}
|
||||
maQuick := &indicator.SMA{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.WindowQuick}}
|
||||
s.ewo = &ElliottWave{
|
||||
maSlow, maQuick,
|
||||
}
|
||||
s.atr = &indicator.ATR{IntervalWindow: types.IntervalWindow{Interval: s.Interval, Window: s.WindowATR}}
|
||||
klines, ok := store.KLinesOfInterval(s.Interval)
|
||||
klineLength := len(*klines)
|
||||
if !ok || klineLength == 0 {
|
||||
return errors.New("klines not exists")
|
||||
}
|
||||
s.startTime = (*klines)[klineLength-1].EndTime.Time()
|
||||
|
||||
for _, kline := range *klines {
|
||||
source := s.GetSource(&kline).Float64()
|
||||
s.ewo.Update(source)
|
||||
s.atr.PushK(kline)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// FIXME: stdevHigh
|
||||
func (s *Strategy) smartCancel(ctx context.Context, pricef float64) int {
|
||||
nonTraded := s.GeneralOrderExecutor.ActiveMakerOrders().Orders()
|
||||
if len(nonTraded) > 0 {
|
||||
left := 0
|
||||
for _, order := range nonTraded {
|
||||
toCancel := false
|
||||
if s.minutesCounter-s.orderPendingCounter[order.OrderID] >= s.PendingMinutes {
|
||||
toCancel = true
|
||||
} else if order.Side == types.SideTypeBuy {
|
||||
if order.Price.Float64()+s.atr.Last()*2 <= pricef {
|
||||
toCancel = true
|
||||
}
|
||||
} else if order.Side == types.SideTypeSell {
|
||||
// 75% of the probability
|
||||
if order.Price.Float64()-s.atr.Last()*2 >= pricef {
|
||||
toCancel = true
|
||||
}
|
||||
} else {
|
||||
panic("not supported side for the order")
|
||||
}
|
||||
if toCancel {
|
||||
err := s.GeneralOrderExecutor.Cancel(ctx, order)
|
||||
if err == nil {
|
||||
delete(s.orderPendingCounter, order.OrderID)
|
||||
} else {
|
||||
log.WithError(err).Errorf("failed to cancel %v", order.OrderID)
|
||||
}
|
||||
log.Warnf("cancel %v", order.OrderID)
|
||||
} else {
|
||||
left += 1
|
||||
}
|
||||
}
|
||||
return left
|
||||
}
|
||||
return len(nonTraded)
|
||||
}
|
||||
|
||||
func (s *Strategy) trailingCheck(price float64, direction string) bool {
|
||||
if s.highestPrice > 0 && s.highestPrice < price {
|
||||
s.highestPrice = price
|
||||
}
|
||||
if s.lowestPrice > 0 && s.lowestPrice < price {
|
||||
s.lowestPrice = price
|
||||
}
|
||||
isShort := direction == "short"
|
||||
for i := len(s.TrailingCallbackRate) - 1; i >= 0; i-- {
|
||||
trailingCallbackRate := s.TrailingCallbackRate[i]
|
||||
trailingActivationRatio := s.TrailingActivationRatio[i]
|
||||
if isShort {
|
||||
if (s.sellPrice-s.lowestPrice)/s.lowestPrice > trailingActivationRatio {
|
||||
return (price-s.lowestPrice)/s.lowestPrice > trailingCallbackRate
|
||||
}
|
||||
} else {
|
||||
if (s.highestPrice-s.buyPrice)/s.buyPrice > trailingActivationRatio {
|
||||
return (s.highestPrice-price)/price > trailingCallbackRate
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (s *Strategy) initTickerFunctions() {
|
||||
if s.IsBackTesting() {
|
||||
s.getLastPrice = func() fixedpoint.Value {
|
||||
lastPrice, ok := s.Session.LastPrice(s.Symbol)
|
||||
if !ok {
|
||||
log.Error("cannot get lastprice")
|
||||
}
|
||||
return lastPrice
|
||||
}
|
||||
} else {
|
||||
s.Session.MarketDataStream.OnBookTickerUpdate(func(ticker types.BookTicker) {
|
||||
bestBid := ticker.Buy
|
||||
bestAsk := ticker.Sell
|
||||
if !util.TryLock(&s.lock) {
|
||||
return
|
||||
}
|
||||
if !bestAsk.IsZero() && !bestBid.IsZero() {
|
||||
s.midPrice = bestAsk.Add(bestBid).Div(Two)
|
||||
} else if !bestAsk.IsZero() {
|
||||
s.midPrice = bestAsk
|
||||
} else if !bestBid.IsZero() {
|
||||
s.midPrice = bestBid
|
||||
}
|
||||
s.lock.Unlock()
|
||||
})
|
||||
s.getLastPrice = func() (lastPrice fixedpoint.Value) {
|
||||
var ok bool
|
||||
s.lock.RLock()
|
||||
defer s.lock.RUnlock()
|
||||
if s.midPrice.IsZero() {
|
||||
lastPrice, ok = s.Session.LastPrice(s.Symbol)
|
||||
if !ok {
|
||||
log.Error("cannot get lastprice")
|
||||
return lastPrice
|
||||
}
|
||||
} else {
|
||||
lastPrice = s.midPrice
|
||||
}
|
||||
return lastPrice
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||
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
|
||||
// Get source function from config input
|
||||
s.SourceSelector.Init()
|
||||
s.OnSuspend(func() {
|
||||
_ = s.GeneralOrderExecutor.GracefulCancel(ctx)
|
||||
})
|
||||
s.OnEmergencyStop(func() {
|
||||
_ = s.GeneralOrderExecutor.GracefulCancel(ctx)
|
||||
_ = s.ClosePosition(ctx, fixedpoint.One)
|
||||
})
|
||||
s.GeneralOrderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)
|
||||
s.GeneralOrderExecutor.BindEnvironment(s.Environment)
|
||||
s.GeneralOrderExecutor.BindProfitStats(s.ProfitStats)
|
||||
s.GeneralOrderExecutor.BindTradeStats(s.TradeStats)
|
||||
s.GeneralOrderExecutor.TradeCollector().OnPositionUpdate(func(p *types.Position) {
|
||||
bbgo.Sync(s)
|
||||
})
|
||||
s.GeneralOrderExecutor.Bind()
|
||||
|
||||
s.orderPendingCounter = make(map[uint64]int)
|
||||
s.minutesCounter = 0
|
||||
|
||||
for _, method := range s.ExitMethods {
|
||||
method.Bind(session, s.GeneralOrderExecutor)
|
||||
}
|
||||
s.GeneralOrderExecutor.TradeCollector().OnTrade(func(trade types.Trade, _profit, _netProfit fixedpoint.Value) {
|
||||
if s.Position.IsDust(trade.Price) {
|
||||
s.buyPrice = 0
|
||||
s.sellPrice = 0
|
||||
s.highestPrice = 0
|
||||
s.lowestPrice = 0
|
||||
} else if s.Position.IsLong() {
|
||||
s.buyPrice = trade.Price.Float64()
|
||||
s.sellPrice = 0
|
||||
s.highestPrice = s.buyPrice
|
||||
s.lowestPrice = 0
|
||||
} else {
|
||||
s.sellPrice = trade.Price.Float64()
|
||||
s.buyPrice = 0
|
||||
s.highestPrice = 0
|
||||
s.lowestPrice = s.sellPrice
|
||||
}
|
||||
})
|
||||
s.initTickerFunctions()
|
||||
|
||||
startTime := s.Environment.StartTime()
|
||||
s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1d, startTime))
|
||||
s.TradeStats.SetIntervalProfitCollector(types.NewIntervalProfitCollector(types.Interval1w, startTime))
|
||||
|
||||
st, _ := session.MarketDataStore(s.Symbol)
|
||||
store := bbgo.NewSerialMarketDataStore(s.Symbol)
|
||||
klines, ok := st.KLinesOfInterval(types.Interval1m)
|
||||
if !ok {
|
||||
panic("cannot get 1m history")
|
||||
}
|
||||
// event trigger order: s.Interval => Interval1m
|
||||
store.Subscribe(s.Interval)
|
||||
store.Subscribe(types.Interval1m)
|
||||
for _, kline := range *klines {
|
||||
store.AddKLine(kline)
|
||||
}
|
||||
store.OnKLineClosed(func(kline types.KLine) {
|
||||
s.minutesCounter = int(kline.StartTime.Time().Sub(s.startTime).Minutes())
|
||||
if kline.Interval == types.Interval1m {
|
||||
s.klineHandler1m(ctx, kline)
|
||||
} else if kline.Interval == s.Interval {
|
||||
s.klineHandler(ctx, kline)
|
||||
}
|
||||
})
|
||||
store.BindStream(session.MarketDataStream)
|
||||
if err := s.initIndicators(store); err != nil {
|
||||
log.WithError(err).Errorf("initIndicator failed")
|
||||
return nil
|
||||
}
|
||||
|
||||
bbgo.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
var buffer bytes.Buffer
|
||||
for _, daypnl := range s.TradeStats.IntervalProfits[types.Interval1d].GetNonProfitableIntervals() {
|
||||
fmt.Fprintf(&buffer, "%s\n", daypnl)
|
||||
}
|
||||
fmt.Fprintln(&buffer, s.TradeStats.BriefString())
|
||||
os.Stdout.Write(buffer.Bytes())
|
||||
wg.Done()
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) klineHandler1m(ctx context.Context, kline types.KLine) {
|
||||
if s.Status != types.StrategyStatusRunning {
|
||||
return
|
||||
}
|
||||
|
||||
stoploss := s.Stoploss.Float64()
|
||||
price := s.getLastPrice()
|
||||
pricef := price.Float64()
|
||||
|
||||
numPending := s.smartCancel(ctx, pricef)
|
||||
if numPending > 0 {
|
||||
log.Infof("pending orders: %d, exit", numPending)
|
||||
return
|
||||
}
|
||||
lowf := math.Min(kline.Low.Float64(), pricef)
|
||||
highf := math.Max(kline.High.Float64(), pricef)
|
||||
if s.lowestPrice > 0 && lowf < s.lowestPrice {
|
||||
s.lowestPrice = lowf
|
||||
}
|
||||
if s.highestPrice > 0 && highf > s.highestPrice {
|
||||
s.highestPrice = highf
|
||||
}
|
||||
exitShortCondition := s.sellPrice > 0 && (s.sellPrice*(1.+stoploss) <= highf ||
|
||||
s.trailingCheck(highf, "short"))
|
||||
exitLongCondition := s.buyPrice > 0 && (s.buyPrice*(1.-stoploss) >= lowf ||
|
||||
s.trailingCheck(lowf, "long"))
|
||||
|
||||
if exitShortCondition || exitLongCondition {
|
||||
_ = s.ClosePosition(ctx, fixedpoint.One)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Strategy) klineHandler(ctx context.Context, kline types.KLine) {
|
||||
source := s.GetSource(&kline)
|
||||
sourcef := source.Float64()
|
||||
s.ewo.Update(sourcef)
|
||||
s.atr.PushK(kline)
|
||||
|
||||
if s.Status != types.StrategyStatusRunning {
|
||||
return
|
||||
}
|
||||
|
||||
stoploss := s.Stoploss.Float64()
|
||||
price := s.getLastPrice()
|
||||
pricef := price.Float64()
|
||||
lowf := math.Min(kline.Low.Float64(), pricef)
|
||||
highf := math.Min(kline.High.Float64(), pricef)
|
||||
|
||||
s.smartCancel(ctx, pricef)
|
||||
|
||||
ewo := types.Array(s.ewo, 3)
|
||||
shortCondition := ewo[0] < ewo[1] && ewo[1] > ewo[2]
|
||||
longCondition := ewo[0] > ewo[1] && ewo[1] < ewo[2]
|
||||
|
||||
exitShortCondition := s.sellPrice > 0 && !shortCondition && s.sellPrice*(1.+stoploss) <= highf || s.trailingCheck(highf, "short")
|
||||
exitLongCondition := s.buyPrice > 0 && !longCondition && s.buyPrice*(1.-stoploss) >= lowf || s.trailingCheck(lowf, "long")
|
||||
|
||||
if exitShortCondition || exitLongCondition {
|
||||
if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil {
|
||||
log.WithError(err).Errorf("cannot cancel orders")
|
||||
return
|
||||
}
|
||||
s.ClosePosition(ctx, fixedpoint.One)
|
||||
}
|
||||
if longCondition {
|
||||
if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil {
|
||||
log.WithError(err).Errorf("cannot cancel orders")
|
||||
return
|
||||
}
|
||||
if source.Compare(price) > 0 {
|
||||
source = price
|
||||
sourcef = source.Float64()
|
||||
}
|
||||
balances := s.GeneralOrderExecutor.Session().GetAccount().Balances()
|
||||
quoteBalance, ok := balances[s.Market.QuoteCurrency]
|
||||
if !ok {
|
||||
log.Errorf("unable to get quoteCurrency")
|
||||
return
|
||||
}
|
||||
if s.Market.IsDustQuantity(
|
||||
quoteBalance.Available.Div(source), source) {
|
||||
return
|
||||
}
|
||||
quantity := quoteBalance.Available.Div(source)
|
||||
createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
||||
Symbol: s.Symbol,
|
||||
Side: types.SideTypeBuy,
|
||||
Type: types.OrderTypeLimit,
|
||||
Price: source,
|
||||
Quantity: quantity,
|
||||
Tag: "long",
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("cannot place buy order")
|
||||
log.Errorf("%v %v %v", quoteBalance, source, kline)
|
||||
return
|
||||
}
|
||||
s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter
|
||||
return
|
||||
}
|
||||
if shortCondition {
|
||||
if err := s.GeneralOrderExecutor.GracefulCancel(ctx); err != nil {
|
||||
log.WithError(err).Errorf("cannot cancel orders")
|
||||
return
|
||||
}
|
||||
if source.Compare(price) < 0 {
|
||||
source = price
|
||||
sourcef = price.Float64()
|
||||
}
|
||||
balances := s.GeneralOrderExecutor.Session().GetAccount().Balances()
|
||||
baseBalance, ok := balances[s.Market.BaseCurrency]
|
||||
if !ok {
|
||||
log.Errorf("unable to get baseCurrency")
|
||||
return
|
||||
}
|
||||
if s.Market.IsDustQuantity(baseBalance.Available, source) {
|
||||
return
|
||||
}
|
||||
quantity := baseBalance.Available
|
||||
createdOrders, err := s.GeneralOrderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
||||
Symbol: s.Symbol,
|
||||
Side: types.SideTypeSell,
|
||||
Type: types.OrderTypeLimit,
|
||||
Price: source,
|
||||
Quantity: quantity,
|
||||
Tag: "short",
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("cannot place sell order")
|
||||
return
|
||||
}
|
||||
s.orderPendingCounter[createdOrders[0].OrderID] = s.minutesCounter
|
||||
return
|
||||
}
|
||||
}
|
50
pkg/strategy/source.go
Normal file
50
pkg/strategy/source.go
Normal file
|
@ -0,0 +1,50 @@
|
|||
package strategy
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
log "github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
type SourceFunc func(*types.KLine) fixedpoint.Value
|
||||
|
||||
var Four fixedpoint.Value = fixedpoint.NewFromInt(4)
|
||||
var Three fixedpoint.Value = fixedpoint.NewFromInt(3)
|
||||
var Two fixedpoint.Value = fixedpoint.NewFromInt(2)
|
||||
|
||||
type SourceSelector struct {
|
||||
Source string `json:"source,omitempty"`
|
||||
getSource SourceFunc
|
||||
}
|
||||
|
||||
func (s *SourceSelector) Init() {
|
||||
switch strings.ToLower(s.Source) {
|
||||
case "close":
|
||||
s.getSource = func(kline *types.KLine) fixedpoint.Value { return kline.Close }
|
||||
case "high":
|
||||
s.getSource = func(kline *types.KLine) fixedpoint.Value { return kline.High }
|
||||
case "low":
|
||||
s.getSource = func(kline *types.KLine) fixedpoint.Value { return kline.Low }
|
||||
case "hl2":
|
||||
s.getSource = func(kline *types.KLine) fixedpoint.Value { return kline.High.Add(kline.Low).Div(Two) }
|
||||
case "hlc3":
|
||||
s.getSource = func(kline *types.KLine) fixedpoint.Value {
|
||||
return kline.High.Add(kline.Low).Add(kline.Close).Div(Three)
|
||||
}
|
||||
case "ohlc4":
|
||||
s.getSource = func(kline *types.KLine) fixedpoint.Value {
|
||||
return kline.High.Add(kline.Low).Add(kline.Close).Add(kline.Open).Div(Four)
|
||||
}
|
||||
case "open":
|
||||
s.getSource = func(kline *types.KLine) fixedpoint.Value { return kline.Open }
|
||||
default:
|
||||
log.Infof("source not set: %s, use hl2 by default", s.Source)
|
||||
s.getSource = func(kline *types.KLine) fixedpoint.Value { return kline.High.Add(kline.Low).Div(Two) }
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SourceSelector) GetSource(kline *types.KLine) fixedpoint.Value {
|
||||
return s.getSource(kline)
|
||||
}
|
|
@ -41,6 +41,7 @@ func (s IntervalSlice) StringSlice() (slice []string) {
|
|||
}
|
||||
|
||||
var Interval1m = Interval("1m")
|
||||
var Interval3m = Interval("3m")
|
||||
var Interval5m = Interval("5m")
|
||||
var Interval15m = Interval("15m")
|
||||
var Interval30m = Interval("30m")
|
||||
|
@ -57,6 +58,7 @@ var Interval1mo = Interval("1mo")
|
|||
|
||||
var SupportedIntervals = map[Interval]int{
|
||||
Interval1m: 1,
|
||||
Interval3m: 3,
|
||||
Interval5m: 5,
|
||||
Interval15m: 15,
|
||||
Interval30m: 30,
|
||||
|
|
|
@ -91,6 +91,20 @@ func (k *KLine) Set(o *KLine) {
|
|||
k.Closed = o.Closed
|
||||
}
|
||||
|
||||
func (k *KLine) Merge(o *KLine) {
|
||||
k.EndTime = o.EndTime
|
||||
k.Close = o.Close
|
||||
k.High = fixedpoint.Max(k.High, o.High)
|
||||
k.Low = fixedpoint.Min(k.Low, o.Low)
|
||||
k.Volume = k.Volume.Add(o.Volume)
|
||||
k.QuoteVolume = k.QuoteVolume.Add(o.QuoteVolume)
|
||||
k.TakerBuyBaseAssetVolume = k.TakerBuyBaseAssetVolume.Add(o.TakerBuyBaseAssetVolume)
|
||||
k.TakerBuyQuoteAssetVolume = k.TakerBuyQuoteAssetVolume.Add(o.TakerBuyQuoteAssetVolume)
|
||||
k.LastTradeID = o.LastTradeID
|
||||
k.NumberOfTrades += o.NumberOfTrades
|
||||
k.Closed = o.Closed
|
||||
}
|
||||
|
||||
func (k KLine) GetStartTime() Time {
|
||||
return k.StartTime
|
||||
}
|
||||
|
|
58
pkg/types/pca.go
Normal file
58
pkg/types/pca.go
Normal file
|
@ -0,0 +1,58 @@
|
|||
package types
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gonum.org/v1/gonum/mat"
|
||||
)
|
||||
|
||||
type PCA struct {
|
||||
svd *mat.SVD
|
||||
}
|
||||
|
||||
func (pca *PCA) FitTransform(x []SeriesExtend, lookback, feature int) ([]SeriesExtend, error) {
|
||||
if err := pca.Fit(x, lookback); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return pca.Transform(x, lookback, feature), nil
|
||||
}
|
||||
|
||||
func (pca *PCA) Fit(x []SeriesExtend, lookback int) error {
|
||||
vec := make([]float64, lookback*len(x))
|
||||
for i, xx := range x {
|
||||
mean := xx.Mean(lookback)
|
||||
for j := 0; j < lookback; j++ {
|
||||
vec[i+j*i] = xx.Index(j) - mean
|
||||
}
|
||||
}
|
||||
pca.svd = &mat.SVD{}
|
||||
diffMatrix := mat.NewDense(lookback, len(x), vec)
|
||||
if ok := pca.svd.Factorize(diffMatrix, mat.SVDThin); !ok {
|
||||
return fmt.Errorf("Unable to factorize")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (pca *PCA) Transform(x []SeriesExtend, lookback int, features int) (result []SeriesExtend) {
|
||||
result = make([]SeriesExtend, features)
|
||||
vTemp := new(mat.Dense)
|
||||
pca.svd.VTo(vTemp)
|
||||
var ret mat.Dense
|
||||
vec := make([]float64, lookback*len(x))
|
||||
for i, xx := range x {
|
||||
for j := 0; j < lookback; j++ {
|
||||
vec[i+j*i] = xx.Index(j)
|
||||
}
|
||||
}
|
||||
newX := mat.NewDense(lookback, len(x), vec)
|
||||
ret.Mul(newX, vTemp)
|
||||
newMatrix := mat.NewDense(lookback, features, nil)
|
||||
newMatrix.Copy(&ret)
|
||||
for i := 0; i < features; i++ {
|
||||
queue := NewQueue(lookback)
|
||||
for j := 0; j < lookback; j++ {
|
||||
queue.Update(newMatrix.At(lookback-j-1, i))
|
||||
}
|
||||
result[i] = queue
|
||||
}
|
||||
return result
|
||||
}
|
Loading…
Reference in New Issue
Block a user