Skip to content

Adaptive Volatility-Aware Recurring Deposit (Smart DCA) Engine #311

Description

@robertocarlous

Problem Statement

The recurring-deposit scheduler (src/jobs/recurringDeposits.ts) fires a fixed amount on a fixed calendar cadence. That is a serviceable DCA baseline, but it ignores everything the platform already knows: the user's portfolio value, current volatility, APY dispersion across protocols, and whether the market is in a drawdown. The result is that contributions land at the worst times (buying at peaks into a single protocol), and users have no visibility into what the plan will actually do before committing to it. This issue upgrades DCA from "cron that moves money" into an adaptive, portfolio-aware investment engine with transparent preview and honest failure semantics.

Current State

  • RecurringDepositPlan (prisma/schema.prisma): amount, assetSymbol, cadence (WEEKLY/BIWEEKLY/MONTHLY), nextRunAt, status (ACTIVE/PAUSED/CANCELLED), lastRunStatus. No volatility knobs, no allocation rules, no plan history, no pause-on-drawdown.
  • src/jobs/recurringDeposits.ts sweeps due plans and deposits a fixed amount; src/routes/recurring-deposits.ts + src/validators/recurring-deposit-validators.ts expose create/update/cancel with fixed-amount semantics only.
  • The agent already has the pieces this needs: ProtocolRate history (for volatility), src/agent/riskScoring.ts (for per-protocol risk), src/jobs/alertRules.ts's valueByInstant aggregation (for drawdown detection), and the deposit path that turns a contribution into a real on-chain deposit.

Proposed Solution

1. Volatility-aware contribution sizing (src/jobs/recurringDeposits.ts)

  • A new contribution policy on the plan: FIXED (current behavior, default, backward compatible) or ADAPTIVE.
  • For ADAPTIVE plans, the per-run amount is scaled from a baseline by a documented volatility regime computed from recent ProtocolRate history and/or the user's own portfolio series:
    • Regime scaling (e.g. more units when the asset is cheap relative to its trailing range; fewer when it is expensive) — mean-reversion-inspired, but the policy must be explicitly stated, unit-tested, and bounded (configurable floor/ceiling as fractions of baseline so no run can exceed a user-approved range).
    • Pause-on-drawdown: if the user's portfolio is in a drawdown beyond a configurable threshold (reusing the POSITION_DRAWDOWN peak-tracking approach from src/jobs/alertRules.ts), the run is skipped and rescheduled (documented) rather than executed — or optionally doubled (documented, user-configurable) — never silently dropped.
  • Every run, adaptive or fixed, records a run ledger row (new RecurringDepositRun model): baseline amount, applied amount, regime snapshot (metrics used), result, and the reasoning for any deviation — so a user can see exactly what the engine did and why. AgentLog stays the audit trail for the deposit itself; the run ledger is the policy audit.

2. Multi-protocol allocation

  • Extend plans with an optional allocation map ({ protocol: weight% }) validated against the same rule set as User.strategyConfig.targetAllocations (weights sum to 100, protocols in the supported set). A single-protocol plan is just a single-entry map — no schema break.
  • Each run splits the (possibly scaled) contribution across allocations, and each leg is a separately tracked deposit. All-or-nothing semantics must be explicit: define whether a failed leg rolls the whole run back (safe default) or partial-executes with a PARTIAL run status that is visibly retried.
  • Interaction with the agent: a contribution that lands while a SavingsGoal is active must respect the goal's riskCeiling when allocating across protocols (the allocation map must pass the same applyRiskCeiling / fail-closed treatment as the strategy engine in src/agent/strategies.ts).

3. Plan preview & simulation

  • GET /api/v1/deposit/recurring/preview — a deterministic simulation of the next N runs under the plan's policy: projected contributions, regime assumptions, projected portfolio trajectory (using current ProtocolRate data and the same non-compounding APY convention as calculateApy so numbers are consistent with the rest of the product). Explicitly labeled simulation, not a guarantee.
  • The preview must render the policy's inputs (regime math, drawdown state, allocation math) so the user can audit the adaptive logic before it touches money.

4. Scheduler hardening

  • Catch-up policy (documented, configurable): a plan whose run was skipped (drawdown pause, provider outage, insufficient balance) — retry next window vs. skip-and-continue vs. accumulate. This must be an explicit, tested state machine, not an emergent bug.
  • Cancellation semantics: cancelling a plan mid-window must not race an in-flight deposit (idempotency: a run already recorded must not double-execute; the deposit path already has idempotency anchors — reuse them, don't invent new ones).
  • Backoff + alerting: repeated failures must emit an operational alert (deduplicated, via alertingService) and eventually auto-pause the plan with a user-visible reason, mirroring the retriable-failure philosophy of src/jobs/referralPayout.ts and src/jobs/fiatReconciliation.ts.

5. API surface (src/routes/recurring-deposits.ts, src/validators/recurring-deposit-validators.ts, docs/openapi.yaml)

  • Extend create/update with policy: FIXED | ADAPTIVE, optional allocation map, regime bounds, pause-on-drawdown threshold, and catch-up mode. All new fields validated with the existing zod-validator style and rejected with 400 + named reason when inconsistent (e.g. adaptive policy without bounds).
  • GET /api/v1/deposit/recurring/:id/runs — run ledger, paginated.
  • GET /api/v1/deposit/recurring/preview — as above.

Edge Cases & Failure Modes

  • Balance insufficient at run time: skip vs. partial vs. fail — explicit, tested, and reflected in lastRunStatus + run ledger; no silent gaps.
  • Protocol delisted / risk score collapses mid-plan: the allocation map fails closed (no contribution to a delisted/unrated protocol), the leg is retried or re-allocated per the documented policy, and the user is notified.
  • Drawdown threshold crossed exactly at run time: deterministic ordering — drawdown state is sampled at run start, not mid-run.
  • Catch-up accumulation exploding: a paused plan with accumulated runs must have a documented cap (e.g. max N accumulated) to avoid a single monster deposit.
  • Adaptive policy with zero history: must fall back to FIXED baseline with a visible flag, never guess from no data.
  • Clock/DB timezone drift: nextRunAt scheduling stays UTC-consistent with the existing job; DST-aware cadences documented.

Security & Privacy Considerations

  • The run ledger and previews are owner-scoped (enforceUserAccess), including parent/child sub-account semantics (actingAsUserId) consistent with src/routes/sub-accounts.ts.
  • Adaptive policy math must not infer/leak data about other users; volatility is computed from public ProtocolRate and the user's own series only.
  • No new secrets: the engine reuses the existing deposit/agent-signed path; the job must never touch WALLET_ENCRYPTION_KEY or user key material.

Out of Scope

  • Recurring withdrawals (a scheduler for selling is a separate feature).
  • ML price forecasting — the adaptive policy is rules-based, documented, and auditable.
  • Changing the underlying deposit mechanism (smart-contract interaction stays as-is).

Suggested Implementation Plan

  1. RecurringDepositPlan schema extensions + RecurringDepositRun model + migration.
  2. Pure policy module (src/jobs/ or src/agent/) for regime scaling, drawdown state, and allocation — unit-tested with fixture rate/value series.
  3. Scheduler rework: run ledger, all-or-nothing legs, catch-up state machine, backoff/auto-pause.
  4. Preview simulation endpoint.
  5. Validators, routes, docs/openapi.yaml, integration tests (including a full run cycle with a real deposit path mocked).

Acceptance Criteria

  • FIXED plans behave byte-for-byte like today (backward-compat regression tests)
  • ADAPTIVE policy scales contributions by a documented, bounded, unit-tested regime model; a no-history plan falls back to baseline with a visible flag
  • Pause-on-drawdown skips-and-reschedules (or doubles, per config) — never silently drops — with a documented state machine
  • Multi-protocol allocation validated and fail-closed on delisted/high-risk protocols, respecting active SavingsGoal riskCeiling
  • Per-run ledger records baseline vs. applied amount, regime snapshot, and deviation reasoning
  • Preview endpoint simulates N future runs deterministically and labels itself as simulation
  • Catch-up is capped, documented, and tested; repeated failures auto-pause with alert + user-visible reason
  • docs/openapi.yaml updated; unit + integration tests green

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26Third CampaignCampaign: Third CampaignenhancementNew feature or request

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions