Merge pull request #43 from c9s/strategy/swing

feature: add swing strategy
This commit is contained in:
Yo-An Lin 2020-10-31 18:27:33 +08:00 committed by GitHub
commit 301851bf35
26 changed files with 645 additions and 308 deletions

View File

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

View File

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

View File

@ -8,7 +8,7 @@ import (
)
func init() {
RegisterExchangeStrategy("test", &TestStrategy{})
RegisterStrategy("test", &TestStrategy{})
}
type TestStrategy struct {

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -10,7 +10,7 @@ import (
)
func init() {
bbgo.RegisterExchangeStrategy("skeleton", &Strategy{})
bbgo.RegisterStrategy("skeleton", &Strategy{})
}
type Strategy struct {

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

View File

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

View File

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

View File

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