code.vegaprotocol.io/vega@v0.79.0/core/liquidity/v2/snapshot.go (about) 1 // Copyright (C) 2023 Gobalsky Labs Limited 2 // 3 // This program is free software: you can redistribute it and/or modify 4 // it under the terms of the GNU Affero General Public License as 5 // published by the Free Software Foundation, either version 3 of the 6 // License, or (at your option) any later version. 7 // 8 // This program is distributed in the hope that it will be useful, 9 // but WITHOUT ANY WARRANTY; without even the implied warranty of 10 // MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 11 // GNU Affero General Public License for more details. 12 // 13 // You should have received a copy of the GNU Affero General Public License 14 // along with this program. If not, see <http://www.gnu.org/licenses/>. 15 16 package liquidity 17 18 import ( 19 "context" 20 "fmt" 21 "sort" 22 "time" 23 24 "code.vegaprotocol.io/vega/core/events" 25 "code.vegaprotocol.io/vega/core/types" 26 "code.vegaprotocol.io/vega/libs/num" 27 "code.vegaprotocol.io/vega/libs/proto" 28 "code.vegaprotocol.io/vega/logging" 29 typespb "code.vegaprotocol.io/vega/protos/vega" 30 snapshotpb "code.vegaprotocol.io/vega/protos/vega/snapshot/v1" 31 ) 32 33 const defaultFeeCalculationTimeStep = time.Minute 34 35 type SnapshotEngine struct { 36 *Engine 37 38 *snapshotV2 39 *snapshotV1 40 } 41 42 func (e SnapshotEngine) OnEpochRestore(ep types.Epoch) { 43 e.slaEpochStart = ep.StartTime 44 } 45 46 func (e SnapshotEngine) V2StateProvider() types.StateProvider { 47 return e.snapshotV2 48 } 49 50 func (e SnapshotEngine) V1StateProvider() types.StateProvider { 51 return e.snapshotV1 52 } 53 54 func (e SnapshotEngine) StopSnapshots() { 55 e.snapshotV1.Stop() 56 e.snapshotV2.Stop() 57 } 58 59 type snapshotV2 struct { 60 *Engine 61 62 pl types.Payload 63 market string 64 65 // liquidity types 66 stopped bool 67 serialisedProvisions []byte 68 serialisedPendingProvisions []byte 69 serialisedPerformances []byte 70 serialisedSupplied []byte 71 serialisedScores []byte 72 serialisedParemeters []byte 73 serialisedFeeStats []byte 74 75 // Keys need to be computed when the engine is instantiated as they are dynamic. 76 hashKeys []string 77 provisionsKey string 78 pendingProvisionsKey string 79 performancesKey string 80 scoresKey string 81 suppliedKey string 82 paramsKey string 83 feeStatsKey string 84 } 85 86 func (e *snapshotV2) Namespace() types.SnapshotNamespace { 87 return types.LiquidityV2Snapshot 88 } 89 90 func (e *snapshotV2) Keys() []string { 91 return e.hashKeys 92 } 93 94 func (e *snapshotV2) GetState(k string) ([]byte, []types.StateProvider, error) { 95 state, err := e.serialise(k) 96 return state, nil, err 97 } 98 99 func (e *snapshotV2) LoadState(ctx context.Context, p *types.Payload) ([]types.StateProvider, error) { 100 if e.Namespace() != p.Data.Namespace() { 101 return nil, types.ErrInvalidSnapshotNamespace 102 } 103 104 switch pl := p.Data.(type) { 105 case *types.PayloadLiquidityV2Provisions: 106 return nil, e.loadProvisions(ctx, pl.Provisions.GetLiquidityProvisions(), p) 107 case *types.PayloadLiquidityV2PendingProvisions: 108 return nil, e.loadPendingProvisions(ctx, pl.PendingProvisions.GetPendingLiquidityProvisions(), p) 109 case *types.PayloadLiquidityV2Performances: 110 return nil, e.loadPerformances(pl.Performances, p) 111 case *types.PayloadLiquidityV2Supplied: 112 return nil, e.loadSupplied(pl.Supplied, p) 113 case *types.PayloadLiquidityV2Scores: 114 return nil, e.loadScores(pl.Scores, p) 115 case *types.PayloadLiquidityV2Parameters: 116 return nil, e.loadParameters(pl.Parameters, p) 117 case *types.PayloadPaidLiquidityV2FeeStats: 118 e.loadFeeStats(pl.Stats, p) 119 return nil, nil 120 default: 121 return nil, types.ErrUnknownSnapshotType 122 } 123 } 124 125 func (e *snapshotV2) Stopped() bool { 126 return e.stopped 127 } 128 129 func (e *snapshotV2) Stop() { 130 e.log.Debug("market has been cleared, stopping snapshot production", logging.MarketID(e.marketID)) 131 e.stopped = true 132 } 133 134 func (e *snapshotV2) serialise(k string) ([]byte, error) { 135 var ( 136 buf []byte 137 err error 138 ) 139 140 switch k { 141 case e.provisionsKey: 142 buf, err = e.serialiseProvisions() 143 case e.pendingProvisionsKey: 144 buf, err = e.serialisePendingProvisions() 145 case e.performancesKey: 146 buf, err = e.serialisePerformances() 147 case e.suppliedKey: 148 buf, err = e.serialiseSupplied() 149 case e.scoresKey: 150 buf, err = e.serialiseScores() 151 case e.paramsKey: 152 buf, err = e.serialiseParameters() 153 case e.feeStatsKey: 154 buf, err = e.serialiseFeeStats() 155 default: 156 return nil, types.ErrSnapshotKeyDoesNotExist 157 } 158 159 if err != nil { 160 return nil, err 161 } 162 163 if e.stopped { 164 return nil, nil 165 } 166 167 switch k { 168 case e.provisionsKey: 169 e.serialisedProvisions = buf 170 case e.pendingProvisionsKey: 171 e.serialisedPendingProvisions = buf 172 case e.performancesKey: 173 e.serialisedPerformances = buf 174 case e.suppliedKey: 175 e.serialisedSupplied = buf 176 case e.scoresKey: 177 e.serialisedScores = buf 178 case e.paramsKey: 179 e.serialisedParemeters = buf 180 case e.feeStatsKey: 181 e.serialisedFeeStats = buf 182 default: 183 return nil, types.ErrSnapshotKeyDoesNotExist 184 } 185 186 return buf, nil 187 } 188 189 func (e *snapshotV2) serialiseProvisions() ([]byte, error) { 190 // these are sorted already, only a conversion to proto is needed 191 lps := e.Engine.provisions.Slice() 192 pblps := make([]*typespb.LiquidityProvision, 0, len(lps)) 193 for _, v := range lps { 194 pblps = append(pblps, v.IntoProto()) 195 } 196 197 payload := &snapshotpb.Payload{ 198 Data: &snapshotpb.Payload_LiquidityV2Provisions{ 199 LiquidityV2Provisions: &snapshotpb.LiquidityV2Provisions{ 200 MarketId: e.market, 201 LiquidityProvisions: pblps, 202 }, 203 }, 204 } 205 206 return e.marshalPayload(payload) 207 } 208 209 func (e *snapshotV2) serialisePendingProvisions() ([]byte, error) { 210 // these are sorted already, only a conversion to proto is needed 211 lps := e.Engine.pendingProvisions.Slice() 212 pblps := make([]*typespb.LiquidityProvision, 0, len(lps)) 213 for _, v := range lps { 214 pblps = append(pblps, v.IntoProto()) 215 } 216 217 payload := &snapshotpb.Payload{ 218 Data: &snapshotpb.Payload_LiquidityV2PendingProvisions{ 219 LiquidityV2PendingProvisions: &snapshotpb.LiquidityV2PendingProvisions{ 220 MarketId: e.market, 221 PendingLiquidityProvisions: pblps, 222 }, 223 }, 224 } 225 226 return e.marshalPayload(payload) 227 } 228 229 func (e *snapshotV2) serialisePerformances() ([]byte, error) { 230 // Extract and sort the parties to serialize a deterministic array. 231 parties := make([]string, 0, len(e.slaPerformance)) 232 for party := range e.slaPerformance { 233 parties = append(parties, party) 234 } 235 sort.Strings(parties) 236 237 performancePerPartySnapshot := make([]*snapshotpb.LiquidityV2PerformancePerParty, 0, len(e.slaPerformance)) 238 for _, party := range parties { 239 partyPerformance := e.slaPerformance[party] 240 241 trueLen := 0 242 registeredPenaltiesPerEpochSnapshot := make([]string, 0, partyPerformance.previousPenalties.Len()) 243 for _, registeredPenalty := range partyPerformance.previousPenalties.Slice() { 244 if registeredPenalty != nil { 245 trueLen++ 246 registeredPenaltiesPerEpochSnapshot = append(registeredPenaltiesPerEpochSnapshot, registeredPenalty.String()) 247 } 248 } 249 registeredPenaltiesPerEpochSnapshot = registeredPenaltiesPerEpochSnapshot[0:trueLen] 250 251 var start int64 252 if partyPerformance.start != (time.Time{}) { 253 start = partyPerformance.start.UnixNano() 254 } 255 256 partyPerformanceSnapshot := &snapshotpb.LiquidityV2PerformancePerParty{ 257 Party: party, 258 ElapsedTimeMeetingSlaDuringEpoch: int64(partyPerformance.s), 259 CommitmentStartTime: start, 260 RegisteredPenaltiesPerEpoch: registeredPenaltiesPerEpochSnapshot, 261 PositionInPenaltiesPerEpoch: uint32(partyPerformance.previousPenalties.Position()), 262 LastEpochFractionOfTimeOnBook: partyPerformance.lastEpochTimeBookFraction, 263 LastEpochFeePenalty: partyPerformance.lastEpochFeePenalty, 264 LastEpochBondPenalty: partyPerformance.lastEpochBondPenalty, 265 RequiredLiquidity: partyPerformance.requiredLiquidity, 266 NotionalVolumeBuys: partyPerformance.notionalVolumeBuys, 267 NotionalVolumeSells: partyPerformance.notionalVolumeSells, 268 } 269 270 performancePerPartySnapshot = append(performancePerPartySnapshot, partyPerformanceSnapshot) 271 } 272 273 payload := &snapshotpb.Payload{ 274 Data: &snapshotpb.Payload_LiquidityV2Performances{ 275 LiquidityV2Performances: &snapshotpb.LiquidityV2Performances{ 276 MarketId: e.market, 277 EpochStartTime: e.slaEpochStart.UnixNano(), 278 PerformancePerParty: performancePerPartySnapshot, 279 }, 280 }, 281 } 282 283 return e.marshalPayload(payload) 284 } 285 286 func (e *snapshotV2) serialiseSupplied() ([]byte, error) { 287 v1Payload := e.suppliedEngine.Payload() 288 289 // Dirty hack to support serialization of a mutualized supplied engine between 290 // liquidity engine version 1 and 2. 291 supplied := v1Payload.GetLiquiditySupplied() 292 return e.marshalPayload(&snapshotpb.Payload{ 293 Data: &snapshotpb.Payload_LiquidityV2Supplied{ 294 LiquidityV2Supplied: &snapshotpb.LiquidityV2Supplied{ 295 MarketId: supplied.MarketId, 296 ConsensusReached: supplied.ConsensusReached, 297 BidCache: supplied.BidCache, 298 AskCache: supplied.AskCache, 299 }, 300 }, 301 }) 302 } 303 304 func (e *snapshotV2) serialiseScores() ([]byte, error) { 305 scores := make([]*snapshotpb.LiquidityScore, 0, len(e.avgScores)) 306 307 keys := make([]string, 0, len(e.avgScores)) 308 for k := range e.avgScores { 309 keys = append(keys, k) 310 } 311 sort.Strings(keys) 312 313 for _, k := range keys { 314 s := &snapshotpb.LiquidityScore{ 315 PartyId: k, 316 Score: e.avgScores[k].String(), 317 } 318 scores = append(scores, s) 319 } 320 321 var lastFeeDistributionTime int64 322 if !e.lastFeeDistribution.IsZero() { 323 lastFeeDistributionTime = e.lastFeeDistribution.UnixNano() 324 } 325 326 var feeCalculationTimeStep time.Duration 327 if e.feeCalculationTimeStep != 0 { 328 feeCalculationTimeStep = e.feeCalculationTimeStep 329 } else { 330 feeCalculationTimeStep = defaultFeeCalculationTimeStep 331 } 332 333 payload := &snapshotpb.Payload{ 334 Data: &snapshotpb.Payload_LiquidityV2Scores{ 335 LiquidityV2Scores: &snapshotpb.LiquidityV2Scores{ 336 MarketId: e.market, 337 RunningAverageCounter: int32(e.nAvg), 338 Scores: scores, 339 LastFeeDistributionTime: lastFeeDistributionTime, 340 FeeCalculationTimeStep: int64(feeCalculationTimeStep), 341 }, 342 }, 343 } 344 345 return e.marshalPayload(payload) 346 } 347 348 func (e *snapshotV2) serialiseParameters() ([]byte, error) { 349 payload := &snapshotpb.Payload{ 350 Data: &snapshotpb.Payload_LiquidityV2Parameters{ 351 LiquidityV2Parameters: &snapshotpb.LiquidityV2Parameters{ 352 MarketId: e.market, 353 MarketSlaParameters: e.slaParams.IntoProto(), 354 StakeToVolume: e.stakeToCcyVolume.String(), 355 BondPenaltySlope: e.nonPerformanceBondPenaltySlope.String(), 356 BondPenaltyMax: e.nonPerformanceBondPenaltyMax.String(), 357 }, 358 }, 359 } 360 361 return e.marshalPayload(payload) 362 } 363 364 func (e *snapshotV2) serialiseFeeStats() ([]byte, error) { 365 payload := &snapshotpb.Payload{ 366 Data: &snapshotpb.Payload_LiquidityV2PaidFeesStats{ 367 LiquidityV2PaidFeesStats: &snapshotpb.LiquidityV2PaidFeesStats{ 368 MarketId: e.market, 369 Stats: e.allocatedFeesStats.ToProto(e.market, e.asset, 0), // I don't think it matters what the epoch is as this is just used for snapshots 370 }, 371 }, 372 } 373 374 return e.marshalPayload(payload) 375 } 376 377 func (e *snapshotV2) marshalPayload(payload *snapshotpb.Payload) ([]byte, error) { 378 buf, err := proto.Marshal(payload) 379 if err != nil { 380 return nil, err 381 } 382 383 return buf, nil 384 } 385 386 func (e *snapshotV2) loadProvisions(ctx context.Context, provisions []*typespb.LiquidityProvision, p *types.Payload) error { 387 e.Engine.provisions = newSnapshotableProvisionsPerParty() 388 389 evts := make([]events.Event, 0, len(provisions)) 390 for _, v := range provisions { 391 provision, err := types.LiquidityProvisionFromProto(v) 392 if err != nil { 393 return err 394 } 395 e.Engine.provisions.Set(v.PartyId, provision) 396 evts = append(evts, events.NewLiquidityProvisionEvent(ctx, provision)) 397 } 398 399 var err error 400 e.serialisedProvisions, err = proto.Marshal(p.IntoProto()) 401 e.broker.SendBatch(evts) 402 return err 403 } 404 405 func (e *snapshotV2) loadPendingProvisions(ctx context.Context, provisions []*typespb.LiquidityProvision, p *types.Payload) error { 406 e.Engine.pendingProvisions = newSnapshotablePendingProvisions() 407 408 evts := make([]events.Event, 0, len(provisions)) 409 for _, v := range provisions { 410 provision, err := types.LiquidityProvisionFromProto(v) 411 if err != nil { 412 return err 413 } 414 e.Engine.pendingProvisions.Set(provision) 415 evts = append(evts, events.NewLiquidityProvisionEvent(ctx, provision)) 416 } 417 418 var err error 419 e.serialisedPendingProvisions, err = proto.Marshal(p.IntoProto()) 420 e.broker.SendBatch(evts) 421 return err 422 } 423 424 func (e *snapshotV2) loadPerformances(performances *snapshotpb.LiquidityV2Performances, p *types.Payload) error { 425 var err error 426 427 e.Engine.slaEpochStart = time.Unix(0, performances.EpochStartTime) 428 429 e.Engine.slaPerformance = map[string]*slaPerformance{} 430 for _, partyPerformance := range performances.PerformancePerParty { 431 registeredPenaltiesPerEpochAsDecimal := make([]*num.Decimal, 0, len(partyPerformance.RegisteredPenaltiesPerEpoch)) 432 for _, registeredPenalty := range partyPerformance.RegisteredPenaltiesPerEpoch { 433 registeredPenaltyAsDecimal, err := num.DecimalFromString(registeredPenalty) 434 if err != nil { 435 return fmt.Errorf("invalid penalty %q for party %q on market %q: %w", registeredPenalty, partyPerformance.Party, performances.MarketId, err) 436 } 437 registeredPenaltiesPerEpochAsDecimal = append(registeredPenaltiesPerEpochAsDecimal, ®isteredPenaltyAsDecimal) 438 } 439 440 previousPenalties := restoreSliceRing[*num.Decimal]( 441 registeredPenaltiesPerEpochAsDecimal, 442 e.Engine.slaParams.PerformanceHysteresisEpochs, 443 int(partyPerformance.PositionInPenaltiesPerEpoch), 444 ) 445 446 var startTime time.Time 447 if partyPerformance.CommitmentStartTime > 0 { 448 startTime = time.Unix(0, partyPerformance.CommitmentStartTime) 449 } 450 451 e.Engine.slaPerformance[partyPerformance.Party] = &slaPerformance{ 452 s: time.Duration(partyPerformance.ElapsedTimeMeetingSlaDuringEpoch), 453 start: startTime, 454 previousPenalties: previousPenalties, 455 lastEpochTimeBookFraction: partyPerformance.LastEpochFractionOfTimeOnBook, 456 lastEpochBondPenalty: partyPerformance.LastEpochBondPenalty, 457 lastEpochFeePenalty: partyPerformance.LastEpochFeePenalty, 458 requiredLiquidity: partyPerformance.RequiredLiquidity, 459 notionalVolumeBuys: partyPerformance.NotionalVolumeBuys, 460 notionalVolumeSells: partyPerformance.NotionalVolumeSells, 461 } 462 } 463 464 e.serialisedPerformances, err = proto.Marshal(p.IntoProto()) 465 return err 466 } 467 468 func (e *snapshotV2) loadSupplied(ls *snapshotpb.LiquidityV2Supplied, p *types.Payload) error { 469 // Dirty hack so we can reuse the supplied engine from the liquidity engine v1, 470 // without snapshot payload namespace issue. 471 err := e.suppliedEngine.Reload(&snapshotpb.LiquiditySupplied{ 472 MarketId: ls.MarketId, 473 ConsensusReached: ls.ConsensusReached, 474 BidCache: ls.BidCache, 475 AskCache: ls.AskCache, 476 }) 477 if err != nil { 478 return err 479 } 480 e.serialisedSupplied, err = proto.Marshal(p.IntoProto()) 481 return err 482 } 483 484 func (e *snapshotV2) loadScores(ls *snapshotpb.LiquidityV2Scores, p *types.Payload) error { 485 var err error 486 487 e.nAvg = int64(ls.RunningAverageCounter) 488 if ls.LastFeeDistributionTime == 0 { 489 e.lastFeeDistribution = time.Time{} 490 } else { 491 e.lastFeeDistribution = time.Unix(0, ls.LastFeeDistributionTime) 492 } 493 494 if ls.FeeCalculationTimeStep != 0 { 495 e.feeCalculationTimeStep = time.Duration(ls.FeeCalculationTimeStep) 496 } else { 497 e.feeCalculationTimeStep = defaultFeeCalculationTimeStep 498 } 499 500 scores := make(map[string]num.Decimal, len(ls.Scores)) 501 for _, p := range ls.Scores { 502 score, err := num.DecimalFromString(p.Score) 503 if err != nil { 504 return err 505 } 506 scores[p.PartyId] = score 507 } 508 509 e.avgScores = scores 510 511 e.serialisedScores, err = proto.Marshal(p.IntoProto()) 512 return err 513 } 514 515 func (e *snapshotV2) loadParameters(ls *snapshotpb.LiquidityV2Parameters, p *types.Payload) error { 516 var err error 517 518 // market SLA parameters 519 e.slaParams = types.LiquiditySLAParamsFromProto(ls.MarketSlaParameters) 520 521 one := num.DecimalOne() 522 e.openPlusPriceRange = one.Add(e.slaParams.PriceRange) 523 e.openMinusPriceRange = one.Sub(e.slaParams.PriceRange) 524 525 // now network SLA parameters 526 bondMax, _ := num.DecimalFromString(ls.BondPenaltyMax) 527 bondSlope, _ := num.DecimalFromString(ls.BondPenaltySlope) 528 stakeToVolume, _ := num.DecimalFromString(ls.StakeToVolume) 529 530 e.nonPerformanceBondPenaltyMax = bondMax 531 e.nonPerformanceBondPenaltySlope = bondSlope 532 e.stakeToCcyVolume = stakeToVolume 533 534 e.serialisedParemeters, err = proto.Marshal(p.IntoProto()) 535 return err 536 } 537 538 func (e *snapshotV2) loadFeeStats(ls *snapshotpb.LiquidityV2PaidFeesStats, _ *types.Payload) { 539 e.allocatedFeesStats = types.NewPaidLiquidityFeesStatsFromProto(ls.Stats) 540 } 541 542 func (e *snapshotV2) buildHashKeys(market string) { 543 e.provisionsKey = (&types.PayloadLiquidityV2Provisions{ 544 Provisions: &snapshotpb.LiquidityV2Provisions{ 545 MarketId: market, 546 }, 547 }).Key() 548 549 e.pendingProvisionsKey = (&types.PayloadLiquidityV2PendingProvisions{ 550 PendingProvisions: &snapshotpb.LiquidityV2PendingProvisions{ 551 MarketId: market, 552 }, 553 }).Key() 554 555 e.performancesKey = (&types.PayloadLiquidityV2Performances{ 556 Performances: &snapshotpb.LiquidityV2Performances{ 557 MarketId: market, 558 }, 559 }).Key() 560 561 e.suppliedKey = (&types.PayloadLiquidityV2Supplied{ 562 Supplied: &snapshotpb.LiquidityV2Supplied{ 563 MarketId: market, 564 }, 565 }).Key() 566 567 e.scoresKey = (&types.PayloadLiquidityV2Scores{ 568 Scores: &snapshotpb.LiquidityV2Scores{ 569 MarketId: market, 570 }, 571 }).Key() 572 573 e.paramsKey = (&types.PayloadLiquidityV2Parameters{ 574 Parameters: &snapshotpb.LiquidityV2Parameters{ 575 MarketId: market, 576 }, 577 }).Key() 578 579 e.feeStatsKey = (&types.PayloadPaidLiquidityV2FeeStats{ 580 Stats: &snapshotpb.LiquidityV2PaidFeesStats{ 581 MarketId: market, 582 }, 583 }).Key() 584 585 e.hashKeys = append([]string{}, 586 e.provisionsKey, 587 e.pendingProvisionsKey, 588 e.performancesKey, 589 e.suppliedKey, 590 e.scoresKey, 591 e.paramsKey, 592 e.feeStatsKey, 593 ) 594 } 595 596 func defaultLiquiditySLAParams() *types.LiquiditySLAParams { 597 return &types.LiquiditySLAParams{ 598 PriceRange: num.DecimalFromFloat(0.05), 599 CommitmentMinTimeFraction: num.DecimalFromFloat(0.95), 600 SlaCompetitionFactor: num.DecimalFromFloat(0.9), 601 PerformanceHysteresisEpochs: 1, 602 } 603 } 604 605 func NewSnapshotEngine( 606 config Config, 607 log *logging.Logger, 608 timeService TimeService, 609 broker Broker, 610 riskModel RiskModel, 611 priceMonitor PriceMonitor, 612 orderBook OrderBook, 613 auctionState AuctionState, 614 asset string, 615 marketID string, 616 stateVarEngine StateVarEngine, 617 positionFactor num.Decimal, 618 slaParams *types.LiquiditySLAParams, 619 ) *SnapshotEngine { 620 if slaParams == nil { 621 slaParams = defaultLiquiditySLAParams() 622 } 623 624 e := NewEngine( 625 config, 626 log, 627 timeService, 628 broker, 629 riskModel, 630 priceMonitor, 631 orderBook, 632 auctionState, 633 asset, 634 marketID, 635 stateVarEngine, 636 positionFactor, 637 slaParams, 638 ) 639 640 se := &SnapshotEngine{ 641 Engine: e, 642 snapshotV2: &snapshotV2{ 643 Engine: e, 644 pl: types.Payload{}, 645 market: marketID, 646 stopped: false, 647 }, 648 snapshotV1: &snapshotV1{ 649 Engine: e, 650 market: marketID, 651 }, 652 } 653 654 se.buildHashKeys(marketID) 655 656 return se 657 }