Skip to content

Refactor health calculations#275

Open
jordanschalm wants to merge 21 commits intomainfrom
balance-sheet-health-statement-refactor
Open

Refactor health calculations#275
jordanschalm wants to merge 21 commits intomainfrom
balance-sheet-health-statement-refactor

Conversation

@jordanschalm
Copy link
Copy Markdown
Member

@jordanschalm jordanschalm commented Mar 17, 2026

This PR refactors several functions and types related to health calculations, with the goal of reducing complexity and duplication.

  • Adds a new Balance type, representing a signed numeric value
    • Some health calculations now operate on Balance type and handle both deposits and withdrawals.
    • InternalBalance now has a Balance-typed internal field. This field was changed to be access(self) (previously was globally mutable).
  • Modifies BalanceSheet:
    • HealthStatement is a summary of effective debt/collateral totals (equivalent to BalanceSheet structure before)
    • BalanceSheet now includes a per-token accounting of effective debts and collaterals. This makes some health-related calculations much simpler, because we can isolate the effect of one token.
  • Refactors all long functions in FlowHealth, and some in FlowALPv0, to use subfunctions and shared logic

jordanschalm and others added 8 commits March 16, 2026 10:19
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ributions

- Add HealthStatement struct for aggregate-only health summaries
- Refactor BalanceSheet to store per-token effective collateral/debt maps
- Add withUpdatedContributions method for immutable per-token updates
- Fix sumUFix128 bug (missing .values on effectiveDebt map)
- Update _getUpdatedBalanceSheet to build per-token maps
- Update FlowALPHealth adjustment functions to use withUpdatedContributions
- Bundle effectiveCollateral/effectiveDebt params into HealthStatement
  for computeRequiredDepositForHealth and computeAvailableWithdrawal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
… pattern

Replace delta-based switch/case logic with a cleaner approach:
1. Compute post-withdrawal true balance via trueBalanceAfterWithdrawal helper
2. Compute new effective contribution from the resulting balance
3. Use withUpdatedContributions to build the new BalanceSheet

The wrapper now constructs a TokenSnapshot instead of passing
individual price/factor/index parameters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ttern

Symmetric to the withdrawal refactor. Adds trueBalanceAfterDeposit helper.
The wrapper now constructs a TokenSnapshot instead of passing
individual price/factor/index parameters.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…elpers

These ~40-line functions duplicated the adjustment logic. Now they
delegate to computeAdjustedBalancesAfter{Deposit,Withdrawal} and
return adjusted.health.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…eAvailableWithdrawal

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…ose it

Renames the SignedQuantity struct to Balance (more natural name) and
eliminates the redundant direction field from InternalBalance by making
its scaledBalance field a Balance (direction + quantity) instead of a
bare UFix128. The InternalBalance init signature is preserved to
minimize churn at construction sites.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
jordanschalm and others added 9 commits March 17, 2026 11:16
…Balance

Consolidate the repeated switch-on-direction pattern in FlowALPHealth into
two new methods that work with Balance directly, replacing the old
withUpdatedContributions which took separate optional collateral/debt values.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
…terDelta

Consolidate the two mirror-image functions into a single trueBalanceAfterDelta
that accepts a Balance (direction + quantity) as the delta argument. Deposits
pass a Credit delta, withdrawals pass a Debit delta.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
The balance field was globally mutable -- this makes it only mutable
through type-defined functions.
…trueBalance helper

Add TokenSnapshot.trueBalance helper to deduplicate scaled→true balance conversion.
Refactor computeRequiredDepositForHealth and computeAvailableWithdrawal from 8 params
to 4 by accepting TokenSnapshot instead of individual scalars. Remove debug logging,
rename variables to generic names, and rename balanceSheet→initialBalanceSheet.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
let snap = view.snapshots[tokenType]!

switch balance.direction {
switch balance.getScaledBalance().direction {
Copy link
Copy Markdown
Collaborator

@m-Peter m-Peter Mar 26, 2026

Choose a reason for hiding this comment

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

It seems that a few lines above, we already calculated the effectiveCollateralTotal & effectiveDebtTotal, as part of:

let preHealth = FlowALPModels.healthFactor(view: view)

And FlowALPModels.healthFactor, seems to be doing the exact same calculation here.
Not directly related to the changes, but I thought of bringing this up, as a potential code reduction to include as well.

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

I agree. There is a secondary implementation of the health logic, on PositionView, which can execute in the view read-only context. I'll take a look to see if healthFactor can easily return a BalanceSheet -- then we'd still have the duplicated balance sheet construction logic in the codebase, but not the duplicated work in this function, at least.

Conceptually, it would be ideal if there was one balance sheet construction object that was oblivious to whether you gave it a PositionView or an InteralPosition, as long as they can both provide the necessary data (likewise with TokenSnapshot vs TokenState).

let price = UFix128(self.config.getPriceOracle().price(ofToken: type)!)

switch balance.direction {
switch balance.getScaledBalance().direction {
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

It seems that a bit of indirection was added, to be able to access the direction & quantity properties of the Balance struct. Maybe we could have dedicated getters for those 2, in the InternalBalance type?

let convertedCollateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type))
effectiveCollateral = effectiveCollateral + (value * convertedCollateralFactor)
let collateralFactor = UFix128(self.config.getCollateralFactor(tokenType: type))
effectiveCollateralByToken[type] = (price * trueBalance) * collateralFactor
Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

The parentheses here shouldn't be necessary, unless they convey more meaning when grouping the individual terms.

/// Returns the true balance of the given token in this position, accounting for interest.
/// Returns balance 0.0 if the position has no balance stored for the given token.
access(all) view fun trueBalance(ofToken: Type): UFix128 {
access(all) fun trueBalance(ofToken: Type): UFix128 {
Copy link
Copy Markdown
Collaborator

@m-Peter m-Peter Mar 26, 2026

Choose a reason for hiding this comment

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

Was there any particular reason to remove the view annotation?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Because it calls trueBalance, which has a compiler error when I make it view. The compiler error was on the line where Balance instance is constructed. I don't really understand why that line violates view annotation rules, but I didn't look into it very deeply.

initial: FlowALPModels.Balance,
target: FlowALPModels.Balance
): UFix128 {
let Credit = FlowALPModels.BalanceDirection.Credit
Copy link
Copy Markdown
Collaborator

@m-Peter m-Peter Mar 26, 2026

Choose a reason for hiding this comment

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

I could be wrong here, but I think we can compute this by re-using minDepositForTargetBalance, e.g.:

return self.minDepositForTargetBalance(initial: target, target: initial)

We are saving only a few lines of code, not sure if it's worth the extra mental effort to understand it, but I feel like this invariant should hold. Thoughts?

Co-authored-by: Ardit Marku <markoupetr@gmail.com>
Co-authored-by: Jordan Schalm <jordan.schalm@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants