src/assets/leverage.ts
v0.3.0 · 1.7 KB
import type { AssetInfo } from './types.js';
const LEVERAGE_EPSILON = 1e-9;
export interface LeverageParams {
/** Integer leverage to send in `updateLeverage`: `min(max(1, floor(wanted + 1e-9)), floor(maxLeverage))`. */
readonly leverage: number;
/** `!onlyIsolated`. Sending `isCross: true` for an isolated-only market is rejected. */
readonly isCross: boolean;
/** True when `leverage` differs from `wanted` by more than float noise (floored or capped). Scale the notional down accordingly. */
readonly clamped: boolean;
}
/**
* Leverage parameters for `updateLeverage` derived from asset meta.
*
* Why: leverage above the pair's `maxLeverage` (xyz ranges from 10x to 50x) makes `updateLeverage` fail, and
* `isCross: true` on an isolated-only market (most xyz pairs) is rejected too. Leverage on HL is an integer ≥ 1.
*
* @throws RangeError for spot assets or a non-positive / non-finite `wanted`.
*/
export function leverageParams(
asset: Pick<AssetInfo, 'coin' | 'market' | 'maxLeverage' | 'onlyIsolated'>,
wanted: number,
): LeverageParams {
if (asset.market !== 'perp') throw new RangeError(`${asset.coin}: leverage does not apply to spot`);
if (!Number.isFinite(wanted) || wanted <= 0) throw new RangeError(`${asset.coin}: wanted leverage must be > 0, got ${wanted}`);
const cap = Math.max(1, Math.floor(asset.maxLeverage + LEVERAGE_EPSILON));
// Epsilon before floor, same approach as size flooring: a computed 0.3 / 0.1 = 2.9999999999999996 must give 3, not 2.
const leverage = Math.min(Math.max(1, Math.floor(wanted + LEVERAGE_EPSILON)), cap);
return { leverage, isCross: !asset.onlyIsolated, clamped: Math.abs(leverage - wanted) > LEVERAGE_EPSILON };
}