Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: fix isolated pnl transfer calc #664

Merged
merged 4 commits into from
Sep 19, 2024
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
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
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 = (diff * balance * tradeLeverage) / (entryPrice + diff * tradeLeverage)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need to check for dividing by zero?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

haha i alwayyyys forget this huh

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 res = (diff * desiredBalance * tradeLeverage) / (entryPrice + diff * tradeLeverage)
max(res, 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
Loading