> ## Documentation Index
> Fetch the complete documentation index at: https://pessoal-86816071.mintlify.site/llms.txt
> Use this file to discover all available pages before exploring further.

# Solver Deep Dive

> Technical documentation of the deterministic solver, including its scoring formula, allocation loop, reserve logic, and current limitations.

# Solver Deep Dive

The current optimizer is implemented in [solver.ts](/Users/giovanna-britto/Documents/PROJETOS/vault-solana/services/solver/src/solver.ts) as `DeterministicSolver`.

It is intentionally simple, deterministic, and auditable.

## Design choice

The solver is not using:

* machine learning
* reinforcement learning
* black-box optimization
* probabilistic routing

Instead, it uses a policy-constrained weighted allocation algorithm with explicit penalties.

## Inputs

The solver constructor receives:

* `strategies`
* `policy`
* `manager`
* `adapters`

Each plan build receives:

* `snapshots`
* `navUsd`
* `generatedAt`

## Output

The solver returns a `RebalancePlan` with:

* `reserveBps`
* `expectedBlendedApyBps`
* `riskBudgetUsageBps`
* `targetWeights`
* `strategyScores`
* `plannedSteps`
* `planHash`
* `targetWeightsHash`
* `txBundleHash`
* `noOp`

## Objective function

For each strategy, the solver computes:

```text theme={null}
score =
  expected_net_apy
  - risk_haircut
  - liquidity_haircut
  - fee_haircut
  - concentration_haircut
  - operational_haircut
```

This is a single-score ranking model, not a mean-variance optimizer.

## Mathematical notation

For documentation purposes, the current V0 solver can be written as:

```text theme={null}
For each strategy i:

S_i = E_i - R_i - L_i - F_i - C_i - O_i
```

Where:

* `S_i` is the final score of strategy `i`
* `E_i` is expected net APY
* `R_i` is risk haircut
* `L_i` is liquidity haircut
* `F_i` is fee haircut
* `C_i` is concentration haircut
* `O_i` is operational haircut

The allocation problem is then:

```text theme={null}
maximize     sum_i (w_i * S_i)

subject to:
  sum_i w_i <= 10_000 - reserveBps
  0 <= w_i <= strategyCap_i
  sum_{i in protocol p} w_i <= protocolCap_p
  sum_{i in exotic} w_i <= exoticCap
  sum_{i in marginfi} w_i <= marginFiCap
```

Important nuance:

* this is not solved with linear programming or quadratic optimization
* the current implementation approximates this objective with deterministic score-ranked proportional allocation under caps

## Exact components

### 1. Expected net APY

The solver delegates this to the adapter:

```ts theme={null}
const expectedNetApyBps = await adapter.getExpectedNetApy(snapshot);
```

For static-style adapters, this is effectively:

```text theme={null}
grossApyBps - feeBps - borrowCostBps - slippageBps
```

### 2. Risk haircut

```text theme={null}
riskHaircutBps = round(riskScoreBps * 0.35)
```

### 3. Liquidity haircut

Liquidity is penalized by profile plus withdrawal delay:

| Liquidity profile | Base penalty |
| ----------------- | ------------ |
| `instant`         | `25 bps`     |
| `same_day`        | `60 bps`     |
| `batched`         | `135 bps`    |
| `term`            | `220 bps`    |

Then:

```text theme={null}
liquidityHaircutBps = profilePenalty + round(withdrawalDelayHours / 2)
```

### 4. Fee haircut

```text theme={null}
feeHaircutBps = feeBps + borrowCostBps + slippageBps
```

### 5. Concentration haircut

```text theme={null}
concentrationHaircutBps = round(protocolConcentrationBps * 0.2)
```

This makes large existing concentrations progressively less attractive.

### 6. Operational haircut

Starts from:

```text theme={null}
operationalComplexityBps
```

Then adds:

| Condition             | Extra penalty |
| --------------------- | ------------- |
| canary sleeve         | `+120 bps`    |
| oracle unhealthy      | `+400 bps`    |
| protocol unhealthy    | `+600 bps`    |
| withdrawals unhealthy | `+300 bps`    |

## Reserve logic

The reserve is dynamic but simple.

The solver counts how many strategies have:

```text theme={null}
operationalHaircutBps >= 500
```

Then computes:

```text theme={null}
reserveBps = clamp(minLiquidReserveBps + unhealthyCount * 100, minLiquidReserveBps, 3000)
```

So every sufficiently unhealthy sleeve raises reserve by `100 bps`, capped at `30%`.

In notation:

```text theme={null}
reserveBps = min(
  3000,
  max(
    minLiquidReserveBps,
    minLiquidReserveBps + 100 * unhealthyCount
  )
)
```

## Candidate filter

A strategy is considered allocatable only if all of these are true:

* snapshot exists
* score exists
* strategy id is in `manager.allowed_strategies`
* strategy status is `active`
* `oracleHealthy === true`
* `protocolHealthy === true`
* `scoreBps > 0`

Important nuance:

* `withdrawalsHealthy` is currently a penalty, not a hard filter

That means the V0 solver may still allocate to a strategy with degraded withdrawal health if the final score stays positive.

## Allocation algorithm

After reserve is set, the solver allocates the remaining basis points in rounds.

### Step 1. Initialize remaining capacity

```text theme={null}
remainingBps = 10_000 - reserveBps
```

It also tracks:

* per-protocol usage
* exotic usage
* MarginFi usage

### Step 2. Sort candidates by score

Candidates are sorted descending by `scoreBps`.

### Step 3. Allocate proportionally by score

For each round:

```text theme={null}
theoretical = max(100, round((remainingBps * candidateScore) / totalScore))
```

That means the solver has a minimum target granularity of `100 bps` when a candidate is still eligible.

### Step 4. Respect caps

Actual allocation is clipped by:

* strategy cap
* protocol cap
* exotic cap
* MarginFi cap
* remaining unallocated bps

Formally:

```text theme={null}
availableCapacity = min(
  strategyCap - currentWeight,
  protocolCap - protocolUsed,
  specialCapRemaining,
  remainingBps
)

allocation = min(theoretical, availableCapacity)
```

In more compact notation:

```text theme={null}
w_i^(round) = min(
  max(100, round(remainingBps * S_i / sum_j S_j)),
  capStrategy_i - currentWeight_i,
  capProtocol_p - protocolUsage_p,
  capSpecial_i,
  remainingBps
)
```

### Step 5. Repeat until no progress

The loop stops when:

* `remainingBps == 0`, or
* no candidate can receive additional weight in the round

## Important behavior: residual cash can remain unallocated

The solver does **not** force all non-reserve capital into strategies.

If all strategy caps are reached before all `remainingBps` are consumed, the difference stays implicitly in cash.

That matters in the MVP because:

* reserve is explicit
* strategy weights are explicit
* residual idle capital is implicit

This creates safety, but it can also create cash drag.

In portfolio notation:

```text theme={null}
cashIdleBps = 10_000 - reserveBps - sum_i w_i
```

That idle capital is not a separate sleeve in the current target weights output, but financially it behaves like zero-yield or low-yield residual cash.

## Planned steps generation

After weights are computed, the solver asks each adapter to build execution steps:

```ts theme={null}
adapter.buildExecutionSteps(snapshot, targetNotionalUsd)
```

Then it:

* removes zero-notional steps except `hold`
* chunks oversized static steps with `chunkExecutionSteps`

## Blended metrics

After target weights are computed, the solver calculates portfolio-level summary metrics.

### Expected blended APY

```text theme={null}
expectedBlendedApyBps =
  sum_i (w_i * expectedNetApy_i) / sum_i w_i
```

In code, this is a weighted average over target weights.

### Risk budget usage

```text theme={null}
riskBudgetUsageBps =
  sum_i (w_i * riskScore_i) / sum_i w_i
```

This is not a VaR model or volatility estimator. It is a weighted average of the strategy risk scores already present in the snapshots.

## Rebalance trigger

The plan is marked `noOp` when the total intended change is too small:

```text theme={null}
totalDeltaBps = sum_i |targetWeight_i - currentWeight_i|

noOp = totalDeltaBps < minRebalanceDeltaBps
```

This avoids paying execution cost for immaterial reallocations.

## Hashes and execution integrity

The solver also emits deterministic hashes:

```text theme={null}
targetWeightsHash = H(target weights)
txBundleHash = H(planned steps)
planHash = H(plan summary)
```

These hashes matter because the executor and on-chain proof flow can reject a rebalance if the approved plan and the executed bundle diverge.

* preserves SDK or manifest bundles as single units when envelopes are already present

This last point is important:

* live protocol bundles are not arbitrarily chunked if `instructionEnvelopes` already exist

## Blended metrics

The plan includes:

### Expected blended APY

A weighted average of `expectedNetApyBps` using `targetWeightBps`.

### Risk budget usage

A weighted average of `riskScoreBps` using `targetWeightBps`.

## Rebalance gating

The solver computes:

```text theme={null}
totalDeltaBps = sum(abs(targetWeightBps - currentWeightBps))
```

Then:

```text theme={null}
noOp = totalDeltaBps < minRebalanceDeltaBps
```

So even a mathematically valid plan may be marked `noOp` if the net change is too small.

## Hashes and proofs

The solver creates:

* `targetWeightsHash`
* `txBundleHash`
* `planHash`

These are later used by Ranger and the Anchor program to bind execution to the approved plan.

## Current limitations

This V0 solver does not yet include:

* covariance between strategies
* explicit drawdown modeling
* dynamic transaction cost estimation from chain state
* probabilistic slippage distributions
* volatility targeting
* regime detection

## Why this is still a good V0

It is:

* explainable
* deterministic
* easy to audit
* easy to backtest
* easy to constrain by policy

That is exactly what you want in an MVP for treasury automation.
