Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
Show all changes
19 commits
Select commit Hold shift + click to select a range
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -1394,6 +1394,33 @@ export default class ServiceHyperliquidExchange extends ServiceBase {
const assetType = precision?.type;
const reduceOnly =
assetType === 'spot' ? false : Boolean(params.reduceOnly);
const formatOptionalTwapPrice = (
price: string | undefined,
fieldName: 'trigger' | 'stop',
) => {
if (!price) {
return undefined;
}
const formattedPrice = formatHlPrice(
price,
szDecimals,
assetType ?? 'perp',
);
if (!formattedPrice) {
throw new OneKeyLocalError(
`TWAP ${fieldName} price is too small for HL tick size`,
);
}
return formattedPrice;
};
const triggerPrice = formatOptionalTwapPrice(
params.triggerPrice,
'trigger',
);
const stopPrice = formatOptionalTwapPrice(params.stopPrice, 'stop');
Comment thread
Minnzen marked this conversation as resolved.
if (triggerPrice && typeof params.triggerAbove !== 'boolean') {
throw new OneKeyLocalError('TWAP trigger direction is required');
}
const twap = {
a: params.assetId,
b: params.isBuy,
Expand All @@ -1402,6 +1429,12 @@ export default class ServiceHyperliquidExchange extends ServiceBase {
m: params.minutes,
t: params.randomize,
};
const details = {
t: triggerPrice
? { p: triggerPrice, a: params.triggerAbove as boolean }
: null,
s: stopPrice ?? null,
};
const client = await this.getExchangeClientForTrading();
const context = await this._buildLogContext();
const requestPayload = {
Expand All @@ -1413,12 +1446,14 @@ export default class ServiceHyperliquidExchange extends ServiceBase {
minutes: params.minutes,
randomize: params.randomize,
},
details,
};

try {
const response = await convertHyperLiquidResponse(() =>
client.twapOrder({
twap,
details,
}),
);
defaultLogger.perp.hyperliquid.twapOrder({
Expand Down
90 changes: 66 additions & 24 deletions packages/kit/src/states/jotai/contexts/hyperliquid/actions.ts
Original file line number Diff line number Diff line change
Expand Up @@ -49,13 +49,21 @@ import { EModalPerpRoutes } from '@onekeyhq/shared/src/routes/perp';
import { getCurrentVisibilityState } from '@onekeyhq/shared/src/utils/appVisibility';
import { memoFn } from '@onekeyhq/shared/src/utils/cacheUtils';
import {
SCALE_ORDER_MIN_NOTIONAL,
getReduceOnlyOrderGuardError,
getReduceOnlyPositionMaxSize,
getReduceOnlyPositionSnapshotError,
getScaleOrderReferencePrice,
getScaleOrderSizeSkew,
} from '@onekeyhq/shared/src/utils/hyperliquidScaleOrderUtils';
import {
TWAP_MAX_DURATION_MINUTES,
TWAP_MIN_DURATION_MINUTES,
TWAP_MIN_ORDER_NOTIONAL,
getTwapTriggerAbove,
getTwapTriggerReferencePrice,
isTwapTotalNotionalValid,
isValidTwapDuration,
} from '@onekeyhq/shared/src/utils/hyperliquidTwapUtils';
import {
getPerpsOrderBookTickOptionWithCache,
getPerpsOrderBookTickOptionsWithCache,
Expand All @@ -64,6 +72,7 @@ import {
import { classifyTpSlOrder } from '@onekeyhq/shared/src/utils/perpsTpSlUtils';
import {
findTokensByAlias,
formatHlPrice,
formatPriceToSignificantDigits,
formatSpotAssetCtx,
getTriggerEffectivePrice,
Expand Down Expand Up @@ -164,10 +173,6 @@ const MAX_LEDGER_UPDATES = 200;
const ACCOUNT_MODE_USER_WALLET_TIMEOUT_MS = platformEnv.isNative
? 60_000
: 15_000;
const TWAP_MIN_DURATION_MINUTES = 5;
const TWAP_MAX_DURATION_MINUTES = 1440;
const TWAP_MIN_ORDER_NOTIONAL = Number(SCALE_ORDER_MIN_NOTIONAL);
const TWAP_ESTIMATED_SLICE_INTERVAL_SECONDS = 30;
const TWAP_SLICE_FILLS_MAX_COUNT = 2000;

const setAbstractionWithUserWalletTimeout = makeTimeoutPromise<
Expand Down Expand Up @@ -1676,6 +1681,8 @@ class ContextJotaiActionsHyperliquid extends ContextJotaiActionsBase {
triggerPrice: '',
executionPrice: '',
triggerReduceOnly: true,
twapTriggerPrice: '',
twapStopPrice: '',
};

// update limit price once using current atom snapshot.
Expand Down Expand Up @@ -1852,6 +1859,8 @@ class ContextJotaiActionsHyperliquid extends ContextJotaiActionsBase {
sizePercent: 0,
triggerPrice: '',
executionPrice: '',
twapTriggerPrice: '',
twapStopPrice: '',
};
// Spot doesn't have margin mode -- force to usd if currently set to margin
const currentPrefs = await perpsTradingPreferencesAtom.get();
Expand Down Expand Up @@ -2419,6 +2428,8 @@ class ContextJotaiActionsHyperliquid extends ContextJotaiActionsBase {
slValue: '',
triggerPrice: '',
executionPrice: '',
twapTriggerPrice: '',
twapStopPrice: '',
});
});

Expand Down Expand Up @@ -2876,20 +2887,18 @@ class ContextJotaiActionsHyperliquid extends ContextJotaiActionsBase {
}

const minutes = Number(formData.twapDurationMinutes ?? 0);
if (
!Number.isInteger(minutes) ||
minutes < TWAP_MIN_DURATION_MINUTES ||
minutes > TWAP_MAX_DURATION_MINUTES
) {
if (!isValidTwapDuration(minutes)) {
throw new OneKeyLocalError(
`TWAP duration must be ${TWAP_MIN_DURATION_MINUTES}-${TWAP_MAX_DURATION_MINUTES} minutes`,
);
}

const markPrice = isSpot
? (params.price ?? env.markPrice ?? '')
: (activeAssetCtxValue?.ctx?.markPrice ?? env.markPrice ?? '');
const markPriceBN = new BigNumber(markPrice);
const markPriceBN = getTwapTriggerReferencePrice({
isSpot,
midPrice: params.price ?? env.markPrice ?? '',
markPrice:
activeAssetCtxValue?.ctx?.markPrice ?? env.markPrice ?? '',
});
if (!markPriceBN.isFinite() || markPriceBN.lte(0)) {
throw new OneKeyLocalError(
'Market price unavailable, please try again',
Expand All @@ -2901,6 +2910,41 @@ class ContextJotaiActionsHyperliquid extends ContextJotaiActionsBase {
env.szDecimals ??
2)
: (activeAssetValue?.universe?.szDecimals ?? env.szDecimals ?? 2);
const rawTriggerPrice = formData.twapTriggerPrice?.trim();
const triggerPrice = rawTriggerPrice
? formatHlPrice(
rawTriggerPrice,
szDecimals,
isSpot ? 'spot' : 'perp',
)
: undefined;
const stopPrice = formData.twapStopPrice?.trim();
let triggerAbove: boolean | undefined;
if (rawTriggerPrice && !triggerPrice) {
throw new OneKeyLocalError(
'TWAP trigger price is too small for HL tick size',
);
}
if (triggerPrice) {
triggerAbove = getTwapTriggerAbove({
triggerPrice,
markPrice: markPriceBN,
});
if (typeof triggerAbove !== 'boolean') {
throw new OneKeyLocalError(
'TWAP trigger price must be positive and differ from the market price',
);
}
}
if (stopPrice) {
const stopPriceBN = new BigNumber(stopPrice);
if (!stopPriceBN.isFinite() || stopPriceBN.lte(0)) {
throw new OneKeyLocalError(
'TWAP stop price must be a positive number',
);
}
}

const resolvedSize = resolveTradingSize({
sizeInputMode: formData.sizeInputMode,
manualSize: formData.size,
Expand All @@ -2922,19 +2966,14 @@ class ContextJotaiActionsHyperliquid extends ContextJotaiActionsBase {
throw new OneKeyLocalError('Order size is required');
}

const totalNotional = resolvedSizeBN.multipliedBy(markPriceBN);
const estimatedSlices = Math.max(
1,
Math.ceil((minutes * 60) / TWAP_ESTIMATED_SLICE_INTERVAL_SECONDS),
);
const averageSliceNotional =
totalNotional.dividedBy(estimatedSlices);
if (
!averageSliceNotional.isFinite() ||
averageSliceNotional.lt(TWAP_MIN_ORDER_NOTIONAL)
!isTwapTotalNotionalValid({
size: resolvedSizeBN,
price: markPriceBN,
})
) {
throw new OneKeyLocalError(
'TWAP order size is too small for this duration',
`TWAP total notional must be at least ${TWAP_MIN_ORDER_NOTIONAL} USDC`,
);
}

Expand Down Expand Up @@ -2977,6 +3016,9 @@ class ContextJotaiActionsHyperliquid extends ContextJotaiActionsBase {
reduceOnly,
minutes,
randomize: formData.twapRandomize ?? true,
triggerPrice,
triggerAbove,
stopPrice,
szDecimals,
},
);
Expand Down
4 changes: 4 additions & 0 deletions packages/kit/src/states/jotai/contexts/hyperliquid/atoms.ts
Original file line number Diff line number Diff line change
Expand Up @@ -309,6 +309,8 @@ export interface ITradingFormData {
twapDurationMinutes?: string;
twapRandomize?: boolean;
twapReduceOnly?: boolean;
twapTriggerPrice?: string;
twapStopPrice?: string;
}

export const { atom: tradingFormAtom, use: useTradingFormAtom } =
Expand Down Expand Up @@ -347,6 +349,8 @@ export const { atom: tradingFormAtom, use: useTradingFormAtom } =
twapDurationMinutes: '10',
twapRandomize: true,
twapReduceOnly: false,
twapTriggerPrice: '',
twapStopPrice: '',
});

export type ITradingFormOrderPriceParams = Pick<
Expand Down
Loading
Loading