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

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 | FWC26

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions