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