Summary
Currently, watchdog thresholds (triggerHF, targetHF, minResultingHF, maxGasGwei) are static — set once via config and unchanged regardless of market conditions. A fixed triggerHF = 1.65 implicitly assumes market risk is constant. It isn't.
Goal: Add a regime-aware risk policy that modestly adjusts bot automation thresholds based on market volatility regime. Display zones for human monitoring stay fixed — the automation layer adapts in the background.
This is not "dynamic HF zones." It's a bounded regime multiplier on the rescue policy, preserving operator clarity while improving capital efficiency in calm markets and safety margins in stressed ones.
Why this makes sense
Liquidation risk is a function of: current HF, asset volatility, correlation structure, HF compression speed, rescue path reliability, and gas/chain conditions. A fixed threshold ignores all of these.
- In a calm, grinding market,
triggerHF = 1.65 may be overly conservative — leaving yield on the table
- In a violent, gap-prone market,
triggerHF = 1.65 may be too loose — not enough buffer
Since our rescue mechanism is collateral top-up (not flash-loan delever), this matters even more: top-up is less efficient per dollar, so in high-vol markets you need more buffer and more time to react.
Where this helps most
Capital efficiency during long benign periods. Right now the same safety margin is carried in sleepy markets, panic markets, trend crashes, and high-gamma chop. A regime multiplier lets you:
- Run tighter in calm conditions
- Widen safety margins when risk regime worsens
The main danger: false precision
Volatility is useful but is not the same as liquidation risk. You can have low realized vol then sudden gap risk, declining vol during complacency before a break, or cross-asset divergence that hurts HF more than the vol metric suggests.
So volatility is a small regime-adjustment input, not the boss. The system uses modest bounded multipliers, not continuous optimization.
Design: Bounded Regime Multiplier
Three regimes (not continuous)
| Regime |
Condition |
Multiplier Range |
Behavior |
| calm |
Annualized vol < calmThreshold (default 40%) |
0.97 - 0.99 |
Tighter thresholds, better capital efficiency |
| normal |
Between calm and stressed thresholds |
1.00 |
Base thresholds unchanged |
| stressed |
Annualized vol > stressedThreshold (default 80%) |
1.03 - 1.08 |
Wider margins, earlier triggers, looser gas caps |
Three discrete regimes are more robust than trying to continuously optimize. Operators can reason about them clearly.
What changes per regime
Example with base triggerHF = 1.70, targetHF = 1.95:
| Param |
Calm |
Normal |
Stressed |
| triggerHF |
1.65 (×0.97) |
1.70 |
1.78 (×1.05) |
| targetHF |
1.90 (×0.97) |
1.95 |
2.05 (×1.05) |
| minResultingHF |
1.80 (×0.97) |
1.85 |
1.94 (×1.05) |
| maxGasGwei |
50 (base) |
50 |
75 (×1.50) |
These are bounded — multipliers are capped and can never push thresholds below hard safety floors.
What stays fixed
Display zones stay fixed. Humans build intuition around stable ranges. Dynamic zones cause confusion, harder post-event analysis, and operator mistrust.
You still think in: safe / comfort / watch / alert / action / critical — with stable boundaries.
The automation layer (trigger, target, gas) adapts modestly underneath.
Regime transitions: slow and hysteretic
- Slow-moving vol measure: 24h rolling window, computed from 5-minute price samples
- Hysteresis band: 5% around regime thresholds to prevent flapping (e.g. must cross 40% to enter calm, must exceed 45% to exit)
- Minimum dwell time: 30 minutes in a regime before transitioning
- Rescue system should be boring: no rapid oscillation between regimes
Implementation Plan
Step 1: Core volatility computation
New file: packages/aave-core/src/volatility.ts
export type VolatilityRegime = 'calm' | 'normal' | 'stressed';
export type PriceSample = {
timestamp: number;
prices: Record<string, number>; // symbol -> USD
};
export type VolatilitySnapshot = {
regime: VolatilityRegime;
annualizedVol: number;
sampleCount: number;
windowMinutes: number;
updatedAt: number;
};
export type RegimeMultipliers = {
calm: number; // e.g. 0.97
normal: number; // always 1.0
stressed: number; // e.g. 1.05
};
export type RegimePolicyConfig = {
enabled: boolean;
// Vol measurement
windowMinutes: number; // default 1440 (24h)
minSamples: number; // default 12 (~1h before activation)
// Regime thresholds (annualized vol)
calmThreshold: number; // default 0.40
stressedThreshold: number; // default 0.80
hysteresisBand: number; // default 0.05
minDwellMs: number; // default 30 * 60 * 1000
// Bounded multipliers
hfMultipliers: RegimeMultipliers; // applied to triggerHF, targetHF, minResultingHF
gasMultipliers: RegimeMultipliers; // applied to maxGasGwei
};
Pure functions:
computeAnnualizedVolatility(samples, windowMinutes) — std dev of log returns, annualized
classifyRegime(vol, config, currentRegime) — with hysteresis
applyRegimeMultiplier(baseValue, regime, multipliers) — bounded application
clampThreshold(value, floor) — hard safety floor enforcement
Step 2: Server-side regime tracker
New file: packages/server/src/regimeTracker.ts
Stateful RegimeTracker class:
- Circular buffer of ~300 price samples (25h at 5min)
- Persists to
data/regime-history.json for crash recovery
- Tracks current regime with dwell time enforcement
- Exposes
getSnapshot(), getEffectiveWatchdogConfig(baseConfig)
Step 3: Config schema changes
packages/aave-core/src/constants.ts — add DEFAULT_REGIME_POLICY_CONFIG
packages/aave-core/src/types.ts — add RegimePolicyConfig type
packages/server/src/storage.ts — add regimePolicy to AlertConfig
packages/server/src/configSchema.ts — add Zod schema
Feature is off by default (enabled: false) — zero impact on existing behavior.
Step 4: Monitor integration
File: packages/server/src/monitor.ts
- Constructor takes optional
RegimeTracker
- In
checkWallet(), after fetchUsdPrices(), call tracker.recordPrices(prices)
- Zone hydration unchanged — display zones stay fixed
- Log current regime per poll cycle for observability
Step 5: Watchdog integration
File: packages/server/src/watchdog.ts
- Add
RegimeTracker reference
- New
getEffectiveConfig(): applies regime multipliers to triggerHF, targetHF, minResultingHF, maxGasGwei
- All other watchdog params unchanged
- Log effective thresholds when they differ from base
Step 6: API & observability
File: packages/server/src/index.ts
GET /api/volatility — current VolatilitySnapshot + effective watchdog thresholds
GET /api/status — include volatility field
GET/PUT /api/config — include regimePolicy
Step 7: Telegram
File: packages/server/src/statusMessage.ts
/status shows current regime and effective trigger/target
- Example:
Regime: calm | Trigger HF: 1.65 (base 1.70 × 0.97)
Files to create/modify
| File |
Action |
Description |
packages/aave-core/src/volatility.ts |
Create |
Vol computation, regime classification, multiplier application |
packages/aave-core/src/index.ts |
Modify |
Re-export volatility module |
packages/aave-core/src/constants.ts |
Modify |
Add DEFAULT_REGIME_POLICY_CONFIG |
packages/aave-core/src/types.ts |
Modify |
Add RegimePolicyConfig, VolatilitySnapshot types |
packages/server/src/regimeTracker.ts |
Create |
Stateful tracker with persistence |
packages/server/src/storage.ts |
Modify |
Add regimePolicy to config |
packages/server/src/configSchema.ts |
Modify |
Add Zod schema |
packages/server/src/monitor.ts |
Modify |
Wire in tracker, log regime |
packages/server/src/watchdog.ts |
Modify |
Apply regime multipliers to effective config |
packages/server/src/index.ts |
Modify |
Add /api/volatility endpoint |
packages/server/src/statusMessage.ts |
Modify |
Show regime in /status |
packages/server/test/volatility.test.ts |
Create |
Unit tests |
Key design principles
- Regime-aware policy, not dynamic zones — display zones are fixed for operator clarity; only bot thresholds adapt
- Bounded multipliers — small range (0.97-1.08), never below hard safety floors
- Three discrete regimes — calm/normal/stressed; more robust than continuous optimization
- Slow-moving and hysteretic — 24h vol window, 5% hysteresis band, 30min dwell time
- Boring rescue system — modest adaptation, clear bounds, human-readable behavior
- Opt-in —
enabled: false by default, zero impact on existing behavior
- Gas policy included — stressed regime loosens gas caps (strong candidate for regime-awareness)
Verification
- Unit tests: vol computation with known series, regime classification with hysteresis, multiplier bounds, safety floor clamping
- Integration: simulate price samples, verify regime transitions, verify effective config differs from base
- Manual:
yarn dev:server with regime policy enabled, check logs + Telegram /status
- Safety: confirm floors enforced, confirm behavior identical when disabled,
yarn test passes
Summary
Currently, watchdog thresholds (
triggerHF,targetHF,minResultingHF,maxGasGwei) are static — set once via config and unchanged regardless of market conditions. A fixedtriggerHF = 1.65implicitly assumes market risk is constant. It isn't.Goal: Add a regime-aware risk policy that modestly adjusts bot automation thresholds based on market volatility regime. Display zones for human monitoring stay fixed — the automation layer adapts in the background.
This is not "dynamic HF zones." It's a bounded regime multiplier on the rescue policy, preserving operator clarity while improving capital efficiency in calm markets and safety margins in stressed ones.
Why this makes sense
Liquidation risk is a function of: current HF, asset volatility, correlation structure, HF compression speed, rescue path reliability, and gas/chain conditions. A fixed threshold ignores all of these.
triggerHF = 1.65may be overly conservative — leaving yield on the tabletriggerHF = 1.65may be too loose — not enough bufferSince our rescue mechanism is collateral top-up (not flash-loan delever), this matters even more: top-up is less efficient per dollar, so in high-vol markets you need more buffer and more time to react.
Where this helps most
Capital efficiency during long benign periods. Right now the same safety margin is carried in sleepy markets, panic markets, trend crashes, and high-gamma chop. A regime multiplier lets you:
The main danger: false precision
Volatility is useful but is not the same as liquidation risk. You can have low realized vol then sudden gap risk, declining vol during complacency before a break, or cross-asset divergence that hurts HF more than the vol metric suggests.
So volatility is a small regime-adjustment input, not the boss. The system uses modest bounded multipliers, not continuous optimization.
Design: Bounded Regime Multiplier
Three regimes (not continuous)
calmThreshold(default 40%)stressedThreshold(default 80%)Three discrete regimes are more robust than trying to continuously optimize. Operators can reason about them clearly.
What changes per regime
Example with base
triggerHF = 1.70,targetHF = 1.95:These are bounded — multipliers are capped and can never push thresholds below hard safety floors.
What stays fixed
Display zones stay fixed. Humans build intuition around stable ranges. Dynamic zones cause confusion, harder post-event analysis, and operator mistrust.
You still think in: safe / comfort / watch / alert / action / critical — with stable boundaries.
The automation layer (trigger, target, gas) adapts modestly underneath.
Regime transitions: slow and hysteretic
Implementation Plan
Step 1: Core volatility computation
New file:
packages/aave-core/src/volatility.tsPure functions:
computeAnnualizedVolatility(samples, windowMinutes)— std dev of log returns, annualizedclassifyRegime(vol, config, currentRegime)— with hysteresisapplyRegimeMultiplier(baseValue, regime, multipliers)— bounded applicationclampThreshold(value, floor)— hard safety floor enforcementStep 2: Server-side regime tracker
New file:
packages/server/src/regimeTracker.tsStateful
RegimeTrackerclass:data/regime-history.jsonfor crash recoverygetSnapshot(),getEffectiveWatchdogConfig(baseConfig)Step 3: Config schema changes
packages/aave-core/src/constants.ts— addDEFAULT_REGIME_POLICY_CONFIGpackages/aave-core/src/types.ts— addRegimePolicyConfigtypepackages/server/src/storage.ts— addregimePolicytoAlertConfigpackages/server/src/configSchema.ts— add Zod schemaFeature is off by default (
enabled: false) — zero impact on existing behavior.Step 4: Monitor integration
File:
packages/server/src/monitor.tsRegimeTrackercheckWallet(), afterfetchUsdPrices(), calltracker.recordPrices(prices)Step 5: Watchdog integration
File:
packages/server/src/watchdog.tsRegimeTrackerreferencegetEffectiveConfig(): applies regime multipliers totriggerHF,targetHF,minResultingHF,maxGasGweiStep 6: API & observability
File:
packages/server/src/index.tsGET /api/volatility— currentVolatilitySnapshot+ effective watchdog thresholdsGET /api/status— includevolatilityfieldGET/PUT /api/config— includeregimePolicyStep 7: Telegram
File:
packages/server/src/statusMessage.ts/statusshows current regime and effective trigger/targetRegime: calm | Trigger HF: 1.65 (base 1.70 × 0.97)Files to create/modify
packages/aave-core/src/volatility.tspackages/aave-core/src/index.tspackages/aave-core/src/constants.tsDEFAULT_REGIME_POLICY_CONFIGpackages/aave-core/src/types.tsRegimePolicyConfig,VolatilitySnapshottypespackages/server/src/regimeTracker.tspackages/server/src/storage.tsregimePolicyto configpackages/server/src/configSchema.tspackages/server/src/monitor.tspackages/server/src/watchdog.tspackages/server/src/index.ts/api/volatilityendpointpackages/server/src/statusMessage.ts/statuspackages/server/test/volatility.test.tsKey design principles
enabled: falseby default, zero impact on existing behaviorVerification
yarn dev:serverwith regime policy enabled, check logs + Telegram/statusyarn testpasses