Skip to content
Open
Show file tree
Hide file tree
Changes from all 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
14 changes: 12 additions & 2 deletions packages/strategies/src/delta-neutral.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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", () => {
Expand Down
2 changes: 1 addition & 1 deletion packages/strategies/src/delta-neutral.ts
Original file line number Diff line number Diff line change
Expand Up @@ -494,8 +494,8 @@ export class DeltaNeutralStrategy implements Strategy<DeltaNeutralConfig> {
);
this.#rebalanceCount += 1;
this.#refreshDelta(price);
this.#markLastObservationRebalanced();
}
this.#markLastObservationRebalanced();
return [];
}

Expand Down
65 changes: 30 additions & 35 deletions packages/strategies/src/scenario-runner.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Decimal } from "./decimal";
import { HealthFactorMonitor } from "./monitor";
import { LiquidationParameterModel } from "./models/liquidation-parameter";

export interface BacktestRunnerConfig {
Expand Down Expand Up @@ -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;
Expand All @@ -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 {
Expand Down Expand Up @@ -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
Expand Down
Loading