mirror of
https://github.com/c9s/bbgo.git
synced 2024-11-10 17:13:51 +00:00
Merge pull request #168 from c9s/feature/mark-trade-strategy
This commit is contained in:
commit
40b376802e
|
@ -1,6 +1,8 @@
|
|||
package bbgo
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
|
||||
log "github.com/sirupsen/logrus"
|
||||
|
||||
"github.com/c9s/bbgo/pkg/types"
|
||||
|
@ -22,6 +24,11 @@ func NewLocalActiveOrderBook() *LocalActiveOrderBook {
|
|||
}
|
||||
}
|
||||
|
||||
func (b *LocalActiveOrderBook) MarshalJSON() ([]byte, error) {
|
||||
orders := b.Backup()
|
||||
return json.Marshal(orders)
|
||||
}
|
||||
|
||||
func (b *LocalActiveOrderBook) Backup() []types.SubmitOrder {
|
||||
return append(b.Bids.Backup(), b.Asks.Backup()...)
|
||||
}
|
||||
|
|
|
@ -221,6 +221,7 @@ func (q *RewardBatchQuery) Query(ctx context.Context, startTime, endTime time.Ti
|
|||
return
|
||||
}
|
||||
|
||||
newCnt := 0
|
||||
for _, o := range rewards {
|
||||
if _, ok := rewardKeys[o.UUID]; ok {
|
||||
continue
|
||||
|
@ -231,11 +232,18 @@ func (q *RewardBatchQuery) Query(ctx context.Context, startTime, endTime time.Ti
|
|||
return
|
||||
}
|
||||
|
||||
newCnt++
|
||||
c <- o
|
||||
startTime = o.CreatedAt.Time()
|
||||
lastID = o.UUID
|
||||
rewardKeys[o.UUID] = struct{}{}
|
||||
}
|
||||
|
||||
if newCnt == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
end := len(rewards) - 1
|
||||
startTime = rewards[end].CreatedAt.Time()
|
||||
lastID = rewards[end].UUID
|
||||
}
|
||||
|
||||
}()
|
||||
|
|
|
@ -72,7 +72,7 @@ type Order struct {
|
|||
func (s *OrderService) Closed(market string, options QueryOrderOptions) ([]Order, error) {
|
||||
payload := map[string]interface{}{
|
||||
"market": market,
|
||||
"state": []OrderState{OrderStateFinalizing, OrderStateDone, OrderStateCancel, OrderStateFailed},
|
||||
"state": []OrderState{OrderStateDone, OrderStateCancel, OrderStateFailed},
|
||||
"order_by": "desc",
|
||||
"pagination": false,
|
||||
}
|
||||
|
|
|
@ -3,12 +3,14 @@ package fixedpoint
|
|||
import (
|
||||
"database/sql/driver"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"math"
|
||||
"strconv"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
const MaxPrecision = 12
|
||||
const DefaultPrecision = 8
|
||||
|
||||
const DefaultPow = 1e8
|
||||
|
@ -64,6 +66,10 @@ func (v Value) Div(v2 Value) Value {
|
|||
return NewFromFloat(v.Float64() / v2.Float64())
|
||||
}
|
||||
|
||||
func (v Value) Floor() Value {
|
||||
return NewFromFloat(math.Floor(v.Float64()))
|
||||
}
|
||||
|
||||
func (v Value) Sub(v2 Value) Value {
|
||||
return Value(int64(v) - int64(v2))
|
||||
}
|
||||
|
@ -156,6 +162,59 @@ func Must(v Value, err error) Value {
|
|||
return v
|
||||
}
|
||||
|
||||
var ErrPrecisionLoss = errors.New("precision loss")
|
||||
|
||||
func Parse(input string) (num int64, numDecimalPoints int, err error) {
|
||||
var neg int64 = 1
|
||||
var digit int64
|
||||
for i := 0 ; i < len(input) ; i++ {
|
||||
c := input[i]
|
||||
if c == '-' {
|
||||
neg = -1
|
||||
} else if c >= '0' && c <= '9' {
|
||||
digit, err = strconv.ParseInt(string(c), 10, 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
num = num * 10 + digit
|
||||
} else if c == '.' {
|
||||
i++
|
||||
if i > len(input) - 1 {
|
||||
err = fmt.Errorf("expect fraction numbers after dot")
|
||||
return
|
||||
}
|
||||
|
||||
for j := i ; j < len(input); j++ {
|
||||
fc := input[j]
|
||||
if fc >= '0' && fc <= '9' {
|
||||
digit, err = strconv.ParseInt(string(fc), 10, 64)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
numDecimalPoints++
|
||||
num = num * 10 + digit
|
||||
|
||||
if numDecimalPoints >= MaxPrecision {
|
||||
return num, numDecimalPoints,ErrPrecisionLoss
|
||||
}
|
||||
} else {
|
||||
err = fmt.Errorf("expect digit, got %c", fc)
|
||||
return
|
||||
}
|
||||
}
|
||||
break
|
||||
} else {
|
||||
err = fmt.Errorf("unexpected char %c", c)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
num = num * neg
|
||||
return num, numDecimalPoints, nil
|
||||
}
|
||||
|
||||
func NewFromString(input string) (Value, error) {
|
||||
v, err := strconv.ParseFloat(input, 64)
|
||||
if err != nil {
|
||||
|
|
68
pkg/fixedpoint/convert_test.go
Normal file
68
pkg/fixedpoint/convert_test.go
Normal file
|
@ -0,0 +1,68 @@
|
|||
package fixedpoint
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestParse(t *testing.T) {
|
||||
type args struct {
|
||||
input string
|
||||
}
|
||||
tests := []struct {
|
||||
name string
|
||||
args args
|
||||
wantNum int64
|
||||
wantNumDecimalPoints int
|
||||
wantErr bool
|
||||
}{
|
||||
{
|
||||
args: args{ input: "-99.9" },
|
||||
wantNum: -999,
|
||||
wantNumDecimalPoints: 1,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
args: args{ input: "0.12345678" },
|
||||
wantNum: 12345678,
|
||||
wantNumDecimalPoints: 8,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
args: args{ input: "a" },
|
||||
wantNum: 0,
|
||||
wantNumDecimalPoints: 0,
|
||||
wantErr: true,
|
||||
},
|
||||
{
|
||||
args: args{ input: "0.1" },
|
||||
wantNum: 1,
|
||||
wantNumDecimalPoints: 1,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
args: args{ input: "100" },
|
||||
wantNum: 100,
|
||||
wantNumDecimalPoints: 0,
|
||||
wantErr: false,
|
||||
},
|
||||
{
|
||||
args: args{ input: "100.9999" },
|
||||
wantNum: 1009999,
|
||||
wantNumDecimalPoints: 4,
|
||||
wantErr: false,
|
||||
},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
gotNum, gotNumDecimalPoints, err := Parse(tt.args.input)
|
||||
if (err != nil) != tt.wantErr {
|
||||
t.Errorf("Parse() error = %v, wantErr %v", err, tt.wantErr)
|
||||
return
|
||||
}
|
||||
if gotNum != tt.wantNum {
|
||||
t.Errorf("Parse() gotNum = %v, want %v", gotNum, tt.wantNum)
|
||||
}
|
||||
if gotNumDecimalPoints != tt.wantNumDecimalPoints {
|
||||
t.Errorf("Parse() gotNumDecimalPoints = %v, want %v", gotNumDecimalPoints, tt.wantNumDecimalPoints)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
|
@ -26,8 +26,8 @@ func init() {
|
|||
bbgo.RegisterStrategy(ID, &Strategy{})
|
||||
}
|
||||
|
||||
// Snapshot is the grid snapshot
|
||||
type Snapshot struct {
|
||||
// State is the grid snapshot
|
||||
type State struct {
|
||||
Orders []types.SubmitOrder `json:"orders,omitempty"`
|
||||
FilledBuyGrids map[fixedpoint.Value]struct{} `json:"filledBuyGrids"`
|
||||
FilledSellGrids map[fixedpoint.Value]struct{} `json:"filledSellGrids"`
|
||||
|
@ -84,8 +84,7 @@ type Strategy struct {
|
|||
// Long means you want to hold more base asset than the quote asset.
|
||||
Long bool `json:"long,omitempty" yaml:"long,omitempty"`
|
||||
|
||||
filledBuyGrids map[fixedpoint.Value]struct{}
|
||||
filledSellGrids map[fixedpoint.Value]struct{}
|
||||
state *State
|
||||
|
||||
// orderStore is used to store all the created orders, so that we can filter the trades.
|
||||
orderStore *bbgo.OrderStore
|
||||
|
@ -93,8 +92,6 @@ type Strategy struct {
|
|||
// activeOrders is the locally maintained active order book of the maker orders.
|
||||
activeOrders *bbgo.LocalActiveOrderBook
|
||||
|
||||
position bbgo.Position
|
||||
|
||||
// any created orders for tracking trades
|
||||
orders map[uint64]types.Order
|
||||
|
||||
|
@ -113,14 +110,18 @@ func (s *Strategy) generateGridSellOrders(session *bbgo.ExchangeSession) ([]type
|
|||
}
|
||||
|
||||
currentPrice := fixedpoint.NewFromFloat(currentPriceFloat)
|
||||
priceRange := s.UpperPrice - s.LowerPrice
|
||||
if priceRange <= 0 {
|
||||
return nil, fmt.Errorf("upper price %f should not be less than or equal to lower price %f", s.UpperPrice.Float64(), s.LowerPrice.Float64())
|
||||
if currentPrice > s.UpperPrice {
|
||||
return nil, fmt.Errorf("current price %f is higher than upper price %f", currentPrice.Float64(), s.UpperPrice.Float64())
|
||||
}
|
||||
|
||||
priceRange := s.UpperPrice - s.LowerPrice
|
||||
numGrids := fixedpoint.NewFromInt(s.GridNum)
|
||||
gridSpread := priceRange.Div(numGrids)
|
||||
startPrice := fixedpoint.Max(s.LowerPrice, currentPrice+gridSpread)
|
||||
|
||||
// find the nearest grid price from the current price
|
||||
startPrice := fixedpoint.Max(
|
||||
s.LowerPrice,
|
||||
s.UpperPrice-(s.UpperPrice-currentPrice).Div(gridSpread).Floor().Mul(gridSpread))
|
||||
|
||||
if startPrice > s.UpperPrice {
|
||||
return nil, fmt.Errorf("current price %f exceeded the upper price boundary %f",
|
||||
|
@ -165,7 +166,7 @@ func (s *Strategy) generateGridSellOrders(session *bbgo.ExchangeSession) ([]type
|
|||
baseBalance.Available.Float64())
|
||||
}
|
||||
|
||||
if _, filled := s.filledSellGrids[price]; filled {
|
||||
if _, filled := s.state.FilledSellGrids[price]; filled {
|
||||
log.Debugf("sell grid at price %f is already filled, skipping", price.Float64())
|
||||
continue
|
||||
}
|
||||
|
@ -182,7 +183,7 @@ func (s *Strategy) generateGridSellOrders(session *bbgo.ExchangeSession) ([]type
|
|||
})
|
||||
baseBalance.Available -= quantity
|
||||
|
||||
s.filledSellGrids[price] = struct{}{}
|
||||
s.state.FilledSellGrids[price] = struct{}{}
|
||||
}
|
||||
|
||||
return orders, nil
|
||||
|
@ -196,14 +197,24 @@ func (s *Strategy) generateGridBuyOrders(session *bbgo.ExchangeSession) ([]types
|
|||
}
|
||||
|
||||
currentPrice := fixedpoint.NewFromFloat(currentPriceFloat)
|
||||
priceRange := s.UpperPrice - s.LowerPrice
|
||||
if priceRange <= 0 {
|
||||
return nil, fmt.Errorf("upper price %f should not be less than or equal to lower price %f", s.UpperPrice.Float64(), s.LowerPrice.Float64())
|
||||
if currentPrice < s.LowerPrice {
|
||||
return nil, fmt.Errorf("current price %f is lower than the lower price %f", currentPrice.Float64(), s.LowerPrice.Float64())
|
||||
}
|
||||
|
||||
priceRange := s.UpperPrice - s.LowerPrice
|
||||
numGrids := fixedpoint.NewFromInt(s.GridNum)
|
||||
gridSpread := priceRange.Div(numGrids)
|
||||
startPrice := fixedpoint.Min(s.UpperPrice, currentPrice-gridSpread)
|
||||
|
||||
// Find the nearest grid price for placing buy orders:
|
||||
// buyRange = currentPrice - lowerPrice
|
||||
// numOfBuyGrids = Floor(buyRange / gridSpread)
|
||||
// startPrice = lowerPrice + numOfBuyGrids * gridSpread
|
||||
// priceOfBuyOrder1 = startPrice
|
||||
// priceOfBuyOrder2 = startPrice - gridSpread
|
||||
// priceOfBuyOrder3 = startPrice - gridSpread * 2
|
||||
startPrice := fixedpoint.Min(
|
||||
s.UpperPrice,
|
||||
s.LowerPrice+(currentPrice-s.LowerPrice).Div(gridSpread).Floor().Mul(gridSpread))
|
||||
|
||||
if startPrice < s.LowerPrice {
|
||||
return nil, fmt.Errorf("current price %f exceeded the lower price boundary %f",
|
||||
|
@ -222,7 +233,7 @@ func (s *Strategy) generateGridBuyOrders(session *bbgo.ExchangeSession) ([]types
|
|||
}
|
||||
|
||||
log.Infof("placing grid buy orders from %f to %f, grid spread %f",
|
||||
(currentPrice - gridSpread).Float64(),
|
||||
startPrice.Float64(),
|
||||
s.LowerPrice.Float64(),
|
||||
gridSpread.Float64())
|
||||
|
||||
|
@ -249,7 +260,7 @@ func (s *Strategy) generateGridBuyOrders(session *bbgo.ExchangeSession) ([]types
|
|||
quoteQuantity.Float64())
|
||||
}
|
||||
|
||||
if _, filled := s.filledBuyGrids[price]; filled {
|
||||
if _, filled := s.state.FilledBuyGrids[price]; filled {
|
||||
log.Debugf("buy grid at price %f is already filled, skipping", price.Float64())
|
||||
continue
|
||||
}
|
||||
|
@ -266,7 +277,7 @@ func (s *Strategy) generateGridBuyOrders(session *bbgo.ExchangeSession) ([]types
|
|||
})
|
||||
balance.Available -= quoteQuantity
|
||||
|
||||
s.filledBuyGrids[price] = struct{}{}
|
||||
s.state.FilledBuyGrids[price] = struct{}{}
|
||||
}
|
||||
|
||||
return orders, nil
|
||||
|
@ -309,7 +320,7 @@ func (s *Strategy) placeGridBuyOrders(orderExecutor bbgo.OrderExecutor, session
|
|||
}
|
||||
|
||||
func (s *Strategy) placeGridOrders(orderExecutor bbgo.OrderExecutor, session *bbgo.ExchangeSession) {
|
||||
log.Infof("placing grid orders...")
|
||||
log.Infof("placing grid orders on side %s...", s.Side)
|
||||
|
||||
switch s.Side {
|
||||
|
||||
|
@ -353,7 +364,7 @@ func (s *Strategy) tradeUpdateHandler(trade types.Trade) {
|
|||
return
|
||||
}
|
||||
|
||||
profit, madeProfit := s.position.AddTrade(trade)
|
||||
profit, madeProfit := s.state.Position.AddTrade(trade)
|
||||
if madeProfit {
|
||||
s.Notify("profit: %f", profit.Float64())
|
||||
}
|
||||
|
@ -417,34 +428,46 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
s.Side = types.SideTypeBoth
|
||||
}
|
||||
|
||||
if s.UpperPrice <= s.LowerPrice {
|
||||
return fmt.Errorf("upper price (%f) should not be less than lower price (%f)", s.UpperPrice.Float64(), s.LowerPrice.Float64())
|
||||
if s.UpperPrice == 0 {
|
||||
return errors.New("upperPrice can not be zero, you forgot to set?")
|
||||
}
|
||||
|
||||
var snapshot Snapshot
|
||||
var snapshotLoaded = false
|
||||
if s.LowerPrice == 0 {
|
||||
return errors.New("lowerPrice can not be zero, you forgot to set?")
|
||||
}
|
||||
|
||||
if s.UpperPrice <= s.LowerPrice {
|
||||
return fmt.Errorf("upperPrice (%f) should not be less than or equal to lowerPrice (%f)", s.UpperPrice.Float64(), s.LowerPrice.Float64())
|
||||
}
|
||||
|
||||
var stateLoaded = false
|
||||
if s.Persistence != nil {
|
||||
if err := s.Persistence.Load(&snapshot, ID, s.Symbol, "snapshot"); err != nil {
|
||||
var state State
|
||||
if err := s.Persistence.Load(&state, ID, s.Symbol, "state"); err != nil {
|
||||
if err != service.ErrPersistenceNotExists {
|
||||
return errors.Wrapf(err, "snapshot load error")
|
||||
return errors.Wrapf(err, "state load error")
|
||||
}
|
||||
} else {
|
||||
log.Infof("active order snapshot loaded")
|
||||
snapshotLoaded = true
|
||||
log.Infof("grid state loaded")
|
||||
stateLoaded = true
|
||||
s.state = &state
|
||||
}
|
||||
}
|
||||
|
||||
s.filledBuyGrids = make(map[fixedpoint.Value]struct{})
|
||||
s.filledSellGrids = make(map[fixedpoint.Value]struct{})
|
||||
|
||||
if s.state == nil {
|
||||
position, ok := session.Position(s.Symbol)
|
||||
if !ok {
|
||||
return fmt.Errorf("position not found")
|
||||
}
|
||||
|
||||
s.position = *position
|
||||
s.state = &State{
|
||||
FilledBuyGrids: make(map[fixedpoint.Value]struct{}),
|
||||
FilledSellGrids: make(map[fixedpoint.Value]struct{}),
|
||||
Position: position,
|
||||
}
|
||||
}
|
||||
|
||||
s.Notify("current position %+v", position)
|
||||
s.Notify("current position %+v", s.state.Position)
|
||||
|
||||
instanceID := fmt.Sprintf("grid-%s-%d", s.Symbol, s.GridNum)
|
||||
s.groupID = generateGroupID(instanceID)
|
||||
|
@ -462,14 +485,10 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
defer wg.Done()
|
||||
|
||||
if s.Persistence != nil {
|
||||
log.Infof("backing up active orders...")
|
||||
log.Infof("backing up grid state...")
|
||||
submitOrders := s.activeOrders.Backup()
|
||||
snapshot := Snapshot{
|
||||
Orders: submitOrders,
|
||||
Position: &s.position,
|
||||
}
|
||||
|
||||
if err := s.Persistence.Save(&snapshot, ID, s.Symbol, "snapshot"); err != nil {
|
||||
s.state.Orders = submitOrders
|
||||
if err := s.Persistence.Save(s.state, ID, s.Symbol, "snapshot"); err != nil {
|
||||
log.WithError(err).Error("can not save active order backups")
|
||||
} else {
|
||||
log.Infof("active order snapshot saved")
|
||||
|
@ -492,22 +511,13 @@ func (s *Strategy) Run(ctx context.Context, orderExecutor bbgo.OrderExecutor, se
|
|||
|
||||
session.Stream.OnTradeUpdate(s.tradeUpdateHandler)
|
||||
session.Stream.OnStart(func() {
|
||||
if snapshotLoaded && len(snapshot.Orders) > 0 {
|
||||
createdOrders, err := orderExecutor.SubmitOrders(ctx, snapshot.Orders...)
|
||||
if stateLoaded && len(s.state.Orders) > 0 {
|
||||
createdOrders, err := orderExecutor.SubmitOrders(ctx, s.state.Orders...)
|
||||
if err != nil {
|
||||
log.WithError(err).Error("active orders restore error")
|
||||
}
|
||||
s.activeOrders.Add(createdOrders...)
|
||||
s.orderStore.Add(createdOrders...)
|
||||
if snapshot.FilledSellGrids != nil {
|
||||
s.filledSellGrids = snapshot.FilledSellGrids
|
||||
}
|
||||
if snapshot.FilledBuyGrids != nil {
|
||||
s.filledBuyGrids = snapshot.FilledBuyGrids
|
||||
}
|
||||
if snapshot.Position != nil {
|
||||
s.position = *snapshot.Position
|
||||
}
|
||||
} else {
|
||||
s.placeGridOrders(orderExecutor, session)
|
||||
}
|
||||
|
|
Loading…
Reference in New Issue
Block a user