Skip to content

Strategy Backtesting & Simulation Sandbox #283

Description

@robertocarlous

Problem Statement

A prospective or existing user has no way to see how a given strategy would have performed before committing real funds to it. They can only find out by living through it. A backtesting sandbox replays a strategy against real historical data and returns a simulated performance curve, so a user can compare "target allocation" against "max yield" against a goal-tracking strategy before choosing — and so contributors/reviewers can sanity-check a new strategy's behavior against history before it ever touches real money.

Current State

Strategies already exist as pluggable implementations of RebalanceStrategy in src/agent/strategies.ts (TargetAllocationStrategy, MaxYieldStrategy, and the goal-tracking one proposed in issue #281). Historical inputs already exist too: YieldSnapshot (per-position APY/yield over time, populated by src/agent/snapshotter.ts) and ProtocolRate (per-protocol APY history). What does not exist today is any performance-math beyond a simple, non-compounding APY calculation (calculateApy/calculateYearsActive in snapshotter.ts) — there is no Sharpe ratio or drawdown calculation anywhere in the codebase (confirmed by search). Portfolio analytics work in #225 is tracked separately and may add some of this math for live portfolios; this issue should implement whatever return/drawdown math it needs for backtesting directly, coordinate with #225 so the two don't diverge into two different definitions of "drawdown," and expose the calculation as a shared, reusable function either way.

Proposed Solution

API Surface

  • POST /api/agent/backtest — behind requireAuth (a backtest is compute-bound and should not be anonymous/public), Zod body: { strategy: StrategyName, startDate, endDate, startingAmount }. Returns a time series ({ date, simulatedValue }[]) plus summary stats (final value, max drawdown, realized simple APY, computed the same way as live reporting for comparability).

Integration Points

  • New src/agent/backtest.ts: a pure replay engine that takes a strategy instance and a historical ProtocolRate/YieldSnapshot window, and simulates period-over-period rebalancing decisions and resulting value — without touching Position, Transaction, CustodialWallet, or any Stellar RPC call. This must be enforced structurally (the backtest engine should not import src/stellar/* at all), not just by convention, so it's not possible for a future change to accidentally make a "simulation" submit a real transaction.
  • Reuse the exact same simple-rate APY convention from src/agent/snapshotter.ts for the summary stat, so a backtest's "realized APY" and a live position's "realized APY" mean the same thing to a user comparing them.
  • Cache results per (strategy, startDate, endDate, startingAmount) tuple — historical ProtocolRate/YieldSnapshot rows don't change once written, so repeated identical requests can be served from a cache rather than recomputing.
  • Backtesting a GoalTrackingStrategy (issue Goal-Based Investing ("Savings Goals") #281) specifically requires simulating its required-rate recalculation over time, not just a static allocation — this is the most complex strategy to backtest correctly and should be the last one implemented, after TargetAllocationStrategy and MaxYieldStrategy prove the engine out.

Edge Cases & Failure Modes

  • Requested date range predates available ProtocolRate/YieldSnapshot history — return a clear "insufficient historical data" response with the earliest available date, not a silently truncated or zero-filled result.
  • startingAmount of zero or negative — reject at validation.
  • A protocol included in the strategy's universe has gaps in its rate history (e.g. it didn't exist yet, or rate collection had a gap) — the engine must have a documented, explicit policy for gaps (e.g. hold previous value vs. exclude the protocol for that period) rather than an undefined/implicit behavior that silently skews results.
  • Extremely long date ranges (multi-year) — consider a max window or pagination on the returned time series to keep response size and compute bounded.

Security & Privacy Considerations

  • No real funds or on-chain state are touched — this is the primary safety property of this feature; it should be verified with an explicit test (see Acceptance Criteria) rather than assumed from code review alone.
  • Backtest results are computed from aggregate historical rate/yield data, not from other individual users' positions, so no cross-user data exposure risk here as currently scoped.

Out of Scope

  • Backtesting against synthetic/hypothetical future rate projections (Monte Carlo-style forward simulation) — historical replay only.
  • A UI/chart renderer — this issue is the API and engine only.

Suggested Implementation Plan

  1. Backtest engine core (src/agent/backtest.ts) with TargetAllocationStrategy and MaxYieldStrategy support, and the "no Stellar import" structural guarantee.
  2. POST /api/agent/backtest endpoint with validation, caching, and gap-handling policy.
  3. Extend to GoalTrackingStrategy once Goal-Based Investing ("Savings Goals") #281 lands.
  4. Fixture-based unit tests with known expected output.

Acceptance Criteria

  • Backtest engine runs entirely against historical data; a test asserts src/agent/backtest.ts has zero imports from src/stellar/
  • Endpoint returns a time series plus summary stats (final value, max drawdown, realized APY) using the same APY convention as live reporting
  • TargetAllocationStrategy and MaxYieldStrategy are both backtestable; documented gap-handling policy for missing rate history
  • Explicit "insufficient historical data" response for out-of-range requests
  • Unit tests (tests/unit/agent/backtest.test.ts) with a fixed synthetic YieldSnapshot/ProtocolRate fixture verifying known expected output
  • Result caching verified by a test asserting a second identical request doesn't recompute
  • docs/openapi.yaml updated

Metadata

Metadata

Assignees

Labels

GrantFox OSSIssue tracked in GrantFox OSSMaybe RewardedIssue may be eligible for a GrantFox rewardOfficial Campaign | FWC26Campaign: Official Campaign | FWC26

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions