diff --git a/packages/strategies/src/delta-neutral.test.ts b/packages/strategies/src/delta-neutral.test.ts index 8f2091ec..769ee0e4 100644 --- a/packages/strategies/src/delta-neutral.test.ts +++ b/packages/strategies/src/delta-neutral.test.ts @@ -256,8 +256,18 @@ describe("DeltaNeutralStrategy stepping", () => { expect(strategy.netDelta.toStroops()).toBe(0n); expect(strategy.fundingPnl.toString()).toBe("21.744"); expect(strategy.rebalanceCount).toBe(1); - expect(strategy.deltaHistory[2]!.rebalanced).toBe(true); - expect(strategy.deltaHistory[3]!.rebalanced).toBe(false); + + const rebalanceObservation = strategy.deltaHistory[2]!; + expect(rebalanceObservation.tick).toBe(2); + expect(rebalanceObservation.netDelta.toString()).toBe("392.1568628"); + expect(rebalanceObservation.netDeltaNotional.toString()).toBe("400"); + expect(rebalanceObservation.rebalanced).toBe(true); + + const followUpObservation = strategy.deltaHistory[3]!; + expect(followUpObservation.tick).toBe(3); + expect(followUpObservation.netDelta.toStroops()).toBe(0n); + expect(followUpObservation.netDeltaNotional.toStroops()).toBe(0n); + expect(followUpObservation.rebalanced).toBe(false); }); it("trims the hedge when a falling price makes the book net short", () => { diff --git a/packages/strategies/src/delta-neutral.ts b/packages/strategies/src/delta-neutral.ts index 6fe18f8e..0a0b55ba 100644 --- a/packages/strategies/src/delta-neutral.ts +++ b/packages/strategies/src/delta-neutral.ts @@ -494,8 +494,8 @@ export class DeltaNeutralStrategy implements Strategy { ); this.#rebalanceCount += 1; this.#refreshDelta(price); + this.#markLastObservationRebalanced(); } - this.#markLastObservationRebalanced(); return []; } diff --git a/packages/strategies/src/scenario-runner.ts b/packages/strategies/src/scenario-runner.ts index ffeb34f8..20e1c5c8 100644 --- a/packages/strategies/src/scenario-runner.ts +++ b/packages/strategies/src/scenario-runner.ts @@ -1,4 +1,5 @@ import { Decimal } from "./decimal"; +import { HealthFactorMonitor } from "./monitor"; import { LiquidationParameterModel } from "./models/liquidation-parameter"; export interface BacktestRunnerConfig { @@ -58,6 +59,7 @@ export class SelfRepayingLoanRunner { readonly config: BacktestRunnerConfig; readonly deleverageThresholdHf: Decimal; readonly deleverageTargetHf: Decimal; + readonly healthFactorMonitor: HealthFactorMonitor; constructor(config: BacktestRunnerConfig) { this.config = config; @@ -76,6 +78,12 @@ export class SelfRepayingLoanRunner { `Deleverage target health factor (${this.deleverageTargetHf.toString()}) must be greater than the threshold (${this.deleverageThresholdHf.toString()})` ); } + + this.healthFactorMonitor = new HealthFactorMonitor({ + liquidationModel: config.liquidationModel, + safetyBuffer: this.deleverageThresholdHf.sub(Decimal.one()), + targetHealthFactor: this.deleverageTargetHf, + }); } runScenario(scenarioName: string, ticks: MarketTick[]): BacktestReport { @@ -141,41 +149,28 @@ export class SelfRepayingLoanRunner { currentHf.lt(this.deleverageThresholdHf) && !debt.isZero() ) { - // Auto-deleverage: calculate required debt repayment to bring HF back to deleverageTargetHf - // Target HF = (Collateral' * LiquidationThreshold) / Debt' - // Where Collateral' = Collateral - (RepayAmount / Price) - // Debt' = Debt - RepayAmount - // Solving for RepayAmount: - // TargetHf * (Debt - Repay) = (CollateralValue - Repay) * Threshold - // TargetHf * Debt - TargetHf * Repay = CollateralValue * Threshold - Repay * Threshold - // Repay * (TargetHf - Threshold) = TargetHf * Debt - CollateralValue * Threshold - const threshold = this.config.liquidationModel.liquidationThreshold; - const targetHf = this.deleverageTargetHf; - - const num = targetHf - .mul(debt) - .sub(collateralAmount.mul(tick.collateralPrice).mul(threshold)); - const den = targetHf.sub(threshold); - - // The sale funds the repayment, so it is bounded by the debt and by the - // value the collateral can raise. - let debtToRepay = num.div(den); - if (debtToRepay.gt(debt)) debtToRepay = debt; - const collateralValue = collateralAmount.mul(tick.collateralPrice); - if (debtToRepay.gt(collateralValue)) debtToRepay = collateralValue; - - let collToSell = debtToRepay.div(tick.collateralPrice); - if (collToSell.gt(collateralAmount)) collToSell = collateralAmount; - collateralAmount = collateralAmount.sub(collToSell); - debt = debt.sub(debtToRepay); - deleveragedCollateral = collToSell; - totalDeleveragedCollateral = totalDeleveragedCollateral.add(collToSell); - - // Recompute HF after deleveraging - currentHf = this.config.liquidationModel.computeHealthFactor( - collateralAmount.mul(tick.collateralPrice), - debt - ); + const decision = this.healthFactorMonitor.evaluate({ + collateralAmount, + collateralPrice: tick.collateralPrice, + debt, + }); + + if (decision.shouldDeleverage) { + const debtToRepay = decision.requiredDebtRepayment; + const collToSell = decision.requiredCollateralToSell; + + collateralAmount = collateralAmount.sub(collToSell); + debt = debt.sub(debtToRepay); + deleveragedCollateral = collToSell; + totalDeleveragedCollateral = + totalDeleveragedCollateral.add(collToSell); + + currentHf = this.healthFactorMonitor.computeHealthFactor({ + collateralAmount, + collateralPrice: tick.collateralPrice, + debt, + }); + } } // 5. Engine liquidation check