Skip to content

Commit 92f556a

Browse files
StefChatzPatricie29linkielinkcmende
authored
feat: integrate keeper fee calculation into PerpsModule and Summary c… (#1701)
Co-authored-by: Patricie <99055449+Patricie29@users.noreply.github.com> Co-authored-by: Linkie Link <linkielink.dev@gmail.com> Co-authored-by: Patricie29 <patricie@marsprotocol.foundation> Co-authored-by: Carlos Mendes <carlosmendessssq@gmail.com>
1 parent 59cd7b9 commit 92f556a

File tree

11 files changed

+222
-24
lines changed

11 files changed

+222
-24
lines changed

public/tradingview.css

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -94,7 +94,7 @@ div[data-name="object-tree-dialog"] * ,
9494
div[data-name="series-properties-dialog"],
9595
/* Checkbox */
9696
[class^='close-'],
97-
/* Close buton series popup */
97+
/* Close button series popup */
9898
[class^='close-']:hover {
9999
background: var(--tv-menu-background) !important;
100100
outline: white !important;
@@ -124,7 +124,7 @@ div[data-name="series-properties-dialog"],
124124
background-color: var(--tv-form-background) !important;
125125
}
126126

127-
/* Devider Lines */
127+
/* Divider Lines */
128128
[class^='content-'],
129129
[class^='container-'],
130130
[class^='footer-'] {
@@ -145,7 +145,7 @@ div[data-role="dialog-content"] div[data-role="list-item"] span,
145145
div[data-name="series-properties-dialog"],
146146
/* Series left sidetabs */
147147
[class^='tab-'],
148-
/* Close buton series popup */
148+
/* Close button series popup */
149149
[class^='close-'] {
150150
color: var(--tv-menu-text) !important;
151151
}
@@ -164,7 +164,7 @@ div[data-role="dialog-content"] div[data-role="list-item"]:hover span,
164164
[class^='wrap-'] [class^='dropdown-']:hover span,
165165
[class^='wrap-'] [class^='dropdown-']:hover [class^='arrowIcon-'],
166166
[class^='menuItem-']:hover,
167-
/* Close buton series popup */
167+
/* Close button series popup */
168168
[class^='close-']:hover {
169169
cursor: pointer !important;
170170
color: var(--tv-menu-text-hover) !important;

public/tradingview_default.css

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -95,7 +95,7 @@ div[data-name="object-tree-dialog"] * ,
9595
div[data-name="series-properties-dialog"],
9696
/* Checkbox */
9797
[class^='close-'],
98-
/* Close buton series popup */
98+
/* Close button series popup */
9999
[class^='close-']:hover {
100100
background: var(--tv-menu-background) !important;
101101
outline: white !important;
@@ -125,7 +125,7 @@ div[data-name="series-properties-dialog"],
125125
background-color: var(--tv-form-background) !important;
126126
}
127127

128-
/* Devider Lines */
128+
/* Divider Lines */
129129
[class^='content-'],
130130
[class^='container-'],
131131
[class^='footer-'] {
@@ -146,7 +146,7 @@ div[data-role="dialog-content"] div[data-role="list-item"] span,
146146
div[data-name="series-properties-dialog"],
147147
/* Series left sidetabs */
148148
[class^='tab-'],
149-
/* Close buton series popup */
149+
/* Close button series popup */
150150
[class^='close-'] {
151151
color: var(--tv-menu-text) !important;
152152
}

src/components/perps/Module/PerpsModule.tsx

Lines changed: 37 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@ import { useUpdatedAccount } from 'hooks/accounts/useUpdatedAccount'
2929
import useAssets from 'hooks/assets/useAssets'
3030
import { useExecutionState } from 'hooks/perps/useExecutionState'
3131
import { useHandleClosing } from 'hooks/perps/useHandleClosing'
32+
import { useKeeperFeeCalculation } from 'hooks/perps/useKeeperFeeCalculation'
3233
import { useLimitPriceInfo } from 'hooks/perps/useLimitPriceInfo'
3334
import { useOpenInterestLeft } from 'hooks/perps/useOpenInterestLeft'
3435
import usePerpsAsset from 'hooks/perps/usePerpsAsset'
@@ -42,6 +43,7 @@ import { useStopPriceInfo } from 'hooks/perps/useStopPriceInfo'
4243
import useTradingFeeAndPrice from 'hooks/perps/useTradingFeeAndPrice'
4344
import useAutoLend from 'hooks/wallet/useAutoLend'
4445
import { BNCoin } from 'types/classes/BNCoin'
46+
import useDebounce from 'hooks/common/useDebounce'
4547

4648
export function PerpsModule() {
4749
// State declarations
@@ -87,7 +89,24 @@ export function PerpsModule() {
8789
isStopOrder,
8890
stopTradeDirection,
8991
)
90-
const { data: tradingFee } = useTradingFeeAndPrice(perpsAsset.denom, amount.plus(previousAmount))
92+
const totalAmount = amount.plus(previousAmount)
93+
const debouncedTotalAmount = useDebounce(totalAmount, 1_000)
94+
// Check if the fee data is stale (calculated for a different amount than what user entered)
95+
const isFeeDataStale = !totalAmount.isEqualTo(debouncedTotalAmount)
96+
97+
const {
98+
data: tradingFee,
99+
isLoading: isTradingFeeLoading,
100+
isValidating: isTradingFeeValidating,
101+
} = useTradingFeeAndPrice(perpsAsset.denom, totalAmount)
102+
103+
// Calculate keeper fees for account simulation
104+
const { keeperFeeFromLends, keeperFeeFromBorrows } = useKeeperFeeCalculation({
105+
orderType: selectedOrderType,
106+
conditionalTriggers,
107+
tradingFee: tradingFee?.fee,
108+
enabled: true,
109+
})
91110

92111
// Custom price info hooks
93112
const limitPriceInfo = useLimitPriceInfo(limitPrice, perpsAsset, tradeDirection)
@@ -169,6 +188,19 @@ export function PerpsModule() {
169188
limitPrice,
170189
})
171190

191+
const totalFeesForSimulation = useMemo(() => {
192+
if (!tradingFee) return null
193+
194+
return {
195+
baseDenom: tradingFee.baseDenom,
196+
price: tradingFee.price,
197+
fee: {
198+
opening: tradingFee.fee.opening.plus(keeperFeeFromLends.amount.negated()),
199+
closing: tradingFee.fee.closing.plus(keeperFeeFromBorrows.amount.negated()),
200+
},
201+
}
202+
}, [tradingFee, keeperFeeFromLends, keeperFeeFromBorrows])
203+
172204
usePositionSimulation({
173205
tradingFee,
174206
perpsVault,
@@ -429,6 +461,10 @@ export function PerpsModule() {
429461
isReduceOnly={isReduceOnly}
430462
validateReduceOnlyOrder={validateReduceOnlyOrder}
431463
conditionalTriggers={conditionalTriggers}
464+
tradingFee={tradingFee?.fee}
465+
keeperFeeFromLends={keeperFeeFromLends}
466+
keeperFeeFromBorrows={keeperFeeFromBorrows}
467+
isTradingFeeLoading={isTradingFeeLoading || isTradingFeeValidating || isFeeDataStale}
432468
/>
433469
</div>
434470
</div>

src/components/perps/Module/Summary.tsx

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -52,6 +52,10 @@ type Props = {
5252
isReduceOnly: boolean
5353
validateReduceOnlyOrder: () => boolean
5454
conditionalTriggers: { sl: string | null; tp: string | null }
55+
tradingFee?: { opening: BigNumber; closing: BigNumber }
56+
keeperFeeFromLends: BNCoin
57+
keeperFeeFromBorrows: BNCoin
58+
isTradingFeeLoading: boolean
5559
}
5660

5761
export default function PerpsSummary(props: Props) {
@@ -66,6 +70,8 @@ export default function PerpsSummary(props: Props) {
6670
baseDenom,
6771
limitPrice,
6872
stopPrice,
73+
keeperFeeFromLends,
74+
keeperFeeFromBorrows,
6975
isReduceOnly,
7076
conditionalTriggers,
7177
validateReduceOnlyOrder,
@@ -120,6 +126,8 @@ export default function PerpsSummary(props: Props) {
120126
stopPrice: isStopOrder ? stopPrice : undefined,
121127
isReduceOnly,
122128
conditionalTriggers,
129+
keeperFeeFromLends,
130+
keeperFeeFromBorrows,
123131
})
124132
return true
125133
}, [
@@ -134,6 +142,8 @@ export default function PerpsSummary(props: Props) {
134142
stopPrice,
135143
isReduceOnly,
136144
conditionalTriggers,
145+
keeperFeeFromLends,
146+
keeperFeeFromBorrows,
137147
submitParentOrderWithChildren,
138148
currentAccount,
139149
feeToken,
@@ -235,7 +245,10 @@ export default function PerpsSummary(props: Props) {
235245
onTxExecuted,
236246
])
237247

238-
const isDisabled = useMemo(() => amount.isZero() || disabled, [amount, disabled])
248+
const isDisabled = useMemo(
249+
() => amount.isZero() || disabled || props.isTradingFeeLoading || !props.tradingFee,
250+
[amount, disabled, props.isTradingFeeLoading, props.tradingFee],
251+
)
239252

240253
const tradingFeeTooltip = useMemo(() => {
241254
const text = 'Trading Fees'

src/dummy/charting_library/charting_library.d.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9567,7 +9567,7 @@ export interface IChartWidgetApi {
95679567
*/
95689568
setPriceToBarRatioLocked(value: boolean, options?: UndoOptions): void
95699569
/**
9570-
* Get an array of the heigh of all panes.
9570+
* Get an array of the height of all panes.
95719571
*
95729572
* **Example**
95739573
* ```javascript
@@ -12054,7 +12054,7 @@ export interface IPineSeries {
1205412054
*/
1205512055
get(n?: number): number
1205612056
/**
12057-
* Set the value for the pine series at the current index interation.
12057+
* Set the value for the pine series at the current index iteration.
1205812058
* @param {number} value - value to be set
1205912059
*/
1206012060
set(value: number): void
@@ -12092,7 +12092,7 @@ export interface IPineSeries {
1209212092
*
1209312093
* @example
1209412094
*
12095-
* Psuedocode of the adopt algorithm:
12095+
* Pseudocode of the adopt algorithm:
1209612096
*
1209712097
* ```
1209812098
* adopt(sourceSeries, destinationSeries, mode) =
Lines changed: 127 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,127 @@
1+
import BigNumber from 'bignumber.js'
2+
import { BN_ZERO } from 'constants/math'
3+
import useCurrentAccount from 'hooks/accounts/useCurrentAccount'
4+
import { useKeeperFee } from 'hooks/perps/useKeeperFee'
5+
import { useMemo } from 'react'
6+
import { BNCoin } from 'types/classes/BNCoin'
7+
import { OrderType } from 'types/enums'
8+
import { byDenom } from 'utils/array'
9+
10+
interface KeeperFeeCalculation {
11+
totalKeeperFeeAmount: BigNumber
12+
keeperFeeFromLends: BNCoin
13+
keeperFeeFromBorrows: BNCoin
14+
}
15+
16+
export function useKeeperFeeCalculation({
17+
orderType,
18+
conditionalTriggers,
19+
tradingFee,
20+
enabled,
21+
}: {
22+
orderType: OrderType
23+
conditionalTriggers: { sl: string | null; tp: string | null }
24+
tradingFee?: { opening: BigNumber; closing: BigNumber }
25+
enabled: boolean
26+
}): KeeperFeeCalculation {
27+
const currentAccount = useCurrentAccount()
28+
const keeperFee = useKeeperFee()
29+
30+
// Extract stable primitive values for dependencies
31+
const keeperFeeDenom = keeperFee.calculateKeeperFee?.denom
32+
const keeperFeeAmount = keeperFee.calculateKeeperFee?.amount.toString()
33+
const tradingFeeOpeningStr = tradingFee?.opening.toString()
34+
const tradingFeeClosingStr = tradingFee?.closing.toString()
35+
36+
return useMemo(() => {
37+
const emptyResult: KeeperFeeCalculation = {
38+
totalKeeperFeeAmount: BN_ZERO,
39+
keeperFeeFromLends: BNCoin.fromDenomAndBigNumber(keeperFee.calculateKeeperFee.denom, BN_ZERO),
40+
keeperFeeFromBorrows: BNCoin.fromDenomAndBigNumber(
41+
keeperFee.calculateKeeperFee.denom,
42+
BN_ZERO,
43+
),
44+
}
45+
46+
if (!enabled || !currentAccount) return emptyResult
47+
48+
const hasTriggers = !!(conditionalTriggers.tp ?? conditionalTriggers.sl)
49+
if (!hasTriggers && orderType === OrderType.MARKET) return emptyResult
50+
51+
const orderKeeperFee = keeperFee.calculateKeeperFee
52+
const numConditionalTriggers =
53+
(conditionalTriggers.tp ? 1 : 0) + (conditionalTriggers.sl ? 1 : 0)
54+
55+
// For limit/stop orders (orderType !== MARKET), the parent order also needs keeper fee
56+
// For market orders, only the conditional triggers need keeper fees
57+
const totalTriggerOrders =
58+
orderType === OrderType.MARKET ? numConditionalTriggers : numConditionalTriggers + 1
59+
60+
let totalKeeperFeeAmount = orderKeeperFee.amount.times(totalTriggerOrders)
61+
62+
// For market orders, the execute_perp_order runs immediately and consumes trading fees
63+
// before the keeper fees are deducted. We need to account for this.
64+
// Limit orders don't have this issue because execute_perp_order is inside the trigger.
65+
if (orderType === OrderType.MARKET && numConditionalTriggers > 0) {
66+
let tradingFeeBuffer = BN_ZERO
67+
68+
if (tradingFee) {
69+
const totalTradingFee = tradingFee.opening.plus(tradingFee.closing)
70+
tradingFeeBuffer = totalTradingFee.times(1.01).integerValue(BigNumber.ROUND_UP)
71+
}
72+
73+
// If trading fee is 0 or very small, use 1% of keeper fees as minimum buffer
74+
// This accounts for trading fees that aren't captured in the API estimate
75+
const minBuffer = orderKeeperFee.amount
76+
.times(numConditionalTriggers)
77+
.times(0.01)
78+
.integerValue(BigNumber.ROUND_UP)
79+
tradingFeeBuffer = BigNumber.max(tradingFeeBuffer, minBuffer)
80+
81+
totalKeeperFeeAmount = totalKeeperFeeAmount.plus(tradingFeeBuffer)
82+
}
83+
84+
let keeperFeeAmountLeft = totalKeeperFeeAmount
85+
86+
const keeperFeeTokenDepositsAmount =
87+
currentAccount.deposits.find(byDenom(orderKeeperFee.denom))?.amount ?? BN_ZERO
88+
89+
keeperFeeAmountLeft = keeperFeeTokenDepositsAmount.isGreaterThan(totalKeeperFeeAmount)
90+
? BN_ZERO
91+
: totalKeeperFeeAmount.minus(keeperFeeTokenDepositsAmount)
92+
93+
const keeperFeeTokenLendsAmount =
94+
currentAccount.lends.find(byDenom(orderKeeperFee.denom))?.amount ?? BN_ZERO
95+
96+
const keeperFeeFromLendsAmount = keeperFeeTokenLendsAmount.isGreaterThan(keeperFeeAmountLeft)
97+
? keeperFeeAmountLeft
98+
: keeperFeeTokenLendsAmount
99+
100+
keeperFeeAmountLeft = keeperFeeAmountLeft.minus(keeperFeeFromLendsAmount)
101+
102+
const keeperFeeFromBorrowAmount = keeperFeeAmountLeft.isZero() ? BN_ZERO : keeperFeeAmountLeft
103+
104+
return {
105+
totalKeeperFeeAmount,
106+
keeperFeeFromLends: BNCoin.fromDenomAndBigNumber(
107+
orderKeeperFee.denom,
108+
keeperFeeFromLendsAmount,
109+
),
110+
keeperFeeFromBorrows: BNCoin.fromDenomAndBigNumber(
111+
orderKeeperFee.denom,
112+
keeperFeeFromBorrowAmount,
113+
),
114+
}
115+
// eslint-disable-next-line react-hooks/exhaustive-deps
116+
}, [
117+
enabled,
118+
currentAccount,
119+
keeperFeeDenom,
120+
keeperFeeAmount,
121+
orderType,
122+
conditionalTriggers.tp,
123+
conditionalTriggers.sl,
124+
tradingFeeOpeningStr,
125+
tradingFeeClosingStr,
126+
])
127+
}

src/hooks/perps/useSubmitParentOrderWithChildren.ts

Lines changed: 11 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,8 @@ export function useSubmitParentOrderWithChildren() {
3030
stopPrice,
3131
isReduceOnly,
3232
conditionalTriggers,
33+
keeperFeeFromLends,
34+
keeperFeeFromBorrows,
3335
}: {
3436
asset: Asset
3537
amount: BigNumber
@@ -40,6 +42,8 @@ export function useSubmitParentOrderWithChildren() {
4042
stopPrice?: BigNumber
4143
isReduceOnly?: boolean
4244
conditionalTriggers: { sl: string | null; tp: string | null }
45+
keeperFeeFromLends: BNCoin
46+
keeperFeeFromBorrows: BNCoin
4347
}) => {
4448
if (!currentAccount || !chainConfig || !address) {
4549
console.error('Missing required dependencies:', {
@@ -69,24 +73,21 @@ export function useSubmitParentOrderWithChildren() {
6973
const perpOrderType: ExecutePerpOrderType =
7074
orderType === OrderType.MARKET ? 'default' : 'parent'
7175

72-
const params: any = {
76+
return executeParentOrderWithConditionalTriggers({
7377
accountId: currentAccount.id,
7478
coin: BNCoin.fromDenomAndBigNumber(asset.denom, orderSize),
7579
reduceOnly: isReduceOnly,
7680
autolend: isAutoLendEnabledForCurrentAccount,
7781
baseDenom,
7882
keeperFee: keeperFee.calculateKeeperFee,
83+
keeperFeeFromLends,
84+
keeperFeeFromBorrows,
7985
orderType: perpOrderType,
8086
conditionalTriggers,
81-
}
82-
83-
if (orderType === OrderType.LIMIT && limitPrice) {
84-
params.limitPrice = limitPrice.toString()
85-
} else if (orderType === OrderType.STOP && stopPrice) {
86-
params.stopPrice = stopPrice.toString()
87-
}
88-
89-
return executeParentOrderWithConditionalTriggers(params)
87+
limitPrice:
88+
orderType === OrderType.LIMIT && limitPrice ? limitPrice.toString() : undefined,
89+
stopPrice: orderType === OrderType.STOP && stopPrice ? stopPrice.toString() : undefined,
90+
})
9091
} catch (error) {
9192
console.error('Error submitting parent order with children:', error)
9293
return false

src/pages/_layout.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ function PageContainer(props: Props) {
4949
)
5050

5151
return (
52-
<div className='relative flex items-center justify-center w-full h-full z-80'>
52+
<div className='relative flex items-center justify-center w-full h-full z-50'>
5353
{props.focusComponent.component}
5454
</div>
5555
)

0 commit comments

Comments
 (0)