decred.org/dcrdex@v1.0.3/client/mm/event_log.go (about)

     1  // This code is available on the terms of the project LICENSE.md file,
     2  // also available online at https://blueoakcouncil.org/license/1.0.0.
     3  
     4  package mm
     5  
     6  import (
     7  	"bytes"
     8  	"context"
     9  	"encoding/binary"
    10  	"encoding/json"
    11  	"fmt"
    12  
    13  	"decred.org/dcrdex/client/asset"
    14  	"decred.org/dcrdex/dex"
    15  	"decred.org/dcrdex/dex/encode"
    16  	"go.etcd.io/bbolt"
    17  )
    18  
    19  // DEXOrderEvent represents a a dex order that a bot placed.
    20  type DEXOrderEvent struct {
    21  	ID           string                     `json:"id"`
    22  	Rate         uint64                     `json:"rate"`
    23  	Qty          uint64                     `json:"qty"`
    24  	Sell         bool                       `json:"sell"`
    25  	Transactions []*asset.WalletTransaction `json:"transactions"`
    26  }
    27  
    28  // CEXOrderEvent represents a a cex order that a bot placed.
    29  type CEXOrderEvent struct {
    30  	ID          string `json:"id"`
    31  	Rate        uint64 `json:"rate"`
    32  	Qty         uint64 `json:"qty"`
    33  	Sell        bool   `json:"sell"`
    34  	BaseFilled  uint64 `json:"baseFilled"`
    35  	QuoteFilled uint64 `json:"quoteFilled"`
    36  }
    37  
    38  // DepositEvent represents a deposit that a bot made.
    39  type DepositEvent struct {
    40  	Transaction *asset.WalletTransaction `json:"transaction"`
    41  	AssetID     uint32                   `json:"assetID"`
    42  	CEXCredit   uint64                   `json:"cexCredit"`
    43  }
    44  
    45  // WithdrawalEvent represents a withdrawal that a bot made.
    46  type WithdrawalEvent struct {
    47  	ID          string                   `json:"id"`
    48  	AssetID     uint32                   `json:"assetID"`
    49  	Transaction *asset.WalletTransaction `json:"transaction"`
    50  	CEXDebit    uint64                   `json:"cexDebit"`
    51  }
    52  
    53  // MarketMakingEvent represents an action that a market making bot takes.
    54  type MarketMakingEvent struct {
    55  	ID             uint64          `json:"id"`
    56  	TimeStamp      int64           `json:"timestamp"`
    57  	Pending        bool            `json:"pending"`
    58  	BalanceEffects *BalanceEffects `json:"balanceEffects,omitempty"`
    59  
    60  	// Only one of the following will be populated.
    61  	DEXOrderEvent   *DEXOrderEvent    `json:"dexOrderEvent,omitempty"`
    62  	CEXOrderEvent   *CEXOrderEvent    `json:"cexOrderEvent,omitempty"`
    63  	DepositEvent    *DepositEvent     `json:"depositEvent,omitempty"`
    64  	WithdrawalEvent *WithdrawalEvent  `json:"withdrawalEvent,omitempty"`
    65  	UpdateConfig    *BotConfig        `json:"updateConfig,omitempty"`
    66  	UpdateInventory *map[uint32]int64 `json:"updateInventory,omitempty"`
    67  }
    68  
    69  // MarketMakingRun identifies a market making run.
    70  type MarketMakingRun struct {
    71  	StartTime int64           `json:"startTime"`
    72  	Market    *MarketWithHost `json:"market"`
    73  }
    74  
    75  // BalanceState contains the fiat rates and bot balances at the latest point of
    76  // a run.
    77  type BalanceState struct {
    78  	FiatRates     map[uint32]float64     `json:"fiatRates"`
    79  	Balances      map[uint32]*BotBalance `json:"balances"`
    80  	InventoryMods map[uint32]int64       `json:"invMods"`
    81  }
    82  
    83  // CfgUpdate contains a BotConfig and the timestamp the bot started to use this
    84  // configuration.
    85  type CfgUpdate struct {
    86  	Timestamp int64      `json:"timestamp"`
    87  	Cfg       *BotConfig `json:"cfg"`
    88  }
    89  
    90  // MarketMakingRunOverview contains information about a market making run.
    91  type MarketMakingRunOverview struct {
    92  	EndTime         *int64            `json:"endTime,omitempty"`
    93  	Cfgs            []*CfgUpdate      `json:"cfgs"`
    94  	InitialBalances map[uint32]uint64 `json:"initialBalances"`
    95  	ProfitLoss      *ProfitLoss       `json:"profitLoss"`
    96  	FinalState      *BalanceState     `json:"finalState"`
    97  }
    98  
    99  // eventLogDB is the interface for the event log database.
   100  type eventLogDB interface {
   101  	// storeNewRun stores a new run in the database. It should be called
   102  	// whenever a new run is started. Events cannot be stored for a run
   103  	// before this is called.
   104  	storeNewRun(startTime int64, mkt *MarketWithHost, cfg *BotConfig, initialState *BalanceState) error
   105  	// storeEvent stores/updates a market making event.
   106  	storeEvent(startTime int64, mkt *MarketWithHost, e *MarketMakingEvent, fs *BalanceState)
   107  	// endRun stores the time that a market making run was ended.
   108  	endRun(startTime int64, mkt *MarketWithHost, endTime int64) error
   109  	// runs returns a list of runs in the database. If n == 0, all of the runs
   110  	// will be returned. If refStartTime and refMkt are not nil, the runs
   111  	// including and before the run with the start time and market will be
   112  	// returned.
   113  	runs(n uint64, refStartTime *uint64, refMkt *MarketWithHost) ([]*MarketMakingRun, error)
   114  	// runOverview returns overview information about a run, not including the
   115  	// events that took place.
   116  	runOverview(startTime int64, mkt *MarketWithHost) (*MarketMakingRunOverview, error)
   117  	// runEvents returns the events that took place during a run. If n == 0,
   118  	// all of the events will be returned. If refID is not nil, the events
   119  	// including and after the event with the ID will be returned. If
   120  	// pendingOnly is true, only pending events will be returned.
   121  	runEvents(startTime int64, mkt *MarketWithHost, n uint64, refID *uint64, pendingOnly bool, filters *RunLogFilters) ([]*MarketMakingEvent, error)
   122  }
   123  
   124  // eventUpdate is used to asynchronously add events to the event log.
   125  type eventUpdate struct {
   126  	runKey []byte
   127  	e      *MarketMakingEvent
   128  	bs     *BalanceState
   129  }
   130  
   131  type boltEventLogDB struct {
   132  	*bbolt.DB
   133  	log          dex.Logger
   134  	eventUpdates chan *eventUpdate
   135  }
   136  
   137  var _ eventLogDB = (*boltEventLogDB)(nil)
   138  
   139  /*
   140   * Schema:
   141   *
   142   * - botRuns
   143   *   - version
   144   *   - runBucket (<startTime><baseID><quoteID><host>)
   145   *     - startTime
   146   *     - endTime
   147   *     - cfg
   148   *     - ib
   149   *     - fs
   150   *     - cfgs
   151   *       - <timestamp> -> <cfg>
   152   *     - events
   153   *       - <eventID> -> <event>
   154   */
   155  
   156  var (
   157  	botRunsBucket = []byte("botRuns")
   158  	versionKey    = []byte("version")
   159  	eventsBucket  = []byte("events")
   160  	cfgsBucket    = []byte("cfgs")
   161  
   162  	startTimeKey   = []byte("startTime")
   163  	endTimeKey     = []byte("endTime")
   164  	initialBalsKey = []byte("ib")
   165  	finalStateKey  = []byte("fs")
   166  	noPendingKey   = []byte("np")
   167  )
   168  
   169  const balanceStateDBVersion uint32 = 1
   170  
   171  func newBoltEventLogDB(ctx context.Context, path string, log dex.Logger) (*boltEventLogDB, error) {
   172  	db, err := bbolt.Open(path, 0600, nil)
   173  	if err != nil {
   174  		return nil, err
   175  	}
   176  
   177  	err = db.Update(func(tx *bbolt.Tx) error {
   178  		_, err := tx.CreateBucketIfNotExists(botRunsBucket)
   179  		return err
   180  	})
   181  	if err != nil {
   182  		return nil, err
   183  	}
   184  
   185  	eventUpdates := make(chan *eventUpdate, 128)
   186  	eventLogDB := &boltEventLogDB{
   187  		DB:           db,
   188  		log:          log,
   189  		eventUpdates: eventUpdates,
   190  	}
   191  
   192  	err = eventLogDB.upgradeDB()
   193  	if err != nil {
   194  		return nil, err
   195  	}
   196  
   197  	go func() {
   198  		eventLogDB.listenForStoreEvents(ctx)
   199  		db.Close()
   200  	}()
   201  
   202  	return eventLogDB, nil
   203  }
   204  
   205  // calcFinalStateBasedOnEventDiff calculated an updated final balance state
   206  // based on the difference between an existing event and its updated version.
   207  func calcFinalStateBasedOnEventDiff(runBucket, eventsBucket *bbolt.Bucket, eventKey []byte, newEvent *MarketMakingEvent) (*BalanceState, error) {
   208  	finalStateB := runBucket.Get(finalStateKey)
   209  	if finalStateB == nil {
   210  		return nil, fmt.Errorf("no final state found")
   211  	}
   212  
   213  	finalState, err := decodeFinalState(finalStateB)
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  
   218  	originalEventB := eventsBucket.Get(eventKey)
   219  	if originalEventB == nil {
   220  		return nil, fmt.Errorf("no original event found")
   221  	}
   222  
   223  	originalEvent, err := decodeMarketMakingEvent(originalEventB)
   224  	if err != nil {
   225  		return nil, err
   226  	}
   227  
   228  	applyDiff := func(curr uint64, diff int64) uint64 {
   229  		if diff > 0 {
   230  			return curr + uint64(diff)
   231  		}
   232  
   233  		if curr < uint64(-diff) {
   234  			return 0
   235  		}
   236  
   237  		return curr + uint64(diff)
   238  	}
   239  
   240  	balanceEffectDiff := newEvent.BalanceEffects.sub(originalEvent.BalanceEffects)
   241  	for assetID, diff := range balanceEffectDiff.settled {
   242  		finalState.Balances[assetID].Available = applyDiff(finalState.Balances[assetID].Available, diff)
   243  	}
   244  	for assetID, diff := range balanceEffectDiff.pending {
   245  		finalState.Balances[assetID].Pending = applyDiff(finalState.Balances[assetID].Pending, diff)
   246  	}
   247  	for assetID, diff := range balanceEffectDiff.locked {
   248  		finalState.Balances[assetID].Locked = applyDiff(finalState.Balances[assetID].Locked, diff)
   249  	}
   250  	for assetID, diff := range balanceEffectDiff.reserved {
   251  		finalState.Balances[assetID].Reserved = applyDiff(finalState.Balances[assetID].Reserved, diff)
   252  	}
   253  
   254  	return finalState, nil
   255  }
   256  
   257  func (db *boltEventLogDB) upgradeDB() error {
   258  	return db.Update(func(tx *bbolt.Tx) error {
   259  		botRuns := tx.Bucket(botRunsBucket)
   260  		versionB := botRuns.Get(versionKey)
   261  
   262  		var version uint32
   263  		if versionB != nil {
   264  			version = encode.BytesToUint32(versionB)
   265  		}
   266  
   267  		if version < balanceStateDBVersion {
   268  			err := botRuns.ForEachBucket(func(k []byte) error {
   269  				err := botRuns.DeleteBucket(k)
   270  				if err != nil {
   271  					return err
   272  				}
   273  				return nil
   274  			})
   275  			if err != nil {
   276  				return err
   277  			}
   278  		}
   279  
   280  		return botRuns.Put(versionKey, encode.Uint32Bytes(balanceStateDBVersion))
   281  	})
   282  }
   283  
   284  // updateEvent is called for each event that is popped off the updateEvent. If
   285  // the event already exists, it is updated. If it does not exist, it is added.
   286  // The stats for the run are also updated based on the event.
   287  func (db *boltEventLogDB) updateEvent(update *eventUpdate) {
   288  	if err := db.Update(func(tx *bbolt.Tx) error {
   289  		botRuns := tx.Bucket(botRunsBucket)
   290  		runBucket := botRuns.Bucket(update.runKey)
   291  		if runBucket == nil {
   292  			return fmt.Errorf("nil run bucket for key %x", update.runKey)
   293  		}
   294  
   295  		eventsBkt, err := runBucket.CreateBucketIfNotExists(eventsBucket)
   296  		if err != nil {
   297  			return err
   298  		}
   299  		eventKey := encode.Uint64Bytes(update.e.ID)
   300  		eventJSON, err := json.Marshal(update.e)
   301  		if err != nil {
   302  			return err
   303  		}
   304  		eventB := versionedBytes(0).AddData(eventJSON)
   305  
   306  		bs := update.bs
   307  		if bs == nil {
   308  			bs, err = calcFinalStateBasedOnEventDiff(runBucket, eventsBkt, eventKey, update.e)
   309  			if err != nil {
   310  				return err
   311  			}
   312  		}
   313  
   314  		if err := eventsBkt.Put(eventKey, eventB); err != nil {
   315  			return err
   316  		}
   317  
   318  		if update.e.UpdateConfig != nil {
   319  			if err := db.storeCfgUpdate(runBucket, update.e.UpdateConfig, update.e.TimeStamp); err != nil {
   320  				return err
   321  			}
   322  		}
   323  
   324  		// Update the final state.
   325  		bsJSON, err := json.Marshal(bs)
   326  		if err != nil {
   327  			return err
   328  		}
   329  		return runBucket.Put(finalStateKey, versionedBytes(0).AddData(bsJSON))
   330  	}); err != nil {
   331  		db.log.Errorf("error storing event: %v", err)
   332  	}
   333  }
   334  
   335  // listedForStoreEvents listens on the updateEvent channel and updates the
   336  // db one at a time.
   337  func (db *boltEventLogDB) listenForStoreEvents(ctx context.Context) {
   338  	for {
   339  		select {
   340  		case e := <-db.eventUpdates:
   341  			db.updateEvent(e)
   342  		case <-ctx.Done():
   343  			for len(db.eventUpdates) > 0 {
   344  				db.updateEvent(<-db.eventUpdates)
   345  			}
   346  			return
   347  		}
   348  	}
   349  }
   350  
   351  func versionedBytes(v byte) encode.BuildyBytes {
   352  	return encode.BuildyBytes{v}
   353  }
   354  
   355  // runKey is the key for a run bucket.
   356  func runKey(startTime int64, mkt *MarketWithHost) []byte {
   357  	return versionedBytes(0).
   358  		AddData(encode.Uint64Bytes(uint64(startTime))).
   359  		AddData(encode.Uint32Bytes(mkt.BaseID)).
   360  		AddData(encode.Uint32Bytes(mkt.QuoteID)).
   361  		AddData([]byte(mkt.Host))
   362  }
   363  
   364  func parseRunKey(key []byte) (startTime int64, mkt *MarketWithHost, err error) {
   365  	ver, pushes, err := encode.DecodeBlob(key)
   366  	if err != nil {
   367  		return 0, nil, err
   368  	}
   369  	if ver != 0 {
   370  		return 0, nil, fmt.Errorf("unknown version %d", ver)
   371  	}
   372  	if len(pushes) != 4 {
   373  		return 0, nil, fmt.Errorf("expected 4 pushes, got %d", len(pushes))
   374  	}
   375  
   376  	startTime = int64(binary.BigEndian.Uint64(pushes[0]))
   377  	mkt = &MarketWithHost{
   378  		BaseID:  binary.BigEndian.Uint32(pushes[1]),
   379  		QuoteID: binary.BigEndian.Uint32(pushes[2]),
   380  		Host:    string(pushes[3]),
   381  	}
   382  	return
   383  }
   384  
   385  func (db *boltEventLogDB) Close() error {
   386  	close(db.eventUpdates)
   387  	return db.DB.Close()
   388  }
   389  
   390  func (db *boltEventLogDB) storeCfgUpdate(runBucket *bbolt.Bucket, newCfg *BotConfig, timestamp int64) error {
   391  	cfgsBucket, err := runBucket.CreateBucketIfNotExists(cfgsBucket)
   392  	if err != nil {
   393  		return err
   394  	}
   395  	cfgB, err := json.Marshal(newCfg)
   396  	if err != nil {
   397  		return err
   398  	}
   399  	return cfgsBucket.Put(encode.Uint64Bytes(uint64(timestamp)), versionedBytes(0).AddData(cfgB))
   400  }
   401  
   402  func (db *boltEventLogDB) cfgUpdates(runBucket *bbolt.Bucket) ([]*CfgUpdate, error) {
   403  	cfgsBucket := runBucket.Bucket(cfgsBucket)
   404  	if cfgsBucket == nil {
   405  		return nil, nil
   406  	}
   407  
   408  	cfgs := make([]*CfgUpdate, 0, cfgsBucket.Stats().KeyN)
   409  	cursor := cfgsBucket.Cursor()
   410  	for k, v := cursor.First(); k != nil; k, v = cursor.Next() {
   411  		timestamp := int64(binary.BigEndian.Uint64(k))
   412  		cfg := new(BotConfig)
   413  		ver, pushes, err := encode.DecodeBlob(v)
   414  		if err != nil {
   415  			return nil, err
   416  		}
   417  		if ver != 0 {
   418  			return nil, fmt.Errorf("unknown version %d", ver)
   419  		}
   420  		if len(pushes) != 1 {
   421  			return nil, fmt.Errorf("expected 1 push for cfg, got %d", len(pushes))
   422  		}
   423  		err = json.Unmarshal(pushes[0], cfg)
   424  		if err != nil {
   425  			return nil, err
   426  		}
   427  		cfgs = append(cfgs, &CfgUpdate{
   428  			Timestamp: timestamp,
   429  			Cfg:       cfg,
   430  		})
   431  	}
   432  
   433  	return cfgs, nil
   434  }
   435  
   436  // storeNewRun stores a new run in the database. It should be called whenever
   437  // a new run is started. Events cannot be stored for a run before this is
   438  // called.
   439  func (db *boltEventLogDB) storeNewRun(startTime int64, mkt *MarketWithHost, cfg *BotConfig, initialState *BalanceState) error {
   440  	return db.Update(func(tx *bbolt.Tx) error {
   441  		botRuns, err := tx.CreateBucketIfNotExists(botRunsBucket)
   442  		if err != nil {
   443  			return err
   444  		}
   445  
   446  		runBucket, err := botRuns.CreateBucketIfNotExists(runKey(startTime, mkt))
   447  		if err != nil {
   448  			return err
   449  		}
   450  
   451  		runBucket.Put(startTimeKey, encode.Uint64Bytes(uint64(startTime)))
   452  
   453  		if err := db.storeCfgUpdate(runBucket, cfg, startTime); err != nil {
   454  			return err
   455  		}
   456  
   457  		initialBals := make(map[uint32]uint64)
   458  		for assetID, bal := range initialState.Balances {
   459  			initialBals[assetID] = bal.Available
   460  		}
   461  		initialBalsB, err := json.Marshal(initialBals)
   462  		if err != nil {
   463  			return err
   464  		}
   465  		runBucket.Put(initialBalsKey, versionedBytes(0).AddData(initialBalsB))
   466  
   467  		fsB, err := json.Marshal(initialState)
   468  		if err != nil {
   469  			return err
   470  		}
   471  		runBucket.Put(finalStateKey, versionedBytes(0).AddData(fsB))
   472  
   473  		return nil
   474  	})
   475  }
   476  
   477  // runs returns a list of runs in the database. If n == 0, all of the runs will
   478  // be returned. If refStartTime and refMkt are not nil, the runs including and
   479  // before the run with the start time and market will be returned.
   480  func (db *boltEventLogDB) runs(n uint64, refStartTime *uint64, refMkt *MarketWithHost) ([]*MarketMakingRun, error) {
   481  	if refStartTime == nil != (refMkt == nil) {
   482  		return nil, fmt.Errorf("both or neither refStartTime and refMkt must be nil")
   483  	}
   484  
   485  	var runs []*MarketMakingRun
   486  	err := db.View(func(tx *bbolt.Tx) error {
   487  		botRuns := tx.Bucket(botRunsBucket)
   488  
   489  		runs = make([]*MarketMakingRun, 0, botRuns.Stats().BucketN)
   490  		cursor := botRuns.Cursor()
   491  
   492  		var k []byte
   493  		if refStartTime == nil {
   494  			k, _ = cursor.Last()
   495  		} else {
   496  			k, _ = cursor.Seek(runKey(int64(*refStartTime), refMkt))
   497  		}
   498  
   499  		for ; k != nil; k, _ = cursor.Prev() {
   500  			if bytes.Equal(k, versionKey) {
   501  				continue
   502  			}
   503  			startTime, mkt, err := parseRunKey(k)
   504  			if err != nil {
   505  				return err
   506  			}
   507  
   508  			runBucket := botRuns.Bucket(k)
   509  			if runBucket == nil {
   510  				return fmt.Errorf("nil run bucket for key %x", k)
   511  			}
   512  
   513  			runs = append(runs, &MarketMakingRun{
   514  				StartTime: startTime,
   515  				Market:    mkt,
   516  			})
   517  
   518  			if n > 0 && uint64(len(runs)) >= n {
   519  				break
   520  			}
   521  		}
   522  
   523  		return nil
   524  	})
   525  	if err != nil {
   526  		return nil, err
   527  	}
   528  
   529  	return runs, nil
   530  }
   531  
   532  func decodeFinalState(finalStateB []byte) (*BalanceState, error) {
   533  	finalState := new(BalanceState)
   534  	ver, pushes, err := encode.DecodeBlob(finalStateB)
   535  	if err != nil {
   536  		return nil, err
   537  	}
   538  	if ver != 0 {
   539  		return nil, fmt.Errorf("unknown final state version %d", ver)
   540  	}
   541  	if len(pushes) != 1 {
   542  		return nil, fmt.Errorf("expected 1 push for final state, got %d", len(pushes))
   543  	}
   544  	err = json.Unmarshal(pushes[0], finalState)
   545  	if err != nil {
   546  		return nil, err
   547  	}
   548  	return finalState, nil
   549  }
   550  
   551  // runOverview returns overview information about a run, not including the
   552  // events that took place.
   553  func (db *boltEventLogDB) runOverview(startTime int64, mkt *MarketWithHost) (*MarketMakingRunOverview, error) {
   554  	var overview *MarketMakingRunOverview
   555  	return overview, db.View(func(tx *bbolt.Tx) error {
   556  		botRuns := tx.Bucket(botRunsBucket)
   557  		key := runKey(startTime, mkt)
   558  		runBucket := botRuns.Bucket(key)
   559  		if runBucket == nil {
   560  			return fmt.Errorf("nil run bucket for key %x", key)
   561  		}
   562  
   563  		initialBalsB := runBucket.Get(initialBalsKey)
   564  		initialBals := make(map[uint32]uint64)
   565  		ver, pushes, err := encode.DecodeBlob(initialBalsB)
   566  		if err != nil {
   567  			return err
   568  		}
   569  		if ver != 0 {
   570  			return fmt.Errorf("unknown initial bals version %d", ver)
   571  		}
   572  		if len(pushes) != 1 {
   573  			return fmt.Errorf("expected 1 push for initial bals, got %d", len(pushes))
   574  		}
   575  		err = json.Unmarshal(pushes[0], &initialBals)
   576  		if err != nil {
   577  			return err
   578  		}
   579  
   580  		finalStateB := runBucket.Get(finalStateKey)
   581  		if finalStateB == nil {
   582  			return fmt.Errorf("no final state found")
   583  		}
   584  
   585  		finalState, err := decodeFinalState(finalStateB)
   586  		if err != nil {
   587  			return err
   588  		}
   589  
   590  		cfgs, err := db.cfgUpdates(runBucket)
   591  		if err != nil {
   592  			return err
   593  		}
   594  
   595  		endTimeB := runBucket.Get(endTimeKey)
   596  		var endTime *int64
   597  		if len(endTimeB) == 8 {
   598  			endTime = new(int64)
   599  			*endTime = int64(binary.BigEndian.Uint64(endTimeB))
   600  		}
   601  
   602  		finalBals := make(map[uint32]uint64)
   603  		for assetID, bal := range finalState.Balances {
   604  			finalBals[assetID] = bal.Available + bal.Pending + bal.Locked + bal.Reserved
   605  		}
   606  
   607  		overview = &MarketMakingRunOverview{
   608  			EndTime:         endTime,
   609  			Cfgs:            cfgs,
   610  			InitialBalances: initialBals,
   611  			ProfitLoss:      newProfitLoss(initialBals, finalBals, finalState.InventoryMods, finalState.FiatRates),
   612  			FinalState:      finalState,
   613  		}
   614  
   615  		return nil
   616  	})
   617  }
   618  
   619  // endRun stores the time that a market making run was ended.
   620  func (db *boltEventLogDB) endRun(startTime int64, mkt *MarketWithHost, endTime int64) error {
   621  	return db.Update(func(tx *bbolt.Tx) error {
   622  		botRuns := tx.Bucket(botRunsBucket)
   623  		key := runKey(startTime, mkt)
   624  		runBucket := botRuns.Bucket(key)
   625  		if runBucket == nil {
   626  			return fmt.Errorf("nil run bucket for key %x", key)
   627  		}
   628  
   629  		return runBucket.Put(endTimeKey, encode.Uint64Bytes(uint64(endTime)))
   630  	})
   631  }
   632  
   633  // storeEvent stores/updates a market making event. BalanceState can be nil if
   634  // the run has ended and the final state should be updated based on the
   635  // difference between the event and the previous version of the event.
   636  func (db *boltEventLogDB) storeEvent(startTime int64, mkt *MarketWithHost, e *MarketMakingEvent, bs *BalanceState) {
   637  	db.eventUpdates <- &eventUpdate{
   638  		runKey: runKey(startTime, mkt),
   639  		e:      e,
   640  		bs:     bs,
   641  	}
   642  }
   643  
   644  func decodeMarketMakingEvent(eventB []byte) (*MarketMakingEvent, error) {
   645  	e := new(MarketMakingEvent)
   646  	ver, pushes, err := encode.DecodeBlob(eventB)
   647  	if err != nil {
   648  		return nil, err
   649  	}
   650  	if ver != 0 {
   651  		return nil, fmt.Errorf("unknown version %d", ver)
   652  	}
   653  	if len(pushes) != 1 {
   654  		return nil, fmt.Errorf("expected 1 push for event, got %d", len(pushes))
   655  	}
   656  	err = json.Unmarshal(pushes[0], e)
   657  	if err != nil {
   658  		return nil, err
   659  	}
   660  	return e, nil
   661  }
   662  
   663  // runEvents returns the events that took place during a run. If n == 0, all of the
   664  // events will be returned. If refID is not nil, the events including and after the
   665  // event with the ID will be returned. If pendingOnly is true, only pending events
   666  // will be returned.
   667  func (db *boltEventLogDB) runEvents(startTime int64, mkt *MarketWithHost, n uint64, refID *uint64, pendingOnly bool, filter *RunLogFilters) ([]*MarketMakingEvent, error) {
   668  	events := make([]*MarketMakingEvent, 0, 32)
   669  
   670  	return events, db.View(func(tx *bbolt.Tx) error {
   671  		botRuns := tx.Bucket(botRunsBucket)
   672  		key := runKey(startTime, mkt)
   673  		runBucket := botRuns.Bucket(key)
   674  		if runBucket == nil {
   675  			return fmt.Errorf("nil run bucket for key %x", key)
   676  		}
   677  
   678  		// If a run has ended, and we have queried for all pending events and
   679  		// nothing was found, we store this fact to avoid having to search for
   680  		// pending events again.
   681  		if pendingOnly {
   682  			noPending := runBucket.Get(noPendingKey)
   683  			if noPending != nil {
   684  				return nil
   685  			}
   686  		}
   687  
   688  		eventsBkt := runBucket.Bucket(eventsBucket)
   689  		if eventsBkt == nil {
   690  			return nil
   691  		}
   692  
   693  		cursor := eventsBkt.Cursor()
   694  		var k, v []byte
   695  		if refID == nil {
   696  			k, v = cursor.Last()
   697  		} else {
   698  			k, v = cursor.Seek(encode.Uint64Bytes(*refID))
   699  		}
   700  
   701  		for ; k != nil; k, v = cursor.Prev() {
   702  			e, err := decodeMarketMakingEvent(v)
   703  			if err != nil {
   704  				return err
   705  			}
   706  
   707  			if pendingOnly && !e.Pending {
   708  				continue
   709  			}
   710  
   711  			if filter != nil && !filter.filter(e) {
   712  				continue
   713  			}
   714  
   715  			events = append(events, e)
   716  			if n > 0 && uint64(len(events)) >= n {
   717  				break
   718  			}
   719  		}
   720  
   721  		// If there are no pending events, and the run as ended, store this
   722  		// information to avoid unnecessary iteration in the future.
   723  		if pendingOnly && len(events) == 0 && refID == nil && n == 0 {
   724  			endTime := runBucket.Get(endTimeKey)
   725  			if endTime != nil {
   726  				runBucket.Put(noPendingKey, []byte{1})
   727  			}
   728  		}
   729  
   730  		return nil
   731  	})
   732  }