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 }