Skip to content

Commit 76e61cd

Browse files
authored
Add market-based fee discount and user staking discount (#824)
1 parent 0d4d279 commit 76e61cd

37 files changed

+423
-36
lines changed

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ allprojects {
5353
}
5454

5555
group = "exchange.dydx.abacus"
56-
version = "1.14.12"
56+
version = "1.14.13"
5757

5858
repositories {
5959
google()

src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInput/TradeInputCalculator.kt

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import exchange.dydx.abacus.output.input.TradeInputSize
1111
import exchange.dydx.abacus.protocols.ParserProtocol
1212
import exchange.dydx.abacus.state.InternalAccountState
1313
import exchange.dydx.abacus.state.InternalConfigsState
14+
import exchange.dydx.abacus.state.InternalMarketFeeDiscountState
1415
import exchange.dydx.abacus.state.InternalMarketState
1516
import exchange.dydx.abacus.state.InternalMarketSummaryState
1617
import exchange.dydx.abacus.state.InternalRewardsParamsState
@@ -123,6 +124,7 @@ internal class TradeInputCalculator(
123124
market = markets[trade.marketId],
124125
rewardsParams = rewardsParams,
125126
feeTiers = configs.feeTiers,
127+
allMarketFeeDiscounts = configs.allMarketFeeDiscounts,
126128
)
127129

128130
accountTransformer.applyTradeToAccount(
@@ -179,6 +181,7 @@ internal class TradeInputCalculator(
179181
market: InternalMarketState?,
180182
rewardsParams: InternalRewardsParamsState?,
181183
feeTiers: List<FeeTier>?,
184+
allMarketFeeDiscounts: Map<String, InternalMarketFeeDiscountState>?,
182185
): InternalTradeInputState {
183186
val marketId = market?.perpetualMarket?.id
184187
val position = if (marketId != null) {
@@ -187,6 +190,13 @@ internal class TradeInputCalculator(
187190
null
188191
}
189192

193+
val clobPairId = market?.perpetualMarket?.clobPairId
194+
val marketFeeDiscountState = if (clobPairId != null) {
195+
allMarketFeeDiscounts?.get(clobPairId)
196+
} else {
197+
null
198+
}
199+
190200
optionsCalculator.calculate(
191201
trade = trade,
192202
position = position,
@@ -202,6 +212,7 @@ internal class TradeInputCalculator(
202212
market = market,
203213
rewardsParams = rewardsParams,
204214
feeTiers = feeTiers,
215+
marketFeeDiscountState = marketFeeDiscountState,
205216
)
206217

207218
return trade

src/commonMain/kotlin/exchange.dydx.abacus/calculator/TradeInput/TradeInputSummaryCalculator.kt

Lines changed: 70 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import exchange.dydx.abacus.output.FeeTier
1313
import exchange.dydx.abacus.output.input.MarginMode
1414
import exchange.dydx.abacus.output.input.OrderSide
1515
import exchange.dydx.abacus.output.input.OrderType
16+
import exchange.dydx.abacus.state.InternalMarketFeeDiscountState
1617
import exchange.dydx.abacus.state.InternalMarketState
1718
import exchange.dydx.abacus.state.InternalRewardsParamsState
1819
import exchange.dydx.abacus.state.InternalSubaccountState
@@ -22,6 +23,7 @@ import exchange.dydx.abacus.state.InternalUserState
2223
import exchange.dydx.abacus.utils.Numeric
2324
import exchange.dydx.abacus.utils.QUANTUM_MULTIPLIER
2425
import exchange.dydx.abacus.utils.Rounder
26+
import kotlinx.datetime.Clock
2527
import kotlin.math.abs
2628
import kotlin.math.pow
2729

@@ -33,25 +35,43 @@ internal class TradeInputSummaryCalculator {
3335
market: InternalMarketState?,
3436
rewardsParams: InternalRewardsParamsState?,
3537
feeTiers: List<FeeTier>?,
38+
marketFeeDiscountState: InternalMarketFeeDiscountState?,
3639
): InternalTradeInputState {
3740
trade.summary = when (trade.type) {
3841
OrderType.Market -> {
39-
calculateForMarketOrder(trade, subaccount, user, market, rewardsParams, feeTiers)
42+
calculateForMarketOrder(
43+
trade = trade,
44+
subaccount = subaccount,
45+
user = user,
46+
market = market,
47+
rewardsParams = rewardsParams,
48+
feeTiers = feeTiers,
49+
marketFeeDiscountState = marketFeeDiscountState,
50+
)
4051
}
4152

4253
OrderType.StopMarket, OrderType.TakeProfitMarket -> {
4354
calculateForStopTakeProfitMarketOrder(
44-
trade,
45-
subaccount,
46-
user,
47-
market,
48-
rewardsParams,
49-
feeTiers,
55+
trade = trade,
56+
subaccount = subaccount,
57+
user = user,
58+
market = market,
59+
rewardsParams = rewardsParams,
60+
feeTiers = feeTiers,
61+
marketFeeDiscountState = marketFeeDiscountState,
5062
)
5163
}
5264

5365
OrderType.Limit, OrderType.StopLimit, OrderType.TakeProfitLimit -> {
54-
calculateForLimitOrder(trade, subaccount, user, market, rewardsParams, feeTiers)
66+
calculateForLimitOrder(
67+
trade = trade,
68+
subaccount = subaccount,
69+
user = user,
70+
market = market,
71+
rewardsParams = rewardsParams,
72+
feeTiers = feeTiers,
73+
marketFeeDiscountState = marketFeeDiscountState,
74+
)
5575
}
5676

5777
else -> null
@@ -67,6 +87,7 @@ internal class TradeInputSummaryCalculator {
6787
market: InternalMarketState?,
6888
rewardsParams: InternalRewardsParamsState?,
6989
feeTiers: List<FeeTier>?,
90+
marketFeeDiscountState: InternalMarketFeeDiscountState?,
7091
): InternalTradeInputSummary? {
7192
val marketOrder = trade.marketOrder ?: return null
7293
val multiplier = getMultiplier(trade)
@@ -90,8 +111,10 @@ internal class TradeInputSummaryCalculator {
90111
val size = marketOrder.size
91112
val usdcSize =
92113
if (price != null && size != null) (price * size) else null
93-
val fee =
114+
val baseFee =
94115
if (usdcSize != null && feeRate != null) (usdcSize * feeRate) else null
116+
val fee = calculateFeeDiscount(baseFee, user, marketFeeDiscountState)
117+
95118
val total =
96119
if (usdcSize != null) {
97120
usdcSize * multiplier + (fee ?: Numeric.double.ZERO) * Numeric.double.NEGATIVE
@@ -148,6 +171,7 @@ internal class TradeInputSummaryCalculator {
148171
market: InternalMarketState?,
149172
rewardsParams: InternalRewardsParamsState?,
150173
feeTiers: List<FeeTier>?,
174+
marketFeeDiscountState: InternalMarketFeeDiscountState?,
151175
): InternalTradeInputSummary? {
152176
val marketOrder = trade.marketOrder ?: return null
153177
val multiplier = getMultiplier(trade)
@@ -210,8 +234,10 @@ internal class TradeInputSummaryCalculator {
210234
val size = marketOrder.size
211235
val usdcSize =
212236
if (price != null && size != null) (price * size) else null
213-
val fee =
237+
val baseFee =
214238
if (usdcSize != null && feeRate != null) (usdcSize * feeRate) else null
239+
val fee = calculateFeeDiscount(baseFee, user, marketFeeDiscountState)
240+
215241
val total =
216242
if (usdcSize != null) {
217243
usdcSize * multiplier + (fee ?: Numeric.double.ZERO) * Numeric.double.NEGATIVE
@@ -250,6 +276,7 @@ internal class TradeInputSummaryCalculator {
250276
market: InternalMarketState?,
251277
rewardsParams: InternalRewardsParamsState?,
252278
feeTiers: List<FeeTier>?,
279+
marketFeeDiscountState: InternalMarketFeeDiscountState?,
253280
): InternalTradeInputSummary {
254281
val multiplier = getMultiplier(trade)
255282

@@ -264,8 +291,10 @@ internal class TradeInputSummaryCalculator {
264291
val size = trade.size?.size
265292
val usdcSize =
266293
if (price != null && size != null) (price * size) else null
267-
val fee =
294+
val baseFee =
268295
if (usdcSize != null && feeRate != null) (usdcSize * feeRate) else null
296+
val fee = calculateFeeDiscount(baseFee, user, marketFeeDiscountState)
297+
269298
val total =
270299
if (usdcSize != null) {
271300
usdcSize * multiplier + (fee ?: Numeric.double.ZERO) * Numeric.double.NEGATIVE
@@ -486,4 +515,34 @@ internal class TradeInputSummaryCalculator {
486515
val postOrderLeverage = position.calculated[CalculationPeriod.post]?.leverage
487516
return postOrderLeverage ?: currentLeverage
488517
}
518+
519+
private fun calculateFeeDiscount(
520+
fee: Double?,
521+
user: InternalUserState?,
522+
marketFeeDiscount: InternalMarketFeeDiscountState?,
523+
): Double? {
524+
if (fee == null) return fee
525+
526+
var finalFee = fee
527+
if (marketFeeDiscount != null && marketFeeDiscount.isApplicable && marketFeeDiscount.chargePercent != null) {
528+
finalFee *= marketFeeDiscount.chargePercent
529+
}
530+
val userStakingTierDiscountPercent = user?.userStakingTierDiscountPercent
531+
if (userStakingTierDiscountPercent != null) {
532+
finalFee *= (1 - userStakingTierDiscountPercent)
533+
}
534+
return finalFee
535+
}
489536
}
537+
538+
private val InternalMarketFeeDiscountState.isApplicable: Boolean
539+
get() {
540+
val now = Clock.System.now()
541+
if (this.startTime != null && this.startTime > now) {
542+
return false
543+
}
544+
if (this.endTime != null && this.endTime < now) {
545+
return false
546+
}
547+
return true
548+
}

src/commonMain/kotlin/exchange.dydx.abacus/output/Market.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -205,6 +205,7 @@ the object may contain empty fields until both payloads are received and process
205205
@Serializable
206206
data class PerpetualMarket(
207207
val id: String,
208+
val clobPairId: String?,
208209
val assetId: String,
209210
val market: String?,
210211
val displayId: String?,
@@ -264,6 +265,7 @@ data class PerpetualMarketSummary(
264265
} else {
265266
val market = PerpetualMarket(
266267
id = marketId,
268+
clobPairId = null,
267269
assetId = asset.id,
268270
market = asset.name,
269271
displayId = asset.displayableAssetId,

src/commonMain/kotlin/exchange.dydx.abacus/processor/configs/ConfigsProcessor.kt

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ import exchange.dydx.abacus.protocols.LocalizerProtocol
77
import exchange.dydx.abacus.protocols.ParserProtocol
88
import exchange.dydx.abacus.state.InternalConfigsState
99
import indexer.models.chain.OnChainEquityTiersResponse
10+
import indexer.models.chain.OnChainFeeDiscountsResponse
1011
import indexer.models.chain.OnChainFeeTiersResponse
1112
import indexer.models.chain.OnChainWithdrawalAndTransferGatingStatusResponse
1213
import indexer.models.chain.OnChainWithdrawalCapacityResponse
@@ -22,6 +23,11 @@ internal interface ConfigsProcessorProtocol : BaseProcessorProtocol {
2223
payload: OnChainFeeTiersResponse?
2324
): InternalConfigsState
2425

26+
fun processOnChainFeeDiscounts(
27+
existing: InternalConfigsState,
28+
payload: OnChainFeeDiscountsResponse?
29+
): InternalConfigsState
30+
2531
fun processWithdrawalGating(
2632
existing: InternalConfigsState,
2733
payload: OnChainWithdrawalAndTransferGatingStatusResponse?
@@ -57,6 +63,14 @@ internal class ConfigsProcessor(
5763
return existing
5864
}
5965

66+
override fun processOnChainFeeDiscounts(
67+
existing: InternalConfigsState,
68+
payload: OnChainFeeDiscountsResponse?
69+
): InternalConfigsState {
70+
existing.allMarketFeeDiscounts = feeTiersProcessor.processFeeDiscounts(payload?.params)
71+
return existing
72+
}
73+
6074
override fun processWithdrawalGating(
6175
existing: InternalConfigsState,
6276
payload: OnChainWithdrawalAndTransferGatingStatusResponse?

src/commonMain/kotlin/exchange.dydx.abacus/processor/configs/FeeTiersProcessor.kt

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,19 @@ import exchange.dydx.abacus.output.FeeTierResources
55
import exchange.dydx.abacus.processor.base.BaseProcessor
66
import exchange.dydx.abacus.protocols.LocalizerProtocol
77
import exchange.dydx.abacus.protocols.ParserProtocol
8+
import exchange.dydx.abacus.state.InternalMarketFeeDiscountState
89
import exchange.dydx.abacus.utils.QUANTUM_MULTIPLIER
10+
import indexer.models.chain.OnChainFeeDiscountsParams
911
import indexer.models.chain.OnChainFeeTier
1012

1113
internal interface FeeTiersProcessorProtocol {
1214
fun process(
1315
payload: List<OnChainFeeTier>?
1416
): List<FeeTier>?
17+
18+
fun processFeeDiscounts(
19+
payload: List<OnChainFeeDiscountsParams>?
20+
): Map<String, InternalMarketFeeDiscountState>?
1521
}
1622

1723
internal class FeeTiersProcessor(
@@ -56,4 +62,19 @@ internal class FeeTiersProcessor(
5662
}
5763
}
5864
}
65+
66+
override fun processFeeDiscounts(
67+
payload: List<OnChainFeeDiscountsParams>?
68+
): Map<String, InternalMarketFeeDiscountState>? {
69+
val marketFeeDiscounts = mutableMapOf<String, InternalMarketFeeDiscountState>()
70+
for (feeDiscount in payload ?: emptyList()) {
71+
val feeDiscountState = InternalMarketFeeDiscountState(
72+
startTime = feeDiscount.startTime,
73+
endTime = feeDiscount.endTime,
74+
chargePercent = feeDiscount.chargePpm?.div(QUANTUM_MULTIPLIER),
75+
)
76+
marketFeeDiscounts[feeDiscount.clobPairId.toString()] = feeDiscountState
77+
}
78+
return marketFeeDiscounts
79+
}
5980
}

src/commonMain/kotlin/exchange.dydx.abacus/processor/markets/MarketProcessor.kt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,7 @@ internal class MarketProcessor(
102102
try {
103103
val newValue = PerpetualMarket(
104104
id = name,
105+
clobPairId = payload.clobPairId,
105106
assetId = MarketId.getAssetId(name) ?: parseException(payload),
106107
oraclePrice = oraclePrice,
107108
market = name,

src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/WalletProcessor.kt

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import indexer.models.chain.OnChainDelegationResponse
2121
import indexer.models.chain.OnChainStakingRewardsResponse
2222
import indexer.models.chain.OnChainUnbondingResponse
2323
import indexer.models.chain.OnChainUserFeeTierResponse
24+
import indexer.models.chain.OnChainUserStakingTierResponse
2425
import indexer.models.chain.OnChainUserStatsResponse
2526
import indexer.models.configs.ConfigsLaunchIncentivePoints
2627

@@ -169,6 +170,17 @@ internal class WalletProcessor(
169170
return existing
170171
}
171172

173+
internal fun processOnChainUserStakingTier(
174+
existing: InternalWalletState,
175+
payload: OnChainUserStakingTierResponse?,
176+
): InternalWalletState {
177+
existing.user = userProcessor.processOnChainUserStakingTier(
178+
existing = existing.user,
179+
payload = payload,
180+
)
181+
return existing
182+
}
183+
172184
internal fun processHistoricalPnls(
173185
existing: InternalWalletState,
174186
payload: List<IndexerPnlTicksResponseObject>?,

src/commonMain/kotlin/exchange.dydx.abacus/processor/wallet/user/UserProcessor.kt

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import exchange.dydx.abacus.protocols.ParserProtocol
55
import exchange.dydx.abacus.state.InternalUserState
66
import exchange.dydx.abacus.utils.QUANTUM_MULTIPLIER
77
import indexer.models.chain.OnChainUserFeeTier
8+
import indexer.models.chain.OnChainUserStakingTierResponse
89
import indexer.models.chain.OnChainUserStatsResponse
910

1011
internal interface UserProcessorProtocol {
@@ -17,6 +18,11 @@ internal interface UserProcessorProtocol {
1718
existing: InternalUserState?,
1819
payload: OnChainUserStatsResponse?,
1920
): InternalUserState
21+
22+
fun processOnChainUserStakingTier(
23+
existing: InternalUserState?,
24+
payload: OnChainUserStakingTierResponse?,
25+
): InternalUserState
2026
}
2127

2228
internal class UserProcessor(
@@ -54,4 +60,16 @@ internal class UserProcessor(
5460
}
5561
return internalState
5662
}
63+
64+
override fun processOnChainUserStakingTier(
65+
existing: InternalUserState?,
66+
payload: OnChainUserStakingTierResponse?,
67+
): InternalUserState {
68+
val internalState = existing ?: InternalUserState()
69+
val discountPpm = parser.asDouble(payload?.discountPpm)
70+
if (discountPpm != null) {
71+
internalState.userStakingTierDiscountPercent = discountPpm / QUANTUM_MULTIPLIER
72+
}
73+
return internalState
74+
}
5775
}

src/commonMain/kotlin/exchange.dydx.abacus/protocols/PublicProtocols.kt

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,7 +123,9 @@ enum class QueryType(val rawValue: String) {
123123
Height("getHeight"),
124124
EquityTiers("getEquityTiers"),
125125
FeeTiers("getFeeTiers"),
126+
FeeDiscounts("getAllPerpMarketFeeDiscounts"),
126127
UserFeeTier("getUserFeeTier"),
128+
UserStakingTier("getUserStakingTier"),
127129
UserStats("getUserStats"),
128130
OptimalNode("getOptimalNode"),
129131
OptimalIndexer("getOptimalIndexer"),

0 commit comments

Comments
 (0)