mirror of
https://github.com/c9s/bbgo.git
synced 2024-09-21 00:31:10 +00:00
Merge pull request #180 from c9s/strategy/xmaker
feature: add strategy xmaker
This commit is contained in:
commit
1293dbb64b
70
README.md
70
README.md
|
@ -234,6 +234,76 @@ vim config/buyandhold.yaml
|
|||
bbgo run --config config/buyandhold.yaml
|
||||
```
|
||||
|
||||
## Adding New Built-in Strategy
|
||||
|
||||
Fork and clone this repository, Create a directory under `pkg/strategy/newstrategy`,
|
||||
write your strategy at `pkg/strategy/newstrategy/strategy.go`.
|
||||
|
||||
Define a strategy struct:
|
||||
|
||||
```go
|
||||
package newstrategy
|
||||
|
||||
import (
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
)
|
||||
|
||||
type Strategy struct {
|
||||
Symbol string `json:"symbol"`
|
||||
Param1 int `json:"param1"`
|
||||
Param2 int `json:"param2"`
|
||||
Param3 fixedpoint.Value `json:"param3"`
|
||||
}
|
||||
```
|
||||
|
||||
Register your strategy:
|
||||
|
||||
```go
|
||||
const ID = "newstrategy"
|
||||
|
||||
const stateKey = "state-v1"
|
||||
|
||||
var log = logrus.WithField("strategy", ID)
|
||||
|
||||
func init() {
|
||||
bbgo.RegisterStrategy(ID, &Strategy{})
|
||||
}
|
||||
```
|
||||
|
||||
Implement the strategy methods:
|
||||
|
||||
```go
|
||||
func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) {
|
||||
session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"})
|
||||
}
|
||||
|
||||
func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) error {
|
||||
// ....
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
Edit `pkg/cmd/builtin.go`, and import the package, like this:
|
||||
|
||||
```go
|
||||
package cmd
|
||||
|
||||
// import built-in strategies
|
||||
import (
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/bollgrid"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/buyandhold"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/flashcrash"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/grid"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/mirrormaker"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/pricealert"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/support"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/swing"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/trailingstop"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/xmaker"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/xpuremaker"
|
||||
)
|
||||
```
|
||||
|
||||
## Write your own strategy
|
||||
|
||||
Create your go package, and initialize the repository with `go mod` and add bbgo as a dependency:
|
||||
|
|
101
config/xmaker-btcusdt.yaml
Normal file
101
config/xmaker-btcusdt.yaml
Normal file
|
@ -0,0 +1,101 @@
|
|||
---
|
||||
notifications:
|
||||
slack:
|
||||
defaultChannel: "dev-bbgo"
|
||||
errorChannel: "bbgo-error"
|
||||
|
||||
# if you want to route channel by symbol
|
||||
symbolChannels:
|
||||
"^BTC": "btc"
|
||||
"^ETH": "eth"
|
||||
|
||||
# if you want to route channel by exchange session
|
||||
sessionChannels:
|
||||
max: "bbgo-max"
|
||||
binance: "bbgo-binance"
|
||||
|
||||
# routing rules
|
||||
routing:
|
||||
trade: "$symbol"
|
||||
order: "$silent"
|
||||
submitOrder: "$silent"
|
||||
pnL: "bbgo-pnl"
|
||||
|
||||
reportPnL:
|
||||
- averageCostBySymbols:
|
||||
- "BTCUSDT"
|
||||
- "BNBUSDT"
|
||||
of: binance
|
||||
when:
|
||||
- "@daily"
|
||||
- "@hourly"
|
||||
|
||||
persistence:
|
||||
json:
|
||||
directory: var/data
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
db: 0
|
||||
|
||||
sessions:
|
||||
max:
|
||||
exchange: max
|
||||
envVarPrefix: MAX
|
||||
|
||||
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
|
||||
max:
|
||||
# orderExecutor is one of the risk control
|
||||
orderExecutor:
|
||||
# symbol-routed order executor
|
||||
bySymbol:
|
||||
BTCUSDT:
|
||||
# basic risk control order executor
|
||||
basic:
|
||||
# keep at least X USDT (keep cash)
|
||||
minQuoteBalance: 100.0
|
||||
|
||||
# maximum BTC balance (don't buy too much)
|
||||
maxBaseAssetBalance: 1.0
|
||||
|
||||
# minimum BTC balance (don't sell too much)
|
||||
minBaseAssetBalance: 0.01
|
||||
|
||||
maxOrderAmount: 1000.0
|
||||
|
||||
crossExchangeStrategies:
|
||||
|
||||
- xmaker:
|
||||
symbol: BTCUSDT
|
||||
sourceExchange: binance
|
||||
makerExchange: max
|
||||
updateInterval: 1s
|
||||
|
||||
# disableHedge disables the hedge orders on the source exchange
|
||||
# disableHedge: true
|
||||
|
||||
hedgeInterval: 10s
|
||||
|
||||
margin: 0.004
|
||||
askMargin: 0.004
|
||||
bidMargin: 0.004
|
||||
|
||||
quantity: 0.001
|
||||
quantityMultiplier: 2
|
||||
|
||||
# numLayers means how many order we want to place on each side. 3 means we want 3 bid orders and 3 ask orders
|
||||
numLayers: 1
|
||||
# pips is the fraction numbers between each order. for BTC, 1 pip is 0.1,
|
||||
# 0.1 pip is 0.01, here we use 10, so we will get 18000.00, 18001.00 and
|
||||
# 18002.00
|
||||
pips: 10
|
||||
persistence:
|
||||
type: redis
|
||||
|
94
config/xmaker-ethusdt.yaml
Normal file
94
config/xmaker-ethusdt.yaml
Normal file
|
@ -0,0 +1,94 @@
|
|||
---
|
||||
notifications:
|
||||
slack:
|
||||
defaultChannel: "dev-bbgo"
|
||||
errorChannel: "bbgo-error"
|
||||
|
||||
# if you want to route channel by symbol
|
||||
symbolChannels:
|
||||
"^BTC": "btc"
|
||||
"^ETH": "eth"
|
||||
|
||||
# if you want to route channel by exchange session
|
||||
sessionChannels:
|
||||
max: "bbgo-max"
|
||||
binance: "bbgo-binance"
|
||||
|
||||
# routing rules
|
||||
routing:
|
||||
trade: "$symbol"
|
||||
order: "$silent"
|
||||
submitOrder: "$silent"
|
||||
pnL: "bbgo-pnl"
|
||||
|
||||
persistence:
|
||||
json:
|
||||
directory: var/data
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
db: 0
|
||||
|
||||
sessions:
|
||||
max:
|
||||
exchange: max
|
||||
envVarPrefix: max
|
||||
|
||||
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
|
||||
max:
|
||||
# orderExecutor is one of the risk control
|
||||
orderExecutor:
|
||||
# symbol-routed order executor
|
||||
bySymbol:
|
||||
ETHUSDT:
|
||||
# basic risk control order executor
|
||||
basic:
|
||||
# keep at least X USDT (keep cash)
|
||||
minQuoteBalance: 100.0
|
||||
|
||||
# maximum ETH balance (don't buy too much)
|
||||
maxBaseAssetBalance: 10.0
|
||||
|
||||
# minimum ETH balance (don't sell too much)
|
||||
minBaseAssetBalance: 0.0
|
||||
|
||||
maxOrderAmount: 1000.0
|
||||
|
||||
crossExchangeStrategies:
|
||||
|
||||
- xmaker:
|
||||
symbol: ETHUSDT
|
||||
sourceExchange: binance
|
||||
makerExchange: max
|
||||
updateInterval: 2s
|
||||
|
||||
# disableHedge disables the hedge orders on the source exchange
|
||||
# disableHedge: true
|
||||
|
||||
hedgeInterval: 10s
|
||||
|
||||
margin: 0.004
|
||||
askMargin: 0.004
|
||||
bidMargin: 0.004
|
||||
|
||||
quantity: 0.01
|
||||
quantityMultiplier: 2
|
||||
|
||||
# numLayers means how many order we want to place on each side. 3 means we want 3 bid orders and 3 ask orders
|
||||
numLayers: 2
|
||||
|
||||
# pips is the fraction numbers between each order. for BTC, 1 pip is 0.1,
|
||||
# 0.1 pip is 0.01, here we use 10, so we will get 18000.00, 18001.00 and
|
||||
# 18002.00
|
||||
pips: 10
|
||||
|
||||
persistence:
|
||||
type: redis
|
||||
|
78
config/xmaker.yaml
Normal file
78
config/xmaker.yaml
Normal file
|
@ -0,0 +1,78 @@
|
|||
---
|
||||
notifications:
|
||||
slack:
|
||||
defaultChannel: "dev-bbgo"
|
||||
errorChannel: "bbgo-error"
|
||||
|
||||
# if you want to route channel by symbol
|
||||
symbolChannels:
|
||||
"^BTC": "btc"
|
||||
"^ETH": "eth"
|
||||
|
||||
# if you want to route channel by exchange session
|
||||
sessionChannels:
|
||||
max: "bbgo-max"
|
||||
binance: "bbgo-binance"
|
||||
|
||||
# routing rules
|
||||
routing:
|
||||
trade: "$symbol"
|
||||
order: "$silent"
|
||||
submitOrder: "$silent"
|
||||
pnL: "bbgo-pnl"
|
||||
|
||||
reportPnL:
|
||||
- averageCostBySymbols:
|
||||
- "BTCUSDT"
|
||||
- "BNBUSDT"
|
||||
of: binance
|
||||
when:
|
||||
- "@daily"
|
||||
- "@hourly"
|
||||
|
||||
persistence:
|
||||
json:
|
||||
directory: var/data
|
||||
redis:
|
||||
host: 127.0.0.1
|
||||
port: 6379
|
||||
db: 0
|
||||
|
||||
sessions:
|
||||
max:
|
||||
exchange: max
|
||||
envVarPrefix: max
|
||||
|
||||
binance:
|
||||
exchange: binance
|
||||
envVarPrefix: binance
|
||||
|
||||
crossExchangeStrategies:
|
||||
|
||||
- xmaker:
|
||||
symbol: "BTCUSDT"
|
||||
sourceExchange: binance
|
||||
makerExchange: max
|
||||
updateInterval: 1s
|
||||
|
||||
# disableHedge disables the hedge orders on the source exchange
|
||||
# disableHedge: true
|
||||
|
||||
hedgeInterval: 10s
|
||||
|
||||
margin: 0.004
|
||||
askMargin: 0.004
|
||||
bidMargin: 0.004
|
||||
|
||||
quantity: 0.001
|
||||
quantityMultiplier: 2
|
||||
|
||||
# numLayers means how many order we want to place on each side. 3 means we want 3 bid orders and 3 ask orders
|
||||
numLayers: 1
|
||||
# pips is the fraction numbers between each order. for BTC, 1 pip is 0.1,
|
||||
# 0.1 pip is 0.01, here we use 10, so we will get 18000.00, 18001.00 and
|
||||
# 18002.00
|
||||
pips: 10
|
||||
persistence:
|
||||
type: redis
|
||||
|
|
@ -173,6 +173,8 @@ func (trader *Trader) Subscribe() {
|
|||
for _, strategy := range strategies {
|
||||
if subscriber, ok := strategy.(ExchangeSessionSubscriber); ok {
|
||||
subscriber.Subscribe(session)
|
||||
} else {
|
||||
log.Errorf("strategy %s does not implement ExchangeSessionSubscriber", strategy.ID())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -180,6 +182,8 @@ func (trader *Trader) Subscribe() {
|
|||
for _, strategy := range trader.crossExchangeStrategies {
|
||||
if subscriber, ok := strategy.(CrossExchangeSessionSubscriber); ok {
|
||||
subscriber.CrossSubscribe(trader.environment.sessions)
|
||||
} else {
|
||||
log.Errorf("strategy %s does not implement CrossExchangeSessionSubscriber", strategy.ID())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -11,5 +11,6 @@ import (
|
|||
_ "github.com/c9s/bbgo/pkg/strategy/support"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/swing"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/trailingstop"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/xmaker"
|
||||
_ "github.com/c9s/bbgo/pkg/strategy/xpuremaker"
|
||||
)
|
||||
|
|
|
@ -311,9 +311,15 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder
|
|||
|
||||
req := e.client.OrderService.NewCreateOrderRequest().
|
||||
Market(toLocalSymbol(order.Symbol)).
|
||||
OrderType(string(orderType)).
|
||||
Side(toLocalSideType(order.Side))
|
||||
|
||||
// convert limit maker to post_only
|
||||
if order.Type == types.OrderTypeLimitMaker {
|
||||
req.OrderType(string(maxapi.OrderTypePostOnly))
|
||||
} else {
|
||||
req.OrderType(string(orderType))
|
||||
}
|
||||
|
||||
if len(order.ClientOrderID) > 0 {
|
||||
req.ClientOrderID(order.ClientOrderID)
|
||||
} else {
|
||||
|
@ -331,7 +337,7 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder
|
|||
|
||||
// set price field for limit orders
|
||||
switch order.Type {
|
||||
case types.OrderTypeStopLimit, types.OrderTypeLimit:
|
||||
case types.OrderTypeStopLimit, types.OrderTypeLimit, types.OrderTypeLimitMaker:
|
||||
if len(order.PriceString) > 0 {
|
||||
req.Price(order.PriceString)
|
||||
} else if order.Market.Symbol != "" {
|
||||
|
@ -339,6 +345,7 @@ func (e *Exchange) SubmitOrders(ctx context.Context, orders ...types.SubmitOrder
|
|||
}
|
||||
}
|
||||
|
||||
|
||||
// set stop price field for limit orders
|
||||
switch order.Type {
|
||||
case types.OrderTypeStopLimit, types.OrderTypeStopMarket:
|
||||
|
|
|
@ -33,6 +33,7 @@ type OrderType string
|
|||
const (
|
||||
OrderTypeMarket = OrderType("market")
|
||||
OrderTypeLimit = OrderType("limit")
|
||||
OrderTypePostOnly = OrderType("post_only")
|
||||
OrderTypeStopLimit = OrderType("stop_limit")
|
||||
OrderTypeStopMarket = OrderType("stop_market")
|
||||
)
|
||||
|
@ -236,7 +237,7 @@ type OrderCancelAllRequestParams struct {
|
|||
|
||||
Side string `json:"side,omitempty"`
|
||||
Market string `json:"market,omitempty"`
|
||||
GroupID int64 `json:"groupID,omitempty"`
|
||||
GroupID int64 `json:"groupID,omitempty"`
|
||||
}
|
||||
|
||||
type OrderCancelAllRequest struct {
|
||||
|
@ -417,62 +418,90 @@ func (s *OrderService) NewCreateMultiOrderRequest() *CreateMultiOrderRequest {
|
|||
return &CreateMultiOrderRequest{client: s.client}
|
||||
}
|
||||
|
||||
type CreateOrderRequestParams struct {
|
||||
*PrivateRequestParams
|
||||
|
||||
Market string `json:"market"`
|
||||
Volume string `json:"volume"`
|
||||
Price string `json:"price,omitempty"`
|
||||
StopPrice string `json:"stop_price,omitempty"`
|
||||
Side string `json:"side"`
|
||||
OrderType string `json:"ord_type"`
|
||||
ClientOrderID string `json:"client_oid,omitempty"`
|
||||
GroupID string `json:"group_id,omitempty"`
|
||||
}
|
||||
|
||||
type CreateOrderRequest struct {
|
||||
client *RestClient
|
||||
|
||||
params CreateOrderRequestParams
|
||||
market *string
|
||||
volume *string
|
||||
price *string
|
||||
stopPrice *string
|
||||
side *string
|
||||
orderType *string
|
||||
clientOrderID *string
|
||||
groupID *string
|
||||
}
|
||||
|
||||
func (r *CreateOrderRequest) Market(market string) *CreateOrderRequest {
|
||||
r.params.Market = market
|
||||
r.market = &market
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *CreateOrderRequest) Volume(volume string) *CreateOrderRequest {
|
||||
r.params.Volume = volume
|
||||
r.volume = &volume
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *CreateOrderRequest) Price(price string) *CreateOrderRequest {
|
||||
r.params.Price = price
|
||||
r.price = &price
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *CreateOrderRequest) StopPrice(price string) *CreateOrderRequest {
|
||||
r.params.StopPrice = price
|
||||
r.stopPrice = &price
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *CreateOrderRequest) Side(side string) *CreateOrderRequest {
|
||||
r.params.Side = side
|
||||
r.side = &side
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *CreateOrderRequest) OrderType(orderType string) *CreateOrderRequest {
|
||||
r.params.OrderType = orderType
|
||||
r.orderType = &orderType
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *CreateOrderRequest) ClientOrderID(clientOrderID string) *CreateOrderRequest {
|
||||
r.params.ClientOrderID = clientOrderID
|
||||
r.clientOrderID = &clientOrderID
|
||||
return r
|
||||
}
|
||||
|
||||
func (r *CreateOrderRequest) Do(ctx context.Context) (order *Order, err error) {
|
||||
req, err := r.client.newAuthenticatedRequest("POST", "v2/orders", &r.params)
|
||||
var payload = map[string]interface{}{}
|
||||
|
||||
if r.market != nil {
|
||||
payload["market"] = r.market
|
||||
}
|
||||
|
||||
if r.volume != nil {
|
||||
payload["volume"] = r.volume
|
||||
}
|
||||
|
||||
if r.price != nil {
|
||||
payload["price"] = r.price
|
||||
}
|
||||
|
||||
if r.stopPrice != nil {
|
||||
payload["stop_price"] = r.stopPrice
|
||||
}
|
||||
|
||||
if r.side != nil {
|
||||
payload["side"] = r.side
|
||||
}
|
||||
|
||||
if r.orderType != nil {
|
||||
payload["ord_type"] = r.orderType
|
||||
}
|
||||
|
||||
if r.clientOrderID != nil {
|
||||
payload["client_oid"] = r.clientOrderID
|
||||
}
|
||||
|
||||
if r.groupID != nil {
|
||||
payload["group_id"] = r.groupID
|
||||
}
|
||||
|
||||
req, err := r.client.newAuthenticatedRequest("POST", "v2/orders", payload)
|
||||
if err != nil {
|
||||
return order, errors.Wrapf(err, "order create error")
|
||||
}
|
||||
|
|
489
pkg/strategy/xmaker/strategy.go
Normal file
489
pkg/strategy/xmaker/strategy.go
Normal file
|
@ -0,0 +1,489 @@
|
|||
package xmaker
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"math"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/bbgo"
|
||||
"github.com/c9s/bbgo/pkg/fixedpoint"
|
||||
"github.com/c9s/bbgo/pkg/service"
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
)
|
||||
|
||||
var defaultMargin = fixedpoint.NewFromFloat(0.01)
|
||||
|
||||
var defaultQuantity = fixedpoint.NewFromFloat(0.001)
|
||||
|
||||
const ID = "xmaker"
|
||||
|
||||
const stateKey = "state-v1"
|
||||
|
||||
var log = logrus.WithField("strategy", ID)
|
||||
|
||||
func init() {
|
||||
bbgo.RegisterStrategy(ID, &Strategy{})
|
||||
}
|
||||
|
||||
func (s *Strategy) ID() string {
|
||||
return ID
|
||||
}
|
||||
|
||||
type State struct {
|
||||
HedgePosition fixedpoint.Value `json:"hedgePosition"`
|
||||
}
|
||||
|
||||
type Strategy struct {
|
||||
*bbgo.Graceful
|
||||
*bbgo.Notifiability
|
||||
*bbgo.Persistence
|
||||
|
||||
Symbol string `json:"symbol"`
|
||||
SourceExchange string `json:"sourceExchange"`
|
||||
MakerExchange string `json:"makerExchange"`
|
||||
|
||||
UpdateInterval types.Duration `json:"updateInterval"`
|
||||
HedgeInterval types.Duration `json:"hedgeInterval"`
|
||||
|
||||
Margin fixedpoint.Value `json:"margin"`
|
||||
BidMargin fixedpoint.Value `json:"bidMargin"`
|
||||
AskMargin fixedpoint.Value `json:"askMargin"`
|
||||
Quantity fixedpoint.Value `json:"quantity"`
|
||||
QuantityMultiplier fixedpoint.Value `json:"quantityMultiplier"`
|
||||
DisableHedge bool `json:"disableHedge"`
|
||||
|
||||
NumLayers int `json:"numLayers"`
|
||||
Pips int `json:"pips"`
|
||||
|
||||
makerSession *bbgo.ExchangeSession
|
||||
sourceSession *bbgo.ExchangeSession
|
||||
|
||||
sourceMarket types.Market
|
||||
makerMarket types.Market
|
||||
|
||||
state *State
|
||||
|
||||
book *types.StreamOrderBook
|
||||
activeMakerOrders *bbgo.LocalActiveOrderBook
|
||||
|
||||
orderStore *bbgo.OrderStore
|
||||
|
||||
lastPrice float64
|
||||
groupID int64
|
||||
|
||||
stopC chan struct{}
|
||||
}
|
||||
|
||||
func (s *Strategy) CrossSubscribe(sessions map[string]*bbgo.ExchangeSession) {
|
||||
sourceSession, ok := sessions[s.SourceExchange]
|
||||
if !ok {
|
||||
panic(fmt.Errorf("source session %s is not defined", s.SourceExchange))
|
||||
}
|
||||
|
||||
sourceSession.Subscribe(types.BookChannel, s.Symbol, types.SubscribeOptions{})
|
||||
|
||||
makerSession, ok := sessions[s.MakerExchange]
|
||||
if !ok {
|
||||
panic(fmt.Errorf("maker session %s is not defined", s.MakerExchange))
|
||||
}
|
||||
makerSession.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: "1m"})
|
||||
}
|
||||
|
||||
func (s *Strategy) updateQuote(ctx context.Context) {
|
||||
if err := s.makerSession.Exchange.CancelOrders(ctx, s.activeMakerOrders.Orders()...); err != nil {
|
||||
log.WithError(err).Errorf("can not cancel orders")
|
||||
return
|
||||
}
|
||||
|
||||
// avoid unlock issue
|
||||
time.Sleep(800 * time.Millisecond)
|
||||
|
||||
sourceBook := s.book.Get()
|
||||
if len(sourceBook.Bids) == 0 || len(sourceBook.Asks) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
if valid, err := sourceBook.IsValid(); !valid {
|
||||
log.WithError(err).Errorf("invalid order book: %v", err)
|
||||
return
|
||||
}
|
||||
|
||||
bestBidPrice := sourceBook.Bids[0].Price
|
||||
bestAskPrice := sourceBook.Asks[0].Price
|
||||
log.Infof("best bid price %f, best ask price: %f", bestBidPrice.Float64(), bestAskPrice.Float64())
|
||||
|
||||
bidQuantity := s.Quantity
|
||||
bidPrice := bestBidPrice.MulFloat64(1.0 - s.BidMargin.Float64())
|
||||
|
||||
askQuantity := s.Quantity
|
||||
askPrice := bestAskPrice.MulFloat64(1.0 + s.AskMargin.Float64())
|
||||
|
||||
log.Infof("quote bid price: %f ask price: %f", bidPrice.Float64(), askPrice.Float64())
|
||||
|
||||
var disableMakerBid = false
|
||||
var disableMakerAsk = false
|
||||
var submitOrders []types.SubmitOrder
|
||||
|
||||
// we load the balances from the account,
|
||||
// however, while we're generating the orders,
|
||||
// the balance may have a chance to be deducted by other strategies or manual orders submitted by the user
|
||||
makerBalances := s.makerSession.Account.Balances()
|
||||
makerQuota := &bbgo.QuotaTransaction{}
|
||||
if b, ok := makerBalances[s.makerMarket.BaseCurrency]; ok {
|
||||
makerQuota.BaseAsset.Add(b.Available)
|
||||
|
||||
if b.Available.Float64() <= s.makerMarket.MinQuantity {
|
||||
disableMakerAsk = true
|
||||
}
|
||||
}
|
||||
|
||||
if b, ok := makerBalances[s.makerMarket.QuoteCurrency]; ok {
|
||||
makerQuota.QuoteAsset.Add(b.Available)
|
||||
|
||||
if b.Available.Float64() <= s.makerMarket.MinNotional {
|
||||
disableMakerBid = true
|
||||
}
|
||||
}
|
||||
|
||||
hedgeBalances := s.sourceSession.Account.Balances()
|
||||
hedgeQuota := &bbgo.QuotaTransaction{}
|
||||
if b, ok := hedgeBalances[s.sourceMarket.BaseCurrency]; ok {
|
||||
hedgeQuota.BaseAsset.Add(b.Available)
|
||||
|
||||
// to make bid orders, we need enough base asset in the foreign exchange,
|
||||
// if the base asset balance is not enough for selling
|
||||
if b.Available.Float64() <= s.sourceMarket.MinQuantity {
|
||||
disableMakerBid = true
|
||||
}
|
||||
}
|
||||
|
||||
if b, ok := hedgeBalances[s.sourceMarket.QuoteCurrency]; ok {
|
||||
hedgeQuota.QuoteAsset.Add(b.Available)
|
||||
|
||||
// to make ask orders, we need enough quote asset in the foreign exchange,
|
||||
// if the quote asset balance is not enough for buying
|
||||
if b.Available.Float64() <= s.sourceMarket.MinNotional {
|
||||
disableMakerAsk = true
|
||||
}
|
||||
}
|
||||
|
||||
if disableMakerAsk && disableMakerBid {
|
||||
log.Warn("maker is disabled due to insufficient balances")
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < s.NumLayers; i++ {
|
||||
// for maker bid orders
|
||||
if !disableMakerBid {
|
||||
if makerQuota.QuoteAsset.Lock(bidQuantity.Mul(bidPrice)) && hedgeQuota.BaseAsset.Lock(bidQuantity) {
|
||||
// if we bought, then we need to sell the base from the hedge session
|
||||
submitOrders = append(submitOrders, types.SubmitOrder{
|
||||
Symbol: s.Symbol,
|
||||
Type: types.OrderTypeLimit,
|
||||
Side: types.SideTypeBuy,
|
||||
Price: bidPrice.Float64(),
|
||||
Quantity: bidQuantity.Float64(),
|
||||
TimeInForce: "GTC",
|
||||
GroupID: s.groupID,
|
||||
})
|
||||
|
||||
makerQuota.Commit()
|
||||
hedgeQuota.Commit()
|
||||
} else {
|
||||
makerQuota.Rollback()
|
||||
hedgeQuota.Rollback()
|
||||
}
|
||||
bidPrice -= fixedpoint.NewFromFloat(s.makerMarket.TickSize * float64(s.Pips))
|
||||
bidQuantity.Mul(s.QuantityMultiplier)
|
||||
}
|
||||
|
||||
// for maker ask orders
|
||||
if !disableMakerAsk {
|
||||
if makerQuota.BaseAsset.Lock(askQuantity) && hedgeQuota.QuoteAsset.Lock(askQuantity.Mul(askPrice)) {
|
||||
// if we bought, then we need to sell the base from the hedge session
|
||||
submitOrders = append(submitOrders, types.SubmitOrder{
|
||||
Symbol: s.Symbol,
|
||||
Type: types.OrderTypeLimit,
|
||||
Side: types.SideTypeSell,
|
||||
Price: askPrice.Float64(),
|
||||
Quantity: askQuantity.Float64(),
|
||||
TimeInForce: "GTC",
|
||||
GroupID: s.groupID,
|
||||
})
|
||||
makerQuota.Commit()
|
||||
hedgeQuota.Commit()
|
||||
} else {
|
||||
makerQuota.Rollback()
|
||||
hedgeQuota.Rollback()
|
||||
}
|
||||
askPrice += fixedpoint.NewFromFloat(s.makerMarket.TickSize * float64(s.Pips))
|
||||
askQuantity.Mul(s.QuantityMultiplier)
|
||||
}
|
||||
}
|
||||
|
||||
if len(submitOrders) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
makerOrderExecutor := &bbgo.ExchangeOrderExecutor{Session: s.makerSession}
|
||||
makerOrders, err := makerOrderExecutor.SubmitOrders(ctx, submitOrders...)
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("order error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s.activeMakerOrders.Add(makerOrders...)
|
||||
s.orderStore.Add(makerOrders...)
|
||||
}
|
||||
|
||||
func (s *Strategy) Hedge(ctx context.Context, pos fixedpoint.Value) {
|
||||
side := types.SideTypeBuy
|
||||
|
||||
if pos == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
quantity := pos
|
||||
if pos < 0 {
|
||||
side = types.SideTypeSell
|
||||
quantity = -pos
|
||||
}
|
||||
|
||||
lastPrice := s.lastPrice
|
||||
sourceBook := s.book.Get()
|
||||
switch side {
|
||||
|
||||
case types.SideTypeBuy:
|
||||
if len(sourceBook.Asks) > 0 {
|
||||
if pv, ok := sourceBook.Asks.First(); ok {
|
||||
lastPrice = pv.Price.Float64()
|
||||
}
|
||||
}
|
||||
|
||||
case types.SideTypeSell:
|
||||
if len(sourceBook.Bids) > 0 {
|
||||
if pv, ok := sourceBook.Bids.First(); ok {
|
||||
lastPrice = pv.Price.Float64()
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
notional := quantity.MulFloat64(lastPrice)
|
||||
if notional.Float64() <= s.sourceMarket.MinNotional {
|
||||
log.Warnf("less than min notional %f, skipping", notional.Float64())
|
||||
return
|
||||
}
|
||||
|
||||
s.Notifiability.Notify("submitting hedge order: %s %s %f", s.Symbol, side, quantity.Float64())
|
||||
orderExecutor := &bbgo.ExchangeOrderExecutor{Session: s.sourceSession}
|
||||
returnOrders, err := orderExecutor.SubmitOrders(ctx, types.SubmitOrder{
|
||||
Symbol: s.Symbol,
|
||||
Type: types.OrderTypeMarket,
|
||||
Side: side,
|
||||
Quantity: quantity.Float64(),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
log.WithError(err).Errorf("market order submit error: %s", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
s.orderStore.Add(returnOrders...)
|
||||
}
|
||||
|
||||
func (s *Strategy) handleTradeUpdate(trade types.Trade) {
|
||||
log.Infof("received trade %+v", trade)
|
||||
|
||||
if trade.Symbol != s.Symbol {
|
||||
return
|
||||
}
|
||||
|
||||
if !s.orderStore.Exists(trade.OrderID) {
|
||||
return
|
||||
}
|
||||
|
||||
q := fixedpoint.NewFromFloat(trade.Quantity)
|
||||
switch trade.Side {
|
||||
case types.SideTypeSell:
|
||||
q = -q
|
||||
|
||||
case types.SideTypeBuy:
|
||||
|
||||
case types.SideTypeSelf:
|
||||
// ignore self trades
|
||||
|
||||
default:
|
||||
log.Infof("ignore non sell/buy side trades, got: %v", trade.Side)
|
||||
return
|
||||
|
||||
}
|
||||
|
||||
log.Infof("identified trade %d with an existing order: %d", trade.ID, trade.OrderID)
|
||||
s.Notify("identified %s trade %d with an existing order: %d", trade.Symbol, trade.ID, trade.OrderID)
|
||||
|
||||
s.state.HedgePosition.AtomicAdd(q)
|
||||
|
||||
pos := s.state.HedgePosition.AtomicLoad()
|
||||
|
||||
log.Warnf("position changed: %f", pos.Float64())
|
||||
s.Notifiability.Notify("%s position is changed to %f", s.Symbol, pos.Float64())
|
||||
|
||||
s.lastPrice = trade.Price
|
||||
}
|
||||
|
||||
func (s *Strategy) CrossRun(ctx context.Context, _ bbgo.OrderExecutionRouter, sessions map[string]*bbgo.ExchangeSession) error {
|
||||
// configure default values
|
||||
if s.UpdateInterval == 0 {
|
||||
s.UpdateInterval = types.Duration(time.Second)
|
||||
}
|
||||
|
||||
if s.HedgeInterval == 0 {
|
||||
s.HedgeInterval = types.Duration(10 * time.Second)
|
||||
}
|
||||
|
||||
if s.NumLayers == 0 {
|
||||
s.NumLayers = 1
|
||||
}
|
||||
|
||||
if s.BidMargin == 0 {
|
||||
if s.Margin != 0 {
|
||||
s.BidMargin = s.Margin
|
||||
} else {
|
||||
s.BidMargin = defaultMargin
|
||||
}
|
||||
}
|
||||
|
||||
if s.AskMargin == 0 {
|
||||
if s.Margin != 0 {
|
||||
s.AskMargin = s.Margin
|
||||
} else {
|
||||
s.AskMargin = defaultMargin
|
||||
}
|
||||
}
|
||||
|
||||
if s.Quantity == 0 {
|
||||
s.Quantity = defaultQuantity
|
||||
}
|
||||
|
||||
// configure sessions
|
||||
sourceSession, ok := sessions[s.SourceExchange]
|
||||
if !ok {
|
||||
return fmt.Errorf("source exchange session %s is not defined", s.SourceExchange)
|
||||
}
|
||||
|
||||
s.sourceSession = sourceSession
|
||||
|
||||
makerSession, ok := sessions[s.MakerExchange]
|
||||
if !ok {
|
||||
return fmt.Errorf("maker exchange session %s is not defined", s.MakerExchange)
|
||||
}
|
||||
|
||||
s.makerSession = makerSession
|
||||
|
||||
s.sourceMarket, ok = s.sourceSession.Market(s.Symbol)
|
||||
if !ok {
|
||||
return fmt.Errorf("source session market %s is not defined", s.Symbol)
|
||||
}
|
||||
|
||||
s.makerMarket, ok = s.makerSession.Market(s.Symbol)
|
||||
if !ok {
|
||||
return fmt.Errorf("maker session market %s is not defined", s.Symbol)
|
||||
}
|
||||
|
||||
// restore state
|
||||
instanceID := fmt.Sprintf("%s-%s", ID, s.Symbol)
|
||||
s.groupID = generateGroupID(instanceID)
|
||||
log.Infof("using group id %d from fnv(%s)", s.groupID, instanceID)
|
||||
|
||||
var state State
|
||||
|
||||
// load position
|
||||
if err := s.Persistence.Load(&state, stateKey); err != nil {
|
||||
if err != service.ErrPersistenceNotExists {
|
||||
return err
|
||||
}
|
||||
|
||||
s.state = &State{}
|
||||
} else {
|
||||
// loaded successfully
|
||||
s.state = &state
|
||||
|
||||
log.Infof("state is restored: %+v", s.state)
|
||||
s.Notify("position is restored => %f", s.state.HedgePosition.Float64())
|
||||
}
|
||||
|
||||
s.book = types.NewStreamBook(s.Symbol)
|
||||
s.book.BindStream(s.sourceSession.Stream)
|
||||
|
||||
s.sourceSession.Stream.OnTradeUpdate(s.handleTradeUpdate)
|
||||
s.makerSession.Stream.OnTradeUpdate(s.handleTradeUpdate)
|
||||
|
||||
s.activeMakerOrders = bbgo.NewLocalActiveOrderBook()
|
||||
s.activeMakerOrders.BindStream(s.makerSession.Stream)
|
||||
|
||||
s.orderStore = bbgo.NewOrderStore(s.Symbol)
|
||||
s.orderStore.BindStream(s.sourceSession.Stream)
|
||||
s.orderStore.BindStream(s.makerSession.Stream)
|
||||
|
||||
s.stopC = make(chan struct{})
|
||||
|
||||
go func() {
|
||||
posTicker := time.NewTicker(s.HedgeInterval.Duration())
|
||||
defer posTicker.Stop()
|
||||
|
||||
ticker := time.NewTicker(s.UpdateInterval.Duration())
|
||||
defer ticker.Stop()
|
||||
for {
|
||||
select {
|
||||
|
||||
case <-s.stopC:
|
||||
return
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
|
||||
case <-ticker.C:
|
||||
s.updateQuote(ctx)
|
||||
|
||||
case <-posTicker.C:
|
||||
position := s.state.HedgePosition.AtomicLoad()
|
||||
abspos := math.Abs(position.Float64())
|
||||
if !s.DisableHedge && abspos > s.sourceMarket.MinQuantity {
|
||||
log.Infof("found position: %f", position.Float64())
|
||||
s.Hedge(ctx, -position)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
s.Graceful.OnShutdown(func(ctx context.Context, wg *sync.WaitGroup) {
|
||||
defer wg.Done()
|
||||
|
||||
close(s.stopC)
|
||||
|
||||
if err := s.Persistence.Save(&s.state, stateKey); err != nil {
|
||||
log.WithError(err).Errorf("can not save state: %+v", s.state)
|
||||
} else {
|
||||
log.Infof("state is saved => %+v", s.state)
|
||||
s.Notify("hedge position %f is saved", s.state.HedgePosition.Float64())
|
||||
}
|
||||
|
||||
if err := s.makerSession.Exchange.CancelOrders(ctx, s.activeMakerOrders.Orders()...); err != nil {
|
||||
log.WithError(err).Errorf("can not cancel orders")
|
||||
}
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateGroupID(s string) int64 {
|
||||
h := fnv.New32a()
|
||||
h.Write([]byte(s))
|
||||
return int64(h.Sum32())
|
||||
}
|
|
@ -58,6 +58,7 @@ type OrderType string
|
|||
|
||||
const (
|
||||
OrderTypeLimit OrderType = "LIMIT"
|
||||
OrderTypeLimitMaker OrderType = "LIMIT_MAKER"
|
||||
OrderTypeMarket OrderType = "MARKET"
|
||||
OrderTypeStopLimit OrderType = "STOP_LIMIT"
|
||||
OrderTypeStopMarket OrderType = "STOP_MARKET"
|
||||
|
|
Loading…
Reference in New Issue
Block a user