Skip to content

Commit 04aa1cd

Browse files
committed
Merge pull request #3451 from vegaprotocol/hotfix/fix-fees-calculation-lp-shares
remove float from the core shares calculation for LP + use shopsring decimal
1 parent 92339d9 commit 04aa1cd

10 files changed

Lines changed: 102 additions & 83 deletions

File tree

execution/equity_shares.go

Lines changed: 29 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,46 +2,50 @@ package execution
22

33
import (
44
"fmt"
5+
"math/big"
6+
7+
"github.com/shopspring/decimal"
58
)
69

710
// lp holds LiquidityProvider stake and avg values
811
type lp struct {
9-
stake float64
10-
share float64
11-
avg float64
12+
stake decimal.Decimal
13+
share decimal.Decimal
14+
avg decimal.Decimal
1215
}
1316

1417
// EquityShares module controls the Equity sharing algorithm described on the spec:
1518
// https://github.com/vegaprotocol/product/blob/02af55e048a92a204e9ee7b7ae6b4475a198c7ff/specs/0042-setting-fees-and-rewarding-lps.md#calculating-liquidity-provider-equity-like-share
1619
type EquityShares struct {
1720
// mvp is the MarketValueProxy
18-
mvp float64
21+
mvp decimal.Decimal
1922

2023
// lps is a map of party id to lp (LiquidityProviders)
2124
lps map[string]*lp
2225
}
2326

24-
func NewEquityShares(mvp float64) *EquityShares {
27+
func NewEquityShares(mvp decimal.Decimal) *EquityShares {
2528
return &EquityShares{
2629
mvp: mvp,
2730
lps: map[string]*lp{},
2831
}
2932
}
3033

31-
func (es *EquityShares) WithMVP(mvp float64) *EquityShares {
34+
func (es *EquityShares) WithMVP(mvp decimal.Decimal) *EquityShares {
3235
es.mvp = mvp
3336
return es
3437
}
3538

3639
// SetPartyStake sets LP values for a given party.
37-
func (es *EquityShares) SetPartyStake(id string, newStake float64) {
40+
func (es *EquityShares) SetPartyStake(id string, newStakeU64 uint64) {
41+
newStake := decimal.NewFromBigInt(new(big.Int).SetUint64(newStakeU64), 0)
3842
v, found := es.lps[id]
3943
// first time we set the newStake and mvp as avg.
4044
if !found {
41-
if newStake > 0 {
45+
if newStake.GreaterThan(decimal.Zero) {
4246
// if marketValueProxy == 0
4347
// we assume mvp will be our stake?
44-
if es.mvp == 0 {
48+
if es.mvp.Equal(decimal.Zero) {
4549
es.mvp = newStake
4650
}
4751
es.lps[id] = &lp{stake: newStake, avg: es.mvp}
@@ -51,33 +55,34 @@ func (es *EquityShares) SetPartyStake(id string, newStake float64) {
5155
return
5256
}
5357

54-
if newStake <= 0 {
58+
if newStake.Equal(decimal.Zero) {
5559
// We are removing an existing stake
5660
delete(es.lps, id)
5761
return
5862
}
5963

60-
if newStake <= v.stake {
64+
if newStake.LessThanOrEqual(v.stake) {
6165
v.stake = newStake
6266
return
6367
}
6468

6569
// delta will allways be > 0 at this point
66-
delta := newStake - v.stake
70+
delta := newStake.Sub(v.stake)
6771
eq := es.mustEquity(id)
68-
v.avg = ((eq * v.avg) + (delta * es.mvp)) / (eq + v.stake)
72+
// v.avg = ((eq * v.avg) + (delta * es.mvp)) / (eq + v.stake)
73+
v.avg = (eq.Mul(v.avg).Add(delta.Mul(es.mvp))).Div(eq.Add(v.stake))
6974
v.stake = newStake
7075
}
7176

7277
// AvgEntryValuation returns the Average Entry Valuation for a given party.
73-
func (es *EquityShares) AvgEntryValuation(id string) float64 {
78+
func (es *EquityShares) AvgEntryValuation(id string) decimal.Decimal {
7479
if v, ok := es.lps[id]; ok {
7580
return v.avg
7681
}
77-
return 0
82+
return decimal.Zero
7883
}
7984

80-
func (es *EquityShares) mustEquity(party string) float64 {
85+
func (es *EquityShares) mustEquity(party string) decimal.Decimal {
8186
eq, err := es.equity(party)
8287
if err != nil {
8388
panic(err)
@@ -90,20 +95,20 @@ func (es *EquityShares) mustEquity(party string) float64 {
9095
// given a party id (i).
9196
//
9297
// Returns an error if the party has no stake.
93-
func (es *EquityShares) equity(id string) (float64, error) {
98+
func (es *EquityShares) equity(id string) (decimal.Decimal, error) {
9499
if v, ok := es.lps[id]; ok {
95-
return (v.stake * es.mvp) / v.avg, nil
100+
return (v.stake.Mul(es.mvp)).Div(v.avg), nil
96101
}
97102

98-
return 0, fmt.Errorf("party %s has no stake", id)
103+
return decimal.Zero, fmt.Errorf("party %s has no stake", id)
99104
}
100105

101106
// Shares returns the ratio of equity for a given party
102-
func (es *EquityShares) Shares(undeployed map[string]struct{}) map[string]float64 {
107+
func (es *EquityShares) Shares(undeployed map[string]struct{}) map[string]decimal.Decimal {
103108
// Calculate the equity for each party and the totalEquity (the sum of all
104109
// the equities)
105-
var totalEquity float64
106-
shares := map[string]float64{}
110+
var totalEquity decimal.Decimal
111+
shares := map[string]decimal.Decimal{}
107112
for id := range es.lps {
108113
// if the party is not one of the deployed parties,
109114
// we just skip
@@ -118,11 +123,11 @@ func (es *EquityShares) Shares(undeployed map[string]struct{}) map[string]float6
118123
panic(err)
119124
}
120125
shares[id] = eq
121-
totalEquity += eq
126+
totalEquity = totalEquity.Add(eq)
122127
}
123128

124129
for id, eq := range shares {
125-
eqshare := eq / totalEquity
130+
eqshare := eq.Div(totalEquity)
126131
shares[id] = eqshare
127132
es.lps[id].share = eqshare
128133
}

execution/equity_shares_test.go

Lines changed: 25 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"testing"
66
"time"
77

8+
"github.com/shopspring/decimal"
89
"github.com/stretchr/testify/assert"
910
"github.com/stretchr/testify/require"
1011

@@ -22,43 +23,43 @@ func TestEquityShares(t *testing.T) {
2223
// TestEquitySharesAverageEntryValuation is based on the spec example:
2324
// https://github.com/vegaprotocol/product/blob/02af55e048a92a204e9ee7b7ae6b4475a198c7ff/specs/0042-setting-fees-and-rewarding-lps.md#calculating-liquidity-provider-equity-like-share
2425
func testAverageEntryValuation(t *testing.T) {
25-
es := execution.NewEquityShares(100)
26+
es := execution.NewEquityShares(decimal.NewFromFloat(100.))
2627

27-
es.SetPartyStake("LP1", 100)
28-
require.EqualValues(t, 100, es.AvgEntryValuation("LP1"))
28+
es.SetPartyStake("LP1", uint64(100))
29+
require.EqualValues(t, decimal.NewFromFloat(100.), es.AvgEntryValuation("LP1"))
2930

30-
es.SetPartyStake("LP1", 200)
31-
require.EqualValues(t, 100, es.AvgEntryValuation("LP1"))
31+
es.SetPartyStake("LP1", uint64(200))
32+
require.True(t, decimal.NewFromFloat(100.).Equal(es.AvgEntryValuation("LP1")))
3233

33-
es.WithMVP(200).SetPartyStake("LP2", 200)
34-
require.EqualValues(t, 200, es.AvgEntryValuation("LP2"))
35-
require.EqualValues(t, 100, es.AvgEntryValuation("LP1"))
34+
es.WithMVP(decimal.NewFromFloat(200.)).SetPartyStake("LP2", uint64(200))
35+
require.True(t, decimal.NewFromFloat(200.).Equal(es.AvgEntryValuation("LP2")))
36+
require.True(t, decimal.NewFromFloat(100.).Equal(es.AvgEntryValuation("LP1")))
3637

37-
es.WithMVP(400).SetPartyStake("LP1", 300)
38-
require.EqualValues(t, 120, es.AvgEntryValuation("LP1"))
38+
es.WithMVP(decimal.NewFromFloat(400.)).SetPartyStake("LP1", uint64(300))
39+
require.True(t, decimal.NewFromFloat(120.).Equal(es.AvgEntryValuation("LP1")))
3940

40-
es.SetPartyStake("LP1", 1)
41-
require.EqualValues(t, 120, es.AvgEntryValuation("LP1"))
42-
require.EqualValues(t, 200, es.AvgEntryValuation("LP2"))
41+
es.SetPartyStake("LP1", uint64(1))
42+
require.True(t, decimal.NewFromFloat(120.).Equal(es.AvgEntryValuation("LP1")))
43+
require.True(t, decimal.NewFromFloat(200.).Equal(es.AvgEntryValuation("LP2")))
4344
}
4445

4546
func testShares(t *testing.T) {
4647
var (
47-
oneSixth = 1.0 / 6
48-
oneThird = 1.0 / 3
49-
oneFourth = 1.0 / 4
50-
threeFourth = 3.0 / 4
51-
twoThirds = 2.0 / 3
52-
half = 1.0 / 2
48+
oneSixth = decimal.NewFromFloat(1.0).Div(decimal.NewFromFloat(6.))
49+
oneThird = decimal.NewFromFloat(1.0).Div(decimal.NewFromFloat(3.))
50+
oneFourth = decimal.NewFromFloat(1.0).Div(decimal.NewFromFloat(4.))
51+
threeFourth = decimal.NewFromFloat(3.0).Div(decimal.NewFromFloat(4.))
52+
twoThirds = decimal.NewFromFloat(2.0).Div(decimal.NewFromFloat(3.))
53+
half = decimal.NewFromFloat(1.0).Div(decimal.NewFromFloat(2.))
5354
)
5455

55-
es := execution.NewEquityShares(100)
56+
es := execution.NewEquityShares(decimal.NewFromFloat(100.))
5657

5758
// Set LP1
5859
es.SetPartyStake("LP1", 100)
5960
t.Run("LP1", func(t *testing.T) {
6061
s := es.Shares(map[string]struct{}{})
61-
assert.Equal(t, 1.0, s["LP1"])
62+
assert.True(t, decimal.NewFromFloat(1.0).Equal(s["LP1"]))
6263
})
6364

6465
// Set LP2
@@ -69,7 +70,7 @@ func testShares(t *testing.T) {
6970

7071
assert.Equal(t, oneThird, lp1)
7172
assert.Equal(t, twoThirds, lp2)
72-
assert.Equal(t, 1.0, lp1+lp2)
73+
assert.True(t, decimal.NewFromFloat(1.0).Equal(lp1.Add(lp2)))
7374
})
7475

7576
// Set LP3
@@ -82,7 +83,7 @@ func testShares(t *testing.T) {
8283
assert.Equal(t, oneSixth, lp1)
8384
assert.Equal(t, oneThird, lp2)
8485
assert.Equal(t, half, lp3)
85-
assert.Equal(t, 1.0, lp1+lp2+lp3)
86+
assert.True(t, decimal.NewFromFloat(1.0).Equal(lp1.Add(lp2).Add(lp3)))
8687
})
8788

8889
// LP2 is undeployed
@@ -97,7 +98,7 @@ func testShares(t *testing.T) {
9798
assert.Equal(t, oneFourth, lp1)
9899
// assert.Equal(t, oneThird, lp2)
99100
assert.Equal(t, threeFourth, lp3)
100-
assert.Equal(t, 1.0, lp1+lp3)
101+
assert.True(t, decimal.NewFromFloat(1.0).Equal(lp1.Add(lp3)))
101102
})
102103
}
103104

execution/fees.go

Lines changed: 9 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -2,8 +2,10 @@ package execution
22

33
import (
44
"errors"
5-
"math"
5+
"math/big"
66
"time"
7+
8+
"github.com/shopspring/decimal"
79
)
810

911
type FeeSplitter struct {
@@ -51,13 +53,15 @@ func (fs *FeeSplitter) activeWindowLength(mvw time.Duration) time.Duration {
5153

5254
// MarketValueProxy returns the market value proxy according to the spec:
5355
// https://github.com/vegaprotocol/product/blob/master/specs/0042-setting-fees-and-rewarding-lps.md
54-
func (fs *FeeSplitter) MarketValueProxy(mvwl time.Duration, totalStake float64) float64 {
56+
func (fs *FeeSplitter) MarketValueProxy(mvwl time.Duration, totalStakeU64 uint64) decimal.Decimal {
57+
totalStake := decimal.NewFromBigInt(new(big.Int).SetUint64(totalStakeU64), 0)
5558
// t is the distance between
5659
awl := fs.activeWindowLength(mvwl)
5760
if awl > 0 {
58-
factor := mvwl.Seconds() / awl.Seconds()
59-
tv := fs.tradeValue
60-
return math.Max(totalStake, factor*float64(tv))
61+
factor := decimal.NewFromFloat(mvwl.Seconds()).Div(
62+
decimal.NewFromFloat(awl.Seconds()))
63+
tv := decimal.NewFromBigInt(new(big.Int).SetUint64(fs.tradeValue), 0)
64+
return decimal.Max(totalStake, factor.Mul(tv))
6165
}
6266
return totalStake
6367
}

execution/fees_test.go

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -4,39 +4,40 @@ import (
44
"testing"
55
"time"
66

7+
"github.com/shopspring/decimal"
78
"github.com/stretchr/testify/require"
89
)
910

1011
func TestFeeSplitter(t *testing.T) {
1112
var (
12-
totalStake float64 = 100
13-
timeWindowStart = time.Now()
14-
marketValueWindowLength = 1 * time.Minute
13+
totalStake uint64 = 100
14+
timeWindowStart = time.Now()
15+
marketValueWindowLength = 1 * time.Minute
1516
)
1617

1718
tests := []struct {
1819
currentTime time.Time
1920
tradedValue uint64
20-
expectedValueProxy float64
21+
expectedValueProxy decimal.Decimal
2122
}{
2223
{
2324
currentTime: timeWindowStart,
24-
expectedValueProxy: 100,
25+
expectedValueProxy: decimal.NewFromFloat(100.),
2526
},
2627
{
2728
tradedValue: 10,
2829
currentTime: timeWindowStart.Add(10 * time.Second),
29-
expectedValueProxy: 100,
30+
expectedValueProxy: decimal.NewFromFloat(100.),
3031
},
3132
{
3233
tradedValue: 100,
3334
currentTime: timeWindowStart.Add(30 * time.Second),
34-
expectedValueProxy: 200,
35+
expectedValueProxy: decimal.NewFromFloat(200.),
3536
},
3637
{
3738
tradedValue: 300,
3839
currentTime: timeWindowStart.Add(3 * marketValueWindowLength),
39-
expectedValueProxy: 300,
40+
expectedValueProxy: decimal.NewFromFloat(300.),
4041
},
4142
}
4243

@@ -50,7 +51,7 @@ func TestFeeSplitter(t *testing.T) {
5051
fs.AddTradeValue(test.tradedValue)
5152

5253
got := fs.MarketValueProxy(marketValueWindowLength, totalStake)
53-
require.Equal(t, test.expectedValueProxy, got)
54+
require.True(t, test.expectedValueProxy.Equal(got))
5455
})
5556
}
5657
}

execution/liquidity_provision.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -174,7 +174,7 @@ func (m *Market) finalizeLiquidityProvisionAmendmentAuction(
174174
// now we can update the liquidity fee to be taken
175175
m.updateLiquidityFee(ctx)
176176
// now we can setup our party stake to calculate equities
177-
m.equityShares.SetPartyStake(party, float64(sub.CommitmentAmount))
177+
m.equityShares.SetPartyStake(party, sub.CommitmentAmount)
178178
// force update of shares so they are updated for all
179179
_ = m.equityShares.Shares(m.liquidity.GetInactiveParties())
180180

@@ -272,7 +272,7 @@ func (m *Market) finalizeLiquidityProvisionAmendmentContinuous(
272272
// now we can update the liquidity fee to be taken
273273
m.updateLiquidityFee(ctx)
274274
// now we can setup our party stake to calculate equities
275-
m.equityShares.SetPartyStake(party, float64(sub.CommitmentAmount))
275+
m.equityShares.SetPartyStake(party, sub.CommitmentAmount)
276276
// force update of shares so they are updated for all
277277
_ = m.equityShares.Shares(m.liquidity.GetInactiveParties())
278278

0 commit comments

Comments
 (0)