decred.org/dcrdex@v1.0.3/client/mm/event_log_test.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  	"context"
     8  	"encoding/json"
     9  	"fmt"
    10  	"path/filepath"
    11  	"reflect"
    12  	"testing"
    13  	"time"
    14  
    15  	"decred.org/dcrdex/client/asset"
    16  	"github.com/davecgh/go-spew/spew"
    17  )
    18  
    19  func tryWithTimeout(t *testing.T, f func() error) {
    20  	t.Helper()
    21  	var err error
    22  	for i := 0; i < 20; i++ {
    23  		time.Sleep(100 * time.Millisecond)
    24  		err = f()
    25  		if err == nil {
    26  			return
    27  		}
    28  	}
    29  	t.Fatal(err)
    30  }
    31  
    32  func TestEventLogDB(t *testing.T) {
    33  	dir := t.TempDir()
    34  
    35  	ctx, cancel := context.WithCancel(context.Background())
    36  	defer cancel()
    37  
    38  	db, err := newBoltEventLogDB(ctx, filepath.Join(dir, "event_log.db"), tLogger)
    39  	if err != nil {
    40  		t.Fatalf("error creating event log db: %v", err)
    41  	}
    42  
    43  	startTime := time.Now().Unix()
    44  	mkt := &MarketWithHost{
    45  		Host:    "dex.com",
    46  		BaseID:  42,
    47  		QuoteID: 60,
    48  	}
    49  
    50  	fiatRates := map[uint32]float64{
    51  		42: 20,
    52  		60: 2500,
    53  	}
    54  
    55  	cfg := &BotConfig{
    56  		Host:    "dex.com",
    57  		BaseID:  42,
    58  		QuoteID: 60,
    59  		CEXName: "Binance",
    60  		ArbMarketMakerConfig: &ArbMarketMakerConfig{
    61  			BuyPlacements: []*ArbMarketMakingPlacement{
    62  				{
    63  					Lots:       1,
    64  					Multiplier: 2,
    65  				},
    66  			},
    67  			SellPlacements: []*ArbMarketMakingPlacement{
    68  				{
    69  					Lots:       1,
    70  					Multiplier: 2,
    71  				},
    72  			},
    73  		},
    74  	}
    75  
    76  	initialBals := map[uint32]uint64{
    77  		42: 3e9,
    78  		60: 3e9,
    79  	}
    80  
    81  	currBals := map[uint32]*BotBalance{
    82  		42: {
    83  			Available: initialBals[42],
    84  		},
    85  		60: {
    86  			Available: initialBals[60],
    87  		},
    88  	}
    89  
    90  	inventoryMods := map[uint32]int64{}
    91  
    92  	currBalanceState := func() *BalanceState {
    93  		balances := make(map[uint32]*BotBalance, len(currBals))
    94  		for k, v := range currBals {
    95  			balances[k] = &BotBalance{
    96  				Available: v.Available,
    97  				Pending:   v.Pending,
    98  				Locked:    v.Locked,
    99  			}
   100  		}
   101  		rates := make(map[uint32]float64, len(fiatRates))
   102  		for k, v := range fiatRates {
   103  			rates[k] = v
   104  		}
   105  		return &BalanceState{
   106  			Balances:      balances,
   107  			FiatRates:     rates,
   108  			InventoryMods: inventoryMods,
   109  		}
   110  	}
   111  
   112  	err = db.storeNewRun(startTime, mkt, cfg, currBalanceState())
   113  	if err != nil {
   114  		t.Fatalf("error storing new run: %v", err)
   115  	}
   116  
   117  	runs, err := db.runs(0, nil, nil)
   118  	if err != nil {
   119  		t.Fatalf("error getting all runs: %v", err)
   120  	}
   121  	if len(runs) != 1 {
   122  		t.Fatalf("expected 1 run, got %d", len(runs))
   123  	}
   124  
   125  	expectedRun := &MarketMakingRun{
   126  		StartTime: startTime,
   127  		Market:    mkt,
   128  	}
   129  	if !reflect.DeepEqual(runs[0], expectedRun) {
   130  		t.Fatalf("expected run:\n%v\n\ngot:\n%v", expectedRun, runs[0])
   131  	}
   132  
   133  	event1 := &MarketMakingEvent{
   134  		ID:        1,
   135  		TimeStamp: startTime + 1,
   136  		BalanceEffects: &BalanceEffects{
   137  			Settled: map[uint32]int64{
   138  				42: 1e6,
   139  			},
   140  			Locked: map[uint32]uint64{
   141  				60: 2e6,
   142  			},
   143  		},
   144  		Pending: true,
   145  		DEXOrderEvent: &DEXOrderEvent{
   146  			ID:   "order1",
   147  			Rate: 5e7,
   148  			Qty:  5e6,
   149  			Sell: false,
   150  			Transactions: []*asset.WalletTransaction{
   151  				{
   152  					Type:   asset.Swap,
   153  					ID:     "tx1",
   154  					Amount: 2e6,
   155  					Fees:   100,
   156  				},
   157  				{
   158  					Type:   asset.Redeem,
   159  					ID:     "tx2",
   160  					Amount: 1e6,
   161  					Fees:   200,
   162  				},
   163  			},
   164  		},
   165  	}
   166  
   167  	currBals[42].Available += 2e6
   168  	currBals[42].Pending += 1e6
   169  	currBals[60].Available += 8e6
   170  	db.storeEvent(startTime, mkt, event1, currBalanceState())
   171  
   172  	event2 := &MarketMakingEvent{
   173  		ID:        2,
   174  		TimeStamp: startTime + 1,
   175  		BalanceEffects: &BalanceEffects{
   176  			Settled: map[uint32]int64{
   177  				42: 3e6,
   178  			},
   179  			Locked: map[uint32]uint64{
   180  				60: 4e6,
   181  			},
   182  		},
   183  		Pending: true,
   184  		CEXOrderEvent: &CEXOrderEvent{
   185  			ID:          "order1",
   186  			Rate:        5e7,
   187  			Qty:         5e6,
   188  			Sell:        false,
   189  			BaseFilled:  1e6,
   190  			QuoteFilled: 2e6,
   191  		},
   192  	}
   193  	currBals[42].Available += 3e6
   194  	currBals[60].Available += 4e6
   195  	currBals[42].Pending += 1e6
   196  	db.storeEvent(startTime, mkt, event2, currBalanceState())
   197  
   198  	// Get all run events
   199  	check := func() error {
   200  		runEvents, err := db.runEvents(startTime, mkt, 0, nil, false, nil)
   201  		if err != nil {
   202  			return fmt.Errorf("error getting run events: %v", err)
   203  		}
   204  		if len(runEvents) != 2 {
   205  			return fmt.Errorf("expected 2 run event, got %d", len(runEvents))
   206  		}
   207  		if !reflect.DeepEqual(runEvents[0], event2) {
   208  			return fmt.Errorf("expected event:\n%v\n\ngot:\n%v", event2, runEvents[0])
   209  		}
   210  		if !reflect.DeepEqual(runEvents[1], event1) {
   211  			return fmt.Errorf("expected event:\n%v\n\ngot:\n%v", event1, runEvents[1])
   212  		}
   213  		return nil
   214  	}
   215  	tryWithTimeout(t, check)
   216  
   217  	// Get only 1 run event
   218  	runEvents, err := db.runEvents(startTime, mkt, 1, nil, false, nil)
   219  	if err != nil {
   220  		t.Fatalf("error getting run events: %v", err)
   221  	}
   222  	if len(runEvents) != 1 {
   223  		t.Fatalf("expected 1 run event, got %d", len(runEvents))
   224  	}
   225  	if !reflect.DeepEqual(runEvents[0], event2) {
   226  		t.Fatalf("expected event:\n%v\n\ngot:\n%v", event2, runEvents[0])
   227  	}
   228  
   229  	// Get run events with ref ID
   230  	runEvents, err = db.runEvents(startTime, mkt, 1, &event1.ID, false, nil)
   231  	if err != nil {
   232  		t.Fatalf("error getting run events: %v", err)
   233  	}
   234  	if len(runEvents) != 1 {
   235  		t.Fatalf("expected 1 run event, got %d", len(runEvents))
   236  	}
   237  	if !reflect.DeepEqual(runEvents[0], event1) {
   238  		t.Fatalf("expected event:\n%v\n\ngot:\n%v", event1, runEvents[0])
   239  	}
   240  
   241  	runs, err = db.runs(0, nil, nil)
   242  	if err != nil {
   243  		t.Fatalf("error getting all runs: %v", err)
   244  	}
   245  	if len(runs) != 1 {
   246  		t.Fatalf("expected 1 run, got %d", len(runs))
   247  	}
   248  	if !reflect.DeepEqual(runs[0], expectedRun) {
   249  		t.Fatalf("expected run:\n%v\n\ngot:\n%v", expectedRun, runs[0])
   250  	}
   251  
   252  	// Update event1 and fiat rates
   253  	event1.BalanceEffects.Settled[42] += 100
   254  	event1.BalanceEffects.Locked[60] -= 100
   255  	event1.Pending = false
   256  	currBals[42].Available += 100 - 20
   257  	currBals[60].Available -= 200 + 10
   258  	fiatRates[42] = 25
   259  	fiatRates[60] = 3000
   260  	db.storeEvent(startTime, mkt, event1, currBalanceState())
   261  
   262  	// Get all run events
   263  	check = func() error {
   264  		runEvents, err := db.runEvents(startTime, mkt, 0, nil, false, nil)
   265  		if err != nil {
   266  			return fmt.Errorf("error getting run events: %v", err)
   267  		}
   268  		if len(runEvents) != 2 {
   269  			return fmt.Errorf("expected 2 run event, got %d", len(runEvents))
   270  		}
   271  		if !reflect.DeepEqual(runEvents[0], event2) {
   272  			return fmt.Errorf("expected event:\n%v\n\ngot:\n%v", event2, runEvents[0])
   273  		}
   274  		if !reflect.DeepEqual(runEvents[1], event1) {
   275  			return fmt.Errorf("expected event:\n%v\n\ngot:\n%v", event1, runEvents[1])
   276  		}
   277  		return nil
   278  	}
   279  	tryWithTimeout(t, check)
   280  
   281  	runs, err = db.runs(0, nil, nil)
   282  	if err != nil {
   283  		t.Fatalf("error getting all runs: %v", err)
   284  	}
   285  	if len(runs) != 1 {
   286  		t.Fatalf("expected 1 run, got %d", len(runs))
   287  	}
   288  	if !reflect.DeepEqual(runs[0], expectedRun) {
   289  		t.Fatalf("expected run:\n%v\n\ngot:\n%v", expectedRun, runs[0])
   290  	}
   291  
   292  	// Fetch pending runs only
   293  	runEvents, err = db.runEvents(startTime, mkt, 0, nil, true, nil)
   294  	if err != nil {
   295  		t.Fatalf("error getting run events: %v", err)
   296  	}
   297  	if len(runEvents) != 1 {
   298  		t.Fatalf("expected 1 run events, got %d", len(runEvents))
   299  	}
   300  	if !reflect.DeepEqual(runEvents[0], event2) {
   301  		t.Fatalf("expected event:\n%v\n\ngot:\n%v", event2, runEvents[0])
   302  	}
   303  
   304  	err = db.endRun(startTime, mkt, startTime+1000)
   305  	if err != nil {
   306  		t.Fatalf("error ending run: %v", err)
   307  	}
   308  
   309  	overview, err := db.runOverview(startTime, mkt)
   310  	if err != nil {
   311  		t.Fatalf("error getting run overview: %v", err)
   312  	}
   313  	if *overview.EndTime != startTime+1000 {
   314  		t.Fatalf("expected end time %d, got %d", startTime+1000, overview.EndTime)
   315  	}
   316  	bs := currBalanceState()
   317  	finalBals := map[uint32]uint64{
   318  		42: bs.Balances[42].Available + bs.Balances[42].Pending + bs.Balances[42].Locked + bs.Balances[42].Reserved,
   319  		60: bs.Balances[60].Available + bs.Balances[60].Pending + bs.Balances[60].Locked + bs.Balances[60].Reserved,
   320  	}
   321  	if !reflect.DeepEqual(overview.InitialBalances, initialBals) {
   322  		t.Fatalf("expected initial balances %v, got %v", initialBals, overview.InitialBalances)
   323  	}
   324  	expPL := newProfitLoss(initialBals, finalBals, nil, fiatRates)
   325  	if overview.ProfitLoss.Profit != expPL.Profit {
   326  		t.Fatalf("expected profit loss %v, got %v", expPL, overview.ProfitLoss)
   327  	}
   328  	if !reflect.DeepEqual(overview.Cfgs[0].Cfg, cfg) {
   329  		t.Fatalf("expected:\n%s\n\ngot:\n%s", spew.Sdump(cfg), spew.Sdump(overview.Cfgs[0]))
   330  	}
   331  
   332  	// Test sorting / pagination of runs
   333  	err = db.storeNewRun(startTime+1, mkt, cfg, currBalanceState())
   334  	if err != nil {
   335  		t.Fatalf("error storing new run: %v", err)
   336  	}
   337  	err = db.storeNewRun(startTime-1, mkt, cfg, currBalanceState())
   338  	if err != nil {
   339  		t.Fatalf("error storing new run: %v", err)
   340  	}
   341  	runs, err = db.runs(2, nil, nil)
   342  	if err != nil {
   343  		t.Fatalf("error getting all runs: %v", err)
   344  	}
   345  	if len(runs) != 2 {
   346  		t.Fatalf("expected 2 runs, got %d", len(runs))
   347  	}
   348  	if runs[0].StartTime != startTime+1 {
   349  		t.Fatalf("expected run start time %d, got %d", startTime+1, runs[0].StartTime)
   350  	}
   351  	if runs[1].StartTime != startTime {
   352  		t.Fatalf("expected run start time %d, got %d", startTime, runs[1].StartTime)
   353  	}
   354  
   355  	refStartTime := uint64(startTime)
   356  	runs, err = db.runs(2, &refStartTime, mkt)
   357  	if err != nil {
   358  		t.Fatalf("error getting all runs: %v", err)
   359  	}
   360  	if len(runs) != 2 {
   361  		t.Fatalf("expected 2 runs, got %d", len(runs))
   362  	}
   363  	if runs[0].StartTime != startTime {
   364  		t.Fatalf("expected run start time %d, got %d", startTime, runs[0].StartTime)
   365  	}
   366  	if runs[1].StartTime != startTime-1 {
   367  		t.Fatalf("expected run start time %d, got %d", startTime-1, runs[1].StartTime)
   368  	}
   369  
   370  	// Update config and modify inventory
   371  	updatedCfgB, _ := json.Marshal(cfg)
   372  	updatedCfg := new(BotConfig)
   373  	json.Unmarshal(updatedCfgB, updatedCfg)
   374  	updatedCfg.ArbMarketMakerConfig.BuyPlacements[0].Lots++
   375  	inventoryMods[42] = 1e6
   376  	inventoryMods[60] = -2e6
   377  	updateCfgEvent := &MarketMakingEvent{
   378  		ID:           3,
   379  		TimeStamp:    startTime + 2,
   380  		UpdateConfig: updatedCfg,
   381  	}
   382  	db.storeEvent(startTime, mkt, updateCfgEvent, currBalanceState())
   383  
   384  	check = func() error {
   385  		runEvents, err := db.runOverview(startTime, mkt)
   386  		if err != nil {
   387  			return fmt.Errorf("error getting run events: %v", err)
   388  		}
   389  		if len(runEvents.Cfgs) != 2 {
   390  			return fmt.Errorf("expected 2 cfgs, got %d", len(runEvents.Cfgs))
   391  		}
   392  		if !reflect.DeepEqual(runEvents.Cfgs[1].Cfg, updatedCfg) {
   393  			return fmt.Errorf("expected updated cfg:\n%v\n\ngot:\n%v", spew.Sdump(updatedCfg), spew.Sdump(runEvents.Cfgs[1].Cfg))
   394  		}
   395  		if !reflect.DeepEqual(runEvents.Cfgs[0].Cfg, cfg) {
   396  			return fmt.Errorf("expected original cfg:\n%v\n\ngot:\n%v", spew.Sdump(cfg), spew.Sdump(runEvents.Cfgs[0].Cfg))
   397  		}
   398  		return nil
   399  	}
   400  
   401  	tryWithTimeout(t, check)
   402  }
   403  
   404  func TestUpdateFinalBalanceDueToEventDiff(t *testing.T) {
   405  	originalEvent := &MarketMakingEvent{
   406  		ID: 1,
   407  		BalanceEffects: &BalanceEffects{
   408  			Settled: map[uint32]int64{
   409  				42: 1e6,
   410  				0:  2e6,
   411  			},
   412  			Locked: map[uint32]uint64{
   413  				42: 3e6,
   414  				0:  4e6,
   415  			},
   416  			Pending: map[uint32]uint64{
   417  				42: 5e6,
   418  				0:  6e6,
   419  			},
   420  			Reserved: map[uint32]uint64{
   421  				42: 7e6,
   422  				0:  8e6,
   423  			},
   424  		},
   425  	}
   426  
   427  	finalState := &BalanceState{
   428  		FiatRates: map[uint32]float64{
   429  			42: 20,
   430  			60: 2500,
   431  		},
   432  		Balances: map[uint32]*BotBalance{
   433  			42: {
   434  				Available: 3e6,
   435  				Locked:    3e6,
   436  				Pending:   5e6,
   437  				Reserved:  7e6,
   438  			},
   439  			0: {
   440  				Available: 4e6,
   441  				Locked:    4e6,
   442  				Pending:   6e6,
   443  				Reserved:  8e6,
   444  			},
   445  		},
   446  	}
   447  
   448  	dir := t.TempDir()
   449  
   450  	ctx, cancel := context.WithCancel(context.Background())
   451  	defer cancel()
   452  
   453  	db, err := newBoltEventLogDB(ctx, filepath.Join(dir, "event_log.db"), tLogger)
   454  	if err != nil {
   455  		t.Fatalf("error creating event log db: %v", err)
   456  	}
   457  
   458  	startTime := time.Now().Unix()
   459  	mkt := &MarketWithHost{
   460  		Host:    "dex.com",
   461  		BaseID:  42,
   462  		QuoteID: 60,
   463  	}
   464  
   465  	cfg := &BotConfig{}
   466  
   467  	err = db.storeNewRun(startTime, mkt, cfg, finalState)
   468  	if err != nil {
   469  		t.Fatalf("error storing new run: %v", err)
   470  	}
   471  
   472  	db.storeEvent(startTime, mkt, originalEvent, finalState)
   473  
   474  	updatedEvent := &MarketMakingEvent{
   475  		ID: 1,
   476  		BalanceEffects: &BalanceEffects{
   477  			Settled: map[uint32]int64{
   478  				42: 1e6 + 100,
   479  				0:  2e6 - 300,
   480  			},
   481  			Locked: map[uint32]uint64{
   482  				42: 3e6 + 500,
   483  				0:  4e6 + 200,
   484  			},
   485  			Pending: map[uint32]uint64{
   486  				42: 5e6 - 100,
   487  				0:  6e6 - 800,
   488  			},
   489  			Reserved: map[uint32]uint64{
   490  				42: 0,
   491  				0:  0,
   492  			},
   493  		},
   494  	}
   495  
   496  	db.storeEvent(startTime, mkt, updatedEvent, nil)
   497  
   498  	expectedUpdatedFinalState := &BalanceState{
   499  		FiatRates: map[uint32]float64{
   500  			42: 20,
   501  			60: 2500,
   502  		},
   503  		Balances: map[uint32]*BotBalance{
   504  			42: {
   505  				Available: 3e6 + 100,
   506  				Locked:    3e6 + 500,
   507  				Pending:   5e6 - 100,
   508  				Reserved:  0,
   509  			},
   510  			0: {
   511  				Available: 4e6 - 300,
   512  				Locked:    4e6 + 200,
   513  				Pending:   6e6 - 800,
   514  				Reserved:  0,
   515  			},
   516  		},
   517  	}
   518  
   519  	checkFinalState := func() error {
   520  		overview, err := db.runOverview(startTime, mkt)
   521  		if err != nil {
   522  			return fmt.Errorf("error getting final state: %v", err)
   523  		}
   524  		if !reflect.DeepEqual(overview.FinalState, expectedUpdatedFinalState) {
   525  			return fmt.Errorf("expected final state:\n%v\n\ngot:\n%v", expectedUpdatedFinalState, finalState)
   526  		}
   527  		return nil
   528  	}
   529  
   530  	tryWithTimeout(t, checkFinalState)
   531  }