Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(x/twap): Geometric Mean TWAP POC #3420

Merged
merged 15 commits into from
Nov 28, 2022
Merged

feat(x/twap): Geometric Mean TWAP POC #3420

merged 15 commits into from
Nov 28, 2022

Conversation

p0mvn
Copy link
Member

@p0mvn p0mvn commented Nov 17, 2022

Part of: #3013

What is the purpose of the change

This is a proof-of-concept Geometric Mean TWAP implementation.

While the arithmetic mean TWAPs are much more widely used, they should theoretically be less accurate in measuring a geometric Brownian motion process (which is how price movements are usually modeled)

Arithmetic TWAP tends to overweight higher prices relative to lower ones.

Therefore, we also support a geometric mean TWAP.

The core functionality stays similar to the arithmetic mean TWAP. However, instead of computing the geometric mean TWAP naively, we use the following property:

GeometricTwap(P) = 1.0001^{ArithmeticTwap(log_{1.0001}{P})}

Naive computation is expensive and easily overflows. As a result, we track logarithms of prices instead of prices themselves in the accumulators.
When geometric twap is requested, we first compute the arithmetic mean of the logarithms, and then exponentiate it with the same base as the logarithm

Brief Changelog

  • added geometric twap accumulator to the twap record
  • implemented geometric accumulator update
  • refactored computeArithmeticTwap method to be more general and named it computeTwap
  • implemented computedGeometricTwap method that is called by computeTwap
  • added tests

Testing and Verifying

This change added tests.

Tracked Future Work

Blocking TODOs

Clean up TODOs

  • address all TODOs for prices and add pruning tests
  • spec and tests for osmomath Dec's TwapLog

Documentation and Release Note

  • Does this pull request introduce a new feature or user-facing behavior changes? no
  • Is a relevant changelog entry added to the Unreleased section in CHANGELOG.md? no
  • How is the feature or change documented? not applicable

@github-actions github-actions bot added C:docs Improvements or additions to documentation C:x/twap Changes to the twap module labels Nov 17, 2022
@p0mvn p0mvn added the V:state/breaking State machine breaking PR label Nov 17, 2022
@p0mvn p0mvn marked this pull request as ready for review November 17, 2022 03:53
@p0mvn p0mvn requested a review from a team November 17, 2022 03:54
osmomath/math.go Outdated

// TODO: spec and tests
func TwapLog(x sdk.Dec) sdk.Dec {
return BigDecFromSDKDec(x).TickLog().SDKDec()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: I think the use of "tick" here is confusing, since afaik that's a concentrated liquidity term.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

TickLog refers to 1.0001 base log which we call as tick.

I'm open to suggestions. This is the best semantics I could think of to refer to this specific choice of base.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed, I also recommend we just do log2

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to understand this why was 1.0001 base log used instead of log_2 in first place?

logP0SpotPrice := osmomath.TwapLog(record.P0LastSpotPrice)
// p0NewGeomAccum = log_{1.0001}{P_0} * timeDelta
p0NewGeomAccum := types.SpotPriceMulDuration(logP0SpotPrice, timeDelta)
newRecord.GeometricTwapAccumulator = newRecord.GeometricTwapAccumulator.Add(p0NewGeomAccum)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is it enough to store this for P0? Is it cause we can later obtain p1 via 1/p0?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exactly. We use this property: Geometric mean of the reciprocals is the reciprocal of the geometric mean.

I attached the proof in the computeGeometricTwap function.

x/twap/logic.go Outdated
// precondition: endRecord.Time >= startRecord.Time
// if (endRecord.LastErrorTime >= startRecord.Time) returns an error at end + result
// if (startRecord.LastErrorTime == startRecord.Time) returns an error at end + result
// if (endRecord.Time == startRecord.Time) returns endRecord.LastSpotPrice
// else returns
// (endRecord.Accumulator - startRecord.Accumulator) / (endRecord.Time - startRecord.Time)
func computeArithmeticTwap(startRecord types.TwapRecord, endRecord types.TwapRecord, quoteAsset string) (sdk.Dec, error) {
func computeTwap(startRecord types.TwapRecord, endRecord types.TwapRecord, quoteAsset string, twapType twapType) (sdk.Dec, error) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In this function we short circuit if timeDelta == 0 and return endRecord.PxLastSpotPrice. Shouldn't that be different for Geometric twap? i.e.: osmomath.Pow(osmomath.Tick, endRecord.PxLastSpotPrice)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The last price is not stored in log scale. Only the accumulator is. So returning the actual price should be what we want

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ahh, of course... that makes sense.

Copy link
Member

@czarcas7ic czarcas7ic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Still grasping the concepts, but overall logic appears sound to me


// accumA = 10 seconds * (spot price = 10) = OneSec * 10 * 10
// accumB = 10 seconds * (spot price = 0.1) = OneSec
// accumC = 10 seconds * (spot price = 20) = OneSec * 10 * 20
accumA, accumB, accumC sdk.Dec = OneSec.MulInt64(10 * 10), OneSec, OneSec.MulInt64(10 * 20)

// geomAccumAB = 10 seconds * (log_{1.0001}{spot price = 10})
geomAccumAB = geometricTenSecAccum.MulInt64(10)
geomAccumAC = geomAccumAB
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the geomAccumAC = geomAccumAB because we use the spot price of the first asset (A) which they share?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's due to how our tests are set up. Specifically, newThreeAssetPoolTwapRecordWithDefaults

Comment on lines +65 to +78
OneSec.MulInt64(3), // accum B
sdk.ZeroDec(), // TODO: choose correct
)

tPlus20sp2ThreeAssetRecordAB, tPlus20sp2ThreeAssetRecordAC, tPlus20sp2ThreeAssetRecordBC = newThreeAssetPoolTwapRecordWithDefaults(
baseTime.Add(20*time.Second),
sdk.NewDec(2), // spot price 0
OneSec.MulInt64(10*10+5*10), // accum A
OneSec.MulInt64(3), // accum B
OneSec.MulInt64(20*10+10*10)) // accum C
OneSec.MulInt64(20*10+10*10), // accum C
sdk.ZeroDec(), // TODO: choose correct
sdk.ZeroDec(), // TODO: choose correct
sdk.ZeroDec(), // TODO: choose correct
)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do we mean here by "choose correct"?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to add more tests where these values would be used.

Currently, this is set to zero to avoid existing arithmetic twap tests from failing due to geometric accumulators not being initialized.

Comment on lines +258 to +261
}
return computeGeometricTwap(startRecord, endRecord, quoteAsset), err
}

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we should explicitly take twapType == geometricTwapType as and else if here. Otherwise we might accidentally compute geomtric twap for an invalid type input

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point. I changed twapType to be a boolean so this is not possible anymore

Comment on lines +165 to +169
baseRecord := withPrice0Set(newEmptyPriceRecord(1, baseTime, denom0, denom1), sdk.OneDec())
tMin1 := baseTime.Add(-time.Second)
tMin1Record := newEmptyPriceRecord(1, tMin1, denom0, denom1)
tMin1Record := withPrice0Set(newEmptyPriceRecord(1, tMin1, denom0, denom1), sdk.OneDec())
tPlus1 := baseTime.Add(time.Second)
tPlus1Record := newEmptyPriceRecord(1, tPlus1, denom0, denom1)
tPlus1Record := withPrice0Set(newEmptyPriceRecord(1, tPlus1, denom0, denom1), sdk.OneDec())
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

why are we now setting price0 instead of initializing with empty price record?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

price0 of 0 would panic because the logarithm of 0 is undefined.

This is safe because, logically, spot price of 0 should be undefined as well

osmomath/math.go Outdated
@@ -15,6 +15,8 @@ var powPrecision, _ = sdk.NewDecFromStr("0.00000001")
var zero sdk.Dec = sdk.ZeroDec()

var (
Tick = sdk.MustNewDecFromStr("1.0001")
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we doing logs respective to this base, rather than just 2

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm tracking this as one of the "Blocking TODOs" in the PR description to revisit next.

I've been following uniswap whitepaper. However, I've also come to the conclusion that we don't need this choice of base and plan to choose the new one while working on overflow tests.

With the base of 2, I've also run into overflow problems so I would like to tackle the correct choice of base next in a separate PR.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Changed base to 2 but still might need to revisit once overflow tests are added

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

SG! (FWIW im not at all married to the base of 2! Just if we don't have a reason for any particular base, or some number being superior to 2, would prefer to default to 2)

x/twap/README.md Outdated

Therefore, we also support a geometric mean TWAP.

The core functionality stays similar to the arithmetic mean TWAP. However, instead of computing the geometric mean TWAP naively, we use the following property:
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

First the naive should be defined

Then how we compute it =p

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

x/twap/README.md Outdated Show resolved Hide resolved
@p0mvn
Copy link
Member Author

p0mvn commented Nov 18, 2022

@czarcas7ic @ValarDragon @nicolaslara all requested changes are added

Copy link
Member

@czarcas7ic czarcas7ic left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM as first step, acknowledging the remaining TODOs. Nice work 👍

@p0mvn
Copy link
Member Author

p0mvn commented Nov 23, 2022

While this is not urgent, this PR is still waiting for one more approval

x/twap/logic.go Outdated Show resolved Hide resolved
x/twap/logic.go Outdated Show resolved Hide resolved
x/twap/logic.go Show resolved Hide resolved
osmomath/math.go Outdated

// TODO: spec and tests
func TwapLog(x sdk.Dec) sdk.Dec {
return BigDecFromSDKDec(x).TickLog().SDKDec()
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm trying to understand this why was 1.0001 base log used instead of log_2 in first place?

Copy link
Contributor

@stackman27 stackman27 left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

LGTM!

@p0mvn
Copy link
Member Author

p0mvn commented Nov 27, 2022

Merging this to continue making progress on geometrict TWAP. The next items are tracked in: #3013

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
C:docs Improvements or additions to documentation C:x/twap Changes to the twap module V:state/breaking State machine breaking PR
Projects
Archived in project
Development

Successfully merging this pull request may close these issues.

5 participants