Skip to content

Commit

Permalink
fix: fix isolated pnl transfer calc (#664)
Browse files Browse the repository at this point in the history
  • Loading branch information
moo-onthelawn authored Sep 19, 2024
1 parent 9399502 commit 8f66f3c
Show file tree
Hide file tree
Showing 5 changed files with 104 additions and 40 deletions.
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ allprojects {
}

group = "exchange.dydx.abacus"
version = "1.11.11"
version = "1.11.12"

repositories {
google()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -13,8 +13,7 @@ import exchange.dydx.abacus.calculator.SlippageConstants.TAKE_PROFIT_MARKET_ORDE
import exchange.dydx.abacus.output.input.MarginMode
import exchange.dydx.abacus.output.input.OrderSide
import exchange.dydx.abacus.protocols.ParserProtocol
import exchange.dydx.abacus.utils.MAX_FREE_CROSS_COLLATERAL_BUFFER_PERCENT
import exchange.dydx.abacus.utils.MAX_FREE_ISOLATED_COLLATERAL_BUFFER_PERCENT
import exchange.dydx.abacus.utils.MAX_FREE_COLLATERAL_BUFFER_PERCENT
import exchange.dydx.abacus.utils.NUM_PARENT_SUBACCOUNTS
import exchange.dydx.abacus.utils.Numeric
import exchange.dydx.abacus.utils.QUANTUM_MULTIPLIER
Expand All @@ -25,6 +24,7 @@ import exchange.dydx.abacus.utils.safeSet
import kollections.JsExport
import kotlinx.serialization.Serializable
import kotlin.math.abs
import kotlin.math.max
import kotlin.math.min
import kotlin.math.pow

Expand Down Expand Up @@ -478,8 +478,9 @@ internal class TradeInputCalculator(
): Map<String, Any>? {
val marketId = parser.asString(market?.get("id"))
val tradeSize = parser.asNativeMap(trade["size"])
val tradeSide = OrderSide.invoke(parser.asString(trade["side"]))

if (tradeSize != null && marketId != null) {
if (tradeSize != null && marketId != null && tradeSide != null) {
val maxMarketLeverage = maxMarketLeverage(market)
val targetLeverage = parser.asDouble(trade["targetLeverage"])
val marginMode = MarginMode.invoke(parser.asString(trade["marginMode"])) ?: MarginMode.Cross
Expand All @@ -490,7 +491,6 @@ internal class TradeInputCalculator(
}
val freeCollateral = parser.asDouble(parser.value(subaccount, "freeCollateral.current")) ?: Numeric.double.ZERO

val tradeSide = OrderSide.invoke(parser.asString(trade["side"]))
val position = parser.asNativeMap(parser.value(subaccount, "openPositions.$marketId"))
val positionNotionalSize = if (position != null) {
parser.asDouble(
Expand All @@ -512,7 +512,7 @@ internal class TradeInputCalculator(
} else {
Numeric.double.ZERO
}
val isTradeSameSide = tradeSide != null &&
val isTradeSameSide =
((tradeSide == OrderSide.Buy && positionSize >= Numeric.double.ZERO) || (tradeSide == OrderSide.Sell && positionSize <= Numeric.double.ZERO))

return when (input) {
Expand Down Expand Up @@ -582,6 +582,7 @@ internal class TradeInputCalculator(
stepSize,
oraclePrice,
isReduceOnly,
tradeSide,
)
}

Expand All @@ -591,13 +592,43 @@ internal class TradeInputCalculator(
return null
}

private fun isolatedPnlImpact(marginMode: MarginMode, size: Double, entryPrice: Double, oraclePrice: Double?, isReduceOnly: Boolean): Double {
// Calculate the difference between the oracle price and the ask/bid price in order to determine immediate PnL impact that would affect collateral checks
// Should only apply to orders that are increasing in position size (not reduceOnly)
// In a cleaner world, this would call MarginCalculator.getShouldTransferInCollateralDeprecated and MarginCalculator.getTransferAmountFromTargetLeverage but because it will be deprecated soon anyways, just passing in the necessary variables
private fun isolatedPnlImpactForBalance(
marginMode: MarginMode,
tradeSide: OrderSide,
balance: Double,
tradeLeverage: Double,
entryPrice: Double,
oraclePrice: Double?,
isReduceOnly: Boolean
): Double {
// Calculates the pnl impact for an isolated order trade, given:
// - the difference between the oracle price and the ask/bid price
// - a total balance to be used for the trade, note this balance should also be used for the pnl impact
//
// This should only apply to orders that are increasing in position size (not reduceOnly).
// In a cleaner world, this would call MarginCalculator.getShouldTransferInCollateralDeprecated and MarginCalculator.getTransferAmountFromTargetLeverage but
// because it will be deprecated soon anyways, just passing in the necessary variables.

// Formula Derivation:
// pnlImpact = diff * size
// size = balance * tradeLeverage / entryPrice
// pnlImpact = diff * (balance - pnlImpact) * tradeLeverage / entryPrice
// pnlImpact = (diff * balance - diff * pnlImpact) * tradeLeverage / entryPrice
// pnlImpact * (entryPrice + diff * tradeLeverage) = diff * balance * tradeLeverage
// pnlImpact = (diff * balance * tradeLeverage) / (entryPrice + diff * tradeLeverage)

return when (marginMode) {
MarginMode.Cross -> Numeric.double.ZERO
MarginMode.Isolated -> if (isReduceOnly) Numeric.double.ZERO else (entryPrice - (oraclePrice ?: entryPrice)).abs() * size
MarginMode.Isolated -> if (isReduceOnly) {
Numeric.double.ZERO
} else {
val diff = when (tradeSide) {
OrderSide.Buy -> entryPrice - (oraclePrice ?: entryPrice)
OrderSide.Sell -> (oraclePrice ?: entryPrice) - entryPrice
}
val pnlImpact = if ((entryPrice + diff * tradeLeverage) > Numeric.double.ZERO) (diff * balance * tradeLeverage) / (entryPrice + diff * tradeLeverage) else Numeric.double.ZERO
max(pnlImpact, Numeric.double.ZERO)
}
}
}

Expand All @@ -613,6 +644,7 @@ internal class TradeInputCalculator(
stepSize: Double,
oraclePrice: Double?,
isReduceOnly: Boolean,
tradeSide: OrderSide,
): Map<String, Any>? {
if (marginMode == MarginMode.Isolated && !isTradeSameSide) {
// For isolated margin orders where the user is trading on the opposite side of their currentPosition, the balancePercent represents a percentage of their current position rather than freeCollateral
Expand All @@ -624,11 +656,7 @@ internal class TradeInputCalculator(
return null
}

val maxPercent = when (marginMode) {
MarginMode.Cross -> MAX_FREE_CROSS_COLLATERAL_BUFFER_PERCENT
MarginMode.Isolated -> MAX_FREE_ISOLATED_COLLATERAL_BUFFER_PERCENT
}
val cappedPercent = min(balancePercent, maxPercent)
val cappedPercent = min(balancePercent, MAX_FREE_COLLATERAL_BUFFER_PERCENT)

val existingBalance = existingPositionNotionalSize.abs() / tradeLeverage
val desiredBalance = when (marginMode) {
Expand Down Expand Up @@ -657,23 +685,25 @@ internal class TradeInputCalculator(
if (entryPrice != null && entryPrice > Numeric.double.ZERO && entrySize != null) {
val entryUsdcSize = entrySize * entryPrice
val entryBalanceSize = entryUsdcSize / tradeLeverage
filled = (balanceTotal + entryBalanceSize + isolatedPnlImpact(marginMode, entrySize, entryPrice, oraclePrice, isReduceOnly)) >= desiredBalance
val pnlImpact = isolatedPnlImpactForBalance(marginMode, tradeSide, desiredBalance, tradeLeverage, entryPrice, oraclePrice, isReduceOnly)
filled = (balanceTotal + entryBalanceSize + pnlImpact) >= desiredBalance

var matchedSize = entrySize
var matchedUsdcSize = entryUsdcSize
var matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode, matchedSize, entryPrice, oraclePrice, isReduceOnly)
var matchedBalance = matchedUsdcSize / tradeLeverage

if (filled) {
matchedBalance = desiredBalance - balanceTotal - isolatedPnlImpact(marginMode, (desiredBalance - balanceTotal) * tradeLeverage / entryPrice, entryPrice, oraclePrice, isReduceOnly)
matchedBalance = desiredBalance - balanceTotal - pnlImpact
matchedUsdcSize = matchedBalance * tradeLeverage
matchedSize = matchedUsdcSize / entryPrice
// Round the size to appropriate step size for market and recalculate
matchedSize =
Rounder.quickRound(
matchedSize,
stepSize,
)
matchedUsdcSize = matchedSize * entryPrice
matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode, matchedSize, entryPrice, oraclePrice, isReduceOnly)
matchedBalance = matchedUsdcSize / tradeLeverage + pnlImpact
}
sizeTotal += matchedSize
usdcSizeTotal += matchedUsdcSize
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,11 +16,11 @@ import exchange.dydx.abacus.state.internalstate.InternalSubaccountState
import exchange.dydx.abacus.state.internalstate.InternalTradeInputState
import exchange.dydx.abacus.state.internalstate.InternalUserState
import exchange.dydx.abacus.state.internalstate.safeCreate
import exchange.dydx.abacus.utils.MAX_FREE_CROSS_COLLATERAL_BUFFER_PERCENT
import exchange.dydx.abacus.utils.MAX_FREE_ISOLATED_COLLATERAL_BUFFER_PERCENT
import exchange.dydx.abacus.utils.MAX_FREE_COLLATERAL_BUFFER_PERCENT
import exchange.dydx.abacus.utils.Numeric
import exchange.dydx.abacus.utils.Rounder
import kollections.toIList
import kotlin.math.max
import kotlin.math.min

internal class TradeInputMarketOrderCalculator() {
Expand Down Expand Up @@ -109,7 +109,7 @@ internal class TradeInputMarketOrderCalculator() {
val tradeSize = trade.size
val freeCollateral = subaccount?.calculated?.get(CalculationPeriod.current)?.freeCollateral

if (tradeSize != null && freeCollateral != null && freeCollateral > Numeric.double.ZERO) {
if (tradeSize != null && tradeSide != null && freeCollateral != null && freeCollateral > Numeric.double.ZERO) {
val maxMarketLeverage = market?.perpetualMarket?.configs?.maxMarketLeverage ?: Numeric.double.ONE
val targetLeverage = trade.targetLeverage
val marginMode = trade.marginMode ?: MarginMode.Cross
Expand All @@ -131,8 +131,7 @@ internal class TradeInputMarketOrderCalculator() {
} else {
Numeric.double.ZERO
}
val isTradeSameSide = tradeSide != null &&
((tradeSide == OrderSide.Buy && positionSize >= Numeric.double.ZERO) || (tradeSide == OrderSide.Sell && positionSize <= Numeric.double.ZERO))
val isTradeSameSide = ((tradeSide == OrderSide.Buy && positionSize >= Numeric.double.ZERO) || (tradeSide == OrderSide.Sell && positionSize <= Numeric.double.ZERO))

return when (input) {
"size.size", "size.percent" -> {
Expand Down Expand Up @@ -195,6 +194,7 @@ internal class TradeInputMarketOrderCalculator() {
stepSize = stepSize,
oraclePrice = oraclePrice,
isReduceOnly = isReduceOnly,
tradeSide = tradeSide,
)
}

Expand All @@ -215,12 +215,40 @@ internal class TradeInputMarketOrderCalculator() {
}
}

private fun isolatedPnlImpact(marginMode: MarginMode, size: Double, entryPrice: Double, oraclePrice: Double?, isReduceOnly: Boolean): Double {
// Calculate the difference between the oracle price and the ask/bid price in order to determine immediate PnL impact that would affect collateral checks
private fun isolatedPnlImpactForBalance(
marginMode: MarginMode,
tradeSide: OrderSide,
desiredBalance: Double,
tradeLeverage: Double,
entryPrice: Double,
oraclePrice: Double?,
isReduceOnly: Boolean
): Double {
// Calculates the pnl impact for an isolated order trade, given:
// - the difference between the oracle price and the ask/bid price
// - a total balance to be used for the trade, note this balance should also be used for the pnl impact
// TODO CT-1192: refactor to call into MarginCalculator.getShouldTransferInCollateralDeprecated and MarginCalculator.getTransferAmountFromTargetLeverage

// Formula Derivation:
// pnlImpact = diff * size
// size = balance * tradeLeverage / entryPrice
// pnlImpact = diff * (balance - pnlImpact) * tradeLeverage / entryPrice
// pnlImpact = (diff * balance - diff * pnlImpact) * tradeLeverage / entryPrice
// pnlImpact * (entryPrice + diff * tradeLeverage) = diff * balance * tradeLeverage
// pnlImpact = (diff * balance * tradeLeverage) / (entryPrice + diff * tradeLeverage)

return when (marginMode) {
MarginMode.Cross -> Numeric.double.ZERO
MarginMode.Isolated -> if (isReduceOnly) Numeric.double.ZERO else (entryPrice - (oraclePrice ?: entryPrice)).abs() * size
MarginMode.Isolated -> if (isReduceOnly) {
Numeric.double.ZERO
} else {
val diff = when (tradeSide) {
OrderSide.Buy -> entryPrice - (oraclePrice ?: entryPrice)
OrderSide.Sell -> (oraclePrice ?: entryPrice) - entryPrice
}
val pnlImpact = if ((entryPrice + diff * tradeLeverage) > Numeric.double.ZERO) (diff * desiredBalance * tradeLeverage) / (entryPrice + diff * tradeLeverage) else Numeric.double.ZERO
max(pnlImpact, Numeric.double.ZERO)
}
}
}

Expand All @@ -236,18 +264,15 @@ internal class TradeInputMarketOrderCalculator() {
stepSize: Double,
oraclePrice: Double?,
isReduceOnly: Boolean,
tradeSide: OrderSide,
): TradeInputMarketOrder? {
if (marginMode == MarginMode.Isolated && !isTradeSameSide) {
// For isolated margin orders where the user is trading on the opposite side of their currentPosition, the balancePercent represents a percentage of their current position rather than freeCollateral
val desiredSize = existingPositionSize.abs() * balancePercent
return createMarketOrderFromSize(size = desiredSize, existingPositionNotionalSize = existingPositionNotionalSize, isTradeSameSide = isTradeSameSide, freeCollateral = freeCollateral, tradeLeverage = tradeLeverage, orderbook = orderbook)
}

val maxPercent = when (marginMode) {
MarginMode.Cross -> MAX_FREE_CROSS_COLLATERAL_BUFFER_PERCENT
MarginMode.Isolated -> MAX_FREE_ISOLATED_COLLATERAL_BUFFER_PERCENT
}
val cappedPercent = min(balancePercent, maxPercent)
val cappedPercent = min(balancePercent, MAX_FREE_COLLATERAL_BUFFER_PERCENT)

val existingBalance = existingPositionNotionalSize.abs() / tradeLeverage

Expand Down Expand Up @@ -276,23 +301,33 @@ internal class TradeInputMarketOrderCalculator() {
if (entryPrice > Numeric.double.ZERO) {
val entryUsdcSize = entrySize * entryPrice
val entryBalanceSize = entryUsdcSize / tradeLeverage
filled = (balanceTotal + entryBalanceSize + isolatedPnlImpact(marginMode = marginMode, size = entrySize, entryPrice = entryPrice, oraclePrice = oraclePrice, isReduceOnly = isReduceOnly)) >= desiredBalance
val pnlImpact = isolatedPnlImpactForBalance(
marginMode = marginMode,
tradeSide = tradeSide,
desiredBalance = desiredBalance,
tradeLeverage = tradeLeverage,
entryPrice = entryPrice,
oraclePrice = oraclePrice,
isReduceOnly = isReduceOnly,
)
filled = (balanceTotal + entryBalanceSize + pnlImpact) >= desiredBalance

var matchedSize = entrySize
var matchedUsdcSize = entryUsdcSize
var matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode = marginMode, size = matchedSize, entryPrice = entryPrice, oraclePrice = oraclePrice, isReduceOnly = isReduceOnly)
var matchedBalance = matchedUsdcSize / tradeLeverage

if (filled) {
matchedBalance = desiredBalance - balanceTotal + isolatedPnlImpact(marginMode = marginMode, size = (desiredBalance - balanceTotal) * tradeLeverage / entryPrice, entryPrice = entryPrice, oraclePrice = oraclePrice, isReduceOnly = isReduceOnly)
matchedBalance = desiredBalance - balanceTotal - pnlImpact
matchedUsdcSize = matchedBalance * tradeLeverage
matchedSize = matchedUsdcSize / entryPrice
// Round the size to appropriate step size for market and recalculate
matchedSize =
Rounder.quickRound(
matchedSize,
stepSize,
)
matchedUsdcSize = matchedSize * entryPrice
matchedBalance = matchedUsdcSize / tradeLeverage + isolatedPnlImpact(marginMode = marginMode, size = matchedSize, entryPrice = entryPrice, oraclePrice = oraclePrice, isReduceOnly = isReduceOnly)
matchedBalance = matchedUsdcSize / tradeLeverage + pnlImpact
}
sizeTotal += matchedSize
usdcSizeTotal += matchedUsdcSize
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,10 +12,9 @@ internal const val QUANTUM_MULTIPLIER = 1_000_000
internal const val SLIPPAGE_PERCENT = "1"

// Trade Constants
internal const val MAX_FREE_CROSS_COLLATERAL_BUFFER_PERCENT = 0.95
internal const val MAX_FREE_COLLATERAL_BUFFER_PERCENT = 0.95

// Isolated Margin Constants
internal const val MAX_FREE_ISOLATED_COLLATERAL_BUFFER_PERCENT = 0.93
internal const val MAX_LEVERAGE_BUFFER_PERCENT = 0.98
internal const val MARGIN_COLLATERALIZATION_CHECK_BUFFER = 0.01;

Expand Down
2 changes: 1 addition & 1 deletion v4_abacus.podspec
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
Pod::Spec.new do |spec|
spec.name = 'v4_abacus'
spec.version = '1.11.11'
spec.version = '1.11.12'
spec.homepage = 'https://github.com/dydxprotocol/v4-abacus'
spec.source = { :http=> ''}
spec.authors = ''
Expand Down

0 comments on commit 8f66f3c

Please sign in to comment.