Purpose: ClientFrame implements timeframe generation for backtesting by creating arrays of timestamps spaced according to configured intervals. It transforms frame schemas (startDate, endDate, interval) into concrete arrays of Date objects that BacktestLogicPrivateService iterates through during historical simulation.
For information about the Frame schema configuration that ClientFrame consumes, see Frame Schemas. For backtest execution flow that uses these timeframes, see Backtest Execution Flow.
ClientFrame is a client implementation that generates timeframe arrays for backtest iteration. It bridges the gap between declarative frame configuration (IFrameSchema) and concrete timestamp arrays used during backtest execution.
Key Responsibilities:
Core Interface:
interface IFrame {
getTimeframe: (symbol: string, frameName: FrameName) => Promise<Date[]>;
}
ClientFrame is instantiated with IFrameParams which extends IFrameSchema with runtime dependencies.
| Field | Type | Description |
|---|---|---|
frameName |
FrameName (string) |
Unique identifier for this frame |
interval |
FrameInterval |
Timestamp spacing (1m, 3m, 5m, 15m, 30m, 1h, 2h, 4h, 6h, 8h, 12h, 1d, 3d) |
startDate |
Date |
Beginning of backtest period (inclusive) |
endDate |
Date |
End of backtest period (inclusive) |
note? |
string |
Optional developer documentation |
callbacks? |
Partial<IFrameCallbacks> |
Optional lifecycle event handlers |
interface IFrameParams extends IFrameSchema {
logger: ILogger;
}
Adds LoggerService instance for internal debug logging during timeframe generation.
interface IFrameCallbacks {
onTimeframe: (
timeframe: Date[],
startDate: Date,
endDate: Date,
interval: FrameInterval
) => void;
}
The onTimeframe callback fires after timeframe array generation, providing the complete array and configuration for validation or logging.
Interval Millisecond Conversion:
| Interval | Milliseconds | Usage |
|---|---|---|
| 1m | 60,000 | High-frequency tick generation |
| 3m | 180,000 | Short-term pattern analysis |
| 5m | 300,000 | Common scalping timeframe |
| 15m | 900,000 | Intraday trading strategies |
| 30m | 1,800,000 | Short-term swing trading |
| 1h | 3,600,000 | Standard hourly analysis |
| 2h | 7,200,000 | Extended intraday periods |
| 4h | 14,400,000 | Multi-hour trend analysis |
| 6h | 21,600,000 | Quarter-day intervals |
| 8h | 28,800,000 | Third-of-day intervals |
| 12h | 43,200,000 | Half-day intervals |
| 1d | 86,400,000 | Daily candle backtests |
| 3d | 259,200,000 | Multi-day pattern testing |
Architecture Layers:
FrameSchemaService and FrameValidationServiceFrameConnectionService provides memoized factory access to ClientFrame instancesClientFrame implements timeframe generation via getTimeframe() methodFrameCoreService contains interval conversion and date iteration algorithmsBacktestLogicPrivateService consumes generated timeframe arrays for iterationAlgorithm Steps:
startDate.getTime()current <= endDate.getTime():
new Date(current) to arraycurrent by intervalMsonTimeframe with generated array and configurationExample Output:
For startDate = 2024-01-01T00:00:00Z, endDate = 2024-01-01T02:00:00Z, interval = 1h:
[
new Date("2024-01-01T00:00:00.000Z"),
new Date("2024-01-01T01:00:00.000Z"),
new Date("2024-01-01T02:00:00.000Z")
]
// 3 timestamps covering 2-hour period at 1-hour intervals
Connection Service Responsibilities:
FrameConnectionService creates ClientFrame instances on demandframeName to prevent redundant instantiationIFrameSchema from FrameSchemaService during constructioninject() from DI containerMemoization Key: frameName (string)
Example:
// First call - creates new ClientFrame
const frame1 = frameConnectionService.getFrame("1d-backtest");
// Second call - returns cached instance
const frame2 = frameConnectionService.getFrame("1d-backtest");
// frame1 === frame2 (same instance)
Execution Flow:
Backtest.run(symbol, {strategyName, exchangeName, frameName})BacktestCommandService.run()BacktestLogicPrivateService.run() (async generator)ClientFrame instance via FrameConnectionService.getFrame(frameName)ClientFrame.getTimeframe() produces Date[] arrayDate in timeframeExecutionContextService.runInContext({symbol, when: date, backtest: true})StrategyCoreService.backtest() for current timestampyield each closed/cancelled signal to userTemporal Isolation: Each timestamp iteration updates ExecutionContextService.context.when, ensuring strategies cannot access future data during getSignal() calls.
import { addFrame } from "backtest-kit";
addFrame({
frameName: "1d-backtest",
interval: "1m", // 1-minute tick resolution
startDate: new Date("2024-01-01T00:00:00Z"),
endDate: new Date("2024-01-02T00:00:00Z"),
callbacks: {
onTimeframe: (timeframe, startDate, endDate, interval) => {
console.log(`Generated ${timeframe.length} timestamps`);
console.log(`Period: ${startDate} to ${endDate}`);
console.log(`Interval: ${interval}`);
}
}
});
Result: 1440 timestamps (24 hours × 60 minutes) spanning 2024-01-01 at 1-minute intervals.
import { Backtest } from "backtest-kit";
for await (const result of Backtest.run("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "1d-backtest" // Uses frame defined above
})) {
if (result.action === "closed") {
console.log("Signal closed at:", result.closeTimestamp);
console.log("PNL:", result.pnl.pnlPercentage);
}
}
Execution: BacktestLogicPrivateService iterates through all 1440 timestamps, invoking strategy logic at each tick.
const INTERVAL_MS: Record<FrameInterval, number> = {
"1m": 60_000,
"3m": 180_000,
"5m": 300_000,
"15m": 900_000,
"30m": 1_800_000,
"1h": 3_600_000,
"2h": 7_200_000,
"4h": 14_400_000,
"6h": 21_600_000,
"8h": 28_800_000,
"12h": 43_200_000,
"1d": 86_400_000,
"3d": 259_200_000
};
function convertInterval(interval: FrameInterval): number {
return INTERVAL_MS[interval];
}
function generateTimeframe(
startDate: Date,
endDate: Date,
intervalMs: number
): Date[] {
const timeframe: Date[] = [];
let current = startDate.getTime();
const end = endDate.getTime();
while (current <= end) {
timeframe.push(new Date(current));
current += intervalMs;
}
return timeframe;
}
Edge Cases:
startDate > endDate, returns [] (empty array)startDate === endDate, returns [startDate] (one element)endDate aligns with interval boundaryThe getTimeframe() method signature includes a symbol parameter:
getTimeframe(symbol: string, frameName: FrameName): Promise<Date[]>
Current Implementation: The symbol parameter is currently unused by ClientFrame but maintained for API consistency with other client interfaces (ClientStrategy, ClientExchange, ClientRisk).
Rationale for Inclusion:
Future Use Cases:
// Performed by FrameValidationService during addFrame()
- frameName must be unique (no duplicates)
- interval must be valid FrameInterval string
- startDate must be valid Date instance
- endDate must be valid Date instance
- callbacks must conform to Partial<IFrameCallbacks> if provided
// Performed by BacktestUtils before execution
- Frame must be registered via addFrame()
- FrameValidationService.validate(frameName, methodName) called
- Throws error if frameName not found
Example Error:
Backtest.run("BTCUSDT", {
strategyName: "my-strategy",
exchangeName: "binance",
frameName: "non-existent-frame" // Error: frame not found
});
// Error: Frame "non-existent-frame" not registered
| Component | Relationship | Description |
|---|---|---|
| FrameSchemaService | Storage | Stores registered IFrameSchema configurations via ToolRegistry |
| FrameValidationService | Validation | Validates frame registration and runtime lookups |
| FrameConnectionService | Factory | Creates and caches ClientFrame instances |
| FrameCoreService | Logic | Provides interval conversion and date iteration algorithms |
| BacktestLogicPrivateService | Consumer | Iterates through generated timeframe arrays |
| ExecutionContextService | Context | Updates context.when for each timestamp iteration |
| MethodContextService | Routing | Provides frameName for frame instance lookup |
(endDate - startDate) / intervalMs + 1 Date objectsOptimization: Date objects created once during getTimeframe() call, then reused throughout backtest iteration.
FrameConnectionService caches ClientFrame instances, preventing regenerationonTimeframe callback invocation per generation