scmaker: basic prototype

This commit is contained in:
c9s 2023-06-13 12:22:59 +08:00
parent a28081a5d2
commit 40f8283616
No known key found for this signature in database
GPG Key ID: 7385E7E464CB0A54
5 changed files with 427 additions and 1 deletions

50
config/scmaker.yaml Normal file
View File

@ -0,0 +1,50 @@
sessions:
binance:
exchange: max
envVarPrefix: max
exchangeStrategies:
- on: max
scmaker:
symbol: USDCUSDT
## adjustmentUpdateInterval is the interval for adjusting position
adjustmentUpdateInterval: 1m
## liquidityUpdateInterval is the interval for updating liquidity orders
liquidityUpdateInterval: 1h
midPriceEMA:
interval: 1h
window: 99
## priceRangeBollinger is used for the liquidity price range
priceRangeBollinger:
interval: 1h
window: 10
k: 1.0
numOfLiquidityLayers: 10
liquidityLayerTick: 0.01
strengthInterval: 1m
liquidityScale:
exp:
domain: [0, 10]
range: [100, 500]
backtest:
sessions:
- max
startTime: "2023-05-01"
endTime: "2023-06-01"
symbols:
- USDCUSDT
account:
max:
balances:
USDC: 5000
USDT: 5000

View File

@ -29,6 +29,7 @@ import (
_ "github.com/c9s/bbgo/pkg/strategy/rebalance"
_ "github.com/c9s/bbgo/pkg/strategy/rsmaker"
_ "github.com/c9s/bbgo/pkg/strategy/schedule"
_ "github.com/c9s/bbgo/pkg/strategy/scmaker"
_ "github.com/c9s/bbgo/pkg/strategy/skeleton"
_ "github.com/c9s/bbgo/pkg/strategy/supertrend"
_ "github.com/c9s/bbgo/pkg/strategy/support"

View File

@ -0,0 +1,44 @@
package scmaker
import (
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
type IntensityStream struct {
*indicator.Float64Series
Buy, Sell *indicator.RMAStream
window int
}
func Intensity(source indicator.KLineSubscription, window int) *IntensityStream {
s := &IntensityStream{
Float64Series: indicator.NewFloat64Series(),
window: window,
Buy: indicator.RMA2(indicator.NewFloat64Series(), window, false),
Sell: indicator.RMA2(indicator.NewFloat64Series(), window, false),
}
threshold := fixedpoint.NewFromFloat(100.0)
source.AddSubscriber(func(k types.KLine) {
volume := k.Volume.Float64()
// ignore zero volume events or <= 10usd events
if volume == 0.0 || k.Close.Mul(k.Volume).Compare(threshold) <= 0 {
return
}
c := k.Close.Compare(k.Open)
if c > 0 {
s.Buy.PushAndEmit(volume)
} else if c < 0 {
s.Sell.PushAndEmit(volume)
}
s.Float64Series.PushAndEmit(k.High.Sub(k.Low).Float64())
})
return s
}

View File

@ -0,0 +1,326 @@
package scmaker
import (
"context"
"fmt"
"math"
log "github.com/sirupsen/logrus"
"github.com/c9s/bbgo/pkg/bbgo"
"github.com/c9s/bbgo/pkg/fixedpoint"
"github.com/c9s/bbgo/pkg/indicator"
"github.com/c9s/bbgo/pkg/types"
)
const ID = "scmaker"
var ten = fixedpoint.NewFromInt(10)
type BollingerConfig struct {
Interval types.Interval `json:"interval"`
Window int `json:"window"`
K float64 `json:"k"`
}
func init() {
bbgo.RegisterStrategy(ID, &Strategy{})
}
// scmaker is a stable coin market maker
type Strategy struct {
Environment *bbgo.Environment
Market types.Market
Symbol string `json:"symbol"`
NumOfLiquidityLayers int `json:"numOfLiquidityLayers"`
LiquidityUpdateInterval types.Interval `json:"liquidityUpdateInterval"`
PriceRangeBollinger *BollingerConfig `json:"priceRangeBollinger"`
StrengthInterval types.Interval `json:"strengthInterval"`
AdjustmentUpdateInterval types.Interval `json:"adjustmentUpdateInterval"`
MidPriceEMA *types.IntervalWindow `json:"midPriceEMA"`
LiquiditySlideRule *bbgo.SlideRule `json:"liquidityScale"`
Position *types.Position `json:"position,omitempty" persistence:"position"`
ProfitStats *types.ProfitStats `json:"profitStats,omitempty" persistence:"profit_stats"`
session *bbgo.ExchangeSession
orderExecutor *bbgo.GeneralOrderExecutor
liquidityOrderBook, adjustmentOrderBook *bbgo.ActiveOrderBook
book *types.StreamOrderBook
liquidityScale bbgo.Scale
// indicators
ewma *indicator.EWMAStream
boll *indicator.BOLLStream
intensity *IntensityStream
}
func (s *Strategy) ID() string {
return ID
}
func (s *Strategy) InstanceID() string {
return fmt.Sprintf("%s:%s", ID, s.Symbol)
}
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
session.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{})
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.AdjustmentUpdateInterval})
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.LiquidityUpdateInterval})
if s.MidPriceEMA != nil {
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: s.MidPriceEMA.Interval})
}
}
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
instanceID := s.InstanceID()
s.session = session
s.book = types.NewStreamBook(s.Symbol)
s.book.BindStream(session.UserDataStream)
s.liquidityOrderBook = bbgo.NewActiveOrderBook(s.Symbol)
s.adjustmentOrderBook = bbgo.NewActiveOrderBook(s.Symbol)
// If position is nil, we need to allocate a new position for calculation
if s.Position == nil {
s.Position = types.NewPositionFromMarket(s.Market)
}
// Always update the position fields
s.Position.Strategy = ID
s.Position.StrategyInstanceID = instanceID
if s.session.MakerFeeRate.Sign() > 0 || s.session.TakerFeeRate.Sign() > 0 {
s.Position.SetExchangeFeeRate(s.session.ExchangeName, types.ExchangeFee{
MakerFeeRate: s.session.MakerFeeRate,
TakerFeeRate: s.session.TakerFeeRate,
})
}
if s.ProfitStats == nil {
s.ProfitStats = types.NewProfitStats(s.Market)
}
scale, err := s.LiquiditySlideRule.Scale()
if err != nil {
return err
}
if err := scale.Solve(); err != nil {
return err
}
s.liquidityScale = scale
s.orderExecutor = bbgo.NewGeneralOrderExecutor(session, s.Symbol, ID, instanceID, s.Position)
s.orderExecutor.BindEnvironment(s.Environment)
s.orderExecutor.BindProfitStats(s.ProfitStats)
s.orderExecutor.Bind()
s.orderExecutor.TradeCollector().OnPositionUpdate(func(position *types.Position) {
bbgo.Sync(ctx, s)
})
s.initializeMidPriceEMA(session)
s.initializePriceRangeBollinger(session)
s.initializeIntensityIndicator(session)
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.AdjustmentUpdateInterval, func(k types.KLine) {
s.placeAdjustmentOrders(ctx)
}))
session.MarketDataStream.OnKLineClosed(types.KLineWith(s.Symbol, s.LiquidityUpdateInterval, func(k types.KLine) {
s.placeLiquidityOrders(ctx)
}))
return nil
}
func (s *Strategy) initializeMidPriceEMA(session *bbgo.ExchangeSession) {
kLines := indicator.KLines(session.MarketDataStream, s.Symbol, s.MidPriceEMA.Interval)
s.ewma = indicator.EWMA2(indicator.ClosePrices(kLines), s.MidPriceEMA.Window)
}
func (s *Strategy) initializeIntensityIndicator(session *bbgo.ExchangeSession) {
kLines := indicator.KLines(session.MarketDataStream, s.Symbol, s.StrengthInterval)
s.intensity = Intensity(kLines, 10)
}
func (s *Strategy) initializePriceRangeBollinger(session *bbgo.ExchangeSession) {
kLines := indicator.KLines(session.MarketDataStream, s.Symbol, s.PriceRangeBollinger.Interval)
closePrices := indicator.ClosePrices(kLines)
s.boll = indicator.BOLL2(closePrices, s.PriceRangeBollinger.Window, s.PriceRangeBollinger.K)
}
func (s *Strategy) placeAdjustmentOrders(ctx context.Context) {
if s.Position.IsDust() {
return
}
}
func (s *Strategy) placeLiquidityOrders(ctx context.Context) {
_ = s.liquidityOrderBook.GracefulCancel(ctx, s.session.Exchange)
ticker, err := s.session.Exchange.QueryTicker(ctx, s.Symbol)
if logErr(err, "unable to query ticker") {
return
}
baseBal, _ := s.session.Account.Balance(s.Market.BaseCurrency)
quoteBal, _ := s.session.Account.Balance(s.Market.QuoteCurrency)
spread := ticker.Sell.Sub(ticker.Buy)
_ = spread
midPriceEMA := s.ewma.Last(0)
midPrice := fixedpoint.NewFromFloat(midPriceEMA)
makerQuota := &bbgo.QuotaTransaction{}
makerQuota.QuoteAsset.Add(quoteBal.Available)
makerQuota.BaseAsset.Add(baseBal.Available)
bandWidth := s.boll.Last(0)
_ = bandWidth
log.Infof("mid price ema: %f boll band width: %f", midPriceEMA, bandWidth)
var liqOrders []types.SubmitOrder
for i := 0; i <= s.NumOfLiquidityLayers; i++ {
fi := fixedpoint.NewFromInt(int64(i))
quantity := fixedpoint.NewFromFloat(s.liquidityScale.Call(float64(i)))
bidPrice := midPrice.Sub(s.Market.TickSize.Mul(fi))
askPrice := midPrice.Add(s.Market.TickSize.Mul(fi))
if i == 0 {
bidPrice = ticker.Buy
askPrice = ticker.Sell
}
log.Infof("layer #%d %f/%f = %f", i, askPrice.Float64(), bidPrice.Float64(), quantity.Float64())
placeBuy := true
placeSell := true
averageCost := s.Position.AverageCost
// when long position, do not place sell orders below the average cost
if !s.Position.IsDust() {
if s.Position.IsLong() && askPrice.Compare(averageCost) < 0 {
placeSell = false
}
if s.Position.IsShort() && bidPrice.Compare(averageCost) > 0 {
placeBuy = false
}
}
quoteQuantity := quantity.Mul(bidPrice)
if !makerQuota.QuoteAsset.Lock(quoteQuantity) {
placeBuy = false
}
if !makerQuota.BaseAsset.Lock(quantity) {
placeSell = false
}
if placeBuy {
liqOrders = append(liqOrders, types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeBuy,
Type: types.OrderTypeLimitMaker,
Quantity: quantity,
Price: bidPrice,
Market: s.Market,
TimeInForce: types.TimeInForceGTC,
})
}
if placeSell {
liqOrders = append(liqOrders, types.SubmitOrder{
Symbol: s.Symbol,
Side: types.SideTypeSell,
Type: types.OrderTypeLimitMaker,
Quantity: quantity,
Price: askPrice,
Market: s.Market,
TimeInForce: types.TimeInForceGTC,
})
}
}
_, err = s.orderExecutor.SubmitOrders(ctx, liqOrders...)
logErr(err, "unable to place liquidity orders")
}
func (s *Strategy) generateOrders(symbol string, side types.SideType, price, priceTick, baseQuantity fixedpoint.Value, numOrders int) (orders []types.SubmitOrder) {
var expBase = fixedpoint.Zero
switch side {
case types.SideTypeBuy:
if priceTick.Sign() > 0 {
priceTick = priceTick.Neg()
}
case types.SideTypeSell:
if priceTick.Sign() < 0 {
priceTick = priceTick.Neg()
}
}
decdigits := priceTick.Abs().NumIntDigits()
step := priceTick.Abs().MulExp(-decdigits + 1)
for i := 0; i < numOrders; i++ {
quantityExp := fixedpoint.NewFromFloat(math.Exp(expBase.Float64()))
volume := baseQuantity.Mul(quantityExp)
amount := volume.Mul(price)
// skip order less than 10usd
if amount.Compare(ten) < 0 {
log.Warnf("amount too small (< 10usd). price=%s volume=%s amount=%s",
price.String(), volume.String(), amount.String())
continue
}
orders = append(orders, types.SubmitOrder{
Symbol: symbol,
Side: side,
Type: types.OrderTypeLimit,
Price: price,
Quantity: volume,
})
log.Infof("%s order: %s @ %s", side, volume.String(), price.String())
if len(orders) >= numOrders {
break
}
price = price.Add(priceTick)
expBase = expBase.Add(step)
}
return orders
}
func logErr(err error, msgAndArgs ...interface{}) bool {
if err == nil {
return false
}
if len(msgAndArgs) == 0 {
log.WithError(err).Error(err.Error())
} else if len(msgAndArgs) == 1 {
msg := msgAndArgs[0].(string)
log.WithError(err).Error(msg)
} else if len(msgAndArgs) > 1 {
msg := msgAndArgs[0].(string)
log.WithError(err).Errorf(msg, msgAndArgs[1:]...)
}
return true
}

View File

@ -170,7 +170,12 @@ func (p *Position) NewMarketCloseOrder(percentage fixedpoint.Value) *SubmitOrder
}
}
func (p *Position) IsDust(price fixedpoint.Value) bool {
func (p *Position) IsDust(a ...fixedpoint.Value) bool {
price := p.AverageCost
if len(a) > 0 {
price = a[0]
}
base := p.Base.Abs()
return p.Market.IsDustQuantity(base, price)
}