mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-22 14:55:16 +00:00
Merge pull request #43 from c9s/strategy/swing
feature: add swing strategy
This commit is contained in:
commit
301851bf35
|
@ -5,25 +5,25 @@ imports:
|
|||
|
||||
notifications:
|
||||
slack:
|
||||
defaultChannel: "#dev-bbgo"
|
||||
errorChannel: "#bbgo-error"
|
||||
defaultChannel: "dev-bbgo"
|
||||
errorChannel: "bbgo-error"
|
||||
|
||||
# if you want to route channel by symbol
|
||||
symbolChannels:
|
||||
"^BTC": "#btc"
|
||||
"^ETH": "#eth"
|
||||
"^BTC": "btc"
|
||||
"^ETH": "eth"
|
||||
|
||||
# if you want to route channel by exchange session
|
||||
sessionChannels:
|
||||
max: "#bbgo-max"
|
||||
binance: "#bbgo-binance"
|
||||
max: "bbgo-max"
|
||||
binance: "bbgo-binance"
|
||||
|
||||
# routing rules
|
||||
routing:
|
||||
trade: "$symbol"
|
||||
order: "$symbol"
|
||||
submitOrder: "$session"
|
||||
pnL: "#bbgo-pnl"
|
||||
pnL: "bbgo-pnl"
|
||||
|
||||
reportPnL:
|
||||
- averageCostBySymbols:
|
||||
|
|
|
@ -1,20 +1,20 @@
|
|||
---
|
||||
notifications:
|
||||
slack:
|
||||
defaultChannel: "#dev-bbgo"
|
||||
errorChannel: "#bbgo-error"
|
||||
defaultChannel: "dev-bbgo"
|
||||
errorChannel: "bbgo-error"
|
||||
|
||||
# if you want to route channel by symbol
|
||||
symbolChannels:
|
||||
"^BTC": "#btc"
|
||||
"^ETH": "#eth"
|
||||
"^BTC": "btc"
|
||||
"^ETH": "eth"
|
||||
|
||||
# object routing rules
|
||||
routing:
|
||||
trade: "$symbol"
|
||||
order: "$symbol"
|
||||
submitOrder: "$session" # not supported yet
|
||||
pnL: "#bbgo-pnl"
|
||||
pnL: "bbgo-pnl"
|
||||
|
||||
sessions:
|
||||
binance:
|
||||
|
|
51
config/swing.yaml
Normal file
51
config/swing.yaml
Normal file
|
@ -0,0 +1,51 @@
|
|||
---
|
||||
notifications:
|
||||
slack:
|
||||
defaultChannel: "dev-bbgo"
|
||||
errorChannel: "bbgo-error"
|
||||
|
||||
# if you want to route channel by symbol
|
||||
symbolChannels:
|
||||
"^BTC": "btc"
|
||||
"^ETH": "eth"
|
||||
"^BNB": "bnb"
|
||||
|
||||
# object routing rules
|
||||
routing:
|
||||
trade: "$symbol"
|
||||
order: "$symbol"
|
||||
submitOrder: "$session" # not supported yet
|
||||
pnL: "bbgo-pnl"
|
||||
|
||||
sessions:
|
||||
binance:
|
||||
exchange: binance
|
||||
envVarPrefix: binance
|
||||
|
||||
riskControls:
|
||||
# This is the session-based risk controller, which let you configure different risk controller by session.
|
||||
sessionBased:
|
||||
# "max" is the session name that you want to configure the risk control
|
||||
binance:
|
||||
# orderExecutors is one of the risk control
|
||||
orderExecutors:
|
||||
# symbol-routed order executor
|
||||
bySymbol:
|
||||
BNBUSDT:
|
||||
# basic risk control order executor
|
||||
basic:
|
||||
minQuoteBalance: 1000.0
|
||||
maxBaseAssetBalance: 50.0
|
||||
minBaseAssetBalance: 10.0
|
||||
maxOrderAmount: 100.0
|
||||
|
||||
exchangeStrategies:
|
||||
- on: binance
|
||||
swing:
|
||||
symbol: BNBUSDT
|
||||
interval: 1m
|
||||
minChange: 0.01
|
||||
baseQuantity: 1.0
|
||||
movingAverageType: EWMA
|
||||
movingAverageInterval: 1m
|
||||
movingAverageWindow: 99
|
|
@ -8,7 +8,7 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
RegisterExchangeStrategy("test", &TestStrategy{})
|
||||
RegisterStrategy("test", &TestStrategy{})
|
||||
}
|
||||
|
||||
type TestStrategy struct {
|
||||
|
|
|
@ -11,20 +11,20 @@ import (
|
|||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/service"
|
||||
"github.com/c9s/bbgo/pkg/store"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
var LoadedExchangeStrategies = make(map[string]SingleExchangeStrategy)
|
||||
|
||||
func RegisterExchangeStrategy(key string, configmap SingleExchangeStrategy) {
|
||||
LoadedExchangeStrategies[key] = configmap
|
||||
}
|
||||
|
||||
var LoadedCrossExchangeStrategies = make(map[string]CrossExchangeStrategy)
|
||||
|
||||
func RegisterCrossExchangeStrategy(key string, configmap CrossExchangeStrategy) {
|
||||
LoadedCrossExchangeStrategies[key] = configmap
|
||||
func RegisterStrategy(key string, s interface{}) {
|
||||
switch d := s.(type) {
|
||||
case SingleExchangeStrategy:
|
||||
LoadedExchangeStrategies[key] = d
|
||||
|
||||
case CrossExchangeStrategy:
|
||||
LoadedCrossExchangeStrategies[key] = d
|
||||
}
|
||||
}
|
||||
|
||||
// Environment presents the real exchange data layer
|
||||
|
@ -59,8 +59,10 @@ func (environ *Environment) AddExchange(name string, exchange types.Exchange) (s
|
|||
return session
|
||||
}
|
||||
|
||||
// Init prepares the data that will be used by the strategies
|
||||
func (environ *Environment) Init(ctx context.Context) (err error) {
|
||||
for _, session := range environ.sessions {
|
||||
for n := range environ.sessions {
|
||||
var session = environ.sessions[n]
|
||||
var markets types.MarketMap
|
||||
|
||||
err = WithCache(fmt.Sprintf("%s-markets", session.Exchange.Name()), &markets, func() (interface{}, error) {
|
||||
|
@ -75,37 +77,9 @@ func (environ *Environment) Init(ctx context.Context) (err error) {
|
|||
}
|
||||
|
||||
session.markets = markets
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncTradesFrom overrides the default trade scan time (-7 days)
|
||||
func (environ *Environment) SyncTradesFrom(t time.Time) *Environment {
|
||||
environ.tradeScanTime = t
|
||||
|
||||
return environ
|
||||
}
|
||||
|
||||
func (environ *Environment) Connect(ctx context.Context) error {
|
||||
var err error
|
||||
|
||||
for n := range environ.sessions {
|
||||
// avoid using the placeholder variable for the session because we use that in the callbacks
|
||||
var session = environ.sessions[n]
|
||||
var log = log.WithField("session", n)
|
||||
|
||||
loadedSymbols := make(map[string]struct{})
|
||||
for _, s := range session.Subscriptions {
|
||||
symbol := strings.ToUpper(s.Symbol)
|
||||
loadedSymbols[symbol] = struct{}{}
|
||||
|
||||
log.Infof("subscribing %s %s %v", s.Symbol, s.Channel, s.Options)
|
||||
session.Stream.Subscribe(s.Channel, s.Symbol, s.Options)
|
||||
}
|
||||
|
||||
// trade sync and market data store depends on subscribed symbols so we have to do this here.
|
||||
for symbol := range loadedSymbols {
|
||||
for symbol := range session.loadedSymbols {
|
||||
var trades []types.Trade
|
||||
|
||||
if environ.TradeSync != nil {
|
||||
|
@ -130,27 +104,23 @@ func (environ *Environment) Connect(ctx context.Context) error {
|
|||
|
||||
session.Trades[symbol] = trades
|
||||
|
||||
currentPrice, err := session.Exchange.QueryAveragePrice(ctx, symbol)
|
||||
averagePrice, err := session.Exchange.QueryAveragePrice(ctx, symbol)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
session.lastPrices[symbol] = currentPrice
|
||||
session.lastPrices[symbol] = averagePrice
|
||||
|
||||
marketDataStore := store.NewMarketDataStore(symbol)
|
||||
marketDataStore := NewMarketDataStore(symbol)
|
||||
marketDataStore.BindStream(session.Stream)
|
||||
session.marketDataStores[symbol] = marketDataStore
|
||||
|
||||
standardIndicatorSet := NewStandardIndicatorSet(symbol)
|
||||
standardIndicatorSet.BindMarketDataStore(marketDataStore)
|
||||
standardIndicatorSet := NewStandardIndicatorSet(symbol, marketDataStore)
|
||||
session.standardIndicatorSets[symbol] = standardIndicatorSet
|
||||
}
|
||||
|
||||
// move market data store dispatch to here, use one callback to dispatch the market data
|
||||
// session.Stream.OnKLineClosed(func(kline types.KLine) { })
|
||||
|
||||
now := time.Now()
|
||||
for symbol := range loadedSymbols {
|
||||
for symbol := range session.loadedSymbols {
|
||||
marketDataStore, ok := session.marketDataStores[symbol]
|
||||
if !ok {
|
||||
return errors.Errorf("symbol %s is not defined", symbol)
|
||||
|
@ -197,12 +167,37 @@ func (environ *Environment) Connect(ctx context.Context) error {
|
|||
}
|
||||
})
|
||||
|
||||
// move market data store dispatch to here, use one callback to dispatch the market data
|
||||
// session.Stream.OnKLineClosed(func(kline types.KLine) { })
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// SyncTradesFrom overrides the default trade scan time (-7 days)
|
||||
func (environ *Environment) SyncTradesFrom(t time.Time) *Environment {
|
||||
environ.tradeScanTime = t
|
||||
return environ
|
||||
}
|
||||
|
||||
func (environ *Environment) Connect(ctx context.Context) error {
|
||||
for n := range environ.sessions {
|
||||
// avoid using the placeholder variable for the session because we use that in the callbacks
|
||||
var session = environ.sessions[n]
|
||||
var logger = log.WithField("session", n)
|
||||
|
||||
if len(session.Subscriptions) == 0 {
|
||||
log.Warnf("no subscriptions, exchange session %s will not be connected", session.Name)
|
||||
logger.Warnf("no subscriptions, exchange session %s will not be connected", session.Name)
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof("connecting session %s...", session.Name)
|
||||
// add the subscribe requests to the stream
|
||||
for _, s := range session.Subscriptions {
|
||||
logger.Infof("subscribing %s %s %v", s.Symbol, s.Channel, s.Options)
|
||||
session.Stream.Subscribe(s.Channel, s.Symbol, s.Options)
|
||||
}
|
||||
|
||||
logger.Infof("connecting session %s...", session.Name)
|
||||
if err := session.Stream.Connect(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
|
|
@ -7,16 +7,34 @@ import (
|
|||
"github.com/sirupsen/logrus"
|
||||
)
|
||||
|
||||
func injectStrategyField(strategy SingleExchangeStrategy, rs reflect.Value, fieldName string, obj interface{}) error {
|
||||
func isSymbolBasedStrategy(rs reflect.Value) (string, bool) {
|
||||
field := rs.FieldByName("Symbol")
|
||||
if !field.IsValid() {
|
||||
return "", false
|
||||
}
|
||||
|
||||
if field.Kind() != reflect.String {
|
||||
return "", false
|
||||
}
|
||||
|
||||
return field.String(), true
|
||||
}
|
||||
|
||||
func hasField(rs reflect.Value, fieldName string) bool {
|
||||
field := rs.FieldByName(fieldName)
|
||||
return field.IsValid()
|
||||
}
|
||||
|
||||
func injectField(rs reflect.Value, fieldName string, obj interface{}, pointerOnly bool) error {
|
||||
field := rs.FieldByName(fieldName)
|
||||
if !field.IsValid() {
|
||||
return nil
|
||||
}
|
||||
|
||||
logrus.Infof("found %s in strategy %T, injecting %T...", fieldName, strategy, obj)
|
||||
logrus.Infof("found %s in %s, injecting %T...", fieldName, rs.Type(), obj)
|
||||
|
||||
if !field.CanSet() {
|
||||
return errors.Errorf("field %s of strategy %T can not be set", fieldName, strategy)
|
||||
return errors.Errorf("field %s of %s can not be set", fieldName, rs.Type())
|
||||
}
|
||||
|
||||
rv := reflect.ValueOf(obj)
|
||||
|
@ -30,6 +48,10 @@ func injectStrategyField(strategy SingleExchangeStrategy, rs reflect.Value, fiel
|
|||
field.Set(rv)
|
||||
} else {
|
||||
// set as value
|
||||
if pointerOnly {
|
||||
return errors.Errorf("field %s %s does not allow value assignment (pointer type only)", field.Type(), rv.Type())
|
||||
}
|
||||
|
||||
field.Set(rv.Elem())
|
||||
}
|
||||
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
package store
|
||||
package bbgo
|
||||
|
||||
import (
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
import "github.com/c9s/bbgo/pkg/types"
|
||||
|
||||
// MarketDataStore receives and maintain the public market data
|
||||
//go:generate callbackgen -type MarketDataStore
|
||||
type MarketDataStore struct {
|
||||
Symbol string
|
||||
|
@ -13,7 +12,7 @@ type MarketDataStore struct {
|
|||
|
||||
LastKLine types.KLine
|
||||
|
||||
kLineUpdateCallbacks []func(kline types.KLine)
|
||||
kLineWindowUpdateCallbacks []func(interval types.Interval, kline types.KLineWindow)
|
||||
|
||||
orderBook *types.StreamOrderBook
|
||||
|
||||
|
@ -80,11 +79,15 @@ func (store *MarketDataStore) handleKLineClosed(kline types.KLine) {
|
|||
}
|
||||
|
||||
func (store *MarketDataStore) AddKLine(kline types.KLine) {
|
||||
window := store.KLineWindows[kline.Interval]
|
||||
window.Add(kline)
|
||||
window, ok := store.KLineWindows[kline.Interval]
|
||||
if !ok {
|
||||
window = types.KLineWindow{kline}
|
||||
} else {
|
||||
window.Add(kline)
|
||||
}
|
||||
store.KLineWindows[kline.Interval] = window
|
||||
|
||||
store.LastKLine = kline
|
||||
|
||||
store.EmitKLineUpdate(kline)
|
||||
store.EmitKLineWindowUpdate(kline.Interval, window)
|
||||
}
|
|
@ -1,18 +1,18 @@
|
|||
// Code generated by "callbackgen -type MarketDataStore"; DO NOT EDIT.
|
||||
|
||||
package store
|
||||
package bbgo
|
||||
|
||||
import (
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
func (store *MarketDataStore) OnKLineUpdate(cb func(kline types.KLine)) {
|
||||
store.kLineUpdateCallbacks = append(store.kLineUpdateCallbacks, cb)
|
||||
func (store *MarketDataStore) OnKLineWindowUpdate(cb func(interval types.Interval, kline types.KLineWindow)) {
|
||||
store.kLineWindowUpdateCallbacks = append(store.kLineWindowUpdateCallbacks, cb)
|
||||
}
|
||||
|
||||
func (store *MarketDataStore) EmitKLineUpdate(kline types.KLine) {
|
||||
for _, cb := range store.kLineUpdateCallbacks {
|
||||
cb(kline)
|
||||
func (store *MarketDataStore) EmitKLineWindowUpdate(interval types.Interval, kline types.KLineWindow) {
|
||||
for _, cb := range store.kLineWindowUpdateCallbacks {
|
||||
cb(interval, kline)
|
||||
}
|
||||
}
|
||||
|
|
@ -62,8 +62,8 @@ type BasicRiskControlOrderExecutor struct {
|
|||
func (e *BasicRiskControlOrderExecutor) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder) ([]types.Order, error) {
|
||||
var formattedOrders []types.SubmitOrder
|
||||
for _, order := range orders {
|
||||
currentPrice, ok := e.session.lastPrices[order.Symbol]
|
||||
if ok {
|
||||
currentPrice, ok := e.session.LastPrice(order.Symbol)
|
||||
if !ok {
|
||||
return nil, errors.Errorf("the last price of symbol %q is not found", order.Symbol)
|
||||
}
|
||||
|
||||
|
@ -140,9 +140,10 @@ func (e *BasicRiskControlOrderExecutor) SubmitOrders(ctx context.Context, orders
|
|||
}
|
||||
|
||||
formattedOrders = append(formattedOrders, o)
|
||||
|
||||
e.Notify(":memo: Submitting %s %s %s order with quantity %s @ %s", o.Symbol, o.Side, o.Type, o.QuantityString, o.PriceString, &o)
|
||||
}
|
||||
|
||||
// e.Notify(":memo: Submitting %s %s %s order with quantity: %s", order.Symbol, order.Type, order.Side, order.QuantityString, order)
|
||||
return e.session.Exchange.SubmitOrders(ctx, formattedOrders...)
|
||||
}
|
||||
|
||||
|
@ -164,7 +165,7 @@ func formatOrder(order types.SubmitOrder, session *ExchangeSession) (types.Submi
|
|||
|
||||
}
|
||||
|
||||
order.QuantityString = market.FormatVolume(order.Quantity)
|
||||
order.QuantityString = market.FormatQuantity(order.Quantity)
|
||||
return order, nil
|
||||
}
|
||||
|
||||
|
|
|
@ -1,11 +1,7 @@
|
|||
package bbgo
|
||||
|
||||
import (
|
||||
"context"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
var (
|
||||
|
@ -17,118 +13,93 @@ var (
|
|||
ErrAssetBalanceLevelTooHigh = errors.New("asset balance level too high")
|
||||
)
|
||||
|
||||
// OrderProcessor does:
|
||||
// - Check quote balance
|
||||
// - Check and control the order amount
|
||||
// - Adjust order amount due to the minAmount configuration and maxAmount configuration
|
||||
// - Canonicalize the volume precision base on the given exchange
|
||||
type OrderProcessor struct {
|
||||
// balance control
|
||||
MinQuoteBalance float64 `json:"minQuoteBalance"`
|
||||
MaxAssetBalance float64 `json:"maxBaseAssetBalance"`
|
||||
MinAssetBalance float64 `json:"minBaseAssetBalance"`
|
||||
MaxOrderAmount float64 `json:"maxOrderAmount"`
|
||||
/*
|
||||
tradingCtx := p.OrderExecutor.Context
|
||||
currentPrice := tradingCtx.CurrentPrice
|
||||
market := order.Market
|
||||
quantity := order.Quantity
|
||||
|
||||
// MinProfitSpread is used when submitting sell orders, it check if there the selling can make the profit.
|
||||
MinProfitSpread float64 `json:"minProfitSpread"`
|
||||
tradingCtx.Lock()
|
||||
defer tradingCtx.Unlock()
|
||||
|
||||
switch order.Side {
|
||||
case types.SideTypeBuy:
|
||||
|
||||
Exchange types.Exchange `json:"-"`
|
||||
}
|
||||
if balance, ok := tradingCtx.Balances[market.QuoteCurrency]; ok {
|
||||
if balance.Available < p.MinQuoteBalance {
|
||||
return errors.Wrapf(ErrQuoteBalanceLevelTooLow, "quote balance level is too low: %s < %s",
|
||||
types.USD.FormatMoneyFloat64(balance.Available),
|
||||
types.USD.FormatMoneyFloat64(p.MinQuoteBalance))
|
||||
}
|
||||
|
||||
func (p *OrderProcessor) Submit(ctx context.Context, order types.SubmitOrder) error {
|
||||
/*
|
||||
tradingCtx := p.OrderExecutor.Context
|
||||
currentPrice := tradingCtx.CurrentPrice
|
||||
market := order.Market
|
||||
quantity := order.Quantity
|
||||
|
||||
tradingCtx.Lock()
|
||||
defer tradingCtx.Unlock()
|
||||
|
||||
switch order.Side {
|
||||
case types.SideTypeBuy:
|
||||
|
||||
if balance, ok := tradingCtx.Balances[market.QuoteCurrency]; ok {
|
||||
if balance.Available < p.MinQuoteBalance {
|
||||
return errors.Wrapf(ErrQuoteBalanceLevelTooLow, "quote balance level is too low: %s < %s",
|
||||
types.USD.FormatMoneyFloat64(balance.Available),
|
||||
types.USD.FormatMoneyFloat64(p.MinQuoteBalance))
|
||||
}
|
||||
|
||||
if baseBalance, ok := tradingCtx.Balances[market.BaseCurrency]; ok {
|
||||
if util.NotZero(p.MaxAssetBalance) && baseBalance.Available > p.MaxAssetBalance {
|
||||
return errors.Wrapf(ErrAssetBalanceLevelTooHigh, "asset balance level is too high: %f > %f", baseBalance.Available, p.MaxAssetBalance)
|
||||
}
|
||||
}
|
||||
|
||||
available := math.Max(0.0, balance.Available-p.MinQuoteBalance)
|
||||
|
||||
if available < market.MinAmount {
|
||||
return errors.Wrapf(ErrInsufficientQuoteBalance, "insufficient quote balance: %f < min amount %f", available, market.MinAmount)
|
||||
}
|
||||
|
||||
quantity = adjustQuantityByMinAmount(quantity, currentPrice, market.MinAmount*1.01)
|
||||
quantity = adjustQuantityByMaxAmount(quantity, currentPrice, available)
|
||||
amount := quantity * currentPrice
|
||||
if amount < market.MinAmount {
|
||||
return fmt.Errorf("amount too small: %f < min amount %f", amount, market.MinAmount)
|
||||
if baseBalance, ok := tradingCtx.Balances[market.BaseCurrency]; ok {
|
||||
if util.NotZero(p.MaxAssetBalance) && baseBalance.Available > p.MaxAssetBalance {
|
||||
return errors.Wrapf(ErrAssetBalanceLevelTooHigh, "asset balance level is too high: %f > %f", baseBalance.Available, p.MaxAssetBalance)
|
||||
}
|
||||
}
|
||||
|
||||
case types.SideTypeSell:
|
||||
available := math.Max(0.0, balance.Available-p.MinQuoteBalance)
|
||||
|
||||
if balance, ok := tradingCtx.Balances[market.BaseCurrency]; ok {
|
||||
if util.NotZero(p.MinAssetBalance) && balance.Available < p.MinAssetBalance {
|
||||
return errors.Wrapf(ErrAssetBalanceLevelTooLow, "asset balance level is too low: %f > %f", balance.Available, p.MinAssetBalance)
|
||||
}
|
||||
if available < market.MinAmount {
|
||||
return errors.Wrapf(ErrInsufficientQuoteBalance, "insufficient quote balance: %f < min amount %f", available, market.MinAmount)
|
||||
}
|
||||
|
||||
quantity = adjustQuantityByMinAmount(quantity, currentPrice, market.MinNotional*1.01)
|
||||
|
||||
available := balance.Available
|
||||
quantity = math.Min(quantity, available)
|
||||
if quantity < market.MinQuantity {
|
||||
return errors.Wrapf(ErrInsufficientAssetBalance, "insufficient asset balance: %f > minimal quantity %f", available, market.MinQuantity)
|
||||
}
|
||||
|
||||
notional := quantity * currentPrice
|
||||
if notional < tradingCtx.Market.MinNotional {
|
||||
return fmt.Errorf("notional %f < min notional: %f", notional, market.MinNotional)
|
||||
}
|
||||
|
||||
// price tick10
|
||||
// 2 -> 0.01 -> 0.1
|
||||
// 4 -> 0.0001 -> 0.001
|
||||
tick10 := math.Pow10(-market.PricePrecision + 1)
|
||||
minProfitSpread := math.Max(p.MinProfitSpread, tick10)
|
||||
estimatedFee := currentPrice * 0.0015 * 2 // double the fee
|
||||
targetPrice := currentPrice - estimatedFee - minProfitSpread
|
||||
|
||||
stockQuantity := tradingCtx.StockManager.Stocks.QuantityBelowPrice(targetPrice)
|
||||
if math.Round(stockQuantity*1e8) == 0.0 {
|
||||
return fmt.Errorf("profitable stock not found: target price %f, profit spread: %f", targetPrice, minProfitSpread)
|
||||
}
|
||||
|
||||
quantity = math.Min(quantity, stockQuantity)
|
||||
if quantity < market.MinLot {
|
||||
return fmt.Errorf("quantity %f less than min lot %f", quantity, market.MinLot)
|
||||
}
|
||||
|
||||
notional = quantity * currentPrice
|
||||
if notional < tradingCtx.Market.MinNotional {
|
||||
return fmt.Errorf("notional %f < min notional: %f", notional, market.MinNotional)
|
||||
}
|
||||
quantity = adjustQuantityByMinAmount(quantity, currentPrice, market.MinAmount*1.01)
|
||||
quantity = adjustQuantityByMaxAmount(quantity, currentPrice, available)
|
||||
amount := quantity * currentPrice
|
||||
if amount < market.MinAmount {
|
||||
return fmt.Errorf("amount too small: %f < min amount %f", amount, market.MinAmount)
|
||||
}
|
||||
}
|
||||
|
||||
order.Quantity = quantity
|
||||
order.QuantityString = market.FormatVolume(quantity)
|
||||
*/
|
||||
case types.SideTypeSell:
|
||||
|
||||
createdOrders, err := p.Exchange.SubmitOrders(ctx, order)
|
||||
_ = createdOrders
|
||||
return err
|
||||
}
|
||||
if balance, ok := tradingCtx.Balances[market.BaseCurrency]; ok {
|
||||
if util.NotZero(p.MinAssetBalance) && balance.Available < p.MinAssetBalance {
|
||||
return errors.Wrapf(ErrAssetBalanceLevelTooLow, "asset balance level is too low: %f > %f", balance.Available, p.MinAssetBalance)
|
||||
}
|
||||
|
||||
quantity = adjustQuantityByMinAmount(quantity, currentPrice, market.MinNotional*1.01)
|
||||
|
||||
available := balance.Available
|
||||
quantity = math.Min(quantity, available)
|
||||
if quantity < market.MinQuantity {
|
||||
return errors.Wrapf(ErrInsufficientAssetBalance, "insufficient asset balance: %f > minimal quantity %f", available, market.MinQuantity)
|
||||
}
|
||||
|
||||
notional := quantity * currentPrice
|
||||
if notional < tradingCtx.Market.MinNotional {
|
||||
return fmt.Errorf("notional %f < min notional: %f", notional, market.MinNotional)
|
||||
}
|
||||
|
||||
// price tick10
|
||||
// 2 -> 0.01 -> 0.1
|
||||
// 4 -> 0.0001 -> 0.001
|
||||
tick10 := math.Pow10(-market.PricePrecision + 1)
|
||||
minProfitSpread := math.Max(p.MinProfitSpread, tick10)
|
||||
estimatedFee := currentPrice * 0.0015 * 2 // double the fee
|
||||
targetPrice := currentPrice - estimatedFee - minProfitSpread
|
||||
|
||||
stockQuantity := tradingCtx.StockManager.Stocks.QuantityBelowPrice(targetPrice)
|
||||
if math.Round(stockQuantity*1e8) == 0.0 {
|
||||
return fmt.Errorf("profitable stock not found: target price %f, profit spread: %f", targetPrice, minProfitSpread)
|
||||
}
|
||||
|
||||
quantity = math.Min(quantity, stockQuantity)
|
||||
if quantity < market.MinLot {
|
||||
return fmt.Errorf("quantity %f less than min lot %f", quantity, market.MinLot)
|
||||
}
|
||||
|
||||
notional = quantity * currentPrice
|
||||
if notional < tradingCtx.Market.MinNotional {
|
||||
return fmt.Errorf("notional %f < min notional: %f", notional, market.MinNotional)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
order.Quantity = quantity
|
||||
order.QuantityString = market.FormatVolume(quantity)
|
||||
*/
|
||||
|
||||
func adjustQuantityByMinAmount(quantity float64, currentPrice float64, minAmount float64) float64 {
|
||||
// modify quantity for the min amount
|
||||
|
|
|
@ -142,15 +142,15 @@ func (router *ObjectChannelRouter) Route(obj interface{}) (channel string, ok bo
|
|||
}
|
||||
|
||||
type TradeReporter struct {
|
||||
notifier Notifier
|
||||
*Notifiability
|
||||
|
||||
channel string
|
||||
channelRoutes map[*regexp.Regexp]string
|
||||
}
|
||||
|
||||
func NewTradeReporter(notifier Notifier) *TradeReporter {
|
||||
func NewTradeReporter(notifiability *Notifiability) *TradeReporter {
|
||||
return &TradeReporter{
|
||||
notifier: notifier,
|
||||
Notifiability: notifiability,
|
||||
channelRoutes: make(map[*regexp.Regexp]string),
|
||||
}
|
||||
}
|
||||
|
@ -182,5 +182,5 @@ func (reporter *TradeReporter) Report(trade types.Trade) {
|
|||
var channel = reporter.getChannel(trade.Symbol)
|
||||
|
||||
var text = util.Render(`:handshake: {{ .Symbol }} {{ .Side }} Trade Execution @ {{ .Price }}`, trade)
|
||||
reporter.notifier.NotifyTo(channel, text, trade)
|
||||
reporter.NotifyTo(channel, text, trade)
|
||||
}
|
||||
|
|
|
@ -2,58 +2,64 @@ package bbgo
|
|||
|
||||
import (
|
||||
"github.com/c9s/bbgo/pkg/indicator"
|
||||
"github.com/c9s/bbgo/pkg/store"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
type IntervalWindow struct {
|
||||
// The interval of kline
|
||||
Interval types.Interval
|
||||
|
||||
// The windows size of EWMA and SMA
|
||||
Window int
|
||||
}
|
||||
|
||||
type StandardIndicatorSet struct {
|
||||
Symbol string
|
||||
// Standard indicators
|
||||
// interval -> window
|
||||
SMA map[IntervalWindow]*indicator.SMA
|
||||
EWMA map[IntervalWindow]*indicator.EWMA
|
||||
SMA map[types.IntervalWindow]*indicator.SMA
|
||||
EWMA map[types.IntervalWindow]*indicator.EWMA
|
||||
|
||||
store *MarketDataStore
|
||||
}
|
||||
|
||||
func NewStandardIndicatorSet(symbol string) *StandardIndicatorSet {
|
||||
func NewStandardIndicatorSet(symbol string, store *MarketDataStore) *StandardIndicatorSet {
|
||||
set := &StandardIndicatorSet{
|
||||
Symbol: symbol,
|
||||
SMA: make(map[IntervalWindow]*indicator.SMA),
|
||||
EWMA: make(map[IntervalWindow]*indicator.EWMA),
|
||||
SMA: make(map[types.IntervalWindow]*indicator.SMA),
|
||||
EWMA: make(map[types.IntervalWindow]*indicator.EWMA),
|
||||
store: store,
|
||||
}
|
||||
|
||||
// let us pre-defined commonly used intervals
|
||||
for interval := range types.SupportedIntervals {
|
||||
for _, window := range []int{7, 25, 99} {
|
||||
set.SMA[IntervalWindow{interval, window}] = &indicator.SMA{
|
||||
Interval: interval,
|
||||
Window: window,
|
||||
}
|
||||
set.EWMA[IntervalWindow{interval, window}] = &indicator.EWMA{
|
||||
Interval: interval,
|
||||
Window: window,
|
||||
}
|
||||
iw := types.IntervalWindow{Interval: interval, Window: window}
|
||||
set.SMA[iw] = &indicator.SMA{IntervalWindow: iw}
|
||||
set.SMA[iw].Bind(store)
|
||||
|
||||
set.EWMA[iw] = &indicator.EWMA{IntervalWindow: iw}
|
||||
set.EWMA[iw].Bind(store)
|
||||
}
|
||||
}
|
||||
|
||||
return set
|
||||
}
|
||||
|
||||
func (set *StandardIndicatorSet) BindMarketDataStore(store *store.MarketDataStore) {
|
||||
for _, inc := range set.SMA {
|
||||
inc.BindMarketDataStore(store)
|
||||
// GetSMA returns the simple moving average indicator of the given interval and the window size.
|
||||
func (set *StandardIndicatorSet) GetSMA(iw types.IntervalWindow) *indicator.SMA {
|
||||
inc, ok := set.SMA[iw]
|
||||
if !ok {
|
||||
inc := &indicator.SMA{IntervalWindow: iw}
|
||||
inc.Bind(set.store)
|
||||
set.SMA[iw] = inc
|
||||
}
|
||||
|
||||
for _, inc := range set.EWMA {
|
||||
inc.BindMarketDataStore(store)
|
||||
return inc
|
||||
}
|
||||
|
||||
// GetEWMA returns the exponential weighed moving average indicator of the given interval and the window size.
|
||||
func (set *StandardIndicatorSet) GetEWMA(iw types.IntervalWindow) *indicator.EWMA {
|
||||
inc, ok := set.EWMA[iw]
|
||||
if !ok {
|
||||
inc := &indicator.EWMA{IntervalWindow: iw}
|
||||
inc.Bind(set.store)
|
||||
set.EWMA[iw] = inc
|
||||
}
|
||||
|
||||
return inc
|
||||
}
|
||||
|
||||
// ExchangeSession presents the exchange connection session
|
||||
|
@ -82,25 +88,29 @@ type ExchangeSession struct {
|
|||
Trades map[string][]types.Trade
|
||||
|
||||
// marketDataStores contains the market data store of each market
|
||||
marketDataStores map[string]*store.MarketDataStore
|
||||
marketDataStores map[string]*MarketDataStore
|
||||
|
||||
// standard indicators of each market
|
||||
standardIndicatorSets map[string]*StandardIndicatorSet
|
||||
|
||||
tradeReporter *TradeReporter
|
||||
loadedSymbols map[string]struct{}
|
||||
}
|
||||
|
||||
func NewExchangeSession(name string, exchange types.Exchange) *ExchangeSession {
|
||||
return &ExchangeSession{
|
||||
Name: name,
|
||||
Exchange: exchange,
|
||||
Stream: exchange.NewStream(),
|
||||
Account: &types.Account{},
|
||||
Subscriptions: make(map[types.Subscription]types.Subscription),
|
||||
markets: make(map[string]types.Market),
|
||||
Trades: make(map[string][]types.Trade),
|
||||
lastPrices: make(map[string]float64),
|
||||
marketDataStores: make(map[string]*store.MarketDataStore),
|
||||
Name: name,
|
||||
Exchange: exchange,
|
||||
Stream: exchange.NewStream(),
|
||||
Subscriptions: make(map[types.Subscription]types.Subscription),
|
||||
Account: &types.Account{},
|
||||
Trades: make(map[string][]types.Trade),
|
||||
|
||||
markets: make(map[string]types.Market),
|
||||
lastPrices: make(map[string]float64),
|
||||
marketDataStores: make(map[string]*MarketDataStore),
|
||||
standardIndicatorSets: make(map[string]*StandardIndicatorSet),
|
||||
|
||||
loadedSymbols: make(map[string]struct{}),
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -110,7 +120,7 @@ func (session *ExchangeSession) StandardIndicatorSet(symbol string) (*StandardIn
|
|||
}
|
||||
|
||||
// MarketDataStore returns the market data store of a symbol
|
||||
func (session *ExchangeSession) MarketDataStore(symbol string) (s *store.MarketDataStore, ok bool) {
|
||||
func (session *ExchangeSession) MarketDataStore(symbol string) (s *MarketDataStore, ok bool) {
|
||||
s, ok = session.marketDataStores[symbol]
|
||||
return s, ok
|
||||
}
|
||||
|
@ -125,11 +135,6 @@ func (session *ExchangeSession) Market(symbol string) (market types.Market, ok b
|
|||
return market, ok
|
||||
}
|
||||
|
||||
func (session *ExchangeSession) ReportTrade(notifier Notifier) *TradeReporter {
|
||||
session.tradeReporter = NewTradeReporter(notifier)
|
||||
return session.tradeReporter
|
||||
}
|
||||
|
||||
// Subscribe save the subscription info, later it will be assigned to the stream
|
||||
func (session *ExchangeSession) Subscribe(channel types.Channel, symbol string, options types.SubscribeOptions) *ExchangeSession {
|
||||
sub := types.Subscription{
|
||||
|
@ -138,6 +143,8 @@ func (session *ExchangeSession) Subscribe(channel types.Channel, symbol string,
|
|||
Options: options,
|
||||
}
|
||||
|
||||
// add to the loaded symbol table
|
||||
session.loadedSymbols[symbol] = struct{}{}
|
||||
session.Subscriptions[sub] = sub
|
||||
return session
|
||||
}
|
||||
|
|
|
@ -27,6 +27,10 @@ type SingleExchangeStrategy interface {
|
|||
Run(ctx context.Context, orderExecutor OrderExecutor, session *ExchangeSession) error
|
||||
}
|
||||
|
||||
type ExchangeSessionSubscriber interface {
|
||||
Subscribe(session *ExchangeSession)
|
||||
}
|
||||
|
||||
type CrossExchangeStrategy interface {
|
||||
Run(ctx context.Context, orderExecutionRouter OrderExecutionRouter, sessions map[string]*ExchangeSession) error
|
||||
}
|
||||
|
@ -85,23 +89,34 @@ func (trader *Trader) SetRiskControls(riskControls *RiskControls) {
|
|||
}
|
||||
|
||||
func (trader *Trader) Run(ctx context.Context) error {
|
||||
|
||||
// pre-subscribe the data
|
||||
for sessionName, strategies := range trader.exchangeStrategies {
|
||||
session := trader.environment.sessions[sessionName]
|
||||
for _, strategy := range strategies {
|
||||
if subscriber, ok := strategy.(ExchangeSessionSubscriber); ok {
|
||||
subscriber.Subscribe(session)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if err := trader.environment.Init(ctx); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// load and run session strategies
|
||||
for sessionName, strategies := range trader.exchangeStrategies {
|
||||
session := trader.environment.sessions[sessionName]
|
||||
|
||||
if session.tradeReporter != nil {
|
||||
session.Stream.OnTrade(func(trade types.Trade) {
|
||||
session.tradeReporter.Report(trade)
|
||||
})
|
||||
} else if trader.tradeReporter != nil {
|
||||
// session based trade reporter
|
||||
for sessionName := range trader.environment.sessions {
|
||||
var session = trader.environment.sessions[sessionName]
|
||||
if trader.tradeReporter != nil {
|
||||
session.Stream.OnTrade(func(trade types.Trade) {
|
||||
trader.tradeReporter.Report(trade)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// load and run session strategies
|
||||
for sessionName, strategies := range trader.exchangeStrategies {
|
||||
var session = trader.environment.sessions[sessionName]
|
||||
|
||||
var baseOrderExecutor = &ExchangeOrderExecutor{
|
||||
// copy the parent notifiers and session
|
||||
|
@ -133,12 +148,41 @@ func (trader *Trader) Run(ctx context.Context) error {
|
|||
// get the struct element
|
||||
rs = rs.Elem()
|
||||
|
||||
if err := injectStrategyField(strategy, rs, "Notifiability", &trader.Notifiability); err != nil {
|
||||
log.WithError(err).Errorf("strategy notifiability injection failed")
|
||||
if err := injectField(rs, "Notifiability", &trader.Notifiability, false); err != nil {
|
||||
log.WithError(err).Errorf("strategy Notifiability injection failed")
|
||||
}
|
||||
|
||||
if err := injectStrategyField(strategy, rs, "OrderExecutor", orderExecutor); err != nil {
|
||||
log.WithError(err).Errorf("strategy orderExecutor injection failed")
|
||||
if err := injectField(rs, "OrderExecutor", orderExecutor, false); err != nil {
|
||||
log.WithError(err).Errorf("strategy OrderExecutor injection failed")
|
||||
}
|
||||
|
||||
if symbol, ok := isSymbolBasedStrategy(rs); ok {
|
||||
log.Infof("found symbol based strategy from %s", rs.Type())
|
||||
if hasField(rs, "Market") {
|
||||
if market, ok := session.Market(symbol); ok {
|
||||
// let's make the market object passed by pointer
|
||||
if err := injectField(rs, "Market", &market, false); err != nil {
|
||||
log.WithError(err).Errorf("strategy %T Market injection failed", strategy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// StandardIndicatorSet
|
||||
if hasField(rs, "StandardIndicatorSet") {
|
||||
if indicatorSet, ok := session.StandardIndicatorSet(symbol); ok {
|
||||
if err := injectField(rs, "StandardIndicatorSet", indicatorSet, true); err != nil {
|
||||
log.WithError(err).Errorf("strategy %T StandardIndicatorSet injection failed", strategy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if hasField(rs, "MarketDataStore") {
|
||||
if store, ok := session.MarketDataStore(symbol); ok {
|
||||
if err := injectField(rs, "MarketDataStore", store, true); err != nil {
|
||||
log.WithError(err).Errorf("strategy %T MarketDataStore injection failed", strategy)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
9
pkg/cmd/builtin.go
Normal file
9
pkg/cmd/builtin.go
Normal file
|
@ -0,0 +1,9 @@
|
|||
package cmd
|
||||
|
||||
// import built-in strategies
|
||||
import (
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/buyandhold"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/pricealert"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/swing"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/xpuremaker"
|
||||
)
|
|
@ -24,11 +24,6 @@ import (
|
|||
"github.com/c9s/bbgo/pkg/notifier/slacknotifier"
|
||||
"github.com/c9s/bbgo/pkg/slack/slacklog"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
|
||||
// import built-in strategies
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/buyandhold"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/pricealert"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/xpuremaker"
|
||||
)
|
||||
|
||||
var errSlackTokenUndefined = errors.New("slack token is not defined.")
|
||||
|
@ -121,7 +116,7 @@ func runConfig(ctx context.Context, userConfig *bbgo.Config) error {
|
|||
log.AddHook(slacklog.NewLogHook(slackToken, conf.ErrorChannel))
|
||||
}
|
||||
|
||||
log.Infof("adding slack notifier...")
|
||||
log.Infof("adding slack notifier with default channel: %s", conf.DefaultChannel)
|
||||
var notifier = slacknotifier.New(slackToken, conf.DefaultChannel)
|
||||
trader.AddNotifier(notifier)
|
||||
}
|
||||
|
|
|
@ -2,6 +2,7 @@ package max
|
|||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
"time"
|
||||
|
||||
"github.com/google/uuid"
|
||||
|
@ -54,7 +55,7 @@ func (e *Exchange) QueryMarkets(ctx context.Context) (types.MarketMap, error) {
|
|||
BaseCurrency: toGlobalCurrency(m.BaseUnit),
|
||||
MinNotional: m.MinQuoteAmount,
|
||||
MinAmount: m.MinQuoteAmount,
|
||||
MinLot: m.MinBaseAmount,
|
||||
MinLot: 1.0 / math.Pow10(m.BaseUnitPrecision), // make it like 0.0001
|
||||
MinQuantity: m.MinBaseAmount,
|
||||
MaxQuantity: 10000.0,
|
||||
MinPrice: 0.1,
|
||||
|
|
|
@ -3,17 +3,19 @@ package indicator
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/store"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
type EWMA struct {
|
||||
Interval types.Interval
|
||||
Window int
|
||||
types.IntervalWindow
|
||||
Values Float64Slice
|
||||
EndTime time.Time
|
||||
}
|
||||
|
||||
func (inc *EWMA) Last() float64 {
|
||||
return inc.Values[len(inc.Values)-1]
|
||||
}
|
||||
|
||||
func (inc *EWMA) calculateAndUpdate(kLines []types.KLine) {
|
||||
if len(kLines) < inc.Window {
|
||||
// we can't calculate
|
||||
|
@ -22,6 +24,8 @@ func (inc *EWMA) calculateAndUpdate(kLines []types.KLine) {
|
|||
|
||||
var index = len(kLines) - 1
|
||||
var lastK = kLines[index]
|
||||
|
||||
// see https://www.investopedia.com/ask/answers/122314/what-exponential-moving-average-ema-formula-and-how-ema-calculated.asp
|
||||
var multiplier = 2.0 / float64(inc.Window+1)
|
||||
|
||||
if inc.EndTime != zeroTime && lastK.EndTime.Before(inc.EndTime) {
|
||||
|
@ -31,7 +35,7 @@ func (inc *EWMA) calculateAndUpdate(kLines []types.KLine) {
|
|||
var recentK = kLines[index-(inc.Window-1) : index+1]
|
||||
if len(inc.Values) > 0 {
|
||||
var previousEWMA = inc.Values[len(inc.Values)-1]
|
||||
var ewma = lastK.Close * multiplier + previousEWMA * (1 - multiplier)
|
||||
var ewma = lastK.Close*multiplier + previousEWMA*(1-multiplier)
|
||||
inc.Values.Push(ewma)
|
||||
} else {
|
||||
// The first EWMA is actually SMA
|
||||
|
@ -42,19 +46,22 @@ func (inc *EWMA) calculateAndUpdate(kLines []types.KLine) {
|
|||
inc.EndTime = kLines[index].EndTime
|
||||
}
|
||||
|
||||
func (inc *EWMA) BindMarketDataStore(store *store.MarketDataStore) {
|
||||
store.OnKLineUpdate(func(kline types.KLine) {
|
||||
// kline guard
|
||||
if inc.Interval != kline.Interval {
|
||||
return
|
||||
}
|
||||
|
||||
if inc.EndTime != zeroTime && inc.EndTime.Before(inc.EndTime) {
|
||||
return
|
||||
}
|
||||
|
||||
if kLines, ok := store.KLinesOfInterval(types.Interval(kline.Interval)); ok {
|
||||
inc.calculateAndUpdate(kLines)
|
||||
}
|
||||
})
|
||||
type KLineWindowUpdater interface {
|
||||
OnKLineWindowUpdate(func(interval types.Interval, window types.KLineWindow))
|
||||
}
|
||||
|
||||
func (inc *EWMA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
|
||||
if inc.Interval != interval {
|
||||
return
|
||||
}
|
||||
|
||||
if inc.EndTime != zeroTime && inc.EndTime.Before(inc.EndTime) {
|
||||
return
|
||||
}
|
||||
|
||||
inc.calculateAndUpdate(window)
|
||||
}
|
||||
|
||||
func (inc *EWMA) Bind(updater KLineWindowUpdater) {
|
||||
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
|
||||
}
|
||||
|
|
|
@ -3,7 +3,6 @@ package indicator
|
|||
import (
|
||||
"time"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/store"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
|
@ -16,12 +15,15 @@ func (s *Float64Slice) Push(v float64) {
|
|||
var zeroTime time.Time
|
||||
|
||||
type SMA struct {
|
||||
Interval types.Interval
|
||||
Window int
|
||||
types.IntervalWindow
|
||||
Values Float64Slice
|
||||
EndTime time.Time
|
||||
}
|
||||
|
||||
func (inc *SMA) Last() float64 {
|
||||
return inc.Values[len(inc.Values)-1]
|
||||
}
|
||||
|
||||
func (inc *SMA) calculateAndUpdate(kLines []types.KLine) {
|
||||
if len(kLines) < inc.Window {
|
||||
return
|
||||
|
@ -40,22 +42,30 @@ func (inc *SMA) calculateAndUpdate(kLines []types.KLine) {
|
|||
inc.EndTime = kLines[index].EndTime
|
||||
}
|
||||
|
||||
func (inc *SMA) BindMarketDataStore(store *store.MarketDataStore) {
|
||||
store.OnKLineUpdate(func(kline types.KLine) {
|
||||
// kline guard
|
||||
if inc.Interval != kline.Interval {
|
||||
return
|
||||
}
|
||||
func (inc *SMA) handleKLineWindowUpdate(interval types.Interval, window types.KLineWindow) {
|
||||
if inc.Interval != interval {
|
||||
return
|
||||
}
|
||||
|
||||
if kLines, ok := store.KLinesOfInterval(kline.Interval); ok {
|
||||
inc.calculateAndUpdate(kLines)
|
||||
}
|
||||
})
|
||||
if inc.EndTime != zeroTime && inc.EndTime.Before(inc.EndTime) {
|
||||
return
|
||||
}
|
||||
|
||||
inc.calculateAndUpdate(window)
|
||||
}
|
||||
|
||||
func (inc *SMA) Bind(updater KLineWindowUpdater) {
|
||||
updater.OnKLineWindowUpdate(inc.handleKLineWindowUpdate)
|
||||
}
|
||||
|
||||
func calculateSMA(kLines []types.KLine) float64 {
|
||||
sum := 0.0
|
||||
length := len(kLines)
|
||||
|
||||
if length == 0 {
|
||||
return 0.0
|
||||
}
|
||||
|
||||
sum := 0.0
|
||||
for _, k := range kLines {
|
||||
sum += k.Close
|
||||
}
|
||||
|
|
|
@ -4,7 +4,7 @@ import (
|
|||
"context"
|
||||
"fmt"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
log "github.com/sirupsen/logrus"
|
||||
"github.com/slack-go/slack"
|
||||
)
|
||||
|
||||
|
@ -20,7 +20,8 @@ type Notifier struct {
|
|||
type NotifyOption func(notifier *Notifier)
|
||||
|
||||
func New(token, channel string, options ...NotifyOption) *Notifier {
|
||||
var client = slack.New(token, slack.OptionDebug(true))
|
||||
// var client = slack.New(token, slack.OptionDebug(true))
|
||||
var client = slack.New(token)
|
||||
|
||||
notifier := &Notifier{
|
||||
channel: channel,
|
||||
|
@ -39,6 +40,10 @@ func (n *Notifier) Notify(format string, args ...interface{}) {
|
|||
}
|
||||
|
||||
func (n *Notifier) NotifyTo(channel, format string, args ...interface{}) {
|
||||
if len(channel) == 0 {
|
||||
channel = n.channel
|
||||
}
|
||||
|
||||
var slackAttachments []slack.Attachment
|
||||
var slackArgsOffset = -1
|
||||
|
||||
|
@ -68,13 +73,15 @@ func (n *Notifier) NotifyTo(channel, format string, args ...interface{}) {
|
|||
nonSlackArgs = args[:slackArgsOffset]
|
||||
}
|
||||
|
||||
logrus.Infof(format, nonSlackArgs...)
|
||||
log.Infof(format, nonSlackArgs...)
|
||||
|
||||
_, _, err := n.client.PostMessageContext(context.Background(), channel,
|
||||
slack.MsgOptionText(fmt.Sprintf(format, nonSlackArgs...), true),
|
||||
slack.MsgOptionAttachments(slackAttachments...))
|
||||
if err != nil {
|
||||
logrus.WithError(err).Errorf("slack error: %s", err.Error())
|
||||
log.WithError(err).
|
||||
WithField("channel", channel).
|
||||
Errorf("slack error: %s", err.Error())
|
||||
}
|
||||
|
||||
return
|
||||
|
|
|
@ -11,7 +11,7 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
bbgo.RegisterExchangeStrategy("buyandhold", &Strategy{})
|
||||
bbgo.RegisterStrategy("buyandhold", &Strategy{})
|
||||
}
|
||||
|
||||
type Strategy struct {
|
||||
|
@ -21,15 +21,27 @@ type Strategy struct {
|
|||
MinDropPercentage float64 `json:"minDropPercentage"`
|
||||
}
|
||||
|
||||
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
||||
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
|
||||
}
|
||||
|
||||
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||
session.Stream.OnKLine(func(kline types.KLine) {
|
||||
// skip k-lines from other symbols
|
||||
if kline.Symbol != s.Symbol {
|
||||
return
|
||||
}
|
||||
|
||||
changePercentage := kline.GetChange() / kline.Open
|
||||
log.Infof("change %f <=> %f", changePercentage, s.MinDropPercentage)
|
||||
})
|
||||
|
||||
session.Stream.OnKLineClosed(func(kline types.KLine) {
|
||||
// skip k-lines from other symbols
|
||||
if kline.Symbol != s.Symbol {
|
||||
return
|
||||
}
|
||||
|
||||
changePercentage := kline.GetChange() / kline.Open
|
||||
|
||||
if changePercentage > s.MinDropPercentage {
|
||||
|
|
|
@ -9,7 +9,7 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
bbgo.RegisterExchangeStrategy("pricealert", &Strategy{})
|
||||
bbgo.RegisterStrategy("pricealert", &Strategy{})
|
||||
}
|
||||
|
||||
type Strategy struct {
|
||||
|
@ -22,8 +22,11 @@ type Strategy struct {
|
|||
MinChange float64 `json:"minChange"`
|
||||
}
|
||||
|
||||
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
||||
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
|
||||
}
|
||||
|
||||
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||
session.Stream.OnKLine(func(kline types.KLine) {
|
||||
market, ok := session.Market(kline.Symbol)
|
||||
if !ok {
|
||||
|
|
|
@ -10,7 +10,7 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
bbgo.RegisterExchangeStrategy("skeleton", &Strategy{})
|
||||
bbgo.RegisterStrategy("skeleton", &Strategy{})
|
||||
}
|
||||
|
||||
type Strategy struct {
|
||||
|
|
167
pkg/strategy/swing/strategy.go
Normal file
167
pkg/strategy/swing/strategy.go
Normal file
|
@ -0,0 +1,167 @@
|
|||
package swing
|
||||
|
||||
import (
|
||||
"context"
|
||||
"math"
|
||||
|
||||
"github.com/pkg/errors"
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
// The indicators (SMA and EWMA) that we want to use are returning float64 data.
|
||||
type Float64Indicator interface {
|
||||
Last() float64
|
||||
}
|
||||
|
||||
func init() {
|
||||
// Register the pointer of the strategy struct,
|
||||
// so that bbgo knows what struct to be used to unmarshal the configs (YAML or JSON)
|
||||
// Note: built-in strategies need to imported manually in the bbgo cmd package.
|
||||
bbgo.RegisterStrategy("swing", &Strategy{})
|
||||
}
|
||||
|
||||
type Strategy struct {
|
||||
// The notification system will be injected into the strategy automatically.
|
||||
// This field will be injected automatically since it's a single exchange strategy.
|
||||
*bbgo.Notifiability
|
||||
|
||||
// OrderExecutor is an interface for submitting order.
|
||||
// This field will be injected automatically since it's a single exchange strategy.
|
||||
bbgo.OrderExecutor
|
||||
|
||||
// if Symbol string field is defined, bbgo will know it's a symbol-based strategy
|
||||
// The following embedded fields will be injected with the corresponding instances.
|
||||
|
||||
// MarketDataStore is a pointer only injection field. public trades, k-lines (candlestick)
|
||||
// and order book updates are maintained in the market data store.
|
||||
// This field will be injected automatically since we defined the Symbol field.
|
||||
*bbgo.MarketDataStore
|
||||
|
||||
// StandardIndicatorSet contains the standard indicators of a market (symbol)
|
||||
// This field will be injected automatically since we defined the Symbol field.
|
||||
*bbgo.StandardIndicatorSet
|
||||
|
||||
// Market stores the configuration of the market, for example, VolumePrecision, PricePrecision, MinLotSize... etc
|
||||
// This field will be injected automatically since we defined the Symbol field.
|
||||
types.Market
|
||||
|
||||
// These fields will be filled from the config file (it translates YAML to JSON)
|
||||
Symbol string `json:"symbol"`
|
||||
|
||||
// Interval is the interval of the kline channel we want to subscribe,
|
||||
// the kline event will trigger the strategy to check if we need to submit order.
|
||||
Interval string `json:"interval"`
|
||||
|
||||
// MinChange filters out the k-lines with small changes. so that our strategy will only be triggered
|
||||
// in specific events.
|
||||
MinChange float64 `json:"minChange"`
|
||||
|
||||
// BaseQuantity is the base quantity of the submit order. for both BUY and SELL, market order will be used.
|
||||
BaseQuantity float64 `json:"baseQuantity"`
|
||||
|
||||
// MovingAverageType is the moving average indicator type that we want to use,
|
||||
// it could be SMA or EWMA
|
||||
MovingAverageType string `json:"movingAverageType"`
|
||||
|
||||
// MovingAverageInterval is the interval of k-lines for the moving average indicator to calculate,
|
||||
// it could be "1m", "5m", "1h" and so on. note that, the moving averages are calculated from
|
||||
// the k-line data we subscribed
|
||||
MovingAverageInterval types.Interval `json:"movingAverageInterval"`
|
||||
|
||||
// MovingAverageWindow is the number of the window size of the moving average indicator.
|
||||
// The number of k-lines in the window. generally used window sizes are 7, 25 and 99 in the TradingView.
|
||||
MovingAverageWindow int `json:"movingAverageWindow"`
|
||||
}
|
||||
|
||||
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
||||
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.Interval})
|
||||
}
|
||||
|
||||
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||
var inc Float64Indicator
|
||||
var iw = types.IntervalWindow{Interval: s.MovingAverageInterval, Window: s.MovingAverageWindow}
|
||||
|
||||
switch s.MovingAverageType {
|
||||
case "SMA":
|
||||
inc = s.StandardIndicatorSet.GetSMA(iw)
|
||||
|
||||
case "EWMA", "EMA":
|
||||
inc = s.StandardIndicatorSet.GetEWMA(iw)
|
||||
|
||||
default:
|
||||
return errors.Errorf("unsupported moving average type: %s", s.MovingAverageType)
|
||||
|
||||
}
|
||||
|
||||
// session.Stream.OnKLineClosed
|
||||
session.Stream.OnKLineClosed(func(kline types.KLine) {
|
||||
// skip k-lines from other symbols
|
||||
if kline.Symbol != s.Symbol {
|
||||
return
|
||||
}
|
||||
|
||||
movingAveragePrice := inc.Last()
|
||||
|
||||
// skip it if it's near zero
|
||||
if movingAveragePrice < 0.0001 {
|
||||
return
|
||||
}
|
||||
|
||||
// skip if the change is not above the minChange
|
||||
if math.Abs(kline.GetChange()) < s.MinChange {
|
||||
return
|
||||
}
|
||||
|
||||
closePrice := kline.Close
|
||||
changePercentage := kline.GetChange() / kline.Open
|
||||
quantity := s.BaseQuantity * (1.0 + math.Abs(changePercentage))
|
||||
|
||||
trend := kline.GetTrend()
|
||||
switch trend {
|
||||
case 1:
|
||||
// if it goes up and it's above the moving average price, then we sell
|
||||
if closePrice > movingAveragePrice {
|
||||
s.notify(":chart_with_upwards_trend: closePrice %f is above movingAveragePrice %f, submitting SELL order", closePrice, movingAveragePrice)
|
||||
|
||||
_, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
||||
Symbol: s.Symbol,
|
||||
Market: s.Market,
|
||||
Side: types.SideTypeSell,
|
||||
Type: types.OrderTypeMarket,
|
||||
Quantity: quantity,
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("submit order error")
|
||||
}
|
||||
}
|
||||
case -1:
|
||||
// if it goes down and it's below the moving average price, then we buy
|
||||
if closePrice < movingAveragePrice {
|
||||
s.notify(":chart_with_downwards_trend: closePrice %f is below movingAveragePrice %f, submitting BUY order", closePrice, movingAveragePrice)
|
||||
|
||||
_, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
||||
Symbol: s.Symbol,
|
||||
Market: s.Market,
|
||||
Side: types.SideTypeBuy,
|
||||
Type: types.OrderTypeMarket,
|
||||
Quantity: quantity,
|
||||
})
|
||||
if err != nil {
|
||||
log.WithError(err).Error("submit order error")
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Strategy) notify(format string, args ...interface{}) {
|
||||
if channel, ok := s.RouteSymbol(s.Symbol); ok {
|
||||
s.NotifyTo(channel, format, args...)
|
||||
} else {
|
||||
s.Notify(format, args...)
|
||||
}
|
||||
}
|
|
@ -13,7 +13,7 @@ import (
|
|||
)
|
||||
|
||||
func init() {
|
||||
bbgo.RegisterExchangeStrategy("xpuremaker", &Strategy{})
|
||||
bbgo.RegisterStrategy("xpuremaker", &Strategy{})
|
||||
}
|
||||
|
||||
type Strategy struct {
|
||||
|
@ -29,8 +29,11 @@ type Strategy struct {
|
|||
activeOrders map[string]types.Order
|
||||
}
|
||||
|
||||
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
||||
session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{})
|
||||
}
|
||||
|
||||
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||
|
||||
s.book = types.NewStreamBook(s.Symbol)
|
||||
s.book.BindStream(session.Stream)
|
||||
|
|
|
@ -1,7 +1,20 @@
|
|||
package types
|
||||
|
||||
import "encoding/json"
|
||||
|
||||
type Interval string
|
||||
|
||||
func (i *Interval) UnmarshalJSON(b []byte) (err error) {
|
||||
var a string
|
||||
err = json.Unmarshal(b, &a)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
*i = Interval(a)
|
||||
return
|
||||
}
|
||||
|
||||
func (i Interval) String() string {
|
||||
return string(i)
|
||||
}
|
||||
|
@ -19,15 +32,25 @@ var Interval1d = Interval("1d")
|
|||
var Interval3d = Interval("3d")
|
||||
|
||||
var SupportedIntervals = map[Interval]int{
|
||||
Interval1m: 1,
|
||||
Interval5m: 5,
|
||||
Interval1m: 1,
|
||||
Interval5m: 5,
|
||||
Interval15m: 15,
|
||||
Interval30m: 30,
|
||||
Interval1h: 60,
|
||||
Interval2h: 60 * 2,
|
||||
Interval4h: 60 * 4,
|
||||
Interval6h: 60 * 6,
|
||||
Interval1h: 60,
|
||||
Interval2h: 60 * 2,
|
||||
Interval4h: 60 * 4,
|
||||
Interval6h: 60 * 6,
|
||||
Interval12h: 60 * 12,
|
||||
Interval1d: 60 * 24,
|
||||
Interval3d: 60 * 24 * 3,
|
||||
Interval1d: 60 * 24,
|
||||
Interval3d: 60 * 24 * 3,
|
||||
}
|
||||
|
||||
// IntervalWindow is used by the indicators
|
||||
type IntervalWindow struct {
|
||||
// The interval of kline
|
||||
Interval Interval
|
||||
|
||||
// The windows size of the indicator (EWMA and SMA)
|
||||
Window int
|
||||
}
|
||||
|
||||
|
|
|
@ -44,14 +44,20 @@ func (m Market) FormatPriceCurrency(val float64) string {
|
|||
}
|
||||
|
||||
func (m Market) FormatPrice(val float64) string {
|
||||
|
||||
p := math.Pow10(m.PricePrecision)
|
||||
val = math.Trunc(val*p) / p
|
||||
return strconv.FormatFloat(val, 'f', m.PricePrecision, 64)
|
||||
}
|
||||
|
||||
func (m Market) FormatQuantity(val float64) string {
|
||||
prec := int(math.Abs(math.Log10(m.MinLot)))
|
||||
p := math.Pow10(prec)
|
||||
val = math.Trunc(val*p) / p
|
||||
return strconv.FormatFloat(val, 'f', prec, 64)
|
||||
}
|
||||
|
||||
func (m Market) FormatVolume(val float64) string {
|
||||
p := math.Pow10(m.PricePrecision)
|
||||
p := math.Pow10(m.VolumePrecision)
|
||||
val = math.Trunc(val*p) / p
|
||||
return strconv.FormatFloat(val, 'f', m.VolumePrecision, 64)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user