diff --git a/config/grid2.yaml b/config/grid2.yaml index aed8bc28d..18725fe9d 100644 --- a/config/grid2.yaml +++ b/config/grid2.yaml @@ -55,6 +55,15 @@ exchangeStrategies: - on: binance grid2: symbol: BTCUSDT + + ## autoRange can be used to detect a price range from a specific time frame + ## the pivot low / pivot high of the given range will be used for lowerPrice and upperPrice. + ## when autoRange is set, it will override the upperPrice/lowerPrice settings. + ## + ## the valid format is [1-9][hdw] + ## example: "14d" means it will find the highest/lowest price that is higher/lower than left 14d and right 14d. + # autoRange: 14d + lowerPrice: 28_000.0 upperPrice: 50_000.0 diff --git a/pkg/strategy/grid2/debug.go b/pkg/strategy/grid2/debug.go new file mode 100644 index 000000000..56fbf92cb --- /dev/null +++ b/pkg/strategy/grid2/debug.go @@ -0,0 +1,48 @@ +package grid2 + +import ( + "fmt" + + "github.com/c9s/bbgo/pkg/bbgo" + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +func debugGrid(grid *Grid, book *bbgo.ActiveOrderBook) { + fmt.Println("================== GRID ORDERS ==================") + + pins := grid.Pins + missingPins := scanMissingPinPrices(book, pins) + missing := len(missingPins) + + for i := len(pins) - 1; i >= 0; i-- { + pin := pins[i] + price := fixedpoint.Value(pin) + + fmt.Printf("%s -> ", price.String()) + + existingOrder := book.Lookup(func(o types.Order) bool { + return o.Price.Eq(price) + }) + + if existingOrder != nil { + fmt.Printf("%s", existingOrder.String()) + + switch existingOrder.Status { + case types.OrderStatusFilled: + fmt.Printf(" | 🔧") + case types.OrderStatusCanceled: + fmt.Printf(" | 🔄") + default: + fmt.Printf(" | ✅") + } + } else { + fmt.Printf("ORDER MISSING ⚠️ ") + if missing == 1 { + fmt.Printf(" COULD BE EMPTY SLOT") + } + } + fmt.Printf("\n") + } + fmt.Println("================== END OF GRID ORDERS ===================") +} diff --git a/pkg/strategy/grid2/pricemap.go b/pkg/strategy/grid2/pricemap.go new file mode 100644 index 000000000..04ae59711 --- /dev/null +++ b/pkg/strategy/grid2/pricemap.go @@ -0,0 +1,16 @@ +package grid2 + +import "github.com/c9s/bbgo/pkg/fixedpoint" + +type PriceMap map[string]fixedpoint.Value + +func buildGridPriceMap(grid *Grid) PriceMap { + // Add all open orders to the local order book + gridPriceMap := make(PriceMap) + for _, pin := range grid.Pins { + price := fixedpoint.Value(pin) + gridPriceMap[price.String()] = price + } + + return gridPriceMap +} diff --git a/pkg/strategy/grid2/strategy.go b/pkg/strategy/grid2/strategy.go index 2c397ee45..5403b453c 100644 --- a/pkg/strategy/grid2/strategy.go +++ b/pkg/strategy/grid2/strategy.go @@ -20,12 +20,13 @@ const ID = "grid2" const orderTag = "grid2" -type PriceMap map[string]fixedpoint.Value - var log = logrus.WithField("strategy", ID) var maxNumberOfOrderTradesQueryTries = 10 +const historyRollbackDuration = 3 * 24 * time.Hour +const historyRollbackOrderIdRange = 1000 + 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) @@ -62,6 +63,8 @@ type Strategy struct { // GridNum is the grid number, how many orders you want to post on the orderbook. GridNum int64 `json:"gridNumber"` + AutoRange *types.SimpleDuration `json:"autoRange"` + UpperPrice fixedpoint.Value `json:"upperPrice"` LowerPrice fixedpoint.Value `json:"lowerPrice"` @@ -134,16 +137,18 @@ func (s *Strategy) ID() string { } func (s *Strategy) Validate() error { - if s.UpperPrice.IsZero() { - return errors.New("upperPrice can not be zero, you forgot to set?") - } + if s.AutoRange == nil { + if s.UpperPrice.IsZero() { + return errors.New("upperPrice can not be zero, you forgot to set?") + } - if s.LowerPrice.IsZero() { - return errors.New("lowerPrice can not be zero, you forgot to set?") - } + if s.LowerPrice.IsZero() { + return errors.New("lowerPrice can not be zero, you forgot to set?") + } - if s.UpperPrice.Compare(s.LowerPrice) <= 0 { - return fmt.Errorf("upperPrice (%s) should not be less than or equal to lowerPrice (%s)", s.UpperPrice.String(), s.LowerPrice.String()) + if s.UpperPrice.Compare(s.LowerPrice) <= 0 { + return fmt.Errorf("upperPrice (%s) should not be less than or equal to lowerPrice (%s)", s.UpperPrice.String(), s.LowerPrice.String()) + } } if s.GridNum == 0 { @@ -165,6 +170,11 @@ func (s *Strategy) Validate() error { func (s *Strategy) Subscribe(session *bbgo.ExchangeSession) { session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: types.Interval1m}) + + if s.AutoRange != nil { + interval := s.AutoRange.Interval() + session.Subscribe(types.KLineChannel, s.Symbol, types.SubscribeOptions{Interval: interval}) + } } // InstanceID returns the instance identifier from the current grid configuration parameters @@ -235,27 +245,6 @@ func (s *Strategy) calculateProfit(o types.Order, buyPrice, buyQuantity fixedpoi return profit } -// collectTradeFee collects the fee from the given trade slice -func collectTradeFee(trades []types.Trade) map[string]fixedpoint.Value { - fees := make(map[string]fixedpoint.Value) - for _, t := range trades { - if fee, ok := fees[t.FeeCurrency]; ok { - fees[t.FeeCurrency] = fee.Add(t.Fee) - } else { - fees[t.FeeCurrency] = t.Fee - } - } - return fees -} - -func aggregateTradesQuantity(trades []types.Trade) fixedpoint.Value { - tq := fixedpoint.Zero - for _, t := range trades { - tq = tq.Add(t.Quantity) - } - return tq -} - func (s *Strategy) verifyOrderTrades(o types.Order, trades []types.Trade) bool { tq := aggregateTradesQuantity(trades) @@ -745,7 +734,6 @@ func (s *Strategy) newGrid() *Grid { // openGrid // 1) if quantity or amount is set, we should use quantity/amount directly instead of using investment amount to calculate. // 2) if baseInvestment, quoteInvestment is set, then we should calculate the quantity from the given base investment and quote investment. -// TODO: fix sell order placement for profitSpread func (s *Strategy) openGrid(ctx context.Context, session *bbgo.ExchangeSession) error { // grid object guard if s.grid != nil { @@ -997,11 +985,7 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang grid := s.newGrid() // Add all open orders to the local order book - gridPriceMap := make(PriceMap) - for _, pin := range grid.Pins { - price := fixedpoint.Value(pin) - gridPriceMap[price.String()] = price - } + gridPriceMap := buildGridPriceMap(grid) lastOrderID := uint64(1) now := time.Now() @@ -1011,14 +995,13 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang firstOrderTime = since lastOrderTime = until } + _ = lastOrderTime // for MAX exchange we need the order ID to query the closed order history if oid, ok := findEarliestOrderID(openOrders); ok { lastOrderID = oid } - _ = lastOrderTime - activeOrderBook := s.orderExecutor.ActiveMakerOrders() // Allocate a local order book @@ -1035,26 +1018,103 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang } } - // Note that for MAX Exchange, the order history API only uses fromID parameter to query history order. - // The time range does not matter. - startTime := firstOrderTime - endTime := now + // if all open orders are the grid orders, then we don't have to recover + missingPrices := scanMissingPinPrices(orderBook, grid.Pins) + if numMissing := len(missingPrices); numMissing <= 1 { + s.logger.Infof("GRID RECOVER: no missing grid prices, stop re-playing order history") + return nil + } else { + // Note that for MAX Exchange, the order history API only uses fromID parameter to query history order. + // The time range does not matter. + // TODO: handle context correctly + startTime := firstOrderTime + endTime := now + maxTries := 3 + for maxTries > 0 { + maxTries-- + if err := s.replayOrderHistory(ctx, grid, orderBook, historyService, startTime, endTime, lastOrderID); err != nil { + return err + } + + // Verify if there are still missing prices + missingPrices = scanMissingPinPrices(orderBook, grid.Pins) + if len(missingPrices) <= 1 { + // skip this order history loop and start recovering + break + } + + // history rollback range + startTime = startTime.Add(-historyRollbackDuration) + if newFromOrderID := lastOrderID - historyRollbackOrderIdRange; newFromOrderID > 1 { + lastOrderID = newFromOrderID + } + + s.logger.Infof("GRID RECOVER: there are still more than two missing orders, rolling back query start time to earlier time point %s, fromID %d", startTime.String(), lastOrderID) + } + } + + debugGrid(grid, orderBook) + + tmpOrders := orderBook.Orders() + + // if all orders on the order book are active orders, we don't need to recover. + if isCompleteGridOrderBook(orderBook, s.GridNum) { + s.logger.Infof("GRID RECOVER: all orders are active orders, do not need recover") + return nil + } + + // for reverse order recovering, we need the orders to be sort by update time ascending-ly + types.SortOrdersUpdateTimeAscending(tmpOrders) + + if len(tmpOrders) > 1 && len(tmpOrders) == int(s.GridNum)+1 { + // remove the latest updated order because it's near the empty slot + tmpOrders = tmpOrders[:len(tmpOrders)-1] + } + + // we will only submit reverse orders for filled orders + filledOrders := types.OrdersFilled(tmpOrders) + + s.logger.Infof("GRID RECOVER: found %d filled grid orders", len(filledOrders)) + + s.grid = grid + for _, o := range filledOrders { + s.processFilledOrder(o) + } + + s.logger.Infof("GRID RECOVER COMPLETE") + + debugGrid(grid, s.orderExecutor.ActiveMakerOrders()) + return nil +} + +// replayOrderHistory queries the closed order history from the API and rebuild the orderbook from the order history. +// startTime, endTime is the time range of the order history. +func (s *Strategy) replayOrderHistory(ctx context.Context, grid *Grid, orderBook *bbgo.ActiveOrderBook, historyService types.ExchangeTradeHistoryService, startTime, endTime time.Time, lastOrderID uint64) error { + gridPriceMap := buildGridPriceMap(grid) // a simple guard, in reality, this startTime is not possible to exceed the endTime // because the queries closed orders might still in the range. - for startTime.Before(endTime) { + orderIdChanged := true + for startTime.Before(endTime) && orderIdChanged { closedOrders, err := historyService.QueryClosedOrders(ctx, s.Symbol, startTime, endTime, lastOrderID) if err != nil { return err } - // need to prevent infinite loop for: len(closedOrders) == 1 and it's creationTime = startTime - if len(closedOrders) == 0 || len(closedOrders) == 1 && closedOrders[0].CreationTime.Time().Equal(startTime) { + // need to prevent infinite loop for: + // if there is only one order and the order creation time matches our startTime + if len(closedOrders) == 0 || len(closedOrders) == 1 && closedOrders[0].OrderID == lastOrderID { break } // for each closed order, if it's newer than the open order's update time, we will update it. + orderIdChanged = false for _, closedOrder := range closedOrders { + if closedOrder.OrderID > lastOrderID { + lastOrderID = closedOrder.OrderID + orderIdChanged = true + } + // skip orders that are not limit order if closedOrder.Type != types.OrderTypeLimit { continue @@ -1089,140 +1149,21 @@ func (s *Strategy) recoverGrid(ctx context.Context, historyService types.Exchang } } } - - missingPrices := scanMissingGridOrders(orderBook, grid) - if len(missingPrices) == 0 { - s.logger.Infof("GRID RECOVER: no missing grid prices, stop re-playing order history") - break - } } - debugOrderBook(orderBook, grid.Pins) - - tmpOrders := orderBook.Orders() - - // if all orders on the order book are active orders, we don't need to recover. - if isCompleteGridOrderBook(orderBook, s.GridNum) { - s.logger.Infof("GRID RECOVER: all orders are active orders, do not need recover") - return nil - } - - // for reverse order recovering, we need the orders to be sort by update time ascending-ly - types.SortOrdersUpdateTimeAscending(tmpOrders) - - if len(tmpOrders) > 1 && len(tmpOrders) == int(s.GridNum)+1 { - // remove the latest updated order because it's near the empty slot - tmpOrders = tmpOrders[:len(tmpOrders)-1] - } - - // we will only submit reverse orders for filled orders - filledOrders := ordersFilled(tmpOrders) - - s.logger.Infof("GRID RECOVER: found %d filled grid orders", len(filledOrders)) - - s.grid = grid - for _, o := range filledOrders { - s.processFilledOrder(o) - } - - s.logger.Infof("GRID RECOVER COMPLETE") - - debugOrderBook(s.orderExecutor.ActiveMakerOrders(), grid.Pins) - return nil } -func isActiveOrder(o types.Order) bool { - return o.Status == types.OrderStatusNew || o.Status == types.OrderStatusPartiallyFilled -} - func isCompleteGridOrderBook(orderBook *bbgo.ActiveOrderBook, gridNum int64) bool { tmpOrders := orderBook.Orders() - if len(tmpOrders) == int(gridNum) && ordersAll(tmpOrders, isActiveOrder) { + if len(tmpOrders) == int(gridNum) && types.OrdersAll(tmpOrders, types.IsActiveOrder) { return true } return false } -func ordersFilled(in []types.Order) (out []types.Order) { - for _, o := range in { - switch o.Status { - case types.OrderStatusFilled: - o2 := o - out = append(out, o2) - } - } - return out -} - -func ordersAll(orders []types.Order, f func(o types.Order) bool) bool { - for _, o := range orders { - if !f(o) { - return false - } - } - return true -} - -func ordersAny(orders []types.Order, f func(o types.Order) bool) bool { - for _, o := range orders { - if f(o) { - return true - } - } - return false -} - -func debugOrderBook(b *bbgo.ActiveOrderBook, pins []Pin) { - fmt.Println("================== GRID ORDERS ==================") - - // scan missing orders - missing := 0 - for i := len(pins) - 1; i >= 0; i-- { - pin := pins[i] - price := fixedpoint.Value(pin) - existingOrder := b.Lookup(func(o types.Order) bool { - return o.Price.Eq(price) - }) - if existingOrder == nil { - missing++ - } - } - - for i := len(pins) - 1; i >= 0; i-- { - pin := pins[i] - price := fixedpoint.Value(pin) - - fmt.Printf("%s -> ", price.String()) - - existingOrder := b.Lookup(func(o types.Order) bool { - return o.Price.Eq(price) - }) - - if existingOrder != nil { - fmt.Printf("%s", existingOrder.String()) - - switch existingOrder.Status { - case types.OrderStatusFilled: - fmt.Printf(" | 🔧") - case types.OrderStatusCanceled: - fmt.Printf(" | 🔄") - default: - fmt.Printf(" | ✅") - } - } else { - fmt.Printf("ORDER MISSING ⚠️ ") - if missing == 1 { - fmt.Printf(" COULD BE EMPTY SLOT") - } - } - fmt.Printf("\n") - } - fmt.Println("================== END OF GRID ORDERS ===================") -} - func findEarliestOrderID(orders []types.Order) (uint64, bool) { if len(orders) == 0 { return 0, false @@ -1258,15 +1199,14 @@ func scanOrderCreationTimeRange(orders []types.Order) (time.Time, time.Time, boo return firstOrderTime, lastOrderTime, true } -// scanMissingGridOrders finds the missing grid order prices -func scanMissingGridOrders(orderBook *bbgo.ActiveOrderBook, grid *Grid) PriceMap { +// scanMissingPinPrices finds the missing grid order prices +func scanMissingPinPrices(orderBook *bbgo.ActiveOrderBook, pins []Pin) PriceMap { // Add all open orders to the local order book gridPrices := make(PriceMap) missingPrices := make(PriceMap) - for _, pin := range grid.Pins { + for _, pin := range pins { price := fixedpoint.Value(pin) gridPrices[price.String()] = price - existingOrder := orderBook.Lookup(func(o types.Order) bool { return o.Price.Compare(price) == 0 }) @@ -1292,7 +1232,16 @@ func (s *Strategy) Run(ctx context.Context, _ bbgo.OrderExecutor, session *bbgo. }) s.groupID = util.FNV32(instanceID) - s.logger.Infof("using group id %d from fnv(%s)", s.groupID, instanceID) + + if s.AutoRange != nil { + indicatorSet := session.StandardIndicatorSet(s.Symbol) + interval := s.AutoRange.Interval() + pivotLow := indicatorSet.PivotLow(types.IntervalWindow{Interval: interval, Window: s.AutoRange.Num}) + pivotHigh := indicatorSet.PivotHigh(types.IntervalWindow{Interval: interval, Window: s.AutoRange.Num}) + s.UpperPrice = fixedpoint.NewFromFloat(pivotHigh.Last()) + s.LowerPrice = fixedpoint.NewFromFloat(pivotLow.Last()) + s.logger.Infof("autoRange is enabled, using pivot high %f and pivot low %f", s.UpperPrice.Float64(), s.LowerPrice.Float64()) + } if s.ProfitSpread.Sign() > 0 { s.ProfitSpread = s.Market.TruncatePrice(s.ProfitSpread) diff --git a/pkg/strategy/grid2/trade.go b/pkg/strategy/grid2/trade.go new file mode 100644 index 000000000..381744ccd --- /dev/null +++ b/pkg/strategy/grid2/trade.go @@ -0,0 +1,27 @@ +package grid2 + +import ( + "github.com/c9s/bbgo/pkg/fixedpoint" + "github.com/c9s/bbgo/pkg/types" +) + +// collectTradeFee collects the fee from the given trade slice +func collectTradeFee(trades []types.Trade) map[string]fixedpoint.Value { + fees := make(map[string]fixedpoint.Value) + for _, t := range trades { + if fee, ok := fees[t.FeeCurrency]; ok { + fees[t.FeeCurrency] = fee.Add(t.Fee) + } else { + fees[t.FeeCurrency] = t.Fee + } + } + return fees +} + +func aggregateTradesQuantity(trades []types.Trade) fixedpoint.Value { + tq := fixedpoint.Zero + for _, t := range trades { + tq = tq.Add(t.Quantity) + } + return tq +} diff --git a/pkg/types/duration.go b/pkg/types/duration.go new file mode 100644 index 000000000..881cbe4d6 --- /dev/null +++ b/pkg/types/duration.go @@ -0,0 +1,130 @@ +package types + +import ( + "encoding/json" + "fmt" + "regexp" + "strconv" + "time" + + "github.com/pkg/errors" +) + +var simpleDurationRegExp = regexp.MustCompile("^(\\d+)([hdw])$") + +var ErrNotSimpleDuration = errors.New("the given input is not simple duration format, valid format: [1-9][0-9]*[hdw]") + +type SimpleDuration struct { + Num int + Unit string + Duration Duration +} + +func (d *SimpleDuration) Interval() Interval { + switch d.Unit { + + case "d": + return Interval1d + case "h": + return Interval1h + + case "w": + return Interval1w + + } + + return "" +} + +func (d *SimpleDuration) UnmarshalJSON(data []byte) error { + var s string + if err := json.Unmarshal(data, &s); err != nil { + return err + } + + sd, err := ParseSimpleDuration(s) + if err != nil { + return err + } + + if sd != nil { + *d = *sd + } + return nil +} + +func ParseSimpleDuration(s string) (*SimpleDuration, error) { + if s == "" { + return nil, nil + } + + if !simpleDurationRegExp.MatchString(s) { + return nil, errors.Wrapf(ErrNotSimpleDuration, "input %q is not a simple duration", s) + } + + matches := simpleDurationRegExp.FindStringSubmatch(s) + numStr := matches[1] + unit := matches[2] + num, err := strconv.Atoi(numStr) + if err != nil { + return nil, err + } + + switch unit { + case "d": + d := Duration(time.Duration(num) * 24 * time.Hour) + return &SimpleDuration{num, unit, d}, nil + case "w": + d := Duration(time.Duration(num) * 7 * 24 * time.Hour) + return &SimpleDuration{num, unit, d}, nil + case "h": + d := Duration(time.Duration(num) * time.Hour) + return &SimpleDuration{num, unit, d}, nil + } + + return nil, errors.Wrapf(ErrNotSimpleDuration, "input %q is not a simple duration", s) +} + +type Duration time.Duration + +func (d *Duration) Duration() time.Duration { + return time.Duration(*d) +} + +func (d *Duration) UnmarshalJSON(data []byte) error { + var o interface{} + + if err := json.Unmarshal(data, &o); err != nil { + return err + } + + switch t := o.(type) { + case string: + sd, err := ParseSimpleDuration(t) + if err == nil { + *d = sd.Duration + return nil + } + + dd, err := time.ParseDuration(t) + if err != nil { + return err + } + + *d = Duration(dd) + + case float64: + *d = Duration(int64(t * float64(time.Second))) + + case int64: + *d = Duration(t * int64(time.Second)) + case int: + *d = Duration(t * int(time.Second)) + + default: + return fmt.Errorf("unsupported type %T value: %v", t, t) + + } + + return nil +} diff --git a/pkg/types/duration_test.go b/pkg/types/duration_test.go new file mode 100644 index 000000000..44a56c80d --- /dev/null +++ b/pkg/types/duration_test.go @@ -0,0 +1,55 @@ +package types + +import ( + "fmt" + "testing" + "time" + + "github.com/stretchr/testify/assert" +) + +func TestParseSimpleDuration(t *testing.T) { + type args struct { + s string + } + tests := []struct { + name string + args args + want *SimpleDuration + wantErr assert.ErrorAssertionFunc + }{ + { + name: "3h", + args: args{ + s: "3h", + }, + want: &SimpleDuration{Num: 3, Unit: "h", Duration: Duration(3 * time.Hour)}, + wantErr: assert.NoError, + }, + { + name: "3d", + args: args{ + s: "3d", + }, + want: &SimpleDuration{Num: 3, Unit: "d", Duration: Duration(3 * 24 * time.Hour)}, + wantErr: assert.NoError, + }, + { + name: "3w", + args: args{ + s: "3w", + }, + want: &SimpleDuration{Num: 3, Unit: "w", Duration: Duration(3 * 7 * 24 * time.Hour)}, + wantErr: assert.NoError, + }, + } + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + got, err := ParseSimpleDuration(tt.args.s) + if !tt.wantErr(t, err, fmt.Sprintf("ParseSimpleDuration(%v)", tt.args.s)) { + return + } + assert.Equalf(t, tt.want, got, "ParseSimpleDuration(%v)", tt.args.s) + }) + } +} diff --git a/pkg/types/market.go b/pkg/types/market.go index e2b08cd21..6cc9466ff 100644 --- a/pkg/types/market.go +++ b/pkg/types/market.go @@ -1,54 +1,13 @@ package types import ( - "encoding/json" - "fmt" "math" - "time" "github.com/leekchan/accounting" "github.com/c9s/bbgo/pkg/fixedpoint" ) -type Duration time.Duration - -func (d Duration) Duration() time.Duration { - return time.Duration(d) -} - -func (d *Duration) UnmarshalJSON(data []byte) error { - var o interface{} - - if err := json.Unmarshal(data, &o); err != nil { - return err - } - - switch t := o.(type) { - case string: - dd, err := time.ParseDuration(t) - if err != nil { - return err - } - - *d = Duration(dd) - - case float64: - *d = Duration(int64(t * float64(time.Second))) - - case int64: - *d = Duration(t * int64(time.Second)) - case int: - *d = Duration(t * int(time.Second)) - - default: - return fmt.Errorf("unsupported type %T value: %v", t, t) - - } - - return nil -} - type Market struct { Symbol string `json:"symbol"` diff --git a/pkg/types/order.go b/pkg/types/order.go index 4444bac87..82456caf9 100644 --- a/pkg/types/order.go +++ b/pkg/types/order.go @@ -394,3 +394,36 @@ func (o Order) SlackAttachment() slack.Attachment { Footer: strings.ToLower(o.Exchange.String()) + templateutil.Render(" creation time {{ . }}", o.CreationTime.Time().Format(time.StampMilli)), } } + +func OrdersFilled(in []Order) (out []Order) { + for _, o := range in { + switch o.Status { + case OrderStatusFilled: + o2 := o + out = append(out, o2) + } + } + return out +} + +func OrdersAll(orders []Order, f func(o Order) bool) bool { + for _, o := range orders { + if !f(o) { + return false + } + } + return true +} + +func OrdersAny(orders []Order, f func(o Order) bool) bool { + for _, o := range orders { + if f(o) { + return true + } + } + return false +} + +func IsActiveOrder(o Order) bool { + return o.Status == OrderStatusNew || o.Status == OrderStatusPartiallyFilled +}