Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions packages/trading-strategies/src/backtest/BacktestConfig.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,11 @@ export interface BacktestConfig {
strategy: TradingSessionStrategy;
/** The trading pair, e.g. BTC/USD or TSLA/USD. */
tradingPair: TradingPair;
/**
* History handed to `strategy.init()` via `getRecentCandles`, mirroring the warm-up a live
* session gets. Must contain only candles from *before* the backtest window — the executor
* deliberately never exposes the backtest candles themselves to `init`, otherwise a strategy
* could peek at the future it is about to be tested on (lookahead bias).
*/
warmupCandles?: Candle[];
}
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ import Big from 'big.js';
import {describe, expect, it} from 'vitest';
import {AlpacaBrokerMock, OrderSide, OrderType} from '@typedtrader/exchange';
import {AllAvailableAmount} from '../trader/index.js';
import type {Candle, TradingRules} from '@typedtrader/exchange';
import type {Candle, MarketDataSource, TradingRules} from '@typedtrader/exchange';
import type {OrderAdvice, TradingSessionState} from '../trader/index.js';
import {TradingPair} from '@typedtrader/exchange';
import {BacktestExecutor} from './BacktestExecutor.js';
Expand Down Expand Up @@ -891,4 +891,56 @@ describe('BacktestExecutor', () => {
expect(result.trades).toHaveLength(0);
});
});

describe('strategy init', () => {
/** Records what its `init` hook can see, mirroring a strategy that warms up from history. */
class InitProbeStrategy extends Strategy {
static override NAME = 'InitProbe';
initializedBeforeFirstCandle: boolean | undefined = undefined;
seenWarmupCandles: Candle[] = [];
#candlesProcessed = 0;

async init(market: Pick<MarketDataSource, 'getRecentCandles'>, pair: TradingPair): Promise<void> {
this.initializedBeforeFirstCandle = this.#candlesProcessed === 0;
this.seenWarmupCandles = await market.getRecentCandles(pair, 10, 60_000);
}

protected override async processCandle(
_candle: OneMinuteBatchedCandle,
_state: TradingSessionState
): Promise<OrderAdvice | void> {
this.#candlesProcessed += 1;
}
}

const runCandles = [
createCandle({close: '105', open: '100', openTimeInISO: '2025-01-01T00:10:00.000Z'}),
createCandle({close: '110', open: '105', openTimeInISO: '2025-01-01T00:11:00.000Z'}),
];

it('calls init before the first candle, mirroring a live TradingSession', async () => {
const strategy = new InitProbeStrategy();
await new BacktestExecutor({broker: createMockExchange(), candles: runCandles, strategy, tradingPair}).execute();
expect(strategy.initializedBeforeFirstCandle).toBe(true);
});

it('feeds init from warmupCandles only — never the candles under test (lookahead guard)', async () => {
const warmup = [createCandle({close: '95', open: '90', openTimeInISO: '2025-01-01T00:00:00.000Z'})];
const strategy = new InitProbeStrategy();
await new BacktestExecutor({
broker: createMockExchange(),
candles: runCandles,
strategy,
tradingPair,
warmupCandles: warmup,
}).execute();
expect(strategy.seenWarmupCandles).toEqual(warmup);
});
Comment on lines +927 to +938

it('gives init an empty history when no warmup candles are configured', async () => {
const strategy = new InitProbeStrategy();
await new BacktestExecutor({broker: createMockExchange(), candles: runCandles, strategy, tradingPair}).execute();
expect(strategy.seenWarmupCandles).toEqual([]);
});
});
});
11 changes: 11 additions & 0 deletions packages/trading-strategies/src/backtest/BacktestExecutor.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,17 @@ export class BacktestExecutor {
// The exact same advice→order translation a live TradingSession uses
const adviceExecutor = new AdviceExecutor({broker: exchange, feeRates, pair: tradingPair, tradingRules});

/*
* Mirror TradingSession.start(), which inits the strategy before the first candle. The
* market view is fed exclusively from `warmupCandles` (never the exchange), so init can
* only ever see history from before the backtest window — not the candles under test.
*/
const warmupCandles = this.#config.warmupCandles ?? [];
await strategy.init?.(
{getRecentCandles: async (_pair, count) => (count > 0 ? warmupCandles.slice(-count) : [])},
tradingPair
);

const trades: BacktestTrade[] = [];
const skippedAdvices: BacktestSkippedAdvice[] = [];
let totalFees = new Big(0);
Expand Down