You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
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.
A UI/chart renderer — this issue is the API and engine only.
Suggested Implementation Plan
Backtest engine core (src/agent/backtest.ts) with TargetAllocationStrategy and MaxYieldStrategy support, and the "no Stellar import" structural guarantee.
POST /api/agent/backtest endpoint with validation, caching, and gap-handling policy.
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
RebalanceStrategyinsrc/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 bysrc/agent/snapshotter.ts) andProtocolRate(per-protocol APY history). What does not exist today is any performance-math beyond a simple, non-compounding APY calculation (calculateApy/calculateYearsActiveinsnapshotter.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— behindrequireAuth(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
src/agent/backtest.ts: a pure replay engine that takes a strategy instance and a historicalProtocolRate/YieldSnapshotwindow, and simulates period-over-period rebalancing decisions and resulting value — without touchingPosition,Transaction,CustodialWallet, or any Stellar RPC call. This must be enforced structurally (the backtest engine should not importsrc/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.src/agent/snapshotter.tsfor 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.(strategy, startDate, endDate, startingAmount)tuple — historicalProtocolRate/YieldSnapshotrows don't change once written, so repeated identical requests can be served from a cache rather than recomputing.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, afterTargetAllocationStrategyandMaxYieldStrategyprove the engine out.Edge Cases & Failure Modes
ProtocolRate/YieldSnapshothistory — return a clear "insufficient historical data" response with the earliest available date, not a silently truncated or zero-filled result.startingAmountof zero or negative — reject at validation.Security & Privacy Considerations
Out of Scope
Suggested Implementation Plan
src/agent/backtest.ts) withTargetAllocationStrategyandMaxYieldStrategysupport, and the "no Stellar import" structural guarantee.POST /api/agent/backtestendpoint with validation, caching, and gap-handling policy.GoalTrackingStrategyonce Goal-Based Investing ("Savings Goals") #281 lands.Acceptance Criteria
src/agent/backtest.tshas zero imports fromsrc/stellar/TargetAllocationStrategyandMaxYieldStrategyare both backtestable; documented gap-handling policy for missing rate historytests/unit/agent/backtest.test.ts) with a fixed syntheticYieldSnapshot/ProtocolRatefixture verifying known expected outputdocs/openapi.yamlupdated