feature: add serialmarketdatastore, add elliottwave strategy to replace ewoDgtrd, add active cancel on general order executor, add pca

This commit is contained in:
zenix 2022-09-01 13:09:03 +09:00
parent 067f1274b3
commit 938dc3c497
11 changed files with 918 additions and 0 deletions

116
config/elliottwave.yaml Normal file
View 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

View File

@ -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

View File

@ -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)

View 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)
}
}
}

View File

@ -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"

View 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)
}

View 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
View 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)
}

View File

@ -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,

View File

@ -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
View 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
}