-
Notifications
You must be signed in to change notification settings - Fork 628
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
Conversation
edeced1
to
ccc7c5a
Compare
…eGeometricTwap stub
6fd661d
to
84bb483
Compare
osmomath/math.go
Outdated
|
||
// TODO: spec and tests | ||
func TwapLog(x sdk.Dec) sdk.Dec { | ||
return BigDecFromSDKDec(x).TickLog().SDKDec() |
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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
?
There was a problem hiding this comment.
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) { |
There was a problem hiding this comment.
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)
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this 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 |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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
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 | ||
) |
There was a problem hiding this comment.
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"?
There was a problem hiding this comment.
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.
} | ||
return computeGeometricTwap(startRecord, endRecord, quoteAsset), err | ||
} | ||
|
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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
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()) |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
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") |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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.
There was a problem hiding this comment.
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
There was a problem hiding this comment.
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: |
There was a problem hiding this comment.
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
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@czarcas7ic @ValarDragon @nicolaslara all requested changes are added |
There was a problem hiding this 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 👍
While this is not urgent, this PR is still waiting for one more approval |
osmomath/math.go
Outdated
|
||
// TODO: spec and tests | ||
func TwapLog(x sdk.Dec) sdk.Dec { | ||
return BigDecFromSDKDec(x).TickLog().SDKDec() |
There was a problem hiding this comment.
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?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
LGTM!
Merging this to continue making progress on geometrict TWAP. The next items are tracked in: #3013 |
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
computeArithmeticTwap
method to be more general and named itcomputeTwap
computedGeometricTwap
method that is called bycomputeTwap
Testing and Verifying
This change added tests.
Tracked Future Work
Blocking TODOs
Pow()
functioncomputeGeometricTwap
Clean up TODOs
TwapLog
Documentation and Release Note
Unreleased
section inCHANGELOG.md
? no